무엇을 관리하는가
memblock은 부팅 초기에 사용하는 물리 메모리 구간 관리자다. 이 시점에는 buddy allocator가 아직 사용 가능한 모든 struct page와 zone을 준비하지 못했고, slab allocator도 정상적인 커널 heap 역할을 할 수 없다. 그런데 page table, NUMA 자료구조, sparse memory metadata, initrd 처리에는 이미 메모리가 필요하다. memblock은 이 짧은 공백을 메운다.
일반적인 free-list allocator처럼 “남은 블록 목록”을 직접 보관하지 않는다는 점이 가장 중요하다. memblock은 펌웨어에서 확인한 RAM을 memory 배열에, 현재 쓰면 안 되는 물리 구간을 reserved 배열에 기록한다. 할당 가능한 구간은 두 배열의 차집합으로 그때그때 계산한다.
memblock.memory에 등록한다.memblock.reserved에 넣는다.memblock의 수명은 길지 않지만, 이때 잘못 예약한 한 페이지가 뒤의 page table 손상이나 부팅 중 무응답으로 이어질 수 있다.
| 항목 | 저장되는 내용 | 오해하기 쉬운 점 |
|---|---|---|
| memory | 커널이 RAM으로 취급할 수 있는 물리 주소 구간 | 현재 비어 있다는 뜻이 아니다. 그 안에 reserved 구간이 들어 있을 수 있다. |
| reserved | 이미 사용 중이거나 다른 이유로 early allocation에서 제외할 구간 | RAM이 아닌 hole 목록이 아니다. 대개 memory 안쪽의 제외 구간이다. |
| free | memory - reserved로 계산한 결과 | 별도 배열로 저장하지 않는다. iterator가 두 정렬 배열을 동시에 훑어 만든다. |
| physmem | 일부 architecture가 실제 설치된 물리 메모리 지도를 별도로 보존할 때 사용하는 배열 | CONFIG_HAVE_MEMBLOCK_PHYS_MAP이 있을 때만 존재하며 memory와 목적이 다르다. |
소스 코드 위치
아래 줄 번호는 Greg Kroah-Hartman의 Linux v6.18.37 tag를 기준으로 한다. 코드가 다른 stable patch level이라면 함수 이름은 같아도 줄 번호는 조금 달라질 수 있다.
| 파일 | 주요 심볼 | 확인할 내용 |
|---|---|---|
| include/linux/memblock.h | memblock_region, memblock_type, iterator macro | 자료구조, flags, physical/virtual allocation API의 반환형 |
| mm/memblock.c | memblock_add_range(), memblock_isolate_range() | 정렬 배열의 삽입, 병합, 분할, 제거 |
| mm/memblock.c | __next_mem_range() | memory - reserved의 교집합 계산 |
| mm/memblock.c | memblock_alloc_range_nid() | 정렬, 주소 범위, NUMA fallback, reservation |
| drivers/of/fdt.c | early_init_dt_scan_memory() | DT의 memory node가 memblock bank가 되는 과정 |
| arch/arm64/mm/init.c | arm64_memblock_init(), bootmem_init() | linear map 범위 정리와 kernel/initrd 예약 |
| mm/mm_init.c | mm_core_init() | memblock_free_all() 호출과 buddy 전환 |
공식 문서: Boot time memory management
문서는 API의 의미를 확인하는 데 쓰고, 이 글의 함수 본문과 줄 번호는 v6.18.37 source tag에 고정했다.
자료구조와 불변 조건
전역 객체 memblock 안에는 memory와 reserved라는 두 memblock_type이 들어 있다. 각 type은 memblock_region의 연속 배열을 가리킨다. linked list가 아니라 배열이므로 binary search와 순차 순회에는 유리하고, 중간 삽입·삭제에는 memmove()가 필요하다. 부팅 초기에 region 수가 비교적 작다는 전제에 맞춘 선택이다.
struct memblock에서 실제 region까지의 연결reserved도 같은 구조를 사용한다. 두 배열은 서로 독립적이므로 동일 주소가 memory와 reserved 양쪽에 동시에 나타나는 것이 정상이다.
| 필드 | 의미 | 코드를 읽을 때 확인할 점 |
|---|---|---|
| base | region의 첫 물리 주소 | 끝 주소는 저장하지 않고 base + size로 계산한다. |
| size | byte 단위 길이 | 내부 표현은 반열린 구간이므로 마지막 byte는 base + size - 1이다. |
| cnt | 현재 유효한 region 수 | 초기 dummy entry는 size가 0이며 첫 실제 추가 때 정상 region으로 바뀐다. |
| max | regions 배열의 수용 가능한 entry 수 | cnt + 필요한 entry 수가 넘으면 배열을 두 배로 키운다. |
| total_size | type에 속한 모든 region size의 합 | 예약과 해제 때 증감하며 free memory 총량과 같지는 않다. |
| current_limit | MEMBLOCK_ALLOC_ACCESSIBLE 요청의 물리 주소 상한 | 호출자가 넘긴 max_addr보다 더 강한 상한이 될 수 있다. |
| bottom_up | 낮은 주소부터 찾을지 선택 | 기본값은 false라서 높은 주소부터 찾는다. |
모든 region은 base 오름차순이다. iterator가 두 배열을 한 번씩 전진할 수 있는 근거다.
같은 type 내부의 region은 서로 겹치지 않는다. 새 구간이 기존 구간과 겹치면 아직 덮이지 않은 조각만 삽입한다.
서로 붙어 있고 flags와 NUMA node가 같은 region은 병합한다. 같은 속성의 이웃을 두 entry로 남겨 두지 않는다.
[base, base + size)로 계산하면 바로 붙은 두 구간의 경계를 한 주소로 표현할 수 있고 길이 계산도 단순해진다.
arm64 부팅 경로
arm64에서는 DTB를 확인하는 순간부터 memblock이 쓰인다. setup_machine_fdt()는 DTB 자체의 물리 구간을 먼저 예약하고, early_init_dt_scan()이 memory node를 읽어 memblock_add()로 RAM bank를 등록한다. 이후 arm64_memblock_init()이 CPU의 물리 주소 폭과 linear map 크기에 맞지 않는 RAM을 잘라 내고 kernel image와 initrd를 예약한다.
memblock.memory에 추가한다.memblock_free_all()로 남은 free page를 buddy에 인계한다.paging_init()과 sparse_init()도 memblock에서 메모리를 가져가므로, 고정 예약을 끝내기 전에 buddy로 넘길 수 없다.
| 시점 | memory 배열 | reserved 배열 | 주요 소비자 |
|---|---|---|---|
| DT memory scan 직후 | 펌웨어가 기술한 RAM bank | DTB와 초기 firmware reservation | architecture memory 정리 코드 |
arm64_memblock_init() 이후 | linear map으로 접근 가능한 RAM | kernel, initrd, reserved-memory | page table, KASAN, sparsemem |
bootmem_init() 이후 | NUMA node와 zone 경계가 반영될 기반 | CMA, crashkernel, hyp 등 추가 예약 포함 | mm_core_init() |
memblock_free_all() 이후 | 대부분의 architecture에서 init 후 폐기 가능 | 예약 page는 buddy에 들어가지 않음 | buddy/slab/vmalloc |
핵심 원본 코드
긴 함수를 통째로 옮기기보다 알고리즘이 갈리는 부분을 발췌했다. 각 코드 아래의 라인별 주석은 발췌문 내부 번호와 실제 source 위치를 함께 읽도록 구성했다.
1. 세 계층의 자료구조
struct memblock_region {
phys_addr_t base;
phys_addr_t size;
enum memblock_flags flags;
#ifdef CONFIG_NUMA
int nid;
#endif
};
struct memblock_type {
unsigned long cnt;
unsigned long max;
phys_addr_t total_size;
struct memblock_region *regions;
char *name;
};
struct memblock {
bool bottom_up;
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
};
설명: region 하나는 물리 구간과 속성을 나타내고, type은 같은 목적의 region 배열을 관리하며, 최상위 memblock은 RAM과 예약 구간을 나란히 둔다. free list나 page 단위 bitmap은 이 구조 안에 없다.
2. 새 조각을 정렬 배열에 삽입
BUG_ON(type->cnt >= type->max);
memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn));
rgn->base = base;
rgn->size = size;
rgn->flags = flags;
memblock_set_region_node(rgn, nid);
type->cnt++;
type->total_size += size;
설명: idx 뒤의 entry를 한 칸씩 밀고 빈 자리에 새 region을 쓴다. 함수가 호출되기 전에 배열 용량이 확보되어 있어야 한다. total_size는 배열이 표현하는 전체 byte 수이며 entry 수와 별개다.
3. memblock_add_range()의 2회 순회
if (!nr_new)
return 0;
if (!insert) {
while (type->cnt + nr_new > type->max)
if (memblock_double_array(type, obase, size) < 0)
return -ENOMEM;
insert = true;
goto repeat;
} else {
memblock_merge_regions(type, start_rgn, end_rgn);
return 0;
}
설명: 첫 순회는 기존 region이 덮지 않은 조각 수를 세고, 필요하면 배열을 확장한다. 두 번째 순회가 실제 삽입을 한다. 삽입 중 배열을 확장하면 pointer와 index를 다시 계산해야 하므로 count 단계와 mutation 단계를 나눈 것이다.
4. 제거할 경계에서 region 분할
if (rbase < base) {
rgn->base = base;
rgn->size -= base - rbase;
type->total_size -= base - rbase;
memblock_insert_region(type, idx, rbase, base - rbase,
memblock_get_region_node(rgn), rgn->flags);
} else if (rend > end) {
rgn->base = end;
rgn->size -= end - rbase;
type->total_size -= end - rbase;
memblock_insert_region(type, idx--, rbase, end - rbase,
memblock_get_region_node(rgn), rgn->flags);
}
설명: 삭제할 주소가 region 가운데에 걸리면 먼저 경계에서 잘라야 한다. 아래쪽 경계와 위쪽 경계에서 각각 한 번 분할할 수 있으므로 새 entry는 최대 두 개 필요하다.
5. memory - reserved의 교집합 계산
r = &type_b->regions[idx_b];
r_start = idx_b ? r[-1].base + r[-1].size : 0;
r_end = idx_b < type_b->cnt ? r->base : PHYS_ADDR_MAX;
if (r_start >= m_end)
break;
if (m_start < r_end) {
if (out_start)
*out_start = max(m_start, r_start);
if (out_end)
*out_end = min(m_end, r_end);
return;
}
설명: type_b의 region 자체가 아니라 “각 reserved region 앞의 빈 공간”을 만든 뒤 memory region과 겹치는 부분을 반환한다. 두 배열이 정렬되어 있으므로 끝난 쪽의 index만 전진하면 된다.
6. 기본 top-down 후보 주소 계산
for_each_free_mem_range_reverse(i, nid, flags, &this_start,
&this_end, NULL) {
this_start = clamp(this_start, start, end);
this_end = clamp(this_end, start, end);
if (this_end < size)
continue;
cand = round_down(this_end - size, align);
if (cand >= this_start)
return cand;
}
설명: 높은 free range부터 역순으로 본다. 요청 크기를 range 끝에서 빼고 아래 방향으로 정렬한 주소가 시작 경계 안에 있으면 그 주소가 후보가 된다. 이 함수는 찾기만 하고, 실제 사용 확정은 뒤의 reserve가 담당한다.
7. 찾은 물리 구간을 예약해 allocation 확정
found = memblock_find_in_range_node(size, align, start, end, nid,
flags);
if (found && !__memblock_reserve(found, size, nid,
MEMBLOCK_RSRV_KERN))
goto done;
if (numa_valid_node(nid) && !exact_nid) {
found = memblock_find_in_range_node(size, align, start, end,
NUMA_NO_NODE, flags);
if (found && !memblock_reserve_kern(found, size))
goto done;
}
설명: 주소를 찾았다는 사실만으로 할당이 끝나지 않는다. 같은 구간이 다음 요청에 다시 보이지 않도록 reserved에 넣어야 성공이다. 특정 node 요청이 실패하고 exact_nid가 거짓이면 전체 node로 범위를 넓혀 한 번 더 시도한다.
8. arm64에서 kernel과 initrd 예약
memblock_add(base, size);
memblock_clear_nomap(base, size);
memblock_reserve(base, size);
memblock_reserve(__pa_symbol(_text), _end - _text);
if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) {
initrd_start = __phys_to_virt(phys_initrd_start);
initrd_end = initrd_start + phys_initrd_size;
}
early_init_fdt_scan_reserved_mem();
설명: linear map 정리 과정에서 빠졌던 initrd 구간을 접근 가능한 RAM으로 다시 넣은 뒤 예약한다. kernel image도 물리 주소로 변환해 예약하고, 마지막에는 DT의 /reserved-memory node까지 처리한다.
9. free range를 buddy allocator로 넘김
for_each_free_mem_range(i, NUMA_NO_NODE, MEMBLOCK_NONE,
&start, &end, NULL)
count += __free_memory_core(start, end);
void __init memblock_free_all(void)
{
unsigned long pages;
free_unused_memmap();
reset_all_zones_managed_pages();
pages = free_low_memory_core_early();
totalram_pages_add(pages);
}
설명: 마지막까지 reserved로 남은 구간은 iterator 결과에 나오지 않는다. 차집합에 남은 byte range만 PFN 경계로 맞춘 뒤 buddy에 반환하고, 반환한 page 수를 전역 RAM 통계에 더한다.
구간 추가와 예약
memblock_add()와 memblock_reserve()는 모두 내부적으로 memblock_add_range()를 사용한다. 차이는 어느 type에 넣느냐뿐이다. 전자는 memblock.memory, 후자는 memblock.reserved를 대상으로 한다.
기존 구간과 겹치면 새 속성으로 덮어쓰지 않는다
새 구간은 기존 region과 겹쳐도 허용된다. 그러나 겹친 부분의 기존 flags와 node ID를 바꾸지는 않는다. 아직 어떤 기존 region에도 포함되지 않은 조각만 새 entry로 삽입한다. 이 규칙 덕분에 먼저 등록된 더 구체적인 속성이 뒤의 넓은 범위 추가로 사라지지 않는다.
추가 요청의 flags나 node가 기존 구간과 다르면 마지막 병합은 일어나지 않는다. 주소가 붙어 있다는 조건만으로는 부족하다.
왜 먼저 세고 나중에 삽입하는가
memblock_add_range()는 최악의 경우 새 구간 하나가 기존 region 사이의 여러 빈틈을 모두 채워 많은 entry를 만들 수 있다. 배열 공간이 충분하다고 확정할 수 없으면 첫 순회에서 필요한 entry 수 nr_new만 센다. 공간이 부족하면 memblock_double_array()가 더 큰 배열을 확보하고 기존 내용을 복사한 뒤, 두 번째 순회에서 실제로 삽입한다.
reserved 배열을 키우는 메모리 자체도 아직 memblock에서 가져올 수 있다. 새로 예약하려던 구간과 region 배열 저장 공간이 겹치면 예약 정보가 자기 자신을 덮게 된다. 그래서 memblock_double_array()는 new_area_start와 new_area_size를 받아 그 구간을 피해 새 배열을 놓고, 배열을 옮긴 뒤 그 저장 공간도 MEMBLOCK_RSRV_KERN으로 예약한다.
병합 조건
하나라도 다르면 두 region을 유지한다. 반대로 겹침이 남아 있다면 BUG_ON(this->base + this->size > next->base)가 발생한다. 즉, merge 함수는 정렬·비중첩 불변 조건을 확인하는 방어선이기도 하다.
분할·제거·해제
배열 중간의 일부만 지우려면 바로 entry를 삭제할 수 없다. 먼저 요청 범위의 시작과 끝이 region 경계가 되도록 memblock_isolate_range()가 자른다. 그 결과 [start_rgn, end_rgn) index 범위에는 삭제 대상만 남고, memblock_remove_range()가 뒤에서 앞으로 entry를 제거한다.
[0x3000, 0x5000) 제거를 위한 isolate반열린 구간이므로 0x5000은 삭제 대상이 아니라 오른쪽 region의 첫 주소다.
memblock_remove()와 memblock_free()는 대상 배열이 다르다
| API | 변경하는 배열 | 결과 |
|---|---|---|
| memblock_remove(base, size) | memory | 해당 물리 구간을 RAM 후보 자체에서 제외한다. 이후 free 계산에도 나타나지 않는다. |
| memblock_phys_free(base, size) | reserved | 예약만 해제한다. 여전히 memory에 포함되어 있다면 다음 memblock allocation 후보가 된다. |
| memblock_free(ptr, size) | reserved | virtual pointer를 __pa()로 바꿔 memblock_phys_free()를 호출한다. |
| memblock_free_late(base, size) | buddy allocator | memblock 인계 뒤 늦게 해제하는 경로로, page를 직접 buddy 쪽에 반환한다. |
memblock_free()는 reserved 배열에서 구간을 뺄 뿐이다. 그 시점에 buddy allocator로 직접 반환하지 않는다. buddy 인계는 뒤의 memblock_free_all()에서 일어난다.free range 계산
for_each_free_mem_range()는 __next_mem_range()에 memory를 포함 집합 type_a로, reserved를 제외 집합 type_b로 넘긴다. 함수는 memory entry의 index를 64-bit iterator 하위 32-bit에, reserved 사이 빈 공간의 index를 상위 32-bit에 보관한다.
실제 구현은 reserved 배열에서 [0, first.base), [prev.end, next.base), [last.end, PHYS_ADDR_MAX)를 만들고 각 memory region과 교차시킨다.
두 배열이 모두 주소 순으로 정렬되어 있기 때문에 이미 끝난 구간으로 되돌아갈 필요가 없다. memory 끝이 먼저 오면 memory index를, 빈 공간 끝이 먼저 오면 reserved-gap index를 전진시킨다. 따라서 모든 entry를 한 번씩 훑는 선형 순회가 가능하다.
flags와 NUMA 조건은 교집합 계산 전에 should_skip_region()에서 memory region 단위로 적용된다. 따라서 “주소상 free”여도 NOMAP이거나 다른 NUMA node인 구간은 요청 조건에 따라 건너뛸 수 있다.
물리 주소 배정
allocation은 두 단계다. 먼저 free iterator에서 후보 주소를 찾고, 그 물리 범위를 reserved 배열에 넣어 다음 요청이 재사용하지 못하게 한다. 후보 탐색 성공과 reserve 성공이 모두 성립해야 할당 성공이다.
top-down과 bottom-up
| 정책 | 후보 계산 | 특징 |
|---|---|---|
| top-down | round_down(this_end - size, align) | 높은 free range부터 낮은 방향으로 찾는다. memblock 전역의 기본 정책이다. |
| bottom-up | round_up(this_start, align) | 낮은 free range부터 높은 방향으로 찾는다. architecture가 정책을 전환한 경우 사용한다. |
top-down 계산은 0x8000 - 0x1800 = 0x6800을 아래로 0x1000 정렬해 0x6000을 고른다.
반환형을 보고 API 계열을 구분한다
| API 계열 | 성공 반환값 | 실패 | 초기화 |
|---|---|---|---|
memblock_phys_alloc*() | phys_addr_t 물리 주소 | 0 | 호출 API에 따라 다름 |
memblock_alloc*() | void * linear-map virtual 주소 | NULL 또는 panic 계열 | 일반 계열은 zeroing, _raw는 zeroing 안 함 |
memblock_alloc_or_panic() | void * | 부팅 중단 | zeroing |
memblock_alloc_internal()은 physical allocation 결과를 phys_to_virt()로 변환한다. 또한 max_addr를 current_limit보다 높게 사용할 수 없게 자르고, 선호한 min_addr 위에서 실패하면 0부터 다시 시도한다. 이 fallback 때문에 min_addr를 절대 하한으로 해석하면 안 된다.
memblock_find_in_range_node()는 시작 주소를 최소 PAGE_SIZE로 올린다. 성공값 0을 실패 표시로 사용하므로 물리 주소 0의 첫 page를 allocation 후보에서 의도적으로 제외한다.
주소 계산 예제
다음은 32 MiB RAM을 가진 단순 보드에서 kernel, DTB, initrd를 예약한 뒤 1 MiB를 2 MiB 정렬로 top-down 할당하는 예다. 실제 보드의 주소는 다르지만 계산 방법은 같다.
| 종류 | 물리 구간 | 크기 |
|---|---|---|
| RAM | [0x40000000, 0x42000000) | 32 MiB |
| kernel | [0x40200000, 0x40A00000) | 8 MiB |
| DTB | [0x41000000, 0x41100000) | 1 MiB |
| initrd | [0x41400000, 0x41800000) | 4 MiB |
마지막 free range D의 끝 0x42000000에서 1 MiB를 뺀 0x41F00000을 2 MiB 경계로 내림 정렬하면 allocation A의 시작 주소는 0x41E00000이다.
1. free range D 선택
top-down 탐색이므로 가장 높은 [0x41800000, 0x42000000)을 먼저 검사한다.
2. 크기 반영
this_end - size = 0x42000000 - 0x00100000 = 0x41F00000이다.
3. 정렬
round_down(0x41F00000, 0x00200000) = 0x41E00000이다. 후보 끝은 0x41F00000이고 free range 안에 들어간다.
4. 예약 확정
[0x41E00000, 0x41F00000)을 reserved 배열에 MEMBLOCK_RSRV_KERN으로 추가한다. 이후 iterator는 이 구간을 free로 반환하지 않는다.
flags와 NUMA
flags는 단순 표식이 아니라 region 병합 가능 여부와 allocation 후보 선택을 바꾼다. should_skip_region()은 memory type을 순회할 때 요청 flags와 region 속성을 비교한다.
| flag | 의미 | allocation/초기화 영향 |
|---|---|---|
| MEMBLOCK_HOTPLUG | 초기 firmware map에서 hot-unpluggable로 표시된 RAM | movable_node 정책에서 일반 allocation이 건너뛸 수 있다. |
| MEMBLOCK_MIRROR | mirrored memory | mirror 요청은 먼저 이 region만 찾고, 실패하면 경고 후 일반 memory로 fallback한다. |
| MEMBLOCK_NOMAP | kernel direct map에 넣지 않을 영역 | 명시적으로 NOMAP을 요청하지 않은 일반 free-range 순회에서 제외된다. |
| MEMBLOCK_DRIVER_MANAGED | firmware System RAM이 아니라 driver가 관리하는 memory | 요청 flags가 없으면 일반 allocation 후보에서 제외된다. |
| MEMBLOCK_RSRV_NOINIT | reserved 구간의 struct page를 완전히 초기화하지 않음 | 사용자가 해당 page metadata 초기화를 책임져야 한다. |
| MEMBLOCK_RSRV_KERN | 커널이 직접 사용하는 예약 | 모든 memblock allocation이 이 flag로 예약된다. |
| MEMBLOCK_KHO_SCRATCH | kexec handover용 scratch memory | handover 초기 단계에는 이 표시가 있는 영역만 allocation 후보가 될 수 있다. |
NUMA node 선택
memblock_add_node()는 region에 node ID를 바로 넣는다. 뒤늦게 node 정보를 알게 되면 memblock_set_node()가 대상 범위를 isolate한 후 내부 region의 nid를 바꾸고 다시 병합한다. 즉, node 경계가 기존 region 한가운데를 통과하면 region이 여러 entry로 나뉜다.
memblock_alloc_range_nid()의 exact_nid가 거짓이면 지정 node에서 실패한 뒤 NUMA_NO_NODE로 전체 memory를 다시 찾는다. 반대로 exact 요청은 다른 node로 넘어가지 않는다. bring-up 중 특정 node에 RAM이 없는 구성에서는 이 차이가 early allocation 실패 여부를 바꾼다.
buddy allocator 인계
bootmem_init()은 이름과 달리 모든 free page를 즉시 buddy에 넣는 함수가 아니다. arm64에서는 PFN 범위 설정, NUMA 초기화, sparse memory, zone size, CMA와 crashkernel 예약 등을 수행한다. 이 과정의 여러 함수가 여전히 memblock allocation을 사용한다.
실제 인계는 generic mm_core_init() 안의 memblock_free_all()에서 일어난다. 먼저 사용하지 않는 memmap 영역을 정리하고 zone의 managed page 수를 다시 계산할 준비를 한다. 이어서 for_each_free_mem_range()가 반환한 각 byte range를 PFN_UP(start)과 PFN_DOWN(end)로 page 경계 안쪽에 맞춘 뒤 __free_pages_memory()를 통해 buddy에 넣는다.
memory - reserved 결과를 얻는다.PFN_UP, 끝은 PFN_DOWN을 적용한다.totalram_pages_add(pages)로 실제 관리 page 수를 더한다.reserved 구간은 첫 단계의 iterator 결과에 없으므로 buddy free list에 들어가지 않는다.
CONFIG_ARCH_KEEP_MEMBLOCK이 꺼진 일반 구성에서는 초기화가 끝난 뒤 memblock 배열을 폐기할 수 있다. memblock API를 slab 준비 뒤에 실수로 호출하면 memblock_alloc_range_nid()가 WARN_ON_ONCE(slab_is_available())로 잡고 kzalloc_node() fallback을 사용한다. 이는 정상 사용 경로라기보다 init-only API 오용을 드러내기 위한 방어다.
부팅 로그로 확인
memblock의 핵심 작업은 tracefs가 준비되기 전에 일어난다. 따라서 런타임 function tracer보다 kernel command line의 memblock=debug와 earlycon이 먼저다.
console=ttyAMA0,115200 earlycon memblock=debug loglevel=8
early_memblock()은 memblock 인자에 debug 문자열이 있으면 전역 memblock_debug를 켠다. 이후 memblock_dbg()가 add, reserve, remove, allocation의 주소와 caller를 출력하고, arm64 bootmem_init() 끝의 memblock_dump_all()이 전체 배열을 보여 준다.
MEMBLOCK configuration:
memory size =
reserved size =
memblock_add:
memblock_reserve:
memblock_alloc_range_nid:
| 증상 | 우선 확인할 구간 | 해석 |
|---|---|---|
| kernel image 압축 해제 뒤 바로 멈춤 | DTB 주소와 RAM bank, kernel image reservation | DT memory node의 base/size 또는 bootloader 배치가 실제 DRAM과 맞는지 확인한다. |
| initrd를 찾지 못함 | initrd가 linear map 범위에 들어오는지, reserve가 남아 있는지 | arm64는 initrd와 kernel이 지나치게 멀면 initrd를 비활성화할 수 있다. |
cannot resize reserved array | memblock_allow_resize() 호출 전 reservation 수 | reserved-memory entry가 초기 정적 배열 용량을 너무 일찍 초과했는지 본다. |
| 초기 page table이 나중에 손상됨 | page table allocation 주소가 reserved에 들어갔는지 | 후보를 찾기만 하고 reserve하지 않았거나 잘못 free한 경로를 의심한다. |
| 표시 RAM이 예상보다 작음 | PA width, linear map clipping, mem=, NOMAP | firmware가 준 RAM과 kernel이 실제 접근 가능한 RAM을 구분한다. |
debugfs 조건: CONFIG_DEBUG_FS와 CONFIG_ARCH_KEEP_MEMBLOCK가 모두 켜진 구성은 부팅 뒤 /sys/kernel/debug/memblock/memory, reserved, physmem을 읽을 수 있다.
일반 배포 kernel은 memblock metadata를 init 후 버릴 수 있으므로 이 파일이 없는 것이 이상한 것은 아니다.
자주 틀리는 부분
memory에는 kernel과 initrd가 차지한 구간도 포함될 수 있다. free 여부는 반드시 reserved와 함께 계산해야 한다.
reserve는 RAM 안의 임시·영구 사용 구간을 제외하고, remove는 그 주소를 관리 가능한 RAM 지도에서 없앤다.
내부 구간은 [base, base + size)다. 로그 출력만 사람이 보기 쉽게 base + size - 1을 사용할 수 있다.
memblock_free()가 즉시 buddy에 반환한다고 생각함early free는 reservation 제거다. 실제 page allocator 인계는 memblock_free_all()에서 일어난다.
memblock_phys_alloc*() 반환값에는 직접 pointer dereference를 하면 안 된다. memblock_alloc*()은 linear-map virtual pointer를 반환한다.
후보 시작 주소가 align 배수여야 하며, 정렬한 뒤에도 candidate + size <= range end가 성립해야 한다.
NOMAP은 direct mapping에서 제외하는 memory 속성이다. reserved 여부와는 별개 축이며 iterator flags에 따라 선택이 달라진다.
memblock metadata는 폐기될 수 있다. 정상 runtime allocation은 buddy/slab/vmalloc API를 사용해야 한다.
코드 검토 항목
- 새 RAM bank와 reservation이 모두 byte overflow 없이
[base, base + size)로 표현되는가? - 새 구간 추가 후 region 배열이 base 오름차순이고 같은 type 내부에 겹침이 남지 않는가?
- 서로 붙은 region의 flags와 NUMA node가 같은 경우 병합되는가?
- 배열 resize 전에 firmware reserved-memory와 initrd 위치를 충분히 알고 있는가?
- allocation 후보를 찾은 뒤 반드시 같은 범위를 reserved에 넣는가?
- 호출자가 원하는 값이 물리 주소인지 linear-map virtual pointer인지 반환형으로 확인했는가?
min_addr,current_limit, NUMA fallback, mirror fallback 중 어느 조건이 절대 조건이고 어느 조건이 선호 조건인지 구분했는가?- arm64의 physical address width와 linear map 크기로 잘려 나가는 RAM이 있는가?
- kernel, DTB, initrd, page table, CMA, crashkernel, reserved-memory가 buddy 인계 전에 모두 예약되어 있는가?
- 초기 free와 late free를 구분하고 buddy가 준비되기 전 page allocator API를 호출하지 않는가?