TRT
Sign in
Operating Systems

A Process Is More Than a Program File

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 Runtime Theory Team8 min read#processes#virtual-memory#loader

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:

  1. Validates the executable format and permission bits (ENOEXEC, EACCES live here).
  2. Unmaps the old address space and maps the new file's segments: .text read+execute, .data/.bss read+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.
  3. Sets up the initial stack with argc, argv[], envp[] and the auxiliary vector (hardware capabilities, the entry point address).
  4. Attaches the dynamic loader for dynamically linked binaries, which maps shared libraries before any of your code runs.
  5. Jumps to the entry point — _start in C, which calls the runtime that eventually calls main.

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

bash
$ 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; execve turns 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 fork cheap, 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 / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, 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:

  1. 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.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. 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.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save