🚀 学习向复刻操作系统链路层:从零实现以太网帧、网卡收发核心机制

一、项目背景与核心定位

1.1 项目背景

链路层是TCP/IP协议栈最底层,负责把网络层IP报文封装成以太网帧,通过网卡硬件完成二进制数据收发。绝大多数开发者从未接触网卡DMA、环形描述符、MAC帧、硬件中断等底层逻辑,所有二层收发、硬件调度全部由操作系统内核封装屏蔽。
本项目配套Socket抽象层、传输层、网络层整套学习复刻,手写基础链路层核心逻辑,同时梳理工业级网卡高性能拓展原理,打通完整协议栈底层链路。

1.2 核心目标

本项目仅用于底层网络原理学习,不手写工业级高并发、零拷贝、多队列硬件驱动,采用「基础机制手写吃透、工业硬件特性仅懂原理、高性能组件复用成熟驱动」的学习策略,两大核心目标:

  1. 手写以太网帧封装解析、基础DMA环形缓冲区、网卡中断收发逻辑,吃透二层MAC寻址、帧校验、硬件报文流转完整原生逻辑;
  2. 完整梳理操作系统链路层为上层网络层屏蔽的硬件、帧封装、中断调度等底层工作,补齐链路层知识盲区,与Socket/传输/网络层形成完整协议栈知识闭环。

1.3 整体分层架构

┌─────────────────────────────────────────────────────────────┐
│                   应用层 (Application Layer)                  │
│  HTTP/FTP/SMTP/DNS... 基于Socket API进行网络通信            │
└──────────────────────────────┬──────────────────────────────┘
                               │
┌──────────────────────────────▼──────────────────────────────┐
│                  🖥️ 传输层 (Transport Layer)                 │
│  TCP/UDP 协议实现 · 端口管理 · 连接状态机 · 可靠传输机制      │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  TCP:三次握手/四次挥手 · 序列号/确认号 · 滑动窗口    │  │
│  │  超时重传 · 流量控制 · 拥塞控制基础算法              │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  UDP:无连接 · 无状态 · 尽力交付 · 端口多路复用       │  │
│  └──────────────────────────────────────────────────────┘  │
└──────────────────────────────┬──────────────────────────────┘
                               │
┌──────────────────────────────▼──────────────────────────────┐
│                  🌐 网络层 (Network Layer)                   │
│  IP/ARP/ICMP 协议 · 路由寻址 · 分片重组 · NAT/防火墙        │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  IP寻址与转发 · ARP地址解析 · ICMP差错控制            │  │
│  │  静态路由表 · IP分片与重组 · TTL生存周期              │  │
│  │  NAT地址转换 · Netfilter防火墙钩子 · 策略路由拓展     │  │
│  └──────────────────────────────────────────────────────┘  │
└──────────────────────────────┬──────────────────────────────┘
                               │
┌──────────────────────────────▼──────────────────────────────┐
│                  🔗 链路层 (Link Layer)                     │
│  以太网帧封装 · MAC寻址 · DMA环形队列 · 网卡中断驱动        │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  以太网头/CRC校验 · 收发描述符 · 硬件中断处理         │  │
│  │  物理连续DMA内存 · PHY通信 · 虚拟网卡封装             │  │
│  │  多队列网卡 · 零拷贝转发 · 硬件卸载校验(工业拓展)     │  │
│  └──────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

核心设计原则

  • 分层解耦:每层只关心本层职责,通过标准接口与上下层交互,上层网络层完全不感知网卡硬件细节;
  • 学习导向:重点手写以太网帧、单队列DMA、基础中断收发,多队列、硬件卸载、RDMA等高复杂工业特性仅了解原理,开发复用现成驱动;
  • 可观测性:帧完整日志、DMA描述符状态打印、中断触发计数,追踪二层报文完整流转;
  • 可测试性:支持虚拟网卡收发、标准以太网互通、CRC校验异常、断中断等场景测试,验证链路层功能正确性。

二、链路层完整全域职责

链路层承担硬件与协议栈中间桥梁,分为基础刚需实现(学习手写)、工业硬件高性能拓展(仅原理,不手写)

(一)基础核心能力

  1. 以太网帧封装与解析:源MAC、目的MAC、EtherType类型字段;
  2. CRC32帧校验:发送生成校验值,接收校验帧完整性,损坏直接丢弃;
  3. DMA环形收发缓冲区:物理连续内存、收发描述符Ring队列;
  4. 硬件中断处理:收包中断触发、中断下半部报文分发;
  5. MAC地址管理:本机硬件MAC、广播MAC、单播/组播区分;
  6. 统一上下层接口:接收网络层IP报文封装帧下发网卡,收到帧剥离二层头上交网络层;
  7. 简易虚拟网卡/标准网卡基础收发抽象。

(二)工业级拓展能力

  1. 硬件卸载Offload:网卡硬件计算IP/TCP/UDP校验和、硬件分片、VLAN剥离;
  2. 多队列RSS网卡:多DMA队列、多核中断亲和,提升并发吞吐;
  3. 零拷贝转发:skb复合缓冲区、DMA直接映射,消除用户/内核拷贝;
  4. VLAN二层标签处理、STP交换机环路防护;
  5. PHY自适应速率、流控PAUSE帧、巨帧Jumbo Frame;
  6. RDMA、DPDK用户态驱动、中断抑制、批量收发优化;
  7. 网卡硬件防火墙、MAC地址过滤、流量限速硬件队列。

三、链路层公共支撑底座

1. 网卡设备抽象结构体

统一屏蔽不同网卡硬件差异,对标Linux net_device

  1. 硬件信息:本机MAC、网卡速率、MTU最大值;
  2. DMA资源:发送Ring、接收Ring物理内存地址、描述符数量;
  3. 回调函数:收包中断回调、下发报文发送接口;
  4. 状态标记:网卡启用/关闭、中断开关、缓冲区占用计数。
工业拓展知识点

多网卡设备链表管理、VLAN子设备绑定、网卡硬件寄存器统一抽象层。

伪代码示例:网卡设备抽象结构体

// 网卡设备抽象(对标Linux net_device)
struct net_device {
    // 硬件信息
    uint8_t mac_addr[6];     // 本机MAC地址
    uint32_t mtu;            // 最大传输单元
    uint32_t link_speed;     // 链路速率(Mbps)
    
    // DMA资源
    struct dma_desc *tx_ring; // 发送描述符环
    struct dma_desc *rx_ring; // 接收描述符环
    uint32_t tx_ring_size;    // 发送环大小
    uint32_t rx_ring_size;    // 接收环大小
    uint32_t tx_head, tx_tail; // 发送环首尾指针
    uint32_t rx_head, rx_tail; // 接收环首尾指针
    
    // 回调函数
    int (*send)(struct net_device *dev, struct eth_frame *frame);
    void (*recv_callback)(struct net_device *dev, struct eth_frame *frame);
    
    // 状态标记
    uint32_t flags;           // 网卡状态标志
    uint32_t irq_num;         // 中断号
    bool is_up;               // 网卡启用状态
};

2. DMA环形描述符管理

手写基础逻辑
  1. 收发描述符结构体:缓冲区物理地址、数据长度、完成标志、中断标记;
  2. 环形队列首尾指针维护:tx_head/tx_tail、rx_head/rx_tail;
  3. 物理连续内存分配:DMA无法使用虚拟碎片化内存,手动申请连续物理页;
  4. 描述符重置逻辑:报文发送/接收完成,重置缓冲区供复用。
工业拓展优化

多RSS多Ring队列、描述符预分配、硬件自动填充描述符、中断合并减少CPU开销。

伪代码示例:DMA描述符结构

// DMA描述符结构(简化版)
struct dma_desc {
    uint64_t buffer_addr;    // 缓冲区物理地址
    uint32_t length;         // 数据长度
    uint16_t status;         // 状态标志
    uint16_t flags;          // 控制标志
    
    // 状态标志位定义
    #define DESC_STATUS_OWN   (1 << 0)  // 硬件拥有描述符
    #define DESC_STATUS_DONE  (1 << 1)  // 传输完成
    #define DESC_STATUS_ERR   (1 << 2)  // 传输错误
    
    // 控制标志位定义
    #define DESC_FLAG_INT     (1 << 0)  // 传输完成产生中断
    #define DESC_FLAG_LAST    (1 << 1)  // 最后一个描述符
};

// DMA环形缓冲区管理
struct dma_ring {
    struct dma_desc *descs;   // 描述符数组
    uint32_t size;            // 环大小
    uint32_t head;            // 生产者指针
    uint32_t tail;            // 消费者指针
    uint32_t free_count;      // 空闲描述符计数
    spinlock_t lock;          // 并发保护锁
};

3. 中断调度底座

  1. 硬件中断注册:网卡收到报文触发硬件中断;
  2. 中断上下文限制:中断内仅做最简单标记,复杂报文解析丢入软中断;
  3. 软中断分发队列:批量处理接收帧,避免阻塞硬件中断。
工业拓展优化

多核中断亲和、中断节流、NAPI批量收包机制,大幅降低CPU占用。

4. 二层通用报文缓冲区

手写简易内存管理,复用全局内存池,存储完整以太网帧;
工业级:skb分段缓冲区、页缓存复用,减少内存分配开销。

四、手写核心子系统1:以太网帧处理模块

以太网头部固定结构(14字节)

目的MAC(6B) + 源MAC(6B) + EtherType(2B)

  • 0x0800:IPv4报文
  • 0x0806:ARP报文
  • 0x86DD:IPv6

伪代码示例:以太网帧头部结构

// 以太网帧头部(14字节)
struct eth_header {
    uint8_t dst_mac[6];    // 目标MAC地址
    uint8_t src_mac[6];    // 源MAC地址
    uint16_t eth_type;      // 上层协议类型(网络字节序)
} __attribute__((packed));

// 以太网帧完整结构(头部 + 载荷 + CRC)
struct eth_frame {
    struct eth_header header;
    uint8_t payload[MTU];   // 最大传输单元载荷
    uint32_t crc32;         // CRC32校验值
};

报文

发送封装流程

  1. 接收网络层下发IP/ARP载荷数据;
  2. 填充源MAC、目标MAC、EtherType字段;
  3. 拼接完整二层帧载荷,计算尾部CRC32校验值;
  4. 写入DMA发送缓冲区,更新发送描述符,通知网卡硬件发送。

接收解析流程

  1. 网卡中断触发,从DMA缓冲区读取完整以太网帧;
  2. 重新计算CRC32,和帧尾部校验值对比,不一致直接丢弃;
  3. 剥离14字节以太网头部,提取上层载荷;
  4. 根据EtherType,将载荷上交网络层IP/ARP处理。

广播/单播区分逻辑

  • 广播MAC FF:FF:FF:FF:FF:FF:全网所有主机接收;
  • 单播MAC:仅目标网卡匹配接收,硬件自动过滤不匹配帧(基础软件模拟,工业网卡硬件过滤)。

五、核心子系统2:DMA环形收发驱动模块

发送流程(网络层 → DMA发送Ring → 网卡硬件)

  1. 链路层接收link_send调用,组装完整以太网帧;
  2. 查询发送Ring空闲描述符,拷贝帧至物理DMA缓冲区;
  3. 填充描述符长度、缓冲区地址、中断标记;
  4. 前移发送尾指针,写网卡寄存器触发硬件发送;
  5. 发送完成中断到来,回收描述符缓冲区,释放内存。

伪代码示例:DMA发送流程详细实现

// DMA发送完整流程
void dma_transmit(struct net_device *dev, void *data, uint32_t len) {
    // 1. 获取发送环索引
    uint32_t tx_idx = dev->tx_head;
    
    // 2. 检查环是否已满
    if ((dev->tx_head + 1) % dev->tx_ring_size == dev->tx_tail) {
        // 环满,等待或返回错误
        schedule_timeout(1);  // 等待1ms
        return;
    }
    
    // 3. 分配DMA缓冲区(物理连续内存)
    dma_addr_t dma_addr = dma_alloc_coherent(len);
    if (!dma_addr) {
        return;  // 分配失败
    }
    
    // 4. 拷贝数据到DMA缓冲区
    memcpy(phys_to_virt(dma_addr), data, len);
    
    // 5. 填充发送描述符
    struct dma_desc *desc = &dev->tx_ring[tx_idx];
    desc->buffer_addr = dma_addr;
    desc->length = len;
    desc->status = DESC_STATUS_OWN;  // 硬件拥有
    desc->flags = DESC_FLAG_INT;     // 完成后中断
    
    // 6. 内存屏障,确保描述符写入完成
    memory_barrier();
    
    // 7. 更新硬件发送尾指针
    dev->tx_head = (tx_idx + 1) % dev->tx_ring_size;
    write_reg(dev, TX_TAIL_REG, dev->tx_head);
    
    // 8. 触发硬件发送
    write_reg(dev, TX_START_REG, 1);
    
    // 9. 记录发送统计
    dev->tx_packets++;
    dev->tx_bytes += len;
}

接收流程(网卡硬件 → DMA接收Ring → 上交网络层)

  1. 初始化时预填充全部接收描述符,分配物理内存;
  2. 网卡收到电信号数据,自动写入DMA接收缓冲区;
  3. 硬件置位描述符完成标记,触发硬件中断;
  4. 中断服务程序标记待处理帧,触发软中断;
  5. 软中断读取完整帧、校验CRC、解析以太网头,回调link_recv_callback交付网络层;
  6. 重置当前描述符缓冲区,放回Ring等待下一次接收。

手写简化取舍

  1. 仅单发送/单接收队列,不实现多RSS队列;
  2. 无硬件校验卸载,全部软件计算CRC32;
  3. 不支持巨帧Jumbo Frame,固定MTU 1500;
  4. 无NAPI批量收包,一帧触发一次中断。

六、核心子系统3:中断与软中断调度模块

硬件中断处理逻辑

  1. 中断上下文禁止复杂运算,仅做三件事:标记有新报文、唤醒软中断、清除网卡中断标志;
  2. 避免在中断内解析帧、拷贝大内存,防止中断阻塞丢失报文。

软中断报文分发

  1. 维护接收帧等待链表;
  2. 主线程/任务循环处理链表内所有以太网帧;
  3. 处理完成后回收DMA缓冲区,重置接收描述符。

七、工业级链路层拓展模块详细原理

拓展1:网卡硬件Offload卸载

  1. TX卸载:网卡硬件自动计算IP/TCP/UDP校验和、自动分片;
  2. RX卸载:硬件校验CRC、IP/TCP校验和、剥离VLAN标签;
    价值:解放CPU,大量报文场景大幅降低主机算力消耗,云服务器、网关标配。

拓展2:RSS多队列多CPU并发

多DMA接收Ring,不同五元组报文分发至不同队列,绑定不同CPU核心,避免单CPU收包瓶颈;配合NAPI批量收包,每秒百万级报文吞吐。

拓展3:DPDK用户态链路驱动

绕过操作系统内核中断、协议栈,直接用户态映射网卡DMA物理内存,轮询收发,消除系统调用、内核拷贝开销,高性能转发专用。

拓展4:VLAN、STP、巨帧、流控

  1. VLAN 802.1Q标签插入二层帧,实现单网卡多网段隔离;
  2. STP防止交换机环路广播风暴;
  3. Jumbo Frame 9000字节巨帧,减少分片次数;
  4. PAUSE流控帧,二层拥塞通知对端暂停发送。

拓展5:RDMA远程直接内存访问

网卡直接读写远端主机内存,无需CPU拷贝,高性能存储、分布式计算场景使用。

八、链路层 ↔ 上下层标准解耦接口

向上对接网络层(网络层调用链路层发送)

// 网络层下发载荷、目标MAC、上层类型,封装以太网帧发送
int link_send(uint8_t *dst_mac, uint16_t eth_type, uint8_t *data, uint16_t len);
// 链路层接收完整二层帧,剥离头部后回调交付网络层
void link_recv_callback(uint16_t eth_type, uint8_t *payload, uint16_t len, uint8_t *src_mac);

网络层完全不感知DMA、中断、CRC、网卡硬件,仅传入MAC与载荷数据。

伪代码示例:链路层发送接口实现

// 链路层发送函数(网络层调用)
int link_send(uint8_t *dst_mac, uint16_t eth_type, uint8_t *data, uint16_t len) {
    // 1. 检查参数有效性
    if (len > MTU - sizeof(struct eth_header) - 4) {
        return -EINVAL;  // 超过MTU限制
    }
    
    // 2. 分配以太网帧缓冲区
    struct eth_frame *frame = alloc_eth_frame();
    if (!frame) {
        return -ENOMEM;  // 内存不足
    }
    
    // 3. 填充以太网头部
    memcpy(frame->header.dst_mac, dst_mac, 6);
    memcpy(frame->header.src_mac, g_local_mac, 6);
    frame->header.eth_type = htons(eth_type);  // 转换为网络字节序
    
    // 4. 拷贝载荷数据
    memcpy(frame->payload, data, len);
    
    // 5. 计算CRC32校验值
    frame->crc32 = calculate_crc32(frame, sizeof(struct eth_header) + len);
    
    // 6. 调用网卡发送
    int ret = netdev_send(g_net_device, frame);
    
    // 7. 释放帧缓冲区
    free_eth_frame(frame);
    
    return ret;
}

// 网卡发送函数(底层驱动)
int netdev_send(struct net_device *dev, struct eth_frame *frame) {
    // 获取发送环空闲描述符
    uint32_t idx = dev->tx_head;
    if (ring_is_full(dev->tx_ring, idx, dev->tx_tail)) {
        return -EBUSY;  // 发送环已满
    }
    
    // 填充DMA描述符
    dev->tx_ring[idx].buffer_addr = virt_to_phys(frame);
    dev->tx_ring[idx].length = frame_size(frame);
    dev->tx_ring[idx].status = 0;
    dev->tx_ring[idx].flags = DESC_FLAG_INT | DESC_FLAG_LAST;
    
    // 更新硬件拥有描述符
    dev->tx_ring[idx].status |= DESC_STATUS_OWN;
    
    // 更新发送指针
    dev->tx_head = (idx + 1) % dev->tx_ring_size;
    
    // 触发硬件发送
    write_reg(dev, TX_REG, idx);
    
    return 0;
}

向下对接硬件寄存器(底层硬件交互)

读写网卡控制、状态、DMA描述符寄存器,学习阶段可基于虚拟网卡模拟,无需真实物理网卡驱动。

九、完整端到端全链路(四层串联)

  1. 上层Socket send写入数据 → TCP传输层封装TCP头部;
  2. TCP调用ip_send下发网络层,组装IP头、处理分片、ARP解析目标MAC;
  3. 网络层调用link_send下发链路层;
  4. 链路层组装以太网头部、计算CRC,写入DMA发送Ring,通知网卡硬件发送;
  5. 对端网卡接收数据写入DMA缓冲区,触发硬件中断;
  6. 软中断读取帧、校验CRC、剥离以太网头,通过回调上交网络层;
  7. 网络层重组分片、分发至TCP;
  8. TCP回复ACK报文反向走完链路层→网络层→传输层→Socket完整流程。

十、链路层分层难点拆分

(一)手写必攻克难点(学习核心)

  1. DMA物理连续内存管理:虚拟/物理地址映射错误直接报文损坏;
  2. 环形描述符首尾指针并发竞争,中断与主线程同时修改易卡死;
  3. CRC32校验算法实现,字节序对齐出错导致和标准设备互通失败;
  4. 中断上下文限制,复杂逻辑放置中断内会丢包、系统卡死;
  5. DMA缓冲区循环复用,忘记重置描述符引发内存越界、重复报文。

(二)工业拓展模块高难度点

  1. RSS多队列哈希分流、多核中断亲和调度;
  2. NAPI批量收包节流逻辑、中断合并优化;
  3. 各类硬件Offload多字段联动适配;
  4. DPDK用户态内存映射、轮询驱动调度;
  5. RDMA网卡内存注册、远程访问权限控制。

十一、四层网络架构职责对照表

分层 手写实现内容 工业级拓展能力(复用组件) 屏蔽上层的全部复杂工作
Socket抽象层 阻塞bind/connect/send/recv/close、缓冲区、句柄管理 IOCP/epoll多路复用、setsockopt全量配置 文件描述符、IO阻塞、连接资源回收
传输层TCP/UDP 状态机、序列号、滑动窗口、基础重传、五元组哈希 SACK、BBR拥塞控制、TCP Fast Open、保活高级配置 可靠传输、丢包修复、流量控制、连接生命周期
网络层IP/ARP/ICMP 寻址、静态路由、ARP缓存、IP分片重组、基础ICMP NAT、防火墙钩子、策略路由、动态路由、VLAN/隧道、QoS 跨网段转发、二层地址映射、分片、流量管控、安全过滤
链路层以太网/DMA 以太网帧封装、CRC校验、单队列DMA、硬件中断收发 RSS多队列、硬件卸载、DPDK、RDMA、VLAN、巨帧、NAPI 网卡硬件驱动、DMA内存、帧校验、硬件中断调度、二层帧封装

十二、学习复刻实操建议

  1. 手写范围锁定:以太网帧封装解析、CRC32校验、单DMA环形队列、基础中断/软中断调度、通用网卡抽象层,基于虚拟网卡完成端到端收发;
  2. 工业级模块学习方案
    • RSS、硬件Offload、NAPI、DPDK、RDMA仅梳理报文流转、硬件工作逻辑;
    • 真实物理网卡驱动、多队列调度直接复用开源lwIP驱动、Linux内核网卡驱动,不从零开发;
  3. 严格分层隔离,链路层不感知IP、TCP任何业务逻辑,仅处理二层帧与硬件DMA;
  4. 结合Socket、传输、网络层三份文档,完整贯通从用户态应用数据到网卡硬件二进制收发的全栈底层逻辑。

十三、整体总结

  1. 链路层是协议栈硬件入口,核心承载以太网帧封装、DMA内存管理、网卡中断调度,是上层三层协议与物理网线的隔离层;
  2. 手写链路层可以直观理解操作系统内核替网络层屏蔽的硬件操作:DMA内存分配、寄存器读写、中断处理、帧CRC校验、二层MAC封装;
  3. 分层接口完全解耦,网络层仅传入目标MAC与载荷,无需关心任何硬件、二层帧细节,完美体现TCP/IP分层独立设计思想;
  4. 区分基础手写模块与工业高性能拓展,既能吃透二层底层核心原理,又掌握生产网关、高性能转发场景的硬件优化手段;
  5. 全套Socket/传输/网络/链路四层文档形成完整学习体系,从零完整看懂操作系统网络子栈每一层底层实现代价与屏蔽的复杂工作。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐