(九)Java網(wǎng)絡(luò)編程之IO模型篇-內(nèi)核Select、Poll、Epoll多路復(fù)用函數(shù)源碼深度歷險(xiǎn)(下)!

五、多路復(fù)用函數(shù) - poll()

? ?poll函數(shù)則是基于select函數(shù)創(chuàng)造出來的,其實(shí)它和select的區(qū)別不大,唯一一點(diǎn)區(qū)別就在于:核心結(jié)構(gòu)不同了,在poll中出現(xiàn)了一種新的結(jié)構(gòu)體pollfd,它不存在最大數(shù)量的限制。但其實(shí)poll的性能與select差距是不大的,因此可以將poll理解成增強(qiáng)版select

5.1、poll()函數(shù)的定義

poll的定義也和select相差不大,準(zhǔn)確來說,所有的多路復(fù)用函數(shù)定義都差不多,如下:

int poll(struct pollfd* fds, nfds_t nfds, int timeout);

相較之前的select函數(shù),poll的入?yún)⑸倭藘蓚€,這是因?yàn)樵谄渲袑⒔Y(jié)構(gòu)體優(yōu)化成了pollfd,關(guān)于這點(diǎn)待會兒聊。先看看三個入?yún)ⅲ?/p>

  • fds:這是由pollfd組成的數(shù)組,數(shù)組每個元素表示要監(jiān)聽的文件描述符及相應(yīng)的事件。
  • nfds:這里表示數(shù)組中一共傳了多少個元素。
  • timeout:這個參數(shù)很好理解,表示未獲取到就緒事件時,允許的等待時間。

然后再看看這個函數(shù)的返回,也是一個int值,和select函數(shù)的返回值含義相同,也包括后續(xù)會分析的epoll,返回值也是int類型,其含義也都一樣。

5.2、poll()的核心結(jié)構(gòu)體:pollfd

緊接著再來看看poll的結(jié)構(gòu)體:pollfd,其定義如下:

struct pollfd{
  int fd;
  short events;
  short revents;
};

這個結(jié)構(gòu)體中有三個成員,分別對應(yīng)著:文件描述符、需要監(jiān)聽的事件以及觸發(fā)的事件,其中fd、events是在調(diào)用時就需傳入的,而revents則是由內(nèi)核監(jiān)聽到事件觸發(fā)后填充的。

5.3、poll()底層源碼分析

? ?了解了pollfd結(jié)構(gòu)之后,對于poll()的使用方式和select大致相同,所以不再舉例說明,接下來再看看poll()的源碼過程,其實(shí)過程也大致與select相似,并且其實(shí)現(xiàn)也同樣位于select.c文件中,執(zhí)行流程如下:

sys_poll → SYSCALL_DEFINE3 → do_sys_poll → do_poll → f_op->poll

整個過程中,最核心的就是do_poll()函數(shù),但先來看看前面的函數(shù)實(shí)現(xiàn):

// 位于fs/select.c文件中(sys_select函數(shù))
SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigend int, 
    nfds, long, timeout_msesc)
{
    struct timespec end_time, *to = NULL;
    int ret;
    // 判斷是否傳入了超時時間,如果傳入了則進(jìn)行相應(yīng)的超時處理
    if (timeout_msesc > 0) {
        to = &end_time;
        poll_select_timeout(to, timeout_msesc / MSEC_PER_SEC,
            NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));
    }
       
    // 調(diào)用最為核心的 do_sys_poll()函數(shù)
    ret = do_sys_poll(n, inp, outp, exp, to);
    
    if (ret == -EINTR) {
        struct restart_block *restart_block;
        
        restart_block = &current_thread_info()->restart_block;
        restart_block->fn = do_restart_poll;
        restart_block->poll.ufds = ufds;
        restart_block->poll.nfds = nfds;
        
        if (timeout_msesc >= 0) {
            restart_block->poll.tv_sec = end_time.tv_sec;
            restart_block->poll.tv_nsec = end_time.tv_nsec;
            restart_block->poll.has_timeout = 1;
        } else 
            restart_block->poll.has_timeout = 0;
    
        ret = -ERESTART_REESTARTBLOCK;
    }
    return ret;
}

這個函數(shù)僅是過渡的作用,稍微做了一些輔助工作,然后就直接調(diào)用了do_sys_poll,那么再來看看這個,源碼實(shí)現(xiàn)如下:

int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
        struct timespec *end_time)
{   
    // 這里依舊用到了poll_wqueues結(jié)構(gòu)
    struct poll_wqueues table;
    int err = -EFAULT, fdcount, len, size;
    // 這里和select相同,采用棧方式存儲用戶態(tài)傳遞的數(shù)據(jù)
    // 同時在這里是基于long做了對齊填充的,能夠充分利用局部性原理
    long stack_pps[POLL_STACK_ALLOC/sizeof(long)];
    // poll_list是新的結(jié)構(gòu)體:本質(zhì)上是一個單向鏈表
    struct poll_list *const head = (struct poll_list *)stack_pps;
    struct poll_list *walk = head; // 定義鏈表頭結(jié)點(diǎn)
    unsigned long todo = nfds;

    if (nfds > current->signal->rlim[RLIMIT_NOFILE].rlim_cur)
        return -EINVAL;

    len = min_t(unsigned int, nfds, N_STACK_PPS);
    // 將用戶空間傳入的所有FD,以鏈表的形式填充在poll_list中
    for (;;) {
        walk->next = NULL;
        walk->len = len;
        if (!len)
            break;
        // 將用戶態(tài)傳遞的數(shù)據(jù)拷貝到內(nèi)核空間
        if (copy_from_user(walk->entries, ufds + nfds-todo,
                    sizeof(struct pollfd) * walk->len))
            goto out_fds;

        todo -= walk->len;
        if (!todo)
            break;
        
        // 在這里如若空間不夠,也會調(diào)用kmalloc分配更大的空間
        len = min(todo, POLLFD_PER_PAGE);
        size = sizeof(struct poll_list) + sizeof(struct pollfd) * len;
        walk = walk->next = kmalloc(size, GFP_KERNEL);
        if (!walk) {
            err = -ENOMEM;
            goto out_fds;
        }
    }
    // 這里依舊調(diào)用了poll_initwait函數(shù)做了初始化工作
    poll_initwait(&table);
    // 然后這里是重點(diǎn):調(diào)用了do_poll()函數(shù)對FD做監(jiān)聽
    fdcount = do_poll(nfds, head, &table, end_time);
    // 善后工作:清空各設(shè)備等待隊(duì)列上的節(jié)點(diǎn)信息
    poll_freewait(&table);
    
    for (walk = head; walk; walk = walk->next) {
        struct pollfd *fds = walk->entries;
        int j;
        // 這是最后的工作:將監(jiān)聽到的事件填充到revents中,
        // 然后通過__put_user寫回用戶態(tài)空間,最后利用goto跳轉(zhuǎn)返回
        for (j = 0; j < walk->len; j++, ufds++)
            if (__put_user(fds[j].revents, &ufds->revents))
                goto out_fds;
    }

    err = fdcount;
out_fds:
    walk = head->next;
    while (walk) {
        struct poll_list *pos = walk;
        walk = walk->next;
        kfree(pos);
    }

    return err;
}

在這里面,其實(shí)就是將之前的do_select()函數(shù)的工作拆開了,拆為了do_sys_poll、do_poll兩個函數(shù)實(shí)現(xiàn),其他過程大致與do_select函數(shù)相同,不同點(diǎn)在于這里面又出現(xiàn)了一個新的結(jié)構(gòu)體:poll_list,定義如下:

struct poll_list {
    struct poll_list *next;
    int len;
    struct pollfd entries[0];
};

從上述結(jié)構(gòu)體的成員很明顯就可看出,這是一個典型的單向的數(shù)組鏈表結(jié)構(gòu),第一個成員代表下一個節(jié)點(diǎn)(數(shù)組鏈表)是誰,第二個成員代表后面可變長數(shù)組的元素?cái)?shù)量,第三個成員則是一個變長數(shù)組,里面存放當(dāng)前這段內(nèi)存上的pollfd

對于這個變長數(shù)組大家會存在些許疑惑,明明上面定義的長度為[0],為何可以變長呢?這是利用到了C語言里的數(shù)組拓展技術(shù),感興趣的可點(diǎn)擊>>這里<<詳細(xì)了解。

同時,對于“數(shù)組鏈表結(jié)構(gòu)”大家可能有些許疑惑,鏈表、數(shù)組這是兩個結(jié)構(gòu),為何會被組合在一塊呢?這是由于poll中,會先采用棧上分配的方式存儲pollfd,但是當(dāng)用戶態(tài)傳入的pollfd過多時,棧上內(nèi)存可能不太夠用,因此就會調(diào)用kmalloc分配新的內(nèi)存,而前面分析select時提過:kmalloc分配的新空間是基于堆內(nèi)存的,所以此時poll就會同時使用多塊內(nèi)存,示意圖如下:

poll_list

也就是說:如果棧上能存儲用戶空間傳遞的pollfd,那么只會出現(xiàn)一個poll_list在棧上,如果存儲不下則會有多個,除開第一個數(shù)組外,其他的都在堆上,因此poll_list結(jié)構(gòu)中的指針會指向另外一個數(shù)組。

OK~,弄明白了poll_list結(jié)構(gòu)體后,對于do_sys_poll函數(shù)的執(zhí)行流程就不再重復(fù)了,大家可參考我源碼中給出的備注,下面直入主題,一起來看看do_poll()函數(shù)會做什么工作:

static int do_poll(unsigned int nfds,  struct poll_list *list,
           struct poll_wqueues *wait, struct timespec *end_time)
{   
    // 在這里會注冊等待阻塞時的回調(diào)函數(shù)
    poll_table* pt = &wait->pt;
    ktime_t expire, *to = NULL;
    int timed_out = 0, count = 0;
    unsigned long slack = 0;

    // 處理超時時間
    if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
        pt = NULL;
        timed_out = 1;
    }
    if (end_time && !timed_out)
        slack = estimate_accuracy(end_time);
    
    // 開啟輪詢:一直監(jiān)聽所有的pollfd
    for (;;) {
        struct poll_list *walk;

        set_current_state(TASK_INTERRUPTIBLE);
        // 外層循環(huán):遍歷所有的poll_list
        for (walk = list; walk != NULL; walk = walk->next) {
            struct pollfd * pfd, * pfd_end;
            pfd = walk->entries;
            pfd_end = pfd + walk->len;
            // 內(nèi)存循環(huán):遍歷poll_list.entries數(shù)組中的所有pollfd
            for (; pfd != pfd_end; pfd++) {
                // 對于每個pollfd對應(yīng)的驅(qū)動的poll()
                if (do_pollfd(pfd, pt)) {
                    // 返回值不為0,表示當(dāng)前FD有數(shù)據(jù)可讀寫,count++
                    count++;
                    pt = NULL;
                }
            }
        }
        // 這里是防止下次循環(huán)時再次注冊等待隊(duì)列
        pt = NULL;
        if (!count) {
            count = wait->error;
            if (signal_pending(current))
                count = -EINTR;
        }
        if (count || timed_out)
            break;

        // 在第一次循環(huán)時,如果設(shè)置了超時時間,那么做一次轉(zhuǎn)換
        if (end_time && !to) {
            expire = timespec_to_ktime(*end_time);
            to = &expire;
        }
        
        // 如若沒有FD出現(xiàn)讀寫事件,則讓當(dāng)前進(jìn)程陷入睡眠阻塞狀態(tài)
        if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
            timed_out = 1;
    }
    __set_current_state(TASK_RUNNING);
    // 最終返回掃描出的活躍FD數(shù)量
    return count;
}

其實(shí)在這個過程中,無非就是開啟輪詢對之前的poll_list進(jìn)行遍歷,然后會對每個pollfd調(diào)用do_pollfd函數(shù),就是檢測每個FD上數(shù)據(jù)是否可讀寫,如果所有pollfd都遍歷完成后,依舊沒有發(fā)現(xiàn)可讀寫的FD,則讓當(dāng)前進(jìn)程睡眠阻塞,由于在函數(shù)最開始也設(shè)置了回調(diào)函數(shù),因此當(dāng)某個FD數(shù)據(jù)準(zhǔn)備就緒后,會由對應(yīng)的驅(qū)動程序喚醒poll。最后再把do_pollfd函數(shù)的實(shí)現(xiàn)放出來:

static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait)
{
    // 定義了一個mask接收FD的可讀寫狀態(tài)
    unsigned int mask;
    int fd;

    mask = 0;
    fd = pollfd->fd;
    if (fd >= 0) {
        int fput_needed;
        struct file * file;

        file = fget_light(fd, &fput_needed);
        mask = POLLNVAL;
        if (file != NULL) {
            mask = DEFAULT_POLLMASK;
            // 在這里又再次對每個FD的poll進(jìn)行了調(diào)用
            if (file->f_op && file->f_op->poll)
                // 這行代碼與之前select函數(shù)的相同
                mask = file->f_op->poll(file, pwait);
            /* Mask out unneeded events. */
            mask &= pollfd->events | POLLERR | POLLHUP;
            fput_light(file, fput_needed);
        }
    }
    pollfd->revents = mask;
    return mask;
}

這個函數(shù)的工作不出所料,的確就是對每個FD進(jìn)行了詢問:“你的數(shù)據(jù)可不可以讓我進(jìn)行讀寫操作呀”?剩下的工作與select函數(shù)后面的過程相同,因此不再繼續(xù)分析,想要加深印象的再跳回select最后那段分析即可。

5.4、poll()總結(jié)與劣勢淺談

? ?到目前為止,關(guān)于多路復(fù)用模型中的poll()函數(shù)也分析明白了,其實(shí)有了select函數(shù)的基礎(chǔ)后,對于poll而言,看起來相信應(yīng)該是十分輕松的。當(dāng)然,由于poll()函數(shù)的實(shí)現(xiàn)和select大致是相同的,因此也不再花費(fèi)時間去對它進(jìn)行總結(jié)。

相較于select而言,由于poll內(nèi)部是基于數(shù)組鏈表構(gòu)建的,所以沒有select位圖的限制,也就解決了select中最多只能監(jiān)聽1024個連接的缺陷。同時由于內(nèi)核返回監(jiān)聽到的事件時,是通過pollfd.revents進(jìn)行傳遞的,因此pollfd是可以被重用的,在下次使用時將pollfd.revents置零即可。但對于其他兩點(diǎn)缺陷,在poll中也依舊存在,不過在epoll中卻得到了解決,所以接下來重點(diǎn)分析epoll實(shí)現(xiàn)。

六、多路復(fù)用函數(shù) - epoll()

? ?epoll也是IO多路復(fù)用模型中最重要的函數(shù),幾乎目前絕大部分的高性能框架,都是基于它構(gòu)建的,例如Nginx、Redis、Netty等,所以對于掌握epoll知識的必要性顯得越發(fā)重要。因?yàn)樵谀憷斫?code>epoll之前,你只知道這些技術(shù)棧性能很高,但不清楚為什么,而你理解epoll之后,對于這些技術(shù)棧性能高的原因也就自然就懂了,那接下來我們一起聊聊epoll

? ?epoll與之前的select、poll函數(shù)不同,它整個過程由epoll_create、epoll_ctl、epoll_wait三個函數(shù)組成,同時最關(guān)鍵的點(diǎn)也在于:epoll直接在內(nèi)核中維護(hù)著一個FD集合,外部不再需要將整個要監(jiān)聽的FD集合拷貝到內(nèi)核了,而是調(diào)用epoll_ctl函數(shù)進(jìn)行管理即可。

對于Java-NIO中的JNI入口,和之前分析的思路相同,因此就不再進(jìn)行演示,感興趣的自己根據(jù)執(zhí)行流程,打開相應(yīng)的目錄文件就可以看到。

接著重點(diǎn)看看epoll系列的函數(shù)定義。

6.1、epoll函數(shù)定義

? ?剛剛聊到過,epoll存在三個函數(shù),它們的定義都位于sys/epoll.h文件中,那么接下來一個個瞧瞧,先看看epoll_create

int epoll_create(int size);
int epoll_create1(int flags);

你沒看錯,create函數(shù)其實(shí)有兩個,但對于入?yún)?code>size的函數(shù)在很早之前就被棄用了,因此一般調(diào)用create函數(shù)都是在調(diào)用epoll_create1(),這個函數(shù)的作用是申請內(nèi)核創(chuàng)建一個epollfd文件,同時申請一個eventpoll結(jié)構(gòu)體(稍后講),然后返回epollfd對應(yīng)的文件描述符。最后再聊聊它的入?yún)ⅲ?/p>

  • size:代表指定內(nèi)核中維護(hù)的FD集合長度,2.6.8版本之后成為了動態(tài)集合,被棄用。
  • flags:這個參數(shù)主要有兩個傳遞值:
    • 0:正常創(chuàng)建epollfd傳入的值。
    • EPOLL_CLOEXEC:當(dāng)fork子進(jìn)程時,子進(jìn)程不會包含epollfd(多進(jìn)程epoll時使用)。

了解了create函數(shù)后,再來看看epoll_ctl函數(shù)的定義:

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

先來說說ctl函數(shù)的作用吧,這個函數(shù)主要就是對于內(nèi)核維護(hù)的epollfd集合進(jìn)行增刪改操作,參數(shù)釋義如下:

  • epfd:表示指定要操作的epollfd
  • op:表示當(dāng)前要進(jìn)行的操作,選項(xiàng)如下:
    • EPOLL_CTL_ADD:注冊操作,代表要往內(nèi)核維護(hù)的集合中新增一個epollfd
    • EPOLL_CTL_MOD:修改操作,代表要更改某個epollfd所對應(yīng)的事件。
    • EPOLL_CTL_DEL:刪除操作,代表要哦承諾內(nèi)核的集合中移除一個epollfd
  • fd:表示epollfd對應(yīng)的文件描述符。
  • event:表示當(dāng)前描述符的事件隊(duì)列。

最后看看epoll_wait函數(shù)的定義:

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

這個函數(shù)的作用就類似于之前的select、poll,調(diào)用之后會阻塞等待至I/O事件發(fā)生,參數(shù)釋義如下:

  • epfd:表示一個等待事件發(fā)生的epollfd
  • evlist:這里用于接收內(nèi)核已監(jiān)聽到的事件集合。
  • maxevents:指上述集合出現(xiàn)就緒事件時,一次能夠拷貝的最大長度。
    • 如果上述集合中的就緒事件小于該值,則一次性全部拷貝過來。
    • 如果上述集合中的就緒事件大于該值,則一次最多拷貝maxevents個事件。
  • timeout:這個參數(shù)和之前的select、poll相同,指定超時時間。

OK,簡單了解三個函數(shù)后,大家需牢記的一點(diǎn)是:這三個函數(shù)都是配套使用的,遵循上述的順序,以create、ctl、wait這種方式依次進(jìn)行調(diào)用,然后就能對一或多個文件描述符進(jìn)行監(jiān)聽。當(dāng)然,對于調(diào)用后究竟發(fā)生了什么?我們接下來通過源碼的方式去揭開面紗。

6.2、epoll的核心結(jié)構(gòu)體

? ?在epoll中存在兩個核心結(jié)構(gòu)體:epoll_event、eventpoll,這兩個結(jié)構(gòu)體貫穿了epoll整個流程,這里先簡單看看它們的定義:

struct epoll_event
{
  // epoll注冊的事件
  uint32_t events; 
  // 這個可以理解成epoll要監(jiān)聽的FD詳細(xì)結(jié)構(gòu)體
  epoll_data_t data;
} __attribute__ ((__packed__));

// 上述data成員的結(jié)構(gòu)定義
typedef union epoll_data
{
    // 自定義的附帶信息,一般傳事件的回調(diào)函數(shù),當(dāng)事件發(fā)生時,
    // 通過回調(diào)函數(shù)將事件添加到list上(Java-Linux-AIO的實(shí)現(xiàn)原型)
    void *ptr;
    // 要監(jiān)聽的描述符對應(yīng)
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

上述的epoll_event簡單來說,可以將其理解成由“文件描述符-需要監(jiān)聽的事件”組成的鍵值對結(jié)構(gòu),其中data是文件描述符的詳細(xì)結(jié)構(gòu)(可以理解成對應(yīng)著一個FD),而events則代表著該FD需要監(jiān)聽的事件,這些事件是在調(diào)用epoll_ctl函數(shù)時,由用戶態(tài)程序指定的,主要有下述一些事件項(xiàng):

  • EPOLLIN:表示文件描述符可讀。
  • EPOLLOUT:表示文件描述符可寫。
  • EPOLLPRI:表示文件描述符有帶外數(shù)據(jù)可讀。
  • EPOLLERR:表示文件描述符發(fā)生錯誤。
  • EPOLLHUP:表示文件描述符被掛斷。
  • EPOLLET:將 EPOLL 設(shè)為邊緣觸發(fā)(Edge Trigger)模式(后續(xù)分析)。
  • EPOLLONESHOT:表示對這個文件描述符只監(jiān)聽一次事件。

簡單有個概念之后,再來看看另外一個核心結(jié)構(gòu)體eventpoll

struct eventpoll {
    // 這個是一把自旋鎖(多線程Epoll時使用)
    spinlock_t lock;
    // 這個是一把互斥鎖(多線程Epoll時使用)
    // 添加、修改、刪除、監(jiān)聽、返回時都會使用這把鎖確保線程安全
    struct mutex mtx;
    // 調(diào)用epoll_wait()時, 會在這個等待隊(duì)列上休眠阻塞
    wait_queue_head_t wq;
    // 這個是用于epollfd本身被poll時使用(一般用不上)
    wait_queue_head_t poll_wait;
    // 存儲所有I/O事件已經(jīng)就緒的FD鏈表
    struct list_head rdllist;
    // 紅黑樹結(jié)構(gòu):存放所有需要監(jiān)聽的節(jié)點(diǎn)
    struct rb_root rbr;
    // 一個連接著所有樹節(jié)點(diǎn)的單向鏈表
    struct epitem *ovflist;
    // 這里保存一些用戶變量, 如fd監(jiān)聽數(shù)量的最大值等
    struct user_struct *user;
};

struct epitem {
    // 紅黑樹節(jié)點(diǎn)(red_black_node的縮寫)
    struct rb_node rbn;
    // 鏈表節(jié)點(diǎn),方便存儲到eventpoll.rdllist中
    struct list_head rdllink;
    // 下一個節(jié)點(diǎn)指針
    struct epitem *next;
    // 當(dāng)前epitem對應(yīng)的fd
    struct epoll_filefd ffd;
    // 這兩個不太懂,似乎跟等待隊(duì)列有關(guān)
    int nwait;
    struct list_head pwqlist;
    // 當(dāng)前epitem屬于那個eventpoll
    struct eventpoll *ep;
    // 鏈表頭
    struct list_head fllink;
    // 當(dāng)前epitem對應(yīng)的事件(FD需要監(jiān)聽的事件)
    struct epoll_event event;
};

在前面提到過,epoll舍棄了select、poll函數(shù)中的思想,不再從用戶態(tài)全量拷貝FD集合到內(nèi)核,而是自己在內(nèi)核中維護(hù)了一個FD集合,而對于FD的管理則是基于eventpoll結(jié)構(gòu)實(shí)現(xiàn)的,eventpoll主要負(fù)責(zé)管理epoll事件,在其內(nèi)部主要有三個成員需要咱們重點(diǎn)關(guān)注:

  • list_head rdllist:存放所有I/O事件已就緒的列表。
  • rb_root rbr:用于存放注冊時epollfd描述符的紅黑樹結(jié)構(gòu)。
  • wait_queue_head_t wq:休眠阻塞時的等待隊(duì)列。

當(dāng)然,對于這個結(jié)構(gòu)體在后續(xù)源碼中會經(jīng)常看到,因此稍后會結(jié)合源碼理解。

6.3、Epoll源碼深度歷險(xiǎn)

? ?整個Epoll機(jī)制由于是三個函數(shù)組成的,因此調(diào)試源碼時則需要依次調(diào)試,我們依舊按照epoll的調(diào)用順序?qū)ζ湓创a進(jìn)行剖析。

6.3.1、epoll_create()函數(shù)源碼分析

epoll_create()函數(shù)對應(yīng)的系統(tǒng)調(diào)用為SYSCALL_DEFINE1(),展開后則對應(yīng)著內(nèi)核的sys_epoll_create函數(shù),如下:

SYSCALL_DEFINE1(epoll_create, int, size)
{
    if (size <= 0)
        return -EINVAL;
    // 直接調(diào)用了create1函數(shù)
    return sys_epoll_create1(0);
}

從上述這點(diǎn)即可看出,為何說size入?yún)?shí)際上在后續(xù)的版本被棄用了,因?yàn)闊o論傳入的size等于多少,本質(zhì)上只會判斷一下是否小于0,然后就調(diào)用了create1()函數(shù),入?yún)t被寫死為0了。接著來看看sys_epoll_create1()

SYSCALL_DEFINE1(epoll_create1, int, flags)
{
    int error;
    struct eventpoll *ep = NULL;//主描述符
    // 檢查一下常量一致性(沒啥用)
    BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);
    // 判斷一下flags是否傳遞了CLOEXEC
    if (flags & ~EPOLL_CLOEXEC)
        return -EINVAL;
    // 創(chuàng)建一個eventpoll并為其分配空間,分配出錯則直接返回執(zhí)行錯誤
    error = ep_alloc(&ep);
    if (error < 0)
        return error;
    // 這里是創(chuàng)建一個匿名真實(shí)的FD并與eventpoll關(guān)聯(lián)(稍后細(xì)聊)
    error = anon_inode_getfd("[eventpoll]", &eventpoll_fops, ep,
                 O_RDWR | (flags & O_CLOEXEC));
    // 如果前面匿名FD創(chuàng)建失敗,釋放之前為ep分配的空間
    if (error < 0)
        ep_free(ep);
    // 返回匿名FD或錯誤碼
    return error;
}

static int ep_alloc(struct eventpoll **pep)
{
    int error;
    struct user_struct *user;
    struct eventpoll *ep;
    // 調(diào)用kzalloc為ep分配空間
    user = get_current_user();
    error = -ENOMEM;
    ep = kzalloc(sizeof(*ep), GFP_KERNEL);
    if (unlikely(!ep))
        goto free_uid;
    // 對ep的每個成員進(jìn)行初始化
    spin_lock_init(&ep->lock);
    mutex_init(&ep->mtx);
    init_waitqueue_head(&ep->wq);
    init_waitqueue_head(&ep->poll_wait);
    INIT_LIST_HEAD(&ep->rdllist);
    ep->rbr = RB_ROOT;
    ep->ovflist = EP_UNACTIVE_PTR;
    ep->user = user;

    *pep = ep;
    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: ep_alloc() ep=%p\n",
             current, ep));
    return 0;
// 分配空間失敗時,清空之前的初始化值,返回錯誤碼
free_uid:
    free_uid(user);
    return error;
}

sys_epoll_create1()源碼也并不復(fù)雜,總共就兩步:

  • ①調(diào)用ep_alloc()函數(shù)創(chuàng)建并初始化一個eventpoll對象。
  • ②調(diào)用anon_inode_getfd()函數(shù)把eventpoll對象映射到一個FD上,并返回這個FD

不過對于第二步,這玩意兒說起來也比較復(fù)雜,想深入研究的可以看看 Linux創(chuàng)建匿名FD 的知識,我們這里就簡單的概述一下:

由于epollfd本身在操作系統(tǒng)上并不存在真正的文件與之對應(yīng),所以內(nèi)核需要為其分配一個真正的struct file結(jié)構(gòu),并且能夠具備真正的FD,然后前面創(chuàng)建出的eventpoll對象則會作為一個私有數(shù)據(jù)保存在file.private_data指針上。這樣做的目的在于:為了能夠通過FD找到一個真實(shí)的struct file,并且能夠通過這個file找到eventpoll對象,然后再通過eventpoll找到epollfd,從而能夠形成一條“關(guān)系鏈”。

6.3.2、epoll_ctl()函數(shù)源碼分析

epoll_create()函數(shù)的源碼并不復(fù)雜,現(xiàn)在緊接著再來看看管理操作epollepoll_ctl()源碼實(shí)現(xiàn),這個函數(shù)與之對應(yīng)的系統(tǒng)調(diào)用為SYSCALL_DEFINE4(),展開后則對應(yīng)sys_epoll_ctl(),下面一起看看:

SYSCALL_DEFINE4(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;
    // 錯誤處理動作及從用戶空間將epoll_event結(jié)構(gòu)拷貝到內(nèi)核空間
    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %p)\n",
             current, epfd, op, fd, event));
    error = -EFAULT;
    if (ep_op_has_event(op) &&
        copy_from_user(&epds, event, sizeof(struct epoll_event)))
        goto error_return;

    // 通過傳入的epfd得到前面創(chuàng)建的真實(shí)struct file結(jié)構(gòu)
    error = -EBADF;
    file = fget(epfd);
    if (!file)
        goto error_return;

    // 這里是獲取到需要監(jiān)聽的FD對應(yīng)的真實(shí)struct file結(jié)構(gòu)
    tfile = fget(fd);
    if (!tfile)
        goto error_fput;

    // 判斷一下要監(jiān)聽的目標(biāo)設(shè)備是否實(shí)現(xiàn)了poll邏輯
    error = -EPERM;
    if (!tfile->f_op || !tfile->f_op->poll)
        goto error_tgt_fput;

    // 判斷一下傳遞的epfd是否有對應(yīng)的eventpoll對象
    error = -EINVAL;
    if (file == tfile || !is_file_epoll(file))
        goto error_tgt_fput;

    // 根據(jù)private_data指針獲取其中存放的eventpoll對象(上面聊過的)
    ep = file->private_data;
    
    // 接下來的操作會開始對內(nèi)核中的結(jié)構(gòu)進(jìn)行修改,先加鎖確保操作安全
    mutex_lock(&ep->mtx);
    // 先從紅黑樹結(jié)構(gòu)中,根據(jù)FD查找一下對應(yīng)的節(jié)點(diǎn)是否存在
    epi = ep_find(ep, tfile, fd);
    error = -EINVAL;
    // 開始判斷用戶具體要執(zhí)行何種操作
    switch (op) {
    // 如果是要注冊(向內(nèi)核添加一個FD)
    case EPOLL_CTL_ADD:
        // 先判斷之前是否已經(jīng)添加過一次當(dāng)前FD
        if (!epi) {
            // 如果沒有添加,則調(diào)用ep_insert()函數(shù)將當(dāng)前fd注冊
            epds.events |= POLLERR | POLLHUP;
            error = ep_insert(ep, &epds, tfile, fd);
        } else // 之前這個FD添加過一次,則返回錯誤碼,不允許重復(fù)注冊
            error = -EEXIST;
        break;
    // 如果是刪除(從內(nèi)核中移除一個FD)
    case EPOLL_CTL_DEL:
        // 如果前面從紅黑樹中能找到與FD對應(yīng)的節(jié)點(diǎn)
        if (epi)
            // 調(diào)用ep_remove()函數(shù)移除相應(yīng)的節(jié)點(diǎn)
            error = ep_remove(ep, epi);
        else // 如果紅黑樹上都沒有FD對應(yīng)的節(jié)點(diǎn),則無法移除,返回錯誤碼
            error = -ENOENT;
        break;
    // 如果是修改(修改內(nèi)核中FD對應(yīng)的事件)
    case EPOLL_CTL_MOD:
        // 和上面的刪除同理,調(diào)用ep_modify()修改FD對應(yīng)的節(jié)點(diǎn)信息
        if (epi) {
            epds.events |= POLLERR | POLLHUP;
            error = ep_modify(ep, epi, &epds);
        } else // 樹上沒有對應(yīng)的節(jié)點(diǎn),依舊返回錯誤碼
            error = -ENOENT;
        break;
    }
    // 修改完成之后,為了確保其他進(jìn)程可操作,記得釋放鎖哦~
    mutex_unlock(&ep->mtx);

// 這里是對應(yīng)上述各種錯誤情況的goto
error_tgt_fput:
    fput(tfile);
error_fput:
    fput(file);
error_return:
    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %p) = %d\n",
             current, epfd, op, fd, event, error));
    return error;
}

epoll_ctl()函數(shù)的實(shí)現(xiàn)過程,看起來是相當(dāng)直觀明了,總結(jié)一下:

  • ①先將用戶傳遞的epoll事件集合epoll_event結(jié)構(gòu)從用戶空間拷貝到內(nèi)核。
  • ②通過epfd找到與之對應(yīng)的struct file結(jié)構(gòu),再找到FD對應(yīng)的file結(jié)構(gòu)。
  • ③判斷要監(jiān)聽的FD設(shè)備是否實(shí)現(xiàn)了poll功能,再根據(jù)private_data獲取eventpoll
  • ④上鎖,然后通過傳入的fd在紅黑樹中查找有沒有對應(yīng)的節(jié)點(diǎn),然后處理用戶操作。
  • ⑤如果是注冊操作,先判斷當(dāng)前FD之前是否注冊過,在樹上是否有相應(yīng)節(jié)點(diǎn):
    • 有:代表之前已經(jīng)添加過一次,不能重復(fù)添加,返回錯誤碼。
    • 沒有:調(diào)用ep_insert()函數(shù),將當(dāng)前FD添加到紅黑樹中。
  • ⑥如果是刪除操作,先看一下樹上有沒有與目標(biāo)FD對應(yīng)的節(jié)點(diǎn):
    • 有:調(diào)用ep_remove()函數(shù),將當(dāng)前FD對應(yīng)的節(jié)點(diǎn)從樹上移除。
    • 沒有:代表FD之前都沒有添加過,找不到要移除的節(jié)點(diǎn),返回錯誤碼。
  • ⑦如果是修改操作,先看一下樹上有沒有與目標(biāo)FD對應(yīng)的節(jié)點(diǎn):
    • 有:調(diào)用ep_modify()函數(shù),根據(jù)用戶的操作項(xiàng),修改對應(yīng)節(jié)點(diǎn)信息。
    • 沒有:代表FD之前都沒有添加過,找不到要修改的節(jié)點(diǎn),返回錯誤碼。
  • ⑧操作完成后,釋放鎖,同時如果前面有錯誤則利用goto處理前面的錯誤信息。

相信認(rèn)真看一遍源碼,以及上述流程后,對于epoll_ctl()函數(shù)的邏輯就明白了,當(dāng)然,諸位有些繞的地方估計(jì)在epoll內(nèi)部結(jié)構(gòu)之間的關(guān)系,上個圖理解:

epoll結(jié)構(gòu)

整個結(jié)構(gòu)關(guān)聯(lián)起來略顯復(fù)雜,但如若之前的epoll_create()函數(shù)真正理解后,其實(shí)也并不難懂,調(diào)用epoll_create后會先創(chuàng)建兩個結(jié)構(gòu)體:一個file結(jié)構(gòu)、一個eventpoll結(jié)構(gòu),然后會將eventpoll保存在file.private_data指針中,同時再將這個file的文件描述符返回給調(diào)用者(用戶態(tài)程序),此時這個返回的FD就是所謂的epollfd

然后當(dāng)我們調(diào)用epoll_ctl()嘗試將一個要監(jiān)聽的SocketFD加入到內(nèi)核時,我們首先需要傳遞一個epfd,而后再ctl()函數(shù)內(nèi)部會根據(jù)這個epfd找到之前創(chuàng)建的file結(jié)構(gòu),再根據(jù)其private_data指針找到前面創(chuàng)建的eventpoll對象,然后定位該對象的內(nèi)部成員:rbr紅黑樹,在將要監(jiān)聽的FD封裝成epitem節(jié)點(diǎn)加入樹中。

當(dāng)然,為了求證上述觀點(diǎn),接下來再看看ep_insert()函數(shù)的實(shí)現(xiàn)。

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;
    // 這里是一個新的結(jié)構(gòu)體(類似于select、poll中的poll_wqueues結(jié)構(gòu))
    struct ep_pqueue epq;
    // 檢查目前是否達(dá)到了當(dāng)前用戶進(jìn)程的最大監(jiān)聽數(shù)
    if (unlikely(atomic_read(&ep->user->epoll_watches) >=
             max_user_watches))
        return -ENOSPC;
    // 利用SLAB機(jī)制分配一個epitem節(jié)點(diǎn)
    if (!(epi = kmem_***_alloc(epi_***, GFP_KERNEL)))
        return -ENOMEM;
    // 初始化epitem節(jié)點(diǎn)的一些成員
    INIT_LIST_HEAD(&epi->rdllink);
    INIT_LIST_HEAD(&epi->fllink);
    INIT_LIST_HEAD(&epi->pwqlist);
    epi->ep = ep;
    // 將要監(jiān)聽的FD以及它的file結(jié)構(gòu)設(shè)置到epitem.ffd成員中
    ep_set_ffd(&epi->ffd, tfile, fd);
    epi->event = *event;
    epi->nwait = 0;
    epi->next = EP_UNACTIVE_PTR;
    // 同時開始準(zhǔn)備調(diào)用fd對應(yīng)設(shè)備的poll
    epq.epi = epi;
    // 這里和select、poll差不多,設(shè)置執(zhí)行poll_wait()時,
    // 其回調(diào)函數(shù)為ep_ptable_queue_proc(稍后分析)
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
    // tfile是需要監(jiān)聽的fd對應(yīng)的file結(jié)構(gòu)
    // 這里就是去調(diào)用fd對應(yīng)設(shè)備的poll,詢問I/O數(shù)據(jù)是否可讀寫
    revents = tfile->f_op->poll(tfile, &epq.pt);
    // 這里是防止執(zhí)行出現(xiàn)錯誤的檢測動作
    error = -ENOMEM;
    if (epi->nwait < 0)
        goto error_unregister;
    // 每個FD會將所有監(jiān)聽自己的epitem鏈起來
    spin_lock(&tfile->f_lock);
    list_add_tail(&epi->fllink, &tfile->f_ep_links);
    spin_unlock(&tfile->f_lock);
    // 上述所有工作完成后,將epitem插入到紅黑樹中
    ep_rbtree_insert(ep, epi);
    // 判斷一下前面調(diào)用poll之后,對應(yīng)設(shè)備上的I/O事件是否就緒
    if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
        // 如果已經(jīng)就緒,那直接將當(dāng)前epitem節(jié)點(diǎn)添加到eventpoll.rdllist中
        list_add_tail(&epi->rdllink, &ep->rdllist);
        // 同時喚醒正在阻塞等待的進(jìn)程
        if (waitqueue_active(&ep->wq))
            wake_up_locked(&ep->wq);
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }
    spin_unlock_irqrestore(&ep->lock, flags);
    atomic_inc(&ep->user->epoll_watches);
    // 調(diào)用poll_wait()執(zhí)行回調(diào)函數(shù)(為了防止鎖資源占用,這是在鎖外調(diào)用)
    if (pwake)
        ep_poll_safewake(&ep->poll_wait);
    return 0;
// 執(zhí)行出錯的goto代碼塊
error_unregister:
    ep_unregister_pollwait(ep, epi);
    spin_lock_irqsave(&ep->lock, flags);
    if (ep_is_linked(&epi->rdllink))
        list_del_init(&epi->rdllink);
    spin_unlock_irqrestore(&ep->lock, flags);
    kmem_***_free(epi_***, epi);
    return error;
}

其實(shí)對于ep_insert()這個函數(shù)呢,說清楚來也并不復(fù)雜,簡單總結(jié)一下:

  • ①對于要監(jiān)聽的FD會先分配一個epitem節(jié)點(diǎn),并且根據(jù)FD對節(jié)點(diǎn)進(jìn)行初始化。
  • ②最開始聲明了一個結(jié)構(gòu)體ep_pqueue,然后會利用它為FD設(shè)置poll_wait回調(diào)函數(shù)。
  • ③嘗試調(diào)用FD對應(yīng)設(shè)備的poll,詢問當(dāng)前FDI/O數(shù)據(jù)是否可被讀寫。
  • ④調(diào)用ep_rbtree_insert()函數(shù)將已經(jīng)構(gòu)建好的epitem節(jié)點(diǎn)插入紅黑樹中。
  • ⑤判斷一下當(dāng)前FDI/O事件是否已就緒(可被讀寫),如果可以則喚醒等待的進(jìn)程。

當(dāng)然,對于新的結(jié)構(gòu)體ep_pqueue,它的功能和之前聊到的poll_wqueues功能大致相同,主要用于設(shè)置喚醒回調(diào),定義如下:

struct ep_pqueue {
    poll_table pt;
    struct epitem *epi;
};

很明顯就可以看出,與poll_wqueues結(jié)構(gòu)中同樣存在poll_table成員,不熟悉的跳回之前講poll_wqueues的環(huán)節(jié)。不過EpollSelect、Poll還是存在些許不同的,在之前設(shè)置poll_wait()的回調(diào)函數(shù)是__pollwait(),但在這里設(shè)置的確是ep_ptable_queue_proc()函數(shù),那這個函數(shù)會做什么事情呢?來看看:

static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
                 poll_table *pt)
{
    struct epitem *epi = ep_item_from_epqueue(pt);
    struct eppoll_entry *pwq;
    if (epi->nwait >= 0 && (pwq = kmem_***_alloc(pwq_***, GFP_KERNEL))) {
        // 初始化等待隊(duì)列, 指定ep_poll_callback為喚醒時的回調(diào)函數(shù)
        init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
        pwq->whead = whead;
        pwq->base = epi;
        // 將節(jié)點(diǎn)加入到等待隊(duì)列中.....
        add_wait_queue(whead, &pwq->wait);
        list_add_tail(&pwq->llink, &epi->pwqlist);
        epi->nwait++;
    } else {
        /* We have to signal that an error occurred */
        epi->nwait = -1;
    }
}

上述重心我們只需要知道一個點(diǎn),這個函數(shù)會在執(zhí)行f_op->poll時被調(diào)用的,在這里最重要的是設(shè)置了一個喚醒時的回調(diào)函數(shù)ep_poll_callback(),也就是當(dāng)某個設(shè)備上I/O事件就緒后,喚醒進(jìn)程時會調(diào)用的函數(shù),實(shí)現(xiàn)如下:

static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    int pwake = 0;
    unsigned long flags;
    // 從等待隊(duì)列獲取epitem節(jié)點(diǎn)(主要目的在于要確認(rèn)哪個進(jìn)程在等待當(dāng)前設(shè)備就緒)
    struct epitem *epi = ep_item_from_wait(wait);
    // 獲取當(dāng)前epitem節(jié)點(diǎn)所在的eventpoll
    struct eventpoll *ep = epi->ep;
    spin_lock_irqsave(&ep->lock, flags);
    if (!(epi->event.events & ~EP_PRIVATE_BITS)) // 檢測錯誤
        goto out_unlock;
    // 檢測目前設(shè)備上就緒的事件是否為我們要監(jiān)聽的事件
    if (key && !((unsigned long) key & epi->event.events))
        // 如果不是,則直接跳轉(zhuǎn)goto
        goto out_unlock;
    // 這里是用來處理事件并發(fā)出現(xiàn)時的情況,
    // 假設(shè)當(dāng)前的回調(diào)方法被執(zhí)行,但epoll_wait()已經(jīng)獲取到了別的IO事件,
    // 那么此時將當(dāng)前設(shè)備發(fā)生的事件,epitem會用一個鏈表存儲,
    // 此時不立即發(fā)給應(yīng)用程序,也不丟棄本次IO事件,
    // 而是等待下次調(diào)用epoll_wait()函數(shù)時返回
    if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
        if (epi->next == EP_UNACTIVE_PTR) {
            epi->next = ep->ovflist;
            ep->ovflist = epi;
        }
        // 然后直接跳轉(zhuǎn)goto
        goto out_unlock;
    }
    // 正常情況下,將當(dāng)前已經(jīng)觸發(fā)IO事件的epitem節(jié)點(diǎn)放入readylist就緒列表
    if (!ep_is_linked(&epi->rdllink))
        list_add_tail(&epi->rdllink, &ep->rdllist);
    // 喚醒調(diào)用epoll_wait后阻塞的進(jìn)程...
    if (waitqueue_active(&ep->wq))
        wake_up_locked(&ep->wq);
    // 如果epollfd也在被poll, 也喚醒隊(duì)列里面的所有成員(多進(jìn)程epoll情況)
    if (waitqueue_active(&ep->poll_wait))
        pwake++;
// 前面的goto跳轉(zhuǎn)處理
out_unlock:
    spin_unlock_irqrestore(&ep->lock, flags);
    if (pwake)
        ep_poll_safewake(&ep->poll_wait);
    return 1;
}

這個喚醒回調(diào)函數(shù),主要干的事情就是處理了幾種特殊情況,然后將IO事件就緒的節(jié)點(diǎn)添加到了eventpoll.readylist就緒列表,緊接著喚醒了調(diào)用epoll_wait()函數(shù)后阻塞的進(jìn)程。

至此,epoll_ctl()函數(shù)調(diào)用后,會執(zhí)行的流程就已經(jīng)分析明白了。當(dāng)然,對于具體如何插入、移除、修改節(jié)點(diǎn)的函數(shù)就不分析了,這里就是紅黑樹結(jié)構(gòu)的知識,大家可參考HashMap集合的元素管理原理。接下來重點(diǎn)看看epoll_wait()函數(shù)。

6.3.3、epoll_wait()函數(shù)源碼分析

epoll_wait()也是整個Epoll機(jī)制中最重要的一步,前面的create、ctl函數(shù)都僅僅是在為wait函數(shù)做準(zhǔn)備工作,epoll_wait()是一個阻塞函數(shù),調(diào)用后會導(dǎo)致當(dāng)前程序發(fā)生阻塞等待,直至獲取到有效的IO事件或超時為止。

epoll_wait()對應(yīng)的系統(tǒng)調(diào)用為SYSCALL_DEFINE4()函數(shù),展開后則是sys_epoll_wait(),不多說直接上源碼:

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
        int, maxevents, int, timeout)
{
    int error;
    struct file *file;
    struct eventpoll *ep;
    // 獲取的最大事件數(shù)量必須大于0,并且不超出ep的最大事件數(shù)
    if (maxevents <= 0 || maxevents > EP_MAX_EVENTS)
        return -EINVAL;
     // 內(nèi)核會驗(yàn)證用戶接收事件的這一段內(nèi)存空間是不是有效的.
    if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event))) {
        error = -EFAULT;
        goto error_return;
    }
    error = -EBADF;
    // 根據(jù)epollfd獲取對應(yīng)的struct file真實(shí)文件
    file = fget(epfd);
    if (!file)
        goto error_return;
    error = -EINVAL;
    // 檢查一下獲取到的file是不是一個epollfd
    if (!is_file_epoll(file))
        goto error_fput;
    // 獲取file的private_data數(shù)據(jù),也就是根據(jù)file獲取eventpoll對象
    ep = file->private_data;
    // 獲取到eventpoll對象調(diào)用ep_poll()函數(shù)(這個是核心函數(shù)!)
    error = ep_poll(ep, events, maxevents, timeout);
error_fput:
    fput(file);
error_return:
    return error;
}

上述邏輯也不難,首先對于用戶態(tài)調(diào)用epoll_wait()函數(shù)時傳遞的一些參數(shù)進(jìn)行了效驗(yàn),因?yàn)閮?nèi)核對于進(jìn)程采取的態(tài)度是絕對不信任,因此對于用戶進(jìn)程遞交的任何參數(shù)都會進(jìn)行效驗(yàn),確保無誤后才會采取下一步措施。當(dāng)上述代碼前面效驗(yàn)了參數(shù)的“合法性”后,又根據(jù)epfd獲取了對應(yīng)的file,然后又根據(jù)file獲取到了eventpoll對象,最后調(diào)用了ep_poll()函數(shù)并傳入了eventpoll對象,再看看這個函數(shù):

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
           int maxevents, long timeout)
{
    int res, eavail;
    unsigned long flags;
    long jtimeout;
    // 等待隊(duì)列
    wait_queue_t wait;
    // 如果調(diào)用epoll_wait時傳遞了阻塞時間,那么先計(jì)算休眠時間, 
    // 毫秒要轉(zhuǎn)換為HZ電磁波動的頻率(比較嚴(yán)謹(jǐn),控制的非常精細(xì))
    jtimeout = (timeout < 0 || timeout >= EP_MAX_MSTIMEO) ?
        MAX_SCHEDULE_TIMEOUT : (timeout * HZ + 999) / 1000;
retry:
    spin_lock_irqsave(&ep->lock, flags);
    res = 0;
    // 判斷一下eventpoll.readylist事件就緒列表是否為空
    if (list_empty(&ep->rdllist)) {
        // 初始化等待隊(duì)列,準(zhǔn)備將當(dāng)前進(jìn)程掛起阻塞
        init_waitqueue_entry(&wait, current);
        // 掛載到如果eventpoll.wq等待隊(duì)列中
        __add_wait_queue_exclusive(&ep->wq, &wait);
        
        // 核心循環(huán)!
        for (;;) {
            // 準(zhǔn)備進(jìn)入阻塞,先將當(dāng)前進(jìn)程設(shè)置為睡眠狀態(tài)(可被信號喚醒)
            set_current_state(TASK_INTERRUPTIBLE);
            // 如果睡眠之前,readylist中有數(shù)據(jù)了或已經(jīng)到了給定的超時事件
            if (!list_empty(&ep->rdllist) || !jtimeout)
                break; // 不睡了,直接中斷循環(huán)
            // 如果出現(xiàn)喚醒信號,也中斷循環(huán),不睡了退出干活
            if (signal_pending(current)) {
                res = -EINTR;
                break;
            }
            // 上述的幾個情況都未發(fā)生,在這里準(zhǔn)備正式進(jìn)入睡眠狀態(tài)
            spin_unlock_irqrestore(&ep->lock, flags);
            // 開始睡覺(如果指定了阻塞時間,jtimeout時間過后會自動醒來)
            jtimeout = schedule_timeout(jtimeout); // 正式進(jìn)入睡眠阻塞
            spin_lock_irqsave(&ep->lock, flags);
        }
        // 出現(xiàn)喚醒信號、或事件已就緒、或已超時,則將進(jìn)程從等待隊(duì)列移除
        __remove_wait_queue(&ep->wq, &wait);
        // 這里設(shè)置一下當(dāng)前進(jìn)程的狀態(tài)為運(yùn)行狀態(tài)(活躍狀態(tài))
        set_current_state(TASK_RUNNING);
    }
    // 因?yàn)槌瑫r的情況下當(dāng)前進(jìn)程也會醒來,所以這里需要再次判斷一下:
    // 目前到底是否已經(jīng)有數(shù)據(jù)就緒了,這里會返回一個布爾值給 eavail
    eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;
    spin_unlock_irqrestore(&ep->lock, flags);
    // 如果確定有事件發(fā)生,那則調(diào)用ep_send_events()將事件拷貝給用戶進(jìn)程
    if (!res && eavail &&
        !(res = ep_send_events(ep, events, maxevents)) && jtimeout)
        goto retry;
    // 最后返回本次監(jiān)聽到的事件數(shù)
    return res;
}

上述的ep_poll()函數(shù),對比select、poll而言要簡單很多,整個流程也沒幾步:

  • ①轉(zhuǎn)換給定的阻塞時間,并判斷就緒列表中事件是否已就緒,沒有則準(zhǔn)備休眠阻塞。
  • ②先初始化等待隊(duì)列并掛載到eventpoll.wq中,然后進(jìn)入循環(huán),設(shè)置進(jìn)程的狀態(tài)。
  • ③在正式進(jìn)入睡眠之前,再次檢測是否有事件就緒、是否已超時、是否出現(xiàn)喚醒信號。
  • ④如果都沒有出現(xiàn),則調(diào)用schedule_timeout(jtimeout)函數(shù)讓進(jìn)程睡眠一定時間。
  • ⑤當(dāng)睡眠超時、或出現(xiàn)喚醒信號、或事件已就緒,將當(dāng)前進(jìn)程從隊(duì)列移除并設(shè)置運(yùn)行狀態(tài)。
  • ⑥有事件到來非超時的情況下,則調(diào)用ep_send_events()將就緒事件拷貝給用戶進(jìn)程。

OK~,上述總結(jié)的流程已經(jīng)描述的十分清晰了,接下來再看看拷貝就緒事件的函數(shù)ep_send_events()

static int ep_send_events(struct eventpoll *ep,
              struct epoll_event __user *events, int maxevents)
{
    struct ep_send_events_data esed;
    // 獲取用戶進(jìn)程傳遞的maxevents值
    esed.maxevents = maxevents;
    // 獲取用戶態(tài)存放就緒事件的集合
    esed.events = events;
    // 調(diào)用ep_scan_ready_list()函數(shù)進(jìn)行具體處理
    return ep_scan_ready_list(ep, ep_send_events_proc, &esed);
}

這個函數(shù)非常簡單,一眼看明白了,其實(shí)最終的拷貝工作是由ep_scan_ready_list()完成的,那么再來看看它:

static int ep_scan_ready_list(struct eventpoll *ep,
                  int (*sproc)(struct eventpoll *,
                       struct list_head *, void *),
                  void *priv)
{
    int error, pwake = 0;
    unsigned long flags;
    struct epitem *epi, *nepi;
    LIST_HEAD(txlist);
    // 先上鎖,防止出現(xiàn)安全問題
    mutex_lock(&ep->mtx);
    spin_lock_irqsave(&ep->lock, flags);
    // 將rdllist中所有就緒的節(jié)點(diǎn)轉(zhuǎn)移到txlist,然后清空rdllist
    list_splice_init(&ep->rdllist, &txlist);
    ep->ovflist = NULL;
    spin_unlock_irqrestore(&ep->lock, flags);
    // sproc是前面調(diào)用當(dāng)前函數(shù)時傳遞的ep_send_events_proc(),
    // 會通過這個函數(shù)處理每個epitem節(jié)點(diǎn)
    error = (*sproc)(ep, &txlist, priv);
    spin_lock_irqsave(&ep->lock, flags);
    // 之前曾講到過,如果epoll正在拷貝數(shù)據(jù)時又發(fā)生了IO事件,
    // 那么則會將這些IO事件保存在ovflist組成一個鏈表,現(xiàn)在來處理這些事件
    for (nepi = ep->ovflist; (epi = nepi) != NULL;
         nepi = epi->next, epi->next = EP_UNACTIVE_PTR) {
        // 將這些直接放入readylist列表中
        if (!ep_is_linked(&epi->rdllink))
            list_add_tail(&epi->rdllink, &ep->rdllist);
    }
    ep->ovflist = EP_UNACTIVE_PTR;
    // 上一次沒有處理完的epitem節(jié)點(diǎn), 重新插入到readylist
    // 因?yàn)閑poll一次只能拷貝maxevents個事件返回用戶態(tài)
    list_splice(&txlist, &ep->rdllist);
    // readylist不為空, 直接喚醒
    if (!list_empty(&ep->rdllist)) {
        // 喚醒的前置工作
        if (waitqueue_active(&ep->wq))
            wake_up_locked(&ep->wq);
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }
    spin_unlock_irqrestore(&ep->lock, flags);
    mutex_unlock(&ep->mtx);
    // 為了防止長時間占用鎖,在鎖外執(zhí)行喚醒工作
    if (pwake)
        ep_poll_safewake(&ep->poll_wait);
    return error;
}

流程就不寫了,源碼中標(biāo)注的很清楚,下面再來看看ep_send_events_proc()函數(shù)是如何處理每個epitem節(jié)點(diǎn)的:

// 注意點(diǎn):這里的入?yún)ist_head并不是readylist,而是上面函數(shù)的txlist
static int ep_send_events_proc(struct eventpoll *ep, struct list_head *head,
                   void *priv)
{
    struct ep_send_events_data *esed = priv;
    int eventcnt;
    unsigned int revents;
    struct epitem *epi;
    struct epoll_event __user *uevent;
    // 先用循環(huán)掃描整個列表(不一定會全部處理,最多只處理maxevents個)
    for (eventcnt = 0, uevent = esed->events;
         !list_empty(head) && eventcnt < esed->maxevents;) {
        // 依次獲取到其中的一個epitem節(jié)點(diǎn)
        epi = list_first_entry(head, struct epitem, rdllink);
        // 緊接著從列表中將這個節(jié)點(diǎn)移除
        list_del_init(&epi->rdllink);
        // 再次讀取當(dāng)前節(jié)點(diǎn)對應(yīng)FD所觸發(fā)的事件,其實(shí)在喚醒回調(diào)函數(shù)中,
        // 這個工作也執(zhí)行過一次,那為啥這里還需要做一次呢?
        // 答案是:為了確保讀到最新的事件,因?yàn)橛行〧D可能前面觸發(fā)了讀就
        // 緒事件,后面又觸發(fā)了寫就緒事件,因此這里要確保嚴(yán)謹(jǐn)性。
        revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) &
            epi->event.events;
        if (revents) {
            // 調(diào)用__put_user將就緒的事件拷貝至用戶進(jìn)程傳遞的事件集合中
            if (__put_user(revents, &uevent->events) ||
                __put_user(epi->event.data, &uevent->data)) {
                list_add(&epi->rdllink, head);
                return eventcnt ? eventcnt : -EFAULT;
            }
            eventcnt++;
            uevent++;
            if (epi->event.events & EPOLLONESHOT)
                epi->event.events &= EP_PRIVATE_BITS;
            else if (!(epi->event.events & EPOLLET)) {
                // 大名鼎鼎的ET和LT,就在這一步會有不同(稍后分析)
                list_add_tail(&epi->rdllink, &ep->rdllist);
            }
        }
    }
    return eventcnt;
}

處理每個epitem節(jié)點(diǎn)的函數(shù)中,重點(diǎn)就做了兩件事:

  • ①讀取了每個epitem節(jié)點(diǎn)最新的就緒事件。
  • ②調(diào)用__put_user()函數(shù)將就緒的事件拷貝至用戶進(jìn)程傳遞的evlist集合中。

至此,epoll_wait()函數(shù)的核心源碼也全都走了一遍,最后來簡單的總結(jié)一下。

6.3.4、Epoll被阻塞的進(jìn)程是如何喚醒的?

? ?到這里,大家應(yīng)該有個疑惑:epoll_wait()函數(shù)中,本質(zhì)上只做了進(jìn)程休眠阻塞的工作,那它什么時候會被喚醒呢?先對于這個點(diǎn)回答一下:大家還記得前面分析epoll_ctl()函數(shù)時,在其中調(diào)用了每個FDpoll嗎?

revents = tfile->f_op->poll(tfile, &epq.pt);

也就是這行代碼,在調(diào)用epoll_ctl()函數(shù)向內(nèi)核插入一個節(jié)點(diǎn)時,就會先詢問一次FDIO數(shù)據(jù)是否可被讀寫,此時如果可以就會直接將這個節(jié)點(diǎn)添加到readylist列表中,但如果對應(yīng)驅(qū)動設(shè)備的IO事件還未就緒,則會將當(dāng)前進(jìn)程注冊到每個FD對應(yīng)設(shè)備的等待隊(duì)列上,并設(shè)置喚醒回調(diào)函數(shù)為ep_poll_callback()

這樣也就意味著,如果某個FD的事件就緒了,就會由對應(yīng)的驅(qū)動設(shè)備執(zhí)行這個回調(diào),在ep_poll_callback()函數(shù)中,會先將對應(yīng)的節(jié)點(diǎn)先插入到readylist列表,然后會嘗試喚醒eventpoll等待隊(duì)列中阻塞的進(jìn)程。

當(dāng)后續(xù)調(diào)用epoll_wait()函數(shù)時,會先判斷readylist列表中是否有事件就緒,如果有就直接讀取返回了,如果沒有則會讓當(dāng)前進(jìn)程阻塞休眠,并將當(dāng)前進(jìn)程添加到eventpoll等待隊(duì)列中,然后某個FD的數(shù)據(jù)就緒后,則會喚醒這個隊(duì)列中阻塞的進(jìn)程,此時調(diào)用epoll_wait()陷入阻塞的進(jìn)程就被喚醒工作了!!發(fā)現(xiàn)沒有,Epoll的源碼設(shè)計(jì)中是環(huán)環(huán)相扣的,十分巧妙!

OK~,搞清楚這點(diǎn)之后,Epoll的核心邏輯已經(jīng)講完了十之八九,還剩下Epoll的兩種事件觸發(fā)機(jī)制未講到,來聊一聊吧。

6.4、Epoll的兩種事件觸發(fā)機(jī)制

? ?相信之前有簡單了解過Epoll的小伙伴都明白,在Epoll中有兩種事件觸發(fā)模式,分別被稱為水平觸發(fā)與邊緣觸發(fā),一般來說,邊緣觸發(fā)的性能遠(yuǎn)超于水平觸發(fā)。

6.4.1、Epoll水平觸發(fā)機(jī)制-LT模式

? ?LT模式也是Epoll的默認(rèn)事件觸發(fā)機(jī)制,也就是當(dāng)某個FDepitem節(jié)點(diǎn))被處理后,如果還依舊存在事件或數(shù)據(jù),則會再次將這個epitem節(jié)點(diǎn)加入readylist列表中,當(dāng)下次調(diào)用epoll_wait()時依舊會返回給用戶進(jìn)程。

大家還記得前面分析ep_send_events_proc()函數(shù)時,最后的那兩行代碼嗎?

else if (!(epi->event.events & EPOLLET)) {
    list_add_tail(&epi->rdllink, &ep->rdllist);
}

在這個函數(shù)中,最后面做了一個簡單的判斷,如果當(dāng)前Epoll的工作模式?jīng)]有設(shè)置成EL,同時當(dāng)前節(jié)點(diǎn)還有事件未處理,就會調(diào)用list_add_tail()函數(shù)將當(dāng)前的epitem節(jié)點(diǎn)重新加入readylist列表。反之,ET模式下則不會這么做。

6.4.2、Epoll邊緣觸發(fā)機(jī)制-ET模式

? ?在ET模式中,就是和LT模式反過來的,當(dāng)處理一個epitem節(jié)點(diǎn)時,就算其中還有事件沒處理完,那我也不會將這個節(jié)點(diǎn)重新加入readylist列表,除非這個節(jié)點(diǎn)對應(yīng)的FD又再次觸發(fā)了新事件,然后再次執(zhí)行了ep_poll_callback()回調(diào)函數(shù),此時才會將其重新加入到readylist

說人話簡單一點(diǎn):就是ET模式下,對于當(dāng)前觸發(fā)的事件,只會通知用戶進(jìn)程一次,就算沒有處理也不會重復(fù)通知,除非這個FD發(fā)生新的事件。而LT模式下則相反,無論何種情況下都能確保事件不丟失。

那又該如何設(shè)置ET觸發(fā)機(jī)制呢?其實(shí)也就是在調(diào)用epoll_ctl()函數(shù)時,指定感興趣的監(jiān)聽事件時,多加一個EPOLLET即可。

基于Epoll機(jī)制構(gòu)建的大部分高性能應(yīng)用,一般都會采用ET模式,例如Nginx

6.5、Epoll小結(jié)與一個爭議的問題

? ?相較于之前的select函數(shù)存在的四個問題,在Epoll中得到了合理解決,但也并非Epoll的性能就一定比Select、Poll要好,在監(jiān)聽的文件描述符較少、且經(jīng)常更換監(jiān)聽的目標(biāo)FD的情況下,Select、Poll的性能反而會更佳。

當(dāng)然,epoll在高并發(fā)的性能下,會有非常優(yōu)異的表現(xiàn),這是由于多方面原因造就的,比如在內(nèi)核中維護(hù)FD避免反復(fù)拷貝切換、對于就緒事件回調(diào)通知,無需用戶進(jìn)程再次輪詢查找、內(nèi)部采用紅黑樹結(jié)構(gòu)維護(hù)節(jié)點(diǎn)、退出ET事件觸發(fā)機(jī)制等.......

? ?同時對于網(wǎng)上一個較為爭議的問題:Epoll到底有沒有試用MMAP共享內(nèi)存呢?從Epoll源碼的角度來看,其實(shí)是并未使用的,在向用戶進(jìn)程返回就緒事件時,本質(zhì)上是調(diào)用了__put_user()函數(shù)將數(shù)據(jù)從內(nèi)核拷貝到了用戶態(tài)。當(dāng)然,我Epoll的源碼是基于內(nèi)核3.x版本的,但聽網(wǎng)上說在早版本里面用到了,但翻閱分析select源碼時用的內(nèi)核2.6版本源碼,在里面Epoll都還未定義完整,僅有部分實(shí)現(xiàn),所以也沒有發(fā)現(xiàn)mmap相關(guān)的API調(diào)用。

不過在Java-NIOFileChannel.transferTo()方法中,以及在Linux系統(tǒng)的sendfile()函數(shù)中確實(shí)用到了,因此操作本地文件數(shù)據(jù)時,確實(shí)會用到mmap共享內(nèi)存。因此,Java-NIO中用到了mmap,但Epoll中應(yīng)該未曾用到。

七、多路復(fù)用模型總結(jié)

? ?看到到這里,也就接近尾聲了,上面已經(jīng)對于Linux系統(tǒng)下提供的多路復(fù)用函數(shù)進(jìn)行了全面深入剖析,大家反復(fù)閱讀幾遍,自然能夠徹底弄懂select、poll、epoll這些函數(shù)的工作原理。

不過對于Epoll的分析還有一些內(nèi)容未曾提及,也就是Epoll喚醒時的驚群問題,大家感興趣的可自行去研究,這里只埋下一個引子,當(dāng)然也并不復(fù)雜。

? ?而整個Java-NIO都是基于底層的多路復(fù)用函數(shù)構(gòu)建的,但本篇僅分析了Linux系統(tǒng)下的多路復(fù)用實(shí)現(xiàn),在本文開篇也提到過:JVM為了維持自己的跨平臺性,因此在不同的系統(tǒng)下會分別調(diào)用不同的多路復(fù)用函數(shù),比如Windows-Select、Mac-KQueue函數(shù),其中Windows-SelectLinux-Select的實(shí)現(xiàn)類似,因此我從JNI調(diào)用的入?yún)⑸蟻碚f大致相同。而Mac-KQueue則與Linux-Epoll類似。但對于其內(nèi)部實(shí)現(xiàn)我并不清楚,畢竟是閉源的系統(tǒng),大家有興趣可以自行研究。

最后,還有Java-AIO的內(nèi)容,其底層是如何實(shí)現(xiàn)的呢?在異步非阻塞式IO的支持方面,Windows系統(tǒng)反而做的更好,因?yàn)樗袑iT實(shí)現(xiàn)IOCP機(jī)制,但Linux、Mac系統(tǒng)則是通過KQueue、Epoll模擬實(shí)現(xiàn)的。

至此,最后也簡單的總結(jié)一下本篇分析的select、poll、epoll三者之間的區(qū)別:

對比項(xiàng) Select Poll Epoll
內(nèi)部數(shù)據(jù)結(jié)構(gòu) 數(shù)組位圖 數(shù)組鏈表 紅黑樹
最大監(jiān)聽數(shù) 1024 理論無限制 理論無限制
事件查找機(jī)制 線性輪詢 線性輪詢 回調(diào)事件直接寫回用戶態(tài)
事件處理時間復(fù)雜度 O(n) O(n) O(1)
性能對比 FD越多越差 FD越多越差 FD增多不會造成影響
FD傳遞機(jī)制 用~核拷貝 用~核拷貝 內(nèi)核維護(hù)結(jié)構(gòu)

......,除上述之外,三者還存在很多細(xì)微差異,大家認(rèn)真看懂本篇自然能心中明了,因此不再贅述,到此為本文畫上句號。

本文由于整篇涉及到Linux內(nèi)核源碼的調(diào)試,由于自己本身不是C開發(fā),所以調(diào)試起來也顯得心里憔悴,但至少對于多路復(fù)用函數(shù)的核心源碼都已闡明。當(dāng)然,如若文中存在不足還請諒解,對于存在誤區(qū)、疑義的地方也歡迎留言指正。最后,也希望大家動動手指點(diǎn)贊支持,在此萬分感謝^_^

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內(nèi)容