1. 程式人生 > >Linux Poll 使用方法和實現

Linux Poll 使用方法和實現


  1. struct pollfd {  
  2.  int fd;        //檔案描述符
  3.  short events;  //要求查詢的事件掩碼
  4.  short revents; //返回的事件掩碼
  5. };  
  6. int poll(struct pollfd *ufds, unsigned int nfds, int timeout);  

         poll函式使用pollfd型別的結構來監控一組檔案控制代碼,ufds是要監控的檔案控制代碼集合,nfds是監控的檔案控制代碼數量,timeout是等待的毫秒數,這段時間內無論I/O是否準備好,poll都會返回。timeout為負數表示無線等待,timeout為0表示呼叫後立即返回。執行結果:為0表示超時前沒有任何事件發生;-1表示失敗;成功則返回結構體中revents不為0的檔案描述符個數。pollfd結構監控的事件型別如下:

  1. #define POLLIN 0x0001
  2. #define POLLPRI 0x0002
  3. #define POLLOUT 0x0004
  4. #define POLLERR 0x0008
  5. #define POLLHUP 0x0010
  6. #define POLLNVAL 0x0020
  7. #define POLLRDNORM 0x0040
  8. #define POLLRDBAND 0x0080
  9. #define POLLWRNORM 0x0100
  10. #define POLLWRBAND 0x0200
  11. #define POLLMSG 0x0400
  12. #define POLLREMOVE 0x1000
  13. #define POLLRDHUP 0x2000

      如上是events事件掩碼的值域,POLLIN|POLLPRI類似於select的讀事件,POLLOUT|POLLWRBAND類似於select的寫事件。當events屬性為POLLIN|POLLOUT,表示監控是否可讀或可寫。在poll返回時,即可通過檢查revents變數對應的標誌位與events是否相同,比如revents中POLLIN事件標誌位被設定,則表示檔案描述符可以被讀取。程式碼段示例:

  1. int sockfd;             //套接字控制代碼
  2. struct pollfd pollfds;  
  3. int timeout;  
  4. timeout = 5000;  
  5. pollfds.fd = sockfd;                //設定監控sockfd
  6. pollfds.events = POLLIN|POLLPRI;            //設定監控的事件
  7. for(;;){  
  8.     switch(poll(&pollfds,1,timeout)){       //開始監控
  9.     case -1:                    //函式調用出錯
  10.         printf("poll error \r\n"
    );  
  11.     break;  
  12.     case 0:  
  13.         printf("time out \r\n");  
  14.     break;  
  15.     default:                    //得到資料返回
  16.         printf("sockfd have some event \r\n");  
  17.         printf("event value is 0x%x",pollfds.revents);  
  18.     break;  
  19.     }  
  20. }  

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

poll()函式:這個函式是某些Unix系統提供的用於執行與select()函式同等功能的函式,下面是這個函式的宣告:

#include <poll.h>

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

引數說明:

fds:是一個struct pollfd結構型別的陣列,用於存放需要檢測其狀態的Socket描述符;每當呼叫這個函式之後,系統不會清空這個陣列,操作起來比較方便;特別是對於 socket連線比較多的情況下,在一定程度上可以提高處理的效率;這一點與select()函式不同,呼叫select()函式之後,select() 函式會清空它所檢測的socket描述符集合,導致每次呼叫select()之前都必須把socket描述符重新加入到待檢測的集合中;因 此,select()函式適合於只檢測一個socket描述符的情況,而poll()函式適合於大量socket描述符的情況;

nfds:nfds_t型別的引數,用於標記陣列fds中的結構體元素的總數量;

timeout:是poll函式呼叫阻塞的時間,單位:毫秒;

返回值:

>0:陣列fds中準備好讀、寫或出錯狀態的那些socket描述符的總數量;

==0:陣列fds中沒有任何socket描述符準備好讀、寫,或出錯;此時poll超時,超時時間是timeout毫秒;換句話說,如果所檢測的 socket描述符上沒有任何事件發生的話,那麼poll()函式會阻塞timeout所指定的毫秒時間長度之後返回,如果timeout==0,那麼 poll() 函式立即返回而不阻塞,如果timeout==INFTIM,那麼poll() 函式會一直阻塞下去,直到所檢測的socket描述符上的感興趣的事件發 生是才返回,如果感興趣的事件永遠不發生,那麼poll()就會永遠阻塞下去;

-1:  poll函式呼叫失敗,同時會自動設定全域性變數errno;

如果待檢測的socket描述符為負值,則對這個描述符的檢測就會被忽略,也就是不會對成員變數events進行檢測,在events上註冊的事件也會被忽略,poll()函式返回的時候,會把成員變數revents設定為0,表示沒有事件發生;

另外,poll() 函式不會受到socket描述符上的O_NDELAY標記和O_NONBLOCK標記的影響和制約,也就是說,不管socket是阻塞的還是非阻塞 的,poll()函式都不會收到影響;而select()函式則不同,select()函式會受到O_NDELAY標記和O_NONBLOCK標記的影 響,如果socket是阻塞的socket,則呼叫select()跟不呼叫select()時的效果是一樣的,socket仍然是阻塞式TCP通訊,相 反,如果socket是非阻塞的socket,那麼呼叫select()時就可以實現非阻塞式TCP通訊;

所以poll() 函式的功能和返回值的含義與 select() 函式的功能和返回值的含義是完全一樣的,兩者之間的差別就是內部實現方式不一樣,select()函式基本上可以在所有支援檔案描述符操作的系統平臺上運 行(如:Linux 、Unix 、Windows、MacOS等),可移植性好,而poll()函式則只有個別的的作業系統提供支援(如:SunOS、Solaris、AIX、HP提供 支援,但是Linux不提供支援),可移植性差;

strust pollfd結構說明:

typedef struct pollfd {
        int fd;                               /* 需要被檢測或選擇的檔案描述符*/
        short events;                   /* 對檔案描述符fd上感興趣的事件 */
        short revents;                  /* 檔案描述符fd上當前實際發生的事件*/
} pollfd_t;

typedef unsigned long   nfds_t;

經常檢測的事件標記: POLLIN/POLLRDNORM(可讀)、POLLOUT/POLLWRNORM(可寫)、POLLERR(出錯)

如果是對一個描述符上的多個事件感興趣的話,可以把這些常量標記之間進行按位或運算就可以了;

比如:對socket描述符fd上的讀、寫、異常事件感興趣,就可以這樣做:struct pollfd  fds;

fds[nIndex].events=POLLIN | POLLOUT | POLLERR;

當 poll()函式返回時,要判斷所檢測的socket描述符上發生的事件,可以這樣做: struct pollfd  fds;

檢測可讀TCP連線請求:

if((fds[nIndex].revents & POLLIN) == POLLIN){//接收資料/呼叫accept()接收連線請求}

檢測可寫:

if((fds[nIndex].revents & POLLOUT) == POLLOUT){//傳送資料}

檢測異常:

if((fds[nIndex].revents & POLLERR) == POLLERR){//異常處理}

 每一個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
可用記憶體不足,無法完成請求。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Poll就是監控檔案是否可讀的一種機制,作用與select一樣。

應用程式的呼叫函式如下:

int poll(struct pollfd *fds,nfds_t nfds, int timeout);

Poll機制會判斷fds中的檔案是否可讀,如果可讀則會立即返回,返回的值就是可讀fd的數量,如果不可讀,那麼就程序就會休眠timeout這麼長的時間,然後再來判斷是否有檔案可讀,如果有,返回fd的數量,如果沒有,則返回0. 

核心實現流程:

當應用程式呼叫poll函式的時候,會呼叫到系統呼叫sys_poll函式,該函式最終呼叫do_poll函式,do_poll函式中有一個死循 環,

在裡面又會利用do_pollfd函式去呼叫驅動中的poll函式(fds中每個成員的字元驅動程式都會被掃描到),驅動程式中的Poll函式的工作 有兩個,

一個就是呼叫poll_wait 函式,把程序掛到等待佇列中去(這個是必須的,你要睡眠,必須要在一個等待佇列上面,否則到哪裡去喚醒你呢??),

另一個是確定相關的fd是否有內容可 讀,如果可讀,就返回1,否則返回0,如果返回1 ,do_poll函式中的count++,    

然後  do_poll函式然後判斷三個條件(if (count ||!timeout || signal_pending(current)))如果成立就直接跳出,如果不成立,

就睡眠timeout個jiffes這麼長的時間(呼叫schedule_timeout實現睡眠),如果在這段時間內沒有其他程序去喚醒它,

那麼第二次執行判斷的時候就會跳出死迴圈。如果在這段時間內有其他程序喚醒它,那麼也可以跳出死迴圈返回

(例如我們可以利用中斷處理函式去喚醒它,這樣的話一有資料可讀,就可以讓它立即返回)。

1.      sys_poll函式位於fs/select.c檔案中,程式碼如下:

asmlinkagelong sys_poll(struct pollfd __user *ufds, unsigned int nfds, long timeout_msecs)

{        

 s64 timeout_jiffies;

         if (timeout_msecs > 0) {

#ifHZ > 1000

             /* We can only overflow if HZ >1000 */

             if (timeout_msecs / 1000 >(s64)0x7fffffffffffffffULL / (s64)HZ)

                 timeout_jiffies = -1;

             else

#endif

                 timeout_jiffies =msecs_to_jiffies(timeout_msecs);

         } 

else

 {

             /* Infinite (< 0) or no (0)timeout */

             timeout_jiffies = timeout_msecs;

         } 

         return do_sys_poll(ufds,nfds, &timeout_jiffies);

}

它對超時引數稍作處理後,直接呼叫do_sys_poll。 

2.      do_sys_poll函式也位於位於fs/select.c檔案中,我們忽略其他程式碼:

intdo_sys_poll(struct pollfd __user *ufds, unsigned int nfds, s64 *timeout)

{

……

poll_initwait(&table);

……

         fdcount = do_poll(nfds, head,&table, timeout);

……

poll_initwait函式非常簡單,它初始化一個poll_wqueues變數table:

poll_initwait> init_poll_funcptr(&pwq->pt, __pollwait); > pt->qproc = qproc;

即table->pt->qproc= __pollwait,__pollwait將在驅動的poll函式裡用到。 

3.      do_sys_poll函式位於fs/select.c檔案中,程式碼如下:

static int do_poll(unsigned int nfds,  struct poll_list *list,   struct poll_wqueues *wait, s64 *timeout)

{

01 ……

02   for (;;){

03 ……

04                   if(do_pollfd(pfd, pt)) {

05                           count++;

06                           pt = NULL;

07                   }

08 ……

09       if(count || !*timeout || signal_pending(current))

10           break;

11       count= wait->error;

12       if(count)

13           break;14

15       if(*timeout < 0) {

16           /*Wait indefinitely */

17           __timeout= MAX_SCHEDULE_TIMEOUT;

18       }else if (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT-1)) {

19           /*

20           * Wait for longer than MAX_SCHEDULE_TIMEOUT. Do it in

21           * a loop

22           */

23           __timeout= MAX_SCHEDULE_TIMEOUT - 1;

24           *timeout-= __timeout;

25       }else {

26           __timeout= *timeout;

27           *timeout= 0;

28       }29

30       __timeout= schedule_timeout(__timeout); // 休眠時間由應用提供

31       if(*timeout >= 0)

32           *timeout+= __timeout;

33   }

34   __set_current_state(TASK_RUNNING);

35   returncount;

36 } 

分析其中的程式碼,可以發現,它的作用如下:

①    從02行可以知道,這是個迴圈,它退出的條件為:

a.      09行的3個條件之一(count非0,超時、有訊號等待處理)

count順0表示04行的do_pollfd至少有一個成功。

b.      11、12行:發生錯誤

②    重點在do_pollfd函式,後面再分析

③    第30行,讓本程序休眠一段時間,注意:應用程式執行poll呼叫後,如果①②的條件不滿足,程序就會進入休眠。那麼,誰喚醒呢?除了休眠到指定時間被系統喚醒外,還可以被驅動程式喚醒──記住這點,這就是為什麼驅動的poll裡要呼叫poll_wait的原因,後面分析。 

4.      do_pollfd函式位於fs/select.c檔案中,程式碼如下:

static inline unsigned int do_pollfd(struct pollfd*pollfd, poll_table *pwait)

{

……

             if(file->f_op && file->f_op->poll)

                     mask= file->f_op->poll(file, pwait);

……

可見,它就是呼叫我們的驅動程式裡註冊的poll函式。 

二、驅動程式:

驅動程式裡與poll相關的地方有兩處:一是構造file_operation結構時,要定義自己的poll函式。二是通過poll_wait來呼叫上面說到的__pollwait函式,pollwait的程式碼如下:

staticinline void poll_wait(struct file * filp, wait_queue_head_t * wait_address,poll_table *p)

{

         if (p && wait_address)

             p->qproc(filp, wait_address, p);

}

p->qproc就是__pollwait函式,從它的程式碼可知,它只是把當前程序掛入我們驅動程式裡定義的一個佇列裡而已。它的程式碼如下:

staticvoid __pollwait(struct file *filp, wait_queue_head_t *wait_address,  poll_table *p)

{

         struct poll_table_entry *entry =poll_get_entry(p);

         if (!entry)

             return;

         get_file(filp);

         entry->filp = filp;

         entry->wait_address = wait_address;

         init_waitqueue_entry(&entry->wait,current);

         add_wait_queue(wait_address,&entry->wait);

執行到驅動程式的poll_wait函式時,程序並沒有休眠,我們的驅動程式裡實現的poll函式是不會引起休眠的。讓程序進入休眠,是前面分析的do_sys_poll函式的30行“__timeout = schedule_timeout(__timeout)”。

poll_wait只是把本程序掛入某個佇列,應用程式呼叫poll > sys_poll> do_sys_poll > poll_initwait,do_poll > do_pollfd > 我們自己寫的poll函式後,再呼叫schedule_timeout進入休眠。如果我們的驅動程式發現情況就緒,可以把這個佇列上掛著的程序喚醒。可見,poll_wait的作用,只是為了讓驅動程式能找到要喚醒的程序。即使不用poll_wait,我們的程式也有機會被喚醒:chedule_timeout(__timeout),只是休眠__time_out這段時間。

現在來總結一下poll機制:

1. poll > sys_poll > do_sys_poll >poll_initwait,poll_initwait函式註冊一下回調函式__pollwait,它就是我們的驅動程式執行poll_wait時,真正被呼叫的函式。 

2. 接下來執行file->f_op->poll,即我們驅動程式裡自己實現的poll函式

   它會呼叫poll_wait把自己掛入某個佇列,這個佇列也是我們的驅動自己定義的;

   它還判斷一下裝置是否就緒。 

3. 如果裝置未就緒,do_sys_poll裡會讓程序休眠一定時間,這個時間是應用提供的“超時時間” 

4. 程序被喚醒的條件有2:一是上面說的“一定時間”到了,二是被驅動程式喚醒。驅動程式發現條件就緒時,就把“某個佇列”上掛著的程序喚醒,這個佇列,就是前面通過poll_wait把本程序掛過去的佇列。 

5. 如果驅動程式沒有去喚醒程序,那麼chedule_timeout(__timeou)超時後,會重複2、3動作1次,直到應用程式給定的時間, 然後返回。

驅動的poll函式編寫模板如下:

static DECLARE_WAIT_QUEUE_HEAD(my_waitq);  //休眠要掛的等待佇列

static unsigned drv_poll(struct file *file, poll_table *wait)
{
unsigned int mask = 0;
poll_wait(file, &my_waitq, wait); // 不會立即休眠
if (有資料)
mask |= POLLIN | POLLRDNORM;
return mask;
}