34.6.2 — SIGHUP and Termination of the Controlling Process
Chapter 34 · The Linux Programming Interface · EmbeddedPathashala
Kernel propagation of SIGHUP to the foreground group after controlling process exits
Key Terms in This Section
Controlling process termination Foreground group SIGHUP SIGCONT follow-up Background group not signalled exec replaces controlling process disc_SIGHUP program
The Key Rule
When the controlling process terminates for any reason — not just because of SIGHUP — the kernel performs these actions:
- Disassociates all processes in the session from the controlling terminal.
- Frees the controlling terminal so another session can claim it.
- Sends SIGHUP (and SIGCONT on Linux) to all members of the terminal’s foreground process group.
Important: Only the foreground process group gets this SIGHUP from the kernel. Background process groups do NOT.
SIGCONT note: On Linux, the SIGHUP is followed by a SIGCONT to ensure the processes are resumed if they had been stopped. SUSv3 does not require this, and most other UNIX implementations do not send SIGCONT in this scenario.
Using exec to Replace the Shell as Controlling Process
The exec shell built-in replaces the shell process with a new program. Since the shell was the controlling process, the new program becomes the controlling process. When the terminal window is then closed, the new program (not the shell) receives SIGHUP.
The disc_SIGHUP Experiment
The book uses a program called disc_SIGHUP to demonstrate this. The program:
- Uses
exec ./disc_SIGHUP d s s— the shell execs this program, making it the controlling process. - Creates children:
d= goes to a different PGID,s= stays in same PGID (foreground). - When the terminal is closed, the parent (controlling process) receives SIGHUP and terminates.
- The kernel then sends SIGHUP to the foreground group children (the
schildren). Thedchild (different PGID) does NOT receive it.
Code Example 1 — disc_SIGHUP: Observing Foreground Group Signalling
/* disc_sighup.c
* Reproduces the book's disc_SIGHUP program.
* Parent creates children; some in same PGID (foreground), some in different PGID.
* Use: exec ./disc_sighup d s s > sig.log 2>&1
* Then close the terminal window.
*
* Compile: gcc -D_GNU_SOURCE -o disc_sighup disc_sighup.c
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <sys/wait.h>
static void handler(int sig)
{
/* Print process ID and signal name — UNSAFE but illustrative */
printf("PID %ld: caught signal %d (%s)\n",
(long)getpid(), sig, strsignal(sig));
fflush(stdout);
}
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "Usage: %s {d|s}... [> sig.log 2>&1]\n"
" d = child goes to Different PGID\n"
" s = child Stays in same PGID (foreground)\n",
argv[0]);
return 1;
}
/* Disable stdout buffering so output appears immediately */
setbuf(stdout, NULL);
pid_t parentPid = getpid();
printf("PID of parent process is: %ld\n", (long)parentPid);
printf("Foreground process group ID is: %ld\n",
(long)tcgetpgrp(STDIN_FILENO));
/* Create one child per argument */
for (int j = 1; j < argc; j++) {
pid_t childPid = fork();
if (childPid == -1) { perror("fork"); return 1; }
if (childPid == 0) {
/* --- CHILD --- */
if (argv[j][0] == 'd') {
/* Move to a different (new) process group */
if (setpgid(0, 0) == -1) {
perror("setpgid");
return 1;
}
}
/* Install SIGHUP handler */
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
if (sigaction(SIGHUP, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
alarm(60); /* Die eventually if not signalled */
printf("PID=%ld PGID=%ld (%s PGID)\n",
(long)getpid(), (long)getpgrp(),
(argv[j][0] == 'd') ? "different" : "same");
fflush(stdout);
for (;;) pause(); /* Wait for signals */
}
/* Parent loops to create more children */
}
/* Parent also installs handler and waits */
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
sigaction(SIGHUP, &sa, NULL);
alarm(60);
printf("PID=%ld PGID=%ld (parent/controlling process)\n",
(long)getpid(), (long)getpgrp());
fflush(stdout);
for (;;) pause();
}
/*
* Run: exec ./disc_sighup d s s > sig.log 2>&1
* Then close terminal window. Check sig.log:
*
* PID of parent process is: 12733
* Foreground process group ID is: 12733
* PID=12755 PGID=12755 (different PGID) ← 'd' child
* PID=12756 PGID=12733 (same PGID) ← 's' children
* PID=12757 PGID=12733 (same PGID)
* PID=12733 PGID=12733 (parent)
* PID 12756: caught signal 1 (Hangup) ← foreground group signalled
* PID 12757: caught signal 1 (Hangup)
* (12755 does NOT appear — different PGID, not foreground group)
*/
Code Example 2 — exec Shell Built-in: Making a Program the Controlling Process
/* become_controlling.c
* When launched with "exec ./become_controlling", this program
* REPLACES the shell as the controlling process for the terminal.
* Closing the terminal sends SIGHUP directly to this program.
*
* Compare two ways to run it:
* Normal: ./become_controlling (shell is still controlling process)
* exec: exec ./become_controlling (this program IS the controlling process)
*
* Compile: gcc -o become_controlling become_controlling.c
*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <fcntl.h>
static void handler(int sig)
{
char msg[128];
int n = snprintf(msg, sizeof(msg),
"PID %ld caught signal %d (%s)\n",
(long)getpid(), sig, strsignal(sig));
write(STDOUT_FILENO, msg, n); /* write() is async-signal-safe */
}
int main(void)
{
/* Install handlers for SIGHUP and SIGTERM */
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
printf("PID=%ld PGID=%ld SID=%ld\n",
(long)getpid(), (long)getpgrp(), (long)getsid(0));
printf("I am%s the session leader\n",
(getpid() == getsid(0)) ? "" : " NOT");
/* Open /dev/tty to verify controlling terminal */
int tty = open("/dev/tty", O_RDONLY);
if (tty == -1) {
printf("No controlling terminal!\n");
} else {
printf("Controlling terminal acquired. tcgetsid=%ld\n",
(long)tcgetsid(tty));
close(tty);
}
printf("\nWaiting... close terminal or send kill -HUP %ld\n",
(long)getpid());
fflush(stdout);
alarm(120);
for (;;) pause();
}
/* Run with exec:
* $ exec ./become_controlling
* PID=9000 PGID=9000 SID=9000
* I am the session leader
* Controlling terminal acquired.
* Waiting...
* (close terminal)
* PID 9000 caught signal 1 (Hangup)
*/
Interview Questions — Section 34.6.2
Q1. When the controlling process terminates, which process groups receive SIGHUP from the kernel?
Only the foreground process group of the controlling terminal receives SIGHUP from the kernel. Background process groups in the same session do NOT receive SIGHUP. This is a key distinction — the shell’s SIGHUP forwarding (Section 34.6.1) is separate from the kernel’s SIGHUP to the foreground group.
Q2. Is the kernel SIGHUP to the foreground group triggered only by SIGHUP? Explain.
No. The kernel sends SIGHUP to the foreground group whenever the controlling process terminates, regardless of the cause. Whether the controlling process terminates due to SIGHUP, SIGKILL, SIGSEGV, or a normal exit(), the kernel’s SIGHUP to the foreground group always follows. It is a consequence of controlling process termination, not of SIGHUP specifically.
Q3. What does the exec shell built-in do differently from running a command normally?
The exec built-in replaces the shell process itself with the new program using execve(). Since the shell was the controlling process (session leader), after exec completes, the new program is now the controlling process. If the terminal is closed, the new program directly receives SIGHUP from the kernel. In contrast, running a command normally makes the command a child of the shell — the shell remains the controlling process.
Q4. A child process moves to a different PGID using setpgid(0,0). The parent (controlling process) then terminates when the terminal is closed. Does the child receive SIGHUP?
No. The kernel only sends SIGHUP to the foreground process group. If the child moved to a different PGID, it is no longer in the foreground group, so it does not receive the kernel’s SIGHUP. This is confirmed by the disc_SIGHUP experiment where the ‘d’ child (different PGID) did not receive SIGHUP.
