1. 程式人生 > >select與poll的區別及使用

select與poll的區別及使用

一、select, poll的區別

select()系統呼叫提供一個機制來實現同步多元I/O:


#include <sys/time.h>
#include <sys/types.h>
#include <unistd.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);


呼叫select()將阻塞,直到指定的檔案描述符準備好執行I/O,或者可選引數timeout指定的時間已經過去。
監視的檔案描述符分為三類set,每一種對應等待不同的事件。readfds中列出的檔案描述符被監視是否有資料可供讀取(如果讀取操作完成則不會阻塞)。writefds中列出的檔案描述符則被監視是否寫入操作完成而不阻塞。最後,exceptfds中列出的檔案描述符則被監視是否發生異常,或者無法控制的資料是否可用(這些狀態僅僅應用於套接字)。這三類set可以是NULL,這種情況下select()不監視這一類事件。
select()成功返回時,每組set都被修改以使它只包含準備好I/O的檔案描述符。例如,假設有兩個檔案描述符,值分別是7和9,被放在readfds中。當select()返回時,如果7仍然在set中,則這個檔案描述符已經準備好被讀取而不會阻塞。如果9已經不在set中,則讀取它將可能會阻塞(我說可能是因為資料可能正好在select返回後就可用,這種情況下,下一次呼叫select()將返回檔案描述符準備好讀取)。
第一個引數n,等於所有set中最大的那個檔案描述符的值加1。因此,select()的呼叫者負責檢查哪個檔案描述符擁有最大值,並且把這個值加1再傳遞給第一個引數。
timeout引數是一個指向timeval結構體的指標,timeval定義如下:

#include <sys/time.h>
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* 10E-6 second */
};


如果這個引數不是NULL,則即使沒有檔案描述符準備好I/O,select()也會在經過tv_sec秒和tv_usec微秒後返回。當select()返回時,timeout引數的狀態在不同的系統中是未定義的,因此每次呼叫select()之前必須重新初始化timeout和檔案描述符set。實際上,當前版本的Linux會自動修改timeout引數,設定它的值為剩餘時間。因此,如果timeout被設定為5秒,然後在檔案描述符準備好之前經過了3秒,則這一次呼叫select()返回時tv_sec將變為2。
如果timeout中的兩個值都設定為0,則呼叫select()將立即返回,報告呼叫時所有未決的事件,但不等待任何隨後的事件。
檔案描述符set不會直接操作,一般使用幾個助手巨集來管理。這允許Unix系統以自己喜歡的方式來實現檔案描述符set。但大多數系統都簡單地實現set為位陣列。FD_ZERO移除指定set中的所有檔案描述符。每一次呼叫select()之前都應該先呼叫它。
fd_set writefds;
FD_ZERO(&writefds);

FD_SET新增一個檔案描述符到指定的set中,FD_CLR則從指定的set中移除一個檔案描述符:
FD_SET(fd, &writefds); /* add 'fd' to the set */
FD_CLR(fd, &writefds); /* oops, remove 'fd' from the set */

設計良好的程式碼應該永遠不使用FD_CLR,而且實際情況中它也確實很少被使用。
FD_ISSET測試一個檔案描述符是否指定set的一部分。如果檔案描述符在set中則返回一個非0整數,不在則返回0。FD_ISSET在呼叫select()返回之後使用,測試指定的檔案描述符是否準備好相關動作:
if (FD_ISSET(fd, &readfds))
/* 'fd' is readable without blocking! */

因為檔案描述符set是靜態建立的,它們對檔案描述符的最大數目強加了一個限制,能夠放進set中的最大檔案描述符的值由FD_SETSIZE指定。在Linux中,這個值是1024。本章後面我們還將看到這個限制的衍生物。
返回值和錯誤程式碼
select()成功時返回準備好I/O的檔案描述符數目,包括所有三個set。如果提供了timeout,返回值可能是0;錯誤時返回-1,並且設定errno為下面幾個值之一:
EBADF
給某個set提供了無效檔案描述符。
EINTR
等待時捕獲到訊號,可以重新發起呼叫。
EINVAL
引數n為負數,或者指定的timeout非法。
ENOMEM
不夠可用記憶體來完成請求。
--------------------------------------------------------------------------------------------------------------

poll()系統呼叫是System V的多元I/O解決方案。它解決了select()的幾個不足,儘管select()仍然經常使用(多數還是出於習慣,或者打著可移植的名義):

#include <sys/poll.h>
int poll (struct pollfd *fds, unsigned int nfds, int timeout);


和select()不一樣,poll()沒有使用低效的三個基於位的檔案描述符set,而是採用了一個單獨的結構體pollfd陣列,由fds指標指向這個組。pollfd結構體定義如下:

#include <sys/poll.h>

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};


每一個pollfd結構體指定了一個被監視的檔案描述符,可以傳遞多個結構體,指示poll()監視多個檔案描述符。每個結構體的events域是監視該檔案描述符的事件掩碼,由使用者來設定這個域。revents域是檔案描述符的操作結果事件掩碼。核心在呼叫返回時設定這個域。events域中請求的任何事件都可能在revents域中返回。合法的事件如下:
POLLIN
有資料可讀。
POLLRDNORM
有普通資料可讀。
POLLRDBAND
有優先資料可讀。
POLLPRI
有緊迫資料可讀。
POLLOUT
寫資料不會導致阻塞。
POLLWRNORM
寫普通資料不會導致阻塞。
POLLWRBAND
寫優先資料不會導致阻塞。
POLLMSG
SIGPOLL訊息可用。

此外,revents域中還可能返回下列事件:
POLLER
指定的檔案描述符發生錯誤。
POLLHUP
指定的檔案描述符掛起事件。
POLLNVAL
指定的檔案描述符非法。

這些事件在events域中無意義,因為它們在合適的時候總是會從revents中返回。使用poll()和select()不一樣,你不需要顯式地請求異常情況報告。
POLLIN | POLLPRI等價於select()的讀事件,POLLOUT | POLLWRBAND等價於select()的寫事件。POLLIN等價於POLLRDNORM | POLLRDBAND,而POLLOUT則等價於POLLWRNORM。
例如,要同時監視一個檔案描述符是否可讀和可寫,我們可以設定events為POLLIN | POLLOUT。在poll返回時,我們可以檢查revents中的標誌,對應於檔案描述符請求的events結構體。如果POLLIN事件被設定,則檔案描述符可以被讀取而不阻塞。如果POLLOUT被設定,則檔案描述符可以寫入而不導致阻塞。這些標誌並不是互斥的:它們可能被同時設定,表示這個檔案描述符的讀取和寫入操作都會正常返回而不阻塞。
timeout引數指定等待的毫秒數,無論I/O是否準備好,poll都會返回。timeout指定為負數值表示無限超時;timeout為0指示poll呼叫立即返回並列出準備好I/O的檔案描述符,但並不等待其它的事件。這種情況下,poll()就像它的名字那樣,一旦選舉出來,立即返回。
返回值和錯誤程式碼
成功時,poll()返回結構體中revents域不為0的檔案描述符個數;如果在超時前沒有任何事件發生,poll()返回0;失敗時,poll()返回-1,並設定errno為下列值之一:
EBADF
一個或多個結構體中指定的檔案描述符無效。
EFAULT
fds指標指向的地址超出程序的地址空間。
EINTR
請求的事件之前產生一個訊號,呼叫可以重新發起。
EINVAL
nfds引數超出PLIMIT_NOFILE值。
ENOMEM
可用記憶體不足,無法完成請求。
--------------------------------------------------------------------------------------------------------------
以上內容來自《OReilly.Linux.System.Programming - Talking.Directly.to.the.Kernel.and.C.Library.2007》
--------------------------------------------------------------------------------------------------------------


二、select poll使用
 

如何管理多個連線?
“我想同時監控一個以上的檔案描述符(fd)/連線(connection)/流(stream),應該怎麼辦?” 

使用 select() 或 poll() 函式。 

注意:select() 在BSD中被引入,而poll()是SysV STREAM流控制的產物。因此,這裡就有了平臺移植上的考慮:純粹的BSD系統可能仍然缺少poll(),而早一些的SVR3系統中可能沒有select(),儘管在SVR4中將其加入。目前兩者都是POSIX. 1g標準,(譯者注:因此在Linux上兩者都存在) 

select()和poll()本質上來講做的是同一件事,只是完成的方法不一樣。兩者都通過檢驗一組檔案描述符來檢測是否有特定的時間將在上面發生並在一定的時間內等待其發生。 

[重要事項:無論select()還是poll()都不對普通檔案起很大效用,它們著重用於套介面(socket)、管道(pipe)、偽終端(pty)、終端裝置(tty)和其他一些字元裝置,但是這些操作都是系統相關(system-dependent)的。] 

2.1.1. 我如何使用select()函式?
select()函式的介面主要是建立在一種叫'fd_set'型別的基礎上。它('fd_set') 是一組檔案描述符(fd)的集合。由於fd_set型別的長度在不同平臺上不同,因此應該用一組標準的巨集定義來處理此類變數: 

fd_set set;
FD_ZERO(&set);       /* 將set清零 */
FD_SET(fd, &set);    /* 將fd加入set */
FD_CLR(fd, &set);    /* 將fd從set中清除 */
FD_ISSET(fd, &set);  /* 如果fd在set中則真 */

在 過去,一個fd_set通常只能包含少於等於32個檔案描述符,因為fd_set其實只用了一個int的位元向量來實現,在大多數情況下,檢查 fd_set能包括任意值的檔案描述符是系統的責任,但確定你的fd_set到底能放多少有時你應該檢查/修改巨集FD_SETSIZE的值。*這個值是系統相關的*,同時檢查你的系統中的select() 的man手冊。有一些系統對多於1024個檔案描述符的支援有問題。[譯者注: Linux就是這樣的系統!你會發現sizeof(fd_set)的結果是128(*8 = FD_SETSIZE=1024) 儘管很少你會遇到這種情況。] 

select的基本介面十分簡單: 

int select(int nfds, fd_set *readset, fd_set *writeset,
fd_set *exceptset, struct timeval *timeout);

其中: 

nfds     
需要檢查的檔案描述符個數,數值應該比是三組fd_set中最大數
更大,而不是實際檔案描述符的總數。
readset    
用來檢查可讀性的一組檔案描述符。
writeset
用來檢查可寫性的一組檔案描述符。
exceptset
用來檢查意外狀態的檔案描述符。(注:錯誤並不是意外狀態)
timeout
NULL指標代表無限等待,否則是指向timeval結構的指標,代表最
長等待時間。(如果其中tv_sec和tv_usec都等於0, 則檔案描述符
的狀態不被影響,但函式並不掛起)

函式將返回響應操作的對應操作檔案描述符的總數,且三組資料均在恰當位置被修改,只有響應操作的那一些沒有修改。接著應該用FD_ISSET巨集來查詢返回的檔案描述符組。 

這裡是一個簡單的測試單個檔案描述符可讀性的例子: 

int isready(int fd)
{
int rc;
fd_set fds;
struct timeval tv;

FD_ZERO(&fds);
FD_SET(fd,&fds);
tv.tv_sec = tv.tv_usec = 0;

rc = select(fd+1, &fds, NULL, NULL, &tv);
if (rc < 0)
return -1;

return FD_ISSET(fd,&fds) ? 1 : 0;
}

當然如果我們把NULL指標作為fd_set傳入的話,這就表示我們對這種操作的發生不感興趣,但select() 還是會等待直到其發生或者超過等待時間。 

[譯者注:在Linux中,timeout指的是程式在非sleep狀態中度過的時間,而不是實際上過去的時間,這就會引起和非Linux平臺移植上的時間不等問題。移植問題還包括在System V風格中select()在函式退出前會把timeout設為未定義的 NULL狀態,而在BSD中則不是這樣, Linux在這點上遵從System V,因此在重複利用timeout指標問題上也應該注意。] 

2.1.2. 我如何使用poll()?
poll ()接受一個指向結構'struct pollfd'列表的指標,其中包括了你想測試的檔案描述符和事件。事件由一個在結構中事件域的位元掩碼確定。當前的結構在呼叫後將被填寫並在事件發生後返回。在SVR4(可能更早的一些版本)中的 "poll.h"檔案中包含了用於確定事件的一些巨集定義。事件的等待時間精確到毫秒 (但令人困惑的是等待時間的型別卻是int),當等待時間為0時,poll()函式立即返回,-1則使poll()一直掛起直到一個指定事件發生。下面是pollfd的結構。 

struct pollfd {
int fd;        /* 檔案描述符 */
short events;  /* 等待的事件 */
short revents; /* 實際發生了的事件 */
};

於select()十分相似,當返回正值時,代表滿足響應事件的檔案描述符的個數,如果返回0則代表在規定事件內沒有事件發生。如發現返回為負則應該立即檢視 errno,因為這代表有錯誤發生。 

如果沒有事件發生,revents會被清空,所以你不必多此一舉。 

這裡是一個例子 

/* 檢測兩個檔案描述符,分別為一般資料和高優先資料。如果事件發生
則用相關描述符和優先度呼叫函式handler(),無時間限制等待,直到
錯誤發生或描述符掛起。*/

#include <stdlib.h>
#include <stdio.h>

#include <sys/types.h>
#include <stropts.h>
#include <poll.h>

#include <unistd.h>
#include <errno.h>
#include <string.h>

#define NORMAL_DATA 1
#define HIPRI_DATA 2

int poll_two_normal(int fd1,int fd2)
{
struct pollfd poll_list[2];
int retval;

poll_list[0].fd = fd1;
poll_list[1].fd = fd2;
poll_list[0].events = POLLIN|POLLPRI;
poll_list[1].events = POLLIN|POLLPRI;

while(1)
{
retval = poll(poll_list,(unsigned long)2,-1);
/* retval 總是大於0或為-1,因為我們在阻塞中工作 */

if(retval < 0)
{
fprintf(stderr,"poll錯誤: %s\n",strerror(errno));
return -1;
}

if(((poll_list[0].revents&POLLHUP) == POLLHUP) ||
((poll_list[0].revents&POLLERR) == POLLERR) ||
((poll_list[0].revents&POLLNVAL) == POLLNVAL) ||
((poll_list[1].revents&POLLHUP) == POLLHUP) ||
((poll_list[1].revents&POLLERR) == POLLERR) ||
((poll_list[1].revents&POLLNVAL) == POLLNVAL))
return 0;

if((poll_list[0].revents&POLLIN) == POLLIN)
handle(poll_list[0].fd,NORMAL_DATA);
if((poll_list[0].revents&POLLPRI) == POLLPRI)
handle(poll_list[0].fd,HIPRI_DATA);
if((poll_list[1].revents&POLLIN) == POLLIN)
handle(poll_list[1].fd,NORMAL_DATA);
if((poll_list[1].revents&POLLPRI) == POLLPRI)
handle(poll_list[1].fd,HIPRI_DATA);
}
}

2.1.3. 我是否可以同時使用SysV IPC和select()/poll()?
*不能。* (除非在AIX上,因為它用一個無比奇怪的方法來實現這種組合) 

一般來說,同時使用select()或poll()和SysV 訊息佇列會帶來許多麻煩。SysV IPC的物件並不是用檔案描述符來處理的,所以它們不能被傳遞給select()和 poll()。這裡有幾種解決方法,其粗暴程度各不相同: 


完全放棄使用SysV IPC。 :-)

用fork(),然後讓子程序來處理SysV IPC,然後用管道或套介面和父程序 說話。父程序則使用select()。 

同上,但讓子程序用select(),然後和父親用訊息佇列交流。 

安排程序傳送訊息給你,在傳送訊息後再發送一個訊號。*警告*:要做好 這個並不簡單,非常容易寫出會丟失訊息或引起死鎖的程式。 

另外還有其他方法。