从零写OS内核|351:网卡——从 PHY 到 socket

摘要:网卡是计算机联网的门户,数据从应用 socket 一路穿越协议栈、驱动、总线到达 PHY,最后变成光/电信号在网线中传输。本篇文章从 MII/PHY 物理层讲起,详解 netdev 驱动框架、ethtool 调试、和 ring buffer 工作原理。wandos 目前无网络栈实现。


1. 网络分层与网卡位置

1.1 网络协议栈与网卡的分层

Bash
用户空间 (应用程序)
    ↓ send()/recv() socket API
内核协议栈 (TCP/IP/UDP/ARP)
    ↓ sk_buff (socket buffer)
网卡驱动 (igb, e1000, r8169, virtio-net...)
    ↓ DMA 描述符
PCIe 总线 (ethernet frame)
    ↓
网卡 (NIC) + PHY
    ↓
网线 / 光纤

网卡在协议栈下方,驱动负责把 sk_buff(内核的网络缓冲区)通过 DMA 发送到网卡的 TX 队列,再由网卡硬件发送到网线。反方向,网卡接收帧,触发 DMA 到 RX 队列,产生硬中断,驱动读取数据构建 sk_buff 送入协议栈。

1.2 PHY 与 MAC:谁在做什么

Bash
┌─────────────────────────────────────────────────────┐
│                    主板 / SOC                        │
│  ┌─────────┐        ┌──────────────┐                │
│  │   CPU   │◄──►    │  MAC 控制器   │                │
│  │         │  AXI/  │  (Media Access│                │
│  │         │  PCIe  │   Controller) │                │
│  └─────────┘        └───────┬───────┘                │
│                             │ MII/RGMII/RMII        │
│                      ┌──────▼───────┐                │
│                      │     PHY      │                │
│                      │ (Physical    │                │
│                      │  Layer Chip)  │                │
│                      └──────┬───────┘                │
│                             │ RJ45 / 光纤             │
└─────────────────────────────┴─────────────────────────┘
  • MAC(Media Access Controller):负责 Ethernet 帧的组装/解析、CRC 计算、DMA 控制,在 SOC/南桥内部
  • PHY(Physical Layer):负责编码(曼彻斯特/64b66b)、时钟恢复、信号驱动,是独立的芯片

1.3 MII 家族:连接 MAC 和 PHY

接口 数据线 时钟 速率
MII 4-bit 25MHz 100Mbps
RMII 2-bit 50MHz 100Mbps
GMII 8-bit 125MHz 1Gbps
RGMII 4-bit 125MHz + DDR 1Gbps
XGMII 32-bit 156.25MHz 10Gbps

RGMII 是目前最常用的 MAC-PHY 接口,使用 DDR 时钟(上下降沿采样),仅用 4 根数据线实现千兆。


2. PHY:物理层的核心

2.1 PHY 功能

PHY 芯片(如 Realtek RTL8201、Microchip LAN8742)负责:

  • 编码/解码:100M 用 MLT-3,1000M 用 4D-PAM5
  • 自动协商(Auto-Negotiation):协商速率和双工
  • 链路检测:判断网线是否连接
  • 信号整形:预加重/去加重、均衡
  • Link LED:指示链路状态

2.2 MII 管理接口:MDIO

MAC 通过 MDIO(Management Data Input/Output)配置和读取 PHY 寄存器:

Bash
MDC (时钟) ──────────────────────────►
MDIO (数据) ──────────────────────────►◄── (双向)

帧格式:
|<ST>|<OP>|<PADDR>|<RADDR>|<TA>|<DATA>|
  2bit  2bit  5bit   5bit  2bit  16bit
  • ST: 01 (MII management)
  • OP: 01 = write, 10 = read
  • PADDR: PHY 地址 (0-31)
  • RADDR: 寄存器地址 (0-31)
  • TA: Turnaround (避免总线冲突)
  • DATA: 16-bit 数据

常用 PHY 寄存器:

寄存器 功能
0x00 Basic Control (重启、自动协商使能)
0x01 Basic Status (链接状态、速率能力)
0x04 Auto-Negotiation Advertisement
0x05 Auto-Negotiation Link Partner Ability
0x10 PHY Specific Status

2.3 MDIO 总线(Linux)

Bash
# 查看 MDIO 总线
$ ls /sys/bus/mdio_bus/
devices  drivers  drivers_autoprobe  uevent

# 查看某个网卡的 PHY
$ cat /sys/class/net/eth0/phydev/driver
r8169

$ mii-tool eth0
eth0: 1000baseT Full duplex, link ok

# 直接用 mii-diag 读寄存器
$ sudo mii-diag eth0
Basic mode control register 0x1200: Auto-negotiation enabled,
Basic mode status register 0x7969 ...

3. 网卡驱动与 netdev

3.1 netdev 架构

Linux 网络设备驱动的核心结构:

Bash
struct net_device {
    char name[IFNAMSIZ];        // "eth0", "wlan0"...
    unsigned long base_addr;     // I/O 地址
    unsigned int irq;           // 中断号

    /* 设备操作函数 */
    int (*open)(struct net_device *);
    int (*stop)(struct net_device *);
    netdev_tx_t (*start_xmit)(struct sk_buff *, struct net_device *);

    /* 统计 */
    struct net_device_stats stats;
};

3.2 发送流程(TX)

C
static netdev_tx_t igb_start_xmit(struct sk_buff *skb, 
                                  struct net_device *netdev) {
    struct igb_adapter *adapter = netdev_priv(netdev);
    u16 count = ...;

    // 1. 计算需要的 DMA 描述符数量
    unsigned int first = adapter->tx_tail;

    // 2. 填充 DMA 描述符
    struct igb_tx_desc *tx_desc = &adapter->tx_desc_ring[first];
    tx_desc->buffer_addr = dma_map_single(&adapter->pdev->dev,
                                          skb->data, skb->len, DMA_TO_DEVICE);
    tx_desc->cmd = IGB_TXD_CMD_EOP | IGB_TXD_CMD_RS;
    tx_desc->length = skb->len;

    // 3. 推进写指针(doorbell)
    adapter->tx_tail++;
    wr32(IGB_TDT(adapter->tx_ring), adapter->tx_tail);

    // 4. 释放 skb(DMA 已完成,网卡会在完成时中断通知)
    dev_kfree_skb_any(skb);

    return NETDEV_TX_OK;
}

3.3 接收流程(RX)

C
static irqreturn_t igb_intr(int irq, void *data) {
    struct net_device *netdev = data;
    struct igb_adapter *adapter = ...;

    // 1. 读取 ICR (Interrupt Cause Register)
    u32 icr = rd32(E1000_ICR);

    if (icr & E1000_ICR_RXSEQ) {  // 接收中断
        napi_schedule(&adapter->napi);
    }
    return IRQ_HANDLED;
}

static int igb_poll(struct napi_struct *napi, int budget) {
    while (received < budget) {
        // 1. 获取 DMA 描述符
        struct igb_rx_desc *rx_desc = &rx_ring[rx_tail];

        if (!(rx_desc->status & E1000_RXD_STAT_DD))
            break;  // 没有完成的数据

        // 2. 构建 sk_buff
        struct sk_buff *skb = netdev_alloc_skb_ip_align(netdev, length);
        skb_put(skb, length);
        eth_type_trans(skb, netdev);  // 识别上层协议(IP/ARP...)

        // 3. 上报协议栈
        netif_receive_skb(skb);
        received++;
    }

    if (received < budget)
        napi_complete(napi);  // 退出 polling
    return received;
}

4. Ring Buffer:DMA 引擎的交通岗

4.1 TX/RX Ring

网卡使用环形缓冲区(Ring)来管理 DMA 描述符,避免每次传输都需要 CPU 干预:

Bash
TX Ring (发送环形缓冲)
┌─────────┬─────────┬─────────┬─────────┐
│ Desc 0  │ Desc 1  │ Desc 2  │ Desc 3  │
│ (空闲)  │ (已使用) │ (已使用)│ (等待完成)│
└────┬────┴────┬────┴────┬────┴────┬─────┘
     │         │         │         │
     ▼         ▼         ▼         ▼
  数据 buf   数据 buf   数据 buf   数据 buf
  (DMA映射) (DMA映射) (DMA映射) (DMA映射)

驱动: write pointer (写指针) ──► 推进新描述符
  NIC: read pointer  (读指针) ◄── 消费描述符
  完成: status DD bit = 1 ────► 驱动回收描述符

4.2 查看网卡的 ring 配置

Bash
$ ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX:   4096
RX Mini:  0
RX Jumbo: 0
TX:   4096
Current hardware settings:
RX:   256        ← 实际 RX 描述符数量
TX:   256        ← 实际 TX 描述符数量

# 查看是否启用 scatter-gather
$ ethtool -k eth0 | grep scatter
scatter-gather: on

4.3 查看网卡统计

Bash
$ ethtool -S eth0 | head -40
NIC statistics:
     rx_packets: 98342
     tx_packets: 87291
     rx_bytes:   118392042
     tx_bytes:   48239482
     rx_broadcast: 4821
     tx_broadcast: 1204
     rx_multicast: 3942
     tx_multicast: 2104
     rx_errors:  0
     tx_errors:  0
     rx_dropped: 0
     tx_dropped: 0
     multicast:  3942
     collisions: 0
     rx_over_errors: 0
     rx_crc_errors: 0
     rx_frame_errors: 0
     rx_fifo_errors: 0
     rx_missed_errors: 0

5. ethtool:网卡调试利器

5.1 基本信息

Bash
$ ethtool eth0
Settings for eth0:
    Supported ports: [ TP MII ]
    Supported link modes:   10baseT/Half 10baseT/Full
                            100baseT/Half 100baseT/Full
                            1000baseT/Full
    Supported pause frame use: No
    Speed: 1000Mb/s
    Duplex: Full
    Port: MII
    PHYAD: 0x01
    Transceiver: external
    Auto-negotiation: on
    MDI-X: Unknown
    Link detected: yes

5.2 强制设置速率/双工

Bash
# 关闭自动协商,手动设置千兆全双工
$ sudo ethtool -s eth0 speed 1000 duplex full autoneg off

# 开启 WOL (Wake-on-LAN)
$ sudo ethtool -s eth0 wol g  # 魔术包唤醒

5.3 抓包统计(不抓包)

Bash
$ ethtool -S eth0 | grep -E "rx_drop|tx_drop|errors"
rx_over_errors: 0
rx_crc_errors: 0
rx_frame_errors: 0
rx_fifo_errors: 0

6. 从网线到 socket:完整数据流

Bash
应用层: HTTP GET /index.html
    ↓
用户空间: send(fd, buf, len, 0)
    ↓
内核: TCP 层 (tcp_sendmsg) 
    → 添加 TCP 头
    ↓
IP 层 (ip_output)
    → 添加 IP 头 + route lookup
    ↓
以太网层 (dev_hard_start_xmit)
    → 添加 Ethernet 头 (dst MAC, src MAC, EtherType)
    ↓
网卡的哥: netdev->ndo_start_xmit(skb)
    ↓
驱动: igb_xmit_frame → DMA 描述符填充
    ↓
PCIe DMA → 网卡 TX Ring → 网卡发送
    ↓
PHY: 编码 → 网线/光纤
    ↓
目标机器: PHY 解码 → MAC DMA → RX Ring
    ↓
驱动: NAPI poll → skb 构建
    ↓
协议栈: IP → TCP → 传输层
    ↓
recv(fd, buf, len, 0) → 应用

7. wandos 对比:无网络栈

wandos 目前无网络栈实现。网卡驱动的复杂度在于:它需要完整的 DMA 引擎驱动、NAPI 中断处理、协议栈实现(TCP/IP/UDP)、以及 socket API。这在 wandos 当前阶段是最复杂的子系统之一。wandos 未来的网络方向会从 virtio-net(虚拟网卡)开始,参考 Linux 的 virtio-net 实现。


公众号底部

📢 关注公众号【内核栈】,获取更多 OS/内核干货

💡 本文属于「从零写OS内核」系列,欢迎回顾:

  • 篇 350:《音频子系统——ALSA 是怎么播放声音的》
  • 篇 349:《SPI 总线——高速串行通信》
  • 篇 348:《I2C 总线——嵌入式系统的血管》

🔗 回复「网卡」获取 ethtool 和 netdev 编程完整示例


作者:内核栈 | 原创出品
本文永久链接:https://kernelstack.dev/351-network-card

最后修改: 2024年2月24日

作者

评论

发表评论

您的邮箱地址不会被公开。