五种IO模型和阻塞IO

1. IO概念

对于前面学习的进程间通信和网络通信,本质上就是IO,所谓IO就是Input/Output,即输入输出,而输入输出是对于内存来说的,把数据从外设(如硬盘)读到内存就是输入,将数据从内存写到外设,就是输出。

2. 五种IO模型

2.1 阻塞IO

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

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

在这里插入图片描述

2.2 非阻塞IO

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

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

在这里插入图片描述

2.3 信号驱动IO

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

在这里插入图片描述

2.4 IO多路转接

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

在这里插入图片描述

2.5 异步IO

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

在这里插入图片描述

2.6 小结

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

3. 高级IO的重要概念

3.1 同步通信 vs 异步通信(synchronous communication / asynchronous communication)

同步和异步关注的是消息通知机制。

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

另外的,在多线程/多进程部分时,也提到过同步和互斥,这里的同步通信和进程之间的同步通信是完全不相干的概念。

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

3.2 阻塞 VS 非阻塞

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

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

3.3 理解者这四者之间的关系

3.4 其他高级IO

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

我们后面的重点为I/O多路转接。

4. 非阻塞IO

4.1 fcntl

include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */);

功能:fcntl 函数是 Linux 系统中的一个系统调用,用于对文件描述符执行各种控制操作。它能管理文件状态标志、文件锁、复制文件描述符等。

常用操作(cmd 参数)

  1. F_GETFL - 获取文件状态标志(如 O_RDONLY、O_WRONLY、O_RDWR、O_APPEND、O_NONBLOCK)。
  2. F_SETFL - 设置文件状态标志(仅可修改 O_APPEND、O_NONBLOCK、O_ASYNC 等标志)。
  3. F_GETFD - 获取 close-on-exec 标志。
  4. F_SETFD - 设置 close-on-exec 标志(使用 FD_CLOEXEC)。
  5. F_GETLKF_SETLKF_SETLKW - 文件锁操作。
  6. F_DUPFD - 复制文件描述符。

返回值:成功时返回依赖命令的值(如 F_GETFL 返回旧标志),失败时返回 -1 并设置 errno

可变参数:第三个参数取决于命令(如 F_SETFL 传入整数,文件锁操作传入 struct flock 指针)。

下面我们使用到是第1、2种功能,获取/设置文件状态标记,就可与将一个文件描述符设置为非阻塞。

4.2 实现 SetNoBlock

基于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 参数。

4.3 轮询方式读入标准输入

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

using namespace std;

void SetNoBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0)
    {
        perror("fcntl");
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

int main()
{
    SetNoBlock(0);
    while (1)
    {
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size < 0)
        {
            cerr << "read" << endl;
            sleep(1);
            continue;
        }
        cout << "input:" << buf << endl;
    }
    return 0;
}

此时,非阻塞情况下,read依旧会以-1进行返回,此时我们如何分辨它是出错了还是非阻塞返回?

我们可以通过错误码来分辨,当错误码为11(EWOULDBLOCK或EAGAIN,表示底层数据没有就绪)的时候,意味着是非阻塞返回。

Logo

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

更多推荐