Linux網絡編程 - 另一種I/O多路複用:poll

上一講我們講到了 I/O 多路複用技術,並以 select 爲核心,展示了 I/O 多路複用技術的能力。select 方法是多個 UNIX 平臺支持的非常常見的 I/O 多路複用技術,但是它有一個缺點,那就是所支持的文件描述符的個數是有限的。在 Linux 系統中,select 的默認最大值爲 1024。

poll 函數

和 select 相比,poll 和內核交互的數據結構有所變化,另外,也突破了文件描述符的個數限制。

int poll(struct pollfd *fds, unsigned long nfds, int timeout);   
返回值:若有就緒描述符則爲其數目,若超時則爲0,若出錯則爲-1
struct pollfd {
    int    fd;       /* file descriptor */
    short  events;   /* events to look for */
    short  revents;  /* events returned */
 };

這個結構體由三個部分組成,首先是描述符 fd,然後是描述符上待檢測的事件類型 events,注意這裏的 events 可以表示多個不同的事件,具體的實現可以通過使用二進制掩碼位操作來完成,例如,POLLIN 和 POLLOUT 可以表示讀和寫事件。

#define    POLLIN    0x0001    /* any readable data available */
#define    POLLPRI   0x0002    /* OOB/Urgent readable data */
#define    POLLOUT   0x0004    /* file descriptor is writeable */

和 select 不同的地方在於,poll 每次檢測之後的結果不會修改原來的傳入值,而是將結果保留在 revents 字段中,這樣就不需要每次檢測完都得重置待檢測的描述字和感興趣的事件。我們可以把 revents 理解成“returned events”。

events 類型的事件可以分爲兩大類。第一類是可讀事件,有以下幾種:

#define POLLIN     0x0001    /* any readable data available */
#define POLLPRI    0x0002    /* OOB/Urgent readable data */
#define POLLRDNORM 0x0040    /* non-OOB/URG data available */
#define POLLRDBAND 0x0080    /* OOB/Urgent readable data */

一般我們在程序裏面有 POLLIN 即可。套接字可讀事件和 select 的 readset 基本一致,是系統內核通知應用程序有數據可以讀,通過 read 函數執行操作不會被阻塞。

第二類是可寫事件,有以下幾種:

#define POLLOUT    0x0004    /* file descriptor is writeable */
#define POLLWRNORM POLLOUT   /* no write type differentiation */
#define POLLWRBAND 0x0100    /* OOB/Urgent data can be written */

一般我們在程序裏面統一使用 POLLOUT。套接字可寫事件和 select 的 writeset 基本一致,是系統內核通知套接字緩衝區已準備好,通過 write 函數執行寫操作不會被阻塞。

以上兩大類的事件都可以在“returned events”得到複用。還有另一大類事件,沒有辦法通過 poll 向系統內核遞交檢測請求,只能通過“returned events”來加以檢測,這類事件是各種錯誤事件。

#define POLLERR    0x0008    /* 一些錯誤發送 */
#define POLLHUP    0x0010    /* 描述符掛起*/
#define POLLNVAL   0x0020    /* 請求的事件無效*/

我們再回過頭看一下 poll 函數的原型。參數 nfds 描述的是數組 fds 的大小,簡單說,就是向 poll 申請的事件檢測的個數。

最後一個參數 timeout,描述了 poll 的行爲。如果是一個 <0 的數,表示在有事件發生之前永遠等待;如果是 0,表示不阻塞進程,立即返回;如果是一個 >0 的數,表示 poll 調用方等待指定的毫秒數後返回。

關於返回值,當有錯誤發生時,poll 函數的返回值爲 -1;如果在指定的時間到達之前沒有任何事件發生,則返回 0,否則就返回檢測到的事件個數,也就是“returned events”中非 0 的描述符個數。

poll 函數有一點非常好,如果我們不想對某個 pollfd 結構進行事件檢測可以把它對應的 pollfd 結構的 fd 成員設置成一個負值。這樣,poll 函數將忽略這樣的 events 事件,檢測完成以後,所對應的“returned events”的成員值也將設置爲 0。

我們發現 poll 函數和 select 不一樣的地方就是,在 select 裏面,文件描述符的個數已經隨着 fd_set 的實現而固定,沒有辦法對此進行配置;而在 poll 函數裏,我們可以控制 pollfd 結構的數組大小,這意味着我們可以突破原來 select 函數最大描述符的限制,在這種情況下,應用程序調用者需要分配 pollfd 數組並通知 poll 函數該數組的大小。

基於 poll 的服務器程序

#define INIT_SIZE 128

int main() 
{
    int listenfd, connfd;
    int ready_number;
    ssize_t n;
    char buf[MAXLINE];
    struct servaddr_in cli_addr;

    listenfd = socket(PF_INET, SOCK_STREAM, 0);

    bzero(&serv_addr, sizeof(serv_addr));
    bzero(&cli_addr, sizeof(cli_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(7878);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    bind(listenfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr));
    listen(listenfd, SOMAXCONN);

    /*初始化pollfd數組,這個數組的第一個元素是listenfd,其餘的用來記錄將要連接的connfd
     數組的大小固定爲 INIT_SIZE,這在實際的生產環境肯定是需要改進的。*/
    struct pollfd event_set[INIT_SIZE];
    /*將監聽套接字 listen_fd 和對應的 POLLRDNORM 事件加入到 event_set 裏,表示我們期望系統內核    
      檢測監聽套接字上的連接建立完成事件*/
    event_set[0].fd = listenfd;
    event_set[0].events = POLLRDNORM;

    // 用-1表示這個數組位置還沒有被佔用
    int i;
    for (i = 1; i < INIT_SIZE; i++) {
        event_set[i].fd = -1;
    }

    for (;;) {
        //timeout 設置爲 -1,表示在 I/O 事件發生之前 poll 調用一直阻塞。
        if ((ready_number = poll(event_set, INIT_SIZE, -1)) < 0) {
            printf("poll failed \n");
            continue;
        }

        if (event_set[0].revents & POLLRDNORM) {
            socklen_t client_len = sizeof(client_addr);
            connfd = accept(listenfd, (struct sockaddr *) &cli_addr,
&client_len);

            //找到一個可以記錄該連接套接字的位置
            for (i = 1; i < INIT_SIZE; i++) {
                if (event_set[i].fd < 0) {
                    /*把連接描述字 connect_fd 也加入到 event_set 裏,而且說明了我們感興趣的事件類型爲 POLLRDNORM,也就是套集字上有數據可以讀*/
                    event_set[i].fd = connfd;
                    event_set[i].events = POLLRDNORM; //POLLIN包括了OOB等帶外數據的檢測,POLLRDNORM則不包括這部分。
                    break;
                }
            }

            if (i == INIT_SIZE) {
                printf("can not hold so many clients\n");
            }
            /*如果處理完監聽套接字之後,就已經完成了這次 I/O 複用所要處理的事情,那麼我們就可以跳過後面的處理,再次進入 poll 調用。*/
            if (--ready_number <= 0)
                continue;
        }

        /*查看 event_set 裏面其他的事件,也就是已連接套接字的可讀事件。這是通過遍歷 event_set 數組來完成的。*/
        for (i = 1; i < INIT_SIZE; i++) {
            int socket_fd;
            if ((socket_fd = event_set[i].fd) < 0)
                continue;
            //通過檢測 revents 的事件類型是 POLLRDNORM 或者 POLLERR,我們可以進行讀操作。
            if (event_set[i].revents & (POLLRDNORM | POLLERR)) {
                if ((n = read(socket_fd, buf, MAXLINE)) > 0) {
                    if (write(socket_fd, buf, n) < 0) {
                        printf("write error\n");
                    }
                } else if (n == 0 || errno == ECONNRESET) {
                    //如果讀到 EOF 或者是連接重置,則關閉這個連接,並且把 event_set 對應的 pollfd 重置
                    close(socket_fd);
                    event_set[i].fd = -1;
                } else {
                    printf("read error\n");
                }

                if (--ready_number <= 0)
                    break;
            }
        }
    }
}

總之,poll 是另一種在各種 UNIX 系統上被廣泛支持的 I/O 多路複用技術,雖然名聲沒有 select 那麼響,能力一點不比 select 差,而且因爲可以突破 select 文件描述符的個數限制,在高併發的場景下尤其佔優勢。

 

溫故而知新 !

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章