Foreground and Background Jobs

Every command you run in a terminal is either in the foreground, meaning it has control of the terminal and you have to wait for it to finish before typing anything else, or in the background, meaning it runs independently while you keep using the shell. Bash’s job control lets you start commands in the background, pause a running program and resume it later, and switch tasks between foreground and background on demand — all without opening a second terminal window.

Overview: How Job Control Works

When Bash starts a command, it creates a new process (or a group of processes, if the command is a pipeline) and assigns it to a process group called a job. Every interactive Bash shell tracks the jobs it has launched and gives each one a small integer job number (%1, %2, and so on) that is separate from the operating system’s process ID (PID). The job number only makes sense to that one shell session; the PID is the number the kernel actually uses to track the process.

A terminal (technically a pseudo-terminal, or pty, in most modern setups) has exactly one foreground process group at a time. Only the foreground process group is allowed to read keystrokes from the terminal and receive terminal-generated signals like Ctrl+C. Everything else attached to that terminal — every background job — keeps running, but the kernel will pause it automatically if it tries to read from the terminal (it receives SIGTTIN) and, depending on shell settings, may also pause it if it tries to write to the terminal.

You control which process group owns the foreground with three signals that Bash sends on your behalf:

  • SIGTSTP (20) — sent when you press Ctrl+Z. Unlike SIGKILL, a process can catch and handle this, but by default it just suspends the process in place, freezing it exactly where it was.
  • SIGCONT (18) — sent by fg or bg to wake a suspended process back up.
  • SIGINT (2) — sent when you press Ctrl+C. This asks the foreground process to terminate; most programs exit, but some catch it and clean up first.

Backgrounding a command with the trailing & operator never suspends it — it starts the command already running, just not attached to your keyboard. Suspending happens only through Ctrl+Z on something already in the foreground.

There is one more piece of internals worth knowing: when your login shell exits (for example, you close the terminal window or log out over SSH), it normally sends SIGHUP (“hangup”) to every job still attached to it, killing them. This is exactly why long-running background jobs need special handling if you want them to survive after you disconnect — covered in the third example below.

Syntax

Job control is a handful of small, composable pieces rather than one command:

command &          # start 'command' directly in the background
Ctrl+Z              # suspend the current foreground job
jobs [-l]           # list jobs known to this shell (-l also shows PIDs)
fg [%n]             # bring job n (or the most recent job) to the foreground
bg [%n]             # resume job n (or the most recent job) in the background
kill %n             # send SIGTERM to job n
disown [%n]         # remove job n from the shell's job table without killing it
nohup command &    # start 'command' immune to SIGHUP, in the background
wait [%n]           # block until job n (or all background jobs) finishes
Symbol / Command Meaning
& Trailing operator that starts a command already running in the background.
%1, %2, … Job specifiers, referring to jobs by the number jobs assigned them, not their PID.
%+ or %% The “current” job — the most recently backgrounded or suspended one.
%- The “previous” job.
%name The job whose command line starts with name, e.g. %nano.

Examples

Example 1: Starting a job in the background and bringing it back

$ sleep 300 &
[1] 48213

Bash immediately prints the job number in brackets (1) and the PID of the new process (48213), then hands the prompt straight back to you — it did not wait for sleep to finish. Check on it at any time:

$ jobs
[1]+  Running                 sleep 300 &

The + marks it as the current job. If you decide you actually want to wait for it after all, pull it into the foreground:

$ fg %1
sleep 300

Bash echoes the command and now your prompt is blocked until sleep finishes (or you press Ctrl+C to cancel it).

Example 2: Suspending a foreground program and resuming it in the background

Say you start editing a file and remember you need to run a quick command first:

$ nano deploy_notes.txt

While nano is running in the foreground, press Ctrl+Z. Bash prints:

[1]+  Stopped                 nano deploy_notes.txt

nano is now frozen in place, not destroyed — every unsaved change is still in memory. You get your prompt back and can run other commands. When you are ready to keep working in it, either resume it in the background:

$ bg %1
[1]+ nano deploy_notes.txt &

or bring it straight back to the foreground with fg %1, which is what you actually want for an interactive editor, since a backgrounded nano will just stop again the instant it tries to read your keystrokes (SIGTTIN). This example is really demonstrating the mechanism — bg is far more useful for commands that do real work without needing terminal input, which is the next example.

Example 3: A background job that survives logout

Backgrounding with & keeps a job running only as long as the shell that launched it stays alive. For a backup script you kick off over SSH and want to keep running after you disconnect, combine nohup (which blocks SIGHUP) with &:

#!/usr/bin/env bash
set -euo pipefail

SRC_DIR="/var/www/app"
DEST="/backups/app-$(date +%F).tar.gz"

tar -czf "$DEST" "$SRC_DIR"
echo "Backup written to $DEST"
$ chmod +x backup.sh
$ nohup ./backup.sh > backup.log 2>&1 &
[1] 51902

We redirected both stdout and stderr to backup.log ourselves, so nohup has nothing left to redirect and stays quiet. The job now ignores SIGHUP: closing the terminal or logging out will not kill it. If you’d rather just detach an already-running job from the current shell instead of restarting it with nohup, use disown:

$ disown %1

disown removes the job from this shell’s job table (so jobs no longer lists it and it won’t receive the shell’s SIGHUP), but unlike nohup it does not touch the process’s own signal handling — it only changes who “owns” it from the shell’s point of view.

How It Works Step by Step

    Step-by-step is expressed as ul below for HTML compliance
  • You type a command and press Enter. Bash forks a child process (or several, for a pipeline) and puts it in a new process group.
  • If the command line ends in &, Bash records that process group as a background job, prints its job number and PID, and immediately returns you to the prompt — it never makes that group the foreground process group of the terminal.
  • If there is no trailing &, the new process group becomes the terminal’s foreground process group, and Bash’s own process waits (via the wait() system call family) for it to finish or stop before showing another prompt.
  • Pressing Ctrl+Z sends SIGTSTP to the foreground process group. The kernel suspends those processes, control returns to Bash, and Bash records the job as “Stopped”.
  • bg %n sends SIGCONT to that process group so it starts running again, but leaves it as a background job attached to no terminal input.
  • fg %n also sends SIGCONT, but first makes that process group the foreground process group again, so it can read your keystrokes and receive Ctrl+C.
  • When the shell itself is about to exit, it sends SIGHUP to every job still in its table (unless the job was started with nohup or later disown-ed), which by default terminates them.

Common Mistakes

Mistake 1: Closing the terminal and losing a long job

Starting a long task in the background and then closing the terminal window assumes it keeps running — it usually doesn’t:

$ ./generate_report.sh &
# closes terminal window
# report generation is killed by SIGHUP partway through

Fix it by making the job immune to SIGHUP before you disconnect, or run it inside tmux/screen so the whole session, not just one job, survives:

$ nohup ./generate_report.sh > report.log 2>&1 &
$ disown

Mistake 2: Confusing the job number with the PID

kill %1 and kill 1 are completely different things — the first politely asks your shell’s job 1 to terminate, the second tries to kill PID 1, which is the system’s init process, and will be refused unless you are root (and would be catastrophic if it succeeded). Always use % when you mean a job number:

$ kill 1        # WRONG if you meant job 1 — this targets PID 1
$ kill %1       # correct: sends SIGTERM to job 1

Mistake 3: Backgrounding a program that still needs your input

Interactive programs like editors, ssh without a command, or anything with a password prompt will stop themselves the instant they try to read the terminal, even after you background them:

$ mysql -u admin -p &
[1]+  Stopped (tty input)     mysql -u admin -p

It looks backgrounded but is actually frozen waiting for input it can no longer receive. Run interactive programs in the foreground, or supply all their input up front (flags, a config file, input redirection) if they truly need to run unattended.

Best Practices

  • Use jobs -l when you need the PID for a job, instead of guessing it from ps output.
  • Prefer %name or %+ over remembering raw numbers when you only have one or two jobs running.
  • Wrap anything that must outlive your login session in nohup ... &, or better, run it inside tmux or screen so you can also reattach and watch its output later.
  • Redirect a background job’s output explicitly (> out.log 2>&1) so it doesn’t try to write to a terminal that may no longer exist, and so you have a record of what happened.
  • Use wait in scripts that launch several background tasks and need to know when they’ve all finished before continuing.
  • Never background an interactive program that will ask for input; run it in the foreground, or automate its input first.
  • Check $? right after a foregrounded job finishes if you need its exit status — running any other command first will overwrite it.

Practice Exercises

  • Start sleep 120 &, confirm it with jobs -l, then use fg to bring it to the foreground and press Ctrl+C to cancel it before it finishes. Check $? afterward.
  • Open a file in nano, suspend it with Ctrl+Z, run ls and jobs to confirm it’s stopped, then use fg to return to editing (backgrounding an editor will just stop it again, as shown above).
  • Write a two-line script that sleeps for 10 seconds and then prints “done”, run it with nohup ./script.sh & disown, and use ps -p <PID> a few seconds later to confirm it’s still running and no longer tied to your shell’s job table.

Summary

  • A trailing & starts a command in the background immediately; Ctrl+Z suspends a command that is already running in the foreground.
  • jobs lists the current shell’s jobs by job number (%1, %2, …), which is not the same as the process’s PID.
  • fg and bg resume a job in the foreground or background respectively, both by sending it SIGCONT.
  • Only the foreground process group can read from the terminal; a backgrounded program that tries to read input will stop itself.
  • Closing your terminal sends SIGHUP to your jobs by default; use nohup or disown, or a terminal multiplexer like tmux, to let a job outlive the session.
  • Always use %n when referring to a job number in commands like kill — a bare number is treated as a PID.