10.5节中曾将系统调用分成两类:“低速”系统调用和其他。低速系统调用是可能会使进程永远阻塞的一类系统调用,包括:

  • 如果某些文件类型(如读管道、终端设备和网络设备)的数据并不存在,读操作可能会使调用者永远阻塞;
  • 如果数据不能被相同的文件类型立即接受(如管道中无空间、网络流控制),写操作可能会使调用者永远阻塞;
  • 在某种条件发生之前打开某些文件类型可能会发生阻塞(如要打开一个终端设备,需要先等待与之连接的调制解调器应答,又如若以只写模式打开FIFO,那么在没有其他进程已用读模式打开该FIFO时也要等待);
  • 对已经加上强制性记录锁的文件进行读写;
  • 某些ioctl操作;
  • 某些进程间通信函数(见第15章)。

我们也曾说过,虽然读写磁盘文件会暂时阻塞调用者,但并不能将与磁盘I/O有关的系统调用视为“低速”。

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

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

  1. 如果调用open获得描述符,则可指定O_NONBLOCK标志(见3.3节)。
  2. 对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志(见3.14节)。图3-12中的函数可用来为一个描述符打开任一文件状态标志。

System V的早期版本使用标志O_NDELAY指定非阻塞方式。在这些System V版本中,如果无数据可读,则read返回0。而UNIX系统又常将read的返回值0解释为文件结束,两者有所混淆。POSIX.1提供了一个非阻塞标志,它的名字和语义都与O_NDELAY不同。确实,在System V的早期版本中,当从read得到返回值0时,我们并不知道该调用是阻塞了还是遇到了文件尾端。POSIX.1要求,对于一个非阻塞的描述符如果无数据可读,则read返回−1,errno被设置为EAGAIN。System V派生的某些平台既支持较旧的O_NDELAY,又支持POSIX.1的O_NONBLOCK,但在本书的实例中只使用POSIX.1规定的特征。较旧的O_NDELAY只是为了向后兼容,不应在新应用程序中使用。

4.3BSD为fcntl提供了FNDELAY标志,其语义也稍有区别。它不只影响描述符的文件状态标志,还将终端设备或套接字的标志更改成非阻塞的,因此不仅影响共享同一文件表项的用户,而且对终端或套接字的所有用户起作用(4.3BSD 非阻塞 I/O 只对终端和套接字起作用)。另外,如果对一个非阻塞描述符的操作不能无阻塞地完成,那么4.3BSD返回EWOULDBLOCK。现今,基于BSD的系统提供POSIX.1的O_NONBLOCK标志,并且将EWOULDBLOCK定义为与POSIX.1的EAGAIN相同。这些系统提供与其他POSIX兼容系统相一致的非阻塞语义:文件状态标志的更改影响同一文件表项的所有用户,但与通过其他文件表项对同一设备的访问无关。

实例

图14-1中的程序是一个非阻塞I/O的实例,它从标准输入读500 000字节,并试图将它们写到标准输出上。该程序先将标准输出设置为非阻塞的,然后用for循环进行输出,每次write调用的结果都在标准错误上打印。函数clr_fl类似于图3-12中的set_fl。这个新函数清除1个或多个标志位。

#include "apue.h"
#include <errno.h>
#include <fcntl.h>

char    buf[500000];

int
main(void)
{
    int    ntowrite, nwrite;
    char    *ptr;

    ntowrite = read(STDIN_FILENO, buf, sizeof(buf));
    fprintf(stderr, "read %d bytes\n", ntowrite);

    set_fl(STDOUT_FILENO, O_NONBLOCK);    /* set nonblocking */

    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);    /* clear nonblocking */

    exit(0);
}

图14-1 长的非阻塞write

若标准输出是普通文件,则可以期望write只执行一次。

$ ls -l /etc/services                                      打印文件长度
-rw-r--r-- 1 root   677959 Jun 23 2009 /etc/services

$ ./a.out < /etc/services > temp.file                    先试一个普通文件
read 500000 bytes
nwrite = 500000, errno = 0                                 一次写
$ ls -l temp.file                                 检验输出文件长度
-rw-rw-r-- 1 sar   500000 Apr 1 13:03 temp.file

但是,若标准输出是终端,则期望write有时返回小于500000的一个数字,有时返回错误。下面是运行结果:

$ ./a.out < /etc/services 2>stderr.out   
终端至输出大量输出至终端……
$ cat stderr.out
read 500000 bytes
nwrite = 999, errno = 0
nwrite = -1, errno = 35
nwrite = -1, errno = 35
nwrite = -1, errno = 35
nwrite = -1, errno = 35
nwrite = 1001, errno = 0
nwrite = -1, errno = 35
nwrite = 1002, errno = 0
nwrite = 1004, errno = 0
nwrite = 1003, errno = 0
nwrite = 1003, errno = 0
nwrite = 1005, errno = 0
nwrite = -1, errno = 35           
.....61个此类错误
nwrite = 1006, errno = 0
nwrite = 1004, errno = 0
nwrite = 1005, errno = 0
nwrite = 1006, errno = 0
nwrite = -1, errno = 35
......108个此类错误
nwrite = 1006, errno = 0
nwrite = 1005, errno = 0
nwrite = 1005, errno = 0
nwrite = -1, errno = 35           
......681个此类错误等等
nwrite = 347, errno = 0

在该系统上,errno 值35 对应的是EAGAIN。终端驱动程序一次能接受的数据量随系统而变。具体结果还会因登录系统时所使用的方式的不同而不同:在系统控制台上登录、在硬接线的终端上登录或用伪终端在网络连接上登录。如果你在终端上运行一个窗口系统,那么也是经由伪终端设备与系统交互。

在此实例中,程序发出了9000多个write调用,但是只有500个真正输出了数据,其余的都只返回了错误。这种形式的循环称为轮询,在多用户系统上用它会浪费CPU时间。14.4节将介绍非阻塞描述符的I/O多路转接,这是进行这种操作的一种比较有效的方法。

有时,可以将应用程序设计成使用多线程的(见第11章),从而避免使用非阻塞I/O。如若我们能在其他线程中继续进行,则可以允许单个线程在I/O调用中阻塞。这种方法有时能简化应用程序的设计(见第21章),但是,线程间同步的开销有时却可能增加复杂性,于是导致得不偿失的后果。

results matching ""

    No results matching ""