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; //必須是FD_SET中最大的描述符 + 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; /*若是被信號中斷,則自啓動,Linux下是自啓動的可以不判斷。
- if (num == -1) //error happened
- return ntotal;
- if (FD_ISSET(1, &rset)) { /*返回時檢查三個fd_set,仍然被set的則爲ready的fd。看是哪一個準備好了讀,或者用循環從1檢查到maxfd*/
- ...
- }
- 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, /* fds是一個pollfd數組,說明關心的描述符和它的狀態。*/
- nfds_t nfds, /* nfds 給出要監視的描述符的數目 */
- int timeout /* timeout 是毫秒錶示的時間值,是poll沒有接受到事件的等待時間。*/
- );
- int ppoll(
- struct pollfd *fds,
- nfds_t nfds,
- const struct timespec *timeout,
- const sigset_t *sigmask
- );
- /* 返回值
- * 0 超時
- * -1 錯誤
- * 成功 返回擁有事件的描述符的數目。並從fds數組的revent裏獲得狀態信息!
- */
- struct pollfd {
- int fd; /* file descriptor */
- short events; /* requested events */
- short revents; /* returned events */
- };
時間參數:
int timeout;
timeout == -1永遠等待直到當所指定的fd有一個已經準備好。
timeout == 0 不等待立即返回
timeout > 0 當指定的fd之一已經準備好,或者超過指定的時間返回(超時返回則返回值0)。
Reference:
APUE Chapter 14
UNP Chapter 6