Shell – Preventing propagation of SIGINT to Parent Process

aixkshshell-scriptsignalstrap:

Considering a scenario where a Parent program (could be a C++ program or a Shell Script) executes a Child shell script, when we hit Control+C (or whatever character is configured to be the INTR character) while the Child Shell Script is executing, a SIGINT is sent to all processes in the foreground process group. This includes the parent process.

Source : POSIX.1-2008 XBD section 11.1.9

Is there a way to override this default behavior? That the CHILD Process alone handles the SIGNAL without it propagating to the parent?

Reference : Stack Overflow Post – Parent Process not Completing when Child is Interrupted (TRAP INT)

Best Answer

(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

  1. 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.
  2. Modify Child to:
    1. Use stty -g to back up the current terminal settings.
    2. Run stty -isig to not generate signals with the INTR, QUIT, and SUSP characters.
    3. 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.
    4. Restore the terminal settings before exiting (ideally from a trap on EXIT).
  3. 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.
  4. 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
    
Related Question