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。