<1> Atomic Operations


Ch.9에서 다룬 race condition을 다시 보자. 

 스레드A

 스레드B

 get val(0)

 get val(0)

 increment val(0 -> 1)

 

 

 increment val(0 -> 1)

 write back val(1)

 

 

 write back val(1)



여기에서 val++가 atomically하게 동작한다고 가정하면 다음과 같이 변화한다.


 스레드A

 스레드B

 get, increment, store val(0->1)

 

 

 get, increment, store val(1->2) 


위의 atomic operation에서는 항상 결과가 2가 됨이 보장된다. 

즉 atomic operation이란 원자가 더 이상 쪼개질 수 없는(것으로 생각되었던) 

물질이듯이 더 이상 쪼개질 수 없는 연산으로 처리됨을 의미한다. 

당연하지만, 더 이상 쪼개질 수 없다는 뜻은 중간에 인터럽트가 존재할 수 없다는 것이고, 따라서 race condition이 발생할 수 없다. 

리눅스 커널에서는 integer와 bit연산에 대한 atomic interface를 각각 제공한다.


1)Atomic integer

atomic_t라는 자료형 및 관련 함수들로 구현된다. 이는 C의 int와 동일하게 32비트 정수이다. 

atomic operation에 굳이 int가 아닌 atomic_t라는 별도의 자료형을 사용하는데에는 두 가지 이유가 있다.

  • atomic 함수에 nonatomic 변수가 전달되거나 nonatomic함수에 atomic 변수가 전달되는 것을 방지한다.

  • 컴파일러가 이 변수의 접근을 최적화하는 것을 방지한다. 


주로 counter로 사용되는 변수에 atomic_t가 자주 사용되는데, 이는 뒤에서 다룰 lock등의 동기화보다 가벼우면서도 동기화를 유지할 수 있기 때문이다. 


2)Atomic bitwise opeartions

Architecture-specific하게 구현된다. 다양한 함수들이 존재하며 nonatomic 함수들과 기능적으로 차이는 없다. 구별을 위해 nonatomic bitwise operation은 __을 앞에 붙인다.



<2> Spinlock


스핀락에서는 단 하나의 스레드만 위험 지역에 진입가능하다. 이름이 '스핀'락인 이유는 위험지역에 진입하고자 하는 스레드들이 while문 등으로 계속 실행하면서 대기하기 때문이다. 물론 위험 지역에 미리 진입 하고 있는 스레드가 없다면 바로 진입 가능하다. 뒤에 나올 세마포어와 다르게, 락을 대기하면서 프로세서 시간을 소모하기 때문에, 위험지역에서 너무 오래 머무르는 일이 없도록 해야한다. 구체적으로는 spin time < 2 * context switch time이 되어야 효율적이라고 할 수 있다. 


또 하나의 특징으로는 스핀락을 얻은 스레드가 휴면 상태로 진입할 수 없다는 점이다. 이 덕분에 후술할 다른 락들과 다르게 인터럽트 핸들러에서 사용 가능하다. 


스핀락은 다음과 같은 형태로 사용한다.


<linux/spinlock.h>

spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;

spin_lock(&mr_lock);
/* critical section */
spin_unlock(&mr_lock);


위와 같은 형태 때문에 락을 코드에 거는 것으로 착각할 수 있는데, 락은 항상 데이터에 거는 것임을 명심해야 한다. 이는 후술할 다른 락들도 마찬가지이다.



<3> Semaphore


세마포어는 상술한 스핀락과 다르게, 프로세서 타임을 소모하면서 락을 대기하는 것이 아니라, 휴면상태로 진입한다. 세마포어를 얻은 스레드가 위험 지역의 처리를 마치고 나올때, 대기중인 스레드가 존재한다면 휴면상태에서 깨워줄 수 있다. 또한 세마포어는 count를 설정하여 위험 지역에 진입가능한 스레드의 개수를 정할 수 있다. 세마포어를 얻을 때 count를 1감소시키고, 반납할때 1증가 시킨다. 만약 count가 0이 된다면 세마포어를 얻을 수 없는 상태이므로 휴면상태로 돌입하여 대기한다. 따라서 초기 count가 3이라면 위험 지역에서 허용 간으한 스레드의 수가 3개란 뜻이다. 


당연하게도, 위험 지역에서 2개 이상의 스레드를 허용할 일은 보통 없다. 따라서 세마포어는 count를 1로 많이 사용하며 이를 상호 배제한 락이라고 하여 뮤텍스(mutex)라 표현한다. 또한 세마포어를 얻은 상태에서 휴면 가능하므로 인터럽트 컨텍스트에서 세마포어를 사용할 수 없고 프로세스 컨텍스트에서만 사용하여야 한다. 마찬가지 이유로 스핀락을 얻은 상태로 휴면상태에 돌입 할 수 없으므로 스핀락을 얻은 상태로 세마포어를 얻을 수는 없다.



<4> Mutex


위의 세마포어에서 설명한 뮤텍스와 별도의 구현으로서의 뮤텍스가 커널에 존재한다. 이후의 뮤텍스는 모두 별도의 구현으로서의 뮤텍스를 지칭한다고 보면 된다. 뮤텍스는 세마포어 버전의 뮤텍스보다 간단한 인터페이스, 더 뛰어난 성능과 더 많은 제약조건을 포함한다. 제약조건들은 다음과 같다.


  1. 락을 획득한 컨텍스트가 반드시 락을 반납해야한다.
  2. 재귀적인 락 획득이나 반납은 불가능하다.
  3. 뮤텍스 락을 소유한 프로세스가 종료될 수 없다.
  4. 인터럽트 핸들러나 bottom half가 뮤텍스를 획득할 수 없다.
  5. 제공되는 API로만 뮤텍스를 관리할 수 있다.


이러한 제약조건들 덕분에 뮤텍스는 세마포어보다 더 뛰어난 성능과 디버깅을 가능하게 해준다.



<5> Semaphore vs Mutex


여기서 지칭하는 뮤텍스는 물론 별도의 구현으로서의 뮤텍스이다. 기본적으로 성능이 더 뛰어난 뮤텍스를 사용하되, 뮤텍스의 제약 조건에 걸리는 경우에만 세마포어를 사용한다.


<6> Spinlock vs Mutex


우선 인터럽트 컨텍스트에서는 휴면 상태에 돌입하지 않는 스핀락만 사용 가능하다. 프로세스 컨텍스트에서는 휴면 상태에 집입할 수 있으므로 뮤텍스만 사용할 수 있다. 만약 제한 조건이 없다면, 락 부담이 적어야 하거나 락의 사용 시간이 짧은 경우에는 스핀락을 사용하고, 그렇지 않은 경우에는 뮤텍스를 사용하면 된다.


 

+ Recent posts