슬랩 캐시가 계속 커질 때 /proc/slabinfo는 어느 캐시에 오브젝트가 몇 개 있는지만 알려주고, 그 오브젝트를 누가 할당했는지는 알려주지 않는다. SLUB에는 오브젝트마다 마지막 할당·해제 호출 스택을 기록하는 기능이 있어서 이 질문에 답할 수 있는데, 커널 ABI 문서와 오래된 자료는 하나같이 /sys/kernel/slab/<cache>/alloc_calls를 읽으라고 안내한다. 정작 요즘 커널에서 그 파일을 찾으면 없다. 이 글에서는 alloc_calls가 어디로 옮겨갔는지, 어떻게 켜서 읽는지, 그리고 kmalloc 캐시에서 호출자를 찾을 때 걸리는 함정을 Ubuntu 24.04 / 커널 6.8 환경에서 직접 확인한 결과로 정리한다.
alloc_calls는 어디로 갔나
$ ls /sys/kernel/slab/kmalloc-1k/ | tr '\n' ' '
aliases align cache_dma cpu_partial cpu_slabs ctor destroy_by_rcu hwcache_align min_partial
objects object_size objects_partial objs_per_slab order partial poison reclaim_account
red_zone remote_node_defrag_ratio sanity_checks shrink skip_kfence slabs slabs_cpu_partial
slab_size store_user total_objects trace usersize validate
$ ls /sys/kernel/slab/kmalloc-1k/alloc_calls
ls: cannot access '/sys/kernel/slab/kmalloc-1k/alloc_calls': No such file or directory
alloc_calls도 free_calls도 없다. sysfs는 파일 하나당 PAGE_SIZE까지만 출력할 수 있고 “한 파일에 한 값” 규칙에도 어긋나서, 두 파일은 5.14에서 debugfs로 옮겨지면서 alloc_traces/free_traces로 이름까지 바뀌었다.
| 커널 | 변화 | 커밋 |
|---|---|---|
| ~5.13 | /sys/kernel/slab/<cache>/alloc_calls, free_calls (2.6.22부터) | – |
| 5.14 | debugfs로 이동 + 이름 변경 → /sys/kernel/debug/slab/<cache>/alloc_traces, free_traces | 64dd68497be5 |
| 5.19 | stackdepot 사용. 호출자 한 줄이 아니라 전체 콜 스택을 저장 | 5cf909c553e9 |
| 6.1 | kmalloc 캐시에 한해 요청 크기와 실제 오브젝트 크기의 차이를 waste=로 표시 | 6edf2576a6cc |
| 6.9 | 부팅 파라미터 이름이 slab_debug=로 변경(slub_debug=는 별칭으로 유지) | – |
파일이 생기는 조건
debugfs 아래 캐시 디렉토리는 모든 캐시에 대해 만들어지지 않는다. slab_debugfs_init()은 SLAB_STORE_USER 플래그가 붙은 캐시만 등록한다.
$ sudo ls /sys/kernel/debug/slab/
$
부팅 파라미터 없이 올라온 시스템에서는 출력이 비어 있다. 아래 조건이 모두 갖춰져야 캐시 디렉토리가 생긴다.
| 조건 | 확인 방법 | 비고 |
|---|---|---|
CONFIG_SLUB_DEBUG=y | grep CONFIG_SLUB_DEBUG= /boot/config-$(uname -r) | 대부분의 배포판 커널에서 기본 y |
CONFIG_STACKDEPOT=y | 동일 | 없으면 호출자 한 줄만 나오고 스택은 안 나옴 |
| debugfs 마운트 | mount | grep debugfs | 파일 권한이 0400이라 root만 읽을 수 있음 |
캐시에 SLAB_STORE_USER | cat /sys/kernel/slab/<cache>/store_user | 아래 세 가지 방법 중 하나로 켬 |
| 켜는 방법 | 적용 범위 | 재부팅 |
|---|---|---|
slub_debug=U,<캐시> 부팅 파라미터 | 지정한 기존 캐시 | 필요 |
CONFIG_SLUB_DEBUG_ON=y | 모든 캐시(FZPU 전부) | 커널 재빌드 |
kmem_cache_create(..., SLAB_STORE_USER, ...) | 내가 만든 캐시 | 불필요 |
디버그 플래그는 캐시가 만들어지는 시점에 확정되기 때문에, 이미 부팅된 시스템에서 기존 캐시에 store_user를 나중에 켜는 방법은 없다. 반대로 내 모듈이 직접 만드는 캐시라면 재부팅 없이 바로 쓸 수 있다.
재부팅 없이 확인하기: SLAB_STORE_USER 캐시
호출 지점 두 곳에서 같은 캐시를 쓰는 모듈을 만들어, 32개는 계속 보유하고 8개는 할당·해제 후 다시 할당하도록 했다.
#include <linux/module.h>
#include <linux/slab.h>
#define NR_META 32
#define NR_SESSION 8
struct demo_obj {
unsigned long id;
char payload[120];
};
static struct kmem_cache *demo_cache;
static struct demo_obj *meta[NR_META];
static struct demo_obj *session[NR_SESSION];
static noinline void alloc_metadata(void)
{
int i;
for (i = 0; i < NR_META; i++)
meta[i] = kmem_cache_zalloc(demo_cache, GFP_KERNEL);
}
static noinline void alloc_session(void)
{
int i;
for (i = 0; i < NR_SESSION; i++)
session[i] = kmem_cache_zalloc(demo_cache, GFP_KERNEL);
}
static noinline void free_session(void)
{
int i;
for (i = 0; i < NR_SESSION; i++)
kmem_cache_free(demo_cache, session[i]);
}
static int __init demo_init(void)
{
demo_cache = kmem_cache_create("demo_slab", sizeof(struct demo_obj),
0, SLAB_STORE_USER, NULL);
if (!demo_cache)
return -ENOMEM;
alloc_metadata(); /* 32개 할당 후 계속 보유 */
alloc_session(); /* 8개 할당 */
free_session(); /* 위 8개 해제 */
alloc_session(); /* 방금 해제한 자리를 재사용 */
return 0;
}
static void __exit demo_exit(void)
{
int i;
for (i = 0; i < NR_META; i++)
kmem_cache_free(demo_cache, meta[i]);
free_session();
kmem_cache_destroy(demo_cache);
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");obj-m := slab_trace_demo.o kmalloc_leak.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean$ make
$ sudo insmod slab_trace_demo.ko
$ ls /sys/kernel/debug/slab/
demo_slab
$ sudo cat /sys/kernel/debug/slab/demo_slab/alloc_traces
32 alloc_metadata+0x2b/0x60 [slab_trace_demo] age=5/5/5 pid=2341 cpus=2
alloc_metadata+0x2b/0x60 [slab_trace_demo]
demo_init+0x35/0xff0 [slab_trace_demo]
do_one_initcall+0x5e/0x340
do_init_module+0x97/0x290
load_module+0xb5f/0xca0
init_module_from_file+0x96/0x100
idempotent_init_module+0x11c/0x310
__x64_sys_finit_module+0x64/0xd0
x64_sys_call+0x2019/0x25a0
do_syscall_64+0x87/0x180
entry_SYSCALL_64_after_hwframe+0x78/0x80
8 alloc_session+0x2b/0x60 [slab_trace_demo] age=5/5/5 pid=2341 cpus=2
alloc_session+0x2b/0x60 [slab_trace_demo]
demo_init+0x44/0xff0 [slab_trace_demo]
do_one_initcall+0x5e/0x340
...
호출 지점별로 살아있는 오브젝트 수가 그대로 집계된다. 같은 캐시라도 alloc_metadata()가 32개, alloc_session()이 8개를 잡고 있다는 것이 바로 보인다.
$ sudo cat /sys/kernel/debug/slab/demo_slab/free_traces
32 <not-available> age=4295310043 pid=0 cpus=0
8 free_session+0x2d/0x50 [slab_trace_demo] age=7/7/7 pid=2341 cpus=2
kmem_cache_free+0x3cf/0x480
free_session+0x2d/0x50 [slab_trace_demo]
demo_init+0x3f/0xff0 [slab_trace_demo]
...
free_traces는 “해제된 오브젝트 목록”이 아니라 지금 할당되어 있는 오브젝트의 직전 해제 기록이다. 커널이 슬랩을 훑을 때(process_slab()) free 비트가 꺼진 오브젝트만 집계하기 때문으로, 한 번도 재사용된 적 없는 32개는 <not-available>로 나오고 해제 후 재할당된 8개만 free_session()을 가리킨다. use-after-free 조사에서 “이 오브젝트가 직전에 어디서 free됐나”를 볼 때 쓰는 정보다.
출력 필드 읽기
| 필드 | 의미 |
|---|---|
| 맨 앞 숫자 | 같은 스택으로 할당되어 현재 살아있는 오브젝트 수 |
심볼+0x2b/0x60 | 기록된 호출자 주소. 아래 들여쓴 줄들이 stackdepot에 저장된 전체 스택 |
waste=20736/324 | (총 낭비/오브젝트당 낭비) 바이트. kmalloc 캐시에서 요청 크기와 실제 크기 차이 |
age=min/avg/max | 할당 후 경과 시간(jiffies). CONFIG_HZ=1000이면 ms와 같음. 값이 하나면 전부 동일 |
pid=1-905 | 할당한 프로세스 PID 범위. 값이 하나면 단일 PID |
cpus=, nodes= | 할당이 일어난 CPU/NUMA 노드 목록. 각각 2개 이상일 때만 출력 |
age가 4295310043처럼 비정상적으로 크면 트랙이 비어 있는(when=0) 항목이다. 위 <not-available> 줄이 그 경우다.
kmalloc 캐시에서 호출자 찾기
실전에서 궁금한 쪽은 보통 kmalloc-*다. 700바이트를 64번 할당하고 놓아주지 않는 모듈로 재현했다.
#include <linux/module.h>
#include <linux/slab.h>
#define NR_LEAK 64
static void *leaked[NR_LEAK];
static noinline void leak_config_blob(void)
{
int i;
for (i = 0; i < NR_LEAK; i++)
leaked[i] = kmalloc(700, GFP_KERNEL); /* kmalloc-1k에서 할당 */
}
static int __init leak_init(void)
{
leak_config_blob();
return 0;
}
static void __exit leak_exit(void)
{
int i;
for (i = 0; i < NR_LEAK; i++)
kfree(leaked[i]);
}
module_init(leak_init);
module_exit(leak_exit);
MODULE_LICENSE("GPL");700바이트면 kmalloc-1k이므로 slub_debug=U,kmalloc-1k로 부팅하고 모듈을 올렸는데, alloc_traces에 leak_config_blob이 나오지 않았다. 모듈 적재 전후로 /proc/slabinfo를 비교해 보면 이유가 드러난다.
$ sudo awk 'NR>2{print $1, $2}' /proc/slabinfo | sort > /tmp/before
$ sudo insmod kmalloc_leak.ko
$ sudo awk 'NR>2{print $1, $2}' /proc/slabinfo | sort > /tmp/after
$ join /tmp/before /tmp/after | awk '$2!=$3 {print $1, $2, "->", $3}'
kmalloc-1k 94 -> 96
kmalloc-rnd-12-1k 130 -> 208
maple_node 1320 -> 1541
task_struct 240 -> 246
vma_lock 6396 -> 6528
할당은 kmalloc-1k이 아니라 kmalloc-rnd-12-1k로 갔다. 6.6에서 들어온 CONFIG_RANDOM_KMALLOC_CACHES가 kmalloc 캐시를 크기별로 16벌씩 복제해두고, 호출 지점 주소의 해시로 그중 하나를 고르기 때문이다. 어느 번호로 갈지는 부팅할 때마다 달라지므로(아래 재부팅 후에는 같은 호출 지점이 rnd-13으로 옮겨갔다) 캐시 이름을 고정해 지정할 수 없고, 대신 glob을 쓴다.
# /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="slub_debug=U,kmalloc-*"
$ sudo insmod kmalloc_leak.ko
$ sudo grep -l leak_config_blob /sys/kernel/debug/slab/*/alloc_traces
/sys/kernel/debug/slab/kmalloc-rnd-13-1k/alloc_traces
$ sudo cat /sys/kernel/debug/slab/kmalloc-rnd-13-1k/alloc_traces
64 leak_config_blob+0x5d/0x90 [kmalloc_leak] waste=20736/324 age=19198/19198/19198 pid=1387 cpus=2
leak_config_blob+0x5d/0x90 [kmalloc_leak]
leak_init+0xe/0xff0 [kmalloc_leak]
do_one_initcall+0x5e/0x340
do_init_module+0x97/0x290
load_module+0xb5f/0xca0
...
64개가 그대로 잡히고, waste=20736/324로 1024바이트 오브젝트에 700바이트만 담아 개당 324바이트씩 총 20KB를 버리고 있다는 것까지 나온다. 이름 매칭은 * 앞부분만 prefix로 비교하므로 kmalloc-*는 kmalloc-rnd-*와 kmalloc-cg-*까지 모두 포함한다.
낭비 기준으로 전체 kmalloc 캐시를 한 번에 줄 세우면 크기 클래스를 잘못 고른 할당 지점이 바로 드러난다.
$ sudo awk '/waste=/ {match($0, /waste=[0-9]+/); print substr($0, RSTART+6, RLENGTH-6), $1, $2}' \
/sys/kernel/debug/slab/kmalloc-*/alloc_traces | sort -rn | head -5
201432 109 mem_cgroup_alloc+0x31/0x550
190944 204 bpf_prog_alloc_no_stats+0xc7/0x2b0
124696 109 mem_cgroup_alloc+0x226/0x550
87296 176 kvmalloc_node+0x24/0x100
54936 109 alloc_shrinker_info+0xf5/0x1f0
첫 컬럼이 총 낭비 바이트, 둘째가 오브젝트 수다. mem_cgroup_alloc() 한 곳만으로 109개 오브젝트에서 약 197KB가 버려지고 있다.
주의사항
store_user를 켜면 오브젝트마다struct track이 두 개(할당/해제) 붙는다. 실측으로 커스텀 캐시는object_size128 →slab_size192,kmalloc-1k은 1024 → 2048로 늘었다.kmalloc-*전체에 거는 것은 재현 시험용으로만 쓰고 운영에는 캐시를 좁혀서 건다.- 디버그 플래그가 붙은 캐시는 다른 캐시와 병합되지 않는다(
aliases가 0이 된다). 추적에는 유리하지만, 병합이 사라지면서 슬랩 개수와 메모리 사용량 자체가 평소와 달라지므로 이 상태의slabinfo수치를 평상시 기준으로 삼으면 안 된다. - stackdepot에는 심볼이 아니라 주소가 저장되고 읽는 시점에 심볼로 변환된다. 모듈을 언로드했다가 다른 모듈을 올리면 예전 스택이 엉뚱한 모듈 심볼로 보일 수 있다(실제로
proto_register가leak_config_blob에서 호출된 것처럼 표시되는 항목이 관찰됐다). Documentation/ABI/testing/sysfs-kernel-slab은 지금도alloc_calls를 sysfs 항목으로 기술하고 있다. 파일이 옮겨진 지 오래됐으니 문서만 보고 sysfs를 뒤지지 말 것.- 커널 6.9부터 부팅 파라미터 이름이
slab_debug=다. 예전 이름slub_debug=도 별칭으로 남아 있어 위 예시는 두 이름 모두 동작한다.
마무리
alloc_traces는 kmemleak처럼 별도 스캐너를 돌리지 않고도 “지금 이 캐시를 붙잡고 있는 코드”를 호출 스택 단위로 집계해 준다. 켜는 비용이 오브젝트당 수십 바이트로 작지 않으니 상시 활성화보다는, 슬랩 사용량이 이상한 캐시를 slabinfo로 먼저 좁힌 뒤 그 캐시에만 slab_debug=U를 걸고 재부팅하는 순서가 현실적이다.