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

운영체제 기본/Linux/RCU/갱신과 메모리 회수

~ldmsys
관리자 (토론 | 기여)님의 2026년 11월 1일 (일) 02:18 판 (관리자님이 운영체제 기본/Linux의 RCU/갱신과 메모리 회수 문서를 운영체제 기본/Linux/RCU/갱신과 메모리 회수 문서로 이동했습니다: Linux 각론 제목을 계층형 경로로 통일)

RCU 갱신은 새 객체를 공개하는 일과 이전 객체를 해제하는 일을 분리한다. 공유 포인터에서 이전 객체를 떼어냈다고 해서, 이미 그 주소를 읽은 독자까지 사라지는 것은 아니기 때문이다.

유예 기간

유예 기간(Grace Period)은 회수에 앞서 기존 RCU 읽기 구간들이 끝났음을 확인하는 기간이다. synchronize_rcu()는 호출 전에 시작된 읽기 구간들이 끝나기를 기다린다. 기다리는 동안 새로 시작한 모든 독자까지 없애거나, 시스템 전체가 동시에 읽지 않는 한 순간을 찾을 필요는 없다.[1]

시간   갱신자                 독자 A                 독자 B
  1                           lock; A 주소 획득
  2    B 초기화
  3    공유 포인터를 B로 교체
  4    synchronize_rcu 시작   A를 계속 읽음
  5                                                   lock; B 주소 획득
  6                           unlock
  7    유예 기간 완료 가능                           B를 계속 읽음
  8    A 해제
  9                                                   unlock

독자 A는 이전 객체를 읽었으므로 끝날 때까지 기다려야 한다. 교체 뒤 새 읽기 구간을 시작해 B를 얻은 독자 B는 A의 회수를 막을 이유가 없다. 구현이 실제로 더 오래 기다리는 경우는 있지만, 모든 미래 독자를 계속 기다리는 것이 API의 요구사항은 아니다. 객체를 찾을 수 있는 공유 경로에서 먼저 제거하고, 그 다음에 유예 기간을 기다리는 순서가 핵심이다.

동기식 회수

앞 장의 설정 객체를 새 값으로 바꾸는 함수다. 읽기 함수는 read_limit()을 그대로 사용한다. 초기 포인터가 NULL이어도 동작한다.

int replace_limit(int value)
{
    struct config *new, *old;

    new = kmalloc(sizeof(*new), GFP_KERNEL);
    if (new == NULL)
        return -ENOMEM;
    new->limit = value;

    mutex_lock(&update_lock);
    old = rcu_dereference_protected(current_config,
                                  lockdep_is_held(&update_lock));
    rcu_assign_pointer(current_config, new);
    mutex_unlock(&update_lock);

    if (old != NULL) {
        synchronize_rcu();
        kfree(old);
    }
    return 0;
}

갱신자용 뮤텍스는 이전 포인터를 얻고 새 포인터로 교체하는 구간을 직렬화한다. rcu_dereference_protected()는 이처럼 다른 락으로 보호되는 갱신 측 접근을 표현한다. 이 예제에서는 각 갱신자가 자신이 떼어낸 객체 하나만 회수하며, 독자는 공개 뒤 불변인 필드만 읽는다. 따라서 유예 기간을 기다리는 동안까지 갱신자용 뮤텍스를 잡을 필요가 없다.

뮤텍스는 갱신자들 사이의 충돌을 막고, RCU는 독자가 가진 이전 포인터의 수명을 지킨다. 서로 다른 문제를 담당하므로 하나만 남기면 같은 보장을 얻지 못한다. 새 객체를 공개하자마자 kfree(old)를 하면 시간선의 독자 A가 해제된 메모리를 읽는다.

콜백을 이용한 비동기 회수

갱신자가 직접 기다리지 않아도 되도록, call_rcu()는 유예 기간이 지난 뒤 실행할 콜백을 등록한다. 객체 안의 struct rcu_head는 대기 중인 콜백의 연결 정보다.

static void free_config(struct rcu_head *head)
{
    struct config *old = container_of(head, struct config, rcu);
    kfree(old);
}

/* 포인터 교체와 update_lock 해제 뒤 */
if (old != NULL)
    call_rcu(&old->rcu, free_config);

이 부분은 앞 예제의 synchronize_rcu(); kfree(old);를 대신한다. 두 회수 방식을 같은 객체에 함께 적용하지 않는다. call_rcu()는 반환했어도 콜백이 아직 실행되지 않았을 수 있으므로, 등록한 객체나 rcu_head를 즉시 다시 쓰면 안 된다. 일반 RCU 콜백에는 잠드는 연산을 넣지 않는다. 단순 객체 해제에는 kfree_rcu() 계열도 사용할 수 있다.[1]

비동기 회수는 갱신자를 기다리게 하지 않는 대신 회수 대기 객체를 쌓을 수 있다. 갱신이 빠르거나 독자가 오래 읽기 구간에 남으면 메모리 사용량과 회수 지연이 증가한다. 유예 기간은 정해진 밀리초를 기다리는 타이머가 아니라, 기존 읽기의 종료를 확인하는 조건이다.

사용 조건과 실패 사례

잘못된 사용 문제가 생기는 이유
객체를 공개하기 전에 필드 초기화를 끝내지 않음 독자가 준비되지 않은 값을 읽을 수 있음
포인터만 바꾸고 이전 객체를 즉시 해제 이미 이전 포인터를 얻은 독자가 남아 있음
읽기 구간을 나간 뒤 포인터 역참조 이후 수명 보장이 끝남
갱신자 동기화 없이 여러 변경을 수행 RCU는 갱신자 상호배제를 제공하지 않음
공개된 객체의 여러 필드를 제자리 변경 객체 생존과 필드 묶음의 일관성은 별개임
독자가 필요한 락을 잡은 채 유예 기간 대기 독자는 락을 기다리고 갱신자는 독자의 종료를 기다릴 수 있음

모듈을 내리는 등 콜백 코드 자체를 없앨 때는, 유예 기간뿐 아니라 등록했던 콜백의 실행 완료도 구분해야 한다. synchronize_rcu()가 모든 이전 call_rcu() 콜백의 실행 완료를 보장하지는 않는다. 더 이상 새 콜백을 등록하지 못하게 정리한 뒤, 필요한 경우 rcu_barrier()로 이미 등록된 콜백의 완료를 기다린다.[2]

참고 문헌

  1. ↑ 1.0 1.1 Linux v6.12, kernel/rcu/tree.c, synchronize_rcu, synchronize_rcu_normal, call_rcu.
  2. ↑ Linux v6.12, Documentation/RCU/rcubarrier.rst, 모듈 해제와 rcu_barrier.