IBM®
메인 컨텐츠로 가기
    Korea [국가변경]    이용약관
 
 
   
        제품    서비스 & 솔루션    고객지원 & 다운로드    회원 서비스    
메인 컨텐츠로 가기

한국 developerWorks  >  리눅스  >

리눅스 동기화 메서드 분석

커널 원자 연산, 스핀락, 뮤텍스

developerWorks
문서 옵션

JavaScript가 필요한 문서 옵션은 디스플레이되지 않습니다.

영어원문

영어원문


제안 및 의견
피드백

난이도 : 중급

M. Tim Jones, 컨설턴트 엔지니어, Emulex Corp.

옮긴이: 박재호 이해영 dwkorea@kr.ibm.com

2008 년 5 월 27 일

리눅스(Linux®) 교육 과정을 거쳤다면, 동기화, 임계 영역, 잠금에 대해 배웠을지도 모르겠습니다. 하지만 커널 내부에서 어떻게 이런 개념을 활용할까요? 이 기사에서는 원자 연산, 스핀락, 읽기/쓰기 잠금, 커널 세마포어를 포함하여 2.6 커널에서 제공하는 잠금 메커니즘을 살펴봅니다. 또한 안전하고 효율적인 커널 코드를 작성하기 위해 사용 가능한 각 메커니즘을 알아봅니다.

이 기사에서는 리눅스 커널에서 사용 가능한 동기화와 잠금 메커니즘 중 몇 가지를 살펴본다. 이런 메커니즘은 2.6.23 커널에서 사용 가능한 여러 메서드를 위한 API 형태로 나타난다. 하지만 API를 파고 들기에 앞서, 해결하려는 문제를 먼저 이해할 필요가 있다.

팀 존스가 developerWorks에 쓴 분석... 연재

동기화와 잠금

병행성 속성이 존재할 때 동기화 메서드가 필요하다. 두 개 이상 프로세스가 동일한 시점에 실행해 잠재적으로 서로에 간섭을 일으키는 경우에 병행성 문제가 생긴다.

병행성 문제는 동일한 CPU를 여러 스레드가 공유하며, 선점이 경쟁 조건을 만들 때 단일 프로세서에서도 일어난다. 선점은 일시적으로 스레드 하나를 중지하고 다른 스레드가 수행하도록 만드는 방법으로 투명하게 CPU를 공유하게 만드는 기법이다. 경쟁 조건은 둘 이상 스레드가 같은 자료 항목을 다루며, 이 결과 실행 시간에 의존할 경우 발생한다. 병행성 문제는 또한 각 프로세서마다 동일한 자료에 동시에 접근하는 여러 스레드가 있을 경우에 다중 프로세서에서도 일어난다. 다중 프로세서에서 진짜 병렬성이 있을 경우 동시에 스레드를 수행한다는 사실을 기억하자. 단일 프로세서에서는 선점으로 병렬성을 추구한다. 병행성 문제는 단일이거나 다중이거나 양쪽 모두 문제가 된다.

리눅스 커널은 양쪽 모두에서 병행성을 지원한다. 커널 자체가 동적으로 동작하므로 다양한 상황에서 경쟁 조건이 생긴다. 리눅스 커널은 또한 대칭 다중 프로세싱(SMP)이라고 알려진 멀티프로세싱을 지원한다. 이 기사 뒤에 나오는 참고자료 절에서 SMP에 대한 내용을 찾아보기 바란다.

경쟁 조건과 싸우려면 임계 영역 개념이 필요하다. 임계 영역은 여러 스레드가 동시에 접근하는 과정에서 보호받는 코드 영역이다. 이 코드 영역은 공유된 자료나 (하드웨어 주변기기와 같은) 서비스를 다룰 수 있다. 임계 영역은 상호 배제라는 원칙으로 접근한다(스레드 하나가 임계 영역에 들어가면, 다른 모든 스레드는 외부에 머물러야 한다).

임계 영역 내부에서 생긴 문제는 데드락 조건이다. 독립적인 임계 영역이 둘 있을 때, 각각은 다른 자원을 보호한다. 각 자원마다 잠금이 가능한데 여기서는 A, B라고 하자. 두 스레드가 자원에 접근할 필요가 있을 때 스레드 X는 A를 잠그고, 스레드 Y는 B를 잠근다. 잠근 다음에는 각 스레드는 다른 스레드가 현재 잡고 있는 다른 잠금을 요청하려고 시도한다(스레드 X는 B를 잠그려 하고, 스레드 Y는 A를 잠그려고 한다). 두 스레드가 이제 데드락에 걸린 이유는 각각 자기 자원을 잡고 있으면서 다른 자원을 요구하기 때문이다. 이런 상황을 회피하는 간단한 해법은 항상 같은 순서로 잠금을 시도하는 방법으로, 특정 스레드가 데드락에 걸리지 않고 정상적으로 완료하도록 만들어준다. 다른 해법으로 데드락 상황 검출 방법이 있다. 표 1에 여기서 다루는 중요한 병행성 용어를 정리했다.


표 1. 병행성에서 중요한 용어 정의
용어정의
경쟁 조건스레드 두 개 이상이 동시에 자원에 접근해 일관성 없는 결과를 낳는 상황
임계 영역공유 자원에 접근하도록 조율된 코드 영역
상호 배제공유 자원에 독점적으로 접근하는 소프트웨어 속성
데드락둘 이상 프로세스와 둘 이상 자원 잠금이 만들어낸 특정 조건으로 프로세스가 생산적인 작업을 하지 못하도록 막아버린다.




위로


리눅스 동기화 메서드

이제 이론적인 측면과 풀어야 할 문제를 이해했으므로 리눅스가 동시성과 상호 배제를 위해 제공하는 다양한 기법을 살펴볼 시간이다. 초기에 상호 배제는 인터럽트 비활성으로 처리했는데, 이런 잠금 형태는 비효율적이다(물론 커널 내부에서 여전히 이런 비효율적인 잠금 기법을 사용한다). 이 메서드는 확장성도 나쁠 뿐더러 다른 프로세서에 대한 상호 배제를 보증하지도 못한다.

다음에 잠금 메커니즘을 설명하겠는데, 가장 먼저 (카운터나 비트마스크와 같은) 간단한 변수를 보호하는 원자 연산을 다룬다. SMP 아키텍처에서 효율적인 대기 잠금 방식인 간단한 스핀락과 읽기/쓰기 스핀락을 다음에 다룬다. 마지막으로 커널 뮤텍스를 다루는데, 원자 API 위에 만들어져 있다.




위로


원자 연산

리눅스 커널에서 가장 단순한 동기화 수단은 원자 연산이다. 원자는 API 함수 내부에 포함된 임계 영역을 의미한다. 잠금이 필요하지 않는 이유는 원자 연산은 속성상 호출 중에 일어나기 때문이다. C 프로그래밍 언어에서는 원자 연산을 보장하지 않으므로 리눅스는 기반 아키텍처를 활용해 원자 연산을 구현한다. 아키텍처마다 상당히 다르므로 원자 연산 구현 방법도 다양하다. 몇몇은 완전히 어셈블리어로 만들어져 있으며, 몇몇은 C와 local_irq_save/local_irq_restore를 사용해서 인터럽트를 비활성화한다.

오래된 잠금 메서드
커널에서 잘못된 잠금 구현 방법은 지역 CPU에서 하드 인터럽트를 비활성화하는 방식이다. 이런 함수는 여전히 커널이 지원하고 있으며, (종종 원자 연산에서) 실제로 쓰이고 있지만 사용을 권장하지는 않는다. local_irq_save 루틴은 인터럽트를 비활성화하며, local_irq_restore는 직전에 비활성화한 인터럽트를 복원한다. 이 루틴은 재진입이 가능한데, 다른 문맥 내부에서 호출이 가능함을 의미한다.

원자 연산은 카운터처럼 보호 대상이 단순한 상황에서 위력을 발휘한다.단순하면서도 간단한 원자 API는 다양한 상황에 맞는 몇몇 연산자를 제공한다. 여기에 API 활용 예를 정리했다.

원자 변수를 선언하려면, 간단히 atomic_t 유형으로 변수를 선언하면 된다. 이 구조체는 단일 int 항목을 포함한다. 다음으로 원자 변수를 ATOMIC_INIT 매크로를 사용해 초기화한다. Listing 1에 나온 예는 원자 카운터를 0으로 설정한다. 또한 원자 변수를 atomic_set 함수로 실행 중에 초기화할 수도 있다.


Listing 1. 원자 변수 생성과 초기화
                
atomic_t my_counter ATOMIC_INIT(0);

... 또는 ...

atomic_set( &my_counter, 0 );

원자 API는 여러 경우를 다루는 풍부한 함수 집합을 제공한다. atomic_read로 원자 변수 내용을 읽고, atomic_add로 원자 변수에 특정 값을 추가한다. 가장 일반적인 연산은 단순히 변수 값을 증가하는 함수로 atomic_inc를 사용한다. 감소 연산자도 존재하는데 add와 증가 연산자의 반대 기능이다. Listing 2는 이런 함수 용례를 보여준다.


Listing 2. 간단한 산술 원자 함수
                
val = atomic_read( &my_counter );

atomic_add( 1, &my_counter );

atomic_inc( &my_counter );

atomic_sub( 1, &my_counter );

atomic_dec( &my_counter );

이 API는 또한 연산 후 테스트를 포함한 다른 공통 기능도 지원한다. 원자 변수를 처리한 다음에 바로 결과를 반환하도록 만든다(즉, 이 둘을 원자 연산 하나로 수행한다). atomic_add_negative라는 특수 함수는 원자 변수에 값을 더한 다음에 결과값이 음수이면 참을 반환한다. 이는 아키텍처에 의존적인 몇몇 커널 내 세마포어 함수가 사용한다.

많은 함수가 변수 값을 반환하지 않지만, 특별히 두 가지 함수는 연산 후 테스트 값을 반환한다. Listing 3에 atomic_add_returnatomic_sub_return을 정리했다.


Listing 3. 연산 후 테스트 함수
                
if (atomic_sub_and_test( 1, &my_counter )) {
  // my_counter는 0
}

if (atomic_dec_and_test( &my_counter )) {
  // my_counter는 0
}

if (atomic_inc_and_test( &my_counter )) {
  // my_counter는 0
}

if (atomic_add_negative( 1, &my_counter )) {
  // my_counter는 음수
}

val = atomic_add_return( 1, &my_counter ));

val = atomic_sub_return( 1, &my_counter ));

아키텍처가 64비트 long을 지원한다면(BITS_PER_LONG은 64), long_t 원자 연산이 가능하다. linux/include/asm-generic/atomic.h에 사용 가능한 long 연산을 볼 수 있다.

또한 원자 API로 비트마스크를 지원하는 함수도 찾을 수 있다. (직전에 살펴본) 산술 연산 대신에 set과 clear 연산을 찾아보자. 특히 SCSI를 비롯하여 여러 드라이버가 이런 원자 연산을 사용한다. 비트마스크 원자 연산 사용이 산술 연산과 조금 다른 이유는 (set mask와 clear mask라는) 두 가지 연산만 가능하기 때문이다. Listing 4에서 보여주듯이 실행한 연산에 값과 비트마스크를 넘겨야 한다.


Listing 4. 비트마스크 원자 함수
                
unsigned long my_bitmask;

atomic_clear_mask( 0, &my_bitmask );

atomic_set_mask( (1<<24), &my_bitmask );


원자 API 명세
원자 연산 구현 내역은 아키텍처에 독립적이므로 ./linux/include/asm-<arch>/atomic.h에서 찾기 바란다.

스핀락

스핀락은 대기 잠금 방식을 사용해서 상호 배제를 가능하게 만드는 특별한 방법이다. 잠금이 가능할 때, 잠근 다음에 상호 배제 행동을 수행하고 끝나면 잠금을 푼다. 잠금이 가능하지 않으면, 잠금이 가능할 때까지 잠금에 대한 스레드 대기 상태로 있는다. 대기 상태가 비효율적으로 보일지 모르지만, 실제로는 스레드를 잠들기 상태로 바꿔서 나중에 잠금이 가능할 때 깨우는 방식보다 훨씬 더 빠르다.

스핀락은 실제로 SMP 시스템에서만 유용하다. 코드가 결국 SMP 시스템에서도 돌아가야 하므로 이런 기능을 단일 프로세스 시스템에 추가하는 작업도 의미가 있다.

스핀락은 두 가지 변종이 있다. 하나는 전체 락이고 나머지는 읽기/쓰기 락이다. 전체 락을 먼저 살펴보자.

우선 간단한 선언을 통해 새로운 스핀락을 생성한다. spin_lock_init 호출을 통하거나 선언 과정에서 초기화될 수 있다. Listing 5에 제시한 각 변종은 결과가 같다.


Listing 5. 스핀락 생성과 초기화
                
spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED;

... 또는 ...

DEFINE_SPINLOCK( my_spinlock );

... 또는 ...

spin_lock_init( &my_spinlock );

이제 스핀락을 정의했으니 사용 가능한 잠금 변종을 살펴보자. 각 변종은 문맥에 따라 쓰임새가 다르다.

먼저 spin_lockspin_unlock 변종을 Listing 6에 정리했다. 이는 가장 단순한 형태로 완벽한 메모리 장벽을 포함하지만 인터럽트 비활성화는 수행하지 않는다. 즉, 인터럽트 핸들러와 잠금 사이에 아무런 상호작용이 없다고 가정한다.


Listing 6. 스핀락 잠금과 잠금해제 함수
                
                spin_lock( &my_spinlock );

// 임계 영역

spin_unlock( &my_spinlock );

다음으로 irqsaveirqrestore 쌍을 Listing 7에 정리했다. spin_lock_irqsave 함수는 스핀락을 얻고 (SMP 경우에) 지역 프로세서에서 인터럽트를 비활성화한다. spin_unlock_irqrestore 함수는 스핀락을 풀고 (flags 인수로) 인터럽트를 복원한다.


Listing 7. 지역 CPU 인터럽트를 비활성화하는 스핀락 변종
                
                spin_lock_irqsave( &my_spinlock, flags );

// 임계 영역

spin_unlock_irqrestore( &my_spinlock, flags );

spin_lock_irqsave/spin_unlock_irqrestore 쌍을 조금 덜 안전하게 만든 변종은 spin_lock_irq/spin_unlock_irq다. 이 변종을 피하라고 추천하는 이유는 인터럽트 상태를 저장하지 않기 때문이다.

마지막으로, 커널 스레드가 bh(bottom half)로 자료를 공유한다면, 또 다른 스핀락 변종을 사용할 수 있다. bh는 디바이스 드라이버에서 인터럽트 처리를 나중에 수행하기 위해 작업을 지연하는 메커니즘이다. 이 스핀락 변종은 지역 CPU에서 소프트 인터럽트를 비활성화한다. 이는 softirq, tasklet, bh를 지역 CPU에서 동작하지 못하도록 막는 효과가 있다. 관련 내용을 Listing 8에 정리했다.


Listing 8. bh와 상호 작용하는 스핀락 함수
                
                spin_lock_bh( &my_spinlock );

// 임계 영역

spin_unlock_bh( &my_spinlock );




위로


읽기/쓰기 스핀락

많은 경우에 있어, 자료 접근은 읽는 쪽이 많고 쓰는 쪽이 적다(읽기 위한 자료 접근이 쓰기 위한 자료 접근보다 일반적으로 더 흔하다). 이런 모델을 지원하기 위해 읽기/쓰기 잠금이 만들어졌다. 이런 모델이 흥미로운 이유는 동시에 여러 곳에서 읽기를 허용하지만 쓰기는 한 곳만 허용하기 때문이다. 쓰는 쪽에서 잠금을 얻으면, 읽는 쪽에 임계 영역 진입을 허용하지 않는다. 일단 읽는 쪽이 잠금을 얻을 경우, 임계 영역에 진입하도록 여러 곳에 읽기를 허용한다. Listing 9는 이런 모델을 보여준다.


Listing 9. 읽기/쓰기 스핀락 함수
                
rwlock_t my_rwlock;

rwlock_init( &my_rwlock );

write_lock( &my_rwlock );

// 임계 영역 -- 읽고 쓸 수 있다.

write_unlock( &my_rwlock );


read_lock( &my_rwlock );

// 임계 영역 -- 읽기 전용

read_unlock( &my_rwlock );

또한 잠금을 원하는 상황에 따라 bh와 IRQ용 읽기/쓰기 스핀락 변종도 있다. 당연한 이야기지만, 읽기/쓰기에 해당하는 잠금이 필요하면, 읽는 쪽과 쓰는 쪽을 구분하지 못하는 표준 스핀락을 대신해서 읽기/쓰기 스핀락을 사용해야 한다.




위로


커널 뮤텍스

뮤텍스는 세마포어 행동 양식을 지원하기 위한 커널 기능이다. 커널 뮤텍스는 원자 API 위에 구현되어 있다. 물론 커널 사용자에게는 이런 사실이 감춰져 있다. 뮤텍스는 단순하지만 기억해야 하는 몇 가지 규칙이 존재한다. 단지 태스크 하나만 한번에 뮤텍스 하나를 얻을 수 있으며, 이 태스크만 뮤텍스 잠금을 풀 수 있다. 재귀적으로 뮤텍스 잠금과 잠금 해제를 수행하지는 못하며, 뮤텍스는 인터럽트 문맥에서 사용하지 못한다. 하지만 뮤텍스는 현재 커널 세마포어 옵션에 비해 좀 더 빠르고 좀 더 단순하므로 요구 사항을 충족한다면 커널 세마포어 대신에 커널 뮤텍스를 활용하자.

DEFINE_MUTEX 매크로를 통해 연산 하나로 뮤텍스를 생성해 초기화한다. 이 매크로는 새로운 뮤텍스를 생성해 구조체를 초기화한다. 구현 방식은 ./linux/include/linux/mutex.h를 살펴보자.

                DEFINE_MUTEX( my_mutex );

뮤텍스 API는 함수 다섯 가지를 제공한다. 그 중 잠금, 잠금 해제, 뮤텍스 테스트에 필요한 함수 세 가지가 있다. 우선 잠금 함수를 살펴보자. 첫 번째 함수인 mutex_trylock은 즉시 잠금을 원하거나 뮤텍스가 사용 가능하지 않을 경우에 제어권이 바로 넘어오기를 원하는 상황에서 사용한다. 관련 내용을 Listing 10에 정리했다.


Listing 10. mutex_trylock으로 뮤텍스 얻기
                
ret = mutex_trylock( &my_mutex );
if (ret != 0) {
  // 잠금 얻기!
} else {
  // 잠금을 얻지 못했다.
}

바로 제어권이 넘어오는 대신에 잠금을 기다리기 원하면, mutex_lock을 호출한다. 이 함수는 뮤텍스가 사용 가능하면 반환하며, 그렇지 않으면 뮤텍스가 사용 가능해질 때까지 잠든다. 두 경우 모두 제어권이 반환되면, 호출하는 쪽에서 뮤텍스를 쥐고 있는 상황이 된다. 마지막으로 mutex_lock_interruptible은 호출하는 쪽이 잠들지도 모르는 상황에 사용한다. 이 경우 함수는 -EINTR을 반환할 수도 있다. 관련 내용을 Listing 11에 정리했다.


Listing 11. 잠들지도 모르는 경우에 뮤텍스 얻기
                
                mutex_lock( &my_mutex );

// 이제 호출하는 쪽에서 잠금에 성공했다.

if (mutex_lock_interruptible( &my_mutex ) != 0)  {

  // 시그널이 인터럽트했고, 뮤텍스를 얻지 못했다.

}

뮤텍스를 잠그고 나면 반드시 잠금을 해제해야 한다. 이런 작업은 mutex_unlock 함수로 가능하다. 이 함수는 인터럽트 문맥에서는 호출되지 못한다. 마지막으로 mutex_is_locked 호출을 통해 뮤텍스 상태를 점검할 수 있다. 이 함수는 실제로 인라인 함수로 컴파일된다. 뮤텍스를 잠그면 잠근 뮤텍스를 반환하고, 그렇지 않으면 0을 반환한다. 관련 내용을 Listing 12에 정리했다.


Listing 12. mutex_is_locked로 뮤텍스 테스트
                
                mutex_unlock( &my_mutex );

if (mutex_is_locked( &my_mutex ) == 0) {

  // 뮤텍스 잠금이 풀렸다.

}

원자 API를 기반으로 만들었기에 뮤텍스 API에 제약은 있지만, 효율적이므로 요구 조건에 맞으면 활용할 가치가 있다.




위로


커널 전체 잠금

마지막으로 커널 전체 잠금(BKL, Big Kernel Lock)이 남아 있다. BKL은 커널에서 점점 사라지고 있지만, 여전히 제거하기 아주 어려운 부분에는 남아있다. BKL은 리눅스에서 여러 프로세서를 지원하도록 만들어줬지만, 좀 더 세분화된 잠금이 천천히 BKL을 대체하고 있다. BKL은 lock_kernelunlock_kernel을 통해 사용할 수 있다. 세부 정보는 ./linux/lib/kernel_lock.c를 살펴보기 바란다.




위로


요약

리눅스는 옵션이 다양한 스위스 군용 칼이며, 잠금 메서드라고 해서 다르지 않다. 원자 잠금은 단순히 잠금 메커니즘 이외에 산술과 비트 연산도 동시에 제공한다. 스핀락은 (대부분 SMP용) 잠금 메커니즘을 제공하며, 읽기/쓰기 스핀락은 한 곳에서 쓰고 여러 곳에서 읽도록 잠금을 얻도록 허용한다. 마지막으로 뮤텍스는 상대적으로 새로운 잠금 메커니즘으로 원자 연산 위에 만들어진 간단한 API를 제공한다. 요구사항이 무엇이든, 리눅스는 여러분의 자료를 보호하기 위한 잠금 방식을 지원할 수 있다.



참고자료

교육

제품 및 기술 얻기
  • Linux Kernel Archives에서 최신 리눅스 원시 코드를 찾아보자. 리눅스 코드는 자체만으로 커널 동작에 대한 가장 직접적인 정보를 제공한다. 또한 (비록 몇몇은 상당히 오래되긴 했지만) Documentation 하위 디렉터리에 많은 문서가 들어있다.

  • SEK for Linux 주문: DB2®, Lotus®, Rational®, Tivoli®, WebSphere®와 같은 최신 리눅스용 IBM 평가판 소프트웨어가 담긴 두 장짜리 DVD 세트를 주문하자.

  • IBM 평가판 소프트웨어: developerWorks에서 직접 내려받아 다음 번 리눅스 개발 프로젝트에 활용하자.


토론


필자소개

M. Tim Jones 사진

M. Tim Jones는 임베디드 소프트웨어 아키텍트이자 GNU/Linux Application Programming, AI Application Programming, BSD Sockets Programming from a Multilanguage Perspective의 저자이기도 한다. Jones의 공학 배경은 정지 위성을 위한 커널 개발에서 시작해 임베디드 시스템 아키텍처와 네트워크 프로토콜 개발에 이르기까지 다양한 분야를 아우른다. Jones는 콜로라도 주, 롱몬트 소재 Emulex 사에서 컨설턴트 엔지니어로 활약한다.




기사에 대한 평가


보다 나은 서비스를 제공하기 위함이오니 잠시 짬을 내어 이 양식을 제출하여 주십시오.



 


 


 


이 문서 북마킹 하기

mar.gar.in mar.gar.in naver naver eolin eolin del.icio.us del.icio.us





위로


DB2, Lotus, Rational, Tivoli, and WebSphere are trademarks of IBM Corporation in the United States, other countries, or both. Linux is a registered trademark of Linus Torvalds in the United States, other countries, or both. 기타 회사, 제품, 및 서비스명은 다른 상표나 서비스 마크일 수 있습니다.

developerWorks 콘텐트를 다른 사이트에 전재하기:
developerWorks 콘텐트에 대한 저작권은 IBM에 있습니다. IBM의 서면 허가나 원본 저자의 허락이 없이는 전재를 금합니다. 저희 콘텐트를 전재하시려면 IBM developerWorks 담당자 에게 문의하십시오.
    IBM 소개 개인정보 보호정책 문의