从内核数据结构的角度理解socket
目录
一、socket核心:内核数据结构的三角关系
(1)struct socket:网络协议的抽象接口
(2)struct sock:传输层协议的控制中心
(3)struct file与files struct:文件系统的接入层
二、Socket调用全流程
(1)socket():创建核心结构,奠定基础协议
(2)bind():绑定地址,确定网络身份
(3)监听与连接:TCP 特有流程,依赖队列管理
1.listen():初始化连接队列,进入监听状态
2.connect():客户端发起连接,触发三次握手
3.accept():提取已完成连接,创建新套接字
(4)数据传输:read()/write() 与缓冲区管理
1.TCP 数据传输:可靠传输的缓冲区协作
2.UDP 数据传输:简单无状态的队列管理
(5)close():释放资源,终止连接
在Linux网络编程中,socket是连接用户态和内核态的核心接口。从socket创建套接字到close关闭套接字,每一步都伴随着内核数据结构的创建、修改与销毁。本文将从内核数据结构的角度,详解socket的调用流程,重点对比TCP、UDP在数据结构上的设计差异,揭示网络是如何统一到文件系统中的。
从数据结构视角看,Socket 的调用过程本质是内核通过 “三层结构”(files_struct
→struct file
→struct socket
→struct sock
)实现的资源管理流程。TCP 因需可靠传输和连接管理,其 struct sock
设计复杂,包含大量状态和队列字段;UDP 作为无连接协议,结构精简,聚焦高效数据转发。
这种 “统一框架(文件系统 + socket 抽象)+ 差异化实现(协议专属 struct sock
)” 的设计,既保证了用户态接口的一致性(均通过 fd
操作),又满足了不同协议的特性需求,是 Linux 内核 “抽象复用” 与 “按需扩展” 思想的经典体现。理解这些数据结构的变化,能帮助我们更深入地掌握网络编程的本质,优化程序性能与可靠性。
一、socket核心:内核数据结构的三角关系
Linux遵循一切皆文件的设计哲学,socket作为特殊的网络文件,其管理依赖三个核心数据结构的协作,这也是理解socket操作的基础。
(1)struct socket:网络协议的抽象接口
struct socket是Socket的“管理层”结构,负责衔接用户态接口与内核协议逻辑。其中的ops字段指向协议专属的操作集(如TCP的tcp_prot_ops UDP 的udp_prot_ops),sk字段指向协议实现的核心数据结构struct sock。
这个结构体也是最上层操作的,聚焦于对外(用户)暴露状态等。当调用如bind的时候,会从这个结构体中找到对应的操作集,然后调用其中的操作函数,从而修改struct sock的成员变量。
struct socket中暴露给用户的状态较为简单,只有“未连接、已连接、正在连接、监听”等通用流程状态,目的是为了让用户知道该套接字处于流程的哪一步。而struct sock则更加复杂详细,在TCP中会有TCP_CLOSED\ESTABLISHED\SYN_SENT等11个状态;在UDP中由于其不是面向连接的协议,所以状态也会较为简单,通常只有“空闲、已绑定”等基础状态。
至于这里的struct file指针,则是“一切皆文件”的体现,socket本质也是一种文件,所有每一个socket套接字会有一个fd文件描述符,而struct file中的private date也会指向该socket,是一种双向查找的关系。
(2)struct sock:传输层协议的控制中心
struct sock是传输层协议的 “实干层” 结构,存储了协议运行的关键状态与资源。TCP 因需要可靠传输和连接管理,其 struct sock包含大量特有字段(如连接队列、滑动窗口);而 UDP 作为无连接协议,结构更精简,仅保留基础的地址和缓冲区信息。
这里可以看到是利用联合体实现的多态。不过公共的字段(如发送缓冲区、接收缓冲区、IP地址、操作集等)。联合体的特点是他本身不知道自己是什么类型,谁填充这个字段,谁就负责管理他,所以这里有一个协议操作集,他们用于操作struct socket本身。体现一个层级架构。
操作集在用户调用socket系统调用的时候,已经告诉了内核这个套接字应用什么协议SOCK_DGRAM、SOCK_STREAM,所以内核就会根据其填充他的操作集ops。
如果未来要新增传输协议,比如替代TCP的某一部分,只需要定义自己的struct proto_ops,然后稍稍修改一下内核中socket创建时ops的指向即可。
(3)struct file与files struct:文件系统的接入层
socket需要通过文件描述符fd被用户操作,这依赖文件系统的核心结构。
struct file是 Socket 接入文件系统的 “适配器”,通过private_data关联struct socket;filese_struct则管理进程的所有fd,确保每个 Socket 对应唯一的fd索引。
而在struct file中我们之前说过有个成员是f_ops,他是这个文件的操作集。不同类型的文件操作集指向的函数是不一样的。
二、Socket调用全流程
(1)socket():创建核心结构,奠定基础协议
用户态调用:
int fd = socket(AF_INET, SOCK_STREAM, 0); // 创建 TCP 套接字
// 或
int fd = socket(AF_INET, SOCK_DGRAM, 0); // 创建 UDP 套接字
内核操作与结构变化:
核心差异:TCP 的 struct sock
会预分配连接管理所需的内存(如 request_sock_queue
),而 UDP 直接初始化缓冲区队列,结构更轻量。
(2)bind():绑定地址,确定网络身份
用户态调用:
struct sockaddr_in addr = {.sin_family = AF_INET,.sin_port = htons(8080),.sin_addr.s_addr = INADDR_ANY
};
bind(fd, (struct sockaddr*)&addr, sizeof(addr));
这里有sockaddr_in和sockaddr两个结构体,他们体现了C语言版本的继承。无论是父类还是子类都是相同的大小,但是第一位成员永远是协议族,当访问到第一个成员之后,就可以按照该协议进行解析这个sockaddr类,从而获取有效信息。这也是必须要填入协议族的原因。
在sockaddr中是由2位协议类型+14位空白字节组成的。可以看做基类
而sockaddr_in则是对他进行重写,前两位仍然表示协议族,后面14个字节则划分成了端口号、IPV4地址、和8位空白字段。可以看做是子类。
除了他们,还有许多的“子类”。如sockaddr_in6、sockaddr_un、sockaddr_nl等,他们分别用于不同的场景。
内核操作与结构变化:
核心差异:绑定操作对 TCP 和 UDP 逻辑相同,均是填充地址信息,无协议特有逻辑。
(3)监听与连接:TCP 特有流程,依赖队列管理
1.listen()
:初始化连接队列,进入监听状态
用户态调用:
listen(fd, 10); // 最大已完成连接队列长度为 10
内核操作与结构变化:
核心差异:UDP 无 listen()
操作,其 struct sock
无连接队列字段,无需初始化。
2.connect()
:客户端发起连接,触发三次握手
用户态调用(客户端):
connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
内核操作与结构变化:
核心差异:UDP 的 connect()
仅在 struct sock
中记录远程地址(sk_daddr
),无三次握手,不改变状态(仍为无连接)。
3.accept()
:提取已完成连接,创建新套接字
用户态调用(服务器):
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);
内核操作与结构变化:
核心差异:UDP 无 accept()
操作,数据传输直接使用绑定的套接字,无需创建新套接字。
(4)数据传输:read()
/write()
与缓冲区管理
TCP 和 UDP 的数据传输均依赖struct sock中的数据发送 / 接收队列(sk_write_queue
/sk_receive_queue
),但因协议特性,管理逻辑差异显著,且实现细节也大不相同。
1.TCP 数据传输:可靠传输的缓冲区协作
2.UDP 数据传输:简单无状态的队列管理
(5)close():释放资源,终止连接
用户态调用:
close(fd);
内核操作与结构变化: