在操做系統中,程序運行的空間分爲內核空間和用戶空間。咱們經常提起的異步I/O,其實質是用戶空間中的程序不用依賴內核空間中的I/O操做實際完成,便可進行後續任務。如下僞代碼模仿了一個從磁盤上獲取文件和一個從網絡中獲取文件的操做。異步I/O的效果就是getFileFromNet的調用不依賴於getFile調用的結束。 php
getFile("file_path"); getFileFromNet("url");
若是以上兩個任務的時間分別爲m和n。採用同步方式的程序要完成這兩個任務的時間總花銷會是m + n。可是若是是採用異步方式的程序,在兩種I/O能夠並行的情況下(好比網絡I/O與文件I/O),時間開銷將會減少爲max(m, n)。 html
有的語言爲了設計得使應用程序調用方便,將程序設計爲同步I/O的模型。這意味着程序中的後續任務都須要等待I/O的完成。在等待I/O完成的過程當中,程序沒法充分利用CPU。爲了充分利用CPU,和使I/O能夠並行,目前有兩種方式能夠達到目的: 前端
前者在性能優化上還有迴旋的餘地,後者的作法純粹是一種加三倍服務器的行爲。
並且如今的大型Web應用中,單機的情形是十分稀少的,一個事務每每須要跨越網絡幾回才能完成最終處理。若是網絡速度不夠理想,m和n值都將會變大,這時同步I/O的語言模型將會露出其最脆弱的狀態。
這種場景下的異步I/O將會體現其優點,max(m, n)的時間開銷能夠有效地緩解m和n值增加帶來的性能問題。而當並行任務更多的時候,m + n + …與max(m, n, …)之間的孰優孰劣更是一目瞭然。從這個公式中,能夠了解到異步I/O在分佈式環境中是多麼重要,而Node.js自然地支持這種異步I/O,這是衆多雲計算廠商對其青睞的根本緣由。 node
咱們聽到Node.js時,咱們經常會聽到異步,非阻塞,回調,事件這些詞語混合在一塊兒。其中,異步與非阻塞聽起來彷佛是同一回事。從實際效果的角度說,異步和非阻塞都達到了咱們並行I/O的目的。可是從計算機內核I/O而言,異步/同步和阻塞/非阻塞實際上時兩回事。 linux
當進行非阻塞I/O調用時,要讀到完整的數據,應用程序須要進行屢次輪詢,才能確保讀取數據完成,以進行下一步的操做。
輪詢技術的缺點在於應用程序要主動調用,會形成佔用較多CPU時間片,性能較爲低下。現存的輪詢技術有如下這些: nginx
read是性能最低的一種,它經過重複調用來檢查I/O的狀態來完成完整數據讀取。select是一種改進方案,經過對文件描述符上的事件狀態來進行判斷。操做系統還提供了poll、epoll等多路複用技術來提升性能。
輪詢技術知足了異步I/O確保獲取完整數據的保證。可是對於應用程序而言,它仍然只能算時一種同步,由於應用程序仍然須要主動去判斷I/O的狀態,依舊花費了不少CPU時間來等待。 git
上一種方法重複調用read進行輪詢直到最終成功,用戶程序會佔用較多CPU,性能較爲低下。而實際上操做系統提供了select方法來代替這種重複read輪詢進行狀態判斷。select內部經過檢查文件描述符上的事件狀態來進行判斷數據是否徹底讀取。可是對於應用程序而言它仍然只能算是一種同步,由於應用程序仍然須要主動去判斷I/O的狀態,依舊花費了不少CPU時間等待,select也是一種輪詢。 github
理想的異步I/O應該是應用程序發起異步調用,而不須要進行輪詢,進而處理下一個任務,只需在I/O完成後經過信號或是回調將數據傳遞給應用程序便可。 windows
幸運的是,在Linux下存在一種這種方式,它原生提供了一種異步非阻塞I/O方式(AIO)便是經過信號或回調來傳遞數據的。
不幸的是,只有Linux下有這麼一種支持,並且還有缺陷(AIO僅支持內核I/O中的O_DIRECT方式讀取,致使沒法利用系統緩存。參見:http://forum.nginx.org/read.php?2,113524,113587#msg-113587
以上都是基於非阻塞I/O進行的設定。另外一種理想的異步I/O是採用阻塞I/O,但加入多線程,將I/O操做分到多個線程上,利用線程之間的通訊來模擬異步。Glibc的AIO即是這樣的典型http://www.ibm.com/developerworks/linux/library/l-async/。然而遺憾在於,它存在一些難以忍受的缺陷和bug。能夠簡單的概述爲:Linux平臺下沒有完美的異步I/O支持。
所幸的是,libev的做者Marc Alexander Lehmann從新實現了一個異步I/O的庫:libeio。libeio實質依然是採用線程池與阻塞I/O模擬出來的異步I/O。
那麼在Windows平臺下的情況如何呢?而實際上,Windows有一種獨有的內核異步IO方案:IOCP。IOCP的思路是真正的異步I/O方案,調用異步方法,而後等待I/O完成通知。IOCP內部依舊是經過線程實現,不一樣在於這些線程由系統內核接手管理。IOCP的異步模型與Node.js的異步調用模型已經十分近似。
以上兩種方案則正是Node.js選擇的異步I/O方案。因爲Windows平臺和*nix平臺的差別,Node.js提供了libuv來做爲抽象封裝層,使得全部平臺兼容性的判斷都由這一層次來完成,保證上層的Node.js與下層的libeio/libev及IOCP之間各自獨立。Node.js在編譯期間會判斷平臺條件,選擇性編譯unix目錄或是win目錄下的源文件到目標程序中。 後端
下文咱們將經過解釋Windows下Node.js異步I/O(IOCP)的簡單例子來探尋一下從JavaScript代碼到系統內核之間都發生了什麼。
不少同窗在碰見Node.js後必然產生過對回調函數究竟如何被調用產生過好奇。在文件I/O這一塊與普通的業務邏輯的回調函數不一樣在於它不是由咱們本身的代碼所觸發,而是系統調用結束後,由系統觸發的。下面咱們以最簡單的fs.open方法來做爲例子,探索Node.js與底層之間是如何執行異步I/O調用和回調函數到底是如何被調用執行的。
fs.open = function(path, flags, mode, callback) { callback = arguments[arguments.length - 1]; if (typeof(callback) !== 'function') { callback = noop; } mode = modeNum(mode, 438 /*=0666*/); binding.open(pathModule._makeLong(path), stringToFlags(flags), mode, callback); };
fs.open的做用是根據指定路徑和參數,去打開一個文件,從而獲得一個文件描述符,是後續全部I/O操做的初始操做。
在JavaScript層面上調用的fs.open方法最終都透過node_file.cc調用到了libuv中的uv_fs_open方法,這裏libuv做爲封裝層,分別寫了兩個平臺下的代碼實現,編譯以後,只會存在一種實現被調用。
在uv_fs_open的調用過程當中,Node.js建立了一個FSReqWrap請求對象。從JavaScript傳入的參數和當前方法都被封裝在這個請求對象中,其中回調函數則被設置在這個對象的oncomplete_sym屬性上。
req_wrap->object_->Set(oncomplete_sym, callback);
對象包裝完畢後,調用QueueUserWorkItem方法將這個FSReqWrap對象推入線程池中等待執行。
QueueUserWorkItem(&uv_fs_thread_proc, req, WT_EXECUTELONGFUNCTION)
QueueUserWorkItem接受三個參數,第一個是要執行的方法,第二個是方法的上下文,第三個是執行的標誌。當線程池中有可用線程的時候調用uv_fs_thread_proc方法執行。該方法會根據傳入的類型調用相應的底層函數,以uv_fs_open爲例,實際會調用到fs__open方法。調用完畢以後,會將獲取的結果設置在req->result上。而後調用PostQueuedCompletionStatus通知咱們的IOCP對象操做已經完成。
PostQueuedCompletionStatus((loop)->iocp, 0, 0, &((req)->overlapped))
PostQueuedCompletionStatus方法的做用是向建立的IOCP上相關的線程通訊,線程根據執行情況和傳入的參數斷定退出。
至此,由JavaScript層面發起的異步調用第一階段就此結束。
在調用uv_fs_open方法的過程當中實際上應用到了事件循環。以在Windows平臺下的實現中,啓動Node.js時,便建立了一個基於IOCP的事件循環loop,並一直處於執行狀態。
uv_run(uv_default_loop());
每次循環中,它會調用IOCP相關的GetQueuedCompletionStatus方法檢查是否線程池中有執行完的請求,若是存在,poll操做會將請求對象加入到loop的pending_reqs_tail屬性上。 另外一邊這個循環也會不斷檢查loop對象上的pending_reqs_tail引用,若是有可用的請求對象,就取出請求對象的result屬性做爲結果傳遞給oncomplete_sym執行,以此達到調用JavaScript中傳入的回調函數的目的。 至此,整個異步I/O的流程完成結束。其流程以下:
事件循環和請求對象構成了Node.js的異步I/O模型的兩個基本元素,這也是典型的消費者生產者場景。在Windows下經過IOCP的GetQueuedCompletionStatus、PostQueuedCompletionStatus、QueueUserWorkItem方法與事件循環實。對於*nix平臺下,這個流程的不一樣之處在與實現這些功能的方法是由libeio和libev提供。
田永強,新浪微博@樸靈,前端工程師,曾就任於SAP,現就任於淘寶,花名樸靈,致力於NodeJS和Mobile Web App方面的研發工做。雙修先後端JavaScript,寄望將NodeJS引薦給更多的工程師。興趣:讀萬卷書,行萬里路。我的Github地址:http://github.com/JacksonTian。