Linux 网络基础之传输层协议TCP(九)从内核源码的角度打通系统与网络之间的关系,套接字多态的体现

目录

一、内核源码

Task_struct

files_struct

[struct file](#struct file)

[socket struct](#socket struct)

[sock struct](#sock struct)

[sk_buff struct](#sk_buff struct)

tcp_struct

[inet_connection_sock struct](#inet_connection_sock struct)

[inet_sock struct](#inet_sock struct)

[sock struct](#sock struct)

udp_sock

二、多态

三、三次握手,套接字,内核结构体

服务端:

三次握手过程:

[accept() 返回新连接的套接字 :​](#accept() 返回新连接的套接字 :)

四次挥手

四、相关问题

五、总结


今天我们就顺着内核源码的脉络,从进程到文件系统,再到套接字抽象,最终深入 TCP 协议的底层结构体,一步步看清:一个用户态的 socket() 调用,在内核中到底对应了哪些数据结构?不同协议 (比如 TCP、UDP) 的套接字,又是如何通过 "多态" 的设计,共用同一套文件系统接口的?

接下来,我们会沿着这张目录里的结构体链条,逐层拆解它们的关系:从管理进程的 task_struct,到进程文件描述符表 files_struct,再到通用文件对象 struct file,然后进入套接字抽象层的 socket struct,再到网络层通用的 sock struct,最后落到 TCP 协议专属的 tcp_struct 和inet_connection_sock struct,把每一层的作用和关联讲透。

一、内核源码

Task_struct

首先是 task_struct,它是 Linux 内核中用来描述进程的内核结构体,每个进程在内核里都对应一个task_struct 实例。在它里面,有一个关键成员 struct files_struct *files,这个指针指向进程的文件管理结构,也就是我们接下来要讲的 files_struct,它是进程和所有打开文件(包括 socket)建立联系的入口。

files_struct

files_struct 结构体是进程的文件描述符管理器,维护着进程打开的所有文件的集合。它里面最核心的部分是 struct file *fd_array ,这是一个数组,数组的索引就是我们用户态里的 "文件描述符",而每个元素都是一个指向 struct file 的指针。也就是说,进程拿到的文件描述符,本质上就是这个数组的下标,通过它就能找到对应的 struct file 对象。

struct file

最后是 struct file 结构体,它是内核中所有打开文件的通用抽象,不管是普通文件、设备文件还是 socket,在内核里都用这个结构来表示。它里面有很多成员用来管理文件状态,比如文件偏移量 f_pos 、文件操作函数表 f_op 等,而和我们网络编程最相关的,就是 void *private_data 指针。这个指针是内核留给具体文件类型的 "私有数据" 字段,对于 socket 来说,内核会把它指向对应的套接字结构体,这样就把通用的 struct file 和具体的网络协议实现关联起来了。

struct file 里的 private_data 是一个 void* 类型,它的设计目的就是用来关联 "具体文件类型的私有数据",对内核来说,它只是一个无类型的指针。对于普通文件,它可能指向文件系统的私有结构;对于设备文件,它可能指向设备驱动的私有数据;而对于 socket 文件,它指向的就是 struct socket。

前面三者的关系可以简单概括为:进程的 task_struct 通过 files 指针找到 files_struct,再通过文件描述符作为下标,从 files_struct 的 fd_array \[\] 中找到对应的 struct file,最后通过struct file 的 private_data 指针,就能拿到 socket 在内核中的具体实现结构体。

下来我们三次握手建立连接,建立出来的TCP连接是要和文件描述符关联的

所以在内核当中就有一个套接字层,通用套接字层: 如下

socket struct

struct socket 是一个通用套接字,BSD 风格的通用套接字抽象,它和具体的协议无关,核心作用是提供一套统一的接口给用户态调用。我们看一下这个结构体的几个重要的成员 :

  1. struct file *file:这是一个回指指针,指向它自己对应的 struct file。内核需要通过这个指针,把套接字的文件操作和通用文件系统逻辑关联起来,比如文件引用计数、关闭时的资源回收都要用到它。

  2. struct sock *sk:这是指向具体协议套接字实现的指针,也就是后面我们要讲的 struct sock。struct socket 是通用层,而 struct sock 是 TCP/UDP 等协议的具体实现,通过这个指针,通用层就能调用到具体协议的逻辑。

  3. wait_queue_head_t wait:这个就是进程的等待队列。当用户态调用 read()、write() 或 accept() 时,如果套接字没有数据可读、无法发送数据或没有新连接,进程就会被阻塞,内核会把进程挂到这个等待队列里,直到条件满足(比如收到数据、发送窗口有空间、新连接到来)再唤醒它。这就是你说的 "阻塞等待" 的底层实现。

sock struct

struct sock 是所有网络协议套接字的通用基类(内核里用 C 语言实现的多态),TCP、UDP 等协议的具体套接字结构体,都是以 struct sock 为开头扩展出来的。它是内核中所有套接字操作的核心载体,我们之前学的滑动窗口、流量控制、拥塞控制,全都是基于这个结构体实现的。

我们前面一直提到的接收 / 发送缓冲区,也就是 sk_receive_queue 和 sk_write_queue,它们本质上是由 sk_buff(内核套接字缓冲区)组成的双向链表队列:

  1. sk_receive_queue:接收队列,网卡收到的数据帧会被封装成 sk_buff,挂到这个队列里,等待用户态通过 read()/recv() 读取。

  2. sk_write_queue:发送队列,用户态调用 write()/send() 发送的数据,会先被封装成 sk_buff 挂到这个队列里,再由内核协议栈按规则发送出去。

这两个队列,就是我们之前说的 "发送 / 接收缓冲区" 的底层实现,逻辑上我们把它当成一块连续的缓冲区,但内核里是用链表管理的一个个 sk_buff 节点。

sk_buff struct

我们前面提到的发送/接收缓冲区,本质上都是由 sk_buff 组成的双向链表队列,而 struct sk_buff 就是 Linux 内核中用来表示一个网络数据包的核心结构体,它是整个网络协议栈处理数据的 "通用容器"。我们看一下几个关键成员 :

  1. struct sk_buff *next 和 struct sk_buff *prev:这两个指针是用来把多个 sk_buff 串成双向链表的,对应着 struct sock 里的 sk_receive_queue 和 sk_write_queue,让内核可以按队列顺序管理和处理数据包。

  2. struct sock *sk:这个指针指向这个数据包所属的套接字 struct sock,内核通过它知道这个数据包属于哪个连接,能正确地把它放到对应的接收队列里,或者从对应的发送队列取出发送。

  3. union h、nh、mac 里的各种协议头指针:比如 struct tcphdr *th、struct iphdr *iph 等,这些是内核为了方便协议栈各层快速访问数据包头部而准备的指针。当数据包从链路层往上传递时,内核会依次设置好 MAC 层、IP 层、传输层的头部指针,这样在处理 TCP、UDP 等协议时,直接通过 th、uh 就能拿到对应协议头,不用自己解析偏移量,大大提高了处理效率。

简单来说,sk_buff 就是一个带着 "元数据" 的数据包容器,它不仅存着数据本身,还记录了这个数据包属于哪个连接、处于协议栈的哪一层、各个协议头的位置,以及和其他数据包的链表关系,内核里所有的网络数据处理,都是围绕着这个结构体展开的。


tcp_struct

三次握手成功建立连接后,其实在底层真正创建的是 tcp_socket 结构体,建立连接后的连接结构体就在这个 tcp_struct 结构体内:

struct tcp_sock 是 TCP 协议的终极结构体,三次握手成功后,内核里维护这个 TCP 连接状态的核心对象就是它。我们之前学到的滑动窗口、流量控制、拥塞控制、序号管理,全都是在这个结构体里实现的。它里面的字段,每一个都对应着我们讲过的 TCP 核心机制:

  1. rcv_nxt / snd_nxt:接收和发送的下一个序号,对应确认应答机制里的序号管理,保证数据按序到达、不重复、不丢失。

  2. snd_wnd:发送窗口大小,就是我们说的滑动窗口里的接收方通告窗口,由接收方通过 ACK 报文更新,直接决定了发送方一次能发多少数据。

  3. window_clamp:窗口大小上限,用来限制接收方通告的窗口,避免窗口膨胀过大导致拥塞。

  4. snd_cwnd:拥塞窗口大小,是拥塞控制的核心变量,慢启动、拥塞避免、快重传等算法,全都是通过调整这个字段来控制发送速率的。

  5. snd_ssthresh:慢启动阈值,是慢启动阶段和拥塞避免阶段的分界点,也是拥塞控制的关键参数。

简单说,struct tcp_sock 就是一个 TCP 连接的 "控制中心",我们之前学的所有 TCP 状态和算法,最终都要映射到这里的字段上。它的第一个成员是 struct inet_connection_sock,这是内核里典型的 "C 语言继承" 写法,让它能复用 INET 层的连接管理逻辑。

inet_connection_sock struct

tcp_struct 中第一个字段 struct inet_connection_sock inet_conn 结构体很重要,这个结构体就是我们说的连接结构体

struct inet_connection_sock 是面向连接的 INET 协议(比如 TCP)的通用管理结构体,"三次握手建立的连接结构体",它就是这个层级的通用载体,TCP 的 struct tcp_sock 只是在它的基础上扩展了 TCP 专属的字段。

它里面的核心字段,全都是为了维护连接状态和异常处理设计的:

  1. icsk_accept_queue:全连接队列,就是服务器 listen() 时维护的、三次握手完成的连接队列,用户态的 accept() 就是从这里取新连接的。

  2. icsk_retransmit_timer:重传定时器,对应超时重传机制,当发送的报文没有收到 ACK 时,这个定时器会触发重传逻辑。

  3. icsk_ca_ops:拥塞控制算法钩子,内核支持多种拥塞控制算法(比如 reno、cubic),通过这个函数指针集合来注入不同的算法实现,TCP 的拥塞控制就是通过这个接口调用的。

  4. icsk_syn_retries:SYN 重传次数,对应三次握手时客户端 SYN 报文的重试机制。

  5. icsk_delack_timer:延迟应答定时器,对应延迟应答机制,用来控制 ACK 报文的延迟发送时间。

它的第一个成员是 struct inet_sock,同样是 "继承" 关系,让它能复用 IP 层的地址信息和通用套接字逻辑。

下面是一些相关字段的含义 :

inet_sock struct

最重要的依然是第一个字段 struct inet_sock icsk_inet,这个结构体就和网络相关了,属于网络IP层,是我们下一部分要学习的内容:

它里面的核心字段,都是网络通信的基础信息:

  1. struct sock sk:它继承的通用套接字基类,就是我们之前讲的 struct sock,所有套接字的通用队列、状态管理都在这里。

  2. saddr / daddr:源 IP 地址和目的 IP 地址,对应三次握手和数据传输时的 IP 层地址信息。

  3. sport / dport:源端口号和目的端口号,对应传输层的端口标识,内核通过这四个字段(源 IP + 源端口 + 目的 IP + 目的端口)来唯一标识一个 TCP 连接。

  4. tos / ttl:IP 头部的服务类型和生存时间字段,用来控制 IP 报文的路由优先级和跳数限制。

简单说,struct inet_sock 是整个套接字的 "IP 层底座",不管是 TCP 还是 UDP,只要是基于 IP 的通信,都要用到这些地址信息。

把 ip,端口号其实就设置在这里了

sock struct

inet_sock struct 结构体中最重要的依然是第一个字段 struct sock sk,这个 struct sock 怎么感觉似曾相识,是的,前面的通用套接字 struct socket 中有一个字段就直接指向了这个 struct sock , 也就是指向了整个 tcp_socket 中最顶级的成员,未来我们如果想访问网络层相关的字段,就将 sk 指针强转,当需要访问 IP 层地址信息时,强转为 struct inet_sock*;当需要处理连接状态、重传定时器时,强转为 struct inet_connection_sock*;当需要操作 TCP 的序号、窗口和拥塞控制参数时,再强转为 struct tcp_sock*,从而实现了 "一指针多用" 的多态效果。所以我们就可以用一个指针通过强转指向多个不同类型的结构体


udp_sock

更重要的是之前我们学习过的 udp,struct udp_sock是 UDP 协议在内核中的专属结构体,tcp_struct 和 udp_struct 不同的是,tcp_struct 有 struct inet_connection_sock 结构体,struct inet_connection_sock 结构体内部有 struct inet_sock 结构体,而 udp_struct 结构体内部没有 struct inet_connection_sock 结构体,而是直接包含了 struct inet_sock 结构体,原因是什么?

原因就是 udp 通信无连接。UDP 不需要三次握手四次挥手,也不需要全连接队列、拥塞控制这些面向连接的机制,所以它的结构体里自然也就没有 struct inet_connection_sock 这一层。它的第一个成员直接是 struct inet_sock,继承了 IP 层的地址信息和通用套接字逻辑,再加上几个 UDP 专属的字段,就构成了完整的 UDP 套接字结构。

这种设计上的差异,本质上就是协议特性在内核结构体上的直接体现:面向连接的 TCP 需要多层嵌套的结构体来支撑复杂的状态管理,而无连接的 UDP 则可以用极简的结构来实现高效的数据传输。

二、多态

所以 udp 的模型图就是

这就是多态的体现! struct sock 就是基类,底层实现了 tcp_struct 和 udp_struct 子类

所以这也就是为什么 struct socket 是通用套接字层,因为底层实现了多态

从用户态文件描述符到内核协议实现的链路中,Linux 套接字的多态设计得到了清晰体现,TCP 和 UDP 共享同一套用户态接口,却在内核中对应着完全不同的协议实现。

对于 TCP 而言,当用户态创建套接字时,内核会为其创建一个通用的 struct socket,并通过 struct file 的 private_data 指针与文件系统关联起来。struct socket 内部的 struct sock *sk 指针,指向的是一个 struct tcp_sock 实例,而这个实例以 C 语言的方式实现了 "继承",其结构体内依次包含了 struct inet_connection_sock、struct inet_sock 和 struct sock 作为首成员。这种设计使得内核既可以通过 struct sock* sk 这一基类指针,调用所有套接字通用的队列管理和状态处理逻辑,也可以根据需要将其安全地强转为 struct inet_connection_sock*,或 struct tcp_sock*,或强转为 struct inet_sock* 。整个过程中,用户态只需要通过文件描述符进行读写,完全感知不到内核中这些复杂的结构体层级和类型转换,这正是多态带来的解耦效果。

而 UDP 作为无连接协议,其多态实现同样遵循这一设计,但结构更为简洁。UDP 的套接字链路同样始于 struct file 和 struct socket,struct sock *sk 指向的是 struct udp_sock 实例,它直接以struct inet_sock 作为首成员,跳过了 TCP 中 struct inet_connection_sock 这一层。这是因为 UDP 不需要维护连接状态、定时器和队列管理,因此 struct udp_sock 只需继承 IP 层的地址信息,再加上少量无连接场景下的优化字段,就能实现高效的数据传输。内核同样可以通过 struct sock* 基类指针,统一处理 UDP 套接字的收发队列,再根据需要强转为 struct inet_sock* 获取 IP 和端口信息,或是强转为 struct udp_sock* 处理 UDP 专属的发送合并等逻辑。

TCP 和 UDP 在内核中不同的结构体层级,正是它们协议特性的直接体现:TCP 的多层嵌套支撑了面向连接的复杂状态管理,而 UDP 的扁平结构则服务于无连接的高效传输。但两者都通过 struct sock 基类和强转机制,实现了对用户态文件系统接口的统一适配,让应用层无需关心底层协议差异,就能使用同一套接口进行网络通信,这就是 Linux 套接字多态设计的精髓所在。

三、三次握手,套接字,内核结构体

服务端:

用户态调用 socket():

用户态调用 bind() 和 listen():

用户态调用 accept():

三次握手过程:

accept() 返回新连接的套接字 :

四次挥手

应用层调用 close,本质是主动告知内核不再发送数据,内核随即在对应tcp_sock中发起四次挥手流程。主动关闭端关闭写通道,发出 FIN 报文进入对应等待状态,收到确认后等待对方关闭信号;被动端收到 FIN 后先回复确认,应用层处理完剩余数据再调用 close 发送 FIN,走完剩余确认流程。主动端进入 TIME_WAIT 阶段等待超时,确保最后报文可靠送达后释放套接字所有内核结构体与资源,被动端收到最后确认便直接完成资源释放,双方彻底断开通信,同时读写缓冲区队列数据也随之完成清理回收。

客户端或者服务端任意一端调用 close,就代表本机不再往外发数据,直接触发内核开始执行四次挥手断开流程,这就是断开连接正式开始的标志。close 只是应用层发起断开动作,真正可靠断连,必须依靠内核四次挥手完成状态流转、报文确认与资源收尾。一端 close 只关闭自己的写方向,还能继续读完对方发来剩余数据,等两端都执行 close,四次挥手完整走完,连接才算彻底终结。

四、相关问题

有个疑问,就是比如说客户端和服务端通信,有多个客户端,比如说抖音,像抖音这类平台存在上亿用户同时访问服务端,是不是每一组客户端与服务端建立的 TCP 连接,都会在双方各自操作系统内核中,单独创建全套套接字结构体,由两端内核分别维护对应连接相关传输逻辑?

是的,这个理解是对的,每一个抖音用户手机,和抖音后台服务器机房,只要建立一条 TCP 通信,就在双方各自操作系统内核里,创建一整套我们上面讲过的所有套接字结构体,两边内核各自维护自己这一侧的连接状态、队列、窗口、序号、定时器。

首先,一台服务器能同时承载成千上万客户端连接,原理就是服务器内核可以批量创建海量独立的tcp_sock 连接结构体。服务器只保留一个监听套接字用来监听请求,每接入一个新用户,内核就立刻新建一套全新独立的 inet_sock、inet_connection_sock、tcp_sock,搭配专属收发 sk_buff 队列,每一条用户连接都是互相独立互不干扰的内核连接对象,各自走自己的三次握手、数据传输、拥塞控制、四次挥手流程。

客户端这边也是同理,我们手机每连上一个抖音服务端口,手机系统内核也会生成属于本机这一侧完整的一套套接字结构体,用来管理本机发送、接收数据,维护本机的滑动窗口、确认应答与重传机制。

也就是说一条 TCP 连接,是两端操作系统内核共同配合维持出来的逻辑链路,本机内核管本机这一半连接状态,服务端内核管服务器那一半连接状态,两边通过网络报文互相同步序号、窗口、确认信息,完成可靠传输。成千上万用户同时在线,本质就是服务器内核中同时存在成千上万个相互独立、互不冲突的 tcp_sock 连接实体,每一个都对应一位用户的通信链路,用户下线关闭 APP 调用关闭套接字,就触发两端各自内核开启四次挥手,走完状态释放流程,彻底清空这一组内核结构体资源。

抖音服务端之所以能撑住上亿级别的访问量,不只是服务器硬件性能强悍,更关键是 Linux 内核网络栈本身对高并发连接做了极致优化,配合多路复用、连接池、短连接复用、协议轻量化调度,再加上分布式集群拆分流量,把海量用户连接分摊到无数台服务器节点上,单台机器承载数万甚至十几万连接都轻轻松松。用户刷视频、滑动浏览都是频繁建立和释放连接,内核快速创建、快速回收套接字结构体资源,配合四次挥手合理释放闲置连接,最大限度节省内存与内核开销,这才是大型互联网平台能撑起海量用户同时在线的底层根本。

五、总结

本文深入解析了Linux内核中TCP/UDP套接字的多态实现机制。从用户态文件描述符出发,通过task_struct、files_struct和structfile逐层递进,揭示了socket在内核中的数据结构链:通用套接字层structsocket通过private_data指针关联具体协议实现,TCP采用多层嵌套结构(tcp_sock→inet_connection_sock→inet_sock→sock) 支持连接管理,而UDP则直接继承inet_sock实现无连接传输。这种基于C语言强转的多态设计,使不同协议能共享统一文件接口,TCP通过复杂结构体实现流量控制等机制,UDP则保持极简结构确保高效传输,体现了内核协议栈的精妙架构。

谢谢大家的观看!

相关推荐
大树889 分钟前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠12 分钟前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质36 分钟前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush439 分钟前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5201 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz1 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工2 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
网络研究院2 小时前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智2 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
treesforest2 小时前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全