Computing History · Unix Lineage
Sessions, Process Groups, and the Controlling Terminal
How the kernel groups processes for signal delivery and job control, why the controlling terminal is a session property, and what happens to a process left in the background.
Signals can be sent to a process, but the interface almost always sends them to a group. The shell’s kill builtin takes a job number, SIGINT from a keyboard is delivered to a foreground group rather than to a single process, and SIGHUP on a terminal hangup reaches every process that shares the terminal’s session. Three nested structures make that possible: the process group, the session, and the controlling terminal. They are kernel-maintained, they determine which processes a signal reaches, and they are the substrate of job control.
The group as the unit of delivery
A process group is a set of processes sharing a group ID. Every process belongs to exactly one group at any time, and setpgid moves a process into another group within the same session or creates a new group whose ID is the caller’s process ID. A process can only move itself or a child that has not yet exec’d, which is what allows a shell to place a pipeline stage into the group it created before the stage runs a program the shell does not control.
The restriction matters. After exec, the parent can no longer change the child’s group, because doing so would let a program interfere with another program’s job control. The shell therefore makes the change twice, once in the parent and once in the child, before the child execs, so that whichever runs first wins and the second call is harmless. This is why a shell’s job-control code contains a pair of setpgid calls around the fork rather than one.
kill with a negative argument sends the signal to every process in the group whose ID is the absolute value. killpg does the same explicitly. Delivery to a group is how a pipeline is interrupted as a unit: the shell sends SIGINT to the foreground group, and every stage receives it, so a pipeline does not leave one stage running while the others die.
The group is also the unit of terminal ownership. The terminal driver does not know about sessions for the purpose of signal generation; it knows about a foreground process group, and when the user types the interrupt character it sends SIGINT to every process in that group. The background group is the rest of the session, and its members are the ones that must stop when they try to read.
The session as the unit of detachment
A session is a set of process groups. setsid creates a new session, makes the calling process its leader, and puts it in a new process group whose ID is the process’s ID. The call fails with EPERM if the caller is already a group leader, because a session leader must be a group leader of a group with no other members, and creating a session whose leader is already a leader of a populated group would produce two group IDs for one process.
The session is what a login establishes. When a user logs in on a terminal, the login program creates a session, sets the terminal as the session’s controlling terminal, and starts a shell in the foreground group. Every process the user starts inherits the session, so a SIGHUP sent to the session reaches all of them, and a hangup of the terminal is delivered as SIGHUP to the foreground group and, by convention, to the session leader.
setsid is the mechanism behind daemons. A process that calls setsid leaves its old session, and if it had a controlling terminal, it loses it. The new session has no controlling terminal, which means terminal-generated signals cannot reach it and a hangup of the terminal it was started from does not kill it. The double-fork idiom is the way to call setsid from a process that is a group leader: fork, let the child call setsid, and let the parent exit, so the child is not a group leader and the call succeeds.
#include <stdio.h>
#include <unistd.h>
int detach(void) {
pid_t pid = fork();
if (pid < 0) return -1;
if (pid > 0) _exit(0); /* parent returns to the shell immediately */
if (setsid() < 0) return -1; /* child is not a group leader: succeeds */
return 0; /* no controlling terminal from here on */
}
The daemon still holds descriptors 0, 1 and 2 open on the old terminal unless it closes or redirects them. setsid removes the controlling terminal, not the open descriptors, so a daemon that keeps them can still write to a terminal it no longer controls, and a terminal that is closed leaves writes failing with EIO. This is why the conventional daemon sequence redirects the three descriptors to /dev/null.
The controlling terminal and the foreground group
A session has at most one controlling terminal, and the association is made when a session leader opens a terminal device that is not already another session’s controlling terminal. On Linux the terminal’s session ID is stored in the driver and the session leader’s record points back at the device. The association is broken by setsid or by the terminal being revoked.
The controlling terminal carries two pieces of state relevant to job control: the session it belongs to, and the foreground process group within that session. tcsetpgrp sets the latter, and it can only be called by a process in the session, which is what prevents an unrelated process from stealing the terminal. The shell calls it after starting a job to make the job’s group the foreground, and calls it again when the job stops to restore its own group.
The terminal driver uses the foreground group in three ways. It sends SIGINT, SIGQUIT and SIGTSTP to the foreground group when the corresponding characters are typed. It sends SIGTTIN to a background group that tries to read, and SIGTTOU to a background group that tries to write, when the terminal’s TOSTOP flag is set. And it discards input that arrives while no process is in the foreground group of a session with a controlling terminal, or delivers it to the foreground group once one exists.
Each of these is an ordinary signal by the time it reaches a process, subject to the delivery rules described in the signals article: the disposition is per process, a handler runs on the interrupted thread’s stack, and the default action is stop for the job-control signals and terminate for the interrupt and quit signals. The terminal driver contributes only the choice of recipients, which is why a program can install a handler for SIGINT and continue after an interrupt character, but cannot install one for SIGKILL.
SIGTTIN’s default action is to stop the process. That is what makes a background job that reads from the terminal pause instead of consuming the shell’s input, and it is why a program run with & that reads from standard input appears to hang: it has been stopped, and the shell reports it as stopped. The fix is either to bring it to the foreground with fg or to redirect its input away from the terminal. SIGTTOU behaves the same way for writes when TOSTOP is set; without TOSTOP, a background process can write to the terminal, which is why output from background jobs interleaves with the shell’s prompt.
A process can avoid the stop signal by ignoring SIGTTIN or SIGTTOU, in which case the read or write returns EIO instead. That is the documented behaviour on Linux, and it is the reason a program that wants to read the terminal from the background must both ignore the signal and accept that it may fail.
Stopping, continuing, and the shell’s bookkeeping
SIGTSTP from the keyboard stops the foreground group. A stopped process remains in the group and the session, consumes no CPU, and can be continued with SIGCONT. The shell learns of the stop through waitpid with the WUNTRACED flag, which reports stopped children as well as exited ones, and it then restores itself as the foreground group.
The status word returned by waitpid distinguishes the cases: WIFEXITED for a normal exit, WIFSIGNALED for death by signal, WIFSTOPPED for a stop, and WIFCONTINUED for a continue reported under WCONTINUED. The shell must handle all four, because a job that is stopped and later continued produces two status transitions for one process, and a job that is stopped and then killed produces a stop followed by a signal death.
The group’s identity persists across these transitions. A stopped group can be continued, and a continued group can be stopped again, and the terminal’s foreground group can be switched to it each time. What does not persist is the group’s membership if a member exits: the group exists as long as at least one process is in it, and the group ID is not reused while any process remains.
The orphaned group is the edge case that the kernel handles specially. A group is orphaned when no process in it has a parent in a different group of the same session. A member of an orphaned group that stops is sent SIGHUP followed by SIGCONT, so that it cannot remain stopped forever with no shell to resume it. This is why a pipeline left in the background after its shell exits does not simply freeze: the kernel terminates it. The rule exists because a stopped process in an orphaned group has no one to continue it, and the alternative is a permanently stuck job holding resources.
Where the model leaks
The three structures are per-session, and a process cannot see another session’s groups or terminal. That is a feature for isolation and a limitation for supervision: a process that wants to manage jobs across sessions must work with process IDs, which are global, rather than with the foreground-group machinery, which is per terminal. Group delivery itself is not restricted to the caller’s session; what restricts it is permission. POSIX specifies that a signal sent to a process group is delivered to every member if the sender has permission to signal at least one of them, so a sender that can reach a single member reaches all of them, which is either convenient or alarming depending on which member the sender expected to reach.
The controlling terminal is a single association, so a process that opens a second terminal device does not acquire a second controlling terminal; it merely has a descriptor. Multiplexers such as terminal emulators that present many windows over one device must implement their own foreground tracking, because the kernel’s notion of a foreground group is per device rather than per window.
The structures also interact badly with containers. A container’s init process is typically a session leader with no controlling terminal, and job control inside the container works only if the container runtime allocates a pseudo-terminal and makes it the controlling terminal of the session. Without that, programs that call tcsetpgrp fail, and interactive shells inside the container cannot manage jobs. The mechanism is the same one described above; what changes is who is allowed to establish the association.
Finally, the identifiers are small integers with no generation counter. A process group ID is a process ID, and process IDs are reused once the last process holding the number exits; a group ID is reused once the last member leaves. A stored group ID is therefore valid only while a member exists, and there is no way to ask whether a number still refers to the group that was meant. Code that remembers a group ID across a period in which the group may have dissolved is relying on reuse not having happened yet. The same property holds for process IDs, and it is the reason waitpid must be given the specific child rather than a wildcard when the program cares which child exited.
The signal mask completes the picture. A process that blocks SIGTSTP cannot be stopped by the terminal’s suspend character, because the signal is delivered to the foreground group but remains pending in the process that blocked it. The shell reports the job as running, the keyboard appears to do nothing, and only SIGSTOP, which cannot be blocked, will suspend it. The kernel permits this because the mask is the process’s own declaration of which signals it is prepared to handle, and a process that has declared it will not act on a stop request has effectively opted out of job control.
References
- The Open Group Base Specifications Issue 7, System Interfaces: setsid — POSIX.1-2017, IEEE Std 1003.1
- The Open Group Base Specifications Issue 7, System Interfaces: tcsetpgrp — POSIX.1-2017, IEEE Std 1003.1
- Linux man-pages: tty_ioctl(4) — Michael Kerrisk, man-pages project
- Advanced Programming in the UNIX Environment — W. Richard Stevens and Stephen A. Rago; Addison-Wesley