原文位址:Async IO on Linux: select, poll, and epoll
作者:Julia Evans
雖然一直是個 Java 程式員,但是
select
、
poll
、
epoll
這些詞彙還是經常聽見的,上次寫完 UNIX I/O 之後又去再看了一下這部分内容,遇到了這篇文章,感覺不錯特此翻譯下來,下面是正文。
今天講一講我從這本書《The Linux Programming Interface》上學到的三個系統調用:
select
、
poll
和
epoll
。
Chapter63:Alternative I/O models
章節内容主要關于當新的資料輸入/輸出到來時,如何監聽如此多的檔案描述符呢?誰需要同時關注這麼多的檔案描述符呢?答案是
Server
。
例如,你在 Linux 上用 node.js 寫一個 web server,實際上它會使用 epoll 系統調用。讓我們談談
epoll
和
select
、
poll
的差別在哪裡,和它們是如何工作的。
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 核心去監聽檔案描述符的目的,它們分别是
poll
、
epoll
和
select
,讓我們先從
poll
和
select
開始,因為章節内容就是從他倆先開始的。
First way: select & poll
這兩個系統調用在任何 UNIX 系統中都有,而 epoll 是 Linux 獨占的。他倆的工作原理是:
- 傳給它們一堆等待資料的檔案描述符
- 它們會回答你,其中哪個檔案描述符對應的資料準備好,可以讀寫了它們會回答你,其中哪個檔案描述符對應的資料準備好,可以讀寫了
我從書裡學到的第一個令人驚訝的事實是,
poll
和
select
的代碼幾乎是相同的!我去看了一下 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
更好。為了證明這點,你可以看看
poll
和
select
的方法簽名:
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 個檔案描述符,即使你隻關心其中幾個。
書中還有許多
poll
和
select
不同的細節,但是這兩點是我學到的最主要的。
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”
兩種檔案描述符通知模式。我之前從沒聽過這種專業術語(可能來自于電子工程界?)總結起來,接受通知有兩種方式:
- 拿到每個可讀的且是你感興趣的 fd 的清單(
)level-triggered
- 每當一個 fd 可讀時就收到一個通知(
)edge-triggered
what’s epoll ?
好,我們可以來講講 epoll 了。我很興奮,因為之前我浏覽代碼經常見到
epoll_wait
,我經常困惑它到底有什麼作用。
epoll 類的系統調用(
epoll_create
,
epoll_ctl
,
epoll_wait
)給予了 Linux 核心檔案描述符來跟蹤和檢查資料更新的功能。
下面是使用
epoll
的步驟:
- 調用
告訴核心你将要 epolling 了!它會傳回你一個 idepoll_create
- 調用
來告訴核心你關心哪些檔案描述符。有趣的是,你可以傳進許多檔案描述符(pipes,FIFOs,sockets,POSIX message queues,inotify instances,devices & more),但不是有規律的檔案。我覺得是合理的 —— pipes & sockets 的 API 很簡單(一個處理對 pipe 的寫,一個處理讀),是以可以說 “這個 pipe 有新的資料可以讀” 。但檔案是另類的,你可以朝一個檔案的中間寫入資料!是以你不能簡單的說 “該檔案有新的資料可以讀取”。epoll_ctl
- 調用
來等待你關心的檔案有資料更新epoll_wait
performance: select & poll vs epoll
書中有個表格比較了監聽十萬個操作下的性能優劣:
是以當你需要監聽大于 10 個 fd 時,使用 epoll 确實會快很多。
License
- 本文遵守創作共享 CC BY-NC-SA 3.0協定