Chapter Summary
Linux organizes processes into a two-level hierarchy for the purpose of terminal management and job control. At the top level is the session: a collection of process groups all sharing the same session ID (SID). Within a session, each process group is a set of related processes sharing a process group ID (PGID).
| SESSION (SID = shell PID, e.g. 400) | ||||
| Process Group 400 (shell alone) |
Process Group 658 (find | wc &) |
Process Group 660 (sort | uniq) |
||
| bash PID=400 session leader controlling process |
find PID=658 wc PID=659 background |
sort PID=660 uniq PID=661 foreground |
||
| Controlling Terminal โ Foreground PGID = 660 | ||||
Key facts: A new process inherits its parent’s PGID and SID. The session leader creates the session via setsid(). The process group leader is the process whose PID equals its PGID. All processes in a session share one controlling terminal (/dev/tty).
When the session leader opens a terminal device for the first time (without O_NOCTTY), that terminal becomes the session’s controlling terminal and the session leader becomes the controlling process. The controlling process receives SIGHUP when a terminal disconnect occurs. /dev/tty always refers to a process’s controlling terminal; opening it fails with ENXIO if the process has no controlling terminal.
| Signal | Trigger | Default Action | Sent To |
|---|---|---|---|
| SIGINT | Control-C | Terminate | Foreground process group |
| SIGQUIT | Control-\ | Terminate + core | Foreground process group |
| SIGTSTP | Control-Z | Stop | Foreground process group |
| SIGSTOP | kill -STOP / bg stop | Stop (uncatchable) | Specified process/group |
| SIGCONT | fg / bg | Resume if stopped | Specified process/group |
| SIGTTIN | BG read from terminal | Stop | Background process group |
| SIGTTOU | BG write (TOSTOP set) | Stop | Background process group |
| SIGHUP | Terminal disconnect | Terminate | Controlling process; then FG group |
A terminal disconnect triggers a two-path chain reaction:
| Path | What happens |
|---|---|
| Path 1 โ Shell handler | Shell receives SIGHUP, its handler forwards SIGHUP to all jobs it created (foreground and background process groups in the session) |
| Path 2 โ Kernel action | If SIGHUP causes the controlling process to terminate, the kernel sends SIGHUP (+ SIGCONT on Linux) to all members of the foreground process group |
When a process group becomes orphaned AND has stopped members: kernel sends SIGHUP + SIGCONT to all members. For orphaned group members trying to use the terminal: read() and write() return EIO instead of stopping the process. SIGTSTP/SIGTTIN/SIGTTOU that would stop a member of an orphaned group are silently discarded.
34.9 TLPI Exercises
Exercise 34-1: Signal to process group race condition
A parent creates children (all in the same process group), makes itself immune to SIGUSR1, then sends SIGUSR1 to the group. What problem can arise, especially with shell pipelines?
prog | other_prog), the shell places all pipeline processes in the same process group. When killpg(getpgrp(), SIGUSR1) is called, SIGUSR1 is sent not just to the intended children, but also to other_prog and any other processes in the shared group that the programmer didn’t create. Those processes receive an unexpected signal. Solution: before sending the signal, ensure each intended child is placed in its own process group using setpgid(), then send signals to specific groups rather than using the parent’s group.Exercise 34-2: setpgid() before and after exec()
Write a program to verify that a parent can change the PGID of a child before exec(), but not after.
setpgid(child_pid, child_pid) immediately โ this should succeed. For the “after exec” case, the child should exec a long-running program (e.g., sleep), and the parent should wait and then try setpgid again โ this should fail with EACCES./* Exercise 34-2 outline */
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void)
{
pid_t child = fork();
if (child == 0) {
sleep(2); /* Give parent time to test both cases */
execl("/bin/sleep", "sleep", "30", NULL);
}
/* Test 1: setpgid BEFORE exec (should succeed) */
if (setpgid(child, child) == 0)
printf("setpgid before exec: SUCCESS\n");
else
printf("setpgid before exec: FAILED (%s)\n", strerror(errno));
/* Wait for child to exec */
sleep(3);
/* Test 2: setpgid AFTER exec (should fail with EACCES) */
if (setpgid(child, child) == 0)
printf("setpgid after exec: SUCCESS (unexpected)\n");
else
printf("setpgid after exec: FAILED with %s (expected EACCES)\n",
strerror(errno));
return 0;
}
Exercise 34-3: setsid() from a process group leader fails
Verify that calling setsid() from a process group leader returns -1 with EPERM, but succeeds after fork().
/* Exercise 34-3 */
#define _XOPEN_SOURCE 500
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void)
{
printf("PID=%ld, PGID=%ld\n", (long)getpid(), (long)getpgrp());
/* A process is a group leader if PID == PGID */
if (getpid() == getpgrp()) {
printf("We are a process group leader - setsid() should fail\n");
if (setsid() == -1)
printf("setsid() FAILED as expected: %s\n", strerror(errno));
else
printf("setsid() succeeded (unexpected!)\n");
}
/* Fork: child is NOT a group leader (child PID != parent PGID) */
pid_t child = fork();
if (child == 0) {
printf("Child PID=%ld, PGID=%ld - calling setsid()\n",
(long)getpid(), (long)getpgrp());
pid_t sid = setsid();
if (sid == -1)
printf("setsid() failed: %s\n", strerror(errno));
else
printf("setsid() succeeded: new SID=%ld\n", (long)sid);
_exit(0);
}
wait(NULL);
return 0;
}
/* Output:
* PID=1234, PGID=1234
* We are a process group leader - setsid() should fail
* setsid() FAILED as expected: Operation not permitted
* Child PID=1235, PGID=1234 - calling setsid()
* setsid() succeeded: new SID=1235
*/
Exercise 34-4: SIGHUP not sent to foreground group when controlling process survives
If SIGHUP does NOT terminate the controlling process (it handles the signal and continues), then the kernel does NOT send SIGHUP to the foreground process group. Verify this.
exec ./disc_SIGHUP s s so our program becomes the controlling process. Have the parent install a SIGHUP handler that prints a message and continues (does not exit). Create children in the same group. Close the terminal window. Only the parent should receive SIGHUP, not the children. This demonstrates that SIGHUP propagation to the foreground group is contingent on the controlling process terminating.Exercise 34-5: Race condition if SIGTSTP unblock is moved to start of handler
In Listing 34-6, what race condition is created if sigprocmask(SIG_UNBLOCK) is placed at the very beginning of the handler?
Exercise 34-6: read() fails with EIO for orphaned group terminal access
Write a program to verify that when a member of an orphaned process group attempts read() from the controlling terminal, read() fails with EIO.
orphan_eio.c in section 34.7.4 demonstrates. Key steps: fork a child, child calls setpgid(0,0) to form its own group, parent exits, child sleeps briefly, child opens /dev/tty and calls read() โ should get EIO.Exercise 34-7: SIGTTIN/SIGTTOU/SIGTSTP discarded vs delivered for orphaned group
Verify that these signals are silently discarded (no effect) when sent to an orphaned process group member with default disposition, but delivered normally when a handler is installed.
Comprehensive Interview Question Bank
Q1: What is the difference between a process group and a session?
A process group is a collection of related processes sharing a PGID, used to send signals collectively to a pipeline or job. A session is a collection of process groups sharing a SID, typically representing one login session or terminal window. Sessions have an optional controlling terminal; process groups do not. The hierarchy is: session > process groups > processes.
Q2: What does setsid() do, and why can’t a process group leader call it?
setsid() creates a new session: the calling process becomes session leader, process group leader, and sole member of a new process group in the new session, with no controlling terminal. A process group leader cannot call setsid() because it would create a new session while leaving other members of its old process group in the old session โ violating the invariant that all members of a process group must belong to the same session.
Q3: What is the race condition in job-control shells when setting a child’s process group?
After fork(), neither parent nor child is guaranteed to run first. If the parent relies on setting the child’s PGID before the child execs, and the child execs first, setpgid() fails with EACCES. Solution: both parent and child call setpgid() to the same value immediately after fork(). The parent ignores EACCES (it means the child already exec’d and set its own group). This double-call ensures the PGID is set regardless of scheduling order.
Q4: What is the controlling process, and what signal does it receive on terminal disconnect?
The controlling process is the process that established the controlling terminal โ typically the session leader (the shell) that first opened the terminal. When a terminal disconnect occurs (modem hangup or terminal window closed), the kernel sends SIGHUP to the controlling process. If SIGHUP terminates the controlling process, the kernel then sends SIGHUP to all members of the foreground process group.
Q5: What is nohup, and what does it do at the signal level?
nohup(1) is a command that starts another command with SIGHUP’s disposition set to SIG_IGN. This makes the command immune to terminal hangup signals. The bash built-in disown serves a similar purpose by removing a job from the shell’s job list, so the shell doesn’t forward SIGHUP to that job when the shell terminates. Both techniques allow long-running background jobs to survive terminal disconnection.
Q6: Under what conditions does SIGCONT have special credential rules?
Normally, a process can only send a signal to another process if they share the same real user ID or if the sender is privileged. SIGCONT is an exception: any process in the same session can send SIGCONT to any other process in the same session, regardless of user credentials. This is needed so that a shell can resume a set-user-ID program (which may have changed its real UID) that it previously stopped with SIGTSTP.
Q7: What is the TOSTOP terminal flag and which signal does it trigger?
TOSTOP (terminal output stop) is a terminal driver flag. When set (via stty tostop), any background process that attempts to write to the controlling terminal receives SIGTTOU. SIGTTOU’s default action is to stop the process, just like SIGTTIN stops background readers. If SIGTTOU is being blocked or ignored, the write is permitted regardless of TOSTOP. The flag is off by default.
Q8: How does the shell use tcsetpgrp() during job control?
When the user types fg %1, the shell calls tcsetpgrp(terminal_fd, job_pgid) to update the terminal driver’s record of the foreground process group to the resumed job’s PGID. When the job is subsequently stopped or moved back to background, the shell calls tcsetpgrp(terminal_fd, shell_pgid) to restore itself as the foreground process group. This controls which process group receives terminal-generated signals (SIGINT, SIGTSTP, etc.).
Q9: What is /dev/tty and when does opening it fail?
/dev/tty is a special file that always refers to the calling process’s controlling terminal, regardless of any stdin/stdout redirections. It is used by programs like getpass() that need direct terminal access. Opening /dev/tty fails with ENXIO (errno 6) if the process has no controlling terminal โ for example, after calling setsid() in a daemon initialization sequence, or in a process that ran with O_NOCTTY.
Q10: Explain the purpose of the SA_RESTART flag when installing job-control signal handlers.
SA_RESTART instructs the kernel to automatically restart certain slow system calls (like read(), write(), pause()) if they are interrupted by the signal. Without SA_RESTART, a read() call interrupted by SIGTSTP/SIGCONT would return -1 with errno EINTR, requiring the program to retry manually. With SA_RESTART, the system call resumes transparently after the handler returns. Job-control signal handlers typically use SA_RESTART to avoid breaking programs that don’t check for EINTR.
Q11: What are the four restrictions on setpgid() calls?
1) The pid argument may only specify the calling process or one of its children (ESRCH if violated). 2) The calling process, the specified process, and the target process group must all be in the same session (EPERM if violated). 3) The specified process cannot be a session leader (EPERM). 4) A process cannot change the PGID of a child after that child has called exec() (EACCES). These restrictions enforce the integrity of the session/process-group hierarchy.
Q12: How does a pipeline’s process group get established by the shell?
The shell forks one child for each command in the pipeline. For the first child, the shell uses setpgid(child1_pid, child1_pid) to make it a new process group leader. For all subsequent children, the shell uses setpgid(childN_pid, child1_pid) to place them in that same process group. Both parent and child call setpgid() to the same value to avoid the race condition. The shell records child1_pid as the PGID for this job.
| Call | Returns | Purpose |
|---|---|---|
getpgrp() |
PGID | Get calling process’s PGID |
getpgid(pid) |
PGID | Get PGID of specified process |
setpgid(pid, pgid) |
0/-1 | Set process group of pid to pgid |
getsid(pid) |
SID | Get session ID of specified process |
setsid() |
new SID | Create new session (not from group leader) |
tcgetpgrp(fd) |
PGID | Get foreground process group of terminal |
tcsetpgrp(fd, pgid) |
0/-1 | Set foreground process group of terminal |
tcgetsid(fd) |
SID | Get session ID for controlling terminal |
ctermid(buf) |
pathname | Get pathname of controlling terminal (usually /dev/tty) |
Chapter 34 Complete!
You have covered all 13 sections of Process Groups, Sessions, and Job Control.
