mprotect()로 한 VMA의 가운데만 read-only로 바꾸면 내용 page를 통째로 복사하는 것은 아닙니다. 서로 다른 속성의 주소 범위를 표현하려고 VMA metadata를 나눕니다. 여기서 struct 복사는 시작일 뿐이며, list node와 reference를 그대로 공유하면 곧바로 수명 문제가 생깁니다.
split을 요구하는 caller들
protection, memlock, advice와 NUMA policy는 VMA 전체에서 같은 속성으로 표현됩니다. mprotect_fixup(), mlock_fixup(), madvise_update_vma(), mbind_range()는 변경한 부분이 이웃과 합쳐지는지 먼저 보고, 합치지 못하면 필요한 경계를 나눕니다. munmap()도 일부만 제거할 때 split을 사용합니다.
wrapper인 split_vma()는 새 VMA가 개수 한계를 넘는지 검사합니다. 내부 __split_vma()를 부르는 caller는 그 검사를 자신이 책임지는 경우가 있으므로 이름에 밑줄이 붙은 helper를 단순히 더 빠른 동등 API로 취급하면 안 됩니다. filesystem이나 driver의 may_split callback은 자체 state의 제약 때문에 split을 거부할 수도 있습니다.
복사할 field와 새로 만들어야 할 연결
vm_area_dup()는 기본 속성을 복사하지만 anon_vma_chain은 빈 list로 시작해야 합니다. RB-tree 기반 구현의 vm_prev·vm_next도 지우고 새 주소 위치에 연결할 준비를 합니다. RB-tree node는 나중에 올바르게 연결되므로 기존 object와 같은 tree node로 남겨서는 안 됩니다. anonymous name 같은 부속 object도 수명 규칙에 따라 참조를 확보해야 합니다.
new_below는 새 object를 split 주소 아래쪽에 둘지 위쪽에 둘지 정합니다. 아래쪽을 새로 만들면 원래 VMA의 시작이 올라가고, 위쪽을 새로 만들면 새 object의 시작과 pgoff가 offset만큼 올라갑니다. 어느 object가 남는지는 달라도 각 주소가 원래의 backing index를 가리킨다는 불변식은 같습니다.
policy, file, anonymous ancestry를 각각 복제합니다
새 VMA의 NUMA policy를 복제하고, 기존 anonymous ancestry의 AVC를 clone하며, file이 있으면 reference를 추가합니다. driver가 open callback을 제공하면 새 VMA가 생겼다는 사실도 전달합니다. anonymous 연결을 clone한다는 것은 folio data를 복사한다는 뜻이 아닙니다. 새 VMA에서도 기존 folio를 역추적할 수 있도록 연결 metadata를 만드는 작업입니다.
중간 allocation 실패에는 그때까지 획득한 reference와 새 object를 되돌리는 unwind가 필요합니다. policy나 AVC allocation이 실패했는데 file reference만 남기거나, 반대로 아직 얻지 않은 reference를 내리면 안 됩니다. split이 단순한 경계 산술보다 훨씬 많은 실패 지점을 가진 이유입니다.
insertion은 마지막에 일관된 상태를 드러냅니다
RB-tree 기반 구현은 vma_adjust()의 insert 인자를 통해 나머지 경계 변경과 새 VMA 연결을 수행합니다. __insert_vm_struct()는 이전 VMA와 RB parent·link 위치를 구한 뒤 list와 tree에 넣고 map_count를 올립니다. rmap interval index도 새 범위를 반영해야 합니다. 한 index에만 새 VMA를 보이게 해 두고 나머지 준비를 나중에 하는 방식으로 읽어서는 안 됩니다.
v6.18의 구체적인 helper와 tree는 다르지만 split이 보존해야 할 계약은 같습니다. 주소 범위는 겹치지 않아야 하고, offset은 원래 contents를 유지해야 하며, 두 VMA가 각자 소유할 연결과 reference는 분명해야 합니다. 이 계약을 기준으로 읽으면 split과 merge가 같은 prepare·complete machinery를 공유하는 이유도 자연스럽게 보입니다.
복사에 성공해도 새 VMA는 아직 준비되지 않았습니다
고전 vm_area_dup()는 slab object를 얻은 뒤 원 struct를 복사합니다. ASSERT_EXCLUSIVE_WRITER()는 flags와 file에 대한 writer 전제를 확인하는 annotation입니다. data_race(*orig)는 복사 중 shared interval node의 변화를 허용하는 문맥을 표시하며, 그 node를 새 VMA에서 다시 초기화·연결한다는 전제와 함께 읽어야 합니다. 이것이 임의의 field를 동기화 없이 바꾸어도 된다는 허가는 아닙니다.
다음은 Linux v5.18 kernel/fork.c의 두 함수 전체입니다. GPL-2.0-only code이며 copyright notice와 license 원문을 구분해 함께 둡니다. tab 표시 외에 함수를 줄이지 않았습니다.
// SPDX-License-Identifier: GPL-2.0-only
// Linux v5.18, kernel/fork.c
// Copyright (C) 1991, 1992 Linus Torvalds
struct vm_area_struct *vm_area_dup(struct vm_area_struct *orig)
{
struct vm_area_struct *new = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);
if (new) {
ASSERT_EXCLUSIVE_WRITER(orig->vm_flags);
ASSERT_EXCLUSIVE_WRITER(orig->vm_file);
/*
* orig->shared.rb may be modified concurrently, but the clone
* will be reinitialized.
*/
*new = data_race(*orig);
INIT_LIST_HEAD(&new->anon_vma_chain);
new->vm_next = new->vm_prev = NULL;
dup_anon_vma_name(orig, new);
}
return new;
}
void vm_area_free(struct vm_area_struct *vma)
{
free_anon_vma_name(vma);
kmem_cache_free(vm_area_cachep, vma);
}
새 object의 anon_vma_chain은 빈 list이고 next/prev도 NULL입니다. 익명 name에 대해서는 별도의 reference를 확보합니다. 원래 vm_rb와 shared interval node를 복사한 값은 새 object가 그 tree의 구성원이 되었다는 뜻이 아닙니다. 새로운 위치에 연결하는 작업이 필요합니다. vm_area_free()는 anonymous name과 slab object만 정리하므로 file·policy·AVC를 먼저 반납하는 책임은 caller에 남아 있습니다.
고전 split의 전체 성공 경로와 실패 경로
아래는 Linux v5.18 mm/mmap.c의 wrapper와 내부 함수 전체입니다. GPL-2.0-only인 kernel 내부 code이며 standalone C 프로그램은 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
// Linux v5.18, mm/mmap.c. Written by obz.
// Address space accounting code: <alan@lxorguk.ukuu.org.uk>
int split_vma(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long addr, int new_below)
{
if (mm->map_count >= sysctl_max_map_count)
return -ENOMEM;
return __split_vma(mm, vma, addr, new_below);
}
int __split_vma(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long addr, int new_below)
{
struct vm_area_struct *new;
int err;
if (vma->vm_ops && vma->vm_ops->may_split) {
err = vma->vm_ops->may_split(vma, addr);
if (err)
return err;
}
new = vm_area_dup(vma);
if (!new)
return -ENOMEM;
if (new_below)
new->vm_end = addr;
else {
new->vm_start = addr;
new->vm_pgoff += ((addr - vma->vm_start) >> PAGE_SHIFT);
}
err = vma_dup_policy(vma, new);
if (err)
goto out_free_vma;
err = anon_vma_clone(new, vma);
if (err)
goto out_free_mpol;
if (new->vm_file)
get_file(new->vm_file);
if (new->vm_ops && new->vm_ops->open)
new->vm_ops->open(new);
if (new_below)
err = vma_adjust(vma, addr, vma->vm_end, vma->vm_pgoff +
((addr - new->vm_start) >> PAGE_SHIFT), new);
else
err = vma_adjust(vma, vma->vm_start, addr, vma->vm_pgoff, new);
/* Success. */
if (!err)
return 0;
/* Clean everything up if vma_adjust failed. */
if (new->vm_ops && new->vm_ops->close)
new->vm_ops->close(new);
if (new->vm_file)
fput(new->vm_file);
unlink_anon_vmas(new);
out_free_mpol:
mpol_put(vma_policy(new));
out_free_vma:
vm_area_free(new);
return err;
}
may_split()은 allocation보다 먼저 실행하므로 driver가 금지한 위치에서는 새 VMA를 만들지 않습니다. new_below=1이면 새 object가 아래 조각이 되고 원래 object는 위 조각으로 남습니다. 0이면 새 object가 위 조각이 되고 원래 object는 아래 조각으로 남습니다. 이는 contents 복사가 아니라 범위의 소유 object를 정하는 선택입니다.
정책 복제 실패는 새 object만 정리하는 경로로, anon_vma clone 실패는 이미 얻은 policy도 반납하는 경로로 갑니다. file reference와 open callback은 그 둘을 통과한 뒤에야 얻습니다. 이후 vma_adjust()가 실패하면 close callback, file fput, AVC unlink, policy put, VMA free가 필요합니다. 오류 label마다 이미 얻은 자원이 다르므로 마지막 label 하나로 무조건 점프하는 방식으로 줄일 수 없습니다.
anon_vma_clone()는 기존 anon_vma object와의 연결들을 복제합니다. 기존 anonymous page를 전부 새로 복사하거나 anon_vma root를 통째로 복사하는 일이 아닙니다. file-backed private VMA의 CoW ancestry도 보존해야 하므로 file reference만 늘리는 것으로 충분하지 않습니다.
split 주소 양쪽에서 pgoff를 계산합니다
가상 예로 원래 VMA를 VA page [40, 46), 시작 pgoff를 900으로 두고 VA page 42에서 나누겠습니다. new_below가 1이면 새 아래 VMA는 [40, 42)·pgoff 900이며, 원래 위 VMA는 [42, 46)·pgoff 902가 됩니다. 0이면 원래 아래 VMA가 [40, 42)·900이고 새 위 VMA가 [42, 46)·902입니다. 어느 object를 새로 만들었든 같은 VA page가 같은 backing page를 가리킵니다.
첫 경로에서는 아래 조각의 끝만 먼저 설정하고, vma_adjust()에 원래 위 조각의 새 시작과 증가한 pgoff를 전달합니다. 두 번째 경로에서는 새 위 조각의 시작/pgoff를 먼저 조정하고 원래 아래 조각의 끝을 전달합니다. 이 두 계산을 모두 반영해야 new_below를 단순한 삽입 방향 flag보다 정확하게 이해할 수 있습니다.
마지막 insertion의 전체 helper
vma_adjust()는 v5.18 include/linux/mm.h의 inline wrapper입니다. header의 원 SPDX 식별자 GPL-2.0을 보존했습니다.
/* SPDX-License-Identifier: GPL-2.0 */
/* Linux v5.18, include/linux/mm.h */
static inline int vma_adjust(struct vm_area_struct *vma, unsigned long start,
unsigned long end, pgoff_t pgoff, struct vm_area_struct *insert)
{
return __vma_adjust(vma, start, end, pgoff, insert, NULL);
}
expand가 NULL이며 insert가 새 VMA라는 점이 merge 경로와 다릅니다. 그 내부에서는 file interval 연결을 준비한 뒤 기존 VMA 경계를 고치고, 다음 두 helper로 process의 list와 RB-tree에 새 VMA를 넣습니다. 둘 다 v5.18 mm/mmap.c의 전체 함수입니다.
// SPDX-License-Identifier: GPL-2.0-only
// Linux v5.18, mm/mmap.c. Written by obz.
// Address space accounting code: <alan@lxorguk.ukuu.org.uk>
static void __insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma)
{
struct vm_area_struct *prev;
struct rb_node **rb_link, *rb_parent;
if (find_vma_links(mm, vma->vm_start, vma->vm_end,
&prev, &rb_link, &rb_parent))
BUG();
__vma_link(mm, vma, prev, rb_link, rb_parent);
mm->map_count++;
}
static void
__vma_link(struct mm_struct *mm, struct vm_area_struct *vma,
struct vm_area_struct *prev, struct rb_node **rb_link,
struct rb_node *rb_parent)
{
__vma_link_list(mm, vma, prev);
__vma_link_rb(mm, vma, rb_link, rb_parent);
}
find_vma_links()가 겹침을 보고하면 여기서는 복구 가능한 allocation 실패가 아니라 caller가 보장해야 할 split 불변식의 파괴이므로 BUG()입니다. 성공하면 list와 RB-tree를 연결하고 map count를 한 번 늘립니다. file interval tree에는 이미 준비된 node를 또 중복 삽입하는 것이 아닙니다.
v6.18의 __split_vma()는 struct vma_iterator와 VMA prepare/complete machinery를 사용합니다. 따라서 위 wrapper의 mm 인자와 RB insertion hints가 현대 signature에도 그대로 있다는 설명은 하지 않습니다. 보존할 계약은 두 범위의 비중첩, pgoff 연속성, 독립 reference 및 실패 시 실제 취득 자원의 반납입니다.
본문에 수록한 v5.18 정의의 원 파일 전체는 다운로드 가능한 source 목록에서 읽으실 수 있습니다. 원 header·license를 보존한 파일과 본문의 표시용 code block을 구분하며, 이 파일 목록이 kernel의 모든 build dependency를 제공하는 것은 아닙니다.
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 5, §5.1.4. 현재 split은 Linux v6.18 mm/vma.c, VMA object 수명은 mm/vma_init.c에 있습니다.