ZetCode

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.

status.sh
#!/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

CommandDescription
command &Run in the background
Ctrl+ZSuspend the foreground job
Ctrl+CInterrupt the foreground job
jobsList jobs (-l with PIDs)
fg %nBring a job to the foreground
bg %nResume a stopped job in the background
kill %nSend a signal to a job
waitWait for background jobs to finish
disown %nRemove a job from the shell's job table
nohup command &Run immune to hang-ups
setsid -f commandRun in a new session

Best practices for job control

Source

Bash manual: Job Control

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Linux tutorials.