RCU 심화 (Read-Copy-Update)
Linux 커널 RCU(Read-Copy-Update) 메커니즘, grace period, SRCU, 사용 패턴 심층 분석.
RCU 개요
RCU(Read-Copy-Update)는 읽기 작업이 대부분인 자료구조에 최적화된 동기화 메커니즘입니다. reader는 잠금 없이 자료구조에 접근하고, writer는 데이터의 복사본을 수정한 뒤 포인터를 원자적으로 교체합니다. 이전 데이터는 모든 reader가 종료한 후(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_RCU | RCU 잠금 규칙 위반 감지 (lockdep 통합) |
CONFIG_RCU_TRACE | RCU 이벤트 tracepoint 활성화 |
CONFIG_RCU_CPU_STALL_TIMEOUT | RCU CPU stall 감지 타임아웃 (기본 21초) |
CONFIG_RCU_BOOST | RCU reader 우선순위 부스팅 (RT 커널) |
CONFIG_RCU_CPU_STALL_CPUTIME | stall 시 CPU 시간 통계 함께 출력 |
CONFIG_RCU_EXP_KTHREAD | expedited 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 2 | stall 중인 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=14979 | 14979 | force-quiescent-state 스캔 횟수. 높을수록 오래 기다린 것 |
detected by 0 | CPU 0 | stall을 감지(보고)한 CPU 번호 |
t=21003 | 21003 | grace period 시작 후 경과한 jiffies (≈21초) |
g=300921 | 300921 | stall 중인 grace period 번호 (gp_seq) |
q=24 | 24 | stall 감지 시점까지 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 backtrace | stall 시점의 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 starved | ps -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();
}
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_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=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() 누락 등입니다.
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 */
/sys/kernel/debug/rcu/rcu_preempt/rcudata에서 어떤 CPU가 quiescent state를 보고하지 않는지 확인합니다. (2) /proc/<pid>/stack으로 해당 CPU에서 실행 중인 태스크의 콜 스택을 확인합니다. (3) ftrace의 irqsoff 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);
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_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);
srcu_struct 인스턴스 사용. (4) SRCU 사용 시 하나의 srcu_struct를 여러 무관한 용도로 공유하면 grace period가 불필요하게 길어지므로, 용도별로 분리하십시오.