ch8. kernel address space
커널은 귀한 메모리를
어떻게 낭비 없이 나누어 줄까?
4KB 단위의 페이지 프레임, 잘게 쪼갠 슬랩 객체, 그리고 선형 주소로 절묘하게 포장한 비연속 영역까지. 성능과 단편화 방지, 하드웨어 제약, 캐시 효율, 심지어 비상 상황까지 아우르는 Linux 2.6 커널 메모리 관리의 거대한 숲을 함께 조망해 봅니다.
큰 그림: 커널은 왜 메모리 관리자가 되어야 할까?OVERVIEW
우리가 1GB의 RAM을 가지고 있다고 해서 커널이 "이 1GB를 내 마음대로 다 써야지"라고 할 수 있을까요? 그렇지 않습니다. 일부 공간은 하드웨어가 미리 예약하고, 또 다른 일부는 커널 자체의 코드와 고정된 자료구조가 차지하게 됩니다. 그렇다면 남은 귀중한 '동적 메모리'를 어떻게 낭비 없이, 빠르고 안전하게 나누어 줄 수 있을까요?
4KB 단위로 연속된 페이지 프레임을 통째로 할당하고 해제합니다. 이 과정의 핵심에는 버디 시스템(Buddy System)이 있습니다.
수십에서 수백 바이트짜리 자잘한 객체들은 슬랩 할당자(Slab Allocator)를 통해 빠르고 효율적으로 관리합니다.
물리적인 위치는 이리저리 떨어져 있더라도, 선형 주소상에서는 마치 연속된 것처럼 보이게 만듭니다. vmalloc이 대표적인 예시입니다.
비유하자면, 페이지 프레임 관리는 4KB짜리 규격 박스를 창고에 차곡차곡 쌓아 관리하는 일이고, 슬랩 할당자는 그 박스 안에 작은 부품들을 세밀하게 분류해 넣는 칸막이 서랍장입니다. 한편 비연속 메모리 관리는 실제 창고 위치는 뿔뿔이 흩어져 있어도, 장부상으로는 한 줄로 길게 이어진 선반처럼 보이게 만드는 마법 같은 기술입니다.
리눅스는 80x86 아키텍처에서 4KB 단위의 페이지 프레임을 표준 할당 단위로 사용합니다. 이렇게 하면 페이지 폴트 해석이 단순해지고, 디스크와 메모리 사이의 데이터 전송 효율도 높아지기 때문입니다. 오늘의 여정은 바로 "이 4KB짜리 페이지 프레임을 어떻게 추적하고 나누어 주는가?"에서 출발합니다.
핵심 O/X 퀴즈
1. 커널이 관리하는 동적 메모리는 전체 RAM에서 하드웨어 예약 영역과 커널 정적 영역을 제외한 나머지 공간이다.
2. 이 장은 커널이 사용자 프로세스를 어떻게 스케줄링하는지 다루고 있다.
3. 리눅스는 이 장의 기준에서 4KB 페이지 프레임을 메모리 할당의 표준 단위로 채택하고 있다.
페이지 디스크립터: 모든 페이지 프레임에는 이름표가 있다struct page / mem_map
수십만 개에 달하는 4KB 단위의 페이지 프레임을 하나하나 추적하기 위해, 리눅스는 각 페이지 프레임마다 일종의 이름표인 페이지 디스크립터(struct page)를 붙여둡니다. 이 디스크립터들은 mem_map이라는 거대한 배열에 순서대로 저장되며, 각 디스크립터는 32바이트 크기를 가져 전체 RAM 용량의 1%가 채 안 되는 공간만을 차지합니다.
- _count
- 참조 횟수를 기록합니다. −1이면 아무도 쓰지 않는 비어 있는(할당 가능한) 페이지를 뜻하고, 0 이상이면 어딘가에서 사용 중임을 의미합니다. 보통 코드에서는
page_count()를 호출하는데, 이는 _count 값에 1을 더해 반환합니다. - flags
- 페이지의 현재 상태를 나타내는 비트들의 묶음입니다. PG_locked(I/O 작업 중 잠금), PG_dirty(내용이 변경됨), PG_slab(슬랩에 소속됨), PG_highmem(HIGHMEM 존에 위치함) 등이 있으며, 메모리 노드나 존 정보도 여기에 함께 인코딩되곤 합니다.
- private
- 페이지가 자유 상태일 때는 버디 시스템의 블록 크기(order)를 기억해 두는 용도로 쓰이는 등, 상황에 따라 그 역할이 유연하게 변하는 필드입니다.
- mapping / index
- 해당 페이지가 페이지 캐시에 속해 있을 경우, 어떤 파일이나 익명 영역과 연결되어 있는지를 나타냅니다.
- lru
- 주로 LRU(Least Recently Used) 리스트를 연결하는 포인터로 쓰이지만, 슬랩 할당자나 버디 시스템 내부에서는 다른 구조를 엮는 데 재활용되기도 합니다.
mem_map은 커널이 RAM 전체 구역을 파악하기 위해 관리하는 거대한 장부와 같습니다. 사람으로 치면 주민등록부, 창고로 치면 정밀한 재고관리표인 셈이죠. 재미있는 점은 같은 칸(필드)이라도 페이지가 지금 비어 있는지, 슬랩에 소속되어 있는지에 따라 적어두는 내용의 의미가 달라진다는 것입니다.
핵심 O/X 퀴즈
1. mem_map 배열은 물리 RAM에 존재하는 페이지 프레임 개수만큼의 디스크립터를 전부 저장하고 있다.
2. _count 값이 -1이라면, 이는 해당 페이지 프레임을 이미 여러 프로세스가 무분별하게 공유하고 있다는 경고이다.
3. flags 필드 안에는 페이지의 단순한 상태뿐 아니라, 페이지가 속한 메모리 노드와 존 정보까지 함께 인코딩될 수 있다.
NUMA와 메모리 존: RAM은 평평한 운동장이 아니다NUMA / ZONE_DMA / ZONE_NORMAL / ZONE_HIGHMEM
복잡한 멀티프로세서 시스템에서는 CPU가 어느 위치의 메모리에 접근하느냐에 따라 물리적인 접근 속도가 미묘하게 달라집니다. 이런 구조를 가리켜 NUMA(Non-Uniform Memory Access) 모델이라고 부릅니다. 이 때문에 전체 물리 메모리는 여러 개의 노드(Node)로 나뉘며, 각 노드는 pg_data_t 구조체로 표현되어 체인처럼 연결됩니다. 다만 80x86 UMA 기반의 일반 PC에서는 커널 코드 구조의 일관성을 위해 전체를 단일 노드(node 0)로 취급합니다.
과거 오래된 ISA 버스 장치들은 설계상 RAM의 처음 16MB 공간까지만 직접 주소를 지정할 수 있었습니다. 이를 배려해 DMA 전용으로 남겨둔 특별 구역입니다.
커널이 복잡한 과정 없이 선형 주소로 곧장 접근할 수 있는 가장 넓고 편안한 일반 구역입니다.
32비트 시스템의 한계 탓에 커널의 선형 주소 공간이 부족하여 항시 직접 연결해둘 수 없는 먼 구역입니다. 이곳에 접근하려면 잠시 '매핑 창문'을 열어야만 합니다.
왜 하필 896MB일까요?: 32비트 리눅스 환경에서 커널은 4GB 선형 주소 공간 중 상단의 1GB만을 자신의 몫으로 사용합니다. 이 1GB 안에는 물리 메모리와 곧바로 연결할 약 896MB의 공간 외에도, 고정 매핑이나 vmalloc 등을 처리할 추가적인 여유 공간이 필요합니다. 그래서 물리 RAM이 896MB를 넘어가면, 커널 입장에서는 그 너머의 메모리를 항시 직접 내려다볼 수 없게 되는 'HIGHMEM 문제'가 발생하게 됩니다.
이렇게 나뉜 각 존은 자신만의 존 디스크립터(Zone Descriptor)를 가집니다. 여기에는 free_pages(남은 페이지 수), 수위 조절을 위한 세 가지 워터마크(pages_min, pages_low, pages_high), CPU별 전용 캐시인 pageset, 그리고 버디 시스템을 관장하는 free_area 등이 꼼꼼하게 기록되어 있습니다. 즉, 존은 단순한 주소의 범위가 아니라 "이 구역 안의 메모리를 어떻게 할당하고, 얼마나 아껴두며, 언제 부족함을 느낄 것인가"를 종합적으로 판단하는 핵심 관리 단위입니다.
핵심 O/X 퀴즈
1. 리눅스는 80x86 UMA 시스템처럼 메모리 접근 속도가 균일한 환경에서도 구조의 일관성을 위해 전체를 단일 노드로 다룬다.
2. ZONE_DMA는 이름에서 알 수 있듯 896MB 이상의 초고속 대용량 데이터 전송을 위한 영역이다.
3. ZONE_HIGHMEM 내의 페이지 프레임들은 32비트 시스템에서 커널이 언제든 별도의 조치 없이 직접 접근할 수 있다.
예약 페이지 풀과 존드 페이지 프레임 할당자Figure 8-2 — Zoned Page Frame Allocator
인터럽트 핸들러나 임계 구역(Critical Region)처럼 도중에 멈추거나 잠들면 안 되는 코드에서는 대기 없이 즉각적으로 할당되는 원자적 할당(GFP_ATOMIC)이 반드시 필요합니다. 이런 요청이 실패하면 시스템이 치명적인 상태에 빠질 수 있으므로, 커널은 항상 최악의 상황을 대비해 예약 페이지 프레임 풀을 비워둡니다. 이 비상금의 최소 규모는 min_free_kbytes 변수에 담기며, 각 존이 관리하는 수위선(워터마크) 역시 이 값을 바탕으로 계산됩니다.
- pages_min
- 절대 침범해서는 안 될 예약 최솟값입니다. 이 선 아래로 물이 빠지면 커널은 메모리 할당에 극도로 엄격한 제한을 가합니다.
- pages_low
- pages_min의 약 5/4 수준입니다. 여유 공간이 이 선 아래로 떨어지면 커널은 슬슬 위기감을 느끼고 kswapd 데몬을 깨워 백그라운드에서 메모리 회수 작업을 시작합니다.
- pages_high
- pages_min의 약 3/2 수준입니다. 여유 공간이 이 선을 넘어서면 커널은 "메모리 상태가 안정적이다"라고 안심합니다.
Figure 8-2의 논리적 구조를 위에서부터 내려다보면, 가장 상층부에 존 할당자(Zone Allocator)가 자리 잡고 있습니다. 그 아래로 각 존마다 단일 페이지 요청을 빠르게 처리하는 CPU별 페이지 프레임 캐시와, 연속된 여러 페이지를 묶어서 관리하는 버디 시스템(Buddy System)이 진을 치고 있습니다. 즉, 메모리 요청이 들어오면 존 할당자가 "어느 존에서 빼줄까"를 결정하고, 선택받은 존 내부의 버디 시스템이나 캐시가 실제로 메모리를 떼어주는 구조입니다.
- alloc_pages(gfp, order)
- 2의 order제곱 개수만큼 연속된 페이지 프레임을 요청하고, 그중 첫 번째 페이지의 디스크립터 주소를 반환받습니다.
- alloc_page(gfp)
- order가 0, 즉 딱 한 장의 페이지를 요청하는 매크로입니다.
- __get_free_pages(gfp, order)
- 할당받은 첫 번째 페이지가 위치한 실제 '선형 주소'를 직접 반환받습니다.
- get_zeroed_page(gfp)
- 보안이나 초기화 목적을 위해 내용을 0으로 깨끗하게 닦아낸 단일 페이지를 받아옵니다.
- __free_pages / free_pages
- 다 쓴 페이지 프레임들을 다시 커널의 품으로 돌려보내는 해제 함수들입니다.
핵심 O/X 퀴즈
1. 원자적 메모리 할당(GFP_ATOMIC) 요청은 비록 실패할 가능성은 있지만, 결코 현재 실행 중인 프로세스를 잠재우거나 대기하게 만들지는 않는다.
2. pages_low와 pages_high 수위선은 시스템이 백그라운드 메모리 회수 작업을 시작하거나 멈추는 중요한 판단 기준이 된다.
3. 존 할당자(Zone Allocator)는 페이지 프레임을 배분하는 것뿐만 아니라, 그 내부의 수십 바이트짜리 작은 객체 배치까지 직접 관장한다.
GFP 플래그와 HIGHMEM 문제어떤 메모리를, 어떤 조건으로 받을 것인가
커널 코드가 "메모리 좀 주세요"라고 요청할 때, 사실 그 이면에는 아주 까다로운 조건들이 함께 따라붙습니다. 이 조건들의 묶음표를 가리켜 gfp_mask라고 부릅니다.
- __GFP_DMA
- 오직 ZONE_DMA 안에서만 메모리를 꺼내 와야 합니다.
- __GFP_HIGHMEM
- 저 멀리 있는 ZONE_HIGHMEM 영역의 메모리도 기꺼이 받아들일 수 있습니다.
- __GFP_WAIT
- 메모리가 부족하다면 당장 서두르지 않고 잠시 대기할 의향이 있습니다.
- __GFP_IO
- 빈 공간을 만들기 위해 디스크에 데이터를 쓰는 I/O 작업을 유발해도 괜찮습니다.
- __GFP_FS
- 파일시스템과 연계된 다소 무거운 작업까지도 허용합니다.
- __GFP_ZERO
- 할당받는 페이지가 무조건 0으로 꽉 채워져 있기를 원합니다.
- GFP_ATOMIC
- 단 1초도 쉴 수 없는 급박한 상황이므로 대기나 I/O 없이 즉각 할당을 원합니다.
- GFP_KERNEL
- 커널 내부에서 흔히 쓰이는 일상적인 조건들의 조합입니다.
이 중에서도 __GFP_DMA와 __GFP_HIGHMEM은 탐색 구역을 결정짓는 존 수식어(Zone Modifier)입니다. __GFP_DMA가 켜져 있으면 커널은 뒤도 돌아보지 않고 ZONE_DMA만 뒤집니다. 반면 __GFP_HIGHMEM이 켜져 있으면, 우선 ZONE_HIGHMEM을 찾아본 뒤 자리가 없으면 ZONE_NORMAL로, 거기도 없으면 최후에 ZONE_DMA로 내려오는 식의 유연한 탐색(Fallback)을 진행합니다. 왜 그럴까요? DMA 구역은 워낙 좁고 소중하기 때문입니다. 식당에 비유하자면 VIP 전용 좌석을 일반 손님에게 섣불리 내주었다가, 정작 VIP(구형 ISA 장치)가 도착했을 때 자리가 없어 쩔쩔매는 불상사를 막기 위함입니다.
HIGHMEM 영역의 치명적인 제약: __get_free_pages(GFP_HIGHMEM, 0)처럼 대놓고 '선형 주소'를 달라고 요구하는 함수에 HIGHMEM 페이지를 쥐여주면 시스템은 큰 혼란에 빠집니다. 앞서 말했듯 HIGHMEM 페이지는 커널 선형 주소 공간과 항시 연결되어 있지 않기 때문입니다. 그래서 HIGHMEM 페이지를 안전하게 다루려면 반드시 선형 주소가 아닌 alloc_pages() 등을 써서 페이지 디스크립터의 주소를 반환받아야만 합니다.
결론적으로 "물리적 RAM 공간이 남아 있다"는 말과 "커널이 당장 선형 주소로 그 공간을 찔러볼 수 있다"는 말은 결코 동의어가 아닙니다. LOWMEM 영역은 언제든 곧바로 접근 가능하지만, HIGHMEM 영역은 창고 저 깊숙한 곳에 실존하더라도 커널이 다가가기 위해서는 잠시 '매핑 창문'을 열어주는 수고로움이 필요합니다.
핵심 O/X 퀴즈
1. __GFP_HIGHMEM 플래그는 HIGHMEM 존의 페이지 프레임 역시 할당 후보군에 포함시켜도 좋다는 허락의 의미를 담고 있다.
2. __get_free_pages() 함수를 사용하면 HIGHMEM 구역에 할당된 페이지라 할지라도 언제나 유효한 선형 주소를 무리 없이 반환받을 수 있다.
3. 80x86 UMA 시스템에서 __GFP_DMA 옵션이 주어지면, 할당자는 오직 ZONE_DMA 안에서만 적합한 페이지를 찾아 헤맨다.
HIGHMEM 영구 커널 매핑: 오래 열어두는 창문kmap() / pkmap_page_table
영구 커널 매핑(Permanent Kernel Mapping)은 커널 선형 주소 공간의 끝자락에 위치한 128MB 구간 중 일부를 활용해 HIGHMEM 페이지와 연결하는 튼튼한 창문입니다. 임시 매핑에 비해 창문을 더 오래 열어둘 수 있다는 장점이 있지만, 만약 남은 빈 창문이 없다면 창문이 생길 때까지 현재 실행 중인 프로세스가 잠들며 대기해야 할 수도 있습니다. 그렇기 때문에 인터럽트 핸들러처럼 도중에 절대 잠들어선 안 되는 문맥(Context)에서는 이 매핑 방식을 사용할 수 없습니다.
- 0
- 아무도 쓰고 있지 않아 즉시 차지할 수 있는 완전히 빈 엔트리입니다.
- 1
- 지금 당장 쓰고 있는 사람은 없지만, 이전 사용자의 흔적(TLB 엔트리)이 아직 지워지지 않아 함부로 쓰면 안 되는 과도기 상태입니다.
- n (n > 1)
- 현재 HIGHMEM 페이지를 누군가 매핑 중이며, 정확히 n−1개의 커널 구성 요소가 이 창문을 함께 바라보고 있음을 뜻합니다.
page_address_htable 해시 테이블을 뒤져보고, 매핑 기록이 없으면 조용히 NULL을 반환합니다.kmap_high()로 진입합니다.map_new_virtual()이 비어 있는 pkmap 엔트리를 찾아 페이지 테이블에 물리 주소를 꾹꾹 적어 넣고 해시 테이블에 연결합니다.pkmap_map_wait라는 대기열 의자에 앉아 까무룩 잠이 듭니다. 나중에 누군가 kunmap()을 호출해 매핑을 내려놓으면 그제야 깨어나 작업을 재개합니다.비유하자면 영구 매핑은 도서관의 장기 열람석과 같습니다. 우리가 찾는 책(HIGHMEM 페이지)은 도서관 지하 깊숙한 서고에 있고, 열람석(커널 선형 주소 창문)의 개수는 한정되어 있습니다. 빈자리가 있으면 책을 올려두고 오래오래 볼 수 있지만, 자리가 다 찼다면 누군가 일어날 때까지 하염없이 기다려야만 하죠. 1분 1초가 급한 인터럽트 상황에서는 절대 이 방식을 택하면 안 되는 이유입니다.
핵심 O/X 퀴즈
1. kmap() 함수는 HIGHMEM 페이지에 대한 영구 매핑 창구를 열어주지만, 공간이 부족할 경우 해당 프로세스를 대기 상태로 블록시킬 위험이 있다.
2. pkmap_count 값이 0이라면, 어떤 이유로 해당 pkmap 엔트리가 망가져 현재 사용할 수 없다는 치명적인 오류를 의미한다.
3. page_address() 함수는 검사 대상이 HIGHMEM 페이지이고 현재 어떠한 매핑도 되어 있지 않다면 NULL을 반환할 수 있다.
HIGHMEM 임시 커널 매핑: 잠깐 열고 바로 닫는 창문kmap_atomic() / fix-mapped address
임시 커널 매핑(Temporary Kernel Mapping)은 자리가 없다고 대기열에서 잠드는 법이 결코 없습니다. 따라서 인터럽트 핸들러나 지연 함수(Deferrable Function)에서도 안심하고 사용할 수 있습니다. 다만 동시에 사용할 수 있는 창문의 수가 극단적으로 적으며, 이 창문을 열고 작업을 하는 도중에는 절대로 다른 일로 빠지거나 잠들어서는 안 된다는 무서운 제약이 따릅니다.
- CPU별 13개의 창문
- KM_BOUNCE_READ, KM_USER0, KM_PTE0 등 각 이름표는 커널의 특정 구성 요소가 어떤 목적의 창문을 쓸 것인지를 명시합니다. 엉뚱한 두 경로가 동일한 창문을 동시에 열어젖혀 꼬이는 일이 없도록 철저히 계산된 설계입니다.
- 고정 매핑 주소(fix-mapped address)
- 커널 선형 주소 공간상의 위치는 늘 일정하게 고정되어 있지만, 그 주소가 가리키는 실제 물리 페이지 프레임의 위치는 매번 휙휙 바뀔 수 있습니다.
임시 매핑은 일종의 '공용 손전등'과 같습니다. 칠흑 같은 HIGHMEM 영역을 살피기 위해 손전등을 잠깐 빌려 비춘 뒤 쏜살같이 반납해야 합니다. 만약 손전등을 쥔 채로 꾸벅꾸벅 졸아버린다면, 다른 누군가가 낚아채어 엉뚱한 방향을 비추는 순간 내가 유심히 보던 대상이 순식간에 바뀌어버리는 아찔한 상황이 벌어집니다. 임시 매핑을 사용하는 코드가 절대 블록되어서는 안 되는 결정적 이유입니다.
핵심 O/X 퀴즈
1. 임시 커널 매핑은 프로세스를 대기 상태로 만들지 않으므로, 촌각을 다투는 인터럽트 문맥 안에서도 무리 없이 호출할 수 있다.
2. kmap_atomic()으로 주소를 확보했다면, 이후 작업을 진행하다 중간에 잠시 블록(대기) 상태에 빠지더라도 데이터는 안전하게 보존된다.
3. 임시 매핑 기법은 CPU마다 할당된 고정 매핑 주소(fix-mapped address) 창을 절묘하게 활용한다.
버디 시스템의 큰 아이디어: 여기저기 흩어진 빈칸을 어떻게 합칠까BUDDY SYSTEM — EXTERNAL FRAGMENTATION
호텔에 단체 손님이 와서 "연속된 빈 방 8개 주세요!"라고 외칩니다. 마침 호텔 전체에 빈 방은 20개나 남아 있었지만, 안타깝게도 모두 이리저리 흩어져 있어 손님을 받을 수 없었습니다. 메모리 세계에서도 이와 똑같은 비극이 일어나곤 하는데, 이를 외부 단편화(External Fragmentation)라고 부릅니다.
리눅스 커널은 이런 외부 단편화의 골칫거리를 해결하고자 연속된 자유 블록들을 아주 체계적으로 모았다가 쪼개는 '버디 시스템'을 곁에 두고 있습니다. 왜 굳이 연속된 공간에 집착할까요? 첫째, 특정 DMA 버퍼처럼 물리적으로 무조건 이어져 있어야만 작동하는 하드웨어가 있기 때문입니다. 둘째, 자잘하게 쪼개진 페이지들을 엮다 보면 페이지 테이블을 너무 자주 건드려야 하고, 이는 TLB flush라는 값비싼 비용으로 되돌아옵니다. 셋째, 큼직하게 이어진 물리 메모리는 '큰 페이지(Huge Page)' 기술과 맞물려 TLB 미스 확률을 획기적으로 낮춰주는 효자 역할을 톡톡히 하기 때문입니다.
order별 자유 블록 리스트 (각 칸 = 2^order 페이지)
버디 시스템은 현재 남아 있는 자유 페이지 프레임들을 크기별로 총 11개의 리스트에 꼼꼼히 나누어 담아둡니다. 여기서 order k라는 것은 2의 k제곱 크기를 가진 페이지 블록을 뜻합니다 (가령 가장 큰 order 10은 1024페이지, 즉 4MB의 덩치를 자랑합니다). 누군가 특정 크기의 블록을 요청했는데 마땅한 매물이 없다면 어떻게 할까요? 과감하게 한 단계 더 큰 블록을 반으로 뚝 잘라 내어주고, 나중에 사용이 끝나 반환될 때 혹시 옆집에 사는 짝꿍(buddy) 블록도 비어 있다면 잽싸게 하나로 합쳐 다시 덩치를 키우는 영리한 방식을 취합니다.
마치 256페이지 크기의 블록을 주문받았는데 창고에 256짜리가 없으면, 512짜리 거대한 블록을 꺼내어 반으로 쪼갠 뒤 256은 손님에게 주고 남은 256은 다시 창고의 256 칸에 고이 모셔두는 셈입니다. 반대로 손님이 256을 다 쓰고 반납하러 오면, 혹시 창고에 원래 짝꿍이었던 256이 아직 비어있는지 쓱 쳐다봅니다. 짝꿍이 비어 있다면 둘을 철컥 결합해 512로 원상 복구하고, 내친김에 512 짝꿍까지 비어 있다면 다시 1024로 덩치를 불려 나가는 마법 같은 병합 쇼를 펼치는 것이죠.
핵심 O/X 퀴즈
1. 외부 단편화란, 시스템 전체에 남은 자유 페이지 수 자체는 충분하지만 그것들이 파편화되어 큰 연속 블록을 할당할 수 없는 뼈아픈 상황을 일컫는다.
2. 버디 시스템은 구현의 단순함을 위해 자유 페이지 프레임을 오직 3가지 크기의 리스트로만 투박하게 관리한다.
3. 버디 시스템에서 다 쓴 블록을 반환할 때, 조건만 잘 맞아떨어지면 짝꿍(buddy) 블록과 연쇄적으로 결합해 점점 더 거대한 자유 블록을 회복할 수 있다.
버디 시스템의 실제 자료구조와 할당·해제 흐름__rmqueue() / __free_pages_bulk()
Linux 2.6에서는 모든 존(Zone)마다 각자의 독립적인 버디 시스템을 한 세트씩 거느리고 있습니다. 각 존 디스크립터 안에 자리한 free_area[k] 배열은 2^k 크기의 자유 블록들을 꿰어놓은 이중 연결 원형 리스트(free_list)와, 현재 그 크기 블록이 몇 개나 남아 있는지 세어둔 nr_free 정보를 품고 있습니다. 묶여 있는 수많은 페이지 중에서 가장 첫 번째 페이지 디스크립터만이 lru 필드를 동원해 앞뒤 블록과 손을 맞잡고, private 필드에 자신의 자랑스러운 크기(order)를 은밀히 새겨넣습니다.
- 1단계
- 손님이 주문한 order 리스트를 들여다보고, 물건이 있다면 즉시 꺼내어 건네줍니다.
- 2단계
- 마땅한 물건이 없다면, 아쉬움을 뒤로하고 한 단계씩 더 큰 덩치의 리스트를 차근차근 위로 훑어 올라갑니다.
- 3단계
- 드디어 더 큰 블록을 발견하면, 도마 위에 올려놓고 필요한 크기가 나올 때까지 반의 반으로 쪼갠 뒤, 남은 조각들은 제각기 알맞은 order 리스트 방에 예쁘게 돌려보냅니다.
- buddy_idx 계산의 비밀
buddy_idx = page_idx ^ (1 << order)— 아주 우아한 비트 연산(XOR) 한 방으로 잃어버린 짝꿍의 시작 위치를 기가 막히게 짚어냅니다.- page_is_buddy() 검문소
- 찾아낸 짝꿍이 정말로 자유의 몸인지, 건드려선 안 될 예약 페이지는 아닌지, PG_private 깃발을 꽂고 있는지, 무엇보다 나와 크기(order)가 동일한 녀석인지를 깐깐하게 검사합니다.
- 감격의 병합
- 모든 조건이 완벽히 일치하면 짝꿍을 대기열 리스트에서 빼내어 둘을 꽉 껴안게 만듭니다. 그리고 한 단계 더 커진 order를 가슴에 품고 다시 상위 짝꿍을 찾아 나서는 훈훈한 반복 과정이 이어집니다.
핵심 O/X 퀴즈
1. Linux 2.6의 버디 시스템은 시스템 전체를 통틀어 단 한 개만 덩그러니 존재하여 모든 존을 일괄 관리한다.
2. 자유 블록 묶음의 가장 첫 번째 페이지 디스크립터가 가진 private 필드는, 자신이 속한 블록 덩어리의 크기(order)를 소중히 간직하는 데 사용된다.
3. __rmqueue()는 손님이 요청한 크기보다 훨씬 더 큰 블록을 찾더라도, 행여나 블록이 상할까 봐 절대 자르지 않고 통째로 넘겨준다.
Per-CPU 페이지 프레임 캐시: 단 한 장의 페이지 요청쯤은 눈 깜짝할 새에hot cache / cold cache
고작 페이지 한 장을 달라고 할 때마다 거대한 버디 시스템 전체에 자물쇠(락)를 채우고 부산을 떠는 것은 너무나도 무겁고 비효율적입니다. 그래서 각 메모리 존은 CPU마다 자신만의 가벼운 전용 보관함인 'CPU별 페이지 프레임 캐시'를 곁에 두고 있습니다.
- hot cache
- 방금 전까지 쓰여서 CPU 하드웨어 캐시에 데이터의 온기가 고스란히 남아 있을 가능성이 농후한 페이지들입니다. 커널이나 프로세스가 이 페이지를 받자마자 무언가 데이터를 마구 써내려 갈 작정이라면 이쪽이 단연 유리합니다.
- cold cache
- 반대로 DMA가 하드웨어 통로를 거쳐 느긋하게 데이터를 채워 넣을 예정이라면, CPU가 당장 그 데이터를 들여다볼 일이 없으므로 굳이 귀한 hot page를 내어줄 필요가 없습니다. 이때는 차갑게 식은 cold page를 건네주는 것이 현명한 선택입니다.
- count
- 현재 이 작은 보관함 안에 담겨 있는 페이지의 총 개수입니다.
- low / high
- 보충과 방출을 결정짓는 임계선입니다. 페이지가 low 밑으로 떨어지면 다급히 버디에게 달려가 한 움큼 얻어오고, high를 넘치게 채워지면 남는 잉여분을 다시 버디의 창고로 반납합니다.
- batch
- 창고와 교류할 때 한 번에 뭉텅이로 옮겨올 페이지의 수량입니다.
buffered_rmqueue() 함수는 누군가 order 0(단일 페이지)을 요청했을 때 이 per-CPU 캐시를 쓱 들여다봅니다. 요청 조건에 __GFP_COLD가 붙어 있다면 차가운 캐시 서랍을 열고, 없다면 따뜻한 캐시 서랍을 엽니다. 반대로 다 쓴 페이지가 되돌아올 때는 언제나 온기가 돌 것으로 간주하여 따뜻한 hot cache 서랍에 쏙 집어넣습니다.
이건 마치 셰프가 요리를 할 때 겪는 상황과 같습니다. 소금이 조금 필요할 때마다 멀리 떨어져 있는 거대한 메인 창고(버디 시스템)까지 땀을 뻘뻘 흘리며 걸어가는 대신, 조리대 바로 옆에 둔 작은 소금통(per-CPU 캐시)에서 핀치로 살짝 집어 쓰는 것과 완벽히 똑같은 이치입니다.
핵심 O/X 퀴즈
1. CPU별 페이지 프레임 캐시는 주로 덩치가 산만한 여러 장의 페이지 묶음 요청을 처리하는 데 최적화되어 있다.
2. cold cache는 할당받은 즉시 CPU가 열정적으로 데이터를 써내려 갈 예정인 페이지에 가장 찰떡궁합이다.
3. 캐시 구조체 안의 low, high, batch 변수는 소금통에 소금이 떨어졌을 때 창고에서 얼마나 퍼올지, 반대로 너무 많을 때 얼마나 되돌려놓을지를 판단하는 현명한 잣대 역할을 한다.
Zone Allocator: 어느 존에서, 얼마나 위험을 감수하고 떼어줄 것인가__alloc_pages() / zone_watermark_ok()
존 할당자(Zone Allocator)의 핵심 지휘관은 단연 __alloc_pages(gfp_mask, order, zonelist)입니다. 이 지휘관은 여러 존이 나열된 리스트(zonelist)를 순찰하며, 각 존의 상태가 괜찮은지 zone_watermark_ok()를 통해 타진해 봅니다. 검문을 통과하는 존이 나타나면 곧장 buffered_rmqueue()에게 눈짓을 보내어 실제 메모리 할당을 시도하게 만듭니다.
kswapd 데몬의 뺨을 때려 깨우고 비동기적인 페이지 청소 작업을 독촉합니다.__GFP_WAIT 옵션이 없어 기다릴 수 없는 성격이라면 눈물을 머금고 실패를 선언합니다. 기다릴 수 있다면 try_to_free_pages()를 발동해 스스로 소매를 걷어붙이고 강제로 공간을 비워봅니다. 이마저도 안 되면 결국 비장의 무기이자 사형 선고인 out_of_memory()(OOM Killer)를 호출합니다.깐깐한 심사관인 zone_watermark_ok()는 단순히 "전체 남은 페이지가 너의 요청량보다 많은가?"라는 1차원적인 질문만 던지지 않습니다. 요청한 양을 떼어주고 나서도 최소한의 비상금(min)이 넉넉히 남는지, 행여나 하위 존을 보호하기 위한 안전지대(lowmem_reserve)를 무례하게 침범하지는 않는지, 결정적으로 요청한 덩치(order)를 온전히 감당할 만큼 이어져 있는 '큰 덩어리 블록'들이 족히 남아 있는지를 아주 다각도로 날카롭게 검증합니다.
한편 해제 절차를 맡은 __free_pages()는 참조 카운터(_count)를 하나 살짝 줄여보는 것으로 임무를 시작합니다. 깎아낸 뒤에도 여전히 0 이상이면 어딘가에서 쓰고 있다는 뜻이므로 가볍게 손을 털고 물러납니다. 완전히 비워졌다면, 덩치가 1장(order 0)일 땐 per-CPU의 따뜻한 캐시 서랍에 쏙 넣어두고, 여러 장 묶음이라면 원래 고향인 버디 시스템으로 너그럽게 돌려보냅니다.
핵심 O/X 퀴즈
1. 존 할당자는 창고에 자리가 부족할 낌새가 보이면 kswapd를 깨워 청소를 시키거나 심지어 스스로 직접 회수 작업에 뛰어들기도 한다.
2. __alloc_pages()는 넘어온 리스트 따위는 무시하고, 오직 가장 안전해 보이는 ZONE_DMA만을 편애하여 거기서만 할당을 시도한다.
3. 단 한 장짜리 자잘한 페이지 프레임이 수명을 다해 반환될 때, 버디를 귀찮게 하지 않고 per-CPU의 따뜻한 캐시로 직행할 수도 있다.
메모리 영역 관리와 슬랩 할당자: 4KB 커다란 박스 안에서 굴러다니는 작은 부품들SLAB ALLOCATOR — INTERNAL FRAGMENTATION
커널이 매번 무식하게 4KB 크기의 큼직한 땅덩어리만 요구하는 것은 결코 아닙니다. 오히려 프로세스 디스크립터나 열려 있는 파일 객체처럼 불과 수십에서 수백 바이트 남짓한 자그마한 객체들을 쉴 새 없이 생성하고 폐기합니다. 만약 이런 조약돌 같은 객체 하나를 만들 때마다 버디 시스템을 졸라 4KB 페이지 전체를 독점하게 만든다면, 그 안에 남는 엄청난 잉여 공간들은 고스란히 버려질 것입니다. 바로 이런 어처구니없는 낭비를 내부 단편화(Internal Fragmentation)라고 꼬집습니다.
- 지독한 객체 재사용
- 다 쓰고 버린 객체를 무자비하게 폐기 처분하지 않습니다. 초기 상태와 거의 비슷한 온전한 형태로 캐시 구석에 곱게 모셔두었다가, 다음번 같은 요청이 오면 재빠르게 꺼내어 재활용합니다.
- 끼리끼리 타입별 관리
- 아무 객체나 한데 뒤섞지 않고, 오직 같은 종류의 객체들만 모아서 동일한 캐시(Cache)에 격리 수용합니다.
- CPU 지역성 존중
- 서로 다른 CPU가 하나의 캐시에 손을 집어넣다 부딪히는 락(Lock) 경쟁을 피하기 위해, CPU별 전용 보조 캐시(local cache)를 두어 하드웨어 캐시의 효율을 극한으로 끌어올립니다.
똑같은 타입의 객체들을 한데 모아둔 거대한 수납장입니다. "파일 객체 전용 수납장", "프로세스 구조체 전용 수납장"처럼 그 용도가 명확히 꼬리표로 붙어 있습니다.
수납장 안을 나누는 서랍 한 칸과 같습니다. 하나 또는 이어져 있는 여러 장의 페이지 프레임으로 구성되며, 캐시 안에 이런 서랍들이 층층이 쌓여 있습니다.
슬랩이라는 서랍 칸막이 안에 가지런히 놓여 있는 진짜 알맹이 커널 객체들입니다. 이미 임자가 있는 것과 아직 비어 있는 녀석들이 사이좋게 섞여 자리하고 있습니다.
핵심 O/X 퀴즈
1. 슬랩 할당자는 덩치가 작은 커널 객체들을 어떻게 하면 더 빠르고 알뜰하게 할당하고 재활용할 수 있을지 고민 끝에 탄생한 메커니즘이다.
2. 내부 단편화란, 전체 공간은 남아도는데 물리적으로 연속된 큰 덩어리의 자유 블록을 도무지 찾을 수 없는 딜레마를 지칭하는 용어이다.
3. 슬랩 할당자의 세계관에서 캐시(cache)란, 철저하게 같은 타입의 객체들만을 선별하여 모아두는 깐깐한 전용 수납장이다.
캐시 디스크립터와 슬랩 디스크립터: 슬랩 세계를 떠받치는 정교한 장부들kmem_cache_t / kmem_list3 / Figure 8-4·8-5
각각의 캐시(수납장)는 자신의 신상명세가 빼곡히 적힌 kmem_cache_t 디스크립터라는 꼬리표를 이마에 붙이고 있습니다.
- array
- 멀티 코어 시대에 발맞춰, CPU별로 따로 챙겨둔 아담한 개인용 보조 캐시(local cache)들을 가리키는 포인터 배열입니다.
- batchcount / limit
- 메인 수납장과 개인 보조 캐시 사이를 오갈 때 한 번에 뭉텅이로 나를 객체의 개수, 그리고 보조 캐시가 빵빵해질 수 있는 최대 한계치를 의미합니다.
- objsize
- 수납장에 담길 알맹이 객체 하나가 차지하는 정확한 크기입니다.
- num
- 슬랩(서랍) 한 칸에 알맹이 객체가 총 몇 개나 빼곡히 들어갈 수 있는지를 나타냅니다.
- gfporder
- 버디 시스템에 주문을 넣을 때, 슬랩 한 칸을 만들기 위해 도대체 몇 장의 페이지 프레임(2^order)을 끌어와야 하는지를 결정하는 지표입니다.
- ctor / dtor
- 객체가 처음 세상에 나올 때 단장해 주는 생성자와, 사라질 때 뒤처리해 주는 소멸자 함수 포인터입니다.
- colour*
- 하드웨어 캐시 라인의 병목을 교묘하게 피하기 위해 도입된 '슬랩 색칠놀이(Slab Coloring)' 관련 변수들입니다.
- slabs_full
- 단 하나의 빈자리도 없이 손님(객체)으로 꽉꽉 들어찬, 빈방 없는 만실 슬랩들의 명단입니다.
- slabs_partial
- 어느 정도 방은 찼지만 여전히 쏠쏠하게 빈자리가 남아 있는 기특한 슬랩들입니다. 새로운 손님이 찾아오면 무조건 이 명단부터 뒤져서 자리를 안내합니다.
- slabs_free
- 손님이 하나도 없어 텅텅 비어 있는 유령 슬랩들입니다. 최악의 경우 언제든 페이지 덩어리째 버디 시스템에 반납당할 1순위 후보이기도 합니다.
수납장 안의 서랍(슬랩) 하나하나를 책임지는 슬랩 디스크립터 역시 만만치 않습니다. 여기엔 list(위의 세 갈래 길 중 자신이 어디 속했는지 연결하는 끈), colouroff(첫 객체를 놓을 위치의 미묘한 여백), s_mem(첫 알맹이 객체의 실제 메모리 주소), inuse(현재 몇 개의 방이 찼는지 세어둔 숫자), free(다음에 바로 손님을 밀어 넣을 빈방 번호) 필드가 존재합니다. 재미있는 것은 이 슬랩 디스크립터가 위치하는 장소입니다. 서랍 안에 넉넉하게 자리가 남으면 서랍 내부(internal) 한쪽에 셋방을 차리지만, 객체 덩치가 너무 커서 자리가 비좁으면 서랍 바깥(external)의 전혀 다른 일반 캐시로 쫓겨나 신세를 지기도 합니다.
일반 캐시(General) vs 특수 캐시(Specific): kmem_cache라는 녀석은 정말 특이합니다. 무려 다른 캐시들의 꼬리표(디스크립터) 자체를 객체로 품고 있는 "장부를 관리하기 위한 거대한 장부 캐시"입니다. 또한 malloc_sizes 배열은 32바이트부터 최대 131072바이트까지 13단계의 크기별로, 각각 DMA용과 일반용 두 가지 맛으로 나누어 총 26개의 '규격화된 다이소 같은 일반 캐시'를 쫙 깔아두고 있습니다. 반면, 특정 구조체만을 위해 맞춤 제작되는 특수 캐시들은 kmem_cache_create()를 호출해 공방에서 수제작으로 깎아 만들고, 용도가 끝나면 kmem_cache_destroy()로 흔적 없이 철거합니다.
핵심 O/X 퀴즈
1. slabs_full, slabs_partial, slabs_free 리스트는 각 서랍(슬랩)의 빈방 유무에 따라 서랍들을 기가 막히게 분류해 두는 똑똑한 색인표이다.
2. 모든 슬랩 디스크립터는 그 크기나 상황을 불문하고 반드시 자신이 관리하는 서랍(슬랩) 바깥의 외딴 캐시에만 세들어 살아야 한다.
3. kmem_cache라는 이름의 캐시는 놀랍게도 다른 캐시의 디스크립터 자체를 보관하는, 관리자를 위한 관리자 격의 특수한 일반 캐시이다.
슬랩의 탄생과 소멸, 그리고 캐시 라인을 속이는 색칠의 마법cache_grow() / slab_destroy() / slab coloring
수납장(캐시)을 탈탈 털어봐도 내줄 빈방(객체)이 하나도 없다면, 커널은 부리나케 cache_grow()를 호출하여 새로운 서랍(슬랩) 한 칸을 창조해 냅니다.
gfporder에 적힌 크기만큼 연속된 빈 페이지들을 덥석 받아옵니다. 그리고 받아온 페이지들의 이마에 PG_slab이라는 소유권 도장을 쾅쾅 찍어줍니다.lru.next에는 자신이 속한 대빵 캐시 디스크립터 주소를, lru.prev에는 바로 위 슬랩 디스크립터 주소를 은밀히 적어 둡니다. 나중에 덜렁 객체 주소 하나만 쥐여줘도 "아, 나는 어느 수납장 몇 번째 서랍 출신이구나!" 하고 총알처럼 소속을 찾아가게 만들기 위한 기막힌 꼼수입니다.kmem_bufctl_t)마다 다음 빈방이 어디인지 가리키는 이정표 화살표를 달아둡니다. 맨 마지막 방에는 "더 이상 빈방 없음(BUFCTL_END)"이라는 팻말을 세워둡니다.반대로 쓸모가 다해 서랍을 부술 때는 slab_destroy()가 나섭니다. 방마다 돌아다니며 소멸자(dtor)를 불러 말끔히 정리를 시킨 뒤, kmem_freepages()로 뼈대가 되었던 페이지 프레임들을 자연의 품(버디 시스템)으로 돌려보냅니다.
- 객체의 오와 열 맞추기 (정렬)
- 기본적으로는 4바이트 단위로 줄을 세우지만,
SLAB_HWCACHE_ALIGN옵션을 켜주면 하드웨어의 L1 캐시 라인 규격에 맞춰 듬성듬성 널찍하게 자리를 잡습니다. 비록 사이사이에 버려지는 여백(공간)은 좀 생길지라도, 데이터에 접근하는 찰나의 속도를 비약적으로 끌어올리는 야심 찬 맞교환(Trade-off) 전략입니다. - 슬랩 색칠놀이 (Slab Coloring)
- 만약 모든 서랍(슬랩)마다 첫 번째 객체가 시작하는 오프셋 위치가 기계적으로 똑같다면, 하드웨어 캐시는 같은 줄(Line)의 방들만 미어터지는 최악의 병목에 직면합니다. 이를 피하기 위해 새 슬랩을 만들 때마다 첫 객체의 시작 위치를 요리조리 조금씩 밀어내어(색깔을 바꾸어) 캐시 라인 전역에 골고루 데이터가 흩어지게 만드는 신들린 분산 기술입니다.
colour_next변수가 이 색칠놀이의 붓을 쥐고 있습니다.
핵심 O/X 퀴즈
1. cache_grow() 함수는 텅 빈 수납장 상황을 타개하고자, 필요한 뼈대(연속 페이지 프레임)를 버디 시스템으로부터 기어코 얻어내어 새로운 슬랩을 지어 올린다.
2. 슬랩 색칠놀이(Slab Coloring)는 각 객체의 내부 데이터를 암호화하여 외부의 악의적인 공격으로부터 보호하는 첨단 보안 기능의 일종이다.
3. 객체 정렬(Alignment) 옵션을 켜면 빈 여백이 생겨나 메모리 낭비가 초래될 수 있지만, 반대급부로 하드웨어 캐시 적중률은 달콤하게 올라갈 수 있다.
개인 캐시의 위력, 그리고 우리가 사랑하는 kmalloc과 kfreearray_cache / malloc_sizes
수많은 CPU 코어들이 밥 먹듯이 객체를 꺼내가려 할 때 메인 수납장 하나에만 매달리면, 락(Lock)을 걸고 푸느라 길바닥에 버리는 시간이 너무 길어집니다. 이 참사를 막기 위해 커널은 캐시마다 CPU 각자의 전용 냉장고, 이른바 보조 캐시(local cache)를 따로 분양해 주었습니다. 이 개인 냉장고 안에는 바로 꺼내 쓸 수 있는 자유 객체들의 포인터 배열이 얌전히 들어차 있습니다.
- avail
- 현재 내 냉장고 안에 당장 집어 먹을 수 있는 객체의 수가 몇 개인지를 나타내며, 동시에 배열에서 다음번 물건을 꺼낼 인덱스 위치를 절묘하게 가리킵니다.
- limit
- 내 냉장고에 욱여넣을 수 있는 최대 객체 수(용량)입니다.
- batchcount
- 냉장고가 텅 비어서 메인 수납장으로 심부름을 갈 때, 한 번에 싹쓸이해 올 목표 수량입니다.
- touched
- 가장 최근에 내 냉장고 문을 한 번이라도 열어본 적이 있는지 기록해 두어, 너무 오랫동안 방치된 냉장고는 아닌지 감시합니다.
avail > 0이라면 락(Lock) 따윈 걸 필요도 없이 배열 맨 위 포인터를 채가듯 꺼내어 휙 던져줍니다. 어마어마하게 빠른 경로입니다.객체를 다 쓰고 버릴 때도 마찬가지입니다. kmem_cache_free()는 개인 냉장고에 쑤셔 넣을 자리가 있으면 가볍게 툭 던져놓고 퇴근합니다. 만약 한계치(limit)까지 꽉 차서 문이 닫히지 않는다면, cache_flusharray()를 불러 냉장고 속 객체들의 절반 정도를 공용 냉장고나 메인 서랍으로 우르르 반납해 속을 비워낸 뒤, 가져온 객체를 여유롭게 밀어 넣습니다.
우리가 자주 쓰던 kmalloc / kfree의 실체: 코드 작성 중 밥 먹듯 쓰는 kmalloc(size, flags)은 사실 malloc_sizes라는 다이소 같은 규격표를 쭉 훑어보고 "이 사이즈면 64바이트 캐시 서랍이 딱이겠군!" 하고 목표 캐시를 정한 뒤 은쩍 kmem_cache_alloc()의 어깨를 툭 치는 대리인에 불과합니다. 반대로 kfree(objp)는 객체 주소 하나만 딱 던져주면, 커널이 그 주소가 속한 페이지 디스크립터의 lru.next에 적힌 메모를 컨닝하여 이 객체의 원소속 캐시를 기가 막히게 찾아낸 뒤 kmem_cache_free()에게 반납 처리를 떠넘기는 우아한 연쇄 작전을 펼칩니다.
핵심 O/X 퀴즈
1. kmem_cache_alloc()은 호출을 받자마자 무거운 락을 덜컥 거는 대신, 현재 CPU의 개인 냉장고(local cache)부터 뒤져 빛의 속도로 응답하려 노력한다.
2. kmalloc()을 호출하면 인자로 넘긴 바이트 크기와 한 치의 오차도 없이 정확히 일치하는 사이즈의 갓 구운 새 페이지 프레임을 무조건 할당해 준다.
3. kfree()는 덜렁 객체 포인터 하나만 넘겨받아도, 그 객체가 사는 페이지의 디스크립터에 묻어둔 힌트를 역추적해 기어코 제집(캐시)을 찾아 해제를 지시한다.
메모리 풀과 비연속 메모리 영역: 최후의 동아줄과 가상 공간의 기적mempool / vmalloc
메모리 풀(Memory Pool)은 블록 장치 서브시스템 같은 깐깐한 커널 부서들이 메모리가 극도로 메말라버린 비상사태에서도 생존할 수 있도록, 아주 적은 양의 비상식량을 따로 빼돌려 창고 깊숙이 은닉해 두는 기법입니다. 전역적으로 누구에게나 열려 있는 원자적 할당 풀과는 결이 다르게, 철저히 '특정 부서(소유자)'만을 위해 미리 파놓은 사제 비상 방공호에 가깝습니다.
- min_nr / curr_nr
- 이 방공호가 무조건 보장해야 할 최소한의 비상 객체 숫자, 그리고 현재 남아 있는 객체의 숫자입니다.
- alloc / free
- 방공호의 객체를 어떻게 찍어내고 부술지를 규정한 하청업체(하위 할당자) 메서드입니다. 만약 슬랩 객체를 다룬다면 이 메서드는 자연스레 kmem_cache_alloc/free의 껍데기가 됩니다.
- mempool_alloc()의 뻔뻔함
- 처음엔 방공호에 손을 대지 않고 일단 바깥 세상(일반 하위 할당자)에다 대고 뻔뻔하게 할당을 요구합니다. 바깥 세상마저 굶주려 할당에 실패하면, 그제야 눈물을 머금고 자신의 방공호 문을 열어 비축분을 하나 꺼내 먹습니다.
- mempool_free()의 채워 넣기
- 다 쓴 객체를 반납할 때, 방공호 비축분(curr_nr)이 최소선(min_nr)에 못 미친다면 밖으로 내보내지 않고 얼른 방공호 창고에 되돌려놓습니다. 비축분이 이미 꽉 차 든든하다면 미련 없이 일반 할당자에게 던져 반환해 버립니다.
그리고 대망의 비연속 메모리 영역(Noncontiguous Memory Area)입니다. 이 기법은 실제 물리적인 땅 조각(페이지 프레임)들은 뿔뿔이 흩어져 있더라도, 커널이 내려다보는 선형 주소 공간의 거울 속에서는 마치 거대한 하나의 덩어리로 이어진 것처럼 환상을 만들어냅니다. 외부 단편화에 얽매이지 않고 거대한 영역을 확보할 수 있다는 치명적인 장점이 있지만, 그 환상을 유지하기 위해 끊임없이 페이지 테이블을 주물럭거려야 하는 무거운 비용, 그리고 오직 4KB 배수 크기로만 놀아야 한다는 엄격한 제약이 뒤따릅니다.
+8MB 버퍼 간격
VMALLOC_START ~ VMALLOC_ENDget_vm_area()가 앞장서서 위 선형 주소 지도상의 VMALLOC 구역 내에 큼지막한 가상의 빈터를 확보해 선점합니다.kmalloc()을 돌려 확보한 가상터 크기만큼의 페이지 디스크립터 포인터 배열을 짜임새 있게 만듭니다. 그러고는 alloc_page(GFP_KERNEL | __GFP_HIGHMEM)를 미친 듯이 반복 호출하여 뿔뿔이 흩어진 물리 페이지들을 여기저기서 닥치는 대로 긁어모읍니다. HIGHMEM 구석에 박힌 녀석들도 환영입니다.이렇게 만들어진 환상을 깨부술 때는 vfree() 나 vunmap()이 출동합니다. __vunmap()의 지휘 아래 remove_vm_area()가 디스크립터를 뒤져서 페이지 테이블의 다리를 하나하나 끊어내고, 필요하다면 빌려왔던 물리 페이지 프레임들과 잡다한 관리용 배열(vm_struct 등)마저 깡그리 해제해 버립니다.
마지막으로 정리하자면, 리눅스 커널 메모리 관리라는 거대한 퍼즐은 페이지 프레임이라는 크고 듬직한 단위, 슬랩 객체라는 잘게 부서진 섬세한 단위, 그리고 선형 주소의 환상으로 포장한 비연속 단위라는 세 가지 상반된 무기들을 상황의 맥락에 맞춰 자유자재로 꺼내어 조합하는 숭고한 기술입니다. 이는 속도와 성능, 단편화의 공포, 하드웨어의 무자비한 족쇄, 캐시의 짜릿한 효율, 그리고 시스템이 무너져 내리는 비상 상황까지 동시에 머릿속에 구겨 넣고 춤을 춰야 하는, 운영체제 설계의 가장 경이롭고 핵심적인 심장부라 할 수 있습니다.