1

My question has nothing to do with WCE (wait and cooperative exit). Assuming i have a script launched in an interactive shell (bash) as a foreground job:

#! /bin/bash
# script name: foreback.sh
sleep 100m &   # child process in bg
sleep 200m &   # ditto
sleep 300m &   # ditto
sleep 7000m    # child process in fg

exit 0

When this script runs, pressing Ctrl-C kills all foreground processes (the process for my script - the parent - and the 4th sleep child process as expected.

My question is: How are those background childs recognized as such when the foreground process group receives SIGINT?

Look at the following ps - output before sending the signal:

TT         TPGID    PPID     PID    PGID    SESS STAT COMMAND
pts/0       9373    9259    9282    9282    9282 Ss    |       \_ /bin/bash
pts/0       9373    9282    9373    9373    9282 S+    |       |   \_ /bin/bash ./foreback.sh
pts/0       9373    9373    9374    9373    9282 S+    |       |       \_ sleep 100m
pts/0       9373    9373    9375    9373    9282 S+    |       |       \_ sleep 200m
pts/0       9373    9373    9376    9373    9282 S+    |       |       \_ sleep 300m
pts/0       9373    9373    9377    9373    9282 S+    |       |       \_ sleep 7000m

Parent and child processes seem to be some kind of "foreground complex", because they all belong to the same terminal process group (TPGID) that is the PID of the parent foreground process and the STAT column shows a plus sign. When sending SIGINT to the foreground process group via Ctrl-C or sending SIGINT to the process group (PGID) via kill -INT -- -pgid, how does the shell know which processes to terminate and which to leave alive? After Ctrl-C or the above mentioned processgroup kill, my ps - output looks like this:

TT         TPGID    PPID     PID    PGID    SESS STAT COMMAND
pts/0       9282    9259    9282    9282    9282 Ss+   |       \_ /bin/bash
pts/0       9282    1742    9374    9373    9282 S     \_ sleep 100m
pts/0       9282    1742    9375    9373    9282 S     \_ sleep 200m
pts/0       9282    1742    9376    9373    9282 S     \_ sleep 300m

The three childs launched in background from parent remain alive, the STAT column indicates they are in background by the missing plus sign, and the terminal process group is now the group of the interactive shell. This is how it should be.

But I cannot see any "flag" that shows "Don't kill me, I'm a background process" at the time, when SIGINT is sent to the foreground processgroup.

I guess that the following is happening:

  1. SIGINT is sent to the foreground process group

  2. The first process that receives the signal and reacts upon it, is the process which PID equals the TPGID (the foreground process group leader).

  3. When this process dies, the shell "remembers" which child processes were launched as background processes and changes their TPGID from the original one to the one of the interactive shell, so that they don't belong to the old foreground process group any longer. Remaining childs that were not launched with & do not undergo this change of their TPGID, receive SIGINT and react upon it (terminate if not handled otherwise)

I comb thru a myriad of websites since weeks, but i cannot find a proper answer.

Any ideas???? Thanks

1
  • 1
    Hint: change sleep 100m & to (trap - INT; exec sleep 100m) & Commented Apr 15, 2023 at 19:51

1 Answer 1

3

Non-interactive shells don't do job control by default, so all processes run by a script are in the same process group as the process that executed the shell. That process group will have been made the foreground process group of the terminal or not by the interactive shell starting the script depending on whether the script was started/put in foreground or not.

But when job control is disabled, POSIX shells have this requirement:

2.11. Signals and Error Handling

If job control is disabled (see the description of set -m) when the shell executes an asynchronous list, the commands in the list shall inherit from the shell a signal action of ignored (SIG_IGN) for the SIGINT and SIGQUIT signals. In all other cases, commands executed by the shell shall inherit the same signal actions as those inherited by the shell from its parent unless a signal action is modified by the trap special built-in (see trap)

So in effect it's a crude form of "job" control where asynchronous lists are immune to Ctrl + c and Ctrl + \.

On Linux:

$ sh -c 'sleep 10 & grep SigIgn "/proc/$!/status"'
SigIgn: 0000000000000006
$ kill -l INT QUIT
2
3

(SigIgn above is a bitmask, with 2nd and 3rd bit set in that case corresponding to SIGINT and SIGQUIT).

Note that when the terminal sends those ^C or ^\ characters upon pressing those key combinations, the shell is not involved in the delivery of those signals. It's the kernel (the tty driver) that sends the signal to the processes in the foreground process group of the terminal.

The TPGID is an attribute of the terminal device (in your case /dev/pts/0), not really the process. ps will always show the same value for all processes running in a session attached to a terminal. When putting a job (process group) in foreground, the interactive shell will do a tcsetpgrp(terminal_fd, its_pid) to tell the terminal driver: this is the process group that is in foreground for that terminal, anything else is in background.

The fact that the surviving sleep processes are in background after ^C is just because the script terminated, so the interactive shell waiting for it will have change back the terminal's foreground process group to its own.

1
  • Thank, Stéphane !! Your answer brought light into the parts of my brain that were stuck in overthinking the problem again & again. I started reading this excellent book by Michael Kerrisk the last days (The Linux Programming Interface). I think, I will find more about the tricky intrinsics of Linux. You've helped me alot. Thanks again. Commented Apr 16, 2023 at 14:45

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .