Ruby 调用 DLL 接口,接口里面 C 代码的 recv 函数返回了中断错误

fightForPG · 2020年04月28日 · 最后由 fightForPG 回复于 2020年05月20日 · 2961 次阅读

项目里面调用 DLL 的接口,接口里面的 C 代码建立了与底层驱动程序的 socket 连接,发送一个取数据的请求以后,用 recv 函数等待返回值,在正确的值返回以前,发生了中断,recv 函数返回了 EINTR 的错误 errno(中断之后的 log 显示,正确的驱动结果值返回了,只不过 recv 还没等到,就被别的信号还是线程调度中断了),在项目代码跟系统版本(CentOS7.6)不变的情况下(最开始是 Ruby2.6.5 出的错),用各个 Ruby 版本实验,将罪魁祸首定位,就是下面这个变更导致的:

hijack SIGCHLD handler for internal use 

Use a global SIGCHLD handler to guard all callers of rb_waitpid.
To work safely with multi-threaded programs, we introduce a
VM-wide waitpid_lock to be acquired BEFORE fork/vfork spawns the
process.  This is to be combined with the new ruby_waitpid_locked
function used by mjit.c in a non-Ruby thread.

Ruby-level SIGCHLD handlers registered with Signal.trap(:CHLD)
continues to work as before and there should be no regressions
in any existing use cases.

超级链接(ruby2.6.0-preview2 和 ruby2.6.0preview3 之间的途中开发版)

这里面针对 SIGCHLD 信号的默认处理函数做了修正,原来是默认忽略信号 (原来是 SIGCHLD 信号的 SIG_DFL 信号处理函数,应该是忽略),现在是无论怎样,都会去执行它新增加的一个信号处理函数,这个函数里面其实也没做啥,就是把一个标志 sigchld_hit 设置为 1,然后在 timer_thread(处于等待 GIL 锁的等待线程会唤醒 timer_thread,去给当前持有 GIL 锁的线程置一个中断位,而并不执行中断,具体的可以参照站里这篇文章:超级链接) 每隔 0.1S 去给 GIL 上锁的时候,检查下 sigchld_hit 是否为 1,如果为 1,就将&vm->waiting_pids 或者&vm->waiting_grps 遍历,每个 pid 都做 waitpid 的操作 (no-blocking,没等到对象 pid 进程结束,就返回 0),如果等到其中一个结束了,就把条件变量 cond 解除 blocking 的信号发出去,激发阻塞在条件变量 cond 上而挂起的众多线程,当中的一个进程(这个线程也在&vm->waiting_pids 或&vm->waiting_grps 里)。--->这个过程会每个进程都做,激发动作不会打断这个循环

而阻塞在条件变量 cond 的方式有两个:一个是 mjit.c 里面的 exec_process(for no-Ruby level thread),它调用了 ruby_waitpid_locked(pid...),如果当前 pid 线程并没有结束,里面用 rb_native_cond_wait(w.cond, &vm->waitpid_lock);挂起了当前线程,这个也是这次变更新加进去的 (ruby_waitpid_locked 函数),还有一个就是诸如 Process.wait/Process.waitall/Process.detach/system('') 在调用时候,会调用 waitpid(WNOHANG):非阻塞 waitpid,但是如果 pid<0并且&vm->waiting_pids 里面有等待的 pid 的时候,同样要进入等待条件变量 cond 的解除信号,例如:Process.waitpid(-1,&status,0)。

综上所述,我觉得是隔 0.1S 一执行的 timer thread 里面增加了对 wait_pid 中是否结束的检查,如果结束了,就解除 cond,唤醒一个挂起线程,造成对 DLL 侧等待结果 recv 函数的中断,而增加的这个检查必须是执行新的 SIGCHLD 信号处理才有效,而且,我在 recv 函数处加了如果出现 EINTR 错误,那么就再次执行 recv 的 retry 处理,并且加了 retry 时候的 log,发现有一次,打印出了一连串的 retry log,而且每个 retry 间隔是 0.1S,仿佛更印证了我的猜测,(但是。。。)所以我尝试了在 Ruby 源码里面(signal.c)里面把默认的处理函数改回忽略这个信号,但是程序执行的时候,发生了某些进程无故中断(抑或是长时间的等待?)的现象,后来我一分析,觉得如果我变了处理函数,忽略这个信号,那么阻塞在条件变量 cond,挂起的进程就没有办法唤醒了,应该是不行,那我看到这次变更的说明里面有--->Ruby-level SIGCHLD handlers registered with Signal.trap(:CHLD) continues to work as before and there should be no regressions in any existing use cases.,意思也就是说 Signal.trap(:CHLD) 的动作不会受影响,我就尝试着在 Ruby 端,也就是项目的服务启动时候(尝试全局修改 CHLD 的信号处理函数),或者是在我认为有可能跟中断有关系的子进程在启动之前(尝试只影响当前要启动的子进程的 CHLD 信号处理),去把信号处理函数设置为忽略信号,都没有起作用。还是出 EINTR 的错误。

所以,我的问题是: ①我怎么才能将 CHLD 和 CLD 信号的处理函数在项目的 Ruby 代码里面改成忽略信号(SIG_IGN),也就是下面这个代码应该放在哪儿能起作用?

Signal.trap("CHLD""IGNORE")
Signal.trap("CLD""IGNORE")

②还是我对这个变更的理解还是有问题,代码的流程理解有误,并没有找到 EINTR 发生的根本原因,希望大神们指正~~~~

希望有大神帮小可一忙,再次感谢!!! PS:还有一点忘了记上去了,最后还有一种可能是我并没有正确的用好 Ruby,所以也就没有出来 Ruby 源代码里面处理 SIGCHLD 信号所预期的那个样子,如果是这样,这个问题就变得更困难了,就是这个变更本身是正确的,是我哪些不正确的利用,原来的 Ruby 版本"容错"掉了它,但现在,这个错误暴露了出来~~~如果大神们看完这个变更觉得人家没有问题,也请狠狠地给我指出来,羞愧我一下。。

我猜你调用的这个接口是启动了一个子进程,然后让子进程输出一些东西,然后 recv 这些东西。

然而子进程退出时会发出 SIGCHILD 信号,信号正好在 recv 调用中被父进程收到,然后就 EINTR 了。

建议想办法更改子进程退出的时机,或者父进程不要 wait 子进程,或者把子进程的管理在 Ruby 里做。 又或者,把调用 DLL 的代码写成不依赖 Ruby 的独立可执行文件 (不过我怀疑依然会 EINTR),在 Ruby 里 system 或者 backtick 调用 。

luikore 回复

我这边问题发生的背景是多进程的场景,也就是这个子进程里面 recv 发生中断,应该是别的进程返回 SIGCHILD 信号,然后执行相应处理函数,唤醒了其他阻塞的进程影响的 PS:recv 调用是在 DLL 接口里面,也就是用 C 代码写的,它是 Ruby 这边调用的一个硬件操作,需要等待它返回结果以后再进行处理,没法不 wait 子进程,而 DLL 里面定义了很多接口,要改成 system 或者 backtick 成本太高(每个 DLL 接口调用时候都可能发生这个问题)。

所以问题发生的原因咱们应该达成了一致,现在想要解决的就是怎么让 SIGCHILD 信号返回时被忽略,不唤醒别的阻塞进程,我用了 Signal.trap("CHLD","IGNORE") 来忽略 SIGCHILD 信号,但是并没有好用,所以我看了看「hijack SIGCHLD handler for internal use 」这个变更的代码,感觉 Ruby 端的应该通过这个方式屏蔽掉了,但是没有起作用。是我的用法有问题么(语法?加的位置?)

你一定要 trap CHLD 的话,可以试试在 C 里 trap, 这样可能可以绕过 Ruby 的 hijack ...

另外用 system 成本不一定高的,测测才知道

C 里面 trap 了,结果刻录命令直接。。。了😂 : growisofs[15083]: :-( waipid failed: No child processes

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册