1. 为什么要把数据流讲成“进城记”
先说说我为什么要写这个系列。这几年帮团队排查线上问题,遇到最多的一类场景就是:服务突然变慢、连接超时、延迟抖动,日志里看应用层没报什么异常,数据库负载也不高,最后定位来定位去,问题都出在网络数据在内核协议栈里堵住了。
但真正让我决定写“数据流”系列的原因,是每次问同事一个问题——一个数据包从网卡进来,到业务代码拿到数据,中间到底经过了多少个环节?大部分人能答出“网卡 → 内核 → socket → 应用”,可再往下追问就卡壳了:中断和软中断是干什么的?NAPI是什么?sk_buff在每层怎么传递?为什么有时网卡没丢包,应用却觉得丢了包?这些细节,教科书上也写了,但写得太散,和真实排障之间隔着一层纱。
所以我想用一篇开篇,把整条链路先串起来。我自己给它起了个名字,叫“数据流的进城记”。你想象一下:数据包从边缘小路(网卡)进城,先过城门(驱动与中断),再上城市环线(协议栈处理),经过立交桥完成转向(路由和连接匹配),最后拐进小区、停到住户门口(socket与用户态)。这条路上一旦哪个路口堵了,整个城市(业务系统)都会跟着出问题。
这篇虽然是开篇,但我会把整条数据流路径完整走一遍,该有的函数名、该看的计数器、该避的坑都会提到。后面的文章,再一篇一篇往深了拆。不管你是做后端开发、中间件维护,还是专职的性能调优,这张全景图打底,后面的所有细节才有落点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据流全景:进城之后要过多少道关
先看收方向,也就是数据从网卡进来、最终交到应用手里的完整链条。如果你用strace跟踪一个普通的recv,你看到的是应用层的一个系统调用,但在这背后,内核至少干了这些事:
网卡收到报文,通过DMA直接写进驱动管理的环形缓冲区(ring buffer),然后触发一次硬中断;硬中断里把收包任务扔给软中断后,软中断上下文调用驱动的NAPI轮询函数收包;收上来的sk_buff经过GRO合并后,进入协议栈;IP层做路由判定,TCP层根据五元组找到对应的socket,把数据放进接收队列;最后唤醒等在socket上的进程,数据通过recv拷到用户态缓冲区。
发方向的路径和收方向是镜像的:应用调用send,数据先拷贝进内核发送缓冲区,TCP层按MSS分段、做拥塞控制和窗口管理,然后交给IP层封装、驱动发送,最终从网卡发出去。整条链路看着很长,但要抓住两个核心对象:sk_buff和socket。
sk_buff就像一个集装箱,里面装着真正的数据负载,还挂着一大堆元信息:这个包从哪个网卡进来、目前处理到哪一层、下一层该往哪送、哪个socket是它的目的地。协议栈处理的过程中,这个容器本身不会在各层间来回复制,而是把指针一路传下去,每层做的只是往容器头上加一个头部信息,或者剥掉一个头部信息。很多刚学网络的人以为数据在每一层都要被复制一遍,实际上绝大多数路径上不复制。真正发生拷贝的地方只有两个:网卡DMA把报文从硬件写进内存是一次硬拷贝;recv和send在内核缓冲区和用户态缓冲区之间的搬运是另一次软拷贝。
socket则相当于住户的门牌号。三次握手建好连接之后,内核里就会有一个struct sock和对应的struct socket,数据包到了TCP层,拿着五元组去连接哈希表里一查,就知道该送进哪个门。理解了这两个核心对象,数据流在你眼里就不再是一堆抽象概念,而是一批货从一个集装箱搬到另一个集装箱、最后按门牌号送进家里的过程。
这里还要强调一个经常被忽略的宏观视角:数据的接收路径和发送路径,不是简单地“反着走”。接收路径的瓶颈更常在软中断、GRO、socket接收队列上;发送路径的瓶颈更常在发送缓冲区、拥塞窗口、qdisc排队上。排查问题的时候,先分清是收还是发,能帮你少走一半弯路。
3. 进城第一关:网卡驱动、硬中断与NAPI
3.1 为什么不能每个包都来一次硬中断
数据进入内核的第一关,是网卡驱动。网卡收到一个完整的以太网帧后,会通过DMA把数据直接写进驱动预先分配好的内存区域,也就是环形缓冲
