Chapter 34 Recap โ€” Process Groups, Sessions, Job Control, Signals

 

34.8 Summary & 34.9 Exercises
Complete Chapter 34 Recap โ€” Process Groups, Sessions, Job Control, Signals
13
Sections Covered
7
TLPI Exercises
15+
Interview Q&A

Chapter 34 Master Keyword Map
Process Group PGID Session SID Session Leader Controlling Terminal Controlling Process setsid() setpgid() tcgetpgrp() tcsetpgrp() SIGHUP SIGTSTP SIGTTIN SIGTTOU SIGCONT Job Control fg / bg Orphaned Process Group EIO nohup /dev/tty

Chapter Summary

Two-Level Hierarchy: Sessions and Process Groups

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

Controlling Terminal and Process

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.

Job Control Signals โ€” Quick Reference
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
SIGHUP Chain Reaction Summary

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
Orphaned Process Groups Summary

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?

Hint & Answer: The problem is that if this program is run as part of a pipeline (e.g., 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.

Approach: Fork a child. In the parent, call 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.

Approach: Use 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?

Answer: If SIGTSTP is unblocked at the start of the handler (before signal() resets disposition to SIG_DFL), a second SIGTSTP arriving at that moment would invoke the handler recursively โ€” because the disposition is still the custom handler. This can lead to stack overflow with rapid SIGTSTP delivery. The correct sequence is: first reset to SIG_DFL (Step 2), then raise (Step 3), then unblock (Step 4). Only then does the pending signal stop the process safely.

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.

Approach: This is exactly what 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.

Approach: Create an orphaned child process. Test 1: child has SIG_DFL for SIGTSTP; parent (before exiting) sends SIGTSTP โ€” child should not stop, signal should be discarded. Test 2: child installs a handler for SIGTSTP; send SIGTSTP โ€” handler should run. Note: for truly orphaned group (init as parent), you need the two-fork pattern.

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.

Quick Reference: System Calls Summary
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.

โ† 34.7.4 Orphaned Groups ๐Ÿ  Course Index

Leave a Reply

Your email address will not be published. Required fields are marked *