避免fork()/ SIGCHLD竞争条件
请考虑以下fork()
/ SIGCHLD
伪代码。
// main program excerpt for (;;) { if ( is_time_to_make_babies ) { pid = fork(); if (pid == -1) { /* fail */ } else if (pid == 0) { /* child stuff */ print "child started" exit } else { /* parent stuff */ print "parent forked new child ", pid children.add(pid); } } } // SIGCHLD handler sigchld_handler(signo) { while ( (pid = wait(status, WNOHANG)) > 0 ) { print "parent caught SIGCHLD from ", pid children.remove(pid); } }
在上面的例子中,有一个竞争条件。 “ /* child stuff */
”可能在“ /* parent stuff */
”开始之前完成,这可能导致孩子的pid在退出后被添加到子列表中,并且永远不会被删除。 当应用程序关闭的时候,父母将无休止地等待已经完成的孩子完成。
我能想到的一个解决方案就是有两个列表: started_children
和finished_children
。 我会在我现在添加到children
的同一个地方添加到started_children
。 但是在信号处理程序中,我将添加到finished_children
而不是从children
节点中删除。 当应用程序关闭时,父级可以等待,直到started_children
和finished_children
之间的差异为零。
我能想到的另一个可能的解决方案是使用共享内存,例如,共享父项的子项列表,并让子项.add
和.remove
自己? 但我对此并不太了解。
编辑:另一个可能的解决方案,这是我想到的第一件事,就是简单地在/* child stuff */
开始添加一个sleep(1)
/* child stuff */
但这对我来说很有趣,这就是我把它留下来的原因。 我甚至不确定它是100%修复。
那么,你如何纠正这种竞争条件? 如果有一个完善的推荐模式,请告诉我!
谢谢。
最简单的解决方案是使用sigprocmask()
在fork()
之前阻止SIGCHLD信号,并在处理pid之后在父代码中解除阻塞。
如果孩子死亡,在解锁信号后将调用SIGCHLD的信号处理程序。 这是一个关键的部分概念 – 在你的情况下,临界部分在fork()
之前开始并在children.add()
之后结束。
如果你不能使用关键片段,也许一个简单的计数器可以完成这项工作。 添加时为+1,删除后为-1,无首先发生一个,最终完成时最终可以为零。
除现有的“儿童”外,还增加了新的数据结构“早逝”。 这将保持儿童的内容清洁。
// main program excerpt for (;;) { if ( is_time_to_make_babies ) { pid = fork(); if (pid == -1) { /* fail */ } else if (pid == 0) { /* child stuff */ print "child started" exit } else { /* parent stuff */ print "parent forked new child ", pid if (!earlyDeaths.contains(pid)) { children.add(pid); } else { earlyDeaths.remove(pid); } } } } // SIGCHLD handler sigchld_handler(signo) { while ( (pid = wait(status, WNOHANG)) > 0 ) { print "parent caught SIGCHLD from ", pid if (children.contains(pid)) { children.remove(pid); } else { earlyDeaths.add(pid); } } }
编辑:如果你的进程是单线程的,这可以简化 – earlyDeaths不必是一个容器,它只需要保持一个pid。
也许是一个乐观的算法? 尝试children.remove(pid),如果失败,继续生活。
或者在尝试删除之前检查pid是否在儿童中?