Linux 7.0 기준 — kmemleak, page_owner를 사용한 커널 및 사용자 공간 메모리 누수 추적 방법과 실전 튜닝 가이드
커널과 사용자 공간에서 발생하는 메모리 누수는 시스템 불안정성, 성능 저하, 서비스 중단을 초래할 수 있다. Linux 커널은 메모리 누수를 탐지하기 위한 여러 도구를 제공하며, 대표적으로 kmemleak과 page_owner가 있다. kmemleak은 참조되지 않는 커널 메모리 블록을 주기적으로 스캔하여 누수를 보고하는 반면, page_owner는 모든 페이지 할당/해제 시 스택 트레ース를 기록하여 특정 페이지가 언제, 왜 할당되었는지 추적할 수 있다. 이 문서에서는 Linux 7.0 소스 기반으로 이 두 도구의 동작 원리와 실전 사용법을 분석한다.
실제 시스템에서는 kmemleak을 사용하여 커널 메모리 누수를 탐지하고, page_owner를 사용하여 특정 페이지의 할당 경로를 추적한다. KASAN은 메모리 접근 오류(버퍼 오버플로, Use-After-Free 등)를 탐지하며, 세 도구를 조합하면 메모리 관련 문제를 체계적으로 진단할 수 있다.
소스 파일 경로:
mm/kmemleak.c
mm/page_owner.c
include/linux/kmemleak.h
include/linux/page_owner.h
# kmemleak 활성화 상태 확인
cat /sys/kernel/debug/kmemleak
# kmemleak 스캔 트리거
echo scan > /sys/kernel/debug/kmemleak
# kmemleak 클리어 (이전 결과 제거)
echo clear > /sys/kernel/debug/kmemleak
# kmemleak 자동 스캔 간격 설정 (초 단위)
echo scan=60 > /sys/kernel/debug/kmemleak
# kmemleak 스택 스캔 켜기/끄기
echo stack=on > /sys/kernel/debug/kmemleak
echo stack=off > /sys/kernel/debug/kmemleak
# page_owner 활성화 (부트 파라미터 필요: page_owner=on)
cat /sys/kernel/debug/page_owner
# page_owner 출력 정렬 (할당 크기 기준)
cat /sys/kernel/debug/page_owner | sort -k 3 -n -r | head -20
# kmemleak 수동 스캔
echo scan > /sys/kernel/debug/kmemleak
# 메모리 누수 의심 시 slabinfo 확인
cat /proc/slabinfo | head -20
# kmemleak에서 발견된 누수 정보 파싱
cat /sys/kernel/debug/kmemleak | grep -A 5 "unreferenced"
# page_owner를 사용한 특정 PFN 추적
python3 - <<'PY'
path = "/sys/kernel/debug/page_owner"
pfn = 0x12345
with open(path, "rb") as f:
f.seek(pfn)
print(f.read(4096).decode("utf-8", "replace"))
PY
할당된 메모리 블록의 메타데이터를 저장하는 구조체. 모든 할당된 커널 메모리 블록에 대해 하나의 kmemleak_object가 생성된다.
struct kmemleak_object {
raw_spinlock_t lock; // 객체 보호용 스핀락
unsigned int flags; // 객체 상태 플래그 (OBJECT_ALLOCATED 등)
struct list_head object_list; // 전체 객체 리스트 연결
struct list_head gray_list; // 스캔 대상 리스트 연결
struct rb_node rb_node; // 레드-블랙 트리 노드 (주소 기반 검색)
struct rcu_head rcu; // RCU 기반 무잠금 순회
atomic_t use_count; // 참조 카운트 (0이면 해제 예약)
unsigned int del_state; // 삭제 상태
unsigned long pointer; // 할당된 메모리 블록 시작 주소
size_t size; // 할당된 메모리 블록 크기
unsigned long excess_ref; // 초과 참조 포인터
int min_count; // 누수 판정 최소 참조 수
int count; // 현재 탐지된 참조 수
u32 checksum; // 객체 무결성 검증용 체크섬
depot_stack_handle_t trace_handle; // 할당 시 스택 트레ース 핸들
struct hlist_head area_list; // 스캔 영역 리스트
unsigned long jiffies; // 생성 타임스탬프
pid_t pid; // 할당한 프로세스 ID
char comm[TASK_COMM_LEN]; // 할당한 프로세스 이름
};
#define OBJECT_ALLOCATED (1 << 0) // 메모리 블록 할당됨
#define OBJECT_REPORTED (1 << 1) // 첫 보고 후 설정
#define OBJECT_NO_SCAN (1 << 2) // 스캔 제외
#define OBJECT_FULL_SCAN (1 << 3) // 전체 영역 스캔
#define OBJECT_PHYS (1 << 4) // 물리 주소 기반
#define OBJECT_PERCPU (1 << 5) // per-CPU 할당
스캔 영역을 정의하는 구조체. 큰 메모리 블록의 일부분만 스캔할 때 사용된다.
struct kmemleak_scan_area {
struct hlist_node node; // 해시 리스트 연결
unsigned long start; // 스캔 시작 주소
size_t size; // 스캔 크기
};
페이지 할당 정보를 저장하는 구조체. 모든 할당된 페이지에 대해 page_ext를 통해 연결된다.
struct page_owner {
unsigned short order; // 할당된 페이지 순서 (0이면 1페이지)
short last_migrate_reason; // 마지막 마이그레이션 이유
gfp_t gfp_mask; // 할당 시 GFP 플래그
depot_stack_handle_t handle; // 할당 시 스택 트레ース 핸들
depot_stack_handle_t free_handle; // 해제 시 스택 트레ース 핸들
u64 ts_nsec; // 할당 타임스탬프 (나노초)
u64 free_ts_nsec; // 해제 타임스탬프 (나노초)
char comm[TASK_COMM_LEN]; // 할당한 프로세스 이름
pid_t pid; // 할당한 프로세스 ID
pid_t tgid; // 할당한 스레드 그룹 ID
pid_t free_pid; // 해제한 프로세스 ID
pid_t free_tgid; // 해제한 스레드 그룹 ID
};
스택 레코드를 연결하는 구조체. stack_depot와 연결하여 스택 트레ース를 관리한다.
struct stack {
struct stack_record *stack_record; // stack_depot 레코드
struct stack *next; // 다음 스택 노드
};
page_owner의 연산자 테이블. page_ext 프레임워크와의 통합을 정의한다.
struct page_ext_operations page_owner_ops = {
.size = sizeof(struct page_owner), // 저장할 메타데이터 크기
.need = need_page_owner, // page_owner 필요 여부 확인
.init = init_page_owner, // 초기화 함수
.need_shared_flags = true, // 공유 플래그 사용
};
새로운 커널 메모리 블록 할당을 kmemleak에 등록한다.
void __ref kmemleak_alloc(const void *ptr, size_t size, int min_count,
gfp_t gfp)
{
pr_debug("%s(0x%px, %zu, %d)\n", __func__, ptr, size, min_count);
// kmemleak 활성화 && 유효한 포인터인 경우 객체 생성
if (kmemleak_enabled && ptr && !IS_ERR(ptr))
create_object((unsigned long)ptr, size, min_count, gfp);
}
역할: kmalloc, kmem_cache_alloc 등에서 메모리 할당 시 호출되어 kmemleak_object를 생성하고 object_list에 추가한다.
커널 메모리 블록 해제를 kmemleak에 등록한다.
void __ref kmemleak_free(const void *ptr)
{
pr_debug("%s(0x%px)\n", __func__, ptr);
// kmemleak 활성화 && 유효한 포인터인 경우 객체 삭제
if (kmemleak_free_enabled && ptr && !IS_ERR(ptr))
delete_object_full((unsigned long)ptr, 0);
}
역할: kfree, kmem_cache_free 등에서 메모리 해제 시 호출되어 해당 kmemleak_object를 삭제한다.
주기적으로 메모리를 스캔하여 누수를 탐지한다.
static void kmemleak_scan(void)
{
struct kmemleak_object *object;
struct zone *zone;
int new_leaks = 0;
jiffies_last_scan = jiffies;
// 1단계: 모든 객체의 참조 수 초기화 (흰색으로 칠하기)
rcu_read_lock();
list_for_each_entry_rcu(object, &object_list, object_list) {
raw_spin_lock_irq(&object->lock);
// lowmem 외부 객체는 검은색으로 칠하기
if ((object->flags & OBJECT_PHYS) &&
!(object->flags & OBJECT_NO_SCAN)) {
unsigned long phys = object->pointer;
if (PHYS_PFN(phys) < min_low_pfn ||
PHYS_PFN(phys + object->size) > max_low_pfn)
__paint_it(object, KMEMLEAK_BLACK);
}
// 참조 수 초기화하고 회색 리스트에 추가
object->count = 0;
if (color_gray(object) && get_object(object))
list_add_tail(&object->gray_list, &gray_list);
raw_spin_unlock_irq(&object->lock);
}
rcu_read_unlock();
// 2단계: per-CPU 섹션 스캔
// 3단계: 모든 존의 페이지 스캔
// 4단계: 태스크 스택 스캔
// 5단계: 회색 리스트 순회하며 참조 수 카운트
}
역할: 회색-검은색 알고리즘을 사용하여 참조되지 않는 메모리 블록을 탐지한다.
static ssize_t kmemleak_write(struct file *file, const char __user *user_buf,
size_t size, loff_t *ppos)
{
if (strncmp(buf, "clear", 5) == 0) {
if (kmemleak_enabled)
kmemleak_clear();
else
__kmemleak_do_cleanup();
goto out;
}
if (!kmemleak_enabled) {
ret = -EPERM;
goto out;
}
if (strncmp(buf, "off", 3) == 0)
kmemleak_disable();
else if (strncmp(buf, "stack=on", 8) == 0)
kmemleak_stack_scan = 1;
else if (strncmp(buf, "stack=off", 9) == 0)
kmemleak_stack_scan = 0;
else if (strncmp(buf, "scan=on", 7) == 0)
start_scan_thread();
else if (strncmp(buf, "scan=off", 8) == 0)
stop_scan_thread();
else if (strncmp(buf, "scan=", 5) == 0) {
unsigned secs;
unsigned long msecs;
ret = kstrtouint(buf + 5, 0, &secs);
if (ret < 0)
goto out;
msecs = secs * MSEC_PER_SEC;
stop_scan_thread();
if (msecs) {
WRITE_ONCE(jiffies_scan_wait, msecs_to_jiffies(msecs));
start_scan_thread();
}
} else if (strncmp(buf, "scan", 4) == 0)
kmemleak_scan();
역할: /sys/kernel/debug/kmemleak에 scan=60, scan, clear, stack=on/off를 써서 스캔 주기와 수동 점검을 제어한다.
페이지 할당 정보를 page_owner에 기록한다.
noinline void __set_page_owner(struct page *page, unsigned short order,
gfp_t gfp_mask)
{
u64 ts_nsec = local_clock();
depot_stack_handle_t handle;
// 할당 시 스택 트레ース 저장
handle = save_stack(gfp_mask);
// page_owner 메타데이터 업데이트
__update_page_owner_handle(page, handle, order, gfp_mask, -1,
ts_nsec, current->pid, current->tgid,
current->comm);
// 스택 레코드 카운트 증가
inc_stack_record_count(handle, gfp_mask, 1 << order);
}
역할: 페이지 할당 시 스택 트레ース와 함께 프로세스 정보를 기록한다.
페이지 해제 정보를 page_owner에 기록한다.
void __reset_page_owner(struct page *page, unsigned short order)
{
struct page_ext *page_ext;
depot_stack_handle_t handle;
depot_stack_handle_t alloc_handle;
struct page_owner *page_owner;
u64 free_ts_nsec = local_clock();
page_ext = page_ext_get(page);
if (unlikely(!page_ext))
return;
page_owner = get_page_owner(page_ext);
alloc_handle = page_owner->handle;
page_ext_put(page_ext);
// 해제 시 스택 트레ース 저장
handle = save_stack(__GFP_NOWARN);
__update_page_owner_free_handle(page, handle, order, current->pid,
current->tgid, free_ts_nsec);
// 할당 핸들의 카운트 감소 (early_handle 제외)
if (alloc_handle != early_handle)
dec_stack_record_count(alloc_handle, 1 << order);
}
역할: 페이지 해제 시 해제 스택 트레ース를 기록하고 할당 핸들의 참조 카운트를 감소시킨다.
static ssize_t read_page_owner(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
if (!static_branch_unlikely(&page_owner_inited))
return -EINVAL;
if (*ppos == 0)
pfn = min_low_pfn;
else
pfn = *ppos;
for (; pfn < max_pfn; pfn++) {
page = pfn_to_page(pfn);
page_ext = page_ext_get(page);
if (unlikely(!page_ext))
continue;
page_owner = get_page_owner(page_ext);
if (!test_bit(PAGE_EXT_OWNER_ALLOCATED, &page_ext->flags))
goto ext_put_continue;
handle = READ_ONCE(page_owner->handle);
if (!handle)
goto ext_put_continue;
*ppos = pfn + 1;
page_owner_tmp = *page_owner;
page_ext_put(page_ext);
return print_page_owner(buf, count, pfn, page,
&page_owner_tmp, handle);
ext_put_continue:
page_ext_put(page_ext);
}
return 0;
}
역할: llseek로 PFN을 맞춘 뒤 page_ext와 PAGE_EXT_OWNER_ALLOCATED를 확인하고 페이지 소유 정보를 출력한다.
kmemleak_alloc() 호출 (SLUB/SLAB 할당 시)
└→ create_object()
└→ __alloc_object() - 메모리 풀에서 kmemleak_object 할당
└→ __link_object() - object_list와 object_tree_root에 추가
kmemleak_free() 호출 (kfree/kmem_cache_free 시)
└→ delete_object_full()
└→ __find_and_remove_object() - 객체 검색 및 제거
└→ __delete_object() - OBJECT_ALLOCATED 플래그 제거
└→ put_object() - RCU 콜백으로 해제 예약
kmemleak_scan() 주기적 호출 (스레드)
└→ 모든 객체의 count 초기화 (흰색)
└→ 회색 리스트에 추가
└→ scan_gray_list() / scan_object() - 객체와 스캔 영역 분기 처리
└→ scan_block() - 메모리 블록 스캔하여 참조 발견
└→ object->count 증가 (검은색으로 칠하기)
└→ count == 0 && min_count > 0인 객체 = 누수로 판정
└→ kmemleak_report() - 누수 보고
__set_page_owner() 호출 (페이지 할당 시)
└→ save_stack() - 스택 트레이스 저장 (stack_depot)
└→ __update_page_owner_handle() - page_owner 메타데이터 업데이트
└→ inc_stack_record_count() - 스택 레코드 카운트 증가
__reset_page_owner() 호출 (페이지 해제 시)
└→ save_stack() - 해제 스택 트레이스 저장
└→ __update_page_owner_free_handle() - 해제 핸들 업데이트
└→ dec_stack_record_count() - 스택 레코드 카운트 감소
read_page_owner() / lseek_page_owner() 호출 (/sys/kernel/debug/page_owner 읽기)
└→ 모든 페이지 순회하며 PAGE_EXT_OWNER 플래그 확인
└→ PAGE_EXT_OWNER_ALLOCATED 플래그 확인 (할당된 페이지만)
└→ print_page_owner() - 페이지 정보와 스택 트레이스 출력
| 특성 | kmemleak | page_owner |
|---|---|---|
| **추적 대상** | 커널 메모리 할당 (kmalloc, kmem_cache 등) | 모든 페이지 할당 (Buddy, SLUB 등) |
| **동작 방식** | 주기적 스캔으로 미참조 객체 탐지 | 할당/해제 시 스택 트레이스 기록 |
| **오버헤드** | 스캔 시 CPU 사용량 증가 | 스택 트레이스 저장으로 메모리 사용 증가 |
| **정확도** | 오탐 가능 (간접 참조 미인식) | 정확한 할당 경로 추적 |
| **활성화** | `echo scan > /sys/kernel/debug/kmemleak` | 부트 파라미터 `page_owner=on` 필요 |
| **출력** | 누수 의심 객체 목록 | 모든 할당된 페이지의 스택 트레이스 |
| 특성 | KASAN | kmemleak |
|---|---|---|
| **목적** | 메모리 접근 오류 탐지 | 메모리 누수 탐지 |
| **탐지 항목** | 버퍼 오버플로, UAF, OOB | 미참조 메모리 블록 |
| **동작 방식** | 메모리 접근 시 실시간 검증 | 주기적 스캔 |
| **오버헤드** | 높음 (접근마다 검증) | 중간 (스캔 시) |
| **활성화** | 커널 컨피그 `CONFIG_KASAN=y` | 커널 컨피그 `CONFIG_DEBUG_KMEMLEAK=y` |
| GFP 플래그 | kmemleak 동작 |
|---|---|
| `GFP_KERNEL` | 객체 생성, 스캔 대상 |
| `GFP_NOWAIT` | 객체 생성, 스캔 대상 |
| `GFP_NOIO` | 객체 생성, 스캔 대상 |
| `GFP_NOFS` | 객체 생성, 스캔 대상 |
| `__GFP_NOLEAKTRACE` | 객체 생성 제외 (SLAB_NOLEAKTRACE) |
| `__GFP_ZERO` | 객체 생성, 0 초기화된 메모리 |
| 필드 | 설명 |
|---|---|
| `order` | 할당된 페이지 순서 (0이면 4KB) |
| `mask` | GFP 플래그 (16진수) |
| `pid` | 할당한 프로세스 ID |
| `tgid` | 할당한 스레드 그룹 ID |
| `ts` | 할당 타임스탬프 (나노초) |
| `PFN` | 페이지 프레임 번호 |
| `type` | 마이그레이션 타입 (MOVABLE, UNMOVABLE 등) |
| `Flags` | 페이지 플래그 |
# 1단계: kmemleak 스캔 간격 설정
echo scan=60 > /sys/kernel/debug/kmemleak
# 2단계: 의심스러운 모듈 로드/언로드
insmod suspect_module.ko
rmmod suspect_module.ko
# 3단계: kmemleak 스캔 트리거
echo scan > /sys/kernel/debug/kmemleak
# 4단계: 결과 확인
cat /sys/kernel/debug/kmemleak
# 1단계: page_owner 활성화 (부트 파라미터 필요)
# GRUB: page_owner=on 추가 후 재부팅
# 2단계: 특정 PFN 할당 정보 확인
python3 - <<'PY'
path = "/sys/kernel/debug/page_owner"
pfn = 0x12345
with open(path, "rb") as f:
f.seek(pfn)
print(f.read(4096).decode("utf-8", "replace"))
PY
# 3단계: 할당 크기 기준 정렬
cat /sys/kernel/debug/page_owner | sort -k 3 -n -r | head -20
# 4단계: 특정 프로세스 할당 추적
cat /sys/kernel/debug/page_owner | grep "pid 1234"
#!/bin/bash
# kmemleak 자동 스캔 스크립트
echo "=== kmemleak 자동 스캔 시작 ==="
# 스캔 간격 설정
echo scan=300 > /sys/kernel/debug/kmemleak
# 스캔 실행
echo scan > /sys/kernel/debug/kmemleak
# 결과 저장
cat /sys/kernel/debug/kmemleak > /home/ryotta205/Chamber_linux/kmemleak_result_$(date +%Y%m%d_%H%M%S).txt
echo "=== 스캔 완료 ==="
echo "결과 파일: /home/ryotta205/Chamber_linux/kmemleak_result_$(date +%Y%m%d_%H%M%S).txt"
| 파라미터 | 기본값 | 설명 |
|---|---|---|
| `scan=60` | 600초 | 자동 스캔 간격 |
| `stack=on/off` | 1 | 태스크 스택 스캔 활성화/비활성화 |
| `scan` | - | 즉시 스캔 트리거 |
| `clear` | - | 보고된 객체를 회색 처리하거나 내부 객체 정리 |
page_owner=on 부트 파라미터 필수page_owner=on 비활성화 유지