Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 12 – Processes, jobs and signals

Background jobs, wait and parallel work

Start Bash jobs with the ampersand, read a PID from the shell, wait for each exit status, run a wait -n pool and xargs -P, and detach a job with nohup.

  • Intermediate
  • 10 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Start a background job with & and read its PID from $!
  • Check each background job's status with wait and name the ones that failed
  • Use wait -n to run a pool of jobs a few at a time, with a Bash 3.2 fallback
  • Choose between a wait -n pool, xargs -P and nohup for parallel and detached work

Before you start

What you’ll learn · Before you start

What you will learn

  • Start a background job with & and read its PID from $!
  • Check each background job's status with wait and name the ones that failed
  • Use wait -n to run a pool of jobs a few at a time, with a Bash 3.2 fallback
  • Choose between a wait -n pool, xargs -P and nohup for parallel and detached work

Before you start

On this page

A script that checks 30 servers one after another spends almost all of its time waiting: 29 of every 30 seconds pass while the shell blocks on a probe it has already sent. Run the probes together and the same work ends in about a second, but only if the script still learns which servers answered and which did not.

You will start a job in the background with &, read its process id from $!, wait for each one and read its exit status: The number from 0 to 255 every command ends with, kept by the shell in $?. 0 means success; Bash uses 126 for found but not executable and 127 for not found. More in “Your first script: shebang, chmod and running it”, run a pool that keeps only a few jobs going at once, hand a list to xargs -P, and start a job that outlives the terminal with nohup.

A background job runs while the shell moves on

Ending a command with & starts it as a background job: A command run with a trailing & so the shell starts it in a subshell and goes on without waiting; $! holds the PID of the most recent one, and wait collects its exit status. and lets the shell carry on with the next line. The Bash manual puts it plainly: a command ended with & runs asynchronously in a subshell: A copy of the shell that runs commands apart from it, as Bash does for each command of a pipeline. It starts with the shell's variables, and every change it makes to them is lost when it exits. More in “Reading files line by line with read”, the shell does not wait for it, and the return status is 0. So & tells you the job started, never how it ends. The special parameter $! holds the process id of the most recent background job, so read it the instant you start a job you mean to track:

fanout.sh: start two workers, keep each PID in $!, wait on each Shell · fanout.sh
#!/usr/bin/env bash
# Start two workers in the background and keep the PID of each one. $! holds the
# PID of the most recent background job, so read it right after the & that made it.
set -u
bash worker.sh alpha 0 &
first=$!
bash worker.sh beta 4 &
second=$!
echo "the shell started both workers and moved on"
# wait PID returns that job's own exit status, so each check is reported on its own
wait "$first"
echo "alpha exited $?"
wait "$second"
echo "beta exited $?"
cat report.alpha report.beta

The worker is a stand-in that writes its own report file and exits with a status. Each job writes a separate file, so two jobs never mix their lines on one stream. The session runs fanout.sh, then barewait.sh:

Two workers run at once; wait PID reports each one, a bare wait returns 0 Python · bg_session.py
from session import files, run

# Background work is started inside script files run with `bash file.sh`: a
# non-interactive shell prints no "[1] 12345" job notice, so the output has no PID
# in it and is the same on every run.
files("worker.sh", "fanout.sh", "barewait.sh")
run("""
# two workers run at the same time; wait PID reports each one's own status
bash fanout.sh
# a bare wait waits for both, then returns 0 even though delta exited 5
bash barewait.sh
""")

Output

# two workers run at the same time; wait PID reports each one's own status
$ bash fanout.sh
the shell started both workers and moved on
alpha exited 0
beta exited 4
report from alpha (rc=0)
report from beta (rc=4)
# a bare wait waits for both, then returns 0 even though delta exited 5
$ bash barewait.sh
bare wait returned 0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bg_session.py

fanout.sh caught first=$! right after the first & and second=$! after the second, so it could later wait on each by name: alpha gave 0 and beta gave 4. barewait.sh instead ended with a bare wait, with no argument. A bare wait waits for every background job and then returns 0, whatever those jobs did, so it printed bare wait returned 0 even though one worker had exited 5. That gap is the whole reason the next section exists.

Key idea

& starts a job and returns 0 at once; $! names the newest job for a later wait. A bare wait is only a barrier: it waits for everything and reports nothing about what failed.

Checkpoint

Check: Starting a job with & and reading its PID from $!

  1. Question 1 of 2 A script runs make build & and then runs rsync up &. What does $! hold on the next line?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The PID of the rsync job, the most recent background job

    Capture each PID the instant you start the job (pids+=("$!")), because the next & overwrites $!.

  2. Question 2 of 2 Two background jobs run; one will exit 5. The script ends with a bare wait and then echo $?. What prints?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 0, because a bare wait returns 0 whatever the jobs did

    A bare wait is a barrier, not a status check. When a job may fail, wait on its PID and read the status.

wait on a PID returns that job’s own status

To learn what a job did, wait on its PID. wait PID waits for that one job and returns its exit status, turning a background job back into a status you can test, just as a foreground command gives one. Naming several PIDs is a trap, though: wait "$a" "$b" waits for both but returns only the status of the last id listed, so the earlier ones are lost. The cure is to save each PID as its job starts and then wait on them one at a time, recording the failures. collect-status.sh does that for five checks:

collect-status.sh: wait on each PID in turn and list the failures Shell · collect-status.sh
#!/usr/bin/env bash
# Start every check at once, remember each job's PID in order, then wait for each
# PID and record the ones that failed. A background job's failure never stops the
# script, so the only way to know what failed is to collect each status yourself.
set -u
names=(web api db cache queue)
codes=(0 0 7 0 2)          # the status each check ends with; 0 is healthy
pids=()
for i in "${!names[@]}"; do
    bash check-one.sh "${names[$i]}" "${codes[$i]}" &
    pids+=("$!")
done
failed=()
for i in "${!pids[@]}"; do
    if ! wait "${pids[$i]}"; then
        failed+=("${names[$i]}")
    fi
done
if ((${#failed[@]})); then
    printf 'failed: %s\n' "${failed[*]}"
    exit 1
fi
echo "every check healthy"

Two checks are set to fail (db with 7, queue with 2). The loop over ${!pids[@]} waits on each saved PID and appends the name to failed when wait reports non-zero:

Five checks, one status each, and why set -e does not catch a background failure Python · collect_session.py
from session import files, run

files("collect-status.sh", "check-one.sh")
run("""
# five checks run at once; the script waits for each PID and names what failed
!bash collect-status.sh
# a failing background job does not stop a set -e script: the shell runs on
bash -c 'set -e; (exit 5) & wait; echo "still running after the failure, status $?"'
""")

Output

# five checks run at once; the script waits for each PID and names what failed
$ bash collect-status.sh
failed: db queue
[exit status 1]
# a failing background job does not stop a set -e script: the shell runs on
$ bash -c 'set -e; (exit 5) & wait; echo "still running after the failure, status $?"'
still running after the failure, status 0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 collect_session.py

The script named failed: db queue and exited 1. The second line shows why you cannot lean on errexit: Bash's set -e option (set -o errexit): the shell ends the script when a simple command returns non-zero, outside the exemption spots (if/while/until condition, every command but the last of && or ||, after !, inside a function called from those, a command substitution, and non-last pipeline stages). More in “set -e, set -u and pipefail: what they really do” here: under set -e, a background job that exits 5 does not stop the script, which ran on and printed status 0. set -e watches the foreground command in front of it, and a job’s status arrives later, through wait. The statuses line up with the common signal convention too: a job stopped with kill PID (a SIGTERM) makes a later wait report 143, that is 128 + 15, as the exit status lesson sets out.

Form Waits for Returns
wait every background job 0, always
wait "$pid" that one job that job’s exit status
wait "$a" "$b" both jobs the status of the last id only
wait -n the next job to finish that job’s status (Bash 4.3)

A bare wait that hides every failure

Wrong: starting jobs with & and ending with a bare wait, then treating status 0 as success; the failures are invisible. Right: save each PID (pids+=("$!")) and wait "$pid" on each, collecting the non-zero ones, so the script can report exactly what went wrong.

Checkpoint

Check: wait on a PID returns that job's own status

  1. Question 1 of 3 A background job runs ( exit 7 ) and its PID is saved in p. What does wait "$p" return?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: 7, the job's own exit status

    wait PID is how you turn a background job back into a status you can test, exactly as a foreground command gives one.

  2. Question 2 of 3 a is a job that exits 3 and b a job that exits 4. The script runs wait "$a" "$b". What status does wait return?

    Choose one answer.

    Show the answer: Question 2 of 3

    Answer: 4, the status of the last PID named

    Because only the last id's status survives, collect failures one PID at a time in a loop rather than naming them all in one wait.

  3. Question 3 of 3 A set -e script starts a background job that exits 5, then runs a bare wait and a later command. Does set -e stop the script at the failed job?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: No; a background job's failure never trips set -e, so the script runs on

    Treat every background job's status as something you must check by hand; set -e will not catch it for you.

A job pool runs a few at a time

Starting every job at once with & is fine for a handful, but 200 jobs at once can exhaust connections, memory or file handles. A job pool: A way to run many jobs a few at a time: start up to a set number N, then wait for one to finish before starting the next, so the parallel work never exceeds the limit N. keeps the parallel work but caps it: run at most N jobs, and start the next only when one finishes. wait -n (added in Bash 4.3, the NEWS file records) is what makes a pool tidy: it returns as soon as any one job finishes, freeing a single slot, and wait -n -p pid (Bash 5.1) stores the PID of the job that just finished, so you can name it. Step through a pool of three slots running six checks:

Interactive · step by step Running six checks three at a time Step through a wait -n pool. Watch a slot open the moment any job ends, and see each failure recorded.
Running six checks three at a time, step 8 of 8. Showing Slot 1 (free), Slot 2 (free), Slot 3 (free), Waiting (0 queued), Finished (6 done, 2 failed). In focus: Finished.Slot 1freeSlot 2freeSlot 3freeWaiting0 queuedFinished6 done, 2 failed

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Fill the three slots. The pool starts three checks at once, one per slot, and leaves app-4, app-5 and app-6 in the queue. The limit is three, so it starts no fourth job yet.
  2. wait -n opens a slot. wait -n returns as soon as any one job ends. app-2 finished healthy, so slot 2 is free again while app-1 and app-3 are still running.
  3. Start the next job. The pool starts app-4 in the slot app-2 left, so three jobs run again. Keeping every slot full is what makes the work finish sooner.
  4. A failure is recorded, not lost. wait -n hands back app-3's status, and it is non-zero. The pool adds app-3 to its failed list and frees slot 3, rather than letting the failure slip past.
  5. Keep the slots full. app-5 starts in slot 3. Only app-6 is left in the queue now, and three checks are running again.
  6. The last job starts. app-1 finishes healthy, so the pool starts app-6 in slot 1. The queue is empty; from here the pool only drains.
  7. Drain the running jobs. The pool keeps calling wait -n until no job is left. app-4 and app-6 end healthy and app-5 ends non-zero, so the failed list is app-3 and app-5.
  8. The result. At most three jobs ever ran at once, and every status was checked, so the two failures are named in the end, sorted as app-3 app-5.

A slot opened the moment app-2 finished, and the pool filled it at once rather than waiting for the whole batch; the one failure, app-3, went onto a list instead of slipping past. parallel-check.sh is that pool, and the session runs it, then shows the version gap and a portable fallback:

parallel-check.sh: at most three checks at once with wait -n -p Shell · parallel-check.sh
#!/usr/bin/env bash
# A job pool: run a list of checks with at most `limit` running at once. wait -n
# (Bash 4.3) returns as soon as any one job finishes, which frees a slot for the
# next, and -p (Bash 5.1) stores the finished job's PID so we can name its host.
set -u
limit=3
hosts=(app-1 app-2 app-3 app-4 app-5 app-6)
codes=(0 0 6 0 2 0)
declare -A host_of
failed=()
running=0
reap() {
    local pid status
    wait -n -p pid
    status=$?
    ((status == 0)) || failed+=("${host_of[$pid]}")
    ((running--))
}
for i in "${!hosts[@]}"; do
    bash check-one.sh "${hosts[$i]}" "${codes[$i]}" &
    host_of[$!]=${hosts[$i]}
    ((running++))
    ((running < limit)) || reap
done
while ((running > 0)); do
    reap
done
if ((${#failed[@]})); then
    mapfile -t failed < <(printf '%s\n' "${failed[@]}" | sort)
    printf 'failed: %s\n' "${failed[*]}"
    exit 1
fi
echo "every host ok"
The wait -n pool, the Bash 3.2 gap, and a batch pool that runs on both Python · pool_session.py
from session import files, run

files("parallel-check.sh", "pool-portable.sh", "check-one.sh")
run("""
# six host checks, three at a time; the failed hosts are listed, sorted
!bash parallel-check.sh
# wait -n needs Bash 4.3; the Bash 3.2 that macOS ships rejects the option
!/bin/bash -c '(exit 1) & wait -n'
# the portable pool runs in batches, so it works in Bash 5.3 and in Bash 3.2 alike
!bash pool-portable.sh
!/bin/bash pool-portable.sh
""")

Output

# six host checks, three at a time; the failed hosts are listed, sorted
$ bash parallel-check.sh
failed: app-3 app-5
[exit status 1]
# wait -n needs Bash 4.3; the Bash 3.2 that macOS ships rejects the option
$ /bin/bash -c '(exit 1) & wait -n'
/bin/bash: line 0: wait: -n: invalid option
wait: usage: wait [n]
[exit status 2]
# the portable pool runs in batches, so it works in Bash 5.3 and in Bash 3.2 alike
$ bash pool-portable.sh
failed: app-3 app-5
[exit status 1]
$ /bin/bash pool-portable.sh
failed: app-3 app-5
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pool_session.py

Both pools reported failed: app-3 app-5 and exited 1. But the middle line matters for anyone shipping a script: macOS still ships Bash 3.2, which has no wait -n and printed wait: -n: invalid option. pool-portable.sh avoids wait -n entirely: it runs the list in batches of three and waits for each whole batch with plain wait, so it ran the same on Bash 5.3 and on Bash 3.2. The trade-off is that one slow job in a batch holds up the next batch, which a wait -n pool avoids.

wait -n is Bash 4.3 and later

wait -n arrived in Bash 4.3 and wait -n -p in Bash 5.1 (the Bash NEWS file). A stock macOS has Bash 3.2, so a pool that must run there as well as on Linux uses a batch of plain wait, or xargs -P, instead.

Checkpoint

Check: A pool runs a few jobs at a time with wait -n

  1. Question 1 of 2 A pool keeps three jobs running. One finishes. Which call lets the pool start the next job as soon as that one slot opens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: wait -n, which returns as soon as any one job finishes

    wait -n is what turns a batch into a pool: a slot opens the moment any job ends, and -p names the job that just finished.

  2. Question 2 of 2 A wait -n pool script is copied to a Mac with its stock /bin/bash. What happens when it runs?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It fails: Bash 3.2 prints "wait: -n: invalid option" and stops

    For a script that must run on a stock macOS too, fall back to a batch pool with plain wait, or to xargs -P, neither of which needs wait -n.

xargs runs commands in parallel for you

When the jobs are “run this one command over each of a list”, xargs -P is a pool in a single line, with no PIDs to track. -P N runs up to N copies at once and -n 1 hands each copy one item. It is a separate program, on macOS and Linux alike, so the shell’s version does not matter:

xargs -P 3 runs four jobs three at a time; each writes its own file Python · xargs_session.py
from session import files, run

files("grade.sh")
run("""
# xargs -P runs up to three copies at once; -n 1 hands each copy one argument
printf '%s\\n' alpha beta gamma delta | xargs -P 3 -n 1 bash grade.sh
# each job wrote its own file, so the lines are whole and in the order we read them
cat alpha.out beta.out gamma.out delta.out
# when any command ends non-zero, xargs ends non-zero too (this BSD xargs returns 1)
!printf '%s\\n' one two bad | xargs -P 2 -n 1 bash grade.sh
cat bad.out
""")

Output

# xargs -P runs up to three copies at once; -n 1 hands each copy one argument
$ printf '%s\n' alpha beta gamma delta | xargs -P 3 -n 1 bash grade.sh
# each job wrote its own file, so the lines are whole and in the order we read them
$ cat alpha.out beta.out gamma.out delta.out
alpha: ok
beta: ok
gamma: ok
delta: ok
# when any command ends non-zero, xargs ends non-zero too (this BSD xargs returns 1)
$ printf '%s\n' one two bad | xargs -P 2 -n 1 bash grade.sh
[exit status 1]
$ cat bad.out
bad: failed

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 xargs_session.py

Each copy wrote its own .out file, so the lines stayed whole and the cat read them back in order; left to share one stream, parallel output interleaves. The last run shows the price of the convenience: when one command exited non-zero, xargs itself ended non-zero (this BSD xargs returned 1), but it gives one combined status for the whole run, not a per-job result. For which item failed, have each copy record its own result, as grade.sh did. Which tool fits depends on your case; answer three questions and let the table rule the others out:

Interactive · answer and compare Which way to run your jobs in parallel? Answer three questions about your jobs. The table rules out the ways that do not fit and says why.
Could starting every job at once overload the target or your own machine?
Must the same script run on a stock macOS (Bash 3.2) as well as on Linux?
Do you need to know which individual jobs failed, not just that one did?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five ways to run jobs at once, against concurrency, portability, status, output and extra tools.
Way to run jobs Caps concurrency Runs in Bash 3.2 Per-job status Output stays grouped No extra tool
& then wait per PID Runs them: all at once Poor starts every job at once, so 50 jobs means 50 at a time Good plain & and wait per PID work in every shell Good wait on each PID in turn gives each job its own status Mixed lines interleave unless each job writes its own file Good nothing beyond the shell itself
batch pool (plain wait) Runs them: N at a time Good runs a batch of N, waits for all of it, then the next batch Good only & and plain wait, so Bash 3.2 runs it Good wait on each PID of the batch for its status Mixed within a batch lines interleave; give each a file Good nothing beyond the shell itself
wait -n pool Runs them: N at a time Good runs at most N; a slot opens the moment any job ends Poor wait -n needs Bash 4.3; Bash 3.2 rejects the option Good wait -n -p names the job that just finished (Bash 5.1) Mixed still interleaves; write per-job files or prefix lines Good built into Bash 4.3 and later
xargs -P N Runs them: N at a time Good -P N runs at most N copies at once Good a separate program, so the shell version does not matter Poor one combined status for the lot, not per job Mixed -P mixes output; have each copy write its own file Good xargs ships on macOS and Linux (-P on both)
GNU Parallel Runs them: N at a time Good -j N runs N at once Good a separate program, any shell Good a per-job exit code, and --halt to stop early Good --keep-order holds each job output back into order Poor must be installed; not on a stock macOS
How the answers decide
  • “Could starting every job at once overload the target or your own machine?” Yes, cap how many run: rules out & then wait per PID. Starting every job at once can overload the target or the machine, so a way that caps concurrency fits better.
  • “Must the same script run on a stock macOS (Bash 3.2) as well as on Linux?” Yes, Bash 3.2 too: rules out wait -n pool and GNU Parallel. A stock macOS has neither wait -n (it needs Bash 4.3) nor GNU Parallel, so neither runs there unchanged.
  • “Do you need to know which individual jobs failed, not just that one did?” Yes, name each failure: rules out xargs -P N. xargs reports one combined status for the whole run, so it cannot tell you which job failed.

In short: a loop of & with a per-PID wait when the list is small; a wait -n pool or xargs -P when you must cap how many run at once; a batch pool when the script must also run on Bash 3.2; and GNU Parallel, named here but not run because it is not installed, when you want grouped output and per-job codes and can install it.

Checkpoint

Check: xargs -P runs commands in parallel

  1. One question printf prints four names into xargs -P 3 -n 1 some-cmd. What does -P 3 do?

    Choose one answer.

    Show the answer: One question

    Answer: Runs up to three copies of the command at once

    xargs -P N -n 1 is a pool in one line, with no PIDs to track; the price is one combined status, not a per-job result.

nohup keeps a job running after logout

A background job still belongs to its shell. When the terminal closes, the shell receives SIGHUP and passes it on to its jobs, so a long job started with a plain & can die with the connection. nohup fixes that: it starts the command set to ignore SIGHUP, and the usual form redirects all three streams so the job never touches a terminal that may vanish.

nohup-logout.sh: start a long job that survives the terminal closing Shell · nohup-logout.sh
#!/usr/bin/env bash
# Start a long job so that it survives the terminal closing. nohup makes the job
# ignore SIGHUP, and redirecting all three streams means the job never reads from
# or writes to a terminal that may vanish. The shell is then free to log out.
set -u
# > log sends standard output to a file, 2>&1 sends standard error the same way,
# and < /dev/null gives the job an input that is always at end of file.
nohup bash long-build.sh > build.log 2>&1 < /dev/null &
echo "build started; its PID is in \$! and its output is in build.log"
# disown would also remove the job from this shell's table so no SIGHUP is sent to
# it on exit; nohup already covers that, so the two are rarely both needed.

> build.log sends standard output to a file, 2>&1 sends standard error the same way, and < /dev/null gives the job an input that is always at end of file, so it never blocks waiting for a terminal. nohup(1) adds one rule worth knowing: if standard output is still a terminal, nohup appends the output to a file called nohup.out instead; with the explicit > build.log above, it does not. The other two tools people reach for do related jobs:

Tool What it does Where
nohup cmd > log 2>&1 & starts the job set to ignore SIGHUP macOS and Linux
setsid cmd runs the command in a new session, with no controlling terminal Linux (util-linux)
disown %1 removes a running job from the shell’s job table, so no SIGHUP is sent on exit Bash, any version

setsid is a Linux tool

setsid starts a command in a new session so no terminal can signal it, but it is part of util-linux and a stock macOS does not have it, so the sessions here do not run it. disown only affects an interactive shell’s own job table; the job control lesson covers disown and disown -h in full.

Checkpoint

Check: nohup keeps a job alive after logout

  1. One question Why does nohup cmd > log 2>&1 < /dev/null & let a job survive the terminal closing?

    Choose one answer.

    Show the answer: One question

    Answer: nohup makes the job ignore SIGHUP, and the redirections keep it off the terminal that may vanish

    nohup covers the signal and the redirections cover the streams; on Linux, setsid in a new session does the same job, and disown only removes a job from an interactive shell's table.

Practise: a parallel checker you can run

Write run_parallel N cmd_file, a script that reads one command per line from cmd_file and runs them with at most N going at once, exiting non-zero if any command failed. Build it from this lesson’s pieces: a wait -n -p pool that caps concurrency, a saved association from each PID to its command, and a failed list. Check it in your own shell against a small cmd_file of your own: a mix of true, false and sh -c 'exit 2' lines should make run_parallel 2 exit non-zero and name the failing lines, while a file of only true lines should exit 0. To prove the cap holds without timing anything, have each command append a line to a counter file on entry and remove it on exit, then check that the counter file never held more than N lines at once. A reference solution built this way exits 0 on the all-true file and non-zero, naming two failures, on the mixed file.

Practice that adapts to you

Practise this lesson

11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 What is the exit status of the shell right after it runs a-command &?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: 0, because & returns at once without waiting

    & is about starting work, not finishing it: the 0 says the job started, and wait is how you later learn what it did.

  2. Question 2 of 11 What does the special parameter $! hold?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: The PID of the most recently started background job

    Read $! the instant a job starts, because the next background job overwrites it.

  3. Question 3 of 11 A loop starts one background job per host. How do you keep every PID so you can wait on each later?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: Append $! to an array as each job starts: pids+=("$!")

    Each & overwrites $!, so capture it the instant the job starts, into an array when there are several.

  4. Question 4 of 11 You start five checks in the background and need to know which ones failed. Which way reliably names them?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: Save each PID as the job starts, then wait on each PID in a loop and record the ones with a non-zero status

    Each job's status is available only through wait on its own PID, so a loop over the saved PIDs is the way to collect every failure.

  5. Question 5 of 11 A background job is stopped with kill PID (a SIGTERM). What status does a later wait on that PID report?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: 143, which is 128 + 15 for SIGTERM

    kill PID sends SIGTERM by default, and wait then reports 143, so a caller reading the status knows the job was terminated.

  6. Question 6 of 11 Why run 200 checks through a pool of, say, 8 at a time instead of starting all 200 with & at once?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: 200 at once can exhaust connections, memory or file handles; a pool caps the load while still running work in parallel

    A pool is about bounding how much runs at once so parallel work does not become a self-inflicted overload.

  7. Question 7 of 11 A pool must run on both Linux (Bash 5) and a stock macOS (Bash 3.2). Which design works unchanged on both?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: A batch pool: start a batch of N, wait for the whole batch, then the next batch, using only & and plain wait

    The batch pool trades wait -n's early slot reuse for portability: a slow job holds up its batch, but only & and plain wait are needed.

  8. Question 8 of 11 A run of xargs -P 2 -n 1 over a list had one command exit 1. The shell shows xargs itself ended non-zero. What does that status tell you?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: That at least one command failed, but not which one; xargs gives a single combined status

    xargs -P is a quick pool, but its one combined status is the trade-off: when you need to know which job failed, collect per-job results yourself.

  9. Question 9 of 11 In xargs -P 4 -n 1 some-cmd, what does -n 1 do?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: Gives each run of the command exactly one argument from the input

    -P sets how many copies run at once and -n sets how many arguments each copy receives, so -P 4 -n 1 is four single-argument runs at a time.

  10. Question 10 of 11 Besides ignoring SIGHUP, which redirections make nohup cmd > log 2>&1 < /dev/null & safe to leave running after logout?

    Choose every answer that is right.

    Show the answer: Question 10 of 11

    Answer:

    • < /dev/null gives the job an input that is always at end of file
    • > log sends standard output to a file instead of the terminal
    • 2>&1 sends standard error to the same place as standard output

    nohup handles the signal; the three redirections handle the streams, so the job neither reads nor writes a terminal that may disappear.

  11. Question 11 of 11 A job is already running in the background without nohup, and the shell is about to exit. What does disown do for it?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: It removes the job from the shell's job table, so the shell does not send it SIGHUP on exit

    disown is the fix for a job you forgot to start under nohup; the job-control lesson covers it and disown -h in full.

Interview questions

Warm-up (fresher to 2–5 years): what does $! hold, and what do wait PID and a bare wait each return?

$! holds the process id of the most recent background job, the one started by the last & (or a coprocess), so you read it straight after starting a job you want to track; the next background job overwrites it. wait PID waits for that one job and returns its own exit status, which is how you turn a background job back into a status you can test: a job that ran (exit 7) makes wait return 7, and a job killed by SIGTERM makes it return 143. A bare wait, with no argument, is different: it waits for every background job to finish and then returns 0, whatever those jobs did. So a bare wait is a barrier, not a status check; when a job may fail, save its PID and wait on it by name.

The two harder questions are about real breakages: a deploy that reported success while two hosts had failed, and a CI step that hung for its whole timeout though it returned at once on a laptop. Think about where you would look first in each case, then open the worked first step below:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A deploy script starts rsync to 12 hosts, each with &, and ends with a bare wait. Two hosts failed, yet the script exited 0 and the release was marked done. The team now needs to run four at a time, on macOS laptops (Bash 3.2) as well as Linux CI. Redesign the loop.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A CI step runs ./start-server.sh, which ends with server &, and the script exits 0. On a laptop the step returns at once, but in CI it hangs until the job times out ten minutes later. Explain why, and fix start-server.sh.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What does the status of cmd & tell you?

    Only that the job started: & returns 0 at once. The job's own result comes later, from wait.

  2. What does $! hold, and when must you read it?

    The process ID of the most recent background job. Read it straight after the & that started the job you want to track, because the next background job overwrites it.

  3. What do wait PID and a bare wait return?

    wait PID waits for that job and returns its own exit status. A bare wait waits for every job and returns 0, so it reports nothing about what failed.

  4. How can a script tell which of several background jobs failed?

    Keep each job's PID and call wait on them one at a time; naming several PIDs in one wait returns only the last one's status, and a background job's failure never trips set -e.

  5. How does a script run many jobs but only a few at a time?

    With a job pool: start jobs until the cap is reached, then wait -n to reuse a slot as soon as any job ends, and wait -n -p var, from Bash 5.1, to learn which one ended. wait -n needs Bash 4.3, so a stock macOS Bash falls back to batches of plain wait.

  6. What do xargs -P N and nohup add for background work?

    xargs -P N is a one-line pool that runs up to N commands at once, with one combined status. nohup cmd > log 2>&1 & keeps a job alive after you log out by ignoring SIGHUP.

Key takeaways

  • & starts a background job and returns 0 at once, saying only that the job started; $! holds the PID of the most recent job, so read it immediately for a later wait.
  • wait PID returns that job’s own exit status, while a bare wait returns 0, and naming several PIDs returns only the last, so collect failures one PID at a time; a background job’s failure never trips set -e.
  • A job pool caps how many run at once: wait -n frees a slot as soon as any job finishes and wait -n -p names it, but wait -n needs Bash 4.3, so a script that must run on a stock macOS falls back to a batch of plain wait.
  • xargs -P N is a one-line pool with a single combined status, and nohup cmd > log 2>&1 & keeps a job alive past a logout by ignoring SIGHUP and keeping it off the terminal.

Terms in this lesson

background job
A command run with a trailing & so the shell starts it in a subshell and goes on without waiting; $! holds the PID of the most recent one, and wait collects its exit status.
job pool
A way to run many jobs a few at a time: start up to a set number N, then wait for one to finish before starting the next, so the parallel work never exceeds the limit N.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.