Libuv源码分析 —— 7. 进程/线程间通信

759 阅读8分钟

网络 I/O 运行原理

unix(like)世界里,一切皆文件,而文件是什么呢?文件就是一串二进制流而已,不管socket,还是FIFO、管道、终端,一切都是文件,一切都是流。在信息 交换的过程中,我们对这些流进行数据的收发操作,简称为I/O操作(input and output),从数据流中读取数据,系统会调用read(读取数据);写入数据,系统调用write(写入数据)。不过话说回来了 ,计算机里有这么多的流,我怎么知道要操作哪个流呢?对,就是文件描述符,即通常所说的fd,一个fd就是一个整数,所以,对这个整数的操作,就是对这个文件(流)的操作。我们创建一个socket,通过系统调用会,返回一个文件描述符,那么剩下对socket的操作就会转化为对这个描述符的操作。

在 Libuv 中,通过 循环 来不断取出 watcher 队列中的事件,这个事件结构体上保存了文件描述符,这个文件描述符上的事件和事件触发后的回调,所以可以通过这个事件结构体来初始化 epoll_event,之后利用 epoll_wait 来等待文件描述符上 I/O 事件的发生,事件发生之后,调用相应的回调函数。我们可以通过调用 IO观察者 相关API来将事件推入 watcher 队列中。

下面就让咱们通过这种机制来了解进程/线程间是如何通信的吧! image.png

进程/线程间通信

  • 实现进程/线程间通信,得用到一个文件描述符,主进程监听这个文件描述符上的读事件。文件描述符的实现可以使用 eventfd 或者管道。这一部分的实现可以看这里
  • 其它线程可以通过往这个文件描述符上 write,来激活这个文件描述符上的读事件。这一部分的实现可以看这里
  • 当主进程接收到这个事件后,调用回调函数。这一部分的实现可以看这里
  • 其他进程/线程和主进程的通信是使用 uv_async_t 结构体实现的。Libuv 使用 loop->async_handles 记 录 所有的 uv_async_t 结 构 体 , 使 用 loop->async_io_watcher 作为所有 uv_async_t 结构体的 io 观察者。即 loop-> async_handles 队列上所有的 handle 都是共享 async_io_watcher 这个 io 观察者。第一次插入一个 uv_async_t 结构体到 async_handle 队列时, 会 初 始 化 io 观 察 者 。 如 果 再 次 注 册 一 个 async_handle , 只 会 在 loop->async_handle 队列和 handle 队列插入一个节点,而不是新增一个 io 观察者。

uv_async_t --- 异步句柄

数据类型
  • uv_async_t

    异步句柄类型。

API
  • int uv_async_init(uv_loop_t* loopuv_async_t* asyncuv_async_cb async_cb)

    初始化句柄。 允许回调函数为NULL。

    返回:0 当成功时,或者一个 < 0 的错误代码当失败时。

    不同于其他句柄初始化函数,句柄立刻开始。

  • int uv_async_send(uv_async_t* async)

    唤醒事件循环并且调用异步句柄的回调函数。

    返回:0 当成功时,或者一个 < 0 的错误代码当失败时。

    从任何线程调用这个函数都是安全的。 回调函数将从循环的线程上被调用。

    libuv将会合并对 uv_async_send() 的调用,那就是说,不是对它的每个调用会 yield 回调函数的执行。 例如:如果在回调函数被调用前一连调用 uv_async_send() 5 次,回调函数将只会调用一次。 如果在回调函数被调用后再次调用 uv_async_send() ,回调函数将会再次被调用。

example
  • 例:
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    #include <uv.h>
    
    uv_loop_t *loop;
    uv_async_t async;
    
    double percentage;
    
    void fake_download(uv_work_t *req) {
        int size = *((int*) req->data);
        int downloaded = 0;
        while (downloaded < size) {
            percentage = downloaded*100.0/size;
            // 把要发送的数据挂到 async.data 上
            async.data = (void*) &percentage;
            // uv_async_send同样是非阻塞的,调用后会立即返回
            // 给 loop 进程发送消息
            uv_async_send(&async);
    
            sleep(1);
            downloaded += (200+random())%1000; 
        }
    }
    
    void after(uv_work_t *req, int status) {
        fprintf(stderr, "Download complete\n");
        uv_close((uv_handle_t*) &async, NULL);
    }
    
    // 函数print_progress是标准的libuv模式,从监视器中抽取数据
    void print_progress(uv_async_t *handle) {
        // 获取线程池中的线程发来的消息
        double percentage = *((double*) handle->data);
        fprintf(stderr, "Downloaded %.2f%%\n", percentage);
    }
    
    int main() {
        loop = uv_default_loop();
    
        uv_work_t req;
        int size = 10240;
        req.data = (void*) &size;
    
        // 初始化在 loop 上线程通信句柄 async 的回调函数 print_progress
        uv_async_init(loop, &async, print_progress);
        // 在线程池选取一个线程执行 fake_download 函数 
        uv_queue_work(loop, &req, fake_download, after);
    
        return uv_run(loop, UV_RUN_DEFAULT);
    }
    
    // 执行结果
    /*
        Downloaded 0.00%
        Downloaded 5.69%
        Downloaded 6.53%
        Downloaded 16.07%
        Downloaded 17.20%
        Downloaded 26.89%
        Downloaded 32.12%
        Downloaded 37.84%
        Downloaded 44.60%
        Downloaded 52.89%
        Downloaded 58.96%
        Downloaded 64.44%
        Downloaded 66.66%
        Downloaded 75.35%
        Downloaded 77.88%
        Downloaded 87.29%
        Downloaded 88.52%
        Downloaded 95.74%
        Download complete
    */
    

源码分析

uv_async_t 结构体

struct uv_async_t {
  // 句柄[handle]相关参数
  // uv_handle_t
  void* data;
  uv_loop_t* loop;
  uv_handle_type type;
  uv_close_cb close_cb;
  void* handle_queue[2];
  union {                                                                     
    int fd;                                                                   
    void* reserved[4];                                                        
  } u; 
  uv_handle_t* next_closing;
  unsigned int flags;

  // 异步句柄相关参数
  uv_async_cb async_cb;                                                       
  void* queue[2];                                                             
  int pending;  
}

uv_async_init

  • 初始化 async handle 的一些字段。然后把 handle 插入 async_handle 队列中
  • 源码
    int uv_async_init(uv_loop_t* loop, uv_async_t* handle, uv_async_cb async_cb) {
      int err;
    
      /* 其实这个函数的内部主要做了以下操作:创建一个管道,并且将管道注册到loop->async_io_watcher,并且start */
      err = uv__async_start(loop);
      if (err)
        return err;
    
      /* 设置UV_HANDLE_REF标记,并且将async handle 插入loop->handle_queue */
      uv__handle_init(loop, (uv_handle_t*)handle, UV_ASYNC);
    
      handle->async_cb = async_cb;
      // 标记是否有任务完成了
      handle->pending = 0;
    
      /* queue 作为队列节点插入 loop->async_handles */
      QUEUE_INSERT_TAIL(&loop->async_handles, &handle->queue);
      // 激活 handle 为 active 状态
      uv__handle_start(handle);
    
      return 0;
    }
    

uv_async_start

  • 首先来介绍一下 eventfd
eventfd
  • eventfd是linux 2.6.22后系统提供的一个轻量级的进程间通信的系统调用[是一个用来通知事件的文件描述符],eventfd通过一个进程间共享的64位计数器完成进程间通信,这个计数器由在linux内核空间维护,用户可以通过调用write方法向内核空间写入一个64位的值,也可以调用read方法读取这个值
  • 新建
    #include <sys/eventfd.h>
    int eventfd(unsigned int initval, int flags);
    

    initval 为64位计数器初始值

    flags 可以是以下三个标志位的OR结果:

    • EFD_CLOEXEC : fork子进程时不继承,对于多线程的程序设上这个值不会有错的。
    • EFD_NONBLOCK: 文件会被设置成O_NONBLOCK,读操作不阻塞。若不设置,一直阻塞直到计数器中的值大于0。
    • EFD_SEMAPHORE : 支持 semophore 语义的read,每次读操作,计数器的值自减1。

    返回 eventfd 类型的文件描述符

  • 读操作

    读取计数器中的值。

    typedef uint64_t eventfd_t;
    int eventfd_read(int fd, eventfd_t *value);
    
    1. 如果计数器中的值大于0:
    • 设置了 EFD_SEMAPHORE 标志位,则返回1,且计数器中的值也减去1。

    • 没有设置 EFD_SEMAPHORE 标志位,则返回计数器中的值,且计数器置0。

    1. 如果计数器中的值为0:
    • 设置了 EFD_NONBLOCK 标志位就直接返回-1。
    • 没有设置 EFD_NONBLOCK 标志位就会一直阻塞直到计数器中的值大于0。
  • 写操作

    向计数器中写入值。

    int eventfd_write(int fd, eventfd_t value);
    
    1. 如果写入值的和小于0xFFFFFFFFFFFFFFFE,则写入成功
    2. 如果写入值的和大于0xFFFFFFFFFFFFFFFE
    • 设置了 EFD_NONBLOCK 标志位就直接返回-1。
    • 如果没有设置 EFD_NONBLOCK 标志位,则会一直阻塞直到read操作执行
  • 关闭
    #include <unistd.h>
    int close(int fd);
    
  • 例:
    #include <sys/eventfd.h>
    #include <unistd.h>
    #include <iostream>
    
    int main() {
        int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
        eventfd_write(efd, 2);
        eventfd_t count;
        eventfd_read(efd, &count);
        std::cout << count << std::endl;
        close(efd);
    }
    

    上述程序主要做了如下事情:

    • 创建事件,初始计数器为0;
    • 写入计数2;
    • 读出计数2
    • 关闭事件
uv_async_start
  • uv__async_start 只会执行一次。主要逻辑是:

    申请用于通信的文件描述符,然后把读端和回调封装到 loop->async_io_watcher, 写端保存在 loop->async_wfd

  • 源码
    // 初始化异步通信的 io 观察者
    static int uv__async_start(uv_loop_t* loop) {
      int pipefd[2];
      int err;
    
      /*
        因为 libuv 在初始化的时候会主动注册一个
        用于主线程和子线程通信的 async handle。
        从而初始化了 async_io_watcher。所以如果后续
        再注册 async handle,则不需要处理了。
    
        父子线程通信时,libuv 是优先使用 eventfd,如果不支持会回退到匿名管道。
        如果是匿名管道
        fd 是管道的读端,loop->async_wfd 是管道的写端
        如果是 eventfd
        fd 是读端也是写端。async_wfd 是-1 
        所以这里判断 loop->async_io_watcher.fd 而不是 async_wfd 的值
     */
      if (loop->async_io_watcher.fd != -1)
        return 0;
    
      #ifdef __linux__
        // 获取一个用于进程间通信的 fd
        err = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
        if (err < 0)
          return UV__ERR(errno);
    
        // 成功则保存起来,不支持则使用管道通信作为进程间通信
        pipefd[0] = err;
        pipefd[1] = -1;
      #else
        err = uv__make_pipe(pipefd, UV_NONBLOCK_PIPE);
        if (err < 0)
          return err;
      #endif
    
      // 初始化 io 观察者 async_io_watcher
      uv__io_init(&loop->async_io_watcher, uv__async_io, pipefd[0]);
      // 注册 io 观察者到 loop 里,并注册需要监听的事件 POLLIN,读
      uv__io_start(loop, &loop->async_io_watcher, POLLIN);
      // 用于主线程和子线程通信的 fd,管道的写端,子线程使用
      loop->async_wfd = pipefd[1];
    
      return 0;
    }
    

uv__async_send ———— 触发事件的方法

  • 源码
    int uv_async_send(uv_async_t* handle) {
      /* Do a cheap read first. */
      if (ACCESS_ONCE(int, handle->pending) != 0)
        return 0;
    
      /*
        设置 async handle 的 pending 标记
        如果 pending 是 0,则设置为 1,返回 0,如果是 1 则返回 1,
        所以同一个 handle 如果多次调用该函数是会被合并的
      */
      if (cmpxchgi(&handle->pending, 0, 1) != 0)
        return 0;
    
      /* Wake up the other thread's event loop. */
      // 真正的唤醒操作是uv__async_send()函数
      uv__async_send(handle->loop);
    
      /* Tell the other thread we're done. */
      if (cmpxchgi(&handle->pending, 1, 2) != 1)
        abort();
    
      return 0;
    }
    
    
    static void uv__async_send(uv_loop_t* loop) {
      const void* buf;
      ssize_t len;
      int fd;
      int r;
    
      buf = "";
      len = 1;
      // 用于异步通信的管道的写端
      fd = loop->async_wfd;
    
    #if defined(__linux__)
      // 说明用的是 eventfd 而不是管道
      if (fd == -1) {
        static const uint64_t val = 1;
        buf = &val;
        len = sizeof(val);
        fd = loop->async_io_watcher.fd;  /* eventfd */
      }
    #endif
      // 通知读端
      do
        r = write(fd, buf, len);
      while (r == -1 && errno == EINTR);
    
      if (r == len)
        return;
    
      if (r == -1)
        if (errno == EAGAIN || errno == EWOULDBLOCK)
          return;
    
      abort();
    }
    

uv_async_io ———— 事件触发后执行的回调

  • uv__async_io 会遍历 loop->async_handles 队里中所有的 uv_async_t。然后判断该 uv_async_t 是否有事件触发(通过 uv_async_t->pending 字段)。如果有的话,则执 行该 uv_async_t 对应的回调

  • 源码

    static void uv__async_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
      char buf[1024];
      ssize_t r;
      QUEUE queue;
      QUEUE* q;
      uv_async_t* h;
    
      assert(w == &loop->async_io_watcher);
    
      for (;;) {
        /* 不断的读取 w->fd 上的数据到 buf 中直到为空,buf 中的数据无实际用途 */
        r = read(w->fd, buf, sizeof(buf));
    
        // 如果数据大于 buf 的长度,接着读,清空这一轮写入的数据
        if (r == sizeof(buf))
          continue;
    
        // 不等于-1,说明读成功,失败的时候返回-1,errno 是错误码
        if (r != -1)
          break;
    
        if (errno == EAGAIN || errno == EWOULDBLOCK)
          break;
    
        // 被信号中断,继续读
        if (errno == EINTR)
          continue;
    
        // 出错,发送 abort 信号
        abort();
      }
    
      // 把 async_handles 队列里的所有节点都移到 queue 变量中
      QUEUE_MOVE(&loop->async_handles, &queue);
    
      /* 遍历队列判断是谁发的消息,并执行相应的回调函数 */
      // 因为 uv_async_init() 函数可能多次被调用,始化多个 async handle,但是 loop->async_io_watcher 
      // 只有一个,所以要遍历 async_handles,来看看是哪些 async 被触发了
      while (!QUEUE_EMPTY(&queue)) {
        // 逐个取出节点
        q = QUEUE_HEAD(&queue);
    
        // 根据结构体字段获取结构体首地址
        h = QUEUE_DATA(q, uv_async_t, queue);
    
        // 从队列中移除该节点
        QUEUE_REMOVE(q);
    
        // 重新插入 async_handles 队列,等待下次事件
        QUEUE_INSERT_TAIL(&loop->async_handles, q);
    
        /*
            判断哪些 async 被触发了。pending 在 uv_async_send
            里设置成 1,如果 pending 等于 1,则清 0,返回 1.如果
            pending 等于 0,则返回 0
        */
        if (0 == uv__async_spin(h))
          continue;  /* Not pending. */
    
        if (h->async_cb == NULL)
          continue;
    
        /* 调用async 的回调函数 */
        h->async_cb(h);
      }
    }