揭秘Mediasoup的进程间管道通讯设计

转载请注明出处:https://blog.csdn.net/impingo
我的开源项目地址:https://github.com/pingostack/pingos
开源项目:https://pingos.io

目录

  • 整体设计
  • Worker问题分析
  • libuv代码追踪
  • 总结
  • QQ交流群:697773082
  • 微信(cczjp1989)

整体设计

nodejs
Worker
Worker
Worker

如上图,Mediasoup项目使用nodejs作为业务层,C++编写的Worker作为WebRTC的任务进程。nodejs和Worker之间使用UnixSocket作为进程间通讯的手段。

Worker问题分析

刚开始接触到Mediasoup V3的时候,Mediasoup中有段代码让我感到疑惑:
揭秘Mediasoup的进程间管道通讯设计_第1张图片
Worker进程将描述符3和描述符4作为消息读取和写入的目标,但是Worker进程由主进程创建,主进程为了与Worker进程通信必然要为每个Worker进程提前创建UnixSocket,而Worker进程只需继承自己的UnixSocket描述符即可。
主进程创建的描述符必然是依次增加的,例如:0、1、2分别代表标准输入(stdin),标准输出(stdout),错误输出(stderr),创建新的描述符时就会从3开始向后累加,子进程越多创建的UnixSocket也就越多,描述符的值会越来越大。
每个Worker进程理应继承主进程的描述符,它是如何做到让每个Worker进程都将3、4作为读写描述符的呢?

创建
创建
创建
创建
创建
创建
分配
分配
分配
分配
分配
分配
nodejs
描述符3
描述符4
描述符5
描述符6
描述符7
描述符8
Worker
Worker
Worker

libuv代码追踪

我们都知道nodejs是基于libuv网络库实现的,为了找到答案,我决定看看libuv底层代码。
我从uv_spawn函数开始追踪,直到看到初始化子进程的函数我就恍然大明白了。

static void uv__process_child_init(const uv_process_options_t* options,
                                   int stdio_count,
                                   int (*pipes)[2],
                                   int error_fd) {
  sigset_t set;
  int close_fd;
  int use_fd;
  int err;
  int fd;
  int n;

  if (options->flags & UV_PROCESS_DETACHED)
    setsid();

  /* First duplicate low numbered fds, since it's not safe to duplicate them,
   * they could get replaced. Example: swapping stdout and stderr; without
   * this fd 2 (stderr) would be duplicated into fd 1, thus making both
   * stdout and stderr go to the same fd, which was not the intention. */
  for (fd = 0; fd < stdio_count; fd++) {
    use_fd = pipes[fd][1];
    if (use_fd < 0 || use_fd >= fd)
      continue;
    pipes[fd][1] = fcntl(use_fd, F_DUPFD, stdio_count);
    if (pipes[fd][1] == -1) {
      uv__write_int(error_fd, UV__ERR(errno));
      _exit(127);
    }
  }

  for (fd = 0; fd < stdio_count; fd++) {
    close_fd = pipes[fd][0];
    use_fd = pipes[fd][1];

    if (use_fd < 0) {
      if (fd >= 3)
        continue;
      else {
        /* redirect stdin, stdout and stderr to /dev/null even if UV_IGNORE is
         * set
         */
        use_fd = open("/dev/null", fd == 0 ? O_RDONLY : O_RDWR);
        close_fd = use_fd;

        if (use_fd < 0) {
          uv__write_int(error_fd, UV__ERR(errno));
          _exit(127);
        }
      }
    }

    if (fd == use_fd)
      uv__cloexec_fcntl(use_fd, 0);
    else
      fd = dup2(use_fd, fd);

    if (fd == -1) {
      uv__write_int(error_fd, UV__ERR(errno));
      _exit(127);
    }

    if (fd <= 2)
      uv__nonblock_fcntl(fd, 0);

    if (close_fd >= stdio_count)
      uv__close(close_fd);
  }

  for (fd = 0; fd < stdio_count; fd++) {
    use_fd = pipes[fd][1];

    if (use_fd >= stdio_count)
      uv__close(use_fd);
  }

  if (options->cwd != NULL && chdir(options->cwd)) {
    uv__write_int(error_fd, UV__ERR(errno));
    _exit(127);
  }

  if (options->flags & (UV_PROCESS_SETUID | UV_PROCESS_SETGID)) {
    /* When dropping privileges from root, the `setgroups` call will
     * remove any extraneous groups. If we don't call this, then
     * even though our uid has dropped, we may still have groups
     * that enable us to do super-user things. This will fail if we
     * aren't root, so don't bother checking the return value, this
     * is just done as an optimistic privilege dropping function.
     */
    SAVE_ERRNO(setgroups(0, NULL));
  }

  if ((options->flags & UV_PROCESS_SETGID) && setgid(options->gid)) {
    uv__write_int(error_fd, UV__ERR(errno));
    _exit(127);
  }

  if ((options->flags & UV_PROCESS_SETUID) && setuid(options->uid)) {
    uv__write_int(error_fd, UV__ERR(errno));
    _exit(127);
  }

  if (options->env != NULL) {
    environ = options->env;
  }

  /* Reset signal disposition.  Use a hard-coded limit because NSIG
   * is not fixed on Linux: it's either 32, 34 or 64, depending on
   * whether RT signals are enabled.  We are not allowed to touch
   * RT signal handlers, glibc uses them internally.
   */
  for (n = 1; n < 32; n += 1) {
    if (n == SIGKILL || n == SIGSTOP)
      continue;  /* Can't be changed. */

#if defined(__HAIKU__)
    if (n == SIGKILLTHR)
      continue;  /* Can't be changed. */
#endif

    if (SIG_ERR != signal(n, SIG_DFL))
      continue;

    uv__write_int(error_fd, UV__ERR(errno));
    _exit(127);
  }

  /* Reset signal mask. */
  sigemptyset(&set);
  err = pthread_sigmask(SIG_SETMASK, &set, NULL);

  if (err != 0) {
    uv__write_int(error_fd, UV__ERR(err));
    _exit(127);
  }

  execvp(options->file, options->args);
  uv__write_int(error_fd, UV__ERR(errno));
  _exit(127);
}
#endif

感兴趣的同学可以仔细阅读,这里我主要解读一下fcntl(use_fd, F_DUPFD, stdio_count);fd = dup2(use_fd, fd);这两步关键操作。

  • fcntl(use_fd, F_DUPFD, stdio_count);
    分配一个大于等于stdio_count的没有被占用的新描述符取代use_fd,如果您看过libuv的代码就知道stdio_count是大于等于3的,所以保证了分配给子进程的描述符大于2,因为0、1、2是系统的默认输入输出占用的。
  • fd = dup2(use_fd, fd);
    函数dup2起到重定向的作用,也就是说经过dup2处理后,描述符fd将和use_fd指向同一个文件。例如:use_fd=10;指向了一个UnixSocket文件,而fd=3未被使用过,经过dup2处理,3和10一样将同时指向UnixSocket文件。
    结合上下文代码可以知道,fd就是pipe数组的下标。对于Mediasoup项目中pipe数组内的信息是pipe[0] = stdin, pipe[1] = stdout, pipe[2] = stderr, pipe[3] = unixsocket, pipe[4] = unixsocket
    那么子进程调用uv__process_child_init初始化之后,pipe数组中的信息就变成了pipe[3] = 3, pipe[4] = 4。
创建
创建
创建
创建
创建
创建
子进程dup2重定向
子进程dup2重定向
子进程dup2重定向
子进程dup2重定向
子进程dup2重定向
子进程dup2重定向
分配
分配
分配
分配
分配
分配
nodejs
描述符3
描述符4
描述符5
描述符6
描述符7
描述符8
描述符3
描述符4
描述符3
描述符4
描述符3
描述符4
Worker
Worker
Worker

总结

libuv使用dup2函数将子进程集成进来的UnixSocket描述符做了一层重定向。
如果想要更加深入地理解libuv和Mediasoup的进程机制,建议详细研究一下libuv创建进程和pipe的流程。

QQ交流群:697773082

微信(cczjp1989)

在这里插入图片描述

你可能感兴趣的:(webrtc,WebRTC,learning,webrtc,c++,PIPE)