深入理解 Linux 阻塞IO与Socket数据结构
一、阻塞IO的直观演示
示例代码:最简单的阻塞接收程序
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>int main() {// 创建TCP套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);// 绑定地址端口struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(8080);addr.sin_addr.s_addr = INADDR_ANY;bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));// 开始监听listen(sockfd, 5);printf("等待客户端连接...\n");// 阻塞点1:接受连接struct sockaddr_in client_addr;socklen_t len = sizeof(client_addr);int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &len);printf("客户端已连接!\n");// 阻塞点2:接收数据char buf[1024];int ret = recv(clientfd, buf, sizeof(buf), 0);printf("收到数据:%s\n", buf);close(clientfd);close(sockfd);return 0;
}
以下是等效的 Java 版本实现,保留了阻塞 IO 的特性并添加了详细注释:
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;public class BlockingIOServer {public static void main(String[] args) {// 创建TCP套接字并绑定端口(对应C的socket+bind+listen)try (ServerSocket serverSocket = new ServerSocket(8080)) {System.out.println("等待客户端连接...");// 阻塞点1:接受客户端连接(对应C的accept)Socket clientSocket = serverSocket.accept();System.out.println("客户端已连接!");// 阻塞点2:接收数据(对应C的recv)InputStream inputStream = clientSocket.getInputStream();byte[] buffer = new byte[1024];// read()方法会阻塞直到有数据到达int bytesRead = inputStream.read(buffer); System.out.println("收到数据:" + new String(buffer, 0, bytesRead));// 自动关闭资源(Java 7+ try-with-resources)} catch (IOException e) {e.printStackTrace();}}
}
其实本质上java的代码和C语言代码是一样的,都是调用对应的系统函数。
阻塞行为分析
accept()
调用阻塞:直到有客户端连接才会继续执行recv()
调用阻塞:当连接建立后,如果客户端不发送数据,进程会一直挂起
二、Socket全生命周期
2.1 创建阶段:从用户态到内核态的旅程
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
流程详解(附内核调用链):
用户空间调用 socket()
↓
系统调用 sys_socket() → 陷入内核
↓
sock_create() → 创建通用socket对象
↓
查找协议族 net_families[AF_INET] → 获取IPv4协议族操作表
↓
调用 inet_create() → 创建TCP/UDP专用socket
│ ↓
分配 struct sock 结构 → 初始化连接状态为 TCP_CLOSE
↓
注册协议操作函数 → 设置 tcp_prot(TCP协议处理引擎)
↓
关联文件描述符 → 通过 file_operations 绑定读写方法
关键步骤深度解析:
-
协议族选择
// net/socket.c static int __init sock_init(void) {// 初始化时注册协议族rc = netlink_kernel_create(&init_net, NETLINK_ROUTE, ...);rc = inet_add_protocol(&icmp_protocol, IPPROTO_ICMP);... }
net_families[AF_INET]
指向ipprot
结构,包含IPv4协议处理函数
-
传输层协议绑定
// net/ipv4/af_inet.c int inet_create(struct net *net, struct socket *sock, int protocol) {// 根据协议类型选择处理引擎if (protocol == IPPROTO_TCP)sock->ops = &inet_stream_ops; // TCP操作集else if (protocol == IPPROTO_UDP)sock->ops = &inet_dgram_ops; // UDP操作集// 分配TCP专用数据结构sk = sk_alloc(net, PF_INET, GFP_KERNEL, &tcp_prot, 0); }
-
资源预分配
- 预分配接收队列缓存:
sk->sk_receive_queue
(基于内存池的sk_buff分配) - 初始化等待队列:
sk->sk_sleep
(后续阻塞操作的基础设施)
- 预分配接收队列缓存:
2.2 连接建立阶段:TCP三次握手的微观视角
connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
状态转换全景图:
TCP_CLOSE → TCP_SYN_SENT → TCP_ESTABLISHED↑ ↑ ↑SYN SYN-ACK ACK
内核处理流水线:
用户调用 connect()
↓
tcp_connect() → 设置状态为 TCP_SYN_SENT
↓
生成SYN报文 → 调用 ip_queue_xmit() 发送
↓
进入状态机等待 → sk->sk_state = TCP_SYN_SENT
↓
等待ACK到达 → 触发 sk_state_change 回调
关键机制详解:
-
SYN报文发送
// net/ipv4/tcp_output.c int tcp_connect(struct sock *sk) {// 构建SYN报文struct sk_buff *skb = alloc_skb(sizeof(struct tcphdr) + ...);tcp_init_nondata_skb(skb, tcp_current_seq(sk), TCPHDR_SYN);// 发送队列管理skb_queue_tail(&sk->sk_write_queue, skb);ip_queue_xmit(sk, NULL, skb); }
-
等待队列机制
// 当前进程进入等待状态 set_current_state(TASK_INTERRUPTIBLE); add_wait_queue_exclusive(&sk->sk_sleep, &wait);while (sk->sk_state != TCP_ESTABLISHED) {if (signal_pending(current))return -EINTR;schedule(); }
-
状态机驱动
// net/ipv4/tcp_states.h static const struct tcp_state_trans tcp_established_transitions = {.transitions = {[TCP_ESTABLISHED] = {.event = TCP_EARLY_DATA,.next_state = TCP_ESTABLISHED,.action = tcp_rcv_established,},// 处理ACK包的回调注册}, };
阻塞行为本质:
当调用 connect()
时:
- 若端口不可用/连接被拒绝 → 立即返回错误
- 若正常发送SYN → 进程进入
TASK_UNINTERRUPTIBLE
等待 - 当收到SYN-ACK后 → 内核完成握手 → 触发
sk->sk_data_ready
- 最终唤醒进程 → 返回成功
2.3 生命周期全景图
各阶段内存管理:
- 创建阶段:预分配接收缓冲区(
sk->sk_rmem_alloc
) - 传输阶段:动态调整发送窗口(
sk->snd_wnd
) - 关闭阶段:释放关联的skb队列
三、核心数据结构详解
3.1 socket结构体家族树
struct socket
├── struct file (VFS层对象)
└── struct sock (协议无关层)├── struct tcp_sock (TCP协议私有数据)└── struct udp_sock (UDP协议私有数据)
具体解释见下面
3.2 关键数据结构详解
3.2.1 struct socket:用户态与内核态的桥梁
struct socket {const struct proto_ops *ops; // 协议操作函数表(TCP/UDP/SCTP等)struct sock *sk; // 核心协议栈对象(传输层控制块)struct file *file; // 关联的文件描述符(VFS接口)
};
核心功能解析:
- ops指向协议族操作表(如inet_stream_ops),代码如下:
const struct proto_ops inet_stream_ops = {.family = PF_INET,.recvmsg = inet_recvmsg, // 接收消息入口.sendmsg = inet_sendmsg, // 发送消息入口.accept = inet_accept, // 接受新连接.bind = inet_bind, // 绑定端口...
};
通过函数指针实现协议无关接口,支持多协议扩展,类比Java接口实现:就像Java中DataSource接口可以有不同的实现类(MySQL/Oracle),C语言通过proto_ops函数指针数组实现协议多态。当用户调用read()系统调用时,最终会通过socket->ops->recvmsg调用具体协议的接收函数。例如udp协议的接收消息,这里的recvmsg会指向udp的接收实现,如果是tcp协议,recvmsg那么就指向tcp的接收实现(经过三次握手后,可以接收消息)。
- *sk,核心协议栈对象,下面会有解释,此处略
- *file,关联的文件描述符,file指针将socket映射到文件系统,实现read()/write()等文件操作语义
3.2.2 struct sock
struct sock {const struct proto *sk_prot; // 协议处理函数(如 tcp_prot)struct sk_buff_head sk_receive_queue; // 接收队列wait_queue_head_t sk_sleep; // 进程等待队列头 sk_wqvoid (*sk_data_ready)(struct sock *sk); // 数据就绪通知回调// ...其他字段
};
- 协议处理引擎 sk_prot
// TCP协议处理结构体
struct proto tcp_prot = {.name = "TCP",.err_handler = tcp_err, // 错误处理.recvmsg = tcp_recvmsg, // 数据接收(含流量控制).sendmsg = tcp_sendmsg, // 数据发送(含拥塞控制)//...其它字段
};
工作流程示例:当应用层调用send()
时,数据会经过以下路径:
用户空间缓冲区 → socket->ops->sendmsg → tcp_sendmsg → 协议栈处理 → 驱动程序发送
- 接收队列 sk_receive_queue,即网络数据包
- 等待队列 sk_sleep,阻塞行为本质:当接收队列为空时,进程会被挂入
sk_sleep
队列,直到有数据到达触发唤醒。 sk_data_ready
负责在数据就绪时触发进程唤醒,是阻塞IO模型的核心机制。具体看下文。
四、阻塞IO唤醒机制
4.1 sk_data_ready
的本质
定义:
sk_data_ready
是 struct sock
中定义的一个函数指针,其类型为 void (*sk_data_ready)(struct sock *sk)
。它是 协议栈向应用层传递数据就绪通知的核心机制,所有传输层协议(TCP/UDP)都需要实现此回调。
代码定位:
// net/core/sock.h
struct sock {...void (*sk_data_ready)(struct sock *sk); // 数据就绪通知回调...
};
4.2 工作流程:从网卡到应用层
以下是 sk_data_ready
被触发的完整链路(以TCP为例):
graph TDA[网卡接收数据包] --> B[硬件中断]B --> C[软中断(napi_schedule)]C --> D[napi_poll处理]D --> E[ip_rcv() → tcp_v4_rcv()]E --> F[协议处理(tcp_rcv)]F --> G[调用sk_data_ready(sk)]G --> H[唤醒等待队列(sk_sleep)]H --> I[应用层recv()返回数据]
关键步骤解析:
- 中断阶段:网卡收到数据包触发硬件中断,注册的中断处理函数标记
NAPI
结构体。 - 软中断阶段:内核的
ksoftirqd
线程执行napi_poll()
,开始处理接收队列。 - 协议栈处理:数据包经过IP层、TCP层解析,最终进入
tcp_v4_rcv()
。 - 触发回调:在协议处理完成后,内核调用
sk->sk_data_ready(sk)
。 - 唤醒进程:
sk_data_ready
的默认实现会调用sk_wake_async()
,最终通过wake_up_interruptible(&sk->sk_sleep)
唤醒等待队列中的进程。
4.3、sk_data_ready
与阻塞IO的阻塞/唤醒机制
4.3 1. 阻塞IO的核心逻辑
当应用层调用阻塞型 recv()
时:
// 系统调用入口 sys_recvfrom
if (skb_queue_empty(&sk->sk_receive_queue)) {// 数据未就绪,进程进入睡眠set_current_state(TASK_INTERRUPTIBLE);add_wait_queue(&sk->sk_sleep, &wait);schedule(); // 主动让出CPUremove_wait_queue(...);
}
4.3.2. sk_data_ready
的触发时机
- 数据就绪时:当
sk_data_ready
被调用时,会触发以下动作:// 默认实现(net/core/sock.c) static inline void sock_def_readable(struct sock *sk, int len) {if (!sock_flag(sk, SOCK_DEAD)) {// 唤醒所有在sk_sleep队列中等待的进程wake_up_interruptible(&sk->sk_sleep);// 触发异步通知(如信号)sk_wake_async(sk, SOCK_WAKE_IO, POLL_IN);} }
4.3.3. 阻塞IO的唤醒本质
- 等待队列(Wait Queue):进程在调用阻塞型
recv()
时会被加入sk->sk_sleep
队列,并设置为不可中断状态(TASK_INTERRUPTIBLE
)。 - 唤醒条件:只有当
sk_data_ready
被调用时,才会触发队列唤醒。这意味着:- 数据必须通过协议栈处理完成(如TCP三次握手完成、数据包校验通过)。
- 内核协议栈确认数据已准备好被用户空间读取。
总结
通过本文的学习,你应该已经掌握:
- 阻塞IO的底层行为模式
- Socket从创建到数据传输的完整生命周期
- 核心数据结构(socket/sock/sk_buff)的协作关系
- 系统调用到内核处理的完整链路
参考文档
- 深入理解Linux网络: 修炼底层内功,掌握高性能原理 (张彦飞)
相关文章:

深入理解 Linux 阻塞IO与Socket数据结构
一、阻塞IO的直观演示 示例代码:最简单的阻塞接收程序 #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h>int main() {// 创建TCP套接字int sockfd socket(AF_INET, SOCK_STREAM, 0);// 绑定地址端口struct sockaddr_in ad…...
DAY 17 训练
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 DAY 17 训练 聚类算法聚类评估指标介绍1. 轮廓系数 (Silhouette Score)2. CH 指数 (Calinski-Harabasz Index)3. DB 指数 (Davies-Bouldin Index) 1. KMeans 聚类算法原理确定…...

如何修改进程优先级?
文章目录 1. 摘要2. 命令实现2.1 使用 renice(调整普通进程的优先级)2.2 使用 chrt(调整实时进程的优先级) 3. 代码实现 1. 摘要 在实际开发中,我们经常会遇到创建进程的场景,但是往往并不关心它的优先级…...
Mind Over Machines 公司:技术咨询与创新的卓越实践
在信息技术飞速发展的时代,企业面临着前所未有的机遇与挑战。如何巧妙运用技术,优化业务流程、提升竞争力,成为众多企业亟待解决的关键问题。Mind Over Machines(MOM)公司,作为一家在技术咨询领域深耕多年的…...

stm32week15
stm32学习 十一.中断 2.NVIC Nested vectored interrupt controller,嵌套向量中断控制器,属于内核(M3/4/7) 中断向量表:定义一块固定的内存,以4字节对齐,存放各个中断服务函数程序的首地址,中断向量表定…...
新手在使用宝塔Linux部署前后端分离项目时可能会出现的问题以及解决方案
常见问题与解决方案 1. 环境配置错误 问题:未正确安装Node.js/Python/JDK等运行时环境解决: 通过宝塔面板的软件商店安装所需环境验证版本: node -v # 查看Node.js版本 python3 --version # 查看Python3版本2. 端口未正确开放 问题&am…...
《从零构建一个简易的IOC容器,理解Spring的核心思想》
大家好呀!今天我们要一起探索Java开发中最神奇的魔法之一 —— Spring框架的IOC容器!🧙♂️ 我会用最最最简单的方式,让你彻底明白这个看似高深的概念。准备好了吗?Let’s go! 🚀 一、什么是IOC容器&…...

QSFP+、QSFP28、QSFP-DD接口分别实现40G、100G、200G/400G以太网接口
常用的光模块结构形式: 1)QSFP等效于4个SFP,支持410Gbit/s通道传输,可通过4个通道实现40Gbps传输速率。与SFP相比,QSFP光模块的传输速率可达SFP光模块的四倍,在部署40G网络时可直接使用QSFP光模块…...
tensorflow 1.x
简介 TensorFlow:2015年谷歌,支持python、C,底层是C,主要用python。支持CNN、RNN等算法,分CPU TensorFlow/GPU TensorFlow。 TensorBoard:训练中的可视化。 快捷键:shiftenter执行命令,Tab键进…...
vue3模版语法
Vue 的模板语法(template syntax)是 Vue 框架中用于声明式地绑定 DOM 的方式,核心是将 HTML 与 Vue 实例的数据绑定起来。 下面是常用的 Vue 模板语法总结(以 Vue 3 Composition API 为基础,也适用于 Vue 2 的 Options…...
java加强 -List集合
List集合是Collection集合下的集合的一种,它有序,可重复,有索引。但由于存在不同的底层实现方法,适合的场景也不同。 ArrayList底层是基于数组存储数据的,而LinkedList底层是基于链表存储数据的。因此,前者…...

PXE安装Ubuntu系统
文章目录 1. 服务器挂载Ubuntu镜像2. 修改dhcp配置文件3. 修改tftp配置文件4.复制网络驱动文件和其他配置文件5. http目录下配置文件6. 踩坑记录6.1 Failed to load ldlinux.c326.2 no space left on device6.3 为啥用pxe安装系统时,客户端需要较大的内存࿱…...

uniapp tabBar 中设置“custom“: true 在H5和app中无效解决办法
uniapp小程序自定义底部tabbar,但是在转成H5和app时发现"custom": true 无效,原生tabbar会显示出来 解决办法如下 在tabbar的list中设置 “visible”:false 代码如下:"tabBar": {"custom": true,//"cust…...

ABP-Book Store Application中文讲解 - 前期准备 - Part 2:创建Acme.BookStore + Angular
ABP-Book Store Application中文讲解-汇总-CSDN博客 因为本系列文章使用的.NET8 SDK,此处仅介绍如何使用abp cli .NET 8 SDK SQL sevrer 2014创建Angular模板的Acme.BookStore。 目录 1. ABP cli创建项目 1.1 打开cmd.exe 1.2 创建项目 2. ABP Studio创建项…...

基于k8s的Jenkins CI/CD平台部署实践(三):集成ArgoCD实现持续部署
基于k8s的Jenkins CI/CD平台部署实践(三):集成ArgoCD实现持续部署 文章目录 基于k8s的Jenkins CI/CD平台部署实践(三):集成ArgoCD实现持续部署一、Argocd简介二、安装Helm三、Helm安装ArgoCD实战1. 添加Arg…...
Starrocks 的 ShortCircuit短路径
背景 本文基于 Starrocks 3.3.5 本文主要来探索一下Starrocks在FE端怎么实现 短路径,从而加速点查查询速度。 在用户层级需要设置 enable_short_circuit 为true 分析 数据流: 直接到StatementPlanner.createQueryPlan方法: ... OptExpres…...
JVM——Java字节码基础
引入 Java字节码(Java Bytecode)是Java技术体系的核心枢纽,所有Java源码经过编译器处理后,最终都会转化为.class文件中的字节码指令。这些指令不依赖于具体的硬件架构和操作系统,而是由Java虚拟机(JVM&…...

控制台打印带格式内容
1. 场景 很多软件会在控制台打印带颜色和格式的文字,需要使用转义符实现这个功能。 2. 详细说明 2.1.转义符说明 样式开始:\033[参数1;参数2;参数3m 可以多个参数叠加,若同一类型的参数(如字体颜色)设置了多个&…...

外网访问内网海康威视监控视频的方案:WebRTC + Coturn 搭建
外网访问内网海康威视监控视频的方案:WebRTC Coturn 需求背景 在仓库中有海康威视的监控摄像头,内网中是可以直接访问到监控摄像的画面,由于项目的需求,需要在外网中也能看到监控画面。 实现这个功能的意义在于远程操控设备的…...
DA14585墨水屏学习(2)
一、user_svc2_wr_ind_handler函数 void user_svc2_wr_ind_handler(ke_msg_id_t const msgid,struct custs1_val_write_ind const *param,ke_task_id_t const dest_id,ke_task_id_t const src_id) {// sprintf(buf2,"HEX %d :",param->length);arch_printf("…...

Linux系统下的延迟任务及定时任务
1、延迟任务 概念: 在系统中我们的维护工作大多数时在服务器行对闲置时进行 我们需要用延迟任务来解决自动进行的一次性的维护 延迟任务时一次性的,不会重复执行 当延迟任务产生输出后,这些输出会以邮件的形式发送给延迟任务发起者 在 RH…...
Spark 之 YarnCoarseGrainedExecutorBackend
YarnCoarseGrainedExecutorBackend executor ID , 在日志里也有体现。 25/05/06 12:41:58 INFO YarnCoarseGrainedExecutorBackend: Successfully registered with driver 25/05...

【网络原理】数据链路层
目录 一. 以太网 二. 以太网数据帧 三. MAC地址 四. MTU 五. ARP协议 六. DNS 一. 以太网 以太网是一种基于有线或无线介质的计算机网络技术,定义了物理层和数据链路层的协议,用于在局域网中传输数据帧。 二. 以太网数据帧 1)目标地址 …...

相或为K(位运算)蓝桥杯(JAVA)
这个题是相或为k,考察相或的性质,用俩个数举例子,011001和011101后面的数不管和哪个数相或都不可能变成前面的数,所以利用这个性质我们可以用相与运算来把和k对应位置的1都积累起来,看最后能不能拼起来k如果能拼起来k那…...

AI汽车时代的全面赋能者:德赛西威全栈能力再升级
AI汽车未来智慧出行场景正在描绘出巨大的商业图景,德赛西威已经抢先入局。 在2025年上海车展开幕前夕,德赛西威发布2030年全新使命愿景——“创领安全、愉悦和绿色的出行生活”,并推出全栈式智慧出行解决方案Smart Solution3.0、车路云一体式…...
Python函数:从基础到进阶的完整指南
在Python编程中,函数是构建高效、可维护代码的核心工具。无论是开发Web应用、数据分析还是人工智能模型,函数都能将复杂逻辑模块化,提升代码复用率与团队协作效率。本文将从函数基础语法出发,深入探讨参数传递机制、高阶特性及最佳实践,助你掌握这一编程基石。 一、函数基…...

学习Python的第四天之网络爬虫
30岁程序员学习Python的第四天之网络爬虫的Scrapy库 Scrapy库的基本信息 Scrapy库的安装 在windows系统中通过管理员权限打开cmd。运行pip install scrapy即可安装。 通过命令scrapy -h可查看scrapy库是否安装成功. Scrapy库的基础信息 scrapy库是一种爬虫框架库 爬虫框…...

5、开放式PLC梯形图编程组件 - /自动化与控制组件/open-plc-programming
76个工业组件库示例汇总 开放式PLC编程环境 这是一个开放式PLC编程环境的自定义组件,提供了一个面向智能仓储堆垛机控制的开放式PLC编程环境。该组件采用苹果科技风格设计,支持多厂商PLC硬件,具有直观的界面和丰富的功能。 功能特点 多语…...
数据指标和数据标签
数据指标和数据标签是数据管理与分析中的两个重要概念,它们在用途、形式和应用场景上有显著区别。以下是两者的详细对比: 1. 核心定义 维度数据指标(Data Metrics)数据标签(Data Tags/Labels)定义量化衡量…...

linux中常用的命令(三)
目录 1- ls(查看当前目录下的内容) 2- pwd (查看当前所在的文件夹) 3- cd [目录名](切换文件夹) 4- touch [文件名] (如果文件不存在,新建文件) 5- mkdir[目录名] (创建目录) 6-rm[文件名]&…...