CUBRID 페이지 버퍼 매니저 — 코드 수준 심층 분석
이 문서의 위치: 상위 분석 문서인
cubrid-page-buffer-manager.md는 설계 의도와 이론적 배경을 다룬다. 이 문서는 코드 수준에서 모든 분기와 필드를 추적한다. 각 장은 독립적으로 읽을 수 있으나, 순서대로 읽으면 데이터 페이지 하나가 버퍼 풀 안에서 겪는 전체 생애주기 — fix, latch, dirty, flush, victimize — 를 온전히 따라갈 수 있다.
목차:
Chapter 1: 자료구조 지도
섹션 제목: “Chapter 1: 자료구조 지도”페이지 버퍼 매니저가 소유하는 모든 구조체를 필드 단위로 정리한 참조 챕터다.
상위 레벨 문서(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 열 하나로 필드 커버리지를 빠짐없이 유지한다.
1.1 비트 패킹 어휘
섹션 제목: “1.1 비트 패킹 어휘”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_dealloc은 PGBUF_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.ctypedef 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 |
|---|---|
raw | 64비트 전체 워드. 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_latch | R/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_list | BCB가 LRU에 진입할 때의 리스트 tick. 리스트의 tick_list와 비교하여 age boost 여부 결정 |
tick_lru3 | zone 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.cstruct 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_PTR로 CAST_PGPTR_TO_BFPTR를 써서 BCB를 복원 |
dummy (32비트) | 4바이트 패딩. 32비트에서 bcb가 4바이트이므로, iopage를 오프셋 8로 밀어 LOG_LSA 정렬을 맞춤 |
iopage | 임베디드 FILEIO_PAGE. 한 번의 할당으로 제어 정보와 온디스크 이미지를 함께 보유 |
iopage의 첫 바이트는 헤더(prv)이며, WAL/복구 경로가 그 lsa와 ptype을 직접 읽는다.
// struct fileio_page -- src/storage/file_io.hstruct 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_hash와 pgbuf_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.Field | Role & 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 / bottom | MRU 헤드 / LRU 테일. 신규/부스트 BCB는 top에 연결, 교체는 bottom에서 진행 |
bottom_1 | zone 1의 마지막 BCB(비어 있으면 NULL). zone 1→2 경계 이동을 O(1)로 처리 |
bottom_2 | zone 2의 마지막 BCB(비어 있으면 NULL). zone 2/3 경계 마커 |
victim_hint | 휘발성 victim 스캔 시작점. 매번 고정된 BCB를 다시 탐색하지 않도록 함 |
count_lru1/2/3 | 존별 BCB 수. 재균형 및 victim 가용성 결정에 사용 |
count_vict_cand | 교체 가능한 BCB 수. victim 검색이 빈 리스트를 건너뛸 수 있게 함(챕터 9) |
threshold_lru1/2 | zone 1/2의 목표 크기. BCB는 자신의 zone이 임계값을 초과하면 한 zone 아래로 내려감. zone 3이 나머지 |
quota | 개인 리스트의 목표 크기. 세션별 적응형(챕터 10). 공유 리스트에는 미사용 |
tick_list | add/boost 시 증가. BCB가 진입 tick을 저장하며, 차이가 오래됨 정도를 나타냄 |
tick_lru3 | BCB가 zone 3으로 내려갈 때 증가. victim-hint 순서를 위해 bcb->tick_lru3에 스탬프 |
flags | 리스트별 플래그 워드. 벌크/쿼터 리스트 상태를 표시 |
index | buf_LRU_list[]에서 이 리스트의 인덱스. BCB flags 하위 16비트에 저장되어 BCB가 자신의 홈 리스트를 알 수 있음 |
불변 조건 —
victim_hint는 실제 첫 번째 victim 이전으로 밀릴 수 있으며, 코드는 이를 허용한다. 힌트 아래의 모든 것은 더티 상태여야 하지만, TPCC 코어 덤프 분석에서 힌트가 실제 첫 번째 교체 가능 BCB 앞에 위치하는 사례가 발견되었다 —TODO로 표시된 미해결 알려진 버그다. 소비자는 힌트를 시작점으로만 취급하고 모든 후보를 재검증해야 한다. 힌트를 정확한 값으로 신뢰하면 유효한 victim을 건너뛰거나 더티 페이지를 교체하게 된다. 챕터 9에서 스캔 과정을 다룬다.
1.7 pgbuf_invalid_list — 프리 풀
섹션 제목: “1.7 pgbuf_invalid_list — 프리 풀”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.Field | Role & rationale |
|---|---|
holder.fix_count | 이 BCB에 대한 재 fix 깊이. 마지막 unfix에서만 래치가 해제됨 |
holder.bufptr | 보유 중인 BCB. 스레드별 holder를 공유 BCB에 연결 |
holder.thrd_link | hold 리스트의 다음 항목. pgbuf_unfix_all이 보유 중인 모든 페이지를 순회할 수 있게 함 |
holder.next_holder | free 리스트의 다음 항목. 재할당 없이 슬롯 재활용 |
holder.perf_stat | PGBUF_HOLDER_STAT 플래그. 페이지 사용의 성능 계측 |
holder.fixed_at/fixed_at_size (dbg) | fix 호출 위치 캡처. 어디서 fix했는지 디버그 추적 |
holder.watch_count | 이 holder에 연결된 watcher 수. 순서 fix watcher(챕터 10)가 holder에 매달림 |
holder.first_watcher/last_watcher | watcher 리스트 양끝. 순서 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/prev | holder의 watcher 리스트 내 링크. 하나의 페이지에 여러 watcher가 붙을 수 있으며 O(1) 분리 가능 |
group_id | 그룹 힙 헤더의 VPID. 순서 fix는 데드락을 피하기 위해 그룹 내 페이지를 순서대로 fix함 |
latch_mode:7 | 요청된 래치 모드. 작은 enum을 7비트에 꽉 채워 패킹 |
page_was_unfixed:1 | 재정렬 중 unfix+refix 발생 시 설정. 캐시된 pgptr이 이동했을 수 있으므로 재검증 필요 |
initial_rank:4 | watcher 초기화 시 순위. 재정렬 전 원하는 fix 순서 |
curr_rank:4 | fix 후의 순위. 순서 위반 fix를 감지 |
이 비트들을 구동하는 순서 fix 재정렬 루프는 챕터 10에서 추적한다.
1.10 pgbuf_buffer_pool — 전역 루트
섹션 제목: “1.10 pgbuf_buffer_pool — 전역 루트”pgbuf_Pool이 모든 것을 하나로 묶는다. 수정자가 반드시 알아야 할 필드들:
| 필드 | Role & rationale |
|---|---|
num_buffers | 총 BCB 프레임 수(≈ 10 × num_trans). 모든 테이블의 범위를 결정하는 고정 풀 크기 |
BCB_table | PGBUF_BCB[]. 제어 블록 배열 |
buf_hash_table | PGBUF_BUFFER_HASH[]. VPID 해시(1.5) |
buf_lock_table | PGBUF_BUFFER_LOCK[]. 스레드당 하나의 대기 중인 읽기 레코드(1.5) |
iopage_table | PGBUF_IOPAGE_BUFFER[]. 페이지 페이로드. BCB_table과 병렬 배열 |
num_LRU_list | 공유 LRU 리스트 수. buf_LRU_list의 첫 번째 슬라이스 |
ratio_lru1/ratio_lru2 | zone 1/2 크기 비율. 각 리스트의 threshold_lru1/2 초기값 결정 |
buf_LRU_list | PGBUF_LRU_LIST[] 공유+가비지+개인. 하나의 배킹 배열, 인덱스로 종류 구분 |
buf_AOUT_list | PGBUF_AOUT_LIST victim 히스토리. 2Q의 “Aout” 절반(챕터 7) |
buf_invalid_list | PGBUF_INVALID_LIST 프리 풀(1.7). 신규 BCB의 소스 |
victim_cand_list | victim 후보 배열. flush 데몬의 작업 세트(챕터 8) |
seq_chkpt_flusher | PGBUF_SEQ_FLUSHER. 속도 제어 체크포인트 flush 상태 |
monitor | PGBUF_PAGE_MONITOR. 더티 카운트, LRU별 히트, victim/fix 카운터 |
quota | PGBUF_PAGE_QUOTA. 개인 리스트 쿼터 튜닝(챕터 10) |
thrd_holder_info | PGBUF_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_victims | victim이 있는 LRU 인덱스 세 개의 LFCQ. 전체 스캔 대신 victim 검색이 이를 참조(챕터 9) |
show_status/_old/_snapshot/_mutex | SHOW STATUS 보고 상태. SHOW 쿼리에 노출되는 통계 |
1.11 zone/index 접근자
섹션 제목: “1.11 zone/index 접근자”패킹된 flags 워드의 유일하게 승인된 읽기 경로. 나머지 코드는 flags를 직접 마스킹하지 않는다.
// pgbuf_bcb_get_zone / _get_lru_index / PGBUF_IS_BCB_IN_LRU -- page_buffer.cSTATIC_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)분기 분석:
pgbuf_bcb_get_zone— 무조건 리턴 하나, 오류 경로 없음. 모든 유효한flags는 다섯 가지PGBUF_ZONE값 중 정확히 하나를 반환한다. 손상된flags는 호출자의switchdefault로 넘어간다.pgbuf_bcb_get_lru_index—assert로 두 분기. 디버그, BCB가 LRU zone에 있음: assert 통과, 하위 16비트(홈 리스트의index, §1.6) 반환. 디버그, LRU zone이 아님(INVALID/VOID): assert 발동 — 해당 비트는 무의미하다. 릴리스에서는 assert가 컴파일 아웃되어 하위 16비트를 그대로 반환하므로, 호출자가 선제 조건을 책임진다. 따라서 호출 지점은PGBUF_IS_BCB_IN_LRU로 가드한다.PGBUF_IS_BCB_IN_LRU— 불리언 하나, 두 결과. zone을PGBUF_LRU_ZONE_MASK(zone 1/2/3)와 AND. INVALID/VOID는 해당 마스크 밖의 비트를 설정하므로 false. LRU zone이면 true. 분기 2를 안전하게 만드는 게이트.
1.12 포인터 관계 파노라마
섹션 제목: “1.12 포인터 관계 파노라마”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.13 챕터 요약 — 핵심 정리
섹션 제목: “1.13 챕터 요약 — 핵심 정리”- BCB 래치는 mutex가 아닌 64비트 원자 union이다.
pgbuf_atomic_latch_impl은latch_mode+waiter_exists+fcnt를 하나의std::atomic<uint64_t>에 패킹하며, 항상raw워드 전체 단위로만 접촉한다. BCB의mutex는 래치가 아닌 리스트/플래그 전환을 보호한다. flags는 하나의 워드에 세 가지를 담는다 — 플래그 바이트(24-31), zone(16-19, INVALID/VOID가 인덱스와 충돌하지 않도록 비트를 건너뜀), LRU index(0-15). §1.11 접근자로만 읽어야 한다.count_fix_and_avoid_dealloc은 두 카운터가 하나로 합쳐진 것이다 — 상위 16비트는 포화 fix 카운트, 하위 16비트는 원자적 avoid-dealloc 카운트. 2바이트 원자 연산이 이식성을 보장하지 않아 합쳐졌다.next_BCB는 LRU 리스트와 프리 리스트가 공유하며, zone이 단일 진실의 원천이다. 재링크와 zone 변경은 하나의 임계 구역이어야 한다.victim_hint는 조언적이며 실제 첫 번째 victim 이전으로 밀릴 수 있다 — 알려진 미해결 버그. 시작점으로만 취급하고 재검증해야 한다.- iopage는 역방향 포인터와 정렬 패드와 함께 임베디드된다.
PAGE_PTR → BCB왕복이 가능하고FILEIO_PAGE는 8바이트 정렬을 유지한다. 그prv.lsa(페이지 말미에prv2로 미러링)가 WAL/복구 워터마크다. - fix는 holder 삼형제에서 스레드별로 장부 관리된다 (anchor → holder →
슬랩 10개).
pgbuf_bcb_get_lru_index는PGBUF_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과 함께 필드별 초기화로 시작한다. monitor의 std::atomic_int 멤버는 memset을 쓸 수 없으므로 .store(0)으로 초기화한다. 이 수동 초기화 덕분에 pgbuf_finalize는 절반만 구축된 풀에서도 안전하게 동작한다. 모든 포인터는 어떤 할당이 일어나기 전에 NULL이 된다(free_index 같은 스칼라 센티널은 여기서 0으로 설정했다가 §2.9에서 -1로 재설정된다). num_buffers는 파라미터를 읽어 하한 처리하며, 두 LRU 존 비율은 각각 클램핑된다.
// pgbuf_initialize -- src/storage/page_buffer.cpgbuf_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.cfor (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_buffer는 PGBUF_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.chashsize = 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_mirror는 hash_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.cthrd_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.cpgbuf_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.cpgbuf_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.cpthread_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_free | free 노드 리스트의 헤드 | 노드는 사전 할당되어 삽입 시 malloc 불필요 |
bufarray | 모든 노드의 단일 할당 블록 | 노드별 할당보다 블록 하나가 효율적 |
num_hashes | MHT 룩업 테이블 수 | 샤딩으로 경합 감소 |
aout_buf_ht | MHT 테이블 배열, VPID to 노드 | ”이 페이지가 최근 evict됐나?”를 O(1)로 판단 |
max_count | 용량; 0이면 비활성화 | 메모리 한도이자 on/off 스위치 |
PGBUF_PAGE_QUOTA(적응형 private LRU 크기 조정 — §2.7에서 채워짐):
| 필드 | 역할 | 존재 이유 |
|---|---|---|
num_private_LRU_list | private LRU 수; 0이면 쿼터 비활성화 | private LRU 기능의 마스터 스위치 |
lru_victim_flush_priority_per_lru | LRU별 플러시 우선순위(TOTAL_LRU_COUNT개 float) | flush 데몬에게 dirty 압력이 어디 있는지 알림 |
private_lru_session_cnt | private LRU별 활성 세션 수 | 세션이 0인 리스트는 회수 가능 |
private_pages_ratio | 전체 BCB 중 private BCB 비율 | 쿼터 조정기가 목표로 하는 값 |
add_shared_lru_idx | shared 리스트로 재배치 시 라운드로빈 커서 | BCB를 shared 리스트 전반에 균등 분산 |
avoid_shared_lru_idx | 재배치 시 건너뛸 shared LRU | 과부하된 리스트에 몰리는 것을 방지 |
last_adjust_time | 마지막 쿼터 조정 타임스탬프 | 조정 빈도 제한 |
adjust_age | 단조 증가하는 조정 카운터 | 쿼터 상태의 버전 관리 |
is_adjusting | 조정기의 재진입 방지 플래그 | 한 번에 하나의 스레드만 쿼터 조정 가능 |
PGBUF_PAGE_MONITOR(LRU별 통계 — §2.8에서 채워짐):
| 필드 | 역할 | 존재 이유 |
|---|---|---|
dirties_cnt | dirty BCB 수(INT64) | flush 긴급도 판단 |
lru_hits | LRU별 LRU1 히트 수(TOTAL_LRU_COUNT개 int) | 쿼터 튜닝을 위한 최신성 품질 신호 |
lru_activity | LRU별 활동 수준 | 유휴 private 리스트 감지 및 회수 판단 |
lru_shared_pgs_cnt | 모든 shared LRU에 걸친 BCB 수(volatile) | private_pages_ratio의 보완 지표 |
pg_unfix_cnt | unfix 카운터(std::atomic_int) | 주기적 쿼터 갱신 트리거 |
lru_victim_req_cnt | 전체 LRU에 걸친 victim 요청 수 | victim 압력 게이지 |
fix_req_cnt | fix 요청 수(std::atomic_int) | 전체 부하 게이지 |
bcb_locks (SERVER_MODE) | 스레드별 BCB 뮤텍스 사용 추적 | 락 경합 진단 |
victim_rich | victim이 충분할 때 true | fix 코드의 빠른 경로 힌트 |
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.cquota = &(pgbuf_Pool.quota); memset (quota, 0, sizeof (PGBUF_PAGE_QUOTA));tsc_getticks ("a->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.cquota->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를 실행해 이미 할당된 메모리를 해제한다.
2.8 pgbuf_initialize_page_monitor
섹션 제목: “2.8 pgbuf_initialize_page_monitor”쿼터 데이터 초기화와 대칭적으로, 모니터는 먼저 포인터 멤버를 다시 NULL로 만들고, PGBUF_TOTAL_LRU_COUNT 크기의 LRU별 정수 배열 두 개를 할당한다.
// pgbuf_initialize_page_monitor -- src/storage/page_buffer.cmonitor->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));#endifmonitor->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.cthrd_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.cif (!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_index는 THREAD_ENTRY에 존재하며(기본값 -1), 트랜잭션이 private 리스트를 획득하면 다른 곳에서 할당된다. 이 함수는 그것을 해석할 뿐이다. 워커가 private LRU를 사용하는 조건은 쿼터가 활성화됐고 그리고 인덱스가 -1이 아닌 경우다. anchor 바인딩은 멱등적(!m_holder_anchor 조건 보호)이며 §2.9의 슬라이스에 O(1) 접근을 제공한다. Vacuum 워커와 SA 모드는 false로 처리되어 shared LRU만 사용한다.
2.11 pgbuf_finalize — 해제 순서
섹션 제목: “2.11 pgbuf_finalize — 해제 순서”해제는 초기화의 역순이 아니다. 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_bcbs를 delete; (12) 세 개의 _lrus_with_victims 큐를 delete; (13) show_status를 free하고 뮤텍스 파괴; (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는 올바르게 동작한다.
2.12 챕터 요약 — 핵심 정리
섹션 제목: “2.12 챕터 요약 — 핵심 정리”- 10개의 서브 초기화 함수, 고정된 순서.
pgbuf_initialize는pgbuf_Pool을 필드별로 초기화(atomic은.store)하고 10개의 서브 초기화 함수를 호출한다. 쿼터 파라미터가 먼저(num_private_LRU_list확정), 쿼터/모니터 데이터가 마지막(PGBUF_TOTAL_LRU_COUNT기준 크기 결정). num_buffers는 하한 처리만 할 뿐 오류 검증은 없다(MAX_NTRANS * 10미만이면 조용히 올림). 각 LRU 존 비율은 독립적으로 클램핑된다(lru1은[0.05, 0.90], lru2는 하한0.05에 합산 여유를 남기는 상한). 저장만 할 뿐 즉시 적용하지 않는다.- BCB와 iopage는 병렬 배열로 대칭 교차 연결된다.
next_BCB체인이 invalid 리스트에 상속된다. 32비트 빌드의int dummy패딩이 iopage의 8바이트 정렬을 보장한다. - 페이지 테이블은 고정 2^20 버킷으로, 각 버킷에 자체
hash_mutex가 있다(전역 락 없음). 2의 거듭제곱 크기가 최종 해시 단계를 단순 AND 연산으로 만든다. 락 테이블과 holder 풀은 버퍼 수가 아닌 스레드 수 기준으로 크기가 결정된다. - 모든 BCB는 invalid 리스트에서 시작한다(
invalid_cnt == num_buffers, 모든 플래그PGBUF_INVALID_ZONE) — 동일한 사실을 두 군데 저장. LRU 임계값은 모든 리스트가 비어 있으므로 0에서 시작한다. - 쿼터는 정수 하나가 스위치다 —
num_private_LRU_list(-1자동,0비활성화, 양수는 4로 하한 처리, SA 모드는 0)가PGBUF_PAGE_QUOTA_IS_ENABLED를 구동한다. 워커가 private LRU를 사용하는 조건은 쿼터가 켜져 있고 동시에THREAD_ENTRY.private_lru_index != -1이어야 한다. - 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 스위치에서 반복적으로 등장한다.
3.1 PAGE_FETCH_MODE 일곱 가지 값
섹션 제목: “3.1 PAGE_FETCH_MODE 일곱 가지 값”// PAGE_FETCH_MODE -- src/storage/page_buffer.htypedef 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_BUFFER | pgbuf_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.cif (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.ctry_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_fix가 retry 출력 매개변수를 설정한 채 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_retry는 pgbuf_fix를 감싸는 얇은 래퍼이며, 루프의 일부가
아니다. NULL을 반환하는 동안 pgbuf_fix를 반복 호출하되 er_errid()를
기준으로 분기한다: NO_ERROR/ER_INTERRUPTED는 i를 증가시키지 않고
재시도하고, 세 가지 타임아웃 오류(ER_LK_UNILATERALLY_ABORTED,
ER_LK_PAGE_TIMEOUT, ER_PAGE_LATCH_TIMEDOUT)는 i++를 수행하며, 그 외
오류는 noretry를 설정한다. noretry || i > retry 조건이 충족되면
ER_PAGE_LATCH_ABORTED와 함께 루프를 종료한다. 따라서 인터럽트는 재시도
횟수를 소모하지 않으며, 다른 오류는 즉시 종료된다.
3.4 VPID 해싱
섹션 제목: “3.4 VPID 해싱”페이지 테이블은 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_table은 pgbuf_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.cbufptr = 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_PGPTR로 PAGE_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_next | BCB 해시 체인의 헤드 (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 분기를 세 가지
결과로 이끈다.
-
Hit (
bufptr != NULL):num_hit증가.NEW_PAGE이면, 페이지가 clean-LSA이거나 dirty임을 assert한다(버퍼에 남아 있는 무효화된 페이지를 재사용하는 NEW_PAGE 경우). 제어는pgbuf_bcb_register_fix와 latch 단계로 넘어간다(Chapter 5). -
OLD_PAGE_IF_IN_BUFFERmiss: 이 모드는 디스크에서 읽지 않으므로 해시 mutex를 해제하고return NULL한다 — miss를 단락시키는 유일한 모드이다. -
일반 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_fix와 pgbuf_set_bcb_page_vpid가 실행된 뒤 페이지
정체성을 재검증한다.
// pgbuf_fix_release -- src/storage/page_buffer.cmaybe_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가 최종 결과를 조종하는 마지막 지점이다.
3.8 챕터 요약 — 핵심 정리
섹션 제목: “3.8 챕터 요약 — 핵심 정리”pgbuf_fix_release는 상태 기계이다: 네 개의 조기 반환 검증, 그 다음try_again루프 — 루프의 유일한 재진입 경로는pgbuf_claim_bcb_for_fix의retry출력 매개변수를 통한 BCB 클레임 경쟁 조건이다.- 제로 대기 트랜잭션(
LK_ZERO_WAIT/LK_FORCE_ZERO_WAIT)은 unconditional fix 요청이 해싱 전에 conditional로 다운그레이드된다. - 잠금 없는 빠른 경로(
pgbuf_lockfree_fix_ro)는 unconditional 요청에서 세 가지 허용 모드의 읽기 latch를 담당한다. CAS는 이미 읽기 latch가 걸려 있고 대기자가 없는 BCB에만 독자를 추가하며, VPID를 ABA 방지 차원에서 재검증한다. pgbuf_hash_func_mirror는 volid 하위 8비트를 20비트 공간의 상위 비트 영역으로 비트 반전하고 pageid와 XOR한다.pgbuf_hash_vpid/pgbuf_compare_vpid는 별도의 Aoutmht테이블 전용이다.pgbuf_search_hash_chain은 두 단계이며, 반환 계약(non-NULL이면 BCB mutex 보유, NULL이면 해시 mutex 보유)과 엄격한 해시→BCB 잠금 순서는 핵심 불변 조건이다.- hit/miss 분기: hit → latch 단계(Ch. 5),
OLD_PAGE_IF_IN_BUFFERmiss → 즉시 NULL, 일반 miss → BCB 클레임(Ch. 4) 및 재시도. - 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.cstruct 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_top | Head 포인터; 체인은 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.cstruct 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_page는 hash_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_sleep이 hash_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_victim이NULL반환(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_victim 중 pgbuf_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_page가 success를 설정하면 이 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로 직접 반환된다.
4.7 챕터 요약 — 핵심 정리
섹션 제목: “4.7 챕터 요약 — 핵심 정리”-
pgbuf_claim_bcb_for_fix는 네 가지 출구를 갖는다: trylock 실패 탈출(분기 A),*try_again=true를 설정한 패자 재시도(B), 할당 실패 해제(C), 그리고bufptr->mutex를 보유한 로드된 BCB 반환 성공(D). -
PGBUF 락은 동일 VPID 할당을 단일 승자로 만든다.
pgbuf_lock_page는 한 스레드에 HOLDER를 반환하고 나머지는 레코드별 FIFO에 WAITER로 파킹한다; 승자가 BCB를 삽입하면pgbuf_unlock_page가 모두를 깨우고 이들은 fix를 재구동하여 히트한다. -
pgbuf_allocate_bcb는 invalid 리스트에서 단락 반환한다 (victimize 없이 반환); victim은pgbuf_victimize_bcb를 위해end:로 폴스루한다. 둘 다 실패하면 서버 모드는 네 가지 깨어남 서브 분기(핸드오프, 탈취됨, 인터럽트,r != NO_ERROR타임아웃 단언)와 함께 direct-victim 큐에서 수면하고, SA/복구 모드는 인라인으로 플러시한다. -
pgbuf_get_bcb_from_invalid_list는 이중 검사 잠금을 사용한다: 락 없이 빈 리스트 빠른 반환, mutex 후 재확인 반환, 그리고invalid_top을 전진시키고invalid_cnt를 감소시키며 BCB를 잠그고 VOID로 전환하는 팝 분기. 모든 읽기 오류 경로는pgbuf_put_bcb_into_invalid_list+pgbuf_unlock_page(...,true)로 빠져나가 반쯤 바인딩된 슬롯이 유출되지 않는다. -
DWB는 모든 읽기 미스에서 디스크보다 먼저 참조된다 (
dwb_read_page가success를 설정하면fileio_read제거); NEW_PAGE는 읽기도 실패도 하지 않으며, 영구 페이지를 미성숙(prv.pageid/volid = -1)으로 표시한다.pgbuf_set_bcb_page_vpid는 최초 분기에서만 identity를 설정하고; 재할당된 영구 페이지에서는 바이트를 그대로 두고 저장된 identity가 VPID와 여전히 일치함을 단언한다. -
클레임 경로는 BCB를 VOID에 남긴다. Victim은
pgbuf_get_victim중pgbuf_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.cunion 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;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
raw | compare_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_latch에 compare_exchange_strong 하나로
재시도한다. 따라서 결정 트리 전체가 단 하나의 원자적 전이로 처리되며, 충돌 시 전체를 재계산한다.
불변 조건 — 래치 워드는 원자적으로 전이한다; 부분 게시는 불가능하다. 모든 변경은
do { old = get_impl(); ...build new... } while (!CAS(old, new))패턴을 따른다. 두 번의 개별 store가 있다면, 동시 fixer가latch_mode == READ는 보면서 stalefcnt를 읽어 호환 읽기를 잘못 승인해 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).
5.2 Holder 부기: pgbuf_holder
섹션 제목: “5.2 Holder 부기: pgbuf_holder”pgbuf_holder는 이 스레드가 하나의 BCB에 수행한 fix를 기록한다. 래치 워드는 전역 fix를 세고, holder는 내 몫을 세어 승격(promotion)과 unfix를 로컬에서 추론할 수 있게 한다.
// struct pgbuf_holder -- src/storage/page_buffer.cstruct 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_stat | PGBUF_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_size | fix 호출 지점(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_SET을
malloc해 확장한다. 어느 경로든 holder는 thrd_hold_list에 푸시된다:
// pgbuf_allocate_thrd_holder_entry -- src/storage/page_buffer.cholder->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 == 0과 watch_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.raw를 new_impl.raw로
compare_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.cif (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_LATCH는waiter_exists를 설정하고 거부로 떨어지며, 무조건적 요청은promote_needed를 설정하고 자신의 fix를 차감(new_impl.impl.fcnt -= holder->fix_count)하고waiter_exists를 설정한 뒤 꼬리 대기자로 블록된다(5.4 참고).
상위 수준 동반 문서에서 언급하는 “도달 불가 제자리 업그레이드”는 역사적으로 제거된
경합 상태의 업그레이드를 가리킨다. 단독 독자 제자리 전환(sub-2-2)은 현재 소스에서
도달 가능하며, 전용 승격 진입점 pgbuf_promote_read_latch_debug와
pgbuf_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_LATCH—ER_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_mode와
request_fix_count(깨어날 때 fcnt에 크레딧할 수치)를 기록한 뒤, as_promote 플래그에 따라
삽입 위치를 결정한다:
// pgbuf_block_bcb -- src/storage/page_buffer.ccur_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_sleep과 pgbuf_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.cthread_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으로
재설정된다. 반환 분기는 세 가지다:
NO_ERROR— 신호 수신. 항목을 재잠금한다.resume_status == THREAD_PGBUF_RESUMED이면 깨우기 측이 래치를 승인한 것(5.6) — NO_ERROR 반환(래치는 이미 우리 것이며fcnt는 깨우기 측이 증가시켰다). 그렇지 않으면 인터럽트:request_latch_mode = PGBUF_NO_LATCH설정, 오류 핸들러 호출,ER_INTERRUPTED발생,ER_FAILED반환.ER_CSS_PTHREAD_COND_TIMEDOUT— 타임아웃. 경쟁 상태에서 RESUMED가 됐다면 NO_ERROR 반환. 트랜잭션이 더 이상 활성 상태가 아니면(logtb_is_current_activefalse)try_again으로 루프(커밋/롤백 중인 트랜잭션은 타임아웃시키지 않음). 그 외에는 페이지 래치 교착 희생자: 모드를 저장하고,request_latch_mode = PGBUF_NO_LATCH(깨우기 측이 우리를 건너뛰는 마커)를 설정하고, 오류 핸들러를 실행하고,goto er_set_return.- else —
pthread_cond실패:er_set_with_oserror(ER_CSS_PTHREAD_COND_TIMEDWAIT),ER_FAILED반환.
er_set_return은 원래 대기 사양에 따라 포맷하고 BCB mutex를 해제한 뒤 ER_FAILED를 반환한다:
LK_INFINITE_WAIT—ER_PAGE_LATCH_TIMEDOUT후ER_LK_UNILATERALLY_ABORTED(FIXME 표시assert (0)으로 보호).- 양수
old_wait_msecs—ER_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.cfor (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 ... */}분기별 설명:
-
PGBUF_NO_LATCH대기자 — 포기한 스레드(타임아웃/인터럽트로 아직 자기 제거 안 됨). Unlink하고continue— “타임아웃 대기자 정리”. -
PGBUF_LATCH_FLUSH대기자 — 래치 holder가 아니며, 플러시가 별도로 깨운다.prev_thrd_entry를 전진시키고continue해 제자리에 유지 — “FLUSH 대기자 건너뛰기” 규칙.prev를 전진시켜(unlink 하지 않고) FLUSH 항목을 대기열에 유지하고 그 뒤 후속자들에게 접근 가능하게 한다. -
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,fcnt에request_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.cif (!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_writer는next_wait_thrd를 순회하며PGBUF_LATCH_READ/PGBUF_LATCH_WRITE항목만 센다(FLUSH와 NO_LATCH는 제외). 마지막 R/W 대기자가 깨어난 뒤 stale 비트가 남아 있으면,pgbuf_latch_bcb_upon_fix의 Case 1이 새 독자들을 영구히 블록할 수 있다(유령 기아 현상).
5.7 챕터 요약 — 핵심 정리
섹션 제목: “5.7 챕터 요약 — 핵심 정리”- 래치는
uint64_t하나(pgbuf_atomic_latch_impl):latch_mode | waiter_exists | fcnt(16 + 16 + 32비트). 단일 워드 CAS만이 동시성 환경에서 모드/카운트 트리플을 찢김 없이 유지한다. pgbuf_latch_bcb_upon_fix는compare_exchange_strong하나로 재시도되는 완전 분기 트리다: 유휴 단락; R-on-R(대기자가 있고 호출자가 holder가 아니면 블록 — 쓰기 기아 방지 가드); W-holder 통과(sub-2-1); 단독 독자 제자리 승격(sub-2-2); 외부자(Case 3)와 경합 승격을 위한 블록 경로.- 단독 독자 R→W 제자리 전환(sub-2-2)은 활성 상태이고 도달 가능하다. 소스에 고정된 승격 API는
pgbuf_promote_read_latch_debug와pgbuf_block_bcb의 단일 승격자 단언이다. 경합된 제자리 업그레이드의 어떤 deprecation도 이 소스에는 보이지 않는 외부 프로젝트 역사다. - Holder(
pgbuf_holder)는 스레드별 BCBfix_count를 기록한다. 래치 워드의fcnt와 비교해 “내가 유일한 fixer인가”를 판단한다.allocate/find/remove는 holder를 자유 목록 또는 보유 목록 중 정확히 하나에만 유지하며(next_holder == NULL단언), 동시에 두 곳에 속하지 않는다. - 블록은 그래프 기반이 아닌 타임아웃 기반이다.
pgbuf_block_bcb는next_wait_thrd에 삽입한다 — 일반 대기자와 fix 경로의promote_needed분기는 꼬리, 전용pgbuf_promote_read_latch_debugAPI(as_promote)만 헤드.pgbuf_timed_sleep은pgbuf_latch_timeout을 기다리며, 타임아웃 시 대기자는 교착 희생자가 되고pgbuf_timed_sleep_error_handling이 이를 제거한다(헤드 경우에서는 숨겨진 독자를 깨워 복구). pgbuf_wakeup_reader_writer는 큐를 한 번 순회한다: NO_LATCH 시체 정리, FLUSH 대기자 건너뛰기, 연속된 모든 READ 대기자 깨우기, 최대 하나의 WRITE 승인. 패스 중 독자들이 승인되는 동안 만난 WRITE 대기자는 추가 독자 승인을 막지 않는다 — 보유된 WRITE latch(should_stop)만 순회를 종료한다.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::flags는 volatile 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_MASK와 PGBUF_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_ZONE | 1<<18 | 0x00040000 | 빈/초기화되지 않은 BCB (invalid 리스트) | PGBUF_BCB_INIT_FLAGS; 해제 시 리셋 |
PGBUF_VOID_ZONE | 2<<18 | 0x00080000 | 과도 상태: 디스크에서 읽은 후 리스트 삽입 전, 또는 리스트에서 제거된 후 희생 전 | 디스크 읽기 경로; 희생자 추출 |
PGBUF_LRU_1_ZONE | 1<<16 | 0x00010000 | 가장 뜨거운 LRU 서브존; 희생 대상 아님 | unfix/boost로 zone 1 진입 |
PGBUF_LRU_2_ZONE | 2<<16 | 0x00020000 | hot과 victim 사이의 버퍼 서브존; 희생 대상 아님 | LRU zone 조정 |
PGBUF_LRU_3_ZONE | 3<<16 | 0x00030000 | 희생화 서브존; 후보가 존재하는 유일한 zone | LRU 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_FLAG | 0x80000000 | pgbuf_bcb_set_dirty, _update_flags | _clear_dirty, _mark_is_flushing | pgbuf_bcb_is_dirty | 예 |
PGBUF_BCB_FLUSHING_TO_DISK_FLAG | 0x40000000 | pgbuf_bcb_mark_is_flushing | _mark_was_flushed / _was_not_flushed | pgbuf_bcb_is_flushing | 예 |
PGBUF_BCB_VICTIM_DIRECT_FLAG | 0x20000000 | direct-victim 핸드오프 (9장) | INVALIDATE로 대체 | pgbuf_bcb_is_direct_victim | 예 |
PGBUF_BCB_INVALIDATE_DIRECT_VICTIM_FLAG | 0x10000000 | direct victim을 선점하는 fixer (4/5장) | 대기자가 재요청할 때 | pgbuf_bcb_is_invalid_direct_victim | 예 |
PGBUF_BCB_MOVE_TO_LRU_BOTTOM_FLAG | 0x08000000 | dealloc 경로 | 이를 이동시키는 unfix (7장) | pgbuf_bcb_should_be_moved_to_bottom_lru | 아니오 |
PGBUF_BCB_TO_VACUUM_FLAG | 0x04000000 | pgbuf_notify_vacuum_follows | vacuum 라우팅 | pgbuf_bcb_is_to_vacuum | 아니오 |
PGBUF_BCB_ASYNC_FLUSH_REQ | 0x02000000 | 비동기 플러시 요청자들 | pgbuf_bcb_mark_is_flushing | pgbuf_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_flags와 pgbuf_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가 동일한 워드의 플래그 비트를 뮤텍스 없이 뒤집을 수 있기 때문이다. 분기 구조는 다음과 같다.
is_valid_victim_candidate = (old_flags & INVALID_VICTIM_CANDIDATE_MASK) == 0— 플래그 속성으로, 이동 전후 모두 동일하게 유지된다.- 탈출 (
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). - 진입 (
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.cSTATIC_INLINE PGBUF_ZONEpgbuf_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_dirty는 CAST_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_dirty는 update_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_dirty와 pgbuf_bcb_avoid_victim
섹션 제목: “6.7 읽기 측: pgbuf_bcb_is_dirty와 pgbuf_bcb_avoid_victim”두 함수 모두 동일한 워드에 대한 단일 마스크 술어다 — 락 없이 volatile 읽기만 수행한다.
// pgbuf_bcb_is_dirty -- src/storage/page_buffer.cSTATIC_INLINE boolpgbuf_bcb_is_dirty (const PGBUF_BCB * bcb){ return (bcb->flags & PGBUF_BCB_DIRTY_FLAG) != 0;}
// pgbuf_bcb_avoid_victim -- src/storage/page_buffer.cSTATIC_INLINE boolpgbuf_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 = 0x0000FFFF | pgbuf_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_fix는 1 << 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장은 두 가지 모두를 소비한다.
6.9 챕터 요약 — 핵심 정리
섹션 제목: “6.9 챕터 요약 — 핵심 정리”bcb->flags는 절반에서 분리되는 32비트 워드 하나다: 하위 16비트 = LRU 인덱스(PGBUF_LRU_INDEX_MASK), 상위 16비트 = 플래그 비트(0x80000000..0x02000000)와 zone 선택자(비트 16-19; LRU1..3<<16, INVALID1<<18, VOID2<<18). 플래그 비트와 zone 비트는 분리되어 있어 두 변경자가 서로를 덮어쓰지 않고 동일한 워드를 공유할 수 있다.pgbuf_bcb_update_flags와pgbuf_bcb_change_zone은 하나의 CAS 재시도 루프를 공유한다: 전자는 플래그 비트를 변경하고(조기 no-op 반환 있음)dirties_cnt와 zone-3 후보 자격을 조정한다. 후자는 zone+index를 변경하고 LRU 뮤텍스 하에서 리스트별 zone 카운터를 조정한다. 모든 조정 분기가 실행되어야 한다. 그렇지 않으면 희생자 카운터가 어긋난다.default: assert(false)분기는 LRU BCB가 정확히 zone 1/2/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는 그 중 한 비트를 읽는다. - Dirty 처리는 일반 변경자 대신 수작업 빠른 경로
pgbuf_bcb_set_dirty를 사용한다(DIRTY 세팅 전용 CAS). 후보 자격/dirty 카운터 불변 조건을 인라인으로 유지한다. BCB 쓰기 래치 —pgbuf_set_dirty_buffer_ptr에서 assert됨, CAS에서가 아님 — 가 동시 쓰기자들을 직렬화하여 DIRTY/LSA 쌍을 일관성 있게 유지한다. oldest_unflush_lsa는pgbuf_set_lsa에서 더티 사이클당 정확히 한 번 확립된다.LSA_ISNULL로 가드되며, 절대 앞으로 이동하지 않고, 체크포인트 redo 수평선과 대조 검증된다. 임시 볼륨과 보조 볼륨은 제외된다(센티넬PGBUF_TEMP_LSA = (-2,-2)).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.cCAST_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.cdo { 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이고 대기자가 없을 때만 존 분기에 도달한다.
7.2 Dealloc 단축 경로와 존 분기
섹션 제목: “7.2 Dealloc 단축 경로와 존 분기”// pgbuf_unlatch_bcb_upon_unfix -- src/storage/page_buffer.cif (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_lru는 PGBUF_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이 깨진다.
7.3 존별 분기 추적
섹션 제목: “7.3 존별 분기 추적”모든 LRU 케이스에 공통으로 등장하는 두 가지 가드 조건을 한 번만 인용하고, 이후 세 존 모두에서 참조한다:
// pgbuf_unlatch_bcb_upon_unfix (per-case prologue) -- src/storage/page_buffer.cif (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.cif (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.cint 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_buf와 pgbuf_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.cstruct 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 / prev | FIFO 링크; 재활용 시에는 free-list 링크로 겸용. prev는 히트 시 O(1) 중간 unlink 제공 |
// struct pgbuf_aout_list -- src/storage/page_buffer.cstruct 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_ht | shard 수와 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의 마지막 BCB | add-to-middle: 이 뒤에 삽입 |
bottom_2 | 존 2의 마지막 BCB | 제거 시 복구 필요(존-2 경계 관리, 7.7) |
victim_hint | victim 탐색 시작점 | 제거마다 전진 |
count_lru1/2/3 | 존별 BCB 수 | count_lru2/2 = 충분히 오래됨 임계값 |
threshold_lru1/2 | 존 크기 목표 | pgbuf_lru_adjust_zone* 구동 |
tick_list | add-to-top/middle마다 증가 | boost 나이 클락(PGBUF_IS_BCB_OLD_ENOUGH) |
tick_lru3 | 존 3 낙하마다 증가 | victim-hint 순서 |
index | 리스트 ID | private 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와 비교하는 값이다.
7.7 LRU 삽입 기본 연산과 Boost
섹션 제목: “7.7 LRU 삽입 기본 연산과 Boost”존 2와 존 3의 boost는 pgbuf_lru_boost_bcb를 통한다:
// pgbuf_lru_boost_bcb -- src/storage/page_buffer.cassert (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_middle은 bottom_1(존 1의 마지막) 뒤에 삽입하고 tick_list를 증가시키며 존 2로 표시한다. pgbuf_lru_add_bcb_to_bottom은 bottom에 이어 붙이고 tick_lru3를 스탬프 찍은 뒤 존 3으로 표시한다.
pgbuf_remove_from_lru_list는 역방향 연산으로, BCB를 VOID로 이동하기 전에 모든 경계 포인터를 복구한다:
// pgbuf_remove_from_lru_list -- src/storage/page_buffer.cif (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.cassert (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 */ } }#endifbcb->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와는 구별된다.
7.8 챕터 요약 — 핵심 정리
섹션 제목: “7.8 챕터 요약 — 핵심 정리”- LRU 이동은
is_zero_fcnt && !blocked_reader_writer조건에서만 수행된다. shared-read unfix는pgbuf_lockfree_unfix_ro로 빠져나가 리스트를 건드리지 않는다. 대기자가 없는 마지막 unfixer만 BCB를 재정렬한다. MOVE_TO_LRU_BOTTOM은 dealloc 단축 경로다. 존 분기를 우회하여 해제된 페이지를 즉시 바닥으로 밀어 빠른 재활용을 유도한다.- 존이 기본 동작을 결정한다: 존 1은 절대 boost하지 않는다. 존 2는
PGBUF_IS_BCB_OLD_ENOUGH(count_lru2의 절반 이상 지났을 때)일 때만 boost한다. 존 3은 실제 unfix에서 항상 boost하거나, ignore-unfix 분기에서 direct victim을 넘긴다. PGBUF_SHOULD_IGNORE_UNFIX는 vacuum-OR-임시볼륨 조건이며, vacuum 전용이 아니다: 임시 볼륨 페이지도 캐시 워밍에서 제외된다. VOID 경로에서는 이를 vacuum 워커 전용인PGBUF_VACUUM_SHOULD_IGNORE_UNFIX로 좁힌다.- Boost =
pgbuf_remove_from_lru_list+pgbuf_lru_add_bcb_to_top— list mutex 하에서 수행된다. add-to-top은tick_list를 증가시켜 다른 모든 BCB의 “충분히 오래됨” 판단 기준을 갱신한다. 제거된 BCB는 항상 VOID로 착지하여 존 필드가 소속 리스트와 일치하도록 유지한다. pgbuf_should_move_private_to_shared는 두 가지 트리거로 발동된다: 외부 스레드 unfix(즉시), 또는 같은 리스트에서 hot(64회 포화 fix 이상)이고 충분히 오래된 페이지 — 경합이 많은 페이지를 shared 풀로 올린다.- 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에서 다뤘으며, 이 챕터는 플러시 경로가 그것들을 어떻게 소비하는지 보여 준다.
8.1 단일 페이지 진입점
섹션 제목: “8.1 단일 페이지 진입점”네 개의 공개 진입점이 한 페이지를 디스크 방향으로 밀어 넣는다. 모두 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.cCAST_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.cif (!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-latch | pgbuf_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를 설정하고, DIRTY와 ASYNC_FLUSH_REQ를 원자적으로 해제한다.
// pgbuf_bcb_mark_is_flushing -- src/storage/page_buffer.cif (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_PAGESIZE의 memcpy). 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_lsa를 NULL화한 뒤 뮤텍스를 해제한다. 저장된 oldest_unflush_lsa가 non-null이면 로그를 강제 플러시한다.
// pgbuf_bcb_flush_with_wal -- src/storage/page_buffer.cLSA_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분기(nulloldest_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_page는 dwb_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.cfor (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_cnt와 fix_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.cif (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를 잠그고 네 가지 가드를 적용한다.
- VPID 변경됨 / 더티 아님 / 이미 플러시 중 ->
num_skipped_already_flushed, 잠금 해제 후 계속. - LRU 피해자 구역을 벗어났거나 fixed/hot 상태 ->
num_skipped_fixed_or_hot, 잠금 해제 후 계속. logpb_need_wal(페이지 LSA가 플러시된 로그를 초과) -> 최대lsa_need_wal기록,count_need_wal증가, 로그 플러시 데몬 깨움,num_skipped_need_wal, 잠금 해제 후 계속.- 그 외 플러시:
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.cstruct 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.6 pgbuf_get_victim_candidates_from_lru
섹션 제목: “8.6 pgbuf_get_victim_candidates_from_lru”8.5에서 호출되는 이 함수는 각 LRU의 아래쪽에서 피해자 구역을 따라 걸으며, 각 리스트의 lru_victim_flush_priority_per_lru로 예산을 배분한다.
// pgbuf_get_victim_candidates_from_lru -- src/storage/page_buffer.cfor (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_list | PGBUF_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_size | flush_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.cPGBUF_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_daemon | entry_callable_task (pgbuf_page_post_flush_execute) | 3단계 루퍼 {1,10,100} ms | pgbuf_assign_flushed_pages — flushed_bcbs 드레인, 직접 피해자 할당 |
pgbuf_Page_maintenance_daemon | entry_callable_task (pgbuf_page_maintenance_execute) | 고정 100 ms | pgbuf_adjust_quotas + pgbuf_direct_victims_maintenance |
플러시 데몬은 명시적으로 깨워진 경우(was_woken_up) 최소 한 번 실행한 뒤, pgbuf_keep_victim_flush_thread_running이 true인 동안 또는 pgbuf_flush_victim_candidates가 stop_iteration을 설정할 때까지 루프를 돈다.
// pgbuf_page_flush_daemon_task::execute -- src/storage/page_buffer.cbool 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_cnt가 num_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를 페이싱 없이 스윕하며, 로그/복구 매니저만 사용한다.
8.9 챕터 요약 — 핵심 정리
섹션 제목: “8.9 챕터 요약 — 핵심 정리”- 모든 단일 페이지 플러시는
pgbuf_bcb_safe_flush_internal로 모이며, 이 함수의 atomic latch CAS 루프가 즉각 플러시, unfix 시 async 요청, 또는PGBUF_LATCH_FLUSHblock 중 하나를 선택한다. pgbuf_bcb_flush_with_wal은 뮤텍스 하에서 페이지 LSA를 저장하고oldest_unflush_lsa를 NULL화한 뒤 뮤텍스를 해제하고,fileio_write/dwb_add_page이전에logpb_flush_log_for_wal (.., &lsa)를 호출함으로써 WAL 불변 조건을 강제한다. 강제를 생략하면 이미 영속된 페이지의 redo가 소실된다.- DIRTY는 플러시 시작 시(
pgbuf_bcb_mark_is_flushing) 해제되므로 동시 re-dirty는 절대 유실되지 않는다. 실패한 쓰기는pgbuf_bcb_mark_was_not_flushed로 DIRTY와 저장된oldest_unflush_lsa를 복구한다. - BCB당 최대 하나의 플러셔:
FLUSHING_TO_DISK플래그와 FLUSH-latch 블로킹 경로가 쓰기를 직렬화하여 이전 이미지가 최신 이미지 위에 덮이는 상황을 방지한다. - 페이지 플러시 데몬(
pgbuf_flush_victim_candidates+pgbuf_get_victim_candidates_from_lru)은 LRU 하단의 더티 후보를 수집하고, fixed/hot/WAL 필요 페이지를 스킵하며, 공유 전역pgbuf_Flush_helper윈도로 이웃을 배치 플러시할 수 있다(전체 구간 WAL을 한 번 강제하며, 순차성을 위해 내부 클린 페이지도 포함 가능). 모든 후보가 WAL 차단된 경우 한 번 재시도한다. - 체크포인트는 속도 제어되는
PGBUF_SEQ_FLUSHER(pgbuf_flush_seq_list)를 사용하며 burst/spread 페이싱과flush_upto_lsaWAL 게이트를 갖는다.flush_all*은 페이싱 없는 스윕이다. - 세 개의 데몬이 존재한다 — 하나의 전용 클래스(페이지 플러시)와 두 개의 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.cstruct 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.cstruct 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_victims | thread_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.cif (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 리스트 수를 초과, 또는 nloops가 num_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.cif (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 — avoid-victim 플래그.
pgbuf_bcb_avoid_victim이PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK=DIRTY | FLUSHING_TO_DISK | VICTIM_DIRECT | INVALIDATE_DIRECT_VICTIM(네 배제 조건: dirty, 플러시 중, 이미 배정, 무효화 대기)을 검사한다. 비트가 하나라도 켜져 있으면continue. - 배제 2 — 고정됨/대기자 있음.
pgbuf_is_bcb_fixed_by_any (bufptr, false):fcnt > 0,next_wait_thrd != NULL, 또는 latch 보유 시 유효하지만 바쁜 상태다.bufptr_victimizable(첫 번째는 CAS로 힌트가 됨)로 기록하고 카운트 후continue.found_victim_cnt가lru_victim_cnt에 도달하면 break. - 확보 —
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()을 단언한다. 기록 + 힌트, 카운트, 조기 종료 준수.
- Trylock 성공 +
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.cif (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.cwhile (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_victim이 75/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.cPGBUF_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의 재큐잉 테스트가 읽는 쿼터/임계값은 모두 여기서 비롯된다.
9.9 챕터 요약 — 핵심 정리
섹션 제목: “9.9 챕터 요약 — 핵심 정리”pgbuf_get_victim은 네 단계 우선순위 스캔이다: 자신의 private(쿼터 초과) → 다른 private(big 우선, restricted가 아니면 ordinary도) → shared(플러시 데몬 없을 때만 루프) → 자신의 private 쿼터 무시(교착상태 회피 최후 수단).NULL반환은 “sleep하고 직접 피해자를 기다려라”를 의미한다.- LFCQ는 인덱스를 보관하며 BCB를 직접 저장하지 않는다. 리스트는 후보가 있고
PGBUF_LRU_VICTIM_LFCQ_FLAG가 설정된 경우에만 큐에 들어간다. 소비자는 쿼터 초과 후보가 있으면 재큐잉하고, 아니면 플래그를 지워 다음 후보가 추가될 때 재진입하게 한다. - 쿼터 게이팅이 working set을 보호한다. private 리스트는 쿼터를 초과하는 동안에만 퇴출 가능하다. big-private(
>100,>2*quota, 후보>1) 리스트는 스캔 전에 재큐잉되어 병렬 드레인을 허용한다. pgbuf_get_victim_from_lru_list는 모든 후보를 BCB lock 하에 재검증한다.PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK의 네 배제 조건을 적용한다.TO_VACUUM은 의도적으로 배제 조건에서 제외된다.victim_hint는 성능 힌트이지 안전 속성이 아니다. 문서화된 드리프트는 스캔 단계만 낭비할 뿐이다. 정확성은 전적으로 lock 하의 BCB별 재검증에서 온다.- 직접 전달은 메일박스 프로토콜이다. 생산자는 대기자를 선택하고(고우선순위/저우선순위 75/25),
VICTIM_DIRECT를 설정한 뒤 대기자의 entry lock 하에bcb_victims[tid]를 기록하고 깨운다. - 소비자는 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_quota와 pgbuf_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_list | private 리스트 수; PGBUF_PAGE_QUOTA_IS_ENABLED는 > 0 | 마스터 활성 스위치; 0이면 서브시스템이 비활성화됨 |
lru_victim_flush_priority_per_lru | 리스트별 float, flush 우선순위 | flush 데몬(Ch 8)이 쿼터 초과 리스트 쪽으로 flushing을 편향시킴 |
private_lru_session_cnt | private 리스트별 활성 세션 수 | pgbuf_assign_private_lru가 세션 수 0인 리스트를 우선 선택 |
private_pages_ratio | private이어야 할 BCB의 비율 | all_private_quota를 유도하는 평활화된 목표값 |
add_shared_lru_idx | shared 리스트 이동 시 사용하는 순환 인덱스 | private-to-shared 마이그레이션 시 shared LRU 배정을 라운드로빈 |
avoid_shared_lru_idx | 이동 시 피할 shared 리스트 | 가장 큰 리스트로의 트래픽을 분산시켜 victimization으로 소진되도록 유도 |
last_adjust_time | 마지막 조정의 TSC_TICKS | 1 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 / prev | holder의 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_rank와 group_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_watcher는 PGBUF_HOLDER의 first_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_quotas와 pgbuf_direct_victims_maintenance(10.3)를
순서대로 호출한다. 주기 게이트와 조기 반환:
- 비활성화 / 이미 실행 중.
if (!PGBUF_PAGE_QUOTA_IS_ENABLED || quota->is_adjusting) return;아니면is_adjusting = 1. - 너무 이름 (< 1 ms).
diff_usec < 1000이면 가드를 해제하고 반환. - 낮은 활동량이고 < 500 ms.
pg_unfix_cnt < PGBUF_TRAN_THRESHOLD_ACTIVITY(num_buffers/4) 이고 경과 시간 < 500 ms이면 반환. 바쁜 풀은 ~1 ms마다, 유휴 풀은 500 ms마다 조정. - 매우 낮은 활동 플래그.
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.cnew_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.cstatic 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_rank는 PGBUF_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_WAIT→ER_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_unfix를 fix_count번 실행, 각 watcher를 PGBUF_CLEAR_WATCHER하고
pg_watcher->page_was_unfixed = true로 표시.
Branch 4 — 누락된 그룹 확인 후 정렬. 요청에 그룹이 없으면 무조건 재픽스하여
group id를 도출(OLD_PAGE_PREVENT_DEALLOC이 OLD_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_watcher와 pgbuf_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로 재시도한다.
10.7 나머지 특수 픽스 경로
섹션 제목: “10.7 나머지 특수 픽스 경로”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_VALID→OLD_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 테넌트가 있음을 알 수 있도록 여기에 명시한다.
10.8 챕터 요약 — 핵심 정리
섹션 제목: “10.8 챕터 요약 — 핵심 정리”- 100 ms Page Maintenance Daemon은
pgbuf_adjust_quotas와pgbuf_direct_victims_maintenance(유휴 victim 백업)를 순서대로 실행한다; 전자는 1 ms / 500 ms 주기와num_buffers/4활동량으로 게이팅되며, 단일 writeris_adjusting플래그(모든 종료 경로에서 반드시 해제)로 보호되고, 리스트별quota/threshold_lru1/2(PGBUF_PRIVATE_LRU_MAX_HARD_QUOTA및 풀의 절반으로 cap)와victim_rich(풀의 10% 초과 시)를 설정한다. - 정렬 픽스는 페이지를 rank(heap-hdr < normal < overflow)로 순위를 매기고 VPID로 정렬한다; 요청보다 나중에 정렬되는 보유 페이지는 unfix되고, 집합을
qsort한 뒤 모두 무조건 재픽스한다. 핵심 출력인page_was_unfixed는 호출자가 해당 페이지를 반드시 다시 읽어야 함을 의미한다(캐시된 포인터가 낡았을 수 있음). - 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한다. - 각 특수 경로는 한 단계를 우회한다:
pgbuf_simple_fix(latchless 임시 페이지),pgbuf_fix_if_not_deallocated(해제됨 = NULL, 오류 아님),pgbuf_invalidate(단일 픽스 페이지 분리),pgbuf_notify_vacuum_follows(일회성 anti-victim 힌트); TDEpflag테넌트는 범위 외 경계다.
이 리비전 시점의 위치 힌트
섹션 제목: “이 리비전 시점의 위치 힌트”아래 줄 번호는 2026-06-17 시점에 관찰한 값이다. 심볼 이름이 정식 앵커이며 줄 번호는 시간이 지나면 어긋나는 힌트다.
| Symbol | File | Line |
|---|---|---|
dwb_set_data_on_next_slot | src/storage/double_write_buffer.cpp | 2686 |
dwb_add_page | src/storage/double_write_buffer.cpp | 2726 |
dwb_is_created | src/storage/double_write_buffer.cpp | 2909 |
fileio_page_reserved | src/storage/file_io.h | 166 |
fileio_page_watermark | src/storage/file_io.h | 179 |
fileio_page | src/storage/file_io.h | 186 |
PGBUF_DEFAULT_FIX_COUNT | src/storage/page_buffer.c | 90 |
PGBUF_NUM_ALLOC_HOLDER | src/storage/page_buffer.c | 94 |
PGBUF_FIX_COUNT_THRESHOLD | src/storage/page_buffer.c | 106 |
pgbuf_latch_timeout | src/storage/page_buffer.c | 107 |
PGBUF_IOPAGE_BUFFER_SIZE | src/storage/page_buffer.c | 118 |
PGBUF_FIND_BCB_PTR | src/storage/page_buffer.c | 135 |
PGBUF_LRU_NBITS | src/storage/page_buffer.c | 148 |
PGBUF_LRU_INDEX_MASK | src/storage/page_buffer.c | 150 |
PGBUF_LRU_INDEX_MASK | src/storage/page_buffer.c | 182 |
PGBUF_LRU_1_ZONE | src/storage/page_buffer.c | 197 |
PGBUF_LRU_ZONE_MASK | src/storage/page_buffer.c | 201 |
PGBUF_INVALID_ZONE | src/storage/page_buffer.c | 205 |
PGBUF_VOID_ZONE | src/storage/page_buffer.c | 206 |
PGBUF_ZONE_MASK | src/storage/page_buffer.c | 211 |
PGBUF_GET_ZONE | src/storage/page_buffer.c | 215 |
PGBUF_GET_LRU_INDEX | src/storage/page_buffer.c | 216 |
PGBUF_BCB_DIRTY_FLAG | src/storage/page_buffer.c | 224 |
PGBUF_BCB_FLUSHING_TO_DISK_FLAG | src/storage/page_buffer.c | 227 |
PGBUF_BCB_VICTIM_DIRECT_FLAG | src/storage/page_buffer.c | 234 |
PGBUF_BCB_INVALIDATE_DIRECT_VICTIM_FLAG | src/storage/page_buffer.c | 235 |
PGBUF_BCB_MOVE_TO_LRU_BOTTOM_FLAG | src/storage/page_buffer.c | 237 |
PGBUF_BCB_TO_VACUUM_FLAG | src/storage/page_buffer.c | 239 |
PGBUF_BCB_ASYNC_FLUSH_REQ | src/storage/page_buffer.c | 241 |
PGBUF_BCB_FLAGS_MASK | src/storage/page_buffer.c | 244 |
PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK | src/storage/page_buffer.c | 258 |
PGBUF_BCB_INIT_FLAGS | src/storage/page_buffer.c | 265 |
PGBUF_BCB_COUNT_FIX_SHIFT_BITS | src/storage/page_buffer.c | 268 |
PGBUF_BCB_AVOID_DEALLOC_MASK | src/storage/page_buffer.c | 269 |
PGBUF_TRAN_THRESHOLD_ACTIVITY | src/storage/page_buffer.c | 276 |
PGBUF_AOUT_NOT_FOUND | src/storage/page_buffer.c | 279 |
PGBUF_SHOULD_IGNORE_UNFIX | src/storage/page_buffer.c | 290 |
HASH_SIZE_BITS | src/storage/page_buffer.c | 295 |
PGBUF_HASH_SIZE | src/storage/page_buffer.c | 296 |
PGBUF_HASH_VALUE | src/storage/page_buffer.c | 300 |
PGBUF_FLUSH_VICTIM_BOOST_MULT | src/storage/page_buffer.c | 305 |
PGBUF_NEIGHBOR_FLUSH_NONDIRTY | src/storage/page_buffer.c | 307 |
PGBUF_MAX_NEIGHBOR_PAGES | src/storage/page_buffer.c | 310 |
PGBUF_NEIGHBOR_POS | src/storage/page_buffer.c | 314 |
PGBUF_CHKPT_MAX_FLUSH_RATE | src/storage/page_buffer.c | 322 |
PGBUF_CHKPT_MIN_FLUSH_RATE | src/storage/page_buffer.c | 323 |
PGBUF_CHKPT_BURST_PAGES | src/storage/page_buffer.c | 326 |
PGBUF_LRU_ZONE_MIN_RATIO | src/storage/page_buffer.c | 342 |
PGBUF_LOCK_HOLDER | src/storage/page_buffer.c | 348 |
pgbuf_holder_stat | src/storage/page_buffer.c | 441 |
pgbuf_batch_flush_helper | src/storage/page_buffer.c | 451 |
pgbuf_holder | src/storage/page_buffer.c | 461 |
pgbuf_holder_anchor | src/storage/page_buffer.c | 479 |
pgbuf_holder_set | src/storage/page_buffer.c | 488 |
pgbuf_atomic_latch_impl | src/storage/page_buffer.c | 494 |
pgbuf_bcb | src/storage/page_buffer.c | 506 |
atomic_latch | src/storage/page_buffer.c | 513 |
flags | src/storage/page_buffer.c | 514 |
next_wait_thrd | src/storage/page_buffer.c | 516 |
count_fix_and_avoid_dealloc | src/storage/page_buffer.c | 528 |
oldest_unflush_lsa | src/storage/page_buffer.c | 536 |
pgbuf_iopage_buffer | src/storage/page_buffer.c | 541 |
struct pgbuf_iopage_buffer | src/storage/page_buffer.c | 541 |
pgbuf_buffer_lock | src/storage/page_buffer.c | 557 |
struct pgbuf_buffer_lock | src/storage/page_buffer.c | 557 |
pgbuf_buffer_hash | src/storage/page_buffer.c | 570 |
pgbuf_lru_list | src/storage/page_buffer.c | 580 |
victim_hint | src/storage/page_buffer.c | 589 |
count_vict_cand | src/storage/page_buffer.c | 602 |
pgbuf_invalid_list | src/storage/page_buffer.c | 621 |
struct pgbuf_invalid_list | src/storage/page_buffer.c | 621 |
pgbuf_aout_buf | src/storage/page_buffer.c | 636 |
struct pgbuf_aout_list | src/storage/page_buffer.c | 645 |
pgbuf_aout_list | src/storage/page_buffer.c | 645 |
pgbuf_seq_flusher | src/storage/page_buffer.c | 669 |
struct pgbuf_page_monitor | src/storage/page_buffer.c | 688 |
struct pgbuf_page_quota | src/storage/page_buffer.c | 710 |
pgbuf_page_quota | src/storage/page_buffer.c | 710 |
struct pgbuf_direct_victim | src/storage/page_buffer.c | 737 |
pgbuf_buffer_pool | src/storage/page_buffer.c | 749 |
struct pgbuf_victim_candidate_list | src/storage/page_buffer.c | 833 |
pgbuf_Flush_helper | src/storage/page_buffer.c | 840 |
AOUT_HASH_IDX | src/storage/page_buffer.c | 854 |
PGBUF_BCB_LOCK | src/storage/page_buffer.c | 869 |
PGBUF_BCB_TRYLOCK | src/storage/page_buffer.c | 871 |
PGBUF_IS_BCB_IN_LRU_VICTIM_ZONE | src/storage/page_buffer.c | 919 |
PGBUF_IS_BCB_IN_LRU | src/storage/page_buffer.c | 920 |
PGBUF_IS_BCB_OLD_ENOUGH | src/storage/page_buffer.c | 927 |
PGBUF_PRIVATE_LRU_MAX_HARD_QUOTA | src/storage/page_buffer.c | 943 |
PGBUF_MIN_PAGES_IN_SHARED_LIST | src/storage/page_buffer.c | 946 |
PGBUF_TOTAL_LRU_COUNT | src/storage/page_buffer.c | 969 |
PGBUF_IS_PRIVATE_LRU_INDEX | src/storage/page_buffer.c | 975 |
PGBUF_LRU_LIST_IS_OVER_QUOTA | src/storage/page_buffer.c | 977 |
PGBUF_LRU_LIST_IS_OVER_QUOTA_WITH_BUFFER | src/storage/page_buffer.c | 987 |
set_latch | src/storage/page_buffer.c | 1310 |
add_fcnt | src/storage/page_buffer.c | 1324 |
set_waiter_exists | src/storage/page_buffer.c | 1368 |
get_latch | src/storage/page_buffer.c | 1398 |
get_impl | src/storage/page_buffer.c | 1406 |
pgbuf_thread_variables_init | src/storage/page_buffer.c | 1415 |
pgbuf_hash_func_mirror | src/storage/page_buffer.c | 1441 |
pgbuf_hash_vpid | src/storage/page_buffer.c | 1480 |
pgbuf_compare_vpid | src/storage/page_buffer.c | 1494 |
pgbuf_initialize | src/storage/page_buffer.c | 1518 |
pgbuf_finalize | src/storage/page_buffer.c | 1796 |
pgbuf_fix_with_retry | src/storage/page_buffer.c | 1993 |
pgbuf_fix_release | src/storage/page_buffer.c | 2041 |
pgbuf_simple_fix | src/storage/page_buffer.c | 2475 |
pgbuf_simple_unfix | src/storage/page_buffer.c | 2569 |
pgbuf_promote_read_latch_debug | src/storage/page_buffer.c | 2624 |
pgbuf_promote_read_latch_release | src/storage/page_buffer.c | 2628 |
pgbuf_unfix | src/storage/page_buffer.c | 2850 |
pgbuf_invalidate | src/storage/page_buffer.c | 3158 |
pgbuf_flush | src/storage/page_buffer.c | 3341 |
pgbuf_flush_with_wal | src/storage/page_buffer.c | 3364 |
pgbuf_flush_if_requested | src/storage/page_buffer.c | 3404 |
pgbuf_flush_all_helper | src/storage/page_buffer.c | 3438 |
pgbuf_get_victim_candidates_from_lru | src/storage/page_buffer.c | 3564 |
pgbuf_flush_victim_candidates | src/storage/page_buffer.c | 3645 |
pgbuf_flush_checkpoint | src/storage/page_buffer.c | 3960 |
pgbuf_flush_chkpt_seq_list | src/storage/page_buffer.c | 4102 |
pgbuf_flush_seq_list | src/storage/page_buffer.c | 4210 |
pgbuf_set_dirty | src/storage/page_buffer.c | 4700 |
pgbuf_set_lsa | src/storage/page_buffer.c | 4771 |
pgbuf_set_tde_algorithm | src/storage/page_buffer.c | 4881 |
pgbuf_set_bcb_page_vpid | src/storage/page_buffer.c | 5214 |
pgbuf_initialize_bcb_table | src/storage/page_buffer.c | 5334 |
pgbuf_initialize_hash_table | src/storage/page_buffer.c | 5452 |
pgbuf_initialize_lock_table | src/storage/page_buffer.c | 5481 |
pgbuf_initialize_lru_list | src/storage/page_buffer.c | 5519 |
pgbuf_initialize_aout_list | src/storage/page_buffer.c | 5582 |
pgbuf_initialize_invalid_list | src/storage/page_buffer.c | 5686 |
pgbuf_initialize_thrd_holder | src/storage/page_buffer.c | 5701 |
pgbuf_allocate_thrd_holder_entry | src/storage/page_buffer.c | 5783 |
pgbuf_find_thrd_holder | src/storage/page_buffer.c | 5870 |
pgbuf_remove_thrd_holder | src/storage/page_buffer.c | 5971 |
pgbuf_latch_bcb_upon_fix | src/storage/page_buffer.c | 6073 |
pgbuf_unlatch_bcb_upon_unfix | src/storage/page_buffer.c | 6417 |
pgbuf_unlatch_void_zone_bcb | src/storage/page_buffer.c | 6652 |
pgbuf_should_move_private_to_shared | src/storage/page_buffer.c | 6758 |
pgbuf_block_bcb | src/storage/page_buffer.c | 6803 |
pgbuf_timed_sleep_error_handling | src/storage/page_buffer.c | 6925 |
pgbuf_timed_sleep | src/storage/page_buffer.c | 7014 |
pgbuf_wakeup_reader_writer | src/storage/page_buffer.c | 7186 |
pgbuf_search_hash_chain | src/storage/page_buffer.c | 7327 |
pgbuf_lockfree_fix_ro | src/storage/page_buffer.c | 7452 |
pgbuf_search_hash_chain_no_bcb_lock | src/storage/page_buffer.c | 7517 |
pgbuf_insert_into_hash_chain | src/storage/page_buffer.c | 7569 |
pgbuf_lock_page | src/storage/page_buffer.c | 7718 |
pgbuf_unlock_page | src/storage/page_buffer.c | 7831 |
pgbuf_allocate_bcb | src/storage/page_buffer.c | 7916 |
pgbuf_claim_bcb_for_fix | src/storage/page_buffer.c | 8133 |
pgbuf_victimize_bcb | src/storage/page_buffer.c | 8372 |
pgbuf_invalidate_bcb | src/storage/page_buffer.c | 8424 |
pgbuf_bcb_safe_flush_force_unlock | src/storage/page_buffer.c | 8494 |
pgbuf_bcb_safe_flush_force_lock | src/storage/page_buffer.c | 8517 |
pgbuf_bcb_safe_flush_internal | src/storage/page_buffer.c | 8550 |
pgbuf_get_bcb_from_invalid_list | src/storage/page_buffer.c | 8644 |
pgbuf_put_bcb_into_invalid_list | src/storage/page_buffer.c | 8693 |
pgbuf_get_victim | src/storage/page_buffer.c | 8805 |
pgbuf_is_bcb_fixed_by_any | src/storage/page_buffer.c | 8995 |
pgbuf_is_bcb_victimizable | src/storage/page_buffer.c | 9023 |
pgbuf_get_victim_from_lru_list | src/storage/page_buffer.c | 9053 |
pgbuf_panic_assign_direct_victims_from_lru | src/storage/page_buffer.c | 9279 |
pgbuf_direct_victims_maintenance | src/storage/page_buffer.c | 9346 |
pgbuf_lfcq_assign_direct_victims | src/storage/page_buffer.c | 9388 |
pgbuf_lru_add_bcb_to_top | src/storage/page_buffer.c | 9432 |
pgbuf_lru_add_bcb_to_middle | src/storage/page_buffer.c | 9482 |
pgbuf_lru_add_bcb_to_bottom | src/storage/page_buffer.c | 9570 |
pgbuf_lru_fall_bcb_to_zone_3 | src/storage/page_buffer.c | 9788 |
pgbuf_lru_boost_bcb | src/storage/page_buffer.c | 9858 |
pgbuf_lru_move_from_private_to_shared | src/storage/page_buffer.c | 10064 |
pgbuf_remove_from_lru_list | src/storage/page_buffer.c | 10089 |
pgbuf_move_bcb_to_bottom_lru | src/storage/page_buffer.c | 10157 |
pgbuf_add_vpid_to_aout_list | src/storage/page_buffer.c | 10201 |
pgbuf_remove_vpid_from_aout_list | src/storage/page_buffer.c | 10282 |
pgbuf_bcb_flush_with_wal | src/storage/page_buffer.c | 10456 |
pgbuf_wake_flush_waiters | src/storage/page_buffer.c | 10694 |
pgbuf_is_exist_blocked_reader_writer | src/storage/page_buffer.c | 10741 |
pgbuf_wakeup | src/storage/page_buffer.c | 11319 |
pgbuf_set_dirty_buffer_ptr | src/storage/page_buffer.c | 11369 |
pgbuf_flush_page_and_neighbors_fb | src/storage/page_buffer.c | 11527 |
pgbuf_flush_neighbor_safe | src/storage/page_buffer.c | 11762 |
pgbuf_add_bufptr_to_batch | src/storage/page_buffer.c | 11820 |
pgbuf_ordered_fix_release | src/storage/page_buffer.c | 11985 |
pgbuf_ordered_unfix | src/storage/page_buffer.c | 12860 |
pgbuf_add_watch_instance_internal | src/storage/page_buffer.c | 12927 |
pgbuf_initialize_page_quota_parameters | src/storage/page_buffer.c | 13326 |
pgbuf_initialize_page_quota | src/storage/page_buffer.c | 13370 |
pgbuf_initialize_page_monitor | src/storage/page_buffer.c | 13430 |
pgbuf_adjust_quotas | src/storage/page_buffer.c | 13639 |
pgbuf_initialize_seq_flusher | src/storage/page_buffer.c | 14016 |
pgbuf_flush_control_from_dirty_ratio | src/storage/page_buffer.c | 14233 |
pgbuf_fix_if_not_deallocated_with_caller | src/storage/page_buffer.c | 14735 |
pgbuf_assign_direct_victim | src/storage/page_buffer.c | 14809 |
pgbuf_assign_flushed_pages | src/storage/page_buffer.c | 14876 |
pgbuf_get_thread_waiting_for_direct_victim | src/storage/page_buffer.c | 14946 |
pgbuf_get_direct_victim | src/storage/page_buffer.c | 14978 |
pgbuf_lru_advance_victim_hint | src/storage/page_buffer.c | 15131 |
pgbuf_bcb_update_flags | src/storage/page_buffer.c | 15171 |
pgbuf_bcb_change_zone | src/storage/page_buffer.c | 15269 |
pgbuf_bcb_get_zone | src/storage/page_buffer.c | 15374 |
pgbuf_bcb_get_zone | src/storage/page_buffer.c | 15375 |
pgbuf_bcb_get_lru_index | src/storage/page_buffer.c | 15386 |
pgbuf_bcb_get_lru_index | src/storage/page_buffer.c | 15387 |
pgbuf_bcb_is_dirty | src/storage/page_buffer.c | 15400 |
pgbuf_bcb_set_dirty | src/storage/page_buffer.c | 15412 |
pgbuf_bcb_mark_is_flushing | src/storage/page_buffer.c | 15463 |
pgbuf_bcb_mark_was_flushed | src/storage/page_buffer.c | 15486 |
pgbuf_bcb_mark_was_not_flushed | src/storage/page_buffer.c | 15500 |
pgbuf_bcb_is_flushing | src/storage/page_buffer.c | 15513 |
pgbuf_bcb_should_be_moved_to_bottom_lru | src/storage/page_buffer.c | 15561 |
pgbuf_notify_vacuum_follows | src/storage/page_buffer.c | 15574 |
pgbuf_bcb_is_to_vacuum | src/storage/page_buffer.c | 15589 |
pgbuf_bcb_avoid_victim | src/storage/page_buffer.c | 15603 |
pgbuf_bcb_register_avoid_deallocation | src/storage/page_buffer.c | 15627 |
pgbuf_bcb_unregister_avoid_deallocation | src/storage/page_buffer.c | 15640 |
pgbuf_bcb_should_avoid_deallocation | src/storage/page_buffer.c | 15684 |
pgbuf_bcb_register_fix | src/storage/page_buffer.c | 15720 |
pgbuf_bcb_is_hot | src/storage/page_buffer.c | 15741 |
pgbuf_lfcq_get_victim_from_private_lru | src/storage/page_buffer.c | 15802 |
pgbuf_lfcq_get_victim_from_shared_lru | src/storage/page_buffer.c | 15894 |
pgbuf_bcb_register_hit_for_lru | src/storage/page_buffer.c | 15979 |
pgbuf_get_page_flush_interval | src/storage/page_buffer.c | 16353 |
pgbuf_page_maintenance_execute | src/storage/page_buffer.c | 16375 |
pgbuf_page_flush_daemon_task | src/storage/page_buffer.c | 16396 |
pgbuf_page_maintenance_daemon_init | src/storage/page_buffer.c | 16531 |
pgbuf_page_flush_daemon_init | src/storage/page_buffer.c | 16549 |
pgbuf_page_post_flush_daemon_init | src/storage/page_buffer.c | 16567 |
pgbuf_is_page_flush_daemon_available | src/storage/page_buffer.c | 16673 |
pgbuf_is_temp_lsa | src/storage/page_buffer.c | 16683 |
pgbuf_init_temp_page_lsa | src/storage/page_buffer.c | 16689 |
PAGE_FETCH_MODE | src/storage/page_buffer.h | 172 |
PGBUF_LATCH_MODE | src/storage/page_buffer.h | 190 |
PGBUF_ORDERED_RANK | src/storage/page_buffer.h | 222 |
pgbuf_watcher | src/storage/page_buffer.h | 234 |
PGBUF_TEMP_LSA | src/storage/page_buffer.h | 258 |
PGBUF_ATOMIC_LATCH | src/storage/page_buffer.h | 365 |
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.