掘金 后端 ( ) • 2024-04-25 17:10

非阻塞IO(Non-blocking IO)是一种IO模型,其特点是在应用程序执行IO操作时,如果数据尚未准备好或无法立即进行IO操作,应用程序不会进入阻塞状态,而是立即返回一个错误码或指示数据未就绪的状态,让应用程序可以继续执行其他任务。

非阻塞式IO的工作流程通常如下:

  1. 应用程序发起非阻塞IO操作(如读取文件、接收网络数据等)。
  2. 如果数据已经准备好,或者可以立即进行IO操作,IO操作会立即完成,应用程序继续执行后续代码。
  3. 如果数据尚未准备好,或者无法立即进行IO操作(如网络数据尚未到达、文件尚未准备好等),IO操作会立即返回一个错误码或指示数据未就绪的状态,应用程序可以继续执行其他任务,而不是等待数据准备就绪。
  4. 应用程序可以周期性地轮询IO操作的状态,直到数据准备就绪或IO操作完成。

非阻塞IO的优点包括:

  • 资源利用率高:应用程序不会一直等待数据准备就绪,而是可以继续执行其他任务,提高了系统的资源利用率。
  • 适合高并发环境:在高并发环境下,非阻塞IO可以有效地处理大量IO请求,而不会导致系统资源的耗尽。

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

非阻塞IO

打开文件时默认都是以阻塞的方式打开的,如果要以非阻塞的方式打开某个文件,需要在使用open函数打开文件时携带O_NONBLOCKO_NDELAY选项,此时就能够以非阻塞的方式打开文件。
open函数的介绍: image.png 这是在打开文件时设置非阻塞的方式,如果要将已经打开的某个文件或套接字设置为非阻塞,此时就需要用到fcntl函数。

fcntl函数

通过man手册认识一下fcntl函数

image.png fcntl函数的原型如下:

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

参数说明:

  • fd:已经打开的文件描述符。
  • cmd:需要进行的操作。
  • …:可变参数,传入的cmd值不同,后面追加的参数也不同。

fcntl函数的常见用法包括:

  1. 获取/设置文件状态标志

    • 使用F_GETFL命令获取文件状态标志,使用F_SETFL命令设置文件状态标志。
    • 文件状态标志控制着文件的各种属性,如读写模式、非阻塞模式等。
  2. 获取/设置文件描述符标识

    • 使用F_GETFD命令获取文件描述符标识,使用F_SETFD命令设置文件描述符标识。
    • 文件描述符标识可以控制文件描述符的关闭行为,如关闭时是否自动释放资源等。
  3. 获取/设置文件锁

    • 使用F_GETLK命令获取文件锁信息,使用F_SETLKF_SETLKW命令设置文件锁。
    • 文件锁用于控制文件的并发访问,可以实现读写锁、共享锁、排它锁等。
  4. 其他控制操作

    • 还可以使用F_DUPFDF_DUPFD_CLOEXEC等命令复制文件描述符,使用F_SETOWNF_GETOWN等命令设置/获取文件描述符的属主等。

设置非阻塞模式这里只介绍第一种用法。
基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞

SetNoBlock

void SetNonBlock(int fd)
{
    //获取当前文件描述符的属性
    int f1 = fcntl(fd, F_GETFL);
    if (f1 < 0)
    {
        perror("fcntl");
        return;
    }
    //设置文件描述符的属性未非阻塞状态O_NONBLOCK
    fcntl(fd, F_SETFD, f1 | O_NONBLOCK);
}

调用上面函数就可以将该文件描述符设置为了非阻塞状态。
示例:
以非阻塞轮询的方式读取标准输入

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
bool SetNonBlock(int fd)
{
    // 获取当前文件描述符的属性
    int f1 = fcntl(fd, F_GETFL);
    if (f1 < 0)
    {
        perror("fcntl");
        return false;
    }
    // 设置文件描述符的属性未非阻塞状态O_NONBLOCK
    fcntl(fd, F_SETFL, f1 | O_NONBLOCK);
    return true;
}
int main()
{
    // 将标准输入设置为非阻塞
    if (!SetNonBlock(0))
    {
        std::cout << "SetNonBlock Fail" << std::endl;
        return -1;
    }

    char buffer[1024];
    while (true)
    {
        ssize_t read_size = read(0, buffer, sizeof(buffer));
        if (read_size < 0)
        {
            // 非阻塞模式下暂时没有数据可读,稍后重试
            if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                std::cout << strerror(errno) << std::endl;
                sleep(1);
                continue;
            }
            // IO操作被信号中断,重新尝试该操作
            else if (errno == EINTR)
            {
                std::cout << strerror(errno) << std::endl;
                sleep(1);
                continue;
            }
            // 其他错误,需要处理
            else
            {
                std::cerr << "read error" << std::endl;
                break;
            }
        }
        // 读到文件结尾,表示对端已关闭连接
        else if (read_size == 0)
        {
            std::cout << "Connection closed" << std::endl;
            break;
        }
        // 读取成功
        else
        {
            buffer[read_size - 1] = '\0';
            std::cout << "echo# " << buffer << std::endl;
        }
    }
    return 0;
}

在非阻塞IO中,错误检查与阻塞IO有所不同,因为在非阻塞模式下,某些IO操作可能会立即返回而不是等待。以下是在非阻塞IO中进行错误检查的一般步骤:

  1. 返回值检查:检查IO操作的返回值,通常是一个整数,表示已读取/写入的字节数或错误码。如果返回值为负数,则表示发生了错误。

  2. 错误码检查:如果返回值为负数,使用errno全局变量来获取错误码,以确定发生了什么错误。

  3. 特定错误处理:根据错误码进行特定的错误处理,可能需要采取不同的措施来处理不同的错误,常见的错误包括:

    • EAGAINEWOULDBLOCK:表示IO操作被非阻塞模式下的文件描述符阻塞,这通常不是一个严重的错误,可以忽略或稍后重试。
    • EINTR:表示IO操作由于被信号中断而失败,通常需要重新尝试该操作。
    • 其他错误码:根据具体情况进行相应的错误处理,可能需要关闭文件描述符、重新连接、记录错误日志等操作。 运行结果:

recording.gif 以上就是关于非阻塞IO的介绍了,设置非阻塞IO一般都是通过SetNoBlock函数进行设置,需要特别关心的就是非阻塞IO和阻塞IO的错误检查的不同。