vmalloc — 가상 연속 메모리 할당
Linux 커널 vmalloc 서브시스템: 가상 주소 공간(Address Space)에서 연속적이지만 물리적으로는 불연속인 메모리를 할당하는 메커니즘, vmap_area 관리, 페이지 테이블(Page Table) 매핑(Mapping), lazy TLB flush, ioremap, huge vmalloc, 성능 특성, 디버깅(Debugging) 종합 가이드.
핵심 요약
- vmalloc() — 가상 주소 연속, 물리 주소 불연속 메모리를 할당합니다. 큰 버퍼 할당에 적합합니다.
- vmap_area — vmalloc 영역 내 할당된 가상 주소 범위를 관리하는 자료구조입니다.
- vm_struct — vmalloc 할당 메타데이터(크기, 플래그, 페이지 배열 등)를 담는 구조체(Struct)입니다.
- 페이지 테이블 매핑 — 개별 물리 페이지를 가상 주소에 매핑하기 위해 PGD→PUD→PMD→PTE 체인을 설정합니다.
- lazy TLB flush — vfree 시 TLB 무효화(Invalidation)를 지연(Latency)시켜 성능을 최적화합니다.
- kvmalloc() — kmalloc 시도 후 실패하면 vmalloc으로 자동 fallback하는 편의 함수입니다.
/* vmalloc 기본 사용 패턴 */
#include <linux/vmalloc.h>
void *buf = vmalloc(1024 * 1024); /* 1MB 가상 연속 메모리 할당 */
if (!buf)
return -ENOMEM;
memset(buf, 0, 1024 * 1024); /* 초기화 (vzalloc 사용 시 불필요) */
/* ... 버퍼 사용 ... */
vfree(buf); /* 해제 (인터럽트 컨텍스트에서도 가능) */
단계별 이해
- 가상 주소 공간 예약 — VMALLOC_START~VMALLOC_END 범위에서 빈 가상 주소 구간을 찾아 vmap_area를 할당합니다.
핵심 함수:
alloc_vmap_area()→ augmented RB 트리(free_vmap_area_root)를 탐색하여 best-fit 빈 영역을 O(log n)으로 찾습니다. - 물리 페이지 할당 — Buddy 할당자에서 개별 페이지(order-0)를 필요한 수만큼 할당합니다.
핵심 함수:
vm_area_alloc_pages()→ 내부에서alloc_pages_node()를 반복 호출합니다. NUMA 노드를 지정한 경우 해당 노드에서 우선 할당합니다. - 페이지 테이블 설정 — 할당된 가상 주소 범위에 대해 커널 페이지 테이블(PGD→PUD→PMD→PTE)을 생성하고, 각 PTE를 해당 물리 페이지에 매핑합니다.
핵심 함수:
vmap_pages_range()→vmap_pages_range_noflush()→ 각 레벨vmap_pages_p4d_range()/vmap_pages_pud_range()/vmap_pages_pmd_range()/vmap_pages_pte_range()을 호출하여 페이지 테이블을 구축합니다. - 가상 주소 반환 — 매핑이 완료된 가상 주소를 호출자에게 반환합니다. 이 주소로 연속 메모리처럼 접근할 수 있습니다.
해제 경로:
vfree()→__vfree()→remove_vm_area()(페이지 테이블 해제) →free_vmap_area_noflush()(가상 주소 반환) → lazy TLB purge 후 물리 페이지 반환(__free_pages()).
개요
vmalloc()은 커널에서 가상 주소 공간에서 연속적이지만, 물리 메모리(Physical Memory)에서는 불연속적인 메모리를 할당하는 함수입니다. 커널의 두 가지 주요 메모리 할당 인터페이스 중 하나로, kmalloc()과 근본적으로 다른 특성을 가집니다.
왜 vmalloc이 필요한가?
커널이 부팅 후 오랜 시간 동작하면 물리 메모리가 단편화(fragmentation)됩니다. 이 상태에서 수십 KB 이상의 연속 물리 메모리를 확보하기 어려워집니다. kmalloc()은 물리적으로 연속인 메모리를 요구하므로 큰 할당이 실패할 수 있습니다. vmalloc()은 개별 페이지를 따로 할당한 뒤, 커널 페이지 테이블을 조작하여 가상 주소 공간에서 연속으로 보이게 매핑합니다.
vmalloc()으로 할당한 메모리는 DMA에 직접 사용할 수 없습니다. DMA 컨트롤러는 물리 주소(Physical Address)를 사용하므로 물리적으로 연속인 메모리(kmalloc, dma_alloc_coherent)가 필요합니다. IOMMU가 있는 시스템에서는 예외적으로 가능할 수 있습니다.
kmalloc vs vmalloc 비교
| 특성 | kmalloc() | vmalloc() |
|---|---|---|
| 물리 메모리 연속성 | 물리적으로 연속 | 물리적으로 불연속 (페이지 단위) |
| 가상 주소(Virtual Address) 연속성 | 가상 주소도 연속 | 가상 주소만 연속 |
| 최대 할당 크기 | 수 MB (order 제한) | VMALLOC_END - VMALLOC_START (수십~수백 GB) |
| 최소 할당 단위 | 8바이트~ | PAGE_SIZE (4KB) |
| 할당 속도 | 빠름 (슬랩/버디) | 느림 (페이지 테이블 조작 필요) |
| TLB 효율 | 높음 (물리 연속 → 대형 페이지 가능) | 낮음 (페이지별 TLB 엔트리 필요) |
| DMA 호환 | 가능 | 직접 불가 (IOMMU 예외) |
| sleep 가능 | GFP_KERNEL 시 가능 | 항상 sleep 가능 (GFP_KERNEL 내부 사용) |
| 인터럽트(Interrupt) 컨텍스트 | GFP_ATOMIC으로 가능 | 사용 불가 (sleep 발생) |
| 주요 용도 | 작은 객체, DMA 버퍼(Buffer) | 모듈 로딩, 큰 버퍼, 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()를 사용합니다.
가상 주소 공간 레이아웃
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) */
ARM64 vmalloc 레이아웃
ARM64(AArch64)의 커널 가상 주소 공간은 x86_64와 구조가 다릅니다. 48비트 VA(4-level paging) 기준으로, 상위 주소 공간(0xFFFF000000000000~)에 커널 영역이 배치되며, vmalloc 영역은 모듈 영역 바로 위에 위치합니다.
/* arch/arm64/include/asm/memory.h — ARM64 vmalloc 영역 정의 */
/* 48-bit VA, 4KB pages 기준 */
#define VA_BITS 48
#define PAGE_OFFSET (0xFFFF800000000000UL)
/* arch/arm64/include/asm/pgtable.h */
#define VMALLOC_START (MODULES_END)
#define VMALLOC_END (VMEMMAP_START - SZ_256M)
/* 모듈 영역: 커널 이미지에서 BL 명령(±128MB) 범위 내 */
#define MODULES_VADDR (KIMAGE_VADDR)
#define MODULES_END (MODULES_VADDR + SZ_128M)
/* x86_64와의 핵심 차이점:
* 1. 모듈 영역이 vmalloc 아래에 별도 배치 (x86_64: 위)
* 2. vmalloc 영역이 훨씬 넓음 (~256TB vs 32TB)
* 3. KASLR: vmalloc_base가 부팅마다 무작위화
* 4. 52-bit VA (LVA) 지원 시 더 넓은 공간 사용 가능
*/
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_area_node: 벌크 페이지 할당 최적화
Linux 5.13부터 vm_area_alloc_pages()에서 alloc_pages_bulk_node()를 사용하여 여러 페이지를 한 번의 Buddy 호출로 할당합니다. 이전에는 페이지마다 개별 alloc_page()를 호출하여, 1MB 할당 시 256번의 Buddy 진입이 필요했습니다. 벌크 할당은 Buddy 락 획득 횟수를 크게 줄여줍니다.
/* mm/vmalloc.c — vm_area_alloc_pages() 벌크 할당 (Linux 6.x, 간략화) */
static 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;
/* 1단계: huge page 시도 (order > 0, 예: order-9 = 2MB) */
if (order > 0) {
while (nr_allocated < nr_pages) {
struct page *page = alloc_pages_node(nid, gfp, order);
if (!page)
break; /* fallback to order-0 */
pages[nr_allocated++] = page;
}
}
/* 2단계: 벌크 할당 (order-0, per-cpu list 활용)
* alloc_pages_bulk_node()는 per-cpu page list에서
* 최대 nr_pages_need개를 한 번에 가져옵니다.
* zone lock을 한 번만 잡고 다수 페이지를 얻으므로 효율적입니다. */
if (nr_allocated < nr_pages) {
unsigned int nr_pages_need = nr_pages - nr_allocated;
nr_allocated += alloc_pages_bulk_node(
gfp, nid, NULL,
nr_pages_need, pages + nr_allocated);
}
/* 3단계: 벌크에서 못 채운 잔여분은 개별 할당 */
while (nr_allocated < nr_pages) {
struct page *page = alloc_page(gfp);
if (!page)
goto fail;
pages[nr_allocated++] = page;
}
return nr_allocated;
fail:
/* 할당된 페이지 정리 */
while (nr_allocated--)
__free_pages(pages[nr_allocated],
pages[nr_allocated] ? compound_order(pages[nr_allocated]) : 0);
return 0;
}
코드 설명
- 9-15행 huge vmalloc이 활성화된 경우(order > 0), 먼저 compound page(2MB) 단위로 할당을 시도합니다. 실패하면 order-0(4KB) fallback으로 넘어갑니다.
-
22-27행
alloc_pages_bulk_node()는 커널 5.13에 도입된 벌크 할당 API입니다. per-CPU page cache(PCP list)에서 한 번의 락으로 다수 페이지를 꺼냅니다. PCP가 부족하면 Buddy에서 배치로 보충합니다. -
30-35행
벌크 할당에서 채우지 못한 잔여 페이지는
alloc_page()로 하나씩 할당합니다. 이는 메모리 압력이 높을 때 발생할 수 있으며, 이 경로에서 OOM 킬러가 호출될 수 있습니다.
/proc/vmstat의 pgalloc_* 카운터로 벌크 할당 비율을 간접 확인할 수 있습니다.
페이지 테이블 매핑
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 엔트리를 복사합니다.
vmalloc_fault: 크로스-CPU 페이지 테이블 동기화
vmalloc 매핑은 init_mm.pgd(커널 마스터 페이지 테이블)에 설정됩니다. 그러나 다른 프로세스의 페이지 테이블에는 이 변경이 즉시 반영되지 않을 수 있습니다. CPU가 아직 동기화되지 않은 vmalloc 영역에 접근하면 page fault가 발생하고, vmalloc_fault() 핸들러가 init_mm에서 해당 엔트리를 복사하여 동기화합니다.
/* arch/x86/mm/fault.c — vmalloc fault 핸들러 (x86_32 개념, 간략화) */
static noinline int vmalloc_fault(unsigned long address)
{
pgd_t *pgd, *pgd_k;
p4d_t *p4d, *p4d_k;
pud_t *pud, *pud_k;
pmd_t *pmd, *pmd_k;
pte_t *pte_k;
/* vmalloc 영역 주소인지 확인 */
if (!(address >= VMALLOC_START && address < VMALLOC_END))
return -1;
/* init_mm (마스터 PGD)에서 해당 주소의 PGD 엔트리 조회 */
pgd_k = pgd_offset_k(address);
if (pgd_none(*pgd_k))
return -1; /* init_mm에도 매핑이 없으면 실제 fault */
/* 현재 프로세스의 PGD에 복사 */
pgd = pgd_offset(current->mm, address);
if (pgd_none(*pgd))
set_pgd(pgd, *pgd_k);
/* P4D → PUD → PMD → PTE도 필요시 동기화 */
p4d_k = p4d_offset(pgd_k, address);
pud_k = pud_offset(p4d_k, address);
pmd_k = pmd_offset(pud_k, address);
if (pmd_none(*pmd_k))
return -1;
pte_k = pte_offset_kernel(pmd_k, address);
if (!pte_present(*pte_k))
return -1;
return 0; /* 동기화 완료 → fault 복귀 후 명령 재시도 */
}
vmalloc_fault()가 불필요합니다. 단, PUD 이하 수준의 테이블은 init_mm에 직접 할당되므로 모든 프로세스에서 동일한 물리 페이지를 참조합니다.
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()를 호출하면 커널 패닉(Kernel Panic)이 발생합니다.
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 블록 캐시(Cache)를 사용하여 락 경합(Contention)을 줄입니다.
/* 빠른 임시 매핑 (작은 영역) */
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 */
per-CPU vmap_block 캐시 메커니즘
vm_map_ram()은 작은 매핑(16페이지 이하)에서 글로벌 vmap_area_lock을 우회하기 위해 per-CPU vmap_block 캐시를 사용합니다. 각 CPU가 미리 예약한 가상 주소 블록(기본 256KB)에서 로컬하게 매핑을 수행하므로, 다른 CPU와의 락 경합 없이 빠르게 동작합니다.
/* mm/vmalloc.c — per-CPU vmap_block 구조체 */
struct vmap_block {
struct vmap_area *va; /* 예약된 가상 주소 영역 */
unsigned long used_map[BITS_TO_LONGS(VMAP_BBMAP_BITS)];
/* 사용 중인 슬롯 비트맵 */
unsigned long dirty_min; /* dirty 범위 최소 */
unsigned long dirty_max; /* dirty 범위 최대 */
unsigned int free; /* 남은 빈 슬롯 수 */
unsigned int dirty; /* 해제되었지만 TLB flush 대기 중 */
struct list_head free_list; /* per-CPU free 블록 리스트 */
struct list_head dirty_list; /* per-CPU dirty 블록 리스트 */
};
/* VMAP_BBMAP_BITS = VMAP_BLOCK_SIZE / PAGE_SIZE = 64 (256KB/4KB)
* 각 vmap_block은 64개 페이지 슬롯을 가짐
* vm_map_ram()은 연속된 count개 슬롯을 bitmap에서 찾아 PTE 설정
* vm_unmap_ram()은 PTE를 지우고 dirty로 표시
* dirty 슬롯은 나중에 일괄 TLB flush 후 재활용 */
vm_map_ram()/vm_unmap_ram()을 사용하면 글로벌 vmap_area_lock 경합 없이 매핑이 가능하여, 높은 IOPS 워크로드에서 성능이 크게 향상됩니다.
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()으로 매핑된 영역에는 일반 포인터 역참조(Dereference) 대신 반드시 readl()/writel() 등의 I/O 접근 함수를 사용해야 합니다. 컴파일러 최적화(Compiler Optimization), 메모리 순서 보장(Ordering), 엔디안(Endianness) 변환 등의 이유입니다. __iomem 어노테이션은 Sparse 도구가 이를 검증하는 데 사용합니다.
vmalloc_to_page / vmalloc_to_pfn 변환
vmalloc 영역의 가상 주소에서 대응하는 물리 페이지(struct page)나 PFN(Page Frame Number)을 구하는 함수입니다. vmalloc 메모리를 사용자 공간(User Space)에 매핑하거나, 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으로 비활성화할 수 있습니다. 커널 모듈(Kernel Module) 로딩에 특히 효과적이며, eBPF JIT 코드에서도 활용됩니다. /proc/vmallocinfo에서 hugepages 키워드로 확인할 수 있습니다.
성능 특성과 오버헤드(Overhead)
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 성능 모니터링 명령 모음 ===
# 1. vmalloc 영역 전체 현황
cat /proc/meminfo | grep -i vmalloc
# VmallocTotal: 34359738367 kB ← 전체 vmalloc 가상 주소 공간
# VmallocUsed: 51240 kB ← 현재 사용 중인 크기
# VmallocChunk: 34359685124 kB ← 최대 연속 빈 공간
# 2. 개별 vmalloc 할당 목록
cat /proc/vmallocinfo | head -20
# 0xffffc90000000000-0xffffc90000005000 20480 bpf_prog_alloc+0x47/0x... pages=4 vmalloc
# 3. 상위 vmalloc 사용자 요약 (함수별 집계)
awk '{print $3}' /proc/vmallocinfo | sort | uniq -c | sort -rn | head -10
# 4. TLB miss 측정 (perf)
perf stat -e dTLB-load-misses,dTLB-store-misses,iTLB-load-misses -- sleep 5
# 5. vmalloc 할당 이벤트 ftrace 추적
echo 1 > /sys/kernel/debug/tracing/events/kmem/kmem_cache_alloc/enable
cat /sys/kernel/debug/tracing/trace_pipe | grep vmalloc
코드 설명
-
/proc/meminfo
VmallocUsed가VmallocTotal대비 높으면 주소 공간 고갈 위험이 있습니다. 32비트 시스템에서는 특히 주의가 필요합니다. - /proc/vmallocinfo 각 vmalloc 할당의 가상 주소 범위, 크기, 호출자 함수, 페이지 수를 보여줍니다. 메모리 누수 추적 시 어떤 함수가 해제하지 않는지 확인할 수 있습니다.
-
perf stat
vmalloc 메모리를 많이 사용하는 워크로드에서
dTLB-load-misses가 급증하면 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. 대형 버퍼 할당 (파일시스템(Filesystem), 네트워킹)
/* 파일시스템 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 구현 시 |
주요 API 사용 예시와 차이점
/* === 1. vmalloc vs vzalloc: 0 초기화 여부 === */
void *buf1 = vmalloc(65536); /* 초기화 안 됨, 이전 데이터 잔류 가능 */
void *buf2 = vzalloc(65536); /* 0으로 초기화됨 (__GFP_ZERO) */
/* === 2. NUMA 노드 지정 할당 === */
int nid = numa_node_id(); /* 현재 CPU의 NUMA 노드 */
void *buf3 = vmalloc_node(1024 * 1024, nid);
/* 지정 노드에서 물리 페이지를 우선 할당 → 접근 지연 최소화 */
/* === 3. __vmalloc: GFP 플래그 세밀 제어 === */
void *buf4 = __vmalloc(4096 * 100,
GFP_KERNEL | __GFP_NOWARN | __GFP_RETRY_MAYFAIL);
/* 할당 실패 시 경고 출력 억제, 적당히만 재시도 */
/* === 4. vmalloc_user: 사용자 공간 mmap 가능 === */
void *shared = vmalloc_user(PAGE_SIZE * 16);
/* 드라이버의 mmap 핸들러에서 remap_vmalloc_range()로 매핑 */
/* === 5. vmap: 이미 할당된 페이지 배열 매핑 === */
struct page *pages[4];
for (int i = 0; i < 4; i++)
pages[i] = alloc_page(GFP_KERNEL);
void *mapped = vmap(pages, 4, VM_MAP, PAGE_KERNEL);
/* 4개 페이지를 연속 가상 주소로 매핑 (페이지 할당 안 함) */
vunmap(mapped); /* 매핑만 해제 (페이지는 직접 해제) */
for (int i = 0; i < 4; i++)
__free_page(pages[i]);
/* === 6. vmalloc_to_page: 가상→물리 변환 === */
void *vaddr = vmalloc(8192);
struct page *pg = vmalloc_to_page(vaddr);
unsigned long pfn = vmalloc_to_pfn(vaddr);
pr_info("page=%p pfn=%lu\n", pg, pfn);
/* === 7. kvmalloc: 자동 선택 (kmalloc 우선 → vmalloc fallback) === */
void *auto_buf = kvmalloc(256 * 1024, GFP_KERNEL);
if (is_vmalloc_addr(auto_buf))
pr_info("vmalloc으로 할당됨\n");
else
pr_info("kmalloc으로 할당됨\n");
kvfree(auto_buf); /* 어느 쪽이든 자동 구분 해제 */
코드 설명
-
vmalloc vs vzalloc
vzalloc()은 내부적으로__GFP_ZERO플래그를 추가하여 할당 직후 0으로 채웁니다. 보안 민감 데이터(암호화 키 버퍼 등)는 반드시 초기화된 메모리를 사용해야 합니다. -
__vmalloc GFP 제어
__GFP_NOWARN은 할당 실패 시 커널 로그 경고를 억제하고,__GFP_RETRY_MAYFAIL은 무한 재시도 대신 적절히 포기합니다. 선택적 캐시 등 실패 허용 경로에 적합합니다. -
vmap / vunmap
vmap()은 이미 할당된 페이지를 가상 주소로 매핑만 하므로,vunmap()후 페이지를 별도로 해제해야 합니다. scatter-gather DMA 결과를 연속 접근할 때 유용합니다. -
kvmalloc / is_vmalloc_addr
kvmalloc()은 작은 크기는kmalloc으로 빠르게, 큰 크기는vmalloc으로 안정적으로 할당합니다.is_vmalloc_addr()로 실제 어느 경로가 선택되었는지 확인할 수 있습니다.
내부 알고리즘 상세
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() 호출이 실제 커널 소스에서 어떤 함수 체인을 거치는지 심층적으로 추적합니다. 각 단계에서 수행하는 역할과 주요 매개변수(Parameter)의 의미를 분석합니다.
/* mm/vmalloc.c — __vmalloc_node_range() 구현 분석 (Linux 6.x 기준, 간략화) */
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;
unsigned long real_align = align;
unsigned int shift = PAGE_SHIFT;
/* 1. 크기 검증 및 정렬 */
size = PAGE_ALIGN(size);
if (!size || (size >> PAGE_SHIFT) > totalram_pages())
return NULL;
/* 2. huge vmalloc 판단: CONFIG_HAVE_ARCH_HUGE_VMALLOC이면
* PMD 크기(2MB) 이상 요청 시 huge page 시도 */
if (vm_flags & VM_ALLOW_HUGE_VMAP)
if (arch_vmap_pmd_supported(prot) &&
size >= PMD_SIZE)
shift = PMD_SHIFT;
/* 3. guard page 추가: 오버플로우 탐지용 빈 페이지 */
size += PAGE_SIZE;
/* 4. __get_vm_area_node(): vmap_area + vm_struct 확보 */
area = __get_vm_area_node(real_size, align,
VM_ALLOC | VM_UNINITIALIZED | vm_flags,
start, end, node, gfp_mask, caller);
if (!area) {
warn_alloc(gfp_mask, NULL,
"vmalloc: allocation failure: %lu bytes", real_size);
return NULL;
}
/* 5. 물리 페이지 할당 + 페이지 테이블 매핑 */
addr = __vmalloc_area_node(area, gfp_mask, prot, shift, node);
if (!addr)
goto fail;
/* 6. KASAN shadow 메모리 설정 (디버깅) */
kasan_vmalloc(area, real_size, gfp_mask);
/* 7. VM_UNINITIALIZED 플래그 제거 → 초기화 완료 표시 */
clear_vm_uninitialized_flag(area);
return addr;
fail:
__vfree(area->addr);
return NULL;
}
코드 설명
-
16-18행
PAGE_ALIGN()으로 크기를 페이지 경계로 올림 정렬합니다.totalram_pages()를 초과하는 요청은 즉시 실패합니다. 0 크기 요청도 NULL을 반환합니다. -
22-26행
VM_ALLOW_HUGE_VMAP플래그가 설정되고 아키텍처가 지원하면, PMD_SIZE(2MB, x86_64) 이상 요청 시 huge page 매핑을 시도합니다.shift가PMD_SHIFT(21)로 변경됩니다. - 29행 할당 영역 끝에 guard page(4KB)를 추가합니다. 이 페이지는 매핑되지 않아, 버퍼 오버플로우 시 page fault가 발생하여 버그를 조기에 탐지합니다.
-
32-38행
__get_vm_area_node()는 free RB 트리에서 빈 가상 주소 공간을 찾고,vmap_area와vm_struct를 할당합니다.VM_UNINITIALIZED플래그를 설정하여 초기화 중임을 표시합니다. -
41-43행
__vmalloc_area_node()는vm_area_alloc_pages()로 물리 페이지를 할당하고,vmap_pages_range()로 페이지 테이블을 설정합니다. 실패 시__vfree()로 정리합니다. -
49행
VM_UNINITIALIZED플래그를 제거합니다.find_vm_area()등으로 검색할 때 초기화 중인 영역을 건너뛸 수 있도록 하는 메커니즘입니다.
struct vm_struct 필드별 해설
struct vm_struct는 vmalloc 할당의 메타데이터를 담는 핵심 구조체입니다. include/linux/vmalloc.h에 정의되어 있으며, 각 vmalloc 할당마다 하나씩 생성됩니다.
/* include/linux/vmalloc.h — struct vm_struct 전체 필드 (Linux 6.x) */
struct vm_struct {
struct vm_struct *next; /* 전역 vmlist 연결 (레거시) */
void *addr; /* 할당된 가상 주소 시작점 */
unsigned long size; /* 전체 크기 (guard page 포함) */
unsigned long flags; /* VM_ALLOC | VM_MAP | VM_IOREMAP ... */
struct page **pages; /* 물리 페이지 포인터 배열 */
unsigned int nr_pages; /* pages[] 배열의 유효 엔트리 수 */
phys_addr_t phys_addr; /* ioremap 시 물리 시작 주소 */
const void *caller; /* __builtin_return_address(0) */
};
| 필드 | 타입 | 설명 | 설정 시점 |
|---|---|---|---|
next | struct vm_struct * | 전역 vmlist 연결 리스트 포인터. 커널 초기 부팅 시 vmlist에 체인되며, 부팅 이후에는 RB 트리 기반 탐색이 주력입니다. /proc/vmallocinfo 출력 시 순회에 사용됩니다. | setup_vmalloc_vm() |
addr | void * | 할당된 가상 주소의 시작점. vmalloc() 반환값과 동일합니다. VMALLOC_START ~ VMALLOC_END 범위 내에 위치합니다. | setup_vmalloc_vm() |
size | unsigned long | 할당 전체 크기. 사용자 요청 크기를 PAGE_ALIGN()한 값 + guard page(PAGE_SIZE)입니다. 따라서 실제 사용 가능한 크기는 size - PAGE_SIZE입니다. | setup_vmalloc_vm() |
flags | unsigned long | 할당 유형을 나타내는 비트 플래그. VM_ALLOC(vmalloc), VM_MAP(vmap), VM_IOREMAP(ioremap), VM_USERMAP(사용자 매핑 가능), VM_FLUSH_RESET_PERMS(모듈 권한 리셋) 등. vfree() 시 VM_ALLOC 여부로 물리 페이지 해제 결정. | __get_vm_area_node() |
pages | struct page ** | 물리 페이지 포인터 배열. nr_pages개의 struct page *를 담습니다. vmalloc_to_page()는 이 배열을 사용하지 않고 페이지 테이블을 워크합니다. vfree() 시 이 배열을 순회하며 __free_pages()를 호출합니다. | vm_area_alloc_pages() |
nr_pages | unsigned int | 할당된 물리 페이지 수. (size - PAGE_SIZE) / PAGE_SIZE로 계산됩니다. huge vmalloc 시에도 개별 base page 수로 기록됩니다. | vm_area_alloc_pages() |
phys_addr | phys_addr_t | ioremap()에서만 사용. 매핑할 물리 주소의 시작점을 기록합니다. vmalloc()에서는 0으로 남습니다. /proc/vmallocinfo에서 phys=로 표시됩니다. | ioremap() |
caller | const void * | 할당을 요청한 함수의 주소. __builtin_return_address(0)으로 캡처합니다. /proc/vmallocinfo에서 심볼명으로 변환되어 표시되므로, 어떤 모듈이나 함수가 vmalloc을 사용하는지 추적할 수 있습니다. | vmalloc() 진입 |
/* 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_alloc_coherent() 매핑 */
#define VM_UNINITIALIZED 0x00000020 /* 초기화 진행 중 (find_vm_area 스킵) */
#define VM_NO_GUARD 0x00000040 /* guard page 없음 (percpu) */
#define VM_KASAN 0x00000080 /* KASAN shadow 할당 필요 */
#define VM_FLUSH_RESET_PERMS 0x00000100 /* 해제 전 권한 RW 복원 필요 (모듈) */
#define VM_ALLOW_HUGE_VMAP 0x00000200 /* PMD/PUD 크기 huge 매핑 허용 */
/* 사용 패턴:
* vmalloc() → flags = VM_ALLOC
* vmap() → flags = VM_MAP
* ioremap() → flags = VM_IOREMAP
* module_alloc() → flags = VM_ALLOC | VM_FLUSH_RESET_PERMS
* vmalloc_huge() → flags = VM_ALLOC | VM_ALLOW_HUGE_VMAP
*/
코드 설명
-
VM_ALLOC vs VM_MAP
VM_ALLOC은vmalloc()이 물리 페이지를 직접 할당한 경우입니다.vfree()시 이 플래그가 있어야pages[]배열의 물리 페이지를 Buddy에 반환합니다.VM_MAP은vmap()으로 이미 존재하는 페이지를 매핑만 한 경우로, 페이지 해제를 하지 않습니다. -
VM_UNINITIALIZED
할당 중간 단계에서 설정됩니다. 다른 CPU가
find_vm_area()를 호출했을 때 아직 초기화가 완료되지 않은 영역을 건너뛰도록 합니다. 모든 설정이 완료된 후clear_vm_uninitialized_flag()로 제거합니다. -
VM_FLUSH_RESET_PERMS
모듈 로더가 코드 메모리를 RX로 전환한 후 설정합니다.
vfree()시 먼저 권한을 RW로 복원하고 TLB flush를 수행한 뒤에야 페이지 테이블과 물리 페이지를 해제합니다. W^X 보안 정책의 핵심 메커니즘입니다.
struct vmap_area 필드별 해설
struct vmap_area는 vmalloc 가상 주소 공간의 영역(Region)을 관리하는 구조체입니다. mm/vmalloc.c에 정의되어 있으며, busy/free 두 가지 RB 트리에서 사용됩니다.
/* mm/vmalloc.c — struct vmap_area 전체 필드 (Linux 6.x) */
struct vmap_area {
unsigned long va_start; /* 가상 주소 시작 (포함) */
unsigned long va_end; /* 가상 주소 끝 (미포함) */
struct rb_node rb_node; /* RB 트리 노드 (busy 또는 free) */
struct list_head list; /* vmap_area_list 연결 */
/*
* union: busy 영역이면 vm, free 영역이면 subtree_max_size 사용
* 동시에 사용되지 않으므로 union으로 메모리 절약
*/
union {
unsigned long subtree_max_size; /* free: 서브트리 내 최대 빈 크기 */
struct vm_struct *vm; /* busy: 연결된 vm_struct */
};
unsigned long flags; /* VMAP_RAM, VMAP_BLOCK 등 내부 플래그 */
};
| 필드 | 타입 | 설명 |
|---|---|---|
va_start | unsigned long | 가상 주소 범위의 시작점 (포함). RB 트리의 정렬 기준(key)입니다. busy 트리에서는 할당된 영역의 시작, free 트리에서는 빈 영역의 시작을 나타냅니다. |
va_end | unsigned long | 가상 주소 범위의 끝점 (미포함). 영역 크기는 va_end - va_start입니다. busy 영역에서는 guard page를 포함한 크기이므로 vm->size와 동일합니다. |
rb_node | struct rb_node | Red-Black 트리 노드. busy 영역은 vmap_area_root, free 영역은 free_vmap_area_root에 삽입됩니다. va_start 기준으로 정렬합니다. |
list | struct list_head | 전역 vmap_area_list 연결. 주소순으로 정렬된 양방향 리스트로, /proc/vmallocinfo 출력 시 순회합니다. purge 시에도 사용됩니다. |
subtree_max_size | unsigned long | free 영역 전용. augmented RB tree의 핵심 필드로, 현재 노드를 루트로 하는 서브트리 내 가장 큰 빈 공간 크기입니다. 탐색 시 이 값이 요청 크기보다 작으면 서브트리 전체를 스킵하여 O(log n) 성능을 보장합니다. |
vm | struct vm_struct * | busy 영역 전용. subtree_max_size와 union으로, 이 가상 주소 영역에 연결된 할당 메타데이터(vm_struct)를 가리킵니다. find_vm_area()에서 주소로부터 vm_struct를 찾을 때 사용합니다. |
flags | unsigned long | 내부 관리 플래그. VMAP_RAM(vmap_block 사용), VMAP_BLOCK(작은 vmap 할당), VMAP_FLAGS_MASK 등. vm_struct의 flags와는 별개의 네임스페이스입니다. |
vmap_area는 가상 주소 공간 관리의 핵심이고, vm_struct는 할당 메타데이터입니다. 모든 vmalloc 영역은 반드시 vmap_area를 가지지만, free 영역이나 VMAP_RAM 유형은 vm_struct가 없을 수 있습니다. ioremap, vmap, vmalloc 모두 같은 vmap_area 관리 체계를 공유합니다.
vfree() 내부 경로와 lazy TLB 배치 처리
vfree()의 내부 경로를 커널 소스 수준에서 추적합니다. 특히 lazy TLB flush와 vunmap chunk batching 메커니즘이 어떻게 성능을 최적화하는지 분석합니다.
/* mm/vmalloc.c — vfree 내부 경로 핵심 코드 (간략화) */
static void __vfree(const void *addr)
{
struct vm_struct *vm;
/* 1. busy RB tree에서 vmap_area 검색 + 제거
* vm_struct를 반환하고, 가상 매핑은 lazy 해제 */
vm = remove_vm_area(addr);
if (unlikely(!vm)) {
WARN(1, "vfree: nonexistent vm area (%p)\n", addr);
return;
}
/* 2. VM_FLUSH_RESET_PERMS: 모듈 코드 메모리인 경우
* 먼저 권한을 RW로 복원 (W^X 해제) */
if (vm->flags & VM_FLUSH_RESET_PERMS)
vm_reset_perms(vm);
/* 3. 물리 페이지 해제 (VM_ALLOC인 경우만) */
if (vm->flags & VM_ALLOC)
__vfree_pages(vm->pages, vm->nr_pages);
/* 4. pages 배열 + vm_struct 메모리 해제 */
kvfree(vm->pages);
kfree(vm);
}
/* remove_vm_area 내부의 lazy purge 경로 */
static void free_vmap_area_noflush(struct vmap_area *va)
{
unsigned long nr_lazy;
/* purge 대기 목록에 추가 */
spin_lock(&vmap_area_lock);
llist_add(&va->purge_list, &vmap_purge_list);
spin_unlock(&vmap_area_lock);
/* lazy 페이지 카운터 증가 */
nr_lazy = atomic_long_add_return(
(va->va_end - va->va_start) >> PAGE_SHIFT,
&vmap_lazy_nr);
/* 임계값 초과 시 일괄 purge 트리거 */
if (unlikely(nr_lazy > VMAP_PURGE_THRESHOLD))
purge_vmap_area_lazy();
}
/* 일괄 TLB flush + 가상 주소 공간 회수 */
static void purge_vmap_area_lazy(void)
{
unsigned long start = ULONG_MAX, end = 0;
struct vmap_area *va;
struct llist_node *llnode;
/* purge_list에서 전체 주소 범위 계산 */
llnode = llist_del_all(&vmap_purge_list);
llist_for_each_entry(va, llnode, purge_list) {
start = min(start, va->va_start);
end = max(end, va->va_end);
}
/* 모든 CPU의 TLB를 한 번에 flush (IPI) */
flush_tlb_kernel_range(start, end);
/* purge된 영역을 free RB tree에 병합 */
llist_for_each_entry_safe(va, n, llnode, purge_list) {
merge_or_add_vmap_area_augmented(va,
&free_vmap_area_root, &free_vmap_area_list);
}
/* lazy 카운터 리셋 */
atomic_long_set(&vmap_lazy_nr, 0);
}
코드 설명
-
8-9행
remove_vm_area()는 busy RB 트리에서 해당 주소의vmap_area를 찾아 제거합니다. 내부에서unmap_kernel_range_noflush()로 PTE를 클리어하지만, TLB flush는 하지 않습니다(lazy). -
16-18행
VM_FLUSH_RESET_PERMS플래그가 있으면vm_reset_perms()를 호출합니다. 모듈 코드가 RX 상태이므로, 먼저 RW로 복원 + TLB flush를 해야 안전하게 페이지를 해제할 수 있습니다. -
33-36행
free_vmap_area_noflush()는 해제된vmap_area를 즉시 free 트리에 넣지 않고, purge 대기 리스트에 추가합니다. TLB 무효화 전에 free 트리에 넣으면, 새 할당이 stale TLB 엔트리와 충돌할 수 있기 때문입니다. -
38-43행
vmap_lazy_nratomic 카운터를 증가시켜 누적 해제 크기를 추적합니다.VMAP_PURGE_THRESHOLD(1MB)를 초과하면purge_vmap_area_lazy()를 트리거합니다. -
51-57행
purge_list의 모든 대기 영역에서 최소
start와 최대end를 계산합니다. 이 범위 전체에 대해 한 번만flush_tlb_kernel_range()를 호출하여 IPI 횟수를 최소화합니다. -
62-65행
TLB flush 완료 후, purge된 영역을 free RB 트리에 삽입합니다.
merge_or_add_vmap_area_augmented()는 인접한 free 영역과 합체하여 단편화를 줄입니다. 동시에 augmented 값(subtree_max_size)도 갱신합니다.
vmalloc 전체 아키텍처 통합
vmalloc 서브시스템의 전체 구조를 하나의 통합 다이어그램으로 정리합니다.
/* vmalloc 전체 아키텍처의 핵심 호출 체인 요약 */
/* 할당 경로: vmalloc() → __vmalloc_node_range() */
void *vmalloc(unsigned long size)
{
return __vmalloc_node_range(size, 1, /* align=1 */
VMALLOC_START, VMALLOC_END,
GFP_KERNEL, PAGE_KERNEL,
0, NUMA_NO_NODE,
__builtin_return_address(0));
}
/* __vmalloc_node_range() 내부 단계:
* 1. alloc_vmap_area() → 가상 주소 예약 (free RB tree 탐색)
* 2. vm_area_alloc_pages() → 물리 페이지 할당 (Buddy)
* 3. vmap_pages_range() → 페이지 테이블 매핑 (PGD→PTE)
* 실패 시 역순 정리: unmap → free pages → free vmap_area
*/
/* 해제 경로: vfree() → __vfree() */
void vfree(const void *addr)
{
/* 인터럽트 컨텍스트면 work queue로 지연 */
if (unlikely(in_interrupt()))
schedule_work(&...); /* __vfree_deferred */
else
__vfree(addr);
/* __vfree: remove_vm_area() → free_vmap_area_noflush()
* → lazy TLB purge → __free_pages() */
}
코드 설명
-
vmalloc()
vmalloc()은__vmalloc_node_range()의 래퍼(Wrapper)로, VMALLOC_START~VMALLOC_END 전체 범위에서 할당합니다.vzalloc()은 여기에__GFP_ZERO를 추가합니다. - 3단계 할당 가상 주소 예약 → 물리 페이지 할당 → 페이지 테이블 매핑의 3단계가 순서대로 실행됩니다. 중간에 실패하면 이전 단계를 역순으로 정리(rollback)합니다.
-
vfree() 지연 처리
인터럽트 컨텍스트에서는 sleep이 불가능하므로
schedule_work()를 통해 프로세스 컨텍스트로 지연 실행합니다. 이 덕분에vfree()는 어디서든 안전하게 호출할 수 있습니다.
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
모듈 로더(Loader), 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 영역 메타데이터 업데이트와
페이지 테이블 갱신 구간에서 락 경합이 발생할 수 있습니다. 특히 작은 크기 할당을 고빈도로 반복하면,
실제 메모리 사용량보다 관리 오버헤드가 먼저 병목(Bottleneck)이 되는 경우가 많습니다.
# === vmap 락 경합 진단 ===
# 1. lock contention 실시간 확인 (perf lock)
perf lock record -a -- sleep 10
perf lock report | grep -i vmap
# vmap_area_lock이 상위에 등장하면 경합 확인
# 2. vmalloc 할당 빈도 확인 (ftrace 이벤트)
echo 1 > /sys/kernel/debug/tracing/events/kmem/mm_page_alloc/enable
cat /sys/kernel/debug/tracing/trace_pipe | grep vmalloc
# 3. 완화: 소형 빈번 할당은 kmalloc/mempool로 전환
# 변경 전 (경합 유발 패턴)
# for (...) buf = vmalloc(512); ... vfree(buf);
# 변경 후 (권장 패턴)
# buf = kmalloc(512, GFP_KERNEL); /* 512B는 slab이 효율적 */
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 사용자와 주소 공간 압력이 간접 충돌할 수 있습니다.
# === vmalloc/ioremap 주소 공간 사용 현황 분리 확인 ===
# 1. vmalloc 영역 전체 현황
grep Vmalloc /proc/meminfo
# VmallocTotal, VmallocUsed, VmallocChunk
# 2. ioremap 매핑 vs vmalloc 매핑 분리
grep -c ioremap /proc/vmallocinfo # ioremap 매핑 수
grep -c vmalloc /proc/vmallocinfo # vmalloc 매핑 수
grep -c vmap /proc/vmallocinfo # vmap 매핑 수
# 3. 대형 ioremap 매핑 상위 목록 (드라이버 분석)
grep ioremap /proc/vmallocinfo | awk '{print $2-$1, $0}' | sort -rn | head -5
# 4. vmalloc 주소 공간 압력 확인
# VmallocChunk가 작아지면 새 할당 실패 위험 증가
awk '/VmallocChunk/{print "남은 연속 공간:", $2, $3}' /proc/meminfo
/proc/meminfo의 일반 메모리 지표와 Vmalloc* 지표를 분리해서 해석해야 정확합니다.
vmap_area 분할과 병합 알고리즘
vmalloc의 가상 주소 관리에서 핵심 연산은 분할(split)과 병합(merge)입니다. 할당 시 적합한 free 영역을 찾으면 필요한 크기만큼 잘라내고(split), 해제 시 인접한 free 영역을 합칩니다(merge). 이 과정은 RB 트리와 연결 리스트를 동시에 갱신합니다.
/* mm/vmalloc.c — 병합/추가 알고리즘 (간략화) */
static void
merge_or_add_vmap_area(struct vmap_area *va,
struct rb_root *root, struct list_head *head)
{
struct vmap_area *sibling;
struct list_head *pos;
bool merged = false;
/* RB 트리에서 삽입 위치 탐색 + 정렬 리스트에서 이웃 찾기 */
pos = find_va_links(va, root, NULL, &link);
/* 이전 영역과 병합 시도 */
if (pos->prev != head) {
sibling = list_entry(pos->prev, struct vmap_area, list);
if (sibling->va_end == va->va_start) {
/* 이전 영역 확장 */
sibling->va_end = va->va_end;
augment_tree_propagate_from(sibling);
kmem_cache_free(vmap_area_cachep, va);
va = sibling;
merged = true;
}
}
/* 다음 영역과 병합 시도 */
if (pos->next != head) {
sibling = list_entry(pos->next, struct vmap_area, list);
if (va->va_end == sibling->va_start) {
/* 현재 영역 확장 + 다음 영역 제거 */
va->va_end = sibling->va_end;
rb_erase_augmented(&sibling->rb_node, root, &cb);
list_del(&sibling->list);
kmem_cache_free(vmap_area_cachep, sibling);
merged = true;
}
}
if (!merged)
link_va(va, root, parent, link, head);
/* subtree_max_size 업데이트 (조상 노드까지) */
augment_tree_propagate_from(va);
}
/proc/meminfo의 VmallocChunk 값이 병합 효과를 반영합니다.
커널 스택과 vmalloc
Linux 4.9부터 x86_64에서 CONFIG_VMAP_STACK=y가 기본 활성화되어, 커널 스택(Kernel Stack)이 vmalloc 영역에 할당됩니다. 이는 스택 오버플로우(Stack Overflow) 감지를 위한 중요한 보안 기능입니다.
/* kernel/fork.c — VMAP_STACK 커널 스택 할당 */
static int alloc_thread_stack_node(struct task_struct *tsk, int node)
{
#ifdef CONFIG_VMAP_STACK
void *stack;
/* 1. per-CPU 캐시에서 먼저 시도 (빠른 경로) */
stack = this_cpu_xchg(cached_stacks[tsk->stack_canary_idx], NULL);
if (stack)
goto out;
/* 2. vmalloc으로 새 스택 할당 (느린 경로)
* THREAD_SIZE + guard pages → vmalloc 영역에 배치
* PAGE_KERNEL: RW, NX (실행 방지) */
stack = __vmalloc_node_range(THREAD_SIZE, THREAD_ALIGN,
VMALLOC_START, VMALLOC_END,
THREADINFO_GFP, PAGE_KERNEL,
0, node, __builtin_return_address(0));
if (!stack)
return -ENOMEM;
out:
tsk->stack = stack;
return 0;
#else
/* 기존 방식: Buddy에서 물리 연속 할당 */
tsk->stack = alloc_pages_node(node, THREADINFO_GFP,
THREAD_SIZE_ORDER);
return tsk->stack ? 0 : -ENOMEM;
#endif
}
vmalloc과 메모리 cgroup 통합
Linux 5.14부터 vmalloc 할당에 __GFP_ACCOUNT 플래그를 사용하면, 해당 할당이 호출 프로세스의 메모리 cgroup(memcg)에 과금됩니다. 이를 통해 컨테이너 환경에서 vmalloc 메모리 사용량을 제한하고 모니터링할 수 있습니다.
/* vmalloc + memcg 과금 사용 예시 */
/* 1. cgroup에 과금되는 vmalloc 할당 */
void *buf = __vmalloc(1024 * 1024,
GFP_KERNEL | __GFP_ACCOUNT); /* → current memcg에 1MB 과금 */
/* 2. kvmalloc도 과금 가능 */
void *buf2 = kvmalloc(256 * 1024,
GFP_KERNEL | __GFP_ACCOUNT); /* kmalloc 또는 vmalloc에 과금 */
/* 3. eBPF 맵 할당 (자동 과금) */
/* kernel/bpf/syscall.c:
* bpf_map_area_alloc() 내부에서 __GFP_ACCOUNT 사용
* → BPF 맵이 생성한 cgroup에 과금됨
* → cgroup 메모리 제한에 의해 맵 생성이 제한될 수 있음 */
/* 4. memcg 사용량 확인 (cgroup v2) */
/* cat /sys/fs/cgroup/<group>/memory.current
* cat /sys/fs/cgroup/<group>/memory.stat
* → slab + vmalloc 과금 합산 표시
* → kernel_stack: VMAP_STACK 사용 시 여기 반영 */
/* 5. 과금 한도 초과 시 동작 */
/* __GFP_ACCOUNT + 메모리 한도 초과 →
* 1) 직접 회수(direct reclaim) 시도
* 2) OOM killer가 cgroup 내 프로세스 종료
* 3) 또는 할당 실패 반환 (GFP_NORETRY 등) */
| vmalloc 호출 유형 | memcg 과금 | 사용처 |
|---|---|---|
vmalloc(GFP_KERNEL) | 과금 안 됨 | 커널 내부 버퍼, 해시 테이블 |
__vmalloc(GFP_KERNEL | __GFP_ACCOUNT) | 과금됨 | 사용자 요청 버퍼 |
kvmalloc(__GFP_ACCOUNT) | 과금됨 | eBPF 맵, cgroup 버퍼 |
module_alloc() | 과금 안 됨 | 커널 모듈 코드 |
alloc_thread_stack_node() | 과금됨 (CONFIG_MEMCG) | VMAP_STACK 커널 스택 |
ioremap() | 과금 안 됨 | 디바이스 MMIO (물리 페이지 할당 없음) |
memory.stat의 kernel_stack과 slab 항목이 급증하면, 사용자 공간 메모리가 아닌 커널 메모리 과금이 원인일 수 있습니다.
아키텍처별 vmalloc 구현 차이
vmalloc의 핵심 알고리즘(mm/vmalloc.c)은 아키텍처 독립적이지만, 가상 주소 레이아웃, 페이지 테이블 구조, TLB 관리 등은 아키텍처마다 다릅니다.
| 특성 | x86_64 | ARM64 | RISC-V (Sv48) |
|---|---|---|---|
| vmalloc 영역 크기 | 32TB (4-level) / 12,800TB (5-level) | ~256TB (48-bit VA) | ~128TB (Sv48) |
| 모듈 영역 위치 | 커널 텍스트 위 (1520MB) | 커널 텍스트 아래 (128MB) | 커널 텍스트 근처 |
| vmalloc_fault | 불필요 (PGD 공유) | 불필요 (PGD 공유) | 불필요 (PGD 공유) |
| Huge vmalloc | 2MB (PMD) 지원 | 2MB (PMD) + 1GB (PUD) 지원 | 2MB (PMD) 지원 |
| KASLR vmalloc base | vmalloc_base 무작위화 | module_alloc_base 무작위화 | 지원 (구현 의존) |
| TLB flush | INVLPG / INVPCID (IPI) | TLBI (broadcast by HW) | SFENCE.VMA |
| VMAP_STACK 기본 | 활성 (16KB) | 활성 (16KB / 64KB) | 활성 (16KB) |
| Direct map 크기 | 64TB | 최대 128TB | ~64TB |
| Paging 수준 | 4-level / 5-level | 3-level / 4-level | Sv39(3) / Sv48(4) / Sv57(5) |
/* 아키텍처별 TLB flush 구현 비교 */
/* x86_64: IPI 기반 — 각 CPU에 인터럽트 전송 */
flush_tlb_kernel_range(start, end);
/* 내부: send_IPI_allbutself(TLB_FLUSH_VECTOR)
* 각 CPU가 인터럽트 핸들러에서 INVLPG 실행
* 대규모 범위: INVPCID 또는 CR3 reload */
/* ARM64: 하드웨어 broadcast — IPI 불필요! */
flush_tlb_kernel_range(start, end);
/* 내부: TLBI VAE1IS (Inner Shareable 도메인)
* 하드웨어가 모든 CPU에 자동 전파
* IPI 오버헤드 없음 → vfree 성능에 유리 */
/* RISC-V: SFENCE.VMA 명령 */
flush_tlb_kernel_range(start, end);
/* 내부: SFENCE.VMA (주소 지정 또는 전역)
* SBI(Supervisor Binary Interface) 통해 다른 hart에 전파
* 또는 하드웨어 broadcast (구현 의존) */
참고자료
커널 소스 코드
- mm/vmalloc.c — vmalloc 핵심 구현
- include/linux/vmalloc.h — vmalloc 헤더 (API, 자료구조)
- mm/util.c — kvmalloc 구현
- arch/x86/mm/ioremap.c — ioremap 구현
커널 문서
관련 커밋 및 패치(Patch)
- mm/vmalloc: huge vmalloc mappings (v5.15) — huge vmalloc 도입 패치
- mm/vmalloc: use augmented rbtree (v5.2) — augmented RB tree 도입
LWN 기사
- LWN: Huge vmalloc mappings
- LWN: Virtually mapped kernel stacks — 가상 매핑 커널 스택 도입을 설명합니다 (2017)
- LWN: vmalloc improvements — vmalloc 성능 개선 작업을 다룹니다 (2019)
Man 페이지
- mmap(2) — 가상 메모리 매핑 시스템 콜입니다
참고 서적
- 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