MGLRU(Multi-Gen LRU) 페이지 에이징이나 fork() 이후 CoW(Copy-on-Write) 처리처럼, 커널이 “이 페이지가 최근에 접근됐는가”를 알아야 하는 곳이 많다. 이걸 알아내는 방법은 두 가지다 — 오래된 PTE에 접근할 때마다 일부러 페이지 폴트를 유발해 소프트웨어가 accessed 비트를 관리하거나, MMU가 접근이 일어나는 순간 하드웨어적으로 알아서 PTE의 accessed 비트를 세워주거나. arch_has_hw_pte_young()은 지금 실행 중인 CPU가 후자(하드웨어 관리)를 지원하는지 런타임에 알려주는 함수다. 이 글에서는 실제 커널 소스로 아키텍처별 구현 차이와 실제 호출 지점 두 곳을 정리한다.
제네릭 기본 구현
아키텍처가 따로 오버라이드하지 않으면 include/linux/pgtable.h의 기본 구현이 쓰인다.
#ifndef arch_has_hw_pte_young
/*
* Return whether the accessed bit is supported on the local CPU.
*
* This stub assumes accessing through an old PTE triggers a page fault.
* Architectures that automatically set the access bit should overwrite it.
*/
static inline bool arch_has_hw_pte_young(void)
{
return IS_ENABLED(CONFIG_ARCH_HAS_HW_PTE_YOUNG);
}
#endif주석 그대로다 — 아무도 오버라이드하지 않으면 “오래된 PTE 접근은 페이지 폴트로 이어진다”고 가정한다. CONFIG_ARCH_HAS_HW_PTE_YOUNG이 꺼져 있으면 무조건 false다.
아키텍처별 구현 비교
| 아키텍처 | 판단 방식 | 근거 |
|---|---|---|
| x86 | 컴파일 타임 상수(항상 true) | arch/x86/Kconfig에서 ARCH_HAS_HW_PTE_YOUNG을 무조건 select — 80386부터 accessed 비트가 하드웨어 관리였기 때문에 판단 자체가 필요 없음 |
| arm64 | 런타임 CPU 기능 검사 | ID_AA64MMFR1_EL1 레지스터의 HAFDBS 필드(ARMv8.1 FEAT_HAFDBS)를 읽어 판단 — 모든 armv8 코어가 지원하는 게 아니라 검사가 필요함 |
| riscv | 런타임 ISA 확장 검사 | Svadu 확장 지원 여부를 riscv_has_extension_unlikely()로 확인 |
x86은 애초에 판단할 필요가 없을 만큼 오래전부터 하드웨어 관리가 표준이었던 반면, arm64와 riscv는 하드웨어 관리 자체가 비교적 최근에 추가된 선택적 기능이라 부팅 시점에 CPU 스스로 물어봐야 한다.
arm64 구현 상세
arm64는 매크로로 cpu_has_hw_af()를 그대로 연결한다.
/*
* On arm64 without hardware Access Flag, copying from user will fail because
* the pte is old and cannot be marked young. So we always end up with zeroed
* page after fork() + CoW for pfn mappings. We don't always have a
* hardware-managed access flag on arm64.
*/
#define arch_has_hw_pte_young cpu_has_hw_af/* Check whether hardware update of the Access flag is supported */
static inline bool cpu_has_hw_af(void)
{
u64 mmfr1;
if (!IS_ENABLED(CONFIG_ARM64_HW_AFDBM))
return false;
/*
* Use cached version to avoid emulated msr operation on KVM
* guests.
*/
mmfr1 = read_sanitised_ftr_reg(SYS_ID_AA64MMFR1_EL1);
return cpuid_feature_extract_unsigned_field(mmfr1,
ID_AA64MMFR1_EL1_HAFDBS_SHIFT);
}커널 빌드 옵션 CONFIG_ARM64_HW_AFDBM이 꺼져 있으면 하드웨어가 실제로 지원해도 무조건 false를 반환한다 — 커널 설정과 실제 CPU 기능 두 조건을 모두 만족해야 하드웨어 경로를 탄다는 뜻이다. 통과하면 ID_AA64MMFR1_EL1의 HAFDBS 필드를 뽑아 지원 여부를 반환한다.
실제 호출 지점 두 곳
첫 번째는 mm/memory.c의 CoW 폴트 처리다. PFN 매핑을 CoW로 복사할 때, 하드웨어 accessed 비트가 없는 아키텍처에서는 그대로 두면 다음 접근에서 또 폴트가 난다. 그래서 소프트웨어가 직접 PTE를 young으로 마킹해준다 — 앞서 arm64 헤더 주석에 있던 “fork() + CoW 이후 zeroed page” 문제가 바로 이 케이스다.
/*
* On architectures with software "accessed" bits, we would
* take a double page fault, so mark it accessed here.
*/
vmf->pte = NULL;
if (!arch_has_hw_pte_young() && !pte_young(vmf->orig_pte)) {
pte_t entry;
vmf->pte = pte_offset_map_lock(mm, vmf->pmd, addr, &vmf->ptl);
if (unlikely(!vmf->pte || !pte_same(ptep_get(vmf->pte), vmf->orig_pte))) {
/*
* Other thread has already handled the fault
* and update local tlb only
*/
if (vmf->pte)
update_mmu_tlb(vma, addr, vmf->pte);
ret = -EAGAIN;
goto pte_unlock;
}
/* ... entry를 young/dirty로 마킹 후 반영 ... */
}두 번째는 mm/vmscan.c의 MGLRU(CONFIG_LRU_GEN) 코드다. MGLRU는 페이지 테이블을 직접 훑어(MM walk) 어떤 페이지가 최근에 쓰였는지 판단하는 모드가 있는데, 이 방식이 의미가 있으려면 accessed 비트가 실제 접근을 반영해야 한다.
static bool should_walk_mmu(void)
{
return arch_has_hw_pte_young() && get_cap(LRU_GEN_MM_WALK);
}하드웨어가 accessed 비트를 관리하지 않는 CPU에서는 MM walk로 페이지 테이블을 훑어도 그 비트가 실제 접근 이력을 담보하지 않으므로, should_walk_mmu()가 false를 반환해 MGLRU가 이 경로를 아예 건너뛴다.
주의사항
| 항목 | 내용 |
|---|---|
| 커널·CPU 두 조건을 모두 만족해야 한다 | arm64에서 CPU가 HAFDBS를 지원해도 CONFIG_ARM64_HW_AFDBM이 꺼져 있으면 소프트웨어 경로로 강제된다. 반대로 커널 옵션이 켜져 있어도 오래된 코어라면 하드웨어 지원 자체가 없다 |
| ARMv8.1 이전 코어에는 이 기능이 없다 | HAFDBS는 ARMv8.1에서 추가된 확장이다. 그보다 오래된 armv8.0 코어는 ID_AA64MMFR1_EL1의 HAFDBS 필드 자체가 0이라 cpu_has_hw_af()가 항상 false를 반환한다 |
| 이 글의 코드는 소스 인용이다 | Linux 커널 v7.1.5 기준 elixir.bootlin.com에서 실제 소스를 그대로 확인해 인용한 것이며, 하드웨어에서 값을 직접 읽어 실행 검증하지는 않았다. 버전에 따라 함수/CONFIG 이름과 위치가 달라질 수 있으니 자신이 쓰는 커널 버전에서 arch_has_hw_pte_young을 다시 검색해볼 것 |
마무리
arch_has_hw_pte_young() 자체는 한 줄짜리 판단 함수지만, 그 답이 CoW 폴트 처리 방식과 MGLRU의 페이지 에이징 전략을 갈라놓는다. x86처럼 하드웨어 관리가 당연한 아키텍처에서는 존재조차 의식할 일이 없다가, arm64나 riscv처럼 하드웨어 지원이 선택적인 아키텍처를 들여다볼 때 비로소 이 함수의 존재 이유가 보인다.