Linux & Bash · Part I · Module 7 · Processes, Jobs and Signals
Parent, Child and Inheritance
Every process on the machine was started by another one, and received a specific, finite list of things from it. Knowing exactly what is on that list — and that nothing travels back up it — settles a surprising number of arguments.
Intermediate18 min readLinuxPart IFundamentalsProcesses
Part I · Module 7 · Chapter 7.4 · Parent, Child and Inheritance
Chapter 7.1 said a child gets a copy of its parent's working directory and environment, and that changes flow downward only. That was asserted and demonstrated once. This chapter makes it precise, because three separate things an engineer runs into — a script that cannot change your directory, a job still running after its shell is gone, and a Z in the ps output — are all the same rule seen from different angles.
1. The Tree
Every process has exactly one parent. Every process except PID 1, which the kernel starts at boot. Follow any chain of parents and it terminates there.
That shape — many things, each with one parent, one root — is a tree.
You can see your own branch of it with the tools from Chapter 7.2:
$ echo $$
11286
$ ps -o pid,ppid,comm -p 11286
PID PPID COMM
11286 11274 bash
$ ps -o pid,ppid,comm -p 11274
PID PPID COMM
11274 24759 /bin/zshTwo hops and the shape is visible: 24759 started 11274, which started 11286, which is you.
Seeing the tree directly
On Linux, ps can draw it:
$ ps -ef --forest | head -12
UID PID PPID C STIME TTY TIME CMD
0 1 0 0 Mar28 ? 123:17 /sbin/init
0 318 1 0 Mar28 ? 0:14 \_ /usr/lib/systemd/systemd-journald
501 639 1 0 Mar28 ? 8:02 \_ /usr/libexec/session-manager
501 11274 639 0 13:11 ttys000 0:00 \_ /bin/zsh
501 11286 11274 0 13:11 ttys000 0:00 \_ bash
501 12547 11286 0 13:22 ttys000 0:00 \_ sleep 300The indentation is the tree. pstree does the same thing more compactly if it is installed:
$ pstree -p 11286
bash(11286)───sleep(12547)Walking down instead of up
-o ppid walks up. To walk down — what did this process start? — use pgrep -P:
$ pgrep -P $$
12547
12560Two direct children of your shell. Combine it with -a on Linux to see what they are:
$ pgrep -aP $$
12547 sleep 300
12560 /bin/bash ./run_sim.shThis is the single most useful question to ask about a long-running job: what has it actually started? Chapter 7.2 ended with exactly that diagnosis.
2. What a Child Inherits
When a process starts a child, the child begins as a near-copy of the parent. "Near" is doing some work there, so here is the list.
Inherited — the child starts with a copy:
| Thing | Note |
|---|---|
| The working directory | Relative paths in the child resolve from the parent's directory |
| The environment | Every exported variable, by value |
| The user and group | Which is why permissions behave predictably |
| Open files | Including the three standard streams — Module 9 |
| The umask | Chapter 5.6's default-permission mask |
| The controlling terminal | Which is why your output appears on your screen — and Chapter 7.8's subject |
| Resource limits | Maximum open files, maximum memory, and so on |
| Signal dispositions | Which signals are ignored — Chapter 7.6 |
Not inherited — the child gets its own:
| Thing | Note |
|---|---|
| Its PID | A new number, always |
| Its parent | The parent is, by definition, the process that started it |
| Its accumulated CPU time | Starts at zero |
| Non-exported shell variables | See the warning below |
| Shell functions and aliases | They live inside one shell process, not in the environment |
3. One Way Only
Here is the rule stated as sharply as it can be:
Chapter 7.1 demonstrated this with cd. Here it is with the full picture of what the child can and cannot touch:
#!/bin/bash
echo " child PID $$, parent $PPID"
echo " started in $(pwd)"
cd /home/engineer/project/rtl
export MARKER=set-by-child
umask 077
echo " now in $(pwd), MARKER=$MARKER, umask $(umask)"$ pwd
/home/engineer/project
$ echo "MARKER=[$MARKER]"
MARKER=[]
$ umask
0022
$ ./changer.sh
child PID 12545, parent 12537
started in /home/engineer/project
now in /home/engineer/project/rtl, MARKER=set-by-child, umask 0077
$ pwd
/home/engineer/project
$ echo "MARKER=[$MARKER]"
MARKER=[]
$ umask
0022Every one of the child's three changes succeeded — it printed proof of all three. Every one of them died with the child. Nothing leaked upward.
This is not a limitation to work around. It is the property that makes a shared machine usable: a script somebody else wrote cannot silently change your working directory, your variables or your file-creation defaults, no matter what it does.
The two things that genuinely do affect your shell
Because the one-way rule is absolute, the only way to change your current shell is to not create a child at all.
Run it in the shell you are in — source. source script.sh (or its one-character alias .) reads the file and executes its lines in the current shell process, exactly as if you had typed them:
$ ./changer.sh # a child process: nothing persists
$ pwd
/home/engineer/project
$ source changer.sh # the SAME process: everything persists
child PID 11286, parent 11274
$ pwd
/home/engineer/project/rtl
$ echo $MARKER
set-by-childLook at the PID the script printed the second time: 11286 — your shell's own PID. No child was created, so there was no copy, so the changes are yours. This is why environment setup scripts are always sourced and never executed, and it is why the instructions for an EDA tool invariably say source setup.sh rather than ./setup.sh.
Use a shell function. A function defined in your shell also runs in your shell, with the same consequence. Chapter 18.1 covers functions; the process-level point is that both exceptions work the same way — by not forking.
4. When the Parent Dies First
Processes do not end in a tidy order. A parent can perfectly well exit while its children are still working, and the question of what happens then has a precise answer — one that explains a great deal of real behaviour on shared machines.
Here is the experiment. A script starts a child and then waits:
#!/bin/bash
sleep 300 &
echo "child PID $!"
sleep 300Run it in the background, note its PID, find its child, then destroy the parent outright:
$ ./parent.sh &
[1] 12298
child PID 12302
$ pgrep -P 12298
12302
12303
$ ps -o pid,ppid,comm -p 12302
PID PPID COMM
12302 12298 sleep
$ kill -9 12298
[1]+ Killed ./parent.sh
$ ps -o pid,ppid,comm -p 12302,12303
PID PPID COMM
12302 1 sleep
12303 1 sleepTwo things happened, and both matter:
- The children survived. Destroying the parent as abruptly as it is possible to destroy anything — Chapter 7.7 explains what
-9is — did not touch them at all. They were still sleeping out their 300 seconds. - Their
PPIDchanged from12298to1.
This is the single most practically important fact in the chapter, for two opposite reasons:
- It is a nuisance when you want things gone. Killing the wrapper script that launched a regression leaves the regression running, now reporting to PID 1 and no longer findable by walking down from the thing you killed. Chapter 7.7 covers killing a whole group of processes rather than one.
- It is a tool when you want things to persist. A job deliberately orphaned keeps running after the shell that started it is gone. That is most of the mechanism behind Chapter 7.8.
It also explains a ps row you will now recognise. A process showing PPID 1 that is clearly not a system service was orphaned at some point — the thing that started it is gone, and PID 1 picked it up.
5. When the Child Dies First
The other order has its own consequence, and its own piece of vocabulary.
When a process exits, it does not disappear immediately. It leaves behind an exit status — a small number saying how it went — and that status is addressed to its parent. The kernel keeps a minimal record of the dead process alive until the parent collects it.
A process in that state is a zombie, shown as Z in the stat column from Chapter 7.2. It is not running. It uses no memory and no CPU. It is an entry in a table, holding a number, waiting to be read.
Normally this lasts microseconds: the parent collects the status and the entry vanishes. Zombies pile up when a parent is written badly — it starts children and never collects their statuses — and each one occupies a PID-table slot.
$ ps -eo pid,ppid,stat,comm | awk '$3 ~ /^Z/'
12901 12880 Z defunct
12902 12880 Z defunct6. Reading a Real Situation
This all comes together the first time something is running that you did not start and cannot immediately explain.
$ ps -u $USER -o pid,ppid,etime,stat,cmd --sort=-etime | head -4
PID PPID ELAPSED STAT CMD
9981 1 02-04:11:32 S /bin/bash ./nightly_regress.sh
12811 9981 01:22:04 R python3 regress.py --seed 4
12834 9981 01:21:58 R python3 regress.py --seed 5Three rows, and the PPID column tells the whole story:
9981hasPPID 1. It is an orphan. The terminal that launched it two days ago is long gone, and PID 1 adopted it. Not a problem — just a fact about its history.12811and12834havePPID 9981. They are its children. The two-day-old script is not stuck; it is supervising.
And now a consequence worth internalising before Chapter 7.7. If you kill 9981:
$ kill 9981
$ ps -o pid,ppid,etime,cmd -p 12811,12834
PID PPID ELAPSED CMD
12811 1 01:24:30 python3 regress.py --seed 4
12834 1 01:23:58 python3 regress.py --seed 5The supervisor is gone. Both simulations are still running, now orphaned and adopted by PID 1 — and no longer supervised by anything, so nothing will collect their results, start the next seed, or clean up after them.
Killing the parent of a job tree is almost never what you actually want.
Practice
1 — Walk up to PID 1. Start from $$ and look up each PPID in turn until you reach 1. Count the hops.
p=$$; while [ "$p" -ne 1 ]; do ps -o pid,ppid,comm -p "$p" | tail -1; p=$(ps -o ppid= -p "$p" | tr -d ' '); done(The while loop is Chapter 17.4's subject — run it now and read it later.)
2 — Walk down. Start two background jobs and list your shell's children with pgrep -P $$. Then start a script that itself starts something, and find the grandchildren by running pgrep -P on the child's PID.
3 — Prove exported and non-exported differ. Set one variable with X=1 and another with export Y=2, then run bash -c 'echo "[$X] [$Y]"'. Explain the result before reading §2 again.
4 — Prove sourcing is different. Write a two-line script that does cd /tmp and echo $$. Run it with ./script.sh, note the PID and your directory. Then run it with source script.sh and note both again. The PID is the tell.
5 — Make an orphan. Reproduce §4's experiment: a script that backgrounds a sleep 300 and then sleeps itself. Kill the script and confirm with ps -o pid,ppid that the child survived and its PPID is now 1.
6 — Find the orphans already on the machine. List every process of yours whose parent is PID 1:
ps -u "$USER" -o pid,ppid,etime,cmd | awk '$2 == 1'Some will be legitimate services. Anything that looks like your own work is something you detached, deliberately or otherwise.
Check Your Understanding
1. A script contains cd /scratch and export SCRATCH=/scratch. You run ./setup.sh. Afterwards neither has taken effect. What went wrong, and what is the fix?
2. You kill the shell script that launched a regression. The simulations keep going. Is this a bug?
3. ps shows a process of yours with PPID 1, started six hours ago. What can you conclude?
4. ps shows forty processes in state Z, all with the same PPID. What is the problem and where is it?
5. Why does running source on an untrusted script deserve more caution than running it as ./script.sh?
6. You want a script to leave you in a different directory when it finishes. What are your options?
7. Remember
- Processes form a tree: one parent each, rooted at PID 1, which the kernel starts at boot.
ps -o ppidwalks up;pgrep -Pwalks down.ps --forestandpstreedraw the whole thing on Linux.- A child inherits the working directory, the environment, the user, open files, the umask, the controlling terminal, resource limits and signal dispositions.
- A child does not inherit its parent's PID, its CPU time, non-exported variables, or shell functions and aliases.
- Only exported variables cross the boundary. A plain assignment belongs to one shell process.
- Inheritance is one-way and absolute. No child changes its parent. There is no flag for it.
source(or.) runs a file in the current shell, with no child, which is the only reason environment setup scripts work — and the reason sourcing an untrusted script is riskier than running it.- Killing a parent does not kill its children. They are orphaned and re-parented to PID 1.
PPID 1on a non-service process means it was orphaned — its original parent is gone.- A zombie is a finished process whose exit status nobody collected. It uses nothing and cannot be killed; fix or end the parent, and PID 1 will clean up.
- Killing the supervisor of a job tree leaves the jobs running, unsupervised. Almost never what you want.
You now know how processes relate to each other. The next chapter is about controlling the ones your own shell started — putting a long command into the background, pulling it back, and juggling several at once.
Where this fits
Part of the Linux & Bash curriculum.
