本次 lab 要给 xv6 实现网卡(Qemu 模拟 E1000 网卡)驱动。任务说明书里给了一大段说明以及 E1000 的操作手册。
实现原理
xv6 发送和接收网络包的流程
发送数据包:
- 用户程序调用 connect 系统调用创建 socket 并获取 socket 的文件描述符;
- 调用 write 系统调用往 socket 的文件描述符中写入数据;
- 进入内核,调用 filewrite 函数,由于要写入的文件类型是 FD_SOCK,所以调用 sockwrite 函数,创建一个 mbuf 并将数据从用户空间拷贝过来;
- 调用 net_tx_udp 函数进行 udp 头部封装;
- 进一步调用 net_tx_ip 函数进行 ip 头部封装;
- 调用 net_tx_eth 函数,并将 m 通过e1000_transmit 函数传递给网卡驱动;
- 由 e1000_transmit 函数将 mbuf 放到发送队列的尾部,等待网卡设备发送。
接受数据包:
- 网卡接收到新的数据包,产生中断,内核调用中断处理函数 e1000_intr;
- 调用 e1000_recv 函数,开始读取缓冲队列中的消息;
- 读到一条消息后,通过net_rx 函数向上层传递;
- 根据消息的类型,判断是调用 net_rx_ip 函数还是 net_rx_arp 函数;
- 如果是 ip 消息,还需要进一步调用net_rx_udp 函数进行拆解;
- 调用 sockrecvudp 函数,在其中找到对应的 socket,再通过 mbufq_pushtail 函数将消息放到队列中,待 sockread 函数读取;
以上就是 xv6 收发网络包的大体流程,具体可以自己阅读源码。
描述符和缓冲队列
驱动中有两种缓冲队列,发送和接收的队列,分别有两种描述符对应这两种队列。描述中中记录了对应缓存的存储地址、数据长度,还有一些标志位来让网卡和网卡驱动进行一些判断:
- E1000_TXD_STAT_DD 标志位就是让网卡驱动在发送数据时判断当前拿到的描述符对应的缓存是否已经发送了;
- E1000_RXD_STAT_DD 标志位就是在接受数据的时候判断是否是没有接收过的数据;
- E1000_TXD_CMD_RS 表示 Report Status,当这个字段被设置时,表示在数据包发送完成后,e1000 网卡会自动填充传输描述符中的报告状态区域,可以用来检查数据包是否发送成功。
- E1000_TXD_CMD_EOP 表示 End Of Packet,当这个字段被设置时,表示数据包已经到达了传输描述符中的缓冲区的末尾。当该字段被设置时,意味着这是一个完整的数据包,可以开始传输了。
上面的这几个 status 或 cmd 位是我们需要用到的。
环形缓冲队列的头尾
regs 数组中存储着 e1000 的寄存器的值,完成 lab 来说,需要使用到 regs[E1000_TDT],即下一个需要传输的环形缓冲队列的索引,还有 regs[E1000_RDT],即当前已经读到并且读过的环形缓冲队列索引。根据 Hints 来增加索引即可。
代码实现
首先实现 e1000_transmit。根据刚刚的调用流程分析,可能会有多个进程同时调用该函数,所以为了防止发生竞态,需要对函数上锁。接下来的步骤就是按照 Hints 来就行:
- 上锁;
- 获取 regs[E1000_TDT] 位置的描述符;
- 判断描述符 status 的 E1000_TXD_STAT_DD 是否被设置,没被设置说明之前数据还没发送,这个描述符对应的位置不能放入一个新的数据,返回 -1;
- 否则如果这个描述符对应的位置有数据则释放(调用 mbuffree);
- 重新设置描述符的 addr、length、cmd;
- 更新 regs[E1000_TDT] 为 (regs[E1000_TDT]+1)% TX_RING_SIZE;
- 将参数 m 放入缓冲区中;
- 释放锁,返回 0;
1 | int |
接着是 e1000_recv,注意它是不可以上锁的。第一是只有在处理中断的时候会调用该函数,不会发生竞态,第二是如果接收到的数据包是 ARP 数据包,那么在解包的时候就会调用 net_tx_arp 函数回复自己的 mac,会调用 e1000_transmit 再次获取锁,发生 panic。
根据 Hints:
- 获取下一个要读的描述符;
- 判断描述符的 E1000_RXD_STAT_DD 位是否被设置,没被设置就返回。注意这里要用一个循环来读取,直到不满足条件:
Your e1000_recv() code must scan the RX ring and deliver each new packet’s mbuf to the network stack (in net.c) by calling net_rx().
- 将描述符指向的数据通过 net_rx 传递给上层;
- 创建一个新的 mbuf 放入该位置中;
- 更新 regs[E1000_RDT] 为当前位置;
1 | static void |
运行结果
这次不用跑 usertests,很快就跑完了。
总结
这次实验给的材料太多了,还挺难看完的。建议选着看就行了,因为我感觉实际上只看 Hints 也能做个八九不离十。这次 lab 相对简单,有很多可以做的事情都在 Optional Challenges 中,感觉有时间可以做一做。
完结撒花
MIT 6.S081 Fall 2020 的 lab 算是全部做完了,如释重负,不过 Optional Challenges 是一个没做。后续计划再整体好好过一遍代码整理对应的知识。其实我感觉收益还是蛮大的,只要动手了,即使实现的版本是性能不高并且简单的,对相应知识的理解也能更加深刻,更别说后续再做 Optional Challenges。
并且我感觉这门课的 lab 比起 6.824 来说更加与课程内容强相关,上课没懂的地方看看代码或许就懂了,上手也会更加简单。
还剩了几堂 Lecture 没看完,希望五月份之前能搞定吧。