(KO) CUBRID AREA 할당기 — 동일 크기 객체에 자리를 떼어 주는 슬랩(slab) 풀
목차
학술적 배경
섹션 제목: “학술적 배경”범용 malloc은 어떤 서브시스템이 동일한 크기의 레코드를 초당 수천 개씩 할당하고 풀어 내는 자리에서는 잘못된 도구다. 두 가지 비용이 큰 자리를 차지한다. 하나는 청크마다의 장부 비용이다. 24바이트짜리 DB_VALUE 한 개에 16~32바이트의 할당기 메타데이터가 들러붙으면, 워킹셋이 두 배로 부풀어 오른다. 다른 하나는 단편화다. 풀려 난 셀은 범용 할당기의 size-class 칸으로 돌아가, 다른 모든 클래스와 캐시 라인과 TLB 엔트리를 두고 다툰다. 방금 풀어 낸 셀이 바로 다시 쓰일 가능성도 거의 없다.
교과서적인 답이 슬랩 할당기(slab allocator)다. Jeff Bonwick이 The Slab Allocator: An Object-Caching Kernel Memory Allocator(USENIX Summer 1994)에서 내놓았고, 그 뒤 Linux 커널이 SLAB / SLUB / SLOB로 다듬어 두었다. 골격은 단순하다. 이어진 메모리 자리(슬랩 또는 블록)를 한 가지 크기의 셀로 미리 잘라 두고, 셀마다 free / in-use를 비트 한 자리로 적어 두며, 슬랩이 차면 다음 슬랩을 사슬에 매단다. 핫 패스에서는 전역 할당기를 절대로 건드리지 않는 식이다.
Database Internals(Petrov)의 4장 “Implementing B-Trees, Database System Concepts(Silberschatz, Korth, Sudarshan, 6판)의 13장이 모두 슬랩/풀 할당을 작고, 모양이 같고, 회전이 빠른 레코드”의 기본 규율로 끌어들인다. 벌크 로드 도중의 인덱스 노드, 커밋 시점의 로그 레코드, 컴파일 도중의 파스 트리 노드, 평가 단계의 식 atom 같은 것들이 그 식구다.
이 모델이 풀어 두는 구현 선택지가 셋이다. 본 문서의 나머지가 이 셋을 따라간다.
- 슬랩을 한 개로 둘 것인가, 여러 개로 갈라 둘 것인가. Bonwick의 커널 슬랩은 코어 사이로 늘어나도록 CPU별 매거진을 둔다. 임베디드와 데이터베이스 엔진은 객체 클래스마다 사슬 하나만 두고, 그 안에서 lock-free로 다툼을 흡수하는 길로 자리 잡는 일이 잦다. CUBRID는 후자다.
- 셀마다의 free 비트는 어디에 둘 것인가. 셀 안에 끼워 넣는 freelist는 포인터 한 번 읽기로 끝나 값싸지만, 풀어 낸 셀을 더럽히고 동시 환경에서는 ABA 위험을 짊어진다. 옆에 따로 둔 비트맵은 셀을 깨끗하게 두고, lock-free CAS로 굴리기 좋다. CUBRID는 비트맵(
LF_BITMAP)을 고른다. - 할당기가 비어 있는 슬랩을 어떻게 찾는가. 사슬을 처음부터 훑으면 O(n)이다. 스레드별 캐시는 그것을 O(1)로 줄여 주지만 스레드 사이의 불균형이라는 대가가 따른다. hint 포인터 하나만 두는 길이 가장 값싸다. CUBRID는 hint(
area->hint_block)를 고른다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”엔진다운 엔진치고 핫 패스의 같은 크기 할당을 어떤 모양의 풀로든 감싸지 않는 곳은 없다. 이 어휘는 Bonwick의 원 논문에 나오지 않는다. 커널 슬랩과 SQL 엔진 사이에서 자라난 공학 어휘다.
클래스마다의 풀, 담는 내용으로 이름을 매단다
섹션 제목: “클래스마다의 풀, 담는 내용으로 이름을 매단다”풀은 일반화된 도구가 아니다. PostgreSQL의 MemoryContext(AllocSetContext, SlabContext, GenerationContext), MySQL의 MEM_ROOT(THD가 들고 있고 구문 단위로 도는), CUBRID의 AREA * — 모두 name 필드를 두고, 자기 서브시스템의 전역 포인터(Value_area, tp_Domain_area, Set_Ref_Area, …)에 매달려 있다. 이름이 있어야 누수가 눈에 들어온다. 런타임 덤프에서 어느 풀이 부풀었는지가 곧장 드러난다.
트랜잭션이 아니라 “단계”에 묶이는 수명
섹션 제목: “트랜잭션이 아니라 “단계”에 묶이는 수명”풀의 진짜 값은 한꺼번에 비우기 프리미티브에 있다. PostgreSQL은 구문 끝에서 MemoryContextReset을 부르고, MySQL은 free_root(MEM_ROOT*, MYF_FLAGS)를, CUBRID는 area_flush를 부른다. 셋 모두 모든 블록을 한 번 훑으며 풀어 낸다. 규율은 한 줄이다. 마음껏 잡아 두었다가 단계 경계에서 한꺼번에 비운다. 그 단계의 정의는 서브시스템이 짊어진다.
셀 추적 — 비트맵인가, 셀 안에 끼워 둔 freelist인가
섹션 제목: “셀 추적 — 비트맵인가, 셀 안에 끼워 둔 freelist인가”두 길이 함께 산다. 셀 안에 끼워 둔 freelist는 풀어 낸 셀의 앞 8바이트에 다음 free 셀을 가리키는 포인터를 적는다(PostgreSQL AllocSet, MySQL MEM_ROOT, 거의 모든 malloc이 그렇다). 옆에 둔 비트맵은 워드 배열을 따로 두어 점유 여부를 적어 둔다. 셀이 깨끗하게 남고, 메모리 hop 한 번이 더 든다. CUBRID는 후자를 골랐다. 비트맵 연산은 32비트 워드에 대한 CAS로 lock-free로 돌리기 쉬운 반면, 셀 안 freelist는 ABA 문제를 안고 가야 한다.
lock-free 빠른 길과 뮤텍스로 막은 느린 길
섹션 제목: “lock-free 빠른 길과 뮤텍스로 막은 느린 길”엔진다운 엔진은 거의 모두 비어 있는 셀 찾기의 빠른 길을 lock-free로, 새 슬랩 잡기의 느린 길을 굵직한 락으로 갈라 둔다. CUBRID에서는 그 경계가 분명하다. area->area_mutex는 새 AREA_BLOCK을 매다는 자리에서만 잡히고, 블록 안의 lock-free LF_BITMAP::get_entry가 핫 패스를 떠맡는다.
가변 크기 셀은 “size class별 풀”로
섹션 제목: “가변 크기 셀은 “size class별 풀”로”CUBRID는 한 AREA 안에서 여러 크기를 함께 굴리지 않는다. AREA는 area_create 자리에서 셀 크기 한 가지로 못 박히고, 서브시스템은 자기에게 필요한 크기 종류만큼 따로 AREA를 만든다. Linux 커널 슬랩 캐시도 같은 길을 간다.
이론과 CUBRID 사이의 이름 짝
섹션 제목: “이론과 CUBRID 사이의 이름 짝”| 이론적 개념 | CUBRID 명칭 |
|---|---|
| 클래스별 풀 | AREA 구조체(area_alloc.h); 객체 클래스마다 전역 한 개(Value_area, tp_Domain_area, …) |
| 같은 크기 셀의 슬랩 | AREA_BLOCK(area_alloc.h); block_size = element_size * alloc_count |
| 셀 점유 추적 | AREA_BLOCK::bitmap(lockfree_bitmap.hpp의 LF_BITMAP) |
| 슬랩 사슬 | AREA::blockset_list → AREA_BLOCKSET_LIST::items[] → AREA_BLOCK * |
| 느린 길의 락 | AREA::area_mutex(area_alloc의 새 블록 잡기 갈래와 area_insert_block 안에서만 잡힌다) |
| 빠른 길의 hint | AREA::hint_block(단일 AREA_BLOCK *) |
| 한 번에 비우기 / 부수기 | area_flush, area_destroy |
| 모든 풀의 전역 등록부 | static AREA *area_List + pthread_mutex_t area_List_lock |
| 디버그용 double-free / use-after-free 잡기 | 셀 prefix의 AREA_PREFIX_INITED(0) / AREA_PREFIX_FREED(0x01010101) 태그(!NDEBUG) |
| 블록당 셀 수(설정) | AREA::alloc_count(LF_BITMAP_COUNT_ALIGN이 32의 배수로 올림) |
| 블록셋당 블록 수 | AREA_BLOCKSET_SIZE = 256(컴파일 타임 상수) |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”AREA 모듈은 헤더 한 개와 .c 한 개, 합쳐 약 1000줄짜리 단출한 모듈이다. 공개 함수는 일곱(area_init, area_final, area_create, area_destroy, area_alloc, area_free, area_flush)에 진단용 area_validate, area_dump까지 합쳐 아홉이다. 자료 구조는 셋(AREA, AREA_BLOCK, AREA_BLOCKSET_LIST)이다.
뚜렷한 선택은 여섯 가지다. (1) 모든 풀을 묶는 전역 단일 연결 리스트(area_List)를 두어 엔진 전체에서 수명 관리를 한 모양으로 잡는다. (2) 슬랩 사슬 자체도 청크화되어 있다. 256개의 블록 포인터를 안고 있는 블록셋이 그 단위다. 사슬이 수백 블록으로 자라도 lookup 구조가 캐시 친화적으로 머문다. (3) 블록마다 앞단에 lock-free 셀 할당을 떠맡는 LF_BITMAP이 자리한다. (4) 단일 hint_block 포인터가 흔한 경우를 비트맵 연산 한 번으로 끝낸다. (5) 뮤텍스는 사슬을 한 블록만큼 늘리는 자리에서만 잡힌다. alloc/free 핫 패스에서는 절대 잡히지 않는다. (6) 디버그 빌드는 셀마다 4바이트(또는 8바이트) prefix를 두어 double-free를 잡는다.
전체 모양
섹션 제목: “전체 모양”flowchart LR
subgraph G["전역 등록부"]
AL["area_List<br/>(static AREA *)"]
ALK["area_List_lock<br/>(pthread_mutex_t)"]
end
subgraph A["AREA (객체 클래스 하나당 한 개)"]
AH["name, element_size,<br/>alloc_count, block_size"]
AHB["hint_block →"]
AM["area_mutex<br/>(사슬을 늘릴 때만)"]
ABL["blockset_list →"]
end
subgraph BS["AREA_BLOCKSET_LIST (256개 포인터씩의 사슬)"]
BS1["BLOCKSET #0<br/>items[0..255]<br/>used_count"]
BS2["BLOCKSET #1<br/>items[0..255]<br/>used_count"]
BSn["…"]
end
subgraph B["AREA_BLOCK (슬랩 한 개)"]
BBM["LF_BITMAP<br/>(셀별 free/used)"]
BBD["data[]<br/>(element_size 셀이 alloc_count 개)"]
end
AL --> A
AL -. next .-> A
A --> ABL
ABL --> BS1
BS1 -. next .-> BS2
BS2 -. next .-> BSn
BS1 --> B
BS2 --> B
AHB --> B
이 그림에 그어진 경계는 셋이다. (전역과 AREA 단위) 프로세스 전체에 걸린 area_List가 풀들을 사슬로 묶는다. lookup은 코드 경로 기준이다. 서브시스템마다 자기 AREA * 전역을 들고 있고, 이름으로 찾아가는 식이 아니다. (AREA 단위와 블록셋 단위) 슬랩 사슬은 두 단이다. 블록셋 배열의 사슬이라는 모양 덕에, 비어 있는 셀을 찾으러 사슬을 훑을 때 set 단위 메타데이터가 닿는 캐시 라인이 최대 ⌈N / 256⌉ 줄에 그친다. 블록 하나하나로 건너뛰는 비용도 흩어진다. (블록 단위와 셀 단위) 한 블록 안에서 스레드가 다투는 자료 구조는 비트맵뿐이다. 셀은 인덱스로 닿는다.
풀 디스크립터 — AREA
섹션 제목: “풀 디스크립터 — AREA”// AREA — src/base/area_alloc.hstruct area{ AREA *next; /* link in the global area_List */
char *name; /* diagnostic only (area_dump) */ size_t element_size; /* cell size, including !NDEBUG prefix */ size_t alloc_count; /* cells per block (32-aligned) */ size_t block_size; /* element_size * alloc_count */
AREA_BLOCKSET_LIST *blockset_list; /* head of slab-pointer chain */ AREA_BLOCK *hint_block; /* hint for the fast path */ pthread_mutex_t area_mutex; /* serialises adding a new block */
/* for dumping */ size_t n_allocs; size_t n_frees;
void (*failure_function) (void); /* OOM hook (ws_abort_transaction in client) */};failure_function 필드는 풀을 들고 있는 서브시스템마다 OOM 회복 훅을 다르게 끼우게 해 준다. 클라이언트는 이 자리를 ws_abort_transaction에 묶어 둔다. 그 덕에 area_alloc_block 안의 malloc이 실패하면 호출 스택이 NULL을 떠받칠 채비도 안 된 상태에서 무너지는 대신 지금 트랜잭션이 abort된다. 서버는 이 자리를 NULL로 두고 에러 코드만 호출자 쪽으로 흘려 올린다.
area_create에 넘긴 element_size는 두 단계로 조용히 손이 가해진다. 디버그 빌드에서는 셀마다 AREA_PREFIX_SIZE(sizeof(double) = 8바이트) 헤더가 더해져 double-free 잡기에 쓰인다. 빌드와 무관하게 크기는 8의 배수로 올림된다. 그래서 호출자가 24바이트짜리 셀을 청해도 릴리스 빌드에서 32바이트, 디버그에서도 32바이트(24 + 8)가 잡힌다. 셀 페이로드의 시작은 디버그에서 data + i*element_size + AREA_PREFIX_SIZE, 릴리스에서 data + i*element_size다.
블록 — AREA_BLOCK
섹션 제목: “블록 — AREA_BLOCK”// AREA_BLOCK — src/base/area_alloc.hstruct area_block{ LF_BITMAP bitmap; char *data;};포인터 둘 분량의 메타데이터에 이어 block_size 바이트의 셀 자리가 따라온다. 블록 자체는 block_size + sizeof(AREA_BLOCK) 크기로 한 번에 malloc되며, data 포인터는 그 뒤에 이어지는 셀 영역을 가리키도록 맞춰진다. 블록을 풀어 내는 일은 bitmap.destroy()로 비트맵 자체 버퍼를 풀어 준 뒤 free_and_init 한 번이면 끝난다.
// area_alloc_block — src/base/area_alloc.cstatic AREA_BLOCK *area_alloc_block (AREA * area){ AREA_BLOCK *new_block; size_t total = area->block_size + sizeof (AREA_BLOCK);
new_block = (AREA_BLOCK *) malloc (total); if (new_block == NULL) { er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_OUT_OF_VIRTUAL_MEMORY, 1, total); if (area->failure_function != NULL) (*(area->failure_function)) (); return NULL; }
new_block->bitmap.init (LF_BITMAP_LIST_OF_CHUNKS, (int) area->alloc_count, LF_AREA_BITMAP_USAGE_RATIO); new_block->data = ((char *) new_block) + sizeof (AREA_BLOCK); return new_block;}곧장 보이지 않는 두 가지가 있다. 첫째, 비트맵은 LF_BITMAP_LIST_OF_CHUNKS 모양으로 초기화된다. 안쪽으로 들어가 보면 32셀짜리 청크마다 자기 start_idx를 따로 두는 셈이라, 서로 다른 스레드의 동시 get_entry 호출이 라운드로빈으로 다른 청크에 떨어지면서 같은 32비트 CAS 워드 위에서 부딪히지 않는다. 둘째, 사용률 임계값(LF_AREA_BITMAP_USAGE_RATIO)이 서버와 클라이언트에서 다르다. SERVER_MODE에서는 LF_BITMAP_95PERCENTILE_USAGE_RATIO, 그 외에서는 LF_BITMAP_FULL_USAGE_RATIO다. 95퍼센트 임계값이라는 뜻은 서버 모드 블록은 95%가 차면 가득 찬 것으로 본다는 뜻이다. 남은 5%의 여유 자리는 마지막 몇 셀에서 일어날 CAS 다툼을 줄여 주고, 메모리 사용이 약간 늘어나는 대가를 치른다. 한 스레드만 도는 클라이언트는 그 부담이 없으니 100%까지 채우는 길을 고른다.
블록셋 — 청크화한 슬랩 사슬
섹션 제목: “블록셋 — 청크화한 슬랩 사슬”// AREA_BLOCKSET_LIST — src/base/area_alloc.h#define AREA_BLOCKSET_SIZE 256
struct area_blockset_list{ AREA_BLOCKSET_LIST *next; AREA_BLOCK *items[AREA_BLOCKSET_SIZE]; int used_count;};블록셋 한 개에는 최대 256개의 블록 포인터가 들어가고, AREA_BLOCK * 값이 오름차순으로 정렬되어 있다(이 값은 그 블록의 셀 저장 영역 가운데 가장 낮은 주소이기도 하다. 상수 오프셋만 다르다). 두 단 구조를 고른 까닭은 다음과 같다.
- 블록을 모두 단순 연결 리스트로 묶어 두면
area_find_block(area_validate와area_free가 부른다)이 셀 포인터의 주인을 찾으러 모든 블록을 O(N)으로 훑어야 한다. - 블록을 단일 배열에 두면 늘어날 때마다 realloc이 필요해진다. 배열을 들고 있던 동시 reader가 stale 포인터로 풀어진 메모리를 더듬는 위험이 따라온다.
- 블록셋 사슬은 양쪽의 좋은 점만 가져온다. 블록셋마다는 고정 크기 배열이라
area_find_block에서 binary search가 가능하고, 자라는 일은 새 블록셋을 사슬 끝에 매다는 식이라 in-place realloc이 없다. reader는 늘 안전하다.
대가는 블록셋이 거의 비어 있어도 2 KB를 차지한다는 점이다. 일반적인 AREA 블록 256개 기준으로(블록당 기본 321024셀 × 셀당 8~256바이트 = 블록당 8 KB ~ 256 KB), 블록셋 오버헤드는 워킹셋의 1% 미만이다.
할당 흐름 — area_alloc
섹션 제목: “할당 흐름 — area_alloc”flowchart LR
S["area_alloc(area)"] --> H["hint_block.bitmap.get_entry()"]
H --> H1{"entry != -1?"}
H1 -- "예 (빠른 길)" --> RET["pointer = data + idx*element_size<br/>(디버그면 + AREA_PREFIX_SIZE)"]
H1 -- "아니오" --> SC["블록셋 사슬 훑기<br/>각 블록: bitmap.get_entry()"]
SC --> SC1{"찾았는가?"}
SC1 -- "예" --> CAS["기회적<br/>ATOMIC_CAS_ADDR hint = block"]
CAS --> RET
SC1 -- "아니오" --> LK["area_mutex 잠금"]
LK --> NB["area_alloc_block (malloc)"]
NB --> INS["area_insert_block (정렬 append)"]
INS --> SETH["hint_block = new_block"]
SETH --> UL["unlock"]
UL --> RET
세 걸음으로 흘러간다. 1단계(lock-free) — hint 블록을 먼저 두드린다. LF_BITMAP::get_entry는 청크 워드 한 자리에 대한 CAS 한 번으로 끝나며, 셀 인덱스 하나를 돌려주거나 가득 찼다면 -1을 돌려준다. 2단계(lock-free) — 모든 블록셋을 VOLATILE_ACCESS 읽기로 훑어 본다. blockset->next와 ->used_count를 그렇게 읽는다. area_insert_block에서 used_count에 거는 ATOMIC_INC_32가 그 직전의 items[] 저장에 대한 release barrier 역할을 하므로, 동시 append도 추가 동기화 없이 그대로 보인다. 3단계(뮤텍스 보유) — 새 블록을 malloc하는 일은 한 번에 한 스레드만 한다. 진 쪽은 다시 시도할 때 새 블록을 만나게 된다.
2단계 안에 가벼운 hint 갱신이 한 자리 숨어 있다. 어떤 할당기가 hint가 아닌 다른 블록에서 비어 있는 셀을 찾았고 마침 hint가 가득 찬 상태라면, ATOMIC_CAS_ADDR(&area->hint_block, hint_block, block)을 시도한다. 이 CAS가 빗나갈 수 있다(다른 스레드가 이미 갈아 둔 경우다). 그래도 어느 쪽도 진전을 잃지 않는다. 평소 부하의 회전 흐름 위에서 hint는 뮤텍스 없이도 비어 있는 블록 쪽으로 모아진다.
// area_alloc — src/base/area_alloc.c (condensed)void *area_alloc (AREA * area){ AREA_BLOCKSET_LIST *blockset; AREA_BLOCK *block, *hint_block; int used_count, i, entry_idx; char *entry_ptr; int rv;
/* Step 1: hint block fast path. */ hint_block = VOLATILE_ACCESS (area->hint_block, AREA_BLOCK *); entry_idx = hint_block->bitmap.get_entry (); if (entry_idx != -1) { block = hint_block; goto found; }
/* Step 2: scan the blockset chain. */ for (blockset = area->blockset_list; blockset != NULL; blockset = VOLATILE_ACCESS (blockset->next, AREA_BLOCKSET_LIST *)) { used_count = VOLATILE_ACCESS (blockset->used_count, int); for (i = 0; i < used_count; i++) { block = VOLATILE_ACCESS (blockset->items[i], AREA_BLOCK *); entry_idx = block->bitmap.get_entry (); if (entry_idx != -1) { /* opportunistic hint upgrade */ hint_block = VOLATILE_ACCESS (area->hint_block, AREA_BLOCK *); if (LF_BITMAP_IS_FULL (&hint_block->bitmap) && !LF_BITMAP_IS_FULL (&block->bitmap)) ATOMIC_CAS_ADDR (&area->hint_block, hint_block, block); goto found; } } }
/* Step 3: grow the chain by one block. */ rv = pthread_mutex_lock (&area->area_mutex);
if (area->hint_block != hint_block) /* re-check after acquiring lock */ { block = area->hint_block; entry_idx = block->bitmap.get_entry (); if (entry_idx != -1) { pthread_mutex_unlock (&area->area_mutex); goto found; } }
block = area_alloc_block (area); /* ... condensed: error handling, area_insert_block, set hint ... */ area->hint_block = block; pthread_mutex_unlock (&area->area_mutex);
found: entry_ptr = block->data + area->element_size * entry_idx;#if !defined (NDEBUG) *(int *) entry_ptr = AREA_PREFIX_INITED; entry_ptr += AREA_PREFIX_SIZE;#endif return ((void *) entry_ptr);}n_allocs / n_frees 카운터는 SERVER_MODE가 아닐 때만 갱신된다. 서버에서는 할당마다 atomic 증가를 걸지 않겠다는 의도가 주석에 명시되어 있다. 카운터는 클라이언트 쪽 area_dump 진단용일 뿐이다.
끼워 넣기 — 블록셋을 주소순으로 유지
섹션 제목: “끼워 넣기 — 블록셋을 주소순으로 유지”// area_insert_block — src/base/area_alloc.c (condensed)static intarea_insert_block (AREA * area, AREA_BLOCK * new_block){ AREA_BLOCKSET_LIST **last_blockset_p = &area->blockset_list; AREA_BLOCKSET_LIST *blockset, *new_blockset; int used_count;
for (blockset = area->blockset_list; blockset != NULL; blockset = blockset->next) { last_blockset_p = &blockset->next; used_count = blockset->used_count; if (used_count == AREA_BLOCKSET_SIZE) continue; /* this set is full */
/* Append iff new_block has a higher address than the last * block in this blockset — keeps items[] address-sorted. */ if (blockset->items[used_count - 1] < new_block) { blockset->items[used_count] = new_block; ATOMIC_INC_32 (&blockset->used_count, 1); /* release barrier */ return NO_ERROR; } }
/* No room — allocate a fresh blockset and append. */ new_blockset = area_alloc_blockset (area); /* ... condensed ... */ new_blockset->items[0] = new_block; ATOMIC_INC_32 (&new_blockset->used_count, 1); *last_blockset_p = new_blockset; return NO_ERROR;}핵심은 셋이다. (a) 끼워 넣기는 마지막 블록 주소가 새 블록보다 작은 첫 번째 비-가득 블록셋의 끝에 append한다. malloc은 뒤이은 호출에 더 높은 주소를 돌려주는 경향이 있어, 같은 블록셋 안에서 새 블록도 주소 정렬을 거의 늘 지킨다. (b) 어느 블록셋에도 들어갈 자리가 없는 경우(예컨대 높은 주소 블록 다음에 낮은 주소 블록이 malloc에서 돌아오는 경우) 새 블록셋을 잡아 사슬 끝에 매단다. 이 새 블록셋의 항목들이 앞 블록셋들의 항목보다 반드시 크리라는 보장은 없다. (c) used_count에 거는 ATOMIC_INC_32가 release barrier 역할이고, 그 직전의 items[used_count] = new_block 저장은 그것보다 먼저 끝나야 한다. 동시에 읽는 쪽(area_alloc의 2단계, area_find_block)은 두 필드를 다시 읽으면서 (item, used_count)가 짝 맞는 모습을 보게 된다.
소스의 “no shifting / re-sort” 주석이 그 대가를 정직하게 시인한다. 블록셋은 거의 정렬되어 있을 뿐이다. area_find_block은 그 한계를 모든 블록셋을 시도하는 식으로 우회한다(포인터 범위에 들어가는 한 곳만이 아니라). 그 흐름은 곧이어 본다.
주인 찾기 — area_find_block
섹션 제목: “주인 찾기 — area_find_block”area_free와 area_validate는 셀 포인터의 주인 블록을 찾아내야 한다. 구조 덕에 그 비용이 블록셋마다 O(log N)짜리 binary search에 사슬 길이만큼의 O(B)(B는 블록셋 개수) 걸음으로 끝난다.
// area_find_block — src/base/area_alloc.c (condensed)static AREA_BLOCK *area_find_block (AREA * area, const void *ptr){ AREA_BLOCKSET_LIST *blockset; AREA_BLOCK *first_block, *last_block, *block; int middle, left, right, pos, used_count;
for (blockset = area->blockset_list; blockset != NULL; blockset = VOLATILE_ACCESS (blockset->next, AREA_BLOCKSET_LIST *)) { used_count = VOLATILE_ACCESS (blockset->used_count, int); first_block = blockset->items[0]; if ((char *) ptr < first_block->data) continue; /* below this set's range */
last_block = VOLATILE_ACCESS (blockset->items[used_count - 1], AREA_BLOCK *); if (last_block->data + area->block_size <= (char *) ptr) continue; /* above this set's range */
/* binary search inside the blockset's address range */ left = 0; right = used_count - 1; while (left <= right) { middle = (left + right) / 2; block = VOLATILE_ACCESS (blockset->items[middle], AREA_BLOCK *); if (block->data > (char *) ptr) right = middle - 1; else left = middle + 1; } pos = right;
block = VOLATILE_ACCESS (blockset->items[pos], AREA_BLOCK *); if ((block->data <= (char *) ptr) && ((char *) ptr < block->data + area->block_size)) return block; } return NULL;}첫 블록과 마지막 블록 검사가 값싼 범위 거름망이다. 포인터가 첫 블록의 data보다 낮거나 마지막 블록의 꼬리보다 높으면 그 블록셋은 통째로 건너뛴다. 포인터가 그 사이에 든다면 블록셋 안쪽에서 binary search로 data가 ptr 이하면서 가장 큰 블록을 찾고, 마지막에 한 번 더 포함 검사를 걸어 확정한다. 이 풀에 정말로 속한 포인터라면 늘 이 검사를 통과한다. 거의 정렬만으로도 충분한 까닭이 있다. 기존 범위보다 낮은 주소의 신선한 malloc 블록은 기존 블록셋에 끼지 않고 새 블록셋으로 떨어진다. 그래서 한 블록셋 안에서 보면 항목들은 만들어질 때 이미 주소순이다.
풀어 내기 — area_free
섹션 제목: “풀어 내기 — area_free”// area_free — src/base/area_alloc.c (condensed)intarea_free (AREA * area, void *ptr){ AREA_BLOCK *block, *hint_block; char *entry_ptr; int entry_idx, offset;
if (ptr == NULL) { /* error */ }
#if !defined (NDEBUG) entry_ptr = ((char *) ptr) - AREA_PREFIX_SIZE; /* strip debug prefix */#else entry_ptr = (char *) ptr;#endif
block = area_find_block (area, (const void *) entry_ptr); if (block == NULL) { /* ER_AREA_ILLEGAL_POINTER */ }
offset = (int) (entry_ptr - block->data); if (offset < 0 || offset % area->element_size != 0) { /* ER_AREA_ILLEGAL_POINTER — pointer not at cell boundary */ }
#if !defined (NDEBUG) if (*(int *) entry_ptr != AREA_PREFIX_INITED) { /* ER_AREA_FREE_TWICE */ } *(int *) entry_ptr = AREA_PREFIX_FREED;#endif
entry_idx = offset / (int) area->element_size; block->bitmap.free_entry (entry_idx);
/* opportunistic hint downgrade — pull hint to a non-full block */ hint_block = VOLATILE_ACCESS (area->hint_block, AREA_BLOCK *); if (LF_BITMAP_IS_FULL (&hint_block->bitmap) && !LF_BITMAP_IS_FULL (&block->bitmap)) ATOMIC_CAS_ADDR (&area->hint_block, hint_block, block);
return NO_ERROR;}검증은 네 단의 깔때기다. (1) 포인터가 NULL이 아닐 것. (2) area_find_block이 주인 블록을 찾아 줄 것. (3) 오프셋이 element_size의 양의 배수일 것(포인터가 셀 한가운데가 아니라 셀 경계에 자리할 것). (4) 디버그 빌드라면 prefix 태그가 AREA_PREFIX_INITED와 들어맞을 것. 어느 한 단이라도 빠지면 free하지 않고 거절한다. area_free는 free와 달리 호출자 버그에 단단하게 막혀 있다.
area_alloc이 upgrade 쪽에서 쓰는 그 hint-CAS가 여기서는 downgrade 쪽에서 돈다. 한 번의 free가 가득 차 있던 블록을 비어 있는 셀 하나 가진 블록으로 바꾸면, hint는 그 블록을 가리키는 쪽으로 기회 닿는 대로 옮겨 간다. 다음 할당이 곧장 그 블록의 빠른 길로 떨어지므로 워킹셋이 좁게 머문다.
수명 — init, create, flush, destroy, final
섹션 제목: “수명 — init, create, flush, destroy, final”sequenceDiagram
participant Boot as boot_sr / boot_cl
participant Init as area_init
participant Owner as 서브시스템 (예: object_primitive)
participant Create as area_create
participant Use as 런타임
participant Final as area_final
Boot->>Init: area_init() (area_List 0으로 초기화)
Boot->>Owner: pr_area_init() / set_area_init() / ws_area_init()
Owner->>Create: area_create("Value containers", sizeof(DB_VALUE), N)
Create-->>Owner: AREA *
Note over Owner: 받은 AREA *를 전역에 보관<br/>(Value_area, tp_Domain_area, …)
Use->>Use: area_alloc / area_free 다수
Boot->>Final: 종료 시 area_final()
Final->>Final: area_List 순회 → 각각 area_flush → free
area_init은 boot_server_main / boot_client_main에서 부르며, 전역 area_List를 0으로 비우는 일만 한다. 그 다음 서브시스템마다의 *_area_init이 자기 size class로 area_create를 부른다. area_create는 AREA를 malloc하고, 첫 블록셋과 첫 블록을 미리 잡아 두어(빈 사슬을 만나는 일을 처음부터 막는다), area_List_lock을 잡고 새 AREA를 전역 리스트의 머리에 매단다.
// area_create — src/base/area_alloc.c (condensed)AREA *area_create (const char *name, size_t element_size, size_t alloc_count){ AREA *area = (AREA *) malloc (sizeof (AREA)); /* ... condensed: name strdup, NULL checks ... */
#if !defined (NDEBUG) element_size += AREA_PREFIX_SIZE;#endif /* round up to 8-byte multiple */ if (element_size % 8) element_size += 8 - (element_size % 8);
area->element_size = element_size; area->alloc_count = LF_BITMAP_COUNT_ALIGN (alloc_count); /* 32-aligned */ area->block_size = area->element_size * area->alloc_count; area->n_allocs = area->n_frees = 0;#if defined (SERVER_MODE) area->failure_function = NULL;#else area->failure_function = ws_abort_transaction;#endif
area->blockset_list = area_alloc_blockset (area); area->hint_block = area_alloc_block (area); area->blockset_list->items[0] = area->hint_block; area->blockset_list->used_count++;
pthread_mutex_init (&area->area_mutex, NULL);
pthread_mutex_lock (&area_List_lock); area->next = area_List; area_List = area; pthread_mutex_unlock (&area_List_lock); return area;}가벼운 두 자리가 있다. 하나, LF_BITMAP_COUNT_ALIGN은 alloc_count를 32(비트맵 워드 한 개)의 배수로 올린다. 100개 셀을 청해도 실제로 잡히는 자리는 128개다. 둘, failure_function이 클라이언트 쪽에서는 ws_abort_transaction에 묶인다. 그래서 가령 pr_make_value 안의 할당이 실패하면 NULL이 호출 스택을 거꾸로 타고 올라가는 대신, 지금 트랜잭션이 롤백된다.
area_destroy는 짝이 되는 동작이다. area_List_lock 아래에서 리스트 연결을 풀어 내고, area_flush로 모든 블록을 풀어 낸 뒤, AREA 자체를 free한다. area_flush는 blockset_list를 훑으면서 블록마다 bitmap.destroy와 free_and_init을 부르고, 블록셋 자체도 풀어 낸다. area_final(전체 area_List를 돌면서 모든 풀을 비운다)의 본체이기도 하다.
디버그 prefix — double-free 막기
섹션 제목: “디버그 prefix — double-free 막기”!NDEBUG 빌드에서는 셀마다 8바이트 prefix가 따라붙는다(정렬을 위해 sizeof(double)로 선언된다). prefix는 4바이트 태그를 안고 있다.
// debug prefix tags — src/base/area_alloc.c#define AREA_PREFIX_SIZE sizeof(double)enum { AREA_PREFIX_INITED = 0, AREA_PREFIX_FREED = 0x01010101};area_alloc은 AREA_PREFIX_INITED(0)을 적은 뒤 prefix 다음 자리의 포인터를 돌려준다. area_free는 셀 포인터에서 8바이트를 빼서 prefix를 다시 읽고, AREA_PREFIX_INITED와 들어맞는지 본 뒤 AREA_PREFIX_FREED로 덮어쓴다. 두 번째 free는 AREA_PREFIX_FREED를 읽고 곧장 ER_AREA_FREE_TWICE를 띄운다. 두 값을 일부러 그렇게 골랐다. 풀어 낸 객체의 어느 정수 필드가 호출자에게 보일 때 0x01010101로 비치므로, 디버거에서 freed slab cell을 한눈에 알아보기 좋다.
prefix의 대가는 할당 비용이다. 24바이트 DB_VALUE는 32바이트(24 페이로드 + 8 prefix)가 되고, 56바이트 TP_DOMAIN은 64바이트가 된다. 프로덕션 빌드에서는 prefix가 사라지고 셀은 올림된 페이로드 크기 그대로다.
AREA를 쓰는 모듈
섹션 제목: “AREA를 쓰는 모듈”src/object/ 아래의 여덟 호출 자리에서 모듈 초기화 시점에 AREA가 만들어진다. 모두 클라이언트와 SA 모드의 핫 패스 같은 크기 객체들이다. 서버 모드의 lock manager나 catalog 같은 서브시스템은 AREA가 아니라 스레드별 arena의 db_private_alloc에서 자리를 받는다.
| AREA 전역 | 원소(Element) | area_create 자리 | 용도 |
|---|---|---|---|
Value_area | DB_VALUE | pr_area_init(object_primitive.c) | pr_make_value가 만드는 범용 값 그릇 |
tp_Domain_area | TP_DOMAIN | tp_init(object_domain.c) | 타입 도메인 디스크립터(선언된 타입마다 한 개) |
Template_area | OBJ_TEMPLATE | obt_area_init(object_template.c) | 객체 update / insert 템플릿 |
Assignment_area | OBJ_TEMPASSIGN | obt_area_init(object_template.c) | 템플릿 안의 필드별 assignment 레코드 |
Template_area | SM_TEMPLATE(스키마) | classobj_area_init(class_object.c) | 스키마 템플릿 편집(CREATE / ALTER class) |
Set_Ref_Area | DB_COLLECTION | set_area_init(set_object.c) | set / multiset / sequence를 가리키는 핸들 단위 reference |
Set_Obj_Area | COL | set_area_init(set_object.c) | set 본체(header + storage) |
Objlist_area | DB_OBJLIST | ws_area_init(work_space.c) | object id 목록의 단일 연결 노드 |
원본 분석의 “AREA structures used by CUBRID modules” 다이어그램도 같은 여덟 AREA를 같은 여덟 전역 변수 이름으로 늘어놓는다.
두 가지를 짚어 둔다. (a) 여덟 AREA가 모두 클라이언트 쪽 또는 standalone 모드 워크로드를 떠받친다. 파스 트리를 짜는 파서·옵티마이저, 더티 객체를 좇는 워크스페이스, 컬렉션 타입 컬럼을 받쳐 주는 set 본체가 그 자리들이다. 서버 본체(lock manager, 페이지 버퍼, 질의 실행기)는 핫 패스에서 AREA에 기대지 않는다. (b) 명명 규약은 <Subsystem>_area 또는 <subsystem>_<class>_area다. 이름 하나가 정확히 한 area_create 호출과 한 객체 클래스에 일대일로 짝지어진다. 그래서 같은 크기의 새 핫 할당을 엔진에 더하려면, 새 전역 AREA *를 선언하고 서브시스템 init에서 area_create를 부르고 alloc/free를 area_alloc / area_free로 흘리면 끝이다.
lock-free 비트맵(LF_BITMAP) — 짧게
섹션 제목: “lock-free 비트맵(LF_BITMAP) — 짧게”엔진의 LF_BITMAP(src/base/lockfree_bitmap.{hpp,cpp})이 AREA_BLOCK마다 앞단에 자리한다. 본 문서와 맞물리는 자리에서 둘만 짚는다.
- 청크 모양. AREA가 쓰는
LIST_OF_CHUNKS는 비트맵을 32셀 단위 청크로 갈라 두고, 청크마다 자기start_idx를 두어 라운드로빈으로 자리를 떼어 준다.ONE_CHUNK는 워드 한 개로 묶는다. AREA는 늘LIST_OF_CHUNKS를 쓴다. 같은 블록에서 동시에 일어나는 할당이 서로 다른 청크에 떨어지도록 하기 위함이다. - 사용률 임계값.
is_full()은entry_count_in_use / entry_count >= usage_threshold일 때 true를 돌려준다. 임계값은init시점에float으로 정한다. 클라이언트와 SA 모드에서는FULL_USAGE_RATIO(1.0), 서버 모드에서는NINTETYFIVE_PERCENTILE_USAGE_RATIO(0.95)다. 95% 쪽을 고른 까닭은 5%의 메모리 여유와 다툼 줄이기를 맞바꾸기 위해서다. 마지막 몇 셀이 모든 할당기를 같은 워드로 몰아넣지 못하게 막아 준다.
본 문서는 LF_BITMAP을 검은 상자로 두고 본다. 더 깊은 lock-free 공학(고경합 시 CAS 재시도 전략, ABA 막기, 청크별 start_idx 라운드로빈)은 따로 잡아 본다. 미해결 질문 §1을 참고하라.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”닻은 심볼 이름이지 줄 번호가 아니다. 지금 자리는
git grep -n '<symbol>' src/base/area_alloc.{c,h}로 짚는다.
공개 API (area_alloc.c)
섹션 제목: “공개 API (area_alloc.c)”area_init,area_final— 모듈 켜기와 끄기.boot_server_main과boot_client_main에서 부른다.area_create— AREA 잡기, 크기 다듬기, 첫 블록셋과 첫 블록을 미리 잡기,area_List에 매달기.area_destroy— 연결 풀기, flush, free.area_alloc— 세 걸음 할당(hint / scan / grow). 핫 패스다.area_free— 주인 블록 찾기, 셀 경계와 디버그 prefix 검증, 비트 풀기.area_validate— 포인터 범위 sanity 검사(헤더에 노출되어 있으나 in-tree 호출자 없음).area_flush— 한 AREA의 모든 블록 풀기.area_dump— 진단용.area_List를 돈다.
내부 함수 (area_alloc.c)
섹션 제목: “내부 함수 (area_alloc.c)”area_alloc_block— 슬랩 한 개malloc, 비트맵 init.area_alloc_blockset— 256슬롯 블록셋 한 개malloc.area_insert_block— 사슬에 정렬 append.area_find_block— 사슬 순회 + 블록셋마다의 binary search.area_info— 한 AREA에 대한 dump 본체.
자료 구조 (area_alloc.h)
섹션 제목: “자료 구조 (area_alloc.h)”struct area,struct area_block,struct area_blockset_list.
설정과 상수
섹션 제목: “설정과 상수”AREA_BLOCKSET_SIZE(area_alloc.h) — 256.AREA_PREFIX_SIZE,AREA_PREFIX_INITED,AREA_PREFIX_FREED(area_alloc.c,!NDEBUG).LF_AREA_BITMAP_USAGE_RATIO—SERVER_MODE에서 0.95, 그 외 1.0.LF_BITMAP_COUNT_ALIGN(lockfree_bitmap.hpp) — 32의 배수로 올림.
area_List(static AREA *),area_List_lock(pthread_mutex_t, 서버 전용).
외부 AREA 주인 (객체 클래스마다 전역 한 개)
섹션 제목: “외부 AREA 주인 (객체 클래스마다 전역 한 개)”Value_area(object_primitive.c) —DB_VALUE.tp_Domain_area(object_domain.c) —TP_DOMAIN.Template_area(object_template.c) —OBJ_TEMPLATE.Assignment_area(object_template.c) —OBJ_TEMPASSIGN.Template_area(class_object.c) — 스키마SM_TEMPLATE(객체 템플릿용과는 별개의 TU-static binding).Set_Ref_Area,Set_Obj_Area(set_object.c) —DB_COLLECTION,COL.Objlist_area(work_space.c) —DB_OBJLIST.
이 개정 시점의 위치 힌트 (2026-04-30)
섹션 제목: “이 개정 시점의 위치 힌트 (2026-04-30)”| 심볼 | 파일 | 줄 |
|---|---|---|
AREA_BLOCKSET_SIZE 매크로 | base/area_alloc.h | 42 |
struct area_block | base/area_alloc.h | 51 |
struct area_blockset_list | base/area_alloc.h | 63 |
struct area | base/area_alloc.h | 74 |
area_create 선언 | base/area_alloc.h | 104 |
area_alloc 선언 | base/area_alloc.h | 108 |
area_free 선언 | base/area_alloc.h | 110 |
AREA_PREFIX_SIZE | base/area_alloc.c | 60 |
AREA_PREFIX_INITED / _FREED | base/area_alloc.c | 64 |
area_List | base/area_alloc.c | 72 |
area_List_lock | base/area_alloc.c | 74 |
LF_AREA_BITMAP_USAGE_RATIO | base/area_alloc.c | 78 |
area_init | base/area_alloc.c | 100 |
area_final | base/area_alloc.c | 119 |
area_create | base/area_alloc.c | 146 |
area_destroy | base/area_alloc.c | 245 |
area_alloc_block | base/area_alloc.c | 287 |
area_alloc_blockset | base/area_alloc.c | 323 |
area_alloc | base/area_alloc.c | 356 |
area_validate | base/area_alloc.c | 482 |
area_free | base/area_alloc.c | 509 |
area_flush | base/area_alloc.c | 592 |
area_insert_block | base/area_alloc.c | 636 |
area_find_block | base/area_alloc.c | 703 |
area_info | base/area_alloc.c | 778 |
area_dump | base/area_alloc.c | 848 |
LF_BITMAP_COUNT_ALIGN | base/lockfree_bitmap.hpp | 89 |
Value_area = area_create(...) | object/object_primitive.c | 1774 |
Objlist_area = area_create(...) | object/work_space.c | 3852 |
Template_area = area_create(...)(object) | object/object_template.c | 158 |
Assignment_area = area_create(...) | object/object_template.c | 164 |
tp_Domain_area = area_create(...) | object/object_domain.c | 659 |
Template_area = area_create(...)(schema) | object/class_object.c | 168 |
Set_Ref_Area = area_create(...) | object/set_object.c | 158 |
Set_Obj_Area = area_create(...) | object/set_object.c | 167 |
소스 검증 노트
섹션 제목: “소스 검증 노트”원본 분석(한글 PDF, v1.0)은 대체로 옳지만, 다섯 자리에서 지금 소스에 비해 늦거나 어긋난다.
- AREA는 프로세스마다 전역 한 개. 원본 분석은 AREA 자체를 단일 풀로 적어 둔다. 그러나 소스는 AREA의 리스트(
area_List)를 들고 있다. 객체 클래스마다 한 개씩이며, 전역으로 잡히는 것은 그 리스트의 머리이지 단일 AREA가 아니다. 분석 자료의 modules using AREA 다이어그램 자체는 옳다. - AREA_BLOCKSET이 lock-free로 할당을 다룬다. 비트맵 셀 단계에서는 옳다(
LF_BITMAP이get_entry/free_entry에 CAS를 쓴다). 그러나 블록을 더하는 단계에서는 옳지 않다.area_alloc의 느린 길은area->area_mutex를 잡고,area_insert_block은 그 뮤텍스 아래에서만 돈다. lock-free 성질은 평소 부하의 핫 패스에 한정된다. - Free list 어휘. 원본 분석은 AREA가 셀의 free list를 들고 있다는 식으로 적었다. 구현에는 리스트가 없다. 셀 점유는 비트맵이 짚는다. 가리키는 의미는 같다.
- Hint 블록. 원본 분석의 struct 그림에는
hint_block이 들어 있지만, 그 역할에 대한 풀이는 없다. hint는 성능에 가장 큰 무게를 두는 단일 구조다. 흔한 경우의area_alloc2단계를 비트맵 연산 한 번으로 줄여 두고,ATOMIC_CAS_ADDR로 hint를 위/아래로 옮기는 기회적 길이 회전 부하 위에서 사슬 훑기 비용이 자리를 차지하지 못하게 막는 핵심이다. - 디버그 prefix. 원본 분석의 struct 그림은 릴리스 빌드의 셀 모양만 보여 준다.
!NDEBUG빌드에서는 셀 앞에 8바이트 prefix(AREA_PREFIX_INITED/AREA_PREFIX_FREED)가 더해져 double-free 잡기가 가능해진다.area_dump가 출력하는element_size가 서브시스템이 청한 값과 다른 까닭이 여기에 있다.
미해결 질문
섹션 제목: “미해결 질문”LF_BITMAP의 깊은 분석. 원본 분석은 이 자리를 후속 문서로 미루며 JIRA CBRD-21800(lock_free.h의 C++ 리팩토링)을 가리킨다. 고경합 환경에서의 비트맵 CAS 재시도 전략, 청크마다의start_idx라운드로빈,is_full임계값의 의미는lockfree_bitmap.cpp(276줄)에 대한 따로 떼어 둔 walk-through로 다룰 만하다.- 느린 길의 락이 왜 AREA마다인가, 전역이 아닌가. 전역 뮤텍스 하나로도 충분해 보이는데(느린 길은 드물다), AREA마다 두는
area_mutex는 AREA마다 40바이트를 더 쓴다. 다툼 때문에 그렇게 둔 것인가(여덟 init이 동시에 도는 자리), 아니면area_List_lock과의 짝 맞춤일 뿐인가. LF_AREA_BITMAP_USAGE_RATIO = 0.95라는 서버 모드의 trade-off. 5%의 여유가 블록 마지막 셀들의 CAS 다툼을 줄이고 메모리를 약간 더 쓴다. 0.9 / 0.99와 견주어 잰 적이 있는가.- 서버에서는 왜
n_allocs/n_frees를 세지 않는가.SERVER_MODE에서는 카운터 증가가 주석으로 가려져 있다.area_dump는 그래도 그 값을 읽으니 0이 찍힐 텐데, dump 길이 일부러 클라이언트 전용으로 잡혀 있는가. - jemalloc / tcmalloc 환경에서의 주소 정렬 약점. 블록셋의 거의 정렬 성질은
malloc이 평균적으로 더 높은 주소를 돌려준다는 가정에 기댄다. 스레드 캐시나 arena 분할이 강한 할당기는 그 가정을 깨뜨릴 수 있다. 폴백(주소가 거꾸로 오면 새 블록셋)은 옳지만 블록셋마다 블록 한 개로 떨어지는 손해가 따른다. 계측해 둘 만하다. area_validate의 호출자. 헤더에 노출되어 있지만 지금 소스에 외부 호출자가 없다. dead code인지, 클라이언트 (db_*API) 코드에서만 부르는지 확인해 둘 만하다.
원본 분석 (raw/code-analysis/cubrid/common/)
섹션 제목: “원본 분석 (raw/code-analysis/cubrid/common/)”code_analysis_Common_module_AREA.pdf— 1차 자료(한글, v1.0, hgryoo). 절 구성: AREA 구조 소개, AREA의 내부 구조, AREA 관련 함수, AREA 구조를 사용하는 CUBRID의 모듈들, TODO. 4쪽, 그림 셋(struct 모양, BLOCKSET 모양, modules-using-AREA 그림)._converted/common-module-area.pdf.md— 같은 자료의markitdown추출본.
형제 문서
섹션 제목: “형제 문서”knowledge/code-analysis/cubrid/cubrid-page-buffer-manager.md— AREA가 아니라db_private_alloc/malloc을 쓰지만,AREA_BLOCK안의LF_BITMAP인스턴스를 거친 클래스마다의 풀 + lock-free 비트맵이라는 같은 디자인 어휘를 함께 쓴다.knowledge/code-analysis/cubrid/cubrid-heartbeat.md— 별개 모듈. 본 문서의 양식 본보기로 인용된다.
교재 / 논문
섹션 제목: “교재 / 논문”- Database Internals(Petrov), 4장 Implementing B-Trees — B-tree 노드 캐시 자리에서 슬랩과 풀 할당을 가리킨다.
- Database System Concepts(Silberschatz, Korth, Sudarshan, 6판), 13장 Storage and File Structure — 같은 크기 할당기가 자리 잡는 buffer manager 풍경.
- Bonwick, The Slab Allocator: An Object-Caching Kernel Memory Allocator, USENIX Summer 1994 — 슬랩의 정전이 되는 논문. 클래스마다의 캐시와 constructor / destructor 패턴을 들여놓았고, AREA는 그 패턴을 단순하게 다시 짠 모양이다.
CUBRID 소스 (/data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”src/base/area_alloc.h— 공개 타입과 API.src/base/area_alloc.c— 구현.src/base/lockfree_bitmap.{hpp,cpp}— 셀 단위의 lock-free 할당을 떠맡는 블록마다의 비트맵.src/object/object_primitive.c—Value_area설치와pr_make_value/pr_clear_value호출 자리.src/object/object_domain.c—tp_Domain_area.src/object/object_template.c—Template_area(객체)와Assignment_area.src/object/class_object.c—Template_area(스키마).src/object/set_object.c—Set_Ref_Area와Set_Obj_Area.src/object/work_space.c—Objlist_area.src/transaction/boot_sr.c,src/transaction/boot_cl.c— 부팅 시area_init, 종료 시area_final호출.src/executables/unittests_area.c— 멀티스레드area_alloc/area_free스트레스 테스트.