리눅스 커널 - Race Condition 회피 방법 (Atomic operations, Disabling Preemption, Spin Locks)

리눅스 커널은 symmetric multi-processing (SMP)를 지원하며, 이는 race condition을 회피/대응할 수 있어야 한다는 말이다 (single core라고 하더라도 preemption 및 interrupt에 의해 동시성은 위협 받을 수 있다.).

SMP: 여러 개의 코어가 같은 메인 메모리를 공유하는 구조. 즉, 공유 메모리의 같은 address에 대한 접근이 동시(race condition)에 일어날 수 있다.

race condition에 대응하기 위해 critical section이 어디인지 알고 있다는 가정 하에 다음 접근 법을 선택할 수 있다.

  • critical section을 atomic하게 처리 (atomic instruction 이용)
  • critical section 도중 preemption을 disable
  • lock을 잡아서 serialize 시킨다 (critical section에 한 스레드만 존재할 수 있도록 한다)

Atomic Operations

하드웨어에서 제공하는 atomic operation (instruction)을 이용하면 락을 잡지 않고도 atomic operation을 보장받을 수 있다.
예시: atomic_inc(), atomic_dec(), test_bit(), set_bit(), change_bit() 등

Atomic operation은 하드웨어 레벨에서 락을 잡기 때문에 시스템 레벨에서 잡는 것 보다는 가볍지만 여전히 비싼(expensive) operation이다.

Disabling Preemption

local_irq_disable()로 인터럽트를 막고, preempt_disable()로 커널 스케줄러에 의한 선점을 막을 수 있다.

Spin Locks

스핀락은 락을 얻을 때까지 busy waiting을 한다. 락을 잡고 critical section 수행 후에 락을 해제하여 operation을 serialize 한다.
스핀락 이용 시, 특정 메모리 주소의 값을 동시에 여러 스레드가 보고 락을 얻을 수 있는지, 기다려야 하는지 판단한다. 같은 메모리 주소를 보기 때문에 cache thrashing 문제가 발생할 수 있다 (같은 메모리 주소에 대한 read/write가 cache invalidation을 발생시키고, cache miss를 일으킨다.). 이러한 문제들을 해결하기 위해 현재 리눅스 커널은 queued spin lock을 사용하고 있다.

queued spin lock은 락을 획득하지 못한 스레드가 락과 관련된 전역 변수 대신에 각 스레드별로 별도의 메모리 위치(큐 노드)에 자신의 대기 정보를 기록하고, 자신의 위치에서만 상태 변화를 관찰하며 대기하기 때문에 캐시 라인 경합(contention)을 줄일 수 있습니다.
락 점유가 해제되면 차례대로 대기 중인 CPU에게 락 소유권이 넘어가므로 공정성을 보장합니다.

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
위와 같이 spin_lock() 함수의 내부구현에서 락을 잡기 전 먼저 preempt_disable()을 불러서 preemption을 막는다. 이렇게 하는 이유는...
  • 스핀락으로 보호하는 임계 영역은 짧고 빠르게 실행되어야 하지만, 만약 락을 획득한 작업이 선점되어 다른 작업으로 컨텍스트 스위칭되면, 락을 점유한 상태에서 작업이 중단되어 동시에 여러 작업이 임계 영역에 접근하는 위험이 생긴다. 이로 인해 데이터 무결성이 깨질 수 있다.
  • 따라서 spin_lock()을 호출하면 내부에서 preempt_disable()을 호출하여 현재 CPU에서 선점 스케줄링을 비활성화해, 락을 가지고 있는 동안 반드시 계속 실행되도록 보장한다.
  • 선점이 비활성화되어도 인터럽트는 여전히 가능하므로, 인터럽트 컨텍스트에서 동일 락을 획득할 여지가 있다면 별도로 인터럽트를 비활성화하는 spin_lock_irq() 계열 함수를 사용해야 한다.

댓글