Linux内核分析之网络协议栈-02
46.1 IP 层 —— 路由与分片
IP 层的职责是"把包送到该去的地方":入口校验 → 路由决策(本地交付/转发/丢弃)→ 交付或转发,途中处理分片重组与 netfilter 穿越。它的两个核心产物——skb_dst(路由缓存条目)与邻居表——不仅是转发的依据,也是本机 TCP 发包的参数来源。本节解剖收包骨架、路由结构与分片机制。
46.1.1 ip_rcv:入口流水线
// net/ipv4/ip_input.c:564(签名)
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
struct net_device *orig_dev)
// net/ipv4/ip_input.c:250
int ip_local_deliver(struct sk_buff *skb)
ip_rcv 的流水线 (每包的 L3 入口):
[1] 头校验: skb 拉线性化 (44.1 节 frags) →
长度一致 / 版本 4 / 头校验和 / TTL>0
[2] netfilter PREROUTING (47.1 节钩子)
— DNAT 在这里改目的地址!
[3] ip_rcv_finish: 路由决策 skb_dst_set(skb, dst)
依据 dst->input/output 函数指针分派:
├─ dst->input = ip_local_deliver (:250)
│ 本地目的: 必要时分片重组 (ip_defrag)
│ → netfilter INPUT → ip_local_deliver_finish
│ 按 iph->protocol 分发 L4:
│ tcp_v4_rcv (46.2) / __udp4_lib_rcv (46.4)
│ / icmp_rcv / raw...
├─ dst->input = ip_forward
│ 转发: TTL 递减 (0 → ICMP Time Exceeded)
│ netfilter FORWARD → MTU 检查 → ip_send
└─ 无路由 → ICMP unreachable + 丢弃
dst->input/output 函数指针是 IP 层的分发机制:路由查找的产物(dst_entry)自带"下一步该调谁"——每包不写 if-else,直接回调。这就是 44.1 节"分发链而非调用栈"思想在 L3 的体现。
46.1.2 路由结构:fib、dst_entry 与邻居
// include/net/dst.h:26(节选要点)
struct dst_entry {
struct net_device *dev; /* 出设备 */
struct dst_ops *ops;
unsigned long _metrics; /* MTU/mss/cwnd 度量 */
unsigned long expires;
int (*input)(struct sk_buff *); /* 收侧下一步 */
int (*output)(struct net *net, struct sock *sk,
struct sk_buff *skb); /* 发侧下一步 */
...
};
// include/net/neighbour.h:140
struct neighbour { ... /* ARP 解析结果与 NUD 状态机 */ };
路由的三级结构:
FIB 表 (fib_table_lookup, net/ipv4/fib_trie.c):
前缀树 (LPC-trie) 最长前缀匹配 —
"ip route add 10.0.0.0/8 via ..." 的存储体
dst_entry (缓存条目):
fib 查询结果的对象化: 出设备/网关/MTU/度量
引用计数 + RCU — TCP 连接建立时缓存在 sk 上
(每包不再查 fib!)
邻居子系统 (neighbour, neighbour.h:140):
下一跳 IP → ARP → MAC 地址
NUD 状态机: INCOMPLETE(探测中)→REACHABLE→
STALE→DELAY→PROBE→FAILED
ARP 未解析时 skb 在 neigh 队列挂起
发包视角 (本机 TCP 用同一套):
connect → ip_route_connect → dst_entry 缓存到 sk
每 skb: dst->output = ip_output → 邻居查 MAC
→ dev_queue_xmit (44.2 节入口)
MSS 的来源: dst->metrics 的 PMTU (46.2 节)
46.1.3 分片与重组:PMTUD 的信号面
发送侧分片:
报文 > dst->mtu → ip_fragment 切片
(TCP 优先靠 MSS 避免 — MSS ≈ PMTU-40;
UDP 大报文只能真分片)
DF 位 (不分片) 设置且超 MTU →
返回 EMSGSIZE + 发 ICMP Frag Needed → PMTUD
(TCP 借此动态缩 MSS; 隧道场景 MTU 探测失败的
"黑洞"是经典运维问题 — tcp_mtu_probing)
接收侧重组:
ip_defrag: 按 (源,目的,id,协议) 挂重组队列
全片到齐 → 合成完整 skb → 交付 L4
资源防护: ipfrag_high_thresh (内存限额) /
ipfrag_time (超时丢弃) — 分片风暴防护
(46.4 节 ICMP 风暴的姊妹问题)
观测:
ip route show table all / ip neigh
nstat -az | grep -i Ip (InHdrErrors/frag 计数)
/proc/sys/net/ipv4/ip_forward (转发开关)
小结
IP 层以"校验 → PREROUTING → 路由决策(dst_entry 的 input/output 函数指针分派)→ 本地交付/转发"的流水线处理每包:skb_dst 反哺 TCP 的 MSS/pacing 参数,FIB trie + dst 缓存 + 邻居 NUD 状态机构成转发三级;分片在发收两端各有限额与 PMTUD 信号(黑洞是运维暗坑);L4 分发(tcp/udp/icmp)是协议链的下一跳。下一章进入 TCP 连接管理——握手队列与状态机。
46.2 TCP 连接管理 —— 三次握手与四次挥手
TCP 连接管理在内核是两个队列 + 一张状态表 + 一台状态机:SYN 到达先进半连接队列(轻量 request_sock),完成握手升级进全连接队列等 accept;挥手则是 fin_wait/time_wait 的状态迁移与轻量化复用。本节解剖内核侧的连接生命周期,以及 SYN Flood、TIME_WAIT 堆积这两个最常见攻击/运维面的机制根源。
46.2.1 监听与两个队列
// net/ipv4/inet_connection_sock.c:650
struct sock *inet_csk_accept(struct sock *sk, struct proto_accept_arg *arg)
// net/ipv4/tcp_input.c:7130 区(request_sock 的处理)
struct request_sock *req;
listen 状态 sock 的两个队列:
┌───────────────────────────────────────────────┐
│ 半连接队列 (SYN_RECV): │
│ SYN → 分配 request_sock (轻量! 非全 sock) │
│ → 回 SYN-ACK → 等最后的 ACK │
│ 容量: backlog 与 tcp_max_syn_backlog 协商 │
├───────────────────────────────────────────────┤
│ 全连接队列 (accept_queue): │
│ ACK → request_sock 升级为完整 sock (tcp_sock) │
│ → 入队 → accept() 从此取出 │
│ (inet_csk_accept :650: 空则睡/O_NONBLOCK │
│ EAGAIN; 摘出后新建 socket+fd — 45.1.3 节) │
│ 容量: min(backlog, somaxconn) │
└───────────────────────────────────────────────┘
握手在内核完成 (第三次 ACK 到达即升级+唤醒 accept
等待者, sock_def_readable 45.2.2 节):
"accept 快慢"不影响"连接建没建" — 只影响全连接
队列溢出: 溢出时 ACK 被弃 → 客户端重传 → 建立延迟
尖刺 (TcpExtListenOverflows 计数 — ss -lnt 的
Recv-Q 看的就是这个队列的当前深度!)
46.2.2 SYN Flood 的两道防线
攻击模型: 海量伪造源地址的 SYN → 半连接队列挤满 →
合法连接进不来 → 服务拒绝
防线一: SYN-ACK 重传 + 队列满丢弃 (原始行为)
防线二: syncookies (tcp_syncookies=1):
半连接队列满时不再分配 request_sock:
SYN-ACK 的序号 = MAC(四元组, 时间计数, MSS...)
(把"本该存储的状态"编码进序号发出!)
合法客户端回 ACK → 序号验证通过 →
cookie_v4_check (net/ipv4/syncookies.c:405)
从 ACK 中反解参数 → **凭空重建 sock**
伪造源的 ACK 无法通过验证 — 无状态抗洪
代价: 不支持部分 TCP 选项 (需协商) — 兜底而非常态
观测: TcpExtSyncookiesSent/Recv — 非零即曾遭洪
46.2.3 状态机与收包分派
// net/ipv4/tcp_ipv4.c:1859
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
// tcp_input.c: tcp_rcv_state_process (非稳态) / tcp_rcv_established (稳态)
TCP 状态机 (内核锚点标注):
主动打开: CLOSED → SYN_SENT → ESTABLISHED
被动打开: LISTEN → SYN_RECV (半连接) → ESTABLISHED
挥手主动: ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2
→ TIME_WAIT (2MSL) → CLOSED
挥手被动: ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED
收包分派 (tcp_v4_do_rcv :1859):
状态 != ESTABLISHED → tcp_rcv_state_process
(握手/挥手的所有特殊处理)
稳态 → tcp_rcv_established (快路径: 乱序检查/
序号校验/直接交付 45.2 节队列)
稳态快路径的工程意义: 大部分包走最少分支 —
46.3 节的 ACK 处理与拥塞控制在慢路径
TIME_WAIT 的存在理由:
[1] 迟到的旧报文在网络中自然死亡 (2MSL 寿命)
[2] 最后 ACK 丢失时的重发窗口
内核优化 (高并发短连接的关键):
tw_sock: 半 sock (比全 sock 小一个量级)
tcp_tw_reuse: 主动方 1s 后可复用 (时间戳防回绕)
cap: tcp_max_tw_buckets (防内存被 TW 挤爆)
发送侧的统一出口是 __tcp_transmit_skb(tcp_output.c:1512):序号/ACK/窗口/选项的填充、校验和,配合 44.1 节 skb 与 46.1 节 dst 的参数(MSS 来自 PMTU)。
46.2.4 挥手的应用侧责任与观测
经典运维对照表:
现象 内核机制 观测/处方
─────────────────────────────────────────────────────────
accept 前 SYN 已成功 握手在内核完成 ss -lnt Recv-Q
SYN_RECV 大量堆积 半连接饱和 syncookies 日志
TIME_WAIT 数十万 短连接主动关闭 tcp_tw_reuse/
长连接化
CLOSE_WAIT 堆积 应用没调 close 应用 fd 泄漏!
连接建立延迟尖刺 全连接溢出 ACK 弃 ListenOverflows
连接莫名 RST 队列溢出/序列号错 tcp tracepoint
"CLOSE_WAIT 堆积 = 应用 bug"是这章最重要的运维结论:被动关闭方收到 FIN 就进 CLOSE_WAIT,唯一出口是应用 close——内核无超时兜底(默认无 keepalive 干预),fd 泄漏即连接泄漏。
小结
TCP 连接管理 = 半连接队列(request_sock + syncookies 编码状态抗洪)+ 全连接队列(升级 sock 等 accept,溢出即延迟尖刺)+ 四元组状态表(tcp_v4_do_rcv 按状态分派快慢路径);握手在内核完成、accept 只是收割,TIME_WAIT 以 tw_sock 轻量存在保障报文寿命语义并可复用。CLOSE_WAIT 堆积、ListenOverflows、SYN cookies 触发——三大运维现象都有精确的内核机制解释。下一章看 ESTABLISHED 之上的动态调控——拥塞控制。
46.3 TCP 拥塞控制
拥塞控制是 TCP 的"共享精神":对网络拥挤做出减窗反应、对空闲做增窗探测——发送速率不由接收窗口决定而由网络反馈决定。内核把它抽象成可插拔的 CA 算法(struct tcp_congestion_ops,tcp.h:1275):reno/cubic/bbr 都实现同一组钩子,运行时可切换。本节解剖插件框架、默认状态机与两代代表算法(CUBIC 的丢包驱动、BBR 的模型驱动),以及 pacing 这个现代底座。
46.3.1 CA 插件框架
// include/net/tcp.h:1275(节选要点)
struct tcp_congestion_ops {
void (*init)(struct sock *sk); /* 连接初始化 */
void (*release)(struct sock *sk); /* 连接释放 */
/* 状态迁移通知 (进入 Recovery/Loss 等) */
void (*set_state)(struct sock *sk, u8 new_state);
void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
/* ACK 携带信息时 (ECN/SACK) */
void (*in_ack_event)(struct sock *sk, u32 flags);
u32 (*undo_cwnd)(struct sock *sk); /* 恢复误判回滚 */
/* 默认状态机的增/降窗钩子 */
u32 (*ssthresh)(struct sock *sk);
u32 (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);
void (*pkts_acked)(struct sock *sk, const struct ack_sample *rs);
/* 自主模式: 完全接管窗口与节奏 */
void (*cong_control)(struct sock *sk, u32 ack, int flag,
const struct rate_sample *rs);
char name[TCP_CA_NAME_MAX];
struct module *owner;
};
// net/ipv4/tcp_cong.c:50
static struct tcp_congestion_ops *tcp_ca_find_autoload(const char *name)
框架的工作流 (每个 ACK 的处理路径):
tcp_ack (46.2 节收包路径内):
├─ 事件检测: 重复 ACK? SACK 报失? RTO 超时?
├─ 状态机: Open / Disorder / CWR / Recovery / Loss
│ (tcp_time_to_recover, tcp_input.c:2718 判定
│ 进入恢复; tcp_try_undo_recovery :2894 恢复
│ 误判回滚 — 丢包误判的后悔药)
└─ 分派给当前 CA:
无 cong_control → 内核调 cong_avoid (增窗)
/ ssthresh (降窗) / undo_cwnd
有 cong_control (BBR 类) → 完全交给算法
(内核状态机被旁路!)
切换: setsockopt(TCP_CONGESTION, "bbr")
→ tcp_ca_find_autoload (:50) 按名查/自动加载模块
全局/按路由两粒度: sysctl tcp_congestion_control
或 ip route ... congctl bbr
46.3.2 CUBIC:丢包驱动的三次函数
// net/ipv4/tcp_cubic.c:167 与 :214
static u32 cubic_root(u64 a)
static inline void bictcp_update(struct bictcp *ca, u32 cwnd, u32 acked)
CUBIC 的窗口轨迹 (每个丢包后重启周期):
cwnd
▲ _____ 慢速逼近新平衡 (三次曲线平台)
│ /
│ / 凹段: 快速恢复到上次丢包窗口 Wmax
│ /
│ / 凸段: 超过 Wmax 后谨慎探索新带宽
│ /
│ / K 时刻 = 上一丢包点
└────────────────────────────▶ t (自上次丢包)
cwnd = C×(t-K)^3 + Wmax
K = cubic_root(Wmax×β) 预算出 "多久回到 Wmax"
丢包: ssthresh = cwnd×β (β=0.7 — 比 reno 的 0.5 温和)
与 reno 兼容模式 (低窗口时用 reno 取大者)
优点: 长肥管道 (大 BDP) 恢复快, RTT 公平性好
缺点: 丢包驱动 — 必须把缓冲填满才丢
→ bufferbloat (深队列设备上延迟飙升)
46.3.3 BBR:模型驱动与 pacing
// net/ipv4/tcp_bbr.c:82-84(状态枚举)
BBR_STARTUP, /* ramp up sending rate rapidly to fill pipe */
...
BBR_PROBE_BW, /* discover, share bw: pace around estimated bw */
// :1027 与 :1148
__bpf_kfunc static void bbr_main(struct sock *sk, u32 ack, int flag,
const struct rate_sample *rs)
.cong_control = bbr_main, /* 完全接管! */
BBR 的模型: 主动测量而非被动等丢包
测两个量:
BtlBw = 瓶颈带宽 (滑动窗口内最大送达速率)
RTprop = 最小 RTT (窗口内最小往返)
BDP = BtlBw × RTprop ← 网络管道的容积
发送钉在 BDP 附近 (不填缓冲! 延迟低)
四状态机 (bbr.c:82 起):
STARTUP 倍增探测直到带宽不再涨 (填管道一次)
DRAIN 排空启动期队列
PROBE_BW 稳态: pacing_rate 在 BtlBw×[0.75,1.25]
间周期波动 (8 相位循环, :344/:406 区)
PROBE_RTT 定期压低速率测最小 RTT
pacing (节奏化发送) 是 BBR 的物理前提:
不再"突发窗口全部包", 而是 sk->sk_pacing_rate
均匀间隔发包 — fq qdisc 按时间戳调度 (47.2.3)
或网卡硬件 pacing 卸载 (44.2 节 features)
| 维度 | CUBIC(默认) | BBR |
|---|---|---|
| 拥塞信号 | 丢包 | 带宽/RTT 测量 |
| 窗口控制 | 内核状态机 + cong_avoid | cong_control 全接管 |
| 队列行为 | 填满缓冲(bufferbloat) | 钉在 BDP(低延迟) |
| 公平性 | 同族内公平 | 与 loss-based 共存时不公平 |
| 依赖 | 无 | pacing(fq qdisc 推荐) |
46.3.4 观测与调优
观测:
ss -ti 每连接: cwnd/ssthresh/retrans/rtt/
pacing_rate/ca_state — 算法行为的直接窗口
nstat TcpExtTCPTimeouts/TCPACKSkipped...
eBPF (tcp:tcp_probe tracepoint) 逐包窗口曲线
调优:
net.ipv4.tcp_congestion_control 默认 CA
net.ipv4.tcp_available_congestion_control
内核编译: CONFIG_TCP_CONG_* (bbr 需模块或内建)
小结
内核把拥塞控制做成事件驱动的插件框架:tcp_congestion_ops 钩子 + 默认状态机(cong_avoid/ssthresh/undo)+ cong_control 自主模式三件套,CUBIC(丢包驱动三次函数,内核默认)与 BBR(BDP 模型 + cong_control 接管 + pacing 底座)是两代哲学的代表;恢复状态机的 undo 机制处理丢包误判,pacing 已是发送侧的现代基础设施。ss -ti 是一切拥塞行为的直接观测窗。下一章看栈中最简单的两个协议——UDP 与 ICMP。
46.4 UDP 与 ICMP
UDP 是"IP + 端口复用"的极简传输:无连接、无流控,内核只管四元组分发、缓冲配额与现代卸载(GSO/GRO/隧道);ICMP 是 IP 的控制面——错误回报、PMTUD 信号、ping/traceroute 本体。两者在内核的复杂度集中在分发与卸载,而非协议状态。本节解剖收包路径、reuseport 分派、隧道骨架与 ICMP 的信令面。
46.4.1 UDP 收包:哈希分发与配额
// net/ipv4/udp.c:2934 与 :2692
int udp_rcv(struct sk_buff *skb)
int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
int is_udplite)
UDP 收包路径:
udp_rcv (:2934)
→ __udp4_lib_rcv (:2692):
[1] 校验: 长度一致 / 校验和 (可卸载)
[2] 查 udp_table: 双哈希表 (绑定 sock 的
端口桶 + established 表) — 四元组精确,
未命中退化到 (本地端口, 通配) 查找
[3] reuseport 分派: 同端口多 sock (SO_REUSEPORT)
按包哈希选一个 — 多进程/线程并行收包
的标准负载均衡 (内核态, 无惊群)
[4] 命中 sock → 45.2 节三选一交付:
队列浅直接拷 / 入 receive_queue / backlog
[5] 配额满: 丢弃 + UDP-MIB 计数
(InErrors/RcvbufErrors — 无流控, 应用自兜)
[6] 未命中 → ICMP port unreachable 回报
(connected UDP socket 的下次 read
得 ECONNREFUSED — 错误的异步传递)
与 TCP 分发的对照:没有连接状态机、没有拥塞控制,一切都收敛为"查表 → 配额 → 入队"三步——但哈希表的并发(每包一次、多核并查)与 reuseport 的分派公平性使 UDP 热路径的工程精度要求反而更高(QUIC 时代 UDP 是主战场)。
46.4.2 UDP 的现代卸载:GSO/GRO 与隧道
卸载谱系 (46.3 节 TSO 的 UDP 版):
UDP GSO (UFO 后继): sendmsg 带 UDP_SEGMENT
→ 一次系统调用提交超大报文, 内核/网卡按
gso_size 分段 — QUIC 吞吐的关键 (syscall
次数 ÷N, 分段在最后时刻做)
UDP GRO: 收侧聚合 — napi_gro_receive (44.3.4)
的 UDP 流合成
隧道骨架 (udp_tunnel):
VXLAN/Geneve/GRE-in-UDP = 外层 UDP + 内层二层
内核提供: 端口复用注册 (隧道端口与普通 UDP
共存于 udp_table)、封装/解封模板、GSO/GRO
穿越 — 38.3 节容器 overlay 网络的传输层
(报文栈: 以太网-VXLAN-UDP-IP-外层以太网)
46.4.3 ICMP:IP 的信令面
// net/ipv4/icmp.c:1443
int icmp_rcv(struct sk_buff *skb)
ICMP 的两类报文与内核处置:
差错类 (Destination Unreachable / Time Exceeded /
Redirect / Fragment Needed):
icmp_rcv (:1443) 按类型分发 →
差错类经 icmp_socket_deliver 回注:
查本地对应连接 (四元组从 ICMP 载荷内嵌的
原始头解析!) → sock 报错 (sk_err)
→ connect/send 的 ECONNREFUSED /
EHOSTUNREACH/EMSGSIZE (45.1 节错误传递链)
Frag Needed → 更新 dst->pmtu → TCP 缩 MSS
(46.1 节 PMTUD 的信号本体)
查询类 (Echo / Echo Reply):
ping 本体 — raw socket 或 ICMP socket
(ping_group_range 决定谁可建; 非特权 ping)
traceroute: 低 TTL 探测 + 收集 Time Exceeded
(每跳一个路由器扣 TTL → 报错的"回声定位")
限速: icmp_ratemask (哪些类型限速) +
icmp_ratelimit (速率) — 差错报文生成受控,
防"错误引发的错误"风暴 (46.1 节 TTL=0 风暴)
46.4.4 三协议对照总表
| 维度 | TCP | UDP | ICMP |
|---|---|---|---|
| 连接 | sock 状态机(46.2) | 无(connected UDP 记对端) | 无 |
| 可靠性 | 序号/重传/SACK | 无 | 无 |
| 流控 | 窗口 + CA(46.3) | 仅 rcvbuf 丢弃 | 速率限制 |
| 分发结构 | ehash 四元组 | udp_table + reuseport | 类型分发 |
| 卸载 | TSO/GRO/校验和 | UDP GSO/GRO/隧道 | — |
| 典型内核函数 | tcp_v4_rcv/tcp_output | __udp4_lib_rcv | icmp_rcv |
| 运维焦点 | 拥塞/重传 | 丢包计数/缓冲 | 限速/PMTUD |
观测:
ss -ulnp UDP socket 与绑定
nstat -az | grep -Ei "udp|icmp" 协议计数全览
ethtool -S udp 分段/聚合卸载计数
/proc/net/snmp Udp: InDatagrams/InErrors 行
小结
UDP 在内核的复杂度集中在"分发与卸载":udp_table 双哈希 + reuseport 内核态负载分派 + GSO/GRO/隧道骨架——QUIC 时代的主战场;配额满是丢弃而非流控,应用自担可靠性。ICMP 是 IP 的信令面:差错经"载荷内嵌原始头"回注本地连接(异步错误的传递链)、Frag Needed 驱动 PMTUD、ping/traceroute 是查询类的两个化身,速率限制防止信令反噬。TCP/IP 四章至此完成——从 IP 分发、TCP 状态机、拥塞插件到无连接协议;下一章看横插在这条链上的过滤框架 Netfilter。