6.1810 / 6.1810 · 第 15 课 · nz

网络编程

本节课探讨内核网络软件栈的结构与过载行为。以 Mogul & Ramakrishnan (1996) 的经典论文为核心,分析"接收活锁"(Receive Livelock)问题及其解决方案——轮询(Polling)机制。

授课教师:nz

日期:2024 年春季(Lecture 20)

难度:中等

阅读:Eliminating Receive Livelock in an Interrupt-Driven Kernel (1996)

为什么关注网络软件处理?


网络协议分层

以太网帧格式

字段大小说明
Preamble8 字节同步(如 10101010...10101011)
Destination MAC6 字节目的以太网地址(48 位)
Source MAC6 字节源以太网地址
EtherType2 字节上层协议类型
Payload46-1500 字节数据
CRC-324 字节校验

以太网地址

IP 头

UDP 头

嵌套模式

高层数据包嵌在低层数据包内:eth | ip | udp | DNS。一层的有效载荷头是下一层的头。所以你会看到一系列头的序列。


内核网络软件栈结构

数据流

接收路径:
NIC 硬件 → NIC DMA RX Ring → RX 中断处理程序 → IP 输入队列
  → 网络线程(IP 处理/转发/ socket 队列)→ 应用

发送路径:
应用 → IP 输出队列 → TX 中断处理程序 → NIC TX DMA Ring → NIC 硬件

net lab 是此结构的子集

每层做什么?

为什么有这么多包队列?

其他可能的安排


今日论文:消除接收活锁

背景:软件路由器非常常见(如 Linux 防火墙、线缆调制解调器、WiFi AP)。最大网络速度常超过路由器速度,因此可能发生过载。

Figure 6-1:原始系统(无修复)

路由器结构:[NIC, RX intr, input q, IP, output q, TX intr, NIC]

问题所在

接收活锁(Receive Livelock)

  1. NIC 中断优先级高于所有其他处理
  2. 随着输入速率增长,中断最终占用 100% CPU
  3. 没有 CPU 时间用于 IP 处理
  4. IP 输入队列增长到最大长度
  5. NIC 中断读每个包,发现队列已满,丢弃包

高层视角:软件浪费时间为最终丢弃的包做部分处理。

理想情况应该是什么样?

如果路由器设计尽可能好,图形应该显示:在过载时,吞吐量保持稳定(不下降),丢弃的包是那些无法处理的。


论文解决方案

核心思想

轮询网络线程

while (1)
  if (NIC 有包等待)
    从 NIC 读一个包
    完整处理该包
  else
    使能中断
    sleep

为什么不完全轮询?

如果包到达慢,轮询浪费 CPU。中断在低负载时更高效。

Figure 6-3:修复后的系统

"Polling (no quota)"为什么表现差?

输入处理饿死了 tx-complete 处理(发送完成中断处理)。

"no quota"为什么快速下降而非逐渐下降?

活锁因丢弃前做更多处理而加剧——每个多余的 rx 包消耗许多 tx 包的 CPU 时间。

Figure 6-4:用户级程序场景

大局观:轮询循环是施加调度控制的地方

通过配额(quota)和反馈(feedback),可以控制资源分配,避免活锁和饥饿。


通用原则

其他方法

方法说明
中断合并积累多个事件后一次性中断
多核 + 多 NIC 队列分散处理负载
用户态 NIC 访问(DPDK)绕过内核栈

类似现象在其他系统领域

一般教训

复杂(多阶段)系统需要仔细调度资源才能在过载中存活。

Linux NAPI

Linux 的 NAPI(New API)方案受此论文启发,结合中断和轮询:


测验

Q1:"接收活锁"(Receive Livelock)是什么?

Q2:论文的核心解决方案是什么?

Q3:为什么需要配额(quota)和反馈(feedback)?

Q4:为什么不完全使用轮询(始终轮询)?

Q5:论文对现代系统的主要影响是什么?

要点总结

协议分层

eth → IP → UDP/TCP → 应用。每层解析/剥离或添加头部。

内核网络栈

NIC DMA、中断处理程序、IP 处理线程、socket 队列组成流水线。

接收活锁

高中断率使 CPU 陷入中断处理,无时间做实际工作,吞吐量崩塌。

轮询 + 反馈

在中断和轮询间切换,通过配额和反馈控制资源分配。

延伸阅读