linux下select 和 poll的用法

來源:互聯網
上載者:User

系統調用select和poll的後端實現,用這兩個系統調用來查詢裝置是否可讀寫,或是否處於某種狀態。對於裝置驅動的編寫者來說,我們經常要告訴應用程式裝置的狀態,也就是說經常要告訴應用程式我們是否有資料已經準備好了。Linux在這個問題上面沒有windows處理得好,它得訊息處理機制並不完善,應用程式通常只能使用read\write\ioctl等方法調用進入驅動程式,如果驅動程式沒有資料就阻塞進程,否則就返回相應得資料。但是,如果應用程式同時要服務多個硬體,就不能被一個裝置掛起。這就要實現為非同步得方式,有資料的返回資料,沒有資料就返回0或者-1等方法,然後應用程式就不斷得讀取來對硬體進行操作。這樣的話,應用程式就每隔一段時間去讀各個檔案,然後判斷是否有資料,如果沒有就繼續sleep。這樣寫得程式結構不清晰,而且反覆的sleep會使得系統效率降低。

select()函數的作用
調用select()將阻塞,直到指定的檔案描述符準備好執行I/O,或者選擇性參數timeout指定的時間已經過去。監視的檔案描述符分為三類set,每一種對應等待不同的事件。readfds中列出的檔案描述符被監視是否有資料可供讀取(如果讀取操作完成則不會阻塞)。writefds中列出的檔案描述符則被監視是否寫入操作完成而不阻塞。最後,exceptfds中列出的檔案描述符則被監視是否發生異常,或者無法控制的資料是否可用(這些狀態僅僅應用於通訊端)。這三類set可以是NULL,這種情況下select()不監視這一類事件。select()成功返回時,每組set都被修改以使它只包含準備好I/O的檔案描述符。由於fd_set類型的長度在不同平台上不同,因此應該用一組標準的宏定義來處理此類變數:

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宏來尋找返回的檔案描述符組。第一個參數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 theset */

設計良好的代碼應該永遠不使用FD_CLR,而且實際情況中它也確實很少被使用。

FD_ISSET測試一個檔案描述符是否指定set的一部分。如果檔案描述符在set中則返回一個非0整數,不在則返回0。FD_ISSET在調用select()返回之後使用,測試指定的檔案描述符是否準備好相關動作:

if (FD_ISSET(fd, &readfds))

因為檔案描述符set是靜態建立的,它們對檔案描述符的最大數目強加了一個限制,能夠放進set中的最大檔案描述符的值由FD_SETSIZE指定。在Linux中,這個值是1024。本章後面我們還將看到這個限制的衍生物。

傳回值和錯誤碼

select()成功時返回準備好I/O的檔案描述符數目,包括所有三個set。如果提供了timeout,傳回值可能是0;錯誤時返回-1,並且設定errno為下面幾個值之一:

EBADF

給某個set提供了無效檔案描述符。

EINTR

等待時捕獲到訊號,可以重新發起調用。

EINVAL

參數n為負數,或者指定的timeout非法。

ENOMEM

不夠可用記憶體來完成請求。

 

Poll()函數的作用

#include <sys/poll.h>

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

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

#include <sys/poll.h>

struct pollfd {

int fd; /* file descriptor */

short events; /* requested events towatch */

short revents; /* returned eventswitnessed */

};

每一個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原理類似,效能上也不存在明顯差異,但select對所監控的檔案描述符數量有限制。poll是一個系統調用,其核心入口函數為sys_poll,sys_poll幾乎不做任何處理直接調用do_sys_poll,do_sys_poll的執行過程可以分為三個部分

1,將使用者傳入的pollfd數組拷貝到核心空間,因為拷貝操作和數組長度相關,時間上這是一個O(n)操作,這一步的代碼在do_sys_poll中包括從函數開始到調用do_poll前的部分。

2,查詢每個檔案描述符對應裝置的狀態,如果該裝置尚未就緒,則在該裝置的等待隊列中加入一項並繼續查詢下一裝置的狀態。查詢完所有裝置後如果沒有一個裝置就緒,這時則需要掛起當前進程等待,直到裝置就緒或者逾時,掛起操作是通過調用schedule_timeout執行的。裝置就緒後進程被通知繼續運行,這時再次遍曆所有裝置,以尋找就緒裝置。這一步因為兩次遍曆所有裝置,時間複雜度也是O(n),這裡面不包括等待時間。相關代碼在do_poll函數中。

3,將獲得的資料傳送到使用者空間並執行釋放記憶體和剝離等待隊列等善後工作,向使用者空間拷貝資料與剝離等待隊列等操作的的時間複雜度同樣是O(n),具體程式碼封裝括do_sys_poll函數中調用do_poll後到結束的部分。

 

epoll()函數的作用

epoll    epoll是Linux下多工IO介面select/poll的增強版本,它能顯著減少程式在大量並發串連中只有少量活躍的情況下的系統CPU利用率,因為它不會複用檔案描述符集合來傳遞結果而迫使開發人員每次等待事件之前都必須重新準備要被偵聽的檔案描述符集合,另一點原因就是擷取事件的時候,它無須遍曆整個被偵聽的描述符集,只要遍曆那些被核心IO事件非同步喚醒而加入Ready隊列的描述符集合就行了。epoll的除了提供select/poll 那種IO事件的電平觸發(Level
Triggered)外,還提供了邊沿觸發(Edge Triggered),這就使得使用者空間程式有可能緩衝IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程式效率。

epoll的介面非常簡單,一共就三個函數:

intepoll_create(int size);   建立一個epoll的控制代碼,size用來告訴核心這個監聽的數目一共有多大。這個參數不同於select()中的第一個參數,給出最大監聽的fd+1的值。需要注意的是,當建立好epoll控制代碼後,它就是會佔用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡。

intepoll_ctl(int epfd, int op, int fd, struct epoll_event *event);   epoll的事件註冊函數,它不同與select()是在監聽事件時告訴核心要監聽什麼類型的事件,而是在這裡先註冊要監聽的事件類型。第一個參數是epoll_create()的傳回值,第二個參數表示動作,用三個宏來表示:

EPOLL_CTL_ADD:註冊新的fd到epfd中;

EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;

EPOLL_CTL_DEL:從epfd中刪除一個fd;

第三個參數是需要監聽的fd,第四個參數是告訴核心需要監聽什麼事,structepoll_event結構如下:

struct epoll_event {

  __uint32_t events;  /* Epoll events */

  epoll_data_t data;  /* User data variable */

};

events可以是以下幾個宏的集合:

EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);

EPOLLOUT:表示對應的檔案描述符可以寫;

EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);

EPOLLERR:表示對應的檔案描述符發生錯誤;

EPOLLHUP:表示對應的檔案描述符被掛斷;

EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。

EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裡

int epoll_wait(int epfd, struct epoll_event * events, int maxevents,int timeout);

等待事件的產生,類似於select()調用。參數events用來從核心得到事件的集合,maxevents告之核心這個events有多大,這個 maxevents的值不能大於建立epoll_create()時的size,參數timeout是逾時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數返回需要處理的事件數目,如返回0表示已逾時。

epoll優點

(1)支援一個進程開啟大數目的socket描述符(FD)

select 最不能忍受的是一個進程所開啟的FD是有一定限制的,由FD_SETSIZE設定,預設值是2048。對於那些需要支援的上萬串連數目的IM伺服器來說顯然太少了。這時候你

一是可以選擇修改這個宏然後重新編譯核心,不過資料也同時指出這樣會帶來網路效率的下降,

二是可以選擇多進程的解決方案(傳統的Apache方案),不過雖然linux上面建立進程的代價比較小,但仍舊是不可忽視的,加上進程間資料同步遠比不上線程間同步的高效,所以也不是一種完美的方案。

epoll則沒有這個限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關係很大。

(2)IO效率不隨FD數目增加而線性下降

傳統的select/poll另一個致命弱點就是當你擁有一個很大的socket集合,不過由於網路延時,任一時間只有部分的socket是"活躍"的,但是select/poll每次調用都會線性掃描全部的集合,導致效率呈現線性下降。

epoll不存在這個問題,它只會對"活躍"的socket進行操作--- 這是因為在核心實現中epoll是根據每個fd上面的callback函數實現的。那麼,只有"活躍"的socket才會主動的去調用 callback函數,其他idle狀態socket則不會,在這點上,epoll實現了一個"偽"AIO,因為這時候推動力在os核心。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環境,epoll並不比select/poll有什麼效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle
connections類比WAN環境,epoll的效率就遠在select/poll之上了。

(3)使用mmap加速核心與使用者空間的訊息傳遞

這點實際上涉及到epoll的具體實現了。無論是select,poll還是epoll都需要核心把FD訊息通知給使用者空間,如何避免不必要的記憶體拷貝就很重要,在這點上,epoll是通過核心於使用者空間mmap同一塊記憶體實現的。而如果你想我一樣從2.5核心就關注epoll的話,一定不會忘記手工mmap這一步的。

(4)核心微調

這一點其實不算epoll的優點了,而是整個linux平台的優點。也許你可以懷疑linux平台,但是你無法迴避linux平台賦予你微調核心的能力。比如,核心TCP/IP協議棧使用記憶體池管理sk_buff結構,那麼可以在運行時期動態調整這個記憶體pool(skb_head_pool)的大小--- 通過echoXXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數的第2個參數(TCP完成3次握手的資料包隊列長度),也可以根據你平台記憶體大小動態調整。更甚至在一個資料包面數目巨大但同時每個資料包本身大小卻很小的特殊系統上嘗試最新的NAPI網卡驅動架構。

例子:

#include<stdio.h>

#include<stdlib.h>

#include<fcntl.h>

#include<sys/select.h>

#include<poll.h>

#include<sys/time.h>

#include<unistd.h>

#include<sys/stat.h>

int main()

{

    fd_set     rfds, wfds;

    int     fd, result;

    char     buf[10];

    struct pollfd fds[2];

     if((fd =open("tempselect", O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR)) < 0)

        printf("opentempselect error");

    FD_ZERO(&rfds);

    FD_ZERO(&wfds);

    FD_SET(fd, &rfds);

    FD_SET(STDIN_FILENO,&wfds);

           if((result =select(fd+1, &rfds, &wfds, NULL, NULL)) == -1)

        perror("selecterror");

    else if(result == 0)

        printf("no fdready\n");

    else

    {

        printf("%d fd(s) ready\n",result);

        if(FD_ISSET(fd,&rfds))

            printf("fd isready for read\n");

        if(read(fd, buf, 10)< 0)

            perror("readfd error");

       if(FD_ISSET(STDIN_FILENO, &wfds))

            printf("STDINis ready for write\n");

    }

    fds[0].fd = fd;

    fds[0].events = POLLIN;

    fds[1].fd = STDIN_FILENO;

    fds[1].events = POLLOUT;

    if((result = poll(fds, 2,-1)) == -1)

        perror("pollerror");

    else

    {

        printf("%d fd(s)ready\n", result);

        if(fds[0].revents ==POLLIN)

            printf("fd isready for read\n");

        if(read(fd, buf, 10)< 0)

            perror("readfd error");

        if(fds[1].revents ==POLLOUT)

            printf("STDINis ready for write\n");

    }

     exit(0);

}

顯示的結果:

2 fd(s) ready

fd is ready for read

read fd error: Bad file descriptor

STDIN is ready for write

2 fd(s) ready

fd is ready for read

read fd error: Bad file descriptor

STDIN is ready for write

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.