spinlock은 선점을 비활성화하고, 필요에 따라서 interrupt나 bottom half도 비활성화를 하므로 spinlock은 오랫동안 락을 들고있어선 안된다. 오랫동안 스케줄링과 인터럽트가 처리되지 않는다고 생각하면 정말 끔찍하다. spinlock은 그 특성상 락을 획득한 동안에는 sleep이 불가능하다. 이러한 상황에서는 spinlock 대신, "sleeping lock"인 semaphore를 사용할 수 있다.
개념
semaphore란
spinlock은 한 번에 하나의 스레드만 락을 획득할 수 있다. 따라서 상호배제(mutual exclusion, 한 번에 하나의 스레드만 critical section에 들어가는 것)이 가능하다. 이와 달리 semaphore는 그 구조상, 여러 개의 스레드가 critial section에 들어갈 수 있다. 이 개수를 count라고 하자. count > 0이면, 락을 획득할 수 있는 상태이며, count == 0이면, 다른 스레드가 락을 반환할 때까지 대기해야 한다. 락을 획득하여 count를 감소하는 것을 P 연산 (Proberen), 락을 반환하여 count를 증가하는 것을 V 연산 (Verhogen)이라고 한다.
counting semaphore vs binary semaphore
count == 1인 semaphore를 binary semaphore라고 하고, count > 1인 semaphore를 counting semaphore라고 부른다.
자, 근데 상식적으로 생각해봤을 때 count > 1인 semaphore는 mutual exclusion이 불가능하다. 아니, 두 개의 스레드가 동시에 critial section 안에서 실행중이라면, 락을 사용하는 이유가 없지 않나? 그래서 나도 아직 이걸 왜 사용하는지는 잘 모르겠다. 적어도 mutual exclusion을 위해서 사용하지는 않는다.
binary semaphore는 한 번에 하나의 스레드만 critial section에 진입할 수 있다. 따라서 우리가 원하는 동기화 매커니즘을 구현할 수 있다. binary semaphore를 MUTEX (MUTual EXclusion)이라고 부르기도 한다.
semaphore API
semaphore의 구조와 할당
/* Please don't access any members of this structure directly */
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
semaphore 구조체에는 아까 우리가 이야기한 count와, 락을 기다리는 wait_list, 그리고 semaphore 구조체 자체를 보호하기 위한 spinlock이 존재한다.
/* static allocation (count == 1) */
DEFINE_SEMAPHORE(name);
/* dynamic allocation */
semaphore sem;
int count = 1;
sema_init(&sem, count);
down, down_killable, down_interruptible, down_trylock, down_timeout
리눅스 커널에서 P 연산에 대응되는 함수가 바로 down이다. down은 현재 count가 0이라면 락을 획득할 때까지 대기하며, count > 0이라면 락을 획득하고 count를 감소한다. 그런데 down 함수를 사용하면 TASK_UNINTERRUPTIBLE 상태가 되기 때문에, 시그널 등이 와도 반응하지 않는다. 그래서 죽일 수 없는 프로세스가 만들어지는데, 이를 방지하기 위해서 일반적으로 down_interruptible 또는 down_killable을 사용한다. 그 외에 down_trylock은, 현재 count > 0인 경우에만 락을 획득하며 count == 0이라면 락을 기다리지 않고 바로 함수를 리턴한다. down_timeout은 락을 기다리되 jiffies 동안만 대기한다.
자세한 설명은 kernel/locking/semaphore.c를 참고하자. 정말 문서화가 잘 되어있다.
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_killable(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
int down_timeout(struct semaphore *sem, long jiffies);
up
V 연산에 대응되는 함수는 up이다. down 함수는 변종이 많지만, up 함수는 단 하나뿐이다. up 함수는 획득한 락을 반환한다. up 함수는 락을 획득한 프로세스가 아니더라도 호출할 수 있다. (up를 임의로 호출해서 count를 증가시킬 수 있다.)
void up(struct semaphore *sem)
간단한 예시
DEFINE_SEMAPHORE(sem);
down_interruptible(&sem);
/* critical section */
up(&sem);
'Kernel > Locking Primitives' 카테고리의 다른 글
[Linux Kernel] RCU (Read-Copy-Update) (1) | 2021.11.05 |
---|---|
[Linux Kernel] semaphore (2) - semaphore 분석 (0) | 2021.04.23 |
[kernel/locking] spinlock (2) - Test And Set, Ticket, ABQL, MCS (0) | 2021.03.28 |
[kernel/locking] spinlock (1) - spinlock 사용법 (5) | 2021.03.27 |
락 (lock)이란 무엇인가 (3) | 2021.03.16 |
댓글