非阻塞IO
系统调用分为两类,“低速”系统调用和其他。
“低速”系统调用指的是可能会使进程永远阻塞的一类系统调用。
非阻塞I/O则指的是当进行诸如open, read, write
等I/O操作时,这些操作不会永远阻塞。如果操作不能完成,则调用立即出错返回,以表示继续该操作将会阻塞。
一般非阻塞I/O需要不断轮询判断是否有数据要进行读写,这种情况是比较浪费CPU时间的。避免非阻塞I/O的两种方式:I/O多路转接或多线程采用阻塞I/O。
低速系统调用
低速系统调用可能会使进程永远阻塞,与非阻塞I/O刚好相反。
下面是低速系统调用的一些情形:
- 如果某些文件类型(读管道、终端设备以及网络设备)的数据不存在,读操作可能会使调用进程永远阻塞。
- 如果数据不能被相同的文件类型立即接受(如管道中无空间、网络流控制),写操作可能会使调用进程永远阻塞。
- 在某种条件发生之前打开某些文件类型可能会发生永久阻塞(如果以只写模式打开FIFO,那么在没有其他进程以读模式打开该FIFO时要一直等待)。
- 对已经加上强制性记录锁的文件进行读写。
- 某些
ioctl
操作。 - 某些进程间通信函数。
虽然读写磁盘会暂时阻塞调用进程,但不会永久阻塞下去,因此与磁盘相关的I/O不属于“低速”系统调用。
非阻塞I/O
非阻塞I/O使我们可以发出open
、read
和write
这样的I/O操作,并使这些操作不会永远阻塞。如果操作不能完成,则调用立即出错返回,表示该操作如继续执行将会阻塞。
对于一个给定的文件描述符,有两种方法为其指定非阻塞I/O。
- 如果要通过调用
open
函数获取描述符,则可指定O_NONBLOCK
标志。 - 对于一个已经打开的文件描述符,则可调用
fcntl
,由该函数打开O_NONBLOCK
文件状态标志。
示例
在这个示例中,从标准输入预计读取五千万字节,然后写入到标准输出中。该程序先将标准输出设置为非阻塞的,然后for
循环进行输出,每次write
调用的结果都在标准错误上打印。
详细代码如下:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char buf[50000000] = {0};
// 设置文件状态标志函数
void set_fl(int fd, int flags)
{
int val;
if ((val = fcntl(fd, F_GETFL, 0)) < 0)
{
perror("fcntl");
exit(-1);
}
val |= flags;
if (fcntl(fd, F_SETFL, val) < 0)
{
perror("fcntl");
exit(-1);
}
}
// 清除文件状态标志
void clr_fl(int fd, int flags)
{
int val;
if ((val = fcntl(fd, F_GETFL, 0)) < 0)
{
perror("fcntl");
exit(-1);
}
val &= ~flags;
if (fcntl(fd, F_SETFL, val) < 0)
{
perror("fcntl");
exit(-1);
}
}
int main()
{
int ntowrite, nwrite;
char* ptr;
// 从标准输入中读取sizeof(buf)个字节
ntowrite = read(STDIN_FILENO, buf, sizeof(buf));
fprintf(stderr, "read %d bytes\n", ntowrite);
// 设置标准输出非阻塞
set_fl(STDOUT_FILENO, O_NONBLOCK);
ptr = buf;
while (ntowrite > 0)
{
errno = 0;
nwrite = write(STDOUT_FILENO, ptr, ntowrite);
fprintf(stderr, "nwrite=%d, errno=%d\n", nwrite, errno);
if (nwrite > 0)
{
ptr += nwrite;
ntowrite -= nwrite;
}
}
clr_fl(STDOUT_FILENO, O_NONBLOCK);
exit(0);
}
执行结果统计:
$ less ./stderr.out
read 2307137 bytes
nwrite=780231, errno=0
nwrite=-1, errno=11
...
nwrite=-1, errno=11
nwrite=4451, errno=0
...
$ grep -rn "errno=0" ./stderr.out | wc -l
25
$ grep -rn "errno=11" ./stderr.out | wc -l
215436
$ wc -l ./stderr.out
215462 ./stderr.out
在Linux系统上,errno=11
表示EAGAIN
。
在这个示例中,程序共调用了215462次write
函数,但是仅有25次输出了数据,其余都调用失败。
这种形式的循环称为轮询,缺点是浪费CPU时间。
失败原因是终端驱动程序无法及时处理这么大的数据量。如果使用阻塞I/O,write
函数则会等到终端驱动程序处理完毕之后继续写入,而非返回错误。
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/apue/%E9%9D%9E%E9%98%BB%E5%A1%9Eio/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。