epoll–原始碼剖析

you_De發表於2019-05-12

1.epoll_create()

在核心建立一個事件表,事件表用檔案表示。所以epoll_create()返回的是一個檔案描述符。主要原始碼:

asmlinkage long sys_epoll_create(int size)
{
    int error, fd;
    struct inode *inode; //inode結構
    struct file *file;  //檔案file結構

    if (size <= 0)
        goto eexit_1;

    error = ep_getfd(&fd, &inode, &file); //獲得fd以及一個file和inode結構
    if (error)
        goto eexit_1;
        
    error = ep_file_init(file);  //file的初始化
    if (error)
        goto eexit_2;

    return fd;

eexit_2:
    sys_close(fd);
eexit_1:

    return error;
}

(1)ep_getfd()

為epoll獲得相應file、inode等資料結構,並與新的fd繫結起來。

首先來看兩個資料結構:

  • a.file結構: 定義了Linux中檔案所需要的所有資訊,代表一個開啟的檔案

file的部分資料結構:

struct file {
    
     /* 檔案對應的目錄項結構。除了用filp->f_dentry->d_inode的方式來訪問索引節點結構之外,裝置驅動程式的開發者們一般無需關心dentry結構。
     */
    struct dentry        *f_dentry;
    
    /**
     * 與檔案相關的操作。核心在執行open操作時,對這個指標賦值,以後需要處理這些操作時就讀取這個指標。
     * 不能為了方便而儲存起來。也就是說,可以在任何需要的時候修改檔案的關聯操作。即"方法過載"。
     */
    struct file_operations    *f_op;


    /**
     * open系統呼叫在呼叫驅動程式的open方法前將這個指標置為NULL。驅動程式可以將這個欄位用於任何目的或者忽略這個欄位。
     * 驅動程式可以用這個欄位指向已分配的資料,但是一定要在核心銷燬file結構前在release方法中釋放記憶體。
     * 它是跨系統呼叫時儲存狀態的非常有用的資源。
     */
    void            *private_data;

};
  • inode結構:

inode在內部表示一個檔案,與file不同,file表示的是檔案描述符。同一個檔案被開啟多次,會有多個file檔案描述符,但是隻會有一個inode。在後面的程式碼中你可以發現,Linux針對epoll的操作專門會有一個屬於epoll的檔案系統,這個可以在初始化inode的時候可以看到。

這是ep_getfd的程式碼:

static int ep_getfd(int *efd, struct inode **einode, struct file **efile)
{
    struct qstr this;
    char name[32];
    struct dentry *dentry;
    struct inode *inode;
    struct file *file;
    int error, fd;


    error = -ENFILE;
    file = get_empty_filp(); //獲得新的空的file檔案描述符
    if (!file)
        goto eexit_1;

    inode = ep_eventpoll_inode();//獲得新的inode結構,代表一個屬於epoll檔案系統的檔案。
    error = PTR_ERR(inode);
    if (IS_ERR(inode))
        goto eexit_2;

    error = get_unused_fd(); //獲得一個未使用的fd
    if (error < 0)
        goto eexit_3;
    fd = error;


    error = -ENOMEM;
    sprintf(name, "[%lu]", inode->i_ino);
    this.name = name;
    this.len = strlen(name);
    this.hash = inode->i_ino;
    dentry = d_alloc(eventpoll_mnt->mnt_sb->s_root, &this);//獲得一個新的dentry檔案目錄項結構並初始化
    if (!dentry)
        goto eexit_4;
    dentry->d_op = &eventpollfs_dentry_operations;
    d_add(dentry, inode);//將inode和目錄項結構繫結
    file->f_vfsmnt = mntget(eventpoll_mnt); //把該檔案所屬的檔案系統置為epoll檔案系統
    file->f_dentry = dentry;
    file->f_mapping = inode->i_mapping;

    file->f_pos = 0;
    file->f_flags = O_RDONLY;
    file->f_op = &eventpoll_fops; //這一步是很重要的一步,用f_op指標指向epoll的回撥數
    file->f_mode = FMODE_READ;
    file->f_version = 0;
    file->private_data = NULL;

    fd_install(fd, file);

    *efd = fd;
    *einode = inode;
    *efile = file;
    return 0;

eexit_4:
    put_unused_fd(fd);
eexit_3:
    iput(inode);
eexit_2:
    put_filp(file);
eexit_1:
    return error;
}

ep_getfd其實就做了三件事:獲取新的file檔案描述符,獲取新的inode結構,獲得新的fd,最後把三者連線繫結在一起,就表示了epoll在核心的事件表。

(2) ep_file_init(file)

前面講到epoll的核心事件表已經建立完畢了,但是我們可以發現epoll是事件一次寫入核心,多次監聽的,然而在之前都沒有發現可以存事件的資料結構紅黑樹。所以這一步ep_file_init就是來解決這個問題的。

下面根據程式碼來說它幹了些什麼

static int ep_file_init(struct file *file)
{
    struct eventpoll *ep;  //一個指向eventpoll的指標

    if (!(ep = kmalloc(sizeof(struct eventpoll), GFP_KERNEL)))
        return -ENOMEM;

    /*下面是對eventpoll結構的一系列初始化*/
    memset(ep, 0, sizeof(*ep));
    rwlock_init(&ep->lock);
    init_rwsem(&ep->sem);
    init_waitqueue_head(&ep->wq);
    init_waitqueue_head(&ep->poll_wait);
    INIT_LIST_HEAD(&ep->rdllist);
    ep->rbr = RB_ROOT; //這裡可以看到紅黑樹的root的初始化

    file->private_data = ep;    //ep指標給file

    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: ep_file_init() ep=%p
",
             current, ep));
    return 0;
}

這一步就是把一個eventpoll的結構建立並初始化,然後讓file->private_data指向這個結構,這個結構中我們就可以找到rb_root,這一步之後epoll_creat()也就是epoll所有的準備工作就已經做完了。

2.epoll_ctl()

因為epoll對監聽事件來說是一次寫入多次監聽的,所以必須要有對事件表的增刪改操作介面,epoll_ctl就是提供給使用者的可以進行事件表進行操作的介面。我們可以通過這個系統呼叫來新增刪除和修改事件。

下面根據原始碼來走一遍:

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;


    error = -EFAULT;
    if (EP_OP_HASH_EVENT(op) &&
        copy_from_user(&epds, event, sizeof(struct epoll_event)))
        goto eexit_1;//把使用者的事件從使用者空間考到核心空間


    error = -EBADF;
    file = fget(epfd); //獲得epoll核心事件表得檔案描述符
    if (!file)
        goto eexit_1;

    tfile = fget(fd); //獲得要操作的事件得檔案描述符
    if (!tfile)
        goto eexit_2;


    error = -EPERM;
    if (!tfile->f_op || !tfile->f_op->poll)
        goto eexit_3; //如果fd檔案描述符中f_op(指向所有檔案操作指標得結構體)和這裡需要用到的poll操作為空,就退出。因為poll是所有Io複用:select 、poll、epoll得底層實現。

    
    error = -EINVAL;
    if (file == tfile || !IS_FILE_EPOLL(file))
        goto eexit_3;   //判斷需要操作的fd檔案描述符是不是epfd檔案描述符和 file是不是epoll檔案系統的檔案

    
    ep = file->private_data;  //拿到核心事件表的eventpoll結構體

    down_write(&ep->sem); //為寫獲得讀寫核心事件表eventpoll的訊號量。

    
    epi = ep_find(ep, tfile, fd);//判斷該事件在核心事件表中是否存在

    error = -EINVAL;
    switch (op) {
    case EPOLL_CTL_ADD:  //新增事件操作
        if (!epi) {
            epds.events |= POLLERR | POLLHUP;

            error = ep_insert(ep, &epds, tfile, fd);
        } 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;
    }

    
    if (epi)
        ep_release_epitem(epi);

    up_write(&ep->sem); //釋放核心事件表eventpoll的讀寫訊號量

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;
}

在epoll_ctl中,主要分為以下幾個步驟:

  • 判斷需要操作的事件在事件表中是否存在
  • 判斷需要進行的操作
  • 使用更底層的程式碼對事件表進行增刪改

epoll比poll和select高效的地方,其實在這裡就可以 看出來,poll的系統呼叫會把該程式“掛”到fd所對應的裝置的等待佇列上,select和poll他們每一次都需要把current “掛”到fd對應裝置的等待佇列,同一個fd來說,你要不停監聽的話,你就需要不停的進行poll,那麼fd太多的話,這樣的poll操作其實是很低效的,epoll則不同的是,每一個事件,它只在新增的時候呼叫poll,以後都不用去呼叫poll,避免了太多重複的poll操作,所以epoll對於poll和select來說是更高效的。
還有一個比較有趣的地方是,所有的事件的儲存都是在紅黑樹裡面,但是我們可以發現紅黑樹的節點其實是這樣的:

struct rb_node
{
    /**
     * 紅黑樹節點的雙親。
     */
    struct rb_node *rb_parent;
    /**
     * 紅黑樹節點顏色。
     */
    int rb_color;
#define    RB_RED        0
#define    RB_BLACK    1
    /**
     * 右孩子
     */
    struct rb_node *rb_right;
    /**
     * 左孩子
     */
    struct rb_node *rb_left;
};

struct rb_root
{
    struct rb_node *rb_node;
};

不管是根節點還是節點,你都看不到資料,那核心是怎麼通過紅黑樹運算元據?

struct epitem {
    
    struct rb_node rbn;
    ........
    }

其實這才是真正的紅黑樹的節點,裡面的東西很多,但是這裡只列出來了相關的。一個fd對應一個這樣的結構,同一個fd只能插入一次,這也是要採用一個紅黑樹這樣的資料結構的其中一個原因,還有一個原因就是因為紅黑樹的查詢等操作效率高。那麼從rb_node到struct epitem就可以很容易的做到了,只需要做一個指標的強轉就可以從rb_node到epitem:


epic = rb_entry(parent, struct epitem, rbn);


#define    rb_entry(ptr, type, member) container_of(ptr, type, member)


#define container_of(ptr, type, member) ({            
        const typeof( ((type *)0)->member ) *__mptr = (ptr);    
        (type *)( (char *)__mptr - offsetof(type,member) );})

這也就跟C++的模板類似,只不過這是C語言實現的,很多方法還是需要再重新定義。但是對C語言來說,在有限的條件下做到這樣的程式碼複用還是非常厲害的。

3.epoll_wait()

之前的兩個函式已經把事件新增到核心事件表,而且已經把當前程式“掛”到fd的所有裝置上,這就相當於一個回撥函式,當對應fd有可處理事件時,就會喚醒等待佇列的程式,程式會把當前可處理的事件及有關資訊記錄到一個rdllist的連結串列中,接下來就是epoll_wait所要做的事了:

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;

    /*檢查超時時間是否有效*/
    jtimeout = timeout == -1 || timeout > (MAX_SCHEDULE_TIMEOUT - 1000) / HZ ?
        MAX_SCHEDULE_TIMEOUT: (timeout * HZ + 999) / 1000;

retry:
    /*獲得eventpoll的寫鎖*/
    write_lock_irqsave(&ep->lock, flags);


    /*rdllist如果是空就阻塞迴圈等待回撥函式往rdllist中寫資料,一旦不為空或者超過超時時間就會退出迴圈*/
    res = 0;
    if (list_empty(&ep->rdllist)) {
        
        init_waitqueue_entry(&wait, current);
        add_wait_queue(&ep->wq, &wait);

        for (;;) {
        
            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);
    }

    /*判斷rdllist是否為空*/
    eavail = !list_empty(&ep->rdllist); 

    /*釋放eventpoll的寫鎖*/
    write_unlock_irqrestore(&ep->lock, flags);


    /*這一步是把核心的rdllist中的事件copy到使用者空間*/
    if (!res && eavail &&
        !(res = ep_events_transfer(ep, events, maxevents)) && jtimeout)
        goto retry;

    return res;
}

簡單總結一下epoll_wait:

  • 檢查超時事件和用來存就緒事件的使用者空間的大小
  • 迴圈等待就緒事件
  • 把就緒事件從核心空間copy到使用者空間

這就是epoll的整個流程和主要的步驟的分析。但是我們沒能看出來它的ET和LT工作模式體現在哪裡?所以下面就來說說epoll的ET模式具體實現。

4.ET模式

ET模式是epoll特有的高效工作模式。主要體現就是一次就緒事件只給使用者提醒一次。ET模式的實現其實就是在epoll_wait的最後一步,從核心連結串列往使用者空間考資料的時候,下面來看程式碼:

static int ep_events_transfer(struct eventpoll *ep,
                  struct epoll_event __user *events, int maxevents)
{
    int eventcnt = 0;
    struct list_head txlist;  //建立一個臨時量,用以儲存就緒的事件

    INIT_LIST_HEAD(&txlist); //初始化

    
    down_read(&ep->sem);  //獲得ep的讀寫訊號量


    /*從ep->rdllist中copy到txlist*/
    if (ep_collect_ready_items(ep, &txlist, maxevents) > 0) {
        /*從txlist到使用者空間*/
        eventcnt = ep_send_events(ep, &txlist, events);

        /*從txlist再反過來copy給ep->rdllist,這一步是具體的ET實現*/
        ep_reinject_items(ep, &txlist);
    }

    up_read(&ep->sem); //釋放ep讀寫訊號量

    return eventcnt;
}

下面的函式就是ET的具體實現,也是ET和LT的區別

static void ep_reinject_items(struct eventpoll *ep, struct list_head *txlist)
{
    int ricnt = 0, pwake = 0;
    unsigned long flags;
    struct epitem *epi;

    /*獲得ep的讀寫鎖*/
    write_lock_irqsave(&ep->lock, flags);

    while (!list_empty(txlist)) {
        /*這一步跟之前講過的rb_node到epi的一步是一樣的*/
        epi = list_entry(txlist->next, struct epitem, txlink);
        
        /*初始化*/
        EP_LIST_DEL(&epi->txlink);

        /*
        1.核心事件表(紅黑樹)不為空
        2.事件沒有設定ET工作模式
        3.就緒事件型別和監聽事件型別相同
        4.該事件的rdllink不為空
        */
        if (EP_RB_LINKED(&epi->rbn) && !(epi->event.events & EPOLLET) &&
            (epi->revents & epi->event.events) && !EP_IS_LINKED(&epi->rdllink)) {
        /*把剛剛臨時量txlist中的該事件繼續新增到rdllist中*/
            list_add_tail(&epi->rdllink, &ep->rdllist);
            ricnt++;
        }
    }

    if (ricnt) {
    
        if (waitqueue_active(&ep->wq))
            wake_up(&ep->wq);
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }

    /*釋放讀寫鎖*/
    write_unlock_irqrestore(&ep->lock, flags);


    if (pwake)
        ep_poll_safewake(&psw, &ep->poll_wait);
}

先說LT模式對於同一個就緒事件會重複提醒,從上面可以看出來是因為它又把依舊就緒且未設定ET標誌的事件重新copy到了rdllist中,所以下一次epoll_wait還是會把該事件返回給使用者。那麼ET這裡就很好解釋了,儘管該事件未處理完,但是你只要設定了ET標誌,我就不會再次把該事件返回給使用者。這就是ET的實現。

相關文章