从零写OS内核 | 网卡——从 PHY 到 socket
你有没有想过这个过程:你输入一个网址,数据包是怎么从你的电脑发送到服务器的? CPU 把数据交给网卡,网卡再把电信号送到路由器——这条链路里,PHY、MAC、 DMA 环形缓冲区、驱动层层相扣。
本文拆解网卡硬件架构、驱动模型(netdev)、收发缓冲区、多队列 RSS,最后给出 Linux 调试命令。wandos 目前无网络栈,文中会单独说明。
一、网卡硬件架构:PHY + MAC + DMA
一张网卡(NIC)内部有三个核心组件:
┌─────────────────────────────────────────────────────────┐
│ 网卡芯片 │
│ ┌─────────┐ ┌─────────┐ ┌─────────────────────┐ │
│ │ PHY │◄──│ MAC │◄──│ DMA │ │
│ │ (物理层)│ │(链路层) │ │ (TX/RX Ring Buffer) │ │
│ └────┬────┘ └────┬────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ RJ45 接口 MII/RGMII PCIe BAR │
│ (电信号) (芯片内部连接) (内存映射) │
└───────────────┴───────────────────────┴───────────────┘
│
PCIe 总线
│
CPU / RAM三个组件的职责:
- PHY(Physical Layer):负责电信号的编码/解码(RJ45 → 差分信号)、Auto-Negotiation(自动协商速率/双工)
- MAC(Media Access Control):负责 Ethernet 帧的封装/解封装(源/目标 MAC 地址、类型字段)
- DMA 控制器:负责在 RAM 和网卡之间直接传输数据,不经过 CPU(”Direct Memory Access”)
二、MII/RGMII 接口:MAC 和 PHY 之间怎么连
MAC 和 PHY 之间有一套标准的并行/串行接口:
| 接口 | 全称 | 数据线 | 典型速率 |
|---|---|---|---|
| MII | Media Independent Interface | 4 tx + 4 rx | 100Mbps |
| RMII | Reduced MII | 2 tx + 2 rx | 100Mbps |
| GMII | Gigabit MII | 8 tx + 8 rx | 1Gbps |
| RGMII | Reduced GMII | 4 tx + 4 rx + 边沿采样 | 1Gbps |
| XGMII | 10 Gigabit MII | 32 tx + 32 rx | 10Gbps |
RGMII 是目前最常见的千兆网口接口(Intel I219-V 等板载网卡都用 RGMII):
MAC ────────────────────────────────────────────────── PHY
─── 4 bits TX ───→
─── 4 bits RX ←───
─── TX clk ───→ ←── RX clk ────
─── TX ctrl ─→ ←── RX ctrl ───三、收发缓冲区:TX/RX Ring Buffer
DMA 控制器通过两个环形缓冲区(Ring Buffer)和 CPU 交互:
TX Ring Buffer(发送) RX Ring Buffer(接收)
┌───────────────────────────────────────┐ ┌───────────────────────────────────────┐
│ [DESC0] [DESC1] [DESC2] ... [DESC15] │ │ [DESC0] [DESC1] [DESC2] ... [DESC15] │
│ ↓ ↓ ↓ ↓ │ │ ↓ ↓ ↓ ↓ │
│ [buf0] [buf1] [buf2] [buf15] │ │ [buf0] [buf1] [buf2] [buf15] │
│ (网卡的 DMA 内存区域) │ │ (CPU 预先分配的 skb 缓冲区) │
└───────────────────────────────────────┘ └───────────────────────────────────────┘
↑ ↓
CPU 写描述符 网卡 DMA 写数据
网卡 DMA 读数据 CPU 读 skbTX 流程(发送一个以太网帧):
- CPU 分配一个
sk_buff(skb),填充数据 - CPU 写描述符(buf 地址 + 长度)到 TX Ring
- 网卡 DMA 读取内存中的数据,发送到网线
- 网卡发送完成,触发中断或写回描述符
- CPU 回收 skb,释放内存
RX 流程(接收一个以太网帧):
- 驱动预先在 RX Ring 中填充
sk_buff缓冲区 - 网卡收到帧,DMA 把数据写入 skb
- 网卡触发中断,通知 driver
- Driver 把 skb 递给上层协议栈(IP 层)
- Driver 补充新的 skb 到 RX Ring
四、驱动模型:netdev
Linux 网卡驱动通过 netdev 框架注册到内核:
// 典型的网卡驱动注册
static int my网卡_probe(struct pci_dev *pci_dev) {
struct net_device *dev = alloc_etherdev(sizeof(struct my网卡_priv));
// ... 初始化 ...
if (register_netdev(dev))
return -ENODEV;
netdev_info(dev, "My NIC driver loadedn");
}netdev 的核心 ops:
static const struct net_device_ops my网卡_ops = {
.ndo_start_xmit = my网卡_xmit, // 发包
.ndo_set_rx_mode = my网卡_set_rx_mode, // 设置混杂/多播模式
.ndo_get_stats64 = my网卡_stats64, // 读统计
.ndo_open = my网卡_open, // 启用网卡
.ndo_stop = my网卡_stop, // 禁用网卡
};Linux 查看网卡状态:
$ ip link show
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP
link/ether 08:00:27:6a:1f:2f brd ff:ff:ff:ff:ff:ff
$ ethtool -i enp0s3
driver: e1000
version: 5.15.0--generic
firmware-version: 0.13.04
expansion-rom-version:
bus-info: 0000:00:03.0
supports-statistics: yes
supports-test: yes
supports-eeprom-access: yes
supports-priv-flags: no
$ cat /proc/net/dev
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets
lo: 12345 123 0 0 0 0 0 0 12345 123
enp0s3: 123456789 12345 0 0 0 0 0 0 987654321 9876五、多队列网卡和 RSS
现代网卡有多个 TX/RX 队列,每个队列绑定不同的 CPU 核心,实现负载均衡:
网卡(8 队列)
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ Q0│ Q1│ Q2│ Q3│ Q4│ Q5│ Q6│ Q7│
└───┴───┴───┴───┴───┴───┴───┴───┘
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
(通过 MSI-X 中断亲和性绑定)
RSS(Receive Side Scaling):
5-tuple (srcIP, dstIP, srcPort, dstPort, proto)
↓ hash
indirection table → CPU 队列查看网卡队列数和 RSS 配置:
$ ethtool -l enp0s3
Current hardware settings:
RX: 8
TX: 8
Other: 0
Combined: 8
$ ethtool -x enp0s3
RX flow hash indirection table for enp0s3:
0: 0 1 2 3 4 5 6 7
1: 0 1 2 3 4 5 6 7
# 每个 CPU 核心负责 8 个队列中的某一个六、wandos 的网络栈
wandos 目前没有网络协议栈实现。 wandos 是一个从零构建的 x86 操作系统,目前的 I/O 能力集中在 VFS/ext2 文件系统、简单的文本输出(VGA/串口)和 PS/2 键盘输入。
如果你想在 wandos 上支持网络,需要:
- 在
arch/x86/下实现 Intel e1000 / RTL8139 网卡驱动 - 实现以太网卡驱动(netdev 模型)
- 实现 TCP/IP 协议栈(或移植 lwIP)
- 对接 socket API(connect/listen/accept)
这是一个复杂的子系统,建议从简单的 UDP echo server 开始,逐步实现 IP/UDP 层。
公众号 footer
相关阅读:
- 《从零写OS内核 | netfilter——Linux 防火墙框架》(231)
- 《从零写OS内核 | conntrack——连接跟踪》(233)
- 《从零写OS内核 | 总线与地址空间——CPU 是怎么找到每个设备的》(343)
动手环节:
运行 ethtool -i $(ip route get 8.8.8.8 | awk '{print $5}' | head -1) 看你的网卡驱动是什么型号,然后用 ethtool -S 查看 TX/RX 队列的统计。
仓库地址:
github.com/golang12306/os-kernel-from-scratch
关注公众号「AI不着急」,回复”资料”获取内核学习路线图。
评论