Starting a program from a shell usually involves creating a child, arranging redirection, replacing its program image, and waiting for termination. This path exposes three boundaries in Linux process management: task identity, resource sharing, and reaping.
Question
Where to look
How can threads share memory yet execute simultaneously?
Section 1: tasks and resource objects
Why does fork share a file offset but isolate memory writes?
Sections 1–2: references and COW
How can exec replace a program without changing its PID?
Section 4: image replacement
Why does an exited child still appear as Z?
Section 5: exit and reaping
Read in order to follow the lifecycle. CPU allocation belongs to the scheduler; handing over execution belongs to context switching.
1. task_struct connects identity to resources
Each schedulable thread has its own task_struct, kernel stack, and scheduling state. Threads in a thread group can share an address space, descriptor table, and signal dispositions while retaining separate TIDs, register contexts, signal masks, and thread-directed pending signals. Sharing resources does not merge them into one scheduling entity: two threads can execute on two CPUs at once.
Establish what “PID” means in a log.getpid() returns the thread group's TGID; gettid() returns the calling thread's TID. These coincide for the thread-group leader and normally differ for other threads. PID namespaces also affect the numbers: PID 1 in a container can have a different ID on the host. Correlation requires both the task identity and the observing namespace.
task_struct does not embed every part of a process. Its pointers and references connect objects with different lifetimes:
Object or field
Responsibility
What it does not imply
mm
User address space, page tables, and VMAs
A shared mm does not mean a shared kernel stack
files
Mapping descriptor numbers to open files
Separate tables can reference the same open file description
fs
Working directory, root directory, umask
This is separate from the descriptor table
sighand / signal
Signal dispositions and thread-group signal/accounting state
The signal mask remains per-thread
Scheduling fields and entities
Policy, priority, run-queue accounting
TASK_RUNNING does not mean currently on a CPU
cred and namespace references
Credentials and resource views
Creating a task does not automatically create namespaces
Why do two descriptor tables advance one offset?
After fork, parent and child have separate descriptor tables, but their descriptors numbered 3 can refer to one struct file. Starting at offset 0:
Operation
Descriptor tables
Shared object
Parent successfully reads 100 bytes
Both descriptors remain
Offset advances to 100
Child reads next
Uses descriptor 3 in its own table
Continues from offset 100
Child closes descriptor 3
Removes only the child's reference
Parent still references the open file description
Both arrows point to the same object, explaining the shared offset.
Independent open() calls normally produce independent offsets. dup() and fork() retain references to the original open file description.
Preparing the visual
Where does the next read begin?
Inherited descriptors share one offset, advanced by either reader. Separate opens have independent offsets. Read in any order and compare both models using the same operation history.
{"id":"process-file-sharing","title":"Where does the next read begin?","summary":"Inherited descriptors share one offset, advanced by either reader. Separate opens have independent offsets. Read in any order and compare both models using the same operation history.","height":950,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<div class=\"presets\" role=\"group\" data-i18n-label=\"models\"><button data-mode=\"fork\" data-i18n=\"fork\"></button><button data-mode=\"open\" data-i18n=\"open\"></button></div>\n<div class=\"read-size\"><label for=\"read-size\" data-i18n=\"readSize\"></label><output id=\"read-size-value\" for=\"read-size\">2</output><input id=\"read-size\" type=\"range\" min=\"1\" max=\"4\" step=\"1\" value=\"2\" data-i18n-label=\"readSize\"></div>\n<div id=\"scene\" class=\"scene\">\n <div class=\"readers\"><div id=\"parent\" class=\"reader\"><span data-i18n=\"parent\"></span><small data-i18n=\"readResult\"></small><strong id=\"parent-result\">AB</strong><code>fd 3</code><button data-reader=\"parent\" data-i18n=\"read\"></button></div><div id=\"child\" class=\"reader\"><span data-i18n=\"child\"></span><small data-i18n=\"readResult\"></small><strong id=\"child-result\">CD</strong><code>fd 3</code><button data-reader=\"child\" data-i18n=\"read\"></button></div></div>\n <svg class=\"links\" viewBox=\"0 0 600 64\" preserveAspectRatio=\"none\" aria-hidden=\"true\"><path id=\"parent-link\"/><path id=\"child-link\"/></svg>\n <div class=\"open-files\"><div id=\"file-a\" class=\"open-file\"><div class=\"file-title\"><span data-i18n=\"opened\"></span><b>A</b></div><div class=\"bytes\" aria-hidden=\"true\"><span>A</span><span>B</span><span>C</span><span>D</span><span>E</span><span>F</span><span>G</span><span>H</span></div><div class=\"position\"><i id=\"cursor-a\"></i></div><div class=\"offset\"><span data-i18n=\"next\"></span><strong id=\"offset-a\">4</strong></div></div><div id=\"file-b\" class=\"open-file\"><div class=\"file-title\"><span data-i18n=\"opened\"></span><b>B</b></div><div class=\"bytes\" aria-hidden=\"true\"><span>A</span><span>B</span><span>C</span><span>D</span><span>E</span><span>F</span><span>G</span><span>H</span></div><div class=\"position\"><i id=\"cursor-b\"></i></div><div class=\"offset\"><span data-i18n=\"next\"></span><strong id=\"offset-b\">2</strong></div></div></div>\n</div>\n<p class=\"caption\" data-i18n=\"contents\"></p>\n<div class=\"copy\"><p id=\"conclusion\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"sharedConclusion\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"separateConclusion\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"beforeRead\"></p></div>\n<div class=\"demo-controls\"><button id=\"auto-play\" aria-pressed=\"false\"><span id=\"auto-label\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"auto\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"pause\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"replayAuto\"></span></button><button id=\"restart\" data-i18n=\"restart\"></button></div><p class=\"note\" data-i18n=\"note\"></p>\n","css":".intro{font-size:14px;color:var(--muted);margin:8px 0 18px}.note{font-size:12px;color:var(--muted);margin-top:16px}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.controls{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}.controls button{min-height:42px}.muted{color:var(--muted)}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.caption{font-size:12px;color:var(--muted)}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.demo-controls{display:flex;gap:10px;align-items:stretch;margin-top:18px}.demo-controls button{min-width:0;min-height:42px;font-size:13px}#auto-play{display:grid;flex:1}#auto-play>span{grid-area:1/1}#auto-play[aria-pressed=\"true\"]{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}#restart{color:var(--muted)}\n.scene{margin-top:22px}.readers{display:grid;grid-template-columns:1fr 1fr;gap:18px}.reader{border:1px solid var(--rule);border-radius:10px;padding:14px;text-align:center;transition:background .3s,border-color .3s}.reader span,.reader small,.reader code{display:block}.reader span{font-weight:600;font-size:14px}.reader small{color:var(--muted);font-size:12px;min-height:2.8em}.reader strong{display:block;font:600 26px var(--viz-font-ui);margin:6px 0}.reader code{font-size:12px;color:var(--muted)}.reader.read{background:var(--accent-soft);border-color:var(--accent)}.links{display:block;width:100%;height:64px;overflow:visible}.links path{fill:none;stroke:var(--accent);stroke-width:2;vector-effect:non-scaling-stroke;transition:d .3s}.open-files{display:grid;grid-template-columns:1fr 1fr;gap:18px;position:relative}.open-file{padding:16px;border:1px solid var(--accent);background:var(--accent-soft);border-radius:10px;min-width:0;transition:opacity .3s,background .3s}.scene.shared #file-a{grid-column:1/3;width:72%;justify-self:center}.scene.shared #file-b{position:absolute;inset:0 0 0 auto;width:calc(50% - 9px);visibility:hidden;opacity:0;pointer-events:none}.file-title{display:flex;justify-content:space-between;align-items:start;gap:6px;font-size:12px;min-height:2.8em;color:var(--muted)}.file-title b{color:var(--accent);font-size:16px}.bytes{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin-top:10px}.bytes span{font:16px var(--viz-font-mono);text-align:center;padding:10px 0;background:var(--paper);border:1px solid var(--rule);transition:background .3s}.bytes span+span{border-left:0}.bytes .latest{background:var(--second-soft);color:var(--ink)}.position{position:relative;height:12px;border-bottom:1px solid var(--accent);margin-bottom:10px}.position i{position:absolute;bottom:-4px;width:8px;height:8px;border-radius:50%;background:var(--accent);transform:translateX(-50%);transition:left .4s}.offset{font-size:12px;display:flex;justify-content:space-between;align-items:center;gap:6px;min-height:2.8em}.offset strong{font:600 20px var(--viz-font-ui)}.caption{margin-top:12px}.controls button{width:100%}\n@media(max-width:480px){.readers,.open-files{gap:10px}.reader{padding:12px 8px}.open-file{padding:12px 8px}.scene.shared #file-a{width:80%}.file-title,.offset{min-height:4.2em}.scene.shared #file-b{width:calc(50% - 5px)}}\n\n.reader button{display:block;width:100%;margin-top:12px;font-size:13px;min-height:42px}.bytes{grid-template-columns:repeat(8,minmax(0,1fr))}.bytes span{font-size:13px}.reader strong{min-height:1.4em}@media(max-width:480px){.bytes span{font-size:10px}.reader button{font-size:12px;padding-inline:5px}}\n\n.read-size{display:grid;grid-template-columns:1fr auto;gap:6px 12px;margin-top:18px;font-size:13px;align-items:center}.read-size input{grid-column:1/3;width:100%;margin:0}.read-size output{color:var(--accent);font-variant-numeric:tabular-nums}\n","js":"const $ = s => document.querySelector(s);\nconst contents = 'ABCDEFGH';\nlet mode = 'fork', history = [], readSize = 2;\nfunction calculate() {\n const offsets = [0, 0], results = {parent: '—', child: '—'}, ranges = [[0,0],[0,0]];\n for (const {who, size} of history) {\n const i = mode === 'fork' || who === 'parent' ? 0 : 1;\n const start = offsets[i], chunk = contents.slice(start, start + size);\n offsets[i] += chunk.length;\n results[who] = chunk || 'EOF';\n ranges[i] = [start, offsets[i]];\n }\n return {offsets, results, ranges};\n}\nfunction finished() {\n const {offsets} = calculate();\n return offsets[0] === contents.length && (mode === 'fork' || offsets[1] === contents.length);\n}\nfunction playbackLabel(running) {\n $('#auto-play').setAttribute('aria-pressed', String(running));\n $('#auto-label').textContent = viz.t(running ? 'pause' : finished() ? 'replayAuto' : 'auto');\n}\nfunction draw() {\n const shared = mode === 'fork', {offsets, results, ranges} = calculate();\n $('#scene').classList.toggle('shared', shared);\n document.querySelectorAll('[data-mode]').forEach(e => e.setAttribute('aria-pressed', String(e.dataset.mode === mode)));\n $('#parent-link').setAttribute('d', shared ? 'M150 0 C150 32 300 32 300 64' : 'M150 0 L150 64');\n $('#child-link').setAttribute('d', shared ? 'M450 0 C450 32 300 32 300 64' : 'M450 0 L450 64');\n for (const who of ['parent', 'child']) {\n $('#'+who+'-result').textContent = results[who];\n $('#'+who).classList.toggle('read', results[who] !== '—');\n }\n for (const [i, file] of ['a','b'].entries()) {\n $('#offset-'+file).textContent = offsets[i];\n $('#cursor-'+file).style.left = offsets[i] / contents.length * 100 + '%';\n document.querySelectorAll('#file-'+file+' .bytes span').forEach((e,j) => e.classList.toggle('latest', j >= ranges[i][0] && j < ranges[i][1]));\n }\n $('#conclusion').textContent = viz.t(!history.length ? 'beforeRead' : shared ? 'sharedConclusion' : 'separateConclusion');\n document.querySelectorAll('[data-reader]').forEach(e => e.textContent = viz.t('read').replace('{n}', readSize));\n $('#read-size-value').textContent = readSize;\n playbackLabel(player.playing);\n}\nconst player = viz.playback({\n delay: 1800,\n canAdvance: () => !finished(),\n advance() {\n let who = history.at(-1)?.who === 'parent' ? 'child' : 'parent';\n const {offsets} = calculate();\n if (mode === 'open' && offsets[who === 'parent' ? 0 : 1] === contents.length) who = who === 'parent' ? 'child' : 'parent';\n history.push({who, size: readSize}); draw();\n },\n onChange: playbackLabel,\n});\ndocument.querySelectorAll('[data-reader]').forEach(e => e.addEventListener('click', () => { player.pause(); history.push({who:e.dataset.reader, size:readSize}); draw(); }));\ndocument.querySelectorAll('[data-mode]').forEach(e => e.addEventListener('click', () => { player.pause(); mode = e.dataset.mode; draw(); }));\n$('#read-size').addEventListener('input', e => { player.pause(); readSize = Number(e.target.value); draw(); });\n$('#auto-play').addEventListener('click', () => {\n if (!player.playing && finished()) { history = []; draw(); }\n player.toggle();\n});\n$('#restart').addEventListener('click', () => { player.pause(); history = []; draw(); });\ndraw();\n","audio":false,"strings":{"auto":"Auto demo","beforeRead":"All offsets start at 0. Choose either process’s read button and follow the result and referenced object.","child":"Child","contents":"Each window shows the same file ABCDEFGH. Highlighting marks its latest read; the dot marks its next read position.","eyebrow":"01 · File references","fork":"Inherited by fork","heading":"Where does the next read begin?","intro":"Choose a read length and ask either process to read, in any order. Switching how the file is opened recomputes the same operation history.","models":"How the file is opened","next":"Next read position","note":"Reads execute successfully in click order, with no other users. A read near the end may return fewer bytes; at the end it returns EOF. The auto demo alternates readers; manual actions pause it.","open":"Separate open calls","opened":"Open file description","parent":"Parent","pause":"Pause demo","read":"Read {n} bytes","readResult":"Latest read","readSize":"Bytes requested per read","replace":"Replace image","replayAuto":"Replay demo","restart":"Reset","separateConclusion":"Each open manages its own offset. A read advances only the object used by that process; the other position stays unchanged.","sharedConclusion":"Both references lead to one offset. Either process advances it when reading, so the other process continues from that position on its next read.","write":"Write to pipe","writeCount":"Messages received"}}
Reading kernel code:current identifies the current task through an architecture-dependent implementation. Do not depend on a kernel-stack rounding formula or hard-coded task_struct offsets.
2. Creation copies relationships; data need not be copied immediately
fork() creates a child with an independent execution flow. It returns the child's PID to the parent and 0 to the child; neither is guaranteed to run first. Creation establishes identity, resource references, scheduling entities, and an initial execution context. Copy-on-write usually reduces the immediate cost of copying private user data pages, but page-table and other metadata work still costs time. Fork does more than copy one task_struct.
Private pages: share initially, isolate writes
Copy-on-write combines delayed copying with isolation. Suppose both processes initially map one physical page. A parent's first write to its private mapping triggers a write-protection fault, and the kernel establishes a mapping that satisfies private-write semantics. The child still sees the original contents. Sharing a physical page is an implementation technique; isolated writes are the promise. Analyze MAP_SHARED mappings separately. Page-table and fault details continue in virtual memory.
clone flags choose resource relationships
clone() and clone3() express sharing through flags with dependencies: CLONE_THREAD requires CLONE_SIGHAND, which requires CLONE_VM.
Creation pattern
Address space and file table
Still separate for each task
Typical POSIX threads
Shared mm, file table, and signal dispositions
Stack, thread identity, execution context
Ordinary fork
Separate mm and table, with shared underlying objects
Child identity, stack, execution context
Namespaces are normally inherited. New namespaces require the appropriate flags, permissions, and other conditions.
Distinguish the glibc clone() wrapper, which accepts a function and stack, from the raw system call, whose ABI varies by architecture. Names such as kernel_clone() and copy_process() help locate implementations; they are not application interfaces. clone(2)
Multithreaded fork: a lock without its owner
Suppose thread B holds a user-space lock when thread A forks. The child contains only A but inherits memory in which the lock may still be held. If A tries to acquire it, it may wait forever for B, which does not exist in the child.
Between fork and exec in a multithreaded program, the child must observe restrictions such as calling only async-signal-safe functions. posix_spawn() often makes preparation easier to control. fork(2)
vfork is more restrictive: the child temporarily shares the address space and suspends the calling thread, but other parent threads may continue. Arbitrary ordinary C logic in the child is unsafe here. vfork(2)
3. Running and waiting describe scheduling conditions
TASK_RUNNING includes tasks executing now and tasks ready to run. Ten such tasks on one CPU normally still mean only one executes at a time. Tasks waiting for I/O, locks, or events may sleep. A wakeup restores eligibility to run; the scheduler decides when execution actually begins.
Observation
Meaning
Interpretation limit
R
Running or runnable
Does not establish CPU saturation or queueing time
S
Interruptible sleep
Whether a signal produces EINTR also depends on the interface and restart rules
D
Uninterruptible-class wait
Not limited to disk I/O; inspect the kernel wait point
T / t
Job-control stop / tracing stop
Different from ordinary sleep or exit
Z
Exited, awaiting reaping
No longer executes, but retains wait-related information
Kernel task states involve bits and protocols; fields such as exit_state separately track exit. The visible letters are not a simple enum to assign directly. TASK_KILLABLE permits fatal signals to interrupt certain waits; it does not mean all D-state tasks became killable in some newer release.
Waiting must close the gap between checking a condition and registering to sleep. A READ_ONCE() followed by schedule() is insufficient. Use interfaces such as wait_event*() and publish the condition's data with the required locking or memory ordering. See the lost-wakeup explanation in context switching.
4. exec preserves identity and replaces the image
A successful execve() neither creates another process nor returns to the old call site. It installs the new address space, user stack, and entry state under the existing process identity. A dynamically linked ELF usually enters its interpreter/dynamic linker first, then runtime initialization reaches main(); the kernel does not directly call main.
Object
After successful exec
Consequence
Private address space
Old heap, stacks, and mappings are replaced
Old pointers do not refer to the new program's data
Other threads
Other threads in the old group terminate
A nonleader caller's TID is also adjusted
File descriptors
Descriptors without FD_CLOEXEC are normally retained
Old function addresses cannot serve as new-image callbacks
Credentials
Changes depend on file properties and execution restrictions
Consider no_new_privs, mount attributes, and tracing
Shell redirection uses descriptor inheritance. The child duplicates a pipe end onto standard output before exec. The new program can write to that same open file object without knowing how the parent created the pipe.
Preparing the visual
A new program. The same process?
exec replaces user code, data, and stack under the same process identity. Standard output without CLOEXEC retains its pipe connection for the new program.
{"id":"process-exec","title":"A new program. The same process?","summary":"exec replaces user code, data, and stack under the same process identity. Standard output without CLOEXEC retains its pipe connection for the new program.","height":950,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<div id=\"process\" class=\"process\"><header><strong data-i18n=\"child\"></strong><span class=\"mono\">PID 4201</span></header>\n <div class=\"image-title\"><span data-i18n=\"space\"></span><span id=\"program-name\"></span></div>\n <div class=\"image-stack\"><section id=\"old-image\" class=\"program-image\"><div class=\"code\"><span class=\"caption\" data-i18n=\"code\"></span><button id=\"replace\" class=\"code-action\"><code>execve(\"./sender\", …);</code><span data-i18n=\"replace\"></span></button><code id=\"unreachable\">puts(\"old\");</code></div><div class=\"memory\"><div><span data-i18n=\"data\"></span><code>buffer = \"old\"</code></div><div><span data-i18n=\"stack\"></span><code>launch()</code><code>count = 3</code></div></div></section><section id=\"new-image\" class=\"program-image\" aria-hidden=\"true\"><div class=\"code\"><span class=\"caption\" data-i18n=\"code\"></span><button id=\"write\" class=\"code-action\"><code>write(1, \"ready\\n\", 6);</code><span data-i18n=\"write\"></span></button><code>/* message loop */</code></div><div class=\"memory\"><div><span data-i18n=\"data\"></span><code>message = \"ready\\n\"</code></div><div><span data-i18n=\"stack\"></span><code>main(argc, argv)</code><code>argv[0] = \"sender\"</code></div></div></section></div>\n <div class=\"fd\"><code>fd 1</code><span data-i18n=\"stdout\"></span></div>\n</div>\n<div class=\"pipe-path\"><i id=\"packet\"></i><span>↓</span><span data-i18n=\"pipe\"></span><span>↓</span></div>\n<div class=\"receiver\"><span data-i18n=\"parent\"></span><div><strong id=\"received\">—</strong><small id=\"writes\"></small></div></div>\n<div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"beforeNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"afterNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"replacedNote\"></p></div>\n<div class=\"demo-controls\"><button id=\"auto-play\" aria-pressed=\"false\"><span id=\"auto-label\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"auto\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"pause\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"replayAuto\"></span></button><button id=\"restart\" data-i18n=\"restart\"></button></div>\n<p class=\"note\" data-i18n=\"note\"></p>\n","css":".intro{font-size:14px;color:var(--muted);margin:8px 0 18px}.note{font-size:12px;color:var(--muted);margin-top:16px}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.controls{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}.controls button{min-height:42px}.muted{color:var(--muted)}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.caption{font-size:12px;color:var(--muted)}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.demo-controls{display:flex;gap:10px;align-items:stretch;margin-top:18px}.demo-controls button{min-width:0;min-height:42px;font-size:13px}#auto-play{display:grid;flex:1}#auto-play>span{grid-area:1/1}#auto-play[aria-pressed=\"true\"]{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}#restart{color:var(--muted)}\n.process{border:2px solid var(--accent);border-radius:12px;overflow:hidden}.process header{display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;padding:14px;background:var(--accent-soft);font-size:14px}.process header span{color:var(--accent)}.image-title{display:flex;justify-content:space-between;gap:10px;padding:16px 16px 8px;color:var(--muted);font-size:12px}.image-title span{flex:1}#program-name{text-align:right}.image-stack{display:grid;margin:0 14px;border:1px solid var(--rule);border-radius:8px;background:var(--surface);overflow:hidden}.program-image{grid-area:1/1;min-width:0;padding:14px;transition:opacity .4s,transform .4s;transform:translateY(0)}#new-image{opacity:0;visibility:hidden;transform:translateY(8px);background:var(--second-soft)}.process.replaced #old-image{opacity:0;visibility:hidden;transform:translateY(-8px)}.process.replaced #new-image{opacity:1;visibility:visible;transform:translateY(0)}.code{min-width:0}.code code{display:block;white-space:normal;overflow-wrap:anywhere;font-size:14px;line-height:1.7}#unreachable{color:var(--muted)}.memory{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px}.memory>div{min-width:0;border-top:1px solid var(--rule);padding-top:10px}.memory span,.memory code{display:block;font-size:12px;overflow-wrap:anywhere}.memory span{color:var(--muted);margin-bottom:4px}.fd{display:flex;align-items:center;gap:12px;margin:14px;padding:10px 12px;background:var(--accent-soft);border:1px solid var(--accent);border-radius:6px;font-size:12px}.fd code{white-space:nowrap;color:var(--accent);font-size:15px}.pipe-path{position:relative;display:flex;flex-direction:column;align-items:center;font-size:12px;color:var(--accent);line-height:1.5;padding:2px 0}.pipe-path span:first-of-type,.pipe-path span:last-of-type{font-size:20px}.pipe-path i{position:absolute;left:calc(50% + 35px);top:0;width:7px;height:7px;border-radius:50%;background:var(--second);opacity:0}.pipe-path.flow i{animation:packet .6s ease-in-out}.receiver{display:flex;align-items:center;justify-content:space-between;gap:12px;border:1px solid var(--accent);background:var(--accent-soft);border-radius:8px;padding:14px;font-size:13px}.receiver strong{font:600 20px var(--viz-font-ui)}.controls button{display:grid;width:100%}.controls button>span{grid-area:1/1}.image-title{min-height:4.2em}\n@keyframes packet{0%{opacity:0;transform:translateY(0)}20%{opacity:1}85%{opacity:1}100%{opacity:0;transform:translateY(65px)}}\n@media(max-width:480px){.program-image{padding:12px}.image-stack{margin:0 10px}.memory{grid-template-columns:1fr}.memory>div{min-height:5.4em}.process header{padding:12px}.code code{font-size:12px}.fd{margin:12px 10px}}\n\n.code-action{width:100%;display:flex;flex-wrap:wrap;justify-content:space-between;text-align:left;gap:6px 12px;padding:10px;margin:6px 0;background:var(--paper);border-color:var(--accent)}.code-action code{font-size:13px}.code-action span{font-size:12px;color:var(--accent)}.receiver>div{text-align:right}.receiver small{display:block;font-size:12px;color:var(--muted);min-height:1.7em}\n","js":"const $ = s => document.querySelector(s);\nlet replaced = false, writes = 0;\nfunction playbackLabel(running) {\n $('#auto-play').setAttribute('aria-pressed', String(running));\n $('#auto-label').textContent = viz.t(running ? 'pause' : writes ? 'replayAuto' : 'auto');\n}\nfunction draw() {\n $('#process').classList.toggle('replaced', replaced);\n $('#old-image').setAttribute('aria-hidden', String(replaced));\n $('#new-image').setAttribute('aria-hidden', String(!replaced));\n $('#program-name').textContent = viz.t(replaced ? 'newProgram' : 'oldProgram');\n $('#received').textContent = writes ? 'ready' : '—';\n $('#writes').textContent = viz.t('writeCount') + ' ' + writes;\n $('#explanation').textContent = viz.t(!replaced ? 'beforeNote' : !writes ? 'replacedNote' : 'afterNote');\n playbackLabel(player.playing);\n}\nfunction replace() { replaced = true; draw(); }\nfunction write() {\n if (!replaced) return;\n writes++; draw();\n const pipe = document.querySelector('.pipe-path');\n pipe.classList.remove('flow'); void pipe.offsetWidth; pipe.classList.add('flow');\n}\nconst player = viz.playback({delay: 1800, canAdvance: () => writes === 0,\n advance() { if (!replaced) replace(); else write(); }, onChange: playbackLabel});\n$('#replace').addEventListener('click', () => { player.pause(); replace(); });\n$('#write').addEventListener('click', () => { player.pause(); write(); });\n$('#auto-play').addEventListener('click', () => {\n if (!player.playing && writes) { replaced = false; writes = 0; document.querySelector('.pipe-path').classList.remove('flow'); draw(); }\n player.toggle();\n});\n$('#restart').addEventListener('click', () => { player.pause(); replaced = false; writes = 0; document.querySelector('.pipe-path').classList.remove('flow'); draw(); });\ndraw();\n","audio":false,"strings":{"afterNote":"PID 4201 and the connection from fd 1 to the pipe stay in place. Using its new code and stack, sender writes ready through the inherited standard output.","auto":"Auto demo","beforeNote":"The outer frame is the process identity; inside is the current program. Successful exec replaces its code, variables, and stack. The following puts(\"old\") never executes.","child":"Child process","code":"Executing code","data":"Data / heap","eyebrow":"02 · Image replacement","heading":"A new program. The same process?","intro":"The child’s standard output is connected to a pipe. Use the actions beside the code to replace the program and trigger each output yourself.","newProgram":"New program: sender","note":"A successful exec in a single-threaded process with CLOEXEC unset on fd 1. Code is excerpted; linker and runtime initialization are omitted. The new program remains in a message loop and can write ready repeatedly.","oldProgram":"Launcher program","parent":"Parent receives","pause":"Pause demo","pipe":"Existing pipe P","read":"Read 2 bytes","readResult":"Latest read","replace":"Replace image","replacedNote":"The image has been replaced, while the PID and output connection remain. Nothing has been written yet. Use Write to pipe in the new program to follow the existing connection.","replayAuto":"Replay demo","restart":"Reset","space":"User address space","stack":"User stack","stdout":"Standard output: connection retained","write":"Write to pipe","writeCount":"Messages received"}}
Applications normally treat a return from exec as failure and inspect errno. Rare failures beyond the point of no return can instead terminate the process. execve(2)
5. exit and wait: why does a zombie remain?
The parent may be busy when its child exits. Discarding the result immediately would prevent the parent from learning whether the child succeeded, returned an error, or died from a signal. Exit therefore releases references to resources such as address spaces and files while retaining termination information and associated metadata for an eligible waiter.
A zombie no longer runs user code but still uses some kernel memory. Exit also does not destroy every formerly referenced object: the parent may hold the same open file description, and other processes may map shared memory. For many objects, destruction depends on releasing the last reference.
User-space cleanup versus kernel exit
exit() invokes exit handlers and flushes standard I/O; _exit() skips that user-space cleanup. On Linux, glibc's _exit() wrapper normally uses exit_group to terminate the thread group, whereas raw SYS_exit terminates only its caller. Distinguish the libc interface from the raw system call when analyzing why other threads also disappeared.
Observation versus consumption
waitpid() or waitid() can consume an exit result. waitid(..., WNOWAIT) observes it while preserving a later wait. The interaction below separates those operations; its calls include WNOHANG so trying them before exit does not block.
Preparing the visual
Does reading the exit code reap the child?
Exit leaves a retained result. WNOWAIT obtains the exit code without consuming that result. Reaping consumes it, while the parent keeps the value it obtained.
{"id":"process-exit-wait","title":"Does reading the exit code reap the child?","summary":"Exit leaves a retained result. WNOWAIT obtains the exit code without consuming that result. Reaping consumes it, while the parent keeps the value it obtained.","height":950,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<div class=\"world\"><section id=\"child\" class=\"actor\"><header><strong data-i18n=\"child\"></strong><code>PID 4201</code></header><div class=\"work\" id=\"work\"><span class=\"pulse\"></span><span id=\"work-label\"></span></div><div class=\"record\" id=\"record\"><span data-i18n=\"record\"></span><strong id=\"record-value\">—</strong><small id=\"record-label\"></small></div><button id=\"exit\" data-i18n=\"exit\"></button></section>\n<div class=\"transfer\" aria-hidden=\"true\"><span>→</span><i id=\"transit\">7</i></div>\n<section class=\"actor parent\"><header><strong data-i18n=\"parent\"></strong><span data-i18n=\"waiting\"></span></header><div class=\"parent-result\"><span data-i18n=\"received\"></span><strong id=\"parent-value\">—</strong><small id=\"parent-label\"></small></div><div class=\"parent-actions\"><button id=\"observe\" data-i18n=\"observe\"></button><button id=\"reap\" data-i18n=\"reap\"></button></div></section></div>\n<div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"liveNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"zombieNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"observedNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"reapedNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"earlyNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"againNote\"></p></div>\n<details><summary data-i18n=\"calls\"></summary><div class=\"call\"><code id=\"call\"></code><p id=\"return-value\"></p></div><p class=\"note\" data-i18n=\"nonblocking\"></p></details>\n<div class=\"controls\"><button id=\"reset\" data-i18n=\"reset\"></button></div><p class=\"note\" data-i18n=\"note\"></p>\n","css":".intro{font-size:14px;color:var(--muted);margin:8px 0 18px}.note{font-size:12px;color:var(--muted);margin-top:16px}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.controls{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}.controls button{min-height:42px}.muted{color:var(--muted)}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.caption{font-size:12px;color:var(--muted)}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.world{display:grid;grid-template-columns:1fr 30px 1fr;align-items:stretch}.actor{min-width:0;padding:16px;border:1px solid var(--rule);border-radius:10px;display:flex;flex-direction:column;gap:14px}.actor header strong,.actor header code,.actor header span{display:block}.actor header strong{font-size:14px}.actor header code,.actor header span{font-size:12px;color:var(--muted)}.work{display:flex;align-items:center;gap:8px;font-size:12px;min-height:3em;color:var(--accent)}.pulse{width:8px;height:8px;border-radius:50%;background:var(--accent);flex-shrink:0}.record{border:1px dashed var(--rule);border-radius:8px;padding:12px;transition:background .3s,opacity .3s;min-width:0}.record>span,.parent-result>span{display:block;font-size:12px;color:var(--muted);min-height:2.8em}.record strong,.parent-result strong{display:block;font:600 30px var(--viz-font-ui);margin:10px 0}.record small,.parent-result small{display:block;font-size:12px;color:var(--muted);min-height:4.2em;overflow-wrap:anywhere}.actor button{font-size:13px;min-width:0;min-height:44px}.actor>button{margin-top:auto}.parent-actions{display:grid;gap:8px;margin-top:auto}.parent-result{padding:12px;border:1px solid var(--rule);border-radius:8px;background:var(--surface);min-width:0}.transfer{position:relative;display:flex;align-items:center;justify-content:center;color:var(--muted)}.transfer>span{font-size:20px}.transfer i{position:absolute;top:45%;font-style:normal;color:var(--second);font-weight:700;opacity:0}.transfer.moving i{animation:result-copy .45s ease-in-out}#child.zombie .pulse,#child.reaped .pulse{background:var(--muted)}#child.zombie .work,#child.reaped .work{color:var(--muted)}#child.zombie .record{border-style:solid;border-color:var(--second);background:var(--second-soft)}#child.reaped .record{opacity:.45}#child.reaped header code{text-decoration:line-through}.parent.has-result .parent-result{border-color:var(--accent);background:var(--accent-soft)}details{margin-top:16px;font-size:12px;color:var(--muted)}summary{cursor:pointer;padding:10px 0}.call code{white-space:normal;overflow-wrap:anywhere}.call p{font-family:var(--viz-font-mono);overflow-wrap:anywhere}.controls button{color:var(--muted)}\n@keyframes result-copy{0%{opacity:0;transform:translateX(-10px)}20%{opacity:1}80%{opacity:1}100%{opacity:0;transform:translateX(10px)}}\n@media(max-width:480px){.world{grid-template-columns:1fr 20px 1fr}.actor{padding:10px 8px;gap:10px}.record,.parent-result{padding:10px 8px}.record>span,.parent-result>span{min-height:4.2em}.record small,.parent-result small{min-height:7em}.actor header{min-height:4.5em}.work{min-height:4.2em}.actor button{padding-inline:7px;font-size:12px;min-height:5.2em}}\n","js":"const $ = s => document.querySelector(s);\nlet state = 'live', parentHasResult = false, observed = false, message = 'liveNote', call = '', result = '—';\nfunction draw() {\n $('#child').className = 'actor ' + state;\n $('#work-label').textContent = viz.t(state === 'live' ? 'running' : state === 'zombie' ? 'stopped' : 'gone');\n $('#record-value').textContent = state === 'zombie' ? '7' : '—';\n $('#record-label').textContent = viz.t(state === 'live' ? 'pending' : state === 'reaped' ? 'consumed' : observed ? 'stillHere' : 'waitable');\n $('#parent-value').textContent = parentHasResult ? '7' : '—';\n $('#parent-label').textContent = viz.t(parentHasResult ? 'saved' : 'notReceived');\n document.querySelector('.parent').classList.toggle('has-result', parentHasResult);\n $('#exit').disabled = state !== 'live';\n $('#explanation').textContent = viz.t(message);\n $('#call').textContent = call || viz.t('noCall');\n $('#return-value').textContent = result;\n}\nfunction transfer() {\n const el = document.querySelector('.transfer');\n el.classList.remove('moving'); void el.offsetWidth; el.classList.add('moving');\n}\n$('#exit').addEventListener('click', () => { state = 'zombie'; message = 'zombieNote'; call = '_exit(7)'; result = viz.t('noReturn'); draw(); });\nfor (const kind of ['observe', 'reap']) $('#'+kind).addEventListener('click', () => {\n call = kind === 'observe' ? 'waitid(P_PID, 4201, …, WEXITED | WNOWAIT | WNOHANG)' : 'waitpid(4201, &status, WNOHANG)';\n if (state === 'live') { message = 'earlyNote'; result = kind === 'observe' ? '0; si_pid = 0' : '0'; }\n else if (state === 'reaped') { message = 'againNote'; result = '−1; errno = ECHILD'; }\n else {\n parentHasResult = true;\n if (kind === 'observe') { observed = true; message = 'observedNote'; result = '0; si_pid = 4201; si_status = 7'; }\n else { state = 'reaped'; message = 'reapedNote'; result = '4201; WEXITSTATUS(status) = 7'; }\n transfer();\n }\n draw();\n});\n$('#reset').addEventListener('click', () => { state = 'live'; parentHasResult = false; observed = false; message = 'liveNote'; call = ''; result = '—'; draw(); });\ndraw();\n","audio":false,"strings":{"againNote":"The child has already been reaped, so waiting again yields ECHILD. The exit code previously obtained by the parent is still 7.","calls":"System call and return value","child":"Child","consumed":"Consumed; no longer waitable","earlyNote":"No exit result exists yet. This nonblocking wait returns immediately, and the child continues executing.","exit":"Exit with code 7","eyebrow":"03 · Exit and reaping","gone":"Reaped","heading":"Does reading the exit code reap the child?","intro":"Keep the child’s retained exit result and the parent’s received value visible together. Exit the child, then compare observation with reaping.","liveNote":"The child is still executing and has no exit result. A wait attempt by the parent does not terminate the child.","noCall":"No call yet","noReturn":"Does not return; child stops executing","nonblocking":"All waits use WNOHANG so they can be tried before exit. WNOWAIT controls consumption; WNOHANG controls blocking.","notReceived":"No result obtained","note":"An ordinary waitable, single-threaded child with default SIGCHLD disposition. The left box represents wait-related information, not a separate kernel structure. Automatic reaping, ptrace, and deferred frees are omitted.","observe":"Observe · WNOWAIT","observedNote":"Both sides show 7: the parent has read the result, but WNOWAIT leaves it unconsumed. The left side retains it and the child remains a zombie.","parent":"Parent","pending":"Not produced yet","reap":"Reap · waitpid","reapedNote":"The parent keeps the value 7 it obtained. The waitable result on the left has been consumed and the child has been reaped.","received":"Exit code obtained by parent","record":"Exit result retained by kernel","reset":"Start over","running":"Program executing","saved":"Value obtained by parent","stillHere":"Retained; still waitable","stopped":"Z · Execution stopped","waitable":"Retained for a waiter","waiting":"The waiting process","zombieNote":"The child has stopped executing. The kernel retains exit code 7, but the parent has not obtained it. This is the Z state shown here."}}
Explicit SIGCHLD = SIG_IGN or SA_NOCLDWAIT changes automatic reaping behavior. If the parent exits first, the applicable subreaper or PID namespace init adopts the orphan.
SIGCHLD's default action of ignoring the signal is not equivalent to explicitly selecting SIG_IGN for reaping behavior. waitpid(2)
Runnable example: follow the lifecycle through a pipe
This single-threaded Linux example combines fork, redirection, exec, observation, and reaping. Save it as lifecycle.c and compile with cc -std=c11 -O2 -Wall -Wextra -Werror lifecycle.c -o lifecycle. The reported PID comes from the new image. Unlike the interaction above, the first two waits here block until an exit result is available: first observe exit code 7, then consume it, then verify that the child is no longer waitable.
#define_GNU_SOURCE#include<assert.h>#include<errno.h>#include<signal.h>#include<stdio.h>#include<stdlib.h>#include<sys/wait.h>#include<unistd.h>#include<fcntl.h>intmain(intargc,char**argv){(void)argv;if(argc ==2){printf("%ld\n",(long)getpid());return7;}struct sigaction sa ={.sa_handler= SIG_DFL };sigemptyset(&sa.sa_mask);assert(sigaction(SIGCHLD,&sa,NULL)==0);int p[2];assert(pipe2(p, O_CLOEXEC)==0);/* Standard input, output, and error must already be open. */assert(p[0]> STDERR_FILENO && p[1]> STDERR_FILENO);pid_t child =fork();assert(child >=0);if(child ==0){close(p[0]);if(dup2(p[1], STDOUT_FILENO)<0)_exit(126);close(p[1]);execl("/proc/self/exe","lifecycle","child",(char*)NULL);_exit(127);}close(p[1]);
FILE *in =fdopen(p[0],"r");assert(in !=NULL);long reported;assert(fscanf(in,"%ld",&reported)==1);assert(reported ==(long)child);assert(fclose(in)==0);
siginfo_t info ={0};assert(waitid(P_PID, child,&info, WEXITED | WNOWAIT)==0);assert(info.si_code== CLD_EXITED && info.si_status==7);int status;assert(waitpid(child,&status,0)== child);assert(WIFEXITED(status)&&WEXITSTATUS(status)==7);
errno =0;assert(waitpid(child,&status, WNOHANG)==-1&& errno == ECHILD);puts("exec preserved PID; exit 7 observed; child reaped");}
Here dup2() creates a standard-output descriptor without CLOEXEC. The original pipe descriptors have CLOEXEC and are explicitly closed. If final standard output also had CLOEXEC, the new program could not report its PID through it. Assertions express the example's assumptions; compile without NDEBUG. Production code also needs ordinary error handling for interruption, resource exhaustion, and cleanup.
6. Kernel threads and live diagnosis
Kernel threads normally have mm == NULL and may borrow active_mm. They still have a task, kernel stack, scheduling policy, and can block. Do not assume their address-space context always uses init_mm.
Stopping requires cooperation.kthread_stop() requests termination, wakes the thread, and waits. The function must check kthread_should_stop(), and its wait paths must give it that opportunity. A request alone cannot finish shutdown if the thread is stuck in a wait that never completes.
Workqueues run work items on managed worker pools; each submission does not create a dedicated kernel thread. During module or device shutdown, prevent new work, cancel or wait for in-flight work, then free its memory. A stop request alone does not establish that data is no longer accessed.
Start diagnosis with Pid, Tgid, NSpid, and Threads in /proc/PID/status, then examine /proc/PID/task/TID/. The parenthesized comm field in /proc/PID/stat can contain spaces, so naive whitespace splitting and fixed field indexes are unreliable. For R, D, and Z, investigate queueing time, the wait object, and the parent's reaping logic respectively.