欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Ruby C Extension 中多进程异常机制

程序员文章站 2024-03-23 15:46:46
...

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 C Extension 中多进程异常机制

一开始我还在思考是不是因为多进程环境下的问题,可能是因为子进程先行抛出异常使ruby runtime终结,导致父进程像孤魂野鬼那样找不到家。

真是瞎tm想。其实只是因为vfork的机制所导致的,因为vfork会阻塞父进程,直到子进程调用execve或者退出。所以上面代码中的rb_raise,恰好不是上述的情况(应该属于控制流的跳转),导致父进程一直在等待子进程,但子进程实际已经因为抛出异常结束了。下图是man-pages
Ruby C Extension 中多进程异常机制

画了一张图来说明上一过程,半圆形块为flowchart中的delay,虽然好像不是这么用的,但是这个右侧进来的箭头就像一个阻塞条件,感觉比较直观。其实多进程中的异常处理和单进程中没有什么不用,假设我们的代码不是直接抛出到顶层而是被包裹在try catch中,那么就是vfork之后的代码都被复制了一份,而上层的ruby运行时也被复制了(虽然man-pages说是共享了内存,但是程序计数器什么的应该仍然是单独的,所以子进程被catch了异常父进程没有退出)。本来应该在prepare之后由execvp来unblock父进程,但是prepare中抛出了异常,直接跳转到了catch部分,导致父进程一直处于等待状态。
Ruby C Extension 中多进程异常机制

如果我们使用普通的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;
}

Ruby C Extension 中多进程异常机制

如果都换成return 1vfork版本仍然会阻塞。

{
    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;
}

Ruby C Extension 中多进程异常机制

查看了一下进程状态,父进程处于Dl+状态,子进程处于S+状态。待研究。
Ruby C Extension 中多进程异常机制

18-09-08 更新追加
在oj-runner的编码过程中为了限制程序的运行时间和内存,需要用到setrlimit()这个系统调用,但是提示RLIMIT_STACK是一个invalid argument,并且RLIMIT_CPU等一些参数虽然可以设置但都没有效果,这是因为在WSL应该是一个定制程度比较高的虚拟化环境,比如说在Ubuntu下的进程都可以通过任务管理器看到,所以并不像VirtualBox虚拟机那样,可能存在有些系统调用没有被实现的情况。参考WSL Release NotesGithub issue,可以看出这部分的系统调用应该是只有部分实现的。

相关标签: Ruby