ADVERTISEMENT
Responsive Ad Unit • Systems Architecture & Low-Level Engineering

Learning Objectives

  • Understand why operating systems create processes.
  • Learn the purpose of fork().
  • Understand Parent and Child processes.
  • Study process creation inside Linux.
  • Understand PID allocation.
  • Prepare for advanced process management topics.

1. Introduction

Modern operating systems execute multiple programs simultaneously. A web browser, music player, terminal, and editor can all run at the same time because each program executes as an independent process. Whenever Linux needs another process, it creates one using the fork() system call. The fork() system call is one of the most fundamental process management operations in UNIX-like operating systems.

2. Why Do We Need fork()?

Suppose a terminal starts another program. If Linux had no mechanism to create another process, the terminal itself would have to stop executing permanently. Instead, Linux creates a new process.

Terminal ↓ User Types ls ↓ fork() ↓ Child Process Created ↓ exec() ↓ ls Program Executes ↓ Terminal Continues Running

Because of fork(), the terminal remains available after the command finishes.

3. Definition

The fork() system call creates a new process called the Child Process from the currently executing Parent Process. Conceptually, the child starts with the same program, memory contents, open file descriptors, and execution context as the parent. In modern Linux systems, this is implemented efficiently using Copy-on-Write (COW). Instead of immediately copying all physical memory pages, the parent and child initially share them as read-only pages. A private copy of a page is created only when either process modifies that page. After fork() returns, both processes execute independently.

4. Parent and Child Process

Before fork() +-------------------------+ | Parent Process | | PID = 1000 | +-------------------------+ | | fork() ▼ After fork() +-------------------------+ | Parent Process | | PID = 1000 | +-------------------------+ || || +-------------------------+ | Child Process | | PID = 1001 | +-------------------------+

The child process is created by the kernel. It receives a unique Process ID (PID), while the parent retains its original PID.

5. What Does the Child Receive?

When fork() succeeds, the child process begins execution with the same logical process state as the parent. In modern Linux, physical memory pages are shared initially using Copy-on-Write (COW), and private copies are created only when a page is modified.

Resource Inherited by Child
Program Code Yes
Global Variables Yes
Heap Yes
Stack Yes
Environment Variables Yes
Open File Descriptors Yes
Signal Dispositions Mostly Yes
PID No (new PID assigned)

6. Process Creation Flow

Application Running ↓ fork() ↓ CPU enters Kernel Mode ↓ Kernel allocates PCB ↓ Kernel allocates PID ↓ Kernel creates Child Process ↓ Kernel duplicates Process Context ↓ fork() returns ↓ Parent Executes Child Executes

7. Return Value of fork()

The return value of fork() allows a program to determine whether it is executing in the parent process or the child process.

Return Value Meaning
> 0 Returned in the parent process. The value represents the PID of the child.
0 Returned in the child process.
-1 Process creation failed.

8. First fork() Program

#include 
#include 

int main()
{
    pid_t pid;

    pid = fork();

    if(pid < 0)
    {
        printf("fork failed\n");
        return 1;
    }

    if(pid == 0)
    {
        printf("Child Process\n");
    }
    else
    {
        printf("Parent Process\n");
    }

    return 0;
}

9. Output

Parent Process

Child Process

The order of these messages is not guaranteed. After fork(), the operating system scheduler decides when each process executes.

10. Execution Timeline

main() ↓ fork() ↓ +----------------------+ Parent ↓ printf() ↓ return +----------------------+ || || +----------------------+ Child ↓ printf() ↓ return +----------------------+

11. Process ID (PID)

Every process executing in Linux is uniquely identified by a Process ID (PID). The operating system assigns this identifier when a new process is created. The PID allows the kernel to distinguish one process from another and manage CPU scheduling, memory allocation, signals, and resource ownership.

Process A PID = 1201 ------------------- Process B PID = 1202 ------------------- Process C PID = 1203
No two active processes have the same Process ID at the same time.

12. Parent Process ID (PPID)

Every process (except the initial system process) is created by another process. The identifier of the process that created it is called the Parent Process ID (PPID).

PID = 100 init | +----------------+ | PID = 450 bash | +----------------+ | PID = 600 my_program
Process PID PPID
init 1 0
bash 450 1
my_program 600 450

13. getpid()

The getpid() system call returns the Process ID of the calling process.

#include <unistd.h>

pid_t getpid(void);

Example

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("PID = %d\n", getpid());

    return 0;
}

Sample Output

PID = 3245

14. getppid()

The getppid() system call returns the Process ID of the parent process.

#include <unistd.h>

pid_t getppid(void);
#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("PID  = %d\n", getpid());
    printf("PPID = %d\n", getppid());

    return 0;
}

15. Parent and Child Process IDs

#include <stdio.h>
#include <unistd.h>

int main()
{
    pid_t pid;

    pid = fork();

    if(pid == 0)
    {
        printf("Child\n");
        printf("PID  = %d\n", getpid());
        printf("PPID = %d\n", getppid());
    }
    else
    {
        printf("Parent\n");
        printf("PID  = %d\n", getpid());
        printf("Child PID = %d\n", pid);
    }

    return 0;
}

16. Execution Flow

Parent Process PID = 500 ↓ fork() ↓ +----------------------------+ Parent PID = 500 Child PID = 501 +----------------------------+ || || +----------------------------+ Child PID = 501 PPID = 500 +----------------------------+

17. Why Does fork() Return Twice?

This is one of the most frequently asked interview questions. Although the fork() system call is invoked once, it returns in two different processes:

  • Once in the parent process.
  • Once in the newly created child process.
fork() ↓ Kernel Creates Child ↓ Return ↓ Parent Receives Child PID || || Child Receives 0

The return value allows the program to determine which process is executing and perform different tasks accordingly.

18. Process Tree

Terminal PID 300 | +------------------+ | Program PID 500 | fork() | +------------------+ | Child PID 501 | fork() | +------------------+ | Grandchild PID 502

Each process can create additional child processes, forming a hierarchical process tree.

19. Observing Processes in Linux

ps -ef

ps -f

pstree

top

htop

These commands display currently running processes, their parent-child relationships, and resource usage.

20. Practice Exercise

Write a program that:

  1. Calls fork().
  2. Prints the PID of the parent.
  3. Prints the PID of the child.
  4. Prints the PPID of the child.
  5. Observe the output using pstree while the program is running.

21. Process Memory Layout Before fork()

Before understanding what fork() does, it is important to understand how a process is organized in memory. Every running process has its own virtual address space, which is divided into multiple logical sections. Each section stores a different type of information required during program execution.

Higher Memory Address +-----------------------------------+ | Stack | |-----------------------------------| | Local Variables | | Function Calls | +-----------------------------------+ | | | Free Virtual Memory | | | +-----------------------------------+ | Heap | |-----------------------------------| | malloc() | | calloc() | | realloc() | +-----------------------------------+ +-----------------------------------+ | BSS Segment | |-----------------------------------| | Uninitialized Global Variables | +-----------------------------------+ +-----------------------------------+ | Data Segment | |-----------------------------------| | Initialized Global Variables | +-----------------------------------+ +-----------------------------------+ | Text Segment | |-----------------------------------| | Program Instructions | | Executable Machine Code | +-----------------------------------+ Lower Memory Address

22. Memory Segments

Segment Purpose
Text Stores executable machine instructions.
Data Stores initialized global and static variables.
BSS Stores uninitialized global and static variables.
Heap Dynamically allocated memory requested at runtime.
Stack Function calls, parameters, return addresses, and local variables.

23. What Happens When fork() is Called?

When a process calls fork(), the kernel creates a new child process. From the application's point of view, the child begins execution with the same program, variables, open files, and memory contents as the parent at the instant fork() returns. Internally, modern Linux avoids immediately copying all physical memory. Instead, both processes initially share the same memory pages using Copy-on-Write (COW). Only when either process writes to a shared page does the kernel create a private copy of that page. After the system call returns, the parent and child continue executing independently.

Before fork() +----------------------+ Parent Process PID = 500 Memory Space +----------------------+ ↓ fork() ↓ After fork() +----------------------+ +----------------------+ Parent Child PID = 500 PID = 501 Text Text Data Data Heap Heap Stack Stack +----------------------+ +----------------------+

24. Are Parent and Child Sharing Memory?

Immediately after fork(), the parent and child appear to have identical memory contents. However, they execute as separate processes. A modification made by one process does not change the memory seen by the other process.

The parent and child have independent process address spaces. Each process continues execution with its own view of memory.

25. Example Program

#include <stdio.h>
#include <unistd.h>

int main()
{
    int value = 100;

    pid_t pid = fork();

    if(pid == 0)
    {
        value = 200;

        printf("Child : %d\n", value);
    }
    else
    {
        printf("Parent : %d\n", value);
    }

    return 0;
}

26. Possible Output

Parent : 100

Child : 200

The parent still prints 100, while the child prints 200. Although both processes started with the same variable value, each process updates its own copy after fork().

27. Memory Independence

Before fork() value = 100 ↓ fork() ↓ Parent value = 100 ↓ Parent prints 100 =========================== Child value = 100 ↓ Child changes value ↓ value = 200 ↓ Child prints 200

28. Heap Memory Example

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    int *num;

    num = malloc(sizeof(int));

    *num = 50;

    pid_t pid = fork();

    if(pid == 0)
    {
        *num = 500;

        printf("Child Heap = %d\n", *num);
    }
    else
    {
        printf("Parent Heap = %d\n", *num);
    }

    free(num);

    return 0;
}

The heap memory is logically independent for the parent and child after fork(). Changes made by one process are not visible in the other process.

29. Key Observations

  • Both processes begin execution immediately after fork().
  • Both processes initially observe the same memory contents.
  • Each process has its own stack and heap.
  • Updating a variable in one process does not change the value seen by the other process.
  • The parent and child execute independently under the control of the scheduler.
  • Modern Linux implements fork() efficiently using Copy-on-Write (COW), so physical memory pages are copied only when they are modified.

30. Interview Questions

  1. Why do the parent and child initially print the same variable value after fork()?
  2. Why doesn't changing a variable in the child affect the parent?
  3. What memory segments exist in a Linux process?
  4. Does the child receive a new Process ID?
  5. Can the parent directly access the child's stack variables?

31. Copy-on-Write (COW)

When learning fork(), it is common to imagine that the operating system immediately duplicates the entire memory of the parent process. Modern Linux systems use a more efficient technique called Copy-on-Write (COW). Instead of copying every memory page during fork(), the parent and child initially share the same physical memory pages. These shared pages are marked as read-only. If either process later attempts to modify a shared page, the kernel creates a private copy of that page for the writing process.

Copy-on-Write reduces memory usage and improves the performance of fork(), especially when the child process soon executes another program using exec().

32. Memory Before fork()

Parent Process +---------------------------+ Text ---------------------------- Data ---------------------------- Heap ---------------------------- Stack +---------------------------+ ↓ Physical Memory +---------------------------+ Page A Page B Page C Page D +---------------------------+

33. Memory Immediately After fork()

Immediately after fork(), both processes refer to the same physical memory pages. No additional copies are created at this stage.

Parent ↓ +-----------+ Page Table +-----------+ | |------------+ | | ▼ ▼ Physical Memory +------------------+ Page A (RO) Page B (RO) Page C (RO) Page D (RO) +------------------+ ▲ ▲ | | |------------+ | +-----------+ Page Table +-----------+ ↓ Child

RO = Read Only

34. What Happens When the Child Writes?

Suppose the child modifies a variable stored on one of the shared pages. The write cannot proceed because the page is currently marked read-only. This triggers a page fault, allowing the kernel to allocate a new page and copy the original contents before completing the write.

Child Writes ↓ Page Fault ↓ Kernel ↓ Allocate New Physical Page ↓ Copy Original Data ↓ Update Child Page Table ↓ Resume Child

35. Memory After Copy-on-Write

Parent ↓ Page A ↓ Shared ↓ Physical Page A =========================== Child ↓ Private Page A ↓ Modified Value

Only the modified page is copied. All unmodified pages continue to be shared.

36. Why Copy-on-Write?

Without COW With COW
Entire process memory copied Only modified pages copied
Slow process creation Fast process creation
Large memory consumption Efficient memory usage
Unnecessary copying Copy only when required

37. Example

#include 
#include 

int main()
{
    int value = 100;

    pid_t pid = fork();

    if(pid == 0)
    {
        value = 500;

        printf("Child : %d\n", value);
    }
    else
    {
        sleep(1);

        printf("Parent : %d\n", value);
    }

    return 0;
}

Initially, both processes observe the value 100. When the child changes the variable to 500, the kernel creates a private copy of the affected memory page for the child. The parent's copy remains unchanged.

38. Copy-on-Write and Page Faults

The first write to a shared page generates a page fault because the page is marked read-only. The kernel then:

  1. Allocates a new physical page.
  2. Copies the original page contents.
  3. Updates the page table of the writing process.
  4. Marks the new page writable.
  5. Restarts the interrupted instruction.
Write Attempt ↓ Read-Only Page ↓ Page Fault ↓ Kernel ↓ Allocate Page ↓ Copy Data ↓ Update Page Table ↓ Continue Execution

39. Why COW Works Well with exec()

A common programming pattern is:

fork() ↓ Child Process ↓ exec() ↓ Load New Program

In this situation, the child quickly replaces its address space with a different program. Because of Copy-on-Write, Linux avoids copying the parent's memory unnecessarily before the replacement occurs.

40. Interview Questions

  1. What problem does Copy-on-Write solve?
  2. Does Linux copy the entire process memory during fork()?
  3. When is a memory page actually copied?
  4. Why is fork() followed by exec() in many programs?
  5. What role do page faults play in Copy-on-Write?

41. Copy-on-Write (COW) Execution Example

The following example demonstrates how Copy-on-Write works in practice. Initially, both parent and child processes appear to have identical memory. However, when one process modifies a variable, the kernel creates a private copy of the affected memory page for that process.

#include <stdio.h>
#include <unistd.h>

int main()
{
    int number = 100;

    pid_t pid = fork();

    if(pid == 0)
    {
        printf("Child Before = %d\n", number);

        number = 500;

        printf("Child After  = %d\n", number);
    }
    else
    {
        sleep(1);

        printf("Parent = %d\n", number);
    }

    return 0;
}

42. Sample Output

Child Before = 100

Child After  = 500

Parent = 100

The parent process continues to observe the original value because it still references its own logical view of memory. The child's modification causes the kernel to create a private copy of the modified page.

43. Step-by-Step Kernel Execution

Step Kernel Activity
1 Parent process executes normally.
2 Parent calls fork().
3 Kernel creates the child process.
4 Parent and child initially share the same physical memory pages.
5 Shared pages are marked read-only.
6 Child attempts to modify a variable.
7 A page fault occurs.
8 Kernel allocates a new physical page.
9 Original page contents are copied.
10 Child page table is updated.
11 Child resumes execution.
12 Parent continues using the original page.

44. Complete Memory Transition

Before fork() Parent | +---------------------+ Page A number = 100 +---------------------+ ↓ fork() ↓ Parent Page Table --------+ | | Child Page Table ---------+ | ▼ Shared Physical Page number = 100 (Read Only) ↓ Child writes number = 500 ↓ Page Fault ↓ Kernel Allocates New Page ↓ Parent ---------------------> Page A number = 100 Child ----------------------> Page B number = 500

45. Why Read-Only Pages?

If both processes were allowed to write directly to the same physical page, one process could accidentally overwrite the other process's data. Marking shared pages as read-only ensures that the kernel is notified before any modification occurs. This notification is provided through a page fault, allowing the kernel to create a private copy for the writing process.

46. Performance Benefits of Copy-on-Write

Without Copy-on-Write With Copy-on-Write
Entire address space copied immediately. Pages copied only when modified.
High memory usage. Efficient memory utilization.
Longer process creation time. Faster process creation.
Many unnecessary memory copies. Only modified pages are copied.
Poor scalability. Better scalability.

47. Important Points

  • Copy-on-Write is an optimization technique used by modern Linux systems.
  • The child initially appears to have the same memory as the parent.
  • Physical memory pages are shared until a write operation occurs.
  • The first write generates a page fault.
  • The kernel creates a private copy only for the modified page.
  • The parent process remains unaffected by changes in the child.
  • Unmodified pages continue to be shared, improving memory efficiency.

48. Interview Questions

  1. Why are shared pages marked read-only after fork()?
  2. What event triggers Copy-on-Write?
  3. Why doesn't Linux immediately duplicate all memory pages?
  4. Which process receives the copied page after a write?
  5. Can both parent and child continue sharing unmodified pages?
  6. Why is Copy-on-Write considered a performance optimization?

49. The exec() System Call

The fork() system call creates a new process. However, the newly created child process initially executes the same program as its parent. Most applications do not want the child to continue running the same program. Instead, they want the child to execute a completely different program. This is the purpose of the exec() family of system calls.

Important fork() creates a new process. exec() replaces the program running inside an existing process.

50. Why Do We Need exec()?

Consider a Linux terminal. When a user types a command such as:

ls

The terminal itself should continue running after the command finishes. If the terminal directly became the ls program, it would no longer exist after ls terminated. Instead, Linux follows these steps:

Terminal ↓ fork() ↓ Child Process ↓ exec("ls") ↓ Runs ls Program ↓ Child Exits ↓ Terminal Continues Running

51. What Does exec() Do?

The exec() system call replaces the current program image with a new program. After a successful exec():

  • The old program no longer exists.
  • The new program starts executing.
  • The Process ID (PID) remains the same.
  • No new process is created.

52. fork() vs exec()

fork() exec()
Creates a new process. Does not create a new process.
Child gets a new PID. PID remains unchanged.
Parent continues executing. Current program is replaced.
Both processes execute. Only the new program executes.
Returns twice. Returns only if an error occurs.

53. Execution Flow

Parent Process ↓ fork() ↓ Parent || || Child ↓ exec() ↓ Old Program Removed ↓ New Program Loaded ↓ Execution Starts From main()

54. Memory Before exec()

Child Process +------------------------+ Text Segment Data Segment BSS Heap Stack +------------------------+ Program A

55. Memory After exec()

Same Process Same PID ↓ Old Memory Removed ↓ Load Executable ↓ New Text Segment ↓ New Data Segment ↓ New Heap ↓ New Stack ↓ Program B Starts

The process remains the same, but its address space is replaced with the new program.

56. Does exec() Return?

If exec() successfully loads the new program, control never returns to the original program. The original process image has already been replaced. The function returns only if an error occurs.

Situation Return Value
Success Does not return
Failure -1

57. Why fork() and exec() Are Used Together

Shell ↓ fork() ↓ Parent Shell || || Child Process ↓ exec() ↓ Requested Program ↓ Exit ↓ Shell Waits ↓ Prompt Returns

Using fork() followed by exec() allows the shell to continue running while the requested command executes in a separate process.

58. Real Linux Example

When the following command is entered in the terminal:

gcc hello.c -o hello

./hello

The shell creates a child process using fork(). The child replaces itself with the hello executable using exec(). After the program exits, the shell continues accepting new commands.

59. Key Points

  • fork() creates a child process.
  • exec() replaces the currently executing program.
  • The Process ID remains unchanged after exec().
  • No new process is created by exec().
  • On success, exec() never returns.
  • The combination of fork() and exec() forms the basis of process creation in UNIX-like systems.

60. Interview Questions

  1. What is the difference between fork() and exec()?
  2. Why does exec() not create a new process?
  3. Does the Process ID change after exec()?
  4. Why does exec() normally never return?
  5. Why do shells use fork() before calling exec()?

61. The exec() Family of Functions

Linux provides several functions that belong to the exec() family. Although their names are different, all of them perform the same basic operation:

  • Replace the current program with a new executable.
  • Do not create a new process.
  • Keep the same Process ID (PID).

The differences between these functions are mainly in how program arguments and environment variables are passed.

62. Members of the exec() Family

Function Description
execl() Arguments passed as a list.
execv() Arguments passed as an array.
execlp() Searches executable using PATH.
execvp() Uses PATH and argument array.
execle() Allows custom environment variables.
execve() Underlying system call interface used by Linux.

63. Understanding the Function Names

Letter Meaning
l Arguments supplied as a list.
v Arguments supplied as an array (vector).
p Search executable using the PATH environment variable.
e Provide a custom environment.

64. exec() Family Overview

exec() | +-------------------------------+ | | execl() execv() | | execlp() execvp() | execle() | execve()

65. execl()

The execl() function accepts program arguments one by one.

#include <unistd.h>

int execl(const char *path,
          const char *arg0,
          const char *arg1,
          ...
          NULL);

Example

execl("/bin/ls",
      "ls",
      "-l",
      NULL);

The executable path must be specified explicitly.

66. execv()

The execv() function receives arguments through an array.

char *args[] =
{
    "ls",
    "-l",
    NULL
};

execv("/bin/ls", args);

This approach is useful when the number of arguments is determined at runtime.

67. execlp()

The execlp() function searches the executable using the PATH environment variable.

execlp("ls",
       "ls",
       "-l",
       NULL);

Unlike execl(), the full path does not need to be specified.

68. execvp()

The execvp() function combines two features:

  • Arguments supplied as an array.
  • Executable searched using PATH.
char *args[] =
{
    "ls",
    "-l",
    NULL
};

execvp("ls", args);

69. execle()

The execle() function allows a program to specify a custom set of environment variables for the new program.

char *env[] =
{
    "USER=guest",
    NULL
};

execle("/bin/printenv",
       "printenv",
       NULL,
       env);

70. execve()

The execve() function is the underlying interface used by the kernel to execute a new program. Most other members of the exec() family eventually invoke execve() after preparing the required arguments.

int execve(const char *pathname,
           char *const argv[],
           char *const envp[]);

71. Choosing the Correct Function

If You Need... Recommended Function
Arguments one by one execl()
Arguments in an array execv()
Automatic PATH search execlp()
PATH search with array arguments execvp()
Custom environment variables execle()
Direct kernel interface execve()

72. Summary

  • All exec() functions replace the current program.
  • None of them creates a new process.
  • The PID remains unchanged.
  • The differences are only in argument passing, PATH searching, and environment handling.
  • Most applications commonly use execvp() because it supports PATH searching and array-based arguments.

73. Interview Questions

  1. What is the difference between execl() and execv()?
  2. When should execvp() be preferred over execv()?
  3. Which function allows custom environment variables?
  4. Which function is ultimately invoked by most members of the exec() family?
  5. Why is execvp() commonly used in shells?

74. The wait() System Call

After creating a child process using fork(), the parent process often needs to wait until the child finishes execution. Linux provides the wait() system call for this purpose. When a parent calls wait(), it is suspended until one of its child processes terminates.

Important The wait() system call synchronizes the execution of parent and child processes.

75. Why Do We Need wait()?

Suppose a parent process creates a child that writes data into a file. If the parent immediately starts reading the file before the child has finished writing, incomplete or incorrect results may occur. Using wait() ensures that the parent proceeds only after the child has completed its work.

Parent ↓ fork() ↓ Child ↓ Writes File ↓ Exit ↓ Parent waits ↓ Parent Reads File

76. Syntax of wait()

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
Parameter Description
status Pointer used to receive the child's exit status.

77. Return Value of wait()

Return Value Meaning
> 0 PID of the terminated child process.
-1 No child exists or an error occurred.

78. Example Program

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid;

    pid = fork();

    if(pid == 0)
    {
        printf("Child Running\n");

        sleep(3);

        printf("Child Finished\n");
    }
    else
    {
        printf("Parent Waiting...\n");

        wait(NULL);

        printf("Parent Continues\n");
    }

    return 0;
}

79. Sample Output

Parent Waiting...

Child Running

Child Finished

Parent Continues

The parent process does not continue until the child terminates.

80. Execution Timeline

Parent ↓ fork() ↓ wait() ↓ Blocked ============================== Child ↓ Runs ↓ Completes Work ↓ exit() ============================== Kernel ↓ Child Terminates ↓ Kernel records the child's termination status ↓ Parent is moved from the Waiting state to the Ready state ↓ Scheduler selects the parent to run ↓ Parent resumes after wait()

81. Process State During wait()

Process State
Parent Waiting (Blocked)
Child Running
After Child Exits Parent becomes Ready and resumes execution.

82. Kernel Activity During wait()

Parent ↓ Calls wait() ↓ Kernel ↓ Check Child Status ↓ Child Finished? ↓ No ↓ Move Parent to Waiting Queue ↓ Scheduler Runs Child ↓ Child Exits ↓ Kernel Wakes Parent ↓ Parent Continues

83. Complete Process Flow

Shell ↓ fork() ↓ Child ↓ exec() ↓ Program Executes ↓ exit() ↓ Kernel Stores Exit Status ↓ Parent wait() ↓ Kernel Returns Exit Status ↓ Shell Displays Prompt

84. Key Points

  • wait() suspends the parent until a child process terminates.
  • The kernel wakes the parent after the child exits.
  • The child's exit status can be collected.
  • wait() helps synchronize parent and child execution.
  • Most command-line shells use wait() after fork() and exec().

85. Interview Questions

  1. Why is wait() required after fork()?
  2. What happens if the parent never calls wait()?
  3. Does wait() consume CPU while waiting?
  4. What value does wait() return?
  5. Can one parent wait for multiple child processes?

86. Child Process Termination using exit()

Every process eventually completes its execution. When a process finishes, it informs the operating system that it is terminating. This is performed using the exit() library function, which eventually invokes the kernel to terminate the process. The process may also terminate by returning from the main() function.

Important Returning from main() is equivalent to calling exit() with the same return value.

87. Syntax of exit()

#include <stdlib.h>

void exit(int status);
Parameter Description
status Exit status returned to the parent process.

88. Exit Status

Every process returns an integer value when it terminates. The parent process can retrieve this value using wait().

Status Meaning
0 Program completed successfully.
Non-zero Program terminated with an error or a specific application-defined status.

89. Example Program

#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("Program Started\n");

    exit(0);

    printf("This statement never executes\n");

    return 0;
}

After exit(0) is executed, the process terminates immediately. Any statements following exit() are not executed.

90. Parent Receiving Child Exit Status

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    int status;

    pid_t pid = fork();

    if(pid == 0)
    {
        printf("Child Running\n");

        exit(25);
    }

    wait(&status);

    printf("Child Finished\n");

    return 0;
}

91. Kernel Activity During exit()

Running Process ↓ exit() ↓ Kernel Receives Request ↓ Close Open Files ↓ Release Memory ↓ Release Other Resources ↓ Store Exit Status ↓ Notify Parent (for example, by sending SIGCHLD) ↓ Process enters the Zombie state ↓ Parent calls wait() or waitpid() ↓ Kernel removes the remaining process entry ↓ Kernel Removes Process Entry

92. What Happens Inside the Kernel?

Step Kernel Operation
1 Stop executing the process.
2 Close all open file descriptors.
3 Release process memory.
4 Release other allocated kernel resources.
5 Save exit status.
6 Notify the parent process.
7 Keep a minimal process entry until the parent collects the status.

93. Why Doesn't Linux Remove the Process Immediately?

The parent process may need to know whether the child completed successfully or failed. For this reason, Linux temporarily retains a small amount of information about the terminated process, including its Process ID (PID) and exit status. Once the parent calls wait(), this information is collected and the remaining process entry is removed.

94. Complete Process Life Cycle

Program Starts ↓ fork() ↓ Child Created ↓ exec() ↓ Program Executes ↓ exit() ↓ Kernel Saves Exit Status ↓ wait() ↓ Kernel Removes Child ↓ Parent Continues

95. Important Points

  • exit() terminates the calling process.
  • Returning from main() has the same effect as calling exit().
  • The parent can obtain the child's exit status using wait().
  • The kernel temporarily retains termination information until the parent collects it.
  • This mechanism prepares the foundation for understanding zombie processes.

96. Interview Questions

  1. What is the difference between return 0; and exit(0);?
  2. Why is the exit status stored by the kernel?
  3. What resources are released when a process exits?
  4. Why doesn't Linux immediately remove a terminated process?
  5. Which system call allows the parent to collect the child's exit status?

97. Zombie Process

A Zombie Process is a process that has finished executing, but whose termination status has not yet been collected by its parent using wait() or waitpid(). Although the process has stopped executing and most of its resources have already been released, the kernel retains a small amount of information about it so that the parent can obtain its termination status.

Important A zombie process is not running. It does not consume CPU time. It only occupies a small process table entry inside the kernel until the parent collects its termination status.

98. Why Does Linux Create Zombie Processes?

When a child process terminates, the parent may still need to determine:

  • Whether the child completed successfully.
  • Why the child terminated.
  • The exit status returned by the child.

To preserve this information, the kernel keeps a minimal process descriptor until the parent retrieves it using wait() or waitpid().

99. Process Life Cycle Including Zombie State

New ↓ Ready ↓ Running ↓ exit() ↓ Zombie ↓ Parent calls wait() ↓ Kernel removes process entry ↓ Process completely destroyed

100. Resources Released During exit()

Resource Released Immediately?
User-space memory Yes
Open file descriptors Yes
Kernel resources Most are released
Process table entry No (retained until wait())
Exit status No (retained until wait())
Process ID (PID) Retained until the zombie is reaped

101. Zombie Process Example

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    pid_t pid = fork();

    if(pid == 0)
    {
        printf("Child exiting...\n");
        exit(0);
    }

    sleep(30);

    return 0;
}

The child terminates immediately. The parent intentionally does not call wait(). For approximately 30 seconds, the child remains a zombie process.

102. Viewing Zombie Processes

ps -el

or

ps -aux

A zombie process is typically displayed with the process state Z. Example:

PID   PPID   STAT   COMMAND

2480  2475   Z      a.out

103. Kernel View of a Zombie Process

Child Executes ↓ exit() ↓ Kernel releases memory ↓ Kernel closes open files ↓ Kernel stores exit status ↓ Kernel marks process as Zombie ↓ Parent calls wait() ↓ Kernel returns exit status ↓ Kernel removes process table entry

104. What Happens If wait() Is Never Called?

If the parent process never calls wait() or waitpid(), the zombie remains in the process table until one of the following occurs:

  • The parent process terminates.
  • The parent later collects the child's termination status.

If the parent terminates before collecting the child's status, the zombie is adopted and eventually reaped by the system's child-reaper process (typically init or systemd, depending on the system).

105. Key Observations

  • A zombie process has already finished execution.
  • It does not execute instructions.
  • It does not consume CPU time.
  • Only minimal kernel bookkeeping information remains.
  • The parent should call wait() or waitpid() to remove the zombie.

106. Interview Questions

  1. What is a zombie process?
  2. Why does Linux keep zombie processes?
  3. Do zombie processes consume CPU time?
  4. Which process resources remain after a child exits?
  5. How can a zombie process be removed?
  6. Which process eventually reaps an unreaped zombie if the parent exits?

107. Orphan Process

An Orphan Process is a running process whose parent process terminates before the child finishes execution. The child process continues running normally even though its original parent no longer exists. To ensure that every process always has a parent, the kernel immediately reassigns the orphan process to the system's child-reaper process (typically init or systemd, depending on the Linux system).

Important An orphan process is still a normal running process. It is not an error condition and is completely managed by the operating system.

108. How Does an Orphan Process Occur?

Parent Process | +--------------------+ | fork() | Child Process | Parent exits | Kernel detects that the child has no parent | Kernel assigns a new parent | init/systemd | Child continues execution

109. Example Program

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t pid = fork();

    if(pid == 0)
    {
        sleep(5);

        printf("Child PID  = %d\n", getpid());
        printf("Parent PID = %d\n", getppid());

        exit(0);
    }

    printf("Parent Exiting...\n");

    return 0;
}

The parent terminates immediately. The child continues executing. When the child later calls getppid(), it reports the Process ID of its new parent.

110. Example Output

Parent Exiting...

Child PID  = 4210

Parent PID = 1

On many Linux systems, the child is re-parented to PID 1, which is typically systemd or another process acting as the system's child-reaper.

111. Kernel Activity

Step Kernel Operation
1 Parent process exits.
2 Kernel checks for active child processes.
3 Each running child is re-parented to the system's child-reaper process.
4 The child continues executing without interruption.
5 When the child later terminates, its new parent collects its exit status.

112. Orphan Process Life Cycle

Parent Running ↓ fork() ↓ Parent || || Child ↓ Parent exits ↓ Kernel reparents child ↓ Child continues running ↓ Child finishes ↓ Child-reaper collects exit status

113. Zombie vs Orphan Process

Zombie Process Orphan Process
Already terminated. Still executing.
Waiting to be reaped. Has lost its original parent.
Consumes no CPU time. May continue using CPU normally.
Retains a minimal process table entry. Executes like any other process.
Removed after wait() or waitpid(). Adopted by the system's child-reaper process.

114. Real Linux Example

Many server applications intentionally create orphan processes. For example, a server may create a background worker process and then allow the original parent process to terminate. The background process continues running independently under the supervision of the operating system.

115. Important Points

  • An orphan process is still running.
  • It is automatically re-parented by the kernel.
  • The child continues execution normally.
  • Re-parenting ensures that every process always has a parent.
  • The system's child-reaper process eventually collects the child's exit status after it terminates.

116. Interview Questions

  1. What is an orphan process?
  2. How is an orphan process different from a zombie process?
  3. Who becomes the new parent of an orphan process?
  4. Does an orphan process stop executing?
  5. Why does Linux re-parent orphan processes?

117. The waitpid() System Call

The wait() system call waits for any one child process to terminate. However, many applications create multiple child processes and need to wait for a specific child. For this purpose, UNIX and Linux provide the waitpid() system call.

Important waitpid() gives the parent process finer control over which child process it waits for and how it waits.

118. Why Do We Need waitpid()?

Consider a server that creates three child processes.

Parent | +----------------------------+ | fork() | +-----------+-----------+ | | | Child A Child B Child C

Using wait(), the parent cannot choose which child finishes first. Using waitpid(), the parent can wait for a specific child identified by its Process ID (PID).

119. Syntax

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid,
              int *status,
              int options);

120. Parameters

Parameter Description
pid Selects which child process(es) to wait for.
status Receives the child's termination status.
options Controls the behavior of waitpid().

121. Meaning of the pid Parameter

pid Value Meaning
> 0 Wait for the child whose PID exactly matches this value.
-1 Wait for any child process (similar to wait()).
0 Wait for any child process in the same process group as the caller.
< -1 Wait for any child whose process group ID equals the absolute value of pid.

122. Common Options

Option Description
0 Block until the selected child changes state.
WNOHANG Return immediately if no selected child has changed state.
WUNTRACED Also report children that have stopped.
WCONTINUED Also report children that were resumed after being stopped (if supported).

123. Example Program

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
    pid_t child;

    child = fork();

    if(child == 0)
    {
        sleep(2);
        exit(0);
    }

    printf("Waiting for child %d\n", child);

    waitpid(child, NULL, 0);

    printf("Child Finished\n");

    return 0;
}

124. Execution Flow

Parent ↓ fork() ↓ Child PID = 3501 ↓ waitpid(3501) ↓ Blocked ===================== Child ↓ Runs ↓ exit() ===================== Kernel ↓ Records Exit Status ↓ Wake Parent ↓ Parent Continues

125. wait() vs waitpid()

wait() waitpid()
Waits for any child. Can wait for a specific child.
Less control. More control.
No options. Supports multiple options.
Simple programs. Servers, shells, and multi-process applications.

126. Key Points

  • waitpid() extends the functionality of wait().
  • It allows a parent to wait for a specific child process.
  • It supports blocking and non-blocking operation.
  • It can monitor different child state changes through option flags.
  • Most UNIX shells and server applications use waitpid() instead of wait() because it provides greater flexibility.

127. Interview Questions

  1. Why is waitpid() preferred over wait() in server applications?
  2. What is the purpose of the pid parameter?
  3. What does WNOHANG do?
  4. How does waitpid() support non-blocking behavior?
  5. When would you use wait() instead of waitpid()?

128. Understanding Child Exit Status

When a child process terminates, the parent can determine how the child finished by examining the status value returned by wait() or waitpid(). The status value is an encoded integer interpreted using macros defined in <sys/wait.h>. Applications should use these macros instead of attempting to interpret the integer value directly.

Important The numeric value stored in status is implementation-defined. Portable applications should always use the POSIX macros provided in <sys/wait.h>.

129. Why Are Macros Required?

The integer returned by wait() contains multiple pieces of information, such as:

  • Whether the child exited normally.
  • Whether the child was terminated by a signal.
  • The child's exit code.
  • Whether the child was stopped or resumed (when requested).

Because this information is encoded into a single integer, the operating system provides standard macros to decode it safely.

130. Common Status Macros

Macro Purpose
WIFEXITED(status) True if the child terminated normally.
WEXITSTATUS(status) Returns the child's exit code (valid only if WIFEXITED() is true).
WIFSIGNALED(status) True if the child terminated because of a signal.
WTERMSIG(status) Returns the terminating signal number.
WIFSTOPPED(status) True if the child is currently stopped.
WSTOPSIG(status) Returns the signal that stopped the child.
WIFCONTINUED(status) True if a previously stopped child resumed execution (when supported).

131. Normal Process Termination

If a program finishes by:

  • calling exit(), or
  • returning from main(),

then the parent can detect this using WIFEXITED(status).

Child ↓ exit(25) ↓ Kernel stores exit status ↓ Parent ↓ wait() ↓ WIFEXITED(status) ↓ TRUE ↓ WEXITSTATUS(status) ↓ 25

132. Example Program

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    int status;

    pid_t pid = fork();

    if(pid == 0)
    {
        exit(25);
    }

    wait(&status);

    if(WIFEXITED(status))
    {
        printf("Exit Code = %d\n",
               WEXITSTATUS(status));
    }

    return 0;
}

133. Sample Output

Exit Code = 25

134. Child Terminated by a Signal

Not every process ends by calling exit(). A process may terminate because it receives a signal, such as SIGSEGV, SIGABRT, or SIGKILL. The parent can detect this using WIFSIGNALED(status).

Child ↓ Signal Received ↓ Kernel Terminates Process ↓ Parent ↓ wait() ↓ WIFSIGNALED(status) ↓ TRUE ↓ WTERMSIG(status) ↓ Signal Number

135. Example

if(WIFSIGNALED(status))
{
    printf("Signal = %d\n",
           WTERMSIG(status));
}

136. Summary Table

Question Macro
Did the child exit normally? WIFEXITED()
What was the exit code? WEXITSTATUS()
Did a signal terminate the child? WIFSIGNALED()
Which signal terminated the child? WTERMSIG()
Was the child stopped? WIFSTOPPED()
Which signal stopped it? WSTOPSIG()

137. Important Points

  • Never interpret the raw status integer directly.
  • Always use the macros provided by <sys/wait.h>.
  • WEXITSTATUS() is valid only if WIFEXITED() returns true.
  • WTERMSIG() is valid only if WIFSIGNALED() returns true.
  • These macros provide a portable interface across POSIX systems.

138. Interview Questions

  1. Why should you never interpret the raw status value directly?
  2. What is the difference between WIFEXITED() and WEXITSTATUS()?
  3. When is WTERMSIG() valid?
  4. Which macro determines whether a child terminated because of a signal?
  5. Why does POSIX provide status macros instead of exposing the status format directly?

139. How a UNIX Shell Executes a Command

One of the most common uses of the fork(), exec(), and waitpid() system calls is inside a UNIX shell. Whenever a user enters a command, the shell creates a child process, executes the requested program in the child, and waits for the child to finish before displaying the next command prompt (for foreground commands).

Example
$ ls -l
The shell itself does not become the ls program. Instead, it creates a child process that executes ls.

140. Command Execution Flow

User ↓ Types Command ↓ Shell Reads Command ↓ fork() ↓ Parent (Shell) Child | | | exec() | | | ls Program | | waitpid() | | | Prompt Program Ends

141. Step-by-Step Execution

Step Description
1 User enters a command.
2 The shell parses the command line.
3 The shell calls fork().
4 The child process calls an exec() function to replace its program image.
5 The requested program executes.
6 The program terminates using exit() or by returning from main().
7 The parent shell calls waitpid() to collect the child's termination status.
8 The shell displays the next prompt.

142. Simplified Shell Implementation

while(1)
{
    read_command();

    pid_t pid = fork();

    if(pid == 0)
    {
        execvp(command[0], command);
        exit(EXIT_FAILURE);
    }

    waitpid(pid, NULL, 0);
}

This simplified example demonstrates the fundamental control flow used by many command-line shells for executing foreground commands.

143. Complete Process Life Cycle

Shell Running ↓ User Types Command ↓ fork() ↓ Parent Shell ---------------- Child | | | exec() | | waitpid() New Program | | Prompt Program Executes | | <---------------------- exit() ↓ Next Command

144. Key Concepts Learned in This Chapter

  • Creating a new process with fork().
  • Understanding parent and child processes.
  • Copy-on-Write (COW) optimization.
  • Replacing the process image with the exec() family.
  • Synchronizing processes using wait() and waitpid().
  • Handling normal termination with exit().
  • Retrieving child termination status.
  • Zombie and orphan processes.
  • Using status macros such as WIFEXITED() and WEXITSTATUS().
  • Understanding how a UNIX shell launches programs.

145. Interview Questions

  1. Why does a shell use fork() before calling exec()?
  2. Why doesn't the shell call exec() directly?
  3. Why is waitpid() preferred by shells?
  4. What happens if the shell never waits for foreground child processes?
  5. How does the shell regain control after a command finishes?

147. Best Practices

  • Always check the return value of fork() for errors.
  • Call an exec() function only in the child process unless replacing the current process is the intended behavior.
  • If an exec() call fails, handle the error and terminate the child process appropriately.
  • Ensure that every terminated child process is reaped using wait() or waitpid() to avoid zombie processes.
  • Use waitpid() when an application manages multiple child processes.
  • Always interpret the child status using the macros defined in <sys/wait.h>.
  • Close file descriptors that are no longer needed after fork(), especially before executing another program.

148. Common Programming Mistakes

Mistake Possible Result
Ignoring the return value of fork() Failure to detect process creation errors.
Not calling wait() or waitpid() Zombie processes accumulate.
Using WEXITSTATUS() without first checking WIFEXITED() Undefined or meaningless results.
Assuming parent and child share writable memory after fork() Incorrect program logic.
Continuing execution after a failed exec() without handling the error Unexpected behavior.

149. Performance Considerations

  • Modern Linux uses Copy-on-Write (COW), so fork() does not immediately duplicate all memory pages.
  • The cost of fork() increases with the size and complexity of the process because page tables and kernel data structures must still be created.
  • Replacing the child immediately with an exec() function is a common and efficient pattern.
  • Long-running server applications often use waitpid() with non-blocking options to manage multiple child processes.

150. Complete Chapter Review

Program Starts ↓ fork() ↓ Parent + Child ↓ Copy-on-Write ↓ exec() ↓ Program Executes ↓ exit() ↓ Kernel Stores Exit Status ↓ wait()/waitpid() ↓ Parent Collects Status ↓ Zombie Removed ↓ Program Ends

151. Quick Revision

Concept Purpose
fork() Create a child process.
exec() Replace the current process image.
wait() Wait for any child process.
waitpid() Wait for a specific child process.
exit() Terminate a process.
Zombie Terminated child awaiting reaping.
Orphan Running child whose parent has terminated.
Copy-on-Write Delay copying memory until modification.

152. Practice Exercises

  1. Write a program that creates one child process and prints the PIDs of both the parent and child.
  2. Create two child processes and wait for them using waitpid().
  3. Modify a variable in the child after fork() and observe Copy-on-Write behavior.
  4. Execute the ls command using execvp().
  5. Create a zombie process and remove it using wait().
  6. Create an orphan process and observe the new parent process using getppid().
  7. Write a simple shell that repeatedly reads a command, executes it using fork() and execvp(), and waits for completion.

153. Next Chapter

The next chapter explores how the operating system decides which process runs next. Topics include:

  • CPU Scheduling
  • Scheduling Goals
  • Scheduling Criteria
  • FCFS
  • SJF
  • Priority Scheduling
  • Round Robin
  • Multilevel Queue
  • Multilevel Feedback Queue
  • Linux Completely Fair Scheduler (CFS)
  • Real-Time Scheduling
  • Context Switching
Knowledge Check

Concept Mastery Assessment — Module 15

Verify your conceptual understanding with 12 targeted technical assessment questions. Instant feedback and explanations are provided for every choice.

Question 1 of 12
What is the primary architectural purpose of fork() System Call & Process Creation in operating systems?
Explanation
fork() System Call & Process Creation is designed to manage copy-on-write (cow) optimization, parent and child process relationship, duplicate descriptors, and pid allocation. within the operating system architecture.
Question 2 of 12
Which system interface or data structure is centrally associated with fork() System Call & Process Creation?
Explanation
Under POSIX and Linux, fork() System Call & Process Creation operates through standardized system calls and kernel control structures.
Question 3 of 12
What safety or concurrency constraint must developers consider when implementing fork() System Call & Process Creation?
Explanation
Proper synchronization, avoidance of race conditions, and handling reentrant execution are critical when working with fork() System Call & Process Creation.
Question 4 of 12
When a system call related to fork() System Call & Process Creation fails in C, how does the POSIX runtime conventionally report the failure?
Explanation
Standard POSIX system calls return -1 or NULL on error, assigning an error integer (e.g. EINVAL, EPERM, EAGAIN) to the thread-local errno variable.
Question 5 of 12
What is the primary performance tradeoff associated with fork() System Call & Process Creation?
Explanation
Operations involving fork() System Call & Process Creation balance performance against kernel transition, memory bandwidth, and cache locality tradeoffs.
Question 6 of 12
What security principle governs access to resources in fork() System Call & Process Creation?
Explanation
The kernel validates credentials, effective permissions, and Linux capabilities before granting access to protected operations.
Question 7 of 12
How does execution of fork() System Call & Process Creation transition between User Space and Kernel Space?
Explanation
User space transitions to kernel mode securely via software traps or SYSCALL instructions that switch processor rings to Ring 0.
Question 8 of 12
What happens to resources allocated by fork() System Call & Process Creation when the owning process terminates?
Explanation
Process resources (open fds, memory maps) are automatically reclaimed by the kernel on termination, while kernel-persistent objects survive.
Question 9 of 12
In production Linux server environments, how is fork() System Call & Process Creation optimized for high throughput?
Explanation
Linux optimizes throughput via zero-copy data paths, asynchronous kernel events, and scalable concurrency primitives.
Question 10 of 12
What standard governs the portability of fork() System Call & Process Creation across Unix, Linux, and macOS platforms?
Explanation
IEEE POSIX standards define consistent APIs across Unix-like platforms ensuring portable source code execution.
Question 11 of 12
Which command-line diagnostic tool is most effective for inspecting and troubleshooting fork() System Call & Process Creation at runtime?
Explanation
Utilities like strace, lsof, and the /proc filesystem provide direct visibility into kernel interactions and resource status.
Question 12 of 12
What is a critical production best practice when developing systems software involving fork() System Call & Process Creation?
Explanation
Defensive programming requires explicit error checking on all system operations, proper resource cleanup, and avoiding memory corruption.
0 / 12 (0%)
Great job! Review the concepts above and proceed when ready.
ADVERTISEMENT
Responsive Ad Unit • Technical Courses & Certifications