54. 메모리 누수 추적 실습

Linux 7.0 기준 — kmemleak, page_owner를 사용한 커널 및 사용자 공간 메모리 누수 추적 방법과 실전 튜닝 가이드

개요

커널과 사용자 공간에서 발생하는 메모리 누수는 시스템 불안정성, 성능 저하, 서비스 중단을 초래할 수 있다. Linux 커널은 메모리 누수를 탐지하기 위한 여러 도구를 제공하며, 대표적으로 kmemleakpage_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

핵심 자료구조

struct kmemleak_object (kmemleak.c:135-161)

할당된 메모리 블록의 메타데이터를 저장하는 구조체. 모든 할당된 커널 메모리 블록에 대해 하나의 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];      // 할당한 프로세스 이름
};

OBJECT_ALLOCATED 플래그 (kmemleak.c:164)

#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 (kmemleak.c:118-122)

스캔 영역을 정의하는 구조체. 큰 메모리 블록의 일부분만 스캔할 때 사용된다.

struct kmemleak_scan_area {
struct hlist_node node;    // 해시 리스트 연결
unsigned long start;       // 스캔 시작 주소
size_t size;               // 스캔 크기
};

struct page_owner (page_owner.c:24-37)

페이지 할당 정보를 저장하는 구조체. 모든 할당된 페이지에 대해 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
};

struct stack (page_owner.c:39-42)

스택 레코드를 연결하는 구조체. stack_depot와 연결하여 스택 트레ース를 관리한다.

struct stack {
struct stack_record *stack_record;  // stack_depot 레코드
struct stack *next;                 // 다음 스택 노드
};

page_ext_operations (page_owner.c:143-148)

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_alloc (kmemleak.c:1090-1097)

새로운 커널 메모리 블록 할당을 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_free (kmemleak.c:1151-1157)

커널 메모리 블록 해제를 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를 삭제한다.

kmemleak_scan (kmemleak.c:1694-1791)

주기적으로 메모리를 스캔하여 누수를 탐지한다.

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단계: 회색 리스트 순회하며 참조 수 카운트
}

역할: 회색-검은색 알고리즘을 사용하여 참조되지 않는 메모리 블록을 탐지한다.

kmemleak_write (kmemleak.c:2098-2170)

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/kmemleakscan=60, scan, clear, stack=on/off를 써서 스캔 주기와 수동 점검을 제어한다.

__set_page_owner (page_owner.c:335-346)

페이지 할당 정보를 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);
}

역할: 페이지 할당 시 스택 트레ース와 함께 프로세스 정보를 기록한다.

__reset_page_owner (page_owner.c:298-333)

페이지 해제 정보를 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);
}

역할: 페이지 해제 시 해제 스택 트레ース를 기록하고 할당 핸들의 참조 카운트를 감소시킨다.

read_page_owner (page_owner.c:660-755)

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_extPAGE_EXT_OWNER_ALLOCATED를 확인하고 페이지 소유 정보를 출력한다.


호출 흐름

kmemleak 누수 탐지 흐름

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() - 누수 보고

page_owner 페이지 추적 흐름

__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 vs page_owner 비교

특성kmemleakpage_owner
**추적 대상**커널 메모리 할당 (kmalloc, kmem_cache 등)모든 페이지 할당 (Buddy, SLUB 등)
**동작 방식**주기적 스캔으로 미참조 객체 탐지할당/해제 시 스택 트레이스 기록
**오버헤드**스캔 시 CPU 사용량 증가스택 트레이스 저장으로 메모리 사용 증가
**정확도**오탐 가능 (간접 참조 미인식)정확한 할당 경로 추적
**활성화**`echo scan > /sys/kernel/debug/kmemleak`부트 파라미터 `page_owner=on` 필요
**출력**누수 의심 객체 목록모든 할당된 페이지의 스택 트레이스

KASAN vs kmemleak 비교

특성KASANkmemleak
**목적**메모리 접근 오류 탐지메모리 누수 탐지
**탐지 항목**버퍼 오버플로, UAF, OOB미참조 메모리 블록
**동작 방식**메모리 접근 시 실시간 검증주기적 스캔
**오버헤드**높음 (접근마다 검증)중간 (스캔 시)
**활성화**커널 컨피그 `CONFIG_KASAN=y`커널 컨피그 `CONFIG_DEBUG_KMEMLEAK=y`

GFP 플래그별 kmemleak 동작

GFP 플래그kmemleak 동작
`GFP_KERNEL`객체 생성, 스캔 대상
`GFP_NOWAIT`객체 생성, 스캔 대상
`GFP_NOIO`객체 생성, 스캔 대상
`GFP_NOFS`객체 생성, 스캔 대상
`__GFP_NOLEAKTRACE`객체 생성 제외 (SLAB_NOLEAKTRACE)
`__GFP_ZERO`객체 생성, 0 초기화된 메모리

page_owner 출력 형식

필드설명
`order`할당된 페이지 순서 (0이면 4KB)
`mask`GFP 플래그 (16진수)
`pid`할당한 프로세스 ID
`tgid`할당한 스레드 그룹 ID
`ts`할당 타임스탬프 (나노초)
`PFN`페이지 프레임 번호
`type`마이그레이션 타입 (MOVABLE, UNMOVABLE 등)
`Flags`페이지 플래그

실전 사용 시나리오

시나리오 1: 커널 모듈 메모리 누수 탐지

# 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

시나리오 2: 특정 페이지 할당 추적

# 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"

시나리오 3: 스크립트를 이용한 자동 누수 탐지

#!/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"

튜닝 가이드

kmemleak 튜닝 파라미터

파라미터기본값설명
`scan=60`600초자동 스캔 간격
`stack=on/off`1태스크 스택 스캔 활성화/비활성화
`scan`-즉시 스캔 트리거
`clear`-보고된 객체를 회색 처리하거나 내부 객체 정리

page_owner 튜닝 고려사항

  • page_owner=on 부트 파라미터 필수
  • 활성화 시 메모리 오버헤드 발생 (페이지당 64바이트)
  • 디버깅 목적으로만 사용 권장
  • production 환경에서는 page_owner=on 비활성화 유지

  • 관련 문서

  • 27-debug.md — 디버그 도구 (KASAN, KFENCE, Kmemleak)
  • 01-page_alloc.md — Buddy Allocator
  • 02-slab.md — SLUB 할당자
  • 41-page_owner.md — Page Owner
  • 42-percpu_mem.md — Per-CPU 메모리

  • SVG 다이어그램

    메모리 누수 추적 도구 계층 구조
    kmemleak 회색-검은색 알고리즘 흐름