从零写OS内核|351:网卡——从 PHY 到 socket
摘要:网卡是计算机联网的门户,数据从应用 socket 一路穿越协议栈、驱动、总线到达 PHY,最后变成光/电信号在网线中传输。本篇文章从 MII/PHY 物理层讲起,详解 netdev 驱动框架、ethtool 调试、和 ring buffer 工作原理。wandos 目前无网络栈实现。
1. 网络分层与网卡位置
1.1 网络协议栈与网卡的分层
用户空间 (应用程序)
↓ 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:谁在做什么
┌─────────────────────────────────────────────────────┐
│ 主板 / 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 寄存器:
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)
# 查看 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 网络设备驱动的核心结构:
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)
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)
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 干预:
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 配置
$ 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: on4.3 查看网卡统计
$ 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: 05. ethtool:网卡调试利器
5.1 基本信息
$ 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: yes5.2 强制设置速率/双工
# 关闭自动协商,手动设置千兆全双工
$ sudo ethtool -s eth0 speed 1000 duplex full autoneg off
# 开启 WOL (Wake-on-LAN)
$ sudo ethtool -s eth0 wol g # 魔术包唤醒5.3 抓包统计(不抓包)
$ 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: 06. 从网线到 socket:完整数据流
应用层: 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
评论