天天看點

一文搞懂select、poll和epoll差別(下)2 poll3 epoll4 總結

1.2 缺點

核心需要将消息傳遞到使用者空間,都需要核心拷貝動作。需要維護一個用來存放大量fd的資料結構,使得使用者空間和核心空間在傳遞該結構時複制開銷大。

  • 每次調用select,都需把fd集合從使用者态拷貝到核心态,fd很多時開銷就很大
  • 同時每次調用select都需在核心周遊傳遞進來的所有fd,fd很多時開銷就很大
  • select支援的檔案描述符數量太小了,預設最大支援1024個
  • 主動輪詢效率很低

2 poll

和select類似,隻是描述fd集合的方式不同,poll使用

pollfd

結構而非select的

fd_set

結構。

管理多個描述符也是進行輪詢,根據描述符的狀态進行處理,但poll沒有最大檔案描述符數量的限制。

poll和select同樣存在一個缺點就是,包含大量檔案描述符的數組被整體複制于使用者态和核心的位址空間之間,而不論這些檔案描述符是否就緒,它的開銷随着檔案描述符數量的增加而線性增大。

它将使用者傳入的數組拷貝到核心空間

然後查詢每個fd對應的裝置狀态:

如果裝置就緒

在裝置等待隊列中加入一項繼續周遊

若周遊完所有fd後,都沒發現就緒的裝置

挂起目前程序,直到裝置就緒或主動逾時,被喚醒後它又再次周遊fd。這個過程經曆多次無意義的周遊。

沒有最大連接配接數限制,因其基于連結清單存儲,其缺點:

  • 大量fd數組被整體複制于使用者态和核心位址空間間,而不管是否有意義
  • 如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd

是以又有了epoll模型。

3 epoll

epoll模型修改主動輪詢為被動通知,當有事件發生時,被動接收通知。是以epoll模型注冊套接字後,主程式可做其他事情,當事件發生時,接收到通知後再去處理。

可了解為event poll,epoll會把哪個流發生哪種I/O事件通知我們。是以epoll是事件驅動(每個事件關聯fd),此時我們對這些流的操作都是有意義的。複雜度也降到O(1)。

asmlinkage int sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
{
    int error;
    struct file *file, *tfile;
    struct eventpoll *ep;
    struct epitem *epi;
    struct epoll_event epds;

    error = -EFAULT;
    if (copy_from_user(&epds, event, sizeof(struct epoll_event)))
        goto eexit_1;

    /* Get the "struct file *" for the eventpoll file */
    error = -EBADF;
    file = fget(epfd);
    if (!file)
        goto eexit_1;

    /* Get the "struct file *" for the target file */
    tfile = fget(fd);
    if (!tfile)
        goto eexit_2;

    /* The target file descriptor must support poll */
    error = -EPERM;
    if (!tfile->f_op || !tfile->f_op->poll)
        goto eexit_3;

    /*
     * We have to check that the file structure underneath the file descriptor
     * the user passed to us _is_ an eventpoll file. And also we do not permit
     * adding an epoll file descriptor inside itself.
     */
    error = -EINVAL;
    if (file == tfile || !IS_FILE_EPOLL(file))
        goto eexit_3;

    /*
     * At this point it is safe to assume that the "private_data" contains
     * our own data structure.
     */
    ep = file->private_data;

    /*
     * Try to lookup the file inside our hash table. When an item is found
     * ep_find() increases the usage count of the item so that it won't
     * desappear underneath us. The only thing that might happen, if someone
     * tries very hard, is a double insertion of the same file descriptor.
     * This does not rapresent a problem though and we don't really want
     * to put an extra syncronization object to deal with this harmless condition.
     */
    epi = ep_find(ep, tfile);

    error = -EINVAL;
    switch (op) {
    case EPOLL_CTL_ADD:
        if (!epi) {
            epds.events |= POLLERR | POLLHUP;

            error = ep_insert(ep, &epds, tfile);
        } else
            error = -EEXIST;
        break;
    case EPOLL_CTL_DEL:
        if (epi)
            error = ep_remove(ep, epi);
        else
            error = -ENOENT;
        break;
    case EPOLL_CTL_MOD:
        if (epi) {
            epds.events |= POLLERR | POLLHUP;
            error = ep_modify(ep, epi, &epds);
        } else
            error = -ENOENT;
        break;
    }

    /*
     * The function ep_find() increments the usage count of the structure
     * so, if this is not NULL, we need to release it.
     */
    if (epi)
        ep_release_epitem(epi);

eexit_3:
    fput(tfile);
eexit_2:
    fput(file);
eexit_1:
    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %u) = %d\n",
             current, epfd, op, fd, event->events, error));

    return error;
}
      

3.1 觸發模式

EPOLLLT和EPOLLET兩種:

  • LT,預設的模式(水準觸發)

    隻要該fd還有資料可讀,每次

    epoll_wait

    都會傳回它的事件,提醒使用者程式去操作,
  • ET是“高速”模式(邊緣觸發)
  • 一文搞懂select、poll和epoll差別(下)2 poll3 epoll4 總結
  • 隻會提示一次,直到下次再有資料流入之前都不會再提示,無論fd中是否還有資料可讀。是以在ET模式下,read一個fd的時候一定要把它的buffer讀完,即讀到read傳回值小于請求值或遇到EAGAIN錯誤

epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,核心就會采用類似回調機制激活該fd,epoll_wait便可收到通知。

EPOLLET觸發模式的意義

若用

EPOLLLT

,系統中一旦有大量無需讀寫的就緒檔案描述符,它們每次調用

epoll_wait

都會傳回,這大大降低處理程式檢索自己關心的就緒檔案描述符的效率。

而采用EPOLLET,當被監控的檔案描述符上有可讀寫事件發生時,epoll_wait會通知處理程式去讀寫。如果這次沒有把資料全部讀寫完(如讀寫緩沖區太小),那麼下次調用epoll_wait時,它不會通知你,即隻會通知你一次,直到該檔案描述符上出現第二次可讀寫事件才會通知你。這比水準觸發效率高,系統不會充斥大量你不關心的就緒檔案描述符。

3.2 優點

  • 沒有最大并發連接配接的限制,能打開的FD的上限遠大于1024(1G的記憶體上能監聽約10萬個端口)
  • 效率提升,不是輪詢,不會随着FD數目的增加效率下降。隻有活躍可用的FD才會調用callback函數
  • 即Epoll最大的優點就在于它隻關心“活躍”的連接配接,而跟連接配接總數無關,是以在實際的網絡環境中,Epoll的效率就會遠遠高于select和poll

記憶體拷貝,利用mmap()檔案映射記憶體加速與核心空間的消息傳遞;即epoll使用mmap減少複制開銷。

epoll通過核心和使用者空間共享一塊記憶體來實作的

表面上看epoll的性能最好,但是在連接配接數少并且連接配接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數回調。

epoll跟select都能提供多路I/O複用的解決方案。在現在的Linux核心裡有都能夠支援,其中epoll是Linux所特有,而select則應該是POSIX所規定,一般作業系統均有實作。

select和poll都隻提供了一個函數——select或者poll函數。而epoll提供了三個函數,epoll_create,epoll_ctl和epoll_wait,epoll_create是建立一個epoll句柄;epoll_ctl是注冊要監聽的事件類型;epoll_wait則是等待事件的産生。

對于第一個缺點,epoll的解決方案在epoll_ctl函數中。每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進核心,而不是在epoll_wait的時候重複拷貝。epoll保證了每個fd在整個過程中隻會拷貝一次。

對于第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的裝置等待隊列中,而隻在epoll_ctl時把current挂一遍(這一遍必不可少)并為每個fd指定一個回調函數,當裝置就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒連結清單)。epoll_wait的工作實際上就是在這個就緒連結清單中檢視有沒有就緒的fd(利用schedule_timeout()實作睡一會,判斷一會的效果,和select實作中的第7步是類似的)。

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

4 總結

select,poll,epoll都是IO多路複用機制,即可以監視多個描述符,一旦某個描述符就緒(讀或寫就緒),能夠通知程式進行相應讀寫操作。

但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實作會負責把資料從核心拷貝到使用者空間。

select,poll實作需要自己不斷輪詢所有fd集合,直到裝置就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒連結清單,期間也可能多次睡眠和喚醒交替,但是它是裝置就緒時,調用回調函數,把就緒fd放入就緒連結清單中,并喚醒在epoll_wait中進入睡眠的程序。雖然都要睡眠和交替,但是select和poll在“醒着”的時候要周遊整個fd集合,而epoll在“醒着”的時候隻要判斷一下就緒連結清單是否為空就行了,這節省了大量的CPU時間。這就是回調機制帶來的性能提升。

select,poll每次調用都要把fd集合從使用者态往核心态拷貝一次,并且要把current往裝置等待隊列中挂一次,而epoll隻要一次拷貝,而且把current往等待隊列上挂也隻挂一次(在epoll_wait的開始,注意這裡的等待隊列并不是裝置等待隊列,隻是一個epoll内部定義的等待隊列)。這也能節省不少的開銷。

參考

Linux下select/poll/epoll機制的比較

https://www.cnblogs.com/anker/p/3265058.html

繼續閱讀