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

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

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

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

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

如果都换成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;
}

run

查看了一下进程状态,父进程处于Dl+状态,子进程处于S+状态。待研究。
ps

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Ruby 异常处理的语法结构为 `begin..rescue..end` 或者 `begin..rescue..else..ensure..end`。 在 `begin` 后面的代码块,我们需要执行可能会抛出异常的代码。如果代码块异常抛出,那么就会跳转到 `rescue` 后面的代码块,进行异常处理。如果没有异常抛出,那么就会跳过 `rescue` 块,执行 `else` 块的代码,最后执行 `ensure` 块的代码。 下面是一个 `begin..rescue..end` 的例子: ```ruby begin x = 1 / 0 rescue ZeroDivisionError => e puts "Error: #{e.message}" end ``` 上述代码,我们执行了一个除以零的操作,会抛出 `ZeroDivisionError` 异常。在 `rescue` 块,我们可以使用 `=>` 符号将异常对象赋值给一个变量,便于对异常进行处理。在这个例子,我们将异常对象赋值给了变量 `e`,并打印出了异常信息。 下面是一个 `begin..rescue..else..ensure..end` 的例子: ```ruby begin file = File.open('data.txt') data = file.read rescue Errno::ENOENT => e puts "Error: #{e.message}" else puts "Data: #{data}" ensure file.close if file end ``` 上述代码,我们尝试打开一个名为 `data.txt` 的文件,并读取其的数据。如果文件不存在,就会抛出 `Errno::ENOENT` 异常,在 `rescue` 块进行异常处理。如果文件存在,就会执行 `else` 块的代码,并输出文件的数据。最后,无论有没有异常抛出,都会执行 `ensure` 块的代码,关闭文件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值