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

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

项目里面调用 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

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