Process Definition
Multiple Instances
Read aloud is unavailable in this browser.
One Program, Many Processes
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:
- A web server spawning 100 worker processes to handle concurrent requests
- A database running multiple query executors in parallel
- Ten users on a remote server, each running their own shell
- A developer opening multiple terminals, each running the same text editor
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.
The Conceptual Model
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:
- Is built according to the same plans (same structure, same layout)
- Exists independently (demolishing one doesn't affect others)
- Has its own furniture, occupants, and state (different contents)
- Can be modified without changing other houses (independent modifications)
Similarly, from a single program, you can create many processes. Each process:
- Executes the same instructions (same code)
- Exists independently (terminating one doesn't affect others)
- Has its own data, heap, stack, and open files (different state)
- Can be modified at runtime without affecting sibling processes
The Key Insight:
Each process created from the same program gets:
- A copy of the code — The same instructions, loaded into its own address space
- Fresh data segments — Uninitialized or initialized to starting values
- Empty heap — Ready for its own dynamic allocations
- Fresh stack — With its own call stack and local variables
- Separate file descriptors — Its own table of open files
- Unique PID — A distinct identity in the system
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.
How Multiple Instances Work
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:
- •Request received — User or system requests execution (e.g., typing command, double-clicking icon)
- •New process created — OS allocates a new process descriptor (task_struct)
- •Unique PID assigned — A fresh, unique process ID is assigned
- •New address space created — Separate virtual memory space with its own page tables
- •Code loaded/mapped — Program code loaded into the new address space
- •Data initialized — Fresh copies of data sections with initial values
- •Stack initialized — New stack with command-line arguments and environment
- •Process scheduled — Added to ready queue for CPU time
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.
Memory Efficiency: Sharing Where Safe
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:
- Memory = 50 × (100 MB + 200 MB) = 15,000 MB = 15 GB
With code sharing:
- Shared code = 100 MB (once)
- Private data = 50 × 200 MB = 10,000 MB
- Total = 10,100 MB ≈ 10 GB
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.
Copy-on-Write: Lazy Copying
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:
- Trap the write attempt (page fault)
- Allocate a new physical page
- Copy the contents from the shared page
- Map the new page for the writing process
- Allow the write to proceed
Benefits of Copy-on-Write:
- •Fast fork() — Creating a child process is nearly instant, regardless of parent's memory size. Only page tables are copied, not data.
- •Memory efficiency — Pages that are never modified are never copied. If a child immediately calls exec(), almost no copying happens.
- •Deferred work — Copying only happens if and when needed. Read-only data (like code) is never copied.
- •Efficient for fork-exec pattern — The common pattern of fork() followed by exec() becomes extremely cheap.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
#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..."); 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)"); printf("Child: First byte = %c", data[0]); // Still shared! printf("Child: Writing to first half (triggers COW)"); // 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", data[0]); // 'B' exit(0); } else { sleep(2); // Let child run printf("Parent: First byte = %c", 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().
Practical Applications of Multiple Instances
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:
- Isolation: One request crashing a worker doesn't affect others
- Parallelism: Multiple CPUs are fully utilized
- Stability: Workers can be recycled after handling N requests (memory leak prevention)
- Graceful restart: New workers with new code; old workers finish current requests
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.
Methods of Creating Multiple Instances
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 |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
#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", 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)", i, getpid()); // Simulate work sleep(2 + i); // Each worker runs for different time printf("Worker %d finished", 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...", 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", finished, WEXITSTATUS(status)); } printf("All workers finished. Master exiting."); 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...*/Instance Independence: What It Really Means
We've said process instances are 'independent,' but what does this mean precisely? Let's examine the dimensions of independence:
- •Memory contents — Variables in one instance don't affect another
- •Execution progress — Each instance has its own instruction pointer
- •Open files — File descriptors are per-process
- •Exit status — Each instance terminates independently
- •Signals received — Signals target specific processes
- •CPU time usage — Each instance tracked separately
- •File system — Two instances writing same file → conflicts
- •Network ports — Only one process can bind a port
- •Database connections — Need coordination for consistency
- •External resources — Printers, hardware devices
- •User-visible state — Same user's files, preferences
- •System resources — CPU and memory are finite
Common Pitfalls with Multiple Instances:
- •Lock files: Applications needing 'single instance' behavior often use lock files. Instance 1 creates
/var/run/app.pid; Instance 2 sees the file and refuses to start. - •Port binding: A web server can't start if another instance already bound port 80. This is often the first symptom of accidentally running multiple instances.
- •Database corruption: If instances write to the same database file (SQLite) without coordination, corruption can occur. Proper databases handle this with locking.
- •Config file races: Two instances reading and writing the same config file can overwrite each other's changes.
- •Log file interleaving: Multiple instances appending to the same log file create interleaved, hard-to-read output.
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.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.