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 invokesfork()to spawn an independent dedicated backend process (PostgresMain). - Signal Handler & Crash Sentry: If any backend crashes with
SIGSEGVorSIGBUS, Postmaster sendsSIGQUITto terminate all active backends, resets shared memory from disk checkpoints, and enters recovery mode to prevent database corruption.
Auxiliary Background Processes¶
- Checkpointer: Periodically issues checkpoints. Scans
Shared Buffers, writes all dirty buffers modified before the checkpoint target to disk, syncs them viafdatasync(), and recordsredo_lsnin the control file. - 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. - WAL Writer: Periodically flushes
WAL Buffersto disk when backends configure asynchronous commits (synchronous_commit = off). - 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. - Archiver: Copies completed 16MB WAL segment files to long-term storage when
archive_mode = on. - 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:
BufferDescriptorsArray: Fixed array of metadata structs (BufferDesc), one per buffer slot.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
- Pinning (
refcount): A buffer cannot be evicted ifrefcount > 0. Any query reading or modifying a page increments its pin count. - Popularity (
usage_count): Max value is 5. Every time a query accesses a buffer,usage_countis incremented up to 5. The clock hand decrementsusage_countas it sweeps past. Only unpinned buffers withusage_count == 0are 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;