天天看點

Linux 新增系統調用的啟示

最近在研究 Linux 核心的時間子系統,為下一篇長文《伺服器程式中的日期與時間》做準備,無意中注意到了 Linux 新增的幾個系統調用的對編寫伺服器代碼的影響,先大緻記錄在這裡。這篇部落格也可算作前一篇《多線程伺服器的常用程式設計模型》的一個注腳。

1. 伺服器程式的風格可能在變

新的建立檔案描述符的 syscall 一般都支援額外的 flags 參數,可以直接指定 O_NONBLOCK 和 FD_CLOEXEC,例如:

  • accept4 – 2.6.28
  • eventfd2 – 2.6.27
  • inotify_init1 – 2.6.27
  • pipe2 – 2.6.27
  • signalfd4 – 2.6.27
  • timerfd_create 2.6.25

以上 6 個 syscalls,除了最後一個是新功能,其餘的都是增強原有的調用,把數字尾号去掉就是原來的 syscall。

O_NONBLOCK 的功能是開啟“非阻塞IO”,而檔案描述符預設是阻塞的。

這些建立檔案描述符的系統調用能直接設定 O_NONBLOCK 選項,或許能反映目前 Linux (服務端)開發的風向,那就是我在前一篇部落格《多線程伺服器的常用程式設計模型》裡推薦的 one loop per thread + (non-blocking IO with IO multiplexing)。從這些核心改動來看,non-blocking IO 已經主流到讓核心增加 syscall 以節省一次 fcntl(2) 調用的程度了。

另外,以下新系統調用可以在建立檔案描述符時開啟 FD_CLOEXEC 選項:

  • dup3 – 2.6.27
  • epoll_create1 – 2.6.27

FD_CLOEXEC 的功能是讓程式 fork() 時,子程序會自動關閉這個檔案描述符 (見下面的更正)。而檔案描述預設是被子程序繼承的(這是傳統 Unix 的一種典型 IPC,比如用 pipe(2) 在父子程序間單向通信)。

以上 8 個新 syscalls 都允許直接指定 FD_CLOEXEC,或許說明 fork() 的主要目的已經不再是建立 worker process 并通過共享的檔案描述符和父程序保持通信,而是像 Windows 的 CreateProcess 那樣建立“幹淨”的程序,其與父程序沒有多少瓜葛。

以上兩個 flags 在我看來,說明 Linux 伺服器開發的主流模型正在由 fork() + worker processes 模型轉變為我前文推薦的多線程模型。fork() 的使用頻度會大大降低,将來或許隻有專門負責啟動别的程序的“看門狗程式”才會調用 fork(),而一般的伺服器程式(此處“伺服器程式”的定義見我前一篇文章)不會再 fork() 出子程序了。原因之一是,fork() 一般不能在多線程程式中調用,因為 Linux 的 fork() 隻克隆目前線程的 thread of control,不克隆其他線程。也就是說不能一下子 fork() 出一個和父程序一樣的多線程子程序,Linux 沒有 forkall() 這樣的系統調用。forkall() 其實也是很難辦的(從語意上),因為其他線程可能等在 condition variable 上,可能阻塞在系統調用上,可能等這 mutex 以跨入臨界區,還可能在密集的計算中,這些都不好全盤搬到子程序裡。由此可見,“看門狗程式”應該是單程序的,而且能捕獲 SIGCHLD,如果 signal 能像“檔案”一樣讀就能大大簡化開發,下面第 2 點正好印證了。

既然如此,那麼在 fork() 時關閉不相幹的檔案描述符就成了常見的需求,幹脆做到系統調用裡得了。

2. Kernel 2.6.22 加入的 signalfd 讓 signal handling 有了新辦法。

signal 處理是 Unix 程式設計的難點,因為 signal 是異步的,而且發生在“目前線程”裡,會遇到“可重入”的難題。其實“線程”是 1993 才加入到 Unix 中,之前的 20 多年根本就沒有“主線程”一說,我這裡的意思是 signal handler 是像 coroutine 一樣被調用的,而不是通常的 subroutine。Raymond Chen 有一篇文章談到了這個問題。

在 Unix/Linux 支援線程以後,signal 就更難處理了,規則變得晦澀(想想 signal delivery 的對象)。而且它不符合“every thing is a file” 的 Unix 哲學,不能把 signal 事件當成檔案來讀。不過 2.6.22 加入的 signalfd 讓事情有了轉機,程式能像處理檔案一樣處理 signal,可以 read,也可以 select/poll/epoll,能融入标準的 IO multiplexing 架構中,而不需要在程式裡另外用一對 pipe 來把 signal 轉為 IO event。(libev 似乎是這麼做的,另外還有 GHC http://hackage.haskell.org/trac/ghc/ticket/1520 )

這下多線程程式與 signals 打交道容易多了,一個 event loop 就能搞定 IO 和 timer 和 signals,完美。

3. Kernel 2.6.25 加入的 timerfd 讓程式的“定時任務”有了新辦法。

我下一篇部落格會詳細分析 Linux 伺服器程式中的日期與時間,其中一塊内容是“定時”,也就是程式借助定時器在未來某個時刻做特定的事情。在 Linux 下辦法很多,基于阻塞的 sleep/nanosleep/clock_nanosleep, 基于 signals 的 rtsignal/timer_create,還有我喜歡的基于 IO multiplexing 的 poll/epoll。不過 poll/epoll 的理論定時精度最多隻有毫秒(函數的參數就是毫秒數,不能指定更高的時間精度),實際等待精度取決于 kernel HZ 等。

如果需要在 event loop 裡做無阻塞的高精度定時,現在可以用 timerfd 了。而且它既然是個 fd,就能很友善地和 non-blocking IO 與 IO multiplexing 融合到一起,渾然天成。當然,檔案描述符是稀缺資源,如果每個 event loop 都采用 timerfd 來做 timer/timeout 似乎是一種浪費(每個 timer 一個 timerfd 更是巨大浪費,因為不是每個 timer 都需要高精度定時),我甯願采用傳統的優先隊列辦法來管理等待到期的 timers(毫秒級的定時精度已經能滿足我的需要),隻在特殊場合動用 timerfd。

4. Kernel 2.6.22 加入的 eventfd 讓“線程間事件通知”有了新辦法。

《多線程伺服器的常用程式設計模型》 提到程序間通信隻用 TCP,而 pipe 的惟一作用是異步喚醒 event loop,現在有了 eventfd,pipe 連這個作用都沒有了。eventfd 是一個比 pipe 更高效的線程間事件通知機制,一方面它比 pipe 少用一個 file descriper,節省了資源;另一方面,eventfd 的緩沖區管理也簡單得多,全部“buffer”一共隻有 8 bytes,不像 pipe 那樣可能有不定長的真正 buffer。

pipe 将來的作用或許主要是被“看門狗程式”用來截獲子程序的 stdout/stderr。

綜上,我前面一篇部落格中提倡的 one loop per thread + (non-blocking IO with IO multiplexing) 伺服器模型依賴一個優質的基于 Reactor 模式的網絡庫。如果要編寫一個話,最好能用 2.6.22 以後的新核心,預計程式設計會簡化不少(至少 eventfd 和 signalfd 能發揮很大作用),我準備寫一個簡單的試試。

最後,我研究 Linux kernel,目的是為了更好地編寫 Linux 的伺服器應用程式。我不是 kernel 專家,也不打算成為專家。

2010-Feb-27 更正:前面說“FD_CLOEXEC 的功能是讓程式 fork() 時,子程序會自動關閉這個檔案描述符”,這是錯誤的,FD_CLOEXEC 顧名思義是在執行 exec() 調用時關閉檔案描述符,防止檔案描述符洩漏給子程序。我對fork()的第一反應是立即執行exec(),故造成了誤解。

繼續閱讀