같은 process의 두 thread가 동시에 memory를 건드린다고 모두 같은 작업을 하는 것은 아닙니다. 한 thread는 새로운 VMA를 만들고, 다른 thread는 기존 VMA 안에 처음 data page를 연결할 수 있습니다. 둘을 무조건 하나의 exclusive lock으로 막으면 단순하지만 fault가 많은 프로그램의 병렬성은 크게 줄어듭니다.
Linux의 mm에는 그래서 여러 보호 범위가 있습니다. 이 글은 upstream v6.18을 기준으로 VMA metadata, page table, address-space policy를 나누어 읽습니다. 과거의 stack expansion이나 red-black tree 검증 경로에 쓰인 lock을 현행 모든 경로의 계약으로 일반화하지 않겠습니다.
mmap_lock은 VMA 구조를 보호합니다
mmap_lock은 mm별 read/write semaphore입니다. 여러 reader가 기존 VMA의 구조와 속성을 안정적으로 읽을 수 있고, writer는 VMA를 추가·제거하거나 보호 속성을 바꾸는 작업을 직렬화합니다. read lock을 가졌다는 사실은 data가 read-only라는 뜻이 아닙니다. fault는 안정적인 VMA를 읽으면서 별도의 PTE lock 아래 page-table entry를 변경할 수 있습니다.
read lock이 있다고 page table이나 folio가 전혀 변하지 않는 것도 아닙니다. reclaim, migration, 다른 fault 등은 각자의 규칙으로 translation을 바꿀 수 있습니다. 또한 mmget()으로 mm을 붙잡은 reference만으로 VMA 구조까지 안정화되지 않습니다. 수명과 mutation의 직렬화는 다른 보장입니다.
현행 configuration에는 per-VMA lock도 있습니다. lock_vma_under_rcu()로 VMA를 찾아 read lock을 얻으면 일부 fault는 mm 전체의 mmap read lock 없이 진행할 수 있습니다. 실패하면 mmap-lock 기반 경로로 fallback해야 합니다. VMA metadata를 쓰는 쪽은 mmap write lock을 먼저 확보하고 필요한 VMA write 보호도 갖춥니다. 단순히 Maple Tree가 RCU 조회를 지원한다는 이유로 lookup 결과 pointer를 장기간 보유할 수는 없습니다.
이 보호 관계는 v6.18 process_addrs 문서에 구체적으로 정리되어 있습니다. VMA를 안정화한 뒤에도 page-table entry 변경에는 해당 level의 lock과 architecture helper가 필요합니다.
helper의 반환과 lock 소유권
mmap_read_lock()과 mmap_write_lock()은 기다려 획득하는 기본 helper입니다. killable variant는 fatal signal에 의해 대기가 중단될 수 있으므로 반환값을 검사해야 합니다. trylock은 즉시 성공 여부를 반환합니다. 실패한 trylock 뒤에 보호된 field를 읽거나 성공한 것처럼 unlock하면 안 됩니다.
mmap_write_downgrade()는 writer에서 reader로 바꾸어 읽기 작업을 이어갈 수 있게 합니다. 반대 방향의 upgrade를 같은 식으로 가정할 수는 없습니다. read lock을 놓고 write lock을 다시 얻었다면 그 사이 VMA가 바뀔 수 있으므로 재조회가 필요합니다. 현재 legacy stack expansion 경로도 이런 drop, write acquisition, 재조회와 downgrade 관계를 명시합니다.
mmap_assert_locked()와 write assertion은 개발 중 잘못된 호출을 드러내는 검사이지 lock을 대신 획득하는 함수가 아닙니다. mmap_lock_is_contended()는 기다리는 이용자가 있는지 보는 데 쓰이며, 긴 procfs 순회가 writer를 지나치게 지연시키지 않도록 양보할 시점을 판단하는 예가 있습니다. 관측 직후 상태가 바뀔 수 있다는 점은 일반적인 contention 검사와 같습니다.
mmap_read_unlock_non_owner()는 다른 context에서 해제를 수행하도록 설계된 호출 경로를 위한 helper입니다. 일반 호출자가 소유권 규칙을 무시해도 된다는 면허가 아닙니다. underlying rwsem뿐 아니라 lockdep과 추적이 해석하는 획득·해제 관계도 맞아야 합니다. helper 이름과 짝을 확인하는 일이 API 사용의 일부입니다.
retry는 같은 VMA에서 재시작한다는 뜻이 아닙니다
fault 중 file I/O나 lock 대기를 위해 mmap lock을 놓을 수 있습니다. VM_FAULT_RETRY 같은 반환 상태는 caller에게 이 사실을 전달합니다. 기존 pointer와 permission 검사를 그대로 재사용하면 lock을 놓은 사이 unmap되거나 속성이 바뀐 VMA를 사용할 수 있습니다. 재획득과 재조회가 필요한 이유입니다.
다만 모든 VM_FAULT_RETRY가 lock 해제를 뜻하는 것은 아닙니다. historical FAULT_FLAG_RETRY_NOWAIT 경로처럼 read lock을 유지하고 retry를 반환하는 예외도 있으므로, 반환 bit만이 아니라 입력 flags와 caller의 lock 상태 계약을 함께 읽어야 합니다. fault와 GUP의 구체적인 전환에서는 이 예외와 조건부 재획득을 구별합니다.
mmap()·munmap()·mprotect()는 구조 변경의 대표적인 writer입니다. fork는 부모 VMA와 CoW 준비를 함께 조율하고, GUP는 page를 얻는 동안 fault를 유발하거나 lock을 넘겨받는 API 계약을 가집니다. mlock은 VMA flag 변경과 population을 구분하고, madvise는 advice 종류에 따라 metadata 변경 또는 범위 순회를 수행합니다. 모든 syscall 이름에 하나의 고정된 read/write lock 모드를 붙이면 내부 단계의 차이를 놓칩니다.
procfs, core dump, OOM, NUMA policy와 migration도 같은 mm에 접근합니다. /proc에서 memory 정보를 읽는 일도 writer와 경쟁할 수 있고, OOM reaper가 mapping을 제거하는 동안 ordinary fault가 그 mm을 안정된 것으로 취급해서는 안 됩니다. 이런 경로가 모이기 때문에 mmap lock의 경합은 page fault 수만으로 설명되지 않습니다.
page_table_lock과 sequence의 역할
mm->page_table_lock은 상위 table의 게시나 split lock이 없는 구성의 entry 변경 등을 보호합니다. PMD/PTE split lock을 사용하면 더 작은 범위의 lock을 선택할 수 있습니다. 어느 architecture, 어느 level, kernel mapping인지 user mapping인지에 따라 실제 lock helper가 달라지므로 spinlock 하나의 이름만으로 모든 entry를 덮을 수는 없습니다.
이 lock은 역사적으로 다른 mm field의 짧은 갱신에도 사용되었습니다. 예전 stack expansion은 mmap read lock 상태에서 VMA 경계를 바꾸는 경로와 gap 요약 갱신을 별도 spinlock으로 직렬화했습니다. 현재의 stack expansion과 Maple Tree 설명에 그 오래된 red-black subtree 갱신 규칙을 그대로 가져와서는 안 됩니다. anonymous reverse mapping의 연결 준비처럼 별도의 공유 상태를 조율하는 경우도 각각의 호출 경로를 확인해야 합니다.
write_protect_seq는 fork 등의 write-protection이 진행되는 동안 GUP가 오래된 writable 판단으로 page를 pin하지 않게 조율하는 sequence입니다. 이것만 얻으면 모든 PTE write가 배타적으로 직렬화되는 일반 spinlock이 아닙니다. sequence를 읽고 재검증하는 쪽의 규칙까지 포함해야 보장이 성립합니다.
page-table lock을 풀었다고 TLB가 자동으로 갱신되는 것도 아닙니다. software writer의 직렬화와 CPU의 cached translation 무효화는 별도로 수행해야 합니다. lock과 reference와 TLB 처리를 하나의 '동기화 완료'라는 말로 합치지 않는 것이 중요합니다.
historical CONFIG_DEBUG_VM_RB의 validate_mm()·browse_rb()도 stack expansion과 동시에 gap을 검사할 수 있어 page_table_lock을 사용했습니다. read semaphore끼리는 서로 배제되지 않으므로, read-side에서 VMA 경계를 바꾸던 구현에는 별도의 직렬화가 필요했습니다. v6.0 __anon_vma_prepare()는 mmap read lock 아래 anon_vma를 준비하고, anon_vma write lock과 page_table_lock 아래 아직 publication되지 않았는지 다시 확인하여 연결합니다. 경쟁자가 먼저 연결했으면 자신이 준비한 임시 자원을 정리합니다. 이 사례는 'read lock이 있으니 모든 field가 불변'이라는 가정을 반박하지만, 현재 모든 VMA 변경에 같은 옛 lock 순서를 적용하라는 뜻은 아닙니다.
core dump의 mode와 filter
mm flags에는 locking과 관계없는 address-space policy도 있습니다. v6.18의 mm_flags_t는 전용 helper를 통해 접근하는 bitmap 표현이므로 예전의 raw word 연산을 모든 field 접근의 모범으로 가져오지 않습니다. dumpable mode는 하위 두 bit에 표현하는 값이며 SUID_DUMP_DISABLE, SUID_DUMP_USER, SUID_DUMP_ROOT의 0, 1, 2를 독립 flag bit처럼 OR하는 것이 아닙니다. get_dumpable()과 set_dumpable()이 이 encoding을 다룹니다. PR_SET_DUMPABLE로 user가 설정할 수 있는 값과 credential 변화 및 suid_dumpable 정책도 구분해야 합니다.
core dump filter는 anonymous private/shared, mapped private/shared, ELF header, hugetlb private/shared와 DAX private/shared 범주를 고릅니다. 이 filter는 VMA의 모든 실제 상태를 한 bit로 판정하는 단순 whitelist가 아닙니다. vma_dump_size()는 special mapping, VM_DONTDUMP, DAX와 hugetlb, I/O mapping 등을 정해진 순서로 검사하며 file-backed private VMA에 anonymous copy가 있는 경우도 고려합니다.
특히 '이름이 있는 VMA는 항상 dump된다'는 일반화는 피해야 합니다. v6.18 always_dump_vma()는 gate mapping, vm_ops의 name callback과 architecture의 special mapping 이름을 확인합니다. user가 붙인 arbitrary anonymous VMA name과 같은 조건이 아닙니다. 이 special 예외는 일반적인 VM_DONTDUMP 검사보다 먼저 적용됩니다.
ELF-header filter는 file offset 0의 읽기 가능한 mapping에서 첫 page를 남겨 실행 이미지 식별을 돕습니다. 실행 permission이나 ELF magic을 판정하는 세부 조건도 있으며 VMA 전체를 무조건 dump하는 flag가 아닙니다. dumpable 여부, range 선택, 출력할 길이를 서로 다른 단계로 보아야 합니다.
기본 filter 역시 범주 전체를 무조건 켜는 값이 아닙니다. v6.18 MMF_DUMP_FILTER_DEFAULT는 anonymous private/shared와 hugetlb private를 기본으로 포함하고, ELF header는 CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS에 따라 추가합니다. user의 /proc/PID/coredump_filter 선택은 이 초기값을 바꿀 수 있습니다. named special mapping과 ordinary anonymous name을 구별하는 것만큼, 기본 filter와 현재 mm의 선택을 구별하는 것도 중요합니다.
mm flag는 현재 수량표가 아닙니다
KSM의 MMF_VM_MERGEABLE과 THP 관련 flag는 mm이 해당 subsystem과 관계되는 상태입니다. 특정 VMA의 VM_MERGEABLE, VM_HUGEPAGE와 같은 객체의 같은 bit가 아닙니다. uprobes의 MMF_HAS_UPROBES와 MMF_RECALC_UPROBES도 instrumentation 상태를 추적합니다.
OOM 관련 상태 역시 구별해야 합니다. MMF_OOM_SKIP은 해당 mm을 OOM 판단·reaping에서 더 다룰 필요가 없는 상태를 표시하는 데 쓰이고, MMF_UNSTABLE은 reaping 등으로 memory 접근이 안정적이지 않음을 나타냅니다. 과거 MMF_OOM_VICTIM이라는 식별자를 현행 bit 목록에 그대로 대입하면 안 됩니다. v6.18에는 MMF_OOM_REAP_QUEUED 등 현재 이름과 역할을 확인해야 합니다.
THP disable 표현도 변했습니다. v6.18에는 MMF_DISABLE_THP_COMPLETELY와 MMF_DISABLE_THP_EXCEPT_ADVISED가 구분됩니다. 과거의 단일 MMF_DISABLE_THP를 현재 구조와 동일하다고 설명하지 않습니다. VMA별 advice와 process 수준 policy가 함께 최종 THP 가능성을 결정합니다.
MMF_MULTIPROCESS는 mm 공유에 관한 상태이며 그 bit 하나가 현재 task 개수의 정확한 counter는 아닙니다. MMF_HAS_PINNED는 FOLL_PIN 경로를 거쳤다는 보수적 이력으로, 이후 unpin해도 mm 수명 동안 지우지 않는다는 주석이 있습니다. 이 bit가 1이라고 현재 pin 수를 계산할 수 없고, 0/1 검사만으로 개별 folio의 안전한 CoW 여부를 완전히 결정할 수도 없습니다.
lock은 지금의 동시 접근을 조율하고, flag는 의미와 policy를 기술하며, reference는 수명을 붙잡습니다. field가 모두 같은 mm 안에 있다는 이유로 이 셋을 같은 종류의 보장으로 읽지 않는 것이 address-space 코드를 이해하는 출발점입니다.
huge zero 관련 이름도 version을 구별합니다. historical MMF_HUGE_ZERO_PAGE에 대응하는 current 이름은 v6.18 MMF_HUGE_ZERO_FOLIO이며, global huge zero folio를 이용한 이력을 나타냅니다. 일반 MM_ANONPAGES의 수량이나 지금 mapping된 모든 huge page의 개수를 뜻하지 않습니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 4의 address-space locking과 flags 설명.
- Linux v6.18 process_addrs: VMA와 page-table locking 계약
- Linux v6.18 mmap_lock.h: helper와 반환 규칙
- Linux v6.18 mm_types.h: mm flag 정의
- Linux v6.18 coredump.c: dump range의 실제 분기
- Linux v6.18 sched/coredump.h: dumpable mode
- Linux v6.18 mmap.c: stack 확장과 lock 재획득