Unix - UNIX fork(), exec(), and wait() System Call Interaction

1. Introduction

In UNIX operating systems, processes are the basic units of program execution. A process is an instance of a running program that has its own execution state, memory space, and system resources. UNIX provides several system calls that allow processes to be created, execute programs, and coordinate with other processes. Three important system calls used for these operations are fork(), exec(), and wait().

The fork() system call creates a new process from an existing process. The exec() family of system calls replaces the current process's program with a different program. The wait() system call allows a parent process to wait for a child process to finish or change state. These system calls are often used together to manage program execution and establish relationships between parent and child processes.

Understanding how these system calls interact is important for learning UNIX process management, command execution, and the internal behavior of shell programs.

2. The fork() System Call

The fork() system call is used to create a new process. The process that calls fork() is known as the parent process, while the newly created process is called the child process.

When fork() executes successfully, both the parent and child processes continue executing from the instruction immediately following the fork() call. Although they begin from the same point in the program, they execute independently and have different process IDs.

The return value of fork() helps the program distinguish between the parent and child processes. In the child process, fork() returns 0. In the parent process, it returns the process ID of the newly created child. If process creation fails, it returns -1 to the parent, and no child process is created.

UNIX systems commonly use a copy-on-write mechanism when creating a child process. Instead of immediately copying all the parent's memory, the operating system initially allows the processes to share memory pages and creates separate copies when modifications are required. This improves efficiency.

For example:

C

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

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

    if (pid == 0) {
        printf("This is the child process.\n");
    } else if (pid > 0) {
        printf("This is the parent process.\n");
    } else {
        perror("fork failed");
    }

    return 0;
}

In this program, fork() creates a child process. Both processes execute the conditional statement, but each follows a different branch according to the return value. The order in which their messages appear is not guaranteed because the operating system schedules the processes independently.

3. The exec() System Call

The exec() family of system calls is used to replace the program currently running inside a process with another program. It does not normally create a new process. Instead, it loads a new program into the existing process.

After a successful exec() call, the original program's code is no longer executed. The new program begins running in the same process, retaining its process ID. The new program receives its own command-line arguments and environment as specified by the particular exec() function used.

There are several variations, including execl(), execv(), execlp(), and execvp(). They differ in how arguments are supplied and whether the executable is searched for using the PATH environment variable.

Consider the following example:

C

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

int main() {
    printf("Starting a new program.\n");

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

    perror("exec failed");
    return 1;
}

In this example, execlp() attempts to replace the current program with the ls command using the -l option. If the replacement succeeds, the process displays the directory listing and does not return to the original program's next statement. The perror() statement runs only if the exec() call fails.

The distinction between fork() and exec() is important. fork() creates a process, while exec() changes the program executed by a process. A program can use fork() first and then call exec() in the child to run a separate command without replacing the parent process.

4. The wait() System Call

The wait() system call allows a parent process to wait for a child process to change state, most commonly by terminating. It is frequently used to ensure that the parent can collect the child's termination status before continuing.

When a child process terminates, the operating system retains a small amount of information about its termination until the parent collects it. During this period, the child may appear as a zombie process. Calling wait() or a related function allows the parent to retrieve the termination information and enables the operating system to release the remaining process-table entry.

The wait() function generally waits for one of the calling process's child processes to terminate. If a suitable child has already terminated, the call can return immediately. If no child has terminated yet, the parent normally blocks until one does. If there are no eligible child processes, the call fails.

A related system call, waitpid(), provides more control by allowing the parent to wait for a particular child or a selected group of children. It also supports options that allow a process to check a child's status without necessarily blocking.

Example:

C

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

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

    if (pid == 0) {
        printf("Child process is running.\n");
        return 5;
    } else if (pid > 0) {
        int status;

        wait(&status);

        if (WIFEXITED(status)) {
            printf("Child exited with status %d.\n",
                   WEXITSTATUS(status));
        }
    } else {
        perror("fork failed");
        return 1;
    }

    return 0;
}

Here, the child process terminates with exit status 5. The parent waits for the child and then checks whether it exited normally. If it did, WEXITSTATUS(status) extracts its exit code. The parent therefore obtains information about the child's completion instead of simply continuing without checking its result.

5. How fork(), exec(), and wait() Work Together

These three system calls are often combined to run another program while allowing the original process to remain active. A typical example is a command-line shell executing a command entered by a user.

The shell creates a child process with fork(). The child calls an exec() function to replace its program with the requested command. Meanwhile, the parent shell calls wait() or waitpid() if it needs to wait for the command to finish before accepting another command.

The process interaction can be represented as follows:

Parent process

Calls fork() to create a child

Two processes exist

Parent continues; child begins executing after fork()

Parent

Calls wait() or waitpid()

Child

Calls exec() to run a new program

Parent resumes

Collects the child's status

Child terminates

The executed program finishes

Simplified flow for a parent that waits for its child. Scheduling and execution timing may vary.

The sequence works as follows:

  1. The parent process invokes fork() to create a child process.

  2. The operating system provides the child with a separate process identity and an execution context.

  3. The child calls exec() to load and execute the required program.

  4. The parent calls wait() or waitpid() if it needs to wait for the child.

  5. The child executes the new program and eventually terminates.

  6. The parent collects the child's termination status and continues its own execution.

If fork() fails, the program must handle the error because there is no child in which to execute the new program. If exec() fails, the child should handle the error and usually terminate with a nonzero status. Otherwise, it might unintentionally continue executing code intended for the original program.

6. Practical Applications

The interaction between these system calls is used in several areas of UNIX and UNIX-like operating systems.

Command execution in shells: When a user enters a command such as ls, a shell can create a child process, use exec() to run the command, and wait for it to finish. Interactive shells may also allow commands to run in the background without waiting immediately.

Running external programs: Applications can launch other programs while retaining control of the original process. For example, a utility can start a separate program to process data and later check whether it completed successfully.

Process coordination: A parent can wait for children before proceeding to another stage of a task. This is useful when one operation depends on the completion of another.

Exit-status handling: By collecting a child's exit status, a parent can determine whether a task succeeded, failed, or terminated abnormally. This helps programs handle errors and decide what to do next.

Pipeline execution: Shells can create multiple child processes to connect commands through pipes. They may use fork(), exec(), and waitpid() to launch commands and collect their completion statuses.

7. Important Differences Between the Three System Calls

Feature fork() exec() family wait()
Main purpose Creates a child process Replaces the current process program Waits for a child state change and collects status
Creates a new process Yes No No
Changes the running program No, not by itself Yes No
Return behavior Returns differently in parent and child Returns only on failure Returns the child PID on success
Typical use Start a child process Run a different program Synchronize parent and child

8. Conclusion

The fork(), exec(), and wait() system calls form an important part of UNIX process management. fork() creates a child process, exec() replaces a process's running program with another program, and wait() allows a parent to collect information about a child's termination or other eligible state changes.

When combined, these calls allow UNIX applications and command-line shells to launch programs, execute tasks independently, coordinate process completion, and handle exit statuses. Understanding their differences and how they work together provides a strong foundation for learning advanced UNIX programming, process control, and operating-system behavior.