Chapter 4 Process Address Space
May 19, 2024
이 글은 Gorman 책 “Understanding the Linux Virtual Memory Manager"의 Chapter 4를 번역한 글입니다.
가상 메모리의 주요 이점 중 하나는 각각의 프로세스들이 물리 메모리에 매핑되는 자신만의 가상 주소 공간을 가진다는 것이다. 이 장에서 우리는 프로세스의 주소 공간과 리눅스가 이것을 관리하는 방법에 대해 다룰 것이다.
커널은 주소 공간을 커널 영역과 유저 영역으로 나누어, 서로 매우 다른 방식으로 관리된다. 예를 들어, 커널을 위한 할당은 즉시 수행되며 CPU에 어떤 프로세스가 돌고있는 지 상관없이 global하게 노출된다. vmalloc()은 예외적으로 minor page fault를 발생시켜서 프로세스 페이지 테이블의 동기화를 진행하지만, 여전히 페이지는 요청하는 즉시 할당된다. 프로세스의 경우, 할당은 오직 linear address space에서의 공간만 예약하며, 페이지 테이블 엔트리는 global한 read-only zero page에 매핑된다. 쓰기가 발생하는 경우, 페이지 폴트가 발생하여 새로운 페이지가 할당되고 페이지 테이블 엔트리에 매핑되며 쓰기가 가능하게 설정된다. Global zero-filled page와 동일하게 보이도록 하기위해 새롭게 할당한 페이지는 0으로 채워진다.
유저 영역은 신뢰할 수 없고 변할 것이라고 가정된다. 4.3절에서 다룰 Lazy TLB switch를 제외하면, 매 context switch 이후에 linear address space의 유저 영역은 변경될 것이다. 그러므로 커널은 유저 공간에서 발생할 수 있는 모든 예외와 주소 오류에 대비해야만 한다. 이 내용은 4.5절에서 다룬다.
이 장은 linear address space가 어떻게 구성되어있는 지, 각 요소의 목적이 무엇인 지에 대해 먼저 설명한다. 그런 다음 각 프로세스를 설명하기 위한 구조체들과, 그들이 어떻게 할당, 초기화, 해제되는 지를 다룬다. 다음으로, 우리는 프로세스 공간의 개별 영역들이 어떻게 생성되며 이와 관련된 함수들에는 무엇이 있는 지를 알려줄 것이다. 내용은 프로세스 주소 공간의 예외 처리, 페이지 폴트, 페이지 폴트를 해결하기 위해 발생하는 다양한 상황들로 이어진다. 마지막으로, 우리는 커널이 유저 공간으로부터 데이터를 어떻게 안전하게 주고받는 지를 설명할 것이다.
4.1 Linear Address Space
유저 관점에서 주소공간은 그저 평평한 선형 주소 공간일 뿐이다. 그러나 예측할 수 있듯이, 커널의 관점은 많이 다르다. 주소 공간은 컨텍스트 스위치가 발생할 때마다 바뀔 수 있는 유저 영역과, 항상 동일한 커널 영역으로 나뉜다. 나뉘는 지점은 PAGE_OFFSET 값으로 결정되며, x86에서 이 값은 0xC0000000이다. 이것은 3GiB는 프로세스가 이용가능하고, 나머지 1GiB는 항상 커널에 의해 매핑된다는 것을 의미한다. 커널 관점에서의 linear address space는 Figure 4.1에 나와있다.

PAGE_OFFSET의 시작 부분에는 실행할 커널 이미지를 로드하기위해 8MiB의 메모리가 예약된다. 8MiB는 단지 커널 이미지 로드를 위해 적절한 양으로 결정된 크기다. 3.6.1절에서 설명했듯이, 커널 이미지는 커널 페이지 테이블의 초기화 과정에서 해당 공간에 로드된다. 2절에서 말했듯이 커널 이미지 거의 바로 뒤에는, UMA 아키텍처의 경우, mem_map 배열이 저장된다. 해당 배열은 보통 ZONE_DMA을 사용하지 않기 위해 16MiB 위치에 저장된다. NUMA 아키텍처의 경우, 가상 mem_map의 일부분들이 이 영역에 흩뿌려져 있으며, 정확한 위치는 아키텍처마다 다르다.
PAGE_OFFSET과 VMALLOC_START - VMALLOC_OFFSET 사이의 영역은 물리 메모리 맵을 담고있다. 이 영역의 크기는 이용가능한 RAM의 양에 의해 결정된다. 3.6절에서 보았듯이, PAGE_OFFSET부터 시작하는 가상 주소 범위를 물리 메모리에 매핑하는 PTE들이 존재한다. 물리 메모리 맵과 vmalloc 주소 공간 사이에는 out of bound 에러를 막기위해 VMALLOC_OFFSET (x86의 경우 8MiB) 만큼의 틈이 존재한다. 예를 들어, 32MiB의 RAM이 있는 x86의 경우, VMALLOC_START는 PAGE_OFFSET + 0x02000000 + 0x00800000에 위치한다.
저메모리 시스템의 경우, 가상 주소 공간의 나머지에서 2개의 페이지만큼의 크기를 제외한 부분은, 연속된 가상 주소 공간에 대해 비연속적인 메모리 할당을 제공하는 vmalloc()에 의해 사용된다. 고메모리 시스템의 경우, PKMAP_BASE - 2*PAGE_SIZE까지만 vmalloc 영역으로 사용되고, 그 이후는 두가지 영역으로 사용된다. 첫번째는 PKMAP_BASE에서 시작하는데, kmap()을 통해 high memory 페이지들을 low memory에 매핑하기 위한 영역이며, 9장에서 더 자세히 논의한다. 두번째는 fixed virtual address mapping (고정 가상 주소 매핑)으로 FIXADDR_START에서 FIXADDR_TOP까지의 영역을 차지한다. 고정 가상 주소는 APIC (Advanced Programmable Interrupt Controller)처럼 컴파일 타임에 가상 주소를 알아야하는 경우에 필요하다. FIXADDR_TOP은 x86에서 0xFFFFE000으로 정적으로 정의되며, 이는 가상 주소의 끝에서 페이지 한 개의 크기만큼 뺀 값이다. 고정 매핑 영역의 크기는 컴파일 타임에 __FIXADDR_SIZE에 계산되며, FIXADDR_TOP에서 빼서 FIXADDR_START를 계산하는 데 사용된다.
vmalloc(), kmap() 그리고 고정 가상 주소 매핑에 요구되는 영역은 ZONE_NORMAL의 크기를 제한한다. 커널이 이러한 함수들을 필요로 하기 때문에, 주소 공간의 최상단에 최소한 VMALLOC_RESERVE만큼은 항상 예약된다. VMALLOC_RESERVE는 아키텍처마다 그 값이 다르며, x86의 경우 128MiB로 정의된다. 이것이 ZONE_NORMAL이 일반적으로 896MiB의 크기로 얘기되는 이유이다 (커널 가상 주소 공간은 1GiB이므로, 여기서 vmalloc으로 예약된 128MiB를 빼면 896MiB가 나온다).
4.2 Managing the Address Space
프로세스가 사용가능한 주소 공간은 mm_struct에 의해 관리되며, 이는 BSD의 vmspace 구조체와 비슷하다.
각 주소 공간은 page 크기로 align된 사용중인 메모리 region들로 구성된다. 그들은 절대 겹치지 않으며, 권한과 목적의 관점에서 서로 관련이 있는 페이지들의 묶음을 나타낸다. 이들은 struct vm_area_struct로 표현되며 BSD의 vm_map_entry 구조체와 유사하다. vm_area_struct로 묶이는 region으로는, malloc()으로 할당되는 프로세스 힙, shared library처럼 메모리 매핑된 파일, 또는 mmap()으로 할당된 anonymous memory 등이 있다. 이 region에 포함된 페이지들은 아직 할당되지 않았을 수도 있고, 메모리에 할당되어 active하게 사용중일 수도 있고, 아니면 할당되었다가 page out되었을 수도 있다.
만약 region이 파일로 백업되어 있다면, 이것의 vm_file 필드는 유효한 값을 저장하고 있을 것이다. vm_file->f_dentry->d_inode->i_mapping을 통해 해당 region과 관련된 address_space 구조체를 얻을 수 있다. address_space 구조체는 디스크에 대한 페이지 기반의 작업을 수행하는데 필요한 모든 파일시스템 관련 정보를 포함한다.
주소 공간과 관련된 구조체들 사이의 관계를 Figure 4.2에서 볼 수 있다. 주소 공관과 region들에 영향을 주는 시스템 콜들이 존재하며, 이는 Table 4.1에 나와있다.

| 시스템 콜 | 설명 |
|---|---|
| fork() | 새로운 프로세스와 주소공간은 생성한다. 모든 페이지들은 COW로 표시되며 페이지 폴트가 발생하여 자신만의 복사본을 만들기 전까지는 두 프로세스에 의해 공유된다. |
| clone() | clone()은 부모 프로세스와 일부 문맥을 공유하는 프로세스를 생성할 수 있도록 해주며, thread를 구현하는 데 사용된다. |
| mmap() | mmap()은 프로세스 선형 주소 공간에 새로운 region을 생성한다. |
| mremap() | 메모리의 region을 다시 매핑하거나 크기를 변경한다. 만약 어떤 매핑에 대해 가상 주소 공간이 이용 불가능하다면 region이 이동되는 경우가 발생할 수 있다 (이동을 caller가 금지하지 않았다면 말이다). |
| munmap() | region의 일부 또는 전체를 해제한다. 만약 region의 중간 부분만 unmap된 경우, 해당 region은 둘로 쪼개진다. |
| shmat() | shared memory segment를 프로세스 주소 공간에 붙인다. |
| execve() | 현재 주소 공간을 대체할 새로운 실행가능한 파일을 불러온다. |
| exit() | 주소 공간과 모든 region들을 삭제한다. |
4.3 Process Address Space Descriptor
프로세스 주소 공간은 mm_struct 구조체를 통해 표현된다. 즉, mm_struct는 프로세스마다 하나씩만 존재하며, 유저스페이스 스레드끼리 공유한다. 실제로 스레드들은 같은 mm_struct를 가리키는 모든 task_struct를 찾아서 task list에서 식별된다.
커널 스레드는 페이지 폴트를 일으키지 않거나 유저 공간에 접근하지 않을 것이기 때문에, 자신만의 mm_struct를 가질 필요가 없다. 한 가지 예외는 vmalloc 공간에 대한 페이지 폴트이다. 페이지 폴트 핸들링 코드에서는 이 상황을 특수 케이스로 다루며, 현재 페이지 테이블을 master 페이지 테이블의 정보로 업데이트한다. 커널 스레드는 mm_struct가 필요하지 않기때문에, task_struct->mm 필드는 항상 NULL이다. boot idle task와 같은 일부 태스크는 mm_struct를 절대 setup하지 않지만, 커널 스레드들은 daemonize()를 호출하여 exit_mm()을 호출하고, 이는 mm_struct의 usage counter를 감소시킨다.
TLB flush는 매우 비싼 작업이므로, PPC와 같은 아키텍처에서는, lazy TLB 기술을 적용하여 유저 공간의 페이지 테이블에는 접근하지 않는 프로세스에 의한 불필요한 TLB flush는 수행하지 않는다. 이는 커널 주소 공간은 항상 global하고, 따라서 모든 프로세서에게 동일하기 때문이다. TLB flush를 발생시키는 switch_mm()을 호출하는 대신에, 이전 태스크의 mm_struct를 현재 태스크의 task_struct->active_mm에 저장한다. 이 기술은 컨텍스트 스위치 시간을 효과적으로 개선하였다.
lazy TLB 모드로 들어갈 때에는, enter_lazy_tlb()를 호출하여 mm_struct가 프로세서들끼리 공유하고 있지 않음을 보장한다. lazy TLB의 두번째 사용 케이스는, 프로세스가 부모에 의해 회수되기를 기다리는 동한 start_lazy_tlb()를 호출하는 경우이다.
mm_struct는 두 타입의 “유저"들을 위해 mm_users, mm_count라고 불리는 참조 카운터를 가진다. mm_users는 페이지 테이블이나 파일 매핑처럼 mm_struct의 유저 공간 부분에 접근하는 프로세스들의 참조 횟수이다. 스레드와 swap_out()와 같은 코드는 이 카운터를 증가시켜서 mm_struct가 삭제되지 않도록 막을 것이다. 카운터 값이 0으로 떨어지면, mm_count를 감소시키기 전에 exit_mmap()이 모든 매핑을 삭제하고 페이지 테이블을 해체할 것이다.
mm_count는 “real” 유저가 존재하면 1로 초기화되며, 그 이후부터는 “anonymous users"의 참조 카운트로 작동한다. anonymous user는 유저 가상 주소 공간에는 관심이 없고, 단순히 mm_struct를 빌리고자 하는 유저를 말한다. 예시로는 lazy TLB switching을 사용하는 커널 스레드가 있다. 이 카운트가 0으로 떨어지면, mm_struct는 안전하게 삭제될 수 있다. mm_count가 필요한 이유는 anonymous user는 mm_struct와 관련된 모든 유저 매핑이 삭제되어 페이지 테이블을 해체해도 상관이 없더라도 mm_struct를 필요로 하기 때문이다.
mm_struct는 <linux/sched.h>에 아래와 같이 정의되어있다:
206 struct mm_struct {
207 struct vm_area_struct * mmap;
208 rb_root_t mm_rb;
209 struct vm_area_struct * mmap_cache;
210 pgd_t * pgd;
211 atomic_t mm_users;
212 atomic_t mm_count;
213 int map_count;
214 struct rw_semaphore mmap_sem;
215 spinlock_t page_table_lock;
216
217 struct list_head mmlist;
221
222 unsigned long start_code, end_code, start_data, end_data;
223 unsigned long start_brk, brk, start_stack;
224 unsigned long arg_start, arg_end, env_start, env_end;
225 unsigned long rss, total_vm, locked_vm;
226 unsigned long def_flags;
227 unsigned long cpu_vm_mask;
228 unsigned long swap_address;
229
230 unsigned dumpable:1;
231
232 /* Architecture-specific MM context */
233 mm_context_t context;
234 };
크기가 큰 이 구조체의 각 필드의 의미는 아래와 같다:
- mmap: 주소 공간의 모든 VMA region을 잇는 linked list의 head이다.
- mm_rb: VMA는 linked list 뿐만아니라, red-black tree로도 관리된다. 이 필드는 rb-tree의 root이다.
- mmap_cache: 마지막 find_vma()에서 탐색한 VMA가 곧 다시 사용되기를 기대하면서 이 필드에 캐싱한다.
- pgd: 이 프로세스의 Page Global Directory
- mm_users: 유저 주소 공간에 접근하는 유저들의 참조 카운터
- mm_count: “real” user가 생길 때 1로 초기화되며, 그 이후부터는 anonymous user의 참조 카운터로 사용된다.
- map_count: 사용중인 VMA의 개수
- mmap_sem: VMA list를 reader와 writer로부터 보호하기 위한 락이다. 이 락의 사용자는 오랫동안 락을 잡아야하는 상황일 것이고 sleep할 수도 있기 때문에, spinlock은 부적절하다. list의 reader는 이 세마포어를 down_read()를 통해 획득한다. writer의 경우, down_write()로 세마포어 락을 잡은 후, 실제로 VMA linked list를 수정할 때 page_table_lock이라는 이름의 스핀락을 추가로 잡는다.
- page_table_lock: mm_struct의 대부분의 필드를 보호한다. 페이지 테이블 뿐만 아니라, RSS count와 VMA의 수정을 보호한다. mmlist: 모든 mm_struct들이 이 필드를 통해 서로 연결된다. start_code, end_code: code 섹션의 시작과 끝 주소. start_data, end_data: data 섹션의 시작과 끝 주소. start_brk, brk: heap의 시작과 끝 주소. start_stack: stack 영역의 시작 주소. arg_start, arg_end: command line 인자들의 시작과 끝 주소. env_start, env_end: 환경 변수의 시작과 끝 주소. rss: Resident Set Size (RSS)는 프로세스의 resident page (실제로 물리 메모리에 올라온 페이지)의 수를 말한다. global zero page는 RSS에 포함되지 않음에 유념해야한다. total_vm: 프로세스의 모든 VMA region에 의해 차지되는 전체 메모리 공간. locked_vm: 메모리에 대해 lock이 걸려있는 resident page의 개수. def_flags: VM_LOCKED의 값만 가질 수 있다. 미래의 모든 매핑에 default로 lock을 걸 지를 결정하는 데 사용된다. cpu_vm_mask: SMP 시스템에서 이용가능한 CPU를 나타내는 bitmask이다. 이 mask는 IPI (Inter-Processor Interrupt)가 특정 함수를 실행해야 하는 지를 판단할 때 사용한다. 각 CPU에 대한 TLB flush를 수행할 때 중요하다. swap_address: pageout daemon이 프로세스 자체를 swap out할 때, 마지막으로 swap out한 주소를 기록하는 데 사용한다. dumpable: prctl()에 의해 set되며, 프로세스을 tracing할 때만 중요하다. context: 아키텍처 의존적인 MMU context이다.
mm_struct를 다루는 함수는 개수가 적으며, Table 4.2에 나와있다.
| 함수 | 설명 |
|---|---|
| mm_init() | mm_struct의 각 필드를 초기값으로 세팅하고, PGD를 할당하고, 스핀락을 초기화하는 등의 mm_struct 초기화 작업을 수행한다. |
| allocate_mm() | slab allocator를 통해 mm_struct를 할당한다. |
| mm_alloc() | allocate_mm()을 통해 mm_struct를 할당하고, mm_init()을 통해 이를 초기화한다. |
| exit_mmap() | mm_struct을 순회하며, 속해있는 모든 VMA를 unmap한다. |
| copy_mm() | 새로운 태스크를 위해 현재 태스크의 mm_struct를 복사한다. 이것은 오직 fork 과정에서만 호출된다. |
| free_mm() | slab allocator에게 mm_struct를 반환한다. |
4.3.1 Allocating a Descriptor
mm_struct 할당은 두 가지 함수를 통해 할 수 있다. 약간의 혼란이 있을 수 있겠지만, 그 둘은 본질적으로 동일하지만 약간의 중요한 차이가 있다. allocate_mm()은 slab allocator을 통해 mm_struct를 할당하는 preprocessor macro이다 (8장 참조). mm_alloc()은 slab을 통해 할당하고 mm_init()을 통해 이를 초기화한다.
4.3.2 Initialising a Descriptor
시스템의 초기 mm_struct는 init_mm이라고 하며, 이는 INIT_MM() 매크로를 통해 컴파일 타임에 정적으로 초기화된다.
238 #define INIT_MM(name) \
239 { \
240 mm_rb: RB_ROOT, \
241 pgd: swapper_pg_dir, \
242 mm_users: ATOMIC_INIT(2), \
243 mm_count: ATOMIC_INIT(1), \
244 mmap_sem: __RWSEM_INITIALIZER(name.mmap_sem), \
245 page_table_lock: SPIN_LOCK_UNLOCKED, \
246 mmlist: LIST_HEAD_INIT(name.mmlist), \
247 }
init_mm이 세팅되고 나면, 새로운 mm_struct들은 그들의 부모 mm_struct를 템플릿으로 사용하여 생성된다. copy_mm()이 그 작업을 담당하며, 이는 프로세스 관련 필드를 init_mm을 이용하여 초기화한다.
4.3.3 Destroying a Descriptor
새로운 유저는 atomic_inc(&mm->mm_users)를 통해 사용 카운트를 증가시키며, mmput()을 통해 감소시킨다. 만약 mm_users가 0으로 감소하게되면, 유저 공간에 대한 사용자가 더 이상 존재하지 않다는 것을 의미하므로, exit_mmap()을 통해 매핑된 region과 페이지 테이블을 삭제한다. 페이지 테이블과 VMA의 모든 유저는 mm_struct 사용자 하나로 간주되므로, mm_count는 mmdrop()을 통해 감소된다. mm_count가 0으로 감소하게되면, mm_struct가 삭제된다.
Memory Regions
프로세스의 주소 공간에서는 아주 일부분만 사용된다. 사용되는 각 region은 vm_area_struct로 표현되며, 서로 겹치지 않으며 각각은 동일한 protection과 목적을 가진 주소들의 집합을 나타낸다. 예시로는 주소공간에 로드되는 read-only 공유 라이브러리 또는 프로세스 heap을 들 수 있다. 특정 프로세스의 매핑된 region들은 그 전체 리스트를 proc 인터페이스인 /proc/PID/maps를 통해 확인할 수 있다 (여기서 PID는 보고자 하는 프로세스의 process ID이다).
region은 Figure 4.2에서 볼 수 있듯이, 많은 구조체들과 관련되어 있다. 맨 위에는 vm_area_struct가 있는데, anonymous memory의 경우에는 이 구조체 하나만으로 충분히 표현할 수 있다.
region이 file로 백업되어있는 경우에는, vm_file 필드를 통해 struct file 구조체를 저장하며, struct file은 struct inode에 대한 포인터를 가지고 있다. inode는 struct address_space를 참조하기 위해 사용되며, address_space는 file의 모든 private 정보를 포함한다. 이 정보에는 디스크에서 페이지를 읽어오거나 쓰거나하는 것과 같은 파일시스템 관련 동작을 수행하는 파일시스템 함수들에 대한 포인터가 포함된다.
struct vm_area_struct는 아래와 같이 <linux/mm.h>에 선언되어있다:
44 struct vm_area_struct {
45 struct mm_struct * vm_mm;
46 unsigned long vm_start;
47 unsigned long vm_end;
49
50 /* linked list of VM areas per task, sorted by address */
51 struct vm_area_struct *vm_next;
52
53 pgprot_t vm_page_prot;
54 unsigned long vm_flags;
55
56 rb_node_t vm_rb;
57
63 struct vm_area_struct *vm_next_share;
64 struct vm_area_struct **vm_pprev_share;
65
66 /* Function pointers to deal with this struct. */
67 struct vm_operations_struct * vm_ops;
68
69 /* Information about our backing store: */
70 unsigned long vm_pgoff;
72 struct file * vm_file;
73 unsigned long vm_raend;
74 void * vm_private_data;
75 };
- vm_mm: VMA가 속한 mm_struct이다.
- vm_start: region의 시작 주소이다.
- vm_end: region의 끝 주소이다.
- vm_next: 주소 공간의 모든 VMA들은 주소 기반으로 정렬되어 이 필드를 통해 singly linked list로 연결된다. 흥미로운 점은, 이것이 커널에서 singly linked list가 사용되는 몇 안되는 경우 중에 하나라는 것이다.
- vm_page_prot: VMA의 각 PTE를 위한 protection flag이다.
- vm_flags: VMA의 protection과 propertiy를 설명하는 flag들이 저장된다. <linux/mm.h>에 정의되어 있으며, Table 4.3에서 확인할 수 있다.
- vm_rb: linked list 뿐아니라, 빠른 탐색을 위해 모든 VMA들은 red-black tree로도 관리된다. 이는 페이지 폴트 핸들링처럼 빠르고 정확하게 region을 찾아야할 때 사용되며, 특히 많은 region들이 매핑되어있는 경우에 중요하다.
- vm_next_share: 공유 라이브러리같은 파일 매핑 기반의 공유 VMA region들은 이 필드를 통해 서로 연결된다.
- vm_pprev_share: vm_next_share와 같이 사용된다.
- vm_ops: open(), close(), nopage()에 대한 함수 포인터를 포함한다. 이들은 disk와 정보를 동기화하는데 사용된다.
- vm_pgoff: 메모리 매핑된 파일 내에서 page align된 오프셋을 저장한다.
- vm_file: 매핑된 파일의 struct file 구조체의 포인터이다.
- vm_raend: read-ahead window의 마지막 주소이다. fault가 발생하면, 해당 페이지 외의 추가적인 많은 페이지들을 메모리에 함께 로드한다. 이 필드는 몇 개의 추가적인 페이지를 로드할 것인지를 결정한다.
- vm_private_data: private 정보를 저장하기 위해 일부 디바이스 드라이버에 의해 사용된다. memory manager와는 관련이 없다.
Table 4.3: Memory Region Flags
- Protection Flags
- VM_READ: read 될 수 있음.
- VM_WRITE: write 될 수 있음.
- VM_EXEC: 실행될 수 있음.
- VM_SHARED: 공유될 수 있음.
- VM_DONTCOPY: VMA가 fork 과정에서 복사될 수 없음.
- VM_DONTEXPAND: region의 크기가 변경될 수 없음. 이 플래그는 사용되지 않음.
- mmap Related Flags
- VM_MAYREAD: VM_READ가 set 될 수 있음.
- VM_MAYWRITE: VM_WRITE가 set 될 수 있음.
- VM_MAYEXEC: VM_EXEC이 set 될 수 있음.
- VM_MAYSHARE: VM_SHARE이 set 될 수 있음.
- VM_GROWSDOWN: Shared segment가 작아질 수 있음.
- VM_GROWSUP: Shared segment가 커질 수 있음.
- VM_SHM: 페이지들이 shared SHM memory segment에 의해 사용됨.
- VM_DENYWRITE: MAP_DENYWRITE를 인자로 mmap()을 호출하면 set 됨. 지금은 사용되지않음.
- VM_EXECUTABLE: MAP_EXECUTABLE을 인자로 mmap()을 호출하면 set 됨. 지금은 사용되지않음.
- VM_STACK_FLAGS: stack을 세팅하는 setup_arg_flags() 함수가 사용하는 플래그임.
- Locking Flags
- VM_LOCKED: set 되어있으면 해당 페이지들은 swap out되지 않음. mlock()을 통해 set 됨.
- VM_IO: 이 영역은 장치의 I/O를 위해 mmap된 영역임을 알려줌. 해당 영역은 core dump되지않음.
- VM_RESERVED: 이 영역은 swap out 하지 않음. 장치 드라이버에 의해 사용됨.
- madvise() Flags
- VM_SEQ_READ: 페이지들이 시퀀셜하게 접근될 것이라는 힌트임.
- VM_RAND_READ: readahead가 쓸모없을 것이라는 것을 알려주는 힌트임.
모든 region들은 vm_next 필드에 linked list로 주소를 기준으로 정렬되어 연결되어있다. free 영역을 찾을 때에는 단순히 리스트를 순회하여 찾으면 되지만, page fault처럼 특정 주소에 해당하는 VMA를 찾을 때는 이렇게 하면 안된다. 이 경우에는, red-black 트리를 순회하며 이는 O(logN)의 시간복잡도를 가진다. rb tree도 주소를 기준으로 정렬되어있고, 따라서 현재 노드보다 작은 주소는 왼쪽 leaf에, 큰 주소는 오른쪽 leaf에 존재한다.
4.4.1 Memory Region Operations
VMA가 지원하는 operation으로는 open(), close(), 그리고 nopage()가 있다. vma->vm_ops 필드에는 struct vm_operations_struct 자료형으로 해당 operation들이 저장된다. 해당 구조체에는 3개의 함수 포인터가 포함되며, <linux/mm.h>에 다음과 같이 선언되어있다:
133 struct vm_operations_struct {
134 void (*open)(struct vm_area_struct * area);
135 void (*close)(struct vm_area_struct * area);
136 struct page * (*nopage)(struct vm_area_struct * area,
unsigned long address,
int unused);
137 };
open()과 close()는 region이 생성되고 삭제될 때마다 호출될 것이다. 이 함수들은 region이 open되고 close될 때마다 추가적인 작업을 수행해야하는 일부 디바이스, 한 파일시스템, 그리고 System V shared regions에 의해서만 사용된다. 예를 들어, System V open() 콜백은 shared segment를 사용하는 VMA의 수를 증가시킬 것이다 (shp->shm_nattch).
중요한 것은 nopage() 콜백 함수이다. 이 콜백 함수는 page fault시에 do_no_page() 함수에서 사용된다. nopage()는 페이지를 page cache에 위치시키거나, 페이지를 할당하고 요청된 데이터를 채워서 반환한다.
대부분의 매핑된 파일들은 generic_file_vm_ops라고 불리는 vm_operations_struct를 사용한다. 이것은 오직 filemap_nopage() 함수만 등록한다. 이 nopage() 함수는 페이지를 page cache에 위치시키거나 디스크로부터 정보를 읽어올 것이다. 해당 구조체는 mm/filemap.c에 아래와 같이 선언되어있다:
2243 static struct vm_operations_struct generic_file_vm_ops = {
2244 nopage: filemap_nopage,
2245 };
4.4.2 File/Device backed memory regions
Region이 파일로 백업되어 있는 경우, vm_file은 Figure 4.2에서 볼 수 있듯이 관련된 address_space와 연결되어있다. 해당 구조체는 디스크에 flush되어야하는 dirty page의 수와 같이, 파일시스템과 관련이 있는 정보들을 포함한다. 이는 <linux/fs.h>에 다음과 같이 선언되어있다:
406 struct address_space {
407 struct list_head clean_pages;
408 struct list_head dirty_pages;
409 struct list_head locked_pages;
410 unsigned long nrpages;
411 struct address_space_operations *a_ops;
412 struct inode *host;
413 struct vm_area_struct *i_mmap;
414 struct vm_area_struct *i_mmap_shared;
415 spinlock_t i_shared_lock;
416 int gfp_mask;
417 };
- clean_pages: 백업 스토리지와 동기화가 필요없는 clean page들의 리스트이다.
- dirty_pages: 백업 스토리지와 동기화가 필요한 dirty page들의 리스트이다.
- locked_pages: 메모리에 락이 잡혀있는 페이지들의 리스트이다.
- nrpages: address space에 의해 사용되고 있는 resident page들의 수이다.
- a_ops: 파일시스템을 다루기 위한 함수의 구조체이다. 각 파일시스템들은 가끔 generic 함수들을 사용하기는 하지만, 대부분 자신만의 address_space_operations를 가지고 있다.
- host: 파일이 속해있는 host inode이다.
- i_mmap: 이 address_space를 사용중인 private mapping들의 리스트이다.
- i_mmap_shared: address_space의 mapping을 공유하는 VMA들의 리스트이다.
- i_shared_lock: 이 구조체를 보호하기위한 스핀락이다.
- gfp_mask: __alloc_pages()를 통해 새로운 페이지를 할당할 때 사용할 마스크이다.
Memory manager는 주기적으로 정보를 디스크에 flush 해야한다. Memory manager는 어떤 정보가 어떻게 디스크에 쓰여지는 지에 관심이 없으므로, a_ops 구조체가 관련된 함수를 호출하는데 사용된다. 이는 <linux/fs.h>에 선언되어있다:
385 struct address_space_operations {
386 int (*writepage)(struct page *);
387 int (*readpage)(struct file *, struct page *);
388 int (*sync_page)(struct page *);
389 /*
390 * ext3 requires that a successful prepare_write() call be
391 * followed by a commit_write() call - they must be balanced
392 */
393 int (*prepare_write)(struct file *, struct page *,
unsigned, unsigned);
394 int (*commit_write)(struct file *, struct page *,
unsigned, unsigned);
395 /* Unfortunately this kludge is needed for FIBMAP.
* Don't use it */
396 int (*bmap)(struct address_space *, long);
397 int (*flushpage) (struct page *, unsigned long);
398 int (*releasepage) (struct page *, int);
399 #define KERNEL_HAS_O_DIRECT
400 int (*direct_IO)(int, struct inode *, struct kiobuf *,
unsigned long, int);
401 #define KERNEL_HAS_DIRECT_FILEIO
402 int (*direct_fileIO)(int, struct file *, struct kiobuf *,
unsigned long, int);
403 void (*removepage)(struct page *);
404 };
각 필드들은 모두 함수 포인터들이며, 다음과 같은 역할을 한다:
- writepage: 페이지를 디스크에 쓴다. 써야하는 부분의 파일 내에서의 오프셋은 페이지 구조체 안에 저장되어있다. 블록을 찾는 것은 파일시스템 코드의 역할이다. buffer.c의 block_write_full_page() 함수를 참조해라.
- readpage: 디스크에서 페이지를 읽는다. buffer.c의 block_read_full_page()를 참조하라.
- sync_page: dirty page를 디스트와 동기화한다. buffer.c의 block_sync_page()를 참조하라.
- prepare_write: 유저 공간의 데이터가 디스크에 쓰이게 될 페이지로 복사될 때 호출되는 함수이다. journaled filesystem의 경우, 이 함수는 파일시스템의 로그가 최신임을 보장한다. 일반적인 파일시스템의 경우, 이는 필요한 버퍼 페이지가 할당되는 것을 보장한다. buffer.c의 block_prepare_write()를 참조하라.
- commit_write: 이 함수는 유저공간의 데이터가 복사된 후, 해당 정보를 디스크에 commit하기위해 호출된다. buffer.c의 block_commit_write()를 참조하라.
- bmap: raw IO가 수행될 수 있도록 block을 매핑한다. 대부분의 경우 파일시스템 관련 코드에서만 사용되지만, swap partition 대신 swap file로 백업된 페이지를 swap out하는 경우에도 사용된다.
- flushpage: 페이지를 release하기전에 해당 페이지에 대기중인 IO가 없음을 보장한다. buffer.c의 discard_bh_page()를 참조하라.
- releasepage: 페이지를 해제하기전에 관련된 모든 버퍼를 flush하는 것을 시도한다. try_to_free_buffers()를 참조하라.
- direct_IO: 이 함수는 inode에 대한 direct IO를 수행할 때 사용된다. #define이 존재하는 이유는
- direct_fileIO: struct file 구조체를 이용하여 direct IO를 수행할 때 사용된다.
- removepage: remove_page_from_inode_queue()가 page cache에서 페이지를 삭제할 때 선택적으로 사용하는 콜백 함수이다.
4.4.3 Creating A Memory Region
mmap() 시스템콜은 프로세스가 새로운 메모리 region을 생성할 때 사용한다. x86에서 이 함수는 sys_mmap2()를 호출하고, 이는 다시 do_mmap2()를 같은 인자로 호출한다. do_mmap2()는 do_mmap_pgoff()가 필요로 하는 인자들을 획득하는 역할을 하며, do_mmap_pgoff()는 모든 아키텍처에 대해서 새로운 영역을 생성하기 위한 중요한 역할을 한다.
do_mmap2()는 먼저 MAP_DENYWRITE와 MAP_EXECUTABLE 비트를 인자로 받은 flag에서 clear한다. 이는 mmap() 메뉴얼 페이지에서 확인할 수 있듯이, 리눅스가 해당 비트들을 무시하기 때문이다. 만약 파일이 매핑되는 것이라면, do_mmap2()는 인자로 받은 파일 디스크립터를 통해 struct file을 찾고, mm_struct->mmap_sem 락을 잡은 후 do_mmap_pgoff()를 호출할 것이다.

do_mmap_pgoff()는 먼저 기본적인 sanity 검사를 수행한다. 파일이나 장치가 매핑되는 것이라면, 이는 먼저 적절한 파일시스템 또는 장치 함수가 이용가능한 지를 체크한다. 그런 다음, 이는 매핑의 크기가 페이지 크기로 정렬되어있는 지 확인하고, 만약 매핑이 커널 주소 공간에 대한 것이라면 매핑을 중단한다. 다음으로 매핑의 크기가 pgoff의 범위를 넘어가지 않음을 보장하며, 마지막으로 프로세스가 이미 너무 많은 region을 매핑하지 않았음을 보장한다.
함수의 나머지는 매우 많은 일을 하지만 넓게 말하면 다음과 같은 스텝으로 구성된다고 할 수 있다:
- 인자값의 sanity check을 수행한다.
- 메모리 매핑을 위한 충분히 큰 선형 주소 공간을 탐색한다. 만약 파일시스템 또는 장치가 get_unmapped_area() 함수를 제공한다면 이를 호출할 것이고, 그렇지 않다면 arch_get_unmapped_area()를 호출할 것이다.
- VM flags를 계산하고 파일 접근 권한에 대해 이를 검사한다.
- 매핑을 수행할 오래된 영역이 있는 경우, 새 매핑에 적합하도록 수정한다.
- 슬랩 할당자를 통해 vm_area_struct를 할당하고, 이를 초기화한다.
- 새 VMA를 링크한다.
- 파일시스템 또는 장치의 mmap 함수를 호출한다.
- 통계값을 업데이트하고 종료한다.
4.4.4 Finding a Mapped Memory Region
find_vma()는 페이지 폴트와 같은 작업 도중 특정 주소가 속한 VMA를 찾기위해 호출하는 함수이다. find_vma()와 메모리 region들에 영향을 주는 다른 API 함수들은 Table 4.3에서 확인할 수 있다.
이는 먼저 마지막 find_vma()가 반환했던 region을 캐싱하고 있는 필드인 mmap_cache를 확인하며, 몇번의 연속된 호출 동안에는 같은 region을 필요로 할 확률이 높다. 만약 mmap_cache가 원하는 region이 아닌 경우, mm_rb 필드에 저장된 red-black tree를 순회한다. 만약 원하는 주소가 어떠한 VMA에도 포함되지 않는다면, 함수는 요청된 주소와 가장 가까운 VMA를 반환한다. 그러므로 find_vma()의 caller는 반환된 VMA가 실제로 원하는 주소를 포함하는 지 확인해야한다.
find_vma_prev()라는 함수도 제공되는데, 이는 find_vma()와 하는 일이 거의 유사하지만, VMA의 prev VMA에 대한 포인터도 같이 반환한다는 점에서 다르며, 이는 VMA가 singly linked list로 관리되기 때문에 필요하다. find_vma_prev()는 드물게 사용되며, 두 VMA가 병합되어야 하는 지를 판단할 때 주로 사용된다. 이는 또한 memory region을 삭제할 때 singly linked list를 업데이트하기 위해 사용된다.
마지막으로 VMA를 탐색하기 위한 함수로는 find_vma_intersection()이 제공되며, 이는 주어진 주소 범위를 걸치고 있는 VMA를 찾기 위해 사용된다. 이 함수의 중요한 사용처는 do_brk()에서 region 크기를 늘리는 경우이다. 이때 region의 증가한 범위가 기존의 region과 중첩되지 않는 것을 보장하기 위해 이를 사용한다.
Table 4.3: Memory Region VMA API
- struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
- 주어진 주소를 포함하는 VMA를 찾는다.
- 만약 해당하는 region이 존재하지 않는다면, 요청된 주소와 가장 근접한 VMA를 반환한다.
- struct vm_area_struct * find_vma_prev(struct mm_struct * mm, unsigned long addr, struct vm_area_struct **pprev)
- prev VMA의 주소도 같이 반환한다는 점을 제외하고는 find_vma()와 동일한 기능을 한다.
- sys_mprotect()를 제외하고는 거의 사용하지 않는다.
- struct vm_area_struct * find_vma_prepare(struct mm_struct * mm, unsigned long addr, struct vm_area_struct ** pprev, rb_node_t *** rb_link, rb_node_t ** rb_parent)
- linked list에서의 prev VMA의 주소와 rb tree의 삽입을 위해 필요한 rb node들의 주소를 반환한다는 점을 제외하고는 find_vma()와 동일하다.
- struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
- 주어진 주소 범위를 교차하는 VMA를 반환한다.
- 어떤 선형 주소 영역이 VMA에 의해 사용되고 있는 지를 확인할 때 유용하다.
- int vma_merge(struct mm_struct * mm, struct vm_area_struct * prev, rb_node_t * rb_parent, unsigned long addr, unsigned long end, unsigned long vm_flags)
- 주어진 VMA를 새로운 주소 범위를 포함하도록 확장하고자 시도한다. 만약 VMA를 확장할 수 없으면, 대신에 다음 VMA를 뒤로 확장할 수 있는 지 확인한다.
- file이나 device 매핑이 없고 권한 문제가 없다면 region들은 서로 병합될 것이다.
- unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags)
- 요청된 크기의 메모리를 커버할 수 있도록 충분히 큰 free region의 주소를 반환한다.
- void insert_vm_struct(struct mm_struct *, struct vm_area_struct *)
- 새로운 VMA를 선형 주소 공간에 삽입한다.
4.4.5 Finding a Free Memory Region
새로운 영역이 메모리에 매핑되면, 해당 매핑을 커버할 수 있을만큼 충분히 큰 free region을 찾아야한다. free region을 찾는 일을 담당하는 함수가 바로 get_unmapped_area()이다.
Figure 4.5에서 볼 수 있듯이, 매핑되지 않은 영역을 찾는 것은 그렇게 많은 일을 요구하지 않는다. 함수는 많은 인자를 받는다. 매핑할 file이나 device를 나타내는 struct file, 파일 내에서 매핑할 영역의 오프셋인 pgoff가 인자로 전달된다. 또한 매핑의 주소인 address와 매핑의 length가 인자로 전달된다. 마지막으로는 영역의 보호 flags가 인자로 전달된다.

만약 비디오 카드와 같은 장치가 매핑된다면, 연관된 f_op->get_unmapped_area() 함수가 사용된다. 그 이유는 장치 또는 파일들은 일반적인 코드가 알 수 없는, 예를 들면 주소가 특정 가상 주소로 align되어야 한다던지,와 같은 추가적인 요구사항을 가질 수도 있기 때문이다.
만약 특별한 요구사항이 없다면, 아키텍처 의존적인 함수인 arch_get_unmapped_area()가 호출된다. 모든 아키텍처가 자신만의 함수를 제공하는 것은 아니다. 제공하지 않는 아키텍처를 위해서, mm/mmap.c에 generic version이 제공된다.
4.4.6 Inserting a memory region
새로운 메모리 region을 삽입하기 위한 주요 함수는 insert_vm_struct()이며, call graph는 Figure 4.6에서 확인할 수 있다. 이는 굉장히 단순한 함수로, 먼저 find_vma_prepare()을 호출하여 사이에 삽입하기위한 두 VMA와 red-black tree에 삽입하기 위해 필요한 노드를 찾는다. 그 다음에는 __vma_link()를 호출하여 새로운 VMA를 연결하기 위한 작업을 수행한다.

insert_vm_struct()는 map_count 필드를 증가시키지 않기때문에 거의 사용하지 않는다. 그 대신, map_count를 증가시키면서 동일한 작업을 수행하는 __insert_vm_struct()를 보통 사용한다.
vma_link()와 __vma_link()가 link를 위해 제공된다. vma_link()는 lock을 잡지 않고 호출하도록 요구된다. 이는 먼저 필요한 모든 락(VMA가 file mapping이라면 file에 대한 락도 포함)을 잡고 __vma_link()를 호출하여 연관된 리스트들에 VMA를 위치시킨다.
중요한 사실은 많은 함수들이 insert_vm_struct() 대신에 find_vma_prepare()를 호출한 후 vma_link()를 통해 VMA를 연결하는 것을 선호한다는 것이다. 그 이유는 insert_vm_struct()를 사용하면 불필요하게 tree를 여러번 순회하게되기 때문이다.
__vma_link()에서의 linking은 세가지 함수로 분리되어있는 세 개의 단계로 구성된다. __vma_link_list()는 linear, singly linked list에 VMA를 삽입한다. 만약 이것이 주소 공간에서의 첫 매핑이라면 (i.e. prev is NULL), 이는 red-black tree에서 root 노드가 될 것이다. 두번째 단계는 노드를 __vma_link_rb() 함수를 red-black tree에 연결하는 것이다. 마지막 단계는 __vma_link_file()을 통해 file share mapping을 고치는 것이며, 이는 기본적으로 vm_pprev_share와 vm_next_share 필드를 통해 linked list에 VMA를 삽입한다.
4.4.7 Merging contiguous regions
Linux는 파일 또는 권한 문제가 없다면 인접한 메모리 영역을 병합하는 역할을 하는 merge_segments() 함수를 사용하곤했다. 그 목적은 sys_mprotect()와 같은 많은 작업들이 매핑의 수를 증가시키기 때문에, VMA의 수를 줄이기 위함이였다. 이것은 많은 매핑들이 순회되어야 하기때문에 비싼 작업이고, 특히 많은 매핑을 가진 어플들이 merge_segments()에서 긴 시간을 소비했기 때문에 이 함수는 제거되었다.
현재 같은 역할을 하는 함수로는 vma_merge()가 있으며, 이는 두 공간에서 사용된다. 첫번째 사용자는 sys_mmap()으로, anonymous region의 경우 병합할 수 있을 확률이 높기 때문에, anonymous region을 매핑하는 경우 이 함수를 호출한다. 두 번째는 do_brk()로, 기존의 영역을 확장하여 새로 할당된 영역과 병합할 때 사용한다. 두 regions을 병합하는 대신, vma_merge()는 기존의 region이 새로운 할당을 만족하도록 확장될 수 있는 지를 체크하여, 새로운 region을 할당할 필요를 없앤다. Region은 파일이나 장치 매핑이 없거나, 두 영역의 권한이 동일한 경우에 확장될 수 있다.
병합을 수행하기위해 명시적으로 사용되는 함수는 존재하지 않지만, region들은 다른 곳에서 병합되기도 한다. 첫 번째는 sys_mprotect() 도중 권한이 변경된 영역이 인접한 영역과 동일하게 되면 병합하는 경우이다. 두 번째는 move_vma() 도중 비슷한 영역이 인접하게 위치하게 되면 병합하는 경우이다.
4.4.8 Remapping and moving a memory region
mremap()은 기존의 메모리 매핑을 늘리거나 줄이기 위해 제공되는 시스템 콜이다. 이는 sys_mremap() 함수로 구현되며, 만약 region이 늘어나거나 다른 region과 중첩되게되고 MREMAP_FIXED 플래그가 설정되어 있지않다면 해당 region을 옮길 수도 있다. Call graph는 Figure 4.7에서 볼 수 있다.

만약 region을 옮겨야한다면, do_mremap()은 먼저 get_unmapped_area()를 호출하여 새로운 크기의 매핑을 포함할 수 있을만틈 큰 region을 찾고, move_vma()를 호출하여 기존의 VMA를 새로운 곳으로 옮긴다. move_vma()의 call graph는 Figure 4.8에서 볼 수 있다.

move_vma()는 옮겨간 곳 근처의 VMA와 합칠 수 있는 지를 먼저 확인한다. 만약 합칠 수 없다면, 새로운 VMA는 말그대로 한번에 하나의 PTE씩 할당된다. 다음으로는 move_page_tables()를 호출하여 기존 매핑의 페이지 테이블 엔트리를 새로운 곳에 복사한다 (Figure 4.9를 참조하라). 페이지 테이블을 옮기기 위한 더 나은 방식이 존재할 수도 있지만, 지금의 방식은 backtracking이 직관적이라 에러 복구 과정을 쉽게 해준다는 장점이 있다.

페이지의 내용 자체는 복사되지 않는다. 대신에, zap_page_range()가 호출되어 이전 매핑의 모든 페이지를 swap out하거나 제거하며, 추후에 페이지 폴트 핸들링 코드가 페이지를 스토리지 또는 파일로부터 다시 swap in 하거나, 장치 의존적인 do_nopage() 함수를 호출할 것이다.
4.4.9 Locking a Memory Region

Linux는 sys_mlock()을 통해 구현된 mlock() 시스템 콜을 통해 특정 주소 범위의 페이지들을 메모리에 락을 걸 수 있다. sys_mlock()의 call graph는 Figure 4.10에서 확인할 수 있다. 크게 보면 이 함수는 단순하다. 락을 걸 주소 범위에 대한 VMA를 생성하고, VM_LOCKED 플래그를 설정하고, 모든 페이지들을 make_pages_present()를 통해 강제로 present 상태로 만든다. sys_mlockall()을 통해 구현되는 시스템 콜인 mlockall() 또한 제공되며, 이는 sys_mlock()이 하는 일을 프로세스의 모든 VMA에 대해서 진행한다. 두 함수 모두 핵심 함수인 do_mlock()에 의존하며, 이는 region을 수정하기 위해 어떤 함수가 필요한 지 결정하는 일과 관련된 VMA들을 찾는 일을 실제로 수행한다.
어떤 메모리에 lock을 걸기 위해서는 몇가지 제약이 있다. VMA가 page 크기로 align되어있기 때문에, 주소 범위 또한 page 크기로 align되어있어야 한다. 이는 단순히 주소 범위를 가장 가까운 page align된 범위로 반올림하여 해결한다. 두 번째 조건은 시스템 관리자가 설정하는 RLIMIT_MLOCK을 초과하지 않는 것이다. 마지막 조건은 각 프로세스가 물리 메모리의 절반만을 한 번에 잠글 수 있다는 것이다. 이는 기능적으로 다소 비합리적인데, 각각의 자식이 일부를 잠그도록 프로세스가 여러 번 fork 하는 것을 막을 방법이 없기 때문이다. 그러나 페이지를 잠글 수 있는 것은 root 프로세스만이 허용되므로 큰 차이는 없다. root 프로세스는 신뢰할 수 있고, 무엇을 하고 있는 지 알고 있다고 가정하는 것은 안전하다. 만약 그렇지 않다면, 결국 시스템이 망가진 것은 시스템 관리자의 책임으로 봐야하며, 그에 따른 모든 문제도 그가 감수해야 할 것이다.
4.4.10 Unlocking the region
시스템 콜인 munlock()과 munlockall()은 각각 sys_munlock()과 sys_munlockall()으로 매핑되어 locking 함수에 대한 상응하는 기능을 제공한다. 이 함수들은 많은 검사를 수행할 필요가 없기 때문에 locking 함수보다 훨씬 간단하다. 두 함수 모두 같은 do_mmap() 함수에 의존하여 영역을 수정한다.
4.4.11 Fixing up regions after locking
잠금 또는 잠금 해제를 할 때, VMA는 4가지 방식 중 하나로 영향을 받으며, 각각은 mlock_fixup()을 통해 수정되어야 한다. 잠금이 전체 VMA에 영향을 미치는 경우에는 mlock_fixup_all()이 호출된다. 두 번째 상황은 mlock_fixup_start()에 의해 처리되며, 이는 영역의 시작 부분이 잠겨 새로운 VMA가 할당되어 새로운 영역을 매핑해야 할 때이다. 세 번째 상황은 mlock_fixup_end()가 처리하는데, 예상할 수 있듯이 영역의 끝 부분이 잠긴 경우이다. 마지막으로, mlock_fixup_middle()은 영역의 중간 부분이 매핑되어 두 개의 새로운 VMA가 할당되어야 하는 경우를 처리한다.
흥미로운 점은 잠금으로 인해 생성된 VMA가 잠금이 해제되더라도 결합되지 않는다는 것이다. 프로세스가 반복적으로 같은 영역에 잠금을 적용할 것으로 예상되며, 지속적으로 영역을 합치고 분할하는 것은 처리 성능을 소모하는 일이므로 그만한 가치가 없다고 간주된다.
4.4.12 Deleting a memory region
메모리 영역을 삭제하는 일은 do_munmap()이 담당한다. 이는 다른 메모리 영역 관련 함수들과 비교하여 상대적으로 간단하며, 기본적으로 세가지 부분으로 나눌 수 있다. 첫 번째는 unmap될 영역을 고려해 red-black tree을 수정하는 것이다. 두 번째는 해당 영역과 관련된 페이지들과 PTE들을 해제하는 것이며, 세 번째는 hole이 생겼다면 region들을 수정하는 것이다.

Red-black tree의 순서를 보장하기 위해, unmap으로 인해 영향을 받는 모든 VMA들을 free라고 불리는 linked list에 넣고, red-black tree로부터 rb_erase()를 이용하여 제거한다. 여전히 존재하는 region들은 그들의 새로운 주소를 이용하여 다시 추가될 것이다.
다음으로, free linked list를 순회하며 VMA가 부분 unmap이 아닌지 확인한다. 영역이 부분적으로만 unmap 될 예정이라 하더라도, 공유 파일 매핑을 제거하기 위해 remove_shared_vm_struct()가 호출된다. 다시 말해, 이것이 부분 unmap인 경우, 수정 과정에서 재생성될 것이다. zap_page_range()는 unmap 될 예정인 영역과 관련된 모든 페이지를 제거하기 위해 호출되며, 이후 부분 unmap을 처리하기 위해 unmap_fixup()이 호출된다.
마지막으로, unmap된 영역과 관련된 모든 페이지 테이블 엔트리를 해제하기 위해 free_pgtables()가 호출된다. 페이지 테이블 엔트리의 해제가 완전하지 않다는 점을 유념해야 한다. 이는 전체 PGD 디렉토리와 그 엔트리들만을 unmap 할 것이기 때문이다. 예를 들어, 하나의 PGD가 매핑에 절반만 사용되었다면, 어떠한 페이지 테이블 엔트리도 해제되지 않는다. 이는 더 세밀한 페이지 테이블 엔트리의 해제가 너무 비용이 많이 들며, 작고 다시 사용될 가능성이 높은 데이터 구조를 해제하는 것이 비효율적이기 때문이다.
4.4.13 Deleting all memory regions
프로세스를 종료할 때에는, mm_sturct와 관련된 모든 VMA들을 unmap 해야한다. 이는 exit_mmap() 함수를 통해 수행한다. 이는 매우 간단한 함수로, 먼저 CPU cache를 flush하고, VMA의 linked list를 순회하여 각각을 unmap 하고, 관련된 페이지들을 해제하며, TLB를 flush하고, 페이지 테이블 엔트리를 삭제한다. 자세한 내용은 Code Commentary에서 다룬다.
4.5 Exception Handling
가상 메모리 관리의 매우 중요한 부분은 커널 주소 공간에서 발생하는 버그가 아닌 예외를 어떻게 처리하는 지이다. 이번 절에서는 0으로 나누기와 같은 에러로 인한 예외는 다루지 않는다. 여기서는 오로지 페이지 폴트로 인한 예외만을 다룰 것이다. 잘못된 참조가 발생하는 두 가지 상황이 있다. 첫 번째는 프로세스가 시스템 호출을 통해 유효하지 않은 포인터를 커널에 전달하는 경우로, 커널은 주소가 PAGE_OFFSET 아래인지만 초기에 확인한 후 안전하게 이를 캐치해야 한다. 두 번째는 커널이 copy_from_user() 또는 copy_to_user()를 사용하여 사용자 공간에서 데이터를 읽거나 쓸 때이다.
컴파일 시간에, 링커는 커널 코드 세그먼트의 __ex_table 섹션에 예외 테이블을 생성한다. 이 테이블은 __start___ex_table에서 시작하여 __stop___ex_table에서 끝난다. 각 항목은 exception_table_entry 타입으로, 실행 지점과 수정 루틴이라는 두 부분으로 구성된다. 페이지 폴트 핸들러가 관리할 수 없는 예외가 발생하면, search_exception_table()을 호출하여 해당 지점에서 오류에 대한 수정 루틴이 제공되었는 지 확인한다. 모듈 지원이 컴파일되어 있다면, 각 모듈의 예외 테이블도 검색된다.
현재 예외의 주소가 테이블에서 찾아지면, 해당 수정 코드의 위치가 반환되어 실행된다. 섹션 4.7에서는 이 방법이 사용자 공간으로의 잘못된 읽기와 쓰기를 어떻게 캐치하는 지 살펴볼 것이다.
4.6 Page Faulting
프로세스 선형 주소 공간에서의 페이지는 꼭 메모리에 할당될 필요는 없다. 예를 들어, 프로세스가 할당한 메모리는 즉시 물리 메모리를 할당하지 않고, vm_area_struct에서의 공간만 예약한다. non-resident page의 다른 예시로는 스왑 아웃된 페이지나 CoW 처리된 read-only 페이지가 있다.
리눅스는 대부분의 OS와 마찬가지로 non-resident page를 처리하기 위한 Demand Fetch 정책을 가지고 있다. 이것은 하드웨어가 페이지 폴트 예외를 발생시킬 때, OS가 그 예외를 포착하고 페이지를 할당할 때만 페이지가 백업 스토리지에서 메모리로 fetch 된다는 것을 의미한다. 이러한 과정은 page prefetching 정책을 통해서 페이지 폴트의 수를 줄일 수 있음을 암시하지만, 리눅스는 이에 대해서는 꽤나 미숙한 편이다. 페이지가 swap space로부터 fetch될 때에는, 해당 페이지 뒤의 2^page_cluster 까지의 추가 페이지를 swapin_readahead()를 통해서 읽어오고, swap cache에 배치한다. 불행히도, 곧 사용될 가능성이 있는 페이지들이 스왑 영역에서 인접해 있을 확률은 낮기 때문에, 이는 좋지 않은 prepaging 정책이다. 리눅스는 프로그램의 동작에 적응하는 prepaging 정책을 통해 이득을 볼 수 있을 것이다.
페이지 폴트에는 major와 minor 타입이 존재한다. Major 페이지 폴트는 비싼 작업인 disk read를 수행해야하는 페이지 폴트를 말하며, 그렇지 않은 경우를 minor 또는 soft 페이지 폴트라고 말한다. 리눅스는 task_struct->maj_flt와 task_struct->min_flt로 해당 타입들의 통계치를 관리한다.
리눅스의 페이지 폴트 핸들러는 Table 4.4에 나열되어 있는 많은 수의 서로 다른 타입의 페이지 폴트를 인식하고 처리할 수 있어야하며, 이 장에서 곧 다룰 것이다.
| Exception | Type | Action |
|---|---|---|
| Region은 valid하지만, 페이지가 할당되지 않음 | Minor | 물리 메모리 할당자를 통해 페이지 프레임을 할당 |
| Region이 valid하지 않지만 stack과 같이 확장 가능한 영역의 뒤에 위치함 | Minor | Region을 확장하고 페이지를 할당 |
| Page가 스왑 아웃되어 있지만 swap cache에 존재함 | Minor | 프로세스 페이지 테이블에 페이지를 다시 매핑하고, swap cache에 대한 참조를 제거함 |
| Page가 스토리지에 스왑 아웃되어있음 | Major | PTE에 저장된 정보를 통해 페이지가 디스크의 어디에 위치해있는 지를 찾고 읽음 |
| Read-only page에 대한 쓰기를 수행함 | Minor | 만약 해당 페이지가 COW 페이지라면 새로운 복사본을 writable 권한으로 만들고 해당 페이지를 프로세스에게 할당함. 만약 그냥 나쁜 쓰기 작업이였다면, SIGSEGV 시그널을 보냄 |
| Region이 invalid하거나 프로세스가 접근할 권한이 없음 | Error | 프로세스에게 SIGSEGV 시그널을 보냄 |
| 커널 주소 공간에서 폴트가 발생함 | Minor | 만약 폴트가 vmalloc 영역에서 발생했다면, 현재 프로세스 페이지 테이블을 init_mm에 의해 유지되는 마스터 페이지 테이블을 기준으로 업데이트함. 이것이 커널 페이지 폴트가 valid한 유일한 경우임. |
| 커널 모드에서 유저 공간 영역에 대해 Fault가 발생함 | Error | 만약 폴트가 발생하면, 이것은 커널이 유저 공간에서 적절한 복사를 수행하지 않았다는 것을 의미함. 이것은 꽤 심각하게 다루어지는 커널 버그이다. |
각 아키텍처는 페이지 폴트를 처리하기위한 아키텍처 의존적인 함수를 등록한다. 이 함수의 이름은 임의로 지어지지만, 보통은 do_page_fault()이며, x86의 call grap를 Figure 4.12에서 볼 수 있다.

이 함수에는 폴트가 발생한 주소, 페이지가 단순히 없어서 폴트가 발생한 것인지 아니면 protection error인지, 읽기 또는 쓰기로 인한 폴트인지, 유저 또는 커널 공간에서의 폴트인지와 같은 풍부한 정보가 제공된다. 이 함수는 이 정보를 토대로 어떤 타입의 폴트가 발생한 것인지를 판단하고, 이 폴트가 아키텍처 독립적인 코드에 의해 어떻게 처리되어야할 지를 결정한다. Figure 4.13의 flow chart는 이 함수가 일반적으로 하는 일을 보여준다. 이 Figure에서 콜론이 붙어있는 식별자는 실제 코드에서의 label과 동일하다.

handle_mm_fault()은 아키텍저 독립적인 가장 윗 단계의 함수이며, 스토리지에 있는 페이지에 대한 폴트 처리, COW 수행 등등의 작업을 처리한다. 이 함수가 1을 반환하면 그것이 minor fault였음을 의미하며, 2라면 major임을 의미하고, 0은 SIGBUS error, 나머지 다른 값들은 out of memory handler를 깨워 전달되게 된다.
4.6.1 Handling a Page Fault
예외 처리자가 어떠한 폴트가 valid memory region에서의 valid page에 대한 폴트임을 알았다면, 해당 폴트는 아키텍처 독립적인 함수인 handle_mm_fault()가 이어서 처리하게된다 (Call graph는 Figure 4.14에서 확인할 수 있다). 이 함수는 만약 필요한 페이지 테이블 엔트리가 없다면 이를 할당하고 handle_pte_fault()를 호출한다.
PTE의 속성에 따라, Figure 4.14에서 볼 수 있는 핸들러 함수 중 하나가 사용된다. 이 함수는 먼저 pte_present()와 pte_none()을 통해 PTE가 present한지, PTE가 할당된 적이 있는 지를 체크한다. 만약 어떠한 PTE도 할당된 적 없었다면 (pte_none()이 true를 반환한다면), do_no_page()가 호출되어 Demand Allocation을 수행한다. 그렇지 않다면 해당 페이지는 disk로 스왑 아웃된 것이므로, do_swap_page()가 호출되어 Demand Paging을 수행한다. 흔치 않은 경우로 virtual file에 속한 페이지가 스왑 아웃된 경우에는 do_no_page()가 이를 처리한다. 이에 대해서는 섹션 12.4에서 다룬다.

두 번째 옵션은 페이지가 쓰여질 것인지다. 만약 PTE가 write protection이 걸려있다면, page는 Copy-On-Write (COW) page이기 때문에 do_wp_page()가 호출된다. COW page는 쓰기가 발생하기 전까지 여러 프로세스들(보통 parent와 child)에 의해 공유되다가, 쓰기를 수행하는 프로세스에 의해 복사본이 생성된다. COW page는 VMA가 writable 하지만 PTE가 writable 하지 않다는 것을 이용하여 구별할 수 있다. 만약 페이지가 COW page가 아닌 경우, 페이지는 단순히 dirty 하다고 표시된다.
마지막 옵션은 read이고 page가 present 한데도 fault가 발생한 경우이다. 이는 3-level page table을 사용하지 않는 일부 아키텍처에서 발생할 수 있다. 이 경우에는 단순히 PTE를 설정하고 young으로 표시한다.
4.6.2 Demand Allocation
프로세스가 어떤 페이지를 처음 접근하는 경우, 해당 페이지는 do_no_page() 함수에 의해 할당되고 데이터로 채워져야한다. 만약 페이지가 속한 VMA와 관련된 vm_operations_struct (vma->vm_ops)가 nopage() 함수를 제공한다면, 해당 함수가 호출된다. 이는 접근 시점에 페이지를 할당하고 데이터를 제공해야하는 메모리 매핑된 장치와 데이터를 스토리지로부터 가지고 와야하는 파일 페이지에 중요하다. 우리는 가장 간단한 케이스인 anonymous page에 대한 내용을 먼저 다룰 것이다.
Handling anonymous pages
만약 vma->vm_ops 필드가 비어있거나 nopage() 함수가 제공되지않는 경우, do_anonymous_page()가 호출되어 anonymous 접근을 처리한다. 처리해야하는 케이스에는 두 가지가 있는데, 처음 읽는 경우와 처음 쓰는 경우이다. Anonymous page의 경우, 처음 읽을 때에는 아직 데이터가 없으므로 처리가 가장 쉽다. 이 경우에는 시스템이 제공하는 0으로 채워진 페이지인 empty_zero_page가 PTE에 매핑되고, write protection을 설정한다. Write protection이 설정되기 때문에, 프로세스가 해당 페이지에 쓰기를 수행하면 추가적인 페이지 폴트가 발생한다. x86의 경우, 전역 zero page가 mem_init()에서 초기화된다.

첫 번째로 페이지를 쓰는 경우에는 alloc_page() 함수를 호출하여 새로운 페이지를 할당하며 (6장 참조), clear_user_highpage()를 통해 0으로 초기화한다. 페이지가 성공적으로 할당되었다면, mm_struct의 Resident Set Size (RSS) 필드를 증가시킨다. 일부 아키텍처의 경우 캐시 일관성을 보장하기 위해 페이지가 유저 공간의 프로세스에 삽입되는 경우 필요에 따라 flush_page_to_ram()을 호출한다. 그런 다음 페이지는 LRU list에 삽입되어 추후에 페이지 회수 코드에 의해 회수될 수 있도록 한다. 마지막으로 새로운 매핑을 프로세스의 페이지 테이블에 추가한다.
Handling file/device backed pages
만약 file이나 device로 매핑되어있다면, nopage() 함수가 vm_operations_struct에 제공될 것이다. file에 매핑된 경우, filemap_nopage()가 자주 사용되며, 페이지를 할당하고 페이지 크기의 데이터를 디스크로부터 읽어오는 기능을 한다. 페이지가 shmfs의 경우처럼 virtual file에 매핑된 경우, shmem_nopage() 함수를 사용할 것이다 (12장 참조). 각 디바이스 드라이버는 서로 다른 nopage() 함수를 제공하며, 그것이 유효한 struct page를 반환해주기만 한다면, 내부 동작에 대해서는 우리에게 지금은 중요치않다.
페이지가 반환되면, 페이지가 성공적으로 할당되었는 지 확인하고, 그렇지 않다면 적절한 에러가 반환되었는 지를 확인한다. 다음으로는 early COW break를 수행해야하는 지를 체크한다. Early COW break는 쓰기로 인한 폴트이고 VM_SHARED flag가 설정되지 않은 경우에 수행된다. Early break를 하는 경우, 새로운 페이지를 할당하고 데이터를 복사한 후, nopage()가 반환한 페이지에 대한 참조 카운트를 감소시킨다.
두 경우 보두, pte_none()을 호출하여 사용할 PTE가 이미 페이지 테이블에 있는 지를 검사한다. 페이지 폴트를 수행하는 전체 기간동안 PTE spinlock을 잡는 것이 아니기 때문에 SMP 머신에서 같은 페이지에 대한 폴트가 거의 동시에 발생하는 것이 가능하므로, 이 확인을 마지막에 꼭 해야한다. 경쟁 상태가 발생하지 않았다면, PTE를 할당하고 통계치를 갱신하며 캐시 일관성을 위한 아키텍처 hook을 호출한다.
4.6.3 Demand Paging
페이지가 백업 스토리지로 스왑 아웃된 상태라면, 섹션 12에서 다루게될 virtual file인 경우를 제외하면, do_swap_page() 함수가 호출되어 페이지를 다시 읽어온다. 페이지를 스토리지에서 찾기 위해 필요한 정보는 PTE 자체에 저장되어 있다. PTE에 있는 정보는 swap 영역에서 페이지를 찾기에 충분하다. 페이지들은 다수의 프로세스들에 의해 공유될 수 있기 때문에, 그들은 항상 즉시 스왑 아웃될 수 있는 것은 아니다. 대신에, 페이지가 스왑아웃 될 때에, 그들은 swap cache에 위치하게 된다.

struct page를 이를 공유하는 각 프로세스의 PTE에 대해 매핑하는 방법이 없기 때문에, 공유되는 페이지를 즉시 스왑 아웃할 수는 없다. 모든 프로세스들의 페이지 테이블을 탐색하는 방법은 너무 오버헤드가 크다. 2.5.x 커널 후반 버전과 2.4.x의 커스텀 패치 버전은 Reverse Mapping (Rmap)이라는 기술을 포함한다는 것을 알고있으면 좋으며, 이는 이 장의 마지막 부분에서 다룰 것이다.
swap cache가 존재하기 때문에, fault가 발생했을 때 해당 페이지가 swap cache에 여전히 존재할 가능성이 있다. 만약 그렇다면, 단순히 page에 대한 참조 카운트를 증가시키고 페이지 테이블에 다시 매핑이 등록되며 migor page fault로서 통계에 등록된다.
만약 페이지가 디스크에만 존재한다면, swapin_readahead()가 호출되어 요청된 페이지과 그 뒤에 인접한 많은 페이지들을 디스크로부터 읽어온다. 읽어오는 페이지들의 수는 mm/swap.c에 정의된 page_cluster 변수에 의해 결정된다. 16MiB보다 작은 RAM을 가진 low memory 머신에서는, 이 변수가 2 또는 3으로 초기화된다. 읽어오는 페이지 수는 비어있거나 나쁜 swap entry가 포함되지 않는 이상 2^page_cluster 개로 결정된다. 이는 seek 작업이 시간적으로 가장 비싸다는 전제하에 동작하며, seek가 끝나게 되면, 이때 탐색된 페이지들은 같이 메모리에 로드되어야 한다.
4.6.4 Copy On Write (COW) Pages
예전에는 fork 할 때, 부모 주소 공간 전체를 child를 위해 복제했었다. 프로세스의 대부분을 백업 스토리지로부터 swap in 해야하는 상황이 발생할 수 있기 때문에, 이는 극도로 비싼 작업이었다. 상당한 오버헤드를 피하기 위해, Copy-On-Write라는 기술이 도입되었다.

Fork를 하는 동안, 두 프로세스의 PTE들을 read-only로 설정하여 write가 발생하면 page fault가 발생하도록 한다. Linux는 COW page를 인지할 수 있는데, 이는 PTE가 write protect되더라도, 해당하는 VMA는 여전히 writable로 표시되기 때문이다. COW은 do_wp_page()를 통해 수행되며, 이는 페이지의 복사본을 만들고 쓰기를 수행한 프로세스에 매핑한다. 필요한 경우, 새로운 swap slot이 페이지에게 예약될 것이다. 이 방법을 통해, fork 과정에서는 오로지 페이지 테이블 엔트리만이 복사된다.
4.7 Copying To/From Userspace
어떤 주소의 페이지가 메모리에 로드되어 있는 지를 빠르게 알 수 있는 방법이 없기 때문에, 프로세스 주소 공간에 직접 접근하는 것은 안전하지 않다. Linux는 주소가 invalid할 때 예외를 일으키는 데 MMU에 의존하며, 페이지 폴트 예외 핸들러가 해당 예외를 캐치하고 이를 처리한다. x86의 경우, __copy_user()가 제공하는 어셈블러를 통해 주소가 완전히 쓸모없는 경우에 해당하는 예외를 처리한다. 예외 처리 코드의 위치는 search_exception_table() 함수를 통해 찾게 된다. Linux는 유저 주소 공간에 또는 유저 공간으로부터 데이터를 안전하게 복사하기위한 API를 많이 제공하며, Table 4.5에서 이를 확인할 수 있다.
Table 4.5: Accessing Process Address Space API
- unsigned long copy_from_user(void *to, const void *from, unsigned long n)
- 유저 공간으로부터 커널 주소 공간에 n 바이트를 복사한다.
- unsigned long copy_to_user(void *to, const void *from, unsigned long n)
- 커널 주소로부터 유저 주소에 n 바이트를 복사한다.
- void copy_user_page(void *to, void *from, unsigned long address)
- 데이터를 유저 공간의 anonymous page 또는 CoW page에 복사한다.
- D-cache alias 문제가 발생하지 않도록 해야하며, 이는 커널 가상 주소를 사용함을 통해 같은 가상 주소의 캐시라인을 사용함으로써 해결할 수 있다.
- void clear_user_page(void *page, unsigned long address)
- page를 zeroing한다는 것 빼고는, copy_user_page()와 비슷하다.
- void get_user(void *to, void *from)
- 유저 공간으로부터 커널 공간에 integer 값을 복사한다.
- void put_user(void *from, void *to)
- 커널 공간으로부터 유저 공간에 integer 값을 복사한다.
- long strncpy_from_user(char *dst, const char *src, long count)
- 유저 공간에서 커널 공간으로 count 바이트만큼의 null로 끝나는 string 데이터를 복사한다.
- long strlen_user(const char *s, long n)
- 유저 공간의 null로 끝나는 string 데이터의 길이를 n만큼 상한으로 반환한다.
- int access_ok(int type, unsigned long addr, unsigned long size)
- 유저 공간의 메모리가 valid하면 0이 아닌 값을 반환하며, 그렇지 않으면 0을 반환한다.
모든 매크로들은 비슷한 방식으로 구현된 어셈블러 함수에 매핑되며, 설명의 편의를 위해, 여기서는 x86에서 copy_from_user()가 어떻게 구현되어 있는 지에 대해서만 논의한다.
복사할 크기를 컴파일 타임에 알 수 있다면, copy_from_user()는 __constant_copy_from_user()를 호출하며, 그렇지 않다면 __generic_copy_from_user()를 호출한다. 크기를 아는 경우에는, 데이터를 1, 2, 또는 4바이트 단위로 복사하는 어셈블러 최적화 기술이 적용된 서로 다른 함수들이 존재하며, 이러한 최적화 외에는 서로 간의 다른 점은 없다.
Generic copy 함수는 __copy_user_zeroing() 함수를 호출하게되며, 이 함수는 세가지 중요한 부분으로 구성된다. 첫 번째는 유저 공간에서 size 크기의 바이트를 실제로 복사하는 어셈블러이다. 어떤 페이지라도 메모리에 존재하지 않다면, 페이지 폴트가 발생할 것이고, 만약 주소가 유효하다면 일반적인 상황과 같이 swap in 될 것이다. 두 번째는 “fixup” 코드이고 세 번째는 첫 번째 파트에서의 명령어들을 두 번째 파트의 fixup 코드로 매핑하는 __ex_table이다.
섹션 4.5에 나와있듯이, 링커가 커널 예외 핸들러 테이블에 copy 명령어의 위치와 fixup 코드의 위치를 복사한다. 잘못된 주소가 읽히면, do_page_fault() 함수는 실행을 계속하여 search_exception_table()을 호출하고, 잘못된 읽기가 발생한 EIP를 찾아 fixup 코드로 점프한다. 이 fixup 코드는 커널 공간에 남아 있는 부분에 0을 복사하고, 레지스터를 수정한 후 반환한다. 이러한 방식으로, 커널은 비용이 많이 드는 검사 없이 유저 공간에 안전하게 접근할 수 있으며, MMU 하드웨어가 예외를 처리하도록 한다.
유저 공간에 접근하는 다른 모든 함수들도 유사한 패턴을 따른다.
What’s New in 2.6
Linear Address Space
선형 주소 공간은 2.4와 거의 동일하게 유지되며 쉽게 인식할 수 없는 수정 사항은 없다. 주요 변경 사항은 유저 공간에서 사용할 수 있는 새 페이지가 고정 주소 가상 매핑에 추가된 것이다. x86에서 이 페이지는 0xFFFFF000에 위치하며 vsyscall 페이지라고 한다. 이 페이지에는 유저 공간에서 커널 공간으로 들어가는 최적의 방법을 제공하는 코드가 있다. 유저 공간 프로그램은 이제 커널 공간에 들어갈 때 전통적인 int 0x80 대신 call 0xFFFFF000을 사용해야 한다.
struct mm_struct 이 구조체는 크게 변경되지 않았다. 첫 번째 변경 사항은 free_area_cache 필드의 추가로, 이는 TASK_UNMAPPED_BASE로 초기화된다. 이 필드는 선형 주소 공간에서 첫 번째 hole이 어디인지 기억하여 검색 시간을 개선하는 데 사용된다. 구조체의 끝에는 core dump와 관련된 일부 필드가 추가되었으며, 이 책의 범위를 벗어난다.
struct vm_area_struct 이 구조체 역시 크게 변경되지 않았다. 주요 차이점은 vm_next_share와 vm_pprev_share가 shared라는 새 필드로 대체된 적절한 연결 리스트로 변경되었다는 것이다. vm_raend는 file readahead가 2.6에서 매우 다르게 구현되어 전체적으로 제거되었다. Readahead는 주로 struct file->f_ra에 저장된 struct file_ra_state 구조체에서 관리된다. Readahead의 구현 방식은 mm/readahead.c에 자세하게 설명되어있다.
struct address_space 첫 번째 변경 사항은 상대적으로 작다. gfp_mask 필드가 flags 필드로 대체되었으며, 첫 __GFP_BITS_SHIFT 비트는 gfp_mask로 사용되며 mapping_gfp_mask()를 통해 접근된다. 나머지 비트는 비동기 IO의 상태를 저장하는 데 사용된다. 설정될 수 있는 두 가지 flag는 비동기 쓰기 중에 파일 시스템이 공간이 부족함을 나타내는 AS_ENOSPC와 IO 오류를 나타내는 AS_EIO이다.
이 구조체는 주로 page cache와 file readahead와 관련된 중요한 추가 사항이 있다. 필드가 상당히 특이하기 때문에 자세히 소개한다:
- page_tree: 이것은 물리 디스크에 있는 블록으로 인덱싱된 이 매핑의 page cache에 있는 모든 페이지의 radix tree이다. 2.4에서는 page cache를 탐색하는 것이 연결 리스트를 순회하는 것이었지만, 2.6에서는 radix tree 순회로 바뀌어 탐색 시간이 크게 줄었다. radix tree는 lib/radis-tree.c에서 구현된다.
- page_lock: page_tree를 보호하는 스핀락이다.
- io_pages: dirty 페이지가 쓰여질 때, do_writepages()가 호출되기 전에 이 리스트에 추가된다. fs/mpage.c의 mpage_write_pages() 위의 주석에서 설명한 것처럼, 쓰여질 페이지는 IO에 의해 이미 잠긴 것을 잠그는 데드락을 피하기 위해 이 리스트에 배치된다.
- dirtied_when: 이 필드는 inode가 처음 더러워진 시간을 jiffies로 기록한다. 이 필드는 inode가 super_block->s_dirty 리스트에서 어디에 위치하는지 결정한다. 이것은 자주 dirty해진 inode가 리스트의 상단에 남아 다른 inode의 쓰기를 방해하는 것을 방지한다.
- backing_dev_info: 이 필드는 readahead와 관련된 정보를 기록한다. 구조체는 include/linux/backing-dev.h에 선언되어 있으며 필드를 설명하는 주석이 있다.
- private_list: 이것은 address_space에 사용할 수 있는 private 리스트이다. mark_buffer_dirty_inode()와 sync_mapping_buffers() 도우미 함수가 사용되면, 이 리스트는 buffer_head->b_assoc_buffers 필드를 통해 buffer_heads를 연결한다.
- private_lock: 이 스핀락은 address_space에서 사용할 수 있다. 이 락의 사용은 매우 복잡하지만 일부 사용 사례는 긴 ChangeLog 2.5.17에서 설명된다. 하지만 주로 이 매핑에서 버퍼를 공유하는 다른 매핑의 리스트를 보호하는 데 관련이 있다. 이 락은 이 private_list를 보호하지 않지만, 이 매핑과 버퍼를 공유하는 다른 address_space의 private_list를 보호할 것이다.
- assoc_mapping: 이것은 이 매핑의 private_list에 포함된 버퍼를 백업하는 address_space이다.
- truncate_count: invalidate_mmap_range() 함수에 의해 region이 잘려나갈 때 증가한다. 페이지 폴트 시 do_no_page()에 의해 검사되어 금방 무효화된 페이지가 잘못 폴트되지 않도록 한다.
struct address_space_operations 이 구조체의 변경 사항은 간단해 보이지만 실제로는 꽤 복잡하다. 변경된 필드는 다음과 같다:
- writepage: writepage() 콜백이 추가 매개변수 struct writeback_control을 사용하도록 변경되었다. 이 구조체는 쓰기 작업에 대한 정보를 기록하는 책임이 있으며, 작성자가 페이지 할당자인지 direct reclaim 또는 kupdated인지, 쓰기 작업이 혼잡한지 여부와 readahead를 제어하기 위해 백업 backing_dev_info에 대한 핸들을 포함한다.
- writepages: dirty_pages에서 모든 페이지를 io_pages로 이동한 후 모든 페이지를 쓰기 위해 호출된다.
- set_page_dirty: 페이지를 dirty로 설정하는 address_space 특정 메소드이다. 주로 백업 스토리지의 address_space_operations와 익명 공유 페이지에서 페이지에 연결된 버퍼가 없는 경우 사용된다.
- readpages: 페이지를 읽어 들일 때 readahead를 정확하게 제어할 수 있도록 사용된다.
- bmap: 이것은 2^32 바이트보다 큰 장치에 대해 처리하기 위해 디스크 섹터를 다루도록 변경되었다.
- invalidatepage: block_flushpage()와 콜백 flushpage()가 block_invalidatepage()와 invalidatepage()로 이름이 변경되었다.
- direct_IO: 2.6에서 새 IO 메커니즘을 사용하도록 변경되었다. 새 메커니즘은 이 책의 범위를 벗어난다.
Memory Regions
mmap()의 작동에는 두 가지 중요한 변경 사항이 있다. 첫 번째는 보안 모듈이 콜백을 등록할 수 있다는 것이다. 이 콜백은 security_file_mmap()이며 관련 security_ops 구조체에서 관련 함수를 조회한다. 기본적으로 이것은 NULL 연산이 될 것이다.
두 번째는 훨씬 더 엄격한 주소 공간 accounting 코드가 생겼다는 것이다. Accounting 될 것으로 예상되는 vm_area_structs는 VM_ACCOUNT flag가 설정될 것이며, 이는 모든 유저 공간 매핑에 해당할 것이다. 유저 공간 영역이 생성되거나 파괴될 때, 함수 vm_acct_memory()와 vm_unacct_memory()는 변수 vm_committed_space를 업데이트한다. 이는 커널이 유저 공간에 할당된 메모리 양에 대해 훨씬 더 좋은 관점을 제공한다.
4GiB/4GiB User/Kernel Split
2.4.x 커널에 대한 한 가지 제한은 커널이 모든 프로세스에 보이는 가상 주소 공간 1GiB만을 가지고 있다는 것이다. 글을 쓰는 시점에서 Ingo Molnar2에 의해 개발된 패치가 있으며, 이 패치는 선택적으로 커널에 자체적인 4GiB 주소 공간을 제공할 수 있다. 패치는 http://redhat.com/mingo/4g-patches/ 에서 사용할 수 있으며 -mm 테스트 트리에 포함되어 있지만, mainline에 병합될 지 여부는 불분명하다.
이 기능은 16GiB이상의 RAM이 있는 32비슽 시스템을 위해 의도되었다. 전통적인 1/3 분할은 최대 1GiB의 RAM을 지원한다. 그 후, high-memory 지원은 일시적으로 high-memory 페이지를 매핑하여 더 큰 양을 지원할 수 있지만, RAM이 많아질수록 이 것은 상당한 병목 현상을 야기한다. 예를 들어, 물리적 RAM의 양이 60GiB 범위에 접근하면, 저메모리의 거의 전체가 mem_map에 의해 소비된다. 커널에 의해 자체 4Gib 가상 주소 공간을 제공함으로써, 메모리를 지원하기가 훨씬 쉽지만, 심각한 패널티는 시스템 호출당 TLB 플러시로 인해 성능에 크게 영향을 미친다는 것이다.
이 패치로 인해 유저 공간과 커널 공간 사이에 공유되는 작은 16MiB 메모리 영역만 있으며, 여기에는 GDT, IDT, TSS, LDT, vsyscall 페이지 및 커널 스택이 저장된다. 커널 공간에 들어가거나 나올 때 페이지 테이블 사이의 실제 전환을 수행하는 트램폴린 코드가 포함되어 있다. 핵심 커널은 이 패치에 의해 크게 영향을 받지 않지만 유저 공간 버퍼에 대한 직접 포인터 접근이 제거된 것과 같은 몇 가지 변경 사항이 있다.
Non-Linear VMA Population
2.4에서는 파일에 백업된 VMA가 선형 방식으로 채워진다. 이 것은 2.6에서 mmap()에 MAP_POPULATE 플래그 도입과 새 시스템 콜인 remap_file_pages()의 도입으로 선택적으로 변경될 수 있다. 이 시스템 콜은 기존 VMA의 임의 페이지를 백업 파일의 임의 위치에 매핑하여 페이지 테이블을 조작함으로써 허용한다.
page out 시, 파일의 비선형 주소는 PTE 내에 인코딩되어 페이지 폴트 시 올바르게 다시 설치될 수 있다. 이것이 어떻게 인코딩되는지는 아키텍처에 따라 다르므로 pgoff_to_pte()와 pte_to_pgoff()라는 두 개의 매크로가 작업을 위해 정의된다.
이 기능은 주로 대규모 매핑을 가진 애플리케이션, 예를 들어 데이터베이스 서버와 에뮬레이터 같은 가상화 애플리케이션에 이점이 있다. 이것은 몇 가지 이유로 도입되었다. 첫째, VMAs는 프로세스당이며, 매핑이 많은 애플리케이션의 경우 상당한 공간 요구 사항이 있다. 둘째, get_unmapped_area()의 free 영역을 찾는 검색은 매핑 수가 많을 경우 매우 비싸다. 셋째, 비선형 매핑은 대부분 페이지를 메모리에 미리 폴트시킬 것이지만, 일반 매핑은 각 페이지에 대해 major 폴트를 일으킬 수 있으나 mmap() 또는 mlock()과 함께 새 플래그 MAP_POPULATE를 사용하여 이를 피할 수 있다. 마지막 이유는 sparse mapping을 피하기 위해서이다. 최악의 경우, 매핑된 파일 페이지마다 하나의 VMA가 필요하다.
그러나 이 기능은 몇 가지 심각한 단점이 있다. 첫 번째는 truncate() 및 mincore() 시스템 콜이 비선형 매핑과 관련하여 손상되었다는 것이다. 두 시스템 콜은 모두 vm_area_struct->vm_pgoff에 의존하는데, 이것은 비선형 매핑에 대해 의미가 없다. 비선형 매핑으로 매핑된 파일이 잘려나가면 VMA내에 존재하는 페이지는 여전히 남아 있을 것이다. 적절한 해결책은 페이지를 메모리에 남겨두되 익명으로 만드는 것이 제안되었지만, 글을 쓰는 시점에서 아직 해결책이 구현되지 않았다.
두 번째 주요 단점은 TLB 무효화이다. 각 재매핑된 페이지는 MMU에 재매핑이 발생했다는 것을 flush_icache_page()를 사용하여 알려야 하지만, 더 중요한 패널티는 flush_tlb_page() 호출과 관련이 있다. 일부 프로세서는 페이지와 관련된 TLB 엔트리만 무효화할 수 있지만, 다른 프로세서는 이를 전체 TLB를 flushing함으로써 구현한다. 재매핑이 자주 발생하면, TLB 미스 증가와 지속적으로 커널 공간에 들어가는 오버헤드로 인해 성능이 저하된다. 어떤 면에서, 이러한 패널티는 프로세서에 따라 영향이 크기 때문에 가장 나쁘다.
이 기능의 미래가 무엇이 될지는 현재 불분명하다. 글을 쓰는 시점에서 이 기능과 관련된 문제를 어떻게 해결할지에 대한 논쟁이 계속되고 있지만, 비선형 매핑은 페이지 아웃, 잘림 및 페이지의 reverse mapping과 관련하여 일반 매핑과 매우 다르게 처리될 가능성이 있다. 이 기능의 주요 사용자가 데이터베이스일 가능성이 높기 때문에, 이 특별한처리는 문제가 되지 않을 것이다.
Page Faulting
페이지 폴트 루틴의 변경 사항은 reverse mapping과 high memory에서 PTE를 지원하는 필요한 변경을 제외하고는 대부분 겉보기에만 있다. 주요 겉보기 변경은 페이지 폴트 루틴이 magic number 대신 자체 설명적인 컴파일 타임에 정의되는 상수를 반환한다는 것이다. handle_mm_fault()의 가능한 반환 값은 VM_FAULT_MINOR, VM_FAULT_MAJOR, VM_FAULT_SIGBUS, VM_FAULT_OOM이다.