Unix - UNIX fork() and exec() Internals: Creating and Replacing Processes

In UNIX systems, fork() and exec() are two fundamental system calls used for process creation and program execution. Although they are commonly used together, they perform completely different operations. The fork() system call creates a new process by duplicating an existing process, while the exec() family of system calls replaces the currently running program inside a process with another program. Understanding these calls is essential for learning how UNIX shells, command execution, and many server applications work.

1. Understanding fork()

The fork() system call creates a new process called the child process from the existing process, which is called the parent process. The child initially receives a copy of the parent's process state, including its memory layout, open file descriptors, environment, and other execution information.

The basic syntax is:

pid_t fork(void);

The return value of fork() helps distinguish the parent and child processes:

  • A return value greater than 0 indicates that the call is executing in the parent process. The value is the process ID of the newly created child.

  • A return value of 0 indicates that the call is executing in the child process.

  • A return value of -1 indicates that process creation failed.

A simple example is:

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

int main() {
    pid_t pid;

    pid = fork();

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

    return 0;
}

After fork() successfully executes, both the parent and child continue execution from the instruction immediately following the fork() call.

2. How fork() Creates a Process

A common misconception is that UNIX immediately creates a completely independent physical copy of all the parent's memory. Modern UNIX-like operating systems generally use a technique called copy-on-write.

Initially, the parent and child can share the same physical memory pages. These pages are marked so that if either process attempts to modify one, the operating system creates a separate copy of that page. This approach makes process creation considerably more efficient than copying the entire address space immediately.

The child receives its own process identity, including a different process ID. However, many other properties are initially inherited from the parent.

For example:

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

Both processes then execute independently.

3. What Does the Child Inherit?

The child process inherits many characteristics of its parent, including:

  • Open file descriptors

  • Environment variables

  • Current working directory

  • User and group credentials

  • Signal dispositions in appropriate cases

  • Resource-related process attributes

  • File descriptor status information

However, the child does not simply become an identical process. It receives a new process ID, and certain process-specific attributes differ between the parent and child.

One particularly important feature is that open file descriptors inherited across fork() generally refer to the same underlying open file descriptions. This makes fork() extremely useful for implementing pipes, redirection, and other forms of UNIX process communication.

4. Understanding exec()

The exec() family works differently from fork().

An exec() call does not create a new process. Instead, it replaces the current process's program with another program.

For example, suppose a process is currently executing a program called programA. If it calls an appropriate exec() function to execute programB, the process continues with programB, but it normally retains the same process ID.

Conceptually:

Before exec():

Process PID 500
    |
    +-- programA

After exec():

Process PID 500
    |
    +-- programB

The process itself remains, but its program image is replaced.

5. The exec() Family

UNIX provides several related functions in the exec() family, including:

execl()
execv()
execle()
execve()
execlp()
execvp()

They differ mainly in how arguments and environment variables are supplied and whether the executable is searched for through the PATH environment variable.

For example:

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

This asks the operating system to replace the current program with /bin/ls and provide -l as an argument.

Another commonly used form is:

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

Here, execvp() searches for ls using the directories specified in PATH.

6. Why fork() and exec() Are Often Used Together

The real power of these system calls becomes apparent when they are combined.

A UNIX shell, for example, needs to execute commands entered by the user. The shell itself must continue running after launching a command. Therefore, it commonly follows a sequence similar to:

Shell
  |
  | fork()
  |
  +----------------+
  |                |
Parent             Child
Shell              Process
 |                  |
 |                  | exec()
 |                  |
 |                  +--> Requested command
 |
Continue

The parent process remains the shell, while the child process replaces its program image with the requested command.

For example, when a user enters:

ls -l

the shell can create a child using fork(). The child can then use exec() to execute the ls program.

The parent shell may subsequently call wait() or waitpid() if it needs to wait for the command to finish.

7. fork() Does Not Replace the Program

It is important to distinguish the two operations.

Suppose a program contains:

fork();
printf("Hello\n");

After a successful fork(), both parent and child execute the printf() statement. Therefore, the output can normally appear twice.

In contrast, consider:

execvp("ls", args);
printf("This may not execute.");

If execvp() succeeds, the original program is replaced by ls. The following printf() statement is therefore not executed as part of the original program.

This is one of the most important characteristics of exec().

8. What Happens When exec() Succeeds?

When an exec() function succeeds, it normally does not return to the calling program.

The operating system loads the requested executable and establishes a new program image for the existing process. This includes replacing elements such as:

  • Program code

  • Program data

  • Stack contents

  • Heap contents

  • Program entry point

The process ID generally remains unchanged.

If exec() fails, however, it does return, usually with a value of -1, and the program can inspect errno to determine the reason for failure.

Example:

if (execvp("ls", args) == -1) {
    perror("exec failed");
}

9. Relationship Between fork() and exec()

The distinction can be summarized as follows:

Feature fork() exec()
Creates a new process Yes No
Replaces program image No Yes
Creates a new PID Yes No
Parent continues separately Yes Not applicable
Commonly used for command execution Yes Yes
Can be used independently Yes Yes

In a typical UNIX command execution model, fork() is used to create the process and exec() is used to make that new process run the desired program.

10. File Descriptors and fork()/exec()

File descriptors are particularly important when these system calls are used by shells and server applications.

When a process calls fork(), the child inherits copies of the parent's file descriptors. These can be used for standard input, standard output, standard error, files, pipes, sockets, and other resources.

This behavior allows a shell to establish redirection before calling exec().

For example:

ls > output.txt

The shell can create a child process, redirect the child's standard output to output.txt, and then execute ls.

Conceptually:

Shell
 |
 +-- fork()
       |
       Child
       |
       +-- Redirect stdout
       |
       +-- exec(ls)

As a result, ls writes its output to the file instead of directly to the terminal.

11. fork() and exec() in UNIX Shells

A simplified shell workflow looks like this:

Read command
     |
Parse command
     |
   fork()
   /    \
Parent  Child
  |       |
wait()   exec()
          |
       Program

For a foreground command, the shell generally waits for the child to finish. For a background command, the shell can allow the child to continue while immediately accepting another command.

This basic mechanism explains why fork() and exec() are fundamental to UNIX command-line environments.

12. Error Handling

Both calls can fail and should therefore be checked carefully.

A fork() call may fail because the system cannot create another process, for example because of process limits or insufficient system resources.

An exec() call may fail because:

  • The executable does not exist.

  • The process does not have permission to execute it.

  • The executable format is invalid.

  • A required path cannot be found.

  • The system encounters another execution-related error.

Good UNIX programs therefore check return values and handle errors appropriately.

13. fork() and exec() Compared with Windows Process Creation

The UNIX model is notable because process creation and program replacement are conceptually separated.

UNIX commonly uses:

fork() → create process
exec() → replace program

This separation gives applications considerable flexibility. A parent can create a child, modify its file descriptors, change its environment, establish communication channels, change directories, adjust certain process attributes, and then call exec().

This model is particularly powerful for shells and server applications.

14. Practical Importance

Understanding fork() and exec() is useful for several areas of UNIX programming:

  • Shell development

  • System programming

  • Server and daemon development

  • Process management

  • Interprocess communication

  • Input/output redirection

  • Pipeline implementation

  • Job control

  • Process supervision

  • Operating-system programming

For example, a UNIX pipeline such as:

cat file.txt | grep error | sort

requires multiple processes and communication channels. The shell can create processes using fork(), connect their standard input and output through pipes, and then use exec() to run the individual commands.

Conclusion

fork() and exec() form one of the most important process-management mechanisms in UNIX. fork() creates a new child process based on the existing process, while exec() replaces the program running inside a process with another executable program. They are frequently combined so that a parent process, such as a shell, can remain active while a child process executes a different command. Understanding their behavior, return values, memory handling, file-descriptor inheritance, and relationship to shells provides a strong foundation for UNIX system programming.