vmalloc — 가상 연속 메모리 할당
Linux 커널 vmalloc 서브시스템: 가상 주소 공간에서 연속적이지만 물리적으로는 불연속인 메모리를 할당하는 메커니즘, vmap_area 관리, 페이지 테이블 매핑, lazy TLB flush, ioremap, huge vmalloc, 성능 특성, 디버깅 종합 가이드.
개요
vmalloc()은 커널에서 가상 주소 공간에서 연속적이지만, 물리 메모리에서는 불연속적인 메모리를 할당하는 함수입니다. 커널의 두 가지 주요 메모리 할당 인터페이스 중 하나로, kmalloc()과 근본적으로 다른 특성을 가집니다.
왜 vmalloc이 필요한가?
커널이 부팅 후 오랜 시간 동작하면 물리 메모리가 단편화(fragmentation)됩니다. 이 상태에서 수십 KB 이상의 연속 물리 메모리를 확보하기 어려워집니다. kmalloc()은 물리적으로 연속인 메모리를 요구하므로 큰 할당이 실패할 수 있습니다. vmalloc()은 개별 페이지를 따로 할당한 뒤, 커널 페이지 테이블을 조작하여 가상 주소 공간에서 연속으로 보이게 매핑합니다.
vmalloc()으로 할당한 메모리는 DMA에 직접 사용할 수 없습니다. DMA 컨트롤러는 물리 주소를 사용하므로 물리적으로 연속인 메모리(kmalloc, dma_alloc_coherent)가 필요합니다. IOMMU가 있는 시스템에서는 예외적으로 가능할 수 있습니다.
kmalloc vs vmalloc 비교
| 특성 | kmalloc() | vmalloc() |
|---|---|---|
| 물리 메모리 연속성 | 물리적으로 연속 | 물리적으로 불연속 (페이지 단위) |
| 가상 주소 연속성 | 가상 주소도 연속 | 가상 주소만 연속 |
| 최대 할당 크기 | 수 MB (order 제한) | VMALLOC_END - VMALLOC_START (수십~수백 GB) |
| 최소 할당 단위 | 8바이트~ | PAGE_SIZE (4KB) |
| 할당 속도 | 빠름 (슬랩/버디) | 느림 (페이지 테이블 조작 필요) |
| TLB 효율 | 높음 (물리 연속 → 대형 페이지 가능) | 낮음 (페이지별 TLB 엔트리 필요) |
| DMA 호환 | 가능 | 직접 불가 (IOMMU 예외) |
| sleep 가능 | GFP_KERNEL 시 가능 | 항상 sleep 가능 (GFP_KERNEL 내부 사용) |
| 인터럽트 컨텍스트 | GFP_ATOMIC으로 가능 | 사용 불가 (sleep 발생) |
| 주요 용도 | 작은 객체, DMA 버퍼 | 모듈 로딩, 큰 버퍼, eBPF |
| 내부 할당자 | Slab → Buddy | Buddy (페이지 단위) + 페이지 테이블 |
| 해제 함수 | kfree() | vfree() |
/* kmalloc vs vmalloc 사용 예시 */
#include <linux/slab.h>
#include <linux/vmalloc.h>
/* 작은 할당 (수 KB 이하) → kmalloc 권장 */
char *small_buf = kmalloc(4096, GFP_KERNEL);
if (!small_buf)
return -ENOMEM;
/* 사용 후 */
kfree(small_buf);
/* 큰 할당 (수십 KB~수 MB) → vmalloc 권장 */
char *large_buf = vmalloc(1024 * 1024); /* 1MB */
if (!large_buf)
return -ENOMEM;
/* 사용 후 */
vfree(large_buf);
/* 자동 선택 (작으면 kmalloc, 크면 vmalloc) */
char *auto_buf = kvmalloc(256 * 1024, GFP_KERNEL); /* 256KB */
kvfree(auto_buf);
코드 설명
-
5-8행
kmalloc()은 물리적으로 연속인 메모리를 할당합니다. 4KB 이하의 작은 할당에 적합하며, 실패 시 NULL을 반환합니다. -
12-15행
vmalloc()은 1MB와 같은 큰 메모리도 안정적으로 할당합니다. 물리적으로 불연속이어도 가상 주소로 연속 접근이 가능합니다. -
18-19행
kvmalloc()은 먼저kmalloc을 시도하고 실패하면vmalloc으로 fallback합니다. 해제 시kvfree()를 사용합니다.
핵심 요약
- vmalloc() — 가상 주소 연속, 물리 주소 불연속 메모리를 할당합니다. 큰 버퍼 할당에 적합합니다.
- vmap_area — vmalloc 영역 내 할당된 가상 주소 범위를 관리하는 자료구조입니다.
- vm_struct — vmalloc 할당 메타데이터(크기, 플래그, 페이지 배열 등)를 담는 구조체입니다.
- 페이지 테이블 매핑 — 개별 물리 페이지를 가상 주소에 매핑하기 위해 PGD→PUD→PMD→PTE 체인을 설정합니다.
- lazy TLB flush — vfree 시 TLB 무효화를 지연시켜 성능을 최적화합니다.
- kvmalloc() — kmalloc 시도 후 실패하면 vmalloc으로 자동 fallback하는 편의 함수입니다.
단계별 이해
- 가상 주소 공간 예약 — VMALLOC_START~VMALLOC_END 범위에서 빈 가상 주소 구간을 찾아 vmap_area를 할당합니다.
Red-black 트리와 free list를 사용하여 빠르게 빈 공간을 탐색합니다.
- 물리 페이지 할당 — Buddy 할당자에서 개별 페이지(order-0)를 필요한 수만큼 할당합니다.
각 페이지는 물리적으로 어디에 있든 상관없이 독립적으로 할당됩니다.
- 페이지 테이블 설정 — 할당된 가상 주소 범위에 대해 커널 페이지 테이블(PGD→PUD→PMD→PTE)을 생성하고, 각 PTE를 해당 물리 페이지에 매핑합니다.
이 과정이 vmalloc이 kmalloc보다 느린 주된 이유입니다.
- 가상 주소 반환 — 매핑이 완료된 가상 주소를 호출자에게 반환합니다. 이 주소로 연속 메모리처럼 접근할 수 있습니다.
해제 시
vfree()를 호출하면 페이지 테이블 해제, TLB flush, 물리 페이지 반환이 수행됩니다.
가상 주소 공간 레이아웃
x86_64 아키텍처에서 커널 가상 주소 공간은 여러 영역으로 나뉘며, vmalloc 영역은 그 중 하나입니다. VMALLOC_START에서 VMALLOC_END까지의 범위가 vmalloc 전용 영역으로, 여기에 vmalloc, vmap, ioremap 할당이 모두 배치됩니다.
/* arch/x86/include/asm/pgtable_64_types.h */
/* 4-level paging (48-bit VA) */
#define VMALLOC_START vmalloc_base /* ~0xffffc90000000000 */
#define VMALLOC_SIZE_TB 32UL
#define VMALLOC_END (VMALLOC_START + (VMALLOC_SIZE_TB << 40) - 1)
/* 5-level paging (57-bit VA) — 더 넓은 영역 사용 가능 */
/* VMALLOC_SIZE_TB = 12800UL (12800 TB!) */
/* ARM64 */
/* VMALLOC_START: MODULES_END (모듈 영역 바로 위) */
/* VMALLOC_END: VMEMMAP_START - guard 영역 */
/* VMALLOC 영역 크기 확인 (부팅 시 dmesg) */
/* [ 0.000000] vmalloc : 0xffffc90000000000 - 0xffffe8ffffffffff (32768 GB) */
vmalloc 내부 아키텍처
vmalloc 서브시스템은 두 가지 핵심 자료구조로 가상 주소 영역을 관리합니다: vmap_area는 가상 주소 범위 관리에, vm_struct는 할당 메타데이터 관리에 사용됩니다.
/* include/linux/vmalloc.h — 핵심 자료구조 */
struct vm_struct {
struct vm_struct *next; /* 전역 리스트 연결 */
void *addr; /* 할당된 가상 주소 시작 */
unsigned long size; /* 크기 (guard page 포함) */
unsigned long flags; /* VM_ALLOC, VM_MAP, VM_IOREMAP 등 */
struct page **pages; /* 할당된 물리 페이지 포인터 배열 */
unsigned int nr_pages; /* 할당된 페이지 수 */
phys_addr_t phys_addr; /* ioremap 시 물리 주소 */
const void *caller; /* 할당 호출 위치 (디버깅용) */
};
/* vm_struct.flags 값들 */
#define VM_IOREMAP 0x00000001 /* ioremap() 매핑 */
#define VM_ALLOC 0x00000002 /* vmalloc() 할당 */
#define VM_MAP 0x00000004 /* vmap() 매핑 */
#define VM_USERMAP 0x00000008 /* remap_vmalloc_range() 사용 */
#define VM_DMA_COHERENT 0x00000010 /* DMA coherent 할당 */
#define VM_FLUSH_RESET_PERMS 0x00000100 /* 모듈 로딩 시 권한 리셋 */
/* mm/vmalloc.c — vmap_area 관리 전역 변수 */
static struct rb_root vmap_area_root = RB_ROOT; /* busy 영역 RB 트리 */
static struct rb_root free_vmap_area_root = RB_ROOT; /* free 영역 RB 트리 */
static DEFINE_SPINLOCK(vmap_area_lock); /* 글로벌 락 */
static struct list_head vmap_area_list; /* 정렬된 리스트 */
static unsigned long vmap_area_pcpu_hole; /* percpu 할당 힌트 */
/* vmap_area 구조체 */
struct vmap_area {
unsigned long va_start; /* 가상 주소 시작 */
unsigned long va_end; /* 가상 주소 끝 */
struct rb_node rb_node; /* busy/free RB 트리 노드 */
struct list_head list; /* 전역 리스트 */
union {
unsigned long subtree_max_size; /* free 트리: 서브트리 최대 빈 공간 */
struct vm_struct *vm; /* busy: 연결된 vm_struct */
};
unsigned long flags; /* VMAP_RAM, BUSY, DIRTY 등 */
};
vmalloc 할당 흐름
vmalloc(size) 호출 시 내부에서 수행되는 전체 과정을 단계별로 추적합니다. 핵심 경로는 가상 주소 공간 확보 → 물리 페이지 할당 → 페이지 테이블 매핑의 3단계입니다.
/* mm/vmalloc.c — vmalloc 할당 핵심 경로 (간략화) */
void *vmalloc(unsigned long size)
{
return __vmalloc_node(size, 1, GFP_KERNEL, NUMA_NO_NODE,
__builtin_return_address(0));
}
EXPORT_SYMBOL(vmalloc);
static void *__vmalloc_node(unsigned long size, unsigned long align,
gfp_t gfp_mask, int node, const void *caller)
{
return __vmalloc_node_range(size, align,
VMALLOC_START, VMALLOC_END,
gfp_mask, PAGE_KERNEL, 0, node, caller);
}
void *__vmalloc_node_range(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;
unsigned long real_size = size;
/* 크기를 PAGE_SIZE로 올림 정렬 */
size = PAGE_ALIGN(size);
/* guard page 크기 추가 */
size += PAGE_SIZE;
/* 1단계: 가상 주소 공간 확보 + vm_struct 할당 */
area = __get_vm_area_node(real_size, align, VM_ALLOC | vm_flags,
start, end, node, gfp_mask, caller);
if (!area)
return NULL;
/* 2단계: 물리 페이지 할당 + 매핑 */
ret = __vmalloc_area_node(area, gfp_mask, prot, node);
if (!ret)
return NULL;
/* 3단계: KASAN shadow 메모리 초기화 (디버깅) */
kasan_vmalloc(area, size, gfp_mask);
return area->addr;
}
코드 설명
-
3-7행
vmalloc()은__vmalloc_node()의 래퍼로, GFP_KERNEL 플래그와 NUMA_NO_NODE를 사용합니다.__builtin_return_address(0)은 호출 위치를 기록합니다. - 27-28행 요청 크기를 페이지 크기로 올림 정렬하고, guard page 크기(PAGE_SIZE)를 추가합니다.
-
31-34행
__get_vm_area_node()는 free RB 트리에서 빈 공간을 찾고,vmap_area와vm_struct를 할당합니다. -
37-39행
__vmalloc_area_node()에서 물리 페이지를 할당하고 페이지 테이블을 설정합니다.
페이지 테이블 매핑
vmalloc의 핵심은 불연속 물리 페이지를 연속 가상 주소에 매핑하는 페이지 테이블 설정입니다. vmap_pages_range()가 4단계 페이지 테이블(PGD→PUD→PMD→PTE)을 순회하며 각 PTE에 물리 페이지를 기록합니다.
/* mm/vmalloc.c — 페이지 테이블 매핑 핵심 */
static int vmap_pages_pte_range(
pmd_t *pmd, unsigned long addr, unsigned long end,
pgprot_t prot, struct page **pages, int *nr)
{
pte_t *pte;
pte = pte_alloc_kernel(pmd, addr); /* PTE 테이블 할당 */
if (!pte)
return -ENOMEM;
do {
struct page *page = pages[*nr];
/* 이미 매핑된 PTE가 있으면 BUG */
if (WARN_ON(!pte_none(*pte)))
return -EBUSY;
if (WARN_ON(!page))
return -ENOMEM;
/* PTE에 물리 페이지 매핑 기록 */
set_pte_at(&init_mm, addr, pte,
mk_pte(page, prot));
(*nr)++;
} while (pte++, addr += PAGE_SIZE, addr != end);
return 0;
}
vmalloc_fault()가 호출되어 init_mm에서 PGD 엔트리를 복사합니다.
vfree 해제 흐름
vfree()는 vmalloc으로 할당된 메모리를 해제합니다. 성능 최적화를 위해 lazy TLB flush 메커니즘을 사용하여 즉시 모든 CPU의 TLB를 무효화하지 않고, 일정량이 쌓이면 일괄 처리합니다.
/* mm/vmalloc.c — vfree 핵심 경로 */
void vfree(const void *addr)
{
if (!addr)
return;
/* 인터럽트 컨텍스트에서 호출 시 워크큐로 지연 */
if (unlikely(in_interrupt())) {
struct vfree_deferred *p = this_cpu_ptr(&vfree_deferred);
llist_add((struct llist_node *)addr, &p->list);
schedule_work(&p->wq);
return;
}
__vfree(addr);
}
EXPORT_SYMBOL(vfree);
static void __vfree(const void *addr)
{
struct vm_struct *vm;
/* vm_struct 제거 + 페이지 테이블 언매핑 */
vm = remove_vm_area(addr);
if (unlikely(!vm)) {
WARN(1, "Trying to vfree() nonexistent vm area (%p)\n", addr);
return;
}
/* 물리 페이지 해제 */
if (vm->flags & VM_ALLOC)
__vfree_pages(vm->pages, vm->nr_pages);
kfree(vm);
}
/* Lazy TLB purge 임계값 */
#define VMAP_PURGE_THRESHOLD (1UL << 20) /* 1MB worth of vmap ranges */
static void purge_vmap_area_lazy(void)
{
/* 1. 모든 CPU의 TLB를 일괄 flush */
flush_tlb_kernel_range(start, end);
/* 2. purge_list의 vmap_area를 free RB 트리로 이동 */
free_purged_vmap_areas(&local_purge_list);
}
vfree()는 인터럽트 컨텍스트에서 호출할 수 있지만, 이 경우 실제 해제는 워크큐에서 지연 처리됩니다. NULL 포인터를 전달하면 아무 일도 하지 않습니다(안전). 단, vmalloc()이 아닌 다른 할당자에서 할당한 메모리에 vfree()를 호출하면 커널 패닉이 발생합니다.
vmap / vunmap
vmap()은 이미 할당된 물리 페이지 배열을 vmalloc 영역에 연속 가상 주소로 매핑하는 함수입니다. vmalloc()과 달리 물리 페이지 할당을 하지 않고, 매핑만 수행합니다.
/* include/linux/vmalloc.h */
/*
* vmap: 이미 할당된 페이지 배열 → 가상 주소 매핑
* @pages: 매핑할 struct page 포인터 배열
* @count: 페이지 수
* @flags: VM_MAP 등
* @prot: PAGE_KERNEL 등 보호 속성
*/
void *vmap(struct page **pages, unsigned int count,
unsigned long flags, pgprot_t prot);
/* vunmap: 가상 주소 매핑 해제 (페이지는 해제하지 않음!) */
void vunmap(const void *addr);
/* === 사용 예제 === */
struct page *pages[4];
void *vaddr;
int i;
/* 1. 물리 페이지를 별도로 할당 */
for (i = 0; i < 4; i++) {
pages[i] = alloc_page(GFP_KERNEL);
if (!pages[i])
goto err;
}
/* 2. vmalloc 영역에 연속 매핑 */
vaddr = vmap(pages, 4, VM_MAP, PAGE_KERNEL);
if (!vaddr)
goto err;
/* 3. vaddr로 16KB 연속 영역처럼 접근 가능 */
memset(vaddr, 0, 4 * PAGE_SIZE);
/* 4. 언매핑 (페이지는 해제 안 됨!) */
vunmap(vaddr);
/* 5. 페이지 별도 해제 */
for (i = 0; i < 4; i++)
__free_page(pages[i]);
vmalloc()= 물리 페이지 할당 + 가상 매핑 (일체형)vmap()= 가상 매핑만 수행 (페이지는 이미 할당되어 있어야 함)vunmap()= 매핑만 해제 (페이지는 호출자가 별도로 해제해야 함)vfree()= 매핑 해제 + 페이지 해제 (일체형)
vm_map_ram / vm_unmap_ram
빈번한 단기 매핑에는 vm_map_ram()/vm_unmap_ram()이 더 효율적입니다. per-CPU vmap 블록 캐시를 사용하여 락 경합을 줄입니다.
/* 빠른 임시 매핑 (작은 영역) */
void *vm_map_ram(struct page **pages, unsigned int count,
int node);
void vm_unmap_ram(const void *mem, unsigned int count);
/* count <= VMAP_MAX_ALLOC (기본 16 페이지) 이면
* per-CPU vmap_block에서 매핑 → 글로벌 락 없이 빠름
* 그 이상이면 일반 vmap 경로로 fallback */
ioremap과의 관계
ioremap()은 디바이스의 MMIO(Memory-Mapped I/O) 영역을 커널 가상 주소 공간에 매핑하는 함수로, vmalloc 인프라를 공유합니다. ioremap 할당도 vmalloc 영역(VMALLOC_START~VMALLOC_END)에 배치되며, vmap_area와 vm_struct(VM_IOREMAP 플래그)를 사용합니다.
/* arch/x86/mm/ioremap.c — ioremap 핵심 */
void __iomem *ioremap(resource_size_t phys_addr, unsigned long size)
{
return __ioremap_caller(phys_addr, size,
PAGE_KERNEL_IO, /* uncacheable, I/O 접근 속성 */
__builtin_return_address(0),
is_new_memtype_allowed(phys_addr, size));
}
/* 내부 동작:
* 1. get_vm_area_caller() → vmap_area 할당 (VM_IOREMAP 플래그)
* 2. ioremap_page_range() → 페이지 테이블 매핑
* (vmalloc과 달리 물리 페이지 할당 없이 직접 물리 주소 매핑)
* 3. __iomem 포인터 반환
*/
/* ioremap 변형들 */
ioremap(addr, size) /* Uncacheable (UC) */
ioremap_wc(addr, size) /* Write-Combining */
ioremap_wt(addr, size) /* Write-Through */
ioremap_cache(addr, size) /* Write-Back (cacheable) */
/* 해제 */
iounmap(vaddr); /* vunmap과 유사한 경로 */
| 함수 | 물리 페이지 할당 | 매핑 대상 | vm_struct flags |
|---|---|---|---|
vmalloc() | Buddy에서 할당 | RAM 페이지 | VM_ALLOC |
vmap() | 할당 안 함 | 이미 할당된 RAM 페이지 | VM_MAP |
ioremap() | 할당 안 함 | 디바이스 MMIO 주소 | VM_IOREMAP |
ioremap()으로 매핑된 영역에는 일반 포인터 역참조 대신 반드시 readl()/writel() 등의 I/O 접근 함수를 사용해야 합니다. 컴파일러 최적화, 메모리 순서 보장, 엔디안 변환 등의 이유입니다. __iomem 어노테이션은 Sparse 도구가 이를 검증하는 데 사용합니다.
vmalloc_to_page / vmalloc_to_pfn 변환
vmalloc 영역의 가상 주소에서 대응하는 물리 페이지(struct page)나 PFN(Page Frame Number)을 구하는 함수입니다. vmalloc 메모리를 사용자 공간에 매핑하거나, DMA 설정 시 필요합니다.
/* mm/vmalloc.c — 가상 주소 → 물리 페이지 변환 */
struct page *vmalloc_to_page(const void *vmalloc_addr)
{
unsigned long addr = (unsigned long)vmalloc_addr;
struct page *page = NULL;
pgd_t *pgd = pgd_offset_k(addr);
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *ptep, pte;
/* 주소가 vmalloc 영역인지 확인 */
if (!is_vmalloc_addr(vmalloc_addr))
return NULL;
/* 4단계 페이지 테이블 워크 */
if (pgd_none(*pgd)) return NULL;
p4d = p4d_offset(pgd, addr);
if (p4d_none(*p4d)) return NULL;
pud = pud_offset(p4d, addr);
/* huge PUD 매핑 확인 (1GB page) */
if (pud_large(*pud))
return pud_page(*pud) + ((addr & ~PUD_MASK) >> PAGE_SHIFT);
pmd = pmd_offset(pud, addr);
/* huge PMD 매핑 확인 (2MB page) */
if (pmd_large(*pmd))
return pmd_page(*pmd) + ((addr & ~PMD_MASK) >> PAGE_SHIFT);
ptep = pte_offset_kernel(pmd, addr);
pte = *ptep;
if (pte_present(pte))
page = pte_page(pte);
return page;
}
EXPORT_SYMBOL(vmalloc_to_page);
/* PFN 변환 */
unsigned long vmalloc_to_pfn(const void *vmalloc_addr)
{
return page_to_pfn(vmalloc_to_page(vmalloc_addr));
}
EXPORT_SYMBOL(vmalloc_to_pfn);
사용 사례: vmalloc 메모리를 사용자 공간에 매핑
/* vmalloc 메모리를 mmap으로 사용자 공간에 매핑하는 예제 */
static int my_mmap(struct file *filp,
struct vm_area_struct *vma)
{
unsigned long uaddr = vma->vm_start;
unsigned long size = vma->vm_end - vma->vm_start;
void *kaddr = my_vmalloc_buffer;
int ret;
/* remap_vmalloc_range: vmalloc 메모리 → 사용자 공간 매핑 */
ret = remap_vmalloc_range(vma, kaddr, 0);
if (ret)
return ret;
return 0;
}
/* 또는 페이지 단위로 직접 매핑 */
static int my_mmap_manual(struct file *filp,
struct vm_area_struct *vma)
{
unsigned long offset;
for (offset = 0; offset < size; offset += PAGE_SIZE) {
struct page *page = vmalloc_to_page(kaddr + offset);
if (vm_insert_page(vma, uaddr + offset, page))
return -EAGAIN;
}
return 0;
}
Huge vmalloc
Linux 5.15부터 도입된 huge vmalloc(CONFIG_HAVE_ARCH_HUGE_VMALLOC)은 vmalloc 할당 시 4KB 페이지 대신 2MB huge page (PMD 수준)를 사용할 수 있게 합니다. 이는 대형 vmalloc 할당의 TLB 효율을 크게 개선합니다.
/* mm/vmalloc.c — huge vmalloc 지원 */
/*
* CONFIG_HAVE_ARCH_HUGE_VMALLOC 활성 시:
* - vmalloc 할당이 PMD_SIZE(2MB) 이상이면 huge page 사용 시도
* - Buddy에서 order-9 compound page 할당 (2MB)
* - PMD 엔트리에 직접 매핑 (PTE 불필요)
*
* 실패 시 자동으로 4KB page fallback
*/
static int vmap_try_huge_pmd(pmd_t *pmd, unsigned long addr,
unsigned long end, phys_addr_t phys_addr,
pgprot_t prot, unsigned int max_page_shift)
{
if (max_page_shift < PMD_SHIFT)
return 0;
/* 2MB 정렬 확인 */
if (!IS_ALIGNED(addr, PMD_SIZE))
return 0;
if (end - addr < PMD_SIZE)
return 0;
/* PMD에 huge 매핑 설정 */
set_pmd_at(&init_mm, addr, pmd,
pmd_mkhuge(pfn_pmd(phys_addr >> PAGE_SHIFT, prot)));
return 1; /* 성공 */
}
/* huge vmalloc 통계 확인 */
/* /proc/vmallocinfo에서 확인 가능:
* 0xffffc90000400000-0xffffc90000600000 2097152 ... pages=1 vmalloc hugepages
* → 2MB를 1개의 huge page로 매핑
*/
CONFIG_HAVE_ARCH_HUGE_VMALLOC=y로 빌드하면 자동으로 활성화됩니다. 부팅 옵션 nohugevmalloc으로 비활성화할 수 있습니다. 커널 모듈 로딩에 특히 효과적이며, eBPF JIT 코드에서도 활용됩니다. /proc/vmallocinfo에서 hugepages 키워드로 확인할 수 있습니다.
성능 특성과 오버헤드
vmalloc은 유연한 대형 메모리 할당을 제공하지만, kmalloc에 비해 몇 가지 성능 오버헤드가 있습니다. 이를 이해하면 적절한 할당자를 선택할 수 있습니다.
TLB Miss 오버헤드 분석
vmalloc 메모리의 가장 큰 성능 비용은 TLB miss입니다. 물리적으로 불연속인 각 페이지마다 별도의 TLB 엔트리가 필요합니다.
| 시나리오 | TLB 엔트리 수 | TLB miss 비용 | 개선 방법 |
|---|---|---|---|
| 1MB vmalloc (4KB pages) | 256개 | 높음 | huge vmalloc 사용 |
| 1MB vmalloc (2MB huge) | 1개 | 매우 낮음 | 이미 최적 |
| 1MB kmalloc | 1개 (연속) | 최소 | - |
| 10MB vmalloc (4KB) | 2560개 | 매우 높음 | huge vmalloc 필수 |
| 10MB vmalloc (2MB huge) | 5개 | 낮음 | 이미 최적 |
perf로 측정할 수 있습니다:
perf stat -e dTLB-load-misses,dTLB-store-misses -- ./my_kernel_test
또한 /proc/vmstat의 nr_vmalloc_huge 카운터로 huge vmalloc 활용률을 모니터링할 수 있습니다.
커널 설정
vmalloc 관련 주요 커널 설정 옵션과 부팅 파라미터입니다.
# vmalloc 관련 커널 설정 옵션
# 기본 vmalloc 지원 (항상 빌드됨, 비활성화 불가)
# CONFIG_MMU=y 가 전제 조건
# Huge vmalloc 지원 (아키텍처별)
config HAVE_ARCH_HUGE_VMALLOC
bool
# x86, arm64 등에서 지원
# 2MB PMD 수준 huge page 매핑 활성화
# vmalloc 디버깅
config DEBUG_VIRTUAL
bool "Debug VM translations"
# vmalloc 주소 변환 검증 강화
# virt_to_page() 등의 잘못된 사용 탐지
# KASAN (vmalloc 메모리 접근 검증)
config KASAN_VMALLOC
bool "Back mappings in vmalloc space with real shadow memory"
# vmalloc 영역에 대한 KASAN shadow 메모리 할당
# use-after-free, out-of-bounds 등 탐지
부팅 파라미터
# vmalloc 관련 부팅 파라미터
# vmalloc 영역 크기 설정 (x86_32에서만 유효)
vmalloc=256M # vmalloc 영역을 256MB로 설정
# x86_64에서는 32TB 고정이므로 불필요
# Huge vmalloc 비활성화
nohugevmalloc # 2MB huge page vmalloc 비활성화
# KASAN vmalloc 제어
kasan.vmalloc=on # vmalloc KASAN 활성화
kasan.vmalloc=off # vmalloc KASAN 비활성화
/proc/vmallocinfo 포맷
# /proc/vmallocinfo 출력 예시
cat /proc/vmallocinfo
# 출력 형식: start-end size caller flags pages=N vmalloc [hugepages] [N*node]
0xffffc90000000000-0xffffc90000005000 20480 load_module+0x1234 pages=4 vmalloc N0=4
0xffffc90000010000-0xffffc90000210000 2097152 bpf_jit_alloc+0x100 pages=1 vmalloc hugepages N0=1
0xffffc90000400000-0xffffc90000420000 131072 n_tty_open+0x15 pages=32 vmalloc N0=32
0xffffc90000800000-0xffffc90000810000 65536 __ioremap_caller+0x90 phys=0xfed00000 ioremap
# 필드 설명:
# start-end : 가상 주소 범위
# size : 할당 크기 (바이트)
# caller : 할당 호출 함수 (심볼+오프셋)
# pages=N : 사용된 물리 페이지 수
# vmalloc : vmalloc 할당 표시
# hugepages : huge page 사용 표시
# ioremap : ioremap 매핑 표시
# phys= : ioremap의 물리 주소
# N0=N : NUMA 노드별 페이지 분포
디버깅과 모니터링
vmalloc 관련 문제를 진단하고 모니터링하는 다양한 방법을 설명합니다.
/proc/vmallocinfo 분석 스크립트
#!/bin/bash
# vmalloc 사용 현황 분석 스크립트
echo "=== vmalloc 전체 통계 ==="
echo "총 할당 수:" $(wc -l < /proc/vmallocinfo)
echo "총 사용 크기:" $(awk '{sum += $2} END {printf "%.2f MB\n", sum/1048576}' /proc/vmallocinfo)
echo ""
echo "=== 유형별 분류 ==="
echo "vmalloc:" $(grep -c 'vmalloc$' /proc/vmallocinfo)
echo "ioremap:" $(grep -c 'ioremap' /proc/vmallocinfo)
echo "modules:" $(grep -c 'load_module' /proc/vmallocinfo)
echo "hugepages:" $(grep -c 'hugepages' /proc/vmallocinfo)
echo ""
echo "=== 가장 큰 할당 Top 10 ==="
sort -k2 -n -r /proc/vmallocinfo | head -10
echo ""
echo "=== 호출자별 할당 횟수 Top 10 ==="
awk '{print $3}' /proc/vmallocinfo | sort | uniq -c | sort -rn | head -10
echo ""
echo "=== NUMA 노드별 페이지 분포 ==="
grep -oP 'N\d+=\d+' /proc/vmallocinfo | sort | awk -F= '{
nodes[$1]+=$2
} END {
for (n in nodes) printf "%s: %d pages (%.2f MB)\n", n, nodes[n], nodes[n]*4/1024
}'
ftrace로 vmalloc 추적
# ftrace로 vmalloc/vfree 호출 추적
# 1. function_graph tracer 설정
cd /sys/kernel/debug/tracing
echo 0 > tracing_on
echo function_graph > current_tracer
echo vmalloc > set_graph_function
echo vfree >> set_graph_function
echo 1 > tracing_on
# 2. 잠시 후 결과 확인
cat trace | head -50
# 3. kmem tracepoint 사용 (더 정밀한 추적)
echo 1 > events/kmem/kmalloc/enable
echo 1 > events/vmalloc/enable # 커널 6.x 이상
# 4. trace_pipe로 실시간 모니터링
cat trace_pipe | grep -E 'vmalloc|vfree'
# 5. 정리
echo 0 > tracing_on
echo nop > current_tracer
KASAN으로 vmalloc 메모리 버그 탐지
/* KASAN + vmalloc 디버깅 예제 */
/* CONFIG_KASAN_VMALLOC=y 필요 */
void test_vmalloc_oob(void)
{
char *buf = vmalloc(100);
if (!buf)
return;
/* Out-of-bounds 접근 → KASAN이 즉시 감지 */
buf[100] = 'X'; /* BUG: KASAN: slab-out-of-bounds */
vfree(buf);
}
void test_vmalloc_uaf(void)
{
char *buf = vmalloc(4096);
vfree(buf);
/* Use-after-free → KASAN이 감지 */
buf[0] = 'Y'; /* BUG: KASAN: use-after-free */
}
/* KASAN 보고서 예시:
* ==================================================================
* BUG: KASAN: vmalloc-out-of-bounds in test_vmalloc_oob+0x38/0x50
* Write of size 1 at addr ffffc90000123064 by task insmod/1234
*
* Allocated by task 1234:
* kasan_save_stack+0x22/0x40
* __vmalloc_node_range+0x1b0/0x2c0
* vmalloc+0x27/0x30
* test_vmalloc_oob+0x18/0x50
* ==================================================================
*/
VmallocTotal— vmalloc 영역 전체 크기VmallocUsed— 현재 사용 중인 vmalloc 크기VmallocChunk— 가장 큰 연속 빈 vmalloc 영역
grep Vmalloc /proc/meminfo로 확인할 수 있습니다.
실전 사용 사례
vmalloc이 실제 커널 코드에서 어떻게 활용되는지 주요 사례를 살펴봅니다.
1. 커널 모듈 로딩
커널 모듈의 코드와 데이터는 vmalloc 영역에 로드됩니다. 모듈 크기가 수십~수백 KB에 달하므로 물리 연속 메모리 확보가 어렵기 때문입니다.
/* kernel/module/main.c — 모듈 로딩 시 vmalloc 사용 */
static int move_module(struct module *mod,
struct load_info *info)
{
/* 모듈 코드 영역 할당 (실행 권한 필요) */
mod->core_layout.base = module_alloc(mod->core_layout.size);
/* module_alloc()의 내부:
* __vmalloc_node_range(size, MODULE_ALIGN,
* MODULES_VADDR, MODULES_END,
* GFP_KERNEL, PAGE_KERNEL_EXEC,
* VM_FLUSH_RESET_PERMS, numa_node, caller);
*
* 모듈은 MODULES_VADDR~MODULES_END 범위에 할당됨
* PAGE_KERNEL_EXEC로 실행 권한 부여
*/
/* 모듈 초기화 전용 영역 (init 후 해제) */
mod->init_layout.base = module_alloc(mod->init_layout.size);
return 0;
}
2. eBPF JIT 컴파일
/* kernel/bpf/core.c — eBPF JIT 코드 할당 */
struct bpf_binary_header *
bpf_jit_binary_alloc(unsigned int proglen,
u8 **image_ptr,
unsigned int alignment,
bpf_jit_fill_hole_t bpf_fill_ill_insns)
{
unsigned int size, hole;
struct bpf_binary_header *hdr;
size = round_up(proglen + sizeof(*hdr), PAGE_SIZE);
/* vmalloc으로 JIT 코드 영역 할당
* 실행 권한(PAGE_KERNEL_EXEC)으로 매핑
* Huge vmalloc이 활성화되면 2MB 매핑 가능 */
hdr = bpf_jit_alloc_exec(size);
if (!hdr)
return NULL;
/* 사용되지 않는 공간을 illegal instruction으로 채움 */
bpf_fill_ill_insns(hdr, size);
*image_ptr = &hdr->image[hole];
return hdr;
}
3. 대형 버퍼 할당 (파일시스템, 네트워킹)
/* 파일시스템 inode 캐시 등 대형 해시 테이블 */
static void init_large_hash_table(void)
{
size_t table_size = 1UL << 20; /* 1M 엔트리 */
size_t bytes = table_size * sizeof(struct hlist_head);
/* 수 MB 해시 테이블 → vmalloc 사용 */
hash_table = vzalloc(bytes);
if (!hash_table)
panic("Failed to allocate hash table\n");
pr_info("Hash table: %zu entries (%zu KB)\n",
table_size, bytes / 1024);
}
/* 네트워크: 대형 수신 버퍼 */
static void alloc_rx_buffer(struct net_device *dev)
{
/* 64KB 이상 버퍼 → kvmalloc 사용 (자동 fallback) */
dev->rx_buf = kvmalloc(256 * 1024, GFP_KERNEL);
if (!dev->rx_buf)
return;
}
/* 커널 모듈에서의 vmalloc 사용 예제 */
static int __init my_module_init(void)
{
void *buffer;
struct page *page;
unsigned long pfn;
/* 1. 기본 vmalloc */
buffer = vmalloc(1024 * 1024); /* 1MB */
if (!buffer)
return -ENOMEM;
/* 2. 0으로 초기화된 vmalloc */
buffer = vzalloc(1024 * 1024);
/* 3. 특정 NUMA 노드에서 할당 */
buffer = vmalloc_node(1024 * 1024, 0); /* NUMA node 0 */
/* 4. 가상 주소 → 물리 페이지 변환 */
page = vmalloc_to_page(buffer);
pfn = vmalloc_to_pfn(buffer);
pr_info("vmalloc addr=%px, page=%px, pfn=%lu\n",
buffer, page, pfn);
/* 5. vmalloc 주소인지 확인 */
pr_info("is_vmalloc_addr=%d\n",
is_vmalloc_addr(buffer));
vfree(buffer);
return 0;
}
module_init(my_module_init);
MODULE_LICENSE("GPL");
코드 설명
-
vzalloc()
vmalloc()+memset(0)의 결합입니다. 내부적으로__GFP_ZERO플래그를 사용하여 할당된 모든 페이지를 0으로 초기화합니다. - vmalloc_node() 특정 NUMA 노드에서 물리 페이지를 할당하도록 요청합니다. 접근 지역성이 중요한 경우에 사용합니다.
-
is_vmalloc_addr()
주어진 주소가 vmalloc 영역(VMALLOC_START ~ VMALLOC_END)에 속하는지 확인합니다.
kfree()와vfree()를 구분할 때 유용합니다.
vmalloc API 전체 목록
| 함수 | 설명 | 비고 |
|---|---|---|
vmalloc(size) | 가상 연속 메모리 할당 | GFP_KERNEL, sleep 가능 |
vzalloc(size) | vmalloc + 0 초기화 | 내부적으로 __GFP_ZERO |
vmalloc_node(size, node) | 특정 NUMA 노드에서 할당 | 메모리 지역성 최적화 |
vzalloc_node(size, node) | vmalloc_node + 0 초기화 | |
vmalloc_32(size) | 32비트 주소 범위(DMA32) 할당 | GFP_DMA32 사용 |
vmalloc_user(size) | 사용자 공간 매핑 가능한 할당 | VM_USERMAP 설정 |
__vmalloc(size, gfp) | GFP 플래그 지정 할당 | 세밀한 제어 필요 시 |
vfree(addr) | vmalloc 메모리 해제 | 인터럽트 컨텍스트에서도 호출 가능 |
vmap(pages, count, flags, prot) | 기존 페이지 배열 매핑 | 페이지 할당 안 함 |
vunmap(addr) | vmap 매핑 해제 | 페이지 해제 안 함 |
kvmalloc(size, gfp) | kmalloc 시도 → vmalloc fallback | 범용 할당자 |
kvfree(addr) | kvmalloc 해제 | kmalloc/vmalloc 자동 구분 |
vmalloc_to_page(addr) | 가상 주소 → struct page | 페이지 테이블 워크 |
vmalloc_to_pfn(addr) | 가상 주소 → PFN | |
is_vmalloc_addr(addr) | vmalloc 영역 주소인지 확인 | |
remap_vmalloc_range(vma, addr, pgoff) | vmalloc → 사용자 공간 매핑 | mmap 구현 시 |
내부 알고리즘 상세
vmalloc의 가상 주소 공간 관리에 사용되는 핵심 알고리즘을 상세히 설명합니다.
Free Space 탐색: Augmented Red-Black Tree
Linux 커널 5.2부터 vmalloc의 빈 공간 탐색은 augmented RB tree를 사용합니다. 각 노드에 subtree_max_size를 저장하여, 요청 크기를 수용할 수 없는 서브트리를 빠르게 스킵합니다.
/* mm/vmalloc.c — augmented RB tree 기반 빈 공간 탐색 */
static struct vmap_area *
find_vmap_lowest_match(struct rb_root *root,
unsigned long size, unsigned long align,
unsigned long vstart)
{
struct vmap_area *va;
struct rb_node *node;
unsigned long length;
node = root->rb_node;
length = size + align; /* 정렬 고려한 최소 필요 크기 */
while (node) {
va = rb_entry(node, struct vmap_area, rb_node);
/* 왼쪽 서브트리가 충분한 공간을 가지면 왼쪽 우선 */
if (get_subtree_max_size(node->rb_left) >= length) {
node = node->rb_left;
continue;
}
/* 현재 노드가 적합한지 확인 */
if (va_size(va) >= length && va->va_start >= vstart)
return va; /* 가장 낮은 주소의 적합한 영역 */
/* 오른쪽 서브트리 탐색 */
if (get_subtree_max_size(node->rb_right) >= length) {
node = node->rb_right;
continue;
}
break; /* 적합한 공간 없음 */
}
return NULL; /* VMALLOC_START~VMALLOC_END 공간 부족 */
}
kvmalloc: 자동 Fallback 메커니즘
/* mm/util.c — kvmalloc 구현 */
void *kvmalloc_node(size_t size, gfp_t flags, int node)
{
gfp_t kmalloc_flags = flags;
void *ret;
/* PAGE_SIZE 이하는 항상 kmalloc */
if (size <= PAGE_SIZE)
return kmalloc_node(size, flags, node);
/* 1차 시도: kmalloc (빠르지만 실패할 수 있음)
* __GFP_NORETRY: OOM killer 호출 방지
* ~__GFP_DIRECT_RECLAIM: 직접 회수 비활성화 (빠른 실패)
*/
kmalloc_flags |= __GFP_NOWARN | __GFP_NORETRY;
if (!(flags & __GFP_RETRY_MAYFAIL))
kmalloc_flags &= ~__GFP_DIRECT_RECLAIM;
ret = kmalloc_node(size, kmalloc_flags, node);
if (ret)
return ret; /* kmalloc 성공 */
/* 2차 시도: vmalloc fallback
* kmalloc 실패 → vmalloc으로 안정적 할당 */
return __vmalloc_node(size, 1, flags, node,
__builtin_return_address(0));
}
EXPORT_SYMBOL(kvmalloc_node);
/* kvfree: kvmalloc 해제 (kmalloc/vmalloc 자동 구분) */
void kvfree(const void *addr)
{
if (is_vmalloc_addr(addr))
vfree(addr);
else
kfree(addr);
}
EXPORT_SYMBOL(kvfree);
vmalloc 전체 아키텍처 통합
vmalloc 서브시스템의 전체 구조를 하나의 통합 다이어그램으로 정리합니다.
vmalloc 주소 단편화와 주소 고갈
vmalloc은 물리 단편화에 강하지만, 반대로 가상 주소 단편화 문제를 겪을 수 있습니다. 작은 할당/해제가 반복되면 빈 영역이 조각나고, VmallocUsed가 낮아도 큰 연속 구간(VmallocChunk)이 부족해 대형 할당이 실패할 수 있습니다.
# vmalloc 단편화/고갈 점검
grep Vmalloc /proc/meminfo
# VmallocTotal / VmallocUsed / VmallocChunk 확인
# 가장 큰 빈 구간이 요청 크기보다 작은지 확인
cat /proc/vmallocinfo | awk '{sum += $2} END {printf "used=%.2f MB\n", sum/1048576}'
VmallocUsed가 낮아도 VmallocChunk가 작으면 대형 할당 실패가 발생합니다. 이 경우 메모리 총량 문제가 아니라 가상 주소 연속성 문제입니다.
vmalloc 권한 전환과 W^X
모듈 로더, eBPF JIT, ftrace trampolines는 vmalloc 영역에서 코드를 생성한 뒤 권한을 전환합니다. 일반 패턴은 RW로 작성 후 RX로 전환하는 W^X 정책입니다.
/* 모듈/JIT 코드 권한 전환 패턴 (요약) */
void *text = module_alloc(text_size);
if (!text)
return -ENOMEM;
/* 1) RW 상태에서 코드 생성 */
memcpy(text, image, text_size);
/* 2) RX 전환 (W^X) */
set_memory_rox((unsigned long)text, text_size >> PAGE_SHIFT);
/* 3) 아키텍처별 캐시/TLB 동기화 수행 */
NUMA 관점 vmalloc 성능 튜닝
vmalloc_node()는 선호 NUMA 노드를 지정하지만, 페이지 부족 시 다른 노드에서 할당될 수 있습니다. 따라서 대형 버퍼에서 지연이 문제라면 노드 바인딩과 함께 /proc/vmallocinfo의 노드 분포(N0=.. N1=..)를 반드시 확인해야 합니다.
# vmalloc 할당의 NUMA 분포 점검
grep -E "vmalloc|hugepages" /proc/vmallocinfo | head -40
# 노드 통계 집계
grep -oE 'N[0-9]+=[0-9]+' /proc/vmallocinfo | awk -F= '{a[$1]+=$2} END {for (i in a) print i,a[i]}'
# 워크로드 NUMA 바인딩
numactl --cpubind=0 --membind=0 ./workload
vmalloc 실패 대응 플레이북
vmalloc 실패는 단일 원인이 아니라 "가상 주소 단편화, 페이지 부족, 권한/컨텍스트 오용"이 복합적으로 얽히는 경우가 많습니다. 아래 순서로 확인하면 원인 분류가 빠릅니다.
# 1) 커널 로그에서 vmalloc 실패 확인
dmesg | grep -Ei 'vmalloc|vmap|out of memory|allocation failed'
# 2) vmalloc 용량/연속성 확인
grep Vmalloc /proc/meminfo
cat /proc/vmallocinfo | tail -100
# 3) 물리 페이지 압박 확인
cat /proc/buddyinfo
cat /proc/zoneinfo | grep -E 'Node|nr_free_pages'
# 4) 원인별 대응 예시
# - 큰 단일 요청을 분할
# - kvmalloc/kvfree로 전환
# - 필요 시 huge vmalloc 여부 점검
vmap 락 경합과 대량 매핑 처리
대규모 시스템에서 vmalloc/vfree가 여러 CPU에서 동시에 호출되면, vmap 영역 메타데이터 업데이트와
페이지 테이블 갱신 구간에서 락 경합이 발생할 수 있습니다. 특히 작은 크기 할당을 고빈도로 반복하면,
실제 메모리 사용량보다 관리 오버헤드가 먼저 병목이 되는 경우가 많습니다.
vfree 이후 lazy flush와 회수 지연
vfree()는 즉시 반환되지만, 내부적으로는 해제된 매핑의 TLB 정리와 페이지테이블 해제가 지연 배치될 수 있습니다.
따라서 "vfree 호출 직후 메모리가 바로 줄지 않는다"는 관찰이 이상이 아닐 수 있습니다.
/* 개념 예시: vfree 후 지연 회수 관점 */
void release_buffer(void *p)
{
vfree(p); /* API 반환은 즉시 */
/* 실제 TLB/페이지테이블 회수는 배치 처리될 수 있음 */
}
| 관찰 증상 | 해석 | 대응 |
|---|---|---|
| /proc/vmallocinfo 감소 지연 | 지연 회수 배치 경로 | 짧은 간격 재측정으로 추세 확인 |
| 주기적 지연 스파이크 | 배치 flush 타이밍 집중 | 해제 시점 분산, 버퍼 재사용 |
| vmap 실패 간헐 발생 | 주소 단편화 + 회수 타이밍 | 요청 크기 분할, 장수명 객체 정리 |
vmalloc과 ioremap 상호작용
아키텍처에 따라 vmalloc/vmap 계열과 ioremap 계열이 같은 상위 가상 주소 공간 정책을 공유합니다. 드라이버가 대형 MMIO 매핑을 자주 만들고 지우는 환경에서는, 일반 vmalloc 사용자와 주소 공간 압력이 간접 충돌할 수 있습니다.
/proc/meminfo의 일반 메모리 지표와 Vmalloc* 지표를 분리해서 해석해야 정확합니다.
참고자료
커널 소스 코드
- mm/vmalloc.c — vmalloc 핵심 구현
- include/linux/vmalloc.h — vmalloc 헤더 (API, 자료구조)
- mm/util.c — kvmalloc 구현
- arch/x86/mm/ioremap.c — ioremap 구현
커널 문서
관련 커밋 및 패치
- mm/vmalloc: huge vmalloc mappings (v5.15) — huge vmalloc 도입 패치
- mm/vmalloc: use augmented rbtree (v5.2) — augmented RB tree 도입
참고 서적 및 문서
- Understanding the Linux Kernel, 3rd Edition — Chapter 8: Memory Management (vmalloc)
- Linux Kernel Development, 3rd Edition — Robert Love, Chapter 12
- Professional Linux Kernel Architecture — Wolfgang Mauerer, Chapter 3.5
- LWN: Huge vmalloc mappings
- 메모리 관리 (기초) — 버디 시스템, 페이지 할당
- 메모리 관리 (심화) — 메모리 회수, compaction
- Slab 할당자 — kmalloc 내부 동작
- 페이지 테이블 — 가상 주소 변환 상세