읽는 지도
메모리 코드는 address translation, physical page ownership, reclaim policy, allocation latency가 한꺼번에 얽힌다. 이 층을 박사급으로 읽는다는 것은 '어느 page가 지금 누구 소유인가'와 '그 소유권을 어떤 lock/refcount가 증명하는가'를 끝까지 추적한다는 뜻이다.
함수 이름보다 입력 객체와 출력 객체를 먼저 본다. 이 토픽에서 어떤 구조체가 생성, 연결, publish, retire되는지 표시한다.
정상 경로와 실패 경로를 같은 무게로 본다. kernel code의 품질은 성공 path보다 error unwind, hotplug, teardown에서 더 잘 드러난다.
왼쪽은 입력 또는 상위 계층이고, 오른쪽으로 갈수록 실제 상태 변경이 커진다. 문C식으로 읽을 때는 이 그림을 먼저 머리에 놓고 코드 조각을 끼워 넣는다.
원본 코드 좌표
Linux 6.18.37 LTS에서 열 파일
Linux 6.18.37 LTS: arch/arm64/mm/mmu.c
Linux 6.18.37 LTS: arch/arm64/mm/init.c
Linux 6.18.37 LTS: include/asm-generic/pgtable-nop4d.h
설명: 첫 파일은 보통 진입 함수가 있는 곳이고, 나머지는 구조체 정의, architecture glue, callback 구현을 확인할 때 같이 연다. 파일을 여러 개 놓고 봐야 이 토픽의 boundary가 보인다.
대표 코드
void __init paging_init
__create_pgd_mapping(pgd, pa, pa, sizeof(u32), PAGE_KERNEL,
early_pgtable_alloc, 0);
}
}
void __init paging_init(void)
{
pgd_t *pgdp = pgd_set_fixmap(__pa_symbol(swapper_pg_dir));
extern pgd_t init_idmap_pg_dir[];
idmap_t0sz = 63UL - __fls(__pa_symbol(_end) | GENMASK(VA_BITS_MIN - 1, 0));
map_kernel(pgdp);
map_mem(pgdp);
pgd_clear_fixmap();
cpu_replace_ttbr1(lm_alias(swapper_pg_dir), init_idmap_pg_dir);
init_mm.pgd = swapper_pg_dir;
memblock_phys_free(__pa_symbol(init_pg_dir),
__pa_symbol(init_pg_end) - __pa_symbol(init_pg_dir));
memblock_allow_resize();
create_idmap();
}
#ifdef CONFIG_MEMORY_HOTPLUG
static void free_hotplug_page_range(struct page *page, size_t size,
struct vmem_altmap *altmap)
{
if (altmap) {
vmem_altmap_free(altmap, size >> PAGE_SHIFT);
} else {
WARN_ON(PageReserved(page));
free_pages((unsigned long)page_address(page), get_order(size));
}
}
읽는 법: 이 절편에서 먼저 볼 것은 return 값이 아니라 상태 변경이다. 어떤 lock을 잡은 뒤 어떤 필드를 바꾸는지, 실패하면 어느 label로 빠지는지, 그리고 바뀐 상태를 다음 호출자가 어떤 전제로 소비하는지 표시한다.
map_mem
}
pgprot_t phys_mem_access_prot(struct file *file, unsigned long pfn,
unsigned long size, pgprot_t vma_prot)
{
if (!pfn_is_map_memory(pfn))
return pgprot_noncached(vma_prot);
else if (file->f_flags & O_SYNC)
return pgprot_writecombine(vma_prot);
return vma_prot;
}
EXPORT_SYMBOL(phys_mem_access_prot);
static phys_addr_t __init early_pgtable_alloc(int shift)
{
phys_addr_t phys;
void *ptr;
phys = memblock_phys_alloc_range(PAGE_SIZE, PAGE_SIZE, 0,
MEMBLOCK_ALLOC_NOLEAKTRACE);
if (!phys)
panic("Failed to allocate page table page\n");
/*
* The FIX_{PGD,PUD,PMD} slots may be in active use, but the FIX_PTE
* slot will be free, so we can (ab)use the FIX_PTE slot to initialise
* any level of table.
*/
ptr = pte_set_fixmap(phys);
memset(ptr, 0, PAGE_SIZE);
/*
* Implicit barriers also ensure the zeroed page is visible to the page
* table walker
*/
pte_clear_fixmap();
return phys;
}
읽는 법: 이 절편에서 먼저 볼 것은 return 값이 아니라 상태 변경이다. 어떤 lock을 잡은 뒤 어떤 필드를 바꾸는지, 실패하면 어느 label로 빠지는지, 그리고 바뀐 상태를 다음 호출자가 어떤 전제로 소비하는지 표시한다.
함수별 독해
architecture page table을 구성해 kernel virtual address와 physical memory 사이의 장기 mapping을 확정한다.
이 섹션은 원본 코드 발췌를 함수 이름 단위로 끊어, 각 함수가 어떤 전제 조건을 만들고 다음 함수가 무엇을 소비하는지 추적한다.
각 노드는 독립 함수가 아니라 전제 조건을 생산하고 소비하는 연결점이다. 코드를 읽을 때는 노드 사이에서 어떤 필드가 바뀌는지 표시한다.
1. void __init paging_init
void __init paging_init 주변에서는 swapper_pg_dir를 중심으로 본다. 이 필드는 kernel page table root 역할을 하므로, 함수가 끝날 때 early mapping exists 상태가 실제로 성립했는지 확인해야 한다.
원본 코드에서 볼 순서는 입력 범위 검증, 중심 필드 갱신, 다른 계층에 보이는 publish 지점, 실패 시 되돌림 순서다. 이 네 칸이 맞아야 다음 함수가 head.S/idmap 기반 최소 mapping을 전제로 삼을 수 있다.
자주 틀리는 해석: page table entry 권한을 linker section 권한과 분리해 봄
2. map_mem
map_mem 주변에서는 memblock.memory를 중심으로 본다. 이 필드는 map 대상 physical range 역할을 하므로, 함수가 끝날 때 kernel mapping built 상태가 실제로 성립했는지 확인해야 한다.
원본 코드에서 볼 순서는 입력 범위 검증, 중심 필드 갱신, 다른 계층에 보이는 publish 지점, 실패 시 되돌림 순서다. 이 네 칸이 맞아야 다음 함수가 text/data/rodata 권한 반영을 전제로 삼을 수 있다.
자주 틀리는 해석: TLB invalidate 없이 mapping 변경을 읽음
| 함수 | 입력 | 상태 변경 | 검증 질문 |
|---|---|---|---|
| void __init paging_init | swapper_pg_dir, caller context, subsystem 전제 조건 | early mapping exists: head.S/idmap 기반 최소 mapping | page table entry 권한을 linker section 권한과 분리해 봄 문제를 코드상 어느 조건문 또는 error label에서 분리하는가 |
| map_mem | memblock.memory, caller context, subsystem 전제 조건 | kernel mapping built: text/data/rodata 권한 반영 | TLB invalidate 없이 mapping 변경을 읽음 문제를 코드상 어느 조건문 또는 error label에서 분리하는가 |
구조체 / 필드 해설
여기서는 “어떤 구조체가 있다”가 아니라 그 필드가 어느 단계에서 쓰기 가능하고 어느 단계부터 관찰 가능한지를 본다. 박사급 리뷰에서는 필드의 뜻보다 보호 규칙이 먼저다.
필드는 구조체 안에 흩어져 있지만, 실제 실행에서는 위 순서로 의미가 이어진다.
| 필드 | 읽는 법 | 확인 |
|---|---|---|
| swapper_pg_dir | kernel page table root | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
| memblock.memory | map 대상 physical range | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
| fixmap | 초기 임시 mapping | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
| linear map | phys_to_virt 전제 | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
상태 전이 그림
상태 전이를 따로 그리는 이유는 정상 path와 실패 path를 같은 표에서 보기 위해서다. 커널 regression은 흔히 마지막 상태가 아니라 중간 상태를 외부에 publish한 뒤 rollback하지 못해서 생긴다.
상태 전이는 단방향처럼 그렸지만, 실패 경로에서는 대부분 역순 rollback이 붙는다.
| 상태 | 의미 | 진입 조건 | 깨지는 지점 |
|---|---|---|---|
| early mapping exists | head.S/idmap 기반 최소 mapping | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
| kernel mapping built | text/data/rodata 권한 반영 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
| linear map built | RAM 직접 mapping 가능 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
| TLB synchronized | 새 page table visibility 확보 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
불변조건 / 실패 케이스
paging_init 의 중심 객체가 publish된 뒤에는 마지막 참조가 사라지기 전까지 teardown path가 모든 callback, timer, IRQ, worker와 경합하지 않아야 한다.
상태 필드를 바꾼 뒤 다른 CPU나 하위 계층이 관찰할 수 있다면 lock, barrier, refcount, RCU 중 어느 장치가 visibility를 보장하는지 확인한다.
중간 단계 실패는 성공 단계의 역순으로 되돌아가야 한다. goto label이 많은 코드는 label 이름보다 어느 resource가 이미 획득됐는지를 표로 적는다.
embedded bring-up에서는 panic보다 silence, timeout, deferred probe, interrupt flood처럼 간접 증상으로 드러나는 경우가 많다.
page table entry 권한을 linker section 권한과 분리해 봄
TLB invalidate 없이 mapping 변경을 읽음
fixmap과 linear map lifetime을 섞음
계측 / 검증
계측은 printk 위치 경쟁이 아니라 가설 검증이다. 먼저 위 상태표에서 멈춘 state를 정하고, 그 state를 바꾸는 함수와 그 결과를 소비하는 함수를 동시에 본다.
| 도구 | 보는 것 | 해석 |
|---|---|---|
| ptdump_debugfs | paging_init 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
| earlycon | paging_init 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
| ftrace:create_mapping | paging_init 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
| cat /sys/kernel/debug/kernel_page_tables | paging_init 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
# 예시: tracefs가 켜진 보드에서 토픽별 event를 좁혀 본다.
mount -t tracefs nodev /sys/kernel/tracing
echo function_graph > /sys/kernel/tracing/current_tracer
echo ':mod:*' > /sys/kernel/tracing/set_ftrace_filter
cat /sys/kernel/tracing/trace_pipe
연구 질문
- paging_init 의 state machine을 네 단계로 줄였을 때, 실제 코드에서 빠지는 intermediate state는 무엇인가?
- paging_init 의 fast path가 생략한 검사는 어느 init path 또는 slow path에서 보증되는가?
- 실험으로 확인한다면 'page table entry 권한을 linker section 권한과 분리해 봄' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?
- 실험으로 확인한다면 'TLB invalidate 없이 mapping 변경을 읽음' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?