4

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...

2
  • That's not the default behavior. Have you set a trap? Can you reproduce it with zsh -f? Apr 4, 2013 at 0:43
  • @Gilles: I've updated my post in response to your comment.
    – kjo
    Apr 4, 2013 at 17:58

2 Answers 2

5

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.

1
  • Thanks, that definitely sheds light on the situation, but there must be more to the issue, because I don't observe the same behavior under all types of Unix. Please see the latest EDIT to my question.
    – kjo
    Apr 4, 2013 at 17:59
1

Basically, you can not send signals to a specific shell function. Functions are a convenience tool to compose blocks of procedural logic. This is similar, but not equivalent to shell scripts.

When running a shell script, the currently running shell/command line interpreter forks, making the child shell/interpreter execute the script. Forking implies creation of a brand new process. That process can be sent arbitrary signals.

When running a shell function, there will be no (implicit) fork, hence no dedicated process, hence no target that a signal could be sent to.

When you hit Ctrl-C in the terminal, and the busy process is the shell itself, it just stops what it is currently doing. In your example it is not even easy to tell if the sleep receives the signal (so that the function exits because it reaches it's end) or if the function is stopped executing.

$ domuch () { for (( i=0; i<1000000; i++)) {} }

will anyway also be stopped by Ctrl-C, and there is definitely nothing a signal could be sent to.

If you want to send a signal, you must involve a sub-shell, either by putting your lines in a script, or by using backticks (no idea how to display them here :-/) or the alternative $() notation:

$ doless () { $(sleep 1000) }

You can make use of the $! special parameter which tells you the PID of the most recently started background process, but only in the same shell, then you can go:

$ doless &
$ kill -int $!
[1] 30769
[1]  + interrupt  dosome

Still it's not the function that receives the signal, but sleep does.

Interestingly enough, if you dispatch sleep to the background from within the function as in

dotoomuch()  () { $(sleep 1000) & }

you will not be able to interrupt it by means if Ctrl-C, though it's blocking the terminal. No idea whether this is desired behaviour (zsh 4.3.10).

2
  • You can use subshell notation: ( sleep 1000 ).
    – user26112
    Aug 16, 2013 at 22:53
  • I don't think $(sleep 1000) & does what you think. The $(cmd) form means "run cmd and substitute its output into the command line". For example, (which /bin/ls) prints "/bin/ls", but $(which /bin/ls) runs /bin/ls in the current dir. So, your $(sleep 1000) & sleeps for 1000 seconds, then inserts the output of sleep 1000, i.e. the empty string, and runs the empty command in the background.
    – ntc2
    Nov 29, 2017 at 23:07

You must log in to answer this question.

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