实验一: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);
}