IO複用之——poll

一. 關於poll

    對於IO複用模型,其優點無疑是免去了對一個個IO事件就緒的等待,轉而代之的是同時對多個IO數據的檢測,當檢測等待的事件中至少有一個就緒的時候,就會返回告訴用戶進程“已經有數據準備好了,快看看是哪個趕緊處理”,而對於IO複用的實現,除了可以用select函數,另外一個函數仍然支持這種複用IO模型,就是poll函數;



二. poll函數的用法

    雖然同樣是對多個IO事件進行檢測等待,但poll和select多少還是有些不同的:

wKiom1dHB9mAvCb_AAAZZrdUWaM197.png

函數參數中,

先來說nfds,這個是指當前需要關心的文件描述符的個數;

timeout同樣是設置超時時間,只是和select的timeout是一個結構體不一樣,這裏只是一個整型類型,且含義是毫秒

fds是一個結構體指針,如下:

wKioL1dHCM_xen90AAAdbbZXhd4736.png

結構體中,

fd表示所要關心的文件描述符;

events表示該文件描述符所關心的事件,這是一個輸入型參數,要告訴操作系統這個文件描述符對應的事件所關心的操作事件是什麼,比如讀或寫;

revents是一個輸出型參數,表示當poll返回時告訴用戶什麼操作事件是就緒的,比如如果POLLIN是就緒的,那麼返回時revent的值就是POLLIN,告訴用戶fd事件的POLLIN是就緒的;

events和revents的值可以爲如下:

wKioL1dHDDKzCxZ_AAAQQ7FNxqY876.png這裏要說明選項其實不止這三個,只是這裏的討論中這三個選項是最常用的;

events設置爲POLLIN表示fd所需要讀取數據,而revents若返回POLLIN則表示data已經ready可以讀取了;

同樣,events設置爲POLLOUT表示fd所關心數據的寫入,而revents返回POLLOUT則表示寫事件就緒可以進行數據的寫入;

至於POLLPRI,後面的解釋是作爲緊急選項來設置的,在TCP協議報文中有個URG的緊急指針是表示先從緊急數據的地方開始讀取,這裏也是這個意思;



三. 栗子時間

    同樣的,這裏可以用poll來編寫基於TCP協議的server端和client端的數據通信,和select一樣,避免使用多進程和多線程的方式轉而使用對多個IO接口的等待:


server服務器端:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <unistd.h>
#include <string.h>

#define _BACKLOG_ 5//網絡中連接請求隊列中的最大等待數目
#define _NUM_ 10//IO事件結構體數組大小

void usage(const char *argv)//命令行參數的差錯判斷
{
    printf("%s   [ip]   [port]\n", argv);
    exit(0);
}

static int CreateListenSocket(int ip, int port)//創建監聽套接字
{
    int sock = socket(AF_INET, SOCK_STREAM, 0); 
    if(sock < 0)
    {   
        perror("socket");
        exit(1);
    }   

    struct sockaddr_in server;//設置本地網絡地址信息
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.s_addr = ip; 

    if(bind(sock, (struct sockaddr*)&server, sizeof(server)) < 0)//綁定
    {   
        perror("bind");
        exit(2);
    }
    
    if(listen(sock, _BACKLOG_) < 0)//監聽
    {
        perror("listen");
        exit(3);
    }

    return sock;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
        usage(argv[0]);
    int port = atoi(argv[2]);
    int ip = inet_addr(argv[1]);

    int listen_sock = CreateListenSocket(ip, port);//獲取監聽socket
    struct sockaddr_in client;//用於存放client端的網絡地址信息
    socklen_t client_len = sizeof(client);

    struct pollfd fds[_NUM_];//用一個結構體數組來存放各種IO事件
    fds[0].fd = listen_sock;//將監聽套接字放入數組中
    fds[0].events = POLLIN;//將監聽事件需要的event設置爲讀取數據POLLIN
    fds[0].revents = 0;

    size_t i = 1;
    for(; i < _NUM_; ++i)//初始化結構體數組中成員
    {
        fds[i].fd = -1;
        fds[i].events = 0;
        fds[i].revents = 0;
    }
    int max_fd = 1;//設置最大的文件描述符個數
    int timeout = 5000;//設置超時時間
    
    while(1)
    {
        switch(poll(fds, max_fd, timeout))
        {
            case -1://出錯
                perror("poll");
                break;
            case 0://超時
                printf("timeout...\n");
                break;
            default://至少有一個事件已經就緒,可以進行IO數據的操作
                {
                    size_t i = 0;
                    for(; i < _NUM_; ++i)
                    {
                            //判斷是否爲監聽事件就緒,如果是代表有連接請求需要處理
                        if((fds[i].fd == listen_sock) && (fds[i].revents == POLLIN))
                        {
                                //處理連接
                            int accept_sock = accept(listen_sock, (struct sockaddr*)&client, &client_len);
                            if(accept_sock < 0)
                            {
                                perror("accept");
                                continue;
                            }
                            printf("connect with a client... [ip]:%s  [port]:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));

                            size_t i = 0;
                            //將創建出新的數據傳輸socket文件描述符設置進poll結構體數組
                            for(; i < _NUM_; ++i)
                            {
                                if(fds[i].fd == -1)
                                {
                                    fds[i].fd = accept_sock;
                                    fds[i].events = POLLIN;//將事件設置爲需要讀取數據
                                    max_fd++;//更新最大文件描述符個數
                                    break;
                                }
                                
                            }
                            if(i == _NUM_)
                                close(accept_sock);//若超出數組個數則表明當前無法處理,關閉掉
                        }//表示爲其他除了listen socket之外的要進行數據傳輸的socket
                        else if((fds[i].fd > 0) && (fds[i].revents == POLLIN))
                        {
                            char buf[1024];
                            //進行數據的讀取
                            ssize_t size = read(fds[i].fd, buf, sizeof(buf)-1);
                            if(size < 0)//讀取出錯
                                perror("read");
                            else if(size == 0)//客戶端關閉,將結構體數組中相應位置清空
                            {
                                 printf("client closed...\n");
                                 //將要關閉的文件描述符和結構體數組中最後一個有效的文件描述符進行交換
                                 //確保當前有效的文件描述符在數組內都是連續的
                                                                 struct pollfd tmp = fds[i];
                                                                 fds[i] = fds[max_fd-1];
                                                                 fds[max_fd-1] = tmp;
                                                                 
                                                                 //交換後要刪除的就在數組的最後,關閉且將數組內相應位置爲無效值
                                                                 close(fds[max_fd-1].fd);
                                                                 fds[max_fd-1].fd = -1; 
                                                                 fds[max_fd-1].events = 0;
                                                                 fds[max_fd-1].revents = 0;
                                                                 --max_fd;//更新當前有效文件描述符的個數
                            }
                            else//讀取成功,輸出數據
                            {
                                buf[size]  ='\0';
                                printf("client# %s\n", buf);
                            }

                        }
                        else
                        {}
                    }
                }
                break;
        }
    }
    return 0;
}

client客戶端這裏就不寫了,和基於TCP協議的socket編程中的client是差不多的;


運行程序:

wKiom1dINs6TtO_ZAAAQyAzXPhg489.png


這裏需要說明的是:

  1. 和select不同,select是用一個fd_set數據類型來存放各個需要操作的IO文件描述符,這裏的poll是用一個結構體來存放文件描述符和鎖關心的事件類型,因此,poll並沒有處理文件描述符的上限,但相同的是,每一次poll返回都仍然需要遍歷來獲取事件就緒的位置以此來進行相應的處理,還是一樣有複製和系統遍歷帶來的額外開銷,當處理事件比較多的時候仍然是低效的;

  2. 在每一次循環到select之前都需要將對應事件的fd_set的集合調用FD_ZERO函數來進行重新的初始化清零,因爲不是會有文件描述符的新建和關閉,需要進行初始化然後再將事件重新一一設置進相應的fd_set裏面;而poll並不需要,因爲使用一個結構體數組來管理相當於結合了select中的設置數組存放文件描述符和添加設置這兩步,每一次進行poll的時候都會將發生就緒的事件對應的revents置位,當處理完畢就會被系統自動歸爲0,並不需要進行手動初始化清零;

  3. 在結構體數組還未使用之前,和被剛定義的變量一樣,結構體成員fd、events和revents都是隨機值,雖然後來使用的時候都會被賦對應的有效值,但爲了避免判斷時的二義性問題最好還是在循環使用前都將其初始化爲統一的可識別的無效值;



《完》

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