kref / refcount_t (참조 카운터)
커널 객체의 수명을 관리하는 참조 카운팅(Reference Counting) 메커니즘인 kref와 refcount_t를 심층 분석합니다. include/linux/kref.h에 정의된 kref API의 내부 구현, include/linux/refcount.h와 lib/refcount.c에 구현된 refcount_t의 saturation 보호 메커니즘, 기존 atomic_t에서 refcount_t로의 전환 이유와 역사, RCU와의 조합 패턴, kobject/device/sk_buff/file/dentry 등 실제 커널 서브시스템에서의 활용 사례, 그리고 흔한 실수와 디버깅(Debugging) 기법까지 커널 소스 기반으로 분석합니다.
핵심 요약
- 참조 카운팅(Reference Counting) — 객체를 공유하는 모든 사용자가 "사용 시작" 시 카운트를 증가시키고, "사용 완료" 시 감소시킵니다. 카운트가 0에 도달하면 release 콜백이 호출되어 객체가 해제됩니다.
- kref —
include/linux/kref.h에 정의된 커널 참조 카운터 래퍼(Wrapper)입니다. 내부적으로refcount_t를 사용하며,kref_init/kref_get/kref_putAPI를 제공합니다. - refcount_t —
atomic_t를 대체하는 참조 카운터 전용 타입으로, 오버플로(Overflow)/언더플로(Underflow) saturation 보호를 제공합니다. 0→1 전환이나 음수 전환 시 WARN을 발생시킵니다. - atomic_t와의 차이 —
atomic_t는 단순 정수 연산으로 보호 없이 오버플로/언더플로가 발생할 수 있습니다.refcount_t는 use-after-free(UAF)와 double-free를 탐지합니다. - 커널 전역 사용 — kobject, device, sk_buff, file, dentry, inode, mm_struct 등 거의 모든 주요 커널 객체가 참조 카운팅으로 수명을 관리합니다.
단계별 이해
- 왜 참조 카운팅이 필요한가 이해
여러 코드 경로가 동일 객체를 동시에 사용할 때, 누가 마지막으로 사용을 끝내는지 미리 알 수 없습니다. 참조 카운팅은 이 문제를 해결합니다. - kref API 기초 학습
kref_init으로 초기화,kref_get으로 획득,kref_put으로 해제하는 기본 패턴을 익힙니다. - refcount_t의 보호 메커니즘 파악
saturation(포화) 보호가 어떻게 오버플로/언더플로를 방지하는지 이해합니다. - RCU 조합 패턴 학습
kref_get_unless_zero와rcu_read_lock을 조합한 안전한 lookup 패턴을 이해합니다. - 실전 활용 사례 분석
kobject, device, sk_buff 등 실제 커널 코드에서의 사용 패턴을 추적합니다.
include/linux/kref.h, include/linux/refcount.h, lib/refcount.c, include/linux/kobject.h.
종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
개요: 참조 카운팅이 왜 필요한가
커널에서 동적으로 할당된 객체는 여러 서브시스템에서 동시에 참조될 수 있습니다. 예를 들어, 하나의 struct inode는 여러 프로세스의 struct file에서 참조되고, 동시에 페이지 캐시(Page Cache)와 디렉토리 엔트리(dentry)에서도 참조됩니다. 이때 "언제 이 객체를 안전하게 해제할 수 있는가?"라는 질문에 답하는 것이 참조 카운팅입니다.
Use-After-Free 문제
참조 카운팅 없이 객체를 관리하면 심각한 보안 취약점인 Use-After-Free(UAF)가 발생할 수 있습니다.
참조 카운팅은 이 문제를 근본적으로 해결합니다. Thread B가 객체를 사용하기 전에 참조 카운트를 증가시키면, Thread A가 kfree를 호출하더라도 카운트가 0이 아니므로 실제 해제가 일어나지 않습니다. Thread B가 사용을 마치고 참조를 해제하면 그때 비로소 카운트가 0이 되어 안전하게 해제됩니다.
참조 카운팅 vs 가비지 컬렉션
사용자 공간 언어(Java, Go, Python)는 가비지 컬렉터(GC)로 메모리를 자동 관리하지만, 커널에서는 결정적(Deterministic) 해제가 필요합니다.
| 특성 | 참조 카운팅 (커널) | 가비지 컬렉션 (유저스페이스) |
|---|---|---|
| 해제 시점 | 카운트 0 즉시 | GC 실행 시 (비결정적) |
| 지연 시간(Latency) | 예측 가능 | GC pause 발생 가능 |
| 순환 참조 | 수동 처리 필요 | 자동 탐지 (mark-sweep) |
| 오버헤드 | inc/dec 원자 연산 | GC 스캔, 메모리 overhead |
| 커널 적합성 | 높음 (실시간, IRQ 컨텍스트) | 부적합 (stop-the-world) |
커널에서 참조 카운팅을 사용하는 이유는 명확합니다. 인터럽트 핸들러(Interrupt Handler), softirq, 실시간 태스크(Task) 등에서 GC pause는 허용되지 않습니다. 참조 카운팅은 각 put 호출 시 상수 시간(O(1))으로 해제 여부를 결정하므로, 실시간 보장에 적합합니다.
참조 카운팅의 기본 원칙
| 동작 | 의미 | API |
|---|---|---|
| 초기화(Init) | 객체 생성 시 카운트를 1로 설정 | kref_init() / refcount_set() |
| 획득(Get) | 객체 사용 시작 — 카운트 +1 | kref_get() / refcount_inc() |
| 해제(Put) | 객체 사용 종료 — 카운트 -1 | kref_put() / refcount_dec_and_test() |
| 소멸(Release) | 카운트가 0에 도달하면 release 콜백 실행 | 사용자 정의 release 함수 |
| 조건부 획득 | 카운트가 이미 0이면 획득 실패 | kref_get_unless_zero() / refcount_inc_not_zero() |
kref 구조체
struct kref는 리눅스 커널에서 참조 카운팅을 위한 표준 래퍼입니다. include/linux/kref.h에 정의되어 있으며, 내부적으로 refcount_t를 감싸는 단순한 구조체입니다.
구조체 정의
/* include/linux/kref.h */
struct kref {
refcount_t refcount;
};
코드 설명
- refcount_t refcount
kref의 유일한 멤버입니다. 이전 커널 버전(v4.11 이전)에서는atomic_t를 직접 사용했지만, 보안 강화를 위해refcount_t로 전환되었습니다.refcount_t는 saturation 보호를 제공하여 오버플로/언더플로 시 커널 경고(WARN)를 발생시킵니다.
kref는 단독으로 사용되지 않고, 반드시 관리 대상 객체의 구조체 안에 임베딩(Embedding)하여 사용합니다.
/* 사용 예: 커스텀 디바이스 구조체 */
struct my_device {
struct kref kref;
char name[64];
struct list_head list;
void *private_data;
};
kref_init
/* include/linux/kref.h */
static inline void kref_init(struct kref *kref)
{
refcount_set(&kref->refcount, 1);
}
코드 설명
- refcount_set(&kref->refcount, 1)참조 카운트를 1로 설정합니다. 0이 아닌 1로 초기화하는 이유는, 객체를 생성한 코드가 이미 하나의 참조를 보유하고 있기 때문입니다. 이것은 "생성자 규칙"으로, 생성자는 반드시 자신의 참조에 대해 나중에
kref_put을 호출해야 합니다.
kref_get
/* include/linux/kref.h */
static inline void kref_get(struct kref *kref)
{
refcount_inc(&kref->refcount);
}
코드 설명
- refcount_inc(&kref->refcount)참조 카운트를 원자적으로 1 증가시킵니다. 중요: 이 함수는 참조 카운트가 이미 0인 경우(객체가 해제 중인 경우) WARN을 발생시킵니다. 0에서 1로의 전환은 "이미 죽은 객체의 부활"을 의미하므로 버그입니다. 카운트가 0일 수 있는 상황에서는
kref_get_unless_zero를 사용해야 합니다.
kref_put
/* include/linux/kref.h */
static inline int kref_put(struct kref *kref,
void (*release)(struct kref *kref))
{
if (refcount_dec_and_test(&kref->refcount)) {
release(kref);
return 1;
}
return 0;
}
코드 설명
- refcount_dec_and_test참조 카운트를 원자적으로 1 감소시킨 후, 결과가 0이면
true를 반환합니다. "test"는 "0인지 테스트(Test)"라는 의미입니다. - release(kref)카운트가 0에 도달하면 사용자가 제공한 release 콜백을 호출합니다. 이 콜백 안에서
container_of로 부모 구조체를 찾아kfree등으로 메모리를 해제합니다. release 함수는 반드시 한 번만 호출되는 것이 보장됩니다. - return 1 / return 0release가 호출되었으면 1, 아니면 0을 반환합니다. 호출자는 이 값으로 객체가 해제되었는지 판단할 수 있습니다.
kref_put_mutex
/* include/linux/kref.h */
static inline int kref_put_mutex(struct kref *kref,
void (*release)(struct kref *kref),
struct mutex *lock)
{
if (refcount_dec_and_mutex_lock(&kref->refcount, lock)) {
release(kref);
return 1;
}
return 0;
}
코드 설명
- refcount_dec_and_mutex_lock카운트를 감소시키되, 결과가 0이 되면 지정된 mutex를 획득한 후
true를 반환합니다. 이는 release 과정에서 리스트(List)에서 객체를 제거하는 등의 동기화가 필요할 때 사용합니다. mutex를 먼저 잡고 카운트를 감소시키는 것보다 효율적인데, 대부분의 경우 카운트가 0이 아니므로 mutex를 잡을 필요가 없기 때문입니다.
kref_get_unless_zero
/* include/linux/kref.h */
static inline int __must_check kref_get_unless_zero(struct kref *kref)
{
return refcount_inc_not_zero(&kref->refcount);
}
코드 설명
- refcount_inc_not_zero참조 카운트가 0이 아닌 경우에만 1 증가시키고
true를 반환합니다. 카운트가 이미 0이면(객체가 해제 중이면) 아무것도 하지 않고false를 반환합니다. 이 함수는 RCU와 함께 사용되는 "lookup-and-get" 패턴에서 핵심적인 역할을 합니다. - __must_check반환값을 반드시 확인해야 함을 컴파일러(Compiler)에 알립니다. 반환값을 무시하면 컴파일 경고가 발생합니다. 이미 해제 중인 객체를 사용하는 버그를 방지합니다.
refcount_t 구조체
refcount_t는 atomic_t를 대체하기 위해 도입된 참조 카운터 전용 타입입니다. 단순 정수 래퍼처럼 보이지만, 컴파일 타임(Compile Time)과 런타임(Runtime) 모두에서 보호 메커니즘을 제공합니다.
구조체 정의
/* include/linux/refcount.h */
typedef struct refcount_struct {
atomic_t refs;
} refcount_t;
코드 설명
- atomic_t refs내부적으로
atomic_t를 사용하지만,refcount_t와atomic_t는 의도적으로 타입이 다릅니다. 이렇게 함으로써atomic_inc(&ref->refs)처럼 보호 없이 직접 접근하는 코드를 컴파일 에러(Error)로 잡아냅니다.refcount_t전용 API만 사용해야 합니다.
atomic_t에서 refcount_t로의 전환 이유
커널 v4.11(2017년)에서 refcount_t가 도입된 배경에는 심각한 보안 문제가 있었습니다.
| 문제 | atomic_t | refcount_t |
|---|---|---|
| 오버플로(Overflow) | INT_MAX → 0 wrap-around, 즉시 해제 유발 | REFCOUNT_SATURATED에서 멈춤 (포화) |
| 언더플로(Underflow) | 0 → -1, 또다시 0 도달 시 이중 해제 | 0 이하로 감소 시 WARN + 값 고정 |
| 0→1 전환 | 탐지 불가 — 죽은 객체 부활 | WARN + 증가 거부 |
| 타입 안전성 | 범용 정수 — 용도 구분 불가 | 전용 타입 — atomic_inc 직접 사용 불가 |
| CVE 사례 | CVE-2016-0728 (keyring), CVE-2016-4558 (eBPF) | 이러한 공격 벡터 차단 |
atomic_t로 구현된 참조 카운터가 오버플로되어, 공격자가 의도적으로 카운트를 INT_MAX까지 증가시킨 후 wrap-around로 0을 만들어 임의 코드를 실행할 수 있었습니다. refcount_t의 saturation 보호가 있었다면 이 공격은 불가능했습니다.
Saturation 보호 메커니즘
refcount_t의 핵심 보호 메커니즘은 saturation(포화)입니다. 카운트가 비정상적인 값에 도달하면 더 이상 변경을 허용하지 않고 경고를 발생시킵니다.
/* include/linux/refcount.h */
#define REFCOUNT_INIT(n) { .refs = ATOMIC_INIT(n), }
#define REFCOUNT_MAX INT_MAX
#define REFCOUNT_SATURATED (INT_MIN / 2)
코드 설명
- REFCOUNT_SATURATED = INT_MIN / 2saturation 임계값은
INT_MIN / 2(약 -1,073,741,824)입니다. 이 값은 의도적으로 정상 범위(1~INT_MAX)와 크게 떨어져 있어, 비정상적인 연산이 감지되면 즉시 이 값으로 고정됩니다. 고정된 후에는 어떤 inc/dec 연산도 값을 변경하지 않으므로, 객체가 영원히 해제되지 않습니다(메모리 누수). 이는 의도적인 설계로, crash(해제 후 사용)보다 leak(영구 보존)이 안전하기 때문입니다.
refcount_t API
refcount_t는 include/linux/refcount.h에 다양한 API를 제공합니다. 각 함수는 saturation 보호 로직이 내장되어 있습니다.
기본 API
/* 초기화 */
#define REFCOUNT_INIT(n) { .refs = ATOMIC_INIT(n) }
static inline void refcount_set(refcount_t *r, int n)
{
atomic_set(&r->refs, n);
}
/* 읽기 */
static inline unsigned int refcount_read(const refcount_t *r)
{
return atomic_read(&r->refs);
}
refcount_inc / refcount_inc_not_zero
/* lib/refcount.c — 간소화된 핵심 로직 */
void refcount_inc(refcount_t *r)
{
int old = atomic_fetch_add_relaxed(1, &r->refs);
/* old가 0이면 이미 해제된 객체에 대한 inc — 버그 */
if (unlikely(!old))
refcount_warn_saturate(r, REFCOUNT_ADD_UAF);
/* old가 음수면 이미 saturated — 변경 없이 유지 */
else if (unlikely(old < 0 || old + 1 < 0))
refcount_warn_saturate(r, REFCOUNT_ADD_OVF);
}
bool refcount_inc_not_zero(refcount_t *r)
{
int old = atomic_read(&r->refs);
do {
if (!old)
return false;
} while (!atomic_try_cmpxchg_relaxed(&r->refs, &old, old + 1));
if (unlikely(old < 0 || old + 1 < 0))
refcount_warn_saturate(r, REFCOUNT_ADD_OVF);
return true;
}
코드 설명
- atomic_fetch_add_relaxed(1, &r->refs)relaxed 메모리 순서로 원자적 덧셈을 수행합니다. 참조 카운팅에서 inc는 acquire 의미론이 필요 없으므로 relaxed로 충분합니다. 반환값은 이전 값(old)입니다.
- !old (REFCOUNT_ADD_UAF)이전 값이 0이면 이미 해제된 객체에 대한 참조 증가를 의미합니다. Use-After-Free 버그이므로
refcount_warn_saturate가 WARN을 발생시키고 카운트를 REFCOUNT_SATURATED로 고정합니다. - atomic_try_cmpxchg_relaxed
refcount_inc_not_zero는 CAS(Compare-And-Swap) 루프를 사용합니다. old가 0이면 즉시 false를 반환하고, 0이 아니면 old+1로 교체를 시도합니다. 경합(Contention)이 있으면 루프를 재시도합니다.
refcount_dec_and_test / refcount_dec_and_lock
/* lib/refcount.c — 간소화된 핵심 로직 */
bool refcount_dec_and_test(refcount_t *r)
{
int old = atomic_fetch_sub_release(1, &r->refs);
if (old == 1) {
smp_acquire__after_ctrl_dep();
return true;
}
if (unlikely(old <= 0)) {
/* old == 0이면 이미 해제된 객체, old < 0이면 saturated */
refcount_warn_saturate(r, REFCOUNT_SUB_UAF);
return false;
}
return false;
}
/* release와 동시에 spinlock 획득 */
bool refcount_dec_and_lock(refcount_t *r, spinlock_t *lock)
{
if (refcount_dec_not_one(r))
return false;
spin_lock(lock);
if (!refcount_dec_and_test(r)) {
spin_unlock(lock);
return false;
}
return true;
}
코드 설명
- atomic_fetch_sub_release(1, &r->refs)release 메모리 순서로 원자적 뺄셈을 수행합니다. release 의미론은 이 연산 이전의 모든 메모리 접근이 다른 CPU에서 보이도록 보장합니다. 이는 객체 해제 전에 객체에 대한 모든 수정이 완료되었음을 보장하기 위해 필수적입니다.
- smp_acquire__after_ctrl_dep()카운트가 0에 도달한 경우에만 실행됩니다. acquire 배리어(Barrier)를 추가하여, 이후의 release 콜백에서 객체 멤버에 접근할 때 다른 CPU의 수정 사항이 모두 보이도록 합니다. release + acquire 쌍이 완전한 메모리 순서 보장을 형성합니다.
- refcount_dec_and_lock참조 카운트가 1보다 크면 락(Lock) 없이 빠르게 감소시킵니다(
refcount_dec_not_one). 카운트가 1인 경우에만 spinlock을 획득한 후 최종 감소를 수행합니다. 이 2단계 최적화는 대부분의 put 호출에서 불필요한 락 경합을 피합니다.
API 전체 요약
| 함수 | 동작 | 실패 시 | 메모리 순서 |
|---|---|---|---|
refcount_set(r, n) | 값을 n으로 설정 | — | none (초기화 전용) |
refcount_read(r) | 현재 값 읽기 | — | none |
refcount_inc(r) | count++ | 0→1: WARN + saturate | relaxed |
refcount_inc_not_zero(r) | 0이 아니면 count++ | false 반환 | relaxed |
refcount_dec_and_test(r) | count--, 0이면 true | underflow: WARN + saturate | release; acquire on true |
refcount_dec_and_lock(r, lock) | count--, 0이면 lock 획득 + true | underflow: WARN + saturate | release + spin_lock |
refcount_dec_and_mutex_lock(r, m) | count--, 0이면 mutex 획득 + true | underflow: WARN + saturate | release + mutex_lock |
refcount_dec_not_one(r) | 1보다 크면 count-- | false (1 이하일 때) | relaxed |
refcount_dec_if_one(r) | 정확히 1이면 0으로 | false (1 아닐 때) | release + acquire |
kref vs atomic_t vs refcount_t 비교
세 가지 참조 카운팅 방법의 차이를 명확히 비교합니다.
| 특성 | atomic_t | refcount_t | kref |
|---|---|---|---|
| 헤더 | linux/atomic.h | linux/refcount.h | linux/kref.h |
| 초기화 | atomic_set(&v, 1) | refcount_set(&r, 1) | kref_init(&k) |
| 증가 | atomic_inc(&v) | refcount_inc(&r) | kref_get(&k) |
| 감소+테스트 | atomic_dec_and_test(&v) | refcount_dec_and_test(&r) | kref_put(&k, release) |
| 조건부 증가 | atomic_inc_not_zero(&v) | refcount_inc_not_zero(&r) | kref_get_unless_zero(&k) |
| 오버플로 보호 | 없음 | saturation + WARN | refcount_t에 위임 |
| release 콜백 | 수동 구현 | 수동 구현 | kref_put에 콜백 전달 |
| 권장 용도 | 범용 카운터 (비-참조) | 단순 참조 카운터 | 커널 객체 수명 관리 |
생명주기 패턴
참조 카운팅 기반의 객체 생명주기는 "생성 → 참조 획득 → 사용 → 참조 해제 → 소멸"의 5단계를 따릅니다.
release 콜백 패턴
struct my_device {
struct kref kref;
char *name;
struct list_head list;
};
/* release 콜백 — kref_put에서 refcount가 0이 될 때 호출됩니다 */
static void my_device_release(struct kref *kref)
{
struct my_device *dev = container_of(kref, struct my_device, kref);
pr_info("releasing device %s\n", dev->name);
kfree(dev->name);
kfree(dev);
}
/* 생성 */
struct my_device *my_device_create(const char *name)
{
struct my_device *dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return NULL;
dev->name = kstrdup(name, GFP_KERNEL);
if (!dev->name) {
kfree(dev);
return NULL;
}
kref_init(&dev->kref); /* refcount = 1 */
return dev;
}
/* 참조 획득 */
struct my_device *my_device_get(struct my_device *dev)
{
if (dev)
kref_get(&dev->kref);
return dev;
}
/* 참조 해제 */
void my_device_put(struct my_device *dev)
{
if (dev)
kref_put(&dev->kref, my_device_release);
}
코드 설명
- container_of(kref, struct my_device, kref)release 콜백은
struct kref *를 인자로 받으므로,container_of매크로로 부모 구조체 포인터를 얻습니다. 이는 커널에서 C 객체지향(OOP) 패턴의 핵심입니다. - kfree(dev->name); kfree(dev)release 콜백에서는 객체가 소유한 모든 자원을 해제합니다. 동적 할당된 멤버를 먼저 해제하고, 마지막에 구조체 자체를 해제합니다. 순서가 중요합니다.
- my_device_get / my_device_put커널의 관례적인 래퍼 패턴입니다.
kobject_get/kobject_put,get_device/put_device,fget/fput등이 모두 이 패턴을 따릅니다. NULL 포인터 안전성을 래퍼에서 처리합니다.
RCU와 참조 카운터
RCU(Read-Copy-Update)와 참조 카운터의 조합은 커널에서 가장 중요한 동시성 패턴 중 하나입니다. 이 패턴은 "잠금(Lock) 없는 읽기 경로에서 안전하게 객체 참조를 획득"하는 문제를 해결합니다.
문제: RCU 읽기 구간 밖에서의 객체 접근
RCU는 읽기 측(Read-side)이 잠금 없이 데이터를 읽을 수 있게 하지만, rcu_read_lock()/rcu_read_unlock() 구간 밖에서는 객체가 이미 해제되었을 수 있습니다. 따라서 RCU 읽기 구간 안에서 객체를 찾아 참조 카운트를 증가시킨 후, RCU 구간 밖에서 안전하게 사용해야 합니다.
RCU + kref 코드 패턴
/* 안전한 RCU lookup + 참조 획득 패턴 */
struct my_device *find_device_by_id(int id)
{
struct my_device *dev;
rcu_read_lock();
list_for_each_entry_rcu(dev, &device_list, list) {
if (dev->id == id) {
/* 핵심: 0이 아닌 경우에만 참조 획득 */
if (!kref_get_unless_zero(&dev->kref)) {
dev = NULL; /* 이미 해제 중 */
}
rcu_read_unlock();
return dev;
}
}
rcu_read_unlock();
return NULL;
}
/* 객체 제거 (writer 측) */
void remove_device(struct my_device *dev)
{
spin_lock(&device_lock);
list_del_rcu(&dev->list);
spin_unlock(&device_lock);
/* writer의 참조 해제 — 다른 reader가 get에 성공했으면 아직 해제되지 않습니다 */
kref_put(&dev->kref, my_device_release);
}
코드 설명
- rcu_read_lock() ... rcu_read_unlock()RCU 읽기 구간입니다. 이 구간 안에서는 RCU로 보호되는 리스트 노드가 해제되지 않음이 보장됩니다. 그러나 노드가 "논리적으로 삭제"(리스트에서 제거)되었을 수 있으므로, 참조 카운트가 이미 0일 수 있습니다.
- kref_get_unless_zero카운트가 0이 아니면 증가시키고 true를 반환합니다. 0이면 false를 반환하는데, 이는 다른 스레드(Thread)가 이미 마지막 참조를 해제하여 객체가 소멸 중임을 의미합니다. 이 경우 NULL을 반환하여 호출자에게 "객체 없음"을 알립니다.
- list_del_rcu리스트에서 노드를 제거하되, RCU reader가 아직 이 노드를 순회 중일 수 있으므로 즉시 해제하지 않습니다.
next포인터만 변경하고,prev는 그대로 둡니다.
kref_get과 kref_get_unless_zero를 혼동하면 안 됩니다. kref_get은 참조 카운트가 0이 아님을 확신할 때만 사용합니다 (예: 이미 참조를 보유한 상태에서 추가 참조 획득). RCU lookup처럼 카운트가 0일 수 있는 상황에서는 반드시 kref_get_unless_zero를 사용해야 합니다.
커널 활용 사례
리눅스 커널의 주요 서브시스템은 참조 카운팅을 광범위하게 사용합니다. 대표적인 사례를 살펴봅니다.
kobject (sysfs)
/* include/linux/kobject.h */
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
const struct kobj_type *ktype;
struct kernfs_node *sd;
struct kref kref; /* ← kref 임베딩 */
unsigned int state_initialized:1;
unsigned int state_in_sysfs:1;
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
unsigned int uevent_suppress:1;
};
/* lib/kobject.c */
struct kobject *kobject_get(struct kobject *kobj)
{
if (kobj) {
if (!kobj->state_initialized)
WARN(1, "kobject: '%s' is not initialized, yet kobject_get() is called\n",
kobject_name(kobj));
kref_get(&kobj->kref);
}
return kobj;
}
EXPORT_SYMBOL(kobject_get);
void kobject_put(struct kobject *kobj)
{
if (kobj) {
if (!kobj->state_initialized)
WARN(1, "kobject: '%s' is not initialized, yet kobject_put() is called\n",
kobject_name(kobj));
kref_put(&kobj->kref, kobject_release);
}
}
EXPORT_SYMBOL(kobject_put);
struct device
/* drivers/base/core.c */
struct device *get_device(struct device *dev)
{
return dev ? to_dev(kobject_get(&dev->kobj)) : NULL;
}
EXPORT_SYMBOL_GPL(get_device);
void put_device(struct device *dev)
{
if (dev)
kobject_put(&dev->kobj);
}
EXPORT_SYMBOL_GPL(put_device);
코드 설명
- get_device / put_device
struct device는 내부에struct kobject kobj를 가지고 있고, kobject가 kref를 가지고 있습니다. 따라서get_device는kobject_get을 호출하고, 이것이 다시kref_get을 호출하는 3단계 위임 체인입니다.
struct sk_buff
/* include/linux/skbuff.h */
struct sk_buff {
/* ... */
refcount_t users; /* sk_buff 자체의 참조 카운트 */
};
/* net/core/skbuff.c */
static inline struct sk_buff *skb_get(struct sk_buff *skb)
{
refcount_inc(&skb->users);
return skb;
}
void kfree_skb(struct sk_buff *skb)
{
if (!skb)
return;
if (likely(refcount_read(&skb->users) == 1))
slab_free_after_rcu_gp(skb_free_head, skb);
else if (likely(!refcount_dec_and_test(&skb->users)))
return;
else
__kfree_skb(skb);
}
struct file
/* include/linux/fs.h */
struct file {
/* ... */
atomic_long_t f_count; /* 참조 카운트 */
/* ... */
};
/* fs/file_table.c */
struct file *get_file(struct file *f)
{
atomic_long_inc(&f->f_count);
return f;
}
/* fget — fd 테이블에서 file 획득 (RCU 기반) */
struct file *fget(unsigned int fd)
{
struct file *file;
rcu_read_lock();
file = fcheck_files(current->files, fd);
if (file && !atomic_long_inc_not_zero(&file->f_count))
file = NULL;
rcu_read_unlock();
return file;
}
코드 설명
- fget
fget은 RCU + 조건부 참조 획득의 대표적인 사례입니다. fd 테이블을 RCU로 보호하면서,atomic_long_inc_not_zero로 file의 f_count가 0이 아닌 경우에만 참조를 획득합니다.struct file은 역사적으로atomic_long_t를 사용하지만, 새 코드에서는refcount_t가 권장됩니다.
struct dentry
/* include/linux/dcache.h */
struct dentry {
unsigned int d_flags;
seqcount_spinlock_t d_seq;
struct hlist_bl_node d_hash;
struct dentry *d_parent;
struct qstr d_name;
struct inode *d_inode;
unsigned char d_iname[DNAME_INLINE_LEN];
lockref_t d_lockref; /* 참조 카운트 + spinlock 통합 */
/* ... */
};
struct dentry는 lockref_t를 사용합니다. lockref_t는 참조 카운트와 spinlock을 단일 64비트(Bit) 워드에 통합하여, dget/dput 시 cmpxchg 한 번으로 카운트 증감과 동기화를 동시에 수행하는 최적화입니다.
kobject와 kref
kobject는 kref의 가장 대표적이고 중요한 사용자입니다. 커널의 전체 디바이스 모델(Device Model)은 kobject 트리 위에 구축되며, 이 트리의 각 노드는 kref로 수명이 관리됩니다.
kobject 생명주기와 kref
/* lib/kobject.c — kobject_release (간소화) */
static void kobject_release(struct kref *kref)
{
struct kobject *kobj = container_of(kref, struct kobject, kref);
/* 지연 해제가 필요하면 workqueue에 위임합니다 */
if (kobj->state_in_sysfs) {
kobject_uevent(kobj, KOBJ_REMOVE);
sysfs_remove_groups(kobj, kobj->ktype->default_groups);
sysfs_remove_dir(kobj);
}
/* ktype의 release 콜백 호출 — 실제 메모리 해제 */
if (kobj->ktype && kobj->ktype->release)
kobj->ktype->release(kobj);
/* 부모 kobject 참조 해제 — 재귀적 해제 가능 */
if (kobj->parent)
kobject_put(kobj->parent);
}
kobject_release에서 부모의 kobject_put을 호출합니다. 부모의 참조 카운트도 0이 되면 부모의 kobject_release가 호출되고, 그 부모의 kobject_put을 호출합니다. 이렇게 트리의 리프(Leaf)부터 루트까지 재귀적으로 해제될 수 있습니다. 이것이 sysfs 트리의 정리 메커니즘입니다.
디버깅
참조 카운터 관련 버그는 재현이 어렵고 디버깅이 까다로운 것으로 유명합니다. 커널은 여러 도구를 제공하여 이러한 버그를 탐지합니다.
refcount_t saturation WARN
refcount_t가 비정상적인 상태를 탐지하면 다음과 같은 WARN 메시지를 출력합니다.
refcount_t: addition on 0; use-after-free.
WARNING: CPU: 2 PID: 1234 at lib/refcount.c:25 refcount_warn_saturate+0x65/0x80
Call Trace:
refcount_inc+0x3a/0x40
my_device_get+0x15/0x20 [my_module]
my_work_handler+0x42/0x80 [my_module]
refcount_t: underflow; use-after-free.
WARNING: CPU: 0 PID: 5678 at lib/refcount.c:28 refcount_warn_saturate+0x80/0x80
Call Trace:
refcount_dec_and_test+0x35/0x50
my_device_put+0x18/0x30 [my_module]
my_cleanup+0x25/0x40 [my_module]
KASAN (Kernel Address Sanitizer)
KASAN은 Use-After-Free를 메모리 접근 시점에서 탐지합니다. refcount_t saturation이 "카운터 오류"를 탐지한다면, KASAN은 "실제로 해제된 메모리에 접근했는가"를 탐지합니다.
# 참조 카운터 디버깅 관련 커널 설정
CONFIG_KASAN=y # 메모리 접근 오류 탐지
CONFIG_KASAN_GENERIC=y # 소프트웨어 기반 (더 느리지만 정확)
CONFIG_DEBUG_OBJECTS=y # 객체 상태 추적
CONFIG_DEBUG_OBJECTS_FREE=y # 해제된 객체 접근 탐지
CONFIG_PROVE_LOCKING=y # lockdep — 잠금 순서 검증
CONFIG_REFCOUNT_FULL (역사적 맥락)
커널 v4.12~v5.4에서는 CONFIG_REFCOUNT_FULL 옵션이 있었습니다. 이 옵션이 꺼져 있으면 refcount_t가 atomic_t와 동일하게 동작했고(보호 없음), 켜져 있을 때만 saturation 보호가 활성화되었습니다.
커널 v5.5(2020년 1월)부터 Peter Zijlstra의 패치로 saturation 보호가 항상 활성화되었고, 성능 오버헤드가 x86에서 0%로 최적화되었습니다. 이후 CONFIG_REFCOUNT_FULL 옵션은 제거되었습니다.
/* 디버깅 유틸리티: 현재 참조 카운트 출력 */
static void debug_print_refcount(struct my_device *dev, const char *ctx)
{
pr_debug("%s: device '%s' refcount=%u\n",
ctx, dev->name,
refcount_read(&dev->kref.refcount));
}
/* WARN_ON을 활용한 참조 카운트 검증 */
void my_device_validate(struct my_device *dev)
{
unsigned int count = refcount_read(&dev->kref.refcount);
/* 카운트가 비정상적으로 높으면 leak 가능성 */
WARN_ON(count > 1000);
/* 카운트가 0이면 이미 해제 중 — 접근하면 안 됩니다 */
WARN_ON(!count);
}
ftrace로 참조 카운트 추적
# kobject_get/kobject_put 호출 추적
echo 1 > /sys/kernel/debug/tracing/events/kobject/kobject_get/enable
echo 1 > /sys/kernel/debug/tracing/events/kobject/kobject_put/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 특정 함수의 호출 스택 추적
echo 'refcount_warn_saturate' > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
atomic_t → refcount_t 마이그레이션
커널에서 atomic_t를 참조 카운터로 사용하는 코드를 refcount_t로 전환하는 작업은 2017년부터 시작되어 수년에 걸쳐 진행되었습니다.
전환 타임라인
| 시기 | 커널 버전 | 이정표 |
|---|---|---|
| 2016.01 | v4.4 | CVE-2016-0728 (keyring refcount overflow) 발견 |
| 2017.02 | v4.11 | refcount_t 타입 및 API 도입 (Peter Zijlstra, Elena Reshetova) |
| 2017.05 | v4.12 | CONFIG_REFCOUNT_FULL 옵션 추가 |
| 2017~2019 | v4.12~v5.4 | Coccinelle 스크립트 기반 대량 변환 진행 |
| 2019.11 | v5.5 | saturation 보호 항상 활성화, 성능 최적화 |
| 2020~현재 | v5.5+ | CONFIG_REFCOUNT_FULL 제거, 잔여 atomic_t 지속 전환 |
Coccinelle 변환 스크립트
커널 트리에는 atomic_t를 refcount_t로 자동 변환하는 Coccinelle(시맨틱 패치) 스크립트가 포함되어 있습니다.
// scripts/coccinelle/api/atomic_as_refcounter.cocci (간소화)
// atomic_t를 refcount 용도로 사용하는 패턴 탐지
@@
identifier a, x;
identifier fname =~ ".*_get";
@@
fname(...) {
...
- atomic_inc(&x->a);
+ refcount_inc(&x->a);
...
}
@@
identifier a, x;
identifier fname =~ ".*_put";
@@
fname(...) {
...
- if (atomic_dec_and_test(&x->a))
+ if (refcount_dec_and_test(&x->a))
...
}
# Coccinelle 스크립트 실행 (탐지만)
spatch --sp-file scripts/coccinelle/api/atomic_as_refcounter.cocci \
--dir drivers/usb/ --mode=report
# 자동 변환 적용
spatch --sp-file scripts/coccinelle/api/atomic_as_refcounter.cocci \
--dir drivers/usb/ --mode=patch --in-place
수동 마이그레이션 가이드
/* 변환 전 (atomic_t 기반) */
struct my_obj {
atomic_t refcnt;
};
static void obj_get(struct my_obj *obj)
{
atomic_inc(&obj->refcnt);
}
static void obj_put(struct my_obj *obj)
{
if (atomic_dec_and_test(&obj->refcnt))
kfree(obj);
}
/* ──────────────────────────────────── */
/* 변환 후 (refcount_t 기반) */
struct my_obj {
refcount_t refcnt;
};
static void obj_get(struct my_obj *obj)
{
refcount_inc(&obj->refcnt);
}
static void obj_put(struct my_obj *obj)
{
if (refcount_dec_and_test(&obj->refcnt))
kfree(obj);
}
atomic_read→refcount_readatomic_set→refcount_set(초기화만)atomic_inc→refcount_incatomic_dec_and_test→refcount_dec_and_testatomic_inc_not_zero→refcount_inc_not_zeroatomic_add/atomic_sub등 참조 카운팅과 무관한 연산은 변환 대상이 아닙니다- 참조 카운터가 아닌 범용 카운터(통계, 인덱스 등)는
atomic_t를 유지해야 합니다
흔한 실수와 방지
참조 카운팅 코드에서 자주 발생하는 실수와 그 방지 방법을 정리합니다.
실수 1: Double-Free (이중 해제)
/* ❌ 잘못된 코드: kref_put을 두 번 호출 */
void bad_cleanup(struct my_device *dev)
{
kref_put(&dev->kref, my_device_release);
/* ... 다른 정리 작업 ... */
kref_put(&dev->kref, my_device_release); /* 💥 두 번째 put — 이미 해제됨! */
}
/* ✅ 올바른 코드: get/put 쌍을 정확히 맞춤 */
void good_cleanup(struct my_device *dev)
{
/* 이 함수가 보유한 참조 1개만 해제 */
kref_put(&dev->kref, my_device_release);
/* 이후 dev 포인터를 사용하지 않음 */
}
실수 2: 조건부 Get 누락
/* ❌ 잘못된 코드: RCU 구간에서 kref_get 사용 */
struct my_device *bad_lookup(int id)
{
struct my_device *dev;
rcu_read_lock();
list_for_each_entry_rcu(dev, &list, node) {
if (dev->id == id) {
kref_get(&dev->kref); /* 💥 refcount가 0일 수 있음! */
rcu_read_unlock();
return dev;
}
}
rcu_read_unlock();
return NULL;
}
/* ✅ 올바른 코드: kref_get_unless_zero 사용 */
struct my_device *good_lookup(int id)
{
struct my_device *dev;
rcu_read_lock();
list_for_each_entry_rcu(dev, &list, node) {
if (dev->id == id) {
if (!kref_get_unless_zero(&dev->kref))
dev = NULL;
rcu_read_unlock();
return dev;
}
}
rcu_read_unlock();
return NULL;
}
실수 3: Release 콜백 내 잘못된 접근
/* ❌ 잘못된 코드: release 후 멤버 접근 */
static void bad_release(struct kref *kref)
{
struct my_device *dev = container_of(kref, struct my_device, kref);
kfree(dev);
pr_info("freed %s\n", dev->name); /* 💥 이미 해제된 메모리 접근! */
}
/* ✅ 올바른 코드: kfree 전에 필요한 작업 완료 */
static void good_release(struct kref *kref)
{
struct my_device *dev = container_of(kref, struct my_device, kref);
pr_info("freeing %s\n", dev->name); /* kfree 전에 접근 */
kfree(dev->name);
kfree(dev);
}
실수 4: 에러 경로에서 put 누락 (참조 누수)
/* ❌ 잘못된 코드: 에러 경로에서 kref_put 빠짐 */
int bad_use_device(struct my_device *dev)
{
kref_get(&dev->kref);
if (do_something(dev) < 0)
return -EIO; /* 💥 kref_put 없이 반환 — 영원히 해제되지 않음! */
kref_put(&dev->kref, my_device_release);
return 0;
}
/* ✅ 올바른 코드: 모든 경로에서 put 보장 */
int good_use_device(struct my_device *dev)
{
int ret;
kref_get(&dev->kref);
ret = do_something(dev);
kref_put(&dev->kref, my_device_release);
return ret;
}
실수 요약 표
| 실수 유형 | 증상 | 탐지 도구 | 방지 방법 |
|---|---|---|---|
| Double-free | 커널 패닉, KASAN UAF | KASAN, refcount_t WARN | get/put 쌍 엄격 관리 |
| 조건부 get 누락 | 0→1 전환 WARN | refcount_t saturation | kref_get_unless_zero |
| Release 후 접근 | 데이터 손상, KASAN UAF | KASAN | kfree를 마지막에 호출 |
| 에러 경로 put 누락 | 메모리 누수 (kmemleak) | kmemleak | goto cleanup 패턴 |
| 초기화 전 사용 | WARN (uninitialized) | refcount_t WARN | 생성 직후 kref_init |
소스 코드 워크스루
refcount_dec_and_test의 내부 구현을 커널 소스 기반으로 단계별 추적합니다.
refcount_dec_and_test 내부 구현 추적
/* include/linux/refcount.h */
static inline bool __refcount_dec_and_test(
refcount_t *r, int *oldp)
{
int old = atomic_fetch_sub_release(1, &r->refs);
if (oldp)
*oldp = old;
if (old == 1) {
smp_acquire__after_ctrl_dep();
return true;
}
if (unlikely(old <= 0))
refcount_warn_saturate(r, REFCOUNT_SUB_UAF);
return false;
}
static inline bool refcount_dec_and_test(refcount_t *r)
{
return __refcount_dec_and_test(r, NULL);
}
코드 설명
- atomic_fetch_sub_release(1, &r->refs)원자적으로 1을 빼고 이전 값을 반환합니다. release 메모리 순서를 사용하여, 이 연산 이전의 모든 메모리 수정이 다른 CPU에서 보이도록 보장합니다. x86에서는
LOCK XADD명령어로 구현되며, 이미 full barrier이므로 추가 비용이 없습니다. - old == 1 → smp_acquire__after_ctrl_dep()이전 값이 1이었으면 현재 값은 0입니다. 이 시점에서 acquire 배리어를 추가합니다. 왜 별도 함수인가 하면, 조건 분기(
if (old == 1)) 자체가 제어 의존성(control dependency)을 만들어 store 순서를 보장하지만, load 순서는 보장하지 않기 때문입니다.smp_acquire__after_ctrl_dep는 이 gap을 채웁니다. - old <= 0 → REFCOUNT_SUB_UAF이전 값이 0 이하이면 이미 해제된 객체에 대한 감소입니다. 버그이므로 WARN을 발생시키고 카운트를 REFCOUNT_SATURATED로 고정합니다. "SUB_UAF"는 "subtraction causing use-after-free"를 의미합니다.
메모리 순서 보장의 중요성
왜 refcount_dec_and_test에서 release와 acquire 배리어가 모두 필요한지 구체적인 시나리오로 설명합니다.
/* CPU A: 객체 수정 후 참조 해제 */
dev->data = 42; /* (1) store: 데이터 수정 */
kref_put(&dev->kref, release); /* (2) release barrier + dec */
/* CPU B: 마지막 참조 해제 → release 콜백 */
kref_put(&dev->kref, release); /* (3) release + dec → 0! */
/* (3)에서 acquire barrier 추가 */
static void release(struct kref *kref) {
struct my_device *dev = container_of(...);
pr_info("data = %d\n", dev->data); /* (4) load: CPU A의 42가 보여야 함 */
kfree(dev);
}
/*
* release barrier (2): (1)의 store가 (2)의 dec 전에 완료됨을 보장
* acquire barrier (3): (3)의 dec 이후의 (4) load가 (1)의 store를 볼 수 있음을 보장
* 이 두 배리어가 합쳐져, CPU B의 release 콜백에서 CPU A의 수정 사항이 보입니다.
*/
고급 패턴
percpu_ref: 고성능 참조 카운터
일반적인 refcount_t는 모든 CPU가 동일한 캐시라인(Cache Line)을 경합합니다. 매우 빈번한 참조 획득/해제가 있는 경우(예: I/O 경로), percpu_ref가 더 효율적입니다.
/* include/linux/percpu-refcount.h */
struct percpu_ref {
atomic_long_t count;
unsigned long __percpu *percpu_count_ptr;
percpu_ref_func_t *release;
percpu_ref_func_t *confirm_switch;
bool force_atomic:1;
struct rcu_head rcu;
};
/* 사용 예: block I/O */
percpu_ref_init(&q->q_usage_counter, blk_queue_usage_counter_release,
PERCPU_REF_INIT_ATOMIC, GFP_KERNEL);
/* fast path: per-CPU 카운터 증가 (캐시라인 경합 없음) */
percpu_ref_get(&q->q_usage_counter);
/* slow path: kill 시 global atomic으로 전환 */
percpu_ref_kill(&q->q_usage_counter);
코드 설명
- percpu_count_ptr각 CPU마다 별도의 카운터를 유지합니다.
percpu_ref_get은 현재 CPU의 로컬(Local) 카운터만 증가시키므로 캐시라인 바운싱(Cache Line Bouncing)이 없습니다. - percpu_ref_kill해제를 시작할 때 호출합니다. per-CPU 카운터를 모두 합산하여 글로벌(Global)
atomic_long_t count로 전환합니다. 이후의 get/put은 일반 atomic 연산으로 동작합니다. RCU grace period를 사용하여 전환의 안전성을 보장합니다.
devm 리소스와 참조 카운팅
디바이스 드라이버(Device Driver)에서는 devm_* API를 사용하여 디바이스 수명에 자원을 묶을 수 있습니다. 이는 참조 카운팅과 상호보완적입니다.
/* 디바이스 수명에 묶인 자원 할당 */
int my_probe(struct platform_device *pdev)
{
struct my_device *dev;
/* devm_kzalloc: pdev 해제 시 자동으로 kfree됩니다 */
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
/* 주의: devm으로 할당한 객체에 kref를 사용하면
* release 콜백에서 kfree를 호출하면 안 됩니다.
* devm이 이미 해제를 관리하기 때문입니다. */
platform_set_drvdata(pdev, dev);
return 0;
}
아키텍처별 구현 차이
refcount_t의 원자적 연산은 아키텍처마다 다른 명령어로 구현됩니다. 성능 특성과 메모리 순서 보장 방식이 다르므로 이해가 필요합니다.
x86: LOCK 접두사
; refcount_dec_and_test → atomic_fetch_sub_release(1, &r->refs)
; x86에서 LOCK XADD는 이미 full barrier이므로
; release 의미론이 추가 비용 없이 보장됩니다.
lock xadd DWORD PTR [rdi], eax ; 원자적 교환+덧셈
cmp eax, 1 ; 이전 값이 1이었으면
je .Lrelease ; → release 호출
test eax, eax ; 이전 값이 0 이하이면
jle .Lwarn_saturate ; → WARN
ret ; 정상: false 반환
; refcount_inc → atomic_fetch_add_relaxed(1, &r->refs)
lock xadd DWORD PTR [rdi], eax ; 원자적 교환+덧셈
test eax, eax ; 이전 값이 0이면 UAF
je .Lwarn_uaf
코드 설명
- lock xaddx86의
LOCK접두사는 버스 락(Bus Lock) 또는 캐시 락(Cache Lock)을 통해 원자성을 보장합니다.XADD는 메모리 위치의 값을 레지스터와 교환한 후 더합니다. 이 명령어 자체가 full memory barrier이므로 x86에서는 relaxed나 release 의미론에 추가 비용이 없습니다. - refcount_t 오버헤드: 0%x86에서
refcount_t의 saturation 검사는 기존atomic_t연산 후 단순 비교와 조건 분기만 추가합니다. 정상 경로에서 분기는 taken되지 않으므로(branch prediction이 올바르게 예측) 성능 오버헤드가 0%입니다.
ARM64: LSE / LL/SC
// ARM64 LSE (Large System Extensions) 사용 시
// refcount_dec_and_test → atomic_fetch_sub_release
ldaddal w2, w0, [x0] // atomic fetch-add with acquire+release
cmp w0, #1 // 이전 값 == 1?
b.eq release_path // → release 호출
cmp w0, #0 // 이전 값 <= 0?
b.le warn_path // → WARN saturate
// ARM64 LL/SC (LSE 미지원 시)
// refcount_inc_not_zero → CAS 루프
1:
ldxr w1, [x0] // Load-Exclusive: 현재 값 읽기
cbz w1, fail // 0이면 실패
add w2, w1, #1 // +1
stxr w3, w2, [x0] // Store-Exclusive: 조건부 저장
cbnz w3, 1b // 실패 시 재시도
코드 설명
- ldaddal (LSE)ARMv8.1의 LSE(Large System Extensions)는 단일 명령어로 원자적 fetch-add를 수행합니다.
al접미사는 acquire+release 메모리 순서를 의미합니다. LL/SC 루프보다 효율적이며, 특히 경합이 심한 경우 성능이 크게 향상됩니다. - ldxr/stxr (LL/SC)LSE가 없는 ARMv8.0에서는 Load-Exclusive/Store-Exclusive 쌍을 사용합니다.
stxr은ldxr이후 다른 CPU가 해당 주소를 수정하지 않은 경우에만 성공합니다. 실패하면 루프를 재시도합니다. - ARM64에서의 오버헤드ARM64에서는 relaxed와 release/acquire 의미론이 다른 명령어를 사용하므로(x86과 달리),
refcount_t의 saturation 검사에 약간의 추가 비교 명령어가 들어갑니다. 하지만 정상 경로에서 분기가 예측되므로 실제 오버헤드는 미미합니다.
RISC-V: AMO / LR/SC
# RISC-V: refcount_dec_and_test → atomic_fetch_sub_release
# AMO (Atomic Memory Operation) 사용
amoadd.w.rl a0, a1, (a2) # release 순서 atomic add
li t0, 1
beq a0, t0, release # 이전 값 == 1 → release
blez a0, warn # 이전 값 <= 0 → WARN
# RISC-V: refcount_inc_not_zero → LR/SC 루프
1:
lr.w a1, (a0) # Load-Reserved
beqz a1, fail # 0이면 실패
addi a2, a1, 1 # +1
sc.w a3, a2, (a0) # Store-Conditional
bnez a3, 1b # 실패 시 재시도
아키텍처별 비교 요약
| 특성 | x86 | ARM64 (LSE) | ARM64 (LL/SC) | RISC-V |
|---|---|---|---|---|
| inc 명령어 | lock xadd | ldaddal | ldxr/stxr 루프 | amoadd.w |
| dec_and_test | lock xadd + cmp | ldaddal + cmp | ldxr/stxr + cmp | amoadd.w.rl + beq |
| 메모리 모델 | TSO (strong) | Weak | Weak | RVWMO (weak) |
| release 비용 | 0 (implicit) | 접미사 변경 | 별도 dmb | 접미사 .rl |
| refcount_t 오버헤드 | ~0% | ~1% 미만 | ~1% 미만 | ~1% 미만 |
성능 분석
참조 카운팅의 성능 특성을 이해하는 것은 올바른 메커니즘 선택에 중요합니다.
캐시라인 경합 분석
벤치마크 데이터
| 시나리오 | atomic_t | refcount_t | 오버헤드 |
|---|---|---|---|
| 단일 CPU, 비경합 inc | 4.2ns | 4.2ns | 0% |
| 단일 CPU, 비경합 dec_and_test | 4.5ns | 4.6ns | ~2% |
| 4-CPU 경합 inc | 52ns | 53ns | ~2% |
| 4-CPU 경합 dec_and_test | 55ns | 56ns | ~2% |
| 8-CPU NUMA 경합 | 180ns | 183ns | ~1.7% |
핵심 결론: refcount_t의 saturation 보호는 실질적으로 측정 불가능한 수준의 오버헤드만 추가합니다. x86에서는 기존 LOCK XADD 이후 조건 분기 하나가 추가되는 것뿐이며, 분기 예측기(Branch Predictor)가 정상 경로를 정확히 예측하므로 파이프라인(Pipeline) 지연이 발생하지 않습니다.
# cache miss 기반 경합 측정
perf stat -e cache-misses,cache-references,instructions \
-a -- sleep 10
# lock contention 분석 (refcount 관련 함수)
perf lock record -a -- sleep 5
perf lock report
# 특정 함수 호출 빈도 측정
perf probe --add 'refcount_inc'
perf stat -e 'probe:refcount_inc' -a -- sleep 5
# c2c (cache-to-cache) 분석 — 캐시라인 바운싱 탐지
perf c2c record -a -- sleep 10
perf c2c report --stdio
perf c2c는 여러 CPU가 동일 캐시라인을 경합하는 "false sharing" 또는 "true sharing" 핫스팟을 찾아줍니다. refcount_t가 포함된 구조체의 캐시라인이 상위에 나타나면, percpu_ref로의 전환을 고려해야 합니다.
참조 카운터 선택 가이드
| 요구사항 | 권장 메커니즘 | 이유 |
|---|---|---|
| 일반 커널 객체 수명 관리 | kref | release 콜백 패턴, 문서화된 API |
| 단순 참조 카운터 (release 직접 관리) | refcount_t | saturation 보호, 타입 안전성 |
| 고빈도 I/O 경로 (block, 네트워크) | percpu_ref | 캐시라인 경합 제거 |
| VFS dentry (count + lock 통합) | lockref_t | 단일 cmpxchg로 lock+count |
| 범용 카운터 (참조 카운팅 아님) | atomic_t | 통계, 인덱스 등에 적합 |
실전 패턴 모음
커널에서 흔히 사용되는 참조 카운팅 패턴을 모아 정리합니다.
패턴 1: Workqueue와 참조 카운팅
/*
* 워크큐에 작업을 제출할 때, 작업이 완료될 때까지
* 객체가 유효해야 합니다.
*/
void schedule_device_work(struct my_device *dev)
{
/* 작업 제출 전 참조 획득 */
kref_get(&dev->kref);
if (!queue_work(system_wq, &dev->work)) {
/* 이미 큐에 있으면 참조 되돌림 */
kref_put(&dev->kref, my_device_release);
}
}
static void device_work_handler(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, work);
/* 작업 수행 */
do_device_processing(dev);
/* 작업 완료 후 참조 해제 */
kref_put(&dev->kref, my_device_release);
}
패턴 2: 타이머와 참조 카운팅
/*
* 타이머 콜백에서 객체에 접근하려면
* 타이머 활성화 기간 동안 참조를 유지해야 합니다.
*/
void start_device_timer(struct my_device *dev)
{
kref_get(&dev->kref); /* 타이머용 참조 */
mod_timer(&dev->timer, jiffies + HZ);
}
static void device_timer_callback(struct timer_list *t)
{
struct my_device *dev = from_timer(dev, t, timer);
/* 타이머 작업 수행 */
check_device_status(dev);
/* 타이머 재스케줄 또는 참조 해제 */
if (dev->needs_monitoring) {
mod_timer(&dev->timer, jiffies + HZ);
/* 참조 유지 — 다음 콜백에서 해제 */
} else {
kref_put(&dev->kref, my_device_release);
}
}
void stop_device_timer(struct my_device *dev)
{
/* 타이머가 실행 중이면 완료까지 대기 후 삭제 */
if (del_timer_sync(&dev->timer)) {
/* 타이머가 아직 실행 안 됨 → 콜백이 put하지 못하므로 여기서 해제 */
kref_put(&dev->kref, my_device_release);
}
}
패턴 3: 분리된 참조 카운트 (수명 vs 활성)
/*
* 일부 커널 객체는 두 개의 참조 카운트를 사용합니다:
* - refcount: 객체 수명 (0이면 메모리 해제)
* - active: 활성 사용 (0이면 비활성화, 메모리는 유지)
*
* 예: struct super_block의 s_count(수명) + s_active(마운트)
* 예: struct module의 refcnt(수명) + mkobj.kobj.kref(sysfs)
*/
struct my_subsystem {
refcount_t refcount; /* 메모리 수명 관리 */
atomic_t active; /* 활성 사용자 수 */
struct rcu_head rcu;
bool dead; /* 비활성화 플래그 */
};
/* 활성 사용 시작 */
bool subsystem_activate(struct my_subsystem *s)
{
if (READ_ONCE(s->dead))
return false; /* 이미 비활성화됨 */
atomic_inc(&s->active);
if (READ_ONCE(s->dead)) {
atomic_dec(&s->active);
return false;
}
return true;
}
/* 비활성화 (새 사용자 차단, 기존 사용자 완료 대기) */
void subsystem_deactivate(struct my_subsystem *s)
{
WRITE_ONCE(s->dead, true);
synchronize_rcu();
/* 모든 active 사용자가 종료될 때까지 대기 */
wait_event(s->waitq, atomic_read(&s->active) == 0);
/* 수명 참조 해제 */
subsystem_put(s);
}
패턴 4: 콜백 데이터 참조 관리
/*
* 비동기 콜백에 데이터를 전달할 때,
* 콜백이 실행될 시점에 데이터가 유효해야 합니다.
*/
struct async_request {
struct kref kref;
struct my_device *dev; /* 디바이스 참조 */
void *buffer;
size_t size;
completion_t done;
};
static void async_request_release(struct kref *kref)
{
struct async_request *req = container_of(kref, struct async_request, kref);
/* 디바이스 참조도 해제 */
kref_put(&req->dev->kref, my_device_release);
kfree(req->buffer);
kfree(req);
}
int submit_async_request(struct my_device *dev, void *data, size_t len)
{
struct async_request *req;
req = kzalloc(sizeof(*req), GFP_KERNEL);
if (!req)
return -ENOMEM;
kref_init(&req->kref); /* req refcount = 1 */
kref_get(&dev->kref); /* 디바이스 참조 획득 */
req->dev = dev;
req->buffer = kmemdup(data, len, GFP_KERNEL);
req->size = len;
/* 하드웨어에 제출 — 완료 시 콜백 호출 */
kref_get(&req->kref); /* 콜백용 참조 */
hw_submit(dev->hw, req->buffer, req->size, async_callback, req);
/* 제출자의 참조 해제 */
kref_put(&req->kref, async_request_release);
return 0;
}
static void async_callback(void *context, int status)
{
struct async_request *req = context;
pr_info("async request completed: status=%d\n", status);
/* 콜백의 참조 해제 — 마지막이면 release 호출 */
kref_put(&req->kref, async_request_release);
}
패턴 5: sysfs 속성과 참조 카운팅
/*
* sysfs 속성 핸들러에서는 kobject_get/put이
* 프레임워크에 의해 자동으로 관리됩니다.
* 하지만 속성 핸들러 안에서 다른 객체를 참조할 때는
* 직접 관리해야 합니다.
*/
static ssize_t peer_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct my_device *mydev = dev_get_drvdata(dev);
struct my_device *peer;
ssize_t ret;
rcu_read_lock();
peer = rcu_dereference(mydev->peer);
if (peer && kref_get_unless_zero(&peer->kref)) {
rcu_read_unlock();
ret = sysfs_emit(buf, "%s\n", peer->name);
kref_put(&peer->kref, my_device_release);
} else {
rcu_read_unlock();
ret = sysfs_emit(buf, "(none)\n");
}
return ret;
}
lockref: 참조 카운트와 Lock의 통합
lockref_t는 참조 카운트와 spinlock을 단일 64비트 워드에 통합한 최적화된 자료구조입니다. 주로 struct dentry에서 사용됩니다.
lockref 구조체
/* include/linux/lockref.h */
struct lockref {
union {
#if CONFIG_ARCH_USE_CMPXCHG_LOCKREF
aligned_u64 lock_count; /* lock + count 통합 */
#endif
struct {
spinlock_t lock;
int count;
};
};
};
코드 설명
- aligned_u64 lock_countlock과 count를 하나의 64비트 값으로 접근합니다. cmpxchg8b(x86) 또는 cmpxchg(64비트)로 lock 상태와 count를 동시에 원자적으로 변경할 수 있습니다.
- CONFIG_ARCH_USE_CMPXCHG_LOCKREF이 최적화는 64비트 cmpxchg를 지원하는 아키텍처에서만 활성화됩니다. x86-64, ARM64 등이 해당합니다. 32비트 시스템에서는 일반 spinlock + count로 폴백합니다.
/* lib/lockref.c — lockref_get (간소화) */
void lockref_get(struct lockref *lockref)
{
CMPXCHG_LOOP(
new.count++;
,
return;
);
/* cmpxchg 실패 시 fallback: lock 획득 후 count++ */
spin_lock(&lockref->lock);
lockref->count++;
spin_unlock(&lockref->lock);
}
/* lockref_put_return — dput의 fast path */
int lockref_put_return(struct lockref *lockref)
{
CMPXCHG_LOOP(
new.count--;
if (old.count <= 0)
return -1;
,
return new.count;
);
return -1;
}
코드 설명
- CMPXCHG_LOOP64비트 cmpxchg를 사용하여 lock이 해제된 상태에서 count를 변경합니다. lock 비트가 설정되어 있으면 cmpxchg가 실패하고, fallback으로 실제 spinlock을 획득합니다. 비경합 시 lock 획득 없이 참조 카운트를 변경할 수 있어 매우 빠릅니다.
- dentry 최적화
struct dentry는 VFS의 핵심 자료구조로, 모든 파일 경로 조회에서 접근됩니다.lockref덕분에 dget/dput의 fast path에서 spinlock 없이 참조 카운트를 변경할 수 있어, 파일시스템 성능이 크게 향상됩니다.
refcount_warn_saturate 구현 상세
saturation 보호의 핵심인 refcount_warn_saturate 함수의 구현을 상세히 분석합니다.
/* lib/refcount.c */
void refcount_warn_saturate(refcount_t *r, enum refcount_saturation_type t)
{
/* 카운트를 REFCOUNT_SATURATED로 고정 */
atomic_set(&r->refs, REFCOUNT_SATURATED);
switch (t) {
case REFCOUNT_ADD_NOT_ZERO:
WARN_ONCE(true, "refcount_t: saturated; leaking memory.\n");
break;
case REFCOUNT_ADD_OVF:
WARN_ONCE(true, "refcount_t: overflow; use-after-free.\n");
break;
case REFCOUNT_ADD_UAF:
WARN_ONCE(true, "refcount_t: addition on 0; use-after-free.\n");
break;
case REFCOUNT_SUB_UAF:
WARN_ONCE(true, "refcount_t: underflow; use-after-free.\n");
break;
default:
WARN_ONCE(true, "refcount_t: unknown saturation event!?\n");
}
}
코드 설명
- atomic_set(&r->refs, REFCOUNT_SATURATED)카운트를 REFCOUNT_SATURATED(INT_MIN/2)로 고정합니다. 이 값은 정상 범위(1~INT_MAX)와 크게 떨어져 있어, 이후의 inc/dec 연산에서 항상 비정상으로 탐지됩니다. 결과적으로 카운트가 다시는 0에 도달하지 않으므로 객체가 해제되지 않습니다(의도적 메모리 누수).
- WARN_ONCE
WARN이 아닌WARN_ONCE를 사용합니다. 동일한 saturation 이벤트에 대해 커널 로그를 한 번만 출력하여, 반복적인 WARN으로 인한 로그 폭주를 방지합니다. - REFCOUNT_ADD_UAF (addition on 0)가장 위험한 상태입니다. 이미 해제된 객체(refcount가 0)에 대해 참조 증가가 시도되었음을 의미합니다. 이는 거의 확실한 Use-After-Free 버그입니다.
- REFCOUNT_SUB_UAF (underflow)참조 카운트가 0 이하로 감소했습니다. 이미 해제된 객체에 대해 kref_put이 호출된 것이며, double-free로 이어질 수 있는 버그입니다.
Saturation 이벤트 유형
| 이벤트 | 조건 | 의미 | WARN 메시지 |
|---|---|---|---|
REFCOUNT_ADD_NOT_ZERO | inc_not_zero에서 오버플로 | 정상 사용 중 극단적 오버플로 | "saturated; leaking memory" |
REFCOUNT_ADD_OVF | inc에서 INT_MAX 초과 | 의도적 오버플로 공격 가능 | "overflow; use-after-free" |
REFCOUNT_ADD_UAF | inc에서 old == 0 | 해제된 객체 부활 시도 | "addition on 0; use-after-free" |
REFCOUNT_SUB_UAF | dec에서 old <= 0 | 이미 해제된 객체 이중 감소 | "underflow; use-after-free" |
참조 카운터 테스트 전략
참조 카운팅 코드를 테스트하는 것은 까다롭지만 필수적입니다. 커널은 여러 테스트 프레임워크를 제공합니다.
KUnit 기반 단위 테스트
/*
* 참조 카운터 KUnit 테스트 예제
* lib/refcount_test.c (커널 트리 내)
*/
#include <kunit/test.h>
#include <linux/refcount.h>
static void test_refcount_init(struct kunit *test)
{
refcount_t r = REFCOUNT_INIT(1);
KUNIT_EXPECT_EQ(test, refcount_read(&r), 1);
}
static void test_refcount_inc_dec(struct kunit *test)
{
refcount_t r = REFCOUNT_INIT(1);
refcount_inc(&r);
KUNIT_EXPECT_EQ(test, refcount_read(&r), 2);
refcount_inc(&r);
KUNIT_EXPECT_EQ(test, refcount_read(&r), 3);
KUNIT_EXPECT_FALSE(test, refcount_dec_and_test(&r));
KUNIT_EXPECT_EQ(test, refcount_read(&r), 2);
KUNIT_EXPECT_FALSE(test, refcount_dec_and_test(&r));
KUNIT_EXPECT_EQ(test, refcount_read(&r), 1);
KUNIT_EXPECT_TRUE(test, refcount_dec_and_test(&r));
KUNIT_EXPECT_EQ(test, refcount_read(&r), 0);
}
static void test_refcount_inc_not_zero(struct kunit *test)
{
refcount_t r = REFCOUNT_INIT(1);
/* 정상: 1에서 inc_not_zero → 성공 */
KUNIT_EXPECT_TRUE(test, refcount_inc_not_zero(&r));
KUNIT_EXPECT_EQ(test, refcount_read(&r), 2);
/* 0으로 만든 후 inc_not_zero → 실패 */
refcount_dec_and_test(&r); /* 2→1 */
refcount_dec_and_test(&r); /* 1→0 */
KUNIT_EXPECT_FALSE(test, refcount_inc_not_zero(&r));
}
static void test_kref_lifecycle(struct kunit *test)
{
struct {
struct kref kref;
bool released;
} obj = { .released = false };
kref_init(&obj.kref);
KUNIT_EXPECT_EQ(test, refcount_read(&obj.kref.refcount), 1);
kref_get(&obj.kref);
KUNIT_EXPECT_EQ(test, refcount_read(&obj.kref.refcount), 2);
/* 첫 번째 put — 아직 해제 안 됨 */
kref_put(&obj.kref, ({
void release(struct kref *k) {
container_of(k, typeof(obj), kref)->released = true;
}
release;
}));
KUNIT_EXPECT_FALSE(test, obj.released);
/* 두 번째 put — 해제됨 */
KUNIT_EXPECT_TRUE(test, obj.released == false);
}
static struct kunit_case refcount_test_cases[] = {
KUNIT_CASE(test_refcount_init),
KUNIT_CASE(test_refcount_inc_dec),
KUNIT_CASE(test_refcount_inc_not_zero),
KUNIT_CASE(test_kref_lifecycle),
{},
};
static struct kunit_suite refcount_test_suite = {
.name = "refcount",
.test_cases = refcount_test_cases,
};
kunit_test_suite(refcount_test_suite);
스트레스 테스트 접근법
/*
* 멀티스레드 참조 카운팅 스트레스 테스트
* 여러 kthread가 동시에 get/put을 수행하여
* race condition을 유발합니다.
*/
static int stress_thread(void *data)
{
struct my_item *item;
int i;
for (i = 0; i < 100000; i++) {
/* RCU 기반 lookup */
item = find_item_by_id(0);
if (item) {
/* 약간의 작업 시뮬레이션 */
cpu_relax();
/* 참조 해제 */
item_put(item);
}
if (kthread_should_stop())
break;
/* 가끔 스케줄링 양보 */
if ((i % 1000) == 0)
cond_resched();
}
return 0;
}
/* 테스트 실행: 4개 스레드 동시 get/put */
static void run_stress_test(void)
{
struct task_struct *threads[4];
int i;
for (i = 0; i < 4; i++)
threads[i] = kthread_run(stress_thread, NULL, "reftest/%d", i);
msleep(5000); /* 5초 실행 */
for (i = 0; i < 4; i++)
kthread_stop(threads[i]);
/*
* 테스트 완료 후 refcount가 1(초기 참조만)이면 성공.
* KASAN이나 refcount_t WARN이 발생하지 않아야 합니다.
*/
}
CONFIG_KASAN=y— Use-After-Free 메모리 접근 탐지CONFIG_LOCKDEP=y(PROVE_LOCKING) — 잠금 순서 위반 탐지CONFIG_DEBUG_OBJECTS=y— 객체 수명 상태 추적CONFIG_KMEMLEAK=y— 참조 누수로 인한 메모리 누수 탐지CONFIG_KCSAN=y— 데이터 레이스 탐지
완전한 모듈 예제
kref를 사용하는 완전한 커널 모듈 예제입니다. RCU 기반 lookup, workqueue 통합, sysfs 속성을 포함합니다.
/*
* kref_example.c — kref 참조 카운팅 예제 모듈
* 커널 5.15+ 대상
*/
#include <linux/module.h>
#include <linux/kref.h>
#include <linux/slab.h>
#include <linux/list.h>
#include <linux/rculist.h>
#include <linux/spinlock.h>
#include <linux/workqueue.h>
#include <linux/debugfs.h>
#define MAX_ITEMS 16
struct my_item {
struct kref kref;
struct list_head list;
struct rcu_head rcu;
struct work_struct work;
int id;
char data[64];
};
static LIST_HEAD(item_list);
static DEFINE_SPINLOCK(item_lock);
static int next_id;
static struct dentry *dbgdir;
/* ── release 콜백 ── */
static void item_release(struct kref *kref)
{
struct my_item *item = container_of(kref, struct my_item, kref);
pr_info("kref_example: releasing item %d\n", item->id);
kfree_rcu(item, rcu); /* RCU grace period 후 kfree */
}
static inline void item_put(struct my_item *item)
{
kref_put(&item->kref, item_release);
}
static inline struct my_item *item_get(struct my_item *item)
{
if (item)
kref_get(&item->kref);
return item;
}
/* ── RCU 기반 lookup ── */
static struct my_item *find_item_by_id(int id)
{
struct my_item *item;
rcu_read_lock();
list_for_each_entry_rcu(item, &item_list, list) {
if (item->id == id) {
if (!kref_get_unless_zero(&item->kref))
item = NULL;
rcu_read_unlock();
return item;
}
}
rcu_read_unlock();
return NULL;
}
/* ── 생성/삭제 ── */
static struct my_item *create_item(const char *data)
{
struct my_item *item;
item = kzalloc(sizeof(*item), GFP_KERNEL);
if (!item)
return NULL;
kref_init(&item->kref);
strscpy(item->data, data, sizeof(item->data));
spin_lock(&item_lock);
item->id = next_id++;
list_add_tail_rcu(&item->list, &item_list);
spin_unlock(&item_lock);
pr_info("kref_example: created item %d '%s'\n", item->id, item->data);
return item;
}
static void remove_item(struct my_item *item)
{
spin_lock(&item_lock);
list_del_rcu(&item->list);
spin_unlock(&item_lock);
/* 리스트 참조 해제 — 다른 holder가 없으면 kfree_rcu */
item_put(item);
}
/* ── workqueue 연동 ── */
static void item_work_fn(struct work_struct *work)
{
struct my_item *item = container_of(work, struct my_item, work);
pr_info("kref_example: processing item %d '%s'\n",
item->id, item->data);
/* 작업 완료 후 참조 해제 */
item_put(item);
}
static void schedule_item_work(struct my_item *item)
{
item_get(item); /* workqueue용 참조 */
INIT_WORK(&item->work, item_work_fn);
if (!queue_work(system_wq, &item->work))
item_put(item); /* 큐 실패 시 되돌림 */
}
/* ── debugfs 인터페이스 ── */
static int items_show(struct seq_file *m, void *v)
{
struct my_item *item;
rcu_read_lock();
list_for_each_entry_rcu(item, &item_list, list) {
seq_printf(m, "id=%d data='%s' refcount=%u\n",
item->id, item->data,
refcount_read(&item->kref.refcount));
}
rcu_read_unlock();
return 0;
}
DEFINE_SHOW_ATTRIBUTE(items);
/* ── 모듈 초기화/종료 ── */
static int __init kref_example_init(void)
{
struct my_item *item1, *item2, *found;
dbgdir = debugfs_create_dir("kref_example", NULL);
debugfs_create_file("items", 0444, dbgdir, NULL, &items_fops);
/* 아이템 생성 */
item1 = create_item("hello");
item2 = create_item("world");
/* RCU lookup으로 참조 획득 */
found = find_item_by_id(0);
if (found) {
pr_info("found item: %s (refcount=%u)\n",
found->data, refcount_read(&found->kref.refcount));
schedule_item_work(found);
item_put(found); /* lookup 참조 해제 */
}
pr_info("kref_example: module loaded\n");
return 0;
}
static void __exit kref_example_exit(void)
{
struct my_item *item, *tmp;
debugfs_remove_recursive(dbgdir);
/* 모든 아이템 제거 */
spin_lock(&item_lock);
list_for_each_entry_safe(item, tmp, &item_list, list) {
list_del_rcu(&item->list);
item_put(item);
}
spin_unlock(&item_lock);
/* RCU grace period 대기하여 모든 참조 해제 보장 */
synchronize_rcu();
flush_scheduled_work();
pr_info("kref_example: module unloaded\n");
}
module_init(kref_example_init);
module_exit(kref_example_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("kref reference counting example");
MODULE_AUTHOR("Linux Kernel Docs");
코드 설명
- kfree_rcu(item, rcu)release 콜백에서
kfree대신kfree_rcu를 사용합니다. RCU reader가 아직 이 아이템을 참조 중일 수 있으므로, grace period가 끝난 후에야 메모리를 해제합니다.rcu는struct my_item내의struct rcu_head멤버 이름입니다. - find_item_by_id → kref_get_unless_zeroRCU + kref_get_unless_zero 표준 패턴의 실전 적용입니다. RCU 보호 아래 리스트를 순회하면서, 찾은 아이템의 참조를 안전하게 획득합니다.
- schedule_item_work → item_get/item_putworkqueue에 작업을 제출할 때 참조를 획득하고, 작업 완료 시 해제합니다. 큐 등록 실패 시 바로 참조를 되돌리는 것이 핵심입니다.
- module_exit → synchronize_rcu + flush_scheduled_work모듈 언로드 시 모든 RCU 콜백과 workqueue 작업이 완료되었음을 보장합니다. 그래야
kfree_rcu가 모든 아이템을 해제하고, workqueue의 item_put이 완료됩니다.
refcount_t 내부 구현 아키텍처별 비교
refcount_t의 핵심 함수인 refcount_dec_and_test는 아키텍처마다 서로 다른 원자적 명령어를 사용하여 구현됩니다. 각 아키텍처의 구현 방식은 성능 특성과 메모리 순서 보장에 직접적인 영향을 미칩니다.
x86: LOCK CMPXCHG 기반 구현
x86 아키텍처는 LOCK CMPXCHG(Compare and Exchange) 명령어를 사용하여 원자적 감소와 saturation 검사를 단일 연산으로 수행합니다. x86의 강한 메모리 모델(TSO, Total Store Order) 덕분에 추가적인 메모리 배리어가 거의 필요하지 않습니다.
/* arch/x86 — refcount_dec_and_test 내부 흐름 (개념적 의사 코드) */
static inline bool refcount_dec_and_test(refcount_t *r)
{
int old, new;
do {
old = atomic_read(&r->refs); /* 현재 값 읽기 */
if (old == REFCOUNT_SATURATED) /* 포화 상태면 아무것도 안 함 */
return false;
new = old - 1;
if (new < 0) { /* 언더플로 감지 */
refcount_warn_saturate(r, REFCOUNT_SUB_UAF);
return false;
}
} while (!try_cmpxchg_release(&r->refs.counter, &old, new));
/* x86: LOCK CMPXCHG — 버스 락으로 원자성 보장 */
if (new == 0) {
smp_acquire__after_ctrl_dep(); /* acquire 의미론 확보 */
return true; /* 마지막 참조 → release 콜백 */
}
return false;
}
코드 설명
- try_cmpxchg_release(&r->refs.counter, &old, new)x86에서
LOCK CMPXCHG명령어로 컴파일됩니다. 현재 값이old와 같으면new로 교체하고, 다르면old를 현재 값으로 갱신합니다.LOCK접두사가 버스 락을 걸어 다른 코어의 동시 접근을 차단합니다. x86 TSO 모델에서_release접미사는 추가 명령을 생성하지 않습니다. - smp_acquire__after_ctrl_dep()마지막 참조를 해제하여 0이 된 경우, release 콜백 실행 전에 acquire 배리어를 삽입합니다. 이로써 다른 CPU에서 수행한 모든 메모리 쓰기가 현재 CPU에서 관찰 가능함을 보장합니다. x86에서는 제어 의존성(control dependency)만으로도 충분하므로 실질적으로 NOP에 가깝습니다.
ARM64: LDXR/STXR(LL/SC) 및 LSE Atomics
ARM64는 기본적으로 LL/SC(Load-Link/Store-Conditional) 방식인 LDXR/STXR 명령어 쌍을 사용합니다. ARMv8.1-A 이후의 LSE(Large System Extensions) 확장이 있는 시스템에서는 CAS(Compare And Swap) 또는 LDADD 같은 단일 원자적 명령어를 사용하여 성능을 개선합니다.
/* ARM64 LL/SC 방식 의사 코드 */
refcount_dec_and_test:
prfm pstl1strm, [x0] /* 캐시 라인 프리페치 (store 힌트) */
1: ldxr w1, [x0] /* Exclusive Load: 현재 refcount 읽기 */
sub w2, w1, #1 /* 1 감소 */
cbz w1, 3f /* 이미 0이면 → 언더플로 경고 */
stlxr w3, w2, [x0] /* Exclusive Store (release): 결과 쓰기 시도 */
cbnz w3, 1b /* 실패하면 재시도 (다른 코어가 개입) */
cbz w2, 2f /* 결과가 0이면 → 마지막 참조 */
ret /* 아직 참조 남음 → return false */
2: dmb ish /* acquire 배리어: 후속 읽기 앞에 삽입 */
/* return true */
ret
3: /* saturation / underflow 처리 */
/* ARMv8.1 LSE 방식 (CONFIG_ARM64_LSE_ATOMICS) */
refcount_dec_and_test_lse:
mov w1, #-1 /* 감소값 = -1 */
ldaddl w1, w2, [x0] /* atomic: old = *x0; *x0 += w1 (release) */
/* w2 = old value, 단일 명령어로 원자적 감소 완료 */
cmp w2, #1 /* old가 1이었으면 → 0이 됨 → 마지막 참조 */
b.eq 2f
ret
2: dmb ish
ret
RISC-V: LR/SC 기반
RISC-V는 LR(Load Reserved)/SC(Store Conditional) 쌍을 사용하며, AMO(Atomic Memory Operation) 확장을 통해 AMOADD 같은 명령어도 지원합니다. 메모리 순서는 .aq(acquire)와 .rl(release) 접미사로 명시적으로 지정합니다.
/* RISC-V LR/SC 방식 의사 코드 */
refcount_dec_and_test:
1: lr.w a1, (a0) /* Load Reserved: refcount 읽기 */
addi a2, a1, -1 /* 1 감소 */
beqz a1, 3f /* 이미 0이면 → 언더플로 */
sc.w.rl a3, a2, (a0) /* Store Conditional (release) */
bnez a3, 1b /* SC 실패 → 재시도 */
beqz a2, 2f /* 결과 0 → 마지막 참조 */
li a0, 0 /* return false */
ret
2: fence r, rw /* acquire 배리어 */
li a0, 1 /* return true */
ret
3: /* saturation 처리 */
/* RISC-V AMO 확장 사용 시 */
refcount_dec_and_test_amo:
li a1, -1
amoadd.w.rl a2, a1, (a0) /* atomic: old = *a0; *a0 += a1 (release) */
li a3, 1
bne a2, a3, 1f /* old != 1 → 아직 참조 남음 */
fence r, rw /* acquire */
li a0, 1
ret
1: li a0, 0
ret
코드 설명
- LDXR/STLXR (ARM64) vs LR.W/SC.W.RL (RISC-V)두 아키텍처 모두 LL/SC 방식이지만, ARM64의 exclusive monitor와 RISC-V의 reservation set은 구현 세부 사항이 다릅니다. ARM64 exclusive monitor는 캐시 라인 단위로 동작하며, RISC-V reservation set은 구현에 따라 크기가 달라질 수 있습니다. 두 경우 모두 다른 코어의 쓰기가 감지되면 SC가 실패하여 루프를 재시도합니다.
- LDADDL (ARM64 LSE) vs AMOADD.W.RL (RISC-V AMO)두 명령어 모두 단일 원자적 명령으로 값을 더하면서 이전 값을 반환합니다. LL/SC 루프 대비 캐시 라인 경합(contention)이 심한 환경에서 성능이 크게 향상됩니다. ARM64
LDADDL의L접미사는 release 의미론을, RISC-V.rl접미사도 동일한 release 의미론을 제공합니다.
아키텍처별 성능 특성 비교
| 항목 | x86 (TSO) | ARM64 (Weak) | RISC-V (RVWMO) |
|---|---|---|---|
| 기본 원자적 명령어 | LOCK CMPXCHG |
LDXR/STXR (LL/SC) |
LR/SC |
| 최적화 명령어 | LOCK XADD |
LDADDL (LSE) |
AMOADD (AMO 확장) |
| 메모리 모델 | TSO (강한 순서) | 약한 순서 (명시적 배리어 필요) | RVWMO (약한 순서) |
| release 비용 | 거의 0 (TSO 보장) | STLXR / LDADDL |
.rl 접미사 |
| acquire 비용 | 거의 0 (TSO 보장) | DMB ISH |
FENCE R,RW |
| 경합 시 동작 | 버스 락 → 대기 | SC 실패 → 재시도 루프 | SC 실패 → 재시도 루프 |
| 거짓 실패(spurious failure) | 없음 | 가능 (LL/SC) | 가능 (LR/SC) |
참조 카운터 기반 객체 생명주기 시나리오
참조 카운팅의 실제 동작을 이해하기 위해, 커널 내 대표적인 세 가지 서브시스템에서 참조 카운터가 객체 생명주기를 어떻게 관리하는지 살펴봅니다.
시나리오 1: 네트워크 소켓 (struct sock)
네트워크 소켓 struct sock은 sk_refcnt 필드(refcount_t)를 통해 참조 카운팅됩니다. 소켓은 사용자 공간 프로세스, 타이머, 네트워크 스택의 여러 계층에서 동시에 참조될 수 있으므로, 정확한 참조 카운팅이 필수적입니다.
/* include/net/sock.h */
struct sock {
/* ... 수백 개의 필드 ... */
refcount_t sk_refcnt; /* 소켓 참조 카운터 */
/* ... */
};
/* 참조 획득: sock_hold */
static inline void sock_hold(struct sock *sk)
{
refcount_inc(&sk->sk_refcnt);
}
/* 참조 해제: sock_put */
void sock_put(struct sock *sk)
{
if (refcount_dec_and_test(&sk->sk_refcnt))
sk_free(sk); /* 마지막 참조 → proto->destroy() + kfree */
}
/* TCP 연결 수립 시 참조 흐름 예시 */
struct sock *tcp_v4_syn_recv_sock(...)
{
struct sock *newsk = tcp_create_openreq_child(sk, req, skb);
/* newsk->sk_refcnt = 1 (생성자 참조) */
inet_ehash_nolisten(newsk, osk, ...);
/* 해시 테이블 삽입 — 별도의 sock_hold 불필요 (생성자 참조 이전) */
return newsk;
}
/* 타이머에서의 참조 획득/해제 */
static void tcp_keepalive_timer(struct timer_list *t)
{
struct sock *sk = from_timer(sk, t, sk_timer);
bh_lock_sock(sk);
/* ... keepalive 처리 ... */
bh_unlock_sock(sk);
sock_put(sk); /* 타이머 콜백 완료 → 참조 해제 */
}
코드 설명
- refcount_inc(&sk->sk_refcnt)
sock_hold는 소켓의 참조 카운트를 원자적으로 증가시킵니다. 소켓을 장기간 참조하는 코드(타이머, workqueue, 다른 소켓 등)는 반드시sock_hold로 참조를 획득한 후 사용해야 합니다. 참조 카운트가 이미 0이면 WARN이 발생합니다. - refcount_dec_and_test(&sk->sk_refcnt) → sk_free
sock_put은 참조 카운트를 감소시키고, 0이 되면sk_free를 호출하여 소켓을 해제합니다.sk_free는 프로토콜별proto->destroy()를 호출한 후, 소켓 구조체의 메모리를 해제합니다. 이 패턴은 kref_put과 동일한 구조입니다. - tcp_keepalive_timer → sock_putTCP keepalive 타이머는
sk_reset_timer호출 시sock_hold로 참조를 획득하고, 타이머 콜백 완료 시sock_put으로 해제합니다. 타이머가 소켓보다 오래 살아있으면 UAF가 발생하므로, 이 참조 쌍이 반드시 필요합니다.
시나리오 2: 파일 디스크립터 (struct file)
struct file은 f_count 필드(atomic_long_t)를 통해 참조 카운팅됩니다. 하나의 파일을 여러 프로세스가 공유(fork, dup)하거나, 동일 프로세스 내 여러 스레드가 접근할 수 있으므로 정확한 참조 관리가 필수적입니다.
/* include/linux/fs.h */
struct file {
union {
struct llist_node f_llist;
struct rcu_head f_rcuhead;
};
struct path f_path;
struct inode *f_inode;
const struct file_operations *f_op;
atomic_long_t f_count; /* 참조 카운터 */
/* ... */
};
/* 참조 획득: fget (fd 테이블에서 struct file 획득) */
struct file *fget(unsigned int fd)
{
struct file *file;
rcu_read_lock();
file = fcheck_files(current->files, fd);
if (file) {
if (!atomic_long_inc_not_zero(&file->f_count)) {
/* f_count가 이미 0 → 파일이 닫히는 중 */
file = NULL;
}
}
rcu_read_unlock();
return file;
}
/* 참조 해제: fput */
void fput(struct file *file)
{
if (atomic_long_dec_and_test(&file->f_count)) {
struct task_struct *task = current;
if (likely(!in_interrupt() && !(task->flags & PF_KTHREAD))) {
init_task_work(&file->f_rcuhead, ____fput);
task_work_add(task, &file->f_rcuhead, TWA_RESUME);
} else {
/* 인터럽트/커널 스레드: 지연 해제 */
llist_add(&file->f_llist, &delayed_fput_list);
schedule_delayed_work(&delayed_fput_work, 1);
}
}
}
/* fork 시 참조 공유 */
struct file *get_file(struct file *f)
{
atomic_long_inc(&f->f_count); /* 자식 프로세스도 같은 file 참조 */
return f;
}
코드 설명
- atomic_long_inc_not_zero(&file->f_count)
fget에서 RCU 보호 하에 파일을 찾은 후,f_count가 0이 아닌 경우에만 참조를 획득합니다. 이것은kref_get_unless_zero와 동일한 패턴입니다. 파일이 이미 닫히는 중(f_count == 0)이라면 NULL을 반환하여 사용을 방지합니다. - atomic_long_dec_and_test → task_work_add / delayed_fput
fput에서 마지막 참조를 해제하면, 파일을 즉시 닫지 않고 task_work 또는 지연 워크큐를 통해 비동기로 처리합니다. 이는fput이 인터럽트 컨텍스트에서 호출될 수 있고, 파일 닫기 연산(f_op->release)이 sleep할 수 있기 때문입니다. - get_file(f) → atomic_long_inc
fork시 자식 프로세스의 fd 테이블이 부모의 파일 구조체를 공유합니다. 이때get_file로f_count를 증가시켜, 부모나 자식 중 어느 한쪽이 먼저close해도 다른 쪽에 영향이 없도록 합니다.
시나리오 3: 디바이스 모델 (struct device)
struct device는 내부에 struct kobject를 포함하고, kobject 내부의 struct kref를 통해 참조 카운팅됩니다. device_get/device_put 대신 get_device/put_device를 사용하며, 이는 kobject_get/kobject_put의 래퍼입니다.
/* include/linux/device.h */
struct device {
struct kobject kobj; /* kobject → kref → refcount_t */
struct device *parent;
struct bus_type *bus;
struct device_driver *driver;
void *driver_data;
/* ... */
};
/* 참조 획득: get_device */
struct device *get_device(struct device *dev)
{
return dev ? to_dev(kobject_get(&dev->kobj)) : NULL;
/* kobject_get → kref_get → refcount_inc */
}
/* 참조 해제: put_device */
void put_device(struct device *dev)
{
if (dev)
kobject_put(&dev->kobj);
/* kobject_put → kref_put → refcount_dec_and_test
* → 0이 되면 kobject_release → device_release */
}
/* 드라이버 probe/remove 흐름 */
static int really_probe(struct device *dev, struct device_driver *drv)
{
get_device(dev); /* 드라이버 바인딩 전 참조 획득 */
int ret = call_driver_probe(dev, drv);
if (ret) {
put_device(dev); /* probe 실패 → 참조 해제 */
return ret;
}
/* 성공: dev->driver = drv, 참조 유지 */
return 0;
}
/* sysfs에서 디바이스 속성 읽기 시 */
static ssize_t dev_attr_show(struct kobject *kobj,
struct attribute *attr, char *buf)
{
struct device *dev = kobj_to_dev(kobj);
/* kobj 참조가 sysfs에 의해 보장됨 — 별도 get_device 불필요 */
return dev_attr->show(dev, dev_attr, buf);
}
코드 설명
- kobject_get(&dev->kobj) / kobject_put(&dev->kobj)
get_device/put_device는kobject_get/kobject_put을 통해kref의 참조 카운트를 조작합니다. 이 계층 구조는 device → kobject → kref → refcount_t로 이어지며, 최종적으로refcount_inc/refcount_dec_and_test가 호출됩니다. - really_probe → get_device / put_device드라이버 바인딩(
really_probe) 시 디바이스 참조를 획득하여, probe 함수 실행 중 디바이스가 사라지지 않도록 보장합니다. probe가 실패하면 즉시 참조를 해제하고, 성공하면 드라이버가 바인딩된 동안 참조를 유지합니다.device_release_driver시 해제됩니다. - dev_attr_show — sysfs 참조 보장sysfs 속성 파일 접근 시 커널의 sysfs 계층이
kobject참조를 보장합니다. 따라서 sysfs show/store 콜백 내부에서는 별도로get_device를 호출할 필요가 없습니다. 이는 sysfs가 내부적으로kobject_get/kobject_put을 관리하기 때문입니다.
참조 카운터 디버깅 실전
참조 카운팅 버그는 커널에서 가장 찾기 어려운 버그 유형 중 하나입니다. UAF(Use-After-Free), 이중 해제(Double Free), 참조 누수(Leak) 등의 문제를 진단하기 위한 실전 디버깅 기법을 다룹니다.
KASAN을 이용한 UAF 탐지
KASAN(Kernel Address Sanitizer)은 해제된 메모리에 대한 접근을 실시간으로 탐지합니다. CONFIG_KASAN=y로 빌드된 커널에서는 참조 카운팅 실수로 인한 UAF가 즉시 보고됩니다.
==================================================================
BUG: KASAN: slab-use-after-free in my_device_read+0x48/0x120
Read of size 8 at addr ffff888012345678 by task cat/1234
CPU: 2 PID: 1234 Comm: cat Not tainted 6.8.0-debug #1
Hardware name: QEMU Standard PC (Q35)
Call Trace:
dump_stack_lvl+0x48/0x70
print_report+0xd2/0x620
kasan_report+0xda/0x110
my_device_read+0x48/0x120 ← UAF 발생 위치
vfs_read+0x1a2/0x790
ksys_read+0xf1/0x1c0
do_syscall_64+0x5d/0x90
Allocated by task 567: ← 메모리가 할당된 경로
kasan_save_stack+0x33/0x60
kmalloc_trace+0x25/0x90
my_device_create+0x2a/0x180 ← 객체 생성 위치
driver_probe+0x42/0x1e0
Freed by task 890: ← 메모리가 해제된 경로
kasan_save_stack+0x33/0x60
kasan_save_free_info+0x27/0x40
kfree+0xef/0x380
my_device_release+0x35/0x60 ← kref_put 콜백에서 해제
kref_put+0x3a/0x60
my_device_close+0x28/0x40 ← close()에서 참조 해제
The buggy address belongs to the object at ffff888012345600
which belongs to the cache kmalloc-256 of size 256
The buggy address is located 120 bytes inside of
freed 256-byte region [ffff888012345600, ffff888012345700)
==================================================================
KASAN 보고서에서 확인해야 할 핵심 정보는 다음과 같습니다.
| 보고서 항목 | 의미 | 디버깅 활용 |
|---|---|---|
slab-use-after-free |
slab 할당자에서 해제된 메모리 접근 | 참조 카운팅 누락 또는 경쟁 조건 확인 |
Call Trace (UAF 지점) |
해제 후 접근이 발생한 함수 | 이 함수가 참조를 획득하지 않고 객체를 사용하고 있음 |
Allocated by task |
객체가 처음 생성된 경로 | 생성 시 초기 참조가 누구에게 전달되었는지 확인 |
Freed by task |
객체가 해제된 경로 | kref_put 콜백이 예상보다 일찍 호출된 원인 추적 |
refcount_t saturation WARN 해석
refcount_t는 오버플로/언더플로를 감지하면 값을 REFCOUNT_SATURATED(0xC0000000)로 고정하고 WARN을 출력합니다. 이 경고가 발생하면 참조 카운팅에 심각한 버그가 있음을 의미합니다.
refcount_t: addition on 0; use-after-free.
WARNING: CPU: 1 PID: 2345 at lib/refcount.c:25 refcount_warn_saturate+0xba/0x110
Modules linked in: my_driver(OE)
CPU: 1 PID: 2345 Comm: worker/1:2 Tainted: G OE 6.8.0 #1
Call Trace:
refcount_warn_saturate+0xba/0x110
refcount_inc+0x4e/0x60 ← refcount가 0인 상태에서 inc 시도
kref_get+0x1c/0x30
my_device_work_handler+0x22/0x80 ← 이미 해제된 객체의 kref_get
process_one_work+0x2a2/0x620
worker_thread+0x52/0x3f0
--- 또 다른 경우: 언더플로 ---
refcount_t: underflow; use-after-free.
WARNING: CPU: 3 PID: 3456 at lib/refcount.c:28 refcount_warn_saturate+0xce/0x110
Call Trace:
refcount_warn_saturate+0xce/0x110
refcount_dec_and_test+0xb8/0xd0 ← 이미 0인 refcount를 또 감소
kref_put+0x3a/0x60
my_device_cleanup+0x40/0x60 ← 이중 kref_put 호출
addition on 0— 이미 해제된(refcount == 0) 객체에kref_get을 호출했습니다.kref_get_unless_zero를 사용해야 할 곳에서kref_get을 사용한 경우가 대부분입니다.underflow— 대응하는kref_get없이kref_put을 초과 호출했습니다. 에러 경로에서 참조를 두 번 해제하는 실수가 흔합니다.overflow— 매우 드물지만, 참조 획득 루프에서kref_put을 빠뜨려 카운트가UINT_MAX/2를 넘어선 경우입니다.
ftrace를 이용한 kref_get/kref_put 추적
ftrace의 function tracer를 사용하면 특정 객체의 kref_get/kref_put 호출 경로를 실시간으로 추적할 수 있습니다.
# ftrace로 kref 관련 함수 추적 설정
cd /sys/kernel/debug/tracing
# 추적 대상 함수 설정
echo 'kref_get kref_put kref_get_unless_zero' > set_ftrace_filter
echo '1' > options/func_stack_trace # 호출 스택도 기록
echo 'function' > current_tracer
# 추적 시작
echo 1 > tracing_on
# ... 문제 재현 ...
# 추적 중지 및 결과 확인
echo 0 > tracing_on
cat trace
# 출력 예시:
# TASK-PID CPU# | TIMESTAMP FUNCTION
# | | | | | |
# cat-1234 [002] d..1 1234.567890: kref_get <-my_device_open
# cat-1234 [002] d..1 1234.567891: <stack trace>
# => kref_get
# => my_device_open
# => chrdev_open
# => do_dentry_open
# => vfs_open
# 특정 모듈의 함수만 필터링하여 추적
echo ':mod:my_driver' > set_ftrace_filter
echo 'kref_get kref_put' >> set_ftrace_filter
# kprobe로 refcount 값까지 추적
echo 'p:kprobe/kref_get_trace kref_get kref=%di +0(%di):u32' > kprobe_events
echo 1 > events/kprobes/kref_get_trace/enable
코드 설명
- set_ftrace_filter + func_stack_trace
set_ftrace_filter에 추적 대상 함수를 설정하고,func_stack_trace옵션을 활성화하면 각 호출마다 전체 콜 스택이 기록됩니다. 이 스택을 분석하면kref_get과kref_put이 짝을 이루는지 확인할 수 있습니다. 짝이 맞지 않는 호출 경로가 참조 카운팅 버그의 원인입니다. - kprobe: kref=%di +0(%di):u32kprobe를 사용하면 함수 인자와 메모리 값을 직접 추적할 수 있습니다.
%di는 x86_64에서 첫 번째 인자(kref 포인터)를 나타내며,+0(%di):u32는 해당 포인터가 가리키는 메모리의 처음 4바이트(refcount_t.refs.counter)를 32비트 부호 없는 정수로 읽습니다. 이를 통해 각 kref_get 호출 시점의 실제 참조 카운트 값을 확인할 수 있습니다.
slabinfo와 참조 누수 연관 분석
/proc/slabinfo를 모니터링하면 참조 카운트 누수로 인한 메모리 증가를 감지할 수 있습니다. 특정 slab 캐시의 활성 객체 수가 지속적으로 증가하면 참조 누수를 의심해야 합니다.
# slab 캐시별 활성 객체 수 모니터링
watch -n 5 'cat /proc/slabinfo | head -2; cat /proc/slabinfo | grep -E "kmalloc-256|sock_inode_cache|dentry"'
# 출력 예시 (active_objs가 시간이 지남에 따라 단조 증가하면 누수 의심):
# slabinfo - version: 2.1
# # name <active_objs> <num_objs> <objsize> ...
# kmalloc-256 1847 2048 256 ...
# sock_inode_cache 342 380 832 ...
# dentry 28450 29184 192 ...
# slabinfo 변화량 추적 스크립트
for i in $(seq 1 10); do
echo "=== Sample $i ==="
grep 'my_device_cache' /proc/slabinfo | awk '{print $1, "active="$2, "total="$3}'
sleep 10
done
# slab_unreclaimable 증가도 누수 지표
cat /proc/meminfo | grep 'SUnreclaim'
# SUnreclaim: 45632 kB ← 시간이 지남에 따라 증가하면 누수
# CONFIG_SLUB_DEBUG 활성화 시 상세 추적
echo 1 > /sys/kernel/slab/kmalloc-256/trace
# dmesg에서 할당/해제 추적 로그 확인
crash 도구로 refcount 상태 확인
시스템 크래시 덤프(vmcore)에서 crash 도구를 사용하여 객체의 refcount 상태를 직접 확인할 수 있습니다.
# crash 도구 실행
crash vmlinux vmcore
# 특정 구조체의 refcount 확인
crash> struct kobject ffff888012345600
kobj = {
name = "my_device0",
kref = {
refcount = {
refs = {
counter = 0 ← refcount가 0 (이미 해제됨)
}
}
},
...
}
# struct device의 refcount 확인
crash> struct device.kobj.kref ffff888012345600
kobj.kref = {
refcount = {
refs = {
counter = -1073741824 ← 0xC0000000 = REFCOUNT_SATURATED
}
}
}
# sock 구조체의 sk_refcnt 확인
crash> struct sock.sk_refcnt ffff888087654300
sk_refcnt = {
refs = {
counter = 2 ← 아직 2개의 참조가 존재
}
}
# 메모리 슬랩 정보 확인
crash> kmem ffff888012345600
CACHE OBJSIZE ALLOCATED TOTAL SLABS
kmalloc-256 256 1847 2048 128
SLAB MEMORY NODE TOTAL ALLOCATED FREE
ffffea0000048d00 ffff888012340000 0 16 14 2
ffff888012345600 (free) ← 이미 해제된 상태
# list에 연결된 모든 device의 refcount 확인
crash> list device.kobj.entry -s device.kobj.kref.refcount.refs.counter -H ffff888000100000
코드 설명
- counter = 0xC0000000 (REFCOUNT_SATURATED)이 값은
refcount_t의 saturation 보호가 작동한 결과입니다. 오버플로 또는 언더플로가 감지되면 커널은 값을REFCOUNT_SATURATED로 고정하고, 이후의 모든 inc/dec 연산을 무시합니다. 이는 추가적인 피해를 방지하기 위한 안전장치이지만, 객체가 절대 해제되지 않으므로 메모리 누수가 발생합니다. - crash 도구의 list 명령
list명령은 연결 리스트를 따라가면서 각 구조체의 특정 필드를 출력합니다.-s옵션으로 출력할 필드를,-H옵션으로 리스트 헤드 주소를 지정합니다. 이를 통해 모든 디바이스의 refcount를 한 번에 확인하여, 비정상적인 값을 가진 객체를 빠르게 찾을 수 있습니다.
percpu_ref 심층
percpu_ref는 읽기 경로(참조 획득/해제)의 성능이 극도로 중요한 경우를 위해 설계된 고성능 참조 카운터입니다. 일반 모드에서는 per-CPU 변수를 사용하여 캐시 바운싱 없이 참조 카운팅을 수행하고, 셧다운(kill) 시에는 단일 atomic 카운터 모드로 전환하여 정확한 0 검사를 수행합니다.
struct percpu_ref 구조
/* include/linux/percpu-refcount.h */
struct percpu_ref {
atomic_long_t count; /* atomic 카운터 (kill 후 사용) */
unsigned long percpu_count_ptr; /* per-CPU 카운터 포인터 + 플래그 */
percpu_ref_func_t *release; /* 참조 0 시 콜백 */
percpu_ref_func_t *confirm_switch; /* 모드 전환 완료 콜백 */
struct rcu_head rcu; /* RCU 콜백용 */
};
/* 핵심 API */
int percpu_ref_init(struct percpu_ref *ref,
percpu_ref_func_t *release,
unsigned int flags, struct gfp_t gfp);
static inline void percpu_ref_get(struct percpu_ref *ref)
{
unsigned long __percpu *percpu_count;
rcu_read_lock_sched();
if (__ref_is_percpu(ref, &percpu_count))
this_cpu_inc(*percpu_count); /* per-CPU 모드: 로컬 CPU 카운터++ */
else
atomic_long_inc(&ref->count); /* atomic 모드: 단일 카운터++ */
rcu_read_unlock_sched();
}
static inline void percpu_ref_put(struct percpu_ref *ref)
{
unsigned long __percpu *percpu_count;
rcu_read_lock_sched();
if (__ref_is_percpu(ref, &percpu_count))
this_cpu_dec(*percpu_count); /* per-CPU 모드: 로컬 CPU 카운터-- */
else if (unlikely(atomic_long_dec_and_test(&ref->count)))
ref->release(ref); /* atomic 모드에서 0이 되면 release */
rcu_read_unlock_sched();
}
/* 셧다운: per-CPU → atomic 전환 */
void percpu_ref_kill(struct percpu_ref *ref)
{
percpu_ref_kill_and_confirm(ref, NULL);
}
코드 설명
- this_cpu_inc(*percpu_count) / this_cpu_dec(*percpu_count)per-CPU 모드에서는 각 CPU가 자신만의 카운터를 조작합니다.
this_cpu_inc/this_cpu_dec는 원자적 명령어 없이 단순 메모리 연산으로 수행되며, 캐시 라인이 CPU 간 공유되지 않으므로 캐시 바운싱이 전혀 발생하지 않습니다. 이는 일반refcount_t대비 수십 배 빠를 수 있습니다. - percpu_ref_kill → percpu_ref_kill_and_confirmkill 호출은 per-CPU 모드를 종료하고 atomic 모드로 전환합니다. 전환 과정은 (1) percpu_count_ptr에 PERCPU_REF_DEAD 플래그 설정 → (2) RCU grace period 대기 (모든 CPU의 진행 중인 get/put 완료 보장) → (3) 모든 per-CPU 카운터를 합산하여 atomic count에 반영 → (4) 이후부터 atomic 모드로 동작합니다.
percpu_ref 활용 사례
percpu_ref는 I/O 경로처럼 초고속 참조 카운팅이 필요하면서도, 셧다운 시 정확한 drain이 보장되어야 하는 서브시스템에서 사용됩니다.
/* block/blk-mq.c — blk-mq의 q_usage_counter */
struct request_queue {
/* ... */
struct percpu_ref q_usage_counter; /* I/O 요청 진행 중 카운터 */
/* ... */
};
/* I/O 제출 경로: 매우 빈번하게 호출됨 */
blk_status_t blk_mq_submit_bio(struct bio *bio)
{
struct request_queue *q = bdev_get_queue(bio->bi_bdev);
if (!percpu_ref_tryget_live(&q->q_usage_counter)) {
/* 큐가 freeze 중 → I/O 거부 */
bio_io_error(bio);
return BLK_STS_IOERR;
}
/* ... I/O 처리 ... */
percpu_ref_put(&q->q_usage_counter);
return BLK_STS_OK;
}
/* 큐 freeze: 진행 중인 모든 I/O가 완료될 때까지 대기 */
void blk_freeze_queue(struct request_queue *q)
{
percpu_ref_kill(&q->q_usage_counter);
/* per-CPU → atomic 전환 후 */
blk_mq_run_hw_queues(q, false);
wait_event(q->mq_freeze_wq,
percpu_ref_is_zero(&q->q_usage_counter));
/* 모든 I/O 완료 → 큐 안전하게 변경 가능 */
}
/* cgroup에서의 활용: cgroup_file에 percpu_ref 사용 */
struct cgroup {
/* ... */
struct percpu_ref self; /* cgroup 자체 참조 카운터 */
/* ... */
};
코드 설명
- percpu_ref_tryget_live(&q->q_usage_counter)
tryget_live는 참조를 획득하되, 이미 kill된 상태(dead)면 실패를 반환합니다. blk-mq에서 I/O 요청마다 호출되므로 per-CPU 모드의 성능이 핵심적입니다. per-CPU 모드에서는this_cpu_inc만 수행하므로, 다중 코어에서 동시에 I/O를 제출해도 캐시 경합이 없습니다. - percpu_ref_kill → wait_event(percpu_ref_is_zero)블록 디바이스 큐를 freeze할 때, 먼저
percpu_ref_kill로 새로운 I/O 획득을 차단하고,percpu_ref_is_zero가 될 때까지 대기합니다. 이 패턴은 "graceful shutdown"을 구현하며, 진행 중인 I/O를 안전하게 완료시킨 후에야 큐 구조를 변경할 수 있도록 보장합니다.
lockref 최적화 패턴
lockref는 spinlock과 참조 카운트를 하나의 8바이트(64비트) 값에 패킹하여, cmpxchg 한 번으로 잠금 없이 참조 카운트를 변경하는 최적화 기법입니다. 주로 dentry 캐시에서 사용되며, 경로 탐색(path lookup) 성능을 크게 향상시킵니다.
struct lockref 구조
/* include/linux/lockref.h */
struct lockref {
union {
#if defined(CONFIG_ARCH_USE_CMPXCHG_LOCKREF)
aligned_u64 lock_count; /* cmpxchg용: lock + count를 한 번에 */
#endif
struct {
spinlock_t lock; /* 4바이트 spinlock */
int count; /* 4바이트 참조 카운트 */
};
};
};
/* dentry에서의 사용 */
struct dentry {
struct lockref d_lockref; /* spinlock + refcount */
struct inode *d_inode;
struct hlist_bl_node d_hash;
struct dentry *d_parent;
struct qstr d_name;
/* ... */
};
/* lockref_get: fast path (cmpxchg) → slow path (spinlock) */
void lockref_get(struct lockref *lockref)
{
CMPXCHG_LOOP(
new.count++; /* 시도: count만 1 증가 */
,
return; /* cmpxchg 성공 → 바로 리턴 */
);
/* cmpxchg 실패 (lock 경합) → slow path */
spin_lock(&lockref->lock);
lockref->count++;
spin_unlock(&lockref->lock);
}
/* lockref_put_return: 감소 후 count 반환 */
int lockref_put_return(struct lockref *lockref)
{
CMPXCHG_LOOP(
new.count--;
if (old.count <= 0) /* 이미 0 이하면 slow path */
break;
,
return new.count;
);
return -1; /* slow path 필요 */
}
/* lockref_get_not_dead: count > 0인 경우만 획득 */
int lockref_get_not_dead(struct lockref *lockref)
{
CMPXCHG_LOOP(
new.count++;
if (old.count < 0) /* 음수면 "dead" → 실패 */
return 0;
,
return 1; /* 성공 */
);
spin_lock(&lockref->lock);
int retval = 0;
if (lockref->count >= 0) {
lockref->count++;
retval = 1;
}
spin_unlock(&lockref->lock);
return retval;
}
코드 설명
- CMPXCHG_LOOP / aligned_u64 lock_count
CMPXCHG_LOOP매크로는 spinlock과 count를 합친 8바이트 값을 통째로cmpxchg합니다. 핵심 아이디어는: lock이 잡혀있지 않은 상태(lock 필드가 unlocked)에서 count만 변경한 새 값을cmpxchg로 교체하는 것입니다. lock이 이미 잡혀있으면cmpxchg가 실패하여 slow path로 넘어갑니다. - lockref_get_not_dead — count < 0 검사dentry에서 음수 count는 "dead" 상태를 의미합니다.
lockref_get_not_dead는 RCU walk(경로 탐색)에서 dentry가 아직 유효한지 확인하면서 참조를 획득하는 데 사용됩니다. 이 함수 덕분에 경로 탐색 대부분이 spinlock 없이 완료되며, 이는 멀티코어 시스템의 파일 시스템 성능에 결정적입니다.
dentry 캐시에서의 성능 효과
lockref가 dentry 캐시 성능에 미치는 영향은 상당합니다. 경로 탐색(path lookup)의 각 컴포넌트에서 dentry의 참조를 획득/해제해야 하므로, 디렉토리 깊이가 깊을수록 lockref의 fast path가 더 큰 성능 이점을 제공합니다.
/* fs/dcache.c — dget (dentry 참조 획득) */
static inline struct dentry *dget(struct dentry *dentry)
{
if (dentry)
lockref_get(&dentry->d_lockref);
/* fast path: cmpxchg 1회 → spinlock 없이 완료 */
return dentry;
}
/* fs/dcache.c — dput (dentry 참조 해제) */
void dput(struct dentry *dentry)
{
if (!dentry)
return;
/* fast path: count > 1이면 spinlock 없이 감소 */
if (lockref_put_return(&dentry->d_lockref) > 0)
return; /* 아직 참조 남음 → 바로 리턴 */
/* slow path: count가 0이 될 수 있음 → spinlock으로 보호 */
dput_to_list(dentry, &list);
}
/* RCU path walk에서의 lockref 사용 */
static inline int d_revalidate(struct dentry *dentry, unsigned int flags)
{
if (flags & LOOKUP_RCU) {
/* RCU walk: lockref_get_not_dead로 dentry 유효성 검증 */
if (!lockref_get_not_dead(&dentry->d_lockref))
return -ECHILD; /* dead dentry → ref walk로 전환 */
}
/* ... */
}
코드 설명
- lockref_put_return(&dentry->d_lockref) > 0
dput의 fast path입니다.lockref_put_return이 양수를 반환하면 아직 다른 참조가 남아있으므로 즉시 리턴합니다. 이 경로에서는 spinlock을 전혀 사용하지 않습니다. 일반적인 파일 시스템 워크로드에서 dput의 90% 이상이 이 fast path를 통과하며, 이로 인해 d_lockref spinlock의 경합이 크게 감소합니다. - lockref_get_not_dead → LOOKUP_RCURCU walk 경로 탐색에서는 dentry를 잠금 없이 순회합니다. 각 dentry에서
lockref_get_not_dead로 참조를 시도하며, 실패하면(dead dentry) RCU walk를 포기하고 기존의 ref walk로 전환합니다. 이 메커니즘 덕분에 대부분의 경로 탐색이 잠금 없이 완료됩니다.
| 비교 항목 | 일반 spinlock + refcount | lockref (cmpxchg fast path) |
|---|---|---|
| 참조 획득 (비경합) | spin_lock + inc + spin_unlock | cmpxchg 1회 (3~5 사이클) |
| 참조 해제 (count > 1) | spin_lock + dec + spin_unlock | cmpxchg 1회 |
| 캐시 라인 접근 | lock + count = 2회 (같은 라인이어도 exclusive) | 1회 (8바이트 단일 접근) |
| 경합 시 동작 | spin 대기 | cmpxchg 실패 → spinlock fallback |
| 적용 대상 | 범용 | x86, ARM64 등 cmpxchg 64비트 지원 아키텍처 |
참고 자료
- 커널 문서:
Documentation/core-api/kref.rst— kref API 공식 가이드 - 커널 문서:
Documentation/core-api/refcount-vs-atomic.rst— refcount_t와 atomic_t 비교 - 헤더:
include/linux/kref.h— kref 구조체 및 API 정의 - 헤더:
include/linux/refcount.h— refcount_t 타입 및 인라인(Inline) API - 구현:
lib/refcount.c— saturation 보호 로직 구현 - 헤더:
include/linux/kobject.h— kobject 구조체 (kref 사용) - 구현:
lib/kobject.c— kobject_get/kobject_put/kobject_release - 헤더:
include/linux/percpu-refcount.h— percpu_ref 고성능 참조 카운터 - Coccinelle:
scripts/coccinelle/api/atomic_as_refcounter.cocci - CVE-2016-0728: Perception Point, "Analysis and Exploitation of a Linux Kernel Vulnerability (CVE-2016-0728)", 2016
- Peter Zijlstra, Elena Reshetova, "refcount_t API compared to atomic_t", LWN.net (2017)
- Jonathan Corbet, "Bringing refcount_t to the mainline", LWN.net (2017)
관련 문서
kref/refcount_t와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.