같은 값을 읽게 하는 기능인가요?
두 CPU가 서로 다른 시점에 읽었다면 서로 다른 버전을 볼 수 있습니다. RCU는 모든 reader가 한 번에 같은 값으로 바뀌도록 맞추는 장치가 아닙니다. 여기서는 공유 포인터 하나가 가리키는 객체를 새 객체로 교체하는 예를 다룹니다. 핵심은 예전 객체를 이미 읽고 있던 코드가 끝나기 전에 그 메모리를 해제하지 않는 것입니다.
갱신하는 코드끼리의 충돌은 별도로 막아야 합니다. writer가 하나뿐이라는 조건을 두거나 mutex 등으로 갱신 순서를 정합니다. reader가 RCU 읽기 구간에 들어갔다고 writer의 포인터 교체가 금지되는 것은 아닙니다. RCU 사용 시 갱신·수명 조건
공유 포인터와 CPU의 지역 포인터를 구분합니다
1. CPU 0이 A를 읽기 시작합니다
| 관찰 위치 | 값 또는 상태 |
|---|---|
| 공유 포인터 current_policy | 객체 A의 주소 |
| CPU 0의 지역 변수 p | 객체 A의 주소를 읽어 보관합니다. |
| 객체 A | threshold = 10. 아직 해제하면 안 됩니다. |
| 객체 B | 아직 다른 CPU에 공개하지 않았습니다. |
CPU 0은 rcu_read_lock()과 rcu_read_unlock() 사이에 있습니다. 지역 포인터 p가 따로 존재한다는 점이 중요합니다.
2. writer가 B를 공개합니다
| 관찰 위치 | 값 또는 상태 |
|---|---|
| 공유 포인터 current_policy | 객체 B의 주소로 교체됩니다. |
| CPU 0의 지역 변수 p | 여전히 객체 A의 주소입니다. |
| 객체 A | CPU 0이 계속 읽을 수 있도록 유지합니다. |
| 객체 B | threshold = 20으로 초기화한 뒤 공개합니다. |
이후 B를 취득한 reader는 B를 사용합니다. 공유 포인터를 바꾼다고 CPU 0의 지역 변수 p까지 B로 덮어쓰지는 않습니다.
3. 예전 읽기 구간의 종료를 기다립니다
writer가 synchronize_rcu()로 필요한 grace period가 지나기를 기다립니다. CPU 0이 읽기 구간을 마칠 때까지 A를 유지합니다. 이것은 B를 공개하기 위한 대기가 아니라 A를 회수하기 위한 대기입니다.
4. A를 해제할 수 있습니다
이 예처럼 모든 reader가 RCU 규칙을 지키고 별도 참조가 남지 않는다면, 대기 후 A를 해제합니다. B를 읽는 새 reader들이 전부 사라져야 하는 것은 아닙니다.
단계 이동은 시간 순서입니다. A와 B는 서로 다른 메모리 객체이며, 표의 주소는 개념적 이름입니다. 포인터 교체는 객체 내용이나 다른 CPU의 지역 변수를 자동으로 복사하지 않습니다.
grace period는 기존 읽기 구간이 끝났음을 확인하는 기간입니다. 임의로 몇 ms를 쉬는 것과 다릅니다. synchronize_rcu()가 기다리는 것은 호출 전에 진행 중이던 읽기 구간이며, 이후 시작한 모든 reader의 종료까지 반드시 기다리는 것은 아닙니다. RCU의 공개·제거·회수 순서
읽는 코드가 보호받는 범위
다음은 설명용 예제입니다. 객체는 공개한 뒤 필드를 바꾸지 않고, 교체와 해제는 아래 writer 경로에서만 수행한다고 가정합니다. 값 10과 20은 실제 커널 설정값이 아닙니다.
struct policy {
int threshold;
};
static struct policy __rcu *current_policy;
static DEFINE_MUTEX(policy_lock);
int read_threshold(void)
{
struct policy *p;
int value = -1;
rcu_read_lock();
p = rcu_dereference(current_policy);
if (p)
value = p->threshold;
rcu_read_unlock();
return value;
}| 코드 | 무엇이 바뀝니까? |
|---|---|
| __rcu | RCU로 다루는 포인터라는 주석 성격의 표시입니다. static analysis에 쓰이며 객체를 자동 할당하거나 참조 횟수를 늘리지 않습니다. |
| rcu_read_lock() | 읽기 구간에 들어갑니다. 다른 reader와 writer를 mutex처럼 모두 잠그지 않습니다. |
| p = rcu_dereference(current_policy) | RCU 규칙에 맞게 공유 포인터를 취득합니다. p에는 객체의 주소가 들어가며 객체 전체를 복사하지 않습니다. |
| value = p->threshold | 보호받는 구간 안에서 정수 필드를 지역 변수에 복사합니다. |
| rcu_read_unlock() | 이 읽기 구간을 끝냅니다. 별도 수명 보장이 없다면 이후 p로 객체에 접근하면 안 됩니다. |
| return value | 객체 주소 대신 이미 복사한 정수를 돌려줍니다. |
rcu_dereference()는 최신 버전을 반드시 받게 하거나 참조 횟수를 자동 증가시키는 함수가 아닙니다. 읽기 구간 밖에서도 객체를 써야 한다면 그 자료구조가 정한 안전한 참조 취득 절차가 추가로 필요합니다. RCU 포인터·읽기 구간 API
초기화 → 공개 → 대기 → 해제를 읽습니다
int replace_threshold(int threshold)
{
struct policy *next, *old;
next = kmalloc(sizeof(*next), GFP_KERNEL);
if (!next)
return -ENOMEM;
next->threshold = threshold;
mutex_lock(&policy_lock);
old = rcu_dereference_protected(current_policy,
lockdep_is_held(&policy_lock));
rcu_assign_pointer(current_policy, next);
mutex_unlock(&policy_lock);
synchronize_rcu();
kfree(old);
return 0;
}| 순서 | 값과 수명 |
|---|---|
| kmalloc과 실패 검사 | 새 객체의 주소를 next에 받습니다. 실패하면 기존 공유 포인터를 바꾸지 않고 -ENOMEM을 돌려줍니다. |
| next->threshold = threshold | 아직 공개하지 않은 객체의 필드를 초기화합니다. |
| mutex_lock과 old 취득 | writer끼리 교체 순서를 정하고 교체 전 객체 주소를 old에 보관합니다. lockdep_is_held는 잠금 조건을 검사하는 표현이며 잠금을 대신 잡지 않습니다. |
| rcu_assign_pointer | 초기화한 객체를 reader에게 공개할 때 필요한 순서를 지킵니다. 이 줄에서 공유 포인터를 바꿉니다. |
| mutex_unlock | 다른 writer가 갱신할 수 있게 합니다. 여기까지 와도 old의 reader가 남을 수 있습니다. |
| synchronize_rcu | 잠들 수 있는 문맥에서 기존 읽기 구간의 종료를 기다립니다. 새 값을 공개하는 함수가 아닙니다. |
| kfree(old) | 더 이상 old를 읽는 코드가 없다는 조건을 갖춘 뒤 회수합니다. 첫 설치에서 old가 NULL이면 kfree(NULL)은 아무것도 하지 않습니다. |
이 코드는 include, 초기 사용자 연결, 모듈 종료 처리를 생략한 예제입니다. 모든 writer가 policy_lock을 사용하고 모든 reader가 위 규칙을 지켜야 합니다. 해제한 old를 다른 곳에서 따로 보관하는 구조에는 그대로 적용할 수 없습니다. 포인터 공개와 synchronize_rcu API
call_rcu와 SRCU는 어느 부분이 다릅니까?
| API·상황 | 의미 |
|---|---|
| synchronize_rcu() | 호출한 쪽이 grace period 완료를 기다립니다. 기다릴 수 있는 문맥이어야 합니다. |
| call_rcu() | grace period 뒤에 실행할 callback을 등록합니다. 반환이 callback 실행 완료를 뜻하지 않습니다. callback에서 임의로 잠들면 안 됩니다. |
| 일반 RCU 읽기 구간 | 명시적인 sleep·blocking을 자유롭게 넣을 수 없습니다. 선점 가능 여부와 잠들 수 있는 API를 호출해도 되는지는 다른 조건입니다. |
| SRCU | 잠들 수 있는 읽기 구간이 필요한 경우의 별도 계열입니다. 같은 srcu_struct와 맞는 읽기·대기 API를 사용해야 합니다. |
SRCU도 읽기 구간 안에서 자기 종료를 기다리는 synchronize_srcu()를 호출해도 된다는 뜻은 아닙니다. srcu_read_lock()이 반환한 값을 맞는 srcu_read_unlock()에 그대로 넘겨야 하며, 일반 RCU와 SRCU의 대기를 섞어 객체 수명을 보장했다고 판단하면 안 됩니다. SRCU domain과 blocking 조건 SRCU 읽기 API
RCU stall은 어디서 진행이 막혔다는 뜻인가요?
stall 경고는 grace period 진행이 예상보다 늦다는 단서입니다. RCU 구현 자체가 잘못됐다는 판정은 아닙니다. interrupt를 오래 막은 루프, 실행 기회를 얻지 못한 RCU thread, 긴 handler, 과도한 콘솔 출력, timer 문제도 조사 대상입니다.
| 로그에서 찾는 부분 | 다음 확인 |
|---|---|
| stalled CPU 또는 task | 반복 출력의 stack에서 같은 위치에 머무는 코드를 찾습니다. |
| detected by CPU 번호 | 경고를 발견한 CPU입니다. 문제가 난 CPU 번호와 같다고 단정하지 않습니다. |
| All QSes seen / kthread starved | reader 쪽 상태만 보지 말고 grace-period thread가 실제로 실행되는지 봅니다. |
| ticks·softirq 값이 진행하지 않음 | timer·interrupt·실행 문맥과 긴 비선점 구간을 확인합니다. |
경고를 끄거나 timeout만 늘리면 메시지는 줄어들 수 있지만 막힌 실행이 복구됐다는 근거는 되지 않습니다. 커널 버전과 설정, 최초 경고, 반복 stack, 직전에 바꾼 driver를 함께 남기고 관찰 도구 자체의 부하도 고려합니다. RCU CPU stall 원인과 로그 해석