커널 개발에서 메모리 누수는 심각한 문제이다. 메모리 누수는 시스템의 성능을 저하시키고, 장기적으로는 시스템이 불안정해지는 원인이 될 수 있기 때문이다. 이를 해결하기 위해 Linux 커널은 Kernel Memory Leak Detector인 kmemleak 툴을 제공한다. kmemleak는 커널에서 발생하는 메모리 누수를 탐지하고 보고하는 도구이다. 이번 글에서는 kmemleak의 사용법에 대해 알아본다.
Kmemleak 적용
Enable CONFIG_DEBUG_KMEMLEAK
kmemleak을 사용하기 위해서는 “Kernel hacking” CONFIG_DEBUG_KMEMLEAK 을 활성화 해야 한다.
$ ./scripts/config --enable DEBUG_KMEMLEAK
$ cat .config| grep KMEMLEAK
CONFIG_HAVE_DEBUG_KMEMLEAK=y
CONFIG_DEBUG_KMEMLEAK=y
CONFIG_DEBUG_KMEMLEAK_MEM_POOL_SIZE=16000
# CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF is not set
CONFIG_DEBUG_KMEMLEAK_AUTO_SCAN=y
커널 쓰레드는 기본적으로 10분 마다 메모리를 캔하고 발견된 참조되지 않은 object들을 출력한다.
kmemleak sysfs 인터페이스
kmemleak sysfs 인터페이스를 사용하려면 아래와 같이 debugfs를 마운트 한다.
mount -t debugfs nodev /sys/kernel/debug
커널 쓰레드는 기본적으로 10분 마다 메모리를 캔하고 발견된 참조되지 않은 object들을 출력한다. debugfs 마운트 후, kmemleak 인터페이스를 확인하고 메모리 누수에 대한 정보를 보기위해 다음을 수행한다.
cat /sys/kernel/debug/kmemleak
중간에 메모리 스캔을 트리거 하려면 다음과 같이 kmemleak 인터페이스에 scan
을 입력한다.
echo scan > /sys/kernel/debug/kmemleak
현재 가능한 모든 kmemleak 리스트를 지우려면 다음과 같이 clear
를 입력한다.
echo clear > /sys/kernel/debug/kmemleak
kmemleak 파라미터
Runtime 시 kmemleak 파라미터를 조정하여 kmemleak 동작을 제어 할 수 있다.
- off : kmemleak 비활성화 (되돌릴수 없음)
- stack=on : 스택 검사 활성화 (default)
- stack=off : 스택 검사 비활성화
- scan=on : 자동 메모리 스캐닝 쓰레드 시작 (default)
- scan=off : 자동 메모리 스캐닝 쓰레드 중지
- scan=<secs> : 자동 메모리 스캐닝 시간을 초단위로 설정 (default 600, 자동 스캔 중지하려면 0)
- scan: 메모리 스캔 트리거
- clear : 현재 보고된 unreferenced 객체를 모두 gray로 표시하여 메모리 leak 의심 목록을 지움
- dump=<addr> : <addr>에서 발견된 객체에 대한 정보 덤프
kmemleak 예제
// SPDX-License-Identifier: GPL-2.0-only
/*
* samples/kmemleak/kmemleak-test.c
*
* Copyright (C) 2008 ARM Limited
* Written by Catalin Marinas <catalin.marinas@arm.com>
*/
#define pr_fmt(fmt) "kmemleak: " fmt
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/list.h>
#include <linux/percpu.h>
#include <linux/fdtable.h>
#include <linux/kmemleak.h>
struct test_node {
long header[25];
struct list_head list;
long footer[25];
};
static LIST_HEAD(test_list);
static DEFINE_PER_CPU(void *, kmemleak_test_pointer);
/*
* Some very simple testing. This function needs to be extended for
* proper testing.
*/
static int kmemleak_test_init(void)
{
struct test_node *elem;
int i;
pr_info("Kmemleak testing\n");
/* make some orphan objects */
pr_info("kmalloc(32) = %p\n", kmalloc(32, GFP_KERNEL));
pr_info("kmalloc(32) = %p\n", kmalloc(32, GFP_KERNEL));
pr_info("kmalloc(1024) = %p\n", kmalloc(1024, GFP_KERNEL));
pr_info("kmalloc(1024) = %p\n", kmalloc(1024, GFP_KERNEL));
pr_info("kmalloc(2048) = %p\n", kmalloc(2048, GFP_KERNEL));
pr_info("kmalloc(2048) = %p\n", kmalloc(2048, GFP_KERNEL));
pr_info("kmalloc(4096) = %p\n", kmalloc(4096, GFP_KERNEL));
pr_info("kmalloc(4096) = %p\n", kmalloc(4096, GFP_KERNEL));
#ifndef CONFIG_MODULES
pr_info("kmem_cache_alloc(files_cachep) = %p\n",
kmem_cache_alloc(files_cachep, GFP_KERNEL));
pr_info("kmem_cache_alloc(files_cachep) = %p\n",
kmem_cache_alloc(files_cachep, GFP_KERNEL));
#endif
pr_info("vmalloc(64) = %p\n", vmalloc(64));
pr_info("vmalloc(64) = %p\n", vmalloc(64));
pr_info("vmalloc(64) = %p\n", vmalloc(64));
pr_info("vmalloc(64) = %p\n", vmalloc(64));
pr_info("vmalloc(64) = %p\n", vmalloc(64));
/*
* Add elements to a list. They should only appear as orphan
* after the module is removed.
*/
for (i = 0; i < 10; i++) {
elem = kzalloc(sizeof(*elem), GFP_KERNEL);
pr_info("kzalloc(sizeof(*elem)) = %p\n", elem);
if (!elem)
return -ENOMEM;
INIT_LIST_HEAD(&elem->list);
list_add_tail(&elem->list, &test_list);
}
for_each_possible_cpu(i) {
per_cpu(kmemleak_test_pointer, i) = kmalloc(129, GFP_KERNEL);
pr_info("kmalloc(129) = %p\n",
per_cpu(kmemleak_test_pointer, i));
}
return 0;
}
static void __exit kmemleak_test_exit(void)
{
struct test_node *elem, *tmp;
/*
* Remove the list elements without actually freeing the
* memory.
*/
list_for_each_entry_safe(elem, tmp, &test_list, list)
list_del(&elem->list);
}
module_init(kmemleak_test_init);
module_exit(kmemleak_test_exit);
MODULE_LICENSE("GPL");
다음은 위 kmemleak 예제 소스를 컴파일 한 ko파일을 라즈베리파이에서 insmod 시 dmesg 에 추가된 메세지 이다.
[ 277.226418] kmemleak_test: loading out-of-tree module taints kernel.
[ 277.227683] kmemleak: Kmemleak testing
[ 277.227703] kmemleak: kmalloc(32) = 000000005ce19789
[ 277.227720] kmemleak: kmalloc(32) = 00000000f03d1900
[ 277.227735] kmemleak: kmalloc(1024) = 00000000ea3776ef
[ 277.227747] kmemleak: kmalloc(1024) = 00000000b254aa2f
[ 277.227763] kmemleak: kmalloc(2048) = 000000008e4ea6cc
[ 277.227782] kmemleak: kmalloc(2048) = 0000000072e7071d
[ 277.227799] kmemleak: kmalloc(4096) = 000000000ebe1d0e
[ 277.227811] kmemleak: kmalloc(4096) = 00000000f7fa0ab7
[ 277.227878] kmemleak: vmalloc(64) = 00000000b8e6f08f
[ 277.227929] kmemleak: vmalloc(64) = 0000000013efdec3
[ 277.227968] kmemleak: vmalloc(64) = 0000000072874f6e
[ 277.228014] kmemleak: vmalloc(64) = 00000000eaf41b31
[ 277.228118] kmemleak: vmalloc(64) = 000000001a742bab
[ 277.228136] kmemleak: kzalloc(sizeof(*elem)) = 00000000c16622eb
[ 277.228147] kmemleak: kzalloc(sizeof(*elem)) = 00000000fde86096
[ 277.228157] kmemleak: kzalloc(sizeof(*elem)) = 00000000d6b3949f
[ 277.228167] kmemleak: kzalloc(sizeof(*elem)) = 0000000098b8ab2f
[ 277.228178] kmemleak: kzalloc(sizeof(*elem)) = 0000000008de7dc5
[ 277.228189] kmemleak: kzalloc(sizeof(*elem)) = 00000000a81b48b5
[ 277.228200] kmemleak: kzalloc(sizeof(*elem)) = 000000004e66a918
[ 277.228210] kmemleak: kzalloc(sizeof(*elem)) = 000000000aa2105c
[ 277.228220] kmemleak: kzalloc(sizeof(*elem)) = 000000009e9731b0
[ 277.228230] kmemleak: kzalloc(sizeof(*elem)) = 00000000af5d3653
[ 277.228247] kmemleak: kmalloc(129) = 00000000a7ced7f1
[ 277.228257] kmemleak: kmalloc(129) = 0000000090866847
[ 277.228268] kmemleak: kmalloc(129) = 000000007ce7e406
[ 277.228279] kmemleak: kmalloc(129) = 00000000d664c79d
다음은 kmemleak scan 시 결과이다. unreference 객체 정보를 출력하여 memory leak 의심 지점을 참고할 수 있다.
root@raspberrypi:~# echo scan > /sys/kernel/debug/kmemleak
root@raspberrypi:~# cat /sys/kernel/debug/kmemleak
unreferenced object 0xffffffc08098b000 (size 4096):
comm "insmod", pid 1914, jiffies 4294961601 (age 318.968s)
hex dump (first 32 bytes):
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
backtrace:
[<00000000b2480bbd>] __vmalloc_node_range+0x710/0x770
[<00000000d3c5750d>] vmalloc+0x64/0x80
[<000000003c8d839c>] kmemleak_test_init+0x180/0x2b0 [kmemleak_test]
[<00000000a8d17a97>] do_one_initcall+0x4c/0x298
[<000000003cc1a023>] do_init_module+0x60/0x218
[<00000000869479ce>] load_module+0x1c08/0x1d40
[<000000008ec63e18>] init_module_from_file+0x8c/0xd8
[<00000000f772d9a4>] __arm64_sys_finit_module+0x1c0/0x290
[<0000000029b60f0f>] invoke_syscall+0x50/0x128
[<000000005fb76f4e>] el0_svc_common.constprop.0+0x48/0xf8
[<00000000e19b1d1f>] do_el0_svc+0x28/0x40
[<00000000228b4432>] el0_svc+0x40/0xf8
[<00000000ec15273e>] el0t_64_sync_handler+0x13c/0x158
[<000000006a7dbf03>] el0t_64_sync+0x190/0x198
unreferenced object 0xffffffc0809c3000 (size 4096):
comm "insmod", pid 1914, jiffies 4294961601 (age 318.972s)
hex dump (first 32 bytes):
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
backtrace:
[<00000000b2480bbd>] __vmalloc_node_range+0x710/0x770
[<00000000d3c5750d>] vmalloc+0x64/0x80
[<00000000b3d9490c>] kmemleak_test_init+0x194/0x2b0 [kmemleak_test]
[<00000000a8d17a97>] do_one_initcall+0x4c/0x298
[<000000003cc1a023>] do_init_module+0x60/0x218
[<00000000869479ce>] load_module+0x1c08/0x1d40
[<000000008ec63e18>] init_module_from_file+0x8c/0xd8
[<00000000f772d9a4>] __arm64_sys_finit_module+0x1c0/0x290
[<0000000029b60f0f>] invoke_syscall+0x50/0x128
[<000000005fb76f4e>] el0_svc_common.constprop.0+0x48/0xf8
[<00000000e19b1d1f>] do_el0_svc+0x28/0x40
[<00000000228b4432>] el0_svc+0x40/0xf8
[<00000000ec15273e>] el0t_64_sync_handler+0x13c/0x158
[<000000006a7dbf03>] el0t_64_sync+0x190/0x198