Process-Related System Calls
14.1 What Is a Process — Program in Execution
14.1.1 Formal and Plain-Language Definition
Hook: Why does double-clicking a browser icon feel instant, while installing the same browser takes minutes and yet does nothing until you launch it?
A process is a program in execution. Installing a program — a browser, a software tool, an antivirus — does not start it. Execution begins when the program is invoked, for example by double-clicking an icon or typing its name on the command line. At that point the operating system assigns CPU time to the program, the CPU fetches instructions one by one from that program and processes them. Because it processes instructions, it is called a process. A widely referenced definition adds a modern qualifier: a process is an instance of a computer program that is being executed by one or many threads. That addition reflects current hardware.
Intuition — program versus process: Think of a recipe card versus a cook in action. The recipe card sitting in a drawer is the program — static instructions on disk. The cook who has read the card, gathered ingredients, and is now chopping and stirring on the counter is the process. The same card can spawn many cooks at different counters, each with their own ingredients and progress. The system call that hires a new cook is fork, which we meet in the next section.
Where the analogy breaks: a cook is a person; a process is the operating system's bookkeeping plus resource allocation. A process does not think — it follows instructions exactly, and the operating system decides when it gets the stove (the CPU).
Formalizing process: In operating system terms, a process is an active entity that represents a program's execution context at a point in time.
- Program (passive): the executable file on disk — code plus initial data, waiting to be used. It has a pathname but no CPU time, no memory image beyond the file.
- Process (active): the instance the kernel creates when it loads that program into memory, builds an address space, assigns a unique identifier (
PID), and schedules it for execution.
Every process carries two parts: the code that describes what to do, and the data the code works on (global variables, heap objects, stack frames). The textbook phrase captures the modern case: a process is an instance of a computer program that is being executed by one or many threads. On a single-core machine an instance with one thread is enough; on a multi-core machine the same process may run several threads in parallel, each on a different core, yet they still belong to the same process container.
No formula is needed here; the key distinction is state: program = file, process = file + memory image + kernel record + execution progress.
Recap + Bridge: A process is not the file you install but the live execution of that file after the operating system gives it CPU time and memory. This container — code plus data plus kernel record — is what the system can copy with fork to make more work happen. Next we see why copying is useful, using a web server that must serve thousands of personal results at once.
14.1.2 Threads and Modern Multicore Reality
On modern machines there is rarely a single core or a single CPU. Most systems have multiple cores or multiple CPUs, so a program is often built to run in parallel. Parallel means multiple threads execute at the same time. A thread is a part of a process that gets executed on a CPU or on one core of a CPU. It shares the process context but runs its own instruction stream. The process definition therefore says one or many threads to capture this parallelism.
Intuition — factory floor: Picture a workshop (the process) that owns a shared workbench, shared tools, and a shared parts bin (the process address space, open files, global data). Inside that workshop, several workers (threads) can stand at different corners of the bench and work at the same time. They share the bench and the bin, but each worker has their own hands and their own list of next steps (their own stack and program counter). If one worker needs to wait for paint to dry, another can keep assembling.
On a single-core CPU the workers take turns very fast so it looks parallel; on a multi-core machine they truly work side by side on separate cores. The process definition says one or many threads so it covers both cases without having to redefine the term for old versus new hardware.
Thread versus process: A thread is the smallest schedulable unit inside a process.
- Belongs to exactly one process; cannot exist alone.
- Shares the process address space, open file descriptors, and global variables with every other thread in that process.
- Has its own stack, registers, and scheduling state, so it can be placed on any available core independently.
A single-threaded process behaves like the classic definition: one instruction stream. A multi-threaded process has several instruction streams cooperating inside the same container. The distinction matters for fork: in current systems such as Linux 3.2 and Solaris 10, fork duplicates only the calling thread, not every thread in the process (see the companion docs R5_Chapter 8_Process Control.txt discussion of copy-on-write and thread duplication). That is why process and thread are kept as separate terms — a process can contain many threads, and duplicating a process is not the same as duplicating each thread.
Scope: This thread-aware definition assumes a general-purpose operating system such as Unix or Linux on multi-core hardware. On a tiny embedded controller with no thread support, a process and a thread may map to the same thing, and parallelism is obtained by creating separate processes rather than threads inside one process. When the hardware has only one core, parallelism is still logical — the kernel interleaves threads over time — but physical overlap does not occur.
Visual intuition: picture a timeline of wall-clock time on the horizontal axis and cores on the vertical axis. On a single core, threads appear as colored segments interleaved on one row — A runs, then B, then A again. On four cores, four rows are active at once, each row showing a different thread's colored blocks executing in parallel. Takeaway: the same process abstraction maps to either time-sharing or true parallel execution depending on hardware.
Real-world: a video editor that decodes video on one thread, decodes audio on another, and renders the preview on a third, all inside one editing process, demonstrates why the modern definition needs one or many threads.
14.1.3 Composition: Code and Data
A process consists of the executable code that was written and describes what to do, plus the data that code will use while it runs. When a process is created it gets an image in memory that holds code, data, and the bookkeeping needed to execute the code with the data. Understanding code plus data as the in-memory imprint helps to see why creating a process needs extra memory.
What the process image holds: When the kernel admits a program, it builds a memory image that contains:
- Code (text) segment — the compiled instructions, normally read-only and sharable between parent and child after
fork. - Data segment — initialized globals; BSS for zero-initialized globals; heap that grows toward higher addresses for dynamic allocation; stack that grows toward lower addresses for function frames and local variables.
- Kernel record — the process control block (PCB), file descriptor table, signal disposition, and identifier fields such as PID, parent PID, and user IDs, discussed again in the life-cycle section.
The phrase code plus data therefore means more than source lines; it means everything the CPU must fetch or read while the process runs, plus the kernel bookkeeping that tracks it. Section R5_Chapter 8_Process Control.txt notes that parent and child share the text segment after fork but get separate copies (via copy-on-write) of data, heap, and stack, which is why creating a process costs memory.
Pitfalls:
- Treating installation as execution — installing writes a file to disk; no CPU time or memory image exists until launch.
- Assuming code alone is the process — without the data and kernel record, the CPU has nothing to operate on and no way to be scheduled or identified.
- Thinking a process is just a thread — a thread runs code; a process owns the shared memory, files, and code that threads run inside.
Visual intuition: draw the process address space as a vertical bar: text at the bottom (fixed, read-only), then initialized data, then BSS, then a large middle gap for the heap growing upward and the stack growing downward with an arrow on each side showing growth. The PCB sits outside this bar in kernel memory, pointing to the bar with a PID label. Takeaway: code plus data is a contiguous image with bookkeeping that ties it to a schedulable identity.
Recap + Bridge: A process is the live pairing of executable code and the data it operates on, plus the kernel record that gives it an identity and resources. That pairing is what must be copied when a new process is created, which explains the memory cost that causes fork to fail under pressure — the story of the next section.
14.2 The fork System Call — Primary Way to Create Processes
14.2.1 The Need for Process Creation
In Unix-like and Linux-like systems, a running program is already a process. For the system to create more activity it needs a method to make new processes from existing ones. The list of process-related system calls for this discussion includes fork, wait, exec, exit, signal, kill, and raise. The first method in that list, fork, is the primary way to create a new process. The new process is called the child process.
Hook: If every running program is already a process, how does the system ever get a second program running without first having someone to start it?
Why fork exists: After boot, the kernel creates the first user process (traditionally init with PID 1, now often launchd or systemd on newer systems). From there, the only way to get more processes is for an existing process to ask the kernel to duplicate itself. That request is fork.
Signature from the companion docs R5_Chapter 8_Process Control.txt:
- Takes no input parameters — it needs no argument to know what to copy; it copies the caller.
- On success it creates one new process called the child; the caller remains the parent.
- The family of process calls for this lecture is:
fork(create),wait/waitpid(collect),exec(replace),exit(end),signal/kill/raise(notify).
Intuition: the system grows like a cell dividing — one living cell becomes two, each carrying the same DNA (code and data image) at the moment of division.
Scope: fork is the classic Unix model. Some systems expose related calls such as vfork, clone on Linux, or rfork on FreeBSD (see R5_Chapter 8_Process Control.txt), and some treat fork-then-exec as a combined spawn. For this lecture the model is plain fork as defined in <unistd.h> with the behavior above.
14.2.2 The Web Server Story — Scaling with Child Processes
A concrete picture makes the purpose of fork easy to hold. Consider a web server as a running process whose job is to listen for web requests from clients and serve a response. For simple informational pages such as a tourist site, a request arrives, the server fetches the page and sends it back. That works for light traffic.
Now picture results day for a large exam. Tens of thousands of clients press submit at the same moment, each request carrying a register number that requires a database lookup to build a personal results page. A single server instance cannot handle thousands of simultaneous lookups by itself. Even though each lookup may take only milliseconds, those milliseconds let a queue of ten to fifteen more requests build up.
The classic solution is delegation by forking. As soon as the parent server receives a request it creates a child process that is fully capable of handling that request. The parent hands the request to the child and immediately returns to its waiting state to listen for the next request. The child fetches data, formats the response, sends it to the client that asked, and then ends itself. On the next arrival the parent repeats the pattern: create a child, pass the request, wait for more work. In this way one listening process scales by manufacturing helpers on demand.
Intuition — restaurant host: The parent server is the host at the entrance who never leaves the door. Each arriving guest (request) is handed to a newly hired waiter (child process) who walks the guest to a table, takes the order, fetches food from the kitchen (database), serves it, and then leaves. The host stays by the door to greet the next guest.
Delegation pattern: The web server creates child per request pattern works as:
- Parent: loops forever in
listen/accept— blocks waiting for the next client. - On accept: calls
fork; on success the child inherits the connected socket descriptor (the file descriptor table is duplicated, perR5_Chapter 8_Process Control.txtFigure 8.2). - Child: closes the listening socket, handles one client (database query by register number, format HTML, write response), closes the connected socket, calls
exit. - Parent: closes the connected socket in its own table (so only the child holds it), returns to
acceptfor the next arrival.
The power is that the parent never blocks on the slow step (database). Even if one lookup takes 50 ms, ten more requests that arrived during those 50 ms can each get their own child in the next loop iterations, so the queue stays short. This is lightweight compared to launching a whole new server program from disk for each request, because the child already carries a copy of the server's code and configuration at the moment of fork.
Worked walkthrough — five rapid requests:
- Time 0 ms: Parent listening. Requests A, B, C arrive together.
- Time 1 ms: Parent accepts A,
fork→ Child A created, handles A's database lookup. - Time 2 ms: Parent loops, accepts B,
fork→ Child B created, handles B. - Time 3 ms: Parent accepts C,
fork→ Child C created, handles C. - Time 60 ms: Child A finishes lookup and response, exits; its termination status sits as a zombie until parent collects it with
wait(or the init adopter reaps it if parent never waits). - Parent was never blocked waiting for A's database call — throughput came from overlapping children.
If no fork existed, requests B and C would wait behind A's 50 ms lookup each, so the third client would wait ~100 ms before service even started. With forking, all three start within a few milliseconds of arrival.
Pitfalls:
- Forgetting that each child must close what it does not need — the parent closes the connected socket, the child closes the listening socket, or descriptor counts grow and connections never fully close.
- Assuming forking scales without bound — each child costs memory for its copied data/heap/stack (copy-on-write delays but does not remove the cost), so tens of thousands of simultaneous forks will exhaust memory and
forkwill return a negative error. - Never collecting children — without
waitor an init reaper, finished children linger as zombies holding a process table slot.
Visual intuition: draw a vertical timeline of the parent as a single line with accept points, and at each accept a short branching line appears, runs for a small horizontal segment (the child handling one request), then ends with an X. The parent line stays continuous at the top, never pausing for the branch's work. Takeaway: the parent is a dispatcher, not a worker.
14.2.3 Historical Context Versus Modern Scaling
The web-server-forks-a-child-per-request pattern is described as the older, traditional way. Today services often scale out differently: multiple server instances run in parallel on different machines and a client-facing component called a load balancer spreads incoming work. A simple policy is round-robin: with five servers, request one goes to server one, request two to server two, and so on, wrapping around after server five. Many policies exist, but the point of telling the older fork story is to show where this way of thinking started and why fork exists as a building block.
Old versus modern scaling:
| Aspect | Fork-per-request (classic) | Load balancer scale-out (modern) |
|---|---|---|
| Where parallelism lives | Inside one machine — parent forks children | Across many machines — many server instances |
| Dispatcher | The parent process itself after each accept |
A front-end load balancer before any server sees the request |
| Scaling limit | Process table and memory on one host | Number of hosts plus network capacity |
| Failure scope | Parent crash stops all children on that host | One server crash drains to others behind the balancer |
| When to pick which | Simple services, low-to-medium concurrency, Unix teaching model | High traffic, fault tolerance, cloud deployments |
The round-robin policy illustrates the balancer's job in the simplest form. With five servers S1 … S5:
so request 1 → S1, 2 → S2, … 5 → S5, 6 → S1 again. More advanced policies use least-connections, weighted round-robin, or latency-aware routing, but the core idea is the same: spread work rather than fork locally. The classic fork model survives as the conceptual ancestor — understanding how one parent manufactures helpers makes it natural to understand how a balancer manufactures or routes to helpers at a larger scale.
Real-world: cloud providers still teach the fork-per-request sketch because it maps directly to container-per-request and serverless-function-per-request designs — the names change, the dispatch-then-handle pattern remains.
14.2.4 What the Child Executes by Default and How to Deviate
By default a new child gets a fresh copy of the parent. In memory that means a separate copy of the parent program — the same code and data — and the child begins executing it on its own. That raises a natural worry: will every child just go back to listening like its parent? Usually that is not what is wanted. The child should do something else, such as handling one specific client request. There is a way to deviate from copying the parent blindly: after a fork the child can be told to execute different code. That deviation is where exec enters, discussed later.
Default copy and the need to deviate: After fork the child's memory image is a copy of the parent's: same code segment, same data/heap/stack snapshot (implemented via copy-on-write so only modified pages are truly copied, per R5_Chapter 8_Process Control.txt). Both resume at the instruction right after the fork call (Section 8.3 fork Function — "Both the child and the parent continue executing with the instruction that follows the call to fork").
That is powerful — the child already knows how to handle a request because it already is the server — but it is also wasteful if every child kept listening for new connections alongside the parent. Two listening loops would fight over the same port and duplicate work. The fix is immediate branching on the fork return value: the child takes a different path. When the needed child work lives in a different program entirely (for example a dedicated results-formatting binary that takes a register number as an argument), the child replaces its copied image with that new program via exec. The sequence is therefore fork → test return → child calls exec(program, args, env) → new image runs; parent stays as the dispatcher and later calls wait/waitpid to collect the child's exit status.
Pitfalls:
- Letting the child fall through into the parent's accept loop — creates duplicate listeners and port contention; always branch immediately after
fork. - Calling
execin the parent by mistake — replaces the listener itself and it can never accept the next request. - Expecting shared variables to stay shared — parent and child have separate data copies after
fork; writing to a variable in the child does not change the parent's variable, as shown in Figure 8.1 ofR5_Chapter 8_Process Control.txt.
Visual intuition: draw the parent memory image as a rectangle labeled SERVER. At fork a second identical rectangle appears next to it with a dotted arrow labeled copy. Immediately after, the child's rectangle is crossed out and replaced with a rectangle labeled HANDLER, while the parent's rectangle stays labeled SERVER. Takeaway: fork clones, then the child diverges.
Recap + Bridge: fork alone copies the parent — useful for a quick helper, but most real helpers need to do different work, so the child branches and often calls exec to become a specialized program. Before we use that pattern, we need the full life journey a process follows and the identifier that lets us tell parent from child — topics of the next two sections.
Exam note: Be ready to explain why fork is needed, what a child executes by default, and why a child often needs to be redirected via branching or exec to different work. Contrast the older fork-per-request model with modern load balancer round robin scale-out and state when each fits.
14.3 Life Cycle of a Process — The Five States
14.3.1 The Five States at a Glance
A process moves through distinct states from start to removal:
- New — the program has been selected to start, for example by double-clicking an icon.
- Ready — memory is available, modules are linked, a process control block has been defined, and the process is loaded and ready to be given the CPU.
- Running — the scheduler has placed the process on the CPU; the processor fetches each instruction, accesses data as needed, and executes instructions one by one.
- Waiting (sometimes called blocked) — the process is paused for an input-output event to finish.
- Terminated — execution is finished, an exit is issued, and resources are returned to the system.
Hook: Why does clicking PRINT not freeze your entire computer while the printer crawls?
The five-state model: This is the classic teaching taxonomy used to explain multiprogramming without naming every scheduler variant.
| State | What it means | What holds the process |
|---|---|---|
| New | The system has been told to start a program but has not yet committed resources | Job queue / admission decision point |
| Ready | Resources committed — memory allocated, modules linked, PCB created — waiting for CPU | Ready queue |
| Running | Instructions are being fetched and executed on a CPU core | The CPU itself |
| Waiting / Blocked | Execution paused until an I/O event completes (disk, printer, network, user input) | Waiting (I/O) queue |
| Terminated | Finished — exit has been issued, but kernel record may linger until collected | Zombie / termination record |
The bold addition in R5_Chapter 8_Process Control.txt — that init inherits orphans and reaps them — adds the system-level guarantee that every process has a parent until the kernel finally discards the record.
14.3.2 From New to Ready to Running to Terminated
The journey begins in the new state when the system is told to start a program. If memory can be allocated and linking succeeds, the process is admitted and moves to ready. Ready means ready to run on the processor but not yet running. The scheduler then picks the process, gives it the CPU, and the state changes to running.
If nothing interrupts it, execution continues through the program until the last statement, where an exit is issued. That signals that the job is done and the process should be removed from memory with all resources reclaimed. The transition is running to terminated.
Two other departures from running are important. If the process gets interrupted by a timeout — each process is given a time slice — an interrupt causes preemption and the process returns to the ready queue so other work can run and the interrupt can be handled. This is running to ready.
Core path and the two exits from Running:
- Admission: New → Ready. The kernel checks memory, links modules, creates the process control block (PCB) — the record that holds PID, parent PID, register snapshot, open files, and scheduling info — and places the PCB on the ready queue.
- Dispatch: Ready → Running. The scheduler selects a ready PCB and loads its context onto a CPU. The CPU fetches instructions one by one, reading data as needed.
- Completion: Running → Terminated. The process executes
exit(or returns frommain, or calls_exit/_Exit) and the kernel reclaims data/heap/stack while preserving a small termination record for the parent to collect. - Preemption: Running → Ready. A timer interrupt ends the current time slice; the kernel saves the process state back into its PCB and moves it to the tail of the ready queue. This is the timeout case in the lecture material.
- Blocking: Running → Waiting. A slow I/O request forces the process off the CPU (see the printer walkthrough next).
- Wake-up: Waiting → Ready (never directly to Running).
The rule that Waiting goes to Ready, not straight to Running, is the most tested point in this section — while one process waited, another was placed on the CPU, and the scheduler never yanks a running process off the CPU purely because a wake-up arrived. The awakened process joins the ready queue and waits its turn.
Visual intuition: draw five circles labeled New, Ready, Running, Waiting, Terminated with arrows: New→Ready, Ready→Running (labeled dispatch), Running→Ready (labeled timeout/preempt), Running→Waiting (labeled I/O request), Waiting→Ready (labeled I/O complete interrupt), Running→Terminated (labeled exit). Takeaway: Ready is the only entry to Running.
Tiny example — adding two numbers and printing:
- New: user double-clicks
adder. - Ready: kernel allocates memory, creates PCB with PID 2441, places it on ready queue.
- Running: scheduler dispatches 2441, CPU executes
a=3; b=4; c=a+b; print(c)step by step. - Terminated:
exit(0)runs, user sees7, kernel frees memory but keeps the exit status until the shell (the parent) callswait.
If the same program had needed printer output, it would have taken the detour through Waiting, covered next.
14.3.3 The Waiting State and the Printer Example
Consider a program that needs to print a document. While running, it requests the printer to take over the job. Keeping the process on the CPU while the printer works would waste the processor. Instead the system moves the process off the CPU into the waiting state and puts it in a waiting queue. The printer prints, the CPU becomes free to run other programs, and the system achieves multiprogramming — many programs making progress over time.
The waiting example was carried fully: the request to print moves the process from running to waiting; the message "please wait until the printing finishes" describes the wait queue; the printer, on completion, sends an interrupt to signal that the job is done; the system then moves the process from waiting back to ready. A frequent point of confusion is why it goes to ready rather than straight to running. While the first process waited, other programs were running. The system cannot just eject a running program. Instead the awakened process re-enters the ready queue so the scheduler can give it the CPU in turn.
Step-by-step trace — the printer walkthrough with PIDs:
- Process P1 (
PID2100) is Running and executesfprintf(printer, doc). The kernel moves P1 from Running → Waiting and enqueues its PCB on the printer's I/O queue. Message shown to the user: "please wait until the printing finishes" — that text is the human face of the wait queue. - CPU becomes free. Scheduler picks P2 (
PID2101) from Ready → Running. P2 now uses the CPU to compute, so the system achieves multiprogramming — P1 slowly printing, P2 actively computing, overlapping in wall-clock time. - Printer hardware finishes, raises an interrupt. The kernel's interrupt handler marks P1's I/O as complete and moves P1 from Waiting → Ready (back to the ready queue).
- Scheduler at its next decision point picks P1 again (or keeps P2 for the rest of its slice). P1 resumes Running after the
fprintfline, checks the return code, and continues.
The key ordering insight from the lecture: Waiting→Ready is not wasteful — it is fair. Jumping straight to Running would require preempting P2 without a scheduling decision, breaking the uniformity of the scheduler's policy.
Intuition — you at a photo-print kiosk: You (P1) hand your USB to the kiosk and it prints. You step aside into a waiting line rather than standing frozen at the machine blocking everyone else. When your photos drop into the tray, the clerk calls your name and you rejoin the main line for the counter, rather than shoving the person currently being served.
Scope: Waiting here means blocked on I/O (I/O-bound pause). Modern systems split this into finer sub-states (e.g., interruptible sleep vs uninterruptible disk wait, stopped, traced), but for this course the coarse five-state view is sufficient. The behavior Waiting → Ready → Running holds across those refinements.
14.3.4 Scheduler, Control Block, and Multiprogramming
The ready queue and the waiting queue are managed by the scheduler. When memory is allocated and the process is loaded, a process control block — the operating system's record for that process — is defined. From there the scheduler assigns the CPU to ready processes, handles time-slice expiry, and re-queues waiting processes after their I/O completes. The same flow applies to a tiny job such as adding two numbers and printing them: new, admitted to ready, scheduled to running, then terminated once the last instruction completes.
Scheduler and PCB — the machinery behind the states:
- Process control block (PCB): the kernel's per-process ledger created during New→Ready. It holds at least the PID, parent PID, saved registers and program counter, memory map pointers, open-file table pointer, signal mask, scheduling priority, and accounting times.
R5_Chapter 6_The Structure of Processes.txtandR5_Chapter 7_Process Control.txtformalize this record as the structure the kernel consults at every context switch. - Ready queue: the list of PCBs whose processes can run but are not currently on a CPU. Managed as a priority queue or round-robin structure depending on the scheduler.
- Waiting queue(s): one per I/O device or event type, holding PCBs whose processes are blocked until that device signals completion.
- Scheduler: the kernel code that repeatedly (a) saves the outgoing Running process's registers into its PCB, (b) picks the next PCB from Ready, (c) loads those registers onto the CPU. Time-slice expiry and I/O interrupts are the two main reasons the scheduler is invoked.
Multiprogramming emerges because the CPU never idles waiting for a printer, disk, or network reply — those waiting PCBs sit off-CPU while other Ready PCBs get the cycles. The printed slide sequence in the lecture (adding two numbers and printing, then the printer walkthrough) is meant to show that the same four-transition skeleton covers both a trivial compute-bound job and an I/O-bound job.
Pitfalls:
- Calling Waiting "dead" — a waiting process is not terminated; it is alive, just not on the CPU, and will become Ready as soon as its I/O completes.
- Forgetting the PCB role — without the PCB the kernel would have nowhere to save registers at preemption; every Running→Ready step depends on that record.
- Assuming I/O completion means immediate resumption — it means readiness, not resumption; scheduling order still decides when it actually runs.
Visual intuition: sketch the scheduler as a traffic officer at an intersection. Ready queue is cars lined at a green-light lane, Waiting queue is cars parked at a service station off the road, the CPU is the intersection itself, and the officer (scheduler) directs one ready car into the intersection, parks any car whose engine needs a tow, and waves a serviced car from the station back into the ready line. Takeaway: progress comes from keeping the intersection fed while slow service happens off-road.
Recap + Bridge: The five states plus the scheduler and PCB explain how the system keeps the CPU busy — New→Ready admission, Ready→Running dispatch, Running→Ready on timeout, Running→Waiting on I/O, and Waiting→Ready on completion. That lifecycle is the track on which process creation (fork), identity (PID), and synchronization (wait, signals) all run, and we turn to those mechanisms next.
Exam note: Be ready to list the five states in order and to walk the printer example: Running→Waiting on print request, Waiting→Ready on interrupt, and the reason the return is to Ready rather than immediately to Running.
14.4 Process Identity and How fork Reports Its Result
14.4.1 Process Identifier — The Tracking Handle
The system tracks every live process through a process identifier, abbreviated PID. When a process is created it is given a PID. This integer is the handle the system uses to refer to that specific process in later operations such as waiting for it or sending it a signal.
PID — the kernel's handle: A PID is a non-negative integer that uniquely names a live process for the human-readable lifetime of that process. It is returned by getpid and the parent's PID by getppid, as declared in <unistd.h>:
- Every new process gets a fresh PID at creation; the companion docs
R5_Chapter 8_Process Control.txtnote thatPID0 is the kernel scheduler/swapper andPID1 isinit(orlaunchdon macOS), soPID0 will never be assigned to a normal child, which is whyforkcan safely use 0 to mean child. - PIDs are reused after processes die, but systems delay reuse so a new process is not mistaken for a recently terminated one.
- The PID is what later calls need:
waitpid(pid, &status, 0)waits for a specific child by PID, andkill(pid, SIGUSR1)signals a specific process by PID.
Analogy: a PID is the numbered tag the cloakroom gives you at a conference — a small integer that stands in for your whole coat and bag while they sit on the rack, unique for the evening and reused next evening.
Scope: A process also has user and group IDs (real, effective, saved) that control permission checks, but for this topic the only identifier that matters is the PID plus its parent PID.
14.4.2 Fork Signature: No Input, Tri-State Result
The fork system call takes no input parameters. Its reporting behavior is unusual and is worth stating slowly. Every program returns an integer to the system to indicate success or failure of its run. Fork follows that, but with a split outcome:
- If fork cannot create a child, for example when there is not enough memory to copy the parent image, it returns a single negative value. The negative value is the error signal that creation did not go through. One reason highlighted is memory pressure: a child needs a fresh copy of the parent, and without extra memory that copy cannot be made.
- If fork succeeds, it returns two values in total. One is zero, and that zero belongs to the child process. The other is a positive value, and that positive value belongs to the parent. The positive value is the PID of the newly created child.
This two-valued success return is the surprising part: after a successful creation both the parent and the child continue execution and each receives a return from the same fork call.
The three-way contract: The canonical declaration from R5_Chapter 8_Process Control.txt is:
where is the PID of the newly created child and is the error case.
- Takes no arguments:
forkcopies the caller, so it needs no pathname, no size, no template — the template is the caller. - Called once, returns twice on success: the call site in source appears once, but after the kernel clones the address space, both the original and the clone resume at the next instruction, each receiving a different return value mapped into its own registers. That is why the phrase "returns two values in total" is literal — one value appears in each process's world.
- Failure mode:
forkfails when the kernel cannot allocate a new PCB or address space — out of memory, process table full, or per-user process limitCHILD_MAXreached (two causes listed inR5_Chapter 8_Process Control.txt). The negative return is the single-process analogue of an exit-code error: one caller, one negative value, no child exists to receive anything.
The surprise for newcomers is not the negative case but the success case — the same source line yields two different integers in two processes at once.
Pitfalls:
- Testing only
if (pid == 0)and forgetting the< 0error branch — then an error looks like whatever the parent branch does by accident. - Treating a positive return as "the parent's own PID" — it is the child's PID, delivered to the parent so the parent can later name that child in
waitpidorkill. The parent's own PID is obtained viagetpid, the child's parent PID viagetppid. - Assuming
forkreturns –1 in the child — error produces no child at all, so there is no second process to return in.
Visual intuition: draw one dot before the call (the parent). At the call, the dot splits into two dots side by side. Annotate the left dot (child) with 0, the right dot (parent) with a tag like 1692, and put a single crossed dot above them labeled –1 for the error case that stays as one dot. Takeaway: one call, up to two return paths, distinguished by three integer regions.
14.4.3 Why Zero for the Child and a Positive PID for the Parent
The zero tells code that "you are the child." The positive PID tells code that "you are the parent, and this is your child." By testing the returned integer, a program can place child work in one branch and parent work in another. Variables involved: PID of parent and PID of child where and the child sees . The error case is signaled by a value in .
Why zero and why the child's PID: R5_Chapter 8_Process Control.txt gives the design rationale explicitly:
- Why the parent gets the child's PID: a parent can have many children, and there is no call that lists "my children's PIDs" after the fact, so the kernel must hand the new child's PID to the parent at birth or the parent would have no way to refer to that child in
waitpidorkill. Storingpid = fork()in the parent therefore saves the handle to that specific child. - Why the child gets zero: a child has exactly one parent, so it never needs the parent to tell it the parent's PID — it can ask the kernel any time with
getppid(). Giving zero is space-efficient and exploits the reservation that PID 0 never belongs to a real child (it is the kernel's scheduler), so 0 unambiguously means child without colliding with any valid child PID. The child can always learn its own PID withgetpid()and its parent's PID withgetppid()if needed.
Putting this together, the idiomatic branch (covered in full in the next section) is:
where the three integer regions , , and map directly to the three control-flow arms.
Quick mental model with concrete PIDs: Suppose the shell has PID 1691 and the parent process in our demo also has PID 1691 before fork. Upon pid = fork():
- In the error universe: one process remains,
pidis –1 inPID1691, no second process exists. - In the success universe:
PID1691 seespid = 1692(positive — "you are the parent and your child is 1692"); the brand-newPID1692 seespid = 0("you are the child"). Both then execute the next line, diverge byiftests, and can report their identities withgetpid()/getppid()to confirm.
If the parent later wants to wait for that specific child, it already has the value: waitpid(pid, &status, 0) where pid still holds 1692.
Visual intuition: draw the fork site as a fork in a road. Signposts after the split read –1 (road closed, stay put), 0 (you are the new side road), and a tagged number like 1692 (you are the original road and that number names the new side road you just opened).
Recap + Bridge: The PID is the system's handle; fork takes no inputs and, unusually, returns twice on success — 0 to the child and the child's positive PID to the parent, or a single negative value on failure. Testing that return value is how code decides who does what after the split, which is the entire technique of the next section.
Exam note: State the tri-state convention precisely — negative means failure with no child, zero marks the child, positive is the child's PID delivered to the parent — and identify which branch is child versus parent in C code. Be ready to give getpid/getppid as the calls that recover the true identity when the fork value alone is not needed.
14.5 Fork in C — First Program and Execution Order
14.5.1 Headers and Build Setup
The demonstrations use C. A common header for standard input-output work is needed, and two additional headers are required to use fork and to have access to the type definitions it relies on. One is the Unix standard header and the other is the types header. The compiler used is GCC, the GNU C compiler, which produces an object program from source.
Build scaffolding for every fork demo: The three headers are not interchangeable — each supplies a distinct layer:
<stdio.h>— standard I/O declarations (printf,fprintf,putc) used to print results. Not related toforkitself.<unistd.h>— the Unix standard header that declaresfork,getpid,getppid,sleep,pause,execvariants,STDOUT_FILENO, etc. Include it or the compiler will not seefork.<sys/types.h>— type definitions such aspid_t(the integer type for PIDs). Strictly,pid_tis defined here; practically, including both<sys/types.h>and<unistd.h>together covers all historic and POSIX orderings, hence the lecture's pattern of including both.
A minimal compilable skeleton is:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(void) {
/* fork demos go here */
return 0;
}
Compilation uses GCC:
where -Wall asks for common warnings and -o names the object program (for example hello). Listing the directory before and after compilation was used in class to confirm that the new file appeared.
Scope: The same header triple appears in almost every Unix process-control example in R5_Chapter 8_Process Control.txt (Figure 8.1 uses #include "apue.h" which itself includes these plus <sys/wait.h> for wait). If only <stdio.h> is included, fork will trigger an implicit-declaration warning or an error depending on the compiler flags.
14.5.2 Code Walkthrough — One Fork Followed by One Print
The minimal example has this shape in the main program:
- include standard I/O, Unix standard, and types headers
- enter main
- call fork with no arguments
- execute a print statement that outputs a line such as "hello world"
- return from main
The key teaching note is that code after the fork is executed by both the parent and the child. The parent existed before the call, the child is created at the call, and from that point onward both continue through the remaining statements.
What the minimal program really says:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(void) {
fork();
printf("hello world\n");
return 0;
}
Step-by-step in kernel terms:
- Before
fork(): one PCB, one address space, one program counter at the call site. fork()system call: kernel allocates a new PCB, copies parent data/heap/stack (copy-on-write delays physical copying), shares text segment, duplicates the file descriptor table so both share the same open stdout offset perR5_Chapter 8_Process Control.txtFigure 8.2, and assigns the child a fresh PID.- After
fork(): two independent scheduling entities, both with the program counter just past theforkcall, so both arrive atprintfand both arrive atreturn 0.
The shock for a newcomer accustomed to functions that return once is that the line after fork runs twice, in two processes, each with its own return value if the call is saved into a variable. Without saving the value, both just print — the system does not automatically label which output came from whom.
Pitfalls:
- Placing work before the
forkand expecting it to run in the child only — everything beforeforkran in the parent alone. - Assuming
printfbeforeforkprints once — buffered I/O nuances mean aprintf("before fork\n")before theforkflushes on newline to a terminal but stays buffered to a file and then appears twice after the data copy, as demonstrated in Figure 8.1 discussion inR5_Chapter 8_Process Control.txt.
Visual intuition: write the source vertically from top to bottom. Draw a single line from the top until fork(), then split into two parallel lines leaving the same point, both passing through printf and return. Label the left branch child (0) and the right branch parent (child PID).
14.5.3 Worked Execution — Why Two Hello Worlds and Why Parent First
Running the minimal program produced:
hello world
hello world
with the shell prompt appearing between the two lines in a characteristic way: first line, then the prompt, then the second line just after the prompt, with a newline restoring the prompt on the next enter. Students were asked how many times the print would run. The consensus "two times" was correct. One execution belongs to the parent, one to the child.
A common first guess tried to explain the prompt placement with threads. The correction kept the distinction: this is about processes, not threads. The ordering was then traced. Creating a process is a heavy task: the system must find a fresh region of memory, copy the parent image there, and prepare the child to start. That takes a small but non-zero time. Meanwhile the parent is already loaded and ready, so it reaches the print first, emits the first line, and returns without waiting for the child. The shell prints its prompt because the parent has finished. Slightly later the child finishes preparation, reaches the same print, emits the second line right after the prompt, and returns. The interval is tiny but visible.
An illustration used two runners: one runner is already on the track and can go immediately, while a new runner must be created and must go through a brief starting phase. The head start explains the parent-first order without needing a sleep or explicit wait.
Trace with wall-clock ticks and prompt interleaving:
- : Shell (
PID1500) forked thehelloprogram (PID1600).helloparent is Running. - :
helloreachesfork(); kernel starts cloning address space for childPID1601. - : Parent 1600 already past
fork, executesprintf→ first linehello world\nappears on the terminal. - : Parent 1600 hits
return 0; kernel moves it to Terminated, notifies its parent the shell; shell prints promptuser@vm:~\$and blocks waiting for next command. - : Child 1601 finishes setup, resumes past
fork, executesprintf→ second linehello world\nlands after the prompt already on screen, so the display looks like:
hello world
user@vm:~\$ hello world
user@vm:~\$
Pressing Enter repaints a fresh prompt on the next line, masking the earlier interleaving.
Intuition of heavy creation versus already running parent: fork is not a library call that returns in user space instantly. The kernel path touches the process table, allocates memory, copies page tables, and duplicates file structures (see the weight described in R5_Chapter 8_Process Control.txt Section 8.3 fork Function). That path is short in human terms (microseconds) but long compared to the parent's single next instruction (a printf already scheduled). The two runners picture maps directly: the existing runner starts the final sprint at the whistle, the new runner must be dressed and placed on the line first.
Everyday analogy — parent who copies notes for a helper: The parent has the recipe already open on the desk and can read the next line at once. The child needs a photocopy of the whole desk before reading — fast, but still slower than reading without copying. That head start is why the first print tends to be the parent's on a fast machine.
Student Q&A — deduplicated:
Q: If a program has ten prints after a single fork, could the output of parent and child interleave and become hard to attribute?
A: Yes. With many prints the parent and child are separate processes that are scheduled on the CPU independently. When each gets a slice it runs its own prints. The lines mix and it is not possible to tell by appearance alone which line came from which process. Attribution needs explicit identification, such as printing the PID, rather than relying on order. The later demo uses getpid for exactly that reason. When only a few prints are present on a fast processor, the parent often finishes its whole slice before the child starts, so the output can look grouped.
Q: Looking at the single-fork program, why was the first hello world from the parent and the second from the child?
A: Because at the fork the system is busy building the child image. That build takes time. The parent is already running and goes straight to the print and to the return. The child needs a moment to be ready, so its print appears after the shell has already shown the prompt for the returned parent.
Pitfalls:
- Explaining the prompt placement via thread scheduling — this is separate address spaces, not shared-stack threads; the correct cause is process-creation overhead.
- Expecting deterministic ordering — the parent usually wins the first-print race on an unloaded fast processor, but the lecture notes that scheduling could flip the order under load. Without
wait, no ordering is guaranteed.
14.5.4 Build and Run Trace
The run used a fresh Ubuntu environment in a virtual machine. The IP address of the box was shown to connect to it, font size was increased for visibility, and the workflow was: create a source file, compile with GCC to produce an output program named hello, list the directory to confirm the object exists, and run the program. The interleaved prompt behavior described above was observed on execution.
Hands-on replication steps:
- Create file
hello.cwith the three includes plusfork(); printf("hello world\n");. - Compile:
gcc -Wall -o hello hello.c - Inspect:
ls -lshould show an executablehelloalongsidehello.c. - Run:
./hello— observe two lines with the shell prompt wedged between them, then press Enter to see a clean prompt again. - Repeat with buffering check:
./hello > out.txt ; cat out.txtstill shows two lines; the buffering-doubling case seen inR5_Chapter 8_Process Control.txtwould require aprintf("before fork\n")before the fork with fully buffered stdout.
The IP and font-size details in the lecture material are classroom logistics for connecting to the lab VM, not program semantics, but reproducing the compile-list-run loop cements that GCC is just the translator from fork-containing C source to an executable the kernel can load and clone.
Recap + Bridge: A fork followed by one print runs that print in both processes — two worlds after one call — and on a fast, unloaded machine the parent's copy tends to appear first because the kernel spends a brief moment building the child's image (the two-runners head start). That exponential potential, when forks repeat, is the whole topic next: one fork doubles, two forks quadruple, three forks create eight.
Exam note: Be ready to predict the number of prints after a single fork and to explain the parent-first prompt interleaving and why grouping differs with processor speed or with an added wait/sleep.
14.6 Multiple Forks — Exponential Process Creation
14.6.1 The 2-to-the-n Rule
When forks are placed in sequence so that every existing process hits the next fork, the number of processes doubles each time. With forks executed by every process that exists at the point of the call, the total number of processes that reach the final print is:
where is the number of fork statements, each fork is of the form , and exponentiation is integer. The number of child processes is one fewer:
because one of the processes is the original parent. The rule was used as a quick check rather than as a full derivation.
- Verbal description: "two to the power n is the number of times you see the print, n is the number of times you have fork."
- Symbol definitions: is the count of fork statements encountered in sequence; counts all processes that survive to the print.
Hook: Why do three innocent-looking fork() lines produce eight identical outputs instead of four?
Doubling logic: Each fork() encountered by a process splits that process into two. If every alive process reaches the next fork, the count doubles. Starting from one parent before any fork:
So total prints equals total processes that arrive at the final statement:
and because exactly one of those is the original parent (the only process that existed before the first fork), children are:
where counts sequential fork() statements with no guard that prevents some branches from forking again. The companion docs phrase this as "two to the power n is the number of times you see the print, n is the number of times you have fork" — a direct quote from the lecture material's verbal rule.
If a branch guards a future fork with if (pid == 0) so only children fork, the count changes — the lecture's example deliberately avoids guards between the forks precisely so every alive process doubles.
Scope: This rule assumes each live process actually executes each fork() on the path to the print. Adding a conditional such as if (pid != 0) fork(); or placing a fork inside a child-only block reduces the base and breaks pure doubling. Fork failure (negative return) also stops doubling for that path. For conditions with (no fork), correctly predicts just the original parent prints once.
Visual intuition: draw a full binary tree of height . Level 0 has 1 node (parent). Each edge is a fork. Count leaves at depth : leaves each represent a process that reaches the final printf. Label the path strings: for , leaves are ancestor→left/child versus right/parent choices — 000, 001, 010, 011, 100, 101, 110, 111. Takeaway: leaves, not internal nodes, correspond to prints.
14.6.2 Worked Computation with Three Forks
The class examined a program with four print-like statements surrounding three fork calls and worked the numbers in full. Students first guessed "four times." The careful count gave:
So eight lines appear. All eight were listed and numbered 1 through 8 on the display. One belongs to the original parent, seven are children. If a question asks how many child processes were created, the answer is . If it asks how many prints came from children only, it is also . The distinction was stressed because many students answer the child count with .
Every child after the first fork can itself become a parent for the next fork. That is why counting is not "three children for three forks" but exponential: child processes also fork.
Trace that produces eight prints:
Program skeleton the lecture traced (three forks, then one print):
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(void) {
fork(); /* n=1 */
fork(); /* n=2 */
fork(); /* n=3 */
printf("hello world\n");
return 0;
}
Step-by-step doubling with real numbers:
- After
fork #1: processes alive = — parent P0, child C1; both continue. - After
fork #2: each of those 2 forks again → alive = — P0, C1, C2 (child of P0 at #2), C3 (child of C1 at #2). - After
fork #3: each of those 4 forks again → alive = — numbered 1 through 8 on the display in the lecture. All eight fall through toprintf.
So:
The professor stressed the trap: a student who answers "how many child processes?" with 8 is counting the original parent as a child. The minus-one matters. And the phrase "four times" as an initial guess came from counting prints before thinking about doubling — there are not three forks so three children, because each new child immediately becomes a forker for later forks.
Pitfalls:
- Answering any child question with — reserve for total processes/prints, use for children.
- Forgetting that children of children count — a fork at depth 3 is executed by all alive processes, not just the original parent.
- Mixing this rule with guarded forks — if
forkresults are tested and some branches callexitbefore the next fork, those branches never double.
14.6.3 PID Hierarchy Demonstration
To make the hierarchy visible, the program was changed to print each process's PID using getpid. Sample output shown had values such as 70, 71, 72, 73, 74, 75, 76, 77 grouped to show parent-child ancestry. The interpretation given was that 70 was an ancestor, 71 was a child of 70, and subsequent IDs branched further, forming a tree. All eight processes printed, confirming the count, and all but one were identified as children.
Making the count visible with PIDs:
Replace the bare print with:
pid_t pid = getpid();
printf("PID %ld hello\n", (long)pid);
On a representative run the display showed eight lines with values like 70 through 77 (exact numbers vary per run; PIDs are assigned sequentially by the kernel and reused after delay per R5_Chapter 8_Process Control.txt).
A hierarchy view presented in class grouped them as a tree:
- Level 0: 70 is the ancestor (the process created when the kernel loaded the program).
- Level 1: 70 forks → 71 appears as a child of 70.
- Level 2: 70 forks again → 72 appears; 71 forks → 73 appears.
- Level 3: 70→74, 71→75, 72→76, 73→77 — eight leaves, each a distinct PID, the original parent 70 plus seven descendants.
Checking that all eight printed confirms total prints = . Checking that all but one have a different PID from the shell's initial PID confirms children = . Collecting the tree in this way also sets up the next section's technique of storing the fork return value to branch explicitly, because each fork site knows which PID is the new child's handle.
Visual intuition: draw the binary tree of depth three where each internal node is a fork point and each edge labels the child's fresh PID. The eight leaf labels are the eight printed PIDs 70–77. Takeaway: the count is also the leaf count of the fork tree.
14.6.4 Student Questions and Answers
Q: For the program with three forks, how many times will hello world print?
A: Four times was an initial guess. The correct count is eight, because each fork doubles the number of processes that reach the print. With , . The formula to remember is two to the power number-of-forks for total prints. The logic is that every alive process hits the next fork and splits, so 1→2→4→8, and all eight run the final print. Only one of the eight is the original parent, so the other seven are children.
Q: How many child processes were created in that run?
A: Seven. One of the eight is the original parent, so children equal . Do not answer a child-only question with . In the PID 70–77 trace, 70 is the ancestor and 71–77 are the seven children that were created across the three doubling levels.
Q: Does the interleaving of parent and child output depend on processor speed?
A: Yes, scheduling affects grouping. On a fast processor the parent may complete its whole slice and emit all its lines before the child starts, making output look grouped. On a slower processor, or with an explicit wait or sleep inserted, more interleaving between parent and child lines can appear. The experiment showed grouped prints on the available fast machine, with the note that adding wait or sleep changes the pattern. Without wait, no order is guaranteed — fork itself does not promise which process runs first (per R5_Chapter 8_Process Control.txt), so grouping is a speed and scheduler artifact, not a language rule.
Recap + Bridge: Sequential unguarded fork calls produce exponential growth — total processes reach the next statement and of them are children, each capable of forking further, visible as a PID tree like 70–77 for . To tame that exponential fan-out and decide who does what, code must capture each fork's return and branch — exactly the pattern of the next section.
Exam note: Master the two formulas with integer exponentiation and the minus-one distinction: total prints , children . Be ready to compute for (8 total, 7 children) and to explain why children also fork and why interleaving versus grouping depends on processor speed and on inserted wait/sleep.
14.7 Distinguishing Parent and Child in Code
14.7.1 Branching on the Return Value
Storing the return of fork in a variable such as pid makes the roles testable:
- if then an error occurred; no child was created and the error branch can exit with a non-zero status
- if then this branch is the child; a child block can be opened
- if then this branch is the parent; the positive value is the child's PID and a parent block can be opened
A common snippet pattern shown was:
- fork and save its result into pid
- print the returned pid value
- if pid less than zero, print an error and exit
- if pid equals zero, enter a block that prints "I am the child with PID ..." and then indicates the child is exiting
- if pid greater than zero, enter a block for the parent and, depending on the experiment, either wait for the child or not
The point of saving the positive PID in the parent is that later system calls such as waitpid can be given that specific child to wait for.
Canonical branch shape: This is the idiom taught across R5_Chapter 8_Process Control.txt (Figure 8.1 and the wait examples) and reproduced in every demo of this lecture. By saving fork's result, one source file describes two roles:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void) {
pid_t pid;
pid = fork();
printf("fork returned %ld\n", (long)pid); /* diagnostic line */
if (pid < 0) {
fprintf(stderr, "fork failed\n");
return 1;
}
if (pid == 0) {
/* child block */
printf("I am the child with PID %ld\n", (long)getpid());
printf("the child process is exiting\n");
return 0;
}
if (pid > 0) {
/* parent block — pid is the child's PID for waitpid */
printf("I am the parent waiting for the child to end\n");
/* wait(&status) or waitpid(pid, &status, 0) here */
}
return 0;
}
Why each arm matters:
- : no child exists; the single process must handle the error (often
perrororfprintfplus non-zero exit). Memory-pressure andCHILD_MAXare the two canonical causes listed inR5_Chapter 8_Process Control.txt. - : the zero tells this process "you are the child." The child's real PID is not zero —
getpid()will show a real value like 1692 — only theforkreturn is zero to distinguish the role without a separate query. - : positive holds the child's fresh PID, so the parent can name that exact child in
waitpid(pid, &status, 0)or inkill(pid, SIGUSR1)later. Without saving it, the parent would need to wait for any child rather than a specific one.
The printf of the raw fork return before branching is a teaching aid — it produces the characteristic fork returned 1692 versus fork returned 0 lines that make the two-world return visible.
Scope: This if chain works when each role should run once after the fork. In the exponential section 14.6 no branch was used, so every process kept forking; here branches prune the tree so controlled parent and child work can follow.
Pitfalls:
- Swapping the signs —
pid > 0is parent, not child; many first attempts reverse them and put parent work in the child. - Forgetting
longcast for%ldwhen printingpid_t—pid_tis an integer type whose width varies; casting tolongavoids truncation warnings. - Testing only
if (pid == 0) else— then the error case with –1 falls into the else and is treated as parent; always test< 0first.
Visual intuition: draw a diamond after fork with three outgoing arrows labeled –1 (error), 0 (child box), and +N (parent box holding the child's tag). Takeaway: one call, three labeled exits, immediate pruning of who executes what.
14.7.2 Worked Output with 1691 and 1692
A demonstration program followed that pattern, with the wait call initially commented out. Its run produced lines including "I am 1691" and "fork returned 1692" and also "fork returned 0". The reading given was:
- "I am 1691" came from the statement before or just after the fork that printed the current process's identity; 1691 identified the parent.
- "fork returned 1692" is the parent view: the positive return is the child's PID, 1692.
- "fork returned 0" is the child view: zero marks the child.
- Additional messages were "I am the child with PID 1692" and "the child process is exiting" from the child branch, and from the parent branch a line such as "I am the parent waiting for the child to end" when waiting was enabled.
The trace emphasized that after fork every statement beyond that point runs twice: once in the parent context where fork returned a positive PID, and once in the child context where it returned zero. That is why two "fork returned" lines appear when creation succeeds, versus a single error line when it fails.
Reading the 1691/1692 console line by line:
Suppose the program starts as PID 1691 (the parent) and the shell is PID 1400. The sequence without waiting was:
I am 1691— printed either beforefork(so only parent prints it) or just afterforkfrom a line outside theifbranches that both would share; in either reading the number matches the parent's PID fromgetpid().fork returned 1692— printed from the post-fork diagnosticprintfin the process that saw positive return — the parent 1691 reports that its child's PID is 1692.fork returned 0— same diagnostic line now in the child 1692, reporting thatforktold this process "you are the child."I am the child with PID 1692— insideif (pid == 0), the child confirms its identity withgetpid()showing 1692 (not 0).the child process is exiting— child's final line beforereturn 0(orexit(0)) in its branch.- If parent waiting was enabled, a line such as
I am the parent waiting for the child to endappears in parent 1691 before it blocks, described in section 14.9.
Why two fork returned lines versus one on error: success creates two address spaces that both reach the diagnostic print, each with a distinct mapping of pid; failure creates only one address space with pid = -1, so the diagnostic runs once and the error arm handles it.
Visual intuition: print two side-by-side timelines: Left timeline (PID 1691, parent) shows fork → 1692 → parent branch; Right timeline (PID 1692, child) shows fork → 0 → child branch. Connect the fork moment with a horizontal clone line from left to right. Takeaway: one source line becomes two printed confirmations.
14.7.3 Demonstration Variants
The same source was compiled and run multiple times under GCC, with directory listings used to confirm that programs such as hello were built. The focus remained on the printed PID values and the branch messages, not on C syntax details.
Variants that were built but not belabored:
- With the
waitline commented out: parent exits promptly; child prints after the shell prompt (orphan pattern), demonstrating the shell-prompt interleaving already seen in section 14.5 but now inside the branching idiom. - With
waitrestored (covered fully in section 14.9): parent blocks atwait/waitpiduntil the child'sthe child process is exitingstatus is collected, so the prompt no longer splits the output. - Recompiles used
gcc -o pid_demo pid_demo.candls -lchecks to verify thatpid_demoappeared freshly each time, since a stale binary would hide source changes to the diagnostic prints.
Recap + Bridge: Saving pid = fork() and testing pid < 0, pid == 0, and pid > 0 converts one split call into controlled parent and child roles — with the diagnostic contrast fork returned 1692 versus fork returned 0 making the split visible and the positive parent-side value becoming the handle for later waitpid and kill. When that handle is not waited for, the system must deal with orphans and zombies, which is exactly the next section's focus.
14.8 Orphan and Zombie Processes
14.8.1 Orphan — Child Outlives Its Parent
An orphan process was described as a child that keeps running after its parent has already finished and terminated. In the demonstration without a wait, the parent printed, returned, and the shell gave back its prompt while the child was still starting and then printing afterward. Because the parent was done, the child had no parent to return to. The human analogy used was simple: an orphan has no parent.
Orphan — definition and how it arises: An orphan process is a live child whose parent has terminated before the child itself finished. In the lecture's demonstration without a wait, the sequence is:
- Parent (
PID1691) callsfork→ child (PID1692) created. - Parent reaches
return 0(orexit(0)) without callingwait; kernel moves parent to Terminated and returns control to the shell, which prints its prompt. - Child 1692, still building or still at
printf, now has a dead parent. It continues running — it was never killed when the parent died — but the per-process parent pointer (PPID) is now stale.
In modern Unix terminology, that child is said to have no parent in the ordinary sense; it needs a new one to collect its final status, discussed in the adopter subsection. The condition is not "parent killed the child" — the parent simply exited first; the child outlived it.
Trace — the orphan pattern from section 14.9's no-wait run:
PID1691 before fork printsI am 1691.- After
fork: 1691 printsfork returned 1692(parent view) and 1692 printsfork returned 0(child view). - Parent 1691 hits the end of
main, kernel reclaims its resources, shell printsuser@vm:~\$. - Child 1692, delayed by clone setup, now executes
I am the child with PID 1692and lands that line after the shell prompt, exactly the interleaving seen in section 14.5 but now with branch messages.
So the observed prompt before child print is also an orphan symptom in this variant — the prompt belongs to the shell that got control back from the terminated parent while the child is still scheduled.
Scope: An orphan is not harmful by itself — the system adopts it and will reap it. The harm case is when many parents create many children and none wait, so adoption plus collection is delayed and the system holds many termination records.
14.8.2 Zombie — Child Waits for a Busy Parent
A zombie process was described as the opposite asymmetry. Here the parent is very busy — not lazy but occupied — and the child finishes its job and wants to return its exit status. If the parent is too occupied to collect that status, the child remains in a zombie state: it is done but cannot be fully reaped because nobody is taking it back. The wording used was "it does not know what to do because nobody is taking it" and "a parent so busy it cannot accept the return status."
Zombie — definition and why it lingers: A zombie process (listed as Z in ps, per R5_Chapter 8_Process Control.txt Section 8.5) is a process that has called exit (or returned from main) but whose parent has not yet called wait or waitpid to collect the termination status. Formally:
- The child's data, heap, stack, and open files are already freed — the kernel has closed descriptors and released memory.
- A small residue remains in the process table:
PID, termination status (exit code or signal number plus macrosWIFEXITED/WEXITSTATUS/WIFSIGNALED/WTERMSIG), and accounting times. This residue exists so the parent can later ask "how did my child end?"
The parent being "too busy" is the lecture's plain phrasing for R5_Chapter 8_Process Control.txt Section 8.5 exit Functions: "if the child terminated normally, the parent can obtain the exit status of the child" via wait — but the kernel keeps that status until someone asks. Until wait runs, the child is essentially a named envelope holding its exit code, waiting on a desk. If many children become zombies at once, the process table fills, and new fork calls can fail with "no more processes" even though no live computation is running.
Contrast with orphan: an orphan is Running or Ready while its parent is gone; a zombie is Terminated in user terms but still has a process-table entry because the parent is alive but has not collected it.
Tiny zombie timeline:
- Parent 1700 forks child 1701.
- Child 1701 does a short job:
printf("work done\n"); exit(5);— kernel moves 1701 to zombie with exit status 5. - Parent 1700 stays in a long loop
for (;;) { /* busy */ }and never callswait. Askingps -o pid,ppid,stat,commnow shows:
PID PPID STAT COMMAND
1700 1200 S parent_busy
1701 1700 Z child [defunct]
Z and [defunct] are the zombie markers. Only when 1700 finally calls wait(&status) and reads WIFEXITED(status) → WEXITSTATUS(status)=5 does 1701 leave the table.
Pitfalls:
- Thinking a zombie consumes CPU or memory like a normal process — it holds only a small kernel record, not the full image, but that record still counts against the process-table limit.
- Confusing the two names — orphan = child alive, parent gone; zombie = child dead, parent has not yet collected. The words map to the human story: orphan has no living parent, zombie is a body that has not been taken.
14.8.3 The Init Process as Adopter
Orphan processes do not remain unowned. They are taken over by the very first process of the Unix or Linux system, often described as the init process. That process collects such orphans and handles their final cleanup. This adoption was presented as the system's answer to children whose parents have already exited.
Init adoption and reaping: As stated in R5_Chapter 8_Process Control.txt Section 8.5:
PID0 is usually the scheduler/swapper (a kernel process with no program on disk).PID1 isinit(older path/etc/init, newer/sbin/init, on macOS 10.4 replaced bylaunchd; on many current Linux systemssystemdplays theinitrole) — a normal user process with superuser privileges that never dies and is started at the end of bootstrap.
Whenever a parent terminates, the kernel walks the process table and re-parents every live child whose PPID matched the dying process, changing that child's PPID to 1. The child is now "adopted by init." When that adopted child finally exits, init's own wait loop collects the termination status, so the orphan never becomes a long-lived zombie under init — init is written to call wait repeatedly for exactly this reason. The same double-fork trick in Figure 8.8 of the companion docs shows how a process can deliberately orphan a grandchild so that init (grandparent) becomes its adopter and no zombie is left behind by the original parent that already waited for the intermediate child.
Check after adoption: getppid() in the orphan that was once 1700 will now return 1 after the re-parenting, and ps will show PPID 1.
Real-world: services that fork per request must decide whether to wait for helpers or let a reaper handle them; forgetting wait leads to orphans or zombies.
Visual intuition: picture a family tree where the parent node disappears. Its child nodes are not deleted — dashed adoption arrows reconnect them to the root node labeled 1/init at the top. Leaves that have already finished (zombies) are removed when the root's wait prunes them; running orphans remain as live adopted leaves.
Recap + Bridge: An orphan is a live child whose parent has already left; a zombie is a dead child whose exit envelope the busy parent has not yet opened — opposite asymmetries with the same remedy: collection. init (PID 1) adopts orphans so they have someone to report to, and an explicit wait/waitpid in the true parent prevents zombies in the first place.
Exam note: State the adoption rule — orphans are inherited by the system's first process (PID 1, described as init), which then reaps them — and contrast orphans (alive, parent gone) with zombies (dead, parent has not collected).
14.9 Wait and Waitpid — Synchronizing Parent and Child
14.9.1 Purpose and Variants
The wait family exists so a parent can pause until its children finish. Two forms were discussed:
- a simple wait with a null parameter, which means "wait for my own child" — any child of the caller
- a more specific waitpid that takes a PID and a status location, which means "wait for that particular child"
A parent may have created multiple children. With the specific form it can name which child it is waiting for by passing that child's PID. The status parameter is where the child's exit information is collected.
Purpose — preventing zombies and controlling order: When a child calls exit, the kernel holds a small termination record (PID, exit code or signal number, timing) until the parent fetches it. The wait family is the fetch operation, and it also provides synchronization — wait blocks the parent if no child has finished yet.
Signatures from R5_Chapter 8_Process Control.txt Section 8.6:
Returns: child PID on success, –1 on error, 0 in the special WNOHANG non-blocking case.
Semantics covered in the lecture:
wait(NULL)— block until any child of the caller terminates; discard the detailed status (passNULLwhen only synchronization is wanted).wait(&status)— block until any child terminates and store the encoded termination status throughstatlocfor later decoding withWIFEXITED/WEXITSTATUSetc.waitpid(pid, &status, 0)— block until the specific child whose PID equalspidterminates (whenpid > 0);pidis typically the positive value saved fromforkin the parent.pidmeanings include –1 (any child), 0 (any child in the same process group), and <–1 (a specific group), listed inR5_Chapter 8_Process Control.txt, but the lecture uses onlypid > 0for one named child.optionsletswaitpidavoid blocking (WNOHANG→ return 0 if no child is ready), watch stopped children (WUNTRACED), or report continued children (WCONTINUED); the lecture uses0for plain blocking wait.
Analogy: wait is waiting at an arrival gate without a name on your sign — you take the first family member who appears. waitpid is waiting with a name tag for one specific arrival — you ignore other family until that person lands, and they hand you an envelope (status) about how their trip went.
Scope: wait(NULL) is the lecture shorthand; some historic spellings write it as "wait with a null parameter." Modern POSIX spelling waitpid(pid, NULL, 0) means wait for that pid but ignore the status details. waitid and wait3/wait4 exist for resource-usage reporting (see R5_Chapter 8_Process Control.txt Section 8.8) but were not used in this lecture's demos.
Visual intuition: draw the parent timeline with a bold bar labeled wait/waitpid blocking the parent line. The child's timeline runs concurrently, ends with exit(status), and a dotted arrow labeled SIGCHLD notifies the parent; the wait bar then unblocks and the parent continues. Takeaway: without the bar the parent would race ahead and leave an orphan; with it they rendezvous.
14.9.2 Demonstration — Without Wait Versus With Wait
The same PID-branching program was run twice to contrast the two behaviors.
- Without wait, i.e., with the wait line commented out: the parent printed first, returned, the shell prompt appeared, then the child's lines appeared after the prompt. The interpretation was that an orphan pattern had been created: the parent did not wait, so the child ran without a waiting parent.
- With wait restored and with a message "I am the parent waiting for the child to end": the parent printed its identity line "I am 1771", printed "fork returned 1722" as the child's PID, printed "fork returned 0" from the child's perspective, printed that it was waiting, and then paused. The child then printed "fork returned 0", "I am the child with PID ..." and "the child process is exiting." Once the child exited, its return status was collected by the parent, and only then did the parent exit and print a final confirmation. No orphan was created in this version.
The displayed sequence for the waiting run was summarized as: parent identity, parent view of fork, child view of fork, parent waiting message, child executing and exiting, parent collecting status and exiting. This was presented as the direct answer to the question "why use wait at all."
Side-by-side traces with PIDs 1771 (parent) and 1722 (child):
Without wait — // wait(&status); commented out:
I am 1771
fork returned 1722 /* parent view — positive child PID */
fork returned 0 /* child view — zero */
I am the child with PID 1722
user@vm:~\$ /* shell prompt returns because parent 1771 did not block */
the child process is exiting /* child 1722 prints after prompt — orphan pattern */
Interpretation: parent 1771 finished its slice quickly, kernel reclaimed it, shell printed the prompt. Child 1722 was still in setup and printed late; PPID of 1722 was re-parented to 1 after 1771 exited, so no zombie was left but an orphan window was visible.
With wait — wait(&status); active and a waiting message added:
I am 1771
fork returned 1722 /* parent view */
fork returned 0 /* child view */
I am the parent waiting for the child to end /* parent before blocking */
fork returned 0 /* child re-entry if diagnostic before branch; duplicates seen in demo */
I am the child with PID 1722 /* child branch */
the child process is exiting /* child calls exit */
[parent wait returns, status collected] /* kernel delivers SIGCHLD + status */
parent: child I waited for has returned (wait == 1722) /* optional check */
parent completing its job
No shell prompt splits the child output, and ps during the wait would show 1722 in S (sleeping, not Z) because the parent is ready to reap. Once wait returns, 1722's table entry is freed immediately.
The summary sequence the lecture repeated — parent identity → parent view → child view → parent waiting → child executing and exiting → parent collecting and exiting — is exactly the rendezvous pattern.
Pitfalls:
- Commenting out
waitand then assuming the child was "lost" because it printed late — it was an orphan adopted byinit, not lost; its output was just delayed past the shell prompt. - Assuming
waitreaps a specific child by magic — plainwait(NULL)reaps any child; to name one,waitpid(pid, &status, 0)with the saved positive PID must be used.
14.9.3 Waiting for a Specific Child and Status Handling
When waitpid is given a specific PID, that PID is the positive value returned by fork in the parent. A status variable is supplied by address so the system can fill it with the child's exit code. A check that compares the return value of waitpid with the PID can confirm that "the child I waited for is the one that came back." Passing null for the status was described as the choice when the caller does not need the detailed exit reason and just wants the synchronization.
Status handling pattern: The lecture's line for the specific wait is:
pid_t pid = fork();
int status;
...
if (pid > 0) {
printf("Parent waiting for child %ld\n", (long)pid);
if (waitpid(pid, &status, 0) != pid)
err_sys("waitpid error");
/* optionally decode status */
if (WIFEXITED(status))
printf("child exited normally, exit = %d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("child killed by signal %d\n", WTERMSIG(status));
}
&statusis the out-parameter — the kernel writes the encoded termination status there, which the macros inR5_Chapter 8_Process Control.txtFigure 8.4 decode. Without&, the kernel would have nowhere to place the bits (null would be legal only when the code does not care about the reason).- Checking
waitpid(...) != pidconfirms the reaped child is indeed the one requested; with plainwaitthe check iswait(&status) != pidafter saving the child's PID separately, as shown in Figure 8.6 of the companion docs. - Passing
NULLforstatlocdrops the exit-reason payload but still provides the blocking and reaping —wait(NULL)andwaitpid(pid, NULL, 0)both block until the named set of children reaches at least one termination; they just do not record the integer that tells normal versus signal exit. The demo usedNULLfor "wait for my own child" when only the rendezvous mattered.
Real-world rule: a server that forks per request typically saves each child's PID and either waits for a specific child after a select timeout or installs a SIGCHLD handler that loops waitpid(-1, &status, WNOHANG) to reap whichever children have finished without blocking the dispatcher. Forgetting any reap path is how long-running services accumulate zombies.
Q: After adding wait, what do you expect the parent to do?
A: The parent will exit only after the child has finished its work. The wait call blocks the parent until the child's exit can be collected. The run confirmed it: the parent printed a waiting message, the child executed to completion, and then the parent resumed and ended. In the trace with PID 1771 and child 1722, the parent's waitpid(1722, &status, 0) stayed blocked while 1722 ran, the kernel delivered SIGCHLD on 1722's exit, waitpid returned 1722, and only then did 1771 reach its own return 0 / final print.
Recap + Bridge: The wait family converts a race into a rendezvous — wait(NULL) parks until any child is reapable, while waitpid(pid, &status, 0) parks until the specific child named by the saved fork return completes and deposits its encoded status. That rendezvous is what prevents orphans in the first demo case and zombies in the second.
Exam note: Be ready to contrast wait with a null parameter (wait for own child, any child) versus waitpid (specific PID in status, collection of exit information, null status when details are not needed), and to trace the 1771/1722 output to show why the no-wait run created an orphan window and the waited run did not.
14.10 The exec Family — Replacing the Child Image
14.10.1 The Idea — Doing Something Different from the Parent
By default a child starts with a copy of the parent program. Often that is not what is wanted. Recall the web server passing a client ID to a backend job: the child should run business logic that fetches from a database, not just loop listening for connections. Exec provides the deviation. After a fork, the child can invoke an exec call to replace its current program image with a new program. The parent code that was copied is discarded and the child begins executing the named program instead.
Hook: If fork already gives the child a full copy of the parent, why does a server child not just loop forever listening on the same port beside its parent?
Exec as deviation: exec does not create a new process — it reincarnates the same process (same PID, same parent, same open-file table) with a different program. From R5_Chapter 8_Process Control.txt Section 8.10:
forkcopies the parent image: text, data, heap, stack.execdiscards that copied image and loads a new program from disk into the same address space, starting at the newmain.
So the classic web-server sequence is fork → child's exec(backend, args, env) → backend runs with the child's PID but with entirely new code. Data such as the client's register number travels in the args/env vectors that exec carries across the image boundary, which is why the lecture material maps "register number and any other client data would travel in the argument and environment vectors."
The parent is untouched — it never calls exec itself, so it stays as the listener and later calls waitpid to collect the backend's exit status. Without exec, every child would duplicate the parent's accept loop and compete for the same listening socket.
Visual intuition: picture the child as a room initially furnished as a copy of the parent's room (the fork copy). exec empties that room completely — walls repainted, furniture swapped — and the same room number now houses the handler. The parent's room across the hall remains unchanged.
14.10.2 The Six Variants and the Used Form
Exec exists in six variants that differ in how arguments and environment are supplied and how the program is located. Details of all six were not pursued; one representative form was used. That form takes the name of the program to run plus two additional parameters: an argument vector and an environment vector. In the class example both were set to null, meaning no extra arguments or environment were forwarded. The example name used in the listing was "child" as the program the new image should run.
Six variants, one demonstration shape: R5_Chapter 8_Process Control.txt formalizes there are seven exec functions in the broader count (execl, execv, execle, execve, execlp, execvp, and fexecve in modern listings); many courses collapse to six by counting the core exec* family without fexecve. They differ on:
- How the program is found (
p→ searchPATH, otherwise explicit pathname). - How arguments are passed (
l→ list,v→ vector). - Whether the environment is inherited or replaced (
e→ explicitenvp).
The variant the lecture used matches the triple-argument pattern:
where the lecture material states the call took the program name plus two additional parameters — an argument vector and an environment vector — both set to NULL in the class example. Conceptually:
- First argument: pathname or program name of the new image (here the example
"child"standing for the compiled file namedchild). - Second: argument vector (
argv/NULLfor no extra args) — the slot that would carry a register number string like"reg=04277"in the web-server story. - Third: environment vector (
envp/NULLfor default) — the slot that would carry key–value pairs such as database credentials.
The companion docs emphasize that exec replaces the caller's text/data/heap/stack while keeping PID constant and keeping file descriptors open unless marked close-on-exec — which is why the child's connected socket remains usable in the new backend program until it explicitly closes it.
Scope: The lecture did not ask students to memorize all six spellings. The examinable point is the idea that exec replaces the child's copied parent image with a named program, and that the example name in the demo "child" must match a real executable on disk (see the compilation trace next). Some implementations list seven variants including fexecve; knowing that a family exists and that the demo used one triple-argument form with NULL args/env suffices.
14.10.3 Parent and Child Programs Walkthrough
Two separate source files were presented:
- The parent program: prints an opening line such as "Parent: hello world", calls fork, saves the result into a variable, tests whether the result is zero to recognize the child, and if it is the child, calls exec with the argument and environment both null and with program name child. The other branch — the parent branch where the return is positive — prints that it is waiting, then calls waitpid on the child's PID with a status location, compares the returned value with the saved PID, and prints a message that the child it waited for has returned before finally printing that the parent is completing its job.
- The child program: defines variables, runs loops that do calculations, then prints lines such as "I am the child. I have completed the work and I am quitting now." Its return status will travel back to the parent through the wait status.
The web-server mapping was repeated explicitly: the register number and any other client data would travel in the argument and environment vectors that exec carries into the new program; the name field names the backend program to run.
Concrete skeleton of the demo:
parent.c (the listener that forks and then waits):
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void) {
pid_t pid;
int status;
printf("Parent: hello world\n");
pid = fork();
if (pid == 0) {
/* child path — replace with handler */
execl("./child", "child", (char *)NULL); /* arg + env both NULL conceptually */
perror("exec failed");
return 1;
}
if (pid > 0) {
printf("Parent waiting for the child to complete\n");
if (waitpid(pid, &status, 0) == pid) {
printf("the child process I waited for has been written back\n");
}
printf("parent completing its own job\n");
}
if (pid < 0) {
perror("fork failed");
}
return 0;
}
child.c (the backend handler that becomes the child image):
#include <stdio.h>
int main(void) {
int i, sum = 0;
printf("hello world\n"); /* same greeting but from the new image */
for (i = 0; i < 100; i++) sum += i;
printf("I am the child. I have completed the work and I am quitting now.\n");
printf("Work completed. Bye for now.\n");
return 0;
}
Flow of control: Parent: hello world always prints in PID of the original parent. After fork, the test pid == 0 is true only in the child's address space, so the child reaches execl and, on success, never returns — the child's next instruction is the first line of child.c's main. The parent's pid > 0 arm reaches waitpid(pid, &status, 0) with the saved positive child handle and blocks. The loops and sums in the child stand in for the lecture's database lookup and response formatting; the printf lines inside child.c are what the parent never executes.
Pitfalls:
- Executing the wrong branch —
execin the parent would replace the listener and the server could never accept the next connection. - Forgetting that
execon success does not return — any code afterexecin the child branch is error handling (perror) for the case where the named program cannot be found. - Mismatching the name string and the file on disk — the string
"child"must be the actual executable name produced by the compiler or the lookup fails withNo such file or directory.
Visual intuition: draw the child's control-flow arrow reaching exec where it vanishes and reappears at the top of a second flowchart labeled CHILD PROGRAM, while the parent's arrow arcs around exec to reach waitpid. Takeaway: same PID, different program, parent patiently blocked.
14.10.4 Compilation and Execution Trace
Both files must be built before the joint run. The steps shown were: compile parent.c into an executable named parent, compile child.c into an executable named child, ensure the child executable name matches the name string used in the parent's exec call, otherwise the lookup will fail, and then run the parent executable.
The joint run was traced in order:
- Parent starts and prints "Parent: hello world."
- Parent reaches fork, creates the child, saves its PID.
- Parent branch recognizes a positive PID and prints "Parent waiting for the child to complete" and blocks in waitpid.
- Child branch recognizes zero, calls exec, is replaced by the child program image, and starts running that program.
- Child program prints "hello world" from its own code, runs its calculations, prints completion lines including "Work completed. Bye for now."
- Child quits; its exit status is delivered to the waiting parent.
- Parent resumes, prints that the child process it waited for has been written back, and prints that it is now completing its own job.
Build and run with GCC — exact steps:
\$ gcc -Wall -o parent parent.c # create executable 'parent'
\$ gcc -Wall -o child child.c # create executable 'child' — must match execl's "./child"
\$ ls -l
-rwxr-xr-x 1 user user ... parent
-rwxr-xr-x 1 user user ... child
-rw-r--r-- 1 user user ... parent.c
-rw-r--r-- 1 user user ... child.c
\$ ./parent
Parent: hello world
Parent waiting for the child to complete
hello world
I am the child. I have completed the work and I am quitting now.
Work completed. Bye for now.
the child process I waited for has been written back
parent completing its own job
\$ echo \$?
0
Why ls matters: it confirms both parent and child executables exist before ./parent runs — if child is missing, the child's execl fails, perror("exec failed") prints No such file or directory, that child then exits, and the parent's waitpid still collects its non-zero exit, but the handler's business logic never ran. The name-match rule is filesystem-level: the kernel executes whatever pathname the first exec argument names; "child" with no slash searches only if the p-variant is used, while "./child" names the file in the current directory explicitly. The demo used a non-p form plus the explicit match, so child in the source and child on disk must agree.
14.10.5 Student Questions and Answers
Q: Does the trace make sense in terms of the earlier web server story?
A: Yes. The parent listening process forks per request, the child replaces its generic copy with request-specific business logic via exec, that logic runs using the client data passed as arguments, and only after the child returns does the parent consider that request done. The demo uses simple hello and work messages to stand in for the database lookup and response formatting. In a real server the execl argument would be something like execl("./results_handler", "results_handler", reg_str, (char*)NULL) where reg_str carries the register number from the HTTP request, and the child's calculations would be the database fetch that builds the personalized results page before the response is sent and the child exits.
Recap + Bridge: fork copies; exec replaces the copy with a genuinely different program while keeping the same process identity, so a dispatcher parent plus waitpid plus a handler child becomes the classic per-request pattern. That helper pattern relied on synchronous rendezvous — next we need the asynchronous side: events that arrive at any time and must interrupt what a process is doing, which are signals.
Exam note: Be ready to sketch the parent source shape — fork, branch on zero, exec in child, waitpid in parent — and to trace the printed lines in order, including why the child's first line appears after the parent's waiting line and why both files must be compiled to matched names before the joint run.
14.11 Signals — Asynchronous Events
14.11.1 What a Signal Is and Why It Matters
A signal was defined simply as an event. When the event occurs it notifies a process, or a thread within it, that something important has happened. After receiving a signal the process must take some action based on what was received. It does not act instantly in the middle of an instruction; it pauses what it was doing and then handles the signal. Signals were framed as a core part of inter-process communication.
Signal — the kernel's tap on the shoulder: A signal is a small integer event ID sent by the kernel or by another process to say "something needs your attention now." The companion docs R5_Chapter 10_Signals.txt formalize signals as the asynchronous notification path — the fact that a child terminated happens at an arbitrary time is delivered as SIGCHLD; pressing Ctrl+C becomes SIGINT; a process can send SIGUSR1 to another with kill.
Key properties:
- Asynchronous: the signal can arrive at any moment in the process's execution, not only when the process checks for it.
- Numbered: each signal has a macro name (
SIGINT,SIGTERM,SIGUSR1,SIGUSR2,SIGCHLD, etc.) and an integer value defined in<signal.h>(for exampleSIGABRTmaps to 6 andSIGFPEto 8 on the platforms discussed inR5_Chapter 8_Process Control.txtFigure 8.6 discussion). - Handled, not executed inside the current instruction: the kernel waits for an instruction boundary, saves the interrupted context, runs the handler, then returns to where it left off — the process "pauses what it was doing and handles it" as the lecture phrasing puts it.
- Inter-process communication flavor: signals carry very little payload (just the signal number and, with
sigqueue/siginfo, a bit more), but they are the lightest-weight way for one process to say "stop," "continue," or "wake up" to another without shared memory or pipes.
Analogy: a signal is the office intercom ping — a short chime with a code ("code 2 = fire drill, code 7 = phone call"). You hear it whenever it happens, you set down what you were typing after finishing the current keystroke, you handle the drill or the call, then you resume typing exactly where you paused.
Scope: Signals are not streaming data — they do not carry a buffer or a message batch. If richer data must move between processes the lecture's earlier fork/exec delegation or the later channels (pipes, sockets from R5_Chapter 15_Interprocess Communication.txt) are the right tool. Signals are the "something happened" tap, with the details handled in code the receiver already runs.
Visual intuition: draw a straight time arrow for a process executing instructions as ticks. At a random tick a vertical arrow labeled SIGINT drops onto the timeline; the process line bends down into a handler box, then bends back to the same point on the straight arrow and continues. Takeaway: the signal interrupts the flow but does not rewrite it.
14.11.2 Inter-Process Communication Context
Two contexts were linked. In the earlier exec example the parent and child were two separate programs coordinating: the parent launched the child and waited. In a single program with a fork, the parent and child are two processes sharing the same original program space. Both are cases of inter-process communication — multiple processes that need coordination. Signals offer a light-weight way to notify between them.
Where signals fit among IPC choices: Both demos — fork plus exec with waitpid (two different programs cooperating) and plain fork with printf branching (two processes inside one original program) — need the same primitive: one process must tell another "your turn" or "I'm done." Options form a spectrum:
| Channel | What moves | Cost / timing |
|---|---|---|
Signals (kill/raise/pause) |
One integer event ID | Lightest, asynchronous, arrives at any time |
Pipes / FIFOs (R5_Chapter 15) |
Byte stream | Heavier, requires buffer and open descriptors |
| Shared memory + semaphores | Full data structures | Fastest for bulk data but needs synchronization |
Signals cover the notification half; when signal handlers coordinate with wait/pause/sleep they become the TELL/WAIT primitives described in R5_Chapter 8_Process Control.txt Section 8.9 Race Conditions (and Section 10.16's signal-based TELL_PARENT/WAIT_PARENT). The lecture introduces signals first as notification before introducing heavier pipes, because a single kill(childPID, SIGUSR1) can wake a pause()-blocked peer with no buffer to manage.
Real-world: a shell that forks a foreground job sleep 100, then forwards its own SIGINT (Ctrl+C) to that job via kill, is using signals as IPC so the foreground job, not the shell, actually dies from the interrupt.
14.11.3 Everyday Example — Infinite Loop and Ctrl+C
A plain example was an allocation program that contains a logic error and enters an infinite loop. Left alone it would exhaust memory and cause problems. The way to stop it interactively is the key sequence Ctrl + C. That sequence generates a signal — an event sent to the program — and the program is expected to react by stopping. The point was that without an explicit notification path, a runaway loop would have no clean way to be told to exit; signals supply that path.
Why an infinite loop needs an outside event:
#include <stdlib.h>
int main(void) {
while (1) {
malloc(1024*1024); /* logic error — never freed, never tested */
/* ... eventually exhausts memory */
}
return 0;
}
On a terminal, the terminal driver translates the keystrokes Ctrl+C into the signal SIGINT and delivers it to the foreground process group. That SIGINT is the "event" the lecture defined — it arrives asynchronously while the loop is spinning inside malloc, the kernel parks the process's current instruction, looks up the disposition for SIGINT, and by default terminates the loop — freeing no further memory. Without such a path the operator would need to power-off or open another terminal to kill the process; with signals, one keystroke is the pre-built escape hatch.
The same mechanism applies to any of the 31 standard signals: the kernel generates SIGCHLD when a child exits, hardware generates SIGSEGV on an invalid address, SIGFPE on divide-by-zero (signal 8 in the lecture's Figure 8.6 note), SIGABRT on abort() (signal 6) — all numbers defined in <signal.h>.
Recap + Bridge: A signal is an asynchronous numbered event that tells a process to pause its current work and handle something important — the kernel-level primitive for notification between processes, presented here as the lightest-weight branch of inter-process communication. The infinite-loop plus Ctrl+C story shows why that primitive must exist in every system: a runaway job needs an interrupt that does not require the job to check for it. The next section shows the code that registers what happens when that interrupt arrives.
Exam note: Define a signal as an event that notifies a process or a thread that something important happened, note that the process pauses its current work to handle it, and place signals as a core inter-process communication mechanism.
14.12 Signal Handling Mechanics
14.12.1 Registration — Header and the signal Call
To handle a signal a program must include the signal.h header (signal.h), which defines the available signal numbers. It must register a handler before the signal matters. Registration uses the signal system call with two pieces of information: which signal to watch for and which function to call when it arrives. A signal number such as SIGINT — the interrupt that Ctrl + C generates — was used. A handler name such as sig_handler, defined by the programmer, was passed as the second argument. The naming is free as long as the name registered and the name defined match; calling it SIG_hand or sig_handler is a choice, not a fixed spelling.
Registration machinery: Handling starts with <signal.h> so macros like SIGINT, SIGUSR1, and the constant SIG_DFL are visible, then a call that wires a signal to a function:
In plain terms (from R5_Chapter 10_Signals.txt style description):
- First argument: signal number to watch —
SIGINTin the lecture's demo (theCtrl+Cinterrupt). - Second argument: pointer to a handler function the programmer wrote, e.g.,
sig_handler/SIG_hand— any legal C identifier, so long as the definition and the registration spelling agree; the name has no magic. - Return value: the previous disposition for that signal (a function pointer or
SIG_DFL/SIG_IGN) if needed for saving and restoring.
Declaration shape of the handler itself:
#include <signal.h>
void sig_handler(int sig) { /* sig is the signal number, here SIGINT */
/* work that is safe in a handler */
}
int main(void) {
signal(SIGINT, sig_handler); /* arm before the loop matters */
/* loop that can be interrupted */
}
The lecture's phrasing "register a handler before the signal matters" matches the companion docs' warning in R5_Chapter 10_Signals.txt that a signal arriving before registration uses the default disposition (for SIGINT, terminate) rather than any custom code.
Scope: signal is the historic simplified interface. Modern POSIX code often prefers sigaction for reliable semantics across flavors, but the lecture deliberately uses signal as the teaching entry point with SIGINT and a named handler to keep the flow explicit.
14.12.2 Handler Function and the Infinite Loop Demo
The demo program had two parts. A handler function defined what to do on a SIGINT: in the example it printed a line such as "Inside handler function" and executed a call that re-registered the signal for its default behavior. The main program registered the handler once at startup, then entered an infinite loop with no exit condition, incrementing a counter and printing a line such as "I am in the main function: infinite loop" once per second with a one-second delay.
Running the program showed the loop printing each second. The first time Ctrl + C was pressed, control transferred into the handler. The handler printed its inside message, called the registration that asks for default handling onward, and then returned to the main loop, which continued printing.
Complete program the lecture traced:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sig_handler(int sig) {
printf("Inside handler function\n");
signal(SIGINT, SIG_DFL); /* next SIGINT uses default termination */
}
int main(void) {
signal(SIGINT, sig_handler);
while (1) {
printf("I am in the main function: infinite loop\n");
sleep(1);
}
return 0;
}
Step-by-step at runtime:
maincallssignal(SIGINT, sig_handler)— kernel records thatSIGINTfor this process now means "callsig_handler" rather than "terminate."mainenterswhile (1)— each secondprintfthensleep(1)runs; the process is interruptible insidesleep.- First
Ctrl+C→ terminal driver sendsSIGINTto the foreground process. Kernel parkssleep, looks up the disposition, invokessig_handler. - Inside handler:
printf("Inside handler function\n")runs — visible as a single line interrupting the stream.signal(SIGINT, SIG_DFL)runs —SIG_DFLis the macro for "restore the default disposition" (default action forSIGINTis process termination). Handler then returns. - Return from handler: kernel resumes the interrupted
sleep/printfloop. The next iterationprintf("I am in the main function: infinite loop\n")appears, proving the process survived the firstSIGINTand looped onward.
This is exactly the desk-and-intercom picture: the worker hears the chime, steps to the side, reads the custom memo, files a note that the next chime means the building default, then sits back at the desk and resumes typing.
14.12.3 Default Action, Re-registration to SIG_DFL, and the Two-Ctrl+C Behavior
The two-press behavior was then explained. The first press does not kill the program in this demo. The default action for SIGINT is to end the program, but the program initially overrides that by registering its own handler. So the first SIGINT is caught and handled by the custom function. Inside that handler the line with SIG_DFL — default — re-registers the signal to mean "use the default function from now on." The abbreviation DFL was expanded as default. Because of that re-registration, the second press of Ctrl + C now triggers the system's built-in action and the program stops immediately. The contrast was stated as: first Ctrl + C handled to print and switch to default, second Ctrl + C executes the default termination.
Why two Ctrl+C presses behave differently:
- After
signal(SIGINT, sig_handler)but before the handler re-arms, the disposition is custom: firstSIGINT→sig_handler(prints, thensignal(SIGINT, SIG_DFL)).SIG_DFLhere is not a function the user wrote — it is the constant the kernel defines to mean "do built-in action." ForSIGINTthat built-in action is process termination. - After the handler's
signal(SIGINT, SIG_DFL)has run, the disposition is default: secondSIGINT→ kernel's default termination path — no handler is invoked, the process dies right away, and the kernel records that death as signal-induced with a non-zero exit status (130 in the lecture, covered next).
The historic signal semantics on some systems auto-reset to SIG_DFL after the first delivery; the demo makes the reset explicit inside the handler so the two-press pattern is portable and visible in the code rather than hidden in a compatibility quirk. R5_Chapter 10_Signals.txt discussion of unreliable versus reliable signals notes this auto-reset behavior as a portability trap, which is why the lecture spells out the re-registration rather than relying on it.
Visual intuition: picture the SIGINT disposition as a switch with two positions: CUSTOM (points to sig_handler) and DEFAULT (points to a trapdoor). Initially set to CUSTOM, the first press flips the switch to DEFAULT and briefly runs the custom box; the second press hits the DEFAULT side and the trapdoor opens.
Pitfalls:
- Assuming the handler runs on the second
Ctrl+C— it does not;SIG_DFLreplaced it. - Forgetting that the handler name must match the registered name —
SIG_handversussig_handlermatters only as a symbol; the kernel never supplies a name by magic, it calls whatever pointer was registered.
14.12.4 Exit Status — Normal Zero Versus 130
The shell's record of how a program ended was shown with the special shell variable that holds the exit status of the last command. A normally ended program such as a plain hello run left that variable at zero, meaning normal termination. The signal-handler program left it at 130 after being ended with SIGINT, meaning it did not end normally but was interrupted. The value 130 was shown twice: once after a normal hello run to show zero, once after killing via Ctrl + C to show 130, and again after a full repeat of the handler demo. The difference was summarized as zero for normal return, non-zero such as 130 when a signal caused the end. The spoken phrase "dollar question" was a reference to that shell variable.
Exit status — what the shell remembers:
- Unix convention:
0means normal termination viaexit(0)/return 0; any non-zero means something else — error exit (exit(1)) or signal termination.R5_Chapter 8_Process Control.txtformalizes this asWIFEXITED/WEXITSTATUSversusWIFSIGNALED/WTERMSIG. - Shell observation variable: the shell stores the last command's termination summary as a numeric variable; in
bashthis is\$?, read withecho \$?orecho \$statusintcsh. The lecture's spoken "dollar question" is the spoken rendering of the student hearing "dollar question mark." - The demo numbers:
./hello ; echo \$?→0immediately after a normal return../handler_demo(firstCtrl+Ccaught) → loop continues, still not exited, so\$?is not yet updated../handler_demothen secondCtrl+C→ program terminates bySIGINT, shell shows\$?as130. On many shells this130is128 + signal_number(SIGINT)whereSIGINTis 2, so128+2=130.
So 0 and 130 are two concrete takeaways: 0 after the hello run versus 130 after the two-press SIGINT termination, and the shell variable is the portable way scripts test whether a process succeeded on its own or was stopped by a signal.
Replicating the two \$? checks:
- Normal:
./hellocompletes, prompt returns,echo \$?prints0—WIFEXITEDwith status 0. - Interrupted: start
./handler_loop(the handler demo), let two lines ofI am in the main function: infinite loopappear. PressCtrl+Conce → seeInside handler function, loop resumes. PressCtrl+Cagain → silent immediate exit. Thenecho \$?prints130—WIFSIGNALEDwithWTERMSIG2 on this shell build. A normalabort()instead would show a different non-zero plus a core annotation in theR5style dump.
Scripts use this: cmd; if [ \$? -ne 0 ]; then ... distinguishes success from a signal death; the 130 is the shell's shorthand for "killed by SIGINT."
Real-world: diagnosing whether a process ended on its own or was killed, using exit codes in scripts.
Recap + Bridge: Ctrl+C is SIGINT, survival hinged on signal(SIGINT, sig_handler) plus the handler's signal(SIGINT, SIG_DFL) switch that makes the first Ctrl+C a caught detour and the second a default termination with exit status 130 versus normal 0. That disposition machinery is the base for the programmatic signals next — raise and kill.
Exam note: Know that Ctrl+C corresponds to SIGINT, that a handler must be registered with signal using a signal number and a handler name, and that SIG_DFL restores default handling, giving the first-CtrlC-caught-then-second-CtrlC-terminates pattern and that normal exit leaves the shell status at 0 while a SIGINT death leaves it at about 130.
14.13 Raising and Sending Signals — raise and kill
14.13.1 Signatures and Semantics
Both functions that send signals are declared in the signal.h header (signal.h). Their signatures were contrasted side by side:
- raise takes a single parameter, a signal number such as SIGUSR1. It sends that signal to the calling process itself. The phrasing used was "to itself." On success it returns zero; on failure it returns a non-zero value. The outcome is binary and local.
- kill takes two parameters, a PID and a signal number. It sends the signal to the process, or to the group of processes, named by that PID. The PID field names the target. So kill can address "another process" by name. Success and failure reporting follows the same zero versus non-zero pattern.
A common mix-up of the name "race" for "raise" was present in the spoken track; the intended call is raise. Similarly, "signal user one" refers to SIGUSR1, a user-defined signal number available for program-to-program messages.
Two ways to name the receiver: Both live in <signal.h> per R5_Chapter 10_Signals.txt style interface, with contrasting signatures:
raise(sig)— shorthand forkill(getpid(), sig). One argument: the signal number such asSIGUSR1from the lecture's minimal examples; the receiver is implicitly "self." Because the target is always local, there is no PID to name and no process-group arithmetic.kill(pid, sig)— two arguments: who (pid) plus what (sig). Thepidinterpretation matchesR5_Chapter 8_Process Control.txtSection 8.6 waitpid convention for sign ofpid: positivepid > 0means one process with that PID, while the companion docs' waitpid listing extends the idea to groups — but the lecture uses only the simple casekill(childPID, SIGUSR1)andkill(getpid(), SIGUSR1)for self. Despite the confusing name,killdoes not only terminate — it sends whichever signal number is named;SIGUSR1is a user-defined ping.
Both return 0 versus non-zero as a sanity oracle: 0 means the kernel accepted the delivery request, non-zero means the signal number was invalid or the named PID does not exist / permission denied.
SIGUSR1 and SIGUSR2 are two intentionally unassigned signals that applications can use as private channels — the lecture chose SIGUSR1 ("signal user one") as the tracing signal for raise/kill demos so its handler would be unmistakably the student-registered one rather than a shell default like SIGINT.
Visual intuition: draw raise as a U-turn arrow looping inside one process box back to its own handler, and kill as a straight arrow from a sender box to a target box labeled by PID. Both arrows are tagged with the same signal number SIGUSR1. Takeaway: same event ID travels — the difference is whether a PID must be named.
Pitfalls:
- Misspelling
raiseasrace— the spoken "race" in the spoken track is the intendedraise(one signal argument, self target). - Thinking
killimplies death —kill(pid, SIGUSR1)is a polite user-defined poke;kill(pid, SIGTERM)orkill(pid, SIGKILL)are the terminating ones. The lecture deliberately usedSIGUSR1to separate the mechanics from any default termination. - Mismatching the signal number between registration and sending — registering
SIGUSR1but callingraise(SIGINT)invokes the wrong disposition and silently does not reach the intended handler.
14.13.2 In-Process Signaling with raise
A minimal in-process example registered a handler for SIGUSR1, then inside main called raise with that same signal number. The order in output was: "Inside main function", then after raise the handler printed "Inside handler function", then control returned and the main printed its continuation line before exiting. The trace shows that raise does not jump to a different process; it causes the same process to handle the signal and then continue. A note was made that the number used in registration and the number passed to raise must match.
Trace — raise inside one process:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void h(int sig) { printf("Inside handler function\n"); }
int main(void) {
signal(SIGUSR1, h);
printf("Inside main function\n");
raise(SIGUSR1); /* same process, same SIGUSR1 */
printf("back in main after handler\n");
return 0;
}
Console order observed (single PID, no second process involved):
Inside main function— normalprintfbefore the raise.raise(SIGUSR1)— kernel deliversSIGUSR1toPIDofmain. Looks up disposition:h.Inside handler function— handler runs synchronously before the nextmainline.back in main after handler— handler returned; kernel restored context;mainresumed at the instruction afterraise.
The requirement that the number in signal(SIGUSR1, h) and the number in raise(SIGUSR1) match is the lecture's "what must match" exam point — mismatch means the kernel would dispatch to SIG_DFL or to a different handler and the expected Inside handler function never appears.
14.13.3 Cross-Process Signaling with kill and getpid
The counterpart replaces raise with kill while keeping the same handler. Inside main the program fetched its own PID with getpid, then called kill with that PID and SIGUSR1. Output order remained the same — main, handler, main — because the target happened to be itself. The difference is the interface: kill requires an explicit PID even when sending to self, whereas raise needs only the signal.
The point of showing the self-targeted kill was as a stepping stone. The same call shape is used when the target is genuinely another process; the PID can be any known value, such as a child's PID obtained from fork or a parent's PID obtained from getppid.
Trace — kill with self PID as a stand-in:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void h(int sig) { printf("Inside handler function\n"); }
int main(void) {
pid_t mypid;
signal(SIGUSR1, h);
printf("Inside main function\n");
mypid = getpid();
kill(mypid, SIGUSR1); /* explicit PID vs raise's implicit self */
printf("back in main after handler\n");
return 0;
}
Console order is identical to the raise trace because the target mypid equals getpid(). The value of the demo is structural: swapping mypid for any other live PID changes nothing about the call shape — later, kill(childPID, SIGUSR1) from the parent or kill(getppid(), SIGUSR1) from the child reuses this exact skeleton. getpid here returns the caller's own PID; getppid will return the parent's PID, which is how the child learns whom to answer.
The lecture flagged this stepping stone explicitly: keep the same SIGUSR1 handler, keep the same Inside main / Inside handler / back in main print skeleton, swap only raise(SIGUSR1) for kill(getpid(), SIGUSR1) — so students see that the kernel path differs only in PID lookup, not in handler dispatch.
Pitfalls:
- Assuming
kill(getpid(), sig)is "more correct" thanraise(sig)— they differ only by spelling and by the one extra system call to fetch the PID; functionality for self-target is identical. - Using
kill(pid, SIGUSR1)with an uninitializedpid— ifpidcame from aforkthat was never checked for failure, a –1 error case could causekill(-1, sig)which has group-broadcast semantics inR5_Chapter 8_Process Control.txtstyle docs, not single-target.
14.13.4 Worked Traces for Both Variants
Two traces were displayed:
- raise variant: registration for SIGUSR1 in main, raise of SIGUSR1, handler prints and returns, remaining main lines execute.
- kill variant: registration in main, getpid into a variable, kill with that variable and SIGUSR1, handler prints and returns, remaining main lines execute.
In both, pressing no external keys is needed; the signal is generated by the program itself.
Side-by-side summary for quick comparison:
| Variant | Registration | Send | Target | Handler prints |
|---|---|---|---|---|
In-process (raise) |
signal(SIGUSR1, h) |
raise(SIGUSR1) |
implicit self | Inside handler function |
Cross-process shape (kill self) |
signal(SIGUSR1, h) |
kill(mypid, SIGUSR1) where mypid=getpid() |
explicit mypid |
Inside handler function |
Output for either, assuming one handler installation before either send:
Inside main function
Inside handler function
back in main after handler
No key press occurred — unlike the SIGINT Ctrl+C demo in section 14.12, here the program creates its own SIGUSR1 synchronously, so the entire exchange is visible without interactive timing. Swapping the kill line to kill(childPID, SIGUSR1) when childPID is the saved positive return from fork is exactly the construct used next to ping another process.
Q: What must match between the registration and the sending?
A: The signal number. If SIGUSR1 is registered, SIGUSR1 must be the value passed to raise or kill. A mismatch means the registered handler will not be the one invoked. In the two single-process demos the registration used SIGUSR1 and the sends both used SIGUSR1; a call such as raise(SIGUSR2) or kill(mypid, SIGINT) would have dispatched to the wrong disposition. The same rule holds for the bidirectional demo — the child's signal(SIGUSR1, child_handler) plus the parent's kill(childPID, SIGUSR1) must agree on SIGUSR1 in both places.
Recap + Bridge: raise(sig) is the one-argument "send to self" wrapper while kill(pid, sig) is the two-argument "send to whoever has this PID" primitive — both declared in <signal.h>, both report zero versus non-zero success, both were shown with SIGUSR1 to produce the same Inside handler function ping without any key press. Replacing the self-PID with a child's PID from fork converts that self-ping into the parent-to-child and child-to-parent exchange built next.
Exam note: Be ready to contrast raise (one signal parameter, sent to itself) with kill (two parameters — PID plus signal — sent to another process), to state their return convention (0 success, non-zero error), and to answer that the signal number must match between signal registration and raise/kill.
14.14 Parent-Child Bidirectional Signaling
14.14.1 Setup — Two Handlers, Fork, and Pause
The last demo put everything together: two signal handlers, one named for the parent and one for the child, a fork to create both roles, registration of each handler in its respective branch, and calls to pause and sleep to park a process while it waits for a signal. The includes again covered standard I/O, the Unix standard header, types, and signal handling. The parent and child handlers had distinct print messages so the observer could tell which ran.
Scaffolding — what makes two-way signals possible:
- Two handlers with distinct bodies and distinct print tags, e.g.,
parent_handlerprintsReceived a response signal from the childandchild_handlerprintsChild received a signal from the parent. - One
forkthat splits into parent and child after the handlers are either installed beforehand (common pattern) or installed immediately inside each branch — the lecture shows registration in the respective branches afterforkso each process only arms its own handler (signal(SIGUSR1, parent_handler)in the parent arm,signal(SIGUSR1, child_handler)in the child arm). - Parking primitives:
pause()blocks until any signal is delivered to the process;sleep(n)parks fornseconds regardless of signals (interruptible by delivery). The child is shown callingpause()after printingChild waiting for signalso it stays dormant without busy polling until the parent'skillarrives. The parent callssleep(1)briefly after the fork so the child has time to reach itspause()before the first signal is sent. - Headers:
<stdio.h>,<unistd.h>,<sys/types.h>, and<signal.h>— the same triple-plus-signal header pattern seen in section 14.5 plus signal.
Skeleton the trace follows:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
void parent_handler(int sig) { printf("Received a response signal from the child\n"); }
void child_handler(int sig) { printf("Child received a signal from the parent\n"); kill(getppid(), SIGUSR1); }
int main(void) {
pid_t pid;
pid = fork();
if (pid == 0) { /* child */
signal(SIGUSR1, child_handler);
printf("Child waiting for signal\n");
pause(); /* wait for parent's SIGUSR1 */
/* after handler returns, process continues / exits */
} else if (pid > 0) { /* parent */
signal(SIGUSR1, parent_handler);
sleep(1); /* give child time to reach pause */
printf("Parent started sending a signal to the child\n");
kill(pid, SIGUSR1); /* parent to child — pid is child's PID */
pause(); /* wait for child's reply */
}
return 0;
}
The two pause() calls are what make the ordering stable despite the kernel's freedom to schedule either process first — exactly the TELL/WAIT idea from R5_Chapter 8_Process Control.txt Section 8.9, realized with signals rather than pipes.
Scope: pause waits for any signal, not a specific SIGUSR1; a spurious unrelated signal would also wake it. Production code in R5_Chapter 10_Signals.txt style would use sigsuspend with masks for specificity, but for this teaching demo pause/sleep with distinct tags suffices to show the reply order.
14.14.2 Step-by-Step Message Exchange
The order of operations traced was:
- Main calls fork. Two execution streams now exist. The original parent keeps its parent identity and holds the positive child PID; the new child sees zero.
- In the child branch, register the child handler for SIGUSR1. Print "Child waiting for signal" and enter pause, which blocks until a signal arrives.
- In the parent branch, register the parent handler for SIGUSR1, then sleep briefly to give the child time to reach its pause.
- Parent sends SIGUSR1 to the child using kill with the child's PID. This is the positive PID the parent received from fork.
- The child's handler runs. It prints a line such as "Child received a signal from the parent." Inside that handler the child sends a reply signal back to its parent using kill with getppid and SIGUSR1. The parent's PID is obtained by the getppid call; no hard-coded value is needed.
- The parent's handler runs in response. It prints a line such as "Received a response signal from the child" and then the parent proceeds to exit.
The roles of getpid and getppid were highlighted: getpid returns the caller's own PID, getppid returns the caller's parent's PID. In that reply step, getppid is how the child learns whom to answer.
Causal chain that makes child-handler first inevitable:
The fork returns once in each process, but only the parent initiates the first kill. The full crossing is:
kill(pid, SIGUSR1)in the parent usespid, the positive child's PID saved fromfork— the only way the parent knows the child's number without a global.kill(getppid(), SIGUSR1)in the child usesgetppid()— the kernel query that returns the caller's parent PID (here the original parent).getpid()would target self by mistake; the lecture's point is that the reply does not need a hard-coded number becausegetppid()supplies the correct destination at reply time.getpidversusgetppid:getpidanswers "who am I?" (useful to label one's own trace line);getppidanswers "who created / adopted me?" (useful to answer). After anorphanre-parenting the reportedgetppid()would become 1, but in this demo the parent is still alive, sogetppid()in the child correctly names the true parent.
The sleep(1) plus pause() pairing is what makes the child guaranteed to be waiting when the first signal lands — without them, the parent's kill could race past the child's signal install, and R5_Chapter 8_Process Control.txt Section 8.9 would call that an unreliable race.
Visual intuition: draw two vertical lifelines. Child lifeline shows SIGNAL ARM → PRINT waiting → PAUSE block (horizontal wait bar). Parent lifeline shows SIGNAL ARM → SLEEP bar → arrow to child's lifeline labeled SIGUSR1 (first crossing) → PAUSE block. Child lifeline then shows handler box with a return arrow back to parent's lifeline labeled SIGUSR1 (reply). Takeaway: two crossings, child first, parent second, each arrow carries the same signal number but a different PID argument.
14.14.3 Observed Output and Control Flow
The observed console lines were summarized as, in order: a line indicating the child was created and now pausing, a line that the parent started sending a signal to the child, a line that the child received the signal from the parent, and a line that the parent received the response from the child before exiting. The trace was shown twice to confirm that the child handler always runs first — the parent is the initiator — and the parent handler runs second as the reply. Waiting, sleeping, and pausing were presented as the reason the order is stable in the demo even though process scheduling is normally flexible.
Observed output — one faithful ordering:
Child waiting for signal
Parent started sending a signal to the child
Child received a signal from the parent
Received a response signal from the child
Mapping to control flow:
Child waiting for signal— child owns the first print because it reachedpause()while the parent was still insleep(1). The parent's subsequent line appears second even though both processes were alive —sleepdelays the parent's first output past the child's waiting announcement.Child received a signal from the parent— appears insidechild_handlerafter the parent'skill(pid, SIGUSR1)is delivered; the lecture emphasized this is always the first handler to run because the parent is the initiator.Received a response signal from the child— appears insideparent_handlerafter the child'skill(getppid(), SIGUSR1)is delivered. The parent was parked inpause()so delivery is immediate; once this handler returns, the parent canreturn 0and exit.
A second run produced the same four lines in the same order, confirming the sleep/pause stabilization. The demo contrasted this with free-running fork demos (sections 14.5–14.6) where no wait/sleep/pause was present and output order was not guaranteed. The causal order here is logical, not accidental: initiator's target handles first, reply target handles second.
Pitfalls:
- Swapping parent PID lookup — using
getpid()in the child's reply would sendSIGUSR1to the child itself, so the parent's handler would never fire and the parent'spause()would wait forever. - Believing the parent always prints first — in this demo the child prints the waiting notice first because the parent's
sleepwas placed before the parent's first visible line; without thatsleep, scheduler could let the parent win the first print.
14.14.4 Student Questions and Answers
Q: Is it always parent first, child second here?
A: In this specific paired demo, yes. The parent is the one that initiates the first kill toward the child, so the child's handler is the first to run. The child's reply then triggers the parent's handler as the second event. Without the pause and sleep, scheduling could vary, but the causal order — parent signals child, child replies to parent — makes child-first the logical outcome. The sleep(1) and pause() are the deliberate pauses that make the reply order stable in the demo even though process scheduling is normally flexible.
Recap + Bridge: The bidirectional pattern is two mirrored sends — kill(childPID, SIGUSR1) from the parent to the child after fork and kill(getppid(), SIGUSR1) from the child's handler back to its parent, separated by pause/sleep so the child's Child received log always precedes the parent's Received a response log, with getppid supplying the reply address without a hard-coded number.
Exam note: Be ready to map kill with the saved child PID in the parent arm and kill with getppid in the child handler, to name which handler prints which line, and to explain why the child handler runs before the parent handler in this initiator-then-reply causality.
Real-world: Lightweight notification between a coordinator and a worker — the parent coordinator pings a specific worker by PID, the worker replies with an acknowledgment using its parent's identity, avoiding polling.
Exam note: Master the whole coordinator-worker signal map — two handlers distinct by print, fork splitting, pause/sleep stabilizing, and the two directional kill calls — as the capstone of the process system calls unit.
Exam Guidance Summary
- A few system calls related to process are the focus: fork, wait, exec, exit, signal, kill, and raise. Be ready to state the purpose of each and to trace programs that combine them. The lecture pairs creation (
fork/exec) with rendezvous (wait/waitpid) and notification (signal/kill/raise); any trace question can combine at least two of these families.
- Fork details to remember: no input parameters; on failure a single negative value; on success zero in the child and a positive PID (the child's PID) in the parent; code after fork runs in both; creating a process is heavier than creating a thread. The tri-state
pid_t fork(void)contract — negative error versus zero child versus positive parent PID — is the single most recalled fact for this exam segment.
- Counting with multiple forks: total processes that reach a final statement equal where is the number of forks; child count equals . Distinguish total prints versus prints coming only from children. For , total prints 8, children 7; the demo's PID hierarchy 70 to 77 was the concrete evidence for that count.
- Distinguishing branches: save the fork return into a variable
pidand branch withpid < 0for error,pid == 0for child,pid > 0for parent. The positive value in the parent is the handle to name that child in laterwaitpidcalls. A frequent error is to put parent work inside the== 0arm.
- Orphan versus zombie: orphan is a child whose parent has already terminated; zombie is a child that has finished but whose parent is too busy to collect its exit status. Orphans are adopted by the system's first process (often described as
initwithPID1, nowlaunchd/systemdon newer systems). The shell-prompt interleaving in section 14.5 is the visual symptom of the orphan window when nowaitwas used.
- Wait family: null-parameter
waitmeans wait for own child;waitpidwith a specificPIDand a status location waits for that child and collects its exit information. Withoutwait, orphans can appear; withwait, the parent blocks until the child can be reaped. PassingNULLfor the status discards details but still provides the blocking rendezvous; the encoding is decoded withWIFEXITED/WEXITSTATUS/WIFSIGNALED/WTERMSIG.
- Exec: used after fork when the child should not execute the parent program; replaces the child's image with a named program. Six variants exist; the example used the form with program name plus argument and environment both set to null. Ensure the executable name matches the string used in the
execcall —"child"in the source must be a real file namedchildaftergcc -o child child.c.
- Signals: a signal is an event that notifies a process that something important happened; the process pauses its current work and handles it.
SIGINTcorresponds toCtrl+C. Handling requires including signal handling definitions and registering a handler viasignalwith a signal number and handler name.SIG_DFLmeans restore the default handling. In the demo, the firstCtrl+Cwas caught by the custom handler and re-registered to default; the secondCtrl+Cended the program.
- Exit status: normal termination leaves the shell's last-status variable at zero; termination by
SIGINTleft it at130in the demonstrations. Refer to that variable without relying on printed shell symbols and remember the two typical values shown. The130is commonly128 + SIGINT(2)— the shell's way of marking a signal death.
- The last syllabus topic on linker and loader was stated as skipped and not part of the examination. The process-related system calls and their demonstrations are the examinable core. Do not spend revision time on linking details for this paper; every exam question in this block will name or trace
fork,wait,exec,signal,kill, orraise(plusexit).
Exam note: Walk the full paper as one story — create with fork, name with PID/getpid/getppid, tame with branching on the fork return, count with , collect with wait/waitpid, specialize with exec, notify with signals. Any printed output question is solved by counting doublings, then applying the three-way pid test, then checking whether a wait/pause/sleep forced ordering.
Key Industry Applications
- Web server scaling — a parent listener that forks a child per request so the child can fetch database data for a single client register number and build a response while the parent stays available for new arrivals; useful for bursty traffic such as large-scale results publication. This pattern appears in classic Unix
inetd-style services and in teaching examples offorkfollowed by per-client handling before returning toaccept.
- Load balancing as a modern complement — a front-end spreads requests across many server instances running on many machines, for example with a round-robin policy of request one to server one, request two to server two, wrapping around; illustrates the shift from per-request forking to horizontal scale-out. Cloud load balancers, Kubernetes service meshes, and CDN edge dispatchers all generalize the same idea: the dispatcher stays hot while workers scale out, with policies beyond round-robin such as least-connections and latency-aware routing.
- I/O-driven design — moving a printing job from running to waiting while the printer works, achieved multiprogramming and explained why waiting processes return to ready before running. The same waiting-state reasoning appears in file-system workloads, database write-behind, and network-driven microservices where latency is hidden by keeping the CPU fed with other ready processes.
- Daemon and helper patterns — parent creates a child with
fork, replaces the child's image with a handler program viaexec, passes client-specific data via argument and environment vectors, and collects completion viawaitpid. Cron jobs, build-system helpers, and container entry-point shims all follow thisfork/exec/waitlifecycle.
- Robustness and diagnosis — using PID values to identify parent versus child, watching for orphans adopted by the
initprocess and for zombies left by busy parents, and usingwaitvariants to avoid them. Dashboards that scrapepsforZ(zombie) counts andPPID1 orphans, plus supervisor patterns like double-fork to avoid zombies, are the production face of sections 14.8–14.9.
- Interactive control —
Ctrl+CasSIGINTto stop a runaway allocation loop, distinguishing a normal exit from a signal-induced exit by its exit status of130versus0. Shell scripts test\$?after foreground jobs, and terminal job control (SIGTSTP,SIGCONT) builds on the same handler-plus-SIG_DFLideas shown in section 14.12.
- Lightweight inter-process notification —
raiseto signal within the same process andkillwith a specificPIDto signal a different process, including the bidirectional parent-to-child and child-to-parent exchange usingkillwith a child'sPIDandkillwithgetppid. Coordinator-worker patterns use this to wake apause()-blocked helper, to implement heartbeats viaSIGUSR1/SIGUSR2, or to notify a parent of progress without a pipe, as demonstrated in the final paired demo where each direction carried the sameSIGUSR1with a differentPIDtarget.
SP Lecture 14 notes · Process-Related System Calls
Sections Breakdown
What Is a Process — Program in Execution
The fork System Call — Primary Way to Create Processes
Life Cycle of a Process — The Five States
PID as the kernel handle via getpid and getppid, fork signature pid_t fork(void) with tri-state return −1 error zero child positive parent, and why zero and child PID are assigned as they are.
Fork in C — First Program and Execution Order
Multiple Forks — Exponential Process Creation
Branching on pid <0 error pid==0 child pid>0 parent, diagnostic fork returned 1692 versus fork returned 0 with getpid confirmation, and variants with and without wait.
Orphan as live child whose parent exited versus zombie as terminated child awaiting collection, shell-prompt symptom, and init PID 1 adoption and reaping guarantee.
Wait and Waitpid — Synchronizing Parent and Child
The exec Family — Replacing the Child Image
Signals — Asynchronous Events
Registration via signal.h and signal SIGINT handler, handler that prints Inside handler and re-registers SIG_DFL, two-Ctrl+C behavior and exit status 0 versus 130 via shell status variable.
Raising and Sending Signals — raise and kill
Two handlers with fork pause and sleep setup, step-by-step parent kill child PID and child reply via kill getppid, observed order Child waiting Child received Received response, and initiator causality.
Consolidated exam focus on fork wait exec signal kill raise plus exit, tri-state fork contract, 2^n counting, branching, orphan versus zombie, wait family, exec replacement, signal handling and exit codes.
Industrial mapping of fork per request to scale-out and load balancing, I/O waiting and multiprogramming latency hiding, fork exec wait daemon pattern, orphan zombie diagnosis, and lightweight signal notification.
Exam Revision Notes
Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.
What Is a Process — Program in Execution
Must-know: Process is program in execution with code plus data and PID identity, one or many threads cover multicore
⚠️ Top pitfall: Confusing program file with live process
Self-check: What does getpid return?
Connects to: 14.2
The fork System Call — Primary Way to Create Processes
Must-know: Fork creates child by copying parent; child defaults to same code then deviates via exec or branch
⚠️ Top pitfall: Letting child fall into parent accept loop
Self-check: What does child execute by default?
Connects to: 14.4
Life Cycle of a Process — The Five States
Must-know: New Ready Running Waiting Terminated and why Waiting goes to Ready not Running
⚠️ Top pitfall: Thinking Waiting goes straight to Running
Self-check: Why Waiting->Ready?
Connects to: 14.4
Process Identity and How fork Reports Its Result
Must-know: Fork takes no args; returns -1 error, 0 child, positive child PID to parent
⚠️ Top pitfall: Treating positive as parent own PID
Self-check: What does fork return in child?
Connects to: 14.7
Fork in C — First Program and Execution Order
Must-know: Code after fork runs in both; parent tends to print first due to clone cost
⚠️ Top pitfall: Explaining prompt via threads
Self-check: How many prints after one fork?
Connects to: 14.6
Multiple Forks — Exponential Process Creation
Must-know: Total prints 2^n, children 2^n-1 for n sequential unguarded forks
⚠️ Top pitfall: Answering child count with 2^n
Self-check: Three forks how many children?
Connects to: 14.7
Distinguishing Parent and Child in Code
Must-know: Save fork return and branch pid<0 error, pid==0 child, pid>0 parent with child handle
⚠️ Top pitfall: Reversing child/parent branches
Self-check: Which branch is child?
Connects to: 14.9
Orphan and Zombie Processes
Must-know: Orphan child alive parent gone adopted by init PID1; zombie child dead parent not yet waited, holds table entry Z
⚠️ Top pitfall: Confusing orphan and zombie direction
Self-check: What init does for orphans?
Connects to: 14.9
Wait and Waitpid — Synchronizing Parent and Child
Must-know: wait NULL any child; waitpid specific PID with status; blocking rendezvous prevents orphans/zombies
⚠️ Top pitfall: Thinking wait names a specific child without pid
Self-check: Difference wait vs waitpid?
Connects to: 14.10
The exec Family — Replacing the Child Image
Must-know: Exec replaces child image keeping PID; six variants; name must match file; fork branch then exec in child then waitpid in parent
⚠️ Top pitfall: Exec in parent replaces listener
Self-check: What exec keeps same?
Connects to: 14.11
Signals — Asynchronous Events
Must-know: Signal is async numbered event; process pauses then handles; part of IPC
⚠️ Top pitfall: Thinking signal carries bulk data
Self-check: What does signal notify?
Connects to: 14.12
Signal Handling Mechanics
Must-know: Include signal.h; signal SIGINT handler; SIG_DFL restores default; first CtrlC caught second kills; exit 130 vs 0
⚠️ Top pitfall: Expecting handler on second CtrlC
Self-check: What SIG_DFL means?
Connects to: 14.13
Raising and Sending Signals — raise and kill
Must-know: Raise one arg self; kill two args PID+signal; both 0 on success; signal numbers must match
⚠️ Top pitfall: Spelling raise as race; thinking kill always kills
Self-check: What must match registration vs send?
Connects to: 14.14
Parent-Child Bidirectional Signaling
Must-know: Parent kill childPID SIGUSR1 then child kill getppid SIGUSR1 reply; pause sleep stabilize; child handler first
⚠️ Top pitfall: Child using getpid instead of getppid for reply
Self-check: Which handler runs first?
Connects to: 14.9
Exam Guidance Summary
Must-know: Exam Guidance Summary
⚠️ Top pitfall:
Self-check:
Key Industry Applications
Must-know: Key Industry Applications
⚠️ Top pitfall:
Self-check:
Was this lecture useful?
BitsNotes AI Assistant
Subject Notes AssistantConfigure AI Chat
Choose how to access the chatbotSigned in as
Powered by BitsNotes — 20 messages per day. No API key needed. Want unlimited access? Use "Bring Your Own Key" mode.
Sign in to use AI Chat
Get 20 free AI messages per day to ask questions about your lecture notes. Sign in with Google or GitHub — it takes 5 seconds.
Sign In to BitsNotesSwitch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.