Page MenuHomeFreeBSD

pwait: Optionally wait until process is reaped
ClosedPublic

Authored by des on Fri, Jul 17, 8:10 PM.
Tags
None
Referenced Files
F164172105: D58314.id182669.diff
Wed, Jul 29, 8:30 AM
F164158295: D58314.id182545.diff
Wed, Jul 29, 5:58 AM
Unknown Object (File)
Tue, Jul 28, 10:32 AM
Unknown Object (File)
Tue, Jul 28, 8:20 AM
Unknown Object (File)
Tue, Jul 28, 5:20 AM
Unknown Object (File)
Tue, Jul 28, 5:17 AM
Unknown Object (File)
Mon, Jul 27, 11:33 PM
Unknown Object (File)
Mon, Jul 27, 11:12 PM

Details

Summary

If the new -r option is specified, wait until the target process not
only terminates but is reaped.

MFC after: 1 week
Sponsored by: Klara, Inc.
Sponsored by: NetApp, Inc.

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Skipped
Unit
Tests Skipped
Build Status
Buildable 74994
Build 71877: arc lint + arc unit

Event Timeline

des held this revision as a draft.

You commented in IRC that you didn't believe the NOTE_REAP was being delivered. Here's what I did:

I deployed these changes and then changed the rc.subr wait_for_pids to add -v to pwait and I can see the "collected" message in the output now

root@current:/usr/src/bin/pwait # service gitea restart
Stopping gitea.
Waiting for PIDS: 72212, 72212: exited with status 0. 72212: collected..

sometimes I'm seeing two NOTE_EXIT events come through like this:

# service gitea restart
Stopping gitea.
Waiting for PIDS: 71485, 71485: exited with status 0. 71485: exited with status 0. 71485: collected..

That is probably from the immediate delivery of the NOTE_EXIT because the processed was flagged as exiting, but the other delivery of NOTE_EXIT in kern_exit.c didn't happen yet.

Moving the discussion from a mail, there:

On Sun, Jul 19, 2026 at 12:48:35PM +0200, Dag-Erling Smørgrav wrote:

A while ago, I tried adding a NOTE_REAP which triggers when a process is
reaped. With debug logging added, I can see that the event gets
generated at the correct point, but for reasons I was unable to figure
out at the time, it never gets delivered.

Perhaps because knotes are dropped on the NOTE_EXIT:

if (event == NOTE_EXIT) {
        kn->kn_flags |= EV_EOF | EV_ONESHOT;

and EV_ONESHOT causes the knote_drop() in kqueue_scan() on reporting.

The patch is here: https://reviews.freebsd.org/D58313

A usage example is here: https://reviews.freebsd.org/D58314

I would really like to get this sorted out before 14.5, because
rc.subr's wait_for_pids() is currently broken in all branches.

Can you explain in which sense it is broken, and what is the intent of the NOTE_REAP?

There are more operations to clear the process, besides reaping. For instance, freeing of the PID might occur after reaping.
There is a planned change that (IMO significantly) modifies how the reaping is handled, see D58264.
I suspect it could interact with what you are doing there.

In D58314#1338061, @kib wrote:

Can you explain in which sense it is broken, and what is the intent of the NOTE_REAP?

pwait currently subscribes to NOTE_EXIT and gets the event too early, so when rc.subr is starting up a service you restarted it checks if the process is already running and finds it in the process table and throws an error. For many services it is not possible to "service foo restart" successfully on FreeBSD 14.4-RELEASE or 15.1-RELEASE. If you try to restart services as fast as possible it's quite easy to hit the race condition and see the error. A few people have noticed that software written in Go is unusually susceptible to this problem.

In D58314#1338061, @kib wrote:

A while ago, I tried adding a NOTE_REAP which triggers when a process is
reaped. With debug logging added, I can see that the event gets
generated at the correct point, but for reasons I was unable to figure
out at the time, it never gets delivered.

Perhaps because knotes are dropped on the NOTE_EXIT:

if (event == NOTE_EXIT) {
        kn->kn_flags |= EV_EOF | EV_ONESHOT;

and EV_ONESHOT causes the knote_drop() in kqueue_scan() on reporting.

No. Please look at the actual patch (D58313) instead of speculating:

The patch is here: https://reviews.freebsd.org/D58313

A usage example is here: https://reviews.freebsd.org/D58314

and again, I know the event gets generated, I instrumented kqueue_scan(), but it's never delivered to the process.

des published this revision for review.Tue, Jul 21, 7:51 PM
des added reviewers: kib, markj.

taking out of draft since I don't get notifications for replies to drafts

This obviously changes the semantic of the pwait(1) tool. Before, it returned control after the process exited. Now, it additionally wait until a reaper makes the wait*() call.

It might be that the new behavior is fine if directed by an option, otherwise I do not think that this is acceptable.

In D58314#1339356, @kib wrote:

This obviously changes the semantic of the pwait(1) tool. Before, it returned control after the process exited. Now, it additionally wait until a reaper makes the wait*() call.

But what does "exited" really mean in this context?

What users are seeing is something like "service gitea restart" fails to start the process again. pwait believed that the process exited (NOTE_EXIT received), but in reality it's still in the process table with the ps state column = REJ (runnable, trying to exit).

Do we know what could have changed in the kernel that surfaced this problem in the first place? I don't remember encountering this with 14.3-RELEASE and older, but it's easily reproduced with 14.4 and 15.0+.

It looks like the process is still exiting, but has not successfully exited.

Thanks for the additional eyes on this.

In D58314#1339356, @kib wrote:

This obviously changes the semantic of the pwait(1) tool. Before, it returned control after the process exited. Now, it additionally wait until a reaper makes the wait*() call.

But what does "exited" really mean in this context?

Exited means that the process developed an exit status, available for retrieval by several means. Additional guarantee is that no process thread is ever going to execute anything in user space.

What users are seeing is something like "service gitea restart" fails to start the process again. pwait believed that the process exited (NOTE_EXIT received), but in reality it's still in the process table with the ps state column = REJ (runnable, trying to exit).

Do we know what could have changed in the kernel that surfaced this problem in the first place? I don't remember encountering this with 14.3-RELEASE and older, but it's easily reproduced with 14.4 and 15.0+.

Nothing changed.

It looks like the process is still exiting, but has not successfully exited.

Thanks for the additional eyes on this.

Do we know what could have changed in the kernel that surfaced this problem in the first place? I don't remember encountering this with 14.3-RELEASE and older, but it's easily reproduced with 14.4 and 15.0+.

Nothing changed in the kernel. I started out fixing bugs in /etc/rc.subr's wait_for_pids(), then tried to make it more efficient, and ended up making it _too_ efficient, to the point were services commonly try to restart after the previous instance has exited but before the process has been reaped. Most daemons either clear their pidfile on exit or don't care, but some don't clear the pidfile on exit and check the pid that it contains on startup, and refuse to start because you can still kill -0 the zombie.

In D58314#1339356, @kib wrote:

It might be that the new behavior is fine if directed by an option, otherwise I do not think that this is acceptable.

Which option character would you suggest? I started out with -z (for “zombie”), but perhaps -r (for “reaped”) is more appropriate?

des retitled this revision from pwait: Don't exit until process is reaped to pwait: Optionally wait until process is reaped.Wed, Jul 22, 8:16 AM
des edited the summary of this revision. (Show Details)
des edited the summary of this revision. (Show Details)

make the new behavior optional

In D58314#1339370, @des wrote:

Nothing changed in the kernel. I started out fixing bugs in /etc/rc.subr's wait_for_pids(), then tried to make it more efficient, and ended up making it _too_ efficient

Thank you, now it all makes sense.

kib added inline comments.
bin/pwait/pwait.c
169

If rflag && !verbose, then NOTE_EXIT is not needed.

This revision is now accepted and ready to land.Wed, Jul 22, 11:51 PM
bin/pwait/pwait.c
169

Very interesting observation, because you're not wrong, but I just found out that if I only ask for NOTE_REAP, it never gets delivered, but if I ask for NOTE_EXIT | NOTE_REAP I get both as expected, so there's something wrong on the kernel side of things.

bin/pwait/pwait.c
169

Ah no, the problem is with the test methodology. The target processes were started by the same shell that runs pwait, so they can't get reaped until pwait returns.

This revision now requires review to proceed.Thu, Jul 23, 5:46 PM
This revision is now accepted and ready to land.Thu, Jul 23, 11:34 PM
jilles added inline comments.
bin/pwait/pwait.1
56–63

This should come with a warning, like "Since this requires the parent processes of the target processes to do something, it may introduce deadlocks.".

This revision now requires review to proceed.Sat, Jul 25, 12:42 PM
bin/pwait/pwait.1
66

How could a reaper be blocked by pwait(1)?

des marked an inline comment as done.Sun, Jul 26, 9:38 AM
des added inline comments.
bin/pwait/pwait.1
66

In this case, which is a distillation of the wait_for_pids_progress test case before I rewrote it (see D58391), sh blocks on read() from the command substitution and does not reap the sleep process until after pwait terminates:

sh -c 'sleep 1 & a=$(pwait -r $!)'

In this even simpler case, sh sees that pwait is the last command in the script, so it just execs without forking first, and pwait ends up _being_ the reaper of its target process:

sh -c 'sleep 1 & pwait -r $!'
This revision is now accepted and ready to land.Mon, Jul 27, 2:09 AM
This revision was automatically updated to reflect the committed changes.