nodejs事件循環階段之poll io

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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章