等待带有超时的子进程的方式实在太多了

这是一篇关于在 Unix 系统中等待子进程完成的多种方法的文章,主要内容如下:

  • 背景与需求:作者经常需要在终端中以重试循环的方式启动程序,如连接启动中的机器、连接启动中的数据库等,常见的解决办法有timeouteb工具,但它们都不能完全满足需求,因此作者决定实现自己的工具ueb,实现最多 10 次重试,每次重试等待时间翻倍,子进程的超时时间与等待时间相同且自适应。
  • 多种等待子进程的方法

    • 传统的sigsuspend方法timeout工具使用的方法,通过signal(SIGCHLD, on_chld_signal)接收子进程结束的信号,使用alarmsetitimer设置超时信号,然后用sigsuspend等待信号,最后需要wait避免产生僵尸进程,但此方法存在很多问题,如信号处理的复杂性、函数的非信号安全性、信号与其他 Unix 实体的不兼容性等。
    • sigtimedwait方法:可以等待信号并设置超时的系统调用,使用起来相对简单,但posix 2001中该调用是可选的,一些操作系统未实现,且timeout程序未使用该方法,可能是为了支持旧的 Unix 系统或非posix系统。
    • 自管道技巧:通过pipe(2)系统调用将信号世界与文件描述符世界连接起来,在SIGCHLD信号处理函数中向自己的管道写入数据,然后用poll等待管道可读,从而实现超时等待子进程结束,但此方法存在一些问题,如poll不能直接获取子进程的退出状态,超时后需要杀死子进程并读取管道,在复杂程序中可能需要使用ppoll避免数据竞争等。
    • signalfd方法:Linux 系统中的系统调用,通过signalfd获取一个可以poll的文件描述符,从而实现与自管道技巧类似的功能,但作者认为引入一个系统调用来实现此功能有些多余,更希望扩展poll以支持其他实体。
    • 进程描述符方法:近年来引入的概念,类似于标准的文件描述符,通过clone3pdfork创建子进程时返回进程描述符,用poll等待进程描述符超时,用pidfd_send_signalclose杀死子进程,获取子进程的退出状态,此方法避免了信号,实现了隔离和可组合性,但没有跨平台的 API。
    • MacOS 和 BSD 的kqueue方法kqueue可以直接与 PIDs 一起使用,类似于pollepoll,但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工具相比规模小很多。
阅读 11
0 条评论