DB Internals AtlasPostgreSQL 18.6 Home About

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

For developers: inside the source

Read path call flow

  1. ReadBuffer() → ReadBufferExtended() → ReadBuffer_common() builds a ReadBuffersOperation and calls StartReadBuffer(), then WaitReadBuffers() if I/O was started. The read-stream code (read_stream.c) uses the multi-block StartReadBuffers() with the same machinery.
  2. PinBufferForBlock() sends temporary relations to LocalBufferAlloc() and everything else to BufferAlloc().
  3. BufferAlloc(): InitBufferTag(), BufTableHashCode(), take the partition lock from BufMappingPartitionLock() in shared mode, BufTableLookup(). Hit: PinBuffer() and return. Miss: release, call GetVictimBuffer(), retake the partition lock exclusively and BufTableInsert(). If another backend inserted the same tag meanwhile, the victim goes back via StrategyFreeBuffer() and the existing buffer is pinned instead. The new mapping gets BM_TAG_VALID, not yet BM_VALID.
  4. GetVictimBuffer(): StrategyGetBuffer(), PinBuffer_Locked(); if BM_DIRTY, take the content lock conditionally in share mode, let a ring reject the buffer if writing it needs a WAL flush (XLogNeedsFlush() + StrategyRejectBuffer()), otherwise FlushBuffer() (XLogFlush() up to the page LSN, PageSetChecksumCopy(), smgrwrite()). InvalidateVictimBuffer() then removes the old mapping.
  5. AsyncReadBuffers() claims the buffer through StartBufferIO() (sets BM_IO_IN_PROGRESS) and issues smgrstartreadv() on an AIO handle. The completion callback buffer_readv_complete_one() runs PageIsVerified() and TerminateBufferIO(), which sets BM_VALID and wakes waiters sleeping in WaitIO() (wait event IO:BufferIO on the per-buffer condition variable).

Locks in play

  • 128 buffer-mapping partition LWLocks (NUM_BUFFER_PARTITIONS); the hash lookup functions in buf_table.c assume the caller already holds the right one.
  • The buffer header bit BM_LOCKED (a spinlock built into the atomic state), used by LockBufHdr() / UnlockBufHdr().
  • content_lock in each descriptor, taken with LockBuffer() in BUFFER_LOCK_SHARE or BUFFER_LOCK_EXCLUSIVE mode; 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