What is Job Control?
Job control lets you manage multiple commands from a single shell. You can run some commands in the background (they run invisibly), bring them to the foreground, stop them, and restart them.
A job is a command or pipeline started by the shell. Each job gets a job number (shown in square brackets) and is placed in its own process group.
Shell Job Control Commands Quick Reference
| Command / Key | Signal Sent | Effect |
|---|---|---|
| command & | none | Start job in background |
| fg [%n] | SIGCONT | Bring job to foreground (and resume if stopped) |
| bg [%n] | SIGCONT | Resume a stopped job in background |
| jobs | none | List all background jobs and their states |
| Ctrl+Z | SIGTSTP → foreground group | Stop the foreground job |
| Ctrl+C | SIGINT → foreground group | Terminate the foreground job |
| kill -STOP %n | SIGSTOP → job | Stop a background job |
| kill %n | SIGTERM → job | Terminate a background job |
Job State Machine
| From State | Command/Signal | To State |
|---|---|---|
| — | command & |
Running in Background |
| — | command (no &) |
Running in Foreground |
| Running in Foreground | Ctrl+Z (SIGTSTP) | Stopped in Background |
| Stopped in Background | fg (SIGCONT) |
Running in Foreground |
| Stopped in Background | bg (SIGCONT) |
Running in Background |
| Running in Background | fg |
Running in Foreground |
| Running in Background | Terminal read attempt | Stopped (SIGTTIN) |
| Running in Foreground | Ctrl+C (SIGINT) | Terminated |
| Running in Background | kill -STOP |
Stopped in Background |
SIGTTIN: Background Process Reading from Terminal
Only the foreground job can read from the terminal. If a background job tries to read() from the terminal:
- The terminal driver sends SIGTTIN to the background process’s entire group.
- The default action stops the job.
- The user brings it to the foreground with
fg, allows the read to complete.
Special cases: If the process is blocking or ignoring SIGTTIN, the read() fails with EIO instead.
SIGTTOU: Background Process Writing to Terminal (TOSTOP)
By default, background jobs CAN write to the terminal. But if you enable the TOSTOP flag (stty tostop), then background writes also stop the job with SIGTTOU.
SIGTTOU is also generated when a background process calls terminal control functions like tcsetpgrp(), tcsetattr(), etc. — regardless of TOSTOP.
Code Example 1 — Program That Responds to SIGTTIN and SIGTTOU
/* sigttin_demo.c
* Demonstrates SIGTTIN when a background process tries to read.
* Run this in the background: ./sigttin_demo &
* Then wait — it gets stopped by SIGTTIN.
* Bring to foreground with: fg
* Compile: gcc -o sigttin_demo sigttin_demo.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
static void sig_handler(int sig)
{
char msg[80];
snprintf(msg, sizeof(msg),
"\n[PID %ld] Caught signal %d (%s)\n",
(long)getpid(), sig, strsignal(sig));
write(STDERR_FILENO, msg, strlen(msg));
}
int main(void)
{
/* Catch SIGTTIN and SIGTTOU so we can observe them */
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = sig_handler;
sigaction(SIGTTIN, &sa, NULL);
sigaction(SIGTTOU, &sa, NULL);
sigaction(SIGCONT, &sa, NULL);
printf("PID=%ld PGID=%ld\n",
(long)getpid(), (long)getpgrp());
printf("Checking if I am in foreground...\n");
fflush(stdout);
/* Open controlling terminal */
int tty = open("/dev/tty", O_RDWR | O_NOCTTY);
/* Try to read — this will cause SIGTTIN if we are in background */
printf("Attempting read from /dev/tty...\n");
fflush(stdout);
char buf[64];
ssize_t n = read(tty == -1 ? STDIN_FILENO : tty,
buf, sizeof(buf)-1);
if (n > 0) {
buf[n] = '\0';
printf("Read: %s\n", buf);
} else if (n == 0) {
printf("EOF from terminal\n");
} else {
perror("read");
}
if (tty != -1) close(tty);
return 0;
}
/*
* $ ./sigttin_demo &
* [1] 9100
* PID=9100 PGID=9100
* Attempting read from /dev/tty...
* [1]+ Stopped ./sigttin_demo ← stopped by SIGTTIN
*
* $ fg
* ./sigttin_demo
* hello ← you type this
* Read: hello
*/
Code Example 2 — Simulating fg/bg Job State Transitions
/* job_state_demo.c
* Shows job states by printing what signals a process receives.
* Run: ./job_state_demo &
* Then try: fg, Ctrl+Z, bg, kill -STOP %1, bg
* Compile: gcc -D_GNU_SOURCE -o job_state_demo job_state_demo.c
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
static void handler(int sig)
{
char msg[80];
int n = snprintf(msg, sizeof(msg),
"[PID %ld] Signal: %s\n",
(long)getpid(), strsignal(sig));
write(STDOUT_FILENO, msg, n);
}
int main(void)
{
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = handler;
/* Install handlers for all job-control signals */
int sigs[] = { SIGTSTP, SIGSTOP, SIGCONT, SIGTTIN, SIGTTOU,
SIGHUP, SIGINT, SIGTERM, 0 };
for (int i = 0; sigs[i]; i++) {
/* Note: SIGSTOP and SIGKILL cannot be caught */
if (sigs[i] != SIGSTOP)
sigaction(sigs[i], &sa, NULL);
}
printf("Job state demo started.\n");
printf("PID=%ld PGID=%ld\n", (long)getpid(), (long)getpgrp());
printf("Try: fg, Ctrl+Z, bg, kill -STOP %d, bg\n", (int)getpid());
fflush(stdout);
/* Loop printing heartbeat every 3 seconds */
for (int tick = 1; ; tick++) {
sleep(3);
printf(" tick %d (still running, PID=%ld)\n",
tick, (long)getpid());
fflush(stdout);
}
return 0;
}
/*
* Run in background, then interact:
* $ ./job_state_demo &
* $ fg → [PID 9200] Signal: Continued
* Ctrl+Z → [PID 9200] Signal: Stopped
* $ bg → [PID 9200] Signal: Continued
* $ kill -STOP 9200 → job stops again
* $ bg %1 → [PID 9200] Signal: Continued
*/
