系统调用分为两类,“低速”系统调用和其他。

“低速”系统调用指的是可能会使进程永远阻塞的一类系统调用。

非阻塞I/O则指的是当进行诸如open, read, write等I/O操作时,这些操作不会永远阻塞如果操作不能完成,则调用立即出错返回,以表示继续该操作将会阻塞

一般非阻塞I/O需要不断轮询判断是否有数据要进行读写,这种情况是比较浪费CPU时间的。避免非阻塞I/O的两种方式:I/O多路转接或多线程采用阻塞I/O

低速系统调用

低速系统调用可能会使进程永远阻塞,与非阻塞I/O刚好相反。

下面是低速系统调用的一些情形:

  1. 如果某些文件类型(读管道、终端设备以及网络设备)的数据不存在,读操作可能会使调用进程永远阻塞。
  2. 如果数据不能被相同的文件类型立即接受(如管道中无空间、网络流控制),写操作可能会使调用进程永远阻塞。
  3. 在某种条件发生之前打开某些文件类型可能会发生永久阻塞(如果以只写模式打开FIFO,那么在没有其他进程以读模式打开该FIFO时要一直等待)。
  4. 对已经加上强制性记录锁的文件进行读写。
  5. 某些ioctl操作。
  6. 某些进程间通信函数。

虽然读写磁盘会暂时阻塞调用进程,但不会永久阻塞下去,因此与磁盘相关的I/O不属于“低速”系统调用。

非阻塞I/O

非阻塞I/O使我们可以发出openreadwrite这样的I/O操作,并使这些操作不会永远阻塞。如果操作不能完成,则调用立即出错返回,表示该操作如继续执行将会阻塞。

对于一个给定的文件描述符,有两种方法为其指定非阻塞I/O。

  1. 如果要通过调用open函数获取描述符,则可指定O_NONBLOCK标志。
  2. 对于一个已经打开的文件描述符,则可调用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函数则会等到终端驱动程序处理完毕之后继续写入,而非返回错误。