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):

  1. 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+C sends SIGINT.
      • Ctrl+\ sends SIGQUIT.
      • Ctrl+Z sends SIGTSTP (suspending the job).
  2. 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 TOSTOP is set, the kernel suspends it with SIGTTOU.

SIGHUP and Terminal Disconnection

When an SSH connection drops, a network cable disconnects, or a terminal emulator window closes:

  1. The terminal driver detects the modem disconnect / carrier drop.
  2. The kernel delivers a SIGHUP (Hangup Signal) to the Session Leader.
  3. The session leader terminates and transmits SIGHUP to all process groups within the session.
  4. Default action for SIGHUP is immediate termination.

How nohup and disown Work

  • nohup command: Sets the signal disposition of SIGHUP to SIG_IGN (ignore) and redirects stdout/stderr to nohup.out. When the terminal closes, the process ignores the broadcasted SIGHUP.
  • disown (bash/zsh): Removes the job from the shell's active job table, preventing the shell from sending SIGHUP to 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