适合零基础,打通计组(网卡、PCIe、DMA、总线、APIC中断控制器) + OS(特权级、进程PCB、等待队列、硬中断/软中断、NAPI、sk_buff、Socket内核对象) + TCP/IP报文解析、上下协议层关系、路由器转发逻辑、协议栈源码流程 + Socket编程
前置知识回顾:CPU特权模型、系统调用、进程/线程、中断、信号、环形缓冲区、条件变量等待队列、虚拟内存页表

#mermaid-svg-F79RvtW8LKVJTq9N{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-F79RvtW8LKVJTq9N .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-F79RvtW8LKVJTq9N .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-F79RvtW8LKVJTq9N .error-icon{fill:#552222;}#mermaid-svg-F79RvtW8LKVJTq9N .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-F79RvtW8LKVJTq9N .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-F79RvtW8LKVJTq9N .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-F79RvtW8LKVJTq9N .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-F79RvtW8LKVJTq9N .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-F79RvtW8LKVJTq9N .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-F79RvtW8LKVJTq9N .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-F79RvtW8LKVJTq9N .marker{fill:#333333;stroke:#333333;}#mermaid-svg-F79RvtW8LKVJTq9N .marker.cross{stroke:#333333;}#mermaid-svg-F79RvtW8LKVJTq9N svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-F79RvtW8LKVJTq9N p{margin:0;}#mermaid-svg-F79RvtW8LKVJTq9N .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-F79RvtW8LKVJTq9N .cluster-label text{fill:#333;}#mermaid-svg-F79RvtW8LKVJTq9N .cluster-label span{color:#333;}#mermaid-svg-F79RvtW8LKVJTq9N .cluster-label span p{background-color:transparent;}#mermaid-svg-F79RvtW8LKVJTq9N .label text,#mermaid-svg-F79RvtW8LKVJTq9N span{fill:#333;color:#333;}#mermaid-svg-F79RvtW8LKVJTq9N .node rect,#mermaid-svg-F79RvtW8LKVJTq9N .node circle,#mermaid-svg-F79RvtW8LKVJTq9N .node ellipse,#mermaid-svg-F79RvtW8LKVJTq9N .node polygon,#mermaid-svg-F79RvtW8LKVJTq9N .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-F79RvtW8LKVJTq9N .rough-node .label text,#mermaid-svg-F79RvtW8LKVJTq9N .node .label text,#mermaid-svg-F79RvtW8LKVJTq9N .image-shape .label,#mermaid-svg-F79RvtW8LKVJTq9N .icon-shape .label{text-anchor:middle;}#mermaid-svg-F79RvtW8LKVJTq9N .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-F79RvtW8LKVJTq9N .rough-node .label,#mermaid-svg-F79RvtW8LKVJTq9N .node .label,#mermaid-svg-F79RvtW8LKVJTq9N .image-shape .label,#mermaid-svg-F79RvtW8LKVJTq9N .icon-shape .label{text-align:center;}#mermaid-svg-F79RvtW8LKVJTq9N .node.clickable{cursor:pointer;}#mermaid-svg-F79RvtW8LKVJTq9N .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-F79RvtW8LKVJTq9N .arrowheadPath{fill:#333333;}#mermaid-svg-F79RvtW8LKVJTq9N .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-F79RvtW8LKVJTq9N .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-F79RvtW8LKVJTq9N .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F79RvtW8LKVJTq9N .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-F79RvtW8LKVJTq9N .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F79RvtW8LKVJTq9N .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-F79RvtW8LKVJTq9N .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-F79RvtW8LKVJTq9N .cluster text{fill:#333;}#mermaid-svg-F79RvtW8LKVJTq9N .cluster span{color:#333;}#mermaid-svg-F79RvtW8LKVJTq9N div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-F79RvtW8LKVJTq9N .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-F79RvtW8LKVJTq9N rect.text{fill:none;stroke-width:0;}#mermaid-svg-F79RvtW8LKVJTq9N .icon-shape,#mermaid-svg-F79RvtW8LKVJTq9N .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F79RvtW8LKVJTq9N .icon-shape p,#mermaid-svg-F79RvtW8LKVJTq9N .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-F79RvtW8LKVJTq9N .icon-shape .label rect,#mermaid-svg-F79RvtW8LKVJTq9N .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F79RvtW8LKVJTq9N .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-F79RvtW8LKVJTq9N .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-F79RvtW8LKVJTq9N :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 路由器内部处理流程
物理层:接收电信号
链路层:剥离入方向以太网帧头
丢弃二层头、FCS
网络层IP:解析IP头
读取目的IP,查询路由表
修改IP头:递减TTL,重新计算IP头部校验和
选择出口网卡,**完全不动TCP/UDP载荷**
链路层:封装全新出口以太网帧头
填入出口MAC、下一跳MAC,加上FCS
物理层转发比特流
主机A 客户端
完整协议栈:应用‑TCP‑IP‑以太网
路由器
主机B 服务端
完整协议栈:以太网‑IP‑TCP‑应用
路由器只处理IP报文;
内部根本不解析TCP头、端口;看不见Socket;
TCP连接是端到端,两端主机维护,路由器无感知
前言
很多初学者学习网络编程直接背诵socket、bind、listen、accept函数,背诵三次握手四次挥手,但缺少报文解析、协议层交互这一环:内核拿到内存里一串二进制字节,如何切分出以太网头、IP头、TCP头、应用数据;上下协议层如何调用、数据如何传递,路由器中间转发做了哪些改动。
常见认知断层:
- DMA把原始二进制帧放到内存,内核如何识别哪里是头,哪里是载荷?
- 协议头部各个字段如何解析,内核结构体和内存二进制如何对应?
- 报文校验和CRC、IP checksum、TCP checksum分别在哪一层做?
- 上下协议层是谁调用谁?封装、解封装、分用demultiplexing是什么?
- 硬中断为什么不能做繁重报文解析?硬中断、软中断、Signal信号三者本质区别?
- Socket只是一个fd吗?阻塞
recv()休眠原理,和条件变量等待队列同源思想。 - TCP状态机、滑动窗口、重传全部内核完成,不是应用库函数。
- 路由器转发报文,哪些字段变化、哪些字段原样保留,为什么路由器不维护TCP连接。
网络本质:跨主机的进程间通信
- 本机IPC:管道、消息队列、信号量、共享内存,仅限单机;
- Socket:既支持本机UNIX域套接字IPC,又支持跨主机网络通信。
一、硬件层|计算机组成视角:PHY、网卡NIC、PCIe、DMA、APIC中断控制器
1.1 网卡完整硬件构成
网卡(NIC)两大部分:
- PHY芯片:模拟电路,完成电/光信号 ↔ 数字比特流转换,差分信号、载波侦听。网线直连PHY。
- MAC控制器:组装/解析以太网帧,硬件做CRC校验,自带片上FIFO缓存,实现MAC介质访问控制协议。
- DMA控制器 :网卡内置DMA,外设与内存直接搬运,不消耗CPU。
- PCIe接口:挂载PCIe总线;驱动通过MMIO内存映射IO读写网卡寄存器。
MMIO:硬件寄存器映射到CPU物理地址空间,驱动读写内存地址即可操作硬件,不需要独立IO指令。
如果没有DMA:每收到1字节触发一次中断,CPU不停搬运数据。千兆网卡几十万包每秒,CPU完全被IO占满,业务进程无法调度,系统卡死。DMA核心价值就是解放CPU。
1.2 Ring Buffer 接收环形缓冲区
网卡驱动初始化:
内核分配物理内存构建环形队列Ring Buffer,把物理地址告知网卡DMA寄存器。
DMA硬件只能识别物理地址,完全看不见页表、虚拟地址。
- RingBuffer每一项描述符:缓冲区物理地址、空闲/占用标记。
- 生产者:网卡DMA,收到以太网帧,DMA写入空闲buffer,标记为已占用。
- 消费者:内核软中断,读取数据包,处理完毕归还描述符,网卡可复用。
RingBuffer容量固定。数据包速率 > 内核处理速率,环形队列写满,网卡硬件直接丢包。
bash
# 查看网卡硬件层面丢包
ethtool -S eth0 | grep rx_fifo_errors
ethtool -S eth0 | grep overrun
1.3 APIC中断控制器、硬件中断IRQ流程
DMA已经把帧写入主内存,CPU并不知道。网卡通过PCIe向APIC发送IRQ中断请求。
CPU行为:
- CPU正在运行用户进程;收到中断请求;
- 保存全部CPU上下文:通用寄存器、RIP程序计数器、CS段寄存器、栈寄存器;
- 切换特权级进入内核态,跳转到驱动注册好的中断服务程序ISR。
⚠️硬件中断IRQ vs Linux Signal信号
|项目|硬件中断IRQ|进程信号Signal|
|---|---|---|
|来源|硬件设备(网卡、时钟、键盘)|内核/其他进程软件生成|
|上下文|中断上下文,不属于任何进程|依附某一个用户进程|
|打断时机|随时打断CPU,甚至打断内核代码|不能打断内核,必须进程返回用户态才递送 |
|允许睡眠阻塞|禁止阻塞、sleep|可被信号唤醒|
二、操作系统内核层:硬中断、软中断SoftIRQ、NAPI机制
2.1 硬中断上下文严格约束
硬中断优先级高,同类中断会被屏蔽。
硬中断不能做报文解析这种重活,如果在ISR里面解析数据包,中断迟迟不退出,新硬件中断被屏蔽,大量丢包,系统卡顿。
设计原则:硬中断只做紧急极简工作;报文解析、协议处理全部交给软中断延后执行。
网卡ISR硬中断只做两件事:
- 读写网卡寄存器,清除硬件中断标志,防止重复触发中断;
- 提交网络接收软中断
NET_RX_SOFTIRQ,立刻退出硬中断上下文。
2.2 软中断 SoftIRQ 与 ksoftirqd 内核线程
Linux预定义多类软中断:网络收包、网络发包、时钟、tasklet等。
每个CPU拥有独立软中断队列,每个CPU对应内核线程 ksoftirqd/$cpu_id。
软中断两个执行时机:
- 硬中断返回时,如果有待处理软中断,优先执行;
- 软中断任务积压过多,交给后台
ksoftirqd调度执行。
软中断上下文依然不能睡眠、不能访问用户空间内存 。
软中断的核心工作:遍历RingBuffer,取出原始帧,分配
sk_buff,开始逐层报文解析,递交内核协议栈。
2.3 NAPI 机制,解决中断风暴
高吞吐场景,每秒数万数据包,如果每包触发一次硬件中断,CPU大量消耗在进出中断上下文,叫中断风暴。
NAPI流程:
- 第一个数据包到达,触发一次硬件中断;
- ISR中关闭网卡硬件接收中断;
- 软中断循环轮询RingBuffer,批量处理全部待处理数据包(在这里批量做报文解析);
- RingBuffer为空,重新打开网卡硬件中断,等待下一次事件。
低流量靠中断通知;高流量轮询批量解析报文,兼顾延迟与吞吐。现代网卡驱动默认开启NAPI。
2.4 sk_buff:承载报文的核心内核结构体
头文件:include/linux/skbuff.h
所有收发数据包全部使用sk_buff管理。sk_buff不复制多份报文,依靠指针偏移完成报文解析,减少内存拷贝。
关键成员:
head/data/tail/end:指向报文缓冲区;mac:以太网帧头指针;network:IP头指针;transport:TCP/UDP传输层头指针;len:报文总有效长度;next:链表指针,多个sk_buff组成队列。
报文解析的本质:移动这几组指针,而不是拷贝内存数据。
原始帧全部存放在一块内存缓冲区,sk_buff内部指针不断向后偏移,剥离各层头部,拿到上层载荷Payload。
sk_buff生命周期:软中断分配 → 逐层报文解析递交协议栈 → payload拷贝进socket缓冲区 → slab分配器释放对象。
slab:内核小对象内存池,sk_buff频繁创建销毁,slab提升性能。
三、完整报文解析流程
内存中,网卡DMA拷贝过来的原始二进制帧排布顺序:
[以太网帧头] [IP头] [TCP头] [应用Payload数据] [FCS‑CRC校验]
注意:网卡硬件已经完成以太网FCS‑CRC校验,如果校验失败,网卡直接丢弃帧,不会递交给内核,内核软件看不到坏帧。
3.1 链路层:以太网报文解析 eth_type_trans()
软中断拿到sk_buff,调用eth_type_trans()做以太网帧解析。
以太网帧头结构体简化:
c
// 内核 include/uapi/linux/if_ether.h
struct ethhdr {
unsigned char h_dest[6]; // 目标MAC
unsigned char h_source[6]; // 源MAC
__be16 h_proto; // 上层协议类型,网络字节序大端
};
解析步骤:
- sk_buff->mac 指向缓冲区起始位置,即以太网头首地址;
- 读取
h_proto字段:0x0800:上层是IPv4报文;0x0806:上层ARP报文;
skb_pull(skb, ETH_HLEN):把sk_buff->data指针向后移动14字节(以太网头长度) 。- 指针移动完成:
skb->network现在指向IP头部起始地址;mac指针保留以太网头位置。
- 指针移动完成:
- 返回上层协议编号,内核据此投递到IP层或ARP模块。
skb_pull()只是修改指针,没有内存拷贝! 这是Linux报文解析的核心优化。
3.2 网络层:IP报文解析 ip_rcv()
ip_rcv()是IP层入口函数,接收来自链路层的sk_buff,此时skb->network指向IP头。
IPv4头部简化结构体:
c
// include/uapi/linux/ip.h
struct iphdr {
__u4 ihl:4; // IP头长度,以4字节为单位
__u4 version:4; // IP版本,4代表IPv4
__u8 tos; // 服务类型
__be16 tot_len; // IP报文总长度(IP头+IP payload)
__be16 id; // 分片ID
__be16 frag_off; // 分片偏移
__u8 ttl; // TTL生存时间
__u8 protocol; // 上层协议:6=TCP,17=UDP,1=ICMP
__be16 check; // IP头部校验和
__be32 saddr; // 源IP
__be32 daddr; // 目的IP
};
IP层解析完整步骤 ip_rcv():
- 读取version,校验是否IPv4;读取ihl计算IP头真实长度;
- 校验IP头部checksum,软件计算头部校验和,和报文check字段对比;校验失败直接丢弃sk_buff;
- 读取
tot_len,校验报文实际长度合法性; - 判断IP分片:如果frag_off表明这是分片包,放入内核分片重组缓冲区
ipq;等待全部分片到达,重组为完整IP报文,再向上递交;分片超时未全部到达直接丢弃; - 路由查找,出现两个关键分支:
- 目的IP为本机网卡IP(普通主机) :本机接收,继续解析;
skb_pull(skb, iphdrlen):data指针向后移动IP头长度;此时skb->transport指向TCP/UDP头部;根据protocol字段分发:6交给TCP模块tcp_v4_rcv(),17交给UDP模块udp_rcv()。 - 目的IP不是本机IP(路由器转发分支) :进入
ip_forward(),不再向上递交传输层。
- 目的IP为本机网卡IP(普通主机) :本机接收,继续解析;
- 转发分支逻辑:TTL‑1;TTL为0丢弃报文回复ICMP;重新计算IP头部校验和;查询路由表选择出口网卡;如有必要执行IP分片;ARP获取下一跳MAC;调用
skb_push()重新封装全新以太网帧头,交给驱动发送。
IP checksum只校验IP头部,不校验后面TCP/UDP载荷。传输层自己维护独立校验和。
3.3 传输层TCP报文解析 tcp_v4_rcv()
此时sk_buff->transport指向TCP头部。
TCP头部简化:
c
// include/uapi/linux/tcp.h
struct tcphdr {
__be16 source; //源端口
__be16 dest; //目的端口
__be32 seq; //序列号
__be32 ack_seq; //确认号
__u16 doff:4; //TCP头长度,4字节单位
__u16 res1:6;
__u16 fin:1,syn:1,rst:1,psh:1,ack:1,urg:1; //标志位
__be16 window; //滑动窗口大小
__be16 check; //TCP校验和(伪头+TCP头+TCP数据)
__be16 urg_ptr;
};
TCP解析流程tcp_v4_rcv():
- 根据目的端口,在内核查找对应的socket对象;找不到socket直接丢弃报文;
- 解析TCP头,读取doff得到TCP头部长度;
- 校验TCP checksum;TCP校验和包含IP伪头部,防止错路由;校验失败丢弃;
- 解析标志位 SYN / ACK / FIN / RST,驱动内核TCP状态机(LISTEN、SYN_RECV、ESTABLISHED、TIME_WAIT等);
- 处理序列号、确认号、滑动窗口、SACK;处理重传队列;
skb_pull(skb, tcphdr_len),指针跳过TCP头部;剩下的内存就是应用层Payload业务数据;- 将Payload数据写入socket内核接收缓冲区;
- 唤醒socket等待队列上面休眠阻塞的进程。
关键点:TCP头部、IP头部、以太网头部全部被sk_buff指针剥离,payload留在socket缓冲区,协议头本身不会拷贝到用户空间。用户recv拿到的只有应用Payload。
UDP报文解析逻辑类似:解析UDP头,校验UDP checksum,payload放入UDP socket接收队列。
四、上下协议层完整关系
4.1 分层核心契约:下层为上层提供服务,上层不关心下层实现
原则:每一层只和自己直接相邻的上下层打交道;本层完全不理解更高层报文语义。
- 上层:服务使用者,把数据交给下层,请求下层完成传输服务;上层不需要知道下层硬件、路由、MAC怎么实现。
- 下层:服务提供者 ,收到上层过来的数据,把上层全部数据当做自己的
Payload(有效载荷),加上自己协议头,继续往下传递。下层看不懂上层头部的含义。
通俗类比寄快递:
- 你(上层应用)把信件交给快递网点(传输层);信件就是Payload。
- 快递网点打包贴单(加TCP头)交给转运中心(IP层);
- 转运中心贴路由标签(加IP头)交给末端派送(链路层);
- 派送打包信封写MAC地址(以太网头)交给运输车辆(物理层比特流)。
快递转运中心不会拆开你的信件阅读内容,只处理自己的快递面单。对应网络:IP层不会解析TCP头,TCP层不会解析HTTP内容。
两个关键术语
- 封装 Encapsulation(发送方向,自上而下)
上层交付的数据,下层在数据前面追加本层协议头部(链路层额外追加FCS尾部),把上层完整数据当作payload。
报文格式:
本层头部 + 上层完整报文(payload)
- 解封装 + 分用 Demultiplexing(接收方向,自下而上)
接收机器从网卡收到完整帧: - 当前层读取自己这一层头部,完成本层工作(校验、寻址);
- 剥掉自己的头部,剩下全部内容当做上层payload;
- 读取头部内的「上层协议标识字段」,把payload交付给对应的上层协议模块 ,这个分发动作叫分用(demultiplexing)。
重点:分用,就是上层多路数据到达下层,下层根据标记投递给正确上层模块。
4.2 各层PDU名称(协议数据单元,面试高频)
同一块二进制数据,站在哪一层看,名字不一样:
| 层级 | PDU名字 | 头部关键字段(用于分用,SAP服务访问点) |
|---|---|---|
| 应用层 | Data应用数据 | 无内核协议头 |
| 传输层(TCP/UDP) | Segment段(TCP) / Datagram(UDP) | 源端口、目的端口(SAP:端口号,区分本机进程) |
| 网络层(IP) | IP数据报Datagram | protocol字段:6=TCP,17=UDP,1=ICMP(SAP:IP+protocol) |
| 链路层(以太网) | Frame帧 | h_proto类型字段:0x0800=IPv4,0x0806=ARP(SAP:ether‑type) |
| 物理层 | Bit比特流 | 无头部,0/1电信号 |
SAP服务访问点:就是上下层之间约定好的标记字段,下层靠这个标记把包交给正确上层模块。
分用完整链路(接收端内核)
- 链路层读到以太网头
h_proto = 0x0800→ 把payload上交IP模块;如果0x0806上交ARP模块。 - IP层解析IP头
protocol=6→ payload上交TCP模块;17上交UDP模块;1上交ICMP。 - TCP模块解析TCP头目的端口,找到本机对应socket,payload写入socket接收缓冲区,唤醒进程。
如果没有这些SAP标记字段,内核收到报文根本不知道该交给哪个上层协议处理。
4.3 Linux内核中,上下层如何传递数据(sk_buff视角)
整个内核协议栈,全程只传递同一个sk_buff对象,不拷贝报文数据,只移动内部指针,这是Linux网络高性能根源。
✅接收路径(自底向上 · 解封装)
原始帧存入sk_buff缓冲区,data一开始指向以太网帧头。
- 链路层eth_type_trans()
skb_pull(skb,14);data指针向后移动14字节,跳过以太网头;此时data指向IP头,剩下全部内容就是IP层Payload,上交ip_rcv()。 - 网络层ip_rcv()
读取IP头,做完校验、分片重组;- 本机接收:
skb_pull(skb, iphdr_len),data继续向后跳过IP头部;此时data指向TCP头,payload上交tcp_v4_rcv()。 - 路由器转发:进入
ip_forward,不走skb_pull向上递交传输层,后续skb_push重新构造二层头。
- 本机接收:
- 传输层tcp_v4_rcv()
解析TCP头;skb_pull(skb,tcphdr_len),data跳过TCP头;此时data直接指向应用层Payload业务数据,payload写入socket接收缓冲区。
⚠️协议头部并没有从内存删除,只是
data指针跨过头部,不再作为本层有效数据;mac/network/transport指针还保存各层头部的位置,供需要时读取。没有任何内存拷贝,只是指针移动。
✅发送路径(自上而下 · 封装)
send系统调用,用户数据拷贝进sk_buff,应用数据放在缓冲区靠后的位置;缓冲区头部预留足够headroom空间,用来向前填充各层头部。
- 传输层:
skb_push(skb,tcp_header_len),data指针向前移动,腾出空间写入TCP头部; - 网络层:继续
skb_push()向前移动data,写入IP头部; - 链路层:继续
skb_push()向前移动data,写入以太网头部; - sk_buff交给驱动、DMA发送出去。
skb_pull():data向后移(接收,剥头);skb_push():data向前移(发送,加头),全程不拷贝报文内容。
4.4 上下层之间,什么信息会传递、什么绝不传递
下层传给上层的内容
- sk_buff整体对象(包含完整缓冲区、各个层指针)
- 本层解析出来的控制元信息 :例如IP层把源IP、目的IP传给传输层;链路层把接收网卡设备
net_device*传递上去。 - payload二进制数据。
❗下层不会把自己的头部内存拷贝一份交给上层 ,上层通过sk_buff保存的
transport/network/mac指针,可以回头读取下层头部,但payload传递只移动data指针。
上层绝对不能传给下层的东西
- 上层业务语义(比如HTTP GET请求内容);下层完全不感知应用业务。
- 上层的socket、进程PCB;链路层/IP层完全不知道什么socket,socket对象只有传输层才会关联。
举例:IP协议完全不知道什么TCP状态机;TCP完全不知道什么MAC地址。分层隔离,解耦。
4.5 路由器设备:只处理物理‑链路‑网络层,不碰传输层、应用层
路由器收到IP报文:
- 链路层解封装,剥掉入方向以太网帧头;payload上交IP层;
- IP层读取IP头部,TTL递减,重新计算IP头部checksum,查询路由表,决定转发出口;完全不解析TCP/UDP头部,TCP载荷原封不动保留;
- 在出口链路层重新封装全新以太网帧头,转发出去。
路由器只会处理物理层、链路层、网络层;不会到达传输层。这体现分层:IP层只负责寻址转发,不关心上层是TCP还是UDP。
一句话记忆:IP报文端到端不变(TTL除外);以太网帧每一跳全部重写。
| 报文部分 | 主机A发出 | 路由器收到 | 路由器转发出去 | 服务器B收到 |
|---|---|---|---|---|
| 源MAC | A‑MAC | A‑MAC | 路由器出口MAC | 路由器出口MAC |
| 目的MAC | 路由器入接口MAC | 路由器入接口MAC | 服务器B‑MAC | B‑MAC |
| 源IP | A‑IP | A‑IP | A‑IP | A‑IP |
| 目的IP | B‑IP | B‑IP | B‑IP | B‑IP |
| TCP端口 | 源端口:目的端口 | 不变 | 不变 | 不变 |
✔️IP全程不变;
✔️TCP端口、TCP报文内容全程不变;
❗MAC地址每一跳发生改变。
NAT路由器是特例:NAT在路由器IP层扩展,会修改IP地址、端口,不属于标准IP路由器。普通三层路由器不做NAT。
Linux普通机器打开转发即可变成路由器:
bash
echo 1 > /proc/sys/net/ipv4/ip_forward
开启后收到非本机目的IP报文,走ip_forward转发分支,不再上送TCP/UDP传输层。
4.6 traceroute原理(路由器TTL机制)
- 客户端发出TTL=1的UDP包;
- 第一台路由器收到,TTL减1等于0,丢弃报文,回复ICMP time‑exceed给源主机;
- 客户端拿到ICMP,得到第一跳路由器IP;
- 发送TTL=2,到达第二台路由器TTL变为0;以此类推,直到抵达目标主机。
traceroute依靠路由器修改TTL、回复ICMP实现,全程路由器不会触碰传输层载荷。
4.7 硬件中断、软中断、协议栈分层关系
- 网卡硬件 + 驱动属于链路层实现;硬中断只是硬件通知机制,不属于协议层;
- 硬中断只做标记软中断,不做任何报文解析;
ksoftirqd软中断上下文启动链路层解析,逐层向上调用IP、TCP;- socket、等待队列属于传输层向上对应用户态的接口。
信号Signal 和协议栈分层无关;信号是内核给进程的软件通知,网络包递送靠协议栈+等待队列,二者机制不同。
4.8 面试易错点整理
-
TCP能不能看见MAC地址?
不能。TCP属于传输层,MAC是链路层头部;TCP代码不会直接读取mac头,sk_buff虽然保存mac指针,但TCP协议逻辑不使用MAC。
-
IP层会修改TCP payload内容吗?
不会;IP只修改IP头部;payload完整交付上层;IP分片只是切割整个IP报文,不会修改TCP段内部字节。
-
接收时,协议头会不会拷贝到用户缓冲区(recv拿到的数据)?
不会! 内核逐层
skb_pull跳过全部以太网、IP、TCP头部;copy_to_user只把最末尾的应用payload拷贝给用户进程;应用程序recv完全看不到底层任何协议头部。 -
UDP和TCP为什么共用IP层?
IP层payload里通过
protocol字段做分用;同一个IP模块,根据数字6/17分别投递给TCP/UDP。 -
为什么sk_buff要保存mac、network、transport三套指针?
当已经
skb_pull移动data指针之后,上层协议依然可以回头读取下层头部信息(例如TCP需要读取IP伪头部做TCP校验和,就要回头访问IP头)。 -
路由器转发报文,会修改TCP校验和吗?
不会。路由器只修改IP头部TTL和IP头部checksum;TCP校验和包含IP伪头部,但路由器不解析TCP,不会动TCP checksum。端主机收到报文自行校验TCP checksum。
-
TCP连接经过路由器,路由器保存连接状态吗?
标准三层路由器不保存任何TCP连接状态;只有NAT设备会维护五元组会话表。
-
为什么MAC地址不能跨互联网通信?
MAC只用于同一局域网二层;跨网段必须依靠IP路由,每一跳MAC被重写。公网数据包,源MAC永远是离你最近的路由器出口MAC,不是你本机MAC。
五、TCP/IP协议栈分层完整收包发包链路
| TCP/IP模型 | 实现位置 | 功能简述 |
|---|---|---|
| 应用层 | 用户态进程 | HTTP业务逻辑,调用Socket系统调用recv/send |
| 传输层(TCP/UDP) | 内核态 | 端口、报文解析、TCP状态机、滑动窗口、重传;UDP无连接 |
| 网络层(IP) | 内核态 | IP报文解析、校验和、分片重组、路由、ICMP、ip_forward转发 |
| 链路层 | 内核驱动 | 以太网帧解析、MAC、RingBuffer、ARP解析、硬件CRC |
| 物理层 | 硬件网卡PHY | 光电信号、DMA、硬件中断 |
TCP三次握手、四次挥手、超时重传、拥塞控制全部内核实现,应用看不到TCP状态机。
5.1 收包完整链路(网线→用户buffer)
- PHY接收电信号,组装以太网帧存入网卡FIFO;DMA搬运帧到内核RingBuffer物理内存。
- 网卡发起硬件中断IRQ,CPU保存寄存器,进入内核态执行ISR硬中断。
- ISR清除中断标记,提交
NET_RX软中断,退出硬中断。 - ksoftirqd执行软中断,从RingBuffer取出帧,分配sk_buff。
- 链路层eth_type_trans():解析以太网帧头,skb_pull剥离二层头,指向IP头。
- 网络层ip_rcv():解析IP头部,校验checksum,IP分片重组;路由判断
- 本机接收:
skb_pull剥离IP头,交给tcp_v4_rcv()传输层; - 路由器转发:调用
ip_forward,修改TTL,重新封装二层帧,直接发送,永远不会走到传输层。
- 本机接收:
- 传输层tcp_v4_rcv():解析TCP头,校验TCP checksum,驱动TCP状态机;skb_pull剥离TCP头,得到Payload;写入socket接收缓冲区;唤醒等待队列进程。
- 用户程序调用
recv()系统调用陷入内核态。 copy_to_user():socket内核缓冲区payload拷贝到用户进程虚拟地址空间buffer。
10.系统调用返回用户态,应用拿到业务数据。
两次拷贝:
1.DMA:网卡硬件→内核RingBuffer内存(硬件搬运,无CPU消耗)
2.copy_to_user:socket内核缓冲区→用户内存(CPU拷贝)
5.2 发包完整链路 send() → 网线
发包是解析的逆过程:在sk_buff前面填充各层协议头。
- 用户调用send()系统调用,用户态切内核态;
copy_from_user()用户buffer拷贝到socket发送缓冲区。
send返回成功≠对方收到数据,仅仅拷贝到本机内核发送缓冲区,后续内核异步发送。
- TCP模块构造TCP头部(序列号、窗口、标志位);
- IP层构造IP头部;计算IP头部checksum;
- 链路层ARP解析得到目标MAC,在sk_buff前面填充以太网帧头;
- sk_buff送入QDisc排队队列;
- 驱动取出sk_buff填入发送RingBuffer;DMA把报文从内存搬运到网卡FIFO;
- PHY转为电信号发送网线。
六、Socket内核对象详解,阻塞IO休眠原理
6.1 Socket是什么
Linux一切皆文件。调用socket()系统调用,内核创建struct socket内核对象,创建file文件结构,返回fd给用户进程。
用户只能操作fd,不能直接访问socket内核对象、缓冲区、TCP状态机;全部操作依靠系统调用陷入内核。
struct socket内核对象核心成员:
- 接收缓冲区、发送缓冲区(内核页内存);
- 四元组:源IP、源端口、对端IP、对端端口;
- TCP状态机:LISTEN、SYN_RECV、ESTABLISHED、TIME_WAIT;
- 等待队列wait_queue_head_t:阻塞IO休眠的进程PCB挂载在这里;
- proto_ops协议操作函数集,对应TCP/UDP处理函数。
阻塞recv底层流程
cpp
recv(connfd, buf, len, 0);
- 用户态执行recv,触发系统调用进入内核。
- 检查socket接收缓冲区是否存在Payload数据:
- ✅有数据:
copy_to_user拷贝payload到用户buffer,系统调用返回; - ❌缓冲区为空:把当前进程PCB挂载socket等待队列;进程状态改为
TASK_INTERRUPTIBLE可中断睡眠;调度器切换其他进程运行,让出CPU。
- ✅有数据:
- 数据包到达内核,完成全部报文解析,payload写入socket缓冲区;内核唤醒等待队列上休眠进程。
- 进程重新调度运行,完成recv拷贝,返回用户态。
pthread_cond_wait 用户态线程等待队列;socket recv阻塞是内核态进程等待队列,底层思想同源:等待队列 + 睡眠 + 事件唤醒。
6.2 listen半连接队列、全连接队列
listen(fd, backlog)
- 半连接队列(SYN队列):收到SYN,状态SYN_RECV,尚未完成三次握手;
- 全连接队列(accept队列):三次握手全部完成ESTABLISHED,等待accept取走;
backlog代表全连接队列最大长度。两个队列满,新SYN直接丢弃,客户端connect失败。
bash
sysctl net.core.somaxconn
6.3 网络字节序(大小端)
x86 CPU小端;TCP/IP协议报文一律使用大端网络字节序。端口号、IP必须转换。
cpp
htons() // uint16_t 主机序→网络序(端口)
htonl() // uint32_t 主机序→网络序(IPv4)
ntohs()
ntohl()
经典坑:端口直接填数字,不调用htons,服务端无法接收连接。
6.4 Socket系统调用清单(关联内核行为)
cpp
// 内核分配struct socket对象,返回fd
int socket(int domain, int type, int protocol);
// bind:绑定IP+端口到socket内核对象
int bind(int fd, struct sockaddr* addr, socklen_t len);
// listen:创建半/全连接队列,socket置为监听状态
int listen(int fd, int backlog);
// accept:阻塞从全连接队列取出完成握手连接,返回新connfd
int accept(int listenfd, struct sockaddr*, socklen_t*);
// connect:客户端,内核组装SYN报文发起三次握手
int connect(int fd, struct sockaddr*, socklen_t len);
send() / recv(); // copy_from_user / copy_to_user,读写socket缓冲区payload
close(fd); // 减少引用计数,释放socket资源,触发四次挥手
七、实操命令,观测内核网络与报文
bash
# 网卡硬件
ip addr
lspci | grep Ethernet
# 路由表
ip route
# socket缓冲区内核参数
sysctl -a | grep net.core.rmem
sysctl -a | grep net.core.wmem
# 查看TCP socket状态,替代netstat
ss -anit
# tcpdump抓包,可以肉眼观察以太网头、IP头、TCP头完整报文
tcpdump -i lo port 8888 -nn -X
# 观察软中断ksoftirqd CPU占用
top
# 开启Linux主机IP转发(充当简易路由器)
echo 1 > /proc/sys/net/ipv4/ip_forward
tcpdump抓包看到的二进制,就是DMA拷贝到内存、内核sk_buff拿到的原始帧内容。
八、高频易错点(结合报文解析底层)
-
recv返回长度小于传入len?
TCP字节流,没有消息边界。socket缓冲区payload不足,recv直接返回现有字节;协议头部已经被内核全部剥离,用户看不到任何以太网/IP/TCP头部,只能拿到payload。业务层必须自己做报文分包。
-
send返回成功代表对方应用收到数据?
错误。send成功只是payload拷贝本机内核发送缓冲区;TCP ACK只是内核协议栈确认,应用程序不会收到通知。本机断电,缓冲区未发出数据直接丢失。
-
CRC、IP checksum、TCP checksum分别在哪一层?
- FCS‑CRC:网卡硬件做,帧错误网卡直接丢弃,内核看不见坏帧。
- IP checksum:内核IP层软件校验,仅校验IP头部。
- TCP checksum:内核传输层校验,包含伪头部、TCP头、TCP payload。
-
DMA搬运数据,为什么ksoftirqd依然CPU很高?
DMA只管网卡与内存搬运;以太网/IP/TCP报文解析、校验和计算、分片重组、copy_to_user拷贝payload全部消耗CPU,这就是ksoftirqd占用CPU的来源。
-
TIME_WAIT:主动关闭方内核socket状态,等待2MSLS,确保对端收到FIN。属于内核对象资源,不是用户进程内存。
九、进阶学习路线
- 手写阻塞TCP服务端、客户端,掌握基础API;
- IO模型:阻塞IO、非阻塞IO、select/poll、epoll深度解析(epitem、内核等待队列)
- TCP进阶:滑动窗口、拥塞控制、RTT、SACK、CLOSE_WAIT;
- 零拷贝 sendfile / mmap,减少内存拷贝;
- Netfilter iptables;
- DPDK 用户态网卡:绕开内核协议栈,用户程序直接操作RingBuffer,自己完成全部报文解析。
