news 2026/10/14 9:37:58

Linux --传输层协议 TCP

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux --传输层协议 TCP

先搞懂TCP在哪里:网络分层模型

要理解TCP,先得知道它在网络体系中的位置。

OSI参考模型TCP/IP分层模型说明
应用层应用层DNS、HTTP、SMTP、SSH、FTP等
表示层应用层TLS/SSL、MIME等
会话层应用层RTP、SIP等
传输层传输层TCP、UDP、SCTP、DCCP
网络层互联网层IP、ICMP、ARP
数据链路层网卡层设备驱动与网络接口
物理层硬件物理传输介质

关键点:TCP属于传输层,是操作系统内核的一部分,向下依赖网络层(IP),向上为应用层提供服务。应用程序通过系统调用(socket API)与TCP交互。

TCP协议段格式:每个字段都是干什么的?

TCP报文段(Segment)的头部结构是理解一切机制的基础。

0 15 16 31 +-------------------+-------------------+ | 16位源端口号 | 16位目的端口号 | +-------------------+-------------------+ | 32位序号 | +---------------------------------------+ | 32位确认序号 | +-------+-------+-----------------------+ |4位首部|保留6位 | U A P R S F | |长度 | | R C S S Y I | | | | G K H T N N | +-------+-------+-----------------------+ | 16位窗口大小 | 16位校验和 | +-------------------+-------------------+ | 16位紧急指针 | 选项 | +-------------------+-------------------+ | 数据 | +---------------------------------------+

核心字段详解

  • 源/目的端口号:标识数据从哪个进程来、到哪个进程去。IP地址标识主机,端口号标识进程。

  • 32位序号:TCP对每个字节都编号。序号是本报文段第一个数据字节的编号。

  • 32位确认序号:期望收到的下一个字节的编号。比如收到1~1000,确认序号就是1001。

  • 4位首部长度:表示TCP头部有多少个32位字(4字节)。最大15,所以头部最大60字节。通常没有选项时是20字节。

  • 6位标志位:

    • URG:紧急指针有效

    • ACK:确认序号有效(连接建立后通常都置1)

    • PSH:提示接收端立刻把数据交给应用层

    • RST:复位连接(对方要求重新建立连接)

    • SYN:同步序号,用于建立连接

    • FIN:发送方准备关闭连接

  • 16位窗口大小:接收端告诉发送端自己还能接收多少字节(流量控制的核心)。

  • 16位校验和:CRC校验,覆盖TCP首部+数据部分。

  • 16位紧急指针:标识紧急数据的位置。

扩展:选项字段中的“窗口扩大因子”

16位窗口最大只能表示65535字节。为了支持更大的窗口,TCP选项中有窗口扩大因子M,实际窗口大小 = 窗口字段值 × 2^M。这在高速网络中非常重要。

TCP的核心机制(上):可靠传输

确认应答(ACK)机制

TCP把每个字节都编号,接收端收到数据后返回ACK,告诉发送端:“我已经收到了X之前的所有数据,下次从X开始发。”

生活类比:唐僧讲经,每讲一段就问“听懂了吗?”;徒弟回答“听懂了,继续下一段”。

关键规则:

  • 每个ACK都携带确认序号,表示“这个序号之前的数据我都收到了”。

  • 发送端收到ACK后,才知道数据被正确接收。

超时重传机制

数据或ACK可能丢失。发送端在特定时间间隔内没收到ACK,就重发。

核心问题:超时时间怎么定?

  • 太长:重传慢,效率低。

  • 太短:频繁重传,浪费带宽。

TCP的动态策略(Linux实现):

  • 以500ms为基本单位。

  • 每次重传后,等待时间翻倍:500ms → 1000ms → 2000ms → 4000ms...

  • 累计到一定次数后,认为网络或对端异常,强制关闭连接。

扩展:自适应重传(Karn算法)
实际TCP实现中,RTT(往返时间)是动态测量的。TCP维护一个加权平均RTT(SRTT)和偏差(RTTVAR),超时时间 = SRTT + 4×RTTVAR。这比固定500ms更智能。

去重与排序

由于重传,接收端可能收到重复数据。TCP利用序号可以轻松去重:如果收到的数据序号小于已确认的序号,直接丢弃。

TCP的核心机制(中):连接管理(三次握手、四次挥手)

三次握手建立连接

客户端 服务器 | | | ① SYN (seq=x) | | ------------------------------------------->| | SYN_SENT SYN_RCVD | | | | ② SYN + ACK (seq=y, ack=x+1) | | <-------------------------------------------| | | | ③ ACK (seq=x+1, ack=y+1) | | ------------------------------------------->| | ESTABLISHED ESTABLISHED | | | | (连接建立,可以传数据了) |

每一步在做什么?

第一次握手:客户端 → 服务器
  • 动作:客户端调用connect(),发送一个SYN报文段。

  • 关键字段:

    • SYN = 1(标志位,表示请求建立连接)

    • seq = x(客户端的初始序列号,随机生成)

  • 客户端状态:CLOSED → SYN_SENT

  • 含义:客户端说“我想和你建立连接,我的初始序列号是x”。

第二次握手:服务器 → 客户端
  • 动作:服务器收到SYN后,如果同意连接,回复一个SYN+ACK报文段。

  • 关键字段:

    • SYN = 1(我也请求建立连接)

    • ACK = 1(确认号有效)

    • seq = y(服务器的初始序列号,随机生成)

    • ack = x + 1(确认收到客户端的SYN,期望下次收到x+1)

  • 服务器状态:LISTEN → SYN_RCVD

  • 含义:服务器说“我同意,我的初始序列号是y,我确认收到你的x了”。

第三次握手:客户端 → 服务器
  • 动作:客户端收到SYN+ACK后,发送一个ACK报文段。

  • 关键字段:

    • ACK = 1

    • seq = x + 1

    • ack = y + 1(确认收到服务器的SYN)

  • 客户端状态:SYN_SENT → ESTABLISHED

  • 服务器状态:收到ACK后,SYN_RCVD → ESTABLISHED

  • 含义:客户端说“我确认收到你的y了,连接建立!”

三次握手的本质:确认双方收发能力

握手能确认什么
第一次服务器能确认:客户端能发,自己能收
第二次客户端能确认:服务器能发,自己能收;服务器能收,客户端能发
第三次服务器能确认:客户端能收,自己能发

经过三次握手,双方都确认了自己和对方的收发能力都正常。

为什么是三次,而不是两次或四次?

防止已失效的连接请求突然到达服务器,导致服务器资源浪费。

假设只有两次握手:
客户端 服务器 | | | ① SYN (seq=x) —— 网络延迟,滞留 —— | | | | (客户端超时,重发SYN) | | ② SYN (seq=x') ---------------------->| | <---------------- SYN+ACK (ack=x'+1) | | ③ ACK ------------------------------> | 连接建立 | | | (此时①的SYN姗姗来迟) | | <---------------- SYN+ACK (ack=x+1) | | | 服务器认为连接建立! | (客户端不理它,服务器却一直等) |
问题:服务器收到一个早已失效的SYN,就认为连接建立了,分配资源等待客户端。但客户端根本不认这个连接,导致服务器资源白白浪费。

三次握手解决这个问题:

  • 服务器收到失效SYN后,回复SYN+ACK,进入SYN_RCVD。

  • 客户端不会对这个失效连接回复ACK。

  • 服务器超时后释放资源,不会真正建立连接。

为什么不是四次?

第二次握手时,服务器可以同时发送SYN和ACK(即SYN+ACK),把两个动作合并成一个报文,所以不需要四次。

总结:

  • 三次是最少次数:既能确认双方收发能力,又能防止失效连接。

  • 两次不安全:无法防止历史连接。

  • 四次没必要:SYN和ACK可以合并。

四次挥手断开连接

客户端 服务器 | | | ① FIN (seq=u) | | ------------------------------------------->| | FIN_WAIT_1 CLOSE_WAIT | | | | ② ACK (ack=u+1) | | <-------------------------------------------| | FIN_WAIT_2 | | | | (服务器处理剩余数据,可能还要发数据) | | | | ③ FIN (seq=v) | | <-------------------------------------------| | LAST_ACK | | | | ④ ACK (ack=v+1) | | ------------------------------------------->| | TIME_WAIT (等待2MSL) CLOSED | | | | (2MSL后)CLOSED |

每一步在做什么?

第一次挥手:客户端 → 服务器
  • 动作:客户端调用close(),发送FIN报文段。

  • 关键字段:

    • FIN = 1

    • seq = u

  • 客户端状态:ESTABLISHED → FIN_WAIT_1

  • 含义:客户端说“我没有数据要发了,准备关闭”。

第二次挥手:服务器 → 客户端
  • 动作:服务器收到FIN后,回复ACK。

  • 关键字段:

    • ACK = 1

    • ack = u + 1

  • 服务器状态:ESTABLISHED → CLOSE_WAIT

  • 客户端状态:收到ACK后,FIN_WAIT_1 → FIN_WAIT_2

  • 含义:服务器说“我知道你要关了,但我可能还有数据要发,你等等”。

注意:此时连接处于半关闭(Half-Close)状态——客户端不能再发数据,但服务器还能发。

第三次挥手:服务器 → 客户端
  • 动作:服务器处理完所有数据后,调用close(),发送FIN。

  • 关键字段:

    • FIN = 1

    • seq = v

  • 服务器状态:CLOSE_WAIT → LAST_ACK

  • 含义:服务器说“我也没数据要发了,准备关闭”。

第四次挥手:客户端 → 服务器
  • 动作:客户端收到FIN后,回复ACK。

  • 关键字段:

    • ACK = 1

    • ack = v + 1

  • 客户端状态:FIN_WAIT_2 → TIME_WAIT

  • 服务器状态:收到ACK后,LAST_ACK → CLOSED

  • 含义:客户端说“我确认收到你的FIN了,你可以关了”。

  • 客户端:等待2MSL后,才进入CLOSED。

为什么是四次,而不是三次?

核心原因:TCP是全双工的,每个方向需要单独关闭。

  • 客户端发FIN:关闭“客户端→服务器”方向。

  • 服务器回ACK:确认收到FIN。

  • 服务器发FIN:关闭“服务器→客户端”方向。

  • 客户端回ACK:确认收到FIN。

为什么不能合并ACK和FIN?

因为服务器收到FIN时,可能还有数据没发完。它必须先回ACK(告诉客户端“我知道你要关了”),等数据发完后再发FIN。

特殊情况:如果服务器收到FIN时也没有数据要发了,可以把ACK和FIN合并,变成三次挥手。但TCP协议默认按四次处理。

为什么客户端要等待2MSL?

MSL = Maximum Segment Lifetime,报文最大生存时间。RFC 1122规定为2分钟,Linux默认60秒。

两个原因:

原因一:确保最后一个ACK能到达服务器
客户端 服务器 | | |<-------- FIN (seq=v) ----| | | |------ ACK (ack=v+1) ---->| ← 这个ACK如果丢失 | | | | 服务器超时重发FIN |<-------- FIN (seq=v) ----| | | |------ ACK (ack=v+1) ---->| | |

如果客户端发完ACK立刻关闭,而ACK丢失,服务器会重发FIN。但客户端已经关闭,无法响应,服务器就无法正常关闭。

等待2MSL,客户端还能响应服务器的重发FIN。

原因二:让本次连接的所有报文在网络中消失

如果客户端立刻用相同端口建立新连接,旧连接的延迟报文可能被新连接误收,导致数据错乱。

等待2MSL,确保本次连接的所有报文都已消亡。

状态转换详解

服务器端:

  • CLOSED → LISTEN:调用listen()

  • LISTEN → SYN_RCVD:收到SYN

  • SYN_RCVD → ESTABLISHED:收到ACK

  • ESTABLISHED → CLOSE_WAIT:收到FIN

  • CLOSE_WAIT → LAST_ACK:调用close(),发送FIN

  • LAST_ACK → CLOSED:收到ACK

客户端:

  • CLOSED → SYN_SENT:调用connect()

  • SYN_SENT → ESTABLISHED:收到SYN+ACK,发送ACK

  • ESTABLISHED → FIN_WAIT_1:调用close(),发送FIN

  • FIN_WAIT_1 → FIN_WAIT_2:收到ACK

  • FIN_WAIT_2 → TIME_WAIT:收到FIN,发送ACK

  • TIME_WAIT → CLOSED:等待2MSL

TIME_WAIT 与 CLOSE_WAIT

核心结论:

  • TIME_WAIT:主动关闭方在发送最后一个ACK后进入的状态。

  • CLOSE_WAIT:被动关闭方在收到FIN并回复ACK后进入的状态。

TIME_WAIT 详解

什么是 TIME_WAIT?

TIME_WAIT 是主动关闭连接的一方在发送最后一个ACK后进入的状态。它需要等待2MSL(报文最大生存时间)后才能进入CLOSED状态。

MSL 的值:

  • RFC 1122 规定为2分钟。

  • Linux 默认tcp_fin_timeout = 60秒,即 MSL = 60秒,2MSL = 120秒?不,实际上 Linux 的 TIME_WAIT 持续时间是60秒(tcp_fin_timeout直接控制 TIME_WAIT 时长,不是2MSL的2倍)。

为什么需要 TIME_WAIT?(核心!)

TIME_WAIT 的存在有两个根本原因:

原因一:确保最后一个ACK能可靠到达服务器
客户端 服务器 | | |<-------- FIN (seq=v) ----| | | |------ ACK (ack=v+1) ---->| ← 这个ACK如果丢失 | | | (客户端立刻关闭) | 服务器超时重发FIN | | |<-------- FIN (seq=v) ----| | | | ✗ 客户端已关闭,无法响应 | | | | 服务器无法正常关闭!

问题:如果客户端发送最后一个ACK后立刻关闭,而这个ACK在网络中丢失,服务器会超时重发FIN。但客户端已经关闭,无法响应,导致服务器一直处于LAST_ACK,无法正常关闭。

解决:客户端等待2MSL,期间如果收到服务器重发的FIN,还能再次发送ACK。

为什么是2MSL?

  • 1MSL:ACK到达服务器的最长时间。

  • 1MSL:服务器重发FIN到达客户端的最长时间。

  • 2MSL = 一来一回,确保能处理所有重发情况。

原因二:让本次连接的所有报文在网络中消失
客户端 服务器 | | | 连接1:数据包延迟在网络中 | | | | 连接1关闭 | | | | 连接2:使用相同四元组 | | | | ✗ 旧数据包到达,被误认为 | | 是新连接的数据! |

问题:TCP连接由四元组(源IP、源端口、目的IP、目的端口)唯一标识。如果客户端立刻用相同端口建立新连接,旧连接的延迟报文可能被新连接误收,导致数据错乱。

解决:等待2MSL,确保本次连接的所有报文都已消亡(超过MSL的报文会被网络丢弃)。

TIME_WAIT 的危害

危害一:端口占用

服务器主动关闭连接后,端口在TIME_WAIT期间不能被重用。如果服务器需要频繁主动关闭连接,会出现:

$ ./server bind error: Address already in use

原因:虽然应用程序终止了,但TCP层的连接还没完全断开。

验证:

$ netstat -apn | grep 8000 tcp 0 0 127.0.0.1:33498 127.0.0.1:8000 CLOSE_WAIT 10830/client tcp 0 0 127.0.0.1:8000 127.0.0.1:33498 FIN_WAIT2 -
危害二:资源浪费

每个TIME_WAIT连接占用一个通信五元组(源IP、源端口、目的IP、目的端口、协议),以及内核中的一些内存资源。

高并发场景:

  • 服务器每秒处理大量短连接。

  • 如果服务器主动关闭,会产生大量TIME_WAIT。

  • 每个TIME_WAIT持续60秒,累积起来可能达到数万甚至数十万。

  • 可能导致端口耗尽、内存耗尽。

如何解决 TIME_WAIT 问题?

方法一:SO_REUSEADDR(最常用)
int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

作用:允许创建端口号相同但IP地址不同的多个socket描述符,或者绑定处于TIME_WAIT的端口。

原理:通知内核,如果端口处于TIME_WAIT,可以重用。

注意:这不能缩短TIME_WAIT时间,只是允许新连接绑定。

方法二:让客户端主动关闭

核心思想:客户端端口多(65535个),不怕TIME_WAIT;服务器端口少(如80、443),TIME_WAIT影响大。

实现:

  • HTTP协议中,客户端发完请求后主动关闭。

  • 服务器设置Connection: close,让客户端关闭。

方法三:使用长连接

核心思想:减少连接建立/关闭频率。

CLOSE_WAIT 详解

什么是 CLOSE_WAIT?

CLOSE_WAIT 是被动关闭连接的一方在收到FIN并回复ACK后进入的状态。

关键:此时连接处于半关闭状态——对方不能再发数据,但自己还能发数据。

正常流程:

  1. 收到FIN。

  2. 回复ACK。

  3. 进入CLOSE_WAIT。

  4. 应用程序处理剩余数据。

  5. 应用程序调用close()。

  6. 发送FIN,进入LAST_ACK。

  7. 收到ACK,进入CLOSED。

问题:如果应用程序一直不调用close(),连接就永远停留在CLOSE_WAIT,导致文件描述符泄漏。

CLOSE_WAIT 的危害

危害一:文件描述符泄漏

每个CLOSE_WAIT连接占用一个文件描述符。Linux默认单进程文件描述符上限是1024(可调整)。

后果:

  • 文件描述符耗尽。

  • 无法接受新连接。

  • 报错Too many open files。

危害二:内存泄漏

每个连接占用内核内存(socket结构、缓冲区等)。

后果:

  • 内存持续增长。

  • 最终OOM(Out Of Memory)。

危害三:连接堆积

大量CLOSE_WAIT连接堆积,导致:

  • 端口占用。

  • 系统负载升高。

  • 服务不可用。

Q:服务器出现大量CLOSE_WAIT是什么原因?

答:代码BUG——服务器收到FIN后没有调用close()。需要检查代码,确保所有路径都能正确关闭socket。

Q:CLOSE_WAIT会持续多久?

答:取决于应用程序何时调用close()。如果不调用,永远持续。

Q:CLOSE_WAIT和TIME_WAIT哪个更严重?

答:CLOSE_WAIT更严重。因为:

  • TIME_WAIT是正常状态,会自行消失。

  • CLOSE_WAIT是异常状态,不修复代码永远不会消失。

  • TIME_WAIT占用端口,CLOSE_WAIT占用文件描述符和内存。

TCP的核心机制(下):高效传输

滑动窗口

问题:一发一收性能太差。
解决:一次发送多个段,无需等待每个ACK。

窗口大小:无需等待确认应答而可以继续发送数据的最大值。

  • 发送前4个段(窗口=4000字节),不用等ACK。

  • 收到第一个ACK后,窗口向后滑动,发送第5个段。

  • 发送缓冲区记录未应答的数据,收到ACK后删除。

窗口与吞吐量
吞吐量 ≈ 窗口大小 / RTT。窗口越大,吞吐量越高。

快重传

情况一:ACK丢了——不要紧,后续ACK可以确认。
情况二:数据包丢了——接收端连续收到相同ACK(如“下一个是1001”),发送端连续3次收到相同ACK,立即重传丢失的数据(不等超时)。这就是快重传。

快恢复
快重传后,不回到慢启动,而是将拥塞窗口减半,然后线性增长(快恢复)。

拥塞控制

问题:网络本身拥堵,贸然发大量数据会雪上加霜。
解决:慢启动 + 拥塞避免。

慢启动:

  • 初始拥塞窗口=1。

  • 每收到一个ACK,拥塞窗口+1(指数增长)。

  • 达到慢启动阈值后,改为线性增长(拥塞避免)。

超时重传时:

  • 慢启动阈值减半。

  • 拥塞窗口置1,重新慢启动。

延迟应答

问题:立刻ACK,窗口可能较小。
解决:等一会再ACK,窗口可能变大。

  • 数量限制:每N个包应答一次(N通常=2)。

  • 时间限制:超过200ms必须应答。

捎带应答

问题:ACK单独发太浪费。
解决:ACK搭应用层响应的顺风车一起发。

  • 例如:客户端问“How are you”,服务器回“Fine, thank you”时顺便带上ACK。

TCP的“性格”:面向字节流与粘包问题

面向字节流

TCP不保留应用层的消息边界。发送端write 100字节,可能被拆成多个TCP段;接收端read时,可能一次读到多个消息。

全双工:一个连接既有发送缓冲区,也有接收缓冲区,可以同时读写。

粘包问题

本质:应用层不知道从哪到哪是一个完整消息。
解决:

  • 定长包:固定大小读取。

  • 变长包:包头加长度字段。

  • 分隔符:如HTTP的\r\n\r\n。

UDP会不会粘包?
不会。UDP保留消息边界,要么收到完整报文,要么不收。

TCP异常情况处理

异常行为
进程终止释放文件描述符,发送FIN,正常关闭
机器重启同进程终止
机器掉电/网线断开接收端认为连接还在,写入时发现异常,发送RST;TCP有保活定时器定期探测
应用层保活HTTP长连接、QQ断线重连等

扩展

┌─────────────────────────────────────────────────────────────┐ │ 进程层 │ │ ┌──────────────┐ │ │ │ task_struct │ │ │ └──────┬───────┘ │ │ │ files │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ struct files_struct │ │ │ │ ┌────────────────────────┐ │ │ │ │ │ struct file* fd_array[]│ │ │ │ │ └───────────┬────────────┘ │ │ │ └──────────────┼───────────────┘ │ │ │ fd_array[n] │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ struct file │ │ │ │ private_data ──────────┼──────┐ │ │ └──────────────────────────────┘ │ │ │ │ │ │ ┌──────────────────────────────┐ │ │ │ │ struct socket │◄─────┘ │ │ │ file ───────────────────┼──────┐ │ │ │ sk ─────────────────────┼───┐ │ │ │ └──────────────────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────────────┐ │ │ │ │ │ struct sock │◄──┘ │ │ │ │ ┌────────────────────────┐ │ │ │ │ │ │ struct inet_sock │ │ │ │ │ │ │ ┌──────────────────┐ │ │ │ │ │ │ │ │struct inet_conn │ │ │ │ │ │ │ │ │ ┌──────────────┐ │ │ │ │ │ │ │ │ │ │structtcp_sock│ │ │ │ │ │ │ │ │ │ └──────────────┘ │ │ │ │ │ │ │ │ └──────────────────┘ │ │ │ │ │ │ └────────────────────────┘ │ │ │ │ └──────────────────────────────┘ │ │ │ │ │ │ 注:struct file 与 struct socket │ │ │ 通过 private_data / file 互相指向 └────────────────────┘ └─────────────────────────────────────────────────────────────┘

这张图非常经典,它展示了Linux 内核中从用户态文件描述符到内核 TCP 协议栈的完整映射关系。

核心结构体全景图(从上到下)

这张图分为四个层级:

  1. 进程层:task_struct(进程控制块)

  2. 文件系统层:struct files_struct→struct file

  3. Socket 抽象层:struct socket

  4. 传输层协议层:struct sock→struct inet_sock→struct inet_connection_sock→struct tcp_sock(以 TCP 为例)

1. 进程与文件系统层

  • task_struct:代表一个进程。每个进程都有自己打开的文件表。

  • struct files_struct:进程打开文件的表。其中fd_array[]是一个指针数组,下标就是文件描述符(fd)。

  • struct file:代表一个打开的文件。对于 socket 来说,它代表一个网络连接。图中的private_data指向struct socket。

2. Socket 抽象层

  • struct socket:这是 BSD Socket 层的抽象。它不关心底层是 TCP 还是 UDP,只提供通用的 socket 接口(如sock->ops->sendmsg)。

    • 它有两个关键指针:

      • struct file *file:指回上层的struct file(用于文件系统交互)。

      • struct sock *sk:指向底层的传输层控制块。

3. 传输层协议层(重点!)

图中的底部是一个多层嵌套的结构,这是 Linux 内核网络栈的精髓——继承与多态。

  • struct sock:最基础的传输层控制块。包含了所有协议通用的字段(如状态、接收队列、发送队列、定时器等)。

  • struct inet_sock:继承自struct sock,增加了 IPv4 相关的字段(如源 IP、目的 IP、TTL 等)。

  • struct inet_connection_sock:继承自struct inet_sock,增加了面向连接协议特有的字段(如三次握手队列、重传定时器、MSS 等)。UDP 不需要这个结构,TCP 需要。

  • struct tcp_sock:继承自struct inet_connection_sock,增加了 TCP 特有的字段(如滑动窗口、拥塞窗口、序列号、确认号等)。

内存布局:这些结构体在内存中是连续嵌套的。一个struct tcp_sock变量的首地址,同时也是struct inet_connection_sock、struct inet_sock和struct sock的首地址。因此,内核可以通过强制类型转换,实现类似 C++ 继承的“向上转型”。

用户调用socket()系统调用时发生了什么?

当用户调用int fd = socket(AF_INET, SOCK_STREAM, 0);时,内核会经历以下步骤:

第一步:创建struct socket

内核调用sock_create(),分配一个struct socket对象。此时它只是一个空壳,还没有绑定具体协议。

第二步:创建struct file

内核在文件系统中创建一个struct file对象,并将它的private_data指向刚刚创建的struct socket。同时,struct socket的file指针也指回这个struct file。这就是图中private_data和file互相指向的原因。

第三步:创建底层协议控制块(struct sock)

根据SOCK_STREAM参数,内核知道要使用 TCP 协议。

  1. 内核调用inet_create()。

  2. 根据协议类型,分配对应的底层结构。对于 TCP,它会分配一个struct tcp_sock对象(这个对象很大,包含了完整的struct sock、inet_sock等)。

  3. 初始化该结构,并将struct socket的sk指针指向这个struct tcp_sock对象。

  4. 关键点:struct socket的ops指针会被设置为inet_stream_ops(TCP 的操作函数集)。如果是 UDP,则会设置为inet_dgram_ops。

第四步:分配文件描述符(fd)

内核在当前进程的files_struct的fd_array[]中找一个空闲的位置,将struct file的指针填入,并返回数组下标作为 fd。

至此,用户拿到的fd就与内核的 TCP 协议栈建立了完整的映射关系。

TCP 与 UDP 在内核实现上的差异

这张图展示的是 TCP(因为有inet_connection_sock和tcp_sock)。如果是 UDP,结构会简化很多:

特性TCPUDP
底层结构tcp_sock→inet_connection_sock→inet_sock→sockudp_sock→inet_sock→sock
连接管理有inet_connection_sock,包含连接队列、重传定时器无连接,不需要inet_connection_sock
Socket 操作集inet_stream_opsinet_dgram_ops
发送数据tcp_sendmsg(可能阻塞、拆分、重传)udp_sendmsg(直接封装成包发出)
接收数据tcp_recvmsg(从接收队列读取,处理乱序、去重)udp_recvmsg(从接收队列读取,保留消息边界)
状态机复杂的 11 种状态(ESTABLISHED, TIME_WAIT 等)基本只有 CLOSE 和 ESTABLISHED(无连接)

UDP 为什么没有inet_connection_sock?
因为 UDP 是无连接的,不需要维护三次握手队列、重传定时器、滑动窗口等面向连接的特性。所以它的结构体层级更浅,内存占用更小,处理速度更快。

调用逻辑:从用户态到内核态的数据流

以 TCP 发送数据为例,说明这些结构体是如何协同工作的:

用户态:write(fd, buf, len) ↓ 系统调用:sys_write(fd, buf, len) ↓ VFS 层:根据 fd 找到 struct file ↓ Socket 层:调用 file->private_data->ops->sendmsg (即 struct socket 的 ops) ↓ INET 层:sock_sendmsg -> inet_sendmsg ↓ TCP 层:tcp_sendmsg(struct sock *sk, ...) ↓ 数据被放入 sk->sk_write_queue (发送队列) ↓ TCP 输出:tcp_write_xmit() 构建 TCP 报文段 ↓ IP 层:ip_queue_xmit() 添加 IP 头 ↓ 数据链路层:dev_queue_xmit() 发送到网卡

关键理解:

  • struct socket是门面,提供统一的文件系统接口(read/write/ioctl)。

  • struct sock(及其子类)是大脑,维护协议状态、队列、定时器。

  • 函数指针(ops)实现了多态。同一个write系统调用,对于 TCP 会走到tcp_sendmsg,对于 UDP 会走到udp_sendmsg。

TCP 协议栈是如何形成的?

这里的“协议栈”有两层含义:

1. 静态的代码栈(协议族注册)

Linux 内核启动时,会调用inet_init(),向内核注册 TCP 和 UDP 协议:

  • 注册tcp_prot(TCP 协议操作集)和udp_prot(UDP 协议操作集)。

  • 注册inet_stream_ops和inet_dgram_ops。

  • 注册 TCP 和 UDP 的收包处理函数(tcp_v4_rcv和udp_rcv)到 IP 层。

这样,当数据包到达时,IP 层知道该交给谁来处理。

2. 动态的连接栈(Socket 实例)

每次用户调用socket(),内核就动态生成一个struct tcp_sock实例。这个实例包含了该连接的所有状态(序列号、窗口、定时器等)。多个连接对应多个tcp_sock实例,它们共享同一套代码逻辑(tcp_prot)。

三次握手的过程,就是内核在这个动态实例上不断更新状态、收发队列、启动定时器的过程。

这张图揭示了 Linux 内核网络实现的三大设计哲学:

  1. 分层抽象:file→socket→sock,每层各司其职。

  2. 继承与多态:通过结构体嵌套和函数指针,实现 C 语言下的面向对象。

  3. 统一接口,差异化实现:用户只用read/write,内核根据SOCK_STREAM或SOCK_DGRAM走完全不同的协议路径。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 9:37:40

Agent Skills 技能体系设计实战:从零构建可维护、可测试的智能体

最近几周我一直在折腾一个叫agent-skills的项目&#xff0c;准确说不是某个开源仓库&#xff0c;而是我自己的一套 Agent 技能体系建设思路。折腾完之后最大的感受是&#xff1a;很多人把 Agent 开发想得太玄&#xff0c;其实真正决定它好用不好用的&#xff0c;往往不是底层大…

作者头像 李华
网站建设 2026/10/14 9:37:26

随机字符串在测试与安全中的技术应用

1. 项目概述"YEWRWREWREWWQEWQ"这个看似随机的字符串组合&#xff0c;实际上可能隐藏着多种潜在的技术应用场景。作为一名长期从事编码与数据处理的技术从业者&#xff0c;我见过不少类似的"无意义"字符串在实际项目中扮演着关键角色。这类字符串通常出现在…

作者头像 李华
网站建设 2026/10/14 9:37:07

让地图成为机器可读的空间服务:对话机器人与地理数据融合实践

lingbot-map这个项目名&#xff0c;拆开就是两件事&#xff1a;lingbot基本是从language bot压出来的&#xff0c;map不用解释&#xff0c;就是地图。合在一起&#xff0c;它指向的是一个把对话交互能力和空间数据能力绑在同一个体系里的项目——机器人能查地图、答方位&#x…

作者头像 李华
网站建设 2026/10/14 9:36:31

知识工作插件化流水线:从设计思路到二次开发实战

1. 这个插件仓库到底在解决什么问题第一次看到knowledge-work-plugins这个仓库名的时候&#xff0c;我下意识以为又是一个把各种零散脚本堆在一起的“杂物间”。点进去翻了两圈才发现&#xff0c;它其实是一套围绕知识工作者日常任务设计的插件集合&#xff0c;核心目标很明确&…

作者头像 李华
网站建设 2026/10/14 9:36:27

CSS 盒模型有什么区别?box-sizing 怎样影响元素宽高?

CSS 盒模型有什么区别&#xff1f;box-sizing 怎样影响元素宽高&#xff1f; CSS 盒模型从里到外是 content、padding、border 和 margin。 默认的 content-box 把 width 和 height 用在内容区上&#xff0c;内边距和边框要另外加。border-box 则让指定的宽高包含内容、内边距…

作者头像 李华