1. 概念
当从一个fd读,写到另一个fd时,可以在下列形式的循环中使用阻塞I/0。
while((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0)
if (write(STDOUT_FILENO, buf, n) != n)
exit(1);
但是如果必须从两个fd中读,如果仍然使用阻塞式I/O,那么程序就会长时间阻塞在一个描述符上。这在网络编程中需要多个socket中获取数据的情况尤为常见。
解决方法一般有如下几种:
a).使用多进程/线程模型,每个进程/线程阻塞式等待一个fd。但是需要之间的多个信号通信机制,增加了程序的复杂性。
b).使用非阻塞式I/O(open with O_NOBLOCK),不断轮询(polling)多个描述符。但浪费CPU时间,并且多次执行read的系统调用。每次polling一遍后应该sleep若干时间,但这个时间很难确定。
c).使用信号驱动I/O模型。首先用sigaction设置SIGIO的信号处理程序,这样内核在数据ready的时候就发送一个SIGIO给进程,进程用信号处理程序接收并处理,完成时成功返回。
d).使用异步I/O(asynchronous I/O)。基本思想是进程告诉内核,当一个fd已经ready的时候,用一个signal通知它。需要注意的是,并非所有的UNIX系统都支持。(System V为这种机制提供了SIGPOLL信号,但是仅当fd是STEAMS设备的时才可用。另外这个信号对每个进程而言只有一个,如果该信号对两个fd都起作用则无法判断哪一个已经ready。为了确定,则将多个fd都设为非阻塞的,以此read来判断)。Linux支持异步I/O但是不默认支持STREAMS机制。与信号驱动I/O相比,信号驱动是通知发起时通知进程,然后将数据从内核读到进程空间。而异步I/O是完成全部过程才通知进程。
e).使用I/O复用(I/O multiplexing)。先构造一张有关fd的列表,然后调用一个函数。直到fd中一个已经准备进行I/O时,这个函数才返回。多路转接是这种问题实现的最好方式。具体函数介绍如下。
2.select和pselect函数
select函数使我们可以执行I/0多路转接,传向select的参数告诉内核:
(1).关心的fd
(2).对于每个fd关心的状态。(读,写或者异常)
从select返回,内核告诉我们:
(1).已经准备号的fd数量。
(2).对于读,写或者异常这三个状态中的每一个,哪些描述符已经准备好。
#include <sys/select.h>
int select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout
);
/*返回值
*-1 出错
*0 没有描述符准备好,并超时
*n>0 返回已准备好的描述符的数量,该值是三个描述符中已准备好的描述符之和,若一个描述既准备好读,又准备好了写,那么返回2。
*/
该函数提供了一种在单个进程中监视多个文件描述符的方法。可以对三种类型的描述符集进行监视:可读(第2个参数:readfds)、可写(第3个参 数:writefds)、处于异常状态(第4个参数:exceptfds)的描述符。从第2个参数起,参数都可以为空(NULL),当文件描述符集为空时,表示不监视其描述符的状态;nfds 是三个文件描述符号中最大的描述符+1。这样就会在一定的范围内搜索需要检测的描述符,否则,将会在所有可选的fd_set中搜索。
最后一个描述符为愿意等待的时间,
struct timeval {
long tv_sec; /*seconds*/
long tv_usec; /*and microseconds*/
}
timeval *timeout有三种情况
a). timeout == NULL 表示永远阻塞,直到fd准备好。
b). timeout->tv_sec == 0 && timeout->tv_usec == 0 表示完全不等待,测试所有的fd并立即返回。这样得到多个fd的状态而不阻塞select函数的polling方法。
c). timeout->tv_sec != 0 && timeout->tv_usec != 0 等待指定的秒数和毫秒数。当指定的fd之一已经ready时,或者指定时间到达时立即返回。如果是超时时返回则返回0。
fd_set类型中,每一个可能的文件描述符占1位。相关辅助函数:
- #include <sys/select.h>
- void FD_CLR(int fd, fd_set *set);
- int FD_ISSET(int fd, fd_set *set);
- void FD_SET(int fd, fd_set *set);
- void FD_ZERO(fd_set *set);
- 例如:
- fd_set rset, wset;
- int maxfd;
-
- FD_ZERO(&rset);
- FD_ZERO(&wset);
-
- FD_SET(STDIN_FILENO, &rset);
-
- FD_SET(STDOUT_FILENO, &wset);
- FD_SET(LOG_FILENO, &wset);
- maxfd = LOG_FILENO + 1;
-
- while(1){
-
- FD_ZERO(&rset);
- FD_ZERO(&wset);
-
- FD_SET(STDIN_FILENO, &rset);
-
- FD_SET(STDOUT_FILENO, &wset);
- FD_SET(LOG_FILENO, &wset);
-
- if ( (ret_no = select(maxfd, &rset, &wset, NULL, NULL)) == -1) && (errno == EINTR)) continue;
-
-
-
- ...
- }
- if (FD_ISSET(1, &wset)) {
- ...
- }
- if (FD_ISSET(LOG_FILENO, &wset)) {
- ...
- }
- }
在什么样的情况下描述符准备好?
.对于读集合(readfds) 中的一个描述符的读操作read不会阻塞,则此描述符准备好。
.对于写集合(writefds) 中的一个描述符的写操作write不会阻塞,则此描述符准备好。
.对于异常集合(exceptfds) 中的一个描述符有一个未决异常状态,则此描述符准备好。
pselect是select变体。与select不同的是timeout使用timespec结构,而且pselect可以使用可选信号屏蔽字,若sigmask为非空,则使用pselect时以原子操作安装该信号屏蔽字,返回时恢复。
3.poll与ppoll:功能类似于select/pselect。与select函数不同,poll函数不时按照文件描述符的类型来组织信息的,而是通过pollfd数组来组织信息。每个数组元素指定一个描述符编号以及一个对其关心的状态(从数字的fds中的event设置)。
- #include <poll.h>
- int poll(
- struct pollfd *fds,
- nfds_t nfds,
- int timeout
- );
-
- int ppoll(
- struct pollfd *fds,
- nfds_t nfds,
- const struct timespec *timeout,
- const sigset_t *sigmask
- );
-
-
-
-
-
-
- struct pollfd {
- int fd;
- short events;
- short revents;
- };
时间参数:
int timeout;
timeout == -1永远等待直到当所指定的fd有一个已经准备好。
timeout == 0 不等待立即返回
timeout > 0 当指定的fd之一已经准备好,或者超过指定的时间返回(超时返回则返回值0)。
Reference:
APUE Chapter 14
UNP Chapter 6