vmalloc — 가상 연속 메모리 할당

Linux 커널 vmalloc 서브시스템: 가상 주소 공간에서 연속적이지만 물리적으로는 불연속인 메모리를 할당하는 메커니즘, vmap_area 관리, 페이지 테이블 매핑, lazy TLB flush, ioremap, huge vmalloc, 성능 특성, 디버깅 종합 가이드.

관련 페이지: 기본 메모리 관리는 메모리 관리 (기초), 고급 메모리 관리는 메모리 관리 (심화), 슬랩 할당자는 Slab 할당자 페이지를 참고하세요.

개요

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 → BuddyBuddy (페이지 단위) + 페이지 테이블
해제 함수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은 페이지 테이블 조작과 가상 주소 공간 관리가 핵심이므로, 주소 변환 메커니즘을 이해해야 합니다.
일상 비유: vmalloc은 여러 곳에 흩어진 빈 방을 하나의 긴 복도로 연결하는 것과 같습니다. 물리적으로는 떨어져 있지만, 복도(가상 주소)를 따라 걸으면 마치 연속된 방처럼 사용할 수 있습니다.

핵심 요약

  • vmalloc() — 가상 주소 연속, 물리 주소 불연속 메모리를 할당합니다. 큰 버퍼 할당에 적합합니다.
  • vmap_area — vmalloc 영역 내 할당된 가상 주소 범위를 관리하는 자료구조입니다.
  • vm_struct — vmalloc 할당 메타데이터(크기, 플래그, 페이지 배열 등)를 담는 구조체입니다.
  • 페이지 테이블 매핑 — 개별 물리 페이지를 가상 주소에 매핑하기 위해 PGD→PUD→PMD→PTE 체인을 설정합니다.
  • lazy TLB flush — vfree 시 TLB 무효화를 지연시켜 성능을 최적화합니다.
  • kvmalloc() — kmalloc 시도 후 실패하면 vmalloc으로 자동 fallback하는 편의 함수입니다.

단계별 이해

  1. 가상 주소 공간 예약 — VMALLOC_START~VMALLOC_END 범위에서 빈 가상 주소 구간을 찾아 vmap_area를 할당합니다.

    Red-black 트리와 free list를 사용하여 빠르게 빈 공간을 탐색합니다.

  2. 물리 페이지 할당 — Buddy 할당자에서 개별 페이지(order-0)를 필요한 수만큼 할당합니다.

    각 페이지는 물리적으로 어디에 있든 상관없이 독립적으로 할당됩니다.

  3. 페이지 테이블 설정 — 할당된 가상 주소 범위에 대해 커널 페이지 테이블(PGD→PUD→PMD→PTE)을 생성하고, 각 PTE를 해당 물리 페이지에 매핑합니다.

    이 과정이 vmalloc이 kmalloc보다 느린 주된 이유입니다.

  4. 가상 주소 반환 — 매핑이 완료된 가상 주소를 호출자에게 반환합니다. 이 주소로 연속 메모리처럼 접근할 수 있습니다.

    해제 시 vfree()를 호출하면 페이지 테이블 해제, TLB flush, 물리 페이지 반환이 수행됩니다.

가상 주소 공간 레이아웃

x86_64 아키텍처에서 커널 가상 주소 공간은 여러 영역으로 나뉘며, vmalloc 영역은 그 중 하나입니다. VMALLOC_START에서 VMALLOC_END까지의 범위가 vmalloc 전용 영역으로, 여기에 vmalloc, vmap, ioremap 할당이 모두 배치됩니다.

x86_64 커널 가상 주소 공간 레이아웃 (4-level paging) Direct Map (page_offset_base) ffff888000000000 ~ ffffc87fffffffff (64 TB) — 물리 메모리 1:1 매핑 Guard Hole (보호 구간) VMALLOC 영역 (vmalloc_base) ffffc90000000000 ~ ffffe8ffffffffff (32 TB) — vmalloc / vmap / ioremap VMALLOC_START VMALLOC_END vmemmap (struct page 배열) ffffea0000000000 ~ ffffeaffffffffff (1 TB) — 페이지 메타데이터 KASAN Shadow (CONFIG_KASAN 활성 시) Kernel Text / Data / BSS ffffffff80000000 ~ ffffffff9fffffff (512 MB) — _text ~ _end 모듈 영역 (MODULES_VADDR) ffffffffa0000000 ~ fffffffffeffffff (1520 MB) — 커널 모듈 코드 Fixmap (ffffffffff000000 ~) — 고정 매핑 VMALLOC 영역 내부 구조 (확대) vmalloc #1 G ioremap #1 G vmap #1 G ... (빈 공간) ... G = Guard Page (PAGE_SIZE) — 오버플로우 탐지용 각 할당 사이에 guard page가 삽입되어 버퍼 오버런을 감지합니다
/* 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) */
Guard Page: 각 vmalloc 할당 사이에 최소 1개의 guard page(PAGE_SIZE, 4KB)가 삽입됩니다. 이 페이지는 매핑되지 않으므로, 할당 영역을 넘어 접근하면 즉시 page fault가 발생하여 버퍼 오버런을 감지할 수 있습니다. 이로 인해 실제 사용 가능한 vmalloc 공간은 요청 크기보다 약간 더 소비됩니다.

vmalloc 내부 아키텍처

vmalloc 서브시스템은 두 가지 핵심 자료구조로 가상 주소 영역을 관리합니다: vmap_area는 가상 주소 범위 관리에, vm_struct는 할당 메타데이터 관리에 사용됩니다.

vmalloc 핵심 자료구조 관계 vmap_area_root Red-Black Tree (busy 영역, va_start 정렬) spinlock: vmap_area_lock free_vmap_area_root Red-Black Tree (빈 영역) va_start 기준 정렬 struct vmap_area va_start : unsigned long va_end : unsigned long rb_node : RB 트리 노드 list : linked list subtree_max : 서브트리 최대 빈 공간 vm : struct vm_struct * flags : busy/free/purge 등 struct vm_struct next : 연결 리스트 addr : void * (가상 주소) size : unsigned long flags : VM_ALLOC 등 pages : struct page ** nr_pages : unsigned int phys_addr : phys_addr_t caller : void * (할당 호출자) flags 예: VM_IOREMAP 노드 vm pages[] 배열 p[0] p[1] p[2] ... struct page * (물리 페이지 포인터) pages 물리 메모리 (불연속) PFN 142 PFN 8301 PFN 55 PFN 3027 ↑ 물리적으로 불연속이지만 가상 주소에서는 연속으로 매핑 busy RB tree free RB tree vm_struct pages 배열 vmap_area
/* 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단계입니다.

vmalloc() 할당 흐름 vmalloc(size) __vmalloc_node_range(size, align, start, end, gfp, prot) __vmalloc_area_node(vm, gfp, prot, node) alloc_vmap_area(size, align, ...) 1. free_vmap_area_root에서 빈 공간 탐색 2. subtree_max_size로 빠른 스킵 3. vmap_area 분할/삽입 4. busy 트리에 추가 alloc_pages_node() x N 1. nr_pages = size / PAGE_SIZE 2. Buddy에서 order-0 페이지 할당 3. vm->pages[] 배열에 저장 4. __GFP_ZERO 시 zero fill vmap_pages_range(addr, end, prot, pages) 페이지 테이블 매핑 vmap_pages_pgd_range → vmap_pages_pud_range → vmap_pages_pmd_range → vmap_pages_pte_range 각 PTE에 물리 페이지 프레임 번호 기록 return vm->addr (가상 주소) 실패 시: __vfree() 할당된 페이지/영역 정리 T1: API 진입 T2: 주소+페이지 T3: PT 매핑 T4: 완료
/* 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_areavm_struct를 할당합니다.
  • 37-39행 __vmalloc_area_node()에서 물리 페이지를 할당하고 페이지 테이블을 설정합니다.

페이지 테이블 매핑

vmalloc의 핵심은 불연속 물리 페이지를 연속 가상 주소에 매핑하는 페이지 테이블 설정입니다. vmap_pages_range()가 4단계 페이지 테이블(PGD→PUD→PMD→PTE)을 순회하며 각 PTE에 물리 페이지를 기록합니다.

vmalloc 페이지 테이블 매핑 (4-level, x86_64) 가상 주소 분해: 0xffffc90000XXX000 PGD [47:39] PUD [38:30] PMD [29:21] PTE [20:12] Offset [11:0] PGD init_top_pgt pgd_offset_k(addr) PUD pud_alloc() pud_offset(pgd) PMD pmd_alloc() pmd_offset(pud) PTE pte_alloc_kernel() set_pte_at(page0) set_pte_at(page1) set_pte_at(page2) PFN 0x8E PFN 0x206D PFN 0x37 vmalloc 동기화 핵심 vmalloc 영역의 PGD 엔트리는 init_mm.pgd (커널 마스터 페이지 테이블)에 설정됩니다. 다른 CPU에서 접근 시 vmalloc_fault()가 init_mm에서 해당 PGD를 복사하여 동기화합니다 (x86_32). 매핑 함수 호출 체인 vmap_pages_range(addr, end, prot, pages, page_shift) └─ vmap_pages_range_noflush(addr, end, prot, pages, page_shift) ├─ vmap_pages_pgd_range(pgd, addr, end, prot, pages, &nr) ├─ vmap_pages_pud_range(pud, addr, end, ...) ├─ vmap_pages_pmd_range(pmd, addr, end, ...) ├─ vmap_pages_pte_range(pte, addr, end, ...) └─ set_pte_at(&init_mm, addr, pte, mk_pte(page, prot)) └─ flush_cache_vmap(addr, end) /* 캐시 flush */
/* 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;
}
페이지 테이블 동기화: x86_64에서는 PGD가 per-process가 아닌 공유 영역(커널 영역)이므로 vmalloc 매핑 후 별도의 동기화가 불필요합니다. 그러나 x86_32에서는 각 프로세스의 PGD가 분리되어 있어, 다른 CPU에서 vmalloc 영역에 처음 접근할 때 vmalloc_fault()가 호출되어 init_mm에서 PGD 엔트리를 복사합니다.

vfree 해제 흐름

vfree()는 vmalloc으로 할당된 메모리를 해제합니다. 성능 최적화를 위해 lazy TLB flush 메커니즘을 사용하여 즉시 모든 CPU의 TLB를 무효화하지 않고, 일정량이 쌓이면 일괄 처리합니다.

vfree() 해제 흐름 (Lazy TLB Flush) vfree(addr) 인터럽트 컨텍스트? Yes schedule_work(&vfree_deferred) 워크큐로 지연 처리 No __vfree(addr) remove_vm_area(addr) 페이지 테이블 해제 (Lazy) 1. vmap_area를 purge_list에 추가 2. VMAP_PURGE_THRESHOLD 초과 시: purge_vmap_area_lazy() → flush_tlb_kernel_range() (IPI) → 페이지 테이블 엔트리 해제 물리 페이지 해제 1. vm->pages[] 배열 순회 2. __free_pages(page, 0) 호출 3. Buddy 시스템에 페이지 반환 4. pages[] 배열 메모리 해제 5. vm_struct/vmap_area 해제 Lazy TLB Flush 이점 즉시 TLB flush는 IPI(Inter-Processor Interrupt)로 모든 CPU를 중단시키므로 비용이 큽니다. 여러 vfree를 모아서 한 번에 flush하면 IPI 횟수가 줄어들어 성능이 크게 개선됩니다 (특히 모듈 언로드, eBPF 프로그램 해제 시).
/* 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 주의사항: 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]);
vmap vs vmalloc 차이:
  • 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_areavm_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 주의: 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 효율을 크게 개선합니다.

일반 vmalloc vs Huge vmalloc 매핑 비교 일반 vmalloc (4KB pages) PMD PTE[0] PTE[1] PTE[2] ... PTE[511] 4KB x 512 = 2MB TLB 엔트리: 512개 필요 PTE 테이블: 4KB 소비 TLB miss 확률: 높음 2MB 할당 시 512번 PTE 설정 메모리 오버헤드: +4KB (PTE 테이블) page_shift = 12 Huge vmalloc (2MB pages) PMD 직접! 2MB Huge Page 물리적 연속 (compound page) PMD 엔트리가 직접 가리킴 PSE 비트 설정됨 TLB 엔트리: 1개만 필요! PTE 테이블: 불필요 (0KB) TLB miss 확률: 매우 낮음 2MB 할당 시 PMD 1개만 설정 메모리 오버헤드: 0KB page_shift = 21
/* 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로 매핑
 */
Huge vmalloc 활성화: CONFIG_HAVE_ARCH_HUGE_VMALLOC=y로 빌드하면 자동으로 활성화됩니다. 부팅 옵션 nohugevmalloc으로 비활성화할 수 있습니다. 커널 모듈 로딩에 특히 효과적이며, eBPF JIT 코드에서도 활용됩니다. /proc/vmallocinfo에서 hugepages 키워드로 확인할 수 있습니다.

성능 특성과 오버헤드

vmalloc은 유연한 대형 메모리 할당을 제공하지만, kmalloc에 비해 몇 가지 성능 오버헤드가 있습니다. 이를 이해하면 적절한 할당자를 선택할 수 있습니다.

vmalloc vs kmalloc 성능 오버헤드 비교 kmalloc (물리 연속) vmalloc (가상 연속) 할당 시간 ~100ns (slab hit) ~1-10us (PT setup + page alloc) TLB 효율 높음 낮음 (페이지별 TLB miss 가능) 캐시 지역성 우수 (연속) 불리 (불연속 물리 주소) 해제 비용 ~50ns (slab) ~500ns-5us (lazy TLB flush) 최대 할당 크기 ~4MB (order-10, 아키텍처 의존) ~32TB (VMALLOC 영역 전체) 선택 기준 - 크기 < PAGE_SIZE: kmalloc (슬랩 효율) - PAGE_SIZE < 크기 < 64KB: 상황에 따라 선택 (kvmalloc 권장) - 크기 > 64KB: vmalloc 우선 - DMA 필요: kmalloc/dma_alloc - 인터럽트 컨텍스트: kmalloc(GFP_ATOMIC)

TLB Miss 오버헤드 분석

vmalloc 메모리의 가장 큰 성능 비용은 TLB miss입니다. 물리적으로 불연속인 각 페이지마다 별도의 TLB 엔트리가 필요합니다.

시나리오TLB 엔트리 수TLB miss 비용개선 방법
1MB vmalloc (4KB pages)256개높음huge vmalloc 사용
1MB vmalloc (2MB huge)1개매우 낮음이미 최적
1MB kmalloc1개 (연속)최소-
10MB vmalloc (4KB)2560개매우 높음huge vmalloc 필수
10MB vmalloc (2MB huge)5개낮음이미 최적
성능 측정: vmalloc의 TLB miss 영향을 perf로 측정할 수 있습니다: perf stat -e dTLB-load-misses,dTLB-store-misses -- ./my_kernel_test 또한 /proc/vmstatnr_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
 * ==================================================================
 */
/proc/meminfo 관련 항목:
  • 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를 저장하여, 요청 크기를 수용할 수 없는 서브트리를 빠르게 스킵합니다.

Free vmap_area Augmented RB Tree 64KB free max: 256KB 16KB free max: 32KB 128KB free max: 256KB 8KB max: 8KB 32KB max: 32KB 4KB max: 4KB 256KB max: 256KB 200KB 요청 → max=256KB >= 200KB 찾음! 탐색 알고리즘 (O(log n)) 1. 루트에서 시작. subtree_max_size < 요청 크기이면 해당 서브트리 전체 스킵 2. 현재 노드의 빈 공간 >= 요청 크기이면 후보로 기록 (best-fit) 3. 왼쪽 자식의 max >= 요청이면 왼쪽 우선 탐색 (작은 주소 선호 → 주소 공간 연속성)
/* 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 서브시스템 전체 아키텍처 API 계층 vmalloc() vzalloc() vfree() vmap() vunmap() ioremap() kvmalloc() module_alloc() 코어 계층 __vmalloc_node_range() remove_vm_area() / __vfree() 가상 주소 공간 관리 alloc_vmap_area() free_vmap_area_noflush() vmap_area_root (busy RB) free_vmap_area_root (free RB) 페이지 테이블 관리 vmap_pages_range() unmap_kernel_range() PGD → PUD → PMD → PTE flush_tlb_kernel_range() Buddy 할당자 alloc_pages_node(node, gfp, 0) 개별 order-0 페이지 할당 (huge vmalloc: order-9) __free_pages(page, 0) — 해제 Lazy TLB Purge purge_vmap_area_lazy() VMAP_PURGE_THRESHOLD(1MB) 초과 시 일괄 TLB flush IPI → flush_tlb_kernel_range() → free areas 물리 메모리 (RAM) page page page page page page 물리적으로 불연속인 개별 페이지들이 가상 주소에서 연속으로 매핑됨

vmalloc 주소 단편화와 주소 고갈

vmalloc은 물리 단편화에 강하지만, 반대로 가상 주소 단편화 문제를 겪을 수 있습니다. 작은 할당/해제가 반복되면 빈 영역이 조각나고, VmallocUsed가 낮아도 큰 연속 구간(VmallocChunk)이 부족해 대형 할당이 실패할 수 있습니다.

vmalloc 주소 공간 단편화 시나리오 초기 상태 큰 연속 빈 공간 (VmallocChunk 큼) 작은 할당/해제 반복 후 총 빈 공간은 충분하지만 최대 연속 구간은 작아짐 결과: 큰 vmalloc 요청 실패 (-ENOMEM)
# 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 정책입니다.

코드용 vmalloc 메모리 권한 전환 (W^X) module_alloc/vmalloc RW 매핑 코드 복사/JIT 생성 text patching set_memory_rox() RW → RX flush_tlb_kernel_range I-cache/TLB 동기화 보안 목표: Writable + Executable 동시 허용 시간 최소화 핫패치/트레이싱 시에는 임시로 RW 전환 후 즉시 RX 복귀
/* 모듈/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_node와 실제 페이지 NUMA 분포 CPU/태스크 위치: Node 0 vmalloc_node(size, node=0) 호출 Node0 page 70% Node1 page 30% 원격 노드 접근 비율이 증가하면 메모리 지연/대역폭 손실 발생 진단/개선 루프 /proc/vmallocinfo에서 N0/N1 분포 확인 cpuset/numactl로 실행 CPU 고정 hot-path는 kmalloc/percpu로 분리 remote page 비율 목표치 이하 유지
# 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 실패는 단일 원인이 아니라 "가상 주소 단편화, 페이지 부족, 권한/컨텍스트 오용"이 복합적으로 얽히는 경우가 많습니다. 아래 순서로 확인하면 원인 분류가 빠릅니다.

vmalloc 실패 분석 절차 1) 증상 수집 dmesg/stacktrace 2) 용량 확인 VmallocChunk/Used 3) 유형 분류 단편화 vs 페이지부족 4) 개선 적용 kvmalloc/분할 판정 기준 VmallocChunk < 요청 크기: 주소 단편화 우선 의심 Buddy 고갈/높은 reclaim: 물리 페이지 부족 가능성
# 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 여부 점검
실무 권장: hot path에서 대용량 버퍼가 자주 필요하면 매 요청마다 vmalloc/vfree를 반복하지 말고, 초기화 시 풀을 만들고 재사용하세요. 단편화와 TLB 플러시 비용을 동시에 줄일 수 있습니다.

vmap 락 경합과 대량 매핑 처리

대규모 시스템에서 vmalloc/vfree가 여러 CPU에서 동시에 호출되면, vmap 영역 메타데이터 업데이트와 페이지 테이블 갱신 구간에서 락 경합이 발생할 수 있습니다. 특히 작은 크기 할당을 고빈도로 반복하면, 실제 메모리 사용량보다 관리 오버헤드가 먼저 병목이 되는 경우가 많습니다.

동시 vmalloc 요청에서의 경합 지점 CPU0..N 요청 vmalloc(소형/빈번) vmap 메타데이터 갱신 영역 탐색/삽입 PTE 구성 + flush 지연 누적 완화 전략 1) 소형 빈번 할당은 kmalloc/slab 또는 mempool로 이동 2) 대형 버퍼는 초기화 시 배치 할당 후 재사용 3) 실패 시 kvmalloc 폴백과 분할 할당 정책 병행

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 사용자와 주소 공간 압력이 간접 충돌할 수 있습니다.

vmap/ioremap 주소 공간 압력 모델 vmalloc 사용자 네트워크/버퍼/메타데이터 vmap 주소 관리 계층 영역 탐색/삽입/회수 ioremap 사용자 PCI BAR/MMIO window 운영 전략 1) 장수명 MMIO 매핑은 초기화 시점에 고정하고 재매핑 빈도 최소화 2) vmalloc 대형 버퍼는 pool화, 요청 시점 폭주 억제 3) 실패 시 vmallocinfo와 드라이버 매핑 패턴을 함께 분석
주의: 메모리 부족(physical OOM)과 vmap 주소 부족은 증상이 비슷해 보일 수 있습니다. /proc/meminfo의 일반 메모리 지표와 Vmalloc* 지표를 분리해서 해석해야 정확합니다.

참고자료

커널 소스 코드

커널 문서

관련 커밋 및 패치

참고 서적 및 문서

다음 학습: