1. 网络协议栈概述:从分层模型到内核流转

做网络调优这么多年,我越来越觉得——搞懂协议栈的分层逻辑,是入门的第一道坎,也是最后一道坎。很多人调参数调了半天,效果出不来,说白了就是没搞明白数据包在内核里到底走了哪条路。

这一章,咱们先把地基打牢。我会从两个经典的分层模型讲起,然后带你看看 Linux 内核里网络协议栈长什么样,最后把数据包的完整旅程走一遍。嗯,这部分内容偏理论,但我会尽量用实际经验串起来。

1.1 OSI 七层模型 vs TCP/IP 四层模型

先问个问题:为什么要有分层?

我个人习惯把网络分层比作「快递系统」。你寄包裹,不需要关心飞机怎么飞、卡车怎么走,你只需要填好地址、贴上单子。每一层只干自己的事,层与层之间通过标准接口通信。这样好处很明显——某一层出了问题,换掉它就行,不影响其他层。

OSI 七层模型是国际标准化组织(ISO)搞的参考模型,理论上很完美,但实际用起来太复杂。TCP/IP 四层模型才是互联网的事实标准。我刚开始学网络时,对着七层模型背了半天,后来发现工作中根本用不到会话层和表示层——它们的功能被应用层吞掉了。

OSI 七层模型 TCP/IP 四层模型 典型协议/设备 核心职责
应用层 应用层 HTTP, DNS, TLS 为用户提供网络服务接口
表示层 SSL/TLS 加密、数据压缩 数据格式转换、加密解密
会话层 NetBIOS, RPC 建立、管理、终止会话
传输层 传输层 TCP, UDP, SCTP 端到端可靠传输、流量控制
网络层 网络层 IP, ICMP, ARP 路由寻址、分组转发
数据链路层 网络接口层 以太网, VLAN, PPP 帧封装、介质访问控制
物理层 网线、光纤、无线电 比特流传输、电气特性
我的经验: 做性能调优时,重点关注传输层和网络层就够了。应用层的问题通常靠抓包分析,链路层的问题找网工。别在七层模型上钻牛角尖——它是个教学工具,不是工程规范。

1.2 Linux 网络协议栈架构

Linux 内核的网络协议栈,说白了就是一套 C 语言写的模块化框架。它不像教科书里画得那么规整,但核心路径非常清晰。

我画了一张架构图,你一看就明白:

Linux 网络协议栈架构 Linux 网络协议栈架构 用户空间 (User Space) Socket API (sys_socket / sys_sendto / sys_recvfrom) 系统调用层 (System Call Interface) 协议无关层 (Protocol Agnostic Layer) struct sock / inet_sock / socket 管理 传输层 (TCP/UDP) tcp_v4_rcv / udp_rcv / tcp_sendmsg 传输层 (SCTP/DCCP) sctp_rcv / dccp_rcv 网络层 (Network Layer) ip_rcv / ip_output / netfilter (iptables/nftables) 邻居子系统 (ARP/NDISC) 路由子系统 (FIB/路由表) 网络设备层 (Network Device Layer) dev_queue_xmit / netif_receive_skb / NAPI 数据流方向:用户空间 → 系统调用 → 协议无关层 → 传输层 → 网络层 → 邻居/路由 → 设备层 → 网卡

这张图里,从上到下依次是:

  • 用户空间:你的应用程序,比如 Nginx、Redis,通过 Socket API 跟内核打交道。
  • 系统调用层:sys_socket、sys_sendto、sys_recvfrom 这些入口函数。嗯,这里有个小坑——每次系统调用都有上下文切换开销,高并发场景下这个开销很可观。
  • 协议无关层:管理 struct sock 和 struct socket。你想想看,TCP 和 UDP 的 socket 操作大部分逻辑是共用的,这一层就是抽离出来的公共部分。
  • 传输层:TCP 的可靠性、UDP 的快速、SCTP 的多流——各干各的。
  • 网络层:IP 路由、分片重组,还有 netfilter 钩子(iptables/nftables 就在这里挂载)。
  • 邻居子系统和路由子系统:ARP 找 MAC 地址,FIB 找下一跳。
  • 网络设备层:抽象了所有网卡驱动,统一用 dev_queue_xmit 发送、netif_receive_skb 接收。

核心要点: 每一层都操作一个数据结构——sk_buff(简称 skb)。这个结构体贯穿整个协议栈,从网卡驱动到用户空间,数据包的所有信息都挂在它上面。调优时,很多参数本质上就是在控制 skb 的分配、克隆、释放行为。

1.3 数据包在内核中的流转路径

理论讲完了,咱们走一遍实际路径。我以一次 TCP 数据包接收为例,看看它从网线到应用程序到底经历了什么。

接收路径(收包)

  1. 网卡 DMA:数据帧到达网卡,通过 DMA 直接写入内存中的 Ring Buffer。这个过程不消耗 CPU——嗯,这是高性能的关键。
  2. 硬中断:网卡触发 IRQ,内核执行中断处理函数。老版本内核在这里直接收包,但中断太多会「活锁」——CPU 光处理中断了,啥也干不了。
  3. NAPI 轮询:现代内核用 NAPI(New API)。硬中断只负责唤醒软中断,然后关闭中断,切换到轮询模式。我调优时经常调整 gro_flush_timeout 和 napi_defer_hard_irqs,这两个参数能显著降低 CPU 开销。
  4. 软中断:ksoftirqd 内核线程执行 net_rx_action,从 Ring Buffer 取出 skb,调用网卡驱动的 poll 方法。
  5. 网络设备层:__netif_receive_skb 被调用,经过 RPS(Receive Packet Steering)分发到不同 CPU 队列。
  6. 网络层:ip_rcv 处理 IP 头,检查校验和、路由查找。如果启用了 netfilter,PREROUTING 钩子在这里触发。
  7. 传输层:tcp_v4_rcv 查找对应的 socket,把 skb 放入接收队列。如果 socket 正在 epoll_wait,内核会唤醒它。
  8. 用户空间:应用程序调用 recvfrom,数据从内核缓冲区拷贝到用户缓冲区。这里有一次内存拷贝——零拷贝技术(比如 splice、sendfile)就是为了省掉这一步。
我曾经踩过的坑: 有一次线上服务延迟飙升,查了半天发现是 Ring Buffer 太小,网卡丢包了。用 ethtool -S eth0 一看,rx_missed_errors 暴涨。调大 rx_ring 参数后,问题解决。所以,排查网络问题时,先看网卡统计——硬件丢包和内核丢包是两码事。

发送路径(发包)

发送路径相对简单,但有个关键点——它不走中断,而是直接调用驱动发送。流程如下:

  1. 应用程序调用 sendto/sendmsg,进入系统调用。
  2. 传输层:tcp_sendmsg 把用户数据切分成 MSS 大小的段,封装 TCP 头,加入发送队列。
  3. 网络层:ip_output 做路由选择,封装 IP 头。如果启用了 netfilter,POSTROUTING 钩子在这里触发。
  4. 邻居子系统:通过 ARP 缓存获取下一跳的 MAC 地址。如果缓存中没有,会阻塞并发送 ARP 请求。
  5. 网络设备层:dev_queue_xmit 把 skb 放入网卡的发送队列,触发驱动发送。
  6. 网卡通过 DMA 从内存读取数据,发送到物理链路。

调优小技巧: 发送路径上,TCP 的 TSO(TCP Segmentation Offload)和 GSO(Generic Segmentation Offload)能把大块数据一次性交给网卡,让网卡硬件做分片。这能大幅降低 CPU 负载。用 ethtool -k eth0 查看 offload 特性是否开启。我习惯在服务器上默认开启 TSO/GSO,除非遇到特定兼容性问题。

1.4 本章小结

这一章我们覆盖了三个核心内容:

  • OSI 七层模型和 TCP/IP 四层模型的对比——记住,TCP/IP 是实战标准,OSI 是理论参考。
  • Linux 网络协议栈的分层架构——从用户空间到网卡驱动,每一层都有明确的职责和数据结构。
  • 数据包的完整流转路径——收包走中断+NAPI,发包走直接调用。sk_buff 是贯穿始终的核心载体。