Operating Systems · Processes and Threads
Process Groups, Sessions, and the Controlling Terminal
How job control is built from two identifiers, why a background job stops when it reads, and how a session leader loses its terminal.
Job control is the part of the process model that most programmers use daily and understand least. Pressing a key that suspends a foreground program, resuming it in the background, and having the shell report the change all depend on two identifiers attached to every process and on a rule about which of them owns the terminal.
Two Identifiers, Nested
Every process has a process group identifier. A process group is a set of processes that receive signals together, created by a process that makes itself a group leader and inherited by everything it forks. The group identifier is the process identifier of the leader, and a group with no members ceases to exist.
Every process also has a session identifier. A session is a set of process groups, and it is created by a call that detaches the caller from its current session and makes it the leader of a new one. The new session begins with one process group containing one process. Forking adds processes to that group, and a process that calls the group-creating call starts a new group within the same session.
The nesting is what gives job control its vocabulary. The shell is a session leader, or is in a session created for it by the terminal emulator or the login process. Each pipeline the shell runs is a process group within that session. The shell puts one group in the foreground and leaves the others in the background, and the terminal’s notion of which group is in the foreground is the state that makes the arrangement work.
The identifiers are inherited across fork, so a process created by a shell pipeline is in the pipeline’s group without doing anything. The inheritance is one line in the list of what the child copies, alongside the descriptor table and signal dispositions described in What fork Copies, and Why Copy-on-Write Defers Almost All of It. The group identifier is not the process identifier except for the leader, which is why a program that wants to signal an entire pipeline sends to the negated group identifier rather than to any individual process.
The Controlling Terminal
A session may have a controlling terminal. The association is established when a session leader that has no controlling terminal opens a terminal device that is not already the controlling terminal of another session; on the POSIX systems the result is fixed, though the standard describes the acquisition as implementation-defined and allows open to pass O_NOCTTY to refuse it explicitly. From that point the terminal belongs to the session, and the session has at most one.
The controlling terminal matters because it is the object that delivers job-control signals. The terminal driver keeps a record of which process group is currently in the foreground. When a key that generates an interrupt is pressed, the driver sends the corresponding signal to every process in the foreground group. When a key that generates a suspend is pressed, the driver sends the stop signal to the same group. The driver does not know which process is reading; it signals the group, and the group’s members decide what to do.
The foreground group is set by the shell with a terminal control call, and the shell restores the previous setting when a job finishes. This is why a background job cannot read from the terminal: when it tries, the terminal driver notices that the calling process is not in the foreground group, sends the background-read signal to that group, and the default action of that signal is to stop the process. The read does not return an error; the process stops, and the shell is notified of the stop.
The same rule applies to writing. A background process that writes to the terminal may be stopped by the background-write signal, depending on a terminal setting that controls whether the signal is generated. The setting exists because a background job that writes to the terminal would interleave its output with the foreground job’s, and the default is to stop it rather than to allow the interleaving.
The alternative behaviour for a background job is to ignore or handle these signals. A program that handles the background-read signal and continues reading gets the terminal input despite not being in the foreground, which is usually a bug but is occasionally what a full-screen program wants. The signal is generated once per attempt, so a handler that continues the read gets a stream of signals, one per read attempt.
Stopping and Continuing
A stopped process is not terminated. Its state is recorded, its address space is intact, and it can be continued by a signal. The stop is entered on the stop signals, and the continue is entered on the continue signal, and the shell’s job table is a list of the groups it has started with their current stop state.
The shell learns of a stop through the child-state-change signal, which is delivered to the shell when a child stops as well as when it exits. The status word reported by the wait call distinguishes the two, and the shell records the group as stopped rather than removing it. The distinction is the one described in Zombies, Orphans, and the Mechanics of Reaping: a stopped child is reported but not reaped.
Continuing a group requires sending the continue signal to the group and then, for a foreground job, transferring the terminal’s foreground group back to it. The two operations are separate, and doing one without the other produces the classic failure where a resumed job runs but cannot read the terminal because the foreground group was never restored.
A stop signal sent to a group stops every member, including the ones that are blocked in a system call. A process blocked in a read is woken, the signal is delivered, and the process stops with the read uncompleted. On continuation the read is restarted, because the kernel records that the call was interrupted by a stop and not by a terminating signal. This is one of the cases where the restart logic is not optional: a program that reads a line and is suspended mid-read must not see a short read on continuation.
Orphaned Groups and the Hangup
The interesting edge cases involve sessions ending. When a process group is orphaned while it contains a stopped process, the kernel sends a hangup signal followed by a continue signal to every member of the group. The hangup tells the programs that their parent session is gone and the default action terminates them; the continue guarantees that a program that handles the hangup is not left stopped with no one to restart it. A program that ignores the hangup continues running with no terminal, which is the state a daemon wants and a login shell does not.
A process group can become orphaned in the job-control sense: no member has a parent in a different group within the same session. An orphaned group is not eligible to be stopped, because a stop would leave it with no one to continue it. The kernel therefore discards the stop signals SIGTSTP, SIGTTIN, and SIGTTOU sent to an orphaned group, and when a group becomes orphaned while one of its members is stopped, it sends a hangup followed by a continue to the whole group. The rule exists so that a job whose shell has died does not sit stopped forever.
The sequence that produces the rule is a shell exiting while a background job is stopped. The job’s group has no parent in a different group, so it is orphaned. If it were left stopped, nothing would ever send it a continue. The kernel sends a hangup followed by a continue instead, and a job that handles or ignores the hangup runs to completion with its output going wherever the descriptors point, which may be a terminal that no longer has a reader.
The hangup on terminal disconnect is a different signal from the hangup sent to a newly orphaned group, and the two can arrive together. A program that wants to survive both handles both and detaches from the terminal explicitly. The detach is the call that creates a new session, which also drops the controlling terminal, because the new session has none and cannot acquire the old one.
Daemonization as a Sequence of Detachments
The conventional daemon sequence is a series of steps that each remove one dependency on the parent or the terminal. The process forks and the parent exits, so the child is reparented and is no longer a child of the shell that started it. The child creates a new session, so it has no controlling terminal and is not in the shell’s session. The child changes its working directory to a directory that will not be unmounted, so it does not hold a mount point busy. The child closes or redirects its standard descriptors, so it does not hold the terminal open.
Each step is separate because each removes a different reference. The session creation removes the terminal association; the fork removes the parent relationship; the descriptor closure removes the last open reference to the terminal device. A daemon that skips the session creation keeps a controlling terminal and will receive the hangup when the terminal closes, which terminates it unless it handles the signal.
The second fork in the conventional sequence exists to ensure that the daemon is not a session leader. A session leader that opens a terminal device becomes its controlling terminal, and a daemon that acquires a terminal has undone the point of detaching. The second fork produces a process that is in the new session but is not its leader, so it cannot acquire a controlling terminal by opening one. This is why the sequence has two forks rather than one, and why the intermediate process exits immediately.
Modern service managers provide the same effect with explicit configuration rather than a fixed sequence of calls. The manager creates the process in its own session, sets the working directory, and provides descriptors that are not terminals. The daemon does not need to perform the sequence itself, and a program that does it anyway ends up in a session the manager did not create, which breaks the manager’s ability to signal the whole service as a group.
What the Identifiers Buy
The two identifiers give the kernel a way to address a set of processes for signals without enumerating them, and they give the terminal driver a way to decide which processes should receive the signals it generates. Neither mechanism provides isolation: a process in a group can signal another process in the same session if the user identifiers permit it, and the group boundary is a convenience for the sender rather than a security boundary.
The security boundary is the user identifier, which is checked when a signal is sent. The group and session identifiers are checked only in the specific cases where the kernel uses them to generate signals, such as the terminal driver’s job-control signals and the hangup on session exit. A program that needs to prevent signals from another process needs a different mechanism, and the identifiers described here are not it.
The practical consequence is that the model is best understood as a naming scheme for collections, layered over a permission check that is independent of it. The names make the shell’s job control expressible and the terminal’s signal generation possible; they do not make one job safe from another.
References
- POSIX.1-2017, System Interfaces — Job Control — IEEE Std 1003.1-2017
- termios(3) — terminal attributes and foreground process group — Linux man-pages project
- Advanced Programming in the UNIX Environment, 3rd edition — Chapter 9, Process Relationships — Stevens and Rago; Addison-Wesley
- setsid(2) — create a session and set the process group ID — Linux man-pages project