콘텐츠로 이동

CUBRID 페이지 버퍼 매니저 — 코드 수준 심층 분석

이 문서의 위치: 상위 분석 문서인 cubrid-page-buffer-manager.md는 설계 의도와 이론적 배경을 다룬다. 이 문서는 코드 수준에서 모든 분기와 필드를 추적한다. 각 장은 독립적으로 읽을 수 있으나, 순서대로 읽으면 데이터 페이지 하나가 버퍼 풀 안에서 겪는 전체 생애주기 — fix, latch, dirty, flush, victimize — 를 온전히 따라갈 수 있다.

목차:

Ch제목상태
1자료구조 지도
2초기화와 메모리 레이아웃
3Fix 진입 경로와 페이지 테이블 조회
4미스 처리 — BCB 획득과 PGBUF 할당 락
5BCB 원자적 래치 획득 블록과 깨우기
6페이지 더티 처리와 플래그·존 워드 구조
7Unfix 시 LRU 이동, Aout 히스토리, Private-to-Shared 마이그레이션
8WAL 규칙 하의 플러시와 플러시 데몬
9피해자 선택 — LFCQ와 직접 피해자 전달
10적응형 쿼터 정렬 픽스와 특수 픽스 경로

페이지 버퍼 매니저가 소유하는 모든 구조체를 필드 단위로 정리한 참조 챕터다. 상위 레벨 문서(cubrid-page-buffer-manager.md)에서 왜 BCB, VPID 해시, 프리 리스트, 멀티존 LRU가 필요한지 설명하고 있으며 — 해당 문서의 ### Buffer Control Block — PGBUF_BCB### Three-zone LRU lists 절을 참조하라 — 여기서는 그 배경을 반복하지 않는다. 상위 문서가 단순화한 부분 — BCB 래치를 pthread_mutex_t + latch_mode 쌍으로 표현한 것 — 은 실제 소스 기준으로 이 챕터에서 수정한다. 모든 구조체는 src/storage/page_buffer.c에 TU-private으로 정의되어 있으며, pgbuf_watcher / PGBUF_LATCH_MODE만 예외적으로 page_buffer.h에 공개되어 있다. 전역 객체는 PGBUF_BUFFER_POOL 타입의 pgbuf_Pool 하나뿐이다. 아래 각 표는 Role & rationale 열 하나로 필드 커버리지를 빠짐없이 유지한다.

BCB에는 비트 패킹된 워드가 두 개 있으며, 이후 챕터들은 접근자 매크로로 이를 읽는다. flags 워드(volatile int, 32비트): 상위 바이트에 BCB 플래그 비트, 비트 16-19에 zone, 하위 16비트에 LRU index가 들어 있다.

// PGBUF zone + index layout -- src/storage/page_buffer.c
#define PGBUF_LRU_NBITS 16
#define PGBUF_LRU_INDEX_MASK (PGBUF_LRU_LIST_MAX_COUNT - 1) /* 0x0000FFFF */
PGBUF_LRU_1_ZONE = 1 << PGBUF_LRU_NBITS, /* 0x00010000 */
PGBUF_LRU_2_ZONE = 2 << PGBUF_LRU_NBITS, /* 0x00020000 */
PGBUF_LRU_3_ZONE = 3 << PGBUF_LRU_NBITS, /* 0x00030000 */
PGBUF_LRU_ZONE_MASK = PGBUF_LRU_1_ZONE | PGBUF_LRU_2_ZONE | PGBUF_LRU_3_ZONE,
PGBUF_INVALID_ZONE = 1 << (PGBUF_LRU_NBITS + 2), /* 0x00040000 */
PGBUF_VOID_ZONE = 2 << (PGBUF_LRU_NBITS + 2), /* 0x00080000 */

2비트를 건너뛰는 것은 의도적 설계다. LRU zone은 비트 16-17을 사용하고, INVALID/VOID는 비트 18 이상으로 점프하여 LRU 인덱스가 비트 16이나 17을 설정할 때 마스크가 충돌하지 않도록 한다. 플래그 비트는 최상위 바이트에 위치한다(PGBUF_BCB_DIRTY_FLAG 0x80000000, 이후 ..._FLUSHING_TO_DISK, ..._VICTIM_DIRECT, ..._INVALIDATE_DIRECT_VICTIM, ..._MOVE_TO_LRU_BOTTOM, ..._TO_VACUUM, ..._ASYNC_FLUSH_REQ 순으로 1비트씩 내려간다). 하나의 워드를 상위에서 하위 방향으로 읽으면: [플래그 바이트 | 예약 | zone 16-19 | LRU index 0-15]. 플래그 의미론은 챕터 6에서 다룬다.

두 번째 패킹 워드인 count_fix_and_avoid_deallocPGBUF_BCB_COUNT_FIX_SHIFT_BITS(16)와 PGBUF_BCB_AVOID_DEALLOC_MASK(0x0000FFFF)로 분리된다. 상위 16비트는 포화 카운터 형태의 fix 횟수(핫 페이지 감지용), 하위 16비트는 원자적으로 변경되는 avoid-deallocation 카운트다. 2바이트 원자 연산이 이식성을 보장하지 않기 때문에 하나의 int로 합쳐졌으며, 필드 주석에 그 이유가 명시되어 있다.

1.2 pgbuf_atomic_latch_impl — 실제 BCB 래치

섹션 제목: “1.2 pgbuf_atomic_latch_impl — 실제 BCB 래치”

이것이 상위 레벨 문서에서 가장 크게 수정되는 부분이다. 래치는 mutex와 latch_mode int의 쌍이 아니라, union으로 재해석되는 64비트 원자 워드다.

// PGBUF_ATOMIC_LATCH + union pgbuf_atomic_latch_impl -- page_buffer.h / page_buffer.c
typedef std::atomic<uint64_t> PGBUF_ATOMIC_LATCH;
union pgbuf_atomic_latch_impl
{
uint64_t raw;
struct {
PGBUF_LATCH_MODE latch_mode; /* uint16_t enum: NO/READ/WRITE/FLUSH/INVALID */
uint16_t waiter_exists; /* a thread is parked on next_wait_thrd */
int32_t fcnt; /* current fix count under the latch */
} impl;
};

BCB의 atomic_latch 필드가 바로 std::atomic<uint64_t> 자체다. 코드는 이를 memory_order_acquire로 로드하여 스택에 PGBUF_ATOMIC_LATCH_IMPL을 만들고, 서브 필드를 편집한 뒤 전체 raw를 CAS로 교체한다(set_latch_and_fcnt, set_latch_and_add_fcnt가 쓰기 절반을 담당하고, get_latch가 읽기 절반을 담당한다).

필드Role & rationale
raw64비트 전체 워드. CAS로 mode+waiter+fcnt를 한 인스트럭션에 갱신하므로 핫 패스에 mutex 불필요
impl.latch_mode현재 모드(PGBUF_LATCH_MODE, uint16_t). 공유 읽기와 배타적 쓰기를 구분
impl.waiter_exists스레드가 파킹 중이면 1. 래치 해제자가 next_wait_thrd를 깨워야 함을 알림
impl.fcnt래치 하에서의 fix 카운트. READ 모드는 fcnt > 1 허용, 0이 되면 해제

PGBUF_LATCH_MODE는 union의 첫 2바이트에 들어가도록 uint16_t 기반으로 정의된다. PGBUF_NO_LATCH=0, _READ=1, _WRITE=2, _FLUSH=3(블록 모드 전용 — 페이지가 flush 모드로 fix되는 일은 없다), _INVALID=4.

불변 조건 — union은 항상 raw 워드 전체 단위로만 접촉한다. 활성 원자에 latch_mode를 직접 쓰면 64비트 워드가 찢어지며, 나머지 절반에서 경쟁 중인 fcnt/waiter_exists 갱신이 유실된다. BCB의 mutex는 래치가 아닌 리스트/플래그 전환을 보호한다. CAS 루프의 각 분기는 챕터 5에서 추적한다.

1.3 pgbuf_bcb — 버퍼 제어 블록 필드별 정리

섹션 제목: “1.3 pgbuf_bcb — 버퍼 제어 블록 필드별 정리”
// struct pgbuf_bcb -- src/storage/page_buffer.c (condensed; SERVER_MODE fields elided)
struct pgbuf_bcb
{
VPID vpid;
PGBUF_ATOMIC_LATCH atomic_latch; /* the 64-bit union latch from 1.2 */
volatile int flags; /* flag byte | zone | lru index (1.1) */
PGBUF_BCB *hash_next, *prev_BCB, *next_BCB;
int tick_lru_list, tick_lru3;
volatile int count_fix_and_avoid_dealloc; /* two-purpose; see 1.1 */
int hit_age;
LOG_LSA oldest_unflush_lsa;
PGBUF_IOPAGE_BUFFER *iopage_buffer;
// ... condensed: mutex, owner_mutex, next_wait_thrd, latch_last_thread under #if SERVER_MODE ...
};
필드Role & rationale
mutex (SM)BCB별 pthread_mutex_t. 래치가 커버하지 않는 리스트/플래그 전환을 직렬화
owner_mutex (SM)mutex를 보유 중인 스레드의 인덱스. 잘못된 소유자/이중 잠금 해제를 위한 단언 보조
vpid적재된 페이지의 volume+page ID. 해시 키
atomic_latchR/W 페이지 래치(union, 1.2). 커널 mutex 핫 패스 외부의 사용자 수준 래치
flags플래그 바이트 + zone + LRU index가 패킹된 워드. 원자적으로 읽을 수 있는 교체/더티 상태 표현
next_wait_thrd (SM)래치에 블록된 스레드의 FIFO 헤드. waiter_exists가 이를 가리킴
latch_last_thread (SM)마지막으로 래치한 스레드. 진단용 기록
hash_next해시 버킷 체인의 다음 BCB. 충돌 체이닝(1.5)
prev_BCB이전 LRU 노드. 이중 연결 LRU로 O(1) 언링크 가능
next_BCB다음 LRU 노드 또는 프리 리스트의 다음. §1.3 불변 조건에 따라 용도 공유
tick_lru_listBCB가 LRU에 진입할 때의 리스트 tick. 리스트의 tick_list와 비교하여 age boost 여부 결정
tick_lru3zone 3 내부의 위치 스탬프. victim-hint가 가장 낮은 zone-3 BCB를 찾는 데 사용
count_fix_and_avoid_dealloc상위 16비트 fix 카운트, 하위 16비트 avoid-dealloc. 핫 페이지 감지와 dealloc 보호를 하나로 합침(1.1)
hit_age마지막 히트 시의 age 스탬프. 활성도/쿼터 계산에 사용(챕터 10)
oldest_unflush_lsa플러시되지 않은 변경의 가장 오래된 LSA. WAL 앵커 — 로그가 내구성을 확보하기 전에는 페이지를 기록하지 않음(챕터 8)
iopage_buffer이 BCB의 페이로드 슬롯 포인터. 제어 메타데이터와 정렬된 페이로드를 분리

불변 조건 — next_BCB는 한 번에 정확히 하나의 리스트에만 속한다. BCB는 LRU 리스트, 무효 프리 리스트, 또는 일시적으로 PGBUF_VOID_ZONE(어느 쪽도 아님) 중 한 상태다. flags의 zone 필드가 진실의 원천이며, zone 변경과 next_BCB 재링크는 하나의 임계 구역 안에서 이루어져야 한다. 그렇지 않으면 BCB가 두 리스트에 동시에 존재하는 것처럼 보인다. 재링크 과정은 챕터 7에서 추적한다.

1.4 pgbuf_iopage_buffer — 페이지 페이로드 슬롯

섹션 제목: “1.4 pgbuf_iopage_buffer — 페이지 페이로드 슬롯”
// struct pgbuf_iopage_buffer -- src/storage/page_buffer.c
struct pgbuf_iopage_buffer
{
PGBUF_BCB *bcb; /* back-pointer to owning BCB */
#if (__WORDSIZE == 32)
int dummy; /* pad so iopage starts 8-byte aligned */
#endif
FILEIO_PAGE iopage; /* the actual buffered IO page */
};
필드Role & rationale
bcb소유 pgbuf_bcb를 가리키는 역방향 포인터. iopage.page에 대한 PAGE_PTRCAST_PGPTR_TO_BFPTR를 써서 BCB를 복원
dummy (32비트)4바이트 패딩. 32비트에서 bcb가 4바이트이므로, iopage를 오프셋 8로 밀어 LOG_LSA 정렬을 맞춤
iopage임베디드 FILEIO_PAGE. 한 번의 할당으로 제어 정보와 온디스크 이미지를 함께 보유

iopage의 첫 바이트는 헤더(prv)이며, WAL/복구 경로가 그 lsaptype을 직접 읽는다.

// struct fileio_page -- src/storage/file_io.h
struct fileio_page_reserved { LOG_LSA lsa; INT32 pageid; INT16 volid;
unsigned char ptype; unsigned char pflag; /* ... condensed ... */ };
struct fileio_page_watermark { LOG_LSA lsa; /* duplicates prv.lsa */ };
struct fileio_page
{
FILEIO_PAGE_RESERVED prv; /* system area at start */
char page[1]; /* user area */
FILEIO_PAGE_WATERMARK prv2; /* end-of-page watermark, duplicates prv.lsa */
};

fileio_page는 헤더 전용이 아니다. 끝에 prv2(FILEIO_PAGE_WATERMARK)가 있어 prv.lsa의 사본을 페이지 말미에 보관한다. page[1]이 flexible array이므로 레이아웃은 논리적인 표현이며, fileio_get_page_watermark_pos가 멤버를 역참조하는 대신 페이지 크기로부터 prv2의 실제 주소를 계산한다.

불변 조건 — iopage_buffer->bcb는 왕복이 가능하며, prv.lsa는 절대 뒤로 가지 않는다. 역방향 포인터(bcb->iopage_buffer->bcb == bcb)는 초기화 시 한 번 설정된다(챕터 2). prv.lsa는 페이지의 내구성 복구 워터마크로, 페이지가 변경될수록 전진하고, 플러시 시 prv2에 미러링된다. oldest_unflush_lsa와 WAL 규칙(챕터 8)이 이 값을 기준으로 비교한다.

1.5 VPID 해시: pgbuf_buffer_hashpgbuf_buffer_lock

섹션 제목: “1.5 VPID 해시: pgbuf_buffer_hash와 pgbuf_buffer_lock”

pgbuf_buffer_hash{ pthread_mutex_t hash_mutex; PGBUF_BCB *hash_next; PGBUF_BUFFER_LOCK *lock_next; }이고, pgbuf_buffer_lock{ VPID vpid; PGBUF_BUFFER_LOCK *lock_next; THREAD_ENTRY *next_wait_thrd; }이다 (mutex/thread 필드는 #if SERVER_MODE).

Struct.FieldRole & rationale
buffer_hash.hash_mutex버킷 락. 해당 버킷의 두 체인 모두를 보호
buffer_hash.hash_next적재된 BCB 체인 헤드. 조회 시 vpid 매칭으로 체인을 순회(챕터 3)
buffer_hash.lock_next대기 중인 PGBUF-lock 체인 헤드. 읽기가 진행 중이어서 아직 BCB가 없는 VPID
buffer_lock.vpid읽기 중으로 예약된 VPID. 두 번째 fixer가 진행 중인 읽기를 발견하고 대기
buffer_lock.lock_next버킷 내 다음 lock 레코드. 동시 진행 중인 읽기들을 체이닝
buffer_lock.next_wait_thrd (SM)이 읽기를 기다리는 큐. 페이지가 도착하면 깨어남

buffer-lock 테이블은 고정 크기다 — 스레드당 최대 하나의 미완료 읽기만 있으므로, 스레드당 하나의 레코드가 배정된다. 챕터 4에서 미스 발생 시 BCB 할당 전에 lock을 선점하는 과정을 추적한다.

flowchart LR
  H["pgbuf_buffer_hash[bucket]"] -->|hash_next| B1["BCB"] -->|hash_next| B2["BCB"]
  H -->|lock_next| K1["pgbuf_buffer_lock vpid=A"] -->|lock_next| K2["pgbuf_buffer_lock vpid=B"]
  K1 -->|next_wait_thrd| T["대기 중인 스레드들"]

Figure 1-1: 하나의 버킷이 hash_mutex 하나 아래에서 적재된 BCB 체인과 대기 lock 체인을 모두 앵커링한다.

1.6 pgbuf_lru_list — 멀티존 LRU 하나

섹션 제목: “1.6 pgbuf_lru_list — 멀티존 LRU 하나”

struct pgbuf_lru_list#if SERVER_MODE pthread_mutex_t mutex 이후에: PGBUF_BCB *top, *bottom, *bottom_1, *bottom_2; PGBUF_BCB *volatile victim_hint; int count_lru1/2/3, count_vict_cand, threshold_lru1/2, quota, tick_list, tick_lru3; volatile int flags; int index;를 가진다.

필드Role & rationale
mutex (SM)리스트 락. 링크 무결성을 보호
top / bottomMRU 헤드 / LRU 테일. 신규/부스트 BCB는 top에 연결, 교체는 bottom에서 진행
bottom_1zone 1의 마지막 BCB(비어 있으면 NULL). zone 1→2 경계 이동을 O(1)로 처리
bottom_2zone 2의 마지막 BCB(비어 있으면 NULL). zone 2/3 경계 마커
victim_hint휘발성 victim 스캔 시작점. 매번 고정된 BCB를 다시 탐색하지 않도록 함
count_lru1/2/3존별 BCB 수. 재균형 및 victim 가용성 결정에 사용
count_vict_cand교체 가능한 BCB 수. victim 검색이 빈 리스트를 건너뛸 수 있게 함(챕터 9)
threshold_lru1/2zone 1/2의 목표 크기. BCB는 자신의 zone이 임계값을 초과하면 한 zone 아래로 내려감. zone 3이 나머지
quota개인 리스트의 목표 크기. 세션별 적응형(챕터 10). 공유 리스트에는 미사용
tick_listadd/boost 시 증가. BCB가 진입 tick을 저장하며, 차이가 오래됨 정도를 나타냄
tick_lru3BCB가 zone 3으로 내려갈 때 증가. victim-hint 순서를 위해 bcb->tick_lru3에 스탬프
flags리스트별 플래그 워드. 벌크/쿼터 리스트 상태를 표시
indexbuf_LRU_list[]에서 이 리스트의 인덱스. BCB flags 하위 16비트에 저장되어 BCB가 자신의 홈 리스트를 알 수 있음

불변 조건 — victim_hint는 실제 첫 번째 victim 이전으로 밀릴 수 있으며, 코드는 이를 허용한다. 힌트 아래의 모든 것은 더티 상태여야 하지만, TPCC 코어 덤프 분석에서 힌트가 실제 첫 번째 교체 가능 BCB 앞에 위치하는 사례가 발견되었다 — TODO로 표시된 미해결 알려진 버그다. 소비자는 힌트를 시작점으로만 취급하고 모든 후보를 재검증해야 한다. 힌트를 정확한 값으로 신뢰하면 유효한 victim을 건너뛰거나 더티 페이지를 교체하게 된다. 챕터 9에서 스캔 과정을 다룬다.

struct pgbuf_invalid_list{ pthread_mutex_t invalid_mutex (SERVER_MODE); PGBUF_BCB *invalid_top; int invalid_cnt; }이다.

필드Role & rationale
invalid_mutex (SM)프리 리스트 락. 프리 BCB의 push/pop을 직렬화
invalid_top프리 체인 헤드(BCB next_BCB로 연결). 미스 발생 시 victim화 전에 여기서 pop(챕터 4)
invalid_cnt프리 BCB 수. 체인을 순회하지 않고 “풀 비었음 → victim화” 결정 가능

프리 BCB는 PGBUF_INVALID_ZONE 상태이며 next_BCB만 사용한다. prev_BCB는 미사용이다 (§1.3 불변 조건 참조).

1.8 holder 삼형제 — 스레드별 fix 장부

섹션 제목: “1.8 holder 삼형제 — 스레드별 fix 장부”

스레드는 각 fix를 BCB가 아닌 자신이 소유하는 pgbuf_holder에 기록한다. 소유권 체인은 pgbuf_holder_anchor(스레드별 헤드) → pgbuf_holder(보유 중인 페이지당 하나) → pgbuf_holder_set(holder가 잘라서 사용하는 슬랩)이다.

// pgbuf_holder / _anchor / _set -- src/storage/page_buffer.c (condensed)
struct pgbuf_holder {
int fix_count; PGBUF_BCB *bufptr;
PGBUF_HOLDER *thrd_link, *next_holder; /* hold-list / free-list links */
PGBUF_HOLDER_STAT perf_stat; /* #if !NDEBUG: char fixed_at[64*1024]; int fixed_at_size; */
int watch_count; PGBUF_WATCHER *first_watcher, *last_watcher; };
// pgbuf_holder_anchor: { int num_free_cnt, num_hold_cnt; PGBUF_HOLDER *thrd_free_list, *thrd_hold_list; }
// pgbuf_holder_set: { PGBUF_HOLDER element[PGBUF_NUM_ALLOC_HOLDER /*==10*/]; PGBUF_HOLDER_SET *next_set; }
Struct.FieldRole & rationale
holder.fix_count이 BCB에 대한 재 fix 깊이. 마지막 unfix에서만 래치가 해제됨
holder.bufptr보유 중인 BCB. 스레드별 holder를 공유 BCB에 연결
holder.thrd_linkhold 리스트의 다음 항목. pgbuf_unfix_all이 보유 중인 모든 페이지를 순회할 수 있게 함
holder.next_holderfree 리스트의 다음 항목. 재할당 없이 슬롯 재활용
holder.perf_statPGBUF_HOLDER_STAT 플래그. 페이지 사용의 성능 계측
holder.fixed_at/fixed_at_size (dbg)fix 호출 위치 캡처. 어디서 fix했는지 디버그 추적
holder.watch_count이 holder에 연결된 watcher 수. 순서 fix watcher(챕터 10)가 holder에 매달림
holder.first_watcher/last_watcherwatcher 리스트 양끝. 순서 fix 중 O(1) 추가/분리
anchor.num_free_cnt/num_hold_cnt프리/사용 중 카운터. “새 슬랩 필요?” 빠른 판단
anchor.thrd_free_list/thrd_hold_list프리/hold 리스트 헤드. 스레드 자신의 fix를 추적하는 개인 뷰
holder_set.element[10]사전 할당된 holder 10개 슬랩. 묶음으로 배포되며 반환되지 않음
holder_set.next_set다음 슬랩. 전역 프리 holder 풀이 슬랩 리스트로 구성됨

1.9 pgbuf_watcher — 순서 fix 비트 필드

섹션 제목: “1.9 pgbuf_watcher — 순서 fix 비트 필드”
// struct pgbuf_watcher -- src/storage/page_buffer.h (debug magic/strings elided)
struct pgbuf_watcher {
PAGE_PTR pgptr; PGBUF_WATCHER *next, *prev;
PGBUF_ORDERED_GROUP group_id; /* VPID of group's HEAP header */
unsigned latch_mode:7; /* requested latch mode */
unsigned page_was_unfixed:1; /* set if any refix occurred */
unsigned initial_rank:4; /* rank at init */
unsigned curr_rank:4; }; /* rank after fix */
필드Role & rationale
pgptr감시되는 페이지 핸들. 호출자가 읽고 쓰는 대상
next/prevholder의 watcher 리스트 내 링크. 하나의 페이지에 여러 watcher가 붙을 수 있으며 O(1) 분리 가능
group_id그룹 힙 헤더의 VPID. 순서 fix는 데드락을 피하기 위해 그룹 내 페이지를 순서대로 fix함
latch_mode:7요청된 래치 모드. 작은 enum을 7비트에 꽉 채워 패킹
page_was_unfixed:1재정렬 중 unfix+refix 발생 시 설정. 캐시된 pgptr이 이동했을 수 있으므로 재검증 필요
initial_rank:4watcher 초기화 시 순위. 재정렬 전 원하는 fix 순서
curr_rank:4fix 후의 순위. 순서 위반 fix를 감지

이 비트들을 구동하는 순서 fix 재정렬 루프는 챕터 10에서 추적한다.

pgbuf_Pool이 모든 것을 하나로 묶는다. 수정자가 반드시 알아야 할 필드들:

필드Role & rationale
num_buffers총 BCB 프레임 수(≈ 10 × num_trans). 모든 테이블의 범위를 결정하는 고정 풀 크기
BCB_tablePGBUF_BCB[]. 제어 블록 배열
buf_hash_tablePGBUF_BUFFER_HASH[]. VPID 해시(1.5)
buf_lock_tablePGBUF_BUFFER_LOCK[]. 스레드당 하나의 대기 중인 읽기 레코드(1.5)
iopage_tablePGBUF_IOPAGE_BUFFER[]. 페이지 페이로드. BCB_table과 병렬 배열
num_LRU_list공유 LRU 리스트 수. buf_LRU_list의 첫 번째 슬라이스
ratio_lru1/ratio_lru2zone 1/2 크기 비율. 각 리스트의 threshold_lru1/2 초기값 결정
buf_LRU_listPGBUF_LRU_LIST[] 공유+가비지+개인. 하나의 배킹 배열, 인덱스로 종류 구분
buf_AOUT_listPGBUF_AOUT_LIST victim 히스토리. 2Q의 “Aout” 절반(챕터 7)
buf_invalid_listPGBUF_INVALID_LIST 프리 풀(1.7). 신규 BCB의 소스
victim_cand_listvictim 후보 배열. flush 데몬의 작업 세트(챕터 8)
seq_chkpt_flusherPGBUF_SEQ_FLUSHER. 속도 제어 체크포인트 flush 상태
monitorPGBUF_PAGE_MONITOR. 더티 카운트, LRU별 히트, victim/fix 카운터
quotaPGBUF_PAGE_QUOTA. 개인 리스트 쿼터 튜닝(챕터 10)
thrd_holder_infoPGBUF_HOLDER_ANCHOR[] 스레드별. 스레드별 holder 앵커(1.8)
thrd_reserved_holder모든 holder의 배킹 메모리. 사전 예약된 holder 공간
free_holder_set_mutex (SM)공유 프리 holder 풀 락. 슬랩 배포 직렬화
free_holder_set/free_index프리 항목이 있는 첫 슬랩, 첫 번째 프리 슬롯. 전역 holder 할당자 커서
check_for_interrupts인터럽트 확인이 필요할 때 설정. 로그 매니저가 TR_TABLE_CS 하에서 토글
is_flushing_victims/is_checkpoint (SM)데몬 상태 플래그. flush와 체크포인트를 조율
direct_victims (SM)victim 배열 + 두 개의 우선순위 waiter LFCQ. 직접 victim 전달(챕터 9)
flushed_bcbs (SM)flush 후 BCB의 LFCQ. flush 후 처리 큐
private/big_private/shared_lrus_with_victimsvictim이 있는 LRU 인덱스 세 개의 LFCQ. 전체 스캔 대신 victim 검색이 이를 참조(챕터 9)
show_status/_old/_snapshot/_mutexSHOW STATUS 보고 상태. SHOW 쿼리에 노출되는 통계

패킹된 flags 워드의 유일하게 승인된 읽기 경로. 나머지 코드는 flags를 직접 마스킹하지 않는다.

// pgbuf_bcb_get_zone / _get_lru_index / PGBUF_IS_BCB_IN_LRU -- page_buffer.c
STATIC_INLINE PGBUF_ZONE pgbuf_bcb_get_zone (const PGBUF_BCB * bcb)
{ return PGBUF_GET_ZONE (bcb->flags); } /* (flags & PGBUF_ZONE_MASK) */
STATIC_INLINE int pgbuf_bcb_get_lru_index (const PGBUF_BCB * bcb)
{ assert (PGBUF_IS_BCB_IN_LRU (bcb)); /* <- precondition */
return PGBUF_GET_LRU_INDEX (bcb->flags); } /* (flags & 0x0000FFFF) */
#define PGBUF_IS_BCB_IN_LRU(bcb) ((pgbuf_bcb_get_zone (bcb) & PGBUF_LRU_ZONE_MASK) != 0)

분기 분석:

  1. pgbuf_bcb_get_zone — 무조건 리턴 하나, 오류 경로 없음. 모든 유효한 flags는 다섯 가지 PGBUF_ZONE 값 중 정확히 하나를 반환한다. 손상된 flags는 호출자의 switch default로 넘어간다.
  2. pgbuf_bcb_get_lru_indexassert로 두 분기. 디버그, BCB가 LRU zone에 있음: assert 통과, 하위 16비트(홈 리스트의 index, §1.6) 반환. 디버그, LRU zone이 아님(INVALID/VOID): assert 발동 — 해당 비트는 무의미하다. 릴리스에서는 assert가 컴파일 아웃되어 하위 16비트를 그대로 반환하므로, 호출자가 선제 조건을 책임진다. 따라서 호출 지점은 PGBUF_IS_BCB_IN_LRU로 가드한다.
  3. PGBUF_IS_BCB_IN_LRU — 불리언 하나, 두 결과. zone을 PGBUF_LRU_ZONE_MASK (zone 1/2/3)와 AND. INVALID/VOID는 해당 마스크 밖의 비트를 설정하므로 false. LRU zone이면 true. 분기 2를 안전하게 만드는 게이트.
flowchart TB
  POOL["pgbuf_buffer_pool (pgbuf_Pool)"]
  POOL -->|BCB_table| BCB["pgbuf_bcb[]"]
  POOL -->|iopage_table| IOP["pgbuf_iopage_buffer[]"]
  POOL -->|buf_hash_table| HASH["pgbuf_buffer_hash[]"]
  POOL -->|buf_lock_table| LOCK["pgbuf_buffer_lock[]"]
  POOL -->|buf_LRU_list| LRU["pgbuf_lru_list[] 공유+가비지+개인"]
  POOL -->|buf_invalid_list| INV["pgbuf_invalid_list"]
  POOL -->|thrd_holder_info| ANC["pgbuf_holder_anchor[] 스레드별"]
  POOL -->|free_holder_set| SET["pgbuf_holder_set 슬랩"]
  HASH -->|hash_next| BCB
  HASH -->|lock_next| LOCK
  BCB -->|iopage_buffer| IOP
  IOP -->|bcb| BCB
  BCB -->|atomic_latch| LATCH["pgbuf_atomic_latch_impl union"]
  LRU -->|top/bottom/bottom_1/bottom_2/victim_hint| BCB
  INV -->|invalid_top via next_BCB| BCB
  ANC -->|thrd_hold_list| HLD["pgbuf_holder"]
  SET -->|element[10]| HLD
  HLD -->|bufptr| BCB
  HLD -->|first_watcher/last_watcher| WAT["pgbuf_watcher"]

Figure 1-2: 전체 포인터 파노라마. 이후 모든 챕터는 이 그림의 부분 그래프 위에서 동작한다.

  1. BCB 래치는 mutex가 아닌 64비트 원자 union이다. pgbuf_atomic_latch_impllatch_mode+waiter_exists+fcnt를 하나의 std::atomic<uint64_t>에 패킹하며, 항상 raw 워드 전체 단위로만 접촉한다. BCB의 mutex는 래치가 아닌 리스트/플래그 전환을 보호한다.
  2. flags는 하나의 워드에 세 가지를 담는다 — 플래그 바이트(24-31), zone(16-19, INVALID/VOID가 인덱스와 충돌하지 않도록 비트를 건너뜀), LRU index(0-15). §1.11 접근자로만 읽어야 한다.
  3. count_fix_and_avoid_dealloc은 두 카운터가 하나로 합쳐진 것이다 — 상위 16비트는 포화 fix 카운트, 하위 16비트는 원자적 avoid-dealloc 카운트. 2바이트 원자 연산이 이식성을 보장하지 않아 합쳐졌다.
  4. next_BCB는 LRU 리스트와 프리 리스트가 공유하며, zone이 단일 진실의 원천이다. 재링크와 zone 변경은 하나의 임계 구역이어야 한다.
  5. victim_hint는 조언적이며 실제 첫 번째 victim 이전으로 밀릴 수 있다 — 알려진 미해결 버그. 시작점으로만 취급하고 재검증해야 한다.
  6. iopage는 역방향 포인터와 정렬 패드와 함께 임베디드된다. PAGE_PTR → BCB 왕복이 가능하고 FILEIO_PAGE는 8바이트 정렬을 유지한다. 그 prv.lsa (페이지 말미에 prv2로 미러링)가 WAL/복구 워터마크다.
  7. fix는 holder 삼형제에서 스레드별로 장부 관리된다 (anchor → holder → 슬랩 10개). pgbuf_bcb_get_lru_indexPGBUF_IS_BCB_IN_LRU가 참일 때만 유효하며, assert가 그 선제 조건을 인코딩한다.

Chapter 2: 초기화와 메모리 레이아웃

섹션 제목: “Chapter 2: 초기화와 메모리 레이아웃”

이 챕터는 다음 질문에 답한다. 첫 번째 pgbuf_fix가 실행되기 전에, 서버 기동 시 모든 페이지 버퍼 구조체는 어디서 만들어지고, 각 테이블은 어떻게 크기가 결정되며, 할당되고, 서로 연결되는가? 상위 수준 개요 문서(cubrid-page-buffer-manager.md)의 CUBRID’s Approach 절은 이미 주요 플레이어들을 소개했다 — BCB, 페이지 테이블, invalid 리스트, 3존 LRU, private LRU, Aout, LFCQ — 그리고 Ch. 1은 구조체 맵을 제공한다. 이 챕터에서는 그것들을 다시 도출하지 않는다.

모든 것은 파일 스코프 객체 하나인 pgbuf_Pool(PGBUF_BUFFER_POOL 타입)에 산다. pgbuf_initialize가 총지휘자 역할을 한다. 이 함수는 pgbuf_Pool을 필드 단위로 초기화한 뒤 크기를 계산하고, 고정된 순서로 10개의 서브 초기화 함수를 호출한다 — 먼저 쿼터 파라미터(이 값이 LRU 개수를 확정하므로), 그다음 그에 의존하는 테이블들, 마지막으로 PGBUF_TOTAL_LRU_COUNT를 기준으로 크기가 결정되는 쿼터/모니터 배열 순이다.

2.1 pgbuf_initialize — 총지휘자와 크기 계산

섹션 제목: “2.1 pgbuf_initialize — 총지휘자와 크기 계산”

함수는 내장 서브구조체에 대한 memset과 함께 필드별 초기화로 시작한다. monitorstd::atomic_int 멤버는 memset을 쓸 수 없으므로 .store(0)으로 초기화한다. 이 수동 초기화 덕분에 pgbuf_finalize는 절반만 구축된 풀에서도 안전하게 동작한다. 모든 포인터는 어떤 할당이 일어나기 전에 NULL이 된다(free_index 같은 스칼라 센티널은 여기서 0으로 설정했다가 §2.9에서 -1로 재설정된다). num_buffers는 파라미터를 읽어 하한 처리하며, 두 LRU 존 비율은 각각 클램핑된다.

// pgbuf_initialize -- src/storage/page_buffer.c
pgbuf_Pool.num_buffers = prm_get_integer_value (PRM_ID_PB_NBUFFERS);
if (pgbuf_Pool.num_buffers < PGBUF_MINIMUM_BUFFERS) /* MAX_NTRANS * 10 */
pgbuf_Pool.num_buffers = PGBUF_MINIMUM_BUFFERS; /* <- 조용히 하한 적용, 오류 없음 */
pgbuf_Pool.ratio_lru1 = prm_get_float_value (PRM_ID_PB_LRU_HOT_RATIO);
pgbuf_Pool.ratio_lru2 = prm_get_float_value (PRM_ID_PB_LRU_BUFFER_RATIO);
pgbuf_Pool.ratio_lru1 = MAX (pgbuf_Pool.ratio_lru1, PGBUF_LRU_ZONE_MIN_RATIO); /* clamp lru1 into */
pgbuf_Pool.ratio_lru1 = MIN (pgbuf_Pool.ratio_lru1, PGBUF_LRU_ZONE_MAX_RATIO); /* [0.05f, 0.90f] */
pgbuf_Pool.ratio_lru2 = MAX (pgbuf_Pool.ratio_lru2, PGBUF_LRU_ZONE_MIN_RATIO); /* lru2 floor */
pgbuf_Pool.ratio_lru2 = MIN (pgbuf_Pool.ratio_lru2, 1.0f - PGBUF_LRU_ZONE_MIN_RATIO - pgbuf_Pool.ratio_lru1);

비율 값은 여기서 저장만 될 뿐이며, 실제로 LRU1/LRU2 임계값에 적용되는 것은 Ch. 6/7이다. 이어서 두 개의 assert가 따른다. ratio_lru2[0.05, 0.90] 범위를 유지하는지, 그리고 두 존의 합이 [0.099, 0.951] 범위 안에 있는지 검증한다. 서브 초기화 함수가 실패하면 goto error로 빠져 pgbuf_finalize를 호출한다.

flowchart TD
  A["pgbuf_initialize"] --> B["pgbuf_initialize_page_quota_parameters\nnum_private_LRU_list 확정"]
  B --> C["pgbuf_initialize_bcb_table\nBCB_table + iopage_table"]
  C --> D["pgbuf_initialize_hash_table\n버킷 2^20개"]
  D --> E["pgbuf_initialize_lock_table\n스레드당 레코드 하나"]
  E --> F["pgbuf_initialize_lru_list\nnum_LRU_list 확정, shared+private 구성"]
  F --> G["pgbuf_initialize_invalid_list\n모든 BCB가 여기서 시작"]
  G --> H["pgbuf_initialize_aout_list"]
  H --> I["pgbuf_initialize_thrd_holder\nholder 세트 사전 할당"]
  I --> J["pgbuf_initialize_page_quota\nTOTAL_LRU_COUNT 기준 배열 할당"]
  J --> K["pgbuf_initialize_page_monitor\nlru_hits/lru_activity 배열"]
  K --> L["victim_cand_list + seq_chkpt_flusher\n+ SERVER_MODE LFCQ + show_status"]
  B -.error.-> Z["goto error -> pgbuf_finalize"]
  C -.error.-> Z
  F -.error.-> Z
  L --> M["NO_ERROR"]

Figure 2-1. 10개의 서브 초기화 함수 호출 순서. 쿼터 파라미터가 가장 먼저 실행되는 이유는 num_private_LRU_list를 확정하기 위해서다. 이 값이 PGBUF_TOTAL_LRU_COUNT를 결정하고, 이 총합이 다시 LRU·쿼터·모니터 배열의 크기를 결정한다. invalid 리스트(G)가 Aout 리스트(H)보다 먼저 준비된다는 점에 주목하라.

10개의 서브 초기화 함수가 끝나면, 총지휘자는 victim_cand_list(버퍼 수만큼), 체크포인트 플러셔(MIN(0.25 * num_buffers, 65536) 크기)를 할당한다. SERVER_MODE에서는 direct-victim 배열(bcb_victims, 스레드당 하나)과 세 개의 lockfree::circular_queue 객체(waiter_threads_high_priority, waiter_threads_low_priority, flushed_bcbs)를 추가로 할당한다. private/big_private_lrus_with_victims 큐는 PGBUF_PAGE_QUOTA_IS_ENABLED 조건에서만 생성되고, shared_lrus_with_victims는 항상 생성된다. 마지막으로 show_status(MAX_NTRANS + 1 레코드)를 할당하고 초기화한다. 이 LFCQ들과 데몬에 대한 상세 설명은 Ch. 8–9에 있다.

불변 조건 — “첫 번째 할당 이전에 모든 포인터는 NULL이어야 한다.” 시행 방법: 함수 초반의 필드별 초기화에서 모든 포인터 멤버를 NULL로 만들어 놓으므로, error: 레이블은 어느 시점에서든 pgbuf_finalize를 호출할 수 있다. finalize의 모든 free 호출은 != NULL 조건으로 보호된다. 위반 시 결과: 포인터 필드가 추가됐는데 이 초기화에서 NULL로 설정되지 않으면, 초기화 중간 실패 시 free_and_init에 쓰레기 값이 전달되어 초기화되지 않은 메모리를 해제하는 사태가 벌어진다.

2.2 pgbuf_initialize_bcb_table — BCB/iopage 할당, 교차 연결, 정렬

섹션 제목: “2.2 pgbuf_initialize_bcb_table — BCB/iopage 할당, 교차 연결, 정렬”

두 개의 병렬 배열이 할당되며, 각각 MEM_SIZE_IS_VALID로 유효성을 검증한다. BCB_table(메타데이터, num_buffers * PGBUF_BCB_SIZEOF)과 iopage_table(페이지 프레임, num_buffers * PGBUF_IOPAGE_BUFFER_SIZE)이다. iopage 실패 경로(크기 불량·OOM 두 가지 모두)는 finalize에 의존하지 않고 직접 BCB_table을 롤백(free_and_init, != NULL 조건 보호)한 뒤 ER_PRM_BAD_VALUE 또는 ER_OUT_OF_VIRTUAL_MEMORY를 반환한다. 이후 BCB 단위 반복문에서 각 BCB를 초기화하고 iopage 프레임과 대칭적으로 교차 연결한다. next_BCB는 모든 BCB를 단방향 리스트(마지막은 NULL)로 연결하며, invalid 리스트가 이 체인을 그대로 상속한다.

// pgbuf_initialize_bcb_table -- src/storage/page_buffer.c
for (i = 0; i < pgbuf_Pool.num_buffers; i++)
{
bufptr = PGBUF_FIND_BCB_PTR (i); /* base + i * sizeof(PGBUF_BCB) */
pthread_mutex_init (&bufptr->mutex, NULL);
VPID_SET_NULL (&bufptr->vpid);
placement_new (&bufptr->atomic_latch, 0); /* C++ atomic needs placement-new, not memset */
bufptr->atomic_latch.store (impl.raw); /* impl = {mode INVALID, no waiter, fcnt 0}; Ch.5 */
bufptr->next_BCB = (i == pgbuf_Pool.num_buffers - 1) ? NULL : PGBUF_FIND_BCB_PTR (i + 1); /* chain */
bufptr->flags = PGBUF_BCB_INIT_FLAGS; /* == PGBUF_INVALID_ZONE, no other flag */
/* ... clear hash_next/prev_BCB/count_fix_and_avoid_dealloc/hit_age/oldest_unflush_lsa/ticks ... */
ioptr = PGBUF_FIND_IOPAGE_PTR (i); /* base + i * PGBUF_IOPAGE_BUFFER_SIZE */
/* ... fileio_init_lsa_of_page; set iopage.prv pageid/volid = -1, ptype UNKNOWN ... */
bufptr->iopage_buffer = ioptr; ioptr->bcb = bufptr; /* <- symmetric cross-link */
}
graph LR
  subgraph BCB_table
    b0["BCB[0]"]
    b1["BCB[1]"]
  end
  subgraph iopage_table
    p0["iopage[0]"]
    p1["iopage[1]"]
  end
  b0 -->|iopage_buffer| p0
  p0 -->|bcb| b0
  b1 -->|iopage_buffer| p1
  p1 -->|bcb| b1
  b0 -->|next_BCB| b1

Figure 2-2. 병렬 배열, 슬롯 단위 대칭 교차 연결, 그리고 invalid 리스트가 상속하는 next_BCB 체인.

불변 조건 — “iopage는 8바이트 정렬이어야 한다.” 시행 방법: struct pgbuf_iopage_bufferPGBUF_BCB *bcb를 첫 번째 필드로 배치하고, 32비트 빌드(__WORDSIZE == 32)에서는 명시적 int dummy를 삽입하여 이어지는 FILEIO_PAGE iopage가 8바이트 경계에서 시작하도록 보장한다(LINUX/WINDOWS/AIX가 아닌 지원하지 않는 플랫폼은 #error로 막힌다). PGBUF_IOPAGE_BUFFER_SIZE(offsetof(.., iopage) + SIZEOF_IOPAGE_PAGESIZE_AND_GUARD())가 PGBUF_FIND_IOPAGE_PTR에서 i를 곱하는 stride이므로, 모든 프레임이 정렬을 유지한다. 위반 시 결과: dummy를 제거하면 버퍼가 잘못 정렬되어 direct I/O의 동작이 정의되지 않는다.

2.3 pgbuf_initialize_hash_table — 고정 2^20 버킷 페이지 테이블

섹션 제목: “2.3 pgbuf_initialize_hash_table — 고정 2^20 버킷 페이지 테이블”

페이지 테이블 크기는 컴파일 타임 상수로, num_buffers와 무관하다.

// pgbuf_initialize_hash_table -- src/storage/page_buffer.c
hashsize = PGBUF_HASH_SIZE; /* (1 << HASH_SIZE_BITS) == 1 << 20 == 1048576 */
pgbuf_Pool.buf_hash_table = (PGBUF_BUFFER_HASH *) malloc (hashsize * PGBUF_BUFFER_HASH_SIZEOF);
/* ... OOM check; loop: pthread_mutex_init each hash_mutex; hash_next = lock_next = NULL ... */

버킷 수를 2의 거듭제곱으로 설정하면 최종 해시 단계가 단순 AND 연산 하나로 끝난다 — pgbuf_hash_func_mirrorhash_val & ((1 << HASH_SIZE_BITS) - 1)으로 마무리되어 나머지 연산이나 나눗셈이 필요 없다(단, 이 함수는 XOR 전에 짧은 반복문으로 volid의 하위 8비트를 상위 비트로 역순 배치하는 처리를 여전히 수행한다). 각 버킷은 자체 hash_mutex를 보유한다(SERVER_MODE 한정) — 전역 페이지 테이블 락은 없다. hash_next를 따라가는 순회는 Ch. 3에서 다룬다.

2.4 pgbuf_initialize_lock_table — 스레드당 버퍼 락 레코드 하나

섹션 제목: “2.4 pgbuf_initialize_lock_table — 스레드당 버퍼 락 레코드 하나”

버퍼 락 테이블은 스레드 인덱스로 접근하는 슬롯 하나씩을 각 서버 스레드에 제공한다. 이 테이블은 미스 처리 중의 랑데부 지점으로 사용된다(Ch. 4):

// pgbuf_initialize_lock_table -- src/storage/page_buffer.c
thrd_num_total = thread_num_total_threads (); /* SA mode asserts thrd_num_total == 1 */
pgbuf_Pool.buf_lock_table = (PGBUF_BUFFER_LOCK *) malloc (thrd_num_total * PGBUF_BUFFER_LOCK_SIZEOF);
/* ... OOM check; loop: VPID_SET_NULL(vpid); lock_next = NULL; (SERVER_MODE) next_wait_thrd = NULL ... */

스레드 수 기준으로 크기를 잡는 이유는 스레드 하나가 동시에 하나의 미스만 처리하기 때문이다. 레코드는 어떤 VPID를 가져오든 재사용된다.

2.5 pgbuf_initialize_lru_list — shared + private 리스트 수 확정과 리스트별 초기화

섹션 제목: “2.5 pgbuf_initialize_lru_list — shared + private 리스트 수 확정과 리스트별 초기화”

이 초기화 함수는 먼저 num_LRU_list(shared 개수)를 확정한다. 파라미터가 0이 아니면 그대로 사용하고, 0이면 자동 계산한다.

// pgbuf_initialize_lru_list -- src/storage/page_buffer.c
pgbuf_Pool.num_LRU_list = prm_get_integer_value (PRM_ID_PB_NUM_LRU_CHAINS);
if (pgbuf_Pool.num_LRU_list == 0)
{
pgbuf_Pool.num_LRU_list = (int) MAX_NTRANS; /* default: one shared list per transaction slot */
if (pgbuf_Pool.num_buffers / pgbuf_Pool.num_LRU_list < PGBUF_MIN_PAGES_IN_SHARED_LIST) /* 1000 */
pgbuf_Pool.num_LRU_list = pgbuf_Pool.num_buffers / PGBUF_MIN_PAGES_IN_SHARED_LIST; /* coarsen */
pgbuf_Pool.num_LRU_list = MAX (pgbuf_Pool.num_LRU_list, 4); /* floor: at least 4 shared LRUs */
}

분기 로직 요약: 트랜잭션당 리스트 하나를 기본으로 하되, 리스트당 페이지가 1000개 미만이면 더 굵게 조정하고, 최소 4개 이상은 보장한다. 할당은 shared + private 합산(PGBUF_TOTAL_LRU_COUNT = PGBUF_SHARED_LRU_COUNT + PGBUF_PRIVATE_LRU_COUNT, 후자는 §2.7의 num_private_LRU_list)을 대상으로 한다. Private 리스트는 상위 인덱스 범위를 차지하며, i >= PGBUF_SHARED_LRU_COUNT이면 PGBUF_IS_PRIVATE_LRU_INDEX(i)가 true가 된다.

// pgbuf_initialize_lru_list -- src/storage/page_buffer.c
pgbuf_Pool.buf_LRU_list = (PGBUF_LRU_LIST *) malloc (PGBUF_TOTAL_LRU_COUNT * PGBUF_LRU_LIST_SIZEOF);
/* ... OOM check; loop over PGBUF_TOTAL_LRU_COUNT lists: ... */
pgbuf_Pool.buf_LRU_list[i].index = i; /* self-index, used to recover list from a BCB */
/* ... pthread_mutex_init; top/bottom/bottom_1/bottom_2 = NULL; counts/victim_hint/ticks cleared ... */
pgbuf_Pool.buf_LRU_list[i].threshold_lru1 = 0; /* <- initial threshold ZERO, set later */
pgbuf_Pool.buf_LRU_list[i].threshold_lru2 = 0; pgbuf_Pool.buf_LRU_list[i].quota = 0;
pgbuf_Pool.buf_LRU_list[i].flags = 0;

shared와 private 모두 같은 반복문을 사용하며, 인덱스 범위만 다를 뿐 구조체는 동일하다. 임계값과 quota는 §2.1의 비율이 아닌 0에서 시작한다. 실제 값은 num_buffers가 분배되고 나서 쿼터 메커니즘(Ch. 7/10)이 채운다. 초기화 시점에는 모든 리스트가 비어 있으므로 0은 올바른 초기값이다.

2.6 pgbuf_initialize_invalid_list와 Aout 리스트

섹션 제목: “2.6 pgbuf_initialize_invalid_list와 Aout 리스트”

invalid(free) 리스트는 가장 단순한 초기화 함수다. 헤드를 BCB[0]으로 가리키고, §2.2에서 구성된 next_BCB 체인을 그대로 신뢰한다.

// pgbuf_initialize_invalid_list -- src/storage/page_buffer.c
pthread_mutex_init (&pgbuf_Pool.buf_invalid_list.invalid_mutex, NULL);
pgbuf_Pool.buf_invalid_list.invalid_top = PGBUF_FIND_BCB_PTR (0); /* head of the next_BCB chain */
pgbuf_Pool.buf_invalid_list.invalid_cnt = pgbuf_Pool.num_buffers; /* every BCB starts invalid */

불변 조건 — “모든 BCB는 invalid 리스트에서 시작한다.” 시행 방법: 모든 BCB의 플래그가 PGBUF_INVALID_ZONE(§2.2)이고 동시에 invalid_cnt == num_buffers — 동일한 사실을 두 군데 저장하는 셈이다. 위반 시 결과: 처음 num_buffers번의 미스는 eviction 없이 여기서 BCB를 꺼내 쓴다. 카운트와 플래그가 맞지 않으면 BCB가 이중 계수되거나 누락될 수 있어, Ch. 7은 BCB가 이동할 때마다 두 값을 동기화한다.

Aout 리스트(pgbuf_initialize_aout_list, 구조체 pgbuf_aout_list)는 재결함(re-fault) 시 해당 페이지가 최근에 evict됐는지 판단하기 위한 eviction 이력을 기록한다. 용량은 num_buffers * aout_ratio(aout_ratio = prm_get_float_value(PRM_ID_PB_AOUT_RATIO))로 계산하되, PGBUF_LIMIT_AOUT_BUFFERS(32768)를 상한으로 한다. 비율이 0 이하면 Aout을 비활성화한다(max_count = 0, Aout_mutex를 초기화한 곧바로 return NO_ERROR). 그렇지 않으면 max_count개의 PGBUF_AOUT_BUF 노드로 구성된 bufarray를 미리 할당하여 free 리스트(Aout_free, bufarray[0]부터 시작)로 연결하고, num_hashes = MAX(max_count / AOUT_HASH_DIVIDE_RATIO, 1)개의 MHT 테이블을 구성한다. error_return 경로는 Aout_free를 NULL로 만들고, bufarray를 해제한 뒤, 첫 번째 NULL 슬롯에서 멈추는 루프(for (i = 0; list->aout_buf_ht[i] != NULL; i++))로 MHT를 파괴한다 — 실제로 생성된 테이블만 파괴하는 방식으로, 전체 num_hashes를 순회하는 pgbuf_finalize 루프와는 다르다. 그런 다음 aout_buf_ht를 해제하고, Aout_mutex를 파괴하며, ER_FAILED를 반환한다.

PGBUF_AOUT_LIST(Aout 컨테이너):

필드역할존재 이유
Aout_mutex (SERVER_MODE)Aout 리스트 전체 보호eviction/재결함 시마다 이력이 변경됨
Aout_top가장 최근에 evict된 쪽 끝최신 이력 항목
Aout_bottom가장 오래된 쪽 끝리스트가 꽉 찼을 때 버려지는 항목
Aout_freefree 노드 리스트의 헤드노드는 사전 할당되어 삽입 시 malloc 불필요
bufarray모든 노드의 단일 할당 블록노드별 할당보다 블록 하나가 효율적
num_hashesMHT 룩업 테이블 수샤딩으로 경합 감소
aout_buf_htMHT 테이블 배열, VPID to 노드”이 페이지가 최근 evict됐나?”를 O(1)로 판단
max_count용량; 0이면 비활성화메모리 한도이자 on/off 스위치

PGBUF_PAGE_QUOTA(적응형 private LRU 크기 조정 — §2.7에서 채워짐):

필드역할존재 이유
num_private_LRU_listprivate LRU 수; 0이면 쿼터 비활성화private LRU 기능의 마스터 스위치
lru_victim_flush_priority_per_lruLRU별 플러시 우선순위(TOTAL_LRU_COUNT개 float)flush 데몬에게 dirty 압력이 어디 있는지 알림
private_lru_session_cntprivate LRU별 활성 세션 수세션이 0인 리스트는 회수 가능
private_pages_ratio전체 BCB 중 private BCB 비율쿼터 조정기가 목표로 하는 값
add_shared_lru_idxshared 리스트로 재배치 시 라운드로빈 커서BCB를 shared 리스트 전반에 균등 분산
avoid_shared_lru_idx재배치 시 건너뛸 shared LRU과부하된 리스트에 몰리는 것을 방지
last_adjust_time마지막 쿼터 조정 타임스탬프조정 빈도 제한
adjust_age단조 증가하는 조정 카운터쿼터 상태의 버전 관리
is_adjusting조정기의 재진입 방지 플래그한 번에 하나의 스레드만 쿼터 조정 가능

PGBUF_PAGE_MONITOR(LRU별 통계 — §2.8에서 채워짐):

필드역할존재 이유
dirties_cntdirty BCB 수(INT64)flush 긴급도 판단
lru_hitsLRU별 LRU1 히트 수(TOTAL_LRU_COUNT개 int)쿼터 튜닝을 위한 최신성 품질 신호
lru_activityLRU별 활동 수준유휴 private 리스트 감지 및 회수 판단
lru_shared_pgs_cnt모든 shared LRU에 걸친 BCB 수(volatile)private_pages_ratio의 보완 지표
pg_unfix_cntunfix 카운터(std::atomic_int)주기적 쿼터 갱신 트리거
lru_victim_req_cnt전체 LRU에 걸친 victim 요청 수victim 압력 게이지
fix_req_cntfix 요청 수(std::atomic_int)전체 부하 게이지
bcb_locks (SERVER_MODE)스레드별 BCB 뮤텍스 사용 추적락 경합 진단
victim_richvictim이 충분할 때 truefix 코드의 빠른 경로 힌트

2.7 쿼터 부트스트랩 — pgbuf_initialize_page_quota_parameters_page_quota

섹션 제목: “2.7 쿼터 부트스트랩 — pgbuf_initialize_page_quota_parameters와 _page_quota”

두 단계로 나눈 것은 의도적인 설계다. 파라미터 초기화는 BCB/LRU 테이블보다 먼저 실행되어야 한다. num_private_LRU_list를 확정해야 하고(PGBUF_TOTAL_LRU_COUNT의 의존성), 데이터 초기화는 그 이후에 실행되어야 한다. 그 합계를 기준으로 배열을 할당하기 때문이다.

// pgbuf_initialize_page_quota_parameters -- src/storage/page_buffer.c
quota = &(pgbuf_Pool.quota); memset (quota, 0, sizeof (PGBUF_PAGE_QUOTA));
tsc_getticks (&quota->last_adjust_time); quota->adjust_age = 0; quota->is_adjusting = 0;
#if defined (SERVER_MODE)
quota->num_private_LRU_list = prm_get_integer_value (PRM_ID_PB_NUM_PRIVATE_CHAINS);
if (quota->num_private_LRU_list == -1)
quota->num_private_LRU_list = MAX_NTRANS + VACUUM_MAX_WORKER_COUNT; /* auto: one per worker */
else if (quota->num_private_LRU_list == 0)
{ /* disabled */ } /* <- explicit no-op branch */
else if (quota->num_private_LRU_list < PGBUF_PRIVATE_LRU_MIN_COUNT) /* 4 */
quota->num_private_LRU_list = PGBUF_PRIVATE_LRU_MIN_COUNT; /* floor when user-set */
#else
quota->num_private_LRU_list = 0; /* SA_MODE: no private LRUs */
#endif

결과 요약: -1(자동)은 MAX_NTRANS + VACUUM_MAX_WORKER_COUNT가 되고, 0은 비활성화된 채로 남고, 양수이지만 4 미만이면 4로 올리며, SA 모드는 항상 0이다. 이 정수가 전역의 PGBUF_PAGE_QUOTA_IS_ENABLED(> 0) 조건을 구동한다. 데이터 초기화 함수는 이어서 두 배열을 할당하고 세션 카운트를 초기화한다.

// pgbuf_initialize_page_quota -- src/storage/page_buffer.c
quota->lru_victim_flush_priority_per_lru = (float *) malloc (PGBUF_TOTAL_LRU_COUNT * sizeof (float)); /* ALL lists */
quota->private_lru_session_cnt = (int *) malloc (PGBUF_PRIVATE_LRU_COUNT * sizeof (int)); /* PRIVATE only */
/* ... each OOM -> error_status, goto exit; loop zeros priority for all, session_cnt only where ... */
/* ... PGBUF_IS_PRIVATE_LRU_INDEX(i) holds, indexed via PGBUF_PRIVATE_LIST_FROM_LRU_INDEX(i) ... */
quota->private_pages_ratio = PGBUF_PAGE_QUOTA_IS_ENABLED ? 1.0f : 0; /* start fully private if enabled */
quota->add_shared_lru_idx = 0; quota->avoid_shared_lru_idx = -1;

두 실패 모두 단일 exit: 레이블로 모이고(여기서 error_status를 반환), 총지휘자의 goto error가 finalize를 실행해 이미 할당된 메모리를 해제한다.

쿼터 데이터 초기화와 대칭적으로, 모니터는 먼저 포인터 멤버를 다시 NULL로 만들고, PGBUF_TOTAL_LRU_COUNT 크기의 LRU별 정수 배열 두 개를 할당한다.

// pgbuf_initialize_page_monitor -- src/storage/page_buffer.c
monitor->lru_hits = (int *) malloc (PGBUF_TOTAL_LRU_COUNT * sizeof (int));
monitor->lru_activity = (int *) malloc (PGBUF_TOTAL_LRU_COUNT * sizeof (int));
/* ... each OOM -> goto exit; loop zeros both; lru_victim_req_cnt/lru_shared_pgs_cnt = 0 ... */
monitor->fix_req_cnt.store (0); monitor->pg_unfix_cnt.store (0); /* atomics: .store, not memset */
#if defined (SERVER_MODE)
if (pgbuf_Monitor_locks) /* forced true in !NDEBUG; param-driven in NDEBUG */
monitor->bcb_locks = (PGBUF_MONITOR_BCB_MUTEX *) calloc (count_threads, sizeof (PGBUF_MONITOR_BCB_MUTEX));
#endif
monitor->victim_rich = false; /* no BCBs in lists yet, so no victims */

bcb_locks는 스레드별(thread_num_total_threads()로 크기 결정)로 할당되며, 락 모니터링이 켜진 경우에만 생성된다(pgbuf_Monitor_locks는 §2.1에서 설정된다 — 디버그 빌드에서는 강제로 true, NDEBUG에서는 PRM_ID_PB_MONITOR_LOCKS로 제어). 모든 오류 경로는 exit:으로 집결한다.

2.9 pgbuf_initialize_thrd_holder — 스레드별 holder 풀 사전 할당

섹션 제목: “2.9 pgbuf_initialize_thrd_holder — 스레드별 holder 풀 사전 할당”

holder는 스레드가 BCB를 fix하고 있다는 것을 기록하는 구조체다. 각 스레드는 PGBUF_DEFAULT_FIX_COUNT(7)개의 holder로 구성된 private free 리스트를 가지므로, 일반적인 fix 경로에서는 메모리 할당이 발생하지 않는다.

// pgbuf_initialize_thrd_holder -- src/storage/page_buffer.c
thrd_num_total = thread_num_total_threads ();
pgbuf_Pool.thrd_holder_info = (PGBUF_HOLDER_ANCHOR *) malloc (thrd_num_total * PGBUF_HOLDER_ANCHOR_SIZEOF);
pgbuf_Pool.thrd_reserved_holder = (PGBUF_HOLDER *) malloc (thrd_num_total * PGBUF_DEFAULT_FIX_COUNT * PGBUF_HOLDER_SIZEOF);
/* ... each OOM check; per-thread anchor i: num_hold_cnt=0, num_free_cnt=7, thrd_hold_list=NULL ... */
pgbuf_Pool.thrd_holder_info[i].thrd_free_list = &(pgbuf_Pool.thrd_reserved_holder[i * PGBUF_DEFAULT_FIX_COUNT]);
/* ... inner loop chains the 7 reserved holders via next_holder, last == NULL ... */
pthread_mutex_init (&pgbuf_Pool.free_holder_set_mutex, NULL);
pgbuf_Pool.free_holder_set = NULL; pgbuf_Pool.free_index = -1; /* -1 == no shared free holder; grow on demand */

예약 holder는 하나의 플랫 배열을 i * PGBUF_DEFAULT_FIX_COUNT로 슬라이싱하여 스레드별로 나눈다. 스레드가 동시에 7개 이상을 fix하면 pgbuf_allocate_thrd_holder_entry가 공유 free_holder_set으로 폴백하는데, 이것은 PGBUF_HOLDER_SET 블록(PGBUF_NUM_ALLOC_HOLDER = 10개 요소씩) 단위로 malloc되고 finalize 전까지 해제되지 않는다. free_index == -1은 “풀이 비었으니 키워라”는 센티널로, §2.1의 초기화에서 잠시 0이었다가 여기서 -1로 재설정된다.

2.10 pgbuf_thread_variables_init — 워커가 private LRU 인덱스를 획득하는 시점

섹션 제목: “2.10 pgbuf_thread_variables_init — 워커가 private LRU 인덱스를 획득하는 시점”

워커의 THREAD_ENTRY가 온라인 상태가 될 때 호출되며, 워커를 자신의 private LRU와 holder anchor에 연결한다.

// pgbuf_thread_variables_init -- src/storage/page_buffer.c
if (!thread_p) return;
if (pgbuf_Pool.quota.num_private_LRU_list > 0 && thread_p->private_lru_index != -1)
thread_p->m_is_private_lru_enabled = true; /* quota on AND this worker has a private slot */
else
thread_p->m_is_private_lru_enabled = false;
if (!thread_p->m_holder_anchor)
thread_p->m_holder_anchor = &pgbuf_Pool.thrd_holder_info[thread_p->index]; /* bind to its slice */

private_lru_indexTHREAD_ENTRY에 존재하며(기본값 -1), 트랜잭션이 private 리스트를 획득하면 다른 곳에서 할당된다. 이 함수는 그것을 해석할 뿐이다. 워커가 private LRU를 사용하는 조건은 쿼터가 활성화됐고 그리고 인덱스가 -1이 아닌 경우다. anchor 바인딩은 멱등적(!m_holder_anchor 조건 보호)이며 §2.9의 슬라이스에 O(1) 접근을 제공한다. Vacuum 워커와 SA 모드는 false로 처리되어 shared LRU만 사용한다.

해제는 초기화의 역순이 아니다. NULL 조건으로 보호된 free 호출의 평탄한 시퀀스로 구성되며, §2.1의 불변 조건 덕분에 어느 시점에서든 안전하다. (1) 해시 테이블 — 모든 hash_mutex 파괴, buf_hash_table 해제; (2) 락 테이블 — buf_lock_table 해제; (3) BCB 테이블 — 모든 BCB mutex 파괴, BCB_table 해제, num_buffers = 0; (4) iopage_table 해제; (5) LRU 리스트 — 모든 리스트 뮤텍스 파괴, buf_LRU_list 해제; (6) invalid_mutex 파괴; (7) thrd holder — thrd_holder_info/thrd_reserved_holder 해제, free_holder_set_mutex 파괴, 지연 성장한 free_holder_set 블록 순회 해제; (8) victim_cand_list, Aout(bufarray 해제, num_hashes 슬롯 각각 mht_destroy, aout_buf_ht 해제, Aout_mutex 파괴, 필드 초기화), seq_chkpt_flusher.flush_list 순; (9) 쿼터 배열; (10) 모니터 배열 + (SERVER_MODE) bcb_locks; (11) SERVER_MODE: direct_victims.bcb_victims 해제, 두 waiter 큐와 flushed_bcbsdelete; (12) 세 개의 _lrus_with_victims 큐를 delete; (13) show_statusfree하고 뮤텍스 파괴; (14) thread_clear_all_holder_anchor() — §2.10의 대칭 역작업.

C++ 객체(lockfree::circular_queue)는 new로 생성됐으므로 free_and_init 대신 delete를 사용한다. 혼용하면 힙이 손상된다. num_buffers는 일찍(3단계) 0으로 초기화되어 경쟁 상태의 독자가 빈 풀을 보게 한다. 모든 free!= NULL로 보호되고 포인터가 §2.1에서 NULL로 초기화되어 있으므로, 풀이 완전히 구축됐든 초기화 중간에 실패했든 finalize는 올바르게 동작한다.

  1. 10개의 서브 초기화 함수, 고정된 순서. pgbuf_initializepgbuf_Pool을 필드별로 초기화(atomic은 .store)하고 10개의 서브 초기화 함수를 호출한다. 쿼터 파라미터가 먼저(num_private_LRU_list 확정), 쿼터/모니터 데이터가 마지막(PGBUF_TOTAL_LRU_COUNT 기준 크기 결정).
  2. num_buffers는 하한 처리만 할 뿐 오류 검증은 없다(MAX_NTRANS * 10 미만이면 조용히 올림). 각 LRU 존 비율은 독립적으로 클램핑된다(lru1은 [0.05, 0.90], lru2는 하한 0.05에 합산 여유를 남기는 상한). 저장만 할 뿐 즉시 적용하지 않는다.
  3. BCB와 iopage는 병렬 배열로 대칭 교차 연결된다. next_BCB 체인이 invalid 리스트에 상속된다. 32비트 빌드의 int dummy 패딩이 iopage의 8바이트 정렬을 보장한다.
  4. 페이지 테이블은 고정 2^20 버킷으로, 각 버킷에 자체 hash_mutex가 있다(전역 락 없음). 2의 거듭제곱 크기가 최종 해시 단계를 단순 AND 연산으로 만든다. 락 테이블과 holder 풀은 버퍼 수가 아닌 스레드 수 기준으로 크기가 결정된다.
  5. 모든 BCB는 invalid 리스트에서 시작한다(invalid_cnt == num_buffers, 모든 플래그 PGBUF_INVALID_ZONE) — 동일한 사실을 두 군데 저장. LRU 임계값은 모든 리스트가 비어 있으므로 0에서 시작한다.
  6. 쿼터는 정수 하나가 스위치다num_private_LRU_list(-1 자동, 0 비활성화, 양수는 4로 하한 처리, SA 모드는 0)가 PGBUF_PAGE_QUOTA_IS_ENABLED를 구동한다. 워커가 private LRU를 사용하는 조건은 쿼터가 켜져 있고 동시에 THREAD_ENTRY.private_lru_index != -1이어야 한다.
  7. finalize는 NULL 조건으로 보호된 평탄한 시퀀스로, 부분 구축 상태에서도 안전하다. C++ 큐는 delete, C 배열은 free를 사용하며, 마지막에 스레드별 holder-anchor 역참조를 정리한다. 이 챕터에서 구성만 하는 데몬 집합은 Ch. 8에서 다룬다.

Chapter 3: Fix 진입 경로와 페이지 테이블 조회

섹션 제목: “Chapter 3: Fix 진입 경로와 페이지 테이블 조회”

모든 페이지 접근은 pgbuf_fix로 진입한다(릴리스 빌드에서는 pgbuf_fix_release, !NDEBUG 환경에서는 pgbuf_fix_debug로 컴파일된다). 이 챕터는 해당 함수를 하나의 상태 기계(state machine)로 해부한다. 인자 검증 시점부터, hit 시 래칭(Chapter 5)으로 제어가 넘어가거나 miss 시 BCB 클레임(Chapter 4)으로 제어가 넘어가는 순간까지를 다룬다. 전체 흐름, 구역(zone)·플래그·BCB 구조체의 의미는 cubrid-page-buffer-manager.md### How a page fix flows, ### Page table — VPID hash, ### Buffer Control Block — PGBUF_BCB 절을 참조한다. 분기의 가장 큰 원인은 PAGE_FETCH_MODE이며, 이 열거형의 일곱 가지 값은 잠금 없는 빠른 경로(lock-free fast path), miss 분기, 페이지 VPID 검사, 그리고 종료 직전의 PAGE_UNKNOWN 스위치에서 반복적으로 등장한다.

// PAGE_FETCH_MODE -- src/storage/page_buffer.h
typedef enum
{
OLD_PAGE = 0, /* must already exist on disk or in buffer */
NEW_PAGE, /* newly allocated; may be created in buffer */
OLD_PAGE_IF_IN_BUFFER, /* return only if resident; never fix from disk */
OLD_PAGE_PREVENT_DEALLOC, /* fetch + mark to block dealloc */
OLD_PAGE_DEALLOCATED, /* deliberately fetch a deallocated page */
OLD_PAGE_MAYBE_DEALLOCATED, /* fetch, tolerate deallocated (warn) */
RECOVERY_PAGE /* recovery: new/old/deallocated all valid */
} PAGE_FETCH_MODE;
모드검증 건너뜀 여부Miss 시 디스크에서 클레임?종료 시점 PAGE_UNKNOWN 페이지 처리
OLD_PAGE아니오assert(false) + ER_ERROR_SEVERITY ER_PB_BAD_PAGEID, unfix 후 NULL 반환
NEW_PAGE아니오예 (버퍼 내 생성)허용, 반환
OLD_PAGE_IF_IN_BUFFERpgbuf_is_valid_page에서 오류 억제아니오 — miss 시 NULL 반환허용, 반환
OLD_PAGE_PREVENT_DEALLOC아니오OLD_PAGE와 동일: assert(false), unfix, NULL
OLD_PAGE_DEALLOCATED아니오허용, 반환
OLD_PAGE_MAYBE_DEALLOCATED아니오경고 ER_PB_BAD_PAGEID, unfix 후 NULL 반환
RECOVERY_PAGE페이지 검증 블록 전체를 우회허용, 반환

3.2 인자 검증과 unconditional→conditional 다운그레이드

섹션 제목: “3.2 인자 검증과 unconditional→conditional 다운그레이드”

pgbuf_fix_release는 공유 상태를 건드리기 전에 검증을 수행한다. 네 가지 가드가 순서대로 실행되며, 각각 조기 return NULL로 이어진다. 처음 두 개는 assert_release (false) 검사로, 잘못된 request_mode(R/W 이외)나 condition을 거부한다. 그 다음 pgbuf_Pool.monitor.fix_req_cnt가 증가하고, 페이지 검증 및 pageid 가드가 뒤따른다.

// pgbuf_fix_release -- src/storage/page_buffer.c
if (pgbuf_get_check_page_validation_level (PGBUF_DEBUG_PAGE_VALIDATION_FETCH)
&& fetch_mode != RECOVERY_PAGE) /* <- recovery skips validation */
{
if (pgbuf_is_valid_page (thread_p, vpid, fetch_mode == OLD_PAGE_IF_IN_BUFFER) != DISK_VALID)
return NULL; /* IF_IN_BUFFER suppresses errors */
}
if (vpid->pageid < 0) /* <- always-on cheap check */
{
er_set (ER_FATAL_ERROR_SEVERITY, ARG_FILE_LINE, ER_PB_BAD_PAGEID, 2, ...);
return NULL; /* fatal: ER_FATAL_ERROR_SEVERITY */
}

페이지 검증 블록은 디버그 검증이 활성화되어 있고 모드가 RECOVERY_PAGE가 아닐 때만 실행된다. 복구 과정에서는 디스크 메타데이터상 존재하지 않는 페이지를 합법적으로 fix할 수 있기 때문이다. OLD_PAGE_IF_IN_BUFFER의 경우 두 번째 인자가 true여서 오류 로그가 억제된다. “유효하지 않음”은 이 모드에서 정상적인 상황이기 때문이다. 결정적인 변환은 그 다음에 온다: condition == PGBUF_UNCONDITIONAL_LATCH이고 pgbuf_find_current_wait_msecs (thread_p)LK_ZERO_WAIT 또는 LK_FORCE_ZERO_WAIT이면, condition은 조용히 PGBUF_CONDITIONAL_LATCH로 변경된다.

불변 조건 — 제로 대기 트랜잭션은 페이지 latch에서 절대 블록되지 않는다. 이 다운그레이드는 여기서, 즉 해싱이나 래칭이 발생하기 전에 이루어지며, 이후의 모든 처리는 변경된 condition을 기준으로 동작한다. 이 단계를 건너뛰면 제로 대기 트랜잭션이 pgbuf_latch_bcb_upon_fix에서 무기한 대기 상태에 빠질 수 있다.

3.3 try_again 루프와 인터럽트 검사

섹션 제목: “3.3 try_again 루프와 인터럽트 검사”

성능 추적 샘플링은 루프 재진입 지점인 try_again: 레이블 직전에 이루어진다.

// pgbuf_fix_release -- src/storage/page_buffer.c
try_again:
if (logtb_get_check_interrupt (thread_p) == true)
if (logtb_is_interrupted (thread_p, true, &pgbuf_Pool.check_for_interrupts) == true)
{
er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_INTERRUPTED, 0);
PGBUF_BCB_CHECK_MUTEX_LEAKS (); /* <- assert no mutex held on exit */
return NULL;
}

인터럽트 검사는 루프 내부에 위치하므로 재시도할 때마다 인터럽트 여부를 다시 확인한다. try_again으로 되돌아오는 구문은 단 하나뿐이다: miss 경로의 pgbuf_claim_bcb_for_fixretry 출력 매개변수를 설정한 채 NULL을 반환하는 경우(BCB 클레임 경쟁 조건; Chapter 4). §3.2의 가드들은 레이블 에 위치하여 단 한 번만 실행된다.

// pgbuf_fix_release -- src/storage/page_buffer.c (miss fork, retry edge)
bufptr = pgbuf_claim_bcb_for_fix (thread_p, vpid, fetch_mode, hash_anchor, &perf, &retry, false);
if (bufptr == NULL)
{
if (retry) { retry = false; goto try_again; } /* <- the only re-entry */
ASSERT_ERROR ();
return NULL;
}

pgbuf_fix_with_retrypgbuf_fix를 감싸는 얇은 래퍼이며, 루프의 일부가 아니다. NULL을 반환하는 동안 pgbuf_fix를 반복 호출하되 er_errid()를 기준으로 분기한다: NO_ERROR/ER_INTERRUPTEDi를 증가시키지 않고 재시도하고, 세 가지 타임아웃 오류(ER_LK_UNILATERALLY_ABORTED, ER_LK_PAGE_TIMEOUT, ER_PAGE_LATCH_TIMEDOUT)는 i++를 수행하며, 그 외 오류는 noretry를 설정한다. noretry || i > retry 조건이 충족되면 ER_PAGE_LATCH_ABORTED와 함께 루프를 종료한다. 따라서 인터럽트는 재시도 횟수를 소모하지 않으며, 다른 오류는 즉시 종료된다.

페이지 테이블은 PGBUF_HASH_VALUE로 인덱싱되며, pgbuf_hash_func_mirror를 호출한다.

// pgbuf_hash_func_mirror -- src/storage/page_buffer.c
#define HASH_SIZE_BITS 20 /* 2^20 ~ 1M anchors, fixed */
#define VOLID_LSB_BITS 8
reverse_mask = 1 << (HASH_SIZE_BITS - 1); /* top bit of the 20-bit space */
for (i = VOLID_LSB_BITS; i > 0; i--) /* bit-reverse low 8 volid bits */
{ if (volid_lsb & lsb_mask) reversed_volid_lsb |= reverse_mask;
reverse_mask >>= 1; lsb_mask <<= 1; }
hash_val = vpid->pageid ^ reversed_volid_lsb; /* XOR pageid with mirrored volid */
hash_val = hash_val & ((1 << HASH_SIZE_BITS) - 1); /* clamp to 2^20 buckets */

“미러(mirror)” 기법은 volid의 하위 8비트를 비트 반전하여 20비트 공간의 상위 비트 영역에 배치한 뒤, 하위 비트를 지배하는 pageid와 XOR 연산을 수행한다. 이렇게 하면 서로 다른 볼륨이 상위 비트 시그니처를 겹치지 않게 가지며, 볼륨이 달라도 인접한 id들이 같은 체인을 공유하지 않는다.

보조 함수 두 개는 Aout 피해자 이력 mht 테이블(Chapter 7)에 사용되며, 주 페이지 테이블과는 무관하다. pgbuf_hash_vpid는 범용 모듈로 해시인 ((vpid->pageid | ((unsigned int) vpid->volid) << 24) % htsize)이고, pgbuf_compare_vpid는 이에 대응하는 정렬 콜백이다(같은 볼륨이면 pageid 차이, 다른 볼륨이면 volid 차이). 주 buf_hash_tablepgbuf_hash_func_mirror만 사용하며 비교는 VPID_EQ로 수행한다.

3.5 잠금 없는 읽기 전용 빠른 경로

섹션 제목: “3.5 잠금 없는 읽기 전용 빠른 경로”

앵커 mutex를 획득하기 전에, 이미 존재하는 페이지에 대한 읽기 fix는 잠금 없이 fix를 시도한다. 이 가드가 활성화되려면 다음 네 조건이 모두 충족되어야 한다: request_mode == PGBUF_LATCH_READ, fetch_mode가 세 가지 허용 모드 중 하나, condition == PGBUF_UNCONDITIONAL_LATCH. 따라서 §3.2 다운그레이드를 거친 제로 대기 트랜잭션은 이 경로를 사용할 수 없다. pgbuf_lockfree_fix_ro가 non-NULL을 반환하면 num_hit을 증가시키고 goto fast_path로 이동하여 해시 탐색과 latch 단계를 우회한다. 이 함수는 잠금 없는 체인 탐색 후 BCB latch 워드에 CAS 연산을 수행한다.

// pgbuf_lockfree_fix_ro -- src/storage/page_buffer.c
bufptr = pgbuf_search_hash_chain_no_bcb_lock (thread_p,
&pgbuf_Pool.buf_hash_table[PGBUF_HASH_VALUE (vpid)], vpid);
if (bufptr == NULL) return NULL; /* not resident -> slow path */
do {
impl = get_impl (&bufptr->atomic_latch); new_impl = impl;
if (impl.impl.latch_mode != PGBUF_LATCH_READ /* must already be read-latched */
|| impl.impl.waiter_exists || impl.impl.fcnt == 0 /* no writer queued, still held */
|| bufptr->vpid.pageid != vpid->pageid /* re-validate identity ... */
|| bufptr->vpid.volid != vpid->volid) /* ... against ABA reuse */
return NULL; /* any failure -> slow path */
new_impl.impl.fcnt++; /* bump fix count */
} while (!bufptr->atomic_latch.compare_exchange_weak (impl.raw, new_impl.raw,
std::memory_order_acq_rel, std::memory_order_acquire));

불변 조건 — 빠른 경로는 이미 읽기 latch가 걸린 BCB에 독자를 추가하는 것만 허용한다. CAS는 latch가 PGBUF_LATCH_READ이고, fcnt != 0이며, 대기자가 없을 때만 성공한다. 따라서 free/write 상태에서 업그레이드하거나 대기 중인 write 요청을 굶기는 일이 없으며, 루프 내 VPID 재검사는 ABA 재사용을 차단한다. 어떤 조건 하나라도 실패하면 슬로우 경로로 NULL을 반환하며, 오류를 발생시키지 않는다.

빠른 경로가 사용하는 체인 탐색 함수 pgbuf_search_hash_chain_no_bcb_lock은 단순하다: mutex나 trylock 없이 hash_anchor->hash_next를 포인터 체이싱하며 첫 번째 VPID_EQ 일치 항목을 반환한다. 동기화는 위의 CAS가 전담한다.

CAS에 성공하면 반환 전에 홀더 부기(bookkeeping) 작업이 남는다. pgbuf_find_thrd_holder는 호출자가 이미 홀더인 경우(holder->fix_count 증가, hold_has_read_latch 설정) 또는 SERVER_MODE에서 pgbuf_allocate_thrd_holder_entry로 새 홀더를 할당하는 경우를 처리한다. 후자에서 NULL이 반환되면 assert(false) 후 NULL을 반환한다. 그 다음에야 CAST_BFPTR_TO_PGPTRPAGE_PTR을 생성하고 호출자가 fast_path:에 도달한다.

3.6 잠금 기반 해시 체인 탐색과 hit/miss 분기

섹션 제목: “3.6 잠금 기반 해시 체인 탐색과 hit/miss 분기”

빠른 경로를 건너뛰거나 NULL을 반환한 경우, 슬로우 경로는 hash_anchor를 설정하고 buf_lock_acquired를 초기화한 뒤 pgbuf_search_hash_chain을 호출한다. 반환된 BCB가 직접 피해자(pgbuf_bcb_is_direct_victim)이면 pgbuf_bcb_update_flags (..., PGBUF_BCB_INVALIDATE_DIRECT_VICTIM_FLAG, ...)로 피해자 대기자에게 이 BCB를 사용할 수 없음을 알린다.

탐색 대상인 앵커는 buf_hash_table[]의 슬롯 하나인 PGBUF_BUFFER_HASH이다.

필드역할이유
hash_mutex버킷 단위 pthread_mutex_t (SERVER_MODE 전용)체인 삽입/제거와 버퍼 락 체인을 직렬화한다. 2단계 탐색 중 유일한 mutex 구간. 전역 잠금이 아닌 버킷 단위 잠금이므로 서로 다른 버킷은 동시에 처리 가능하다.
hash_nextBCB 해시 체인의 헤드 (PGBUF_BCB *)pgbuf_search_hash_chain이 각 BCB의 hash_next를 따라 포인터 체이싱하는 체인. 이 버킷에 상주하는 페이지들이 여기에 연결된다.
lock_next버퍼 락 체인의 헤드 (PGBUF_BUFFER_LOCK *)스레드가 클레임했지만 아직 삽입하지 않은 VPID를 기록한다(miss 경로, Chapter 4). 같은 VPID에 대한 두 번째 fixer가 중복 클레임 대신 대기하도록 한다. hash_mutex의 보호를 받는다.

pgbuf_search_hash_chain이 핵심 함수이며, 두 단계 탐색과 엄격한 반환 계약을 갖는다 — non-NULL이면 호출자가 bufptr->mutex를 보유한 것이고 (해시 mutex가 아님), NULL이면 호출자가 hash_anchor->hash_mutex를 보유한 것이다.

1단계(one_phase:)는 해시 mutex 없이 체인을 탐색하며, 일치하는 BCB에 비블록 PGBUF_BCB_TRYLOCK을 시도한다. 일치하는 bufptr 각각에 대한 핵심 처리:

// pgbuf_search_hash_chain -- src/storage/page_buffer.c (one_phase core)
rv = PGBUF_BCB_TRYLOCK (bufptr);
if (rv != 0)
{ if (rv != EBUSY) goto two_phase; /* trylock error -> escalate */
PGBUF_BCB_LOCK (bufptr); } /* EBUSY -> block on the bcb mutex */
if (!VPID_EQ (&(bufptr->vpid), vpid)) /* bcb reused under us? */
{ PGBUF_BCB_UNLOCK (bufptr); goto one_phase; } /* <- restart phase 1 */
break; /* matched + locked -> return bufptr */

1단계에서 나가는 분기는 세 가지이다(Figure 3-1): 깔끔한 trylock + VPID 재검사 성공(return bufptr), EBUSY → 블록 PGBUF_BCB_LOCK 후 재검사, 비-EBUSY 오류 → goto two_phase 에스컬레이션. 잠금 후 재검사는 일치와 잠금 사이에 슬롯이 재사용된 경우를 잡아낸다.

2단계(two_phase:/try_again:)는 해시 mutex를 잡고 동일한 탐색을 다시 수행하며, 세 가지 점이 다르다: 깔끔한 trylock 성공 시 반환 전에 해시 mutex를 해제하고, EBUSY이면 블록 PGBUF_BCB_LOCK 이전에 해시 mutex를 해제한 뒤 goto try_again으로 재검증하며, 비-EBUSY 실패는 치명적er_set_with_oserror (ER_CSS_PTHREAD_MUTEX_TRYLOCK) 후 NULL을 반환한다.

불변 조건 — 잠금 순서는 해시 mutex 다음에 BCB mutex이며, 절대 역순이 아니다. 2단계는 블록 PGBUF_BCB_LOCK 전에 반드시 해시 mutex를 먼저 해제한다. 순서를 뒤집으면 삽입/제거 경로와 교착 상태가 발생한다. ER_CSS_PTHREAD_MUTEX_TRYLOCK 분기는 이 함수가 해시 mutex를 보유하지 않은 채로 NULL을 반환하는 유일한 경우이며 — OS 수준의 치명적 장애를 의미한다.

flowchart TD
  A["pgbuf_search_hash_chain"] --> B["one_phase: 체인 탐색, 해시 mutex 없음"]
  B --> C{"VPID 일치?"}
  C -- "아니오, 체인 끝" --> TP["two_phase"]
  C -- "예" --> D["PGBUF_BCB_TRYLOCK"]
  D -- "rv==0" --> E{"VPID 여전히 동일?"}
  D -- "EBUSY" --> F["PGBUF_BCB_LOCK 블록"]
  D -- "기타 오류" --> TP
  F --> E
  E -- "아니오, 재사용됨" --> B
  E -- "예" --> R1["bufptr 반환, BCB mutex 보유"]
  TP --> G["hash_mutex 획득; 체인 탐색"]
  G --> H{"VPID 일치?"}
  H -- "아니오, 끝" --> R2["NULL 반환, 해시 mutex 보유"]
  H -- "예" --> I["PGBUF_BCB_TRYLOCK"]
  I -- "rv==0 또는 EBUSY" --> JK["hash_mutex 해제; EBUSY면 PGBUF_BCB_LOCK"]
  I -- "기타 오류" --> ERR["치명적 오류: NULL 반환"]
  JK --> L{"VPID 여전히 동일?"}
  L -- "아니오" --> G
  L -- "예" --> R3["bufptr 반환, BCB mutex 보유"]

pgbuf_fix_release로 돌아와, 반환된 bufptr은 hit/miss 분기를 세 가지 결과로 이끈다.

  1. Hit (bufptr != NULL): num_hit 증가. NEW_PAGE이면, 페이지가 clean-LSA이거나 dirty임을 assert한다(버퍼에 남아 있는 무효화된 페이지를 재사용하는 NEW_PAGE 경우). 제어는 pgbuf_bcb_register_fix와 latch 단계로 넘어간다(Chapter 5).

  2. OLD_PAGE_IF_IN_BUFFER miss: 이 모드는 디스크에서 읽지 않으므로 해시 mutex를 해제하고 return NULL한다 — miss를 단락시키는 유일한 모드이다.

  3. 일반 miss: pgbuf_claim_bcb_for_fix를 호출한다(Chapter 4). NULL이고 retry이면 goto try_again, NULL이고 retry가 아니면 ASSERT_ERROR 후 NULL 반환, 성공이면 buf_lock_acquired = true를 설정하고 페이지 VPID 재검사로 계속 진행한다.

3.7 클레임 후 VPID 재검사와 maybe_deallocated 분기

섹션 제목: “3.7 클레임 후 VPID 재검사와 maybe_deallocated 분기”

hit 또는 클레임 성공 후 호출자는 bufptr->mutex를 보유한다. pgbuf_bcb_register_fixpgbuf_set_bcb_page_vpid가 실행된 뒤 페이지 정체성을 재검증한다.

// pgbuf_fix_release -- src/storage/page_buffer.c
maybe_deallocated = (fetch_mode == OLD_PAGE_MAYBE_DEALLOCATED);
if (pgbuf_check_bcb_page_vpid (bufptr, maybe_deallocated) != true)
{
if (buf_lock_acquired)
{ pgbuf_put_bcb_into_invalid_list (thread_p, bufptr); /* releases bcb mutex */
(void) pgbuf_unlock_page (thread_p, hash_anchor, vpid, true); }
else
{ PGBUF_BCB_UNLOCK (bufptr); } /* hit case: just unlock */
PGBUF_BCB_CHECK_MUTEX_LEAKS ();
return NULL;
}
if (fetch_mode == OLD_PAGE_PREVENT_DEALLOC)
pgbuf_bcb_register_avoid_deallocation (bufptr); /* pin against dealloc */

maybe_deallocated 플래그는 pgbuf_check_bcb_page_vpid의 조건을 완화하여 OLD_PAGE_MAYBE_DEALLOCATED 모드에서 할당 해제된 VPID가 실패로 처리되지 않도록 한다. 정리 분기는 소유권에 따라 다르다: 새로 클레임한 경우 (buf_lock_acquired)는 무효 목록으로 재활용되고 페이지 락이 해제되며, hit 경우는 BCB만 잠금 해제된다. 이 지점을 지나면 함수는 latch 단계(Chapter 5)로 진입하고, 성공 시 fast_path:로 점프하여 §3.1의 PAGE_UNKNOWN 스위치가 실행된다 — fetch mode가 최종 결과를 조종하는 마지막 지점이다.

  1. pgbuf_fix_release는 상태 기계이다: 네 개의 조기 반환 검증, 그 다음 try_again 루프 — 루프의 유일한 재진입 경로는 pgbuf_claim_bcb_for_fixretry 출력 매개변수를 통한 BCB 클레임 경쟁 조건이다.
  2. 제로 대기 트랜잭션(LK_ZERO_WAIT/LK_FORCE_ZERO_WAIT)은 unconditional fix 요청이 해싱 전에 conditional로 다운그레이드된다.
  3. 잠금 없는 빠른 경로(pgbuf_lockfree_fix_ro)는 unconditional 요청에서 세 가지 허용 모드의 읽기 latch를 담당한다. CAS는 이미 읽기 latch가 걸려 있고 대기자가 없는 BCB에만 독자를 추가하며, VPID를 ABA 방지 차원에서 재검증한다.
  4. pgbuf_hash_func_mirror는 volid 하위 8비트를 20비트 공간의 상위 비트 영역으로 비트 반전하고 pageid와 XOR한다. pgbuf_hash_vpid/ pgbuf_compare_vpid는 별도의 Aout mht 테이블 전용이다.
  5. pgbuf_search_hash_chain은 두 단계이며, 반환 계약(non-NULL이면 BCB mutex 보유, NULL이면 해시 mutex 보유)과 엄격한 해시→BCB 잠금 순서는 핵심 불변 조건이다.
  6. hit/miss 분기: hit → latch 단계(Ch. 5), OLD_PAGE_IF_IN_BUFFER miss → 즉시 NULL, 일반 miss → BCB 클레임(Ch. 4) 및 재시도.
  7. fetch mode는 네 지점을 조종한다 — 빠른 경로 자격, miss 단락 조건, maybe_deallocated 재검사, 그리고 최종 PAGE_UNKNOWN 스위치. 따라서 fix 관련 버그는 호출자의 모드 선택에서 출발해 추적해야 한다.

Chapter 4: 미스 처리 — BCB 획득과 PGBUF 할당 락

섹션 제목: “Chapter 4: 미스 처리 — BCB 획득과 PGBUF 할당 락”

3장은 pgbuf_fix 조회가 페이지 테이블에 없음을 반환하는 지점에서 끝났다. 이 장이 답하는 질문은 다음과 같다: 스레드는 어떻게 VPID를 경쟁 할당자로부터 보호하고, invalid 리스트나 victim에서 새 BCB를 확보하고, 페이지 바이트를 읽어 BCB를 해시 체인에 삽입하는가? 동반 문서(cubrid-page-buffer-manager.md §“How a page fix flows”, §“PGBUF lock”)가 Step 1 / Step 2를 개략적으로 설명한다. 여기서는 모든 분기를 상세히 추적한다. 독자는 PGBUF_BCB 레이아웃, 다섯 가지 존(zone), 1–3장 및 동반 문서 §“LFCQ”의 victim 소스를 이미 알고 있다고 가정한다.

미스 경로는 네 겹의 양파 구조다: pgbuf_claim_bcb_for_fix(외부 조율자)가 버킷별 VPID 락을 pgbuf_lock_page로 획득하고, pgbuf_allocate_bcb(소스 선택자: invalid 리스트, 그 다음 victim, 그 다음 direct-victim 큐에서 대기)를 호출한다. 그 중 가장 저렴한 소스는 pgbuf_get_bcb_from_invalid_list다. Victim 탐색(pgbuf_get_victim)과 direct-victim 핸드오프는 9장에서 다루는 블랙박스다.

4.1 pgbuf_invalid_list — 프리 풀, 모든 필드

섹션 제목: “4.1 pgbuf_invalid_list — 프리 풀, 모든 필드”

Invalid 리스트는 어떤 페이지에도 바인딩되지 않은 BCB들의 풀이다(서버 시작 시 모든 BCB; 런타임에 오류로 롤백되거나 무효화된 BCB). 하나의 mutex로 보호되는 LIFO 스택이다.

// struct pgbuf_invalid_list -- src/storage/page_buffer.c
struct pgbuf_invalid_list
{
#if defined(SERVER_MODE)
pthread_mutex_t invalid_mutex; /* integrity of the singly-linked list */
#endif
PGBUF_BCB *invalid_top; /* head of the list (LIFO) */
int invalid_cnt; /* # of entries */
};
필드역할존재 이유
invalid_mutex스택의 push/pop을 직렬화없으면 두 팝퍼가 동일한 head를 동시에 가져갈 수 있다. SERVER_MODE 전용 — SA 모드는 단일 스레드다.
invalid_topHead 포인터; 체인은 bufptr->next_BCB로 연결Invalid BCB는 어떤 LRU 리스트에도 없으므로, invalid 체인 연결로 next_BCB LRU 포인터를 재사용한다 — 별도 필드 없음.
invalid_cnt사용 가능한 BCB의 실시간 카운트할당량 계산(pgbuf_adjust_quotas, 10장)에서 읽힌다; mutex를 잡기 전에 풀이 소진되었는지 빠르게 확인하는 용도이기도 하다.

불변 조건 — invalid_top은 오직 next_BCB로만 체인을 이루며, BCB가 invalid 리스트에 있는 것은 그 존이 PGBUF_INVALID_ZONE인 경우와 동치다. pgbuf_get_bcb_from_invalid_list는 팝한 BCB를 PGBUF_VOID_ZONE으로 전환한다; pgbuf_put_bcb_into_invalid_list는 다시 INVALID로 되돌리고, (bufptr->flags & PGBUF_BCB_FLAGS_MASK) == 0을 단언한다 — 풀로 돌아가는 BCB에는 dirty/flushing/victim 플래그가 없어야 한다. 위반 시 BCB가 미결 flush를 광고하는 상태로 프리 풀에 재진입하고, 이후 획득자가 오래된 페이지 바이트를 깨끗한 새 페이지로 취급하게 된다.

flowchart LR
  IT["invalid_top"] --> B1["BCB a"]
  B1 -->|next_BCB| B2["BCB b"]
  B2 -->|next_BCB| B3["BCB c"]
  B3 -->|next_BCB| NUL["NULL"]

그림 4-1 — Invalid 리스트는 각 BCB의 next_BCB 포인터로 엮인 LIFO 스택이며, invalid_cnt가 그 길이를 추적한다.

pgbuf_get_bcb_from_invalid_list — 이중 검사 잠금 방식의 팝

섹션 제목: “pgbuf_get_bcb_from_invalid_list — 이중 검사 잠금 방식의 팝”

이것이 pgbuf_allocate_bcb의 가장 저렴한 소스다. 락 프리 빠른 경로를 사용하여 BCB 하나를 팝하므로, “풀 비어 있음”이 빈번한 경우 mutex를 건드리지 않는다.

// pgbuf_get_bcb_from_invalid_list -- src/storage/page_buffer.c
if (pgbuf_Pool.buf_invalid_list.invalid_top == NULL) /* (1) fast path: empty */
return NULL; /* no mutex taken */
rv = pthread_mutex_lock (&pgbuf_Pool.buf_invalid_list.invalid_mutex);
if (pgbuf_Pool.buf_invalid_list.invalid_top == NULL) /* (2) re-check under mutex */
{ pthread_mutex_unlock (...); return NULL; } /* someone emptied it */
else /* (3) pop the LIFO top */
{ bufptr = pgbuf_Pool.buf_invalid_list.invalid_top;
pgbuf_Pool.buf_invalid_list.invalid_top = bufptr->next_BCB; /* advance head */
pgbuf_Pool.buf_invalid_list.invalid_cnt -= 1;
pthread_mutex_unlock (...);
PGBUF_BCB_LOCK (bufptr); /* now hold bufptr->mutex */
bufptr->next_BCB = NULL; /* sever invalid-chain link */
pgbuf_bcb_change_zone (thread_p, bufptr, 0, PGBUF_VOID_ZONE); /* INVALID -> VOID */
return bufptr; }

세 가지 분기: (1) 락 없이 빈 상태를 확인하고 NULL 반환 (mutex 없음). (2) mutex 획득 후 재확인 — 두 읽기 사이에 경쟁 팝퍼가 리스트를 비웠다면 NULL 반환. (3) 실제 팝 — invalid_top을 전진시키고, invalid_cnt를 감소시키고, 리스트 mutex를 해제한 뒤 BCB를 잠그고, 체인 링크를 null로 끊고, PGBUF_VOID_ZONE으로 전환하여 bufptr->mutex를 보유한 채 반환.

4.2 pgbuf_claim_bcb_for_fix — 외부 조율자

섹션 제목: “4.2 pgbuf_claim_bcb_for_fix — 외부 조율자”

Fix 경로는 페이지 테이블 미스 시 이 함수를 호출한다. 계약이 독특하다: hash_anchor->hash_mutex를 보유한 채 진입하며, 그것을 해제하고 나가거나, *try_again을 설정하거나, bufptr->mutex를 보유한 완전히 로드된 BCB를 반환할 수 있다. 네 가지 출구 분기:

// pgbuf_claim_bcb_for_fix -- src/storage/page_buffer.c
/* Branch A: a prior trylock on the bucket failed -> bail, no retry. */
if (er_errid () == ER_CSS_PTHREAD_MUTEX_TRYLOCK)
{ pthread_mutex_unlock (&hash_anchor->hash_mutex); return NULL; }
/* Branch B: take the VPID lock; hash_mutex is released inside. */
if (!already_locked && pgbuf_lock_page (...) != PGBUF_LOCK_HOLDER)
{ *try_again = true; return NULL; } /* <- LOSER of a same-VPID race */
bufptr = pgbuf_allocate_bcb (thread_p, vpid);
if (bufptr == NULL) /* Branch C: pool dirty / interrupted */
{ ASSERT_ERROR (); (void) pgbuf_unlock_page (..., true); return NULL; }
/* Branch D: success. Scrub the fresh BCB. */
bufptr->vpid = *vpid;
/* atomic_latch <- {PGBUF_NO_LATCH, waiter=false, fcnt=0}; clears stale victim latch */
pgbuf_bcb_update_flags (..., 0, PGBUF_BCB_ASYNC_FLUSH_REQ); /* clear stray flag */
LSA_SET_NULL (&bufptr->oldest_unflush_lsa); /* nothing unflushed yet */

분기 A — 버킷에 대한 trylock 실패 시 오류 슬롯에 ER_CSS_PTHREAD_MUTEX_TRYLOCK이 남는다; mutex를 해제하고 *try_again을 건드리지 않은 채 NULL을 반환한다 (호출자가 false로 사전 초기화했으므로 goto try_again이 발화하지 않고, pgbuf_fix 자체의 재시도 루프가 조회를 다시 구동한다). 분기 B — 레이스 프로토콜: already_locked는 dealloc 인식 호출자에서만 true다. HOLDER → VPID를 소유하므로 다음으로 진행. WAITER → 다른 스레드가 할당 중; pgbuf_lock_page에서 이미 수면했으므로 *try_again = true로 설정하고, 호출자의 goto try_again이 조회를 재실행하여 승자가 삽입한 BCB에 히트한다. 분기 C (§4.4 할당 실패) — VPID 락을 되돌려야 한다: mutex를 보유하지 않으므로 pgbuf_unlock_page(..., true)hash_anchor->hash_mutex를 재획득하여 레코드를 제거한다. 분기 D — BCB를 초기화한다; atomic_latch 리셋이 victim의 낡은 PGBUF_LATCH_INVALID를 지운다 (실제 latch는 5장에서 획득한다). 이후 바이트가 로드된다(§4.5).

flowchart TD
  S["hash_mutex를 보유한 채 진입"] --> A{"errid == TRYLOCK?"}
  A -- yes --> AR["hash_mutex 해제; NULL 반환\ntry_again 미변경, false 유지"]
  A -- no --> B{"pgbuf_lock_page\n== HOLDER?"}
  B -- "WAITER" --> BR["try_again=true; NULL 반환\n-> 호출자 재조회, 히트"]
  B -- "HOLDER" --> C["pgbuf_allocate_bcb"]
  C --> D{"bufptr == NULL?"}
  D -- yes --> DR["unlock_page need_hash=true\nNULL 반환; 오류 전파"]
  D -- no --> E["BCB 초기화; 바이트 로드 -> 4.5"]
  E --> G["bufptr->mutex 보유 채 BCB 반환"]

그림 4-2 — pgbuf_claim_bcb_for_fix 분기 맵. B-WAITER와 성공 분기만 호출자에게 다른 상태를 남기며, 두 오류 분기는 모두 VPID 락을 해제한다.

4.3 pgbuf_lock_page / pgbuf_unlock_page — VPID 레이스 락

섹션 제목: “4.3 pgbuf_lock_page / pgbuf_unlock_page — VPID 레이스 락”

PGBUF 락은 BCB latch도 아니고 버킷 mutex도 아니다. 이것은 VPID를 키로 하는 논리 락으로, BCB 체인과 동일한 해시 버킷에 달린 별도 체인 위에 있다: 아직 BCB가 없는 VPID에 정확히 한 스레드만 할당하도록 보장한다. 락 레코드 pgbuf_buffer_lock은 스레드당 하나씩 정적으로 사전 할당된다(핫 경로에서 malloc 없음):

// struct pgbuf_buffer_lock -- src/storage/page_buffer.c
struct pgbuf_buffer_lock
{
VPID vpid; /* the VPID being allocated */
PGBUF_BUFFER_LOCK *lock_next; /* next record on this bucket's lock chain */
#if defined(SERVER_MODE)
THREAD_ENTRY *next_wait_thrd; /* FIFO of threads blocked on this VPID */
#endif
};

pgbuf_lock_pagehash_anchor->hash_mutex를 보유한 채 진입하며 반환 전에 항상 이를 해제한다. 두 가지 분기:

// pgbuf_lock_page -- src/storage/page_buffer.c
for (cur = hash_anchor->lock_next; cur != NULL; cur = cur->lock_next)
if (VPID_EQ (&cur->vpid, vpid)) /* LOSER: VPID already being allocated */
{
cur_thrd_entry->next_wait_thrd = cur->next_wait_thrd;
cur->next_wait_thrd = cur_thrd_entry; /* push onto waiter FIFO */
pgbuf_sleep (cur_thrd_entry, &hash_anchor->hash_mutex); /* releases mutex, sleeps */
if (cur_thrd_entry->resume_status != THREAD_PGBUF_RESUMED)
{ /* woke for interrupt: re-take mutex, splice self out of waiter list */ }
return PGBUF_LOCK_WAITER;
}
/* WINNER: VPID absent. Claim this thread's static record. */
cur = &pgbuf_Pool.buf_lock_table[cur_thrd_entry->index];
cur->vpid = *vpid; cur->next_wait_thrd = NULL;
cur->lock_next = hash_anchor->lock_next; hash_anchor->lock_next = cur; /* link at head */
pthread_mutex_unlock (&hash_anchor->hash_mutex);
return PGBUF_LOCK_HOLDER;

패자 분기 (VPID가 체인에 있음): 해당 레코드의 next_wait_thrd FIFO에 push하고; pgbuf_sleephash_mutex를 해제하고 현재 스레드를 중단한다. resume_status != THREAD_PGBUF_RESUMED 서브 분기는 승자가 unlock하지 않고 인터럽트로 스레드가 깨어난 경우를 처리한다: hash_mutex를 재획득하고 대기자 리스트에서 자신을 제거하여, 이후 pgbuf_unlock_page가 이미 떠난 스레드를 깨우지 않도록 한다. 결과는 WAITER. 승자 분기 (VPID 없음): 이 스레드의 정적 레코드를 클레임하고, 체인 head에 연결하고, HOLDER 반환.

불변 조건 — BCB 없는 VPID 할당은 동시에 최대 하나만 진행 중이다. 승자가 hash_mutex 하에서 레코드를 설치한 후에 이를 해제하므로, 동일 mutex 하의 이후 스캐너는 레코드를 발견하고 대기자가 된다. 두 승자가 생기면 하나의 VPID에 두 개의 BCB가 만들어져 조회가 비결정적이 되고 한 복사본의 쓰기가 유실된다.

pgbuf_unlock_page는 그 거울상이다. need_hash_mutex는 함수가 직접 hash_anchor->hash_mutex를 획득할지(오류 경로, true), 아니면 호출자가 이미 보유 중인지(pgbuf_insert_into_hash_chain 이후 성공 경로, false)를 나타낸다.

// pgbuf_unlock_page -- src/storage/page_buffer.c
if (need_hash_mutex) pthread_mutex_lock (&hash_anchor->hash_mutex);
/* find this VPID's record; if found, unlink it ... */
if (cur != NULL)
{
/* splice out of lock_next chain */
pthread_mutex_unlock (&hash_anchor->hash_mutex);
while ((t = cur->next_wait_thrd) != NULL) /* wake EVERY waiter */
{ cur->next_wait_thrd = t->next_wait_thrd; t->next_wait_thrd = NULL;
pgbuf_wakeup_uncond (t); }
}
else pthread_mutex_unlock (&hash_anchor->hash_mutex); /* record gone (error case) */

레코드를 제거하고 mutex를 해제한 뒤 모든 대기자를 깨운다. 깨어난 각 패자는 fix를 다시 구동하여 테이블에서 BCB를 찾아(성공 경로에서 unlock 전에 삽입됨) 3장 해시 히트 경로로 진행한다. mutex 해제 후에 깨우는 것은 즉각적인 재경합을 피하기 위해서다.

4.4 pgbuf_allocate_bcb — 소스 선택자

섹션 제목: “4.4 pgbuf_allocate_bcb — 소스 선택자”

VPID 락 승자는 실제 BCB가 필요하다. 선택자는 비용 순서대로 세 가지 소스를 시도한다.

// pgbuf_allocate_bcb -- src/storage/page_buffer.c
bufptr = pgbuf_get_bcb_from_invalid_list (thread_p); /* Source 1: free list, cheapest */
if (bufptr != NULL) return bufptr; /* short-circuit: SKIPS the 'end:' victimize */
bufptr = pgbuf_get_victim (thread_p); /* Source 2: scan LFCQs */
if (bufptr != NULL) goto end; /* victim still needs pgbuf_victimize_bcb */

소스 1은 단락 반환한다: invalid 리스트에서 온 BCB(§4.1)는 어떤 페이지에도 바인딩되지 않고 플래그도 없으므로 즉시 반환한다 — end:에 도달하지 않아 victimize를 수행하지 않는다. 소스 2는 다르다: pgbuf_get_victim이 반환될 때 victim은 이미 LRU 리스트에서 제거되고 PGBUF_VOID_ZONE으로 전환된 상태다 — pgbuf_get_victim_from_lru_list가 BCB를 건네기 전에 pgbuf_remove_from_lru_list(unlink + 존 전환 수행)를 호출하기 때문이다(§4.6). 아직 남아 있는 것은 해시 체인에서의 BCB 링크와 이전 latch다. 바로 이 때문에 소스 2는 end:로 폴스루해야 한다: pgbuf_victimize_bcb가 해시 체인에서 분리하고 latch를 무효화한다. 즉 goto end는 LRU 제거를 위한 것이 아니라 — 그것은 pgbuf_get_victim 내부에서 이미 완료되었다 — 해시 체인과 latch 정리를 위한 것이다.

두 소스 모두 실패하면, 빌드 모드와 데몬 가용성에 따라 동작이 분기된다. flush 데몬이 동작 중인 SERVER_MODE에서 스레드는 direct-victim 대기자 큐에 진입하고 타임아웃과 함께 중단된다:

// pgbuf_allocate_bcb -- src/storage/page_buffer.c (SERVER_MODE, flush daemon up)
retry:
high_priority = high_priority || VACUUM_IS_THREAD_VACUUM (thread_p)
|| pgbuf_is_thread_high_priority (thread_p);
thread_lock_entry (thread_p);
if (high_priority) waiter_threads_high_priority->produce (thread_p);
else if (!waiter_threads_low_priority->produce (thread_p)) /* low queue jammed */
{ if (!waiter_threads_high_priority->produce (thread_p)) { assert(false); goto end; } }
pgbuf_wakeup_page_flush_daemon (thread_p); /* ensure SOMEONE will feed us */
r = thread_suspend_timeout_wakeup_and_unlock_entry (..., THREAD_ALLOC_BCB_SUSPENDED);

high_priority는 vacuum 스레드, 핫 페이지 latch를 이미 보유한 스레드, 또는 재시도 시에 true가 된다. low-priority produce 실패 서브 분기는 선점된 소비자 교착을 방지한다: low 큐가 받지 못하면 스레드를 high 큐에 밀어 넣는다. 진입 후 flush 데몬을 깨우고 중단된다. 깨어날 때의 네 가지 서브 분기:

  • 정상 핸드오프 (THREAD_ALLOC_BCB_RESUMED): 생산자가 이 스레드의 슬롯에 BCB를 넣었다; pgbuf_get_direct_victim이 읽어 goto end.
  • 탈취됨(Stolen-back): pgbuf_get_direct_victimNULL 반환(BCB가 할당과 get 사이에 재고정됨, 동반 문서 §“Direct victim hand-off”); high_priority를 설정하고 goto retry.
  • 인터럽트/종료 (다른 resume_status): 절반 할당된 victim이 있으면 되돌리고, ER_INTERRUPTED를 발생시켜 클레임 경로가 분기 C를 취하게 한다.
  • r != NO_ERROR (타임아웃 else): 타임아웃 없음을 단언하고 resume_status를 재설정한다; assert 하에서는 발생 불가능한 경로다.

no-daemon else (SA 모드 또는 크래시 복구)는 생산자를 위해 수면할 수 없으므로 pgbuf_wakeup_page_flush_daemon으로 플러시하고, pgbuf_get_victim으로 재스캔하며, 이제 victim이 있다고 단언한다. 공유 꼬리 부분이 획득한 victim을 victimize한다:

// pgbuf_allocate_bcb -- src/storage/page_buffer.c
end:
if (bufptr != NULL)
{ if (pgbuf_victimize_bcb (thread_p, bufptr) != NO_ERROR) { assert (false); bufptr = NULL; } }
else if (er_errid () == NO_ERROR)
er_set (..., ER_PB_ALL_BUFFERS_DIRTY, ...);
return bufptr;

pgbuf_victimize_bcb는 BCB mutex 하에서 victimize 가능성을 재확인하고, BCB를 해시 체인에서 제거(pgbuf_delete_from_hash_chain)하며, atomic latch에 PGBUF_LATCH_INVALID를 설정한다. 존은 변경하지 않는다 — victim은 pgbuf_get_victimpgbuf_remove_from_lru_list에 의해 이미 VOID로 이동했다(§4.6). Invalid 리스트 BCB는 이 단계를 건너뛴다(체인에 있은 적이 없음). bufptr가 여전히 NULL이면 분기 C가 전파할 오류를 갖도록 ER_PB_ALL_BUFFERS_DIRTY를 설정한다.

불변 조건 — pgbuf_allocate_bcb에서 비-NULL로 나오는 모든 BCB는 해시 체인과 LRU 리스트에서 분리되어 bufptr->mutex를 보유한 채 반환된다. Invalid 리스트 BCB는 체인에 연결된 적 없고, victim은 pgbuf_remove_from_lru_list + pgbuf_victimize_bcb로 분리된다. 아직 연결된 BCB가 유출되면 획득자가 두 번째 VPID를 이전 VPID로도 도달 가능한 슬롯에 바인딩하게 된다.

4.5 페이지 바이트 로드 — NEW_PAGE vs. 읽기 vs. DWB

섹션 제목: “4.5 페이지 바이트 로드 — NEW_PAGE vs. 읽기 vs. DWB”

pgbuf_claim_bcb_for_fix의 분기 D로 돌아와서, 초기화된 BCB에 페이지 바이트가 필요하다. 분기는 fetch_mode에 따른다.

// pgbuf_claim_bcb_for_fix -- src/storage/page_buffer.c (read branch)
if (fetch_mode != NEW_PAGE)
{
/* DWB first: a torn-write copy may be fresher than the volume. */
if (dwb_read_page (thread_p, vpid, &...iopage, &success) != NO_ERROR)
{ assert (false); return NULL; } /* (1) DWB error: can't-happen */
else if (success == true) { /* copied from DWB, no disk read */ }
else if (fileio_read (...) == NULL) /* (2) volume read failed */
{ ASSERT_ERROR ();
pgbuf_put_bcb_into_invalid_list (thread_p, bufptr); /* releases bufptr->mutex */
(void) pgbuf_unlock_page (..., true); return NULL; }
/* (3) decrypt if TDE-protected; on failure roll back like (2) */
if (tde_algo != TDE_ALGORITHM_NONE && tde_decrypt_data_page (...) != NO_ERROR)
{ ASSERT_ERROR (); pgbuf_put_bcb_into_invalid_list (...); pgbuf_unlock_page (..., true); return NULL; }
if (pgbuf_is_temporary_volume (vpid->volid) && !pgbuf_is_temp_lsa (...)) /* temp first-touch */
{ pgbuf_init_temp_page_lsa (...); pgbuf_set_dirty_buffer_ptr (thread_p, bufptr); }
}

읽기 분기DWB 우선 순서를 따른다(동반 문서 §“Double Write Buffer”): dwb_read_pagesuccess를 설정하면 이 VPID의 복사본이 double-write 버퍼에 있는 것이므로 디스크 읽기를 건너뛴다. DWB 미스 시에만 fileio_read가 볼륨에 접근한다. 세 가지 오류 서브 분기 — (1) DWB 오류 가드, (2) 볼륨 읽기 실패, (3) TDE 복호화 실패 — 는 각각 pgbuf_put_bcb_into_invalid_list(VPID를 null로, PGBUF_LATCH_INVALID 설정, 존을 INVALID로 전환, bufptr->mutex 해제)와 pgbuf_unlock_page(..., true)로 BCB를 되돌린다. 이로써 반쯤 읽힌 바이트가 풀에 잔류하지 않는다. Temp 최초 접촉 서브 분기는 sentinel temp LSA를 설정하고 페이지를 dirty로 표시한다.

// pgbuf_claim_bcb_for_fix -- src/storage/page_buffer.c (NEW_PAGE branch)
else
{
if (pgbuf_is_temporary_volume (vpid->volid))
pgbuf_init_temp_page_lsa (&...iopage, IO_PAGESIZE);
else fileio_init_lsa_of_page (&...iopage, IO_PAGESIZE);
if (bufptr->vpid.volid > NULL_VOLID) /* perm: mark page immature */
{ ...iopage.prv.pageid = -1; ...iopage.prv.volid = -1; }
}
return bufptr;

NEW_PAGE 분기는 디스크에서 읽을 내용이 없으므로 페이지 내 LSA를 초기화하고, 영구 볼륨의 경우 prv.pageid/volid = -1을 설정하여 페이지를 미성숙(immature) 으로 표시한다 — 실제 identity는 나중에 pgbuf_set_bcb_page_vpid(§4.6)가 기록한다. 실패할 수 없으므로 롤백 서브 분기가 없다. 두 분기 모두 bufptr->mutex를 보유한 채 로드된 BCB를 반환한다.

4.6 pgbuf_set_bcb_page_vpid와 해시 체인 삽입

섹션 제목: “4.6 pgbuf_set_bcb_page_vpid와 해시 체인 삽입”

Fix 경로(5장 영역, 바인드 단계를 위해 여기서 소개)는 페이지 identity를 설정하고 BCB를 삽입한다. pgbuf_set_bcb_page_vpid는 세 가지 분기를 갖는다:

// pgbuf_set_bcb_page_vpid -- src/storage/page_buffer.c
if (bufptr == NULL || VPID_ISNULL (&bufptr->vpid)) /* (A) guard: nothing to stamp */
{ assert (bufptr != NULL); assert (!VPID_ISNULL (&bufptr->vpid)); return; }
if (bufptr->vpid.volid > NULL_VOLID) /* permanent volume only */
{
if (prv.pageid == NULL_PAGEID && prv.volid == NULL_VOLID) /* (B) first time */
{ prv.pageid = bufptr->vpid.pageid; /* write identity into header */
prv.volid = bufptr->vpid.volid;
prv.ptype = PAGE_UNKNOWN; /* + p_reserve_1/2, tde_nonce zeroed */ }
else /* (C) already stamped */
{ assert (prv.volid == bufptr->vpid.volid); /* values not reset on dealloc */
assert (prv.pageid == bufptr->vpid.pageid); } /* identity must match -- no rewrite */
}

(A) 상단 가드: NULL BCB 또는 null VPID는 호출자 버그 — 단언하고 바이트를 건드리지 않은 채 반환. (B) 최초 경로(§4.5의 미성숙 NEW_PAGE sentinel): VPID를 페이지 내 헤더에 기록하여 바이트가 디스크에서 자기 식별 가능하게 한다. (C) else — 재할당/이미 설정된 영구 페이지(페이지 내 identity는 deallocation 후에도 유지): 함수가 바이트를 그대로 두고 단언만 수행한다. 저장된 prv.volid/pageid가 BCB의 VPID와 여전히 일치함을 확인. Temp 페이지(volid <= NULL_VOLID)는 세 분기 모두를 통과하며 아무 동작도 하지 않는다.

// pgbuf_insert_into_hash_chain -- src/storage/page_buffer.c
pthread_mutex_lock (&hash_anchor->hash_mutex);
bufptr->hash_next = hash_anchor->hash_next; hash_anchor->hash_next = bufptr; /* link at head */
/* hash_mutex stays held; released by the following pgbuf_unlock_page (need_hash_mutex=false) */

pgbuf_insert_into_hash_chain은 BCB를 버킷 BCB 체인의 head에 연결하고 의도적으로 hash_mutex를 계속 보유한다 — 바로 이어지는 pgbuf_unlock_page(..., false)가 VPID 락 레코드를 제거하고 동일한 mutex를 해제한다. 두 연산에 걸쳐 보유함으로써, 경쟁 패자가 BCB가 체인에 있지만 VPID 락은 이미 사라진 창을 볼 수 없다(그렇지 않으면 잘못된 승자가 될 수 있다).

BCB는 LRU의 어디에 배치되는가? 이 시점에서 BCB는 PGBUF_VOID_ZONE에 있다. Invalid 리스트 BCB는 pgbuf_get_bcb_from_invalid_list에 의해 그곳에 놓였고; victim은 pgbuf_remove_from_lru_list에 의해 그곳으로 이동했다 (호출 체인: pgbuf_get_victim -> pgbuf_get_victim_from_lru_list -> pgbuf_remove_from_lru_list, 그 꼬리에서 pgbuf_bcb_change_zone (..., PGBUF_VOID_ZONE) 수행). pgbuf_victimize_bcb는 이후 해시 체인에서 분리하고 latch를 무효화할 뿐이다. VOID는 “아직 어떤 리스트에도 없음”을 의미한다. BCB는 클레임/삽입 중에 LRU 리스트에 진입하지 않는다; 대응하는 unfix 시점에 존에 배치된다 — pgbuf_unfix가 일반적으로 LRU 2로, vacuum 워커이거나 VPID가 Aout-history 히트인 경우(부스트) LRU 1로 라우팅한다 — 7장의 주제다.

stateDiagram-v2
  [*] --> INVALID: 서버 시작
  INVALID --> VOID: invalid 리스트에서 팝
  Victim --> VOID: get_victim 중 pgbuf_remove_from_lru_list
  VOID --> LRU2: 일반 unfix -> 7장
  VOID --> LRU1: vacuum 또는 Aout 히트 unfix -> 7장
  VOID --> INVALID: 읽기 오류 롤백

그림 4-3 — 클레임된 BCB의 존 궤적. 클레임 경로는 VOID에 도달시키며, LRU 배치는 unfix(7장)에서 이루어진다. 읽기 오류 발생 시 INVALID로 직접 반환된다.

  1. pgbuf_claim_bcb_for_fix는 네 가지 출구를 갖는다: trylock 실패 탈출(분기 A), *try_again=true를 설정한 패자 재시도(B), 할당 실패 해제(C), 그리고 bufptr->mutex를 보유한 로드된 BCB 반환 성공(D).

  2. PGBUF 락은 동일 VPID 할당을 단일 승자로 만든다. pgbuf_lock_page는 한 스레드에 HOLDER를 반환하고 나머지는 레코드별 FIFO에 WAITER로 파킹한다; 승자가 BCB를 삽입하면 pgbuf_unlock_page가 모두를 깨우고 이들은 fix를 재구동하여 히트한다.

  3. pgbuf_allocate_bcb는 invalid 리스트에서 단락 반환한다 (victimize 없이 반환); victim은 pgbuf_victimize_bcb를 위해 end:로 폴스루한다. 둘 다 실패하면 서버 모드는 네 가지 깨어남 서브 분기(핸드오프, 탈취됨, 인터럽트, r != NO_ERROR 타임아웃 단언)와 함께 direct-victim 큐에서 수면하고, SA/복구 모드는 인라인으로 플러시한다.

  4. pgbuf_get_bcb_from_invalid_list는 이중 검사 잠금을 사용한다: 락 없이 빈 리스트 빠른 반환, mutex 후 재확인 반환, 그리고 invalid_top을 전진시키고 invalid_cnt를 감소시키며 BCB를 잠그고 VOID로 전환하는 팝 분기. 모든 읽기 오류 경로는 pgbuf_put_bcb_into_invalid_list + pgbuf_unlock_page(...,true)로 빠져나가 반쯤 바인딩된 슬롯이 유출되지 않는다.

  5. DWB는 모든 읽기 미스에서 디스크보다 먼저 참조된다 (dwb_read_pagesuccess를 설정하면 fileio_read 제거); NEW_PAGE는 읽기도 실패도 하지 않으며, 영구 페이지를 미성숙(prv.pageid/volid = -1)으로 표시한다. pgbuf_set_bcb_page_vpid는 최초 분기에서만 identity를 설정하고; 재할당된 영구 페이지에서는 바이트를 그대로 두고 저장된 identity가 VPID와 여전히 일치함을 단언한다.

  6. 클레임 경로는 BCB를 VOID에 남긴다. Victim은 pgbuf_get_victimpgbuf_remove_from_lru_list를 거쳐 VOID에 도달하며 — pgbuf_victimize_bcb가 아니다, 그것은 해시 체인만 분리하고 latch만 무효화한다. 해시 체인 삽입은 승자/패자 레이스 창을 닫기 위해 VPID 락 해제에 걸쳐 hash_mutex를 보유하며; LRU 존 배치는 7장의 unfix로 미뤄진다.

Chapter 5: BCB 원자적 래치 획득 블록과 깨우기

섹션 제목: “Chapter 5: BCB 원자적 래치 획득 블록과 깨우기”

3장에서는 BCB를 손에 넣고 mutex를 보유한 상태까지 다뤘다. 이제 핵심 질문은 다음과 같다: BCB mutex를 소유한 fixer 입장에서, 페이지 단위 읽기/쓰기 래치가 어떻게 호환성을 판단하고, 호환되지 않는 요청을 페이지별 대기자 목록에 블록시키며, 타임아웃 처리와 해제 시 순서 보장 깨우기를 수행하는가? 설계 수준의 래치 의미론은 상위 수준 동반 문서의 “래치 모드와 fix 프로토콜” 절을 참고하고, 여기서는 모든 분기를 직접 추적한다.

이 서브시스템은 64비트 워드 하나인 atomic_latch와 BCB 위의 단방향 연결 next_wait_thrd 큐를 기반으로 동작한다. BCB mutex는 를 직렬화하고, 래치 워드는 lock-free CAS로 변경되므로 깨어난 대기자가 mutex를 다시 잡지 않고도 승인을 게시할 수 있다.

5.1 압축 래치 워드: pgbuf_atomic_latch_impl

섹션 제목: “5.1 압축 래치 워드: pgbuf_atomic_latch_impl”

래치는 std::atomic<uint64_t> atomic_latch이며, union으로 접근한다:

// union pgbuf_atomic_latch_impl -- src/storage/page_buffer.c
union pgbuf_atomic_latch_impl {
uint64_t raw; /* the word actually CAS'd */
struct {
PGBUF_LATCH_MODE latch_mode; /* enum:uint16_t NO_LATCH=0, READ=1, WRITE=2, FLUSH=3 */
uint16_t waiter_exists; /* 1 if next_wait_thrd has a R/W waiter */
int32_t fcnt; /* number of granted fixes */
} impl;
};
필드역할존재 이유
rawcompare_exchange_*가 조작하는 uint64_t 페이로드세 필드를 CAS 한 번으로 원자적으로 이동시킨다. 이 워드를 공유하기 때문에 찢긴 읽기(torn read)가 원천 차단된다. 레이아웃(16 + 16 + 32비트)이 64비트를 정확히 채운다.
impl.latch_mode현재 승인된 모드. PGBUF_LATCH_FLUSH블록 모드일 뿐, 승인 모드로 사용되지 않는다 — PGBUF_LATCH_MODE 헤더에 명시됨.호환성 결정(5.3)에서 가장 먼저 읽힌다. PGBUF_NO_LATCH는 “유휴” 센티널로도 사용된다.
impl.waiter_exists카운트가 아닌 힌트: R/W 요청이 하나 이상 대기열에 들어오면 true.쓰기 기아 방지 가드(5.3, Case 1).
impl.fcnt현재 모드를 공유하는 모든 holder의 총 fix 횟수0이면 페이지 모드 전환 또는 희생(victim) 처리가 가능하다. 단독 독자 판별을 위해 특정 holder의 fix_count와 비교된다.

get_impl은 acquire load로 스냅샷을 찍는다. 단일 필드 변경자들 (set_latch, add_fcnt, set_waiter_exists, set_latch_and_fcnt, set_latch_and_add_fcnt)은 각각 load/compare_exchange_weak 루프로 동작한다. pgbuf_latch_bcb_upon_fix는 이 헬퍼들을 직접 호출하지 않는다. 새로 찍은 old_impl로부터 new_impl 전체를 계산하고, bufptr->atomic_latchcompare_exchange_strong 하나로 재시도한다. 따라서 결정 트리 전체가 단 하나의 원자적 전이로 처리되며, 충돌 시 전체를 재계산한다.

불변 조건 — 래치 워드는 원자적으로 전이한다; 부분 게시는 불가능하다. 모든 변경은 do { old = get_impl(); ...build new... } while (!CAS(old, new)) 패턴을 따른다. 두 번의 개별 store가 있다면, 동시 fixer가 latch_mode == READ는 보면서 stale fcnt를 읽어 호환 읽기를 잘못 승인해 holder 회계를 깨뜨릴 수 있다. 단일 워드 CAS가 이 찢긴 중간 상태를 원천 차단한다. 결정 트리 CAS는 strong 형식(허위 실패 없음)을 사용하고, 단일 필드 헬퍼와 오류/깨우기 복구 루프는 weak 형식을 자체 재시도 루프 안에서 사용한다.

flowchart LR
  BCB["pgbuf_bcb"] --> AL["atomic_latch\n(uint64_t word)"]
  BCB --> NWT["next_wait_thrd\n(THREAD_ENTRY*)"]
  AL --> IMPL["impl: latch_mode | waiter_exists | fcnt"]
  NWT --> T1["THREAD_ENTRY\nrequest_latch_mode\nrequest_fix_count\nwait_for_latch_promote"]
  T1 --> T2["THREAD_ENTRY ..."]
  TH["THREAD_ENTRY\nm_holder_anchor"] --> HL["thrd_hold_list"]
  HL --> H1["pgbuf_holder\nfix_count, bufptr"]
  H1 --> H2["pgbuf_holder (thrd_link)"]

Figure 5-1 — 래치 워드와 그 위의 두 목록: BCB별 대기자 큐(next_wait_thrd)와 스레드별 holder 목록(thrd_hold_list).

pgbuf_holder이 스레드하나의 BCB에 수행한 fix를 기록한다. 래치 워드는 전역 fix를 세고, holder는 내 몫을 세어 승격(promotion)과 unfix를 로컬에서 추론할 수 있게 한다.

// struct pgbuf_holder -- src/storage/page_buffer.c
struct pgbuf_holder {
int fix_count; /* the count of fix by the holder */
PGBUF_BCB *bufptr; /* pointer to BCB */
PGBUF_HOLDER *thrd_link; /* next holder in this thread's hold list */
PGBUF_HOLDER *next_holder;/* next in this thread's *free* list */
PGBUF_HOLDER_STAT perf_stat;
#if !defined(NDEBUG)
char fixed_at[64 * 1024]; /* call-site trail for leak debugging */
int fixed_at_size;
#endif
int watch_count; /* number of PGBUF_WATCHERs on this holder */
PGBUF_WATCHER *first_watcher;
PGBUF_WATCHER *last_watcher;
};
필드역할존재 이유
fix_count이 스레드가 이 BCB를 fix한 횟수old_impl.impl.fcnt == holder->fix_count(5.3)이면 “전역 == 내 몫”, 즉 유일한 fixer. 0이면 holder가 재활용된다.
bufptr기록된 BCB로의 역방향 포인터pgbuf_find_thrd_holder가 이 값으로 매칭한다.
thrd_link이 스레드의 사용 중 목록(thrd_hold_list)에서 다음 holder스레드가 보유한 여러 페이지를 연결한다. 이 목록에 있는 동안 next_holder는 NULL이어야 한다(pgbuf_find_thrd_holder에서 단언).
next_holder이 스레드의 자유 목록(thrd_free_list)에서 다음 holder자유 상태일 때만 유효하고, thrd_link는 사용 중일 때만 유효하다. 동시에 두 목록에 속하지 않는다.
perf_statPGBUF_HOLDER_STAT 비트 필드(dirty_before_hold, dirtied_by_holder, hold_has_write_latch, hold_has_read_latch)perfmon에 데이터 제공. hold_has_* 비트는 pgbuf_latch_bcb_upon_fix의 어느 분기에서든 래치가 승인될 때 설정된다.
fixed_at / fixed_at_sizefix 호출 지점(file:line) 연결 문자열을 담는 고정 크기 버퍼와 그 길이NDEBUG 빌드에서만 존재하는 누수/이중 fix 디버깅 용도. 릴리스 빌드에는 없다.
watch_count / first_watcher / last_watcher순서 있는 fix watcher 체인(10장)재활용 전에 반드시 0 / NULL이어야 한다 — pgbuf_remove_thrd_holder에서 watch_count == 0을 단언한다.

불변 조건 — holder는 두 스레드별 목록 중 정확히 하나에만 속한다. 사용 중일 때는 next_holder == NULL 상태로 thrd_hold_list에 있고 (pgbuf_find_thrd_holder가 순회 중 모든 노드에 단언), 자유 상태일 때는 next_holder로 이어진 thrd_free_list에 있으며 thrd_link는 무효다. 자유 목록 링크와 보유 목록 링크가 동시에 유효한 곳을 가리키는 경우는 없다.

세 가지 헬퍼가 목록을 관리한다. **pgbuf_allocate_thrd_holder_entry**는 thrd_free_list가 비어있지 않으면 전역 mutex 없이 팝하고, 그렇지 않으면 free_holder_set_mutex를 잡아 공유 free_holder_set에서 다음 요소를 꺼내며 free_index == -1이면 새 PGBUF_HOLDER_SETmalloc해 확장한다. 어느 경로든 holder는 thrd_hold_list에 푸시된다:

// pgbuf_allocate_thrd_holder_entry -- src/storage/page_buffer.c
holder->next_holder = NULL; /* disconnect from free list */
holder->thrd_link = thrd_holder_info->thrd_hold_list; /* push onto hold list */
thrd_holder_info->thrd_hold_list = holder;
thrd_holder_info->num_hold_cnt += 1;
holder->first_watcher = NULL; holder->last_watcher = NULL; holder->watch_count = 0;

**pgbuf_find_thrd_holder**는 bufptr이 일치하는 holder를 찾을 때까지 thrd_hold_list를 순회하고 없으면 NULL을 반환한다. assert (holder->next_holder == NULL)은 “두 목록 동시 소속 불가” 불변 조건을 강제한다. **pgbuf_remove_thrd_holder**는 fix_count == 0watch_count == 0을 단언한 뒤, holder를 thrd_free_list에 먼저 prepend하고, 그 다음 thrd_hold_list에서 unlink한다 (헤드 특수 처리, 아니면 predecessor까지 순회). 항목을 찾지 못하면 assert (false)로 떨어지고 ER_FAILED를 반환한다.

5.3 pgbuf_latch_bcb_upon_fix — 호환성 결정 트리

섹션 제목: “5.3 pgbuf_latch_bcb_upon_fix — 호환성 결정 트리”

호출자는 BCB mutex를 보유하고 있으며, scope_exit unlock_BCB 가드가 어느 출구에서든 이를 해제한다. 단, 분기에서 .release()를 호출하면 해제하지 않는다. 호출자의 holder를 한 번 조회한 뒤, do { ...new_impl 재계산... } while (!compare_exchange_strong) 루프를 실행한다. request_fcnt는 1로 시작하며 매 재시도 상단에서 리셋된다.

flowchart TB
  S["snapshot old_impl; new_impl = old_impl\nrequest_fcnt = 1"] --> IDLE{"buf_lock_acquired\nor latch_mode == NO_LATCH ?"}
  IDLE -- yes --> SETIDLE["is_page_idle=true\nnormalize old to clean idle\nnew: mode=request, fcnt=1"]
  IDLE -- no --> C1{"READ req on\nREAD-latched page ?"}
  C1 -- yes --> W{"waiter_exists ?"}
  W -- no --> GR1["can_latch=true; fcnt++"]
  W -- yes --> OWN{"holder != NULL ?"}
  OWN -- yes --> GR2["can_latch=true; fcnt++"]
  OWN -- no --> BLK1["can_latch=false\nwriter-starvation guard"]
  C1 -- no --> H{"holder != NULL ?"}
  H -- no --> BLK2["Case 3: can_latch=false\nwaiter_exists=true"]
  H -- yes --> WR{"latch_mode == WRITE ?"}
  WR -- yes --> GR3["Sub 2-1: can_latch=true; fcnt++"]
  WR -- no --> SOLE{"old fcnt == holder fix_count ?"}
  SOLE -- yes --> GR4["Sub 2-2: in-place promote\nmode=WRITE, fcnt=1"]
  SOLE -- no --> COND{"CONDITIONAL ?"}
  COND -- yes --> CFAIL["can_latch=false\nwaiter_exists=true, then reject"]
  COND -- no --> PROM["promote_needed=true\nfcnt -= holder fix_count\nwaiter_exists=true"]

Figure 5-2 — new_impl 계산의 모든 분기. 루프는 old_impl.rawnew_impl.rawcompare_exchange_strong으로 CAS한다. 실패하면 스냅샷을 새로 찍고 트리 전체를 다시 순회한다.

유휴 단락(Idle short-circuit). buf_lock_acquired(새 BCB, 4장)이거나 페이지가 PGBUF_NO_LATCH 상태이면, 코드는 new_impl을 만들기 전에 old_impl을 깨끗한 유휴 상태로 정규화한다. 이는 CAS 기댓값에 영향을 미친다:

// pgbuf_latch_bcb_upon_fix -- src/storage/page_buffer.c
if (is_page_idle == true) {
old_impl.impl.waiter_exists = false; /* <- expect a clean word */
old_impl.impl.latch_mode = PGBUF_NO_LATCH; old_impl.impl.fcnt = 0;
new_impl = old_impl;
new_impl.impl.latch_mode = request_mode; new_impl.impl.fcnt = 1; /* grant */
}

(SA_MODE에서만, holder가 없는 비유휴 페이지는 래치 누수다 — 코드는 assert (0)을 내리고 유휴로 취급한다.)

Case 1 — R on R. 대기자 없음: 승인(can_latch = true; fcnt++). 대기자 있음: 이미 holder인 경우(재진입)에만 참여 허용. 새 독자(holder == NULL)는 블록된다.

불변 조건 — 독자는 대기 중인 쓰기자에게 양보한다. waiter_exists가 설정되면, 재진입 독자만 R-latch에 합류할 수 있다(holder == NULL 검사). 이를 위반하면 새 독자의 연속 진입이 대기 중인 쓰기자를 무기한 지연시킨다.

Case 2 — 호출자가 이미 holder인 경우 (R-on-R이 아니므로, 페이지가 WRITE-latched이거나 R-holder가 WRITE를 요청하는 상황):

  • Sub-2-1, 페이지 WRITE-latched: re-fix(R 또는 W)는 순수 통과 처리, can_latch = true; fcnt++W-holder 단축 경로.
  • Sub-2-2, 페이지 READ-latched에서 WRITE 요청(제자리 승격): old_impl.impl.fcnt == holder->fix_count이면 호출자가 유일한 독자이므로 래치를 제자리에서 WRITE로 전환 (mode = WRITE; fcnt = 1). 다른 독자가 있다면, PGBUF_CONDITIONAL_LATCHwaiter_exists를 설정하고 거부로 떨어지며, 무조건적 요청은 promote_needed를 설정하고 자신의 fix를 차감(new_impl.impl.fcnt -= holder->fix_count)하고 waiter_exists를 설정한 뒤 꼬리 대기자로 블록된다(5.4 참고).

상위 수준 동반 문서에서 언급하는 “도달 불가 제자리 업그레이드”는 역사적으로 제거된 경합 상태의 업그레이드를 가리킨다. 단독 독자 제자리 전환(sub-2-2)은 현재 소스에서 도달 가능하며, 전용 승격 진입점 pgbuf_promote_read_latch_debugpgbuf_block_bcb의 단일 승격자 단언(아래 요약 항목 3 참고)과 함께 공존한다.

Case 3 — 호출자가 holder가 아니고 요청이 호환되지 않는 경우 (R-latched 페이지에 W, W-latched 페이지에 외부자의 R/W): can_latch = false; waiter_exists = true. 스레드는 블록된다.

CAS 성공 후 함수는 결과 플래그에 따라 분기한다:

  • is_page_idle 또는 can_latch — 승인됨: holder를 할당(유휴/외부자 경로)하거나 기존 holder의 fix_count를 증가시키고, 성능 비트를 설정하고, latch_last_thread를 갱신하고, NO_ERROR를 반환한다.
  • promote_needed — holder의 read fix를 request_fcnt에 합산 (request_fcnt += holder->fix_count), holder를 초기화, pgbuf_remove_thrd_holder, 블록 경로로 진입.
  • block/promote + PGBUF_CONDITIONAL_LATCHER_FAILED 거부(트랜잭션의 wait_msec == LK_ZERO_WAIT이면 먼저 ER_LK_PAGE_TIMEOUT 발생).
  • block/promote, 무조건적unlock_BCB.release(), pgbuf_block_bcb(..., as_promote = false) 호출. 반환 시 래치가 보유됨. holder를 fix_count = request_fcnt로 할당하고, *is_latch_wait = true를 설정하고, NO_ERROR를 반환한다.

5.4 pgbuf_block_bcb — 대기열 삽입 및 슬립

섹션 제목: “5.4 pgbuf_block_bcb — 대기열 삽입 및 슬립”

호출자는 waiter_exists가 true인 상태로 BCB mutex를 보유한다(단언). request_latch_moderequest_fix_count(깨어날 때 fcnt에 크레딧할 수치)를 기록한 뒤, as_promote 플래그에 따라 삽입 위치를 결정한다:

// pgbuf_block_bcb -- src/storage/page_buffer.c
cur_thrd_entry->request_latch_mode = request_mode;
cur_thrd_entry->request_fix_count = request_fcnt; /* SPECIAL_NOTE */
if (as_promote) {
/* Safe guard: there can be only one promoter. */
assert (bufptr->next_wait_thrd == NULL
|| !bufptr->next_wait_thrd->wait_for_latch_promote);
cur_thrd_entry->next_wait_thrd = bufptr->next_wait_thrd; /* head insert */
bufptr->next_wait_thrd = cur_thrd_entry;
} else { cur_thrd_entry->next_wait_thrd = NULL; /* ... walk to tail, link ... */ }

as_promote 플래그는 호출자의 선택이며, 두 가지 서로 다른 호출 경로를 구분한다:

  • 헤드 삽입(as_promote = true) 은 오직 pgbuf_promote_read_latch_debug (명시적 pgbuf_promote_read_latch 경로)에서만 사용한다. 이미 read latch를 해제한 승격자는 새 대기자와의 경쟁에서 반드시 이겨야 하므로 큐 앞으로 들어간다. 단언은 큐 안에 승격자가 최대 하나임을 강제한다.
  • 꼬리 삽입(as_promote = false, FIFO) 은 다른 모든 호출자가 사용한다 — pgbuf_latch_bcb_upon_fix(5.3)의 promote_needed 분기도 포함된다. 따라서 fix 도중 발견된 승격은 헤드가 아닌 꼬리에 삽입된다. 전용 promote API만 헤드 삽입을 한다. 비동기 플러시 경로(8장)도 PGBUF_LATCH_FLUSH로 꼬리 삽입한다.

이후 모드에 따라 슬립한다:

  • PGBUF_LATCH_FLUSH(플러시 대기자, 8장)는 thread_suspend_wakeup_and_unlock_entry무기한 슬립한다. RESUMED가 아닌 깨우기(인터럽트)에서는 BCB를 재잠금하고, next_wait_thrd에서 자신을 unlink하고, ER_FAILED를 반환한다.
  • R/W 대기자pgbuf_timed_sleep을 통과한다. CUBRID는 페이지 래치 사이에 대기 그래프를 구축하지 않으며, 타임아웃에 의존한다 — “타임아웃으로 깨어난 요청은 희생자(victim)로 처리된다.” 정상 반환 시 bufptr->latch_last_thread = thread_p를 설정한다.

5.5 pgbuf_timed_sleeppgbuf_timed_sleep_error_handling

섹션 제목: “5.5 pgbuf_timed_sleep과 pgbuf_timed_sleep_error_handling”

pgbuf_timed_sleep은 스레드 항목을 잠근 뒤 BCB mutex를 해제(순서: 스레드 항목 내부 BCB)하고, 타임아웃을 계산한 뒤 슬립한다:

// pgbuf_timed_sleep -- src/storage/page_buffer.c
thread_lock_entry (thread_p); PGBUF_BCB_UNLOCK (bufptr);
old_wait_msecs = wait_secs = pgbuf_find_current_wait_msecs (thread_p);
/* LK_ZERO_WAIT/LK_FORCE_ZERO_WAIT -> 0, else wait_secs = pgbuf_latch_timeout */
try_again:
to.tv_sec = (int) time (NULL) + wait_secs;
thread_p->resume_status = THREAD_PGBUF_SUSPENDED;
r = thread_suspend_timeout_wakeup_and_unlock_entry (thread_p, &to, THREAD_PGBUF_SUSPENDED);

pgbuf_latch_timeout은 기본값이 300 * 1000이며, 부트 시 PRM_ID_PAGE_LATCH_TIMEOUT으로 재설정된다. 반환 분기는 세 가지다:

  1. NO_ERROR — 신호 수신. 항목을 재잠금한다. resume_status == THREAD_PGBUF_RESUMED이면 깨우기 측이 래치를 승인한 것(5.6) — NO_ERROR 반환(래치는 이미 우리 것이며 fcnt는 깨우기 측이 증가시켰다). 그렇지 않으면 인터럽트: request_latch_mode = PGBUF_NO_LATCH 설정, 오류 핸들러 호출, ER_INTERRUPTED 발생, ER_FAILED 반환.
  2. ER_CSS_PTHREAD_COND_TIMEDOUT — 타임아웃. 경쟁 상태에서 RESUMED가 됐다면 NO_ERROR 반환. 트랜잭션이 더 이상 활성 상태가 아니면(logtb_is_current_active false) try_again으로 루프(커밋/롤백 중인 트랜잭션은 타임아웃시키지 않음). 그 외에는 페이지 래치 교착 희생자: 모드를 저장하고, request_latch_mode = PGBUF_NO_LATCH (깨우기 측이 우리를 건너뛰는 마커)를 설정하고, 오류 핸들러를 실행하고, goto er_set_return.
  3. elsepthread_cond 실패: er_set_with_oserror(ER_CSS_PTHREAD_COND_TIMEDWAIT), ER_FAILED 반환.

er_set_return원래 대기 사양에 따라 포맷하고 BCB mutex를 해제한 뒤 ER_FAILED를 반환한다:

  • LK_INFINITE_WAITER_PAGE_LATCH_TIMEDOUTER_LK_UNILATERALLY_ABORTED (FIXME 표시 assert (0)으로 보호).
  • 양수 old_wait_msecsER_PAGE_LATCH_TIMEDOUT + ER_LK_PAGE_TIMEOUT (save_request_latch_mode 보고).
  • 그 외 — 그냥 잠금 해제.

pgbuf_timed_sleep_error_handling 은 대기자가 큐를 포기할 때 실행되며, BCB를 재잠금하고 세 가지 경우로 스레드를 unlink한다:

flowchart TB
  L["PGBUF_BCB_LOCK"] --> E{"next_wait_thrd == NULL ?"}
  E -- yes --> R0["경우 1: 깨우기 측이 이미 제거, 반환"]
  E -- no --> F{"head == thrd_entry ?"}
  F -- no --> M["경우 2: 목록 순회, thrd_entry unlink, 반환"]
  F -- yes --> H["경우 3: 헤드 팝\n연속된 READ 대기자 깨우기\n승인 불가 또는 WRITE까지"]

Figure 5-3 — 세 가지 제거 경우. 경우 3(헤드에서 포기하는 스레드)만 그 뒤에 숨어있던 독자들을 깨워 큐를 복구해야 한다. 경우 3에서는 헤드를 팝한 뒤 루프를 돈다. 각 후속 스레드 중 페이지 latch_mode == READ이고 대기자가 READ를 원하는 경우 fcnt += request_fix_count를 CAS(compare_exchange_weak)하고, 항목을 잠그고, unlink하고, 깨운다(pgbuf_wakeup). WRITE 대기자이거나 승인 불가 상태면 루프를 종료한다.

5.6 pgbuf_wakeup_reader_writer — 래치 해제 시 순서 보장 깨우기

섹션 제목: “5.6 pgbuf_wakeup_reader_writer — 래치 해제 시 순서 보장 깨우기”

unfix가 fcnt를 0으로 낮추고 모드를 PGBUF_NO_LATCH로 리셋할 때(진입 시 둘 다 단언), 이 함수는 next_wait_thrd를 한 번 순회하며 승인 가능한 것을 승인한다. 호출자는 BCB mutex를 보유한다.

// pgbuf_wakeup_reader_writer -- src/storage/page_buffer.c
for (thrd_entry = bufptr->next_wait_thrd; thrd_entry != NULL; thrd_entry = next_thrd_entry) {
next_thrd_entry = thrd_entry->next_wait_thrd;
if (thrd_entry->request_latch_mode == PGBUF_NO_LATCH) { /* unlink, continue -- corpse */ }
if (thrd_entry->request_latch_mode == PGBUF_LATCH_FLUSH) {
assert (pgbuf_bcb_is_async_flush_request (bufptr) || pgbuf_bcb_is_flushing (bufptr));
prev_thrd_entry = thrd_entry; continue; /* skip -- leave in list, do NOT wake */
}
/* ... R/W grant via compare_exchange_strong loop ... */
}

분기별 설명:

  1. PGBUF_NO_LATCH 대기자 — 포기한 스레드(타임아웃/인터럽트로 아직 자기 제거 안 됨). Unlink하고 continue — “타임아웃 대기자 정리”.

  2. PGBUF_LATCH_FLUSH 대기자 — 래치 holder가 아니며, 플러시가 별도로 깨운다. prev_thrd_entry를 전진시키고 continue해 제자리에 유지 — “FLUSH 대기자 건너뛰기” 규칙. prev를 전진시켜(unlink 하지 않고) FLUSH 항목을 대기열에 유지하고 그 뒤 후속자들에게 접근 가능하게 한다.

  3. R/W 대기자 — 새 impl을 대상으로 내부 CAS 루프(compare_exchange_strong) 진입:

    • latch_mode == NO_LATCH, 또는 (latch_mode == READ 이고 대기자가 READ 원함): 승인 가능. 스레드 항목을 잠금. request_latch_mode == PGBUF_NO_LATCH를 재확인 (외부 검사와 잠금 사이에 타임아웃이 발생했을 수 있음) — 그러면 unlink, can_grant = false, break. 그렇지 않으면 can_grant = true, fcntrequest_fix_count 추가, latch_mode를 대기자 모드로 설정.
    • 그 외, latch_mode == READ(이번 패스에서 R이 이미 승인됐고 이 대기자는 WRITE 원함): prev_thrd_entry = thrd_entry로 설정하고 내부 CAS 루프만 break. should_stop은 false 유지. 쓰기자는 제자리에 남겨 건너뛰고, 외부 순회는 계속되어 그 뒤 READ 대기자를 찾는다(“Look for other readers.”). 외부 순회는 WRITE 팔에서만 실제로 멈춘다.
    • 그 외(latch_mode == WRITE): should_stop = true, break.

    내부 루프 후, should_stop은 외부 루프를 중단시킨다. 아니면 can_grant가 대기자를 unlink하고 pgbuf_wakeup한다.

순효과(헤더 주석과 일치): READ 승인마다 latch_mode == READ가 남으므로, 헤드에 연속된 모든 READ 대기자가 깨어난다. 페이지가 R-granted 상태에서 만난 WRITE 대기자는 건너뛰고 그 뒤를 계속 스캔한다. 보유된 WRITE latch(WRITE 팔, should_stop 경유)만이 패스를 종료하므로, 최대 하나의 쓰기자만 승인된다.

마지막으로 함수는 힌트를 재계산한다:

// pgbuf_wakeup_reader_writer -- src/storage/page_buffer.c
if (!pgbuf_is_exist_blocked_reader_writer (bufptr))
set_waiter_exists (&bufptr->atomic_latch, false); /* clear the guard */

불변 조건 — waiter_exists는 R/W 대기자가 존재할 때만 true다. pgbuf_is_exist_blocked_reader_writernext_wait_thrd를 순회하며 PGBUF_LATCH_READ / PGBUF_LATCH_WRITE 항목만 센다(FLUSH와 NO_LATCH는 제외). 마지막 R/W 대기자가 깨어난 뒤 stale 비트가 남아 있으면, pgbuf_latch_bcb_upon_fix의 Case 1이 새 독자들을 영구히 블록할 수 있다(유령 기아 현상).

  1. 래치는 uint64_t 하나(pgbuf_atomic_latch_impl): latch_mode | waiter_exists | fcnt (16 + 16 + 32비트). 단일 워드 CAS만이 동시성 환경에서 모드/카운트 트리플을 찢김 없이 유지한다.
  2. pgbuf_latch_bcb_upon_fixcompare_exchange_strong 하나로 재시도되는 완전 분기 트리다: 유휴 단락; R-on-R(대기자가 있고 호출자가 holder가 아니면 블록 — 쓰기 기아 방지 가드); W-holder 통과(sub-2-1); 단독 독자 제자리 승격(sub-2-2); 외부자(Case 3)와 경합 승격을 위한 블록 경로.
  3. 단독 독자 R→W 제자리 전환(sub-2-2)은 활성 상태이고 도달 가능하다. 소스에 고정된 승격 API는 pgbuf_promote_read_latch_debugpgbuf_block_bcb의 단일 승격자 단언이다. 경합된 제자리 업그레이드의 어떤 deprecation도 이 소스에는 보이지 않는 외부 프로젝트 역사다.
  4. Holder(pgbuf_holder)는 스레드별 BCB fix_count를 기록한다. 래치 워드의 fcnt와 비교해 “내가 유일한 fixer인가”를 판단한다. allocate/find/remove는 holder를 자유 목록 또는 보유 목록 중 정확히 하나에만 유지하며(next_holder == NULL 단언), 동시에 두 곳에 속하지 않는다.
  5. 블록은 그래프 기반이 아닌 타임아웃 기반이다. pgbuf_block_bcbnext_wait_thrd에 삽입한다 — 일반 대기자와 fix 경로의 promote_needed 분기는 꼬리, 전용 pgbuf_promote_read_latch_debug API(as_promote)만 헤드. pgbuf_timed_sleeppgbuf_latch_timeout을 기다리며, 타임아웃 시 대기자는 교착 희생자가 되고 pgbuf_timed_sleep_error_handling이 이를 제거한다(헤드 경우에서는 숨겨진 독자를 깨워 복구).
  6. pgbuf_wakeup_reader_writer는 큐를 한 번 순회한다: NO_LATCH 시체 정리, FLUSH 대기자 건너뛰기, 연속된 모든 READ 대기자 깨우기, 최대 하나의 WRITE 승인. 패스 중 독자들이 승인되는 동안 만난 WRITE 대기자는 추가 독자 승인을 막지 않는다 — 보유된 WRITE latch(should_stop)만 순회를 종료한다.
  7. waiter_exists는 매 깨우기 후 pgbuf_is_exist_blocked_reader_writer로 정확히 재추적되므로, stale 비트가 새 독자를 유령 기아에 빠뜨릴 수 없다.

Chapter 6: 페이지 더티 처리와 플래그·존 워드 구조

섹션 제목: “Chapter 6: 페이지 더티 처리와 플래그·존 워드 구조”

BCB의 쓰기 래치(write latch)를 보유한 상태에서 페이지를 수정할 때 실질적으로 일어나는 일은 세 가지 작은 동작으로 구성된다. 페이지 이미지에 redo LSA를 스탬핑하고, BCB에 dirty 표시를 하고, 최초 한 번에 한해 가장 오래된 미플러시 LSA를 기록하는 것이다. 이 동작 중 어느 것도 뮤텍스를 사용하지 않는다. 대신 32비트 워드 하나(bcb->flags)를 lock-free CAS 재시도 루프로 변경하며, 별도로 패킹된 카운터 워드 하나를 함께 활용한다. 이 장은 그 접근자 레이어를 해부한다. 이후 모든 장 — unfix(7장), WAL 하에서의 플러시(8장), 희생자 선정(9장) — 이 이 레이어를 읽거나 변경하므로, 여기서 확립된 불변 조건들은 구조 전반의 핵심이다. WAL 계약, 체크포인트 수평선 등의 배경은 고수준 동반 문서를 참조한다. 이 장은 그 이론을 재유도하지 않는다.

6.1 하나의 워드, 세 개의 필드: flags, zone, lru index

섹션 제목: “6.1 하나의 워드, 세 개의 필드: flags, zone, lru index”

PGBUF_BCB::flagsvolatile int이다. 소스 코드의 주석이 레이아웃을 정확히 고정한다(Figure 6-1): “(bcb flags + zone = 2 bytes) + (lru index = 2 bytes)”. 즉 32비트 워드는 절반에서 분리된다. 하위 16비트는 LRU 리스트 인덱스(PGBUF_LRU_INDEX_MASK = 0x0000FFFF, PGBUF_LRU_NBITS = 16)이고, 상위 16비트는 플래그 비트와 zone 선택자를 함께 수용한다.

// pgbuf_bcb (struct) -- src/storage/page_buffer.c
volatile int flags; /* <- packed: flag bits + zone + lru index */
// ... condensed ...
volatile int count_fix_and_avoid_dealloc; /* <- a SECOND packed word, see 6.8 */
LOG_LSA oldest_unflush_lsa; /* <- WAL watermark, established once per dirty cycle */

상위 절반에서 두 네임스페이스는 비트 단위로 분리된다. 7개의 플래그 비트는 최상단(0x80000000..0x02000000, 비트 25-31)을 차지하고, zone 열거형은 인덱스 바로 위인 비트 16-19에 위치한다. Zone 값은 PGBUF_LRU_NBITS의 시프트로 표현된다. LRU 서브존은 1<<16, 2<<16, 3<<16이며, LRU 외 zone은 두 비트를 더 건너뛰어(PGBUF_LRU_NBITS + 2 = 18) LRU 마스크와 충돌하지 않는다 — PGBUF_INVALID_ZONE = 1<<18, PGBUF_VOID_ZONE = 2<<18. PGBUF_BCB_FLAGS_MASKPGBUF_ZONE_MASK | PGBUF_LRU_INDEX_MASK는 단 하나의 비트도 공유하지 않으므로, pgbuf_bcb_update_flags는 플래그 비트만, pgbuf_bcb_change_zone은 zone+index만 — 서로의 영역을 보존하며 — 동일한 워드를 CAS로 수정할 수 있다.

BCB가 생성될 때의 초기값은 PGBUF_BCB_INIT_FLAGS = PGBUF_INVALID_ZONE이다: 플래그 비트 없음, zone = INVALID, index 0. 이것이 Figure 6-3의 시작 상태이다.

전체 zone 카탈로그:

Zone 값비트숫자값의미설정 주체 (zone 이동은 pgbuf_bcb_change_zone을 경유)
PGBUF_INVALID_ZONE1<<180x00040000빈/초기화되지 않은 BCB (invalid 리스트)PGBUF_BCB_INIT_FLAGS; 해제 시 리셋
PGBUF_VOID_ZONE2<<180x00080000과도 상태: 디스크에서 읽은 후 리스트 삽입 전, 또는 리스트에서 제거된 후 희생 전디스크 읽기 경로; 희생자 추출
PGBUF_LRU_1_ZONE1<<160x00010000가장 뜨거운 LRU 서브존; 희생 대상 아님unfix/boost로 zone 1 진입
PGBUF_LRU_2_ZONE2<<160x00020000hot과 victim 사이의 버퍼 서브존; 희생 대상 아님LRU zone 조정
PGBUF_LRU_3_ZONE3<<160x00030000희생화 서브존; 후보가 존재하는 유일한 zoneLRU zone 조정 / zone 2에서 낙하

세 개의 마스크가 이를 디코딩한다. PGBUF_LRU_ZONE_MASK(= 1|2|3 << 16)는 세 LRU 서브존 비트를 OR하고, PGBUF_ZONE_MASK(= PGBUF_LRU_ZONE_MASK | PGBUF_INVALID_ZONE | PGBUF_VOID_ZONE)는 모든 zone을 포괄하며, PGBUF_LRU_INDEX_MASK는 하위 16비트 리스트 인덱스를 담는다. PGBUF_GET_ZONE(flags)(PGBUF_ZONE)(flags & PGBUF_ZONE_MASK)이다.

6.2 플래그 카탈로그: 모든 비트, 생산자, 소비자

섹션 제목: “6.2 플래그 카탈로그: 모든 비트, 생산자, 소비자”

flags의 상위 비트에는 7개의 플래그가 있다. 복합 마스크 PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK는 처음 네 가지의 OR로, BCB가 희생 대상이 될 수 없는 상태를 나타낸다. “희생 차단?” 열은 해당 마스크에 포함되는지 여부를 나타낸다. 표에는 플래그별로 생산자, 클리어 주체, 읽기 주체가 기재된다.

플래그비트생산자클리어 주체읽기 주체희생 차단?
PGBUF_BCB_DIRTY_FLAG0x80000000pgbuf_bcb_set_dirty, _update_flags_clear_dirty, _mark_is_flushingpgbuf_bcb_is_dirty
PGBUF_BCB_FLUSHING_TO_DISK_FLAG0x40000000pgbuf_bcb_mark_is_flushing_mark_was_flushed / _was_not_flushedpgbuf_bcb_is_flushing
PGBUF_BCB_VICTIM_DIRECT_FLAG0x20000000direct-victim 핸드오프 (9장)INVALIDATE로 대체pgbuf_bcb_is_direct_victim
PGBUF_BCB_INVALIDATE_DIRECT_VICTIM_FLAG0x10000000direct victim을 선점하는 fixer (4/5장)대기자가 재요청할 때pgbuf_bcb_is_invalid_direct_victim
PGBUF_BCB_MOVE_TO_LRU_BOTTOM_FLAG0x08000000dealloc 경로이를 이동시키는 unfix (7장)pgbuf_bcb_should_be_moved_to_bottom_lru아니오
PGBUF_BCB_TO_VACUUM_FLAG0x04000000pgbuf_notify_vacuum_followsvacuum 라우팅pgbuf_bcb_is_to_vacuum아니오
PGBUF_BCB_ASYNC_FLUSH_REQ0x02000000비동기 플러시 요청자들pgbuf_bcb_mark_is_flushingpgbuf_bcb_is_async_flush_request아니오

mark_is_flushing이 페이지가 dirty 상태일 때 호출되면, DIRTY(플러시가 이미지를 캡처함)와 ASYNC_FLUSH_REQ(요청이 이제 진행 중)를 함께 클리어하면서 FLUSHING을 세팅한다. 하나의 전이로 세 비트를 동시에 교체하는 것이다.

// PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK -- src/storage/page_buffer.c
#define PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK \
(PGBUF_BCB_DIRTY_FLAG \
| PGBUF_BCB_FLUSHING_TO_DISK_FLAG \
| PGBUF_BCB_VICTIM_DIRECT_FLAG \
| PGBUF_BCB_INVALIDATE_DIRECT_VICTIM_FLAG) /* <- the 4 disqualifiers; the other 3 flags are victim-neutral */

불변 조건 — 희생자 후보 카운트는 LRU zone 3에 속하면서 네 개의 비적격 비트를 하나도 가지지 않는 BCB의 수와 정확히 일치한다. BCB가 zone 3에 머무르는 동안 비적격 비트를 추가하거나 제거하는 모든 전이는 대칭적으로 후보 등록/해제를 수행해야 한다. 이는 pgbuf_bcb_update_flags, pgbuf_bcb_change_zone, pgbuf_bcb_set_dirty 빠른 경로에서 각각 강제된다. 이 중 하나라도 누락되면 LRU 희생자 카운터가 어긋나고, 희생자 선정 과정(9장)에서 유효한 후보를 건너뛰거나 유령 후보를 쫓게 된다. pgbuf_bcb_avoid_victim은 동일한 마스크에 대한 읽기 측 질의이다.

6.3 공유 CAS 루프 구조: pgbuf_bcb_update_flagspgbuf_bcb_change_zone

섹션 제목: “6.3 공유 CAS 루프 구조: pgbuf_bcb_update_flags와 pgbuf_bcb_change_zone”

두 함수는 하나의 lock-free 골격을 공유한다 — bcb->flags를 읽고, 새 워드를 계산하고, CAS를 시도하고, 실패하면 재시도한다. 차이는 어느 절반을 재계산하느냐와 이후에 무엇을 조정하느냐뿐이다.

pgbuf_bcb_update_flags는 일반적인 플래그 변경자다. zone과 이름 없는 플래그를 보존하면서 일부 비트를 세팅하거나 클리어한다. dirty 빠른 경로를 제외한 모든 플래그 전이가 이 함수를 거친다.

// pgbuf_bcb_update_flags -- src/storage/page_buffer.c
assert ((set_flags & (~PGBUF_BCB_FLAGS_MASK)) == 0); /* <- callers may only touch flag bits ... */
assert ((clear_flags & (~PGBUF_BCB_FLAGS_MASK)) == 0); /* <- ... never zone/index bits */
do
{
old_flags = bcb->flags;
new_flags = old_flags | set_flags;
new_flags = new_flags & (~clear_flags);
if (old_flags == new_flags)
return; /* <- no-op: bits already as desired, skip CAS + bookkeeping (contention saver) */
}
while (!ATOMIC_CAS_32 (&bcb->flags, old_flags, new_flags));

pgbuf_bcb_change_zone은 반대다. 동일한 루프를 사용하지만 zone+index를 재계산한다 — new_flags = (old_flags & PGBUF_BCB_FLAGS_MASK) | new_zone_idx;(여기서 new_zone_idx = PGBUF_MAKE_ZONE(new_lru_idx, new_zone)) — 모든 플래그 비트를 보존하며, 조기 no-op 반환이 없다.

CAS 이후 두 함수는 분기한다. **update_flags**는 두 가지 보정을 수행한다(Figure 6-2). zone-3 희생자 후보 조정(PGBUF_GET_ZONE(old_flags) == PGBUF_LRU_3_ZONE인 경우만 — zone은 여기서 바뀌지 않으므로 old_flags에서 읽음)과, DIRTY 비트의 토글 여부에 따른 dirties_cnt 조정이다. 후자는 0 <= dirties_cnt <= num_buffers를 assert로 닫는다.

flowchart TD
  A["진입: set_flags, clear_flags — Figure 6-2"] --> B["old = bcb->flags<br/>new = (old | set) & ~clear"]
  B --> C{"old == new?"}
  C -->|예| R["반환 (no-op)"]
  C -->|아니오| D{"CAS(flags, old, new)?"}
  D -->|실패| B
  D -->|성공| E{"zone(old) == LRU_3?"}
  E -->|예| F{"희생자 후보 자격 변경?"}
  F -->|"후보 됨"| G["lru_add_victim_candidate"]
  F -->|"후보 탈락"| H["lru_remove_victim_candidate"]
  F -->|"변화 없음"| I
  E -->|아니오| I["DIRTY 비트 토글?"]
  G --> I
  H --> I
  I -->|"set->clear"| J["dirties_cnt -= 1"]
  I -->|"clear->set"| K["dirties_cnt += 1"]
  I -->|"변화 없음"| L["범위 assert; 완료"]
  J --> L
  K --> L

**change_zone**은 리스트별 zone 카운터와 희생자 후보 자격을 조정한다. Zone 이동은 LRU 리스트 뮤텍스 하에서 수행되므로 count_lru1/2/3은 단순 증감으로 처리되며, 원자적 연산이 아니다. CAS는 flags에 대해서만 lock-free다 — 동시에 pgbuf_set_dirty가 동일한 워드의 플래그 비트를 뮤텍스 없이 뒤집을 수 있기 때문이다. 분기 구조는 다음과 같다.

  1. is_valid_victim_candidate = (old_flags & INVALID_VICTIM_CANDIDATE_MASK) == 0 — 플래그 속성으로, 이동 전후 모두 동일하게 유지된다.
  2. 탈출 (old_flags & PGBUF_LRU_ZONE_MASK): 이전 리스트가 shared였으면 lru_shared_pgs_cnt를 감소; 이전 zone에 따라 switch로 해당 count_lruN 감소; zone 3이면서 유효 후보면 pgbuf_lru_remove_victim_candidate; default: assert(false).
  3. 진입 (new_zone & PGBUF_LRU_ZONE_MASK): 대칭적으로 증가, zone 3의 유효 후보에 pgbuf_lru_add_victim_candidate; default: assert(false).

default: assert(false) 분기는 전체성 불변 조건을 인코딩한다: LRU zone에 속한 BCB는 정확히 zone 1/2/3 중 하나에만 속한다(zone 필드는 멤버십 마스크가 아니라 단일 값이다). 두 번째 assert는 힌트 일관성을 보장한다: lru_list->victim_hint != bcb || zone(old) != LRU_3 — zone 3 BCB가 떠나기 전에 힌트가 이미 재지정되어 있어야 한다. 단, 체크포인트가 update_flags로 동시에 재지정하는 경우는 예외다.

stateDiagram-v2
  [*] --> INVALID: 초기 플래그 = PGBUF_INVALID_ZONE
  INVALID --> VOID: 디스크에서 읽기
  VOID --> LRU: unfix이 리스트에 삽입
  LRU --> LRU: zone 1에서 2, 2에서 3으로 조정
  LRU --> VOID: 희생자로 선택됨
  VOID --> [*]: 새 페이지에 재사용
  note right of LRU
    LRU_3 서브존만
    희생 대상 자격 있음
  end note

Figure 6-3: pgbuf_bcb_change_zone에 의해 구동되는 zone 전이. 플래그 네임스페이스는 모든 엣지에서 변경 없이 유지된다.

6.4 pgbuf_bcb_get_zone과 디코드 매크로

섹션 제목: “6.4 pgbuf_bcb_get_zone과 디코드 매크로”

pgbuf_bcb_get_zone은 순수한 디코드 함수로, 워드를 마스킹하여 zone 열거형을 반환한다.

// pgbuf_bcb_get_zone -- src/storage/page_buffer.c
STATIC_INLINE PGBUF_ZONE
pgbuf_bcb_get_zone (const PGBUF_BCB * bcb)
{
return PGBUF_GET_ZONE (bcb->flags); /* <- (flags & PGBUF_ZONE_MASK) */
}

이 위에 두 개의 매크로가 구축되어 있으며, 이후 장들이 가장 자주 묻는 두 가지 질문에 답한다.

// PGBUF_IS_BCB_IN_LRU* -- src/storage/page_buffer.c
#define PGBUF_IS_BCB_IN_LRU_VICTIM_ZONE(bcb) (pgbuf_bcb_get_zone (bcb) == PGBUF_LRU_3_ZONE)
#define PGBUF_IS_BCB_IN_LRU(bcb) ((pgbuf_bcb_get_zone (bcb) & PGBUF_LRU_ZONE_MASK) != 0)

PGBUF_IS_BCB_IN_LRU_VICTIM_ZONE은 정확한 등치 비교(zone 3만 희생 대상)이고, PGBUF_IS_BCB_IN_LRU마스크 검사다 — PGBUF_LRU_ZONE_MASK가 세 LRU 서브존 비트를 모두 OR하므로 zone 1/2/3는 일치하지만, VOID(2<<18)와 INVALID(1<<18)는 그 비트들이 마스크 범위 밖이라 일치하지 않는다. pgbuf_bcb_get_lru_index는 하위 16비트 인덱스를 반환하기 전에 PGBUF_IS_BCB_IN_LRU를 assert한다.

6.5 dirty 설정: 세 가지 진입점, 하나의 빠른 경로

섹션 제목: “6.5 dirty 설정: 세 가지 진입점, 하나의 빠른 경로”

수정자는 작은 호출 체인을 거쳐 dirty 처리에 도달한다. 공개 함수인 pgbuf_set_dirtyCAST_PGPTR_TO_BFPTR로 BCB를 복원하고, vpid를 검증한(디버그 전용)한 뒤, pgbuf_set_dirty_buffer_ptr에 위임하며, 호출자가 free_page == FREE를 전달한 경우에만 unfix를 수행한다. pgbuf_set_dirty_buffer_ptr는 실제 변경자 위에 놓인 래치/성능 레이어다.

// pgbuf_set_dirty_buffer_ptr -- src/storage/page_buffer.c
pgbuf_bcb_set_dirty (thread_p, bufptr);
holder = pgbuf_find_thrd_holder (thread_p, bufptr);
assert (get_latch (&bufptr->atomic_latch) == PGBUF_LATCH_WRITE); /* <- dirtier MUST hold the write latch */
assert (holder != NULL);
// ... condensed: mark holder->perf_stat.dirtied_by_holder, perfmon PSTAT_PB_NUM_DIRTIES ...

불변 조건 — 페이지는 세터가 BCB 쓰기 래치를 보유하는 동안에만 더티로 만들 수 있다. assert(get_latch(...) == PGBUF_LATCH_WRITE)가 이를 강제하며, 동시 쓰기자들(5장)을 직렬화하여 DIRTY/LSA 쌍이 일관성 있게 유지된다. 각각은 lock-free로 변경되더라도 마찬가지다. pgbuf_bcb_set_dirty의 CAS는 동일한 워드의 무관한 비트에 경쟁하는 다른 스레드(래치 없는 change_zone)에 대해서만 방어하며, 두 쓰기자 간의 경쟁에 대해서는 보호하지 않는다.

pgbuf_bcb_set_dirtyupdate_flags를 우회하는 수작업 빠른 경로다. dirty 처리가 가장 빈번한 경우이기 때문이다(소스 주석에 명시적으로 설명됨).

// pgbuf_bcb_set_dirty -- src/storage/page_buffer.c
do
{
old_flags = bcb->flags;
if (old_flags & PGBUF_BCB_DIRTY_FLAG)
return; /* <- already dirty: skip CAS + counter (common case) */
}
while (!ATOMIC_CAS_32 (&bcb->flags, old_flags, old_flags | PGBUF_BCB_DIRTY_FLAG));
ATOMIC_INC_64 (&pgbuf_Pool.monitor.dirties_cnt, 1); /* <- dirties_cnt += 1; assert range follows */
if (PGBUF_GET_ZONE (old_flags) == PGBUF_LRU_3_ZONE
&& (old_flags & PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK) == 0)
pgbuf_lru_remove_victim_candidate (thread_p, pgbuf_lru_list_from_bcb (bcb), bcb); /* <- newly dirty -> drop candidacy */

분기 구조: (1) 이미 dirty이면 조기 반환; (2) CAS로 비트 세팅; (3) dirties_cnt += 1 후 범위 assert; (4) BCB가 DIRTY 전에 유효한 zone-3 후보였다면(old_flags를 읽어 검사) 새 비트가 자격을 박탈하므로 제거한다 — §6.2 불변 조건을 속도를 위해 인라인으로 처리. 이 빠른 경로는 DIRTY를 세팅하기만 하므로, update_flags와 달리 dirty-cleared 분기나 add-candidate 분기는 필요하지 않다.

6.6 가장 오래된 미플러시 LSA 기록 — pgbuf_set_lsa

섹션 제목: “6.6 가장 오래된 미플러시 LSA 기록 — pgbuf_set_lsa”

pgbuf_set_lsa(로그/복구 관리자 전용)는 redo LSA를 스탬핑하고 더티 사이클당 최초 한 번에 한해 oldest_unflush_lsa를 확립한다. 임시 볼륨과 보조 볼륨에 대한 특수 분기가 있다.

// pgbuf_set_lsa -- src/storage/page_buffer.c
// ... condensed: debug-gated page-pointer validation may return NULL; assert (lsa_ptr != NULL) ...
if (pgbuf_is_temp_lsa (bufptr->iopage_buffer->iopage.prv.lsa)
|| PGBUF_IS_AUXILIARY_VOLUME (bufptr->vpid.volid) == true)
return NULL; /* <- branch 2: temp/aux pages are never WAL-tracked: bail */
if (pgbuf_is_temporary_volume (bufptr->vpid.volid) == true)
{
pgbuf_init_temp_page_lsa (&bufptr->iopage_buffer->iopage, IO_PAGESIZE); /* <- branch 3: force sentinel temp LSA */
if (logtb_is_current_active (thread_p))
return NULL; /* <- active txn on temp page carries no real LSA */
}
fileio_set_page_lsa (&bufptr->iopage_buffer->iopage, lsa_ptr, IO_PAGESIZE); /* <- branch 4: write redo LSA into image */
if (LSA_ISNULL (&bufptr->oldest_unflush_lsa)) /* <- branch 5: FIRST dirty since last flush? */
{
if (LSA_LT (lsa_ptr, &log_Gl.chkpt_redo_lsa))
{ /* ... condensed: re-read chkpt_redo_lsa under chkpt_lsa_lock; if still older,
raise ER_LOG_CHECKPOINT_SKIP_INVALID_PAGE + assert(false) ... */ }
LSA_COPY (&bufptr->oldest_unflush_lsa, lsa_ptr); /* <- watermark established */
}
// ... condensed: branch 6, #if defined(NDEBUG) also calls pgbuf_set_dirty_buffer_ptr (safety net) ...

주석이 압축하는 두 가지 사실이 있다. pgbuf_is_temp_lsa는 저장된 LSA를 센티넬 PGBUF_TEMP_LSA = { NULL_LOG_PAGEID - 1, NULL_LOG_OFFSET - 1 }(즉 (-2,-2))와 비교한다. 그리고 워터마크가 set-dirty가 아닌 여기에 위치하는 이유는, LSA가 존재하기 전에 페이지가 dirty 상태가 될 수 있기 때문이다 — 그러므로 최초 LSA 설정 시점에 닻을 내린다. 릴리스 빌드의 #if defined(NDEBUG) 꼬리 부분이 pgbuf_set_dirty_buffer_ptr를 강제 호출하는 것은, LSA가 방금 기록되어 반드시 플러시되어야 하므로 놓친 set-dirty 호출에 대한 안전망 역할을 한다.

불변 조건 — oldest_unflush_lsa는 아직 디스크에 없는 가장 이른 수정의 LSA로, clean 상태 이후 첫 dirty 시 한 번 설정되며 플러시 시에만 클리어된다. LSA_ISNULL 가드 덕분에 같은 사이클의 이후 set_lsa 호출들은 이 값에 손대지 않는다. 값은 절대 앞으로 이동하지 않는다. 8장의 WAL 규칙은 로그 플러시 순서 결정에 이를 읽고 성공적인 플러시 후 NULL로 리셋한다. 체크포인트는 이 값으로 가장 오래된 dirty 페이지를 찾는다.

6.7 읽기 측: pgbuf_bcb_is_dirtypgbuf_bcb_avoid_victim

섹션 제목: “6.7 읽기 측: pgbuf_bcb_is_dirty와 pgbuf_bcb_avoid_victim”

두 함수 모두 동일한 워드에 대한 단일 마스크 술어다 — 락 없이 volatile 읽기만 수행한다.

// pgbuf_bcb_is_dirty -- src/storage/page_buffer.c
STATIC_INLINE bool
pgbuf_bcb_is_dirty (const PGBUF_BCB * bcb)
{
return (bcb->flags & PGBUF_BCB_DIRTY_FLAG) != 0;
}
// pgbuf_bcb_avoid_victim -- src/storage/page_buffer.c
STATIC_INLINE bool
pgbuf_bcb_avoid_victim (const PGBUF_BCB * bcb)
{
return (bcb->flags & PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK) != 0; /* <- ANY of the 4 disqualifiers */
}

두 함수는 계층 관계를 이룬다. dirty BCB는 항상 avoid_victim을 true로 만들지만(DIRTY가 마스크에 포함되므로), avoid_victim은 플러시 중이거나 (무효화된) direct victim인 clean BCB에 대해서도 true가 될 수 있다. 따라서 9장의 희생자 스캔은 is_dirty가 아닌 avoid_victim을 호출한다 — dirty 여부는 비적격 사유 네 가지 중 하나일 뿐이다. 형제 per-flag 읽기 함수들(pgbuf_bcb_is_flushing, _is_direct_victim, _is_invalid_direct_victim, _is_async_flush_request, _is_to_vacuum, _should_be_moved_to_bottom_lru)은 각각 동일한 단일 비트 (flags & FLAG) != 0 검사다.

6.8 이중 목적 카운터 — count_fix_and_avoid_dealloc

섹션 제목: “6.8 이중 목적 카운터 — count_fix_and_avoid_dealloc”

flags와 겹치지 않는 별도의 volatile 워드로, 개의 16비트 서브 카운터를 32비트 int 하나에 패킹하여 각각을 단일 원자적 연산으로 변경할 수 있다.

서브 필드비트마스크 / 시프트변경자읽기 주체
avoid-dealloc 카운트하위 16비트PGBUF_BCB_AVOID_DEALLOC_MASK = 0x0000FFFFpgbuf_bcb_register_avoid_deallocation (+1), _unregister_ (-1, CAS)pgbuf_bcb_should_avoid_deallocation
fix 카운트상위 16비트<< PGBUF_BCB_COUNT_FIX_SHIFT_BITS (16)pgbuf_bcb_register_fix (+ 1<<16, 상한 있음)pgbuf_bcb_is_hot

이 둘이 합쳐진 이유는(struct 주석에 명시됨), avoid-dealloc은 원자적으로 변경되어야 하는데 2바이트 원자 연산은 일반적이지 않으므로, 두 가지가 CPU 네이티브 4바이트 워드 하나를 함께 사용하는 것이다.

// pgbuf_bcb_register_avoid_deallocation -- src/storage/page_buffer.c
assert ((bcb->count_fix_and_avoid_dealloc & 0x00008000) == 0); /* <- low-half top bit clear: overflow guard */
(void) ATOMIC_INC_32 (&bcb->count_fix_and_avoid_dealloc, 1); /* <- +1 touches only the low half */

register_fix1 << 16을 더하지만, 상한선 PGBUF_FIX_COUNT_THRESHOLD << 16 미만일 때만 수행한다 — 한번 hot이 되면 카운팅을 멈춘다(hotness는 단방향 래치이지, 실시간 카운트가 아니다). pgbuf_bcb_is_hot은 동일한 PGBUF_FIX_COUNT_THRESHOLD << 16과 비교한다(fix 카운트가 LRU hotness를 구동함, 7장). unregister 경로는 CAS 루프를 사용하며, avoid-dealloc 카운트가 이미 0인 경우도 허용한다 — 페이지가 희생되고 재로드된 pgbuf_ordered_fix 코너 케이스로, er_log_debug로 기록하고 언더플로우 없이 break한다. 이 카운터는 두 번째, 직교적 희생자 게이트다 — fix되거나 dealloc 보호된 페이지를 도달 불가로 유지하며, dirtiness가 탑승하는 플래그 게이트와 독립적이다. 9장은 두 가지 모두를 소비한다.

  1. bcb->flags는 절반에서 분리되는 32비트 워드 하나다: 하위 16비트 = LRU 인덱스(PGBUF_LRU_INDEX_MASK), 상위 16비트 = 플래그 비트(0x80000000..0x02000000)와 zone 선택자(비트 16-19; LRU 1..3<<16, INVALID 1<<18, VOID 2<<18). 플래그 비트와 zone 비트는 분리되어 있어 두 변경자가 서로를 덮어쓰지 않고 동일한 워드를 공유할 수 있다.
  2. pgbuf_bcb_update_flagspgbuf_bcb_change_zone은 하나의 CAS 재시도 루프를 공유한다: 전자는 플래그 비트를 변경하고(조기 no-op 반환 있음) dirties_cnt와 zone-3 후보 자격을 조정한다. 후자는 zone+index를 변경하고 LRU 뮤텍스 하에서 리스트별 zone 카운터를 조정한다. 모든 조정 분기가 실행되어야 한다. 그렇지 않으면 희생자 카운터가 어긋난다. default: assert(false) 분기는 LRU BCB가 정확히 zone 1/2/3 중 하나에 속한다는 사실을 인코딩한다.
  3. 4비트 PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK(DIRTY, FLUSHING, VICTIM_DIRECT, INVALIDATE_DV)가 희생 비적격을 정의한다. 나머지 세 플래그(MOVE_TO_LRU_BOTTOM, TO_VACUUM, ASYNC_FLUSH_REQ)는 희생 중립적이다. pgbuf_bcb_avoid_victim은 전체 마스크를 읽고, pgbuf_bcb_is_dirty는 그 중 한 비트를 읽는다.
  4. Dirty 처리는 일반 변경자 대신 수작업 빠른 경로 pgbuf_bcb_set_dirty를 사용한다(DIRTY 세팅 전용 CAS). 후보 자격/dirty 카운터 불변 조건을 인라인으로 유지한다. BCB 쓰기 래치 — pgbuf_set_dirty_buffer_ptr에서 assert됨, CAS에서가 아님 — 가 동시 쓰기자들을 직렬화하여 DIRTY/LSA 쌍을 일관성 있게 유지한다.
  5. oldest_unflush_lsapgbuf_set_lsa에서 더티 사이클당 정확히 한 번 확립된다. LSA_ISNULL로 가드되며, 절대 앞으로 이동하지 않고, 체크포인트 redo 수평선과 대조 검증된다. 임시 볼륨과 보조 볼륨은 제외된다(센티넬 PGBUF_TEMP_LSA = (-2,-2)).
  6. count_fix_and_avoid_dealloc은 두 번째 패킹된 워드다. fix 카운트(상위 16비트, 상한 있음, pgbuf_bcb_is_hot으로 hotness 구동)와 avoid-dealloc 카운트(하위 16비트)를 담아 두 가지 모두 하나의 네이티브 원자 연산에 수용한다 — dirtiness가 탑승하는 플래그 게이트와 독립적인, 직교적 fix/dealloc 희생자 게이트다.

Chapter 7: Unfix 시 LRU 이동, Aout 히스토리, Private-to-Shared 마이그레이션

섹션 제목: “Chapter 7: Unfix 시 LRU 이동, Aout 히스토리, Private-to-Shared 마이그레이션”

이 챕터가 답하는 질문: unfix 시 BCB가 세 LRU 존을 어떻게 이동하는가, 언제 top으로 boost되고, 언제 private 리스트에서 shared 리스트로 이주하는가, Aout 2Q 고스트 리스트는 어떤 역할을 하는가? 존 모델, private/shared 분리, 2Q 설계 의도는 상위 컴패니언 문서(cubrid-page-buffer-manager.md)에서 다룬다. BCB 구조체, 패킹된 flags/zone 워드, dirty 비트는 Chapter 1·6에서 이미 설명했으므로 여기서는 참조만 한다.

7.1 Unfix 진입 경로 — pgbuf_unfix에서 pgbuf_unlatch_bcb_upon_unfix까지

섹션 제목: “7.1 Unfix 진입 경로 — pgbuf_unfix에서 pgbuf_unlatch_bcb_upon_unfix까지”
// pgbuf_unfix -- src/storage/page_buffer.c
CAST_PGPTR_TO_BFPTR (bufptr, pgptr);
holder_status = pgbuf_unlatch_thrd_holder (thread_p, bufptr, &holder_perf_stat);
// ... perf tracking (perfmon_pbx_unfix) elided ...
if (pgbuf_lockfree_unfix_ro (thread_p, bufptr)) /* <- pure read latch: CAS-drop fcnt, no mutex */
return; /* <- never touches LRU */
PGBUF_BCB_LOCK (bufptr);
(void) pgbuf_unlatch_bcb_upon_unfix (thread_p, bufptr, holder_status); /* releases mutex inside */

불변 조건 — read-only fast path는 LRU를 재정렬하지 않는다. shared latch가 해제될 때 존은 변경되지 않는다. pgbuf_lockfree_unfix_ro가 CAS 성공 후 true를 반환하면 즉시 리턴한다. 재정렬은 마지막 unfixer나 writer에게만 허용된다 — 그렇지 않으면 모든 reader가 리스트 mutex를 놓고 경합하게 된다.

pgbuf_unlatch_bcb_upon_unfix가 실질적인 결정 엔진이다. 프롤로그에서 CAS로 fix count를 감소시킨다:

// pgbuf_unlatch_bcb_upon_unfix -- src/storage/page_buffer.c
do {
blocked_reader_writer = false; is_zero_fcnt = false;
impl_orig = get_impl (&bufptr->atomic_latch); impl_new = impl_orig;
impl_new.impl.fcnt--; /* <- drop one fix */
blocked_reader_writer = impl_orig.impl.waiter_exists;
if (impl_new.impl.fcnt == 0) {
is_zero_fcnt = true; impl_new.impl.latch_mode = PGBUF_NO_LATCH; /* <- last unfixer drops latch */
}
if (impl_new.impl.fcnt < 0) { /* <- "freed too much": defensive reset */
assert (false); er_set (...); impl_new.impl.fcnt = 0;
impl_new.impl.waiter_exists = false;
impl_new.impl.latch_mode = PGBUF_NO_LATCH; is_zero_fcnt = true; break;
}
} while (!bufptr->atomic_latch.compare_exchange_weak (impl_orig.raw, impl_new.raw, ...));

CAS(Chapter 5)는 is_zero_fcnt(마지막 보유자 여부)와 blocked_reader_writer(latch 대기자 존재 여부)를 결정한다. 재정렬은 is_zero_fcnt && !blocked_reader_writer 일 때만 수행된다 — 대기 중인 스레드가 있다면 BCB를 즉시 재-latch하므로 이동은 낭비다.

flowchart TD
  A["pgbuf_unlatch_bcb_upon_unfix\nCAS: fcnt--"] --> B{"is_zero_fcnt?"}
  B -->|no| W["waiter 깨우기\nmutex 해제"]
  B -->|yes| C{"MOVE_TO_LRU_BOTTOM?"}
  C -->|yes| D["pgbuf_move_bcb_to_bottom_lru\ndealloc 단축 경로"]
  C -->|no| E{"blocked_reader_writer?"}
  E -->|yes| W
  E -->|no| F["존(zone) 분기"]
  F --> Z0["VOID -> pgbuf_unlatch_void_zone_bcb"]
  F --> Z1["LRU_1 -> 유지 또는 prv->shr"]
  F --> Z2["LRU_2 -> 유지, 오래됐으면 boost, 또는 prv->shr"]
  F --> Z3["LRU_3 -> boost 또는 prv->shr 또는 direct-victim"]
  Z0 --> W
  Z1 --> W
  Z2 --> W
  Z3 --> W
  D --> W

Figure 7-1 — pgbuf_unlatch_bcb_upon_unfix의 분기 구조. fcnt가 0이고 대기자가 없을 때만 존 분기에 도달한다.

// pgbuf_unlatch_bcb_upon_unfix -- src/storage/page_buffer.c
if (is_zero_fcnt) {
assert (LSA_ISNULL (&bufptr->oldest_unflush_lsa) || pgbuf_bcb_is_dirty (bufptr));
if (pgbuf_bcb_should_be_moved_to_bottom_lru (bufptr)) /* <- MOVE_TO_LRU_BOTTOM flag */
pgbuf_move_bcb_to_bottom_lru (thread_p, bufptr); /* dealloc shortcut */
else if (blocked_reader_writer == false) {
th_lru_idx = PGBUF_THREAD_HAS_PRIVATE_LRU (thread_p)
? PGBUF_LRU_INDEX_FROM_PRIVATE (PGBUF_PRIVATE_LRU_FROM_THREAD (thread_p)) : -1; /* own list or none */
switch (pgbuf_bcb_get_zone (bufptr)) { /* ... see 7.3 ... */ }
}
}

pgbuf_bcb_should_be_moved_to_bottom_lruPGBUF_BCB_MOVE_TO_LRU_BOTTOM_FLAG 비트를 검사한다. 이 플래그는 dealloc 경로에서 설정된다 — 해제된 페이지는 hot 위치에 둘 가치가 없으므로 즉시 바닥으로 밀어 최우선 희생 대상으로 만든다. th_lru_idx(자신의 private 리스트 인덱스, 없으면 -1)는 이후 모든 private/shared 판단의 기준점이 된다.

불변 조건 — oldest_unflush_lsa가 있으면 dirty 비트도 반드시 있다. Chapter 6의 WAL 불변 조건: 미처리 flush LSA가 있는 페이지는 dirty 상태를 유지해야 한다. 그렇지 않으면 flush 데몬(Chapter 8)이 해당 페이지를 건너뛰어 WAL이 깨진다.

모든 LRU 케이스에 공통으로 등장하는 두 가지 가드 조건을 한 번만 인용하고, 이후 세 존 모두에서 참조한다:

// pgbuf_unlatch_bcb_upon_unfix (per-case prologue) -- src/storage/page_buffer.c
if (PGBUF_SHOULD_IGNORE_UNFIX (thread_p, bufptr)) { ...KEEP_VAC stat...; break; } /* <- 캐시 워밍 안 함 */
if (pgbuf_should_move_private_to_shared (thread_p, bufptr, th_lru_idx)) { /* <- see 7.5 */
pgbuf_lru_move_from_private_to_shared (thread_p, bufptr); ...PRV_TO_SHR_MID stat...; break;
}

PGBUF_SHOULD_IGNORE_UNFIX는 vacuum 전용이 아니다. 실제 정의는 VACUUM_IS_THREAD_VACUUM_WORKER (th) || pgbuf_is_temporary_volume (buf->vpid.volid) (SERVER_MODE; 그 외에는 false)다. vacuum 워커 임시 볼륨 페이지 양쪽에 발동된다 — 두 경우 모두 BCB를 hot으로 승격시키거나 캐시를 워밍해서는 안 된다(소스 주석에서는 체크포인트 스레드도 같은 그룹으로 언급한다). pgbuf_should_move_private_to_shared(7.5)는 경합이 많은 페이지를 shared로 올린다. 기본 액션은 이 가드 이후에만 존에 따라 달라진다. 단, LRU_3 케이스는 private-to-shared 검사보다 SHOULD_IGNORE_UNFIX 분기를 먼저 적용한다(아래 참조).

VOID (Chapter 4): pgbuf_unlatch_void_zone_bcb에 위임(7.4).

LRU_1 (가장 hot): 가드 이후에는 아무것도 하지 않고 hit만 기록한다 — 존 1은 재정렬이 없다:

/* after the per-case prologue, plus a PRV_KEEP/SHR_KEEP stat: */
pgbuf_bcb_register_hit_for_lru (bufptr); break; /* <- never boost zone 1 */

LRU_2 (boost 가능 존): 충분히 오래된 경우에만 boost:

if (PGBUF_IS_BCB_OLD_ENOUGH (bufptr, pgbuf_lru_list_from_bcb (bufptr)))
pgbuf_lru_boost_bcb (thread_p, bufptr); /* <- 충분히 오래됨 -> top으로 승격 */
else { ...PRV_KEEP / SHR_KEEP stat... } /* <- 너무 최신: 제자리 유지 */
pgbuf_bcb_register_hit_for_lru (bufptr); break;

LRU_3 (희생 존): 실제 unfix는 항상 boost하지만, PGBUF_SHOULD_IGNORE_UNFIX 분기에서는 BCB를 direct victim으로 넘길 수 있다:

case PGBUF_LRU_3_ZONE:
if (PGBUF_SHOULD_IGNORE_UNFIX (...)) {
if (!pgbuf_bcb_avoid_victim (bufptr) && pgbuf_assign_direct_victim (thread_p, bufptr))
{ ...DIRECT_VACUUM_LRU stat... } /* <- 대기 중인 스레드에 직접 넘김 */
else { ...THREE_KEEP_VAC stat... }
break;
}
if (pgbuf_should_move_private_to_shared (...)) { ...move; THREE_PRV_TO_SHR_MID...; break; }
pgbuf_lru_boost_bcb (thread_p, bufptr); /* <- 규칙 3: 존 3에서는 항상 boost */
pgbuf_bcb_register_hit_for_lru (bufptr); break;

분기 처리가 끝난 뒤 함수는 latch 대기자를 깨운다(pgbuf_wakeup_reader_writer). 비동기 flush가 요청된 경우(pgbuf_bcb_is_async_flush_request)에는 pgbuf_bcb_safe_flush_force_unlock(내부에서 unlock)을 사용하고, 그렇지 않으면 직접 unlock한다. assert (... != PGBUF_LATCH_FLUSH)는 unfix가 flush latch를 만나지 않음을 보장한다 — flush는 데몬의 몫이다(Chapter 8).

7.4 VOID 존 처리 — pgbuf_unlatch_void_zone_bcb와 Aout 히트

섹션 제목: “7.4 VOID 존 처리 — pgbuf_unlatch_void_zone_bcb와 Aout 히트”

VOID BCB는 비거주(non-resident) 페이지를 위해 방금 할당된 상태다. 먼저 Aout에서 VPID를 제거(re-fix를 히트로 기록)한 뒤, private 리스트 소유 여부와 Aout 멤버십에 따라 분기한다:

// pgbuf_unlatch_void_zone_bcb -- src/storage/page_buffer.c
if (pgbuf_Pool.buf_AOUT_list.max_count > 0) { aout_enabled = true;
aout_list_id = pgbuf_remove_vpid_from_aout_list (thread_p, &bcb->vpid); } /* <- 2Q 조회+제거 */
if (PGBUF_VACUUM_SHOULD_IGNORE_UNFIX (thread_p)) { /* vacuum worker only here */
if (!pgbuf_bcb_avoid_victim (bcb) && pgbuf_assign_direct_victim (thread_p, bcb)) {
// ... if Aout on: pgbuf_add_vpid_to_aout_list (..., aout_list_id) ... <- re-ghost
return; }
aout_list_id = PGBUF_AOUT_NOT_FOUND; /* <- vacuum never gets Aout-boost */
}
if (thread_private_lru_index != -1) {
if (PGBUF_VACUUM_SHOULD_IGNORE_UNFIX (thread_p)) { /* <- vacuum: top, hit 없음 */
pgbuf_lru_add_new_bcb_to_top (thread_p, bcb, thread_private_lru_index); return; }
if (!aout_enabled || thread_private_lru_index == aout_list_id) { /* <- Aout HIT -> LRU 1 top */
pgbuf_lru_add_new_bcb_to_top (thread_p, bcb, thread_private_lru_index);
pgbuf_bcb_register_hit_for_lru (bcb); return; }
if (aout_list_id == PGBUF_AOUT_NOT_FOUND) { /* <- cold miss -> middle */
pgbuf_lru_add_new_bcb_to_middle (thread_p, bcb, thread_private_lru_index);
pgbuf_bcb_register_hit_for_lru (bcb); return; }
/* fall through: ghosted in a *different* private list -> shared */
}
pgbuf_lru_add_new_bcb_to_middle (thread_p, bcb, pgbuf_get_shared_lru_index_for_add ()); /* <- shared middle */
if (!PGBUF_VACUUM_SHOULD_IGNORE_UNFIX (thread_p)) pgbuf_bcb_register_hit_for_lru (bcb);

이 분기는 PGBUF_VACUUM_SHOULD_IGNORE_UNFIX(vacuum 워커 전용)를 기준으로 한다 — 7.3의 PGBUF_SHOULD_IGNORE_UNFIX에 포함된 임시 볼륨 조건은 VOID 경로에는 적용되지 않는다. private LRU 스레드, Aout 활성화, non-vacuum 조건에서의 배치 규칙:

Aout 결과배치 위치의미
== aout_list_id자신의 private 리스트 top (LRU 1)내 리스트에서 퇴거됐던 페이지 — 2Q second-touch, hot으로 승격
!aout_enabled자신의 private 리스트 top히스토리 없음; 첫 unfix를 warm으로 간주
PGBUF_AOUT_NOT_FOUND자신의 private 리스트 middle처음 보는 페이지 — cold, LRU-1/2 경계에 착지
다른 리스트shared 리스트 middle여러 워커가 공유; 할당량(Chapter 10) 적용

불변 조건 — Aout 제거는 배치보다 먼저 일어난다. aout_list_id는 삽입 전에 한 번만 캡처되며 전체 분기를 이끈다. 제거가 배치 이후에 일어났다면, 같은 페이지를 re-fix하는 두 스레드가 모두 “히트”로 처리되어 이중 승격이 발생할 수 있다. Aout_mutex(7.6)가 조회-제거를 직렬화하므로 정확히 한 스레드만 고스트를 소비한다.

7.5 pgbuf_should_move_private_to_shared — 마이그레이션 판단

섹션 제목: “7.5 pgbuf_should_move_private_to_shared — 마이그레이션 판단”
// pgbuf_should_move_private_to_shared -- src/storage/page_buffer.c
int bcb_lru_idx = pgbuf_bcb_get_lru_index (bcb);
if (PGBUF_IS_SHARED_LRU_INDEX (bcb_lru_idx)) return false; /* <- 이미 shared */
if (thread_private_lru_index != bcb_lru_idx) return true; /* 조건 1: 다른 스레드가 unfix */
if (!pgbuf_bcb_is_hot (bcb)) return false; /* 조건 2a: hot이어야 함 */
if (!PGBUF_IS_BCB_OLD_ENOUGH (bcb, PGBUF_GET_LRU_LIST (bcb_lru_idx))) return false; /* 조건 2b: 그리고 오래됨 */
return true; /* hot + 오래됨 -> shared로 올림 */

두 가지 트리거: (1) 외부 스레드 unfix — BCB가 private 리스트 X에 있지만 unfixer의 리스트는 Y(또는 -1)인 경우. 두 명 이상의 워커가 접근한 페이지는 shared로 이동한다. (2) hot이고 오래됨 — 같은 리스트이지만, hot(pgbuf_bcb_is_hot)하고 충분히 오래된 경우 shared로 올린다.

// pgbuf_bcb_is_hot / pgbuf_bcb_register_fix -- src/storage/page_buffer.c
// hot: count_fix_and_avoid_dealloc >= (PGBUF_FIX_COUNT_THRESHOLD << PGBUF_BCB_COUNT_FIX_SHIFT_BITS)
// == 64 << 16 (fix count lives in the high 16 bits)
// register_fix saturates: stops incrementing once the threshold bit is set.

count_fix_and_avoid_dealloc는 fix count(상위 16비트, pgbuf_bcb_register_fix가 증가, 64회 임계값에서 포화)와 avoid-dealloc count(하위 16비트, PGBUF_BCB_AVOID_DEALLOC_MASK)를 하나의 필드에 패킹한다. Chapter 6 참조.

// PGBUF_IS_BCB_OLD_ENOUGH -- src/storage/page_buffer.c
#define PGBUF_IS_BCB_OLD_ENOUGH(bcb, lru_list) \
(PGBUF_AGE_DIFF ((bcb)->tick_lru_list, (lru_list)->tick_list) >= ((lru_list)->count_lru2 / 2))

BCB는 삽입 시 tick_list에서 tick_lru_list를 스탬프로 찍는다. tick_list는 add-to-top/middle마다 증가한다. “충분히 오래됨”의 기준은 존 2 크기의 절반(count_lru2 / 2) 이상의 신규 삽입이 앞질러 지나간 경우다 — 짧은 시간 안에 두 번 fix된 페이지는 두 번째 unfix 시에 boost되지 않는다. PGBUF_AGE_DIFF는 31비트 tick의 랩어라운드를 처리한다.

7.6 Aout 2Q 고스트 리스트 — pgbuf_aout_bufpgbuf_aout_list

섹션 제목: “7.6 Aout 2Q 고스트 리스트 — pgbuf_aout_buf와 pgbuf_aout_list”

Aout은 최근에 희생된 페이지의 VPID만 보관한다(BCB는 아님) — FIFO 구조에 shard별 해시 테이블을 덧붙여 O(1) 조회를 제공한다.

// struct pgbuf_aout_buf -- src/storage/page_buffer.c
struct pgbuf_aout_buf {
VPID vpid; /* page VPID */
int lru_idx; /* which LRU list it was evicted from */
PGBUF_AOUT_BUF *next; /* next element in list */
PGBUF_AOUT_BUF *prev; /* prev element in list */
};
필드역할
vpid고스트 식별자 / 해시 키; VPID_SET_NULL이면 빈 노드
lru_idx퇴거됐던 LRU 리스트 — re-fix 시 동일 리스트로 복귀(7.4)
next / prevFIFO 링크; 재활용 시에는 free-list 링크로 겸용. prev는 히트 시 O(1) 중간 unlink 제공
// struct pgbuf_aout_list -- src/storage/page_buffer.c
struct pgbuf_aout_list {
pthread_mutex_t Aout_mutex; /* integrity of the whole list (SERVER_MODE) */
PGBUF_AOUT_BUF *Aout_top; /* top of the queue (most recent) */
PGBUF_AOUT_BUF *Aout_bottom; /* bottom of the queue (oldest) */
PGBUF_AOUT_BUF *Aout_free; /* free list of recycled nodes */
PGBUF_AOUT_BUF *bufarray; /* preallocated node array */
int num_hashes; /* number of hash shards */
MHT_TABLE **aout_buf_ht; /* per-shard VPID -> node hash */
int max_count; /* capacity; <= 0 disables Aout */
};
필드역할
Aout_mutex전체 리스트+해시 잠금; 7.4의 조회-제거를 직렬화
Aout_top / Aout_bottom가장 최신 / 가장 오래된 고스트 — 삽입 지점 vs 퇴출 지점
Aout_free재활용 노드; 희생 경로에서 malloc 방지
bufarray사전 할당된 백킹 스토리지; 런타임 할당 없음
num_hashes / aout_buf_htshard 수와 shard별 MHT_TABLE*; FIFO 위에서 O(1) VPID 조회(AOUT_HASH_IDX)
max_count용량 / 활성화 스위치; <= 0이면 Aout 전체 비활성화

BCB들이 이동하는 LRU 리스트 구조체(pgbuf_lru_list)는 7.5/7.7이 의존하는 경계 포인터와 틱 클락을 보관한다:

필드역할사용처
top / bottom리스트 양 끝점add-to-top, add-to-bottom
bottom_1존 1의 마지막 BCBadd-to-middle: 이 뒤에 삽입
bottom_2존 2의 마지막 BCB제거 시 복구 필요(존-2 경계 관리, 7.7)
victim_hintvictim 탐색 시작점제거마다 전진
count_lru1/2/3존별 BCB 수count_lru2/2 = 충분히 오래됨 임계값
threshold_lru1/2존 크기 목표pgbuf_lru_adjust_zone* 구동
tick_listadd-to-top/middle마다 증가boost 나이 클락(PGBUF_IS_BCB_OLD_ENOUGH)
tick_lru3존 3 낙하마다 증가victim-hint 순서
index리스트 IDprivate vs shared 분류
flowchart LR
  subgraph AOUT["pgbuf_aout_list (FIFO + 해시)"]
    T["Aout_top\n(최신)"] --> N1["노드"] --> N2["노드"] --> B["Aout_bottom\n(가장 오래됨)"]
    HT["aout_buf_ht[shard]\nVPID -> 노드"] -.-> N1
    FR["Aout_free\n(재활용)"]
  end
  VICT["희생(victimization)\npgbuf_add_vpid_to_aout_list"] --> T
  B -->|"가득 참 -> 가장 오래된 것 제거"| FR
  REFIX["re-fix\npgbuf_remove_vpid_from_aout_list"] -.->|히트| FR

Figure 7-2 — 고정 용량 FIFO에 해시 인덱스를 붙인 Aout의 구조.

pgbuf_add_vpid_to_aout_list(7.4의 direct-victim 분기와 pgbuf_lru_fall_bcb_to_zone_3에서 호출): Aout_mutex 하에서, Aout_free가 비어 있으면 Aout_bottom을 퇴출(mht_rem)하고, 아니면 빈 노드를 팝한다. lru_idx/vpid를 스탬프로 찍고 mht_put로 등록 후 Aout_top에 연결한다. pgbuf_remove_vpid_from_aout_list: mht_get으로 조회한다. 없으면 PGBUF_AOUT_NOT_FOUND(-2, 진짜 fault) 반환. 있으면 aout_list_id = aout_buf->lru_idx를 캡처하고, unlink → mht_rem → VPID 초기화 → lru_idx 리셋 → 노드를 Aout_free에 push한 뒤 lru_idx를 반환한다 — 7.4가 thread_private_lru_index와 비교하는 값이다.

존 2와 존 3의 boost는 pgbuf_lru_boost_bcb를 통한다:

// pgbuf_lru_boost_bcb -- src/storage/page_buffer.c
assert (zone != PGBUF_LRU_1_ZONE); /* <- 존 1에서는 절대 호출 안 함 */
pthread_mutex_lock (&lru_list->mutex);
pgbuf_remove_from_lru_list (thread_p, bcb, lru_list);/* unlink */
pgbuf_lru_add_bcb_to_top (thread_p, bcb, lru_list); /* 존 1 top에 재연결 */
if (zone == PGBUF_LRU_2_ZONE) pgbuf_lru_adjust_zone1 (thread_p, lru_list, true); /* 존 1만 늘어남 */
else pgbuf_lru_adjust_zones (thread_p, lru_list, true); /* 존 3: 전체 재조정 */
pthread_mutex_unlock (&lru_list->mutex);

pgbuf_lru_add_bcb_to_top은 링크를 이어 붙이고 top(빈 경우 bottom/bottom_1도)을 갱신하며, tick_list를 증가시킨다(이 클락이 다른 모든 BCB를 PGBUF_IS_BCB_OLD_ENOUGH 기준으로 “노화”시킨다). 그런 다음 change_zone으로 PGBUF_LRU_1_ZONE으로 표시한다. pgbuf_lru_add_bcb_to_middlebottom_1(존 1의 마지막) 뒤에 삽입하고 tick_list를 증가시키며 존 2로 표시한다. pgbuf_lru_add_bcb_to_bottombottom에 이어 붙이고 tick_lru3를 스탬프 찍은 뒤 존 3으로 표시한다.

pgbuf_remove_from_lru_list는 역방향 연산으로, BCB를 VOID로 이동하기 전에 모든 경계 포인터를 복구한다:

// pgbuf_remove_from_lru_list -- src/storage/page_buffer.c
if (lru_list->top == bufptr) lru_list->top = bufptr->next_BCB;
if (lru_list->bottom == bufptr) lru_list->bottom = bufptr->prev_BCB;
if (lru_list->bottom_1 == bufptr) lru_list->bottom_1 = bufptr->prev_BCB;
if (lru_list->bottom_2 == bufptr) { /* <- 존-2 경계는 특별 처리 필요 */
if (bufptr->prev_BCB != NULL && pgbuf_bcb_get_zone (bufptr->prev_BCB) == PGBUF_LRU_2_ZONE)
lru_list->bottom_2 = bufptr->prev_BCB;
else { assert (lru_list->count_lru2 == 1); lru_list->bottom_2 = NULL; }
}
/* splice neighbors, null this bcb's links */
pgbuf_lru_advance_victim_hint (thread_p, lru_list, bufptr, bcb_prev, false);
pgbuf_bcb_change_zone (thread_p, bufptr, 0, PGBUF_VOID_ZONE); /* <- 이제 어떤 존에도 속하지 않음 */

불변 조건 — 제거된 BCB의 존은 링크 상태와 일치한다: VOID로 착지하며, victim hint가 먼저 전진한다. 함수 마지막에 change_zone(..., PGBUF_VOID_ZONE)을 호출하므로, 리스트에서 unlink된 BCB는 LRU 존 태그를 유지하지 않는다. 또한 splice 이전에 pgbuf_lru_advance_victim_hint를 먼저 호출한다 — hint를 먼저 전진시키지 않으면 victimizer가 prev_BCB로 이미 해제된 링크를 추적할 수 있다. Boost = remove + add-to-top은 BCB를 잠시 VOID 상태로 만들지만, 전체 시퀀스가 lru_list->mutex 하에서 수행되므로 다른 스레드는 이 간극을 관찰하지 못한다.

pgbuf_lru_fall_bcb_to_zone_3는 반대 방향의 강등 연산으로, 존 1/2가 임계값을 초과할 때 zone-adjust 함수들이 호출한다:

// pgbuf_lru_fall_bcb_to_zone_3 -- src/storage/page_buffer.c
assert (pgbuf_bcb_get_zone (bcb) == PGBUF_LRU_1_ZONE || pgbuf_bcb_get_zone (bcb) == PGBUF_LRU_2_ZONE);
#if defined (SERVER_MODE)
if (pgbuf_is_bcb_victimizable (bcb, false) && pgbuf_is_any_thread_waiting_for_direct_victim ()) {
if (pgbuf_bcb_is_to_vacuum (bcb)) { /* ...stat; fall through... */ }
else if (PGBUF_BCB_TRYLOCK (bcb) == 0) { /* <- 조건부: 리스트/bcb 잠금 순서 데드락 방지 */
VPID vpid_copy = bcb->vpid;
if (pgbuf_is_bcb_victimizable (bcb, true) && pgbuf_assign_direct_victim (thread_p, bcb)) {
pgbuf_remove_from_lru_list (thread_p, bcb, lru_list); PGBUF_BCB_UNLOCK (bcb);
pgbuf_add_vpid_to_aout_list (thread_p, &vpid_copy, lru_list->index); /* <- 퇴장 시 고스트 등록 */
return; }
PGBUF_BCB_UNLOCK (bcb); /* not assigned; fall through */
} }
#endif
bcb->tick_lru3 = lru_list->tick_lru3; /* stamp zone-3 position */
if (++lru_list->tick_lru3 >= DB_INT32_MAX) lru_list->tick_lru3 = 0;
pgbuf_bcb_change_zone (thread_p, bcb, lru_list->index, PGBUF_LRU_3_ZONE);

PGBUF_BCB_TRYLOCK을 조건부로 사용하는 이유: 정상 잠금 순서는 bcb-then-list인데, 여기서는 이미 list mutex를 보유하고 있다. 데드락을 피하기 위해 잠금 획득에 실패하면 포기하고, BCB는 나중에 victimizer에 의해 처리된다(Chapter 9). direct-victim 분기는 VPID를 Aout에 고스트로 등록하고 퇴장한다 — 7.4의 조회와 결합하여 2Q 루프를 완성한다. tick_lru3(바닥에서 작은 값)는 victim hint에 사용되며, boost 나이를 측정하는 tick_lru_list와는 구별된다.

  1. LRU 이동은 is_zero_fcnt && !blocked_reader_writer 조건에서만 수행된다. shared-read unfix는 pgbuf_lockfree_unfix_ro로 빠져나가 리스트를 건드리지 않는다. 대기자가 없는 마지막 unfixer만 BCB를 재정렬한다.
  2. MOVE_TO_LRU_BOTTOM은 dealloc 단축 경로다. 존 분기를 우회하여 해제된 페이지를 즉시 바닥으로 밀어 빠른 재활용을 유도한다.
  3. 존이 기본 동작을 결정한다: 존 1은 절대 boost하지 않는다. 존 2는 PGBUF_IS_BCB_OLD_ENOUGH(count_lru2의 절반 이상 지났을 때)일 때만 boost한다. 존 3은 실제 unfix에서 항상 boost하거나, ignore-unfix 분기에서 direct victim을 넘긴다.
  4. PGBUF_SHOULD_IGNORE_UNFIX는 vacuum-OR-임시볼륨 조건이며, vacuum 전용이 아니다: 임시 볼륨 페이지도 캐시 워밍에서 제외된다. VOID 경로에서는 이를 vacuum 워커 전용인 PGBUF_VACUUM_SHOULD_IGNORE_UNFIX로 좁힌다.
  5. Boost = pgbuf_remove_from_lru_list + pgbuf_lru_add_bcb_to_top — list mutex 하에서 수행된다. add-to-top은 tick_list를 증가시켜 다른 모든 BCB의 “충분히 오래됨” 판단 기준을 갱신한다. 제거된 BCB는 항상 VOID로 착지하여 존 필드가 소속 리스트와 일치하도록 유지한다.
  6. pgbuf_should_move_private_to_shared는 두 가지 트리거로 발동된다: 외부 스레드 unfix(즉시), 또는 같은 리스트에서 hot(64회 포화 fix 이상)이고 충분히 오래된 페이지 — 경합이 많은 페이지를 shared 풀로 올린다.
  7. Aout은 고정 용량의 VPID 전용 고스트 FIFO에 해시 인덱스를 붙인 구조다. 조회-제거는 Aout_mutex 하에 직렬화되며, 배치보다 선행한다 — 정확히 한 스레드만 고스트를 소비하여 이중 승격을 방지한다. 같은 private 리스트에서 re-fix를 발견하면 LRU 1 top으로, 다른 리스트이면 shared로, 발견 실패 시에는 cold 상태로 middle에 착지한다. 희생 시에는 VPID를 다시 고스트로 등록하여 루프가 닫힌다.

Chapter 8: WAL 규칙 하의 플러시와 플러시 데몬

섹션 제목: “Chapter 8: WAL 규칙 하의 플러시와 플러시 데몬”

더티 페이지는 가장 최근 변경에 대한 로그 레코드가 디스크에 기록되기 전까지 디스크에 도달할 수 없다. 이 챕터는 페이지 버퍼가 pgbuf_bcb_flush_with_wal 안에서 로그-우선-페이지(log-before-page) 순서를 어떻게 강제하는지, 세 개의 플러시 데몬이 쓰기를 어떻게 조율하고 배치하는지, 그리고 이중 쓰기 버퍼(DWB)가 어느 시점에 쓰기를 가로채는지를 추적한다. WAL의 이유와 상위 수준 그림은 cubrid-page-buffer-manager.md(“Write-Ahead Logging”, “Flushing and the daemons”)를 참조한다. DWB의 블록 구조와 크래시 복구 근거는 cubrid-double-write-buffer.md에, logpb_flush_log_for_wal의 내구성 시맨틱과 flushed-LSA 장부 관리는 cubrid-log-manager-detail.md에 각각 있으며, 여기서는 재도출하지 않고 참조만 한다. 플러싱/더티 플래그와 atomic-latch FLUSH 모드는 챕터 6과 5에서 다뤘으며, 이 챕터는 플러시 경로가 그것들을 어떻게 소비하는지 보여 준다.

네 개의 공개 진입점이 한 페이지를 디스크 방향으로 밀어 넣는다. 모두 pgbuf_bcb_safe_flush_internal로 모여들며, 이 함수가 플러시를 지금 수행할지, 위임할지, 아니면 대기할지를 결정한다.

flowchart TB
  F["pgbuf_flush\n(선택적으로 unfix 후 반환)"] --> FW["pgbuf_flush_with_wal"]
  FIR["pgbuf_flush_if_requested\n(영구 write-latched 페이지)"] -->|ASYNC_FLUSH_REQ 설정 시| SFFU
  FW --> SFFU["pgbuf_bcb_safe_flush_force_unlock"]
  SFFU --> SFI["pgbuf_bcb_safe_flush_internal"]
  SFI -->|immediate_flush| BFW["pgbuf_bcb_flush_with_wal"]
  SFI -->|다른 스레드가 write-latch 보유| REQ["ASYNC_FLUSH_REQ 설정\n보유 스레드가 unfix 시 플러시"]
  SFI -->|이미 플러싱 중 / latch 충돌, 동기| BLK["pgbuf_block_bcb\nPGBUF_LATCH_FLUSH 대기"]

Figure 8-1 — 단일 페이지 플러시 진입점들이 pgbuf_bcb_safe_flush_internal로 수렴하는 구조.

pgbuf_flush_with_wal은 정식 호출자다. READ 이상의 latch를 호출 스레드가 보유하고 있음을 단언하고, BCB 뮤텍스를 잠근 뒤 동기적으로 위임한다.

// pgbuf_flush_with_wal -- src/storage/page_buffer.c
CAST_PGPTR_TO_BFPTR (bufptr, pgptr);
/* In CUBRID, the caller is holding WRITE page latch */
assert (get_latch (&bufptr->atomic_latch) >= PGBUF_LATCH_READ
&& pgbuf_find_thrd_holder (thread_p, bufptr) != NULL);
PGBUF_BCB_LOCK (bufptr);
if (pgbuf_bcb_safe_flush_force_unlock (thread_p, bufptr, true) != NO_ERROR) /* <- synchronous=true */
{ ASSERT_ERROR (); return NULL; }

pgbuf_flush는 이를 감싸며, free_page == FREE인 경우 이후에 unfix까지 수행한다. 함수 헤더 주석에는 페이지가 디스크에 도달함을 보장하지 않는다고 명시되어 있으므로, 내구성이 필요한 호출자는 pgbuf_flush_with_wal을 사용하고 반환값을 확인해야 한다. pgbuf_flush_if_requested는 페이지를 영구적으로 write-latched 상태로 보유 중인 스레드(일반적인 unfix로 플러시를 트리거할 수 없는 경우)를 위해 존재한다. 이 함수는 WRITE latch가 호출자에게 있음을 단언하고, pgbuf_bcb_is_async_flush_request(bcb)를 확인하여 설정된 경우에만 잠금 후 synchronous=false로 플러시한다 — 이는 데몬/체크포인트가 write-latched 피해자에게 설정하는 PGBUF_BCB_ASYNC_FLUSH_REQ 플래그의 소비 측면이다.

8.2 결정 핵심부: pgbuf_bcb_safe_flush_internal

섹션 제목: “8.2 결정 핵심부: pgbuf_bcb_safe_flush_internal”

호출자는 BCB 뮤텍스를 보유한 채 진입한다. 이 함수는 클린 페이지를 단락(short-circuit)시킨 뒤, 아래 결과들 중 하나를 선택하는 CAS 루프를 실행한다. 즉각적인 플러시가 불가능한 경우는 정확히 두 가지다. 소스에 명확히 서술된 대로, 페이지가 다른 스레드에 의해 write-latched 상태이거나(쓰기 중 내용이 변경될 수 있음), 다른 스레드가 이미 플러시 중이거나(두 쓰기가 이전 버전을 새 버전 위에 덮을 수 있음)이다.

// pgbuf_bcb_safe_flush_internal -- src/storage/page_buffer.c
if (!pgbuf_bcb_is_dirty (bufptr))
return NO_ERROR; /* <- 클린: 할 일 없음, 잠금 유지 */
do {
immediate_flush = false; block = false; is_flushing = false;
impl = get_impl (&bufptr->atomic_latch); impl_new = impl;
is_flushing = pgbuf_bcb_is_flushing (bufptr);
if (!is_flushing
&& (impl.impl.latch_mode == PGBUF_NO_LATCH || impl.impl.latch_mode == PGBUF_LATCH_READ
|| (impl.impl.latch_mode == PGBUF_LATCH_WRITE
&& pgbuf_find_thrd_holder (thread_p, bufptr) != NULL))) /* <- I am the writer */
immediate_flush = true;
else {
assert (is_flushing || impl.impl.latch_mode == PGBUF_LATCH_WRITE); /* <- only these reach else */
if (synchronous)
{ block = true; impl_new.impl.waiter_exists = true; } /* <- publish waiter into latch word */
}
} while (!bufptr->atomic_latch.compare_exchange_strong (impl.raw, impl_new.raw, ...));
결과조건처리
immediate_flush플러시 중이 아님; 무latch, read-latch, 또는 내가 write-latchpgbuf_bcb_flush_with_wal (..., false, locked) — 즉시 플러시
async 요청플러시 중 아님, 다른 스레드가 write-latch 보유pgbuf_bcb_update_flags (..., PGBUF_BCB_ASYNC_FLUSH_REQ, 0) — 보유자가 unfix 시 플러시
block플러싱 중이거나 외부 write-latch이고 synchronous==true*locked=false; pgbuf_block_bcb (..., PGBUF_LATCH_FLUSH, ...) — 슬립
대기 없이 반환외부 latch/플러시이고 synchronous==false플러시 없이 NO_ERROR 반환

async-request 플래그는 즉각 플러시 경로를 취하지 못하고 BCB가 이미 플러싱 중이 아닐 때(즉, 외부 write-latch 상황), synchronous 값에 관계없이 설정된다는 점을 주목하라. 동기 호출자는 이 경우 추가로 block까지 한다.

불변 조건 — BCB당 플러셔는 최대 하나. 페이지는 플러셔가 FLUSHING 플래그를 커밋할 때 pgbuf_bcb_is_flushing이 false인 상태에서만 플러시된다(pgbuf_bcb_flush_with_wal 내부에서 설정). 두 번째 스레드는 is_flushing == true를 보고 immediate_flush를 취하지 못하며, block(PGBUF_LATCH_FLUSH)하거나 반환한다. 이를 위반하면 두 개의 fileio_write 호출이 경쟁하여 이전 이미지가 최신 이미지 위에 착지해 페이지를 손상시킨다. force_unlock/force_lock 래퍼는 내부 함수가 block 시 뮤텍스를 해제할 수 있기 때문에 locked 출력 파라미터를 정규화할 뿐이다.

8.3 pgbuf_bcb_flush_with_wal — 내구성 쓰기

섹션 제목: “8.3 pgbuf_bcb_flush_with_wal — 내구성 쓰기”

챕터의 핵심. 호출자가 뮤텍스를 보유한 채 진입하면, 이 함수는 페이지를 복사하고 WAL을 강제하며, DWB나 직접 쓰기로 기록한 뒤 성공 시 FLUSHING을 해제하고 대기자를 깨운다. 실패 시에는 DIRTY와 oldest_unflush_lsa를 복구한다.

flowchart TB
  A["mark_is_flushing\nFLUSHING 설정, DIRTY 해제"] --> C["copy_unflushed_lsa\nlsa+oldest_unflush 저장\noldest_unflush NULL화, UNLOCK"]
  C --> D{oldest_unflush_lsa\n비-null?}
  D -->|yes| E["logpb_flush_log_for_wal"]
  D -->|no| F["디버그: 변경 미로깅"]
  E --> G{uses_dwb?}
  F --> G
  G -->|yes| H["dwb_add_page"]
  G -->|no| I["fileio_write"]
  H --> J{오류?}
  I --> J
  J -->|실패| K["mark_was_not_flushed\nDIRTY+lsa 복구, 대기자 깨움, ER_FAILED"]
  J -->|ok, 플러시 스레드 + 대기자| L["flushed_bcbs에 큐잉\npost-flush 데몬 깨움"]
  J -->|ok| M["mark_was_flushed\nFLUSHING 해제, 대기자 깨움"]

Figure 8-2 — pgbuf_bcb_flush_with_wal 분기 맵.

Step 1 — 플러시 선점, DIRTY 해제. was_dirty = pgbuf_bcb_mark_is_flushing (...)FLUSHING_TO_DISK를 설정하고, DIRTYASYNC_FLUSH_REQ를 원자적으로 해제한다.

// pgbuf_bcb_mark_is_flushing -- src/storage/page_buffer.c
if (pgbuf_bcb_is_dirty (bcb)) {
pgbuf_bcb_update_flags (thread_p, bcb, PGBUF_BCB_FLUSHING_TO_DISK_FLAG,
PGBUF_BCB_DIRTY_FLAG | PGBUF_BCB_ASYNC_FLUSH_REQ); /* <- set | clear */
return true;
}

불변 조건 — DIRTY는 플러시 시작 시 해제, 완료 시가 아님. 복사-쓰기 구간 중 동시 re-dirty가 발생하면 DIRTY가 다시 설정되며 유실되지 않는다. 해당 페이지는 그저 다시 플러시된다. 쓰기 실패 시, pgbuf_bcb_mark_was_not_flushed가 (was_dirty일 때) DIRTY를 복구한다.

Step 2 — 이미지 복사. start_copy_page 시점에 페이지가 스택의 iopage로 복사된다(TDE 암호화된 경우 tde_encrypt_data_page, 그렇지 않으면 IO_PAGESIZEmemcpy). uses_dwb인 경우, 복사본은 dwb_set_data_on_next_slot에 의해 DWB 슬롯에 스테이징된다. 슬롯이 승인되면 로컬 iopage는 null이 되고 제어 흐름은 copy_unflushed_lsa로 점프한다.

Step 3 — WAL 강제. copy_unflushed_lsa 시점에 페이지 LSA와 oldest_unflush_lsa를 저장하고, bufptr->oldest_unflush_lsaNULL화한 뒤 뮤텍스를 해제한다. 저장된 oldest_unflush_lsa가 non-null이면 로그를 강제 플러시한다.

// pgbuf_bcb_flush_with_wal -- src/storage/page_buffer.c
LSA_COPY (&lsa, &(bufptr->iopage_buffer->iopage.prv.lsa));
LSA_COPY (&oldest_unflush_lsa, &bufptr->oldest_unflush_lsa);
LSA_SET_NULL (&bufptr->oldest_unflush_lsa);
PGBUF_BCB_UNLOCK (bufptr); *is_bcb_locked = false;
if (!LSA_ISNULL (&oldest_unflush_lsa))
logpb_flush_log_for_wal (thread_p, &lsa); /* <- log-before-page: force log up to page LSA */

WAL 불변 조건 — 쓰기 전에 페이지 LSA까지의 로그가 내구성을 가져야 한다. 페이지는 로그 테일이 lsa(페이지 자체의 prv.lsa)까지 강제 플러시되기 전에는 fileio_write/dwb_add_page에 전달되지 않는다. 강제는 구조적으로 보장된다. logpb_flush_log_for_wal (thread_p, &lsa)가 뮤텍스 해제와 쓰기 사이에 위치하며, lsa는 뮤텍스 해제 이전에 읽히기 때문에 이후 재도장(re-stamp)될 수 없다. 트리거 조건은 oldest_unflush_lsa != NULL이지만, 강제 대상은 oldest_unflush_lsa가 아닌 lsa(가장 새로운 변경) 쪽이다. 생략 시 발생하는 문제: 페이지 쓰기 후 로그 쓰기 전에 크래시가 발생하면, 이미 영속된 변경의 redo/undo가 디스크에 없어 복구가 재구성하거나 롤백하지 못하며 페이지는 조용히 손상된다. logpb_flush_log_for_wal이 특정 LSA까지 내구성을 어떻게 보장하는지는 cubrid-log-manager-detail.md를 참조하라. else 분기(null oldest_unflush_lsa)는 “변경됐지만 로깅되지 않은” 드문 경우(임시 볼륨)이며, 디버그 노트만 출력한다.

bufptr->oldest_unflush_lsa를 null화하면, 쓰기 구간 중 re-dirty가 새 값을 재도장할 수 있고 이후 플러시가 다시 강제 플러시한다. 쓰기 실패 시 저장된 값이 복구된다(Step 5a).

Step 4 — 쓰기. DWB 사용 여부는 uses_dwb = dwb_is_created () && !is_temp(임시 볼륨은 항상 우회)로 결정된다. uses_dwb이면 dwb_add_page가 페이지의 VPID를 스테이징된 슬롯에 등록한다. DWB는 페이지를 배치하고 전체 블록을 이중 쓰기 영역에, 그 후 데이터 파일에 플러시한다(블록 구조와 단편 쓰기 복구 논거는 cubrid-double-write-buffer.md). 미묘한 분기: 스테이징과 추가 사이에 DWB가 비활성화된 경우, dwb_add_pagedwb_slot == NULL을 반환하므로 코드가 uses_dwb를 해제하고 재잠금 후 goto start_copy_page로 직접 쓰기를 재시도한다. 직접 경로는 DWB가 전역으로 존재할 때 FILEIO_WRITE_NO_COMPENSATE_WRITE 모드로 단순한 fileio_write를 수행한다(이중 쓰기로 단편 쓰기 보상이 불필요해짐). 그렇지 않으면 FILEIO_WRITE_DEFAULT_WRITE.

Step 5a — 쓰기 실패. 재잠금, pgbuf_bcb_mark_was_not_flushed (.., was_dirty)가 FLUSHING을 해제하고 DIRTY를 복구하며, 저장된 oldest_unflush_lsa를 복구하고, PGBUF_LATCH_FLUSH 대기자를 깨운다(next_wait_thrd != NULL인 경우에만), ER_FAILED를 반환한다.

Step 5b — 성공, 데몬 핸드오프. 현재 스레드가 페이지 플러시 데몬(is_page_flush_thread)이고, post-flush 데몬이 존재하며, 직접 피해자를 기다리는 스레드가 있고 BCB가 pgbuf_Pool.flushed_bcbs에 수락(produce)되면, BCB는 잠금 해제 상태이지만 클리어되지 않은 채로 남겨 post-flush 데몬이 피해자로 할당하도록 하며(챕터 9), 데몬이 깨워진다 — 이 경로에서 mark_was_flushed는 의도적으로 호출되지 않는다. Step 5c(그 외의 경우)에서는 재잠금 후 pgbuf_bcb_mark_was_flushed를 호출하여 FLUSHING을 해제하고, 대기자가 있으면 flush 대기자를 깨운다.

8.4 FLUSH 대기자 깨우기: pgbuf_wake_flush_waiters

섹션 제목: “8.4 FLUSH 대기자 깨우기: pgbuf_wake_flush_waiters”

8.2의 block 분기를 택한 스레드들은 request_latch_mode == PGBUF_LATCH_FLUSH인 채로 BCB의 next_wait_thrd 리스트에 파킹된다. 깨우는 함수는 FLUSH 대기자만 연결 해제하고, READ/WRITE latch 대기자는 그대로 둔다.

// pgbuf_wake_flush_waiters -- src/storage/page_buffer.c
for (crt_waiter = bcb->next_wait_thrd; crt_waiter != NULL; crt_waiter = save_next_waiter) {
save_next_waiter = crt_waiter->next_wait_thrd;
if (crt_waiter->request_latch_mode == PGBUF_LATCH_FLUSH) {
if (prev_waiter != NULL) prev_waiter->next_wait_thrd = save_next_waiter;
else bcb->next_wait_thrd = save_next_waiter; /* <- unlink only FLUSH waiters */
crt_waiter->next_wait_thrd = NULL;
pgbuf_wakeup_uncond (crt_waiter);
} else {
prev_waiter = crt_waiter; /* <- keep latch waiters threaded */
}
}

호출자는 BCB 뮤텍스를 보유해야 한다. 8.3의 실패 경로와 성공 경로 모두 next_wait_thrd != NULL일 때 이를 호출한다. FLUSH 대기자와 latch 대기자가 하나의 리스트에 혼재하기 때문에, 루프가 절단 대신 prev_waiter를 추적한다.

8.5 페이지 플러시 데몬: 후보 수집과 플러시

섹션 제목: “8.5 페이지 플러시 데몬: 후보 수집과 플러시”

pgbuf_flush_victim_candidates가 데몬 본체다. 스캔 범위를 결정하고, LRU에서 더티 후보를 수집하며, 로그를 강제하고, 생존한 각 후보를 플러시한다.

적응형 스캔 너비. lru_victim_req_cntfix_req_cnt를 읽고 초기화하여 lru_miss_rate를 구한 뒤, 미스가 높을 때 flush_ratio * num_buffers를 최대 PGBUF_FLUSH_VICTIM_BOOST_MULT(=10)배까지 높인다. 단, 체크포인트 중에는 부스트하지 않는다(체크포인트가 이미 플러시하고 있으므로 이중 플러시를 방지). 결과값은 약 200 MB 분량의 페이지로 상한이 설정된다.

// pgbuf_flush_victim_candidates -- src/storage/page_buffer.c
if (pgbuf_Pool.is_checkpoint == false) {
lru_dynamic_flush_adj = MAX (1.0f, 1 + (PGBUF_FLUSH_VICTIM_BOOST_MULT - 1) * lru_miss_rate);
lru_dynamic_flush_adj = MIN (PGBUF_FLUSH_VICTIM_BOOST_MULT, lru_dynamic_flush_adj);
} else lru_dynamic_flush_adj = 1.0f;
check_count_lru = (int) (cfg_check_cnt * lru_dynamic_flush_adj);
check_count_lru = MIN (check_count_lru, (200 * 1024 * 1024) / DB_PAGESIZE);

수집 후 분기. victim_count == 0이면 플러시할 것이 없다. 스캔이 실제로 시도됐을 때(check_count_lru > 0 && lru_sum_flush_priority > 0)만 *stop을 true로 설정하고(8.8의 데몬 루프가 이 시점에 중단됨) goto end로 이동한다. 그렇지 않으면 로그 플러시 데몬을 깨우고(WAL에 현재 로그가 필요 — log_wakeup_log_flush_daemon, 데몬이 없으면 logpb_force_flush_pages), PRM_ID_PB_SEQUENTIAL_VICTIM_FLUSH 하에서는 VPID 순으로 목록을 qsort하고, is_flushing_victims = true를 설정한다.

후보별 루프. 각 후보마다 BCB를 잠그고 네 가지 가드를 적용한다.

  1. VPID 변경됨 / 더티 아님 / 이미 플러시 중 -> num_skipped_already_flushed, 잠금 해제 후 계속.
  2. LRU 피해자 구역을 벗어났거나 fixed/hot 상태 -> num_skipped_fixed_or_hot, 잠금 해제 후 계속.
  3. logpb_need_wal (페이지 LSA가 플러시된 로그를 초과) -> 최대 lsa_need_wal 기록, count_need_wal 증가, 로그 플러시 데몬 깨움, num_skipped_need_wal, 잠금 해제 후 계속.
  4. 그 외 플러시: PGBUF_NEIGHBOR_PAGES > 1이면 pgbuf_flush_page_and_neighbors_fb, 그렇지 않으면 pgbuf_bcb_flush_with_wal (..., true, &is_bcb_locked) (is_page_flush_thread=true; 루프가 BCB 잠금이 유지된 경우 해제함). 오류 -> goto end.

repeat 재시도. end에서, 모든 후보가 순전히 WAL 때문에 스킵됐고(count_need_wal == victim_count) 직접 피해자를 기다리는 스레드가 있으면, 데몬이 직접 로그를 강제(logpb_flush_log_for_wal)하고 정확히 한 번 repeat으로 점프한다(두 번째 패스에서 LSA가 진전됐음을 단언). 이후 is_flushing_victims를 해제한다.

이웃 배치: pgbuf_flush_page_and_neighbors_fb. PGBUF_NEIGHBOR_PAGES > 1이면 분기 4가 이 함수를 호출한다. 이 함수는 앵커 주변에 연속 VPID 윈도를 확장하여 물리적으로 인접한 페이지들을 한 번의 순차 스윕으로 기록한다. 윈도 상태는 정적 파일 스코프 전역, pgbuf_Flush_helper(타입 pgbuf_batch_flush_helper)에 있다 — 호출별 스택 객체가 아니다. 전용 페이지 플러시 데몬만이 유일한 호출자이고 각 호출이 진입 시 카운터를 초기화하므로, 단일 공유 인스턴스는 안전하다.

// pgbuf_batch_flush_helper -- src/storage/page_buffer.c
struct pgbuf_batch_flush_helper
{
int npages; /* <- pages currently staged in the window */
int fwd_offset; /* <- pages added forward (higher pageid) of anchor */
int back_offset; /* <- pages added backward (lower pageid) of anchor */
PGBUF_BCB *pages_bufptr[2 * PGBUF_MAX_NEIGHBOR_PAGES - 1]; /* <- window BCBs */
VPID vpids[2 * PGBUF_MAX_NEIGHBOR_PAGES - 1]; /* <- their VPIDs */
};
// static PGBUF_BATCH_FLUSH_HELPER pgbuf_Flush_helper; <- the single shared instance
필드역할존재 이유
npages윈도에 스테이징된 페이지 수윈도별 플러시 루프의 끝 경계; 꼬리/선두 이웃이 클린이면 축소
fwd_offset앵커로부터 전방(높은 pageid) 도달 범위다음 전방 후보는 anchor + fwd_offset + 1
back_offset앵커로부터 후방(낮은 pageid) 도달 범위다음 후방 후보는 anchor - back_offset - 1
pages_bufptr[2*MAX-1]윈도 구성원별 BCB 핸들플러시할 BCBs; 앵커 기준 양방향 PGBUF_MAX_NEIGHBOR_PAGES-1(=31)에 도달하도록 크기 결정
vpids[2*MAX-1]구성원별 VPID 스냅샷, pages_bufptr[]와 병렬검증 키: pgbuf_flush_neighbor_safe가 재확인하여 쓰기 전 VPID가 변경된 구성원을 스킵

PGBUF_NEIGHBOR_POS (off)는 앵커 기준으로 배열을 인덱싱한다(PGBUF_NEIGHBOR_PAGES - 1 + off). 윈도는 더티 페이지만으로 구성되지 않는다. PGBUF_NEIGHBOR_FLUSH_NONDIRTY가 활성화된 경우, 프로브는 내부의 클린 페이지도 의도적으로 포함하여 디스크 상의 연속 구간을 유지한다. 단, 두 개의 연속 non-dirty(NEIGHBOR_ABORT_TWO_CONSECTIVE_NONDIRTIES)가 발생하거나, non-dirty가 작은 임계값을 초과한 뒤 윈도의 절반을 넘으면(NEIGHBOR_ABORT_TOO_MANY_NONDIRTIES) 배치를 포기한다. 맨 꼬리나 선두의 클린 페이지는 이후 트리밍(오프셋과 npages 감소)되어 불필요한 쓰기로 구간이 끝나지 않도록 한다. 스윕 전에 이웃 경로는 전체 윈도 범위 WAL을 한 번 강제한다.

// pgbuf_flush_page_and_neighbors_fb -- src/storage/page_buffer.c
/* WAL protocol: force log record to disk */
logpb_flush_log_for_wal (thread_p, &log_newest_oldest_unflush_lsa);
for (pos = PGBUF_NEIGHBOR_POS (-helper->back_offset); pos <= PGBUF_NEIGHBOR_POS (helper->fwd_offset); pos++)
error = pgbuf_flush_neighbor_safe (thread_p, helper->pages_bufptr[pos], &helper->vpids[pos], &was_page_flushed);

pgbuf_flush_neighbor_safe는 각 구성원을 단일 페이지 경로로 재라우팅하여(VPID 재검증) 페이지별 WAL이 유지되도록 한다. 배치 강제는 단지 연속 쓰기 시작 전에 로그가 최신임을 보장할 뿐이다. 단일 페이지 윈도(npages <= 1)는 배치 강제를 스킵하고 해당 페이지를 직접 플러시한다.

8.5에서 호출되는 이 함수는 각 LRU의 아래쪽에서 피해자 구역을 따라 걸으며, 각 리스트의 lru_victim_flush_priority_per_lru로 예산을 배분한다.

// pgbuf_get_victim_candidates_from_lru -- src/storage/page_buffer.c
for (bufptr = pgbuf_Pool.buf_LRU_list[lru_idx].bottom;
bufptr != NULL && PGBUF_IS_BCB_IN_LRU_VICTIM_ZONE (bufptr) && i > 0;
bufptr = bufptr->prev_BCB, i--) {
if (pgbuf_bcb_is_dirty (bufptr)) {
pgbuf_Pool.victim_cand_list[victim_cand_count].bufptr = bufptr;
pgbuf_Pool.victim_cand_list[victim_cand_count].vpid = bufptr->vpid;
victim_cand_count++; /* <- dirty -> flush before victimization */
}
#if defined (SERVER_MODE)
else if (try_direct_assign && pgbuf_is_any_thread_waiting_for_direct_victim ()
&& pgbuf_is_bcb_victimizable (bufptr, false) && PGBUF_BCB_TRYLOCK (bufptr) == 0) {
if (pgbuf_is_bcb_victimizable (bufptr, true) && pgbuf_assign_direct_victim (thread_p, bufptr)) {
try_direct_assign = false; *assigned_directly = true; /* <- clean bcb handed to a waiter */
}
PGBUF_BCB_UNLOCK (bufptr);
}
#endif
}

두 가지 출력이 있다. 더티 BCB는 후보 리스트로 가고(피해자화 전에 플러시가 필요), 단일 클린 victimizable BCB는 trylock 하에서 굶주린 대기자에게 직접 전달될 수 있다(assigned_directly). 스캔이 절대 블록하지 않도록. 후보 VPID는 스냅샷되므로 8.5의 플러시 루프가 재할당된 BCB를 탐지할 수 있다(거기서의 가드 1). 전체 탐색은 LRU별 mutex 하에서 실행된다.

8.7 seq-flusher와 pgbuf_flush_seq_list 페이싱

섹션 제목: “8.7 seq-flusher와 pgbuf_flush_seq_list 페이싱”

체크포인트 플러싱은 PGBUF_SEQ_FLUSHER로 속도 제어된다. 피해자 데몬과 달리, 체크포인트 I/O가 포그라운드를 굶기지 않도록 쓰기를 1초 “슈퍼 인터벌” 단위로 분산한다.

struct pgbuf_seq_flusher — 모든 필드:

필드역할존재 이유
flush_listPGBUF_VICTIM_CANDIDATE_LIST(bufptr+vpid) 배열이번 패스의 작업 집합
flush_upto_lsa나열된 모든 페이지의 oldest-LSA 최대값WAL 게이트; 이를 초과한 페이지는 스킵
control_intervals_cnt현재 1초 슈퍼 인터벌에서 경과한 인터벌 수flush_per_interval 계산에 투입
control_flushed슈퍼 인터벌 내 지금까지 플러시된 페이지 수느린 인터벌을 다음 인터벌에서 보상 가능하게 함
interval_msec하나의 페이싱 인터벌 지속 시간pgbuf_flush_chkpt_seq_list에서 1000 * PGBUF_CHKPT_BURST_PAGES / chkpt_flush_rate로 계산, chkpt_flush_rate = 1000 / PRM_ID_LOG_CHECKPOINT_SLEEP_MSECS — 구조체의 flush_rate 필드에서 오지 않음
flush_max_sizeflush_list의 용량, 초기화 시 설정배치 크기 상한; 가득 차면 체크포인트가 재충전
flush_cnt현재 유효 원소 수플러시 루프의 끝 경계
flush_idx다음으로 플러시할 원소의 인덱스인터벌 경계를 넘어 재개
flushed_pages이번 호출에서 플러시된 페이지 수 (반환 파라미터)호출자가 누적
flush_rate최대 페이지/초 (음수 = 무제한)페이싱 수학이 수렴하는 목표; 매 인터벌마다 chkpt_flush_rate로 설정
burst_mode즉각 청크 플러시 vs. 한 페이지 후 슬립burst는 데이터 I/O를 순차적으로 유지

pgbuf_initialize_seq_flusher는 구조체를 초기화하고, flush_max_size를 설정하며, flush_list를 할당하고, burst_mode = true를 기본값으로 한다. pgbuf_flush_seq_list는 제어 카운터에서 flush_per_interval을 유도한다. control_intervals_cnt > 0이면 flush_rate * (control_intervals_cnt+1) / control_total_cnt_intervals에서 이미 플러시된 수(보상)를 빼고, PGBUF_CHKPT_MIN_FLUSH_RATE(=50) 스케일 하한을 적용한다. 루프는 flush_idx < flush_cnt && flushed_pages < flush_per_interval 동안 실행된다.

// pgbuf_flush_seq_list -- src/storage/page_buffer.c
PGBUF_BCB_LOCK (bufptr); locked_bcb = true;
if (!VPID_EQ (&bufptr->vpid, &f_list[seq_flusher->flush_idx].vpid) || !pgbuf_bcb_is_dirty (bufptr)
|| (flush_if_already_flushed == false && !LSA_ISNULL (&bufptr->oldest_unflush_lsa)
&& LSA_GT (&bufptr->oldest_unflush_lsa, &seq_flusher->flush_upto_lsa)))
{ PGBUF_BCB_UNLOCK (bufptr); dropped_pages++; continue; } /* <- stale / beyond chkpt horizon */
if (pgbuf_bcb_safe_flush_force_lock (thread_p, bufptr, true) == NO_ERROR) { /* ... done_flush = true */ }

flush_if_already_flushed 휴리스틱은 이미 플러시된 페이지를 재플러시하는 경우를 VPID가 다음 리스트 항목과 연속일 때로만 제한한다 — 불필요한 쓰기 방지보다 쓰기 순차성을 우선한다. 각 페이지 후 비-burst 모드는 time_remaining / pages_remaining ms를 슬립하여 쓰기를 분산한다(1000 / PGBUF_CHKPT_MAX_FLUSH_RATE 미만이면 스킵). burst 모드는 절대적인 limit_time만 확인하고 초과 시 (*time_rem = -1로) 중단한다. flush_upto_lsa는 WAL 게이트다. oldest-unflush LSA가 이 값 이하인 페이지만 해당 체크포인트에서 플러시된다.

8.8 세 개의 데몬과 체크포인트 플러시

섹션 제목: “8.8 세 개의 데몬과 체크포인트 플러시”

SERVER_MODE에서 REGISTER_DAEMON으로 세 개의 데몬이 등록된다.

데몬태스크 타입루퍼본체
pgbuf_Page_flush_daemon전용 pgbuf_page_flush_daemon_task (cubthread::entry_task 서브클래스)pgbuf_get_page_flush_interval (PRM_ID_PAGE_BG_FLUSH_INTERVAL_MSECS > 0이면 시간 제한, 아니면 무한 대기)pgbuf_flush_victim_candidates 루프
pgbuf_Page_post_flush_daemonentry_callable_task (pgbuf_page_post_flush_execute)3단계 루퍼 {1,10,100} mspgbuf_assign_flushed_pagesflushed_bcbs 드레인, 직접 피해자 할당
pgbuf_Page_maintenance_daemonentry_callable_task (pgbuf_page_maintenance_execute)고정 100 mspgbuf_adjust_quotas + pgbuf_direct_victims_maintenance

플러시 데몬은 명시적으로 깨워진 경우(was_woken_up) 최소 한 번 실행한 뒤, pgbuf_keep_victim_flush_thread_running이 true인 동안 또는 pgbuf_flush_victim_candidatesstop_iteration을 설정할 때까지 루프를 돈다.

// pgbuf_page_flush_daemon_task::execute -- src/storage/page_buffer.c
bool force_one_run = pgbuf_Page_flush_daemon->was_woken_up ();
while (force_one_run || pgbuf_keep_victim_flush_thread_running ()) {
pgbuf_flush_victim_candidates (&thread_ref, prm_get_float_value (PRM_ID_PB_BUFFER_FLUSH_RATIO),
&m_perf_track, &stop_iteration);
force_one_run = false;
if (stop_iteration) break;
}

이는 클래스 기반 전용 태스크 유일의 구현체다. post-flush와 maintenance는 entry_callable_task 안의 호출 가능 함수다. 포그라운드 스레드는 피해자를 찾지 못하면 pgbuf_wakeup_page_flush_daemon으로 데몬을 깨운다. pgbuf_flush_control_from_dirty_ratio는 별도의 적응형 신호를 추가한다 — dirties_cntnum_buffers/2를 초과할수록 이차적으로 증가하는 속도 상승값과 더티 증가율을 합산하여, 풀이 포화되기 전에 더 공격적으로 플러시하도록 한다.

체크포인트 플러시. pgbuf_flush_checkpoint는 로그를 flush_upto_lsa까지 강제하고, is_checkpoint=true를 설정한 뒤 모든 BCB를 스캔한다. oldest_unflush_lsa <= flush_upto_lsa인 더티 비임시 페이지는 공유 seq_chkpt_flusher.flush_list에 추가된다. 리스트가 가득 차면(>= flush_max_size) VPID로 qsort한 뒤 pgbuf_flush_chkpt_seq_list(이는 페이싱된 pgbuf_flush_seq_list를 호출)로 드레인하고 재충전한다. prev_chkpt_redo_lsa보다 오래된 페이지는 단언(ER_LOG_CHECKPOINT_SKIP_INVALID_PAGE) — 이전 체크포인트에서 이미 플러시됐어야 한다. 스킵된 페이지들의 가장 작은 미플러시 LSA가 smallest_lsa로 반환되어 redo 수평선을 진전시킨다. flush_all 패밀리(pgbuf_flush_all, _all_unfixed, _all_unfixed_and_set_lsa_as_null)는 pgbuf_flush_all_helper로 모든 BCB를 페이싱 없이 스윕하며, 로그/복구 매니저만 사용한다.

  1. 모든 단일 페이지 플러시는 pgbuf_bcb_safe_flush_internal로 모이며, 이 함수의 atomic latch CAS 루프가 즉각 플러시, unfix 시 async 요청, 또는 PGBUF_LATCH_FLUSH block 중 하나를 선택한다.
  2. pgbuf_bcb_flush_with_wal은 뮤텍스 하에서 페이지 LSA를 저장하고 oldest_unflush_lsa를 NULL화한 뒤 뮤텍스를 해제하고, fileio_write/dwb_add_page 이전에 logpb_flush_log_for_wal (.., &lsa)를 호출함으로써 WAL 불변 조건을 강제한다. 강제를 생략하면 이미 영속된 페이지의 redo가 소실된다.
  3. DIRTY는 플러시 시작 시(pgbuf_bcb_mark_is_flushing) 해제되므로 동시 re-dirty는 절대 유실되지 않는다. 실패한 쓰기는 pgbuf_bcb_mark_was_not_flushed로 DIRTY와 저장된 oldest_unflush_lsa를 복구한다.
  4. BCB당 최대 하나의 플러셔: FLUSHING_TO_DISK 플래그와 FLUSH-latch 블로킹 경로가 쓰기를 직렬화하여 이전 이미지가 최신 이미지 위에 덮이는 상황을 방지한다.
  5. 페이지 플러시 데몬(pgbuf_flush_victim_candidates + pgbuf_get_victim_candidates_from_lru)은 LRU 하단의 더티 후보를 수집하고, fixed/hot/WAL 필요 페이지를 스킵하며, 공유 전역 pgbuf_Flush_helper 윈도로 이웃을 배치 플러시할 수 있다(전체 구간 WAL을 한 번 강제하며, 순차성을 위해 내부 클린 페이지도 포함 가능). 모든 후보가 WAL 차단된 경우 한 번 재시도한다.
  6. 체크포인트는 속도 제어되는 PGBUF_SEQ_FLUSHER(pgbuf_flush_seq_list)를 사용하며 burst/spread 페이싱과 flush_upto_lsa WAL 게이트를 갖는다. flush_all*은 페이싱 없는 스윕이다.
  7. 세 개의 데몬이 존재한다 — 하나의 전용 클래스(페이지 플러시)와 두 개의 callable 태스크(post-flush, maintenance). DWB가 생성된 경우, 모든 비임시 쓰기를 데이터 파일 이전에 블록으로 스테이징한다(cubrid-double-write-buffer.md 참조).

Chapter 9: 피해자 선택 — LFCQ와 직접 피해자 전달

섹션 제목: “Chapter 9: 피해자 선택 — LFCQ와 직접 피해자 전달”

이 챕터가 답하는 질문은 하나다. 무효(free) 리스트가 비었을 때, 스레드는 어떻게 퇴출 가능한 BCB를 찾는가? 그리고 하나도 없을 때, 해제된 BCB는 어떻게 대기 중인 스레드에게 곧바로 전달되는가? 상위 개요 문서는 “LFCQ — 피해자 선택”과 “직접 피해자 전달”을 높은 수준에서 다뤘다. 여기서는 모든 분기를 추적한다.

두 경로는 서로 대칭이다. pull 경로(pgbuf_get_victim)는 계층형 lock-free 큐(LFCQ)를 스캔해 깨끗한 BCB를 확보한다. push 경로(pgbuf_assign_direct_victim / pgbuf_get_direct_victim)는 그 역방향이다. 이미 BCB를 정리한 생산자(producer)가 대기 스레드를 깨우고, BCB를 해당 스레드의 메일박스 슬롯에 써 넣은 뒤 LRU를 건너뛴다. pull 경로에서 실패한 스레드는 대기자(waiter)가 되어 suspend/wake 처리를 받는다(5챕터 참조).

9.1 두 구조체: 메일박스와 후보 슬롯

섹션 제목: “9.1 두 구조체: 메일박스와 후보 슬롯”

pgbuf_direct_victim은 전역 메일박스-큐 레코드(pgbuf_Pool.direct_victims)로, SERVER_MODE에서만 사용된다. pgbuf_victim_candidate_list는 플러시 데몬(8챕터)이 채우는 임시 배열이다. 명세서에서 이름이 언급되므로 범위에 포함한다.

// pgbuf_direct_victim -- src/storage/page_buffer.c
struct pgbuf_direct_victim {
PGBUF_BCB **bcb_victims; /* per-thread mailbox: bcb_victims[tid] = BCB handed to thread tid */
lockfree::circular_queue<THREAD_ENTRY *> *waiter_threads_high_priority;
lockfree::circular_queue<THREAD_ENTRY *> *waiter_threads_low_priority;
};
// pgbuf_victim_candidate_list -- src/storage/page_buffer.c
struct pgbuf_victim_candidate_list {
PGBUF_BCB *bufptr; /* selected BCB as victim candidate */
VPID vpid; /* page id of the page managed by the BCB */
};
구조체.필드역할존재 이유
direct_victim.bcb_victimsthread_p->index 기준 num_total_threads 크기의 BCB 포인터 배열. 슬롯 [tid]는 tid의 BCB 또는 NULL.메일박스. 생산자는 대기자의 entry lock 하에 슬롯을 쓰고, 대기자는 깨어난 뒤 자신의 슬롯을 읽고 NULL로 지운다 — 스레드당 슬롯 하나, 경합 없음.
direct_victim.waiter_threads_high_priority시스템을 블로킹하고 있는 스레드들의 LFCQ.먼저 소진 — 레이턴시가 중요한 수정 스레드가 큐를 앞선다.
direct_victim.waiter_threads_low_priority대기를 허용하는 스레드들의 LFCQ.고우선순위 앞에서 4번에 1번 소진 — 75/25 가중치(§9.5).
victim_candidate_list.bufptr플러시 패스가 정리 대상으로 선택한 BCB.두 번째 패스에서 재스캔 없이 재잠금+플러시 가능.
victim_candidate_list.vpid선택 시점의 bufptr->vpid 스냅샷.플러시 전 재배정 감지용. vpid가 더 이상 일치하지 않는 bufptr는 건너뜀.

9.2 pgbuf_get_victim — 단계별 LFCQ 스캔

섹션 제목: “9.2 pgbuf_get_victim — 단계별 LFCQ 스캔”

큐는 리스트 인덱스를 보관하며, BCB는 직접 저장하지 않는다. 인덱스는 해당 리스트의 count_vict_cand > 0이고 PGBUF_LRU_VICTIM_LFCQ_FLAG가 설정된 경우에만 큐에 들어간다. 함수는 네 단계를 순서대로 순회하며, 잠금을 획득한 첫 번째 BCB를 반환한다.

1단계 — 자신의 private 리스트, 쿼터를 초과한 private LRU가 있을 때만:

// pgbuf_get_victim -- src/storage/page_buffer.c
if (PGBUF_THREAD_HAS_PRIVATE_LRU (thread_p)) {
private_lru_idx = PGBUF_LRU_INDEX_FROM_PRIVATE (PGBUF_PRIVATE_LRU_FROM_THREAD (thread_p));
lru_list = PGBUF_GET_LRU_LIST (private_lru_idx);
if (PGBUF_LRU_LIST_IS_ONE_TWO_OVER_QUOTA (lru_list) /* zone1+2 exceeds quota */
|| (PGBUF_LRU_LIST_IS_OVER_QUOTA (lru_list) && lru_list->count_vict_cand > 0)) {
victim = pgbuf_get_victim_from_lru_list (thread_p, private_lru_idx);
if (victim != NULL) { return victim; } /* <- happy path */
if (!PGBUF_VACUUM_SHOULD_IGNORE_UNFIX (thread_p))
restrict_other = PGBUF_LRU_LIST_IS_OVER_QUOTA_WITH_BUFFER (lru_list); /* gate stage 2 */
searched_own = true;
} }

restrict_other는 vacuum이 아닌 스레드가 쿼터를 여유 있게 초과한 경우(quota + MAX(10, quota*0.01) 버퍼)에만 설정되어, 2단계를 큰 private 리스트로 제한한다. searched_own은 4단계가 1단계를 반복하지 않도록 막는다.

2단계 — 다른 private 리스트, PGBUF_PAGE_QUOTA_IS_ENABLED && has_flush_thread 조건을 만족할 때만 진입한다. pgbuf_lfcq_get_victim_from_private_lru (thread_p, restrict_other)(§9.4)를 호출해 첫 번째 획득에서 반환한다.

3단계 — shared, 감시 루프 내 — 유일하게 반복하는 단계이며, 플러시 데몬이 없을 때만 루프가 돈다:

do {
victim = pgbuf_lfcq_get_victim_from_shared_lru (thread_p, has_flush_thread);
if (victim != NULL) { return victim; } /* <- happy path */
current_consume_cursor = pgbuf_Pool.shared_lrus_with_victims->get_consumer_cursor ();
}
while (!has_flush_thread && !pgbuf_Pool.shared_lrus_with_victims->is_empty ()
&& ((int) (current_consume_cursor - initial_consume_cursor) <= pgbuf_Pool.num_LRU_list)
&& (++nloops <= pgbuf_Pool.num_LRU_list));

while 조건 네 가지가 각각 스핀을 멈춘다. 플러시 데몬 존재, 큐 비어 있음, 소비된 인덱스 수가 shared 리스트 수를 초과, 또는 nloopsnum_LRU_list를 초과(안전 장치). 플러시 데몬이 있으면 본문은 정확히 한 번만 실행된다.

4단계 — 최후 수단인 자신의 private 리스트, 쿼터 무시. 1~3단계가 모두 실패했고 1단계가 한 번도 실행되지 않은 경우(PGBUF_THREAD_HAS_PRIVATE_LRU && !searched_own)에만, pgbuf_get_victim_from_lru_list를 자신의 리스트에 다시 호출해 결과를 반환한다. 그렇지 않으면 return victim(NULL)으로 빠진다. 이는 소스에 문서화된 교착상태 — 모든 private 리스트가 쿼터 바로 아래에 있고, shared 리스트에는 zone-3이 없으며, 퇴출 또는 플러시 대상이 없는 상황 — 를 막기 위한 것이다. NULL 반환은 호출자에게 대기자 큐에 들어가 sleep하라는 신호다(5챕터). Figure 9-1이 네 단계 전체를 도식화한다.

Figure 9-1 — pgbuf_get_victim 단계별 스캔.

flowchart TD
  B{"자신의 private가 쿼터 초과?"} -- 예 --> C["victim_from_lru 자신"]
  C -->|찾음| R["victim 반환"]
  C -->|실패| D["restrict_other, searched_own 설정"]
  B -- 아니오 --> E
  D --> E{"쿼터+플러시 스레드?"}
  E -- 예 --> F["lfcq private: big 우선, 그 다음 ordinary"]
  F -->|찾음| R
  E -- 아니오 --> G
  F -->|실패| G["루프: lfcq shared"]
  G -->|찾음| R
  G -->|소진| H{"searched_own 아님?"}
  H -- 예 --> I["victim_from_lru 자신, 쿼터 무시"]
  I -->|찾음| R
  H -- 아니오 --> J["NULL: 대기"]
  I -->|실패| J

9.3 pgbuf_get_victim_from_lru_list — 아래에서 위로 스캔, 네 가지 배제 조건

섹션 제목: “9.3 pgbuf_get_victim_from_lru_list — 아래에서 위로 스캔, 네 가지 배제 조건”

BCB가 실제로 확보되는 곳이다. victim_hint에서 zone 3 아래쪽 방향으로 스캔하며, 배제 마스크를 적용하고, 성공하면 BCB를 제거한 뒤 잠금 상태로 반환한다. 스캔 전에 세 가지 조기 NULL 반환이 있고, 힌트가 재동기화된다:

// pgbuf_get_victim_from_lru_list -- src/storage/page_buffer.c
if (lru_list->count_vict_cand == 0) { return NULL; } /* <- 1: no candidates, no mutex */
pthread_mutex_lock (&lru_list->mutex);
if (lru_list->bottom == NULL || !PGBUF_IS_BCB_IN_LRU_VICTIM_ZONE (lru_list->bottom))
{ pthread_mutex_unlock (&lru_list->mutex); return NULL; } /* <- 2: no zone-3 */
if (PGBUF_IS_PRIVATE_LRU_ONE_TWO_OVER_QUOTA (lru_idx))
pgbuf_lru_adjust_zones (thread_p, lru_list, false); /* shrink zone1 so zone3 grows */
lru_victim_cnt = lru_list->count_vict_cand;
if (lru_victim_cnt <= 0) { pthread_mutex_unlock (&lru_list->mutex); return NULL; } /* <- 3: race emptied it */
if (!pgbuf_bcb_is_dirty (lru_list->bottom) && lru_list->victim_hint != lru_list->bottom)
(void) ATOMIC_TAS_ADDR (&lru_list->victim_hint, /* resync drifted hint */
PGBUF_IS_BCB_IN_LRU_VICTIM_ZONE (lru_list->bottom) ? lru_list->bottom : (PGBUF_BCB *) NULL);
bufptr_start = (lru_list->victim_hint == NULL) ? lru_list->bottom : lru_list->victim_hint;

스캔 루프. bufptr_start에서 위쪽으로 prev_BCB를 따라 zone 3 내에서 최대 MAX_DEPTH(1000) 스텝만큼 순회한다. 각 BCB마다:

  1. 배제 1 — avoid-victim 플래그. pgbuf_bcb_avoid_victimPGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK = DIRTY | FLUSHING_TO_DISK | VICTIM_DIRECT | INVALIDATE_DIRECT_VICTIM(네 배제 조건: dirty, 플러시 중, 이미 배정, 무효화 대기)을 검사한다. 비트가 하나라도 켜져 있으면 continue.
  2. 배제 2 — 고정됨/대기자 있음. pgbuf_is_bcb_fixed_by_any (bufptr, false): fcnt > 0, next_wait_thrd != NULL, 또는 latch 보유 시 유효하지만 바쁜 상태다. bufptr_victimizable(첫 번째는 CAS로 힌트가 됨)로 기록하고 카운트 후 continue. found_victim_cntlru_victim_cnt에 도달하면 break.
  3. 확보 — PGBUF_BCB_TRYLOCK(조건부: 리스트 mutex를 보유한 상태이므로 BCB mutex에서 블록되어선 안 됨 — 락 순서 규칙, 5챕터):
    • Trylock 성공 + pgbuf_is_bcb_victimizable(bufptr, true) 참: 성공 — 힌트를 bufptr->prev_BCB로 전진시키고, pgbuf_remove_from_lru_list를 호출한다. 이어서 waiter_threads_low_priority->size() >= 5 + num_total_threads/20 (저우선순위 백로그 드레인) 조건이 맞으면 pgbuf_panic_assign_direct_victims_from_lru로 패닉 배정한다. 새 bottom이 dirty면 플러시 데몬을 깨운다. 잠금 해제 후 VPID를 Aout에 추가(7챕터)하고 잠금 상태의 BCB를 반환.
    • Trylock 성공 but 퇴출 불가(플래그가 우리 아래에서 바뀜): PGBUF_BCB_UNLOCK, 다음 반복.
    • Trylock 실패: BCB mutex가 다른 곳에서 보유 중이며, 이는 플러시 데몬이 있을 때만 가능하다 — pgbuf_is_page_flush_daemon_available()을 단언한다. 기록 + 힌트, 카운트, 조기 종료 준수.

TO_VACUUM 비고: PGBUF_BCB_TO_VACUUM_FLAG는 마스크에 포함되지 않으므로 to-vacuum BCB는 이 스캔에서 여전히 퇴출 가능하다. unfix/직접 배정 시점에 LRU 맨 아래로 강제 이동하지, 이 스캔 단계에서 하지 않는다.

실패 후처리. 확보에 실패하고 stale 힌트에서 후보를 찾지 못한 경우(bufptr_victimizable == NULL && victim_hint != NULL) → 힌트를 bottom으로 재설정(후보가 남아 있으면) 또는 CAS로 NULL로 초기화하고, 잠금 해제 후 플러시 데몬을 깨우고 NULL 반환.

불변 조건 — victim_hint는 스캔할 가치가 있는 가장 낮은 지점을 표시한다. count_vict_cand는 zone 3의 깨끗한 BCB 수를 센다. 스캔은 힌트에서 위쪽으로만 진행하며, BCB가 깨끗한 상태로 zone 3에 진입/이탈할 때 LRU 북킵핑 헬퍼가 유지하는 count_vict_cand를 조기 종료 상한으로 신뢰한다. 힌트는 모든 확보/기록 시의 CAS 전진과 위의 재동기화로 정직하게 유지된다. 문서화된 TPCC 드리프트(힌트가 첫 번째 퇴출 가능 BCB 앞에 위치)는 스캔 단계만 낭비할 뿐이다. 각 후보는 자신의 BCB lock 하에 재검증되므로 힌트는 성능 힌트이지 안전 속성이 아니다. 드리프트는 허용되며 수정되지 않는다.

9.4 private LFCQ 헬퍼의 쿼터 게이팅

섹션 제목: “9.4 private LFCQ 헬퍼의 쿼터 게이팅”

pgbuf_lfcq_get_victim_from_private_lru어느 private 리스트를 스캔할지, 그리고 재큐잉할지를 결정한다:

// pgbuf_lfcq_get_victim_from_private_lru -- src/storage/page_buffer.c
if (pgbuf_Pool.big_private_lrus_with_victims->consume (lru_idx)) { /* big first */ }
else {
if (restricted) { return NULL; } /* <- restricted: big only */
if (!pgbuf_Pool.private_lrus_with_victims->consume (lru_idx)) { return NULL; } /* <- both empty */ }
lru_list = PGBUF_GET_LRU_LIST (lru_idx);
if (PGBUF_LRU_LIST_COUNT (lru_list) > PBGUF_BIG_PRIVATE_MIN_SIZE /* big: >100 ... */
&& PGBUF_LRU_LIST_COUNT (lru_list) > 2 * lru_list->quota && lru_list->count_vict_cand > 1) {
if (pgbuf_Pool.big_private_lrus_with_victims->produce (lru_idx)) added_back = true; } /* re-queue BIG before scan */
victim = pgbuf_get_victim_from_lru_list (thread_p, lru_idx);
if (added_back) return victim;
if (lru_list->count_vict_cand > 0 && PGBUF_LRU_LIST_IS_OVER_QUOTA (lru_list))
{ if (pgbuf_Pool.private_lrus_with_victims->produce (lru_idx)) return victim; }
lru_list->flags &= ~PGBUF_LRU_VICTIM_LFCQ_FLAG; /* not re-queued: clear so next candidate re-adds it */
return victim;

불변 조건 — private 리스트는 쿼터를 초과하는 동안에만 퇴출 가능하다. “big-private” = count > 100 이고 count > 2*quota 이고 후보 >1개; 스캔 전에 재큐잉되므로 다른 피어가 병렬로 드레인할 수 있다. big이 아닌 리스트는 후보가 있고 쿼터를 초과하는 동안에만 재큐잉되며, 그렇지 않으면 LFCQ 플래그가 지워져 pgbuf_adjust_quotas(§9.8)나 새 후보가 다시 추가할 때까지 순환에서 빠진다. 쿼터 이하인 리스트는 결코 침탈당하지 않는다. shared 형제 함수인 pgbuf_lfcq_get_victim_from_shared_lru는 쿼터가 없어, 단순히 count_vict_cand > 0이면 재큐잉하고 (단일 스레드 방식으로) 같은 리스트를 한 번 더 시도한다.

9.5 pgbuf_assign_direct_victim — 생산자 측

섹션 제목: “9.5 pgbuf_assign_direct_victim — 생산자 측”

BCB가 깨끗하고 자유로워지면(플러시 종료, §9.3의 패닉 배정, 또는 마지막 unfix), 소유자는 이를 대기 스레드에 직접 전달할 수 있다. 생산자는 BCB mutex를 보유한 상태이며, 허용되는 무효화 플래그는 FLUSHING_TO_DISK 하나뿐이다(플러시 자체가 이 함수를 호출함):

// pgbuf_assign_direct_victim -- src/storage/page_buffer.c
while (pgbuf_get_thread_waiting_for_direct_victim (waiter_thread)) { /* 75/25: low 1-in-4, else high */
thread_lock_entry (waiter_thread);
if (waiter_thread->resume_status != THREAD_ALLOC_BCB_SUSPENDED)
{ thread_unlock_entry (waiter_thread); continue; } /* <- waiter gone, try next */
thread_wakeup_already_had_mutex (waiter_thread, THREAD_ALLOC_BCB_RESUMED);
pgbuf_bcb_update_flags (thread_p, bcb, PGBUF_BCB_VICTIM_DIRECT_FLAG, PGBUF_BCB_FLUSHING_TO_DISK_FLAG);
pgbuf_Pool.direct_victims.bcb_victims[waiter_thread->index] = bcb; /* <- write mailbox before unlock */
thread_unlock_entry (waiter_thread);
return true; } /* <- assigned */
return false; /* <- no waiters */

pgbuf_get_thread_waiting_for_direct_victim75/25 가중치(저우선순위 큐는 4번에 1번, 나머지는 고우선순위)를 구현하며, 죽은 큐 항목은 건너뛴다. while은 더 이상 THREAD_ALLOC_BCB_SUSPENDED 상태가 아닌 대기자도 건너뛴다. BCB 포인터는 entry lock을 해제하기 전에 기록되므로, 대기자가 빈 슬롯을 보며 깨어나는 일은 없다. 큐가 비어 있으면 false를 반환하고 호출자가 BCB를 정상 경로로 처리한다.

불변 조건 — 전달된 피해자는 배타적으로 소유되므로 다른 스레드가 확보할 수 없다. 생산자는 BCB mutex를 보유한 채로 진입하고(PGBUF_BCB_CHECK_OWN) PGBUF_BCB_VICTIM_DIRECT_FLAG보유 중에 설정한다. 이 플래그는 PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK의 네 비트 중 하나이므로, 그 순간부터 pgbuf_bcb_avoid_victim이 true를 반환해 §9.3 스캔, §9.4 헬퍼, pgbuf_assign_flushed_pages 모두가 이 BCB를 건너뛴다. bcb_victims[tid]의 유일한 기록자는 생산자이고, 유일한 독자는 §9.6의 ATOMIC_TAS_ADDR을 통한 스레드 tid다 — 스레드당 슬롯 하나, 단일 생산자, 단일 소비자. pgbuf_invalidate_bcb조차 처리를 미룬다(§9.7). 따라서 배정과 수거 사이에 BCB는 정확히 한 대기 스레드가 논리적으로 소유한다. 동시 re-fixer는 BCB를 가로챌 수 없고, INVALIDATE_DIRECT_VICTIM을 표시해 해제만 요청할 수 있다(§9.6).

9.6 pgbuf_get_direct_victim — 소비자 측과 무효화 재시도

섹션 제목: “9.6 pgbuf_get_direct_victim — 소비자 측과 무효화 재시도”

슬롯 읽기는 슬롯을 원자적으로 지우는 TAS다:

// pgbuf_get_direct_victim -- src/storage/page_buffer.c
PGBUF_BCB *bcb = (PGBUF_BCB *) ATOMIC_TAS_ADDR (&pgbuf_Pool.direct_victims.bcb_victims[thread_p->index], NULL);
PGBUF_BCB_LOCK (bcb);
if (pgbuf_bcb_is_invalid_direct_victim (bcb)) { /* <- re-fix race */
pgbuf_bcb_update_flags (thread_p, bcb, 0, PGBUF_BCB_INVALIDATE_DIRECT_VICTIM_FLAG); /* clear it */
PGBUF_BCB_UNLOCK (bcb);
return NULL; } /* <- caller re-sleeps */
pgbuf_bcb_update_flags (thread_p, bcb, 0, PGBUF_BCB_VICTIM_DIRECT_FLAG); /* clear VICTIM_DIRECT */
if (!pgbuf_is_bcb_victimizable (bcb, true)) { assert (false); PGBUF_BCB_UNLOCK (bcb); return NULL; }
switch (pgbuf_bcb_get_zone (bcb)) {
case PGBUF_VOID_ZONE: break; /* already detached (from flush) */
case PGBUF_INVALID_ZONE: assert (false); break; /* impossible */
default: /* still in an LRU: detach + Aout */
lru_idx = pgbuf_bcb_get_lru_index (bcb);
pgbuf_lru_remove_bcb (thread_p, bcb);
pgbuf_add_vpid_to_aout_list (thread_p, &bcb->vpid, lru_idx); break; }
return bcb; /* locked, in VOID zone */

무효화 재시도. 배정과 수거 사이에 re-fixer가 해시 히트로 BCB를 찾을 수 있다(3챕터). VICTIM_DIRECT BCB를 가로챌 수 없으므로 INVALIDATE_DIRECT_VICTIM을 설정한다. 대기자는 이를 확인하고 플래그를 지워(소유권을 반환) 잠금을 해제한 뒤 NULL을 반환한다 — 호출자는 이를 pgbuf_get_victim 실패처럼 처리해 재큐잉하고 sleep한다. BCB는 그대로 남는다. 이것이 “BCB를 제자리에 두고 다시 sleep”하는 경로다. 그렇지 않은 경우 zone switch가 LRU 내에 있는 BCB를 분리하거나(Aout 기록) VOID 상태이면 아무것도 하지 않는다. 사후 조건: 잠금 상태, VOID_ZONE, 재사용 가능.

Figure 9-2 — 직접 전달.

stateDiagram-v2
  [*] --> Clean: bcb 플러시 완료 또는 clean 상태로 unfix
  Clean --> Assigning: 직접 피해자 배정, bcb mutex 보유
  Assigning --> NoWaiter: 큐 소진, false 반환
  Assigning --> Assigned: 살아있는 대기자 \n VICTIM_DIRECT 설정 \n 메일박스 기록 후 깨움
  Assigned --> Collected: 대기자 TAS로 슬롯 읽기 \n VICTIM_DIRECT 확인
  Collected --> Detached: VICTIM_DIRECT 해제 \n LRU에서 제거 \n Aout 추가
  Assigned --> Invalidated: re-fixer가 INVALIDATE_DIRECT_VICTIM 설정
  Invalidated --> ReSleep: 대기자가 플래그 해제 \n NULL 반환 \n 재큐잉
  NoWaiter --> [*]
  Detached --> [*]
  ReSleep --> [*]

9.7 pgbuf_invalidate_bcb와 이미 배정된 피해자

섹션 제목: “9.7 pgbuf_invalidate_bcb와 이미 배정된 피해자”

pgbuf_invalidate_bcb는 페이지가 사라질 때(할당 해제, 볼륨 제거) BCB를 페이지 테이블에서 제거한다. 이 챕터에서 다루는 분기는 단 하나다. 이미 배정된 직접 피해자는 그대로 둔다pgbuf_bcb_is_direct_victim (bufptr)가 true이면 함수는 잠금을 해제하고 NO_ERROR를 반환한다. 대기 스레드가 곧 이를 퇴출할 것이므로, 경쟁적으로 무효화하면 전달 과정이 깨진다. (나머지 분기 — LATCH_INVALID 조기 no-op, dirty 해제 및 zone 제거, invalid 리스트로의 NO_LATCH 해시 체인 삭제, 예상치 못한 assert(false) 꼬리 — 는 일반 BCB 정리 경로이며 BCB 수명주기 챕터에 속한다.)

9.8 공급자로서의 pgbuf_adjust_quotas

섹션 제목: “9.8 공급자로서의 pgbuf_adjust_quotas”

pgbuf_adjust_quotas(전체 로직은 10챕터)는 시스템 전체를 실행 가능 상태로 유지한다. 각 private 리스트의 quota와 zone 임계값을 재계산하고, 순환에서 빠진 후보 보유 쿼터 초과 리스트를 LFCQ에 다시 추가한다. §9.2의 단계 게이트와 §9.4의 재큐잉 테스트가 읽는 쿼터/임계값은 모두 여기서 비롯된다.

  1. pgbuf_get_victim은 네 단계 우선순위 스캔이다: 자신의 private(쿼터 초과) → 다른 private(big 우선, restricted가 아니면 ordinary도) → shared(플러시 데몬 없을 때만 루프) → 자신의 private 쿼터 무시(교착상태 회피 최후 수단). NULL 반환은 “sleep하고 직접 피해자를 기다려라”를 의미한다.
  2. LFCQ는 인덱스를 보관하며 BCB를 직접 저장하지 않는다. 리스트는 후보가 있고 PGBUF_LRU_VICTIM_LFCQ_FLAG가 설정된 경우에만 큐에 들어간다. 소비자는 쿼터 초과 후보가 있으면 재큐잉하고, 아니면 플래그를 지워 다음 후보가 추가될 때 재진입하게 한다.
  3. 쿼터 게이팅이 working set을 보호한다. private 리스트는 쿼터를 초과하는 동안에만 퇴출 가능하다. big-private(>100, >2*quota, 후보 >1) 리스트는 스캔 전에 재큐잉되어 병렬 드레인을 허용한다.
  4. pgbuf_get_victim_from_lru_list는 모든 후보를 BCB lock 하에 재검증한다. PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK의 네 배제 조건을 적용한다. TO_VACUUM은 의도적으로 배제 조건에서 제외된다.
  5. victim_hint는 성능 힌트이지 안전 속성이 아니다. 문서화된 드리프트는 스캔 단계만 낭비할 뿐이다. 정확성은 전적으로 lock 하의 BCB별 재검증에서 온다.
  6. 직접 전달은 메일박스 프로토콜이다. 생산자는 대기자를 선택하고(고우선순위/저우선순위 75/25), VICTIM_DIRECT를 설정한 뒤 대기자의 entry lock 하에 bcb_victims[tid]를 기록하고 깨운다.
  7. 소비자는 re-fix 경쟁을 처리한다: INVALIDATE_DIRECT_VICTIM을 확인하면 플래그를 지우고 BCB를 그대로 두어 NULL을 반환해 호출자가 다시 sleep하게 한다. pgbuf_invalidate_bcb도 마찬가지로 이미 배정된 직접 피해자를 건드리지 않는다.

Chapter 10: 적응형 쿼터 정렬 픽스와 특수 픽스 경로

섹션 제목: “Chapter 10: 적응형 쿼터 정렬 픽스와 특수 픽스 경로”

Chapters 3-9의 단일 페이지 생애주기 밖에 세 가지 패밀리가 존재한다: 100 ms마다 private LRU 리스트 크기를 재조정하는 적응형 쿼터 데몬, pgbuf_fix 위에서 멀티 페이지 데드락을 방지하는 정렬 픽스(ordered fix), 그리고 정상 경로의 일부를 우회하는 특수 픽스 경로들이다. private/shared 리스트, victim 존, LFCQ 큐에 대해서는 동반 문서 cubrid-page-buffer-manager.md와 Chapter 9를 참조한다 — 여기서 재유도하지 않는다.

10.1 두 구조체: pgbuf_page_quotapgbuf_watcher

섹션 제목: “10.1 두 구조체: pgbuf_page_quota와 pgbuf_watcher”

pgbuf_page_quota는 하나의 인스턴스(pgbuf_Pool.quota)로 존재하며, pgbuf_adjust_quotas가 읽고 쓰는 전역 상태를 담는다. 리스트별 출력값 (quota, threshold_lru1/2)은 각 PGBUF_LRU_LIST(Chapter 1)에 위치하며, 이 구조체에는 없다.

필드역할존재 이유
num_private_LRU_listprivate 리스트 수; PGBUF_PAGE_QUOTA_IS_ENABLED> 0마스터 활성 스위치; 0이면 서브시스템이 비활성화됨
lru_victim_flush_priority_per_lru리스트별 float, flush 우선순위flush 데몬(Ch 8)이 쿼터 초과 리스트 쪽으로 flushing을 편향시킴
private_lru_session_cntprivate 리스트별 활성 세션 수pgbuf_assign_private_lru가 세션 수 0인 리스트를 우선 선택
private_pages_ratioprivate이어야 할 BCB의 비율all_private_quota를 유도하는 평활화된 목표값
add_shared_lru_idxshared 리스트 이동 시 사용하는 순환 인덱스private-to-shared 마이그레이션 시 shared LRU 배정을 라운드로빈
avoid_shared_lru_idx이동 시 피할 shared 리스트가장 큰 리스트로의 트래픽을 분산시켜 victimization으로 소진되도록 유도
last_adjust_time마지막 조정의 TSC_TICKS1 ms / 500 ms 주기 체크를 제어
adjust_age단조 증가 카운터, 조정마다 증가다른 코드가 비교하는 세대 스탬프
is_adjusting재진입 가드한 번에 하나의 스레드만 adjust 본체를 실행

pgbuf_watcher는 호출자가 소유하는 정렬 픽스 핸들로서, 스택에 할당되고 PGBUF_INIT_WATCHER(w, rank, hfid)로 초기화된 뒤 pgbuf_ordered_fix에 전달된다. 그런 다음 holder의 watcher 리스트에 연결되어 기계적 재픽스(re-fix)가 가능하게 된다.

필드역할존재 이유
pgptr고정된 페이지, 또는 NULL픽스 출력; “watcher가 살아있는가”의 테스트 기준(PGBUF_IS_CLEAN_WATCHER)
next / prevholder의 watcher 리스트 내 링크하나의 holder(하나의 고정된 BCB)가 여러 watcher를 가질 수 있음
group_id그룹의 heap-header 페이지 VPID데드락 키: 동일 힙의 페이지들이 하나의 그룹을 공유
latch_mode (7비트)이 watcher가 보유한 latch재픽스 시 동일 모드를 복원; 어느 watcher에서든 WRITE이면 해당 페이지를 승격
page_was_unfixed (1비트)정렬 픽스가 이 페이지를 unfix-and-refix했으면 true호출자에게 “페이지 내 포인터가 이동했으니 다시 읽어라”를 알림
initial_rank (4비트)초기화 시 설정된 rank픽스 이전 호출자가 선언한 rank
curr_rank (4비트)픽스 후 유효 rank이 페이지가 자신의 그룹 헤더이면 PGBUF_ORDERED_HEAP_HDR로 승격
magic (디버그)0x12345678초기화되지 않은/잘못된 watcher를 감지
watched_at / init_at (디버그)소스 위치 문자열누수 / 이중 픽스 진단

불변 조건(watcher rank 단조성, 그룹 내): 동일한 물리 페이지 위의 모든 watcher는 같은 curr_rankgroup_id를 가져야 한다. pgbuf_ordered_fix_release가 holder의 watcher 리스트를 스캔하면서 이를 강제하며, 불일치가 발생하면 ER_PB_ORDERED_INCONSISTENCY(치명적 오류)를 발생시킨다. 위반 시 아래의 VPID 정렬이 정의되지 않으며 데드락 보장이 무너진다.

rank 순서는 enum PGBUF_ORDERED_RANK(page_buffer.h)로 정의된다: PGBUF_ORDERED_HEAP_HDR = 0(가장 먼저 픽스) < PGBUF_ORDERED_HEAP_NORMAL < PGBUF_ORDERED_HEAP_OVERFLOW(가장 나중에 픽스) < PGBUF_ORDERED_RANK_UNDEFINED (sentinel). pgbuf_watcherPGBUF_HOLDERfirst_watcher..last_watcher 체인에 매달리며(bufptr이 고정된 PGBUF_BCB), group_id에 해당 heap-header VPID를 태깅하여 그룹을 식별한다.

10.2 pgbuf_adjust_quotas — 100 ms마다 private 쿼터 재계산

섹션 제목: “10.2 pgbuf_adjust_quotas — 100 ms마다 private 쿼터 재계산”

Page Maintenance Daemon(100 ms cubthread::looper)이 pgbuf_page_maintenance_execute를 호출하고, 부팅 가드(BO_IS_FLUSH_DAEMON_AVAILABLE) 통과 후 pgbuf_adjust_quotaspgbuf_direct_victims_maintenance(10.3)를 순서대로 호출한다. 주기 게이트와 조기 반환:

  1. 비활성화 / 이미 실행 중. if (!PGBUF_PAGE_QUOTA_IS_ENABLED || quota->is_adjusting) return; 아니면 is_adjusting = 1.
  2. 너무 이름 (< 1 ms). diff_usec < 1000이면 가드를 해제하고 반환.
  3. 낮은 활동량이고 < 500 ms. pg_unfix_cnt < PGBUF_TRAN_THRESHOLD_ACTIVITY(num_buffers/4) 이고 경과 시간 < 500 ms이면 반환. 바쁜 풀은 ~1 ms마다, 유휴 풀은 500 ms마다 조정.
  4. 매우 낮은 활동 플래그. pg_unfix_cnt.exchange(0)으로 읽고 초기화; 이전 값이 < THRESHOLD/100이면 low_overall_activity = true.

이후 last_adjust_time을 스탬프하고 adjust_age를 증가시킨다.

Phase A — 리스트별 hits. PGBUF_TOTAL_LRU_COUNT개 리스트를 한 번 순회한다. lru_hits = ATOMIC_TAS_32(&monitor->lru_hits[i], 0)으로 읽고 초기화한 뒤, hits/sec으로 환산하여 lru_private_hits/lru_shared_hits에 누적하고, total_victims += PGBUF_GET_LRU_LIST(i)->count_vict_cand를 더한다. 각 private 리스트의 활동 샘플은 히스토리 평활화를 적용한다: diff_usec >= tensec_usec(>10 s)이면 monitor->lru_activity[i] = lru_hits(오래된 샘플 대체); 그렇지 않으면 시간 가중 혼합 ((tensec_usec - diff_usec) * old + diff_usec * lru_hits) / tensec_usec.

Phase B — private 비율. low_overall_activity이면 private_ratio = MIN_PRIVATE_RATIO로 강제(private 기아); 아니면 lru_private_hits / (private + shared)[0.01, 0.998](shared 최소 1)로 클램핑한 뒤 10 s 평활화하여 quota->private_pages_ratio에 저장.

Phase C — 재분배 (상호 배타적인 두 분기):

  • private 활동 없음 (sum_private_lru_activity_total == 0): all_private_quota = 0; 모든 private 리스트에 quota = threshold_lru1 = threshold_lru2 = 0, 리스트 mutex 아래 pgbuf_lru_adjust_zones 적용(페이지가 있을 경우), victim 후보가 있으면 LFCQ에 추가(pgbuf_lfcq_add_lru_with_victims).
  • 일부 private 활동 있음 (else): 예산은 all_private_quota = (int)((num_buffers - invalid_cnt) * quota->private_pages_ratio)이며, 활동 비율로 리스트에 분배:
// pgbuf_adjust_quotas (phase C, active) -- src/storage/page_buffer.c
new_quota = (int) ((float) lru_activity[i] / sum_private_lru_activity_total * all_private_quota);
new_quota = MIN (new_quota, PGBUF_PRIVATE_LRU_MAX_HARD_QUOTA); /* absolute cap */
new_quota = MIN (new_quota, num_buffers / 2); /* half-pool cap */
lru_list->threshold_lru1 = lru_list->threshold_lru2 = (int) (new_quota * PGBUF_LRU_ZONE_MIN_RATIO);

두 개의 cap은 단일 리스트가 풀을 독점하는 것을 방지한다; threshold_lru1/2는 Chapter 7 unfix 경로가 읽는 존 크기다.

Phase D — shared 리스트. 남은 예산을 균등 분배: avg_shared_lru_size = (num_buffers - all_private_quota) / num_LRU_list, 최소값은 PGBUF_MIN_SHARED_LIST_ADJUST_SIZE; threshold_lru1/2는 설정된 ratio_lru1/2로부터 계산. 임계값을 초과한 각 shared 리스트는 재조닝(re-zone)되고, victim 후보가 있으면 큐에 추가된다.

Phase E — victim_rich 및 해제. `monitor.victim_rich = total_victims

= (int)(0.1 * num_buffers); quota->is_adjusting = 0;. victim_rich`는 Chapter 9의 저비용 “victimization을 강하게 밀어붙일까?” 힌트로, 풀의 10% 초과 시 true가 된다.

불변 조건(단일 writer adjust): is_adjusting은 진입 시 설정되고 모든 종료 경로 (4개의 조기 반환 포함, 끝 부분 포함)에서 반드시 해제된다. 뮤텍스가 아닌 단일 스레드 데몬을 위한 best-effort 플래그다. 해제를 잊은 조기 return이 있으면 서브시스템이 영구적으로 동결된다(이후 모든 호출이 게이트 1에서 걸림).

flowchart TD
  B{"활성화 상태이고\n조정 중이 아님?"} -->|아니오| Z["return"]
  B -->|예| C["is_adjusting=1"]
  C --> D{"diff<1ms?"}
  D -->|예| Y["is_adjusting=0;\nreturn"]
  D -->|아니오| E{"낮은 활동량이고\ndiff<500ms?"}
  E -->|예| Y
  E -->|아니오| K{"총 활동량==0?"}
  K -->|예| J["전체 private quota=0"]
  K -->|아니오| S["활동량 비율로 분배,\n절대 cap 및 pool/2 cap 적용"]
  J --> L["shared 임계값;\nvictim_rich; is_adjusting=0"]
  S --> L

Figure 10-1. pgbuf_adjust_quotas: 주기 게이트(1-4)와 Phase C의 두 재분배 분기.

10.3 pgbuf_direct_victims_maintenance — 백업 victim 전달

섹션 제목: “10.3 pgbuf_direct_victims_maintenance — 백업 victim 전달”

빠른 경로는 unfix/flush의 부작용으로 victim을 배정하지만, 유휴 시스템에서는 이 경로가 전혀 발동하지 않아 대기 중인 스레드가 굶을 수 있다. 이 백업은 리스트를 라운드로빈으로 순회하며 victim을 직접 전달하는데, private 리스트와 shared 리스트 각각 한 번씩 수행한다:

// pgbuf_direct_victims_maintenance -- src/storage/page_buffer.c
static int prv_index = 0; /* round-robin cursors, single-threaded use only */
static int shr_index = 0;
for (index = prv_index, restarted = false;
pgbuf_is_any_thread_waiting_for_direct_victim () && nassigns > 0
&& index != prv_index && !restarted;
(index == PGBUF_PRIVATE_LRU_COUNT - 1) ? index = 0, restarted = true : index++)
pgbuf_lfcq_assign_direct_victims (thread_p, PGBUF_LRU_INDEX_FROM_PRIVATE (index), &nassigns);
prv_index = index; /* persist cursor for next tick */
// ... a second, structurally identical loop over shared lists, then shr_index = index ...

커서는 prv_index에서 시작하므로, index != prv_index는 반복자가 그 값을 지난 뒤에야 랩어라운드 종료자로 작동한다. 각 루프는 (a) 대기 스레드가 없거나, (b) 반복당 예산 DEFAULT_ASSIGNS_PER_ITERATION(5)이 소진되거나, (c) 한 번 랩(restarted)된 경우에 종료된다. prv_index = index / shr_index = index 쓰기백이 static 커서를 틱 간에 유지하므로 매 틱마다 다른 리스트를 스윕하게 된다 — 따라서 단일 스레드 전용 사용. pgbuf_lfcq_assign_direct_victims는 캐시된 victim_hint가 낡으면 lru_list->bottom에서부터 재시도하며(CAS가 힌트를 초기화), 스스로 힌트를 복구한다.

10.4 pgbuf_ordered_fix_release — 멀티 페이지 데드락 방지

섹션 제목: “10.4 pgbuf_ordered_fix_release — 멀티 페이지 데드락 방지”

힙 연산은 여러 페이지를 동시에 보유하는데, 서로 다른 순서로 픽스하면 데드락이 발생한다. 힙 헤더는 반드시 먼저 고정되어야 하므로, 단순 VPID 순서만으로는 부족하다. 정렬 픽스는 rank(헤더 < 일반 < 오버플로우)를 유지하고, rank 내에서는 VPID로 정렬하며 — 새 요청이 보유 페이지의 이 순서를 위반하면 — 위반 페이지를 unfix하고, 정렬한 뒤, 순서대로 재픽스한다. 진입 계약: req_watcher->pgptr은 NULL이어야 하며, 그렇지 않으면 ER_FAILED_ASSERTION; 요청된 VPID가 그룹 헤더이면 curr_rankPGBUF_ORDERED_HEAP_HDR, 아니면 initial_rank.

Branch 1 — 조건부 첫 번째 시도. 스레드가 다른 페이지를 보유하지 않으면 (holder == NULL, 또는 holder->thrd_link == NULL && VPID_EQ(req_vpid, &holder->bufptr->vpid) — 이 페이지만), PGBUF_UNCONDITIONAL_LATCH를 사용; 그렇지 않으면 잠재적 데드락을 빠르게 감지하기 위해 PGBUF_CONDITIONAL_LATCH를 사용. 이후 ret_pgptr = pgbuf_fix_release(...).

  • 성공: holder를 찾아 group id 확인(기존 watcher에서, 또는 PAGE_HEAP이면 pgbuf_get_groupid_and_unfix로 힙 헤더 픽스), pgbuf_add_watch_instance_internal로 연결, goto exit. 순서 재조정이 필요 없는 일반 경우.
  • 실패, 오류에 따라 분기: ER_PB_BAD_PAGEID/ER_INTERRUPTED → 종료; OLD_PAGE_MAYBE_DEALLOCATED+ER_PB_BAD_PAGEID → 해제됨으로 처리, 종료; LK_ZERO_WAIT/LK_FORCE_ZERO_WAITER_LK_PAGE_TIMEOUT(force의 경우 오류 미설정, 스캔 계속), 종료; UNCONDITIONAL → 이미 블록되어 실패, 오류와 함께 종료; else(조건부 실패) → 재정렬로 진행, er_status 초기화.

Branch 2 — 보유 페이지 분류. holder를 순회하며 watch_count <= 0(watcher 없음; 데드락 안전 가정)은 건너뜀. 각 watched holder의 watcher를 ordered_holders_info[]에 수집하며 10.1 불변 조건을 검증. diff = pgbuf_compare_hold_vpid_for_sort(req, held): diff < 0(보유 페이지가 요청 페이지 이후 정렬) → unfix 목록에 저장; diff == 0(동일 페이지) → ER_FAILED_ASSERTION; diff > 0(보유 페이지가 이전 정렬) → 고정 유지. 요청 페이지에 아직 그룹이 없으면(req_page_has_group == false), diff-1로 강제하여 모든 watched 페이지를 unfix하고 재픽스한다.

Branch 3 — 순서 위반 페이지 unfix. 저장된 각 항목마다, pgbuf_bcb_register_avoid_deallocation(holder->bufptr)로 간격 동안 핀 고정, pgbuf_unfixfix_count번 실행, 각 watcher를 PGBUF_CLEAR_WATCHER하고 pg_watcher->page_was_unfixed = true로 표시.

Branch 4 — 누락된 그룹 확인 후 정렬. 요청에 그룹이 없으면 무조건 재픽스하여 group id를 도출(OLD_PAGE_PREVENT_DEALLOCOLD_PAGE로 다운그레이드되면 dealloc-prevent 해제), 요청 페이지를 추가하고, pgbuf_compare_hold_vpid_for_sort(rank, volid, pageid)로 qsort.

Branch 5 — 정렬된 순서로 재픽스. 요청 페이지는 호출자의 request_mode/fetch_mode 사용; 복원 페이지는 저장된 latch_mode+OLD_PAGE 사용. 이제 모두 PGBUF_UNCONDITIONAL_LATCH — 순서가 보장되므로 블록킹도 데드락 없음. 실패: ER_INTERRUPTED는 종료; OLD_PAGE_MAYBE_DEALLOCATED 하의 요청 페이지에서 ER_PB_BAD_PAGEID는 허용; 보유 페이지 복원 실패 → ER_PB_ORDERED_REFIX_FAILED (심각 — watcher가 부분적으로 살아있는 상태).

불변 조건(호출자는 page_was_unfixed를 반드시 지켜야 함): unfix-and-refix된 모든 watcher에는 page_was_unfixed == true가 설정된다. 그 페이지에 캐시된 포인터는 이제 유효하지 않을 수 있다 — 간격 동안 다른 스레드가 수정했을 수 있기 때문이다. 낡은 포인터를 재사용하면 손상된 데이터를 읽게 된다 — 정렬 픽스에서 가장 중요한 계약이다.

10.5 pgbuf_ordered_unfix — watcher를 인식하는 unfix

섹션 제목: “10.5 pgbuf_ordered_unfix — watcher를 인식하는 unfix”

pgbuf_ordered_unfix는 대응 함수다: watcher_object->pgptr == NULL이면 assert_release(false)를 호출하고 반환; 아니면 pgbuf_get_holder로 holder를 찾고, for (watcher = holder->last_watcher; ...; watcher = watcher->prev) 스캔으로 정확한 watcher를 찾은 뒤, 불변 조건 assert(assert(holder->fix_count >= holder->watch_count)) 후 pgbuf_remove_watcherpgbuf_unfix 하나를 호출한다.

불변 조건 (fix_count >= watch_count): holder는 watcher보다 더 많이 픽스될 수 있지만 (일반 fix는 watcher를 추가하지 않음), 그 반대는 불가능하다. 여기와 10.4의 분류 루프에서 assert. 위반 시 watcher가 픽스보다 오래 살아있음을 의미 — use-after-unfix.

10.6 pgbuf_promote_read_latch_release — R-to-W 승격

섹션 제목: “10.6 pgbuf_promote_read_latch_release — R-to-W 승격”

완전히 unfix하지 않고 보유 중인 READ latch를 WRITE로 변환한다, PGBUF_PROMOTE_CONDITION에 따라; 패킹된 atomic_latch(Chapter 5)에 대한 CAS 루프:

  • 유일한 reader (holder->fix_count == impl.fcnt): 다음 대기자가 promoter이면 ER_PAGE_LATCH_PROMOTE_FAIL 반환; 아니면 impl_new.latch_mode = PGBUF_LATCH_WRITE로 인플레이스 플립. 블로킹 없음.
  • 다른 reader가 존재 (fix_count != fcnt): PGBUF_PROMOTE_ONLY_READER이거나 다음 대기자가 promoter → ER_PAGE_LATCH_PROMOTE_FAIL 반환(케이스 #1/#2). PGBUF_PROMOTE_SHARED_READER → 우리의 fix를 fcnt에서 빼고, waiter_exists를 표시하고, need_block을 설정하고 루프를 나감.

need_block이면, 사실상 unfix(holder->fix_count = 0, holder 제거)하고, thread_p->wait_for_latch_promote = true로 설정하고, pgbuf_block_bcb로 WRITE를 위해 블록하며, 깨어나면 저장된 fix_count로 holder를 재할당한다.

불변 조건(promoter 상호 배제): BCB의 대기자 중 wait_for_latch_promote를 가진 자는 최대 한 명; promoter 대기자를 감지하는 두 분기 모두 뒤에 줄 서지 않고 abort한다. 두 promoter가 블록되면 서로 상대방의 read latch가 해제되기를 기다려 데드락이 된다. abort 시 ER_PAGE_LATCH_PROMOTE_FAIL 반환; 호출자는 unfix + WRITE fix로 재시도한다.

  • pgbuf_simple_fix / pgbuf_simple_unfix — 임시 파일 전용. Latch 없음, LRU 뮤텍스 없음; add_fcnt(&bufptr->atomic_latch, 1)만 수행하며, latch를 획득하지 않음(“일반 FIX(LATCH)와 혼용 불가”). 버퍼에 있으면 → direct-victim 클레임이 대기 중이고 need_fix이면 무효화, 아니면 NULL. 없으면 → need_fix이면: hash 잠금, pgbuf_claim_bcb_for_fix, 삽입, private/shared LRU에 추가; 아니면 NULL. Unfix = 잠금, add_fcnt(..., -1), 잠금 해제.
  • pgbuf_fix_if_not_deallocated — vacuum의 dealloc 인식 fix. 먼저 disk_is_page_sector_reserved: DISK_INVALID → NO_ERROR, *page = NULL(해제됨, 오류 아님); DISK_ERROR → 전파; DISK_VALIDOLD_PAGE_MAYBE_DEALLOCATED로 실제 픽스, 그 후 NULL + ER_PB_BAD_PAGEID는 recovery-redo 중이 아닌 한 삼킴(경쟁 발생).
  • pgbuf_invalidate — 페이지 드롭 (호출자가 WRITE를 보유). fcnt > 1 → 그냥 unfix(pgbuf_unlatch_thrd_holder + pgbuf_unlatch_bcb_upon_unfix), 무효화 없음. fcnt == 1 → dirty이면 flush(pgbuf_bcb_safe_flush_force_lock), VPID 기록, unfix, 재잠금, 재확인; BCB가 재사용, 재픽스, 또는 avoid-victim 상태이면 건너뜀, 그렇지 않으면 pgbuf_invalidate_bcb로 분리. 영구 페이지는 commit 후 postpone으로, 임시 페이지는 무조건 실행. pgbuf_invalidate_all은 볼륨 전체를 반복.
  • pgbuf_notify_vacuum_follows / pgbuf_bcb_is_to_vacuum — vacuum 힌트. pgbuf_bcb_update_flags(thread_p, bcb, PGBUF_BCB_TO_VACUUM_FLAG, 0)으로 PGBUF_BCB_TO_VACUUM_FLAG를 설정(“vacuum이 재방문할 예정이므로 victim 대상으로 선호하지 않음”). pgbuf_bcb_is_to_vacuum으로 테스트; victim-flush 경로가 flush 확정 시 이를 해제하므로 힌트는 일회성이다.
  • TDE 훅 (범위 외). pgbuf_set_tde_algorithm은 알고리즘이 변경되지 않으면 조기 반환; 아니면 기존 비트(pflag &= ~FILEIO_PAGE_FLAG_ENCRYPTED_MASK)를 지우고 iopage->prv.pflag에 새 암호화 비트를 OR하며(FILEIO_PAGE_FLAG_ENCRYPTED_AES/_ARIA), skip_logging이 아니면 undoredo를 로그하고 dirty 표시. 페이지 버퍼는 이 비트를 dirty/flush 경로로 전달할 뿐(Ch 6); 실제 암/복호화는 TDE 모듈의 역할이다. 유지보수자가 pflag에 TDE 테넌트가 있음을 알 수 있도록 여기에 명시한다.
  1. 100 ms Page Maintenance Daemon은 pgbuf_adjust_quotaspgbuf_direct_victims_maintenance(유휴 victim 백업)를 순서대로 실행한다; 전자는 1 ms / 500 ms 주기와 num_buffers/4 활동량으로 게이팅되며, 단일 writer is_adjusting 플래그(모든 종료 경로에서 반드시 해제)로 보호되고, 리스트별 quota/threshold_lru1/2(PGBUF_PRIVATE_LRU_MAX_HARD_QUOTA 및 풀의 절반으로 cap)와 victim_rich(풀의 10% 초과 시)를 설정한다.
  2. 정렬 픽스는 페이지를 rank(heap-hdr < normal < overflow)로 순위를 매기고 VPID로 정렬한다; 요청보다 나중에 정렬되는 보유 페이지는 unfix되고, 집합을 qsort한 뒤 모두 무조건 재픽스한다. 핵심 출력인 page_was_unfixed는 호출자가 해당 페이지를 반드시 다시 읽어야 함을 의미한다(캐시된 포인터가 낡았을 수 있음).
  3. Watcher 불변 조건(페이지당 동일 rank/group; holder당 fix_count >= watch_count)은 치명적으로 강제된다. pgbuf_promote_read_latch_release는 유일한 reader이면 R-to-W를 인플레이스 플립, PGBUF_PROMOTE_SHARED_READER이면 블록, 다른 promoter가 대기 중이면 ER_PAGE_LATCH_PROMOTE_FAIL로 abort한다.
  4. 각 특수 경로는 한 단계를 우회한다: pgbuf_simple_fix(latchless 임시 페이지), pgbuf_fix_if_not_deallocated(해제됨 = NULL, 오류 아님), pgbuf_invalidate(단일 픽스 페이지 분리), pgbuf_notify_vacuum_follows(일회성 anti-victim 힌트); TDE pflag 테넌트는 범위 외 경계다.

아래 줄 번호는 2026-06-17 시점에 관찰한 값이다. 심볼 이름이 정식 앵커이며 줄 번호는 시간이 지나면 어긋나는 힌트다.

SymbolFileLine
dwb_set_data_on_next_slotsrc/storage/double_write_buffer.cpp2686
dwb_add_pagesrc/storage/double_write_buffer.cpp2726
dwb_is_createdsrc/storage/double_write_buffer.cpp2909
fileio_page_reservedsrc/storage/file_io.h166
fileio_page_watermarksrc/storage/file_io.h179
fileio_pagesrc/storage/file_io.h186
PGBUF_DEFAULT_FIX_COUNTsrc/storage/page_buffer.c90
PGBUF_NUM_ALLOC_HOLDERsrc/storage/page_buffer.c94
PGBUF_FIX_COUNT_THRESHOLDsrc/storage/page_buffer.c106
pgbuf_latch_timeoutsrc/storage/page_buffer.c107
PGBUF_IOPAGE_BUFFER_SIZEsrc/storage/page_buffer.c118
PGBUF_FIND_BCB_PTRsrc/storage/page_buffer.c135
PGBUF_LRU_NBITSsrc/storage/page_buffer.c148
PGBUF_LRU_INDEX_MASKsrc/storage/page_buffer.c150
PGBUF_LRU_INDEX_MASKsrc/storage/page_buffer.c182
PGBUF_LRU_1_ZONEsrc/storage/page_buffer.c197
PGBUF_LRU_ZONE_MASKsrc/storage/page_buffer.c201
PGBUF_INVALID_ZONEsrc/storage/page_buffer.c205
PGBUF_VOID_ZONEsrc/storage/page_buffer.c206
PGBUF_ZONE_MASKsrc/storage/page_buffer.c211
PGBUF_GET_ZONEsrc/storage/page_buffer.c215
PGBUF_GET_LRU_INDEXsrc/storage/page_buffer.c216
PGBUF_BCB_DIRTY_FLAGsrc/storage/page_buffer.c224
PGBUF_BCB_FLUSHING_TO_DISK_FLAGsrc/storage/page_buffer.c227
PGBUF_BCB_VICTIM_DIRECT_FLAGsrc/storage/page_buffer.c234
PGBUF_BCB_INVALIDATE_DIRECT_VICTIM_FLAGsrc/storage/page_buffer.c235
PGBUF_BCB_MOVE_TO_LRU_BOTTOM_FLAGsrc/storage/page_buffer.c237
PGBUF_BCB_TO_VACUUM_FLAGsrc/storage/page_buffer.c239
PGBUF_BCB_ASYNC_FLUSH_REQsrc/storage/page_buffer.c241
PGBUF_BCB_FLAGS_MASKsrc/storage/page_buffer.c244
PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASKsrc/storage/page_buffer.c258
PGBUF_BCB_INIT_FLAGSsrc/storage/page_buffer.c265
PGBUF_BCB_COUNT_FIX_SHIFT_BITSsrc/storage/page_buffer.c268
PGBUF_BCB_AVOID_DEALLOC_MASKsrc/storage/page_buffer.c269
PGBUF_TRAN_THRESHOLD_ACTIVITYsrc/storage/page_buffer.c276
PGBUF_AOUT_NOT_FOUNDsrc/storage/page_buffer.c279
PGBUF_SHOULD_IGNORE_UNFIXsrc/storage/page_buffer.c290
HASH_SIZE_BITSsrc/storage/page_buffer.c295
PGBUF_HASH_SIZEsrc/storage/page_buffer.c296
PGBUF_HASH_VALUEsrc/storage/page_buffer.c300
PGBUF_FLUSH_VICTIM_BOOST_MULTsrc/storage/page_buffer.c305
PGBUF_NEIGHBOR_FLUSH_NONDIRTYsrc/storage/page_buffer.c307
PGBUF_MAX_NEIGHBOR_PAGESsrc/storage/page_buffer.c310
PGBUF_NEIGHBOR_POSsrc/storage/page_buffer.c314
PGBUF_CHKPT_MAX_FLUSH_RATEsrc/storage/page_buffer.c322
PGBUF_CHKPT_MIN_FLUSH_RATEsrc/storage/page_buffer.c323
PGBUF_CHKPT_BURST_PAGESsrc/storage/page_buffer.c326
PGBUF_LRU_ZONE_MIN_RATIOsrc/storage/page_buffer.c342
PGBUF_LOCK_HOLDERsrc/storage/page_buffer.c348
pgbuf_holder_statsrc/storage/page_buffer.c441
pgbuf_batch_flush_helpersrc/storage/page_buffer.c451
pgbuf_holdersrc/storage/page_buffer.c461
pgbuf_holder_anchorsrc/storage/page_buffer.c479
pgbuf_holder_setsrc/storage/page_buffer.c488
pgbuf_atomic_latch_implsrc/storage/page_buffer.c494
pgbuf_bcbsrc/storage/page_buffer.c506
atomic_latchsrc/storage/page_buffer.c513
flagssrc/storage/page_buffer.c514
next_wait_thrdsrc/storage/page_buffer.c516
count_fix_and_avoid_deallocsrc/storage/page_buffer.c528
oldest_unflush_lsasrc/storage/page_buffer.c536
pgbuf_iopage_buffersrc/storage/page_buffer.c541
struct pgbuf_iopage_buffersrc/storage/page_buffer.c541
pgbuf_buffer_locksrc/storage/page_buffer.c557
struct pgbuf_buffer_locksrc/storage/page_buffer.c557
pgbuf_buffer_hashsrc/storage/page_buffer.c570
pgbuf_lru_listsrc/storage/page_buffer.c580
victim_hintsrc/storage/page_buffer.c589
count_vict_candsrc/storage/page_buffer.c602
pgbuf_invalid_listsrc/storage/page_buffer.c621
struct pgbuf_invalid_listsrc/storage/page_buffer.c621
pgbuf_aout_bufsrc/storage/page_buffer.c636
struct pgbuf_aout_listsrc/storage/page_buffer.c645
pgbuf_aout_listsrc/storage/page_buffer.c645
pgbuf_seq_flushersrc/storage/page_buffer.c669
struct pgbuf_page_monitorsrc/storage/page_buffer.c688
struct pgbuf_page_quotasrc/storage/page_buffer.c710
pgbuf_page_quotasrc/storage/page_buffer.c710
struct pgbuf_direct_victimsrc/storage/page_buffer.c737
pgbuf_buffer_poolsrc/storage/page_buffer.c749
struct pgbuf_victim_candidate_listsrc/storage/page_buffer.c833
pgbuf_Flush_helpersrc/storage/page_buffer.c840
AOUT_HASH_IDXsrc/storage/page_buffer.c854
PGBUF_BCB_LOCKsrc/storage/page_buffer.c869
PGBUF_BCB_TRYLOCKsrc/storage/page_buffer.c871
PGBUF_IS_BCB_IN_LRU_VICTIM_ZONEsrc/storage/page_buffer.c919
PGBUF_IS_BCB_IN_LRUsrc/storage/page_buffer.c920
PGBUF_IS_BCB_OLD_ENOUGHsrc/storage/page_buffer.c927
PGBUF_PRIVATE_LRU_MAX_HARD_QUOTAsrc/storage/page_buffer.c943
PGBUF_MIN_PAGES_IN_SHARED_LISTsrc/storage/page_buffer.c946
PGBUF_TOTAL_LRU_COUNTsrc/storage/page_buffer.c969
PGBUF_IS_PRIVATE_LRU_INDEXsrc/storage/page_buffer.c975
PGBUF_LRU_LIST_IS_OVER_QUOTAsrc/storage/page_buffer.c977
PGBUF_LRU_LIST_IS_OVER_QUOTA_WITH_BUFFERsrc/storage/page_buffer.c987
set_latchsrc/storage/page_buffer.c1310
add_fcntsrc/storage/page_buffer.c1324
set_waiter_existssrc/storage/page_buffer.c1368
get_latchsrc/storage/page_buffer.c1398
get_implsrc/storage/page_buffer.c1406
pgbuf_thread_variables_initsrc/storage/page_buffer.c1415
pgbuf_hash_func_mirrorsrc/storage/page_buffer.c1441
pgbuf_hash_vpidsrc/storage/page_buffer.c1480
pgbuf_compare_vpidsrc/storage/page_buffer.c1494
pgbuf_initializesrc/storage/page_buffer.c1518
pgbuf_finalizesrc/storage/page_buffer.c1796
pgbuf_fix_with_retrysrc/storage/page_buffer.c1993
pgbuf_fix_releasesrc/storage/page_buffer.c2041
pgbuf_simple_fixsrc/storage/page_buffer.c2475
pgbuf_simple_unfixsrc/storage/page_buffer.c2569
pgbuf_promote_read_latch_debugsrc/storage/page_buffer.c2624
pgbuf_promote_read_latch_releasesrc/storage/page_buffer.c2628
pgbuf_unfixsrc/storage/page_buffer.c2850
pgbuf_invalidatesrc/storage/page_buffer.c3158
pgbuf_flushsrc/storage/page_buffer.c3341
pgbuf_flush_with_walsrc/storage/page_buffer.c3364
pgbuf_flush_if_requestedsrc/storage/page_buffer.c3404
pgbuf_flush_all_helpersrc/storage/page_buffer.c3438
pgbuf_get_victim_candidates_from_lrusrc/storage/page_buffer.c3564
pgbuf_flush_victim_candidatessrc/storage/page_buffer.c3645
pgbuf_flush_checkpointsrc/storage/page_buffer.c3960
pgbuf_flush_chkpt_seq_listsrc/storage/page_buffer.c4102
pgbuf_flush_seq_listsrc/storage/page_buffer.c4210
pgbuf_set_dirtysrc/storage/page_buffer.c4700
pgbuf_set_lsasrc/storage/page_buffer.c4771
pgbuf_set_tde_algorithmsrc/storage/page_buffer.c4881
pgbuf_set_bcb_page_vpidsrc/storage/page_buffer.c5214
pgbuf_initialize_bcb_tablesrc/storage/page_buffer.c5334
pgbuf_initialize_hash_tablesrc/storage/page_buffer.c5452
pgbuf_initialize_lock_tablesrc/storage/page_buffer.c5481
pgbuf_initialize_lru_listsrc/storage/page_buffer.c5519
pgbuf_initialize_aout_listsrc/storage/page_buffer.c5582
pgbuf_initialize_invalid_listsrc/storage/page_buffer.c5686
pgbuf_initialize_thrd_holdersrc/storage/page_buffer.c5701
pgbuf_allocate_thrd_holder_entrysrc/storage/page_buffer.c5783
pgbuf_find_thrd_holdersrc/storage/page_buffer.c5870
pgbuf_remove_thrd_holdersrc/storage/page_buffer.c5971
pgbuf_latch_bcb_upon_fixsrc/storage/page_buffer.c6073
pgbuf_unlatch_bcb_upon_unfixsrc/storage/page_buffer.c6417
pgbuf_unlatch_void_zone_bcbsrc/storage/page_buffer.c6652
pgbuf_should_move_private_to_sharedsrc/storage/page_buffer.c6758
pgbuf_block_bcbsrc/storage/page_buffer.c6803
pgbuf_timed_sleep_error_handlingsrc/storage/page_buffer.c6925
pgbuf_timed_sleepsrc/storage/page_buffer.c7014
pgbuf_wakeup_reader_writersrc/storage/page_buffer.c7186
pgbuf_search_hash_chainsrc/storage/page_buffer.c7327
pgbuf_lockfree_fix_rosrc/storage/page_buffer.c7452
pgbuf_search_hash_chain_no_bcb_locksrc/storage/page_buffer.c7517
pgbuf_insert_into_hash_chainsrc/storage/page_buffer.c7569
pgbuf_lock_pagesrc/storage/page_buffer.c7718
pgbuf_unlock_pagesrc/storage/page_buffer.c7831
pgbuf_allocate_bcbsrc/storage/page_buffer.c7916
pgbuf_claim_bcb_for_fixsrc/storage/page_buffer.c8133
pgbuf_victimize_bcbsrc/storage/page_buffer.c8372
pgbuf_invalidate_bcbsrc/storage/page_buffer.c8424
pgbuf_bcb_safe_flush_force_unlocksrc/storage/page_buffer.c8494
pgbuf_bcb_safe_flush_force_locksrc/storage/page_buffer.c8517
pgbuf_bcb_safe_flush_internalsrc/storage/page_buffer.c8550
pgbuf_get_bcb_from_invalid_listsrc/storage/page_buffer.c8644
pgbuf_put_bcb_into_invalid_listsrc/storage/page_buffer.c8693
pgbuf_get_victimsrc/storage/page_buffer.c8805
pgbuf_is_bcb_fixed_by_anysrc/storage/page_buffer.c8995
pgbuf_is_bcb_victimizablesrc/storage/page_buffer.c9023
pgbuf_get_victim_from_lru_listsrc/storage/page_buffer.c9053
pgbuf_panic_assign_direct_victims_from_lrusrc/storage/page_buffer.c9279
pgbuf_direct_victims_maintenancesrc/storage/page_buffer.c9346
pgbuf_lfcq_assign_direct_victimssrc/storage/page_buffer.c9388
pgbuf_lru_add_bcb_to_topsrc/storage/page_buffer.c9432
pgbuf_lru_add_bcb_to_middlesrc/storage/page_buffer.c9482
pgbuf_lru_add_bcb_to_bottomsrc/storage/page_buffer.c9570
pgbuf_lru_fall_bcb_to_zone_3src/storage/page_buffer.c9788
pgbuf_lru_boost_bcbsrc/storage/page_buffer.c9858
pgbuf_lru_move_from_private_to_sharedsrc/storage/page_buffer.c10064
pgbuf_remove_from_lru_listsrc/storage/page_buffer.c10089
pgbuf_move_bcb_to_bottom_lrusrc/storage/page_buffer.c10157
pgbuf_add_vpid_to_aout_listsrc/storage/page_buffer.c10201
pgbuf_remove_vpid_from_aout_listsrc/storage/page_buffer.c10282
pgbuf_bcb_flush_with_walsrc/storage/page_buffer.c10456
pgbuf_wake_flush_waiterssrc/storage/page_buffer.c10694
pgbuf_is_exist_blocked_reader_writersrc/storage/page_buffer.c10741
pgbuf_wakeupsrc/storage/page_buffer.c11319
pgbuf_set_dirty_buffer_ptrsrc/storage/page_buffer.c11369
pgbuf_flush_page_and_neighbors_fbsrc/storage/page_buffer.c11527
pgbuf_flush_neighbor_safesrc/storage/page_buffer.c11762
pgbuf_add_bufptr_to_batchsrc/storage/page_buffer.c11820
pgbuf_ordered_fix_releasesrc/storage/page_buffer.c11985
pgbuf_ordered_unfixsrc/storage/page_buffer.c12860
pgbuf_add_watch_instance_internalsrc/storage/page_buffer.c12927
pgbuf_initialize_page_quota_parameterssrc/storage/page_buffer.c13326
pgbuf_initialize_page_quotasrc/storage/page_buffer.c13370
pgbuf_initialize_page_monitorsrc/storage/page_buffer.c13430
pgbuf_adjust_quotassrc/storage/page_buffer.c13639
pgbuf_initialize_seq_flushersrc/storage/page_buffer.c14016
pgbuf_flush_control_from_dirty_ratiosrc/storage/page_buffer.c14233
pgbuf_fix_if_not_deallocated_with_callersrc/storage/page_buffer.c14735
pgbuf_assign_direct_victimsrc/storage/page_buffer.c14809
pgbuf_assign_flushed_pagessrc/storage/page_buffer.c14876
pgbuf_get_thread_waiting_for_direct_victimsrc/storage/page_buffer.c14946
pgbuf_get_direct_victimsrc/storage/page_buffer.c14978
pgbuf_lru_advance_victim_hintsrc/storage/page_buffer.c15131
pgbuf_bcb_update_flagssrc/storage/page_buffer.c15171
pgbuf_bcb_change_zonesrc/storage/page_buffer.c15269
pgbuf_bcb_get_zonesrc/storage/page_buffer.c15374
pgbuf_bcb_get_zonesrc/storage/page_buffer.c15375
pgbuf_bcb_get_lru_indexsrc/storage/page_buffer.c15386
pgbuf_bcb_get_lru_indexsrc/storage/page_buffer.c15387
pgbuf_bcb_is_dirtysrc/storage/page_buffer.c15400
pgbuf_bcb_set_dirtysrc/storage/page_buffer.c15412
pgbuf_bcb_mark_is_flushingsrc/storage/page_buffer.c15463
pgbuf_bcb_mark_was_flushedsrc/storage/page_buffer.c15486
pgbuf_bcb_mark_was_not_flushedsrc/storage/page_buffer.c15500
pgbuf_bcb_is_flushingsrc/storage/page_buffer.c15513
pgbuf_bcb_should_be_moved_to_bottom_lrusrc/storage/page_buffer.c15561
pgbuf_notify_vacuum_followssrc/storage/page_buffer.c15574
pgbuf_bcb_is_to_vacuumsrc/storage/page_buffer.c15589
pgbuf_bcb_avoid_victimsrc/storage/page_buffer.c15603
pgbuf_bcb_register_avoid_deallocationsrc/storage/page_buffer.c15627
pgbuf_bcb_unregister_avoid_deallocationsrc/storage/page_buffer.c15640
pgbuf_bcb_should_avoid_deallocationsrc/storage/page_buffer.c15684
pgbuf_bcb_register_fixsrc/storage/page_buffer.c15720
pgbuf_bcb_is_hotsrc/storage/page_buffer.c15741
pgbuf_lfcq_get_victim_from_private_lrusrc/storage/page_buffer.c15802
pgbuf_lfcq_get_victim_from_shared_lrusrc/storage/page_buffer.c15894
pgbuf_bcb_register_hit_for_lrusrc/storage/page_buffer.c15979
pgbuf_get_page_flush_intervalsrc/storage/page_buffer.c16353
pgbuf_page_maintenance_executesrc/storage/page_buffer.c16375
pgbuf_page_flush_daemon_tasksrc/storage/page_buffer.c16396
pgbuf_page_maintenance_daemon_initsrc/storage/page_buffer.c16531
pgbuf_page_flush_daemon_initsrc/storage/page_buffer.c16549
pgbuf_page_post_flush_daemon_initsrc/storage/page_buffer.c16567
pgbuf_is_page_flush_daemon_availablesrc/storage/page_buffer.c16673
pgbuf_is_temp_lsasrc/storage/page_buffer.c16683
pgbuf_init_temp_page_lsasrc/storage/page_buffer.c16689
PAGE_FETCH_MODEsrc/storage/page_buffer.h172
PGBUF_LATCH_MODEsrc/storage/page_buffer.h190
PGBUF_ORDERED_RANKsrc/storage/page_buffer.h222
pgbuf_watchersrc/storage/page_buffer.h234
PGBUF_TEMP_LSAsrc/storage/page_buffer.h258
PGBUF_ATOMIC_LATCHsrc/storage/page_buffer.h365
  • cubrid-page-buffer-manager.md — 상위 개요 문서. flush 경로는 cubrid-double-write-buffer.md, flush가 따르는 WAL 규칙은 cubrid-log-manager-detail.md 참조.
  • 원시 분석 자료: raw/code-analysis/cubrid/storage/buffer_manager/.
  • 코드: src/storage/page_buffer.{c,h}.
  • 방법론: knowledge/methodology/code-analysis-detail-doc.md.