Implementing Job Control in Linux: Complete Guide
Implementing Job Control in Linux: Complete Guide
· The Linux Programming Interface · EmbeddedPathashala
Key Terms in This Section
Three Requirements for Job Control Support
Job control is not just a shell feature — it requires cooperation between three layers of the system:
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
*/
