운영체제 기본/Linux/메모리 매핑/페이지 폴트 처리: 두 판 사이의 차이

잔글 관리자님이 운영체제 기본/Linux의 파일 메모리 매핑/첫 접근과 페이지 폴트 문서를 운영체제 기본/Linux의 메모리 매핑/페이지 폴트 처리 문서로 이동했습니다: 공통 mmap 구조와 매핑 유형별 설명으로 재구성
공통 mmap·페이지 폴트와 대상별 처리 경로 분리
1번째 줄: 1번째 줄:
{{이전 다음 문서
{{이전 다음 문서
|이전=← 이전 문서<br />[[운영체제 기본/Linux의 파일 메모리 매핑/mmap과 주소 공간|mmap과 주소 공간]]
|이전=← 이전 문서<br>[[운영체제 기본/Linux의 메모리 매핑/mmap과 주소 공간|mmap과 주소 공간]]
|목차=[[운영체제 기본/Linux의 파일 메모리 매핑|파일 메모리 매핑 목차]]
|목차=[[운영체제 기본/Linux의 메모리 매핑|메모리 매핑 목차]]
|다음=다음 문서 →<br>[[운영체제 기본/Linux의 메모리 매핑/익명 메모리 매핑|익명 메모리 매핑]]
}}
}}


== 파일 매핑의 첫 메모리 접근 ==
<code>mmap()</code>으로 만든 VMA는 주소 구간의 의미와 권한을 정한다. 그 주소의 PTE가 아직 없거나 접근에 필요한 권한이 부족하면 CPU가 <strong>페이지 폴트(Page Fault)</strong>를 발생시킨다. 정상적인 지연 할당과 쓰기 시 복사(Copy-on-Write, COW)도 이 예외를 이용하므로, 페이지 폴트가 곧 프로그램 오류라는 뜻은 아니다.
앞 글에서 <code>mmap()</code>은 파일의 일부를 프로세스 주소 공간에 연결하는 VMA를 만들었다. 여기서는 Linux v6.12의 x86-64와 RISC-V RV64를 대표 사례로 그 주소를 읽는다. x86-64는 IDT 예외 진입, RISC-V는 S-mode 커널의 트랩 진입을 기준으로 한다. 파일은 두 경우 모두 일반 ext4 파일이며, 페이지 캐시를 이용하는 buffered 경로다. 매핑은 <code>PROT_READ</code>와 <code>MAP_PRIVATE</code>로 만들었으며, 읽으려는 주소의 PTE가 아직 없는 경우를 따라간다.


<pre>
Linux v6.12의 기본 페이지 경로를 기준으로, CPU 예외 진입과 공통 메모리 관리 코드의 연결을 살펴본다. 실제 페이지를 마련하는 방법은 익명 메모리와 파일 매핑 장에서 이어진다.
/* p는 성공한 mmap()의 반환값이며 i는 매핑과 파일 범위 안의 오프셋이다. */
const unsigned char *bytes = p;
unsigned char value = bytes[i];
</pre>
 
load 명령은 <code>read()</code>를 호출하지 않는다. CPU는 가상 주소 <code>bytes + i</code>를 읽는다. VMA는 커널이 관리하는 영역 설명이고, PTE는 CPU의 주소 변환에 쓰이는 페이지 테이블 항목이다. 따라서 '''VMA가 존재한다는 사실과 그 주소를 지금 변환할 수 있다는 사실은 다르다'''. 파일 내용이 이미 메모리에 있더라도 이 프로세스의 PTE는 없을 수 있다.
 
앞선 다른 주소의 폴트가 주변 PTE까지 설치했다면 첫 load도 바로 성공한다. 반대로 당장 사용할 PTE가 없으면, 정상적인 매핑 안의 읽기라도 [[운영체제 기본/가상 메모리#페이지 폴트의 처리|페이지 폴트]]가 발생한다. 이는 프로그램 오류와 다르다.


== CPU 예외와 VMA 검사 ==
== CPU 예외와 VMA 검사 ==
두 CPU 모두 필요한 주소 변환이 없으면 동기 예외를 일으킨다. 커널은 폴트 주소와 접근 종류를 알아내고, VMA가 그 접근을 허용하는지 검사한 뒤 공통 함수 <code>handle_mm_fault()</code>에 복구를 요청한다. '''예외를 받는 장치와 정보의 표현은 아키텍처별이고, 파일 페이지를 준비하는 핵심 경로는 공통이다.'''
두 CPU 모두 필요한 주소 변환이 없으면 동기 예외를 일으킨다. 커널은 폴트 주소와 접근 종류를 알아내고, VMA가 그 접근을 허용하는지 검사한 뒤 공통 함수 <code>handle_mm_fault()</code>에 복구를 요청한다. '''예외 진입 경로와 정보의 표현은 아키텍처별이고, VMA와 페이지 테이블을 다루는 핵심 경로는 공통이다.'''


<pre>
<pre>
33번째 줄: 25번째 줄:
   |
   |
   v
   v
handle_mm_fault -> file page -> install PTE -> retry load
handle_mm_fault -> 대상별 처리 -> PTE 설정 -> 명령 재시도
</pre>
</pre>


40번째 줄: 32번째 줄:
=== x86-64: #PF, CR2, 오류 코드 ===
=== x86-64: #PF, CR2, 오류 코드 ===


사용 가능한 변환이 없으면 x86-64 CPU는 페이지 폴트 예외 <code>#PF</code>를 발생시킨다. 예외 벡터는 14이고 폴트 주소는 <code>CR2</code>에 남는다. CPU가 전달한 오류 코드는 페이지가 존재하지 않는 경우와 보호 위반, 읽기와 쓰기, 사용자 모드 접근 등의 정보를 구별한다. 이 장의 경우는 사용자 모드에서 아직 연결되지 않은 페이지를 읽는 경우다.
사용 가능한 변환이 없으면 x86-64 CPU는 페이지 폴트 예외 <code>#PF</code>를 발생시킨다. 예외 벡터는 14이고 폴트 주소는 <code>CR2</code>에 남는다. CPU가 전달한 오류 코드는 페이지가 존재하지 않는 경우와 보호 위반, 읽기와 쓰기, 사용자 모드 접근 등의 정보를 구별한다. 아직 연결되지 않은 주소의 읽기와, 읽기 전용 PTE에 대한 쓰기는 오류 코드에서 구별된다.


<pre>
<pre>
56번째 줄: 48번째 줄:
=== RISC-V RV64: scause, stval, sepc ===
=== RISC-V RV64: scause, stval, sepc ===


RISC-V에서는 <code>stvec</code>가 지정하는 트랩 진입점으로 제어가 넘어간다. S-mode 커널의 트랩 진입에서 <code>scause</code>는 원인을, <code>stval</code>은 페이지 폴트 주소를, <code>sepc</code>는 중단된 명령의 주소를 제공한다. 페이지 폴트 원인 번호는 명령어 가져오기 12, load 13, store/AMO 15로 나뉜다. 따라서 이 예제의 읽기는 원인 13인 load page fault다. x86의 페이지 폴트 오류 코드 비트를 그대로 적용하는 설명은 맞지 않는다.<ref name="rv-csr">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/include/asm/csr.h?h=v6.12 Linux v6.12 arch/riscv/include/asm/csr.h], EXC_*_PAGE_FAULT, CSR_SCAUSE, CSR_STVAL, CSR_SEPC와 S-mode 별칭.</ref>
RISC-V에서는 <code>stvec</code>가 지정하는 트랩 진입점으로 제어가 넘어간다. S-mode 커널의 트랩 진입에서 <code>scause</code>는 원인을, <code>stval</code>은 페이지 폴트 주소를, <code>sepc</code>는 중단된 명령의 주소를 제공한다. 페이지 폴트 원인 번호는 명령어 가져오기 12, load 13, store/AMO 15로 나뉜다. 따라서 읽기 접근은 원인 13인 load page fault다. x86의 페이지 폴트 오류 코드 비트를 그대로 적용하는 설명은 맞지 않는다.<ref name="rv-csr">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/include/asm/csr.h?h=v6.12 Linux v6.12 arch/riscv/include/asm/csr.h], EXC_*_PAGE_FAULT, CSR_SCAUSE, CSR_STVAL, CSR_SEPC와 S-mode 별칭.</ref>


<pre>
<pre>
71번째 줄: 63번째 줄:


=== VMA 검사와 공통 폴트 처리 ===
=== VMA 검사와 공통 폴트 처리 ===
두 아키텍처의 처리 함수 모두 폴트 주소를 포함하는 VMA를 찾아 권한을 검사한다. 매핑이 없거나 허용되지 않은 접근이면 정상적인 파일 읽기 복구 대신 신호 처리로 이어질 수 있다. 유효한 읽기라면 <code>handle_mm_fault()</code>로 진행한다.<ref name="x86fault" /><ref name="rv-fault" />
두 아키텍처의 처리 함수 모두 폴트 주소를 포함하는 VMA를 찾아 권한을 검사한다. 매핑이 없거나 허용되지 않은 접근이면 정상적인 폴트 복구 대신 신호 처리로 이어질 수 있다. VMA가 허용한 접근이라면 <code>handle_mm_fault()</code>로 진행한다.<ref name="x86fault" /><ref name="rv-fault" />


== 파일 읽기 폴트 처리 경로 ==
== 공통 폴트 처리와 분기 ==
<code>handle_mm_fault()</code>는 공통 검사와 회계 처리를 감싸며, 여기서 추적하는 일반 경로는 <code>__handle_mm_fault()</code>로 들어간다. 커널은 페이지 테이블 계층을 따라간다. PTE가 비어 있다면 다음 분기가 중요하다.
CPU는 VMA를 읽지 않는다. CPU가 보는 것은 페이지 테이블이고, 커널은 예외를 받은 뒤 VMA를 확인한다. 따라서 VMA가 쓰기를 허용해도 PTE는 일시적으로 읽기 전용일 수 있다. 쓰기 시점에 COW를 수행하려고 의도적으로 남겨 둔 차이다.


<pre>
<pre>
handle_mm_fault
handle_mm_fault → __handle_mm_fault → handle_pte_fault
   → __handle_mm_fault
   |
   → handle_pte_fault
   +-- PTE 없음 → do_pte_missing
   → do_pte_missing
   |      +-- private anonymous → do_anonymous_page
   → do_fault
   |      +-- 파일 등의 처리기 → do_fault
   → do_read_fault
   |
  +-- 읽기 전용 PTE에 쓰기 → do_wp_page
</pre>
</pre>


<code>do_pte_missing()</code>는 익명 메모리와 파일 매핑을 구분한다. 이 예제는 파일 매핑이므로 <code>do_fault()</code>로 가며, 쓰기 fault가 아니므로 <code>do_read_fault()</code>를 선택한다. <code>MAP_PRIVATE</code>라고 해서 읽는 순간 파일 전체를 사본으로 만드는 것은 아니다. 지금은 쓰기 시 복사가 아닌 읽기 경로다.<ref name="memory">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/memory.c?h=v6.12 Linux v6.12 mm/memory.c], handle_mm_fault, __handle_mm_fault, handle_pte_fault, do_pte_missing, do_fault, do_read_fault, finish_fault, set_pte_range.</ref>
<code>do_pte_missing()</code>은 private anonymous VMA와 파일 등의 처리기를 사용하는 VMA를 나눈다. <code>do_fault()</code>는 읽기, private 쓰기, shared 쓰기를 각각 <code>do_read_fault()</code>, <code>do_cow_fault()</code>, <code>do_shared_fault()</code>로 나눈다. 각 분기는 VMA가 허용한 접근에 필요한 페이지와 권한을 준비한다.<ref name="memory">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/memory.c?h=v6.12 Linux v6.12, mm/memory.c], do_pte_missing, do_anonymous_page, do_fault, do_wp_page, wp_page_copy.</ref>
 


앞으로 [[운영체제 기본/Linux의 메모리 매핑/익명 메모리 매핑|익명 매핑]]에서는 0으로 보이는 메모리를 마련하고, [[운영체제 기본/Linux의 메모리 매핑/파일 매핑과 COW|파일 매핑]]에서는 파일 페이지를 가져오거나 사본을 만든다. [[운영체제 기본/Linux의 메모리 매핑/MMIO 장치 매핑|MMIO 매핑]]은 드라이버가 매핑 시점에 장치 PTE를 설치할 수 있으므로, 모든 매핑이 이 지연 할당 순서를 거치는 것은 아니다.


== 파일 데이터 준비와 PTE 설치 ==
필요한 입출력이나 메모리를 기다리는 동안 현재 태스크가 잠들 수 있다. 그동안 CPU는 다른 태스크를 실행할 수 있다. 커널이 필요한 페이지를 준비하고 페이지 테이블 잠금 아래 PTE를 설치하면, 예외에서 돌아온 CPU가 중단한 명령을 다시 실행한다. 파일 데이터가 이미 준비되어 있거나 주변 페이지를 함께 연결하는 최적화가 적용되면 더 짧은 경로로 끝날 수도 있다.


캐시에 바로 매핑할 페이지가 없는 경우에는 <code>__do_fault()</code>가 <code>vma-&gt;vm_ops-&gt;fault(vmf)</code>를 호출한다. 앞 글에서 ext4가 등록한 이 함수 포인터의 대상이 <code>filemap_fault()</code>다. 가상 메모리와 파일 데이터 준비의 연결점이다.<ref name="memory" /><ref name="ext4" />
== PTE 설정과 접근 재시도 ==
 
공통 코드가 사용하는 PTE라는 이름이 하드웨어 비트 형식까지 같다는 뜻은 아니다. x86-64는 present 비트와 읽기/쓰기, 사용자 접근 등의 속성을 사용하고, RISC-V는 V(valid), R, W, X, U 등의 비트로 유효성과 접근 조건을 표현한다. 페이지 테이블 단계와 실제 엔트리 조작은 아키텍처별 구현에 맡긴다. 이 장에서 중요한 공통 결과는 '''해당 프로세스의 가상 주소가 준비된 페이지를 허용된 권한으로 가리키게 되는 것'''이다.<ref name="x86-pte">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/include/asm/pgtable_types.h?h=v6.12 Linux v6.12 arch/x86/include/asm/pgtable_types.h], 페이지 테이블 속성 비트.</ref><ref name="rv-pte">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/include/asm/pgtable-bits.h?h=v6.12 Linux v6.12 arch/riscv/include/asm/pgtable-bits.h], _PAGE_PRESENT, _PAGE_READ, _PAGE_WRITE, _PAGE_EXEC, _PAGE_USER.</ref>
파일 데이터가 이미 준비되어 있을 수도 있고, 저장장치에서 읽어 올 필요가 있을 수도 있다. 필요한 작업을 기다리는 동안 현재 태스크가 잠들 수 있다는 점은 [[운영체제 기본/프로세스와 스레드#입출력 대기와 깨우기|입출력 대기]]에 연결된다. 잠금을 놓았다가 다시 얻어 fault 처리를 재시도할 수도 있다. 파일 데이터는 [[운영체제 기본/Linux의 파일시스템/페이지 캐시#read의 복사와 mmap의 매핑|페이지 캐시]]를 통해 공급된다.<ref name="filemap">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/filemap.c?h=v6.12 Linux v6.12 mm/filemap.c], filemap_fault와 filemap_map_pages.</ref>
 
정상적으로 파일 페이지를 얻은 경로에서는 <code>finish_fault()</code>가 페이지 테이블 잠금 아래 상태를 다시 확인하고 <code>set_pte_range()</code>를 통해 PTE를 설치한다. PTE는 해당 페이지와 보호 속성을 연결한다. 예외 처리가 성공적으로 끝나 사용자 실행으로 돌아오면 중단됐던 load 명령이 다시 실행된다. 이번에는 주소 변환이 가능하므로 그 바이트를 읽는다.<ref name="memory" />
 
이미 캐시에 페이지가 준비되어 있다면 더 짧은 경로를 사용할 수 있다. <code>do_read_fault()</code>는 조건이 맞으면 먼저 <code>do_fault_around()</code>를 시도한다. ext4의 <code>map_pages</code> 콜백은 <code>filemap_map_pages()</code>이며, 이미 준비된 파일 페이지를 주변 주소까지 매핑할 수 있다. 여기서 해결되면 <code>filemap_fault()</code>는 호출하지 않는다. <ref name="memory" /><ref name="ext4">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/ext4/file.c?h=v6.12 Linux v6.12 fs/ext4/file.c], ext4_file_vm_ops와 ext4_file_mmap.</ref>
 
=== 아키텍처별 PTE와 접근 재시도 ===
공통 코드가 사용하는 PTE라는 이름이 하드웨어 비트 형식까지 같다는 뜻은 아니다. x86-64는 present 비트와 읽기/쓰기, 사용자 접근 등의 속성을 사용하고, RISC-V는 V(valid), R, W, X, U 등의 비트로 유효성과 접근 조건을 표현한다. 페이지 테이블 단계와 실제 엔트리 조작은 아키텍처별 구현에 맡긴다. 이 장에서 중요한 공통 결과는 '''해당 프로세스의 가상 주소가 준비된 파일 페이지를 허용된 권한으로 가리키게 되는 것'''이다.<ref name="x86-pte">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/include/asm/pgtable_types.h?h=v6.12 Linux v6.12 arch/x86/include/asm/pgtable_types.h], 페이지 테이블 속성 비트.</ref><ref name="rv-pte">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/include/asm/pgtable-bits.h?h=v6.12 Linux v6.12 arch/riscv/include/asm/pgtable-bits.h], _PAGE_PRESENT, _PAGE_READ, _PAGE_WRITE, _PAGE_EXEC, _PAGE_USER.</ref>


RISC-V의 정상적인 페이지 폴트 복구는 시스템 콜의 <code>ecall</code>처럼 <code>epc</code>를 다음 명령으로 증가시키지 않는다. 저장된 fault 명령 위치로 복귀해 같은 load를 다시 수행한다. x86-64도 성공한 페이지 폴트 처리 뒤에는 중단된 load를 다시 실행한다. 레지스터 복원과 사용자 모드 복귀 방식은 [[운영체제 기본/Linux의 ELF 로딩/초기 스택과 사용자 모드 진입#사용자 모드 진입: x86-64와 RISC-V|두 아키텍처의 사용자 모드 진입]]에서 다룬다. 커널 안에서 잠금이나 입출력 때문에 폴트 처리를 재시도하는 일과, 사용자 모드로 돌아가 CPU 명령을 다시 실행하는 일은 구별한다.<ref name="rv-traps" /><ref name="rv-entry" /><ref name="rv-fault" /><ref name="x86fault" />
RISC-V의 정상적인 페이지 폴트 복구는 시스템 콜의 <code>ecall</code>처럼 <code>epc</code>를 다음 명령으로 증가시키지 않는다. 저장된 fault 명령 위치로 복귀해 같은 load를 다시 수행한다. x86-64도 성공한 페이지 폴트 처리 뒤에는 중단된 load를 다시 실행한다. 레지스터 복원과 사용자 모드 복귀 방식은 [[운영체제 기본/Linux의 ELF 로딩/초기 스택과 사용자 모드 진입#사용자 모드 진입: x86-64와 RISC-V|두 아키텍처의 사용자 모드 진입]]에서 다룬다. 커널 안에서 잠금이나 입출력 때문에 폴트 처리를 재시도하는 일과, 사용자 모드로 돌아가 CPU 명령을 다시 실행하는 일은 구별한다.<ref name="rv-traps" /><ref name="rv-entry" /><ref name="rv-fault" /><ref name="x86fault" />


== read와 mmap의 비용과 수명 ==
일반적인 buffered <code>read()</code>는 시스템 콜(System Call)이 요청한 파일 데이터를 사용자 버퍼로 복사해 전달한다. 반면 이 <code>mmap()</code> 경로는 파일 데이터가 있는 페이지를 사용자 가상 주소에 연결하고 CPU load로 읽게 한다. 별도 버퍼 복사를 피하더라도 저장장치 입출력이 없거나 모든 복사가 사라지거나 언제나 더 빠르지는 않다. 폴트와 페이지 테이블 관리 비용도 접근 패턴에 좌우된다.
매핑 시작 파일 오프셋은 페이지 크기에 맞게 정렬하고, 읽을 바이트가 매핑 길이와 파일 데이터 범위 안에 있는지 확인한다. <code>mmap()</code> 성공은 파일이 이후에도 같은 길이라는 보장이 아니다. 다른 실행 흐름이 파일을 줄여 해당 페이지가 파일 끝 밖으로 밀려나면, VMA 범위 안의 주소라도 <code>SIGBUS</code>가 발생할 수 있다. 이는 단순한 VMA 부재와 다르다.<ref name="filemap" />
성공한 매핑은 파일 디스크립터를 <code>close()</code>한 뒤에도 유지된다. 그러나 <code>munmap()</code> 뒤의 포인터는 더 이상 그 매핑을 가리키지 않는다. 주소가 재사용될 수도 있으므로 이후 접근이 반드시 신호로 실패하지도 않는다.


== 참고 문헌 ==
== 참고 문헌 ==
116번째 줄: 94번째 줄:


{{이전 다음 문서
{{이전 다음 문서
|이전=← 이전 문서<br />[[운영체제 기본/Linux의 파일 메모리 매핑/mmap과 주소 공간|mmap과 주소 공간]]
|이전=← 이전 문서<br>[[운영체제 기본/Linux의 메모리 매핑/mmap과 주소 공간|mmap과 주소 공간]]
|목차=[[운영체제 기본/Linux의 파일 메모리 매핑|파일 메모리 매핑 목차]]
|목차=[[운영체제 기본/Linux의 메모리 매핑|메모리 매핑 목차]]
|다음=다음 문서 →<br>[[운영체제 기본/Linux의 메모리 매핑/익명 메모리 매핑|익명 메모리 매핑]]
}}
}}
{{운영체제 기본 둘러보기}}
{{운영체제 기본 둘러보기}}