앞선 글에서 lock이란 무엇이고, lock의 필요성에 대해서 알아보았다. 이번 글에서는 가장 기본적인 spinlock을 리눅스 커널에서 어떻게 구현했는지 알아볼 것이다. 우선 관련된 개념을 몇 가지 살펴보자. 참고로, 이 문서에서 다루는 spinlock은 커널에서 사용하는 spinlock에 대해 다루고 있다. userspace에서의 spinlock은 pthread spinlock을 사용해야 한다.
자료구조
spinlock은 include/linux/spinlock_types.h에 spinlock_t로 정의되어있다. 한 번 살펴보자.
/* include/linux/spinlock_types.h */
typedef struct spinlock {
union {
struct raw_spinlock rlock;
/* 디버깅과 관련된 부분 */
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;
#ifdef ~ #endif 부분은 config에 따라서, 디버깅 관련 기능을 활성화한 경우에 추가되는 코드이다. 그 부분을 제외하면 spinlock_t는 raw_spinlock_t만을 갖고 있다.
/* include/linux/spinlock_types.h */
typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
/* 디버깅과 관련된 부분 */
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned int magic, owner_cpu;
void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} raw_spinlock_t;
raw_spinlock_t는 arch_spinlock_t를 갖고 있으며 이는 아키텍처에 종속적인 부분이다.
선점 비활성화
linux kernel에서 preempt_disable은 선점을 비활성화하는 함수이다. 프로세스 선점이 가능하면 lock을 들고있는 도중에 다른 프로세스가 실행되어 critical section에 동시에 여러 프로세스가 접근할 수 있게 된다. 정확히 말하면 프로세스 A가 spinlock을 획득하고, 휴면 상태로 전환한 뒤 프로세스 B가 실행되면 A로 다시 돌아왔을 때 데이터의 무결성을 보장할 수 없다. spinlock을 획득하면 내부적으로 preempt_disable을 호출해서 선점을 비활성화한다.
인터럽트 비활성화
spinlock으로 보호하는 자원이 인터럽트 핸들러에서도 사용되는 경우, 인터럽트를 비활성화해야만 한다. 선점을 비활성화해도 여전히 인터럽트는 발생할 수 있다. spinlock을 획득한 상태에서 인터럽트가 발생하고, 인터럽트 핸들러에서 해당 lock이 필요한 경우에는 데드락이 발생하게 된다. 이를 위해 커널 단에서 spin_lock_irqsave, spin_lock_irqrestore 함수를 통해 인터럽트를 비활성화하고, spinlock을 획득하는 함수를 지원한다. 만약 인터럽트 핸들러에서 절대로 해당 spinlock을 사용하지 않는단걸 알고 있다면 spin_lock, spin_unlock을 사용해도 된다. 이 경우엔 인터럽트가 비활성화되지 않는다.
softirq 비활성화
spinlock으로 보호하는 자원을 softirq에서 사용하는 경우에는 spin_lock_bh / spin_unlock_bh 함수를 사용할 수 있다.
사용법
spinlock을 사용한다면 다음과 같이 사용할 수 있다.
#include <linux/spinlock.h>
/* 정적으로 spinlock 정의 */
DEFINE_SPINLOCK(lock);
spin_lock(&lock);
/* Critical Section! */
spin_unlock(&lock);
spinlock을 얻으면서 인터럽트도 비활성화시키려면 spin_lock_irq / spin_unlock_irq를 사용할 수 있다.
#include <linux/spinlock.h>
/* 정적으로 spinlock 정의 */
DEFINE_SPINLOCK(lock);
spin_lock_irq(&lock);
/* Critical Section! */
spin_unlock_irq(&lock);
그런데, 인터럽트를 비활성화 하려면 현재 인터럽트가 활성화되어있다는 걸 알아야한다. 따라서 현재 인터럽트 활성화 여부에 관계 없이, 활성화된 경우에만 인터럽트를 비활성화하려면 spin_lock_irqsave / spin_unlock_irqrestore 를 사용하면 된다.
#include <linux/spinlock.h>
/* 정적으로 spinlock 정의 */
DEFINE_SPINLOCK(lock);
unsigned long flags; // 현재 상태에 대한 플래그
spin_lock_irqsave(&lock, flags);
/* Critical Section! */
spin_unlock_irqrestore(&lock, flags);
spinlock의 장단점
장점
인터럽트 컨텍스트에서도 사용할 수 있다. 인터럽트 컨텍스트에선 휴면이 불가능하므로 뮤텍스, 세마포어 같은 휴면 가능한 락은 사용할 수 없다.
단점
spinlock을 들고있는 동안은 선점, 인터럽트를 비활성화하므로 오랫동안 잡고있으면 안된다. 단, 휴면을 하고, 다시 돌아오는 시간 (context switching 2번)이 락을 잡고있는 시간보다 길다면 spinlock을 사용하는게 좋다.
참고자료
'Kernel > Locking Primitives' 카테고리의 다른 글
[Linux Kernel] RCU (Read-Copy-Update) (1) | 2021.11.05 |
---|---|
[Linux Kernel] semaphore (2) - semaphore 분석 (0) | 2021.04.23 |
[Linux Kernel] semaphore (1) - semaphore의 개념과 사용법 (1) | 2021.04.22 |
[kernel/locking] spinlock (2) - Test And Set, Ticket, ABQL, MCS (0) | 2021.03.28 |
락 (lock)이란 무엇인가 (3) | 2021.03.16 |
댓글