리눅스에서 프로세스 메모리를 친절하게 탐험하기

4 hours ago 2

  • 리눅스의 프로세스 메모리 구조를 실제 동작 수준에서 설명하며, 가상 주소 공간과 물리 메모리 간의 관계를 단계별로 해설
  • 페이지 테이블, VMA, mmap, page fault, CoW 등 핵심 메커니즘을 중심으로 프로세스가 메모리를 어떻게 소유하고 접근하는지 구체적으로 설명
  • /proc 파일 시스템을 통해 프로세스별 메모리 상태를 관찰하는 방법과, pagemap, kpageflags 등 고급 진단 도구의 역할을 소개
  • Transparent Huge Pages(THP) , userfaultfd, PAGEMAP_SCAN 등 최신 커널 기능을 통한 성능 최적화와 사용자 공간 더티 트래킹 기법을 다룸
  • Meltdown 대응 PTI, TLB 플러시, W^X 정책 등 보안 및 성능 관련 커널 설계 원리를 함께 설명하여, 리눅스 메모리 관리의 전체적 이해 제공

프로세스 메모리의 기본 구조

  • 프로그램이 실행되면 거대한 연속 메모리가 있는 것처럼 보이지만, 실제로는 리눅스 커널이 페이지 단위로 동적으로 구성
    • CPU는 페이지 테이블을 조회해 가상 주소를 물리 프레임으로 변환
    • 매핑이 없으면 페이지 폴트(page fault) 발생, 커널이 새 페이지를 할당하거나 오류를 반환
  • 물리 RAM이 부족하면 커널은 사용하지 않은 페이지를 디스크로 옮기거나 파일 페이지를 제거해 공간 확보
  • /proc은 커널이 메모리상에 구성한 가상 파일 시스템으로, 프로세스와 커널 상태를 파일 형태로 노출

주소 공간과 VMA

  • 각 프로세스는 하나의 주소 공간 객체를 가지며, 내부는 여러 VMA(Virtual Memory Area) 로 구성
    • VMA는 동일한 권한(R/W/X)과 동일한 백엔드(익명 메모리 또는 파일)를 가진 연속 주소 범위
  • 페이지 테이블은 하드웨어가 참조하는 구조로, 가상 페이지와 물리 페이지 간 매핑 정보(PTE) 를 저장
  • 주소 공간 변경은 세 가지 시스템 호출로 수행
    • mmap: 새 영역 생성
    • mprotect: 권한 변경
    • munmap: 매핑 제거
  • 페이지는 4KiB 기본 단위, 일부 시스템은 2MiB·1GiB의 큰 페이지도 지원

/proc/self/maps로 보는 메모리 구성

  • cat /proc/self/maps 명령으로 프로세스의 메모리 맵을 확인 가능
    • 실행 파일의 코드·데이터·bss, 힙, 익명 매핑, 공유 라이브러리, 스택 등이 표시
  • [vdso]와 [vvar] 영역은 커널이 매핑한 고속 시스템 호출용 코드와 데이터

mmap의 동작 원리

  • mmap은 실제 메모리 할당이 아니라 주소 공간에 대한 약속을 기록
    • 페이지는 첫 접근 시점에 할당
  • 파일 매핑 시 offset은 페이지 정렬되어야 하며, 파일 끝을 넘는 접근은 SIGBUS 발생
  • MAP_SHARED는 파일에 직접 반영, MAP_PRIVATE는 쓰기 시 복사(CoW) 로 독립 페이지 생성
  • MAP_FIXED_NOREPLACE는 지정 주소에 이미 매핑이 있으면 실패하도록 하여 안전성 확보

첫 번째 접근과 페이지 폴트

  • 새 매핑에 첫 접근 시 CPU가 페이지 테이블을 찾지 못하면 페이지 폴트 발생
    • 커널은 주소 유효성, 접근 권한, 존재 여부를 검사
    • 익명 매핑이면 0으로 채운 새 페이지를 할당, 파일 매핑이면 페이지 캐시에서 읽음
  • minor fault는 데이터가 이미 RAM에 있을 때, major fault는 디스크 I/O가 필요한 경우
  • 스택은 가드 페이지로 보호되어, 너무 아래쪽 접근 시 SIGSEGV 발생

fork()와 MAP_PRIVATE의 Copy-on-Write

  • fork 시 부모와 자식은 동일한 물리 페이지를 공유, 모두 읽기 전용으로 표시
    • 쓰기 시점에만 새 페이지를 복사하여 독립 유지
  • MAP_PRIVATE 파일 매핑도 동일 원리로 작동
  • 관련 옵션
    • vfork: 부모 주소 공간 공유
    • clone(CLONE_VM): 스레드 생성
    • MADV_DONTFORK, MADV_WIPEONFORK: 자식 프로세스에서 매핑 제외 또는 0으로 초기화

권한 변경과 TLB 무효화

  • mprotect로 페이지 권한 변경 시 커널은 VMA 분할 및 페이지 테이블 수정, 이후 TLB 무효화 수행
  • W^X 정책에 따라 페이지는 동시에 쓰기·실행 불가
  • TLB(Translation Lookaside Buffer)는 최근 주소 변환 캐시로, 무효화 시 잠시 지연 발생

/proc을 통한 상세 관찰

  • /proc/<pid>/maps, smaps, smaps_rollup으로 영역별 권한·RSS·HugePage 사용량 확인
  • /proc/<pid>/pagemap은 페이지 단위 상태(존재, 스왑, PFN 등)를 제공하나, PFN은 일반 사용자에게 비공개
  • /proc/kpagecount, /proc/kpageflags는 PFN별 매핑 수와 페이지 속성(익명, 파일, dirty 등) 표시
  • mincore, SEEK_DATA/SEEK_HOLE로 희소 파일의 데이터/홀 구간 식별 가능
  • PAGEMAP_SCAN과 userfaultfd를 결합해 사용자 공간 더티 트래킹 구현 가능

Transparent Huge Pages (THP)와 mTHP

  • THP는 자주 접근하는 메모리를 자동으로 큰 페이지(2MiB 등)로 묶어 TLB 효율 향상
    • khugepaged 스레드가 인접 페이지를 병합
  • mTHP는 16KiB·64KiB 등 다양한 크기의 가변 대형 페이지(folio) 지원
  • /proc/self/smaps의 AnonHugePages, FilePmdMapped로 사용 여부 확인
  • /sys/kernel/mm/transparent_hugepage/에서 시스템 전체 설정 관리
  • MADV_HUGEPAGE, MADV_NOHUGEPAGE로 영역별 제어 가능

사용자 공간 더티 트래킹

  • userfaultfd와 PAGEMAP_SCAN을 이용해 변경된 페이지만 복사 가능
    • 커널이 한 번의 원자적 연산으로 스캔과 쓰기 보호 수행
    • 스냅샷, 라이브 마이그레이션 등에서 효율적

TLB 플러시 메커니즘

  • x86에서 TLB 무효화는 두 가지 방식
    • INVLPG: 단일 페이지 무효화
    • 페이지 테이블 루트 재로딩으로 전체 플러시
  • PCID와 INVPCID는 프로세스별 TLB 태그 관리로 불필요한 플러시 감소
  • tlb_single_page_flush_ceiling은 커널이 페이지 단위 vs 전체 플러시를 선택하는 임계값

Meltdown 대응: Page Table Isolation (PTI)

  • Meltdown은 투기 실행 중 커널 데이터가 캐시를 통해 노출될 수 있는 취약점
  • 리눅스는 PTI(Page Table Isolation) 로 사용자·커널 주소 공간을 분리
    • 진입 시 CR3 전환으로 커널 전용 페이지 테이블 사용
    • PCID를 활용해 TLB 플러시 최소화
  • 기본적으로 활성화되어 있으며, nopti로 비활성화 가능

커널의 안전한 매핑 변경 절차

  • 매핑 변경 시 순서
    1. 캐시 규칙 처리
    2. 페이지 테이블 수정
    3. TLB 무효화
  • 커널 내부 매핑(vmap, vmalloc)도 I/O 전후에 캐시·TLB 동기화 수행
  • 일부 아키텍처는 코드 복사 후 명령 캐시 플러시 필요

x86의 스택과 호출 구조

  • 64비트 모드에서 RIP, RSP, RBP 레지스터 사용, 스택은 하향 성장
  • System V AMD64 ABI에 따라 인자 전달은 RDI, RSI, RDX, RCX, R8, R9, 반환값은 RAX
  • 사용자 모드는 ring 3, 커널은 ring 0, 시스템 콜과 인터럽트는 게이트를 통해 전환

오류 상황과 진단

  • mmap → EINVAL: 파일 오프셋 정렬 오류
  • mmap → ENOMEM: 가상 공간 부족 또는 overcommit 제한
  • 파일 매핑 접근 시 SIGBUS: EOF 초과 접근
  • mprotect(PROT_EXEC) → EACCES: noexec 마운트 또는 W^X 정책
  • fork() 후 RSS 증가: CoW로 인한 페이지 복사
  • MAP_FIXED로 기존 매핑 덮어씀 → MAP_FIXED_NOREPLACE 권장

실무용 체크리스트

  • 즉시 메모리 확보: mmap + PROT_READ|PROT_WRITE + MAP_PRIVATE|MAP_ANONYMOUS
  • 코드 생성 시: W^X 유지, mprotect(PROT_READ|PROT_EXEC)
  • 파일 매핑 시: offset 페이지 정렬, EOF 초과 접근 금지
  • 페이지 폴트 많을 때: MADV_WILLNEED 또는 사전 접근
  • 메모리 사용 분석: /proc/<pid>/smaps_rollup → /proc/<pid>/maps
  • 대형 프로세스 fork: CoW 고려, 자식에서 exec 사용
  • 지연 민감 환경: THP/mTHP, mlock, TLB 동작 관찰

Read Entire Article