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则保持极简结构确保高效传输,体现了内核协议栈的精妙架构。

谢谢大家的观看!

相关推荐
Hui_AI7209 小时前
抖店铺货自动化:7个核心功能的技术实现方案
大数据·运维·人工智能·自动化·产品运营·ai写作·内容运营
yyuuuzz9 小时前
独立开发者线上服务运维的几点实践经验
运维·服务器·网络·云计算·aws
想唱rap9 小时前
IO多路转接Select
运维·服务器·网络·数据库·sql·tcp/ip·mysql
樱桃花下的小猫9 小时前
Rust 服务器倍率参数配置指南
服务器·云鸢互联·零门槛一键搭建·新手友好无技术门槛要求·腐蚀rust服务器一键开服·腐蚀rust·腐蚀rust低延迟稳定服务器
深藏bIue9 小时前
MySQL切换服务器数据迁移记录
服务器·mysql·oracle
云智慧AIOps社区9 小时前
轻帆云ITSM|制造业智能化转型,从流程重构看 IT 服务管理发展新趋势
运维·自动化·aiops·智能运维·itsm平台·it服务管理系统
闵孚龙9 小时前
Claude Code 技能系统全解析:AI Agent 自定义能力、SKILL.md、MCP 扩展、上下文预算与企业级自动化落地
运维·人工智能·自动化
沪漂阿龙9 小时前
Docker 面试题详解:容器、镜像、Dockerfile、网络、Volume、Compose、安全与生产实践一次讲透
网络·安全·docker
corpse20109 小时前
CentOS Linux release 8.5.2111下的CVE-2026-31431 Linux内核提权漏洞处置
linux·运维·centos