Skip to content

01. PostgreSQL Architecture & Process Model

PostgreSQL employs a multi-process, client-server shared-memory architecture rather than a threaded model. This design provides hardware-isolated memory protection per connection, zero risk of process-wide memory corruption from rogue C extensions, and fault tolerance at the cost of higher per-connection operating system context-switching overhead.


1. Process Hierarchy & Daemon Lifecycle

graph TD
    PM["Postmaster (Daemon / PID 1)"]
    PM -->|"fork() per client"| BE1["Backend Process (Client A)"]
    PM -->|"fork() per client"| BE2["Backend Process (Client B)"]
    PM -->|"fork() per client"| BEn["Backend Process (Client N)"]

    subgraph Auxiliary Processes
        PM --> CP["Checkpointer"]
        PM --> BW["Background Writer (bgwriter)"]
        PM --> WW["WAL Writer"]
        PM --> AL["Autovacuum Launcher"]
        PM --> AR["Archiver"]
        PM --> WS["WAL Sender / Receiver"]
    end

    subgraph Shared Memory Block
        SB["Shared Buffers (Pool)"]
        WB["WAL Buffers"]
        LTC["Lock / Proc Tables"]
    end

    BE1 <--> SB
    BE2 <--> SB
    CP <--> SB
    BW <--> SB
    WW <--> WB

The Postmaster Daemon

  • Role: The supervisor process (postgres). Listens on TCP port 5432 and UNIX domain sockets.
  • Connection Handshake: Upon accepting a connect() system call, Postmaster performs an authentication handshake (SCRAM-SHA-256 / MD5 / GSSAPI) and invokes fork() to spawn an independent dedicated backend process (PostgresMain).
  • Signal Handler & Crash Sentry: If any backend crashes with SIGSEGV or SIGBUS, Postmaster sends SIGQUIT to terminate all active backends, resets shared memory from disk checkpoints, and enters recovery mode to prevent database corruption.

Auxiliary Background Processes

  1. Checkpointer: Periodically issues checkpoints. Scans Shared Buffers, writes all dirty buffers modified before the checkpoint target to disk, syncs them via fdatasync(), and records redo_lsn in the control file.
  2. Background Writer (bgwriter): Continuously searches for dirty buffers using a 2-hand clock sweep and writes them to the OS page cache asynchronously so incoming backend queries almost always find clean buffers without waiting on I/O.
  3. WAL Writer: Periodically flushes WAL Buffers to disk when backends configure asynchronous commits (synchronous_commit = off).
  4. Autovacuum Launcher & Workers: Spawns workers (autovacuum_max_workers) to purge dead MVCC tuples, update Free Space Maps (FSM), and freeze transaction IDs to prevent transaction wraparound.
  5. Archiver: Copies completed 16MB WAL segment files to long-term storage when archive_mode = on.
  6. WAL Sender & Receiver: Streams Write-Ahead Log bytes across TCP sockets for physical streaming replication.

2. Memory Context Hierarchy (MemoryContextData)

PostgreSQL does not use bare malloc() and free(). Instead, all dynamic allocations are managed via hierarchical Memory Contexts. When an operation completes, PostgreSQL deallocates the entire context tree in a single pass, eliminating memory leaks even on transaction abort (elog(ERROR)).

TopMemoryContext
├── ErrorContext
├── MessageContext (re-set per client wire protocol command)
├── CacheMemoryContext (relcache, syscache, plans)
└── TopTransactionContext
    └── CurTransactionContext (freed upon COMMIT/ROLLBACK)
        └── PortalContext (per cursor / execution portal)
            └── ExecutorState (per query execution tree)

Each memory chunk has an 8-byte chunk header linking back to its owning MemoryContextData. If an exception occurs deep inside an index scan, the siglongjmp handler jumps straight back to the transaction coordinator, which calls MemoryContextReset(CurTransactionContext), instantly freeing all scratch memory without traversing individual nodes.


3. Shared Buffers & The Clock Sweep Algorithm

shared_buffers represents PostgreSQL's in-memory cache for 8KB disk pages. It consists of two synchronized data structures:

  1. BufferDescriptors Array: Fixed array of metadata structs (BufferDesc), one per buffer slot.
  2. BufferBlocks: Contiguous shared memory region split into 8192-byte page frames.

Anatomy of BufferDesc

typedef struct BufferDesc {
    BufferTag   tag;             // RelFileNode (db, tablespace, relation), forkNum, blockNum
    int         buf_id;          // index in BufferBlocks
    uint32      state;           // atomic flags: BM_DIRTY, BM_VALID, refcount, usage_count
    int         freeNext;        // free list pointer
    LWLock      content_lock;    // read/write content lock
} BufferDesc;

The Clock Sweep Eviction Algorithm

When a backend needs to read a block that is not present in shared_buffers, it allocates an eviction candidate using the Clock Sweep algorithm:

stateDiagram-v2
    [*] --> CheckHand: Move Clock Hand to Next Buffer
    CheckHand --> CheckRef: Inspect Buffer State
    CheckRef --> CheckHand: refcount > 0 (Pinned, Skip)
    CheckRef --> DecrementUsage: refcount == 0 & usage_count > 0
    DecrementUsage --> CheckHand: usage_count = usage_count - 1
    CheckRef --> CheckDirty: refcount == 0 & usage_count == 0
    CheckDirty --> FlushVictim: BM_DIRTY set
    FlushVictim --> ClaimSlot: Write to disk / OS Cache
    CheckDirty --> ClaimSlot: Buffer Clean
    ClaimSlot --> [*]: Return buf_id to Backend
  1. Pinning (refcount): A buffer cannot be evicted if refcount > 0. Any query reading or modifying a page increments its pin count.
  2. Popularity (usage_count): Max value is 5. Every time a query accesses a buffer, usage_count is incremented up to 5. The clock hand decrements usage_count as it sweeps past. Only unpinned buffers with usage_count == 0 are chosen for eviction.

4. Production Diagnostics: Shared Buffer Inspection

You can inspect the contents of your buffer pool using the standard pg_buffercache extension:

-- Enable inspection extension
CREATE EXTENSION IF NOT EXISTS pg_buffercache;

-- Identify top relations occupying Shared Buffers and their dirty page ratio
SELECT 
    c.relname,
    pg_size_pretty(count(*) * 8192) as buffered_size,
    round(100.0 * count(*) / (SELECT setting FROM pg_settings WHERE name='shared_buffers')::integer, 2) AS buffer_pool_pct,
    round(100.0 * count(*) FILTER (WHERE b.isdirty) / count(*), 1) AS dirty_ratio_pct,
    round(avg(b.usagecount), 2) AS avg_usage_count
FROM pg_buffercache b
JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid)
JOIN pg_database d ON (b.reldatabase = d.oid AND d.datname = current_database())
GROUP BY c.relname
ORDER BY count(*) DESC
LIMIT 10;