项目里面调用 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 版本"容错"掉了它,但现在,这个错误暴露了出来~~~如果大神们看完这个变更觉得人家没有问题,也请狠狠地给我指出来,羞愧我一下。。