面試官問:Node 與底層之間如何執行異步 I/O 調用

本文你能學到:javascript

  • Node.js 與底層之間是如何執行異步I/O調用的?和事件循環怎麼聯繫上的呢?
  • 爲何說 Node 高性能,Node 的異步I/O 對高性能助力了什麼?
  • Node 的事件循環,你對事件怎麼理解?

看完本文後,你應該能更好的去理解事件循環,知道事件是怎麼來的,Node 究竟執行異步I/O調用。若是面試官再問事件循環還有Node與底層之間如何執行異步I/O,我以爲你把本文的流程說清楚,應該能加分!本文對事件循環中的具體步驟沒有詳細講解,每一個步驟看官方文檔更佳。java

理解本文先要學習的幾個概念

Node.js 模塊分類

nodejs模塊能夠分爲下面三類:node

  • 核心模塊(native模塊):包含在 Node.js 源碼中,被編譯進 Node.js 可執行二進制文件 JavaScript 模塊,其實也就是lib和deps目錄下的js文件,好比經常使用的http,fs等等。
  • 內建模塊(built-in模塊):通常咱們不直接調用,而是在 native 模塊中調用,而後咱們再require。
  • 第三方模塊:非 Node.js 源碼自帶的模塊均可以統稱第三方模塊,好比 express,webpack 等等。
    • JavaScript 模塊,這是最多見的,咱們開發的時候通常都寫的是 JavaScript 模塊
    • JSON 模塊,這個很簡單,就是一個 JSON 文件
    • C/C++ 擴展模塊,使用 C/C++ 編寫,編譯以後後綴名爲 .node

好比 Node 源碼lib目錄下的 fs.js 就是 native 模塊,而fs.js調用的 src 目錄下的 node_fs.cc 就是內建模塊。linux

libuv

Libuv是一個高性能的,事件驅動的異步I/O庫,它自己是由C語言編寫的,具備很高的可移植性。libuv封裝了不一樣平臺底層對於異步IO模型的實現,libuv 的 API 包含有時間,非阻塞的網絡,異步文件操做,子進程等等,因此它還自己具有着Windows, Linux均可使用的跨平臺能力。webpack

經典libuv圖(來源網上)c++

IOCP

概念:輸入輸出完成端口(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() 添加的用戶代碼

Node 與底層之間的異步I/O調用流程

對比圖中兩段經典api代碼(server.listenfs.open,選擇兩種api的緣由:網絡 I/O 表明和文件 I/O 表明)和以前 libuv 圖片,咱們來一塊兒理解異步I/O調用流程

上圖展現了libuv細節的流程,圖中代碼很簡單,包括2個部分:

  1. server.listen() 是用來建立 TCP server 時,一般放在最後一步執行的代碼。主要指定服務器工做的端口以及回調函數。

  2. fs.open() 是用異步的方式打開一個文件。

選擇兩個示例很簡單,由於 libuv 架構圖可視:libuv 對 Network I/O和 File I/O 採用不一樣的機制。

上圖右半部分,主要分紅兩個部分:

  1. 主線程:主線程也是 node 啓動時執行的現成。node 啓動時,會完成一系列的初始化動做,啓動 V8 engine,進入下一個循環。

  2. 線程池:線程池的數量能夠經過環境變量 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++代碼調用簡單提一下,有興趣的小夥伴能夠繼續深刻研究。

File I/O

(這裏就用到了文初提到的模塊分類知識)先是 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 觀察者的隊列中,而後將其看成事件處理。

看到這裏,前面提到的**是否有事件待處理,去哪裏查看?事件怎麼進入事件循環的?**這兩個問題是否是搞懂了。

文字配上圖。更清晰!

Network 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相同了。

異步 I/O 助力 Node.js 高性能

傳統的服務器模型

  • 同步式: 同步的服務,一次只能處理一個請求,而且其他請求都處於等待狀態。
  • 每進程/每請求: 爲每一個請求啓動一個進程,這樣能夠處理多個請求,可是不具備擴展性,系統資源有限,開啓太多進程不太合適
  • 每線程/每請求: 爲每一個請求啓動一個線程來處理。儘管線程比進程輕量,可是每一個線程也都會佔用必定內存,當大併發請求的時候,也會佔用很大內存,致使服務器緩慢。

Node就不同了!

看了文章前面的內容,Node 經過事件驅動的方式處理請求,無需爲每一個請求建立額外的對應線程,能夠省掉建立線程和銷燬線程的開銷,同時操做系統在調度任務時由於線程較少,上下文切換的代價很低。這也是 Node.js 高性能之一

Nginx 目前也採用了和 Node 相同的事件驅動方式,有興趣的也去了解下,不過 Nginx 採用 c 語言編寫。

關注我

做者簡介:koala,專一完整的 Node.js 技術棧分享,從 JavaScript 到 Node.js,再到後端數據庫,祝您成爲優秀的高級 Node.js 工程師。【程序員成長指北】做者,Github 博客開源項目 github.com/koala-codin…

  • 歡迎加我微信【 ikoala520 】,拉你 進 Node.js 高級進階羣,長期交流學習...
  • 歡迎關注「程序員成長指北」,一個用心幫助你成長的公衆號...

參考

本文不少內容來自樸靈老師的 《深刻淺出Node.js》,這本書雖然出版好久了,給個人感受仍是越看越香,本身能夠邊看邊擴展,推薦。

Libuv學習——文件處理 zhuanlan.zhihu.com/p/97789391

高性能異步 I/O 模型庫 libuv 設計思路概述 blog.csdn.net/ababab12345…

相關文章
相關標籤/搜索