【高级IO-1】探索五种 I/O 模型及其高级I/O技术:基于 fcntl() 的代码应用

1. 五种IO模型

我们以一个例子:对于钓鱼,可以总体分为等待和鱼上钩两个过程。

假设有五个人去钓场钓鱼:

  • A:一直盯着鱼钩,期间不做其他事,直到上鱼。
  • B:拿着手机,同时钓鱼和看手机,每隔一段时间去看一看鱼钩。
  • C:在鱼漂处挂一个铃铛,专注看手机,直要铃铛一响,就证明上鱼了,此时收杆。
  • D:直接架起10个鱼钩,来回走动,观察每个杆的情况。
  • E:根本不去钓场,让自己的小弟e去钓鱼,E安排好情况后就去上班了,当e钓到鱼后给E汇报情况即可。

不妨想想,哪个人的钓鱼效率最高?

正如前面的效率问题,只要单位时间等待的时间比重越短,效率最高,
比如钓场的一条鱼一定会上钩,那么对于D来说,钓到鱼的概率就是10/14,而其他人就是1/14,自然D的效率最高,而对于上面五种方式,实际上为五种IO模型:

  1. 阻塞式 IO (Blocking IO)

    • 当应用程序发起一个 IO 请求时,程序会被阻塞直到该 IO 操作完成。
    • 在阻塞式 IO 模型中,应用程序在等待 IO 操作完成期间会被挂起,直到数据准备好被读取或写入。
  2. 非阻塞式 IO (Non-blocking IO)

    • 在非阻塞式 IO 模型中,应用程序发起一个 IO 请求后不会被阻塞,而是立即返回。
    • 应用程序可以周期性地检查 IO 操作是否完成,从而避免了被阻塞的情况。
    • 虽然非阻塞 IO 模型减少了等待时间,但是需要应用程序不断轮询以检查 IO 状态,可能会增加 CPU 开销。
  3. 多路复用式 IO (I/O Multiplexing)

    • 多路复用式 IO 模型通过 selectpollepoll 等机制,允许应用程序同时监视多个 IO 事件。
    • 应用程序通过调用这些机制中的一个来等待多个 IO 事件的发生,从而避免了阻塞。
    • 当某个 IO 事件发生时,应用程序会被唤醒,并可以处理该 IO 事件。
  4. 信号驱动式 IO (Signal-driven IO)

    • 在信号驱动式 IO 模型中,应用程序发起一个 IO 请求后,继续执行其他任务。
    • 当 IO 操作完成时,操作系统向应用程序发送一个信号来通知 IO 完成,然后应用程序处理该信号并读取或写入数据。
  5. 异步 IO (Asynchronous IO)

    • 异步 IO 模型中,应用程序发起一个 IO 请求后,不需要等待 IO 操作完成,而是可以继续执行其他任务。
    • 当 IO 操作完成时,操作系统会通知应用程序,应用程序可以处理已完成的 IO 操作。

我们把IO分为了两个阶段(等 &拷贝),如果两个阶段参加了任意一个(或都参加),就叫做同步IO(A~D),如果没有参加任意一个阶段,则为异步IO(E)


2. 高级IO的重要概念

2.1 同步通信 与 异步通信

上面我们通过例子简单介绍了同步通信与异步通信的概念,这里我们加深一下了解:

  1. 同步通信

    • 在同步通信中,发送方发送数据后会等待接收方对数据的响应,直到接收到响应后才继续执行后续操作
    • 这意味着发送方和接收方在通信过程中是相互等待的,直到完成数据的传输和处理。
    • 同步通信的一个常见例子是阻塞式 I/O,比如传统的文件 I/O 操作和网络套接字的阻塞模式。
  2. 异步通信

    • 在异步通信中,发送方发送数据后不会立即等待接收方的响应,而是继续执行其他操作。
    • 发送方在发送数据后不会阻塞等待,而是通过回调函数、轮询或事件驱动等方式在后续得到接收方的响应或通知。
    • 异步通信的一个常见例子是非阻塞式 I/O,比如异步 I/O 操作和事件驱动的网络编程模型。

简单总结,即:

  • 在同步通信中,调用者发出请求后会主动等待结果的返回,直到得到结果才继续执行后续操作。
  • 在异步通信中,调用者发出请求后不会立即等待结果,而是继续执行其他操作,被调用者则通过状态、通知或回调函数等方式通知调用者结果的情况。

2.2 阻塞与非阻塞

阻塞:

  • 阻塞模式下,当程序执行一个操作时,如果该操作无法立即完成,程序将暂停执行,直到操作完成才会继续执行后续操作
  • 阻塞模式下,调用者会一直等待直到操作完成,期间无法进行其他任务。

非阻塞:

  • 非阻塞模式下,当程序执行一个操作时,如果该操作无法立即完成,程序将不会暂停执行,而是立即返回一个状态或错误码给调用者
  • 非阻塞模式下,调用者可以继续执行其他任务,不必等待操作完成。

2.3 如何理解四者间的关系?

  1. 同步通信可以是阻塞或非阻塞,例如:
    • 在同步阻塞通信中,发送方发送消息后会阻塞等待接收方响应;
    • 而在同步非阻塞通信中,发送方发送消息后可以继续执行其他任务,但会定期检查接收方的响应状态。

2. 异步通信通常是非阻塞的,因为发送方发送消息后不会等待接收方响应,而是继续执行其他任务,接收方处理完消息后再通知发送方。

3. 其他高级IO

下面简单介绍一些高级IO,下文将讨论的是I/O多路转接:

当然,下面是对这些高级 I/O 技术的简单介绍:

3.1 非阻塞 I/O

  • 定义:非阻塞 I/O 是一种 I/O 操作模式,在该模式下,I/O 操作不会阻塞当前线程。如果数据无法立即读取或写入,I/O 操作会立即返回,而不是让线程等待直到操作完成。
  • 特点
    • 适用于需要处理大量并发连接的场景。
    • 常用于网络编程中,例如在网络服务器中处理多个客户端连接。

3.2 纪录锁(Record Lock)

  • 定义:纪录锁是一种锁机制,用于对文件中的特定区域进行锁定。这种锁通常用于数据库系统或其他需要锁定特定记录的应用程序。
  • 特点
    • 可以锁定文件的部分内容而不是整个文件。
    • 提供精细的锁定控制,有助于避免锁争用和提高并发性能。

3.3 系统 V 流机制

  • 定义:系统 V 流(Streams)机制是一种用于处理数据流的 I/O 模型,支持对数据流的过滤和转换。通过使用流,可以将数据通过多个处理模块进行处理,每个模块可以独立处理数据的某一部分。
  • 特点
    • 提供了一种模块化的数据处理方式。
    • 常用于 Unix 系统中的管道和套接字编程中。

3.4 I/O 多路复用(I/O Multiplexing)

  • 定义:I/O 多路复用是指在单一线程中同时处理多个 I/O 流。它允许一个线程同时监控多个 I/O 通道(例如套接字),并在某些通道准备好进行读写操作时得到通知。
  • 实现方式
    • select:监控多个文件描述符,检查哪些描述符已准备好进行读写。
    • poll:类似于 select,但可以处理更多的文件描述符。
    • epoll(Linux):比 selectpoll 更高效,适用于处理大量并发连接的场景。
    • kqueue(BSD):与 epoll 类似,用于高效的事件通知。

3.5 readv 和 writev 函数

  • 定义
    • readv:从文件描述符中读取数据到多个缓冲区。
    • writev:从多个缓冲区写入数据到文件描述符。
  • 特点
    • 提高了 I/O 操作的效率,特别是对于需要处理多个缓冲区的数据时。
    • 允许在单一系统调用中处理多个缓冲区,减少了系统调用的开销。

3.6 存储映射 I/O(mmap)

  • 定义mmap 是一种将文件内容映射到进程的虚拟内存地址空间的技术。通过 mmap,可以将文件或设备映射到内存中,然后通过内存访问文件内容,而不是通过传统的读写操作。
  • 特点
    • 提高了文件 I/O 的效率,因为可以像访问内存一样访问文件内容。
    • 支持大文件的高效处理,并且可以用于实现进程间通信(IPC)。

4. 代码理解

非阻塞IO

fcntl()

fcntl用于对文件描述符进行各种操作,包括复制、关闭、获取/设置文件描述符标志以及对文件描述符的各种属性进行操作。

一般我们用fcntl有以下操作:

  • 复制描述符:可以通过 F_DUPFD 操作复制文件描述符。
  • 获取/设置文件描述符标志:可以通过 F_GETFLF_SETFL 操作来获取和设置文件描述符的状态标志,如非阻塞标志等。
  • 获取/设置文件状态标志:可以通过 F_GETFDF_SETFD 操作来 获取和设置
  • 获得/设置异步I/O所有权:通过F_GETOWN或F_SETOWN
  • 取消记录锁:可以通过 F_SETLK 操作取消记录锁。
  • 锁定文件:可以通过 F_SETLK 和 F_SETLKW 操作来对文件进行加锁,用于多进程/线程间对文件的访问控制。

下面我们利用fcntl写一个简单的代码例子(利用第三条:获取/设置文件状态标志):


① SetNonBlock(设置进程非阻塞状态)

基于上面介绍的fcntl,我们来实现一个设置进程非阻塞的代码:

bool SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL); // 获取指定文件描述符 fd 的文件状态标志
    if(fl < 0) {
        return false;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置文件状态标志为非阻塞

    return true;
}

对代码进行解释:

  1. int fl = fcntl(fd, F_GETFL)
    • 用于获取指定文件描述符 fd 的文件状态标志
    • F_GETFL 参数表示获取文件的状态标志;
  2. fcntl(fd, F_SETFL, fl | O_NONBLOCK)
    • 传入F_SETFL 参数,表示设置文件的状态标志
    • 通过将原来的文件状态标志 fl 与 O_NONBLOCK进行按位或 。O_NONBLOCK 是一个宏定义,用于表示将文件设置为非阻塞模式。

轮询方式读取标准输入

首先将标准输入设置为非阻塞,随后在循环内进行读取操作:

int main()
{
	SetNonBlock(0); // 将标准输入设置为非阻塞
    char buffer[1024];
	
	// 循环读取:
	// 在读取数据时,如果返回的字节数 s 大于0,表示成功读取了数据。然后将读取的数据输出到标准输出,并清空缓冲区。
	// 如果读取数据失败,则根据 errno 的值进行不同的处理。
    while(true)
    {
        sleep(1);
        errno = 0;
        // 非阻塞时,以出错形式返回,告知上层:数据未就绪
        ssize_t s = read(0, buffer, sizeof(buffer) - 1); // 0: 标准输入
        if(s > 0) {
            buffer[s-1] = 0;
            std::cout << "echo# " << buffer << " errno[---]" << errno << "errstring: " << strerror(errno) << std::endl;
        } else {
            // 若errno为11,意味着是底层数据未就绪,并非出错
            std::cout << "read \"error\" " << "errno: " << errno << "errstring: " << strerror(errno) << std::endl;
   		}
    }
}

执行上面的代码,会发现错误码为11:意味着数据非就绪(非阻塞下),并非一种错误,只需要等待数据就绪。

在这里插入图片描述
随后可以完善代码:

int main()
{
	SetNonBlock(0); // 设置一次非阻塞
    char buffer[1024];
    while(true)
    {
        sleep(2);
        errno = 0;
        // 非阻塞时,以出错形式返回,告知上层:数据未就绪
        ssize_t s = read(0, buffer, sizeof(buffer) - 1); // 0: 标准输入
        if(s > 0) {
            buffer[s-1] = 0;
            std::cout << "echo# " << buffer << " errno[---]" << errno << " | errstring: " << strerror(errno) << std::endl;
        } else {
            // 若errno为11,意味着是底层数据未就绪,并非出错
            // std::cout << "read \"error\" " << "errno:" << errno << " | errstring: " << strerror(errno) << std::endl;
            if(errno == EWOULDBLOCK || errno == EAGAIN) // 均为11
            {
                std::cout << "当前0号fd数据未就绪. try again!" << std::endl;
                continue;
            } 
            else if (errno == EINTR)
            {
                std::cout << "read被信号中断" << std::endl;
                continue;
            }
            else
            {
                // 错误处理
                std::cout << "read error" << std::endl;
            }
        }
    }
}

此时我们执行程序,当未输入数据时,进入的分支是errno == EWOULDBLOCK || errno == EAGAIN,一旦输入数据,可以正确读取

在这里插入图片描述

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值