전체 흐름
CPU의 translation/access/permission fault가 모두 새 페이지 할당을 뜻하지는 않습니다. ARM64 예외 핸들러는 FAR·ESR·pt_regs에서 주소와 접근 종류를 정하고, 보호된 VMA에서 권한을 확인한 뒤 handle_mm_fault()로 넘깁니다. 결과는 기존 PTE의 접근 비트 갱신, anonymous/file 처리, 재시도, OOM 또는 시그널일 수 있습니다.
ESR → 실행·쓰기·읽기
pt_regs → EL0/EL1
실패·권한 불일치: mmap_lock 경로
EL1: mmap_lock 경로
handle_mm_fault() → hugetlb 또는 일반 page table
일반 사용자 fault 중심의 개념도입니다. huge page와 kernel translation fault는 단순화했습니다.
고정 소스: arch/arm64/mm/fault.c:530–700, mm/memory.c:4655–4727, 4947–5023.
소스 코드 위치
먼저 확인할 Linux v6.6 소스 파일
Linux v6.6: arch/arm64/mm/fault.c
Linux v6.6: mm/memory.c
Linux v6.6: mm/mmap.c
설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.
GitHub 원본:
대표 코드
do_page_fault
}
#define VM_FAULT_BADMAP ((__force vm_fault_t)0x010000)
#define VM_FAULT_BADACCESS ((__force vm_fault_t)0x020000)
static vm_fault_t __do_page_fault(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long addr,
unsigned int mm_flags, unsigned long vm_flags,
struct pt_regs *regs)
{
/*
* Ok, we have a good vm_area for this memory access, so we can handle
* it.
* Check that the permissions on the VMA allow for the fault which
* occurred.
*/
if (!(vma->vm_flags & vm_flags))
return VM_FAULT_BADACCESS;
return handle_mm_fault(vma, addr, mm_flags, regs);
}
static bool is_el0_instruction_abort(unsigned long esr)
{
return ESR_ELx_EC(esr) == ESR_ELx_EC_IABT_LOW;
}
/*
* Note: not valid for EL1 DC IVAC, but we never use that such that it
* should fault. EL0 cannot issue DC IVAC (undef).
*/
static bool is_write_abort(unsigned long esr)
{
return (esr & ESR_ELx_WNR) && !(esr & ESR_ELx_CM);
}
static int __kprobes do_page_fault(unsigned long far, unsigned long esr,
struct pt_regs *regs)
{
const struct fault_info *inf;
struct mm_struct *mm = current->mm;
확인 사항: 이 발췌는 __do_page_fault()의 VMA 권한 검사와 do_page_fault() 시작을 함께 보여 줍니다. handle_mm_fault() 정의는 mm/memory.c에 있습니다. 첫 닫는 중괄호는 발췌 경계입니다.
do_page_fault 진입부의 다음 발췌
* Check that the permissions on the VMA allow for the fault which
* occurred.
*/
if (!(vma->vm_flags & vm_flags))
return VM_FAULT_BADACCESS;
return handle_mm_fault(vma, addr, mm_flags, regs);
}
static bool is_el0_instruction_abort(unsigned long esr)
{
return ESR_ELx_EC(esr) == ESR_ELx_EC_IABT_LOW;
}
/*
* Note: not valid for EL1 DC IVAC, but we never use that such that it
* should fault. EL0 cannot issue DC IVAC (undef).
*/
static bool is_write_abort(unsigned long esr)
{
return (esr & ESR_ELx_WNR) && !(esr & ESR_ELx_CM);
}
static int __kprobes do_page_fault(unsigned long far, unsigned long esr,
struct pt_regs *regs)
{
const struct fault_info *inf;
struct mm_struct *mm = current->mm;
vm_fault_t fault;
unsigned long vm_flags;
unsigned int mm_flags = FAULT_FLAG_DEFAULT;
unsigned long addr = untagged_addr(far);
struct vm_area_struct *vma;
if (kprobe_page_fault(regs, esr))
return 0;
/*
* If we're in an interrupt or have no user context, we must not take
* the fault.
*/
확인 사항: 이 두 번째 발췌도 arch/arm64/mm/fault.c의 do_page_fault() 시작부입니다. 아래 VMA 잠금·retry 분기는 원본 링크에서 이어서 확인합니다.
함수별 분석
do_mem_abort()의 fault_info가 page fault 경로를 택하면 do_page_fault()가 접근 유형을 해석합니다. EL0 빠른 VMA 경로의 권한 불일치는 이 버전에서 곧장 SIGSEGV로 가지 않고 lock_mmap에서 재검사합니다. 느린 경로의 __do_page_fault()가 권한 검사 후 handle_mm_fault()를 호출합니다.
| 위치 | 입력·판정 | 분기·나갈 때 |
|---|---|---|
| ARM64 do_page_fault() | far·esr·regs, current->mm | faulthandler_disabled/no mm이면 no_context. EL0만 per-VMA 잠금 먼저 시도. |
| VMA 빠른 경로 | lock_vma_under_rcu()의 VMA read lock | 권한 불일치·VMA 없음은 mmap_lock 경로. RETRY/COMPLETED는 잠금 해제를 반영. |
| VMA 느린 경로 | lock_mm_and_find_vma() 성공 시 mmap_lock | __do_page_fault()에서 vm_flags 검사 후 handle_mm_fault(). RETRY면 VMA 재조회. |
| MM 공통 처리 | VMA·주소·FAULT_FLAG_* | hugetlb 또는 __handle_mm_fault(); 일반 PTE는 없음/스왑/NUMA/쓰기 보호/접근 갱신. |
잠금 계약: VM_FAULT_RETRY 또는 VM_FAULT_COMPLETED 뒤에는 이전 VMA를 계속 사용하지 않습니다. RETRY에서는 주소로 VMA를 다시 찾습니다.
구조체와 필드
예외 입력, VMA 접근 정책, 페이지 테이블 상태, 반환값은 서로 다른 판단 근거입니다. 하나의 순차적 객체 열로 읽으면 VMA가 PTE를 직접 소유한다는 잘못된 인상을 줍니다.
| 객체·값 | 역할 | 보호·수명 |
|---|---|---|
| far / esr / pt_regs | 주소, exception 원인, EL0/EL1·접근 유형 | arch fault 진입의 입력; 사용자/커널 처리 구별 |
| mm_struct / vm_area_struct | 주소 공간과 해당 범위의 vm_flags | per-VMA read lock 또는 mmap_lock으로 보호; RETRY 뒤 옛 VMA 사용 금지 |
| vm_fault / orig_pte | MM 내부 주소·flags·원래 PTE 상태 | 일반 present PTE 갱신은 ptl 아래 재검사 |
| vm_fault_t | 해결, RETRY, COMPLETED, OOM, SIGBUS 등의 결과 | 호출자의 unlock·재탐색·시그널 분기 결정 |
실행 단계와 상태 변화
fault 성공은 항상 “PTE 하나를 새로 설치했다”는 뜻이 아닙니다. 이미 present인 PTE는 접근/dirty 상태만 바꾸거나 경쟁자가 먼저 해결했을 수 있습니다.
| PTE 상태 | 일반 PTE의 대표 경로 | 호출자 관찰 |
|---|---|---|
| 빈 PTE | anonymous → do_anonymous_page(); file → do_fault() | 페이지 준비와 PTE 설치 또는 retry/error 가능 |
| non-present PTE | do_swap_page() | swap/marker 복구 또는 오류 |
| NUMA 보호 PTE | do_numa_page() | NUMA balancing |
| 쓰기 보호 PTE | do_wp_page() | COW 또는 쓰기 허용. 단순 할당과 다름 |
| present PTE | ptl 아래 재확인 후 young/dirty/access 갱신 | 새 folio 생성 없이 해결 가능 |
hugetlb 및 상위 단계의 huge PMD/PUD 경로는 별도입니다. 이 표는 일반 PTE 경로만 분리합니다.
불변 조건과 실패 사례
no_context 경로를 봅니다.do_wp_page()로 향합니다. 모든 COW를 단순 할당으로 요약하지 않습니다.계측과 검증
/proc/<pid>/maps로 주소가 어느 VMA에 있는지 대조하고, perf stat -e page-faults,minor-faults,major-faults -- <command>로 fault 발생량을 볼 수 있습니다. 카운터만으로 특정 PTE 분기나 시그널 원인을 증명하지는 못합니다.
| 질문 | 관찰 자료 | 한계 |
|---|---|---|
| 주소가 VMA 밖인가? | /proc/<pid>/maps, SIGSEGV si_code | 실행 중 VMA 변화에 유의해 동일 주소·시점 확인 |
| fault가 반복되는가? | minor/major counters, perf page-faults | COW·swap·NUMA·권한 실패를 카운터만으로 구분 못함 |
| 잠금 재시도를 거쳤나? | fault.c의 retry label, 필요 시 표적 ftrace | 잠금 해제와 VMA 재획득을 함께 확인 |
추가 확인 사항
- EL0 빠른 VMA 경로의 VMA 없음과 권한 불일치는 어디로 이동하는가?
- 빈 PTE, non-present, NUMA 보호, 쓰기 보호, present PTE는 각각 어디로 갈라지는가?
- VM_FAULT_RETRY/COMPLETED 뒤 잠금과 VMA 수명은 누가 책임지는가?
- 사용자 시그널과 커널 no_context를 어떤 조건으로 구분하는가?