当前位置: 首页 > java >正文

【Linux】Linux 信号驱动I/O

参考博客:https://blog.csdn.net/qq_42611237/article/details/126029834

一、信号驱动I/O概念

  • 信号驱动 IO(Signal-Driven I/O)是一种同步IO 模型,允许进程在文件描述符(如套接字)就绪时收到内核发送的信号通知,而无需主动轮询或阻塞等待。这种模型通过将 IO 事件转换为信号,实现了程序与 IO 操作的解耦,提高了系统的并发处理能力。

为什么信号驱动 I/O 是同步的?

  • 阻塞点: 在信号驱动 I/O 中,尽管进程在等待数据就绪时不被阻塞(可以处理其他任务),但当数据就绪的信号到达后,进程必须在信号处理函数中调用一个阻塞的 read(或 recvfrom 等)系统调用来实际执行数据从内核缓冲区到用户缓冲区的拷贝工作。 这个 read 调用会阻塞进程,直到数据拷贝完成。

  • 进程的主动参与: 进程需要主动发起这个阻塞的拷贝操作 (read)。内核只是通知了“数据准备好了”,并没有完成拷贝。

信号驱动 I/O 在“数据就绪通知”阶段是非阻塞的(符合异步的感觉),但在关键的“数据拷贝”阶段仍然是阻塞的(同步的本质)。按照 I/O 操作是否导致进程阻塞在数据拷贝上的核心标准来划分,它被归类为同步 I/O。

二、信号驱动工作流程

在这里插入图片描述

  1. 注册兴趣:进程通过系统调用(如fcntl)告诉内核:“我对这个文件描述符的 IO 事件感兴趣,请在它就绪时通知我”。
  2. 信号绑定:进程通过sigaction注册信号处理函数,指定当 IO 事件发生时应执行的代码。
  3. 异步通知:当文件描述符就绪时(如有数据可读、可写空间可用),内核向进程发送SIGIO信号。
  4. 事件处理:进程在信号处理函数中执行相应的 IO 操作

三、信号驱动IO核心 API

3.1 启用信号驱动模式

要让套接字支持信号驱动 IO,必须进行两步配置:

  1. 设置属主进程
fcntl(sock_fd, F_SETOWN, getpid());
  • 作用:告诉内核 “当这个套接字就绪时,向当前进程发送信号”。
  • 原理:每个文件描述符在内核中维护一个属主进程 ID,信号将发送到该进程。
  1. 启用异步模式(O_ASYNC)
int flags = fcntl(sock_fd, F_GETFL);
fcntl(sock_fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
  • O_ASYNC:启用异步通知模式,使套接字就绪时触发SIGIO信号。
  • O_NONBLOCK:设置非阻塞模式,避免在信号处理函数中阻塞(通常与 O_ASYNC 配合使用)。

3.2 信号处理:注册 SIGIO 处理函数

使用sigaction函数注册信号处理函数:

struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sigio_handler;  // 信号处理函数
sigemptyset(&sa.sa_mask);       // 处理信号期间不阻塞其他信号
sa.sa_flags = SA_RESTART;       // 自动重启被信号中断的系统调用if (sigaction(SIGIO, &sa, NULL) < 0) {perror("sigaction");exit(1);
}

3.3 数据读取:非阻塞操作

在信号处理函数中读取数据时,通常使用MSG_DONTWAIT标志进行非阻塞读取:

ssize_t len = recv(sock_fd, buffer, sizeof(buffer), MSG_DONTWAIT);
if (len < 0) {if (errno == EWOULDBLOCK || errno == EAGAIN) {// 没有数据可读,正常情况} else {// 处理其他错误}
} else if (len == 0) {// 客户端关闭连接
} else {// 处理读取到的数据
}

四、基于信号驱动IO的服务端实现

4.1 代码实现

socket封装

#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <cstring>
#include <netinet/in.h>class Sock
{public:static int Socket(){int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cerr << "socket error" << std::endl;exit(1);}return sock;}static void Bind(int sock, uint16_t port){struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_addr.s_addr = INADDR_ANY;local.sin_port = htons(port);if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){std::cout << "bind error !" << std::endl;exit(2);}}static void Listen(int sock){if (listen(sock, 5) < 0){std::cerr << "listen error" << std::endl;exit(3);}}static int Accept(int sock){struct sockaddr_in peer;socklen_t len = sizeof(peer);int fd = accept(sock, (struct sockaddr *)&peer, &len);if (fd >= 0){return fd;}else{return -1;}}static void Connect(int sock, std::string ip, uint16_t port){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port);server.sin_addr.s_addr = inet_addr(ip.c_str());if (connect(sock, (struct sockaddr *)&server, sizeof(server) == 0)){std::cout << "connect sucess" << std::endl;}else{std::cout << "connect failed" << std::endl;exit(4);}}
};

基于信号驱动的服务端

#include<string>
#include<sys/signal.h>
#include<fcntl.h>
#include<signal.h>
#include<unistd.h>
#include"sock.hpp"#define NUM 1024
int listen_fd = 0;
int fd_arrays[NUM];void handler(int signo){int sock = Sock::Accept(listen_fd);if(sock >= 0){ //新连接std::cout << "sock:" << listen_fd << " get a new connection :" << sock << std::endl;    int pos = 1;for(pos ; pos < NUM ; ++pos){if(fd_arrays[pos] == -1){break;}}if(pos < NUM){fd_arrays[pos] = sock;std::cout << "connect sucess !" << std::endl;//设置客户端套接字SIGIO属性fcntl(sock,F_SETOWN,getpid());int flags = fcntl(sock,F_GETFL);fcntl(sock,F_SETFL,O_ASYNC | O_NONBLOCK | flags);}else{std::cout << "connction is full !" << std::endl;close(sock);}}else{ //收到消息for(int i = 1 ; i < NUM ; ++i){if(fd_arrays[i] == -1){continue;}char buffer[1024];memset(buffer,0,sizeof(buffer));ssize_t len = recv(fd_arrays[i],buffer,sizeof(buffer) - 1 , MSG_DONTWAIT); // 非阻塞if(len > 0){std::cout << "sock:" << fd_arrays[i] << " send: " << buffer << std::endl;break;  }else if(len == 0){std::cout << "sock:" << fd_arrays[i] << " close" << std::endl;close(fd_arrays[i]);fd_arrays[i] = -1;}else if(errno != EAGAIN && errno != EWOULDBLOCK){ perror("recv");close(fd_arrays[i]);fd_arrays[i] = -1;}}}}int main(int argc, char* argv[]){if(argc < 2){std::cerr << "argc < 2" << std::endl;return 1;}uint16_t port = (uint16_t)atoi(argv[1]);listen_fd = Sock::Socket();int opt = 1;setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));Sock::Bind(listen_fd,port);Sock::Listen(listen_fd);std::cout << "Server is listening on port:" << port << std::endl; //设置SIGIO属性fcntl(listen_fd,F_SETOWN,getpid()); // 设置属主进程int flags = fcntl(listen_fd,F_GETFL); //获取文件状态fcntl(listen_fd,F_SETFL,flags | O_ASYNC | O_NONBLOCK); //异步非阻塞//注册SIGIO处理函数struct sigaction sa;memset(&sa,0,sizeof(sa));sa.sa_handler = handler; //注册回调函数sigemptyset(&sa.sa_mask);sa.sa_flags = SA_RESTART;if(sigaction(SIGIO,&sa,NULL) < 0){perror("sigaction");return 1;}for(int i = 0 ; i < NUM ; ++i){fd_arrays[i] = -1;}fd_arrays[0] = listen_fd;while(true){pause();}close(listen_fd);for(int i = 1 ; i < NUM; ++i){if(fd_arrays[i] != -1){close(fd_arrays[i]);fd_arrays[i] = 0;}}return 0;
}

Makefile编译文件

CXX = g++CXXFLAGS = -Wall -std=c++14SRCS = main.cpp OBJS = $(SRCS:.cpp=.o)TARGET = serverall:$(TARGET)$(TARGET):$(OBJS)$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS)%.o:%.cpp$(CXX) $(CXXFLAGS) -c $< -o $@clean:rm -f $(OBJS) $(TARGET).PHONY:all clean	

客户端代码

#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <thread>#define PORT 8080
#define BUFFER_SIZE 1024// 接收服务器消息
void receive_messages(int socket) {char buffer[BUFFER_SIZE];while (true) {memset(buffer, 0, sizeof(buffer));ssize_t bytes_received = recv(socket, buffer, BUFFER_SIZE, 0);if (bytes_received <= 0) {std::cout << "服务器断开连接。" << std::endl;close(socket);return;}std::cout << "收到消息: " << buffer << std::endl;}
}int main() {int client_socket;struct sockaddr_in server_addr;// 创建客户端套接字client_socket = socket(AF_INET, SOCK_STREAM, 0);if (client_socket < 0) {std::cerr << "套接字创建失败。" << std::endl;return -1;}// 初始化服务器地址server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);server_addr.sin_addr.s_addr = inet_addr("10.22.79.251");  // 服务器 IP// 连接到服务器if (connect(client_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {std::cerr << "连接服务器失败。" << std::endl;return -1;}std::cout << "成功连接服务器。" << std::endl;// 创建线程接收服务器消息std::thread receive_thread(receive_messages, client_socket);receive_thread.detach();  // 分离线程以便独立运行// 发送消息给服务器char message[BUFFER_SIZE];while (true) {std::cin.getline(message, BUFFER_SIZE);send(client_socket, message, strlen(message), 0);}close(client_socket);return 0;
}

4.2 测试结果

编译服务端代码

make

在这里插入图片描述

运行服务器

./server 8080

在这里插入图片描述

客户端连接服务端
在这里插入图片描述

客户端发送数据到服务端

在这里插入图片描述

客户端断开连接

在这里插入图片描述

五、总结

5.1 信号驱动 IO 的优缺点

优点

  1. 低延迟响应:无需轮询,事件触发即时响应,适合实时系统。
  2. 资源高效:相比多线程 / 多进程模型,节省线程创建和上下文切换开销。
  3. 编程简单:比epoll等复杂机制更容易实现,代码结构清晰。
  4. 异步非阻塞:主线程可以继续执行其他任务,提高 CPU 利用率。

缺点

  1. 信号处理限制
    • 信号处理函数中只能执行异步安全的系统调用。
    • 复杂操作需通过管道等机制通知主线程处理。
  2. 信号丢失问题
    • 标准信号(如 SIGIO)不支持排队,多个同类信号可能合并为一个。
    • 若需要可靠处理,应使用实时信号(如 SIGRTMIN)。
  3. 可扩展性有限
    • 在高并发场景下,信号处理函数可能成为瓶颈。
    • 现代系统更推荐使用epoll(Linux)或kqueue(BSD)。

5.2 信号驱动 IO 对比 IO 多路复用

特性信号驱动 IOIO 多路复用(epoll)
事件通知方式信号(SIGIO)主动查询就绪列表
编程复杂度较低(只需处理信号)较高(需维护事件循环)
并发处理能力中等(受信号处理函数限制)高(适合万级连接)
延迟低(信号触发即时响应)中(需等待轮询周期)
适用场景中小规模连接,实时性要求高大规模连接,高性能服务器

更多资料:https://github.com/0voice

http://www.xdnf.cn/news/14405.html

相关文章:

  • Git 配置 SSH 密钥与私钥教程(跨平台完整指南)
  • 京东API接口最新指南:店铺所有商品接口的接入与使用
  • 易语言模拟真人鼠标轨迹算法 - 非贝塞尔曲线
  • 大模型的开发应用(十一):对话风格微调项目(下):微调与部署
  • 《AI辅助编程:从零掌握核心逻辑》工作坊开业
  • mysql修改密码笔记
  • 基于51单片机的智能小车:按键调速、障碍跟踪、红外循迹与数码管显示(一个合格的单片机课设)
  • 浙江康冠锁业携智能锁具亮相2025上海国际快递物流展
  • 山东大学软件学院创新项目实训开发日志——第十七周(二)
  • 【C语言扩展识别实数负数】2022-5-29
  • Web第二次方向考核复盘
  • OpenHarmony 5.0读取文件并写入到另一份文件(公共文件夹),并保持原先的格式以及编码类型
  • 论文略读:Does Refusal Training in LLMs Generalize to the Past Tense?
  • Hierarchical Vector Quantization for Unsupervised Action Segmentation
  • 介质访问控制——随机访问控制
  • Java的DI依赖注入
  • 2025如何快速给人物模型添加骨骼
  • 【Python机器学习(一)】NumPy/Pandas手搓决策树+使用Graphviz可视化(以西瓜书数据集为例)
  • 【深度剖析】领信卓越:福耀玻璃的数字化转型(上篇2:转型动机分析)
  • 嵌入式知识篇---三种坐标系
  • 揭开肾细胞的分子密码:当 METTL3 遇上 FOSL1【AbMole】
  • Android 与 ESP-01 WIFI模块通信
  • Tomcat 配置双击启动
  • 141. 环形链表
  • 概率期望DP
  • 【茶社茶楼专用软件】佳易王茶社茶楼计时计费会员管理软件介绍
  • 深度解析企业风控API技术实践:构建全方位企业风险画像系统
  • 【运维系列】【ubuntu22.04】安装Docker
  • 【性能优化】启用zram
  • 个人笔记-- TCL 替换