轉載:http://blog.csdn.net/u012432778/article/details/47347133
概述
Linux提供了三種 I/O 多路復用方案:select,poll和epoll。在這一篇博客里先討論select, poll 在將下一篇中介紹,epoll是Linux特有的高級解決方案,將在接下來中介紹。
select()
select()系統調用提供了一種實現同步 I/O 多路復用的機制:
? ? ? ? ?#include <sys/select.h>
? ? ? ? ?int select (int n,
? ? ? ? ? ? ? ? ? ? ? ? ??fd_set *readfds,
? ? ? ? ? ? ? ? ? ? ? ? ??fd_set *writefds,
? ? ? ? ? ? ? ? ? ? ? ? ??fd_set *exceptfds,
? ? ? ? ? ? ? ? ? ? ? ? ??struct timeval *timeout);
? ? ? ? ?FD_CLR(int fd, fd_set *set);
? ? ? ? ?FD_ISSET(int fd, fd_set *set);
? ? ? ? ?FD_SET(int fd, fd_set *set);
? ? ? ? ?FD_ZERO(fd_set *set);
在給定的文件描述符 I/O 就緒之前并且還沒有超出指定的時間限制,select()調用就會阻塞。
監視的文件描述符可以分為3類,分別等待不同的事件。對于 readfds 集中的文件描述符,監視是否有數據可讀(即某個讀操作是否可以無阻塞完成);對于 writefds 集中的文件描述符,監視是否有某個寫操作可以無阻塞完成;對于 exceptfds 中的文件描述符,監視是否有發生異常,或者出現帶外 (out-of-band) 數據(這些場景只適用于socket)。指定的集合可能是NULL,在這種情況下,select() 不會監視該事件。
成功返回時,每個集合都修改成只包含相應類型的 I/O 就緒的文件描述符。舉個例子,假定 readfds 集中有兩個文件描述符 7 和 9。當調用返回時,如果描述符 7 還在集合中,它在 I/O 讀取時不會阻塞。如果描述符 9 不在集合中,它在讀取時很可能會發生阻塞。(這里說的是“很可能”是因為在調用完成后,數據可能已經就緒了。在這種場景下,下一次調用 select() 就會返回描述符可用。)
第一個參數 n,其值等于所有集合中文件描述符的最大值加 1 。因此,select() 調用負責檢查哪個文件描述符值最大,將該最大值 加 1 后傳給第一個參數。
參數 timeout 是指向 timeval 結構體的指針,定義如下:
? ? ? ? #include <sys/time.h>
? ? ? ? struct timeval {
? ? ? ? ? ? ? ?long tv_sec; ? ? ? ? ? ??/* seconds */
? ? ? ? ? ? ? ?long tv_usec; ? ? ? ? ??/* microseconds */
? ? ? ? };
如果該參數不是NULL,在 tv_sec 秒 tv_usec 微妙后。select() 調用會返回,即使沒有一個文件描述符處于 I/O 就緒狀態。返回時,在不同的UNIX系統中,該結構體是未定義的,因此每次調用必須(和文件描述符集一起)重新初始化。實際上,當前Linux版本會自動修改該參數,把值修改成剩余的時間。因此,如果超時設置是 5 秒,在文件描述符可用之前已逝去了 3 秒,那么在調用返回時,tv.tv_sec 的值就是 2。
如果超時值都是設置成 0,調用會立即返回,調用時報告所有事件都掛起,而不會等待任何后續事件。
不是直接操作文件描述符集,而是通過輔助宏來管理。通過這種方式,UNIX系統可以按照所希望的方式來實現。不過,大多數系統把集合實現成位數組。
FD_ZERO 從指定集合中刪除所有的文件描述符。每次調用 select() 之前,都應該調用該宏。
? ? ? ?fd_set writefds;
? ? ? ?FD_ZERO(&writefds);
FD_SET 向指定集中添加一個文件描述符,而 FD_CLR 則從指定集中刪除一個文件描述符。
? ? ? ?FD_SET(fd, &writefds); ? ? ? ? ? /* add 'fd' to the set */
? ? ? ?FD_CLR(fd, &writefds); ? ? ? ? ? /* oops, remove 'fd' from the set */
設計良好的代碼應該不需要使用FD_CLR,極少使用該宏。
FD_ISSET 檢查一個文件描述符是否在給定集合中。如果在,則返回非0值,否則返回 0。當select() 調用返回時,會通過 FD_ISSET 來檢查文件描述符是否就緒:
? ? ? ?if (FD_ISSET(fd, &readfds))?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?/* 'fd' is readable without blocking */
由于文件描述符集是靜態建立的,所以文件描述符數存在上限值,而且存在最大文件描述符值,這兩個值都是由 FD_SETSIZE 設置。在Linux,該值是1024。
返回值和錯誤碼
select() 調用成功時,返回三個集合中 I/O 就緒的文件描述符總數。如果給出了超時設置,返回值可能是 0 。出錯時。返回 -1 ,并把 errno 值設置成如下值之一:
EBADF ? ? ? ?某個集合中存在非法文件描述符。
EINTR ? ? ? ?等待時捕獲了一個信號,可以重新發起調用。
EINVAL ? ? ?參數 n 是負數,或者設置的超時時間值非法。
ENOMEN ? ?沒有足夠的內存來完成該請求
select() 示例
用select() 實現可移植的sleep功能
在各個Unix系統中,相比微秒級的sleep功能,對select() 的實現更普遍,因此select() 調用常常被作為可移植的sleep實現機制:把所有三個集都設置成NULL,超時值設置為非NULL。如下:
? ? ? ?struct timeval tv;
? ? ? ?tv.tv_sec = 0;
? ? ? ?tv.tv_usec = 500;
? ? ? ?/* sleep for 500 microseconds */
? ? ? ?select(0, NULL, NULL, NULL, &tv);
Linux提供了更高精度的sleep機制。
pselect()
select()系統調用很流行,它最初是在 4.2BSD 中引入的,但是 POSIX 標準在 POSIX 1003.1g-2000 和后來的 POSIX 1003.1-2001中定義了自己的 pselect() 方法:
? ? ? ?#define _XOPEN_SOURCE 600
? ? ? ?#include <sys/select.h>
? ? ? ?int pselect(int n,
? ? ? ? ? ? ? ? ? ? ? ? ?fd_set *readfds,
? ? ? ? ? ? ? ? ? ? ? ? ?fd_set *writefds,
? ? ? ? ? ? ? ? ? ? ? ? ?fd_set *exceptfds,
? ? ? ? ? ? ? ? ? ? ? ? ?const struct timespec *timeout,
? ? ? ? ? ? ? ? ? ? ? ? ?const sigset_t *sigmask);
? ? ? ?/* these are the same as those used by select() *
? ? ? ?FD_CLR(int fd, fd_set *set);
? ? ? ?FD_ISSET(int fd, fd_set *set);
? ? ? ?FD_SET(int fd, fd_set *set);
? ? ? ?FD_ZERO(fd_set *set);
pselect() 和 select() 存在三點區別:
- pselect() 的timeout 參數使用了timespec 結構體,而不是 timeval 結構體。timespec 結構體使用秒和納秒,而不是毫秒,從理論上講更精確些。但是實際上,這兩個結構體在毫秒精度上已經不可靠了。
- pselect() 調用不會修改 timeout 參數。因此,在后續調用中,不需要重新初始化該參數。
- select() 系統調用沒有sigmask參數。當這個參數設置為NULL時,pselect() 的行為和select() 相同。
timespec 結構體定義如下:
? ? ? #include <sys/time.h>
? ? ? struct timespec {
? ? ? ? ? ? ?long tv_sec; ? ? ? ? /* seconds */
? ? ? ? ? ? ?long tv_nsec; ? ? ? /* nanoseconds */
? ? ? };
把pselect() 添加到UNIX工具箱主要原因是為了增加 sigmask 參數,該參數是為了解決文件描述符和信號之間等待出現競爭條件。假設信號處理程序設置了全局標志位(大部分如此),進程每次調用 select() 之前會檢查該標志位。現在,假定在檢查標志位和調用之間收到信號,應用可能會一直阻塞,永遠都不會響應該信號。pselect() 提供了一組可阻塞信號,應用在調用時可以設置這些信號來解決這個問題。阻塞的信號要等到解除阻塞才會處理。一旦 pselect() 返回,內核會恢復老的信號掩碼。
在Linux內核2.6.16之前,pselect() 還不是系統調用,而是由 glibc 提供的對 select() 調用的簡單封裝。該封裝對出現競爭的風險最小化,但是沒有完全的消除競爭。當真正引入了新的系統調用pselect() 之后,才徹底解決了這個競爭問題。
雖然和 select() 相比,pselect() 有一定的改進,但大多數應用還是使用 select(),有的是出于習慣,有的是為了更好的可移植性。