When you type a command such as ls -l into a Linux shell, the shell usually does not transform itself into ls. Instead, Unix-like systems traditionally split the job into two operations: fork() creates a child process, then an exec() function replaces that child’s current program with the program you actually asked to run.
This is the next step after understanding systemd and Linux services. systemd can request that a service start, but below that management layer the operating system still has to create processes, load executable code, connect file descriptors, schedule CPU time, and hand control to the new program. fork() and exec() are central pieces of that path.
The Short Version: Create, Then Replace
fork() creates a new process by making a child from the calling process. After a successful fork, both parent and child continue executing from the instruction after the fork call. The child gets a new process ID, or PID, while keeping a relationship to the parent through its parent PID, or PPID.
exec() is different. It does not create another process. An exec-family call replaces the program image inside the process that called it. The process keeps its PID, but its code, data, stack, loaded executable, and much of its userspace memory are replaced by the new program. The Linux/POSIX fork documentation explicitly describes the common pattern in which fork is followed by an exec function when the goal is to run a different program.

What fork() Actually Returns
The clever part of fork() is that the same call returns in two processes. The return value tells the code which side it is running on. In the parent, fork returns the child’s PID. In the child, fork returns 0. If creation fails, the parent receives -1 and no child is created.
#include <stdio.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
printf("child: PID=%d PPID=%d\n", getpid(), getppid());
} else {
printf("parent: PID=%d child=%d\n", getpid(), pid);
}
return 0;
}
Because both processes continue independently, their exact print order is not guaranteed. The CPU scheduler decides which runnable process gets CPU time next. That is why process creation and process scheduling are related but different operating-system jobs.
fork() Does Not Normally Copy Every Byte of RAM Immediately
At first glance, duplicating a large process sounds expensive. Modern Linux avoids blindly copying all physical memory by using copy-on-write. Parent and child initially reference the same physical memory pages where possible. The kernel protects those mappings so that if either process tries to modify a shared page, a private copy is created for the writer at that point.
This is especially useful for the common fork-then-exec pattern. If the child immediately calls exec, most of the inherited memory was only needed for a tiny window and can be discarded when the new executable is loaded. The Linux kernel therefore avoids doing a huge amount of work that the child might immediately throw away.
exec() Keeps the Process but Replaces the Program
There is no single userspace function literally named just exec(). C libraries expose an exec family such as execl(), execv(), execvp(), and execve(). On Linux, execve() is the underlying system call that loads the new executable. Its manual explains that the calling program is replaced by a new program with a newly initialized stack, heap, and data segments.
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("before exec\n");
execl("/bin/ls", "ls", "-l", NULL);
/* Reached only if exec failed. */
perror("execl");
return 1;
}
If execl() succeeds, the original program does not continue to the next line. The process is now running /bin/ls. That path connects naturally to the Linux filesystem: BitcoinVersus.Tech previously covered the /bin directory, while the shell may also search locations listed in the PATH environment variable when you type a command without a full pathname.
Why File Descriptors Matter Between fork() and exec()
A child created by fork inherits copies of the parent’s open file descriptors. That is one of the reasons the split between fork and exec is so powerful. The child can change where standard input, standard output, and standard error point before loading the new program.
0 = standard input
1 = standard output
2 = standard error
A shell implementing command > output.txt can fork, open the file, duplicate that file descriptor onto descriptor 1, then exec the requested command. The new program starts with stdout already pointing at the file. The program does not need to know that the shell rewired it.
The same mechanism powers pipelines. For producer | consumer, the shell can create a pipe, fork children, connect the producer’s stdout to the pipe’s write end and the consumer’s stdin to the read end, then exec each program. This is closely related to the broader Linux rule explained in our file descriptor explainer: files, pipes, sockets, and devices can all be represented through descriptor numbers inside a process.
The Parent Usually Calls wait()
After launching a child, a shell or service manager often needs to know when that child finishes. Functions such as wait() and waitpid() let the parent collect the child’s termination status. Without that bookkeeping, an exited child can remain represented by a small kernel record called a zombie process until the parent collects its status.
pid_t pid = fork();
if (pid == 0) {
execl("/bin/ls", "ls", "-l", NULL);
_exit(127);
}
if (pid > 0) {
int status;
waitpid(pid, &status, 0);
}
This parent-child relationship is also why tools such as top, ps, process trees, and service managers can show hierarchies instead of treating every process as unrelated.
What Happens When You Type ls -l?
- The shell is already running. It reads your command line.
- The shell parses the command. It identifies
ls, the-largument, redirections, pipes, and background operators if present. - The shell resolves the executable. It may use PATH to find something such as
/usr/bin/ls. - The shell calls fork. A child process is created.
- The child prepares I/O. File descriptors can be redirected or connected to pipes.
- The child calls exec. Its shell program image is replaced by
ls. - The kernel schedules the child. The
lsprogram runs and writes its output. - The parent waits. For a foreground job, the shell normally waits until the child completes before presenting another prompt.
You Can Watch execve() With strace
strace makes this less abstract because it can display system calls made by a program. The official strace project describes it as a diagnostic and debugging utility for tracing system calls and signals.
strace -f -e trace=process bash -c 'ls -l'
# Or focus on exec:
strace -f -e trace=execve bash -c 'ls -l'
On a typical system you will see an execve() for the program being launched. The exact fork-related syscall shown by tracing can vary because Linux and its C library may use lower-level primitives such as clone() underneath higher-level process-creation APIs.
fork() Is Not the Only Way to Launch Work
The classic Unix model is not the only process-creation interface. POSIX also provides posix_spawn(), and Linux exposes primitives such as clone() that can create processes or threads with more control over what is shared. Libraries and runtimes may choose different implementations depending on performance, threading, portability, and security requirements.
The distinction matters especially in large multithreaded programs. After a process with multiple threads forks, the child starts with only the thread that called fork, while inherited userspace synchronization state can be awkward. That is one reason modern runtimes often prefer spawn-style APIs for straightforward “launch this program” work.
How This Connects Back to systemd
When you run systemctl start example.service, systemd handles the service-level policy: dependencies, environment, privileges, cgroups, restart rules, logging, and lifecycle. Beneath that policy, Linux still has to create and execute the service process. Understanding fork/exec makes service managers less mysterious because you can separate who decided the program should start from how the operating system actually creates and loads the process.
The Bottom Line
fork() creates a child process; exec() replaces a process’s current program with a new one. The combination lets a shell or service manager prepare a child—rewire file descriptors, change directories, adjust credentials, connect pipes, set environment variables—before loading the requested executable. Copy-on-write keeps the fork step from blindly duplicating every memory page, and wait() lets the parent track how the child finishes.
The next useful rabbit hole is what exactly is execve() loading? That leads into ELF executable files, the dynamic linker, shared libraries, argv, environment variables, virtual memory mappings, and how the kernel hands execution to a new program.
Primary technical references: the Linux/POSIX fork() documentation and the Linux execve() manual.
Editor’s Note
This article describes the classic Unix/Linux process-launch model at an introductory level. The exact kernel syscall sequence can differ by architecture, C library, shell, threading model, and whether software uses fork, vfork, clone, posix_spawn, or another runtime abstraction.
We volunteer daily to ensure the credibility of the information on this platform is Verifiably True. If you would like to support our research initiatives, please donate here: 3C9o19EH5HSiwEPyCTmEKzxhNCbo2X6TTb
BitcoinVersus.tech is not a financial advisor. This media platform reports on financial and technology subjects purely for informational purposes.

Leave a comment