fork and exec
Create child processes with fork, run programs with exec, and wait for children with waitpid — the Unix process lifecycle in C.
- Use fork to create a child process and handle both parent and child paths
- Use execv or execvp to replace a process image with a different program
- Use waitpid to collect child exit status and avoid zombie processes
- Build the classic fork-exec-wait pattern
The fork/exec combination is how Unix creates new processes. fork copies the current process; exec replaces the copy with a different program. This two-step design is elegant: fork alone is useful for parallel work sharing the same code; the combination of fork then exec is how every shell command is run.
fork
#include <unistd.h>
#include <sys/types.h>
pid_t pid = fork();fork() creates an exact copy of the current process. It returns:
- In the parent: the child's process ID (a positive integer).
- In the child: 0.
- On error: -1 (no child was created).
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
/* Child process */
printf("Child: PID=%d, parent PID=%d\n", getpid(), getppid());
return 0;
} else {
/* Parent process */
printf("Parent: PID=%d, child PID=%d\n", getpid(), pid);
/* wait for child to finish */
int status;
waitpid(pid, &status, 0);
printf("Child exited with status %d\n", WEXITSTATUS(status));
}
return 0;
}After fork, both processes run independently from the same point. The only difference is the return value of fork.
exec — replacing the process image
exec replaces the current process's code and data with a new program. The process ID stays the same, but the program being run changes:
#include <unistd.h>
execvp("ls", (char *[]){"ls", "-la", "/tmp", NULL});
/* If execvp succeeds, this line is never reached */
perror("execvp"); /* only reached on failure */execvp(file, argv) searches PATH for file and runs it with argv (an array of string pointers, terminated by NULL). The last element of argv must be NULL.
The fork-exec-wait pattern
Running an external program:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int run_command(const char *path, char *const argv[]) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return -1;
}
if (pid == 0) {
/* Child: replace with the target program */
execvp(path, argv);
/* Only reached if execvp fails */
perror("execvp");
_exit(127); /* use _exit, not exit -- avoids flushing parent's buffers */
}
/* Parent: wait for child */
int status;
if (waitpid(pid, &status, 0) < 0) {
perror("waitpid");
return -1;
}
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
int main(void) {
char *args[] = {"ls", "-la", "/tmp", NULL};
int ret = run_command("ls", args);
printf("Command returned: %d\n", ret);
return 0;
}waitpid and zombie processes
When a child exits, it becomes a zombie — it holds a slot in the process table with its exit status until the parent calls wait or waitpid. If the parent never waits, the zombie persists until the parent exits.
int status;
pid_t finished = waitpid(pid, &status, 0); /* blocking wait */
pid_t finished = waitpid(-1, &status, WNOHANG); /* non-blocking: returns 0 if no child has exited */WIFEXITED(status) — true if the child exited normally.
WEXITSTATUS(status) — the exit code (0–255).
WIFSIGNALED(status) — true if the child was killed by a signal.
WTERMSIG(status) — the signal number that killed it.
fork without exec — parallel workers
fork without exec creates a child that continues running the same program. This is useful for parallel processing:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int n_workers = 4;
for (int i = 0; i < n_workers; i++) {
pid_t pid = fork();
if (pid == 0) {
/* Each child handles a portion of the work */
printf("Worker %d started (PID %d)\n", i, getpid());
/* do work here */
_exit(0);
}
}
/* Parent waits for all children */
while (wait(NULL) > 0) { }
printf("All workers done\n");
return 0;
}Close file descriptors and mutexes after fork. The child inherits all of the parent's open file descriptors and any locked mutexes. Close file descriptors the child does not need. Never use mutexes across a fork — the child gets a copy of the mutex in whatever state it was in, which can cause deadlocks. Use pthread_atfork to register cleanup handlers.
Where to go next
Next: pipes and inter-process communication — connecting processes with pipes, and the design of Unix pipelines.
Signals and signal handlers
Handle Unix signals in C — installing signal handlers, the restrictions on async-signal-safe code, and using sigaction for reliable signal handling.
Pipes and inter-process communication
Connect processes with Unix pipes — creating pipes with pipe(), redirecting stdout/stdin with dup2(), and the design of Unix pipelines.