read lock으로 VMA를 찾은 다음, 느린 file I/O를 기다려야 하는 상황을 생각해 보겠습니다. lock을 계속 잡으면 mapping을 바꾸려는 다른 thread가 오래 기다립니다. 그렇다고 lock을 놓고 예전 VMA pointer로 돌아오면 그 사이의 munmap()을 놓칩니다. Linux의 fault와 memory 관측 API를 읽을 때 중요한 질문은 read/write 구분만이 아니라, 보호가 끊기는 순간 무엇을 별도로 붙잡고 무엇을 다시 조회하느냐입니다.
helper 구현은 official v6.18을 기준으로 합니다. historical 호출 사례는 v6.0으로 따로 명시합니다. 같은 함수 이름이라도 버전별 모든 호출 edge와 lock 범위가 동일하다고 가정하지 않습니다.
획득 실패는 보호가 없는 상태입니다
다음은 v6.18 include/linux/mmap_lock.h의 전체 helper 정의입니다. kernel 내부 GPL code이며 원래 SPDX·source 정보와 code/GPL-2.0.txt를 함께 보존했습니다.
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 309-315.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline void mmap_write_lock(struct mm_struct *mm)
{
__mmap_lock_trace_start_locking(mm, true);
down_write(&mm->mmap_lock);
mm_lock_seqcount_begin(mm);
__mmap_lock_trace_acquire_returned(mm, true, true);
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 365-370.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline void mmap_read_lock(struct mm_struct *mm)
{
__mmap_lock_trace_start_locking(mm, false);
down_read(&mm->mmap_lock);
__mmap_lock_trace_acquire_returned(mm, false, true);
}
v6_18_mmap_write_lock_killable.c
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 325-335.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline int mmap_write_lock_killable(struct mm_struct *mm)
{
int ret;
__mmap_lock_trace_start_locking(mm, true);
ret = down_write_killable(&mm->mmap_lock);
if (!ret)
mm_lock_seqcount_begin(mm);
__mmap_lock_trace_acquire_returned(mm, true, ret == 0);
return ret;
}
v6_18_mmap_read_lock_killable.c
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 372-380.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline int mmap_read_lock_killable(struct mm_struct *mm)
{
int ret;
__mmap_lock_trace_start_locking(mm, false);
ret = down_read_killable(&mm->mmap_lock);
__mmap_lock_trace_acquire_returned(mm, false, ret == 0);
return ret;
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 382-390.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline bool mmap_read_trylock(struct mm_struct *mm)
{
bool ret;
__mmap_lock_trace_start_locking(mm, false);
ret = down_read_trylock(&mm->mmap_lock) != 0;
__mmap_lock_trace_acquire_returned(mm, false, ret);
return ret;
}
기본 blocking helper는 underlying rwsem 획득을 기다립니다. killable variant는 성공 시 0이고 fatal signal에 의한 실패를 반환할 수 있습니다. read trylock은 즉시 true/false를 반환합니다. 실패한 뒤에는 unlock할 lock도, 안정적으로 읽을 VMA도 없습니다. syscall wrapper가 그 실패를 -EINTR 등으로 변환하는 것은 또 한 단계입니다.
historical helper 목록에는 write trylock도 있었지만 위 v6.18 header의 API 목록에 그 함수를 임의로 추가하지 않았습니다. kernel 내부 helper는 영구적인 user ABI가 아니므로 사용하려는 tag에서 정의를 확인해야 합니다.
중첩과 assertion은 획득을 대신하지 않습니다
v6_18_mmap_write_lock_nested.c
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 317-323.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline void mmap_write_lock_nested(struct mm_struct *mm, int subclass)
{
__mmap_lock_trace_start_locking(mm, true);
down_write_nested(&mm->mmap_lock, subclass);
mm_lock_seqcount_begin(mm);
__mmap_lock_trace_acquire_returned(mm, true, true);
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 69-72.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline void mmap_assert_locked(const struct mm_struct *mm)
{
rwsem_assert_held(&mm->mmap_lock);
}
v6_18_mmap_assert_write_locked.c
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 74-77.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline void mmap_assert_write_locked(const struct mm_struct *mm)
{
rwsem_assert_held_write(&mm->mmap_lock);
}
nested helper의 subclass는 여러 mm lock을 정해진 순서로 획득하는 상황을 lockdep에 설명합니다. 동일 lock의 재귀 획득을 허용하는 기능이 아닙니다. fork의 주소 공간 복제처럼 부모와 새 mm을 다루는 경로에서는 어느 mm의 lock을 먼저 얻었는지까지 중요합니다. assertion 역시 검사일 뿐 획득은 아닙니다. 현재 helper는 rwsem_assert_held*()에 위임하므로 예전의 특정 debug macro 조합을 모든 버전의 구현으로 인용하지 않습니다.
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 392-396.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline void mmap_read_unlock(struct mm_struct *mm)
{
__mmap_lock_trace_released(mm, false);
up_read(&mm->mmap_lock);
}
v6_18_mmap_read_unlock_non_owner.c
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 401-405.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline void mmap_read_unlock_non_owner(struct mm_struct *mm)
{
__mmap_lock_trace_released(mm, false);
up_read_non_owner(&mm->mmap_lock);
}
non-owner 해제는 일반 unlock과 underlying annotation이 다릅니다. 임의의 task가 남의 read lock을 풀어도 되는지 검사하여 허용하는 함수가 아닙니다. 획득한 경로와 해제하는 context의 계약이 맞을 때 사용합니다. refcount로 mm을 붙잡았다는 사실도 이 pairing을 대신하지 않습니다.
downgrade는 보호가 끊기는 재획득과 다릅니다
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 345-349.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline void vma_end_write_all(struct mm_struct *mm)
{
mmap_assert_write_locked(mm);
mm_lock_seqcount_end(mm);
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 351-356.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline void mmap_write_unlock(struct mm_struct *mm)
{
__mmap_lock_trace_released(mm, true);
vma_end_write_all(mm);
up_write(&mm->mmap_lock);
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 358-363.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline void mmap_write_downgrade(struct mm_struct *mm)
{
__mmap_lock_trace_acquire_returned(mm, false, true);
vma_end_write_all(mm);
downgrade_write(&mm->mmap_lock);
}
current writer는 mmap lock을 반환하거나 downgrade하기 직전에 해당 mm의 per-VMA write 보호도 끝냅니다. vma_end_write_all()을 일반 호출자가 중간에서 편의상 부르면 상위 caller가 잡았다고 생각한 VMA write 보호까지 끝낼 수 있습니다. 원래 주석이 수동 호출을 경고하는 이유입니다.
downgrade는 writer에서 reader로 보호를 이어가는 동작입니다. read lock을 놓고 write lock을 얻는 반대 순서는 같은 연속성을 보장하지 않습니다. stack 확장에 writer가 필요해 lock을 다시 얻었다면 VMA와 이웃, limit을 재검사해야 합니다. historical stack-gap 갱신을 page_table_lock으로 보호하던 방식과 현재 Maple Tree의 writer 계약을 같은 알고리즘으로 설명하지 않습니다.
fault와 GUP는 lock 상태를 반환합니다
fault는 read-side VMA 보호 아래에서도 PTE를 설치할 수 있습니다. PTE의 mutation은 해당 PTL과 architecture helper를 따릅니다. 다른 folio lock을 기다리거나 I/O를 시작할 때 mmap lock을 놓는 경로가 있으면 caller는 반환 bit와 입력 flag 계약을 함께 읽어야 합니다. VM_FAULT_RETRY와 VM_FAULT_COMPLETED 같은 결과를 단순한 성공·실패 bool로 바꾸면 이미 반환된 lock을 다시 풀 수 있습니다.
구현코드가 아닌 호출관계입니다. historical fault 경로에서 관심 지점을 좁혀 표시했습니다.
do_user_addr_fault -> handle_mm_fault -> __handle_mm_fault
handle_pte_fault -> do_anonymous_page / do_swap_page / do_wp_page
do_fault -> do_read_fault / do_cow_fault / do_shared_fault
__do_fault -> vma->vm_ops->fault -> filemap_fault
anonymous first write, present PTE의 write-protection, file callback은 서로 다른 분기입니다. file fault의 maybe_unlock_mmap_for_io() 계열이나 folio lock retry는 lock을 계속 보유한다는 전제를 깨뜨릴 수 있습니다. current per-VMA fault는 mmap lock 없이 출발할 수도 있으므로 helper 이름만 보고 언제나 mmap read lock이 있다고 단정하지 않습니다. v6.18 fault 진입과 filemap fault의 계약이 기준입니다.
GUP도 비슷한 문제를 API에 드러냅니다. historical v6.0 process_vm_rw_single_vec()는 remote mm의 read lock을 얻어 pin_user_pages_remote()에 locked 상태를 넘기고, 반환 뒤 그 값이 참일 때만 unlock합니다. pin으로 얻은 page의 수명과 원래 VMA pointer의 안정성은 다른 보장입니다. fast GUP에서 slow path로 내려가는 것, FOLL_LONGTERM 검증, retry 뒤 재획득도 이 소유권 문제와 연결됩니다.
mlock은 또 다른 두 단계입니다. VMA의 locked flag를 바꾸는 writer 단계가 끝난 뒤 population은 read-side에서 fault를 유발할 수 있습니다. 따라서 lock을 한 번 잡고 syscall 전체를 같은 모드로 처리한다고 그리면 안 됩니다. VM_LOCKONFAULT는 없는 page의 준비를 뒤로 미루므로 population 자체를 생략하는 조건도 있습니다.
I/O를 기다릴 때에는 file을 따로 붙잡습니다
v6.0 msync()는 VMA를 조사할 때 mmap read lock을 사용합니다. 쓰기 동기화 API라는 이유로 mmap writer라고 분류할 수 없습니다. MS_SYNC인 shared file range에서는 file reference를 얻고, mmap lock을 놓은 뒤 vfs_fsync_range()를 호출합니다. 다음 range를 이어가야 한다면 lock을 다시 잡고 find_vma()로 재조회합니다. file 수명을 보호하는 reference가 예전 VMA 수명을 연장하는 것은 아닙니다.
v6.0 madvise()는 advice에 따라 read/write mode를 선택합니다. madvise_willneed()는 file reference와 offset을 준비한 뒤 mmap read lock 밖에서 vfs_fadvise()를 수행합니다. madvise_remove()의 hole-punch 경로는 vfs_fallocate()를 사용합니다. 두 함수를 모두 fadvise 호출로 요약하면 실제 filesystem 작업을 잘못 설명합니다. population은 fault가 lock을 놓았는지 확인하고 다시 획득하며, stale predecessor를 그대로 유지하지 않습니다.
이 패턴은 mmap·munmap·mremap의 구조 변경과도 대비됩니다. 그쪽 writer는 index와 범위를 바꾸고, ELF load와 exec는 새 mm의 mapping 구성 및 old mm과의 전환을 조율합니다. fork에서는 부모의 CoW 준비와 새 mm의 index 구성이 필요하고 마지막 mmput에서는 exit_mmap으로 user range를 제거합니다. 'memory 관련 syscall이면 모두 writer'나 'data를 쓰는 API이면 mmap writer'라는 분류로는 실제 경합을 설명할 수 없습니다.
관측과 회수도 같은 주소 공간을 봅니다
procfs의 maps·smaps·numa_maps 순회는 VMA를 안정적으로 읽어야 하고, pagemap은 page-table 상태를 조사하며, clear_refs는 추적 상태를 바꿉니다. mem·cmdline·environ이 실제 memory를 읽는 경로는 mm reference와 remote access 계약까지 필요합니다. core dump는 signal 처리에서 이어질 수 있지만, user 복귀 경로의 모든 exception이 곧바로 dump를 만드는 것은 아닙니다. 선택된 coredump 경로의 VMA snapshot 단계에서 필요한 보호를 따로 봐야 합니다.
v6_18_mmap_lock_is_contended.c
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Official Linux v6.18: include/linux/mmap_lock.h
* Complete function; source lines 407-410.
* Body unchanged. Kernel dependencies and configuration are required.
* Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
*/
static inline int mmap_lock_is_contended(struct mm_struct *mm)
{
return rwsem_is_contended(&mm->mmap_lock);
}
긴 smaps rollup은 위 helper로 경합을 관측하여 writer에게 양보할 수 있습니다. 양보 뒤 계속 읽는 경우에는 range의 변경 가능성을 고려해야 합니다. CPU exception의 진단 출력도 VMA 이름을 찾기 위해 read 보호가 필요할 수 있으며, fault error 경로는 진단 전에 이미 보유한 lock을 반환하는지 구별해야 합니다.
OOM reaper는 task가 종료할 때까지 무작정 기다리는 대신 mm을 reaping할 수 있습니다. VMA 구조를 읽는 read-side 보호와 PTE 제거를 조합하므로 'mapping을 제거하면 반드시 mmap writer'라는 역추론도 틀립니다. process_mrelease()와 ordinary fault가 만나는 상태, MMF_UNSTABLE과 MMF_OOM_SKIP을 구분해야 합니다. prctl도 exe file·이미지 경계, anonymous VMA 이름, THP policy라는 서로 다른 대상에 따라 read/write 보호가 달라집니다.
NUMA에서는 VMA policy를 바꾸는 mbind와 node를 조회·이동하는 작업이 같은 lock 범위를 쓰지 않습니다. 특히 historical v6.0 task_numa_work()는 독립적인 NUMA kernel thread가 아니라 task-work context입니다. mmap_read_trylock()에 실패하면 이번 scan을 넘기며, task_tick_numa()가 task_work_add(..., TWA_RESUME)로 일을 예약합니다. mincore()의 residency 조사, mprotect()의 VMA permission 변경, swapoff의 PTE 복구도 그 목적과 보호 대상을 따로 읽어야 합니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 4의 mmap lock helper와 mmap/fault/GUP/mlock/madvise/core/procfs/OOM/NUMA 호출 사례.
- Linux v6.18 mmap_lock.h: 전체 helper 계약
- Linux v6.18 process_addrs: VMA·page-table 보호 범위
- Linux v6.0 msync.c: read lock과 fsync 중 drop
- Linux v6.0 madvise.c: advice별 lock과 I/O 전후 재조회
- Linux v6.0 process_vm_access.c: pin과 locked 반환
- Linux v6.0 fair.c: NUMA task work
- Linux v6.18 coredump.c: VMA snapshot
- Linux v6.18 task_mmu.c: procfs VMA와 page-table 조사
- Linux v6.18 oom_kill.c: reaping과 mm 상태