Bash & the POSIX Shell Module 12 – Processes, jobs and signals
Job control: Ctrl-Z, fg, bg, jobs, disown
Stop a running command with Ctrl-Z, move it with bg and fg, name jobs with %1 and %name, and see what disown does to a job when the terminal hangs up.
What you will learn
- Use Ctrl-Z, bg, fg and jobs to stop a command and move it between the foreground and the background
- Find a job by number, by the + and - markers, and by name with a job spec such as %name or %?string
- Explain why fg and bg fail in a script and what a process group has to do with it
- Distinguish disown, disown -h and nohup by what each does to a job when the terminal hangs up
Before you start
What you’ll learn · Before you start
What you will learn
- Use Ctrl-Z, bg, fg and jobs to stop a command and move it between the foreground and the background
- Find a job by number, by the + and - markers, and by name with a job spec such as %name or %?string
- Explain why fg and bg fail in a script and what a process group has to do with it
- Distinguish disown, disown -h and nohup by what each does to a job when the terminal hangs up
Before you start
On this page
A test run that takes 12 minutes holds your terminal, and you want the prompt back after 20 seconds. Bash can pause the run, move it behind the prompt and bring it forward again, because an interactive shell: The program that reads command lines, expands them and starts the programs they name, then waits for them to finish. Bash, dash and zsh are shells. More in “Shells, terminals and which Bash you have” keeps a numbered table of jobs and hands the keyboard to one of them at a time.
You will stop a command with Ctrl-Z, resume it with bg and fg, name jobs with %1, %- and %name, see why a script cannot use these commands, and learn what disown changes when a terminal hangs up.
Ctrl-Z stops the foreground job, and bg and fg move it
job control: The shell's ability to stop a running command, continue it in the foreground or the background, and list what it tracks. It is on in interactive shells and off in scripts unless set -m turns it on. is the shell’s ability to stop a command and continue it later. The Bash manual says it is supplied jointly by the terminal driver and the shell: the driver turns a key into a signal: A short message the kernel delivers to a process, with a name, a number and a default action such as ending it. A program can handle or ignore most signals; KILL and STOP cannot be caught. More in “Signals, kill and timeout”, and Bash keeps track of what happened to each command. Bash treats one pipeline as one job and numbers jobs from 1.
Three keys matter. The terminal sends the signal to the job that currently owns the keyboard, called the foreground job:
| Key | stty name | Signal | Effect on the foreground job |
|---|---|---|---|
| Ctrl-C | intr |
SIGINT | ends it, unless the program handles it |
| Ctrl-Z | susp |
SIGTSTP | pauses it, so the shell gets the prompt back |
| Ctrl-\ | quit |
SIGQUIT | ends it, usually with a core dump |
The POSIX stty page names the three characters INTR, QUIT and SUSP, and the session below starts by asking stty which keys they are on this terminal. worker.sh is a stand-in for a long job: it announces itself and then waits until a file called release exists.
#!/bin/bash
# worker.sh: a stand-in for a long job. It says it started, waits until the file "release" exists,
# then says it finished.
echo 'worker: running'
until [ -e release ]; do
sleep 0.2
done
echo 'worker: finished' The session runs Bash 5.3.20 under a pseudo-terminal, so the keys are real. Look for the Stopped line after Ctrl-Z, the different text bg and fg print, and what jobs lists each time:
from session import run, files
files("worker.sh")
run("""
stty -a | tr ';' '\\n' | grep -E ' (intr|quit|susp) '
> bash worker.sh
wait: worker: running
^Z
jobs
bg
jobs
> fg
fg: bash worker.sh
release: worker: finished
rm release
> bash worker.sh
wait: worker: running
^C
jobs
""") Output
$ stty -a | tr ';' '\n' | grep -E ' (intr|quit|susp) ' intr = ^C quit = ^\ susp = ^Z $ bash worker.sh worker: running ^Z [1]+ Stopped bash worker.sh $ jobs [1]+ Stopped bash worker.sh $ bg [1]+ bash worker.sh & $ jobs [1]+ Running bash worker.sh & $ fg bash worker.sh worker: finished $ rm release $ bash worker.sh worker: running ^C $ jobs
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 keys_session.py
After Ctrl-Z, Bash printed [1]+ Stopped and gave the prompt back. The 1 is the job number and the + marks the current job. The job did not end: jobs still lists it, paused. bg then printed the job’s line with an &, which is exactly the form of a job started in the background, and jobs showed it as Running. fg printed the command line and the shell went quiet again, because the job owned the keyboard once more. Here it ended on its own when the release file appeared, which is why the transcript reaches worker: finished without a key. The second run shows the other end: Ctrl-C ended a fresh foreground job, and jobs listed nothing.
Step through the same life as four places a job can be:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The job starts in the foreground. bash worker.sh was typed without an ampersand, so it owns the terminal and printed "worker: running". The shell waits for it and cannot take another command.
- Ctrl-Z stops it. The terminal turns Ctrl-Z into SIGTSTP for the foreground group. The job pauses and the shell prints "[1]+ Stopped" and a prompt. Number 1 is its job number.
- bg lets it run behind the prompt. bg sent SIGCONT and the job runs again, but without the keyboard. jobs now lists it as Running, and the shell accepts commands while it works.
- fg brings it back. fg gave the keyboard back to the job. The shell printed its command line and waits again, so a key now goes to the job, not to the shell.
- Ctrl-C ends it. Ctrl-C sent SIGINT to the foreground group. The job ended, jobs printed nothing afterwards, and the shell took the keyboard back. A job in the background ends with kill or when it finishes.
A job is in the foreground, stopped, in the background, or gone, and each key or command moves it along one arrow. Only the foreground job receives keystrokes.
Key idea
Ctrl-Z pauses the foreground job, bg resumes it behind the prompt, fg resumes it in front, and jobs lists what the shell is tracking. Paused is not finished: a stopped job is still in the table.
The pause has a cost that is easy to forget, because a stopped job is quiet.
Stopped jobs and leaving the shell
If you try to exit while jobs are stopped, Bash prints a warning instead, and a second exit straight away ends the stopped jobs (Bash manual, Job Control Basics). Run jobs, then fg or kill them, before you leave.
Checkpoint
Check: Stopping a job with Ctrl-Z and moving it with bg and fg
A job spec picks a job by number, marker or name
Once there is more than one job, you need a way to say which one. A job spec: A word starting with % that names a job to commands such as fg, bg, kill and wait: %1 by number, %+ and %- by marker, %name by command prefix, %?text by a substring of the command line. is a word that starts with % and that fg, bg, kill, wait, disown and jobs all accept in place of a process id:
| Job spec | Picks |
|---|---|
%1 |
job number 1 |
%+, %% or a lone % |
the current job, marked + in jobs |
%- |
the previous job, marked - |
%name |
the job whose command line starts with name |
%?text |
the job whose command line contains text |
The next session starts three jobs and then picks them in different ways. Watch which job carries the + and the -, the one job spec that fails, and how the markers move after a job ends:
from session import run, files
files("wait.sh")
run("""
sleep 61 &
bash wait.sh &
sleep 62 &
jobs
! fg %sleep
> fg %bash
fg: bash wait.sh
release:
jobs
kill %-; wait %-; echo "status $?"
kill %?62; wait %?62; echo "status $?"
jobs
""") Output
$ sleep 61 & [1] (pid) $ bash wait.sh & [2] (pid) $ sleep 62 & [3] (pid) $ jobs [1] Running sleep 61 & [2]- Running bash wait.sh & [3]+ Running sleep 62 & $ fg %sleep bash: fg: sleep: ambiguous job spec $ fg %bash bash wait.sh $ jobs [1]- Running sleep 61 & [3]+ Running sleep 62 & $ kill %-; wait %-; echo "status $?" [1]- Terminated: 15 sleep 61 status 143 $ kill %?62; wait %?62; echo "status $?" [3]+ Terminated: 15 sleep 62 status 143 $ jobs
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 specs_session.py
The newest job, number 3, got the + and number 2 got the -. fg %sleep failed with ambiguous job spec, because two commands begin with sleep; the manual says Bash reports an error when a prefix matches more than one job. fg %bash matched exactly one, so it ran in the foreground until it ended. Afterwards jobs showed job 1 as the previous job and job 3 still as the current one, so kill %- reached sleep 61. The wait %- that followed returned 143, which is 128 plus 15, the exit status of a process ended by SIGTERM (the exit status lesson has the table). The last job was picked by text: %?62 found sleep 62 by the 62 inside its command line.
Shortcuts for the two commands you use most
A job spec on its own is a command: %1 means fg %1, and %1 & means bg %1 (Bash manual, Job Control Basics). With no argument, fg, bg and kill %+ act on the current job.
The shortcut only helps when the spec names one job, and a prefix is where that goes wrong.
A prefix that matches two jobs
Wrong: typing kill %sleep when two sleep jobs exist; Bash refuses with an ambiguity error and nothing is killed. Right: use the job number, or a longer prefix or text that appears in one command line only, such as %?62.
Checkpoint
Check: Picking a job with %1, %+, %-, %name and %?text
Scripts run with job control off, and process groups explain why
To let a key reach one job and not the others, the shell puts each job in its own process group: A set of processes the system can signal together. A shell with job control puts each job in its own group, and the terminal sends keyboard signals such as Ctrl-C only to the foreground group., a set of processes that the system can signal together. The terminal has exactly one foreground process group at a time. The manual describes the rule: members of the foreground group receive keyboard-generated signals such as SIGINT, and processes in other groups are immune to them.
That arrangement is switched on by the monitor option, which set -m toggles. POSIX ties job control to that option and turns it on by default only in a shell that runs interactively. A script is not interactive, so it starts with the option off. The session asks Bash directly, then tries fg in a script, then runs a small script with and without the option:
#!/bin/bash
# groups.sh: start a background job, then ask whether it has a process group of its own.
sleep 30 &
if kill -0 -- "-$!" 2> /dev/null; then
echo 'the job has its own process group'
else
echo 'the job shares the script process group'
fi
kill "$!" from session import run, files
files("groups.sh")
run("""
set -o | grep monitor
bash -c 'set -o | grep monitor'
bash -c 'sleep 30 & fg; kill $!'
bash -c 'sleep 30 & jobs; kill %1 && echo "kill %1 worked"; wait'
bash groups.sh
bash -m groups.sh
""") Output
$ set -o | grep monitor monitor on $ bash -c 'set -o | grep monitor' monitor off $ bash -c 'sleep 30 & fg; kill $!' bash: line 1: fg: no job control $ bash -c 'sleep 30 & jobs; kill %1 && echo "kill %1 worked"; wait' [1]+ Running sleep 30 & kill %1 worked $ bash groups.sh the job shares the script process group $ bash -m groups.sh the job has its own process group
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 scripts_session.py
The interactive shell printed monitor on and the bash -c one printed monitor off. With the option off, fg stopped with fg: no job control, and bg fails the same way. groups.sh asks whether the job it started has a process group of its own with kill -0 -- "-$!": a minus sign before the number means the group with that id, and signal 0 sends nothing and only reports whether the target exists. Run as bash groups.sh, the job shared the script’s group; run as bash -m groups.sh, it had a group of its own.
| Interactive shell | Script | |
|---|---|---|
monitor option |
on | off |
fg and bg |
work | fail with no job control |
| Background job’s process group | its own | the script’s |
| Keys such as Ctrl-C | reach the foreground job only | reach the script and what it started |
Job specs are less certain in a script
The manual says kill and wait need process ids when job control is off. Yet in the session, jobs listed a job and kill %1 ended it inside bash -c, with the option off. Treat job specs as an interactive habit: in a script, store $! when you start a job and use that.
Checkpoint
Check: Why job control is off in scripts, and process groups
disown and nohup decide whether a job survives a hang-up
Closing a terminal window, or losing the connection behind it, makes the terminal send SIGHUP to the shell. The Bash manual says an interactive shell that receives SIGHUP exits, and before that it resends the SIGHUP to every job in its table, running or stopped; it sends SIGCONT first to stopped jobs so that they can receive it. A job ends only if it does not handle or ignore the signal, and most commands do not.
Two builtins take a job out of that handover. Plain disown %2 removes the job from the table, so the shell no longer knows it. disown -h %2 keeps the job in the table but marks it, so the shell does not send it SIGHUP. The session starts an inner shell, starts three sleep jobs, treats them differently, and sends the inner shell a SIGHUP as a closing terminal would; alive.sh then looks for each sleep:
#!/bin/bash
# alive.sh N: say whether "sleep N" is still running. It looks a few times, because a process that
# got SIGHUP needs a moment to disappear.
for _ in 1 2 3 4 5 6 7 8 9 10; do
pgrep -f "sleep $1" > /dev/null || break
sleep 0.2
done
if pgrep -f "sleep $1" > /dev/null; then echo "sleep $1: still running"; else echo "sleep $1: gone"; fi from session import run, files
files("alive.sh")
run("""
# An inner shell stands in for a terminal window that is about to be closed.
bash --norc --noprofile -i
sleep 7101 &
sleep 7102 &
sleep 7103 &
disown -h %2
disown %3
jobs
kill -HUP $$
""")
run("""
alive.sh 7101
alive.sh 7102
alive.sh 7103
pkill -f 'sleep 710[23]'
""") Output
$ bash --norc --noprofile -i $ sleep 7101 & [1] (pid) $ sleep 7102 & [2] (pid) $ sleep 7103 & [3] (pid) $ disown -h %2 $ disown %3 $ jobs [1]- Running sleep 7101 & [2]+ Running sleep 7102 & $ kill -HUP $$ Hangup: 1 bash --norc --noprofile -i $ alive.sh 7101 sleep 7101: gone $ alive.sh 7102 sleep 7102: still running $ alive.sh 7103 sleep 7103: still running $ pkill -f 'sleep 710[23]'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hangup_session.py
jobs listed jobs 1 and 2 only, because disown %3 had removed the third, while disown -h %2 kept job 2 listed. After the hang-up, the outer shell reported Hangup: 1 for the inner shell, sleep 7101 was gone, and both sleep 7102 and sleep 7103 were still running. Step through the same hang-up:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Three jobs, three treatments. The shell started sleep 7101, 7102 and 7103. It ran disown -h %2 and then disown %3, so job 1 is untouched, job 2 is marked and job 3 has left the shell's table.
- The terminal hangs up. Closing a terminal window or dropping a connection makes the terminal send SIGHUP to the shell. The session sent kill -HUP to its inner shell to do the same.
- The shell passes the hang-up on. An interactive shell that gets SIGHUP resends it to every job in its table, except those marked by disown -h. Job 1 gets it. Job 2 is skipped. Job 3 is not in the table at all.
- Only job 1 is gone. sleep 7101 died of SIGHUP. sleep 7102 and sleep 7103 were still running when the session looked, so a marked job and a forgotten job both survive the hang-up.
| Command | What it changes |
|---|---|
disown %n |
the shell forgets the job: no SIGHUP, and jobs no longer lists it |
disown -h %n |
the job stays listed, but the shell does not send it SIGHUP |
nohup cmd |
starts a new command that ignores SIGHUP from the first instant |
nohup is the third route and acts on a job before it starts; the background jobs lesson shows its redirections. Both disown forms act on a job that already runs. A separate shell option, huponexit, makes a login shell send SIGHUP to all its jobs when it exits normally; it is off by default.
jobs and finished jobs in Bash 5.3
The Bash 5.3 NEWS says it implements the POSIX requirement that running the jobs builtin removes jobs from the jobs list. This lesson’s sessions were recorded on 5.3.20; an older Bash may keep a reported job in the list for longer.
Checkpoint
Check: What disown, disown -h and nohup do at a hang-up
Practise: take a job through every state
In your own terminal, start sleep 300. Press Ctrl-Z, run jobs, then bg, then jobs again, and finish with fg followed by Ctrl-C. The two jobs lines should read Stopped and then Running, and the shell should have no job left at the end. Next start sleep 301 & and sleep 302 &, then end the older one with a job spec built from the - marker, and the newer one with a spec built from the text 302. Check that wait on each returns 143 and that a last jobs prints nothing. If a spec is refused, read the ambiguity message and choose a longer one.
Practice that adapts to you
Practise this lesson
9 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): what does Ctrl-Z send, and what do bg, fg and jobs do next?
Ctrl-Z makes the terminal send SIGTSTP to the foreground process group, which pauses the job and returns the prompt; the shell prints a line such as [1]+ Stopped with the job number. The job is paused, not ended, and stays in the shell’s job table. jobs lists the table with each job’s number, its state and its command line, and + and - mark the current and previous jobs. bg sends the paused job SIGCONT and lets it run in the background, as if it had been started with &, so the shell takes commands again. fg also continues the job but gives it the terminal, so the shell waits for it and keyboard input and Ctrl-C go to the job. With no argument, both act on the current job; %n or %name picks another.
The two harder questions are about situations where the keyboard cannot do what you need: a job that must outlive its terminal, and a script that switched job control on. Decide what you would check first, then read the worked first step on each page:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A 40-minute make runs in the foreground of an SSH session, and the Wi-Fi is about to drop. You cannot restart the build. What can you do right now, what does each step protect against and what does it leave exposed, and how would you start such a job next time?
-
Mid to senior
A wrapper script turns on set -m so it can use kill %1. Since then, Ctrl-C ends the wrapper but leaves its workers running, and a worker that reads standard input shows as Stopped. Explain both symptoms, judge whether set -m is the right tool here and give an alternative.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does Ctrl-Z do to the foreground job?
The terminal sends SIGTSTP to the foreground process group, which pauses the job and gives the prompt back. The job is stopped, not finished, and stays in the shell's job table.
-
What do bg, fg and jobs do?
bg resumes a stopped job in the background, fg resumes it in the foreground, and jobs lists the job table with each job's number, state and command, + marking the current job and - the previous one.
-
How do you name a job in fg, bg or kill?
With a job spec starting with %: %1 by number, %+ and %- by marker, %name by the start of its command and %?text by text anywhere in it. Bash refuses a prefix that matches two jobs.
-
Why do fg and bg fail inside a script?
Job control is on in interactive shells and off in scripts, so they answer no job control. set -m turns it on, and then each job gets its own process group.
-
Why does job control give each job its own process group?
So the terminal can send keys such as Ctrl-C and Ctrl-Z to the foreground job alone, while background jobs keep running untouched.
-
How can a job survive the hang-up of its terminal?
At a hang-up an interactive shell resends SIGHUP to every job in its table. disown removes a job from the table, disown -h keeps it listed but marked so it gets no SIGHUP, and nohup starts a command that ignores SIGHUP.
Key takeaways
- Ctrl-Z sends SIGTSTP to the foreground process group and pauses the job;
bgresumes it in the background,fgresumes it in the foreground, andjobslists the table with a number and a+or-marker. - A job spec starting with
%picks a job:%1by number,%+and%-by marker,%nameby command prefix and%?textby substring, and Bash refuses a prefix that matches two jobs. - Job control gives each job its own process group so a key reaches only the foreground one; it is on in interactive shells and off in scripts, where
fgandbgfail withno job controlunlessset -mturns it on. - At a hang-up an interactive shell resends SIGHUP to every job in its table;
disownremoves a job from the table,disown -hkeeps it listed but marked, andnohupstarts a command that ignores SIGHUP.
Terms in this lesson
- job control
- The shell's ability to stop a running command, continue it in the foreground or the background, and list what it tracks. It is on in interactive shells and off in scripts unless set -m turns it on.
- job spec
- A word starting with % that names a job to commands such as fg, bg, kill and wait: %1 by number, %+ and %- by marker, %name by command prefix, %?text by a substring of the command line.
- process group
- A set of processes the system can signal together. A shell with job control puts each job in its own group, and the terminal sends keyboard signals such as Ctrl-C only to the foreground group.
References
- Bash Reference Manual: Job Control Basics (Chet Ramey (GNU Bash maintainer))
- Bash Reference Manual: Job Control Builtins (bg, fg, jobs, kill, wait, disown) (Chet Ramey (GNU Bash maintainer))
- Bash Reference Manual: Signals (what a shell does on SIGHUP) (Chet Ramey (GNU Bash maintainer))
- POSIX.1-2024 Shell Command Language: 2.11 Job Control (The Open Group and IEEE)
- POSIX.1-2024: stty (the INTR, QUIT and SUSP characters) (The Open Group and IEEE)
- signal(7): the standard signals and their default actions (man7.org (Linux man-pages))
- Bash NEWS: new features in bash-5.3 (Chet Ramey (GNU Bash maintainer))
Report a problem with this lesson
Kept only in this browser. Your Learn progress