这是一篇关于在 Unix 系统中等待子进程完成的多种方法的文章,主要内容如下:
- 背景与需求:作者经常需要在终端中以重试循环的方式启动程序,如连接启动中的机器、连接启动中的数据库等,常见的解决办法有
timeout
和eb
工具,但它们都不能完全满足需求,因此作者决定实现自己的工具ueb
,实现最多 10 次重试,每次重试等待时间翻倍,子进程的超时时间与等待时间相同且自适应。 多种等待子进程的方法:
- 传统的
sigsuspend
方法:timeout
工具使用的方法,通过signal(SIGCHLD, on_chld_signal)
接收子进程结束的信号,使用alarm
或setitimer
设置超时信号,然后用sigsuspend
等待信号,最后需要wait
避免产生僵尸进程,但此方法存在很多问题,如信号处理的复杂性、函数的非信号安全性、信号与其他 Unix 实体的不兼容性等。 sigtimedwait
方法:可以等待信号并设置超时的系统调用,使用起来相对简单,但posix 2001
中该调用是可选的,一些操作系统未实现,且timeout
程序未使用该方法,可能是为了支持旧的 Unix 系统或非posix
系统。- 自管道技巧:通过
pipe(2)
系统调用将信号世界与文件描述符世界连接起来,在SIGCHLD
信号处理函数中向自己的管道写入数据,然后用poll
等待管道可读,从而实现超时等待子进程结束,但此方法存在一些问题,如poll
不能直接获取子进程的退出状态,超时后需要杀死子进程并读取管道,在复杂程序中可能需要使用ppoll
避免数据竞争等。 signalfd
方法:Linux 系统中的系统调用,通过signalfd
获取一个可以poll
的文件描述符,从而实现与自管道技巧类似的功能,但作者认为引入一个系统调用来实现此功能有些多余,更希望扩展poll
以支持其他实体。- 进程描述符方法:近年来引入的概念,类似于标准的文件描述符,通过
clone3
或pdfork
创建子进程时返回进程描述符,用poll
等待进程描述符超时,用pidfd_send_signal
或close
杀死子进程,获取子进程的退出状态,此方法避免了信号,实现了隔离和可组合性,但没有跨平台的 API。 - MacOS 和 BSD 的
kqueue
方法:kqueue
可以直接与 PIDs 一起使用,类似于poll
或epoll
,但kqueue
是有状态的,需要在子进程结束或被杀死后删除对其 PID 的监视,libkqueue
库可以在所有主要操作系统上提供kqueue
的兼容性层,但在 Linux 上使用pidfd_open
+poll/epoll
更简单,在 Solaris/illumos 上有自己的port
系统,但不支持 PIDs。 - Linux 的
io_uring
方法:通过io_uring
队列系统调用,将wait
和超时一起排队等待完成,类似于epoll
,使系统调用异步化,liburing
在单元测试中使用了此方法,也可以只排队waitid
并使用io_uring_wait_cqe_timeout
模拟poll
,但io_uring
只支持现代内核,一些云提供商因安全原因禁用它。 - 线程方法:在一个线程中负责启动子进程并等待,在主线程中用
pthread_timedjoin_np
等待线程超时,此方法工作但比较繁琐,需要处理线程与信号的交互和数据竞争等问题。 - 主动轮询方法:在用户代码中循环并微睡眠以主动轮询子进程状态,这是一种不必要的方法,可能导致功耗问题和延迟,仅用于完整性。
- 传统的
- 结论:作者认为信号和创建子进程是 Unix 中最难的部分,不同的方法各有优缺点,在复杂程序中应根据需求选择合适的方法,如需要最大的可移植性可使用
sigsuspend
,不害怕信号可使用sigtimedwait
,在 Linux 和 FreeBSD 上可使用进程描述符,在 MacOS 和 BSDs 上可使用kqueue
(使用libkqueue
在 Linux 上兼容),在 bleeding edge Linux 上可使用io_uring
,在只关心 Linux 且害怕使用io_uring
时可使用signalfd
+poll
。作者还强调了良好的 API 和抽象的重要性,以及操作系统开发者正在努力引入新的更好的抽象,如进程描述符。最后,作者遗憾地指出所有操作系统之间的碎片化问题,希望io_uring
能在未来得到更广泛的应用。 - 代码附录:代码可在这里获取,除
libc
外无其他依赖,所有程序大小在最差情况下为 27 KiB,且不分配内存,与eb
工具相比规模小很多。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。