
Node.js? is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.
Node.js官網上的介紹,其中事件驅動非阻塞I/O模型是被大家所津津樂道的,但是有多少人真正了解其究竟呢?有人可能會想到libuv,沒錯,libuv確實是其幕后英雄。那么問題又來了,到底是怎么用libuv實現(xiàn)的呢?下面我們來一探究竟。

libuv當初主要就是為Node.js開發(fā)的,提供跨平臺的事件驅動異步I/O能力,當然現(xiàn)在肯定不僅限于Node.js使用。我們先來看一下libuv的Design overview。

從架構圖上看,libuv是對多個平臺上的事件驅動異步I/O庫進行了封裝,如Linux下的epoll、FreeBSD下的kqueue、Solaris下的event ports、Windows下的IOCP。

上圖所描述的事件循環(huán)是libuv中最重要的概念,其中的Poll for I/O就是事件驅動異步I/O能力的核心。到這里我們有必要先了解一些基礎知識,Linux IO模式及 select、poll、epoll詳解,否則后面的東西就不是特別好理解了。
正題
經過前面的學習,應該對libuv有了一個整體的印象,總結一下, libuv其實就是把各種handle和io_watcher放到事件循環(huán)里,然后每一次循環(huán)都去檢查一下是否有他們關心的事件需要處理,有則調用相應的callback,沒有則繼續(xù)循環(huán)。要想弄清楚Node.js之異步那些事,我們需要關心的是,Node.js如何運行事件循環(huán),何時把handle和io_watcher放入事件循環(huán),以及如何調用相應的callback。
開始之前,本次分析的代碼版本為Node.js v0.12.6,Linux平臺。
Run
node.cc中Start方法運行事件循環(huán),精華部分如下。唯一有些特別的地方就是,在一個while循環(huán)中包了兩個uv_run,模式分別是UV_RUN_ONCE和UV_RUN_NOWAIT,其原因在中間的兩行注釋中已經說得很明白了。
...
bool more;
do {
more = uv_run(env->event_loop(), UV_RUN_ONCE);
if (more == false) {
EmitBeforeExit(env);
// Emit `beforeExit` if the loop became alive either after emitting
// event, or after running some callbacks.
more = uv_loop_alive(env->event_loop());
if (uv_run(env->event_loop(), UV_RUN_NOWAIT) != 0)
more = true;
}
} while (more == true);
...
然后我們可以看看core.c中uv_run方法的代碼,跟上面事件循環(huán)的流程圖是可以一一對應的。
Data Structure
繼續(xù)看代碼之前,有必要先了解一下重要的數(shù)據(jù)結構和相互的關系,以便更好的理解。

io_watcher
接著我之前文章Node.js之HelloWorld背后的大坑的思路,還拿Hello World舉例子,跟libuv有關的代碼都在tcp_warp.cc里面了。
TCPWrap::New

stream.c中uv__stream_init方法有如下代碼,將io_watcher的cb設置為uv__stream_io,fd設置為-1,這里只是在stream層面做的初始化設置,后面到tcp層面還會有相應的改變。
uv__io_init(&stream->io_watcher, uv__stream_io, -1);
TCPWrap::Bind

tcp.c的maybe_new_socket方法中,uv__socket方法生成了新的fd,uv__stream_open方法將其設置到io_watcher的fd。
TCPWrap::Listen

tcp.c的uv_tcp_listen方法中有如下代碼,將io_watcher的cb設置為uv__server_io,uv__server_io里面會調用connection_cb,connection_cb已經被設置為cb,而這個cb正是tcp_wrap.cc中的TCPWrap::OnConnection方法。
...
tcp->connection_cb = cb;
/* Start listening for connections. */
tcp->io_watcher.cb = uv__server_io;
uv__io_start(tcp->loop, &tcp->io_watcher, UV__POLLIN);
...
core.c中uv__io_start方法有如下代碼,利用void* watcher_queue[2]變量將io_watcher加入到uv_loop_t的隊列中去,具體操作詳見queue.h。將uv_loop_t的uv__io_t** watchers當做數(shù)組使用,fd為下標,io_watcher為對應的值。
...
if (QUEUE_EMPTY(&w->watcher_queue))
QUEUE_INSERT_TAIL(&loop->watcher_queue, &w->watcher_queue);
if (loop->watchers[w->fd] == NULL) {
loop->watchers[w->fd] = w;
loop->nfds++;
}
...
uv__io_poll
linux-core.c中的uv__io_poll方法,一行一行的讀就可以了,前面的鋪墊已經做得很充分了,只要讀懂謎底便可揭曉。
未完
- 接下來我們來說說
process.nextTick(callback)的事,在node.js中定義如下,把callback放到了nextTickQueue隊列中,那么Node.js是在什么時候消費這個隊列的呢?
function nextTick(callback) {
// on the way out, don't bother. it won't get fired anyway.
if (process._exiting)
return;
var obj = {
callback: callback,
domain: process.domain || null
};
nextTickQueue.push(obj);
tickInfo[kLength]++;
}
-
tcp_wrap.cc中TCPWrap::OnConnection方法有如下代碼,MakeCallback方法的出處如下圖。
tcp_wrap->MakeCallback(env->onconnection_string(), ARRAY_SIZE(argv), argv);

-
async-wrap.cc中MakeCallback方法有如下代碼。
env()->tick_callback_function()->Call(process, 0, NULL);
-
node.cc中SetupNextTick方法有如下代碼,對tick_callback_function()進行了設定。
env->set_tick_callback_function(args[1].As<Function>());
-
node.cc中SetupProcessObject方法有如下代碼,SetupNextTick被設定為process中的_setupNextTick方法。
NODE_SET_METHOD(process, "_setupNextTick", SetupNextTick);
-
node.js中startup.processNextTick方法有如下代碼。
process._setupNextTick(tickInfo, _tickCallback, _runMicrotasks);
-
node.js中_tickCallback方法代碼如下,消費nextTickQueue隊列中的callback方法。
function _tickCallback() {
var callback, threw, tock;
scheduleMicrotasks();
while (tickInfo[kIndex] < tickInfo[kLength]) {
tock = nextTickQueue[tickInfo[kIndex]++];
callback = tock.callback;
threw = true;
try {
callback();
threw = false;
} finally {
if (threw)
tickDone();
}
if (1e4 < tickInfo[kIndex])
tickDone();
}
tickDone();
}
省略去中間步驟,實際上是產生了如下的調用關系。
TCPWrap::OnConnection()
↓↓↓
_tickCallback()
總結
簡單說,整個過程是這樣的,事件循環(huán)中有相應I/O事件發(fā)生的時候,libuv調用Node.js C++部分的回調,C++部分調用JavaScript部分的回調,順便調用nextTick設定的回調。
還是認真讀代碼吧,以上寫的僅供參考。