process의 memory를 읽어 화면에 출력하는 일은 수동적인 관측처럼 보입니다. 하지만 경로 이름을 찾는 동안 VMA가 사라질 수 있고, 출력 buffer를 user memory로 복사하다가 다시 fault할 수도 있습니다. CPU exception을 처리하는 도중에는 잠들어 기다리는 것 자체가 허용되지 않을 수도 있습니다.
이 차이를 이해하기 위해 공식 Linux v6.0의 x86-64와 procfs 경로를 살펴보겠습니다. 동일한 pathname이나 함수 이름이 현재 kernel에서도 모든 lock 범위를 그대로 유지한다는 뜻은 아닙니다. 아래의 관계 설명은 전체 exception·VFS 구현의 line별 해설이 아니라 VMA 보호의 경계에 초점을 둡니다.
user 복귀가 곧 core dump는 아닙니다
system call, IRQ와 debug exception은 서로 다른 진입점이지만 user mode로 돌아가기 전에 pending work를 처리하는 경로에 합류할 수 있습니다. v6.0 entry/common.c의 exit_to_user_mode_prepare()는 work flag가 있을 때 loop를 실행하고, loop는 IRQ를 허용한 상태에서 필요한 일을 수행한 뒤 다시 flag를 읽습니다.
signal 처리는 _TIF_SIGPENDING뿐 아니라 _TIF_NOTIFY_SIGNAL 조건에서도 arch_do_signal_or_restart()로 이어집니다. 이 함수는 get_signal()이 실제 전달할 signal을 고르도록 합니다. 그 결과에는 user handler로 전달하는 경우와 syscall restart도 있습니다. core dump는 signal의 기본 fatal 처리가 선택되고 sig_kernel_coredump() 조건을 만족하는 별도 분기입니다. get_signal()의 fatal 경로는 이 조건 안에서 do_coredump()를 호출합니다.
다음은 구현코드가 아닌 조건부 호출관계입니다.
syscall/IRQ/debug return to user
exit_to_user_mode_prepare -> exit_to_user_mode_loop
arch_do_signal_or_restart -> get_signal
fatal default action + sig_kernel_coredump
do_coredump -> dump_vma_snapshot
x86의 ret_from_fork도 user 복귀 쪽에서는 syscall_exit_to_user_mode()를 사용합니다. 일반 IRQ의 irqentry_exit()는 user_mode(regs)일 때 user 복귀 경로를 고릅니다. exc_int3()는 kernel text patch용 처리를 먼저 거르고 user·kernel 경로를 나누며, user debug의 exc_debug_user()도 user 복귀 함수를 호출합니다. SEV의 user exc_vmm_communication()은 처리하지 못한 예외를 SIGBUS로 바꿀 수 있고, debug 예외에는 별도 우회가 있습니다. 이 연결들은 core dump의 가능한 상위 경로이지 모든 IRQ·breakpoint가 VMA writer를 획득한다는 뜻이 아닙니다. traps.c, sev.c, entry_64.S가 각 분기를 보여 줍니다.
snapshot은 VMA pointer를 그대로 보관하지 않습니다
v6.0 dump_vma_snapshot()은 killable mmap writer를 얻습니다. 당시 source의 설명은 read lock 아래 stack expansion이 경계를 바꿀 수 있으므로 writer가 필요하다는 것입니다. allocation에 실패하면 writer를 반환하고 snapshot도 실패합니다.
성공 경로는 VMA별로 필요한 경계·flags·offset 등의 metadata를 별도 배열에 저장하고, 연결된 file이 있으면 reference를 얻습니다. writer를 놓은 뒤에는 그 배열과 file reference를 이용합니다. 따라서 snapshot은 live VMA pointer 목록을 잠깐 모아 두었다가 lock 없이 순회하는 방식이 아닙니다. file 내용의 후속 조사와 전체 dump I/O까지 mmap writer를 계속 잡아야 한다는 의미도 아닙니다.
이 설명은 v6.0의 정확한 lock 선택입니다. stack의 규칙이 변했다는 일반 지식만으로 특정 최신 tag의 dump_vma_snapshot()이 reader로 바뀌었다고 추측하지 않습니다. 어떤 상태를 snapshot하는지와 실제 함수가 무슨 mode를 얻는지는 해당 source에서 각각 확인해야 합니다.
진단 출력은 기다리지 못할 수도 있습니다
fault나 trap에서 출력하는 executable 이름도 VMA lookup이 필요합니다. 그러나 v6.0 print_vma_addr()는 blocking read lock이 아니라 mmap_read_trylock()을 사용합니다. atomic context일 수 있으므로 실패하면 이름 출력을 포기합니다. 경로용 임시 buffer도 GFP_NOWAIT로 얻습니다. 로그에 file 이름이 없다고 그 주소의 VMA가 없었다는 뜻은 아닙니다.
호출하는 쪽도 조건부입니다. show_signal()은 unhandled-signal 출력 설정과 실제 unhandled 여부, rate limit을 검사합니다. page fault의 show_signal_msg()도 무조건 실행되는 정상 fault 후처리가 아닙니다. rt_sigreturn()의 잘못된 frame은 signal_fault()로 이어질 수 있고, general protection의 fixup_iopl_exception()은 IOPL emulation 설정과 opcode 등의 조건이 맞을 때만 경고와 보정을 수행합니다. 보정에 성공하면 signal로 끝나지 않을 수도 있습니다.
divide error·overflow·invalid TSS·segment-not-present·stack-segment·coprocessor-segment-overrun 같은 trap은 do_error_trap()·do_trap() 계열로 모입니다. invalid opcode도 별도 handler를 거쳐 이 계열에 연결됩니다. bounds와 alignment check에는 각자의 notifier·user/kernel·보정 조건이 있습니다. 이 다양한 예외의 공통점은 “모두 page fault다”가 아니라, 선택된 진단이 결국 print_vma_addr()의 짧은 trylock lookup을 사용할 수 있다는 것입니다. v6.0 traps.c의 공통 함수와 각 진입점을 함께 보아야 합니다.
page fault에서는 이미 획득한 lock을 가진 오류 경로도 따로 처리합니다. __bad_area()는 mmap read lock을 먼저 반환한 뒤 __bad_area_nosemaphore()로 갑니다. 아직 획득하지 않은 상태의 bad_area_nosemaphore()는 그 반환을 반복하지 않습니다. 이후 진단에서 read trylock을 새로 시도하는 것과 기존 fault lock의 해제를 구별해야 이중 unlock을 피할 수 있습니다.
maps의 iterator와 smaps rollup은 다르게 이어 갑니다
maps·smaps·numa_maps의 seq iterator는 open 시점에 파일을 열었다고 읽기 전체의 VMA를 영구 고정하지 않습니다. m_start()는 task와 유효한 mm 사용 reference를 얻고 killable read lock을 획득한 다음 위치에 해당하는 VMA를 찾습니다. m_stop()은 policy reference와 read lock, mm·task reference를 짝지어 반환합니다. lock 획득 실패와 이미 끝난 task의 경우에도 성공 경로처럼 unlock하지 않습니다.
smaps_rollup은 긴 통계 수집 중 writer 경합을 만나면 read lock을 양보할 수 있습니다. show_smaps_rollup()은 마지막 VMA의 끝을 저장한 뒤 lock을 다시 얻고 find_vma(last_vma_end - 1)로 조회합니다. 예전 VMA가 없어졌으면 다음 VMA에서 계속하고, 같은 VMA가 남아 있으면 다음으로 진행하며, 더 이상 없으면 끝납니다. VMA가 합쳐져 저장한 끝을 넘어선다면 새로 늘어난 뒷부분만 통계에 더합니다. 예전 pointer의 vm_next를 무조건 따라가는 것과 다릅니다.
pagemap_read()도 read lock 아래 page table을 조사하지만 큰 읽기 전체를 한 번에 고정하지 않습니다. chunk를 walk하고 unlock한 뒤 user buffer로 복사합니다. PFN 공개에는 capability 검사도 별도로 있습니다. 반면 clear_refs_write()는 이 tag에서 writer를 사용합니다. soft-dirty 조정에는 VMA 상태, page-table walk, MMU notifier와 TLB 처리까지 필요합니다. 읽기용 파일인지 쓰기용 파일인지가 아니라 내부에서 어떤 상태를 안정화하거나 바꾸는지가 lock 계약을 결정합니다.
map_files는 이름과 현재 mapping을 다시 대조합니다
/proc/pid/map_files의 이름은 start-end 형식이지만 한 번 만들어진 dentry가 해당 VMA를 계속 소유하는 것은 아닙니다. proc_map_files_lookup()은 권한과 이름을 검사하고 mm read lock 아래 정확히 맞는 VMA와 file을 확인합니다. map_files_get_link()는 다시 lookup하고 반환할 path에 path_get()을 수행한 뒤 lock을 놓습니다. map_files_d_revalidate()도 RCU lookup을 그대로 처리하지 않고 현재 mm의 exact range를 재검사합니다.
directory 순회는 또 다릅니다. proc_map_files_readdir()은 read lock 아래 경계·mode를 수집한 뒤 unlock하고 dentry를 생성합니다. user 쪽 출력 과정이 fault할 수 있어 mmap lock 순서 문제가 생길 수 있기 때문입니다. 따라서 proc_map_files_instantiate()가 lookup과 readdir 양쪽에서 호출된다는 관계를, 양쪽 모두 read lock을 잡은 채 instantiate한다는 뜻으로 읽으면 안 됩니다. 같은 callee라도 caller의 보호 상태는 달라집니다.
memory 내용과 경계 metadata의 보호도 다릅니다
/proc/pid/mem의 read·write는 mem_rw()에서 access_remote_vm()으로 이어집니다. cmdline은 proc_pid_cmdline_read()·get_task_cmdline()·get_mm_cmdline()으로, environ은 environ_read()로 들어갑니다. 문자열의 주소 범위를 복사하는 단계와 그 주소의 실제 byte를 읽는 단계는 별도입니다. v6.0 proc base.c는 arg·env 경계를 mm->arg_lock으로 짧게 보호하고 실제 접근은 remote-memory helper에 맡깁니다. cmdline 끝이 덮어쓰인 setproctitle 사례에는 get_mm_proctitle() 경로도 있습니다.
__access_remote_vm()은 mmap read lock을 얻고 page를 확보하여 복사한 뒤 reference를 반환합니다. 이 역사적 호출은 GUP에 locked = NULL을 전달하여 해당 구간에서 VMA를 사용하는 계약을 유지합니다. 설정에 따라 일반 page를 얻지 못한 IO·PFN mapping은 vm_ops->access를 시도할 수도 있습니다. 요청한 byte 수 전체가 아니라 실제 옮긴 byte 수가 반환되므로 부분 읽기·쓰기를 고려해야 합니다.
mm reference, VMA lock, backing page reference와 path reference는 서로 대체재가 아닙니다. procfs와 진단 함수는 이들을 필요한 구간에만 붙잡고, 잠들 수 없는 경로에서는 정보 출력을 포기하기도 합니다. 관측 결과를 해석할 때에도 그 짧은 보호 범위를 결과 전체의 원자적 snapshot으로 확대하지 않는 것이 중요합니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 4 §4.3.4.6.
- Linux v6.0 entry/common.c, signal.c, coredump.c: 조건부 user 복귀와 snapshot.
- Linux v6.0 task_mmu.c, base.c, memory.c: procfs와 remote access의 수명 계약.