(Inspired by Gilles's answer)
With the ISIG
flag set, the only way for the Child
script to get SIGINT
without its parent getting SIGINT
is for it to be in its own process group. This can be accomplished with the set -m
option.
If you turn on the -m
option in the Child
shell script, it will perform job control without being interactive. This will cause it to run stuff in a separate process group, preventing the parent from receiving the SIGINT
when the INTR
character is read.
Here is the POSIX description of the -m
option:
-m
This option shall be supported if the implementation supports the User Portability Utilities option. All jobs shall be run in their own process groups. Immediately before the shell issues a prompt after completion of the background job, a message reporting the exit status of the background job shall be written to standard error. If a foreground job stops, the shell shall write a message to standard error to that effect, formatted as described by the jobs utility. In addition, if a job changes status other than exiting (for example, if it stops for input or output or is stopped by a SIGSTOP signal), the shell shall write a similar message immediately prior to writing the next prompt. This option is enabled by default for interactive shells.
The -m
option is similar to -i
, but it doesn't alter the shell's behavior nearly as much as -i
does.
Example:
the Parent
script:
#!/bin/sh
trap 'echo "PARENT: caught SIGINT; exiting"; exit 1' INT
echo "PARENT: pid=$$"
echo "PARENT: Spawning child..."
./Child
echo "PARENT: child returned"
echo "PARENT: exiting normally"
the Child
script:
#!/bin/sh -m
# ^^
# notice the -m option above!
trap 'echo "CHILD: caught SIGINT; exiting"; exit 1' INT
echo "CHILD: pid=$$"
echo "CHILD: hit enter to exit"
read foo
echo "CHILD: exiting normally"
This is what happens when you hit Control+C while Child
is waiting for input:
$ ./Parent
PARENT: pid=12233
PARENT: Spawning child...
CHILD: pid=12234
CHILD: hit enter to exit
^CCHILD: caught SIGINT; exiting
PARENT: child returned
PARENT: exiting normally
Notice how the parent's SIGINT
handler is never executed.
Alternatively, if you'd rather modify Parent
instead of Child
, you can do this:
the Parent
script:
#!/bin/sh
trap 'echo "PARENT: caught SIGINT; exiting"; exit 1' INT
echo "PARENT: pid=$$"
echo "PARENT: Spawning child..."
sh -m ./Child # or 'sh -m -c ./Child' if Child isn't a shell script
echo "PARENT: child returned"
echo "PARENT: exiting normally"
the Child
script (normal; no need for -m
):
#!/bin/sh
trap 'echo "CHILD: caught SIGINT; exiting"; exit 1' INT
echo "CHILD: pid=$$"
echo "CHILD: hit enter to exit"
read foo
echo "CHILD: exiting normally"
Alternative ideas
- Modify the other processes in the foreground process group to ignore
SIGINT
for the duration of Child
. This doesn't address your question, but it may get you what you want.
- Modify
Child
to:
- Use
stty -g
to back up the current terminal settings.
- Run
stty -isig
to not generate signals with the INTR
, QUIT
, and SUSP
characters.
- In the background, read the terminal input and send the signals yourself as appropriate (e.g., run
kill -QUIT 0
when Control+\ is read, kill -INT $$
when Control+C is read). This is not trivial, and it may not be possible to get this to work smoothly if the Child
script or anything it runs is meant to be interactive.
- Restore the terminal settings before exiting (ideally from a trap on
EXIT
).
- Same as #2 except rather than running
stty -isig
, wait for the user to hit Enter or some other non-special key before killing Child
.
Write your own setpgid
utility in C, Python, Perl, etc. that you can use to call setpgid()
. Here's a crude C implementation:
#define _XOPEN_SOURCE 700
#include <unistd.h>
#include <signal.h>
int
main(int argc, char *argv[])
{
// todo: add error checking
void (*backup)(int);
setpgid(0, 0);
backup = signal(SIGTTOU, SIG_IGN);
tcsetpgrp(0, getpid());
signal(SIGTTOU, backup);
execvp(argv[1], argv + 1);
return 1;
}
Example usage from Child
:
#!/bin/sh
[ "${DID_SETPGID}" = true ] || {
# restart self after calling setpgid(0, 0)
exec env DID_SETPGID=true setpgid "$0" "$@"
# exec failed if control reached this point
exit 1
}
unset DID_SETPGID
# do stuff here
-m
trick doesn't seem to work withksh
(2020.0.0). Interestingly, if your shell isdash
(0.5.10.2), and you putset +m
in the Child's shell (even if it was originally called with-m
), it will change the process group to match the parent's process group. But if your shell isbash
(5.0.17), it does not do that. – jarno Oct 25 '20 at 11:50set -e
in the Child. – jarno Oct 25 '20 at 11:54