C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
143: Process Creation with fork and exec
A few years back, I was mentoring a junior dev who was trying to build a basic shell. He had discovered the system() function and thought he'd found a cheat code. It worked fine for simple things, but the moment he tried to implement piping—sending the output of one command into another—he hit a wall. system() is just a high-level wrapper; it doesn't give you any control over the underlying process. In his attempt to figure it out, he accidentally wrote a fork() call inside a while loop without a proper break condition. He created a "fork bomb" that spawned thousands of processes in seconds, locking up his entire workstation. He had to hard-reboot, and he learned a very visceral lesson about how process creation actually works in Unix.
The Weird Magic of fork()
The first thing you need to wrap your head around is that fork() doesn't start a new program. Instead, it creates a nearly identical copy of the process that called it. Imagine your program suddenly splitting into two identical twins. Both twins are at the exact same line of code, they have the same variables, and they both share the same file descriptors.
The only way you can tell which twin you are is by looking at the return value of fork(). In the parent process, fork() returns the Process ID (PID) of the new child. In the child process, it returns 0. This is where you'll spend most of your time: writing an if statement to tell the parent and child to do different things.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) {
printf("I am the child! My PID is %d\n", getpid());
// Child does something specific here
} else {
printf("I am the parent! I created child %d\n", pid);
// Parent does something else here
}
return 0;
}
Swapping Brains with exec()
Now, usually, you don't actually want two identical copies of your program running forever. You want the child to run a different program entirely—like ls, grep, or gcc. That's where the exec family of functions comes in. When you call execvp or execl, the current process image is completely wiped out and replaced by the new program. It's like the process undergoes a brain transplant.
Crucially, if exec is successful, it never returns. The code following the exec call in the child process simply doesn't exist anymore because that memory has been overwritten by the new executable. If exec actually returns, it means something went wrong (like the file wasn't found), and you need to handle that error.
char *args[] = {"/bin/ls", "-l", "/usr/bin", NULL};
if (execvp(args[0], args) == -1) {
perror("Execution failed");
}
Cleaning Up the Mess
Here is where people often get lazy and end up with "zombie processes." When a child process finishes, it doesn't fully disappear; it stays in the system process table so the parent can read its exit status. If the parent ignores the child, that entry stays there forever. These are zombies—dead, but still taking up a slot in the process table.
To prevent this, the parent needs to "reap" the child using wait() or waitpid(). This tells the kernel, "I've acknowledged that my child is done, you can clear the record now." I've seen production servers slowly crawl to a halt because a developer forgot a wait() call in a loop that spawned thousands of short-lived workers. Always clean up after your children.
📋 Practical Task
Build a Mini-Command Launcher
Your task is to write a C program that acts as a primitive command launcher. The program should accept a single command-line argument (the path to an executable) and run it using the fork and execvp pattern.
Requirements:
- The program must take one argument from
argv[1]. - It must
fork()a child process. - The child process must use
execvp()to execute the provided path. - The parent process must use
wait()to ensure the child finishes before the parent exits. - If no argument is provided, the program should print a usage message and exit.
- If
execvpfails, the child should print an error message before exiting.
Test your program by running it as: ./launcher /bin/date or ./launcher /bin/whoami.
There are no comments for now.