如何设置一个 shell 脚本的进程组

37
如何设置shell脚本的进程组?我希望所有子进程都在同一进程组中。
我期望类似于C语言中的setpgid()

2
我想从同一个shell脚本(自身)设置进程组。 - Jacob
6个回答

25

正如PSkocik指出的那样,在大多数shell中,通过激活作业控制(“监视模式”),可以将进程运行在它自己的进程组中。

(set -m; exec process_in_its_own_group)

Linux有一个 setsid 实用工具,它在自己的会话中运行作为参数传递的命令(使用同名的系统调用)。这比在其自己的进程组中运行更强大,而像 setpgrp 那样将其放在自己的进程组中也许对您的目的来说是可以的。

如果你想将该进程放在现有进程组而不是自己的进程组中(即如果你想要setpgid的全部功能),那么没有通用的shell实用程序。您必须使用C/Perl/…


缺少 setpgrp 实用程序是由于任何技术障碍吗? - Piotr Dobrogost
@PiotrDobrogost 不需要。在大多数Shell中,可以使用set -m来完成(参见PSkocik的答案)。 - Gilles 'SO- stop being evil'
尝试使用这个来解决有没有办法在不使用^C的情况下退出“less”跟随模式?,但是没有成功。在将(…) | less -R更改为(…) | (set -m; exec less -R)后,我再也没有输出了。 - Piotr Dobrogost
5
set -m需要一个终端设备,但该设备可能不存在。 - josch

15

我会回答我所理解的部分:

如何强制当前的bash shell脚本成为自己的进程组:

我在我的bash脚本开头添加了这个:

pgid_from_pid() {
    local pid=$1
    ps -o pgid= "$pid" 2>/dev/null | egrep -o "[0-9]+"
}

pid="$$"
if [ "$pid" != "$(pgid_from_pid $pid)" ]; then
    exec setsid "$(readlink -f "$0")" "$@"
fi

我为什么需要这个东西?

从交互式bash会话启动程序时,它会得到自己的新进程组。但是,如果您的程序是从bash脚本(非交互式)调用的,则不是这种情况。如果您的程序依赖于在两种条件下都成为进程组所有者,则需要使用此功能。


非常感谢你,你救了我的命!我花了很多时间来实现我的目标,你的解决方案是锦上添花! - mixo
1
我不明白为什么你要将变量“pid”传递给函数pgid_from_pid()。你的函数能够读取外部变量,为了在函数内使用传递的参数,你需要使用常见的$1、$2语法来访问它们。 - Jadzia
@Jadzia,有一行代码丢失了,很抱歉。感谢您的发现。已经进行了编辑。 - vaab
为什么需要grep调用?据我所知,ps命令只会输出pgid而已。 - stefanct

10

我认为 Bourne、bash 或 zsh 不会让你这样做,但你可以使用内置的 setpgrp 在 Perl 中实现(注意与 POSIX 稍有不同的名称)。将 PID 设置为零以修改 Perl 进程本身的组:

setpgrp(0, 12345) || die "$!"

你可能认为可以通过在Bash中使用Perl(例如将$$传递给Perl脚本)来设置Bash进程的组,但我认为Perl进程无法修改它没有派生出来的进程的组。

根据你想要做什么,各种shell中的作业控制功能可能以不同的方式提供所需的功能,比如如果你只想从终端分离。

更新:我认为这个答案收到了几个没有明确解释的负评。我猜测是因为投票者误解了问题,问题是询问如何更改当前shell的进程组。或者他们知道如何从shell执行setpgrp,但却保守秘密。


3
刚刚核查了一下,交互式 shell 会在新的进程组中运行程序。非交互式的 shell(例如由 cron 启动的 shell)会在同一个进程组中运行程序(因为没有控制终端,所以没有必要在进程之间进行复用)。而且,不能使用 shell 内置命令来更改进程组。 - Maxim Egorushkin
我的回答是关于改变当前进程的进程组。请具体说明我说错了什么,以便我重新评估它。 - Rob Davis
3
我猜他们不喜欢Perl的依赖关系,但这并不公平对待你的回答。在我看来,投反对票应该需要一条评论。 - nhed
我没有投反对票,但请注意这不是一个可直接使用的答案。问题并没有问如何更改 Perl 脚本的进程组 (; - spawn
是的,因为在shell中没有办法发出命令并更改该shell的进程组,就像原始问题中请求的setpgid()函数一样。一个解决方法是在不同的组中启动一个单独的进程(请参见另一个答案),另一个解决方法是使用不同的工具,如perl或作业控制(请参见此答案)。当一个问题无法按要求解决时,提供近似解决方案作为解决方法是合适的。所有好的答案都做到了这一点。 - Rob Davis

7
如果您打开set -m,新进程将在一个新的进程组中生成,如果它们被放到后台,它们不会忽略SIGINT和SIGQUIT。
if  [ $$ = $(ps -o pgid -hp $$) ]; then
   echo already a process group leader;
else
   set -m
   $0 "$@" #optionally with &
   set +m
fi

新的进程程序组在执行set -m接管终端前台进程组后运行,除非它们在后台运行。 set -m 显然是半标准的,如果实现支持“用户可移植性工具”,则 POSIX 要求使用。实际上,它适用于 bashdashkshpdkshshyashzsh。但 posh 不支持。

4

如果您的意图是清理任何生成的子shell进程(即使脚本本身不是直接从交互式shell启动,而是来自另一个进程,并且因此不会自动成为其自己的进程组长),则可以参考以下综合答案。必要时将当前脚本作为新的进程组长重新启动。

# First, obtain the current PGID, by parsing the output of "ps".
pgid=$(($(ps -o pgid= -p "$$")))

# Check if we're already the process group leader; if not, re-launch ourselves.
# Use setsid instead of set -m (...) to avoid having another subshell in between. This helps that the trap gets executed when the script is killed.
[ $$ -eq $pgid ] || exec setsid --wait "${BASH_SOURCE[0]}" "$@"

# Kill any subshell processes when the script exits.
trap "kill -- -$pgid" EXIT
# Note: If the script only starts background jobs, and that's all you care about, you can replace all of the above with this simple trap:
#trap "jobs -p | xargs kill --" EXIT  # Kill remaining jobs when the script exits.

嵌套命令

当一个脚本调用另一个进行子shell清理时,会引入另一种复杂性。进程组领导不会嵌套;一旦一个脚本担任领导职责,它的生命周期就不再由父脚本控制,因此,当父脚本被中断或终止时,嵌套脚本将继续存在。这通常不是用户想要的。

以下脚本片段通过协作模型扩展了上述实现,以便只有顶级脚本担任进程组领导,并通过导出$PGID将其传递给子shell。如果子shell发现已经存在领导者,则不会自己担任领导,而是将自己的清理任务限制在剩余的作业中。其他子shell将在顶级脚本退出时被终止。(因此,当一个脚本只调用一个或只调用几个其他脚本时,这种协作模型最有效)。

if [ -z "$PGID" ]; then # No parent script has become the process group leader yet.
    pgid=$(($(ps -o pgid= -p "$$")))    # By defining this, we'll be killing subshell processes of this process group when we're done or interrupted. Any children with the same ambition will defer to us.
    if [ $$ -eq $pgid ]; then
        export PGID=$pgid   # We are (already / after setsid) in our own process group, announce our leadership to any children, so that they don't become leaders themselves and thereby decouple themselves from our lifetime control.
    else
        exec setsid --wait "${BASH_SOURCE[0]}" "$@" # Use setsid instead of set -m (...) to avoid having another subshell in between.
    fi
fi

if [ -n "$pgid" ]; then
    trap "kill -- -$pgid" EXIT  # If we're the leader, kill subshell processes when the script exits.
else
    trap "jobs -p | xargs kill --" EXIT  # Someone else is the leader; killing remaining jobs is all we can do here.
fi

1
谢谢,我认为这是最好的答案。由于某种原因,“setsid”技术(是的,早些时候介绍过)对我有效,而“set -m”则没有。 - Metamorphic

-1

正如@Rob Davis在他的回答中指出的,设置进程组并不是您想要的。

相反,您需要使用它们的进程控制机制。这个回答介绍了在linuxborne操作系统上为sh执行此操作的方法。简而言之:

#! /bin/sh
# Kill all opened jobs on exit.
trap 'kill $(jobs -p)' EXIT

这将终止在后台打开的任何作业(例如使用&)。


1
这种方法可能是可行的,但你做错了。$(jobs -p)会强制将jobs -p放入子shell中,其中作业列表将为空(尝试sleep 10&sleep&sleep&)。您需要将pid列表写入临时文件,然后从该文件中读取它。至少对于大多数shell来说(ksh和bash设法避免子shell,并且它们还为陷阱内置执行类似的魔术--您的解决方案应在这两个shell中工作,但不适用于其他shell)。 - Petr Skocik

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