Linux怎么接受数据包

网卡把数据写入内存,内核通过 NAPI 取出数据,再经过链路层、IP 层、TCP/UDP 层逐层解封装,最后放入 Socket 接收缓冲区并唤醒应用。

一、完整流程

复制代码
网线、光纤或无线信号
→ 网卡
→ DMA
→ RX Ring Buffer
→ 硬件中断
→ NAPI + 软中断
→ 网卡驱动取包
→ sk_buff
→ 网络接口层
→ IP层
→ TCP/UDP层
→ Socket接收缓冲区
→ recv()
→ 用户程序

这正是文章给出的 Linux 收包主线。

1. 网卡接收到数据帧

网卡收到的不是一段单纯的应用数据,而是一个完整的数据帧:

复制代码
| 以太网头 | IP头 | TCP头 | 应用数据 | FCS |

网卡首先进行一些硬件处理,例如:

判断目标 MAC 是否应该接收

校验以太网帧

进行校验和卸载

根据 RSS 选择接收队列

将数据写入对应缓冲区

驱动初始化网卡时,已经创建了 RX Ring。Ring 中的描述符大致记录:

复制代码
这个接收缓冲区在哪块内存
这个缓冲区能够存放多少数据
当前是否已经被网卡使用

2. 通过 DMA 写入内存

网卡通常不会让 CPU 逐字节复制数据,而是通过 DMA 把数据直接写入驱动预先准备的内存:

复制代码
网卡
→ DMA
→ 内存中的接收缓冲区

写完以后,网卡更新 RX Ring 中的描述符,例如标记:

复制代码
这个位置已经收到一个长度为 1500 字节的帧

因此,RX Ring 本身主要是描述符组成的环形队列,真正的数据通常位于描述符指向的内存页或缓冲区中。文章将其概括为"网卡通过 DMA 把网络包写入 Ring Buffer"。

3. 网卡触发硬件中断

数据已经进入内存,但操作系统还不知道,于是网卡触发硬件中断:

复制代码
网卡 → CPU:有新的数据到了

CPU 暂停当前工作,找到该网卡注册的中断处理函数。

硬中断处理函数不会完整解析 TCP/IP,因为硬中断上下文应尽快结束。它主要完成:

复制代码
确认中断
→ 暂时屏蔽或抑制该队列的中断
→ 调用 napi_schedule()
→ 安排后续轮询处理

NAPI 是 Linux 网络栈使用的事件处理机制。设备先通过中断通知主机,主机再调度 NAPI,通过驱动的 poll 方法批量处理数据。

4. NAPI 和软中断批量取包

如果每收到一个包都产生一次完整中断,高流量时 CPU 会频繁进入硬中断,效率很低。

NAPI 采用:

复制代码
第一次有包:中断通知
后续一批包:主动轮询

典型过程是:

复制代码
硬中断
→ 调度 NET_RX_SOFTIRQ
→ 执行 NAPI poll
→ 从 RX Ring 批量读取多个数据包

NAPI 的 poll 方法有一个 budget,限制一次最多处理多少个接收包:

复制代码
还有包,并且达到 budget
→ 以后继续 poll,不立即恢复中断

所有包处理完毕
→ napi_complete_done()
→ 重新启用网卡中断

这种方式把多次硬中断合并成一次通知加批量处理。官方文档指出,NAPI 处理通常运行在软中断上下文,并且中断一般保持屏蔽到轮询完成。

文章把软中断处理统一描述成由 ksoftirqd 完成,这是便于理解的简化。实际软中断也可能在硬中断返回过程中直接执行;只有处理时间过长或任务积压时,才更可能交给 ksoftirqd

5. 驱动构造 sk_buff

驱动从 RX Ring 取出描述符后,会把收到的数据交给 Linux 网络栈,并使用:

复制代码
struct sk_buff

也就是 skb 来描述它。

sk_buff 是 Linux 表示网络包的主要结构,但结构体本身主要保存元数据,真正的数据位于关联的内存缓冲区中。

概念上:

复制代码
sk_buff
├── 数据起始位置
├── 数据长度
├── 接收网卡
├── 协议类型
├── 校验和状态
└── 指向实际数据的指针

驱动可能调用类似:

复制代码
napi_gro_receive()
→ netif_receive_skb()

skb 交给上层网络协议栈。

这里还可能执行 GRO,将同一 TCP 流的多个小包暂时合并成一个较大的 skb,减少协议栈处理次数。

6. 网络接口层解析以太网帧

进入网络接口层后,内核检查:

复制代码
帧是否合法
目标 MAC 是否匹配
上层协议是什么

以太网头中的 EtherType 可以表示:

复制代码
0x0800 → IPv4
0x86DD → IPv6
0x0806 → ARP

确定是 IPv4 后,逻辑上剥离以太网头:

复制代码
接收前:
| MAC头 | IP头 | TCP头 | 数据 |

剥离后:
        | IP头 | TCP头 | 数据 |
        ↑
      skb->data

所谓"剥离"通常不需要复制全部数据,而是向后调整 skb->data 指针。文章正是用这个指针移动解释各层之间如何避免重复复制。

7. IP 层判断包往哪里走

IPv4 数据会进入类似下面的路径:

复制代码
ip_rcv()
→ Netfilter PRE_ROUTING
→ 路由判断

IP 层检查:

IP 版本和首部长度

IP 头校验和

数据包长度

目标 IP

分片是否需要重组

上层是 TCP、UDP 还是 ICMP

随后查询路由,判断数据包的归宿:

复制代码
目标是本机
→ 交给本机传输层

目标不是本机并允许转发
→ 进入 FORWARD 路径

没有路由或不允许转发
→ 丢弃

本机数据通常经过 PRE_ROUTINGLOCAL_IN;转发数据则经过 PRE_ROUTINGFORWARDPOST_ROUTING

8. TCP 根据四元组寻找 Socket

如果 IP 头中的协议字段是 TCP,内核把数据交给 TCP:

复制代码
ip_local_deliver()
→ tcp_v4_rcv()

对于已经建立的 TCP 连接,内核使用四元组查找 Socket:

复制代码
源 IP
源端口
目标 IP
目标端口

例如:

复制代码
客户端:10.0.0.10:53000
服务器:10.0.0.20:8080

对应的连接是:

复制代码
10.0.0.10:53000
→ 10.0.0.20:8080

TCP 接下来会:

校验 TCP 报文

检查序列号

丢弃重复数据

暂存乱序数据

重组连续字节流

处理 ACK

更新接收窗口

必要时发送 ACK

将数据加入 Socket 接收队列

文章将这一步概括为:传输层根据四元组找到对应 Socket,然后把数据放入 Socket 接收缓冲区

9. 如果收到的是 TCP 握手包

如果收到的是一个新的 SYN,而不是已有连接的数据:

复制代码
客户端 → 服务器:SYN

内核会根据目标 IP 和目标端口找到监听 Socket:

复制代码
0.0.0.0:8080

然后:

复制代码
创建半连接状态
→ 返回 SYN + ACK
→ 收到最终 ACK
→ 建立完整连接
→ 放入 accept 队列

应用调用:

复制代码
accept(listen_fd, ...);

就能取得这个已连接 Socket。后续数据包再通过四元组查找对应的连接 Socket

10. 数据进入 Socket 接收缓冲区

TCP 将整理好的数据加入:

复制代码
Socket Receive Buffer

此时数据已经从:

复制代码
以太网帧
→ IP 数据包
→ TCP 报文段
→ TCP 字节流

如果应用正在阻塞等待:

复制代码
recv(fd, buf, size, 0);

内核会唤醒这个进程。应用再次获得 CPU 后,将数据从 Socket 接收缓冲区复制到用户空间:

复制代码
内核 Socket 接收缓冲区
→ 用户程序 buffer

然后 recv() 返回实际读取的字节数。这是文章所说的接收路径中的一次主要数据复制

相关推荐
莫浅子1 小时前
Day 4:USB 2.0 时序与带宽预算
linux·运维·网络
ly76891 小时前
Linux 从入门到实践:系统架构、常用命令、服务管理与故障排查详解
linux·运维·系统架构
新手unity自用笔记1 小时前
unity基于Socket的网络学习
网络·网络协议·学习·unity·c#·游戏引擎
小雪崩2 小时前
嵌入式学习 day27:标准IO
linux·c语言·学习
XUEYUAN52122 小时前
Cloudflare 防护机制深度剖析与跨境数据采集工程化实践
服务器·网络·网络协议·tcp/ip·http
Arnold.Shen2 小时前
Dell Storage SC - 如何发送SupportAssist,并启用安全控制台
linux·安全
青瓦梦滋2 小时前
IP/MAC帧/ARP协议
运维·服务器·网络·网络协议·tcp/ip
wunaiqiezixin2 小时前
MIT 6.S081 xv6 源码精读(Scheduling):sched、scheduler
linux·unix·os·xv6·mit6.s081
书生执笔画浮沉3 小时前
记录一个由浮点运算引起的崩溃
linux
嵌入式阿蔡3 小时前
传感器_02:MPU6050 六轴 IMU 姿态入门——从一串看不懂的数,到一个不漂的角度
网络·stm32·单片机·嵌入式硬件·嵌入式实时数据库