읽는 지도
VFS/filesystem 코드는 path name을 dentry/inode/file/page cache operation으로 번역하는 계층이다. 이름 해석의 consistency와 데이터 I/O의 consistency를 분리해서 봐야 한다.
함수 이름보다 입력 객체와 출력 객체를 먼저 본다. 이 토픽에서 어떤 구조체가 생성, 연결, publish, retire되는지 표시한다.
정상 경로와 실패 경로를 같은 무게로 본다. kernel code의 품질은 성공 path보다 error unwind, hotplug, teardown에서 더 잘 드러난다.
왼쪽은 입력 또는 상위 계층이고, 오른쪽으로 갈수록 실제 상태 변경이 커진다. 문C식으로 읽을 때는 이 그림을 먼저 머리에 놓고 코드 조각을 끼워 넣는다.
원본 코드 좌표
Linux 6.18.37 LTS에서 열 파일
Linux 6.18.37 LTS: fs/dcache.c
Linux 6.18.37 LTS: fs/inode.c
Linux 6.18.37 LTS: fs/file_table.c
Linux 6.18.37 LTS: fs/namespace.c
설명: 첫 파일은 보통 진입 함수가 있는 곳이고, 나머지는 구조체 정의, architecture glue, callback 구현을 확인할 때 같이 연다. 파일을 여러 개 놓고 봐야 이 토픽의 boundary가 보인다.
GitHub 원본:
대표 코드
void __init vfs_caches_init
/* SLAB cache for __getname() consumers */
struct kmem_cache *names_cachep __read_mostly;
EXPORT_SYMBOL(names_cachep);
void __init vfs_caches_init_early(void)
{
int i;
for (i = 0; i < ARRAY_SIZE(in_lookup_hashtable); i++)
INIT_HLIST_BL_HEAD(&in_lookup_hashtable[i]);
dcache_init_early();
inode_init_early();
}
void __init vfs_caches_init(void)
{
names_cachep = kmem_cache_create_usercopy("names_cache", PATH_MAX, 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC, 0, PATH_MAX, NULL);
dcache_init();
inode_init();
files_init();
files_maxfiles_init();
mnt_init();
bdev_cache_init();
chrdev_init();
}
읽는 법: 이 절편에서 먼저 볼 것은 return 값이 아니라 상태 변경이다. 어떤 lock을 잡은 뒤 어떤 필드를 바꾸는지, 실패하면 어느 label로 빠지는지, 그리고 바뀐 상태를 다음 호출자가 어떤 전제로 소비하는지 표시한다.
dcache_init
dhash_entries = simple_strtoul(str, &str, 0);
return 1;
}
__setup("dhash_entries=", set_dhash_entries);
static void __init dcache_init_early(void)
{
/* If hashes are distributed across NUMA nodes, defer
* hash allocation until vmalloc space is available.
*/
if (hashdist)
return;
dentry_hashtable =
alloc_large_system_hash("Dentry cache",
sizeof(struct hlist_bl_head),
dhash_entries,
13,
HASH_EARLY | HASH_ZERO,
&d_hash_shift,
NULL,
0,
0);
d_hash_shift = 32 - d_hash_shift;
}
static void __init dcache_init(void)
{
/*
* A constructor could be added for stable state like the lists,
* but it is probably not worth it because of the cache nature
* of the dcache.
*/
dentry_cache = KMEM_CACHE_USERCOPY(dentry,
SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|SLAB_MEM_SPREAD|SLAB_ACCOUNT,
d_iname);
/* Hash may have been set up in dcache_init_early */
if (!hashdist)
return;
읽는 법: 이 절편에서 먼저 볼 것은 return 값이 아니라 상태 변경이다. 어떤 lock을 잡은 뒤 어떤 필드를 바꾸는지, 실패하면 어느 label로 빠지는지, 그리고 바뀐 상태를 다음 호출자가 어떤 전제로 소비하는지 표시한다.
함수별 독해
VFS가 path lookup과 open file state를 빠르게 처리할 수 있도록 dentry/inode/file cache를 초기화한다.
이 섹션은 원본 코드 발췌를 함수 이름 단위로 끊어, 각 함수가 어떤 전제 조건을 만들고 다음 함수가 무엇을 소비하는지 추적한다.
각 노드는 독립 함수가 아니라 전제 조건을 생산하고 소비하는 연결점이다. 코드를 읽을 때는 노드 사이에서 어떤 필드가 바뀌는지 표시한다.
1. void __init vfs_caches_init
void __init vfs_caches_init 주변에서는 dcache를 중심으로 본다. 이 필드는 pathname component cache 역할을 하므로, 함수가 끝날 때 cache created 상태가 실제로 성립했는지 확인해야 한다.
원본 코드에서 볼 순서는 입력 범위 검증, 중심 필드 갱신, 다른 계층에 보이는 publish 지점, 실패 시 되돌림 순서다. 이 네 칸이 맞아야 다음 함수가 slab 기반 object 확보을 전제로 삼을 수 있다.
자주 틀리는 해석: dentry와 inode identity를 혼동함
2. dcache_init
dcache_init 주변에서는 dcache를 중심으로 본다. 이 필드는 pathname component cache 역할을 하므로, 함수가 끝날 때 hash tables sized 상태가 실제로 성립했는지 확인해야 한다.
원본 코드에서 볼 순서는 입력 범위 검증, 중심 필드 갱신, 다른 계층에 보이는 publish 지점, 실패 시 되돌림 순서다. 이 네 칸이 맞아야 다음 함수가 lookup table 준비을 전제로 삼을 수 있다.
자주 틀리는 해석: cache init을 filesystem mount와 같은 단계로 봄
| 함수 | 입력 | 상태 변경 | 검증 질문 |
|---|---|---|---|
| void __init vfs_caches_init | dcache, caller context, subsystem 전제 조건 | cache created: slab 기반 object 확보 | dentry와 inode identity를 혼동함 문제를 코드상 어느 조건문 또는 error label에서 분리하는가 |
| dcache_init | dcache, caller context, subsystem 전제 조건 | hash tables sized: lookup table 준비 | cache init을 filesystem mount와 같은 단계로 봄 문제를 코드상 어느 조건문 또는 error label에서 분리하는가 |
구조체 / 필드 해설
여기서는 “어떤 구조체가 있다”가 아니라 그 필드가 어느 단계에서 쓰기 가능하고 어느 단계부터 관찰 가능한지를 본다. 박사급 리뷰에서는 필드의 뜻보다 보호 규칙이 먼저다.
필드는 구조체 안에 흩어져 있지만, 실제 실행에서는 위 순서로 의미가 이어진다.
| 필드 | 읽는 법 | 확인 |
|---|---|---|
| dcache | pathname component cache | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
| inode cache | filesystem object metadata | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
| file cache | open instance allocation | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
| mount cache | namespace traversal state | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
상태 전이 그림
상태 전이를 따로 그리는 이유는 정상 path와 실패 path를 같은 표에서 보기 위해서다. 커널 regression은 흔히 마지막 상태가 아니라 중간 상태를 외부에 publish한 뒤 rollback하지 못해서 생긴다.
상태 전이는 단방향처럼 그렸지만, 실패 경로에서는 대부분 역순 rollback이 붙는다.
| 상태 | 의미 | 진입 조건 | 깨지는 지점 |
|---|---|---|---|
| cache created | slab 기반 object 확보 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
| hash tables sized | lookup table 준비 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
| mount subsystem ready | namespace root 구성 가능 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
| proc/sysfs users | 후속 init이 사용 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
불변조건 / 실패 케이스
vfs_caches_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처럼 간접 증상으로 드러나는 경우가 많다.
dentry와 inode identity를 혼동함
cache init을 filesystem mount와 같은 단계로 봄
negative dentry를 실패만으로 해석함
계측 / 검증
계측은 printk 위치 경쟁이 아니라 가설 검증이다. 먼저 위 상태표에서 멈춘 state를 정하고, 그 state를 바꾸는 함수와 그 결과를 소비하는 함수를 동시에 본다.
| 도구 | 보는 것 | 해석 |
|---|---|---|
| slabinfo | vfs_caches_init 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
| vfs_* tracepoints | vfs_caches_init 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
| perf probe d_alloc | vfs_caches_init 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
| cat /proc/slabinfo | vfs_caches_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
연구 질문
- vfs_caches_init 의 state machine을 네 단계로 줄였을 때, 실제 코드에서 빠지는 intermediate state는 무엇인가?
- vfs_caches_init 의 fast path가 생략한 검사는 어느 init path 또는 slow path에서 보증되는가?
- 실험으로 확인한다면 'dentry와 inode identity를 혼동함' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?
- 실험으로 확인한다면 'cache init을 filesystem mount와 같은 단계로 봄' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?