같은 파일의 이웃한 두 구간을 같은 protection으로 mapping해도 /proc에 VMA 두 개가 남을 수 있습니다. 주소의 인접성은 merge의 출발점일 뿐입니다. backing의 identity, 논리 offset, reverse mapping 관계까지 맞아야 한 VMA로 표현해도 의미가 보존됩니다.
빈 hole을 채우는 merge와 속성을 바꾸는 merge
RB-tree 기반 구현의 vma_merge(mm, prev, addr, end, ...)는 두 가지 상황을 함께 다룹니다. 하나는 hole에 새 범위를 넣으면서 앞·뒤 VMA를 확장하는 경우입니다. 앞만, 뒤만, 양쪽 모두와 합칠 수 있습니다. 다른 하나는 기존 VMA 일부의 protection·locking·advice·NUMA policy를 바꾸어 이웃과 같아지는 경우입니다.
뒤의 경우에는 전체 VMA를 없애는 것뿐 아니라 한쪽 경계를 이동하여 현재 VMA를 줄이면서 이웃을 늘리는 작업도 필요합니다. RB-tree 기반 구현의 여덟 case는 이 범위 관계를 분류한 것입니다. area는 조정 대상에 걸친 VMA, prev와 next는 caller가 정한 주변 관계이며, __vma_adjust() 내부에서 같은 이름을 다른 대상으로 쓰는 점에 주의해야 합니다.
mapping을 만드는 mmap_region()·do_brk_flags(), relocation의 copy_vma(), 속성을 바꾸는 mprotect_fixup()·madvise_update_vma()·mlock_fixup()·mbind_range()가 merge를 시도하는 이유가 여기에 있습니다. “mmap 최적화 전용 기능”이 아닙니다. merge 실패는 해당 system call 전체의 실패와도 다릅니다. caller는 필요한 split과 새 VMA를 사용해 같은 효과를 표현할 수 있습니다.
같은 주소 권한만 비교하지 않습니다
기본 flags는 같아야 하지만 VM_SOFTDIRTY 차이만으로 VMA를 쪼개 두지는 않습니다. 그 대신 합친 결과의 soft-dirty 의미를 보존해야 합니다. NUMA policy도 동등해야 하며 file mapping에서는 같은 struct file이어야 합니다. 같은 pathname이나 같은 inode라는 조건으로 임의 대체하면 file별 state를 잃을 수 있습니다.
offset은 연속이어야 합니다. 앞 VMA가 N pages를 차지하면 뒤 VMA의 vm_pgoff는 앞의 offset에 N을 더한 값이어야 합니다. 앞에 붙이는 검사와 뒤에 붙이는 검사가 서로 다른 인자를 받으므로 equality 하나만 보고 “offset이 모두 동일해야 한다”고 읽어서는 안 됩니다.
vm_ops->close가 있으면 driver가 VMA마다 state를 둘 가능성이 있으므로 merge를 제한합니다. anonymous name도 다르면 합칠 수 없습니다. userfaultfd context 같은 추가 state 역시 version별 predicate의 일부입니다. VM_SPECIAL 범위는 보통의 merge 시도에서 배제합니다.
여기서 anonymous name과 /proc이 합성하는 [heap]·[stack] label은 같은 field가 아닙니다. v5.18의 procfs는 break와 stack 범위를 먼저 판별하고, 따로 지정된 anonymous name은 [anon:...] 형식으로 표시합니다. 화면의 특수 label이 같다는 이유만으로 merge predicate의 name 비교까지 통과한다고 판단할 수 없습니다. v5.18 fs/proc/task_mmu.c
anon_vma는 folio를 다시 쓰지 않고 유지해야 합니다
anonymous folio는 VMA가 아니라 anon_vma를 향합니다. 두 VMA가 각자 다른 active anon_vma를 사용하면 한 VMA로 합친 뒤 새 folio를 어느 쪽에 연결할지 문제가 됩니다. 이미 연결된 folio를 전부 바꾸는 비용을 치르지 않으려고 merge 가능성을 제한합니다.
양쪽에 anon_vma가 있으면 같은 객체인지가 핵심입니다. 한쪽에 아직 없다면 단순한 이웃 관계를 가져올 여지가 있지만 fork ancestry가 복잡한 VMA까지 같은 방식으로 섞을 수는 없습니다. 앞·뒤가 각각 입력 범위와 merge 가능하더라도 둘 사이의 anonymous compatibility를 별도로 확인하는 이유입니다.
RB-tree 기반 구현의 predecessor 우선 분기는 앞 VMA로 결과를 흡수하고, successor-only 분기는 뒤 VMA를 앞쪽으로 확장합니다. 조정 실패 시 NULL은 “merge하지 못했다”는 뜻입니다. RB-tree 기반 구현의 case 번호나 helper signature를 v6.18에 그대로 대응시키기보다 현재 mm/vma.c의 predicate와 caller 계약을 읽어야 합니다.
여덟 case를 결과 object로 나누어 읽습니다
caller는 대상 address space인 mm의 mmap_lock을 write로 보유해야 합니다. vma_merge()가 내부에서 이 lock을 새로 얻어 주는 public allocation API는 아닙니다. vm_flags는 바꾸려는 범위의 속성이며, file·offset·policy·anonymous state도 같은 범위에 맞추어 넘겨야 합니다.
여기서는 고전 vma_merge()의 code comment에 있는 case 번호를 사용하겠습니다. addr와 end는 바꾸려는 반열린 범위이며 end는 포함되지 않습니다. prev가 NULL이면 vma_next()는 첫 VMA를 가져옵니다. 먼저 그 결과를 area로 두고, area->vm_end == end이면 뒤쪽 검사 대상 next를 한 칸 더 이동시킵니다. 이 준비를 보지 않으면 source 안의 next가 언제나 변경 대상 바로 다음 object라고 오해하기 쉽습니다.
빈 hole을 채우는 경우에는 앞과 뒤가 모두 맞는 case 1, 앞만 맞는 case 2, 뒤만 맞는 case 3이 있습니다. 앞이 살아남는 1·2는 시작과 pgoff를 앞 VMA에서 유지하고 끝을 늘립니다. 3은 뒤 VMA가 살아남아 시작을 addr로 내리며, 새 pgoff는 next->vm_pgoff - pglen입니다. pglen은 (end - addr) >> PAGE_SHIFT로 계산한 page 수입니다. byte 차이를 pgoff에서 바로 빼는 연산이 아닙니다.
속성 변경에서는 부분 범위와 전체 범위가 갈립니다. case 4는 prev 안의 tail [addr, prev->vm_end)가 뒤 VMA와 같아지는 경우입니다. prev는 addr에서 끝나도록 줄고 next의 시작은 그만큼 내려갑니다. 반대로 case 5는 area의 prefix가 prev와 같아져 prev의 끝은 올라가고 area의 시작도 같은 주소로 올라갑니다. 입력 시작이 언제나 area의 시작이라는 전제는 case 4에 적용할 수 없습니다.
전체 area를 흡수할 때 양쪽이 맞으면 case 6, 앞만 맞으면 7, 뒤만 맞으면 8입니다. 6·7은 prev를 반환합니다. 8에서는 원래 area를 없애고 원래 next를 아래쪽으로 확장하여 반환합니다. 따라서 같은 최종 주소 범위를 얻더라도 살아남는 vm_area_struct의 identity는 다를 수 있습니다.
__vma_adjust()가 실패하면 NULL을 반환하고, 성공하면 살아남은 VMA에 khugepaged 통지를 한 뒤 그 pointer를 반환합니다. NULL이 “아무 속성도 바뀌지 않았음”이나 system call 전체의 실패를 보장하는 것은 아닙니다. caller가 새로운 VMA 표현을 준비하거나 오류를 처리하는 계약을 따로 읽어야 합니다.
고전 vma_merge 전체와 현대 경로의 차이
아래는 Linux v5.18 mm/mmap.c의 vma_merge() 전체입니다. GPL-2.0-only code이며 license 원문을 함께 제공합니다. kernel 내부용 함수로서 단독 실행 프로그램은 아닙니다. 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>
struct vm_area_struct *vma_merge(struct mm_struct *mm,
struct vm_area_struct *prev, unsigned long addr,
unsigned long end, unsigned long vm_flags,
struct anon_vma *anon_vma, struct file *file,
pgoff_t pgoff, struct mempolicy *policy,
struct vm_userfaultfd_ctx vm_userfaultfd_ctx,
struct anon_vma_name *anon_name)
{
pgoff_t pglen = (end - addr) >> PAGE_SHIFT;
struct vm_area_struct *area, *next;
int err;
/*
* We later require that vma->vm_flags == vm_flags,
* so this tests vma->vm_flags & VM_SPECIAL, too.
*/
if (vm_flags & VM_SPECIAL)
return NULL;
next = vma_next(mm, prev);
area = next;
if (area && area->vm_end == end) /* cases 6, 7, 8 */
next = next->vm_next;
/* verify some invariant that must be enforced by the caller */
VM_WARN_ON(prev && addr <= prev->vm_start);
VM_WARN_ON(area && end > area->vm_end);
VM_WARN_ON(addr >= end);
/*
* Can it merge with the predecessor?
*/
if (prev && prev->vm_end == addr &&
mpol_equal(vma_policy(prev), policy) &&
can_vma_merge_after(prev, vm_flags,
anon_vma, file, pgoff,
vm_userfaultfd_ctx, anon_name)) {
/*
* OK, it can. Can we now merge in the successor as well?
*/
if (next && end == next->vm_start &&
mpol_equal(policy, vma_policy(next)) &&
can_vma_merge_before(next, vm_flags,
anon_vma, file,
pgoff+pglen,
vm_userfaultfd_ctx, anon_name) &&
is_mergeable_anon_vma(prev->anon_vma,
next->anon_vma, NULL)) {
/* cases 1, 6 */
err = __vma_adjust(prev, prev->vm_start,
next->vm_end, prev->vm_pgoff, NULL,
prev);
} else /* cases 2, 5, 7 */
err = __vma_adjust(prev, prev->vm_start,
end, prev->vm_pgoff, NULL, prev);
if (err)
return NULL;
khugepaged_enter_vma_merge(prev, vm_flags);
return prev;
}
/*
* Can this new request be merged in front of next?
*/
if (next && end == next->vm_start &&
mpol_equal(policy, vma_policy(next)) &&
can_vma_merge_before(next, vm_flags,
anon_vma, file, pgoff+pglen,
vm_userfaultfd_ctx, anon_name)) {
if (prev && addr < prev->vm_end) /* case 4 */
err = __vma_adjust(prev, prev->vm_start,
addr, prev->vm_pgoff, NULL, next);
else { /* cases 3, 8 */
err = __vma_adjust(area, addr, next->vm_end,
next->vm_pgoff - pglen, NULL, next);
/*
* In case 3 area is already equal to next and
* this is a noop, but in case 8 "area" has
* been removed and next was expanded over it.
*/
area = next;
}
if (err)
return NULL;
khugepaged_enter_vma_merge(area, vm_flags);
return area;
}
return NULL;
}
이 tag의 통지 함수는 khugepaged_enter_vma_merge()입니다. 다른 구현에서 보이는 khugepaged_enter_vma()라는 이름으로 몰래 바꾸지 않았습니다. 또한 v6.18은 vma_merge_new_range()·vma_merge_existing_range()와 struct vma_merge_struct를 사용하는 다른 구조입니다. 위 signature와 case별 __vma_adjust() 호출을 현대 kernel에 그대로 대입할 수 없습니다. v6.18 mm/vma.c
merge predicate의 전체 조건
다음 네 함수도 같은 v5.18 file의 전체 code입니다. 첫 함수는 flags·file pointer·close callback·userfaultfd context·anonymous name을 검사합니다. source에서 VM_SOFTDIRTY만 flags 비교에서 제외하는 것은 결과의 dirty 의미를 caller가 보존한다는 전제가 있기 때문입니다.
// 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 inline int is_mergeable_vma(struct vm_area_struct *vma,
struct file *file, unsigned long vm_flags,
struct vm_userfaultfd_ctx vm_userfaultfd_ctx,
struct anon_vma_name *anon_name)
{
/*
* VM_SOFTDIRTY should not prevent from VMA merging, if we
* match the flags but dirty bit -- the caller should mark
* merged VMA as dirty. If dirty bit won't be excluded from
* comparison, we increase pressure on the memory system forcing
* the kernel to generate new VMAs when old one could be
* extended instead.
*/
if ((vma->vm_flags ^ vm_flags) & ~VM_SOFTDIRTY)
return 0;
if (vma->vm_file != file)
return 0;
if (vma->vm_ops && vma->vm_ops->close)
return 0;
if (!is_mergeable_vm_userfaultfd_ctx(vma, vm_userfaultfd_ctx))
return 0;
if (!anon_vma_name_eq(anon_vma_name(vma), anon_name))
return 0;
return 1;
}
static inline int is_mergeable_anon_vma(struct anon_vma *anon_vma1,
struct anon_vma *anon_vma2,
struct vm_area_struct *vma)
{
/*
* The list_is_singular() test is to avoid merging VMA cloned from
* parents. This can improve scalability caused by anon_vma lock.
*/
if ((!anon_vma1 || !anon_vma2) && (!vma ||
list_is_singular(&vma->anon_vma_chain)))
return 1;
return anon_vma1 == anon_vma2;
}
anonymous compatibility의 list_is_singular()는 ancestry가 단순한지에 관한 조건입니다. 한쪽 anon_vma가 NULL이고 비교 대상 VMA의 chain이 하나이거나, 비교할 VMA 인자 자체가 NULL이면 제한된 호환을 허용합니다. 둘 다 non-NULL이면 같은 anon_vma인지 봅니다. 입력 범위가 아직 anon_vma를 갖지 않아 prev와 next 각각에는 맞아도, 양쪽의 서로 다른 anon_vma를 한꺼번에 섞을 수 있다는 결론은 나오지 않습니다. 양쪽 merge 때 is_mergeable_anon_vma(prev->anon_vma, next->anon_vma, NULL)를 다시 부르는 이유입니다. 이 제한에는 fork로 복잡해진 ancestry와 anon_vma lock 확장성도 관계합니다.
// 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 int
can_vma_merge_after(struct vm_area_struct *vma, unsigned long vm_flags,
struct anon_vma *anon_vma, struct file *file,
pgoff_t vm_pgoff,
struct vm_userfaultfd_ctx vm_userfaultfd_ctx,
struct anon_vma_name *anon_name)
{
if (is_mergeable_vma(vma, file, vm_flags, vm_userfaultfd_ctx, anon_name) &&
is_mergeable_anon_vma(anon_vma, vma->anon_vma, vma)) {
pgoff_t vm_pglen;
vm_pglen = vma_pages(vma);
if (vma->vm_pgoff + vm_pglen == vm_pgoff)
return 1;
}
return 0;
}
static int
can_vma_merge_before(struct vm_area_struct *vma, unsigned long vm_flags,
struct anon_vma *anon_vma, struct file *file,
pgoff_t vm_pgoff,
struct vm_userfaultfd_ctx vm_userfaultfd_ctx,
struct anon_vma_name *anon_name)
{
if (is_mergeable_vma(vma, file, vm_flags, vm_userfaultfd_ctx, anon_name) &&
is_mergeable_anon_vma(anon_vma, vma->anon_vma, vma)) {
if (vma->vm_pgoff == vm_pgoff)
return 1;
}
return 0;
}
after는 기존 VMA의 시작 offset에 그 VMA의 page 수를 더해 입력 시작 offset과 비교합니다. before가 받는 offset은 caller가 이미 pgoff + pglen으로 계산한 입력 범위의 끝 다음 page이므로, 뒤 VMA의 시작 offset과 바로 비교할 수 있습니다. 두 함수의 equality가 다르게 생긴 이유가 여기에 있습니다. page-cache index 끝의 wrap 검사를 여기서 반복하지 않는 것은 upstream comment에 명시된 do_mmap()의 사전 검사를 전제로 한 것입니다. 이 함수를 임의의 VMA와 offset에 적용할 수 있는 public API로 보시면 안 됩니다.
본문에 수록한 v5.18 정의의 원 파일 전체는 다운로드 가능한 source 목록에서 읽으실 수 있습니다. 원 header·license를 보존한 파일과 본문의 표시용 code block을 구분하며, 이 파일 목록이 kernel의 모든 build dependency를 제공하는 것은 아닙니다.
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 5, §5.1.1–5.1.2. 현재 predicate와 merge helper는 Linux v6.18 mm/vma.c, 여러 index의 보호 규칙은 Process Addresses를 참고하실 수 있습니다.