1. 程式人生 > >linux下select,poll,epoll的使用與重點分析

linux下select,poll,epoll的使用與重點分析

end 復用 cps typedef lis callback 指向 hub 機制

好久沒用I/O復用了,感覺差點兒相同都快忘完了。記得當初剛學I/O復用的時候花了好多時間。可是因為那會不太愛寫博客,導致花非常多時間搞明確的東西,依舊非常easy忘記。俗話說眼過千遍不如手過一遍,的確。在以後的學習中,不管知識的難易亦或是重要程度怎樣。我都會盡量義博客的形式記錄下來,這樣即能用博客來督促自己學習,也能加深對知識的理解倆全其美,好了廢話不說了。

I/O復用的基本概述

I/O復用技術主要是用來同一時候監聽多個套接字描寫敘述符,使得我們的程序大幅度的提高性能,一般例如以下情況會用到I/O復用技術
(1)程序須要同一時候處理多個socket
(2)客戶端程序需同一時候處理用戶輸入和網絡連接
(3)TCPserver要同一時候處理監聽socket和連接socket
(4)server要同一時候處理TCP和UDP請求
(5)server要同一時候處理多個端口

1.select系統調用

#include<sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);

.ndf參數指定被監聽文件描寫敘述符個數,它通常被設為select監聽的全部文件描寫敘述符加1。
.readfds,writefds,exceptfds參數分別指向可讀,可寫和異常事件,應用程序通過將自己感興趣的文件描寫敘述符增加到相應的集合中去,select調用返回時。內核將改動他們來通知應用程序哪些文件描寫敘述符已經就緒,timeout為超時時間,select調用成功返回就緒的文件描寫敘述符個數
我們一般使用例如以下宏來訪問fd_set中的位

#include<sys/select.h>
FD_ZERO(fd_set *fdset);     //清除fdset的全部位
FD_SET(int fd,fd_set *fdset);//設置fdset的fd位
FD_CLR(int fd,fd_set *fdset);//清除fdset的位fd
int FD_ISSET(int fd,fd_set *fdset); //測試fdset的位是否被設置

文件描寫敘述符就緒條件
(1)socket內核接收緩沖區大於或等於其低水位標誌SO_RCVLOWAT.此時我們能夠無堵塞的讀該socket
(2)socket通信的對方關閉連接,此時對該socket的讀操作將返回0
(3)監聽socket上有新的連接請求
(4)socket上有未處理的錯誤
(5)socket的內核發送緩沖區大於其低水位字節SO_SNDLOWAT
(6)socket使用非堵塞connect連接成功或失敗之後
(7)socket上有未處理的錯誤
詳細實例


參考偽代碼例如以下

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

int main(void)
{
    if(argc < 3)
    {
        cout<<"參數有誤"<<endl;
    }

    char *ip = argv[1];
    int port = atoi(argv[2]);

    int ret = 0;
    struct sockaddr_in address;
    bzero(&address,sizeof(adddress));
    address.sin_family = AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr);
    address.sin_port = htons(port);

    int listenfd = socket(AF_INET,SOCK_STREAM,0);
    assert(ret != -1);

    ret = bind(listenfd,(struct sockaddr *)&address,sizeof(address));
    assert(ret != -1);

    ret = listen(listenfd,5);
    assert(ret != -1);

    struct sockaddr_in client_address;
    socklen_t len = sizeof(client_address); 

    int connfd = accept(listenfd,(struct sockaddr *)&client_address,&len);

    if(connfd < 0)
    {
        cout<<"error"<<endl;
        close(listenfd);
    }

    char buf[1024];
    fd_set readfds;
    FD_ZERO(&readfds);

    while(1)
    {
        bzero(buf,1024);

        FD_SET(connfd,&readfds);

        ret = select(connfd + 1,&readfds,NULL,NULL,NULL);
        if(ret < 0)
        {
            cout<<"error"<<endl;
        }

        //判讀可讀事件是否發生
        if(FD_ISSET(connfd,&readfds))
        {
            ret = recv(connfd,buf,sizeof(buf) - 1,0);

            if(ret <= 0)
            {
                break;
            }

            cout<<buf<<endl;
        }


    }

    close(listenfd);
    close(connfd);

    return 0;
}

select的特點
select的內部實現調用了poll(以下會寫道,所以讀者能夠先跳過這裏,去讀poll,然後在回頭一起看這個)。全部它和poll的特點同樣。僅僅是函數接口有所不同,本質一樣
poll的特點例如以下
(1)將用戶傳入的pollfd數據(相應select的描寫敘述符集合)復制到內核空間,這個拷貝過程事件復雜度為O(N)
(2)挨個查詢每個文件描寫敘述符的狀態,假設無就緒的文件描寫敘述符,則進程就會掛起等待,知道發生超時或設備驅動再次喚醒它。然後它再次遍歷全部的文件描寫敘述符,找出發生事件的文件描寫敘述符。因為其共遍歷2次文件文件描寫敘述符,所以其事件復雜度為O(N)
(3)將獲得的數據拷貝至用戶空間。時間復雜度又是O(N)

2.poll的使用

poll的原型例如以下

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

當中fds為pollfd類型的結構體數組其結構體定義例如以下

struct pollfd
{
    int fd;      //要監聽文件描寫敘述符
    short events;//註冊的事件
    short revents;//實際發生的事件,內核填充
}

poll可監聽的事件類型例如以下(僅僅列出了經常使用的)

事件 描寫敘述
POLLIN 數據可讀
POLLOUT 數據可寫
POLLRDHUB TCP連接被對方關閉,或對方關閉了寫操作
POLLHUB 掛起,比方管道的寫端被關閉
POLLERR 錯誤

nfds為監聽的文件描寫敘述符個數
timeout為超時事件
索引Poll返回的文件描寫敘述符

int ret = poll(fds,MAX_EVENT_NUMBER,-1);

//必須遍歷全部文件描寫敘述符找到當中的就緒事件(也能夠依據已知的就緒個數進行簡單的優化)

for(int i = 0;i<MAX_EVENT_NUMBER,i++)
{
    if(fds[i].revents & POLLIN)
    {
        int sock = fds[i];
    }
}

關於poll的特點讀者能夠回到select去看,前面有寫到,因為實在說其和select的內部實現機制是一樣的所以不是必需多余寫

3.epoll的使用

epoll是linux特有的I/O復用函數,他在實現和使用上與其它I/O復用有所不同。

它是使用一組函數來完畢任務的,其次epoll把用戶關心的文件描寫敘述符上的事件放在一個內核事件表中。

epoll須要使用一個額外的文件描寫敘述符來來唯一的標識內核中的這個事件表,文件描寫敘述符使用epoll_create()來創建

#include<sys/epoll.h>
int epoll_create(int size);

size參數告訴內核事件表須要多大,該函數返回的文件描寫敘述符。將用於接下來全部函數的第一個參數

以下函數用來操作內核事件表

#include<sys/epoll.h>
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);

fd為要操作的文件描寫敘述符,op參數則指定操作類型,操作類型有例如以下幾種

操作類型 詳細描寫敘述
EPOLL_CTL_ADD 往事件表中註冊fd上的事件
EPOLL_CTL_MOD 改動fd上的註冊事件
EPOLL_CTL_DEL 刪除fd上的註冊事件

epoll_event結構體的定義例如以下

struct epoll_event
{
    _uint32_t events    //epoll事件
    epoll_data_t        //用戶數據
}

當中epoll_data_t是個聯合體其定義例如以下

typedef union epoll_data
{
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
}epoll_data_t

epoll_wait()函數
epoll事件調用的主要接口就是epoll_wait函數,它在一段超時事件內等待一組文件描寫敘述符上的事件

#include<sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);

該函數成功時返回就緒的文件描寫敘述符個數
假設epoll_wait檢測到就緒事件,就將全部的就緒事件賦值到第二個參數epoll_event數組當中,它僅僅用於輸出檢測到的就緒事件。不想poll的數組參數既用於傳入用戶註冊的事件,又用於輸出內核檢測的事件,這會極大的減少應用程序索引文件描寫敘述符的效率
epoll的使用

int ret = epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
//僅僅需遍歷ret個文件描寫敘述符
for(int i = 0;i<ret;i++)
{
int sockfd = events[i].data.fd;
}
特別註意epoll對文件描寫敘述符有倆中模式LT和ET。ET是epoll的高效模式

epoll的特點
epoll在內核實現中是依據每個fd上的俄callback函數來實現的,僅僅有活躍的fd才會主動調用callback,其它的fd則不會。

假設所監控的全部文件描寫敘述符基本上都是活躍的那麽epoll和select或poll差距不是太大。可是要是所監控的文件描寫敘述符僅僅有少數活躍,epoll的效率要遠高於他倆

linux下select,poll,epoll的使用與重點分析