Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

🌐 vmalloc — 가상 연속 메모리 할당

관련 소스: mm/vmalloc.c (5485줄), include/linux/vmalloc.h (337줄)
관련 문서: Buddy Allocator · SLUB 할당자 · Memblock 할당자 · 메모리 관리 개요

개요 (Overview)

vmalloc은 커널 가상 주소 공간에서 가상적으로 연속된 메모리를 할당합니다. 물리적으로는 비연속일 수 있지만, 페이지 테이블 매핑을 통해 가상 주소 공간에서는 연속으로 보입니다. 큰 커널 버퍼, 모듈 로딩, ioremap 등에 사용되며, DMA 버퍼에는 적합하지 않습니다.

kmalloc이 물리 연속 메모리를 할당하는 반면, vmalloc은 개별 페이지를 할당한 후 페이지 테이블을 조작하여 가상 연속으로 매핑합니다. 따라서 kmalloc보다 느리지만, 큰 할당에 유리합니다.

도서관에 비유하면 kmalloc은 붙어 있는 책장을 통째로 확보하는 방식이고, vmalloc은 여러 층에 흩어진 책을 하나의 연속된 목록 번호로 묶는 방식입니다. 사용자는 연속된 번호만 보지만, 관리자는 각 번호가 실제 어느 책장에 있는지 별도의 표를 유지해야 하므로 페이지 테이블 설정과 TLB/캐시 동기화 비용이 추가됩니다.

커널 가상 메모리맵에서 vmalloc/ioremap 영역은 직접 매핑(Direct Mapping) 영역과 별도입니다. x86_64 4단계 페이지 테이블의 대표 배치에서는 직접 매핑이 ffff888000000000 근처부터, vmalloc/ioremap 영역이 ffffc90000000000 근처부터 시작하지만 실제 값은 KASLR, 5단계 페이지 테이블, KASAN, 아키텍처 설정에 따라 달라집니다. 따라서 vmalloc 주소에는 __pa()/__va() 산술 변환을 적용하지 말고, 주소 성격은 is_vmalloc_addr()와 현재 부팅 인스턴스의 /proc/vmallocinfo, kernel_page_tables로 확인해야 합니다.

소스 파일 경로:

mm/vmalloc.c              ← 메인 구현 (5485줄)
include/linux/vmalloc.h   ← API 선언 및 vm_struct 정의 (337줄)
include/linux/mm.h        ← vmalloc_to_page(), is_vmalloc_addr() 등
mm/internal.h             ← 내부 헬퍼 함수
vmalloc 호출 흐름

빠른 점검 명령

# vmalloc 영역 사용 현황
cat /proc/vmallocinfo | wc -l              # 전체 vmalloc 영역 수
cat /proc/vmallocinfo | head -10           # 상위 vmalloc 할당 목록
grep -i vmalloc /proc/meminfo              # VmallocTotal, VmallocUsed, VmallocChunk

# vmalloc 주소 범위 확인
sudo awk 'NR <= 10 {print $1}' /proc/vmallocinfo  # 시작-끝 가상 주소 범위

# 특정 프로세스의 vmalloc 사용량
cat /proc/$$/status | grep -i vm           # VmData, VmStk 등

# vmalloc 할당 정보 (커널 5.10+)
sudo cat /proc/vmallocinfo | grep -i "vm_struct\|pages=" | head -20

# 메모리 압력 확인
cat /proc/pressure/memory                  # PSI 메모리 압력

# 커널 메모리맵에서 vmalloc 영역 확인
grep -E 'Vmalloc|PageTables' /proc/meminfo

# 직접 매핑, vmalloc, 페이지 테이블 사용량을 함께 확인
grep -E 'Vmalloc|PageTables|DirectMap' /proc/meminfo

# vmalloc/ioremap 실제 매핑과 호출자 확인
sudo cat /proc/vmallocinfo | grep -E 'vmalloc|ioremap|vmap' | head -30

# 물리 주소 공간에서 RAM과 MMIO hole 구분
cat /proc/iomem | head -80

# x86 CONFIG_X86_PTDUMP와 debugfs가 있으면 커널 페이지 테이블 확인
sudo cat /sys/kernel/debug/kernel_page_tables 2>/dev/null | grep -E 'vmalloc|ioremap|page_offset|direct' | head -40

# 주소 폭과 디버그 옵션이 vmalloc 영역 배치에 영향을 주는지 확인
zgrep -E 'CONFIG_X86_5LEVEL|CONFIG_KASAN|CONFIG_MITIGATION_PAGE_TABLE_ISOLATION|CONFIG_HAVE_ARCH_HUGE_VMALLOC' /proc/config.gz 2>/dev/null

핵심 자료구조

vmalloc 핵심 자료구조 관계도

struct vm_struct — vmalloc 할당 메타데이터

/* include/linux/vmalloc.h:52-69 */
struct vm_struct {
    union {
        struct vm_struct *next;    /* vm_area 조기 등록. */
        struct llist_node llnode;  /* 오류 경로에서 비동기 해제. */
    };

    void            *addr;
    unsigned long    size;
    unsigned long    flags;
    struct page    **pages;
#ifdef CONFIG_HAVE_ARCH_HUGE_VMALLOC
    unsigned int     page_order;
#endif
    unsigned int     nr_pages;
    phys_addr_t      phys_addr;
    const void      *caller;
    unsigned long    requested_size;
};

필드 설명:

  • addr: vmalloc 영역에서 할당받은 가상 주소
  • size: guard page 포함 전체 크기 (flags에 VM_NO_GUARD 없으면 +PAGE_SIZE)
  • flags: 할당 유형 (VM_ALLOC=0x02, VM_IOREMAP=0x01, VM_MAP=0x04 등)
  • pages: alloc_pages_bulk()로 할당한 물리 페이지 배열
  • page_order: huge vmalloc 활성화 시 PMD_SHIFT 등으로 설정
  • nr_pages: 실제 할당된 페이지 수
  • caller: 디버깅용 할당 호출자 주소
  • struct vmap_area — 가상 주소 영역 관리

    /* include/linux/vmalloc.h:71-89 */
    struct vmap_area {
        unsigned long va_start;
        unsigned long va_end;
    
        struct rb_node rb_node;         /* 주소 정렬 rbtree */
        struct list_head list;          /* 주소 정렬 list */
    
        /*
         * 다음 두 변수는 하나로 묶을 수 있다. vmap_area 객체는 다음 중 하나다.
         *    1) "free" tree(root는 free_vmap_area_root)에 있음
         *    2) 또는 "busy" tree(root는 vmap_area_root)에 있음
         */
        union {
            unsigned long subtree_max_size; /* "free" tree에 있음 */
            struct vm_struct *vm;           /* "busy" tree에 있음 */
        };
        unsigned long flags; /* vm_map_ram 영역 타입 표시 */
    };

    필드 설명:

  • va_start/va_end: vmalloc 주소 공간 내 가상 주소 범위
  • rb_node: red-black tree 노드 (주소 정렬)
  • subtree_max_size: free tree에서 사용되는 augmented 필드
  • vm: busy tree에서 연결된 vm_struct 포인터
  • struct vmap_node — per-CPU vmap 관리 노드

    /* mm/vmalloc.c:936-952 */
    static struct vmap_node {
        /* 단순한 크기별 격리 저장소. */
        struct vmap_pool pool[MAX_VA_SIZE_PAGES];
        spinlock_t pool_lock;
        bool skip_populate;
    
        /* 이 노드의 관리 데이터. */
        struct rb_list busy;
        struct rb_list lazy;
    
        /*
         * 해제 준비가 끝난 영역.
         */
        struct list_head purge_list;
        struct work_struct purge_work;
        unsigned long nr_purged;
    } single;

    동작 방식:

  • 시스템 초기에는 single 노드 하나만 사용
  • 나중에 vmap 초기화 후 nr_vmap_nodes만큼 확장
  • 주소 기반으로 addr_to_node()로 vmap_area를 특정 노드에 매핑
  • 핵심 함수

    1. vmalloc_noprof — 메인 할당 API

    /* mm/vmalloc.c:4157-4162 */
    void *vmalloc_noprof(unsigned long size)
    {
        return __vmalloc_node_noprof(size, 1, GFP_KERNEL, NUMA_NO_NODE,
                                    __builtin_return_address(0));
    }
    EXPORT_SYMBOL(vmalloc_noprof);

    역할: 크기만 받아 기본 GFP_KERNEL 플래그로 vmalloc 호출

    분기: 없음 (단순 래퍼)

    2. __vmalloc_node_range_noprof — 핵심 할당 로직

    /* mm/vmalloc.c:3986-4103 */
    void *__vmalloc_node_range_noprof(unsigned long size, unsigned long align,
                unsigned long start, unsigned long end, gfp_t gfp_mask,
                pgprot_t prot, unsigned long vm_flags, int node,
                const void *caller)
    {
        struct vm_struct *area;
        void *ret;
        kasan_vmalloc_flags_t kasan_flags = KASAN_VMALLOC_NONE;
        unsigned long original_align = align;
        unsigned int shift = PAGE_SHIFT;
    
        if (WARN_ON_ONCE(!size))
            return NULL;
    
        if ((size >> PAGE_SHIFT) > totalram_pages()) {
            warn_alloc(gfp_mask, NULL,
                "vmalloc error: size %lu, exceeds total pages", size);
            return NULL;
        }
    
        if (vmap_allow_huge && (vm_flags & VM_ALLOW_HUGE_VMAP)) {
            /*
             * Huge page를 시도한다. PAGE_KERNEL 할당에만 시도한다.
             * module 같은 다른 할당은 apply_to_page_range가 huge page를
             * 지원하지 않기 때문에 아직 huge page를 기대하지 않는다.
             */
    
            if (arch_vmap_pmd_supported(prot) && size >= PMD_SIZE)
                shift = PMD_SHIFT;
            else
                shift = arch_vmap_pte_supported_shift(size);
            align = max(original_align, 1UL << shift);
        }
    
    again:
        area = __get_vm_area_node(size, align, shift, VM_ALLOC |
                                  VM_UNINITIALIZED | vm_flags, start, end, node,
                                  gfp_mask, caller);
        if (!area) {
            bool nofail = gfp_mask & __GFP_NOFAIL;
            warn_alloc(gfp_mask, NULL,
                "vmalloc error: size %lu, vm_struct allocation failed%s",
                size, (nofail) ? ". Retrying." : "");
            if (nofail) {
                schedule_timeout_uninterruptible(1);
                goto again;
            }
            goto fail;
        }
    
        /*
         * __vmalloc_area_node()와 kasan_unpoison_vmalloc()에 넘길
         * 인자를 준비한다.
         */
        if (pgprot_val(prot) == pgprot_val(PAGE_KERNEL)) {
            if (kasan_hw_tags_enabled()) {
                /*
                 * tagging을 허용하도록 protection bit를 바꾼다.
                 * 매핑 전에 해야 한다.
                 */
                prot = arch_vmap_pgprot_tagged(prot);
    
                /*
                 * VM_ALLOC 매핑을 뒷받침하는 물리 페이지에 대해 page_alloc
                 * poisoning과 zeroing을 건너뛴다. 대신 메모리는
                 * kasan_unpoison_vmalloc()에서 poison/zero 처리된다.
                 */
                gfp_mask |= __GFP_SKIP_KASAN | __GFP_SKIP_ZERO;
            }
            /* 매핑이 PAGE_KERNEL이라는 점을 기록한다. */
            kasan_flags |= KASAN_VMALLOC_PROT_NORMAL;
        }
    
        /* 물리 페이지를 할당하고 vmalloc 공간에 매핑한다. */
        ret = __vmalloc_area_node(area, gfp_mask, prot, shift, node);
        if (!ret)
            goto fail;
    
        /*
         * 페이지가 매핑되었으므로 접근 가능하다고 표시한다.
         * KASAN_VMALLOC_INIT 설정 조건은 __GFP_SKIP_ZERO 관점에서
         * post_alloc_hook()의 조건과 보완되어야 하며, 같은 조건에서
         * 메모리가 초기화되도록 맞춘다. tag 기반 KASAN 모드는
         * 일반 non-executable 할당에만 tag를 부여한다.
         */
        kasan_flags |= KASAN_VMALLOC_VM_ALLOC;
        if (!want_init_on_free() && want_init_on_alloc(gfp_mask) &&
            (gfp_mask & __GFP_SKIP_ZERO))
            kasan_flags |= KASAN_VMALLOC_INIT;
    
        /* KASAN_VMALLOC_PROT_NORMAL은 필요할 때 이미 설정되어 있다. */
        area->addr = kasan_unpoison_vmalloc(area->addr, size, kasan_flags);
    
        /*
         * 이 함수에서 새로 할당한 vm_struct에는 VM_UNINITIALIZED 플래그가 있다.
         * 이는 vm_struct가 아직 완전히 초기화되지 않았음을 뜻한다.
         * 이제 완전히 초기화되었으므로 여기서 플래그를 제거한다.
         */
        clear_vm_uninitialized_flag(area);
    
        if (!(vm_flags & VM_DEFER_KMEMLEAK))
            kmemleak_vmalloc(area, PAGE_ALIGN(size), gfp_mask);
    
        return area->addr;
    
    fail:
        if (shift > PAGE_SHIFT) {
            shift = PAGE_SHIFT;
            align = original_align;
            goto again;
        }
        return NULL;
    }

    분기 로직:

    1. size 검증: totalram_pages() 초과 시 실패

    2. huge page 시도: vmap_allow_huge && VM_ALLOW_HUGE_VMAP && size >= PMD_SIZE

    3. 할당 실패 시: __GFP_NOFAIL이면 1 tick 대기 후 재시도

    4. PAGE_SHIFT fallback: huge page 실패 시 order-0로 재시도

    3. __get_vm_area_node — 가상 주소 영역 예약

    /* mm/vmalloc.c:3203-3251 */
    struct vm_struct *__get_vm_area_node(unsigned long size,
            unsigned long align, unsigned long shift, unsigned long flags,
            unsigned long start, unsigned long end, int node,
            gfp_t gfp_mask, const void *caller)
    {
        struct vmap_area *va;
        struct vm_struct *area;
        unsigned long requested_size = size;
    
        BUG_ON(in_interrupt());
        size = ALIGN(size, 1ul << shift);
        if (unlikely(!size))
            return NULL;
    
        if (flags & VM_IOREMAP)
            align = 1ul << clamp_t(int, get_count_order_long(size),
                           PAGE_SHIFT, IOREMAP_MAX_ORDER);
    
        area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
        if (unlikely(!area))
            return NULL;
    
        if (!(flags & VM_NO_GUARD))
            size += PAGE_SIZE;
    
        area->flags = flags;
        area->caller = caller;
        area->requested_size = requested_size;
    
        va = alloc_vmap_area(size, align, start, end, node, gfp_mask, 0, area);
        if (IS_ERR(va)) {
            kfree(area);
            return NULL;
        }
    
        /*
         * VM_ALLOC이 아닌 매핑의 페이지는 접근 가능하다고 표시한다.
         * vmalloc 코드 밖에서 매핑될 수 있으므로 여기서 최선 노력 방식으로 수행한다.
         * VM_ALLOC 매핑은 __vmalloc_node_range()에서 매핑된 뒤 접근 가능 표시된다.
         * hardware tag 기반 KASAN에서는 non-VM_ALLOC 매핑 표시를 건너뛴다.
         */
        if (!(flags & VM_ALLOC))
            area->addr = kasan_unpoison_vmalloc(area->addr, requested_size,
                                KASAN_VMALLOC_PROT_NORMAL);
    
        return area;
    }

    역할: vm_struct를 만들고 vmalloc 주소 범위에서 연속 가상 영역을 예약합니다. VM_NO_GUARD가 없으면 guard page를 더하고, VM_IOREMAPIOREMAP_MAX_ORDER를 넘지 않는 범위에서 정렬을 키웁니다.

    분기 로직:

    1. 인터럽트 컨텍스트 금지: BUG_ON(in_interrupt())

    2. 크기 정렬: huge vmalloc이면 shift 기준으로 정렬

    3. guard page 추가: 기본적으로 size += PAGE_SIZE

    4. 가상 주소 예약: alloc_vmap_area()가 free tree 또는 per-node pool에서 영역을 찾아 busy tree에 연결

    4. __vmalloc_area_node — 물리 페이지 할당 및 매핑

    /* mm/vmalloc.c:3827-3933 */
    static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
                                     pgprot_t prot, unsigned int page_shift,
                                     int node)
    {
        const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
        bool nofail = gfp_mask & __GFP_NOFAIL;
        unsigned long addr = (unsigned long)area->addr;
        unsigned long size = get_vm_area_size(area);
        unsigned long array_size;
        unsigned int nr_small_pages = size >> PAGE_SHIFT;
        unsigned int page_order;
        unsigned int flags;
        int ret;
    
        array_size = (unsigned long)nr_small_pages * sizeof(struct page *);
    
        /* __GFP_NOFAIL과 "noblock" 플래그는 함께 쓸 수 없다. */
        if (!gfpflags_allow_blocking(gfp_mask))
            nofail = false;
    
        if (!(gfp_mask & (GFP_DMA | GFP_DMA32)))
            gfp_mask |= __GFP_HIGHMEM;
    
        /* 재귀는 엄격하게 제한된다. */
        if (array_size > PAGE_SIZE) {
            area->pages = __vmalloc_node_noprof(array_size, 1, nested_gfp, node,
                        area->caller);
        } else {
            area->pages = kmalloc_node_noprof(array_size, nested_gfp, node);
        }
    
        if (!area->pages) {
            warn_alloc(gfp_mask, NULL,
                "vmalloc error: size %lu, failed to allocated page array size %lu",
                nr_small_pages * PAGE_SIZE, array_size);
            goto fail;
        }
    
        set_vm_area_page_order(area, page_shift - PAGE_SHIFT);
        page_order = vm_area_page_order(area);
    
        /*
         * 고차 nofail 할당은 매우 비싸고 잠재적으로 위험하다
         * (이른 OOM, 방해적인 reclaim과 compaction 등).
         *
         * __vmalloc_node_range_noprof()는 고차 시도가 실패하면
         * order-0 페이지로 대체한다.
         */
        area->nr_pages = vm_area_alloc_pages(
                vmalloc_gfp_adjust(gfp_mask, page_order), node,
                page_order, nr_small_pages, area->pages);
    
        atomic_long_add(area->nr_pages, &nr_vmalloc_pages);
    
        /* vm의 모든 페이지는 같은 memcg에 청구되어 있으므로 첫 페이지를 사용한다. */
        if (gfp_mask & __GFP_ACCOUNT && area->nr_pages)
            mod_memcg_page_state(area->pages[0], MEMCG_VMALLOC,
                         area->nr_pages);
    
        /*
         * 할당 요청을 완료할 만큼 충분한 페이지를 얻지 못했다면,
         * 일부가 있더라도 vfree()를 통해 해제한다.
         */
        if (area->nr_pages != nr_small_pages) {
            /*
             * vm_area_alloc_pages()는 메모리 부족뿐 아니라 다음 이유로도 실패할 수 있다.
             *
             * - 대기 중인 fatal signal
             * - huge page-order 페이지 부족
             *
             * huge page 경우에는 항상 order-0으로 재시도하므로 둘 다 경고가 부적절하다.
             */
            if (!fatal_signal_pending(current) && page_order == 0)
                warn_alloc(gfp_mask, NULL,
                    "vmalloc error: size %lu, failed to allocate pages",
                    area->nr_pages * PAGE_SIZE);
            goto fail;
        }
    
        /*
         * 페이지 테이블 할당은 외부 gfp mask를 무시하므로
         * scope API로 강제한다.
         */
        flags = memalloc_apply_gfp_scope(gfp_mask);
        do {
            ret = __vmap_pages_range(addr, addr + size, prot, area->pages,
                    page_shift, nested_gfp);
            if (nofail && (ret < 0))
                schedule_timeout_uninterruptible(1);
        } while (nofail && (ret < 0));
        memalloc_restore_scope(flags);
    
        if (ret < 0) {
            warn_alloc(gfp_mask, NULL,
                "vmalloc error: size %lu, failed to map pages",
                area->nr_pages * PAGE_SIZE);
            goto fail;
        }
    
        return area->addr;
    
    fail:
        defer_vm_area_cleanup(area);
        return NULL;
    }

    분기 로직:

    1. 페이지 배열 할당: array_size > PAGE_SIZE → vmalloc, 아니면 kmalloc

    2. huge page 실패 시: order-0 fallback (vm_area_alloc_pages 내부)

    3. 매핑 실패 시: nofail이면 1 tick 대기 후 재시도

    4. 할당 불완전 시: fatal_signal_pending 아니고 order-0이면 경고 후 실패

    vm_area_alloc_pages()는 먼저 가능한 큰 order 페이지를 시도하고, order-0에서는 bulk allocator를 사용합니다. bulk 요청은 한 번에 최대 100페이지로 제한되고, 일부만 채워지면 단일 페이지 할당 경로로 내려갑니다.

    /* mm/vmalloc.c:3640-3760 */
    static inline unsigned int
    vm_area_alloc_pages(gfp_t gfp, int nid,
            unsigned int order, unsigned int nr_pages, struct page **pages)
    {
        unsigned int nr_allocated = 0;
        unsigned int nr_remaining = nr_pages;
        unsigned int max_attempt_order = MAX_PAGE_ORDER;
        struct page *page;
        int i;
        unsigned int large_order = ilog2(nr_remaining);
        gfp_t large_gfp = vmalloc_gfp_adjust(gfp, large_order) & ~__GFP_DIRECT_RECLAIM;
    
        large_order = min(max_attempt_order, large_order);
    
        /*
         * 처음에는 page allocator가 large-order 페이지를 주도록 시도한다.
         * __vmap_pages_range()가 정확히 order 길이의 물리 연속 chunk를 기대하므로
         * order보다 작은 chunk는 시도하지 않는다.
         */
        while (large_order > order && nr_remaining) {
            if (nid == NUMA_NO_NODE)
                page = alloc_pages_noprof(large_gfp, large_order);
            else
                page = alloc_pages_node_noprof(nid, large_gfp, large_order);
    
            if (unlikely(!page)) {
                max_attempt_order = --large_order;
                continue;
            }
    
            split_page(page, large_order);
            for (i = 0; i < (1U << large_order); i++)
                pages[nr_allocated + i] = page + i;
    
            nr_allocated += 1U << large_order;
            nr_remaining = nr_pages - nr_allocated;
    
            large_order = ilog2(nr_remaining);
            large_order = min(max_attempt_order, large_order);
        }
    
        /*
         * order-0 페이지는 bulk allocator를 사용한다. 실패로 인해 page array가
         * 일부만 채워졌거나 전혀 채워지지 않으면 더 관대한 단일 페이지
         * allocator로 대체한다.
         */
        if (!order) {
            while (nr_allocated < nr_pages) {
                unsigned int nr, nr_pages_request;
    
                /*
                 * 허용되는 최대 요청은 하드코딩되어 있으며 호출당 100페이지다.
                 * bulk allocator에서 긴 preemption-off 상황을 막기 위한 것으로,
                 * 범위는 [1:100]이다.
                 */
                nr_pages_request = min(100U, nr_pages - nr_allocated);
    
                /* 메모리 할당은 mempolicy를 고려해야 한다. nid == NUMA_NO_NODE일 때
                 * nearest node를 잘못 사용하면 안 된다. 그렇지 않으면 mempolicy가
                 * interleaving으로 메모리를 할당하려는데 한 노드에만 할당될 수 있다.
                 */
                if (IS_ENABLED(CONFIG_NUMA) && nid == NUMA_NO_NODE)
                    nr = alloc_pages_bulk_mempolicy_noprof(gfp,
                                nr_pages_request,
                                pages + nr_allocated);
                else
                    nr = alloc_pages_bulk_node_noprof(gfp, nid,
                                nr_pages_request,
                                pages + nr_allocated);
    
                nr_allocated += nr;
    
                /*
                 * 0페이지 또는 일부 페이지만 얻었다면 단일 페이지 allocator로 대체한다.
                 */
                if (nr != nr_pages_request)
                    break;
            }
        }
    
        /* 고차 페이지 또는 "bulk" 실패 시 대체 경로. */
        while (nr_allocated < nr_pages) {
            if (!(gfp & __GFP_NOFAIL) && fatal_signal_pending(current))
                break;
    
            if (nid == NUMA_NO_NODE)
                page = alloc_pages_noprof(gfp, order);
            else
                page = alloc_pages_node_noprof(nid, gfp, order);
    
            if (unlikely(!page))
                break;
    
            /*
             * 고차 할당은 호출자가 독립적인 작은 페이지처럼 다룰 수 있어야 한다.
             * 일부 드라이버는 vmalloc_to_page() 페이지에 자체 refcounting을 하고,
             * 일부는 page->mapping, page->lru 등을 사용한다.
             */
            if (order)
                split_page(page, order);
    
            /*
             * page-order 페이지를 할당하고 매핑하지만 tracking은 PAGE_SIZE 페이지마다
             * 수행하여 vm_struct API가 물리/매핑 크기에 의존하지 않게 한다.
             */
            for (i = 0; i < (1U << order); i++)
                pages[nr_allocated + i] = page + i;
    
            nr_allocated += 1U << order;
        }
    
        return nr_allocated;
    }

    5. vfree — 메모리 해제

    /* mm/vmalloc.c:3442-3487 */
    void vfree(const void *addr)
    {
        struct vm_struct *vm;
        int i;
    
        if (unlikely(in_interrupt())) {
            vfree_atomic(addr);
            return;
        }
    
        BUG_ON(in_nmi());
        kmemleak_free(addr);
        might_sleep();
    
        if (!addr)
            return;
    
        vm = remove_vm_area(addr);
        if (unlikely(!vm)) {
            WARN(1, KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
                    addr);
            return;
        }
    
        if (unlikely(vm->flags & VM_FLUSH_RESET_PERMS))
            vm_reset_perms(vm);
        /* vm의 모든 페이지는 같은 memcg에 청구되어 있으므로 첫 페이지를 사용한다. */
        if (vm->nr_pages && !(vm->flags & VM_MAP_PUT_PAGES))
            mod_memcg_page_state(vm->pages[0], MEMCG_VMALLOC, -vm->nr_pages);
        for (i = 0; i < vm->nr_pages; i++) {
            struct page *page = vm->pages[i];
    
            BUG_ON(!page);
            /*
             * huge vmalloc의 high-order 할당은 split되므로
             * order-0 할당 배열처럼 해제할 수 있다.
             */
            __free_page(page);
            cond_resched();
        }
        if (!(vm->flags & VM_MAP_PUT_PAGES))
            atomic_long_sub(vm->nr_pages, &nr_vmalloc_pages);
        kvfree(vm->pages);
        kfree(vm);
    }
    EXPORT_SYMBOL(vfree);

    분기 로직:

    1. 인터럽트 컨텍스트: vfree_atomic()으로 지연 해제

    2. NMI 컨텍스트: BUG_ON()으로 패닉

    3. VM_FLUSH_RESET_PERMS: 권한 초기화 후 해제

    4. VM_MAP_PUT_PAGES: 페이지 배열 소유권 이전됨

    6. vfree_atomic — 지연 해제

    /* mm/vmalloc.c:3408-3423 */
    void vfree_atomic(const void *addr)
    {
        struct vfree_deferred *p = raw_cpu_ptr(&vfree_deferred);
    
        BUG_ON(in_nmi());
    
        kmemleak_free(addr);
    
        /*
         * 선점 가능한 컨텍스트에서 호출될 수 있으므로 raw_cpu_ptr()를 사용한다.
         * 여기서는 선점이 문제되지 않는다. llist_add() 구현은 lockless라서
         * 다른 CPU의 리스트에 추가하더라도 동작하며, schedule_work()도 괜찮다.
         */
        if (addr && llist_add((struct llist_node *)addr, &p->list))
            schedule_work(&p->wq);
    }

    역할: 원자적 컨텍스트에서 주소를 per-CPU llist에 추가하고 workqueue에서 나중에 vfree() 경로로 넘깁니다. NMI에서는 허용하지 않으며, addr == NULL이면 리스트에 넣지 않습니다.

    호출 흐름

    vmalloc(size)
      └─> __vmalloc_node_noprof(size, 1, GFP_KERNEL, NUMA_NO_NODE, caller)
            └─> __vmalloc_node_range_noprof(size, align, VMALLOC_START, VMALLOC_END, ...)
                  ├─> [1] __get_vm_area_node()         ← 가상 주소 영역 할당
                  │     └─> alloc_vmap_area()          ← vmap_area red-black tree 검색
                  ├─> [2] __vmalloc_area_node()        ← 물리 페이지 할당 + 매핑
                  │     ├─> kmalloc_node() / __vmalloc_node()  ← 페이지 배열 할당
                  │     ├─> vm_area_alloc_pages()      ← Buddy/bulk allocator에서 페이지 할당
                  │     └─> __vmap_pages_range()       ← 페이지 테이블 매핑
                  ├─> [3] kasan_unpoison_vmalloc()     ← KASAN 해제
                  └─> [4] clear_vm_uninitialized_flag() ← VM_UNINITIALIZED 제거
    
    vfree(addr)
      ├─> [interrupt context] vfree_atomic()
      │     └─> llist_add() + schedule_work()
      └─> [process context] remove_vm_area()
            └─> [1] find_vmap_area()           ← vmap_area 검색
            └─> [2] unlink_va()               ← busy tree에서 제거
            └─> [3] __free_page() per page    ← 페이지별 Buddy 반환
            └─> [4] kvfree(vm->pages)         ← 페이지 배열 해제
            └─> [5] kfree(vm)                 ← vm_struct 해제

    조건별 비교

    vmalloc vs kmalloc

    특성kmallocvmalloc
    **물리 연속**O (반드시 연속)X (비연속 가능)
    **가상 연속**OO
    **최대 크기**~4MB (order-10)가상 주소 공간 한도
    **DMA 사용**OX
    **속도**빠름 (SLUB 직접)느림 (페이지 테이블 매핑)
    **slab 캐시**kmalloc-32, kmalloc-64 등사용 안 함
    **대표 사용처**커널 오브젝트, small buffer큰 버퍼, 모듈 로딩

    직접 매핑 vs vmalloc/ioremap

    특성직접 매핑 (Direct Mapping)vmalloc/ioremap 영역
    **주요 대상**System RAM의 선형 매핑vmalloc 페이지, vmap 매핑, MMIO ioremap
    **물리 연속성**물리 주소와 선형 오프셋 관계물리적으로 흩어진 페이지 또는 장치 물리 주소 가능
    **주소 변환**`__pa()` / `__va()` 산술 변환 가능페이지 테이블 매핑을 조회해야 하며 산술 변환 금지
    **판별 API**`virt_addr_valid()`가 직접 매핑 RAM 판별에 가까움`is_vmalloc_addr()`, `/proc/vmallocinfo`로 확인
    **DMA 적합성**일반적으로 DMA API와 함께 사용 가능`vmalloc()` 주소는 직접 DMA 버퍼로 부적합
    **대표 사용처**Buddy/SLUB가 반환한 일반 커널 메모리큰 커널 버퍼, 모듈, BPF/JIT 보조 영역, MMIO mapping

    ioremap()은 RAM을 새로 할당하는 API가 아니라 장치 레지스터 같은 MMIO 물리 주소를 커널 가상 주소로 접근 가능하게 만드는 API입니다. 임베디드 SoC나 PCI 드라이버에서는 Device Tree/ACPI/PCI BAR가 알려준 리소스를 devm_ioremap_resource()로 매핑하고, DMA 버퍼는 별도로 dma_alloc_coherent() 또는 dma_map_*() 계열을 사용해야 합니다.

    kvmalloc 선택 기준

    상황권장 API이유
    크기가 작고 물리 연속성이 필요`kmalloc()`SLUB fast path와 DMA 친화성
    크기가 크지만 물리 연속성이 필요 없음`vmalloc()`외부 단편화 영향을 덜 받음
    크기가 런타임에 달라 작을 수도 큼직할 수도 있음`kvmalloc()``kmalloc()` 우선 시도 후 큰 요청에서 vmalloc fallback
    0으로 초기화된 큰 버퍼`kvzalloc()` / `vzalloc()`초기화 비용을 API 의미에 포함
    NUMA 노드 선호가 있음`kvmalloc_node()` / `vmalloc_node()`page allocator 호출 시 노드 힌트 전달

    kvmalloc() 계열은 작은 크기에서는 kmalloc의 빠른 경로를 살리고, 큰 크기나 단편화 상황에서는 vmalloc fallback으로 성공률을 높입니다. 단, fallback 이후 반환 주소는 vmalloc 주소일 수 있으므로 물리 연속성, DMA 직접 사용, virt_to_phys() 같은 가정은 두면 안 됩니다.

    vmalloc API 비교

    API특징용도
    `vmalloc(size)`GFP_KERNEL, nozero큰 커널 버퍼
    `vzalloc(size)`GFP_KERNEL + __GFP_ZERO제로 초기화 필요 시
    `vmalloc_node(size, node)`특정 NUMA 노드NUMA 친화 할당
    `vmalloc_user(size)`VM_USERMAP 플래그userspace 매핑 가능
    `vmalloc_32(size)`32비트 호환32비트 드라이버
    `__vmalloc(size, gfp)`GFP 플래그 지정세밀한 제어

    huge vmalloc 조건

    조건결과
    `CONFIG_HAVE_ARCH_HUGE_VMALLOC` 미활성화huge vmalloc 불가
    `vmap_allow_huge = false` (nohugevmalloc 파라미터)huge vmalloc 불가
    `size < PMD_SIZE` (일반적으로 2MB)order-0 할당
    `arch_vmap_pmd_supported(prot)` falsePMD 매핑 불가
    huge page 실패order-0 fallback 자동

    vmalloc 영역 배치에 영향을 주는 조건

    조건영향확인 방법
    x86_64 4단계 페이지 테이블vmalloc/ioremap 영역이 대표적으로 `ffffc90000000000` 근처에 배치`sudo cat /sys/kernel/debug/kernel_page_tables`
    x86_64 5단계 페이지 테이블(LA57)유저/커널 가상 주소 폭이 커지고 vmalloc 영역 규모도 PB 단위로 확장`grep la57 /proc/cpuinfo`, `zgrep CONFIG_X86_5LEVEL /proc/config.gz`
    KASLR절대 주소가 부팅마다 달라질 수 있음`dmesggrep -i kaslr`
    KASANshadow 영역이 추가되어 vmalloc 주변 배치와 가드 정책에 영향`zgrep CONFIG_KASAN /proc/config.gz`
    PTI사용자 CR3와 커널 CR3 관점의 매핑이 달라짐`grep pti /proc/cpuinfo`
    ARM64/RISC-VVA_BITS, 페이지 크기, Sv39/Sv48/Sv57에 따라 vmalloc 범위가 다름`uname -m`, `zgrep -E 'CONFIG_ARM64_VA_BITSCONFIG_RISCV' /proc/config.gz`

    vfree 동작 조건

    컨텍스트동작
    프로세스 컨텍스트즉시 해제 (remove_vm_area + __free_page)
    인터럽트 컨텍스트vfree_atomic() → workqueue 지연 해제
    NMI 컨텍스트BUG_ON() 패닉
    `__GFP_NOFAIL` 할당실패 시 재시도 로직 없음 (경고 후 실패)

    관련 문서

  • 메모리 관리 개요 — 전체 메모리 관리 조감도
  • Buddy Allocator — 물리 페이지 할당자
  • SLUB 할당자 — 커널 오브젝트 캐시
  • VMA / mmap — 가상 메모리 영역 관리
  • Memblock 할당자 — 부팅 초기 할당
  • 페이지 회수 — kswapd, direct reclaim

  • 요약

    vmalloc의 핵심 내용:

    1. vmalloc 3단계 내부 동작: alloc_vmap_area() → alloc_pages_bulk() → vmap_pages_range()

    2. kvmalloc: kmalloc 우선 시도 후 vmalloc fallback

    3. vmalloc vs kmalloc 비교: 물리 연속, 최대 크기, 용도

    4. vmalloc 영역 명령어: /proc/vmallocinfo, /proc/meminfo의 VmallocTotal/Used/Chunk

    5. ioremap과의 관계: vmalloc/ioremap 영역 공유 (커널 가상 메모리맵)

    vmalloc은 가상 주소 예약, 물리 페이지 확보, 페이지 테이블 매핑, KASAN/memcg 후처리가 함께 움직이는 경로이므로 주소 성격과 할당 컨텍스트를 먼저 구분해야 안전하게 사용할 수 있습니다.