Fundamentals 25 min read

Understanding Linux Process Creation, Waiting, and Execution: fork, wait, and exec

This article explains how Linux processes are created, managed, and replaced using the fork, wait (including waitpid), and exec system calls, covering their prototypes, return values, copy‑on‑write optimization, and practical C code examples that demonstrate their coordinated use in real‑world scenarios.

Deepin Linux
Deepin Linux
Deepin Linux
Understanding Linux Process Creation, Waiting, and Execution: fork, wait, and exec

In Linux, a process is the basic unit of resource allocation and scheduling, represented by a task_struct (PCB). Each process has its own address space, file descriptors, and registers, and the kernel manages them via the PCB.

Processes enable multitasking; for example, each terminal window runs as an independent process, allowing concurrent execution without interference.

The three essential system calls for process programming are fork (create a new process), wait (parent waits for child termination), and exec (replace the current program image).

1. Process Creation: fork Function Analysis

1.1 fork Basics

fork is declared in <unistd.h> as pid_t fork(void); . It creates a child process that is almost an exact copy of the parent, assigning a new PID while sharing some resources such as open file descriptors.

1.2 fork Return Values and Execution Logic

The call returns twice: in the parent it returns the child's PID, in the child it returns 0, and on failure it returns -1 and sets errno . This allows the program to distinguish parent and child execution paths.

#include
#include
#include
int main() {
    pid_t pid;
    // Create child process
    pid = fork();
    // Check return value
    if (pid < 0) {
        perror("fork error");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // Child process
        printf("I am the child process, my pid is %d, my parent's pid is %d\n", getpid(), getppid());
    } else {
        // Parent process
        printf("I am the parent process, my pid is %d, and my child's pid is %d\n", getpid(), pid);
    }
    return 0;
}

Running the program prints the PID of both parent and child, clearly showing their separate execution paths.

1.3 Copy‑On‑Write Mechanism

Instead of copying the entire address space immediately, Linux marks shared pages as read‑only and duplicates them only when a write occurs. This reduces memory usage and speeds up fork for large processes.

2. Process Waiting: wait Function Analysis

2.1 Purpose of wait

wait blocks the parent until one of its children terminates, then reclaims the child's resources and provides its exit status, preventing zombie processes.

2.2 Prototype and Parameters

Declared in <sys/types.h> and <sys/wait.h> as pid_t wait(int *status); . If status is NULL, the parent only waits; otherwise, the exit information is stored in the pointed integer.

WIFEXITED(status): true if child exited normally.

WEXITSTATUS(status): returns the child's exit code.

WIFSIGNALED(status): true if child was terminated by a signal.

WTERMSIG(status): returns the terminating signal number.

2.3 wait Example

#include
#include
#include
#include
#include
int main() {
    pid_t pid;
    int status;
    // Create child process
    pid = fork();
    if (pid < 0) {
        perror("fork error");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // Child process
        printf("I am the child process, my pid is %d\n", getpid());
        sleep(2);
        exit(3);
    } else {
        // Parent process
        printf("I am the parent process, my pid is %d, and my child's pid is %d\n", getpid(), pid);
        wait(&status);
        if (WIFEXITED(status)) {
            printf("The child process exited normally, exit status is %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("The child process was terminated by a signal, signal number is %d\n", WTERMSIG(status));
        }
    }
    return 0;
}

The output shows the parent and child PIDs and the child's exit status.

2.4 waitpid Extension

waitpid (prototype pid_t waitpid(pid_t pid, int *status, int options); ) adds flexibility:

Specify which child to wait for (positive PID, 0 for same process group, -1 for any child, < -1 for any child in a specific group).

Use options such as WNOHANG for non‑blocking wait.

Retrieve additional states with WUNTRACED and WCONTINUED .

Example of non‑blocking waitpid :

#include
#include
#include
#include
#include
int main() {
    pid_t pid;
    int status;
    pid = fork();
    if (pid < 0) {
        perror("fork error");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        printf("I am the child process, my pid is %d\n", getpid());
        sleep(5);
        exit(3);
    } else {
        printf("I am the parent process, my pid is %d, and my child's pid is %d\n", getpid(), pid);
        while (1) {
            pid_t ret = waitpid(pid, &status, WNOHANG);
            if (ret == 0) {
                printf("The child process is still running, I can do other things\n");
                sleep(1);
            } else if (ret == pid) {
                if (WIFEXITED(status)) {
                    printf("The child process exited normally, exit status is %d\n", WEXITSTATUS(status));
                } else if (WIFSIGNALED(status)) {
                    printf("The child process was terminated by a signal, signal number is %d\n", WTERMSIG(status));
                }
                break;
            } else {
                perror("waitpid error");
                break;
            }
        }
    }
    return 0;
}

This demonstrates that the parent can continue other work while periodically checking the child's status.

3. Program Replacement: exec Function Analysis

3.1 exec Family Overview

The exec family replaces the current process image with a new program without creating a new PID. It is commonly used after fork to run a different command, such as a shell executing ls .

3.2 Prototypes and Parameters

#include
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

Key parameters:

path : absolute path to the executable.

file : name searched in PATH if it contains no '/'.

arg/argv : arguments passed to the new program; the last element must be NULL .

envp : optional environment array; if omitted, the child inherits the parent's environment.

3.3 exec Example

#include
#include
#include
int main() {
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        perror("fork error");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // Child process executes "ls -l" via execlp
        if (execlp("ls", "ls", "-l", NULL) == -1) {
            perror("execlp error");
            exit(EXIT_FAILURE);
        }
    } else {
        // Parent waits for child
        wait(NULL);
        printf("Child process has finished.\n");
    }
    return 0;
}

The child replaces its image with ls -l , the parent waits, and then prints a completion message.

4. Coordination of fork, wait, and exec

4.1 Common Application Scenarios

In a shell, the user command triggers fork to create a child, the child calls exec to run the command (e.g., ls -l ), and the parent uses wait to reap the child. Web servers use the same pattern to handle client connections with separate child processes.

4.2 Full Code Demonstration

#include
#include
#include
#include
#include
int main() {
    pid_t pid;
    int status;
    pid = fork();
    if (pid < 0) {
        perror("fork error");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // Child process executes "ls -l"
        char *argv[] = {"ls", "-l", NULL};
        if (execvp("ls", argv) == -1) {
            perror("execvp error");
            exit(EXIT_FAILURE);
        }
    } else {
        // Parent process
        printf("I am the parent process, my pid is %d, and my child's pid is %d\n", getpid(), pid);
        wait(&status);
        if (WIFEXITED(status)) {
            printf("The child process exited normally, exit status is %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("The child process was terminated by a signal, signal number is %d\n", WTERMSIG(status));
        }
    }
    return 0;
}

The parent calls fork to create a child.

The child receives a return value of 0, builds an argument array, and calls execvp to run ls -l . On success, the child's memory image is replaced.

The parent receives the child's PID, prints both PIDs, and calls wait to block until the child finishes, then reports the child's exit status.

This example clearly shows how fork , exec , and wait work together to create a new process, replace its program, and clean up resources.

LinuxprocessC Programmingsystem callsforkExecwait
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.