Unix is a multi-user, multitasking operating system. It proves a portable, modular experience through a command line interface which allows it to be used in various ways, i.e., it gives the user a flexible experience with the ability to customize their components fully. Moreover, Unix is the basis and/or inspiration for many operating systems, including Linux, macOS, and many more.
Within the operating system, Unix system calls allow user-level programs to interact with system resources. This allows users to bridge the gap between the applications and the operating system. These calls are used for the file system, process, and interprocess control.
Now that we know what system calls are and what they do, let us look at some important ones.
The fork()
system call creates a new child process. A child process is a new process with the same memory contents as its parent; however, it is allocated a separate memory space from the parent process. The newly created child process becomes independent from its parent as soon as it is created. Moreover, the child process starts from where the fork()
call was created; therefore, both processes start executing instructions after the call is executed. The parent process returns the process ID (PID), while the child process returns a zero value for the fork()
command.
#include <stdio.h>#include <stdlib.h>#include <unistd.h>int main(int argc, char *argv[]) {printf("hello world (pid:%d)\n", (int) getpid());int returned_id = fork();if (returned_id < 0) { // fork failed; exitfprintf(stderr, "fork failed\n");exit(1);} else if (returned_id == 0) { // child (new process)printf("hello, I am child (pid:%d)\n", (int) getpid());} else { // parent goes down this path (main)printf("hello, I am parent of %d (pid:%d)\n", returned_id, (int) getpid());}return 0;}
Let us break down the code given above.
Lines 1–7: Imports standard C libraries and initializes the main()
function. It also prints out a "hello world" message with the process ID to let us know which ID the initial process uses.
Line 8: Creates a fork()
and assigns its return value to the returned_id
variable.
Lines 9–11: The if
conditional for when the fork()
created fails. This conditional branch prints the error and a prompt statement. Moreover, it ends the entire program with the exit(1)
command.
Lines 12–13: The if
conditional for when the fork()
encounters the child process. This conditional branch prints the process ID for the child and a prompt statement.
Lines 14–16: The if
conditional for when the fork()
encounters the parent process. This conditional branch prints the process ID for the child and parent and a prompt statement.
Note: This code widget will not give outputs that will show different order and without duplicate prompts (as it should) due to its backend configuration. For that, we can download and run this code on our operating systems.
The wait()
system call creates dependencies between processes. It is useful when we need a parent process to wait for their child. This prevents random order of process execution which can create inconsistencies in our code. We can implement this by using the wait()
command on our parent processes which will delay its execution until the child finishes its execution. After the child process, it will return to the parent, and the parent can proceed as needed.
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>int main(int argc, char *argv[]) {printf("hello world (pid:%d)\n", (int) getpid());int returned_id = fork();if (returned_id < 0) { // fork failed; exitfprintf(stderr, "fork failed\n");exit(1);} else if (returned_id == 0) { // child (new process)printf("hello, I am child (pid:%d)\n", (int) getpid());} else { // parent goes down this path (main)int wait_pid = wait(NULL);printf("hello, I am parent of %d (wait_pid:%d) (pid:%d)\n", returned_id, wait_pid, (int) getpid());}return 0;}
Let us break down the additions in our previous code.
Line 15: This wait()
command forces the parent process to wait for the child process before executing its other commands.
The exec()
system call runs another program without our main program. It does this by loading a new binary image which overwrites code and data. It initializes a new stack, heap, and the required registers. Moreover, it executes from the main()
of the new program, and when successful, it returns no value. The exec()
call takes two parameters: the binary file name and the arguments array.
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <string.h>int main(int argc, char *argv[]) {printf("hello world (pid:%d)\n", (int) getpid());int returned_id = fork();if (returned_id < 0) { // fork failed; exitfprintf(stderr, "fork failed\n");exit(1);} else if (returned_id == 0) { // child (new process)printf("hello, I am child (pid:%d)\n", (int) getpid());char *myargs[3];myargs[0] = strdup("wc"); // word count programmyargs[1] = strdup("file.c"); // file to countmyargs[2] = NULL; // end of arrayexecvp(myargs[0], myargs); // runs word count} else { // parent goes down this path (main)int wait_pid = wait(NULL);printf("hello, I am parent of %d (wait_pid:%d) (pid:%d)\n", returned_id, wait_pid, (int) getpid());}return 0;}
Let us break down the additions in our previous code.
Line 15: Initializes a char array with size 3.
Lines 16–18: Insert appropriate values in the myargs
array.
Line 19: Runs the execvp()
command, which is a version of the exec()
command for Linux machines.
System calls are essential in understanding and manipulating the hardware-level processes through user commands, providing a gateway between the two entities. Moreover, this becomes a vital tool in virtualizing the operating system and its resources, such as CPU, memory, concurrency, and persistence.
We have introduced you to some basic and useful system calls, but there are many more. To learn more, try exploring some more, like the open()
, read()
, write()
, and socket()
call.