Memblock은 Linux 커널 부팅 초기에 일반 메모리 할당자가 아직 사용 불가능한 상태에서 물리 메모리 영역을 관리하기 위한 메커니즘이다. 시스템 메모리를 "사용 가능한 메모리(memory)"와 "예약된 메모리(reserved)" 두 가지 컬렉션으로 나누어 관리하며, 펌웨어(E820/UEFI/Device Tree)가 전달한 물리 메모리 레이아웃을 파싱하여 초기 할당을 수행한다. 부팅이 완료되면 memblock_free_all()을 통해 Buddy Allocator에 남은 페이지를 모두 반환하고, CONFIG_ARCH_KEEP_MEMBLOCK이 설정되지 않으면 memblock 데이터 구조 자체를 해제한다.
memblock은 크게 세 가지 메모리 타입을 관리한다: memory(커널이 사용할 수 있는 물리 메모리), reserved(이미 할당/예약된 영역), physmem(실제 물리 메모리, 일부 아키텍처에서만 지원). 각 타입은 struct memblock_type으로 표현되며, 정렬된 memblock_region 배열을 포함한다. 할당은 bottom-up 또는 top-down 방식으로 수행되며, NUMA 노드 할당, 미러링, nomap 플래그 등을 지원한다.
일상 비유: memblock은 정식 창고 관리 시스템(Buddy Allocator)이 켜지기 전, 이사 현장에서 임시로 쓰는 큰 종이 배치도와 같다. 펌웨어가 넘겨준 건물 도면에서 사용할 수 있는 방은 memory에 표시하고, 커널 이미지, initrd, ACPI/EFI 테이블, crashkernel처럼 이미 점유한 방은 reserved에 표시한 뒤, 공사가 끝나면 남은 방 목록을 Buddy Allocator에 넘긴다.
소스 파일 경로:
mm/memblock.c ← 핵심 구현 (2768줄)
include/linux/memblock.h ← 공개 API 및 자료구조 정의 (624줄)
# memblock 디버그 활성화 (커널 파라미터)
# 부팅 시: memblock=debug
# 현재 memblock 상태 확인 (CONFIG_MEMBLOCK_DEBUG 활성화 시)
dmesg | grep -i memblock
# 물리 메모리 전체 크기 확인
cat /proc/meminfo | head -5
# Buddy Allocator에 반환된 후 메모리 현황
cat /proc/buddyinfo
# memblock이 관리하는 영역 확인 (debugfs)
sudo cat /sys/kernel/debug/memblock/memory 2>/dev/null
sudo cat /sys/kernel/debug/memblock/reserved 2>/dev/null
# 커널 부팅 시 memblock 메시지 확인
dmesg | grep -E 'BIOS-e820|Memory:|MEMBLOCK'
# NUMA 노드별 메모리 배치
numactl -H 2>/dev/null
# 펌웨어/커널이 본 물리 주소 공간
cat /proc/iomem | head -80
# memblock에 영향을 준 부팅 파라미터 확인
cat /proc/cmdline
# initrd, crashkernel, reserved-memory까지 함께 확인
dmesg | grep -Ei 'memblock|BIOS-e820|efi: mem|OF: fdt|reserved-memory|initrd|crashkernel'
# CONFIG_HAVE_MEMBLOCK_PHYS_MAP + debugfs 유지 시 physmem 확인
sudo cat /sys/kernel/debug/memblock/physmem 2>/dev/null
# 직접 매핑, 페이지 테이블, vmalloc 사용량 확인
grep -E 'Vmalloc|DirectMap|PageTables' /proc/meminfo
# x86 CONFIG_X86_PTDUMP + debugfs 활성화 시 커널 페이지 테이블 확인
sudo cat /sys/kernel/debug/kernel_page_tables 2>/dev/null | head -40
struct memblock_region {
phys_addr_t base;
phys_addr_t size;
enum memblock_flags flags;
#ifdef CONFIG_NUMA
int nid;
#endif
};
위치: include/linux/memblock.h:73-80. base와 size는 반열림 구간 [base, base + size)를 만들고, flags는 HOTPLUG/MIRROR/NOMAP 같은 속성을 담는다. CONFIG_NUMA에서는 nid가 함께 저장되어 초기 물리 주소 범위를 NUMA 노드로 묶을 수 있다.
struct memblock_type {
unsigned long cnt;
unsigned long max;
phys_addr_t total_size;
struct memblock_region *regions;
char *name;
};
위치: include/linux/memblock.h:90-96. cnt는 현재 유효한 region 개수이고 max는 배열 용량이다. total_size는 이 타입에 들어 있는 영역 크기의 합이며, regions는 base 기준으로 정렬된 memblock_region 배열을 가리킨다.
struct memblock {
bool bottom_up; /* is bottom up direction? */
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
};
위치: include/linux/memblock.h:105-110. bottom_up은 낮은 주소부터 찾을지 높은 주소부터 찾을지를 정하고, current_limit은 MEMBLOCK_ALLOC_ACCESSIBLE 요청의 상한으로 쓰인다. memory는 사용 가능한 RAM 목록, reserved는 이미 점유된 물리 주소 목록이다.
enum memblock_flags {
MEMBLOCK_NONE = 0x0, /* 특별한 요청 없음 */
MEMBLOCK_HOTPLUG = 0x1, /* 핫플러그 가능한 영역 */
MEMBLOCK_MIRROR = 0x2, /* 미러링된 영역 */
MEMBLOCK_NOMAP = 0x4, /* don't add to kernel direct mapping */
MEMBLOCK_DRIVER_MANAGED = 0x8, /* always detected via a driver */
MEMBLOCK_RSRV_NOINIT = 0x10, /* don't initialize struct pages */
MEMBLOCK_RSRV_KERN = 0x20, /* memory reserved for kernel use */
MEMBLOCK_KHO_SCRATCH = 0x40, /* scratch memory for kexec handover */
};
위치: include/linux/memblock.h:55-64. MEMBLOCK_NOMAP은 직접 매핑에서 제외되는 RAM을 표시하고, MEMBLOCK_RSRV_KERN은 memblock 할당 API가 예약한 커널용 영역에 붙는다. MEMBLOCK_DRIVER_MANAGED와 MEMBLOCK_KHO_SCRATCH는 일반 System RAM과 다르게 취급해야 하는 영역을 구분한다.
static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_MEMORY_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_RESERVED_REGIONS] __initdata_memblock;
struct memblock memblock __initdata_memblock = {
.memory.regions = memblock_memory_init_regions,
.memory.max = INIT_MEMBLOCK_MEMORY_REGIONS,
.memory.name = "memory",
.reserved.regions = memblock_reserved_init_regions,
.reserved.max = INIT_MEMBLOCK_RESERVED_REGIONS,
.reserved.name = "reserved",
.bottom_up = false,
.current_limit = MEMBLOCK_ALLOC_ANYWHERE,
};
위치: mm/memblock.c:123-140. 초기 배열 용량은 INIT_MEMBLOCK_MEMORY_REGIONS와 INIT_MEMBLOCK_RESERVED_REGIONS로 시작한다. 영역이 더 많이 필요할 때는 memblock_allow_resize() 이후 memblock_double_array()가 새 배열을 잡아 확장한다.
struct memblock_type physmem = {
.regions = memblock_physmem_init_regions,
.max = INIT_PHYSMEM_REGIONS,
.name = "physmem",
};
위치: mm/memblock.c:142-148. CONFIG_HAVE_MEMBLOCK_PHYS_MAP이 있는 아키텍처에서는 physmem이 실제 물리 메모리 목록을 별도로 보존한다. mem= 같은 제한으로 커널 사용 가능 메모리가 줄어도, 펌웨어가 보고한 실제 물리 메모리 관찰점은 physmem 쪽에 남을 수 있다.
// mm/memblock.c:609-713
static int __init_memblock memblock_add_range(struct memblock_type *type,
phys_addr_t base, phys_addr_t size,
int nid, enum memblock_flags flags)
역할: 새로운 메모리 영역을 memblock 타입에 추가한다. 기존 영역과 중복될 수 있으며, 중복 시 겹치지 않는 부분만 삽입한다. 추가 후 인접 호환 영역을 자동 병합한다.
분기 로직:
cnt * 2 + 1 <= max이면 바로 삽입 가능하며, 그렇지 않으면 크기 확장 후 반복memblock_double_array()로 배열 크기 2배 확장 가능if (!size)
return 0;
/* special case for empty array */
if (type->regions[0].size == 0) {
WARN_ON(type->cnt != 0 || type->total_size);
type->regions[0].base = base;
type->regions[0].size = size;
type->regions[0].flags = flags;
memblock_set_region_node(&type->regions[0], nid);
type->total_size = size;
type->cnt = 1;
return 0;
}
/*
* The worst case is when new range overlaps all existing regions,
* then we'll need type->cnt + 1 empty regions in @type. So if
* type->cnt * 2 + 1 is less than or equal to type->max, we know
* that there is enough empty regions in @type, and we can insert
* regions directly.
*/
if (type->cnt * 2 + 1 <= type->max)
insert = true;
위치: mm/memblock.c:619-642. 첫 region을 넣는 경우와 배열 여유가 충분한 경우를 먼저 빠르게 처리한다.
/* insert the remaining portion */
if (base < end) {
nr_new++;
if (insert) {
if (start_rgn == -1)
start_rgn = idx;
end_rgn = idx + 1;
memblock_insert_region(type, idx, base, end - base,
nid, flags);
}
}
if (!nr_new)
return 0;
/*
* If this was the first round, resize array and repeat for actual
* insertions; otherwise, merge and return.
*/
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;
}
위치: mm/memblock.c:684-712. 첫 pass는 필요한 region 수를 계산하고, 두 번째 pass는 삽입 후 인접한 같은 속성의 region을 병합한다.
// mm/memblock.c:308-328
static phys_addr_t __init_memblock memblock_find_in_range_node(
phys_addr_t size, phys_addr_t align,
phys_addr_t start, phys_addr_t end, int nid,
enum memblock_flags flags)
역할: 지정된 범위와 NUMA 노드에서 요청 크기의 빈 영역을 찾는다.
분기 로직:
end == MEMBLOCK_ALLOC_ACCESSIBLE → memblock.current_limit로 대체start는 최소 PAGE_SIZE (첫 페이지 할당 회피)memblock_bottom_up() → bottom-up / top-down 분기static phys_addr_t __init_memblock memblock_find_in_range_node(phys_addr_t size,
phys_addr_t align, phys_addr_t start,
phys_addr_t end, int nid,
enum memblock_flags flags)
{
/* pump up @end */
if (end == MEMBLOCK_ALLOC_ACCESSIBLE ||
end == MEMBLOCK_ALLOC_NOLEAKTRACE)
end = memblock.current_limit;
/* avoid allocating the first page */
start = max_t(phys_addr_t, start, PAGE_SIZE);
end = max(start, end);
if (memblock_bottom_up())
return __memblock_find_range_bottom_up(start, end, size, align,
nid, flags);
else
return __memblock_find_range_top_down(start, end, size, align,
nid, flags);
}
위치: mm/memblock.c:308-328. MEMBLOCK_ALLOC_NOLEAKTRACE도 접근 가능 상한과 같은 방식으로 current_limit을 사용하지만, 나중에 kmemleak 등록을 건너뛰는 의미가 추가된다.
// mm/memblock.c:1474-1546
phys_addr_t __init memblock_alloc_range_nid(phys_addr_t size,
phys_addr_t align, phys_addr_t start,
phys_addr_t end, int nid, bool exact_nid)
역할: 부팅 중 메모리를 할당하고 물리 주소를 반환한다.
분기 로직:
slab_is_available() → slab 할당자로 fallbackexact_nid == false → 모든 노드로 확장kmemleak_alloc_phys()로 누수 추적 제외if (WARN_ON_ONCE(slab_is_available())) {
void *vaddr = kzalloc_node(size, GFP_NOWAIT, nid);
return vaddr ? virt_to_phys(vaddr) : 0;
}
if (!align) {
/* powerpc에서 부팅 초기에는 WARN을 사용할 수 없음 */
dump_stack();
align = SMP_CACHE_BYTES;
}
again:
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;
}
if (flags & MEMBLOCK_MIRROR) {
flags &= ~MEMBLOCK_MIRROR;
pr_warn_ratelimited("Could not allocate %pap bytes of mirrored memory\n",
&size);
goto again;
}
위치: mm/memblock.c:1487-1518. slab 준비 이후의 우발적 호출은 kzalloc_node()로 우회하고, NUMA fallback과 mirror fallback은 같은 검색 함수를 다른 조건으로 다시 호출한다.
done:
/*
* Skip kmemleak for those places like kasan_init() and
* early_pgtable_alloc() due to high volume.
*/
if (end != MEMBLOCK_ALLOC_NOLEAKTRACE)
/*
* Memblock allocated blocks are never reported as
* leaks. This is because many of these blocks are
* only referred via the physical address which is
* not looked up by kmemleak.
*/
kmemleak_alloc_phys(found, size, 0);
/*
* Some Virtual Machine platforms, such as Intel TDX or AMD SEV-SNP,
* require memory to be accepted before it can be used by the
* guest.
*
* Accept the memory of the allocated buffer.
*/
accept_memory(found, size);
return found;
위치: mm/memblock.c:1522-1545. 물리 주소만으로 참조되는 초기 할당은 kmemleak 오탐을 피하고, TDX/SEV-SNP 같은 게스트 환경에서는 반환 전에 메모리 accept를 수행한다.
// mm/memblock.c:1716-1732
void * __init memblock_alloc_try_nid(phys_addr_t size, phys_addr_t align,
phys_addr_t min_addr, phys_addr_t max_addr, int nid)
역할: memblock_alloc_range_nid()의 결과를 phys_to_virt()로 변환하여 가상 주소를 반환하고, memset(ptr, 0, size)로 제로잉한다.
void * __init memblock_alloc_try_nid(
phys_addr_t size, phys_addr_t align,
phys_addr_t min_addr, phys_addr_t max_addr,
int nid)
{
void *ptr;
memblock_dbg("%s: %llu bytes align=0x%llx nid=%d from=%pa max_addr=%pa %pS\n",
__func__, (u64)size, (u64)align, nid, &min_addr,
&max_addr, (void *)_RET_IP_);
ptr = memblock_alloc_internal(size, align,
min_addr, max_addr, nid, false);
if (ptr)
memset(ptr, 0, size);
return ptr;
}
위치: mm/memblock.c:1716-1732. memblock_alloc_try_nid_raw()와 달리 반환 직전 버퍼를 0으로 채운다.
void __init memblock_free_all(void)
{
unsigned long pages;
free_unused_memmap();
reset_all_zones_managed_pages();
memblock_clear_kho_scratch_only();
pages = free_low_memory_core_early();
totalram_pages_add(pages);
}
역할: 부팅이 완료되면 남은 모든 free 메모리를 Buddy Allocator에 반환한다. free_low_memory_core_early()가 for_each_free_mem_range()로 순회하며 __free_pages_memory()로 각 영역을 해제한다.
펌웨어 메모리맵 (E820/UEFI/DT)
│
▼
arch_mem_init() / setup_arch()
│
├── memblock_add(base, size) ← 물리 메모리 등록
│ └── memblock_add_range()
│ ├── memblock_insert_region()
│ ├── memblock_double_array() ← 배열 확장
│ └── memblock_merge_regions() ← 인접 영역 병합
│
├── memblock_reserve(base, size) ← 예약 (커널 이미지, initrd 등)
│ └── __memblock_reserve()
│ └── memblock_add_range(&memblock.reserved, ...)
│
├── memblock_alloc(size, align) ← 부팅 중 할당
│ └── memblock_alloc_try_nid()
│ └── memblock_alloc_internal()
│ └── memblock_alloc_range_nid()
│ ├── memblock_find_in_range_node()
│ │ ├── __memblock_find_range_top_down()
│ │ └── __memblock_find_range_bottom_up()
│ └── __memblock_reserve()
│
└── memblock_free_all() ← 부팅 완료 시 Buddy에 반환
├── free_unused_memmap()
├── reset_all_zones_managed_pages()
└── free_low_memory_core_early()
└── __free_pages_memory()
memblock_add()의 원천은 아키텍처별 펌웨어 메모리맵이다. x86은 BIOS E820 또는 UEFI Memory Map을, ARM64/임베디드 시스템은 Device Tree의 /memory와 /reserved-memory를, NUMA 서버는 ACPI SRAT/HMAT 같은 노드 정보를 함께 사용한다. 이 단계에서 memory에 들어가는 것은 커널이 일반 RAM으로 사용할 수 있는 범위이고, 장치 MMIO, 펌웨어 런타임 서비스, ACPI 테이블, initrd, crashkernel, CMA 예약 영역은 일반 할당 대상으로 남겨 두면 안 된다.
reserved는 단순히 "사용 불가" 목록이 아니라 초기 부팅 중 이미 목적이 정해진 물리 주소 목록이다. 커널 이미지와 초기 페이지 테이블은 부팅 자체에 필요하고, initrd는 루트 파일시스템 전개 전까지 보존되어야 하며, crashkernel은 패닉 이후 두 번째 커널이 사용할 수 있어야 한다. Device Tree의 /reserved-memory는 펌웨어, 보안 영역, 디스플레이 버퍼, 원격 프로세서 공유 메모리, CMA처럼 운영 중에도 일반 Buddy 할당과 섞이면 안 되는 영역을 표현한다.
/proc/iomem은 부팅 이후 물리 주소 공간을 보는 가장 직접적인 관찰점이다. System RAM으로 표시된 범위가 대체로 memblock memory의 출발점이고, 그 하위의 Kernel code, Kernel data, reserved, Crash kernel 같은 항목은 memblock reserved 또는 이후 resource tree에서 예약된 영역과 연결된다. 커널 가상 주소 배치가 궁금할 때는 /proc/meminfo의 DirectMap, Vmalloc, PageTables와 kernel_page_tables debugfs를 함께 봐야 한다.
| 조건 | 할당 방향 | 메모리 타입 | 사용 API | 비고 |
|---|---|---|---|---|
| UMA 시스템 | top-down (기본) | memory + reserved | `memblock_add()` + `memblock_reserve()` | NUMA 노드 미사용 |
| NUMA 시스템 | top-down (기본) | memory + reserved | `memblock_add_node()` + `memblock_set_node()` | 노드별 영역 관리 |
| bottom-up 모드 | bottom-up | memory + reserved | `memblock_set_bottom_up(true)` 이후 할당 | 특정 초기화 시 사용 |
| 미러 메모리 | top-down | memory (MIRROR 플래그) | `memblock_mark_mirror()` + 일반 할당 | 미러 할당 실패 시 일반 영역으로 fallback |
| KHO scratch | bottom-up | memory (KHO_SCRATCH 플래그) | `memblock_set_kho_scratch_only()` | kexec handover 시 임시 메모리 |
| nomap 영역 | top-down | memory (NOMAP 플래그) | `memblock_mark_nomap()` | 직접 매핑에서 제외, struct page는 PageReserved() |
| memblock=debug | - | - | - | 부팅 시 상세 로그 출력 |
| slab 사용 가능 시 | - | - | `kzalloc_node()` fallback | memblock 초기화 후 slab이 준비되면 자동 전환 |
| 입력 | memblock 처리 | 주의할 점 | 확인 방법 | ||
|---|---|---|---|---|---|
| BIOS E820 | RAM 범위는 `memblock_add()`, reserved/ACPI/NVS/MMIO는 예약 또는 제외 | E820의 전체 물리 주소 공간과 커널 직접 매핑 범위는 같은 표가 아님 | `dmesg \ | grep BIOS-e820`, `cat /proc/iomem` | |
| UEFI Memory Map | Conventional Memory는 사용 후보, Runtime/ACPI/Reserved 타입은 보존 | EFI runtime 서비스 영역은 나중에도 접근 경로가 필요할 수 있음 | `dmesg \ | grep 'efi: mem'` | |
| Device Tree `/memory` | RAM bank를 `memory` region으로 등록 | bank 사이 hole과 SoC MMIO를 RAM으로 취급하면 안 됨 | `dmesg \ | grep -Ei 'OF: fdt\ | Memory:'` |
| Device Tree `/reserved-memory` | CMA, framebuffer, secure memory 등을 `reserved` 또는 NOMAP으로 분리 | 드라이버 전용 공유 메모리는 Buddy에 섞이면 데이터 손상 가능 | `dmesg \ | grep -Ei 'reserved-memory\ | cma'` |
| ACPI SRAT/HMAT | `memblock_add_node()` 또는 `memblock_set_node()`로 NUMA 노드 연결 | 주소가 연속이어도 latency/locality가 다를 수 있음 | `numactl -H`, `/sys/devices/system/node/` | ||
| `mem=`, `crashkernel=`, `movable_node` | 사용 가능 상한, crash kernel 예약, hotplug 선호 플래그에 영향 | `physmem`과 `memory`가 서로 다른 의미를 가질 수 있음 | `cat /proc/cmdline`, `cat /proc/iomem` |
| 구분 | memblock | Buddy Allocator | SLUB |
|---|---|---|---|
| 사용 시점 | 부팅 초기, 일반 할당자 준비 전 | Zone과 `struct page` 초기화 이후 | Buddy 위에서 커널 객체 할당 시 |
| 관리 단위 | 물리 주소 구간 `[base, size)` | `struct page`와 order별 free list | 캐시별 object/slab |
| 대표 API | `memblock_add()`, `memblock_reserve()`, `memblock_alloc()` | `alloc_pages()`, `__free_pages()` | `kmalloc()`, `kmem_cache_alloc()` |
| 강점 | 펌웨어 메모리맵과 예약 영역을 단순한 구간 배열로 정리 | 런타임 페이지 할당/해제와 병합 처리 | 작은 객체 반복 할당 최적화 |
| 전환점 | `memblock_free_all()`에서 남은 free range 반환 | 이후 일반 페이지 할당을 담당 | slab 준비 후 memblock 우발 호출 fallback 대상 |
| 함수 | 반환 타입 | 특징 |
|---|---|---|
| `memblock_phys_alloc()` | `phys_addr_t` | 물리 주소 반환, 제로잉 없음 |
| `memblock_alloc()` | `void *` | 가상 주소 반환, 제로잉 포함 |
| `memblock_alloc_raw()` | `void *` | 가상 주소 반환, 제로잉 없음 |
| `memblock_alloc_low()` | `void *` | 낮은 주소 영역에서 할당 |
| `memblock_alloc_node()` | `void *` | 특정 NUMA 노드에서 할당 |
| `memblock_alloc_or_panic()` | `void *` | 할당 실패 시 panic 발생 |
부팅이 완료되면 memblock_free_all()이 호출되어 모든 남은 페이지를 Buddy Allocator로 넘긴다. 이 과정에서:
1. free_unused_memmap() — 사용하지 않는 struct page 배열 메모리를 해제
2. reset_all_zones_managed_pages() — Zone의 managed_pages 카운터 리셋
3. free_low_memory_core_early() — for_each_free_mem_range()로 순회하며 __free_pages_memory()로 각 페이지를 Buddy에 반환
4. totalram_pages_add() — 전체 RAM 페이지 수 업데이트
이후 CONFIG_ARCH_KEEP_MEMBLOCK이 없으면 memblock_discard()가 호출되어 memblock 내부 배열을 해제하고, memblock 포인터는 NULL로 설정된다.