In the previous parts of this series, we covered system call basics and file I/O. Now it’s time to explore one of the most powerful aspects of Unix-like systems: process management.
Processes are the execution units of programs. Linux provides a set of system calls to create, replace, synchronize and terminate processes. Understanding these is essential for writing multitasking programs, shells, servers and operating system tools.
1. What is a Process?
A process is an instance of a running program. Each process has:
- A process ID (PID)
- Its own memory space (stack, heap, text, data)
- File descriptors
- Environment variables
- Scheduling information
Linux uses system calls to control process life cycles: creation, execution, synchronization, and termination.
Creating Processes: fork()
The fork() system call creates a new process by duplicating the calling process.
#include <unistd.h>
pid_t fork(void);
- On success, returns 0 to the child process.
- Returns the child’s PID to the parent process.
- Returns -1 on error.
Both processes continue execution from the point of the fork() call.
Example:
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork failed");
return 1;
}
else if (pid == 0)
{
printf("Child process (PID = %d)\n", getpid());
}
else
{
printf("Parent process (PID = %d, Child PID = %d)\n", getpid(), pid);
}
return 0;
}
3. Executing Programs: execve() and Family
fork() creates a new process, but it’s just a copy of the parent. To actually run another program, you use the exec family of system calls.
execve()
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
- pathname: path to executable
- argv: argument vector (NULL-terminated)
- envp: environment variables (NULL-terminated)
If successful, execve() does not return, the process image is replaced by the new program.
Example:
#include <unistd.h>
#include <stdio.h>
int main()
{
char *args[] = { "/bin/ls", "-l", NULL };
execve("/bin/ls", args, NULL);
perror("execve failed"); // only reached if error occurs
return 1;
}
Other exec Variants
- execl, execp, execv, etc: convenient wrappers around execve() with slightly different argument styles.
- Most programs use execvp() because it searches the PATH environment variable.
4. Waiting for Child Processes: wait() and waitpid()
When a child process finishes, it becomes a zombie until the parent collects its exit status with wait() or waitpid().
wait()
#include <sys/wait.h>
pid_t wait(int *wstatus);
- Blocks until any child process terminates.
- Returns PID of terminated child.
waitpid()
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
- Can wait for a specific child (pid).
- Supports non-blocking (WNOHANG) and stopped child processes.
Example:
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
printf("Child running...\n");
_exit(42);
}
else
{
int status;
wait(&status);
if (WIFEXITED(status))
{
printf("Child exited with code %d\n", WEXITSTATUS(status));
}
}
return 0;
}
5. Terminating Processes: exit() and _exit()
- exit(status): cleans up stdio buffers and terminates.
- _exit(status): lower-level, terminates immediately (used inside child after fork() if exec fails).
The status is returned to the parent through wait() or waitpid().
6. Process Relationships: Orphans and Zombies
- Zombie process: a terminated child whose exit status has not been collected. It occupies a slot in the process table.
- Orphan process: a child whose parent has terminated. Linux reassigns orphans to init (PID 1), which adopts them and calls wait().
Managing children properly with wait() is crucial to avoid zombies.
7. Example: A Minimal Program Launcher
This example demonstrates how fork(), execvp(), and waitpid() combine to launch commands.
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main()
{
char *args[] = { "ls", "-l", NULL };
pid_t pid = fork();
if (pid == 0)
{
execvp(args[0], args);
perror("exec failed");
_exit(1);
}
else
{
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status))
{
printf("Child exited with code %d\n", WEXITSTATUS(status));
}
}
return 0;
}
8. TL/DR
- fork() duplicates a process.
- execve() replaces the process image with a new program.
- wait() and waitpid() synchronize parent and child processes.
- exit() and _exit() terminate processes with a status code.
- Proper process management prevents zombies and ensures system stability.
Next, in Part 4: Project – Building a Basic Linux Shell, we’ll put all of this together to create a working shell that can run commands, handle redirection and support basic piping.