슬랩 캐시가 메모리를 얼마나 쓰는지 확인할 때 보통 /proc/slabinfo를 먼저 연다. 하지만 slabinfo에는 캐시별 오브젝트 수와 슬랩 수만 있어서, 이 캐시가 다른 캐시와 병합된 것인지, 슬랩 한 장이 몇 페이지인지, per-cpu partial 리스트에 몇 장이 물려 있는지는 볼 수 없다. SLUB은 그 내부 상태를 캐시 단위로 /sys/kernel/slab/<cache>/에 노출하고, 규격은 커널 소스의 Documentation/ABI/testing/sysfs-kernel-slab에 정의돼 있다.
문제는 이 문서가 2007년 SLUB 도입 시점에 작성된 뒤 부분적으로만 갱신돼, 지금 커널에서 사라졌거나 읽기/쓰기 속성이 뒤집힌 항목이 그대로 남아 있다는 점이다. 이 글에서는 Ubuntu 24.04(6.8.0-137-generic)와 QEMU에 올린 6.12 커널에서 값을 직접 찍어 속성별 의미, 캐시 병합 규칙, 문서와 실제의 차이를 정리한다.
캐시 하나의 상태 전부 찍어보기
#!/bin/bash
c=/sys/kernel/slab/dentry
echo "=== dentry 캐시 sysfs 전체 ==="
for f in $(ls $c); do
case $f in shrink|validate) continue;; esac
printf "%-24s %s\n" "$f" "$(cat $c/$f 2>/dev/null | head -1 | cut -c1-200)"
done
echo
echo "=== /proc/slabinfo (dentry) ==="
head -2 /proc/slabinfo
grep -E "^dentry " /proc/slabinfoshrink와 validate는 쓰기 전용이라 읽기 루프에서 뺐다.
=== dentry 캐시 sysfs 전체 ===
aliases 0
align 8
cache_dma 0
cpu_partial 120
cpu_slabs 36 N0=36
ctor
destroy_by_rcu 0
hwcache_align 0
min_partial 5
objects 24572 N0=24572
object_size 192
objects_partial 233 N0=233
objs_per_slab 21
order 0
partial 12 N0=12
poison 0
reclaim_account 1
red_zone 0
remote_node_defrag_ratio 100
sanity_checks 0
skip_kfence 0
slabs 1171 N0=1171
slabs_cpu_partial 315(30) C0=73(7) C1=52(5) C2=52(5) C3=21(2) C4=52(5) C5=63(6)
slab_size 192
store_user 0
total_objects 24591 N0=24591
trace 0
usersize 40
=== /proc/slabinfo (dentry) ===
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
dentry 24572 24591 192 21 1 : tunables 0 0 0 : slabdata 1171 1171 0
sysfs의 objects / total_objects / slabs가 slabinfo의 active_objs / num_objs / active_slabs와 정확히 같은 값이다. slabinfo에 없는 정보는 그 아래 partial, cpu_slabs, slabs_cpu_partial처럼 리스트별로 쪼갠 수치와 order, usersize 같은 캐시 속성 쪽이다.
| 속성 | 의미 | R/W |
|---|---|---|
aliases | 이 캐시로 병합된 다른 캐시 수 (refcount - 1) | R |
align | 오브젝트 정렬 경계(byte) | R |
cache_dma | ZONE_DMA에서 할당되는 캐시인지 | R |
cpu_partial | per-cpu partial 리스트에 유지할 오브젝트 수 | RW |
cpu_slabs | 현재 per-cpu 슬랩 수(노드별) | R |
ctor | 생성자 함수 심볼(없으면 공백) | R |
destroy_by_rcu | SLAB_TYPESAFE_BY_RCU 여부 | R |
hwcache_align | 캐시라인 정렬 요청 여부 | R |
min_partial | 노드 partial 리스트에 남겨둘 빈 슬랩 최소 개수 | RW |
objects | 사용 중인 오브젝트 수 | R |
object_size | 메타데이터 제외 오브젝트 크기 | R |
objects_partial | partial 슬랩에 들어 있는 오브젝트 수 | R |
objs_per_slab | 슬랩 한 장에 들어가는 오브젝트 수 | R |
order | 슬랩 한 장의 페이지 order | R |
partial | 노드 partial 리스트의 슬랩 수 | R |
poison | 포이즌 패턴 기록 여부(slub_debug=P) | R |
reclaim_account | SLAB_RECLAIM_ACCOUNT 여부(SReclaimable로 집계) | R |
red_zone | 레드존 여부(slub_debug=Z) | R |
remote_node_defrag_ratio | 원격 노드 partial 슬랩을 가져올 비율(%), NUMA 전용 | RW |
sanity_checks | 해제 시 무결성 검사 여부(slub_debug=F) | R |
shrink | 빈 슬랩 회수 + partial 리스트 정렬 (쓰기 전용, 1만 허용) | W |
skip_kfence | KFENCE 샘플링 제외 여부(ABI 문서에 없음) | RW |
slabs | 이 캐시의 전체 슬랩 수 | R |
slabs_cpu_partial | per-cpu partial 슬랩 수, objects(slabs) 형식 | R |
slab_size | 메타데이터/정렬 포함 실제 오브젝트 크기 | R |
store_user | 할당·해제 호출자 기록 여부(slub_debug=U) | R |
total_objects | 확보된 슬랩 전체 수용량 | R |
trace | 할당·해제마다 커널 로그를 남길지 | R |
usersize | usercopy로 노출이 허용된 영역 크기 | R |
validate | 캐시 무결성 검증 실행(쓰기 전용) | W |
R/W 구분은 파일 권한(-r-------- vs -rw-------)과 mm/slub.c의 SLAB_ATTR_RO() / SLAB_ATTR() 매크로 양쪽에서 확인한 값이다.
ABI 문서와 실제 커널의 차이
#!/bin/bash
doc=~/kbuild/linux-6.12/Documentation/ABI/testing/sysfs-kernel-slab
grep "^What:" $doc | sed "s#.*/sys/kernel/slab/<cache>/##" | grep -v "^What" | sort > /tmp/doc.txt
ls /sys/kernel/slab/dentry | sort > /tmp/real.txt
echo "문서 기재: $(wc -l < /tmp/doc.txt)개 / 실제 존재: $(wc -l < /tmp/real.txt)개"
echo "--- 문서에 있으나 없는 속성 ---"
comm -23 /tmp/doc.txt /tmp/real.txt | tr "\n" " "; echo
echo "--- 실제로만 있는 속성 ---"
comm -13 /tmp/doc.txt /tmp/real.txt | tr "\n" " "; echo문서 기재: 49개 / 실제 존재: 30개
--- 문서에 있으나 없는 속성 ---
alloc_calls alloc_fastpath alloc_from_partial alloc_refill alloc_slab alloc_slowpath cpuslab_flush deactivate_empty deactivate_full deactivate_remote_frees deactivate_to_head deactivate_to_tail free_add_partial free_calls free_fastpath free_frozen free_remove_partial free_slab free_slowpath order_fallback
--- 실제로만 있는 속성 ---
skip_kfence
사라진 20개 중 18개는 CONFIG_SLUB_STATS가 필요한 통계 속성이고(우분투 커널은 이 옵션이 꺼져 있다), 나머지 alloc_calls / free_calls 두 개는 아예 다른 곳으로 옮겨갔다.
| 문서 기재 | 6.8/6.12 실제 | 확인 방법 |
|---|---|---|
order는 writable | -r-------- (SLAB_ATTR_RO) | 파일 권한 + mm/slub.c |
cpu_partial은 read-only | -rw------- (SLAB_ATTR) | 값을 써서 동작 확인 |
alloc_calls / free_calls | debugfs의 alloc_traces / free_traces로 이동 | mm/slub.c에 alloc_calls 없음 |
skip_kfence 없음 | 존재(RW) | /sys/kernel/slab/<cache>/ 디렉터리 목록 |
옮겨간 alloc_traces는 slub_debug=U로 부팅해 store_user가 켜진 캐시에서만 만들어진다. QEMU에 6.12 커널을 올려 확인했다.
qemu-system-x86_64 -machine q35 -cpu max \
-kernel ~/kbuild/build-gcc/arch/x86/boot/bzImage \
-initrd ~/kbuild/ir-trace.cpio.gz \
-append "console=ttyS0 no_timer_check slub_debug=U" \
-nographic -m 2048 -smp 2 -no-reboot===MARK debugfs
caches_with_debugfs=160
===MARK files
alloc_traces free_traces
===MARK alloc_traces
160 __d_alloc+0x24/0x1d0 age=5851/5855/5860 pid=1 cpus=1
__d_alloc+0x24/0x1d0
d_alloc+0x14/0x80
d_alloc_parallel+0x54/0x3d0
__lookup_slow+0x58/0x130
lookup_one_len+0x93/0xa0
start_creating+0xa0/0x160
__debugfs_create_file+0x3b/0x1f0
debugfs_slab_add+0x3b/0x60
slab_debugfs_init+0x4d/0x70
do_one_initcall+0x56/0x220
kernel_init_freeable+0x19e/0x2d0
dentry 캐시 최상위 할당자가 debugfs_slab_add, 즉 지금 읽고 있는 debugfs 엔트리 160개를 만드느라 소비한 dentry다. 슬랩 관측 인터페이스 자체가 슬랩을 쓴다는 점은 뒤에서 수치로 다시 확인한다.
캐시 병합과 콜론 이름 규칙
우분투 24.04의 /sys/kernel/slab에는 엔트리 539개가 있는데 그중 162개는 디렉터리가 아니라 :0000192 같은 이름으로 걸린 심볼릭 링크다. 크기와 플래그가 같은 캐시들을 SLUB이 하나로 병합한 결과다.
#!/bin/bash
echo "=== dentry / kmalloc-192 는 실제 디렉터리인가 심볼릭 링크인가 ==="
for n in dentry kmalloc-192 aio_kiocb anon_vma_chain :0000192; do
printf "%-16s " "$n"
if [ -L "/sys/kernel/slab/$n" ]; then echo "symlink -> $(readlink /sys/kernel/slab/$n)"; else echo "directory"; fi
done
echo
echo "=== 병합 그룹: :0000192 에 걸린 이름들 ==="
for l in /sys/kernel/slab/*; do
[ -L "$l" ] || continue
[ "$(readlink $l)" = ":0000192" ] && basename $l
done
echo "aliases(:0000192) = $(cat '/sys/kernel/slab/:0000192/aliases')"=== dentry / kmalloc-192 는 실제 디렉터리인가 심볼릭 링크인가 ===
dentry directory
kmalloc-192 directory
aio_kiocb symlink -> :0000192
anon_vma_chain symlink -> :A-0000064
:0000192 directory
=== 병합 그룹: :0000192 에 걸린 이름들 ===
aio_kiocb
bio_integrity_payload
dmaengine-unmap-16
ecryptfs_sb_cache
inet_peer_cache
ip6_mrt_cache
ip_dst_cache
ip_mrt_cache
skbuff_ext_cache
uid_cache
virtio_scsi_cmd
aliases(:0000192) = 10
이름은 11개인데 aliases는 10이다. aliases_show()가 s->refcount - 1을 그대로 돌려주기 때문에, 처음 캐시를 만든 하나는 세지 않는다.
디렉터리 이름 :A-0000064의 숫자는 slab_size(메타데이터 포함 크기)이고, 그 앞의 문자는 병합 판정에 쓰인 플래그다.
*p++ = ':';
if (s->flags & SLAB_CACHE_DMA)
*p++ = 'd';
if (s->flags & SLAB_CACHE_DMA32)
*p++ = 'D';
if (s->flags & SLAB_RECLAIM_ACCOUNT)
*p++ = 'a';
if (s->flags & SLAB_CONSISTENCY_CHECKS)
*p++ = 'F';
if (s->flags & SLAB_ACCOUNT)
*p++ = 'A';
if (p != name + 1)
*p++ = '-';
p += snprintf(p, ID_STR_LENGTH - (p - name), "%07u", s->size);SLAB_ACCOUNT는 sysfs에 별도 파일이 없으므로, 그 캐시가 cgroup 메모리로 계상되는지는 이름 앞 A 문자로만 알 수 있다.
병합을 끄면 어떻게 달라지는지 같은 커널(6.12, defconfig)을 slab_nomerge만 바꿔 두 번 부팅해 비교했다.
| 부팅 옵션 | sysfs 엔트리 | 실제 캐시 | 심볼릭 링크 | sysfs 합계 | meminfo Slab |
|---|---|---|---|---|---|
| 기본 | 305 | 115 | 190 | 6,940 kB | 9,592 kB |
slab_nomerge | 229 | 229 | 0 | 8,092 kB | 9,868 kB |
캐시 229개가 병합되면 실제 캐시 115개로 줄고 슬랩은 1.1MB 덜 쓴다. 대신 병합 그룹에 속한 캐시들은 cpu_partial 같은 튜닝 값과 통계를 모두 공유한다.
sysfs 값으로 슬랩 사용량 합산하기
#!/bin/bash
total=0
for d in /sys/kernel/slab/*; do
[ -L "$d" ] && continue # 심볼릭 링크는 중복 집계되므로 제외
s=$(cat "$d/slabs" 2>/dev/null | cut -d' ' -f1) || continue
o=$(cat "$d/order" 2>/dev/null) || continue
total=$(( total + s * (4096 << o) ))
done
echo "sysfs 합계 : $(( total / 1024 )) kB"
awk 'NR>2 {sum += $(NF-2) * $6 * 4096} END {printf "slabinfo 합계 : %d kB\n", sum/1024}' /proc/slabinfo
grep -E "^Slab:|^SReclaimable:|^SUnreclaim:" /proc/meminfoslabs가 세는 것은 바이트가 아니라 슬랩 장수이므로, order를 반영해야 실제 크기가 된다.
sysfs 합계 : 84568 kB
slabinfo 합계 : 85000 kB
Slab: 106344 kB
SReclaimable: 39272 kB
SUnreclaim: 67072 kB
sysfs 합계와 slabinfo 합계는 0.5% 차이로 사실상 같지만(합산하는 동안에도 값이 움직인다), /proc/meminfo의 Slab보다는 20MB 넘게 작다. 그 격차의 상당 부분은 합산 작업 자체가 만든 것이다.
#!/bin/bash
snap() {
printf "%-14s Slab=%s kB dentry_slabs=%s inode_cache_slabs=%s\n" "$1" \
"$(awk '/^Slab:/{print $2}' /proc/meminfo)" \
"$(cut -d' ' -f1 /sys/kernel/slab/dentry/slabs)" \
"$(cut -d' ' -f1 /sys/kernel/slab/inode_cache/slabs)"
}
echo 2 > /proc/sys/vm/drop_caches; sleep 1
snap "[drop 직후]"
n=0
for d in /sys/kernel/slab/*; do
[ -L "$d" ] && continue
cat "$d"/* > /dev/null 2>&1
n=$((n+1))
done
snap "[전체 sweep 후]"
echo " 읽은 캐시 수: $n"
echo 2 > /proc/sys/vm/drop_caches; sleep 1
snap "[다시 drop]"[drop 직후] Slab=89088 kB dentry_slabs=791 inode_cache_slabs=208
[전체 sweep 후] Slab=95864 kB dentry_slabs=871 inode_cache_slabs=615
읽은 캐시 수: 377
[다시 drop] Slab=89720 kB dentry_slabs=820 inode_cache_slabs=239
캐시 377개의 파일을 한 번 훑었을 뿐인데 Slab이 6.8MB 늘었다. sysfs 파일마다 dentry와 inode가 실체화되기 때문이며(inode_cache 208→615장), drop_caches로 되돌아간다.
쓰기 가능한 속성 다루기
shrink — 회수되는 것과 안 되는 것
#!/bin/bash
snap() { printf "%-16s Slab=%s kB dentry: objects=%s total_objects=%s slabs=%s cpu_slabs=%s\n" "$1" \
"$(awk '/^Slab:/{print $2}' /proc/meminfo)" \
"$(cut -d' ' -f1 /sys/kernel/slab/dentry/objects)" \
"$(cut -d' ' -f1 /sys/kernel/slab/dentry/total_objects)" \
"$(cut -d' ' -f1 /sys/kernel/slab/dentry/slabs)" \
"$(cut -d' ' -f1 /sys/kernel/slab/dentry/cpu_slabs)"; }
echo 2 > /proc/sys/vm/drop_caches; sleep 1
find /usr /etc /var -xdev > /dev/null 2>&1
snap "[find 직후]"
for c in /sys/kernel/slab/*/shrink; do echo 1 > $c 2>/dev/null; done
snap "[전체 shrink 후]"
echo 2 > /proc/sys/vm/drop_caches; sleep 1
snap "[drop_caches 후]"
for c in /sys/kernel/slab/*/shrink; do echo 1 > $c 2>/dev/null; done
snap "[다시 shrink]"[find 직후] Slab=105152 kB dentry: objects=21294 total_objects=21294 slabs=1014 cpu_slabs=32
[전체 shrink 후] Slab=98716 kB dentry: objects=21983 total_objects=22092 slabs=1052 cpu_slabs=16
[drop_caches 후] Slab=81744 kB dentry: objects=7637 total_objects=16275 slabs=775 cpu_slabs=30
[다시 shrink] Slab=81296 kB dentry: objects=7211 total_objects=16128 slabs=768 cpu_slabs=15
전체 캐시 shrink는 6.4MB를 회수했지만 살아 있는 dentry는 그대로 21,983개다. 사용 중인 오브젝트를 버리는 일은 drop_caches의 몫이고(17MB 추가 회수), shrink는 빈 슬랩과 per-cpu 슬랩 정리까지만 한다.
cpu_partial — 값을 바꿔 확인하기
C=/sys/kernel/slab/dentry
show() { printf "%-16s cpu_partial=%-4s cpu_slabs=%-5s slabs_cpu_partial=%s\n" "$1" \
"$(cat $C/cpu_partial)" "$(cut -d' ' -f1 $C/cpu_slabs)" "$(cut -d' ' -f1 $C/slabs_cpu_partial)"; }
orig=$(cat $C/cpu_partial)
find /usr -xdev > /dev/null 2>&1; show "[기본값]"
echo 0 > $C/cpu_partial
find /usr -xdev > /dev/null 2>&1; echo 2 > /proc/sys/vm/drop_caches; sleep 1; show "[cpu_partial=0]"
echo $orig > $C/cpu_partial
find /usr -xdev > /dev/null 2>&1; show "[원복]"[기본값] cpu_partial=120 cpu_slabs=28 slabs_cpu_partial=231(22)
[cpu_partial=0] cpu_partial=0 cpu_slabs=10 slabs_cpu_partial=63(6)
[원복] cpu_partial=120 cpu_slabs=17 slabs_cpu_partial=147(14)
0으로 두어도 완전히 비지는 않는다. put_cpu_partial()는 리스트가 비어 있을 때는 검사 없이 한 장을 붙이고 다음 번에야 cpu_partial_slabs와 비교해 반납하므로, CPU당 최대 한 장은 남는다(6코어에서 6장 관측).
validate — 디버그가 켜진 캐시에서만
# echo 1 > /sys/kernel/slab/dentry/validate; echo exit=$?
bash: line 1: echo: write error: Invalid argument
exit=1
static ssize_t validate_store(struct kmem_cache *s,
const char *buf, size_t length)
{
int ret = -EINVAL;
if (buf[0] == '1' && kmem_cache_debug(s)) {
ret = validate_slab_cache(s);
if (ret >= 0)
ret = length;
}
return ret;
}kmem_cache_debug()가 아닌 캐시에는 무조건 -EINVAL을 돌려준다. slub_debug=FZPU로 부팅한 쪽과 나란히 비교하면 디버그 활성화의 대가도 함께 보인다.
| 항목 | 기본 부팅 | slub_debug=FZPU |
|---|---|---|
dentry slab_size | 192 | 288 |
dentry objs_per_slab | 21 | 14 |
red_zone / poison / store_user | 0 / 0 / 0 | 1 / 1 / 1 |
validate 종료 코드 | 1 (EINVAL) | 0 |
| 캐시 병합 | 115개로 병합 | 병합 없음(229개) |
| 부팅 직후 Slab | 8,024 kB | 15,716 kB |
오브젝트마다 레드존과 호출자 기록이 붙어 슬랩당 오브젝트가 21개에서 14개로 줄고, 부팅 직후 슬랩 사용량은 두 배 가까이 뛴다.
주의사항
| 항목 | 내용 |
|---|---|
| 권한 | /sys/kernel/slab/<cache>/ 아래 파일은 읽기 전용이 0400, 쓰기 가능한 것이 0600이라 전부 root 전용이다. 일반 사용자로 수집 스크립트를 돌리면 빈 값이 아니라 Permission denied가 난다. |
| 관측 부작용 | 전체 캐시를 순회해 읽는 것만으로 슬랩이 수 MB 늘어난다. 빌드 A/B 비교처럼 수치를 맞대볼 때는 양쪽에 완전히 동일한 수집 스크립트를 써야 한다. |
slabs_cpu_partial | 괄호 앞의 오브젝트 수는 slabs_cpu_partial_show()에서 슬랩이 절반 찼다고 가정하고 계산한 추정치다. 정확한 값이 필요하면 슬랩 수(괄호 안)만 쓴다. |
| 병합된 캐시 튜닝 | 심볼릭 링크로 걸린 캐시에 값을 쓰면 병합 그룹 전체가 영향을 받는다. 특정 캐시만 조정하려면 slab_nomerge로 부팅해야 한다. |
| 디버그 속성 | red_zone, poison, store_user, sanity_checks는 읽기 전용이라 런타임에 켤 수 없다. slub_debug 부팅 파라미터가 유일한 경로다. |
shrink 비용 | 노드 리스트 락을 잡고 partial 리스트를 정렬한다. 운영 중인 시스템에서 전체 캐시를 대상으로 반복 실행하지 말 것. |
| ABI 문서 | order / cpu_partial의 R/W 표기, alloc_calls 항목은 현재 커널과 다르다. 판단이 필요하면 mm/slub.c의 SLAB_ATTR / SLAB_ATTR_RO 매크로를 직접 확인하는 편이 빠르다. |
마무리
/sys/kernel/slab은 slabinfo보다 한 단계 아래를 보여준다. 캐시가 병합된 결과인지, 슬랩 한 장이 몇 페이지이고 몇 개가 per-cpu에 물려 있는지, 디버그 옵션이 켜져 있는지가 전부 파일 하나씩으로 드러난다. 다만 규격 문서는 20년 가까이 누적된 상태라 그대로 믿으면 안 되고, 값을 하나 바꿔보거나 mm/slub.c를 열어 확인하는 편이 확실하다.