10

I have the following in a script:

yes >/dev/null &
pid=$!
echo $pid
sleep 2
kill -INT $pid
sleep 2
ps aux | grep yes

When I run it, the output shows that yes is still running by the end of the script. However, if I run the commands interactively then the process terminates successfully, as in the following:

> yes >/dev/null &
[1] 9967
> kill -INT 9967
> ps aux | grep yes
sean ... 0:00 grep yes

Why does SIGINT terminate the process in the interactive instance but not in the scripted instance?

EDIT

Here's some supplementary information that may help to diagnose the issue. I wrote the following Go program to simulate the above script.

package main

import (
    "fmt"
    "os"
    "os/exec"
    "time"
)

func main() {
    yes := exec.Command("yes")
    if err := yes.Start(); err != nil {
        die("%v", err)
    }

    time.Sleep(time.Second*2)

    kill := exec.Command("kill", "-INT", fmt.Sprintf("%d", yes.Process.Pid))
    if err := kill.Run(); err != nil {
        die("%v", err)
    }

    time.Sleep(time.Second*2)

    out, err := exec.Command("bash", "-c", "ps aux | grep yes").CombinedOutput()
    if err != nil {
        die("%v", err)
    }
    fmt.Println(string(out))
}

func die(msg string, args ...interface{}) {
    fmt.Fprintf(os.Stderr, msg+"\n", args...)
    os.Exit(1)
}

I built it as main and running ./main in a script, and running ./main and ./main & interactively give the same, following, output:

sean ... 0:01 [yes] <defunct>
sean ... 0:00 bash -c ps aux | grep yes
sean ... 0:00 grep yes

However, running ./main & in a script gives the following:

sean ... 0:03 yes
sean ... 0:00 bash -c ps aux | grep yes
sean ... 0:00 grep yes

This makes me believe that the difference has less to do on Bash's own job control, though I'm running all of this in a Bash shell.

eZanmoto
  • 385
  • Unable to replicate; behavior was identical when interactive or run in a script in my test. – DopeGhoti Jun 21 '17 at 17:57
  • Able to replicate here - yes terminates immediately with interactive kill but stays running with the script. – Joe P Jun 21 '17 at 18:01
  • 1
    What shell are you using? – thrig Jun 21 '17 at 18:02
  • Replicated with GNU bash, version 4.3.11(1)-release (x86_64-pc-linux-gnu) - what shell and version do you have? – Joe P Jun 21 '17 at 18:02
  • The scripted instance is running the background command with SIGINT and SIGQUIT ignored for some reason. – Petr Skocik Jun 21 '17 at 18:25
  • I'm running GNU bash, version 4.3.46(1)-release (x86_64-pc-linux-gnu) on Linux ubuntu-512mb-lon1-01 4.4.0-64-generic #85-Ubuntu SMP Mon Feb 20 11:50:30 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux, a DigitalOcean box running Ubuntu 16.04.2 LTS. I've also been able to replicate the behaviour in the official debian:8.6 docker image. Confirming with more images could indicate a kernel-related reason. – eZanmoto Jun 21 '17 at 18:44

2 Answers2

6

What shell is used is a concern as different shells handle job control differently (and job control is complicated; job.c in bash presently weighs in at 3,300 lines of C according to cloc). pdksh 5.2.14 versus bash 3.2 on Mac OS X 10.11 for instance show:

$ cat code
pkill yes
yes >/dev/null &
pid=$!
echo $pid
sleep 2
kill -INT $pid
sleep 2
pgrep yes
$ bash code
38643
38643
$ ksh code
38650
$ 

Also relevant here is that yes performs no signal handling so inherits whatever there is to be inherited from the parent shell process; if by contrast we do perform signal handling—

$ cat sighandlingcode 
perl -e '$SIG{INT} = sub { die "ouch\n" }; sleep 5' &
pid=$!
sleep 2
kill -INT $pid
$ bash sighandlingcode 
ouch
$ ksh sighandlingcode 
ouch
$ 

—the SIGINT is triggered regardless the parent shell, as perl here unlike yes has changed the signal handling. There are system calls relevant to signal handling which can be observed with things like DTrace or here strace on Linux:

-bash-4.2$ cat code
pkill yes
yes >/dev/null &
pid=$!
echo $pid
sleep 2
kill -INT $pid
sleep 2
pgrep yes
pkill yes
-bash-4.2$ rm foo*; strace -o foo -ff bash code
21899
21899
code: line 9: 21899 Terminated              yes > /dev/null
-bash-4.2$ 

We find that the yes process ends up with SIGINT ignored:

-bash-4.2$ egrep 'exec.*yes' foo.21*
foo.21898:execve("/usr/bin/pkill", ["pkill", "yes"], [/* 24 vars */]) = 0
foo.21899:execve("/usr/bin/yes", ["yes"], [/* 24 vars */]) = 0
foo.21903:execve("/usr/bin/pgrep", ["pgrep", "yes"], [/* 24 vars */]) = 0
foo.21904:execve("/usr/bin/pkill", ["pkill", "yes"], [/* 24 vars */]) = 0
-bash-4.2$ grep INT foo.21899
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f18ebee0250}, {SIG_DFL, [], SA_RESTORER, 0x7f18ebee0250}, 8) = 0
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f18ebee0250}, {SIG_DFL, [], SA_RESTORER, 0x7f18ebee0250}, 8) = 0
rt_sigaction(SIGINT, {SIG_IGN, [], SA_RESTORER, 0x7f18ebee0250}, {SIG_DFL, [], SA_RESTORER, 0x7f18ebee0250}, 8) = 0
--- SIGINT {si_signo=SIGINT, si_code=SI_USER, si_pid=21897, si_uid=1000} ---
-bash-4.2$ 

Repeat this test with the perl code and one should see that SIGINT is not ignored, or also that under pdksh there is no ignore being set as there is in bash. With "monitor mode" turned on like it is in interactive mode in bash, yes is killed.

-bash-4.2$ cat monitorcode 
#!/bin/bash
set -m
pkill yes
yes >/dev/null &
pid=$!
echo $pid
sleep 2
kill -INT $pid
sleep 2
pgrep yes
pkill yes
-bash-4.2$ ./monitorcode 
22117
[1]+  Interrupt               yes > /dev/null
-bash-4.2$ 
thrig
  • 34,938
  • 2
    I tested this with bash,dash,ksh,posh,pdksh,sh,yash, and zsh, and all except posh ignore SIGINT and SIGQUIT in their backgrounded processes in noninteractive mode. I wonder what the historical reason for this is (tried asking here https://stackoverflow.com/questions/45106725/why-do-shells-ignore-sigint-and-sigquit-in-backgrounded-processes). – Petr Skocik Jul 14 '17 at 17:05
3

Background jobs are not supposed to be tied to the shell that started them. If you exit a shell, they will continue running. As such they shouldn't be interrupted by SIGINT, not by default. When job control is enabled, that is fulfilled automatically, since background jobs are running in separate process groups. When job control is disabled (generally in non-interactive shells), bash makes the asynchronous commands ignore SIGINT.

The relevant parts of the documentation:

Non-builtin commands started by Bash have signal handlers set to the values inherited by the shell from its parent. When job control is not in effect, asynchronous commands ignore SIGINT and SIGQUIT in addition to these inherited handlers. Commands run as a result of command substitution ignore the keyboard-generated job control signals SIGTTIN, SIGTTOU, and SIGTSTP.

https://www.gnu.org/software/bash/manual/html_node/Signals.html

To facilitate the implementation of the user interface to job control, the operating system maintains the notion of a current terminal process group ID. Members of this process group (processes whose process group ID is equal to the current terminal process group ID) receive keyboard-generated signals such as SIGINT. These processes are said to be in the foreground. Background processes are those whose process group ID differs from the terminal’s; such processes are immune to keyboard-generated signals. Only foreground processes are allowed to read from or, if the user so specifies with stty tostop, write to the terminal. Background processes which attempt to read from (write to when stty tostop is in effect) the terminal are sent a SIGTTIN (SIGTTOU) signal by the kernel’s terminal driver, which, unless caught, suspends the process.

https://www.gnu.org/software/bash/manual/html_node/Job-Control-Basics.html

More on it here.

x-yuri
  • 3,373