五种IO模型与非阻塞IO

核心思想:单位时间内,减少10中,等待的比重!

一.引入

人=进程;鱼=数据;钓鱼=IO,鱼竿=文件描述符,河=操作系统;桶=用户缓冲区

  • 张三:专注钓鱼,鱼漂不动,张三不动 – 阻塞IO
  • 李四:不会因为鱼没有上钩而卡在检测鱼漂上 – 非阻塞IO
  • 王五:通过给鱼竿挂上铃铛,让鱼上钩反向通知我 – 信号驱动IO
  • 赵六:拉了一卡车的鱼竿(100只鱼竿同时钓)-- 多路复用,多路转接
  • 田七:我是喜欢吃鱼!发起钓鱼,小王钓鱼了 – 异步IO

问题:

  1. 阻塞VS非阻塞:会因为IO条件不具备,阻塞会卡住,直到条件就绪,非阻塞,检测到IO条件不具备,出错返回。

    不同:等待方式不同!阻塞和非阻塞对一件IO任务的效率是一样的,鱼咬没咬钩跟看不看没关系,只是非阻塞在等的时候做了更多的其他事情,这些事情可能包含其他IO任务,所以说非阻塞IO效率高!

  2. 谁的钓鱼效率最高?

    赵六:单位时间内,鱼咬钩的概率高,等的比重,就会很低!

  3. 王五有没有等待呢?

    没有,但是王五只是不需要检测,也参与钓鱼中钓的过程!

  4. 结论:阻塞,非阻塞,信号驱动,多路复用—同步IO
    只要有人参与了IO不管是[等,拷贝],就是同步的!

  5. 同步IO vs 异步IO
    IO=等+拷贝,凡是参与IO等或者拷贝任意一个或多个阶段—同步IO;
    否则,发起IO或者IO工作流和你的工作流无关—异步IO。

  6. IO同步≠线程同步
    两者没有任何关系,IO同步是是否参与的问题;线程同步是先后顺序问题。

二.五种IO模型

1. 阻塞IO

在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.

阻塞IO是最常见的IO模型.

image-20260318101041565

2. 非阻塞IO

如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用.

image-20260318101134874

3. 信号驱动IO

内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.

image-20260318101208989

4. IO多路转接

虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.

image-20260318101418695

5. 异步IO

由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

image-20260318101517051

6. 小结

任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少.

三.高级IO重要概念

在这里, 我们要强调几个概念

1. 同步通信 vs 异步通信

(synchronous communication/ asynchronouscommunication)

同步和异步关注的是消息通信机制.

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.

另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不相干的概念.

  • 进程/线程同步也是进程/线程之间直接的制约关系
  • 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候.

以后在看到 “同步” 这个词, 一定要先搞清楚大背景是什么. 这个同步, 是同步通信异步通信的同步, 还是同步与互斥的同步.

2. 阻塞 vs 非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.

3. 理解这四者的关系

[妖怪蒸唐僧的例子]

4. 其他高级IO

非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO.

我们此处重点讨论的是I/O多路转接

四.非阻塞IO

1. fcntl

一个文件描述符, 默认都是阻塞IO.

函数原型如下.

fcntl 的作用是对文件描述符进行各种控制操作(如复制fd、设置非阻塞、修改文件状态标志等)

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

传入的cmd的值不同, 后面追加的参数也不相同.

fcntl函数有5种功能:

  • 复制一个现有的描述符(cmd=F_DUPFD).
  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
  • 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.

参数

  • fd:文件描述符。
  • cmd:控制命令,常见的有:
    • F_GETFL:获取文件状态标志
    • F_SETFL:设置文件状态标志(如设置 O_NONBLOCK 非阻塞)
  • arg(可选参数):根据 cmd 不同含义不同,例如:
    • F_SETFL 时传入新的 flag(如 O_NONBLOCK

返回值

  • 成功:返回根据cmd不同返回不同值

      1. cmd为F_GETFL时

        返回一个 int 类型的“位图”,里面包含当前 fd 的状态标志,比如:O_RDONLYO_WRONLYO_RDWRO_APPENDO_NONBLOCK

      2. cmd为F_GETFL时

        返回 0

  • 失败:返回-1 并设置 errno

2.使用fcntl设置非阻塞

基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞.

void SetNoBlock(int fd) {
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0) {
        perror("fcntl");
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
  • 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
  • 然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.

注意:

fcntl(fd, F_SETFL, O_NONBLOCK); // ❌ 错误写法

这样会覆盖原来的 flag

3. 轮询方式读取标准输入

read返回值是-1,并设置errno为 EAGAINEWOULDBLOCK时,代表现在还没读到数据,不是数据出错了。

read返回值是-1,并设置errno为EINTR时,代表读的过程中被信号中断,没读完。

#include <iostream>
#include <unistd.h>
#include <fcntl.h>

// θ标准输入,默认就是阻塞的
void SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if(fl<0)
    {
        perror("fcntl");
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

int main()
{
    SetNonBlock(0);
    char buffer[1024];
    while (true)
    {
        // Linux中ctrl+d:标识输入结束,read返回值是o,类似读到文件结尾
        ssize_t n = read(0, buffer, sizeof(buffer));
        if(n>0)
        {
            buffer[n-1] = 0;
            std::cout << buffer << std::endl;
        }
        else if(n<0) //非阻塞read,如果底层数据没有准备好,不算数据读取出错
        {
            // 1.读取出错;2.底层没有数据准备好
            if(errno == EAGAIN || errno==EWOULDBLOCK)
            {
                std::cout << "数据还没准备好..." << std::endl;
                sleep(1);
            }
            else if (errno == EINTR) //read被信号中断
            {
                continue;
            }
            else
            {
                // read出错
                exit(-1);
            }
        }
        else
        {
            break;
        }
    }
    return 0;
}

结果:

数据还没准备好...
abc <-手动输入的
abc <-程序返回的
数据还没准备好...
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐