-
스핀락개발/컴퓨터사이언스 2018. 7. 23. 22:23
멀티프로세서 환경에서는 시스템에서 생성된 프로세스가 하드웨어에 설치된 CPU의 수만큼 동시에 실행된다. CPU에서 동작 중인 프로세스는 메모리 영역에 할당된 데이터중 연산이 필요한 값을 읽고 쓰는 작업을 수행하게 되는데 이때 여러 개의 프로세스가 같은 데이터 영역을 공유하게 되는 경우가 있다. 여러 개의 프로세스가 공통된 전역 변수의 값을 읽는 경우가 대표적인 예다.
데이터 값이 고정되고 모든 프로세스가 고정된 값을 읽기만 하면 별 다른 문제는 없다. 그런데 누군가가 쓰게 되는 경우부터 싱크 문제가 생긴다. A랑 B 프로세스가 있다고 해보자. A 프로세스는 C와 D의 데이터 값을 수정하게 되어있고, B 프로세스는 A프로세스보다 조금 늦게 생성되어 A 프로세스가 수정한 값을 읽도록 짜여져 있다. 싱글코어에선 A 프로세스 다음에 B 프로세스가 스케줄링돼 수정 작업과 읽는 작업이 순차적으로 이뤄진다. 그런데 멀티코어에서는 B프로세스가 A 프로세스의 작업이 끝나기 전에 스케줄링 될 수도 있다. 이런 경우에는 수정된 C, D 값을 읽지못하고 예전 값을 읽게 될수도 있다.
static __always_inline void spin_lock(spinlock_t *lock) static __always_inline void spin_unlock(spinlock_t *lock)
이런 경우를 막기위해 리눅스에서는 spin_lock이라는 API를 제공한다. spin_lock은 매개변수(spinlock_t)의 값을 이용해 다른 프로세스의 공유 영역에 대한 접근을 막을 수 있는 함수다. spin_lock을 부르면 매개 변수의 값을 보고 다른 프로세스의 점유 유무를 확인한다. 점유한 상태가 아니면 매개변수의 값을 점유 상태로 바꾸고 공유 영역에서 수행할 작업을 수행하고 그렇지 않으면 다른 프로세스가 놓을 때까지 (busy waiting으로)기다린다. 수행한 작업을 완료한 후 spin_unlock 함수를 통해 점유 상태를 취소한다. 취소 후에는 대기중이던 프로세스가 매개 변수의 값을 읽고 재점유한다.
다시 예제로 돌아와보자. A 프로세스가 값을 수정하기 전에 B 프로세스가 읽는 것을 막기 위해선 값을 수정하는 A 프로세스가 값을 수정하는 작업과 B 프로세스가 값을 읽는 작업 앞에 spin_lock을 걸어두면 된다. 수정 작업이 먼저 실행되기 때문에 A 프로세스가 먼저 lock을 획득하게 되고 중간에 읽기 작업을 수행하는 B 프로세스는 lock을 획득하지 못해 busy waiting 상태로 기다린다. 수정작업이 모두 끝난 A는 다른 프로세스가 접근 할 수 있도록 spin_unlock을 수행하게 되고 이 이후에 B 프로세스는 lock을 획득해 수정된 값을 읽을 수 있게 된다.
여기서 더 나아가 하드웨어에서 인터럽트가 들어온 상황을 생각해보자. 프로세스가 lock을 획득해서 작업하는 도중에 인터럽트가 들어오면 CPU는 하던일을 중단하고 인터럽트 서비스 루틴을 처리한다. 그런데 만약 인터럽트를 처리하는 작업중에서 인터럽트가 들어오기전 프로세스가 획득한 lock을 갖기 위해 spin_lock을 부르는 코드가 있다고 해보자. 인터럽트 서비스 루틴도 일반 프로세스와 마찬가지로 lock을 획득 할 수 있을 때까지 대기 상태에 있다. 그런데 lock을 놓아줘야하는 프로세스는 여전히 인터럽트에 걸린 상태에서 다시 스케줄링 되지 못하고 있다. 서로가 서로의 진행을 막고있는 셈이다. 이런 경우 프로세스는 교착 상태(Dead Lock)에 빠진다.
static __always_inline void spin_lock_irq(spinlock_t *lock) static __always_inline void spin_unlock_irq(spinlock_t *lock)
이를 해결하기 위해 리눅스에서는 lock을 획득한 상태에서는 cpu의 인터럽트를 막을 수 있는 함수를 제공한다. spin_lock_irq 함수를 사용하면 해당 lock을 획득하고 다시 놓을 때까지 프로세스의 인터럽트를 막는다. 이 시간 동안 들어오는 인터럽트는 pending 상태로 대기하고 있다가 spin_unlock_irq가 불리면 다시 재실행 된다.
그런데 spin_unlock_irq는 disable 되어있던 인터럽트를 모두 enable 시키는데 이러면 함수의 depth가 깊어지는 경우 문제가 발생 할 수 있다. 어떤 함수가 spin_lock_irq를 부르고 lock 내부 루틴을 타는 함수가 또 spin_lock_irq를 부르면 정상적으로 함수가 종료 될 때 spin_unlock_irq가 두번 불리게 될 것이다. 그런데 spin_unlock_irq는 예전에 인터럽트가 몇번 disable 됐는지 상관없이 모두다 enable하게돼 첫번째 spin_unlock_irq와 두번째 spin_unlock_irq가 호출되는 사이 지점은 인터럽트가 disable이 되지 않는다. 즉 spin_lock_irq를 못쓰고 spin_lock을 쓰는것과 마찬가지인 셈. 이 사이에 하드웨어 인터럽트가 들어와 똑같은 lock에 spin_lock을 걸면 예전처럼 deadlock이 걸린다.
#define spin_lock_irqsave(lock, flags) \ do { \ raw_spin_lock_irqsave(spinlock_check(lock), flags); \ } while (0) static __always_inline void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) { raw_spin_unlock_irqrestore(&lock->rlock, flags); }
이런 경우를 해결하고자 spin_lock_irqsave라는 함수가 있다. 잘 보면 이 함수는 매개변수로 flags라는 값을 하나 더 주는 것을 볼 수 있다. flag에다가 현재 interrupt의 상태를 보존해 놓는 것이다. 이 값을 활용해 더 안전하게 스핀락을 처리할 수 있도록 한다.
'개발 > 컴퓨터사이언스' 카테고리의 다른 글
Cgroup (Control Group) (0) 2018.09.15 CPU pinning과 taskset (0) 2018.08.27 eventfd (0) 2018.07.18 workqueue 사용법 (0) 2018.07.16 tasklet 사용법 (0) 2018.06.17