Bash – Why does Bash ignore SIGINT if its currently running child handles it

bashsignals

The following Bash loop stops if I interrupt it with ^C:

(while true; do sleep 5; done)

The following, however, does not:

(while true; do aplay test.wav; done)

The difference, from what I can tell, is that aplay catches and handles SIGINT, whereas sleep does not.

Why is this? Does POSIX specify this behavior somewhere (I notice that Dash does the same thing), and if so, why? I cannot quite understand why this behavior is desirable. In fact, how can Bash even tell the difference? I must admit I'm not aware of any mechanism (other than ptrace or /proc/ hacking, at least) by which one process can tell how another one handles signals.

Also, is there a way to counteract it? I notice that not even a trap 'exit 0' SIGINT before the loop helps.

(EDIT: Running the loops in a subshell is important, because otherwise the parent shell does not receive the SIGINT.)

Best Answer

The problem is well explained here, as WCE wait and cooperative exit and I encourage you to read it. Basically, your signal is received by all foreground processes, ie the shell and the program, sleep or aplay. The program exits with return code 130 if it does not handle the signal. The shell does a wait for the child to end, and sees this, and the fact that it got the signal too, so exits.

When the program captures the signal, it often just exits with code 1 (as with aplay). When the shell waits for the child, it sees that it did not end due to the signal and so has to assume the signal was a normal aspect of the program's working, so the shell carries on as normal.

For your example, the best way to handle aplay is to check its return code for non-zero and stop:

(while aplay test.wav; do :; done)

The above-mentioned article goes on to explain that a well-behaved program that wants to trap sigint to do some cleanup, should then disable its handler, and re-kill itself in order to get the correct exit flags set.

Related Question