Process Creation in Linux

Photo by Lukas on Unsplash

Process Creation in Linux

A Detailed In-Depth Guide

Hello, and welcome!
In this article, I'll be taking you through the intricacies of process creation in Linux. As I delve deeper into the core concepts of the Linux kernel, I’m documenting my learnings to offer a comprehensive guide. Whether you're just starting with system programming or brushing up on the fundamentals, this write-up will provide valuable insights into the detailed mechanisms behind process creation in Linux.

1. Introduction

In Linux, process creation is a fundamental operation that allows multitasking, runs user applications, and manages system resources. The process creation sequence is critical to understanding the underlying mechanics of how the Linux kernel manages resources, memory, and CPU allocation. This article provides a step-by-step explanation of how processes are created in contemporary Linux systems, covering all intermediate concepts from the moment a new process is requested until it either terminates or transitions into a different state.


2. Understanding the Basics of Processes

2.1 What is a Process?

A process is an instance of a program that is running on the system. It contains the program’s code, its data, and allocated resources such as memory, file descriptors, and CPU time.

In Linux, each process is uniquely identified by its Process ID (PID). Processes can be either system processes (such as background services or kernel tasks) or user processes (applications run by users).

2.2 Process Control Block (PCB)

Every process is represented in the kernel using a data structure called the Process Control Block (PCB), which is implemented in Linux as the task_struct. The PCB holds all the information about the process:

+---------------------+
|   task_struct        |
+---------------------+
|  PID: Process ID     |
|  State: Running, etc |
|  Registers: CPU info |
|  Memory: Mappings    |
|  Files: Open FDs     |
|  Parent PID: PPID    |
+---------------------+
  • Process ID (PID): The unique identifier for the process.

  • Process State: Indicates whether the process is running, waiting, or terminated.

  • Registers: CPU register states that hold the execution context.

  • Memory Map: The virtual memory assigned to the process.

  • File Descriptors: List of open files by the process.

2.3 System Calls and the Kernel

Processes cannot directly access the hardware or kernel functions. Instead, they rely on system calls to interact with the kernel. For process creation, important system calls include:

  • fork(): Creates a new process by duplicating the calling process.

  • exec(): Replaces the current process memory with a new program.

  • wait(): Causes a parent process to wait for the child to finish.

  • clone(): Creates a new process with specific resource-sharing options.


3. Overview of Process Creation

When a new process is created in Linux, the steps involve:

  1. Creating a new process via the fork() system call.

  2. Sharing memory efficiently using the Copy-on-Write mechanism.

  3. Replacing the process’s memory with a new program using the exec() system call.

3.1 The Role of fork()

The fork() system call is the classic method for creating new processes in Linux. It works by duplicating the parent process into a child process. The new process gets a copy of the parent’s memory, register state, and open file descriptors. However, to save memory, modern Linux systems employ the Copy-on-Write (COW) technique (covered below).

Steps Involved in fork():
  1. System Call Entry: The process invokes the fork() system call, and control passes from user space to kernel space.

  2. Task Struct Creation: The kernel allocates a new task_struct for the child process, duplicating the state of the parent.

  3. PID Assignment: A new Process ID (PID) is assigned to the child process.

  4. Copying Parent’s Context: The child process inherits a copy of the parent’s:

    • Memory mappings

    • File descriptors

    • Signal handlers

    • Other process-specific resources

  5. Return Values:

    • The child process gets 0 as the return value from fork().

    • The parent process gets the PID of the child as the return value.

The fork() system call creates a new process by duplicating the calling (parent) process. The new process, called the child process, is an almost identical copy of the parent, with the same code, memory, and file descriptors.

+------------------+             +------------------+
|  Parent Process  |  fork() ->  |   Child Process   |
+------------------+             +------------------+
       PID=100                       PID=101

The key differences between the parent and child processes are:

  • The child process has a different PID.

  • fork() return values:

    • The parent receives the PID of the child.

    • The child receives 0.

Diagram 1: Forking a Process
+-------------------+                        +-------------------+
| Parent Process    |   -- fork() -->        |  Child Process     |
+-------------------+                        +-------------------+
| PID: 123          |                        | PID: 124           |
| Memory: Shared    |                        | Memory: Shared     |
| File Descriptors: |                        | File Descriptors:  |
+-------------------+                        +-------------------+

3.2 Memory Management and Copy-on-Write

After fork(), both the parent and child processes share the same memory pages. However, Linux uses a technique called Copy-on-Write (COW) to ensure memory efficiency. Both processes share the memory pages until one process modifies them. At that point, the kernel makes a copy of the modified pages for the process that made the change.

So in summary:

  • Shared Memory: Both processes initially share the same physical memory pages.

  • Memory Write: If either process writes to a shared page, a new physical page is allocated, and the modification occurs there.

  • Efficiency: This avoids unnecessary memory duplication when the child immediately calls exec().

Diagram 2: Copy-on-Write Mechanism
+-------------------+                        +-------------------+
| Parent Memory     |  <--- Shared Pages ---> | Child Memory      |
+-------------------+                        +-------------------+

After Modification:
+-------------------+                        +-------------------+
| Parent Memory     |      Pages Copied      | Child Memory       |
| (Unmodified)      |  <-- Copy-on-Write --> | (Modified Copy)    |
+-------------------+                        +-------------------+

This technique reduces memory usage, particularly when the child process immediately replaces its memory space using exec().

3.3 Replacing the Program with exec()

In many cases, after creating a child process using fork(), the child does not continue to run the same program as the parent. Instead, it replaces its memory space with a new program using the exec() system call.

Steps in exec():
  1. Loading the New Program: The child process calls exec() to load a new executable file into memory.

  2. Replacing Memory: The current memory, code, and data of the child process are discarded and replaced with the new program’s memory.

  3. Same PID: The PID remains unchanged, but the process now executes a different program.

  4. New Program Execution: The child starts executing the new program from its entry point.

Often, the child process created by fork() does not need to continue running the same program as the parent. Instead, it loads a different program into memory using the exec() system call. exec() replaces the current process memory space with a new program, but the process keeps its PID.

Before `exec()`:
+-------------------+
|  Old Program      |
+-------------------+

After `exec()`:
+-------------------+
|  New Program      |
+-------------------+

The exec() family of system calls (execv(), execve(), etc.) loads the executable file of a new program into memory, and the process continues executing from the entry point of the new program.

3.4 Handling Process Termination with wait()

The wait() system call ensures that the parent process can wait for its child to finish executing. When the child terminates, the parent retrieves the child’s exit status using wait().

Steps in wait():
  1. Parent Waits: The parent process calls wait() to block until one of its child processes exits.

  2. Child Termination: The child process terminates, and its resources are cleaned up.

  3. Exit Status: The parent process retrieves the exit status of the child and resumes execution.

Diagram 4: Process Waiting

+-------------------+                    +-------------------+
| Parent Process    |    -- wait() -->   |  Child Process     |
+-------------------+                    +-------------------+
| Running           |                    | Terminating        |
| Waiting for PID=124                    | PID=124 Exits      |
+-------------------+                    +-------------------+

If the parent doesn’t call wait(), the child becomes a zombie process until the parent cleans up its exit status.


4. Advanced Process Creation

4.1 vfork()

The vfork() system call is a variant of fork(), designed to optimize process creation when the child immediately calls exec(). Unlike fork(), which copies the entire address space of the parent process, vfork() suspends the parent process until the child calls exec(). The child and parent share the same address space without copying it.

Differences between fork() and vfork():

  • fork(): Duplicates the entire address space (with Copy-on-Write).

  • vfork(): Shares the address space until exec() is called.

4.2 clone()

The clone() system call allows for even more control over process creation. It is a more flexible variant of fork() and is used to create threads in Linux. It allows fine-grained control over what resources are shared between the parent and child, such as memory, file descriptors, and signal handlers while isolating others (e.g., execution stack).

It lets the parent and child share specific resources (e.g., memory, file descriptors) clone() is commonly used to create threads, where multiple execution flows share the same address space.

The clone() system call

Key Flags in clone():
  • CLONE_VM: Share the same memory space (used for threading).

  • CLONE_FILES: Share the same file descriptor table.

  • CLONE_SIGHAND: Share the same signal handlers.

Diagram 3: clone() Resource Sharing
+-------------------+                    +-------------------+
| Parent Thread     |   -- clone() -->   |  Child Thread      |
+-------------------+                    +-------------------+
| Memory: Shared    |                    | Memory: Shared     |
| FDs: Shared       |                    | FDs: Shared        |
| PID: Thread ID    |                    | PID: Thread ID     |
+-------------------+                    +-------------------+

5. Process States and Lifecycle

Once a process is created, it transitions between various states during its lifecycle:

  1. New: The process is being created.

  2. Running: The process is being executed on the CPU.

  3. Waiting: The process is waiting for some event (e.g., I/O completion).

  4. Terminated: The process has finished execution.

  5. Zombie: The process has terminated, but the parent has not yet collected its exit status.

Diagram 4: Process Lifecycle
+----------------+    +-------------+    +-----------------+    +-------------+
|    New (fork)  | -> |   Running   | -> |   Terminated    | -> |   Zombie     |
+----------------+    +-------------+    +-----------------+    +-------------+
                            |                     ^


                            |                     |
                            v                     |
                       +-----------+              |
                       |  Waiting  | -------------+
                       +-----------+

A zombie process is created when the parent process fails to call wait() to collect the exit status of a child process.

Process creation in Linux is a detailed and complex operation, involving multiple stages and mechanisms that ensure memory efficiency, resource sharing, and process isolation. From the use of fork() to create new processes, Copy-on-Write to optimize memory usage, and exec() to load new programs, Linux provides robust tools for handling multitasking. Advanced techniques like vfork() and clone() allow for finer control over resource sharing and process management.

Thank You!

As a passionate system programming enthusiast, I thrive on exploring the intricacies of low-level coding, from optimizing performance to understanding the deeper aspects of system internals. I’d love to hear your thoughts—whether it’s feedback, suggestions, or even a friendly discussion about system-level programming challenges. Feel free to connect with me, and let’s dive deeper into the fascinating world of systems development together!