page 배열을 모두 확보했는데 mapping이 실패했다면 무엇을 되돌려야 할까요? 아직 연결하지 않은 page만 반환하면 될 것 같지만, 앞쪽 주소에는 이미 PTE가 설치됐을 수 있습니다. 상위 page-table page도 새로 만들어졌을 수 있습니다. mapping helper의 negative 반환값은 아무 상태도 바꾸지 않았다는 뜻이 아닙니다.
여기서는 upstream Linux v6.18의 vmap_pages_range()부터 PTE 설치까지를 읽습니다. code는 mm/vmalloc.c의 GPL-2.0-only 전체 함수이며 kernel 의존성이 있습니다. 정의별 출처 기록, GPL-2.0 text를 함께 제공합니다. standalone 실행이나 kernel 실측은 하지 않았습니다.
배열 원소는 언제나 base page입니다
입력 범위는 [addr, end)입니다. pages의 원소는 큰 leaf 한 개의 목록이 아니라 PAGE_SIZE 단위의 struct page *입니다. page_shift는 허용하는 mapping 크기의 상한과 backing의 alignment·연속성 계약에 관계합니다. 큰 shift를 숫자로 넘겼다고 임의로 흩어진 page들이 연속된 physical block으로 바뀌지 않습니다.
가장 바깥의 vmap_pages_range()는 하위 결과를 받은 뒤 flush_cache_vmap()을 호출하고 그 결과를 반환합니다. 하위가 실패해도 이 호출을 건너뛰지 않는 구조입니다. 이 cache hook과 TLB invalidation은 같은 동작이 아니며 architecture별 cache 유지 계약을 따라야 합니다.
// 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:696-704
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: vmap_pages_range
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
int vmap_pages_range(unsigned long addr, unsigned long end,
pgprot_t prot, struct page **pages, unsigned int page_shift)
{
int err;
err = vmap_pages_range_noflush(addr, end, prot, pages, page_shift);
flush_cache_vmap(addr, end);
return err;
}
v6.18의 vmap_pages_range_noflush()에는 KMSAN hook이 먼저 있습니다. hook이 실패하면 바로 반환하고, 성공한 경우 실제 mapper인 __vmap_pages_range_noflush()로 갑니다. 책의 예전 같은 이름 함수가 큰-page 분기를 직접 담았던 구조와 구별해야 합니다.
// 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:673-682
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: vmap_pages_range_noflush
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
int vmap_pages_range_noflush(unsigned long addr, unsigned long end,
pgprot_t prot, struct page **pages, unsigned int page_shift)
{
int ret = kmsan_vmap_pages_range_noflush(addr, end, prot, pages,
page_shift);
if (ret)
return ret;
return __vmap_pages_range_noflush(addr, end, prot, pages, page_shift);
}
큰 mapping은 배열을 건너뛰어 진행합니다
큰 mapping을 사용할 수 없거나 shift가 base-page이면 작은-page walker로 갑니다. 큰 mapping 경로의 배열 index는 1U << (page_shift - PAGE_SHIFT)씩 증가합니다. 4 KiB·2 MiB 관계라면 한 번에 512개 원소를 대표하는 physical 구간을 처리합니다. virtual cursor는 별도로 1UL << page_shift씩 증가합니다. byte 좌표와 배열 index를 같은 stride로 다뤄서는 안 됩니다.
현재 code는 첫 page의 physical 주소를 page_to_phys(pages[i])로 얻습니다. 책의 __pa(page_address(pages[i])) 표현을 현재 code인 것처럼 옮기지 않습니다. page_address()에서 일반 direct-map pointer를 얻을 수 있는지의 조건과, 이미 가진 page의 physical 주소를 구하는 작업은 다르기 때문입니다. 각 큰 덩어리의 내부 page들이 실제 연속이라는 계약은 caller가 충족해야 합니다.
// 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:647-671
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: __vmap_pages_range_noflush
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
int __vmap_pages_range_noflush(unsigned long addr, unsigned long end,
pgprot_t prot, struct page **pages, unsigned int page_shift)
{
unsigned int i, nr = (end - addr) >> PAGE_SHIFT;
WARN_ON(page_shift < PAGE_SHIFT);
if (!IS_ENABLED(CONFIG_HAVE_ARCH_HUGE_VMALLOC) ||
page_shift == PAGE_SHIFT)
return vmap_small_pages_range_noflush(addr, end, prot, pages);
for (i = 0; i < nr; i += 1U << (page_shift - PAGE_SHIFT)) {
int err;
err = vmap_range_noflush(addr, addr + (1UL << page_shift),
page_to_phys(pages[i]), prot,
page_shift);
if (err)
return err;
addr += 1UL << page_shift;
}
return 0;
}
여기서 부르는 vmap_range_noflush()의 architecture별 큰-leaf 구현은 이 작은-page 함수들의 이름을 바꾼 것이 아닙니다. 큰-page 실패에도 앞에서 성공한 범위가 남을 수 있다는 원칙은 같습니다.
PGD 경계는 요청 끝을 넘지 않습니다
small-page 경로는 init_mm의 kernel root에서 시작하고 pgd_addr_end()로 현재 entry가 맡을 부분만 잘라 내려갑니다. 누적 nr은 다음에 사용할 page 배열 index이고 mask는 어느 page-table level이 수정됐는지 나타냅니다. 두 값 모두 child 호출 사이에 이어집니다. PGD가 bad이면 그 변경 가능성도 mask에 기록합니다.
v6.18에서는 child 실패 시 loop를 break한 뒤에도 mask와 ARCH_PAGE_TABLE_SYNC_MASK를 검사합니다. 앞쪽에서 상위 entry를 만들었을 수 있으므로 반환 전에 필요한 kernel-root 동기화를 수행합니다. 책의 예전 listing처럼 error 자리에서 바로 return하는 형태와 다릅니다. 실패를 무조건 '변경 없음'으로 취급해서는 안 되는 구체적인 이유입니다.
vmap_small_pages_range_noflush.c
// 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:611-636
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: vmap_small_pages_range_noflush
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
static int vmap_small_pages_range_noflush(unsigned long addr, unsigned long end,
pgprot_t prot, struct page **pages)
{
unsigned long start = addr;
pgd_t *pgd;
unsigned long next;
int err = 0;
int nr = 0;
pgtbl_mod_mask mask = 0;
BUG_ON(addr >= end);
pgd = pgd_offset_k(addr);
do {
next = pgd_addr_end(addr, end);
if (pgd_bad(*pgd))
mask |= PGTBL_PGD_MODIFIED;
err = vmap_pages_p4d_range(pgd, addr, next, prot, pages, &nr, &mask);
if (err)
break;
} while (pgd++, addr = next, addr != end);
if (mask & ARCH_PAGE_TABLE_SYNC_MASK)
arch_sync_kernel_mappings(start, end);
return err;
}
x86도 이 동기화가 항상 불필요한 architecture가 아닙니다. v6.18의 pgtable_64_types.h와 init_64.c에 mask·arch_sync_kernel_mappings() 경로가 있습니다. 초기 공유 table이 있다는 일반 설명만으로 runtime 상위 변경의 처리를 제거할 수 없습니다.
중간 level은 다음 table을 준비합니다
P4D·PUD·PMD helper는 각각 p4d_alloc_track()·pud_alloc_track()·pmd_alloc_track()으로 필요한 child table을 준비합니다. 실패하면 -ENOMEM입니다. 성공하면 해당 level의 addr_end()로 경계를 나누고 다음 level에 같은 pages, 누적 nr과 mask를 전달합니다. folded level에서는 새로운 물리 table 하나를 반드시 allocation하는 것이 아닙니다.
// 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:593-609
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: vmap_pages_p4d_range
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
static int vmap_pages_p4d_range(pgd_t *pgd, unsigned long addr,
unsigned long end, pgprot_t prot, struct page **pages, int *nr,
pgtbl_mod_mask *mask)
{
p4d_t *p4d;
unsigned long next;
p4d = p4d_alloc_track(&init_mm, pgd, addr, mask);
if (!p4d)
return -ENOMEM;
do {
next = p4d_addr_end(addr, end);
if (vmap_pages_pud_range(p4d, addr, next, prot, pages, nr, mask))
return -ENOMEM;
} while (p4d++, addr = next, addr != end);
return 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:575-591
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: vmap_pages_pud_range
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
static int vmap_pages_pud_range(p4d_t *p4d, unsigned long addr,
unsigned long end, pgprot_t prot, struct page **pages, int *nr,
pgtbl_mod_mask *mask)
{
pud_t *pud;
unsigned long next;
pud = pud_alloc_track(&init_mm, p4d, addr, mask);
if (!pud)
return -ENOMEM;
do {
next = pud_addr_end(addr, end);
if (vmap_pages_pmd_range(pud, addr, next, prot, pages, nr, mask))
return -ENOMEM;
} while (pud++, addr = next, addr != end);
return 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:557-573
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: vmap_pages_pmd_range
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
static int vmap_pages_pmd_range(pud_t *pud, unsigned long addr,
unsigned long end, pgprot_t prot, struct page **pages, int *nr,
pgtbl_mod_mask *mask)
{
pmd_t *pmd;
unsigned long next;
pmd = pmd_alloc_track(&init_mm, pud, addr, mask);
if (!pmd)
return -ENOMEM;
do {
next = pmd_addr_end(addr, end);
if (vmap_pages_pte_range(pmd, addr, next, prot, pages, nr, mask))
return -ENOMEM;
} while (pmd++, addr = next, addr != end);
return 0;
}
여기에는 error 전달의 손실도 있습니다. 이 함수들은 child의 nonzero 반환을 모두 -ENOMEM으로 바꿉니다. 따라서 맨 아래에서 -EBUSY나 -EINVAL이 나왔더라도 상위 caller가 그 값을 그대로 받는다고 설명하면 틀립니다. PTE helper의 국소적 원인과 외부로 전달되는 error를 분리해 보아야 합니다.
PTE 설치는 완료한 만큼만 진행합니다
PTE helper는 먼저 kernel용 PTE table을 allocation·tracking합니다. 그 뒤 lazy MMU mode에 들어가 각 slot을 확인합니다. 기존 PTE가 비어 있지 않으면 -EBUSY, 배열 원소가 NULL이면 -ENOMEM, PFN이 유효하지 않으면 -EINVAL로 loop를 중단합니다. 이미 사용 중인 entry를 덮어쓰기 위한 API가 아닙니다.
유효한 page에서는 mk_pte(page, prot)로 값을 만들고 set_pte_at()으로 설치한 다음 nr을 늘립니다. 실패한 원소까지 성공 수에 포함하지 않습니다. loop를 빠져나온 뒤에는 lazy MMU mode를 종료하고 PTE modification bit를 기록합니다. table allocation 자체가 실패한 앞쪽 return과, 일부 mapping 이후 validation이 실패한 break는 정리할 상태가 다릅니다.
// 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:513-555
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmalloc.c
* Complete function: vmap_pages_pte_range
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
static int vmap_pages_pte_range(pmd_t *pmd, unsigned long addr,
unsigned long end, pgprot_t prot, struct page **pages, int *nr,
pgtbl_mod_mask *mask)
{
int err = 0;
pte_t *pte;
/*
* nr is a running index into the array which helps higher level
* callers keep track of where we're up to.
*/
pte = pte_alloc_kernel_track(pmd, addr, mask);
if (!pte)
return -ENOMEM;
arch_enter_lazy_mmu_mode();
do {
struct page *page = pages[*nr];
if (WARN_ON(!pte_none(ptep_get(pte)))) {
err = -EBUSY;
break;
}
if (WARN_ON(!page)) {
err = -ENOMEM;
break;
}
if (WARN_ON(!pfn_valid(page_to_pfn(page)))) {
err = -EINVAL;
break;
}
set_pte_at(&init_mm, addr, pte, mk_pte(page, prot));
(*nr)++;
} while (pte++, addr += PAGE_SIZE, addr != end);
arch_leave_lazy_mmu_mode();
*mask |= PGTBL_PTE_MODIFIED;
return err;
}
pfn_valid()는 여기서 page metadata의 적합성을 검사하는 조건이지 어떤 physical 주소든 접근해도 된다는 범용 허가가 아닙니다. 배열 원소 수, object lifetime과 mapping의 소유권도 caller의 계약입니다. 이 helper가 독립적인 page reference나 GUP pin을 추가로 확보해 주는 것은 아닙니다.
rollback은 바깥 소유자가 맡습니다
다음은 구현코드가 아닌 호출관계입니다. 각 화살표는 모든 architecture의 leaf 분기를 뜻하지 않습니다.
__vmalloc_area_node
-> vmap_pages_range
-> vmap_pages_range_noflush
-> __vmap_pages_range_noflush
-> small-page walk or large-range mapper
-> vfree on a terminal mapping failure
walker가 반환했다고 부분 PTE가 자동으로 모두 제거되는 것은 아닙니다. vmalloc allocation의 바깥 소유자는 vm_struct와 실제 확보한 page 수를 갖고 있으며 최종 실패 시 vfree() 경로로 mapping·page·배열·metadata를 정리합니다. 원래 요청에 NOFAIL이 있으면 mapping을 기다렸다 재시도하는 경로도 있으므로, negative 결과를 받는 모든 caller가 즉시 free하는 것은 아닙니다.
새 mapping을 게시하는 동기화, 제거한 translation의 TLB 정리, backing page를 반환하는 시점은 연결되어 있지만 같은 한 동작은 아닙니다. 이 경계를 나누어 읽으면 성공한 주소 수와 page 수, 남아 있는 table 변경을 각각 추적할 수 있습니다. 긴 page-table walk에서 가장 중요한 정보는 함수 이름의 나열보다 이 부분 성공의 소유권입니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 3 §3.5.4.
- Linux v6.18 mm/vmalloc.c: mapping wrapper, range walker, PTE validation.
- Linux v6.18 mm/pgalloc-track.h: allocation과 수정 mask.
- Linux v6.18 x86 init_64.c: kernel mapping 동기화.