• Uncategorized

About linux : Bash-swallowing-sub-shell-children-process-when-executing-a-single-command

Question Detail

Bumped into an unexpected bash/sh behavior and I wonder someone can explain the rationale behind it, and provide a solution to the question below.

In an interactive bash shell session, I execute:

$ bash -c 'sleep 10 && echo'

With ps on Linux it looks like this:

\_ -bash
\_ bash -c sleep 10 && echo
\_ sleep 10

The process tree is what I would expect:

  • My interactive bash shell process ($)
  • A children shell process (bash -c ...)
  • a sleep children process

However, if the command portion of my bash -c is a single command, e.g.:

$ bash -c 'sleep 10'

Then the middle sub-shell is swallowed, and my interactive terminal session executes sleep “directly” as children process.
The process tree looks like this:

\_ -bash
\_ sleep 10

So from process tree perspective, these two produce the same result:

  • $ bash -c 'sleep 10'
  • $ sleep 10

What is going on here?

Now to my question: is there a way to force the intermediate shell, regardless of the complexity of the expression passed to bash -c ...?

(I could append something like ; echo; to my actual command and that “works”, but I’d rather not. Is there a more proper way to force the intermediate process into existence?)

(edit: typo in ps output; removed sh tag as suggested in comments; one more typo)

Question Answer

There’s actually a comment in the bash source that describes much of the rationale for this feature:

/* If this is a simple command, tell execute_disk_command that it
   might be able to get away without forking and simply exec.
   This means things like ( sleep 10 ) will only cause one fork.
   If we're timing the command or inverting its return value, however,
   we cannot do this optimization. */
if ((user_subshell || user_coproc) && (tcom->type == cm_simple || tcom->type == cm_subshell) &&
    ((tcom->flags & CMD_TIME_PIPELINE) == 0) &&
    ((tcom->flags & CMD_INVERT_RETURN) == 0))
  {
    tcom->flags |= CMD_NO_FORK;
    if (tcom->type == cm_simple)
      tcom->value.Simple->flags |= CMD_NO_FORK;
  }

In the bash -c '...' case, the CMD_NO_FORK flag is set when determined by the should_suppress_fork function in builtins/evalstring.c.

It is always to your benefit to let the shell do this. It only happens when:

  • Input is from a hardcoded string, and the shell is at the last command in that string.
  • There are no further commands, traps, hooks, etc. to be run after the command is complete.
  • The exit status does not need to be inverted or otherwise modified.
  • No redirections need to be backed out.

This saves memory, causes the startup time of the process to be slightly faster (since it doesn’t need to be forked), and ensures that signals delivered to your PID go direct to the process you’re running, making it possible for the parent of sh -c 'sleep 10' to determine exactly which signal killed sleep, should it in fact be killed by a signal.

However, if for some reason you want to inhibit it, you need but set a trap — any trap will do:

# run the noop command (:) at exit
bash -c 'trap : EXIT; sleep 10'

You may also like...

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.