本文你能學到:javascript
看完本文後,你應該能更好的去理解事件循環,知道事件是怎麼來的,Node 究竟執行異步I/O調用。若是面試官再問事件循環還有Node與底層之間如何執行異步I/O,我以爲你把本文的流程說清楚,應該能加分!本文對事件循環中的具體步驟沒有詳細講解,每一個步驟看官方文檔更佳。java
nodejs模塊能夠分爲下面三類:node
好比 Node 源碼lib目錄下的 fs.js 就是 native 模塊,而fs.js調用的 src 目錄下的 node_fs.cc 就是內建模塊。linux
Libuv是一個高性能的,事件驅動的異步I/O庫,它自己是由C語言編寫的,具備很高的可移植性。libuv封裝了不一樣平臺底層對於異步IO模型的實現,libuv 的 API 包含有時間,非阻塞的網絡,異步文件操做,子進程等等,因此它還自己具有着Windows, Linux均可使用的跨平臺能力。webpack
經典libuv圖(來源網上)c++
概念:輸入輸出完成端口(Input/Output Completion Port,IOCP), 是支持多個同時發生的異步I/O操做的應用程序編程接口,在Windows NT的3.5版本之後,或AIX5版之後或Solaris第十版之後,開始支持。git
我直接這麼說概念你可能也不太懂。能夠暫時知道 Windows 下注意經過 IOCP 來向系統內核發送 I/O 調用和從內核獲取已完成的 I/O 操做,配以事件循環,完成異步I/O的過程。在 linux 下經過 epoll 實現這個過程,也就是由 libuv 自行實現。程序員
IOCP 的另外一個應用場景在以前Node.js進程與線程那篇文章也有寫過。Mater 和 app worker 進程通訊使用到。github
線程池,是一種線程的使用模式,它爲了下降線程使用中頻繁的建立和銷燬所帶來的資源消耗與代價。 經過建立必定數量的線程,讓他們時刻準備就緒等待新任務的到達,而任務執行結束以後再從新回來繼續待命。web
這就是線程池最核心的設計思路,「複用線程,平攤線程的建立與銷燬的開銷代價」。
本文使用到線程池的地方:在 Node 中,不管是 *nix 仍是 Window 平臺。內部完成 I/O 任務的都有用到線程池。
libuv 目前使用了一個全局的線程池,全部的循環均可以往其中加入任務。目前有三種操做會在這個線程池中執行:
文件系統操做
DNS 函數(getaddrinfo 和 getnameinfo)
經過 uv_queue_work() 添加的用戶代碼
對比圖中兩段經典api代碼(server.listen
和fs.open
,選擇兩種api的緣由:網絡 I/O 表明和文件 I/O 表明)和以前 libuv 圖片,咱們來一塊兒理解異步I/O調用流程
server.listen() 是用來建立 TCP server 時,一般放在最後一步執行的代碼。主要指定服務器工做的端口以及回調函數。
fs.open() 是用異步的方式打開一個文件。
選擇兩個示例很簡單,由於 libuv 架構圖可視:libuv 對 Network I/O和 File I/O 採用不一樣的機制。
上圖右半部分,主要分紅兩個部分:
主線程:主線程也是 node 啓動時執行的現成。node 啓動時,會完成一系列的初始化動做,啓動 V8 engine,進入下一個循環。
線程池:線程池的數量能夠經過環境變量 UV_THREADPOOL_SIZE 配置,最大不超過 128 個,默認爲 4 個。
在Node.js 中經典的代碼調用方式:都是從 JavaScript 調用 Node 核心模塊,核心模塊調用 C++ 內建模塊,內建模塊經過 libuv 進行系統調用。請記住這段話
不論是server.listen
仍是fs.open
,他們在開啓一個 node 服務(進程)的時候,Node會建立一個while(true)的循環,這個循環就是事件循環。每執行一次循環體的過程,咱們稱之爲Tick。每一個Tick的過程就是查看是否有事件待處理,若是有,就取出事件及其相關的回調函數。若是存在關聯的回調函數,就執行。而後進入下一個循環,若是再也不有事件處理,退出進程。
這裏咱們知道事件循環已經建立了,上面加粗字體查看是否有事件待處理,去哪裏查看?事件怎麼進入事件循環的?什麼狀況會產生事件繼續往下看。
繼續看這張圖,講解一下事件產生基本流程,(注意網絡I/O和文件I/O會有一些不一樣)這裏對c++代碼調用簡單提一下,有興趣的小夥伴能夠繼續深刻研究。
(這裏就用到了文初提到的模塊分類知識)先是 javascript 代碼,而後調用 lib/fs.js
核心模塊代碼 fs.open
,核心模塊調用 C++ 內建模塊 src/node_file.cc
,內建模塊c++代碼會有一個平臺判斷,而後經過 libuv 進行系統調用。
從前面到達 libuv ,會有一個參數,請求對象,也就是open函數前面整個流程傳遞進來的請求對象,它保存了全部狀態,包括送入線程池等待執行以及I/O操做完畢後的回調處理。
請求對象組裝完成後,送入 libuv 中建立的 I/O 線程池,線程池中的 I/O 操做完畢後,會將獲取的結果存儲到 req->result 屬性上,而後通知某函數通知 IOCP ,告知當前對象操做已經完成。
在這整個過程當中,進程初期建立的事件循環中有一個 I/O 觀察者,每次 Tick 的執行中,它會調用 IOCP 相關的方法檢查線程池中是否有執行完成的請求,若是存在,會講請求對象和以前綁定的 result 屬性,加入到 I/O 觀察者的隊列中,而後將其看成事件處理。
看到這裏,前面提到的**是否有事件待處理,去哪裏查看?事件怎麼進入事件循環的?**這兩個問題是否是搞懂了。
文字配上圖。更清晰!
V8 engine 執行從 server.listen()
開始,調用 builtin module Tcp_wrap
的過程。
在建立TCP連接的過程當中,libuv直接參與Tcp_wrap.cc
函數中的 TCPWrap::listen()
調用uv_listen()開始到執行uv_io_start()
結束。看起來很短暫的過程,實際上是相似linux kernel的中斷處理機制。
uv_io_start()
負載將 handle 插入處處理的water queue
中。這樣的好處是請求可以當即獲得處理。中斷處理機制裏面的下半部分與數據處理操做類似,交由主線程去完成處理。
重要:雖然 libuv 的異步文件 I/O 操做是經過線程池實現的,可是網絡 I/O 老是在單線程中執行的,注意最後仍是會把完成的內容做爲事件加入事件循環,事件循環就和文件I/O相同了。
傳統的服務器模型
Node就不同了!
看了文章前面的內容,Node 經過事件驅動的方式處理請求,無需爲每一個請求建立額外的對應線程,能夠省掉建立線程和銷燬線程的開銷,同時操做系統在調度任務時由於線程較少,上下文切換的代價很低。這也是 Node.js 高性能之一
Nginx 目前也採用了和 Node 相同的事件驅動方式,有興趣的也去了解下,不過 Nginx 採用 c 語言編寫。
做者簡介:koala,專一完整的 Node.js 技術棧分享,從 JavaScript 到 Node.js,再到後端數據庫,祝您成爲優秀的高級 Node.js 工程師。【程序員成長指北】做者,Github 博客開源項目 github.com/koala-codin…
本文不少內容來自樸靈老師的 《深刻淺出Node.js》,這本書雖然出版好久了,給個人感受仍是越看越香,本身能夠邊看邊擴展,推薦。
Libuv學習——文件處理 zhuanlan.zhihu.com/p/97789391
高性能異步 I/O 模型庫 libuv 設計思路概述 blog.csdn.net/ababab12345…