Linux命令集:setsid

17

我正在尝试编写一个包装器,它将作为会话领导者执行一个脚本。 我对Linux命令setsid的行为感到困惑。考虑这个名为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

输出结果取决于它是被执行还是被源化:

$ 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

因此,我认为当脚本被源时,setsid会立即返回,但当脚本被执行时,它会等待其子进程。

为什么控制tty的存在会影响setsid呢?谢谢!

编辑:为了澄清,我添加了pid / ppid / sid报告到所有相关命令。

3个回答

25

setsid 实用程序的源代码非常简单。你会注意到它只在看到它的进程 ID 和进程组 ID 相同时(也就是,看到它是进程组领导者时)才进行 fork() —— 而且它从不 wait() 子进程:如果它进行了 fork(),则父进程立即返回。如果它 没有 进行 fork(),那么它会表现出等待子进程的样子,但实际上它就是子进程,而它所运行的 Bash 正是在进行 wait()(正如它一直在做的那样)。 (当然,当它真正执行了 fork() 时,Bash 就不能 wait() 它创建的子进程,因为进程要 wait() 它们的子进程,而不是孙子进程。)

因此,你看到的行为是不同行为的直接后果:

  • 当你运行 . ./test.shsource ./test.sh 等命令(或者直接在 Bash 提示符下运行 setsid),Bash 为了作业控制而会为 setsid 启动一个新的进程组 ID,以便 setsid 具有与其进程组 ID 相同的进程 ID(也就是说,它是进程组领导者),因此它将进行 fork() 并且不会 wait()
  • 当你运行./test.sh或者bash test.sh时,如果它启动了setsid,那么setsid会成为与运行它的脚本相同的进程组,因此其进程ID和进程组ID将不同,所以它不会fork(),看起来就像在等待(实际上并没有执行wait())。

  • 4
    你是正确的。我在想是否值得建议给 setsid 增加一个额外的标志,比如 -w,只有在存在子进程时它才会等待。目前它的行为不一致:仅当它由组长运行(并且fork)时,它才会立即返回。另外,正如你所说,只有 setsid 可以等待其子进程,调用者的 bash 无法等待孙子进程。 - Matei David
    1
    是的,一般来说,在没有等待的情况下进行fork操作有点奇怪。我想我的大学操作系统教授不会批准这样做。:-P - ruakh
    @MateiDavid 链接的源代码包含条目 2008-08-20 Daniel Kahn Gillmor - 如果分叉,请等待子进程并发出其返回代码 (-w选项),这是您在四年后编写的上面评论中建议添加的内容。我错过了什么? - Piotr Dobrogost

    1

    我观察到的行为与您不同,但符合我的预期。您能使用set -x确保您看到的是正确的吗?

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

    当运行./test.sh 1时,脚本的父进程——交互式shell——是会话领导者,因此$$ != $SID,条件成立。

    当运行. test.sh 1时,交互式shell在进程中执行脚本,并且是自己的会话领导者,因此$$ == $SID,条件不成立,从而永远不会执行内部子脚本。


    你说得对;但我的担忧是,当启动脚本的 shell(无论是执行还是源)_不是_会话领导者时会发生什么。尝试像我在原始示例中所做的那样添加另一个级别的 bash。 (我想要实现的是将脚本作为保证的会话领导者运行。如果我假设这是情况,那么首先就没有必要有脚本。) - Matei David

    0

    我认为您的脚本没有任何问题。我在您的代码中添加了额外的语句以查看发生了什么:

        #!/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
    

    你关心的情况是:

    ./test.sh 1 
    

    相信我,运行这个修改过的脚本,你会看到发生了什么。如果不是一个会话领导者的shell运行脚本,那么它就会简单地进入else块。我有什么遗漏吗?

    现在我明白你的意思了:当你像这样使用你的脚本./test.sh 1时,父进程会等待子进程完成。子进程阻塞了父进程。但是如果你将子进程放在后台启动,你会注意到父进程在子进程之前完成。所以只需在你的脚本中做出这个改变:

          setsid bash test.sh &
    

    1
    我需要这段代码的原因是为了将它纳入更复杂的脚本中,然后可以手动执行或手动源化,甚至可能由SGE作业引擎运行。我需要的是父进程和子进程之间的一致行为。理想情况下:如果setsid是必需的(即我不是已经是会话领导者),那么我希望父进程等待子进程。在我的示例中,两种情况下都满足if部分:父进程的PID != 父进程的SID。让我困扰的是当脚本被源化时,then部分不成立:父进程先退出,然后子进程执行。 - Matei David

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接