深入學習IO多路復用 select/poll/epoll 實現原理

来源:https://www.cnblogs.com/88223100/archive/2023/01/10/Deeply-learn-the-implementation-principle-of-IO-multiplexing-select_poll_epoll.html
-Advertisement-
Play Games

select/poll/epoll 是 Linux 伺服器提供的三種處理高併發網路請求的 IO 多路復用技術,是個老生常談又不容易弄清楚其底層原理的知識點,本文打算深入學習下其實現機制。 Linux 伺服器處理網路請求有三種機制,select、poll、epoll,本文打算深入學習下其實現原理。 ... ...


select/poll/epoll 是 Linux 伺服器提供的三種處理高併發網路請求的 IO 多路復用技術,是個老生常談又不容易弄清楚其底層原理的知識點,本文打算深入學習下其實現機制。

Linux 伺服器處理網路請求有三種機制,select、poll、epoll,本文打算深入學習下其實現原理。

吃水不忘挖井人,最近兩周花了些時間學習了張彥飛大佬的文章 圖解 | 深入揭秘 epoll 是如何實現 IO 多路復用的 和其他文章 ,及出版的書籍《深入理解 Linux 網路》,對阻塞 IO、多路復用、epoll 等的實現原理有了一定的瞭解;飛哥的文章描述底層源碼邏輯比較清晰,就是有時候歸納總結事情本質的抽象程度不夠,涉及內核源碼細節的講述較多,會讓讀者產生一定的學習成本,本文希望在這方面改進一下。

 

0. 結論

本文其他的內容主要是得出了下麵幾個結論:

  1. 伺服器要接收客戶端的數據,要建立 socket 內核結構,主要包含兩個重要的數據結構,(進程)等待隊列,和(數據)接收隊列,socket 在進程中作為一個文件,可以用文件描述符 fd 來表示,為了方便理解,本文中, socket 內核對象 ≈ fd 文件描述符 ≈ TCP 連接;

  2. 阻塞 IO 的主要邏輯是:服務端和客戶端建立了連接 socket 後,服務端的用戶進程通過 recv 函數接收數據時,如果數據沒有到達,則當前的用戶進程的進程描述符和回調函數會封裝到一個進程等待項中,加入到 socket 的進程等待隊列中;如果連接上有數據到達網卡,由網卡將數據通過 DMA 控制器拷貝到內核記憶體的 RingBuffer 中,並向 CPU 發出硬中斷,然後,CPU 向內核中斷進程 ksoftirqd 發出軟中斷信號,內核中斷進程 ksoftirqd 將內核記憶體的 RingBuffer 中的數據根據數據報文的 IP 和埠號,將其拷貝到對應 socket 的數據接收隊列中,然後通過 socket 的進程等待隊列中的回調函數,喚醒要處理該數據的用戶進程;

  3. 阻塞 IO 的問題是:一次數據到達會進行兩次進程切換,一次數據讀取有兩處阻塞,單進程對單連接;

  4. 非阻塞 IO 模型解決了“兩次進程切換,兩處阻塞,單進程對單連接”中的“兩處阻塞”問題,將“兩處阻塞”變成了“一處阻塞”,但依然存在“兩次進程切換,一處阻塞,單進程對單連接”的問題;

  5. 用一個進程監聽多個連接的 IO 多路復用技術解決了“兩次進程切換,一處阻塞,單進程對單連接” 中的“兩次進程切換,單進程對單連接”,剩下了“一處阻塞”,這是 Linux 中同步 IO 都會有的問題,因為 Linux 沒有提供非同步 IO 實現;

  6. Linux 的 IO 多路復用用三種實現:select、poll、epoll。select 的問題是:

a)調用 select 時會陷入內核,這時需要將參數中的 fd_set 從用戶空間拷貝到內核空間,高併發場景下這樣的拷貝會消耗極大資源;(epoll 優化為不拷貝)

b)進程被喚醒後,不知道哪些連接已就緒即收到了數據,需要遍歷傳遞進來的所有 fd_set 的每一位,不管它們是否就緒;(epoll 優化為非同步事件通知)

c)select 只返回就緒文件的個數,具體哪個文件可讀還需要遍歷;(epoll 優化為只返回就緒的文件描述符,無需做無效的遍歷)

d)同時能夠監聽的文件描述符數量太少,是 1024 或 2048;(poll 基於鏈表結構解決了長度限制)

  1. poll 只是基於鏈表的結構解決了最大文件描述符限制的問題,其他 select 性能差的問題依然沒有解決;終極的解決方案是 epoll,解決了 select 的前三個缺點;

  2. epoll 的實現原理看起來很複雜,其實很簡單,註意兩個回調函數的使用:數據到達 socket 的等待隊列時,通過回調函數 ep_poll_callback 找到 eventpoll 對象中紅黑樹的 epitem 節點,並將其加入就緒列隊 rdllist,然後通過回調函數 default_wake_function 喚醒用戶進程 ,並將 rdllist 傳遞給用戶進程,讓用戶進程準確讀取就緒的 socket 的數據。這種回調機制能夠定向準確的通知程式要處理的事件,而不需要每次都迴圈遍歷檢查數據是否到達以及數據該由哪個進程處理,日常開發中可以學習借鑒下這種思想。

1. Linux 怎樣處理網路請求

1.1 阻塞 IO

要講 IO 多路復用,最好先把傳統的同步阻塞的網路 IO 的交互方式剖析清楚。

如果客戶端想向 Linux 伺服器發送一段數據 ,C 語言的實現方式是:

int main()
{
     int fd = socket();      // 創建一個網路通信的socket結構體
     connect(fd, ...);       // 通過三次握手跟伺服器建立TCP連接
     send(fd, ...);          // 寫入數據到TCP連接
     close(fd);              // 關閉TCP連接
}

服務端通過如下 C 代碼接收客戶端的連接和發送的數據:

int main()
{
     fd = socket(...);        // 創建一個網路通信的socket結構體
     bind(fd, ...);           // 綁定通信埠
     listen(fd, 128);         // 監聽通信埠,判斷TCP連接是否可以建立
     while(1) {
         connfd = accept(fd, ...);              // 阻塞建立連接
         int n = recv(connfd, buf, ...);        // 阻塞讀數據
         doSomeThing(buf);                      // 利用讀到的數據做些什麼
         close(connfd);                         // 關閉連接,迴圈等待下一個連接
    }
}

把服務端處理請求的細節展開,得到如下圖所示的同步阻塞網路 IO 的數據接收流程:

圖片
圖1.1 同步阻塞網路IO的數據接收流程

主要步驟是:

1)服務端通過 socket() 函數陷入內核態進行 socket 系統調用,該內核函數會創建 socket 內核對象,主要有兩個重要的結構體,(進程)等待隊列,和(數據)接收隊列,為了方便理解,等待隊列前可以加上進程二字,其實不加更準確,接收隊列同樣;進程等待隊列,存放了服務端的用戶進程 A 的進程描述符和回調函數;socket 的數據接收隊列,存放網卡接收到的該 socket 要處理的數據;

2)進程 A 調用 recv() 函數接收數據,會進入到 recvfrom() 系統調用函數,發現 socket 的數據等待隊列沒有它要接收的數據到達時,進程 A 會讓出 CPU,進入阻塞狀態,進程 A 的進程描述符和它被喚醒用到的回調函數 callback func 會組成一個結構體叫等待隊列項,放入 socket 的進程等待隊列;

3)客戶端的發送數據到達服務端的網卡;

4)網卡首先會將網路傳輸過來的數據通過 DMA 控製程序複製到記憶體環形緩衝區 RingBuffer 中;

5)網卡向 CPU 發出硬中斷

6)CPU 收到了硬中斷後,為了避免過度占用 CPU 處理網路設備請求導致其他設備如滑鼠和鍵盤的消息無法被處理,會調用網路驅動註冊的中斷處理函數,進行簡單快速處理後向內核中斷進程 ksoftirqd 發出軟中斷,就釋放 CPU,由軟中斷進程處理複雜耗時的網路設備請求邏輯;

7)內核中斷進程 ksoftirqd 收到軟中斷信號後,會將網卡複製到記憶體的數據,根據數據報文的 IP 和埠號,將其拷貝到對應 socket 的接收隊列;

8)內核中斷進程 ksoftirqd 根據 socket 的數據接收隊列的數據,通過進程等待隊列中的回調函數,喚醒要處理該數據的進程 A,進程 A 會進入 CPU 的運行隊列,等待獲取 CPU 執行數據處理邏輯;

9)進程 A 獲取 CPU 後,會回到之前調用 recvfrom() 函數時阻塞的位置繼續執行,這時發現 socket 內核空間的等待隊列上有數據,會在內核態將內核空間的 socket 等待隊列的數據拷貝到用戶空間,然後才會回到用戶態執行進程的用戶程式,從而真的解除阻塞

用戶進程 A 在調用 recvfrom() 系統函數時,有兩個階段都是等待的:在數據沒有準備好的時候,進程 A 等待內核 socket 準備好數據;內核准備好數據後,進程 A 繼續等待內核將 socket 等待隊列的數據拷貝到自己的用戶緩衝區;在內核完成數據拷貝到用戶緩衝區後,進程 A 才會從 recvfrom() 系統調用中返回,並解除阻塞狀態。整體流程如下:

圖片
圖1.2 阻塞IO模型

在 IO 阻塞邏輯中,存在下麵三個問題:

  1. 進程在 recv 的時候大概率會被阻塞掉,導致一次進程切換;

  2. 當 TCP 連接上的數據到達服務端的網卡、並從網卡複製到內核空間 socket 的數據等待隊列時,進程會被喚醒,又是一次進程切換;並且,在用戶進程繼續執行完 recvfrom() 函數系統調用,將內核空間的數據拷貝到了用戶緩衝區後,用戶進程才會真正拿到所需的數據進行處理;

  3. 一個進程同時只能等待一條連接,如果有很多併發,則需要很多進程;

總結:一次數據到達會進行兩次進程切換,一次數據讀取有兩處阻塞,單進程對單連接

1.2 非阻塞 IO

為瞭解決同步阻塞 IO 的問題,操作系統提供了非阻塞的 recv() 函數,這個函數的效果是:如果沒有數據從網卡到達內核 socket 的等待隊列時,系統調用會直接返回,而不是阻塞的等待。

如果我們要產生一個非阻塞的 socket,在 C 語言中如下代碼所示:

// 創建socket
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
...
// 更改socket為nonblock
fcntl(sock_fd, F_SETFL, fdflags | O_NONBLOCK);
// connect
....
while(1)  {
    int recvlen = recv(sock_fd, recvbuf, RECV_BUF_SIZE) ;
    ......
}
...

非阻塞 IO 模型如下圖所示:

圖片
圖1.3 非阻塞IO模型

從上圖中,我們知道,非阻塞 IO,是將等待數據從網卡到達 socket 內核空間這一部分變成了非阻塞的,用戶進程調用 recvfrom() 會重覆發送請求檢查數據是否到達內核空間,如果沒有到,則立即返回,不會阻塞。不過,當數據已經到達內核空間的 socket 的等待隊列後,用戶進程依然要等待 recvfrom() 函數將數據從內核空間拷貝到用戶空間,才會從 recvfrom() 系統調用函數中返回。

非阻塞 IO 模型解決了“兩次進程切換,兩處阻塞,單進程對單連接”中的“兩處阻塞”問題,將“兩處阻塞”變成了“一處阻塞”,但依然存在“兩次進程切換,一處阻塞,單進程對單連接”的問題。

1.3 IO 多路復用

要解決“兩次進程切換,單進程對單連接”的問題,伺服器引入了 IO 多路復用技術,通過一個進程處理多個 TCP 連接,不僅降低了伺服器處理網路請求的進程數,而且不用在每個連接的數據到達時就進行進程切換,進程可以一直運行並只處理有數據到達的連接,當然,如果要監聽的所有連接都沒有數據到達,進程還是會進入阻塞狀態,直到某個連接有數據到達時被回調函數喚醒。

IO 多路復用模型如下圖所示:

圖片
圖1.4 IO多路復用模型

從上圖可知,系統調用 select 函數阻塞執行並返回數據就緒的連接個數,然後調用 recvfrom() 函數將到達內核空間的數據拷貝到用戶空間,儘管這兩個階段都是阻塞的,但是由於只會處理有數據到達的連接,整體效率會有極大的提升。

到這裡,阻塞 IO 模型的“兩次進程切換,兩處阻塞,單進程對單連接”問題,通過非阻塞 IO 和多路復用技術,就只剩下了“一處阻塞”這個問題,即 Linux 伺服器上用戶進程一定要等待數據從內核空間拷貝到用戶空間,如果這個步驟也變成非阻塞的,也就是進程調用 recvfrom 後立刻返回,內核自行去準備好數據並將數據從內核空間拷貝到用戶空間、再 notify 通知用戶進程去讀取數據,那就是 IO 非同步調用,不過,Linux 沒有提供非同步 IO 的實現,真正意義上的網路非同步 IO 是 Windows 下的 IOCP(IO 完成埠)模型,這裡就不探討了。

2. 詳解 select、poll、epoll 實現原理

2.1 select 實現原理

select 函數定義

Linux 提供的 select 函數的定義如下:

int select(
    int nfds,                     // 監控的文件描述符集里最大文件描述符加1
    fd_set *readfds,              // 監控有讀數據到達文件描述符集合,引用類型的參數
    fd_set *writefds,             // 監控寫數據到達文件描述符集合,引用類型的參數
    fd_set *exceptfds,            // 監控異常發生達文件描述符集合,引用類型的參數
    struct timeval *timeout);     // 定時阻塞監控時間

readfds、writefds、errorfds 是三個文件描述符集合。select 會遍歷每個集合的前 nfds 個描述符,分別找到可以讀取、可以寫入、發生錯誤的描述符,統稱為“就緒”的描述符。然後用找到的子集替換這三個引用參數中的對應集合,返回所有就緒描述符的數量。

timeout 參數表示調用 select 時的阻塞時長。如果所有 fd 文件描述符都未就緒,就阻塞調用進程,直到某個描述符就緒,或者阻塞超過設置的 timeout 後,返回。如果 timeout 參數設為 NULL,會無限阻塞直到某個描述符就緒;如果 timeout 參數設為 0,會立即返回,不阻塞。

文件描述符 fd

文件描述符(file descriptor)是一個非負整數,從 0 開始。進程使用文件描述符來標識一個打開的文件。Linux 中一切皆文件。

系統為每一個進程維護了一個文件描述符表,表示該進程打開文件的記錄表,而文件描述符實際上就是這張表的索引。每個進程預設都有 3 個文件描述符:0 (stdin)、1 (stdout)、2 (stderr)。

socket

socket 可以用於同一臺主機的不同進程間的通信,也可以用於不同主機間的通信。操作系統將 socket 映射到進程的一個文件描述符上,進程就可以通過讀寫這個文件描述符來和遠程主機通信。

socket 是進程間通信規則的高層抽象,而 fd 提供的是底層的具體實現。socket 與 fd 是一一對應的。通過 socket 通信,實際上就是通過文件描述符 fd 讀寫文件。

本文中,為了方便理解,可以認為 socket 內核對象 ≈ fd 文件描述符 ≈ TCP 連接。

fd_set 文件描述符集合

select 函數參數中的 fd_set 類型表示文件描述符的集合。

由於文件描述符 fd 是一個從 0 開始的無符號整數,所以可以使用 fd_set 的二進位每一位來表示一個文件描述符。某一位為 1,表示對應的文件描述符已就緒。比如比如設 fd_set 長度為 1 位元組,則一個 fd_set 變數最大可以表示 8 個文件描述符。當 select 返回 fd_set = 00010011 時,表示文件描述符 1、2、5 已經就緒。

fd_set 的 API

fd_set 的使用涉及以下幾個 API:

#include <sys/select.h>
int FD_ZERO(int fd, fd_set *fdset);  // 將 fd_set 所有位置 0
int FD_CLR(int fd, fd_set *fdset);   // 將 fd_set 某一位置 0
int FD_SET(int fd, fd_set *fd_set);  // 將 fd_set 某一位置 1
int FD_ISSET(int fd, fd_set *fdset); // 檢測 fd_set 某一位是否為 1

select 監聽多個連接的用法

服務端使用 select 監控多個連接的 C 代碼是:

#define MAXCLINE 5       // 連接隊列中的個數
int fd[MAXCLINE];        // 連接的文件描述符隊列

int main(void)
{
      sock_fd = socket(AF_INET,SOCK_STREAM,0)          // 建立主機間通信的 socket 結構體
      .....
      bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr);         // 綁定socket到當前伺服器
      listen(sock_fd, 5);  // 監聽 5 個TCP連接

      fd_set fdsr;         // bitmap類型的文件描述符集合,01100 表示第1、2位有數據到達
      int max;

      for(i = 0; i < 5; i++)
      {
          .....
          fd[i] = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);   // 跟 5 個客戶端依次建立 TCP 連接,並將連接放入 fd 文件描述符隊列
      }

      while(1)               // 迴圈監聽連接上的數據是否到達
      {
        FD_ZERO(&fdsr);      // 對 fd_set 即 bitmap 類型進行複位,即全部重置為0

        for(i = 0; i < 5; i++)
        {
             FD_SET(fd[i], &fdsr);      // 將要監聽的TCP連接對應的文件描述符所在的bitmap的位置置1,比如 0110010110 表示需要監聽第 1、2、5、7、8個文件描述符對應的 TCP 連接
        }

        ret = select(max + 1, &fdsr, NULL, NULL, NULL);  // 調用select系統函數進入內核檢查哪個連接的數據到達

        for(i=0;i<5;i++)
        {
            if(FD_ISSET(fd[i], &fdsr))      // fd_set中為1的位置表示的連接,意味著有數據到達,可以讓用戶進程讀取
            {
                ret = recv(fd[i], buf,sizeof(buf), 0);
                ......
            }
        }
  }

從註釋中,我們可以看到,在一個進程中使用 select 監控多個連接的主要步驟是:

1)調用 socket() 函數建立主機間通信的 socket 結構體,bind() 綁定 socket 到當前伺服器,listen() 監聽五個 TCP 連接;

2)調用 accept() 函數建立和 5 個客戶端的 TCP 連接,並把連接的文件描述符放入 fd 文件描述符隊列;

3) 定義一個 fd_set 類型的變數 fdsr;

4)調用 FD_ZERO,將 fdsr 所有位置 0;

5)調用 FD_SET,將 fdsr 要監聽的幾個文件描述符的位置 1,表示要監聽這幾個文件描述符指向的連接;

6)調用 select() 函數,並將 fdsr 參數傳遞給 select;

7)select 會將 fdsr 中就緒的位置 1,未就緒的位置 0,返回就緒的文件描述符的數量;

8)當 select 返回後,調用 FD_ISSET 檢測哪些位為 1,表示對應文件描述符對應的連接的數據已經就緒,可以調用 recv 函數讀取該連接的數據了。

select 的執行過程

在伺服器進程 A 啟動的時候,要監聽的連接的 socket 文件描述符是 3、4、5,如果這三個連接均沒有數據到達網卡,則進程 A 會讓出 CPU,進入阻塞狀態,同時會將進程 A 的進程描述符和被喚醒時用到的回調函數組成等待隊列項加入到 socket 對象 3、4、5 的進程等待隊列中,註意,這時 select 調用時,fdsr 文件描述符集會從用戶空間拷貝到內核空間,如下圖所示:

圖片
圖2.1 select進程啟動時,沒有數據到達網卡

當網卡接收到數據,然後網卡通過中斷信號通知 CPU 有數據到達,執行中斷程式,中斷程式主要做了兩件事:

1)將網路數據寫入到對應 socket 的數據接收隊列裡面;

2)喚醒隊列中的等待進程 A,重新將進程 A 放入 CPU 的運行隊列中;

假設連接 3、5 有數據到達網卡,註意,這時 select 調用結束時,fdsr 文件描述符集會從內核空間拷貝到用戶空間:

圖片
圖2.2 select進程有數據到達時,會通過回調函數喚醒進程進行數據的讀取

select 的缺點

從上面兩圖描述的執行過程,可以發現 select 實現多路復用有以下缺點:

1.性能開銷大

1)調用 select 時會陷入內核,這時需要將參數中的 fd_set 從用戶空間拷貝到內核空間,select 執行完後,還需要將 fd_set 從內核空間拷貝回用戶空間,高併發場景下這樣的拷貝會消耗極大資源;(epoll 優化為不拷貝)

2)進程被喚醒後,不知道哪些連接已就緒即收到了數據,需要遍歷傳遞進來的所有 fd_set 的每一位,不管它們是否就緒;(epoll 優化為非同步事件通知)

3)select 只返回就緒文件的個數,具體哪個文件可讀還需要遍歷;(epoll 優化為只返回就緒的文件描述符,無需做無效的遍歷)

2.同時能夠監聽的文件描述符數量太少。受限於 sizeof(fd_set) 的大小,在編譯內核時就確定了且無法更改。一般是 32 位操作系統是 1024,64 位是 2048。(poll、epoll 優化為適應鏈表方式)

第 2 個缺點被 poll 解決,第 1 個性能差的缺點被 epoll 解決。

2.2 poll 實現原理

和 select 類似,只是描述 fd 集合的方式不同,poll 使用 pollfd 結構而非 select 的 fd_set 結構。

struct pollfd {
    int fd;           // 要監聽的文件描述符
    short events;     // 要監聽的事件
    short revents;    // 文件描述符fd上實際發生的事件
};

管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但 poll 無最大文件描述符數量的限制因其基於鏈表存儲

select 和 poll 在內部機制方面並沒有太大的差異。相比於 select 機制,poll 只是取消了最大監控文件描述符數限制,並沒有從根本上解決 select 存在的問題。

2.3 epoll 實現原理

epoll 是對 select 和 poll 的改進,解決了“性能開銷大”和“文件描述符數量少”這兩個缺點,是性能最高的多路復用實現方式,能支持的併發量也是最大。

epoll 的特點是:

1)使用紅黑樹存儲一份文件描述符集合,每個文件描述符只需在添加時傳入一次,無需用戶每次都重新傳入;—— 解決了 select 中 fd_set 重覆拷貝到內核的問題

2)通過非同步 IO 事件找到就緒的文件描述符,而不是通過輪詢的方式;

3)使用隊列存儲就緒的文件描述符,且會按需返回就緒的文件描述符,無須再次遍歷;

epoll 的基本用法是:

int main(void)
{
      struct epoll_event events[5];
      int epfd = epoll_create(10);         // 創建一個 epoll 對象
      ......
      for(i = 0; i < 5; i++)
      {
          static struct epoll_event ev;
          .....
          ev.data.fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);
          ev.events = EPOLLIN;
          epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);  // 向 epoll 對象中添加要管理的連接
      }

      while(1)
      {
         nfds = epoll_wait(epfd, events, 5, 10000);   // 等待其管理的連接上的 IO 事件

         for(i=0; i<nfds; i++)
         {
             ......
             read(events[i].data.fd, buff, MAXBUF)
         }
  }

主要涉及到三個函數:

int epoll_create(int size);   // 創建一個 eventpoll 內核對象

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);   // 將連接到socket對象添加到 eventpoll 對象上,epoll_event是要監聽的事件

int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);      // 等待連接 socket 的數據是否到達

epoll_create

epoll_create 函數會創建一個 struct eventpoll 的內核對象,類似 socket,把它關聯到當前進程的已打開文件列表中。

eventpoll 主要包含三個欄位:

struct eventpoll {
 wait_queue_head_t wq;      // 等待隊列鏈表,存放阻塞的進程

 struct list_head rdllist;  // 數據就緒的文件描述符都會放到這裡

 struct rb_root rbr;        // 紅黑樹,管理用戶進程下添加進來的所有 socket 連接
        ......
}

wq:等待隊列,如果當前進程沒有數據需要處理,會把當前進程描述符和回調函數 default_wake_functon 構造一個等待隊列項,放入當前 wq 對待隊列,軟中斷數據就緒的時候會通過 wq 來找到阻塞在 epoll 對象上的用戶進程。

rbr:一棵紅黑樹,管理用戶進程下添加進來的所有 socket 連接。

rdllist:就緒的描述符的鏈表。當有的連接數據就緒的時候,內核會把就緒的連接放到 rdllist 鏈表裡。這樣應用進程只需要判斷鏈表就能找出就緒進程,而不用去遍歷整棵樹。

eventpoll 的結構如圖 2.3 所示:

圖片
圖2.3 eventpoll對象的結構

epoll_ctl

epoll_ctl 函數主要負責把服務端和客戶端建立的 socket 連接註冊到 eventpoll 對象里,會做三件事:

1)創建一個 epitem 對象,主要包含兩個欄位,分別存放 socket fd 即連接的文件描述符,和所屬的 eventpoll 對象的指針;

2)將一個數據到達時用到的回調函數添加到 socket 的進程等待隊列中,註意,跟第 1.1 節的阻塞 IO 模式不同的是,這裡添加的 socket 的進程等待隊列結構中,只有回調函數,沒有設置進程描述符,因為在 epoll 中,進程是放在 eventpoll 的等待隊列中,等待被 epoll_wait 函數喚醒,而不是放在 socket 的進程等待隊列中;

3)將第 1)步創建的 epitem 對象插入紅黑樹;

圖片
圖2.4 epoll_ctl執行結果

epoll_wait

epoll_wait 函數的動作比較簡單,檢查 eventpoll 對象的就緒的連接 rdllist 上是否有數據到達,如果沒有就把當前的進程描述符添加到一個等待隊列項里,加入到 eventpoll 的進程等待隊列里,然後阻塞當前進程,等待數據到達時通過回調函數被喚醒。

當 eventpoll 監控的連接上有數據到達時,通過下麵幾個步驟喚醒對應的進程處理數據:

1)socket 的數據接收隊列有數據到達,會通過進程等待隊列的回調函數 ep_poll_callback 喚醒紅黑樹中的節點 epitem;

2)ep_poll_callback 函數將有數據到達的 epitem 添加到 eventpoll 對象的就緒隊列 rdllist 中;

3)ep_poll_callback 函數檢查 eventpoll 對象的進程等待隊列上是否有等待項,通過回調函數 default_wake_func 喚醒這個進程,進行數據的處理;

4)當進程醒來後,繼續從 epoll_wait 時暫停的代碼繼續執行,把 rdlist 中就緒的事件返回給用戶進程,讓用戶進程調用 recv 把已經到達內核 socket 等待隊列的數據拷貝到用戶空間使用。

圖片
圖2.5 epoll_wait 在有數據到達 socket 時、依次通過兩個回調函數喚醒進程

3. 總結

從阻塞 IO 到 epoll 的實現中,我們可以看到 wake up 回調函數機制被頻繁的使用,至少有三處地方:一是阻塞 IO 中數據到達 socket 的等待隊列時,通過回調函數喚醒進程,二是 epoll 中數據到達 socket 的等待隊列時,通過回調函數 ep_poll_callback 找到 eventpoll 中紅黑樹的 epitem 節點,並將其加入就緒列隊 rdllist,三是通過回調函數 default_wake_func 喚醒用戶進程 ,並將 rdllist 傳遞給用戶進程,讓用戶進程準確讀取數據 。從中可知,這種回調機制能夠定向準確的通知程式要處理的事件,而不需要每次都迴圈遍歷檢查數據是否到達以及數據該由哪個進程處理,提高了程式效率,在日常的業務開發中,我們也可以借鑒下這一機制。

References

圖解 | 深入揭秘 epoll 是如何實現 IO 多路復用的!

圖解 Linux 網路包接收過程

圖解 | 深入理解高性能網路開發路上的絆腳石 - 同步阻塞網路 IO

從 linux 源碼看 socket 的阻塞和非阻塞

Select、Poll、Epoll 詳解

你管這破玩意叫 IO 多路復用?

I/O 多路復用,select / poll / epoll 詳解

大話 Select、Poll、Epoll

IO 多路復用底層原理全解,select,poll,epoll,socket,系統中斷,進程調度,系統調用

 

作者:mingguangtu

本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/Deeply-learn-the-implementation-principle-of-IO-multiplexing-select_poll_epoll.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 項目地址:https://github.com/pikeduo/TXTReader PyQt5中文手冊:https://maicss.gitbook.io/pyqt-chinese-tutoral/pyqt5/ QtDesigner學習地址:https://youcans.blog.csdn.net ...
  • 14. 最長公共首碼 題目描述 編寫一個函數來查找字元串數組中的最長公共首碼。 如果不存在公共首碼,返回空字元串 ""。 方法 暴力演算法 先判斷字元串數組是否有為空,為空直接返回空 令第一個字元串作為基準進行比較 設置一個長度,作為最後最長公共首碼的長度 迴圈判斷,選取最小長度 代碼 package ...
  • 2023-01-09 一、在IDEA中創建Maven版的web工程 (1)步驟: ①創建一個maven模塊,命名為“maven_web_end”,之後需要創建web工程的目錄。在“maven_web_end.src.main”下創建“webapp”文件夾(命名必須為webapp,否則識別不了);在“ ...
  • 作者:小牛呼嚕嚕 | https://xiaoniuhululu.com 電腦內功、JAVA底層、面試相關資料等更多精彩文章在公眾號「小牛呼嚕嚕 」 大家好,我是呼嚕嚕,這次我們一起來看看Java記憶體區域,本文 基於HotSpot 虛擬機,JDK8, 乾貨滿滿 前言 Java 記憶體區域, 也叫運行 ...
  • Excelize 是 Go 語言編寫的用於操作 Office Excel 文檔基礎庫,支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多種文檔格式。2023年1月9日,社區正式發佈了 2.7.0 版本,該版本包含了多項新增功能、錯誤修複和相容性提升優化。 ...
  • ThreadLocal是一個關於創建線程局部變數的類。 通常情況下,我們創建的變數是可以被任何一個線程訪問並修改的。而使用ThreadLocal創建的變數只能被當前線程訪問,其他線程則無法訪問和修改。ThreadLocal在設計之初就是為解決併發問題而提供一種方案,每個線程維護一份自己的數據,達到線... ...
  • 移動滑鼠到你想要的位置,然後進行點擊,某些時候是很有用的 using System; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; private void button ...
  • 1 ansible 常用指令總結,並附有相關示例。 /usr/bin/ansible 主程式,臨時命令執行工具 /usr/bin/ansible-doc 查看配置文檔,模塊功能查看工具,相當於man /usr/bin/ansible-playbook 定製自動化任務,編排劇本工具,相當於腳本 /us ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...