Memory
Buffer manager internals
The buffer manager maps a relation block to a shared-buffer slot, pins it while a backend uses it, and on a miss chooses a reusable buffer, performs any needed writeback, reads the requested block and publishes the new mapping.
In plain words
The buffer manager is the part of PostgreSQL that decides which table and index pages sit in shared memory. Every page has an "address" (which table, which file part, which page number). To find a page quickly, PostgreSQL looks that address up in a hash table instead of searching the whole cache.
If the page is already there, the session pins it, a bit like putting a "in use" sticker on it, so nobody can throw it out while it is being read.
If the page is not there, PostgreSQL needs a free slot. Once no never-used slot is left, it walks around the cache like a clock hand: pages in use are skipped, recently used pages get a second chance, and the first page nobody has used for a while is replaced. If that page was changed and not yet saved, it is written to disk first. Then the new page is read in and handed to the session.
For DBAs and operators
The lookup key
BufferTag identifies one relation block using tablespace OID, database OID, relation file number, fork number and block number. buf_table.c maintains the shared hash from that tag to a buffer ID. The hash table is partition-locked; the lookup routines themselves expect the caller to hold the appropriate mapping lock.
What a buffer descriptor tracks
Each shared-buffer slot has a BufferDesc. Its state includes flags such as BM_VALID and BM_DIRTY, a pin/reference count, and a usage count used by replacement. A pin protects a buffer from replacement; content locks protect access to the page contents. These are different jobs.
Replacement
StrategyGetBuffer() in freelist.c first tries the caller's ring (if any) and the free list of never-used or invalidated buffers, then falls back to the clock sweep. The sweep skips pinned buffers. A nonzero usage count is reduced and the sweep continues; an unpinned buffer with usage count zero is a candidate. Bulk operations can use a BufferAccessStrategy ring so they preferentially recycle a small working set instead of sweeping through the whole cache.
Source trail
buf_internals.h—BufferTag,BufferDesc, flags and state.buf_table.c— tag-to-buffer hash.freelist.c— clock sweep and access strategies.bufmgr.c— lookup, pins and buffer I/O.
For developers: inside the source
Read path call flow
ReadBuffer()→ReadBufferExtended()→ReadBuffer_common()builds aReadBuffersOperationand callsStartReadBuffer(), thenWaitReadBuffers()if I/O was started. The read-stream code (read_stream.c) uses the multi-blockStartReadBuffers()with the same machinery.PinBufferForBlock()sends temporary relations toLocalBufferAlloc()and everything else toBufferAlloc().BufferAlloc():InitBufferTag(),BufTableHashCode(), take the partition lock fromBufMappingPartitionLock()in shared mode,BufTableLookup(). Hit:PinBuffer()and return. Miss: release, callGetVictimBuffer(), retake the partition lock exclusively andBufTableInsert(). If another backend inserted the same tag meanwhile, the victim goes back viaStrategyFreeBuffer()and the existing buffer is pinned instead. The new mapping getsBM_TAG_VALID, not yetBM_VALID.GetVictimBuffer():StrategyGetBuffer(),PinBuffer_Locked(); ifBM_DIRTY, take the content lock conditionally in share mode, let a ring reject the buffer if writing it needs a WAL flush (XLogNeedsFlush()+StrategyRejectBuffer()), otherwiseFlushBuffer()(XLogFlush()up to the page LSN,PageSetChecksumCopy(),smgrwrite()).InvalidateVictimBuffer()then removes the old mapping.AsyncReadBuffers()claims the buffer throughStartBufferIO()(setsBM_IO_IN_PROGRESS) and issuessmgrstartreadv()on an AIO handle. The completion callbackbuffer_readv_complete_one()runsPageIsVerified()andTerminateBufferIO(), which setsBM_VALIDand wakes waiters sleeping inWaitIO()(wait eventIO:BufferIOon the per-buffer condition variable).
Locks in play
- 128 buffer-mapping partition LWLocks (
NUM_BUFFER_PARTITIONS); the hash lookup functions inbuf_table.cassume the caller already holds the right one. - The buffer header bit
BM_LOCKED(a spinlock built into the atomic state), used byLockBufHdr()/UnlockBufHdr(). content_lockin each descriptor, taken withLockBuffer()inBUFFER_LOCK_SHAREorBUFFER_LOCK_EXCLUSIVEmode;LockBufferForCleanup()additionally waits until the caller holds the only pin (BM_PIN_COUNT_WAITER).buffer_strategy_lock, a spinlock that only guards the free list.
Temporary tables
localbuf.c reuses BufferDesc for backend-local buffers, without locks or atomics. Local buffers have negative Buffer numbers (BufferIsLocal()), their own hash table and their own clock sweep in GetLocalVictimBuffer().
Experimenting
Use pg_buffercache_evict() to turn a hit into a miss, then compare EXPLAIN (ANALYZE, BUFFERS) and pg_stat_io (reads, hits, evictions, reuses per context). src/test/modules/test_aio exercises the read path, including failed reads and checksum errors. There is no hook to replace the buffer manager; AMs choose a ring by passing a BufferAccessStrategy from GetAccessStrategy() to ReadBufferExtended().
Parameters that shape this
shared_buffers temp_buffers vacuum_buffer_usage_limit bgwriter_lru_multiplier io_method
Related topics
Shared buffers Page layout Background writer Asynchronous I/O Local (per-backend) memory