Memory · Linux v6.6 · source + diagram note

vmalloc

Linux v6.6의 vmalloc 내부 page 할당과 vmap의 호출자 page 매핑을 나누어 읽습니다. 해제 함수와 page 소유권은 flags와 성공 여부에 따라 달라집니다.

전체 흐름

vmalloc 계열은 필요한 backing page를 내부에서 확보해 연속된 커널 가상 주소에 매핑한다. vmap은 호출자가 이미 가진 struct page * 배열을 연속 가상 주소에 매핑한다. 둘 다 물리 주소가 연속이라는 보장은 없으며, 한 API가 다른 공개 API를 차례로 부르는 흐름도 아니다.

두 생성 경로

__vmalloc_node_range → __get_vm_area_node → __vmalloc_area_node와 vmap(pages, count, flags, prot)를 별도 레인으로 읽는다. 전자는 page 확보, 후자는 호출자 page 사용이 핵심 차이다.

소유권과 해제

기본 vmap은 vunmap으로 매핑을 해제하고 page는 호출자가 관리한다. 성공한 VM_MAP_PUT_PAGES 경로는 배열과 각 page 참조 하나를 넘겨 vfree로 해제한다.

그림 1. vmalloc·vmap·해제 경로와 page 소유권 (원본 도식)
vmalloc은 VA 예약 후 내부에서 backing page를 할당하고 매핑한다. 별개 API vmap은 호출자 제공 page를 매핑한다. 기본 vmap은 vunmap 후 page를 호출자가 보유하고, VM_MAP_PUT_PAGES를 준 성공한 vmap은 배열과 각 page 참조 하나를 이전해 vfree로 해제한다.

화살표는 코드상 생략된 helper를 포함한 요약 경로이며 공개 vmap()이 vmalloc()의 다음 호출이라는 뜻이 아니다. 근거: vmalloc.c:2879–2933, 3115–3140, 3279–3322.

소스 코드 위치

고정 기준: Linux v6.6

로컬 mm/vmalloc.c 원문과 행별 보기에서 예약·매핑·해제를 대조한다. 기존의 mm/vmalloc.c, include/linux/vmalloc.h, mm/ioremap.c 링크도 유지한다.

이 글은 고정된 v6.6 mm/vmalloc.c의 조건을 설명한다. ioremap의 cache attribute와 아키텍처별 TLB 처리 전체를 이 파일만으로 일반화하지 않는다.

대표 코드

__vmalloc_node_range: VM_ALLOC 영역과 내부 할당 호출

원본 코드: mm/vmalloc.c:3279–3322 (로컬 행별 보기) · v6.6 원본 링크
again:
	area = __get_vm_area_node(real_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",
			real_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;

__get_vm_area_node()로 VA 영역을 예약하고 VM_ALLOC을 붙인다. 뒤에서 __vmalloc_area_node()를 호출해 물리 페이지 확보와 매핑을 수행한다. 이 발췌에는 vmap() 호출이 없다.

__vmalloc_area_node: 페이지 배열과 backing page 확보

원본 코드: mm/vmalloc.c:3115–3140 (로컬 행별 보기) · v6.6 원본 링크
	array_size = (unsigned long)nr_small_pages * sizeof(struct page *);

	if (!(gfp_mask & (GFP_DMA | GFP_DMA32)))
		gfp_mask |= __GFP_HIGHMEM;

	/* Please note that the recursion is strictly bounded. */
	if (array_size > PAGE_SIZE) {
		area->pages = __vmalloc_node(array_size, 1, nested_gfp, node,
					area->caller);
	} else {
		area->pages = kmalloc_node(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);
		free_vm_area(area);
		return NULL;
	}

	set_vm_area_page_order(area, page_shift - PAGE_SHIFT);
	page_order = vm_area_page_order(area);

	area->nr_pages = vm_area_alloc_pages(gfp_mask | __GFP_NOWARN,
		node, page_order, nr_small_pages, area->pages);

내부에서 페이지 포인터 배열을 마련한 뒤 vm_area_alloc_pages()로 backing page를 채운다. 같은 함수의 3181–3184행은 그 배열을 vmap_pages_range()에 넘긴다. 이 매핑 helper와 별도 공개 API vmap()을 혼동하지 않는다.

vmap: 호출자가 준 page 배열의 매핑

원본 코드: mm/vmalloc.c:2879–2933 (로컬 행별 보기) · v6.6 원본 링크
/**
 * vmap - map an array of pages into virtually contiguous space
 * @pages: array of page pointers
 * @count: number of pages to map
 * @flags: vm_area->flags
 * @prot: page protection for the mapping
 *
 * Maps @count pages from @pages into contiguous kernel virtual space.
 * If @flags contains %VM_MAP_PUT_PAGES the ownership of the pages array itself
 * (which must be kmalloc or vmalloc memory) and one reference per pages in it
 * are transferred from the caller to vmap(), and will be freed / dropped when
 * vfree() is called on the return value.
 *
 * Return: the address of the area or %NULL on failure
 */
void *vmap(struct page **pages, unsigned int count,
	   unsigned long flags, pgprot_t prot)
{
	struct vm_struct *area;
	unsigned long addr;
	unsigned long size;		/* In bytes */

	might_sleep();

	if (WARN_ON_ONCE(flags & VM_FLUSH_RESET_PERMS))
		return NULL;

	/*
	 * Your top guard is someone else's bottom guard. Not having a top
	 * guard compromises someone else's mappings too.
	 */
	if (WARN_ON_ONCE(flags & VM_NO_GUARD))
		flags &= ~VM_NO_GUARD;

	if (count > totalram_pages())
		return NULL;

	size = (unsigned long)count << PAGE_SHIFT;
	area = get_vm_area_caller(size, flags, __builtin_return_address(0));
	if (!area)
		return NULL;

	addr = (unsigned long)area->addr;
	if (vmap_pages_range(addr, addr + size, pgprot_nx(prot),
				pages, PAGE_SHIFT) < 0) {
		vunmap(area->addr);
		return NULL;
	}

	if (flags & VM_MAP_PUT_PAGES) {
		area->pages = pages;
		area->nr_pages = count;
	}
	return area->addr;
}

pages는 호출자가 전달한다. vmap()은 VA 영역을 확보하고 그 페이지를 매핑하지만 이 함수 안에서 새 backing page를 할당하지 않는다. VM_MAP_PUT_PAGES가 있는 성공 경로에서만 kmalloc/vmalloc 메모리로 만든 배열 자체와 각 page의 참조 하나가 넘어간다.

vfree: vmalloc 소유 페이지 또는 이전된 참조 해제

원본 코드: mm/vmalloc.c:2821–2848 (로컬 행별 보기) · v6.6 원본 링크
	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);
	for (i = 0; i < vm->nr_pages; i++) {
		struct page *page = vm->pages[i];

		BUG_ON(!page);
		mod_memcg_page_state(page, MEMCG_VMALLOC, -1);
		/*
		 * High-order allocs for huge vmallocs are split, so
		 * can be freed as an array of order-0 allocations
		 */
		__free_page(page);
		cond_resched();
	}
	atomic_long_sub(vm->nr_pages, &nr_vmalloc_pages);
	kvfree(vm->pages);
	kfree(vm);
}

remove_vm_area()로 매핑을 제거한 다음 기록된 page마다 __free_page()를 호출하고 배열을 해제한다. 실제 물리 page의 최종 해제는 참조가 남아 있는지에 따른다. VM_MAP_PUT_PAGES로 성공한 vmap 결과에도 이 해제 API가 지정된다.

vunmap: 보통의 vmap 매핑만 해제

원본 코드: mm/vmalloc.c:2860–2875 (로컬 행별 보기) · v6.6 원본 링크
void vunmap(const void *addr)
{
	struct vm_struct *vm;

	BUG_ON(in_interrupt());
	might_sleep();

	if (!addr)
		return;
	vm = remove_vm_area(addr);
	if (unlikely(!vm)) {
		WARN(1, KERN_ERR "Trying to vunmap() nonexistent vm area (%p)\n",
				addr);
		return;
	}
	kfree(vm);

vunmap()은 가상 영역을 제거하고 vm_struct를 해제한다. 이 경로에는 page별 __free_page()가 없다. 기본 vmap 호출자의 page 배열·참조는 호출자 책임으로 남는다.

함수별 분석

1. vmalloc 계열의 내부 할당

__vmalloc_node_range()는 __get_vm_area_node()로 VM_ALLOC 영역을 확보한 뒤 __vmalloc_area_node()를 호출한다. 후자는 page 포인터 배열을 만들고 vm_area_alloc_pages()로 page를 확보한 다음 vmap_pages_range()에 전달한다. 중간 helper를 생략한 요약이며 공개 vmap() 호출 체인이 아니다.

2. 호출자 page의 vmap

vmap()은 전달받은 pages를 vmap_pages_range()로 매핑한다. VM_MAP_PUT_PAGES가 설정된 성공 경로만 area->pages와 nr_pages를 기록한다. 실패하면 NULL을 돌려주며 성공한 소유권 이전으로 취급하지 않는다.

3. 나중의 해제

vfree()와 vunmap()은 모두 remove_vm_area()를 거치지만 page 처리 범위는 다르다. 기본 vmap 결과는 vunmap() 뒤에도 호출자 page 소유권이 유지된다. vmalloc 및 성공한 VM_MAP_PUT_PAGES 경로는 vfree()가 기록된 page 참조와 배열을 정리한다.

구조체와 필드

가상 영역 기록과 물리 page 소유권을 한 화살표로 묶지 않는다. 같은 연속 VA라도 누가 page를 마련했는지와 해제 책임이 다르다.

객체/필드역할확인 지점
vmap_area충돌하지 않는 VA 구간의 관리 noderemove_vm_area
vm_struct주소·flags·page 배열 등을 기록하는 영역 descriptorvmap area
area->pagesvmalloc 내부 확보 page 또는 조건부 이전된 vmap 배열vmap flags / vmalloc allocation
VM_MAP_PUT_PAGES성공한 vmap에서 배열 및 각 page 참조 하나를 넘기는 조건API 계약

실행 단계와 상태 변화

생성·매핑·해제는 API별로 나누어 비교한다. vfree가 모든 vmap 결과의 일반적인 해제 함수는 아니다.

그림 2. 연속 VA 슬롯과 떨어진 물리 page의 대응
vmalloc이 내부에서 확보했거나 vmap 호출자가 제공한 page 배열을 매핑 helper에 전달한다. 결과 커널 가상 주소의 네 슬롯은 연속이지만 예시 물리 PFN 41, 205, 812, 909는 떨어져 있다.

PFN 숫자는 관계를 설명하기 위한 예시이며 실행 결과가 아니다. 기본 page 단위의 개념도이고 huge vmalloc 선택이나 물리 page의 실제 배열을 단정하지 않는다. 근거: vmap():2916–2932, __vmalloc_area_node():3139–3140와 3181–3183.

기본 가드 페이지: 예약한 VA이지만 사용 페이지로 매핑하지 않음
보통의 vmalloc(size)

__vmalloc_node_range()에 추가 VM_NO_GUARD 플래그를 넘기지 않는다. 따라서 __get_vm_area_node()가 요청 크기 외에 가드 한 페이지를 예약한다. 예약:2592–2595 · 기본 진입:3382–3386

vmap(pages[], ...)

VM_NO_GUARD 요청은 경고 후 제거하므로 성공한 vmap()도 기본 가드 예약을 유지한다. 실제 페이지 매핑은 count로 계산한 사용 크기에만 한다. 플래그:2906–2911 · 사용 크기:2916–2923

위 막대는 조건을 설명하는 개념도다. __vmalloc_node_range()의 다른 호출자가 VM_NO_GUARD를 명시하면 예약 크기가 달라질 수 있다. 분기 근거:2592–2595

경로backing page 마련연속 VA 매핑정상 해제/소유권
vmalloc 계열내부 vm_area_alloc_pages내부 vmap_pages_rangevfree가 매핑·기록된 page 참조·배열을 정리
기본 vmap호출자가 미리 제공vmap_pages_rangevunmap은 매핑만 정리; page/배열은 호출자 책임
vmap + VM_MAP_PUT_PAGES호출자가 미리 제공vmap_pages_range성공 시 배열·각 page 참조 하나 이전; vfree로 정리

remove_vm_area()는 free_unmap_vmap_area()를 호출한다. 이 경로는 mapping 제거와 lazy VA 처리에 관여하지만, 모든 아키텍처에서 같은 순간에 모든 CPU TLB가 비워진다고 단정하지 않는다.

불변 조건과 실패 사례

VA 연속 ≠ phys_contiguous

vmalloc/vmap이 연속 가상 주소를 반환해도 물리 page 연속성을 보장하지 않는다. DMA 등 물리 연속성이 필요한 사용자는 별도 조건을 확인해야 한다.

page lifetime

기본 vmap 호출자는 매핑이 살아 있는 동안 page를 유효하게 유지해야 한다. VM_MAP_PUT_PAGES는 성공 시 배열과 참조 하나를 이전하므로 그 후 호출자가 중복 해제하지 않는다.

해제 문맥

vunmap()은 interrupt context에서 호출하면 안 된다(2864행). vfree()는 NMI가 아닌 interrupt context에서 vfree_atomic()으로 넘겨 deferred 해제를 요청하지만, 두 함수 모두 NMI에서는 호출하면 안 된다(2777행 · 2812–2817행).

cache/TLB

vmalloc과 ioremap의 사용 목적 및 protection은 다를 수 있다. 이 글은 구체적 cache attribute나 TLB 완료 시점을 전 아키텍처에 대해 약속하지 않는다.

계측과 검증

영역이 보이는지와 backing page 소유권을 별도로 확인한다. 관찰 가능한 주소 범위만으로 물리 연속성이나 page의 최종 해제 완료를 추론하지 않는다.

관찰 수단알 수 있는 것한계
cat /proc/vmallocinfo현재 기록된 vmalloc/vmap 영역과 호출자 정보각 page의 물리 연속성 증거는 아님
trace_alloc_vmap_area / trace_free_vmap_area_noflushVA 예약과 lazy 해제 지점trace 설정 의존; page 참조의 최종 해제와 구별
page-table dump해당 VA의 mapping 속성아키텍처·debug 설정 의존
TLB 성능 카운터워크로드 중 miss 변화단일 vmalloc 호출의 인과관계를 직접 증명하지 않음

추가 확인 사항

  • 호출자가 준 page 배열이 VM_MAP_PUT_PAGES 조건을 만족하며 성공했는가?
  • 현재 주소는 vmalloc 내부 page인가, 기본 vmap의 caller-owned page인가?
  • 해제 뒤 여전히 남은 page 참조는 누가 보유하며, mapping 제거 시점과 최종 free 시점은 어떻게 구분할 것인가?