1. 程式人生 > >[譯] Async IO on Linux: select, poll, and epoll

[譯] Async IO on Linux: select, poll, and epoll

雖然一直是個 Java 程式設計師,但是 selectpollepoll 這些詞彙還是經常聽見的,上次寫完 UNIX I/O 之後又去再看了一下這部分內容,遇到了這篇文章,感覺不錯特此翻譯下來,下面是正文。

Chapter63:Alternative I/O models

章節內容主要關於當新的資料輸入/輸出到來時,如何監聽如此多的檔案描述符呢?誰需要同時關注這麼多的檔案描述符呢?答案是 Server

例如,你在 Linux 上用 node.js 寫一個 web server,實際上它會使用 epoll 系統呼叫。讓我們談談 epollselectpoll 的區別在哪裡,和它們是如何工作的。

Servers need to watch a lot of file descriptors

假設你是一個 web server,每次你使用 accept 系統呼叫接收一個連線時,你會得到一個新的檔案描述符來表示那個連線。

作為一個 web server,同一時間你可能有成千上萬的連線。你需要知道何時某個連線有新的資料需要發給你,這樣你才能處理請求並返回響應。

怎樣監聽這些檔案描述符呢?你可能會用下面的迴圈方式:

for x in open_connections:
    if has_new_input(x):
        process_input(x)

上述程式碼的問題是,它會浪費許多 CPU。與其消耗所有 CPU 時間去詢問:“有資料更新麼?現在呢?現在呢?現在有麼?”,我們還不如直接告訴核心,“現在有 100 個檔案描述符,當其中一個有資料更新時通知我。”

有三個系統呼叫方法可以讓你達到告知 Linux 核心去監聽檔案描述符的目的,它們分別是 pollepollselect,讓我們先從 pollselect 開始,因為章節內容就是從他倆先開始的。

First way: select & poll

這兩個系統呼叫在任何 UNIX 系統中都有,而 epoll 是 Linux 獨佔的。他倆的工作原理是:

  1. 傳給它們一堆等待資料的檔案描述符
  2. 它們會回答你,其中哪個檔案描述符對應的資料準備好,可以讀寫了它們會回答你,其中哪個檔案描述符對應的資料準備好,可以讀寫了

我從書裡學到的第一個令人驚訝的事實是,pollselect 的程式碼幾乎是相同的!我去看了一下 Linux 核心原始碼中關於 poll

select 的定義之後確信這是真的。

它倆都呼叫了很多相同的函式,書裡特別提到的是 poll 返回了一堆可能的 fd 集合例如

POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR

select 僅僅告知你

there’s input / there’s output / there’s an error

相比於 poll 返回的更具體的結果,例如 fd 集合,select 僅僅返回粗粒度的資訊,例如“你可以讀取資訊了”。你可以自己閱讀這部分功能的具體程式碼。

我從書中學習到的另一個事實是,在檔案描述符稀少的情況下,poll 的效能比 select 更好。為了證明這點,你可以看看 pollselect 的方法簽名:

int ppoll(struct pollfd *fds, nfds_t nfds,
          const struct timespec *tmo_p, const sigset_t
          *sigmask)`
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
            fd_set *exceptfds, const struct timespec *timeout,
            const sigset_t *sigmask);

poll 方法中,你告訴它 “這是我想監聽的檔案描述符:1,3,8,19 等等” (即是 pollfd 引數)。select 方法中,你告訴它 “我希望監聽 19 個檔案描述符,我關心其中某個fd的三種(read/write/exception)狀態變更(select 使用三個點陣圖來表示三個 fdset)” 所以當 select 執行時,它會輪詢這 19 個檔案描述符,即使你只關心其中幾個。

書中還有許多 pollselect 不同的細節,但是這兩點是我學到的最主要的。

why don’t we use poll and select ?

但是,我們說了你的 nods.js web 伺服器不會使用 select 或者 poll,而是使用 epoll,這是為什麼呢?

從書中可得:

每次呼叫 select 或者 poll,核心必須檢查所有上述的檔案描述符來發現它們是否準備好了。當監聽的檔案描述符數量非常多、範圍非常大時,耗時就會很誇張、效能自然也不好。

總結看就是核心不會記錄它應該監聽的檔案描述符列表。

Signal-driven I/O (is this a thing people use ?)

書中描述了兩種通知核心記錄監聽檔案描述符列表的方式:訊號驅動式 I/O 和 epoll。訊號驅動式 I/O 讓核心在一個檔案描述符更新資料時,通過呼叫 fcntl 返回一個訊號給你。我從沒聽過任何人使用這個,書中敘述看上去就認為 epoll 是更好的,所以我們乾脆就直接忽略了,來談談 epoll 吧。

level-triggered vs edge-triggered

在我們談論 epoll 時,我們先來討論一下 “level-triggered”“edge-triggered” 兩種檔案描述符通知模式。我之前從沒聽過這種專業術語(可能來自於電子工程界?)總結起來,接受通知有兩種方式:

  1. 拿到每個可讀的且是你感興趣的 fd 的列表(level-triggered
  2. 每當一個 fd 可讀時就收到一個通知(edge-triggered

what’s epoll ?

好,我們可以來講講 epoll 了。我很興奮,因為之前我瀏覽程式碼經常見到 epoll_wait,我經常困惑它到底有什麼作用。

epoll 類的系統呼叫(epoll_create, epoll_ctl, epoll_wait)給予了 Linux 核心檔案描述符來跟蹤和檢查資料更新的功能。

下面是使用 epoll 的步驟:

  1. 呼叫 epoll_create 告訴核心你將要 epolling 了!它會返回你一個 id
  2. 呼叫 epoll_ctl 來告訴核心你關心哪些檔案描述符。有趣的是,你可以傳進許多檔案描述符(pipes,FIFOs,sockets,POSIX message queues,inotify instances,devices & more),但不是有規律的檔案。我覺得是合理的 —— pipes & sockets 的 API 很簡單(一個處理對 pipe 的寫,一個處理讀),所以可以說 “這個 pipe 有新的資料可以讀” 。但檔案是另類的,你可以朝一個檔案的中間寫入資料!所以你不能簡單的說 “該檔案有新的資料可以讀取”。
  3. 呼叫 epoll_wait 來等待你關心的檔案有資料更新

performance: select & poll vs epoll

書中有個表格比較了監聽十萬個操作下的效能優劣:
所以當你需要監聽大於 10 個 fd 時,使用 epoll 確實會快很多。

License