ch9. process address space
주소는 먼저,
메모리는 나중에
프로세스가 malloc()을 호출하면 물리 RAM을 즉시 할당받을까요? 그렇지 않습니다. 커널은 먼저 "주소 사용권"만 부여하며, 실제 페이지 프레임은 해당 메모리에 처음 접근하는 순간 할당됩니다. Linux의 수요 페이징, COW, 페이지 폴트 핸들러가 바로 이 지연 할당의 원칙을 완벽하게 구현합니다.
큰 그림: 프로세스 주소 공간은 "메모리"가 아니라 "주소 사용권"이다OVERVIEW
프로세스가 동적 메모리를 요청할 때, 커널은 즉시 물리 메모리를 내어줄까요? 아니면 "나중에 사용할 수 있는 주소 범위"만 미리 약속해 둘까요?
커널은 사용자 프로세스의 메모리 요청을 매우 보수적으로 다룹니다. 프로그램을 실행하더라도 모든 코드가 당장 쓰이는 것은 아니며, 대규모 메모리를 요청했다고 해서 모든 영역을 즉시 읽고 쓰는 것도 아니기 때문입니다. 또한 사용자 프로그램의 동작을 전적으로 예측할 수 없으므로, 커널은 우선 주소 공간만 등록해 두고 실제 페이지 프레임은 요청이 현실화되는 순간에 비로소 할당합니다.
프로세스 주소 공간은 실제 지어진 건물이 아니라 "이 땅을 사용할 권리"가 명시된 토지 계약서와 같습니다. 메모리 영역은 계약서에 명시된 구획, 페이지 테이블은 등기부등본이며, 페이지 폴트는 "아직 이 땅에 건물이 없는데요?"라고 커널에 알리는 이벤트입니다. 커널은 이 알림을 받고 나서야 건물을 세우거나(할당), 불법 점거에 대해 퇴거 명령(에러 처리)을 내립니다.
- ①
- 프로세스 주소 공간의 본질적인 개념
- ②
- 주소 공간을 관리하는
mm_struct와vm_area_struct자료구조 - ③
- 주소 공간의 크기를 조절하는
do_mmap(),do_munmap(),brk() - ④
- 페이지 폴트 핸들러 — 수요 페이징(Demand Paging)과 Copy On Write의 동작 원리
- ⑤
- 프로세스의 생명주기에 따른 주소 공간의 생성과 소멸 과정
핵심 문장: do_page_fault()는 단순한 에러 처리 함수가 아닙니다. 잘못된 주소 접근은 SIGSEGV로 단호하게 처리하고, 아직 물리 페이지가 할당되지 않은 합법적 접근은 수요 페이징으로 자연스럽게 이어주는 리눅스 메모리 관리의 최고 게이트키퍼입니다.
핵심 O/X 퀴즈
1. 사용자 프로세스가 동적 메모리를 요청하면 커널은 예외 없이 즉시 물리 페이지 프레임을 할당한다.
2. 프로세스 주소 공간은 프로세스가 접근할 수 있도록 허가된 선형 주소들의 논리적 집합이다.
3. 페이지 폴트는 항상 프로그램의 논리적 오류를 의미한다.
프로세스 주소 공간: 각 프로세스는 독립적인 선형 주소 세계를 가진다LINEAR ADDRESS SPACE
한 프로세스의 0x08048000 주소는 다른 프로세스의 동일한 주소와 아무런 연관이 없습니다. 커널은 이처럼 독립적인 주소 공간을 메모리 영역(Memory Region) 단위로 쪼개어 관리합니다. 메모리 영역을 정의하는 세 가지 핵심 요소는 '시작 선형 주소', '길이', '접근 권한'이며, 이때 시작 주소와 길이는 반드시 4KB 페이지 크기의 배수로 맞아떨어져야 합니다.
- execve()
- 새로운 프로그램을 메모리에 적재할 때 — 기존 주소 공간을 지우고 완전히 새로 구성합니다.
- mmap()
- 파일을 메모리에 매핑하거나, 파일과 무관한 익명 영역을 요청할 때 추가됩니다.
- 힙 확장
- 프로그램 내부에서 malloc()을 통해 메모리를 동적으로 늘릴 때, 내부적으로 brk()나 mmap()이 호출되어 영역이 확장됩니다.
- 스택 확장
- 함수 호출이 깊어져 사용자 모드 스택이 낮은 주소 방향으로 한계를 넘어 자라날 때 발생합니다.
- IPC 공유 메모리
- shmat() 시스템 콜을 통해 다른 프로세스와 메모리 영역을 공유할 때 연결됩니다.
페이지 폴트 판단의 절대적 기준: 커널이 각 프로세스의 메모리 영역을 꼼꼼히 기록하는 가장 큰 이유는 페이지 폴트가 발생했을 때 이를 정확히 처리하기 위함입니다. "문제의 주소가 프로세스 주소 공간에 속하는가?" — 속한다면 지연된 메모리를 내어주는 수요 페이징으로, 속하지 않는다면 가차 없이 SIGSEGV를 발생시킵니다.
핵심 O/X 퀴즈
1. 완전히 동일한 선형 주소 값이라도, 어떤 프로세스에서 접근하느냐에 따라 전혀 다른 데이터를 가리킬 수 있다.
2. 메모리 영역의 시작 주소와 길이는 효율을 위해 1바이트 단위로 세밀하게 지정할 수 있다.
3. 접근한 주소가 프로세스 주소 공간에 정상적으로 포함되어 있지만 아직 맵핑된 물리 페이지가 없다면, 커널은 이를 수요 페이징으로 해결한다.
mm_struct: 프로세스 주소 공간을 관리하는 총괄 장부MEMORY DESCRIPTOR
프로세스의 주소 공간을 정의하는 모든 핵심 정보는 mm_struct(메모리 디스크립터)라는 거대한 구조체 안에 담겨 있습니다. 각 프로세스 디스크립터(task_struct)의 mm 필드가 바로 이 장부를 가리킵니다.
- 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는 프로세스의 전체 주소 공간 레이아웃과 상태를 담고 있는 메모리 디스크립터다.
2. 프로세스의 total_vm 값이 매우 크다면, 이는 해당 프로세스가 그만큼의 물리적 RAM을 선점하고 있다는 뜻이다.
3. mm_users와 mm_count는 이름만 다를 뿐 구조체의 생명 주기를 관리하는 동일한 역할의 카운터다.
커널 스레드의 주소 공간: mm은 비워두고 active_mm을 빌려 쓴다KERNEL THREAD / LAZY TLB
커널 스레드는 사용자 모드로 전환되는 일 없이 오로지 커널 공간 안에서만 실행됩니다. 따라서 사용자 모드를 위한 주소 공간 자체가 필요 없으며, 커널 스레드의 프로세스 디스크립터에서 mm 필드는 언제나 NULL로 비워져 있습니다.
그렇다고 페이지 테이블 없이 CPU가 명령어를 실행할 수는 없습니다. CPU 구조상 항상 유효한 페이지 테이블 디렉터리가 지정되어 있어야 합니다. 이 딜레마를 해결하기 위해 커널 스레드는 바로 직전에 실행되던 일반 프로세스의 active_mm을 잠시 빌려 씁니다. 주소 공간을 공식적으로 "소유"하지는 않지만, CPU가 계속 멈추지 않고 커널 코드를 실행할 수 있도록 기존의 렌즈(페이지 테이블)를 그대로 둔 채 작업을 수행하는 효율적인 구조입니다.
- 일반 프로세스
- mm == active_mm — 자신이 소유한 메모리 디스크립터를 동일하게 가리킵니다.
- 커널 스레드
- mm = NULL, active_mm = 이전 프로세스의 mm — 소유권은 없지만 실행을 위해 타인의 페이지 테이블을 차용합니다.
핵심 O/X 퀴즈
1. 커널 스레드는 사용자 주소 공간을 가질 필요가 없으므로 mm 필드가 항상 NULL이다.
2. 커널 스레드는 페이지 테이블 자체를 참조하지 않고 물리 주소로 직접 명령어를 실행한다.
3. 이처럼 페이지 테이블을 빌려 쓰는 방식은 컨텍스트 스위칭 시 불필요한 TLB 플러시(flush)를 막아 성능을 높인다.
vm_area_struct: 연속된 메모리 구역을 정의하는 단위VMA
mm_struct 안에서 관리되는 개별적인 구획 하나하나를 vm_area_struct(VMA)라고 부릅니다. vm_start는 해당 구역의 첫 번째 선형 주소를 담고 있으며, vm_end는 구역이 끝난 직후의 첫 번째 선형 주소를 가리킵니다. 즉, 영역의 실제 길이는 vm_end - vm_start로 계산되며, vm_end 주소 자체는 이 영역에 포함되지 않습니다.
- 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 필드에 기록된 주소는 해당 메모리 영역이 사용할 수 있는 가장 마지막 바이트의 주소다.
2. 새로 생성하려는 메모리 영역이 기존 영역과 바로 인접해 있고 접근 권한 등의 속성이 같으면 커널은 구조체를 새로 만들지 않고 하나로 병합한다.
3. vm_area_struct는 프로세스에 할당된 실제 물리 페이지 프레임 하나와 1:1로 대응되는 구조체다.
리스트와 레드-블랙 트리: 두 가지 길로 엮은 영리한 탐색 구조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 구조체를 복제하여 리스트용과 트리용으로 각각 따로 보관한다.
2. 레드-블랙 트리는 전체 메모리 영역을 순서대로 스캔할 때보다, 특정 주소가 어느 구역에 속하는지 빠르게 점찍어 찾을 때 그 진가를 발휘한다.
3. mmap_cache는 특정 주소 검색을 시작하기 전, 마지막으로 성공했던 구역을 먼저 찔러보아 탐색 비용을 없애려는 최적화 기법이다.
접근 권한의 이중 구조: 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 플래그는 커널 수준에서 해당 영역의 의도된 접근 권한을 선언하는 역할을 한다.
2. vm_area_struct의 vm_flags에 기록된 값과 실제 CPU가 참조하는 페이지 테이블의 하드웨어 보호 비트는 언제나 완벽하게 일치해야만 한다.
3. Copy On Write 기법을 구현하기 위해서는 커널이 쓰기 작업이 일어나는 순간을 정확히 포착할 수 있어야 하므로 페이지 폴트를 의도적으로 유발시킨다.
공간을 뒤지는 탐색자들: 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이 아닌 유효한 구역을 반환했다면, 질의한 주소는 무조건 해당 구역 범위 안에 포함되어 있다고 확신할 수 있다.
2. get_unmapped_area()는 요청한 크기만큼의 물리적 RAM 페이지를 할당하여 반환하는 함수다.
3. free_area_cache는 이전에 빈 공간을 찾기 위해 탐색을 종료했던 위치를 기억해두었다가 다음 탐색의 출발점으로 삼아 속도를 높인다.
영토를 확장하다: do_mmap()과 새로운 영역의 탄생DO_MMAP()
do_mmap()은 사용자 프로세스의 가상 주소 공간에 새로운 선형 주소 구간을 개척하는 핵심 엔진입니다. 특정 파일을 메모리에 매핑할 때는 파일 객체와 그 위치(offset)를 건네받고, 파일이 없는 순수 메모리(익명 매핑)를 원할 때는 크기와 권한 정보만으로 작업을 시작합니다.
get_unmapped_area()를 호출해 이 거대한 영역이 안전하게 안착할 수 있는 빈 가상 주소 구간을 찾아냅니다.vm_flags를 결정합니다.do_munmap()의 철거반을 불러 기존 영역을 강제로 밀어버립니다.vma_merge()를 통해 자원을 아낍니다.vm_area_struct 객체를 하나 얻어와 정성껏 필드들을 채워 넣습니다.vma_link()를 통해 이 새 객체를 프로세스의 연결 리스트와 레드-블랙 트리에 단단히 엮고, 프로세스의 전체 영역 개수와 크기(total_vm) 통계를 업데이트합니다.make_pages_present()를 호출해 물리 페이지를 끌어다 꽂습니다.명백히 예외적인 VM_LOCKED 상황을 제외한다면, do_mmap()이 무사히 끝났다는 것은 단지 장부에 "이 주소를 써도 좋다"는 허가 도장이 찍혔을 뿐입니다. 실제 데이터를 담을 물리적 빈 페이지는 프로세스가 벅찬 마음으로 첫 번째 데이터를 써넣는 순간, 페이지 폴트를 통해 조용히 배달됩니다.
핵심 O/X 퀴즈
1. do_mmap()은 mmap() 시스템 콜의 실질적인 커널 내부 구현체로, 프로세스의 주소 공간에 새 구간을 추가하는 역할을 담당한다.
2. do_mmap()이 에러 없이 성공적으로 반환되었다면, 요청한 크기만큼의 물리 메모리 할당 작업도 그 시점에 이미 모두 완료된 것이다.
3. 새 영역을 만들 때 기존 영역과 겹쳐지는 부분을 발견하면 do_mmap()은 무조건 에러를 뿜으며 중단된다.
영토를 반환하다: do_munmap()과 정교한 철거 작업DO_MUNMAP()
do_munmap()은 반대로 프로세스의 주소 공간에서 특정 선형 주소 구간을 파내어 제거하는 임무를 맡습니다. 단순히 계약서 한 장을 찢는 일이 아닙니다. 제거하려는 구간이 기존 영역의 일부만 애매하게 걸치고 있을 수 있어 상당히 정교한 분할 수술이 필요합니다.
find_vma_prev()를 이용해 칼을 댈 제거 구간 이후에 끝나는 첫 번째 메모리 영역을 찾아냅니다. 아예 겹치는 구역이 없다면 성공(0)을 반환하고 가볍게 끝냅니다.split_vma()를 호출해 살려둬야 할 앞쪽 덩어리를 뚝 떼어냅니다.split_vma()로 뒷부분의 생존 구역을 분리해 냅니다.detach_vmas_to_be_unmapped()를 실행해 리스트와 레드-블랙 트리에서 정확히 잘라낸 폐기 대상 영역들을 뽑아내고, 꼬일 수 있는 mmap_cache를 무효화합니다.unmap_region()이 투입되어 하드웨어 페이지 테이블 엔트리(PTE)를 벅벅 지우고, CPU의 TLB 캐시를 날리며, 연결되어 있던 물리 페이지 프레임들을 시스템으로 반환합니다.vm_area_struct 껍데기들을 kmem_cache_free()로 소각해 메모리를 회수합니다.건물 임대 계약을 해지하는 과정과 똑같습니다. 문서상 계약만 취소한다고 끝나는 게 아닙니다. 건물의 출입 통제 권한을 회수하고, 끌어다 쓴 전기와 수도 선을 끊고, 남겨진 쓰레기와 비품들을 말끔히 청소하는 종합적인 원상복구 작업(unmap_region)이 반드시 뒤따라야 완전히 방을 뺐다고 할 수 있습니다.
핵심 O/X 퀴즈
1. do_munmap()은 항상 정확히 하나의 전체 메모리 영역만 골라서 삭제할 수 있다.
2. 논리적 관리 구조인 리스트와 트리에서 영역을 뽑아내면, CPU가 바라보는 페이지 테이블과 물리 메모리 정리도 커널에 의해 마법처럼 자동으로 동기화된다.
3. 프로세스의 가상 주소 공간이 축소되면 그만큼 total_vm 통계 수치도 함께 감소한다.
페이지 폴트 핸들러의 거대한 분기점: 오류인가, 지연의 마법인가do_page_fault() / Figure 9-4
x86 아키텍처에서 페이지 폴트를 진두지휘하는 총사령관은 do_page_fault()입니다. 문제가 발생하면 CPU는 즉시 말썽을 일으킨 선형 주소를 cr2 레지스터에 박아 넣고 커널을 호출하며, 핸들러 함수는 당시 CPU 레지스터 상태(pt_regs)와 사태의 원인을 담은 3비트짜리 error_code를 넘겨받습니다.
- bit 0
- 0 = 페이지가 아예 메모리에 없는 상태(not present), 1 = 권한을 어긴 불법 접근 시도.
- bit 1
- 0 = 데이터를 읽거나 실행하려 했음, 1 = 데이터를 쓰거나 변경하려 했음.
- bit 2
- 0 = 커널 모드 권한으로 접근 중 발생함, 1 = 사용자 모드 권한으로 접근 중 발생함.
페이지 폴트의 재판 과정 (Figure 9-4 요약 흐름도)
handle_mm_fault()로 메모리 배달 지시.expand_stack()으로 조용히 영역을 늘려줌.핵심 O/X 퀴즈
1. 페이지 폴트 핸들러는 어떤 주소에서 문제가 터졌는지 알아내기 위해 x86의 cr2 제어 레지스터 값을 읽어온다.
2. 전달받은 error_code는 단순한 에러 번호가 아니라, 비트 단위로 쪼개져 결석(not present)인지 불법 침입(권한 위반)인지를 세밀하게 알려준다.
3. 사용자 스택 영역의 경계를 1바이트라도 벗어난 주소에 접근하면 커널은 예외 없이 즉시 SIGSEGV 에러를 발생시킨다.
운명의 갈림길: 주소 밖의 불법 접근과 주소 안의 합법적 지연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()라는 실무 담당자를 호출합니다.
- minor fault
- 디스크를 읽을 필요 없이 메모리 안에서 눈썹 휘날리게 뚝딱 처리된 가벼운 폴트입니다. (예: 0으로 채워진 빈 페이지 연결, 기존 페이지 테이블 업데이트)
- major fault
- 필요한 데이터가 디스크(스왑이나 파일)에 있어서, 프로세스를 잠시 재우고 느릿느릿 디스크 I/O를 기다려야만 했던 무거운 폴트입니다.
- VM_FAULT_SIGBUS
- 매핑된 파일의 물리적 끝을 넘어서 접근하는 등, 하드웨어 장치나 파일 시스템 레벨에서 심각한 문제가 발생했을 때 던지는 오류입니다.
- VM_FAULT_OOM
- 물리적 메모리가 바닥나서(Out Of Memory) 도저히 페이지를 내어줄 수 없는 끔찍한 상황입니다. OOM Killer가 출동해 덩치 큰 프로세스들을 사냥하기 시작합니다.
핵심 O/X 퀴즈
1. 사용자 모드에서 프로세스에 허락되지 않은 구역을 멋대로 건드리면 커널은 SIGSEGV 신호를 발생시킨다.
2. 절대 무결해야 할 커널 모드에서 발생한 페이지 폴트는 예외 없이 커널의 치명적 버그로 간주되어 시스템을 강제 정지시킨다.
3. handle_mm_fault()는 페이지 디렉터리와 미들 테이블 등의 계층 구조를 뚫어주는 역할을 하며, 최종적인 물리 페이지 배정 작업은 더 깊숙한 하위 함수로 위임한다.
수요 페이징: 진짜 메모리는 발등에 불이 떨어질 때 내어준다DEMAND PAGING / handle_pte_fault()
수요 페이징(Demand Paging)이란, 물리 페이지 프레임의 실제 할당을 프로세스가 '정말로 그 메모리에 접근하는 최후의 순간'까지 최대한 늦추는 고도의 지연 전술입니다. 어차피 프로그램은 자신이 예약해 둔 거대한 주소 공간을 한꺼번에 다 쓰지 않기 때문에, 미리 통째로 물리 메모리를 묶어두는 것은 엄청난 낭비입니다.
- 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. 수요 페이징 기법은 프로세스가 시작되기 전 필요한 모든 물리 페이지를 한방에 할당해 실행 속도를 끌어올리는 기술이다.
2. 힙(Heap) 영역 같은 익명 메모리를 처음 읽으려 시도할 때, 리눅스는 즉시 깨끗하게 비워진 프로세스 전용 물리 페이지 하나를 무조건 새로 만들어준다.
3. empty_zero_page를 평화롭게 공유하다가 프로세스가 무언가를 쓰려고 시도하면, 커널은 그제서야 COW 매커니즘을 발동시켜 진정한 독립 페이지를 쥐여준다.
Copy On Write: 펜을 들기 전까지 복사본은 존재하지 않는다COW / do_wp_page()
초창기 낡은 Unix 시스템에서 fork()는 참으로 무식했습니다. 자식이 태어나면 부모가 가진 수백 메가바이트의 메모리 페이지를 일일이 무식하게 찍어 복사했습니다. 문제는 갓 태어난 자식이 곧바로 execve()를 불러 새 프로그램으로 탈바꿈하면서, 기껏 땀 흘려 복사해 준 그 무거운 메모리를 모조리 쓰레기통에 처박는다는 것이었습니다. 현대의 Linux는 이 참사를 막기 위해 COW(Copy On Write)라는 우아한 마법을 부립니다.
회의실에서 한 장의 기획안(원본 물리 페이지)을 부모와 자식이 나란히 앉아 함께 들여다봅니다. 둘 다 눈으로 읽기만 한다면 굳이 문서를 한 장 더 복사할 이유가 없습니다. 그런데 자식이 "여기 이 문구 좀 고쳐볼까?" 하고 펜을 드는(쓰기 시도) 바로 그 찰나! 커널 비서가 번개처럼 나타나 문서를 한 장 복사해 줍니다. 자식은 자신의 복사본에 맘껏 낙서를 하고, 부모는 온전한 원본을 계속 볼 수 있습니다.
do_wp_page()가 출동합니다.copy_page()). 그리고 쓰기를 시도한 쪽의 PTE가 갓 구워낸 새 페이지를 쳐다보도록 연결망을 고치고 낡은 TLB 캐시를 날려버립니다.핵심 O/X 퀴즈
1. COW는 자식 프로세스의 독립성과 안전성을 보장하기 위해, fork()가 호출되는 그 순간 모든 데이터를 깔끔하게 복사해 두는 철저한 분리 기법이다.
2. COW의 핵심 트릭은 논리적으로 쓰기 권한이 있더라도 하드웨어 PTE를 억지로 쓰기 금지(Write-Protected) 상태로 만들어 의도적인 페이지 폴트를 유도하는 것이다.
3. do_wp_page()가 출동했다 하더라도, 해당 페이지를 나 혼자만 바라보고 있는 상태라면 멍청하게 복사를 수행하지 않고 권한만 쓰기 가능으로 풀어준다.
커널 영역의 지연 동기화: vmalloc_fault의 은밀한 전파VMALLOC_FAULT
vmalloc()을 호출해 연속적이지 않은 커널 메모리 영역을 조립해 낼 때, 영리한 리눅스는 수많은 프로세스의 페이지 테이블을 일일이 찾아다니며 업데이트하는 생고생을 하지 않습니다. 오직 최상위 마스터 커널 페이지 테이블(init_mm.pgd) 한 곳에만 몰래 새 매핑을 적어두고 시치미를 뗍니다. 변경 사항을 모든 프로세스에 즉시 뿌리는 끔찍한 오버헤드를 회피하기 위한 또 다른 지연 전술입니다.
do_page_fault()가 상황을 스캔합니다. "주소가 TASK_SIZE 위쪽(커널 구역)이고, 커널 모드에서 발생했으며, 권한 위반이 아니라 단순히 매핑이 없는(not-present) 상태군." → vmalloc_fault 전담반으로 사건을 이첩합니다.여기서 중요한 깨달음을 얻어야 합니다. 리눅스에서 페이지 폴트는 단순한 에러의 징표가 아닙니다. 그것은 "미뤄뒀던 지연 작업을 이제는 제발 마무리해달라"는 커널 내부의 긴밀한 알람시계입니다. vmalloc의 매핑 전파도, 빈 메모리를 채우는 수요 페이징도, 펜을 들 때 비로소 문서를 복사하는 COW도 모두 이 요란한 알람 소리에 맞춰 우아하게 춤을 춥니다.
핵심 O/X 퀴즈
1. vmalloc()으로 커널 메모리 구조를 변경하면, 시스템 안정성을 위해 현재 실행 중인 모든 프로세스의 페이지 테이블에 즉시 그 변경 사항을 강제로 동기화시킨다.
2. vmalloc_fault가 발생했을 때 커널이 하는 핵심적인 조치는 마스터 커널 페이지 테이블의 최신 매핑 조각을 현재 속 터지는 프로세스의 페이지 테이블로 복사해 주는 것이다.
3. 마스터 커널 페이지 테이블을 뒤져봤는데도 해당 매핑 정보를 찾을 수 없다면, 커널은 이를 정상적인 지연 상황으로 받아들이고 빈 페이지를 알아서 할당해 준다.
주소 공간의 탄생과 파멸: fork(), clone(), 그리고 exit_mm()LIFECYCLE
새로운 프로세스(또는 스레드)가 세상에 태어날 때 커널은 copy_mm()이라는 산부인과 의사를 호출합니다. 호출자가 CLONE_VM이라는 탯줄 플래그를 달고 왔다면 부모의 거대한 주소 공간을 고스란히 공유하는 가벼운 스레드가 되고, 탯줄이 없다면 부모와 단절된 독자적인 주소 세계를 구축하는 프로세스가 됩니다.
새로운 구조체를 파지 않고 부모의 mm_struct를 뻔뻔하게 같이 씁니다. 장부에 mm_users 카운터만 하나 올리면 끝이라 눈부시게 빠릅니다. 멀티 스레딩 모델의 핵심 뼈대입니다.
번듯한 독립을 위해 새 mm_struct를 짜고 최상위 페이지 디렉터리(PGD)를 새로 팝니다. dup_mmap()을 통해 부모의 VMA들을 모조리 복제합니다. 이때 나만 쓰던 쓰기 가능 페이지조차 부모 자식 양쪽 모두 읽기 전용(Read-Only)으로 묶어버려 험난한 COW 전투를 준비시킵니다.
프로세스가 죽음을 맞이할 때 장부를 치우는 장의사입니다. mm 필드를 떼어내고(NULL) CPU를 lazy TLB 모드로 전환해 숨통을 틔워준 뒤, mmput()을 불러 VMA 구조체들과 비대해진 페이지 테이블을 갈기갈기 찢어 시스템에 반환합니다. (단, 커널 스레드는 애초에 빈털터리라 이 철거 작업 없이 조용히 떠납니다.)
핵심 O/X 퀴즈
1. 태어날 때 CLONE_VM 깃발을 들고나온 태스크는 무거운 복사 과정 없이 부모 프로세스의 주소 공간 전체를 내 집처럼 함께 공유한다.
2. 전통적인 무거운 fork() 과정에서는 부모와 자식 간의 우발적인 데이터 훼손을 막기 위해, 사적이고 쓰기 가능한(private writable) 페이지들조차 일시적으로 양쪽 모두 읽기 전용으로 설정되어 버린다.
3. 음지에서 묵묵히 일하던 커널 스레드 역시 수명이 다해 종료될 때는, 혹시 모를 누수를 막기 위해 프로세스의 사용자 주소 공간과 페이지 테이블을 해제하는 exit_mm()의 장례 절차를 엄격하게 거쳐야 한다.
힙의 은밀한 확장: brk(), sbrk(), malloc() 뒤에 숨겨진 진실HEAP / sys_brk()
개발자가 동적 메모리의 바다에서 헤엄칠 때 의지하는 malloc(), calloc(), free() 같은 함수들은 사실 커널이 직접 제공하는 것이 아니라, C 표준 라이브러리(glibc)가 편의를 위해 감싸놓은 껍데기에 불과합니다. 진짜로 힙(Heap) 영역의 경계선(brk)을 고무줄처럼 늘리고 줄이는 커널의 유일한 직통 시스템 콜은 brk()뿐입니다.
do_munmap() 철거반을 시켜 잘려나간 자투리 구역을 깔끔하게 철거하고 장부의 mm->brk를 낮춥니다.find_vma_intersection()을 날려 늘어날 자리에 남의 영역이 알박기를 하고 있지는 않은지 충돌 여부를 살핍니다.do_mmap()의 다이어트 버전인 do_brk()를 돌려 순수한 익명 메모리 영역을 쭉 늘려주고, 마침내 mm->brk 포인터를 새 영토 끝에 당당히 꽂습니다.개발자가 malloc()을 통해 기가바이트 단위의 힙 확장에 성공하며 환호성을 지를지라도, 그 순간 커널이 건네준 것은 가상의 영토가 그려진 빈 지도 한 장뿐입니다. 그 거대한 메모리 영역을 채울 실제 물리 페이지들은, 포인터가 데이터의 첫 바이트를 건드려 페이지 폴트의 비명을 지르는 가장 절박한 순간에야 비로소 수요 페이징의 이름으로 조달됩니다. 심지어 너무 큰 할당 요청은 brk()로 힙을 늘리는 대신, mmap()을 통해 저 멀리 외딴섬(독립된 익명 매핑)에 뚝 떨어뜨려 놓기도 합니다.
핵심 O/X 퀴즈
1. 시스템 구조상 힙(Heap) 영역의 현재 끝 지점을 관리하고 갱신하는 실질적인 커널 시스템 콜은 brk()다.
2. C 프로그램에서 흔히 쓰는 malloc()은 순수한 커널 내부 함수이며 사용자 모드의 C 라이브러리와는 아무런 관련이 없다.
3. 힙을 현재보다 더 넓은 주소로 확장하려 할 때, 커널은 무턱대고 늘리지 않고 그 진행 방향에 다른 메모리 영역이 가로막고 있는지 충돌 검사를 반드시 거친다.
대단원 마무리: 가상의 허락과 지연된 현실, 그리고 페이지 폴트라는 심판SUMMARY
오늘 길게 달려온 이 거대한 지식의 숲을 단 한 문장으로 관통한다면 다음과 같습니다.
"리눅스의 메모리 관리는 쿨하게 주소 사용권부터 내어주고, 무겁고 비싼 진짜 메모리 할당과 복사는 벼랑 끝까지 미루다가, 페이지 폴트라는 절묘한 알람이 울리는 순간 마법처럼 끼워 맞추는 기가 막힌 지연 전술의 결정체다."
- ①
- 커널은 물리 메모리가 아니라 '가상의 선형 주소 사용권'을 먼저 줍니다. 이 사용권의 단위가
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()은 이를 포장한 라이브러리입니다. 이 거대한 힙의 확장 역시 수요 페이징의 지연 법칙을 철저히 따릅니다.
마지막으로 스스로에게 던지는 질문입니다. 여러분의 프로세스가 어떤 주소를 건드렸다가 와장창 페이지 폴트가 터졌습니다. 이제 여러분은 초보자처럼 "앗, 버그 났다!"라고 머리를 감싸 쥐면 안 됩니다. 느긋하게 팔짱을 끼고, 커널의 빙의자가 되어 차례차례 물어봐야 합니다.