Linux Job Control
last modified October 23, 2026
Job control lets us run several programs from one terminal: we start
commands in the background, suspend the program in the foreground, and
switch between them. A job is a pipeline started from the shell;
the shell numbers jobs and lets us refer to them as %1,
%2, and so on. Job control is a feature of the shell, so the
commands jobs, fg, bg, and
disown are shell builtins.
The examples were run in an interactive Bash 5.2 session. Job control works the same way in zsh and other POSIX shells, though the format of the status messages differs. Process IDs are those of the test system and differ on every run. Sections that use scripts run them without job control, as non-interactive shells do.
Running a command in the background
An ampersand at the end of a command runs it in the background. The shell does not wait for it to finish and immediately shows a new prompt.
sleep 300 & echo $! jobs
The shell prints the job number in brackets and the process ID. The
special parameter $! holds the PID of the most recent
background job, which is useful in scripts. The jobs command
lists the jobs of the current shell.
$ sleep 300 & [1] 20 $ echo $! 20 $ jobs [1]+ Running sleep 300 &
Listing jobs
The jobs command shows each job's number, state, and command.
sleep 100 & sleep 200 & sleep 300 & jobs jobs -l jobs -p
The + marks the current job, the one that fg and
bg use when no job is given, and - marks the
previous job. The -l option adds process IDs, and
-p prints only the PIDs, for example for kill.
Jobs belong to one shell; another terminal window does not see them.
$ sleep 100 & [1] 34 $ sleep 200 & [2] 36 $ sleep 300 & [3] 38 $ jobs [1] Running sleep 100 & [2]- Running sleep 200 & [3]+ Running sleep 300 & $ jobs -l [1] 34 Running sleep 100 & [2]- 36 Running sleep 200 & [3]+ 38 Running sleep 300 & $ jobs -p 34 36 38
Suspending and resuming
Pressing Ctrl+Z suspends the foreground job. The
bg command resumes it in the background, and
fg brings it back to the foreground.
sleep 400 ^Z jobs bg jobs fg ^C jobs
Ctrl+Z sends the SIGTSTP signal; the process is
stopped and the shell prompt returns. bg continues it in the
background as if it had been started with &. In the
foreground, Ctrl+C sends SIGINT, which terminates
the program. A typical use is an editor: we suspend vim with
Ctrl+Z, run a few commands, and return to it with
fg.
$ sleep 400 ^Z [1]+ Stopped sleep 400 $ jobs [1]+ Stopped sleep 400 $ bg [1]+ sleep 400 & $ jobs [1]+ Running sleep 400 & $ fg sleep 400 ^C $ jobs
Job specifications
Builtins such as fg, bg, kill, and
wait accept job specifications that start with
%.
sleep 100 & sleep 200 & tail -f /dev/null & jobs kill %1 jobs kill %?200 %tail jobs
%1 refers to job number 1, %tail to the job whose
command starts with tail, and %?200 to the job
whose command contains 200. %+ or
%% is the current job and %- the previous one.
Unlike PIDs, job numbers are short and do not have to be looked up. The
shell reports that a job has terminated the next time it lists jobs or
prints a prompt.
$ sleep 100 & [1] 71 $ sleep 200 & [2] 73 $ tail -f /dev/null & [3] 75 $ jobs [1] Running sleep 100 & [2]- Running sleep 200 & [3]+ Running tail -f /dev/null & $ kill %1 $ jobs [1] Terminated sleep 100 [2]- Running sleep 200 & [3]+ Running tail -f /dev/null & $ kill %?200 %tail $ jobs [2]- Terminated sleep 200 [3]+ Terminated tail -f /dev/null
Output of background jobs
A background job still writes to the terminal, and its output is mixed with
whatever we are doing. The count.sh script prints a line every
second.
./count.sh & echo hello ./count.sh > count.log 2>&1 & cat count.log
The ticks appear right after the prompt, and we type echo
hello into the middle of the output; the command still works, but
the screen is confusing. The Done message is printed at the
next prompt. For anything but short jobs, we redirect both standard output
and standard error to a file and read it when needed.
$ ./count.sh & [1] 93 $ tick 1 tick 2 tick 3 echo hello hello [1]+ Done ./count.sh $ ./count.sh > count.log 2>&1 & [1] 100 $ cat count.log tick 1 tick 2 tick 3 [1]+ Done ./count.sh > count.log 2>&1
Background jobs and input
A background job cannot read from the terminal. When it tries, it is stopped.
cat > notes.txt & jobs fg buy milk ^D cat notes.txt
The kernel stops the background cat with the
SIGTTIN signal as soon as it reads from the terminal. After
fg, it gets the input it was waiting for; we type a line and
end the input with Ctrl+D. A background job that must not wait
for input should get its input from a file or from <
/dev/null.
$ cat > notes.txt & [1] 117 $ jobs [1]+ Stopped cat > notes.txt $ fg cat > notes.txt buy milk $ cat notes.txt buy milk
Exiting with stopped jobs
The shell warns us when we try to exit while jobs are stopped.
sleep 500 ^Z exit jobs kill %1 jobs
After the warning, a second exit would leave the shell and
terminate the stopped jobs. Here we kill the job instead. A stopped
process cannot handle a signal, so Bash's kill builtin also
sends SIGCONT to a stopped job; the Stopped line
right after kill is a pending notification that is already
outdated, and the next jobs shows the real state.
$ sleep 500 ^Z [1]+ Stopped sleep 500 $ exit exit There are stopped jobs. $ jobs [1]+ Stopped sleep 500 $ kill %1 [1]+ Stopped sleep 500 $ jobs [1]+ Terminated sleep 500
Running jobs in parallel
In scripts, & and the wait builtin run tasks
concurrently. Job control (the fg/bg part) is off
in scripts, but background jobs and wait work.
cat parallel.sh time ./parallel.sh
The script starts three tasks that take 3, 1, and 2 seconds, and
wait without arguments waits for all of them. The whole script
takes three seconds instead of six, and the tasks finish in order of their
duration, not in the order they were started.
$ cat parallel.sh
#!/bin/bash
for n in 3 1 2; do
(sleep "$n"; echo "task $n done") &
done
echo "started $(jobs -p | wc -l) jobs"
wait
echo "all done"
$ time ./parallel.sh
started 3 jobs
task 1 done
task 2 done
task 3 done
all done
real 0m3.007s
user 0m0.004s
sys 0m0.013s
Exit status of background jobs
The exit status of a background job is returned by wait when
it is given the job's PID.
#!/bin/bash sleep 1 & p1=$! (sleep 2; exit 3) & p2=$! wait "$p1" echo "job $p1 exited with $?" wait "$p2" echo "job $p2 exited with $?"
We save the PID from $! right after starting each job. The
second job exits with status 3.
./status.sh
wait PID returns the exit status of that process, so a script
can detect which of its parallel tasks failed. Bash also has wait
-n, which waits for whichever job finishes next.
$ ./status.sh job 157 exited with 0 job 158 exited with 3
Closing the terminal
When a terminal window is closed or an SSH connection drops, the shell
receives SIGHUP (hang up) and passes it on to its jobs, which
usually terminates them. We simulate closing the terminal by sending the
signal to the shell itself.
sleep 1001 & kill -HUP $$ pgrep -a sleep; echo "exit: $?"
$$ is the PID of the shell. The shell exits, and the last
command, run afterwards from another shell, finds no sleep
process: the job was killed together with the shell. The following
sections show how to keep jobs running.
$ sleep 1001 & [1] 170 $ kill -HUP $$ $ pgrep -a sleep; echo "exit: $?" exit: 1
nohup
The nohup command starts a program that ignores
SIGHUP.
nohup ./worker.sh & kill -HUP $$ pgrep -a worker cat nohup.out
Because the output would otherwise go to a terminal that may disappear,
nohup appends it to nohup.out and tells us so (the
message appears after the prompt, because the job prints it after the
shell has already shown the prompt). After the shell is gone, the worker is
still running. Usually we redirect the output ourselves: nohup
./worker.sh > worker.log 2>&1 &.
$ nohup ./worker.sh & [1] 184 $ nohup: ignoring input and appending output to 'nohup.out' kill -HUP $$ $ pgrep -a worker 184 /bin/bash ./worker.sh $ cat nohup.out worker 184 started
disown
The disown builtin protects a job that is already running,
when we forgot to use nohup.
sleep 1002 & sleep 1003 & jobs disown %1 disown -h %2 jobs kill -HUP $$ pgrep -a sleep
disown %1 removes the job from the job table, so the shell no
longer knows about it and does not send it SIGHUP.
disown -h %2 keeps the job in the table, so it can still be
managed with fg or kill %2, but marks it not to
receive the signal. Both processes survive. A suspended job should be
resumed with bg before it is disowned, otherwise it stays
stopped.
$ sleep 1002 & [1] 203 $ sleep 1003 & [2] 205 $ jobs [1]- Running sleep 1002 & [2]+ Running sleep 1003 & $ disown %1 $ disown -h %2 $ jobs [2]+ Running sleep 1003 & $ kill -HUP $$ $ pgrep -a sleep 203 sleep 1002 205 sleep 1003
Detaching with setsid
The setsid command runs a program in a new session, completely
detached from the terminal.
setsid -f ./worker.sh > worker.log 2>&1 ps -o pid,ppid,sid,tty,cmd -C worker.sh cat worker.log
The -f option forks, so the command returns immediately. The
ps output shows that the worker leads its own session (its SID
equals its PID) and has no controlling terminal (?); its
parent is PID 1, because the intermediate process of setsid has
already exited. No hang-up of our terminal can reach it. For services
that must run permanently, a systemd unit is the better solution; for
interactive work that should survive a disconnect, use a terminal
multiplexer such as tmux or screen.
$ setsid -f ./worker.sh > worker.log 2>&1
$ ps -o pid,ppid,sid,tty,cmd -C worker.sh
PID PPID SID TT CMD
217 1 217 ? /bin/bash ./worker.sh
$ cat worker.log
worker 217 started
Job control commands
| Command | Description |
|---|---|
command & | Run in the background |
Ctrl+Z | Suspend the foreground job |
Ctrl+C | Interrupt the foreground job |
jobs | List jobs (-l with PIDs) |
fg %n | Bring a job to the foreground |
bg %n | Resume a stopped job in the background |
kill %n | Send a signal to a job |
wait | Wait for background jobs to finish |
disown %n | Remove a job from the shell's job table |
nohup command & | Run immune to hang-ups |
setsid -f command | Run in a new session |
Best practices for job control
- Redirect background output: Send the output of background jobs to a file so that it does not clutter the terminal.
- Use job numbers:
kill %2is shorter and safer than looking up a PID. - Protect long jobs: Use
nohup,disown, orsetsidfor jobs that must survive logging out, or run them insidetmux. - Check exit status: In scripts, save
$!and usewait PIDto detect failed background tasks.
Source
In this article, we have run commands in the background, suspended and resumed jobs, referred to them by job specifications, run tasks in parallel in scripts, and kept jobs running after the terminal closes. See also the kill and ps articles.
Author
List all Linux tutorials.