Welcome back. In our previous lesson, we examined the context switch, the fundamental OS mechanism that enables multitasking. We saw that this operation, while essential, carries a significant performance cost, especially when switching between processes with separate memory spaces. This naturally leads to the question: if creating and switching between processes is so expensive, how do programs actually request these services from the OS?
This lesson answers that question by exploring the core of process management: system calls. We will describe the role of system calls as the exclusive interface between user applications and the operating system kernel. Specifically, we will focus on three cornerstone system calls in UNIX-like systems: fork(), exec(), and wait(). By the end of this lesson, you will be able to describe how these calls work together to create, run, and manage processes, forming the foundation of how programs are launched in almost every modern operating system.
System Calls: The Gateway to the Kernel
An operating system protects its own integrity and ensures fair resource allocation by enforcing a strict boundary between user applications and the kernel. This is typically achieved through two distinct CPU modes:
- User Mode: The normal mode where applications run. In this mode, the CPU has restricted access to hardware and memory to prevent a misbehaving program from crashing the entire system.
- Kernel Mode: A privileged mode where the OS kernel runs. In this mode, the CPU has unrestricted access to all hardware and memory.
A user program cannot directly create a process or access a file on the disk. To perform such privileged operations, it must make a request to the kernel. This formal request mechanism is the system call. When an application makes a system call, the CPU transitions from user mode to kernel mode, a process that involves a context switch into the kernel. The kernel then executes the requested service on the application's behalf and returns the result, switching back to user mode.
The following article provides a concise overview of this crucial relationship between programs and processes, and the role of system calls as the bridge between user mode and kernel mode.
fork, exec, wait and exit - Percona Community
This blog post from the Percona Community clearly distinguishes between a program and a process and explains the transition from user mode to kernel mode during a system call.
Please read the introductory section, starting from the beginning, to solidify the distinction between a program on disk and a process in execution. Then, read the next section, titled Usermode and Kernel, to understand how a system call triggers a context switch into the kernel.
Creating a New Process: The fork() System Call
In UNIX and its derivatives (including Linux and macOS), the sole method for creating a new process is the fork() system call. Its behavior is one of the most unique and powerful concepts in operating systems. When a process (the parent) calls fork(), the OS creates a new process (the child) that is a near-perfect duplicate of the parent.
This duplication includes the parent's address space (code, global variables, heap, and stack), file descriptors, and environment variables. The child process doesn't start from the main() function; instead, its execution begins as if it had also just returned from the fork() call.
The key to managing this is the return value of fork():
- In the parent process,
fork()returns the Process ID (PID) of the newly created child. - In the child process,
fork()returns0. - If the creation fails,
fork()returns a negative value in the parent.
This difference in return values allows the program to branch its logic and execute different code in the parent and child.
[PDF] Interlude: Process API - cs.wisc.edu
This chapter from the excellent book Operating Systems: Three Easy Pieces provides a clear, code-driven explanation of fork().
Please read section 5.1, The fork() System Call. Pay close attention to the code example p1.c and how the if-else block uses the return code rc to distinguish between the parent and child paths. Notice the discussion on non-determinism—the OS scheduler decides which process runs first after the fork.
The diagram below provides a visual step-by-step walkthrough of what happens during a fork() call, showing how the parent's state is duplicated to create the child.

Running a Different Program: The exec() System Call
Creating a clone of a process is useful, but it's not the whole story. Most of the time, we want to create a new process to run a different program. This is the job of the exec() family of system calls (e.g., execl(), execvp()).
An exec() call replaces the memory image of the current process with a new program. The new program's code and data are loaded from an executable file, and the process starts executing from the new program's main() function. Crucially, exec() does not create a new process; it transforms the existing one. The PID does not change. If the exec() call is successful, it never returns to the original code, as the original code has been completely overwritten.
The combination of fork() and exec() is the standard UNIX pattern for launching a new program:
- Call
fork()to create a child process. - In the child process, call
exec()to replace the child's program with the new program.
[PDF] Interlude: Process API - cs.wisc.edu
Continuing with the OSTEP chapter, this section introduces exec().
Read section 5.3, Finally, The exec() System Call. The p3.c example demonstrates the classic fork()-then-exec() pattern. The parent forks a child, and the child uses execvp() to run the wc (word count) program.
Synchronizing Processes: The wait() System Call
After a fork(), the parent and child processes run concurrently, and the scheduler determines their execution order. However, a parent often needs to wait for its child to complete a task before it continues. The wait() system call provides this synchronization.
When a parent process calls wait(), the OS suspends the parent until one of its children terminates. Once a child terminates (by calling exit() or finishing main), wait() returns in the parent, providing information about the child's termination, such as its exit status.
Using wait() is critical for two reasons:
- Orchestration: It allows a parent to know when a child's task is done, ensuring a predictable order of operations.
- Resource Cleanup: It's the primary mechanism for cleaning up terminated child processes. A child process that has terminated but whose parent has not yet called
wait()becomes a zombie process. A zombie holds onto its entry in the process table, consuming a system resource. It's the parent's responsibility to "reap" its children withwait()to free this entry.
The following video explains this orchestration and the concept of zombie processes very clearly.
Process creation and termination - fork(), exec(), wait() and exit()
This video explains how wait() is used for parent-child synchronization and what happens when it's not used correctly, leading to zombie processes.
Watch from the introduction to wait to understand how wait() makes a parent process suspend its execution. Then, continue watching the crucial segment from on zombie processes, which explains why calling wait() is essential for proper resource management.
Putting It All Together: How a Shell Works
The separation of fork() and exec() is not an accident; it's a profoundly powerful design. It allows a child process to manipulate its environment after the fork() but before the exec(). The most common example of this is the command-line shell (like bash).
When you type a command, such as cat my_file.txt | grep "error" > results.txt, the shell performs a beautiful dance of process management:
- The shell calls
fork()to create a child process. - In the child, before calling
exec(), it manipulates its file descriptors. It closes standard output and opensresults.txt, and it sets up apipe()to connect to another process. - The child then calls
exec()to runcat,grep, etc. Because file descriptors are inherited across anexec()call, the new program's output is automatically redirected without the program itself needing to be aware of it. - Meanwhile, the parent shell calls
wait()to pause until the child command completes. - Once the child finishes,
wait()returns, and the shell prints a new prompt, ready for your next command.

The elegance of this design is that simple, single-purpose utilities (cat, grep, wc) can be chained together in powerful ways, without any of them needing to know about redirection or pipes. All the complex setup is handled by the shell in the small window of time between fork() and exec().
[PDF] Interlude: Process API - cs.wisc.edu
This final section from the OSTEP chapter explains the motivation behind this powerful API design.
Please read section 5.4, Why? Motivating The API. This brief section crystallizes why separating fork and exec is essential for building a flexible and powerful tool like a UNIX shell.
Conclusion
In this lesson, we explored the fundamental system calls that applications use to manage processes. These operations are the building blocks for multitasking from the application's perspective.
Here are the key takeaways:
- System calls are the protected interface that allows user-mode programs to request privileged services from the kernel, such as process creation.
- The
fork()system call creates a new child process that is a near-identical duplicate of the parent. - The
exec()family of calls replaces the current process's image with a new program, allowing a process to run different code. - The
wait()system call allows a parent to synchronize with a child's termination, retrieve its exit status, and prevent the creation of zombie processes. - The separation of
fork()andexec()is a powerful design pattern that enables features like I/O redirection and pipes, as demonstrated by the UNIX shell.
Now that we understand how processes are created and managed, our next lesson will address a critical question: when multiple processes are ready to run, how does the OS decide which one gets to use the CPU next? We will explore this by comparing several CPU scheduling algorithms, such as First-Come, First-Served (FCFS), Shortest Job First (SJF), and Round Robin.