job control

Suspend, resume and detach what you started

Updated 2026-08-29

A job is one thing your shell started, and it is not the same unit as a process: a pipeline of four commands is four processes and one job. jobs, fg, bg and disown are shell builtins that work on that list, so they only ever know about commands this shell started. Another terminal's background job is invisible to them, and only ps sees across the whole machine.

The numbers in brackets are not PIDs

[1] is a job number, counted per shell and reused as jobs finish. Anywhere a command takes a job, %1 names it, %+ is the current job and %- the previous one, and %str matches a job whose command begins with str. The + and - in the jobs output mark those last two.

kill accepts a job spec as well as a PID, so kill %1 and kill 4749 can be the same instruction. $! holds the PID of the job you started most recently. Save it into a variable if you want to signal that job later.

None of this works in a script

Job control is a feature of an interactive shell, and bash switches it off everywhere else. A script that calls fg gets no job control and stops; %1 is not understood; Ctrl-Z has no meaning where there is no terminal to send it. set -m turns it back on, and is occasionally the right answer, but a script that wants to manage several children usually wants $! and wait instead.

The examples below use set -m wherever a job has to be suspended, because otherwise the shell never notices it happened.

Ctrl-Z, and what comes after it

Ctrl-Z suspends the foreground job by sending it TSTP. The process stops where it is, keeping its memory and its open files, and does nothing until something resumes it. fg resumes it in the foreground, bg resumes it in the background, and both send CONT to do so.

The usual reason to want it is that you started something long without an & and would like your prompt back. Ctrl-Z, then bg, then disown if you also intend to close the terminal. Keeping a program running after you log out has the case where that is not good enough.

36 outputs, collapsed by default

The jobs table

Every example here starts its own jobs, so there is no sample data to declare. They all send the job's output to /dev/null, as you would with a real background job: anything it prints otherwise lands in the middle of whatever you are typing. jobs echoes the command back exactly as you gave it, redirect included.

List the shell's jobs

sleep 300 >/dev/null 2>&1 & jobs

& starts the command in the background and the shell adds it to a table. jobs prints that table: the job number in brackets, the state, and the command as you typed it.

Show output
[1]+  Running                 sleep 300 > /dev/null 2>&1 &

Read the plus and minus markers

sleep 300 >/dev/null 2>&1 & sleep 200 >/dev/null 2>&1 & sleep 100 >/dev/null 2>&1 & jobs

+ marks the current job and - the one before it. Anything that takes a job spec and is given none acts on the + job, so those two characters decide what a bare fg or bg will do.

Show output
[1]   Running                 sleep 300 > /dev/null 2>&1 &
[2]-  Running                 sleep 200 > /dev/null 2>&1 &
[3]+  Running                 sleep 100 > /dev/null 2>&1 &

Start a pipeline in the background

sleep 300 2>&1 | cat >/dev/null 2>&1 & jobs

The two programs share a single entry. A whole pipeline counts as one job, so suspending or killing it takes every stage with it instead of leaving cat reading from a pipe nothing writes to.

Show output
[1]+  Running                 sleep 300 2>&1 | cat > /dev/null 2>&1 &

See the processes inside that one job

sleep 300 2>&1 | cat >/dev/null 2>&1 & jobs -l

-l breaks the job open: one PID per stage, indented under the job number. Job and process are different units, and this is the listing where that stops being an abstract point.

Show output

Your output will differ: the PIDs will differ, and their width sets the indentation of the second line

[1]+   196 Running                 sleep 300 2>&1
       197                       | cat > /dev/null 2>&1 &

Show the PID beside each job

sleep 300 >/dev/null 2>&1 & sleep 200 >/dev/null 2>&1 & jobs -l

-l adds the process id. That is the column you need when handing a job to something outside the shell.

Show output

Your output will differ: the PIDs will differ

[1]-  4749 Running                 sleep 300 > /dev/null 2>&1 &
[2]+  4750 Running                 sleep 200 > /dev/null 2>&1 &

Print only the PIDs

sleep 300 >/dev/null 2>&1 & sleep 200 >/dev/null 2>&1 & jobs -p

-p drops everything but the PIDs, a line each, ready to pipe into kill or xargs.

Show output

Your output will differ: the PIDs will differ

4759
4760

Ask about one job

sleep 300 >/dev/null 2>&1 & sleep 200 >/dev/null 2>&1 & jobs %2

A job spec narrows the listing to one entry.

Show output
[2]+  Running                 sleep 200 > /dev/null 2>&1 &

Separate the running from the stopped

set -m; sleep 300 >/dev/null 2>&1 & sleep 200 >/dev/null 2>&1 & kill -STOP %1; sleep 0.3; jobs -r; echo ---; jobs -s

-r lists only running jobs and -s only stopped ones. Note that stopping job 1 also made it the current job: the + moved.

Show output
[2]-  Running                 sleep 200 > /dev/null 2>&1 &
---
[1]+  Stopped                 sleep 300 > /dev/null 2>&1

See a job that has finished

sleep 0.2 & sleep 0.5; jobs

A completed job stays in the table until it is reported once, then disappears. That is why an interactive shell prints [1]+ Done at some arbitrary later moment rather than the instant the job ended: it tells you the next time it draws a prompt.

Show output
[1]+  Done                    sleep 0.2

See a job that was signalled

sleep 300 >/dev/null 2>&1 & kill %1; sleep 0.3; jobs

Terminated rather than Done, because the job did not choose to exit. A job killed with -9 reports Killed here instead.

Show output
[1]+  Terminated              sleep 300 > /dev/null 2>&1

Naming a job

Signal a job by number

sleep 300 >/dev/null 2>&1 & kill %1; sleep 0.3; jobs

kill takes %1 wherever it takes a PID. The shell resolves the spec and signals the job's process group, so a suspended pipeline goes as one thing rather than a stage at a time.

Show output
[1]+  Terminated              sleep 300 > /dev/null 2>&1

Name the current and previous jobs

sleep 300 >/dev/null 2>&1 & sleep 200 >/dev/null 2>&1 & jobs %+; jobs %-

%+ and %- are the two the markers point at. %% is another spelling of %+.

Show output
[2]+  Running                 sleep 200 > /dev/null 2>&1 &
[1]-  Running                 sleep 300 > /dev/null 2>&1 &

Name a job by how its command starts

sleep 300 >/dev/null 2>&1 & jobs %sle

%str matches the job whose command line begins with str. It is an error rather than a guess if two jobs match, so this is safe to use interactively and unwise in a script.

Show output
[1]+  Running                 sleep 300 > /dev/null 2>&1 &

Name a job by any part of its command

sleep 300 >/dev/null 2>&1 & kill %?300; sleep 0.3; jobs

%?str matches on a substring rather than a prefix. Use it when the interesting part of the command is an argument.

Show output
[1]+  Terminated              sleep 300 > /dev/null 2>&1

Capture the PID of the job you just started

sleep 300 >/dev/null 2>&1 & p=$!; kill "$p"; sleep 0.3; jobs

$! is the PID of the most recent background command. Save it into a variable straight away, because the next & overwrites it, and a script that wants to signal one particular child later has nothing else to go on.

Show output
[1]+  Terminated              sleep 300 > /dev/null 2>&1

Wait for a specific job

sleep 1 & p=$!; wait $p; echo "exit was $?"

wait blocks until the job finishes and then reports its exit status. A script that fans work out collects it back this way.

Show output
exit was 0

Wait for everything

sleep 0.5 & sleep 1 & wait; echo "all done"

With no argument, wait returns when every background job has finished. This is the last line of most scripts that fan work out.

Show output
all done

Wait for whichever finishes first

sleep 0.5 & sleep 2 & wait -n; echo "first finished"

wait -n returns as soon as any one job ends. A script keeping a fixed number of workers busy needs that, since waiting for the whole batch idles every worker until the slowest finishes.

Show output
first finished

Suspending and resuming

Ctrl-Z sends TSTP to the foreground job. No example can press it, so these use kill -STOP, the same thing minus the ability to be caught. All of them set set -m first, for the reason the next section covers.

Suspend a running job

set -m; sleep 300 >/dev/null 2>&1 & kill -STOP %1; sleep 0.3; jobs

The job stops where it is. It keeps its memory, its open files and any lock it was holding, so a suspended job is not a cheap thing to leave lying about.

Show output
[1]+  Stopped                 sleep 300 > /dev/null 2>&1

Resume it in the background

set -m; sleep 300 >/dev/null 2>&1 & kill -STOP %1; sleep 0.3; bg %1; sleep 0.3; jobs

bg sends CONT and leaves the job in the background. It echoes the job it acted on, and that is the first line here.

Show output
[1]+ sleep 300 > /dev/null 2>&1 &
[1]+  Running                 sleep 300 > /dev/null 2>&1 &

Resume it in the foreground

set -m; sleep 1 & fg

fg prints the command it is resuming and then waits for it, so the shell has no prompt until the job ends. A one-second job is used here for the obvious reason.

Show output
sleep 1

Bring back the current job with no argument

set -m; sleep 1 & sleep 300 >/dev/null 2>&1 & fg %1

A bare fg takes the + job: the most recently started or suspended one, rather than the one you were last thinking about. Name the job when there is more than one.

Show output
sleep 1

Check that a stopped job is really stopped

set -m; sleep 300 >/dev/null 2>&1 & kill -STOP %1; sleep 0.3; ps -o state= -p $!

T in the ps state column, the same letter the kill page uses. The shell's Stopped and the kernel's T are two views of one fact.

Show output
T

Why none of it works in a script

Job control belongs to an interactive shell. Everywhere else bash switches it off, and the failure is quiet in one direction and loud in the other.

Call fg from a script

printf "fg\n" > try.sh; bash try.sh

The loud direction. fg, bg and %1 all need job control, and a script has none, so the builtin refuses rather than guessing.

Show output
try.sh: line 1: fg: no job control

Stop a job without job control

sleep 300 >/dev/null 2>&1 & kill -STOP %1; sleep 0.5; jobs; ps -o state= -p $!

The quiet direction, and the more dangerous of the two. The process really is stopped, which the T on the second line shows, but the shell still calls it Running: without job control it is not told when a child stops, only when one exits.

Show output
[1]+  Running                 sleep 300 > /dev/null 2>&1 &
T

Turn job control on

set -m; sleep 300 >/dev/null 2>&1 & kill -STOP %1; sleep 0.5; jobs; ps -o state= -p $!

The same script with set -m. Nothing about the process changed; the shell is now watching for the notification it was ignoring before.

Show output
[1]+  Stopped                 sleep 300 > /dev/null 2>&1
T

Outliving the shell

A background job is still the shell's child, and it is still in the shell's jobs table. Both of those have to be dealt with for it to survive the terminal going away, and the three usual commands each deal with a different one.

Read the mask of signals a background job ignores

sleep 300 >/dev/null 2>&1 &
sleep 0.3
mask=$(awk '/^SigIgn/{print $2}' /proc/$!/status)
printf '%08x\n' $(( 0x$mask & 0x7fffffff ))

SigIgn in /proc/<pid>/status is the set of signals a process ignores, as hex, one bit per signal with bit 0 standing for signal 1. 6 is binary 110, so bits 1 and 2, so signals 2 and 3: INT and QUIT. Bash starts every background job that way, which is why Ctrl-C has never reached one. The & 0x7fffffff keeps signals 1 to 31, the standard ones. Above those sit the real-time signals, which the C library claims a couple of for its own use, and which of them show up here is not the same on every machine.

Show output
00000006

See the one bit nohup sets

nohup sleep 300 >/dev/null 2>&1 &
sleep 0.3
mask=$(awk '/^SigIgn/{print $2}' /proc/$!/status)
printf '%08x\n' $(( 0x$mask & 0x7fffffff ))

7 rather than 6: bit 0 is now set, so HUP joins the two bash had already blocked. nohup sets that and then executes the program, so the setting is in place before the program runs a single instruction and there is nothing for it to override.

Show output
00000007

Read those bits by name instead of counting them

sleep 300 >/dev/null 2>&1 &
sleep 0.3
mask=$(awk '/^SigIgn/{print $2}' /proc/$!/status)
for sig in HUP INT QUIT; do
  echo "$sig $(( (0x$mask >> ($(kill -l $sig) - 1)) & 1 ))"
done

kill -l turns a signal name into its number, and the number into the bit to shift down to. The same 6 as two examples ago, spelled out: HUP is the gap, and it is the gap the three commands below each close differently.

Show output
HUP 0
INT 1
QUIT 1

Confirm nohup closed the gap

nohup sleep 300 >/dev/null 2>&1 &
sleep 0.3
mask=$(awk '/^SigIgn/{print $2}' /proc/$!/status)
for sig in HUP INT QUIT; do
  echo "$sig $(( (0x$mask >> ($(kill -l $sig) - 1)) & 1 ))"
done

All three ignored. This is the whole of what nohup does about signals, and it is worth knowing how little that is: the job still dies if anything sends it TERM, and it is still in the shell's jobs table until something takes it out.

Show output
HUP 1
INT 1
QUIT 1

Start something with nohup

nohup ./long-import.sh > import.log 2>&1 &

The form to actually type. Without a redirect nohup sends output to nohup.out in the current directory, but only when it started from a terminal, so a redirect you chose yourself is one less thing to be surprised by. There is no output block here because that terminal-only behaviour cannot be reproduced in a pipeline.

Take a job out of the shell's table

sleep 300 >/dev/null 2>&1 & disown; jobs; echo "still running: $(pgrep -c -x sleep)"

jobs prints nothing and the process is still there. disown removes the entry, so the shell no longer signals it on exit and no longer reports on it.

Show output
still running: 1

Disown one job by name

sleep 300 >/dev/null 2>&1 & sleep 200 >/dev/null 2>&1 & disown %1; jobs

With a job spec, only that entry goes. Job 2 is still the shell's business.

Show output
[2]+  Running                 sleep 200 > /dev/null 2>&1 &

Disown everything at once

sleep 300 >/dev/null 2>&1 & sleep 200 >/dev/null 2>&1 & disown -a; jobs; echo "still running: $(pgrep -c -x sleep)"

-a clears the table. Both processes are running and neither is the shell's concern any more.

Show output
still running: 2

Keep the job listed but shield it from HUP

sleep 300 >/dev/null 2>&1 & disown -h %1; jobs

-h is the middle option: the job stays in the table, so fg and jobs still work on it, but the shell will not send it HUP when it exits. Reach for this when you want the job back later and are only worried about the disconnection.

Show output
[1]+  Running                 sleep 300 > /dev/null 2>&1 &

Start something in a session of its own

setsid sleep 300 >/dev/null 2>&1 & sleep 0.4; pgrep -c -x sleep

setsid does not touch the jobs table or the signal mask. It puts the process in a new session with no controlling terminal at all, so there is nothing that could hang up on it. This is the mechanism a daemon uses.

Show output
1

Check whether a job survived the shell

bash -c 'sleep 300 >/dev/null 2>&1 & disown' ; sleep 0.4; pgrep -c -x sleep

The inner shell started a job, disowned it and exited. The sleep is still running, now reparented to PID 1. Processes and signals covers what reparenting means for anything you left behind.

Show output
1