转载请注明出处:https://blog.csdn.net/impingo
我的开源项目地址:https://github.com/pingostack/pingos
开源项目:https://pingos.io
整体设计
如上图,Mediasoup项目使用nodejs作为业务层,C++编写的Worker作为WebRTC的任务进程。nodejs和Worker之间使用UnixSocket作为进程间通讯的手段。
Worker问题分析
刚开始接触到Mediasoup V3的时候,Mediasoup中有段代码让我感到疑惑:
Worker进程将描述符3和描述符4作为消息读取和写入的目标,但是Worker进程由主进程创建,主进程为了与Worker进程通信必然要为每个Worker进程提前创建UnixSocket,而Worker进程只需继承自己的UnixSocket描述符即可。
主进程创建的描述符必然是依次增加的,例如:0、1、2分别代表标准输入(stdin),标准输出(stdout),错误输出(stderr),创建新的描述符时就会从3开始向后累加,子进程越多创建的UnixSocket也就越多,描述符的值会越来越大。
每个Worker进程理应继承主进程的描述符,它是如何做到让每个Worker进程都将3、4作为读写描述符的呢?
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。
总结
libuv使用dup2函数将子进程集成进来的UnixSocket描述符做了一层重定向。
如果想要更加深入地理解libuv和Mediasoup的进程机制,建议详细研究一下libuv创建进程和pipe的流程。