Linux的I/O模型笔记

        在 Linux 系统中,I/O 模型描述了应用程序与内核之间交互数据的方式,核心差异体现在 “等待数据就绪” 和 “数据复制” 两个阶段的处理方式上。理解这五种模型有助于优化程序的 I/O 性能,尤其是在高并发场景中。

目录

一、阻塞 I/O(Blocking I/O)

生动例子:去便利店买冰镇可乐

特点:

二、非阻塞 I/O(Non-blocking I/O)

生动例子:去便利店买冰镇可乐(非阻塞版)

特点:

阻塞 vs 非阻塞的核心区别

三、复用式I/O模型

生动例子:超市的 “集中叫号系统”

select函数

pselect函数

poll与ppoll函数

select 模型 的核心操作函数       

四、信号驱动式I/O模型

生动例子:“留个电话,好了叫你”

五、异步I/O模型

生动例子:“全程代办,送货上门”


        首先讲一下两个比较常用的I/O模型,阻塞与非阻塞

一、阻塞 I/O(Blocking I/O)

        当进程发起 I/O 操作(比如读数据)后,如果数据还没准备好(比如网卡还没收到数据、磁盘还没读入内存),进程会被操作系统 "挂起",进入阻塞状态。此时进程无法做任何事,只能等待数据就绪后,操作系统唤醒它继续处理。

生动例子:去便利店买冰镇可乐
  • 你(进程)走到便利店,对老板说:"我要一瓶冰镇可乐(发起读 I/O 请求)。"
  • 老板打开冰箱一看,冰镇可乐刚卖完,需要从仓库拿冰块重新冰镇(数据未就绪)。
  • 老板对你说:"你在这儿等着,好了叫你(进程被阻塞)。"
  • 你啥也做不了,只能站在原地发呆、刷手机(其实刷手机也做不了,因为进程被挂起了),直到老板把冰镇可乐递给你(数据就绪,进程被唤醒),你付完钱离开(处理数据)。
特点:
  • 简单直接,进程不需要主动轮询数据是否就绪。
  • 缺点是阻塞期间进程无法处理其他任务,可能导致资源利用率低(比如一个服务器进程阻塞在等待客户端数据时,无法响应其他客户端)。

二、非阻塞 I/O(Non-blocking I/O)

        当进程发起 I/O 操作后,如果数据还没准备好,操作系统不会阻塞进程,而是立即返回一个 "数据未就绪" 的错误(比如 Linux 中的EAGAINEWOULDBLOCK)。进程可以继续做其他事,之后定期再次发起 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)一起去一家大型超市,超市的冰镇可乐放在仓库里,需要工作人员去取,流程如下:

  1. 多人同时发起请求
    你和 A、B、C 四个人都想买冰镇可乐,于是同时走到超市服务台,告诉服务员:“我们要冰镇可乐,好了麻烦通知一下”。此时你们并没有像阻塞 I/O 那样站在服务台傻等,也没有像非阻塞 I/O 那样反复跑回来问,而是各自找了个座位坐下玩手机(不阻塞,可做其他事)。

  2. 服务员充当 “复用器”
    服务员手里有一个 “叫号机”(相当于select/poll/epoll等复用函数),会把你们四个人的需求记在上面,然后去仓库处理(同时监控多个 I/O 请求)。

    • 可能先拿到 A 的可乐,回来在叫号机上划掉 A 的需求,喊 “A,你的可乐好了”;
    • 过一会儿拿到你的可乐,再划掉你的需求,喊 “你,你的可乐好了”;
    • 最后处理 B 和 C 的需求。
  3. 按需响应,不浪费时间
    你和朋友在座位上玩手机时,不需要反复去问服务员(避免非阻塞 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:内核直接把 “货送到家”,你只需要 “签收”。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值