VMA의 시작 주소를 두 page 옮기는 일은 vm_start += 2 * PAGE_SIZE 한 줄로 끝나지 않습니다. 같은 VMA는 process의 주소 index뿐 아니라 file과 anonymous reverse mapping index에도 연결됩니다. 경계 변경은 이 index들이 잠깐이라도 서로 다른 범위를 주장하지 않도록 조정하는 작업입니다.
start, end, pgoff가 하나의 좌표계를 이룹니다
RB-tree 기반 구현의 __vma_adjust()는 직접 경계를 조정할 VMA와 새 start·exclusive end·pgoff를 받고, 필요하면 insert할 VMA 또는 확장할 VMA도 받습니다. 첫 VMA 인자가 언제나 최종 merge 결과는 아닙니다. case 4에서는 이 인자의 VMA를 줄이고 이웃을 늘리며, case 8에서는 내부 pointer를 교환합니다. merge에서는 insert가 NULL이며 split에서는 새 VMA를 삽입합니다. 앞뒤 경계를 이동하면 보존할 file offset과 anonymous 논리 page index도 함께 맞추어야 합니다.

그림에서 VA page [24, 30)과 file index 144부터 시작하는 VMA의 앞 두 page를 떼면 새 범위는 [26, 30), 새 pgoff는 146입니다. start만 바꾸고 offset을 144로 두면 같은 VA가 다른 file byte를 가리키게 됩니다.
내부 next와 remove_next를 구별합니다
RB-tree 기반 구현의 adjust_next는 이웃 경계를 얼마나 이동할지, remove_next는 다음 object를 제거할지와 반복 처리를 어떻게 할지를 표현합니다. 단순 제거는 한 번이지만 세 VMA를 양쪽에서 합치는 경로는 두 pass가 필요합니다. 현재 VMA를 뒤 이웃에 흡수하는 경로에서는 결과로 남길 VMA를 맞추려고 내부 vma와 next를 바꾸므로, caller의 next와 함수 내부 next를 같은 객체로 읽으면 cleanup 대상이 뒤집힙니다.
exporter와 importer는 anonymous state를 가진 source에서 결과 VMA로 필요한 anon_vma 관계를 가져오는 역할입니다. 주소 길이를 합치는 산술과 ancestry를 보존하는 작업을 분리한 것입니다. 이러한 변수는 RB-tree 기반 경로의 설명이며 현재 tag에 같은 지역변수가 존재한다는 주장은 아닙니다.
tree에서 빼고 경계를 고친 뒤 다시 넣습니다
file-backed VMA는 address_space->i_mmap의 write 보호가 필요합니다. anonymous 연결은 root anon_vma의 write lock 아래 interval node를 먼저 제거합니다. 조정되는 이웃에도 같은 준비를 합니다. private file mapping은 file 관계와 anonymous CoW 관계를 동시에 가질 수 있으므로 한쪽 tree만 처리하면 안 됩니다.
그다음 vm_start, vm_end, vm_pgoff와 이웃의 경계를 실제로 갱신합니다. file interval node와 anonymous AVC interval node를 새 범위로 재삽입하고 필요한 gap 요약을 고칩니다. 일부 architecture의 data-cache maintenance도 순서에 영향을 주지만 x86-64의 해당 hook을 일반적인 TLB flush와 혼동하면 안 됩니다.
흡수된 VMA가 있으면 주소 list/tree와 file interval tree에서 연결을 제거하고 writable mapping 추적을 내립니다. 필요 없는 anonymous chain, file reference, policy reference와 VMA object를 정리합니다. 두 번째 pass가 필요한 경우에는 이 cleanup을 마친 뒤 남은 이웃을 대상으로 반복합니다.
주소 정렬은 그대로여도 metadata는 바뀝니다
경계를 변경해도 VMA들의 정렬 순서가 바뀌지 않으면 RB-tree node를 전부 재배치할 필요는 없습니다. 그러나 gap 요약과 interval endpoint는 반드시 고쳐야 합니다. 같은 이유로 page table을 바꾸지 않는 protection metadata 조정에도 rmap index 작업이 따라붙을 수 있습니다.
validate_mm()과 debug interval 검사는 이런 불변식을 점검하지만 lock을 대체하지는 않습니다. v6.18은 VMA prepare/complete와 Maple Tree 기반 작업으로 구조가 달라졌습니다. 유지되는 규칙은 주소 index, backing interval index, object 수명과 accounting을 하나의 변경으로 일관되게 보존한다는 것입니다.
byte delta와 page offset을 같은 식으로 쓰면 안 됩니다
case 4에서 내부 next는 caller의 next입니다. adjust_next = addr - prev->vm_end에 해당하는 음수 byte delta만큼 뒤 VMA를 아래로 늘립니다. exporter는 줄어드는 prev, importer는 늘어나는 next입니다. case 5에서는 내부 next가 caller의 area이며 end - area->vm_start라는 양수 byte delta만큼 area의 앞을 줄입니다. 이번에는 exporter가 area, importer가 prev입니다.
pgoff에 적용하는 것은 byte delta 자체가 아니라 adjust_next >> PAGE_SHIFT입니다. 뒤 VMA를 앞쪽으로 확장할 때의 새 offset도 next->vm_pgoff - ((end - addr) >> PAGE_SHIFT)로 계산합니다. 이 식의 addr/end는 caller인 vma_merge()의 변경 범위입니다. byte 길이를 먼저 page 수로 바꾼다는 괄호가 중요합니다. 예를 들어 4 KiB page 두 개를 앞에 더하고 원래 offset이 4000이면 새 offset은 3998입니다. 이는 독립적인 좌표 계산이며 측정 결과가 아닙니다.
remove_next가 0인 case 2–5에서는 VMA object를 없애지 않습니다. case 1·7의 값 1은 한 object를 흡수함을, case 6의 값 2는 두 object를 두 pass로 흡수함을 뜻합니다. case 8의 값 3은 단순히 세 object를 제거한다는 count가 아닙니다. 먼저 내부 vma와 next를 교환해 원래 successor를 결과 object로 만들고 원래 area를 제거합니다. 따라서 교환 후 next는 제거 대상인 옛 area이며, 옛 successor는 살아남습니다.
case 6의 첫 pass에서는 내부 next가 area이므로 end를 area 끝으로 잠시 제한합니다. area를 정리한 다음 살아 있는 vma->vm_next를 얻고, remove_next를 2에서 1로 바꾸고 end를 마지막 successor 끝으로 바꾸어 again으로 돌아갑니다. 첫 pass의 area에 anon_vma가 없으면 그다음 VMA를 exporter로 삼는 예외도 있습니다. 이때 clone이 필요한 조건은 exporter에 anon_vma가 있고 importer에는 없는 경우이며, anon_vma_clone() 실패는 tree 변경을 진행하기 전에 반환합니다.
rmap tree는 다른 좌표계를 사용합니다
process 주소 tree의 순서와 file·anonymous interval tree의 key는 다릅니다. 고전 file interval의 시작은 vm_pgoff, 포함되는 마지막 index는 vm_pgoff + vma_pages(vma) - 1입니다. anonymous AVC interval도 해당 VMA의 같은 논리 page 좌표를 사용합니다. 따라서 이 tree를 단순히 vm_start가 key인 VA tree라고 설명하면 private file CoW와 relocation을 이해하기 어렵습니다. v5.18 mm/interval_tree.c
anon_vma_interval_tree_pre_update_vma(vma)는 그 VMA에 연결된 AVC node들을 각각의 anon_vma tree에서 빼는 작업입니다. root 아래의 다른 VMA 연결을 전부 비우는 것이 아닙니다. file-backed private mapping은 두 종류의 interval tree에 동시에 나타날 수 있으므로 file의 i_mmap_rwsem write 보호와 anon_vma root의 write 보호를 모두 지켜야 합니다. 현재 VMA에 anon_vma가 없고 조정할 이웃에만 있으면 그 이웃의 anon_vma로 보호할 대상을 정하는 예외도 있습니다.
insertion에서는 file interval node를 __vma_link_file(insert)로 먼저 노출하는 architecture 관련 순서가 있습니다. arm/parisc의 cache maintenance가 instantiated page를 계속 찾을 수 있게 하려는 준비이며, process 주소 list/tree에는 기존 VMA의 경계를 고친 뒤에 연결합니다. 모든 index를 동시에 한 명령으로 바꾼다는 뜻이 아니라 필요한 lock 아래 일관된 관찰 경계를 만드는 절차입니다.
unlink와 free 사이에서 사라지는 것들
다음은 Linux v5.18 mm/mmap.c와 mm/util.c의 helper 전체입니다. GPL-2.0-only code이며 license 원문을 함께 둡니다. tab 표시 외에 함수 내부를 생략하지 않았습니다.
// 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 __always_inline void __vma_unlink(struct mm_struct *mm,
struct vm_area_struct *vma,
struct vm_area_struct *ignore)
{
vma_rb_erase_ignore(vma, &mm->mm_rb, ignore);
__vma_unlink_list(mm, vma);
/* Kill the cache */
vmacache_invalidate(mm);
}
/* Linux v5.18, mm/util.c */
void __vma_unlink_list(struct mm_struct *mm, struct vm_area_struct *vma)
{
struct vm_area_struct *prev, *next;
next = vma->vm_next;
prev = vma->vm_prev;
if (prev)
prev->vm_next = next;
else
mm->mmap = next;
if (next)
next->vm_prev = prev;
}
static void __remove_shared_vm_struct(struct vm_area_struct *vma,
struct file *file, struct address_space *mapping)
{
if (vma->vm_flags & VM_SHARED)
mapping_unmap_writable(mapping);
flush_dcache_mmap_lock(mapping);
vma_interval_tree_remove(vma, &mapping->i_mmap);
flush_dcache_mmap_unlock(mapping);
}
__vma_unlink()의 ignore 인자는 case 8의 교환 중간 상태를 debug RB 검증에서 어떻게 다룰지 정합니다. object를 free하지 않고 살려 두는 일반 ignore flag는 아닙니다. list에서는 predecessor가 없을 때 mm의 head를 바꾸고 successor가 있을 때만 backward link를 고칩니다. file interval 제거는 i_mmap_rwsem 보호 아래 수행하고, VM_SHARED였던 VMA의 writable-mapping 추적도 내립니다. 이 추적은 cache maintenance뿐 아니라 memfd의 write sealing 판단에도 중요합니다.
그다음 anon_vma interval을 새 범위로 복구하고 anon_vma lock을 놓은 뒤 file interval lock을 놓습니다. 흡수한 VMA는 이후 file reference, anon_vma chain 관계, map count, NUMA policy를 정리하고 free합니다. 이 경로가 일반 unmap처럼 vm_ops->close를 부르지 않는 것은 merge predicate가 그러한 VMA를 미리 거절하기 때문입니다.
전체 함수에서 lock의 수명을 확인합니다
고전 VMA의 구조체와 __vma_adjust() 전체에서는 THP와 uprobe 호출을 포함한 위 단계가 한 함수 안에서 어떤 순서로 이어지는지 확인하실 수 있습니다. v6.18의 vma_prepare()·vma_complete()는 같은 이름의 지역변수를 재현하지 않으므로 고전 case의 숫자보다 보호하는 index와 살아남는 object를 기준으로 비교하는 편이 정확합니다.
본문에 수록한 v5.18 정의의 원 파일 전체는 다운로드 가능한 source 목록에서 읽으실 수 있습니다. 원 header·license를 보존한 파일과 본문의 표시용 code block을 구분하며, 이 파일 목록이 kernel의 모든 build dependency를 제공하는 것은 아닙니다.
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 5, §5.1.3. 현재 index 변경은 Linux v6.18 mm/vma.c, lock의 보호 범위는 Process Addresses를 참고하실 수 있습니다.