RCU 심화 (Read-Copy-Update)

Linux 커널 RCU(Read-Copy-Update) 메커니즘, grace period, SRCU, 사용 패턴 심층 분석.

관련 표준: LKMM (Linux Kernel Memory Model), C11 Memory Model — RCU의 읽기 측 동기화와 메모리 순서 보장은 이 메모리 모델 규격에 기반합니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

RCU 개요

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

RCU Update 과정 1. 원본 읽기 ptr old data 2. 복사 후 수정 ptr old data new data 3. 포인터 교체 ptr old data new data Grace Period: 모든 기존 reader 종료 대기 → old data 해제 (kfree_rcu)
RCU의 Read-Copy-Update 과정: 포인터 교체 후 grace period 동안 이전 데이터 유지

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 후 자동 해제 */

Grace Period

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

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

RCU 보호 연결 리스트

/* RCU-protected linked list traversal */
rcu_read_lock();
list_for_each_entry_rcu(entry, &my_list, list) {
    /* safely read entry fields */
    process(entry);
}
rcu_read_unlock();

/* Adding to RCU list */
spin_lock(&list_lock);
list_add_rcu(&new->list, &my_list);
spin_unlock(&list_lock);

/* Removing from RCU list */
spin_lock(&list_lock);
list_del_rcu(&old->list);
spin_unlock(&list_lock);
kfree_rcu(old, rcu_head);

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);

RCU 변형 비교

변형Reader 슬립용도
RCU (Classic)불가일반적인 커널 자료구조
SRCU가능슬립이 필요한 reader
RCU-bh불가softirq 컨텍스트 (네트워크)
RCU-sched불가선점 불가 구간
Tasks RCU가능trampoline, BPF
💡

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

RCU 내부 구현

Tree RCU

대규모 SMP 시스템에서 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);
}

Quiescent State

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

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 필드명 지정 */
kfree_rcu_mightsleep(old);   /* rcu_head 없이도 가능 (5.x+) */

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

/* 4. rcu_barrier: 모든 call_rcu 콜백 완료 대기 */
rcu_barrier();  /* 모듈 언로드 시 필수 */

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

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);

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

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 환경에서 이를 누락하면 모듈 언로드 시 100% 재현되는 커널 크래시가 발생합니다.

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가 불필요하게 길어지므로, 용도별로 분리하십시오.