公司动态
Linux IO模型详解:从阻塞到多路复用
摘要本文通过生动的场景假设深入浅出地讲解Linux系统中的五种主要IO模型阻塞IO、非阻塞IO、信号驱动IO、IO多路复用select/poll/epoll。文章结合代码示例和对比表格帮助读者理解不同IO模型的工作原理、优缺点及适用场景。一、引言理解IO模型的重要性在Linux系统编程中IO输入/输出操作是程序与外部世界交互的基础。不同的IO模型对程序的性能、资源消耗和并发处理能力有着决定性影响。理解各种IO模型的工作原理对于编写高效、稳定的网络服务和系统程序至关重要。二、场景假设一妈妈与孩子的故事假设妈妈有一个孩子孩子在房间里睡觉妈妈需要及时获知孩子是否醒了如何做妈妈在房间待着和孩子一起睡妈妈不累但是不能干其他的事情时不时的看一下孩子其他时间可以干一些其他的事情累但是可以干其他的事情妈妈在客厅干活听孩子是否哭了二者互不耽误这三种方式分别对应了三种不同的IO模型方式1阻塞IO - 妈妈一直等待不能做其他事方式2非阻塞IO - 妈妈定期检查可以兼顾其他事但很累方式3信号驱动IO - 孩子醒了主动通知妈妈三、阻塞式IOBlocking IO阻塞IO模式是最普遍使用的I/O模式大部分程序使用的都是阻塞模式的I/O。学习的读写函数在调用过程中会发生阻塞相关函数如下读操作中的read读阻塞→ 需要读缓冲区中有数据可读读阻塞解除写操作中的write写阻塞→ 阻塞情况比较少主要发生在写入的缓冲区的大小小于要写入的数据量的情况下写操作不进行任何拷贝工作将发生阻塞一旦缓冲区有足够的空间内核将唤醒进程数据从用户缓冲区拷贝到相应的发送数据缓冲区。阻塞IO的特点同步性同步描述调用IO操作的线程会被阻塞直到操作完成优点编程简单CPU利用率高等待时不占用CPU缺点一个线程只能处理一个IO并发能力差适用场景小规模IO操作对性能要求不高四、非阻塞IONon-blocking IO当我们设置为非阻塞模式我们相当于告诉系统内核当我请求IO操作不能够马上完成你想让我的进程进行休眠等待的时候不要这么做。请马上返回一个错误给我。当一个应用程序使用了非阻塞模式的套接字它需要使用一个循环来不停的测试是否一个文件描述符有数据可读称作polling。应用程序不停的polling内核来检查是否IO操作已经就绪这将是一个极浪费CPU资源的操作。这种模式使用中不普遍。4.1 通过函数自带参数设置4.2 通过设置文件描述符的属性设置非阻塞fcntl函数详解int fcntl(int fd, int cmd, ... /* arg */ ); 功能设置文件描述符属性 参数 fd: 文件描述符 cmd设置方式 - 功能选择 F_GETFL 获取文件描述符的状态信息 第三个参数忽略 F_SETFL 设置文件描述符的状态信息 通过第三个参数设置 O_NONBLOCK 非阻塞 O_ASYNC 异步 O_SYNC 同步 arg: 设置的值 (in) 返回值 特殊选择返回特殊值 - F_GETFL 返回的状态值(int) 其他成功0 失败-1更新errno 使用示例以标准输入0为例 0-原本阻塞、读权限 修改或添加非阻塞示例代码设置标准输入为非阻塞模式#include stdio.h #include fcntl.h #include unistd.h int main(int argc, char const *argv[]) { char buf[32] {}; // 读/改/写三步法设置非阻塞 // 1. 获取文件描述符的属性 int flag fcntl(0, F_GETFL); // 2. 修改文件描述符的属性添加非阻塞标志 flag | O_NONBLOCK; // 3. 设置文件描述符属性 fcntl(0, F_SETFL, flag); // 测试非阻塞读取 while (1) { if (fgets(buf, sizeof(buf), stdin) NULL) printf(读取失败非阻塞模式下无数据可读\n); else printf(读取成功%s\n, buf); sleep(1); } return 0; }恢复阻塞模式注意如果需要恢复阻塞模式可以按以下方式设置// 清除非阻塞标志 flag ~O_NONBLOCK; // 重新设置文件描述符属性 fcntl(0, F_SETFL, flag);非阻塞IO的特点同步性非同步异步轮询描述调用IO操作时如果不能立即完成操作会立即返回线程可以继续执行其他操作优点可以同时处理多路IO提高并发能力缺点轮询方式耗费CPU资源效率较低适用场景高并发网络服务器减少线程阻塞时间五、信号驱动IOSignal-driven IO5.1 异步通知机制异步通知异步通知是一种非阻塞的通知机制发送方发送通知后不需要等待接收方的响应或确认。通知发送方可以继续执行其他操作而无需等待接收方处理通知。5.2 工作原理通过信号方式当内核检测到设备数据后会主动给应用发送信号SIGIO应用程序收到信号后做异步处理即可应用程序需要把自己的进程号告诉内核并打开异步通知机制5.3 示例代码鼠标事件信号驱动#include stdio.h #include unistd.h #include fcntl.h #include signal.h int fd; // 信号处理函数 void handler(int sig) { char buf[32] {0}; read(fd, buf, sizeof(buf)); printf(mouse事件: %s\n, buf); } int main(int argc, char const *argv[]) { // 打开鼠标设备文件 fd open(/dev/input/mouse1, O_RDONLY); if (fd 0) { perror(open err); return -1; } // 1. 设置将文件描述符和进程号提交给内核驱动 // 一旦fd有事件响应则内核驱动会给进程号发送一个SIGIO的信号 fcntl(fd, F_SETOWN, getpid()); // 2. 设置异步通知 int flags; flags fcntl(fd, F_GETFL); // 获取原属性 flags | O_ASYNC; // 给flags设置异步 O_ASYNC通知 fcntl(fd, F_SETFL, flags); // 修改的属性设置进去此时fd属于异步 // 3. signal捕捉SIGIO信号 --- SIGIO:内核通知进程有新的IO信号可用 // 一旦内核给进程发送SIGIO信号则执行handler signal(SIGIO, handler); // 主程序可以继续执行其他任务 while (1) { printf(主程序正在运行...\n); sleep(1); } return 0; }5.4 信号驱动IO的特点同步性异步描述当IO操作可以进行时内核会发送信号通知进程优点不需要轮询CPU利用率高缺点需要底层驱动的支持编程复杂度较高适用场景实时性要求高的应用避免轮询开销六、三种IO模型对比对比项阻塞IO(Blocking IO)非阻塞IO(Non-blocking IO)信号驱动IO(Signal-driven IO)同步性同步非同步异步描述调用IO操作的线程会被阻塞直到操作完成调用IO操作时如果不能立即完成操作会立即返回线程可以继续执行其他操作当IO操作可以进行时内核会发送信号通知进程特点最常见、效率低、不耗费CPU轮询、耗费CPU可以处理多路IO效率高异步通知方式需要底层驱动的支持适应场景小规模IO操作对性能要求不高高并发网络服务器减少线程阻塞时间实时性要求高的应用避免轮询开销七、场景假设二妈妈与三个孩子的故事假设妈妈有三个孩子分别在不同的房间里睡觉需要及时获知每个孩子是否醒了如何做阻塞IO在一个房间 - 只能照顾一个孩子非阻塞IO不停的每个房间查看 - 超级无敌累但是也可以干点其他的事情信号驱动IO不行因为只有一个信号不知道是哪个孩子醒了更好的解决方案不停的每个房间看超级无敌累但是也可以干点其他的事情妈妈在客厅睡觉孩子醒了之后自己找妈妈既可以休息也可以及时获取状态这个场景引出了IO多路复用的概念即一个进程可以同时监控多个IO事件。八、IO多路复用select/poll/epoll8.1 为什么需要IO多路复用应用程序中同时处理多路输入输出流时面临的问题阻塞模式若采用阻塞模式得不到预期的目的非阻塞模式对多个输入进行轮询但又太浪费CPU时间多进程/线程若设置多个进程/线程分别处理一条数据通路将产生进程/线程间的同步与通信问题使程序变得更加复杂比较好的方法是使用IO多路复用技术。其基本思想是先构造一张有关描述符的表最大1024然后调用一个函数当这些文件描述符中的一个或多个已准备好进行IO时函数才返回函数返回时告诉进程哪个描述符已就绪可以进行IO操作九、select模型9.1 select的特点监听数量限制一个进程最多只能监听1024个文件描述符效率问题select被唤醒之后要重新轮询效率相对低内存拷贝select每次都会清空未发生响应的文件描述符每次拷贝都需要从用户空间到内核空间效率低开销大9.2 select编程步骤先构造一张关于文件描述符的表清空表 FD_ZERO将关心的文件描述符添加到表中 FD_SET调用select函数判断是哪一个或者哪些文件描述符产生了事件 FD_ISSET做对应的逻辑处理9.3 select函数原型#include sys/time.h #include sys/types.h #include unistd.h int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 功能实现IO的多路复用 参数 nfds关注的最大的文件描述符1 readfds关注的读表 writefds关注的写表 exceptfds关注的异常表 timeout超时的设置 NULL一直阻塞直到有文件描述符就绪或出错 时间值为0仅仅检测文件描述符集的状态然后立即返回 时间值不为0在指定时间内如果没有事件发生则超时返回0并清空设置的时间值 struct timeval { long tv_sec; /* 秒 */ long tv_usec; /* 微秒 10^-6秒 */ }; 返回值 准备好的文件描述符的个数 -1失败 0超时检测时间到并且没有文件描述符准备好 注意 select返回后关注列表中只存在准备好的文件描述符 操作表函数 void FD_CLR(int fd, fd_set *set); // 清除集合中的fd位 void FD_SET(int fd, fd_set *set); // 将fd放入关注列表中 int FD_ISSET(int fd, fd_set *set); // 判断fd是否在集合中 是→1 不是→0 void FD_ZERO(fd_set *set); // 清空关注列表9.4 练习同时监听键盘和鼠标输入需求输入鼠标的时候响应鼠标事件输入键盘的时候响应键盘事件两路IO#include stdio.h #include sys/time.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include string.h int main(int argc, char const *argv[]) { char buf[32] {}; int ret; // 打开鼠标设备文件 int fd open(/dev/input/mouse0, O_RDONLY); if (fd 0) { perror(open err); return -1; } // 1. 构造一张关于文件描述符的表 fd_set rfds; while (1) { // 2. 清空表