Loading learning content...
One of the most powerful capabilities of modern operating systems is the ability to run multiple instances of the same program simultaneously. Each instance is a separate, independent process with its own memory, state, and execution context.
This isn't a trivial feature—it's the foundation upon which servers, parallel computing, and multi-user systems are built. Consider:
In each case, the same program file on disk gives rise to multiple simultaneous processes and this is normal, expected behavior.
By the end of this page, you will understand how multiple processes can originate from the same program, why each instance is truly independent, how the OS manages shared code efficiently, and the practical applications of multi-instance execution.
To understand multiple instances, we must revisit and deepen our understanding of the program-process relationship.
The Blueprint Analogy:
Think of a program as an architectural blueprint for a house. From a single blueprint, you can construct many houses. Each house:
Similarly, from a single program, you can create many processes. Each process:
The Key Insight:
Each process created from the same program gets:
The processes share nothing by default. Any sharing (inter-process communication) must be explicitly arranged.
Two processes running the same program will execute the same instructions, but their journeys may diverge based on input data, timing, and environment. One instance might take a different code path, encounter an error, or run longer. They're independent travelers on parallel paths.
When you launch a program multiple times, the operating system performs the process creation sequence each time, resulting in completely separate processes.
The Creation Sequence:
1234567891011121314151617181920212223242526272829303132
# Launch Python with different scripts - creates separate processes$ python3 script_a.py & # Process 1[1] 10001 $ python3 script_b.py & # Process 2[2] 10002 $ python3 script_c.py & # Process 3[3] 10003 # All three are independent processes from the same program$ ps aux | grep python3user 10001 0.5 1.2 python3 script_a.pyuser 10002 0.3 1.1 python3 script_b.pyuser 10003 0.4 1.0 python3 script_c.py # Each has its own memory space$ cat /proc/10001/maps | head -300400000-00600000 r--p 00000000 python3 (code)00800000-00900000 rw-p 00000000 [heap]7ffe00000000-7ffe00100000 rw-p 00000000 [stack] $ cat /proc/10002/maps | head -300400000-00600000 r--p 00000000 python3 (code) # Different physical pages!00800000-00880000 rw-p 00000000 [heap] # Different size7ffe00000000-7ffe00120000 rw-p 00000000 [stack] # Different size # Killing one doesn't affect the others$ kill 10001$ ps aux | grep python3user 10002 0.3 1.1 python3 script_b.py # Still runninguser 10003 0.4 1.0 python3 script_c.py # Still runningNotice that all processes might show similar virtual addresses (like 0x00400000 for code). This doesn't mean they share memory—each process has its own page tables that translate these virtual addresses to different physical memory locations. The addresses look the same but refer to completely different data.
If every process gets its own copy of everything, wouldn't running 100 instances of a program use 100x the memory? Fortunately, operating systems are smarter than that.
The Optimization: Code Sharing
Since program code (the .text section) is read-only—it doesn't change during execution—the OS can safely share a single physical copy among all processes running that program. This is called text sharing or code sharing.
| Memory Region | Shared? | Reason |
|---|---|---|
| Text (Code) | Yes ✓ | Read-only; never modified; safe to share physically |
| Shared Libraries (.so/.dll) | Yes ✓ | Same reason; loaded once, mapped to many processes |
| Data (Initialized) | No ✗ | Each process modifies its own copy |
| BSS (Uninitialized) | No ✗ | Each process modifies its own copy |
| Heap | No ✗ | Dynamic allocations are process-specific |
| Stack | No ✗ | Function calls and local variables are process-specific |
Memory Savings Example:
Consider 50 instances of a web server with 100 MB code + 200 MB average data per process:
Without sharing:
With code sharing:
Savings: 5 GB (33% reduction)
For large applications with many instances, this optimization is crucial. Servers running hundreds of worker processes would be impossible without code sharing.
When shared libraries (like libc, libssl, or Python packages) are used, the savings compound. If 100 Python processes all use numpy, that 100 MB library is loaded once, not 100 times. This is why 'shared' libraries are called shared—they're shared across processes in physical memory.
Modern operating systems employ a powerful optimization called Copy-on-Write (COW) that further reduces the memory cost of multiple instances, particularly when processes are created via fork().
The Problem COW Solves:
When fork() creates a new process, it should create a complete copy of the parent's address space. For a process with 1 GB of memory, copying 1 GB takes time and memory. But often, the child immediately calls exec() to run a different program, making the copy wasted effort.
The COW Solution:
Instead of copying immediately, the OS marks all memory pages as read-only in both parent and child. Both processes share the same physical pages initially. Only when either process tries to write to a page does the OS:
Benefits of Copy-on-Write:
12345678910111213141516171819202122232425262728293031323334353637383940414243
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h> int main() { // Allocate 100 MB size_t size = 100 * 1024 * 1024; char *data = malloc(size); // Fill with data for (size_t i = 0; i < size; i++) { data[i] = 'A'; } printf("Parent using ~100MB. Forking...\n"); pid_t pid = fork(); // At this point, due to COW, both processes share the 100MB // Total memory used: still ~100MB, not 200MB! if (pid == 0) { // Child process printf("Child: Reading data (no copy triggered)\n"); printf("Child: First byte = %c\n", data[0]); // Still shared! printf("Child: Writing to first half (triggers COW)\n"); // This write triggers COW for the affected pages for (size_t i = 0; i < size/2; i++) { data[i] = 'B'; // Pages are copied as they're written } // Now ~50MB has been copied for child; 50MB still shared printf("Child: First byte = %c\n", data[0]); // 'B' exit(0); } else { sleep(2); // Let child run printf("Parent: First byte = %c\n", data[0]); // Still 'A'! wait(NULL); } return 0;}While COW is powerful, it can cause unexpected memory spikes. If a parent process with 8 GB of data forks a child that modifies everything, you'll suddenly need 16 GB as all pages are copied. This is why some languages (like Ruby) had 'COW-unfriendly' garbage collectors that touched all memory, triggering massive copy operations after fork().
The ability to run multiple instances of the same program is not just a technical capability—it's the foundation of modern system architecture. Let's examine the key use cases:
Multi-Process Web Servers:
Web servers like Apache (prefork mode) and nginx create multiple worker processes to handle concurrent requests. Benefits:
Example: nginx architecture
Master Process (PID 1000)
├── Worker 1 (PID 1001) - handling requests
├── Worker 2 (PID 1002) - handling requests
├── Worker 3 (PID 1003) - handling requests
└── Worker 4 (PID 1004) - handling requests
All workers run the same code but handle different requests independently.
Use multiple processes when you need: strong isolation (security, fault tolerance), to utilize multiple CPU cores with Python/Ruby (due to GIL), or when components might crash. Use threads when you need: shared memory for efficiency, low-latency communication, or fine-grained parallelism within a single application.
There are several ways to create multiple process instances, each appropriate for different scenarios:
| Method | Mechanism | Use Case |
|---|---|---|
| Shell command | User types command multiple times | Interactive use; running tools |
| fork() | Process duplicates itself | Server worker pools; parallel processing |
| fork() + exec() | Duplicate then replace program | Shell executing commands |
| spawn()/posix_spawn() | Create process with new program directly | When fork()'s COW overhead is unwanted |
| CreateProcess() (Windows) | Windows API for process creation | Windows applications |
| Process pools | Pre-forked workers waiting for tasks | Web servers; task queues |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h> #define NUM_WORKERS 4 int main() { pid_t pids[NUM_WORKERS]; printf("Master process PID: %d\n", getpid()); // Create multiple worker processes for (int i = 0; i < NUM_WORKERS; i++) { pids[i] = fork(); if (pids[i] == 0) { // Child process (worker) printf("Worker %d started (PID: %d)\n", i, getpid()); // Simulate work sleep(2 + i); // Each worker runs for different time printf("Worker %d finished\n", i); exit(i); // Each worker exits with different status } if (pids[i] < 0) { perror("fork failed"); exit(1); } } // Master waits for all workers printf("Master waiting for %d workers...\n", NUM_WORKERS); for (int i = 0; i < NUM_WORKERS; i++) { int status; pid_t finished = wait(&status); printf("Worker with PID %d exited with status %d\n", finished, WEXITSTATUS(status)); } printf("All workers finished. Master exiting.\n"); return 0;} /* Output:Master process PID: 5000Worker 0 started (PID: 5001)Worker 1 started (PID: 5002)Worker 2 started (PID: 5003)Worker 3 started (PID: 5004)Master waiting for 4 workers...Worker 0 finishedWorker with PID 5001 exited with status 0Worker 1 finishedWorker with PID 5002 exited with status 1...*/We've said process instances are 'independent,' but what does this mean precisely? Let's examine the dimensions of independence:
Common Pitfalls with Multiple Instances:
/var/run/app.pid; Instance 2 sees the file and refuses to start.Process instances are independent in terms of their internal state, but they share the external world. If two instances both try to delete the same file, only one succeeds. If both try to update the same database row, they conflict. Designing for multi-instance safety requires conscious attention to shared external resources.
We've explored the rich topic of multiple process instances—a capability that underpins modern computing. Let's consolidate the key insights:
What's Next:
Now that we understand how multiple independent process instances can exist, we'll examine the characteristics that define a process. What attributes does every process have? What information does the OS track? Understanding process characteristics completes our picture of what a process is.
You now understand how operating systems support multiple instances of programs running as separate processes. This knowledge is essential for understanding server architectures, parallel processing, and why process isolation makes modern multi-tasking possible.