1. Overview
System calls provide an interface to the services made available by an operating system. The system calls fork(), vfork(), exec(), and clone() are all used to create and manipulate processes.
In this tutorial, we’ll discuss each of these system calls and the differences between them.
2. fork()
Processes execute the fork() system call to create a new child process.
The process executing the fork() call is called a parent process. The child process created receives a unique Process Identifier (PID) but retains the parent’s PID as its Parent Process Identifier (PPID).
The child process has identical data to its parent process. However, both processes have separate address spaces.
After the creation of the child process, both the parent and child processes execute simultaneously. They execute the next step after the fork() system call.
Since the parent and child processes have different address spaces, any modifications made to one process will not reflect on the other.
Later improvements introduced the copy-on-write mechanism, which allows the parent and child processes to share the same address space. This removed the need to copy data to the child process. If any process modifies the pages in the shared address space, the system allocates a new address space that allows both processes to run independently.
2.1. Executing fork()
Let’s create a simple C program that shows us how the fork() system call works.
First, we create a file named fork_test.c with the Nano editor:
$ nano fork_test.c
Next, we add this content:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
int main(int argc, char **argv) {
pid_t pid = fork();
if (pid==0) {
printf("This is the Child process and pid is: %d\n",getpid());
exit(0);
} else if (pid > 0) {
printf("This is the Parent process and pid is: %d\n",getpid());
} else {
printf("Error while forking\n");
exit(EXIT_FAILURE);
}
return 0;
}
Here, we’re starting a new process and using the variable pid to store the process identifier of the child process created by the fork() call. We then proceed to check if the value of pid returned by the fork() call is equal to zero. The fork() call returns the value of the child process as zero to differentiate it from its parent. The actual value of the child process identifier is the value returned to the parent process. Finally, we check for errors and print an error message.
After saving the changes, we use the cc command to compile fork_test.c:
$ cc fork_test.c
This creates an executable file called a.out in the working directory.
Finally, we can execute the a.out file:
$ ./a.out
This is the Parent process and pid is: 69032
This is the Child process and pid is: 69033
We can see here that the parent and child processes have different process identifiers.
In the output above, the fork() call returns the output twice, once in the parent process and once in the child process.
We’re using the getpid() function call to get the actual PID of the parent and child processes in the if…else block.
3. vfork()
The vfork() system call was first introduced in BSD v3.0. It’s a legacy system call that was originally created as a simpler version of the fork() system call. This is because executing the fork() system call, before the copy-on-write mechanism was created, involved copying everything from the parent process, including address space, which was very inefficient.
Similar to the fork() system call, vfork() also creates a child process that’s identical to its parent process. However, the child process temporarily suspends the parent process until it terminates. This is because both processes use the same address space, which contains the stack, stack pointer, and instruction pointer.
vfork() acts as a special case of the clone() system call. It creates new processes without copying the address space of the parent process. This is useful in performance-oriented applications.
The parent process is always suspended once the child process is created. It remains suspended until the child process terminates normally, abnormally, or until it executes the exec system call starting a new process.
The child process created by the vfork() system call inherits its parent’s attributes. These include file descriptors, current working directory, signal dispositions, and more.
3.1. Executing vfork()
Let’s create a simple C program to show how the vfork() system call works.
First, we create a file named vfork_test.c:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
pid_t pid = vfork(); //creating the child process
printf("parent process pid before if...else block: %d\n", getpid());
if (pid == 0) { //checking if this is the a child process
printf("This is the child process and pid is: %d\n\n", getpid());
exit(0);
} else if (pid > 0) { //parent process execution
printf("This is the parent process and pid is: %d\n", getpid());
} else {
printf("Error while forking\n");
exit(EXIT_FAILURE);
}
return 0;
}
Here, we’re using variable pid to store the PID of the child process created by the vfork() call. We then check to see the value of the parent’s PID before the if…else block.
After saving the changes, let’s compile vfork_test.c:
$ cc vfork_test.c
Finally, we can execute the created a.out file:
$ ./a.out
parent process pid before if...else block: 117117
This is the child process and pid is: 117117
parent process pid before if...else block: 117116
This is the parent process and pid is: 117116
The vfork() system call returns the output twice, first in the child process and then in the parent process.
Since both processes share the same address space, we have matching PID values in the first output. In the if else block, the child process is run first because it blocks the parent process while executing.
4. exec()
The exec() system function runs a new process in the context of an existing process and replaces it. This is also referred to as an overlay.
The function doesn’t create a new process, so the PID doesn’t change. However, the new process replaces the data, heap, stack, and machine code of the current process. It loads the new process into the current process space and executes it from the entry point. Control never returns to the original process unless there’s an exec() error.
This system function belongs to a family functions that includes execl(), execlp(), execv(), execvp(), execle(), and execve().
4.1. Executing exec()
For a simple test, we’ll create two C programs to show how the exec() system call works. We’ll use the exec() call to run the second program from the first program.
Let’s create the first program named exec_test1.c:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
printf("PID of exec_test1.c = %d\n", getpid());
char *args[] = {"Hello", "From", "Parent", NULL};
execv("./exec_test2", args);
printf("Back to exec_test1.c");
return 0;
}
Here, we’re creating a function called main() and passing in arguments. Inside the function, we’re printing the PID after fetching it using the getpid() function. We then declare a character array where we pass in three strings as arguments. We’re calling the execv() system call, then passing in the results of executing the second program as an argument.
Let’s now create the second program called exec_test2.c:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
printf("Hello from exec_test2.c\n");
printf("PID of exec_test2.c process is: %d\n", getpid());
return 0;
}
Here, we’re printing a message and the PID of the process launched by exec() from the first program.
We use the cc command to compile exec_test1.c to an executable:
$ cc exec_test1.c -o exec_test1
This creates an executable file called exec_test1 in the working directory.
Then, we compile the second program:
$ cc exec_test2.c -o exec_test2
This creates an executable file called exec_test1 in the working directory.
Finally, we can execute the exec_test1 file:
$ ./exec_test1
PID of exec_test1.c = 171939
Hello from exec_test2.c
PID of exec_test2.c process is: 171939
From the output above, we can notice that the PID didn’t change in the second program’s process. Furthermore, the last print statement from exec_test1.c file wasn’t printed. This is because executing the execv() system call replaced the currently running process, and we haven’t included a way of returning back to the first process.
5. clone()
The clone() system call is an upgraded version of the fork call. It’s powerful since it creates a child process and provides more precise control over the data shared between the parent and child processes. The caller of this system call can control the table of file descriptors, the table of signal handlers, and whether the two processes share the same address space.
clone() system call allows the child process to be placed in different namespaces. With the flexibility that comes with using the clone() system call, we can choose to share an address space with the parent process, emulating the vfork() system call. We can also choose to share file system information, open files, and signal handlers using different flags available.
This is the signature of the clone() system call:
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
Let’s breakdown some parts to understand more:
- *fn: pointer that points to a function
- *stack: points to the smallest byte of a stack
- pid_t: process identifier (PID)
- *parent_tid: points to the storage location of child process thread identifier (TID) in parent process memory
- *child_tid: points to the storage location of the child process thread identifier (TID) in the child process memory
5.1. Executing clone()
Let’s create a simple C program to see how the clone() system call works.
First, we create a file named clone_test.c:
// We have to define the _GNU_SOURCE to get access to clone(2) and the CLONE_*
#define _GNU_SOURCE
#include <sched.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int child_func(void* arg) {
char* buffer = (char*)arg;
printf("Child sees buffer = \"%s\"\n", buffer);
strcpy(buffer, "hello from child");
return 0;
}
int main(int argc, char** argv) {
// Allocate stack for child task.
const int STACK_SIZE = 65536;
char* stack = malloc(STACK_SIZE);
if (!stack) {
perror("malloc");
exit(1);
}
// When called with the command-line argument "vm", set the CLONE_VM flag on.
unsigned long flags = 0;
if (argc > 1 && !strcmp(argv[1], "vm")) {
flags |= CLONE_VM;
}
char buffer[100];
strcpy(buffer, "hello from parent");
if (clone(child_func, stack + STACK_SIZE, flags | SIGCHLD, buffer) == -1) {
perror("clone");
exit(1);
}
int status;
if (wait(&status) == -1) {
perror("wait");
exit(1);
}
printf("Child exited with status %d. buffer = \"%s\"\n", status, buffer);
return 0;
}
Here, we’re using clone() in two ways, once with the CLONE_VM flag and once without. We’re passing a buffer into the child process, and the child process writes a string to it. We then allocate a stack size for the child process and create a function that checks whether we’re executing the file using the CLONE_VM (vm) option. Furthermore, we’re creating a buffer of 100 bytes in the parent process and copying a string to it, then executing the clone() system call and checking for errors.
We use the cc command to compile exec_test.c to an executable:
$ cc clone_test.c
This creates an executable file called a.out in the working directory.
Finally, we can execute the a.out file:
./a.out
Child sees buffer = "hello from parent"
Child exited with status 0. buffer = "hello from parent"
When we execute it without the vm argument, the CLONE_VM flag isn’t active, and the parent process virtual memory is cloned into the child process. The child process can access the message passed by the parent process in buffer, but anything written into buffer by the child isn’t accessible to the parent process.
But, when we pass in the vm argument, CLONE_VM is active and the child process shares the parent’s process memory. We can see it writing into buffer:
$ ./a.out vm
Child sees buf = "hello from parent"
Child exited with status 0. buf = "hello from child"
This time our message is different, and we can see the message passed from the child process.
6. Comparison
Let’s look at this table showing an overview and comparison of each of these system calls:
Comparison Factor
fork()
vfork()
exec()
clone()
Invoking
fork(), creates a child process of the invoking process
vfork(), creates a child process that has shares some attributes with the parent
exec(), replaces the invoking process
clone(), creates a child process and offers more control on data shared
Process ID
Parent process and child process have unique IDs
Parent process and child process have the same ID
The process running and the process that replaces it, have the same PID
Parent process and child process have unique IDs but can share when specified
Execution
Parent and child processes start simultaneously
Parent process is temporarily suspended while child process runs
Parent process is terminated and the new process starts at entry point
Parent and child processes start simultaneously
7. Conclusion
In this article, we’ve discussed the differences and similarities between the fork(), vfork(), exec(), and clone() system calls.
We saw that fork(), vfork(), and clone() work similarly, with minor differences in how they handle data. We also built and executed a few simple C programs demonstrating how each system call operates.
The vfork() system call is considered obsolete and it’s better to use the fork() system call, especially since it has the copy-on-write feature.