CUBRID File & Disk Manager — 코드 수준 심층 분석
이 문서의 위치: 상위 분석 문서인
cubrid-disk-manager.md는 파일 매니저와 디스크 매니저 양쪽의 설계 의도 및 이론적 배경을 다룬다. 이 문서는file_manager.c를 중심으로 디스크 매니저를 기반 계층으로 삼아 모든 분기와 필드를 코드 수준에서 추적한다. 각 장은 독립적으로 읽을 수 있지만, 순서대로 읽으면 단일 데이터 페이지가 예약된 섹터에서 해당 파일의 소유 페이지가 되기까지의 전체 생명주기를 커널 내부에서 따라갈 수 있다.
목차:
Chapter 1: 자료구조 지도
섹션 제목: “Chapter 1: 자료구조 지도”이 챕터는 문서 전체의 용어 사전 역할을 한다. 이후 챕터들은 이 구조체들 위에서 작동하는 연산을 추적하면서 각 필드를 다시 설명하지 않는다. 독자의 질문은 이것이다: 디스크 관리자와 파일 관리자가 공유하는 구조체는 무엇이며, 각 필드는 무슨 의미인가? 설계 배경은 보조 문서 cubrid-disk-manager.md(“볼륨 레이아웃”, “파일 아키텍처”, “영구/임시 목적 분리”)를 참고하라. 이 챕터는 그 이론을 전제로 하고 필드 이름만 명시한다.
모든 것은 두 경계로 구조화된다. 디스크/파일 경계: 디스크 관리자는 볼륨을 소유하고 섹터(64페이지 단위 익스텐트)를 배분하며, 파일 관리자는 그로부터 페이지를 잘라낸다. 온디스크/인메모리 경계: 일부 구조체는 페이지에 바이트 그대로 영속된다(disk_volume_header, file_header, file_extensible_data, file_partial_sector). 나머지는 이를 요약·조율하기 위해 서버 힙에만 존재한다(disk_cache, disk_extend_info, disk_stab_cursor, disk_reserve_context).
1.1 디스크 측, 한눈에
섹션 제목: “1.1 디스크 측, 한눈에”flowchart TB
subgraph ondisk["온디스크 (볼륨당 하나)"]
VH["disk_volume_header<br/>모든 볼륨의 페이지 0"]
STAB["섹터 할당 테이블<br/>비트맵 페이지, 섹터당 1비트"]
end
subgraph mem["인메모리 (disk_Cache, 프로세스 전역)"]
DC["disk_cache"]
DC --> VOLS["vols[LOG_MAX_DBVOLID+1]<br/>볼륨별 disk_cache_volinfo"]
DC --> PERM["perm_purpose_info<br/>disk_perm_info"]
DC --> TEMP["temp_purpose_info<br/>disk_temp_info"]
PERM --> PEI["extend_info: disk_extend_info"]
TEMP --> TEI["extend_info: disk_extend_info"]
end
subgraph transient["일시적 (예약 호출별 / 순회별)"]
RC["disk_reserve_context"]
RC --> CVR["cache_vol_reserve[]:<br/>disk_cache_vol_reserve"]
CUR["disk_stab_cursor"]
end
VH -. "캐시됨" .-> VOLS
STAB -. "순회됨" .-> CUR
DC -. "소진됨" .-> RC
Figure 1-1. 디스크 측 구조체 관계도. disk_Cache는 모든 볼륨의 단일 인메모리 요약이다. disk_reserve_context와 disk_stab_cursor는 섹터 예약 및 비트맵 순회 중에 사용되는 일시적 스크래치 구조체다.
disk_volume_header — 모든 볼륨의 영속 페이지 0
섹션 제목: “disk_volume_header — 모든 볼륨의 영속 페이지 0”디스크 관리자 구조체 중 유일하게 가변 크기를 갖는다. 볼륨 경로 문자열 전체를 담는 var_fields[1] 유연 영역으로 끝나기 때문에, sizeof를 절대 사용해서는 안 된다(소스의 DON'T USE sizeof on this structure 주석이 이를 명시한다).
// disk_volume_header -- src/storage/disk_manager.cstruct disk_volume_header{ char magic[CUBRID_MAGIC_MAX_LENGTH]; /* magic for file/magic Unix utility; DON'T MOVE */ INT16 iopagesize; INT16 volid; INT8 db_charset; INT8 dummy1; DB_VOLPURPOSE purpose; DB_VOLTYPE type; DKNPAGES sect_npgs; /* pages per sector (== DISK_SECTOR_NPAGES = 64) */ DKNSECTS nsect_total; DKNSECTS nsect_max; SECTID hint_allocsect; DKNPAGES stab_npages; PAGEID stab_first_page; PAGEID sys_lastpage; INT32 dummy2; INT64 db_creation; INT64 vol_creation; LOG_LSA chkpt_lsa; HFID boot_hfid; INT32 reserved0; INT32 reserved1; INT32 reserved2; INT32 reserved3; INT16 next_volid; INT16 offset_to_vol_fullname; INT16 offset_to_next_vol_fullname; INT16 offset_to_vol_remarks; char var_fields[1]; /* variable: vol_fullname, next_vol_fullname, remarks */};| 필드 | 역할 | 존재 이유 |
|---|---|---|
magic | 바이트 0의 고정 시그니처 | file/magic(5) 유틸리티와 CUBRID 자체 검사가 이로 볼륨을 식별한다. 위치를 절대 옮겨서는 안 된다. |
iopagesize | 포맷 당시의 IO 페이지 크기 | 온전성 검사용으로만 쓰인다. 권위 있는 크기는 로그에 있다. |
volid | 이 볼륨의 식별자 | 자기 식별용. 길 잃은 페이지를 소속 볼륨으로 역추적한다. |
db_charset | 데이터베이스 문자셋 코드 | 볼륨은 DB 문자셋과 일치해야 하며 attach 시점에 검사된다. |
dummy1, dummy2 | 정렬 패딩 | — |
purpose | 영구/임시 데이터 목적 | 여유 공간이 어느 롤업에 공급될지 결정한다(§1.2). |
type | 영구/임시 볼륨 타입 | purpose와 다르다. 영구 타입 볼륨도 임시 목적에 사용될 수 있다. |
sect_npgs | 섹터당 페이지 수 | 항상 64. 포맷이 자기 서술적이 되도록 저장한다. |
nsect_total | 현재 포맷된 섹터 수 | 현재 물리적으로 존재하는 섹터 ID의 상한. |
nsect_max | 모든 확장 후 최대 섹터 수 | 할당 테이블을 처음 한 번만 크기를 잡을 수 있게 한다. 비트맵이 이동하지 않는다. |
hint_allocsect | 다음 스캔 시작 섹터 | 비트맵의 알려진 가득 찬 앞부분을 건너뛴다. |
stab_npages | 테이블 길이(페이지 단위) | DISK_STAB_NPAGES(nsect_max). 커서 순회의 경계를 정한다. |
stab_first_page | 첫 번째 비트맵 페이지 ID | 테이블은 헤더 다음에 시작된다. 커서는 이 값으로 오프셋을 매핑한다. |
sys_lastpage | 마지막 시스템 페이지 | <= sys_lastpage인 모든 것은 헤더+테이블이다. 사용자 섹터는 그 다음에 온다. |
db_creation | DB 생성 타임스탬프 | 모든 곳에 복제되어, 외부 볼륨이 attach되지 못하도록 막는다. |
vol_creation | 이 볼륨의 생성 시각 | 볼륨별 출처 정보. |
chkpt_lsa | 복구 시작 LSA | 복구 시 이 볼륨보다 오래된 로그 레코드는 건너뛴다. |
boot_hfid | 부트/시스템 힙 파일 ID | 다중 볼륨 접근을 부트스트랩한다. |
reserved0..3 | 오프셋 변경 없이 앞으로 호환 가능한 성장을 위한 여유 INT32 네 개 | — |
next_volid | 다음 볼륨으로의 링크 | 볼륨 집합은 단방향 연결 체인이다. |
offset_to_vol_fullname | var_fields 내 오프셋 | 이 볼륨의 경로 문자열. |
offset_to_next_vol_fullname | var_fields 내 오프셋 | 다음 볼륨의 경로(카탈로그 없이 체인을 따라간다). |
offset_to_vol_remarks | var_fields 내 오프셋 | 자유 형식 비고. |
var_fields | 유연 꼬리 | 세 문자열을 담는다. 길이는 페이지 내에서 var_fields가 시작하는 바이트 오프셋을 DB_PAGESIZE에서 뺀 값이다. |
불변 조건 — 섹터 할당 테이블은
nsect_total이 아니라nsect_max기준으로 단 한 번 크기가 결정된다.stab_npages == DISK_STAB_NPAGES(nsect_max)이며,sys_lastpage는 생성 시점에 헤더와 전체 테이블을 포함한다. 확장(5장)은nsect_total을nsect_max까지 올릴 뿐,stab_first_page나stab_npages는 절대 건드리지 않는다. 테이블이 이동할 수 있다면, 캐시된disk_stab_cursor.pageid와 예약된 모든VSID가 댕글링 포인터가 될 것이다.
disk_cache_volinfo, disk_extend_info, disk_perm_info, disk_temp_info, disk_cache
섹션 제목: “disk_cache_volinfo, disk_extend_info, disk_perm_info, disk_temp_info, disk_cache”이 다섯 구조체는 인메모리 여유 공간 요약을 구성한다. disk_cache가 루트이며, 정확히 하나만 존재한다(static DISK_CACHE *disk_Cache).
// disk_cache_volinfo -- src/storage/disk_manager.cstruct disk_cache_volinfo{ DB_VOLPURPOSE purpose; DKNSECTS nsect_free; /* hint of free sectors on this volume */};필드 (disk_cache_volinfo) | 역할 | 존재 이유 |
|---|---|---|
purpose | 볼륨별 목적(영구/임시) | 볼륨 헤더를 읽지 않고도 vols[volid]를 분류한다. |
nsect_free | 볼륨별 여유 섹터 힌트 | 빠른 볼륨별 추정값. 비트맵이 권위 있는 수치이고, 이것은 캐시 힌트다. |
// disk_extend_info -- src/storage/disk_manager.cstruct disk_extend_info{ volatile DKNSECTS nsect_free; /* free sectors across all volumes of this purpose */ volatile DKNSECTS nsect_total; volatile DKNSECTS nsect_max; volatile DKNSECTS nsect_intention; /* sectors a thread intends to add by extending */ pthread_mutex_t mutex_reserve;#if !defined (NDEBUG) volatile int owner_reserve; /* debug: tid holding mutex_reserve */#endif DKNSECTS nsect_vol_max; VOLID volid_extend; DB_VOLTYPE voltype;};필드 (disk_extend_info) | 역할 | 존재 이유 |
|---|---|---|
nsect_free | 같은 목적의 모든 볼륨에 걸친 여유 섹터 | 빠른 수 예약은 어떤 비트맵도 건드리기 전에 이 값을 감소시킨다(4장). 크로스 스레드 가시성을 위해 volatile. |
nsect_total | 이 목적의 포맷된 섹터 수 | 소진 상태와 단순 단편화를 구분한다. |
nsect_max | 이 목적의 상한 | ”기존 볼륨 확장”과 “새 볼륨 추가”를 구분한다. |
nsect_intention | 확장자가 약속했지만 아직 커밋하지 않은 섹터 수 | 천둥 무리 확장을 방지한다(5장). |
mutex_reserve | 네 카운터를 보호하는 락 | 핫 예약 경로를 직렬화한다. |
owner_reserve | 디버그용 소유자 tid | NDEBUG 전용 락 규율 보조 도구. |
nsect_vol_max | 새 볼륨이 가질 수 있는 최대 섹터 수 | 단일 확장의 크기를 제한한다. |
volid_extend | 다음 확장이 성장시킬 볼륨 | 캐시된 대상으로, 재스캔이 없다. |
voltype | 이 롤업의 볼륨 타입 | 영구/임시를 태깅한다. |
// disk_perm_info / disk_temp_info -- src/storage/disk_manager.cstruct disk_perm_info { DISK_EXTEND_INFO extend_info; };struct disk_temp_info { DISK_EXTEND_INFO extend_info; DKNSECTS nsect_perm_free; /* free sectors on PERMANENT volumes usable for temp purpose */ DKNSECTS nsect_perm_total;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
disk_perm_info.extend_info | 영구 목적 롤업 | 모든 영구 여유 공간이 여기로 집약된다. |
disk_temp_info.extend_info | 임시 볼륨 롤업 | 실제 임시 볼륨의 여유 공간. |
disk_temp_info.nsect_perm_free | 영구 볼륨에서 임시 목적으로 사용 가능한 여유 섹터 | 임시 볼륨이 소진됐을 때의 대체 풀(임시-온-영구). 임시 할당이 실제 임시 볼륨을 먼저 선호할 수 있도록 별도로 유지된다. |
disk_temp_info.nsect_perm_total | 그러한 섹터의 전체 수 | 대체 풀의 크기를 나타낸다. |
// disk_cache -- src/storage/disk_manager.cstruct disk_cache{ int nvols_perm; int nvols_temp; DISK_CACHE_VOLINFO vols[LOG_MAX_DBVOLID + 1]; /* per-volume free hint, indexed by volid */ DISK_PERM_PURPOSE_INFO perm_purpose_info; DISK_TEMP_PURPOSE_INFO temp_purpose_info; pthread_mutex_t mutex_extend; /* never take while holding a reserve mutex */#if !defined (NDEBUG) volatile int owner_extend;#endif};필드 (disk_cache) | 역할 | 존재 이유 |
|---|---|---|
nvols_perm | 영구 볼륨 수 | 순회 경계 및 배치 결정. |
nvols_temp | 임시 볼륨 수 | 동일. 임시 볼륨은 vols의 높은 끝에서 인덱싱된다. |
vols[] | 볼륨별 disk_cache_volinfo | vols[volid] 직접 조회. LOG_MAX_DBVOLID + 1 크기. |
perm_purpose_info | 영구 롤업 | 집계된 영구 여유 공간. |
temp_purpose_info | 임시 롤업 | 집계된 임시 여유 공간과 영구 대체 풀. |
mutex_extend | 볼륨 집합 확장을 위한 락 | mutex_reserve보다 거친 granularity. |
owner_extend | 디버그용 소유자 | NDEBUG 전용. |
LOG_MAX_DBVOLID는 VOLID_MAX - 1(SHRT_MAX - 1)이므로 vols[]는 유효한 모든 VOLID를 인덱싱할 수 있다.
불변 조건 — 락 순서:
mutex_reserve이후에mutex_extend, 역순은 절대 안 된다. 두 구조체의 주석 모두 이를 명시한다. 예약(빈번)은mutex_reserve를 잡고, 확장(드뭄)은mutex_extend를 잡는다. 두 스레드 간에 역순으로 잡으면 교착 상태가 발생한다. 4장과 5장이 이에 의존한다.
disk_stab_cursor와 DISK_STAB_UNIT
섹션 제목: “disk_stab_cursor와 DISK_STAB_UNIT”섹터 할당 테이블은 섹터당 1비트인 비트맵이다. 순회 단위는 UINT64다.
// DISK_STAB_UNIT -- src/storage/disk_manager.ctypedef UINT64 DISK_STAB_UNIT; /* one 64-bit word of the bitmap */
// disk_stab_cursor -- src/storage/disk_manager.cstruct disk_stab_cursor{ const DISK_VOLUME_HEADER *volheader; PAGEID pageid; /* current bitmap page id (real, not table-relative) */ int offset_to_unit; int offset_to_bit; SECTID sectid; PAGE_PTR page; /* fixed bitmap page (NULL until fixed) */ DISK_STAB_UNIT *unit; /* pointer to current unit inside page */};| 필드 | 역할 | 존재 이유 |
|---|---|---|
volheader | 순회 중인 볼륨 | stab_first_page/nsect_total 경계의 출처. |
pageid | 현재 실제 비트맵 페이지 | sectid와 stab_first_page로부터 계산된다. |
offset_to_unit | 페이지 내 UINT64 워드 위치 | DISK_ALLOCTBL_SECTOR_UNIT_OFFSET. |
offset_to_bit | 워드 내 비트 위치 | DISK_ALLOCTBL_SECTOR_BIT_OFFSET. |
sectid | 커서가 가리키는 섹터 | (page, unit, bit) 트리플이 이를 분해한다. |
page | 핀된 페이지 포인터 | NULL = 고정된 페이지 없음. non-NULL = latch 보유 중. |
unit | offset_to_unit의 page 내 포인터 | 주소를 재계산하지 않고 라이브 워드를 읽고 쓴다. |
불변 조건 —
page == NULL이면 latch가 없고, 그 역도 성립한다. 페이지 경계를 넘을 때는 반드시 다음 페이지를 fix하기 전에 기존page를 unfix하고unit을 리셋해야 한다. 순회 후 non-NULLpage가 남으면 latch 누수다. 4장의 비트맵 커밋 단계가 이에 의존한다.
disk_cache_vol_reserve와 disk_reserve_context
섹션 제목: “disk_cache_vol_reserve와 disk_reserve_context”2단계 예약(4장)이 사용하는 일시적 스크래치. disk_reserve_context는 예약 하나가 진행되는 동안 호출자 스택에 산다.
// disk_cache_vol_reserve -- src/storage/disk_manager.cstruct disk_cache_vol_reserve{ VOLID volid; /* a volume from which sectors were drawn */ DKNSECTS nsect; /* how many sectors drawn from it */};
// disk_reserve_context -- src/storage/disk_manager.cstruct disk_reserve_context{ int nsect_total; /* total sectors this request must reserve */ VSID *vsidp; /* output cursor: next VSID write position */ DISK_CACHE_VOL_RESERVE cache_vol_reserve[VOLID_MAX]; /* per-volume tally drawn from cache */ int n_cache_vol_reserve; int n_cache_reserve_remaining; /* entries not yet committed to bitmaps */ DKNSECTS nsects_lastvol_remaining; /* sectors still owed on the last volume */ DB_VOLPURPOSE purpose;};필드 (disk_reserve_context) | 역할 | 존재 이유 |
|---|---|---|
nsect_total | 요청이 필요로 하는 섹터 수 | 루프의 목표량. |
vsidp | 호출자의 VSID[]에 대한 쓰기 커서 | 커밋된 섹터가 여기에 순차 추가된다. |
cache_vol_reserve[] | 볼륨별 계획(볼륨, 수량) | 1단계에서 캐시로부터 채워지고, 2단계에서 비트맵에 재실행된다. VOLID_MAX 크기. |
n_cache_vol_reserve | 채워진 계획 항목 수 | 재실행 루프의 경계. |
n_cache_reserve_remaining | 아직 커밋되지 않은 항목 수 | 정밀한 롤백을 가능하게 한다. |
nsects_lastvol_remaining | 현재 볼륨에서 아직 처리되지 않은 섹터 수 | 하나의 항목 내에서의 진행 상태. |
purpose | 요청 목적 | 영구 또는 임시 롤업으로 라우팅한다. |
disk_cache_vol_reserve는 단순히 (volid, nsect) 쌍이며, 이 배열이 예약 계획이다. DISK_PRERESERVE_BUF_DEFAULT(16)는 캐시 예약이 채우는 기본 배치 크기다.
1.2 파일 측, 한눈에
섹션 제목: “1.2 파일 측, 한눈에”flowchart TB
subgraph hdrpage["파일 헤더 페이지 (파일의 페이지 0)"]
FH["file_header"]
FH --> TS["tablespace: file_tablespace"]
FH --> DESC["descriptor: file_descriptors (union, 64 B)"]
FH -. "offset_to_partial_ftab" .-> PART["partial 테이블:<br/>file_extensible_data of file_partial_sector"]
FH -. "offset_to_full_ftab" .-> FULL["full 테이블:<br/>file_extensible_data of VSID"]
FH -. "offset_to_user_page_ftab" .-> UPT["사용자 페이지 테이블 (numerable):<br/>file_extensible_data of VPID"]
end
PART --> PS["file_partial_sector<br/>{ vsid, page_bitmap }"]
PART -. "vpid_next" .-> MOREP["오버플로 extdata 페이지"]
Figure 1-2. 파일 측 구조체 관계도. 헤더 페이지는 file_header를 임베드한다. file_header는 테이블스페이스 정책, 타입별 디스크립터, 그리고 동일 페이지에 공존하는 세 익스텐서블 테이블을 가리키는 바이트 오프셋 세 개를 담는다(오버플로 페이지는 vpid_next로 연결된다).
file_header — 모든 파일의 영속 페이지 0
섹션 제목: “file_header — 모든 파일의 영속 페이지 0”// file_header -- src/storage/file_manager.cstruct file_header{ INT64 time_creation; VFID self; /* this file's own VFID */ FILE_TABLESPACE tablespace; FILE_DESCRIPTORS descriptor; /* Page counts. */ int n_page_total; int n_page_user; int n_page_ftab; int n_page_free; /* reserved-on-disk, not yet allocated */ int n_page_mark_delete; /* numerable: pages marked deleted */ /* Sector counts. */ int n_sector_total; int n_sector_partial; int n_sector_full; int n_sector_empty; /* empty sectors are a subset of partial */ FILE_TYPE type; INT32 file_flags; /* NUMERABLE / TEMPORARY / ENCRYPTED_* */ VOLID volid_last_expand; INT16 offset_to_partial_ftab; INT16 offset_to_full_ftab; INT16 offset_to_user_page_ftab; /* user page table (numerable only) */ VPID vpid_sticky_first; /* first page if sticky; never deallocated */ /* Temporary files: last-allocation cursor. */ VPID vpid_last_temp_alloc; int offset_to_last_temp_alloc; /* Numerable files. */ VPID vpid_last_user_page_ftab; /* last page of user page table (append point) */ VPID vpid_find_nth_last; /* cache: page of last find_nth result */ int first_index_find_nth_last; /* cache: index of first entry in that page */ INT32 reserved0; INT32 reserved1; INT32 reserved2; INT32 reserved3;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
time_creation | 파일 생성 타임스탬프 | 출처 정보. |
self | 파일 자신의 VFID | 격리된 헤더 페이지가 자기 파일을 알 수 있게 한다. |
tablespace | 확장 정책(아래) | 성장 적극성을 결정한다. |
descriptor | 타입별 메타데이터 union(아래) | 각 타입이 자신의 ID를 여기에 저장한다. |
n_page_total | 소유한 페이지(사용자 + 테이블 + 여유) | 마스터 회계. |
n_page_user | 소유자에게 전달된 페이지 수 | 실질적인 수치. |
n_page_ftab | 세 테이블이 사용하는 페이지 수 | 오버헤드. 오버플로 시 증가. |
n_page_free | 예약됐지만 할당되지 않은 페이지 수 | 새 예약 없이 사용 가능한 수. |
n_page_mark_delete | numerable 파일에서 삭제 표시된 페이지 수 | numerable 파일은 페이지를 제거하지 않고 표시한다(10장). |
n_sector_total | 예약된 섹터 수 | = n_sector_partial + n_sector_full. |
n_sector_partial | 부분 섹터 수 | 여유 페이지가 있음. partial 테이블에 있다. |
n_sector_full | 가득 찬 섹터 수 | 64페이지 전부 사용됨. full 테이블에 있다(영구만 해당). |
n_sector_empty | 페이지가 0개인 섹터 수 | partial의 부분집합. 확장 시 먼저 회수된다. |
type | FILE_TYPE | 테이블 레이아웃과 numerable/temp 적격성을 선택한다. |
file_flags | 비트 플래그 | NUMERABLE 0x1, TEMPORARY 0x2, ENCRYPTED_AES 0x4, ENCRYPTED_ARIA 0x8. FILE_IS_*로 접근한다. |
volid_last_expand | 마지막으로 성장한 볼륨 | 다음 확장을 위한 지역성 힌트. |
offset_to_partial_ftab | 이 페이지 내 partial 테이블 오프셋 | FILE_HEADER_GET_PART_FTAB이 범위를 assert한다. |
offset_to_full_ftab | full 테이블 오프셋 | 임시 파일이 아님을 assert한다. |
offset_to_user_page_ftab | 사용자 페이지 테이블 오프셋 | numerable 전용. numerable임을 assert한다. |
vpid_sticky_first | sticky인 경우 첫 번째 페이지 | 절대 할당 해제되지 않는다(11장). |
vpid_last_temp_alloc | 임시 할당 커서: 페이지 | 임시 파일은 앞으로만 할당하며, 해제하지 않는다(8장). |
offset_to_last_temp_alloc | 임시 할당 커서: 섹터 오프셋 | 그 커서의 오프셋 구성 요소. |
vpid_last_user_page_ftab | numerable: 추가 지점 | 새 사용자 페이지가 여기에 추가된다(10장). |
vpid_find_nth_last | numerable: 캐시된 find-nth 페이지 | 순차 find-nth를 최적화한다(10장). |
first_index_find_nth_last | numerable: 캐시된 첫 번째 항목 인덱스 | 위 캐시의 보조 값. |
reserved0..3 | 앞으로 호환성을 위한 여유 INT32 네 개 | — |
불변 조건 — 회계 균형:
n_page_total == n_page_user + n_page_ftab + n_page_free,n_sector_total == n_sector_partial + n_sector_full, 그리고n_sector_empty <= n_sector_partial. 7장~9장의 모든 alloc/dealloc은 헤더 latch 하에 이 값들을 세트로 조정한다. 어긋나면 파일이 소유하지 않는 공간을 소유한다고 믿거나, 공간을 누수하게 된다.file_validate와FILE_HEADER_GET_*_FTABassert가 감시한다. empty 부분집합 관계 덕분에 확장 시 별도 테이블 없이 빈 섹터를 우선 선택할 수 있다.
file_extensible_data — 범용 다중 페이지 테이블 컴포넌트
섹션 제목: “file_extensible_data — 범용 다중 페이지 테이블 컴포넌트”세 개의 파일 테이블 모두 file_extensible_data다. 작은 헤더 뒤에 고정 크기 항목 배열이 이어지며, 페이지와 페이지가 체인으로 연결된다.
// file_extensible_data -- src/storage/file_manager.cstruct file_extensible_data{ VPID vpid_next; /* next component page, NULL if last */ INT16 max_size; /* capacity in bytes for items in this component */ INT16 size_of_item; /* byte size of one item */ INT16 n_items; /* number of items currently stored */};| 필드 | 역할 | 존재 이유 |
|---|---|---|
vpid_next | 다음 컴포넌트 페이지 링크 | 오버플로 페이지를 체인으로 연결. NULL이면 마지막. |
max_size | 이 컴포넌트의 바이트 용량 | n_items의 상한. |
size_of_item | 항목 하나의 크기 | partial 테이블 = file_partial_sector(16 B), full = VSID(8 B), 사용자 페이지 = VPID(8 B). 하나의 포맷, 세 가지 항목 타입. |
n_items | 저장된 항목 수 | 순회를 주도한다. 삽입/삭제 시 증감한다. |
불변 조건 —
n_items * size_of_item <= max_size, 항목은FILE_EXTDATA_HEADER_ALIGNED_SIZE부터 빽빽하게 채워진다. 오버플로가 발생할 삽입은vpid_next로 연결된 새 컴포넌트를 할당한다. 삭제는 꼬리를 당겨 내린다. 밀도 불변 조건 덕분에 find-nth가 위치로 인덱싱할 수 있다(6장~10장).
file_partial_sector, FILE_ALLOC_BITMAP, file_tablespace, file_descriptors
섹션 제목: “file_partial_sector, FILE_ALLOC_BITMAP, file_tablespace, file_descriptors”// FILE_ALLOC_BITMAP -- src/storage/file_manager.htypedef UINT64 FILE_ALLOC_BITMAP; /* one bit per page in a sector (64 pages) */#define FILE_FULL_PAGE_BITMAP 0xFFFFFFFFFFFFFFFF /* Full allocation bitmap */#define FILE_EMPTY_PAGE_BITMAP 0x0000000000000000 /* Empty allocation bitmap */#define FILE_ALLOC_BITMAP_NBITS ((int) (sizeof (FILE_ALLOC_BITMAP) * CHAR_BIT)) /* 64 */
// file_partial_sector -- src/storage/file_manager.hstruct file_partial_sector{ VSID vsid; /* MUST be first member: reinterpreted as VSID in file table */ FILE_ALLOC_BITMAP page_bitmap;};VSID는 { int32_t sectid; short volid; } = 6바이트 패딩 후 8바이트, FILE_ALLOC_BITMAP은 UINT64 = 8바이트. 따라서 sizeof (file_partial_sector) == 16.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
file_partial_sector.vsid | 예약된 섹터의 ID(8 B) | 계약상 첫 번째 멤버여야 한다. full 테이블은 bare VSID를 저장하므로, file_partial_sector*를 VSID*로 재해석할 수 있다. 승격 시 이를 사용한다. |
file_partial_sector.page_bitmap | 64비트 페이지 할당 맵(8 B) | 비트 i 가 설정됨 = 페이지 i 가 할당됨. FILE_FULL_PAGE_BITMAP = 가득 참. FILE_EMPTY_PAGE_BITMAP = 비어 있음. |
불변 조건 —
vsid는 의도적으로 첫 번째 멤버다. 소스 주석: “VSID must be first member … the FILE_PARTIAL_SECTOR pointers in file table are reinterpreted as VSID.” 부분 섹터가 가득 차면, 파일 관리자는 bitmap을 복사하지 않고 선두의VSID바이트들을 full 테이블로 이동시킨다. 순서를 바꾸면 full 테이블이 조용히 오염된다.FILE_ALLOC_BITMAP_NBITS == DISK_SECTOR_NPAGES == 64이므로, 비트맵 하나가 정확히 섹터 하나의 64페이지를 커버한다.
// file_tablespace -- src/storage/file_manager.hstruct file_tablespace{ INT64 initial_size; /* bytes the file starts with */ float expand_ratio; /* fraction of current size to add when expanding */ int expand_min_size; /* lower clamp on an expansion, in bytes */ int expand_max_size; /* upper clamp on an expansion, in bytes */};| 필드 | 역할 | 존재 이유 |
|---|---|---|
initial_size | 시작 바이트 크기 | 생성 시 MAX(1, npages) * DB_PAGESIZE. |
expand_ratio | 성장 비율 | 영구 파일은 현재 크기의 약 1%(FILE_TABLESPACE_DEFAULT_RATIO_EXPAND). 임시는 0. |
expand_min_size | 최소 확장 크기 | 영구 파일은 최소 한 섹터. 임시는 0. |
expand_max_size | 최대 확장 크기 | 단일 성장을 제한한다(영구 1024섹터, 임시 0). |
임시 파일은 FILE_TABLESPACE_FOR_TEMP_NPAGES를 사용하여 ratio/min/max를 0으로 설정한다. 임시 파일은 동일한 방식으로 자동 확장하지 않는다.
// file_descriptors -- src/storage/file_manager.h/* note: if you change file descriptors size, make sure to change disk compatibility version too! */#define FILE_DESCRIPTORS_SIZE 64union file_descriptors{ FILE_HEAP_DES heap; FILE_OVF_HEAP_DES heap_overflow; FILE_BTREE_DES btree; FILE_OVF_BTREE_DES btree_key_overflow; /* TODO: rename FILE_OVF_BTREE_DES */ FILE_EHASH_DES ehash; FILE_VACUUM_DATA_DES vacuum_data; char dummy_align[FILE_DESCRIPTORS_SIZE];};아래 멤버별 구조체 형태는 추가 주석이다(소스는 각 FILE_*_DES를 별도로 정의하며, 인라인으로 정의하지 않는다).
| 멤버 | 형태 (주석) | 역할 | 존재 이유 |
|---|---|---|---|
heap | { OID class_oid; HFID hfid; } | 힙 파일의 class OID + HFID | 힙 파일이 자기 클래스와 힙 ID를 역참조한다. |
heap_overflow | { HFID hfid; OID class_oid; } | 오버플로 힙의 HFID + class OID | 대형 힙 행의 오버플로 레코드. |
btree | { OID class_oid; int attr_id; } | 인덱스 파일의 class OID + 속성 ID | btree 파일이 인덱싱하는 클래스와 속성을 안다. |
btree_key_overflow | { BTID btid; OID class_oid; } | 긴 키 오버플로 파일(file_ovf_btree_des) | 긴 키는 별도 파일로 오버플로된다. |
ehash | { OID class_oid; int attr_id; } | 익스텐서블 해시의 class OID + attr id | 해시 대상 속성을 식별한다. |
vacuum_data | { VPID vpid_first; } | vacuum 데이터의 첫 번째 VPID | vacuum의 부기 파일. |
dummy_align | char[FILE_DESCRIPTORS_SIZE] | 64바이트 패딩 | union을 FILE_DESCRIPTORS_SIZE에 고정한다. 소스는 이 크기를 온디스크 호환성 버전에 연계시키므로 함부로 변경해서는 안 된다. |
이 union은 file_header.type에 따라 해석된다. FILE_TYPE_CAN_BE_NUMERABLE, FILE_TYPE_IS_ALWAYS_TEMP, 그리고 file_flags 비트가 파일이 실제로 어떤 테이블을 가지는지 결정한다. 자세한 내용은 6장과 10장에서 다룬다.
1.3 챕터 요약 — 핵심 정리
섹션 제목: “1.3 챕터 요약 — 핵심 정리”- 영속되는 페이지 0 구조체는 두 개다.
disk_volume_header(볼륨당 하나)와file_header(파일당 하나). 나머지는 인메모리에서 요약(disk_cache계열)하거나 하나의 연산에 대한 스크래치(disk_reserve_context,disk_stab_cursor)다. - 디스크 관리자는 섹터(64페이지 익스텐트)를
nsect_max기준으로 단 한 번 크기가 결정된 1비트/섹터 테이블로 추적하여 배분한다. 테이블이 이동하지 않기 때문에, 예약된 모든VSID와 캐시된 커서가 볼륨 확장 후에도 유효하다. disk_cache는 단일 인메모리 여유 공간 오라클이다.vols[]볼륨별 힌트가 두 목적별 롤업(disk_perm_info,disk_temp_info)에 집약되며,mutex_reserve이후에mutex_extend라는 락 순서는 교착 방지를 위한 하드 불변 조건이다.- 섹터 예약은 2단계다.
disk_reserve_context가 캐시에서 계획을cache_vol_reserve[]로 소진시키고,disk_stab_cursor로 비트맵에 재실행한다.*_remaining카운터가 부분 예약을 정밀하게 되돌릴 수 있게 한다. - 파일 관리자는 예약된 섹터에서 세 개의
file_extensible_data테이블(partial, full, 사용자 페이지)을 거쳐 페이지를 잘라낸다.size_of_item만 다른 동일한 체인 밀집 고정 항목 포맷이다. file_partial_sector(16 B)는vsid를 의도적으로 첫 번째에 배치하여, 가득 찬 섹터를 포인터 재해석으로 full 테이블에 승격할 수 있게 한다. 64비트page_bitmap은 정확히 한 섹터의 64페이지를 매핑한다.file_header의 페이지 및 섹터 카운터는 회계 등식으로 균형을 맞춰야 한다.offset_to_*_ftab세 개, 임시 할당 커서, numerable find-nth 캐시가 일반/임시/numerable 파일을 구별하는 유일한 상태다. 동작 의미는 7장, 8장, 10장에서 다룬다.
Chapter 2: 초기화와 메모리 관리
섹션 제목: “Chapter 2: 초기화와 메모리 관리”1장의 구조체들은 디스크에 독립적인 형태로 저장되지 않는다. 진실의 원천(source of truth)은 볼륨 헤더와 섹터 할당 테이블(3장)이며, disk_Cache는 서버 부팅 시마다 해당 헤더들로부터 재계산되는 *파생 롤업(derived rollup)*이다. 이 장은 다음 질문에 답한다: 서버 시작 시 disk_Cache와 파일 매니저 전역 변수들은 어디서 오는가, 그리고 마운트된 볼륨 체인을 순회하면서 캐시는 어떻게 재구성되는가? CUBRID가 거친 단위(coarse-grained)의 RAM 카운터를 유지하는 이유는 “인-메모리 캐시” 절을 참조한다.
2.1 부트스트랩 호출 체인
섹션 제목: “2.1 부트스트랩 호출 체인”부팅 시 두 모듈이 깨어난다. 디스크 매니저(disk_Cache와 disk_manager_init를 소유)는 디스크로부터 상태를 복원하고, 파일 매니저(file_Tempcache와 트래커 전역 변수, file_manager_init를 소유)는 RAM만 초기화한다.
flowchart TD boot["서버 부팅"] --> dmi["disk_manager_init(load_from_disk=true)"] dmi --> dci["disk_cache_init -> malloc disk_Cache"] dci --> dclav["disk_cache_load_all_volumes"] dclav --> fmm["fileio_map_mounted: 마운트된 볼륨 순회"] fmm --> dclv["disk_cache_load_volume (볼륨별)"] dclv --> dvb["disk_volume_boot: 헤더 읽기 + 여유 섹터 집계"] boot --> fmi["file_manager_init"] fmi --> ftci["file_tempcache_init -> file_Tempcache 초기화"]
Figure 2-1. 부팅 시 초기화 팬아웃.
2.2 disk_manager_init — 파라미터 캡처, 재로드 가드, 조건부 로드
섹션 제목: “2.2 disk_manager_init — 파라미터 캡처, 재로드 가드, 조건부 로드”disk_manager_init는 순서대로 네 가지 작업을 수행한다: 임시 볼륨 섹터 상한 도출, 로깅 플래그 캡처, 캐시 (재)할당, 조건부 디스크 로드.
// disk_manager_init -- src/storage/disk_manager.cintdisk_manager_init (THREAD_ENTRY * thread_p, bool load_from_disk){ int error_code = NO_ERROR;
disk_Temp_max_sects = (DKNSECTS) prm_get_integer_value (PRM_ID_BOSR_MAXTMP_PAGES); if (disk_Temp_max_sects < 0) disk_Temp_max_sects = SECTID_MAX; /* <- negative param means "no cap" (infinite) */ else disk_Temp_max_sects = disk_Temp_max_sects / DISK_SECTOR_NPAGES; /* <- pages -> sectors */ // ... condensed: disk_Logging = prm_get_bool_value (PRM_ID_DISK_LOGGING) ...
if (disk_Cache != NULL) disk_cache_final (); /* <- idempotent reload: tear down stale cache first */ error_code = disk_cache_init (); if (error_code != NO_ERROR) { ASSERT_ERROR (); return error_code; /* <- malloc failure: nothing to clean up */ } assert (disk_Cache != NULL);
if (load_from_disk && !disk_cache_load_all_volumes (thread_p)) { ASSERT_ERROR_AND_SET (error_code); disk_manager_final (); /* <- partial load failed: roll the whole cache back */ return error_code; } return NO_ERROR;}분기별 동작:
| 분기 | 조건 | 효과 |
|---|---|---|
disk_Temp_max_sects < 0 | 파라미터가 음수 (기본값 -1) | cap = SECTID_MAX → 임시 공간 무제한 |
| else | 파라미터 >= 0 | 파라미터는 페이지 수; / DISK_SECTOR_NPAGES → 섹터 상한으로 변환 |
disk_Cache != NULL | 이전 캐시 존재 (재로드) | disk_cache_final로 먼저 해제 — init을 멱등성(idempotent)으로 만듦 |
disk_cache_init != NO_ERROR | malloc 실패 | 조기 반환; 해제할 할당이 없음 |
load_from_disk && load fails | 볼륨 하나가 부팅 실패 | disk_manager_final로 절반 구성된 캐시 해제 후 오류 전파 |
load_from_disk == false | 첫 번째 볼륨 포맷 경로 | 캐시는 빈 상태로 유지; 호출자가 직접 채움 |
정적 초기값 static DKNSECTS disk_Temp_max_sects = -2;는 pre-init 센티넬(“아직 계산되지 않음”)로, 파라미터 기본값인 -1(“무제한”)과 구별된다. disk_manager_init는 항상 PRM_ID_BOSR_MAXTMP_PAGES(temp_file_max_size_in_pages)로부터 이 값을 덮어쓴다. 이 값은 이후 permanent 볼륨을 temp용으로 증량(growth)하는 상한 역할을 한다.
불변 조건 — 재로드 경로는 파괴 후 재구축(destructive-then-rebuilding)이다.
disk_manager_init는 복수 회 실행될 수 있으므로(복구 단계 이후 재로드) 이전 캐시를 절대 누수시켜서는 안 된다.disk_Cache != NULL가드가disk_cache_final을 먼저 호출한다. 이 가드 없이 두 번째 init을 실행하면 이전 할당과 세 개의 뮤텍스가 누수된다.
2.3 disk_cache_init — 전역 캐시 할당 및 초기화
섹션 제목: “2.3 disk_cache_init — 전역 캐시 할당 및 초기화”disk_cache_init는 disk_Cache의 유일한 할당자다. 하나의 flat DISK_CACHE를 malloc하는데, vols[] 배열은 LOG_MAX_DBVOLID 크기로 인라인에 포함된다. 이후 모든 카운터를 0으로 초기화하므로, 볼륨별 로드 시 롤업에 단순히 더하는 방식이 가능해진다.
// disk_cache_init -- src/storage/disk_manager.cstatic intdisk_cache_init (void){ int i; assert (disk_Cache == NULL); /* <- never double-allocate */ disk_Cache = (DISK_CACHE *) malloc (sizeof (DISK_CACHE)); if (disk_Cache == NULL) { /* ... er_set OUT_OF_VIRTUAL_MEMORY, return ER_OUT_OF_VIRTUAL_MEMORY ... */ }
disk_Cache->nvols_perm = disk_Cache->nvols_temp = 0; disk_Cache->perm_purpose_info.extend_info.nsect_vol_max = /* default new-vol size */ DISK_SECTS_ROUND_UP ((DKNSECTS) (prm_get_bigint_value (PRM_ID_DB_VOLUME_SIZE) / IO_SECTORSIZE)); // ... condensed: perm free/total/max = 0 (load ADDS in); volid_extend = NULL_VOLID; voltype = PERM ... // ... condensed: temp extend_info same vol_max, zeroed, NULL_VOLID; nsect_perm_free/total = 0 ... // ... condensed: 3 pthread_mutex_init (perm/temp mutex_reserve, mutex_extend) ... for (i = 0; i <= LOG_MAX_DBVOLID; i++) /* <- inclusive of highest legal volid */ { disk_Cache->vols[i].purpose = DISK_UNKNOWN_PURPOSE; /* <- every slot starts "no volume here" */ disk_Cache->vols[i].nsect_free = 0; } return NO_ERROR;}nsect_vol_max(두 purpose 모두)는 이후 자동 확장(auto-extension) 시의 기본 신규 볼륨 크기이며, 현재 값이 아니다. volid_extend는 모두 NULL_VOLID로 시작하고(로드 중 발견됨), nvols_*는 0, 모든 슬롯은 DISK_UNKNOWN_PURPOSE / 여유 0으로 시작한다. 로드가 더하기만 하므로, 로드 전에 반드시 disk_cache_init가 선행되어야 한다.
2.4 disk_cache_load_all_volumes — 마운트된 볼륨 체인 순회
섹션 제목: “2.4 disk_cache_load_all_volumes — 마운트된 볼륨 체인 순회”disk_cache_load_all_volumes는 얇은 래퍼(thin wrapper)다. 캐시 존재를 assert한 후 fileio_map_mounted (thread_p, disk_cache_load_volume, NULL)을 반환하여, 체인 워커에 볼륨별 콜백을 위임한다.
fileio_map_mounted(file_io.c 소재)가 실제 워커다. 파일-IO 볼륨 정보 헤더를 두 패스로 순회한다: permanent 볼륨은 volid 0부터 next_perm_volid - 1까지 오름차순, 이후 temporary 볼륨은 next_temp_volid까지 내림차순(파일-IO가 관리하는 온-디스크 next_volid 체인에 해당). 언마운트된 슬롯(vol_info_p->vdes == NULL_VOLDES)은 건너뛴다. 콜백이 false를 반환하면 순회를 중단하고 false를 반환하며, disk_manager_init는 이를 치명적 오류로 처리한다.
flowchart TD
start["fileio_map_mounted"] --> permloop{"perm volid <= next_perm_volid-1?"}
permloop -- "vdes 살아있음" --> cb1["disk_cache_load_volume(volid)"]
permloop -- "건너뜀 / 완료" --> temploop{"temp volid >= next_temp_volid?"}
cb1 -- false --> stopf["return false"]
cb1 -- true --> permloop
temploop -- "vdes 살아있음" --> cb2["disk_cache_load_volume(volid)"]
cb2 -- false --> stopf
cb2 -- true --> temploop
temploop -- done --> okt["return true"]
Figure 2-2. fileio_map_mounted 두 패스 순회로 캐시 로드를 구동하는 모습.
2.5 disk_cache_load_volume — 헤더 하나를 롤업에 반영
섹션 제목: “2.5 disk_cache_load_volume — 헤더 하나를 롤업에 반영”캐시 재구성의 핵심. 볼륨별로 disk_volume_boot을 호출해 헤더를 부팅하고(헤더 읽기와 여유 섹터 집계 — 3장), 결과를 적절한 purpose info에 합산한다.
// disk_cache_load_volume -- src/storage/disk_manager.cstatic booldisk_cache_load_volume (THREAD_ENTRY * thread_p, INT16 volid, void *ignore){ DB_VOLPURPOSE vol_purpose; DB_VOLTYPE vol_type; DISK_VOLUME_SPACE_INFO space_info = DISK_VOLUME_SPACE_INFO_INITIALIZER;
if (disk_volume_boot (thread_p, volid, &vol_purpose, &vol_type, &space_info) != NO_ERROR) { ASSERT_ERROR (); return false; /* <- aborts the whole map walk */ }
if (vol_type != DB_PERMANENT_VOLTYPE) { /* don't save temporary volumes... they will be dropped anyway */ return true; /* <- temp-type volumes are not cached at all */ }
if (vol_purpose == DB_PERMANENT_DATA_PURPOSE) { // perm_purpose_info.extend_info.nsect_{free,total,max} += space_info.n_{free,total,max}_sects // ... condensed: assert nsect_free <= nsect_total <= nsect_max ... if (space_info.n_total_sects < space_info.n_max_sects) { assert (disk_Cache->perm_purpose_info.extend_info.volid_extend == NULL_VOLID); disk_Cache->perm_purpose_info.extend_info.volid_extend = volid; /* <- this vol can still grow */ } } else /* perm type, temp purpose */ { assert (space_info.n_total_sects == space_info.n_max_sects); /* <- perm-as-temp is fully grown */ // temp_purpose_info.nsect_perm_{free,total} += space_info.n_{free,total}_sects // ... condensed: assert nsect_perm_free <= nsect_perm_total ... }
disk_Cache->vols[volid].nsect_free = space_info.n_free_sects; disk_Cache->vols[volid].purpose = vol_purpose; disk_Cache->nvols_perm++; /* <- runs for BOTH branches above */ return true;}분기별 동작:
| 분기 | 조건 | 효과 |
|---|---|---|
| 부팅 실패 | disk_volume_boot != NO_ERROR | false 반환; map 순회 및 전체 init 중단 |
vol_type != DB_PERMANENT_VOLTYPE | temporary-type 볼륨 | true 반환 — 캐시에 등록하지 않음 (부팅 시 삭제/재포맷됨) |
vol_purpose == DB_PERMANENT_DATA_PURPOSE | perm 볼륨, perm 데이터 | free/total/max를 perm_purpose_info.extend_info에 합산; 최대 크기 미만이면 volid_extend 설정 |
| else (perm type, temp purpose) | temp 용도로 전용된 perm 볼륨 | free/total를 temp_purpose_info.nsect_perm_*에 합산; 완전히 성장했다고 assert |
else 분기가 미묘한 케이스다: type(재시작 후 생존 여부)과 purpose(저장 내용)는 독립적이다. perm-type/temp-purpose 볼륨의 공간은 nsect_perm_*(“temp에 빌려준 permanent 섹터”)에 롤업되며, temp_purpose_info.extend_info(진정한 temporary-type 볼륨, 위에서 이미 건너뜀)와는 구별된다. perm 경로의 assert (... == NULL_VOLID)는 성장 중인 permanent 볼륨이 최대 하나임을 강제한다. if/else 이후 슬롯 기록(vols[volid].*)과 nvols_perm++는 purpose와 무관하게 모든 permanent-TYPE 볼륨에 무조건 실행된다. 따라서 perm-as-temp 볼륨은 nvols_perm에 집계되고 nvols_temp에는 집계되지 않는다. temporary-type 볼륨은 일찍 반환했으므로, 전체 로드 완료 후 nvols_temp == 0이 된다.
불변 조건 — 캐시는 파생 롤업이므로 정당하게 과소 집계될 수 있다.
nsect_free는 언제든 실제보다 낮은 값일 수 있다. 4장에서 설명하는 두 단계 예약 프로토콜(two-step reservation protocol)이 이 특성에 의존한다. 예약은 캐시를 보수적으로 차감한 뒤 나중에 할당 테이블과 정합을 맞출 수 있다.nsect_free를 정확한 값으로 취급해서는 안 된다. 할당 테이블이 진실의 원천이다.
2.6 disk_manager_final / disk_cache_final — 해제
섹션 제목: “2.6 disk_manager_final / disk_cache_final — 해제”해제는 분기가 단순하다. disk_manager_final은 disk_cache_final에 위임한다.
// disk_manager_final -- src/storage/disk_manager.cvoid disk_manager_final (void) { disk_cache_final (); }
// disk_cache_final -- src/storage/disk_manager.cstatic voiddisk_cache_final (void){ if (disk_Cache == NULL) { return; /* <- safe to call when never initialized */ } // ... condensed: assert perm/temp owner_reserve == -1 and owner_extend == -1 (no lock held at teardown) ... // ... condensed: pthread_mutex_destroy the perm/temp mutex_reserve and mutex_extend ... free_and_init (disk_Cache); /* <- frees and NULLs the pointer */}disk_Cache == NULL 가드는 final을 멱등성으로 만들기 때문에, 재로드 경로와 로드 실패 롤백 모두 무조건적으로 호출할 수 있다. 세 개의 owner_* assert(디버그 전용)는 해제 시점에 어떤 스레드도 reserve 또는 extend 뮤텍스를 보유해서는 안 된다는 사실을 명시한다. 위반은 파괴된 잠긴 뮤텍스가 아니라 이 지점에서 감지된다. free_and_init은 포인터를 NULL로 초기화하므로, 이후 disk_cache_init가 assert (disk_Cache == NULL)을 통과할 수 있다.
2.7 file_manager_init / file_manager_final과 파일 매니저 전역 변수
섹션 제목: “2.7 file_manager_init / file_manager_final과 파일 매니저 전역 변수”파일 매니저는 디스크에서 아무것도 복원하지 않는다. 로깅 플래그 하나를 캡처하고, 크기 가정을 검증하며, 임시 파일 캐시를 초기화할 뿐이다.
// file_manager_init -- src/storage/file_manager.cintfile_manager_init (void){ file_Logging = prm_get_bool_value (PRM_ID_FILE_LOGGING); assert (FILE_DESCRIPTORS_SIZE == sizeof (FILE_DESCRIPTORS)); /* <- layout self-check */ return file_tempcache_init ();}
// file_manager_final -- src/storage/file_manager.cvoid file_manager_final (void) { file_tempcache_final (); }file_manager_init는 file_Tracker_vfid / file_Tracker_vpid를 건드리지 않는다. 이 변수들은 정적으로 0 초기화(VFID_INITIALIZER / VPID_INITIALIZER)되며, 부팅 중 트래커 파일이 생성되거나 위치를 확인할 때만 채워진다(6장, 9장). file_Tempcache 역시 정적으로 선언되어 있으며, file_tempcache_init가 채우기 전까지 “빈” 상태다.
// file_tempcache_init -- src/storage/file_manager.cstatic intfile_tempcache_init (void){ int ntrans = logtb_get_number_of_total_tran_indices () + 1; /* SERVER_MODE; else 1 */ assert (file_Tempcache.tran_files == NULL); /* <- tran_files != NULL means "initialized" */
// ... condensed: free_entries/cached_* = NULL, ncached_* = 0, nfree_entries_max = ntrans*8 ... file_Tempcache.ncached_max = prm_get_integer_value (PRM_ID_MAX_ENTRIES_IN_TEMP_FILE_CACHE); pthread_mutex_init (&file_Tempcache.mutex, NULL);
file_Tempcache.tran_files = (FILE_TEMPCACHE_TRAN_ENTRY *) malloc (ntrans * sizeof (...)); if (file_Tempcache.tran_files == NULL) { pthread_mutex_destroy (&file_Tempcache.mutex); /* <- undo the mutex on alloc failure */ // ... er_set OUT_OF_VIRTUAL_MEMORY; return ER_OUT_OF_VIRTUAL_MEMORY ... } // ... condensed: memset tran_files; per-tran mutex_init loop; memset spacedb_temp ... return NO_ERROR;}분기 분석: 유일한 비자명 분기는 malloc 실패다. 이 경우 반환 전에 file_Tempcache.mutex를 파괴하여 반쯤 구성된 상태를 남기지 않는다. file_tempcache_final은 이를 거울처럼 반영한다 — tran_files == NULL이면 조기 반환, 아니면 트랜잭션별 리스트, 캐시된 numerable/not-numerable 리스트, free-entry 풀을 모두 해제하고 뮤텍스를 파괴한다.
불변 조건 —
file_Tempcache.tran_files == NULL이 “미초기화” 센티넬이다.init(assert 경유)과final(조기 반환 경유) 모두tran_files를 tempcache 존재 여부의 단일 진실로 취급한다. 이 값을 할당하거나 해제하는 코드는 이 불변 조건을 지켜야 한다. 그렇지 않으면final이 실제 해제를 건너뛰거나 이중 해제가 발생한다.
2.8 챕터 요약 — 핵심 정리
섹션 제목: “2.8 챕터 요약 — 핵심 정리”disk_manager_init만이disk_Cache의 유일한 조립자이며 멱등성을 가진다:disk_Cache != NULL가드로 이전 캐시를 해제하고,disk_cache_init로 새로 할당하며,load_from_disk실패 시disk_manager_final로 롤백한다.disk_cache_init는 모든 롤업 카운터를 0으로 초기화하여 로드가 순수하게 더하기만 하도록 하고, 모든vols[]슬롯을DISK_UNKNOWN_PURPOSE로 시드(seed)한다.- 캐시는 마운트된 볼륨을 순회하며 재구성된다 —
fileio_map_mounted(perm 오름차순 / temp 내림차순 두 패스,next_*_volid경계), 살아있는 디스크립터마다disk_cache_load_volume한 번씩 호출. disk_cache_load_volume은 type과 purpose를 구분한다: temp-type 볼륨은 건너뛰고, perm-data는perm_purpose_info.extend_info에 반영(단 하나의volid_extend설정 가능), perm-type/temp-purpose는temp_purpose_info.nsect_perm_*에 반영.nvols_perm++는 purpose 무관하게 모든 permanent-type 볼륨에 실행되므로, 전체 로드 후nvols_temp == 0이다.- 캐시는 파생된 하한(lower-bound) 롤업으로 여유 섹터를 정당하게 과소 집계할 수 있다. 할당 테이블이 진실의 원천이다.
disk_Temp_max_sects는-2로 시작(pre-init 센티넬, 파라미터 기본값-1= 무제한과 구별),PRM_ID_BOSR_MAXTMP_PAGES로부터 덮어씌워진다: 음수는SECTID_MAX에 매핑되고, 음수가 아닌 페이지 수는DISK_SECTOR_NPAGES로 나눈다.- 파일 매니저는 디스크에서 아무것도 복원하지 않는다:
file_manager_init는 플래그 캡처와file_tempcache_init실행만 하고, 트래커 전역 변수는 정적*_INITIALIZER0값을 유지하며,file_Tempcache.tran_files == NULL이 init과 final 모두를 지키는 미초기화 센티넬이다.
Chapter 3: 볼륨 포맷과 섹터 할당 테이블
섹션 제목: “Chapter 3: 볼륨 포맷과 섹터 할당 테이블”이 챕터가 답하는 질문: CUBRID 볼륨은 디스크 위에 어떻게 배치되는가, 그리고 디스크 매니저는 섹터 할당 테이블의 비트를 한 번에 하나씩 스캔하지 않고 어떻게 뒤집는가? 상위 문서(cubrid-disk-manager.md)는 섹터가 왜 할당 단위인지, 비트맵이 왜 free-list보다 나은지를 다룬다. 이 챕터는 바이트 단위의 레이아웃, 포맷 시 기록 경로, 그리고 비트맵-as-functor 메커니즘을 추적한다. DISK_VOLUME_HEADER와 DISK_STAB_CURSOR는 Chapter 1에서 필드별로 소개된다.
3.1 온디스크 볼륨 레이아웃
섹션 제목: “3.1 온디스크 볼륨 레이아웃”모든 CUBRID 볼륨 — 영구적이든 임시적이든, 첫 번째든 확장이든 — 은 하나의 매크로 레이아웃을 공유한다. 페이지 0은 볼륨 헤더, 이어서 연속된 섹터 테이블(STAB) 페이지, 그 뒤에 데이터가 온다. 세 개의 헤더 필드가 이를 고정한다.
// disk_volume_header_set_stab -- src/storage/disk_manager.cvolheader->stab_first_page = DISK_VOLHEADER_PAGE + 1; /* <- STAB은 항상 페이지 1에서 시작 */volheader->stab_npages = CEIL_PTVDIV (volheader->nsect_max, DISK_STAB_PAGE_BIT_COUNT); /* <- nsect_total이 아닌 nsect_max 기준으로 크기 결정 */volheader->sys_lastpage = volheader->stab_first_page + volheader->stab_npages - 1; /* <- 마지막 시스템 예약 페이지 */DISK_VOLHEADER_PAGE는 0이므로 stab_first_page는 항상 페이지 1이다. 핵심적인 선택은 제수(divisor)에 있다 — nsect_total이 아니라 nsect_max: 볼륨은 STAB이 최초 포맷 시 최대 용량 기준으로 크기가 결정되었기 때문에, 데이터 영역을 이동하지 않고도 사용 크기를 용량까지 늘릴 수 있다. Chapter 5(확장)가 이에 의존한다 — 확장 시에는 이미 존재하는 STAB 비트를 뒤집기만 하며 볼륨을 재배치하지 않는다.
flowchart LR
subgraph Volume["볼륨 파일 (페이지)"]
H["페이지 0<br/>DISK_VOLUME_HEADER<br/>magic, volid, purpose,<br/>nsect_total, nsect_max,<br/>stab_first_page, stab_npages,<br/>sys_lastpage, hint_allocsect"]
S["페이지 1 .. sys_lastpage<br/>섹터 할당 테이블 (STAB)<br/>stab_npages 페이지, UINT64 단위<br/>1비트 == 1섹터"]
D["페이지 sys_lastpage+1 .. 끝<br/>데이터 섹터<br/>각 64페이지"]
end
H --> S --> D
Figure 3-1: 모든 CUBRID 볼륨의 매크로 레이아웃. STAB은 nsect_max 기준으로 크기가 결정되므로 데이터 영역의 시작 위치는 변하지 않는다.
“섹터”는 연속된 64개의 페이지(DISK_SECTOR_NPAGES)다. SECTOR_FROM_PAGEID(pageid)는 pageid / 64다. 볼륨이 포맷 시 자체적으로 예약하는 시스템 섹터 수는 SECTOR_FROM_PAGEID(sys_lastpage) + 1 (헤더 + 모든 STAB 페이지를 올림한 값)이며, 이 값이 disk_stab_init (§3.3)을 구동한다.
불변 조건 — STAB 크기는 nsect_max에 고정된다. disk_verify_volume_header는 stab_npages == CEIL_PTVDIV(nsect_max, DISK_STAB_PAGE_BIT_COUNT), stab_npages >= CEIL_PTVDIV(nsect_total, ...), stab_first_page == DISK_VOLHEADER_PAGE + 1을 assert한다. nsect_total로 크기를 결정하면 이후 확장 시 새 섹터에 대한 비트맵 비트가 없어 다음 헤더 fetch 시 assert가 발화한다.
3.2 disk_format과 disk_format_first_volume — 헤더 기록
섹션 제목: “3.2 disk_format과 disk_format_first_volume — 헤더 기록”disk_format은 어떤 볼륨이든 생성한다. disk_format_first_volume은 첫 번째 볼륨(LOG_DBFIRST_VOLID)과 캐시를 부트스트랩하는 얇은 shim이다. disk_manager_init을 호출하고, disk_Cache->nvols_perm = 1을 설정하며(실패 시 0으로 롤백), ext_info.nsect_total == ext_info.nsect_max로 설정한다(첫 번째 볼륨에는 여유분이 없다).
disk_format에는 많은 에러 경로가 있다. 아래 플로차트는 엣지 레이블로 모든 분기를 반영한다. 본문은 플로차트가 담을 수 없는 내용만 보완한다.
flowchart TD
A["이름 & purpose 유효성 검사"] -->|이름 너무 김| RET1["return ER_..._TOO_LONG"]
A -->|잘못된 purpose| RET2["return ER_DISK_UNKNOWN_PURPOSE"]
A -->|ok| B{"voltype == PERMANENT?"}
B -->|yes: undo RVDK_FORMAT 로그| C["양쪽 경로 모두 force flush\n이후 fileio_format OS 파일"]
B -->|no| C
C -->|NULL_VOLDES| RET3["return error, 정리할 것 없음"]
C -->|ok| E["페이지 0 NEW_PAGE fix,\nptype PAGE_VOLHEADER"]
E -->|fix 실패| X["goto exit"]
E -->|ok| F["헤더 채우기,\nset_stab"]
F --> G{"sys_lastpage >= extend_npages?"}
G -->|yes: ER_IO_FORMAT_BAD_NPAGES| X
G -->|no: params/name/remarks 설정, err goto exit| I{"PERMANENT?"}
I -->|yes: RVDK_NEWVOL + RVDK_FORMAT redo offset=-1| K["disk_stab_init"]
I -->|no| K
K -->|err| X
K -->|ok| L{"PERMANENT이고 volid != FIRST?"}
L -->|yes: disk_set_link 이전 볼륨, err goto exit| N{"PERMANENT?"}
L -->|no| N
N -->|yes: RVDK_FORMAT redo offset=0| P{"TEMPORARY?"}
N -->|no| P
P -->|yes: flush+dwb, sys pages temp-LSA, err goto exit| R["nsect_free_out, dirty_and_free,\nflush + dwb_synchronize"]
P -->|no| R
R --> X["exit: 헤더 페이지 unfix"]
X --> S{"error_code != NO_ERROR?"}
S -->|no| RET4["return NO_ERROR"]
S -->|yes| T["pgbuf_invalidate_all"]
T --> U{"TEMPORARY?"}
U -->|yes| V["disk_unformat 즉시 실행,\n임시 볼륨은 로그 없음"]
U -->|no| RET5["return error, rollback이 제거"]
V --> RET5
Figure 3-2: disk_format의 모든 분기. 아래쪽의 정리(cleanup) 분기가 크래시 안전성의 핵심이다.
플로차트가 완전히 담을 수 없는 두 가지 사항:
- undo는 논리적이고, force-flush는 무조건적이다. undo
RVDK_FORMAT(log_append_undo_data, 이름만 저장)만voltype == DB_PERMANENT_VOLTYPE에 조건부다 — 페이지 단위 undo가 없으므로 rollback이 볼륨 전체를 제거할 수 있게 한다. 그러나logpb_force_flush_pages는 양쪽 경로 모두에서 실행된다. 따라서 OS 파일이 존재하기 전에 로그가 디스크에 도달하고, 포맷 도중 크래시가 발생해도 복구가 가능하다. exit:분기. fix 이후 에러 발생 시goto exit는 헤더 페이지를 unfix한 후pgbuf_invalidate_all을 실행한다. 임시 볼륨은 즉시disk_unformat된다(로그 없음, rollback 의존 없음). 영구 볼륨은 에러를 반환하고 top-action rollback(Chapter 5)이 논리 undo를 재실행하도록 맡긴다. 두 개의 영구RVDK_FORMATredo는disk_stab_init이전에addr.offset = -1, 링크 연결 이후에0을 사용한다 — 복구가 시작된 포맷과 완료된 포맷을 구별하기 위한 sentinel 값이다.
3.3 disk_stab_init — 비트맵 초기화
섹션 제목: “3.3 disk_stab_init — 비트맵 초기화”헤더가 기록된 후, disk_stab_init은 모든 STAB 페이지를 순회하며 시스템 섹터(헤더+STAB가 차지하는 섹터)를 예약 상태로 표시하고 나머지는 0(free)으로 둔다.
// disk_stab_init -- src/storage/disk_manager.cDKNSECTS nsects_sys = SECTOR_FROM_PAGEID (volheader->sys_lastpage) + 1; /* <- 사전 예약할 섹터 수 */assert (nsects_sys < DISK_STAB_PAGE_BIT_COUNT); /* <- 시스템 영역이 STAB 페이지 0에 들어감을 보장 */for ( /* 각 STAB 페이지 */ ; ...; vpid_stab.pageid++) { page_stab = pgbuf_fix (..., NEW_PAGE, PGBUF_LATCH_WRITE, ...); // NULL -> return error pgbuf_set_page_ptype (thread_p, page_stab, PAGE_VOLBITMAP); if (volheader->purpose == DB_TEMPORARY_DATA_PURPOSE) pgbuf_set_lsa_as_temporary (...); /* <- 임시 볼륨은 로그 없음 */ memset (page_stab, 0, DB_PAGESIZE); /* <- 기본적으로 모든 섹터 free */ if (nsects_sys > 0) /* <- 시스템 섹터가 남아있는 동안만 실행 (페이지 0만) */ { nsect_copy = nsects_sys; disk_stab_cursor_set_at_sectid (volheader, /* page start */ ..., &start_cursor); if ( /* 마지막 STAB 페이지 */ ) disk_stab_cursor_set_at_end (volheader, &end_cursor); /* <- end = nsect_total */ else disk_stab_cursor_set_at_sectid (volheader, /* next page start */ ..., &end_cursor); error_code = disk_stab_iterate_units (..., disk_stab_set_bits_contiguous, &nsect_copy); } // err -> unfix + return if (volheader->purpose != DB_TEMPORARY_DATA_PURPOSE) /* <- 영구 볼륨: 페이지 이미지가 아닌 카운트만 로그 */ { DKNSECTS nsects_set = nsects_sys - nsect_copy; log_append_redo_data2 (thread_p, RVDK_INITMAP, NULL, page_stab, NULL_OFFSET, sizeof (nsects_set), &nsects_set); } if (!LOG_ISRESTARTED ()) { pgbuf_set_dirty (...); pgbuf_flush (..., FREE); page_stab = NULL; } /* <- 포맷 시: flush, 이후 pool 무효화 */ else pgbuf_set_dirty_and_free (thread_p, page_stab); /* <- 복구 재실행 시: dirty+free */ nsects_sys = nsect_copy; nsect_copy = 0; /* <- 다음 페이지로 나머지 이월 (일반적으로 페이지 1 이후에는 0) */ }모든 분기는 인라인으로 태그되어 있다. 루프는 stab_npages번 실행되며 각 페이지를 0으로 초기화한다. nsects_sys > 0 블록은 첫 번째 페이지에서만 실행되며(assert가 시스템 섹터가 그 안에 들어감을 보장), disk_stab_set_bits_contiguous는 BIT64_FULL 단위 전체를 채운 다음 end cursor까지 남은 비트를 채운다.
3.4 disk_unformat — OS 파일 삭제
섹션 제목: “3.4 disk_unformat — OS 파일 삭제”파괴 과정은 단순하다. 디스크 매니저는 인메모리 비트맵을 소유하지 않으므로, disk_unformat은 flush, 페이지 버퍼 이미지 무효화, 파일 삭제만 수행한다.
// disk_unformat -- src/storage/disk_manager.cvolid = fileio_find_volume_id_with_label (thread_p, vol_fullname);if (volid != NULL_VOLID) { (void) pgbuf_flush_all (thread_p, volid); /* <- 더티 페이지 내보내기 */ (void) pgbuf_invalidate_all (thread_p, volid); /* <- 풀에서 제거 */ }fileio_unformat (thread_p, vol_fullname); /* <- OS 파일 삭제 */return ret; /* <- 항상 NO_ERROR */유일한 분기는 volid != NULL_VOLID다. 마운트되지 않은 볼륨(레이블에 해당하는 id 없음)은 flush/invalidate를 건너뛰고 fileio_unformat만 실행한다. 이 함수는 §3.2에서 임시 볼륨의 에러 경로에서 disk_format이 호출하고, 영구 포맷 undo 시 복구가 호출하는 함수다.
3.5 비트맵-as-functor 패턴
섹션 제목: “3.5 비트맵-as-functor 패턴”호출자는 STAB을 비트 단위로 읽지 않는다. 매니저는 이를 64비트 단위로 양자화하고, 단위 범위마다 DISK_STAB_UNIT_FUNC 콜백을 구동하는 단일 이터레이터 — disk_stab_iterate_units — 를 노출한다. 예약, 예약 해제, free 카운트, 사용 여부 확인, 연속 설정 모두 서로 다른 콜백일 뿐이다.
양자화. DISK_STAB_UNIT은 UINT64다. SECTID를 위치로 매핑하는 매크로는 순수한 정수 산술이다 — 평면 인덱스를 (page, unit, bit)로 분할한다.
// allocation-table addressing macros -- src/storage/disk_manager.c#define DISK_ALLOCTBL_SECTOR_PAGE_OFFSET(sect) ((sect) / DISK_STAB_PAGE_BIT_COUNT)#define DISK_ALLOCTBL_SECTOR_UNIT_OFFSET(sect) (((sect) % DISK_STAB_PAGE_BIT_COUNT) / DISK_STAB_UNIT_BIT_COUNT)#define DISK_ALLOCTBL_SECTOR_BIT_OFFSET(sect) (((sect) % DISK_STAB_PAGE_BIT_COUNT) % DISK_STAB_UNIT_BIT_COUNT)#define DISK_STAB_NPAGES(nsect_max) (CEIL_PTVDIV (nsect_max, DISK_STAB_PAGE_BIT_COUNT))DISK_STAB_NPAGES는 disk_volume_header_set_stab에서 사용한 것과 동일한 CEIL_PTVDIV를 사용하여 헤더 필드와 매크로가 일치함을 보장한다.
flowchart LR SECT["SECTID"] --> PG["페이지 오프셋\nsect / PAGE_BIT_COUNT"] SECT --> UN["유닛 오프셋\n(sect mod PAGE_BIT_COUNT) / 64"] SECT --> BT["비트 오프셋\n(sect mod PAGE_BIT_COUNT) mod 64"] PG --> POS["cursor.pageid = stab_first_page + 페이지 오프셋"] UN --> POS2["cursor.offset_to_unit"] BT --> POS3["cursor.offset_to_bit"]
Figure 3-3: SECTID가 세 개의 나머지/나눗셈 매크로에 의해 (page, unit, bit)로 분할된다. cursor는 세 값 모두와 live unit 포인터를 저장한다.
Cursor 위치 설정
섹션 제목: “Cursor 위치 설정”세 개의 인라인 setter가 DISK_STAB_CURSOR(필드는 Chapter 1)를 초기화한다. 대상 섹터만 다를 뿐 모두 page/unit을 NULL로 둔다(페이지는 disk_stab_cursor_fix에 의해 지연 fix된다).
disk_stab_cursor_set_at_sectid— 일반 케이스:0 <= sectid <= nsect_total을 assert하고, 세 매크로로pageid/offset_to_unit/offset_to_bit를 채우며,pageid가stab_npages범위 내에 있음을 assert한다.disk_stab_cursor_set_at_end—set_at_sectid(volheader, nsect_total, cursor)로 마지막 유효 섹터의 다음 위치를 가리킨다.nsect_total이 unit-rounded(DISK_SECTS_ASSERT_ROUNDED)임을 먼저 assert해 반복이 64비트 경계에서 끝남을 보장한다.disk_stab_cursor_set_at_start—sectid = 0,pageid = stab_first_page, 두 오프셋 모두0으로 하드코딩한다(set_at_sectid를 건너뜀. 모두 0인 위치는 자명하다).
불변 조건 — cursor 위치 일관성. disk_stab_cursor_check_valid는 (pageid - stab_first_page) * PAGE_BIT_COUNT + offset_to_unit * 64 + offset_to_bit == sectid를 assert하고, unit != NULL일 때 (char*)unit - page == offset_to_unit * DISK_STAB_UNIT_SIZE_OF를 assert한다. 이터레이터는 콜백 호출 전마다 이를 재확립한다. 오프셋이 sectid와 불일치하면 예약된 VSID가 잘못된 섹터를 가리켜 — 조용한 cross-linking 손상이 발생한다.
이터레이터
섹션 제목: “이터레이터”// disk_stab_iterate_units -- src/storage/disk_manager.cassert (disk_stab_cursor_compare (start, end) < 0); /* <- start가 end보다 엄격히 앞에 있어야 함 */for (cursor = *start; cursor.pageid <= end->pageid; cursor.pageid++, cursor.offset_to_unit = 0) { error_code = disk_stab_cursor_fix (thread_p, &cursor, mode); /* <- 이 STAB 페이지 fix */ // ... err -> return ... end_unit = ((DISK_STAB_UNIT *) cursor.page) + (cursor.pageid == end->pageid ? end->offset_to_unit : DISK_STAB_PAGE_UNITS_COUNT); /* <- 마지막 페이지 클램프 */ for (; cursor.unit < end_unit; cursor.unit++, cursor.offset_to_unit++, cursor.sectid += (DISK_STAB_UNIT_BIT_COUNT - cursor.offset_to_bit), /* <- 남은 비트만큼 전진 */ cursor.offset_to_bit = 0) { error_code = f_unit (thread_p, &cursor, &stop, f_unit_args); /* <- functor 호출 */ if (error_code != NO_ERROR) { disk_stab_cursor_unfix (...); return error_code; } if (stop) { disk_stab_cursor_unfix (...); return NO_ERROR; } /* <- 조기 종료 */ } disk_stab_cursor_unfix (thread_p, &cursor); }내부 stride는 sectid를 DISK_STAB_UNIT_BIT_COUNT - cursor.offset_to_bit만큼 전진시킨다 — 보통은 64 전체이지만, 콜백이 offset_to_bit를 유닛 중간에 남겨둘 수 있으므로(disk_stab_unit_reserve처럼) stride가 보정한다. 두 가지 단락(short-circuit)은 페이지를 먼저 unfix한다: 콜백 에러(에러 반환)와 콜백이 *stop = true를 설정한 경우(NO_ERROR 반환). disk_stab_iterate_units_all은 set_at_start/set_at_end로 이를 감싼다.
예약 — disk_stab_unit_reserve
섹션 제목: “예약 — disk_stab_unit_reserve”분기가 가장 많은 functor다. nsects_lastvol_remaining까지의 free 비트를 예약하고 각 VSID를 기록한다. 세 분기 모두 인라인으로 태그되어 있다.
// disk_stab_unit_reserve -- src/storage/disk_manager.cif (*cursor->unit == BIT64_FULL) return NO_ERROR; /* <- (1) 꽉 찬 유닛: free 없음, 건너뜀; dirty/log 없음 */context = (DISK_RESERVE_CONTEXT *) args;if (*cursor->unit == 0) /* <- (2) 빈 유닛: 한 번의 store로 최대 64개 확보 */ { int bits_to_set = MIN (context->nsects_lastvol_remaining, DISK_STAB_UNIT_BIT_COUNT); *cursor->unit = (bits_to_set == DISK_STAB_UNIT_BIT_COUNT) ? BIT64_FULL : bit64_set_trailing_bits (*cursor->unit, bits_to_set); log_unit = *cursor->unit; context->nsects_lastvol_remaining -= bits_to_set; /* ... VSID 하나씩 방출 ... */ }else /* <- (3) 혼합 유닛: 선행 1 건너뛰고 각 free 비트 설정 */ { log_unit = 0; for (cursor->offset_to_bit = bit64_count_trailing_ones (*cursor->unit), cursor->sectid += cursor->offset_to_bit; cursor->offset_to_bit < DISK_STAB_UNIT_BIT_COUNT && context->nsects_lastvol_remaining > 0; cursor->offset_to_bit++, cursor->sectid++) if (!disk_stab_cursor_is_bit_set (cursor)) { disk_stab_cursor_set_bit (cursor); log_unit = bit64_set (log_unit, cursor->offset_to_bit); context->nsects_lastvol_remaining--; /* ... VSID push ... */ } }assert (log_unit != 0 && (log_unit & *cursor->unit) == log_unit);if (context->purpose == DB_PERMANENT_DATA_PURPOSE) /* <- 영구 볼륨: undoredo delta; 임시 볼륨은 로깅 건너뜀 */ log_append_undoredo_data2 (thread_p, RVDK_RESERVE_SECTORS, NULL, cursor->page, cursor->offset_to_unit, sizeof (log_unit), sizeof (log_unit), &log_unit, &log_unit);pgbuf_set_dirty (thread_p, cursor->page, DONT_FREE);if (context->nsects_lastvol_remaining <= 0) *stop = true;log_unit은 이번 호출에서 설정한 비트만 누적한다. 영구 볼륨에서는 RVDK_RESERVE_SECTORS의 redo 이미지와 undo 이미지 모두로 사용된다(redo는 재설정, undo는 해제).
불변 조건 — log_unit은 유닛의 설정된 비트의 엄격한 부분집합이다. assert (log_unit != 0 && (log_unit & *cursor->unit) == log_unit)는 로깅된 delta가 실제로 설정된 비트만 담고 no-op이 아님을 보장한다. *cursor->unit에 없는 비트가 있으면 복구의 redo가 live 실행에서 설정되지 않은 비트를 설정하게 되어 — 로깅된 비트맵과 live 비트맵 사이에 divergence가 발생한다.
예약 해제 — disk_stab_unit_unreserve
섹션 제목: “예약 해제 — disk_stab_unit_unreserve”mirror functor로, 호출자가 이미 알고 있는(context->vsidp에 정렬된) 섹터 ID의 비트를 해제한다.
// disk_stab_unit_unreserve -- src/storage/disk_manager.cwhile (context->nsects_lastvol_remaining > 0 && context->vsidp->sectid < cursor->sectid + DISK_STAB_UNIT_BIT_COUNT) { unreserve_bits = bit64_set (unreserve_bits, context->vsidp->sectid - cursor->sectid); /* <- 이 유닛 윈도우 누적, abs->rel 비트 변환 */ context->nsects_lastvol_remaining--; context->vsidp++; nsect++; }assert ((unreserve_bits & (*cursor->unit)) == unreserve_bits); /* <- 설정된 비트만 해제 */if (unreserve_bits != 0) /* <- 변경 없는 유닛 건너뜀 */ { if (context->purpose == DB_PERMANENT_DATA_PURPOSE) /* <- 영구 볼륨: commit 시 해제를 postpone, rollback은 건너뜀 */ log_append_postpone (thread_p, RVDK_UNRESERVE_SECTORS, &addr /* page,offset_to_unit */, ..., &unreserve_bits); else /* <- 임시 볼륨: 즉시 해제 + temp reserve lock 하에 캐시 업데이트 */ { (*cursor->unit) &= ~unreserve_bits; pgbuf_set_dirty (thread_p, cursor->page, DONT_FREE); disk_cache_update_vol_free (cursor->volheader->volid, nsect); } }if (context->nsects_lastvol_remaining <= 0) *stop = true;purpose 분기가 기억할 비대칭이며, 인라인으로 태그되어 있다. 영구 볼륨의 unreserve는 postpone 레코드를 방출하므로 rollback이 이를 절대 실행하지 않아 섹터가 예약 상태로 유지된다. 임시 볼륨의 unreserve는 즉시 해제하고 캐시의 free 카운트를 업데이트한다.
불변 조건 — unreserve는 설정된 비트만 해제한다. assert((unreserve_bits & *cursor->unit) == unreserve_bits)는 해제되는 모든 섹터가 실제로 예약 상태였음을 강제한다. 위반은 이중 해제 또는 stale VSID 목록을 의미하며, free 섹터 회계가 손상된다.
3.6 64비트 결합과 hint_allocsect 비고
섹션 제목: “3.6 64비트 결합과 hint_allocsect 비고”숨겨진 64비트 결합. cursor 프리미티브는 bit64_is_set, bit64_set, bit64_set_trailing_bits, bit64_count_trailing_ones, bit64_count_zeros를 호출하는데 — 모두 64비트 피연산자에 하드와이어드되어 있다. DISK_STAB_UNIT 주석은 단위 타입이 “수정되고 자동으로 처리될 수 있다”고 암시하지만, typedef UINT64 DISK_STAB_UNIT을 변경하면 모든 bit64_* 호출과 BIT64_FULL이 조용히 깨진다. 양자화 매크로는 DISK_STAB_UNIT_SIZE_OF로 적응하지만, 비트 연산 레이어는 그렇지 않다. (“자동” 주장이 실제로 맞았던 적이 있었는지는 미해결 질문이다.) 64비트를 고정된 계약으로 취급하라.
hint_allocsect. disk_format은 이를 NULL_SECTID로만 초기화한다. live 업데이트는 Chapter 4가 소유하는 예약 경로(disk_reserve_sectors_in_volume)에 있다. 여기서 관련된 미묘함: unreserve 이후에 stale해진다 — disk_stab_unit_unreserve는 hint 아래의 비트를 해제하지만 hint를 낮추지는 않으므로, 이후 예약이 새로 해제된 섹터를 건너뛰고 wrap-around 패스가 이를 회수할 때까지 기다린다. 이는 최적화이지 불변 조건이 아니므로, 코드는 이를 로깅하거나 dirty로 표시하지 않는다.
3.7 챕터 요약 — 핵심 정리
섹션 제목: “3.7 챕터 요약 — 핵심 정리”- 레이아웃은 고정되어 있으며 헤더가 구동한다. 페이지 0은 헤더다.
stab_first_page(항상 1)에서DISK_STAB_NPAGES(nsect_max)로 크기가 결정된 연속 STAB이 시작되고, 데이터는sys_lastpage다음에 온다.nsect_total이 아닌nsect_max로 크기를 결정함으로써 볼륨이 재배치 없이 성장할 수 있다. disk_format은 크래시 안전성을 위해 분기가 많다. 논리 undo(RVDK_FORMAT)는 영구 볼륨에만 해당하지만,logpb_force_flush_pages는 OS 파일 생성 전에 무조건 실행된다. 영구 볼륨은 헤더 redo를 두 번 로깅하며(offset-1후0), 임시 볼륨은 temp-LSA를 사용하고 에러 시 즉시disk_unformat된다.disk_stab_init은 정확히 시스템 섹터를 사전 예약한다 (SECTOR_FROM_PAGEID(sys_lastpage)+1, 모두 첫 번째 STAB 페이지에), 나머지는 free로 두고, 페이지 이미지가 아닌 카운트만 로깅한다(RVDK_INITMAP).- 비트맵은 비트 단위로 스캔되지 않는다.
SECTID는 세 개의 매크로로(page, unit, bit)로 분해되고,disk_stab_iterate_units는 64비트 단위마다DISK_STAB_UNIT_FUNC를 구동하며 full/empty 유닛에서 단락된다. - 예약과 예약 해제는 purpose 분기가 있는 mirror functor다. 영구 예약은 undoredo delta를 로깅하고, 영구 예약 해제는 postpone 레코드를 사용해 rollback이 섹터를 유지하도록 한다. 임시 볼륨은 로깅을 건너뛰고 캐시를 직접 업데이트한다.
log_unit/unreserve_bits불변 조건이 로깅된 비트맵과 live 비트맵을 동기화 상태로 유지한다. - 64비트는 조정 가능한 값이 아닌 하드 계약이다:
bit64_*프리미티브와BIT64_FULL은DISK_STAB_UNIT의 낙관적 주석에도 불구하고 단위 크기로 매개변수화되지 않는다. hint_allocsect는 Chapter 4가 소유하는 live 상태다.disk_format은 이를NULL_SECTID로만 초기화한다. 여기서의 미묘함은 unreserve 이후의 staleness — hint 아래 섹터를 해제해도 hint가 낮아지지 않는다.
Chapter 4: 섹터 예약 2단계 프로토콜
섹션 제목: “Chapter 4: 섹터 예약 2단계 프로토콜”N개의 섹터가 필요한 파일은 하나의 잠금 아래에서 N개의 비트를 한꺼번에 뒤집지 않는다. 디스크 매니저는 작업을 서로 독립된 두 단계로 분리한다(상위 동반 문서인 CUBRID Disk Manager에서 캐시가 왜 존재하는지를 설명한다). 이 챕터의 질문은 하나다: 파일이 N개의 섹터를 요청할 때, 디스크 매니저는 어떻게 hot 뮤텍스 구간을 짧게 유지하면서 크래시-세이프하게 여러 볼륨에 걸쳐 섹터를 배분하는가?
4.1 예약을 담는 두 구조체
섹션 제목: “4.1 예약을 담는 두 구조체”예약 정보는 disk_reserve_context가 보관한다. 이 구조체는 disk_reserve_sectors 안에 스택 로컬로 선언되며(unreserve 경로에서도 재구성된다), 아래의 모든 함수에 인자로 관통된다.
// disk_reserve_context -- src/storage/disk_manager.cstruct disk_reserve_context{ int nsect_total; /* original request size */ VSID *vsidp; /* write cursor into output array */ DISK_CACHE_VOL_RESERVE cache_vol_reserve[VOLID_MAX]; /* per-volume ledger from step 1 */ int n_cache_vol_reserve; /* ledger slots used */ int n_cache_reserve_remaining; /* cache-phase debt */ DKNSECTS nsects_lastvol_remaining; /* current-volume bitmap debt */ DB_VOLPURPOSE purpose; /* permanent-data / temporary-data */};| 필드 | 역할 | 존재 이유 |
|---|---|---|
nsect_total | 요청 크기 N의 불변 복사본. | 최종 assert (vsidp - reserved_sectors == n_sectors) 에서 사용; 절대 감소하지 않음. |
vsidp | reserved_sectors[] 배열에 대한 쓰기 포인터. | vsidp - reserved_sectors = 지금까지 예약된 섹터 수; 오류 경로에서 롤백 범위를 읽을 때 사용. |
cache_vol_reserve[] | 1단계 원장, 볼륨별 {volid, nsect} 하나. | 2단계에서 재생; 오류 경로에서 아직 비트가 뒤집히지 않은 섹터를 환급할 때 사용. |
n_cache_vol_reserve | 사용된 원장 슬롯 수. | 2단계 루프와 롤백 스캔의 반복 상한. |
n_cache_reserve_remaining | 캐시 단계 부채; N에서 시작해 disk_reserve_from_cache_volume이 감소시키며, 0이 되면 완료. | 1단계에서 볼륨 순회와 확장 결정을 구동. |
nsects_lastvol_remaining | 비트맵 단계 부채(현재 볼륨 단위); 볼륨별로 초기화되고 비트가 뒤집힐 때마다 감소. | disk_stab_unit_reserve가 이 값을 기준으로 동작; 0이 되면 *stop 설정. |
purpose | DB_PERMANENT_DATA_PURPOSE / DB_TEMPORARY_DATA_PURPOSE. | 캐시 뮤텍스/확장 정보 선택, STAB 변경 로깅 여부 결정. |
// disk_cache_vol_reserve -- src/storage/disk_manager.cstruct disk_cache_vol_reserve { VOLID volid; DKNSECTS nsect; };| 필드 | 역할 | 존재 이유 |
|---|---|---|
volid | 캐시가 예약한 볼륨. | 2단계에서 해당 볼륨의 헤더를 고치고 비트를 뒤집음; 롤백 시 캐시 카운터 감소. |
nsect | volid에서 약속한 섹터 수. | nsects_lastvol_remaining의 초기값; 롤백 시 undo로 반환된 섹터만큼 감소하며 아직 뒤집히지 않은 잔여분을 남김. |
원장 항목 {volid, nsect} 각각이 2단계의 볼륨별 스캔 하나를 개시한다(Figure 4-1).
flowchart LR
RC["disk_reserve_context"] --> L["cache_vol_reserve[i]\n{volid, nsect}"] -.nsects_lastvol_remaining 초기화.-> S2["2단계 볼륨별 스캔 -> reserved_sectors[]"]
Figure 4-1. 예약 컨텍스트, 볼륨별 원장, 그리고 출력 배열을 채우는 2단계 스캔.
불변식 — 두 잔여 카운터는 절대 혼용되지 않는다.
n_cache_reserve_remaining은 캐시 부채이고, nsects_lastvol_remaining은 현재 볼륨 비트맵 부채다. 캐시 단계가 끝날 때 n_cache_reserve_remaining == 0이고 sum(cache_vol_reserve[i].nsect) == N이 된다. 이 둘을 분리함으로써 2단계는 캐시를 재건드리지 않고 볼륨 단위로 반복 실행될 수 있다; 혼용하면 볼륨 스캔이 부분적으로 완료됐을 때 캐시 회계를 오염시킬 수 있다.
4.2 외부 드라이버: disk_reserve_sectors
섹션 제목: “4.2 외부 드라이버: disk_reserve_sectors”disk_reserve_sectors(thread_p, purpose, volid_hint, n_sectors, reserved_sectors)는 디스크/파일 경계 호출이다. volid_hint는 인자로 받지만 실제로는 무시되며, 볼륨 순서는 purpose가 결정한다.
- 가드 검사.
assert로 purpose가 perm 또는 temp인지 확인;n_sectors <= 0 || reserved_sectors == NULL이면assert_release(false); ER_FAILED. - 영구(permanent) 예약에 대한 sysop 선행 조건 (해당 STAB 변경은 외부 트랜잭션의 로그에 기록된다):
// disk_reserve_sectors -- src/storage/disk_manager.cif (purpose != DB_TEMPORARY_DATA_PURPOSE && !log_check_system_op_is_started (thread_p)){ assert (false); er_set (...ER_GENERIC_ERROR, 0); return ER_FAILED; } /* caller forgot sysop */
retry:/log_sysop_start— 임시(temp) 예약도 비트맵 단계의 범위를 지정하기 위해 sysop을 연다.CSECT_DISK_CHECK를 reader로 획득 (일관성 검사기와의 상호 배제). 실패 시log_sysop_abort; return.- 컨텍스트 초기화:
nsect_total = n_cache_reserve_remaining = n_sectors,vsidp = reserved_sectors,n_cache_vol_reserve = 0. - 1단계 —
disk_reserve_from_cache호출. 오류 발생 시goto error. - 2단계 —
disk_reserve_sectors_in_volume을[0, n_cache_vol_reserve)범위로 루프. 오류 발생 시goto error. - 성공.
assert ((vsidp - reserved_sectors) == n_sectors); csect 해제;log_sysop_attach_to_outer; 디버그 빌드에서did_extend이면disk_check;return NO_ERROR.error:경로는 4.7절에서 롤백을 처리한다.
flowchart TD C["csect_enter CSECT_DISK_CHECK (sysop 시작 후)"] -->|실패| AB["log_sysop_abort, 오류 반환"] C -->|성공| D["컨텍스트 초기화"] --> E["1단계: disk_reserve_from_cache"] E -->|오류| ERR["goto error: 롤백 (4.7)"] E -->|성공| F["2단계 루프: 원장 항목별 disk_reserve_sectors_in_volume"] F -->|오류| ERR F -->|성공| H["assert vsidp-base==N, attach_to_outer, NO_ERROR"]
Figure 4-2. disk_reserve_sectors 제어 흐름.
4.3 1단계 진입점: disk_reserve_from_cache
섹션 제목: “4.3 1단계 진입점: disk_reserve_from_cache”reserve 뮤텍스를 카운터 연산 구간에만 잡은 채로, 여유 섹터 수를 원장으로 이동시키고 공간이 부족하면 디스크를 확장한다.
disk_Cache == NULL->assert_release(false); return ER_FAILED.- purpose의 reserve 뮤텍스 획득 (
disk_cache_lock_reserve_for_purpose). - 임시 purpose는 perm-type-temp-purpose 볼륨을 진짜 temp 볼륨보다 먼저 선호:
// disk_reserve_from_cache -- src/storage/disk_manager.cif (context->purpose == DB_TEMPORARY_DATA_PURPOSE){extend_info = &disk_Cache->temp_purpose_info.extend_info;if (disk_Cache->temp_purpose_info.nsect_perm_free > 0)disk_reserve_from_cache_vols (DB_PERMANENT_VOLTYPE, context); /* <- perm-temp first */if (context->n_cache_reserve_remaining <= 0) /* satisfied from perm-temp */{ disk_cache_unlock_reserve_for_purpose (context->purpose); return NO_ERROR; }// ... temp-ceiling check, then fall through to temp-volume extend ...}elseextend_info = &disk_Cache->perm_purpose_info.extend_info;
nsect_perm_free는 temp purpose를 수용하는 perm 타입 볼륨의 여유 섹터 수이며, 0이면 해당 볼륨들은 건너뛴다. - 임시 공간 상한 검사 (temp 분기, temp 볼륨 확장 전):
extend_info->nsect_total - extend_info->nsect_free + n_cache_reserve_remaining > disk_Temp_max_sects이면er_set (ER_BO_MAXTEMP_SPACE_HAS_BEEN_EXCEEDED ...); unlock; return. 피연산자는 컨텍스트의nsect_total이 아니라 extend-info 풀 집계값이다. - 공통 꼬리:
assert (n_cache_reserve_remaining > 0)및 이 스레드가 뮤텍스를 보유 중인지 assert. - 풀에 여유가 있으면 기존 공간에서 예약:
if (extend_info->nsect_free > context->n_cache_reserve_remaining) /* strict >: 약간의 여유 확보, 5장 참조 */{disk_reserve_from_cache_vols (extend_info->voltype, context);if (context->n_cache_reserve_remaining <= 0){ disk_cache_unlock_reserve (extend_info); return NO_ERROR; } /* <- 기존 공간으로 완료 */}
- 공간 부족 시 확장.
extend_info->nsect_intention을 증가시켜(동시 예약자에게 신호) reserve 뮤텍스를 해제하고,disk_lock_extend()를 획득한 다음 reserve 뮤텍스를 재획득해 다시 검사한다. 다른 스레드가 이미 확장해서nsect_free가 충분해졌다면: intention을 감소시키고,disk_reserve_from_cache_vols를 재시도한 후 반환한다. 그렇지 않으면disk_extend(5장)를 호출하고 intention을 되돌린다. 모든 출구에서 두 잠금이 해제된다. - 확장 후.
disk_extend오류 시 해당 오류를 반환한다. 그래도n_cache_reserve_remaining > 0이면assert_release(false); ER_FAILED. 그 외에는*did_extend = true; return NO_ERROR.
불변식 — reserve 뮤텍스는 STAB 스캔이나 확장 중에는 절대 보유하지 않는다. intention 카운터가 느린 확장 중에 뮤텍스를 내려놓을 수 있게 해주는 핸드오프 토큰이며, 이 덕분에 두 스레드가 이중으로 확장하는 사태를 막는다.
4.4 볼륨 순회: disk_reserve_from_cache_vols
섹션 제목: “4.4 볼륨 순회: disk_reserve_from_cache_vols”// disk_reserve_from_cache_vols -- src/storage/disk_manager.cif (type == DB_PERMANENT_VOLTYPE) /* perm: ascend 0..nvols_perm */ { start_iter = 0; end_iter = disk_Cache->nvols_perm; incr = 1; min_free = MIN (context->nsect_total, perm...nsect_vol_max) / 2; }else /* temp: descend from top of volid space */ { start_iter = LOG_MAX_DBVOLID; end_iter = LOG_MAX_DBVOLID - disk_Cache->nvols_temp; incr = -1; min_free = MIN (context->nsect_total, temp...nsect_vol_max) / 2; }min_free = MAX (min_free, 1); /* half the smaller of request/per-vol max, floored at 1 */
for (volid_iter = start_iter; volid_iter != end_iter && context->n_cache_reserve_remaining > 0; /* stop when range exhausted or debt paid */ volid_iter += incr) { if (disk_Cache->vols[volid_iter].purpose != context->purpose) continue; /* wrong purpose */ if (disk_Cache->vols[volid_iter].nsect_free < min_free) continue; /* too fragmented */ disk_reserve_from_cache_volume (volid_iter, context); }4.5 볼륨 하나의 카운터 감소: disk_reserve_from_cache_volume
섹션 제목: “4.5 볼륨 하나의 카운터 감소: disk_reserve_from_cache_volume”1단계에서 섹터를 캐시 밖으로 실제로 이동시키는 유일한 지점이다.
// disk_reserve_from_cache_volume -- src/storage/disk_manager.cif (context->n_cache_vol_reserve >= LOG_MAX_DBVOLID) { assert_release (false); return; } /* <- 원장 오버플로 방지 */disk_check_own_reserve_for_purpose (context->purpose); /* <- 우리가 뮤텍스를 보유 중인지 assert */nsects = MIN (disk_Cache->vols[volid].nsect_free, context->n_cache_reserve_remaining);disk_cache_update_vol_free (volid, -nsects); /* <- 캐시 + purpose 풀 감소 */context->cache_vol_reserve[context->n_cache_vol_reserve].volid = volid;context->cache_vol_reserve[context->n_cache_vol_reserve].nsect = nsects;context->n_cache_reserve_remaining -= nsects;context->n_cache_vol_reserve++; /* <- 비트맵 미접촉, 카운터만 변경 */disk_cache_update_vol_free는 해당 purpose 풀의 집계값도 함께 조정한다.
4.6 2단계: disk_reserve_sectors_in_volume의 비트 뒤집기
섹션 제목: “4.6 2단계: disk_reserve_sectors_in_volume의 비트 뒤집기”원장 항목별로, write latch 아래(캐시 뮤텍스는 비보유 상태)에서 볼륨 헤더를 고정하고 볼륨별 부채가 0이 될 때까지 STAB 비트를 뒤집는다.
- 원장 읽기.
volid = cache_vol_reserve[vol_index].volid;NULL_VOLID이면assert_release(false); ER_FAILED.nsects_lastvol_remaining = cache_vol_reserve[vol_index].nsect로 초기화. - 볼륨 헤더
PGBUF_LATCH_WRITE로 고정; 오류 시return. - 힌트 기반 스캔.
disk_stab_iterate_units(..., disk_stab_unit_reserve, context)를 통한 세 가지 스캔 형태; 각 오류 경로는goto exit:// disk_reserve_sectors_in_volume -- src/storage/disk_manager.cif (volheader->hint_allocsect > 0 && volheader->hint_allocsect < volheader->nsect_total){// ... cursors hint..end; iterate ... /* after hint */if (context->nsects_lastvol_remaining > 0) /* still short: wrap start..hint */{ end_cursor = start_cursor; disk_stab_cursor_set_at_start (volheader, &start_cursor);error_code = disk_stab_iterate_units (...); }}else{ /* ... cursors start..end; iterate whole table ... */ } - 반드시 완료되어야 한다.
if (nsects_lastvol_remaining != 0) { assert_release(false); ER_FAILED; goto exit; }— 잔여가 남는다는 것은 캐시와 비트맵이 불일치한다는 뜻(버그)이다. - 힌트 전진.
hint_allocsect = (vsidp - 1)->sectid + 1; 최선 노력(best-effort)으로, dirty 표시도 로깅도 하지 않는다. exit:고정된 헤더가 있으면 해제;return error_code.
비트 뒤집기는 disk_stab_unit_reserve 콜백 안에 있으며, 64비트 STAB unit 단위로 호출된다(unit이 BIT64_FULL이면 조기 반환; 0이면 일괄 채움; 중간값은 비트 단위 처리). 새로운 섹터는 각각 context->vsidp에 기록된다. 영구(permanent) purpose는 변경마다 로그를 남긴다:
// disk_stab_unit_reserve -- src/storage/disk_manager.cif (context->purpose == DB_PERMANENT_DATA_PURPOSE) /* redo+undo image = the changed bits mask */ log_append_undoredo_data2 (thread_p, RVDK_RESERVE_SECTORS, NULL, cursor->page, cursor->offset_to_unit, sizeof (log_unit), sizeof (log_unit), &log_unit, &log_unit);pgbuf_set_dirty (thread_p, cursor->page, DONT_FREE);if (context->nsects_lastvol_remaining <= 0) { *stop = true; } /* <- end the volume scan */redo 이미지와 undo 이미지는 동일한 log_unit 마스크이며, 복구 핸들러 disk_rv_reserve_sectors / disk_rv_unreserve_sectors는 CSECT_DISK_CHECK 아래에서 캐시를 재동기화한다(복구 챕터). 임시(temporary) 예약은 로그를 남기지 않는다 — 재시작 시 비트맵이 전체 초기화되므로.
불변식 — 캐시 뮤텍스는 2단계 전체에서 해제된 상태다. 1단계에서 카운터를 선차감했으므로, 2단계는 page latch와 WAL만 건드린다. 결과적으로 hot reserve 뮤텍스는 O(볼륨 수) 카운터 연산에만 잡히며, O(섹터 수) 비트맵 I/O에는 절대 걸리지 않는다.
4.7 실패와 롤백
섹션 제목: “4.7 실패와 롤백”둘 중 어느 단계에서든 오류가 발생하면 disk_reserve_sectors는 error:로 점프한다. nreserved = vsidp - reserved_sectors를 실제로 뒤집힌 섹터 수라 하자.
nreserved > 0이고 temp purpose인 경우: 아무것도 로깅되지 않았으므로 abort가 부분 비트맵 변경을 되돌릴 수 없다; 인터럽트 검사를 비활성화하고, VSID를qsort한 다음,disk_unreserve_ordered_sectors_without_csect를 호출한다. permanent는 이 단계를 건너뛴다 — 아래의log_sysop_abort가 로깅된 변경을 undo한다.- 원장과 abort/undo 결과를 조정한다: 뒤집힌 섹터마다 해당 볼륨의
cache_vol_reserve[].nsect를 감소시켜 아직 뒤집히지 않은 섹터만 남긴다:// disk_reserve_sectors (error path) -- src/storage/disk_manager.cfor (iter_vsid = 0; iter_vsid < nreserved; iter_vsid++){for (iter = 0; iter < context.n_cache_vol_reserve; iter++)if (reserved_sectors[iter_vsid].volid == context.cache_vol_reserve[iter].volid){ context.cache_vol_reserve[iter].nsect--; break; } /* <- don't double-credit */assert (iter < context.n_cache_vol_reserve);} - 잔여분 환급:
disk_cache_free_reserved(&context)를 호출해(reserve 뮤텍스 아래에서disk_cache_update_vol_free로 남은nsect를 되돌린다). - csect 해제,
log_sysop_abort— permanent purpose는 이 과정에서 로깅된 STAB 비트가 롤백된다. - 오류 분류. 예상 가능한 IO/인터럽트 오류(
ER_INTERRUPTED,ER_IO_MOUNT_FAIL,ER_IO_FORMAT_OUT_OF_SPACE,ER_IO_WRITE,ER_BO_CANNOT_CREATE_VOL)는 그대로 반환된다. 그 외의 오류는assert_release(false)를 트리거하고 자가 치유를 시도한다: 아직retried가 아니면disk_check(thread_p, true)를 호출하고, 결과가DISK_INVALID이면 오류를 지우고retried = true로 설정한 뒤goto retry. 두 번째 실패이거나 비-스큐 원인이면 그냥 반환한다.
disk_unreserve_ordered_sectors_without_csect는 정렬된 VSID 목록으로 새로운 컨텍스트를 재구성한다. 연속된 동일-volid 구간을 원장 항목으로 묶고(volid와 sectid가 오름차순인지 assert), 그룹별로 disk_unreserve_sectors_from_volume을 호출한다. 첫 번째 오류가 발생하면(ASSERT_ERROR (); return error_code;) 나머지 그룹은 환급하지 않고 반환한다. 이 함수의 disk_stab_unit_unreserve 콜백은 비트를 지우는 동시에 캐시에도 섹터를 돌려보내는데, 2단계 조정 루프가 이 “캐시도 이미 반환됨” 효과를 보상한다.
불변식 — 예약은 캐시→비트맵 순, 해제는 비트맵→캐시 순; 캐시는 절대 과잉 계상하지 않는다. 예약 시 카운터가 먼저 감소하고 비트가 나중에 설정된다; 해제 시 비트가 먼저 지워지고 카운터가 나중에 올라간다. 두 과도 상태 모두 캐시가 비트맵보다 적은 여유를 표시하게 만든다. 따라서 두 예약자가 동시에 같은 섹터가 비어 있다고 통보받는 일은 없다; disk_check가 제한된 스큐를 수리하며, 이것이 error: 경로가 disk_check를 통한 재시도를 할 수 있는 이유다.
4.8 챕터 요약 — 핵심 정리
섹션 제목: “4.8 챕터 요약 — 핵심 정리”- 두 단계는 완전히 분리된다. 1단계(
disk_reserve_from_cache)는 짧은 reserve 뮤텍스 아래에서 캐시의 여유 섹터 수를 이동시키고, 2단계(disk_reserve_sectors_in_volume)는 그 뮤텍스가 해제된 상태에서 page latch 아래 STAB 비트를 뒤집는다. - 두 부채 카운터는 혼용되지 않는다.
n_cache_reserve_remaining(캐시)과nsects_lastvol_remaining(볼륨별 비트맵)은 서로 독립적이며, 2단계는cache_vol_reserve[]를 기준으로 볼륨 단위로 구동된다. - 임시 purpose는 perm-type-temp-purpose 볼륨을 먼저 선호한다.
nsect_perm_free > 0이면 해당 볼륨을 먼저 스캔하고, 이후disk_Temp_max_sects로 상한이 걸린 temp 볼륨 확장으로 넘어간다. - hot 뮤텍스는 느린 작업 중에 절대 잡히지 않는다. intention 카운터가
disk_extend동안 reserve 뮤텍스를 내려놓을 수 있게 하며, 2단계는 reserve 뮤텍스를 다시 잡지 않는다. - 영구 예약은 STAB unit 단위로 WAL 로깅되고, 임시 예약은 로깅하지 않는다. 임시 비트맵은 재시작 시 일괄 초기화되므로, 임시 롤백은
disk_unreserve_ordered_sectors_without_csect를 직접 호출해 비트를 되돌린다. - 과도 스큐는 항상 보수적이다. 예약은 캐시→비트맵, 해제는 비트맵→캐시 순서이므로 캐시가 실제보다 더 많은 여유를 표시하는 경우는 없다;
disk_check가 제한된 스큐를 수리하고,error:경로는 한 번에 한해 재시도한다. - 오류 경로는 환급 전에 반드시 조정한다. 이미 반환된 섹터의 원장 항목을 먼저 감소시키고, 이후
disk_cache_free_reserved는 한 번도 뒤집히지 않은 잔여분만 환급해 이중 계상을 방지한다.
Chapter 5: 중첩 최상위 액션으로서의 볼륨 확장
섹션 제목: “Chapter 5: 중첩 최상위 액션으로서의 볼륨 확장”이 챕터가 답하는 질문: 섹터 예약 1단계에서 permanent-type / temporary-purpose 폴백마저 고갈되어 캐시가 요청을 더 이상 처리할 수 없을 때 내부에서 무슨 일이 일어나는가? 예약 중인 스레드는 예약을 완료하기 전에 데이터베이스를 성장시켜야 한다 — 기존 OS 파일을 확장하거나 새 볼륨을 생성해야 한다. 이 챕터는 disk_reserve_from_cache에서 disk_extend, disk_volume_expand, disk_add_volume으로 이어지는 확장 에스컬레이션을 추적하고, 왜 이 성장 작업이 트리거된 예약과 독립적으로 커밋되는 중첩 최상위 액션이어야 하는지를 설명한다. 4장과 고수준 동반 문서(cubrid-disk-manager.md, “섹터 예약” 절)의 캐시-vs-디스크 분리 논의를 이어받는다.
5.1 확장이 트리거되는 지점: disk_reserve_from_cache의 경쟁 윈도우
섹션 제목: “5.1 확장이 트리거되는 지점: disk_reserve_from_cache의 경쟁 윈도우”실행 중인 여유 카운트가 n_cache_reserve_remaining을 충족하지 못하면, 해당 함수는 *의도(intention)*를 기록하고 reserve 뮤텍스를 해제한 뒤 extend 뮤텍스를 획득한다. 이 순서는 필수다 — mutex_extend에는 never get expand mutex while keeping reserve mutexes라는 주석이 달려 있다. 반대 순서로 잠금을 획득하면, mutex_extend를 이미 보유한 채 동시 확장 중인 다른 스레드와 교착 상태가 발생한다.
// disk_reserve_from_cache -- src/storage/disk_manager.c extend_info->nsect_intention += context->n_cache_reserve_remaining; /* <- publish demand BEFORE releasing */ disk_cache_unlock_reserve (extend_info); disk_lock_extend (); /* <- serializes all expanders; flips reserve -> extend mutex */ disk_cache_lock_reserve (extend_info); if (extend_info->nsect_free > context->n_cache_reserve_remaining) /* <- race: someone already grew it */ { extend_info->nsect_intention -= context->n_cache_reserve_remaining; disk_reserve_from_cache_vols (extend_info->voltype, context); if (context->n_cache_reserve_remaining <= 0) { disk_cache_unlock_reserve (extend_info); disk_unlock_extend (); return NO_ERROR; } /* <- no extend */ extend_info->nsect_intention += context->n_cache_reserve_remaining; } save_remaining = context->n_cache_reserve_remaining; /* <- snapshot, to undo intention after extend */ disk_cache_unlock_reserve (extend_info); error_code = disk_extend (thread_p, extend_info, context); /* <- the slow path */두 뮤텍스 사이의 구간이 경쟁 윈도우다: 다른 스레드가 먼저 mutex_extend를 잡아 볼륨을 키우고 nsect_free를 다시 채울 수 있다. disk_lock_extend() 이후의 이중 확인이 이를 처리한다 — 볼륨이 이미 충분히 커져 있다면 이 스레드는 nsect_intention 증가분을 되돌리고 디스크 I/O 없이 확장된 캐시에서 예약한다. (disk_extend의 서두에는 assert (disk_Cache->owner_extend == thread_get_entry_index (thread_p))가 있어, extend 뮤텍스 하에서만 실행됨을 증명한다.)
불변 조건 —
nsect_intention은 미충족 수요의 핵심 누산기다. 스레드는 reserve 뮤텍스 하에서 남은 필요량을 더하고, 충족 후 동일한save_remaining스냅샷을 뺀다. 이것이 깨지면 (오류 경로에서 더하기 없이 빼기를 빠뜨리면), 이후의 모든disk_extend가nsect_intention을 성장 하한선으로 읽으므로 누출된 양만큼 영구적으로 과잉 할당된다.
5.2 disk_extend: 얼마나 키울지 결정한 후 expand-then-add 실행
섹션 제목: “5.2 disk_extend: 얼마나 키울지 결정한 후 expand-then-add 실행”disk_extend는 mutex_extend 하에서 DISK_EXTEND_INFO 카운터(1장) 스냅샷을 기반으로 성장 규모를 산정한 뒤, 두 단계로 실행한다.
// disk_extend -- src/storage/disk_manager.c target_free = MAX ((DKNSECTS) (total * 0.01), DISK_MIN_VOLUME_SECTS); /* <- 1% of size, floored */ nsect_extend = MAX (target_free - free, 0) + intention; /* <- coalesce all unmet demand */ if (nsect_extend <= 0) return NO_ERROR; /* <- branch 1: free exceeds target, no intentions */ // ... condensed ... if (total < max) /* <- phase 1: extendable volume still has room */ { to_expand = MIN (nsect_extend, max - total); /* <- never exceed this volume's ceiling */ log_sysop_start (thread_p); /* <- NESTED TOP ACTION begins */ error_code = disk_volume_expand (thread_p, extend_info->volid_extend, voltype, to_expand, &nsect_free_new); if (error_code != NO_ERROR) { ASSERT_ERROR (); log_sysop_abort (thread_p); return error_code; } /* <- header undo */ log_sysop_commit (thread_p); /* <- commit independently of outer reservation */ if (extend_info->nsect_total == extend_info->nsect_max) extend_info->volid_extend = NULL_VOLID; /* <- maxed out; never extend this volume again */ nsect_extend -= nsect_free_new; // ... condensed: bump nsect_total; under reserve mutex update vol_free + reserve ahead ... if (nsect_extend <= 0) return NO_ERROR; /* <- expansion alone covered the demand */ } // ... condensed: assert (nsect_extend > 0); volext init (nsect_max, voltype, purpose, overwrite=false) ... while (nsect_extend > 0) /* <- phase 2: add fresh volumes */ { if (check_interrupt && logtb_is_interrupted (thread_p, true, &continue_check)) { er_set (..., ER_INTERRUPTED, 0); return ER_INTERRUPTED; } /* <- branch: only if re-enabled */ volext.nsect_total = nsect_extend + DISK_SYS_NSECT_SIZE (volext.nsect_max); // ... condensed: clamp to [DISK_MIN_VOLUME_SECTS, nsect_max] then DISK_SECTS_ROUND_UP ... error_code = disk_add_volume (thread_p, &volext, &volid_new, &nsect_free_new); if (error_code != NO_ERROR) { ASSERT_ERROR (); return error_code; } /* <- disk_add_volume aborted its own sysop */ nsect_extend -= nsect_free_new; // ... condensed: bump nsect_total/nsect_max; under reserve mutex set vol_free + reserve ahead ... if (extend_info->nsect_total < extend_info->nsect_max) extend_info->volid_extend = volid_new; /* <- newest non-maxed volume becomes extendable */ } return NO_ERROR;nsect_extend는 (음수가 될 수 없는) 여유 공간 부족분에 intention을 더하므로, 하나의 확장이 이 목적(purpose)에 블록된 모든 스레드를 동시에 처리한다. 1단계는 상한 이하의 volid_extend 볼륨을 제자리에서 키우고 선예약(reserve ahead)한다. 2단계의 세 분기 — 인터럽트, disk_add_volume 오류(호출자 이미 abort), volid_extend 갱신(상한 미달 시에만) — 는 코드에 인라인으로 주석 처리되어 있다.
불변 조건 — 목적(purpose)당 정확히 하나의 볼륨만 “확장 가능”하다.
volid_extend가 1단계에서 키울 단일 볼륨을 가리키고, 볼륨이nsect_max에 도달하는 즉시NULL_VOLID로 초기화되며 가장 최근의 상한 미달 볼륨으로 재지정된다. 이것이 깨지면 최대에 도달한 볼륨이to_expand = MIN(nsect_extend, max - total)이 양수가 아닌 채로disk_volume_expand에 도달해 assert를 트리거한다.
flowchart TD
C{"nsect_extend <= 0?"} -->|예| Z1["NO_ERROR 반환"]
C -->|아니오| D{"total < max?"}
D -->|예| E["sysop_start; disk_volume_expand"]
E --> F{"오류?"} -->|예| G["sysop_abort; 오류 반환"]
F -->|아니오| H["sysop_commit; 캐시 + 선예약"]
H --> I{"nsect_extend <= 0?"} -->|예| Z2["NO_ERROR 반환"]
I -->|아니오| J["2단계 루프"]
D -->|아니오| J
J --> K{"인터럽트?"} -->|예| L["ER_INTERRUPTED 반환"]
K -->|아니오| M["volext 크기 결정; disk_add_volume"]
M --> N{"오류?"} -->|예| O["오류 반환"]
N -->|아니오| P["캐시 + 선예약; 상한 미달이면 volid_extend 설정"]
P --> Q{"nsect_extend > 0?"} -->|예| K
Q -->|아니오| Z3["NO_ERROR 반환"]
Figure 5-1. disk_extend의 분기 완전 흐름: 크기 산정, 선택적 제자리 확장, 그 다음 볼륨 추가 루프.
5.3 disk_volume_expand: 하나의 파일을 자체 sysop으로 확장
섹션 제목: “5.3 disk_volume_expand: 하나의 파일을 자체 sysop으로 확장”disk_volume_expand는 단일 볼륨을 제자리에서 키운다. 6단계 레시피의 핵심은 순서 — 이 순서가 성장을 크래시 안전하게 만든다.
// disk_volume_expand -- src/storage/disk_manager.c error_code = disk_get_volheader (thread_p, volid, PGBUF_LATCH_WRITE, &page_volheader, &volheader); if (error_code != NO_ERROR) { assert_release (false); er_set (..., ER_GENERIC_ERROR, 0); return ER_FAILED; } /* <- header fix fatal */ do_logging = (volheader->type == DB_PERMANENT_VOLTYPE); /* <- temp volumes are not logged */ log_sysop_start (thread_p); /* step 1: own sysop so header change can be undone */ volheader->nsect_total += nsect_extend; if (do_logging) log_append_undoredo_data2 (thread_p, RVDK_VOLHEAD_EXPAND, ...); /* step 2: header undo/redo */ volume_new_npages = DISK_SECTS_NPAGES (volheader->nsect_total); if (do_logging) log_append_dboutside_redo (thread_p, RVDK_EXPAND_VOLUME, ...); /* step 3: unattached redo */ pgbuf_set_dirty_and_free (thread_p, page_volheader); /* free header only after step 3 is logged */ log_sysop_commit (thread_p); /* step 4: cancel the header-undo */ logpb_force_flush_pages (thread_p); /* step 5: log MUST be on disk before the file grows */ error_code = fileio_expand_to (thread_p, volid, volume_new_npages, voltype); /* step 6: grow OS file */ if (error_code != NO_ERROR) { assert (false); return error_code; } /* <- cannot-happen: growth already durable; cache desyncs */ *nsect_extended_out = nsect_extend; return NO_ERROR;헤더 픽스 실패는 치명적이다. do_logging 분기는 임시 볼륨(복구 대상이 아님)의 경우 두 로그 레코드 모두를 건너뛴다. fileio_expand_to 실패는 cannot-happen 분기인데, log_sysop_commit이 이미 성장을 durable하게 만들었기 때문이다. RVDK_VOLHEAD_EXPAND(disk_rv_volhead_extend_undo/..._redo)는 동일한 델타만큼 nsect_total과 캐시를 조정하고, RVDK_EXPAND_VOLUME은 복구 시 fileio_expand_to를 재실행한다.
불변 조건 — 파일 성장 redo 로그는 파일이 실제로 커지기 전에 durable해야 한다. 5단계(
logpb_force_flush_pages)는 커밋된 헤더 갱신과fileio_expand_to사이에 위치한다. 이것을 건너뛰면, 그 사이에 크래시가 발생할 경우 복구된 헤더와 OS 파일의 크기가 불일치한다.
5.4 disk_add_volume: 새 OS 파일을 세 개의 레지스트리에 연결
섹션 제목: “5.4 disk_add_volume: 새 OS 파일을 세 개의 레지스트리에 연결”제자리 확장이 소진되면 disk_extend는 disk_add_volume을 호출한다 — 두 번째 중첩 최상위 액션으로, 캐시를 변경하는 단계들을 log_sysop_start/log_sysop_commit으로 감싼다.
// disk_add_volume -- src/storage/disk_manager.c if (disk_Cache->nvols_perm + disk_Cache->nvols_temp >= LOG_MAX_DBVOLID) return ER_BO_MAXNUM_VOLS_HAS_BEEN_EXCEEDED; /* <- volume-id space exhausted */ error_code = boot_get_new_volume_name_and_id (..., &volid); /* step 1: name + id from boot */ // ... condensed: raw-device symlink, partition free-space check ... if (nsect_part_max >= 0 && nsect_part_max < extinfo->nsect_max) return ER_IO_FORMAT_OUT_OF_SPACE; /* step 2 failed: not enough OS disk space */ if (!extinfo->overwrite && fileio_is_volume_exist (extinfo->name)) { /* ... condensed: disk_can_overwrite_data_volume check ... */ return ER_BO_VOLUME_EXISTS; /* <- refuse to clobber an existing file */ } log_sysop_start (thread_p); /* NESTED TOP ACTION begins */ if (extinfo->voltype == DB_PERMANENT_VOLTYPE) disk_Cache->nvols_perm++; /* step 3: cache before format */ else disk_Cache->nvols_temp++; disk_Cache->vols[volid].purpose = extinfo->purpose; error_code = disk_format (thread_p, boot_db_full_name (), volid, extinfo, nsects_free_out); /* step 4 */ if (error_code != NO_ERROR) { ASSERT_ERROR (); goto exit; } if (extinfo->voltype == DB_PERMANENT_VOLTYPE) if (logpb_add_volume (NULL, volid, extinfo->name, DB_PERMANENT_DATA_PURPOSE) == NULL_VOLID) { ASSERT_ERROR_AND_SET (error_code); goto exit; } /* step 5: register in _vinf (perm only) */ error_code = boot_dbparm_save_volume (thread_p, extinfo->voltype, volid); /* step 6: persist in boot_Db_parm */ if (error_code != NO_ERROR) { ASSERT_ERROR (); if (extinfo->voltype == DB_TEMPORARY_VOLTYPE && disk_unformat (thread_p, extinfo->name) != NO_ERROR) assert (false); /* <- rollback won't drop temp file; do it by hand */ goto exit; } *volid_out = volid;exit: if (error_code == NO_ERROR) log_sysop_commit (thread_p); else { log_sysop_abort (thread_p); if (extinfo->voltype == DB_TEMPORARY_VOLTYPE) disk_Cache->nvols_temp--; /* <- undo cache count manually */ else disk_Cache->nvols_perm--; } return error_code;세 개의 레지스트리(Figure 5-2): **boot_Db_parm**은 마지막에 갱신된다(그 전에 크래시가 나도 댕글링 참조가 없다). _vinf 레지스트리는 logpb_add_volume으로, 영구 볼륨에 한해 등록된다. **disk_Cache**의 nvols_*와 vols[volid].purpose는 먼저 증가시켜, disk_format의 페이지 픽스가 볼륨이 분류된 상태로 진행될 수 있게 한다. 모든 goto exit는 하나의 log_sysop_abort로 수렴된다. 로깅이 되돌릴 수 없는 두 가지는 abort 암에서 수동으로 처리한다 — 원시 nvols_* 카운터와, 임시 볼륨의 경우 파일(disk_unformat: 임시 생성은 저널링되지 않기 때문). 영구 볼륨의 파일은 로그된 포맷 레코드로 복구가 처리한다.
graph TD AV["disk_add_volume\n새 볼륨 파일"] --> BP["boot_Db_parm\nboot_dbparm_save_volume()"] AV --> VI["_vinf 볼륨 정보 레지스트리\nlogpb_add_volume() 영구 볼륨만"] AV --> DC["disk_Cache\nnvols_*++, vols[volid].purpose"] AV --> FMT["disk_format()\n파일 초기화, volheader + 섹터 테이블 기록"]
Figure 5-2. disk_add_volume이 새 볼륨을 연결하는 세 개의 레지스트리와 온디스크 포맷 단계.
5.5 disk_add_volume_extension: addvoldb / 부트 타임 진입점, 그리고 퇴역한 데몬
섹션 제목: “5.5 disk_add_volume_extension: addvoldb / 부트 타임 진입점, 그리고 퇴역한 데몬”disk_extend는 자동 경로이고, disk_add_volume_extension은 명시적 진입점으로 addvoldb와 데이터베이스 생성 시에 호출된다. nsect_intention을 기준으로 크기를 산정하지 않고 호출자가 npages를 직접 지정하지만, 동일한 직렬화를 존중한다 — disk_lock_extend()와 CSECT_DISK_CHECK 리더 래치를 잡아 관리자 addvol이 자동 disk_extend와 경쟁하지 않도록 한다.
// disk_add_volume_extension -- src/storage/disk_manager.c error_code = csect_enter_as_reader (thread_p, CSECT_DISK_CHECK, INF_WAIT); disk_lock_extend (); /* <- block other expansions */ // ... condensed: realpath, fill ext_info from caller args ... ext_info.nsect_total = disk_sectors_to_extend_npages (npages); ext_info.nsect_max = ext_info.nsect_total; /* <- born at its max: never auto-grown */ if (voltype == DB_TEMPORARY_VOLTYPE) { if (disk_Cache->temp_purpose_info.extend_info.nsect_total + ext_info.nsect_total > disk_Temp_max_sects) { er_set (..., ER_BO_MAXTEMP_SPACE_HAS_BEEN_EXCEEDED, ...); disk_unlock_extend (); csect_exit (thread_p, CSECT_DISK_CHECK); return ER_BO_MAXTEMP_SPACE_HAS_BEEN_EXCEEDED; } /* <- temp-space cap: release BOTH locks */ ext_info.voltype = DB_TEMPORARY_VOLTYPE; } else ext_info.voltype = DB_PERMANENT_VOLTYPE; error_code = disk_add_volume (thread_p, &ext_info, &volid_new, &nsect_free); if (error_code != NO_ERROR) { ASSERT_ERROR (); disk_unlock_extend (); csect_exit (thread_p, CSECT_DISK_CHECK); return error_code; } // ... condensed: bump per-purpose nsect_total/nsect_max, update vol_free under reserve mutex ... disk_unlock_extend (); csect_exit (thread_p, CSECT_DISK_CHECK); *volid_out = volid_new; return NO_ERROR;ext_info.nsect_max = ext_info.nsect_total은 사용자가 추가한 볼륨이 최대 크기로 태어남을 의미하며, 제자리 확장의 후보가 되지 않는다. 세 가지 분기: 임시 공간 초과 조기 반환, disk_add_volume 오류 반환(두 잠금 모두 해제), 그리고 성공 경로. 추가 후 처리(bookkeeping)는 임시 목적으로 쓰이는 permanent-type 볼륨과 진짜 temporary-type 볼륨을 구분한다 — 캐시 전반에서 사용되는 삼중 분류다.
퇴역한 데몬. disk_extend 상단의 주석에는 “안정적인 여유 공간 수준을 유지”하는 자동 확장 스레드가 여전히 언급되어 있지만, 그 데몬은 이미 제거되었다 — 바로 이 때문에 nsect_intention이 지금은 유일한 수요 병합 메커니즘이다: extend 뮤텍스를 먼저 획득한 첫 번째 스레드가 자신을 위해서뿐 아니라 대기 중에 의도를 게시한 모든 스레드를 위해 충분히 키워야 한다.
5.6 외부 트랜잭션이 아닌 중첩 최상위 액션이어야 하는 이유
섹션 제목: “5.6 외부 트랜잭션이 아닌 중첩 최상위 액션이어야 하는 이유”disk_volume_expand와 disk_add_volume 모두 durable 작업을 외부 예약의 트랜잭션에 얹지 않고 log_sysop_start/log_sysop_commit으로 감싼다. 확장을 수행한 스레드는 새 공간의 모든 공동 사용자를 대신해 행동한다: 선예약(reserve-ahead)은 트리거 예약에 새 섹터를 건네지만, 대기 중인 다른 스레드들은 extend 뮤텍스가 해제된 후 같은 볼륨에서 예약한다. 만약 성장이 외부 트랜잭션에 얹혀 있고 그 트랜잭션이 나중에 롤백된다면, 모든 공동 사용자가 강제로 롤백되어야 한다 — 여러 트랜잭션이 의존하는 볼륨이 사라진다. 독립적인 중첩 최상위 액션으로 커밋하면 트리거 트랜잭션의 운명과 무관하게 공간이 durable해진다: 예약은 여전히 abort될 수 있지만, 볼륨은 유지된다. 이는 동반 문서에서 파일 테이블 갱신에 적용되는 규율을 성장의 가장 큰 단위에 적용한 것이다.
5.7 챕터 요약 — 핵심 정리
섹션 제목: “5.7 챕터 요약 — 핵심 정리”- 확장 경로는 캐시가 두 번 실패한 후에만 진입된다.
disk_reserve_from_cache는nsect_intention을 기록하고 reserve 뮤텍스를 해제한 뒤mutex_extend를 획득하고 여유 공간을 재확인한다 — 이중 확인이 두 뮤텍스 사이에 다른 스레드가 이미 볼륨을 키운 경쟁을 흡수한다. nsect_intention은 핵심 누산기다. 자동 확장 데몬이 제거된 상황에서 이것이 동시 수요를 병합하는 유일한 메커니즘이다.disk_extend는 이것을nsect_extend에 더해 하나의 확장이 모든 대기 스레드를 처리하게 하며, 쌍을 이루는+=/-=(save_remaining스냅샷 사용)이 오류 경로 전반에서 균형을 유지한다.disk_extend는 expand-then-add 방식이다. 단일volid_extend볼륨을nsect_max까지 제자리에서 키운 다음(목적당 하나의 볼륨만 성장 가능), 잔여 수요를 처리하기 위해 새 볼륨을 추가하는 루프를 돌며 각 단계 후 호출자의 컨텍스트에 선예약한다.disk_volume_expand는 로그-우선-성장 순서를 지킨다. 헤더 undo/redo와 unattachedRVDK_EXPAND_VOLUMEredo, 강제 로그 플러시, 그 다음fileio_expand_to— 이것의 실패는 구조적으로 복구 불가능하다.disk_add_volume은 새 파일을 세 개의 레지스트리에 연결한다 —boot_Db_parm(마지막),_vinf파일(영구 볼륨만),disk_Cache(카운트 먼저) — 오류 시 로그되지 않은 캐시 카운터를 수동으로 되돌리고 고아가 된 임시 파일을 unformat한다.disk_add_volume_extension은 명시적 addvol / 부트 타임 쌍둥이다: 동일한mutex_extend직렬화, 호출자가 공급하는 크기, 그리고nsect_max == nsect_total이므로 사용자 볼륨은 자동 성장 대상이 되지 않는다.- 성장은 중첩 최상위 액션이므로 공동 사용자가 인질로 잡히지 않는다: 확장을 독립적으로 커밋하면, 이후 트리거 예약의 롤백이 다른 트랜잭션이 이미 의존하는 볼륨을 파괴할 수 없다.
Chapter 6: 파일 생성과 세 테이블 레이아웃
섹션 제목: “Chapter 6: 파일 생성과 세 테이블 레이아웃”상위 문서(cubrid-disk-manager.md)는 파일이 왜 예약된 섹터의 집합인지를 설명한다. 이 챕터는 그 기계적 후속 질문에 답한다. disk_reserve_sectors(4장)가 정렬된 VSID 배열을 반환하고 나면, file_create는 그것을 어떻게 실제로 사용 가능한 파일로 만드는가 — 헤더 페이지, VFID, 그리고 이후의 모든 할당이 의존하는 partial / full / user-page 테이블까지. VSID 배열이 이미 존재한다고 가정하고, 파일 매니저 측의 흐름을 추적한다.
file_create(file_manager.c)가 유일한 엔진이다. 나머지 file_create_heap / _temp / _ehash 계열은 불리언 두 개(is_temp, is_numerable), FILE_TYPE, FILE_TABLESPACE, 그리고 선택적 FILE_DESCRIPTORS만 결정한 뒤 file_create를 호출하는 얇은 래퍼에 불과하다.
6.1 헤더 페이지에 상주하는 네 구조체
섹션 제목: “6.1 헤더 페이지에 상주하는 네 구조체”파일의 헤더 페이지(PAGE_FTAB)는 file_header 구조체 하나로 시작하며, 그 뒤에 file_extensible_data 테이블 헤더가 최대 세 개 이어진다. file_header의 멤버 두 개는 그 자체가 구조체(FILE_TABLESPACE, FILE_DESCRIPTORS)다.
// struct file_header -- src/storage/file_manager.cstruct file_header{ INT64 time_creation; /* Time of file creation. */ VFID self; /* Self VFID */ FILE_TABLESPACE tablespace; /* The table space definition */ FILE_DESCRIPTORS descriptor; /* File descriptor. Depends on file type. */ // ... page / sector counters, flags, table offsets, temp+numerable cursors ...};file_header 필드.
| 필드 | 역할 / 존재 이유 |
|---|---|
time_creation | 파일 생성 시각(wall-clock); 재사용된 fileid를 구별한다. |
self | 파일 자신의 VFID; 복구 시 헤더를 자기 식별할 수 있게 한다. |
tablespace | 내장 FILE_TABLESPACE; 영구 파일 확장(5장), temp는 0으로 초기화. |
descriptor | 내장 FILE_DESCRIPTORS 공용체; 타입별 소유자 메타데이터. |
n_page_total | 전체 섹터에 걸친 총 페이지 수; 할당 상한선. |
n_page_user | 불출된 사용자 페이지(0으로 시작); 테이블 페이지와 구분. |
n_page_ftab | 파일 테이블이 점유한 페이지 수; 헤더(1)부터 시작. |
n_page_free | 예약됐지만 미할당 페이지; 7/8장에서 소진된다. |
n_page_mark_delete | 삭제 표시된 numerable 페이지; 표시만 하고 압축하지 않는다. |
n_sector_total | 예약 섹터 수; n_sectors와 동일. |
n_sector_partial | 여유 페이지가 있는 섹터(total-full); 할당 후보. |
n_sector_full | 테이블로 완전히 소비된 섹터; 영구 파일 전용. |
n_sector_empty | 할당된 페이지가 없는 섹터; -1로 시작(헤더 섹터). |
type | FILE_TYPE 열거형; 타입 라우팅. |
file_flags | NUMERABLE/TEMPORARY/ENCRYPTED_*; FILE_IS_* 매크로의 근거. |
volid_last_expand | 마지막으로 섹터를 공급한 볼륨; 다음 확장의 시작점. |
offset_to_partial_ftab | partial 테이블까지의 오프셋; GET_PART_FTAB의 기준. |
offset_to_full_ftab | full 테이블까지의 오프셋; 영구 파일 전용, 그 외 NULL_OFFSET. |
offset_to_user_page_ftab | user-page 테이블까지의 오프셋; numerable 전용, 그 외 NULL_OFFSET. |
vpid_sticky_first | 삭제 불가 첫 페이지; 이후에 설정(11장). |
vpid_last_temp_alloc + offset_to_last_temp_alloc | temp 할당 커서(페이지 + 오프셋); temp 단축 경로(8장). |
vpid_last_user_page_ftab | 마지막 user-page-table 페이지; numerable 추가 시 사용(10장). |
vpid_find_nth_last / first_index_find_nth_last | find_nth 캐시 위치; nth 조회 가속(10장). |
reserved0..3 | 패딩, 0으로 초기화; 하위 호환성 확보. |
graph LR FH["file_header"] -->|embeds| TS["FILE_TABLESPACE"] FH -->|embeds union| DES["FILE_DESCRIPTORS"] FH -->|offset_to_partial_ftab| PT["partial table"] FH -->|offset_to_full_ftab perm| FT["full table"] FH -->|offset_to_user_page_ftab numerable| UT["user-page table"]
Figure 6-1. file_header는 두 구조체를 내장하고, 헤더 페이지 내의 file_extensible_data 테이블 최대 세 개를 오프셋으로 가리킨다.
FILE_TABLESPACE — FILE_TABLESPACE_FOR_PERM_NPAGES / _FOR_TEMP_NPAGES 매크로가 설정하는 네 필드: initial_size(요청 바이트 MAX(1,npages)*DB_PAGESIZE, total_size의 씨앗값); expand_ratio(기하급수적 성장 비율, temp는 0); expand_min_size / expand_max_size(확장 단위 상·하한, temp는 모두 0이므로 자동 확장 없음).
FILE_DESCRIPTORS 는 64바이트(FILE_DESCRIPTORS_SIZE)로 패딩된 union이다. 팔(arm): heap(class_oid, hfid), heap_overflow, btree(class_oid, attr_id), btree_key_overflow, ehash(class_oid, attr_id), vacuum_data(vpid_first), dummy_align(64바이트 풋프린트 강제). 이 고정 크기는 디스크 포맷의 일부다 — 헤더 주석이 직접 경고한다: “if you change file descriptors size, make sure to change disk compatibility version too!”
file_extensible_data 는 file_header 뒤에 최대 세 번 반복되는 테이블 헤더다. 네 필드: vpid_next(테이블이 헤더 페이지를 넘칠 때의 연속 페이지 링크), max_size(file_extdata_init에서 고정된 바이트 단위 최대 용량), size_of_item(항목 하나의 바이트 수 — 구조체 하나, 세 가지 항목 타입), n_items(저장된 항목 수, 0으로 시작).
6.2 크기 추정: 데이터와 최악의 경우 파일 테이블 섹터
섹션 제목: “6.2 크기 추정: 데이터와 최악의 경우 파일 테이블 섹터”file_create는 요청된 바이트 크기를 섹터 수로 변환한 뒤, 파일 자체 테이블을 위한 섹터를 추가로 예약한다. 추정은 의도적으로 보수적이다 — 과다 예약은 비용이 싸지만, 부족 예약은 생성 도중 확장을 강제한다.
// file_create -- src/storage/file_manager.ctotal_size = tablespace->initial_size;if (!is_numerable) max_size_ftab = total_size / 8 / 1024; /* <- partial+full (~1 byte/8KB) */else max_size_ftab = total_size * 33 / 8 / 1024; /* <- + user-page table */total_size += max_size_ftab;n_sectors = (int) CEIL_PTVDIV (total_size, DB_SECTORSIZE);vsids_reserved = (VSID *) db_private_alloc (thread_p, n_sectors * sizeof (VSID));db_private_alloc 실패 시: er_set(ER_OUT_OF_VIRTUAL_MEMORY) 후 goto exit(아직 아무것도 예약되지 않은 상태). 성공하면, 영구 파일에 한해(do_logging = !is_temp) log_sysop_start로 시스템 오퍼레이션을 연다. temp 파일은 이 단계를 건너뛴다. 이 do_logging 분기는 이후 dirty/unfix 호출마다 반복된다.
6.3 섹터 예약과 VFID 선택
섹션 제목: “6.3 섹터 예약과 VFID 선택”// file_create -- src/storage/file_manager.cvolpurpose = is_temp ? DB_TEMPORARY_DATA_PURPOSE : DB_PERMANENT_DATA_PURPOSE;error_code = disk_reserve_sectors (thread_p, volpurpose, NULL_VOLID, n_sectors, vsids_reserved);if (error_code != NO_ERROR) { ASSERT_ERROR (); goto exit; }was_temp_reserved = is_temp; /* <- arm temp-leak cleanup */volid_last_expand = vsids_reserved[n_sectors - 1].volid; /* <- before sort! */qsort (vsids_reserved, n_sectors, sizeof (VSID), disk_compare_vsids);volid_last_expand는 정렬 이전에 캡처한다. 섹터는 예약 순서로 반환되므로, 마지막 섹터가 가장 최근에 확장된 볼륨을 가리키며 이곳에서 성장을 이어가야 한다. was_temp_reserved는 exit 레이블에서 수동 예약 해제를 트리거하기 위한 플래그다(temp 예약은 복구로 되돌릴 수 없다).
이후 헤더 페이지(곧 VFID)를 선택하는 분기가 타입에 따라 갈린다(Figure 6-2):
flowchart TD
B{"SERVER_MODE and type in\nBTREE/HEAP/HEAP_REUSE_SLOTS?"}
B -->|yes| C["scan fileids in first volume\nvacuum_is_file_dropped per fileid"]
C --> D{"non-dropped found?"}
D -->|yes| E["vfid = found_vfid"]
D -->|no| F["assert_release false -> exit"]
B -->|no| G["vfid = first page of sectid[0]"]
E --> H["vpid_fhead = vfid"]
G --> H
Figure 6-2. 헤더 페이지 / VFID 선택 분기.
기본 경로는 정렬된 첫 번째 섹터의 첫 번째 페이지를 VFID로 사용한다. vacuum 인식 분기(SERVER_MODE이고 type이 BTREE/HEAP/HEAP_REUSE_SLOTS인 경우)는, vacuum이 “dropped”로 간주하는 VFID를 재사용하면 dropped-files 목록이 오염되기 때문에 존재한다. 이 분기는 첫 번째 볼륨의 모든 섹터에서 모든 fileid를 순회하며 vacuum_is_file_dropped가 정상이라고 보고하는 첫 번째 fileid를 선택한다. 해당 함수가 오류를 반환하면 goto exit, 완전히 dropped된 첫 번째 볼륨은 assert_release(false)(불가능한 상황).
6.4 헤더 초기화
섹션 제목: “6.4 헤더 초기화”// file_create -- src/storage/file_manager.cpage_fhead = pgbuf_fix (thread_p, &vpid_fhead, NEW_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH);if (page_fhead == NULL) { ASSERT_ERROR_AND_SET (error_code); goto exit; }// ... condensed: memset(0), set ptype PAGE_FTAB, fhead = page; self/tablespace/type set ...if (des != NULL) { fhead->descriptor = *des; } /* <- temp/query-area pass NULL */if (is_numerable) { fhead->file_flags |= FILE_FLAG_NUMERABLE; }if (is_temp) { fhead->file_flags |= FILE_FLAG_TEMPORARY; }// ... condensed: time_creation, NULL cursors, zero counters ...fhead->n_page_ftab = 1; /* <- the header page is itself a table page */fhead->n_sector_empty--; /* <- start negative: header sector is not empty */헤더는 new 페이지로 fix하고(NULL이면 에러), 0으로 초기화한 뒤 PAGE_FTAB으로 타입을 지정하고 self/tablespace/type을 기록한다. 직관적이지 않은 초기값이 두 가지 있다. n_page_ftab은 1로 시작한다(헤더 자체가 테이블 페이지이므로). n_sector_empty는 -1로 시작해 헤더 섹터가 이후 partial 섹터 집계에서 빈 섹터로 계산되지 않도록 한다.
6.5 세 테이블 레이아웃 — 헤더 바이트 예산의 네 가지 변형
섹션 제목: “6.5 세 테이블 레이아웃 — 헤더 바이트 예산의 네 가지 변형”오프셋 커서 offset_ftab이 FILE_HEADER_ALIGNED_SIZE(고정 헤더 직후의 첫 번째 바이트)로 초기화된 뒤, file_create는 나머지 DB_PAGESIZE - offset_ftab 바이트를 테이블로 분할한다(Figure 6-3). 분할 방식은 (is_temp, is_numerable) 조합에 따라 네 가지로 나뉜다:
flowchart TD
N{"is_numerable?"}
N -->|yes| NT{"is_temp?"}
N -->|no| RT{"is_temp?"}
NT -->|yes| A["temp numerable\npartial 1/16, user-page 15/16"]
NT -->|no| B["perm numerable\npartial 1/32, full 1/32, user-page 15/16"]
RT -->|yes| C["temp regular\npartial = all remaining"]
RT -->|no| D["perm regular\npartial 1/2, full 1/2"]
Figure 6-3. 헤더 페이지 분할의 네 가지 변형. 모든 변형에서 partial 테이블은 반드시 할당된다; full과 user-page는 조건부.
각 테이블은 file_extdata_init(item_size, size, extdata)로 초기화된다. item_size는 partial에서 sizeof(FILE_PARTIAL_SECTOR), full에서 sizeof(VSID), user-page에서 sizeof(VPID)다. 각 fhead->offset_to_*_ftab = offset_ftab 대입 후에는 offset_ftab += file_extdata_max_size(extdata)로 커서를 전진시켜 다음 테이블이 정렬된 위치에서 시작하게 한다. 영구 numerable 변형만이 커서를 두 번 전진시키고(partial 뒤, full 뒤) 나머지 공간을 user-page 테이블에 할당한다. 나머지 변형은 커서를 최대 한 번만 전진시킨다.
불변 조건: 모든 파일은 올바르게 정렬된 partial 테이블을 갖는다. 네 분기 모두 offset_to_partial_ftab != NULL_OFFSET을 단언하며, 모든 offset_to_*_ftab 대입 뒤에는 assert((INT16) DB_ALIGN(offset, MAX_ALIGNMENT) == offset)가 따른다. partial 테이블은 범용 진입점이며(7/8장이 이곳을 먼저 순회), full / user-page 오프셋은 사용하지 않는 경우 NULL_OFFSET으로 유지된다. 정렬은 구성상 보장된다(FILE_HEADER_ALIGNED_SIZE는 사전 정렬된 값이고, file_extdata_max_size는 정렬된 크기를 반환한다). FILE_HEADER_GET_*_FTAB 매크로는 이후 모든 읽기에서 이 계약을 강제한다: GET_FULL_FTAB은 !FILE_IS_TEMPORARY(fh)를 단언하고, GET_USER_PAGE_FTAB은 FILE_IS_NUMERABLE(fh)를 단언하며, 세 매크로 모두 오프셋이 [FILE_HEADER_ALIGNED_SIZE, DB_PAGESIZE) 범위임을 확인한다. 잘못 설정된 오프셋은 조용한 오염이 아니라 큰 소리의 크래시로 이어지며, 정렬 오류는 INT64/VPID의 비정렬 접근을 의미한다.
6.6 partial 테이블 채우기와 full 섹터 분리
섹션 제목: “6.6 partial 테이블 채우기와 full 섹터 분리”file_create는 vsids_reserved를 순회하며 섹터마다 FILE_PARTIAL_SECTOR 하나를 partial 테이블에 추가한다(file_extdata_append). 헤더 내 테이블이 가득 차면(file_extdata_is_full), 현재 기록 중인 섹터에서 연속 페이지를 할당하고 vpid_next로 체인을 잇고 n_page_ftab을 증가시킨 뒤 그곳에서 이어간다. 연속 페이지의 비트는 해당 섹터의 비트맵에 설정되므로 사용자에게 다시 제공되지 않는다.
순회가 끝나고 나서 partsect_ftab이 가리키는 마지막 섹터 자체가 가득 찬 경우(마지막 테이블 페이지를 담고 있었다면), partsect_ftab++; fhead->n_sector_full++로 처리한다. 이후 영구 파일에 한해 파일 테이블로 완전히 소비된 섹터들을 partial 테이블에서 full 테이블로 이동시킨다:
// file_create (full-sector migration) -- src/storage/file_manager.cif (!is_temp && fhead->n_sector_full > 0) { // ... condensed: GET_PART_FTAB + GET_FULL_FTAB into extdata_part_ftab / extdata_full_ftab ... for (i = 0; i < fhead->n_sector_full; i++) { partsect_iter = (FILE_PARTIAL_SECTOR *) file_extdata_at (extdata_part_ftab, i); /* ... condensed: drops the file_extdata_is_full / assert_release(false) guard ... */ file_extdata_append (extdata_full_ftab, &partsect_iter->vsid); /* <- VSID only */ } file_extdata_remove_at (extdata_part_ftab, 0, fhead->n_sector_full); }temp 파일은 이 과정을 전부 건너뛴다(full 테이블이 없음). 대신 temp 커서를 초기화한다(vpid_last_temp_alloc = vpid_fhead, offset_to_last_temp_alloc = n_sector_full). numerable 파일(temp 또는 perm)은 user-page-table 헤드를 초기화한다(vpid_last_user_page_ftab와 vpid_find_nth_last 모두 vpid_fhead로 설정). 마지막으로 카운터를 정산한다 — n_sector_total = n_sectors, n_sector_partial = total - full, n_sector_empty += n_sector_partial, n_page_total = n_sector_total * DISK_SECTOR_NPAGES, n_page_free = n_page_total - n_page_ftab — 그리고 file_header_sanity_check가 헤더의 내부 일관성을 단언한다.
6.7 커밋, 트래커 등록, 에러/exit 경로
섹션 제목: “6.7 커밋, 트래커 등록, 에러/exit 경로”// file_create (finish) -- src/storage/file_manager.cif (do_logging) { pgbuf_log_new_page (thread_p, page_fhead, DB_PAGESIZE, PAGE_FTAB); pgbuf_unfix_and_init (thread_p, page_fhead); }else { pgbuf_set_dirty_and_free (thread_p, page_fhead); } /* <- temp: no redo log */if (!is_temp && file_type != FILE_TRACKER) { error_code = file_tracker_register (thread_p, vfid, file_type, NULL); if (error_code != NO_ERROR) { ASSERT_ERROR (); goto exit; } }if (is_temp) { ATOMIC_INC_32 (&file_Tempcache.spacedb_temp.nfile, 1); /* ...stats... */ }영구 파일은 redo를 위해 헤더를 로깅하고 파일 트래커에 등록한다 — 단, FILE_TRACKER 자신은 제외한다(등록하면 순환이 되므로; 트래커 등록은 11장). temp 파일은 인메모리 spacedb_temp 카운터만 증가시킨다. 공유 exit 레이블은 모든 분기의 실패를 처리한다: (1) 아직 고정 중인 page_ftab / page_fhead를 unfix; (2) is_sysop_started이면, 오류 시 log_sysop_abort(예약+레이아웃을 롤백), 성공 시 log_sysop_end_logical_undo(RVFL_DESTROY, vfid)를 등록해 이후 트랜잭션 abort 시 파일 전체를 철거할 수 있게 함; (3) 오류 시 VFID_SET_NULL(vfid)으로 호출자가 반쪽짜리 ID를 볼 수 없게 하고, was_temp_reserved이면 temp 섹터를 수동으로 예약 해제(복구가 대신 처리해 주지 않으므로) — 이때 logtb_set_check_interrupt(false) 하에서 실행; (4) 항상 db_private_free(vsids_reserved).
6.8 래퍼와 각각의 설정값
섹션 제목: “6.8 래퍼와 각각의 설정값”모든 공개 생성 함수는 고정된 (is_temp, is_numerable, file_type) 조합으로 file_create를 호출한다:
| 래퍼 | file_type | is_temp | is_numerable | 디스크립터 | 테이블스페이스 |
|---|---|---|---|---|---|
file_create_heap | FILE_HEAP / FILE_HEAP_REUSE_SLOTS | no | no | heap (class_oid) | perm, npages=1 |
file_create_temp | FILE_TEMP | yes | no | NULL | temp |
file_create_temp_numerable | FILE_TEMP | yes | yes | NULL | temp |
file_create_query_area | FILE_QUERY_AREA | yes | no | NULL | temp, npages=1 |
file_create_ehash | FILE_EXTENDIBLE_HASH | 호출자의 is_tmp | yes | ehash | temp 크기 |
file_create_ehash_dir | FILE_EXTENDIBLE_HASH_DIRECTORY | 호출자의 is_tmp | yes | ehash | temp 크기 |
file_create_heap은 디스크립터를 구성하고(memset 후 des.heap.class_oid = *class_oid) file_create_with_npages로 라우팅한다. 세 가지 temp 래퍼는 모두 file_create_temp_internal을 거치는데, 이것은 단순 pass-through가 아니다:
// file_create_temp_internal -- src/storage/file_manager.cerror_code = file_tempcache_get (thread_p, ftype, is_numerable, &tempcache_entry);if (VFID_ISNULL (&tempcache_entry->vfid)) /* <- cache miss: create fresh */ { FILE_TABLESPACE_FOR_TEMP_NPAGES (&tablespace, npages); file_tempcache_lock_tran_entry (tran_entry); /* <- rmutex_topop guard */ error_code = file_create (thread_p, ftype, &tablespace, NULL, true, is_numerable, vfid_out); file_tempcache_unlock_tran_entry (tran_entry); // ... condensed: on error file_tempcache_retire_entry + return; else cache the vfid ... }else { *vfid_out = tempcache_entry->vfid; } /* <- cache hit: reuse, no file_create */file_tempcache_push_tran_file (thread_p, tempcache_entry);즉, temp 파일 생성은 file_create를 완전히 건너뛰고 캐시된 파일을 반환할 수 있다. 실제로 file_create를 호출하는 경우, file_create 내의 log_sysop_start가 rmutex_topop을 사용하기 때문에 한 트랜잭션의 병렬 워커 간에 안전하지 않아 트랜잭션별 잠금으로 호출을 감싼다(tempcache는 11장에서 다룬다). ehash 래퍼는 얇다: temp 크기의 테이블스페이스, FILE_EHASH_DES를 디스크립터로, is_numerable = true 고정(nth-page 조회용), is_temp는 호출자의 is_tmp를 그대로 전달한다.
6.9 챕터 요약 — 핵심 정리
섹션 제목: “6.9 챕터 요약 — 핵심 정리”file_create가 단일 엔진이다. 래퍼는(file_type, is_temp, is_numerable, descriptor, tablespace)만 선택한다. heap/ehash는 디스크립터를 제공하고, temp/query-area는NULL을 전달한다.- 예약 섹터 수는 파일 자체 테이블을 수용하기 위해 과다 추정된다(일반 파일
total/8/1024추가 바이트, numerabletotal*33/8/1024). 이로써 생성 도중 확장을 피한다. VFID는 첫 번째 예약 섹터의 첫 번째 페이지다 — 단,SERVER_MODE하의 heap/btree는 vacuum이 여전히 dropped로 보는 fileid를 지나쳐 스캔한다. - 헤더 페이지는
file_header하나와file_extensible_data테이블 최대 세 개를 담으며, 변형에 따라 분할된다: perm regular 1/2+1/2, perm numerable 1/32+1/32+15/16, temp regular partial 전용, temp numerable 1/16+15/16. 두 불변 조건이 유지된다: 모든 파일은 partial 테이블을 가지며, 모든 오프셋은MAX_ALIGNMENT-정렬된다(FILE_HEADER_GET_*_FTAB이 강제). - 영구 파일은 파일 테이블로 완전히 소비된 섹터를 full 테이블로 이동시킨다. temp 파일은 대신 커서(
vpid_last_temp_alloc) 하나를 유지한다. do_logging = !is_temp가 내구성을 결정한다: 영구 파일은 헤더를 로깅하고RVFL_DESTROY를 논리적 undo로 등록하는 sysop을 실행하고, temp 파일은 set-dirty-and-free로 처리하며 에러 시 수동으로 예약을 해제하고 tempcache에서file_create없이 바로 제공될 수 있다.
Chapter 7: 영구 파일 페이지 할당
섹션 제목: “Chapter 7: 영구 파일 페이지 할당”이 챕터는 다음 질문에 답한다: 이미 섹터를 확보한 영구 파일에서 file_alloc이 다음 사용자 페이지를 어떻게 넘겨주는가, 그리고 O(1) 할당을 가능하게 하는 Partial 테이블 헤드 불변식은 어떻게 유지되는가? file_alloc, 엔진 함수 file_perm_alloc, 그리고 그 헬퍼들을 단계별로 추적한다. 섹터, Partial vs. Full 테이블, FILE_EXTENSIBLE_DATA에 관한 이론은 동반 문서(cubrid-disk-manager.md, “File layout” / “Three-table model”)를 참조한다. 임시 할당(Ch.8)과 numerable 테이블(Ch.10)은 이 챕터의 범위 밖이다.
7.1 데이터 단위: file_partial_sector
섹션 제목: “7.1 데이터 단위: file_partial_sector”partial 테이블의 모든 항목은 file_partial_sector(typedef FILE_PARTIAL_SECTOR)이며, file_perm_alloc은 빠른 경로(fast-path) 할당 때마다 이 구조체의 bitmap을 수정한다.
// file_partial_sector -- src/storage/file_manager.hstruct file_partial_sector{ VSID vsid; /* Important - VSID must be first member ... * Sometimes, the FILE_PARTIAL_SECTOR pointers * in file table are reinterpreted as VSID. */ FILE_ALLOC_BITMAP page_bitmap;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
vsid | 섹터 식별자 { volid, sectid }; 이 항목이 커버하는 64페이지 범위의 on-disk 주소. | ”이 섹터는 이 파일이 예약했다”는 표시. Full 테이블 항목이기도 하다(불변식 참조). |
page_bitmap | 64비트 FILE_ALLOC_BITMAP(UINT64); 비트 k가 세트 ⇒ 페이지 k 할당됨. DISK_SECTOR_NPAGES == 64, 페이지당 비트 하나. | 스캔 대신 비트 하나만 뒤집으면 된다. 0x0…0 = FILE_EMPTY_PAGE_BITMAP, 0xF…F = FILE_FULL_PAGE_BITMAP. |
불변식 — VSID는 반드시 첫 번째 멤버여야 한다. Full 테이블 및 확장 코드는 FILE_PARTIAL_SECTOR *를 VSID *로 재해석한다(full 테이블 항목은 그냥 VSID이다). vsid 앞에 다른 필드가 오면 그 캐스트는 쓰레기 값을 읽게 된다. 구조체 레이아웃 자체가 계약이다.
classDiagram
class FILE_PARTIAL_SECTOR {
VSID vsid
FILE_ALLOC_BITMAP page_bitmap
}
FILE_PARTIAL_SECTOR --> "full table reuses the prefix" VSID : vsid is first
Figure 7-1. Full 테이블은 순수 VSID만 저장한다 — partial 항목의 맨 앞 필드와 정확히 같다. 이 접두사 호환성은 이 챕터 전반에 걸쳐 반복된다.
7.2 file_alloc: 디스패처
섹션 제목: “7.2 file_alloc: 디스패처”file_alloc은 헤더를 고정(fix)하고, FILE_IS_TEMPORARY 여부로 분기하며, 할당을 수행한 뒤 선택적으로 페이지를 등록·초기화한다. 영구 경로는 논리적 언두(logical-undo) 시스템 오퍼레이션 안에서 수행된다.
// file_alloc -- src/storage/file_manager.cpage_fhead = pgbuf_fix (thread_p, &vpid_fhead, OLD_PAGE, PGBUF_LATCH_WRITE, ...);// ... condensed ...if (FILE_IS_TEMPORARY (fhead)) error_code = file_temp_alloc (thread_p, page_fhead, FILE_ALLOC_USER_PAGE, vpid_out); /* <- Ch.8 */else { log_sysop_start_atomic (thread_p); /* <- nested top action, atomic so undo is one unit */ is_sysop_started = true; error_code = file_perm_alloc (thread_p, page_fhead, FILE_ALLOC_USER_PAGE, vpid_out); /* <- 7.3 */ VFID_COPY ((VFID *) undo_log_data, vfid); VPID_COPY ((VPID *) (undo_log_data + sizeof (VFID)), vpid_out); /* <- undo payload {vfid,vpid} */ }나머지 종료 경로(goto exit): (1) pgbuf_fix 실패 → 즉시 반환, 아무것도 고정되지 않음. (2) numerable → file_numerable_add_page(tail call, Ch.10). (3) f_init 제공됨 → 새 페이지를 NEW_PAGE로 고정, 초기화, TDE 설정, page_out 또는 unfix로 반환; 실패 시 unfix. (4) f_init 없음 → 임시 파일임이 assert됨; page_out 요청 시 raw 페이지 반환. (5) exit → 에러 시 sysop 중단(abort), 성공 시 log_sysop_end_logical_undo (RVFL_ALLOC, …)으로 커밋-앤드-언두; 이후 정합성 검사 및 unfix. 구조적 변경은 file_perm_alloc 내부에서 즉시 redo 로깅되며, sysop의 단일 논리적 언두(RVFL_ALLOC)는 “deallocate {vfid,vpid}” 레코드다 — Ch.5의 nested-top-action 규율이 그대로 적용된다.
7.3 file_perm_alloc: 엔진
섹션 제목: “7.3 file_perm_alloc: 엔진”네 단계로 구성된다: 여유 페이지 확보 → 헤더 섹션에 partial 섹터 존재 보장 → 헤드 섹터 비트 뒤집기 → 가득 찬 경우 full 테이블로 이동.
flowchart TD
A["file_perm_alloc(alloc_type)"] --> B{"n_page_free == 0 ?"}
B -- 예 --> C["file_perm_expand\n새 섹터 예약"]
B -- 아니오 --> D
C --> D{"헤더 partial 섹션 비어 있음 ?"}
D -- 예 --> E["file_table_move_partial_sectors_to_header"]
E --> F{"vpid_alloc_out 설정됨 ?"}
F -- 예 --> Z["goto exit\n페이지 이미 선택됨"]
F -- 아니오 --> G
D -- 아니오 --> G["partsect = partial 섹션의 헤드"]
G --> H["file_partsect_alloc:\n첫 번째 0-비트 세트, vpid 반환"]
H --> I{"alloc_type ==\nTABLE_PAGE_FULL_SECTOR ?"}
I -- 예 --> J["file_table_append_full_sector_page"]
I -- 아니오 --> K
J --> K["file_header_alloc:\n카운터 갱신 + WAL"]
K --> L{"섹터 가득 참 ?"}
L -- 아니오 --> Z2["정상 종료"]
L -- 예 --> M["헤드를 partial 테이블에서 제거"]
M --> N["file_table_add_full_sector(vsid)"]
N --> Z2
Figure 7-2. file_perm_alloc 제어 흐름 — 모든 분기와 goto 포함.
Phase 1 — 여유 페이지 보장
섹션 제목: “Phase 1 — 여유 페이지 보장”// file_perm_alloc -- src/storage/file_manager.cif (fhead->n_page_free == 0) { error_code = file_perm_expand (thread_p, page_fhead); /* <- 7.4 */ if (error_code != NO_ERROR) { ASSERT_ERROR (); goto exit; } }assert (fhead->n_page_free > 0 && fhead->n_sector_partial > 0);불변식 — n_page_free > 0인 동안 헤더는 Partial 항목을 가진다. 여유 페이지는 오직 partial 섹터 안에만 존재하므로(full 섹터에는 없고, empty는 partial의 부분집합), 여유 페이지가 있다는 것은 partial 섹터가 있다는 뜻이다 — 두 assert가 이를 확인한다. Phase 2는 그 섹터가 헤더 섹션에 있음을 추가로 보장한다.
Phase 2 — 헤드 섹션 비어 있지 않음 보장
섹션 제목: “Phase 2 — 헤드 섹션 비어 있지 않음 보장”FILE_HEADER_GET_PART_FTAB (fhead, extdata_part_ftab);if (file_extdata_is_empty (extdata_part_ftab)) { error_code = file_table_move_partial_sectors_to_header (thread_p, page_fhead, alloc_type, vpid_alloc_out); /* <- 7.5 */ if (error_code != NO_ERROR) { ASSERT_ERROR (); goto exit; } if (!VPID_ISNULL (vpid_alloc_out)) { goto exit; /* <- 비워진 overflow 페이지를 할당 결과로 재사용; 완료 */ } }assert (!file_extdata_is_empty (extdata_part_ftab));이동이 헤더를 다시 채운 경우(vpid_alloc_out이 NULL, fall through) 또는 overflow 페이지를 비우고 그 페이지 자체를 결과로 재사용한 경우(vpid_alloc_out 설정 → goto exit, bitmap 수정 없음 — CBRD-21242 경로, 7.5).
Phase 3 — 헤드 비트 뒤집기
섹션 제목: “Phase 3 — 헤드 비트 뒤집기”partsect = (FILE_PARTIAL_SECTOR *) file_extdata_start (extdata_part_ftab); /* <- head item, position 0 */assert (!file_partsect_is_full (partsect));was_empty = file_partsect_is_empty (partsect);if (!file_partsect_alloc (partsect, vpid_alloc_out, &offset_to_alloc_bit)) /* <- 7.6 */ { assert_release (false); /* head sector must have a free page (invariant) */ error_code = ER_FAILED; goto exit; }log_append_undoredo_data2 (thread_p, RVFL_PARTSECT_ALLOC, NULL, page_fhead, (PGLENGTH) ((char *) partsect - page_fhead), /* <- offset of partsect in page */ ..., &offset_to_alloc_bit, &offset_to_alloc_bit); /* <- undo == redo == bit offset */불변식 — 헤드 섹터에는 항상 여유 페이지가 있다. 할당은 항상 position 0(file_extdata_start)을 읽으며, Phase 1–2가 거기에 가득 차지 않은 partial 섹터가 있음을 보장하므로, 두 assert는 헤드가 full인 상황을 논리 오류로 취급한다. RVFL_PARTSECT_ALLOC은 헤더 페이지 내 partsect의 바이트 오프셋 위치에 비트 오프셋만 로깅한다(undo == redo).
FILE_ALLOC_TABLE_PAGE vs FILE_ALLOC_USER_PAGE
섹션 제목: “FILE_ALLOC_TABLE_PAGE vs FILE_ALLOC_USER_PAGE”비트를 뒤집은 직후, if (alloc_type == FILE_ALLOC_TABLE_PAGE_FULL_SECTOR)이면 file_table_append_full_sector_page (…)(7.7)을 호출한다. alloc_type은 페이지의 용도를 나타내며, file_header_alloc(7.8)이 그에 따라 n_page_user 또는 n_page_ftab을 증가시킨다. enum file_alloc_type은 세 값을 갖는다 — FILE_ALLOC_USER_PAGE, FILE_ALLOC_TABLE_PAGE, FILE_ALLOC_TABLE_PAGE_FULL_SECTOR. 마지막 값은 full 테이블이 새 페이지를 필요로 할 때 file_table_add_full_sector가 요청한다. 해당 페이지는 현재 섹터가 이동하기 전에 연결되어야 한다 — 그렇지 않으면 이동 시 공간이 없어 재귀가 발생한다. 세 번째 값이 존재하는 이유가 바로 이것이다.
Phase 4 — 가득 찬 경우 full 테이블로 이동
섹션 제목: “Phase 4 — 가득 찬 경우 full 테이블로 이동”is_full = file_partsect_is_full (partsect);file_header_alloc (fhead, alloc_type, was_empty, is_full); /* <- 7.8: counters + WAL */file_log_fhead_alloc (thread_p, page_fhead, alloc_type, was_empty, is_full);if (is_full) { VSID vsid_full = partsect->vsid; /* <- save before removal */ file_log_extdata_remove (thread_p, extdata_part_ftab, page_fhead, 0, 1); file_extdata_remove_at (extdata_part_ftab, 0, 1); /* <- drop head item */ error_code = file_table_add_full_sector (thread_p, page_fhead, &vsid_full); /* <- 7.7 */ if (error_code != NO_ERROR) { ASSERT_ERROR (); goto exit; } }카운터를 먼저 갱신한 뒤(중첩 할당 이전에 정확한 상태 유지), 가득 찬 헤드 섹터를 position 0에서 제거하고 full 테이블에 추가한다 — Partial 헤드 불변식을 복원한다. 롤백은 file_alloc을 감싸는 sysop이 책임진다.
7.4 file_perm_expand: partial 테이블 재충전
섹션 제목: “7.4 file_perm_expand: partial 테이블 재충전”n_page_free == 0일 때 호출된다. 새 섹터 배치를 예약하고, 빈 partial 항목으로 헤더에 추가한다.
// file_perm_expand -- src/storage/file_manager.cexpand_size_in_sectors = (int) ((float) fhead->n_sector_total * fhead->tablespace.expand_ratio);expand_size_in_sectors = MAX (expand_size_in_sectors, expand_min_size_in_sectors);expand_size_in_sectors = MIN (expand_size_in_sectors, expand_max_size_in_sectors); /* <- clamp to header capacity */// ... condensed: db_private_alloc vsids_reserved buffer ...log_sysop_start (thread_p); /* <- separate committed sysop: expansion is permanent */error_code = disk_reserve_sectors (thread_p, DB_PERMANENT_DATA_PURPOSE, fhead->volid_last_expand, expand_size_in_sectors, vsids_reserved); /* fail -> goto exit, abort */qsort (vsids_reserved, expand_size_in_sectors, sizeof (VSID), disk_compare_vsids);partsect.page_bitmap = FILE_EMPTY_PAGE_BITMAP;for (... each reserved vsid ...) { partsect.vsid = *vsid_iter; file_extdata_append (extdata_part_ftab, &partsect); } /* <- empty entries into header */fhead->n_sector_total += expand_size_in_sectors;fhead->n_sector_empty = fhead->n_sector_partial = expand_size_in_sectors; /* asserted 0 before */fhead->n_page_free = expand_size_in_sectors * DISK_SECTOR_NPAGES; /* asserted 0 before */fhead->n_page_total += fhead->n_page_free;분기 설명: (1) 크기는 헤더 file_extdata_remaining_capacity로 제한 — 확장은 새 테이블 페이지를 필요로 하지 않는다. (2) VSID 버퍼 db_private_alloc 실패 → ER_OUT_OF_VIRTUAL_MEMORY, sysop 없이 반환. (3) disk_reserve_sectors 실패 → goto exit, sysop 중단. (4) 성공 시 카운터 설정(각각 0임을 assert하여 확장이 완전 소진 시에만 실행됨을 확인). 내부 sysop은 성공 시 커밋, 에러 시 중단(자체 nested top action, Ch.5); RVFL_EXPAND는 예약된 VSID 배열을 redo로 로깅하고 undo는 비어 있다.
7.5 file_table_move_partial_sectors_to_header
섹션 제목: “7.5 file_table_move_partial_sectors_to_header”헤더 섹션은 비어 있지만 overflow 페이지에 아직 partial 섹터가 있는 상황: 첫 번째 overflow 페이지에서 항목을 헤더로 끌어올린다.
// file_table_move_partial_sectors_to_header -- src/storage/file_manager.cpage_part_ftab_first = pgbuf_fix (thread_p, &extdata_part_ftab_head->vpid_next, OLD_PAGE, ...); /* fail -> exit */n_items_to_move = file_extdata_item_count (extdata_part_ftab_first);if (n_items_to_move == 0) { assert_release (false); error_code = ER_FAILED; goto exit; }// ... condensed: re-check header is empty ...n_items_to_move = MIN (n_items_to_move, file_extdata_remaining_capacity (extdata_part_ftab_head)); /* <- cap to header room */file_extdata_append_array (extdata_part_ftab_head, file_extdata_start (extdata_part_ftab_first), n_items_to_move);file_log_extdata_add (thread_p, extdata_part_ftab_head, page_fhead, 0, n_items_to_move, ...);if (n_items_to_move < file_extdata_item_count (extdata_part_ftab_first)) { /* partial move: remove copied prefix; first page survives */ file_log_extdata_remove (thread_p, extdata_part_ftab_first, page_part_ftab_first, 0, n_items_to_move); file_extdata_remove_at (extdata_part_ftab_first, 0, n_items_to_move); }else { /* whole page drained: unlink and REUSE it (CBRD-21242) */ VPID save_next = extdata_part_ftab_head->vpid_next; /* <- drained page id, saved before relink */ // ... relink: head->vpid_next = first->vpid_next (skip drained page) ... *vpid_alloc_out = save_next; pgbuf_dealloc_page (thread_p, page_part_ftab_first); if (alloc_type == FILE_ALLOC_TABLE_PAGE_FULL_SECTOR) { file_table_append_full_sector_page (...); } else if (alloc_type == FILE_ALLOC_USER_PAGE) { fhead->n_page_ftab--; fhead->n_page_user++; log_append_undoredo_data2 (thread_p, RVFL_FHEAD_CONVERT_FTAB_TO_USER, ...); } }분기 분기 이전의 에러/assert: 헤더 vpid_next가 NULL → assert(false), ER_FAILED; 첫 번째 overflow 페이지 pgbuf_fix 실패 → goto exit; n_items_to_move == 0 → assert_release(false); 헤더가 실제로 비어 있지 않음 → 조용히 goto exit. 전체 소진(full-drain) 경로는 재연결 이전에 vpid_next를 저장하고, 비워진 페이지를 결과로 재사용하며, 테이블 페이지를 사용자 페이지로 전환한다(RVFL_FHEAD_CONVERT_FTAB_TO_USER) — 해제-후-재할당 루프를 피하기 위한 것이다. Phase 2가 !VPID_ISNULL (vpid_alloc_out)에서 단락(short-circuit)하는 이유가 여기에 있다.
7.6 file_partsect_alloc과 비트 헬퍼들
섹션 제목: “7.6 file_partsect_alloc과 비트 헬퍼들”할당은 헤드 섹터 bitmap에서 비트 하나를 뒤집는 것이다.
// file_partsect_alloc -- src/storage/file_manager.cint offset_to_zero = bit64_count_trailing_ones (partsect->page_bitmap); /* <- index of first 0-bit */if (offset_to_zero >= FILE_ALLOC_BITMAP_NBITS) /* 64: bitmap all ones */ { assert (file_partsect_is_full (partsect)); return false; } /* <- caller treats as logic error */file_partsect_set_bit (partsect, offset_to_zero);if (offset_out) *offset_out = offset_to_zero;if (vpid_out) /* <- reconstruct VPID from vsid + offset */ { vpid_out->volid = partsect->vsid.volid; vpid_out->pageid = SECTOR_FIRST_PAGEID (partsect->vsid.sectid) + offset_to_zero; }return true;bit64_count_trailing_ones는 가장 낮은 미설정 비트를 찾는다(페이지는 섹터 시작부터 밀집 배치된다). file_partsect_set_bit은 해당 비트가 비어 있음을 assert하고 bit64_set으로 OR 연산한다. 역함수 file_partsect_pageid_to_offset은 SECTOR_FIRST_PAGEID (sectid)를 빼서 오프셋을 구한다 — 해제(deallocate) 경로에서 사용된다(Ch.9). bitmap 자체가 페이지 목록이다.
7.7 Full 섹터 추가: file_table_add_full_sector와 file_table_append_full_sector_page
섹션 제목: “7.7 Full 섹터 추가: file_table_add_full_sector와 file_table_append_full_sector_page”헤드 섹터가 가득 차면 그 VSID가 full 테이블로 이동한다.
// file_table_add_full_sector -- src/storage/file_manager.cFILE_HEADER_GET_FULL_FTAB (fhead, extdata_full_ftab);error_code = file_extdata_find_not_full (thread_p, &extdata_full_ftab, &page_ftab, &found);if (!found) { /* full table is full: allocate a NEW table page for it */ error_code = file_perm_alloc (thread_p, page_fhead, FILE_ALLOC_TABLE_PAGE_FULL_SECTOR, &vpid_ftab_new); /* <- recursion */ page_ftab = pgbuf_fix (thread_p, &vpid_ftab_new, OLD_PAGE, ...); /* already initialized */ extdata_full_ftab = (FILE_EXTENSIBLE_DATA *) page_ftab; }page_extdata = page_ftab != NULL ? page_ftab : page_fhead; /* <- which page the add is logged against */file_extdata_find_ordered (extdata_full_ftab, vsid, disk_compare_vsids, &found, &pos);if (found) { assert_release (false); error_code = ER_FAILED; goto exit; } /* duplicate VSID */file_extdata_insert_at (extdata_full_ftab, pos, 1, vsid); /* + file_log_extdata_add(..., page_extdata, ...) */분기 설명: (1) 기존 컴포넌트에 여유 공간 있음 → 정렬된 위치에 삽입. (2) 공간 없음 → FILE_ALLOC_TABLE_PAGE_FULL_SECTOR로 file_perm_alloc을 재귀 호출; 이 타입은 새 페이지를 이동 전에 full 테이블에 추가하므로 유계(bounded) 재귀다. (3) 중복 VSID → ER_FAILED. 항목은 이진 탐색을 위해 disk_compare_vsids 기준으로 정렬 유지된다.
file_table_append_full_sector_page는 새 페이지를 초기화하고 체인의 헤드에 연결한다:
// file_table_append_full_sector_page -- src/storage/file_manager.cpage_ftab = pgbuf_fix (thread_p, vpid_new, NEW_PAGE, ...); /* fail -> ASSERT_ERROR_AND_SET, return */pgbuf_set_page_ptype (thread_p, page_ftab, PAGE_FTAB);file_extdata_init (sizeof (VSID), DB_PAGESIZE, extdata_new_ftab); /* <- full entries are bare VSIDs */VPID_COPY (&extdata_new_ftab->vpid_next, &extdata_full_ftab->vpid_next); /* new page points at old head */pgbuf_log_new_page (thread_p, page_ftab, file_extdata_size (extdata_new_ftab), PAGE_FTAB);pgbuf_unfix_and_init (thread_p, page_ftab); /* <- new page no longer fixed */file_log_extdata_set_next (thread_p, extdata_full_ftab, page_fhead, vpid_new); /* old head -> new page */VPID_COPY (&extdata_full_ftab->vpid_next, vpid_new);file_extdata_init이 sizeof (FILE_PARTIAL_SECTOR)가 아닌 sizeof (VSID)를 사용하는 점에 주목하라 — 7.1의 접두사 호환성이 여기서 실제로 작동한다.
7.8 file_header_alloc의 카운터 갱신
섹션 제목: “7.8 file_header_alloc의 카운터 갱신”file_header_alloc은 여덟 개 헤더 카운터(n_page_total/user/ftab/free, n_sector_total/partial/full/empty)를 유지하는 유일한 지점이다.
// file_header_alloc -- src/storage/file_manager.cfhead->n_page_free--;if (alloc_type == FILE_ALLOC_USER_PAGE) fhead->n_page_user++;else fhead->n_page_ftab++; /* table page of either flavor */if (was_empty) fhead->n_sector_empty--; /* sector now holds a page: no longer empty */if (is_full) { fhead->n_sector_partial--; fhead->n_sector_full++; } /* migrated to full */맨 앞의 assert (!was_empty || !is_full)는 단 한 번의 할당으로 섹터가 empty→full이 될 수 없음을 강제한다(가능한 전이는 empty→partial 또는 partial→full이다). file_log_fhead_alloc은 3-bool redo {is_ftab_page, was_empty, is_full}을 기록하며, 이는 file_rv_fhead_alloc이 재생한다. n_page_total/n_sector_total은 오직 확장 시에만 변경된다(7.4).
7.9 챕터 요약 — 핵심 정리
섹션 제목: “7.9 챕터 요약 — 핵심 정리”file_alloc은FILE_IS_TEMPORARY로 분기한다: 임시 파일 →file_temp_alloc(Ch.8), sysop 없음; 영구 파일 →log_sysop_end_logical_undo (RVFL_ALLOC, {vfid,vpid})로 닫히는 atomic nested-top-action sysop.file_partial_sector는{vsid, page_bitmap}이며,vsid는 반드시 첫 번째 멤버여야 한다 — full 테이블 코드가 포인터를 순수VSID로 재해석하며, 64비트 bitmap은 64페이지 섹터의 페이지당 비트 하나다.- Phase 1–2(확장, 헤더로 이동)가 두 핵심 불변식을 복원한다: 비트 뒤집기는
file_partsect_alloc에서bit64_count_trailing_ones로 위치를 찾고,SECTOR_FIRST_PAGEID + offset으로 VPID를 재구성한다. - 가득 찬 섹터는 full 테이블로 이동한다:
file_header_alloc이 카운터를 먼저 갱신하고, 헤드 항목이 정렬된 full 테이블로 이동하며, full 테이블은FILE_ALLOC_TABLE_PAGE_FULL_SECTOR를 사용하는 유계 재귀로 확장된다.FILE_ALLOC_USER_PAGEvs_TABLE_PAGE[_FULL_SECTOR]에 따라n_page_uservsn_page_ftab이 결정되며,n_*_total을 늘리는 것은file_perm_expand뿐이다(RVFL_EXPAND, 자체 커밋 sysop). - Numerable 등록은 tail call이다(
file_numerable_add_page, Ch.10); full-drain 분기는 비워진 overflow 페이지를 재사용하고(CBRD-21242),RVFL_FHEAD_CONVERT_FTAB_TO_USER로 테이블-to-사용자 전환을 로깅한다.
Chapter 8: 임시 파일 페이지 할당
섹션 제목: “Chapter 8: 임시 파일 페이지 할당”임시 파일은 정렬, 해시 조인, 쿼리 결과 materialization을 지원한다. 임시 파일은 단일 트랜잭션 안에서 생성되고 소멸하거나(또는 재사용을 위해 tempcache에 보관된다 — Ch.11), 그렇기 때문에 디스크 관리자는 영구 파일에 필요한 대부분의 메커니즘을 배제한다. 이 챕터가 답하는 질문은 다음과 같다: 임시 파일이 Partial→Full 마이그레이션을 건너뛰는 이유는 무엇이며, 단일 헤더 커서만으로 어떻게 O(1) 할당과 무로깅(no-logging)을 달성하는가? 설계 의도에 대한 상위 수준의 근거는 companion 문서인 cubrid-disk-manager.md에 있다. 이 챕터는 코드를 추적하면서 file_perm_alloc(Ch.7)과 대조한다.
8.1 file_alloc의 분기
섹션 제목: “8.1 file_alloc의 분기”모든 페이지 할당은 file_alloc에서 시작한다. 헤더를 fix하고 sanity check를 수행한 다음, 단 하나의 조건식이 실행 경로를 둘로 나눈다:
// file_alloc -- src/storage/file_manager.cif (FILE_IS_TEMPORARY (fhead)) error_code = file_temp_alloc (thread_p, page_fhead, FILE_ALLOC_USER_PAGE, vpid_out); /* <- no sysop, no undo */else { log_sysop_start_atomic (thread_p); /* <- permanent path opens a nested top action (Ch.5) */ is_sysop_started = true; error_code = file_perm_alloc (thread_p, page_fhead, FILE_ALLOC_USER_PAGE, vpid_out); VFID_COPY ((VFID *) undo_log_data, vfid); /* <- pack (VFID,VPID) logical-undo payload */ VPID_COPY ((VPID *) (undo_log_data + sizeof (VFID)), vpid_out); }세 가지 비대칭이 모든 곳에 영향을 미친다: 임시 경로는 system operation을 시작하지 않고 (is_sysop_started가 false로 유지됨), undo 데이터를 구성하지 않으며, file_temp_alloc을 호출한다. 종료 레이블의 sysop 에필로그는 if (is_sysop_started)로 보호되므로, 임시 경로는 전체 블록(log_sysop_abort와 log_sysop_end_logical_undo (thread_p, RVFL_ALLOC, ...))을 건너뛴다. 따라서 임시 할당은 복구 시 재생할 로그를 전혀 남기지 않는다. 트랜잭션이 중간에 실패하면 파일은 그냥 폐기된다 — 로그된 것이 없으니 롤백할 것도 없다.
f_init 처리도 분기된다: 임시 파일의 f_init은 NULL일 수 있고(정렬 버퍼는 자체적으로 페이지를 초기화한다), else 경로는 FILE_IS_TEMPORARY (fhead)를 assert한 뒤 NEW_PAGE로 페이지를 fix한다. Numerable 임시 파일은 여전히 file_numerable_add_page(Ch.10)를 호출한다 — 임시 파일이라도 사용자 페이지 테이블 등록은 면제되지 않는다. 이 분기가 Figure 8-2의 최상단이다.
8.2 헤더 커서: 전체 부기 상태
섹션 제목: “8.2 헤더 커서: 전체 부기 상태”영구 파일은 Partial과 Full이라는 두 개의 확장 가능한 테이블을 유지하고 그 사이에서 섹터를 마이그레이션한다. 임시 파일은 Partial 테이블만 유지하며, 두 개의 필드로 이루어진 커서가 전체 할당 상태를 담는다:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
vpid_last_temp_alloc | 현재 채워지고 있는 섹터를 담은 Partial 테이블 페이지의 VPID | 할당 시 라이브 테이블 페이지로 바로 이동할 수 있게 한다. 헤더 내 복사본이면 헤더 VPID와 동일, 그렇지 않으면 오버플로 PAGE_FTAB 페이지 |
offset_to_last_temp_alloc | 해당 페이지의 extensible data 내에서 채워지고 있는 FILE_PARTIAL_SECTOR의 인덱스 | 정확한 섹터를 가리킨다. 섹터가 가득 찰 때만 전진하므로, 페이지 내에서 완전히 소비된 섹터 수도 겸한다 |
struct 주석은 설계 계약을 직접 명시한다 — “임시 파일 페이지는 절대 해제되지 않는다 … 커서를 유지한다: 섹터가 꽉 차면 커서가 증가하고, 페이지 전체가 꽉 차면 다음 페이지로 이동한다”:
// FILE_HEADER -- src/storage/file_manager.cVPID vpid_last_temp_alloc; /* VPID of partial table page last used to allocate a page. */int offset_to_last_temp_alloc; /* Sector offset in partial table last used to allocate a page. */커서는 생성 시 초기화된다: file_create의 임시 경로는 vpid_last_temp_alloc = vpid_fhead(헤더 자체의 Partial 테이블)로 설정하고 offset_to_last_temp_alloc = fhead->n_sector_full로 설정하여, 생성 시점에 이미 가득 찬 섹터들을 건너뛴다.
불변 조건 (커서 일관성).
offset_to_last_temp_alloc은 항상vpid_last_temp_alloc의 extensible data 내에서 유효한 인덱스이거나, 정확히 해당 item count(“다음 호출에서 다음 페이지로 전진”)와 같아야 한다.file_temp_alloc은 역참조 전에 두 조건 모두를 assert한다. 위반 시,file_extdata_at이 배열 경계를 초과하여 인접 섹터 디스크립터를 오염시키거나 VSID 자리를 garbage로 읽는다.
graph LR H["FILE_HEADER"] -->|vpid_last_temp_alloc| P0["Partial 테이블 페이지\n헤더 내부 또는 PAGE_FTAB"] H -->|offset_to_last_temp_alloc| PS["FILE_PARTIAL_SECTOR + page_bitmap"] P0 -->|vpid_next| P1["다음 Partial 테이블 페이지 ..."]
Figure 8-1. 커서와 테이블의 관계. Full 테이블 없음 — 가득 찬 섹터는 커서 앞에 그대로 남는다.
8.3 file_temp_alloc 분기별 코드 추적
섹션 제목: “8.3 file_temp_alloc 분기별 코드 추적”이 함수는 먼저 인터럽트 검사를 비활성화한다(logtb_set_check_interrupt (thread_p, false), 이전 값을 save_check_interrupt에 저장). 롤백이 없으므로 절반만 진행된 임시 할당이 중간에 해제되어서는 안 된다. 그 다음 FILE_IS_TEMPORARY (fhead)를 assert한다.
Step 1 — 라이브 Partial 테이블 페이지 위치 확인. 커서가 헤더를 가리키면 헤더 내 테이블을 직접 사용하고, 그렇지 않으면 오버플로 페이지를 write latch로 fix한다(실패 시 ER_INTERRUPTED만 허용):
// file_temp_alloc -- src/storage/file_manager.cif (VPID_EQ (&vpid_fhead, &fhead->vpid_last_temp_alloc)) FILE_HEADER_GET_PART_FTAB (fhead, extdata_part_ftab); /* <- table lives in header page */else { page_ftab = pgbuf_fix (thread_p, &fhead->vpid_last_temp_alloc, OLD_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH); if (page_ftab == NULL) { error_code = er_errid (); if (error_code != ER_INTERRUPTED) assert_release (false); goto exit; } extdata_part_ftab = (FILE_EXTENSIBLE_DATA *) page_ftab; }Step 2 — 여유 페이지가 없으면 확장. file_temp_expand의 인라인 구현에 해당한다: n_page_free == 0이면 디스크 관리자(Ch.4)에 DB_TEMPORARY_DATA_PURPOSE로 새 섹터 하나를 예약하여 임시 볼륨에 할당받는다:
// file_temp_alloc -- src/storage/file_manager.cif (fhead->n_page_free == 0) { FILE_PARTIAL_SECTOR partsect_new = FILE_PARTIAL_SECTOR_INITIALIZER; error_code = disk_reserve_sectors (thread_p, DB_TEMPORARY_DATA_PURPOSE, fhead->volid_last_expand, 1, &partsect_new.vsid); if (error_code != NO_ERROR) { /* same ER_INTERRUPTED-tolerated handling as Step 1 */ goto exit; }이후 현재 페이지에 FILE_PARTIAL_SECTOR를 하나 더 기록할 공간이 있는지에 따라 두 개의 하위 분기로 나뉜다. 하위 분기 2a — 테이블 페이지가 꽉 찬 경우: 새 섹터를 기록할 공간이 없으므로, 해당 섹터의 첫 번째 페이지를 새 Partial 테이블 페이지 용도로 전용한다(bit 0 설정, 타입 PAGE_FTAB, 이전 vpid_next 연결, 커서를 offset 0으로 이동):
// file_temp_alloc -- src/storage/file_manager.cif (file_extdata_is_full (extdata_part_ftab)) { vpid_ftab_new.volid = partsect_new.vsid.volid; vpid_ftab_new.pageid = SECTOR_FIRST_PAGEID (partsect_new.vsid.sectid); file_partsect_set_bit (&partsect_new, 0); /* <- page 0 becomes the table page */ page_ftab_new = pgbuf_fix (thread_p, &vpid_ftab_new, NEW_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH); if (page_ftab_new == NULL) { error_code = ER_FAILED; goto exit; } pgbuf_set_page_ptype (thread_p, page_ftab_new, PAGE_FTAB); VPID_COPY (&extdata_part_ftab->vpid_next, &vpid_ftab_new); /* <- link old table -> new table */ if (page_ftab != NULL) pgbuf_set_dirty_and_free (thread_p, page_ftab); VPID_COPY (&fhead->vpid_last_temp_alloc, &vpid_ftab_new); /* <- cursor wraps to fresh table page */ fhead->offset_to_last_temp_alloc = 0; page_ftab = page_ftab_new; extdata_part_ftab = (FILE_EXTENSIBLE_DATA *) page_ftab; file_extdata_init (sizeof (FILE_PARTIAL_SECTOR), DB_PAGESIZE, extdata_part_ftab); ATOMIC_INC_32 (&file_Tempcache.spacedb_temp.npage_reserved, DISK_SECTOR_NPAGES - 1); ATOMIC_INC_32 (&file_Tempcache.spacedb_temp.npage_ftab, 1); }else ATOMIC_INC_32 (&file_Tempcache.spacedb_temp.npage_reserved, DISK_SECTOR_NPAGES); /* all pages reservable */커서가 새 테이블 페이지로 이동하는 유일한 지점이 바로 이 확장 경로다. 테이블 페이지로 떼어낸 페이지는 npage_ftab으로 계산되고, 나머지 DISK_SECTOR_NPAGES - 1개만 예약 가능하다. 어느 하위 분기를 거치든 섹터를 append하고 카운터를 증가시킨다 — 테이블 페이지 호스팅 여부는 partsect_new.page_bitmap으로 구분된다(2a에서만 비어 있지 않음):
// file_temp_alloc -- src/storage/file_manager.cfile_extdata_append (extdata_part_ftab, &partsect_new);fhead->n_sector_partial++; fhead->n_sector_total++; // n_page_free/n_page_total += DISK_SECTOR_NPAGESif (partsect_new.page_bitmap == FILE_EMPTY_PAGE_BITMAP) fhead->n_sector_empty++;else { fhead->n_page_free--; fhead->n_page_ftab++; } /* <- table page already consumed */불변 조건 (섹터는 Partial에서 벗어나지 않는다). 가득 찬 섹터는 all-ones 비트맵을 그 자리에 유지한다. Full 테이블로 마이그레이션하지 않는다. 위반 시, 커서 offset(페이지 내 소비된 섹터 수를 겸함)이 extensible data 레이아웃과 일치하지 않아, Step 3의 페이지 이동이 라이브 섹터를 건너뛰게 된다.
Step 3 — 커서가 item count에 위치하면 다음 페이지로 이동. 이전 호출이 offset_to_last_temp_alloc을 현재 꽉 찬 페이지의 마지막 섹터 바로 다음으로 남겨놓았을 수 있다. if (fhead->offset_to_last_temp_alloc == file_extdata_item_count (extdata_part_ftab)) 조건이 충족되면: file_extdata_is_full (...) 및 !VPID_ISNULL (&extdata_part_ftab->vpid_next)를 assert하고, 기존 page_ftab을 unfix한 뒤 vpid_next를 write latch로 fix하며(오직 ER_INTERRUPTED만 허용), vpid_last_temp_alloc = vpid_next; offset_to_last_temp_alloc = 0으로 설정한다.
Step 4 — 커서 아래 섹터에서 할당. file_partsect_alloc이 첫 번째 0 비트를 설정한다. 여기서 false 반환은 불가능하며(커서는 절대 가득 찬 섹터를 가리키지 않는다), 만약 발생하면 로직 오류로 처리한다:
// file_temp_alloc -- src/storage/file_manager.cpartsect = (FILE_PARTIAL_SECTOR *) file_extdata_at (extdata_part_ftab, fhead->offset_to_last_temp_alloc);was_empty = file_partsect_is_empty (partsect);if (!file_partsect_alloc (partsect, vpid_alloc_out, NULL)) { assert_release (false); error_code = ER_FAILED; goto exit; } /* <- full sector under cursor == bug */if (file_partsect_is_full (partsect)) { is_full = true; fhead->offset_to_last_temp_alloc++; } /* <- advance cursor; page hop deferred to next call */file_header_alloc (fhead, alloc_type, was_empty, is_full); /* <- shared with perm path: pure counter math */pgbuf_set_dirty (thread_p, page_fhead, DONT_FREE);커서는 매 할당마다 전진하는 것이 아니라 섹터가 가득 찰 때 전진한다: 섹터에 여유 비트가 남아있는 동안은 커서가 고정되므로, 일반적인 경우에는 헤더와 테이블 페이지 하나만 접근하면 된다. file_header_alloc은 영구 경로와 공유되는 헬퍼(Ch.7)다. 임시 파일에서도 is_full 처리로 n_sector_full/n_sector_partial이 갱신되지만, 테이블 마이그레이션은 동반되지 않는다 — 이 카운터들은 테이블 멤버십이 아니라 통계 정보일 뿐이다.
Step 5 — 무조건 실행되는 정리. exit 레이블은 모든 경로에서 실행된다: file_header_sanity_check, page_ftab unfix(보유 중인 경우), logtb_set_check_interrupt로 인터럽트 플래그 복원, return error_code. pgbuf_set_dirty가 로그 append와 짝을 이루는 경우는 없다 — 유일한 내구성 동작은 임시 데이터의 non-WAL flush를 위해 페이지를 dirty로 표시하는 것뿐이다.
flowchart TD
A["file_temp_alloc\n인터럽트 검사 비활성화"] --> C{"커서 == 헤더 VPID?"}
C -->|yes| F{"n_page_free == 0?"}
C -->|no| E["커서의 테이블 페이지 fix"] --> F
F -->|yes| G["disk_reserve_sectors 1 섹터"] --> H{"테이블 페이지 꽉 참?"}
H -->|yes| I["page0을 PAGE_FTAB으로 전용\nvpid_next 연결, 커서 초기화"] --> L["섹터 append, 카운터 증가"]
H -->|no| J["DISK_SECTOR_NPAGES 예약"] --> L
F -->|no| K{"offset == item_count?"}
L --> K
K -->|yes| N["vpid_next fix, 커서 offset 0"] --> O["file_extdata_at + file_partsect_alloc"]
K -->|no| O
O --> Q{"섹터 가득 참?"}
Q -->|yes| R["offset_to_last_temp_alloc++"] --> T["file_header_alloc, set_dirty"]
Q -->|no| T
T --> U["exit: unfix, 인터럽트 복원"]
Figure 8-2. file_temp_alloc 완전 분기 맵 — 두 가지 확장 하위 분기와 지연된 페이지 이동을 포함한다.
8.4 Full 테이블, postpone, WAL이 없는 이유
섹션 제목: “8.4 Full 테이블, postpone, WAL이 없는 이유”Full 테이블이 영구 파일에 존재하는 이유는 여유 페이지가 없는 섹터를 스캔 시 건너뛰기 위해서다(Ch.7). 임시 파일은 스캔을 수행하지 않는다 — 단일 커서 섹터에서 순차 할당한다 — 따라서 가득 찬 섹터는 다시 방문되지 않으며, 두 번째 테이블은 설계가 회피하는 로깅 비용만 더할 뿐 이득이 없다. companion 문서 cubrid-disk-manager.md에 절감 효과가 열거되어 있고, 위 코드가 그 메커니즘이다.
불변 조건 (단조 증가, 부기 없는 할당). 커서는 오직 전진만 한다(섹터 가득 참 → offset++, item count 도달 → 페이지 이동).
file_dealloc은 임시 파일에서 빈else분기를 밟는다 — postpone 없음, 비트맵 변경 없음 — 즉 해제를 완전히 건너뛴다(Ch.9). 따라서 커서 뒤의 어떤 섹터도 free 비트를 회복하지 않으며, 단조 속성은 조정(reconciliation) 없이도 유지된다. 위반 시(커서 뒤의 비트가 해제됨), 해당 페이지는 조용히 누수되고n_page_free가 실제와 어긋나게 된다.
8.5 재활용: 커서 리셋
섹션 제목: “8.5 재활용: 커서 리셋”이 최소주의적 설계 덕분에 tempcache(Ch.11)는 파일을 재구성(rebuild) 대신 *리셋(reset)*으로 재활용할 수 있다. file_temp_reset_user_pages는 Partial 테이블 비트맵을 재수집하고, n_sector_*/n_page_* 카운터를 재구축하며, 사용자 카운트를 초기화하고, 커서를 헤더 VPID, offset 0으로 되감는다:
// file_temp_reset_user_pages -- src/storage/file_manager.cfhead->n_page_user = 0; // ... n_sector_*/n_page_* rebuilt from re-collected bitmaps ...fhead->vpid_last_temp_alloc = vpid_fhead; /* <- cursor rewinds to header VPID, offset 0 */fhead->offset_to_last_temp_alloc = 0;이 초기값은 file_create의 것과 다르다. file_create는 offset_to_last_temp_alloc = fhead->n_sector_full로 설정하지만, 리셋은 항상 offset 0으로 되감는다. 리셋된 파일은 예약된 섹터를 그대로 유지하여(디스크 왕복 없음) 처음부터 다시 페이지를 제공한다 — Partial→Full 메커니즘을 생략한 덕분에 할당 상태가 두 개의 정수로 축약되어 리셋 비용이 사실상 없다.
8.6 챕터 요약 — 핵심 정리
섹션 제목: “8.6 챕터 요약 — 핵심 정리”file_alloc은FILE_IS_TEMPORARY에 따라 분기한다: 임시 경로는 sysop 없음, undo 데이터 없음, 로그 레코드 없음으로file_temp_alloc을 호출하고, 영구 경로는RVFL_ALLOClogical undo를 가진 nested top action 안에서file_perm_alloc을 래핑한다.- 임시 파일은 Partial 섹터 테이블만 유지하며, 가득 찬 섹터는 그 자리에 남는다. 전체 할당 상태는
vpid_last_temp_alloc/offset_to_last_temp_alloc두 필드로 표현된다. - 커서 덕분에 할당이 **O(1)**이 된다: Step 4는
file_partsect_alloc으로 커서 섹터에서 직접 할당하고, 섹터가 가득 찰 때만 offset을 증가시키며, 페이지 이동은 다음 호출(Step 3)로 미룬다. - 확장은 인라인(
n_page_free == 0)으로 수행된다:DB_TEMPORARY_DATA_PURPOSE로 섹터를 하나 예약하고, 테이블 페이지 꽉 참 하위 분기는 섹터의 첫 번째 페이지를 새PAGE_FTAB으로 전용하여vpid_next를 연결하고 커서를 offset 0으로 이동시킨다. - Full 테이블 마이그레이션, postpone, WAL 모두 없다 — 임시 페이지는 개별 해제되지 않으므로(
file_dealloc이 빈else분기를 밟음), 커서는 증명 가능하게 단조 증가하며 조정이 필요 없다. file_header_alloc은 영구 경로와 공유되지만, 임시 파일에서is_full처리는 통계 카운터 갱신일 뿐이다 — 테이블 이동이 수반되지 않는다.- 부기 없는 설계 덕분에 tempcache는 테이블을 재구성하는 대신 커서 리셋(
vpid_last_temp_alloc = 헤더 VPID,offset_to_last_temp_alloc = 0,n_page_user = 0)과 비트맵 기반 카운터 재구축으로 파일을 재활용할 수 있다(Ch.11). 리셋은 offset 0으로 되돌아가며,file_create의n_sector_full초기값과 다르다.
Chapter 9: 페이지 반환과 파일 삭제
섹션 제목: “Chapter 9: 페이지 반환과 파일 삭제”이 챕터는 영구 할당(Chapter 7)의 역방향 흐름을 추적한다:
페이지 하나, 그리고 파일 전체가 어떻게 반환되며, 왜 실제 비트 클리어는
커밋 시점까지 미뤄지는가? 사전 지식으로 Chapter 4(2단계 예약 프로토콜,
비트맵-then-캐시 해제 순서 불변식)와 Chapter 7(file_perm_alloc, Partial/Full
테이블)을 가정한다. 섹터 비트맵 및 디스크/파일 분리에 대한 설명은 동반
문서 cubrid-disk-manager.md를 참고한다. 핵심 사실: 해제된 영구 페이지나
섹터는 동기적으로 클리어되지 않는다 — 해제자는 postpone 로그 레코드를
스테이징하고, 클리어는 do-postpone 시점에 실행된다.
9.1 왜 지연시키는가 — committed-releaser 위험
섹션 제목: “9.1 왜 지연시키는가 — committed-releaser 위험”트랜잭션 T1이 페이지 P를 해제하는 즉시 비트가 클리어된다면, 두 번째 트랜잭션이 해당 섹터를 예약하고 P를 할당받아 데이터를 커밋할 수 있다. 이후 T1이 중단(abort)되면, 언두(undo)가 P의 이전 내용을 복원하면서 두 번째 트랜잭션의 커밋된 데이터를 덮어쓰게 된다. CUBRID는 클리어를 do-postpone까지 지연시킨다 — do-postpone은 커밋이 논리적으로 확정된 후에만 실행되므로, 그 시점까지 비트는 세트 상태를 유지하고, 어떤 할당자도 해당 페이지를 내줄 수 없다. Chapter 4의 해제 순서 불변식과 동일한 논리다.
불변식 (deferred-free): 활성 트랜잭션이 해제한 영구 페이지/섹터는 do-postpone까지 비트가 세트 상태를 유지한다. 이는 모든 영구 해제를 비트맵 인라인 변경 대신
log_append_postpone (RVFL_DEALLOC)/(RVDK_UNRESERVE_SECTORS)경로로 라우팅함으로써 강제된다. 위반 시, 동시 할당자가 해당 페이지를 재배분하고 이후 abort가 새 소유자의 데이터를 오염시킨다.
stateDiagram-v2 [*] --> Allocated Allocated --> PostponeStaged : file_dealloc \n RVFL_DEALLOC 추가, 비트 유지 PostponeStaged --> Allocated : 트랜잭션 abort \n postpone 폐기, 페이지 할당 유지 PostponeStaged --> Freed : do-postpone \n file_perm_dealloc이 비트 클리어 Freed --> [*]
Figure 9-1 — 영구 페이지 비트의 생명주기. abort 엣지가 핵심이다: do-postpone 이전까지 디스크에는 아무 변화도 없다.
9.2 file_dealloc — 스테이징, 해제가 아님
섹션 제목: “9.2 file_dealloc — 스테이징, 해제가 아님”file_dealloc은 페이지 하나를 돌려주는 공개 진입점이지만, 이름과 달리 보통
스테이징만 수행한다. 헤더 픽스(fix)는 조건부다: 신뢰할 수 있는 구체적
file_type_hint가 있는 릴리스 빌드는 I/O를 아끼기 위해 픽스를 건너뛰는 반면,
디버그 빌드는 항상 픽스하며(#if defined (NDEBUG) 가드) hint가 fhead->type과
일치하고 vpid가 고정(sticky) 첫 번째 페이지가 아님을 단언한다. postpone
결정은 불확실성 아래 보수적으로 내려진다 — 파일이 임시 파일임을 증명할 수
없으면 postpone한다:
// file_dealloc -- src/storage/file_manager.c if ((fhead != NULL && !FILE_IS_TEMPORARY (fhead)) || file_type_hint != FILE_TEMP) { VFID_COPY ((VFID *) log_data, vfid); VPID_COPY ((VPID *) (log_data + sizeof (VFID)), vpid); log_append_postpone (thread_p, RVFL_DEALLOC, &log_addr, LOG_DATA_SIZE, log_data); /* <- stage only */ } /* else: we do not deallocate pages from temporary files */RVFL_DEALLOC 레코드는 (VFID, VPID)만 담는다 — 비트맵 상태는 없다 — 실제
작업이 do-postpone에서 재계산되기 때문이다. 임시 파일은 else 경로를 밟는다
(소멸 또는 tempcache 리셋 시 일괄 회수, Chapter 11). 이후 두 개의 조기 종료(early
exit)가 numerability 기준으로 분기한다: !FILE_TYPE_CAN_BE_NUMERABLE (file_type_hint)(타입상 numerable 불가)이면 goto exit, !FILE_IS_NUMERABLE (fhead)(타입은 허용하지만 이 파일은 아님)이면 다시 goto exit. 진정으로
numerable한 파일만 지금 당장 동작한다 — user page 테이블에서 페이지를
검색하고 FILE_USER_PAGE_MARK_DELETED를 설정하며, 비임시 파일의 경우
RVFL_USER_PAGE_MARK_DELETE를 로깅한다(상세 내용은 Chapter 10으로 위임).
불변식 (numerable consistency): numerable 파일에서 페이지는 user page 테이블에 존재해야 하며 이미 삭제 표시되어 있으면 안 된다(
!found및FILE_USER_PAGE_IS_MARKED_DELETED에 대한assert_release (false)로 강제). 위반 시, user page 테이블과 할당 테이블이 발산(diverge)한 것 — 하드 버그다.
exit: 레이블에서 고정되어 있던 page_fhead와 page_ftab을 언픽스(unfix)한다.
9.3 file_perm_dealloc — do-postpone에서의 실제 비트 클리어
섹션 제목: “9.3 file_perm_dealloc — do-postpone에서의 실제 비트 클리어”커밋 시, do-postpone은 각 RVFL_DEALLOC을 file_rv_dealloc_on_postpone →
file_rv_dealloc_internal 경로로 재실행한다. 이 함수는 헤더를 픽스하고, 시스템
연산(system operation)을 시작하며, file_perm_dealloc을 호출한다 — 비트가
마침내 클리어되는 곳이다. 진입 시 계약을 단언한다: log_check_system_op_is_started
(sysop 안에 있어야 함)과 !FILE_IS_TEMPORARY (fhead) (영구 파일 전용). 이후
vsid_dealloc을 vpid_dealloc으로부터 계산한다(SECTOR_FROM_PAGEID).
불변식 (sysop-wrapped table change):
file_perm_dealloc내의 모든 파일 테이블 변경은 헤더 페이지를 언픽스하기 전에 중첩 시스템 연산으로 커밋되어야 한다. 위반 시, 업데이트 도중 크래시가 발생하면 Partial/Full 테이블과 헤더 카운터가 원자적 복구 경계 없이 불일치 상태로 남는다.
flowchart TB
START["file_perm_dealloc(vpid)"] --> SEARCH["Partial 테이블 검색"]
SEARCH --> FOUND{Partial에서 찾음?}
FOUND -- yes --> CLEAR["partsect에서 비트 클리어<br/>RVFL_PARTSECT_DEALLOC 로그<br/>is_empty?"]
FOUND -- no --> REMOVE["Full 테이블에서 vsid 제거<br/>was_full = true"]
REMOVE --> MERGED{ftab 페이지 병합 소멸?}
MERGED -- "같은 섹터" --> SAMESEC["병합된 페이지 비트도 클리어<br/>ftab dealloc 시뮬레이션"]
MERGED -- "다른 섹터" --> RECURSE["file_perm_dealloc(merged) 재귀"]
MERGED -- none --> BUILD["partsect_new = FULL minus bit 구성"]
SAMESEC --> BUILD
RECURSE --> BUILD
BUILD --> SPACE{Partial에 빈 슬롯?}
SPACE -- yes --> INSERT["file_extdata_insert_at 정렬 삽입"]
SPACE -- no --> NEWPG["file_perm_alloc 새 ftab 페이지"]
CLEAR --> HDR["file_header_dealloc<br/>카운터 갱신"]
INSERT --> HDR
NEWPG --> HDR
HDR --> DEALLOC["pgbuf_dealloc_page(vpid)"]
DEALLOC --> EXIT["exit: page_ftab 언픽스"]
Figure 9-2 — file_perm_dealloc의 분기 맵. 왼쪽: 섹터가 이미 Partial 상태인
일반 케이스. 오른쪽: 섹터가 Full이었던 경우로, Full-to-Partial 마이그레이션이
발생하며 재귀할 수 있다.
왼쪽 분기 — 이미 Partial. 섹터에 빈 페이지가 있어 이미 Partial 테이블에
있는 경우: 비트를 클리어하고, is_empty를 재계산하며, log_append_undoredo_data로
RVFL_PARTSECT_DEALLOC을 로깅한다 — undoredo이지 postpone이 아닌데,
do-postpone 시점에서는 이미 해제를 실행하고 있으므로 테이블 편집은
일반적인 복구 가능한 변경이기 때문이다.
오른쪽 분기 — 섹터가 Full이었음. 예약된 섹터는 정확히 하나의 테이블에만
있으므로(Chapter 6), Partial에 없다면 Full에 있다. 함수는 was_full = true로
설정하고 Full 테이블에서 file_extdata_find_and_remove_item을 호출한다. 이때
마지막 Full-테이블 컴포넌트가 비워지면 vpid_merged — 이제 고아가 된 테이블
페이지 — 가 반환되며, 이것도 해제해야 한다. 가드는 Figure 9-2의 두 병합 케이스로
표현되며, VSID_IS_SECTOR_OF_VPID (&vsid_dealloc, &vpid_merged) 여부에 따라
결정된다:
- 다른 섹터 →
file_perm_dealloc (..., &vpid_merged, FILE_ALLOC_TABLE_PAGE)로 재귀하여 정상적으로 해제; - 같은 섹터 (Partial로 이동하는 섹터) → 재귀하지 않는다;
is_merged_page_from_sector를 설정하고 새 디스크립터에서도 해당 페이지의 비트를 클리어한다.file_header_dealloc (..., FILE_ALLOC_TABLE_PAGE, ...)후pgbuf_dealloc_page (vpid_merged)로 어카운팅을 시뮬레이션한다.
새 디스크립터는 partsect_new.page_bitmap = FILE_FULL_PAGE_BITMAP에서 시작해
file_partsect_clear_bit로 해제된 비트를 클리어한 뒤 정렬된 위치에 삽입된다.
Partial에 빈 슬롯이 없으면 file_perm_alloc (FILE_ALLOC_TABLE_PAGE)에서 새
테이블 페이지를 가져온다. 가드: file_extdata_find_ordered는 VSID가 없다고
보고해야 한다(중복 시 assert_release (false)), assert (page_ftab == NULL)은
모든 임시 테이블 페이지가 언픽스되었음을 확인한다.
테일 — 두 분기 공통. file_header_dealloc (fhead, alloc_type, is_empty, was_full)이 n_page_free / 섹터 카운터를 조정하고(file_log_fhead_dealloc으로
로깅), 페이지는 픽스되어 pgbuf_dealloc_page로 넘겨지며, PSTAT_FILE_NUM_PAGE_DEALLOCS가
증가한다. is_empty/was_full이 계산을 주도한다: was_full 섹터는 이제 빈
페이지를 기여하고, is_empty 섹터는 완전히 비게 된다. 대부분의 에러 경로는
ASSERT_ERROR (); goto exit로 고정된 page_ftab을 언픽스한다. Full 분기의
두 하위 경로 — 다른 섹터 고아의 재귀 file_perm_dealloc과 같은 섹터 병합
페이지의 pgbuf_fix 실패 — 는 대신 return error_code를 직접 반환하는데, 그
시점에서 page_ftab이 여전히 NULL이므로 안전하다. 복구 도중 하드 실패 가드는
이 함수가 아니라 한 단계 위의 file_rv_dealloc_internal(§9.8)에 있다.
9.4 file_destroy — 파일 전체 반환
섹션 제목: “9.4 file_destroy — 파일 전체 반환”파일을 삭제하면 예약된 모든 섹터가 반환된다. is_temp 플래그가 함수 전체를
두 경로로 분기한다. 프롤로그: 영구 파일은 file_tracker_unregister(카탈로그
가시적, 먼저 드롭됨)를 호출하고, 임시 파일은 logtb_set_check_interrupt (thread_p, false)를 호출하여 삭제가 중간에 중단되어 페이지가 누수되지 않도록 한다. 헤더를
픽스하고 file_table_collect_all_vsids로 모든 섹터를 수집한 뒤, 두 경로는
이빅션(eviction)에서 갈라졌다가 하나의 disk_unreserve_ordered_sectors 호출에서
다시 합쳐진다.
flowchart TB
P["file_destroy(vfid, is_temp)"] --> FORK{is_temp?}
FORK -- no --> UNREG["file_tracker_unregister"]
FORK -- yes --> NOINT["인터럽트 체크 비활성화"]
UNREG --> FIX["헤더 페이지 픽스"]
NOINT --> FIX
FIX --> COLLECT["file_table_collect_all_vsids<br/>-> vsid_collector"]
COLLECT --> FORK2{영구 또는 임시?}
FORK2 -- permanent --> PDEAL["Partial+Full에서 file_sector_map_dealloc<br/>user+ftab 각 페이지 pgbuf_dealloc_page<br/>pgbuf_dealloc_page(header)"]
FORK2 -- temporary --> TDEAL["Partial에서 file_sector_map_dealloc_temp<br/>각 페이지 pgbuf_dealloc_temp_page<br/>Tempcache 카운터 감소"]
PDEAL --> UNRES["disk_unreserve_ordered_sectors"]
TDEAL --> UNRES
UNRES --> EXIT["exit: 수집기 해제, 헤더 언픽스,<br/>인터럽트 체크 복원"]
Figure 9-3 — file_destroy의 두 경로.
영구 경로. file_extdata_apply_funcs가 Partial과 Full 각각에
file_extdata_collect_ftab_pages(파일-테이블-페이지 섹터를 FILE_FTAB_COLLECTOR로
수집)와 file_sector_map_dealloc(각 user 페이지를 픽스하여 pgbuf_dealloc_page)을
적용한다. 이어서 수집된 각 테이블-페이지 섹터와 마지막으로 헤더를 이빅트한다.
섹터가 unreserve되기 전에 소유한 모든 페이지가 PAGE_UNKNOWN 이빅션 후보가
된다.
임시 경로. Full 테이블이 없으므로 Partial만 file_sector_map_dealloc_temp /
pgbuf_dealloc_temp_page로 순회한다. 아무것도 로깅하지 않으며 누락된 페이지를
허용한다(pgbuf_simple_fix NULL → continue) — 임시 페이지는 반드시 디스크에
있을 필요가 없기 때문이다. 이후 전역 tempcache spacedb_temp 카운터(Chapter 11)를
감소시키고 헤더를 해제한다.
불변식 (evict-before-unreserve): 파일의 모든 버퍼 풀 페이지는 섹터가 unreserve되기 전에 이빅션 후보가 되어야 한다(
pgbuf_dealloc_page/pgbuf_dealloc_temp_page). 위반 시, 오래된 더티 BCB가 이미 unreserve되고 다른 파일에 의해 재예약된 섹터로 플러시될 수 있으며, 한 파일의 바이트가 다른 파일에 쓰이게 된다.
exit: 레이블은 범용 정리다: 헤더 언픽스, 두 수집기 배열 db_private_free,
임시 케이스를 위한 인터럽트 체크 플래그 복원.
9.5 file_vsid_collector와 file_table_collect_all_vsids
섹션 제목: “9.5 file_vsid_collector와 file_table_collect_all_vsids”수집기는 고정 크기 배열과 카운트로 구성된다:
// struct file_vsid_collector -- src/storage/file_manager.cstruct file_vsid_collector { VSID *vsids; int n_vsids; };| 필드 | 역할 | 존재 이유 |
|---|---|---|
vsids | fhead->n_sector_total개의 VSID를 담아 db_private_alloc으로 할당된 배열의 포인터 | 출력 버퍼로, 섹터 수에 정확히 맞게 크기가 결정되어 realloc이 불필요하다. |
n_vsids | 추가된 섹터의 누적 카운트 | 수집 도중에는 배열 커서로, 수집 완료 후에는 disk_unreserve_ordered_sectors에 넘기는 요소 수로 사용된다. 수집 후 반드시 n_sector_total과 같아야 한다. |
file_table_collect_all_vsids는 배열을 할당한 뒤 file_table_collect_vsid
(collector->vsids[collector->n_vsids++] = *vsid)를 Partial과 — 영구 파일만
— Full 전체에 적용한다:
// file_table_collect_all_vsids -- src/storage/file_manager.c collector_out->vsids = (VSID *) db_private_alloc (thread_p, fhead->n_sector_total * sizeof (VSID)); FILE_HEADER_GET_PART_FTAB (fhead, extdata_ftab); error_code = file_extdata_apply_funcs (thread_p, extdata_ftab, NULL, NULL, file_table_collect_vsid, collector_out, ...); if (!FILE_IS_TEMPORARY (fhead)) { FILE_HEADER_GET_FULL_FTAB (fhead, extdata_ftab); /* <- temporary files have no full table */ error_code = file_extdata_apply_funcs (thread_p, extdata_ftab, NULL, NULL, file_table_collect_vsid, collector_out, ...); } if (collector_out->n_vsids != fhead->n_sector_total) assert_release (false); /* <- the count invariant, checked */ qsort (collector_out->vsids, fhead->n_sector_total, sizeof (VSID), disk_compare_vsids); /* <- ordered output */불변식 (complete collection): 수집된 VSID 수는 반드시
fhead->n_sector_total과 같아야 한다. 위반 시, 파일 부기(bookkeeping)가 손상된 것이며 삭제가assert_release (false)로 실패한다.
마지막 qsort는 다음 함수의 사전 조건을 확립한다 — disk_unreserve_ordered_sectors가
한 번의 순회로 볼륨별로 배치(batch) 처리할 수 있도록 VSID 목록을 (volid, sectid)
순으로 정렬한다.
9.6 pgbuf_dealloc_page — 이빅션 힌트
섹션 제목: “9.6 pgbuf_dealloc_page — 이빅션 힌트”file_perm_dealloc과 영구 파일의 file_destroy 경로 모두 해제된 각 페이지를
pgbuf_dealloc_page로 넘긴다. 이 함수는 플러시나 쓰기 I/O를 수행하지 않는다
— 페이지 타입을 PAGE_UNKNOWN으로 초기화하고 BCB를 victimization 방향으로
유도할 뿐이다:
// pgbuf_dealloc_page -- src/storage/page_buffer.c /* how it works: page is "deallocated" by resetting its type to PAGE_UNKNOWN. also prepare bcb for victimization. * note: the bcb used to be invalidated. but that means flushing page to disk and waiting for IO write. that may be * too slow. if we add the bcb to the bottom of a lru list, it will be eventually flushed by flush thread and * victimized. */ CAST_PGPTR_TO_BFPTR (bcb, page_dealloc); assert (get_fcnt (&bcb->atomic_latch) == 1); /* <- caller must hold the only latch */할당 해제는 힌트이지 동기적 폐기가 아니다 — 페이지는 나중에 flush 스레드에 의해 플러시될 수 있으며, 바로 그 때문에 evict-before-unreserve 불변식(§9.4)이 섹터가 재사용 가능해지기 전에 이 함수를 호출하도록 요구한다.
9.7 disk_unreserve_ordered_sectors — 섹터 반환
섹션 제목: “9.7 disk_unreserve_ordered_sectors — 섹터 반환”Chapter 4 예약의 디스크 관리자 대응 함수: CSECT_DISK_CHECK를 reader로 취하고
disk_unreserve_ordered_sectors_without_csect에 위임하는 얇은 래퍼다. 워커는
§9.5의 정렬을 활용한다 — volid를 공유하는 연속적인 vsids를 DISK_RESERVE_CONTEXT
안에서 볼륨별 런(run)으로 묶고(런 간 volid가 엄격히 증가하며, 같은 런 내에서는
sectid), 볼륨당 하나의 disk_unreserve_sectors_from_volume을 발행한다. 이 함수는
sector-table 단위를 순회하며 disk_stab_unit_unreserve를 호출한다 — 영구-vs-임시
postpone 분기가 내려앉는 잎 노드로, §9.2의 분기를 섹터 수준에서 그대로 반영한다:
// disk_stab_unit_unreserve -- src/storage/disk_manager.c assert ((unreserve_bits & (*cursor->unit)) == unreserve_bits); /* <- all target bits were actually set */ if (unreserve_bits != 0) { if (context->purpose == DB_PERMANENT_DATA_PURPOSE) log_append_postpone (thread_p, RVDK_UNRESERVE_SECTORS, &addr, sizeof (unreserve_bits), &unreserve_bits); /* <- deferred */ else { (*cursor->unit) &= ~unreserve_bits; /* <- bitmap cleared NOW */ /* ... pgbuf_set_dirty + lock_reserve_for_purpose condensed ... */ disk_cache_update_vol_free (cursor->volheader->volid, nsect); /* <- then cache, Ch.4 order */ } }영구 목적(purpose)은 log_append_postpone (RVDK_UNRESERVE_SECTORS)로
클리어를 스테이징하여 섹터 단위에서 deferred-free 불변식(§9.1)을 유지한다.
임시 목적은 Chapter 4의 해제 순서 불변식이 명령하는 비트맵-then-캐시 순서로
즉시 비트를 클리어한다(비트 클리어 후 disk_cache_update_vol_free). 진입 assert는
해제하려는 섹터가 실제로 예약되어 있었음을 보장한다.
9.8 abort 경로 — 상태 복원
섹션 제목: “9.8 abort 경로 — 상태 복원”영구 할당 해제를 abort하는 것은 비용이 없다: 스테이징된 postpone 레코드가
폐기되어 실행되지 않으므로, 페이지는 할당된 상태로 유지된다(Figure 9-1의 역방향
엣지). 실제 언두는 페이지 할당이 롤백될 때만 발생한다. do-postpone과 언두 모두
file_rv_dealloc_internal로 라우팅된다. 이 함수는 헤더를 픽스하고, sysop을
열고, file_perm_dealloc을 호출하며 — 복구 리플레이가 조용히 실패해서는 안
되므로 — NO_ERROR가 아닌 모든 반환값에
if (error_code != NO_ERROR) { assert_release (false); }로 하드 실패한다. 이후
하나의 파라미터로 sysop을 봉인한다: 에러 시 log_sysop_abort, FILE_RV_DEALLOC_COMPENSATE
(alloc의 언두)이면 log_sysop_end_logical_compensate, 그 외(dealloc의 do-postpone)이면
log_sysop_end_logical_run_postpone — 세 가지 모두 헤더가 언픽스되기 전에 테이블
변경을 내구성 있게 만든다(§9.3).
9.9 챕터 요약 — 핵심 정리
섹션 제목: “9.9 챕터 요약 — 핵심 정리”file_dealloc은 스테이징한다, 해제하지 않는다 — 비임시 파일은(VFID, VPID)를 담은RVFL_DEALLOCpostpone 레코드를 추가한다. 임시 파일은 아무것도 할당 해제하지 않는다. numerable 파일은 user-page-table 항목에 mark-delete도 수행한다(Chapter 10).- Postpone이 committed-releaser 창(window)을 닫는다 — 해제자의 커밋이 불가역적이 되기 전에 어떤 트랜잭션도 해제된 페이지를 가져갈 수 없으며, Chapter 4의 해제 순서 불변식의 섹터 수준 유사체다.
file_perm_dealloc이 실제 해제다, 분기가 많다. Partial: 비트 클리어. Full: Partial로 마이그레이션, 고아가 된 테이블 페이지는 재귀로 해제하되 같은 섹터 고아는 인라인 처리. 반드시 system operation 안에서 실행해야 한다.file_destroy는is_temp기준으로 처음부터 끝까지 분기한다. 영구: 등록 해제,pgbuf_dealloc_page로 이빅트, unreserve는 postpone. 임시: 인터럽트 비활성화,pgbuf_dealloc_temp_page로 이빅트, tempcache 카운터 조정, unreserve는 즉시.- 수집 후 삭제, 정렬하여 —
file_table_collect_all_vsids는 정확히n_sector_total개의 VSID를 수집(카운트 단언)하고qsort로 정렬하여 unreserve가 볼륨별로 배치 처리되도록 한다. pgbuf_dealloc_page는 이빅션 힌트이지 플러시가 아니다 —PAGE_UNKNOWNBCB를 victimization 큐에 넣으므로, 페이지는 섹터가 unreserve되기 전에 이빅트되어야 한다.- Postpone 분기의 최종 지점은
disk_stab_unit_unreserve다 — 영구는RVDK_UNRESERVE_SECTORS를 스테이징하고, 임시는 비트맵 후 캐시를 인라인으로 클리어하며, 영구 할당 해제의 abort는 비용이 없다.
Chapter 10: 순번 파일과 사용자 페이지 테이블
섹션 제목: “Chapter 10: 순번 파일과 사용자 페이지 테이블”numerable 파일은 한 가지 약속을 추가한다: “내가 할당한 n번째 페이지를 할당 순서대로 상각 O(1)에 꺼내줘.” 3~7장의 섹터 할당 기구는 이 질문에 답할 수 없다 — 그 구조는 소유권을 저장할 뿐, 순서를 저장하지 않는다. 그래서 numerable 계층은 동일한 VPID들을 대상으로 두 번째 인덱스를 별도로 외부화한다: 사용자 페이지 테이블(User Page Table, UPT). 개략적인 계약은 cubrid-disk-manager.md의 “Numerable files” 절을 참고하고, 여기서는 모든 분기를 하나씩 추적한다.
10.1 섹터 테이블이 할당 순서를 복원할 수 없는 이유
섹션 제목: “10.1 섹터 테이블이 할당 순서를 복원할 수 없는 이유”Partial·Full 섹터 테이블(3장)은 예약과 탐색을 이진 탐색으로 처리하기 위해 disk_compare_vsids로 VSID 정렬 상태를 유지한다. 이 정렬은 두 가지 방식으로 순서 정보를 파괴한다. (1) 승격이 배치 정체성을 지운다 — 섹터가 꽉 차면 Partial → Full로 이동하면서 VSID 순으로 재정렬되고, 어느 배치가 예약했는지 흔적이 사라진다. (2) 확장 간 교차가 순서를 뒤섞는다 — 새 예약에 걸쳐 있는 배치는 사용자가 수신한 순서와 무관한 VSID 순으로 배치되므로, 비트맵을 정렬 순서로 스캔하면 사용자가 받은 순서와 다른 페이지 시퀀스가 나온다.
섹터 테이블은 멤버십은 알지만 순서는 모른다. 사용자 페이지 테이블은 이 잃어버린 순서를 VPID의 추가 전용(append-only) 목록 — 사용자 페이지 한 항목, 할당 순서대로 — 으로 재외부화하여, find_nth(n)이 그 안에서 위치 인덱스가 되도록 한다.
테이블은 FILE_EXTENSIBLE_DATA 컴포넌트의 체인(file_manager.c 전반에 쓰이는 extdata 프리미티브)으로 구성되며, 각 항목은 bare VPID다. 헤더는 O(1) 추가를 위해 vpid_last_user_page_ftab에 마지막 컴포넌트를 캐시하고, FILE_HEADER_GET_USER_PAGE_FTAB은 헤더 페이지 내의 첫 번째 컴포넌트를 가리킨다.
10.2 find-nth 컨텍스트와 헤더의 순서 유지 필드들
섹션 제목: “10.2 find-nth 컨텍스트와 헤더의 순서 유지 필드들”file_find_nth_context (struct { VPID *vpid_nth; int nth; int first_index; })는 스캔 콜백에 걸쳐 전달되는 누산기다:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
vpid_nth | 찾은 VPID를 담는 출력 파라미터 포인터 | 스캔이 호출자 슬롯에 직접 기록하도록 |
nth | 남은 인덱스, 컴포넌트·항목을 건너뛸 때마다 감소 | 카운트다운; 대상 항목에 도달하면 스캔 중단 |
first_index | 현재 컴포넌트 entry 0의 전역 절대 인덱스 | 캐시를 위한 값: 착지 컴포넌트가 전역 시퀀스 어디서 시작하는지 |
다섯 개의 FILE_HEADER 필드가 순서 기구를 담당한다(구조체는 1장 참고):
| 필드 | 역할 | 존재 이유 |
|---|---|---|
vpid_last_user_page_ftab | 마지막 UPT 컴포넌트 페이지 힌트 | O(1) 추가 대상; 테이블이 헤더 내에 있을 때는 헤더 VPID와 동일 |
vpid_find_nth_last | 마지막 find_nth 착지 페이지 캐시 | 연속적인 find_nth(n), find_nth(n+1)...이 테이블 중간에서 재개되도록 |
first_index_find_nth_last | vpid_find_nth_last의 entry 0에 해당하는 전역 인덱스 | 캐시된 페이지를 다음 탐색을 위한 절대 오프셋으로 변환 |
n_page_user | 전체 사용자 페이지 수(mark-delete 포함) | 유효 페이지 수의 분자 |
n_page_mark_delete | mark-delete 비트가 설정된 항목 수 | 보정 항: 유효 페이지 = n_page_user - n_page_mark_delete |
불변 조건 (유효 카운트 보정). 탐색 가능한 페이지 수는
n_page_user - n_page_mark_delete이며,n_page_user가 아니다.file_numerable_find_nth는 자동 할당 테스트와 marked 항목 건너뛰기 양쪽에서 이를 강제한다. 이 값이 틀어지면find_nth가 삭제된 페이지를 반환하거나 잘못된 인덱스에 할당하게 된다.file_header_update_mark_deleted가 set/clear마다+1/-1을 로그에 남겨 정확성을 유지한다.
불변 조건 (캐시 유효성).
FILE_CACHE_LAST_FIND_NTH는 비병렬 스레드의FILE_TEMPnumerable 파일에 대해서만 true이므로, 캐시는 write latch나 dirty 플래그 없이 읽고 쓸 수 있다. 할당 해제가 발생하면 캐시가 초기화되고(VPID_SET_NULL (&fhead->vpid_find_nth_last)), 추가는 꼬리만 연장하므로 캐시가 그대로 유효하다.
10.3 file_numerable_add_page — 모든 할당 시점의 추가
섹션 제목: “10.3 file_numerable_add_page — 모든 할당 시점의 추가”FILE_IS_NUMERABLE (fhead)인 경우, file_alloc은 페이지 비트를 설정한 직후 file_numerable_add_page를 호출하여 UPT를 할당과 동기화한다. 이 함수는 vpid_last_user_page_ftab으로 꼬리를 찾아(헤더 VPID와 같으면 in-header, 아니면 pgbuf_fix WRITE), 꼬리가 꽉 찼을 때 컴포넌트를 체이닝한 뒤 추가한다:
// file_numerable_add_page -- src/storage/file_manager.cif (VPID_EQ (&fhead->vpid_last_user_page_ftab, &vpid_fhead)) FILE_HEADER_GET_USER_PAGE_FTAB (fhead, extdata_user_page_ftab); /* tail in header */else page_ftab = pgbuf_fix (..., OLD_PAGE, PGBUF_LATCH_WRITE, ...); /* else fix tail page */// ... condensed: if (file_extdata_is_full) chain via file_temp_alloc/file_perm_alloc ...file_extdata_append (extdata_user_page_ftab, vpid); /* <- the append */flowchart TD
A["힌트 = vpid_last_user_page_ftab"] --> B{"힌트 == 헤더 VPID?"}
B -->|yes| C["extdata = in-header UPT"]
B -->|no| D{"pgbuf_fix WRITE 성공?"}
D -->|no| Z["ASSERT_ERROR_AND_SET, goto exit"]
D -->|yes| F["extdata = 해당 ftab 페이지"]
C --> G{"file_extdata_is_full?"}
F --> G
G -->|no| M["file_extdata_append vpid"]
G -->|yes| H{"FILE_IS_TEMPORARY?"}
H -->|yes| I["file_temp_alloc TABLE_PAGE"]
H -->|no| J["file_perm_alloc TABLE_PAGE"]
I --> K["fix NEW_PAGE, prev->next 링크,\n extdata 초기화,\n last_user_page_ftab 갱신"]
J --> K
K --> M
M --> N{"임시 파일?"}
N -->|no| O["file_log_extdata_add WAL"]
N -->|yes| P["pgbuf_set_dirty만 수행"]
O --> Q["exit: page_ftab 보유 시 unfix"]
P --> Q
Z --> Q
Figure 10-1. file_numerable_add_page, 모든 분기.
다시 짚고 넘어갈 분기는 임시·영구 파일 간 로깅 비대칭이다(Figure 10-1 노드 N): 영구 파일의 추가는 file_log_extdata_add WAL을 생성하고(컴포넌트 체이닝 시 RVFL_FHEAD_SET_LAST_USER_PAGE_FTAB undoredo 추가), 임시 파일은 페이지를 dirty로만 표시한다. 끝의 assert (!file_extdata_is_full (...)) 단언이 오버플로를 차단한다.
10.4 file_numerable_find_nth — 인덱스 기반 조회
섹션 제목: “10.4 file_numerable_find_nth — 인덱스 기반 조회”이 함수는 헤더를 READ로 fix하고 numerable을 단언한 뒤, 세 갈래로 분기한다. 끝에서 자동 할당(auto_alloc && nth == fhead->n_page_user - fhead->n_page_mark_delete)인 경우 latch를 승격하고 file_alloc으로 파일을 확장하며, ER_PAGE_LATCH_PROMOTE_FAIL 시에는 WRITE로 재fix한 뒤 재검사한다. 그 외에는 n_page_mark_delete를 기준으로 분기한다: 구멍이 있으면 모든 항목을 방문하고(file_extdata_find_nth_vpid_and_skip_marked), 구멍이 없으면 컴포넌트를 스트라이드하면서 아래 조건이 성립하면 캐시에서 재개한다:
// file_numerable_find_nth (no-holes branch) -- src/storage/file_manager.cif (FILE_CACHE_LAST_FIND_NTH (fhead, thread_p) && !VPID_ISNULL (&fhead->vpid_find_nth_last) && !VPID_EQ (&vpid_fhead, &fhead->vpid_find_nth_last) && nth >= fhead->first_index_find_nth_last) { find_nth_context.first_index = fhead->first_index_find_nth_last; /* resume from cache */ find_nth_context.nth -= fhead->first_index_find_nth_last; } /* <- rebase the countdown */flowchart TD
A["헤더 READ fix, numerable 단언"] --> B{"auto_alloc이고 nth == 유효 페이지 수?"}
B -->|yes| C{"latch 승격 성공?"}
C -->|FAIL| E["WRITE로 재fix, 재검사, file_alloc, exit"]
C -->|ok| F["file_alloc, exit"]
B -->|no| G{"n_page_mark_delete > 0?"}
G -->|yes| H["모든 항목에 skip-marked 적용"]
G -->|no| I{"캐시 사용 가능?"}
I -->|yes| J["캐시 페이지 fix, nth 리베이스"]
I -->|no| K["first_index = 0, 헤더부터"]
J --> L["find_nth_vpid: 컴포넌트 스트라이드"]
K --> L
L --> M{"캐시 저장 조건 충족?"}
M -->|yes| N["착지 페이지 + first_index 저장"]
M -->|no| O["캐시 갱신 생략"]
H --> P{"vpid_nth가 여전히 NULL?"}
N --> P
O --> P
P -->|yes| Q["assert_release false, ER_FAILED"]
P -->|no| R["exit: 페이지 unfix"]
Figure 10-2. file_numerable_find_nth, 모든 분기.
위 세 조건(특히 역방향 재개를 금지하는 nth >= first_index_find_nth_last)은 검색이 테이블 중간에서 시작해 착지 컴포넌트만 걸어가도록 보장한다 — find_nth(0), find_nth(1), ... 순서로 반복하는 run-merge 패턴의 상각 O(1)이 이로부터 나온다. 종료 정리 코드는 별칭(alias)된 페이지 포인터를 이중으로 unfix하지 않도록 처리한다.
10.5 두 가지 스캔 콜백
섹션 제목: “10.5 두 가지 스캔 콜백”file_extdata_apply_funcs는 컴포넌트별 및/또는 항목별 함수를 호출한다. file_extdata_find_nth_vpid는 컴포넌트별(구멍 없음) 콜백으로, 컴포넌트 전체가 O(1) 스트라이드 하나다:
// file_extdata_find_nth_vpid -- src/storage/file_manager.cint count_vpid = file_extdata_item_count (extdata);if (count_vpid <= find_nth_context->nth) { find_nth_context->nth -= count_vpid; /* <- skip whole component */ find_nth_context->first_index += count_vpid; } /* <- keep global index accurate */else { VPID_COPY (find_nth_context->vpid_nth, (VPID *) file_extdata_at (extdata, find_nth_context->nth)); assert (!FILE_USER_PAGE_IS_MARKED_DELETED (find_nth_context->vpid_nth)); /* <- no holes */ *stop = true; }file_extdata_find_nth_vpid_and_skip_marked는 항목별(구멍 있음) 콜백으로, 삭제된 항목이 슬롯은 차지하되 인덱스는 소비하지 않으므로 모든 VPID를 검사한다:
// file_extdata_find_nth_vpid_and_skip_marked -- src/storage/file_manager.cif (FILE_USER_PAGE_IS_MARKED_DELETED (vpidp)) return NO_ERROR; /* <- skip, do not advance nth */if (find_nth_context->nth == 0) { *find_nth_context->vpid_nth = *vpidp; *stop = true; }else find_nth_context->nth--;이 비대칭이 핵심이다: 구멍이 없으면 컴포넌트를 스트라이드하고 first_index를 유지해 캐시를 초기화할 수 있지만, 구멍이 있으면 그럴 수 없다 — 컴포넌트의 유효 항목 수가 전체 항목 수와 다르기 때문이다.
10.6 mark-delete 기구 (영구 numerable 파일)
섹션 제목: “10.6 mark-delete 기구 (영구 numerable 파일)”numerable 파일에서는 트랜잭션 도중 UPT 중간에서 페이지가 사라질 수 없다 — 그렇게 되면 이후 페이지들의 번호가 바뀌어 동시 find_nth가 깨진다. 따라서 file_dealloc은 두 단계로 제거한다. 1단계는 트랜잭션 내에서, pageid의 최상위 비트만 설정한다(FILE_USER_PAGE_MARK_DELETE_FLAG == 0x80000000, FILE_USER_PAGE_MARK_DELETED (vpid_found) 매크로 사용). 영구 파일에 한해 RVFL_USER_PAGE_MARK_DELETE undoredo를 로그에 남기고, file_header_update_mark_deleted (..., 1)로 카운터를 올리며, FILE_CACHE_LAST_FIND_NTH이면 캐시를 초기화한다. 슬롯은 그대로 유지되고 이후 인덱스는 흔들리지 않으며, find_nth는 항목별 콜백에서 이 항목을 건너뛴다.
2단계는 커밋 run-postpone 시점에, file_extdata_find_and_remove_item으로 항목을 물리적으로 제거한다: UPT가 추가 순서로 정렬되어 있어 VSID 순이 아니므로 체인을 선형으로(ordered=false) 걸으면서, file_extdata_remove_at(로깅: file_log_extdata_remove)으로 항목을 삭제하고 VPID를 출력 파라미터로 꺼낸 뒤, 비어진 컴포넌트를 이전 컴포넌트와 병합하고 해제된 테이블 페이지를 vpid_merged로 보고한다. 시스템 op가 활성화되어 있어야 한다고 단언하며, 항목이 없으면 assert_release(false)를 실행한다. 표시된 항목을 꺼낼 때 카운터를 다시 내린다:
// file_dealloc run-postpone body -- src/storage/file_manager.cfile_extdata_find_and_remove_item (..., vpid_dealloc, file_compare_vpids, false, &vpid_removed, &vpid_merged);if (!VPID_ISNULL (&vpid_merged)) /* table page emptied -> free it */ file_perm_dealloc (thread_p, page_fhead, &vpid_merged, FILE_ALLOC_TABLE_PAGE);if (FILE_USER_PAGE_IS_MARKED_DELETED (&vpid_removed)) file_header_update_mark_deleted (thread_p, page_fhead, -1); /* <- counter back down */롤백 시에는 file_rv_user_page_unmark_delete_logical이 1단계를 취소한다. 동시 트랜잭션이 테이블을 이동시켰을 수 있으므로 원래 위치를 신뢰할 수 없다 — VPID로 재탐색(file_extdata_search_item)하고, 비트가 설정되어 있음을 단언한 뒤, FILE_USER_PAGE_CLEAR_MARK_DELETED로 비트를 지우고, log_append_compensate로 RVFL_USER_PAGE_MARK_DELETE_COMPENSATE 레코드를 기록한다.
불변 조건 (삭제 중 슬롯 안정성). mark-delete된 항목은 커밋 전까지 이동하거나 인덱스가 바뀌지 않으므로, 동시 독자의 캐시된
vpid_find_nth_last는 mark 도중에도 구조적으로 유효하다(할당 해제는 슬롯이 아닌 캐시만 초기화한다). mark 시점에 압축하면 트랜잭션 도중 페이지 번호가 바뀐다.
10.7 file_numerable_truncate — 할당 해제 기반 축소
섹션 제목: “10.7 file_numerable_truncate — 할당 해제 기반 축소”truncation은 find_nth + file_dealloc에 기댄 유일한 공개 축소 경로다:
// file_numerable_truncate -- src/storage/file_manager.cif (!FILE_IS_NUMERABLE (fhead)) { assert_release (false); error_code = ER_FAILED; goto exit; }if (fhead->n_page_mark_delete != 0) { assert (false); return NO_ERROR; } /* <- refuse mid-dealloc */while (fhead->n_page_user > npages) { /* repeatedly drop index npages */ file_numerable_find_nth (thread_p, vfid, npages, false, NULL, NULL, &vpid); /* auto-alloc off */ file_dealloc (thread_p, vfid, &vpid, fhead->type); }반복마다 현재 인덱스 npages에 있는 페이지를 할당 해제한다. n_page_user가 줄어들면서 루프는 정확히 npages에서 끝난다. n_page_mark_delete != 0이면 진행을 거부하는데, 절반만 완료된 할당 해제가 있으면 인덱스가 무의미해지기 때문이다.
10.8 실제 호출자와 데드 코드 발견
섹션 제목: “10.8 실제 호출자와 데드 코드 발견”file_numerable_find_nth는 두 파일 타입 계열에 걸쳐 세 곳에서 호출된다. mark-delete는 영구 파일 계열에서만 실제로 동작한다. 확장 해시(extendible-hash) 계열은 src/storage/extendible_hash.c와 src/query/query_hash_scan.c의 파일 해시 스캔 코드 모두에서 소비된다 — fhs_fix_nth_page가 file_numerable_find_nth를 직접 호출하며, 파일은 file_create_ehash / file_create_ehash_dir로 생성되므로 FILE_EXTENDIBLE_HASH(_DIRECTORY) 타입, 즉 스토리지 행과 동일한 계열이다.
| 호출자 | 파일 타입 | 할당 해제 여부 | mark-delete 사용 여부 |
|---|---|---|---|
외부 정렬 런 파일 (external_sort.c, file_create_temp_numerable) | FILE_TEMP | 없음 | 없음 (데드 코드) |
확장 해시 버킷/디렉터리 (extendible_hash.c find_nth, truncate) | FILE_EXTENDIBLE_HASH(_DIRECTORY) | 있음 | 있음 |
파일 해시 스캔 FHS (query_hash_scan.c, fhs_fix_nth_page) | FILE_EXTENDIBLE_HASH(_DIRECTORY) | truncate 경로에서 | 있음 |
numerable이 아닌 임시 파일 소비자 — list_file 쿼리 중간 결과와 쿼리 결과 캐시(FILE_QUERY_AREA) — 는 이 계층을 건드리지 않는다. 이들은 QFILE_PAGE_HEADER.next_vpid로 페이지를 체이닝하며 find_nth 계약이 없다.
핵심 발견.
FILE_TEMPnumerable 파일(외부 정렬)의 경우file_temp_alloc은 절대 할당 해제하지 않으므로,FILE_USER_PAGE_MARK_DELETED,n_page_mark_delete, 그리고RVFL_USER_PAGE_MARK_DELETE*체인 전체가 사실상 데드 코드다:n_page_mark_delete는 항상 0이고find_nth는 항상 구멍 없음/캐시 경로를 탄다. 테이블 자료구조 자체는 여전히 필수 — 정렬 merge가 의존하는 순서 계약을 공급한다. 데드인 부분은 삭제 보조 장치이지, 테이블 자체가 아니다.
10.9 챕터 요약 — 핵심 정리
섹션 제목: “10.9 챕터 요약 — 핵심 정리”- VSID 정렬 섹터 테이블은 파일이 소유한 페이지는 알지만 순서를 버린다. 사용자 페이지 테이블은 이 순서를 추가 전용 VPID 목록으로 재외부화하여
find_nth(n)이 위치 인덱스가 되도록 한다. file_numerable_add_page는file_alloc내에서 할당마다 VPID 하나를 추가하며,vpid_last_user_page_ftab을 O(1) 꼬리 힌트로 사용하고 꼬리가 꽉 차면 컴포넌트를 체이닝한다(영구 파일은 로깅, 임시 파일은 dirty 표시만).file_numerable_find_nth는 구멍 없음 경로에서만 O(1) 상각이다(file_extdata_find_nth_vpid가 컴포넌트를 스트라이드하고 캐시가 테이블 중간에서 재개한다). 구멍 있음 경로는 항목별 skip 스캔으로 퇴화한다.- 유효 페이지 수는
n_page_user - n_page_mark_delete이며, 자동 할당과 삭제 항목 건너뛰기를 모두 제어하고 로그된 델타로 정확히 유지된다. - 영구 파일의 삭제는 두 단계다: 1단계에서
FILE_USER_PAGE_MARK_DELETE_FLAG를 설정하고 카운터를 올리며 슬롯을 유지하고, 2단계에서 run-postpone 시점에file_extdata_find_and_remove_item으로 제거한다. 롤백은 VPID로 재탐색하여 비트를 지운다. file_numerable_truncate는find_nth(npages)+file_dealloc루프의 얇은 래퍼로,n_page_mark_delete != 0인 동안에는 실행을 거부한다.FILE_TEMPnumerable 파일(외부 정렬)에서 mark-delete는 데드 코드지만, 테이블 자체는 여전히 필수다 — 데드인 부분은 삭제 보조 장치이지 구조 자체가 아니다.
Chapter 11: 특수 경로 — Tempcache, Tracker, Sticky Page, TDE, 복구 처리기
섹션 제목: “Chapter 11: 특수 경로 — Tempcache, Tracker, Sticky Page, TDE, 복구 처리기”6~10장의 단일 페이지 생명주기 옆에는 다섯 가지 보조 기계가 함께 존재한다: 임시 파일 캐시(temp-file cache), File Tracker, sticky-first-page 이스케이프 해치, TDE 플래그, 그리고 복구 처리기. 이 장은 코드만을 해부한다. 왜 이런 설계인지에 대해서는 컴패니언 문서의 “Temporary file cache”, “File destruction and the File Tracker”, “Two-step sector reservation” 절을 참조하라.
11.1 임시 파일 캐시: 파일 전체를 재활용하기
섹션 제목: “11.1 임시 파일 캐시: 파일 전체를 재활용하기”file_Tempcache는 퇴역한 임시 파일을 통째로 보관하는 전역 풀(pool)이다. 다음 요청이 같은 모양의 파일을 요구하면, 새로 파괴-생성하는 대신 캐시에서 하나를 꺼내 돌려준다. 세 개의 구조체가 협력한다.
// file_tempcache_entry -- src/storage/file_manager.cstruct file_tempcache_entry { VFID vfid; FILE_TYPE ftype; FILE_TEMPCACHE_ENTRY *next; };
// file_tempcache_tran_entry -- src/storage/file_manager.cstruct file_tempcache_tran_entry { pthread_mutex_t mutex; FILE_TEMPCACHE_ENTRY *head;#if !defined (NDEBUG) int owner_mutex;#endif};
// file_tempcache -- src/storage/file_manager.cstruct file_tempcache { FILE_TEMPCACHE_ENTRY *free_entries; int nfree_entries_max, nfree_entries; FILE_TEMPCACHE_ENTRY *cached_not_numerable, *cached_numerable; int ncached_max, ncached_not_numerable, ncached_numerable; pthread_mutex_t mutex;#if !defined (NDEBUG) int owner_mutex;#endif FILE_TEMPCACHE_TRAN_ENTRY *tran_files; SPACEDB_FILES spacedb_temp;};static FILE_TEMPCACHE file_Tempcache;file_tempcache_entry
| 필드 | 역할 | 존재 이유 |
|---|---|---|
vfid | 캐시된 파일을 식별 | 캐시는 디스크립터가 아닌 실제로 할당된 파일을 저장한다 |
ftype | 캐시된 파일의 타입 | get은 타입으로 매칭하며, 타입이 맞지 않으면 그 자리에서 재설정한다 |
next | 연결 리스트 링크 | 하나의 엔트리는 free_entries, 트랜잭션 리스트, 캐시 리스트 사이를 이동하되 동시에 두 곳에 있을 수 없다 |
file_tempcache_tran_entry (트랜잭션 인덱스별로 하나씩)
| 필드 | 역할 | 존재 이유 |
|---|---|---|
mutex | 이 트랜잭션의 head에 대한 트랜잭션별 락 | commit/abort 드레인 과정에서 file_tempcache_lock_tran_entry / unlock_tran_entry가 이를 잡는다 |
head | 이 트랜잭션이 생성하고 아직 소유하는 파일들 | commit/abort 시 file_tempcache_drop_tran_temp_files가 드레인한다 |
owner_mutex | NDEBUG 전용 소유권 추적기 | lock/unlock 단언에서 어느 스레드가 mutex를 보유하는지 기록한다 |
file_tempcache (전역)
| 필드 | 역할 | 존재 이유 |
|---|---|---|
free_entries | 빈 엔트리 껍데기 풀 | 캐시 연산마다 malloc/free하는 비용을 피한다 |
nfree_entries_max / nfree_entries | 껍데기 풀의 상한 / 현재 크기 | init에서 max를 ntrans * 8로 설정한다 |
cached_not_numerable | 퇴역한 일반 임시 파일 | get(numerable=false) 호출 시 이 리스트에서 꺼낸다 |
cached_numerable | 퇴역한 numerable 임시 파일 | 사용자 페이지 테이블이 다르므로(10장) 별도 리스트로 관리한다 |
ncached_max | 전체 용량 (PRM_ID_MAX_ENTRIES_IN_TEMP_FILE_CACHE) | not_numerable + numerable >= max이면 put이 거부한다 |
ncached_not_numerable / ncached_numerable | 리스트별 카운트 | 리스트와 항상 동기화된다(아래 불변 조건 참조) |
mutex | 전역 리스트와 껍데기 풀을 보호 | 모든 전역 상태를 하나의 락으로 직렬화한다 |
owner_mutex | NDEBUG 전용 소유권 추적기 | file_tempcache_lock / unlock 단언에서 어느 스레드가 mutex를 잡았는지 기록한다 |
tran_files | 트랜잭션별 리스트 배열 | 트랜잭션 인덱스로 직접 접근하므로 commit이 O(1)이다 |
spacedb_temp | 임시 공간 회계 | SPACEDB 리포팅에 제공된다 |
불변 조건 — 리스트 헤드와 카운트는 항상 일치한다.
(cached_not_numerable == NULL) == (ncached_not_numerable == 0)이 성립하며, numerable에 대해서도 마찬가지다.put은 연결하기 전에 두 조건을 모두 단언한다. 카운트가 틀어지면get은 언더플로가 발생하고(ncached_* > 0단언 실패),put은ncached_max를 초과하여 임시 파일을 누수시킨다.
11.1.1 file_tempcache_get — 재활용 파일 또는 새 껍데기 반환
섹션 제목: “11.1.1 file_tempcache_get — 재활용 파일 또는 새 껍데기 반환”// file_tempcache_get -- src/storage/file_manager.c*entry = numerable ? file_Tempcache.cached_numerable : file_Tempcache.cached_not_numerable;if (*entry != NULL && (*entry)->ftype != ftype) { /* cached file is wrong type */ error_code = file_temp_set_type (thread_p, &(*entry)->vfid, ftype); if (error_code != NO_ERROR) *entry = NULL; /* <- re-type failed: fall to miss */ else (*entry)->ftype = ftype;}if (*entry != NULL) { /* hit: unlink, decrement the matching ncached_* */ ... }else { error_code = file_tempcache_alloc_entry (entry); /* miss: bare shell, VFID_SET_NULL */ }다섯 가지 분기가 있다: 히트/타입-일치는 꺼내고 카운트를 줄인다; 히트/재설정-성공은 ftype을 패치한 뒤 꺼낸다; 히트/재설정-실패는 *entry를 null로 만들어 미스 경로로 떨어뜨린다; 미스는 껍데기를 할당한다(vfid == NULL); 껍데기 할당 실패는 에러를 전파한다. 히트는 이미 할당된 파일을 가리키고, 미스는 호출자가 생성에 사용할 껍데기를 반환한다.
11.1.2 file_tempcache_put — 파일을 다시 받아들이거나 거부하기
섹션 제목: “11.1.2 file_tempcache_put — 파일을 다시 받아들이거나 거부하기”// file_tempcache_put -- src/storage/file_manager.cif (file_header_copy (...&entry->vfid, &fhead) != NO_ERROR || fhead.n_page_user > prm_get_integer_value (PRM_ID_MAX_PAGES_IN_TEMP_FILE_CACHE)) return false; /* <- too big / unreadable: no lock taken yet */file_tempcache_lock ();if (ncached_not_numerable + ncached_numerable < ncached_max) { if (file_temp_reset_user_pages (thread_p, &entry->vfid) != NO_ERROR) { file_tempcache_unlock (); return false; } /* <- reset failed: cannot reuse */ /* push onto cached_numerable / cached_not_numerable per FILE_IS_NUMERABLE(&fhead) */ file_tempcache_unlock (); return true;}file_tempcache_unlock (); return false; /* cache full */파일을 보존하는 종료 경로는 하나뿐이다: 헤더 복사 실패 또는 너무 큰 경우(false, 락 획득 전), 캐시 만원(false), 리셋 실패(false), 그리고 모두 통과한 경우(실제 헤더의 FILE_IS_NUMERABLE에 따라 리스트에 push, true). false를 반환받으면 호출자는 파일을 파괴해야 한다.
11.1.3 Commit/abort 드레인 — file_tempcache_drop_tran_temp_files
섹션 제목: “11.1.3 Commit/abort 드레인 — file_tempcache_drop_tran_temp_files”// file_tempcache_drop_tran_temp_files -- src/storage/file_manager.cint tran_index = file_get_tempcache_entry_index (thread_p);file_tempcache_lock_tran_entry (&file_Tempcache.tran_files[tran_index]);if (file_Tempcache.tran_files[tran_index].head != NULL) file_tempcache_cache_or_drop_entries (thread_p, &file_Tempcache.tran_files[tran_index].head);file_tempcache_unlock_tran_entry (&file_Tempcache.tran_files[tran_index]);file_tempcache_cache_or_drop_entries는 head를 순회하면서, 각 엔트리마다 file_tempcache_put을 호출하고, false가 반환되면 file_destroy(..., true)(중간에 누수가 없도록 인터럽트를 억제)를 호출한 다음 file_tempcache_retire_entry를 호출한다. 순회가 끝나면 리스트는 비어 있다. tran_files는 ntrans 크기이며, 서버 모드에서 ntrans = logtb_get_number_of_total_tran_indices () + 1(인덱스 0을 예약하는 +1)이고, SA 모드에서는 1이다 — 배열은 ntrans 크기이지, ntrans + 1이 아니다.
11.1.4 쿼리 매니저 소유 파일 — file_temp_preserve / file_temp_retire_preserved
섹션 제목: “11.1.4 쿼리 매니저 소유 파일 — file_temp_preserve / file_temp_retire_preserved”요청보다는 오래 살아야 하되 세션보다는 오래 살아서는 안 되는 임시 파일은 트랜잭션 리스트에 남아 있을 수 없다. 그렇지 않으면 다음 commit 시 회수된다. file_temp_preserve가 이를 제거한다:
// file_temp_preserve -- src/storage/file_manager.centry = file_tempcache_pop_tran_file (thread_p, vfid);if (entry == NULL) assert_release (false); /* must have been on the list */else file_tempcache_retire_entry (entry); /* return the shell; file is now untracked */소유자는 작업이 끝나면 file_temp_retire_preserved = file_temp_retire_internal(..., /*was_preserved=*/true)를 호출한다. was_preserved 플래그는 엔트리를 얻는 방식을 바꾼다: preserved 파일은 어느 리스트에도 없으므로 retire는 인자로 받은 vfid로 새 껍데기를 할당한다. 반면 non-preserved retire는 기존 엔트리를 꺼낸다. 두 경로 모두 file_tempcache_put으로 합류하며, false이면 file_destroy(..., true)를 호출한다.
불변 조건 — 임시 파일은 정확히 하나의 추적 리스트에 존재한다: 해당 트랜잭션의
head, OR preserved 상태(어느 리스트에도 없음), OR 전역 캐시 리스트, OR 파괴됨.file_temp_preserve는 *꺼내기(popping)*로 이 핸드오프를 강제한다. 꺼내지 않으면 commit 드레인과 쿼리 매니저 retire가 모두 파일을 제거하여 이중 해제가 발생한다.
11.2 File Tracker: 영구 파일 카탈로그
섹션 제목: “11.2 File Tracker: 영구 파일 카탈로그”File Tracker는 데이터베이스당 하나의 영구 파일로, 본문은 FILE_TRACK_ITEM 레코드의 단일 FILE_EXTENSIBLE_DATA 체인이다 — 영구 파일 하나당 레코드 하나. 부팅 시 boot_Db_parm->trk_vfid에서 시드되는 두 전역 변수로 접근한다: file_Tracker_vfid(VFID)와 file_Tracker_vpid(sticky 첫 번째 페이지, 11.3절). boot_sr.c는 생성 시 file_tracker_create를, 재시작마다 file_tracker_load를 호출한다.
// file_track_metadata / file_track_item -- src/storage/file_manager.cunion file_track_metadata { /* 8 bytes, role depends on item->type */ FILE_TRACK_HEAP_METADATA heap; /* { bool is_marked_deleted; bool dummy[7]; } */ INT64 metadata_size_tracker; /* forces the union to exactly 8 bytes */};struct file_track_item { INT32 fileid; INT16 volid; INT16 type; /* type is a FILE_TYPE cast to INT16 */ FILE_TRACK_METADATA metadata; /* total 16 bytes */};file_track_item — 검색 키는 (volid, fileid):
| 필드 | 역할 | 존재 이유 |
|---|---|---|
fileid | VFID의 하위 4바이트 | volid와 함께 파일을 고유하게 식별한다 |
volid | 파일이 있는 볼륨 | file_compare_track_items로 정렬하여 이진 탐색을 지원한다 |
type | 16비트로 캐스팅된 FILE_TYPE | file_tracker_map이 각 헤더를 고정하지 않고 타입별 필터링을 할 수 있다 |
metadata | 타입별 부가 데이터 | heap에서만 의미가 있으며, 그 외에는 0이다 |
file_track_metadata — union이므로 type에 따라 의미가 달라지는 역할 매트릭스:
item->type | 활성 멤버 | 의미 |
|---|---|---|
FILE_HEAP / FILE_HEAP_REUSE_SLOTS | heap.is_marked_deleted | heap이 논리적으로 삭제되었지만 재사용을 위해 유지됨(file_tracker_item_reuse_heap) |
| 그 외 모든 타입 | metadata_size_tracker | 미사용; file_tracker_register에서 metadata == NULL일 때 0으로 기록 |
불변 조건 — 아이템은 체인 전체에 걸쳐
file_compare_track_items기준으로 정렬되어 있다;register는 이진 탐색 위치에 삽입한다.unregister(file_extdata_find_and_remove_item)와 반복자의 커서 재개 로직 모두 이 순서에 의존한다 — 순서가 어긋난 삽입이 발생하면 나중에 실재하는 파일을 조회할 때 조용히 놓친다.
flowchart LR parm["boot_Db_parm->trk_vfid"] --> vfid["file_Tracker_vfid"] parm --> sticky["sticky 첫 번째 페이지"] sticky --> vpid["file_Tracker_vpid"] vpid --> head["FILE_EXTENSIBLE_DATA (헤드 페이지)"] head -->|vpid_next| more["FILE_EXTENSIBLE_DATA (추가 페이지)"] head --> items["FILE_TRACK_ITEM[] (volid,fileid,type,metadata)"]
Figure 11-1. 부팅 파라미터에서 Tracker 전역 변수를 거쳐 extensible-data 아이템 체인까지.
11.2.1 file_tracker_register — 영구 생성 시 아이템 추가
섹션 제목: “11.2.1 file_tracker_register — 영구 생성 시 아이템 추가”모든 영구 파일 생성 시 file_create(6장)에서 호출되며, 시작된 시스템 op 하에서 실행된다.
// file_tracker_register -- src/storage/file_manager.cassert (log_check_system_op_is_started (thread_p));item.volid = vfid->volid; item.fileid = vfid->fileid; item.type = (INT16) ftype;if (metadata == NULL) item.metadata.metadata_size_tracker = 0; /* zero-fill */else item.metadata = *metadata;page_track_head = pgbuf_fix (..., &file_Tracker_vpid, OLD_PAGE, PGBUF_LATCH_WRITE, ...);if (page_track_head == NULL) { ASSERT_ERROR_AND_SET (error_code); return error_code; }error_code = file_tracker_register_internal (thread_p, page_track_head, &item);실제 배치 로직은 file_tracker_register_internal에 있다: 여유가 있는 페이지를 찾고(file_extdata_find_not_full), 없으면 새 tracker 페이지를 할당하여(file_alloc(&file_Tracker_vfid, ...)) file_log_extdata_set_next로 연결한다; 이진 탐색으로 슬롯을 찾고; 중복이 없음을 단언(assert_release(false))한다; file_extdata_insert_at + file_log_extdata_add, 더티 마킹. 에러 종료와 중복 경로 모두 goto exit로 이동한다.
11.2.2 file_tracker_unregister — 영구 파괴 시 아이템 제거
섹션 제목: “11.2.2 file_tracker_unregister — 영구 파괴 시 아이템 제거”// file_tracker_unregister -- src/storage/file_manager.clog_sysop_start (thread_p); /* its own nested system op */item_inout.volid = vfid->volid; item_inout.fileid = vfid->fileid;error_code = file_extdata_find_and_remove_item (..., &item_inout, file_compare_track_items, true, &item_inout, &vpid_merged);if (error_code != NO_ERROR) goto exit; /* -> sysop_abort */if (!VPID_ISNULL (&vpid_merged)) /* removal emptied/merged a page */ error_code = file_dealloc (thread_p, &file_Tracker_vfid, &vpid_merged, FILE_TRACKER);exit: if (error_code != NO_ERROR) log_sysop_abort (thread_p); else log_sysop_end_logical_undo (thread_p, RVFL_TRACKER_UNREGISTER, NULL, sizeof (item_inout), &item_inout);분기를 정리하면: fix 실패는 조기 반환(sysop 없음); find-and-remove 실패 또는 merge 후 dealloc 실패는 모두 goto exit → abort; 성공은 논리적 undo로 종료한다. 물리적 undo가 아닌 논리적(logical) undo가 핵심이다 — 체인이 압축되면서 아이템이 페이지 사이를 이동하기 때문에, 물리적 undo는 잘못된 슬롯을 대상으로 삼게 된다. 대신 undo는 저장된 아이템으로 file_tracker_register_internal을 재실행한다(file_rv_tracker_unregister_undo).
11.2.3 file_tracker_map — 모든 파일 열거
섹션 제목: “11.2.3 file_tracker_map — 모든 파일 열거”// file_tracker_map -- src/storage/file_manager.cpage_track_head = pgbuf_fix (..., &file_Tracker_vpid, OLD_PAGE, latch_mode, ...);while (true) { /* walk the extdata chain */ for (index_item = 0; index_item < file_extdata_item_count (extdata); index_item++) { error_code = func (thread_p, page_extdata, extdata, index_item, &stop, args); if (error_code != NO_ERROR || stop) goto exit; /* error, or callback early-out */ } if (page_track_other != NULL) pgbuf_unfix_and_init (thread_p, page_track_other); if (VPID_ISNULL (&extdata->vpid_next)) break; /* end of chain */ page_track_other = pgbuf_fix (..., &extdata->vpid_next, OLD_PAGE, latch_mode, ...); if (page_track_other == NULL) goto exit; page_extdata = page_track_other;}map은 헤드 페이지를 고정하고 page_track_other 하나를 순환시키므로, 동시에 최대 두 개의 latch만 잡힌다. 이에 대응하는 file_tracker_interruptable_iterate는 커서(vfid)와 OID lock을 반환하여 긴 스캔이 tracker를 고정하지 않고도 중단과 재개가 가능하도록 한다 — FILE_GET_TRACKER_LOCK_MODE 매크로는 B-tree에 IX_LOCK을, 그 외에는 SCH_S_LOCK을 선택한다.
11.3 Sticky 첫 번째 페이지
섹션 제목: “11.3 Sticky 첫 번째 페이지”일부 파일은 첫 번째 사용자 페이지를 영원히 고정된 VPID에 유지해야 한다 — tracker 자신과 boot HFID 힙이 그 예다. file_alloc_sticky_first_page는 페이지 #1을 할당하고 이를 기록한다.
// file_alloc_sticky_first_page -- src/storage/file_manager.cassert (fhead->n_page_user == 0 && VPID_ISNULL (&fhead->vpid_sticky_first)); /* brand-new file */error_code = file_alloc (thread_p, vfid, f_init, f_init_args, vpid_out, page_out);if (error_code != NO_ERROR) goto exit;log_append_undoredo_data2 (thread_p, RVFL_FHEAD_STICKY_PAGE, NULL, page_fhead, 0, sizeof (VPID), sizeof (VPID), &fhead->vpid_sticky_first, vpid_out);fhead->vpid_sticky_first = *vpid_out; /* remember it */pgbuf_set_dirty (thread_p, page_fhead, DONT_FREE);일반적인 file_alloc에 헤더 쓰기 하나가 로깅된 형태다(file_rv_fhead_sticky_page로 복구). 핵심은 dealloc 시에 있다: file_dealloc과 그 헬퍼들은 assert (!VPID_EQ (&fhead->vpid_sticky_first, vpid))를 수행하여 sticky 페이지를 9장 생명주기에서 면제한다. 이것은 디버그 단언(NDEBUG에서는 컴파일 제외)이지 런타임 단락(short-circuit)이 아니다 — 호출자가 sticky VPID를 절대 전달하지 않는다는 계약에 의존한다. file_get_sticky_first_page는 이를 다시 읽어오며(assert_release(false) if NULL), file_tracker_load가 file_Tracker_vpid를 복구하는 방식이 바로 이것이다.
11.4 TDE 플래그 — 할당과 직교하는 관심사
섹션 제목: “11.4 TDE 플래그 — 할당과 직교하는 관심사”TDE는 fhead->file_flags의 상호 배타적인 두 비트다: FILE_FLAG_ENCRYPTED_AES (0x4)와 FILE_FLAG_ENCRYPTED_ARIA (0x8).
// file_set_tde_algorithm_internal -- src/storage/file_manager.cfhead->file_flags &= ~FILE_FLAG_ENCRYPTED_MASK; /* clear both bits first */switch (tde_algo) { case TDE_ALGORITHM_AES: fhead->file_flags |= FILE_FLAG_ENCRYPTED_AES; break; case TDE_ALGORITHM_ARIA: fhead->file_flags |= FILE_FLAG_ENCRYPTED_ARIA; break; case TDE_ALGORITHM_NONE: break; /* already cleared */}섹터 예약(4장)도 페이지 할당(7/8장)도 이 플래그를 참조하지 않는다. file_get_tde_algorithm_internal은 두 비트가 동시에 설정되지 않았음을 단언한 후 AES, ARIA, 또는 NONE을 반환한다. 암호화는 버퍼 레이어에서 페이지 단위로 적용된다. 할당 기계는 알고리즘에 무관하므로, 할당 코드를 수정하는 독자에게 TDE는 사실상 무시해도 되는 요소다.
11.5 공유 기본 요소 — file_extdata_apply_funcs
섹션 제목: “11.5 공유 기본 요소 — file_extdata_apply_funcs”모듈 내의 모든 테이블(tracker 아이템, 사용자 페이지 테이블, 섹터 테이블)은 FILE_EXTENSIBLE_DATA 체인이며, 거의 모든 순회는 이 범용 방문자를 거친다.
// file_extdata_apply_funcs -- src/storage/file_manager.cwhile (true) { if (f_extdata != NULL) { error_code = f_extdata (...); if (error_code || stop) goto exit; } /* per-page */ if (f_item != NULL) /* per-item */ for (i = 0; i < file_extdata_item_count (extdata_in); i++) { error_code = f_item (..., file_extdata_at (extdata_in, i), i, &stop, ...); if (error_code || stop) goto exit; } if (VPID_ISNULL (&extdata_in->vpid_next)) break; // ... unfix current, fix extdata_in->vpid_next, goto exit on NULL ...}exit: if (stop && page_out != NULL) *page_out = page_extdata; /* hand page back, latched */ else if (page_extdata != NULL) pgbuf_unfix (thread_p, page_extdata);선택적 콜백이 두 개(f_extdata는 페이지별, f_item은 아이템별) 있으며, 둘 중 하나가 stop이나 에러를 발생시킬 수 있다. 핵심은 종료 정책이다: stop이 발생하고 page_out이 있으면, 페이지는 latch된 채로 반환되어 탐색-후-수정이 그 자리에서 이루어질 수 있다; 그렇지 않으면 여기서 unfix한다. 이 구조가 file_extdata_search_item / _find_ordered(이진 탐색), _insert_at / _remove_at, file_extdata_merge의 기반이 된다. latch_mode는 for_write이면 WRITE, 아니면 READ다.
11.6 이 장의 복구 처리기와 미해결 질문
섹션 제목: “11.6 이 장의 복구 처리기와 미해결 질문”두 모듈 모두 RV* enum으로 인덱싱되는 undo/redo/dump 콜백을 등록한다. 이 장의 기계가 도입하는 처리기들:
| RV 인덱스 | 처리기 | 재실행 대상 |
|---|---|---|
RVFL_FHEAD_STICKY_PAGE | file_rv_fhead_sticky_page | sticky-first-page VPID (11.3절) |
RVFL_TRACKER_UNREGISTER | file_rv_tracker_unregister_undo | tracker 제거의 논리적 undo (11.2.2절) |
RVFL_SET_TDE_ALGORITHM | file_rv_set_tde_algorithm | TDE 플래그 변경 (11.4절) |
RVFL_EXTDATA_ADD/REMOVE/SET_NEXT/MERGE | file_rv_extdata_add / _remove / _set_next / _merge | 모든 extensible-data 편집 (11.5절) |
RVDK_FORMAT | disk_rv_undo_format, disk_rv_redo_format, disk_rv_dump_hdr | 볼륨 생성/포맷 — 아래의 미해결 질문 |
핵심 생명주기 처리기들 — 섹터 예약/해제, 볼륨 헤더 확장, 파일 헤더 alloc/dealloc, partial-sector 비트맵 편집, 지연된 dealloc, 파일 파괴 — 은 3~5장, 7장, 9장에 속한다. 해당 RVDK_* / RVFL_* 항목은 그 장들을 참조하라.
미해결 질문 —
disk_format도중 충돌 시 멱등성.disk_rv_redo_format은is_first_call플래그(rcv->offset == -1)를 가지며, 두 번의 호출 중 첫 번째에서는 disk-cache 업데이트를 건너뛴다. 이는 format 처리기가 충돌 전disk_format이 얼마나 진행되었는지에 대한 암묵적 가정을 인코딩하고 있음을 의미한다. 볼륨 파일 생성 / 캐시 등록 /volume_info기록이라는 각 단계의 모든 인터리빙이 — 특히 redo의 두 호출 사이에서 충돌하는 경우가 — 처리기만으로 증명 가능한지는 불분명하며, 검증 대상으로 남아 있다.
11.7 챕터 요약 — 핵심 정리
섹션 제목: “11.7 챕터 요약 — 핵심 정리”file_Tempcache는 임시 파일 전체를 재활용한다:get은 캐시 리스트에서 꺼내거나 껍데기를 할당하고,put은 리셋된 파일을 받아들이거나false를 반환하여 호출자가 파괴하게 한다.- 임시 파일은 정확히 하나의 추적 리스트에만 존재한다;
file_temp_preserve는 쿼리 매니저를 위해 트랜잭션 리스트에서 꺼내고,drop_tran_temp_files는 나머지를 commit/abort 시 드레인한다. - File Tracker는
(volid, fileid)순으로 정렬된FILE_TRACK_ITEM체인으로,trk_vfid→file_Tracker_vfid/file_Tracker_vpid로 접근한다;unregister는 아이템이 페이지 간에 이동하기 때문에 논리적 undo를 사용한다. - Sticky 첫 번째 페이지는 계약상 생명주기에서 면제된다 —
file_dealloc은 해당 페이지가vpid_sticky_first인지 *단언(assert)*만 할 뿐이며(디버그 빌드), 릴리즈 빌드는 호출자가 절대 그 VPID를 전달하지 않는다는 것에 의존한다. - TDE(상호 배타적인 AES/ARIA 비트가
file_flags에 있음)는 할당과 직교하며, 이 비트는 바이트가 디스크에 닿을 때만 읽힌다. file_extdata_apply_funcs는 모든 테이블 뒤에 있는 단일 엔진으로, 페이지별/아이템별 콜백과 함께 정지된 페이지를 latch된 채로 반환하는 종료 정책을 제공한다.- 복구는
RV*상수로 인덱싱된다; 증명되지 않은 코너 케이스는disk_format충돌 지점 전반에 걸친disk_rv_*_format멱등성이다.
이 리비전 시점의 위치 힌트
섹션 제목: “이 리비전 시점의 위치 힌트”아래 줄 번호는 2026-06-23 시점에 관찰한 값이다. 심볼 이름이 정식 앵커이며 줄 번호는 시간이 지나면 어긋나는 힌트다.
| Symbol | File | Line |
|---|---|---|
bit64_count_trailing_ones | src/base/bit.c | 515 |
PRM_ID_BOSR_MAXTMP_PAGES | src/base/system_parameter.c | 1246 |
DB_VOLPURPOSE | src/compat/dbtype_def.h | 196 |
DB_VOLTYPE | src/compat/dbtype_def.h | 203 |
VSID | src/compat/dbtype_def.h | 939 |
fhs_fix_nth_page | src/query/query_hash_scan.c | 1078 |
disk_volume_header | src/storage/disk_manager.c | 75 |
disk_cache_volinfo | src/storage/disk_manager.c | 155 |
disk_extend_info | src/storage/disk_manager.c | 162 |
disk_perm_info | src/storage/disk_manager.c | 180 |
disk_temp_info | src/storage/disk_manager.c | 186 |
nsect_perm_free | src/storage/disk_manager.c | 189 |
disk_cache | src/storage/disk_manager.c | 194 |
disk_Cache | src/storage/disk_manager.c | 209 |
disk_Temp_max_sects | src/storage/disk_manager.c | 211 |
DISK_STAB_UNIT | src/storage/disk_manager.c | 224 |
disk_stab_cursor | src/storage/disk_manager.c | 229 |
DISK_STAB_PAGE_BIT_COUNT | src/storage/disk_manager.c | 250 |
DISK_ALLOCTBL_SECTOR_PAGE_OFFSET | src/storage/disk_manager.c | 253 |
DISK_ALLOCTBL_SECTOR_UNIT_OFFSET | src/storage/disk_manager.c | 255 |
DISK_STAB_NPAGES | src/storage/disk_manager.c | 263 |
disk_cache_vol_reserve | src/storage/disk_manager.c | 273 |
DISK_PRERESERVE_BUF_DEFAULT | src/storage/disk_manager.c | 278 |
disk_reserve_context | src/storage/disk_manager.c | 281 |
DISK_MIN_VOLUME_SECTS | src/storage/disk_manager.c | 300 |
DISK_SYS_NSECT_SIZE | src/storage/disk_manager.c | 347 |
disk_format | src/storage/disk_manager.c | 512 |
disk_unformat | src/storage/disk_manager.c | 822 |
disk_rv_undo_format | src/storage/disk_manager.c | 1235 |
disk_rv_redo_format | src/storage/disk_manager.c | 1340 |
disk_extend | src/storage/disk_manager.c | 1633 |
disk_volume_expand | src/storage/disk_manager.c | 1904 |
disk_rv_volhead_extend_redo | src/storage/disk_manager.c | 2022 |
disk_rv_volhead_extend_undo | src/storage/disk_manager.c | 2081 |
disk_add_volume | src/storage/disk_manager.c | 2117 |
disk_add_volume_extension | src/storage/disk_manager.c | 2326 |
disk_volume_boot | src/storage/disk_manager.c | 2443 |
disk_cache_load_volume | src/storage/disk_manager.c | 2567 |
disk_cache_init | src/storage/disk_manager.c | 2627 |
disk_cache_final | src/storage/disk_manager.c | 2688 |
disk_cache_load_all_volumes | src/storage/disk_manager.c | 2714 |
disk_cache_free_reserved | src/storage/disk_manager.c | 2728 |
disk_cache_update_vol_free | src/storage/disk_manager.c | 2748 |
disk_lock_extend | src/storage/disk_manager.c | 2791 |
disk_unlock_extend | src/storage/disk_manager.c | 2817 |
disk_cache_lock_reserve_for_purpose | src/storage/disk_manager.c | 2837 |
disk_volume_header_set_stab | src/storage/disk_manager.c | 3166 |
disk_verify_volume_header | src/storage/disk_manager.c | 3179 |
disk_stab_cursor_set_at_sectid | src/storage/disk_manager.c | 3258 |
disk_stab_cursor_set_at_end | src/storage/disk_manager.c | 3284 |
disk_stab_cursor_set_at_start | src/storage/disk_manager.c | 3303 |
disk_stab_cursor_check_valid | src/storage/disk_manager.c | 3372 |
disk_stab_cursor_is_bit_set | src/storage/disk_manager.c | 3414 |
disk_stab_cursor_set_bit | src/storage/disk_manager.c | 3429 |
disk_stab_cursor_fix | src/storage/disk_manager.c | 3493 |
disk_stab_unit_reserve | src/storage/disk_manager.c | 3544 |
disk_stab_iterate_units | src/storage/disk_manager.c | 3665 |
disk_stab_iterate_units_all | src/storage/disk_manager.c | 3738 |
disk_stab_set_bits_contiguous | src/storage/disk_manager.c | 3807 |
disk_rv_reserve_sectors | src/storage/disk_manager.c | 3899 |
disk_rv_unreserve_sectors | src/storage/disk_manager.c | 3982 |
disk_reserve_sectors_in_volume | src/storage/disk_manager.c | 4066 |
disk_reserve_sectors | src/storage/disk_manager.c | 4290 |
disk_reserve_from_cache | src/storage/disk_manager.c | 4463 |
disk_reserve_from_cache_vols | src/storage/disk_manager.c | 4612 |
disk_reserve_from_cache_volume | src/storage/disk_manager.c | 4666 |
disk_unreserve_ordered_sectors | src/storage/disk_manager.c | 4703 |
disk_unreserve_ordered_sectors_without_csect | src/storage/disk_manager.c | 4735 |
disk_unreserve_sectors_from_volume | src/storage/disk_manager.c | 4794 |
disk_stab_unit_unreserve | src/storage/disk_manager.c | 4848 |
disk_stab_init | src/storage/disk_manager.c | 4909 |
disk_manager_init | src/storage/disk_manager.c | 5002 |
disk_manager_final | src/storage/disk_manager.c | 5044 |
disk_format_first_volume | src/storage/disk_manager.c | 5062 |
disk_sectors_to_extend_npages | src/storage/disk_manager.c | 6845 |
DISK_VOLHEADER_PAGE | src/storage/disk_manager.h | 35 |
fileio_map_mounted | src/storage/file_io.c | 3448 |
file_header | src/storage/file_manager.c | 90 |
n_page_mark_delete | src/storage/file_manager.c | 104 |
volid_last_expand | src/storage/file_manager.c | 117 |
vpid_sticky_first | src/storage/file_manager.c | 123 |
vpid_last_temp_alloc | src/storage/file_manager.c | 132 |
offset_to_last_temp_alloc | src/storage/file_manager.c | 133 |
vpid_last_user_page_ftab | src/storage/file_manager.c | 139 |
vpid_find_nth_last | src/storage/file_manager.c | 156 |
first_index_find_nth_last | src/storage/file_manager.c | 157 |
FILE_HEADER_ALIGNED_SIZE | src/storage/file_manager.c | 167 |
FILE_FLAG_NUMERABLE | src/storage/file_manager.c | 170 |
FILE_FLAG_ENCRYPTED_AES | src/storage/file_manager.c | 172 |
FILE_FLAG_ENCRYPTED_ARIA | src/storage/file_manager.c | 173 |
FILE_CACHE_LAST_FIND_NTH | src/storage/file_manager.c | 181 |
FILE_HEADER_GET_PART_FTAB | src/storage/file_manager.c | 199 |
FILE_HEADER_GET_FULL_FTAB | src/storage/file_manager.c | 203 |
FILE_HEADER_GET_USER_PAGE_FTAB | src/storage/file_manager.c | 208 |
file_extensible_data | src/storage/file_manager.c | 232 |
FILE_EXTDATA_HEADER_ALIGNED_SIZE | src/storage/file_manager.c | 240 |
FILE_TABLESPACE_FOR_PERM_NPAGES | src/storage/file_manager.c | 281 |
FILE_TABLESPACE_FOR_TEMP_NPAGES | src/storage/file_manager.c | 287 |
file_vsid_collector | src/storage/file_manager.c | 296 |
file_alloc_type | src/storage/file_manager.c | 388 |
FILE_USER_PAGE_MARK_DELETE_FLAG | src/storage/file_manager.c | 425 |
FILE_USER_PAGE_IS_MARKED_DELETED | src/storage/file_manager.c | 426 |
FILE_USER_PAGE_MARK_DELETED | src/storage/file_manager.c | 427 |
FILE_USER_PAGE_CLEAR_MARK_DELETED | src/storage/file_manager.c | 428 |
file_find_nth_context | src/storage/file_manager.c | 433 |
file_tempcache_entry | src/storage/file_manager.c | 448 |
file_tempcache_tran_entry | src/storage/file_manager.c | 457 |
file_tempcache | src/storage/file_manager.c | 467 |
file_Tempcache | src/storage/file_manager.c | 490 |
file_Tracker_vfid | src/storage/file_manager.c | 496 |
file_Tracker_vpid | src/storage/file_manager.c | 497 |
file_track_metadata | src/storage/file_manager.c | 507 |
file_track_item | src/storage/file_manager.c | 515 |
file_manager_init | src/storage/file_manager.c | 859 |
file_manager_final | src/storage/file_manager.c | 872 |
file_header_alloc | src/storage/file_manager.c | 1093 |
file_header_update_mark_deleted | src/storage/file_manager.c | 1317 |
file_extdata_init | src/storage/file_manager.c | 1492 |
file_extdata_max_size | src/storage/file_manager.c | 1520 |
file_extdata_apply_funcs | src/storage/file_manager.c | 1886 |
file_extdata_find_and_remove_item | src/storage/file_manager.c | 2571 |
file_partsect_is_full | src/storage/file_manager.c | 2758 |
file_partsect_is_empty | src/storage/file_manager.c | 2770 |
file_partsect_set_bit | src/storage/file_manager.c | 2796 |
file_partsect_pageid_to_offset | src/storage/file_manager.c | 2826 |
file_partsect_alloc | src/storage/file_manager.c | 2847 |
file_create_with_npages | src/storage/file_manager.c | 3101 |
file_create_heap | src/storage/file_manager.c | 3126 |
file_create_temp_internal | src/storage/file_manager.c | 3155 |
file_create_temp | src/storage/file_manager.c | 3217 |
file_create_temp_numerable | src/storage/file_manager.c | 3231 |
file_create_query_area | src/storage/file_manager.c | 3244 |
file_create_ehash | src/storage/file_manager.c | 3261 |
file_create_ehash_dir | src/storage/file_manager.c | 3285 |
file_create | src/storage/file_manager.c | 3311 |
file_table_collect_vsid | src/storage/file_manager.c | 3915 |
file_table_collect_all_vsids | src/storage/file_manager.c | 3934 |
file_destroy | src/storage/file_manager.c | 4121 |
file_temp_retire_preserved | src/storage/file_manager.c | 4445 |
file_temp_retire_internal | src/storage/file_manager.c | 4476 |
file_perm_expand | src/storage/file_manager.c | 4644 |
file_table_move_partial_sectors_to_header | src/storage/file_manager.c | 4772 |
file_table_append_full_sector_page | src/storage/file_manager.c | 4976 |
file_table_add_full_sector | src/storage/file_manager.c | 5026 |
file_perm_alloc | src/storage/file_manager.c | 5166 |
file_alloc | src/storage/file_manager.c | 5405 |
file_alloc_sticky_first_page | src/storage/file_manager.c | 5681 |
file_rv_fhead_sticky_page | src/storage/file_manager.c | 5753 |
file_get_sticky_first_page | src/storage/file_manager.c | 5779 |
file_set_tde_algorithm_internal | src/storage/file_manager.c | 5896 |
file_get_tde_algorithm_internal | src/storage/file_manager.c | 5963 |
file_dealloc | src/storage/file_manager.c | 6116 |
file_perm_dealloc | src/storage/file_manager.c | 6309 |
file_rv_dealloc_internal | src/storage/file_manager.c | 6616 |
file_rv_dealloc_on_undo | src/storage/file_manager.c | 6758 |
file_rv_dealloc_on_postpone | src/storage/file_manager.c | 6773 |
file_numerable_add_page | src/storage/file_manager.c | 7935 |
file_extdata_find_nth_vpid | src/storage/file_manager.c | 8119 |
file_extdata_find_nth_vpid_and_skip_marked | src/storage/file_manager.c | 8153 |
file_numerable_find_nth | src/storage/file_manager.c | 8193 |
file_rv_user_page_mark_delete | src/storage/file_manager.c | 8381 |
file_rv_user_page_unmark_delete_logical | src/storage/file_manager.c | 8406 |
file_numerable_truncate | src/storage/file_manager.c | 8577 |
file_temp_alloc | src/storage/file_manager.c | 8650 |
disk_reserve_sectors | src/storage/file_manager.c | 8715 |
file_temp_reset_user_pages | src/storage/file_manager.c | 8949 |
file_temp_preserve | src/storage/file_manager.c | 9143 |
file_tempcache_init | src/storage/file_manager.c | 9171 |
file_tempcache_final | src/storage/file_manager.c | 9234 |
file_tempcache_get | src/storage/file_manager.c | 9414 |
file_tempcache_put | src/storage/file_manager.c | 9541 |
file_tempcache_drop_tran_temp_files | src/storage/file_manager.c | 9645 |
file_tempcache_cache_or_drop_entries | src/storage/file_manager.c | 9664 |
file_tempcache_pop_tran_file | src/storage/file_manager.c | 9702 |
file_tracker_create | src/storage/file_manager.c | 9861 |
file_tracker_load | src/storage/file_manager.c | 9910 |
file_tracker_register | src/storage/file_manager.c | 9960 |
file_tracker_register_internal | src/storage/file_manager.c | 10016 |
file_tracker_unregister | src/storage/file_manager.c | 10113 |
file_tracker_map | src/storage/file_manager.c | 10306 |
file_tracker_interruptable_iterate | src/storage/file_manager.c | 10992 |
file_heap_des | src/storage/file_manager.h | 82 |
file_btree_des | src/storage/file_manager.h | 98 |
file_ovf_btree_des | src/storage/file_manager.h | 106 |
FILE_DESCRIPTORS_SIZE | src/storage/file_manager.h | 128 |
file_descriptors | src/storage/file_manager.h | 130 |
file_tablespace | src/storage/file_manager.h | 143 |
FILE_ALLOC_BITMAP | src/storage/file_manager.h | 153 |
FILE_FULL_PAGE_BITMAP | src/storage/file_manager.h | 154 |
FILE_ALLOC_BITMAP_NBITS | src/storage/file_manager.h | 157 |
file_partial_sector | src/storage/file_manager.h | 162 |
pgbuf_dealloc_page | src/storage/page_buffer.c | 14562 |
DISK_SECTOR_NPAGES | src/storage/storage_common.h | 109 |
trk_vfid | src/transaction/boot_sr.c | 119 |
LOG_MAX_DBVOLID | src/transaction/log_volids.hpp | 34 |
cubrid-disk-manager.md— 파일 매니저와 디스크 매니저 양쪽을 다루는 상위 수준 동반 문서.raw/code-analysis/cubrid/storage/disk_manager/아래의 원본 분석 자료 및 numerable 파일 Q&A 노트raw/code-analysis/cubrid/file-manager-numerable-qa.md.- 코드:
src/storage/file_manager.{c,h},src/storage/disk_manager.{c,h}. - 방법론:
knowledge/methodology/code-analysis-detail-doc.md.