网络四:协议栈与流水线架构
skaiuijing
引言
计算机任何问题都可以通过加上一层中间件解决,如果不能,那就再加一层。
硬件流水线
在CPU执行过程中,有一个问题需要解决,那就是如何提高CPU频率?
传统的数字集成电路,通常是单周期架构或者是多周期架构。
单周期:每条指令在一个时钟周期内完成所有操作:取指、译码、执行、访存、写回。
多周期:将指令执行过程分成多个阶段,每个阶段用一个时钟周期。
CPU执行,很明显是一个串行的过程,那么如何将串行的程序转化为并行呢?
其实只要加中间件就行了。
程序并行化
流水线架构
早期的并行化思想可以从流水线架构中体现出来,通过流水线设计,将串行程序拆分成类似并行执行,当然,这属于硬件层面的并行操作了,但是,这一思想仍然可以应用在上层的操作系统中。
1 | graph LR |
并行化TCP/IP协议栈设计
流水线架构的思想可以应用在网络协议栈中,这在多核条件下是十分有利的.
每一个协议栈,基本遵守TCP/IP分层架构,对于任一层来说,只有上层和下层是可见的,基于此,实际上每一层都是可以使用多个线程/进程运行的.
对于输入的数据报,要层层检查并校验,对于输出的数据报,也要要层层构造.
实际上,每一层都是分离的,它可以完全不用写成一种不断嵌套的code形式,而是在层与层之间建立一个缓冲带,类似于流水线之间的寄存器.
数据报可以被挂载在中间的队列上,每一层只用对自己看得见的输入及输出队列负责.而对输入及输入队列的处理,都可以写成线程/进程,这些将会交由不同的CPU负责.
这样,就完成了一个流水线架构的TCP/IP协议栈.
虽然在单核条件下可能并不具有优势,但是在多核条件下,协议栈将具备优越的并行化性能.
DPDK
DPDK是一种专为 用户态高速收发包而设计的网络框架,但是它为什么能这么快呢?
在并行化方面,笔者认为有两大特点:
1.队列缓冲带
2.多核流水线并行处理
处理流程如下:把程序的执行划分为流水线,多CPU的多进程同时处理,解析、分类、路由、调度,都有对应的线程同时处理。
1 | flowchart LR |
队列缓冲带
这种架构设计不管是FreeBSD内核还是Linux内核都有实现。
其实就是在网卡与处理数据包的进程之间建立了一个缓冲带,从同步模型变成了异步模型。
落后的同步模型:
1 | graph LR |
落后的同步模型太慢了,这种一对一的框架根本满足不了高速网络处理,因此,在现代操作系统中,都是:
1 | graph LR |
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 和软中断机制显著提升了数据包接收效率。但当数据最终递交给用户进程时,处理模型仍然存在性能瓶颈。
用户进程通常使用 select
或 poll
来轮询多个 socket 的状态。这些 API 虽然支持多路复用(即“多对一”),但它们的实现方式存在以下问题:
1.每次调用都要遍历整个 fd 集合,哪怕只有一个 fd 就绪
2.每次都需重新传入 fd 集合,内核无法复用状态
3.在高并发场景下,轮询开销随 fd 数量线性增长
因此,虽然 select
并非严格意义上的“一对一模型”,它的行为在高负载下却表现出类似的低效特征。
所以笔者建议,如果你在编写网络处理程序时对网络性能有要求,那么尽量少使用这种传统系统调用,而是使用epoll。
那么epoll是怎么解决这个问题的呢?其实也是引入了中间件,也就是红黑树与就绪队列这些,避免了数据包的轮询,只处理就绪的fd列表。
算了,后面会进行详细介绍了,这里点到为止。