Kernel bypass #1 内核路径

内核路径

  1. 光纤
  2. PHY(物理层):SerDes 转换(Serializer/Deserializer)
  3. MAC:负责以太网成帧/解帧,FCS校验,MAC地址过滤(网卡内部的MAC芯片负责)
  4. NIC网卡:网卡内部有SRAM做数据缓冲
  5. RSS分流:对五元组算(src_ip, dst_ip, src_port, dst_port, protocol)分流到不同的RX queue
  6. DMA:一种机制,网卡根据 RX descripor 中的 DMA 地址,将收到的包直接写入主机 DRAM 中的 DMA buffer(无须CPU搬运数据)
  7. PCIe总线:物理层(128b/130b),网卡和CPU的数据总线
  8. RX/TX descriptor ring:存在DRAM的描述符环,里面主要是 DMA buffer 地址,长度,状态等元数据,RSS 会把流量分到不同的 RX queue
  9. MSI/MSI-X中断:用PCIe message通知CPU有数据来了
  10. NAPI:中断+轮训,然后在softirq 的 poll 收包
  11. sk_buff:Linux 内核表示网络包的核心数据结构
  12. 协议栈(IP-TCP/UDP-socket):内核负责协议处理,并把数据放入 socket receive queue;当用户调用 read / recv 时,再从内核buffer 拷贝到用户 buffer
  13. read()/recv()/ syscall:用户读取数据

名词学习

1. PHY

PHY 是物理层相关模块,负责链路层以下的信号处理,SerDes,时钟恢复,编码解码,链路训练等。PHY 负责将底层电信号处理成MAC可以理解的数据流。

2. MAC

MAC 负责以太网二层处理

  • 以太网帧的发送和接收
  • 源/目的 MAC 地址的处理
  • FCS 生成和校验
  • VLAN,checksum offload 等硬件卸载能力的配合

3. DMA

网卡把数据写入RX descriptor 指向的 DMA buffer 的机制,NIC 内部的DMA 引擎先构造 PCIe Memory Write TLP(Transaction Layer Packet)。然后经过 PCIe 链路发到 Root Complex(CPU/南桥内),通过 Root Complex 路由到内存控制器,然后写入 DRAM 中的 DMA buffer。

4. RSS

RSS 分流,网卡对于每个接受包提取五元组(src IP, dst IP, src port, dst port, protocol)做哈希,然后通过哈希选一个接受队列,保证同流保序,不同的队列绑定不同的CPU 核心,独立中断。这个是在 DMA 写入主机内存前决定的:RSS先选择RX queue,然后网卡使用该 queue 的 RX descriptor,把数据写入对应 DMA buffer。

5. MSI / MSI-X

Message Signaled Interrupt 和 MSI Extended 是 PCI/PCIe 设备的中断机制,用内存写事务替代物理中断引脚来通知CPU。

  • 传统 INTx:拉低物理IRQ 线,中断控制器,CPU
  • MSI/MSI-X:设备发PCIe memory write TLP 写入特定的 MSI/MSI-X 地址,平台中断控制器/APIC 机制再把中断投递到目标CPU

LAPIC(Local Advanced Programmable Interrupt Controller,本地高级可编程中断控制器) 是 x86
CPU 每个核心内部集成的一个硬件模块,负责管理该核心的中断

为什么RSS通常搭配MSI-X:RSS 每个接收队列需要绑定独立中断和CPU,MSI-X给每个队列分配一个独立的中断向量,不同的向量路由到不同的CPU核的 LAPIC。如果没有MSI-X,所有队列共用一个中断,RSS的多队列分流就白做,中断还是会打断同一个CPU。

6. Interrupt coalescence

中断聚合,网卡攒够一批包发一次中断的技术,高吞吐场景下,每秒都有几十万到上百万的包,如果每个包都触发中断,CPU会无法正常工作。

与内核 NAPI 结合

linux NAPI 的 poll 模式天然配合中断聚合,一次中断后内核用 poll() 在 budget 限制内尽量处理队列里的包,然后在重新 arm 中断。

7. PCIe

分层

  1. 事务层:TLP 的组装 / 解析
  2. 数据链路层:序列号 + CRC
  3. 物理层:8b/10b 编码,128b/130b 编码,Ordered Sets,加扰

Lane 与带宽

PCIe Gen5 x16 全双工总带宽 = 32 GT/s * 128/130 * 16 / 8 * 2 = 126.03 GB/s

8. Descriptor Ring

描述符环是驱动和网卡之间共享的一块内存,用于协调包的收发,是DMA机制的核心数据结构。

RX工作流程:

  1. 网卡通过DMA将包数据写到 desc 指向的 skb 缓冲区
  2. 网卡回填 desc 里面的元数据,翻转 OWN 位 “驱动”
  3. 网卡发 MSI-X 中断通知CPU
  4. 驱动 NAPI poll – 轮询 desc ring
    • 检查 desc[head].OWN == “驱动”
      • 取走 skb,递交协议栈
      • 分配新skb挂到 desc[head]
      • 设 OWN == “网卡”
      • head = (head + 1)% N
    • 否:没包了,结束poll

TX工作流程:

  1. 驱动从协议栈里面拿到skb
  2. 找到空闲的 TX desc(OWN == 驱动)
  3. 把 skb 数据区的 DMA 地址填入 desc
  4. 设置 OWN == “网卡拥有”
  5. 写网卡tail 寄存器,通知网卡有新包要发送
  6. 网卡 DMA 从 skb 缓存区读取数据,打包发送
  7. 网卡回写 desc,OWN = “驱动”

9. sk_buff

Linux 内核网络栈中表示一个网络数据包的核心数据结构,从网卡驱动到 TCP/IP 协议,再到socket层,

内部结构

  1. Headroom
  2. 链路层头(MAC)
  3. 网络层头(IP)
  4. 传输层头(TCP/UDP)
  5. payload(用户数据)
  6. Tailroom

驱动通常提前准备好带有 headroom/tailroom 的skb 或 page buffer,网卡DMA写入后,驱动在 poll 中设置 skb 的长度,协议头指针,checksum 状态等元数据。

10. copy_to_user

#include <linux/uaccess.h>

unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);

Linux 内核函数,将数据从内核空间拷贝到用户空间,内核与用户态之间安全传输数据的唯一

内核访问用户态内存时应使用 uaccess 系列 API,例如 copy_to_user()、copy_from_user(),不能直接随意解引用用户态指针

参考:

https://www.alphaxiv.org/zh/overview/2405.09499v1

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注