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 语言写的模块化框架。它不像教科书里画得那么规整,但核心路径非常清晰。
我画了一张架构图,你一看就明白:
这张图里,从上到下依次是:
- 用户空间:你的应用程序,比如 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 数据包接收为例,看看它从网线到应用程序到底经历了什么。
接收路径(收包)
- 网卡 DMA:数据帧到达网卡,通过 DMA 直接写入内存中的 Ring Buffer。这个过程不消耗 CPU——嗯,这是高性能的关键。
- 硬中断:网卡触发 IRQ,内核执行中断处理函数。老版本内核在这里直接收包,但中断太多会「活锁」——CPU 光处理中断了,啥也干不了。
- NAPI 轮询:现代内核用 NAPI(New API)。硬中断只负责唤醒软中断,然后关闭中断,切换到轮询模式。我调优时经常调整 gro_flush_timeout 和 napi_defer_hard_irqs,这两个参数能显著降低 CPU 开销。
- 软中断:ksoftirqd 内核线程执行 net_rx_action,从 Ring Buffer 取出 skb,调用网卡驱动的 poll 方法。
- 网络设备层:__netif_receive_skb 被调用,经过 RPS(Receive Packet Steering)分发到不同 CPU 队列。
- 网络层:ip_rcv 处理 IP 头,检查校验和、路由查找。如果启用了 netfilter,PREROUTING 钩子在这里触发。
- 传输层:tcp_v4_rcv 查找对应的 socket,把 skb 放入接收队列。如果 socket 正在 epoll_wait,内核会唤醒它。
- 用户空间:应用程序调用 recvfrom,数据从内核缓冲区拷贝到用户缓冲区。这里有一次内存拷贝——零拷贝技术(比如 splice、sendfile)就是为了省掉这一步。
发送路径(发包)
发送路径相对简单,但有个关键点——它不走中断,而是直接调用驱动发送。流程如下:
- 应用程序调用 sendto/sendmsg,进入系统调用。
- 传输层:tcp_sendmsg 把用户数据切分成 MSS 大小的段,封装 TCP 头,加入发送队列。
- 网络层:ip_output 做路由选择,封装 IP 头。如果启用了 netfilter,POSTROUTING 钩子在这里触发。
- 邻居子系统:通过 ARP 缓存获取下一跳的 MAC 地址。如果缓存中没有,会阻塞并发送 ARP 请求。
- 网络设备层:dev_queue_xmit 把 skb 放入网卡的发送队列,触发驱动发送。
- 网卡通过 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 是贯穿始终的核心载体。