12

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)

Guddu
  • 233

3 Answers3

11

(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
    
  • The -m trick doesn't seem to work with ksh (2020.0.0). Interestingly, if your shell is dash (0.5.10.2), and you put set +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 is bash (5.0.17), it does not do that. – jarno Oct 25 '20 at 11:50
  • There are also differences in which trap is called, if you have set -e in the Child. – jarno Oct 25 '20 at 11:54
  • What's the zsh version? – Niing Mar 08 '22 at 18:20
4

As the chapter you cite from POSIX explains, SIGINT is sent to the whole foreground process group. Therefore, to avoid killing the program parent, arrange for it to run in its own process group.

Shells don't give access to setpgrp via a builtin or syntactic construct, but there's an indirect way to achieve it, which is to run the shell interactively. (Thanks to Stéphane Gimenez for the trick.)

ksh -ic '
  … the part that needs to be interruptible without bothering the parent …
'
  • +1 for the clever idea, although beware that the shell's behavior changes when it is an interactive shell (at least the POSIX shell does; I'm not familiar with the details of ksh). Examples: ${ENV} is sourced, the shell won't immediately exit when it encounters an error, SIGQUIT and SIGTERM are ignored. – Richard Hansen Jun 29 '13 at 23:06
1

Well, from the Stack Overflow question you referenced it clearly states that the parent needs to be configured to handle the signal.

Secondly, the POSIX reference clearly states that "If ISIG is set, the INTR character shall be discarded when processed".

So that's two options. The third would be to run the child in its own process group.

bahamat
  • 39,666
  • 4
  • 75
  • 104
  • Thanks for your response. I also need to figure out a way to have the user exit from the script. Is there a way to set ISIG and at the same time allow for some CONTRL Key combination (CTRL-Q may be) to exit out of the shell script without sending signals to parent and likewise? Also, could you please tell me how could I run the child in a different process group than its parent? – Guddu Jun 28 '13 at 05:42