Locking / RCU · Linux 6.18.37 LTS · source + diagram note

RCU init

최신 LTS 기준: kernel.org longterm 6.18.37 기준으로 파일 위치와 링크를 맞춘 원본 코드 분석 노트입니다. rcu_init, grace period, callback.

전체 흐름

락, RCU, 원자 연산은 여러 실행 흐름이 함께 사용하는 상태의 접근 순서와 객체 수명을 관리한다. lock API를 외우는 것이 아니라 context, memory model, object lifetime을 하나의 그래프로 읽어야 한다.

핵심 확인 사항

함수의 입력과 처리 결과를 먼저 확인한다. 구조체를 생성하고 연결하는 시점, 다른 코드에서 사용할 수 있게 하는 시점, 더 이상 사용하지 않아 회수하는 시점을 구분한다.

오류 처리 확인

정상 처리와 오류 처리를 함께 확인한다. 오류 복구, 장치의 실행 중 연결·분리(hotplug), 자원 해제 과정에서도 상태가 올바르게 정리되는지 점검한다.

그림 1. RCU init 이 kernel 안에서 놓이는 위치
shared objectpointer, state, refcount
→
serializationspinlock, mutex, rwsem, atomic
→
visibilitybarrier, READ_ONCE/WRITE_ONCE
→
lifetimerefcount, RCU grace period, free

왼쪽부터 처리 단계를 나열했다. 각 코드 조각에서 바뀌는 상태를 해당 단계와 대조한다.

그림으로 먼저 보기: 제거와 회수 사이의 시간

RCU에서 공유 포인터를 바꾸는 순간과 옛 객체를 해제하는 순간은 다릅니다. reader A가 이미 얻은 old를 읽고 있다면, updater가 새 객체를 공개해도 A의 참조는 즉시 사라지지 않습니다. 그림에서는 옛 객체를 새로 얻는 경로를 제거한 뒤 call_rcu()를 호출하고, 필요한 grace period가 지난 뒤 callback에서 옛 객체를 회수합니다.

가로로 이동해 그림을 읽을 수 있습니다. 도해 A · reader와 callback의 시간 관계 크게 열기 ↗

기존 reader A는 old를 읽고, 포인터 교체 후 시작한 reader B는 새 객체를 읽는다. A 종료를 포함하는 grace period 이후 callback이 old를 해제하며 B는 계속 실행 중일 수 있다.
도해 A · reader와 callback의 시간 관계. t0–t4는 순서를 보여 주는 기호입니다. updater 상자는 t1의 이벤트이며 폭은 실행 시간이 아닙니다. GP 시작과 callback 실행은 지연될 수 있으며, 마지막 reader가 끝나는 바로 그 순간을 GP 완료나 callback 실행 시점으로 단정하지 않습니다.

소스 근거: tree.c:3188–3245; tree.c:2524–2620. DUJINLABS 자체 제작 개념도.

서로 다른 세 시점을 구분하기
시점보장 / 상태이 시점에 하면 안 되는 일
포인터 교체새로 진입한 reader가 old를 얻는 경로를 제거한다. 기존 reader는 old를 계속 사용할 수 있다.old를 곧바로 해제
call_rcu() 반환callback 등록을 마친다. 해당 callback이 필요한 GP를 기다린다.callback 완료로 간주
callback 진입등록 전에 진행 중이던 RCU read-side critical section들이 완료된 뒤 실행한다.더 늦게 시작한 모든 reader까지 끝났다고 단정

이 예시는 적절한 RCU 게시·읽기 API를 사용하고 updater끼리의 충돌은 별도로 직렬화한 단일 포인터 교체입니다. reader가 임계구역 밖으로 포인터를 가져가려면 refcount 등 별도 수명 보장이 필요합니다. synchronize_rcu()는 호출자가 GP를 기다리는 방식이고, call_rcu()는 callback을 등록하는 방식입니다. 둘 모두 reader와 updater의 임의 쓰기 충돌을 자동으로 해결하는 범용 락은 아닙니다.

소스 코드 위치

먼저 확인할 Linux 6.18.37 LTS 소스 파일

원본 코드:
Linux 6.18.37 LTS: kernel/rcu/tree.c
Linux 6.18.37 LTS: kernel/rcu/update.c
Linux 6.18.37 LTS: include/linux/rcupdate.h

설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.

대표 코드

void __init rcu_init

원본 코드: kernel/rcu/tree.c:4866-4918 · v6.18.37 · 로컬 원문
void __init rcu_init(void)
{
	int cpu = smp_processor_id();

	rcu_early_boot_tests();

	rcu_bootup_announce();
	sanitize_kthread_prio();
	rcu_init_geometry();
	rcu_init_one();
	if (dump_tree)
		rcu_dump_rcu_node_tree();
	if (use_softirq)
		open_softirq(RCU_SOFTIRQ, rcu_core_si);

	/*
	 * We don't need protection against CPU-hotplug here because
	 * this is called early in boot, before either interrupts
	 * or the scheduler are operational.
	 */
	pm_notifier(rcu_pm_notify, 0);
	WARN_ON(num_online_cpus() > 1); // Only one CPU this early in boot.
	rcutree_prepare_cpu(cpu);
	rcutree_report_cpu_starting(cpu);
	rcutree_online_cpu(cpu);

	/* Create workqueue for Tree SRCU and for expedited GPs. */
	rcu_gp_wq = alloc_workqueue("rcu_gp", WQ_MEM_RECLAIM | WQ_PERCPU, 0);
	WARN_ON(!rcu_gp_wq);

	sync_wq = alloc_workqueue("sync_wq", WQ_MEM_RECLAIM | WQ_UNBOUND, 0);
	WARN_ON(!sync_wq);

	/* Respect if explicitly disabled via a boot parameter. */
	if (rcu_normal_wake_from_gp < 0) {
		if (num_possible_cpus() <= WAKE_FROM_GP_CPU_THRESHOLD)
			rcu_normal_wake_from_gp = 1;
	}

	/* Fill in default value for rcutree.qovld boot parameter. */
	/* -After- the rcu_node ->lock fields are initialized! */
	if (qovld < 0)
		qovld_calc = DEFAULT_RCU_QOVLD_MULT * qhimark;
	else
		qovld_calc = qovld;

	// Kick-start in case any polled grace periods started early.
	(void)start_poll_synchronize_rcu_expedited();

	rcu_test_sync_prims();

	tasks_cblist_init_generic();
}

확인 사항: 이 코드에서는 반환값보다 상태 변경을 먼저 확인한다. 어느 잠금을 획득한 뒤 어떤 필드를 바꾸는지, 실패 시 어느 레이블로 분기하는지, 변경된 상태를 다음 호출자가 어떤 전제로 사용하는지 추적한다.

call_rcu

원본 코드: kernel/rcu/tree.c:3241-3244 · v6.18.37 · 로컬 원문
void call_rcu(struct rcu_head *head, rcu_callback_t func)
{
	__call_rcu_common(head, func, enable_rcu_lazy);
}

call_rcu()는 callback을 즉시 실행하거나 현재 호출자를 grace period 끝까지 재우는 함수가 아니다. head와 func를 __call_rcu_common()에 전달해 지연 실행을 등록한다. callback 안에서 무엇을 해제할지는 사용자가 정하며, 이미 공개한 객체는 먼저 독자가 새로 찾지 못하도록 분리해야 한다.

함수별 분석

read-side를 가볍게 유지하면서 update/free를 grace period 뒤로 미루는 RCU core state를 초기화한다.

원본 코드를 함수별로 나누어 살펴본다. 각 함수가 준비하는 값과 다음 함수가 그 값을 사용하는 방식을 확인한다.

그림 2. RCU init 함수 체인과 관찰 지점
entrycaller가 넘기는 객체와 context를 확정
→
void __init rcu_initRCU structures 준비
→
call_rcureader tracking 시작
→
observable statetracepoint, counter, sysfs/proc에서 확인되는 결과

각 단계의 입력과 처리 결과를 연결해 본다. 다음 단계로 넘어가기 전에 바뀌는 필드를 코드에서 확인한다.

1. void __init rcu_init

void __init rcu_init 주변에서는 rcu_state를 중심으로 본다. 이 필드는 global GP state 역할을 하므로, 함수가 끝날 때 boot init 상태가 실제로 성립했는지 확인해야 한다.

원본 코드에서 입력 범위 검사, 주요 필드 변경, 다른 계층에서 사용할 수 있게 하는 시점, 실패 시 복구 순서를 확인한다. 이 조건을 만족해야 다음 함수가 RCU structures 준비을 전제로 삼을 수 있다.

자주 틀리는 해석: RCU read lock을 mutual exclusion으로 오해함

2. call_rcu

call_rcu 주변에서는 rcu_data를 중심으로 본다. 이 필드는 CPU별 quiescent accounting 역할을 하므로, 함수가 끝날 때 read-side enters 상태가 실제로 성립했는지 확인해야 한다.

원본 코드에서 입력 범위 검사, 주요 필드 변경, 다른 계층에서 사용할 수 있게 하는 시점, 실패 시 복구 순서를 확인한다. 이 조건을 만족해야 다음 함수가 reader tracking 시작을 전제로 삼을 수 있다.

자주 틀리는 해석: pointer publish에 rcu_assign_pointer를 빼먹음

함수입력상태 변경확인할 사항
void __init rcu_initrcu_state, 호출 시 실행 문맥, 하위 시스템의 준비 조건boot init: RCU structures 준비RCU read lock을 mutual exclusion으로 오해함 문제를 코드상 어느 조건문 또는 error label에서 분리하는가
call_rcurcu_data, 호출 시 실행 문맥, 하위 시스템의 준비 조건read-side enters: reader tracking 시작pointer publish에 rcu_assign_pointer를 빼먹음 문제를 코드상 어느 조건문 또는 error label에서 분리하는가

구조체와 필드

구조체의 필드를 언제 바꿀 수 있고 다른 코드가 언제 읽을 수 있는지 확인한다. 필드의 의미와 함께 잠금 등 접근을 보호하는 규칙을 살펴본다.

그림 3. RCU init 핵심 필드 연결
rcu_stateglobal GP state
→
rcu_dataCPU별 quiescent accounting
→
rcu_nodetree hierarchy
→
rcu_headcallback anchor

각 단계에서 읽고 쓰는 필드를 구조체 정의와 대조한다.

어떤 코드가 필드를 변경하고 어떤 방식으로 접근을 보호하는지, 객체를 언제 해제하는지 원본 코드에서 확인한다.

필드읽는 법
rcu_stateglobal GP state
rcu_dataCPU별 quiescent accounting
rcu_nodetree hierarchy
rcu_headcallback anchor

초기화 코드를 자료구조에 연결하기

rcu_init()은 부팅 초기에 tree geometry와 node 상태, 부팅 CPU의 RCU 상태를 준비합니다. 아래 그림은 초기화 뒤 사용할 관계를 보여 줍니다. rcu_init_one()은 node의 lock·gp_seq·parent 등을 초기화하고, possible CPU마다 rcu_data.mynode를 담당 leaf에 연결합니다. 가능한 모든 CPU를 그 자리에서 online으로 만드는 것은 아닙니다.

가로로 이동해 그림을 읽을 수 있습니다. 도해 B · Tree RCU 객체 관계 크게 열기 ↗

전역 rcu_state의 root와 leaf rcu_node 아래에 CPU별 rcu_data가 있다. mynode가 담당 leaf를 가리키며 callback은 각 cblist에 저장된다.
도해 B · Tree RCU 객체 관계. GP 진행을 모으는 계층과 callback 대기열의 역할을 구분합니다. 네 CPU·두 leaf는 관계를 펼친 설명용 배치이며 실제 작은 시스템의 tree 깊이나 fanout을 재현한 것이 아닙니다. PREEMPT_RCU의 선점된 reader는 blkd_tasks 등으로 별도 추적합니다.

소스 근거: tree.c:4642–4734; tree.c:4240–4265; tree.c:4866–4918. DUJINLABS 자체 제작 개념도.

초기화 함수에서 그림의 어느 부분이 준비되는가
코드 경계준비하는 관계소스에서 확인할 점
rcu_init_geometry()CPU 수와 설정에 따른 tree geometry고정된 2단 tree라고 가정하지 않는다.
rcu_init_one()rcu_node 계층, parent, per-CPU mynode 연결qsmask·sequence·lock·blocked-reader list 초기화
rcutree_prepare_cpu() 등현재 부팅 CPU의 RCU 준비와 online 보고초기 rcu_init()은 online CPU가 하나인 시점
이후 GP / callback 처리GP 완료 관찰 → ready callback 추출 → 실행rcu_do_batch()는 ready callback만 처리하며 batch 제한이 있다.

node의 qsmask는 quiescent-state 보고를 추적하고, CPU별 cblist는 callback을 GP 진행에 맞춰 구분합니다. 이 둘을 하나의 “작업 큐”로 그리면 GP가 끝났다는 사실과 callback을 실제 실행했다는 사실이 섞입니다. callback offload 설정 등은 실행 문맥을 바꿀 수 있으므로, 그림의 CPU별 소속을 “언제나 그 CPU의 softirq에서 실행”이라는 뜻으로 읽지 않습니다.

실행 단계와 상태 변화

실행 단계를 따로 정리하면 정상 경로와 실패 경로를 나란히 비교할 수 있다. 커널 문제는 최종 결과보다 준비가 덜 된 중간 상태를 다른 코드에 공개한 뒤, 실패 시 제대로 정리하지 못할 때 자주 발생한다.

그림 4. RCU init 실행 단계
boot initRCU structures 준비
→
read-side entersreader tracking 시작
→
callback queuedfree 지연
→
grace period endsold reader 없음 보장

화살표는 정상 진행 방향을 나타낸다. 중간 단계에서 실패하면 각 단계의 오류 처리 또는 대체 경로로 이동한다.

상태의미진입 조건깨지는 지점
boot initRCU structures 준비앞 단계의 준비와 오류 처리가 끝난 뒤다음 단계가 준비되지 않은 상태를 사용하거나 필요한 정리를 늦출 때
read-side entersreader tracking 시작앞 단계의 준비와 오류 처리가 끝난 뒤다음 단계가 준비되지 않은 상태를 사용하거나 필요한 정리를 늦출 때
callback queuedfree 지연앞 단계의 준비와 오류 처리가 끝난 뒤다음 단계가 준비되지 않은 상태를 사용하거나 필요한 정리를 늦출 때
grace period endsold reader 없음 보장앞 단계의 준비와 오류 처리가 끝난 뒤다음 단계가 준비되지 않은 상태를 사용하거나 필요한 정리를 늦출 때
RCU init 검토 기준 = 상태 변화 + 소유권 변화 + 외부 공개 시점

불변 조건과 실패 사례

lifetime

RCU init의 핵심 객체가 외부에 공개된 뒤에는 마지막 참조가 사라질 때까지 해제 경로가 callback, timer, IRQ, worker와 경합하지 않아야 한다.

ordering

다른 CPU나 하위 계층이 변경된 필드를 읽는다면 잠금과 메모리 배리어가 접근 순서를 어떻게 보장하는지 확인한다. 참조 횟수와 RCU로 관리하는 객체 수명도 별도로 확인한다.

오류 복구

중간 단계에서 실패하면 이미 확보한 자원을 의존 관계에 맞춰 해제해야 한다. goto 레이블이 많다면 각 분기에 도달했을 때 확보한 자원을 표로 정리한다.

bring-up symptom

임베디드 보드를 처음 구동할 때는 패닉 대신 로그 중단, 시간 초과, 장치 초기화 지연, 과도한 인터럽트 같은 증상으로 문제가 드러날 수 있다.

주의

RCU read lock을 mutual exclusion으로 오해함

주의

pointer publish에 rcu_assign_pointer를 빼먹음

주의

callback 실행 시점을 즉시 free로 봄

계측과 검증

로그를 무작정 추가하지 말고 확인할 가설을 먼저 세운다. 상태표에서 처리가 멈춘 단계를 찾은 뒤, 상태를 바꾸는 함수와 그 결과를 사용하는 함수를 함께 살펴본다.

로그가 출력되었다고 처리가 끝난 것으로 판단하지 않는다. 원본 코드에서 로그 앞뒤의 필드 변경을 확인한다.

도구보는 것
rcu:* tracepointsRCU init 실행이 어느 단계에서 멈추는지 확인
rcutorture conceptsRCU init 실행이 어느 단계에서 멈추는지 확인
rcu_cpu_stallRCU init 실행이 어느 단계에서 멈추는지 확인
ftrace:call_rcuRCU init 실행이 어느 단계에서 멈추는지 확인
기본 추적 경로:
# 예시: tracefs가 켜진 보드에서 토픽별 event를 좁혀 본다.
mount -t tracefs nodev /sys/kernel/tracing
echo function_graph > /sys/kernel/tracing/current_tracer
echo ':mod:*' > /sys/kernel/tracing/set_ftrace_filter
cat /sys/kernel/tracing/trace_pipe

추가 확인 사항

  • RCU init 의 state machine을 네 단계로 줄였을 때, 실제 코드에서 빠지는 intermediate state는 무엇인가?
  • RCU init 의 fast path가 생략한 검사는 어느 init path 또는 slow path에서 보증되는가?
  • 실험으로 확인한다면 'RCU read lock을 mutual exclusion으로 오해함' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?
  • 실험으로 확인한다면 'pointer publish에 rcu_assign_pointer를 빼먹음' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?