在他关于self-pipe 技巧的网页上,Dan Bernstein 解释了一个带有和信号的竞争条件,提供了一种解决方法并得出结论:select()
当然,正确的做法是
fork()
返回文件描述符,而不是进程 ID。
他的意思是什么——它是关于能够select()
在子进程上处理它们的状态变化而不是必须使用信号处理程序来获得这些状态变化的通知吗?
在他关于self-pipe 技巧的网页上,Dan Bernstein 解释了一个带有和信号的竞争条件,提供了一种解决方法并得出结论:select()
当然,正确的做法是
fork()
返回文件描述符,而不是进程 ID。
他的意思是什么——它是关于能够select()
在子进程上处理它们的状态变化而不是必须使用信号处理程序来获得这些状态变化的通知吗?
该问题在您的源代码中进行了描述,
select()
应该被类似的信号中断SIGCHLD
,但在某些情况下它不能很好地工作。所以解决方法是将信号写入管道,然后由select()
. 监视文件描述符select()
是为了解决这个问题。解决方法实质上是将信号事件转换为文件描述符事件。如果
fork()
一开始只返回一个 fd,则不需要解决方法,因为该 fd 可能会直接与select()
.所以是的,你在最后一段中的描述对我来说似乎是正确的。
fd(或某种其他类型的内核句柄)比普通进程 ID 号更好的另一个原因是 PID 可以在进程终止后重用。在某些情况下,当向进程发送信号时,这可能是一个问题,可能无法确定该进程是您认为的进程,而不是另一个重用相同 PID 的进程。(虽然我认为在向子进程发送信号时这应该不是问题,因为父进程必须在子进程上运行
wait()
才能释放其 PID。)Bernstein 没有为这个“正确的事情”评论提供太多背景信息,但我会冒险猜测:让 fork(2) 返回 PID 与 open(2)、creat(2) 等返回文件描述符不一致。Unix 系统的其余部分可以使用代表进程的文件描述符而不是 PID 来完成进程操作。存在一个系统调用signalfd(2),它允许信号和文件描述符之间更好的交互,并表明表示进程的文件描述符可以工作。
这只是对“如果 Unix 的设计不同于它的设计,那就太好了”的思考。
PID 的问题在于它们存在于一个全局命名空间中,可以在其中为另一个进程重用它们,如果
fork()
在父进程中返回某种可以保证始终引用子进程的句柄,那就太好了,并且它可以通过继承或 unix 套接字 /SCM_RIGHTS
[1] 传递给其他进程。另请参阅此处的讨论,了解最近在 Linux 中“修复”该问题的努力,包括添加一个标志,
clone()
使其返回 pid-fd 而不是 PID。但即便如此,这也不会消除对自管道 hack [2] 或更好接口的需求,因为通知父进程有关子进程状态的信号并不是您希望在主循环中处理的唯一信号的程序。不幸的是,像
epoll(7) + signalfd(2)
Linux 或kqueue(2)
BSD 这样的东西不是标准的——唯一的标准接口(但在旧系统上不支持)是低劣的pselect(2)
。[1] 防止 PID 在系统调用返回并使用其返回值时被重新循环,
waitpid()
可能可以在较新的系统上通过使用waitid(.., WNOWAIT)
来实现。[2] 我不会评论 DJ Bernstein 声称他发明了它(对不起,apophasis ;-))。
关键是有许多程序基于使用select(2) / poll(2) / epoll(7)监视文件描述符来操作事件循环样式模型。但是有各种事件在历史上没有使用文件描述符通知——其中包括子进程的进程状态转换(例如,终止)。需要单独处理这些其他事件(包括计时器到期、信号和同步事件,例如信号量更改),使事件循环模型的编程变得复杂。
在过去的几年里,Linux 开发一直在解决这个问题,所以现在我们有了signalfd(2)(使信号可以从文件描述符中读取)、eventfd(2)(句柄是文件描述符的同步原语) , 和timerfd_create(2)(创建通过文件描述符通知的计时器),所有这些都会产生可以提供给select(2) / poll(2) / epoll(7)的文件描述符。
最后,最近的 Linux 版本添加了进程句柄作为文件描述符的概念。clone3()的
CLONE_PIDFD
标志可用于创建一个子进程,为其返回一个文件描述符作为句柄。文件描述符同样可以提供给select(2) / poll(2) / epoll(7)并指示子进程是否终止是可读的。