관련 소스:mm/vmalloc.c(5485줄),include/linux/vmalloc.h(337줄)
관련 문서: Buddy Allocator · SLUB 할당자 · Memblock 할당자 · 메모리 관리 개요
vmalloc은 커널 가상 주소 공간에서 가상적으로 연속된 메모리를 할당합니다. 물리적으로는 비연속일 수 있지만, 페이지 테이블 매핑을 통해 가상 주소 공간에서는 연속으로 보입니다. 큰 커널 버퍼, 모듈 로딩, ioremap 등에 사용되며, DMA 버퍼에는 적합하지 않습니다.
kmalloc이 물리 연속 메모리를 할당하는 반면, vmalloc은 개별 페이지를 할당한 후 페이지 테이블을 조작하여 가상 연속으로 매핑합니다. 따라서 kmalloc보다 느리지만, 큰 할당에 유리합니다.
도서관에 비유하면 kmalloc은 붙어 있는 책장을 통째로 확보하는 방식이고, vmalloc은 여러 층에 흩어진 책을 하나의 연속된 목록 번호로 묶는 방식입니다. 사용자는 연속된 번호만 보지만, 관리자는 각 번호가 실제 어느 책장에 있는지 별도의 표를 유지해야 하므로 페이지 테이블 설정과 TLB/캐시 동기화 비용이 추가됩니다.
커널 가상 메모리맵에서 vmalloc/ioremap 영역은 직접 매핑(Direct Mapping) 영역과 별도입니다. x86_64 4단계 페이지 테이블의 대표 배치에서는 직접 매핑이 ffff888000000000 근처부터, vmalloc/ioremap 영역이 ffffc90000000000 근처부터 시작하지만 실제 값은 KASLR, 5단계 페이지 테이블, KASAN, 아키텍처 설정에 따라 달라집니다. 따라서 vmalloc 주소에는 __pa()/__va() 산술 변환을 적용하지 말고, 주소 성격은 is_vmalloc_addr()와 현재 부팅 인스턴스의 /proc/vmallocinfo, kernel_page_tables로 확인해야 합니다.
소스 파일 경로:
mm/vmalloc.c ← 메인 구현 (5485줄)
include/linux/vmalloc.h ← API 선언 및 vm_struct 정의 (337줄)
include/linux/mm.h ← vmalloc_to_page(), is_vmalloc_addr() 등
mm/internal.h ← 내부 헬퍼 함수
# vmalloc 영역 사용 현황
cat /proc/vmallocinfo | wc -l # 전체 vmalloc 영역 수
cat /proc/vmallocinfo | head -10 # 상위 vmalloc 할당 목록
grep -i vmalloc /proc/meminfo # VmallocTotal, VmallocUsed, VmallocChunk
# vmalloc 주소 범위 확인
sudo awk 'NR <= 10 {print $1}' /proc/vmallocinfo # 시작-끝 가상 주소 범위
# 특정 프로세스의 vmalloc 사용량
cat /proc/$$/status | grep -i vm # VmData, VmStk 등
# vmalloc 할당 정보 (커널 5.10+)
sudo cat /proc/vmallocinfo | grep -i "vm_struct\|pages=" | head -20
# 메모리 압력 확인
cat /proc/pressure/memory # PSI 메모리 압력
# 커널 메모리맵에서 vmalloc 영역 확인
grep -E 'Vmalloc|PageTables' /proc/meminfo
# 직접 매핑, vmalloc, 페이지 테이블 사용량을 함께 확인
grep -E 'Vmalloc|PageTables|DirectMap' /proc/meminfo
# vmalloc/ioremap 실제 매핑과 호출자 확인
sudo cat /proc/vmallocinfo | grep -E 'vmalloc|ioremap|vmap' | head -30
# 물리 주소 공간에서 RAM과 MMIO hole 구분
cat /proc/iomem | head -80
# x86 CONFIG_X86_PTDUMP와 debugfs가 있으면 커널 페이지 테이블 확인
sudo cat /sys/kernel/debug/kernel_page_tables 2>/dev/null | grep -E 'vmalloc|ioremap|page_offset|direct' | head -40
# 주소 폭과 디버그 옵션이 vmalloc 영역 배치에 영향을 주는지 확인
zgrep -E 'CONFIG_X86_5LEVEL|CONFIG_KASAN|CONFIG_MITIGATION_PAGE_TABLE_ISOLATION|CONFIG_HAVE_ARCH_HUGE_VMALLOC' /proc/config.gz 2>/dev/null
/* include/linux/vmalloc.h:52-69 */
struct vm_struct {
union {
struct vm_struct *next; /* vm_area 조기 등록. */
struct llist_node llnode; /* 오류 경로에서 비동기 해제. */
};
void *addr;
unsigned long size;
unsigned long flags;
struct page **pages;
#ifdef CONFIG_HAVE_ARCH_HUGE_VMALLOC
unsigned int page_order;
#endif
unsigned int nr_pages;
phys_addr_t phys_addr;
const void *caller;
unsigned long requested_size;
};
필드 설명:
addr: vmalloc 영역에서 할당받은 가상 주소size: guard page 포함 전체 크기 (flags에 VM_NO_GUARD 없으면 +PAGE_SIZE)flags: 할당 유형 (VM_ALLOC=0x02, VM_IOREMAP=0x01, VM_MAP=0x04 등)pages: alloc_pages_bulk()로 할당한 물리 페이지 배열page_order: huge vmalloc 활성화 시 PMD_SHIFT 등으로 설정nr_pages: 실제 할당된 페이지 수caller: 디버깅용 할당 호출자 주소/* include/linux/vmalloc.h:71-89 */
struct vmap_area {
unsigned long va_start;
unsigned long va_end;
struct rb_node rb_node; /* 주소 정렬 rbtree */
struct list_head list; /* 주소 정렬 list */
/*
* 다음 두 변수는 하나로 묶을 수 있다. vmap_area 객체는 다음 중 하나다.
* 1) "free" tree(root는 free_vmap_area_root)에 있음
* 2) 또는 "busy" tree(root는 vmap_area_root)에 있음
*/
union {
unsigned long subtree_max_size; /* "free" tree에 있음 */
struct vm_struct *vm; /* "busy" tree에 있음 */
};
unsigned long flags; /* vm_map_ram 영역 타입 표시 */
};
필드 설명:
va_start/va_end: vmalloc 주소 공간 내 가상 주소 범위rb_node: red-black tree 노드 (주소 정렬)subtree_max_size: free tree에서 사용되는 augmented 필드vm: busy tree에서 연결된 vm_struct 포인터/* mm/vmalloc.c:936-952 */
static struct vmap_node {
/* 단순한 크기별 격리 저장소. */
struct vmap_pool pool[MAX_VA_SIZE_PAGES];
spinlock_t pool_lock;
bool skip_populate;
/* 이 노드의 관리 데이터. */
struct rb_list busy;
struct rb_list lazy;
/*
* 해제 준비가 끝난 영역.
*/
struct list_head purge_list;
struct work_struct purge_work;
unsigned long nr_purged;
} single;
동작 방식:
single 노드 하나만 사용nr_vmap_nodes만큼 확장addr_to_node()로 vmap_area를 특정 노드에 매핑/* mm/vmalloc.c:4157-4162 */
void *vmalloc_noprof(unsigned long size)
{
return __vmalloc_node_noprof(size, 1, GFP_KERNEL, NUMA_NO_NODE,
__builtin_return_address(0));
}
EXPORT_SYMBOL(vmalloc_noprof);
역할: 크기만 받아 기본 GFP_KERNEL 플래그로 vmalloc 호출
분기: 없음 (단순 래퍼)
/* mm/vmalloc.c:3986-4103 */
void *__vmalloc_node_range_noprof(unsigned long size, unsigned long align,
unsigned long start, unsigned long end, gfp_t gfp_mask,
pgprot_t prot, unsigned long vm_flags, int node,
const void *caller)
{
struct vm_struct *area;
void *ret;
kasan_vmalloc_flags_t kasan_flags = KASAN_VMALLOC_NONE;
unsigned long original_align = align;
unsigned int shift = PAGE_SHIFT;
if (WARN_ON_ONCE(!size))
return NULL;
if ((size >> PAGE_SHIFT) > totalram_pages()) {
warn_alloc(gfp_mask, NULL,
"vmalloc error: size %lu, exceeds total pages", size);
return NULL;
}
if (vmap_allow_huge && (vm_flags & VM_ALLOW_HUGE_VMAP)) {
/*
* Huge page를 시도한다. PAGE_KERNEL 할당에만 시도한다.
* module 같은 다른 할당은 apply_to_page_range가 huge page를
* 지원하지 않기 때문에 아직 huge page를 기대하지 않는다.
*/
if (arch_vmap_pmd_supported(prot) && size >= PMD_SIZE)
shift = PMD_SHIFT;
else
shift = arch_vmap_pte_supported_shift(size);
align = max(original_align, 1UL << shift);
}
again:
area = __get_vm_area_node(size, align, shift, VM_ALLOC |
VM_UNINITIALIZED | vm_flags, start, end, node,
gfp_mask, caller);
if (!area) {
bool nofail = gfp_mask & __GFP_NOFAIL;
warn_alloc(gfp_mask, NULL,
"vmalloc error: size %lu, vm_struct allocation failed%s",
size, (nofail) ? ". Retrying." : "");
if (nofail) {
schedule_timeout_uninterruptible(1);
goto again;
}
goto fail;
}
/*
* __vmalloc_area_node()와 kasan_unpoison_vmalloc()에 넘길
* 인자를 준비한다.
*/
if (pgprot_val(prot) == pgprot_val(PAGE_KERNEL)) {
if (kasan_hw_tags_enabled()) {
/*
* tagging을 허용하도록 protection bit를 바꾼다.
* 매핑 전에 해야 한다.
*/
prot = arch_vmap_pgprot_tagged(prot);
/*
* VM_ALLOC 매핑을 뒷받침하는 물리 페이지에 대해 page_alloc
* poisoning과 zeroing을 건너뛴다. 대신 메모리는
* kasan_unpoison_vmalloc()에서 poison/zero 처리된다.
*/
gfp_mask |= __GFP_SKIP_KASAN | __GFP_SKIP_ZERO;
}
/* 매핑이 PAGE_KERNEL이라는 점을 기록한다. */
kasan_flags |= KASAN_VMALLOC_PROT_NORMAL;
}
/* 물리 페이지를 할당하고 vmalloc 공간에 매핑한다. */
ret = __vmalloc_area_node(area, gfp_mask, prot, shift, node);
if (!ret)
goto fail;
/*
* 페이지가 매핑되었으므로 접근 가능하다고 표시한다.
* KASAN_VMALLOC_INIT 설정 조건은 __GFP_SKIP_ZERO 관점에서
* post_alloc_hook()의 조건과 보완되어야 하며, 같은 조건에서
* 메모리가 초기화되도록 맞춘다. tag 기반 KASAN 모드는
* 일반 non-executable 할당에만 tag를 부여한다.
*/
kasan_flags |= KASAN_VMALLOC_VM_ALLOC;
if (!want_init_on_free() && want_init_on_alloc(gfp_mask) &&
(gfp_mask & __GFP_SKIP_ZERO))
kasan_flags |= KASAN_VMALLOC_INIT;
/* KASAN_VMALLOC_PROT_NORMAL은 필요할 때 이미 설정되어 있다. */
area->addr = kasan_unpoison_vmalloc(area->addr, size, kasan_flags);
/*
* 이 함수에서 새로 할당한 vm_struct에는 VM_UNINITIALIZED 플래그가 있다.
* 이는 vm_struct가 아직 완전히 초기화되지 않았음을 뜻한다.
* 이제 완전히 초기화되었으므로 여기서 플래그를 제거한다.
*/
clear_vm_uninitialized_flag(area);
if (!(vm_flags & VM_DEFER_KMEMLEAK))
kmemleak_vmalloc(area, PAGE_ALIGN(size), gfp_mask);
return area->addr;
fail:
if (shift > PAGE_SHIFT) {
shift = PAGE_SHIFT;
align = original_align;
goto again;
}
return NULL;
}
분기 로직:
1. size 검증: totalram_pages() 초과 시 실패
2. huge page 시도: vmap_allow_huge && VM_ALLOW_HUGE_VMAP && size >= PMD_SIZE
3. 할당 실패 시: __GFP_NOFAIL이면 1 tick 대기 후 재시도
4. PAGE_SHIFT fallback: huge page 실패 시 order-0로 재시도
/* mm/vmalloc.c:3203-3251 */
struct vm_struct *__get_vm_area_node(unsigned long size,
unsigned long align, unsigned long shift, unsigned long flags,
unsigned long start, unsigned long end, int node,
gfp_t gfp_mask, const void *caller)
{
struct vmap_area *va;
struct vm_struct *area;
unsigned long requested_size = size;
BUG_ON(in_interrupt());
size = ALIGN(size, 1ul << shift);
if (unlikely(!size))
return NULL;
if (flags & VM_IOREMAP)
align = 1ul << clamp_t(int, get_count_order_long(size),
PAGE_SHIFT, IOREMAP_MAX_ORDER);
area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
if (unlikely(!area))
return NULL;
if (!(flags & VM_NO_GUARD))
size += PAGE_SIZE;
area->flags = flags;
area->caller = caller;
area->requested_size = requested_size;
va = alloc_vmap_area(size, align, start, end, node, gfp_mask, 0, area);
if (IS_ERR(va)) {
kfree(area);
return NULL;
}
/*
* VM_ALLOC이 아닌 매핑의 페이지는 접근 가능하다고 표시한다.
* vmalloc 코드 밖에서 매핑될 수 있으므로 여기서 최선 노력 방식으로 수행한다.
* VM_ALLOC 매핑은 __vmalloc_node_range()에서 매핑된 뒤 접근 가능 표시된다.
* hardware tag 기반 KASAN에서는 non-VM_ALLOC 매핑 표시를 건너뛴다.
*/
if (!(flags & VM_ALLOC))
area->addr = kasan_unpoison_vmalloc(area->addr, requested_size,
KASAN_VMALLOC_PROT_NORMAL);
return area;
}
역할: vm_struct를 만들고 vmalloc 주소 범위에서 연속 가상 영역을 예약합니다. VM_NO_GUARD가 없으면 guard page를 더하고, VM_IOREMAP은 IOREMAP_MAX_ORDER를 넘지 않는 범위에서 정렬을 키웁니다.
분기 로직:
1. 인터럽트 컨텍스트 금지: BUG_ON(in_interrupt())
2. 크기 정렬: huge vmalloc이면 shift 기준으로 정렬
3. guard page 추가: 기본적으로 size += PAGE_SIZE
4. 가상 주소 예약: alloc_vmap_area()가 free tree 또는 per-node pool에서 영역을 찾아 busy tree에 연결
/* mm/vmalloc.c:3827-3933 */
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
pgprot_t prot, unsigned int page_shift,
int node)
{
const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
bool nofail = gfp_mask & __GFP_NOFAIL;
unsigned long addr = (unsigned long)area->addr;
unsigned long size = get_vm_area_size(area);
unsigned long array_size;
unsigned int nr_small_pages = size >> PAGE_SHIFT;
unsigned int page_order;
unsigned int flags;
int ret;
array_size = (unsigned long)nr_small_pages * sizeof(struct page *);
/* __GFP_NOFAIL과 "noblock" 플래그는 함께 쓸 수 없다. */
if (!gfpflags_allow_blocking(gfp_mask))
nofail = false;
if (!(gfp_mask & (GFP_DMA | GFP_DMA32)))
gfp_mask |= __GFP_HIGHMEM;
/* 재귀는 엄격하게 제한된다. */
if (array_size > PAGE_SIZE) {
area->pages = __vmalloc_node_noprof(array_size, 1, nested_gfp, node,
area->caller);
} else {
area->pages = kmalloc_node_noprof(array_size, nested_gfp, node);
}
if (!area->pages) {
warn_alloc(gfp_mask, NULL,
"vmalloc error: size %lu, failed to allocated page array size %lu",
nr_small_pages * PAGE_SIZE, array_size);
goto fail;
}
set_vm_area_page_order(area, page_shift - PAGE_SHIFT);
page_order = vm_area_page_order(area);
/*
* 고차 nofail 할당은 매우 비싸고 잠재적으로 위험하다
* (이른 OOM, 방해적인 reclaim과 compaction 등).
*
* __vmalloc_node_range_noprof()는 고차 시도가 실패하면
* order-0 페이지로 대체한다.
*/
area->nr_pages = vm_area_alloc_pages(
vmalloc_gfp_adjust(gfp_mask, page_order), node,
page_order, nr_small_pages, area->pages);
atomic_long_add(area->nr_pages, &nr_vmalloc_pages);
/* vm의 모든 페이지는 같은 memcg에 청구되어 있으므로 첫 페이지를 사용한다. */
if (gfp_mask & __GFP_ACCOUNT && area->nr_pages)
mod_memcg_page_state(area->pages[0], MEMCG_VMALLOC,
area->nr_pages);
/*
* 할당 요청을 완료할 만큼 충분한 페이지를 얻지 못했다면,
* 일부가 있더라도 vfree()를 통해 해제한다.
*/
if (area->nr_pages != nr_small_pages) {
/*
* vm_area_alloc_pages()는 메모리 부족뿐 아니라 다음 이유로도 실패할 수 있다.
*
* - 대기 중인 fatal signal
* - huge page-order 페이지 부족
*
* huge page 경우에는 항상 order-0으로 재시도하므로 둘 다 경고가 부적절하다.
*/
if (!fatal_signal_pending(current) && page_order == 0)
warn_alloc(gfp_mask, NULL,
"vmalloc error: size %lu, failed to allocate pages",
area->nr_pages * PAGE_SIZE);
goto fail;
}
/*
* 페이지 테이블 할당은 외부 gfp mask를 무시하므로
* scope API로 강제한다.
*/
flags = memalloc_apply_gfp_scope(gfp_mask);
do {
ret = __vmap_pages_range(addr, addr + size, prot, area->pages,
page_shift, nested_gfp);
if (nofail && (ret < 0))
schedule_timeout_uninterruptible(1);
} while (nofail && (ret < 0));
memalloc_restore_scope(flags);
if (ret < 0) {
warn_alloc(gfp_mask, NULL,
"vmalloc error: size %lu, failed to map pages",
area->nr_pages * PAGE_SIZE);
goto fail;
}
return area->addr;
fail:
defer_vm_area_cleanup(area);
return NULL;
}
분기 로직:
1. 페이지 배열 할당: array_size > PAGE_SIZE → vmalloc, 아니면 kmalloc
2. huge page 실패 시: order-0 fallback (vm_area_alloc_pages 내부)
3. 매핑 실패 시: nofail이면 1 tick 대기 후 재시도
4. 할당 불완전 시: fatal_signal_pending 아니고 order-0이면 경고 후 실패
vm_area_alloc_pages()는 먼저 가능한 큰 order 페이지를 시도하고, order-0에서는 bulk allocator를 사용합니다. bulk 요청은 한 번에 최대 100페이지로 제한되고, 일부만 채워지면 단일 페이지 할당 경로로 내려갑니다.
/* mm/vmalloc.c:3640-3760 */
static inline unsigned int
vm_area_alloc_pages(gfp_t gfp, int nid,
unsigned int order, unsigned int nr_pages, struct page **pages)
{
unsigned int nr_allocated = 0;
unsigned int nr_remaining = nr_pages;
unsigned int max_attempt_order = MAX_PAGE_ORDER;
struct page *page;
int i;
unsigned int large_order = ilog2(nr_remaining);
gfp_t large_gfp = vmalloc_gfp_adjust(gfp, large_order) & ~__GFP_DIRECT_RECLAIM;
large_order = min(max_attempt_order, large_order);
/*
* 처음에는 page allocator가 large-order 페이지를 주도록 시도한다.
* __vmap_pages_range()가 정확히 order 길이의 물리 연속 chunk를 기대하므로
* order보다 작은 chunk는 시도하지 않는다.
*/
while (large_order > order && nr_remaining) {
if (nid == NUMA_NO_NODE)
page = alloc_pages_noprof(large_gfp, large_order);
else
page = alloc_pages_node_noprof(nid, large_gfp, large_order);
if (unlikely(!page)) {
max_attempt_order = --large_order;
continue;
}
split_page(page, large_order);
for (i = 0; i < (1U << large_order); i++)
pages[nr_allocated + i] = page + i;
nr_allocated += 1U << large_order;
nr_remaining = nr_pages - nr_allocated;
large_order = ilog2(nr_remaining);
large_order = min(max_attempt_order, large_order);
}
/*
* order-0 페이지는 bulk allocator를 사용한다. 실패로 인해 page array가
* 일부만 채워졌거나 전혀 채워지지 않으면 더 관대한 단일 페이지
* allocator로 대체한다.
*/
if (!order) {
while (nr_allocated < nr_pages) {
unsigned int nr, nr_pages_request;
/*
* 허용되는 최대 요청은 하드코딩되어 있으며 호출당 100페이지다.
* bulk allocator에서 긴 preemption-off 상황을 막기 위한 것으로,
* 범위는 [1:100]이다.
*/
nr_pages_request = min(100U, nr_pages - nr_allocated);
/* 메모리 할당은 mempolicy를 고려해야 한다. nid == NUMA_NO_NODE일 때
* nearest node를 잘못 사용하면 안 된다. 그렇지 않으면 mempolicy가
* interleaving으로 메모리를 할당하려는데 한 노드에만 할당될 수 있다.
*/
if (IS_ENABLED(CONFIG_NUMA) && nid == NUMA_NO_NODE)
nr = alloc_pages_bulk_mempolicy_noprof(gfp,
nr_pages_request,
pages + nr_allocated);
else
nr = alloc_pages_bulk_node_noprof(gfp, nid,
nr_pages_request,
pages + nr_allocated);
nr_allocated += nr;
/*
* 0페이지 또는 일부 페이지만 얻었다면 단일 페이지 allocator로 대체한다.
*/
if (nr != nr_pages_request)
break;
}
}
/* 고차 페이지 또는 "bulk" 실패 시 대체 경로. */
while (nr_allocated < nr_pages) {
if (!(gfp & __GFP_NOFAIL) && fatal_signal_pending(current))
break;
if (nid == NUMA_NO_NODE)
page = alloc_pages_noprof(gfp, order);
else
page = alloc_pages_node_noprof(nid, gfp, order);
if (unlikely(!page))
break;
/*
* 고차 할당은 호출자가 독립적인 작은 페이지처럼 다룰 수 있어야 한다.
* 일부 드라이버는 vmalloc_to_page() 페이지에 자체 refcounting을 하고,
* 일부는 page->mapping, page->lru 등을 사용한다.
*/
if (order)
split_page(page, order);
/*
* page-order 페이지를 할당하고 매핑하지만 tracking은 PAGE_SIZE 페이지마다
* 수행하여 vm_struct API가 물리/매핑 크기에 의존하지 않게 한다.
*/
for (i = 0; i < (1U << order); i++)
pages[nr_allocated + i] = page + i;
nr_allocated += 1U << order;
}
return nr_allocated;
}
/* mm/vmalloc.c:3442-3487 */
void vfree(const void *addr)
{
struct vm_struct *vm;
int i;
if (unlikely(in_interrupt())) {
vfree_atomic(addr);
return;
}
BUG_ON(in_nmi());
kmemleak_free(addr);
might_sleep();
if (!addr)
return;
vm = remove_vm_area(addr);
if (unlikely(!vm)) {
WARN(1, KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
addr);
return;
}
if (unlikely(vm->flags & VM_FLUSH_RESET_PERMS))
vm_reset_perms(vm);
/* vm의 모든 페이지는 같은 memcg에 청구되어 있으므로 첫 페이지를 사용한다. */
if (vm->nr_pages && !(vm->flags & VM_MAP_PUT_PAGES))
mod_memcg_page_state(vm->pages[0], MEMCG_VMALLOC, -vm->nr_pages);
for (i = 0; i < vm->nr_pages; i++) {
struct page *page = vm->pages[i];
BUG_ON(!page);
/*
* huge vmalloc의 high-order 할당은 split되므로
* order-0 할당 배열처럼 해제할 수 있다.
*/
__free_page(page);
cond_resched();
}
if (!(vm->flags & VM_MAP_PUT_PAGES))
atomic_long_sub(vm->nr_pages, &nr_vmalloc_pages);
kvfree(vm->pages);
kfree(vm);
}
EXPORT_SYMBOL(vfree);
분기 로직:
1. 인터럽트 컨텍스트: vfree_atomic()으로 지연 해제
2. NMI 컨텍스트: BUG_ON()으로 패닉
3. VM_FLUSH_RESET_PERMS: 권한 초기화 후 해제
4. VM_MAP_PUT_PAGES: 페이지 배열 소유권 이전됨
/* mm/vmalloc.c:3408-3423 */
void vfree_atomic(const void *addr)
{
struct vfree_deferred *p = raw_cpu_ptr(&vfree_deferred);
BUG_ON(in_nmi());
kmemleak_free(addr);
/*
* 선점 가능한 컨텍스트에서 호출될 수 있으므로 raw_cpu_ptr()를 사용한다.
* 여기서는 선점이 문제되지 않는다. llist_add() 구현은 lockless라서
* 다른 CPU의 리스트에 추가하더라도 동작하며, schedule_work()도 괜찮다.
*/
if (addr && llist_add((struct llist_node *)addr, &p->list))
schedule_work(&p->wq);
}
역할: 원자적 컨텍스트에서 주소를 per-CPU llist에 추가하고 workqueue에서 나중에 vfree() 경로로 넘깁니다. NMI에서는 허용하지 않으며, addr == NULL이면 리스트에 넣지 않습니다.
vmalloc(size)
└─> __vmalloc_node_noprof(size, 1, GFP_KERNEL, NUMA_NO_NODE, caller)
└─> __vmalloc_node_range_noprof(size, align, VMALLOC_START, VMALLOC_END, ...)
├─> [1] __get_vm_area_node() ← 가상 주소 영역 할당
│ └─> alloc_vmap_area() ← vmap_area red-black tree 검색
├─> [2] __vmalloc_area_node() ← 물리 페이지 할당 + 매핑
│ ├─> kmalloc_node() / __vmalloc_node() ← 페이지 배열 할당
│ ├─> vm_area_alloc_pages() ← Buddy/bulk allocator에서 페이지 할당
│ └─> __vmap_pages_range() ← 페이지 테이블 매핑
├─> [3] kasan_unpoison_vmalloc() ← KASAN 해제
└─> [4] clear_vm_uninitialized_flag() ← VM_UNINITIALIZED 제거
vfree(addr)
├─> [interrupt context] vfree_atomic()
│ └─> llist_add() + schedule_work()
└─> [process context] remove_vm_area()
└─> [1] find_vmap_area() ← vmap_area 검색
└─> [2] unlink_va() ← busy tree에서 제거
└─> [3] __free_page() per page ← 페이지별 Buddy 반환
└─> [4] kvfree(vm->pages) ← 페이지 배열 해제
└─> [5] kfree(vm) ← vm_struct 해제
| 특성 | kmalloc | vmalloc |
|---|---|---|
| **물리 연속** | O (반드시 연속) | X (비연속 가능) |
| **가상 연속** | O | O |
| **최대 크기** | ~4MB (order-10) | 가상 주소 공간 한도 |
| **DMA 사용** | O | X |
| **속도** | 빠름 (SLUB 직접) | 느림 (페이지 테이블 매핑) |
| **slab 캐시** | kmalloc-32, kmalloc-64 등 | 사용 안 함 |
| **대표 사용처** | 커널 오브젝트, small buffer | 큰 버퍼, 모듈 로딩 |
| 특성 | 직접 매핑 (Direct Mapping) | vmalloc/ioremap 영역 |
|---|---|---|
| **주요 대상** | System RAM의 선형 매핑 | vmalloc 페이지, vmap 매핑, MMIO ioremap |
| **물리 연속성** | 물리 주소와 선형 오프셋 관계 | 물리적으로 흩어진 페이지 또는 장치 물리 주소 가능 |
| **주소 변환** | `__pa()` / `__va()` 산술 변환 가능 | 페이지 테이블 매핑을 조회해야 하며 산술 변환 금지 |
| **판별 API** | `virt_addr_valid()`가 직접 매핑 RAM 판별에 가까움 | `is_vmalloc_addr()`, `/proc/vmallocinfo`로 확인 |
| **DMA 적합성** | 일반적으로 DMA API와 함께 사용 가능 | `vmalloc()` 주소는 직접 DMA 버퍼로 부적합 |
| **대표 사용처** | Buddy/SLUB가 반환한 일반 커널 메모리 | 큰 커널 버퍼, 모듈, BPF/JIT 보조 영역, MMIO mapping |
ioremap()은 RAM을 새로 할당하는 API가 아니라 장치 레지스터 같은 MMIO 물리 주소를 커널 가상 주소로 접근 가능하게 만드는 API입니다. 임베디드 SoC나 PCI 드라이버에서는 Device Tree/ACPI/PCI BAR가 알려준 리소스를 devm_ioremap_resource()로 매핑하고, DMA 버퍼는 별도로 dma_alloc_coherent() 또는 dma_map_*() 계열을 사용해야 합니다.
| 상황 | 권장 API | 이유 |
|---|---|---|
| 크기가 작고 물리 연속성이 필요 | `kmalloc()` | SLUB fast path와 DMA 친화성 |
| 크기가 크지만 물리 연속성이 필요 없음 | `vmalloc()` | 외부 단편화 영향을 덜 받음 |
| 크기가 런타임에 달라 작을 수도 큼직할 수도 있음 | `kvmalloc()` | `kmalloc()` 우선 시도 후 큰 요청에서 vmalloc fallback |
| 0으로 초기화된 큰 버퍼 | `kvzalloc()` / `vzalloc()` | 초기화 비용을 API 의미에 포함 |
| NUMA 노드 선호가 있음 | `kvmalloc_node()` / `vmalloc_node()` | page allocator 호출 시 노드 힌트 전달 |
kvmalloc() 계열은 작은 크기에서는 kmalloc의 빠른 경로를 살리고, 큰 크기나 단편화 상황에서는 vmalloc fallback으로 성공률을 높입니다. 단, fallback 이후 반환 주소는 vmalloc 주소일 수 있으므로 물리 연속성, DMA 직접 사용, virt_to_phys() 같은 가정은 두면 안 됩니다.
| API | 특징 | 용도 |
|---|---|---|
| `vmalloc(size)` | GFP_KERNEL, nozero | 큰 커널 버퍼 |
| `vzalloc(size)` | GFP_KERNEL + __GFP_ZERO | 제로 초기화 필요 시 |
| `vmalloc_node(size, node)` | 특정 NUMA 노드 | NUMA 친화 할당 |
| `vmalloc_user(size)` | VM_USERMAP 플래그 | userspace 매핑 가능 |
| `vmalloc_32(size)` | 32비트 호환 | 32비트 드라이버 |
| `__vmalloc(size, gfp)` | GFP 플래그 지정 | 세밀한 제어 |
| 조건 | 결과 |
|---|---|
| `CONFIG_HAVE_ARCH_HUGE_VMALLOC` 미활성화 | huge vmalloc 불가 |
| `vmap_allow_huge = false` (nohugevmalloc 파라미터) | huge vmalloc 불가 |
| `size < PMD_SIZE` (일반적으로 2MB) | order-0 할당 |
| `arch_vmap_pmd_supported(prot)` false | PMD 매핑 불가 |
| huge page 실패 | order-0 fallback 자동 |
| 조건 | 영향 | 확인 방법 | |
|---|---|---|---|
| x86_64 4단계 페이지 테이블 | vmalloc/ioremap 영역이 대표적으로 `ffffc90000000000` 근처에 배치 | `sudo cat /sys/kernel/debug/kernel_page_tables` | |
| x86_64 5단계 페이지 테이블(LA57) | 유저/커널 가상 주소 폭이 커지고 vmalloc 영역 규모도 PB 단위로 확장 | `grep la57 /proc/cpuinfo`, `zgrep CONFIG_X86_5LEVEL /proc/config.gz` | |
| KASLR | 절대 주소가 부팅마다 달라질 수 있음 | `dmesg | grep -i kaslr` |
| KASAN | shadow 영역이 추가되어 vmalloc 주변 배치와 가드 정책에 영향 | `zgrep CONFIG_KASAN /proc/config.gz` | |
| PTI | 사용자 CR3와 커널 CR3 관점의 매핑이 달라짐 | `grep pti /proc/cpuinfo` | |
| ARM64/RISC-V | VA_BITS, 페이지 크기, Sv39/Sv48/Sv57에 따라 vmalloc 범위가 다름 | `uname -m`, `zgrep -E 'CONFIG_ARM64_VA_BITS | CONFIG_RISCV' /proc/config.gz` |
| 컨텍스트 | 동작 |
|---|---|
| 프로세스 컨텍스트 | 즉시 해제 (remove_vm_area + __free_page) |
| 인터럽트 컨텍스트 | vfree_atomic() → workqueue 지연 해제 |
| NMI 컨텍스트 | BUG_ON() 패닉 |
| `__GFP_NOFAIL` 할당 | 실패 시 재시도 로직 없음 (경고 후 실패) |
vmalloc의 핵심 내용:
1. vmalloc 3단계 내부 동작: alloc_vmap_area() → alloc_pages_bulk() → vmap_pages_range()
2. kvmalloc: kmalloc 우선 시도 후 vmalloc fallback
3. vmalloc vs kmalloc 비교: 물리 연속, 최대 크기, 용도
4. vmalloc 영역 명령어: /proc/vmallocinfo, /proc/meminfo의 VmallocTotal/Used/Chunk
5. ioremap과의 관계: vmalloc/ioremap 영역 공유 (커널 가상 메모리맵)
vmalloc은 가상 주소 예약, 물리 페이지 확보, 페이지 테이블 매핑, KASAN/memcg 후처리가 함께 움직이는 경로이므로 주소 성격과 할당 컨텍스트를 먼저 구분해야 안전하게 사용할 수 있습니다.