Skip to content

nshlib: Reap the background commands that exited. - #3889

Merged
acassis merged 1 commit into
apache:masterfrom
casaroli:nsh-reap-background
Oct 22, 2026
Merged

acassis merged 1 commit into
apache:masterfrom
casaroli:nsh-reap-background

Conversation

@casaroli

@casaroli casaroli commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

With CONFIG_SCHED_CHILD_STATUS, the kernel keeps the exit status of each child until the parent waits for it. NSH waits only for foreground commands. A background command (cmd &) and each pipeline stage before the last one are also children of NSH, but NSH never waits for them. Each one keeps a child status entry in the kernel heap until NSH exits, and the console NSH does not exit.

nsh_builtin() tried to prevent this with SA_NOCLDWAIT around the start of a background builtin. This does not work: SA_NOCLDWAIT applies to the whole process, sigaction() with it also discards the status of all other children, and the kernel does not clear the flag when the old action comes back. So after the first background builtin, NSH keeps no child status at all, and wait can fail with ECHILD. The code also needed CONFIG_ENABLE_ALL_SIGNALS, and nsh_fileapp() (all programs of a kernel build) had no such code.

This PR removes the SA_NOCLDWAIT code. Before it runs a command, nsh_parse_command() now reaps all children that exited, with waitpid(-1, WNOHANG). NSH waits for a foreground command before it starts the next command, so a child that exited at this point is one that no other code waits for. The commit message explains why a SIGCHLD handler or a list of background pids was not used.

Impact

  • Only configurations with CONFIG_SCHED_CHILD_STATUS and CONFIG_SCHED_WAITPID. Others do not change.
  • NSH no longer keeps one kernel heap entry for each background command or pipeline stage.
  • With CONFIG_ENABLE_ALL_SIGNALS, a background builtin no longer turns off the child status of NSH. wait for a background command works again in this case.
  • wait still gets the exit status of a background command that runs. As before, wait for a command that already exited reports an error.
  • No documentation change: the NSH documentation does not describe this behavior.

Testing

qemu-armv8a:knsh (kernel build) and qemu-armv8a:nsh (flat build) under HVF, with CONFIG_SCHED_CHILD_STATUS=y (the default in kernel builds with apache/nuttx#20837; qemu-armv8a:nsh already sets it) and CONFIG_PIPES=y. In the console NSH, a script runs a short program in the background 500 times; this is done 3 times. Then a script runs 100 two-stage pipelines; this is done 2 times. free after each round:

Step knsh, apps master: Kmem used / nused knsh, this PR nsh, apps master: Umem used / nused nsh, this PR
boot 78552 / 57 78552 / 57 79848 / 70 79848 / 70
500 background jobs 96984 / 542 78608 / 58 79848 / 70 79848 / 70
1000 background jobs 116248 / 1042 78608 / 58 79848 / 70 79848 / 70
1500 background jobs 136248 / 1542 78608 / 58 79848 / 70 79848 / 70
100 pipelines 140288 / 1647 78840 / 61 80024 / 72 80024 / 72
200 pipelines 144320 / 1748 78840 / 61 80024 / 72 80024 / 72

The flat build on master does not grow because of SA_NOCLDWAIT, but there the second wait fails: nsh: wait: wait failed: 10.

Both builds, with this PR:

  • A foreground command keeps its exit status: bgjob 7 (knsh) and sh -c "exit 7" (nsh) give $? = 7.
  • wait $! for a background command that sleeps 1 s and exits with 0 gives $? = 0, and with exit code 5 gives $? = 1, with no error.
  • ostest passes (Exiting with status 0).

tools/checkpatch.sh -c -u -m -g passes.

With CONFIG_SCHED_CHILD_STATUS, the kernel keeps the exit status of
each child until the parent waits for it.  NSH waits only for a
foreground command.  A background command ("cmd &") and each pipeline
stage before the last one are also children of NSH, but NSH never waits
for them.  Each of them keeps a status entry in the kernel heap until
NSH exits, and the console NSH does not exit.

nsh_builtin() tried to prevent this for builtin applications.  It set
SA_NOCLDWAIT for SIGCHLD before it started a background command, and
restored the old action after.  This does not work:

- SA_NOCLDWAIT applies to the whole process.  sigaction() with
  SA_NOCLDWAIT also discards the status of all children that NSH did
  not reap yet, and NuttX does not clear the flag when the old action
  comes back.  After the first background builtin, NSH keeps no child
  status at all, also not for its foreground commands.
- The code was only compiled with CONFIG_ENABLE_ALL_SIGNALS.
- nsh_fileapp() had no such code, so every background program of a
  kernel build kept its status.

Remove that code.  Instead, nsh_parse_command() reaps all children that
exited, with waitpid(-1, WNOHANG), before it runs a command.  NSH runs
one command at a time and waits for a foreground command before it
starts the next one.  Thus a child that exited at this point is one
that no other code waits for.  The "wait" command still gets the status
of a background command that runs, because it calls waitpid() for that
pid.

Two other solutions were not used.  A SIGCHLD handler that calls
waitpid(-1) needs CONFIG_ENABLE_ALL_SIGNALS, and it can take the
status of a foreground command before NSH waits for it.  A list of the
background pids needs memory for each job, and it does not reap a
foreground command when its waitpid() fails, for example with EINTR.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
@acassis
acassis merged commit e0174bf into apache:master Oct 22, 2026
62 of 82 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants