The Linux system is written using a combination of C language and assembly, and system calls are encapsulated within the C standard library. fork(), wait(), and exec() are all system calls. Specifically, fork() is used to create a child process, exec() is used to execute a program within a process, and wait() is used to block the parent process until a child process terminates. In this article, we will discuss how to use these system calls and get a glimpse of how processes work. This post is best read in conjunction with my previous post on Process Concepts.

fork()

fork() is used to create a new child process. The function returns 0 in the child process if creation is successful, and a negative value in the parent process if creation fails.

Let’s look at a code snippet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

int main(){
int apple = 5; // 5 apples
pid_t pid;

pid = fork(); // Create a child process
if(pid < 0){ // If creation failed
printf("Error: creating child process");
exit(EXIT_FAILURE); // Exit
}

if(pid == 0){// If creation was successful and we are in the child process
// All operations inside this if-block belong to the child process.
apple -= 4; // Eat 4 apples
}else if(pid > 0){ // If we are in the parent process (pid > 0, child PID)
printf("There are %d apples", apple); // Output the number of apples
}
}

Guess whether the output is 1 or 5.

When we create a child process, the entire code, including all current parameters (variables and their values), is copied into the child process. Thus, any changes the child process makes are only effective within the child process itself. See the diagram below:

p1

The answer is “There are 5 apples”. Did you guess correctly?

Among these parameters, the only difference is the pid. In the child process, the pid returned by fork() is 0, while in the parent process, it is the process ID of the newly created child process. It is important to understand that the child process’s scope is not limited to that if statement; it does not cease to be a child process once it moves outside the if block. To prove that both the child and parent processes possess their own copy of the apple variable, and that they are independent, try the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main(){
int apple = 5; // 5 apples
pid_t pid;

pid = fork(); // Create a child process
if(pid < 0){ // If creation failed
printf("Error: creating child process");
exit(EXIT_FAILURE); // Exit
}

if(pid == 0){// If creation was successful
// All operations inside this if-block belong to the child process.
apple -= 4; // Eat 4 apples
}
printf("There are %d apples\n", apple); // Output the number of apples
}

The shell will show two lines of output:

1
2
There are 5 apples
There are 1 apples

(The order of the two outputs is not guaranteed.)

One output is from the parent process, where the apples were not eaten; the other is from the child process, where four apples were eaten. As you can see, code outside the if statement is executed by both processes. The if statement is only used as a conditional to separate code that should run in the child process from code that should run in the parent process. Next, let’s talk about exec().

exec()

exec() is a general name for a family of functions used to execute a specific file. They include execl(), execlp(), execle(), execv(), execvp(), and execvpe(). I will follow up with a post about the file system later. For now, assume that exec() can be used to run a program.

With so many functions, which one should I use? Don’t panic. With exec as the prefix, we can determine the correct function by looking at the remaining letters:

  • l stands for a variable length List of arguments, and v stands for an Vector (an array of arguments).
  • Functions with e mean you need to set the Environment; functions without e do not.
  • Functions with p mean you input the filename of the executable, and it will search for it in your $PATH environment variable. Functions without p require the full Path to the executable file.

The code snippet below demonstrates the difference between execlp() and execvp(). The program outputs two identical results: one from the child process and one from the parent process. What’s different is that the child process uses execlp(), and the parent process uses execvp(). According to the rules above, execlp() accepts a list of arguments and a filename, while execvp() accepts an argument array (vector) and a filename. You can try to change to different functions to see their effects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(){
char args1[] = "ls";
char args2[] = "-la";

char* command[] = {args1, args2, NULL};
pid_t pid1;

pid1 = fork();
if(pid1 < 0){
printf("Error: creating child process");
}

if(pid1 == 0){
// execlp takes n arguments, where the first is the filename, followed by the command arguments.
// The last argument must be NULL.
execlp(args1, args1, args2, NULL);
}else{
// Parent process
wait(NULL);
// execvp takes two arguments, one is the filename, and the other is the argument array (vector).
// The last element of the array must be NULL.
execvp(args1, command);
}
}

The purpose of the NULL argument is to tell the function that this is the end of the argument list.

wait()

We used wait() in the previous section without a detailed introduction. Its usage is also straightforward and is commonly paired with fork(). wait(NULL) means to wait for any child process to terminate. If wait() is not used, the main (parent) process will not be affected and will continue execution. However, when the child process terminates, its resources are not fully reclaimed, and it becomes a zombie process^1. The return value of wait() is the ID of the terminated child process.

If you remember writing your very first Hello World program, you surely remember the return 0; at the end of the main() function. Textbooks will tell you: “return 0 means this function returns the value 0,” but you might not understand why 0 is returned, or to whom. Now that you understand the concepts of parent and child processes, you know that this 0 is returned to the parent process to inform it of the child process’s exit status.

To catch this return value, we need to define an integer status. We will use this variable later to determine the child process’s exit status.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

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

if(pid < 0){
printf("Error: creating child process");
}

if(pid == 0){
return 0; // Try changing this return value and check the output.
}else{
int status;
int id = wait(&status);
printf("Child process ID: %d\n", id);
printf("Raw child exit status: %d\n", status);
// Use the WEXITSTATUS macro to get the actual exit value (e.g., the 0 from 'return 0;')
printf("Decoded child exit value is: %d\n", WEXITSTATUS(status));
}
}

WEXITSTATUS is a predefined macro in <sys/wait.h> that is used to extract the exit code from the process status. There are many other macros available; for details, check here.

Here is a big question that you must not get confused about:

  1. The return value of wait() is not the child process’s exit code, but the child process’s ID.
  2. status is not the child process’s exit code itself, but a collection of information about the process’s state.

Additionally, it is worth noting that the wait() function does not just wait for the child process to terminate, but returns when there is a state transition in the child process. According to the documentation:

A state change is considered to be: the child terminated; the child was stopped by a signal; or the child was resumed by a signal^2.

A process is considered to have had a state transition when the child terminates, is suspended by a signal, or is reactivated by a signal.

Let’s summarize:

  1. To create a child process, we use fork(), and this function’s return value tells us whether the creation was successful and in which process we are.
  2. To execute a program within a child process, we use exec(). This function has multiple variations; the specific function to use depends on the parameters you have at hand.
  3. wait() is used by the parent process to wait for and inquire about the status of its child process.