I/O複用

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章