AlgoInsight

Free lesson · Operating Systems

Demonstrate a zombie process

Easy11 min readoperating-systemsosconcurrency

The Full Journey of a Process

In the previous topics, you learned each piece of the process story in isolation - what a process is, how it is created with fork and exec, the five states it moves through, and the PCB that tracks its entire existence. This topic puts all the pieces together into a single coherent picture: the complete lifecycle of a process from the moment it is born to the moment it disappears from the system.

Understanding the full lifecycle matters for interviews because real-world problems - zombie processes, orphan processes, resource leaks - are lifecycle problems. They happen when the lifecycle is disrupted: a parent dies before its child, a child exits but nobody cleans it up, a process finishes but its resources are not released. These are exactly the kinds of problems that show up in production systems and in interview questions.

The Complete Lifecycle

A process goes through six distinct phases during its life. Each phase involves specific OS operations and specific changes to the PCB.

graph TD
    A["1. Creation<br/>fork() called by parent"] --> B["2. Initialization<br/>PCB created, memory set up,<br/>state = New"]
    B --> C["3. Ready to Run<br/>State = Ready,<br/>placed in ready queue"]
    C --> D["4. Execution<br/>Alternates between Running,<br/>Ready, and Waiting<br/>for its entire active life"]
    D --> E["5. Termination<br/>Process calls exit() or is killed,<br/>state = Terminated"]
    E --> F["6. Cleanup<br/>Parent calls wait(),<br/>PCB removed from<br/>process table"]
    
    D -->|"Most of the<br/>process's life<br/>is spent here"| D
This diagram shows the six phases of a process lifecycle in sequence. Creation happens when fork is called. Initialization involves creating the PCB and setting up memory. Ready to Run means the process is placed in the ready queue. Execution is the longest phase where the process alternates between Running, Ready, and Waiting states. Termination happens when the process calls exit or is killed. Cleanup happens when the parent calls wait and the PCB is removed. The Execution phase has a self-loop indicating the process spends most of its life cycling through states in this phase.

Phase 1: Creation

A process comes into existence when an existing process calls fork(). The OS allocates a new PID, creates a PCB for the new process, and sets up the child's memory using copy-on-write from the parent. The child inherits the parent's open file descriptors, environment variables, and working directory.

If the child needs to run a different program, it calls exec() to replace its memory contents with the new program's code and data. The PID stays the same - exec does not create a new process.

The creation phase is typically very fast - microseconds on modern hardware. The OS uses copy-on-write to avoid copying memory unnecessarily, and the PCB setup is a well-optimized kernel operation.

Phase 2: Initialization

During initialization, the OS completes the setup of the new process. It sets the process state to New, assigns scheduling parameters (initial priority, time quantum), and registers the process in the process table. On Linux, this means creating a new task_struct and linking it into the system's process list.

Initialization also involves setting up the process's virtual address space. The OS creates a page table for the process (or, with copy-on-write, configures the child's page table to share the parent's pages). It sets up the stack, initializes signal handlers, and configures resource limits.

Once initialization is complete, the OS transitions the process state from New to Ready and places it in the ready queue. The process is now eligible to be scheduled.

Phase 3: Ready to Run

In the Ready state, the process is fully initialized and able to execute, but it is waiting in the ready queue for the scheduler to pick it. This wait can be nearly instantaneous (if the CPU is idle) or noticeable (if many other processes are also in the ready queue competing for CPU time).

The process has no control over how long it stays in the Ready state. That decision belongs to the scheduler, which picks the next process to run based on the scheduling algorithm in use - first-come-first-served, round robin, priority-based, or a more sophisticated approach like the Multi-Level Feedback Queue. Module 03 covers scheduling algorithms in depth.

Phase 4: Execution

This is where the process spends the vast majority of its lifetime. During execution, the process cycles between three states:

Running - the process is on the CPU, executing instructions. It stays here until its time slice expires, it blocks on I/O, or it finishes.

Ready - the process was preempted (its time slice expired or a higher-priority process arrived) and is waiting in the ready queue for another turn on the CPU.

Waiting - the process is blocked on I/O, a lock, a timer, or some other external event. It cannot continue until the event completes.

A typical process transitions between these three states thousands or millions of times during its execution. A web server, for example, runs (to process a request), waits (for the next request to arrive on the network socket), runs again, waits again, in a loop that may last for days, weeks, or months.

Phase 5: Termination

A process terminates in one of four ways:

Normal exit. The process finishes its work and calls exit() (or returns from main()). This is the clean case. The process's exit code is saved in its PCB (typically 0 for success, non-zero for error).

Error exit. The process encounters an error it cannot recover from and calls exit() with a non-zero exit code. This is still a controlled termination - the process chose to exit.

Fatal signal. Another process (or the OS itself) sends a signal that terminates the process. SIGKILL (signal 9) is the most forceful - the process cannot catch, block, or ignore it. SIGTERM (signal 15) is the polite request to terminate - the process can catch it and perform cleanup before exiting.

Unrecoverable error. The process does something that causes the OS to terminate it - accessing memory it does not own (segmentation fault, SIGSEGV), executing an illegal instruction (SIGILL), dividing by zero (SIGFPE). These are not signals the process sent itself - they are signals the OS sends in response to a violation.

When termination happens, the OS releases most of the process's resources - its memory is freed, its open files are closed, its network sockets are shut down, and its shared memory segments are detached. But the OS does not remove the PCB from the process table yet. The PCB stays because it contains the process's exit status, which the parent needs to collect.

Phase 6: Cleanup

The final phase happens when the parent process calls wait() or waitpid() to collect the terminated child's exit status. At this point, the OS removes the child's PCB from the process table, frees the last few bytes of kernel memory associated with the process, and the process is truly gone.

If the parent calls wait() promptly, the cleanup phase is nearly instantaneous. If the parent does not call wait(), the terminated child becomes a zombie process - a process that has finished executing but still occupies an entry in the process table. Zombies are one of the most commonly asked-about process lifecycle anomalies in interviews.

Zombie Processes: The Undead

A zombie process is a process that has terminated but whose parent has not yet called wait() to collect its exit status. The zombie is dead - it is not executing any code, it is not using CPU time, it does not have any memory allocated for its program - but it still occupies an entry in the process table because its exit status has not been collected.

Zombies are marked with the state Z in the ps output. You might see them listed as <defunct> in the command column.

sequenceDiagram
    participant Parent as Parent Process
    participant Child as Child Process
    participant OS as Operating System
    
    Parent->>OS: fork()
    OS-->>Child: Child created (PID 200)
    
    Note over Child: Child does some work
    
    Child->>OS: exit(0)
    OS->>OS: Free child's memory,<br/>close files, release resources
    OS->>OS: Keep child's PCB with<br/>exit status = 0
    
    Note over Child: Child is now a ZOMBIE<br/>State = Z, PID still in table
    
    Note over Parent: Parent is busy doing<br/>other things, has not<br/>called wait() yet
    
    Note over Child: Zombie persists...<br/>still occupying a<br/>process table entry
    
    Parent->>OS: waitpid(200, &status, 0)
    OS-->>Parent: Exit status = 0
    OS->>OS: Remove PID 200 from<br/>process table
    
    Note over Child: Zombie is gone.<br/>Process fully cleaned up.
This sequence diagram shows how a zombie process is created and cleaned up. The parent forks a child process. The child does work and then exits. The OS frees the child's resources but keeps its PCB with the exit status. The child is now a zombie. The parent eventually calls waitpid, receives the exit status, and the OS removes the child's PCB from the process table. The zombie is gone.

Why Zombies Are a Problem

A single zombie process is harmless - it uses a tiny amount of kernel memory for its PCB entry and nothing else. The problem arises when zombies accumulate. Each zombie occupies one entry in the process table, and the process table has a finite size. If a buggy parent process forks many children and never calls wait() on any of them, the zombies pile up until the process table is full. At that point, the OS cannot create any new processes - the entire system is stuck because a misbehaving program left too many zombies behind.

In production systems, zombie accumulation is a real operational issue. A server that forks worker processes to handle requests but does not properly wait for them to finish will gradually fill the process table with zombies. Monitoring systems at FAANG companies track zombie process counts and alert when they start accumulating.

How to Prevent Zombies

The simplest prevention is for the parent to call wait() or waitpid() for every child it creates. But sometimes the parent is busy and cannot afford to block waiting for each child. In that case, there are two common strategies.

Strategy 1: Asynchronous wait with SIGCHLD. When a child terminates, the OS sends the SIGCHLD signal to the parent. The parent can install a signal handler that calls waitpid() whenever SIGCHLD is received. This cleans up zombies as soon as they appear without blocking the parent's main execution.

Strategy 2: Double fork. The parent forks, the child immediately forks again (creating a grandchild), and the first child exits immediately. The parent waits for the first child (which exited right away, so the wait is fast). The grandchild is now an orphan - its parent (the first child) is dead. Init (PID 1) adopts the grandchild and will automatically wait for it when it terminates. This eliminates the zombie problem entirely because the actual long-running work is done by the grandchild, and init always cleans up orphans.

import os
import time

# Demonstrate a zombie process
pid = os.fork()

if pid == 0:
    # Child process: exit immediately
    print(f"Child (PID {os.getpid()}): I am exiting now")
    os._exit(0)  # Child exits
else:
    # Parent process: do NOT call wait() right away
    print(f"Parent (PID {os.getpid()}): Child PID is {pid}")
    print(f"Parent: Sleeping for 2 seconds without calling wait()...")
    print(f"Parent: During this time, the child is a zombie.")
    print(f"Parent: Check with: ps -p {pid} -o pid,stat,comm")
    time.sleep(2)
    
    # Now clean up the zombie
    os.waitpid(pid, 0)
    print(f"Parent: Called wait(). Zombie is cleaned up.")

Orphan Processes: When the Parent Dies First

An orphan process is a process whose parent has terminated before it has. The child is still running, but it no longer has a parent. This is the opposite of the zombie scenario - there, the child died first and the parent had not cleaned up. Here, the parent died first and the child is still alive.

When a process becomes an orphan, the OS does not let it float parentless forever. Instead, it reassigns the orphan to init (PID 1) - the first process on the system. Init becomes the orphan's new parent. Init periodically calls wait() on all its children, so when the orphan eventually terminates, init will clean it up. No zombie problem.

This behavior is a safety net built into Unix. It ensures that every process always has a parent and that no process can become a permanent zombie just because its parent died unexpectedly.

Interview: The zombie and orphan questions come up frequently. Here is how to handle them:

Q: "What is a zombie process?" "A zombie is a process that has terminated but whose parent has not called wait() to collect its exit status. It occupies an entry in the process table but uses no other resources. Zombies are problematic when they accumulate because they fill the process table and prevent new process creation."

Q: "What is an orphan process?" "An orphan is a process whose parent has terminated while the child is still running. The OS reassigns the orphan to init (PID 1), which will clean it up when it eventually terminates. Orphans are generally harmless - they are adopted by init and cleaned up properly."

Q: "How do you prevent zombie processes?" "Three approaches: the parent calls wait() or waitpid() for every child, the parent installs a SIGCHLD handler that calls waitpid() asynchronously, or the parent uses the double-fork technique where the first child exits immediately and the grandchild is adopted by init."

Resource Cleanup During Termination

When a process terminates, the OS has a significant cleanup job. It needs to reclaim every resource the process was using so those resources are available to other processes. The cleanup involves several steps.

Memory release. All the memory pages allocated to the process are freed - the text segment, data segment, heap, and stack. For shared pages (from copy-on-write), the OS decrements the reference count and frees the page only if no other process is using it.

File descriptor closure. Every open file descriptor is closed. If the process had files open for writing, any buffered data is flushed to disk before the file is closed. If the process held locks on files (via flock or fcntl), those locks are released.

Network socket shutdown. Open network sockets are closed. If the process was listening on a port, that port becomes available for other processes. If the process had established TCP connections, those connections enter the TIME_WAIT state (a TCP protocol detail, not an OS detail, but relevant for system design discussions).

Shared memory detachment. If the process was using shared memory segments, it is detached from those segments. The shared memory itself is not freed unless this was the last process using it.

Signal handler reset. The process's signal handlers are removed.

Child process reassignment. If the terminating process had children that are still running, those children become orphans and are reassigned to init.

After all of this cleanup, the only thing left is the PCB entry with the exit status. That remains until the parent calls wait().

Exercise: Starter: Write a program that creates a child process and then immediately calls os.waitpid() to wait for it. Verify that no zombie is created by checking ps while the child is running and after it has exited.

Intermediate: Write a program that creates 5 child processes. Each child should print its PID and exit. The parent should NOT call wait() immediately - instead, it should sleep for 10 seconds. During those 10 seconds, open another terminal and use ps aux | grep Z to find the zombie processes. After the sleep, have the parent call wait() for each child and verify the zombies disappear.

Advanced: Implement the double-fork technique in Python or C. The parent forks once. The first child forks again (creating a grandchild) and then exits immediately. The parent waits for the first child. The grandchild does some work (sleep for 5 seconds, then print a message) and exits. Verify that the grandchild's parent PID changes to 1 (init) after the first child exits. Verify that no zombies are created.

Back to Operating Systems