在 Linux 系统中,I/O 模型描述了应用程序与内核之间交互数据的方式,核心差异体现在 “等待数据就绪” 和 “数据复制” 两个阶段的处理方式上。理解这五种模型有助于优化程序的 I/O 性能,尤其是在高并发场景中。
目录
首先讲一下两个比较常用的I/O模型,阻塞与非阻塞
一、阻塞 I/O(Blocking I/O)
当进程发起 I/O 操作(比如读数据)后,如果数据还没准备好(比如网卡还没收到数据、磁盘还没读入内存),进程会被操作系统 "挂起",进入阻塞状态。此时进程无法做任何事,只能等待数据就绪后,操作系统唤醒它继续处理。
生动例子:去便利店买冰镇可乐
- 你(进程)走到便利店,对老板说:"我要一瓶冰镇可乐(发起读 I/O 请求)。"
- 老板打开冰箱一看,冰镇可乐刚卖完,需要从仓库拿冰块重新冰镇(数据未就绪)。
- 老板对你说:"你在这儿等着,好了叫你(进程被阻塞)。"
- 你啥也做不了,只能站在原地发呆、刷手机(其实刷手机也做不了,因为进程被挂起了),直到老板把冰镇可乐递给你(数据就绪,进程被唤醒),你付完钱离开(处理数据)。
特点:
- 简单直接,进程不需要主动轮询数据是否就绪。
- 缺点是阻塞期间进程无法处理其他任务,可能导致资源利用率低(比如一个服务器进程阻塞在等待客户端数据时,无法响应其他客户端)。
二、非阻塞 I/O(Non-blocking I/O)
当进程发起 I/O 操作后,如果数据还没准备好,操作系统不会阻塞进程,而是立即返回一个 "数据未就绪" 的错误(比如 Linux 中的EAGAIN或EWOULDBLOCK)。进程可以继续做其他事,之后定期再次发起 I/O 请求,直到数据就绪,再进行处理。
生动例子:去便利店买冰镇可乐(非阻塞版)
- 你(进程)走到便利店,对老板说:"我要一瓶冰镇可乐(发起读 I/O 请求)。"
- 老板打开冰箱一看,冰镇可乐还没好,对你说:"没好呢,你先去忙别的,过会儿再来问(返回错误,不阻塞进程)。"
- 你没闲着,去旁边买了杯奶茶(处理其他任务),过了 5 分钟又去问老板:"可乐好了吗?"(再次发起 I/O 请求)。
- 老板说:"还没好,再等等(再次返回错误)。"
- 你又去看了会儿街景(继续处理其他任务),过了 3 分钟再问,老板说:"好了!"(数据就绪),你付完钱拿着可乐离开(处理数据)。
特点:
- 进程不会被阻塞,可以在等待数据的同时处理其他任务,资源利用率更高。
- 缺点是需要进程主动 "轮询"(反复询问数据是否就绪),频繁的轮询会消耗 CPU 资源(比如老板被你反复询问,也会占用他的时间)。
阻塞 vs 非阻塞的核心区别
- 阻塞 I/O:"等不到数据就不干活,直到数据来了再动"。
- 非阻塞 I/O:"等不到数据就先干别的,回头再来看数据来了没"。
两种模型各有适用场景:阻塞 I/O 适合简单场景(如单机脚本),非阻塞 I/O 适合需要同时处理多个任务的场景(如服务器),但通常需要配合 I/O 多路复用(如select/epoll)来减少轮询开销。
三、复用式I/O模型
I/O 多路复用通过一个 “监控者”(select/poll/epoll)实现对多个 I/O 资源的集中管理,只在资源就绪时才进行处理,避免了大量进程 / 线程的创建和阻塞,是高并发场景(如 Web 服务器)的 “性能利器”。
生动例子:超市的 “集中叫号系统”
假设你不是一个人买可乐,而是带着 3 个朋友(A、B、C)一起去一家大型超市,超市的冰镇可乐放在仓库里,需要工作人员去取,流程如下:
多人同时发起请求
你和 A、B、C 四个人都想买冰镇可乐,于是同时走到超市服务台,告诉服务员:“我们要冰镇可乐,好了麻烦通知一下”。此时你们并没有像阻塞 I/O 那样站在服务台傻等,也没有像非阻塞 I/O 那样反复跑回来问,而是各自找了个座位坐下玩手机(不阻塞,可做其他事)。服务员充当 “复用器”
服务员手里有一个 “叫号机”(相当于select/poll/epoll等复用函数),会把你们四个人的需求记在上面,然后去仓库处理(同时监控多个 I/O 请求)。
- 可能先拿到 A 的可乐,回来在叫号机上划掉 A 的需求,喊 “A,你的可乐好了”;
- 过一会儿拿到你的可乐,再划掉你的需求,喊 “你,你的可乐好了”;
- 最后处理 B 和 C 的需求。
按需响应,不浪费时间
你和朋友在座位上玩手机时,不需要反复去问服务员(避免非阻塞 I/O 的 “轮询开销”),也不需要一直盯着服务员(避免阻塞 I/O 的 “等待浪费”),只需要在听到自己的名字时过去取可乐即可(只在 I/O 就绪时才处理)。
对应到 Linux 的复用式 I/O
- 你和朋友:相当于多个需要进行 I/O 操作的进程 / 线程(比如多个客户端发起网络请求)。
- 服务员和叫号机:相当于
select/epoll等复用函数,负责监控多个 I/O 描述符(文件、网络连接等)的状态。- “可乐好了”:相当于 I/O 操作就绪(比如数据到达、连接建立等),此时复用函数会通知进程 / 线程 “某个 I/O 可以处理了”。
select函数
select是 I/O 多路复用的工具,它能让进程同时监控多个文件描述符(FD),阻塞等待其中任意一个或多个 FD “就绪”(如:有新连接请求、有数据可读),从而高效处理多客户端场景。
核心逻辑:“先告诉内核要监控哪些 FD,然后阻塞等待,内核告诉你哪些 FD 就绪了,再针对性处理”。
pselect函数
pselect 是 select 的增强版,核心优势在于 纳秒级超时 和 原子化信号掩码切换,适合需要精确信号同步和高超时精度的场景。日常开发中,若无需处理复杂信号,select 或更现代的 epoll(Linux)、kqueue(BSD)可能更常用,但 pselect 在特定场景下不可替代。
poll与ppoll函数
poll 和 ppoll 都是 Unix/Linux 系统中用于 I/O 多路复用的系统调用,用于监控多个文件描述符(FD)的状态变化(如可读、可写、异常等),以实现高效的 I/O 事件处理。二者的核心功能相似,但在接口设计和特性上存在差异
select 模型 的核心操作函数
原来select是要为每一个被监控的fd的状态分配一个集合(
select需要为三种事件状态(可读、可写、异常)分别维护一个独立的fd_set集合监控时,需要将关心的 FD 分别加入三个集合(例如 “关心 FD=5 可读” 就用FD_SET(5, &readfds)),内核返回后,三个集合会被修改,只保留 “就绪” 的 FD(未就绪的会被自动移除)),而poll则是将其打包为了结构体,监控起来更高效方便了(poll用struct pollfd结构体将 “FD 本身” 和 “关心的状态” 打包在一起,一个结构体即可表示一个 FD 的所有监控需求(例如events=POLLIN|POLLOUT表示同时关心可读和可写),内核返回时,仅修改revents字段(标记实际就绪的事件),fd和events保持不变,下次调用无需重新初始化)。对于我自己来说全志V536项目message.c第1400行左右,在这里用到了fd集合
while(app_state->msg_loop == MSG_LOOP_ENABLED) { FD_ZERO(&read_fds); FD_SET(fd_out_r, &read_fds); FD_SET(fd_internal_r, &read_fds); ret = select(maxfds + 1, &read_fds, NULL, NULL, NULL); if (ret > 0) { // first:FIFO_NAME_HIGH_PRIO if (FD_ISSET(fd_internal_r, &read_fds)) { ret = read(fd_internal_r, fifo_data, FIFO_DATA_SIZE); if (ret > 0) { // cardvInputMessageProcess(fifo_data, ret, TRUE); // memset(fifo_data, 0x00, FIFO_DATA_SIZE); app_cmd_input_process(fifo_data, ret, 1); memset(fifo_data, 0x00, FIFO_DATA_SIZE); } } FD_CLR(fd_internal_r, &read_fds); // second:FIFO_NAME if (FD_ISSET(fd_out_r, &read_fds)) { ret = read(fd_out_r, fifo_data, FIFO_DATA_SIZE); if (ret > 0) { // cardvInputMessageProcess(fifo_data, ret, FALSE); // memset(fifo_data, 0x00, FIFO_DATA_SIZE); app_cmd_input_process(fifo_data, ret, 0); memset(fifo_data, 0x00, FIFO_DATA_SIZE); } } FD_CLR(fd_out_r, &read_fds); } else if (ret < 0) { printf("FIFO select failed\n"); close(fd_out_r); close(fd_internal_r); app_cmd_release(); return MSG_ERR_SELECT; } else if (0 == ret) { printf("FIFO select timeout\n"); close(fd_out_r); close(fd_internal_r); app_cmd_release(); return MSG_ERR_SELECT; } }
1. poll
- 核心原理:通过一个
struct pollfd结构体数组传递需要监控的文件描述符及其关心的事件,内核会阻塞等待这些 FD 上的事件就绪,并返回就绪的 FD 及其状态。
2. ppoll
- 核心原理:在
poll的基础上增加了对信号掩码(signal mask)的支持,可原子地设置信号掩码并等待事件,避免信号处理对 I/O 监控的干扰。
四、信号驱动式I/O模型
信号驱动式 I/O 是一种通过内核发送信号通知用户进程 I/O 就绪状态的模型,核心特点是用户进程在 I/O 准备阶段不阻塞,仅在信号触发后处理 I/O 操作。
生动例子:“留个电话,好了叫你”
你特别渴,想喝冰镇可乐,但便利店的冰柜刚卖空,老板说 “正在从仓库补货,大概 10 分钟能冰好”。
- 你不想傻等,跟老板说:“我先去旁边公园坐会儿,可乐冰好后你打我电话(留个信号),我再过来拿。”
- 接下来你完全自由:去公园散步、刷手机,不用管便利店的进度,甚至可能忘了这回事。
- 10 分钟后,老板打电话(触发信号):“可乐冰好了,快来拿!” 你收到信号,才回到便利店付钱、拿可乐。
对应 Linux 逻辑:
- 进程提前告诉内核:“等 I/O 操作(比如数据接收)准备好后,给我发个信号(SIGIO)。”
- 之后进程可以去做其他事情,不用阻塞。
- 当 I/O 准备好时,内核会给进程发信号,进程收到信号后,再去执行实际的 I/O 操作(比如读取数据)。
五、异步I/O模型
异步 I/O 是一种 “全委托” 式的 I/O 模型,核心特点是用户进程发起 I/O 请求后无需干预,内核会完成从数据准备到复制的全过程,并在最终结果就绪时通知进程。
生动例子:“全程代办,送货上门”
你懒得出门,打开外卖软件,下单了一瓶冰镇可乐,备注 “送到家,放门口就行”
- 下单后你彻底不管了:该追剧追剧,该工作工作,完全不用关心便利店有没有货、什么时候冰好、骑手什么时候取货。
- 过了 20 分钟,门铃响了(或收到短信):“您的可乐放门口了,请查收。” 你开门就能拿到冰好的可乐,全程不用自己跑一趟。
对应 Linux 逻辑:
- 进程直接告诉内核:“帮我完成整个 I/O 操作(包括等待数据准备好、读取数据、把数据放到指定缓冲区),做完了告诉我一声。”
- 进程发起请求后,立刻返回,可以去做任何事情,整个过程中不会被阻塞。
- 当内核把所有 I/O 工作都完成后(数据已经就绪并放到进程指定的地方),会通过信号或回调通知进程:“事情办完了,你直接用数据就行。”
一句话总结区别:
- 信号驱动式 I/O:内核告诉你 “准备好的信号”,你自己去 “取货”;
- 异步 I/O:内核直接把 “货送到家”,你只需要 “签收”。

被折叠的 条评论
为什么被折叠?



