https://seokbeomkim.github.io/posts/rcu/

1. 들어가기 전에 …

앞으로 기술하는 내용은 http://jake.dothome.co.kr/rcu/#comment-214230 에서 발췌하여 정리하는 내용이므로 원본 내용 확인을 위해서는 링크에서 직접 보길 권한다. RCU 내용 뿐만 아니라, 리눅스 커널 전반적인 내용에 대해 정말 자세하게 정리가 잘 되어있다.

RCU(Read, Copy, Update)란 리눅스 커널 내에서 주로 읽기 연산만 일어나고 쓰기 연산의 비중은 매우 작은 객체에 주로 쓰이는 동기화 기법이다. Reader-Writer 락과 비슷한 동기화 기법인데, RW 락에 대해 RCU가 가지는 상대적인 강점으로는 읽기 연산이 wait-free(읽기 연산에 대해 Block이 일어나지 않음)이며 그 오버헤드가 극도로 작다는 점 등이 있다. 대신 쓰기 연산의 오버헤드가 상당하고 쓰기에 필요한 동기화의 시간 복잡도가 최소한 RCU로 보호되는 객체의 크기에 비례한다.

2. RCU란?

불변 객체(Immutable Object)에 대한 쓰레드는 안전하다. 즉, 어떤 코드 영역을 수행하는 동안 공유되는 메모리 구간이 불변인 경우라면, 코드로 인한 상태의 변화는 항상 예측 가능하므로 코드 수행의 결과가 항상 결정적(Deterministic)이다.
메모리 배리어 등을 통해 가시성이 확보된 경우 단일 워드에 대한 쓰기 연산은 대개 원자적이다.
어떤 객체의 상태를 변경하는 코드를 수행할 때 그 변경이 다른 쓰레드의 입장에서 한 순간에 원자적으로 이루어지는 것처럼 보인다면, 연관된 코드들의 수행 내역은 단일 쓰레드로 재현 가능하다. 쓰레드 간 코드 수행 순서에 대한 구체적인 합의가 필요한 경우는 드물기 때문에 대부분 이 정도만 보장되어도 동기화로서 충분한 역할을 할 수 있다.

GC(Garbage Collector)를 지원하는 요즘 언어들은 객체를 직접 변경하는 대신 불변 객체를 새로 만들어서 사용한다. 예를 들어, 자바의 경우는 문자열을 변경할 경우, 변경된 결과의 불변 문자열을 가져와 새로운 인스턴스를 할당하여 사용한다(String Internalization). 즉, 기존의 객체를 변경하는 대신 새로운 불변 객체를 만들어 쓰레드의 안전성을 쉽게 확보하는 것이 RCU의 기본 개념이다.

RCU로 보호되는 객체에 대한 읽기를 하려고 하는 경우, 평소처럼 포인터를 가져와 읽기 전용으로 해당 객체를 다루면 된다. 하지만 RCU로 보호되는 객체를 변경하려는 경우, 객체를 직접 변경하는 대신 새로 할당한 메모리 영역에 객체를 복사한 뒤 새로 만든 객체를 변경한다. 기존의 객체는 불변이므로 이 과정은 안전하다.

하지만 실제로 GC와 달리 직접 관련된 자원을 해제해 관리해줘야 하는 경우 Reader 입장에서 더 이상 접근할 방도가 없는 객체인 경우만 해제해주면 되지만 이를 위해서는 레퍼런스 카운팅이 필요하다. 즉, 읽기 영역에 진입/이탈 시에 원자적인 정수 연산이 적어도 한 번씩은 일어나는데 리눅스 커널에서는 이를 간단하게 해결한다.

RCU는 커널 코드 내에서만 사용되며 커널 모드에서 수행이 끝나면 RCU의 읽기 구간 역시 끝나게 된다. 선점형 커널을 사용하고 있는 경우 문맥 교환이 한번씩 이루어지고 비선점형 커널인 경우 읽기가 끝나면 모든 동작들이 종료된 것이므로 메모리를 안전하게 해제할 수 있다.

3. RCU의 장/단점

RCU는 read-side overhead를 최소화하는데 목적이 있기 때문에 동기화 로직이 읽기 동작에 더 많은 비율로 사용되는 경우만 사용한다. 수정 동작이 10% 이상인 경우에는 오히려 성능이 떨어지므로 RCU 대신 다른 동기화 기법을 선택해야 한다.

3-1. 장점

  1. read-side overhead가 거의 없다. (zero wait, zero overhead)
  2. Deadlock 이슈 없음
  3. Priority Inversion 이슈 없음
  4. Unbounded Latency 이슈 없음