17

I am trying to write a wrapper which will execute a script as a session leader. I am confused by the behaviour of the linux command setsid. Consider this script, called test.sh:

#!/bin/bash
SID=$(ps -p $$ --no-headers -o sid)
if [ $# -ge 1 -a $$ -ne $SID ] ; then
  setsid bash test.sh
  echo pid=$$ ppid=$PPID sid=$SID parent
else
  sleep 2
  echo pid=$$ ppid=$PPID sid=$SID child
  sleep 2
fi

The output differs depending on whether it is executed or sourced:

$ bash
$ SID=$(ps -p $$ --no-headers -o sid)
$ echo pid=$$ ppid=$PPID sid=$SID
pid=9213 ppid=9104 sid= 9104
$ ./test.sh 1 ; sleep 5
pid=9326 ppid=9324 sid= 9326 child
pid=9324 ppid=9213 sid= 9104 parent
$ . ./test.sh 1 ; sleep 5
pid=9213 ppid=9104 sid= 9104 parent
pid=9336 ppid=1 sid= 9336 child
$ echo $BASH_VERSION 
4.2.8(1)-release
$ exit
exit

So, it seems to me that setsid returns immediately when the script is sourced, but it waits for its child when the script is executed. Why would the presence of a controlling tty have anything to do with setsid? Thanks!

Edit: For clarification I added pid/ppid/sid reporting to all relevant commands.

Matei David
  • 2,322
  • 3
  • 23
  • 36

3 Answers3

25

The source code of the setsid utility is actually very straightforward. You'll note that it only fork()s if it sees that its process ID and process-group ID are equal (i.e., if it sees that it's a process group leader) — and that it never wait()s for its child process: if it fork()s, then the parent process just returns immediately. If it doesn't fork(), then it gives the appearance of wait()ing for a child, but really what happens is just that it is the child, and it's Bash that's wait()ing (just as it always does). (Of course, when it really does fork(), Bash can't wait() for the child it creates, because processes wait() for their children, not their grandchildren.)

So the behavior that you're seeing is a direct consequence of a different behavior:

  • when you run . ./test.sh or source ./test.sh or whatnot — or for that matter, when you just run setsid directly from the Bash prompt — Bash will launch setsid with a new process-group-ID for job control purposes, so setsid will have the same process-ID as its process-group-ID (that is, it's a process group leader), so it will fork() and won't wait().
  • when you run ./test.sh or bash test.sh or whatnot and it launches setsid, setsid will be part of the same process group as the script that's running it, so its process-ID and process-group-ID will be different, so it won't fork(), so it'll give the appearance of waiting (without actually wait()ing).
ruakh
  • 175,680
  • 26
  • 273
  • 307
  • 4
    You are right. I wonder if it would be worth proposing that `setsid` take on an extra flag, say `-w`, on whose presence it should wait for its child if there is one. As it is, I feel its behaviour is inconsistent: it returns immediately if and only if it's run by a group leader (and it forks). Plus, like you say, only `setsid` could wait for its child, the invoking `bash` cannot wait for a grandchild. – Matei David Mar 19 '12 at 13:14
  • 1
    Yes; and just in general, it's kind of strange to `fork` without `wait`ing. I don't think my college Operating Systems professor would have approved. :-P – ruakh Mar 19 '12 at 13:21
  • @MateiDavid The linked source code contains the entry _2008-08-20 Daniel Kahn Gillmor - if forked, wait on child process and emit its return code._ (`-w` option) which is what you suggest be added in the comment above written four years later. What am I missing? – Piotr Dobrogost Nov 22 '20 at 16:09
1

The behavior I observe is what I expect, though different from yours. Can you use set -x to make sure you're seeing things right?

$ ./test.sh 1
child
parent
$ . test.sh 1
child
$ uname -r
3.1.10
$ echo $BASH_VERSION
4.2.20(1)-release

When running ./test.sh 1, the script's parent — the interactive shell — is the session leader, so $$ != $SID and the conditional is true.

When running . test.sh 1, the interactive shell is executing the script in-process, and is its own session leader, so $$ == $SID and the conditional is false, thus never executing the inner child script.

ephemient
  • 198,619
  • 38
  • 280
  • 391
  • You're right; but my concern is what happens when the shell which launches the script (either execute or source) is _not_ a session leader. Try adding another level of `bash` like I did in my original example. (What I'm trying to achieve is to run the script as a guaranteed session leader. If I assume that's the case, there's no point in having the script in the first place.) – Matei David Mar 13 '12 at 03:54
0

I do not see any problem with your script as is. I added extra statements in your code to see what is happening:

    #!/bin/bash

    ps -H -o pid,ppid,sid,cmd

    echo '$$' is $$

    SID=`ps -p $$ --no-headers -o sid`

    if [ $# -ge 1 -a $$ -ne $SID ] ; then
      setsid bash test.sh
      echo pid=$$ ppid=$PPID sid=$SID parent
    else
      sleep 2
      echo pid=$$ ppid=$PPID sid=$SID child
      sleep 2
    fi

The case that concerns you is:

./test.sh 1 

And trust me run this modified script and you will see exactly what is happening. If the shell which is not a session leader runs the script then it simply goes to else block. Am I missing something?

I see now what you mean: When you do ./test.sh 1 with your script as is then parent waits for the child to complete. child blocks the parent. But if you start the child in background then you will notice that parent completes before child. So just make this change in your script:

      setsid bash test.sh &
Ankur Agarwal
  • 23,692
  • 41
  • 137
  • 208
  • 1
    The reason I want this piece of code is to incorporate it in a more complex script, which in turn could be executed by hand, or sourced by hand, or even run by an SGE job engine for all I know. What I need is a _consistent_ behaviour of parent and child. Ideally: _if_ `setsid` is necessary (i.e., I am not already session leader) _then_ I'd like the parent to wait for the child. The _if_ part is true in both scenarios in my example: parent `PID` != parent `SID`. What bugs me is that the _then_ part doesn't hold when the script is sourced: parent exits before child. – Matei David Mar 13 '12 at 16:33