Update: trap requires removing SIG prefix on condition, although some shells support including it. See comment below.

The ampersand "&" runs a command in the background in a new process. When its parent process (the command that runs the bash script in your case) ends, this background process will reset its parent process to init (process with PID 1), but will not die. When you press ctrl+c, you are sending an interrupt signal to the foreground process, and it will not affect the background process.

In order to kill the background process, you should use the kill command with the PID of the most recent background process, which could be obtained by $!.

If you want the to use ctrl+c to kill both the script and background process, you can do this:

trap 'kill $BGPID; exit' INT
sleep 1024 &    # background command
BGPID=$!
sleep 1024      # foreground command of the script

trap modifies the trap handler of the SIGINT (trap requires removing the SIG prefix but some shell may support including it) so the script will kills the process with $BGPID before it exits.

Answer from lnyng on Stack Exchange
🌐
Reddit
reddit.com › r/bash › how not to kill a background process with ctrl-c?
r/bash on Reddit: How NOT to kill a background process with CTRL-C?
August 14, 2022 -

I am running RetroPie/Debian 10 on Raspberry Pi 4 with EmulationStation front-end. Sometimes I quit ES and leave the terminal up when I'm not using it. And sometimes I want to start up ES from the couch with my wireless controller, rather than cross the room and use the keyboard. (I know, right?)

Essentially what I want is for a gamepad button combo to have the same effect as typing "logout" on the keyboard.

...anyway, I thought I had it solved with jslisten (watches /dev/input/js{n} for a predefined button combo & runs a command on activation):

https://github.com/workinghard/jslisten

How I've set it up:

To activate it when I quit ES, I add this line to ~/.bashrc:

[[ "$(tty)" == "/dev/tty1" ]] && (/opt/retropie/configs/all/jslisten/jslisten /dev/input/js0 &)

...it is configured to watch for my button combo and then run a script I've called logmeout.sh that looks for and kills the oldest instance of bash on tty1:

#!/bin/bash

pid="$(pgrep -o -t tty1 bash)"
[[ -n "$pid" ]] && kill "$pid"

And then, to disable it at login, I add pkill jslisten to autostart.sh, before emulationstation #auto is invoked.

This is mostly working as intended. I can pick up my controller from across the room, press select + guide + start and, as long as it was just sitting at the terminal, I'll be logged out, at which point the autostart triggers and logs me back in to EmulationStation. Just like I wanted.

On the other hand if it is NOT just sitting at the terminal (I've tested while editing a file in nano, while watching system temp with watch -n1 vcgencmd measure_temp and while running emulationstation manually from command line), nothing happens; the current task is not interrupted and when I finish it, I am not suddenly logged out or anything bad like that. Also just like I wanted.

There's just one little thorn left I have to work out. Sometimes it just...doesn't do anything. I'll push the buttons and nothing happens. Unplug and replug the controller's wireless dongle, nothing. After a quick relog from the keyboard, everything is working again...weird.

The next time it happened I thought to check the running processes with ps, and the jslisten process was not listed. So why not...

...I tracked it down to any time I hit ctrl-c in the terminal, it's killing the process. Why? And what can I do about it?

Also this only happens when it's started from .bashrc; if I run the exact same command from terminal and ctrl-c, the process persists (terminal output re-enactment):

# {quit from EmulationStation}
pi@retropie:~ $ ps
  PID TTY          TIME CMD
13883 tty1     00:00:00 bash
14115 tty1     00:00:09 jslisten
16286 tty1     00:00:00 ps
pi@retropie:~ $ ^C
pi@retropie:~ $ ps
  PID TTY          TIME CMD
13883 tty1     00:00:00 bash
16300 tty1     00:00:00 ps
pi@retropie:~ $ [[ "$(tty)" == "/dev/tty1" ]] && (/opt/retropie/configs/all/jslisten/jslisten /dev/input/js0 &)
pi@retropie:~ $ ps
  PID TTY          TIME CMD
13883 tty1     00:00:00 bash
16339 tty1     00:00:00 jslisten
16340 tty1     00:00:00 ps
pi@retropie:~ $ ^C
pi@retropie:~ $ ps
  PID TTY          TIME CMD
13883 tty1     00:00:00 bash
16339 tty1     00:00:00 jslisten
16341 tty1     00:00:00 ps
pi@retropie:~ $ 
🌐
Reddit
reddit.com › r/bash › bash script, ctrl-c and backgrounding processes
r/bash on Reddit: bash script, Ctrl-C and backgrounding processes
February 16, 2020 -

I've written a long bash script that will kick off numerous processes and puts them in the background. For some commands the script will use nohup <cmd> &, and other commands it will runrails server -d for backgrounding. The script can take up to a minute to complete. While the script is running and starting up the processes, sometimes if I hit Ctrl-C and it will die immediately, other times it will not die immediately and just keep chugging along.

Is there a way to have Ctrl-C consistently kill the script immediately?

Top answer
1 of 5
23

Your kill command is backwards.

Like many UNIX commands, options that start with a minus must come first, before other arguments.

If you write

kill -INT 0

it sees the -INT as an option, and sends SIGINT to 0 (0 is a special number meaning all processes in the current process group).

But if you write

kill 0 -INT

it sees the 0, decides there's no more options, so uses SIGTERM by default. And sends that to the current process group, the same as if you did

kill -TERM 0 -INT    

(it would also try sending SIGTERM to -INT, which would cause a syntax error, but it sends SIGTERM to 0 first, and never gets that far.)

So your main script is getting a SIGTERM before it gets to run the wait and echo DONE.

Add

trap 'echo got SIGTERM' TERM

at the top, just after

trap 'killall' INT

and run it again to prove this.

As Stephane Chazelas points out, your backgrounded children (process1, etc.) will ignore SIGINT by default.

In any case, I think sending SIGTERM would make more sense.

Finally, I'm not sure whether kill -process group is guaranteed to go to the children first. Ignoring signals while shutting down might be a good idea.

So try this:

#!/bin/bash
trap 'killall' INT

killall() {
    trap '' INT TERM     # ignore INT and TERM while shutting down
    echo "**** Shutting down... ****"     # added double quotes
    kill -TERM 0         # fixed order, send TERM not INT
    wait
    echo DONE
}

./process1 &
./process2 &
./process3 &

cat # wait forever
2 of 5
11

Unfortunately, commands started in background are set by the shell to ignore SIGINT, and worse, they can't un-ignore it with trap. Otherwise, all you'd have to do is

(trap - INT; exec process1) &
(trap - INT; exec process2) &
trap '' INT
wait

Because process1 and process2 would get the SIGINT when you press Ctrl-C since they're part of the same process group which is the foreground process group of the terminal.

The code above will work with pdksh and zsh which in that regard are not POSIX conformant.

With other shells, you would have to use something else to restore the default handler for SIGINT like:

perl -e '$SIG{INT}=DEFAULT; exec "process1"' &

or use a different signal like SIGTERM.

🌐
YouTube
youtube.com › the computer oracle
How to use Ctrl+C to kill all background processes started in a Bash script? - YouTube
--------------------------------------------------Become or hire the top 3% of the developers on Toptal https://topt.al/25cXVn-------------------------------...
Published   December 23, 2023
Views   8
Find elsewhere
Top answer
1 of 2
2

First, you should know that kill 0 send the signal to all process in current process group. (see kill(1) - Linux man page) My guess is that it is killing more than it should. This is why wait is not waiting, it is being terminated for some reason. My best bet to solve this problem is to iterate over running jobs from jobs -pr killing one by one: this way I assure the signal is sent only to child processes in that moment and nothing more.

To get this to work I did several tests, but I was stuck on sending SIGINT to child processes. They simple don't react to it! Searching the web I've found this answer on Stack Overflow:

https://stackoverflow.com/questions/2524937/how-to-send-a-signal-sigint-from-script-to-script-bash

So the real problem is that you cannot send SIGINT from one script to other, because in non-interactive shells the signal is ignored. I was not able to workaround this issue (using bash -i to call the child script does not to work).

I know you probably want to send SIGINT and wait for the child processes to shutdown gracefully, but if you do not mind to use SIGTERM (or any other signal but SIGINT) this is the best script I wrote:

#!/bin/bash
trap 'killall' INT

killall() {
    echo '**** Shutting down... ****'
    jobs -pr
    for i in $(jobs -pr); do
        kill -TERM $i
    done
    wait
    echo DONE
}

./long.sh &
./long.sh &
./long.sh &

cat # wait forever

To test, I've created this long.sh script:

#!/bin/bash
trap 'killall' TERM

echo "Started... $$" >> file.txt

killall() {
    # shutting down gracefully
    echo "Finished... $$" >> file.txt
    exit # don't forget this!
}

# infinite loop
while true; do echo loop >/dev/null ; done

In this last script I did not use sleep function because it creates a new process that was staying on memory even after the script finishes. In your script, you can call new processes but you should assure to broadcast the SIGTERM (or any other signal you've used) to all of them.

2 of 2
0

This seems to work. Be sure to use "/usr/bin/kill" and not the Bash built-in "kill".

[myles@marklar ~]$ /usr/bin/kill --version
kill from util-linux-2.13-pre7

Note that job control is not enabled in non-interactive Bash shells (e.g. scripts).

#!/bin/bash

trap kill_jobs 2

kill_jobs()
{
        while /usr/bin/kill 0; do :; done
        exit 0
}

foo &
foo &
foo &

sleep 777777
🌐
Helpdoco
helpdoco.com › Linux-Unix › ctrl-c-kills-nohup-background-processes.htm
Ctrl-C Kills Nohup Background Processes
setsid runs its first argument (nohup) with a new Process Group ID, thus stopping Ctrl-C from killing it. nohup runs its first argument (COMMAND) such that it will ignore hangup signals (SIGHUP). ... This command can be used on the command line or inside a shell script.
🌐
SoByte
sobyte.net › post › 2022-01 › different-from-ctrl-c-and-kill
The difference between Ctrl+C and Kill to kill a process - SoByte
January 8, 2022 - By default, programs started via ... to standard input and output.) Killing a process via <Ctrl+C> or <Ctrl+\> in bash sends a signal to every process in the foreground process group....
🌐
Atomic Spin
spin.atomicobject.com › start-stop-bash-background-process
A Script to Start (and Stop!) Background Processes in Bash
July 14, 2020 - Now, our script will stay running until the background processes finish, but if we ctrl+c, our background processes will still stay running. We can fix that with a trap. trap "kill 0" EXIT ./someProcessA & ./someProcessB & wait
🌐
Baeldung
baeldung.com › home › processes › how to kill a background process in linux
How to Kill a Background Process | Baeldung on Linux
March 18, 2024 - This is useful, for example, when we have a frozen web browser (like Chrome or Firefox) with multiple tabs open. We can easily kill all processes running on the browser and close it by using: ... fg is a command used to bring a background process to the foreground. Then, we can simply stop the process by using Ctrl+C:
🌐
DigitalOcean
digitalocean.com › community › tutorials › how-to-use-bash-s-job-control-to-manage-foreground-and-background-processes
How To Use Bash's Job Control to Manage Foreground and Background Processes | DigitalOcean
September 25, 2021 - This operates on your most recently ... To specify a different job, use its job number: ... Once a job is in the foreground, you can kill it with CTRL + C, let it complete, or suspend and move it to the background again....
🌐
Narkive
comp.unix.solaris.narkive.com › 6OrEhc0m › how-to-send-control-break-or-ctrl-c-signal-to-background-process
how to send control-break or ctrl-c signal to background process?
This is short for "kill -INT 1234", which sends a SIGINT signal to the process. As it turns out, this just what happens to the process if you hit Control-C. The terminal driver intercepts the Control-C character and sends a SIGINT to "all foreground processes".