日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

linux服務器開發相關視頻解析:

linux下的epoll實戰揭秘——支撐億級IO的底層基石

epoll的網絡模型,從redis,memcached到nginx,一起搞定

epoll 是 linux 特有的一個 I/O 事件通知機制。很久以來對 epoll 如何能夠高效處理數以百萬記的文件描述符很有興趣。近期學習、研究了 epoll 源碼,在這個過程中關于 epoll 數據結構和作者的實現思路產生出不少疑惑,在此總結為了 10 個問題并逐個加以解答和分析。

Question 1:是否所有的文件類型都可以被 epoll 監視?

答案:不是。看下面這個實驗代碼:

#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define MAX_EVENTS 1

int main (void)
{
    int epfd;
    epfd = epoll_create(100); /* 創建epoll實例,預計監聽100個fd */
    if (epfd < 0) {
        perror ("epoll_create");
    }

    struct epoll_event *events;
    int nr_events, i;
    events = malloc (sizeof (struct epoll_event) * MAX_EVENTS);
    if (!events) {
        perror("malloc");
        return 1;
    }

    /* 打開一個普通文本文件 */
    int target_fd = open ("./11.txt", O_RDONLY);
    printf("target_fd %dn", target_fd);
    int target_listen_type = EPOLLIN;
    for (i = 0; i < 1; i++) {
        int ret;
        events[i].data.fd = target_fd; /* epoll調用返回后,返回給應用進程的fd號 */
        events[i].events = target_listen_type; /* 需要監聽的事件類型 */
        ret = epoll_ctl (epfd, EPOLL_CTL_ADD, target_fd, &events[i]); /* 注冊fd到epoll實例上 */
        if (ret) {
     printf("ret %d, errno %dn", ret, errno);
            perror ("epoll_ctl");
        }
    }

    /* 應用進程阻塞在epoll上,超時時長置為-1表示一直等到有目標事件才會返回 */
    nr_events = epoll_wait(epfd, events, MAX_EVENTS, -1);
    if (nr_events < 0) {
        perror ("epoll_wait");
        free(events);
        return 1;
    }
    for (i = 0; i < nr_events; i++) {
        /* 打印出處于就緒狀態的fd及其事件 */
        printf("event=%d on fd=%dn", events[i].events, events[i].data.fd);
    }
    free (events);
    close(epfd);
    return 0;
}

編譯、運行上面的代碼,會打印出下列信息:

gcc epoll_test.c -o epdemo
./epdemo
target_fd 4
ret -1, errno 1
epoll_ctl: Operation not permitted

正常打開了"txt"文件 fd=4, 但調用 epoll_ctl 監視這個 fd 時卻 ret=-1 失敗了, 并且錯誤碼為 1,錯誤信息為"Operation not permitted"。錯誤碼指明這個 fd 不能夠被 epoll 監視。

那什么樣的 fd 才可以被 epoll 監視呢?

只有底層驅動實現了 file_operations 中 poll 函數的文件類型才可以被 epoll 監視!socket 類型的文件驅動是實現了 poll 函數的,因此才可以被 epoll 監視。struct file_operations 聲明位置是在 include/linux/fs.h 中。

Question 2:ep->wq 的作用是什么?

答案:wq 是一個等待隊列,用來保存對某一個 epoll 實例調用 epoll_wait()的所有進程。

一個進程調用 epoll_wait()后,如果當前還沒有任何事件發生,需要讓當前進程掛起等待(放到 ep->wq 里);當 epoll 實例監視的文件上有事件發生后,需要喚醒 ep->wq 上的進程去繼續執行用戶態的業務邏輯。之所以要用一個等待隊列來維護關注這個 epoll 的進程,是因為有時候調用 epoll_wait()的不只一個進程,當多個進程都在關注同一個 epoll 實例時,休眠的進程們通過這個等待隊列就可以逐個被喚醒了。

多個進程關注同一個 epoll 實例,那么有事件發生后先喚醒誰?后喚醒誰?還是一起全喚醒?這涉及到一個稱為“驚群效應”的問題。

Question 3:什么是 epoll 驚群?

答案:多個進程等待在 ep->wq 上,事件觸發后所有進程都被喚醒,但只有其中 1 個進程能夠成功繼續執行的現象。其他被白白喚起的進程等于做了無用功,可能會造成系統負載過高的問題。下面這段代碼能夠直觀感受什么是 epoll 驚群:

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netdb.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>
#define PROCESS_NUM 10
static int create_and_bind (char *port)
{
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(atoi(port));
    bind(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    return fd;
}

static int make_socket_non_blocking (int sfd)
{
    int flags, s;

    flags = fcntl (sfd, F_GETFL, 0);
    if (flags == -1)
    {
        perror ("fcntl");
        return -1;
    }

    flags |= O_NONBLOCK;
    s = fcntl (sfd, F_SETFL, flags);
    if (s == -1)
    {
        perror ("fcntl");
        return -1;
    }

    return 0;
}

#define MAXEVENTS 64

int main (int argc, char *argv[])
{
    int sfd, s;
    int efd;
    struct epoll_event event;
    struct epoll_event *events;

    sfd = create_and_bind("8001");
    if (sfd == -1)
        abort ();

    s = make_socket_non_blocking (sfd);
    if (s == -1)
        abort ();

    s = listen(sfd, SOMAXCONN);
    if (s == -1)
    {
        perror ("listen");
        abort ();
    }

    efd = epoll_create(MAXEVENTS);
    if (efd == -1)
    {
        perror("epoll_create");
        abort();
    }

    event.data.fd = sfd;
    //event.events = EPOLLIN | EPOLLET;
    event.events = EPOLLIN;
    s = epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &event);
    if (s == -1)
    {
        perror("epoll_ctl");
        abort();
    }

    /* Buffer where events are returned */
    events = calloc(MAXEVENTS, sizeof event);
    int k;
    for(k = 0; k < PROCESS_NUM; k++)
    {
        int pid = fork();
        if(pid == 0)
        {

            /* The event loop */
            while (1)
            {
                int n, i;
                n = epoll_wait(efd, events, MAXEVENTS, -1);
                printf("process %d return from epoll_wait!n", getpid());
             for (i = 0; i < n; i++)
                {
                    if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN)))
                    {
                        /* An error has occured on this fd, or the socket is not ready for reading (why were we notified then?) */
                        fprintf (stderr, "epoll errorn");
                        close (events[i].data.fd);
                        continue;
                    }
                    else if (sfd == events[i].data.fd)
                    {
                        /* We have a notification on the listening socket, which means one or more incoming connections. */
                        struct sockaddr in_addr;
                        socklen_t in_len;
                        int infd;
                        char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];

                        in_len = sizeof in_addr;
                        infd = accept(sfd, &in_addr, &in_len);
                        if (infd == -1)
                        {
                            printf("process %d accept failed!n", getpid());
                            break;
                        }
                        printf("process %d accept successed!n", getpid());

                        /* Make the incoming socket non-blocking and add it to the list of fds to monitor. */
                        close(infd);
                    }
                }
            }
        }
    }
    int status;
    wait(&status);
    free (events);
    close (sfd);
    return EXIT_SUCCESS;
}

將服務端的監聽 socket fd 加入到 epoll_wait 的監視集合中,這樣當有客戶端想要建立連接,就會事件觸發 epoll_wait 返回。此時如果 10 個進程同時在 epoll_wait 同一個 epoll 實例就出現了驚群效應。所有 10 個進程都被喚起,但只有一個能成功 accept。

通過十個問題助你徹底理解linux epoll工作原理

 

為了解決 epoll 驚群,內核后續的高版本又提供了 EPOLLEXCLUSIVE 選項和 SO_REUSEPORT 選項,我個人理解兩種解決方案思路上的不同點在于:EPOLLEXCLUSIVE 是在喚起進程階段起作用,只喚起排在隊列最前面的 1 個進程;而 SO_REUSEPORT 是在分配連接時起作用,相當于每個進程自己都有一個獨立的 epoll 實例,內核來決策把連接分配給哪個 epoll。

【文章福利】需要C/C++ Linux服務器架構師學習資料加群812855908(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等)

通過十個問題助你徹底理解linux epoll工作原理

 

Question 4:ep->poll_wait 的作用是什么?

答案:ep->poll_wait 是 epoll 實例中另一個等待隊列。當被監視的文件是一個 epoll 類型時,需要用這個等待隊列來處理遞歸喚醒。

在閱讀內核代碼過程中,ep->wq 還算挺好理解,但我發現伴隨著 ep->wq 喚醒, 還有一個 ep->poll_wait 的喚醒過程。比如下面這段代碼,在 eventpoll.c 中出現了很多次:

/* 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_locked(&ep->wq);
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }

    spin_unlock_irqrestore(&ep->lock, flags);

    atomic_long_inc(&ep->user->epoll_watches);

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

查閱很多資料后才搞明白其實 epoll 也是一種文件類型,其底層驅動也實現了 file_operations 中的 poll 函數,因此一個 epoll 類型的 fd 可以被其他 epoll 實例監視。而 epoll 類型的 fd 只會有“讀就緒”的事件。當 epoll 所監視的非 epoll 類型文件有“讀就緒”事件時,當前 epoll 也會進入“讀就緒”狀態。

因此如果一個 epoll 實例監視了另一個 epoll 就會出現遞歸。舉個例子,如圖所示:

通過十個問題助你徹底理解linux epoll工作原理

 

  1. epollfd1 監視了 2 個“非 epoll”類型的 fd
  2. epollfd2 監視了 epollfd1 和 2 個“非 epoll”類型的 fd

如果 epollfd1 所監視的 2 個 fd 中有可讀事件觸發,fd 的 ep_poll_callback 回調函數會觸發將 fd 放到 epollfd1 的 rdllist 中。此時 epollfd1 本身的可讀事件也會觸發,就需要從 epollfd1 的 poll_wait 等待隊列中找到 epollfd2,調用 epollfd1 的 ep_poll_callback(將 epollfd1 放到 epollfd2 的 rdllist 中)。因此 ep->poll_wait 是用來處理 epoll 間嵌套監視的情況的。

Question 5:ep->rdllist 的作用是什么?

答案:epoll 實例中包含就緒事件的 fd 組成的鏈表。

通過掃描 ep->rdllist 鏈表,內核可以輕松獲取當前有事件觸發的 fd。而不是像 select()/poll() 那樣全量掃描所有被監視的 fd,再從中找出有事件就緒的。因此可以說這一點決定了 epoll 的性能是遠高于 select/poll 的。

看到這里你可能又產生了一個小小的疑問:為什么 epoll 中事件就緒的 fd 會“主動”跑到 rdllist 中去,而不用全量掃描就能找到它們呢? 這是因為每當調用 epoll_ctl 新增一個被監視的 fd 時,都會注冊一下這個 fd 的回調函數 ep_poll_callback, 當網卡收到數據包會觸發一個中斷,中斷處理函數再回調 ep_poll_callback 將這個 fd 所屬的“epitem”添加至 epoll 實例中的 rdllist 中。

Question 6:ep->ovflist 的作用是什么?

答案:在 rdllist 被占用時,用來在不持有 ep->lock 的情況下收集有就緒事件的 fd。

當 epoll 上已經有了一些就緒事件的時候,內核需要掃描 rdllist 將就緒的 fd 返回給用戶態。這一步通過 ep_scan_ready_list 函數來實現。其中 sproc 是一個回調函數(也就是 ep_send_events_proc 函數),來處理數據從內核態到用戶態的復制。

/**
 * ep_scan_ready_list - Scans the ready list in a way that makes possible for the scan code, to call f_op->poll(). Also allows for O(NumReady) performance.
 * @ep: Pointer to the epoll private data structure.
 * @sproc: Pointer to the scan callback.
 * @priv: Private opaque data passed to the @sproc callback.
 * Returns: The same integer error code returned by the @sproc callback.
 */
static int ep_scan_ready_list(struct eventpoll *ep,
                  int (*sproc)(struct eventpoll *,
                       struct list_head *, void *),
                  void *priv)

由于 rdllist 鏈表業務非常繁忙(epoll 增加監視文件、修改監視文件、有事件觸發...等情況都需要操作 rdllist),所以在復制數據到用戶空間時,加了一個 ep->mtx 互斥鎖來保護 epoll 自身數據結構線程安全,此時其他執行流程里有爭搶 ep->mtx 的操作都會因命中 ep->mtx 進入休眠。

但加鎖期間很可能有新事件源源不斷地產生,進而調用 ep_poll_callback(ep_poll_callback 不用爭搶 ep->mtx 所以不會休眠),新觸發的事件需要一個地方來收集,不然就丟事件了。這個用來臨時收集新事件的鏈表就是 ovflist。我的理解是:引入 ovflist 后新產生的事件就不用因為想向 rdllist 里寫而去和 ep_send_events_proc 爭搶自旋鎖(ep->lock), 同時 ep_send_events_proc 也可以放心大膽地在無鎖(不持有 ep->lock)的情況下修改 rdllist。

看代碼時會發現,還有一個 txlist 鏈表,這個鏈表用來最后向用戶態復制數據,rdllist 要先把自己的數據全部轉移到 txlist,然后 rdllist 自己被清空。ep_send_events_proc 遍歷 txlist 處理向用戶空間復制,復制成功后如果是水平觸發(LT)還要把這個事件還回 rdllist,等待下一次 epoll_wait 來獲取它。

ovflist 上的 fd 會合入 rdllist 上等待下一次掃描;如果 txlist 上的 fd 沒有處理完,最后也會合入 rdllist。這 3 個鏈表的關系是這樣:

通過十個問題助你徹底理解linux epoll工作原理

 

Question 7:epitem->pwqlist 隊列的作用是什么?

答案:用來保存這個 epitem 的 poll 等待隊列。

首先介紹下什么是 epitem。epitem 是 epoll 中很重要的一種數據結構, 是紅黑樹和 rdllist 的基本組成元素。需要監聽的文件和事件信息,都被包裝在 epitem 結構里。

struct epitem {
    struct rb_node rbn;  // 用于加入紅黑樹
    struct list_head rdllink; // 用于加入rdllist
    struct epoll_filefd ffd; // 包含被監視文件的文件指針和fd信息
    struct list_head pwqlist; // poll等待隊列
    struct eventpoll *ep; // 所屬的epoll實例
    struct epoll_event event;  // 關注的事件
    /* 其他成員省略 */
};

回憶一下上文說到,每當用戶調用 epoll_ctl()新增一個監視文件,都要給這個文件注冊一個回調函數 ep_poll_callback, 當網卡收到數據后軟中斷會調用這個 ep_poll_callback 把這個 epitem 加入到 ep->rdllist 中。

pwdlist 就是跟 ep_poll_callback 注冊相關的

當調用 epoll_ctl()新增一個監視文件后,內核會為這個 epitem 創建一個 eppoll_entry 對象,通過 eppoll_entry->wait_queue_t->wait_queue_func_t 來設置 ep_poll_callback。pwdlist 為什么要做成一個隊列呢,直接設置成 eppoll_entry 對象不就行了嗎?實際上不同文件類型實現 file_operations->poll 用到等待隊列數量可能不同。雖然大多數都是 1 個,但也有例外。比如“scullpipe”類型的文件就用到了 2 個等待隊列。

pwqlist、epitem、fd、epoll_entry、ep_poll_callback 間的關系是這樣:

通過十個問題助你徹底理解linux epoll工作原理

 

Question 8:epmutex、ep->mtx、ep->lock 3 把鎖的區別是?

答案:鎖的粒度和使用目的不同。

  1. epmutex 是一個全局互斥鎖,epoll 中一共只有 3 個地方用到這把鎖。分別是 ep_free() 銷毀一個 epoll 實例時、eventpoll_release_file() 清理從 epoll 中已經關閉的文件時、epoll_ctl() 時避免 epoll 間嵌套調用時形成死鎖。我的理解是 epmutex 的鎖粒度最大,用來處理跨 epoll 實例級別的同步操作。
  2. ep->mtx 是一個 epoll 內部的互斥鎖,在 ep_scan_ready_list() 掃描就緒列表、eventpoll_release_file() 中執行 ep_remove()刪除一個被監視文件、ep_loop_check_proc()檢查 epoll 是否有循環嵌套或過深嵌套、還有 epoll_ctl() 操作被監視文件增刪改等處有使用。可以看出上述的函數里都會涉及對 epoll 實例中 rdllist 或紅黑樹的訪問,因此我的理解是 ep->mtx 是一個 epoll 實例內的互斥鎖,用來保護 epoll 實例內部的數據結構的線程安全。
  3. ep->lock 是一個 epoll 實例內部的自旋鎖,用來保護 ep->rdllist 的線程安全。自旋鎖的特點是得不到鎖時不會引起進程休眠,所以在 ep_poll_callback 中只能使用 ep->lock,否則就會丟事件。

Question 9:epoll 使用紅黑樹的目的是什么?

答案:用來維護一個 epoll 實例中所有的 epitem。

用戶態調用 epoll_ctl()來操作 epoll 的監視文件時,需要增、刪、改、查等動作有著比較高的效率。尤其是當 epoll 監視的文件數量達到百萬級的時候,選用不同的數據結構帶來的效率差異可能非常大。

通過十個問題助你徹底理解linux epoll工作原理

 

從時間(增、刪、改、查、按序遍歷)、空間(存儲空間大小、擴展性)等方面考量,紅黑樹都是非常優秀的數據結構(當然這以紅黑樹比較高的實現復雜度作為代價)。epoll 紅黑樹中的 epitem 是按什么順序組織的。閱讀代碼可以發現是先比較 2 個文件指針的地址大小,如果相同再比較文件 fd 的大小。

/* Compare RB tree keys */
static inline int ep_cmp_ffd(struct epoll_filefd *p1, struct epoll_filefd *p2)
{
    return (p1->file > p2->file ? +1 : (p1->file < p2->file ? -1 : p1->fd - p2->fd));
}

epoll、epitem、和紅黑樹間的組織關系是這樣:

通過十個問題助你徹底理解linux epoll工作原理

 

Question 10:什么是水平觸發、邊緣觸發?

答案:水平觸發(LT)和邊緣觸發(ET)是 epoll_wait 的 2 種工作模式。水平觸發:關注點是數據(讀操作緩沖區不為空,寫操作緩沖區不為滿),epoll_wait 總會返回就緒。LT 是 epoll 的默認工作模式。

邊緣觸發:關注點是變化,只有監視的文件上有數據變化發生(讀操作關注有數據寫進緩沖區,寫操作關注數據從緩沖區取走),epoll_wait 才會返回。

看一個實驗 ,直觀感受下 2 種模式的區別, 客戶端都是輸入“abcdefgh” 8 個字符,服務端每次接收 2 個字符。

水平觸發時,客戶端輸入 8 個字符觸發了一次讀就緒事件,由于被監視文件上還有數據可讀故一直返回讀就緒,服務端 4 次循環每次都能取到 2 個字符,直到 8 個字符全部讀完。

通過十個問題助你徹底理解linux epoll工作原理

 

邊緣觸發時,客戶端同樣輸入 8 個字符但服務端一次循環讀到 2 個字符后這個讀就緒事件就沒有了。等客戶端再輸入一個字符串后,服務端關注到了數據的“變化”繼續從緩沖區讀接下來的 2 個字符“c”和”d”。

通過十個問題助你徹底理解linux epoll工作原理

 

小結

本文通過 10 個問題,其實也是從 10 個不同的視角去觀察 epoll 這間宏偉的殿堂。至此也基本介紹完了 epoll 從監視事件,到內部數據結構組織、事件處理,最后到 epoll_wait 返回的整體工作過程。最后附上一張 epoll 相關數據結構間的關系圖,在學習 epoll 過程中它曾解答了我心中不少的疑惑,我愿稱之為燈塔~

通過十個問題助你徹底理解linux epoll工作原理

 

分享到:
標簽:linux epoll
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定