32

I'm trying to build a process container. The container will trigger other programs. For example - a bash script that launches running background tasks with '&' usage.

The important feature I'm after is this: when I kill the container, everything that has been spawned under it should be killed. Not just direct children, but their descendants too.

When I started this project, I mistakenly believed that when you killed a process its children were automatically killed too. I've sought advice from people who had the same incorrect idea. While it's possible to catch a signal and pass the kill on to children, that's not what I'm looking for here.

I believe what I want to be achievable, because when you close an xterm, anything that was running within it is killed unless it was nohup'd. This includes orphaned processes. That's what I'm looking to recreate.

I have an idea that what I'm loooking for involves unix sessions.

If there was a reliable way to identify all the descendants of a process, it would be useful to be able to send them arbitrary signals, too. e.g. SIGUSR1.

Lesmana
  • 27,439
  • Well, killing the parent process sends SIGHUP to it's direct children processes. The default handler of hang up signal aborts process execution, so the default route is to kill all the descendants. Read more on processes and process groups. – alex Jun 11 '11 at 13:27
  • Closing the xterm kills everything spawned in the xterm because the TTY is destroyed. If you can come up with a way to create a tty which child processes can use, you can then destroy the TTY and accomplish the same thing. Any process which didnt close that TTY (nohup & friends) will get a SIGHUP. – phemmer Jun 12 '11 at 10:34
  • @alex Is that true if the children processes are backgrounded? – Daniel Kaplan Jan 01 '23 at 23:31

9 Answers9

38

If you send a signal to a process, that process gets killed. I wonder how the rumor that killing a process also kills other processes got started, it seems particularly counter-intuitive.

There are, however, ways to kill more than one process. But you won't be sending a signal to one process. You can kill a whole process group by sending a signal to -1234 where 1234 is the PGID (process group ID), which is the PID of the process group leader. When you run a pipeline, the whole pipeline starts out as a process group (the applications may change this by calling setpgid or setpgrp).

When you start processes in the background (foo &), they are in their own process group. Process groups are used to manage access to the terminal; normally only the foreground process group has access to the terminal. The background jobs remain in the same session, but there's no facility to kill a whole session or even to enumerate the process groups or processes in a session, so that doesn't help much.

When you close a terminal, the kernel sends the signal SIGHUP to all processes that have it as their controlling terminal. These processes form a session, but not all sessions have a controlling terminal. For your project, one possibility is therefore to start all the processes in their own terminal, created by script, screen, etc. Kill the terminal emulator process to kill the contained processes (assuming they haven't seceded with setsid).

You can provide more isolation by running the processes as their own user, who doesn't do anything else. Then it's easy to kill all the processes: run kill (the system call or the utility) as that user and use -1 as the PID argument to kill, meaning “all of that user's processes”.

You can provide even more isolation, but with considerably more setup by running the contained processes in an actual container.

  • 1
    If you program your own processes, then you can use something such as: prctl(PR_SET_PDEATHSIG, SIGHUP);, more info: http://man7.org/linux/man-pages/man2/prctl.2.html – Alexis Wilke Jul 19 '16 at 19:33
  • Gilles, you mention this - "When you close a terminal, the kernel sends the signal SIGHUP to all processes that have it as their controlling terminal". From this I'm surmising the terminal sends SIGHUP to the session leader (shell) who forwards it on to all process groups within the session. Which of the two would be right? – iruvar Oct 24 '18 at 04:35
8

A reliable way to identiry all the descendants of a process is to use the command pstree <pid> where pid is your parent process id.

Read the man page on pstree here.

To signal all members of a process group: killpg(<pgrp>, <sig>);
where pgrp is the process group number and sig is the signal.

To wait for children in a specified process group: waitpid(-<pgrp>, &status, ...);

An alternative to what you're doing is to run your process container in a new bash shell. Create the new bash shell with the command bash and then run your processes. When you want all processes to be ended, exit the shell with the command exit.

6

Within the parent script trap the kill signal and have it kill all the children. For example,

#!/bin/bash
# kill the parent and children together
trap "kill 0" EXIT
# create all the children
for n in $(seq 1 100)
do
    ( echo "begin $n"; sleep 60; echo "end $n" ) &
done
# wait for the children to complete
wait
HalosGhost
  • 4,790
6

You could use this kind of shell script:

set -m; (
# start the processes in this container:
...
) & set +m; pid=$!
...
# terminate the container:
kill -- -"$pid"

The trick is to enable job control for the container subshell so that the processes executed there get common unique process group ID, which you can use by the given kill command to kill all the processes.

jarno
  • 620
  • 1
    This was the only answer that worked for me, on OSX without gnu ps. Also other answers kill kill 0 killed my parent script, which I wanted to avoid because I wanted to do processing after the children exited. – silico-biomancer Jun 15 '20 at 23:22
  • Note that it might be unnecessary to switch the m option, if it is already set in the beginning. If you use dash (0.5.10.2) and you call the shell with -m, then set -m will change process group of the shell match with the caller's process group. – jarno Oct 25 '20 at 13:08
  • @BlueDrink9 Does my other answer work for you? – jarno Oct 25 '20 at 14:31
3

Use

unshare -fp --kill-child -- yourprogram

If you kill unshare, all child processes (that yourprogram may have spawned) will be killed.

This is now possible with util-linux 2.32; I implemented this upstream. It requires either user namespaces (kernel config option CONFIG_USER_NS=y) or root privileges. See also here.

nh2
  • 1,721
  • 2
  • 14
  • 22
1

rkill command from pslist package sends given signal (or SIGTERM by default) to specified process and all its descendants:

rkill [-SIG] pid/name...
Onlyjob
  • 644
  • 5
  • 10
0

Another option for killing all descendants of the shell: jobs -p | xargs -n 1 pkill -P

0

At least if you use Bash (5.0.17), you could use the following:

    #!/bin/bash -m
    set +m
    trap 'kill -- -$$' EXIT
    process1 &
    process2 &
    ....
    wait

There are some problems if you send USR1 signal to the process group, though.

If you use dash (0.5.10.2), set +m will switch process group to match the process group of the caller, thus killing this process (group) would kill the caller, too.

jarno
  • 620
-1

pgrep -P ppid: Restrict matches to processes with a parent process ID in the comma-separated list ppid.

# ZSH code
function ps-children() {
    pgrep -P "$1"
}
function ps-grandchildren() {
  local children=( $(ps-children "$1") ) pid

for pid in $children[@] do "$0" "$pid" done

print -r -- "${(F)children}" } function kill-withchildren() { setopt localoptions re_match_pcre local sig=2 if [[ "$1" =~ '-\d+' ]] ; then sig="$1" shift fi local pids=("$@") pid

for pid in &quot;$pids[@]&quot; ; do
    local children=(&quot;${(@f)$(ps-grandchildren &quot;$pid&quot;)}&quot;)
    kill -$sig &quot;$pid&quot; &quot;$children[@]&quot;
done

}

HappyFace
  • 1,612
  • This is odd, you call the script recursively, but there are only functions in the script. Also your for loops do not work in Bash. What does "${(F)children}" mean? – jarno Sep 05 '20 at 22:50
  • 1
    @jarno $children[@] will eventually become empty, and the recursion will terminate. The code is in zsh; "${(F)children}" joins the array using \n. – HappyFace Sep 06 '20 at 03:16