A program file is a persistent description of code and data; a process is a running instance with execution state and an address space. The file sits on disk in ELF (or Mach-O, PE) format — bytes describing sections, symbols and entry point. The moment the kernel runs execve, those bytes become a live object: private memory, a program counter, open file descriptors, credentials, signal handlers and a scheduling entity. Two processes can run the same file and share nothing but the read-only text pages.
What execve actually builds
The kernel's loader does five things in order:
- Validates the executable format and permission bits (
ENOEXEC,EACCESlive here). - Unmaps the old address space and maps the new file's segments:
.textread+execute,.data/.bssread+write. On modern systems the base is randomized — ASLR places the text, heap, mmap region and stack at different addresses on every run, which is why a crash address differs between runs. - Sets up the initial stack with
argc,argv[],envp[]and the auxiliary vector (hardware capabilities, the entry point address). - Attaches the dynamic loader for dynamically linked binaries, which maps shared libraries before any of your code runs.
- Jumps to the entry point —
_startin C, which calls the runtime that eventually callsmain.
None of this copies the file into memory: the segments are mapped, and pages are faulted in on first access. A 50 MB binary that touches 4 MB of code costs 4 MB of RAM.
The address space, as the process sees it
$ cat /proc/self/maps | head -8
55e9a1c00000-55e9a1c0b000 r-xp 00000000 08:01 131074 /usr/bin/cat
55e9a1e0a000-55e9a1e0d000 r--p 0000b000 08:01 131074 /usr/bin/cat
55e9a1e0d000-55e9a1e0e000 rw-p 0000e000 08:01 131074 /usr/bin/cat
55e9a1e0e000-55e9a1e2f000 rw-p 00000000 00:00 0 [heap]
7f1a4c000000-7f1a4c021000 rw-p 00000000 00:00 0
7f1a4c5c0000-7f1a4c728000 r-xp 00000000 08:01 524571 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffd3a9f1000-7ffd3aa12000 rw-p 00000000 00:00 0 [stack]
7ffd3aa1b000-7ffd3aa1e000 r-xp 00000000 00:00 0 [vdso]Each line is a contiguous virtual range with permissions: executable text, read-only data, the writable heap (grows upward via brk), mmap regions for libraries and large allocations, and the stack (growing downward). The same virtual addresses exist in every process — 0x55e9a1c00000 is cat's text here and something entirely different in another process — and the page tables translate them to different physical frames. That translation, plus the permission bits, is what makes one process's 0x1 another process's unmapped or protected address.
Laziness: demand paging and copy-on-write
Memory appears allocated but isn't until touched. An anonymous mapping of 1 GB that is only written at the edges consumes a handful of pages; the first touch raises a page fault, the kernel allocates a frame and updates the table, and the instruction resumes. This is also how fork gets cheap: immediately after a fork, child and parent share every page read-only, and whichever writes first pays for a private copy. The classic fork(); exec(); pattern therefore copies almost nothing before the new program replaces the address space entirely.
Where isolation comes from, and where it stops
User/kernel separation (ring levels on x86), per-process page tables, and a separate descriptor table give a process a private world: file descriptors, cwd, umask, signal dispositions and resource limits are all part of the execution context, not just the code. What isolation does not give you is safety from yourself — threads inside one process share the heap, so races remain, and an mmaped shared file or a shm_open region deliberately punches a hole in the boundary. Crashes are contained because a bad pointer faults inside the owning process's address space and never reaches another's memory.
Key takeaways
- The program file is a template;
execveturns it into a mapped, randomized, lazily-faulted address space plus kernel-managed state (fds, credentials, signals). - Virtual addresses are per-process names for physical frames — identical addresses in two processes refer to different memory, enforced by page tables and permission bits.
- Nothing is resident until touched: demand paging makes large binaries and
forkcheap, and copy-on-write explains why the first write after a fork is expensive. - Isolation covers memory and descriptors, not shared heap between threads or explicitly shared mappings — those are back under your control.
Production cost model and latency numbers
To apply A Process Is More Than a Program File effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:
| Operation / Boundary | Typical Latency | Dominant Bottleneck | Production Mitigation |
|---|---|---|---|
| L1 / L2 Cache Access | 1 ns – 4 ns | Register dependency chains & pointer chasing | Contiguous memory layouts, cache-line alignment (64 B), and batch iteration |
| Main Memory (DRAM) Fetch | 60 ns – 100 ns | TLB misses, random heap indirection, NUMA hops | HugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers |
| Kernel Syscall / Context Switch | 1 µs – 5 µs | User-to-kernel mode switch, page-table flush, lock contention | Non-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O |
| NVMe SSD Random Read / fsync | 50 µs – 500 µs | Block I/O queue depth, write amplification, WAL flushes | Group commit, sequential append logs, and bounded buffer-pool pinning |
| Intra-Region RPC Roundtrip | 0.5 ms – 2.5 ms | Serialization, TCP/TLS framing, tail queueing ($L = \lambda W$) | Connection pooling, deadline propagation, and p95 hedged requests |
Failure modes, debugging, and operational checklist
In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on A Process Is More Than a Program File, walk through three verification questions:
- What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
- How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized
100 msslowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early. - Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.