我们每天发出的每一个网络请求、每一个数据包,其实都像一辆“车”:从你的网卡这个“边缘小路”驶入内核这座城市的道路系统,经过链路层的街区、IP层的环岛、TCP层的城市环线,最终抵达应用层那个“市中心”——进程的Socket。而数据流的返回路线,又是另一套“出城”的路径。我花了不少时间把Linux内核TCP协议栈的收包链路从头到尾走读了一遍,越读越觉得,用“进城记”来形容这条数据流再贴切不过:边缘小路是网卡驱动,城市小路是链路层,主干道是IP层,而那座最繁忙、最容易堵车的城市环线,就是TCP协议栈。
这篇是系列的第1篇,我会先带大家把整条数据流的“城市地图”画出来,梳理从网卡到应用层socket背后的核心函数链、关键机制和对应的源码位置。适合正在学内核网络、做网络性能调优、或者单纯想搞懂“一个数据包到底经历了什么”的朋友。看完你会知道该去哪里看代码、用什么工具追踪一条真实的数据流,以及以后遇到“丢包、延迟高、CPU软中断飙高”这类问题,该往哪个方向去查。
1. 从边缘小路进城:先给数据流画一张地图
1.1 数据包的一生(从网卡到应用)——核心函数链一览
我最早看协议栈时,最容易迷失的地方是整个路径上的函数太多了,一进去就晕。后来我习惯先画“主干道函数链”,把每个环节的标志性函数先列出来,再逐个展开。
一个数据包从网线进入本机,到被应用读到,典型路径是这样的:
- 网卡收到数据,DMA写入内存中的ring buffer,触发中断或等待NAPI轮询;
- 网卡驱动注册的
poll函数被调用(比如ixgbe的ixgbe_poll),调用napi_gro_receive; - GRO处理之后,进入
__netif_receive_skb_core,这里会做协议分发,把包交给IP层处理函数ip_rcv; ip_rcv做IP层校验、选项处理,查路由:是本机包还是转发包?本机包走ip_local_deliver;- 如果是TCP报文,
ip_local_deliver会调用tcp_v4_rcv,进入TCP层; tcp_v4_rcv根据四元组查找socket,找到之后调用tcp_v4_do_rcv或tcp_rcv_established;- 数据被放入socket的接收队列,唤醒等待的进程;
- 进程调用
recvmsg,内核最终通过skb_copy_datagram_iter把数据从内核skb拷贝到用户缓冲区。
你看,这就是一条完整的“进城主干道”。我通常把这条链分成四段去看:驱动/NAPI段、链路层/IP段、TCP段、Socket/用户态段。后续所有细节,都是对这四个段落的加深。
1.2 为什么要走读源码?直接看协议栈源码的价值
有人会问:直接用tcpdump抓包不也能看到数据包吗?但抓包看到的是“数据包长什么样”,源码看到的是“数据包在内核里经历了什么”。尤其在排查性能问题时,抓包往往只能确认数据是否到达网卡,但包到了网卡之后,是在驱动层被丢弃、在IP层处理太慢、还是在TCP层因为窗口问题无法递交?这些信息必须深入到内核数据流里才能理解。
我自己的体会是,走读源码最有价值的产出是建立起“性能直觉”。比如当你看到net_rx_action的budget机制,就能理解为什么高压力下ksoftirqd只处理一定配额;当你看到tcp_v4_rcv里的bh_lock_sock和backlog队列,就能明白为什么CPU核多时TCP锁竞争会成为一个显著的瓶颈。这些都不只是面试八股,而是定位线上问题时的地图。
另外,源码是最精确的“文档”。网络协议RFC可能写得抽象,但内核源码把每个行为都落地成了具体的实现。去查Documentation/networking配合源码看,比抱着《TCP/IP详解》空转要直接得多。
1.3 你需要准备什么环境(内核源码、阅读工具、抓包工具)
我建议准备一个干净的Linux环境,用发行版自带的或者手动下载一份匹配的内核源码。几个实用准备:
- 内核源码:Ubuntu/CentOS下可以
apt-get source linux或yumdownloader --source,更省事的是从The Linux Kernel Archives下载对应版本的linux-x.y.z.tar.xz。我习惯把源码解压到/usr/src/linux,用ctags建索引,方便跳转。 - 阅读工具:Vim/VS Code + ctags/gtags足够。用VS Code装个“C/C++”插件,全局搜索函数时会方便很多。不过源码最强大的是“在这里搜调用关系”,比如对着
tcp_v4_rcv用grep -R搜“EXPORT_SYMBOL”可以快速看导出符号。 - 追踪工具:
perf、ftrace、bpftrace,这三个是我在实战中几乎天天用的组合拳。后面第6章会详细写怎么用。 - 网卡与驱动:建议先用常见的e1000e、ixgbe、mlx5这类驱动起步,资料多、代码清晰。我看新手上来直接看四五个网卡驱动会很痛苦,不如盯住一个驱动,把一条路走通。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 进城的第一站:网卡驱动与NAPI
2.1 数据到了网卡之后,是谁把它叫醒的(中断与软中断)
网卡收到数据后,进程还睡得正香,怎么知道有数据来了?答案是硬中断和软中断配合。
传统方式:网卡每收到一个包,就触发一次硬件中断,CPU打断当前任务,执行中断处理程序,把数据从网卡缓存拷贝到内存skb,然后投递到协议栈。这种方式在高吞吐下有个大问题:中断太频繁,CPU大部分时间都花在打断和恢复上,活都干不了。这就是著名的“中断风暴”。
于是有了NAPI:网卡收包时先以硬中断的方式“踢”CPU一脚,告诉它“我有数据了”,但真正的数据搬运,则由CPU在软中断上下文去轮询网卡接收队列,批量处理。这就像进城的车辆不必每辆车都鸣笛,而是先有一辆车报告“车队来了”,然后路口值守人员一次放行一批。
在代码上,硬中断里典型动作是:网卡驱动注册的中断处理函数(例如ixgbe_msix_ring)里调用`napi_schedule
