PostgreSQL Source Code Analysis — The Buffer Manager
Here we analyze the code of PostgreSQL’s buffer manager. The buffer is extremely important and directly affects the database’s performance and stability. It is the key component through which the database’s SQL computation layer interacts with external storage (disk). Both flushing data pages to disk and reading them back go through the buffer.

README
src/backend/storage/buffer/README
Many design considerations of PostgreSQL’s buffer are actually similar to those of buffers in other application programs — for example, buffer size, eviction policy, and how to improve the hit rate. These are common problems that any buffer design must face. Let’s take a look at how the buffer in a database is designed.
- Buffer initialization
- Reading/writing the buffer — reading a data page: on a hit, increment the reference count and return; on a miss, the page must be read from disk into the buffer. First a slot must be allocated in the buffer; if there is free space, use it directly; otherwise an eviction policy must be applied to evict a data page.
- Eviction policy. (Clock-sweep algorithm)
Buffer Manager
As we know, a buffer can be implemented with a link-hashtable. A buffer is essentially a hash table, just implemented in different forms. In a hash table we must define the key and the value. In PostgreSQL, the buffer stores page data; its key must uniquely locate a page and requires <tablespace, database, relation, forknumber, block number> to identify a page, and its value is the data page.
typedef struct buftag
{
RelFileNode rnode; /* physical relation identifier */
ForkNumber forkNum;
BlockNumber blockNum; /* blknum relative to begin of reln */
} BufferTag;
typedef struct RelFileNode
{
Oid spcNode; /* tablespace */
Oid dbNode; /* database */
Oid relNode; /* relation */
} RelFileNode;
When a backend process needs to read a data page, it sends a request to the buffer manager for a data page in the form of a buftag. On a hit, the buffer manager returns a buffer_id — the slot that holds the requested page data — and the backend reads the page data from that slot.

As mentioned above, let’s look at what the buffer manager actually is. The buffer manager is composed of three layers: the buffer table, the buffer descriptors, and the buffer pool.

- buffer pool: an array that stores page data, each slot corresponding to a buffer_id.
- buffer descriptor: an array that stores buffer descriptors, each descriptor corresponding to a slot in the buffer pool.
typedef struct BufferDesc
{
BufferTag tag; /* ID of page contained in buffer */
int buf_id; /* buffer's index number (from 0) */
/* state of the tag, containing flags, refcount and usagecount */
pg_atomic_uint32 state; /* 10 bits flags | 4 bits usage count | 18 bits refcount */
int wait_backend_pgprocno; /* backend of pin-count waiter */
int freeNext; /* link in freelist chain */
LWLock content_lock; /* to lock access to buffer contents */
} BufferDesc;
- buffer table: a hash table storing the mapping between buffer_tags and buffer_ids.
/* entry for buffer lookup hashtable */
typedef struct
{
BufferTag key; /* Tag of a disk page */
int id; /* Associated buffer ID */
} BufferLookupEnt;
Reading the Buffer
The relevant flow when reading the buffer is as follows:
Buffer ReadBuffer(Relation reln, BlockNumber blockNum)
--> ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL, NULL);
--> ReadBuffer_common(RelationGetSmgr(reln), reln->rd_rel->relpersistence,forkNum, blockNum, mode, strategy, &hit);
--> BufferAlloc(smgr, relpersistence, forkNum, blockNum, strategy, &found);
--> INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); /* create a tag so we can lookup the buffer */
--> BufTableHashCode(&newTag); // hash function: input buftag, output hash value
--> get_hash_value(SharedBufHash, (void *) tagPtr);
--> buf_id = BufTableLookup(&newTag, newHash); // look up the buffer table by buftag to get buf_id; on a hit returns the id, otherwise -1
--> hash_search_with_hash_value(SharedBufHash,(void *) tagPtr,hashcode,HASH_FIND,NULL);
// if hit, return; otherwise continue
--> StrategyGetBuffer(strategy, &buf_state); // get a free available buffer, return its buffer descriptor; default strategy is NULL
--> GetBufferFromRing(strategy, buf_state);
--> BufTableInsert(&newTag, newHash, buf->buf_id); // insert the newly obtained buf_id into the buffer table
--> smgrread(smgr, forkNum, blockNum, (char *) bufBlock); // read from disk into the buffer
On a hit: first construct the buftag, then feed it to the hash function to obtain the hash value, and look up the buf_id in the hash table using that hash value. On a hit, buf_id is greater than 0; then pin the page so it cannot be evicted (otherwise it would affect the page read). Modify BufferDesc->state, refcount + 1, usage + 1.
// get the data page via the buffer
Page page = BufferGetPage(buf);
static Buffer ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
BlockNumber blockNum, ReadBufferMode mode, BufferAccessStrategy strategy, bool *hit)
{
BufferDesc *bufHdr;
Block bufBlock;
bool found;
// ...
if (isLocalBuf)
{
// ...
} else {
bufHdr = BufferAlloc(smgr, relpersistence, forkNum, blockNum, strategy, &found);
}
/* if it was already in the buffer pool, we're done */
if (found) // if the buffer was hit
{
if (!isExtend)
{
// ...
return BufferDescriptorGetBuffer(bufHdr); // return the buffer
}
}
}
On a miss: we must first try to evict a page, then read the page from disk into a buffer slot, and then return the corresponding buf_id based on that buffer slot.
static Buffer ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
BlockNumber blockNum, ReadBufferMode mode, BufferAccessStrategy strategy, bool *hit)
{
BufferDesc *bufHdr;
Block bufBlock;
bool found;
// ...
if (isLocalBuf)
{
// ...
} else {
bufHdr = BufferAlloc(smgr, relpersistence, forkNum, blockNum, strategy, &found);
}
/* if it was already in the buffer pool, we're done */
if (found) // if the buffer was hit
{
if (!isExtend)
{
// ...
return BufferDescriptorGetBuffer(bufHdr); // return the buffer
}
}
// if the buffer was not hit
bufBlock = isLocalBuf ? LocalBufHdrGetBlock(bufHdr) : BufHdrGetBlock(bufHdr);
if (isExtend)
{
/* new buffers are zero-filled */
MemSet((char *) bufBlock, 0, BLCKSZ);
/* don't set checksum for all-zero page */
smgrextend(smgr, forkNum, blockNum, (char *) bufBlock, false);
// ...
}
else
{
/* Read in the page, unless the caller intends to overwrite it and just wants us to allocate a buffer. */
if (mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK)
MemSet((char *) bufBlock, 0, BLCKSZ);
else
{ // read from disk into the buffer
smgrread(smgr, forkNum, blockNum, (char *) bufBlock);
// ...
}
}
return BufferDescriptorGetBuffer(bufHdr);
}
static BufferDesc *BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
BlockNumber blockNum,BufferAccessStrategy strategy,bool *foundPtr)
{
BufferTag newTag; /* identity of requested block */
uint32 newHash; /* hash value for newTag */
LWLock *newPartitionLock; /* buffer partition lock for it */
BufferTag oldTag; /* previous identity of selected buffer */
uint32 oldHash; /* hash value for oldTag */
LWLock *oldPartitionLock; /* buffer partition lock for it */
uint32 oldFlags;
int buf_id;
BufferDesc *buf;
bool valid;
uint32 buf_state;
/* create a tag so we can lookup the buffer */
INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum);
/* determine its hash code and partition lock ID */
newHash = BufTableHashCode(&newTag); // hash function: input buftag, output hash value
newPartitionLock = BufMappingPartitionLock(newHash);
/* see if the block is in the buffer pool already */
LWLockAcquire(newPartitionLock, LW_SHARED);
buf_id = BufTableLookup(&newTag, newHash); // look up the buffer table by buftag to get buf_id; on a hit returns the id, otherwise -1
if (buf_id >= 0)
{
// hit
}
// not hit: first try to evict a data page to get a free buffer
/* Loop here in case we have to try another victim buffer */
for (;;)
{
// ...
/* Select a victim buffer. The buffer is returned with its header spinlock still held!*/
buf = StrategyGetBuffer(strategy, &buf_state);
/* Pin the buffer and then release the buffer spinlock */
PinBuffer_Locked(buf);
/* If the buffer was dirty, try to write it out. */
if (oldFlags & BM_DIRTY)
{
/* We need a share-lock on the buffer contents to write it out */
if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), LW_SHARED))
{
/*
* If using a nondefault strategy, and writing the buffer
* would require a WAL flush, let the strategy decide whether
* to go ahead and write/reuse the buffer or to choose another
* victim. We need lock to inspect the page LSN, so this
* can't be done inside StrategyGetBuffer.
*/
if (strategy != NULL)
{
XLogRecPtr lsn;
/* Read the LSN while holding buffer header lock */
buf_state = LockBufHdr(buf);
lsn = BufferGetLSN(buf);
UnlockBufHdr(buf, buf_state);
if (XLogNeedsFlush(lsn) &&StrategyRejectBuffer(strategy, buf))
{
/* Drop lock/pin and loop around for another buffer */
LWLockRelease(BufferDescriptorGetContentLock(buf));
UnpinBuffer(buf, true);
continue; // try to evict another page
}
}
// if the evicted page is dirty, it must be flushed to disk
FlushBuffer(buf, NULL);
LWLockRelease(BufferDescriptorGetContentLock(buf));
ScheduleBufferTagForWriteback(&BackendWritebackContext, &buf->tag);
}
else
{
/* Someone else has locked the buffer, so give it up and loop back to get another one. */
UnpinBuffer(buf, true);
continue;
}
}
// ...
}
else
{
/* if it wasn't valid, we need only the new partition */
LWLockAcquire(newPartitionLock, LW_EXCLUSIVE);
/* remember we have no old-partition lock or tag */
oldPartitionLock = NULL;
/* keep the compiler quiet about uninitialized variables */
oldHash = 0;
}
/*
* Try to make a hashtable entry for the buffer under its new tag.
* This could fail because while we were writing someone else
* allocated another buffer for the same block we want to read in.
* Note that we have not yet removed the hashtable entry for the old
* tag.
*/
buf_id = BufTableInsert(&newTag, newHash, buf->buf_id); // insert the newly obtained buf_id into the buffer table
if (buf_id >= 0)
{
/*
* Got a collision. Someone has already done what we were about to
* do. We'll just handle this as if it were found in the buffer
* pool in the first place. First, give up the buffer we were
* planning to use.
*/
UnpinBuffer(buf, true);
/* Can give up that buffer's mapping partition lock now */
if (oldPartitionLock != NULL &&
oldPartitionLock != newPartitionLock)
LWLockRelease(oldPartitionLock);
/* remaining code should match code at top of routine */
buf = GetBufferDescriptor(buf_id);
valid = PinBuffer(buf, strategy);
/* Can release the mapping lock as soon as we've pinned it */
LWLockRelease(newPartitionLock);
*foundPtr = true;
if (!valid)
{
/*
* We can only get here if (a) someone else is still reading
* in the page, or (b) a previous read attempt failed. We
* have to wait for any active read attempt to finish, and
* then set up our own read attempt if the page is still not
* BM_VALID. StartBufferIO does it all.
*/
if (StartBufferIO(buf, true))
{
/*
* If we get here, previous attempts to read the buffer
* must have failed ... but we shall bravely try again.
*/
*foundPtr = false;
}
}
return buf;
}
/*
* Need to lock the buffer header too in order to change its tag.
*/
buf_state = LockBufHdr(buf);
/*
* Somebody could have pinned or re-dirtied the buffer while we were
* doing the I/O and making the new hashtable entry. If so, we can't
* recycle this buffer; we must undo everything we've done and start
* over with a new victim buffer.
*/
oldFlags = buf_state & BUF_FLAG_MASK;
if (BUF_STATE_GET_REFCOUNT(buf_state) == 1 && !(oldFlags & BM_DIRTY))
break;
UnlockBufHdr(buf, buf_state);
BufTableDelete(&newTag, newHash);
if (oldPartitionLock != NULL &&
oldPartitionLock != newPartitionLock)
LWLockRelease(oldPartitionLock);
LWLockRelease(newPartitionLock);
UnpinBuffer(buf, true);
}
/*
* Okay, it's finally safe to rename the buffer.
*
* Clearing BM_VALID here is necessary, clearing the dirtybits is just
* paranoia. We also reset the usage_count since any recency of use of
* the old content is no longer relevant. (The usage_count starts out at
* 1 so that the buffer can survive one clock-sweep pass.)
*
* Make sure BM_PERMANENT is set for buffers that must be written at every
* checkpoint. Unlogged buffers only need to be written at shutdown
* checkpoints, except for their "init" forks, which need to be treated
* just like permanent relations.
*/
buf->tag = newTag;
buf_state &= ~(BM_VALID | BM_DIRTY | BM_JUST_DIRTIED |
BM_CHECKPOINT_NEEDED | BM_IO_ERROR | BM_PERMANENT |
BUF_USAGECOUNT_MASK);
if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM)
buf_state |= BM_TAG_VALID | BM_PERMANENT | BUF_USAGECOUNT_ONE;
else
buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE;
UnlockBufHdr(buf, buf_state);
if (oldPartitionLock != NULL)
{
BufTableDelete(&oldTag, oldHash);
if (oldPartitionLock != newPartitionLock)
LWLockRelease(oldPartitionLock);
}
LWLockRelease(newPartitionLock);
/*
* Buffer contents are currently invalid. Try to obtain the right to
* start I/O. If StartBufferIO returns false, then someone else managed
* to read it before we did, so there's nothing left for BufferAlloc() to
* do.
*/
if (StartBufferIO(buf, true))
*foundPtr = false;
else
*foundPtr = true;
return buf;
}
Eviction Policy
The most important metric of a buffer is the hit rate. To improve the hit rate, a suitable eviction policy must be chosen, evicting the least recently / least frequently used data pages as much as possible. Of course, eviction also comes with constraints — for example, a page currently being accessed cannot be evicted (it is pinned). This raises the question of how to measure usage frequency. In this regard, the state field (10 bits flags | 4 bits usage count | 18 bits refcount) contains the refcount and usagecount counters, which measure usage frequency.
typedef struct BufferDesc
{
BufferTag tag; /* ID of page contained in buffer */
int buf_id; /* buffer's index number (from 0) */
/* state of the tag, containing flags, refcount and usagecount */
pg_atomic_uint32 state; /* 10 bits flags | 4 bits usage count | 18 bits refcount */
int wait_backend_pgprocno; /* backend of pin-count waiter */
int freeNext; /* link in freelist chain */
LWLock content_lock; /* to lock access to buffer contents */
} BufferDesc;
The specific eviction policy chosen by PostgreSQL is the clock-sweep algorithm.
refcount: the reference count, holding the number of PostgreSQL processes currently accessing the corresponding page; also called the pin count.usagecount: the usage count, holding the number of times the corresponding page has been accessed since it was loaded into the buffer pool.

When a page needs to be evicted, which one should be chosen? We treat the buffer descriptors as a circular list. nextVictimBuffer is a uint32 variable that always points to some buffer descriptor and rotates in clockwise order.
typedef struct
{
/* Spinlock: protects the values below */
slock_t buffer_strategy_lock;
/*
* Clock sweep hand: index of next buffer to consider grabbing. Note that
* this isn't a concrete buffer - we only ever increase the value. So, to
* get an actual buffer, it needs to be used modulo NBuffers.
*/
pg_atomic_uint32 nextVictimBuffer;
int firstFreeBuffer; /* Head of list of unused buffers */
int lastFreeBuffer; /* Tail of list of unused buffers */
/*
* NOTE: lastFreeBuffer is undefined when firstFreeBuffer is -1 (that is,
* when the list is empty)
*/
/*
* Statistics. These counters should be wide enough that they can't
* overflow during a single bgwriter cycle.
*/
uint32 completePasses; /* Complete cycles of the clock sweep */
pg_atomic_uint32 numBufferAllocs; /* Buffers allocated since last reset */
/*
* Bgworker process to be notified upon activity or -1 if none. See
* StrategyNotifyBgWriter.
*/
int bgwprocno;
} BufferStrategyControl;
First, get the buffer descriptor that nextVictimBuffer points to. If the buffer descriptor is not pinned, check its usagecount: if it is 0, select the page in the slot corresponding to that descriptor for eviction; if it is not 0, decrement it by 1 and continue scanning the next descriptor until an eviction victim is found.
/*
* StrategyGetBuffer
*
* Called by the bufmgr to get the next candidate buffer to use in
* BufferAlloc(). The only hard requirement BufferAlloc() has is that
* the selected buffer must not currently be pinned by anyone.
*
* strategy is a BufferAccessStrategy object, or NULL for default strategy.
*
* To ensure that no one else can pin the buffer before we do, we must
* return the buffer with the buffer header spinlock still held.
*/
BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
{
BufferDesc *buf;
int bgwprocno;
int trycounter;
uint32 local_buf_state; /* to avoid repeated (de-)referencing */
// ...
/*
* We count buffer allocation requests so that the bgwriter can estimate
* the rate of buffer consumption. Note that buffers recycled by a
* strategy object are intentionally not counted here.
*/
pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
// first check whether the freelist still has free space; if so, take the next free buffer directly; otherwise enter the clock-sweep eviction logic
if (StrategyControl->firstFreeBuffer >= 0)
{
while (true)
{
/* Acquire the spinlock to remove element from the freelist */
SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
if (StrategyControl->firstFreeBuffer < 0)
{
SpinLockRelease(&StrategyControl->buffer_strategy_lock);
break;
}
// take a free buffer directly from the freelist
buf = GetBufferDescriptor(StrategyControl->firstFreeBuffer);
Assert(buf->freeNext != FREENEXT_NOT_IN_LIST);
/* Unconditionally remove buffer from freelist */
StrategyControl->firstFreeBuffer = buf->freeNext;
buf->freeNext = FREENEXT_NOT_IN_LIST;
// ...
}
}
// if there are no free pages left, run the clock-sweep algorithm to evict a page
/* Nothing on the freelist, so run the "clock sweep" algorithm */
trycounter = NBuffers;
for (;;)
{
buf = GetBufferDescriptor(ClockSweepTick()); // equivalent to a circular buffer-descriptor array
/*
* If the buffer is pinned or has a nonzero usage_count, we cannot use
* it; decrement the usage_count (unless pinned) and keep scanning.
*/
local_buf_state = LockBufHdr(buf);
if (BUF_STATE_GET_REFCOUNT(local_buf_state) == 0)
{
if (BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0)
{
local_buf_state -= BUF_USAGECOUNT_ONE; // usagecount != 0, so decrement by 1
trycounter = NBuffers;
}
else
{
/* Found a usable buffer */
if (strategy != NULL)
AddBufferToRing(strategy, buf);
*buf_state = local_buf_state;
return buf; // found usagecount == 0, evict this page
}
}
else if (--trycounter == 0)
{
/*
* We've scanned all the buffers without making any state changes,
* so all the buffers are pinned (or were when we looked at them).
* We could hope that someone will free one eventually, but it's
* probably better to fail than to risk getting stuck in an
* infinite loop.
*/
UnlockBufHdr(buf, local_buf_state);
elog(ERROR, "no unpinned buffers available");
}
UnlockBufHdr(buf, local_buf_state);
}
}
// equivalent to a circular buffer-descriptor array
/* ClockSweepTick - Helper routine for StrategyGetBuffer()
*
* Move the clock hand one buffer ahead of its current position and return the
* id of the buffer now under the hand. */
static inline uint32 ClockSweepTick(void)
{
uint32 victim;
/*
* Atomically move hand ahead one buffer - if there's several processes
* doing this, this can lead to buffers being returned slightly out of
* apparent order.
*/
victim =
pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer, 1);
if (victim >= NBuffers)
{
uint32 originalVictim = victim;
/* always wrap what we look up in BufferDescriptors */
victim = victim % NBuffers;
/*
* If we're the one that just caused a wraparound, force
* completePasses to be incremented while holding the spinlock. We
* need the spinlock so StrategySyncStart() can return a consistent
* value consisting of nextVictimBuffer and completePasses.
*/
if (victim == 0)
{
uint32 expected;
uint32 wrapped;
bool success = false;
expected = originalVictim + 1;
while (!success)
{
/*
* Acquire the spinlock while increasing completePasses. That
* allows other readers to read nextVictimBuffer and
* completePasses in a consistent manner which is required for
* StrategySyncStart(). In theory delaying the increment
* could lead to an overflow of nextVictimBuffers, but that's
* highly unlikely and wouldn't be particularly harmful.
*/
SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
wrapped = expected % NBuffers;
success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
&expected, wrapped);
if (success)
StrategyControl->completePasses++;
SpinLockRelease(&StrategyControl->buffer_strategy_lock);
}
}
}
return victim;
}
Buffer Initialization
To create a buffer, it must first be initialized. The first problem faced is how large to make the buffer and whether its size can be adjusted dynamically.
main(int argc, char *argv[])
--> PostmasterMain(argc, argv);
--> reset_shared(); // Set up shared memory and semaphores.
--> CreateSharedMemoryAndSemaphores(); // Creates and initializes shared memory and semaphores.
--> CalculateShmemSize(&numSemas); // Calculates the amount of shared memory and number of semaphores needed.
--> add_size(size, BufferShmemSize());
--> PGSharedMemoryCreate(size, &shim);
--> InitShmemAccess(seghdr);
--> InitBufferPool(); // initialize the buffer pool
// 1. initialize buffer descriptors
--> ShmemInitStruct("Buffer Descriptors",NBuffers * sizeof(BufferDescPadded),&foundDescs);
// 2. initialize the buffer pool
--> ShmemInitStruct("Buffer Blocks", NBuffers * (Size) BLCKSZ, &foundBufs);
--> StrategyInitialize(!foundDescs);
// 3. initialize the buffer table
--> InitBufTable(NBuffers + NUM_BUFFER_PARTITIONS);
--> ShmemInitHash("Shared Buffer Lookup Table", size, size, &info, HASH_ELEM | HASH_BLOBS | HASH_PARTITION);
--> hash_create(name, init_size, infoP, hash_flags);
Buffer initialization and space allocation:
void InitBufferPool(void)
{
bool foundBufs,foundDescs,foundIOCV,foundBufCkpt;
/* Align descriptors to a cacheline boundary. */
BufferDescriptors = (BufferDescPadded *) // buffer descriptors
ShmemInitStruct("Buffer Descriptors",
NBuffers * sizeof(BufferDescPadded),
&foundDescs);
BufferBlocks = (char *) // buffer pool, holds data pages
ShmemInitStruct("Buffer Blocks",
NBuffers * (Size) BLCKSZ, &foundBufs);
// ...
}
/*
* Initialize shmem hash table for mapping buffers
* size is the desired hash table size (possibly more than NBuffers)
*/
void InitBufTable(int size)
{
HASHCTL info;
/* assume no locking is needed yet */
/* BufferTag maps to Buffer */
info.keysize = sizeof(BufferTag);
info.entrysize = sizeof(BufferLookupEnt);
info.num_partitions = NUM_BUFFER_PARTITIONS;
// hash table
SharedBufHash = ShmemInitHash("Shared Buffer Lookup Table",
size, size,
&info,
HASH_ELEM | HASH_BLOBS | HASH_PARTITION);
}
Computing the buffer size, introducing the shared_buffer configuration parameter.
int NBuffers = 1000; // value of the GUC parameter shared_buffer
/*
* BufferShmemSize
*
* compute the size of shared memory for the buffer pool including
* data pages, buffer descriptors, hash tables, etc.
*/
Size
BufferShmemSize(void)
{
Size size = 0;
/* size of buffer descriptors */
size = add_size(size, mul_size(NBuffers, sizeof(BufferDescPadded)));
/* to allow aligning buffer descriptors */
size = add_size(size, PG_CACHE_LINE_SIZE);
/* size of data pages */
size = add_size(size, mul_size(NBuffers, BLCKSZ));
/* size of stuff controlled by freelist.c */
size = add_size(size, StrategyShmemSize());
/* size of I/O condition variables */
size = add_size(size, mul_size(NBuffers,
sizeof(ConditionVariableMinimallyPadded)));
/* to allow aligning the above */
size = add_size(size, PG_CACHE_LINE_SIZE);
/* size of checkpoint sort array in bufmgr.c */
size = add_size(size, mul_size(NBuffers, sizeof(CkptSortItem)));
return size;
}