E1000网卡
在早期计算机中,CPU需要通过一些特殊的汇编指令一个字节一个字节地从网卡读数据,这太慢了。而现代高性能设备(如网卡、显卡、SSD)都使用DMA(Direct Memory Access)。
基本流程:
-
CPU 在内存(RAM)中准备好空闲的缓冲区(Packet Buffers)
-
CPU告诉 网卡(NIC):缓冲区的地址在这个列表里,从这个列表存取
-
网卡 直接通过总线访问RAM,把收到的网络包写入缓冲区,或者读取缓冲区的数据发送出去
-
网卡 完成后,通过 中断(Interrupt)通知CPU。
核心机制
CPU通知网卡,缓冲区的地址都在一个列表中,这列表其实就是描述符环(Descriptor Ring)。
描述符环就是一个结构体(16字节),不存储网络包的数据,而是存储指向数据包的指针(物理地址)以及状态标志。
E1000 使用两个环形队列(Circular Arrays):一个用于接收(RX Ring),一个用于发送(TX Ring)。
发送环 TX Ring
发送环就像是有一个传送带,CPU在一端放数据、网卡在另一端取数据。

这个发送环的内存结构就是一个 struct tx_desc 数组,包含了2个寄存器:
E1000_TDH(Transmit Descriptor Head):硬件(网卡)读到了哪里
E1000_TDT(Transmit Descriptor Tail):软件写到了哪里
在这个[Head,Tail)区间的描述符是在正在等待发送的,其余的部分是空闲,可以放入新数据。
完整的发送流程(e1000_transmit):
-
在这个环形数组中,找到 TDT 指向的位置。
-
检查这个位置是否"可用"(即硬件是否已经把上一轮的包发完了?检查 E1000_TXD_STAT_DD 标志位)。
-
如果可用,把新数据包(mbuf)的物理地址填入这个描述符。
-
设置命令位(如EOP-- End Of Packet, RS-ReportStatus)
-
更新TDT指针(TDT=(TDT+1)%SIZE)。这一步等于告诉网卡:"我有新数据放上去了,快发!"
接收环 RX Ring
由于网卡不知道什么时候会有包传递过来,所以CPU会提前准备好一大堆空缓冲区放在环形队列中。

接收环的内存结构就是 rx_desc 数组,也有2个寄存器:
E1000 RDH(Receive Descriptor Head):硬件写到了哪里(也就是最新的包放在了哪里)。
E1000_RDT(Receive Descriptor Tail):硬件可以写到哪里(软件告诉硬件:从Head到Tail都是我给你的空缓冲区
这里[Head,Tail] 区间是硬件 可以使用的空闲槽位。当网卡收到包,它会填入 RDH 指向的槽位,然后
RDH++。软件 总是去检查(RDT+1)的位置,看看硬件是否把数据填进去了(检查 DD 标志位)。
完整的接收流程(e1000_recv):
-
计算我们要检查的索引:idx=(RDT +1)% SIZE
-
检查rx_ring[idx]的status是否包含E1000_RXD_STAT_DD(Descriptor Done).
如果没有设定,说明硬件还没收到包,直接返回。
如果设定了,说明有新包
-
从描述符中取出数据包(mbuf),交给上层网络协议栈(net_rx())。
-
你把旧的 mbuf 取走了,必须分配一个新的空白mbuf 填回这个槽位,否则下次网卡就没地方存数据了。
-
更新描述符,清空状态位。
-
更新RDT指针(RDT = idx)
mbuf
这是Unix系统中标准的网络缓冲区结构

networking
需要实现网卡的 e1000_transmit 和e1000_recv 函数。
第一步
将上层网络协议栈传递下来的mbuf(数据包)放入发送环(TX Ring),并通知网卡硬件发送。主要的逻辑是:
a. 加锁:因为多个进程可能同时尝试发送数据包,我们需要保护TX Ring
b. 获取索引:读取regs[E1000_TDT]获取下一个可用的位置(Tail)。
c. 检查溢出:检查当前描述符的status是否包含E1000_TXD_STAT_DD,如果没有 DD 标志,说明上一轮在这个位置发的包还没发完,环满了,返回错误
d. 释放旧包:如果这个位置之前有包(tx_mbufs[idx]不为空),说明网卡已经发完了它(因为DD位设立了),我们需要调用mbuffree释放旧的内存。
e. 填入新包:更新 tx_mbufs[idx]指向新的m,设置描述符addr为(uint64)m->head,设置描述符length为m->len;
关键:设置cmd为E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS ,EOP(End of Packet):这是包的结尾。RS(Report Status):必须设置,否则网卡发完后不会设置 DD位,下次你的程序就会以为环一直是满的。
f. 更新 Tail: 将regs[E1000_TDT]加1(取模),通知网卡有新数据。
g.解锁。
cpp
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.
//
// 1. 获取锁. 多个进程可能同时发送数据包,要保护发送接收环
acquire(&e1000_lock);
// 2. 获取当前的尾部索引 (Tail),读取regs[E1000_TDT]获取下一个可用的位置tail
uint32 idx = regs[E1000_TDT];
// 3. 检查当前描述符的状态
// E1000_TXD_STAT_DD (Descriptor Done) 表示硬件是否处理完了该描述符,如果 DD 没有置位,说明之前放入的包还没发出去,环满了
// 检查当前描述符的状态是否包含E1000_TXD_STAT_DD
if ((tx_ring[idx].status & E1000_TXD_STAT_DD) == 0) {
release(&e1000_lock);
return -1; // 发送失败,缓冲区满
}
// 4. 释放该索引处旧的mbuf
// 这个位置之前有包,不为空说明网卡已经发完了
if (tx_mbufs[idx]) {
mbuffree(tx_mbufs[idx]);
}
// 5. 将新的 mbuf 放入记录数组
tx_mbufs[idx] = m;
// 6. 填充发送描述符
tx_ring[idx].addr = (uint64)m->head; // 数据包物理地址
tx_ring[idx].length = m->len; // 数据长度
// EOP: End Of Packet (这是包的最后一个分片)
// RS: Report Status (告诉硬件发完后把 status 里的 DD 位置 1)
tx_ring[idx].cmd = E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS;
// 7. 更新 TDT (Tail Pointer), 推进指针
// 这一步之后,硬件就会感知到有新包,开始 DMA 传输
regs[E1000_TDT] = (idx + 1) % TX_RING_SIZE;
// 8. 释放锁
release(&e1000_lock);
return 0;
}
第二步
扫描接收环(RX Ring),把所有已经到达的包取出来交给上层,并补充新的空mbuf给网卡。主要的逻辑是:
a.计算索引,RDT的下一个位置:(regs[E1000_RDT] + 1) % RX_RING_SIZE .
b. 循环处理:因为一次中断可能发来多个包,所以需要用while循环或者是检查所有可用包。
c. 检查状态:检查 rx_ring[idx].status是否包含E1000_RXD_STAT_DD,如果没有 DD,说明这个位置硬件还没填入数据,说明所有新包都处理完了,退出循环。
d. 取出数据:更新mbuf->len为描述符里的length,并将这个mbuf交给上层:net_rx(mbuf)交给上层:net_rx(mbuf)
e. 补充空闲位置:我们把旧的mbuf给上层了,必须申请一个新的mbuf(mbufalloc)填回环里,否则下次网卡没地方写数据。如果mbufalloc失败怎么办?通常可以跳过或者退出,但为了不破坏环,我们最好在调用 net_rx之前先分配新的。
f. 重置描述符:设置 rx_ring[idx].addr为新mbuf地址,清空 status:rx_ring[idx].status=0,如果不清零,下次循环到这又会以为有包来了。
g. 推进 RDT:regs [E1000_RDT] = idx。
cpp
static void
e1000_recv(void)
{
//
// Your code here.
//
// Check for packets that have arrived from the e1000
// Create and deliver an mbuf for each packet (using net_rx()).
//
// 扫描接收环队列,把所有到达的包给上层,并补充新的mbuf给网卡
while(1){
// 获取下一个等待软件处理的索引
// RDT 指向的是"硬件可用的最后一个位置",所以我们要处理的是 RDT+1
uint32 idx = (regs[E1000_RDT] + 1) % RX_RING_SIZE;
// 2. 检查 DD (Descriptor Done) 标志
// 如果没有 DD,说明硬件还没把包放到这个位置,我们可以退出了
if((rx_ring[idx].status & E1000_RXD_STAT_DD) == 0){
break;
}
// 3. 拿到接收到的 mbuf
struct mbuf *m = rx_mbufs[idx];
// 4. 尝试分配一个新的 mbuf 来替换旧的
// Hint: mbufalloc might fail
struct mbuf *new_m = mbufalloc(0);
if(new_m == 0){
// 如果内存不足分配失败,我们无法取出当前包(因为没法补空位)
// 只能丢弃这次处理机会,等下次有内存了再说,或者直接退出
break;
}
// 5. 更新接收到的 mbuf 的长度
m->len = rx_ring[idx].length;
// 6. 将接收到的包传递给上层网络协议栈
net_rx(m);
// 7. 将新的 mbuf 填入环中 (Refill)
rx_mbufs[idx] = new_m;
rx_ring[idx].addr = (uint64)new_m->head;
rx_ring[idx].status = 0; // 必须清空状态位!
// 8. 推进 RDT (Tail Pointer)
regs[E1000_RDT] = idx;
}
}