libuv源碼分析之stream第一篇

流的實現在libuv裏佔了很大篇幅,今天分析一下流的實現。首先看數據結構。流在libuv裏用uv_stream_s表示,他屬於handle族。繼承於uv_handle_s。

struct uv_stream_s {
  // uv_handle_s的字段
  void* data;        
  // 所屬事件循環   
  uv_loop_t* loop;  
  // handle類型    
  uv_handle_type type;  
  // 關閉handle時的回調
  uv_close_cb close_cb; 
  // 用於插入事件循環的handle隊列
  void* handle_queue[2];
  union {               
    int fd;             
    void* reserved[4];  
  } u;      
  // 用於插入事件循環的closing階段對應的隊列
  uv_handle_t* next_closing; 
  // 各種標記 
  unsigned int flags;
  // 流拓展的字段
  // 用戶寫入流的字節大小,流緩存用戶的輸入,然後等到可寫的時候才做真正的寫
  size_t write_queue_size; 
  // 分配內存的函數,內存由用戶定義,主要用來保存讀取的數據               
  uv_alloc_cb alloc_cb;  
  // 讀取數據的回調                 
  uv_read_cb read_cb; 
  // 連接成功後,執行connect_req的回調(connect_req在uv__xxx_connect中賦值)
  uv_connect_t *connect_req; 
  // 關閉寫端的時候,發送完緩存的數據,執行shutdown_req的回調(shutdown_req在uv_shutdown的時候賦值)    
  uv_shutdown_t *shutdown_req;
  // 流對應的io觀察者,即文件描述符+一個文件描述符事件觸發時執行的回調   
  uv__io_t io_watcher;  
  // 流緩存下來的,待寫的數據         
  void* write_queue[2];       
  // 已經完成了數據寫入的隊列   
  void* write_completed_queue[2];
  // 完成三次握手後,執行的回調
  uv_connection_cb connection_cb;
  // 操作流時出錯碼
  int delayed_error;  
  // accept返回的通信socket對應的文件描述符           
  int accepted_fd;    
  // 同上,用於緩存更多的通信socket對應的文件描述符           
  void* queued_fds;
}

流的實現中,最核心的字段是io觀察者,其餘的字段是和流的性質相關的。io觀察者封裝了流對應的文件描述符和文件描述符事件觸發時的回調。比如讀一個流,寫一個流,關閉一個流,連接一個流,監聽一個流,在uv_stream_s中都有對應的字段去支持。但是本質上是靠io觀察者去驅動的。
1 讀一個流,就是io觀察者中的文件描述符。可讀事件觸發時,執行用戶的讀回調。
2 寫一個流,先把數據寫到流中,然後io觀察者中的文件描述符。可寫事件觸發時,執行最後的寫入,並執行用戶的寫完成回調。
3 關閉一個流,就是io觀察者中的文件描述符。可寫事件觸發時,如果待寫的數據已經寫完(比如發送完),然後執行關閉流的寫端。接着執行用戶的回調。
4 連接一個流,比如作爲客戶端去連接服務器。就是io觀察者中的文件描述符。可讀事件觸發時(建立三次握手成功),執行用戶的回調。
5 監聽一個流,就是io觀察者中的文件描述符。可讀事件觸發時(有完成三次握手的連接),執行用戶的回調。
今天我們具體分析一下流讀寫操作的實現。首先我們看一下如何初始化一個流。

// 初始化流
void uv__stream_init(uv_loop_t* loop,
                     uv_stream_t* stream,
                     uv_handle_type type) {
  int err;
  // 記錄handle的類型
  uv__handle_init(loop, (uv_handle_t*)stream, type);
  stream->read_cb = NULL;
  stream->alloc_cb = NULL;
  stream->close_cb = NULL;
  stream->connection_cb = NULL;
  stream->connect_req = NULL;
  stream->shutdown_req = NULL;
  stream->accepted_fd = -1;
  stream->queued_fds = NULL;
  stream->delayed_error = 0;
  QUEUE_INIT(&stream->write_queue);
  QUEUE_INIT(&stream->write_completed_queue);
  stream->write_queue_size = 0;
  // 這個邏輯看起來是爲了拿到一個備用的文件描述符,如果以後觸發UV_EMFILE錯誤(打開的文件太多)時,使用這個備用的fd
  if (loop->emfile_fd == -1) {
    err = uv__open_cloexec("/dev/null", O_RDONLY);
    if (err < 0)
        err = uv__open_cloexec("/", O_RDONLY);
    if (err >= 0)
      loop->emfile_fd = err;
  }
  // 初始化io觀察者,把文件描述符(這裏還沒有,所以是-1)和回調uv__stream_io記錄在io_watcher上
  uv__io_init(&stream->io_watcher, uv__stream_io, -1);
}

我們看到流的初始化沒有太多邏輯。主要是初始化一些字段。接着我們看一下如何打開(激活)一個流。

// 關閉nagle,開啓長連接,保存fd 
int uv__stream_open(uv_stream_t* stream, int fd, int flags) {

  // 還沒有設置fd或者設置的同一個fd則繼續,否則返回busy
  if (!(stream->io_watcher.fd == -1 || stream->io_watcher.fd == fd))
    return UV_EBUSY;
    
  // 設置流的標記
  stream->flags |= flags;

  if (stream->type == UV_TCP) {
    // 關閉nagle算法
    if ((stream->flags & UV_HANDLE_TCP_NODELAY) && uv__tcp_nodelay(fd, 1))
      return UV__ERR(errno);

    // 開啓SO_KEEPALIVE,使用tcp長連接,一定時間後沒有收到數據包會發送心跳包
    if ((stream->flags & UV_HANDLE_TCP_KEEPALIVE) &&
        uv__tcp_keepalive(fd, 1, 60)) {
      return UV__ERR(errno);
    }
  }
   // 保存socket對應的文件描述符到io觀察者中,libuv會在io poll階段監聽該文件描述符
  stream->io_watcher.fd = fd;

  return 0;
}

打開一個流,本質上就是給這個流關聯一個文件描述符。還有一些屬性的設置。有了文件描述符,後續就可以操作這個流了。下面我們逐個操作分析。
1 讀
我們在一個流上執行uv_read_start。流的數據(如果有的話)就會源源不斷地流向調用方。

int uv_read_start(uv_stream_t* stream,
                  uv_alloc_cb alloc_cb,
                  uv_read_cb read_cb) {
  assert(stream->type == UV_TCP || stream->type == UV_NAMED_PIPE ||
      stream->type == UV_TTY);
  // 流已經關閉,不能讀
  if (stream->flags & UV_HANDLE_CLOSING)
    return UV_EINVAL;
  // 流不可讀,說明可能是隻寫流
  if (!(stream->flags & UV_HANDLE_READABLE))
    return -ENOTCONN;
  // 標記正在讀
  stream->flags |= UV_HANDLE_READING;
  // 記錄讀回調,有數據的時候會執行這個回調
  stream->read_cb = read_cb;
  // 分配內存函數,用於存儲讀取的數據
  stream->alloc_cb = alloc_cb;
  // 註冊讀事件
  uv__io_start(stream->loop, &stream->io_watcher, POLLIN);
  // 激活handle,有激活的handle,事件循環不會退出
  uv__handle_start(stream);

  return 0;
}

執行uv_read_start本質上是給流對應的文件描述符在epoll中註冊了一個可讀事件。並且給一些字段賦值,比如讀回調函數,分配內存的函數。打上正在做讀取操作的標記。然後在可讀事件觸發的時候,讀回調就會被執行,這個邏輯我們後面分析。除了開始讀取數據,還有一個讀操作就是停止讀取。對應的函數是uv_read_stop。

int uv_read_stop(uv_stream_t* stream) {
  // 是否正在執行讀取操作,如果不是,則沒有必要停止
  if (!(stream->flags & UV_HANDLE_READING))
    return 0;
  // 清除 正在讀取 的標記
  stream->flags &= ~UV_HANDLE_READING;
  // 撤銷 等待讀事件
  uv__io_stop(stream->loop, &stream->io_watcher, POLLIN);
  // 對寫事件也不感興趣,停掉handle。允許事件循環退出
  if (!uv__io_active(&stream->io_watcher, POLLOUT))
    uv__handle_stop(stream);
  stream->read_cb = NULL;
  stream->alloc_cb = NULL;
  return 0;
}

和start相反,start是註冊等待可讀事件和打上正在讀取這個標記,stop就是撤銷等待可讀事件和清除這個標記。另外還有一個輔助函數,判斷流是否設置了可讀屬性。

int uv_is_readable(const uv_stream_t* stream) {
  return !!(stream->flags & UV_HANDLE_READABLE);
}

2 寫
我們在流上執行uv_write就可以往流中寫入數據。

int uv_write(
	   // 一個寫請求,記錄了需要寫入的數據和信息。數據來自下面的const uv_buf_t bufs[]
  	   uv_write_t* req,
  	   // 往哪個流寫
       uv_stream_t* handle,
       // 需要寫入的數據
       const uv_buf_t bufs[],
       // 個數
       unsigned int nbufs,
       // 寫完後執行的回調
       uv_write_cb cb
) {
  return uv_write2(req, handle, bufs, nbufs, NULL, cb);
}

uv_write是直接調用uv_write2。第四個參數是NULL。代表是一般的寫數據,不傳遞文件描述符。

int uv_write2(
	uv_write_t* req,
    uv_stream_t* stream,
    const uv_buf_t bufs[],
    unsigned int nbufs,
    // 需要傳遞的文件描述符所在的流,這裏不分析,在分析unix的時候再分析
    uv_stream_t* send_handle,
    uv_write_cb cb
) 
{
  int empty_queue;
  // 是不可寫流
  if (!(stream->flags & UV_HANDLE_WRITABLE))
    return -EPIPE;
  // 流中緩存的數據大小是否爲0
  empty_queue = (stream->write_queue_size == 0);

  // 初始化一個寫請求
  uv__req_init(stream->loop, req, UV_WRITE);
  // 寫完後執行的回調
  req->cb = cb;
  // 往哪個流寫
  req->handle = stream;
  // 寫出錯的錯誤碼,初始化爲0
  req->error = 0;
  QUEUE_INIT(&req->queue);
  // 默認buf
  req->bufs = req->bufsml;
  // 不夠則擴容
  if (nbufs > ARRAY_SIZE(req->bufsml))
    req->bufs = uv__malloc(nbufs * sizeof(bufs[0]));
  // 把需要寫入的數據填充到req中
  memcpy(req->bufs, bufs, nbufs * sizeof(bufs[0]));
  // 需要寫入的buf個數
  req->nbufs = nbufs;
  // 目前寫入的buf個數,初始化是0
  req->write_index = 0;
  // 更新流中待寫數據的總長度,就是每個buf的數據大小加起來
  stream->write_queue_size += uv__count_bufs(bufs, nbufs);

  // 插入待寫隊列
  QUEUE_INSERT_TAIL(&stream->write_queue, &req->queue);
  /*
   stream->connect_req非空說明是作爲客戶端,並且正在建立三次握手,建立成功會置connect_req爲NULL。
   這裏非空說明還沒有建立成功或者不是作爲客戶端(不是連接流)。即沒有用到connect_req這個字段。
  */
  if (stream->connect_req) {
    /* Still connecting, do nothing. */
  }
  else if (empty_queue) { 
    // 待寫隊列爲空,則直接觸發寫動作,即操作文件描述符
    uv__write(stream);
  }
  else {
    /*
	    隊列非空,說明往底層寫,uv__write中不一樣會註冊等待可寫事件,所以這裏註冊一下
	    給流注冊等待可寫事件,觸發的時候,把數據消費掉
    */
    uv__io_start(stream->loop, &stream->io_watcher, POLLOUT);
  }

  return 0;
}

uv_write2的主要邏輯就是封裝一個寫請求,插入到流的待寫隊列。然後根據當前流的情況。看是直接寫入還是等待會再寫入。架構大致如下。

我們繼續看真正的寫操作。

static void uv__write(uv_stream_t* stream) {
  struct iovec* iov;
  QUEUE* q;
  uv_write_t* req;
  int iovmax;
  int iovcnt;
  ssize_t n;
  int err;

start:
  // 待寫隊列爲空,沒得寫
  if (QUEUE_EMPTY(&stream->write_queue))
    return;
  // 遍歷待寫隊列,把每個節點的數據寫入底層
  q = QUEUE_HEAD(&stream->write_queue);
  req = QUEUE_DATA(q, uv_write_t, queue);
  /*
    struct iovec {
        ptr_t iov_base; // 數據首地址
        size_t iov_len; // 數據長度
    };  
    iovec和bufs結構體的定義一樣
  */
  // 轉成iovec格式發送
  iov = (struct iovec*) &(req->bufs[req->write_index]);
  // 待寫的buf個數,nbufs是總數,write_index是當前已寫的個數
  iovcnt = req->nbufs - req->write_index;
  // 最多能寫幾個
  iovmax = uv__getiovmax();

  // 取最小值
  if (iovcnt > iovmax)
    iovcnt = iovmax;

  // 有需要傳遞的描述符
  if (req->send_handle) {
    // 需要傳遞文件描述符的邏輯,分析unix域的時候再分析
  } else { // 單純發送數據,則直接寫
    do {
      if (iovcnt == 1) {
        n = write(uv__stream_fd(stream), iov[0].iov_base, iov[0].iov_len);
      } else {
        n = writev(uv__stream_fd(stream), iov, iovcnt);
      }
    } while (n == -1 && errno == EINTR);
  }
  // 發送出錯
  if (n < 0) {
    // 發送失敗的邏輯,我們不具體分析
  } else {
    // 寫成功,n是寫成功的字節數
    while (n >= 0) {
      // 本次待寫數據的首地址
      uv_buf_t* buf = &(req->bufs[req->write_index]);
      // 某個buf的數據長度
      size_t len = buf->len;
      // len如果大於n說明本buf的數據部分被寫入
      if ((size_t)n < len) {
        // 更新指針,指向下次待發送的數據首地址
        buf->base += n;
        // 更新待發送數據的長度
        buf->len -= n;
        // 更新待寫數據的總長度
        stream->write_queue_size -= n;
        n = 0;
        // 設置了一直寫標記,則繼續寫
        if (stream->flags & UV_HANDLE_BLOCKING_WRITES) {
          goto start;
        } else {
          // 否則等待可寫事件觸發的時候再寫
          break;
        }
      } else {
        // 本buf的數據完成被寫入,更新下一個待寫入的buf位置
        req->write_index++;
        n -= len;
        // 更新待寫數據總長度
        stream->write_queue_size -= len;
        // 如果寫完了全部buf,觸發回調
        if (req->write_index == req->nbufs) {
          // 寫完了本請求的數據,做後續處理
          uv__write_req_finish(req);
          return;
        }
      }
    }
  }
  // 到這說明數據還沒有完全被寫入,註冊等待可寫事件,等待繼續寫
  uv__io_start(stream->loop, &stream->io_watcher, POLLOUT);
  return;

error:
  // 寫出錯
  req->error = err;
  uv__write_req_finish(req);
  // 撤銷等待可寫事件
  uv__io_stop(stream->loop, &stream->io_watcher, POLLOUT);
  // 沒有註冊了等待可讀事件,則停掉流
  if (!uv__io_active(&stream->io_watcher, POLLIN))
    uv__handle_stop(stream);
}

我們看一下寫完一個請求後,libuv如何處理他。邏輯在uv__write_req_finish函數。

// 把buf的數據寫入完成或寫出錯後觸發的回調
static void uv__write_req_finish(uv_write_t* req) {
  uv_stream_t* stream = req->handle;
  // 移出隊列
  QUEUE_REMOVE(&req->queue);
  // 寫入成功了
  if (req->error == 0) {
    /*
      bufsml是默認的buf數,如果不夠,則bufs指向新的內存,
      然後再儲存數據。兩者不等說明申請了額外的內存,需要free掉
    */ 
    if (req->bufs != req->bufsml)
      uv__free(req->bufs);
    req->bufs = NULL;
  }
  // 插入寫完成隊列
  QUEUE_INSERT_TAIL(&stream->write_completed_queue, &req->queue);
  // 插入pending隊列,在pending階段執行回調
  uv__io_feed(stream->loop, &stream->io_watcher);
}

uv__write_req_finish的邏輯比較簡單,就是把節點從待寫隊列中移除。然後插入寫完成隊列。最後把io
觀察者插入pending隊列。在pending節點會知道io觀察者的回調(uv__stream_io)。流模塊的邏輯比較多,今天先分析到這裏。後續繼續分析其他操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章