[譯] libuv 設計概述
概述
libuv 最初是為 Node.js 所作的跨平臺庫。它基于事件驅動的異步 I/O 模型。
libuv 不僅僅只提供了對于不同 I/O 輪詢機制的簡單抽象:“句柄(handles)”和“流(streams)”也提供了對于 socket 和其他相關實例的高度抽象。同時 libuv 還提供了跨平臺文件 I/O 接口和多線程接口等等。
下圖展示了 libuv 的不同組成部分,以及與這些部分相關的子模塊:
句柄(handles)和請求(requests)
為了能使用戶介入事件循環(event loop),libuv 為用戶提供了兩個抽象:句柄和請求。
句柄表示一個在其被激活時可以執行某些操作且持久存在的對象。例如:當一個預備句柄(prepare handle)處于激活時,它的回調函數會在每次事件循環中被調用;每當一個新 TCP 連接來到時,一個 TCP 服務器句柄的連接回調函數就會被調用。
請求(通常)表示一個短暫存在的操作。這些操作可以操作于句柄,例如寫請求(write requests)用于向一個句柄寫入數據。但是又如 getaddrinfo 請求則不依賴于一個句柄,它們直接在事件循環上執行。
事件循環
事件循環是 libuv 的核心部分。它為所有的 I/O 操作建立了上下文,并且執行于一個單線程中。你可以在多個不同的線程中運行多個事件循環。除非另有說明,不然 libuv 的事件循環(以及其他循環或句柄提供的 API) 并不是線程安全的 。
事件循環遵循著普遍的單線程異步 I/O 行為:所有的(網絡)I/O 體現在非阻塞的 socket 上,對于不同的平臺,libuv 會選取最佳的輪詢機制:Linux 上為 epoll ,OSX 和其他 BSD 上為 kqueue ,SunOS 上為 event ports , Windows 上則為 IOCP 。作為循環迭代的一部分,事件循環會阻塞并等待被添加的 socket 上 I/O 活動的發生。然后根據當前的 socket 情況(可讀,可寫,掛起)觸發相應的回調函數。所以,一個句柄是可以執行讀操作,寫操作或其他 I/O 行為。
為了能更好的理解事件循環是如何工作的,下圖展示了事件循環一次迭代的所有過程:
-
事件循環中的“現在時間(now)”被更新。事件循環會在一次循環迭代開始的時候緩存下當時的時間,用于減少與時間相關的系統調用次數。
-
如果事件循環仍是存活(alive)的,那么迭代就會開始,否則循環會立刻退出。如果一個循環內包含激活的可引用句柄,激活的請求或正在關閉的句柄,那么則認為該循環是存活的。
-
執行超時定時器(due timers)。所有在循環的“現在時間”之前超時的定時器都將在這個時候得到執行。
-
執行等待中回調(pending callbacks)。正常情況下,所有的 I/O 回調都會在輪詢 I/O 后立刻被調用。但是有些情況下,回調可能會被推遲至下一次循環迭代中再執行。任何上一次循環中被推遲的回調,都將在這個時候得到執行。
-
執行閑置句柄回調(idle handle callbacks)。盡管它有個不怎么好聽的名字,但只要這些閑置句柄是激活的,那么在每次循環迭代中它們都會執行。
-
執行預備回調(prepare handle)。預備回調會在循環為 I/O 阻塞前被調用。
-
開始計算輪詢超時(poll timeout)。在為 I/O 阻塞前,事件循環會計算它即將會阻塞多長時間。以下為計算該超時的規則:
-
如果循環帶著 UV_RUN_NOWAIT 標識執行,那么超時將會是 0 。
-
如果循環即將停止( uv_stop() 已在之前被調用),那么超時將會是 0 。
-
如果循環內沒有激活的句柄和請求,那么超時將會是 0 。
-
如果循環內有激活的閑置句柄,那么超時將會是 0 。
-
如果有正在等待被關閉的句柄,那么超時將會是 0 。
-
如果不符合以上所有,那么該超時將會是循環內所有定時器中最早的一個超時時間,如果沒有任何一個激活的定時器,那么超時將會是無限長(infinity)。
-
-
事件循環為 I/O 阻塞。此時事件循環將會為 I/O 阻塞,持續時間為上一步中計算所得的超時時間。所有與 I/O 相關的句柄都將會監視一個指定的文件描述符,等待一個其上的讀或寫操作來激活它們的回調。
-
執行檢查句柄回調(check handle callbacks)。在事件循環為 I/O 阻塞結束后,檢查句柄的回調將會立刻執行。檢查句柄本質上是預備句柄的對應物(counterpart)。
-
執行關閉回調(close callbacks)。如果一個句柄通過調用 uv_close() 被關閉,那么這將會調用關閉回調。
-
盡管在為 I/O 阻塞后可能并沒有 I/O 回調被觸發,但是仍有可能這時已經有一些定時器已經超時。若事件循環是以 UV_RUN_ONCE 標識執行,那么在這時這些超時的定時器的回調將會在此時得到執行。
-
迭代結束。如果循環以 UV_RUN_NOWAIT 或 UV_RUN_ONCE 標識執行,迭代便會結束,并且 uv_run() 將會返回。如果循環以 UV_RUN_DEFAULT 標識執行,那么如果若它還是存活的,它就會開始下一次迭代,否則結束。
重要:雖然 libuv 的異步文件 I/O 操作是通過線程池實現的,但是網絡 I/O 總是在單線程中執行的。
注意:雖然在不同平臺上使用的輪詢機制不同,但 libuv 的執行模型在不同平臺下都是保持一致。
文件 I/O
與網絡 I/O 不同,并不存在 libuv 可以依靠的各特定平臺下的文件 I/O 基礎函數,所以目前的實現是在線程中執行阻塞的文件 I/O 操作來模擬異步。
更多關于跨平臺異步文件 I/O 操作的內容,可參閱 此文 。
libuv 目前使用了一個全局的線程池,所有的循環都可以往其中加入任務。目前有三種操作會在這個線程池中執行:
-
文件系統操作
-
DNS 函數(getaddrinfo 和 getnameinfo)
-
通過 uv_queue_work() 添加的用戶代碼
注意:更多關于 libuv 線程池的信息請參閱 此文 。請牢記線程池的大小是有限的。
來自: https://segmentfault.com/a/1190000005873917