poll io是nodejs非常重要的一個階段,文件io、網絡io、信號處理等都在這個階段處理。這也是最複雜的一個階段。處理邏輯在uv__io_poll這個函數。這個函數比較複雜,我們分開分析。
開始說poll io之前,先了解一下他相關的一些數據結構。
1 io觀察者uv__io_t。這個結構體是poll io階段核心結構體。他主要是保存了io相關的文件描述符、回調、感興趣的事件等信息。
2 watcher_queue觀察者隊列。所有需要libuv處理的io觀察者都掛載在這個隊列裏。libuv會逐個處理。
我們看如何初始化一個io觀察者
// 初始化io觀察者
void uv__io_init(uv__io_t* w, uv__io_cb cb, int fd) {
// 初始化隊列,回調,需要監聽的fd
QUEUE_INIT(&w->pending_queue);
QUEUE_INIT(&w->watcher_queue);
w->cb = cb;
w->fd = fd;
// 上次加入epoll時感興趣的事件,在執行完epoll操作函數後設置
w->events = 0;
// 當前感興趣的事件,在再次執行epoll函數之前設置
w->pevents = 0;
}
我們再看一下如何註冊一個io觀察到libuv。
void uv__io_start(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
// 設置當前感興趣的事件
w->pevents |= events;
// 可能需要擴容
maybe_resize(loop, w->fd + 1);
if (w->events == w->pevents)
return;
// io觀察者沒有掛載在其他地方則插入libuv的io觀察者隊列
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_start函數就是把一個io觀察者插入到libuv的觀察者隊列中,並且在watchers數組中保存一個映射關係。libuv在poll io階段會處理io觀察者隊列。
下面我們開始分析poll io階段。先看第一段邏輯。
// 沒有io觀察者,則直接返回
if (loop->nfds == 0) {
assert(QUEUE_EMPTY(&loop->watcher_queue));
return;
}
// 遍歷io觀察者隊列
while (!QUEUE_EMPTY(&loop->watcher_queue)) {
// 取出當前頭節點
q = QUEUE_HEAD(&loop->watcher_queue);
// 脫離隊列
QUEUE_REMOVE(q);
// 初始化(重置)節點的前後指針
QUEUE_INIT(q);
// 通過結構體成功獲取結構體首地址
w = QUEUE_DATA(q, uv__io_t, watcher_queue);
// 設置當前感興趣的事件
e.events = w->pevents;
// 這裏使用了fd字段,事件觸發後再通過fd從watchs字段裏找到對應的io觀察者,沒有使用ptr指向io觀察者的方案
e.data.fd = w->fd;
// w->events初始化的時候爲0,則新增,否則修改
if (w->events == 0)
op = EPOLL_CTL_ADD;
else
op = EPOLL_CTL_MOD;
// 修改epoll的數據
epoll_ctl(loop->backend_fd, op, w->fd, &e)
// 記錄當前加到epoll時的狀態
w->events = w->pevents;
}
第一步首先遍歷io觀察者,修改epoll的數據,即感興趣的事件。然後準備進入等待,如果設置了UV_LOOP_BLOCK_SIGPROF的話。libuv會做一個優化。如果調setitimer(ITIMER_PROF,…)設置了定時觸發SIGPROF信號,則到期後,並且每隔一段時間後會觸發SIGPROF信號,這裏如果設置了UV_LOOP_BLOCK_SIGPROF救護屏蔽這個信號。否則會提前喚醒epoll_wait。
psigset = NULL;
if (loop->flags & UV_LOOP_BLOCK_SIGPROF) {
sigemptyset(&sigset);
sigaddset(&sigset, SIGPROF);
psigset = &sigset;
}
/*
http://man7.org/linux/man-pages/man2/epoll_wait.2.html
pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = epoll_wait(epfd, &events, maxevents, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);
即屏蔽SIGPROF信號,避免SIGPROF信號喚醒epoll_wait,但是卻沒有就緒的事件
*/
nfds = epoll_pwait(loop->backend_fd,
events,
ARRAY_SIZE(events),
timeout,
psigset);
// epoll可能阻塞,這裏需要更新事件循環的時間
uv__update_time(loop)
在epoll_wait可能會引起主線程阻塞,具體要根據libuv當前的情況。所以wait返回後需要更新當前的時間,否則在使用的時候時間差會比較大。因爲libuv會在每輪時間循環開始的時候緩存當前時間這個值。其他地方直接使用,而不是每次都去獲取。下面我們接着看epoll返回後的處理(假設有事件觸發)。
// 保存epoll_wait返回的一些數據,maybe_resize申請空間的時候+2了
loop->watchers[loop->nwatchers] = (void*) events;
loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;
for (i = 0; i < nfds; i++) {
// 觸發的事件和文件描述符
pe = events + i;
fd = pe->data.fd;
// 根據fd獲取io觀察者,見上面的圖
w = loop->watchers[fd];
// 會其他回調裏被刪除了,則從epoll中刪除
if (w == NULL) {
epoll_ctl(loop->backend_fd, EPOLL_CTL_DEL, fd, pe);
continue;
}
if (pe->events != 0) {
// 用於信號處理的io觀察者感興趣的事件觸發了,即有信號發生。
if (w == &loop->signal_io_watcher)
have_signals = 1;
else
// 一般的io觀察者指向回調
w->cb(loop, w, pe->events);
nevents++;
}
}
// 有信號發生,觸發回調
if (have_signals != 0)
loop->signal_io_watcher.cb(loop, &loop->signal_io_watcher, POLLIN);
這裏開始處理io事件,執行io觀察者裏保存的回調。但是有一個特殊的地方就是信號處理的io觀察者需要單獨判斷。他是一個全局的io觀察者,和一般動態申請和銷燬的io觀察者不一樣,他是存在於libuv運行的整個生命週期。async io也是。這就是poll io的整個過程。最後看一下epoll_wait阻塞時間的計算規則。
// 計算epoll使用的timeout
int uv_backend_timeout(const uv_loop_t* loop) {
// 下面幾種情況下返回0,即不阻塞在epoll_wait
if (loop->stop_flag != 0)
return 0;
// 沒有東西需要處理,則不需要阻塞poll io階段
if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
return 0;
// idle階段有任務,不阻塞,儘快返回直接idle任務
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
// 同上
if (!QUEUE_EMPTY(&loop->pending_queue))
return 0;
// 同上
if (loop->closing_handles)
return 0;
// 返回下一個最早過期的時間,即最早超時的節點
return uv__next_timeout(loop);
}