kref / refcount_t (참조 카운터)

커널 객체의 수명을 관리하는 참조 카운팅(Reference Counting) 메커니즘인 krefrefcount_t를 심층 분석합니다. include/linux/kref.h에 정의된 kref API의 내부 구현, include/linux/refcount.hlib/refcount.c에 구현된 refcount_t의 saturation 보호 메커니즘, 기존 atomic_t에서 refcount_t로의 전환 이유와 역사, RCU와의 조합 패턴, kobject/device/sk_buff/file/dentry 등 실제 커널 서브시스템에서의 활용 사례, 그리고 흔한 실수와 디버깅(Debugging) 기법까지 커널 소스 기반으로 분석합니다.

전제 조건: Atomic 연산, 메모리 배리어(Memory Barrier), 커널 오브젝트(kobject) 문서를 먼저 읽으세요. 참조 카운터(Reference Counter)는 원자적 연산(Atomic Operation) 위에 구축되며, kobject는 kref의 가장 대표적인 사용자입니다.
일상 비유: 참조 카운팅은 도서관 대출 카운터와 같습니다. 책(객체)이 서가에 있을 때 대출 카운트는 1(생성자가 소유)입니다. 누군가 대출하면 카운트가 증가하고, 반납하면 감소합니다. 카운트가 0이 되면 "아무도 이 책을 사용하지 않으므로" 서가에서 제거(메모리 해제)할 수 있습니다. refcount_t는 여기에 "대출 카운트가 음수가 되는 것을 방지하는 잠금 장치"를 추가한 것입니다.

핵심 요약

  • 참조 카운팅(Reference Counting) — 객체를 공유하는 모든 사용자가 "사용 시작" 시 카운트를 증가시키고, "사용 완료" 시 감소시킵니다. 카운트가 0에 도달하면 release 콜백이 호출되어 객체가 해제됩니다.
  • krefinclude/linux/kref.h에 정의된 커널 참조 카운터 래퍼(Wrapper)입니다. 내부적으로 refcount_t를 사용하며, kref_init/kref_get/kref_put API를 제공합니다.
  • refcount_tatomic_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 등 거의 모든 주요 커널 객체가 참조 카운팅으로 수명을 관리합니다.

단계별 이해

  1. 왜 참조 카운팅이 필요한가 이해
    여러 코드 경로가 동일 객체를 동시에 사용할 때, 누가 마지막으로 사용을 끝내는지 미리 알 수 없습니다. 참조 카운팅은 이 문제를 해결합니다.
  2. kref API 기초 학습
    kref_init으로 초기화, kref_get으로 획득, kref_put으로 해제하는 기본 패턴을 익힙니다.
  3. refcount_t의 보호 메커니즘 파악
    saturation(포화) 보호가 어떻게 오버플로/언더플로를 방지하는지 이해합니다.
  4. RCU 조합 패턴 학습
    kref_get_unless_zerorcu_read_lock을 조합한 안전한 lookup 패턴을 이해합니다.
  5. 실전 활용 사례 분석
    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)가 발생할 수 있습니다.

Use-After-Free 시나리오: 참조 카운팅 없는 경우 Thread A Thread B 메모리 상태 obj = kmalloc() obj: 유효 (allocated) ptr = obj (포인터 복사) obj: 유효 (2곳 참조) kfree(obj) obj: 해제됨 (freed) ptr->field (접근 시도) UAF! 해제된 메모리 접근 결과: 커널 패닉(Panic), 데이터 손상, 또는 권한 상승(Privilege Escalation) 취약점

참조 카운팅은 이 문제를 근본적으로 해결합니다. Thread B가 객체를 사용하기 전에 참조 카운트를 증가시키면, Thread A가 kfree를 호출하더라도 카운트가 0이 아니므로 실제 해제가 일어나지 않습니다. Thread B가 사용을 마치고 참조를 해제하면 그때 비로소 카운트가 0이 되어 안전하게 해제됩니다.

UAF의 심각성: Use-After-Free는 Linux 커널 보안 취약점의 가장 큰 원인 중 하나입니다. 2020~2024년 커널 CVE 통계에서 UAF는 전체의 약 30~40%를 차지합니다. 공격자는 해제된 메모리를 다른 객체로 재할당받아(heap spraying) 커널 코드 실행, 권한 상승, 정보 유출을 달성할 수 있습니다. 참조 카운팅은 이러한 공격의 근본 원인을 제거합니다.

참조 카운팅 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))으로 해제 여부를 결정하므로, 실시간 보장에 적합합니다.

순환 참조 주의: 참조 카운팅은 순환 참조(Circular Reference)를 자동으로 탐지하지 못합니다. 예를 들어, A가 B를 참조하고 B가 A를 참조하면 둘 다 카운트가 0이 되지 않아 메모리 누수가 발생합니다. 커널에서는 이를 방지하기 위해 "약한 참조(weak reference)" 패턴을 사용하거나, 소유권 계층(부모→자식)을 명확히 하여 한쪽만 참조 카운트를 보유합니다. 예: kobject의 부모-자식 관계에서는 자식이 부모를 참조하지만, 부모는 자식의 리스트만 유지하고 별도 참조를 잡지 않습니다.

참조 카운팅의 기본 원칙

동작의미API
초기화(Init)객체 생성 시 카운트를 1로 설정kref_init() / refcount_set()
획득(Get)객체 사용 시작 — 카운트 +1kref_get() / refcount_inc()
해제(Put)객체 사용 종료 — 카운트 -1kref_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 refcountkref의 유일한 멤버입니다. 이전 커널 버전(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)에 알립니다. 반환값을 무시하면 컴파일 경고가 발생합니다. 이미 해제 중인 객체를 사용하는 버그를 방지합니다.
kref API 계층 구조 kref_init() kref_get() kref_put() kref_get_unless_zero() refcount_set() refcount_inc() refcount_dec_and_test() refcount_inc_not_zero() atomic_inc() atomic_dec_return() kref refcount_t atomic + saturation 보호 로직

refcount_t 구조체

refcount_tatomic_t를 대체하기 위해 도입된 참조 카운터 전용 타입입니다. 단순 정수 래퍼처럼 보이지만, 컴파일 타임(Compile Time)과 런타임(Runtime) 모두에서 보호 메커니즘을 제공합니다.

구조체 정의

/* include/linux/refcount.h */
typedef struct refcount_struct {
    atomic_t refs;
} refcount_t;
코드 설명
  • atomic_t refs내부적으로 atomic_t를 사용하지만, refcount_tatomic_t는 의도적으로 타입이 다릅니다. 이렇게 함으로써 atomic_inc(&ref->refs)처럼 보호 없이 직접 접근하는 코드를 컴파일 에러(Error)로 잡아냅니다. refcount_t 전용 API만 사용해야 합니다.

atomic_t에서 refcount_t로의 전환 이유

커널 v4.11(2017년)에서 refcount_t가 도입된 배경에는 심각한 보안 문제가 있었습니다.

문제atomic_trefcount_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)이러한 공격 벡터 차단
CVE-2016-0728 사례: Linux keyring 서브시스템에서 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 값 범위와 Saturation 보호 REFCOUNT_SATURATED (INT_MIN/2) — 포화 상태, 모든 연산 거부 0 — 해제 완료, inc 시 WARN + saturation으로 고정 1 ~ INT_MAX — 정상 동작 범위 inc/dec 자유롭게 수행 → 어떤 연산도 값을 변경하지 않음 → dec_and_test가 true 반환 → release 호출 → inc: count++, dec: count--, dec_and_test: count-- 후 0이면 true 오버플로 시도 → WARN + saturate 0에서 dec 시도 → WARN + saturate 값(value) ↓

refcount_t API

refcount_tinclude/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_relaxedrefcount_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 + saturaterelaxed
refcount_inc_not_zero(r)0이 아니면 count++false 반환relaxed
refcount_dec_and_test(r)count--, 0이면 trueunderflow: WARN + saturaterelease; acquire on true
refcount_dec_and_lock(r, lock)count--, 0이면 lock 획득 + trueunderflow: WARN + saturaterelease + spin_lock
refcount_dec_and_mutex_lock(r, m)count--, 0이면 mutex 획득 + trueunderflow: WARN + saturaterelease + 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 비교

세 가지 참조 카운팅 방법의 차이를 명확히 비교합니다.

참조 카운터 3종 비교: 보호 수준 atomic_t (레거시) 오버플로 보호: 없음 언더플로 보호: 없음 0→1 전환 탐지: 불가 타입 안전성: 범용 정수 CVE-2016-0728 CVE-2016-4558 refcount_t (현재 표준) 오버플로 보호: saturation 언더플로 보호: WARN + saturate 0→1 전환 탐지: WARN 타입 안전성: 전용 타입 성능 오버헤드: ~1% 미만 (x86: 0%, ARM: 약간의 추가 비교) kref (최상위 래퍼) refcount_t의 모든 보호 계승 release 콜백 패턴 강제 kref_put_mutex 등 확장 API Documentation/core-api/kref.rst 사용 예: kobject, usb_device, drm_gem_object, ... kref는 refcount_t 위에 구축 대체
특성atomic_trefcount_tkref
헤더linux/atomic.hlinux/refcount.hlinux/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 + WARNrefcount_t에 위임
release 콜백수동 구현수동 구현kref_put에 콜백 전달
권장 용도범용 카운터 (비-참조)단순 참조 카운터커널 객체 수명 관리

생명주기 패턴

참조 카운팅 기반의 객체 생명주기는 "생성 → 참조 획득 → 사용 → 참조 해제 → 소멸"의 5단계를 따릅니다.

kref 기반 객체 생명주기 1. 생성 (Create) kmalloc + kref_init refcount = 1 2. 공유 (Share) kref_get refcount++ 3. 사용 (Use) obj->field 접근 refcount 변경 없음 4. 해제 (Put) kref_put refcount-- refcount == 0? 아니오 (다른 참조 존재) 5. 소멸 (Release Callback) container_of → kfree refcount = 0, 메모리 해제 실전 코드 흐름 obj = create_my_obj(); // kref_init → refcount=1 kref_get(&obj->kref); // refcount=2 kref_put(&obj->kref, rel); // refcount=1 kref_put(&obj->kref, rel); // refcount=0 → rel() 호출 예: rel = 의미

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_get_unless_zero 안전한 Lookup 패턴 Reader (Lookup) Writer (Remove + Free) rcu_read_lock() list_for_each_entry_rcu() — 리스트 순회 obj 발견 — 아직 RCU 보호 아래 kref_get_unless_zero(&obj->kref) ← 핵심! 성공: refcount++ → 안전 사용 실패: 이미 해제 중 → NULL 반환 rcu_read_unlock() obj->data 접근 (refcount 보호, RCU 불필요) kref_put(&obj->kref, release) — 사용 완료 list_del_rcu(&obj->list) kref_put(&obj->kref, release) refcount-- (아직 0이 아닐 수 있음) synchronize_rcu() — grace period 대기 모든 RCU reader가 종료된 후 reader가 get에 성공했으면 reader의 put이 최종 해제를 수행

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_getkref_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_devicestruct device는 내부에 struct kobject kobj를 가지고 있고, kobject가 kref를 가지고 있습니다. 따라서 get_devicekobject_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;
}
코드 설명
  • fgetfget은 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 dentrylockref_t를 사용합니다. lockref_t는 참조 카운트와 spinlock을 단일 64비트(Bit) 워드에 통합하여, dget/dput 시 cmpxchg 한 번으로 카운트 증감과 동기화를 동시에 수행하는 최적화입니다.

커널 서브시스템별 참조 카운터 사용 VFS inode: i_count dentry: d_lockref file: f_count super_block: s_active atomic_long_t / lockref_t 디바이스 모델 kobject: kref device: kobject.kref driver: module.refcnt class: kref kref (refcount_t) 네트워킹 sk_buff: users sock: sk_refcnt net_device: dev_refcnt dst_entry: __refcnt refcount_t 메모리 관리 page/folio: _refcount mm_struct: mm_count vm_area_struct: vm_ref anon_vma: refcount atomic_t / refcount_t 보안 cred: usage key: usage pid: count refcount_t

kobject와 kref

kobject는 kref의 가장 대표적이고 중요한 사용자입니다. 커널의 전체 디바이스 모델(Device Model)은 kobject 트리 위에 구축되며, 이 트리의 각 노드는 kref로 수명이 관리됩니다.

kobject 생명주기와 kref

kobject 생명주기: kref 기반 수명 관리 kobject_init kref_init(&kobj->kref) refcount = 1 kobject_add sysfs 디렉토리 생성 parent에 등록 kobject_get kref_get(&kobj->kref) refcount++ kobject_put kref_put(&kobj->kref, kobject_release) refcount == 0? kobject_release → kobject_cleanup 체인 (refcount가 0일 때만) uevent(KOBJ_REMOVE) sysfs 디렉토리 제거 ktype->release(kobj) 부모 kobject_put ktype->release가 실제 메모리 해제를 담당 (예: device_release → kfree(dev))
/* 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_tatomic_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.01v4.4CVE-2016-0728 (keyring refcount overflow) 발견
2017.02v4.11refcount_t 타입 및 API 도입 (Peter Zijlstra, Elena Reshetova)
2017.05v4.12CONFIG_REFCOUNT_FULL 옵션 추가
2017~2019v4.12~v5.4Coccinelle 스크립트 기반 대량 변환 진행
2019.11v5.5saturation 보호 항상 활성화, 성능 최적화
2020~현재v5.5+CONFIG_REFCOUNT_FULL 제거, 잔여 atomic_t 지속 전환

Coccinelle 변환 스크립트

커널 트리에는 atomic_trefcount_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_readrefcount_read
  • atomic_setrefcount_set (초기화만)
  • atomic_increfcount_inc
  • atomic_dec_and_testrefcount_dec_and_test
  • atomic_inc_not_zerorefcount_inc_not_zero
  • atomic_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 UAFKASAN, refcount_t WARNget/put 쌍 엄격 관리
조건부 get 누락0→1 전환 WARNrefcount_t saturationkref_get_unless_zero
Release 후 접근데이터 손상, KASAN UAFKASANkfree를 마지막에 호출
에러 경로 put 누락메모리 누수 (kmemleak)kmemleakgoto cleanup 패턴
초기화 전 사용WARN (uninitialized)refcount_t WARN생성 직후 kref_init

소스 코드 워크스루

refcount_dec_and_test의 내부 구현을 커널 소스 기반으로 단계별 추적합니다.

refcount_dec_and_test 내부 구현 추적

refcount_dec_and_test() 내부 흐름 refcount_dec_and_test(&r) old = atomic_fetch_sub_release(1, &r->refs) [release 배리어] old == 1 ? 예 (마지막 참조) smp_acquire__after_ctrl_dep() return true → release 호출 아니오 old > 1 ? return false (아직 참조 있음) old <= 0 refcount_warn_saturate(r, REFCOUNT_SUB_UAF) WARN(1, "underflow; use-after-free.") atomic_set(&r->refs, REFCOUNT_SATURATED) return false (값 영구 고정)
/* 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 쌍을 사용합니다. stxrldxr 이후 다른 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           # 실패 시 재시도

아키텍처별 비교 요약

특성x86ARM64 (LSE)ARM64 (LL/SC)RISC-V
inc 명령어lock xaddldaddalldxr/stxr 루프amoadd.w
dec_and_testlock xadd + cmpldaddal + cmpldxr/stxr + cmpamoadd.w.rl + beq
메모리 모델TSO (strong)WeakWeakRVWMO (weak)
release 비용0 (implicit)접미사 변경별도 dmb접미사 .rl
refcount_t 오버헤드~0%~1% 미만~1% 미만~1% 미만

성능 분석

참조 카운팅의 성능 특성을 이해하는 것은 올바른 메커니즘 선택에 중요합니다.

캐시라인 경합 분석

참조 카운터 유형별 캐시라인 동작 refcount_t / kref (단일 카운터) CPU 0 CPU 1 CPU 2 CPU 3 동일 캐시라인 경합! percpu_ref (CPU별 카운터) CPU 0 local cnt CPU 1 local cnt CPU 2 local cnt CPU 3 local cnt 경합 없음 (각자 로컬) refcount_t 성능 특성 비경합 시: ~5-10ns (L1 캐시 히트) 경합 시: ~50-200ns (캐시라인 바운싱) NUMA 원격: ~100-500ns 적합: 중간 빈도 get/put (초당 수천~수만) percpu_ref 성능 특성 get/put: ~2-5ns (로컬 캐시 전용) kill 시: 글로벌 전환 비용 (RCU GP) 메모리: CPU 수 × sizeof(long) 적합: 고빈도 get/put (초당 수십만 이상)

벤치마크 데이터

시나리오atomic_trefcount_t오버헤드
단일 CPU, 비경합 inc4.2ns4.2ns0%
단일 CPU, 비경합 dec_and_test4.5ns4.6ns~2%
4-CPU 경합 inc52ns53ns~2%
4-CPU 경합 dec_and_test55ns56ns~2%
8-CPU NUMA 경합180ns183ns~1.7%

핵심 결론: refcount_t의 saturation 보호는 실질적으로 측정 불가능한 수준의 오버헤드만 추가합니다. x86에서는 기존 LOCK XADD 이후 조건 분기 하나가 추가되는 것뿐이며, 분기 예측기(Branch Predictor)가 정상 경로를 정확히 예측하므로 파이프라인(Pipeline) 지연이 발생하지 않습니다.

perf로 참조 카운터 경합 측정:
# 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로의 전환을 고려해야 합니다.

참조 카운터 선택 가이드

요구사항권장 메커니즘이유
일반 커널 객체 수명 관리krefrelease 콜백 패턴, 문서화된 API
단순 참조 카운터 (release 직접 관리)refcount_tsaturation 보호, 타입 안전성
고빈도 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 없이 참조 카운트를 변경할 수 있어, 파일시스템 성능이 크게 향상됩니다.
lockref: 64비트 통합 워드 구조 spinlock_t lock 비트 0~31 (locked, pending, tail) int count 비트 32~63 (참조 카운트) bit 0 bit 31 bit 32 bit 63 cmpxchg로 64비트 전체를 원자적으로 교체 → lock 상태 확인 + count 변경을 한 번에 수행 Fast path: lock 비트가 0이면 cmpxchg로 count만 변경 (lock 없음) Slow path: lock 비트가 1이면 spin_lock() 획득 후 count 변경

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_ONCEWARN이 아닌 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_ZEROinc_not_zero에서 오버플로정상 사용 중 극단적 오버플로"saturated; leaking memory"
REFCOUNT_ADD_OVFinc에서 INT_MAX 초과의도적 오버플로 공격 가능"overflow; use-after-free"
REFCOUNT_ADD_UAFinc에서 old == 0해제된 객체 부활 시도"addition on 0; use-after-free"
REFCOUNT_SUB_UAFdec에서 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가 끝난 후에야 메모리를 해제합니다. rcustruct 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 LDADDLL 접미사는 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)
refcount_dec_and_test — 아키텍처별 명령어 흐름 비교 x86 (TSO) ARM64 (Weak Order) RISC-V (RVWMO) MOV eax, [refcount] SUB eax, 1 + saturation 검사 LOCK CMPXCHG [refcount] 실패? → 루프 재시도 결과 == 0? → true 반환 TSO 보장: release/acquire 추가 배리어 불필요 (NOP) LDXR w1, [refcount] SUB w2, w1, #1 STLXR w3, w2, [refcount] CBNZ w3 → 재시도 (거짓 실패 가능) CBZ w2 → 마지막 참조? DMB ISH (acquire 배리어) 약한 순서 → 명시적 배리어 필요 LSE 대안: LDADDL (단일 명령) LL/SC 루프 제거 → 경합 시 우수 LR.W a1, (refcount) ADDI a2, a1, -1 SC.W.RL a3, a2, (refcount) BNEZ a3 → 재시도 (거짓 실패 가능) BEQZ a2 → 마지막 참조? FENCE R,RW (acquire 배리어) RVWMO → 명시적 fence 필요 AMO 대안: AMOADD.W.RL (단일 명령) LR/SC 루프 제거 강조 표시된 명령어 = 핵심 원자적 연산 (각 아키텍처의 결정적 차이) LOCK CMPXCHG (버스 락) STLXR (Exclusive Store) SC.W.RL (Store Conditional)

참조 카운터 기반 객체 생명주기 시나리오

참조 카운팅의 실제 동작을 이해하기 위해, 커널 내 대표적인 세 가지 서브시스템에서 참조 카운터가 객체 생명주기를 어떻게 관리하는지 살펴봅니다.

시나리오 1: 네트워크 소켓 (struct sock)

네트워크 소켓 struct socksk_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_freesock_put은 참조 카운트를 감소시키고, 0이 되면 sk_free를 호출하여 소켓을 해제합니다. sk_free는 프로토콜별 proto->destroy()를 호출한 후, 소켓 구조체의 메모리를 해제합니다. 이 패턴은 kref_put과 동일한 구조입니다.
  • tcp_keepalive_timer → sock_putTCP keepalive 타이머는 sk_reset_timer 호출 시 sock_hold로 참조를 획득하고, 타이머 콜백 완료 시 sock_put으로 해제합니다. 타이머가 소켓보다 오래 살아있으면 UAF가 발생하므로, 이 참조 쌍이 반드시 필요합니다.
struct sock 참조 카운터 생명주기 사용자 프로세스 TCP 스택 Keepalive 타이머 sk_refcnt socket() 시스템 콜 sk_alloc() refcnt = 1 inet_ehash_nolisten() refcnt = 1 (해시 테이블은 별도 참조 없음) sk_reset_timer() sock_hold() refcnt = 2 send()/recv() I/O refcnt = 2 close() 시스템 콜 sock_put() refcnt = 1 tcp_keepalive_timer() refcnt = 1 sock_put(sk) refcnt = 0 sk_free() — 소켓 소멸 마지막 sock_put()이 refcnt를 0으로 만들면 sk_free()가 호출되어 소켓이 소멸됩니다

시나리오 2: 파일 디스크립터 (struct file)

struct filef_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_fputfput에서 마지막 참조를 해제하면, 파일을 즉시 닫지 않고 task_work 또는 지연 워크큐를 통해 비동기로 처리합니다. 이는 fput이 인터럽트 컨텍스트에서 호출될 수 있고, 파일 닫기 연산(f_op->release)이 sleep할 수 있기 때문입니다.
  • get_file(f) → atomic_long_incfork 시 자식 프로세스의 fd 테이블이 부모의 파일 구조체를 공유합니다. 이때 get_filef_count를 증가시켜, 부모나 자식 중 어느 한쪽이 먼저 close해도 다른 쪽에 영향이 없도록 합니다.
struct file (f_count) 참조 카운터 생명주기 부모 프로세스 자식 프로세스 커널 I/O 경로 f_count open() → alloc_file() f_count = 1 fork() get_file() f_count = 2 dup(fd) f_count = 3 fget(fd) f_count = 4 fput() (I/O 완료) f_count = 3 close(fd) → fput() f_count = 2 close(dup_fd) → fput() f_count = 1 exit() → fput() f_count = 0 ____fput → f_op->release() 마지막 fput()이 f_count를 0으로 만들면, task_work를 통해 비동기로 파일을 해제합니다

시나리오 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_devicekobject_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을 관리하기 때문입니다.
struct device (kobject→kref) 참조 카운터 생명주기 버스 드라이버 디바이스 드라이버 sysfs / 사용자공간 kobj.kref device_register(dev) kref = 1 kobject_add (sysfs 생성) sysfs 디렉토리 생성 kref = 1 really_probe() get_device(dev) kref = 2 sysfs show (kobject_get) kref = 3 sysfs 완료 (kobject_put) kref = 2 device_release_driver() put_device(dev) kref = 1 device_unregister() kobject_put(&dev->kobj) kref = 0 kobject_release → device_release() — 소멸 device → kobject → kref 계층을 통해 참조가 관리되며, 마지막 put_device()가 device_release()를 트리거합니다

참조 카운터 디버깅 실전

참조 카운팅 버그는 커널에서 가장 찾기 어려운 버그 유형 중 하나입니다. 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 호출
saturation WARN 유형별 원인:
  • 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_traceset_ftrace_filter에 추적 대상 함수를 설정하고, func_stack_trace 옵션을 활성화하면 각 호출마다 전체 콜 스택이 기록됩니다. 이 스택을 분석하면 kref_getkref_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를 한 번에 확인하여, 비정상적인 값을 가진 객체를 빠르게 찾을 수 있습니다.
참조 카운터 디버깅 워크플로우 증상 감지 KASAN: slab-use-after-free 해제 후 접근 (UAF) refcount_warn_saturate 오버플로 / 언더플로 / 0에서 inc 메모리 증가 (SUnreclaim) 참조 누수 → 객체 미해제 1. Allocated by / Freed by 스택 비교 분석 1. WARN 메시지 유형 확인 (addition on 0 / underflow) 1. /proc/slabinfo 모니터링 active_objs 추이 확인 2. ftrace로 kref_get/put 호출 경로 추적 2. Call Trace에서 kref_get/put 짝 맞추기 분석 2. SLUB_DEBUG trace 할당/해제 로그 분석 3. crash 도구로 객체 상태 확인 4. 근본 원인 식별 get/put 불일치 에러 경로 누락 경쟁 조건 (race)

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를 안전하게 완료시킨 후에야 큐 구조를 변경할 수 있도록 보장합니다.
percpu_ref: per-CPU 모드 → atomic 모드 전환 Phase 1: per-CPU 모드 (고성능) CPU 0 percpu_count +3 CPU 1 percpu_count +5 CPU 2 percpu_count +2 atomic count (초기값: 1) per-CPU 모드에서 미사용 this_cpu_inc/dec 원자적 명령 불필요 캐시 바운싱 없음 percpu_ref_kill() 호출 Phase 2: 전환 과정 1. PERCPU_REF_DEAD 플래그 설정 → 새로운 get이 atomic 경로로 전환 2. call_rcu_sched() → RCU grace period 대기 (진행 중인 per-CPU get/put 완료 보장) 3. 모든 per-CPU 카운터 합산: atomic_count += SUM(per_cpu_count[0..N]) → 여기서는 +3 +5 +2 = +10 Phase 3: atomic 모드 (정확한 0 검사) atomic count 1 + 10 = 11 (초기 1 + per-CPU 합산 10) atomic_long_dec_and_test 정확한 0 도달 시 release() 콜백 호출

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_countCMPXCHG_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) > 0dput의 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비트 지원 아키텍처
lockref: fast path (cmpxchg) vs slow path (spinlock) lockref_get(lockref) 호출 old = READ lock_count (8바이트 한 번에 읽기) lock이 잡혀있는가? 아니오 (fast path) 예 (slow path) new = old, new.count++ CMPXCHG(lock_count, old, new) 성공? 완료 (잠금 없음) 아니오 (재시도/fallback) 100회 재시도 후 slow path로 spin_lock(&lockref->lock) lockref->count++ spin_unlock(&lockref->lock) 완료 (잠금 사용)

참고 자료

공식 문서 및 핵심 소스:

kref/refcount_t와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.