0. 序
算是填了一个坑吧。
1 qemu参数的解释
ifeq ($(LAB),net)
QEMUOPTS += -netdev user,id=net0,hostfwd=udp::$(FWDPORT)-:2000 -object filter-dump,id=net0,netdev=net0,file=packets.pcap
QEMUOPTS += -device e1000,netdev=net0,bus=pcie.0
endif
查看qemu的手册,在设备模拟上,有一个Device Front End和Device Back End的概念,这里的-device
以及后面的参数指定了设备的前端,即操作系统可见的部分,这是一张e1000的网卡,然后连在pcie的第0根总线上。-netdev
则对应设备的后端,指出qemu应该如何模拟这个设备,这里包括了使用user模式,hostfwd
指定了qemu如何对host和guest的udp,tcp连接进行转发。-object
参数则要求qemu保存e1000网卡对应的网络流量,这些参数的含义都可以在qemu的invokation页面找到,然后nettests.c中连接对应的转发端口向主机的server.py发送消息,完成了guest和host的通信。至此,清楚了实验的搭建逻辑。
2 pci总线枚举
pci_init()
{
// we'll place the e1000 registers at this address.
// vm.c maps this range.
uint64 e1000_regs = 0x40000000L;
// qemu -machine virt puts PCIe config space here.
// vm.c maps this range.
uint32 *ecam = (uint32 *) 0x30000000L;
// look at each possible PCI device on bus 0.
for(int dev = 0; dev < 32; dev++){
int bus = 0;
int func = 0;
int offset = 0;
uint32 off = (bus << 16) | (dev << 11) | (func << 8) | (offset);
volatile uint32 *base = ecam + off;
uint32 id = base[0];
// 100e:8086 is an e1000
if(id == 0x100e8086){
// command and status register.
// bit 0 : I/O access enable
// bit 1 : memory access enable
// bit 2 : enable mastering
base[1] = 7;
__sync_synchronize();
for(int i = 0; i < 6; i++){
uint32 old = base[4+i];
// writing all 1's to the BAR causes it to be
// replaced with its size.
base[4+i] = 0xffffffff;
__sync_synchronize();
uint32 new = base[4 + i];
uint32 sz = (~(new & 0xfffffff0)) + 1;
printf("%d\n", sz); // 打印出pci设备需要的内存大小
base[4+i] = old;
}
// tell the e1000 to reveal its registers at
// physical address 0x40000000.
base[4+0] = e1000_regs;
e1000_init((uint32*)e1000_regs);
}
}
}
之前看xv6源码时很困惑的一个地方在于xv6中假定了所有设备的MMIO地址都是已知的(硬编码在内核中),但实际的操作系统为了应对多种的硬件,显然不能这样做。这里的pci设备枚举的代码清楚了操作系统如何找到pci设备以及对应的物理地址。
这里的代码在干嘛可以参考pci配置空间,我想因为qemu参数中显式地指定了e1000是连接在pcie.0上,所以这里没有对总线进行枚举,并不清楚func域在pcie中的含义。通过在BAR寄存器中写入e1000_regs
,系统就显式地指定了e1000网卡应该位于物理地址的哪一部分。
3 e1000网卡的初始化
这一段其实没什么好说的,参考e1000网卡手册的第14章就清楚了。特别说一下这个作为dma buffer的mbuf
结构。
struct mbuf {
struct mbuf *next; // the next mbuf in the chain
char *head; // the current start position of the buffer
unsigned int len; // the length of the buffer
char buf[MBUF_SIZE]; // the backing store
};
从手册的描述上看,一个buffer就是一个字符数组就行了,为什么这里还需要next, head, len
这些数据结构?其实这些数据结构并不是用在dma里面的,而是用于后面协议栈向上传递使用的。比如next指针,把mbuf
串为一个链表,作为udp套接字的buffer链。
4 lab实现
int e1000_transmit(struct mbuf *m)
{
//
// Your code here.
//
// the mbuf contains an ethernet frame; program it into
// the TX descriptor ring so that the e1000 sends it. Stash
// a pointer so that it can be freed after sending.
//
acquire(&e1000_lock);
int pkt_index = regs[E1000_TDT] % TX_RING_SIZE;
if ((tx_ring[(pkt_index + 1) % TX_RING_SIZE].status & E1000_TXD_STAT_DD) == 0)
{
release(&e1000_lock);
return -1;
}
for (int i = 0; i < TX_RING_SIZE; ++i)
{
if ((tx_ring[i].status & E1000_TXD_STAT_DD) == 1 && tx_mbufs[i])
{
mbuffree(tx_mbufs[i]);
tx_mbufs[i] = 0;
}
}
tx_mbufs[pkt_index] = mbufalloc(0);
memmove(tx_mbufs[pkt_index]->buf, m->head, m->len);
tx_ring[pkt_index].addr = (uint64)tx_mbufs[pkt_index]->head;
tx_ring[pkt_index].length = m->len;
tx_ring[pkt_index].cmd = E1000_TXD_CMD_RS | E1000_TXD_CMD_EOP | 2;
tx_ring[pkt_index].status = 0;
regs[E1000_TDT] = (regs[E1000_TDT] + 1) % TX_RING_SIZE;
release(&e1000_lock);
return 0;
}
static void
e1000_recv(void)
{
//
// Your code here.
//
// Check for packets that have arrived from the e1000
// Create and deliver an mbuf for each packet (using net_rx()).
//
//acquire(&e1000_lock);
while (1)
{
int pkt_index = (regs[E1000_RDT] + 1) % RX_RING_SIZE;
struct mbuf *m = 0;
if ((rx_ring[pkt_index].status & E1000_RXD_STAT_EOP) == 0)
return;
if (rx_ring[pkt_index].errors != 0)
goto done;
m = mbufalloc(0);
if (!m)
goto done;
m->len = rx_ring[pkt_index].length;
memmove(m->buf, rx_mbufs[pkt_index]->buf, rx_ring[pkt_index].length);
done:
rx_ring[pkt_index].errors = 0;
rx_ring[pkt_index].status = 0;
regs[E1000_RDT] = (regs[E1000_RDT] + 1) % RX_RING_SIZE;
//release(&e1000_lock);
if (m)
net_rx(m);
}
}
有几个细节需要注意
- 环形队列一个很不好的地方在于会使buffer长度减少1,这里实际上最多只能有15个pending的buffer
- 虽然在
e1000_init
函数中写延迟中断寄存器为0,并不代表真的每个packet都会触发一次中断,实际上一次中断还是可能带来多个packet。 - 在
recv
函数中没有加锁,这一点我不太确定。我觉得涉及到一个cpu affinity
问题,就是网卡收到包,向PLIC申请中断后,PLIC到底向哪个hart发起中断,我没有在PLIC-SPEC里面找到相应的描述。查看kernel/plic.c
可以看到
void
plicinithart(void)
{
int hart = cpuid();
// set uart's enable bit for this hart's S-mode.
*(uint32*)PLIC_SENABLE(hart)= (1 << UART0_IRQ) | (1 << VIRTIO0_IRQ);
#ifdef LAB_NET
// hack to get at next 32 IRQs for e1000
*(uint32*)(PLIC_SENABLE(hart)+4) = 0xffffffff;
#endif
// set this hart's S-mode priority threshold to 0.
*(uint32*)PLIC_SPRIORITY(hart) = 0;
}
而在main函数的初始化中,每个hart都会调用这个函数,即每个hart都开启了S-mode下的外部中断使能。因此这个地方留作一个困惑吧。