Linuxepoll模型詳解及原始碼分析

adoryn發表於2018-06-03
版權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/zhaobryant/article/details/80557262

一、epoll簡介

epoll是當前在Linux下開發大規模併發網路程式的熱門選擇,epoll在Linux2.6核心中正式引入,和select相似,都是IO多路複用(IO multiplexing)技術。

按照man手冊的說法,epoll是為處理大批量控制程式碼而做了改進的poll

Linux下有以下幾個經典的伺服器模型:

1、PPC模型和TPC模型

PPC(Process Per Connection)模型和TPC(Thread Per Connection)模型的設計思想類似,就是給每一個到來的連線都分配一個獨立的程式或者執行緒來服務。對於這兩種模型,其需要耗費較大的時間和空間資源。當管理連線數較多時,程式或執行緒的切換開銷較大。因此,這類模型能接受的最大連線數都不會高,一般都在幾百個左右。

2、select模型

對於select模型,其主要有以下幾個特點:

  • 最大併發數限制:由於一個程式所開啟的fd(檔案描述符)是有限制的,由FD_SETSIZE設定,預設值是1024/2048,因此,select模型的最大併發數就被限制了。

  • 效率問題:每次進行select呼叫都會線性掃描全部的fd集合。這樣,效率就會呈現線性下降。

  • 核心/使用者空間記憶體拷貝問題:select在解決將fd訊息傳遞給使用者空間時採用了記憶體拷貝的方式。這樣,其處理效率不高。

3、poll模型

對於poll模型,其雖然解決了select最大併發數的限制,但依然沒有解決掉select的效率問題和記憶體拷貝問題。

4、epoll模型

對比於其他模型,epoll做了如下改進:

支援一個程式開啟較大數目的檔案描述符(fd)

select模型對一個程式所開啟的檔案描述符是有一定限制的,其由FD_SETSIZE設定,預設為1024/2048。這對於那些需要支援上萬連線數目的高併發伺服器來說顯然太少了,這個時候,可以選擇兩種方案:一是可以選擇修改FD_SETSIZE巨集然後重新編譯核心,不過這樣做也會帶來網路效率的下降;二是可以選擇多程式的解決方案(傳統的Apache方案),不過雖然Linux中建立執行緒的代價比較小,但仍然是不可忽視的,加上程式間資料同步遠不及執行緒間同步的高效,所以也不是一種完美的方案。

但是,epoll則沒有對描述符數目的限制,它所支援的檔案描述符上限是整個系統最大可以開啟的檔案數目,例如,在1GB記憶體的機器上,這個限制大概為10萬左右。

IO效率不會隨檔案描述符(fd)的增加而線性下降

傳統的select/poll的一個致命弱點就是當你擁有一個很大的socket集合時,不過任一時間只有部分socket是活躍的,select/poll每次呼叫都會線性掃描整個socket集合,這將導致IO處理效率呈現線性下降。

但是,epoll不存在這個問題,它只會對活躍的socket進行操作,這是因為在核心實現中,epoll是根據每個fd上面的callback函式實現的。因此,只有活躍的socket才會主動去呼叫callback函式,其他idle狀態socket則不會。在這一點上,epoll實現了一個偽AIO,其內部推動力在核心。

在一些benchmark中,如果所有的socket基本上都是活躍的,如高速LAN環境,epoll並不比select/poll效率高,相反,過多使用epoll_ctl,其效率反而還有稍微下降。但是,一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。

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

無論是select,poll還是epoll,它們都需要核心把fd訊息通知給使用者空間。因此,如何避免不必要的記憶體拷貝就很重要了。對於該問題,epoll通過核心與使用者空間mmap同一塊記憶體來實現。

核心微調

這一點其實不算epoll的優點了,而是整個Linux平臺的優點,Linux賦予開發者微調核心的能力。比如,核心TCP/IP協議棧使用記憶體池管理sk_buff結構,那麼,可以在執行期間動態調整這個記憶體池大小(skb_head_pool)來提高效能,該引數可以通過使用echo xxxx > /proc/sys/net/core/hot_list_length來完成。再如,可以嘗試使用最新的NAPI網路卡驅動架構來處理資料包數量巨大但資料包本身很小的特殊場景。

二、epoll API

epoll只有epoll_createepoll_ctlepoll_wait這三個系統呼叫。其定義如下:

#include <sys/epoll.h>

int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

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

1、epoll_create

#include <sys/epoll.h>

int epoll_create(int size);

可以呼叫epoll_create方法建立一個epoll的控制程式碼。

需要注意的是,當建立好epoll控制程式碼後,它就會佔用一個fd值。在使用完epoll後,必須呼叫close函式進行關閉,否則可能導致fd被耗盡。

2、epoll_ctl

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll的事件註冊函式,它不同於select是在監聽事件時告訴核心要監聽什麼型別的事件,而是通過epoll_ctl註冊要監聽的事件型別。

第一個引數epfd:epoll_create函式的返回值。

第二個引數events:表示動作型別。有三個巨集來表示:
* EPOLL_CTL_ADD:註冊新的fd到epfd中;
* EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
* EPOLL_CTL_DEL:從epfd中刪除一個fd。

第三個引數fd:需要監聽的fd。

第四個引數event:告訴核心需要監聽什麼事件。

struct epoll_event結構如下所示:

// 儲存觸發事件的某個檔案描述符相關的資料
typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

// 感興趣的事件和被觸發的事件
struct epoll_event {
    __uint32_t events; // Epoll events
    epoll_data_t data; // User data variable
};

如上所示,對於Epoll Events,其可以是以下幾個巨集的集合:

  • EPOLLIN:表示對應的檔案描述符可讀(包括對端Socket);
  • EPOLLOUT:表示對應的檔案描述符可寫;
  • EPOLLPRI:表示對應的檔案描述符有緊急資料可讀(帶外資料);
  • EPOLLERR:表示對應的檔案描述符發生錯誤;
  • EPOLLHUP:表示對應的檔案描述符被結束通話;
  • EPOLLET:將EPOLL設為邊緣觸發(Edge Triggered),這是相對於水平觸發(Level Triggered)而言的。
  • EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket,需要再次

3、epoll_wait

#include <sys/epoll.h>

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

收集在epoll監控的事件中已經發生的事件。引數events是分配好的epoll_event結構體陣列,epoll將會把發生的事件賦值到events陣列中(events不可以是空指標,核心只負責把資料賦值到這個event陣列中,不會去幫助我們在使用者態分配記憶體)。maxevents告訴核心這個events陣列有多大,這個maxevents的值不能大於建立epoll_create時的size。引數timeout是超時時間(毫秒)。如果函式呼叫成功,則返回對應IO上已準備好的檔案描述符數目,如果返回0則表示已經超時。

三、epoll工作模式

1. LT模式(Level Triggered,水平觸發)

該模式是epoll的預設工作模式,其同時支援阻塞和非阻塞socket。核心會告訴開發者一個檔案描述符是否就緒,如果開發者不採取任何操作,核心仍會一直通知。

2. ET模式(Edge Triggered,邊緣觸發)

該模式是一種高速處理模式,當且僅當狀態發生變化時才會獲得通知。在該模式下,其假定開發者在接收到一次通知後,會完整地處理該事件,因此核心將不再通知這一事件。注意,緩衝區中還有未處理的資料不能說是狀態變化,因此,在ET模式下,開發者如果只讀取了一部分資料,其將再也得不到通知了。正確的做法是,開發者自己確認讀完了所有的位元組(一直呼叫read/write直到出錯EAGAGIN為止)。

Nginx預設採用的就是ET(邊緣觸發)。

四、epoll高效性探討

epoll的高效性主要體現在以下三個方面:

(1)select/poll每次呼叫都要傳遞所要監控的所有fd給select/poll系統呼叫,這意味著每次呼叫select/poll時都要將fd列表從使用者空間拷貝到核心,當fd數目很多時,這會造成效能低效。對於epoll_wait,每次呼叫epoll_wait時,其不需要將fd列表傳遞給核心,epoll_ctl不需要每次都拷貝所有的fd列表,只需要進行增量式操作。因此,在呼叫epoll_create函式之後,核心已經在核心開始準備資料結構用於存放需要監控的fd了。其後,每次epoll_ctl只是對這個資料結構進行簡單的維護操作即可

(2)核心使用slab機制,為epoll提供了快速的資料結構。在核心裡,一切都是檔案。因此,epoll向核心註冊了一個檔案系統,用於儲存所有被監控的fd。當呼叫epoll_create時,就會在這個虛擬的epoll檔案系統中建立一個file節點。epoll在被核心初始化時,同時會分配出epoll自己的核心告訴cache區,用於存放每個我們希望監控的fd。這些fd會以紅黑樹的形式儲存在核心cache裡,以支援快速查詢、插入和刪除。這個核心高速cache,就是建立連續的實體記憶體頁,然後在之上建立slab層,簡單的說,就是物理上分配好想要的size的記憶體物件,每次使用時都使用空閒的已分配好的物件。

(3)當呼叫epoll_ctl往epfd註冊百萬個fd時,epoll_wait仍然能夠快速返回,並有效地將發生的事件fd返回給使用者。原因在於,當我們呼叫epoll_create時,核心除了幫我們在epoll檔案系統新建file節點,同時在核心cache建立紅黑樹用於儲存以後由epoll_ctl傳入的fd外,還會再建立一個list連結串列,用於儲存準備就緒的事件。當呼叫epoll_wait時,僅僅觀察這個list連結串列中有無資料即可。如果list連結串列中有資料,則返回這個連結串列中的所有元素;如果list連結串列中沒有資料,則sleep然後等到timeout超時返回。所以,epoll_wait非常高效,而且,通常情況下,即使我們需要監控百萬計的fd,但大多數情況下,一次也只返回少量準備就緒的fd而已。因此,每次呼叫epoll_wait,其僅需要從核心態複製少量的fd到使用者空間而已。那麼,這個準備就緒的list連結串列是怎麼維護的呢?過程如下:當我們執行epoll_ctl時,除了把fd放入到epoll檔案系統裡file物件對應的紅黑樹之外,還會給核心中斷處理程式註冊一個回撥函式,其告訴核心,如果這個fd的中斷到了,就把它放到準備就緒的list連結串列中。

如此,一棵紅黑樹、一張準備就緒的fd連結串列以及少量的核心cache,就幫我們解決了高併發下fd的處理問題。

總結一下:

  • 執行epoll_create時,建立了紅黑樹和就緒list連結串列;
  • 執行epoll_ctl時,如果增加fd,則檢查在紅黑樹中是否存在,存在則立即返回,不存在則新增到紅黑樹中,然後向核心註冊回撥函式,用於當中斷事件到來時向準備就緒的list連結串列中插入資料。
  • 執行epoll_wait時立即返回準備就緒連結串列裡的資料即可。

五、epoll原始碼分析

eventpoll_init過程:

static int __init eventpoll_init(void)
{
    int error;

    init_MUTEX(&epsem);

    /* Initialize the structure used to perform safe poll wait head wake ups */
    ep_poll_safewake_init(&psw);

    /* Allocates slab cache used to allocate "struct epitem" items */
    epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem),
            0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC,
            NULL, NULL);

    /* Allocates slab cache used to allocate "struct eppoll_entry" */
    pwq_cache = kmem_cache_create("eventpoll_pwq",
            sizeof(struct eppoll_entry), 0,
            EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL);

    /*
     * Register the virtual file system that will be the source of inodes
     * for the eventpoll files
     */
    error = register_filesystem(&eventpoll_fs_type);
    if (error)
        goto epanic;

    /* Mount the above commented virtual file system */
    eventpoll_mnt = kern_mount(&eventpoll_fs_type);
    error = PTR_ERR(eventpoll_mnt);
    if (IS_ERR(eventpoll_mnt))
        goto epanic;

    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: successfully initialized.
",
            current));
    return 0;

epanic:
    panic("eventpoll_init() failed
");
}

其中,epoll用slab分配器kmem_cache_create分配記憶體用於存放struct epitemstruct eppoll_entry

當向系統中新增一個fd時,就會建立一個epitem結構體,這是核心管理epoll的基本資料結構:

/*
* Each file descriptor added to the eventpoll interface will
* have an entry of this type linked to the hash.
*/
struct epitem {
    /* RB-Tree node used to link this structure to the eventpoll rb-tree */
    struct rb_node rbn;   // 用於主結構管理的紅黑樹

    /* List header used to link this structure to the eventpoll ready list */
    struct list_head rdllink;  // 事件就緒佇列

    /* The file descriptor information this item refers to */
    struct epoll_filefd ffd; // 用於主結構中的連結串列

    /* Number of active wait queue attached to poll operations */
    int nwait; // 事件個數

    /* List containing poll wait queues */
    struct list_head pwqlist; // 雙向連結串列,儲存著被監控檔案的等待佇列

    /* The "container" of this item */
    struct eventpoll *ep;  // 該項屬於哪個主結構體

    /* The structure that describe the interested events and the source fd */
    struct epoll_event event; // 註冊的感興趣的時間

    /*
     * Used to keep track of the usage count of the structure. This avoids
     * that the structure will desappear from underneath our processing.
     */
    atomic_t usecnt;

    /* List header used to link this item to the "struct file" items list */
    struct list_head fllink;

    /* List header used to link the item to the transfer list */
    struct list_head txlink;

    /*
     * This is used during the collection/transfer of events to userspace
     * to pin items empty events set.
     */
    unsigned int revents;
};

對於每個epfd,其對應的資料結構為:

/*
* This structure is stored inside the "private_data" member of the file
* structure and rapresent the main data sructure for the eventpoll
* interface.
*/
struct eventpoll {
    /* Protect the this structure access */
    rwlock_t lock;

    /*
     * This semaphore is used to ensure that files are not removed
     * while epoll is using them. This is read-held during the event
     * collection loop and it is write-held during the file cleanup
     * path, the epoll file exit code and the ctl operations.
     */
    struct rw_semaphore sem;

    /* Wait queue used by sys_epoll_wait() */
    wait_queue_head_t wq;

    /* Wait queue used by file->poll() */
    wait_queue_head_t poll_wait;

    /* List of ready file descriptors */
    struct list_head rdllist;  // 準備就緒的事件連結串列

    /* RB-Tree root used to store monitored fd structs */
    struct rb_root rbr;   // 用於管理所有fd的紅黑樹(根節點)
};

eventpoll在epoll_create時建立:

/*
* It opens an eventpoll file descriptor by suggesting a storage of "size"
* file descriptors. The size parameter is just an hint about how to size
* data structures. It won`t prevent the user to store more than "size"
* file descriptors inside the epoll interface. It is the kernel part of
* the userspace epoll_create(2).
*/
asmlinkage long sys_epoll_create(int size)
{
    int error, fd;
    struct eventpoll *ep;
    struct inode *inode;
    struct file *file;

    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d)
",
             current, size));

    /*
     * Sanity check on the size parameter, and create the internal data
     * structure ( "struct eventpoll" ).
     */
    error = -EINVAL;
    if (size <= 0 || (error = ep_alloc(&ep)) != 0)  // ep_alloc為eventpoll分配記憶體並初始化
        goto eexit_1;

    /*
     * Creates all the items needed to setup an eventpoll file. That is,
     * a file structure, and inode and a free file descriptor.
     */
    error = ep_getfd(&fd, &inode, &file, ep); // 建立於eventpoll相關的資料結構,包括file、inode和fd等資訊
    if (error)
        goto eexit_2;

    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d) = %d
",
             current, size, fd));

    return fd;

eexit_2:
    ep_free(ep);
    kfree(ep);
eexit_1:
    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d) = %d
",
             current, size, error));
    return error;
}

如上,核心中維護了一棵紅黑樹,大致結構如下:

red_black_tree.png

下面是epoll_ctl函式過程:

/*
* The following function implements the controller interface for
* the eventpoll file that enables the insertion/removal/change of
 * file descriptors inside the interest set.  It represents
* the kernel part of the user space epoll_ctl(2).
*/
asmlinkage long
sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event)
{
    int error;
    struct file *file, *tfile;
    struct eventpoll *ep;
    struct epitem *epi;
    struct epoll_event epds;

    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %p)
",
             current, epfd, op, fd, event));

    error = -EFAULT;
    if (ep_op_hash_event(op) &&
        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);  // 獲取epfd對應的檔案
    if (!file)
        goto eexit_1;

    /* Get the "struct file *" for the target file */
    tfile = fget(fd);  // 獲取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;

    down_write(&ep->sem);

    /* Try to lookup the file inside our hash table */
    epi = ep_find(ep, tfile, fd);  // 在雜湊表中查詢,防止重複新增

    error = -EINVAL;
    switch (op) {
    case EPOLL_CTL_ADD:  // 新增節點,呼叫ep_insert函式
        if (!epi) {
            epds.events |= POLLERR | POLLHUP;

            error = ep_insert(ep, &epds, tfile, fd);
        } else
            error = -EEXIST;
        break;
    case EPOLL_CTL_DEL:  // 刪除節點,呼叫ep_remove函式
        if (epi)
            error = ep_remove(ep, epi);
        else
            error = -ENOENT;
        break;
    case EPOLL_CTL_MOD:  // 修改節點,呼叫ep_modify函式
        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);

    up_write(&ep->sem);

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

    return error;
}

對於ep_insert函式,基本程式碼如下:

static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
             struct file *tfile, int fd)
{
    int error, revents, pwake = 0;
    unsigned long flags;
    struct epitem *epi;
    struct ep_pqueue epq;

    error = -ENOMEM;
    // 分配一個epitem結構體來儲存每個加入的fd
    if (!(epi = kmem_cache_alloc(epi_cache, SLAB_KERNEL)))
        goto eexit_1;

    /* Item initialization follow here ... */
    // 初始化結構體
    ep_rb_initnode(&epi->rbn);
    INIT_LIST_HEAD(&epi->rdllink);
    INIT_LIST_HEAD(&epi->fllink);
    INIT_LIST_HEAD(&epi->txlink);
    INIT_LIST_HEAD(&epi->pwqlist);
    epi->ep = ep;
    ep_set_ffd(&epi->ffd, tfile, fd);
    epi->event = *event;
    atomic_set(&epi->usecnt, 1);
    epi->nwait = 0;

    /* Initialize the poll table using the queue callback */
    epq.epi = epi;
    // 安裝poll回撥函式
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

    /*
     * Attach the item to the poll hooks and get current event bits.
     * We can safely use the file* here because its usage count has
     * been increased by the caller of this function.
     */
    // 將當前item新增至poll hook中,然後獲取當前event位
    revents = tfile->f_op->poll(tfile, &epq.pt);

    /*
     * We have to check if something went wrong during the poll wait queue
     * install process. Namely an allocation for a wait queue failed due
     * high memory pressure.
     */
    if (epi->nwait < 0)
        goto eexit_2;

    /* Add the current item to the list of active epoll hook for this file */
    spin_lock(&tfile->f_ep_lock);
    list_add_tail(&epi->fllink, &tfile->f_ep_links);
    spin_unlock(&tfile->f_ep_lock);

    /* We have to drop the new item inside our item list to keep track of it */
    write_lock_irqsave(&ep->lock, flags);

    /* Add the current item to the rb-tree */
    ep_rbtree_insert(ep, epi);

    /* If the file is already "ready" we drop it inside the ready list */
    if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
        list_add_tail(&epi->rdllink, &ep->rdllist);

        /* Notify waiting tasks that events are available */
        if (waitqueue_active(&ep->wq))
            wake_up(&ep->wq);
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }

    write_unlock_irqrestore(&ep->lock, flags);

    /* We have to call this outside the lock */
    if (pwake)
        ep_poll_safewake(&psw, &ep->poll_wait);

    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: ep_insert(%p, %p, %d)
",
             current, ep, tfile, fd));

    return 0;

eexit_2:
    ep_unregister_pollwait(ep, epi);

    /*
     * We need to do this because an event could have been arrived on some
     * allocated wait queue.
     */
    write_lock_irqsave(&ep->lock, flags);
    if (ep_is_linked(&epi->rdllink))
        ep_list_del(&epi->rdllink);
    write_unlock_irqrestore(&ep->lock, flags);

    kmem_cache_free(epi_cache, epi);
eexit_1:
    return error;
}

其中,init_poll_funcptrtfile->f_op->poll將ep_ptable_queue_proc註冊到epq.pt中的qproc中。

ep_ptable_queue_proc函式設定了等待佇列的ep_poll_callback回撥函式。在裝置硬體資料到來時,硬體中斷函式喚醒該等待佇列上等待的程式時,會呼叫喚醒函式ep_poll_callback。

ep_poll_callback函式主要的功能是將被監視檔案的等待事件就緒時,將檔案對應的epitem例項新增到就緒佇列中,當使用者呼叫epoll_wait時,核心會將就緒佇列中的事件報告給使用者。

epoll_wait的實現如下:

/*
* Implement the event wait interface for the eventpoll file. It is the kernel
* part of the user space epoll_wait(2).
*/
asmlinkage long sys_epoll_wait(int epfd, struct epoll_event __user *events,
                   int maxevents, int timeout)
{
    int error;
    struct file *file;
    struct eventpoll *ep;

    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_wait(%d, %p, %d, %d)
",
             current, epfd, events, maxevents, timeout));

    /* The maximum number of event must be greater than zero */
    if (maxevents <= 0 || maxevents > MAX_EVENTS) // 檢查maxevents引數
        return -EINVAL;

    /* Verify that the area passed by the user is writeable */
    // 檢查使用者空間傳入的events指向的記憶體是否可寫
    if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event))) {
        error = -EFAULT;
        goto eexit_1;
    }

    /* Get the "struct file *" for the eventpoll file */
    error = -EBADF;
    file = fget(epfd); // 獲取epfd對應的eventpoll檔案的file例項,file結構是在epoll_create中建立的
    if (!file)
        goto eexit_1;

    /*
     * We have to check that the file structure underneath the fd
     * the user passed to us _is_ an eventpoll file.
     */
    error = -EINVAL;
    if (!is_file_epoll(file))
        goto eexit_2;

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

    /* Time to fish for events ... */
    // 核心處理函式
    error = ep_poll(ep, events, maxevents, timeout);

eexit_2:
    fput(file);
eexit_1:
    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_wait(%d, %p, %d, %d) = %d
",
             current, epfd, events, maxevents, timeout, error));

    return error;
}

其中,呼叫ep_poll函式,具體流程如下:

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
           int maxevents, long timeout)
{
    int res, eavail;
    unsigned long flags;
    long jtimeout;
    wait_queue_t wait;

    /*
     * Calculate the timeout by checking for the "infinite" value ( -1 )
     * and the overflow condition. The passed timeout is in milliseconds,
     * that why (t * HZ) / 1000.
     */
    jtimeout = (timeout < 0 || timeout >= EP_MAX_MSTIMEO) ?
        MAX_SCHEDULE_TIMEOUT : (timeout * HZ + 999) / 1000;

retry:
    write_lock_irqsave(&ep->lock, flags);

    res = 0;
    if (list_empty(&ep->rdllist)) {
        /*
         * We don`t have any available event to return to the caller.
         * We need to sleep here, and we will be wake up by
         * ep_poll_callback() when events will become available.
         */
        init_waitqueue_entry(&wait, current);
        add_wait_queue(&ep->wq, &wait);

        for (;;) {
            /*
             * We don`t want to sleep if the ep_poll_callback() sends us
             * a wakeup in between. That`s why we set the task state
             * to TASK_INTERRUPTIBLE before doing the checks.
             */
            set_current_state(TASK_INTERRUPTIBLE);
            if (!list_empty(&ep->rdllist) || !jtimeout)
                break;
            if (signal_pending(current)) {
                res = -EINTR;
                break;
            }

            write_unlock_irqrestore(&ep->lock, flags);
            jtimeout = schedule_timeout(jtimeout);
            write_lock_irqsave(&ep->lock, flags);
        }
        remove_wait_queue(&ep->wq, &wait);

        set_current_state(TASK_RUNNING);
    }

    /* Is it worth to try to dig for events ? */
    eavail = !list_empty(&ep->rdllist);

    write_unlock_irqrestore(&ep->lock, flags);

    /*
     * Try to transfer events to user space. In case we get 0 events and
     * there`s still timeout left over, we go trying again in search of
     * more luck.
     */
    if (!res && eavail &&
        !(res = ep_events_transfer(ep, events, maxevents)) && jtimeout)
        goto retry;

    return res;
}

ep_send_events函式用於向使用者空間傳送就緒事件。ep_send_events函式將使用者傳入的記憶體簡單封裝到ep_send_events_data結構中,然後呼叫ep_scan_ready_list將就緒佇列中的事件傳入使用者空間的記憶體。

六、參考

Epoll詳解及原始碼分析——CSDN部落格


相關文章