6

When I do:

/bin/bash -c 'cat /proc/$$/cmdline'

The output I get is:

cat/proc/25050/cmdline

Whereas the output I expected was:

/bin/bash -c 'cat /proc/$$/cmdline'

On the other hand when I do:

/bin/bash -c 'echo $$; cat /proc/$$/cmdline'

I get the expected output, which is:

28259
/bin/bash-cecho $$; cat /proc/$$/cmdline

It seems like $$ is cat's pid rather than bash/sh's pid.
Why is this?
Does the shell do some kind of parsing and execve() style replace? If so, how does it know cat's PID before it even does the replace?

1 Answer 1

4

In order to understand this behaviour, one has to figure how bash executes commands passed to it on the command line. The key point is that if the command is simple enough, there's no fork (or clone or anything like that).

$ strace -f -e clone,execve /bin/bash -c 'cat /proc/$$/cmdline'
execve("/bin/bash", ["/bin/bash", "-c", "cat /proc/$$/cmdline"], [/* 80 vars */]) = 0
execve("/bin/cat", ["cat", "/proc/2942/cmdline"], [/* 80 vars */]) = 0
cat/proc/2942/cmdline+++ exited with 0 +++
$

OTOH if the command is more complicated, bash forks:

$ strace -f -e clone,execve /bin/bash -c 'echo $$; cat /proc/$$/cmdline'
execve("/bin/bash", ["/bin/bash", "-c", "echo $$; cat /proc/$$/cmdline"], [/* 80 vars */]) = 0
2933
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7ff64e6779d0) = 2934
Process 2934 attached
[pid  2934] execve("/bin/cat", ["cat", "/proc/2933/cmdline"], [/* 80 vars */]) = 0
/bin/bash-cecho $$; cat /proc/$$/cmdline[pid  2934] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2934, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++
$

It seems like $$ is cat's pid rather than bash/sh's pid.

It's actually both. bash execves cat directly, so one becomes the other.

To understand what exactly is needed for the no-fork behaviour, we need to look at the source code. There's this comment:

      /*
       * IF
       *   we were invoked as `bash -c' (startup_state == 2) AND
       *   parse_and_execute has not been called recursively AND
       *   we're not running a trap AND
       *   we have parsed the full command (string == '\0') AND
       *   we're not going to run the exit trap AND
       *   we have a simple command without redirections AND
       *   the command is not being timed AND
       *   the command's return status is not being inverted
       * THEN
       *   tell the execution code that we don't need to fork
       */

Source

Sign up to request clarification or add additional context in comments.

3 Comments

I saw this output when I straced it as well. Where ever it determines that the command is not complex and after tokenization it determines that it does not need to fire any subprocesses (as a result of ; or || or && or |) then it simply execves the command pass to it. Otherwise it clones and then execves in the child/clone. What I don't quite get is why.
@ffledgling There is a comment in the source IF we were invoked as `bash -c' (startup_state == 2) AND parse_and_execute has not been called recursively AND we're not running a trap AND we have parsed the full command (string == '\0') AND we're not going to run the exit trap AND we have a simple command without redirections AND the command is not being timed AND the command's return status is not being inverted THEN tell the execution code that we don't need to fork
Would you mind adding this to the answer and linking to the relevant line in the official source?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.