큰 kernel buffer를 요청했는데 RAM은 충분해 보이고 할당은 실패합니다. 이때 확인할 것은 physical page만이 아닙니다. vmalloc은 흩어진 page들을 하나의 연속된 kernel virtual range로 묶으므로, 그 주소들을 배치할 빈 구간도 필요합니다. 충분한 free page와 충분히 긴 virtual hole은 서로 다른 자원입니다.
이 글은 vmalloc의 앞부분, 즉 어느 주소를 소유할 것인지를 정하는 과정을 다룹니다. 설명의 기준은 x86-64와 upstream v6.18입니다. 예전 free-tree 알고리즘도 함께 살펴보되, 현재의 모든 요청이 하나의 global tree만 통과한다고 가정하지 않습니다.
연속성이 필요한 쪽은 virtual address입니다
vmalloc이 반환한 pointer에서 연속된 byte를 읽을 수 있다고 해서 physical address도 연속된 것은 아닙니다. page table이 서로 떨어진 physical page들을 연속된 virtual page에 연결합니다. 따라서 일반적인 vmalloc buffer를 하나의 physical address와 길이로 표현하여 DMA 장치에 전달할 수는 없습니다. DMA API가 다루는 주소와 CPU virtual pointer도 별개의 계약입니다.
vmalloc()은 일반적인 kernel allocation이고 vzalloc()은 zero-filled allocation입니다. NUMA node를 지정하는 계열과 GFP mask를 받는 계열도 있습니다. v6.18에서는 allocation profiling wrapper 때문에 실제 정의에 _noprof suffix가 나타납니다. zeroing은 이름만 바뀐 별도 주소 공간이 아니라 allocation의 초기화 조건입니다.
핵심 구현인 __vmalloc_node_range_noprof()는 크기, alignment, 허용 주소의 시작과 끝, GFP mask, page protection, vm flags, node, caller를 받습니다. 주소의 끝은 exclusive bound입니다. 크기 0, 지나치게 큰 요청, alignment와 반올림으로 표현할 수 없는 크기는 정상적인 buffer가 될 수 없습니다. GFP 인자를 받는다고 해서 임의의 allocator flag 조합이 지원되는 것도 아닙니다. 해당 함수의 주석은 direct reclaim을 요구하며 지원하는 reclaim modifier도 제한합니다.
v6.18의 이 계약은 GFP_KERNEL·GFP_NOFS·GFP_NOIO를 지원하고 zone modifier는 지원하지 않는다고 명시합니다. reclaim modifier 중에서는 __GFP_DIRECT_RECLAIM이 필요하고 __GFP_NOFAIL만 지원하며, __GFP_NORETRY·__GFP_RETRY_MAYFAIL은 지원하지 않습니다. 책의 예전 __vmalloc_node() comment가 NOFAIL도 지원하지 않는다고 적은 부분을 현재 전체 경로의 계약으로 가져오면 실제 retry branch와 충돌합니다. 공개 API와 그 version의 comment·구현을 함께 확인해야 합니다.
huge vmap을 허용한 경로는 큰 leaf에 맞는 page shift와 alignment를 시도할 수 있습니다. 그러나 모든 vmalloc이 huge mapping인 것은 아닙니다. protection, architecture 지원, allocation 크기와 NUMA 배치 등을 검토하고, 큰 page 시도가 실패하면 base-page 경로로 다시 시도할 수 있도록 설계되어 있습니다. 처음 시도한 alignment가 실패 뒤에도 무조건 유지된다고 읽어서는 안 됩니다.
현재 huge-vmap 판단에는 version 차이가 있습니다. 책의 구현은 node 미지정 요청에서 size / num_online_nodes()를 사용해 interleave의 최악 조건을 가늠했지만, v6.18의 PMD 후보는 arch_vmap_pmd_supported(prot) && size >= PMD_SIZE로 검사합니다. PMD 후보를 쓰지 않는 쪽의 arch_vmap_pte_supported_shift()는 architecture가 PTE 기반 mapping에 허용할 shift를 고르는 hook이며, 책에서는 PowerPC의 특수 경로를 예로 듭니다. 이 hook의 존재를 x86에서도 PMD보다 큰 임의의 PTE leaf가 가능하다는 뜻으로 읽지 않습니다. 현재 함수는 원래 alignment를 보관했다가 큰-page 실패 후 되돌립니다. 책의 per-node 산식을 아래 현재 code의 실행 조건으로 섞지 않습니다.
공개 API의 관계도 반환값과 입력 계약으로 읽는 편이 좋습니다. array 계열은 원소 수·크기의 overflow, vcalloc은 zeroing, node 계열은 NUMA 선택, user 계열은 remapping 허용, huge 계열은 큰 mapping 허용이라는 서로 다른 조건을 더합니다. vmalloc_32의 32는 32-bit virtual pointer를 반환한다는 뜻이 아니라 제한된 backing memory zone과 관련됩니다. 아래 세 wrapper의 전체 정의에서는 alignment 1, NUMA_NO_NODE, caller 기록과 __GFP_ZERO의 차이를 바로 볼 수 있습니다.
code는 Linux v6.18 mm/vmalloc.c의 GPL-2.0-only 전체 함수이며, 책의 축약 listing을 복원한 code는 아닙니다. source·license 기록과 GPL-2.0을 함께 제공합니다. kernel 의존성은 별도이며 실행하지 않았습니다.
현재 API 선택에서는 vmalloc_array()가 기본 GFP, __vmalloc_array()가 caller의 GFP를 사용하며 둘 다 곱셈 overflow를 검사하는 경로로 모입니다. vcalloc()·__vcalloc()은 여기에 zeroing을 추가합니다. 이 array wrapper들은 v6.18 mm/util.c에 있습니다. __vmalloc()은 GFP를 받고, node 계열은 __vmalloc_node()에 명시적 node를 전달합니다. vmalloc_user()·vmalloc_32_user()는 zeroing과 VM_USERMAP을 더합니다. v6.18의 vmalloc_huge()는 header wrapper에서 vmalloc_huge_node()로 연결되어 VM_ALLOW_HUGE_VMAP을 전달합니다. 이는 예전 API 관계도를 동일 이름의 현재 직접 호출 그래프로 복제하지 않고 입력 조건으로 읽는 방법입니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 1993 Linus Torvalds
* Support of BIGMEM added by Gerhard Wichert, Siemens AG, July 1999
* SMP-safe vmalloc/vfree/ioremap, Tigran Aivazian <tigran@veritas.com>, May 2000
* Major rework to support vmap/vunmap, Christoph Hellwig, SGI, August 2002
* Numa awareness, Christoph Lameter, SGI, June 2005
* Improving global KVA allocator, Uladzislau Rezki, Sony, May 2019
*/
/*
* Linux v6.18, mm/vmalloc.c:3991-3995
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: vmalloc_noprof
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
void *vmalloc_noprof(unsigned long size)
{
return __vmalloc_node_noprof(size, 1, GFP_KERNEL, NUMA_NO_NODE,
__builtin_return_address(0));
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 1993 Linus Torvalds
* Support of BIGMEM added by Gerhard Wichert, Siemens AG, July 1999
* SMP-safe vmalloc/vfree/ioremap, Tigran Aivazian <tigran@veritas.com>, May 2000
* Major rework to support vmap/vunmap, Christoph Hellwig, SGI, August 2002
* Numa awareness, Christoph Lameter, SGI, June 2005
* Improving global KVA allocator, Uladzislau Rezki, Sony, May 2019
*/
/*
* Linux v6.18, mm/vmalloc.c:4032-4036
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: vzalloc_noprof
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
void *vzalloc_noprof(unsigned long size)
{
return __vmalloc_node_noprof(size, 1, GFP_KERNEL | __GFP_ZERO, NUMA_NO_NODE,
__builtin_return_address(0));
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 1993 Linus Torvalds
* Support of BIGMEM added by Gerhard Wichert, Siemens AG, July 1999
* SMP-safe vmalloc/vfree/ioremap, Tigran Aivazian <tigran@veritas.com>, May 2000
* Major rework to support vmap/vunmap, Christoph Hellwig, SGI, August 2002
* Numa awareness, Christoph Lameter, SGI, June 2005
* Improving global KVA allocator, Uladzislau Rezki, Sony, May 2019
*/
/*
* Linux v6.18, mm/vmalloc.c:3957-3962
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: __vmalloc_node_noprof
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
void *__vmalloc_node_noprof(unsigned long size, unsigned long align,
gfp_t gfp_mask, int node, const void *caller)
{
return __vmalloc_node_range_noprof(size, align, VMALLOC_START, VMALLOC_END,
gfp_mask, PAGE_KERNEL, 0, node, caller);
}
핵심 함수에는 KASAN이 protection을 tagging용으로 바꾸고 page allocator의 poisoning·zeroing을 뒤쪽 vmalloc 초기화로 미루는 분기도 있습니다. mapping 후 접근 가능 상태를 반영하고, 초기화 flag를 지운 다음 VM_DEFER_KMEMLEAK가 없을 때 kmemleak 처리를 합니다. 아래는 이 분기들도 제거하지 않은 전체 정의입니다. 모든 KASAN mode의 내부 동작을 같은 것으로 단정하지 않습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 1993 Linus Torvalds
* Support of BIGMEM added by Gerhard Wichert, Siemens AG, July 1999
* SMP-safe vmalloc/vfree/ioremap, Tigran Aivazian <tigran@veritas.com>, May 2000
* Major rework to support vmap/vunmap, Christoph Hellwig, SGI, August 2002
* Numa awareness, Christoph Lameter, SGI, June 2005
* Improving global KVA allocator, Uladzislau Rezki, Sony, May 2019
*/
/*
* Linux v6.18, mm/vmalloc.c:3819-3936
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: __vmalloc_node_range_noprof
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
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)) {
/*
* Try huge pages. Only try for PAGE_KERNEL allocations,
* others like modules don't yet expect huge pages in
* their allocations due to apply_to_page_range not
* supporting them.
*/
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;
}
/*
* Prepare arguments for __vmalloc_area_node() and
* kasan_unpoison_vmalloc().
*/
if (pgprot_val(prot) == pgprot_val(PAGE_KERNEL)) {
if (kasan_hw_tags_enabled()) {
/*
* Modify protection bits to allow tagging.
* This must be done before mapping.
*/
prot = arch_vmap_pgprot_tagged(prot);
/*
* Skip page_alloc poisoning and zeroing for physical
* pages backing VM_ALLOC mapping. Memory is instead
* poisoned and zeroed by kasan_unpoison_vmalloc().
*/
gfp_mask |= __GFP_SKIP_KASAN | __GFP_SKIP_ZERO;
}
/* Take note that the mapping is PAGE_KERNEL. */
kasan_flags |= KASAN_VMALLOC_PROT_NORMAL;
}
/* Allocate physical pages and map them into vmalloc space. */
ret = __vmalloc_area_node(area, gfp_mask, prot, shift, node);
if (!ret)
goto fail;
/*
* Mark the pages as accessible, now that they are mapped.
* The condition for setting KASAN_VMALLOC_INIT should complement the
* one in post_alloc_hook() with regards to the __GFP_SKIP_ZERO check
* to make sure that memory is initialized under the same conditions.
* Tag-based KASAN modes only assign tags to normal non-executable
* allocations, see __kasan_unpoison_vmalloc().
*/
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 already set if required. */
area->addr = kasan_unpoison_vmalloc(area->addr, size, kasan_flags);
/*
* In this function, newly allocated vm_struct has VM_UNINITIALIZED
* flag. It means that vm_struct is not fully initialized.
* Now, it is fully initialized, so remove this flag here.
*/
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;
}
PTE 기반 큰 mapping의 후보
PTE 기반 mapping에도 architecture별 크기 선택이 있을 수 있습니다. 구체적으로 v6.18 PowerPC 8xx의 mmu-8xx.h는 요청 크기가 512 KiB 이상이면 shift 19, 16 KiB 이상이면 shift 14, 그보다 작으면 PAGE_SHIFT를 후보로 반환합니다. 이는 PowerPC 전체의 공통 page 크기 표도, x86 PTE에서 임의 크기의 leaf를 만든다는 뜻도 아닙니다.
요청 크기만 보고 후보를 고른 뒤 실제 mapping에서는 조건을 다시 검사합니다. arch_vmap_try_size()는 남은 virtual 길이, caller의 최대 shift, virtual 시작과 physical PFN 양쪽의 alignment를 확인합니다. 충분한 byte가 있어도 시작이 어긋나면 그 크기를 사용할 수 없습니다. arch_vmap_pte_range_map_size()는 먼저 512 KiB를 시도하고, base page가 16 KiB인 경우에는 그 단위를 유지하며, 그렇지 않으면 16 KiB 후보를 검사한 뒤 base-page로 내려갑니다.
다음은 같은 official tag의 GPL-2.0 전체 함수 세 개입니다. 이름만 소개하는 대신 크기 후보와 실제 적합성 판정을 직접 비교할 수 있게 분리했습니다. kernel 내부 의존성은 별도이며, 이 tag를 책의 정확한 source revision이라고 주장하지 않습니다.
powerpc_arch_vmap_pte_supported_shift.c
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Linux v6.18, PowerPC 8xx helper, complete function.
* Source: arch/powerpc/include/asm/nohash/32/mmu-8xx.h
* Lines: 253-261; body unchanged, kernel dependencies required.
* License: GPL-2.0.txt; see provenance.json for official URL and SHA.
*/
static inline int arch_vmap_pte_supported_shift(unsigned long size)
{
if (size >= SZ_512K)
return 19;
else if (size >= SZ_16K)
return 14;
else
return PAGE_SHIFT;
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Linux v6.18, PowerPC 8xx helper, complete function.
* Source: arch/powerpc/include/asm/nohash/32/mmu-8xx.h
* Lines: 222-238; body unchanged, kernel dependencies required.
* License: GPL-2.0.txt; see provenance.json for official URL and SHA.
*/
static inline bool arch_vmap_try_size(unsigned long addr, unsigned long end, u64 pfn,
unsigned int max_page_shift, unsigned long size)
{
if (end - addr < size)
return false;
if ((1UL << max_page_shift) < size)
return false;
if (!IS_ALIGNED(addr, size))
return false;
if (!IS_ALIGNED(PFN_PHYS(pfn), size))
return false;
return true;
}
powerpc_arch_vmap_pte_range_map_size.c
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Linux v6.18, PowerPC 8xx helper, complete function.
* Source: arch/powerpc/include/asm/nohash/32/mmu-8xx.h
* Lines: 240-250; body unchanged, kernel dependencies required.
* License: GPL-2.0.txt; see provenance.json for official URL and SHA.
*/
static inline unsigned long arch_vmap_pte_range_map_size(unsigned long addr, unsigned long end,
u64 pfn, unsigned int max_page_shift)
{
if (arch_vmap_try_size(addr, end, pfn, max_page_shift, SZ_512K))
return SZ_512K;
if (PAGE_SIZE == SZ_16K)
return SZ_16K;
if (arch_vmap_try_size(addr, end, pfn, max_page_shift, SZ_16K))
return SZ_16K;
return PAGE_SIZE;
}
이 예외는 앞의 huge-vmap 성공 조건을 약하게 만들지 않습니다. caller가 얻은 backing의 연속성과 alignment, architecture의 설치 규칙을 모두 만족해야 합니다. 크기 후보를 고르는 hook과 page table에 실제 translation을 설치하는 helper를 같은 기능으로 읽지 않아야 합니다.
vm_struct와 vmap_area의 역할
vm_struct는 사용 중인 allocation의 설명서입니다. addr와 size는 주소 범위를, pages와 nr_pages는 실제 확보한 backing page들을, page_order는 backing allocation의 단위를 나타냅니다. caller는 진단에 도움이 됩니다. phys_addr는 특히 I/O remapping과 관련된 정보이며, 모든 allocation에서 의미가 같지 않다고 해서 사용하지 않는 field는 아닙니다.
vmap_area는 주소 구간 관리에 집중합니다. 어느 interval을 차지하는지, tree와 list에서 어디에 연결되는지, 그 주소에 대응하는 vm_struct가 무엇인지가 중요합니다. 두 객체를 구분하면 주소를 예약했지만 backing이 아직 완성되지 않은 중간 상태도 표현할 수 있습니다. VM_UNINITIALIZED도 이런 초기화 과정과 관계가 있습니다.
flag들은 서로 다른 축입니다. VM_ALLOC은 vmalloc backing allocation, VM_IOREMAP은 I/O mapping, VM_USERMAP은 user mapping을 허용하는 용도를 표시합니다. VM_DMA_COHERENT는 DMA 관련 allocation을 구별하며, VM_KASAN은 KASAN 관련 상태입니다. VM_ALLOW_HUGE_VMAP은 큰 mapping의 허용이지 성공 증명이 아닙니다. VM_DEFER_KMEMLEAK은 kmemleak 처리를 미루는 조건입니다.
next는 early initialization의 vmalloc list에 쓰이며, current requested_size는 guard·alignment를 더한 size와 별도로 원래 요청을 기록합니다. VM_MAP은 vmap 경로를 표시합니다. VM_USERMAP과 VM_DMA_COHERENT는 remap_vmalloc_range_partial()의 허용 검사에서 둘을 동시에 요구하는 AND가 아니라 어느 쪽도 없을 때 거절하는 OR 조건입니다. VM_UNINITIALIZED의 관측 경로도 version별로 확인해야 합니다. v6.18의 vmalloc_info_show()는 이 flag가 남아 있으면 해당 allocation의 출력을 건너뛰며, flag를 확인한 뒤에는 초기화 완료 측의 write barrier와 짝인 read barrier를 사용합니다. 단순한 통계용 표시만이 아니라 미완성 field를 읽지 않도록 하는 publication 조건입니다.
다음은 v6.18 vmalloc.h의 두 전체 구조체입니다. header의 SPDX는 GPL-2.0입니다. vmap_area의 union은 free 상태의 subtree_max_size와 busy 상태의 vm이 동시에 유효한 field가 아님을 보여 줍니다. header comment의 global busy-root 이름을 현재 모든 allocation의 단일 global lock으로 확대하지 않습니다.
// SPDX-License-Identifier: GPL-2.0
/*
* Linux v6.18, include/linux/vmalloc.h:52-65
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/vmalloc.h
* Complete struct: vm_struct
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
struct vm_struct {
struct vm_struct *next;
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;
};
// SPDX-License-Identifier: GPL-2.0
/*
* Linux v6.18, include/linux/vmalloc.h:67-85
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/vmalloc.h
* Complete struct: vmap_area
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
struct vmap_area {
unsigned long va_start;
unsigned long va_end;
struct rb_node rb_node; /* address sorted rbtree */
struct list_head list; /* address sorted list */
/*
* The following two variables can be packed, because
* a vmap_area object can be either:
* 1) in "free" tree (root is free_vmap_area_root)
* 2) or "busy" tree (root is vmap_area_root)
*/
union {
unsigned long subtree_max_size; /* in "free" tree */
struct vm_struct *vm; /* in "busy" tree */
};
unsigned long flags; /* mark type of vm_map_ram area */
};
반환 뒤 누가 page를 해제하는지도 flag 계약의 일부입니다. VM_MAP_PUT_PAGES를 지정한 vmap()은 성공 시 page 배열과 각 page reference의 소유권을 넘겨받습니다. 반면 일반적인 vmap()은 caller가 가진 page들을 mapping할 뿐입니다. VM_FLUSH_RESET_PERMS는 해제 때 permission과 alias 정리가 필요한 vmalloc allocation에 관계하며, v6.18 vmap()은 이 flag를 허용하지 않습니다. flag 이름만 보고 두 API에 같은 해제 규칙을 적용하면 안 됩니다.
VM_FLUSH_RESET_PERMS의 현재 해제 경로는 direct-map alias를 먼저 invalid 상태로 두고, 해당 범위를 포함해 _vm_unmap_aliases()로 TLB 정리를 수행한 뒤 default permission을 되돌립니다. flush 직후 옛 속성의 alias가 다시 cache되는 것을 피하려는 순서입니다. direct map을 영구히 없애는 flag도, 모든 free에서 무조건 같은 전역 flush를 하라는 flag도 아닙니다. v6.18 vm_reset_perms()와 vfree()가 이 순서를 보여 줍니다.
guard는 backing page가 아닙니다
주소 예약의 중심인 __get_vm_area_node()는 요청을 적절한 단위로 반올림하고, metadata를 준비한 뒤 주소 allocator를 호출합니다. 일반적인 allocation에는 끝부분의 guard page를 위한 virtual space도 추가합니다. 이 guard에는 buffer용 physical page를 할당하지 않습니다.
따라서 vm_struct.size와 caller가 사용할 수 있는 data 길이는 같지 않을 수 있습니다. get_vm_area_size()는 guard가 있는 경우 그 크기를 빼고 backing할 길이를 돌려줍니다. 배열 길이 계산이나 mapping 끝주소 계산에서 guard까지 data로 취급하면 보호 구간을 스스로 없애게 됩니다.
guard는 흔한 끝 경계 침범을 fault로 드러내는 장치이지, 모든 잘못된 pointer 연산을 포착하는 장치는 아닙니다. 한 page를 건너뛰는 잘못된 접근이나 allocation 내부의 범위 오류까지 막지는 못합니다. VM_NO_GUARD라는 내부 선택지가 존재해도 일반적인 호출자가 이를 마음대로 생략해도 된다는 뜻은 아닙니다. 현재 vmap()은 이 flag를 경고하고 지웁니다. 한 allocation의 top guard가 이웃 allocation의 bottom 쪽 보호에도 영향을 주기 때문입니다.
CONFIG_VMAP_STACK의 kernel stack이 대표적인 사용처입니다. stack overflow가 이웃 객체를 조용히 훼손하기 전에 guard fault로 드러날 수 있고 architecture의 fault 처리에서 stack overflow를 구별할 수 있습니다. 그렇다고 kernel-mode stack overflow가 복구 가능한 정상 user fault로 바뀌거나 모든 overflow가 탐지되는 것은 아닙니다.
metadata allocation 자체도 실패할 수 있습니다. 주소를 찾지 못했다면 아직 page-table mapping이 없어도 vm_struct를 반환해야 합니다. 반대로 주소 확보 뒤에는 interval을 사용 중인 구조에 게시하고 객체들을 연결해야 합니다. 이 순서가 reservation과 backing allocation 사이의 경계를 만듭니다.
아래 전체 함수에서는 alignment overflow로 size가 0이 되는 검사, I/O remap용 alignment 조정, metadata allocation 실패, guard 추가와 KASAN 분기를 확인할 수 있습니다. v6.18은 flags·caller·requested size를 먼저 vm_struct에 적고 그 pointer를 alloc_vmap_area()에 전달합니다. 후자가 주소·크기를 결합한 다음 busy 구조에 게시합니다. 책의 setup_vmalloc_vm_locked()를 global lock 아래 호출하는 순서와 다릅니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 1993 Linus Torvalds
* Support of BIGMEM added by Gerhard Wichert, Siemens AG, July 1999
* SMP-safe vmalloc/vfree/ioremap, Tigran Aivazian <tigran@veritas.com>, May 2000
* Major rework to support vmap/vunmap, Christoph Hellwig, SGI, August 2002
* Numa awareness, Christoph Lameter, SGI, June 2005
* Improving global KVA allocator, Uladzislau Rezki, Sony, May 2019
*/
/*
* Linux v6.18, mm/vmalloc.c:3164-3212
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: __get_vm_area_node
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
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;
}
/*
* Mark pages for non-VM_ALLOC mappings as accessible. Do it now as a
* best-effort approach, as they can be mapped outside of vmalloc code.
* For VM_ALLOC mappings, the pages are marked as accessible after
* getting mapped in __vmalloc_node_range().
* With hardware tag-based KASAN, marking is skipped for
* non-VM_ALLOC mappings, see __kasan_unpoison_vmalloc().
*/
if (!(flags & VM_ALLOC))
area->addr = kasan_unpoison_vmalloc(area->addr, requested_size,
KASAN_VMALLOC_PROT_NORMAL);
return area;
}
현재 별도로 존재하는 setup_vmalloc_vm()은 아래처럼 field를 결합하는 작은 helper입니다. 이 함수 자체가 lock을 획득하지는 않습니다. 같은 이름이 남아 있다는 이유로 위 현재 reservation 경로가 이것을 호출한다고 연결하면 안 됩니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 1993 Linus Torvalds
* Support of BIGMEM added by Gerhard Wichert, Siemens AG, July 1999
* SMP-safe vmalloc/vfree/ioremap, Tigran Aivazian <tigran@veritas.com>, May 2000
* Major rework to support vmap/vunmap, Christoph Hellwig, SGI, August 2002
* Numa awareness, Christoph Lameter, SGI, June 2005
* Improving global KVA allocator, Uladzislau Rezki, Sony, May 2019
*/
/*
* Linux v6.18, mm/vmalloc.c:1995-2003
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: setup_vmalloc_vm
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
static inline void setup_vmalloc_vm(struct vm_struct *vm,
struct vmap_area *va, unsigned long flags, const void *caller)
{
vm->flags = flags;
vm->addr = (void *)va->va_start;
vm->size = vm->requested_size = va_size(va);
vm->caller = caller;
va->vm = vm;
}
가장 낮은 적합 구간을 찾는 방법
빈 주소들을 매 요청마다 처음부터 훑으면 비용이 큽니다. free interval을 red-black tree에 저장하고 각 subtree에서 가능한 최대 길이를 요약하면, 요청보다 작은 hole만 있는 subtree를 건너뛸 수 있습니다. 주소순 list는 이웃을 찾거나 범위를 합치는 데 도움이 됩니다. tree의 균형과 augmentation은 서로 다른 불변식입니다. 회전 뒤에도 요약 길이가 맞아야 다음 검색이 유효합니다.
단순한 길이 비교만으로는 부족합니다. hole의 시작을 alignment에 맞춰 올린 뒤에도 요청이 들어가야 합니다. 길이가 같은 두 hole이라도 시작 주소의 나머지에 따라 결과가 달라집니다. find_vmap_lowest_match()는 subtree의 크기 요약으로 후보를 줄이고, is_within_this_va()와 뒤따르는 allocation 단계에서 alignment 및 bound를 검사합니다. 낮은 주소부터 맞는 후보를 찾되, 막힌 가지에서는 아직 확인하지 않은 오른쪽 subtree로 되돌아가야 합니다.
큰 alignment에서는 최악의 앞쪽 padding까지 고려하여 검색 크기를 늘리는 방법을 사용합니다. 하지만 align <= PAGE_SIZE일 때는 page-aligned hole에 추가 이득이 없으며, 요청이 정확히 지정한 [vstart, vend) 전체를 차지하는 경우까지 보수적인 padding을 더하면 가능한 요청도 놓칠 수 있습니다. v6.18 __alloc_vmap_area()에는 이 예외가 명시되어 있습니다. 마지막으로 선택된 시작과 크기의 합이 wrap하지 않는지, 실제 끝이 vend를 넘지 않는지도 별도로 확인합니다.
현재 구현은 vmap node별 busy 관리와 재사용 가능한 pool 등의 최적화도 갖습니다. global free tree에 대한 설명은 여전히 그 경로를 이해하는 데 유용하지만, 모든 작은 allocation이 항상 같은 검색 비용을 치른다는 성능 모델로 확대하면 부정확합니다.
hole을 사용한 뒤 남는 네 가지 모양
하나의 free interval 안에 요청을 놓으면 전체를 쓰거나, 왼쪽 끝을 쓰거나, 오른쪽 끝을 쓰거나, 가운데를 씁니다. classify_va_fit_type()의 FL_FIT_TYPE, LE_FIT_TYPE, RE_FIT_TYPE, NE_FIT_TYPE이 각각 이 경우입니다. 범위에 들어가지 않는 NOTHING_FIT은 allocation 성공 유형이 아닙니다.

그림은 하나의 free interval을 잘라 사용하는 경우만 보여 줍니다. 동시 allocation의 lock, metadata 확보와 tree 회전은 생략했습니다.
왼쪽을 사용했다면 남은 interval의 시작이 오른쪽으로 이동합니다. 오른쪽을 사용했다면 남은 interval의 끝이 왼쪽으로 이동합니다. 전체를 사용하면 free node를 제거합니다. 가운데를 사용하면 왼쪽과 오른쪽 두 free interval이 남으므로 추가 metadata가 필요합니다. 이 산술은 이름보다 그림의 남는 부분을 기준으로 확인하는 편이 안전합니다.
추가 node가 필요할 수 있기 때문에 free-tree lock 아래에서 무제한 sleepable allocation을 할 수는 없습니다. preload한 metadata를 이용하거나 제한된 allocation을 시도하고, 확보하지 못하면 요청을 실패시킬 수 있습니다. 변경 뒤에는 subtree 최대 길이를 다시 전파해야 합니다. busy tree에 새 interval을 삽입할 때에는 새 끝이 기존 시작 이하이면 왼쪽, 새 시작이 기존 끝 이상이면 오른쪽으로 갑니다. 그 외에는 overlap입니다. 끝점이 같은 인접 interval은 겹치지 않는 half-open 범위입니다.
실패 전에 재사용할 수 있는 주소를 회수합니다
주소를 해제했다고 해서 그 순간부터 아무 CPU에서나 같은 주소를 안전하게 재사용할 수 있는 것은 아닙니다. 이전 mapping의 TLB entry가 남아 있을 수 있으므로 lazy unmap된 범위와 즉시 재사용 가능한 free range를 구분해야 합니다. allocator가 공간 부족을 만났을 때 lazy 영역을 purge하고 다시 찾는 이유입니다. reclaim notifier를 호출하여 다른 이용자가 주소를 돌려주도록 기회를 주는 경로도 있습니다.
lazy threshold의 단위도 중요합니다. lazy_max_pages()는 free-list node의 개수가 아니라 지연 해제된 page 수와 비교하는 기준입니다. 큰 interval 하나와 작은 interval 여러 개를 같은 'entry 한 개'로 세면 reclaim 시점을 잘못 설명하게 됩니다. vmap_notify_list는 blocking notifier이며 일반 slab shrinker와 동일한 등록 인터페이스는 아닙니다. 이용자는 register_vmap_purge_notifier()와 unregister_vmap_purge_notifier()로 이 chain의 callback을 등록·해제합니다. allocator는 0으로 준비한 freed의 주소를 callback에 전달합니다. callback이 이 출력 변수를 갱신하면 allocator는 freed > 0인지 보고 retry 여부를 정합니다. chain 호출의 정수 반환값과 freed는 서로 다른 값입니다.
반대로 retry를 했다는 사실만으로 allocation이 성공하지는 않습니다. 필요한 연속 interval이 없거나 alignment와 지정 범위를 만족하지 못하거나 metadata가 부족하면 실패합니다. 전체 vmalloc 영역이 크더라도 특정 요청의 허용 구간은 작을 수 있습니다. x86-64 기본 layout의 4-level 약 32 TiB, 5-level 약 12.5 PiB라는 예약 규모는 실제 RAM, 모든 config의 유효 data 공간, allocation 성공률 중 어느 것도 보장하지 않습니다. KMSAN 같은 configuration은 그 안의 사용 방식도 바꿉니다.
이제 확보된 것은 buffer의 주소입니다. backing page가 모두 준비되고 page table에 연결되기 전까지, 그 주소를 정상적인 data pointer로 외부에 공개해서는 안 됩니다. 이 경계를 지키면 vmalloc 실패도 하나의 막연한 out-of-memory가 아니라 어느 자원을 확보하지 못했는지로 나누어 읽을 수 있습니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 3의 vmalloc 주소 예약과 free-area 검색 설명.
- Linux v6.18 mm/vmalloc.c: 주소 예약, fit 분류와 retry 구현
- Linux v6.18 include/linux/vmalloc.h: vm_struct와 VM flags
- Linux v6.18 x86-64 memory layout