POSIX Process Groups, Sessions & Job Control
In Unix and POSIX operating systems, processes are organized into a strict three-tier hierarchy: Processes, Process Groups, and Sessions.
This hierarchy is the architectural foundation of terminal job control, shell pipelines (cat file | grep pattern | wc -l), signal distribution (Ctrl+C), and daemon execution.
┌────────────────────────────────────────────────────────────────────────┐
│ SESSION (SID = 100) │
│ Attached to Controlling Terminal (/dev/pts/0) │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ FOREGROUND Process Group (PGID = 200) │ │
│ │ [Process 200 (Leader)] ──pipe──► [Process 201] │ │
│ │ (Receives keyboard signals: Ctrl+C -> SIGINT, Ctrl+Z -> SIGTSTP)│ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ BACKGROUND Process Group (PGID = 100 - Shell) │ │
│ │ [Process 100 (bash)] │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ BACKGROUND Process Group (PGID = 300) │ │
│ │ [Process 300 (sleep 100 &)] │ │
│ │ (Generates SIGTTIN if it attempts to read from the terminal) │ │
│ └───────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
The Three Identifiers: PID, PGID, and SID
| Identifier | Concept | Acquisition / Modification | Role |
|---|---|---|---|
| PID (Process ID) | Individual process identity | Assigned by kernel on fork() |
Tracks resources, thread groups, and lifecycle. |
| PGID (Process Group ID) | Group of coordinating processes | getpgrp(), setpgid(pid, pgid) |
Targets signals to an entire pipeline (kill(-pgid, sig)). |
| SID (Session ID) | Collection of process groups | getsid(pid), setsid() |
Binds to a controlling terminal; defines session boundary. |
Process Group Leaders
A process is a Process Group Leader if its PID == PGID. A leader can create a new group or add child processes to its group. A process group remains alive as long as at least one process remains in the group, even if the leader exits.
Session Leaders
A process is a Session Leader if its PID == SID. A session leader can allocate and attach a controlling terminal to the session.
Controlling Terminal & Signal Routing
A session can have at most one controlling terminal (such as /dev/pts/1 or /dev/tty1):
- The Foreground Process Group:
- At any moment, only one process group in the session is the designated foreground group (set via
tcsetpgrp(fd, pgid)). - Only the foreground process group receives input typed into the terminal.
- When special control keys are pressed, the terminal line discipline generates signals directly to the foreground process group:
Ctrl+CsendsSIGINT.Ctrl+\sendsSIGQUIT.Ctrl+ZsendsSIGTSTP(suspending the job).
- At any moment, only one process group in the session is the designated foreground group (set via
- Background Process Groups:
- Background jobs can execute concurrently in user space.
- If a background process attempts to read from the controlling terminal, the kernel suspends it by sending
SIGTTIN. - If a background process attempts to write to the terminal and
TOSTOPis set, the kernel suspends it withSIGTTOU.
SIGHUP and Terminal Disconnection
When an SSH connection drops, a network cable disconnects, or a terminal emulator window closes:
- The terminal driver detects the modem disconnect / carrier drop.
- The kernel delivers a
SIGHUP(Hangup Signal) to the Session Leader. - The session leader terminates and transmits
SIGHUPto all process groups within the session. - Default action for
SIGHUPis immediate termination.
How nohup and disown Work
nohup command: Sets the signal disposition ofSIGHUPtoSIG_IGN(ignore) and redirectsstdout/stderrtonohup.out. When the terminal closes, the process ignores the broadcastedSIGHUP.disown(bash/zsh): Removes the job from the shell's active job table, preventing the shell from sendingSIGHUPto that specific child group when the shell exits.
The Canonical POSIX Daemon Pattern
A true Unix daemon must detach completely from any controlling terminal and session so it cannot be affected by terminal closures or user logouts.
The standard 6-step POSIX daemonization procedure:
#include <fcntl.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
void daemonize(void) {
// Step 1: First fork() - parent exits
// Ensures child is running in background and is NOT a process group leader
pid_t pid = fork();
if (pid < 0) exit(EXIT_FAILURE);
if (pid > 0) exit(EXIT_SUCCESS); // Parent terminates
// Step 2: setsid() - creates a new session
// Detaches from old session and controlling terminal; becomes session leader
if (setsid() < 0) exit(EXIT_FAILURE);
// Ignore SIGHUP
signal(SIGHUP, SIG_IGN);
// Step 3: Second fork() - parent exits
// Guarantees child is NOT a session leader, preventing it from ever
// accidentally re-acquiring a controlling terminal when opening a TTY
pid = fork();
if (pid < 0) exit(EXIT_FAILURE);
if (pid > 0) exit(EXIT_SUCCESS);
// Step 4: Reset file creation mask
umask(0);
// Step 5: Change working directory to root
// Prevents holding a mounted filesystem open (which blocks unmounting)
if (chdir("/") < 0) exit(EXIT_FAILURE);
// Step 6: Close standard descriptors and redirect to /dev/null
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
open("/dev/null", O_RDONLY); // stdin = 0
open("/dev/null", O_WRONLY); // stdout = 1
open("/dev/null", O_WRONLY); // stderr = 2
}
Frequently Asked Questions (FAQ)
What is the difference between a process group and a session in POSIX?
A process group is a collection of processes working on a single task (such as a shell pipeline), while a session is a collection of one or more process groups bound to a single controlling terminal. Process groups allow signals (like SIGINT) to be broadcast to all processes in a pipeline simultaneously, whereas sessions enforce boundaries between terminal login environments.
Why does the classic Unix daemon call fork() twice?
The first fork() ensures the process is not a group leader so setsid() will succeed, while the second fork() ensures the new process is not a session leader, permanently preventing it from reacquiring a controlling terminal. On System V-derived Unix systems, a session leader without a controlling terminal automatically acquires one when opening a TTY device; calling fork() a second time drops session leadership privileges permanently.
Why does closing an SSH session terminate background processes?
When an SSH terminal closes, the kernel sends SIGHUP (Hangup) to the session leader, which automatically broadcasts SIGHUP to every foreground and background process group in that session. If a background process has not ignored SIGHUP (e.g. using nohup or sigaction), the default action kills the process immediately.
Related Topics
- POSIX Signals Architecture & sigaction: Signal delivery, masks, and async-signal safety.
- POSIX Overview: Portability contracts across Unix-like systems.
- Linux - fork() System Call: Process cloning, address spaces, and Copy-On-Write semantics.
