리눅스에서 프로세스 메모리를 친절하게 탐험하기
(0xkato.xyz)- 리눅스의 프로세스 메모리 구조를 실제 동작 수준에서 설명하며, 가상 주소 공간과 물리 메모리 간의 관계를 단계별로 해설
- 페이지 테이블, 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로 비활성화 가능
커널의 안전한 매핑 변경 절차
- 매핑 변경 시 순서
- 캐시 규칙 처리
- 페이지 테이블 수정
- 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 동작 관찰
Hacker News 의견
-
이런 짧은 설명글들이 정말 마음에 듦
이미 알고 있는 내용이라도, 읽는 동안 다시 한번 확인할 수 있어서 도움이 됨 -
“mmap, without the fog” 같은 문구를 보면, 뭔가 LLM이 공동 작성한 글처럼 느껴져서 괜히 불안하고 짜증이 남
- 글의 어조가 마치 Gemini에게 쉬운 설명을 부탁했을 때처럼 느껴짐
거기에 “without the fog” 같은 이상한 표현이 들어가 있어서 chatgpt가 같이 쓴 것 같은 인상임
- 글의 어조가 마치 Gemini에게 쉬운 설명을 부탁했을 때처럼 느껴짐
-
Instruction pipelining 이야기를 보니, 예전 6502 같은 단순한 아키텍처 시절로 돌아가고 싶다는 생각이 듦
그때는 복잡한 매핑이나 프록시 없이 “있는 그대로” 작동했음
빠른 인터커넥트만 있다면 그런 단순함을 다시 꿈꿔볼 수도 있을 것 같음- 물론 이런 ‘편법들(cheats)’ 이 성능 향상에는 기여했음을 인정함
하지만 Meltdown, Spectre 같은 문제를 보면 복잡성이 커진 대가도 분명 있었음
지금처럼 무어의 법칙이 한계에 다다른 시점에서는 이런 복잡성의 트레이드오프가 과연 최선인지 의문임 - 사실 글에서 설명하는 건 가상 메모리(virtual memory) 개념인데, 이건 6502보다 10년은 앞선 기술임
- 복잡성이 늘어난 건 사실이지만, 그만큼 얻은 것도 많음
단순함이 꼭 더 낫다고는 생각하지 않음 - 그런데 왜 그런 단순함을 그리워하는지 궁금함
- 물론 이런 ‘편법들(cheats)’ 이 성능 향상에는 기여했음을 인정함
-
웹사이트가 위험하거나 안전하지 않은 도메인으로 차단되었다고 나옴
- 혹시 회사 노트북을 쓰는 중인지? 회사의 보안 부서가 .xyz 도메인을 신뢰하지 않을 수도 있음
- 아마 보안 소프트웨어가 잘못 작동한 것 같음
VirusTotal 검사 결과를 보면 문제없음 - 단순한 오탐(false alarm) 같음
- 어떤 브라우저가 차단한 건지 궁금함
- 웃김 lol
-
에러 리포트가 “잡음(noise)”일 뿐이라는 말이 무슨 뜻인지 궁금함