RCU (Read-Copy-Update)

Linux 커널의 RCU(Read-Copy-Update)를 대상으로 read-side 임계구역, 포인터 발행 규칙(rcu_assign_pointer/rcu_dereference), grace period, call_rcu/kfree_rcu 기반 회수 모델을 코드 흐름 중심으로 설명합니다. 또한 Tree RCU와 SRCU의 선택 기준, 흔한 오류(UAF/RCU stall), 운영 환경에서의 디버깅 절차까지 심층적으로 다룹니다.

전제 조건: 커널 아키텍처인터럽트 문서를 먼저 읽으세요. 동기화 선택은 실행 문맥(프로세스/IRQ/softirq)에 직접 좌우되므로, 먼저 컨텍스트 경계를 정확히 잡아야 합니다.
일상 비유: 이 주제는 교차로 신호 제어와 비슷합니다. 동시에 진입하는 흐름을 규칙 없이 처리하면 충돌이 나듯이, 락과 대기 규칙이 없으면 레이스와 데드락이 발생합니다.

핵심 요약

  • 핵심 순서 — Copy 후 rcu_assign_pointer(), 이후 grace period 대기, 마지막 해제가 기본 패턴입니다.
  • 읽기 측 규칙rcu_read_lock() 구간에서 rcu_dereference()로 접근해야 합니다.
  • 해제 규칙 — 즉시 kfree() 대신 call_rcu()/kfree_rcu()를 사용합니다.
  • 변형 선택 — read-side에서 슬립이 필요하면 SRCU를 사용합니다.
  • 운영 리스크 — RCU stall과 모듈 언로드 시 콜백 잔존 문제를 반드시 점검합니다.

단계별 이해

  1. 읽기/쓰기 책임 분리
    reader는 빠른 조회, writer는 포인터 교체와 회수를 담당한다는 구조를 먼저 고정합니다.
  2. 포인터 발행 규칙 학습
    rcu_assign_pointer()rcu_dereference() 쌍을 먼저 익힙니다.
  3. 회수 타이밍 검증
    grace period 완료 전에 메모리를 해제하지 않는지 체크합니다.
  4. 디버깅 루틴 확보
    stall 로그와 /sys/kernel/debug/rcu 정보를 함께 보는 절차를 마련합니다.
관련 표준: LKMM (Linux Kernel Memory Model), C11 Memory Model — RCU의 읽기 측 동기화와 메모리 순서 보장은 이 메모리 모델 규격에 기반합니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

RCU 개요

쉬운 설명: RCU를 일상 비유로 먼저 이해하세요.

RCU란? 도서관 도서 교체에 비유

일상 비유: 도서관에서 인기 있는 백과사전의 새 버전이 나왔습니다. 사서는 어떻게 해야 할까요?

  1. 옛날 방식 (Lock): 도서관 문을 잠그고(lock), 모든 사람을 내보낸 뒤, 책을 교체하고, 다시 문을 연다(unlock).

    ❌ 문제: 책 1권 교체하는 동안 도서관 전체가 폐쇄됨 (읽기도 막힘)

  2. RCU 방식 (Read-Copy-Update):

    1. 새 책을 옆 선반에 조용히 놓는다 (Copy)
    2. 표지판을 "새 책은 옆 선반에 있음"으로 바꾼다 (Update)
    3. 현재 옛날 책을 읽고 있는 사람들이 다 나갈 때까지 기다린다 (Grace Period)
    4. 모두 나가면 옛날 책을 치운다 (kfree_rcu)

    ✅ 장점: 독자들은 기다릴 필요 없이 계속 읽을 수 있음!

3가지 핵심 개념:

개념 의미 API
Read 잠금 없이 읽기 (매우 빠름) rcu_read_lock() / rcu_read_unlock()
Copy 원본을 복사하고 수정 kmalloc() + 데이터 복사
Update 포인터를 새 데이터로 교체 rcu_assign_pointer()
Grace Period 모든 독자가 읽기를 끝낼 때까지 대기 synchronize_rcu() / call_rcu()

언제 RCU를 사용하나요?

✅ RCU가 좋은 경우:
  • 읽기가 90% 이상 (쓰기는 드묾)
  • 데이터가 포인터로 참조됨
  • 읽기가 매우 빨라야 함
  • 예: 라우팅 테이블, 프로세스 리스트
❌ RCU가 안 좋은 경우:
  • 읽기와 쓰기가 비슷한 비율
  • 데이터가 크고 복사 비용이 높음
  • 즉시 메모리 해제가 필요
  • → spinlock이나 mutex 사용
주의: RCU는 "lock-free"가 아니라 "read-side lock-free"입니다. Reader는 잠금이 없지만, writer는 여전히 동기화가 필요합니다 (보통 spinlock 사용).

RCU(Read-Copy-Update)는 읽기 작업이 대부분인 자료구조에 최적화된 동기화 메커니즘입니다. reader는 잠금 없이 자료구조에 접근하고, writer는 데이터의 복사본을 수정한 뒤 포인터를 원자적으로 교체합니다. 이전 데이터는 모든 reader가 종료한 후(grace period) 안전하게 해제됩니다.

RCU Read-Copy-Update 과정 ① 초기 상태 rcu_read_lock() R1 R2 rcu_dereference() ptr old data (v1) rcu_read_unlock() ② 복사 & 수정 (Writer) ptr old data (v1) 복사 new data (v2) 수정됨 W 수정 ③ 포인터 교체 rcu_assign_pointer() R1 이미 참조중 (이전 rcu_dereference) old data (v1) ptr new data (v2) R3 신규 reader 기존 reader는 old data를 안전하게 계속 참조 ④ Grace Period → 해제 synchronize_rcu() / call_rcu() ← Grace Period → R1 완료 ✓ R2 완료 ✓ ptr new data (v2) kfree_rcu() old data (v1) 해제
RCU의 Read-Copy-Update 과정: 포인터 교체 후 grace period 동안 이전 데이터 유지

RCU 메모리 순서 보장

RCU의 정확성은 메모리 순서(memory ordering) 보장에 의존합니다. Writer가 새 데이터를 발행하고 Reader가 이를 참조할 때, CPU와 컴파일러의 재배치로 인해 불완전한 데이터가 보일 수 있습니다. RCU는 rcu_assign_pointer()rcu_dereference()라는 두 API로 이 문제를 해결합니다.

ℹ️

LKMM 연계: Linux Kernel Memory Model(LKMM)은 RCU의 메모리 순서 보장을 공식적으로 정의합니다. rcu_assign_pointer()는 LKMM의 release 의미론에, rcu_dereference()address dependency(C11의 consume에 해당)에 매핑됩니다. 자세한 내용은 tools/memory-model/ 디렉토리를 참고하세요.

Writer 측: store-release 의미론

rcu_assign_pointer(p, v)는 내부적으로 smp_store_release()를 사용합니다. 이는 포인터 저장 이전의 모든 메모리 쓰기가 포인터 저장보다 먼저 다른 CPU에 보이도록 보장합니다.

/* include/linux/rcupdate.h — rcu_assign_pointer 내부 */
#define rcu_assign_pointer(p, v)              \
do {                                              \
    smp_store_release(&(p), (typeof(p))(v));   \
} while (0)

/* 즉, Writer 코드에서: */
new_entry->field_a = 42;          /* ① 데이터 초기화 */
new_entry->field_b = "hello";     /* ② 데이터 초기화 */
rcu_assign_pointer(gptr, new_entry); /* ③ store-release: ①②가 반드시 ③ 전에 보임 */

Reader 측: address dependency 보장

rcu_dereference(p)는 포인터를 읽은 후 그 포인터를 통한 후속 접근이 재배치되지 않도록 보장합니다. 이는 하드웨어 address dependency와 컴파일러 배리어를 결합하여 구현됩니다.

/* include/linux/rcupdate.h — rcu_dereference 내부 */
#define rcu_dereference(p) \
    rcu_dereference_check(p, 0)
/* → READ_ONCE(p) + 컴파일러 배리어
 *   READ_ONCE는 volatile 시맨틱으로 컴파일러 최적화(값 캐싱) 방지
 *   하드웨어 address dependency는 대부분 아키텍처에서 자동 보장 */

/* Reader 코드에서: */
rcu_read_lock();
p = rcu_dereference(gptr);      /* ① 포인터 읽기 (address dependency 시작) */
if (p) {
    x = p->field_a;               /* ② dependency chain: ①의 값에 의존 → 재배치 불가 */
    y = p->field_b;               /* ③ 마찬가지로 ① 이후 보장 */
}
rcu_read_unlock();

아키텍처별 차이

아키텍처rcu_assign_pointer() 구현rcu_dereference() 구현비고
x86/x86_64컴파일러 배리어만 (TSO 보장)컴파일러 배리어만TSO(Total Store Order)로 store-release가 자연 보장
ARM64stlr (store-release 명령)address dependency + 컴파일러 배리어weak ordering이므로 명시적 release 필요
ARM32dmb (데이터 메모리 배리어)address dependency + 컴파일러 배리어full barrier 사용 (store-release 명령 없음)
PowerPClwsync (lightweight sync)address dependency + 컴파일러 배리어weak ordering, dependency ordering은 하드웨어 보장
RISC-Vfence rw,waddress dependency + 컴파일러 배리어RVWMO 모델, release fence 사용

배리어 비교: RCU vs 범용 배리어

메커니즘Writer 비용Reader 비용보장 범위용도
rcu_assign_pointer() + rcu_dereference()store-release (경량)address dependency (거의 0)포인터와 포인터가 가리키는 데이터 간 순서RCU 포인터 발행/참조
smp_store_release() + smp_load_acquire()store-releaseload-acquirerelease 이전 모든 쓰기 ↔ acquire 이후 모든 읽기범용 단방향 배리어 쌍
smp_wmb() + smp_rmb()write barrierread barrier쓰기 순서 / 읽기 순서 각각 보장전통적 배리어 쌍
smp_mb()full barrierfull barrier모든 방향 순서 보장최후 수단 (비용 큼)
⚠️

흔한 실수: rcu_dereference() 없이 RCU 보호 포인터를 직접 읽으면(p = gptr;), 컴파일러가 값을 레지스터에 캐싱하거나 접근 순서를 변경할 수 있습니다. 이는 CONFIG_PROVE_RCU가 켜져 있으면 lockdep 경고로 잡히지만, 꺼져 있으면 간헐적 데이터 손상으로 나타나 디버깅이 매우 어렵습니다.

RCU 메모리 순서 보장: Writer store-release & Reader dependency chain Writer (CPU 0) time new->field_a = 42 new->field_b = "hello" smp_store_release() 배리어 이 위의 모든 쓰기가 아래 store 전에 완료 보장 rcu_assign_pointer(gptr, new) 포인터 값이 다른 CPU의 캐시로 전파 kfree_rcu(old, rcu) /* GP 후 해제 */ Reader (CPU 1) time rcu_read_lock() p = rcu_dereference(gptr) address dependency chain p->field 접근은 p 로드 이후 보장 (하드웨어 의존성) x = p->field_a /* 42 보장 */ y = p->field_b /* "hello" 보장 */ rcu_read_unlock() 포인터 전파
Writer의 store-release가 데이터 초기화를 포인터 발행 전에 완료시키고, Reader의 address dependency가 포인터 로드 후의 필드 접근 순서를 보장합니다.

RCU 핵심 API

#include <linux/rcupdate.h>

/* === Reader Side === */
rcu_read_lock();                    /* RCU read-side critical section 시작 */
p = rcu_dereference(gbl_ptr);       /* 포인터 읽기 (memory barrier 포함) */
if (p)
    do_something(p->field);
rcu_read_unlock();                  /* RCU read-side critical section 종료 */

/* === Writer Side === */
struct my_data *old, *new;
new = kmalloc(sizeof(*new), GFP_KERNEL);
*new = *old;                        /* 복사 */
new->field = new_value;             /* 수정 */
rcu_assign_pointer(gbl_ptr, new);   /* 포인터 원자적 교체 */
synchronize_rcu();                  /* grace period 대기 (블로킹) */
kfree(old);                         /* 이전 데이터 해제 */

/* 또는 비동기 해제 */
kfree_rcu(old, rcu_head);           /* grace period 후 자동 해제 */

Read-Side API 상세

RCU read-side critical section은 rcu_read_lock()rcu_read_unlock() 쌍으로 구성됩니다. 내부 구현은 커널 선점 설정에 따라 다릅니다.

rcu_read_lock() 내부 동작

/* include/linux/rcupdate.h */

/* non-preemptible 커널 (CONFIG_PREEMPT_COUNT=n):
 *   rcu_read_lock()은 컴파일러 배리어(barrier())만 삽입.
 *   선점이 불가하므로 컨텍스트 스위치 자체가 QS 신호가 됨.
 */
static inline void rcu_read_lock(void)
{
    __rcu_read_lock();  /* non-preempt: preempt_disable() */
    __acquire(RCU);    /* sparse 어노테이션 */
    rcu_lock_acquire(&rcu_lock_map);  /* lockdep */
    RCU_LOCKDEP_WARN(!rcu_is_watching(),
        "rcu_read_lock() used illegally while idle");
}

/* preemptible 커널 (CONFIG_PREEMPT_RCU=y):
 *   per-task nesting 카운터를 증감.
 *   선점되어도 RCU read-side를 추적할 수 있음.
 */
void __rcu_read_lock(void)
{
    current->rcu_read_lock_nesting++;
    if (IS_ENABLED(CONFIG_PROVE_RCU))
        WARN_ON_ONCE(current->rcu_read_lock_nesting > RCU_NEST_MAX);
    barrier();  /* 컴파일러 재배치 방지 */
}

rcu_read_lock_bh() / rcu_read_unlock_bh()

Bottom-half(softirq)를 비활성화하는 RCU read-side 진입점입니다. 최신 커널(v5.0+)에서는 RCU-bh가 통합 RCU로 합쳐졌지만, API 자체는 하위 호환성을 위해 유지됩니다.

/* rcu_read_lock_bh(): softirq 비활성화 + RCU read-side 진입 */
static inline void rcu_read_lock_bh(void)
{
    local_bh_disable();
    __acquire(RCU_BH);
    rcu_lock_acquire(&rcu_bh_lock_map);
}

/* rcu_read_unlock_bh(): softirq 재활성화 + RCU read-side 퇴장 */
static inline void rcu_read_unlock_bh(void)
{
    rcu_lock_release(&rcu_bh_lock_map);
    __release(RCU_BH);
    local_bh_enable();
}

/* 사용 예: 네트워크 패킷 수신 경로 */
rcu_read_lock_bh();
hlist_for_each_entry_rcu(sk, &udp_table->hash[slot], node) {
    if (udp_match(sk, saddr, daddr, sport, dport))
        udp_queue_rcv_skb(sk, skb);
}
rcu_read_unlock_bh();

rcu_read_lock_sched() / rcu_read_unlock_sched()

선점을 비활성화하여 RCU-sched 보호를 제공합니다. 최신 커널에서는 rcu_read_lock()과 동일한 효과이나, 명시적으로 선점 비활성화 의미를 전달할 때 사용합니다.

static inline void rcu_read_lock_sched(void)
{
    preempt_disable();
    __acquire(RCU_SCHED);
    rcu_lock_acquire(&rcu_sched_lock_map);
}

static inline void rcu_read_unlock_sched(void)
{
    rcu_lock_release(&rcu_sched_lock_map);
    __release(RCU_SCHED);
    preempt_enable();
}

rcu_read_lock_held() 패밀리

현재 컨텍스트가 올바른 RCU read-side critical section 내부에 있는지 검증하는 함수들입니다. CONFIG_PROVE_RCU=y 빌드에서 lockdep 검증에 사용됩니다.

함수검증 대상반환값
rcu_read_lock_held()일반 RCU read-side1: 보유 중, 0: 미보유
rcu_read_lock_bh_held()RCU-bh read-side (또는 softirq 비활성)1: 보유 중, 0: 미보유
rcu_read_lock_sched_held()RCU-sched read-side (또는 선점 비활성)1: 보유 중, 0: 미보유
rcu_read_lock_any_held()위 3가지 중 하나라도 보유1: 하나 이상 보유, 0: 전부 미보유
rcu_read_lock_trace_held()RCU-Tasks-Trace read-side1: 보유 중, 0: 미보유
srcu_read_lock_held(sp)특정 SRCU 도메인 read-side1: 보유 중, 0: 미보유
/* rcu_dereference_check()와 결합한 사용 예 */
p = rcu_dereference_check(gbl_ptr,
        rcu_read_lock_held() ||
        lockdep_is_held(&my_mutex));

/* RCU_LOCKDEP_WARN: 조건이 참이면 경고 출력 */
RCU_LOCKDEP_WARN(!rcu_read_lock_held(),
    "RCU read lock not held");

guard(rcu) / scoped_guard(rcu)

C scope 기반 자동 언락 패턴입니다. 블록을 벗어나면 자동으로 rcu_read_unlock()이 호출됩니다 (Linux 6.5+).

#include <linux/cleanup.h>

/* guard(rcu): 현재 scope 종료 시 자동 rcu_read_unlock() */
{
    guard(rcu)();
    p = rcu_dereference(gbl_ptr);
    if (!p)
        return -ENOENT;  /* 자동으로 rcu_read_unlock() 호출됨 */
    val = p->field;
}   /* ← 여기서도 자동 rcu_read_unlock() */

/* scoped_guard(rcu): 블록 내에서만 RCU 보호 */
scoped_guard(rcu) {
    p = rcu_dereference(gbl_ptr);
    process(p);
}   /* ← rcu_read_unlock() 자동 */

/* guard(rcu)는 rcu_read_lock_bh, rcu_read_lock_sched에도 적용 가능 */
guard(rcu_bh)();    /* rcu_read_lock_bh() + 자동 unlock */
guard(rcu_sched)(); /* rcu_read_lock_sched() + 자동 unlock */
💡

guard(rcu) 장점: 조기 리턴(return), goto, 예외 경로에서 rcu_read_unlock() 누락을 원천 차단합니다. 새 코드에서는 적극 활용을 권장합니다.

포인터 접근 API 상세

RCU 포인터 접근 API는 컴파일러/CPU 재배치를 방지하여 올바른 의존성 순서를 보장합니다. 컨텍스트에 따라 올바른 변형을 선택해야 합니다.

함수사용 컨텍스트lockdep 검증의존성 순서
rcu_dereference(p)RCU read-side CS 내부rcu_read_lock_held()O
rcu_dereference_bh(p)RCU-bh read-side CS 내부rcu_read_lock_bh_held()O
rcu_dereference_sched(p)RCU-sched read-side CS 내부rcu_read_lock_sched_held()O
rcu_dereference_protected(p, c)Writer (잠금 보유 상태)조건 c 검증불필요 (단일 writer)
rcu_dereference_check(p, c)Reader 또는 Writerrcu_read_lock_held() || cO
rcu_dereference_raw(p)특수 상황 (부팅 초기 등)없음O
rcu_dereference_raw_check(p)NULL 허용 raw없음O
srcu_dereference(p, sp)SRCU read-side CS 내부srcu_read_lock_held(sp)O
srcu_dereference_check(p, sp, c)SRCU Reader 또는 Writersrcu_read_lock_held(sp) || cO
rcu_dereference_protected_check(p, c)Writer 전용 (조건부)c 검증불필요
/* rcu_dereference() 변형 — 컨텍스트에 따라 올바른 함수 선택 */

/* 1. 일반 RCU read-side critical section 내에서 */
p = rcu_dereference(gbl_ptr);

/* 2. Writer 경로 — 이미 다른 잠금으로 보호된 경우 */
/*    rcu_read_lock 없이도 사용 가능 (lockdep 검증 포함) */
spin_lock(&my_lock);
p = rcu_dereference_protected(gbl_ptr,
        lockdep_is_held(&my_lock));  /* 잠금 보유 증명 */
spin_unlock(&my_lock);

/* 3. SRCU read-side critical section 내에서 */
idx = srcu_read_lock(&my_srcu);
p = srcu_dereference(gbl_ptr, &my_srcu);
srcu_read_unlock(&my_srcu, idx);

/* 4. 디버그 빌드 전용 — rcu_read_lock 없이 접근 (테스트용) */
p = rcu_dereference_raw(gbl_ptr);  /* 배리어만, lockdep 검증 없음 */

/* 5. Reader 또는 Writer 양쪽에서 사용 — 조건부 검증 */
p = rcu_dereference_check(gbl_ptr,
        lockdep_is_held(&my_lock));  /* RCU lock 또는 my_lock 보유 시 OK */

rcu_access_pointer()

rcu_access_pointer()는 의존성 순서(dependency ordering) 없이 RCU 포인터의 값만 확인합니다. 포인터가 가리키는 데이터에 접근하지 않고, NULL 여부나 변경 여부만 체크할 때 사용합니다.

/* rcu_access_pointer(): 포인터 값만 확인, 역참조 금지 */
if (rcu_access_pointer(callback) != NULL) {
    /* 콜백이 등록되어 있음 — 이 시점에서 callback 역참조 금지! */
    /* 실제 사용은 rcu_read_lock() 내에서 rcu_dereference()로 */
}

/* 올바른 사용 패턴: NULL 체크 후 별도로 역참조 */
if (rcu_access_pointer(dev->rx_handler)) {
    rcu_read_lock();
    handler = rcu_dereference(dev->rx_handler);
    if (handler)
        ret = handler(&skb);
    rcu_read_unlock();
}
⚠️

rcu_access_pointer vs rcu_dereference 구분: rcu_access_pointer()는 의존성 배리어가 없으므로 반환된 포인터를 역참조하면 안 됩니다. 단지 "포인터가 NULL인가?", "포인터가 변경되었는가?" 같은 질문에만 사용합니다. 데이터를 읽으려면 반드시 rcu_dereference()를 사용하세요.

rcu_replace_pointer()

rcu_replace_pointer()는 RCU 포인터를 원자적으로 교체하고 이전 값을 반환합니다. rcu_assign_pointer() + 이전 값 저장을 한 번에 수행합니다.

/* rcu_replace_pointer(rcu_ptr, new, condition)
 * = rcu_dereference_protected() + rcu_assign_pointer() 원자 결합
 * 반환값: 이전 포인터 값 */

spin_lock(&my_lock);
old = rcu_replace_pointer(gbl_ptr, new_data,
        lockdep_is_held(&my_lock));
spin_unlock(&my_lock);

/* old를 grace period 후 해제 */
if (old)
    kfree_rcu(old, rcu_head);

/* 매크로 내부 구현 */
#define rcu_replace_pointer(rcu_ptr, ptr, c) \
({ \
    typeof(ptr) __tmp = rcu_dereference_protected((rcu_ptr), (c)); \
    rcu_assign_pointer((rcu_ptr), (ptr)); \
    __tmp; \
})
💡

선택 기준: Reader 경로는 rcu_dereference(), Writer가 이미 잠금을 보유한 상태에서 포인터를 읽어야 하면 rcu_dereference_protected()를 사용하세요. 후자는 불필요한 rcu_read_lock()을 추가하지 않아도 되며, CONFIG_PROVE_RCU 빌드에서 lockdep이 잠금 보유 여부를 자동 검증합니다.

포인터 발행 API 상세

포인터 발행(publish) API는 writer가 새 데이터를 reader에게 안전하게 공개하는 메커니즘입니다. store-release 의미론으로 데이터 초기화가 포인터 공개 전에 완료됨을 보장합니다.

rcu_assign_pointer() 심층

/* rcu_assign_pointer(p, v): RCU 포인터 발행
 *
 * 내부 구현: smp_store_release() 기반
 * - 이전의 모든 메모리 쓰기가 완료된 후 포인터를 공개
 * - reader가 rcu_dereference()로 읽을 때 초기화된 데이터를 보장
 *
 * 순서 보장:
 *   new->field = value;             ← ① 데이터 초기화
 *   rcu_assign_pointer(gbl, new);   ← ② 포인터 공개 (store-release)
 *   // reader는 ②를 통해 포인터를 얻으면 ①이 완료된 상태임을 보장받음
 */
#define rcu_assign_pointer(p, v) \
do { \
    uintptr_t _r_a_p__v = (uintptr_t)(v); \
    rcu_check_sparse(p, __rcu); \
    if (__builtin_constant_p(v) && (_r_a_p__v) == (uintptr_t)NULL) \
        WRITE_ONCE((p), (typeof(p))(_r_a_p__v)); \
    else \
        smp_store_release(&(p), RCU_INITIALIZER((typeof(p))_r_a_p__v)); \
} while (0)

RCU_INIT_POINTER()

RCU_INIT_POINTER()는 메모리 배리어 없이 RCU 포인터를 설정합니다. 초기화 시점에 아직 reader가 접근할 수 없는 상황이거나, NULL로 설정할 때 사용합니다.

/* RCU_INIT_POINTER(p, v): 배리어 없는 RCU 포인터 초기화
 *
 * 사용 조건 (하나 이상 만족해야 함):
 *   1. 다른 CPU가 아직 포인터에 접근할 수 없는 초기화 단계
 *   2. NULL로 설정 (데이터가 없으므로 의존성 순서 불필요)
 *   3. 구조체가 아직 공개되지 않은 상태
 *
 * 위 조건을 만족하지 않으면 반드시 rcu_assign_pointer() 사용!
 */
#define RCU_INIT_POINTER(p, v) \
    WRITE_ONCE((p), RCU_INITIALIZER(v))

/* 올바른 사용 예 */
struct my_struct *obj = kzalloc(sizeof(*obj), GFP_KERNEL);
obj->callback = my_func;
RCU_INIT_POINTER(obj->rcu_ptr, NULL);  /* OK: NULL 설정 */

/* 초기화 완료 후 공개 시에는 rcu_assign_pointer() 사용 */
rcu_assign_pointer(global_obj, obj);

/* ✗ 잘못된 사용: 이미 공개된 포인터를 non-NULL로 변경 */
RCU_INIT_POINTER(global_obj, new_obj);  /* BUG: 배리어 없음! */
⚠️

RCU_INIT_POINTER vs rcu_assign_pointer: RCU_INIT_POINTER()는 배리어가 없어 빠르지만, 이미 reader가 접근 가능한 포인터를 non-NULL 값으로 변경하는 데 사용하면 reader가 초기화되지 않은 데이터를 볼 수 있습니다. 확신이 없으면 항상 rcu_assign_pointer()를 사용하세요.

Grace Period API 상세

synchronize_rcu() 내부 동작

/* synchronize_rcu(): 현재 진행 중인 모든 RCU read-side CS가
 * 종료될 때까지 블로킹 대기.
 *
 * 내부 흐름:
 *   1. 새 grace period 시작 요청 (또는 이미 진행 중인 GP에 합류)
 *   2. rcu_gp_kthread가 모든 CPU에 QS 보고 요청
 *   3. 모든 CPU가 QS를 보고하면 GP 완료
 *   4. 호출자에게 제어 반환
 *
 * 주의: process 컨텍스트에서만 호출 가능 (블로킹 함수)
 *       인터럽트, softirq, atomic 컨텍스트에서 호출 금지
 */
synchronize_rcu();

/* synchronize_net(): synchronize_rcu()의 별칭
 * 네트워크 서브시스템에서 의미론적 명확성을 위해 사용 */
static inline void synchronize_net(void)
{
    synchronize_rcu();
}

call_rcu() 내부

/* call_rcu(&obj->rcu, callback): 비동기 RCU 콜백 등록
 *
 * 내부 동작:
 *   1. 현재 CPU의 rcu_data->cblist (segmented callback list)에 추가
 *   2. segcb 리스트는 4개 세그먼트로 분류:
 *      - RCU_DONE_TAIL:   GP 완료된 콜백 (실행 대기)
 *      - RCU_WAIT_TAIL:   현재 GP 대기 중
 *      - RCU_NEXT_READY_TAIL: 다음 GP에 할당 예정
 *      - RCU_NEXT_TAIL:   새로 등록된 콜백
 *   3. GP 완료 시 콜백이 RCU_DONE_TAIL로 이동
 *   4. softirq(RCU_SOFTIRQ) 또는 rcuo kthread에서 실행
 */
void call_rcu(struct rcu_head *head,
             rcu_callback_t func)
{
    __call_rcu_common(head, func, false);
}

/* call_rcu_hurry(): 긴급 콜백 — GP 시작을 즉시 요청 */
void call_rcu_hurry(struct rcu_head *head,
                    rcu_callback_t func)
{
    __call_rcu_common(head, func, true);  /* lazy=false 강제 */
}

kfree_rcu 변형

APIrcu_head 필요sleep 가능설명
kfree_rcu(ptr, rhf)O (필드명 지정)XGP 후 kfree. rcu_head 오프셋으로 container_of 역산
kfree_rcu_mightsleep(ptr)XOrcu_head 없는 구조체. 내부적으로 GP 대기 후 kfree
kvfree_rcu(ptr, rhf)OXkvmalloc으로 할당된 메모리 해제 (kmalloc/vmalloc 자동 구분)
kvfree_rcu_mightsleep(ptr)XOkvfree 버전의 mightsleep 변형
/* kfree_rcu: rcu_head 필드가 있는 경우 */
struct my_data {
    int value;
    struct rcu_head rcu;  /* 16바이트 추가 */
};
kfree_rcu(old, rcu);

/* kfree_rcu_mightsleep: rcu_head 없이 사용 (process 컨텍스트 전용) */
struct small_data {
    int key;
    int val;
    /* rcu_head 없음 — 구조체가 작아서 16바이트 낭비 방지 */
};
kfree_rcu_mightsleep(old);  /* 내부: synchronize_rcu() + kfree() */

/* kvfree_rcu: kvmalloc()으로 할당한 메모리에 사용 */
buf = kvmalloc(large_size, GFP_KERNEL);
/* ... 사용 후 ... */
kvfree_rcu(buf, rcu);  /* kmalloc이면 kfree, vmalloc이면 vfree */

rcu_barrier() 심층

/* rcu_barrier(): 모든 CPU에 등록된 call_rcu() 콜백이
 * 완료될 때까지 블로킹 대기.
 *
 * synchronize_rcu()와의 차이:
 *   - synchronize_rcu(): 현재 GP만 대기 (콜백 실행은 보장 안 함)
 *   - rcu_barrier(): 현재 등록된 모든 콜백의 실행 완료까지 대기
 *
 * 내부 동작:
 *   1. 각 CPU에 sentinel 콜백 등록
 *   2. 모든 sentinel 콜백이 실행될 때까지 대기
 *   3. sentinel 실행 = 그 CPU의 이전 콜백 모두 완료
 *
 * 주요 사용처: module_exit()에서 모듈 코드를 참조하는
 *             콜백이 모두 완료된 것을 보장
 */
rcu_barrier();

/* 관련 API */
srcu_barrier(&my_srcu);        /* SRCU 콜백 대기 */
rcu_barrier_tasks();           /* RCU-Tasks 콜백 대기 */
rcu_barrier_tasks_trace();     /* RCU-Tasks-Trace 콜백 대기 */

Polled Grace Period API

Polled GP API는 synchronize_rcu()처럼 블로킹하지 않고, GP 완료 여부를 비블로킹으로 폴링할 수 있는 API입니다. 비동기적으로 GP 완료를 확인해야 하는 상황에서 유용합니다.

블로킹 GP vs Polled GP 타임라인 비교 블로킹 방식: synchronize_rcu() Writer update synchronize_rcu() — 블로킹 대기 (수십 ms) kfree(old) CPU가 블로킹되어 다른 작업 불가 (프로세스 슬립) 폴링 방식: start_poll + poll_state Writer update start_poll() 다른 작업 수행 (블로킹 없음) poll: false 추가 작업 poll: true! kfree(old) ← Grace Period (백그라운드) → CPU가 자유롭게 다른 작업 수행 가능 — 주기적으로 GP 완료 여부만 확인 Polled GP: 동일한 안전성, 더 나은 CPU 활용
Polled Grace Period: synchronize_rcu()와 달리 블로킹하지 않고 비동기적으로 GP 완료를 확인합니다.
함수동작반환값
get_state_synchronize_rcu()현재 GP 상태 스냅샷 획득 (GP 시작 요청 안 함)GP 쿠키 (unsigned long)
start_poll_synchronize_rcu()새 GP 시작 요청 + 상태 스냅샷 획득GP 쿠키
poll_state_synchronize_rcu(cookie)GP 완료 여부 비블로킹 확인true: 완료, false: 미완료
cond_synchronize_rcu(cookie)GP 미완료 시 블로킹, 이미 완료면 즉시 반환void
get_completed_synchronize_rcu()이미 완료된 GP를 나타내는 쿠키 반환GP 쿠키 (항상 "완료")
same_state_synchronize_rcu(c1, c2)두 쿠키가 같은 GP를 참조하는지 확인true: 같은 GP
/* Polled GP 사용 패턴: 비블로킹 GP 대기 */
unsigned long cookie;

/* 1단계: 업데이트 후 GP 시작 요청 + 쿠키 획득 */
rcu_assign_pointer(gbl_ptr, new_data);
cookie = start_poll_synchronize_rcu();

/* 2단계: 다른 작업 수행 (블로킹 없음) */
do_other_work();

/* 3단계: GP 완료 여부 폴링 */
if (poll_state_synchronize_rcu(cookie)) {
    /* GP 완료 — old_data 안전하게 해제 가능 */
    kfree(old_data);
} else {
    /* 아직 미완료 — 나중에 다시 확인하거나 cond_synchronize_rcu() */
    cond_synchronize_rcu(cookie);  /* 미완료시만 블로킹 */
    kfree(old_data);
}

/* 활용 예: 이미 완료된 GP 쿠키로 즉시 처리 */
cookie = get_completed_synchronize_rcu();
cond_synchronize_rcu(cookie);  /* 항상 즉시 반환 */
💡

Polled GP 사용 시점: (1) GP 대기 중 CPU를 낭비하고 싶지 않을 때, (2) 여러 객체의 해제를 배치 처리할 때, (3) 상태 머신에서 비동기적으로 GP 완료를 확인해야 할 때 유용합니다. 단순 해제는 kfree_rcu()가 더 편리합니다.

Expedited Grace Period 제어 API

Expedited GP는 IPI를 사용해 모든 CPU에 강제 QS를 유발하여 GP를 빠르게 완료합니다. 시스템 전체에 영향을 주므로 제어 API가 제공됩니다.

함수동작사용 시점
synchronize_rcu_expedited()IPI로 강제 GP 완료 (수 ms)모듈 로드/언로드, 초기화
rcu_expedite_gp()이후 synchronize_rcu()를 expedited로 전환부팅 초기, 일시적 가속
rcu_unexpedite_gp()expedited 모드 해제 (중첩 카운터 감소)가속 구간 종료 후
rcu_gp_is_expedited()현재 expedited 모드 여부 확인디버깅/로깅
/* Expedited GP: IPI로 모든 CPU에 강제 QS 유발 */
synchronize_rcu_expedited();  /* 빠르지만 CPU간 IPI 비용 발생 */

/* expedited 모드 전환: 부팅 초기 가속 예 */
rcu_expedite_gp();    /* 이후 synchronize_rcu()가 expedited로 동작 */
/* ... 부팅 초기화 작업 ... */
synchronize_rcu();    /* 내부적으로 expedited로 실행됨 */
rcu_unexpedite_gp(); /* 정상 모드 복귀 (중첩 카운터 기반) */

/* 콜백 버전: expedited GP 불필요 */
call_rcu(&p->rcu, my_callback);   /* 비동기: 정상 GP 사용 */
⚠️

Expedited GP 사용 제한:

  • 허용: 모듈 로드/언로드, 시스템 초기화, 일회성 관리 경로 (MODULE_INIT, sysctl 변경 등)
  • 금지: 핫 경로(hot path), 반복 호출 루프 — 모든 CPU에 IPI를 전송하므로 실시간 응답성이 저하되고 대규모 시스템에서 성능 병목이 됩니다.

커널은 CONFIG_RCU_EXP_KTHREAD=y를 통해 expedited GP를 전용 kthread로 오프로드할 수 있습니다.

__rcu Sparse Annotation

__rcu 어노테이션은 RCU 포인터임을 명시하는 sparse 정적 분석 마커입니다. 컴파일러에게는 영향을 주지 않지만, sparse 도구로 RCU API 오용을 감지합니다.

/* __rcu 어노테이션 사용 */
struct my_struct {
    struct data __rcu *ptr;   /* RCU 보호 포인터임을 명시 */
    int value;
};

/* sparse 검사가 감지하는 오류들:
 *   1. __rcu 포인터를 rcu_dereference() 없이 직접 역참조
 *   2. rcu_assign_pointer() 대신 직접 대입
 *   3. rcu_dereference()에 non-__rcu 포인터 전달
 */

/* ✗ sparse 경고 발생 */
p = obj->ptr;                     /* warning: dereference of noderef */
obj->ptr = new_data;              /* warning: assignment to noderef */

/* ✓ 올바른 사용 */
p = rcu_dereference(obj->ptr);
rcu_assign_pointer(obj->ptr, new_data);

/* sparse 실행: */
/* make C=2 CF="-D__CHECK_ENDIAN__" drivers/my_driver.o */

Grace Period

Grace period는 rcu_assign_pointer() 호출 시점에 이미 진행 중인 모든 RCU read-side critical section이 종료될 때까지의 기간입니다. Grace period가 끝나면 이전 데이터에 접근하는 reader가 없음이 보장됩니다.

ℹ️

rcu_read_lock()은 선점만 비활성화하며 (non-preemptible 커널에서는 no-op), 매우 가벼운 연산입니다. spinlock이나 mutex와 달리 캐시라인 바운싱이 없어 확장성이 뛰어납니다.

Grace Period & Quiescent State 타임라인 time → Reader A Reader B CPU idle Writer Grace Period rcu_read_lock() rcu_read_unlock() → QS ✓ rcu_read_lock() rcu_read_unlock() → QS ✓ IDLE CTX_SWITCH idle / context-switch = Quiescent State 자동 인식 rcu_assign_pointer() synchronize_rcu() 블로킹 대기 반환 후 kfree(old) ← Grace Period (모든 Reader의 QS 완료까지) → A QS B QS → GP 종료
Grace Period: 모든 진행 중인 RCU read-side critical section이 완료(Quiescent State)될 때까지의 구간. CPU idle 진입이나 context switch도 QS로 인식됩니다.

Expedited Grace Period 개념

synchronize_rcu_expedited()는 IPI를 사용해 모든 CPU에 강제 QS를 유발하여 GP를 수 밀리초 이내로 단축합니다. 상세 API와 제어 함수는 Expedited Grace Period 제어 API 섹션을 참고하세요.

Grace Period Kthread 동작 상세

RCU grace period는 전용 커널 스레드 rcu_gp_kthread()가 관리합니다. 이 kthread는 GP 시작, FQS(Force-Quiescent-State) 스캔, GP 정리를 반복하는 상태 머신입니다.

gp_seq 시퀀스 번호 진행

Grace period는 gp_seq 카운터의 하위 2비트로 4단계를 구분합니다:

gp_seq & 0x3단계설명
0b00idleGP가 진행 중이지 않음
0b01GP startedGP 시작됨, qsmask 초기화 진행 중
0b10GP in progress모든 노드의 qsmask 설정 완료, QS 대기 중
0b11GP cleanup모든 QS 수집 완료, 콜백 실행 준비
/* kernel/rcu/tree.c — rcu_gp_kthread() 메인 루프 (간략화) */
static int rcu_gp_kthread(void *unused)
{
    for (;;) {
        /* ① GP 대기: 콜백이 등록될 때까지 sleep */
        swait_event_idle_exclusive(rsp->gp_wq,
            READ_ONCE(rsp->gp_flags) & RCU_GP_FLAG_INIT);

        /* ② GP 초기화: rcu_node 트리의 qsmask 설정 */
        rcu_gp_init();  /* gp_seq += 1 (idle→started) */

        /* ③ FQS 루프: 주기적으로 미보고 CPU 확인 */
        do {
            rcu_gp_fqs();     /* force_qs_rnp()로 각 rcu_node 스캔 */
            cond_resched();
            msleep(jiffies_till_next_fqs);
        } while (!rcu_gp_fqs_check_wake());

        /* ④ GP 정리: 콜백 큐 진행, gp_seq 완료 */
        rcu_gp_cleanup();  /* gp_seq += 1 (cleanup→idle) */
    }
}

Force-Quiescent-State (FQS) 메커니즘

FQS는 GP가 지연될 때 미보고 CPU를 능동적으로 확인하는 메커니즘입니다:

/* kernel/rcu/tree.c — FQS 스캔 (간략화) */
static void force_qs_rnp(int (*f)(struct rcu_data *rdp))
{
    rcu_for_each_leaf_node(rnp) {
        mask = rnp->qsmask;
        for_each_leaf_node_cpu_mask(rnp, cpu, mask) {
            rdp = per_cpu_ptr(&rcu_data, cpu);
            if (f(rdp))  /* dynticks 카운터로 QS 여부 확인 */
                mask &= ~rdp->grpmask;  /* QS 통과한 CPU 클리어 */
        }
        if (mask != rnp->qsmask)
            rcu_report_qs_rnp(mask ^ rnp->qsmask, rnp);
    }
}

Expedited GP: IPI 기반 강제 QS

Expedited grace period(synchronize_rcu_expedited())는 일반 FQS 대신 IPI(Inter-Processor Interrupt)를 사용하여 non-idle CPU에 즉시 QS를 강제합니다:

💡

FQS 주기 튜닝: rcutree.jiffies_till_first_fqsrcutree.jiffies_till_next_fqs 부트 파라미터로 FQS 스캔 주기를 조정할 수 있습니다. 값이 작으면 GP가 빨리 완료되지만 CPU 오버헤드가 증가합니다.

rcu_gp_kthread() 상태 머신 IDLE (Sleep) gp_seq & 0x3 = 0b00 GP Init qsmask 설정, gp_seq++ FQS Loop 미보고 CPU 스캔 GP Cleanup 콜백 큐 진행, gp_seq++ Callback Invoke RCU_SOFTIRQ / rcuo kthread callback 등록 wakeup qsmask 설정 완료 FQS 반복 모든 QS 수집 세그멘트 이동 다음 GP 대기 (또는 즉시 시작) Expedited GP 경로 (synchronize_rcu_expedited) FQS 대신 IPI로 non-idle CPU에 즉시 QS 강제 → GP 수 ms 이내 완료 (일반 GP: 수십 ms ~ 수 초)
rcu_gp_kthread()는 IDLE → GP Init → FQS Loop → GP Cleanup 순환으로 grace period를 관리합니다. Expedited GP는 IPI를 사용해 이 과정을 대폭 단축합니다.

RCU 보호 연결 리스트

list_for_each_entry_rcu()는 doubly-linked list(struct list_head)를 순회하며, hlist_for_each_entry_rcu()는 단방향 해시 버킷(struct hlist_head)에 사용합니다. 해시 테이블처럼 버킷이 많고 각 체인이 짧을 때는 hlist가 메모리 효율적입니다.

RCU 리스트 연산 — 포인터 교체 과정 list_add_rcu(&new->list, head) Before: head A B After: head NEW A B ① NEW->next = A (배리어 전), ② head->next = NEW (smp_store_release) list_del_rcu(&entry->list) Before: A B C After: A C B B->next는 유지 (reader 안전) list_replace_rcu(&old->list, &new->list) Before: A OLD C After: A NEW C 핵심 규칙: • list_add_rcu(): 삽입 전 new->next를 먼저 설정한 후, smp_store_release로 prev->next를 새 노드로 교체 • list_del_rcu(): 전후 노드를 연결하되, 삭제된 노드의 next 포인터는 유지 (기존 reader가 순회 가능) • list_replace_rcu(): 원자적으로 old를 new로 대체, old->next 유지 • 삭제/교체된 노드는 반드시 kfree_rcu() 또는 call_rcu()로 GP 후 해제 (즉시 kfree = UAF)
RCU 리스트 연산: 포인터 교체 과정에서 기존 reader의 안전한 순회를 보장합니다. 삭제된 노드의 next 포인터가 유지되는 것이 핵심입니다.

리스트 변경 API

모든 RCU 리스트 변경 함수는 writer 측에서 호출하며, 외부 잠금(spinlock 등)으로 보호해야 합니다. 내부적으로 rcu_assign_pointer() 의미론을 사용하여 포인터 공개 전 데이터 초기화를 보장합니다.

함수동작비고
list_add_rcu(new, head)head 다음에 new 삽입smp_store_release 사용
list_add_tail_rcu(new, head)리스트 끝에 new 삽입tail 삽입
list_del_rcu(entry)entry 제거 (next 포인터 유지)entry->next 건드리지 않음
list_replace_rcu(old, new)old를 new로 원자적 교체old->next 유지
list_splice_init_rcu(list, head, sync)리스트 합치기 (RCU 안전)sync에 synchronize_rcu 전달
INIT_LIST_HEAD_RCU(ptr)RCU 리스트 헤드 초기화WRITE_ONCE 기반
/* 리스트에 추가 */
spin_lock(&list_lock);
list_add_rcu(&new->list, &my_list);  /* rcu_assign_pointer() 의미론 */
spin_unlock(&list_lock);

/* 리스트 끝에 추가 */
spin_lock(&list_lock);
list_add_tail_rcu(&new->list, &my_list);
spin_unlock(&list_lock);

/* 리스트에서 삭제: list_del_rcu + kfree_rcu 패턴 */
spin_lock(&list_lock);
list_del_rcu(&old->list);           /* 순회 중 reader에게는 여전히 유효 */
spin_unlock(&list_lock);
kfree_rcu(old, rcu_head);           /* Grace Period 후 자동 해제 */

/* list_del_rcu 주의: 삭제 후 해당 노드를 즉시 kfree()하면 UAF!
 * 반드시 kfree_rcu() 또는 call_rcu()를 사용해야 합니다. */

/* list_replace_rcu: 원자적 교체 */
spin_lock(&list_lock);
list_replace_rcu(&old->list, &new->list);
spin_unlock(&list_lock);
kfree_rcu(old, rcu_head);

/* list_splice_init_rcu: 두 리스트를 RCU 안전하게 합치기 */
spin_lock(&lock);
list_splice_init_rcu(&src_list, &dst_list, synchronize_rcu);
spin_unlock(&lock);

리스트 순회 API

매크로동작조건
list_for_each_entry_rcu(pos, head, member)RCU read-side에서 리스트 순회rcu_read_lock() 필요
list_for_each_entry_rcu(pos, head, member, cond)조건부 lockdep 검증 순회 (4인자)cond으로 잠금 보유 증명
list_for_each_entry_lockless(pos, head, member)lockless 순회 (READ_ONCE 기반)RCU/lock 불필요, 배리어 없음
list_entry_rcu(ptr, type, member)리스트 노드에서 구조체 포인터 획득rcu_dereference 내장
list_first_or_null_rcu(ptr, type, member)첫 번째 요소 또는 NULL 반환rcu_read_lock() 필요
list_next_or_null_rcu(head, ptr, type, member)다음 요소 또는 NULL (끝이면)rcu_read_lock() 필요
list_entry_lockless(ptr, type, member)lockless 버전의 list_entry배리어 없음
/* 기본 순회 */
rcu_read_lock();
list_for_each_entry_rcu(entry, &my_list, list) {
    process(entry);
}
rcu_read_unlock();

/* 4인자 변형: Writer가 잠금 보유 중 순회 */
list_for_each_entry_rcu(entry, &my_list, list,
        lockdep_is_held(&my_lock)) {
    modify(entry);
}

/* list_first_or_null_rcu: 첫 요소 안전 접근 */
rcu_read_lock();
entry = list_first_or_null_rcu(&my_list,
        struct my_struct, list);
if (entry)
    val = entry->value;
rcu_read_unlock();

/* list_next_or_null_rcu: 안전한 다음 요소 접근 */
rcu_read_lock();
next = list_next_or_null_rcu(&my_list, &current->list,
        struct my_struct, list);
rcu_read_unlock();

RCU hlist 연산

hlist(hash list)는 list_head보다 메모리 효율적인 단방향 연결 리스트입니다. 해시 테이블의 버킷처럼 헤드가 많고 각 체인이 짧을 때 최적입니다. hlist_head는 포인터 1개(8바이트), list_head는 포인터 2개(16바이트)입니다.

hlist 변경 API

함수동작
hlist_add_head_rcu(n, h)hlist 헤드에 노드 삽입
hlist_add_before_rcu(n, next)next 앞에 삽입
hlist_add_behind_rcu(n, prev)prev 뒤에 삽입
hlist_add_tail_rcu(n, h)hlist 끝에 삽입
hlist_del_rcu(n)노드 제거 (pprev 포이즌, next 유지)
hlist_del_init_rcu(n)노드 제거 + 재초기화 (재삽입 가능)
hlist_replace_rcu(old, new)old를 new로 원자적 교체
/* hlist 기본 연산 */
struct my_entry {
    int key;
    int value;
    struct hlist_node node;
    struct rcu_head rcu;
};

/* 삽입 */
spin_lock(&bucket_lock);
hlist_add_head_rcu(&entry->node, &bucket->head);
spin_unlock(&bucket_lock);

/* 삭제 */
spin_lock(&bucket_lock);
hlist_del_rcu(&entry->node);
spin_unlock(&bucket_lock);
kfree_rcu(entry, rcu);

/* 교체 */
spin_lock(&bucket_lock);
hlist_replace_rcu(&old->node, &new->node);
spin_unlock(&bucket_lock);
kfree_rcu(old, rcu);

/* hlist_del_init_rcu: 삭제 후 재삽입이 필요할 때 */
spin_lock(&lock);
hlist_del_init_rcu(&entry->node);  /* 노드를 빈 상태로 초기화 */
spin_unlock(&lock);
/* GP 후 다른 버킷에 재삽입 가능 */
synchronize_rcu();
spin_lock(&new_lock);
hlist_add_head_rcu(&entry->node, &new_bucket->head);
spin_unlock(&new_lock);

hlist 순회 API

/* 기본 hlist RCU 순회 */
rcu_read_lock();
hlist_for_each_entry_rcu(entry, &bucket->head, node) {
    if (entry->key == target_key) {
        result = entry->value;
        break;
    }
}
rcu_read_unlock();

/* 조건부 lockdep 변형 */
hlist_for_each_entry_rcu(entry, &bucket->head, node,
        lockdep_is_held(&bucket_lock)) {
    /* Writer 경로에서 잠금 보유 중 순회 */
}

/* hlist 헬퍼 매크로 */
first = hlist_entry_safe(
    rcu_dereference_raw(hlist_first_rcu(&head)),
    struct my_entry, node);

/* hlist_next_rcu: 다음 노드 접근 */
next_node = hlist_next_rcu(node);  /* rcu_dereference 내장 */

hlist_nulls 연산

hlist_nulls는 리스트 끝을 NULL 대신 NULL 마커 값으로 표시하는 변형입니다. 해시 테이블에서 노드가 순회 도중 다른 버킷으로 이동했을 때 이를 감지할 수 있습니다.

/* hlist_nulls: 버킷 이동 감지가 필요한 해시 테이블에 사용
 *
 * 일반 hlist: 리스트 끝 = NULL
 * hlist_nulls: 리스트 끝 = (bucket_id << 1) | 1
 *
 * 순회 완료 시 끝 마커의 bucket_id를 확인하여
 * 순회 중 노드가 다른 버킷으로 이동했는지 감지 가능
 */

/* hlist_nulls 순회 패턴 (네트워크 소켓 lookup 등) */
unsigned int slot = hash(key) % HASH_SIZE;
struct hlist_nulls_node *node;
struct my_entry *entry;

begin:
rcu_read_lock();
hlist_nulls_for_each_entry_rcu(entry, node,
        &hash_table[slot], node) {
    if (entry->key == target_key) {
        if (!refcount_inc_not_zero(&entry->refcnt))
            goto begin;
        rcu_read_unlock();
        return entry;
    }
}
rcu_read_unlock();

/* 끝 마커 확인: 순회 중 버킷 이동이 발생했는지 감지 */
if (get_nulls_value(node) != slot)
    goto begin;  /* 버킷 이동 감지 → 재시도 */

/* hlist_nulls 변경 API */
hlist_nulls_add_head_rcu(&entry->node, &hash_table[slot]);
hlist_nulls_del_rcu(&entry->node);

hlist_bl (Bit-Locked hlist) 연산

hlist_bl은 hlist 헤드 포인터의 최하위 비트를 스핀락으로 사용하는 공간 최적화 구조입니다. 별도의 spinlock 없이 리스트 헤드 자체에 잠금을 내장하여 해시 테이블의 메모리 사용을 절반으로 줄입니다.

/* hlist_bl: 디렉토리 엔트리 캐시(dcache) 등에서 사용
 * 헤드 포인터의 bit 0 = lock bit
 * 나머지 비트 = 실제 포인터
 */
#include <linux/list_bl.h>

/* 잠금 + 순회 */
hlist_bl_lock(&head);
hlist_bl_for_each_entry(entry, node, &head, d_hash) {
    if (dentry_match(entry, name))
        found = entry;
}
hlist_bl_unlock(&head);

/* RCU 순회 (잠금 없이) */
rcu_read_lock();
hlist_bl_for_each_entry_rcu(entry, node, &head, d_hash) {
    if (dentry_match(entry, name)) {
        dget(entry);  /* 참조 카운트 증가 */
        rcu_read_unlock();
        return entry;
    }
}
rcu_read_unlock();

/* hlist_bl 변경 (bit lock 보유 상태에서) */
hlist_bl_lock(&head);
hlist_bl_add_head_rcu(&new->d_hash, &head);
hlist_bl_del_rcu(&old->d_hash);
hlist_bl_unlock(&head);
ℹ️

hlist_bl 사용처: Linux dcache(디렉토리 엔트리 캐시)가 대표적입니다. 수백만 개의 해시 버킷마다 별도의 spinlock을 두면 메모리 낭비가 심하므로, 포인터 최하위 비트를 활용하여 lock을 내장합니다.

SRCU (Sleepable RCU)

표준 RCU의 read-side critical section에서는 슬립할 수 없습니다. 슬립이 필요한 경우 SRCU를 사용합니다:

DEFINE_SRCU(my_srcu);

int idx = srcu_read_lock(&my_srcu);
/* can sleep here */
srcu_read_unlock(&my_srcu, idx);

/* Writer */
synchronize_srcu(&my_srcu);

SRCU 심화 — 비용과 제한사항

SRCU는 srcu_struct 인스턴스를 독립 도메인으로 관리하므로 Grace Period와 메모리 비용 면에서 일반 RCU와 다른 특성을 가집니다.

항목일반 RCUSRCU
Grace Period 지연수십 ms (자연 QS 대기)수 ms ~ 수 초 (슬립 가능 reader 대기)
메모리 비용전역 per-CPU rcu_datasrcu_struct 당 per-CPU 카운터 배열
read-side 비용거의 무비용per-CPU 카운터 증감 (캐시 지역성 활용)
도메인 분리불가 (전역 GP)가능 (srcu_struct 별 독립 GP)
/* SRCU 핵심 규칙: srcu_read_lock() 반환값을 srcu_read_unlock()에 전달 */

/* ✓ 올바른 사용 */
int idx = srcu_read_lock(&my_srcu);     /* idx = 현재 GP 카운터 인덱스 (0 또는 1) */
/* ... sleep 가능 작업 ... */
srcu_read_unlock(&my_srcu, idx);         /* idx로 올바른 카운터 감소 */

/* ✗ 잘못된 패턴: idx 분실 → GP 추적 불가 */
srcu_read_lock(&my_srcu);               /* BUG: 반환값 무시 */
srcu_read_unlock(&my_srcu, 0);          /* BUG: 하드코딩된 인덱스 → 잘못된 카운터 감소 */

/* call_srcu vs synchronize_srcu 선택 */
synchronize_srcu(&my_srcu);             /* 블로킹: 현재 컨텍스트에서 GP 완료 대기 */
call_srcu(&my_srcu, &p->rcu, my_cb);   /* 비동기: GP 후 콜백 실행 (process 컨텍스트) */

/* 모듈 언로드 시 */
srcu_barrier(&my_srcu);                 /* call_srcu 콜백 완료 대기 */
⚠️

SRCU 인스턴스 공유 금지: 서로 무관한 서브시스템이 하나의 srcu_struct를 공유하면 Grace Period가 불필요하게 길어집니다. 용도별로 별도의 DEFINE_SRCU() 또는 DEFINE_STATIC_SRCU() 인스턴스를 사용하십시오.

init/cleanup_srcu_struct

동적으로 srcu_struct를 할당/해제할 때 사용합니다. 정적 선언(DEFINE_SRCU)이 불가능한 경우에 필요합니다.

/* 동적 SRCU 초기화/해제 */
struct srcu_struct my_srcu;

/* 초기화 (모듈 init 등에서) */
ret = init_srcu_struct(&my_srcu);
if (ret)
    return ret;  /* 메모리 부족 시 실패 가능 */

/* 사용 */
idx = srcu_read_lock(&my_srcu);
/* ... */
srcu_read_unlock(&my_srcu, idx);

/* 해제 (모듈 exit 등에서) */
cleanup_srcu_struct(&my_srcu);
/* 주의: 모든 SRCU reader가 종료되고 콜백이 완료된 후에만 호출 */

/* cleanup_srcu_struct_quiesced(): GP 완료 후에만 호출 가능
 * cleanup_srcu_struct()보다 빠르지만 GP 미완료 시 BUG() */
synchronize_srcu(&my_srcu);
cleanup_srcu_struct_quiesced(&my_srcu);

/* 정적 선언 변형들 */
DEFINE_SRCU(global_srcu);           /* 전역 SRCU (exported) */
DEFINE_STATIC_SRCU(local_srcu);     /* static SRCU (파일 스코프) */

SRCU Fast 변형

DEFINE_SRCU_FASTsrcu_read_lock_fast()는 NMI-safe하지 않지만 더 빠른 SRCU 변형입니다. per-CPU 카운터 대신 task-local 카운터를 사용하여 read-side 오버헤드를 줄입니다.

SRCU 2상 카운터 플립 메커니즘 Phase idx=0 (현재 active) Phase idx=1 (대기 중) CPU0: lock[0]= 3 unlock[0]= 2 CPU1: lock[0]= 5 unlock[0]= 5 CPU2: lock[0]= 1 unlock[0]= 1 CPU0: lock[1]= 0 unlock[1]= 0 CPU1: lock[1]= 0 unlock[1]= 0 CPU2: lock[1]= 0 unlock[1]= 0 GP 감지: sum(lock[0]) == sum(unlock[0])? 3+5+1=9 vs 2+5+1=8 → 불일치 → reader 1명 잔존 CPU0의 lock[0]=3, unlock[0]=2 → reader가 1개 더 진입 중 idx 플립 GP 시작 시 idx를 0→1로 플립 새 reader는 lock[1] 증가 기존 reader가 unlock[0]하면 GP 완료 srcu_read_lock() 반환값 idx로 올바른 카운터를 감소시키는 것이 핵심
SRCU 2상 카운터: GP 시작 시 인덱스를 플립하여 새/기존 reader를 분리합니다. 모든 CPU의 lock[old_idx] 합 == unlock[old_idx] 합이 되면 GP 완료.
/* SRCU Fast 변형 (Linux 6.5+) */
DEFINE_SRCU_FAST(my_fast_srcu);

/* srcu_read_lock_fast: task-local 카운터 사용 (더 빠름) */
int idx = srcu_read_lock_fast(&my_fast_srcu);
/* ... sleep 가능 ... */
srcu_read_unlock_fast(&my_fast_srcu, idx);

/* 주의: SRCU Fast는 NMI-safe하지 않음
 * NMI 컨텍스트에서는 srcu_read_lock_nmisafe() 사용 */

srcu_read_lock_nmisafe

NMI(Non-Maskable Interrupt) 컨텍스트에서 안전한 SRCU read-side 진입점입니다. 원자적 카운터 연산을 사용하여 NMI 재진입에도 안전합니다.

/* NMI-safe SRCU: 원자적 카운터 사용 */
int idx = srcu_read_lock_nmisafe(&my_srcu);
/* NMI 핸들러 내에서도 안전 */
srcu_read_unlock_nmisafe(&my_srcu, idx);

/* 일반 srcu_read_lock()은 this_cpu_inc()를 사용하여
 * NMI에서 재진입 시 카운터가 손상될 수 있음.
 * nmisafe 변형은 atomic_inc()를 사용하여 이를 방지 */

synchronize_srcu_expedited

/* synchronize_srcu_expedited: SRCU GP를 빠르게 완료 */
synchronize_srcu_expedited(&my_srcu);

/* 일반 synchronize_srcu()보다 빠르지만 CPU 비용 높음
 * 모든 CPU에 IPI를 전송하여 SRCU reader 존재 여부를 즉시 확인 */

Polled SRCU Grace Period

/* Polled SRCU GP: 일반 RCU polled GP와 동일한 패턴 */
unsigned long cookie;

cookie = start_poll_synchronize_srcu(&my_srcu);
/* 다른 작업 수행 */
if (poll_state_synchronize_srcu(&my_srcu, cookie)) {
    /* GP 완료 */
    kfree(old);
}

/* 관련 API */
cookie = get_state_synchronize_srcu(&my_srcu);  /* GP 시작 안 함 */
cond_synchronize_srcu(&my_srcu, cookie);        /* 미완료시만 블로킹 */

list/hlist_for_each_entry_srcu

/* SRCU read-side에서 리스트 순회 */
int idx = srcu_read_lock(&my_srcu);
list_for_each_entry_srcu(entry, &my_list, list,
        srcu_read_lock_held(&my_srcu)) {
    /* sleep 가능 처리 */
    process(entry);
}
srcu_read_unlock(&my_srcu, idx);

/* hlist SRCU 순회 */
idx = srcu_read_lock(&my_srcu);
hlist_for_each_entry_srcu(entry, &head, node,
        srcu_read_lock_held(&my_srcu)) {
    process(entry);
}
srcu_read_unlock(&my_srcu, idx);

smp_mb__after_srcu_read_unlock

/* smp_mb__after_srcu_read_unlock(): SRCU unlock 후 배리어
 *
 * srcu_read_unlock() 자체는 메모리 배리어를 포함하지 않을 수 있음.
 * unlock 후 메모리 순서가 필요하면 이 배리어를 명시적으로 삽입.
 */
srcu_read_unlock(&my_srcu, idx);
smp_mb__after_srcu_read_unlock();
/* 이후의 메모리 접근이 unlock 전 접근 이후로 보장됨 */

guard(srcu) / scoped_guard(srcu)

/* guard(srcu): scope 기반 자동 SRCU unlock (Linux 6.5+) */
{
    guard(srcu)(&my_srcu);
    p = srcu_dereference(gbl_ptr, &my_srcu);
    if (!p)
        return -ENOENT;  /* 자동 srcu_read_unlock() */
    result = p->field;
}   /* ← srcu_read_unlock() 자동 */

/* scoped_guard(srcu): 블록 범위 SRCU 보호 */
scoped_guard(srcu, &my_srcu) {
    p = srcu_dereference(gbl_ptr, &my_srcu);
    result = process(p);
}

RCU 변형 비교

변형Reader 슬립용도
RCU (Classic)불가일반적인 커널 자료구조
SRCU가능슬립이 필요한 reader
RCU-bh (역사적 구분)불가과거 softirq 중심 사용. 최신 커널에서는 통합된 RCU 문맥으로 이해
RCU-sched (역사적 구분)불가과거 선점 비활성 문맥 중심 구분. 최신 커널에서는 개념적으로 통합
Tasks RCU가능trampoline, BPF
💡

RCU는 커널에서 가장 널리 사용되는 동기화 메커니즘 중 하나입니다. 네트워크 라우팅 테이블, 파일시스템 dentry 캐시, 모듈 리스트 등 읽기 중심 자료구조에 광범위하게 사용됩니다.

동기화 메커니즘 성능 특성 비교

아래 차트는 Spinlock, RWLock, Seqlock, Mutex, RCU의 성능 특성을 정성적으로 비교합니다. 막대가 길수록 해당 항목의 부담이 큰 것입니다.

동기화 메커니즘 성능 특성 비교 (정성적) Reader 부하 Writer 부하 메모리 비용 구현 복잡도 Reader 부하 Writer 부하 메모리 비용 구현 복잡도 Spinlock 높음 높음 최소 낮음 RWLock 중간 높음 낮음 낮음 Seqlock 낮음 중간 최소 중간 Mutex 높음 높음 낮음 낮음 RCU 최소! ★ 중간 높음(per-CPU) 높음 • RCU: Reader 부하 최소화 — 읽기 집중 자료구조에 최적 (라우팅 테이블, dentry 캐시 등) • Seqlock: 쓰기 빈도 낮고 데이터가 작을 때 유리 (jiffies, 시스템 시각 등) • Spinlock/Mutex: 읽기/쓰기 균형이거나 구현 단순성이 중요할 때 • RCU 메모리 비용: per-CPU rcu_data 구조체 + 콜백 큐 + rcu_node 트리 • RCU 복잡도: Writer의 Copy-Update-reclaim 패턴 + grace period 관리 필요
정성적 비교: 막대 길이는 해당 특성의 상대적 부담을 나타냅니다. RCU는 Reader 부하가 압도적으로 낮지만 메모리 비용과 구현 복잡도가 높습니다.

RCU 내부 구현

Tree RCU

대규모 SMP(Symmetric Multi-Processing, 대칭형 다중 처리) 시스템에서 grace period 감지를 확장 가능하게 만드는 트리 기반 구현입니다. CPU들을 rcu_node 트리로 계층화하여 quiescent state를 집계합니다.

/* kernel/rcu/tree.c - quiescent state 보고 */
/*
 * rcu_node 트리 구조 (64 CPU 예시):
 *
 *         [root rcu_node]          ← fanout = 16
 *        /       |       \
 *   [node0]  [node1]  [node2]  [node3]
 *    /  \     /  \     /  \     /  \
 *  CPUs CPUs CPUs CPUs CPUs CPUs CPUs CPUs
 */

/* 각 CPU가 quiescent state 통과 시 */
static void rcu_report_qs_rnp(unsigned long mask,
    struct rcu_node *rnp)
{
    rnp->qsmask &= ~mask;  /* 해당 CPU 비트 클리어 */
    if (rnp->qsmask == 0)
        /* 모든 자식 CPU가 QS 통과 → 상위 노드에 보고 */
        rcu_report_qs_rnp(rnp->grpmask, rnp->parent);
}
Tree RCU 계층 구조 — Grace Period QS 집계 흐름 Root rcu_node qsmask = 0b1111 → GP 완료 rcu_node[0] rcu_node[1] rcu_node[2] rcu_node[3] CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7 QS ↑ QS 집계 흐름 (하향식 알림 → 상향식 보고): ① Grace Period 시작: Root가 qsmask 설정 ② 각 CPU가 QS 통과 시 rcu_report_qs_rdp() 호출 ③ 리프 노드 qsmask 비트 클리어 → 0이면 상위 보고 ④ Root qsmask == 0 → Grace Period 완료 ⑤ 콜백 큐 실행 (softirq RCU_SOFTIRQ)
Tree RCU: CPU들을 rcu_node 트리로 계층화하여 QS(Quiescent State)를 상향식으로 집계합니다. 대규모 SMP 시스템에서 grace period 감지 확장성을 보장합니다.

Quiescent State

Quiescent State(QS)는 "해당 CPU가 현재 어떤 RCU read-side critical section에도 있지 않음"을 나타내는 상태입니다. RCU는 모든 CPU가 최소 한 번 QS를 통과하면 grace period를 완료합니다. 이를 효율적으로 추적하기 위해 dynticks 카운터를 활용합니다 — 짝수 값은 CPU가 idle(확장 QS)임을, 홀수 값은 커널 코드 실행 중임을 나타냅니다.

컨텍스트Quiescent State 인식
유저모드 진입CPU가 유저 공간에 있으면 RCU read-side에 없음
idle 루프CPU가 idle이면 RCU critical section 밖
컨텍스트 스위치스케줄링 발생 = 이전 RCU 구간 종료
softirq 완료RCU-bh에서의 quiescent state

PREEMPT_RCU 동작 원리

커널 선점 설정(CONFIG_PREEMPT / CONFIG_PREEMPT_RT)에 따라 RCU의 Quiescent State 판별 방식과 rcu_read_lock() 비용이 달라집니다.

non-PREEMPT vs PREEMPT_RCU 동작 비교 non-PREEMPT RCU rcu_read_lock() 비용: 컴파일러 배리어(barrier())만 — 거의 무비용 (no-op에 가까움) Quiescent State 판별: • 컨텍스트 스위치 발생 • CPU가 idle 루프 진입 • 유저 모드 진입 → CPU 전환 자체가 QS 신호 (카운터 불필요) 선점된 Reader 처리: 선점 자체가 QS → reader 추적 불필요 Read-side 비용: 최소 PREEMPT_RCU rcu_read_lock() 비용: per-task rcu_read_lock_nesting 카운터 증감 (원자 연산) Quiescent State 판별: • rcu_read_unlock() 호출 시 카운터 감소 • 카운터 == 0이 되면 QS 보고 • 선점되어도 카운터가 유지됨 → 추적 필요 → rcu_node의 preempted reader 목록에 등록 선점된 Reader 처리: rcu_read_unlock_special()로 GP 완료 체크 후 해제 Read-side 비용: 소폭 높음
non-PREEMPT RCU는 CPU 전환 이벤트로 QS를 감지하여 read-side 비용이 거의 없습니다. PREEMPT_RCU는 선점된 reader를 추적하기 위해 카운터와 추적 목록을 사용합니다.
항목non-PREEMPTPREEMPT_RCU
rcu_read_lock() 비용barrier() 전용 (no-op에 가까움)nesting 카운터 증감 (원자 연산)
QS 판별 기준컨텍스트 스위치 / idle / 유저모드rcu_read_unlock() 시 카운터 == 0
선점된 reader선점 자체가 QS → 추적 불필요rcu_node preempted 목록에 등록
설정 확인grep CONFIG_PREEMPT /boot/config-$(uname -r)CONFIG_PREEMPT=y 또는 CONFIG_PREEMPT_RT=y
💡

실무 선택 기준: 데스크톱/RT 커널(CONFIG_PREEMPT_RT)은 PREEMPT_RCU를 사용합니다. 서버/임베디드에서 read-side 성능이 최우선이라면 CONFIG_PREEMPT=n (non-preemptible)을 검토하세요. 현재 커널 설정은 grep -E "CONFIG_PREEMPT" /boot/config-$(uname -r)로 확인합니다.

rcu_node / rcu_data 구조체 상세

Tree RCU의 핵심 자료구조인 struct rcu_data(per-CPU)와 struct rcu_node(계층 노드)의 주요 필드를 상세히 살펴봅니다.

struct rcu_data 주요 필드

/* kernel/rcu/tree.h — struct rcu_data (per-CPU, 간략화) */
struct rcu_data {
    /* GP 추적 */
    unsigned long       gp_seq;         /* 이 CPU가 인지한 최신 GP 번호 */
    unsigned long       gp_seq_needed;  /* 콜백 처리에 필요한 GP 번호 */
    bool                cpu_no_qs;      /* true = 아직 QS 미보고 */
    bool                core_needs_qs;  /* GP kthread가 QS 요청 중 */

    /* dynticks 추적 (EQS: Extended Quiescent State) */
    int                 dynticks_nesting; /* 짝수=idle(EQS), 홀수=커널 실행 */
    int                 dynticks_nmi_nesting; /* NMI 중첩 카운터 */

    /* 콜백 리스트 (4-tail segmented) */
    struct rcu_segcblist cblist;        /* 세그멘트화된 콜백 큐 */

    /* 소속 정보 */
    struct rcu_node     *mynode;        /* 이 CPU가 속한 리프 rcu_node */
    unsigned long       grpmask;        /* mynode에서의 비트 위치 */
    int                 cpu;            /* CPU 번호 */
};

struct rcu_node 주요 필드

/* kernel/rcu/tree.h — struct rcu_node (간략화) */
struct rcu_node {
    raw_spinlock_t      lock;           /* 노드 보호 락 */
    unsigned long       gp_seq;         /* 이 노드의 GP 시퀀스 */

    /* QS 추적 비트마스크 */
    unsigned long       qsmask;         /* 아직 QS 미보고 자식 비트맵 */
    /*   리프: 각 비트 = CPU, 내부 노드: 각 비트 = 자식 rcu_node */
    unsigned long       qsmaskinit;     /* GP 시작 시 qsmask 초기값 */
    unsigned long       grpmask;        /* 부모 노드에서의 비트 위치 */

    /* PREEMPT_RCU: 선점된 reader 추적 */
    struct list_head    blkd_tasks;     /* 선점된 RCU reader 태스크 목록 */
    unsigned long       expmask;        /* expedited GP 미보고 CPU */

    /* 트리 구조 */
    struct rcu_node     *parent;        /* 부모 노드 (root는 NULL) */
    int                 level;          /* 트리 깊이 (0 = root) */
    int                 grplo, grphi;   /* 관할 CPU 범위 */
};

Tree Fanout 계산

CONFIG 옵션기본값의미
CONFIG_RCU_FANOUT_LEAF16리프 rcu_node가 관할하는 최대 CPU 수
CONFIG_RCU_FANOUT64 (64bit) / 32 (32bit)내부 노드의 자식 수
예시: 256 CPU 시스템, FANOUT_LEAF=16, FANOUT=64
  Level 0 (Root):   1개 rcu_node   (최대 64 자식)
  Level 1 (Leaf):  16개 rcu_node   (각 16 CPU 관할)
  → 총 17개 rcu_node, 트리 깊이 2

예시: 4096 CPU 시스템, FANOUT_LEAF=16, FANOUT=64
  Level 0 (Root):    1개 rcu_node
  Level 1 (Inner):   4개 rcu_node  (각 64 자식)
  Level 2 (Leaf):  256개 rcu_node  (각 16 CPU 관할)
  → 총 261개 rcu_node, 트리 깊이 3

Per-CPU rcu_data 메모리 풋프린트

struct rcu_data는 CPU당 약 300~500바이트를 차지합니다 (커널 버전과 CONFIG에 따라 변동). 콜백 큐(cblist)의 엔트리는 별도 할당이므로, 콜백이 폭증하면 총 메모리 사용량이 크게 증가합니다.

ℹ️

PREEMPT_RCU의 blkd_tasks: 선점된 RCU reader는 rcu_node->blkd_tasks 리스트에 등록됩니다. GP가 완료되려면 이 리스트가 비어야 합니다. 선점된 reader가 장시간 스케줄링되지 않으면 GP가 지연되어 stall로 이어질 수 있습니다. CONFIG_RCU_BOOST를 활성화하면 이런 reader의 우선순위를 높여 GP 지연을 방지합니다.

rcu_data / rcu_node 구조체 관계도 struct rcu_node (리프) gp_seq: 현재 GP 시퀀스 qsmask: 0b1010 (CPU 1,3 미보고) qsmaskinit: 0b1111 (GP 시작값) blkd_tasks: 선점된 reader 목록 lock: raw_spinlock (노드 보호) grplo=0, grphi=15 (CPU 0~15) → parent rcu_node rcu_data (CPU 0) gp_seq: 현재 GP 추적 cpu_no_qs: false (QS 완료) dynticks: 짝수 (idle) cblist: [3 callbacks] mynode: → rcu_node grpmask: 0b0001 rcu_data (CPU 1) gp_seq: 이전 GP cpu_no_qs: true (미보고) dynticks: 홀수 (커널 실행) cblist: [12 callbacks] mynode: → rcu_node grpmask: 0b0010 rcu_data (CPU 2) gp_seq: 현재 GP cpu_no_qs: false dynticks: 짝수 cblist: [0 callbacks] mynode: → rcu_node grpmask: 0b0100 rcu_data (CPU 3) gp_seq: 이전 GP cpu_no_qs: true dynticks: 홀수 cblist: [7 callbacks] mynode: → rcu_node grpmask: 0b1000 qsmask = 0b1010 0 = QS 보고 완료 1 = QS 미보고 (GP 블로킹)
rcu_node의 qsmask 비트마스크와 per-CPU rcu_data의 관계. QS 미보고 CPU(비트 1)가 모두 클리어되면 해당 노드의 GP가 완료됩니다.

RCU & CPU 핫플러그 상호작용

CPU가 online/offline 상태로 전환될 때 RCU는 qsmask 조정, 콜백 마이그레이션, rcu_data 초기화 등의 작업을 수행해야 합니다. 이 과정이 잘못되면 GP 무한 대기, 콜백 소실, 데드락이 발생합니다.

CPU Offline 경로

/* CPU offline 시 RCU 처리 흐름 (간략화) */

/* ① dying 단계: CPU가 아직 실행 중이지만 offline 진행 */
rcutree_dying_cpu(cpu)
{
    rdp = per_cpu_ptr(&rcu_data, cpu);
    /* 이 CPU를 QS 통과로 강제 보고 */
    rcu_report_qs_rdp(rdp);
    /* dynticks를 offline 상태로 설정 */
    rdp->cpu_started = false;
}

/* ② dead 단계: CPU가 완전히 비활성화된 후 */
rcutree_dead_cpu(cpu)
{
    rdp = per_cpu_ptr(&rcu_data, cpu);
    rnp = rdp->mynode;

    /* qsmask에서 이 CPU 비트 제거 */
    rnp->qsmaskinit &= ~rdp->grpmask;

    /* 콜백 마이그레이션: 이 CPU의 콜백 → 다른 CPU로 이동 */
    rcu_migrate_callbacks(cpu);
}

CPU Online 경로

/* CPU online 시 RCU 처리 흐름 */
rcu_cpu_starting(cpu)
{
    rdp = per_cpu_ptr(&rcu_data, cpu);
    rnp = rdp->mynode;

    /* qsmaskinit에 이 CPU 비트 추가 */
    rnp->qsmaskinit |= rdp->grpmask;

    /* rcu_data 초기화: GP 시퀀스, 콜백 큐 등 */
    rdp->gp_seq = rnp->gp_seq;
    rdp->cpu_no_qs = true;
    rdp->cpu_started = true;

    /* 현재 진행 중인 GP가 있으면 qsmask에도 추가 */
    if (rcu_gp_in_progress())
        rnp->qsmask |= rdp->grpmask;
}

콜백 마이그레이션 상세

상황처리주의사항
CPU offline, 콜백 잔존콜백을 surviving CPU로 이동 (rcu_migrate_callbacks())이동 대상 CPU의 콜백 큐 급증 가능
NOCB 모드 CPU offlinercuo kthread가 콜백 계속 처리rcuo kthread가 중단되면 콜백 소실
선점된 reader의 CPU offlineblkd_tasks에 남아 있음, 다른 CPU에서 실행 후 QS 보고reader가 재스케줄되지 않으면 GP stall
⚠️

핫플러그 데드락 시나리오: RCU read-side critical section 내에서 CPU hotplug lock(cpus_read_lock())을 잡으면 데드락이 발생할 수 있습니다. CPU offline 경로가 synchronize_rcu()를 호출하고, 이 GP가 hotplug lock을 잡은 채로 sleep 중인 reader를 기다리게 되기 때문입니다. 항상 cpus_read_lock()rcu_read_lock() 바깥에서 잡으세요.

CPU Offline 시 RCU 처리 흐름 time 초기 상태 qsmask = 0b1111 (CPU 0~3 모두 미보고) CPU 0 CPU 1 CPU 2 ✕ CPU 3 rcutree_dying_cpu(2) CPU 2 강제 QS 보고 → qsmask &= ~0b0100 qsmask = 0b1011 rcutree_dead_cpu(2) qsmaskinit &= ~0b0100 (이후 GP에서 제외) rcu_migrate_callbacks(2) CPU 2의 콜백 → surviving CPU로 이동 DONE/WAIT/NEXT_READY/NEXT 세그멘트 모두 이전 CPU 2 콜백 큐 [cb1, cb2, cb3] surviving CPU 콜백 [..., cb1, cb2, cb3] CPU 2 완전 제거: GP에서 제외, 콜백 보존, 데이터 손실 없음
CPU offline 시 rcutree_dying_cpu()가 QS를 강제 보고하고, rcutree_dead_cpu()가 qsmaskinit에서 제거한 뒤, 미처리 콜백을 다른 CPU로 마이그레이션합니다.

RCU 콜백 처리

/* 콜백 등록 방법들 */

/* 1. call_rcu: 비동기 콜백 등록 */
void my_free_callback(struct rcu_head *head)
{
    struct my_data *p = container_of(head, struct my_data, rcu);
    kfree(p);
}
call_rcu(&old->rcu, my_free_callback);

/* 2. kfree_rcu: 단순 kfree용 간편 API */
kfree_rcu(old, rcu);         /* rcu_head 필드명 지정 */
/* rcu_head 필드가 없으면 call_rcu() + container_of() 콜백을 사용 */

/* 3. synchronize_rcu: 동기적 대기 (블로킹) */
synchronize_rcu();  /* grace period 완료까지 슬립 */

/* 4. rcu_barrier: 모든 call_rcu 콜백 완료 대기 */
rcu_barrier();  /* 모듈 언로드 시 필수 — 수 ms ~ 수 초 소요 가능 */
ℹ️

콜백 실행 컨텍스트: call_rcu() 콜백은 softirq 컨텍스트에서 실행됩니다. 구체적으로 RCU_SOFTIRQ를 통해 콜백을 등록한 CPU 또는 rcuo 오프로드 kthread에서 실행됩니다. 따라서 콜백 내에서는 GFP_ATOMIC만 사용 가능하며, sleep 불가합니다.

rcu_barrier() 소요 시간: 최소 수 ms(빠른 경우)에서 grace period 지연이 누적되면 수 초까지 걸릴 수 있습니다. 시스템 부하가 높거나 콜백이 많은 상황에서는 예상보다 오래 걸릴 수 있으므로 핫 경로에서 호출하지 마십시오.

⚠️

Callback Flooding 주의: 짧은 시간에 call_rcu()를 대량으로 호출하면 RCU 콜백 큐가 폭증하여 rcu_barrier() 지연, 메모리 소비 급증, 시스템 응답성 저하로 이어집니다. 대량 삭제 시에는 가능하면 synchronize_rcu() 한 번 호출 후 일괄 kfree()하는 방식을 검토하십시오.

모듈 언로드 시 call_rcu()로 등록된 미처리 콜백이 있으면 rcu_barrier()를 호출해야 합니다. 콜백이 모듈 코드를 참조하는 경우 use-after-free가 발생할 수 있습니다.

콜백 세그멘테이션 (4-Tail Segmented List)

RCU 콜백 큐는 단순 연결 리스트가 아니라 4-tail 세그멘트 리스트(struct rcu_segcblist)로 관리됩니다. 각 세그멘트는 콜백의 GP 완료 상태를 나타내며, GP가 진행됨에 따라 콜백이 세그멘트 간 이동합니다.

세그멘트상수의미상태
DONERCU_DONE_TAILGP 완료, 실행 대기즉시 실행 가능
WAITRCU_WAIT_TAIL현재 GP 완료 대기현재 GP가 끝나면 DONE으로 이동
NEXT_READYRCU_NEXT_READY_TAIL다음 GP에 참여 예정다음 GP 시작 시 WAIT로 이동
NEXTRCU_NEXT_TAIL방금 등록됨GP 배정 대기, call_rcu() 시 여기에 추가
/* kernel/rcu/rcu_segcblist.h — 4-tail 구조 (간략화) */
struct rcu_segcblist {
    struct rcu_head   *head;          /* 전체 리스트의 시작 */
    struct rcu_head  **tails[RCU_CBLIST_NSEGS]; /* 4개 tail 포인터 */
    /*  tails[DONE]       → DONE 세그멘트의 끝
     *  tails[WAIT]       → WAIT 세그멘트의 끝
     *  tails[NEXT_READY] → NEXT_READY 세그멘트의 끝
     *  tails[NEXT]       → NEXT 세그멘트의 끝 (== 전체 리스트 끝) */
    long              len;            /* 전체 콜백 수 */
    long              seglen[RCU_CBLIST_NSEGS]; /* 세그멘트별 콜백 수 */
};

단순 리스트 대비 4-tail의 이점

Lazy Callback (CONFIG_RCU_LAZY)

CONFIG_RCU_LAZY (v6.2+)를 활성화하면, call_rcu()로 등록된 콜백이 즉시 GP를 트리거하지 않고 배칭됩니다. 콜백이 일정량 쌓이거나 타이머가 만료되면 한꺼번에 GP를 시작합니다.

항목일반 모드RCU_LAZY 모드
call_rcu() 후 GP 시작즉시 (콜백 등록 → wakeup)지연 (배칭 후 일괄 시작)
전력 소비잦은 wakeup으로 전력 소모wakeup 빈도 감소 → 절전 효과
메모리 해제 지연GP 완료 즉시배칭 지연 + GP 완료
적합 환경서버, 고성능모바일, 임베디드, 저전력
강제 실행call_rcu_hurry()로 lazy → 즉시 승격
4-Tail Segmented Callback List GP N 진행 중 (콜백 큐 상태) DONE GP N-1 완료 → 실행 가능 cb1 cb2 WAIT GP N 완료 대기 중 cb3 cb4 cb5 NEXT_READY GP N+1에 참여 예정 cb6 NEXT 방금 등록 (call_rcu) cb7 cb8 tails[DONE] tails[WAIT] tails[NEXT_READY] tails[NEXT] GP N 완료 → 세그멘트 이동 (tail 포인터 재배치만, O(1)) GP N 완료 후 (콜백 큐 상태) DONE (cb1~cb5 실행 가능) 이전 DONE + WAIT 통합 WAIT (cb6) 이전 NEXT_READY → WAIT 승격 NEXT_READY + NEXT 이전 NEXT → NEXT_READY 승격 RCU_SOFTIRQ → cb1(), cb2(), cb3(), cb4(), cb5() 실행
GP 완료 시 WAIT 세그멘트가 DONE으로, NEXT_READY가 WAIT로 승격됩니다. tail 포인터만 재배치하므로 O(1)에 완료됩니다.

RCU 보호 해시 테이블

#include <linux/rhashtable.h>

/* rhashtable: 자동 리사이징되는 RCU 해시 테이블 */
struct my_entry {
    int                 key;
    char                value[64];
    struct rhash_head   node;
    struct rcu_head     rcu;
};

static const struct rhashtable_params my_params = {
    .key_len     = sizeof(int),
    .key_offset  = offsetof(struct my_entry, key),
    .head_offset = offsetof(struct my_entry, node),
};

/* 초기화 */
struct rhashtable ht;
rhashtable_init(&ht, &my_params);

/* 삽입 (writer, 잠금 필요) */
rhashtable_insert_fast(&ht, &entry->node, my_params);

/* 조회 (reader, RCU 보호) */
rcu_read_lock();
struct my_entry *e = rhashtable_lookup_fast(&ht, &key, my_params);
rcu_read_unlock();

/* 삭제 */
rhashtable_remove_fast(&ht, &entry->node, my_params);
kfree_rcu(entry, rcu);

SLAB_TYPESAFE_BY_RCU

SLAB_TYPESAFE_BY_RCU는 slab 캐시 플래그로, 해제된 슬랩 객체가 grace period 내에서 동일 타입으로 재사용될 수 있음을 보장합니다. kfree_rcu()와는 근본적으로 다른 메커니즘입니다.

SLAB_TYPESAFE_BY_RCU vs kfree_rcu 비교 kfree_rcu(ptr, rcu) Writer: kfree_rcu(obj, rcu) obj 해제 요청 Grace Period 대기 GP 완료 후 kfree() 호출 → 메모리 완전 해제 (slab으로 반환) → 동일 주소 재할당 시 다른 타입 가능 장점: • reader 재검증 불필요 • 구현 단순 단점: • GP 지연 동안 메모리 점유 • rcu_head (16바이트) 필드 필요 SLAB_TYPESAFE_BY_RCU Writer: kfree(obj) 즉시 호출 obj 해제 즉시 slab 반환 재사용 가능! GP 없이 즉시 해제, slab 내에서 재활용 → 동일 타입 객체로만 재사용 보장 → reader가 "stale" 객체를 볼 수 있음! 장점: • GP 지연 없이 즉시 메모리 재활용 • rcu_head 필드 불필요 (메모리 절약) 단점: • reader가 재사용된 객체를 읽을 수 있음 • double-check 패턴 필수 (재검증)
kfree_rcu는 GP 후 안전 해제, SLAB_TYPESAFE_BY_RCU는 즉시 해제 후 동일 타입 재사용을 보장합니다.

kfree_rcu와의 차이

항목kfree_rcu()SLAB_TYPESAFE_BY_RCU
해제 시점GP 완료 후즉시 (kfree)
메모리 재사용GP 후 임의 타입즉시, 동일 slab 캐시 타입만
reader 안전성GP 내 접근 완전 안전재사용된 객체 접근 가능 → 재검증 필수
rcu_head 필요O (구조체에 포함)X (불필요)
GP 지연 영향메모리 점유 누적없음 (즉시 반환)
구현 난이도단순높음 (double-check 패턴)
/* SLAB_TYPESAFE_BY_RCU slab 캐시 생성 */
struct kmem_cache *my_cache;
my_cache = kmem_cache_create("my_objects",
    sizeof(struct my_obj), 0,
    SLAB_TYPESAFE_BY_RCU,  /* ← 핵심 플래그 */
    NULL);

/* Writer: 즉시 해제 (GP 대기 불필요) */
spin_lock(&lock);
hlist_del_rcu(&obj->node);
spin_unlock(&lock);
kmem_cache_free(my_cache, obj);  /* 즉시 반환! kfree_rcu 불필요 */

/* Reader: double-check 패턴 필수! */
rcu_read_lock();
hlist_for_each_entry_rcu(obj, &bucket, node) {
    if (obj->key == target_key) {
        /* ①: 키 일치 확인 */
        spin_lock(&obj->lock);
        /* ②: 재검증 — 해제 후 재사용된 객체일 수 있음 */
        if (likely(obj->key == target_key &&
                    !obj->dead)) {
            /* 유효한 객체 확인됨 */
            refcount_inc(&obj->ref);
            spin_unlock(&obj->lock);
            rcu_read_unlock();
            return obj;
        }
        spin_unlock(&obj->lock);
    }
}
rcu_read_unlock();
⚠️

SLAB_TYPESAFE_BY_RCU 핵심 규칙: reader가 찾은 객체는 이미 해제되고 재할당된 다른 인스턴스일 수 있습니다. 따라서 반드시 잠금을 획득한 후 키/상태를 재검증(double-check)해야 합니다. 이 재검증 없이 사용하면 논리적 오류가 발생합니다.

사용 사례

사용처이유
dcache (dentry)수백만 개 dentry의 rcu_head 16바이트 절약, 빈번한 할당/해제 경로 최적화
inode cacheinode 수가 많고 해제 빈도 높음, GP 지연 없는 즉시 재활용
PID 할당 (struct pid)PID 구조체의 빈번한 할당/해제
네트워크 소켓대량의 소켓 연결/해제 시 GP 지연 축소
VMA (vm_area_struct)per-VMA lock과 결합한 메모리 관리 최적화

RCU 콜백 오프로딩 (NOCB)

RCU NOCB(No-Callback)는 RCU 콜백 처리를 softirq에서 전용 kthread로 오프로드하는 메커니즘입니다. 실시간(RT) 시스템이나 HPC 환경에서 특정 CPU의 지터(jitter)를 최소화하는 데 사용됩니다.

RCU NOCB 아키텍처 — 콜백 오프로드 기본 모드: softirq에서 콜백 처리 CPU 0 RCU_SOFTIRQ 콜백 실행 (같은 CPU) 문제: softirq가 RT 태스크를 지연시킴 (지터 발생) NOCB 모드: 전용 kthread로 콜백 오프로드 CPU 0 (RT 작업 전용) 콜백 전달 rcuo kthread 콜백 큐 처리 ← 다른 CPU에서 실행 가능 CPU 1 rcuog/rcuop 그룹 CPU 0은 콜백 처리에서 완전 해방 → RT 지터 최소화
NOCB: RCU 콜백을 전용 kthread(rcuo)로 오프로드하여 대상 CPU의 softirq 부담을 제거합니다.

NOCB 아키텍처

NOCB 모드에서는 call_rcu()로 등록된 콜백이 해당 CPU의 softirq가 아닌 전용 rcuo kthread에서 실행됩니다.

/* NOCB 동작 흐름:
 *
 * 1. CPU N에서 call_rcu() 호출
 * 2. 콜백이 per-CPU cblist에 등록
 * 3. rcuog(RCU offload GP) kthread가 GP 완료 감지
 * 4. rcuop(RCU offload process) kthread가 콜백 실행
 *    → CPU N의 softirq가 아닌 rcuop kthread에서 실행
 *
 * kthread 종류:
 *   rcuog/N  — GP 감시 kthread (그룹 리더)
 *   rcuop/N  — 콜백 처리 kthread
 */

rcuo kthreads

# NOCB kthread 확인
ps -eo pid,cls,pri,comm | grep rcu
# 출력 예:
#   12  TS  19  rcuog/0     ← GP 감시 (그룹 리더)
#   13  TS  19  rcuop/0     ← 콜백 처리 (CPU 0)
#   14  TS  19  rcuop/1     ← 콜백 처리 (CPU 1)
#   ...

# rcuop kthread의 우선순위를 RT로 변경 (지터 최소화)
chrt -f -p 2 $(pgrep -x rcuop/0)

# NOCB CPU 확인
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata | grep -i nocb

CONFIG_RCU_NOCB_CPU

설정설명
CONFIG_RCU_NOCB_CPU=yNOCB 지원 활성화 (커널 빌드)
rcu_nocbs=0-3,8부트 파라미터: 지정 CPU에서 콜백 오프로드
rcu_nocbs=all모든 CPU에서 콜백 오프로드
rcupdate.rcu_nocb_gp_stride=4rcuog 그룹당 CPU 수 (기본값: 자동)
# 커널 부트 파라미터 예
# RT CPU 0-3에서 RCU 콜백 오프로드
GRUB_CMDLINE_LINUX="rcu_nocbs=0-3 isolcpus=0-3 nohz_full=0-3"

# 런타임 NOCB 전환 (Linux 5.18+, CONFIG_RCU_NOCB_CPU_CB_BOOST=y)
echo 1 > /sys/kernel/debug/rcu/rcu_preempt/nocb/0  # CPU 0 NOCB 활성화
echo 0 > /sys/kernel/debug/rcu/rcu_preempt/nocb/0  # CPU 0 NOCB 비활성화

RT/HPC 사용 사례

💡

NOCB + isolcpus + nohz_full 조합: RT/HPC 환경에서 특정 CPU를 최대한 격리하려면 세 가지를 함께 사용합니다.

  • isolcpus=0-3: 스케줄러에서 CPU 격리
  • nohz_full=0-3: 타이머 틱 제거 (adaptive-ticks)
  • rcu_nocbs=0-3: RCU 콜백 오프로드

이 조합으로 CPU 0-3은 커널 간섭 없이 RT 태스크에 전념할 수 있습니다.

QS 보고 유틸리티 API

Quiescent State(QS) 유틸리티는 RCU에게 "이 CPU는 현재 RCU read-side에 없다"고 명시적으로 알리는 함수들입니다. 장시간 실행 루프에서 RCU stall을 방지하는 데 사용합니다.

함수동작사용 시점
cond_resched_rcu()RCU unlock → 스케줄링 양보 → RCU lockRCU read-side 내 장시간 루프
cond_resched_tasks_rcu_qs()RCU-Tasks에게 QS 보고커널 스레드 장시간 루프
rcu_all_qs()모든 RCU 유형에 QS 보고 (PREEMPT_RCU 전용)PREEMPT_RCU에서 busy-wait 루프
rcu_softirq_qs_periodic(old)주기적 softirq QS 보고softirq 핸들러 내 장시간 처리
rcu_note_context_switch()스케줄러가 컨텍스트 스위치 시 RCU에 통지스케줄러 내부 (직접 호출 불필요)
rcu_cpu_stall_reset()stall 감지 타이머 리셋의도적 장시간 작업 전
/* cond_resched_rcu(): 장시간 RCU 순회에서 stall 방지 */
rcu_read_lock();
list_for_each_entry_rcu(item, &very_long_list, node) {
    process(item);
    cond_resched_rcu();  /* 내부: rcu_read_unlock → cond_resched → rcu_read_lock */
}
rcu_read_unlock();

/* cond_resched_tasks_rcu_qs(): 커널 스레드에서 RCU-Tasks QS */
while (!kthread_should_stop()) {
    do_work();
    cond_resched_tasks_rcu_qs();  /* RCU-Tasks GP 진행 허용 */
}

/* rcu_all_qs(): PREEMPT_RCU에서 명시적 QS */
/* non-preempt 커널에서는 no-op */
for (i = 0; i < LARGE_COUNT; i++) {
    process_item(i);
    if (!(i % 1024))
        rcu_all_qs();  /* 1024 반복마다 QS 보고 */
}

/* rcu_cpu_stall_reset(): 의도적 장시간 작업 전 stall 타이머 리셋 */
rcu_cpu_stall_reset();
/* ... 수 초 걸리는 의도적 작업 ... */
⚠️

cond_resched_rcu() vs rcu_all_qs(): cond_resched_rcu()는 RCU를 해제하고 스케줄링 양보까지 수행하므로 더 무겁지만 확실합니다. rcu_all_qs()는 PREEMPT_RCU에서만 효과가 있으며, RCU read-side CS 밖에서만 호출해야 합니다.

RCU 디버깅

디버깅 관련 CONFIG 옵션

CONFIG 옵션기능
CONFIG_PROVE_RCURCU 잠금 규칙 위반 감지 (lockdep 통합)
CONFIG_RCU_TRACERCU 이벤트 tracepoint 활성화
CONFIG_RCU_CPU_STALL_TIMEOUTRCU CPU stall 감지 타임아웃 (기본 21초)
CONFIG_RCU_BOOSTRCU reader 우선순위 부스팅 (RT 커널)
CONFIG_RCU_CPU_STALL_CPUTIMEstall 시 CPU 시간 통계 함께 출력
CONFIG_RCU_EXP_KTHREADexpedited grace period 전용 kthread 사용
# 런타임에서 stall 타임아웃 조정 (초 단위)
echo 60 > /sys/module/rcupdate/parameters/rcu_cpu_stall_timeout

# stall 경고 억제 (디버깅 시 잠시 끄기)
echo 1 > /sys/module/rcupdate/parameters/rcu_cpu_stall_suppress

# /sys/kernel/debug/rcu/ 에서 RCU 상태 확인
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata

디버깅/검증 API 상세

함수/매크로동작사용 시점
RCU_LOCKDEP_WARN(cond, msg)cond이 참이면 lockdep 경고 출력RCU 규칙 위반 감지
rcu_sleep_check()RCU read-side에서 sleep 시도 감지might_sleep() 내부에서 호출
rcu_is_watching()현재 CPU가 RCU 감시 중인지 확인idle/offline 상태 확인
rcu_head_init(head)rcu_head를 디버그 모드로 초기화DEBUG_OBJECTS_RCU_HEAD 사용 시
rcu_head_after_call_rcu(head, func)rcu_head가 call_rcu() 후 상태인지 확인double-free 감지
rcu_check_sparse(p, space)sparse 타입 검증__rcu 어노테이션 검증
/* RCU_LOCKDEP_WARN 사용 예 */
RCU_LOCKDEP_WARN(!rcu_read_lock_held() &&
    !lockdep_is_held(&my_mutex),
    "must hold RCU read lock or my_mutex");

/* rcu_is_watching(): CPU가 RCU 감시 상태인지 확인
 * idle이나 offline CPU에서는 false 반환 */
if (rcu_is_watching()) {
    /* RCU 연산 안전 */
    p = rcu_dereference(gbl_ptr);
}

/* rcu_head_init: DEBUG_OBJECTS_RCU_HEAD 활성화 시 사용 */
#ifdef CONFIG_DEBUG_OBJECTS_RCU_HEAD
rcu_head_init(&obj->rcu);  /* 초기화 추적 시작 */
#endif

/* rcu_head_after_call_rcu: call_rcu() 후 중복 호출 감지 */
if (rcu_head_after_call_rcu(&obj->rcu, my_callback)) {
    /* 이미 call_rcu()가 호출된 rcu_head — double-free 방지 */
    WARN_ON(1);
}

ftrace/tracepoint 실습 예제

RCU 내부 동작을 실시간으로 추적하려면 ftrace tracepoint를 활용합니다. CONFIG_RCU_TRACE=y가 필요합니다.

Grace Period 추적

# trace-cmd로 RCU GP 이벤트 기록
trace-cmd record -e rcu:rcu_grace_period -e rcu:rcu_grace_period_init \
    -e rcu:rcu_quiescent_state_report sleep 5

# 결과 확인
trace-cmd report | head -30

# 출력 예시:
#  rcu_gp-18 [000] rcu_grace_period:   rcu_preempt 12345 start
#  <idle>-0  [003] rcu_quiescent_state: rcu_preempt 12345 cpu=3
#  rcu_gp-18 [000] rcu_grace_period:   rcu_preempt 12345 end

# GP 시작(start) → 각 CPU QS 보고 → GP 종료(end) 흐름 확인

콜백 실행 추적

# RCU 콜백 관련 이벤트
trace-cmd record -e rcu:rcu_callback -e rcu:rcu_batch_start \
    -e rcu:rcu_batch_end -e rcu:rcu_invoke_callback sleep 5

# 출력 예시:
#  ksoftirqd/2 [002] rcu_batch_start:   rcu_preempt CBs=15 bl=10
#  ksoftirqd/2 [002] rcu_invoke_callback: rcu_preempt rhp=ffff... func=kfree_rcu_work
#  ksoftirqd/2 [002] rcu_batch_end:     rcu_preempt CBs-invoked=10 idle=....

# CBs: 대기 콜백 수, bl: blimit, CBs-invoked: 실행된 콜백 수

RCU utilization 이벤트

# rcu_utilization: RCU 내부 함수 진입/탈출 추적
trace-cmd record -e rcu:rcu_utilization sleep 2
trace-cmd report | grep -c "Start"   # GP kthread 활성 횟수
trace-cmd report | grep -c "End"     # GP kthread 완료 횟수

# 비율이 불균형하면 GP kthread가 과부하 상태

perf 기반 GP 지연 분석

# perf로 RCU 이벤트 통계 수집
perf stat -e 'rcu:rcu_grace_period' -e 'rcu:rcu_batch_start' \
    -e 'rcu:rcu_invoke_callback' -a sleep 10

# 출력 예시:
#   1,234 rcu:rcu_grace_period       (GP 횟수)
#   5,678 rcu:rcu_batch_start        (콜백 배치 횟수)
#  12,345 rcu:rcu_invoke_callback    (개별 콜백 실행 횟수)

# GP당 평균 콜백 수 = invoke / grace_period ≈ 10
# 이 값이 blimit에 가까우면 콜백 배치가 제대로 동작하는 것

Stall 원인 축소: /proc/sched_debug 활용

# RCU stall 발생 시 런큐 상태 확인
cat /proc/sched_debug | grep -A 10 "cpu#3"

# 확인 포인트:
# - .nr_running: 실행 대기 태스크 수 (매우 높으면 CPU 포화)
# - .curr: 현재 실행 중인 태스크 (RCU reader가 점유 중인지)
# - .clock: 마지막 스케줄러 틱 시간 (오래되었으면 틱 중단)

# softirq 지연 확인
cat /proc/softirqs | grep RCU
# RCU_SOFTIRQ 카운터가 특정 CPU에서 멈춰 있으면 해당 CPU에서
# softirq가 실행되지 못하는 상황 (IRQ disabled 장시간 등)
💡

실전 디버깅 순서: (1) /sys/kernel/debug/rcu/rcu_preempt/rcudata로 per-CPU 상태 확인 → (2) trace-cmd로 GP/QS 이벤트 기록 → (3) /proc/sched_debug로 stall CPU의 스케줄링 상태 확인 → (4) perf stat으로 전체 RCU 처리량 정량 평가.

RCU CPU Stall 경고 심화

⚠️

심화 디버깅 가이드: 이 섹션은 앞서 다룬 RCU 메커니즘(read-side critical section, grace period, 콜백 처리)의 이해를 전제로 합니다. RCU stall은 grace period가 완료되지 못하는 상황이므로, RCU의 기본 동작 원리를 먼저 숙지한 후 읽으시기 바랍니다.

RCU CPU stall 경고는 grace period가 비정상적으로 오래 지속될 때 커널이 출력하는 진단 메시지입니다. 이 메시지는 시스템 행(hang), 성능 저하, 데드락의 근본 원인을 추적하는 핵심 단서입니다.

Stall 감지 메커니즘

RCU는 grace period 시작 후 일정 시간(CONFIG_RCU_CPU_STALL_TIMEOUT, 기본 21초) 내에 모든 CPU가 quiescent state를 보고하지 않으면 stall로 판단합니다:

/* kernel/rcu/tree_stall.h — stall 감지 흐름 */
/*
 *  1. grace period 시작 (gp_seq 증가)
 *  2. 타이머 설정: jiffies + rcu_cpu_stall_timeout
 *  3. 타이머 만료 시점에 아직 미응답 CPU가 있으면:
 *     → rcu_check_gp_stall_expiry() 호출
 *     → print_cpu_stall() 또는 print_other_cpu_stall() 실행
 *  4. 첫 경고 후 추가 타임아웃마다 반복 경고 출력
 */

static void check_cpu_stall(struct rcu_data *rdp)
{
    unsigned long gs1, gs2, gps;  /* grace-period 시퀀스 */
    unsigned long j, js;          /* jiffies, stall 시점  */

    gs1 = READ_ONCE(rcu_state.gp_seq);
    js  = READ_ONCE(rcu_state.jiffies_stall);
    gps = READ_ONCE(rcu_state.gp_start);
    j   = jiffies;

    if (rcu_gp_in_progress() &&
        time_after(j, js)) {
        /* 자기 자신이 stall 중인지 확인 */
        if (rcu_is_cpu_rrupt_from_idle()) {
            /* idle → 정상, QS 보고 */
        } else if (ULONG_CMP_GE(j, js + RCU_STALL_RAT_DELAY)) {
            /* 자기 자신이 stall → print_cpu_stall() */
            print_cpu_stall(gps, gs1);
        } else {
            /* 다른 CPU가 stall → print_other_cpu_stall() */
            print_other_cpu_stall(gs2, gps);
        }
    }
}

Stall 유형

커널은 두 가지 유형의 stall 메시지를 출력합니다:

유형의미메시지 패턴
Self-detected stall현재 CPU 자신이 QS를 보고하지 못함self-detected stall on CPU
Other CPU stall다른 CPU(들)가 QS를 보고하지 못함detected stalls on CPUs/tasks

Stall 메시지 해부

실제 커널이 출력하는 RCU stall 메시지를 필드별로 분석합니다. 메시지의 각 부분이 무엇을 의미하는지 이해하면 근본 원인을 빠르게 좁힐 수 있습니다.

다른 CPU에서 감지된 stall

/* 실제 메시지 (줄 바꿈은 가독성을 위해 추가) */
rcu: INFO: rcu_sched detected stalls on CPUs/tasks:
         2-...!: (1 GPs behind) idle=fb2/1/0x4000000000000000
                 softirq=1254/1280 fqs=14979
         (detected by 0, t=21003 jiffies, g=300921, q=24)
rcu: rcu_sched kthread starved for 23001 jiffies!

이 메시지를 한 줄씩 해석합니다:

필드의미
rcu_sched-stall이 발생한 RCU flavor (rcu_preempt, rcu_sched 등)
2-...!CPU 2stall 중인 CPU 번호. !는 해당 CPU가 오프라인 또는 softirq/hardirq 컨텍스트에 있음을 표시
(1 GPs behind)1해당 CPU가 현재 grace period보다 몇 GP 뒤처져 있는지
idle=fb2/1/0x4...3개 값idle 상태 추적: dynticks nesting / dynticks nmi nesting / dynticks counter
softirq=1254/1280처리/발생softirq 카운터: 처리된 수 / 발생한 수. 차이가 크면 softirq 처리 지연
fqs=1497914979force-quiescent-state 스캔 횟수. 높을수록 오래 기다린 것
detected by 0CPU 0stall을 감지(보고)한 CPU 번호
t=2100321003grace period 시작 후 경과한 jiffies (≈21초)
g=300921300921stall 중인 grace period 번호 (gp_seq)
q=2424stall 감지 시점까지 QS를 보고한 CPU 수

Self-detected stall

/* self-detected stall 메시지 */
rcu: INFO: rcu_preempt self-detected stall on CPU
         3-....: (21005 ticks this GP) idle=4ce/1/0x4000000000000002
                 (t=21031 jiffies g=41052 q=55 ncpus=8)
rcu:     NMI backtrace for cpu 3
/* ... 스택 트레이스 출력 ... */
필드의미
3-....:CPU 3에서 self-detected. 접미사 .은 정상, !은 문제 있음
(21005 ticks this GP)이 grace period 동안 해당 CPU에서 경과한 틱 수
ncpus=8시스템의 온라인 CPU 수
NMI backtracestall 시점의 CPU 스택 트레이스 (NMI로 강제 덤프)

CPU 상태 플래그 상세

CPU 번호 뒤의 플래그 문자(2-...!)는 해당 CPU의 상태를 나타냅니다:

/* kernel/rcu/tree_stall.h — CPU 상태 플래그 */
/*
 * 포맷: CPU번호-FLAG1FLAG2FLAG3FLAG4FLAG5
 *
 *  위치  의미
 *  ----  -------------------------------------------
 *  1번째  'O' = 오프라인,  '.' = 온라인
 *  2번째  'o' = RCU가 해당 CPU를 오프라인으로 인식, '.' = 아님
 *  3번째  'N' = 틱 기반 QS 대기 중,  '.' = 아님
 *  4번째  'D' = dynticks 확장 QS,  '.' = 아님
 *  5번째  '!' = hardirq/softirq/NMI 컨텍스트, '.' = 아님
 */

/* 예시 해석 */
2-...!:   /* CPU 2, 온라인, RCU온라인, 틱QS없음, dyntick없음,
              irq 컨텍스트에서 멈춤 */
5-....:   /* CPU 5, 모두 정상 — 일반 컨텍스트에서 stall */
7-O...:   /* CPU 7, 오프라인 상태 — hotplug 관련 문제 */

idle 필드 상세 해석

/* idle=AAA/BBB/CCC 형식 */

idle=fb2/1/0x4000000000000000

/*
 * AAA (fb2) = dynticks nesting 카운터
 *   - 짝수: CPU가 extended QS(idle/usermode) 안에 있음
 *   - 홀수: CPU가 커널 코드 실행 중
 *   - 값 자체는 nesting 깊이 추적용
 *
 * BBB (1) = dynticks NMI nesting 카운터
 *   - 0: NMI/IRQ가 아님
 *   - 양수: NMI 또는 hardirq nesting 레벨
 *
 * CCC (0x4000000000000000) = dynticks 상태 카운터
 *   - 짝수: idle/offline (RCU가 무시 가능)
 *   - 홀수: 활성 상태 (QS 보고 필요)
 *   - 비트 62: 0x4... → 해당 CPU가 grace period를 인식했음을 표시
 */

태스크 기반 stall (PREEMPT_RCU)

CONFIG_PREEMPT_RCU 커널에서는 특정 태스크가 RCU read-side critical section 안에서 선점된 채 stall될 수 있습니다:

/* 태스크 stall 메시지 예시 */
rcu: INFO: rcu_preempt detected stalls on CPUs/tasks:
         P3271   /* ← CPU가 아닌 PID 3271 태스크가 stall! */
         (detected by 5, t=21007 jiffies, g=82150, q=30)
rcu: rcu_preempt kthread starved for 15000 jiffies!

/*
 * P3271 → PID 3271이 rcu_read_lock() 안에서 선점되어
 *          grace period 완료를 막고 있음
 *
 * 확인 방법:
 *   cat /proc/3271/stack    # 해당 태스크의 커널 스택
 *   cat /proc/3271/status   # 태스크 상태 및 스케줄링 정보
 */

kthread starved 메시지

/* kthread starvation 메시지 */
rcu: rcu_sched kthread starved for 23001 jiffies!
     last ran 23001 jiffies ago on CPU 4
     with state 0x2

/*
 * RCU grace-period kthread(rcuog/rcuop)가 스케줄링되지 못함
 *
 * state 값 해석 (task_state):
 *   0x0 = TASK_RUNNING       (실행 가능하지만 CPU 시간을 못 받음)
 *   0x1 = TASK_INTERRUPTIBLE
 *   0x2 = TASK_UNINTERRUPTIBLE (D 상태 — I/O 대기 등)
 *
 * state=0x0(RUNNING)인데 실행 못 함:
 *   → 높은 우선순위 태스크에 밀림 (RT 우선순위 문제)
 *   → CPU가 인터럽트 폭주 중
 *
 * state=0x2(UNINTERRUPTIBLE):
 *   → kthread가 I/O나 락 대기 중
 *   → 메모리 부족으로 할당 대기 가능
 */

주요 Stall 원인과 진단

원인증상진단 방법
인터럽트 비활성화 상태에서 긴 루프단일 CPU stall, idle 값 홀수NMI backtrace에서 local_irq_disable() 추적
preemption 비활성화 + 긴 연산self-detected, 틱 카운트 높음backtrace에서 preempt_disable() 호출 위치 확인
softirq 폭주softirq 처리/발생 차이 큼/proc/softirqs 비교, NET_RX 등 확인
RCU read-side critical section 너무 김태스크 stall (Pnnnn)/proc/PID/stack으로 rcu_read_lock() 위치 확인
RCU kthread 스케줄링 불가kthread starved 메시지chrt -p $(pidof rcuog/0)으로 우선순위 확인
실시간(RT) 태스크가 CPU 독점여러 CPU stall, kthread starvedps -eo pid,cls,rtprio,comm | grep -E "FF|RR"
하드웨어 문제 (NMI 폭주, 메모리 오류)불규칙한 stall, MCE 동반dmesg | grep -i mce, /proc/interrupts의 NMI 카운트
가상머신 vCPU steal time여러 CPU 동시 stall/proc/stat의 steal 값, 호스트 과부하 확인

Stall 디버깅 순서

/* RCU stall 발생 시 단계별 디버깅 절차 */

/* 1단계: 메시지 유형 파악 */
# self-detected → 해당 CPU에서 원인 찾기
# detected on CPUs/tasks → 나열된 CPU/태스크 조사
# kthread starved → 스케줄링 문제 우선 의심

/* 2단계: NMI backtrace 분석 */
# stall 메시지 직후의 스택 트레이스를 확인
# → 어떤 함수에서 멈춰 있는지가 핵심 단서

/* 3단계: idle 필드로 CPU 상태 판단 */
# idle=짝수/... → CPU가 idle인데 QS 미보고 (RCU 버그?)
# idle=홀수/... → 커널 실행 중 (코드 경로 추적 필요)

/* 4단계: softirq 카운터 확인 */
$ cat /proc/softirqs         # 각 CPU별 softirq 카운트
$ watch -n1 cat /proc/softirqs  # 실시간 변화 관찰
# 특정 CPU의 특정 softirq(NET_RX 등)가 급증하면 폭주 의심

/* 5단계: CPU 스케줄링 상태 확인 */
$ cat /proc/sched_debug      # 각 CPU 런큐 상태
$ cat /proc/PID/sched        # 특정 태스크 스케줄링 통계

/* 6단계: RCU 내부 상태 확인 */
$ cat /sys/kernel/debug/rcu/rcu_preempt/rcudata
# cpu, ctw, gpc, tne 등 per-CPU RCU 상태

$ cat /sys/kernel/debug/rcu/rcu_preempt/rcugp
# grace period 진행 상태

실전 사례별 메시지 분석

사례 1: spinlock 데드락으로 인한 stall

/* 메시지 */
rcu: INFO: rcu_sched self-detected stall on CPU
         3-....: (63015 ticks this GP) idle=9b6/0/0x1
NMI backtrace for cpu 3
Call Trace:
 <IRQ>
  native_queued_spin_lock_slowpath+0x1c5/0x200
  _raw_spin_lock+0x30/0x40
  my_driver_irq_handler+0x42/0x150 [my_driver]
  __handle_irq_event_percpu+0x4c/0x1c0

/*
 * 분석:
 * - idle=9b6 (짝수가 아님 → 커널 코드 실행 중)
 * - IRQ 컨텍스트에서 spinlock 획득 대기 중 (spin_lock_slowpath)
 * - my_driver 인터럽트 핸들러가 이미 다른 컨텍스트에서
 *   잡고 있는 락을 재요청 → ABBA 데드락 또는 irq-safe 미사용
 *
 * 해결:
 * - spin_lock() → spin_lock_irqsave()로 변경
 * - lockdep(CONFIG_PROVE_LOCKING)으로 데드락 패턴 확인
 */

사례 2: 커널 모듈의 무한 루프

/* 메시지 */
rcu: INFO: rcu_preempt detected stalls on CPUs/tasks:
         5-....: (1 GPs behind) idle=d32/1/0x4000000000000001
                 softirq=8532/8532 fqs=10521
NMI backtrace for cpu 5
Call Trace:
  buggy_poll_status+0x18/0x30 [my_module]
  buggy_workqueue_fn+0x85/0xb0 [my_module]
  process_one_work+0x1e5/0x3f0
  worker_thread+0x50/0x3c0

/*
 * 분석:
 * - softirq=8532/8532 (차이 0 → softirq 폭주 아님)
 * - fqs=10521 (매우 높음 → 오래 기다림)
 * - idle의 마지막 값이 홀수(0x...1) → CPU가 활성 상태
 * - backtrace: workqueue에서 실행 중인 buggy_poll_status가
 *   하드웨어 상태를 무한 폴링 (타임아웃 없는 busy-wait)
 *
 * 해결:
 * - 폴링 루프에 cond_resched() 또는 타임아웃 추가
 * - 또는 wait_event_timeout()으로 이벤트 기반 대기로 전환
 */

사례 3: RT 태스크로 인한 kthread starvation

/* 메시지 */
rcu: INFO: rcu_sched detected stalls on CPUs/tasks:
         0-....: (1 GPs behind) idle=21a/1/0x4000000000000001
         1-....: (1 GPs behind) idle=b7e/1/0x4000000000000001
rcu: rcu_sched kthread starved for 52003 jiffies!
     last ran 52003 jiffies ago on CPU 0
     with state 0x0

/*
 * 분석:
 * - 여러 CPU가 동시에 stall (CPU 0, 1)
 * - kthread starved: state=0x0 → TASK_RUNNING인데 실행 못 함
 *   → 높은 우선순위 태스크에 밀려서 스케줄링 안 됨
 *
 * 진단:
 *   # RCU kthread 우선순위 확인
 *   chrt -p $(pgrep rcu_sched)
 *   → SCHED_OTHER (일반 스케줄링)
 *
 *   # RT 태스크 확인
 *   ps -eo pid,cls,rtprio,psr,comm | grep -E "FF|RR"
 *   → PID 1500 FIFO 99 0 stress-rt  (CPU 0 독점)
 *   → PID 1501 FIFO 99 1 stress-rt  (CPU 1 독점)
 *
 * 해결:
 * - CONFIG_RCU_BOOST=y → RCU 우선순위 부스팅 활성화
 * - RT 태스크에 sched_yield() 또는 주기적 sleep 추가
 * - rcutree.kthread_prio=2 커널 파라미터로 RCU kthread RT 우선순위 부여
 */

사례 4: 가상머신 steal time

/* 메시지 */
rcu: INFO: rcu_sched detected stalls on CPUs/tasks:
         0-....: (1 GPs behind) idle=c2e/1/0x4000000000000000
         1-....: (1 GPs behind) idle=a14/1/0x4000000000000000
         2-....: (1 GPs behind) idle=098/1/0x4000000000000000
         3-....: (1 GPs behind) idle=f40/1/0x4000000000000000
rcu: rcu_sched kthread starved for 45002 jiffies!

/*
 * 분석:
 * - 모든 CPU가 동시에 stall → 호스트 레벨 문제 의심
 * - idle 카운터의 마지막 값이 짝수(0x...0) → CPU들이 idle이었음!
 *   → vCPU가 호스트에서 스케줄링되지 못해 idle 탈출 불가
 *
 * 진단:
 *   # steal time 확인
 *   cat /proc/stat | head -5
 *   → cpu  1234 56 7890 12345 0 0 [steal] 0 0 0
 *   # steal 값이 높으면 호스트가 vCPU 시간을 빼앗은 것
 *
 *   # VM 안에서 확인
 *   vmstat 1
 *   → st 컬럼(steal time %) 확인
 *
 * 해결:
 * - 호스트 과부하 해소 (VM 밀도 감소)
 * - RCU stall 타임아웃 증가:
 *   echo 60 > /sys/module/rcupdate/parameters/rcu_cpu_stall_timeout
 * - rcupdate.rcu_cpu_stall_timeout=60 커널 파라미터
 */

Stall 예방 패턴

/* ✗ 나쁜 패턴: 긴 루프에서 RCU 차단 */
rcu_read_lock();
for (i = 0; i < 1000000; i++) {
    process_item(items[i]);     /* RCU critical section이 너무 김 */
}
rcu_read_unlock();

/* ✓ 좋은 패턴: 주기적으로 RCU 구간 재시작 */
for (i = 0; i < 1000000; i++) {
    rcu_read_lock();
    process_item(rcu_dereference(items[i]));
    rcu_read_unlock();

    if (need_resched())
        cond_resched();           /* 선점 포인트 제공 */
}

/* ✓ 좋은 패턴: 긴 루프에서 cond_resched_rcu() */
rcu_read_lock();
list_for_each_entry_rcu(entry, &my_list, list) {
    process(entry);
    cond_resched_rcu();          /* RCU unlock → resched → lock */
}
rcu_read_unlock();

/* ✗ 나쁜 패턴: 인터럽트 핸들러에서 타임아웃 없는 폴링 */
while (!(readl(reg) & DONE_BIT))
    ;  /* 하드웨어 응답 없으면 영원히 대기 */

/* ✓ 좋은 패턴: 타임아웃이 있는 폴링 */
unsigned long timeout = jiffies + msecs_to_jiffies(100);
while (!(readl(reg) & DONE_BIT)) {
    if (time_after(jiffies, timeout)) {
        dev_err(dev, "device timeout\\n");
        return -ETIMEDOUT;
    }
    cpu_relax();
}
ftrace로 stall 추적: echo 1 > /sys/kernel/debug/tracing/events/rcu/enable로 RCU tracepoint를 활성화하면 grace period 진행, QS 보고, 콜백 실행 등을 실시간으로 추적할 수 있습니다.

RCU 관련 주요 버그 사례

RCU는 강력한 동기화 메커니즘이지만, 잘못 사용하면 use-after-free, CPU stall, 커널 크래시 등 심각한 버그로 이어집니다. 아래는 실전에서 자주 발생하는 RCU 관련 버그 패턴과 올바른 해결 방법입니다.

1. RCU Use-After-Free 클래식 패턴

rcu_read_lock() 없이 RCU 보호 구조체에 접근하거나, call_rcu() 콜백에서 객체를 해제한 뒤 다른 CPU에서 해당 객체를 참조하면 use-after-free가 발생합니다. 특히 list_for_each_entry_rcu()로 리스트를 순회하는 도중 다른 CPU가 요소를 해제하면 위험합니다.

치명적 버그: RCU read-side critical section 밖에서 rcu_dereference()를 호출하면 컴파일러 최적화로 인해 dangling pointer를 참조할 수 있습니다. 이는 KASAN으로도 재현이 어려운 간헐적 크래시를 유발합니다.
/* ✗ 잘못된 패턴: rcu_read_lock() 없이 RCU 보호 포인터 접근 */
struct my_data *p;
p = rcu_dereference(global_ptr);  /* BUG: RCU read lock 없음 */
do_something(p->field);             /* use-after-free 가능 */

/* ✗ 잘못된 패턴: 순회 중 요소 해제 */
rcu_read_lock();
list_for_each_entry_rcu(entry, &head, list) {
    if (entry->should_delete) {
        list_del_rcu(&entry->list);
        kfree(entry);  /* BUG: 다른 CPU가 아직 참조 중일 수 있음 */
    }
}
rcu_read_unlock();

/* ✓ 올바른 패턴: RCU read lock + call_rcu()로 지연 해제 */
static void my_rcu_free(struct rcu_head *head)
{
    struct my_data *p = container_of(head, struct my_data, rcu);
    kfree(p);
}

/* 읽기 측: 반드시 rcu_read_lock() 안에서 접근 */
rcu_read_lock();
p = rcu_dereference(global_ptr);
if (p)
    do_something(p->field);
rcu_read_unlock();

/* 업데이트 측: list_del_rcu() 후 call_rcu()로 지연 해제 */
spin_lock(&my_lock);
list_del_rcu(&entry->list);
spin_unlock(&my_lock);
call_rcu(&entry->rcu, my_rcu_free);  /* grace period 후 안전하게 해제 */
CONFIG_PROVE_RCU 활용: 커널 빌드 시 CONFIG_PROVE_RCU=y를 활성화하면 lockdep 기반으로 RCU read-side critical section 밖에서의 rcu_dereference() 호출, 잘못된 RCU API 사용 등을 런타임에 감지합니다. 개발 및 테스트 환경에서는 반드시 활성화하십시오.

2. RCU CPU Stall 실전 디버깅

커널 로그에 "rcu: INFO: rcu_sched self-detected stall on CPU" 메시지가 출력되는 것은 특정 CPU가 RCU grace period 완료에 필요한 quiescent state를 보고하지 못하고 있다는 의미입니다. 주요 원인은 인터럽트 비활성화 상태에서의 장시간 실행, tight loop에서의 cond_resched() 누락 등입니다.

RCU CPU Stall 주요 원인: 인터럽트를 비활성화(local_irq_disable() 또는 spin_lock_irqsave())한 상태에서 장시간 실행하면 해당 CPU는 RCU quiescent state를 보고할 수 없어 grace period가 차단됩니다. 이는 전체 시스템의 RCU 콜백 처리를 지연시키고, 메모리 사용량 급증으로 이어질 수 있습니다.
/* ✗ 잘못된 패턴: tight loop에서 cond_resched() 누락 */
rcu_read_lock();
list_for_each_entry_rcu(item, &very_long_list, node) {
    expensive_processing(item);  /* 수천 개 항목 처리 시 stall 발생 */
}
rcu_read_unlock();

/* ✓ 올바른 패턴: cond_resched_rcu()로 주기적 양보 */
rcu_read_lock();
list_for_each_entry_rcu(item, &very_long_list, node) {
    expensive_processing(item);
    cond_resched_rcu();  /* RCU unlock → 스케줄링 → RCU lock */
}
rcu_read_unlock();

/* RCU CPU Stall Timeout 튜닝 (기본값: 21초) */
/* 부팅 파라미터로 조정 */
rcupdate.rcu_cpu_stall_timeout=60  /* 60초로 확장 (디버깅 시 유용) */

/* 런타임 상태 확인 */
/* /sys/kernel/debug/rcu/rcu_preempt/rcudata 내용 예시: */
/*   0 c=12345 g=12346 pq=1 qp=1 dt=5231/1/0 dn=3 ... */
/*   c: completed grace period, g: current grace period */
/*   pq: passed quiescent state, qp: quiescent state pending */
/*   dt: dyntick idle info, dn: dyntick nesting */
Stall 디버깅 절차: (1) /sys/kernel/debug/rcu/rcu_preempt/rcudata에서 어떤 CPU가 quiescent state를 보고하지 않는지 확인합니다. (2) /proc/<pid>/stack으로 해당 CPU에서 실행 중인 태스크의 콜 스택을 확인합니다. (3) ftraceirqsoff tracer로 인터럽트 비활성화 구간을 측정합니다.

3. 모듈 언로드 시 RCU 콜백 미완료 문제

커널 모듈이 call_rcu()로 콜백을 등록한 후, 해당 콜백이 실행되기 전에 모듈이 언로드되면 콜백 함수의 코드가 이미 해제된 메모리 영역을 가리키게 됩니다. 이후 RCU가 콜백을 실행하려 하면 커널 크래시(page fault)가 발생합니다.

모듈 언로드 크래시: call_rcu()는 비동기적으로 콜백을 등록합니다. grace period는 수 밀리초에서 수십 밀리초 소요되므로, module_exit()에서 rcu_barrier()를 호출하지 않으면 모듈 코드 영역이 해제된 후 콜백이 실행되어 커널 패닉이 발생합니다.
/* ✗ 잘못된 패턴: rcu_barrier() 없이 모듈 언로드 */
static void my_rcu_callback(struct rcu_head *head)
{
    struct my_obj *obj = container_of(head, struct my_obj, rcu);
    kfree(obj);
}

static void my_remove(struct my_obj *obj)
{
    list_del_rcu(&obj->list);
    call_rcu(&obj->rcu, my_rcu_callback);  /* 콜백 등록 (아직 미실행) */
}

static void __exit my_module_exit(void)
{
    my_cleanup_all();
    /* BUG: rcu_barrier() 누락 — 콜백 완료 전에 모듈 코드 해제됨 */
}

/* ✓ 올바른 패턴: rcu_barrier()로 콜백 완료 대기 */
static void __exit my_module_exit(void)
{
    my_cleanup_all();          /* 모든 객체에 대해 call_rcu() 호출 */
    rcu_barrier();              /* 모든 CPU의 RCU 콜백 완료 대기 */
    /* 이제 안전하게 모듈 언로드 가능 */
}
module_exit(my_module_exit);
rcu_barrier() 사용 규칙: call_rcu(), call_srcu(), call_rcu_tasks() 등 비동기 RCU 콜백을 사용하는 모듈은 module_exit()에서 해당 barrier(rcu_barrier(), srcu_barrier() 등)를 호출해야 합니다. CONFIG_MODULE_UNLOAD=y 환경에서 이를 누락하면 모듈 언로드 경로에서 Use-After-Free 또는 커널 크래시로 이어질 수 있습니다.

4. SRCU vs RCU 선택 오류 사례

일반 RCU의 read-side critical section에서는 sleep이 불가능합니다(CONFIG_PREEMPT_RCU에서도 voluntary sleep은 금지). 블록 I/O 완료 경로, 파일시스템 코드 등 sleep이 필요한 구간에서 일반 RCU를 사용하면 데드락이나 RCU stall이 발생합니다. 이때 SRCU(Sleepable RCU)를 사용해야 합니다.

RCU read-side에서의 sleep: rcu_read_lock()rcu_read_unlock() 사이에서 mutex_lock(), kmalloc(GFP_KERNEL), copy_to_user() 등 sleep 가능 함수를 호출하면 CONFIG_PROVE_RCU 환경에서 "suspicious RCU usage" 경고가 출력되며, 최악의 경우 grace period가 무한정 지연됩니다.
/* ✗ 잘못된 패턴: sleep 가능 경로에서 일반 RCU 사용 */
rcu_read_lock();
p = rcu_dereference(shared_ptr);
mutex_lock(&p->mutex);          /* BUG: sleep 가능! */
result = vfs_read(p->file, ...); /* BUG: 블록 I/O 발생 가능 */
mutex_unlock(&p->mutex);
rcu_read_unlock();

/* ✓ 올바른 패턴: SRCU 사용 */
DEFINE_STATIC_SRCU(my_srcu);

/* 읽기 측: srcu_read_lock()은 sleep 허용 */
int idx;
idx = srcu_read_lock(&my_srcu);
p = srcu_dereference(shared_ptr, &my_srcu);
mutex_lock(&p->mutex);          /* OK: SRCU read-side에서 sleep 가능 */
result = vfs_read(p->file, ...); /* OK: 블록 I/O도 안전 */
mutex_unlock(&p->mutex);
srcu_read_unlock(&my_srcu, idx);

/* 업데이트 측 */
rcu_assign_pointer(shared_ptr, new_data);
synchronize_srcu(&my_srcu);     /* SRCU grace period 대기 */
kfree(old_data);
RCU vs SRCU 선택 기준: (1) Read-side에서 sleep이 필요하면 → SRCU. (2) 읽기가 매우 빈번하고 오버헤드를 최소화해야 하면 → 일반 RCU (read-side 오버헤드 거의 0). (3) Grace period 지연을 도메인별로 분리해야 하면 → 별도의 srcu_struct 인스턴스 사용. (4) SRCU 사용 시 하나의 srcu_struct를 여러 무관한 용도로 공유하면 grace period가 불필요하게 길어지므로, 용도별로 분리하십시오.

성능 튜닝 가이드라인

RCU 성능은 워크로드 특성에 따라 크게 달라집니다. 이 섹션에서는 워크로드별 최적 설정과 주요 파라미터 튜닝 방법을 안내합니다.

워크로드별 튜닝 결정 트리

RCU 워크로드별 설정 결정 트리 실시간(RT) 요구사항? Yes RT 워크로드 CONFIG_PREEMPT_RT=y RCU_BOOST=y, nohz_full=, isolcpus= No CPU 격리 필요? (HPC/NFV) Yes HPC / NFV 격리 rcu_nocbs=, nohz_full=, isolcpus= NOCB + RCU_FAST_NO_HZ=y No 전력 최적화 필요? (모바일) Yes 모바일 / 임베디드 CONFIG_RCU_LAZY=y RCU_FAST_NO_HZ=y, 배칭 최적화 No 범용 서버 (기본값) CONFIG_PREEMPT=n 또는 voluntary 기본 RCU 설정, 필요시 NOCB 부분 적용 핵심 원칙: ① 격리 CPU에서 RCU 콜백 제거 → rcu_nocbs= ② idle 전력 절감 → RCU_FAST_NO_HZ ③ GP 지연 허용 → RCU_LAZY ④ GP 가속 → blimit↑, expedited
워크로드 특성에 따른 RCU 설정 결정 트리. RT/HPC/모바일 등 환경에 맞는 CONFIG와 부트 파라미터 조합을 선택합니다.

주요 튜닝 파라미터

파라미터기본값조정 시점효과
blimit10콜백 큐 폭증 시한 번에 처리하는 최대 콜백 수. 값 증가 → 콜백 처리 속도↑, softirq 지연↑
qhimark10000메모리 압박 시콜백 수가 이 값 초과 → GP 강제 시작. 낮추면 메모리 소비↓
qlowmark100qhimark와 쌍으로콜백 수가 이 값 이하로 떨어지면 강제 GP 해제
jiffies_till_first_fqs3 (jiffies)GP 지연 민감 시첫 FQS까지 대기 시간. 줄이면 GP 빨라지지만 CPU 부하↑
jiffies_till_next_fqs3 (jiffies)GP 지연 민감 시FQS 간 대기 시간
kthread_prio0RT 환경RCU kthread의 SCHED_FIFO 우선순위. RT 환경에서는 2~3 권장
# 런타임 튜닝 예시
echo 30 > /sys/module/rcutree/parameters/blimit           # 콜백 배치 크기 증가
echo 5000 > /sys/module/rcutree/parameters/qhimark        # 강제 GP 임계값 하향
echo 1 > /sys/module/rcutree/parameters/jiffies_till_first_fqs # FQS 빠르게

NOCB + nohz_full + isolcpus 최적 조합

CPU 격리 워크로드에서 최적의 조합:

# 커널 부트 파라미터 예시: CPU 4-15를 격리
isolcpus=nohz,domain,managed_irq,4-15  \
nohz_full=4-15                          \
rcu_nocbs=4-15                          \
irqaffinity=0-3

# isolcpus: 스케줄러 도메인에서 제외, 관리 IRQ 차단
# nohz_full: tick-less 모드 (1개 태스크만 실행 시 틱 중지)
# rcu_nocbs: RCU 콜백을 rcuo kthread로 오프로드 → 격리 CPU 간섭 제거
# irqaffinity: 하드웨어 IRQ를 시스템 관리 작업용 CPU(0-3)로 제한

irqaffinity=0-3는 인터럽트를 격리 CPU가 아니라 시스템 관리 작업을 맡는 CPU로 모아, RT/HPC 태스크가 도는 격리 CPU의 지터를 줄이기 위한 설정입니다.

💡

CONFIG_RCU_FAST_NO_HZ vs 기본: 이 옵션은 idle CPU가 콜백 대기 중일 때도 잠들 수 있게 합니다. 전력 절감 효과가 있지만, GP 완료 지연이 발생할 수 있습니다. nohz_full 환경에서는 항상 활성화를 권장합니다.

perf_stats 해석법

# RCU 성능 통계 확인
cat /sys/kernel/debug/rcu/rcu_preempt/perf_stats

# 출력 예시:
# Completed grace periods:              12345
# Grace-period duration (ns):           avg=1234567 max=9876543
# Force-quiescent-state scans:          678
# Expedited grace periods:              45
# RCU callback invocations:             56789

# 주의 지표:
# - GP duration max가 수 초 이상 → stall 가능성 조사
# - FQS scans이 GP 수 대비 매우 높음 → CPU가 QS 보고 지연
# - Expedited GP 비율 높음 → expedited 남용 여부 확인

CONFIG 옵션 종합

RCU 관련 주요 커널 설정 옵션을 기능별로 분류합니다.

RCU 구현 선택

CONFIG 옵션설명기본값
CONFIG_TREE_RCUSMP용 Tree RCU (대부분의 커널)SMP 시 자동 선택
CONFIG_PREEMPT_RCU선점 가능 RCU (PREEMPT 커널)CONFIG_PREEMPT=y 시 자동
CONFIG_TINY_RCUUP(단일 프로세서)용 경량 RCU!SMP 시 자동 선택
CONFIG_TASKS_RCURCU-Tasks 지원y (BPF 등 필요 시)
CONFIG_TASKS_TRACE_RCURCU-Tasks-Trace 지원y
CONFIG_SRCUSRCU 지원y

성능 튜닝

CONFIG 옵션설명기본값
CONFIG_RCU_NOCB_CPURCU 콜백 오프로딩 지원n (활성화 권장: RT/HPC)
CONFIG_RCU_FAST_NO_HZidle CPU에서 RCU 콜백 가속 처리n
CONFIG_RCU_BOOSTGP 지연 시 reader 우선순위 부스팅RT 커널에서 y
CONFIG_RCU_BOOST_DELAY부스팅 시작 지연 (ms)500
CONFIG_RCU_EXP_KTHREADexpedited GP를 전용 kthread로 처리n
CONFIG_RCU_FANOUTrcu_node 트리 fanout (가지 수)64 (64비트), 32 (32비트)
CONFIG_RCU_FANOUT_LEAF리프 노드 fanout16
CONFIG_RCU_NOCB_CPU_CB_BOOSTNOCB 콜백 처리 kthread 우선순위 부스트n
CONFIG_RCU_LAZY콜백 배치 처리 (지연 실행, 전력 절약)n

디버깅 및 테스트

CONFIG 옵션설명기본값
CONFIG_PROVE_RCUlockdep 기반 RCU 규칙 검증PROVE_LOCKING 의존
CONFIG_RCU_CPU_STALL_TIMEOUTstall 감지 타임아웃 (초)21
CONFIG_RCU_CPU_STALL_CPUTIMEstall 시 CPU 시간 통계 출력n
CONFIG_RCU_TRACERCU tracepoint 활성화n
CONFIG_RCU_TORTURE_TESTRCU 스트레스 테스트 모듈n (m 권장: 테스트 시)
CONFIG_RCU_REF_SCALE_TESTRCU 참조 획득 성능 벤치마크n
CONFIG_DEBUG_OBJECTS_RCU_HEADrcu_head 오브젝트 추적 (double-free 감지)n
CONFIG_RCU_STRICT_GRACE_PERIOD엄격한 GP 검증 (디버그 전용)n
CONFIG_PROVE_RCU_LISTRCU 리스트 순회 규칙 검증n
💡

개발/테스트 권장 설정: CONFIG_PROVE_RCU=y, CONFIG_PROVE_RCU_LIST=y, CONFIG_DEBUG_OBJECTS_RCU_HEAD=y, CONFIG_RCU_CPU_STALL_CPUTIME=y를 활성화하면 RCU 관련 버그를 조기에 발견할 수 있습니다. 운영 환경에서는 성능 영향을 고려하여 비활성화하세요.

커널 부트 파라미터 종합

rcupdate.* 파라미터

파라미터설명기본값
rcupdate.rcu_cpu_stall_timeout=NRCU stall 감지 타임아웃 (초)21
rcupdate.rcu_cpu_stall_suppress=1stall 경고 억제0
rcupdate.rcu_cpu_stall_suppress_at_boot=1부팅 중 stall 경고 억제0 (일부 배포판 1)
rcupdate.rcu_cpu_stall_ftrace_dump=1stall 시 ftrace 버퍼 덤프0
rcupdate.rcu_expedited=1모든 synchronize_rcu를 expedited로0
rcupdate.rcu_normal=1expedited GP 사용 금지 (정상 GP만)0
rcupdate.rcu_normal_after_boot=1부팅 후 expedited → normal 전환0
rcupdate.rcu_task_stall_timeout=NRCU-Tasks stall 타임아웃 (ms)600000
rcupdate.rcu_self_test=1부팅 시 RCU 자체 테스트 실행0

rcutree.* 파라미터

파라미터설명기본값
rcutree.blimit=N1회 softirq에서 처리할 최대 콜백 수10
rcutree.qhimark=N콜백 큐 하이워터마크 (GP 가속 트리거)10000
rcutree.qlowmark=N콜백 큐 로우워터마크 (가속 해제)100
rcutree.jiffies_till_first_fqs=N첫 Force QS까지 jiffies1
rcutree.jiffies_till_next_fqs=N이후 Force QS 간격 jiffies1
rcutree.kthread_prio=NRCU GP kthread RT 우선순위1 (SCHED_FIFO)
rcutree.rcu_kick_kthreads=1stall 시 kthread 강제 깨우기0

NOCB 관련 파라미터

파라미터설명예시
rcu_nocbs=CPULIST콜백 오프로드 대상 CPUrcu_nocbs=0-3,8
rcu_nocbs=all모든 CPU 콜백 오프로드
rcupdate.rcu_nocb_gp_stride=Nrcuog 그룹당 CPU 수4

srcutree.* 파라미터

파라미터설명기본값
srcutree.exp_holdoff=Nexpedited SRCU GP 간 대기 (us)25
srcutree.counter_wrap_check=N카운터 래핑 감지 주기 (GP 수)4096
srcutree.convert_to_big=Nbig SRCU 전환 임계값자동
# 부트 파라미터 조합 예 (RT/HPC 시스템)
GRUB_CMDLINE_LINUX="rcu_nocbs=0-7 \
  rcupdate.rcu_cpu_stall_timeout=60 \
  rcutree.kthread_prio=2 \
  rcupdate.rcu_normal_after_boot=1 \
  isolcpus=0-7 nohz_full=0-7"

# 런타임 파라미터 확인
cat /sys/module/rcupdate/parameters/rcu_cpu_stall_timeout
cat /sys/module/rcutree/parameters/blimit
cat /sys/module/rcutree/parameters/kthread_prio

RCU-Tasks (Trampoline RCU)

RCU-Tasks는 태스크(task_struct)를 단위로 grace period를 추적하는 RCU 변형입니다. BPF 트램폴린, 함수 훅, ftrace 패치 등 함수 포인터 수준의 동적 코드 수정이 필요한 경우에 사용됩니다.

변형QS 단위주요 사용처grace period 유발 방법
RCU-Taskstask 스케줄링 포인트BPF trampoline, ftrace, livepatchsynchronize_rcu_tasks()
RCU-Tasks-Rudetask context 전환task 전환 필요 코드 패치synchronize_rcu_tasks_rude()
RCU-Tasks-TraceBPF 프로그램 실행BPF 프로그램 교체synchronize_rcu_tasks_trace()
/* RCU-Tasks 사용 패턴 (BPF trampoline 교체 예) */
#include <linux/rcupdate_trace.h>

/* 새 trampoline 설치 후 이전 것의 안전한 해제 */
rcu_assign_pointer(prog->trampoline, new_tramp);

/* 모든 태스크가 이전 trampoline을 떠날 때까지 대기 */
synchronize_rcu_tasks();  /* 모든 task가 스케줄링 포인트를 한 번 통과할 때까지 */

bpf_trampoline_free(old_tramp);

/* RCU-Tasks-Trace read-side (BPF 프로그램 내부) */
rcu_read_lock_trace();
/* BPF 프로그램 실행 구간 */
rcu_read_unlock_trace();
ℹ️

일반 RCU와의 차이: 일반 RCU는 CPU 단위로 QS를 추적하지만, RCU-Tasks는 태스크 단위로 추적합니다. idle CPU나 커널 스레드가 없는 환경에서도 동작하며, grace period는 일반 RCU보다 훨씬 길 수 있습니다(수백 ms ~ 수 초). 핫 경로가 아닌 코드 패치, 모듈 교체 경로에서만 사용하세요.

call_rcu_tasks / rcu_barrier_tasks

/* call_rcu_tasks(): 비동기 RCU-Tasks 콜백 등록 */
call_rcu_tasks(&obj->rcu, my_tasks_callback);

/* 콜백은 모든 task가 스케줄링 포인트를 통과한 후 실행 */
void my_tasks_callback(struct rcu_head *head)
{
    struct my_obj *obj = container_of(head,
            struct my_obj, rcu);
    kfree(obj);
}

/* rcu_barrier_tasks(): 모든 call_rcu_tasks 콜백 완료 대기 */
rcu_barrier_tasks();  /* 모듈 언로드 시 필수 */

/* 모듈 exit 패턴 */
static void __exit my_exit(void)
{
    cleanup_all();
    rcu_barrier_tasks();       /* RCU-Tasks 콜백 완료 */
    rcu_barrier_tasks_trace(); /* RCU-Tasks-Trace 콜백 완료 */
    rcu_barrier();              /* 일반 RCU 콜백 완료 */
}

call_rcu_tasks_trace 상세

/* RCU-Tasks-Trace: BPF 프로그램 교체용
 * read-side: rcu_read_lock_trace() / rcu_read_unlock_trace()
 * write-side: synchronize_rcu_tasks_trace() / call_rcu_tasks_trace()
 */

/* BPF 프로그램 교체 패턴 */
rcu_assign_pointer(bpf_prog, new_prog);
synchronize_rcu_tasks_trace();  /* 모든 BPF 실행 완료 대기 */
bpf_prog_free(old_prog);

/* 비동기 변형 */
call_rcu_tasks_trace(&prog->rcu, free_prog_callback);

/* barrier 변형 */
rcu_barrier_tasks_trace();

/* rcu_read_lock_trace_held(): lockdep 검증 */
WARN_ON_ONCE(!rcu_read_lock_trace_held());

call_rcu_tasks_rude (deprecated)

⚠️

Deprecated: synchronize_rcu_tasks_rude()call_rcu_tasks_rude()는 Linux 6.x에서 deprecated 상태입니다. 기존 코드 호환을 위해 유지되지만, 새 코드에서는 synchronize_rcu_tasks() 또는 synchronize_rcu_tasks_trace()를 사용하세요.

/* RCU-Tasks-Rude (deprecated)
 * 모든 CPU에서 context switch를 강제하여 GP 완료.
 * RCU-Tasks보다 공격적이지만 시스템 영향이 큼. */
synchronize_rcu_tasks_rude();  /* deprecated — 사용 자제 */
call_rcu_tasks_rude(&head, cb); /* deprecated */

실제 커널 서브시스템 사용 패턴

RCU는 Linux 커널 전반에서 사용됩니다. 주요 서브시스템별 RCU 활용 패턴과 핵심 코드를 살펴봅니다.

실전에서는 API 이름보다 어떤 수명 관리 패턴을 선택하느냐가 더 중요합니다. 먼저 반복적으로 등장하는 구현 패턴을 보고, 이어서 실제 서브시스템 사례에 연결해 보겠습니다.

구현 패턴 선택 빠른 지도

상황Reader 경로Writer 경로회수 방식핵심 이유
읽기 위주의 설정 스냅샷rcu_read_lock() + rcu_dereference()새 객체 생성 후 rcu_replace_pointer()kfree_rcu()핫 경로가 불변 스냅샷을 즉시 읽을 수 있음
조회 후 락 밖에서 오래 사용RCU lookup 후 refcount_inc_not_zero()테이블에서 제거 후 참조 반납마지막 put에서 kfree_rcu()RCU 밖으로 객체를 안전하게 들고 나가려면 소유권 승격 필요
대량 삭제 또는 재구성기존 리스트를 계속 순회list_del_rcu()로 분리 후 한 번에 GP 대기synchronize_rcu() 뒤 일괄 kfree()콜백 폭주를 줄이고 회수 비용을 배치 처리
함수 테이블 또는 핸들러 핫스왑RCU로 현재 테이블 참조완성된 테이블 전체를 교체call_rcu()부분 수정 중간 상태를 reader가 보지 않게 함
read-side에서 sleep 필요srcu_read_lock() + srcu_dereference()포인터 교체 후 synchronize_srcu()GP 뒤 일반 kfree() 또는 call_srcu()표준 RCU는 sleep 불가, SRCU는 가능
💡

패턴 선택 기준: reader가 포인터를 rcu_read_unlock() 밖으로 들고 나갈 필요가 없으면 단순 포인터 스왑이 가장 싸고, 들고 나가야 하면 refcount 승격이 필요합니다. update가 대량이면 call_rcu()를 개별 호출하기보다 한 번의 grace period로 묶는 편이 더 낫습니다.

패턴 1: 읽기 위주 설정 스냅샷 교체

가장 흔한 형태는 작고 불변인 설정 객체를 통째로 교체하는 방식입니다. 네트워크 fast path의 정책 스냅샷, 파일시스템의 마운트 옵션 캐시, 드라이버의 런타임 설정처럼 읽기는 매우 잦고 갱신은 드문 상황에서 적합합니다.

struct fast_cfg {
    u32 batch;
    u32 timeout_ms;
    bool drop_invalid;
    struct rcu_head rcu;
};

static DEFINE_MUTEX(cfg_lock);
static struct fast_cfg __rcu *g_cfg;

int fast_path_handle(struct packet *pkt)
{
    const struct fast_cfg *cfg;
    u32 batch, timeout_ms;
    bool drop_invalid;

    rcu_read_lock();
    cfg = rcu_dereference(g_cfg);
    if (!cfg) {
        rcu_read_unlock();
        return -ENOENT;
    }

    /* 발행 후에는 불변 객체이므로 값을 지역 변수로 복사해 락 밖으로 전달 */
    batch = cfg->batch;
    timeout_ms = cfg->timeout_ms;
    drop_invalid = cfg->drop_invalid;
    rcu_read_unlock();

    return run_pipeline(pkt, batch, timeout_ms, drop_invalid);
}

int fast_cfg_update(u32 batch, u32 timeout_ms, bool drop_invalid)
{
    struct fast_cfg *new_cfg, *old_cfg;

    new_cfg = kmalloc(sizeof(*new_cfg), GFP_KERNEL);
    if (!new_cfg)
        return -ENOMEM;

    new_cfg->batch = batch;
    new_cfg->timeout_ms = timeout_ms;
    new_cfg->drop_invalid = drop_invalid;

    mutex_lock(&cfg_lock);
    old_cfg = rcu_replace_pointer(g_cfg, new_cfg,
                                 lockdep_is_held(&cfg_lock));
    mutex_unlock(&cfg_lock);

    if (old_cfg)
        kfree_rcu(old_cfg, rcu);

    return 0;
}
  1. Reader는 포인터를 오래 들고 있지 않습니다. 필요한 스칼라 값만 복사하고 바로 rcu_read_unlock() 합니다.
  2. Writer는 기존 객체를 수정하지 않습니다. 항상 새 객체를 만든 뒤 한 번에 포인터를 교체해야 reader가 반쯤 갱신된 상태를 보지 않습니다.
  3. 중첩 포인터가 있으면 깊은 복사 또는 별도 수명 관리가 필요합니다. 바깥 객체만 RCU로 보호하고 내부 버퍼를 공유하면 결국 내부 버퍼 쪽에서 UAF가 납니다.

패턴 2: RCU 조회 후 refcount로 소유권 승격

RCU reader는 원칙적으로 rcu_read_unlock() 이후 객체를 계속 쓰면 안 됩니다. lookup 결과를 잠금 밖으로 넘겨 오래 사용해야 한다면, RCU 안에서 refcount를 올려 소유권을 가져오는 handoff가 필요합니다. 소켓 lookup, PID lookup, 세션 테이블 검색에서 매우 자주 쓰입니다.

struct session {
    u32 id;
    refcount_t refcnt;
    struct hlist_node node;
    struct rcu_head rcu;
};

static DEFINE_SPINLOCK(session_lock);
static struct hlist_head session_ht[SESSION_HT_SIZE];

struct session *session_lookup_get(u32 id)
{
    struct session *s;
    u32 slot = hash_min(id, SESSION_HT_BITS);

    rcu_read_lock();
    hlist_for_each_entry_rcu(s, &session_ht[slot], node) {
        if (s->id != id)
            continue;

        /* unlock 전에 ref를 잡아야 GP 경계 밖으로 안전하게 넘길 수 있음 */
        if (!refcount_inc_not_zero(&s->refcnt))
            continue;

        rcu_read_unlock();
        return s;
    }
    rcu_read_unlock();
    return NULL;
}

void session_remove(struct session *s)
{
    spin_lock(&session_lock);
    hlist_del_rcu(&s->node);
    spin_unlock(&session_lock);

    /* 해시 테이블이 갖고 있던 참조 1개 반납 */
    session_put(s);
}

void session_put(struct session *s)
{
    if (refcount_dec_and_test(&s->refcnt))
        kfree_rcu(s, rcu);
}
⚠️

핵심 레이스: rcu_read_unlock() 후에 refcount를 올리면 늦습니다. 그 사이 writer가 객체를 테이블에서 제거하고 마지막 참조를 내려 GP 후 해제 경로로 보낼 수 있기 때문입니다. 반드시 RCU read-side 내부에서 refcount_inc_not_zero()를 호출해야 합니다.

이 패턴의 장점은 reader 핫 경로는 여전히 가볍게 유지하면서도, 찾은 객체를 I/O 완료 경로나 workqueue로 안전하게 넘길 수 있다는 점입니다. 반대로 lookup 성공률이 낮고 참조 승격이 매우 잦다면 refcount cacheline 경합이 병목이 될 수 있으므로, 그 경우에는 per-CPU 캐시나 배치 handoff를 검토해야 합니다.

패턴 3: 대량 삭제는 한 번의 grace period로 배치 회수

규칙 집합 재로딩, 라우팅 정책 전체 교체, 큰 리스트 재정렬처럼 짧은 시간에 많은 객체를 retire해야 할 때는 각 객체마다 call_rcu()를 거는 방식이 비효율적입니다. process context에서 블로킹이 가능하다면, RCU 링크만 먼저 끊고 한 번만 synchronize_rcu() 한 뒤 묶어서 해제하는 편이 더 단순하고 메모리 압력도 낮습니다.

struct rule {
    struct list_head list;
    struct list_head retire_node;  /* 생성 시 INIT_LIST_HEAD 필요 */
    u32 key;
};

static LIST_HEAD(active_rules);
static DEFINE_SPINLOCK(rules_lock);

void rules_reload(struct list_head *new_rules)
{
    struct rule *rule, *tmp;
    LIST_HEAD(retired_rules);

    spin_lock(&rules_lock);
    list_for_each_entry_safe(rule, tmp, &active_rules, list) {
        list_del_rcu(&rule->list);
        INIT_LIST_HEAD(&rule->retire_node);
        list_add_tail(&rule->retire_node, &retired_rules);
    }
    list_splice_tail_init(new_rules, &active_rules);
    spin_unlock(&rules_lock);

    /* reader들이 old rule을 모두 빠져나올 때까지 한 번만 대기 */
    synchronize_rcu();

    list_for_each_entry_safe(rule, tmp, &retired_rules, retire_node) {
        list_del_init(&rule->retire_node);
        kfree(rule);
    }
}

패턴 4: 함수 테이블 전체를 교체하는 핫스왑

BPF hook, 프로토콜 handler, LSM 정책 함수 테이블처럼 함수 포인터 묶음을 바꾸는 경우에는 개별 함수 포인터를 제자리 수정하지 말고, 완성된 테이블을 통째로 교체해야 합니다. 그래야 reader가 old/new 함수 조합이 섞인 중간 상태를 보지 않습니다.

struct parser_ops {
    int (*parse)(struct packet *pkt);
    int (*classify)(struct packet *pkt);
    struct rcu_head rcu;
};

static DEFINE_MUTEX(parser_lock);
static struct parser_ops __rcu *g_parser_ops;

int rx_classify(struct packet *pkt)
{
    const struct parser_ops *ops;
    int ret = -ENOENT;

    rcu_read_lock();
    ops = rcu_dereference(g_parser_ops);
    if (ops)
        ret = ops->classify(pkt);
    rcu_read_unlock();

    return ret;
}

static void parser_ops_free_rcu(struct rcu_head *rh)
{
    struct parser_ops *ops = container_of(rh, struct parser_ops, rcu);
    kfree(ops);
}

void parser_ops_install(struct parser_ops *new_ops)
{
    struct parser_ops *old_ops;

    mutex_lock(&parser_lock);
    old_ops = rcu_replace_pointer(g_parser_ops, new_ops,
                                 lockdep_is_held(&parser_lock));
    mutex_unlock(&parser_lock);

    if (old_ops)
        call_rcu(&old_ops->rcu, parser_ops_free_rcu);
}

왜 call_rcu()인가? 이 예시에서는 단순 kfree_rcu()보다 call_rcu()가 더 적절합니다. 함수 테이블과 함께 모듈 참조, 통계 버퍼, 보조 메모리 정리 같은 사용자 정의 destructor가 필요할 수 있기 때문입니다. 또한 모듈이 이 콜백을 제공한다면 module_exit()에서 rcu_barrier()가 필요하다는 점도 자연스럽게 연결됩니다.

⚠️

금지 패턴: g_parser_ops->classify = new_fn; 같은 제자리 변경은 위험합니다. reader 하나는 새 classify를 보고 다른 reader는 아직 옛 parse를 보는 식으로, 함수들 간의 일관된 조합이 깨질 수 있습니다.

패턴 5: sleep 가능한 정책 조회는 SRCU로 분리

reader가 mutex를 잡거나 펌웨어 로딩, 파일 I/O, 메모리 할당처럼 sleep 가능한 작업을 해야 하면 일반 RCU는 맞지 않습니다. 이 경우에는 해당 서브시스템 전용 srcu_struct를 두고, read-side를 SRCU 도메인으로 분리하는 편이 안전합니다.

struct policy_state {
    struct mutex lock;
    u32 version;
};

DEFINE_STATIC_SRCU(policy_srcu);
static DEFINE_MUTEX(policy_update_lock);
static struct policy_state __rcu *policy_ptr;

int policy_check(struct request *req)
{
    struct policy_state *policy;
    int idx, ret;

    idx = srcu_read_lock(&policy_srcu);
    policy = srcu_dereference(policy_ptr, &policy_srcu);
    if (!policy) {
        ret = -ENOENT;
        goto out;
    }

    mutex_lock(&policy->lock);     /* SRCU read-side에서는 sleep 가능 */
    ret = slow_policy_match(policy, req);
    mutex_unlock(&policy->lock);
out:
    srcu_read_unlock(&policy_srcu, idx);
    return ret;
}

void policy_reload(struct policy_state *new_policy)
{
    struct policy_state *old_policy;

    mutex_lock(&policy_update_lock);
    old_policy = rcu_dereference_protected(policy_ptr,
                    lockdep_is_held(&policy_update_lock));
    rcu_assign_pointer(policy_ptr, new_policy);
    mutex_unlock(&policy_update_lock);

    synchronize_srcu(&policy_srcu);
    kfree(old_policy);
}
  1. SRCU 도메인을 분리 - 무관한 정책 엔진이 하나의 srcu_struct를 공유하면 grace period가 서로의 tail latency를 끌어올립니다.
  2. writer는 여전히 직렬화가 필요 - SRCU가 있다고 해서 writer 경쟁이 사라지지 않습니다. 포인터 교체 자체는 mutex나 spinlock으로 보호해야 합니다.
  3. 업데이트 빈도가 높으면 비동기 회수도 검토 - reload가 매우 잦다면 synchronize_srcu() 대신 call_srcu()로 회수를 밀어내는 것이 더 나을 수 있습니다.
ℹ️

정리: 표준 RCU는 "아주 짧고 아주 자주 실행되는 reader"에, SRCU는 "sleep 가능하지만 상대적으로 무거운 reader"에 맞습니다. 두 메커니즘을 섞어 쓰기보다, 각 자료구조가 요구하는 reader 특성에 맞춰 하나를 명확히 선택하는 편이 디버깅과 lockdep 추론에 유리합니다.

RCU가 보호하는 범위와 보호하지 않는 범위

실전 버그의 상당수는 "RCU가 객체를 보호한다"는 문장을 너무 넓게 해석해서 발생합니다. RCU가 직접 보장하는 것은 RCU read-side에서 도달 가능한 포인터 경로grace period 전까지의 메모리 유지입니다. 반대로 타이머, workqueue, DMA, 사용자 참조처럼 RCU lookup 없이 객체를 직접 들고 있는 사용자는 별도 종료 절차가 필요합니다.

대상RCU만으로 충분한가추가로 필요한 것설명
rcu_dereference()rcu_read_unlock() 이전 접근O없음전형적인 RCU 보호 구간
unlock 이후에도 객체 사용Xrefcount 또는 다른 소유권RCU는 임계구역 밖 수명을 보장하지 않음
timer/workqueue/completion 콜백Xdel_timer_sync(), cancel_work_sync()콜백은 이미 객체 포인터를 직접 보유할 수 있음
모듈 코드의 함수 포인터조건부synchronize_rcu() 또는 rcu_barrier()언등록 후에도 기존 reader가 함수 본문을 실행 중일 수 있음
중첩 자원Xdestructor에서 개별 정리RCU는 바깥 컨테이너 해제 시점만 늦출 뿐 내부 자원 수명 정책까지 대신하지 않음
⚠️

핵심 원칙: "해시 테이블에서 제거했으니 끝"이라고 생각하면 위험합니다. RCU는 새 lookup을 막는 데는 좋지만, 이미 예약된 타이머나 이미 큐에 올라간 work item까지 자동으로 없애 주지는 않습니다.

패턴 6: timer/workqueue를 가진 객체의 제거 순서

연결 추적 객체, 캐시 엔트리, 장치 상태 객체처럼 해시 테이블에 RCU로 게시되면서 동시에 timer나 workqueue도 붙어 있는 경우가 많습니다. 이때는 RCU 제거비동기 실행원 종료를 분리해서 처리해야 합니다.

struct conn {
    refcount_t refcnt;
    bool dead;
    struct hlist_node node;
    struct timer_list keepalive_timer;
    struct work_struct retry_work;
    struct rcu_head rcu;
};

static DEFINE_SPINLOCK(conn_lock);
static struct hlist_head conn_ht[CONN_HT_SIZE];

static void conn_retry_workfn(struct work_struct *work)
{
    struct conn *c = container_of(work, struct conn, retry_work);

    if (READ_ONCE(c->dead))
        return;

    retry_packet(c);

    if (!READ_ONCE(c->dead))
        mod_timer(&c->keepalive_timer, jiffies + HZ);
}

static void conn_timer_fn(struct timer_list *t)
{
    struct conn *c = from_timer(c, t, keepalive_timer);

    if (READ_ONCE(c->dead))
        return;

    queue_work(system_unbound_wq, &c->retry_work);
}

void conn_remove(struct conn *c)
{
    WRITE_ONCE(c->dead, true);   /* 재arm 차단 */

    spin_lock(&conn_lock);
    hlist_del_rcu(&c->node);      /* 새 lookup 차단 */
    spin_unlock(&conn_lock);

    del_timer_sync(&c->keepalive_timer);
    cancel_work_sync(&c->retry_work);

    /* 해시 테이블이 갖고 있던 publication ref 반납 */
    conn_put(c);
}

void conn_put(struct conn *c)
{
    if (refcount_dec_and_test(&c->refcnt))
        kfree_rcu(c, rcu);
}
  1. 먼저 dead 플래그 - timer와 work가 서로를 다시 예약할 수 있으므로, 제거 초기에 재arm을 막아야 합니다.
  2. 그 다음 RCU 링크 제거 - 새 reader가 더 이상 객체를 찾지 못하게 합니다.
  3. 그 다음 비동기 실행원 종료 - del_timer_sync()cancel_work_sync()는 이미 실행 중인 콜백까지 끝날 때까지 기다립니다.
  4. 마지막으로 참조 반납 - 기존 RCU reader가 남아 있더라도 kfree_rcu()가 GP 뒤에 메모리를 정리합니다.

왜 이 순서인가? 만약 conn_put()를 먼저 호출해 마지막 참조를 떨어뜨리면, 아직 종료되지 않은 timer/work가 해제 예정 객체를 건드릴 수 있습니다. 반대로 timer/work만 먼저 끄고 해시에서 늦게 빼면, 그 사이 새 lookup이 계속 들어와 객체 생존 시간이 늘어납니다.

패턴 7: 관련 필드를 하나의 루트 객체로 묶어 발행

버킷 배열, 크기, 기본 정책, 함수 테이블처럼 함께 일관되게 바뀌어야 하는 필드가 여러 개라면, 전역 변수 여러 개를 따로 publish하지 말고 하나의 루트 객체에 묶어 RCU로 교체해야 합니다. 이는 설정 스냅샷보다 한 단계 큰 패턴으로, 해시 테이블 루트나 정책 엔진 컨텍스트에 자주 쓰입니다.

struct acl_root {
    u32 mask;
    bool default_deny;
    struct acl_bucket *buckets;
    struct acl_ops *ops;
    struct rcu_head rcu;
};

static DEFINE_MUTEX(acl_lock);
static struct acl_root __rcu *g_acl_root;

static void acl_root_free_rcu(struct rcu_head *rh)
{
    struct acl_root *root = container_of(rh, struct acl_root, rcu);

    kfree(root->buckets);
    kfree(root);
}

int acl_lookup(const struct flow_key *key)
{
    const struct acl_root *root;
    const struct acl_bucket *bucket;
    u32 slot;
    int ret;

    rcu_read_lock();
    root = rcu_dereference(g_acl_root);
    if (!root) {
        rcu_read_unlock();
        return -ENOENT;
    }

    slot = flow_hash(key) & root->mask;
    bucket = &root->buckets[slot];
    ret = root->ops->match(bucket, key, root->default_deny);
    rcu_read_unlock();

    return ret;
}

int acl_reload(struct acl_root *new_root)
{
    struct acl_root *old_root;

    mutex_lock(&acl_lock);
    old_root = rcu_dereference_protected(g_acl_root,
                    lockdep_is_held(&acl_lock));
    rcu_assign_pointer(g_acl_root, new_root);
    mutex_unlock(&acl_lock);

    if (old_root)
        call_rcu(&old_root->rcu, acl_root_free_rcu);
    return 0;
}
💡

왜 전역 변수 여러 개가 아니라 루트 1개인가? g_mask, g_buckets, g_ops를 따로 갱신하면 reader가 서로 다른 세대의 값을 섞어 볼 수 있습니다. 반면 루트 객체 하나만 publish하면 reader는 항상 같은 generation의 필드 집합을 봅니다.

이 패턴은 "설정 스냅샷"보다 더 큰 단위의 publish에 해당합니다. 핵심은 연관 필드끼리 묶어서 deep copy 후 한 번에 교체하는 것이며, 해제 콜백에서 루트 아래의 하위 버퍼까지 같이 정리하는 점입니다.

패턴 8: XArray lockless lookup과 RCU handoff

현대 커널에서는 해시 리스트만이 아니라 XArray 같은 공용 자료구조 위에서도 RCU 기반 lockless lookup을 자주 사용합니다. 이때도 원리는 같습니다. xa_load()로 얻은 포인터는 일시적인 RCU 보호 포인터일 뿐이므로, 임계구역 밖으로 들고 나가려면 refcount 승격이 필요합니다.

struct file_obj {
    refcount_t refcnt;
    struct inode *inode;
    struct rcu_head rcu;
};

DEFINE_XARRAY(file_xa);

struct file_obj *file_get_by_id(unsigned long id)
{
    struct file_obj *obj;

    rcu_read_lock();
    obj = xa_load(&file_xa, id);
    if (obj && !refcount_inc_not_zero(&obj->refcnt))
        obj = NULL;
    rcu_read_unlock();

    return obj;
}

int file_publish(unsigned long id, struct file_obj *new_obj)
{
    void *old;

    refcount_set(&new_obj->refcnt, 1);   /* XArray가 publication ref 1개 보유 */
    old = xa_store(&file_xa, id, new_obj, GFP_KERNEL);
    if (xa_is_err(old))
        return xa_err(old);

    if (old)
        file_put(old);
    return 0;
}

void file_remove(unsigned long id)
{
    struct file_obj *obj;

    obj = xa_erase(&file_xa, id);
    if (obj)
        file_put(obj);
}

void file_put(struct file_obj *obj)
{
    if (refcount_dec_and_test(&obj->refcnt))
        kfree_rcu(obj, rcu);
}

패턴 9: 콜백 체인 언등록과 모듈 언로드 경계

trace hook, 프로토콜 핸들러 목록, 내부 notifier 유사 구조처럼 콜백 객체 목록을 RCU로 돌리는 경우도 많습니다. 이런 패턴은 reader가 함수를 순회해 바로 호출하므로 빠르지만, 반대로 언등록 후에도 기존 reader가 함수 본문을 계속 실행할 수 있다는 점을 항상 의식해야 합니다.

struct pkt_hook {
    int (*fn)(struct packet *pkt);
    struct list_head node;
    struct rcu_head rcu;
};

static LIST_HEAD(pkt_hook_list);
static DEFINE_MUTEX(pkt_hook_lock);

int pkt_run_hooks(struct packet *pkt)
{
    struct pkt_hook *hook;
    int ret = 0;

    rcu_read_lock();
    list_for_each_entry_rcu(hook, &pkt_hook_list, node) {
        ret = hook->fn(pkt);
        if (ret)
            break;
    }
    rcu_read_unlock();

    return ret;
}

int pkt_hook_register(struct pkt_hook *hook)
{
    mutex_lock(&pkt_hook_lock);
    list_add_rcu(&hook->node, &pkt_hook_list);
    mutex_unlock(&pkt_hook_lock);
    return 0;
}

static void pkt_hook_free_rcu(struct rcu_head *rh)
{
    struct pkt_hook *hook = container_of(rh, struct pkt_hook, rcu);
    kfree(hook);
}

void pkt_hook_unregister(struct pkt_hook *hook)
{
    mutex_lock(&pkt_hook_lock);
    list_del_rcu(&hook->node);
    mutex_unlock(&pkt_hook_lock);

    call_rcu(&hook->rcu, pkt_hook_free_rcu);
}
  1. 언등록 직후에도 기존 reader는 hook을 돌 수 있습니다. 따라서 list_del_rcu() 다음에 즉시 kfree() 하면 UAF가 납니다.
  2. 콜백 본문이 sleep하면 안 됩니다. 이 패턴은 보통 패킷 경로나 tracepoint 같은 핫 경로용입니다. sleep 가능한 콜백 목록이면 일반 RCU 대신 SRCU나 blocking notifier가 맞습니다.
  3. 모듈 언로드는 한 단계 더 필요합니다. hook 구조체를 call_rcu()로 해제하는 모듈은 module_exit()에서 rcu_barrier()까지 호출해야 콜백과 해제 함수가 모두 끝난 뒤 모듈 텍스트를 내려갈 수 있습니다.
ℹ️

실전 판단법: "reader가 데이터를 읽는가"뿐 아니라 "reader가 함수 호출까지 하는가"를 구분하세요. 함수 호출형 RCU는 객체 메모리뿐 아니라 코드 수명까지 같이 생각해야 해서, 모듈 경계에서 특히 더 까다롭습니다.

여기까지가 가장 자주 보이는 기본 패턴입니다. 아래부터는 실제 커널 코드에서 한 단계 더 자주 부딪히는 고급 조합을 정리합니다. 특히 "RCU 하나만으로 해결되지 않는 부분"과 "다른 동기화와 결합해야 정확해지는 부분"을 의식해서 읽는 것이 중요합니다.

패턴 10: RCU + seqcount로 일관된 스칼라 묶음 읽기

RCU는 포인터 수명을 다루는 데 뛰어나지만, 서로 연관된 여러 스칼라 값을 한 세트로 일관되게 읽는 문제는 별도의 도구가 더 낫습니다. 대표적인 조합이 RCU + seqcount입니다. 객체의 정체성(identity)과 생존은 RCU가 보장하고, 객체 내부의 짧은 scalar 묶음의 일관성은 seqcount가 보장합니다.

struct classifier_ops {
    int (*classify)(struct packet *pkt,
                    u32 rate_limit, u32 burst, bool drop_invalid);
    struct rcu_head rcu;
};

struct qos_policy {
    seqcount_t cfg_seq;
    u32 rate_limit;
    u32 burst;
    bool drop_invalid;
    struct classifier_ops __rcu *cls;
    struct rcu_head rcu;
};

static DEFINE_MUTEX(qos_lock);
static struct qos_policy __rcu *g_qos;

int qos_classify(struct packet *pkt)
{
    struct qos_policy *policy;
    struct classifier_ops *cls;
    unsigned seq;
    u32 rate_limit, burst;
    bool drop_invalid;
    int ret;

    rcu_read_lock();
    policy = rcu_dereference(g_qos);
    if (!policy) {
        rcu_read_unlock();
        return -ENOENT;
    }

    do {
        seq = read_seqcount_begin(&policy->cfg_seq);
        rate_limit = READ_ONCE(policy->rate_limit);
        burst = READ_ONCE(policy->burst);
        drop_invalid = READ_ONCE(policy->drop_invalid);
        cls = rcu_dereference(policy->cls);
    } while (read_seqcount_retry(&policy->cfg_seq, seq));

    ret = cls->classify(pkt, rate_limit, burst, drop_invalid);
    rcu_read_unlock();
    return ret;
}

void qos_update_scalars(struct qos_policy *policy,
                             u32 rate_limit, u32 burst, bool drop_invalid)
{
    mutex_lock(&qos_lock);
    write_seqcount_begin(&policy->cfg_seq);
    policy->rate_limit = rate_limit;
    policy->burst = burst;
    policy->drop_invalid = drop_invalid;
    write_seqcount_end(&policy->cfg_seq);
    mutex_unlock(&qos_lock);
}

void qos_replace_classifier(struct qos_policy *policy,
                                  struct classifier_ops *new_cls)
{
    struct classifier_ops *old_cls;

    mutex_lock(&qos_lock);
    write_seqcount_begin(&policy->cfg_seq);
    old_cls = rcu_dereference_protected(policy->cls,
                     lockdep_is_held(&qos_lock));
    rcu_assign_pointer(policy->cls, new_cls);
    write_seqcount_end(&policy->cfg_seq);
    mutex_unlock(&qos_lock);

    if (old_cls)
        call_rcu(&old_cls->rcu, classifier_free_rcu);
}
⚠️

흔한 오해: seqcount만 있으면 포인터까지 안전하다고 생각하기 쉽습니다. 하지만 seqcount는 "찢어진 읽기"를 막을 뿐, writer가 옛 포인터가 가리키는 메모리를 해제하는 순간을 지연시키지 않습니다. 포인터 생존은 여전히 RCU나 refcount가 맡아야 합니다.

패턴 11: 슬롯 재사용과 ABA 문제는 generation으로 차단

RCU lookup이 빠르다고 해도, id 슬롯이 재사용되는 구조에서는 예전 핸들이 우연히 새 객체를 가리키는 문제가 남습니다. 이건 메모리 UAF가 아니라 논리적 ABA 버그입니다. 파일 핸들, 세션 ID, 내부 객체 핸들 테이블에서 흔합니다. 해결은 generation counter를 핸들에 포함시키는 것입니다.

struct handle_obj {
    u32 id;
    u32 gen;              /* publish 후 불변 */
    refcount_t refcnt;
    struct rcu_head rcu;
};

static DEFINE_SPINLOCK(handle_lock);
static struct handle_obj __rcu *handle_slots[MAX_HANDLES];

static inline u64 encode_handle(struct handle_obj *obj)
{
    return ((u64)obj->gen << 32) | obj->id;
}

struct handle_obj *handle_get(u64 handle)
{
    u32 id = (u32)handle;
    u32 gen = handle >> 32;
    struct handle_obj *obj;

    rcu_read_lock();
    obj = rcu_dereference(handle_slots[id]);
    if (!obj || READ_ONCE(obj->gen) != gen ||
        !refcount_inc_not_zero(&obj->refcnt))
        obj = NULL;
    rcu_read_unlock();

    return obj;
}

int handle_publish(u32 id, struct handle_obj *new_obj)
{
    struct handle_obj *old_obj;

    spin_lock(&handle_lock);
    old_obj = rcu_dereference_protected(handle_slots[id],
                    lockdep_is_held(&handle_lock));
    new_obj->id = id;
    new_obj->gen = old_obj ? old_obj->gen + 1 : 1;
    refcount_set(&new_obj->refcnt, 1);
    rcu_assign_pointer(handle_slots[id], new_obj);
    spin_unlock(&handle_lock);

    if (old_obj)
        handle_put(old_obj);
    return 0;
}

void handle_put(struct handle_obj *obj)
{
    if (refcount_dec_and_test(&obj->refcnt))
        kfree_rcu(obj, rcu);
}
  1. generation은 publish 후 불변이어야 합니다. 기존 객체의 gen을 제자리 증가시키면 reader가 같은 포인터에서 서로 다른 세대를 보게 됩니다.
  2. 핸들 검증은 refcount 승격 전에 끝내는 것이 일반적입니다. 잘못된 generation이면 애초에 소유권을 줄 필요가 없습니다.
  3. RCU는 메모리 안전을, generation은 의미 안전을 보장합니다. 둘은 역할이 다릅니다.

패턴 12: hlist_nulls로 재해시 중 lookup 재시도

흐름 테이블, 소켓 테이블처럼 엔트리가 버킷 사이를 옮겨 다니는 해시 구조에서는 단순 hlist보다 hlist_nulls가 유리합니다. reader가 순회 중에 엔트리가 다른 버킷으로 이동해도, 리스트 끝의 nulls 마커로 "내가 잘못된 버킷을 끝까지 읽었다"는 사실을 감지하고 재시도할 수 있기 때문입니다.

struct flow_entry {
    u32 key;
    u32 bucket_id;
    refcount_t refcnt;
    struct hlist_nulls_node node;
    struct rcu_head rcu;
};

static DEFINE_SPINLOCK(flow_lock);
static struct hlist_nulls_head flow_ht[FLOW_HT_SIZE];

struct flow_entry *flow_lookup_get(u32 key)
{
    u32 slot = hash_min(key, FLOW_HT_BITS);
    struct hlist_nulls_node *node;
    struct flow_entry *flow;

begin:
    rcu_read_lock();
    hlist_nulls_for_each_entry_rcu(flow, node, &flow_ht[slot], node) {
        if (flow->key != key)
            continue;
        if (!refcount_inc_not_zero(&flow->refcnt))
            continue;
        rcu_read_unlock();
        return flow;
    }
    rcu_read_unlock();

    if (get_nulls_value(node) != slot)
        goto begin;  /* 순회 중 rehash/move 감지 */

    return NULL;
}

void flow_move_bucket(struct flow_entry *flow, u32 new_slot)
{
    spin_lock(&flow_lock);
    flow->bucket_id = new_slot;
    hlist_nulls_del_rcu(&flow->node);
    hlist_nulls_add_head_rcu(&flow->node, &flow_ht[new_slot]);
    spin_unlock(&flow_lock);
}

왜 일반 hlist가 아니라 hlist_nulls인가? 일반 hlist에서는 reader가 이전 버킷을 순회하다가 엔트리가 다른 버킷으로 이동하면, 그 reader는 "못 찾았다"는 결과를 정상 종료로 오인할 수 있습니다. nulls 마커는 이 종료가 정말 빈 버킷의 끝이었는지, 아니면 중간에 구조가 이동했는지를 구분하게 해 줍니다.

💡

적합한 상황: 키 갱신, 리밸런싱, namespace 이동처럼 객체가 버킷을 옮길 수 있는 해시 구조라면 hlist_nulls가 좋은 선택입니다. 반대로 객체가 한 번 들어간 버킷에서 거의 움직이지 않는다면 일반 hlist가 더 단순합니다.

패턴 13: sleep 가능한 listener 집합은 SRCU + call_srcu

앞의 `패턴 9`는 hot path 콜백 목록에 적합했습니다. 하지만 listener가 펌웨어 요청, sysfs 접근, mutex, 메모리 할당처럼 sleep 가능한 작업을 수행해야 하면 일반 RCU callback list는 맞지 않습니다. 이때는 SRCU 기반 listener 집합이 자연스럽습니다.

struct pm_listener {
    int (*fn)(struct pm_event *ev);
    struct list_head node;
    struct rcu_head rcu;
};

DEFINE_STATIC_SRCU(pm_srcu);
static LIST_HEAD(pm_listener_list);
static DEFINE_MUTEX(pm_listener_lock);

int pm_dispatch_event(struct pm_event *ev)
{
    struct pm_listener *lst;
    int idx, ret = 0;

    idx = srcu_read_lock(&pm_srcu);
    list_for_each_entry_srcu(lst, &pm_listener_list, node,
            srcu_read_lock_held(&pm_srcu)) {
        ret = lst->fn(ev);         /* sleep 가능 */
        if (ret)
            break;
    }
    srcu_read_unlock(&pm_srcu, idx);
    return ret;
}

int pm_listener_register(struct pm_listener *lst)
{
    mutex_lock(&pm_listener_lock);
    list_add_rcu(&lst->node, &pm_listener_list);
    mutex_unlock(&pm_listener_lock);
    return 0;
}

static void pm_listener_free_srcu(struct rcu_head *rh)
{
    struct pm_listener *lst = container_of(rh, struct pm_listener, rcu);
    kfree(lst);
}

void pm_listener_unregister(struct pm_listener *lst)
{
    mutex_lock(&pm_listener_lock);
    list_del_rcu(&lst->node);
    mutex_unlock(&pm_listener_lock);

    call_srcu(&pm_srcu, &lst->rcu, pm_listener_free_srcu);
}
  1. dispatch path가 sleep 가능하므로 srcu_read_lock()을 사용합니다.
  2. 언등록 후 free는 call_srcu()로 미룹니다. SRCU reader가 모두 빠져나오기 전에는 listener 구조체를 해제할 수 없습니다.
  3. 모듈 경계라면 srcu_barrier()까지 필요합니다. call_srcu() callback을 모듈이 제공한다면 module exit에서 반드시 기다려야 합니다.

이 패턴은 blocking notifier와 비슷한 문제를 다루지만, 자료구조 형식과 lifetime 정책을 직접 통제할 수 있다는 점이 다릅니다. 내부 서브시스템에서 listener 객체에 별도 상태나 통계를 붙이고 싶다면 custom SRCU list가 더 유연합니다.

패턴 14: 공개 후 제자리 수정 가능한 필드와 금지되는 필드

RCU에서 가장 흔한 설계 질문 중 하나는 이것입니다. "이 필드는 publish 이후에도 그냥 바꿔도 되나?" 답은 필드 유형에 따라 다릅니다. 중요한 기준은 reader가 그 필드를 단독 값으로 읽는지, 다른 필드와 조합해서 해석하는지, 포인터를 따라가며 lifetime을 기대하는지입니다.

필드 종류제자리 수정 가능 여부필요한 도구이유
통계 카운터대체로 가능atomic_t, u64_stats, per-CPU 변수reader가 약간의 stale 값을 허용하는 경우가 많음
단일 bool/flag조건부 가능READ_ONCE/WRITE_ONCE찢어진 읽기만 피하면 되는 경우
여러 scalar의 의미적 묶음그냥은 위험copy-replace 또는 seqcount서로 다른 세대가 섞이면 논리 오류
reader가 역참조하는 포인터직접 overwrite 금지rcu_assign_pointer() + GP 후 free옛 포인터가 가리키는 메모리 생존이 필요
길이 + 버퍼 포인터 쌍제자리 수정 비권장루트 객체 교체길이만 새 값이고 버퍼는 옛 값이면 메모리 오류 가능
함수 포인터 묶음개별 변경 금지ops 테이블 통교체중간 조합 노출 위험
struct nexthop {
    struct rcu_head rcu;
};

static DEFINE_SPINLOCK(route_lock);

struct route_cfg {
    atomic_long_t packets;          /* 제자리 증가 가능 */
    bool enabled;                   /* READ_ONCE/WRITE_ONCE 가능 */
    u32 mtu;                        /* 단독 의미면 WRITE_ONCE 가능 */
    struct nexthop __rcu *nh;       /* 포인터는 RCU 규칙 필요 */
};

void route_fastpath_hit(struct route_cfg *cfg)
{
    atomic_long_inc(&cfg->packets);
}

bool route_is_enabled(struct route_cfg *cfg)
{
    return READ_ONCE(cfg->enabled);
}

void route_set_enabled(struct route_cfg *cfg, bool on)
{
    WRITE_ONCE(cfg->enabled, on);
}

void route_replace_nh(struct route_cfg *cfg, struct nexthop *new_nh)
{
    struct nexthop *old_nh;

    spin_lock(&route_lock);
    old_nh = rcu_replace_pointer(cfg->nh, new_nh,
                 lockdep_is_held(&route_lock));
    spin_unlock(&route_lock);

    if (old_nh)
        call_rcu(&old_nh->rcu, nexthop_free_rcu);
}

핵심 감각: stale 값이 허용되는 단일 스칼라는 제자리 수정이 가능하지만, 다른 메모리 객체로 이어지는 관계가 들어가는 순간부터는 거의 항상 RCU publication 규칙이 다시 등장합니다. "포인터가 아니면 가볍고, 포인터면 무겁다"가 아니라, reader가 그 값을 어떻게 해석하는가가 기준입니다.

패턴 15: 부모는 고정하고 leaf 객체만 교체하는 세밀한 업데이트

패턴 7에서는 관련 필드를 모두 루트 객체에 묶어 통째로 교체했습니다. 하지만 모든 변경이 그렇게 무거울 필요는 없습니다. 부모 객체의 정체성은 유지하면서 하위 leaf 포인터 하나만 바꾸면 충분한 경우도 많습니다. 예를 들어 tenant, netdev, namespace 같은 상위 컨텍스트는 오래 살고, 그 밑의 현재 정책/쿼터/백엔드만 자주 바뀌는 경우입니다.

struct quota_state {
    u64 hard_limit;
    u64 soft_limit;
    struct rcu_head rcu;
};

struct tenant_ctx {
    spinlock_t lock;
    refcount_t refcnt;
    struct quota_state __rcu *quota;
};

int tenant_charge(struct tenant_ctx *tenant, u64 amount)
{
    struct quota_state *quota;
    int ret = 0;

    rcu_read_lock();
    quota = rcu_dereference(tenant->quota);
    if (!quota || amount > quota->hard_limit)
        ret = -EDQUOT;
    rcu_read_unlock();

    return ret;
}

void tenant_replace_quota(struct tenant_ctx *tenant,
                                struct quota_state *new_quota)
{
    struct quota_state *old_quota;

    spin_lock(&tenant->lock);
    old_quota = rcu_replace_pointer(tenant->quota, new_quota,
                    lockdep_is_held(&tenant->lock));
    spin_unlock(&tenant->lock);

    if (old_quota)
        kfree_rcu(old_quota, rcu);
}
💡

설계 감각: 자주 바뀌는 leaf와 오래 사는 parent를 분리하면 copy 비용을 줄일 수 있습니다. 반대로 parent와 leaf가 항상 한 세트로 움직여야 한다면 미세 최적화보다 generation 일관성이 더 중요합니다.

고급 패턴 선택 체크리스트

질문권장 패턴
reader가 unlock 뒤에도 객체를 계속 쓰는가패턴 2 또는 8처럼 refcount handoff
timer/workqueue가 객체를 직접 들고 있는가패턴 6 teardown 순서 적용
여러 scalar를 한 세트로 읽어야 하는가패턴 10의 seqcount 재검증
핸들이 재사용될 수 있는가패턴 11 generation 추가
객체가 버킷 사이를 이동하는가패턴 12 hlist_nulls
listener/reader가 sleep 가능한가패턴 5 또는 13의 SRCU
관련 필드가 같은 generation이어야 하는가패턴 7 루트 통교체
parent는 안정적이고 leaf만 자주 바뀌는가패턴 15 leaf 교체

네트워크: FIB 라우팅 테이블

net/ipv4/fib_trie.c의 라우팅 테이블은 RCU의 대표적 사용처입니다. 패킷 수신 경로(softirq)에서 매번 락 없이 라우팅 조회가 가능합니다.

/* net/ipv4/fib_trie.c — RCU 보호 라우팅 조회 (간략화) */
int fib_table_lookup(struct fib_table *tb,
    const struct flowi4 *flp, struct fib_result *res)
{
    struct trie *t = (struct trie *)tb->tb_data;
    struct key_vector *pn;

    rcu_read_lock();
    pn = rcu_dereference(t->trie);  /* trie 루트 접근 */
    while (pn) {
        /* RCU 보호 하에 trie 순회 — 락 없이 수백만 PPS 처리 */
        pn = rcu_dereference(pn->child[idx]);
    }
    rcu_read_unlock();
    return ret;
}

/* 라우팅 엔트리 업데이트 시: */
spin_lock_bh(&trie_lock);         /* writer 동기화 */
rcu_assign_pointer(pn->child[idx], new_node);
spin_unlock_bh(&trie_lock);
call_rcu(&old_node->rcu, fib_free_node); /* GP 후 해제 */

VFS: dentry 캐시 + SLAB_TYPESAFE_BY_RCU

fs/dcache.c의 dentry 캐시는 SLAB_TYPESAFE_BY_RCU를 사용하여, slab 할당자가 GP 없이도 메모리를 재사용할 수 있게 합니다. 대신 reader가 접근한 dentry가 재할당되었을 수 있으므로, seqlock으로 유효성을 재검증합니다.

/* fs/dcache.c — SLAB_TYPESAFE_BY_RCU + seqlock 패턴 */
rcu_read_lock();
retry:
    dentry = rcu_dereference(parent->d_child);
    seq = read_seqcount_begin(&dentry->d_seq);
    /* dentry 필드 접근 */
    name = dentry->d_name;
    if (read_seqcount_retry(&dentry->d_seq, seq))
        goto retry;  /* 재할당 감지 → 재시도 */
rcu_read_unlock();

/* slab 생성 시 TYPESAFE_BY_RCU 플래그 */
dentry_cache = kmem_cache_create("dentry",
    sizeof(struct dentry), 0,
    SLAB_RECLAIM_ACCOUNT | SLAB_PANIC | SLAB_TYPESAFE_BY_RCU,
    NULL);
⚠️

SLAB_TYPESAFE_BY_RCU 주의: 이 플래그는 객체 메모리가 GP 없이 재사용될 수 있음을 의미합니다. 따라서 RCU reader가 접근한 객체가 이미 다른 용도로 재할당되었을 수 있습니다. 반드시 seqlock이나 generation counter로 유효성을 재검증해야 합니다. 단순 RCU 보호와 혼동하면 심각한 UAF 버그가 됩니다.

프로세스: task_struct 순회

/* kernel/fork.c, include/linux/sched/signal.h */

/* RCU 보호 프로세스 리스트 순회 */
rcu_read_lock();
for_each_process(p) {
    /* p는 RCU 보호 하에 안전하게 참조 가능
     * p가 exit하더라도 GP 동안 task_struct가 유지됨 */
    if (p->pid == target_pid) {
        get_task_struct(p);  /* 참조 카운트 증가 (RCU 밖에서 사용하려면) */
        break;
    }
}
rcu_read_unlock();

/* find_task_by_vpid()도 내부적으로 RCU 사용 */
rcu_read_lock();
task = find_task_by_vpid(pid);
if (task)
    get_task_struct(task);
rcu_read_unlock();

메모리: anon_vma 역매핑

/* mm/rmap.c — anon_vma RCU 순회 (간략화) */
/* 물리 페이지 → 가상 주소 역매핑에서 RCU 사용 */
rcu_read_lock();
anon_vma_interval_tree_foreach(avc, &anon_vma->rb_root, start, end) {
    vma = avc->vma;
    /* RCU 보호 하에 VMA 접근 — page reclaim 핫 경로 */
    address = vma_address(page, vma);
    try_to_unmap_one(folio, vma, address, ...);
}
rcu_read_unlock();

/* anon_vma 해제 시 */
call_rcu(&anon_vma->rcu, anon_vma_free); /* GP 후 안전 해제 */

네트워크 디바이스: rx_handler 교체

/* net/core/dev.c — rx_handler 교체 패턴 */
/* 수신 핸들러 등록 (bridge, bonding 등) */
int netdev_rx_handler_register(struct net_device *dev,
    rx_handler_func_t *rx_handler, void *rx_handler_data)
{
    rcu_assign_pointer(dev->rx_handler_data, rx_handler_data);
    rcu_assign_pointer(dev->rx_handler, rx_handler);
    return 0;
}

/* 패킷 수신 경로에서 (softirq) */
rx_handler = rcu_dereference(skb->dev->rx_handler);
if (rx_handler) {
    /* 락 없이 핸들러 호출 — 수백만 PPS에서도 무잠금 */
    ret = rx_handler(&skb);
}

/* 핸들러 해제 시 */
RCU_INIT_POINTER(dev->rx_handler, NULL);
synchronize_net();  /* 네트워크 RCU GP 대기 */
Linux 커널 서브시스템별 RCU 사용 지점 RCU Core API 네트워크 FIB trie, rx_handler netfilter, socket lookup VFS dentry cache, inode mount table, file lookup 프로세스 관리 task_struct 순회 cred, pid namespace 메모리 관리 anon_vma, SLAB RCU page table, mmap 보안 SELinux, cred 블록/스토리지 blk-mq, device map
RCU는 네트워크(라우팅, 소켓), VFS(dentry, inode), 프로세스 관리(task 순회), 메모리 관리(anon_vma) 등 커널 핫 경로 전반에서 lock-free 읽기를 제공합니다.

고급 RCU Trick 모음

앞선 섹션들이 "어떤 자료구조를 어떻게 RCU로 설계할 것인가"에 가까웠다면, 이 섹션은 실제 커널 코드에서 자주 보이는 구현 트릭과 관용 패턴을 모아 둔 것입니다. 여기서 말하는 trick은 편법이 아니라, 성능과 정확성을 동시에 만족시키기 위해 반복적으로 등장하는 미세 설계 기법을 뜻합니다.

Trick핵심 API해결하는 문제
소유권 handoffrcu_pointer_handoff()RCU 보호 포인터를 refcount/락 소유 포인터로 전환
빈 경로 빠른 탈출rcu_access_pointer()대부분 NULL인 fast path에서 불필요한 RCU 진입 회피
재검증 후 사용lookup + lock + validateRCU가 메모리 안전만 보장하고 의미 안전은 별도 보장해야 하는 상황
콜백에서 무거운 정리 이관call_rcu() + workqueueRCU 콜백에서 sleep 불가 제약 회피
대형 가변 객체 회수kvfree_rcu()kvmalloc/vmalloc 기반 큰 객체를 GP 뒤에 안전하게 해제
혼합 컨텍스트 증명rcu_dereference_check()RCU reader와 writer-locked 경로가 같은 헬퍼를 공유할 때 lockdep에 의도 전달
초기화 포인터 publish 구분RCU_INIT_POINTER / rcu_assign_pointer()초기화/teardown과 live publication의 메모리 순서를 구분
publication refrefcount_inc_not_zero(), kfree_rcu()테이블이 보유한 참조와 외부 사용자 참조를 명확히 분리
ℹ️

읽는 방법: 각 trick의 표면 문법보다도, "무슨 invariant를 지키기 위해 이 기법이 필요한가"를 중심으로 보세요. RCU 코드는 API 이름보다도 수명 전이 규칙reader가 볼 수 있는 중간 상태의 범위가 핵심입니다.

Trick 1: rcu_pointer_handoff()로 소유권 전이를 문서화

RCU lookup 뒤에 refcount를 올렸거나 락을 획득해서, 포인터가 더 이상 "RCU에만 의존하는 임시 포인터"가 아니라 "이제는 다른 소유권 규칙 아래 안전한 포인터"가 되는 순간이 있습니다. 이때 rcu_pointer_handoff()를 쓰면 sparse에 의도를 명확히 전달할 수 있습니다.

struct cache_obj {
    u32 key;
    refcount_t refcnt;
    struct hlist_node node;
    struct rcu_head rcu;
};

struct cache_obj *cache_lookup_get(u32 key)
{
    struct cache_obj *obj;

    rcu_read_lock();
    hlist_for_each_entry_rcu(obj, &cache_ht[hash_min(key, CACHE_BITS)], node) {
        if (obj->key != key)
            continue;
        if (!refcount_inc_not_zero(&obj->refcnt))
            continue;
        rcu_read_unlock();
        return rcu_pointer_handoff(obj);
    }
    rcu_read_unlock();
    return NULL;
}

Trick 2: rcu_access_pointer()로 대부분 비어 있는 fast path를 싸게 거르기

훅, 필터, optional callback처럼 대부분의 시간에는 NULL이고 가끔만 설정되는 포인터가 있습니다. 이런 경우 매번 rcu_read_lock()으로 들어가는 비용도 아깝다면, 먼저 rcu_access_pointer()로 값만 보고 빠르게 탈출한 뒤, 실제 역참조가 필요할 때만 정식 RCU read-side에 진입할 수 있습니다.

int egress_maybe_run(struct packet *pkt)
{
    struct egress_prog *prog;
    int ret = 0;

    /* 값만 확인 — 역참조 금지 */
    if (!rcu_access_pointer(pkt->dev->egress_prog))
        return 0;

    rcu_read_lock();
    prog = rcu_dereference(pkt->dev->egress_prog);
    if (prog)
        ret = prog_run(prog, pkt);
    rcu_read_unlock();

    return ret;
}
  1. 첫 번째 체크는 힌트일 뿐입니다. rcu_access_pointer() 결과를 보고 "있다"고 판단해도, 실제로 RCU lock을 잡고 들어갔을 때는 이미 NULL로 바뀌었을 수 있습니다.
  2. 그래서 두 번째 체크가 필요합니다. 위 코드에서 if (prog)가 그 역할입니다.
  3. 반대로 NULL fast reject는 안전합니다. 처음에 NULL을 봤다면 단지 "지금 이 순간 없었다"는 뜻일 뿐이므로 그대로 빠져나와도 의미가 맞습니다.
⚠️

자주 하는 실수: rcu_access_pointer()의 반환값을 바로 역참조하는 것입니다. 이 API는 포인터 값 확인용이지, 데이터를 읽기 위한 API가 아닙니다. 역참조가 시작되는 순간에는 반드시 rcu_dereference()로 넘어가야 합니다.

Trick 3: RCU lookup 후 락을 잡고 의미를 재검증

RCU는 "이 포인터가 가리키는 메모리가 아직 살아 있다"는 사실은 잘 보장하지만, "이 객체가 지금도 논리적으로 유효하다"는 사실까지 대신 증명해 주지는 않습니다. 그래서 lookup을 RCU로 빠르게 하고, 그 뒤 객체 락을 잡거나 참조를 획득한 다음 키, 상태, 리스트 연결 여부를 다시 확인하는 패턴이 매우 흔합니다.

struct peer {
    spinlock_t lock;
    u32 key;
    bool dead;
    refcount_t refcnt;
    struct hlist_node node;
    struct rcu_head rcu;
};

struct peer *peer_lookup_lock(u32 key)
{
    struct peer *peer;

retry:
    peer = peer_lookup_get(key);   /* 패턴 2의 RCU + refcount lookup */
    if (!peer)
        return NULL;

    spin_lock(&peer->lock);
    if (peer->dead || peer->key != key) {
        spin_unlock(&peer->lock);
        peer_put(peer);
        goto retry;
    }

    return peer;   /* lock + refcount 둘 다 확보 */
}

언제 필요한가? 객체가 재활용되거나, 제거 직전에 dead 상태로 바뀌거나, lookup key와 내부 상태가 동기적으로 움직이지 않을 수 있는 구조에서는 거의 항상 필요합니다. 특히 SLAB_TYPESAFE_BY_RCU 계열과 잘 어울립니다.

Trick 4: call_rcu() 콜백은 가볍게, 무거운 정리는 workqueue로 넘기기

call_rcu() 콜백은 sleep할 수 없습니다. 하지만 실제 해제 작업이 fput(), kvfree(), mutex, firmware 정리처럼 잠들 수 있는 연산을 포함할 수 있습니다. 이럴 때 흔히 쓰는 trick이, RCU 콜백에서는 단지 workqueue에 넘기는 역할만 하고 실제 무거운 정리는 worker가 담당하게 하는 것입니다.

struct big_state {
    struct work_struct free_work;
    void *blob;                 /* kvmalloc() 메모리 */
    struct file *trace_file;  /* sleep 가능한 정리 필요 */
    struct rcu_head rcu;
};

static void big_state_free_workfn(struct work_struct *work)
{
    struct big_state *st = container_of(work, struct big_state, free_work);

    if (st->trace_file)
        fput(st->trace_file);  /* sleep 가능 */
    kvfree(st->blob);
    kfree(st);
}

static void big_state_free_rcu(struct rcu_head *rh)
{
    struct big_state *st = container_of(rh, struct big_state, rcu);

    INIT_WORK(&st->free_work, big_state_free_workfn);
    queue_work(system_unbound_wq, &st->free_work);
}

void big_state_retire(struct big_state *old)
{
    call_rcu(&old->rcu, big_state_free_rcu);
}

Trick 5: 큰 가변 길이 객체는 kvfree_rcu()로 정리

비트맵, 필터 테이블, 문자열 사전처럼 크기가 커서 kmalloc()로는 불안정하고 kvmalloc()을 써야 하는 객체가 있습니다. 이런 객체를 RCU로 교체할 때는 kfree_rcu() 대신 kvfree_rcu()가 훨씬 편합니다.

struct big_bitmap {
    u32 nr_bits;
    struct rcu_head rcu;
    unsigned long bits[];
};

static DEFINE_MUTEX(bitmap_lock);
static struct big_bitmap __rcu *g_bitmap;

int bitmap_reload(u32 nr_bits)
{
    struct big_bitmap *new_bm, *old_bm;
    size_t nlongs = BITS_TO_LONGS(nr_bits);

    new_bm = kvmalloc(struct_size(new_bm, bits, nlongs), GFP_KERNEL);
    if (!new_bm)
        return -ENOMEM;

    new_bm->nr_bits = nr_bits;
    bitmap_zero(new_bm->bits, nr_bits);

    mutex_lock(&bitmap_lock);
    old_bm = rcu_replace_pointer(g_bitmap, new_bm,
                   lockdep_is_held(&bitmap_lock));
    mutex_unlock(&bitmap_lock);

    if (old_bm)
        kvfree_rcu(old_bm, rcu);
    return 0;
}

왜 좋은가? 새 객체가 작은 경우 내부적으로 kfree 경로를 타고, 큰 경우 vfree 경로를 타는 차이를 호출자가 직접 신경 쓰지 않아도 됩니다. 대형 테이블을 자주 스왑하는 구성에서는 코드 단순화 효과가 큽니다.

Trick 6: rcu_dereference_check()로 혼합 컨텍스트를 lockdep에 증명

실전 헬퍼 함수 중에는 reader 경로에서는 RCU lock 아래서 호출되고, writer 경로에서는 이미 mutex나 spinlock을 쥔 채로 호출되는 경우가 많습니다. 이런 함수를 위해 rcu_dereference_check()를 쓰면 "이 포인터는 RCU reader이거나 writer lock 아래서만 읽는다"는 사실을 lockdep에 문서화할 수 있습니다.

struct policy_db {
    struct policy_root __rcu *root;
    struct mutex update_lock;
};

static struct policy_root *policy_root_get(struct policy_db *db)
{
    return rcu_dereference_check(db->root,
           lockdep_is_held(&db->update_lock));
}

int policy_lookup(struct policy_db *db, const struct request *req)
{
    struct policy_root *root;

    rcu_read_lock();
    root = policy_root_get(db);
    rcu_read_unlock();
    return root ? match_request(root, req) : -ENOENT;
}

void policy_replace(struct policy_db *db, struct policy_root *new_root)
{
    struct policy_root *old_root;

    mutex_lock(&db->update_lock);
    old_root = policy_root_get(db);   /* writer도 같은 헬퍼 재사용 */
    rcu_assign_pointer(db->root, new_root);
    mutex_unlock(&db->update_lock);

    if (old_root)
        call_rcu(&old_root->rcu, policy_root_free_rcu);
}

이 trick의 장점은 "reader용 getter"와 "writer용 getter"를 따로 만들지 않아도 된다는 점입니다. 대신 전제 조건을 lockdep 조건식으로 명시해, 디버그 빌드에서 잘못된 호출 문맥을 잡아낼 수 있습니다.

Trick 7: RCU_INIT_POINTER와 rcu_assign_pointer()의 역할 분리

둘 다 포인터를 저장하지만 의미는 다릅니다. rcu_assign_pointer()live reader가 존재하는 시스템에서의 publish이고, RCU_INIT_POINTER()아직 reader가 없거나, 다른 동기화로 이미 완전히 배제된 상황에서의 단순 대입에 가깝습니다.

상황권장 API이유
부팅 초기, 아직 외부 reader 없음RCU_INIT_POINTER()불필요한 publish 배리어 생략 가능
운영 중 새 객체를 reader에게 게시rcu_assign_pointer()초기화 완료 후 publish 순서 보장 필요
운영 중 포인터를 NULL로 제거상황 의존reader가 동시에 볼 수 있으면 일반적으로 RCU_INIT_POINTER보다 rcu_assign_pointer가 보수적
writer lock이 reader를 완전히 배제하는 teardownRCU_INIT_POINTER()이미 외부 관찰자가 없음
/* 1. 초기 부팅/생성 경로 — 아직 외부 reader 없음 */
RCU_INIT_POINTER(dev->rx_handler, NULL);

/* 2. 운영 중 새 핸들러 게시 */
new_handler->ctx = ctx;
new_handler->flags = flags;
rcu_assign_pointer(dev->rx_handler, new_handler);

/* 3. 운영 중 해제 — 기존 reader가 있을 수 있으므로 GP 필요 */
rcu_assign_pointer(dev->rx_handler, NULL);
synchronize_rcu();

/* 4. 완전히 고립된 객체 파괴 경로 */
mutex_lock(&dev->destroy_lock);   /* 외부 접근 완전 차단 */
RCU_INIT_POINTER(dev->rx_handler, NULL);
mutex_unlock(&dev->destroy_lock);
⚠️

보수적으로 가도 손해가 적습니다. 문맥이 확실하지 않다면 rcu_assign_pointer()를 쓰는 편이 낫습니다. 반대로 live publication인데 RCU_INIT_POINTER()를 써 버리면 메모리 순서 버그가 숨어들 수 있습니다.

Trick 8: publication ref를 별도 개념으로 두면 teardown이 단순해진다

앞의 여러 예시에서 이미 암묵적으로 썼던 개념이 publication ref입니다. 이는 "테이블이나 리스트가 이 객체를 외부에 게시하고 있으므로, 자료구조 자체가 객체 참조 1개를 보유한다"는 규칙입니다. 이 규칙을 명시하면 teardown과 lookup handoff가 매우 정리됩니다.

struct job {
    refcount_t refcnt;
    bool dead;
    struct hlist_node node;
    struct rcu_head rcu;
};

struct job *job_alloc_publish(u32 key)
{
    struct job *job = kzalloc(sizeof(*job), GFP_KERNEL);
    if (!job)
        return NULL;

    refcount_set(&job->refcnt, 1);   /* publication ref */
    publish_job_to_hash(job, key);
    return job;
}

struct job *job_get_from_lookup(u32 key)
{
    struct job *job;

    rcu_read_lock();
    job = lookup_job_rcu(key);
    if (job && !refcount_inc_not_zero(&job->refcnt))
        job = NULL;
    rcu_read_unlock();
    return job;
}

void job_unpublish(struct job *job)
{
    WRITE_ONCE(job->dead, true);
    remove_job_from_hash_rcu(job);
    job_put(job);   /* publication ref 반납 */
}

void job_put(struct job *job)
{
    if (refcount_dec_and_test(&job->refcnt))
        kfree_rcu(job, rcu);
}

Trick 9: 객체 수에 따라 synchronize_rcu()와 call_rcu()를 전략적으로 섞기

자주 놓치는 부분은 call_rcu()synchronize_rcu()가 서로 배타적인 선택지가 아니라는 점입니다. 실전에서는 retire 수량과 문맥에 따라 둘을 섞어서 쓰는 것이 더 낫습니다.

상황권장이유
객체 1~2개 제거, 호출 경로가 비블로킹이어야 함call_rcu() / kfree_rcu()writer latency 최소화
수천 개 객체 일괄 retire, process contextsynchronize_rcu() 후 일괄 freecallback flood 방지
소수는 즉시, 다수는 배치핫 경로는 call_rcu(), maintenance path는 batch freelatency와 메모리 사용량 균형
void route_gc_run(bool incremental)
{
    if (incremental) {
        /* 핫 경로 근처: 작은 수량만 비동기 retire */
        kfree_rcu(victim, rcu);
        return;
    }

    /* maintenance 스레드: 큰 배치는 한 번에 GP 대기 */
    detach_many_routes_rcu(&retired);
    synchronize_rcu();
    free_retired_routes(&retired);
}

이 trick의 핵심은 API를 고르는 것이 아니라 어떤 호출 문맥에서 어떤 비용을 지불할지 분리하는 것입니다. RCU 자체보다 운영 비용이 병목이 되는 구간에서는 이런 전략적 혼합이 체감 차이를 만듭니다.

Trick 10: 1회성 publish는 cmpxchg_release 계열로 경합을 줄일 수 있다

가끔은 "최초 한 번만 설치되면 되고, 이후에는 거의 바뀌지 않는" singleton 포인터가 있습니다. 이런 경우 매번 별도 writer lock을 잡기보다, 완전히 초기화한 객체를 한 번만 원자적으로 publish하는 방식이 더 단순할 수 있습니다. 이때 쓰는 저수준 trick이 cmpxchg_release 계열입니다.

struct global_prog {
    u32 version;
    struct rcu_head rcu;
};

static struct global_prog __rcu *g_prog;

int prog_install_once(struct global_prog *new_prog)
{
    struct global_prog *old;

    /* new_prog는 여기 오기 전에 완전히 초기화되어 있어야 함 */
    old = cmpxchg_release((struct global_prog **)&g_prog,
                          NULL, new_prog);
    if (!old)
        return 0;        /* 내가 설치 성공 */

    kfree(new_prog);      /* 다른 writer가 먼저 설치함 */
    return -EEXIST;
}

struct global_prog *prog_get(void)
{
    struct global_prog *prog;

    rcu_read_lock();
    prog = rcu_dereference(g_prog);
    rcu_read_unlock();
    return prog;
}
⚠️

저수준 trick: 이 방식은 명확한 불변 조건이 있을 때만 쓰는 편이 좋습니다. "writer가 여럿이지만 최초 설치 한 번만 경쟁한다" 같은 매우 좁은 문제에만 적용하고, 일반적인 운영 중 갱신 경로에는 남용하지 마십시오.

Trick 11: rcu_head_init()와 rcu_head_after_call_rcu()로 이중 retire를 잡기

복잡한 오류 경로나 중복 cleanup 경로가 있는 서브시스템에서는 같은 객체에 대해 call_rcu()가 두 번 들어가는 실수가 생각보다 흔합니다. 이건 보통 늦게 폭발하는 use-after-free나 double-free로 이어집니다. 디버그 빌드에서 이 실수를 일찍 잡아내는 trick이 rcu_head_init()rcu_head_after_call_rcu() 조합입니다.

struct retire_obj {
    bool dead;
    struct rcu_head rcu;
};

static void retire_obj_free_rcu(struct rcu_head *rh)
{
    struct retire_obj *obj = container_of(rh, struct retire_obj, rcu);
    kfree(obj);
}

void retire_obj_init(struct retire_obj *obj)
{
    rcu_head_init(&obj->rcu);
    obj->dead = false;
}

void retire_obj_queue_free(struct retire_obj *obj)
{
    if (READ_ONCE(obj->dead))
        return;

    WRITE_ONCE(obj->dead, true);

    if (rcu_head_after_call_rcu(&obj->rcu, retire_obj_free_rcu))
        return;   /* 이미 retire 등록됨 */

    call_rcu(&obj->rcu, retire_obj_free_rcu);
}

언제 특히 유용한가? 에러 처리, timeout 처리, normal cleanup이 서로 다른 경로에서 같은 객체를 정리할 수 있는 드라이버나 네트워크 상태 머신에서 유용합니다. 운영 빌드에서 완전한 동기화 수단을 대신하지는 않지만, 디버그 단계에서 문제를 빨리 드러내 줍니다.

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