关于饿了么 Child Process 模块答案的疑问
发布于 7 年前 作者 hdumok 3939 次浏览 来自 问答

文档地址 https://github.com/ElemeFE/node-interview/blob/master/sections/process.md#process

饿了么文档: Child Process 子进程 (Child Process) 是进程中一个重要的概念. 你可以通过 Node.js 的 child_process 模块来执行可执行文件, 调用命令行命令, 比如其他语言的程序等. 也可以通过该模块来将 .js 代码以子进程的方式启动. 比较有名的网易的分布式架构 pomelo 就是基于该模块 (而不是 cluster) 来实现多进程分布式架构的. child_process.fork 与 POSIX 的 fork 有什么区别? Node.js 的 child_process.fork() 不像 POSIX fork(2) 系统调用, 不会拷贝当前父进程. 这里对于其他语言转过的同学可能比较误导, 可以作为一个比较偏的面试题. … Cluster Cluster 是常见的 Node.js 利用多核的办法. 它是基于 child_process.fork() 实现的, 所以 cluster 产生的进程之间是通过 IPC 来通信的, 并且它也没有拷贝父进程的空间 而是通过加入 cluster.isMaster 这个标识, 来区分父进程以及子进程, 达到类似 POSIX 的 fork 的效果. … 如果了解 Node.js 的 IPC 的话, 可以问个比较有意思的问题 在 IPC 通道建立之前, 父进程与子进程是怎么通信的? 如果没有通信, 那 IPC 是怎么建立的? 这个问题也挺简单, 只是个思路的问题. 在通过 child_process 建立子进程的时候, 是可以指定子进程的 env (环境变量) 的. 所以 Node.js 在启动子进程的时候, 主进程先建立 IPC 频道, 然后将 IPC 频道的 fd (文件描述符 ) 通过环境变量 (NODE_CHANNEL_FD) 的方式传递给子进程, 然后子进程通过 fd 连上 IPC 与父进程建立连接.

我的问题是,据我所知,unix的文件描述符表是进程相关的,每个进程都有一份, 如果Nodejs创建子进程不是使用 POSIX 的方式,那子进程也不会复制父进程的文件描述符表和上下文,那么通过NODE_CHANNEL_FD传递一个数字给子进程,子进程该怎么建立连接????

我的想法是 可能一: child_process.fork() 是调用了 POSIX 的fork的,那就说的通了,文件描述符表示一样的,拿到的FD指向文件表项也就相同了 可能二: NODE_CHANNEL_FD传递的不是常规的FD(数字),因为据我所知,IPC无论是靠命名管道(Window),还是unix domain socket(UNIX) S,都使用了文件系统里的一个文件路径,那如果这个 NODE_CHANNEL_FD里存的是个路径,那也说的通了,但是感觉这个可能性很小

8 回复

Unix上还有域套接字方案呐。。。

Unix Domain Socket

这才是Unix上专门拿来做IPC的,不能做网络通信

@hdumok 刚才又翻了下代码,child_process 模块的 fork 调用的 libuv 下的 uv_spawn,这个函数里在 unix 下使用的就是系统 fork() 函数,应该是 “写时复制” 的类型吧,所以应该在 fork() 时确实是完全共享(不是复制)了父进程的数据和状态的

但是有意思的是,我发现NODE_CHANNEL_FD 传递的并不是你提到的在主进程中 uv_spawn 调用系统 socketpair 生成的两个 Pipe 的 fd,实际上是 stdio 数组中 ‘ipc’ 值的索引而已,但是在 fork() 后在子进程中用 dup2() 做了一个文件描述符的重定向:

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 == -1) {
          uv__write_int(error_fd, -errno);
          _exit(127);
        }
      }
    }

    if (fd == use_fd)
      uv__cloexec(use_fd, 0);
    else{
	 //这里把主进程中打开的 Pipe 管道 fd 重定向到了 stdio 数组中 ‘ipc’ 值的索引
      fd = dup2(use_fd, fd);
    }

找这段代码确实花了一点心思,一开始我就奇怪 NODE_CHANNEL_FD 传递一个 ipc 值的索引是干撒。。。

@hyj1991 这段代码里并没有出现NODE_CHANNEL_FD啊 能否指点一下,NODE_CHANNEL_FD在这段代码里的影响?? 这段代码给我的感觉是,父子进程至少传递了3组管道,如果pipes里少于3个有效数组,use_fd = open("/dev/null", fd == 0 ? O_RDONLY : O_RDWR);也强行用/dev/null补上了,这三组管道是否否对应stdin stdout stderr? if (fd == use_fd) uv__cloexec(use_fd, 0); else{ fd = dup2(use_fd, fd); } 这段代码给我的感觉是,如果pipes[0][1] 存的是0(STDIN) 就设置它不自动关闭,以此类推pipes[1][1]是 STDOUT ,pipes[2][1] 是STDERR, 如果use_fd不是这三个值就设置子进程的fd共享该user_fd的文件表项,即拷贝文件描述符,也就是说,子进程把这三组管道留给自己的那端对接到自己的输入输出错误输出上了。这样理解是否正确,但整个过程都没看到NODE_CHANNEL_FD的踪影,能否指点下NODE_CHANNEL_FD在这个过程中的作用?因为我觉得父进程用pipes[fd][0],子进程用pipes[fd][1]这样一设定管道问题就ok了,不需要不使用其他值啊?

@hyj1991 还有个问题想请教一下,就是关于多进程时,主进程分发socket的问题,之前看朴灵的书,还有其他博客,都讲到主进程接受连接,将socket分发给工作进程重建连接,这个分发的message里有文件描述符,但问题是,父进程在创建子进程之前的文件描述符表的那部分是共享给子进程了,但是,后面开始监听后,父进程接受到的socket占用的fd都是新的,子进程没有继承,完全是父进程自己内存中的东西,怎样通过分发一个 fd数字就让子进程建立连接呢?

@hdumok 仔细理解下 fd = dup2(use_fd, fd); 这段代码,fd就是索引值,即env传递的那个值,这里用dup2做了重定向

来自酷炫的 CNodeMD

@hyj1991 但看这段代码里fd是取自循环索引值

@hdumok 你要结合lib/process.js,lib/child_process.js 以及 lib/internal/child_process.js 这三个js源码文件一起看,大致说下吧:

  • child_process.fork 会在 spawn 入参的 options 属性中增加一个 stdio 属性:[0, 1, 2, 'ipc']
  • child_process.spawn 最终调用 lib/internal/child_process.js 中 ChildProcess 类的 spawn 原型方法:里面会给fork出来的子进程设置env:options.envPairs.push('NODE_CHANNEL_FD=' + ipcFd)注意:这里的 ipcFd,并不是 socketpair 生成的真正的管道的fd,而是上面的 stdio 数组的 ipc 属性的索引,具体可以看下代码
  • 接着通过 process_wrap.cc 调用 libuv 的 uv_spawn,就是之前提到的 dup2 重定向,把 socketpair 生成的真正的管道的描述符重定向到 fd 变量:这个 fd 变量你也看到了取自循环索引,如果你从 uv_spawn 开始跟源码的话就能发现此索引一定等于上面提到的 stdio 数组的 ipc 属性的索引
  • lib/process.js 中定义了一些进程启动时的预处理,里面的 setupChannel 就是检测 当前进程包含 process.env.NODE_CHANNEL_FD 的话,那么调用 lib/child_process.js 的 _forkChild 方法把该 env 传递的 fd 重新绑定到自己的管道上以加入事件循环,那么这里的 fd 可以看到很明显就是重定向过后的了,所以能和主进程进行 ipc 通信

以上部分找到对应源代码并且把流程理清后,就能看到真正的子进程打开父进程建立的 ipc 管道的流程了~

回到顶部