/sys/kernel/slab으로 SLUB 캐시 상태 들여다보기

슬랩 캐시가 메모리를 얼마나 쓰는지 확인할 때 보통 /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/slabinfo

shrinkvalidate는 쓰기 전용이라 읽기 루프에서 뺐다.

=== 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_dmaZONE_DMA에서 할당되는 캐시인지R
cpu_partialper-cpu partial 리스트에 유지할 오브젝트 수RW
cpu_slabs현재 per-cpu 슬랩 수(노드별)R
ctor생성자 함수 심볼(없으면 공백)R
destroy_by_rcuSLAB_TYPESAFE_BY_RCU 여부R
hwcache_align캐시라인 정렬 요청 여부R
min_partial노드 partial 리스트에 남겨둘 빈 슬랩 최소 개수RW
objects사용 중인 오브젝트 수R
object_size메타데이터 제외 오브젝트 크기R
objects_partialpartial 슬랩에 들어 있는 오브젝트 수R
objs_per_slab슬랩 한 장에 들어가는 오브젝트 수R
order슬랩 한 장의 페이지 orderR
partial노드 partial 리스트의 슬랩 수R
poison포이즌 패턴 기록 여부(slub_debug=P)R
reclaim_accountSLAB_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_kfenceKFENCE 샘플링 제외 여부(ABI 문서에 없음)RW
slabs이 캐시의 전체 슬랩 수R
slabs_cpu_partialper-cpu partial 슬랩 수, objects(slabs) 형식R
slab_size메타데이터/정렬 포함 실제 오브젝트 크기R
store_user할당·해제 호출자 기록 여부(slub_debug=U)R
total_objects확보된 슬랩 전체 수용량R
trace할당·해제마다 커널 로그를 남길지R
usersizeusercopy로 노출이 허용된 영역 크기R
validate캐시 무결성 검증 실행(쓰기 전용)W

R/W 구분은 파일 권한(-r-------- vs -rw-------)과 mm/slub.cSLAB_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_callsdebugfs의 alloc_traces / free_traces로 이동mm/slub.calloc_calls 없음
skip_kfence 없음존재(RW)/sys/kernel/slab/<cache>/ 디렉터리 목록

옮겨간 alloc_tracesslub_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
기본3051151906,940 kB9,592 kB
slab_nomerge22922908,092 kB9,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/meminfo

slabs가 세는 것은 바이트가 아니라 슬랩 장수이므로, 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_size192288
dentry objs_per_slab2114
red_zone / poison / store_user0 / 0 / 01 / 1 / 1
validate 종료 코드1 (EINVAL)0
캐시 병합115개로 병합병합 없음(229개)
부팅 직후 Slab8,024 kB15,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.cSLAB_ATTR / SLAB_ATTR_RO 매크로를 직접 확인하는 편이 빠르다.

마무리

/sys/kernel/slab은 slabinfo보다 한 단계 아래를 보여준다. 캐시가 병합된 결과인지, 슬랩 한 장이 몇 페이지이고 몇 개가 per-cpu에 물려 있는지, 디버그 옵션이 켜져 있는지가 전부 파일 하나씩으로 드러난다. 다만 규격 문서는 20년 가까이 누적된 상태라 그대로 믿으면 안 되고, 값을 하나 바꿔보거나 mm/slub.c를 열어 확인하는 편이 확실하다.

참고

답글 남기기