skaiuijing

引言

计算机任何问题都可以通过加上一层中间件解决,如果不能,那就再加一层。

硬件流水线

在CPU执行过程中,有一个问题需要解决,那就是如何提高CPU频率?

传统的数字集成电路,通常是单周期架构或者是多周期架构。

单周期:每条指令在一个时钟周期内完成所有操作:取指、译码、执行、访存、写回。

多周期:将指令执行过程分成多个阶段,每个阶段用一个时钟周期。

CPU执行,很明显是一个串行的过程,那么如何将串行的程序转化为并行呢?

其实只要加中间件就行了。

程序并行化

流水线架构

早期的并行化思想可以从流水线架构中体现出来,通过流水线设计,将串行程序拆分成类似并行执行,当然,这属于硬件层面的并行操作了,但是,这一思想仍然可以应用在上层的操作系统中。

1
2
graph LR
Aa(第一级)-->Ab(寄存器)-->Ac(第二级)-->Ad(寄存器)

并行化TCP/IP协议栈设计

流水线架构的思想可以应用在网络协议栈中,这在多核条件下是十分有利的.

每一个协议栈,基本遵守TCP/IP分层架构,对于任一层来说,只有上层和下层是可见的,基于此,实际上每一层都是可以使用多个线程/进程运行的.

对于输入的数据报,要层层检查并校验,对于输出的数据报,也要要层层构造.

实际上,每一层都是分离的,它可以完全不用写成一种不断嵌套的code形式,而是在层与层之间建立一个缓冲带,类似于流水线之间的寄存器.

数据报可以被挂载在中间的队列上,每一层只用对自己看得见的输入及输出队列负责.而对输入及输入队列的处理,都可以写成线程/进程,这些将会交由不同的CPU负责.

这样,就完成了一个流水线架构的TCP/IP协议栈.

虽然在单核条件下可能并不具有优势,但是在多核条件下,协议栈将具备优越的并行化性能.

DPDK

DPDK是一种专为 用户态高速收发包而设计的网络框架,但是它为什么能这么快呢?

在并行化方面,笔者认为有两大特点:

1.队列缓冲带

2.多核流水线并行处理

处理流程如下:把程序的执行划分为流水线,多CPU的多进程同时处理,解析、分类、路由、调度,都有对应的线程同时处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
flowchart LR
A[RX Queue<br/>网卡接收] --> B[Parser<br/>协议解析]
B --> C[Classifier<br/>路由/ACL判断]
C --> D[Modifier<br/>NAT/QoS/加密]
D --> E[TX Queue<br/>发送队列]
E --> F[NIC<br/>网卡发送]

subgraph Core Binding
A -->|Core 0| B
B -->|Core 1| C
C -->|Core 2| D
D -->|Core 3| E
end

队列缓冲带

这种架构设计不管是FreeBSD内核还是Linux内核都有实现。

其实就是在网卡与处理数据包的进程之间建立了一个缓冲带,从同步模型变成了异步模型。

落后的同步模型:

1
2
graph LR
Aa(数据包到来)-->Ab(触发中断)-->Ac(唤醒接收进程)-->Ad(处理数据包)

落后的同步模型太慢了,这种一对一的框架根本满足不了高速网络处理,因此,在现代操作系统中,都是:

1
2
graph LR
A(数据包到来)-->B(触发中断)-->C(挂载在缓冲队列上)--通知接收进程-->D(只要缓冲区有数据包就持续处理)

Linux内核

在Linux中,一个数据包最简单的处理如下:

数据包到达网卡 : 网卡选择一个Ringbuf的空闲指针(entry),通过DMA 将数据包写入对应的内存。

触发硬中断(IRQ) : 网卡向 CPU 发出中断信号,通知有新数据到达。

执行中断处理函数(上半部): 驱动程序响应中断,但只做最小处理:

1.禁用网卡中断

2.标记软中断 pending

3.快速退出,释放 CPU

软中断处理(下半部)由 ksoftirqd 执行

顺便一提,ksoftirqd线程的个数其实与CPU个数有关,数据包被哪个CPU处理并不是随机的,而是与CPU亲和度有关,网卡的中断可以通过 /proc目录下的文件设置亲和性,指定哪些 CPU 可以响应该中断。

总的来说,数据包的处理在下半部处理如下:

1.每个 CPU 都有一个 ksoftirqd/N 线程(其中 N 是 CPU 编号)

2.对应的线程检查是否有 NET_RX_SOFTIRQ 挂起

3.调用网卡驱动注册的 poll() 函数

4.从 Ring Buffer 中提取数据包,封装为 sk_buff

5.将数据包交给协议栈(IP → TCP/UDP → socket)

唤醒用户进程 :数据包最终进入 socket 的接收队列,用户进程被唤醒读取数据。

问题

Linux 内核通过 Ring Buffer 和软中断机制显著提升了数据包接收效率。但当数据最终递交给用户进程时,处理模型仍然存在性能瓶颈。

用户进程通常使用 selectpoll 来轮询多个 socket 的状态。这些 API 虽然支持多路复用(即“多对一”),但它们的实现方式存在以下问题:

1.每次调用都要遍历整个 fd 集合,哪怕只有一个 fd 就绪

2.每次都需重新传入 fd 集合,内核无法复用状态

3.在高并发场景下,轮询开销随 fd 数量线性增长

因此,虽然 select 并非严格意义上的“一对一模型”,它的行为在高负载下却表现出类似的低效特征。

所以笔者建议,如果你在编写网络处理程序时对网络性能有要求,那么尽量少使用这种传统系统调用,而是使用epoll。

那么epoll是怎么解决这个问题的呢?其实也是引入了中间件,也就是红黑树与就绪队列这些,避免了数据包的轮询,只处理就绪的fd列表。

算了,后面会进行详细介绍了,这里点到为止。