mmap()은 주소를 정하고, 접근 권한을 정하며, 수정 내용을 누구와 공유할지도 정합니다. 이 세 가지를 모두 “memory allocation 옵션”으로 묶으면 위험한 오해가 생깁니다. 주소가 같다고 같은 객체인 것은 아니며, writable VMA라고 처음부터 writable PTE가 설치되는 것도 아닙니다.
주소와 offset은 서로 다른 좌표입니다
addr가 NULL이면 kernel이 주소를 고릅니다. NULL이 아니어도 MAP_FIXED가 없으면 hint일 뿐이며, 실제 반환 주소는 달라질 수 있습니다. length는 양수여야 하고 실제 mapping 경계는 page 단위로 다룹니다. file offset도 page-aligned여야 하지만 파일의 현재 EOF 안에만 있어야 하는 것은 아닙니다. mapping 생성의 유효성과 나중에 backing을 제공할 수 있는지는 별도 검사입니다.
PROT_NONE은 접근을 허용하지 않는 상태이며 READ·WRITE·EXEC를 모두 끈 경우입니다. 나머지 protection의 hardware 관계는 architecture와 personality에 영향을 받습니다. file descriptor는 mapping 후 닫아도 됩니다. VMA가 보관하는 file reference가 열린 descriptor slot의 수명과 분리되기 때문입니다. anonymous mapping에서는 portable한 사용을 위해 fd를 -1, offset을 0으로 지정합니다.
반환 값은 mapping의 시작 주소이며 실패 표시는 MAP_FAILED, 즉 (void *)-1입니다. 실패 원인은 errno로 확인합니다. NULL과 비교하는 allocation 관용구를 그대로 가져오면 안 됩니다. 주소 0의 mapping은 보통 최소 주소 정책으로 제한되지만, API의 실패 sentinel 자체가 0인 것은 아닙니다. PROT_NONE으로 확보한 범위는 주소 reservation이나 guard page로 사용할 수 있습니다. 접근 권한이 없다는 것과 VMA가 없어 다른 thread가 그 주소를 자유롭게 차지할 수 있다는 것은 다릅니다.
PRIVATE는 snapshot 저장소가 아닙니다
MAP_PRIVATE file mapping은 read에서 page cache를 참조할 수 있지만 write에서는 anonymous copy를 만듭니다. 그 수정은 원래 파일로 writeback하지 않습니다. 그러나 이것이 외부 파일 변경과 truncate로부터 완전히 독립된 snapshot이라는 뜻은 아닙니다. VMA는 file mapping 관계를 유지하므로 truncate invalidation의 대상이 될 수 있습니다. 파일 전체 삭제와 열려 있는 inode의 truncate도 같은 사건이 아닙니다.
일반적인 page-cache file에서 truncate로 새 EOF를 완전히 넘어간 범위는, 이미 CoW한 anonymous PTE까지 제거될 수 있습니다. 그 수정분은 원 file에 저장된 것이 아니므로 잃을 수 있고, 축소된 범위에 다시 접근하면 SIGBUS가 발생할 수 있습니다. 이후 file을 다시 늘려도 사라진 private 수정분이 되살아나는 것은 아닙니다. 이 동작을 보통의 file overwrite가 모든 private copy를 초기화한다는 뜻으로 확대해서는 안 됩니다. Linux v6.18의 truncate와 private CoW 처리
MAP_SHARED는 같은 backing을 mapping한 다른 process에 변경을 보일 수 있게 합니다. 파일 변경을 저장장치에 영속화하는 시점은 별도 sync 문제입니다. MAP_ANONYMOUS | MAP_SHARED도 가능한데, kernel 내부에서는 shmem backing과 file fault machinery를 사용합니다. application이 fd 없이 요청했다는 사실과 kernel의 vma_is_anonymous() 판정은 일치하지 않을 수 있습니다.
MAP_SHARED_VALIDATE는 shared mapping에서 알 수 없는 flags를 거부하는 interface입니다. legacy MAP_SHARED는 호환성을 위해 일부 알 수 없는 flags를 무시합니다. MAP_SYNC처럼 명시적 지원이 필요한 기능은 VALIDATE를 요구하며 DAX 지원 조건도 만족해야 합니다. 일반 page-cache file의 동기 writeback 스위치가 아닙니다.
지원되는 DAX mapping의 MAP_SYNC는 write fault에서 필요한 filesystem metadata의 영속성 조건과 관계합니다. application의 모든 store가 자동으로 persistent해진다는 뜻은 아니며, persistent-memory data를 위한 적절한 CPU flush·ordering 작업은 여전히 필요합니다. mapping의 crash-consistency 계약과 일반 msync()·page-cache writeback의 계약을 섞어서는 안 됩니다.
고정 주소, population, residency는 독립된 선택입니다
MAP_FIXED는 기존 mapping을 덮어쓸 수 있습니다. 다른 thread가 주소 공간을 바꾸는 동안 /proc에서 빈 공간을 본 뒤 FIXED로 mapping하는 방식은 reservation이 아닙니다. 충돌하면 실패해야 한다면 MAP_FIXED_NOREPLACE의 의미가 맞습니다. 구형 kernel과 호환해야 할 때에는 요청 주소와 반환 주소도 비교해야 합니다.
MAP_POPULATE는 PTE와 backing을 미리 준비하려 하지만 그 과정의 실패가 곧 mmap 실패는 아닙니다. 이후 fault가 없다는 보장을 주지 않습니다. MAP_NONBLOCK과 함께 지정하면 population이 취소되는 legacy 동작도 있습니다. MAP_LOCKED 역시 best-effort prefault와 residency 정책을 함께 건드리며, 성공적으로 끝난 명시적 mlock()과 동일한 초기화 보장으로 받아들이면 안 됩니다.
reserve와 특수 flags의 한계
MAP_NORESERVE는 writable private mapping의 commit accounting과 관계합니다. strict overcommit 정책은 이를 무시할 수 있으며, huge page reservation은 별도 규칙입니다. reserve를 생략한다고 실제 RAM이 생기지는 않습니다. mapping 성공 뒤 fault 시점의 allocation 실패 가능성은 남습니다. Committed_AS는 resident page 수가 아니라 정책상 청구된 commit 양입니다.
구현에서는 우선 허용되는 요청에 VM_NORESERVE를 반영하고, mmap_region()이 accountable_mapping()으로 commit 청구 대상을 가립니다. 일반적으로 private writable이며 hugetlb와 별도 reservation 예외가 아닌 mapping이 대상입니다. 청구를 통과하면 VM_ACCOUNT로 그 사실을 남겨 나중에 정확히 반납합니다. VM_NORESERVE 때문에 이 청구 경로를 건너뛴다는 것과 모든 security 검사를 건너뛴다는 것은 다릅니다. 앞서 수행하는 security_mmap_file()과 이 단계의 security_vm_enough_memory_mm()는 서로 다른 검사입니다. strict 정책에서는 일반 mapping의 NORESERVE 요청을 그대로 존중하지 않지만 hugetlb에는 자체 reservation 의미가 남습니다.
v6.18의 __vm_enough_memory()는 청구 대상에 먼저 commit을 더하고 정책을 검사합니다. OVERCOMMIT_ALWAYS는 이를 허용하며, OVERCOMMIT_GUESS는 이번 요청의 page 수가 전체 RAM과 swap의 합보다 큰지 검사합니다. 후자를 현재 free RAM이나 MemAvailable과의 비교로 읽으면 안 됩니다. NORESERVE로 비청구 대상이 된 mapping은 이 helper 경로와 그에 연결된 청구 검사를 건너뛰므로 해당 범위가 Committed_AS에 같은 방식으로 더해지지 않습니다. ALWAYS에서도 "허용된다"와 "청구하지 않는다"는 다른 말입니다. Linux v6.18의 __vm_enough_memory()
MAP_GROWSDOWN은 성장 가능한 VMA를 요청하며 guard gap과 stack limit을 없애지 않습니다. MAP_STACK만으로 같은 기능을 켜는 것도 아닙니다. MAP_32BIT는 x86-64에서 처음 2 GiB 안에 배치하려는 옵션이며 “32-bit 주소 공간 전체”와 다릅니다. FIXED와 함께 쓰면 무시됩니다. MAP_HUGETLB와 huge-size encoding은 지원 page size와 reservation 조건이 따로 있습니다.
downward growth는 VMA 아래쪽의 fault에 반응하는 규칙이며 일반 anonymous mapping을 무제한 stack으로 만드는 기능이 아닙니다. 이웃 VMA와의 guard gap, RLIMIT_STACK·address-space limit, growable VMA의 조건을 만족해야 합니다. guard의 기본값 256 pages는 4 KiB page에서 1 MiB이지만 모든 system의 고정 byte 수는 아닙니다. boot의 stack_guard_gap 설정과 page size에 따라 달라지고, upward-growing architecture에서는 보호할 방향도 반대입니다. 여러 thread의 stack은 보통 별도 anonymous mapping이므로 각 stack의 수명과 guard를 따로 관리합니다.
MAP_ANON은 MAP_ANONYMOUS의 alias입니다. hugetlb 크기의 예인 MAP_HUGE_2MB·MAP_HUGE_1GB는 각각 size의 base-2 logarithm을 MAP_HUGE_SHIFT 위치에 담는 encoding입니다. 이름이 정의되어 있다는 사실만으로 해당 크기의 huge page pool이나 권한이 확보되지는 않습니다. MAP_NONBLOCK 역시 파일 I/O 전체를 nonblocking으로 바꾸는 일반 계약이 아니라 population과 조합되는 flag입니다.
API bits와 VMA bits의 값이 같을 필요도 없습니다. RB-tree 계열의 calc_vm_flag_bits()는 MAP_GROWSDOWN을 VM_GROWSDOWN으로, MAP_LOCKED를 VM_LOCKED로, MAP_SYNC를 VM_SYNC로 변환하고 architecture별 변환 결과를 합칩니다. 내부 _calc_vm_trans()는 bit의 존재를 다른 bit 위치로 옮기는 연산을 최적화한 helper이지 별도의 mapping 정책이 아닙니다. 반면 FIXED나 POPULATE처럼 제어 흐름에서 직접 소비하는 flag도 있으므로 모든 MAP bit가 같은 이름의 VM bit로 영구 저장된다고 생각하면 안 됩니다.
MAP_UNINITIALIZED는 no-MMU 및 해당 configuration의 특수 기능입니다. 보통의 MMU system에서 anonymous page의 zero 초기화를 없애는 최적화 옵션이 아닙니다. MAP_FILE, MAP_DENYWRITE, MAP_EXECUTABLE은 현재 Linux에서 무시되는 호환 flags입니다. 이름이 그럴듯하다고 새 권한이나 정책이 생기지 않습니다.
v5.18 flag 변환의 전체 정의
calc_vm_flag_bits()의 반환 값은 MAP flag 전체를 저장한 복사본이 아닙니다. 실제로 변환하는 세 bit와 architecture 확장을 아래에서 확인할 수 있습니다. Linux v5.18 include/linux/mman.h 전체에는 다음 macro와 함수가 함께 있습니다. 전체 source 파일은 v5.18 코드 묶음에서 받으실 수 있습니다. 각 파일의 원 header와 license를 보존했으며, 한국어 해설과 upstream code의 license는 구분합니다. 이 묶음은 kernel 전체 build tree나 userspace 실행 예제가 아닙니다.
먼저 L133–L136의 _calc_vm_trans 전체입니다. 인자인 두 상수는 각각 단일 bit라는 전제가 있습니다. bit가 정의되지 않아 0인 configuration도 처리하므로, 이 식을 임의의 여러-bit mask 변환으로 일반화하면 안 됩니다.
/* SPDX-License-Identifier: GPL-2.0 */
/* Linux v5.18: include/linux/mman.h */
#define _calc_vm_trans(x, bit1, bit2) \
((!(bit1) || !(bit2)) ? 0 : \
((bit1) <= (bit2) ? ((x) & (bit1)) * ((bit2) / (bit1)) \
: ((x) & (bit1)) / ((bit1) / (bit2))))
공식 v5.18 include/linux/mman.h, L153–L160 · 전체 source 파일
/* SPDX-License-Identifier: GPL-2.0 */
/* Linux v5.18: include/linux/mman.h */
static inline unsigned long
calc_vm_flag_bits(unsigned long flags)
{
return _calc_vm_trans(flags, MAP_GROWSDOWN, VM_GROWSDOWN ) |
_calc_vm_trans(flags, MAP_LOCKED, VM_LOCKED ) |
_calc_vm_trans(flags, MAP_SYNC, VM_SYNC ) |
arch_calc_vm_flag_bits(flags);
}
arch_calc_vm_flag_bits는 architecture의 asm/mman.h가 override할 수 있습니다. 같은 header의 default는 다음과 같으며, architecture의 확장이 언제나 0이라는 뜻은 아닙니다.
/* SPDX-License-Identifier: GPL-2.0 */
/* Linux v5.18: include/linux/mman.h */
#ifndef arch_calc_vm_flag_bits
#define arch_calc_vm_flag_bits(flags) 0
#endif
FIXED·NOREPLACE·POPULATE처럼 do_mmap()의 제어 흐름에서 소비하는 bit는 이 반환 값에 없더라도 실제 정책에 영향을 줍니다. 따라서 flags의 의미를 이 함수 하나의 변환 결과만으로 판정하지 않습니다.
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 5, §5.0.2–5.0.3. interface와 version 조건은 mmap(2), kernel의 flag 해석은 Linux v6.18 mm/mmap.c를 참고하실 수 있습니다.