Linux网络基础(一):从计算机组成、操作系统到Socket

适合零基础,打通计组(网卡、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头、应用数据;上下协议层如何调用、数据如何传递,路由器中间转发做了哪些改动。

常见认知断层:

  1. DMA把原始二进制帧放到内存,内核如何识别哪里是头,哪里是载荷?
  2. 协议头部各个字段如何解析,内核结构体和内存二进制如何对应?
  3. 报文校验和CRC、IP checksum、TCP checksum分别在哪一层做?
  4. 上下协议层是谁调用谁?封装、解封装、分用demultiplexing是什么?
  5. 硬中断为什么不能做繁重报文解析?硬中断、软中断、Signal信号三者本质区别?
  6. Socket只是一个fd吗?阻塞recv()休眠原理,和条件变量等待队列同源思想。
  7. TCP状态机、滑动窗口、重传全部内核完成,不是应用库函数
  8. 路由器转发报文,哪些字段变化、哪些字段原样保留,为什么路由器不维护TCP连接。

网络本质:跨主机的进程间通信

  • 本机IPC:管道、消息队列、信号量、共享内存,仅限单机;
  • Socket:既支持本机UNIX域套接字IPC,又支持跨主机网络通信。

一、硬件层|计算机组成视角:PHY、网卡NIC、PCIe、DMA、APIC中断控制器

1.1 网卡完整硬件构成

网卡(NIC)两大部分:

  1. PHY芯片:模拟电路,完成电/光信号 ↔ 数字比特流转换,差分信号、载波侦听。网线直连PHY。
  2. MAC控制器:组装/解析以太网帧,硬件做CRC校验,自带片上FIFO缓存,实现MAC介质访问控制协议。
  3. DMA控制器 :网卡内置DMA,外设与内存直接搬运,不消耗CPU
  4. 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行为:

  1. CPU正在运行用户进程;收到中断请求;
  2. 保存全部CPU上下文:通用寄存器、RIP程序计数器、CS段寄存器、栈寄存器;
  3. 切换特权级进入内核态,跳转到驱动注册好的中断服务程序ISR。

⚠️硬件中断IRQ vs Linux Signal信号

|项目|硬件中断IRQ|进程信号Signal|

|---|---|---|

|来源|硬件设备(网卡、时钟、键盘)|内核/其他进程软件生成|

|上下文|中断上下文,不属于任何进程|依附某一个用户进程|

|打断时机|随时打断CPU,甚至打断内核代码|不能打断内核,必须进程返回用户态才递送 |

|允许睡眠阻塞|禁止阻塞、sleep|可被信号唤醒|


二、操作系统内核层:硬中断、软中断SoftIRQ、NAPI机制

2.1 硬中断上下文严格约束

硬中断优先级高,同类中断会被屏蔽。

硬中断不能做报文解析这种重活,如果在ISR里面解析数据包,中断迟迟不退出,新硬件中断被屏蔽,大量丢包,系统卡顿。

设计原则:硬中断只做紧急极简工作;报文解析、协议处理全部交给软中断延后执行。

网卡ISR硬中断只做两件事:

  1. 读写网卡寄存器,清除硬件中断标志,防止重复触发中断;
  2. 提交网络接收软中断NET_RX_SOFTIRQ,立刻退出硬中断上下文。

2.2 软中断 SoftIRQ 与 ksoftirqd 内核线程

Linux预定义多类软中断:网络收包、网络发包、时钟、tasklet等。

每个CPU拥有独立软中断队列,每个CPU对应内核线程 ksoftirqd/$cpu_id

软中断两个执行时机:

  1. 硬中断返回时,如果有待处理软中断,优先执行;
  2. 软中断任务积压过多,交给后台ksoftirqd调度执行。

软中断上下文依然不能睡眠、不能访问用户空间内存

软中断的核心工作:遍历RingBuffer,取出原始帧,分配sk_buff开始逐层报文解析,递交内核协议栈。

2.3 NAPI 机制,解决中断风暴

高吞吐场景,每秒数万数据包,如果每包触发一次硬件中断,CPU大量消耗在进出中断上下文,叫中断风暴

NAPI流程:

  1. 第一个数据包到达,触发一次硬件中断;
  2. ISR中关闭网卡硬件接收中断;
  3. 软中断循环轮询RingBuffer,批量处理全部待处理数据包(在这里批量做报文解析);
  4. 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;             // 上层协议类型,网络字节序大端
};

解析步骤:

  1. sk_buff->mac 指向缓冲区起始位置,即以太网头首地址;
  2. 读取h_proto字段:
    • 0x0800:上层是IPv4报文;
    • 0x0806:上层ARP报文;
  3. skb_pull(skb, ETH_HLEN)把sk_buff->data指针向后移动14字节(以太网头长度)
    • 指针移动完成:skb->network现在指向IP头部起始地址;mac指针保留以太网头位置。
  4. 返回上层协议编号,内核据此投递到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()

  1. 读取version,校验是否IPv4;读取ihl计算IP头真实长度;
  2. 校验IP头部checksum,软件计算头部校验和,和报文check字段对比;校验失败直接丢弃sk_buff;
  3. 读取tot_len,校验报文实际长度合法性;
  4. 判断IP分片:如果frag_off表明这是分片包,放入内核分片重组缓冲区ipq;等待全部分片到达,重组为完整IP报文,再向上递交;分片超时未全部到达直接丢弃;
  5. 路由查找,出现两个关键分支:
    • 目的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()不再向上递交传输层
  6. 转发分支逻辑: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()

  1. 根据目的端口,在内核查找对应的socket对象;找不到socket直接丢弃报文;
  2. 解析TCP头,读取doff得到TCP头部长度;
  3. 校验TCP checksum;TCP校验和包含IP伪头部,防止错路由;校验失败丢弃;
  4. 解析标志位 SYN / ACK / FIN / RST,驱动内核TCP状态机(LISTEN、SYN_RECV、ESTABLISHED、TIME_WAIT等);
  5. 处理序列号、确认号、滑动窗口、SACK;处理重传队列;
  6. skb_pull(skb, tcphdr_len),指针跳过TCP头部;剩下的内存就是应用层Payload业务数据
  7. 将Payload数据写入socket内核接收缓冲区;
  8. 唤醒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内容。

两个关键术语

  1. 封装 Encapsulation(发送方向,自上而下)
    上层交付的数据,下层在数据前面追加本层协议头部(链路层额外追加FCS尾部),把上层完整数据当作payload。

报文格式:本层头部 + 上层完整报文(payload)

  1. 解封装 + 分用 Demultiplexing(接收方向,自下而上)
    接收机器从网卡收到完整帧:
  2. 当前层读取自己这一层头部,完成本层工作(校验、寻址);
  3. 剥掉自己的头部,剩下全部内容当做上层payload;
  4. 读取头部内的「上层协议标识字段」,把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服务访问点:就是上下层之间约定好的标记字段,下层靠这个标记把包交给正确上层模块。

分用完整链路(接收端内核)

  1. 链路层读到以太网头h_proto = 0x0800 → 把payload上交IP模块;如果0x0806上交ARP模块。
  2. IP层解析IP头protocol=6 → payload上交TCP模块;17上交UDP模块;1上交ICMP。
  3. TCP模块解析TCP头目的端口,找到本机对应socket,payload写入socket接收缓冲区,唤醒进程。

如果没有这些SAP标记字段,内核收到报文根本不知道该交给哪个上层协议处理。

4.3 Linux内核中,上下层如何传递数据(sk_buff视角)

整个内核协议栈,全程只传递同一个sk_buff对象,不拷贝报文数据,只移动内部指针,这是Linux网络高性能根源。

✅接收路径(自底向上 · 解封装)

原始帧存入sk_buff缓冲区,data一开始指向以太网帧头。

  1. 链路层eth_type_trans()
    skb_pull(skb,14)data指针向后移动14字节,跳过以太网头;此时data指向IP头,剩下全部内容就是IP层Payload,上交ip_rcv()
  2. 网络层ip_rcv()
    读取IP头,做完校验、分片重组;
    • 本机接收:skb_pull(skb, iphdr_len)data继续向后跳过IP头部;此时data指向TCP头,payload上交tcp_v4_rcv()
    • 路由器转发:进入ip_forward,不走skb_pull向上递交传输层,后续skb_push重新构造二层头。
  3. 传输层tcp_v4_rcv()
    解析TCP头;skb_pull(skb,tcphdr_len)data跳过TCP头;此时data直接指向应用层Payload业务数据,payload写入socket接收缓冲区。

⚠️协议头部并没有从内存删除,只是data指针跨过头部,不再作为本层有效数据;mac/network/transport指针还保存各层头部的位置,供需要时读取。没有任何内存拷贝,只是指针移动

✅发送路径(自上而下 · 封装)

send系统调用,用户数据拷贝进sk_buff,应用数据放在缓冲区靠后的位置;缓冲区头部预留足够headroom空间,用来向前填充各层头部

  1. 传输层:skb_push(skb,tcp_header_len)data指针向前移动,腾出空间写入TCP头部;
  2. 网络层:继续skb_push()向前移动data,写入IP头部;
  3. 链路层:继续skb_push()向前移动data,写入以太网头部;
  4. sk_buff交给驱动、DMA发送出去。

skb_pull():data向后移(接收,剥头);skb_push():data向前移(发送,加头),全程不拷贝报文内容

4.4 上下层之间,什么信息会传递、什么绝不传递

下层传给上层的内容

  1. sk_buff整体对象(包含完整缓冲区、各个层指针)
  2. 本层解析出来的控制元信息 :例如IP层把源IP、目的IP传给传输层;链路层把接收网卡设备net_device*传递上去。
  3. payload二进制数据。

❗下层不会把自己的头部内存拷贝一份交给上层 ,上层通过sk_buff保存的transport/network/mac指针,可以回头读取下层头部,但payload传递只移动data指针。

上层绝对不能传给下层的东西

  1. 上层业务语义(比如HTTP GET请求内容);下层完全不感知应用业务。
  2. 上层的socket、进程PCB;链路层/IP层完全不知道什么socket,socket对象只有传输层才会关联。

举例:IP协议完全不知道什么TCP状态机;TCP完全不知道什么MAC地址。分层隔离,解耦

4.5 路由器设备:只处理物理‑链路‑网络层,不碰传输层、应用层

路由器收到IP报文:

  1. 链路层解封装,剥掉入方向以太网帧头;payload上交IP层;
  2. IP层读取IP头部,TTL递减,重新计算IP头部checksum,查询路由表,决定转发出口;完全不解析TCP/UDP头部,TCP载荷原封不动保留
  3. 在出口链路层重新封装全新以太网帧头,转发出去。

路由器只会处理物理层、链路层、网络层;不会到达传输层。这体现分层: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机制)

  1. 客户端发出TTL=1的UDP包;
  2. 第一台路由器收到,TTL减1等于0,丢弃报文,回复ICMP time‑exceed给源主机;
  3. 客户端拿到ICMP,得到第一跳路由器IP;
  4. 发送TTL=2,到达第二台路由器TTL变为0;以此类推,直到抵达目标主机。

traceroute依靠路由器修改TTL、回复ICMP实现,全程路由器不会触碰传输层载荷。

4.7 硬件中断、软中断、协议栈分层关系

  1. 网卡硬件 + 驱动属于链路层实现;硬中断只是硬件通知机制,不属于协议层;
  2. 硬中断只做标记软中断,不做任何报文解析
  3. ksoftirqd软中断上下文启动链路层解析,逐层向上调用IP、TCP;
  4. socket、等待队列属于传输层向上对应用户态的接口。

信号Signal 和协议栈分层无关;信号是内核给进程的软件通知,网络包递送靠协议栈+等待队列,二者机制不同。

4.8 面试易错点整理

  1. TCP能不能看见MAC地址?

    不能。TCP属于传输层,MAC是链路层头部;TCP代码不会直接读取mac头,sk_buff虽然保存mac指针,但TCP协议逻辑不使用MAC。

  2. IP层会修改TCP payload内容吗?

    不会;IP只修改IP头部;payload完整交付上层;IP分片只是切割整个IP报文,不会修改TCP段内部字节。

  3. 接收时,协议头会不会拷贝到用户缓冲区(recv拿到的数据)?

    不会! 内核逐层skb_pull跳过全部以太网、IP、TCP头部;copy_to_user只把最末尾的应用payload拷贝给用户进程;应用程序recv完全看不到底层任何协议头部。

  4. UDP和TCP为什么共用IP层?

    IP层payload里通过protocol字段做分用;同一个IP模块,根据数字6/17分别投递给TCP/UDP。

  5. 为什么sk_buff要保存mac、network、transport三套指针?

    当已经skb_pull移动data指针之后,上层协议依然可以回头读取下层头部信息(例如TCP需要读取IP伪头部做TCP校验和,就要回头访问IP头)。

  6. 路由器转发报文,会修改TCP校验和吗?

    不会。路由器只修改IP头部TTL和IP头部checksum;TCP校验和包含IP伪头部,但路由器不解析TCP,不会动TCP checksum。端主机收到报文自行校验TCP checksum。

  7. TCP连接经过路由器,路由器保存连接状态吗?

    标准三层路由器不保存任何TCP连接状态;只有NAT设备会维护五元组会话表。

  8. 为什么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)

  1. PHY接收电信号,组装以太网帧存入网卡FIFO;DMA搬运帧到内核RingBuffer物理内存。
  2. 网卡发起硬件中断IRQ,CPU保存寄存器,进入内核态执行ISR硬中断。
  3. ISR清除中断标记,提交NET_RX软中断,退出硬中断。
  4. ksoftirqd执行软中断,从RingBuffer取出帧,分配sk_buff。
  5. 链路层eth_type_trans():解析以太网帧头,skb_pull剥离二层头,指向IP头
  6. 网络层ip_rcv():解析IP头部,校验checksum,IP分片重组;路由判断
    • 本机接收:skb_pull剥离IP头,交给tcp_v4_rcv()传输层;
    • 路由器转发:调用ip_forward,修改TTL,重新封装二层帧,直接发送,永远不会走到传输层。
  7. 传输层tcp_v4_rcv():解析TCP头,校验TCP checksum,驱动TCP状态机;skb_pull剥离TCP头,得到Payload;写入socket接收缓冲区;唤醒等待队列进程
  8. 用户程序调用recv()系统调用陷入内核态。
  9. copy_to_user():socket内核缓冲区payload拷贝到用户进程虚拟地址空间buffer。
    10.系统调用返回用户态,应用拿到业务数据。

两次拷贝:

1.DMA:网卡硬件→内核RingBuffer内存(硬件搬运,无CPU消耗)

2.copy_to_user:socket内核缓冲区→用户内存(CPU拷贝)

5.2 发包完整链路 send() → 网线

发包是解析的逆过程:在sk_buff前面填充各层协议头

  1. 用户调用send()系统调用,用户态切内核态;copy_from_user()用户buffer拷贝到socket发送缓冲区。

send返回成功≠对方收到数据,仅仅拷贝到本机内核发送缓冲区,后续内核异步发送。

  1. TCP模块构造TCP头部(序列号、窗口、标志位);
  2. IP层构造IP头部;计算IP头部checksum;
  3. 链路层ARP解析得到目标MAC,在sk_buff前面填充以太网帧头;
  4. sk_buff送入QDisc排队队列;
  5. 驱动取出sk_buff填入发送RingBuffer;DMA把报文从内存搬运到网卡FIFO;
  6. PHY转为电信号发送网线。

六、Socket内核对象详解,阻塞IO休眠原理

6.1 Socket是什么

Linux一切皆文件。调用socket()系统调用,内核创建struct socket内核对象,创建file文件结构,返回fd给用户进程。

用户只能操作fd,不能直接访问socket内核对象、缓冲区、TCP状态机;全部操作依靠系统调用陷入内核

struct socket内核对象核心成员:

  1. 接收缓冲区、发送缓冲区(内核页内存);
  2. 四元组:源IP、源端口、对端IP、对端端口;
  3. TCP状态机:LISTEN、SYN_RECV、ESTABLISHED、TIME_WAIT;
  4. 等待队列wait_queue_head_t:阻塞IO休眠的进程PCB挂载在这里;
  5. proto_ops协议操作函数集,对应TCP/UDP处理函数。

阻塞recv底层流程

cpp 复制代码
recv(connfd, buf, len, 0);
  1. 用户态执行recv,触发系统调用进入内核。
  2. 检查socket接收缓冲区是否存在Payload数据:
    • ✅有数据:copy_to_user拷贝payload到用户buffer,系统调用返回;
    • ❌缓冲区为空:把当前进程PCB挂载socket等待队列;进程状态改为TASK_INTERRUPTIBLE可中断睡眠;调度器切换其他进程运行,让出CPU。
  3. 数据包到达内核,完成全部报文解析,payload写入socket缓冲区;内核唤醒等待队列上休眠进程。
  4. 进程重新调度运行,完成recv拷贝,返回用户态。

pthread_cond_wait 用户态线程等待队列;socket recv阻塞是内核态进程等待队列,底层思想同源:等待队列 + 睡眠 + 事件唤醒。

6.2 listen半连接队列、全连接队列

listen(fd, backlog)

  1. 半连接队列(SYN队列):收到SYN,状态SYN_RECV,尚未完成三次握手;
  2. 全连接队列(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拿到的原始帧内容。


八、高频易错点(结合报文解析底层)

  1. recv返回长度小于传入len?

    TCP字节流,没有消息边界。socket缓冲区payload不足,recv直接返回现有字节;协议头部已经被内核全部剥离,用户看不到任何以太网/IP/TCP头部,只能拿到payload。业务层必须自己做报文分包。

  2. send返回成功代表对方应用收到数据?

    错误。send成功只是payload拷贝本机内核发送缓冲区;TCP ACK只是内核协议栈确认,应用程序不会收到通知。本机断电,缓冲区未发出数据直接丢失。

  3. CRC、IP checksum、TCP checksum分别在哪一层?

  • FCS‑CRC:网卡硬件做,帧错误网卡直接丢弃,内核看不见坏帧。
  • IP checksum:内核IP层软件校验,仅校验IP头部
  • TCP checksum:内核传输层校验,包含伪头部、TCP头、TCP payload。
  1. DMA搬运数据,为什么ksoftirqd依然CPU很高?

    DMA只管网卡与内存搬运;以太网/IP/TCP报文解析、校验和计算、分片重组、copy_to_user拷贝payload全部消耗CPU,这就是ksoftirqd占用CPU的来源。

  2. TIME_WAIT:主动关闭方内核socket状态,等待2MSLS,确保对端收到FIN。属于内核对象资源,不是用户进程内存。


九、进阶学习路线

  1. 手写阻塞TCP服务端、客户端,掌握基础API;
  2. IO模型:阻塞IO、非阻塞IO、select/poll、epoll深度解析(epitem、内核等待队列)
  3. TCP进阶:滑动窗口、拥塞控制、RTT、SACK、CLOSE_WAIT;
  4. 零拷贝 sendfile / mmap,减少内存拷贝;
  5. Netfilter iptables;
  6. DPDK 用户态网卡:绕开内核协议栈,用户程序直接操作RingBuffer,自己完成全部报文解析
相关推荐
袁小皮皮不皮17 小时前
11.HCIP IS-IS协议基础知识
服务器·网络·笔记·网络协议·智能路由器
zbtlink3 天前
给自动售货机配的那台WiFi6路由器,它用上了吗
网络·智能路由器
爱刷碗的苏泓舒3 天前
网络通信入门:交换机、路由器与防火墙的角色、关系、功能和使用场景
网络·智能路由器·网络通信·防火墙·交换机
hongmai6668885 天前
ESP32-C5-WROOM-1-N16R8:双频Wi-Fi 6与多协议融合,重新定义物联网连接新标准
笔记·嵌入式硬件·物联网·智能路由器·risc-v
Shell运维手记5 天前
ARP 协议超详细讲解(适合网工考试 / 运维理解)
运维·网络·网络协议·智能路由器
白狐_7986 天前
【408计算机网络|第04章·下|408-CN-04B】网络层(下):路由算法、RIP、OSPF、BGP、IPv6与路由器
计算机网络·算法·智能路由器
Inhand陈工6 天前
路由器专题二:有线为主,蜂窝备份——映翰通路由器链路备份功能详解
运维·网络·物联网·智能路由器·系统安全
Yuiiii__7 天前
一次“在家连不上公司内网服务”的排障实战记录
网络·智能路由器
hxhy008 天前
H3C NX30Pro 路由刷机指南
智能路由器