Shell – Why is `kill -s INT ` behaving differently from `Ctrl-C`

killshellsignalszsh

Starting with:

% donothing () { echo $$; sleep 1000000 }
% donothing
47139

If at this point I hit Ctrl-C at the same terminal that is controlling the shell, then the function donothing does indeed terminate, and I get the command prompt back.

But if instead, from a different shell session, I run

% kill -s INT 47139

…the donothing function does not terminate. (Ditto if I pass -47139 as the last argument to kill.)

Do I need to use a different PID from the one I'm using to achieve with kill -s INT the same effect that I can achieve interactively with Ctrl-C at the controlling terminal?

(I'm primarily interested in the answer for zsh.)


EDIT: in response to Gilles comment: no, I have not set any traps (though I would be interested to learn how to confirm that no traps are set: is there a way to list all the active traps?); and yes, I can reproduce this behavior with zsh -f. In fact, I can reproduce it under the most extreme way I know to get an absolutely "bare" zsh (please let me know if there's a better way):

% /usr/bin/env -i /usr/bin/zsh -f
% donothing () { echo $$; sleep 1000000 }
12345

…etc.

Since my original post, I have replicated the behavior described above both under Darwin and under NetBSD, but not under Linux (i.e. under Linux kill -s INT <zsh PID> does kill the donothing function defined above, without killing the parent shell in the process).

So maybe this is a BSD-ish thing. More specifically, in every Unix I've tried this with, the sleep process is a child of the zsh process, but only under Linux is the SIGINT signal that was sent to the zsh process propagated to the sleep process.

Therefore, in order to be able to kill the donothing function non-interactively under Darwin and NetBSD I need a way to find the PID of the sleep-ing child, and send the SIGINT to it (which, admittedly, does sound pretty heartless). More generally, to kill a shell function non-interactively under Darwin and NetBSD, I need to recursively/depth-first find the descendants of the zsh process and send them the SIGINT to them…

Best Answer

Control-c from the terminal sends an interrupt signal to the process group associated with the terminal, which includes the sleep process. Sending SIGINT via kill only reaches one process, and in this case it happens to be the wrong process because $$ returns the process ID of the shell, not the sleep process. Shells normally ignore SIGINT.