34.7.2 — Implementing Job Control
Chapter 34 · The Linux Programming Interface · EmbeddedPathashala
What the kernel, terminal driver, and shell each do to make job control work
Key Terms in This Section
Kernel requirements Terminal driver record Shell job-control support SIGCONT credentials exception SIGTTIN/SIGTTOU special cases isatty() job_mon pipeline demo SIGCHLD
Three Requirements for Job Control Support
Job control is not just a shell feature — it requires cooperation between three layers of the system:
1 Kernel: Must provide the job-control signals: SIGTSTP, SIGSTOP, SIGCONT, SIGTTIN, SIGTTOU, and SIGCHLD. SIGCHLD allows the shell (parent of all jobs) to know when a child terminates or stops.
2 Terminal driver: Must store the session ID and foreground PGID. When signal-generating characters (Ctrl+Z, Ctrl+C) are typed, the driver sends the signal to the foreground group. The driver also generates SIGTTIN/SIGTTOU when background processes access the terminal.
3 Shell: Must provide fg, bg, jobs commands. Uses tcsetpgrp() to move jobs between foreground and background. Monitors job states via SIGCHLD and wait().
Special Rule: SIGCONT Credential Exception
Normally: A process can only send a signal to another process if it shares the same user ID (real or effective).
Exception for SIGCONT: The kernel allows a process to send SIGCONT to any process in the same session, regardless of credentials. This is necessary because if a job changed its user ID (set-UID program), the shell could not normally send it signals — but it must still be able to resume stopped jobs.
SIGTTIN and SIGTTOU Special Cases
- If SIGTTIN is blocked or ignored, a read() from the terminal fails with EIO instead of stopping the process.
- If SIGTTOU is blocked or ignored, a write() to the terminal succeeds even if TOSTOP is set (TOSTOP is bypassed).
- Regardless of TOSTOP, functions that change terminal driver state (
tcsetpgrp(), tcsetattr(), tcflush(), tcflow(), tcsendbreak(), tcdrain()) always generate SIGTTOU for background processes. If SIGTTOU is blocked/ignored, these calls succeed.
The job_mon Pipeline Demo
The book’s job_mon program is designed to be run in a pipeline: ./job_mon | ./job_mon | ./job_mon. It demonstrates:
- All processes in a pipeline share the same PGID (all in the same process group = job).
- When SIGINT is sent (Ctrl+C), all processes in the foreground group receive it.
- SIGTSTP (Ctrl+Z) stops all of them; SIGCONT (bg) resumes all of them.
- The pipeline message passing (integer passed via pipe) shows process ordering.
isatty(STDIN_FILENO) tells the first process in the pipeline that its stdin is a terminal (not a pipe). isatty(STDOUT_FILENO) tells the last process its stdout is a terminal (not a pipe).
Code Example 1 — job_mon: Observing Pipeline Process Groups
/* job_mon.c
* Run multiple instances in a pipeline to observe how the shell
* puts all pipeline processes in one process group (job).
*
* Usage: ./job_mon | ./job_mon | ./job_mon
* or: ./job_mon | ./job_mon &
*
* Compile: gcc -D_GNU_SOURCE -o job_mon job_mon.c
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <fcntl.h>
static int cmdNum = 0; /* Our position in the pipeline */
static void handler(int sig)
{
/* Display foreground group — only if we are the group leader */
if (getpid() == getpgrp()) {
fprintf(stderr, " [FG PGID = %ld]\n",
(long)tcgetpgrp(STDERR_FILENO));
}
fprintf(stderr, " Process %ld (cmd #%d) received: %s\n",
(long)getpid(), cmdNum, strsignal(sig));
if (sig == SIGTSTP) {
/* Caught SIGTSTP doesn't auto-stop. Raise SIGSTOP to actually stop. */
raise(SIGSTOP);
}
}
int main(void)
{
/* Install handlers */
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = handler;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTSTP, &sa, NULL);
sigaction(SIGCONT, &sa, NULL);
/* First in pipeline: stdin is a terminal */
if (isatty(STDIN_FILENO)) {
fprintf(stderr, "Terminal FG process group: %ld\n",
(long)tcgetpgrp(STDIN_FILENO));
fprintf(stderr, "%-6s %-6s %-6s %-6s %-6s\n",
"Cmd", "PID", "PPID", "PGID", "SID");
cmdNum = 0;
} else {
/* Read message from previous stage in pipeline */
if (read(STDIN_FILENO, &cmdNum, sizeof(cmdNum)) <= 0) {
fprintf(stderr, "read from pipe failed\n");
return 1;
}
}
cmdNum++; /* Our position = predecessor's number + 1 */
/* Print our identity */
fprintf(stderr, "%-6d %-6ld %-6ld %-6ld %-6ld\n",
cmdNum,
(long)getpid(),
(long)getppid(),
(long)getpgrp(),
(long)getsid(0));
/* Pass message to next stage (if stdout is a pipe, not a terminal) */
if (!isatty(STDOUT_FILENO)) {
if (write(STDOUT_FILENO, &cmdNum, sizeof(cmdNum)) == -1) {
fprintf(stderr, "write to pipe failed\n");
}
}
/* Wait for signals */
for (;;) pause();
}
/*
* $ ./job_mon | ./job_mon | ./job_mon
* Terminal FG process group: 1204
* Cmd PID PPID PGID SID
* 1 1226 1204 1226 1204
* 2 1227 1204 1226 1204 ← all same PGID!
* 3 1228 1204 1226 1204
*
* Ctrl+C:
* [FG PGID = 1226]
* Process 1228 (cmd #3) received: Interrupt
* Process 1227 (cmd #2) received: Interrupt
* Process 1226 (cmd #1) received: Interrupt
*/
Code Example 2 — Minimal Job-Control Shell Loop
/* mini_shell.c
* A minimal job-control shell that demonstrates the core
* setpgid + tcsetpgrp pattern used by real shells.
* Compile: gcc -o mini_shell mini_shell.c
* NOTE: This is educational only — not a full shell.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <errno.h>
int main(void)
{
pid_t shell_pgid = getpgrp();
int tty_fd = open("/dev/tty", O_RDWR);
if (tty_fd == -1) { perror("open /dev/tty"); return 1; }
printf("mini_shell PID=%ld PGID=%ld\n",
(long)getpid(), (long)shell_pgid);
printf("Type a command (or 'quit'):\n");
char line[128];
while (1) {
printf("mini$ ");
fflush(stdout);
if (!fgets(line, sizeof(line), stdin)) break;
line[strcspn(line, "\n")] = '\0';
if (strcmp(line, "quit") == 0) break;
if (line[0] == '\0') continue;
/* Check for background (&) */
int background = 0;
int len = strlen(line);
if (len > 0 && line[len-1] == '&') {
background = 1;
line[len-1] = '\0';
while (strlen(line) > 0 && line[strlen(line)-1] == ' ')
line[strlen(line)-1] = '\0';
}
pid_t child = fork();
if (child == -1) { perror("fork"); continue; }
if (child == 0) {
/* --- CHILD: create its own process group --- */
setpgid(0, 0); /* child is group leader */
if (!background) {
/* Foreground: give child terminal access */
tcsetpgrp(tty_fd, getpgrp());
}
/* Default signal dispositions for the child */
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
signal(SIGTSTP, SIG_DFL);
char *args[] = { "/bin/sh", "-c", line, NULL };
execvp(args[0], args);
perror("exec");
_exit(127);
}
/* --- PARENT: mirror setpgid to avoid race --- */
setpgid(child, child); /* Ignore EACCES */
if (background) {
printf("[bg] PID=%ld\n", (long)child);
} else {
/* Foreground: give terminal to child, wait, take back */
tcsetpgrp(tty_fd, child);
int status;
waitpid(child, &status, WUNTRACED);
/* Restore shell to foreground */
tcsetpgrp(tty_fd, shell_pgid);
if (WIFSTOPPED(status))
printf("[stopped] PID=%ld\n", (long)child);
}
}
tcsetpgrp(tty_fd, shell_pgid);
close(tty_fd);
printf("mini_shell exiting\n");
return 0;
}
/*
* mini$ sleep 10 ← runs in foreground; Ctrl+Z stops it
* mini$ ls & ← runs in background
* mini$ quit
*/
Interview Questions — Section 34.7.2
Q1. What three system-level components must cooperate to support job control?
The kernel must provide job-control signals (SIGTSTP, SIGSTOP, SIGCONT, SIGTTIN, SIGTTOU, SIGCHLD). The terminal driver must maintain a record of the session ID and foreground PGID, and must generate signals when characters are typed or when background processes access the terminal. The shell must implement fg/bg/jobs commands and use tcsetpgrp() to manage which group is in the foreground.
Q2. Why is SIGCONT special regarding signal delivery credentials?
Normally, a process can only send a signal to another process if their user IDs match. SIGCONT is exempt from this rule within a session — any process can send SIGCONT to any other process in the same session, regardless of user ID. This exception is necessary so the shell can resume stopped jobs that might have changed their user ID (e.g., set-UID programs).
Q3. What does isatty() return when called on a file descriptor that refers to a pipe?
isatty() returns 0 (false) for a pipe. It returns non-zero (true) only for file descriptors referring to a terminal. This is how the first and last processes in a pipeline detect their position: the first has isatty(STDIN_FILENO) == true (stdin is the terminal, not a pipe), and the last has isatty(STDOUT_FILENO) == true.
Q4. When SIGTTIN is blocked, what happens when a background process calls read() on the terminal?
Instead of the process being stopped by SIGTTIN, the read() call fails and returns -1 with errno set to EIO. This is intentional — the process would have no way of knowing that its read was blocked if the signal was silently blocked without any error indication.
