Skip to content

Foreground and Background Jobs: &, jobs, bg, fg

A single terminal window runs exactly one command at a time by default — the shell waits for the current command to finish before it reads and executes the next one. For anything long-running (compressing a large archive, exporting a database, running a batch script) that is inconvenient: the terminal is unusable for anything else until the command completes. The shell's job control feature solves this by letting you move a command between running in the foreground (attached to the terminal, blocking further input) and the background (detached, running independently while you keep working), and by letting you pause a running command entirely without terminating it.

This article covers the shell's own job control — distinct from the kernel-level signal mechanism in Signals and Process Control, though job control is built directly on top of it — and the practical difference between a background job that survives your logout and one that does not.

Starting a Job in the Background: &

Appending & to a command starts it in the background immediately, and the shell returns control to you right away instead of waiting:

sleep 300 &
[1] 22110

The shell prints two numbers: [1] is the job number — a small, shell-local identifier, reused across separate background jobs in the same shell session — and 22110 is the actual PID the kernel assigned this process. Both refer to the exact same running process; the job number is purely a shell convenience for the commands covered below, while the PID is the identifier every other tool (ps, kill) understands.

Listing and Referring to Jobs: jobs

sleep 300 &
sleep 600 &
jobs
[1]-  Running                 sleep 300 &
[2]+  Running                 sleep 600 &

The + marks the current job — the one that fg or bg will act on by default if no job is specified — and - marks the previous job, the one that becomes current if the current job finishes or is brought to the foreground. Referring to a job explicitly uses % followed by its number:

fg %1
kill %2

%1 and %2 are shell-only notation, valid only in the same shell session that started those jobs — they mean nothing to ps or to any other program, and a kill %2 is translated by the shell itself into the equivalent kill on the underlying PID before it ever reaches the kernel.

Moving Between Foreground and Background: fg and bg

Ctrl+Z sends SIGTSTP (see Signals and Process Control) to the foreground job, pausing it and immediately returning control of the terminal to the shell — without terminating anything:

sleep 300

Pressing Ctrl+Z here:

^Z
[1]+  Stopped                 sleep 300

The job is now paused (STAT of T in ps) but still exists in memory with all its state intact. From here:

bg %1

resumes it, but in the background — equivalent to sending SIGCONT and detaching it from blocking further terminal input — while:

fg %1

brings it back to the foreground, reattaching it to the terminal and blocking further input until it finishes or is paused again.

This Ctrl+Z then bg sequence is the standard fix for "I forgot to add & when I started this long-running command" — you do not need to kill and restart the command, only reclaim the terminal for it.

The Critical Difference: Background Jobs and the Controlling Terminal

A background job started with plain & is still a child of the shell and, by default, still tied to the terminal's session. If the terminal closes — the SSH connection drops, the terminal window is closed — the shell normally sends SIGHUP to every job still attached to it, which (per the default disposition covered in Signals and Process Control) terminates them unless they specifically handle or ignore that signal.

long-export.sh &

Closing the terminal here, without any further precaution, is very likely to kill long-export.sh partway through — a common, painful surprise for anyone new to remote administration over SSH.

Surviving a Terminal Disconnect: nohup

nohup long-export.sh > export.log 2>&1 &

nohup explicitly makes the wrapped command ignore SIGHUP, so it survives the terminal closing. It also redirects output to nohup.out by default if you do not redirect it yourself — always redirect explicitly, as shown, so the log ends up somewhere predictable rather than in whatever directory you happened to be in when you ran the command.

The Shell-Level Alternative: disown

long-export.sh &
disown

disown removes the job from the shell's own job table, so the shell no longer considers it one of "its" jobs to signal on exit — functionally similar to nohup for this specific purpose, but applied after the fact to a job you already started, rather than requiring you to have wrapped the command from the beginning.

The More Robust Answer: A Terminal Multiplexer or a Real Service

nohup and disown solve "survive one disconnect," but neither gives you a way to reattach and interactively monitor the job's progress afterward, and neither is the right tool for something that should run continuously as actual infrastructure. For an interactive long-running task you want to periodically check on, tmux or screen (start a session, detach with a keybinding, reattach later from any terminal, even a different SSH connection entirely) is the better fit. For a genuinely recurring or continuously running workload, the correct long-term answer is a proper systemd service, which is managed independently of any terminal session at all — that pattern is covered in the systemd module later in this course.

Interview angle

"Why did my backup script die when I closed my SSH session, even though I ran it with &?" is one of the most common early-career support questions, and the answer distinguishes candidates who understand job control from those who only know the syntax. The precise answer: & alone only detaches the command from blocking further terminal input — it does not detach the process from the terminal's session or protect it from SIGHUP on hangup. nohup, disown, a multiplexer, or a proper service unit are the actual mechanisms that provide survival across a disconnect, each suited to a different situation.

Process Groups and Sessions: What & Actually Changes

Every process belongs to a process group, and every process group belongs to a session, typically anchored to a controlling terminal. Only one process group per session can be the foreground process group at a time — the one that receives keyboard-generated signals like SIGINT (Ctrl+C) and SIGTSTP (Ctrl+Z) and is allowed to read from the terminal directly.

ps -o pid,pgid,sid,tpgid,stat,cmd
    PID   PGID    SID  TPGID STAT CMD
   3990   3990   3990   3990 Ss   -bash
  22110  22110   3990   3990 R    sleep 300

TPGID (terminal's current foreground process group) matching a process's own PGID indicates that process is currently in the foreground group for this terminal. & starts a new process group for the background job without making it the terminal's foreground group, which is precisely why Ctrl+C in the terminal does not affect a backgrounded job — the keyboard-generated signal only goes to whichever process group TPGID currently names.

Waiting for Background Jobs to Finish: wait

A script that starts several background jobs and needs to know when all of them are done — rather than exiting immediately while they are still running — uses the shell's own wait builtin:

process-file-a.sh &
process-file-b.sh &
process-file-c.sh &
wait
echo "All three finished"

wait with no arguments blocks until every backgrounded job started by this shell has completed; wait %2 (or wait <pid>) waits for one specific job. This is the standard pattern for a script that fans out independent work across several background processes and needs a single synchronization point before continuing — a basic form of parallelism entirely within shell scripting, without any additional tooling.

A Realistic Scenario: A Long Migration Script Over SSH

Problem. You need to run a multi-hour database migration script over an SSH connection to a remote server, and you cannot guarantee the connection will stay up the entire time, but you also want to be able to check on its progress periodically without waiting at the terminal the whole time.

Check the plan against what actually survives a disconnect:

A plain & is insufficient here — the SSH session closing (intentionally or not) would send SIGHUP and very likely kill the migration midway, which for a database migration is a genuinely dangerous partial-completion state, not just an inconvenience.

Use a multiplexer for both survival and later inspection:

tmux new -s migration

Inside the new tmux session:

./run-migration.sh 2>&1 | tee migration.log

Detach without stopping anything, using the tmux prefix key (default Ctrl+b) followed by d. The migration keeps running inside the detached session, entirely independent of the SSH connection's state.

Reconnect later, from the same or a different terminal, and reattach:

tmux attach -t migration

Verify progress without necessarily reattaching interactively:

tail -n 20 migration.log
ps -o pid,etime,cmd -C run-migration.sh

Conclusion. The combination of tmux (survival plus reattachment) and tee writing to a log file (a persistent record independent of the terminal scrollback) is the right tool for an interactive, long-running, checkable task — nohup alone would have made it survive a disconnect but not given you a way to watch it interactively afterward, and a bare & would very plausibly have lost the migration partway through.

Common Mistakes

Assuming & Alone Survives a Disconnect

& only detaches a job from blocking further terminal input; it does not protect the process from SIGHUP when the terminal's session ends. Pair it with nohup, disown, or run inside a multiplexer for anything that must survive a disconnect.

Confusing the Job Number with the PID

%1 is meaningful only inside the shell session that created it; kill %1 works because the shell translates it before sending anything to the kernel, but kill -9 1 (using the job number as if it were a PID) targets an entirely different, likely nonexistent or critical, process.

Expecting Ctrl+C to Stop a Backgrounded Job

Ctrl+C sends SIGINT only to the terminal's current foreground process group. A job moved to the background with & or bg is no longer in that group, so Ctrl+C has no effect on it — use kill %<job> or kill <pid> instead.

Redirecting Nothing When Backgrounding a Job

A backgrounded job with no output redirection still writes to the terminal, which can interleave confusingly with whatever else you type next, and stops entirely if the job tries to read from a terminal that has moved on to something else. Always redirect both stdout and stderr explicitly for a genuinely detached job.

Using nohup Where a Multiplexer Was Actually Needed

nohup solves survival, not interactive reattachment or progress monitoring. For a task you want to check in on periodically, tmux/screen is the better fit; for a task that should run indefinitely as real infrastructure, a systemd service is the right answer, not either of these.

Exercises

  1. Start sleep 120 &, run jobs, then kill %1 (not kill <pid>), and confirm with a second jobs call that the job is gone. Explain what the shell actually did to translate %1 into something the kernel understands.
  2. Start a foreground sleep 60, pause it with Ctrl+Z, confirm its STAT with ps -o stat, resume it in the background with bg, and then bring it back to the foreground with fg before it finishes. Narrate each state transition.
  3. Start three background jobs that each sleep for a different duration, use wait to block until all three finish, and explain what would have happened to the script's exit timing if wait had been omitted.
  4. Explain, using TPGID and PGID from ps -o pid,pgid,tpgid, why Ctrl+C does not affect a job you have moved to the background with bg, tying your answer to which process group currently owns the terminal's foreground.

Verification criterion: any answer claiming plain & alone is sufficient for a task that must survive a terminal or SSH disconnect is incorrect — your answer must name the additional mechanism (nohup, disown, or a multiplexer) actually responsible for that survival.

References

  • Linux man-pages: bash(1) — Job Control section: https://man7.org/linux/man-pages/man1/bash.1.html
  • GNU Bash Reference Manual: Job Control: https://www.gnu.org/software/bash/manual/html_node/Job-Control.html
  • GNU Bash Reference Manual: nohup: https://www.gnu.org/software/coreutils/manual/html_node/nohup-invocation.html
  • Linux man-pages: nohup(1): https://man7.org/linux/man-pages/man1/nohup.1.html
  • Linux man-pages: setsid(2), setpgid(2): https://man7.org/linux/man-pages/man2/setpgid.2.html
  • tmux project wiki: https://github.com/tmux/tmux/wiki
  • GNU Screen manual: https://www.gnu.org/software/screen/manual/screen.html