background
- 在开始写代码之前,回顾一下xv6book的第五章会有帮助
- 你将使用E1000去处理网络通信
- E1000会和qemu模拟的lan通信
- 在qemu模拟的lan中
- xv6的地址是10.0.2.15
- qemu模拟的计算机的地址是10.0.2.2
- qemu会将所有的网络包都记录在
packets.pcap
中 - 文件
kernel/e1000.c
包含了E1000的初始化代码,以及你需要补充的接收和发送的空函数 kernel/e1000_dev.h
包含了寄存器和标志位的定义kernel/net.c
和kernel/net.h
包含了一个简单的内核栈去实现IP,UDP,ARP协议。这些文件也包含了一个灵活的数据结构去持有packet,叫作mbuf
your job
- 完成
kernel/e1000.c
中的e1000_transmit()
和e1000_recv()
简单捋一下实验的思路
- 首先,我们要修改的是设备驱动,也就是内核层面的代码,它会和硬件设备协同完成数据包的发送和接受
- 发送数据包时
- 内核只需要将已经准备好的mbuf放到一个缓冲数组中,就完事了。这就是缓冲数组的优点,我往里面一扔就行了
- 网卡中应该也有固定的程序,它会自己讲缓冲数组的数据包给发送出去
- 我们只需要完成1中的任务,网卡那边不需要我们管
- 接受数据包时
- 当网卡接收了数据时,它会将它存入另一个缓冲数组,存好之后它们通过一个中断,告诉内核来收数据了
- 内核只需要将这个缓冲数组中已经到达的数据包传递给上层应用即可
以上就是基本的交互框架,但是因为设备驱动是内核,是纯软件,而网卡设备是硬件,所以双方的交互就有点麻烦。这里通过了一个很神奇的操作,就是寄存器映射,将硬件的寄存器给映射到了内核的地址空间中,我们访问内核的某个地址,就是在访问硬件的寄存器,这一下子就打通了内核和硬件之间的桥梁
在e1000_init
中,就将寄存器映射的起始地址赋值给了regs
,并且将各种信息和地址都存放到寄存器中,比如数组tx_ring
的地址就放到了regs[E1000_TDBAL] = (uint64)tx_ring;
至此,准备工作就做完了,我们现在就需要增加内核代码,使其能够和网卡配合,完成数据报的发送和接受
hints:e1000_transmit
这个lab很有意思,它的hints基本就是给了你所有的伪代码,你一个一个去实现就行了
- 首先,让我们通过
E1000_TDT
为索引去regs取出当前的index,其中regs就是一个uint
类型的数组的头指针 - 判断这个index指向的buf的状态,通过这个index去
tx_ring
中取出des,状态就存于des里的status中。这里要和一个宏E1000_TXD_STAT_DD相与进行判断 - 如果这个buf还存着之前的值,将它通过
mbuffree
给free掉 - 按提示修改des的各种参数,并且当前buf修改为传入的参数m即可。其中des的cmd参数没有给出提示,估计是想让我们自己查手册,我直接抄了大佬的E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS
- 最后,更新寄存器的值(空闲buf的指针,也就是第1步取出来东西的那个寄存器)
这里有个注意点就是,需要在函数首尾加锁。因为同一时刻,可能有多个进程想要通过网卡发送数据,这就形成了竞争的问题
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);
uint32 index = regs[E1000_TDT];
struct tx_desc *des = &tx_ring[index];
if (!(des->status & E1000_TXD_STAT_DD)) {
release(&e1000_lock);
return -1;
}
if (tx_mbufs[index]) {
mbuffree(tx_mbufs[index]);
}
des->addr = (uint64)m->head;
des->length = m->len;
des->cmd = E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS;
tx_mbufs[index] = m;
regs[E1000_TDT] = (regs[E1000_TDT] + 1) % TX_RING_SIZE;
release(&e1000_lock);
return 0;
}
hints:e1000_recv
- 首先通过寄存器中
E1000_RDT
的值+1对RX_RING_SIZE取模获取待接收数据的索引 - 判断这个索引指向的buf的状态是否是待接收
- 如果是待接收,修改m->len并且通过
net_rx
将这个buf传递给上层 - 通过mbufalloc在这个索引处再次新建一个buf,并且将这个buf的des的data指针指向这个buf的head,然后将状态设置为0
- 最后将这个索引的寄存器的值+1,
这里有两个注意点
- 不需要加锁,因为这里给出了提示。如果这个函数没有运行完,那么不会产生另一个中断
void e1000_intr(void) {
// tell the e1000 we've seen this interrupt;
// without this the e1000 won't raise any
// further interrupts.
regs[E1000_ICR] = 0xffffffff;
e1000_recv();
}
- 需要使用while循环,把能读的数据包都读出来。我猜是因为一次中断不一定代表只有一个数据包到了,甚至在处理中断的过程中,还会有数据包到。如果每次中断只读一个,会导致丢很多包