본문으로 이동
  • 현재 모든 문서는 작성 중입니다. 보시는 데 불편함이 있더라도 양해 부탁드립니다.

운영체제 기본/Linux/RCU/포인터 공개와 읽기 구간

~ldmsys
관리자 (토론 | 기여)님의 2026년 11월 1일 (일) 01:12 판 (Linux v6.12 RCU 읽기·갱신·유예 기간 각론 작성)
(차이) ← 이전 판 | 최신판 (차이) | 다음 판 → (차이)

RCU를 적용하려면 먼저 두 문제를 나눠야 한다. 독자가 새 객체의 주소를 얻었을 때 초기화된 내용을 볼 수 있어야 하고, 이미 얻은 객체는 읽기가 끝날 때까지 살아 있어야 한다. 포인터 공개와 읽기 구간은 이 두 조건을 함께 구성한다.

읽기 중심 자료구조

설정 객체 하나를 여러 CPU에서 자주 읽고, 설정 변경은 드물게 수행한다고 하자. 뮤텍스로 읽기와 쓰기를 모두 감싸면 올바르게 만들 수 있지만, 모든 읽기가 같은 락의 상태를 갱신한다. RCU에서는 독자가 갱신자용 뮤텍스를 획득하지 않는다. 갱신자는 새 객체를 만든 뒤 공유 포인터를 바꾸고, 이전 객체의 회수는 미룬다.

공개 전                       공개 후
current_config ──> 객체 A     current_config ──> 객체 B
                   값 10                       값 20
                               기존 독자 ──> 객체 A
                                             값 10

기존 독자가 A를 보아도 되는 자료구조라는 전제가 중요하다. 모든 독자가 즉시 최신 값을 보아야 하거나 여러 객체의 변경을 한 번에 관찰해야 한다면, 그 조건을 만족시키는 별도 설계가 필요하다. 이 예제는 공개한 객체의 필드를 이후에 수정하지 않고 새 객체로 교체한다.

객체 초기화와 포인터 공개

다음 자료구조에서 current_config가 RCU로 보호되는 공유 포인터이고, update_lock은 갱신자끼리 경쟁하지 않게 하는 뮤텍스다. struct rcu_head는 나중에 회수 콜백을 등록할 때 사용한다.

struct config {
    int limit;
    struct rcu_head rcu;
};

static struct config __rcu *current_config;
static DEFINE_MUTEX(update_lock);

갱신자는 새 객체의 필드를 먼저 채운 뒤 rcu_assign_pointer(current_config, new)로 주소를 공개한다. 단순히 C 포인터를 대입하는 것으로 끝내지 않는 이유는 컴파일러와 CPU의 메모리 접근 순서까지 맞춰야 하기 때문이다. v6.12의 rcu_assign_pointer()는 일반적인 비NULL 포인터 공개에서 smp_store_release()를 사용한다. 초기화가 공개보다 뒤로 관찰되는 것을 막는 공개 연산이다.[1]

독자는 rcu_dereference()로 공유 포인터를 가져와 그 포인터를 통해 필드를 읽는다. 이 연산은 컴파일러의 부적절한 재읽기·재배치와 필요한 메모리 순서를 다룬다. 단순한 포인터 읽기, 락 획득, 무조건적인 acquire 장벽을 모두 같은 것으로 보면 안 된다. RCU 포인터를 통한 주소 의존성을 보존하는 것이 이 API 사용 규칙의 일부다.[2]

읽기 구간과 객체 수명

int read_limit(void)
{
    struct config *p;
    int value = 0;

    rcu_read_lock();
    p = rcu_dereference(current_config);
    if (p != NULL)
        value = p->limit;
    rcu_read_unlock();
    return value;
}

rcu_read_lock()과 rcu_read_unlock() 사이가 RCU 읽기 구간(Read-side Critical Section)이다. 이 이름의 lock은 갱신자가 가진 뮤텍스와 경쟁해서 얻는 소유권을 뜻하지 않는다. 독자가 RCU 규칙에 따라 객체를 사용하는 구간을 표시하고, 갱신자는 회수 전에 그 구간의 종료를 확인한다.

포인터를 얻는 것부터 마지막 역참조까지 모두 읽기 구간 안에 있어야 한다. 위 함수는 정수 값을 복사해서 반환하므로 구간을 나간 뒤에도 그 값을 사용할 수 있다. 반면 p 자체를 반환하고 구간 밖에서 p->limit을 읽으면, 그 사이 객체가 해제될 수 있다. 구간 밖으로 객체를 가져가려면 참조 카운트 등 별도의 수명 보장 절차가 필요하다.

읽기 구간은 객체의 수명과 공개 순서를 보장하는 프로토콜이다. 기존 객체의 필드를 갱신자가 제자리에서 변경하면서 독자에게 일관된 여러 필드를 보장하는 기능까지 자동으로 생기지는 않는다. 또한 여러 갱신자가 서로의 변경을 덮어쓰는 문제는 update_lock 같은 별도 동기화로 해결한다.[3]

읽기 구간의 선점과 대기

이 장의 CONFIG_PREEMPT_RCU=y에서는 읽기 구간의 태스크도 CPU에서 선점될 수 있다. 그러므로 rcu_read_lock()을 항상 선점 금지로 설명할 수 없다. 대신 커널은 읽기 중 선점된 태스크를 별도로 추적한다.

일반 RCU 읽기 구간에서는 I/O 대기나 잠드는 뮤텍스 획득처럼 명시적으로 블록하는 연산을 넣지 않는다. 특히 같은 구간 안에서 synchronize_rcu()를 호출하면 자신이 끝나기를 기다리는 관계가 생긴다. 잠드는 독자가 필요한 용도에는 별도 API인 SRCU 등을 검토한다. PREEMPT_RT의 특정 락 처리 예외를 일반 RCU의 자유로운 수면 허용으로 확대해서는 안 된다.[1]

아키텍처와 공통 API

x86-64와 RISC-V RV64 모두 같은 RCU API와 Tree RCU 알고리즘을 사용한다. 차이는 공개·읽기 순서를 구현하는 아키텍처별 장벽과 메모리 모델에 있다. x86-64에서 특정 장벽 명령이 필요 없다고 해서 C의 보통 포인터 대입·읽기로 RCU API를 대체해도 되는 것은 아니다. 컴파일러 제약과 다른 아키텍처에서의 순서까지 API가 맡는다.

참고 문헌

  1. ↑ 1.0 1.1 Linux v6.12, include/linux/rcupdate.h, rcu_assign_pointer, rcu_dereference와 읽기 구간 API.
  2. ↑ Linux v6.12 RCU 문서, rcu_dereference의 포인터와 의존성 규칙.
  3. ↑ Linux v6.12 RCU 요구사항, 유예 기간·공개 보장과 보장하지 않는 조건.