实验一:NIC
分层结构:
1 2 3
| 应用层 (nettest.c) ←→ 网络协议栈 (net.c) ←→ 设备驱动 (e1000.c) 用户程序 协议处理 硬件控制 ←→ 硬件 (E1000网卡) 物理设备
|
主要是 e1000_transmit() 和e1000_recv()
要理解 DMA(直接内存访问)与 描述符环 (Descriptor Ring),理解ring中head和tail怎么移动的
理解
1 2 3 4 5 6 7 8 9 10 11
| // 描述符结构定义(位于e1000_dev.h) struct tx_desc { uint64 addr; // 数据缓冲区地址 uint16 length; // 数据长度 uint8 cso; // 校验和偏移 uint8 cmd; // 命令字段 uint8 status; // 状态字段(重点!) uint8 css; // 校验和起始 uint16 special; }; rx_desc同理
|
下面开始写代码
1 2 3 4 5 6 7
| #define TX_RING_SIZE 16 static struct tx_desc tx_ring[TX_RING_SIZE] __attribute__((aligned(16))); static char *tx_bufs[TX_RING_SIZE]; // <--- 【新增】用于记录发送的 buffer 指针
#define RX_RING_SIZE 16 static struct rx_desc rx_ring[RX_RING_SIZE] __attribute__((aligned(16))); static char *rx_bufs[RX_RING_SIZE]; // <--- 【新增】用于记录接收的 buffer 指针
|
e1000_transmit思路是这样的:拿锁 -> 读 Tail -> 检查该位置是否可用(DD位) -> 回收旧内存 -> 放新内存地址进环 -> 设命令位 (EOP/RS) -> Tail 加 1-> 放锁。它的作用是将上层协议栈打包好的数据,通过 DMA(直接内存访问)机制交给网卡硬件发送出去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| int e1000_transmit(char *buf, int len) { acquire(&e1000_lock);
// 1. 获取发送环的下一个索引 (TDT,Transmit Descriptor Tail) uint32 idx = regs[E1000_TDT];
// 2. 检查环是否满了 // E1000_TXD_STAT_DD 未设置,说明硬件还没处理完这一圈在这个位置的包 if((tx_ring[idx].status & E1000_TXD_STAT_DD) == 0){ release(&e1000_lock); return -1; }
// 3. 释放旧的 buffer // 如果这个位置有上次发送遗留的 buffer,现在硬件已经处理完了,可以释放了 if(tx_bufs[idx]){ kfree(tx_bufs[idx]); tx_bufs[idx] = 0; }
// 4. 记录新的 buffer 指针 (以便将来释放) tx_bufs[idx] = buf;
// 5. 填充 DMA 描述符 tx_ring[idx].addr = (uint64)buf; tx_ring[idx].length = len; // CMD_EOP: 包结束, CMD_RS: 报告状态 (让硬件发完后设置 DD 位) tx_ring[idx].cmd = E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS;
// 6. 更新 TDT,通知硬件开始工作 regs[E1000_TDT] = (idx + 1) % TX_RING_SIZE;
release(&e1000_lock); return 0; }
|
e1000_recv思路是这样的:拿锁 -> 读 Tail (RDT,Receive Descriptor Tail) 的下一个位置 -> 检查是否有新包(DD位) -> 死循环处理直到没包*-> 取出包 -> 申请新包补位*-> 清理状态位 -> 更新 Tail (RDT) -> 放锁 -> 把包扔给上层。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| static void e1000_recv(void) { while(1){ acquire(&e1000_lock);
// 1. 计算要检查的索引 (RDT + 1) uint32 idx = (regs[E1000_RDT] + 1) % RX_RING_SIZE;
// 2. 检查是否有新包 (DD 标志) if((rx_ring[idx].status & E1000_RXD_STAT_DD) == 0){ release(&e1000_lock); return; }
// 3. 获取接收到的 buffer char *buf = rx_bufs[idx]; // 如果是刚启动,rx_bufs 里可能是空的, // 我们需要从 descriptor 里把 init 阶段分配的地址找回来 if(!buf){ buf = (char *)rx_ring[idx].addr; } int len = rx_ring[idx].length;
// 4. 分配新 buffer 替换旧的 char *new_buf = kalloc(); if(new_buf == 0){ // 内存不足,暂时放弃接收这个包 release(&e1000_lock); return; }
// 5. 更新描述符指向新 buffer rx_bufs[idx] = new_buf; rx_ring[idx].addr = (uint64)new_buf; rx_ring[idx].status = 0; // 清除状态位
// 6. 推进 RDT regs[E1000_RDT] = idx;
release(&e1000_lock);
// 7. 将旧 buffer (包含数据) 传给网络栈 net_rx(buf, len); } }
|
1 2 3 4 5 6 7 8 9
| void e1000_intr(void) { // 告诉 e1000 我们处理了中断 (写 ICR 寄存器清除中断) regs[E1000_ICR] = 0xffffffff;
// 处理接收 e1000_recv(); }
|
测试通过(这里只展示部分):
1 2 3
| huzayn@huzayn-VMware-Virtual-Platform:~/xv6/xv6-labs-2025$ python3 nettest.py txone tx: listening for a UDP packet txone: OK
|
实验2: UDP Receive
数据流是这样的:
1 2 3 4
| // 数据包的生命周期跨越两个实验 应用数据 → UDP头部 → IP头部 → 以太网帧 → 网卡驱动 ↑ ↑ UDP Receive实验 NIC实验
|
网络协议结构:
数据包的封装与解包:
- Ethernet Header (14字节):最外层。包含 MAC 地址。在
net_rx 中已被剥离或检查。
- IP Header (20字节):中间层。包含源 IP 和目标 IP。我们需要跳过它找到 UDP。
- UDP Header (8字节):内层。包含 源端口 (Sport) 和 目标端口 (Dport)。这是你用来区分要把数据给哪个进程的关键。
- Payload:最里面。用户真正想要的数据。
协议规定网络传输时使用 大端序 (Big-Endian),即高位字节在前。但是RISC-V (以及 x86) 使用 小端序 (Little-Endian),即低位字节在前。所以要使用ntohs()以及ntohl()来从包里读取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // 定义一个链表节点,用来串联接收到的 packet buffer struct rxbuf { struct rxbuf *next; char *data; // 指向 kalloc 分配的 page int len; // 数据包实际长度 };
// 定义 UDP Socket struct sock { struct sock *next; // 全局链表 uint16 port; // 绑定的端口 struct spinlock lock; struct rxbuf *rxq; // 接收队列头 (Receive Queue) };
// 全局变量:所有绑定的 sockets 链表 static struct sock *sockets;
|
sys_bind实现了Socket绑定功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| uint64 sys_bind(void) { int port; struct sock *s;
// 1. 获取参数 argint(0, &port);
s = (struct sock *)kalloc(); if(!s) return -1; memset(s, 0, PGSIZE); s->port = port; initlock(&s->lock, "sock"); s->rxq = 0;
acquire(&netlock); // 头插法插入全局链表 s->next = sockets; sockets = s; release(&netlock);
return 0; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| uint64 sys_recv(void) { int dport; uint64 src_ip_addr; uint64 src_port_addr; uint64 buf_addr; int maxlen; struct sock *s; struct rxbuf *node; int ret_len = -1;
// 1. 获取参数 argint(0, &dport); argaddr(1, &src_ip_addr); argaddr(2, &src_port_addr); argaddr(3, &buf_addr); argint(4, &maxlen);
// 2. 查找对应的 socket acquire(&netlock); for(s = sockets; s; s = s->next){ if(s->port == dport) break; } release(&netlock);
if(!s) return -1; // 端口未绑定
acquire(&s->lock);
// 3. 等待队列中有数据 while(s->rxq == 0){ if(myproc()->killed){ release(&s->lock); return -1; } sleep(s, &s->lock); }
// 4. 取出头部节点 node = s->rxq; s->rxq = node->next; release(&s->lock);
// 5. 解析数据 struct eth *eth = (struct eth *)node->data; struct ip *ip = (struct ip *)(eth + 1); int ip_hl = (ip->ip_vhl & 0x0F) * 4; struct udp *udp = (struct udp *)((char *)ip + ip_hl); char *payload = (char *)(udp + 1); int udp_len = ntohs(udp->ulen); int payload_len = udp_len - sizeof(struct udp); // 6. 拷贝信息给用户 struct proc *p = myproc(); uint32 src_ip = ntohl(ip->ip_src); uint16 sport = ntohs(udp->sport);
if(copyout(p->pagetable, src_ip_addr, (char*)&src_ip, sizeof(src_ip)) < 0 || copyout(p->pagetable, src_port_addr, (char*)&sport, sizeof(sport)) < 0){ goto bad; }
int copy_len = (payload_len > maxlen) ? maxlen : payload_len; if(copyout(p->pagetable, buf_addr, payload, copy_len) < 0){ goto bad; } ret_len = copy_len;
done: kfree(node->data); // 释放 buffer kfree((void*)node); // 释放 rxbuf 节点 return ret_len;
bad: ret_len = -1; goto done; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| void ip_rx(char *buf, int len) { // 声明局部变量 struct eth *eth = (struct eth *)buf; // 以太网头部指针(数据包起始位置) struct ip *ip = (struct ip *)(eth + 1); // IP头部指针(紧接以太网头之后) struct udp *udp; // UDP头部指针(稍后计算) uint16 dport; // 目标端口号(主机字节序) struct sock *s; // 指向找到的socket结构 struct rxbuf *node; // 新接收缓冲区节点 struct rxbuf *tmp; // 临时指针用于遍历队列
// 调试输出:只在第一次收到IP包时打印 static int seen_ip = 0; if(seen_ip == 0) printf("ip_rx: received an IP packet\n"); seen_ip = 1;
// 协议过滤:只处理UDP协议,其他协议直接丢弃 if(ip->ip_p != IPPROTO_UDP){ kfree(buf); // 释放数据包缓冲区 return; // 非UDP协议,直接返回 }
// 解析IP头部长度并计算UDP头部位置 int ip_hl = (ip->ip_vhl & 0x0F) * 4; // 提取IP头部长度(4字节为单位)并转换为字节数 udp = (struct udp *)((char *)ip + ip_hl); // UDP头部在IP头部之后(考虑可变长度选项) dport = ntohs(udp->dport); // 将目标端口从网络字节序转换为主机字节序
// 查找绑定到目标端口的socket(需要全局锁保护链表遍历) acquire(&netlock); // 获取全局网络锁,保护socket链表 for(s = sockets; s; s = s->next){ // 遍历全局socket链表 if(s->port == dport) // 检查端口是否匹配 break; // 找到匹配的socket,退出循环 } release(&netlock); // 释放全局锁(查找完成)
// 如果没有找到绑定该端口的socket,丢弃数据包 if(!s){ kfree(buf); // 释放数据包缓冲区 return; // 无对应socket,直接返回 }
// 获取该socket的锁,准备操作其接收队列 acquire(&s->lock);
// 检查当前接收队列的长度(流量控制) int qlen = 0; tmp = s->rxq; // 从队列头开始遍历 while(tmp){ qlen++; // 统计队列中数据包数量 tmp = tmp->next; // 移动到下一个节点 }
// 如果队列已满(>=16个包),丢弃新数据包(防止内存耗尽攻击) if(qlen >= 16){ release(&s->lock); // 释放socket锁 kfree(buf); // 释放数据包缓冲区 return; // 队列已满,丢弃数据包 }
// 分配新的接收缓冲区节点结构 node = (struct rxbuf *)kalloc(); if(!node){ // 内存分配失败处理 release(&s->lock); // 释放socket锁 kfree(buf); // 释放数据包缓冲区 return; // 内存不足,丢弃数据包 } // 初始化新节点 node->data = buf; // 保存数据包指针 node->len = len; // 保存数据包长度 node->next = 0; // 新节点作为队列尾节点
// 将新节点加入接收队列(尾部插入,保持FIFO顺序) if(s->rxq == 0){ // 队列为空的情况 s->rxq = node; // 新节点成为队列头 } else { // 队列非空的情况 tmp = s->rxq; // 从头节点开始遍历 while(tmp->next) // 找到当前尾节点 tmp = tmp->next; tmp->next = node; // 将新节点链接到队列尾部 }
// 唤醒所有在该socket上等待的接收进程 wakeup(s); // 释放socket锁 release(&s->lock); }
|