Handling of SIGHUP by the Shell

 

34.6.1 — Handling of SIGHUP by the Shell
Chapter 34 · The Linux Programming Interface · EmbeddedPathashala
How bash forwards SIGHUP to jobs, and what nohup / disown do
Key Terms in This Section

Shell SIGHUP handler Job forwarding nohup(1) disown (bash) Process group not created by shell Same session different PGID SIGCONT after SIGHUP

What Does the Shell Do When It Receives SIGHUP?

The login shell is typically the controlling process for the terminal. Most shells (bash, ksh) install a SIGHUP handler. When SIGHUP arrives (e.g., terminal window closed), the handler does the following before terminating:

  1. Sends SIGHUP to each process group the shell created (both foreground and background jobs).
  2. On some shells (bash, ksh), also sends SIGHUP to stopped background jobs if the shell exits normally (logout or Ctrl+D).

Key point: The shell only sends SIGHUP to process groups it created. If a child process created its own process group (moved to a new PGID), the shell does not send SIGHUP to that group — even though it is in the same session.

Experiment from the book: Run catch_SIGHUP (which creates a child that moves to a new PGID) and close the terminal. The parent and same-group child receive SIGHUP; the different-group child does NOT — even though they share the same session.

The catch_SIGHUP Experiment Explained

The book’s catch_SIGHUP program creates a parent and child. Optionally, the child moves to its own process group. Here is what happens when the terminal is closed:

Scenario What happens
Parent and child in same PGID (shell’s job) Both receive SIGHUP from the shell
Child moved to a different PGID Parent receives SIGHUP; child does NOT (shell didn’t create that group)

nohup and disown — Protecting Processes from SIGHUP

nohup(1) command: Runs a command with SIGHUP set to SIG_IGN. The process survives terminal close and shell exit. Output is redirected to nohup.out if stdout is a terminal.

disown (bash built-in): Removes a job from bash’s job table. Bash does not send SIGHUP to disowned jobs when the shell exits — the job is simply forgotten by the shell.

nohup myprogram & — immune to SIGHUP
disown %1 — remove job 1 from bash’s table

Code Example 1 — catch_SIGHUP: Same Group vs Different Group

/* catch_sighup.c
 * Reproduces the book's catch_SIGHUP program.
 * Creates a parent + child.
 * If a command-line argument is given, child moves to a new PGID.
 * Run in background, close terminal, observe who receives SIGHUP.
 *
 * Compile: gcc -o catch_sighup catch_sighup.c
 * Run (same group): ./catch_sighup > samegroup.log 2>&1 &
 * Run (diff group): ./catch_sighup x > diffgroup.log 2>&1 &
 * Then close terminal and: cat samegroup.log / cat diffgroup.log
 */
#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

static void handler(int sig)
{
    /* Empty — just catches the signal so process doesn't terminate */
    (void)sig;
}

int main(int argc, char *argv[])
{
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = handler;

    /* Install SIGHUP handler */
    if (sigaction(SIGHUP, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    pid_t childPid = fork();
    if (childPid == -1) { perror("fork"); return 1; }

    if (childPid == 0) {
        /* --- CHILD --- */
        /* If argument given, move to own process group */
        if (argc > 1) {
            if (setpgid(0, 0) == -1) {
                perror("setpgid");
                return 1;
            }
        }
    }

    /* Parent and child both reach here */
    printf("PID=%ld; PPID=%ld; PGID=%ld; SID=%ld\n",
           (long)getpid(),
           (long)getppid(),
           (long)getpgrp(),
           (long)getsid(0));
    fflush(stdout);

    alarm(60); /* Ensure termination if not signalled */

    for (;;) {
        pause(); /* Wait for signals */
        printf("%ld: caught SIGHUP\n", (long)getpid());
        fflush(stdout);
    }
}
/*
 * samegroup.log (no argument):
 *   PID=5612; PPID=5611; PGID=5611; SID=5533  ← child (same group)
 *   PID=5611; PPID=5533; PGID=5611; SID=5533  ← parent
 *   5611: caught SIGHUP                         ← parent got it
 *   5612: caught SIGHUP                         ← child got it too
 *
 * diffgroup.log (with argument):
 *   PID=5614; PPID=5613; PGID=5614; SID=5533  ← child (diff PGID!)
 *   PID=5613; PPID=5533; PGID=5613; SID=5533  ← parent
 *   5613: caught SIGHUP                         ← parent got it
 *   (child did NOT get it — shell never created PGID 5614)
 */

Code Example 2 — Programmatic nohup: SIG_IGN before exec

/* my_nohup.c
 * Simplified implementation of the nohup(1) command.
 * Sets SIGHUP to SIG_IGN, then execs the given program.
 * Compile: gcc -o my_nohup my_nohup.c
 * Usage: ./my_nohup sleep 100
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <command> [args...]\n", argv[0]);
        return 1;
    }

    /* Step 1: Ignore SIGHUP — this disposition is preserved across exec() */
    if (signal(SIGHUP, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return 1;
    }

    /* Step 2: If stdout is a terminal, redirect to nohup.out */
    if (isatty(STDOUT_FILENO)) {
        fprintf(stderr, "Appending output to nohup.out\n");
        int fd = open("nohup.out",
                      O_WRONLY | O_CREAT | O_APPEND, 0600);
        if (fd == -1) {
            perror("open nohup.out");
            return 1;
        }
        if (dup2(fd, STDOUT_FILENO) == -1) {
            perror("dup2");
            return 1;
        }
        close(fd);
    }

    /* Step 3: exec the requested command
     * SIG_IGN for SIGHUP is preserved across execv() per POSIX */
    execvp(argv[1], &argv[1]);
    perror("execvp"); /* Only reached on error */
    return 127;
}
/* Test:
 *   ./my_nohup bash -c 'for i in 1 2 3 4 5; do sleep 2; echo tick $i; done'
 *   (close terminal)
 *   cat nohup.out  → still prints tick 1 through tick 5
 */

Interview Questions — Section 34.6.1

Q1. When the shell receives SIGHUP, which process groups does it forward SIGHUP to?
The shell forwards SIGHUP to each process group that the shell itself created — both foreground and background jobs. It does NOT send SIGHUP to process groups that were not created by the shell, even if those groups are in the same session. This is why moving a child to its own PGID (with setpgid(0,0)) can protect it from the shell’s SIGHUP forwarding.
Q2. How does nohup(1) protect a process from SIGHUP?
nohup sets the disposition of SIGHUP to SIG_IGN before executing the command via exec(). Signal dispositions set to SIG_IGN are preserved across exec(), so the child process and all its descendants inherit the SIG_IGN disposition for SIGHUP and will never be killed by it.
Q3. What is the difference between nohup and disown in bash?
nohup is an external command that sets SIGHUP to SIG_IGN before exec’ing the command — the process itself ignores SIGHUP. disown is a bash built-in that removes a job from bash’s internal job table — bash simply forgets about the job and will not send it SIGHUP when the shell exits, but the process itself still has its original SIGHUP disposition.
Q4. A child calls setpgid(0,0) to move to its own process group. If the terminal is closed, will the child receive SIGHUP?
No, not from the shell. The shell only sends SIGHUP to process groups it created. Since the child’s new PGID was created by the child, not the shell, the shell does not forward SIGHUP to it. However, if the child is in the foreground process group when the controlling process terminates, the kernel would send SIGHUP directly.

Leave a Reply

Your email address will not be published. Required fields are marked *