본문으로 이동
  • 현재 모든 문서는 작성 중입니다. 보시는 데 불편함이 있더라도 양해 부탁드립니다.

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

~ldmsys
용어 통일: 시스템 콜, 컨텍스트 스위치와 첫 등장 영문 병기
잔글 Linux 각론 경로 변경에 따른 본문·목차·틀 링크 갱신
 
(같은 사용자의 중간 판 8개는 보이지 않습니다)
1번째 줄: 1번째 줄:
[[운영체제 기본/Linux의 파일 메모리 매핑|파일 메모리 매핑 목차]] · 이전 글 [[운영체제 기본/Linux의 파일 메모리 매핑/mmap과 주소 공간|mmap과 주소 공간]]
{{이전 다음 문서
|이전=← 이전 문서<br>[[운영체제 기본/Linux/메모리 매핑/mmap과 주소 공간|mmap과 주소 공간]]
|목차=[[운영체제 기본/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, 전통적인 IDT 예외 진입, 일반 ext4 파일의 buffered 경로를 기준으로 그 주소를 읽는다. 매핑은 <code>PROT_READ</code>와 <code>MAP_PRIVATE</code>로 만들었고 <code>MAP_POPULATE</code>와 DAX는 사용하지 않는다. 아래 흐름은 보통 크기 페이지의 PTE가 아직 없는 경우다.
Linux v6.12의 기본 페이지 경로를 기준으로, CPU 예외 진입과 공통 메모리 관리 코드의 연결을 살펴본다. 실제 페이지를 마련하는 방법은 익명 메모리와 파일 매핑 장에서 이어진다.
 
== CPU 예외와 VMA 검사 ==
두 CPU 모두 필요한 주소 변환이 없으면 동기 예외를 일으킨다. 커널은 폴트 주소와 접근 종류를 알아내고, VMA가 그 접근을 허용하는지 검사한 뒤 공통 함수 <code>handle_mm_fault()</code>에 복구를 요청한다. '''예외 진입 경로와 정보의 표현은 아키텍처별이고, VMA와 페이지 테이블을 다루는 핵심 경로는 공통이다.'''


<pre>
<pre>
/* p는 성공한 mmap()의 반환값이며 i는 매핑과 파일 범위 안의 오프셋이다. */
load bytes[i]
const unsigned char *bytes = p;
  |
unsigned char value = bytes[i];
  +-- x86-64: #PF -> IDT -> exc_page_fault
  |            -> handle_page_fault -> do_user_addr_fault
  |
  +-- RV64: page fault -> handle_exception
                -> do_page_fault -> handle_page_fault
  |
  v
VMA lookup + access check
  |
  v
handle_mm_fault -> 대상별 처리 -> PTE 설정 -> 명령 재시도
</pre>
</pre>


load 명령은 <code>read()</code>를 호출하지 않는다. CPU는 가상 주소 <code>bytes + i</code>를 읽는다. VMA는 커널이 관리하는 영역 설명이고, PTE는 CPU의 주소 변환에 쓰이는 페이지 테이블 항목이다. 따라서 '''VMA가 존재한다는 사실과 그 주소를 지금 변환할 수 있다는 사실은 다르다'''. 파일 내용이 이미 메모리에 있더라도 이 프로세스의 PTE는 없을 수 있다.
이 예외는 장치가 비동기적으로 보낸 완료 인터럽트가 아니다. 지금 실행한 load 때문에 발생한다. 두 소스 트리에 <code>handle_page_fault</code>라는 같은 이름이 등장하지만 서로 다른 아키텍처별 함수다.
 
앞선 다른 주소의 폴트가 주변 PTE까지 설치했다면 첫 load도 바로 성공한다. 반대로 당장 사용할 PTE가 없으면, 정상적인 매핑 안의 읽기라도 [[운영체제 기본/가상 메모리#페이지 폴트의 처리|페이지 폴트]]가 발생한다. 이는 프로그램 오류와 다르다.


== CPU 예외에서 VMA 검사까지 ==
=== x86-64: #PF, CR2, 오류 코드 ===


present PTE가 없으면 x86-64 CPU는 페이지 폴트 예외 <code>#PF</code>를 발생시킨다. 예외 벡터는 14이고 fault 주소는 CR2에 남는다. [[운영체제 기본/장치 제어와 입출력#처리 진입점과 IDT|IDT]]에서 본 진입 장치를 사용하지만, 이 사건은 장치가 비동기적으로 보낸 완료 인터럽트가 아니라 지금 실행한 명령 때문에 발생한 동기 예외다.
사용 가능한 변환이 없으면 x86-64 CPU는 페이지 폴트 예외 <code>#PF</code>를 발생시킨다. 예외 벡터는 14이고 폴트 주소는 <code>CR2</code>에 남는다. CPU가 전달한 오류 코드는 페이지가 존재하지 않는 경우와 보호 위반, 읽기와 쓰기, 사용자 모드 접근 등의 정보를 구별한다. 아직 연결되지 않은 주소의 읽기와, 읽기 전용 PTE에 대한 쓰기는 오류 코드에서 구별된다.


<pre>
<pre>
load bytes[i]
load bytes[i]
   → #PF, vector 14
   → #PF, vector 14, CR2 = 폴트 주소, 오류 코드
   → asm_exc_page_fault
   → IDT → asm_exc_page_fault
   → exc_page_fault
   → exc_page_fault
   → handle_page_fault
   → handle_page_fault
   → do_user_addr_fault
   → do_user_addr_fault
  → handle_mm_fault
</pre>
</pre>


v6.12의 IDT는 해당 예외를 <code>asm_exc_page_fault</code>로 연결하며, C 쪽 <code>exc_page_fault</code>는 전통적인 진입 경로에서 CR2를 읽고 <code>handle_page_fault()</code>를 호출한다. 사용자 주소에 대한 처리는 <code>do_user_addr_fault()</code>로 이어진다.<ref name="idt">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/kernel/idt.c?h=v6.12 Linux v6.12 arch/x86/kernel/idt.c], 페이지 폴트 IDT 항목.</ref><ref name="x86fault">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/mm/fault.c?h=v6.12 Linux v6.12 arch/x86/mm/fault.c], exc_page_fault, handle_page_fault, do_user_addr_fault.</ref>
v6.12의 [[운영체제 기본/장치 제어와 입출력#처리 진입점과 IDT|IDT]]는 해당 예외를 <code>asm_exc_page_fault</code>로 연결한다. C 쪽 <code>exc_page_fault</code>는 이 진입 경로에서 CR2를 읽고 <code>handle_page_fault()</code>를 호출한다. 사용자 주소 처리는 <code>do_user_addr_fault()</code>로 이어진다.<ref name="idt">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/kernel/idt.c?h=v6.12 Linux v6.12 arch/x86/kernel/idt.c], 페이지 폴트 IDT 항목.</ref><ref name="x86fault">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/mm/fault.c?h=v6.12 Linux v6.12 arch/x86/mm/fault.c], exc_page_fault, handle_page_fault, do_user_addr_fault와 오류 코드 검사.</ref>
 
커널은 먼저 fault 주소를 포함하는 VMA를 찾아 접근 권한을 확인한다. v6.12에는 <code>lock_vma_under_rcu()</code>를 이용한 빠른 경로가 있고, 사용할 수 없거나 재시도가 필요하면 <code>lock_mm_and_find_vma()</code> 경로로 넘어간다. 매핑이 없거나 허용되지 않은 접근이라면 정상적인 파일 읽기 복구를 진행하지 않고 신호 처리로 이어질 수 있다. 유효한 읽기라면 <code>handle_mm_fault()</code>로 실제 주소 변환 복구를 요청한다.<ref name="x86fault" />


== 파일 읽기 폴트로 분기하기 ==
=== RISC-V RV64: scause, stval, sepc ===


<code>handle_mm_fault()</code>는 공통 검사와 회계 처리를 감싸며, 여기서 추적하는 일반 경로는 <code>__handle_mm_fault()</code>로 들어간다. 커널은 페이지 테이블 계층을 따라간다. PTE가 비어 있다면 다음 분기가 중요하다.
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>
handle_mm_fault
load bytes[i]
   → __handle_mm_fault
  → load page fault: scause = 13, stval = 폴트 주소, sepc = load 주소
   → handle_pte_fault
   → handle_exception
   → do_pte_missing
   → excp_vect_table의 load page fault 항목
   → do_fault
   → do_page_fault                [arch/riscv/kernel/traps.c]
   → do_read_fault
   → handle_page_fault            [arch/riscv/mm/fault.c]
   → handle_mm_fault              [mm/memory.c]
</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>handle_exception</code>은 트랩 레지스터 값을 <code>pt_regs</code>의 <code>cause</code>, <code>badaddr</code>, <code>epc</code> 등에 저장한다. 예외 표가 선택하는 <code>do_page_fault()</code>는 <code>handle_page_fault()</code>를 부른다. 이 함수는 저장된 원인과 주소를 사용하고, <code>access_error()</code>에서 VMA의 읽기·쓰기·실행 조건을 확인한다.<ref name="rv-entry">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/kernel/entry.S?h=v6.12 Linux v6.12 arch/riscv/kernel/entry.S], handle_exception, excp_vect_table, ret_from_exception.</ref><ref name="rv-traps">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/kernel/traps.c?h=v6.12 Linux v6.12 arch/riscv/kernel/traps.c], do_page_fault.</ref><ref name="rv-fault">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/mm/fault.c?h=v6.12 Linux v6.12 arch/riscv/mm/fault.c], handle_page_fault, access_error.</ref>


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


== 파일 데이터 준비와 PTE 설치 ==
== 공통 폴트 처리와 분기 ==
CPU는 VMA를 읽지 않는다. CPU가 보는 것은 페이지 테이블이고, 커널은 예외를 받은 뒤 VMA를 확인한다. 따라서 VMA가 쓰기를 허용해도 PTE는 일시적으로 읽기 전용일 수 있다. 쓰기 시점에 COW를 수행하려고 의도적으로 남겨 둔 차이다.


fault-around로 해결하지 못한 일반 경로에서는 <code>__do_fault()</code>가 <code>vma-&gt;vm_ops-&gt;fault(vmf)</code>를 호출한다. 앞 글에서 ext4가 등록한 이 함수 포인터의 대상이 <code>filemap_fault()</code>다. 가상 메모리와 파일 데이터 준비의 연결점이다.<ref name="memory" /><ref name="ext4" />
<pre>
handle_mm_fault → __handle_mm_fault → handle_pte_fault
  |
  +-- PTE 없음 → do_pte_missing
  |      +-- private anonymous → do_anonymous_page
  |      +-- 파일 등의 처리기 → do_fault
  |
  +-- 읽기 전용 PTE에 쓰기 → do_wp_page
</pre>


파일 데이터가 이미 준비되어 있을 수도 있고, 저장장치에서 읽어 올 필요가 있을 수도 있다. 필요한 작업을 기다리는 동안 현재 태스크가 잠들 수 있다는 점은 [[운영체제 기본/프로세스와 스레드#기다리는 작업에 CPU를 주지 않는 이유|입출력 대기]]에 연결된다. 잠금을 놓았다가 다시 얻어 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>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>


정상적으로 파일 페이지를 얻은 경로에서는 <code>finish_fault()</code>가 페이지 테이블 잠금 아래 상태를 다시 확인하고 <code>set_pte_range()</code>를 통해 PTE를 설치한다. PTE는 해당 페이지와 보호 속성을 연결한다. 예외 처리가 성공적으로 끝나 사용자 실행으로 돌아오면 중단됐던 load 명령이 다시 실행된다. 이번에는 주소 변환이 가능하므로 그 바이트를 읽는다.<ref name="memory" />
앞으로 [[운영체제 기본/Linux/메모리 매핑/익명 메모리 매핑|익명 매핑]]에서는 0으로 보이는 메모리를 마련하고, [[운영체제 기본/Linux/메모리 매핑/파일 매핑과 COW|파일 매핑]]에서는 파일 페이지를 가져오거나 사본을 만든다. [[운영체제 기본/Linux/메모리 매핑/MMIO 장치 매핑|MMIO 매핑]]은 드라이버가 매핑 시점에 장치 PTE를 설치할 수 있으므로, 모든 매핑이 이 지연 할당 순서를 거치는 것은 아니다.


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


일반적인 buffered <code>read()</code>는 시스템 콜(System Call)이 요청한 파일 데이터를 사용자 버퍼로 복사해 전달한다. 반면 이 <code>mmap()</code> 경로는 파일 데이터가 있는 페이지를 사용자 가상 주소에 연결하고 CPU load로 읽게 한다. 별도 버퍼 복사를 피하더라도 저장장치 입출력이 없거나 모든 복사가 사라지거나 언제나 더 빠르지는 않다. 폴트와 페이지 테이블 관리 비용도 접근 패턴에 좌우된다.
== 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>


매핑 시작 파일 오프셋은 페이지 크기에 맞게 정렬하고, 읽을 바이트가 매핑 길이와 파일 데이터 범위 안에 있는지 확인한다. <code>mmap()</code> 성공은 파일이 이후에도 같은 길이라는 보장이 아니다. 다른 실행 흐름이 파일을 줄여 해당 페이지가 파일 끝 밖으로 밀려나면, VMA 범위 안의 주소라도 <code>SIGBUS</code>가 발생할 수 있다. 이는 단순한 VMA 부재와 다르다.<ref name="filemap" />
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" />


성공한 매핑은 파일 디스크립터를 <code>close()</code>한 뒤에도 유지된다. 그러나 <code>munmap()</code> 뒤의 포인터는 더 이상 그 매핑을 가리키지 않는다. 주소가 재사용될 수도 있으므로 이후 접근이 반드시 신호로 실패하지도 않는다.


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

2026년 11월 1일 (일) 02:21 기준 최신판

mmap()으로 만든 VMA는 주소 구간의 의미와 권한을 정한다. 그 주소의 PTE가 아직 없거나 접근에 필요한 권한이 부족하면 CPU가 페이지 폴트(Page Fault)를 발생시킨다. 정상적인 지연 할당과 쓰기 시 복사(Copy-on-Write, COW)도 이 예외를 이용하므로, 페이지 폴트가 곧 프로그램 오류라는 뜻은 아니다.

Linux v6.12의 기본 페이지 경로를 기준으로, CPU 예외 진입과 공통 메모리 관리 코드의 연결을 살펴본다. 실제 페이지를 마련하는 방법은 익명 메모리와 파일 매핑 장에서 이어진다.

CPU 예외와 VMA 검사

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

load bytes[i]
  |
  +-- x86-64: #PF -> IDT -> exc_page_fault
  |             -> handle_page_fault -> do_user_addr_fault
  |
  +-- RV64: page fault -> handle_exception
                 -> do_page_fault -> handle_page_fault
  |
  v
VMA lookup + access check
  |
  v
handle_mm_fault -> 대상별 처리 -> PTE 설정 -> 명령 재시도

이 예외는 장치가 비동기적으로 보낸 완료 인터럽트가 아니다. 지금 실행한 load 때문에 발생한다. 두 소스 트리에 handle_page_fault라는 같은 이름이 등장하지만 서로 다른 아키텍처별 함수다.

x86-64: #PF, CR2, 오류 코드

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

load bytes[i]
  → #PF, vector 14, CR2 = 폴트 주소, 오류 코드
  → IDT → asm_exc_page_fault
  → exc_page_fault
  → handle_page_fault
  → do_user_addr_fault
  → handle_mm_fault

v6.12의 IDT는 해당 예외를 asm_exc_page_fault로 연결한다. C 쪽 exc_page_fault는 이 진입 경로에서 CR2를 읽고 handle_page_fault()를 호출한다. 사용자 주소 처리는 do_user_addr_fault()로 이어진다.[1][2]

RISC-V RV64: scause, stval, sepc

RISC-V에서는 stvec가 지정하는 트랩 진입점으로 제어가 넘어간다. S-mode 커널의 트랩 진입에서 scause는 원인을, stval은 페이지 폴트 주소를, sepc는 중단된 명령의 주소를 제공한다. 페이지 폴트 원인 번호는 명령어 가져오기 12, load 13, store/AMO 15로 나뉜다. 따라서 읽기 접근은 원인 13인 load page fault다. x86의 페이지 폴트 오류 코드 비트를 그대로 적용하는 설명은 맞지 않는다.[3]

load bytes[i]
  → load page fault: scause = 13, stval = 폴트 주소, sepc = load 주소
  → handle_exception
  → excp_vect_table의 load page fault 항목
  → do_page_fault                [arch/riscv/kernel/traps.c]
  → handle_page_fault            [arch/riscv/mm/fault.c]
  → handle_mm_fault              [mm/memory.c]

handle_exception은 트랩 레지스터 값을 pt_regs의 cause, badaddr, epc 등에 저장한다. 예외 표가 선택하는 do_page_fault()는 handle_page_fault()를 부른다. 이 함수는 저장된 원인과 주소를 사용하고, access_error()에서 VMA의 읽기·쓰기·실행 조건을 확인한다.[4][5][6]

VMA 검사와 공통 폴트 처리

두 아키텍처의 처리 함수 모두 폴트 주소를 포함하는 VMA를 찾아 권한을 검사한다. 매핑이 없거나 허용되지 않은 접근이면 정상적인 폴트 복구 대신 신호 처리로 이어질 수 있다. VMA가 허용한 접근이라면 handle_mm_fault()로 진행한다.[2][6]

공통 폴트 처리와 분기

CPU는 VMA를 읽지 않는다. CPU가 보는 것은 페이지 테이블이고, 커널은 예외를 받은 뒤 VMA를 확인한다. 따라서 VMA가 쓰기를 허용해도 PTE는 일시적으로 읽기 전용일 수 있다. 쓰기 시점에 COW를 수행하려고 의도적으로 남겨 둔 차이다.

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

do_pte_missing()은 private anonymous VMA와 파일 등의 처리기를 사용하는 VMA를 나눈다. do_fault()는 읽기, private 쓰기, shared 쓰기를 각각 do_read_fault(), do_cow_fault(), do_shared_fault()로 나눈다. 각 분기는 VMA가 허용한 접근에 필요한 페이지와 권한을 준비한다.[7]

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

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

PTE 설정과 접근 재시도

공통 코드가 사용하는 PTE라는 이름이 하드웨어 비트 형식까지 같다는 뜻은 아니다. x86-64는 present 비트와 읽기/쓰기, 사용자 접근 등의 속성을 사용하고, RISC-V는 V(valid), R, W, X, U 등의 비트로 유효성과 접근 조건을 표현한다. 페이지 테이블 단계와 실제 엔트리 조작은 아키텍처별 구현에 맡긴다. 이 장에서 중요한 공통 결과는 해당 프로세스의 가상 주소가 준비된 페이지를 허용된 권한으로 가리키게 되는 것이다.[8][9]

RISC-V의 정상적인 페이지 폴트 복구는 시스템 콜의 ecall처럼 epc를 다음 명령으로 증가시키지 않는다. 저장된 fault 명령 위치로 복귀해 같은 load를 다시 수행한다. x86-64도 성공한 페이지 폴트 처리 뒤에는 중단된 load를 다시 실행한다. 레지스터 복원과 사용자 모드 복귀 방식은 두 아키텍처의 사용자 모드 진입에서 다룬다. 커널 안에서 잠금이나 입출력 때문에 폴트 처리를 재시도하는 일과, 사용자 모드로 돌아가 CPU 명령을 다시 실행하는 일은 구별한다.[5][4][6][2]


참고 문헌

  1. ↑ Linux v6.12 arch/x86/kernel/idt.c, 페이지 폴트 IDT 항목.
  2. ↑ 2.0 2.1 2.2 Linux v6.12 arch/x86/mm/fault.c, exc_page_fault, handle_page_fault, do_user_addr_fault와 오류 코드 검사.
  3. ↑ Linux v6.12 arch/riscv/include/asm/csr.h, EXC_*_PAGE_FAULT, CSR_SCAUSE, CSR_STVAL, CSR_SEPC와 S-mode 별칭.
  4. ↑ 4.0 4.1 Linux v6.12 arch/riscv/kernel/entry.S, handle_exception, excp_vect_table, ret_from_exception.
  5. ↑ 5.0 5.1 Linux v6.12 arch/riscv/kernel/traps.c, do_page_fault.
  6. ↑ 6.0 6.1 6.2 Linux v6.12 arch/riscv/mm/fault.c, handle_page_fault, access_error.
  7. ↑ Linux v6.12, mm/memory.c, do_pte_missing, do_anonymous_page, do_fault, do_wp_page, wp_page_copy.
  8. ↑ Linux v6.12 arch/x86/include/asm/pgtable_types.h, 페이지 테이블 속성 비트.
  9. ↑ Linux v6.12 arch/riscv/include/asm/pgtable-bits.h, _PAGE_PRESENT, _PAGE_READ, _PAGE_WRITE, _PAGE_EXEC, _PAGE_USER.