리눅스 커널의 이해
12

ch9. process address space

linuxfilesystemkernel
PROCESS ADDRESS SPACE · CH.9
LECTURE NOTE — LINUX KERNEL, CHAPTER 9

주소는 먼저,
메모리는 나중에

프로세스가 malloc()을 호출하면 물리 RAM을 즉시 할당받을까요? 그렇지 않습니다. 커널은 먼저 "주소 사용권"만 부여하며, 실제 페이지 프레임은 해당 메모리에 처음 접근하는 순간 할당됩니다. Linux의 수요 페이징, COW, 페이지 폴트 핸들러가 바로 이 지연 할당의 원칙을 완벽하게 구현합니다.

kernel space
~4 GB
(unused)
↓ user stack
≈ 3 GB
mmap / shared libs
↑ heap (brk)
bss
data
text (code)
≈ 0x08048000
vm_area_struct 리스트 + RB 트리 mm_struct 총괄 관리 페이지 폴트 → 수요 할당
01

큰 그림: 프로세스 주소 공간은 "메모리"가 아니라 "주소 사용권"이다OVERVIEW

프로세스가 동적 메모리를 요청할 때, 커널은 즉시 물리 메모리를 내어줄까요? 아니면 "나중에 사용할 수 있는 주소 범위"만 미리 약속해 둘까요?

커널은 사용자 프로세스의 메모리 요청을 매우 보수적으로 다룹니다. 프로그램을 실행하더라도 모든 코드가 당장 쓰이는 것은 아니며, 대규모 메모리를 요청했다고 해서 모든 영역을 즉시 읽고 쓰는 것도 아니기 때문입니다. 또한 사용자 프로그램의 동작을 전적으로 예측할 수 없으므로, 커널은 우선 주소 공간만 등록해 두고 실제 페이지 프레임은 요청이 현실화되는 순간에 비로소 할당합니다.

프로세스 주소 공간은 실제 지어진 건물이 아니라 "이 땅을 사용할 권리"가 명시된 토지 계약서와 같습니다. 메모리 영역은 계약서에 명시된 구획, 페이지 테이블은 등기부등본이며, 페이지 폴트는 "아직 이 땅에 건물이 없는데요?"라고 커널에 알리는 이벤트입니다. 커널은 이 알림을 받고 나서야 건물을 세우거나(할당), 불법 점거에 대해 퇴거 명령(에러 처리)을 내립니다.

오늘 강의의 다섯 가지 핵심 주제
프로세스 주소 공간의 본질적인 개념
주소 공간을 관리하는 mm_structvm_area_struct 자료구조
주소 공간의 크기를 조절하는 do_mmap(), do_munmap(), brk()
페이지 폴트 핸들러 — 수요 페이징(Demand Paging)과 Copy On Write의 동작 원리
프로세스의 생명주기에 따른 주소 공간의 생성과 소멸 과정

핵심 문장: do_page_fault()는 단순한 에러 처리 함수가 아닙니다. 잘못된 주소 접근은 SIGSEGV로 단호하게 처리하고, 아직 물리 페이지가 할당되지 않은 합법적 접근은 수요 페이징으로 자연스럽게 이어주는 리눅스 메모리 관리의 최고 게이트키퍼입니다.

핵심 O/X 퀴즈

1. 사용자 프로세스가 동적 메모리를 요청하면 커널은 예외 없이 즉시 물리 페이지 프레임을 할당한다.
X대부분의 경우 주소 사용권만 먼저 등록하며, 실제 물리 페이지는 해당 주소에 접근하는 순간 할당됩니다.
2. 프로세스 주소 공간은 프로세스가 접근할 수 있도록 허가된 선형 주소들의 논리적 집합이다.
O실제 물리 RAM의 목록이 아니라, 커널이 사용을 승인한 선형 주소의 범위입니다.
3. 페이지 폴트는 항상 프로그램의 논리적 오류를 의미한다.
X수요 페이징, COW, vmalloc 지연 동기화 등 커널이 의도한 정상적인 지연 처리 과정에서도 빈번하게 발생합니다.
02

프로세스 주소 공간: 각 프로세스는 독립적인 선형 주소 세계를 가진다LINEAR ADDRESS SPACE

한 프로세스의 0x08048000 주소는 다른 프로세스의 동일한 주소와 아무런 연관이 없습니다. 커널은 이처럼 독립적인 주소 공간을 메모리 영역(Memory Region) 단위로 쪼개어 관리합니다. 메모리 영역을 정의하는 세 가지 핵심 요소는 '시작 선형 주소', '길이', '접근 권한'이며, 이때 시작 주소와 길이는 반드시 4KB 페이지 크기의 배수로 맞아떨어져야 합니다.

프로세스가 새로운 메모리 영역을 획득하는 대표적인 경우
execve()
새로운 프로그램을 메모리에 적재할 때 — 기존 주소 공간을 지우고 완전히 새로 구성합니다.
mmap()
파일을 메모리에 매핑하거나, 파일과 무관한 익명 영역을 요청할 때 추가됩니다.
힙 확장
프로그램 내부에서 malloc()을 통해 메모리를 동적으로 늘릴 때, 내부적으로 brk()나 mmap()이 호출되어 영역이 확장됩니다.
스택 확장
함수 호출이 깊어져 사용자 모드 스택이 낮은 주소 방향으로 한계를 넘어 자라날 때 발생합니다.
IPC 공유 메모리
shmat() 시스템 콜을 통해 다른 프로세스와 메모리 영역을 공유할 때 연결됩니다.

페이지 폴트 판단의 절대적 기준: 커널이 각 프로세스의 메모리 영역을 꼼꼼히 기록하는 가장 큰 이유는 페이지 폴트가 발생했을 때 이를 정확히 처리하기 위함입니다. "문제의 주소가 프로세스 주소 공간에 속하는가?" — 속한다면 지연된 메모리를 내어주는 수요 페이징으로, 속하지 않는다면 가차 없이 SIGSEGV를 발생시킵니다.

핵심 O/X 퀴즈

1. 완전히 동일한 선형 주소 값이라도, 어떤 프로세스에서 접근하느냐에 따라 전혀 다른 데이터를 가리킬 수 있다.
O각 프로세스는 자신만의 독립적인 페이지 테이블과 메모리 영역 구성을 가지고 있기 때문입니다.
2. 메모리 영역의 시작 주소와 길이는 효율을 위해 1바이트 단위로 세밀하게 지정할 수 있다.
X반드시 하드웨어 페이지 크기(일반적으로 4KB)의 정수배로 정렬되어야 합니다.
3. 접근한 주소가 프로세스 주소 공간에 정상적으로 포함되어 있지만 아직 맵핑된 물리 페이지가 없다면, 커널은 이를 수요 페이징으로 해결한다.
O이것이 바로 커널이 메모리를 효율적으로 쓰는 정상적인 지연 할당 매커니즘입니다.
03

mm_struct: 프로세스 주소 공간을 관리하는 총괄 장부MEMORY DESCRIPTOR

프로세스의 주소 공간을 정의하는 모든 핵심 정보는 mm_struct(메모리 디스크립터)라는 거대한 구조체 안에 담겨 있습니다. 각 프로세스 디스크립터(task_struct)의 mm 필드가 바로 이 장부를 가리킵니다.

mm_struct의 주요 관리 항목
mmap
프로세스가 가진 모든 메모리 영역을 선형적으로 엮은 연결 리스트의 시작점입니다.
mm_rb
빠른 검색을 위해 동일한 메모리 영역들을 구조화한 레드-블랙 트리의 최상단 루트입니다.
mmap_cache
프로세스가 가장 최근에 접근했던 메모리 영역을 기억하여 검색 속도를 높입니다(지역성 활용).
pgd
Page Global Directory — 주소 변환이 시작되는 페이지 테이블의 최상위 디렉터리 주소입니다.
mm_users
이 주소 공간을 함께 공유하고 있는 경량 프로세스(스레드)의 총 숫자입니다.
mm_count
메모리 디스크립터 구조체 자체에 대한 참조 카운터입니다. mm_users가 0이 되어 주소 공간이 비워지더라도, mm_count가 남아 있다면 구조체를 메모리에서 해제해서는 안 됩니다.
mmap_sem
여러 스레드가 메모리 영역을 동시에 수정하지 못하도록 보호하는 읽기/쓰기 세마포어입니다.
start_brk / brk
동적 할당 영역인 힙(Heap)의 시작점과 현재 가장 끝 지점을 나타냅니다.
start_stack
사용자 모드 스택이 시작되는 최초의 주소입니다.
total_vm / rss
주소 공간에 맵핑된 전체 페이지 수(total_vm)와 현재 실제 RAM에 적재된 페이지 수(rss)입니다. 이 둘의 차이가 지연 할당의 규모를 보여줍니다.

total_vm과 rss의 차이: total_vm은 "커널이 사용을 허가한 총 주소 페이지의 수"를 의미하고, rss(Resident Set Size)는 "그중 실제로 물리 RAM에 올라와 있는 페이지의 수"를 뜻합니다. 이 두 값의 격차가 바로 수요 페이징이 얼마나 활발히 일어나고 있는지를 보여주는 지표입니다.

프로세스가 하나의 기업이라면 mm_struct는 그 기업이 임대하여 사용 중인 모든 사무실의 종합 임대차 계약서 파일입니다. 어떤 층을 계약했는지, 창고(힙)와 휴게실(스택)은 어디인지, 건물 전체 도면(페이지 테이블 최상위 디렉터리)은 어디에 있는지, 그리고 실제로 직원이 출근해 불을 켜 둔 방(RSS)은 몇 개인지를 모두 기록하고 있습니다.

핵심 O/X 퀴즈

1. mm_struct는 프로세스의 전체 주소 공간 레이아웃과 상태를 담고 있는 메모리 디스크립터다.
O프로세스의 근간이 되는 정보 구조체이며, task_struct의 mm 필드를 통해 접근합니다.
2. 프로세스의 total_vm 값이 매우 크다면, 이는 해당 프로세스가 그만큼의 물리적 RAM을 선점하고 있다는 뜻이다.
Xtotal_vm은 허가받은 가상 주소 공간의 크기일 뿐이며, 실제 RAM 점유량은 rss 값으로 확인해야 합니다.
3. mm_users와 mm_count는 이름만 다를 뿐 구조체의 생명 주기를 관리하는 동일한 역할의 카운터다.
Xmm_users는 공간을 공유하는 스레드 수이고, mm_count는 디스크립터 구조체 자체의 안전한 해제를 위한 참조 카운터로 명확히 역할이 다릅니다.
04

커널 스레드의 주소 공간: mm은 비워두고 active_mm을 빌려 쓴다KERNEL THREAD / LAZY TLB

커널 스레드는 사용자 모드로 전환되는 일 없이 오로지 커널 공간 안에서만 실행됩니다. 따라서 사용자 모드를 위한 주소 공간 자체가 필요 없으며, 커널 스레드의 프로세스 디스크립터에서 mm 필드는 언제나 NULL로 비워져 있습니다.

그렇다고 페이지 테이블 없이 CPU가 명령어를 실행할 수는 없습니다. CPU 구조상 항상 유효한 페이지 테이블 디렉터리가 지정되어 있어야 합니다. 이 딜레마를 해결하기 위해 커널 스레드는 바로 직전에 실행되던 일반 프로세스의 active_mm을 잠시 빌려 씁니다. 주소 공간을 공식적으로 "소유"하지는 않지만, CPU가 계속 멈추지 않고 커널 코드를 실행할 수 있도록 기존의 렌즈(페이지 테이블)를 그대로 둔 채 작업을 수행하는 효율적인 구조입니다.

mm과 active_mm의 차이
일반 프로세스
mm == active_mm — 자신이 소유한 메모리 디스크립터를 동일하게 가리킵니다.
커널 스레드
mm = NULL, active_mm = 이전 프로세스의 mm — 소유권은 없지만 실행을 위해 타인의 페이지 테이블을 차용합니다.

핵심 O/X 퀴즈

1. 커널 스레드는 사용자 주소 공간을 가질 필요가 없으므로 mm 필드가 항상 NULL이다.
O커널 모드의 코드와 데이터만 접근하면 되기 때문에 사용자 영역 맵핑이 필요치 않습니다.
2. 커널 스레드는 페이지 테이블 자체를 참조하지 않고 물리 주소로 직접 명령어를 실행한다.
XCPU는 항상 페이징 기법 위에서 동작하므로, 직전 프로세스의 active_mm을 통해 페이지 테이블을 빌려 씁니다.
3. 이처럼 페이지 테이블을 빌려 쓰는 방식은 컨텍스트 스위칭 시 불필요한 TLB 플러시(flush)를 막아 성능을 높인다.
O이것이 Lazy TLB 기법의 핵심으로, 커널 영역은 모든 프로세스가 동일하게 맵핑되어 있으므로 이전 테이블을 유지해도 문제가 없습니다.
05

vm_area_struct: 연속된 메모리 구역을 정의하는 단위VMA

mm_struct 안에서 관리되는 개별적인 구획 하나하나를 vm_area_struct(VMA)라고 부릅니다. vm_start는 해당 구역의 첫 번째 선형 주소를 담고 있으며, vm_end는 구역이 끝난 직후의 첫 번째 선형 주소를 가리킵니다. 즉, 영역의 실제 길이는 vm_end - vm_start로 계산되며, vm_end 주소 자체는 이 영역에 포함되지 않습니다.

vm_area_struct의 주요 필드 분석
vm_start / vm_end
메모리 구역이 시작되는 지점과 끝나는 지점의 경계선입니다 (vm_end는 미포함).
vm_mm
이 구역을 소유하고 있는 부모 프로세스의 mm_struct를 가리키는 역방향 포인터입니다.
vm_next
주소가 증가하는 순서대로 다음 메모리 구역을 가리키는 연결 리스트 포인터입니다.
vm_flags
이 구역이 가진 읽기, 쓰기, 실행 등의 접근 권한과 스택 확장 방향 등의 속성 플래그를 담습니다.
vm_page_prot
하드웨어 페이지 테이블 엔트리(PTE)에 직접적으로 새겨질 보호 비트 설정값입니다.
vm_file
파일이 맵핑된 영역이라면 해당 파일 객체를 가리키고, 힙이나 스택 같은 익명 매핑이라면 NULL을 가집니다.
vm_ops
이 구역을 조작할 때 호출해야 하는 open, close, fault 처리기 등의 연산 함수 포인터 묶음입니다.

프로세스가 새로운 주소 구간을 요청할 때, 새로 할당할 구간이 기존 영역과 맞닿아 있고 접근 권한(vm_flags)까지 완전히 일치한다면 커널은 두 영역을 하나로 합칩니다(Merge). 반대로 기존 영역의 한가운데를 해제해버리면, 남은 영역은 두 개의 독립적인 VMA로 쪼개집니다(Split).

명심해야 할 점은 vm_area_struct 자체가 실제 물리 페이지 프레임이나 데이터를 담고 있지는 않다는 것입니다. 페이지 폴트가 발생했을 때 커널이 이 구조체를 열어보고 "읽기가 허용된 곳인가? 파일에서 데이터를 퍼와야 하는가, 아니면 0으로 채운 빈 페이지를 줘야 하는가?"를 즉각 판단할 수 있도록 만들어 둔 논리적 판단의 명세서입니다.

핵심 O/X 퀴즈

1. vm_area_struct의 vm_end 필드에 기록된 주소는 해당 메모리 영역이 사용할 수 있는 가장 마지막 바이트의 주소다.
Xvm_end는 영역 바깥으로 넘어간 첫 번째 주소를 의미합니다. 유효한 주소 범위는 vm_start부터 vm_end-1까지입니다.
2. 새로 생성하려는 메모리 영역이 기존 영역과 바로 인접해 있고 접근 권한 등의 속성이 같으면 커널은 구조체를 새로 만들지 않고 하나로 병합한다.
O불필요한 메타데이터의 증가를 막기 위해 vma_merge() 함수가 적극적으로 병합을 시도합니다.
3. vm_area_struct는 프로세스에 할당된 실제 물리 페이지 프레임 하나와 1:1로 대응되는 구조체다.
X하나의 VMA는 수천 페이지를 아우르는 '연속된 논리적 주소 구간의 성격'을 묘사하는 구조체입니다.
06

리스트와 레드-블랙 트리: 두 가지 길로 엮은 영리한 탐색 구조Figure 9-2 / 9-3

하나의 프로세스가 가진 수많은 메모리 구역(VMA)들은 주소가 낮음에서 높음으로 증가하는 순서에 따라 연결 리스트로 길게 이어져 있습니다. 그러나 수만 개의 페이지 폴트가 발생할 때마다 리스트를 처음부터 순차 탐색한다면 시스템은 금방 느려질 것입니다. 그래서 Linux 2.6부터는 동일한 VMA 객체들을 레드-블랙 트리(Red-Black Tree)라는 균형 이진 탐색 트리에도 함께 편입시켜 둡니다.

두 자료구조의 완벽한 역할 분담
연결 리스트 (mmap)
프로세스의 전체 주소 맵을 출력하거나 구역 전체를 순서대로 훑어야 할 때 사용합니다. 순차 탐색에는 O(n)의 시간이 걸리지만 구조가 직관적이고 단순합니다.
레드-블랙 트리 (mm_rb)
페이지 폴트가 발생한 특정 주소가 어느 구역에 속하는지 단번에 짚어낼 때 씁니다. 탐색 시간이 O(log n)이므로, 관리하는 구역이 두 배로 늘어나도 탐색 단계는 단 한 번만 추가됩니다.
mmap_cache
여기에 더해 커널은 프로세스가 마지막으로 참조했던 구역을 캐시로 기억해 둡니다. find_vma()는 트리를 타기 전에 이 캐시부터 확인하여 지역성의 이점을 극대화합니다.

기본적으로 단일 프로세스가 조각내어 가질 수 있는 메모리 구역의 최대 개수는 65,536개로 제한되어 있으며, 이 한계치는 /proc/sys/vm/max_map_count를 통해 시스템 상황에 맞게 튜닝할 수 있습니다.

핵심 O/X 퀴즈

1. 리눅스 커널은 메모리 영역 관리를 위해 VMA 구조체를 복제하여 리스트용과 트리용으로 각각 따로 보관한다.
X객체를 복제하는 것이 아니라, 하나의 VMA 객체 안에 리스트를 위한 포인터와 트리를 위한 포인터를 동시에 가지고 엮이는 구조입니다.
2. 레드-블랙 트리는 전체 메모리 영역을 순서대로 스캔할 때보다, 특정 주소가 어느 구역에 속하는지 빠르게 점찍어 찾을 때 그 진가를 발휘한다.
O페이지 폴트 처리의 핵심인 특정 주소 탐색 시간을 획기적으로 줄여주는 O(log n) 자료구조입니다.
3. mmap_cache는 특정 주소 검색을 시작하기 전, 마지막으로 성공했던 구역을 먼저 찔러보아 탐색 비용을 없애려는 최적화 기법이다.
O프로그램의 메모리 접근은 지역성을 띠므로, 연속된 폴트가 같은 구역 안에서 발생할 확률이 매우 높기 때문입니다.
07

접근 권한의 이중 구조: vm_flags와 하드웨어 보호 비트VM_FLAGS

프로세스 주소 공간에 "이 주소를 써도 좋다"고 허락을 내렸다면, 그다음은 "어떻게 써도 좋은가?"를 규정해야 합니다. 해당 영역을 읽고, 쓰고, 실행할 수 있는지, 아니면 다른 프로세스와 데이터를 공유할 수 있는지 등의 다채로운 속성들이 모두 vm_flags에 빼곡히 기록됩니다.

주요 vm_flags의 의미
VM_READ / VM_WRITE / VM_EXEC
해당 구역의 데이터를 읽거나, 쓰거나, 코드로 실행할 수 있는 기본적인 권한입니다.
VM_SHARED
이 구역에 쓴 변경 사항이 다른 프로세스나 매핑된 파일에 반영됨을 의미합니다.
VM_GROWSDOWN
메모리 영역이 현재보다 더 낮은 주소 방향으로 자동 확장될 수 있음을 허용합니다 (주로 사용자 스택).
VM_LOCKED
이 영역의 데이터는 디스크의 스왑 영역으로 쫓겨나지(Swap-out) 않고 항상 RAM에 상주해야 함을 강제합니다.
VM_IO
메모리 맵드 I/O(MMIO)를 위해 하드웨어 디바이스의 주소 공간과 직접 연결된 영역입니다.
VM_DONTCOPY / VM_DONTEXPAND
fork()를 할 때 자식에게 복사해주지 않거나, mremap()을 통한 임의 확장을 금지합니다.

COW를 위한 기만적인 보호 비트: 커널은 영리한 꼼수를 부립니다. 논리적으로 쓰기 권한(VM_WRITE)이 있는 영역일지라도, 하드웨어 페이지 테이블 수준에서는 잠시 쓰기 불가(read-only) 상태로 속여 놓습니다. 부모와 자식 프로세스가 같은 물리 페이지를 쳐다보고 있다가 누군가 데이터를 고치려 시도하는 순간, 페이지 폴트가 발생해 커널을 깨웁니다. 이렇듯 접근 권한 설정은 단순한 보안 기능을 넘어, 메모리 절약 기법을 작동시키는 핵심 트리거 역할을 수행합니다.

핵심 O/X 퀴즈

1. VM_READ, VM_WRITE, VM_EXEC 플래그는 커널 수준에서 해당 영역의 의도된 접근 권한을 선언하는 역할을 한다.
O이러한 논리적 선언들이 취합되어 최종적으로 하드웨어 PTE의 보호 비트로 변환 적용됩니다.
2. vm_area_struct의 vm_flags에 기록된 값과 실제 CPU가 참조하는 페이지 테이블의 하드웨어 보호 비트는 언제나 완벽하게 일치해야만 한다.
XCOW(Copy On Write) 상황처럼, 논리적으로는 쓰기가 가능(VM_WRITE)하더라도 커널의 의도적 개입을 위해 하드웨어 레벨에서는 일시적으로 쓰기 불가(Read-Only)로 설정되는 경우가 흔합니다.
3. Copy On Write 기법을 구현하기 위해서는 커널이 쓰기 작업이 일어나는 순간을 정확히 포착할 수 있어야 하므로 페이지 폴트를 의도적으로 유발시킨다.
O하드웨어 단에서 쓰기 금지를 걸어두면 쓰기 시도 시 CPU가 커널에 폴트 예외를 던져 복사 시점을 알려줍니다.
08

공간을 뒤지는 탐색자들: find_vma()와 get_unmapped_area()VMA SEARCH

find_vma(mm, addr)는 커널이 가장 빈번하게 호출하는 핵심 탐색 함수입니다. 이 함수는 인자로 받은 addr보다 큰 vm_end를 가진 첫 번째 메모리 영역을 찾아 반환합니다. 주의할 점은, 찾아낸 영역이 반드시 addr를 포함하고 있다는 보장은 없다는 것입니다. 따라서 반환값을 받은 커널은 vm_start ≤ addr 조건이 참인지 한 번 더 검증해야 합니다. 탐색은 지역성을 고려해 mmap_cache를 먼저 찔러본 뒤, 실패하면 레드-블랙 트리를 타고 내려갑니다.

상황별 탐색 도구 모음
find_vma_prev()
목표 영역뿐만 아니라 바로 직전에 위치한 영역까지 세트로 찾아줍니다. 구역을 새로 끼워 넣거나 삭제할 때 리스트를 연결하기 위해 반드시 필요합니다.
find_vma_intersection()
특정 선형 주소의 범위(시작~끝) 전체와 조금이라도 겹치는 영역이 있는지를 확인합니다. 힙 영역을 쭉 늘리기 전에 충돌 사고를 막기 위한 용도로 주로 쓰입니다.
get_unmapped_area()
새로운 메모리 영역을 깔고 싶을 때, 아무도 쓰지 않고 텅 비어있는 선형 주소 공간의 적절한 자리를 탐색해 줍니다. free_area_cache를 통해 이전에 찾았던 빈자리를 기억하여 탐색을 가속합니다.

핵심 O/X 퀴즈

1. find_vma() 함수가 NULL이 아닌 유효한 구역을 반환했다면, 질의한 주소는 무조건 해당 구역 범위 안에 포함되어 있다고 확신할 수 있다.
X탐색한 구역의 vm_start가 질의 주소보다 한참 뒤에 있을 수도 있으므로, 포함 여부는 별도로 확인(vm_start ≤ addr)해야 합니다.
2. get_unmapped_area()는 요청한 크기만큼의 물리적 RAM 페이지를 할당하여 반환하는 함수다.
X물리 RAM 할당과는 전혀 무관하며, 단지 논리적인 가상 주소 공간 안에서 빈틈(사용 가능한 선형 주소 구간)을 찾아줄 뿐입니다.
3. free_area_cache는 이전에 빈 공간을 찾기 위해 탐색을 종료했던 위치를 기억해두었다가 다음 탐색의 출발점으로 삼아 속도를 높인다.
O항상 0번 주소부터 훑는 비효율을 막기 위한 스마트한 캐싱 기법입니다.
09

영토를 확장하다: do_mmap()과 새로운 영역의 탄생DO_MMAP()

do_mmap()은 사용자 프로세스의 가상 주소 공간에 새로운 선형 주소 구간을 개척하는 핵심 엔진입니다. 특정 파일을 메모리에 매핑할 때는 파일 객체와 그 위치(offset)를 건네받고, 파일이 없는 순수 메모리(익명 매핑)를 원할 때는 크기와 권한 정보만으로 작업을 시작합니다.

1
초기 유효성 검증. 요청한 길이가 0은 아닌지, 유저 공간 한계선(TASK_SIZE)을 넘지 않는지, 프로세스의 최대 메모리 영역 허용 개수를 초과하지 않는지 꼼꼼히 따집니다.
2
빈자리 탐색. get_unmapped_area()를 호출해 이 거대한 영역이 안전하게 안착할 수 있는 빈 가상 주소 구간을 찾아냅니다.
3
속성 조합. PROT_READ/WRITE 같은 기본 보호 속성과 MAP_SHARED 같은 동작 플래그를 버무려 최종 vm_flags를 결정합니다.
4
충돌 조율. MAP_FIXED 옵션으로 특정 주소를 고집했는데 그곳에 이미 남의 구역이 있다면, do_munmap()의 철거반을 불러 기존 영역을 강제로 밀어버립니다.
5
한도액 확인. 시스템이 프로세스에 허락한 전체 주소 공간의 크기 제한(RLIMIT_AS)을 넘지 않는지 재무 검증을 거칩니다.
6
병합 시도. 새로 들어갈 자리의 앞뒤 이웃 영역과 권한/속성이 완전히 일치한다면, 새로운 구조체를 발급하는 대신 기존 이웃의 크기만 쓱 늘리는 vma_merge()를 통해 자원을 아낍니다.
7
객체 할당. 병합에 실패했다면 슬랩 할당기(slab allocator)에서 새 vm_area_struct 객체를 하나 얻어와 정성껏 필드들을 채워 넣습니다.
8
구조망 편입. vma_link()를 통해 이 새 객체를 프로세스의 연결 리스트와 레드-블랙 트리에 단단히 엮고, 프로세스의 전체 영역 개수와 크기(total_vm) 통계를 업데이트합니다.
9
즉각 할당 예외 처리. 만약 사용자가 VM_LOCKED 플래그를 달아 "절대 스왑되지 않게 즉시 메모리를 줘!"라고 요구했다면, 지연 할당의 원칙을 깨고 바로 make_pages_present()를 호출해 물리 페이지를 끌어다 꽂습니다.

명백히 예외적인 VM_LOCKED 상황을 제외한다면, do_mmap()이 무사히 끝났다는 것은 단지 장부에 "이 주소를 써도 좋다"는 허가 도장이 찍혔을 뿐입니다. 실제 데이터를 담을 물리적 빈 페이지는 프로세스가 벅찬 마음으로 첫 번째 데이터를 써넣는 순간, 페이지 폴트를 통해 조용히 배달됩니다.

핵심 O/X 퀴즈

1. do_mmap()은 mmap() 시스템 콜의 실질적인 커널 내부 구현체로, 프로세스의 주소 공간에 새 구간을 추가하는 역할을 담당한다.
O동적 라이브러리 로딩부터 파일 입출력 매핑까지 다양한 요구를 처리하는 핵심 함수입니다.
2. do_mmap()이 에러 없이 성공적으로 반환되었다면, 요청한 크기만큼의 물리 메모리 할당 작업도 그 시점에 이미 모두 완료된 것이다.
XVM_LOCKED 같은 특수 옵션이 없다면, 기본적으로 물리 메모리 할당은 철저히 뒤로 미루고 논리적인 주소 공간만 열어줍니다.
3. 새 영역을 만들 때 기존 영역과 겹쳐지는 부분을 발견하면 do_mmap()은 무조건 에러를 뿜으며 중단된다.
X사용자가 강제 매핑(MAP_FIXED)을 지시했다면, 충돌하는 기존 영역을 do_munmap()으로 과감히 도려내고 새 영역을 안착시킵니다.
10

영토를 반환하다: do_munmap()과 정교한 철거 작업DO_MUNMAP()

do_munmap()은 반대로 프로세스의 주소 공간에서 특정 선형 주소 구간을 파내어 제거하는 임무를 맡습니다. 단순히 계약서 한 장을 찢는 일이 아닙니다. 제거하려는 구간이 기존 영역의 일부만 애매하게 걸치고 있을 수 있어 상당히 정교한 분할 수술이 필요합니다.

1
기본 규격 심사. 지우려는 시작 주소가 4KB 페이지 경계에 딱 맞게 떨어지는지, 제거 길이가 0은 아닌지 등을 검증합니다.
2
타겟 조준. find_vma_prev()를 이용해 칼을 댈 제거 구간 이후에 끝나는 첫 번째 메모리 영역을 찾아냅니다. 아예 겹치는 구역이 없다면 성공(0)을 반환하고 가볍게 끝냅니다.
3
앞부분 절단. 제거하려는 구간이 어떤 거대한 메모리 영역의 한가운데에서 시작된다면, split_vma()를 호출해 살려둬야 할 앞쪽 덩어리를 뚝 떼어냅니다.
4
뒷부분 절단. 마찬가지로 제거 구간이 마지막 영역의 한가운데에서 끝난다면, 다시 split_vma()로 뒷부분의 생존 구역을 분리해 냅니다.
5
장부 정리. detach_vmas_to_be_unmapped()를 실행해 리스트와 레드-블랙 트리에서 정확히 잘라낸 폐기 대상 영역들을 뽑아내고, 꼬일 수 있는 mmap_cache를 무효화합니다.
6
물리적 철거. unmap_region()이 투입되어 하드웨어 페이지 테이블 엔트리(PTE)를 벅벅 지우고, CPU의 TLB 캐시를 날리며, 연결되어 있던 물리 페이지 프레임들을 시스템으로 반환합니다.
7
마무리 갱신. total_vm 통계를 줄이고, 역할을 다한 vm_area_struct 껍데기들을 kmem_cache_free()로 소각해 메모리를 회수합니다.

건물 임대 계약을 해지하는 과정과 똑같습니다. 문서상 계약만 취소한다고 끝나는 게 아닙니다. 건물의 출입 통제 권한을 회수하고, 끌어다 쓴 전기와 수도 선을 끊고, 남겨진 쓰레기와 비품들을 말끔히 청소하는 종합적인 원상복구 작업(unmap_region)이 반드시 뒤따라야 완전히 방을 뺐다고 할 수 있습니다.

핵심 O/X 퀴즈

1. do_munmap()은 항상 정확히 하나의 전체 메모리 영역만 골라서 삭제할 수 있다.
X제거 범위가 한 영역의 일부만 걸쳐 있다면, split_vma()를 통해 구역을 반으로 가른 뒤 필요한 부분만 정밀하게 도려냅니다.
2. 논리적 관리 구조인 리스트와 트리에서 영역을 뽑아내면, CPU가 바라보는 페이지 테이블과 물리 메모리 정리도 커널에 의해 마법처럼 자동으로 동기화된다.
X자동화되지 않으며, unmap_region() 함수가 별도로 투입되어 페이지 테이블 삭제와 프레임 회수라는 육체노동을 직접 수행해야 합니다.
3. 프로세스의 가상 주소 공간이 축소되면 그만큼 total_vm 통계 수치도 함께 감소한다.
O제거된 논리적 주소 구간의 크기만큼 프로세스가 점유한 가상 메모리 크기 통계도 정확히 업데이트됩니다.
11

페이지 폴트 핸들러의 거대한 분기점: 오류인가, 지연의 마법인가do_page_fault() / Figure 9-4

x86 아키텍처에서 페이지 폴트를 진두지휘하는 총사령관은 do_page_fault()입니다. 문제가 발생하면 CPU는 즉시 말썽을 일으킨 선형 주소를 cr2 레지스터에 박아 넣고 커널을 호출하며, 핸들러 함수는 당시 CPU 레지스터 상태(pt_regs)와 사태의 원인을 담은 3비트짜리 error_code를 넘겨받습니다.

error_code의 3비트 비밀 보고서
bit 0
0 = 페이지가 아예 메모리에 없는 상태(not present), 1 = 권한을 어긴 불법 접근 시도.
bit 1
0 = 데이터를 읽거나 실행하려 했음, 1 = 데이터를 쓰거나 변경하려 했음.
bit 2
0 = 커널 모드 권한으로 접근 중 발생함, 1 = 사용자 모드 권한으로 접근 중 발생함.

페이지 폴트의 재판 과정 (Figure 9-4 요약 흐름도)

1심.
커널 구역 침범인가? (주소 ≥ TASK_SIZE) → vmalloc_fault라는 특수 처리로 넘기거나, 명백한 커널 버그(bad_area)로 사형 선고.
사용자 구역 주소인가? → 다음 심판으로 회부.
2심.
인터럽트 등 치명적 커널 컨텍스트에서 발생했는가? → 시스템 붕괴 위험! 즉시 bad_area로 처리.
일반적인 프로세스 실행 중인가? → mmap_sem 자물쇠를 걸고 find_vma()로 탐색 개시.
3심.
찾아낸 VMA 안에 주소가 정상적으로 쏙 들어가는가? → 합법적인 구역(good_area). 읽기/쓰기 권한까지 맞다면 handle_mm_fault()로 메모리 배달 지시.
VMA 바깥의 황무지이거나 권한이 일치하지 않는가? → 불법 접근(bad_area). 프로세스에게 SIGSEGV 철퇴를 내림.
예외.
스택 끄트머리를 약간 벗어났는가? → 접근 주소가 vm_start보다 조금 낮더라도, 해당 영역이 스택(VM_GROWSDOWN)이고 한도 내라면 expand_stack()으로 조용히 영역을 늘려줌.

핵심 O/X 퀴즈

1. 페이지 폴트 핸들러는 어떤 주소에서 문제가 터졌는지 알아내기 위해 x86의 cr2 제어 레지스터 값을 읽어온다.
O하드웨어인 CPU가 폴트를 유발한 선형 주소를 직접 cr2에 담아 친절하게 커널에 넘겨줍니다.
2. 전달받은 error_code는 단순한 에러 번호가 아니라, 비트 단위로 쪼개져 결석(not present)인지 불법 침입(권한 위반)인지를 세밀하게 알려준다.
O이 3개의 비트를 조합하여 커널은 상황에 맞는 대처 시나리오를 빠르고 정확하게 결정합니다.
3. 사용자 스택 영역의 경계를 1바이트라도 벗어난 주소에 접근하면 커널은 예외 없이 즉시 SIGSEGV 에러를 발생시킨다.
X해당 영역이 아래로 자라는 스택(VM_GROWSDOWN) 성격을 가졌고, 접근 패턴이 타당하다면 expand_stack()을 통해 융통성 있게 공간을 넓혀줍니다.
12

운명의 갈림길: 주소 밖의 불법 접근과 주소 안의 합법적 지연bad_area / good_area / handle_mm_fault()

불법 판정 (bad_area): 접근한 주소가 프로세스의 권리 밖이라면 자비는 없습니다. 사용자 모드에서 벌어진 일이라면 악명 높은 SIGSEGV(Segmentation Fault) 신호를 보내 프로세스를 사살합니다. 만약 커널 모드 내부에서 이런 실수가 터졌다면, 커널은 패닉에 빠지기 전에 exception table을 뒤져 미리 준비된 수습 코드(fixup)가 있는지 필사적으로 찾습니다. 수습이 가능하면 -EFAULT를 반환하고, 수습조차 안 되는 찐 커널 버그라면 시스템을 멈추는 'Kernel oops'를 터뜨립니다.

합법 판정 (good_area): 허가된 주소 공간 안이고 권한(VM_READ/WRITE/EXEC)까지 딱 들어맞는다면, 이것은 에러가 아니라 커널이 기다려왔던 '지연 할당의 타이밍'입니다. 커널은 이제 진짜 물리 페이지를 엮어주기 위해 handle_mm_fault()라는 실무 담당자를 호출합니다.

handle_mm_fault()의 4가지 작업 결과 보고서
minor fault
디스크를 읽을 필요 없이 메모리 안에서 눈썹 휘날리게 뚝딱 처리된 가벼운 폴트입니다. (예: 0으로 채워진 빈 페이지 연결, 기존 페이지 테이블 업데이트)
major fault
필요한 데이터가 디스크(스왑이나 파일)에 있어서, 프로세스를 잠시 재우고 느릿느릿 디스크 I/O를 기다려야만 했던 무거운 폴트입니다.
VM_FAULT_SIGBUS
매핑된 파일의 물리적 끝을 넘어서 접근하는 등, 하드웨어 장치나 파일 시스템 레벨에서 심각한 문제가 발생했을 때 던지는 오류입니다.
VM_FAULT_OOM
물리적 메모리가 바닥나서(Out Of Memory) 도저히 페이지를 내어줄 수 없는 끔찍한 상황입니다. OOM Killer가 출동해 덩치 큰 프로세스들을 사냥하기 시작합니다.

핵심 O/X 퀴즈

1. 사용자 모드에서 프로세스에 허락되지 않은 구역을 멋대로 건드리면 커널은 SIGSEGV 신호를 발생시킨다.
OC/C++ 프로그래머들을 괴롭히는 그 유명한 "Segmentation fault"의 정체가 바로 이것입니다.
2. 절대 무결해야 할 커널 모드에서 발생한 페이지 폴트는 예외 없이 커널의 치명적 버그로 간주되어 시스템을 강제 정지시킨다.
X사용자 공간에서 넘어온 포인터가 쓰레기값일 경우 등 예측 가능한 상황에 대비해, exception table에 안전한 복구(fixup) 코드를 준비해 두고 유연하게 대처합니다.
3. handle_mm_fault()는 페이지 디렉터리와 미들 테이블 등의 계층 구조를 뚫어주는 역할을 하며, 최종적인 물리 페이지 배정 작업은 더 깊숙한 하위 함수로 위임한다.
O최상위 PGD부터 차근차근 계층을 짚어내려간 뒤, 진정한 알맹이 작업은 handle_pte_fault()라는 전문가에게 넘깁니다.
13

수요 페이징: 진짜 메모리는 발등에 불이 떨어질 때 내어준다DEMAND PAGING / handle_pte_fault()

수요 페이징(Demand Paging)이란, 물리 페이지 프레임의 실제 할당을 프로세스가 '정말로 그 메모리에 접근하는 최후의 순간'까지 최대한 늦추는 고도의 지연 전술입니다. 어차피 프로그램은 자신이 예약해 둔 거대한 주소 공간을 한꺼번에 다 쓰지 않기 때문에, 미리 통째로 물리 메모리를 묶어두는 것은 엄청난 낭비입니다.

handle_pte_fault()의 3가지 실전 해결책 (PTE가 비어있을 때)
PTE가 완전히 비어 있음
이 주소에 난생처음 접근한 상황입니다. 파일 매핑 구역이면 do_no_page()가 파일 데이터를 퍼오고, 힙이나 스택 같은 익명 구역이면 do_anonymous_page()가 순백의 새 페이지를 달아줍니다.
비선형 파일 매핑
특수한 형태의 파일 매핑으로 PTE가 특별한 상태값을 가질 때, do_file_page()를 호출해 꼬인 매핑을 풀어냅니다.
Swap 공간에 잠들어 있음
예전에 RAM 부족으로 하드디스크의 스왑 영역으로 쫓겨났던 페이지입니다. do_swap_page()가 출동해 디스크에서 먼지를 털어내고 다시 RAM으로 모셔 옵니다.

궁극의 짠돌이 최적화, zero page: 새로 발급된 익명 메모리는 보안상 어차피 내용이 전부 0으로 채워져 있어야 합니다. 이 점을 노려, 프로세스가 이 빈 공간을 그저 '읽기'만 한다면 커널은 굳이 새 물리 페이지를 짜내지 않습니다. 대신 시스템 전체가 공유하는 단 하나의 투명한 empty_zero_page를 읽기 전용으로 슬쩍 연결해 줍니다. 그러다 나중에 누군가 이곳에 '쓰기' 시도를 하면, 그때서야 비로소 가짜를 치우고 진짜 전용 물리 페이지를 깔아줍니다. 이것이 지연 전략이 빚어낸 가장 극단적이고 우아한 메모리 절약 기법입니다.

핵심 O/X 퀴즈

1. 수요 페이징 기법은 프로세스가 시작되기 전 필요한 모든 물리 페이지를 한방에 할당해 실행 속도를 끌어올리는 기술이다.
X정반대입니다. 실제 접근 시 발생하는 페이지 폴트를 신호탄으로 삼아, 할당을 마지막 순간까지 늦추고 아끼는 기술입니다.
2. 힙(Heap) 영역 같은 익명 메모리를 처음 읽으려 시도할 때, 리눅스는 즉시 깨끗하게 비워진 프로세스 전용 물리 페이지 하나를 무조건 새로 만들어준다.
X단순히 읽기만 한다면 모두가 돌려쓰는 전역 empty_zero_page를 연결해주어 물리 메모리 소비를 0으로 만듭니다.
3. empty_zero_page를 평화롭게 공유하다가 프로세스가 무언가를 쓰려고 시도하면, 커널은 그제서야 COW 매커니즘을 발동시켜 진정한 독립 페이지를 쥐여준다.
O읽을 때는 환상을 공유하고, 쓸 때는 실체를 내어주는 완벽한 지연 할당의 사이클입니다.
14

Copy On Write: 펜을 들기 전까지 복사본은 존재하지 않는다COW / do_wp_page()

초창기 낡은 Unix 시스템에서 fork()는 참으로 무식했습니다. 자식이 태어나면 부모가 가진 수백 메가바이트의 메모리 페이지를 일일이 무식하게 찍어 복사했습니다. 문제는 갓 태어난 자식이 곧바로 execve()를 불러 새 프로그램으로 탈바꿈하면서, 기껏 땀 흘려 복사해 준 그 무거운 메모리를 모조리 쓰레기통에 처박는다는 것이었습니다. 현대의 Linux는 이 참사를 막기 위해 COW(Copy On Write)라는 우아한 마법을 부립니다.

회의실에서 한 장의 기획안(원본 물리 페이지)을 부모와 자식이 나란히 앉아 함께 들여다봅니다. 둘 다 눈으로 읽기만 한다면 굳이 문서를 한 장 더 복사할 이유가 없습니다. 그런데 자식이 "여기 이 문구 좀 고쳐볼까?" 하고 펜을 드는(쓰기 시도) 바로 그 찰나! 커널 비서가 번개처럼 나타나 문서를 한 장 복사해 줍니다. 자식은 자신의 복사본에 맘껏 낙서를 하고, 부모는 온전한 원본을 계속 볼 수 있습니다.

1
fork() 직후, 부모와 자식의 페이지 테이블 엔트리(PTE)는 사이좋게 똑같은 물리 페이지를 가리킵니다. 하지만 두 PTE 모두 하드웨어 레벨의 쓰기 가능(writable) 비트가 강제로 꺼진 상태입니다.
2
어느 한쪽이 데이터를 수정하려고 펜을 대는 순간, 쓰기 금지에 걸려 요란한 페이지 폴트가 터지고 do_wp_page()가 출동합니다.
3
싱글족 처리: 조사해 보니 이미 상대방이 죽거나 떠나서 이 페이지를 나 혼자만 쓰고 있다면? 굳이 복사할 필요 없이 PTE의 쓰기 금지 자물쇠만 쓱 풀어주고(minor fault) 평화롭게 끝냅니다.
4
동거인 분리: 여전히 누군가와 함께 공유 중이라면, 새 물리 페이지 프레임을 하나 얻어다 원본의 데이터를 토씨 하나 안 틀리고 베껴 씁니다(copy_page()). 그리고 쓰기를 시도한 쪽의 PTE가 갓 구워낸 새 페이지를 쳐다보도록 연결망을 고치고 낡은 TLB 캐시를 날려버립니다.
5
동시성 경계: 새 페이지를 구해오는 동안 프로세스가 잠시 잠들 수 있는데, 깨어난 사이에 다른 스레드가 장난을 쳤을 수 있으므로 다시 락(lock)을 걸고 PTE 상태가 그대로인지 이중 삼중으로 의심하고 검증합니다.

핵심 O/X 퀴즈

1. COW는 자식 프로세스의 독립성과 안전성을 보장하기 위해, fork()가 호출되는 그 순간 모든 데이터를 깔끔하게 복사해 두는 철저한 분리 기법이다.
X복사라는 무거운 작업을 누군가 데이터를 진짜로 훼손하려(Write) 할 때까지 필사적으로 뒤로 미루는(Delay) 게으른 최적화 기법입니다.
2. COW의 핵심 트릭은 논리적으로 쓰기 권한이 있더라도 하드웨어 PTE를 억지로 쓰기 금지(Write-Protected) 상태로 만들어 의도적인 페이지 폴트를 유도하는 것이다.
O폴트가 터져야만 커널이 사태를 인지하고 do_wp_page()를 투입해 복사본을 만들어 줄 수 있기 때문입니다.
3. do_wp_page()가 출동했다 하더라도, 해당 페이지를 나 혼자만 바라보고 있는 상태라면 멍청하게 복사를 수행하지 않고 권한만 쓰기 가능으로 풀어준다.
O쓸데없는 메모리 낭비와 복사 비용을 줄이기 위한 커널의 꼼꼼한 분기 처리가 돋보이는 부분입니다.
15

커널 영역의 지연 동기화: vmalloc_fault의 은밀한 전파VMALLOC_FAULT

vmalloc()을 호출해 연속적이지 않은 커널 메모리 영역을 조립해 낼 때, 영리한 리눅스는 수많은 프로세스의 페이지 테이블을 일일이 찾아다니며 업데이트하는 생고생을 하지 않습니다. 오직 최상위 마스터 커널 페이지 테이블(init_mm.pgd) 한 곳에만 몰래 새 매핑을 적어두고 시치미를 뗍니다. 변경 사항을 모든 프로세스에 즉시 뿌리는 끔찍한 오버헤드를 회피하기 위한 또 다른 지연 전술입니다.

1
커널 모드로 실행 중인 어떤 프로세스가 vmalloc으로 만든 주소에 접근합니다. 하지만 그 프로세스의 개인 페이지 테이블에는 아직 업데이트가 안 되어 텅 비어있으므로 무지몽매한 페이지 폴트가 터집니다.
2
폴트 사령관 do_page_fault()가 상황을 스캔합니다. "주소가 TASK_SIZE 위쪽(커널 구역)이고, 커널 모드에서 발생했으며, 권한 위반이 아니라 단순히 매핑이 없는(not-present) 상태군." → vmalloc_fault 전담반으로 사건을 이첩합니다.
3
전담반은 cr3 레지스터에서 현재 프로세스의 최상위 디렉터리(PGD)를 확보한 뒤, 마스터 커널 페이지 테이블의 똑같은 주소 위치를 슬쩍 들여다봅니다.
4
마스터 장부에는 최신 매핑 정보가 찬란하게 적혀 있습니다. 전담반은 이를 현재 프로세스의 낡은 PGD에 조용히 복사해 붙여넣고 쿨하게 떠납니다. 이제 프로세스는 방해받지 않고 접근을 이어갑니다.
5
만약 마스터 장부마저 텅 비어있다면? 이것은 지연된 업데이트가 아니라 진짜로 엉뚱한 곳을 찌른 심각한 커널 오류(no_context)로 간주되어 비상사태가 선포됩니다.

여기서 중요한 깨달음을 얻어야 합니다. 리눅스에서 페이지 폴트는 단순한 에러의 징표가 아닙니다. 그것은 "미뤄뒀던 지연 작업을 이제는 제발 마무리해달라"는 커널 내부의 긴밀한 알람시계입니다. vmalloc의 매핑 전파도, 빈 메모리를 채우는 수요 페이징도, 펜을 들 때 비로소 문서를 복사하는 COW도 모두 이 요란한 알람 소리에 맞춰 우아하게 춤을 춥니다.

핵심 O/X 퀴즈

1. vmalloc()으로 커널 메모리 구조를 변경하면, 시스템 안정성을 위해 현재 실행 중인 모든 프로세스의 페이지 테이블에 즉시 그 변경 사항을 강제로 동기화시킨다.
X엄청난 낭비를 피하기 위해 오직 마스터 장부 하나만 고쳐놓고, 각 프로세스에는 그들이 해당 주소를 건드리는(vmalloc_fault) 순간 개별적으로 전파해 줍니다.
2. vmalloc_fault가 발생했을 때 커널이 하는 핵심적인 조치는 마스터 커널 페이지 테이블의 최신 매핑 조각을 현재 속 터지는 프로세스의 페이지 테이블로 복사해 주는 것이다.
O이런 게으르지만 효율적인 복사 메커니즘 덕분에 불필요한 전체 동기화 비용이 사라집니다.
3. 마스터 커널 페이지 테이블을 뒤져봤는데도 해당 매핑 정보를 찾을 수 없다면, 커널은 이를 정상적인 지연 상황으로 받아들이고 빈 페이지를 알아서 할당해 준다.
X마스터에도 정보가 없다면 이것은 의도된 지연이 아니라 명백히 잘못된 번지수를 짚은 치명적인 커널 버그(no_context) 상황입니다.
16

주소 공간의 탄생과 파멸: fork(), clone(), 그리고 exit_mm()LIFECYCLE

새로운 프로세스(또는 스레드)가 세상에 태어날 때 커널은 copy_mm()이라는 산부인과 의사를 호출합니다. 호출자가 CLONE_VM이라는 탯줄 플래그를 달고 왔다면 부모의 거대한 주소 공간을 고스란히 공유하는 가벼운 스레드가 되고, 탯줄이 없다면 부모와 단절된 독자적인 주소 세계를 구축하는 프로세스가 됩니다.

생사를 가르는 세 가지 핵심 경로
clone(CLONE_VM)

새로운 구조체를 파지 않고 부모의 mm_struct를 뻔뻔하게 같이 씁니다. 장부에 mm_users 카운터만 하나 올리면 끝이라 눈부시게 빠릅니다. 멀티 스레딩 모델의 핵심 뼈대입니다.

전통적 fork()

번듯한 독립을 위해 새 mm_struct를 짜고 최상위 페이지 디렉터리(PGD)를 새로 팝니다. dup_mmap()을 통해 부모의 VMA들을 모조리 복제합니다. 이때 나만 쓰던 쓰기 가능 페이지조차 부모 자식 양쪽 모두 읽기 전용(Read-Only)으로 묶어버려 험난한 COW 전투를 준비시킵니다.

exit_mm()

프로세스가 죽음을 맞이할 때 장부를 치우는 장의사입니다. mm 필드를 떼어내고(NULL) CPU를 lazy TLB 모드로 전환해 숨통을 틔워준 뒤, mmput()을 불러 VMA 구조체들과 비대해진 페이지 테이블을 갈기갈기 찢어 시스템에 반환합니다. (단, 커널 스레드는 애초에 빈털터리라 이 철거 작업 없이 조용히 떠납니다.)

핵심 O/X 퀴즈

1. 태어날 때 CLONE_VM 깃발을 들고나온 태스크는 무거운 복사 과정 없이 부모 프로세스의 주소 공간 전체를 내 집처럼 함께 공유한다.
O동일한 mm_struct 장부를 들여다보며 mm_users 카운터만 증가시키는 전형적인 경량 스레드(Thread)의 탄생 과정입니다.
2. 전통적인 무거운 fork() 과정에서는 부모와 자식 간의 우발적인 데이터 훼손을 막기 위해, 사적이고 쓰기 가능한(private writable) 페이지들조차 일시적으로 양쪽 모두 읽기 전용으로 설정되어 버린다.
Odup_mmap()과 그 하위 함수들이 꼼꼼하게 두 녀석의 장부를 읽기 전용으로 조작하여, 향후 펜을 들 때 반드시 COW 페이지 폴트가 터지도록 지뢰를 깔아둡니다.
3. 음지에서 묵묵히 일하던 커널 스레드 역시 수명이 다해 종료될 때는, 혹시 모를 누수를 막기 위해 프로세스의 사용자 주소 공간과 페이지 테이블을 해제하는 exit_mm()의 장례 절차를 엄격하게 거쳐야 한다.
X커널 스레드는 태생부터 사용자 주소 공간(mm)이 NULL인 흙수저였으므로, 해제할 사용자 테이블 자체가 없어 깔끔하고 신속하게 생을 마감합니다.
17

힙의 은밀한 확장: brk(), sbrk(), malloc() 뒤에 숨겨진 진실HEAP / sys_brk()

개발자가 동적 메모리의 바다에서 헤엄칠 때 의지하는 malloc(), calloc(), free() 같은 함수들은 사실 커널이 직접 제공하는 것이 아니라, C 표준 라이브러리(glibc)가 편의를 위해 감싸놓은 껍데기에 불과합니다. 진짜로 힙(Heap) 영역의 경계선(brk)을 고무줄처럼 늘리고 줄이는 커널의 유일한 직통 시스템 콜은 brk()뿐입니다.

1
철통 보안 락 획득. 힙 경계를 허무는 것은 곧 주소 공간 지형도를 뜯어고치는 중대사이므로, 가장 먼저 mmap_sem 세마포어에 쓰기(Write) 모드로 육중한 자물쇠를 채웁니다.
2
선넘기 방지망. 요청한 새로운 끝 주소(brk)가 실수로 코드 영역(end_code)을 파먹으려 든다면, 단호히 거절하고 예전 경계선을 그대로 뱉어냅니다.
3
4KB 각 맞추기. 삐뚤빼뚤한 요청 주소를 하드웨어 취향에 맞게 페이지 크기(PAGE_SIZE)의 배수로 칼같이 올려치기(Page Align) 하여 단정하게 정렬합니다.
4
영토 축소 (다이어트). 만약 새 경계선이 현재보다 줄어들었다면? do_munmap() 철거반을 시켜 잘려나간 자투리 구역을 깔끔하게 철거하고 장부의 mm->brk를 낮춥니다.
5
영토 확장 전 눈치 게임. 힙을 늘리려 할 때는 프로세스의 총 메모리 한도(RLIMIT_DATA)를 깼는지 노려보고, find_vma_intersection()을 날려 늘어날 자리에 남의 영역이 알박기를 하고 있지는 않은지 충돌 여부를 살핍니다.
6
본격 개척 (do_brk). 모든 검문을 통과하면 do_mmap()의 다이어트 버전인 do_brk()를 돌려 순수한 익명 메모리 영역을 쭉 늘려주고, 마침내 mm->brk 포인터를 새 영토 끝에 당당히 꽂습니다.

개발자가 malloc()을 통해 기가바이트 단위의 힙 확장에 성공하며 환호성을 지를지라도, 그 순간 커널이 건네준 것은 가상의 영토가 그려진 빈 지도 한 장뿐입니다. 그 거대한 메모리 영역을 채울 실제 물리 페이지들은, 포인터가 데이터의 첫 바이트를 건드려 페이지 폴트의 비명을 지르는 가장 절박한 순간에야 비로소 수요 페이징의 이름으로 조달됩니다. 심지어 너무 큰 할당 요청은 brk()로 힙을 늘리는 대신, mmap()을 통해 저 멀리 외딴섬(독립된 익명 매핑)에 뚝 떨어뜨려 놓기도 합니다.

핵심 O/X 퀴즈

1. 시스템 구조상 힙(Heap) 영역의 현재 끝 지점을 관리하고 갱신하는 실질적인 커널 시스템 콜은 brk()다.
Osys_brk() 커널 함수가 직접 mm_struct의 brk 필드 값을 조율하며 영토의 끝을 정의합니다.
2. C 프로그램에서 흔히 쓰는 malloc()은 순수한 커널 내부 함수이며 사용자 모드의 C 라이브러리와는 아무런 관련이 없다.
Xmalloc()은 영리한 사용자 공간의 메모리 관리자(glibc 등)일 뿐이며, 창고가 부족할 때만 커널에 SOS(brk, mmap)를 쳐서 도매로 공간을 떼어옵니다.
3. 힙을 현재보다 더 넓은 주소로 확장하려 할 때, 커널은 무턱대고 늘리지 않고 그 진행 방향에 다른 메모리 영역이 가로막고 있는지 충돌 검사를 반드시 거친다.
Ofind_vma_intersection() 함수를 통해 기존 영토와의 겹침을 확인하여 끔찍한 주소 붕괴 사고를 미연에 방지합니다.
18

대단원 마무리: 가상의 허락과 지연된 현실, 그리고 페이지 폴트라는 심판SUMMARY

오늘 길게 달려온 이 거대한 지식의 숲을 단 한 문장으로 관통한다면 다음과 같습니다.

"리눅스의 메모리 관리는 쿨하게 주소 사용권부터 내어주고, 무겁고 비싼 진짜 메모리 할당과 복사는 벼랑 끝까지 미루다가, 페이지 폴트라는 절묘한 알람이 울리는 순간 마법처럼 끼워 맞추는 기가 막힌 지연 전술의 결정체다."

절대 잊지 말아야 할 강의의 10가지 정수
커널은 물리 메모리가 아니라 '가상의 선형 주소 사용권'을 먼저 줍니다. 이 사용권의 단위가 vm_area_struct(VMA)이며, 이를 묶어 관리하는 총괄 장부가 mm_struct입니다.
수많은 VMA들은 전체를 훑을 때는 연결 리스트로, 특정 주소를 저격해 찾을 때는 O(log n)의 레드-블랙 트리로 관리되며, mmap_cache로 속도의 정점을 찍습니다.
VMA의 vm_flags가 지시하는 접근 권한은 결국 CPU 하드웨어 PTE의 보호 비트로 변환됩니다. 커널은 COW를 위해 이 비트를 은밀히 쓰기 금지로 속이기도 합니다.
do_mmap()은 주소 공간을 넓히고 do_munmap()은 깎아냅니다. 늘릴 때는 기존 구역과 병합(Merge)하려 애쓰고, 지울 때는 정교하게 쪼갭니다(Split).
모든 페이지 폴트 핸들러는 무작정 에러를 띄우지 않습니다. '주소가 내 구역인가?' → '권한은 맞게 건드렸나?' → '아! 아직 배달 전이구나'를 논리적으로 따져 묻는 판사입니다.
수요 페이징(Demand Paging): 물리적 페이지 프레임의 할당을 코드나 데이터에 진짜 손이 닿는 그 찰나의 접근 시점까지 극단적으로 미루는 기술입니다.
Zero Page: 데이터를 그저 읽기만 하려는 다수에게는 내용이 텅 빈 똑같은 0번 페이지를 환상처럼 공유시켜, 메모리 소비를 사실상 0으로 만드는 마술입니다.
Copy On Write (COW): 부모와 자식이 무거운 메모리를 통째로 복사하는 비효율을 버리고, 누군가 감히 펜을 들어 글씨를 고치려(Write) 할 때 비로소 복사본을 만들어줍니다.
vmalloc_fault: 커널 메모리가 변동되었을 때, 모든 장부를 고치는 대신 마스터 장부 하나만 고쳐두고, 뒷북을 치는 프로세스들에게 폴트를 계기로 최신 맵핑을 살짝 귀띔해 줍니다.
힙(Heap) 관리: 동적 메모리 확장의 근본적인 커널 콜은 brk()이며, malloc()은 이를 포장한 라이브러리입니다. 이 거대한 힙의 확장 역시 수요 페이징의 지연 법칙을 철저히 따릅니다.

마지막으로 스스로에게 던지는 질문입니다. 여러분의 프로세스가 어떤 주소를 건드렸다가 와장창 페이지 폴트가 터졌습니다. 이제 여러분은 초보자처럼 "앗, 버그 났다!"라고 머리를 감싸 쥐면 안 됩니다. 느긋하게 팔짱을 끼고, 커널의 빙의자가 되어 차례차례 물어봐야 합니다.

Q1.
잠깐, 저 주소가 내 계약서(VMA) 안에 당당히 포함된 영토가 맞긴 한가?
Q2.
영토는 맞는데, 내가 감히 쓰기(Write)나 실행(Exec) 같은 오버된 권한을 휘두른 건 아닌가?
Q3.
권한도 완벽하다면, 이건 에러가 아니다! 커널이 게으름을 피우느라 아직 물리 페이지를 안 준 것일 뿐이다. → 당당히 수요 페이징 요구.
Q4.
아니면 혹시 내가 부모 자식 간의 끈끈한 공유를 깨려는 COW의 방아쇠를 당긴 건 아닐까? 아니면 vmalloc 지연 동기화의 알람을 울린 건가?

최종장 O/X 퀴즈

1. 최신 리눅스의 메모리 관리 철학은 물리 페이지의 배정, 부담스러운 데이터 복사, 무거운 매핑 동기화 작업을 최대한 앞당겨 시스템의 반응 속도를 높이는 것이다.
X완전히 반대입니다. 모든 무거운 작업들을 페이지 폴트가 터지는 최후의 순간(접근 시점)까지 끈질기게 미루는(Delay) 게으르면서도 극도로 효율적인 지연 전략을 씁니다.
2. x86의 do_page_fault() 핸들러는 단순히 버그가 난 프로세스를 처형하는 망나니가 아니라, 합법적인 지연 할당의 요구와 진짜 불법 접근을 섬세하게 골라내는 리눅스 메모리 생태계의 최고 재판관이다.
O결코 두려워할 에러가 아닙니다. 이 핸들러의 명판결이 없다면 수요 페이징도 COW도 작동할 수 없습니다.
3. mm_struct와 vm_area_struct라는 핵심 구조체의 존재를 몰라도, 수요 페이징이나 COW가 어떤 논리로 작동하는지 커널 소스를 완벽하게 이해할 수 있다.
X이 두 가지 구조체야말로 커널이 '주소를 허가해 줄지 말지', '읽기를 줄지 쓰기를 줄지' 판단하는 절대적인 명세서이자 법전이므로, 이를 모르면 전체 흐름의 뼈대를 잡을 수 없습니다.

리눅스의 프로세스 주소 공간 관리는 "논리적 주소의 허가는 쿨하게 내어주고, 무거운 실제 물리 메모리의 배정과 복사는 벼랑 끝까지 미루며, 페이지 폴트라는 극적인 알람을 계기로 현실의 자원을 빚어내는 시스템"입니다.

강의노트 — Linux Kernel, Chapter 9 "Process Address Space" 기반 재구성