分类: 系统运维
2012-04-01 20:22:12
当我们从一个描述符里读并写到另一个时,我们可以在一个循环里使用阻塞I/O,比如:
while ((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0)
if (write(STDOUT_FILENO, buf, n) != n)
err_sys("write error");
我们一次又一次看到这种形式的阻塞I/O。如果我们必须从两个描述符里读是怎么样呢?在这种情况下,我们不能在描述符上执行阻塞read,因为在我们阻塞在一个描述符上的read上时,数据可以出现在另一个描述符上。需要一个不同的技术来处理这种情况。
让 我们看下telnet命令的结构。在这个程序里,我们从终端(标准输入)里读,并写到一个网络连接,以及从网络连接上读并写到终端(标准输出)。在网络连 接的另一端,telnetd守护进程读我们输入的东西,并展示给外壳,好像是我们登录了一个远程机器一样。telnetd守护进程把任何由我们输出的命令 产生的输出发送回我们,通过telnet命令,来显示到我们的终端。
telnet进程有两个输入和两个输出。我们不能在各个输入上执行一个阻塞read,因为我们不知道哪个输入会有我们的数据。
处理这种特殊问题的方法是把进程分为两块(使用fork),每半部分处理一个方向的数据。(系统V的uucp通信包提供的cu命令有类似的结构。)
如 果我们使用两个进程,我们可以让每个进程执行一个阻塞read。但是这导致一个问题,当操作终止时。如果子进程收到一个文件末尾(telnetd守护进程 断开了网络连接),那么子进程然后终止,父进程被SIGCHLD信号通知。但是如果父进程终止(用户在终端输入文件末尾),那么父进程然后必须告诉子进程 停止。我们可以使用一个信号来处理(例如SIGUSR1),但是这确实复杂化了程序。
作为两个进程的替代,我们可以使用单个进程里的两个线程。这避免了终止的复杂,但要求我们处理线程间的同步,它加入比所节省的更多的复杂性。
我们可以在单个进程里使用非阻塞I/O,能把两个描述符设置为非阻塞的,并在第一个描述符上执行一个read。如果数据出现,我们读取它并处理它。如果没有 数据可读,调用立即返回。我们然后为第二个描述符做相同的事。在此之后,我们等待一段时间(可能几秒),然后尝试再次从第一个描述符读。这种类型的循环被 称为轮询。问题是它浪费CPU时间。多数时间,没有数据可读,所以我们浪费时间在执行read系统调用上。我们也必须猜想在每次循环里要等多久。尽管它在 任何支持非阻塞I/O的系统上工作,但是轮循应该在多任务系统上避免。
另一个技术被称为异步I/O。为了执行它,我们告诉内核用一个信号通 知我们,当一个描述符已经准备好I/O时。这样做有两个问题。首先,不是所有系统都支持这个特性(这是SUS的可选特性)。系统V为这个技术提供了 SIGPOLL信号,但是信号只在描述符指向一个STREAMS设备时才工作。BSD有类似的信号,SIGIO,但是它有相似的限制:它只当描述符指向一 个终端或网络时才会工作。这个技术的第二个问题是每个进程只有一个这样的信号(SIGPOLL或SIGIO)。如果我们为两个描述符启用这个信号,(在我 们提过的例子里,从两个描述符里读),信号的发生不告诉我们哪个描述符准备好了。为了确定哪个描述符已准备,我们仍需要设置每个为非阻塞的,然后依次尝试 它们。我们在14.6节简单讨论异步I/O。
一个更好的技术是使用I/O复用。要这样做,我们建立一个我们感兴趣的描述符列表(通常多于一个)并调用一个不返回的函数,直到某个描述符准备好I/O。在从这个函数返回时,我们被告知哪个描述符准备好I/O了。
三个函数--poll、pselect和select--允许我们执行I/O复用。下表总结了哪些平台支持它们。注意select由基于POSIX.1标准定义,但poll是基础的一个XSI扩展。
系统 | poll | pselect | select | |
---|---|---|---|---|
SUS | XSI | * | * | * |
FreeBSD | * | * | * | |
Linux2.4.22 | * | * | * | * |
Mac OS X 10.3 | * | * | * | |
Solaris 9 | * | * | * |
I/O复用随着select函数由4.2BSD提供。这个函数总是和任何描述 符都工作,尽管它的主要使用是为了终端I/O和网络I/O。SVR3加上了poll函数,当STREAMS机制被加入时。然而,最开始poll只对 STREAMS设备有用。在SVR4里,允许poll工作在任何描述符上的支持被加入。
14.5.1 select和pselect函数
select函数让我们执行I/O复用,在所有POSIX平台下。我们传入select的参数告诉内核
1、我们对哪个描述符感兴趣;
2、为每个描述符我们对什么条件感兴趣。(我们想要从一个给定的描述符读吗?我们想向一个给定的描述符写吗?我们对一个给定的描述符的一个例外条件感兴趣吗?)
3、我们想等待多久。(我们可以永远地等,等待一段时间,或完全不等。)
在从select返回时,内核告诉我们
1、已经准备好的描述符的数量的总计数。
2、哪些描述符准备好三种情况(read、write或例外条件)。
有了返回信息,我们可以调用恰当的I/O函数(通常read或write)并知道哪些函数不会阻塞。
让我们首先看下最后的参数。这指明我们想要等待多久:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};
有三种情况。
tvptr == NULL:永远等待。这个无尽的等待可以被中断,如果我们捕获一个信号。当某个指定的描述符准备好或当一个信号被捕获时会返回。如果一个信号被捕获,那么select返回-1,errno设为EINTR。
tvptr->tv_sec == 0 && tvptr->usec == 0:不要等待,所有指定的描述符被测试,然后立即返回。这是轮询系统来找到多个描述符状态而不在select函数里阻塞的一种方法。
tvptr->tv_sec != 0 || tvptr->tv_usec != 0:等待指定的秒数和毫秒数。当某个指定的描述符准备好或计时过期时返回。如果在符合要描述符准备好之前计时过期,那么返回值为0。(如果系统不提供微秒 的精度,那么tvptr->tv_usec直会约到最近的支持的值。)和第一种情况一样,这个等待也被一个捕获的信号中断。
POSIX.1 允许一个实现修改timeval结构,所以在select返回时,你不能假设结构体和在调用select之前包含相同的值。FreeBSD、Mac OS X、Solaris都保持结构体不变,但是Linux用剩余时间更新它,如果select在计时过期之前返回。
中间三个参数 --readfds、writefds、和exceptfds--是描述符集的指针。这三个集合指定我们对哪些描述符,以及哪个情况(可读、可写或一个异 常条件)感兴趣。一个描述符集被存储在一个fd_set数据类型里。这个数据类型由实现选择以便它能为每个可能的描述符存储一个位。我们可以只视它为一个 大的位数组。
我们对fd_set数据类型唯一可以做的事情是分配一个这种类型的变量,把一个这样类型的变量赋给另一个相同类型的变量,或使用下面四个函数的某个来处理这种类型的变量。
这些接口可以作为函数或宏实现。一个fd_set被设为所有0位,通过调用FD_ZERO。为了打开一个集合里的单个位,我们使用FD_SET。我们可以调用FD_CLR来清除单个位。最后,我们可以测试一个给定位是否在集合里被打开,使用FD_ISSET。
在声明一个描述符集后,我们必须清零这个集合,使用FD_ZERO。我们然后为每个感兴趣的描述符在集合里设置位,就如:
fd_set rset;
int fd;
FD_ZERO(&rset);
FD_SET(fd, &rset);
FD_SET(STDIN_FILENO, &rset);
在从select返回时,我们可以用FD_ISSET测试一个给定的位是否仍然开启:
if (FD_ISSET(fd, &rset)) {
...
}
select 的中间三个参数(描述符集的指针)中的任一个(或全部)可以是空指针,如果我们不对那个情况感兴趣。如果所有三个指针都是NULL,那么我们有一个比 sleep提供的更高精度的计时器。(回想10.19节slee等待整数秒数。有了select,我们可以等待比1秒更小的间隔;真实的精度取决于系统的 时钟。)
select的第一个参数,masfdp1,表示“最大文件描述符加1.”我们计算我们感兴趣的最高的描述符,包括所有三个描述
集,加上1,就是第一个参数。我们可以只设置第一个参数为FD_SETSIZE,一个在
例如,如果我们这样写:
fd_set readset, writeset;
FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_SET(0, &readset);
FD_SET(3, &readset);
FD_SET(1, &writeset);
FD_SET(2, &writeset);
select(4, &readset, &writeset, NULL, NULL);
那么,描述符4以上(包括4)的位都不会被检查。
我们必须在最大描述符号上加1的原因是描述符从0开始,而第一个参数实际上是要检查的描述符的数量的计数(从描述符0开始)。
select有三种可能的返回值。
1、一个-1的返回值表示一个错误发生。例如,如果信号在任何指定的描述符准备好前被捕获,这可能发生。在这种情况下,没有一个描述符集会被修改。
2、一个0的返回值表示没有描述符准备。这在时间限制过期时没有任何描述符准备好时发生。当这个发生时,所有的描述符集都将被清0.
3、一个正的返回值指明准备好的描述符的数量。这个值是在所有三个集里面准备好的描述符的和,所以如果相同的描述符准备好读和写,那么它将在返回时值计算两次。唯一留在这三个描述符集里的位只有那么对应对准备好的描述符的。
我们现在需要更明确的“准备好”的意思。
1、一个在读集合(readfds)里的描述符被视为准备好,当从那个描述符的一个read不会阻塞。
2、一个在写集合(writefds)里的描述符被视为准备好,当向那么描述符的一个write不会阻塞。
3、一个在异常集全(exceptfds)里描述符被视为准备好,当一个异常情况被添加到那个描述符。当前,一个对应于在网络连接上超过边界的数据的到达或在被置为包模式的伪终端的特定情况发生时。
4、普通文件的文件描述符问题返回为读、写和异常条件准备好。
意 识到一个描述符是否是阻塞的不会影响select是否阻塞是很重要的。那是说,如果我们有一个非阻塞的描述符,我们想从它读,我们调用带有5秒计时的 select,select会最多阻塞5秒。类似的,如果我们执行一个无尽的计时,select会阻塞,直到描述符的数据准备好,或一个信号被捕获。
如果我们碰一个描述符的文件尾,那么那个描述符被select视为可读。我们然后调用read,它返回0,在UNIX系统上表示文件末尾的方法。(许多人不正确地假设select当文件末尾到达时,会指出一个在描述符上的异常条件。)
POSIX.1也定义了select的变体,称为pselect。
pselect函数和select一样,除了以下的例外:
1、
select的计时值被一个timeval结构体指明,但是对于pselect,一个timespec结构体被使用。(回想11.6节timespec结
构体的定义。)作为秒和微秒的替代,timespec结构体以秒和纳秒表示计时值。这提供了更高精度的计时,如果平台支持这种粒度的话。
2、pselect的计时值被声明为const,我们被保证它的值不会因为pselect的调用被改变。
3、一个可先的信号掩码参数在pselect里可用。如果sigmask为空,那么pselect和select对待信号的行为一样。否则,sigmask指向一个信号掩码,它在pselect调用时被自动安装。返回时,之前的信号掩码被恢复。
14.5.2 poll函数
poll函数和select相似,除程序员接口不同。正如我们将看到的,poll和STREAMS系统绑定,因为它起源于系统V,尽管我们可以在其它类型的文件描述符上使用它。
使用poll,我们不必为每个条件(读、写和异常条件)建立一堆描述符,如我们用select做的,我们建立一个pollfd结构体的数据,每个数组元素指定一个描述符号和我们对那个描述符感兴趣的条件。
struct pollfd {
int fd; /* file descriptor to check, or <0 to ignore */
short events; /* event of interest on fd */
short revents; /* event that occurred on fd */
};
在fdarray数组里的元素数由nfds指定。
历
史上,对于nfds参数如何被声明有几种不同的方式。SVR3指定数组里的元素个数作为一个无符号长整型,它看起来过大。在SVR4手册里,poll的原
型展示了第二个参数的数据类型为size_t。(回想第二章原始系统数据类型。)但是
对应于SVR4的SVID显示poll的第一个参数为struct pollfd fdarray[],而SVR4手册面显示这个参数为struct pollfd *fdarray。在C语言,两个声明是一样的。我们使用第一个声明来表示fdarray指向一个结构体数组,而不是单个结构体。
为了告诉 肉体我们在每个描述符上对什么事件感兴趣,我们必须设置每个数组元素的events成员为下表的一个或多个值。在返回时,revents成员被内核设置, 指明每个描述符上的哪些事件已发生。(注意poll不改变events成员。这和select不同,它修改它的参数来表示什么已经准备好了。)
名字 | events的输入? | revents的结果? | 描述 |
---|---|---|---|
POLLIN | * | * | 除了高优先级之外的数据可以不被阻塞地读,等价于(POLLRDNORM|POLLRDBAND)。 |
POLLRDNORM | * | * | 普通数据(优先带宽为0)可以无阻塞地读。 |
POLLRDBAND | * | * | 非0优先带宽的数据可以无阻塞地读。 |
POLLPRI | * | * | 高优先级数据可以无阻塞地读。 |
POLLOUT | * | * | 普通数据可以可以无阻塞地写。 |
POLLWRNORM | * | * | 和POLLOUT一样。 |
POLLWRBAND | * | * | 非0优先带宽的数据可以可以无阻塞地写。 |
POLLERR | * | 一个错误发生。 | |
POLLHUP | * | 一个挂起发生。 | |
POLLNVAL | * | 描述符没有指向一个打开的文件。 |
当一个描述符被挂起(POLLHUP)时,我们不再能够写入这个描述符。然而,仍然可能有数据从这个描述符上读。
poll的最后参数指明我们想要等待多久。和select一样,有三种情况。
timeout == -1:永远等待。(一些系统在
timeout == 0:不等待。所有指定的描述符被测试,我们立即返回。这是轮询系统来找到多个描述符状态而不阻塞poll调用的方法。
timeout > 0:等待timeout毫秒。我们当某个指定的描述符准备好或当timeout过期时返回。如果timeout在任何描述符准备好之前过期,那么返回值为0。(如果你的系统不提供毫秒精度,timeout会向上约到最近的支持的值。)
意识到文件末尾和挂起的区别是重要的。如果我们不从终端进程并输入文件末尾符,那么POLLIN被打开把以我们可以讲到文件末尾符(read返回0)。POLLHUP在revents里不被打开。如果我们从一个挂起的猫或电话线上读,那么我们收到POLLHUP通知。
和select一样,一个描述符是否阻塞不影响poll阻塞。
select和poll的中断能力
当中断的系统调用的自动重启在4.2BSD引入时(10.5节),select函数从未被重启。这个特性在多数系统上延续,即使SA_RESTEART选项 被指定。但是在SVR4下,如果SA_RESTART被指定,那么甚至select和poll会自动重启。为了避免这个影响我们,当我们移植软件到 SVR4的后代系统时,我们总是使用signal_intr函数,如果信号可以中断一个select或poll调用。
本文四个实现没有一个重启poll或select,当一个信号被收到时,即使SA_RESTART标志被使用。