非阻塞IO(Non-blocking IO)是一种IO模型,其特点是在应用程序执行IO操作时,如果数据尚未准备好或无法立即进行IO操作,应用程序不会进入阻塞状态,而是立即返回一个错误码或指示数据未就绪的状态,让应用程序可以继续执行其他任务。
非阻塞式IO的工作流程通常如下:
- 应用程序发起非阻塞IO操作(如读取文件、接收网络数据等)。
- 如果数据已经准备好,或者可以立即进行IO操作,IO操作会立即完成,应用程序继续执行后续代码。
- 如果数据尚未准备好,或者无法立即进行IO操作(如网络数据尚未到达、文件尚未准备好等),IO操作会立即返回一个错误码或指示数据未就绪的状态,应用程序可以继续执行其他任务,而不是等待数据准备就绪。
- 应用程序可以周期性地轮询IO操作的状态,直到数据准备就绪或IO操作完成。
非阻塞IO的优点包括:
- 资源利用率高:应用程序不会一直等待数据准备就绪,而是可以继续执行其他任务,提高了系统的资源利用率。
- 适合高并发环境:在高并发环境下,非阻塞IO可以有效地处理大量IO请求,而不会导致系统资源的耗尽。
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一 般只有特定场景下才使用。
非阻塞IO
打开文件时默认都是以阻塞的方式打开的,如果要以非阻塞的方式打开某个文件,需要在使用open函数打开文件时携带O_NONBLOCK
或O_NDELAY
选项,此时就能够以非阻塞的方式打开文件。
open函数的介绍:
这是在打开文件时设置非阻塞的方式,如果要将已经打开的某个文件或套接字设置为非阻塞,此时就需要用到fcntl
函数。
fcntl函数
通过man手册认识一下fcntl函数
fcntl
函数的原型如下:
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */);
参数说明:
- fd:已经打开的文件描述符。
- cmd:需要进行的操作。
- …:可变参数,传入的cmd值不同,后面追加的参数也不同。
fcntl
函数的常见用法包括:
-
获取/设置文件状态标志:
- 使用
F_GETFL
命令获取文件状态标志,使用F_SETFL
命令设置文件状态标志。 - 文件状态标志控制着文件的各种属性,如读写模式、非阻塞模式等。
- 使用
-
获取/设置文件描述符标识:
- 使用
F_GETFD
命令获取文件描述符标识,使用F_SETFD
命令设置文件描述符标识。 - 文件描述符标识可以控制文件描述符的关闭行为,如关闭时是否自动释放资源等。
- 使用
-
获取/设置文件锁:
- 使用
F_GETLK
命令获取文件锁信息,使用F_SETLK
和F_SETLKW
命令设置文件锁。 - 文件锁用于控制文件的并发访问,可以实现读写锁、共享锁、排它锁等。
- 使用
-
其他控制操作:
- 还可以使用
F_DUPFD
、F_DUPFD_CLOEXEC
等命令复制文件描述符,使用F_SETOWN
、F_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中进行错误检查的一般步骤:
-
返回值检查:检查IO操作的返回值,通常是一个整数,表示已读取/写入的字节数或错误码。如果返回值为负数,则表示发生了错误。
-
错误码检查:如果返回值为负数,使用
errno
全局变量来获取错误码,以确定发生了什么错误。 -
特定错误处理:根据错误码进行特定的错误处理,可能需要采取不同的措施来处理不同的错误,常见的错误包括:
EAGAIN
或EWOULDBLOCK
:表示IO操作被非阻塞模式下的文件描述符阻塞,这通常不是一个严重的错误,可以忽略或稍后重试。EINTR
:表示IO操作由于被信号中断而失败,通常需要重新尝试该操作。- 其他错误码:根据具体情况进行相应的错误处理,可能需要关闭文件描述符、重新连接、记录错误日志等操作。
运行结果:
以上就是关于非阻塞IO的介绍了,设置非阻塞IO一般都是通过SetNoBlock函数进行设置,需要特别关心的就是非阻塞IO和阻塞IO的错误检查的不同。