操作系统-net

E1000网卡

在早期计算机中,CPU需要通过一些特殊的汇编指令一个字节一个字节地从网卡读数据,这太慢了。而现代高性能设备(如网卡、显卡、SSD)都使用DMA(Direct Memory Access)。

基本流程:

  1. CPU 在内存(RAM)中准备好空闲的缓冲区(Packet Buffers)

  2. CPU告诉 网卡(NIC):缓冲区的地址在这个列表里,从这个列表存取

  3. 网卡 直接通过总线访问RAM,把收到的网络包写入缓冲区,或者读取缓冲区的数据发送出去

  4. 网卡 完成后,通过 中断(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):

  1. 在这个环形数组中,找到 TDT 指向的位置。

  2. 检查这个位置是否"可用"(即硬件是否已经把上一轮的包发完了?检查 E1000_TXD_STAT_DD 标志位)。

  3. 如果可用,把新数据包(mbuf)的物理地址填入这个描述符。

  4. 设置命令位(如EOP-- End Of Packet, RS-ReportStatus)

  5. 更新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):

  1. 计算我们要检查的索引:idx=(RDT +1)% SIZE

  2. 检查rx_ring[idx]的status是否包含E1000_RXD_STAT_DD(Descriptor Done).

如果没有设定,说明硬件还没收到包,直接返回。

如果设定了,说明有新包

  1. 从描述符中取出数据包(mbuf),交给上层网络协议栈(net_rx())。

  2. 你把旧的 mbuf 取走了,必须分配一个新的空白mbuf 填回这个槽位,否则下次网卡就没地方存数据了。

  3. 更新描述符,清空状态位。

  4. 更新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;
  }
}
相关推荐
极客智造27 分钟前
深入详解 C++ 智能指针:RAII 原理、分类特性、底层机制与工程实战
c++·智能指针
王璐WL1 小时前
【C++】类的默认成员函数(上)
c++
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【区间贪心】:区间覆盖(加强版)
c++·算法·贪心·csp·信奥赛·区间贪心·区间覆盖(加强版)
宏笋2 小时前
C++11完美转发的作用和用法
c++
格发许可优化管理系统2 小时前
MathCAD许可类型全面解析:选择最适合您的许可证
c++
旖-旎3 小时前
深搜(二叉树的所有路径)(6)
c++·算法·leetcode·深度优先·递归
GIS阵地3 小时前
QGIS的分类渲染核心类解析
c++·qgis·开源gis
凯瑟琳.奥古斯特4 小时前
C++变量与基本类型精解
开发语言·c++
想唱rap4 小时前
UDP套接字编程
服务器·网络·c++·网络协议·ubuntu·udp
来日可期13144 小时前
计算机存储视角下的有符号数:不止是“正负”那么简单
c++