通過完整示例來理解如何使用 epoll

LynnShaw發表於2015-10-29

網路伺服器通常使用一個獨立的程式或執行緒來實現每個連線。由於高效能應用程式需要同時處理大量的客戶端,這種方法就不太好用了,因為資源佔用和上下文切換時間等因素影響了同時處理大量客戶的能力。另一種方法是在一個執行緒中使非阻塞 I/O,以及一些就緒通知方法,即當你可以在一個套接字上讀寫更多資料的時候告訴你。

本文介紹了 Linux 的 epoll(7) 機制,它是 Linux 最好的就緒通知機制。我們用 C 語言編寫了示例程式碼,實現了一個完整的 TCP 伺服器。 我假設您有一定 C 語言程式設計經驗,知道如何在 Linux 上編譯和執行程式,並且可以閱讀手冊檢視各種需要的 C 函式。

epoll 是在 Linux 2.6 中引入的,在其他類 UNIX 操作系統上不可用。它提供了一個類似於 select(2) 和 poll(2) 函式的功能:

  • select(2) 一次可以監測 FD_SETSIZE數量大小的描述符,FD_SETSIZE 通常是一個在 libc 編譯時指定的小數字。
  • poll(2) 一次可以監測的描述符數量並沒有限制,但撇開其它因素,我們每次都不得不檢查就緒通知,線性掃描所有通過描述符,這樣時間複雜度為 O(n)而且很慢。

epoll 沒有這些固定限制,也不執行任何線性掃描。因此它可以更高效地執行和處理大量事件。

一個 epoll 例項可由 epoll_create(2) 或 epoll_create1(2) (它們採用不同的引數)建立,它們的返回值是一個 epoll 例項。epoll_ctl(2) 用來新增或刪除監聽 epoll 例項的描述符。epoll_wait(2) 用來等待被監聽的描述符事件,一直阻塞到事件可用。更多資訊請參見相關手冊。

當描述符被新增到 epoll 例項時,有兩種模式:電平觸發和邊緣觸發(譯者注:借鑑電路里面的概念)。當你使用電平觸發模式,並且資料可以被讀取,epoll_wait(2) 函式總是會返回就緒事件。如果你還沒有讀完資料,並且再次在 epoll 例項上呼叫 epoll_wait(2) 函式監聽這個描述符,由於還有資料可讀,那麼它會再次返回這個事件。在邊緣觸發模式下,你只會得到一次就緒通知。如果你沒有將資料全部讀走,並且再次在 epoll 例項上呼叫 epoll_wait(2) 函式監聽這個描述符,它就會阻塞,因為就緒事件已經傳送過了。

傳遞到 epoll_ctl(2) 的 epoll 事件結構體如下。對每一個被監聽的描述符,你可以關聯到一個整數或者一個使用者資料的指標。

現在我們開始寫程式碼。我們將實現一個小的 TCP 服務器,將傳送到這個套接字的所有資料列印到標準輸出上。首先編寫一個 create_and_bind() 函式,用來建立和繫結 TCP 套接字:

create_and_bind() 包含一個標準程式碼塊,用一種可移植的方式來獲得 IPv4 和 IPv6 套接字。它接受一個 port 字串引數,可由 argv[1] 傳遞。getaddrinfo(3) 函式返回一堆 addrinfo 結構體到 result 變數中,它們與傳入的 hints引數是相容的。addrinfo結構體像這樣:

我們依次遍歷這些結構體並用它們建立套接字,直到可以建立並繫結一個套接字。如果成功了,create_and_bind() 返回這個套接字描述符。如果失敗則返回 -1

下面我們編寫一個函式,用於將套接字設定為非阻塞狀態。make_socket_non_blocking() 為傳入的 sfd 引數設定 O_NONBLOCK 標誌:

現在說說 main() 函式吧,它裡面包含了這個程式的事件迴圈。這是主要程式碼:

main() 首先呼叫 create_and_bind() 新建套接字。然後把套接字設定非阻塞模式,再呼叫listen(2)。接下來它建立一個 epoll 例項 efd,新增監聽套接字 sfd ,用電平觸發模式來監聽輸入事件。

外層的 while 迴圈是主要事件迴圈。它呼叫epoll_wait(2),執行緒保持阻塞以等待事件到來。當事件就緒,epoll_wait(2) 用 events 引數返回事件,這個引數是一群 epoll_event 結構體。

當我們新增新的監聽輸入連線以及刪除終止的現有連線時,efd 這個 epoll 例項在事件迴圈中不斷更新。

當事件是可用的,它們可以有三種型別:

  • 錯誤:當一個錯誤連線出現,或事件不是一個可以讀取資料的通知,我們只要簡單地關閉相關的描述符。關閉描述符會自動地移除 efd 這個 epoll 例項的監聽列表。
  • 新連線:當監聽描述符 sfd 是可讀狀態,這表明一個或多個連線已經到達。當有一個新連線, accept(2) 接受這個連線,列印一條相應的訊息,把這個到來的套接字設置為非阻塞狀態,並將其新增到 efd 這個 epoll 例項的監聽列表。
  • 客戶端資料:當任何一個客戶端描述符的資料可讀時,我們在內部 while 迴圈中用 read(2) 以 512 位元組大小讀取資料。這是因為當前我們必須讀走所有可讀的資料,當監聽描述符是邊緣觸發模式下,我們不會再得到事件。被讀取的資料使用 write(2) 被寫入標準輸出(fd=1)。如果 read(2) 返回 0,這表示 EOF 並且我們可以關閉這個客戶端的連線。如果返回 -1,errno 被設定為 EAGAIN,這表示這個事件的所有資料被讀走,我們可以返回主迴圈。

就是這樣。它在一個迴圈中執行,在監聽列表中新增和刪除描述符。

下載 epoll-example.c 程式碼。

更新1:電平和邊緣觸發的定義被顛倒錯誤了(雖然程式碼是正確的)。這是被Reddit使用者 bodski 發現的。文章現在正確了。我應該在釋出前校對的。對不起,並感謝謝指出錯誤。:)

更新2:程式碼被修改成連線將被阻塞時才執行accept(2),所以如果多個連線到達,我們全部接受。這是Reddit使用者 pitchford 提出。謝謝你的評論。 :)

相關文章