C Processes, fork() & exec(): System-Level Multitasking and IPC

C Processes, fork() & exec(): System-Level Multitasking and IPC
Table of Contents
- Processes vs Threads: The Isolation Trade-off
- fork(): Creating a Child Process
- Copy-on-Write: Why fork() Is Fast
- exec(): Replacing the Process Image
- The fork+exec Pattern: How Shells Work
- waitpid(): Preventing Zombie Processes
- Pipes: Inter-Process Communication
- Shared Memory: High-Speed IPC
- Process Groups and Session Leaders
- Real-World Case Studies
- Frequently Asked Questions
- Key Takeaway
Processes vs Threads: The Isolation Trade-off
| Feature | Thread | Process |
|---|---|---|
| Memory | Shared heap + own stack | Completely separate address space |
| Communication | Direct (shared variables) | IPC needed (pipe, socket, shm) |
| Creation cost | ~µs (stack allocation) | ~ms (address space copy) |
| Fault isolation | One crash kills all threads | One crash doesn't affect others |
| Security | No isolation | Full OS-level isolation |
| Use case | CPU parallelism, I/O overlap | Reliability, multi-user systems |
fork(): Creating a Child Process
fork() duplicates the current process. After the call, both parent and child are running:
Critical: After fork(), both parent and child continue executing from the line after the fork() call. The return value of fork() is the only way to determine which process you're in.
Copy-on-Write: Why fork() Is Fast
A naive implementation of fork() would copy the entire parent's address space (potentially gigabytes). Modern kernels use Copy-on-Write (CoW):
- After
fork(), both parent and child share the same physical pages with read-only mappings. - When either process writes to a page, the kernel copies that specific page and gives the writing process its own private copy.
- Pages that are never modified are never copied.
For a 100MB process, fork() costs only microseconds (just page table copies), not hundreds of milliseconds of actual memory copying.
exec(): Replacing the Process Image
exec() family replaces the current process's code, data, and stack with a new program. The PID remains the same, but everything else is completely new:
Key fact: If exec succeeds, it never returns — the current process is completely replaced. If it returns, that means it failed (and errno is set).
The fork+exec Pattern: How Shells Work
Every shell (bash, zsh, fish) uses the fork+exec pattern to run commands:
waitpid(): Preventing Zombie Processes
When a child process exits, it doesn't disappear immediately. It becomes a zombie — its entry remains in the process table until the parent calls wait() or waitpid() to collect its exit status:
Preventing zombie accumulation in long-running servers:
Pipes: Inter-Process Communication
Pipes are unidirectional byte streams connecting two processes. They are the oldest and simplest form of IPC:
This is exactly how shell pipelining works: ls -l | grep ".c" — ls writes to a pipe, grep reads from it.
Shared Memory: High-Speed IPC
For high-throughput IPC (database shared buffers, multimedia pipelines), POSIX shared memory lets two processes share a physical memory page — zero copy:
PostgreSQL's shared_buffers uses shared memory — the database buffer pool is one large shmget segment shared among all backend worker processes.
Real-World Case Studies
| System | Strategy | Why |
|---|---|---|
| Google Chrome | One process per tab | Crash isolation: one bad page doesn't kill browser |
| Nginx | Master + N worker processes | Workers can be killed/restarted without downtime |
| Apache (prefork) | One process per request | Isolation between HTTP clients |
| PostgreSQL | One process per connection | Client crashes don't affect the database engine |
| Bash shell | fork+exec for every command | Clean separation of shell state from command state |
| Android | One Zygote + fork per app | Fast app startup via CoW from pre-loaded runtime |
Frequently Asked Questions
What happens if the parent exits before the child?
The child becomes an orphan. The Linux kernel automatically re-parents it to init (PID 1) or the current subreaper, which will call wait() to clean it up. Orphans don't cause problems — unreaped zombies do.
Can processes share memory safely without explicit shared memory?
No. After fork, parent and child have separate address spaces (with CoW). Writing to shared_var in the child doesn't affect the parent's copy. Use pipes, shared memory (shm_open), sockets, or memory-mapped files for actual sharing.
What is a daemon process?
A daemon is a background process that: detaches from its controlling terminal, creates a new session (setsid()), changes to the root directory (chdir("/")), and redirects stdin/stdout/stderr to /dev/null. System services (sshd, nginx, postgrad) run as daemons.
How does vfork differ from fork?
vfork creates a child that temporarily shares the parent's address space (no CoW) and suspends the parent until the child calls exec or exit. It's ultra-fast but extremely dangerous — the child must not access or modify any variables, as it uses the parent's stack. Use only immediately followed by exec.
Key Takeaway
fork() and exec() are the Foundation of Unix Multitasking. They are the primitive operations from which shells, web servers, and databases are built. Process isolation — the guarantee that one process's crash, bug, or malicious behavior cannot corrupt another — is one of operating systems' most important security properties.
Understanding fork, exec, waitpid, and IPC mechanisms positions you to build multi-process server architectures that are both high-performance and fault-tolerant.
Read next: Signals & Interrupt Handling: Trapping OS Events →
Part of the C Mastery Course — 30 modules from C basics to expert systems engineering.
