Ruby C Extension 中多进程异常机制
Ruby C Extension 中多进程异常机制
这个标题名字取得很屌,但是只是我在将Python版本的Lo-runner(一个OJ评测工具)改成Ruby版本中遇到的一些好玩的东西。
问题的背景可以如下代码中给出,子进程中会抛出异常的代码被简化为prepare()
:
{
pid_t pid;
int fd_err[2];
if (pipe2(fd_err, O_NONBLOCK) != -1) {
close(fd_err[0]);
close(fd_err[1]);
raise(...);
}
pid = vfork();
if (pid < 0)
rb_raise(...);
else if (pid == 0) {
close(fd_err[0]);
if (prepare() == -1) {
int r = write(fd_err[1], err, strlen(err)); // err为一个char *字符串
_exit(r);
}
if (execvp(runobj->cmd[0], runobj->cmd) == -1) {
int r = write(fd_err[1], err, strlen(err));
_exit(r);
}
}
else {
int r;
close(fd_err[1]);
char buffer[100] = {0};
r = read(fd_err[0], buffer, 90);
if (r > 0) {
waitpid(pid, NULL, WNOHANG);
rb_raise(...);
}
close(fd_err[0]);
return 1;
}
}
这段代码的作用是开一个子进程,使用execvp
来运行评测,在prepare
过程中产生的异常,都通过一个pipe
传输到父进程,再由父进程抛给ruby runtime。所以,为什么不能由子进程来抛出异常?
所以比如我们将代码修改为如下,并且将返回的1作为true打印:
{
pid_t pid = vfork();
if (pid < 0)
rb_raise(...);
else if (pid == 0)
rb_raise(rb_eRuntimeError, "child raising");
else
return 1;
}
运行的结果如下图中第一次ruby raise.rb
,可以看到在报了Runtime Error之后,程序并没有自动退出,而是陷入等待,只能通过Ctrl C
终结了它。
一开始我还在思考是不是因为多进程环境下的问题,可能是因为子进程先行抛出异常使ruby runtime终结,导致父进程像孤魂野鬼那样找不到家。
真是瞎tm想。其实只是因为vfork
的机制所导致的,因为vfork
会阻塞父进程,直到子进程调用execve
或者退出。所以上面代码中的rb_raise
,恰好不是上述的情况(应该属于控制流的跳转),导致父进程一直在等待子进程,但子进程实际已经因为抛出异常结束了。下图是man-pages。
画了一张图来说明上一过程,半圆形块为flowchart中的delay,虽然好像不是这么用的,但是这个右侧进来的箭头就像一个阻塞条件,感觉比较直观。其实多进程中的异常处理和单进程中没有什么不用,假设我们的代码不是直接抛出到顶层而是被包裹在try catch中,那么就是vfork
之后的代码都被复制了一份,而上层的ruby运行时也被复制了(虽然man-pages说是共享了内存,但是程序计数器什么的应该仍然是单独的,所以子进程被catch了异常父进程没有退出)。本来应该在prepare
之后由execvp
来unblock父进程,但是prepare
中抛出了异常,直接跳转到了catch部分,导致父进程一直处于等待状态。
如果我们使用普通的fork()
函数,则不会因为阻塞原因出现上述情况,如下图的第二次ruby raise.rb
所示。
{
pid_t pid = fork();
if (pid < 0)
rb_raise(...);
else if (pid == 0)
rb_raise(rb_eRuntimeError, "child raising");
else
return 1;
}
如果都换成return 1
,vfork
版本仍然会阻塞。
{
pid_t pid = fork();
if (pid < 0)
rb_raise(...);
else if (pid == 0)
return 1;
else
return 1;
}
{
pid_t pid = vfork();
if (pid < 0)
rb_raise(...);
else if (pid == 0)
return 1;
else
return 1;
}
查看了一下进程状态,父进程处于Dl+
状态,子进程处于S+
状态。待研究。
18-09-08 更新追加
在oj-runner的编码过程中为了限制程序的运行时间和内存,需要用到setrlimit()
这个系统调用,但是提示RLIMIT_STACK
是一个invalid argument
,并且RLIMIT_CPU
等一些参数虽然可以设置但都没有效果,这是因为在WSL应该是一个定制程度比较高的虚拟化环境,比如说在Ubuntu下的进程都可以通过任务管理器看到,所以并不像VirtualBox虚拟机那样,可能存在有些系统调用没有被实现的情况。参考WSL Release Notes和Github issue,可以看出这部分的系统调用应该是只有部分实现的。