Repository navigation
Report a child's state without collecting it (WNOWAIT) in WAITPID - #38
Merged
Merged
Conversation
A WAITPID completion now says that a child is waitable and what its status is; the child stays so until the caller collects it with waitpid(WNOHANG) for the reported pid and the options it waited with. Until then every wait that covers the child reports it again, with the same status, so two waits on it both complete and a caller whose collect finds it gone (-ECHILD: another waiter took it) resubmits. A completion nobody acts on, a cancelled or timed out op, and ior_queue_exit() leave the child as it is, and no pid is freed for reuse until the caller collects it, so a consumer needs no cache of collected statuses keyed by pid, which pid reuse would make wrong, not just large. io_uring submits IORING_OP_WAITID with WNOWAIT. Everything else that asked waitpid(WNOHANG), the pidfd fallback on kernels before 6.7 and the timer probe on io_uring, the worker's first pass, the pidfd and kqueue wake and the timer probe on the thread backend, goes through one shared probe, ior_waitpid_probe() in ior_worker_pool.c: waitid(WEXITED | WNOHANG | WNOWAIT) with waitpid's pid and options mapped to waitid's, and the waitpid(2) status rebuilt from siginfo, which moves there from ior_uring.c. A stop or continue report stays pending the same way until a wait with WUNTRACED or WCONTINUED collects it. Windows never collected anything and is unchanged. test_waitpid collects each reported child itself and checks it was still waitable with the reported status; a stop report is collected with WUNTRACED, a continue with WCONTINUED. Two new cases: a WNOHANG wait, a pending wait and an any-child wait all report the same exited child, then -ECHILD once it is collected, and two waits pending on one live child both complete. test_fork collects its child after the report.
macOS's waitid reports a stopped child whatever the options ask: its kernel tests WSTOPPED with the pre-UNIX03 value 0177, which every option bit overlaps, so WEXITED alone matches. Through ior_waitpid_probe() a WAITPID op without WUNTRACED on a stopped child completed with a stop status the caller could not collect (waitpid(WNOHANG) returns 0), and a wait for any child or a group reported a stopped child in place of an exited sibling behind it on the kernel's list, forever, since WNOWAIT never consumes the stop. main used waitpid(WNOHANG), which honours WUNTRACED, and was not affected. The probe now drops a stop without WUNTRACED and a continue without WCONTINUED, as waitpid would. On macOS, when an any-child or group probe runs into such a stop, the children the wait covers are probed one by one: proc_listchildpids() for any child, sysctl(KERN_PROC_PGRP) filtered to this process's children for a group; both list zombies, which getpgid() and proc_pidinfo() do not see. The common path stays a single waitid call. test_waitpid_any_stopped: an older child exits while a newer one in its group is stopped; a plain wait on the stopped child stays pending, and a wait for any child and one for the group both report the exited sibling. Killing the stopped child completes the pending wait.
The test read status[0] to show the wait on the stopped child had written nothing while the op was still in flight; the worker writes it once the child is killed, and nothing orders the test's read before that write (kill() is no synchronization), so TSan reported the race on the Linux threads job. The -ETIME wait already shows the op has not completed, so the read goes.
bukka
added a commit
that referenced
this pull request
Sep 30, 2026
A WAITPID completion now says that a child is waitable and what its status is; ior never collects the child. It stays waitable until the caller collects it with waitpid(WNOHANG) for the reported pid and the options it waited with. Until then every wait that covers the child reports it again with the same status, so two waits on it both complete, and a caller whose collect finds it gone (another waiter took it) resubmits. A completion nobody acts on, a cancelled or timed out op, and ior_queue_exit() leave the child as it is, and no pid is freed for reuse until the caller collects it, so a consumer needs no cache of collected statuses keyed by pid. io_uring submits IORING_OP_WAITID with WNOWAIT. Everything else that asked waitpid(WNOHANG), the pidfd fallback before 6.7, the timer probes on both backends, the worker's first pass, the pidfd and kqueue wakes, goes through one shared probe, ior_waitpid_probe(): waitid(WEXITED | WNOHANG | WNOWAIT) with waitpid's pid and options mapped to waitid's, and the waitpid status rebuilt from siginfo. A stop or continue report stays pending the same way until a wait with WUNTRACED or WCONTINUED collects it. Windows never collected anything and is unchanged. macOS's waitid reports a stopped child whatever the options ask, because the kernel tests WSTOPPED with the pre-UNIX03 value 0177, which every option bit overlaps. Left alone, a plain wait on a stopped child completed with a stop status the caller could not collect, and an any-child or group wait reported a stopped child in place of an exited sibling behind it on the kernel's list, forever. The probe now drops a stop without WUNTRACED and a continue without WCONTINUED, and on macOS an any-child or group probe that runs into such a stop probes the covered children one by one (proc_listchildpids(), or sysctl(KERN_PROC_PGRP) filtered to this process's children; both list zombies). test_waitpid collects each reported child itself and checks it was still waitable with the reported status. New cases: an exited child reported by a WNOHANG wait, a pending wait and an any-child wait, then -ECHILD once collected; two waits pending on one live child both complete; a stopped child stays pending for a plain wait and does not hide an exited sibling from an any-child or group wait. test_fork collects its child after the report. Verified on macOS 15.7: the full suite passes, and the stopped-child test fails without the macOS probe.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A WAITPID completion now says that a child is waitable and what its status is; ior never collects the child. It stays waitable until the caller collects it with
waitpid(WNOHANG)for the reported pid and the options it waited with. Until then every wait that covers the child reports it again with the same status, so two waits on it both complete, and a caller whose collect finds it gone (another waiter took it) resubmits. A completion nobody acts on, a cancelled or timed out op, andior_queue_exit()leave the child as it is, and no pid is freed for reuse until the caller collects it, so a consumer needs no cache of collected statuses keyed by pid.How
IORING_OP_WAITIDwithWNOWAIT.waitpid(WNOHANG)(the pidfd fallback before 6.7, the timer probes on both backends, the worker's first pass, the pidfd and kqueue wakes) goes through one shared probe,ior_waitpid_probe():waitid(WEXITED | WNOHANG | WNOWAIT)with waitpid's pid and options mapped to waitid's, and the waitpid status rebuilt fromsiginfo. A stop or continue report stays pending the same way until a wait withWUNTRACEDorWCONTINUEDcollects it.macOS (second commit): its
waitidreports a stopped child whatever the options ask, because the kernel testsWSTOPPEDwith the pre-UNIX03 value 0177, which every option bit overlaps. Left alone, a plain wait on a stopped child completed with a stop status the caller could not collect, and an any-child or group wait reported a stopped child in place of an exited sibling behind it on the kernel's list, forever. The probe now drops a stop withoutWUNTRACEDand a continue withoutWCONTINUED, and on macOS an any-child or group probe that runs into such a stop probes the covered children one by one (proc_listchildpids, orsysctl(KERN_PROC_PGRP)filtered to this process's children; both list zombies).Tests
test_waitpidcollects each reported child itself and checks it was still waitable with the reported status. New cases: an exited child reported by a WNOHANG wait, a pending wait and an any-child wait, then-ECHILDonce collected; two waits pending on one live child both complete; a stopped child stays pending for a plain wait and does not hide an exited sibling from an any-child or group wait.test_forkcollects its child after the report.