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位。相关辅助函数:
 
  1. #include <sys/select.h> 
  2. void FD_CLR(int fd, fd_set *set);   //清除其中的一位 
  3. int FD_ISSET(int fd, fd_set *set);  //看其中的一位是否被设定 
  4. void FD_SET(int fd, fd_set *set);   //设定其中的一位 
  5. void FD_ZERO(fd_set *set);      //清零 
  6. 例如: 
  7. fd_set rset, wset; 
  8. int maxfd; 
  9.      
  10. FD_ZERO(&rset);        //不管定义哪一个集合都必须要先清零 
  11. FD_ZERO(&wset);     
  12. //设置读集 
  13. FD_SET(STDIN_FILENO, &rset);     
  14. //设置写集 
  15. FD_SET(STDOUT_FILENO, &wset); 
  16. FD_SET(LOG_FILENO, &wset);     
  17. maxfd = LOG_FILENO + 1;        //必须是FD_SET中最大的描述符 + 1 
  18.  
  19. while(1){ 
  20.     /*每次都必须要先清零*/ 
  21.     FD_ZERO(&rset);         
  22.     FD_ZERO(&wset);     
  23.     /*设置读集*/ 
  24.     FD_SET(STDIN_FILENO, &rset);     
  25.     /*设置写集*/ 
  26.     FD_SET(STDOUT_FILENO, &wset); 
  27.     FD_SET(LOG_FILENO, &wset);    
  28.  
  29.     if ( (ret_no = select(maxfd, &rset, &wset, NULL, NULL)) == -1) && (errno == EINTR)) continue/*若是被信号中断,则自启动,Linux下是自启动的可以不判断。 
  30.     if (num == -1)          //error happened 
  31.         return ntotal; 
  32.     if (FD_ISSET(1, &rset)) {    /*返回时检查三个fd_set,仍然被set的则为ready的fd。看是哪一个准备好了读,或者用循环从1检查到maxfd*/ 
  33.             ... 
  34.         } 
  35.         if (FD_ISSET(1, &wset)) {    
  36.             ... 
  37.         } 
  38.     if (FD_ISSET(LOG_FILENO, &wset)) {    
  39.             ... 
  40.         } 
        
在什么样的情况下描述符准备好?
    .对于读集合(readfds)     中的一个描述符的读操作read不会阻塞,则此描述符准备好。
    .对于写集合(writefds)     中的一个描述符的写操作write不会阻塞,则此描述符准备好。
    .对于异常集合(exceptfds)     中的一个描述符有一个未决异常状态,则此描述符准备好。
 
pselect是select变体。与select不同的是timeout使用timespec结构,而且pselect可以使用可选信号屏蔽字,若sigmask为非空,则使用pselect时以原子操作安装该信号屏蔽字,返回时恢复。
 
3.poll与ppoll:功能类似于select/pselect。与select函数不同,poll函数不时按照文件描述符的类型来组织信息的,而是通过pollfd数组来组织信息。每个数组元素指定一个描述符编号以及一个对其关心的状态(从数字的fds中的event设置)。
 
  1. #include <poll.h> 
  2. int poll( 
  3.     struct pollfd *fds, /* fds是一个pollfd数组,说明关心的描述符和它的状态。*/ 
  4.     nfds_t nfds,        /* nfds    给出要监视的描述符的数目 */ 
  5.     int timeout     /* timeout    是毫秒表示的时间值,是poll没有接受到事件的等待时间。*/ 
  6. ); 
  7.  
  8. int ppoll( 
  9.     struct pollfd *fds,  
  10.     nfds_t nfds,         
  11.     const struct timespec *timeout, 
  12.     const sigset_t *sigmask 
  13. ); 
  14. /* 返回值 
  15.  *       0     超时 
  16.  *       -1    错误 
  17.  *       成功   返回拥有事件的描述符的数目。并从fds数组的revent里获得状态信息!    
  18.  */ 
  19.  
  20. struct pollfd { 
  21.     int   fd;         /* file descriptor */ 
  22.     short events;     /* requested events */ 
  23.     short revents;    /* returned events */ 
  24. }; 
时间参数:
int timeout;
timeout == -1永远等待直到当所指定的fd有一个已经准备好。
timeout == 0 不等待立即返回
timeout > 0  当指定的fd之一已经准备好,或者超过指定的时间返回(超时返回则返回值0)。
 
 
Reference:
APUE Chapter 14
UNP Chapter 6