外观
net:从 DMA 描述符到用户收到 UDP 数据
版本:net,提交 982b43b57332a19e171d58534f13233be7009d1f。本章重点是驱动 I/O、内存所有权和唤醒流程;协议原理只保留理解本实验所需部分。2025 除 E1000 收发,还要求完成 UDP 接收队列。
设备与 CPU 如何交换工作
CPU 不逐字节把整包写进网卡寄存器,而是准备内存缓冲区和描述符。描述符保存缓冲区地址、长度与状态,形成环形队列;设备通过 DMA 读写这些缓冲区。控制寄存器中的 head/tail 与完成位表达双方交接进度。
flowchart LR A[用户 send] --> B[构建 UDP/IP/Ethernet] B --> C[TX 描述符交给设备] C --> D[QEMU E1000] D --> E[RX 描述符完成] E --> F[net_rx / ip_rx] F --> G[端口队列] G --> H[recv copyout 到用户]
查看流程图文本
flowchart LR A[用户 send] --> B[构建 UDP/IP/Ethernet] B --> C[TX 描述符交给设备] C --> D[QEMU E1000] D --> E[RX 描述符完成] E --> F[net_rx / ip_rx] F --> G[端口队列] G --> H[recv copyout 到用户]
所有权是驱动最重要的线索:CPU 填完并发布描述符后,不能立刻释放缓冲区,因为设备可能还没读完;收到数据并交给上层后,也不能把同一缓冲区立即交回设备覆盖。
TX 与 RX 的实现顺序
TX 从 E1000_TDT 对应槽开始,检查 DD 表示上一次发送完成。未完成则返回忙,而不是覆盖旧请求;已完成才回收旧缓冲区并写新描述符。设置长度及所需命令位后更新 tail,按照 TX_RING_SIZE 取模。
RX 从 (RDT+1)%RX_RING_SIZE 检查 DD。循环处理本次已经完成的所有槽,不要假定一次中断只有一包。交付给 net_rx 后,为描述符提供新的空缓冲区、清状态、更新 RDT。环大小只有有限项,必须测超过一圈的流量。
锁范围不能机械覆盖所有函数调用。net_rx 处理 ARP 时可能调用发送路径;如果收发共用同一锁且接收持锁调用上层,可能递归取得锁而死锁。应明确接收槽交接和协议处理之间的边界,保证释放锁后缓冲区仍归当前处理者拥有。
UDP 接收队列
kernel/net.c 的任务入口是 ip_rx()、sys_recv()、sys_bind()。bind 为端口建立接收状态;ip_rx 识别 UDP 和目标端口,将数据包按到达顺序排队;recv 若无数据则通过 sleep/wakeup 等待,有数据时取出并 copyout 源地址、源端口与 payload。
每个端口最多积压 16 包。队列满时丢弃新包并释放其资源,不能让一个端口耗尽内存,也不能因为该端口满就停止处理其他端口的包。未绑定端口同样丢弃。队列长度、链表指针与唤醒条件必须由一致的锁保护。
协议多字节字段是网络字节序,而 CPU 按小端解释内存,应在边界使用 ntohs/ntohl。UDP 的 ulen 包含 8 字节 UDP 头,payload 长度要扣除;不能把它当作整帧长度。即便实验不要求完整协议栈,也应先检查头部和长度是否落在当前收到的 buffer 内,避免越界复制。
按链路分层定位故障
先用 xv6 nettest txone 验证发送;再由宿主 python3 nettest.py rxone 验证接收。用 tcpdump -XXnr packets.pcap 看 QEMU 捕获的包:没有发包,检查 TX;有 ARP 请求无回复,检查 RX 与 ARP 到 TX 的交接;ARP 完整而 recv 不醒,检查 UDP 解析、端口字节序、队列与 sleep 协议。
最后在宿主启动 python3 nettest.py grade,xv6 运行 nettest grade,再 make grade。少量包成功、绕环后失败,重点看索引、完成位和旧 buffer 回收;重复测试后内存减少,重点看丢包路径是否 kfree。
自测:为什么一次 RX 中断后只处理一个包可能卡住?
设备可能合并通知,多个描述符已经完成但只触发一次可见中断。若只处理一个,其余包未必立即获得下一次通知。接收函数应排空本次可见的完成槽,直到遇到未完成槽。
来源:2025 net 官方任务;源码入口 e1000.c/e1000_dev.h/net.c/net.h/user/nettest.c。QEMU 的设备模型验证不等于真实网卡硬件验证。