Introduction to spinlocks
소개
이 파트는 linux-insides 책의 새로운 챕터를 시작합니다. 앞의 chapter 에서는 타이머와 시간 관리에 대한 내용을 다뤘습니다. 이제 다음으로 넘어갑시다. 이 파트의 제목에서 이미 이해하셨겠지만, 이 챕터는 리눅스 커널의 동기화 기본기능들을 설명합니다.
항상 그랬듯, 동기화에 관련된 뭔가를 고려하기 전에, 동기화 기본기능
이 일반적으로 무엇인가에 대해 알아보겠습니다. 사실, 동기화 기본기능은 두개 이상의 병렬 프로세스나 쓰레드가 특정 코드 영역을 동시에 수행하지 못하게 하는 소프트웨어 메커니즘입니다. 예를 들어, kernel/time/clocksource.c 파일의 다음 코드를 봅시다:
이 코드는 특정 clocksource 를 clock source 리스트에 추가하는 __clocksource_register_scale
함수에서 가져온 겁니다. 이 함수는 등록된 clock source 를 가지고 있는 리스트에 여러 연산을 수행합니다. 예를 들어, clocksource_enqueue
함수는 주어진 clock source 를 등록된 clocksource를 가지고 있는 리스트 - clocksource_list
에 추가합니다. 이 코드는 두 함수로 싸여져 있음을 알아두시기 바랍니다: 하나의 패러미터 (여기선 clocksource_mutex
) 를 받는 mutex_lock
과 mutex_unlock
입니다.
이 함수들은 mutex 동기화 기본기능에 기반한 locking 과 unlocking 을 나타냅니다. mutex_lock
이 수행되면, 이 함수는 우리가 두개 이상의 쓰레드가 이 mutex 소유자가 mutex_unlock
을 수행하기 전까지는 이 코드를 동시에 수행하는 걸 막을 수 있게 해줍니다. 달리 말하면, 우리는 clocksource_list
의 병렬 연산을 방지합니다. 여기서 mutex
가 필요한 이유가 뭘까요? 두개의 병렬 프로세스가 하나의 clock source 를 등록하려 하면 어떻게 될까요. 우리가 이미 알고 있듯, clocksource_enqueue
함수는 주어진 clock source 를 clocksource_list
리스트에 가장 큰 rating 을 갖는 clock source (시스템에서 가장 높은 frequency 를 갖는 등록된 clock source) 바로 뒤에 추가시킵니다:
만약 두개의 병렬 프로세스가 이걸 동시에 수행하면, 두 프로세스 모두 같은 entry
를 보게 되어 race condition 을 일으킬 수 있는데 이를 달리 말하면, 두번째 프로세스가 list_add
를 수행함으로써 첫번째 쓰레드의 clock source 를 덮어쓰게 될겁니다.
이 간단한 예제 외에, 동기화 기본기능은 리눅스 커널의 모든 곳에 있습니다. 앞의 chapter 또는 다른 챕터를 다시 보거나 일반적인 리눅스 커널 솟 크도르르 보게 되면 이런 것들을 많이 볼 수 있을 겁니다. 우린 리눅스 커널에서 mutex
가 어떻게 구현되어 있는지는 고려하지 않겠습니다. 사실, 리눅스 커널은 다양한 동기화 기본기능들을 제공합니다:
mutex
;semaphore
;seqlock
;atomic operation
;기타 등등.
우린 이 챕터를 spinlock
으로 시작하겠습니다.
리눅스 커널의 spinlock.
spinlock
간단히 말해 은 두개의 상태를 가질 수 있는 변수를 갖는 낮은 단계의 동기화 메커니즘입니다:
획득됨 (acquired)
;해제됨 (released)
.
spinlock
을 획득하고자 하는 각 프로세스는 spinlock 획득됨
을 의미하는 값을 이 변수에 써야하고 이후에는 spinlock 해제됨
상태를 이 변수에 써야 합니다. 만약 어떤 프로세스가 spinlock
으로 보호되는 코드를 수행하려 하면, 이 프로세스는 이 락을 잡고 있는 프로세스가 그 락을 놓기 전까지 멈춰 있게 됩니다. 이 경우 모든 관련된 연산은 원자적 (atomic) 이어서 race condition 상태를 방지할 수 있어야 합니다. 이 spinlock
은 리눅스 커널의 spinlock_t
타입으로 표현됩니다. 우리가 리눅스 커널 코드를 보려 한다면, 이 타입이 광범위하게 사용되는 걸 볼 수 있을 겁니다. 이 spinlock_t
는 다음과 같이 정의되어 있으며:
include/linux/spinlock_types.h 헤더 파일에 있습니다. 이 구현은 CONFIG_DEBUG_LOCK_ALLOC
커널 설정 옵션의 상태에 종속적임을 알 수 있을 겁니다. 이건 지금은 건너뛸텐데, 모든 디버깅 관련된 것들은 이 파트의 끝에서 다룰 것이기 때문입니다. 따라서, CONFIG_DEBUG_LOCK_ALLOC
커널 설정 옵션은 비활성화 되어 있다면, 이 spinlock_t
는 raw_spinlock
이라는 하나의 필드를 갖는 union 만을 갖습니다:
이 raw_spinlock
구조체는 같은 헤더 파일에 정의되어 있으며 일반' 스핀락의 구현을 나타냅니다.
raw_spinlock` 구조체가 어떻게 정의되어 있는지 봅시다:
arch_spinlock_t
는 아키텍쳐에 특수한 spinlock
구현을 나타냅니다. 앞서 언급되었듯, 디버깅 커널 설정 옵션은 건너뛰겠습니다. 이 책은 x86_64 아키텍쳐에 집중되어 있으므로, 우리가 보고자 하는 arch_spinlock_t
는 include/asm-generic/qspinlock_types.h 헤더 파일에 있으며 다음과 같습니다:
지금은 이 구조체를 더 들여다 보지 않겠습니다. 스핀락을 사용하는 연산을 알아봅시다. 리눅스 커널은 spinlock
에 대해 다음과 같은 연산들을 제공합니다:
spin_lock_init
- 특정spinlock
의 초기화를 수행합니다;spin_lock
- 특정spinlock
을 획득합니다;spin_lock_irqsave
와spin_lock_irq
- 이 프로세서에서의 인터럽트를 불능화시키고 앞의 인터럽트 상태를
flags
에 보존하거나/하지 않습니다;spin_unlock
- 특정spinlock
을 해제합니다;spin_unlock_bh
- 특정spinlock
을 해제하고 소프트웨어 인터럽트를 활성화 시킵니다;spin_is_locked
- 특정spinlock
의 상태를 리턴합니다;그리고 그 외에 기타등등.
spin_lock_init
매크로의 구현을 들여다 봅시다. 앞서 썼듯, 이것 등의 매크로는 include/linux/spinlock.h 헤더 파일에 있으며 spin_lock_init
매크로는 아래와 같습니다:
보듯이, spin_lock_init
매크로는 spinlock
을 받아서 두개의 연산을 수행합니다: 해당 spinlock
을 체크하고 raw_spin_lock_init
을 수행합니다. spinlock_check
의 구현은 상당히 간단한데, 이 함수는 단지 주어진 spinlock
의 raw_spinlock_t
를 리턴해서 우리가 정확히 평범한
raw spinlock 을 가졌음을 확신할 수 있게 합니다:
raw_spin_lock_init
매크로입니다:
이 매크로는 __RAW_SPIN_LOCK_UNLOCKED
값을 주어진 spinlock
의 raw_spinlock_t
에 저장합니다. __RAW_SPIN_LOCK_UNLOCKED
매크로의 이름에서 유추할 수 있듯이, 이 매크로는 주어진 spinlock
을 초기화 하고 이를 해제된
상태로 설정합니다. 이 매크로는 include/linu/spinlock_types.h 헤더 파일에 있으며 다음 매크로로 확장됩니다:
앞에서 설명했듯, 우린 동기화 기본 기능의 디버깅에 관련된 것들은 고려하지 않겠습니다. 이 경우 우린 SPIN_DEBUG_INIT
과 SPIN_DEP_MAP_INIT
매크로를 무시합니다. 따라서 __RAW_SPINLOCK_UNLOCKED
매크로는 아래와 같이 확장됩니다:
여기서 __ARCH_SPIN_LOCK_UNLOCKED
는 x86_64 에서 아래와 같습니다:
따라서, spin_lock_init
매크로의 확장 후에는, 주어진 spinlock
이 초기화 되고 그 상태는 해제됨
이 됩니다.
이제 우리는 spinlock
을 어떻게 초기화 하는지 알았으니, 리눅스 커널이 spinlock
을 조정하기 위해 제공하는 API 를 알아봅시다. 첫번째는 스핀락을 획득
할 수 있게 하는 함수입니다:
이 raw_spin_lock
매크로는 같은 헤더파일 내에 정의되어 있으며 _raw_spin_lock
으로 확장됩니다:
_raw_spin_lock
은 CONFIG_SMP
옵션이 설정되어 있는지 그리고 CONFIG_INLINE_SPIN_LOCK
옵션이 설정되어 있는지에 종속적으로 정의되어 있습니다. 만약 SMP 이 비활성화 되어 있다면, _raw_spin_lock
은 include/linux/spinlock_api_up.h 헤더 파일에 정의되어 있습니다:
SMP 가 활성화 되어 있고 CONFIG_INLINE_SPIN_LOCK
이 설정되어 있다면, include/linux/spinlock_api_smp.h 헤더 파일에 다음과 같이 정의되어 있습니다:
만약 SMP 가 활성화 되어 있고 CONFIG_INLINE_SPIN_LOCK
이 설정되어 있지 않다면, kernel/locking/spinlock.c 에 다음과 같이 정의되어 있습니다:
여기선 뒤쪽의 _raw_spin_lock
형태를 고려하겠습니다. __raw_spin_lock
함수는 아래와 같습니다:
볼 수 있듯, 여기선 먼저 include/linux/preempt.h 의 preempt_disable
매크로를 호출해서 (더 자세한 건 리눅스 커널 초기화 프로세스 챕터의 아홉번째 part 를 참고하세요) preemption 을 불능화 시킵니다. 이 spinlock
을 해제할 때 preemption 은 다시 활성화 될겁니다:
락을 잡기 위해 spin 하고 있는 사이에 다른 프로세스가 이 프로세스를 preempt 하는걸 막기 위해 이걸 해야 합니다. spin_acquire
매크로는 다른 연결을 통해 다음과 같이 확장됩니다:
이 lock_acquire
함수는:
앞에서 이야기했듯 디버깅이나 트레이싱에 관련된 것들은 다루지 않겠습니다. lock_acquire
함수의 중요 포인트는 raw_local_irq_save
매크로를 호출함으로써 하드웨어 인터럽트를 불능화 시키는 것으로, 주어진 spinlock 은 활성화 된 하드웨어 인터럽트에서 획득될 수도 있기 때문입니다. 이런 방법으로 이 프로세스는 preempt 되지 않게 됩니다. lock_acquire
함수의 마지막에서 raw_local_irq_restore
매크로를 통해 하드웨어 인터럽트를 다시 활성화 시킴을 알아두시기 바랍니다. 짐작했겠지만, __lock_acquire
함수의 주요 부분은 kernel/locking/lockdep.c 소스 코드 파일에 있습니다.
이 __lock_acquire
함수는 좀 커 보입니다. 우린 이 함수가 무슨 일을 하는지 이해하려 노력해 보겠지만, 여기서는 아닙니다. 사실 이 함수는 리눅스 커널 lock validator 와 연관되어 있으며 그건 이 파트의 주제가 아닙니다. 다시 __raw_spin_lock
함수로 돌아가서, 결국 아래 정의를 보게 됩니다:
이 LOCK_CONTENDED
매크로는 include/linux/lockdep.h 헤더 파일에 정의되어 있는데 주어진 spinlock
을 가지고 특정 함수를 호출할 뿐입니다:
우리의 경우, 이 lock
은 include/linux/spinlock.h 의 do_raw_spin_lock
함수이고 _lock
은 주어진 raw_spinlock_t
입니다:
여기서의 __acquire
는 그저 Sparse 에 연관된 매크로이고 우린 지금은 여기엔 관심 없습니다. arch_spin_lock
매크로는 include/asm-generic/qspinlock.h 헤더 파일에 다음과 같이 정의되어 있습니다:
이 파트는 여기서 멈춥니다. 다음 파트에서, 우린 queued spinlock 이 어떻게 동작하는지 알아보고 관련된 컨셉들을 알아봅니다.
결론
이 섹션은 리눅스 커널의 동기화 기본 기능에 대해 다루는 첫번째 파트를 마칩니다. 이 파트에서, 우린 리눅스 커널에 의해 제공되는 첫번째 동기화 기본 기능인 spinlock
을 알아봤습니다. 다음 파트에서 우린 이 흥미로운 주제에 더 깊이 들어가보고 다른 동기화
관련된 것들을 알아보겠습니다.
질문이나 제안이 있다면, 제게 트위터 0xAX 로 연락 주시거나 email 을 보내주시거나 issue 를 만들어 주시기 바랍니다.
링크
Last updated