MSDN을 보면 시스템 전용이라고만 나와 있다.
개발자들은 사용하지 말라는 뜻으로 보인다.
그래도 궁금하므로 좀 더 알아봐야 겠다.
사실 Windows Internals 3장에도 이게 어떤 일을 하는지 나름대로 잘 설명되어 있다.
구글링해보면 더 자세한 설명들도 종종 나온다.
정리하자면...
일단 프로토타입은 아래와 같다.
1. KSPIN_LOCK_QUEUE_NUMBER에 대해
가장 먼저 눈에 들어오는 파라미터 타입 KSPIN_LOCK_QUEUE_NUMBER를 먼저 알아보자.
이건 WDK에 포함된 헤더파일에 다음과 같이 정의되어 있다.
정의된 이름들을 보면 어떤 경우에 사용하는 것인지 대충 감이 온다.
커널을 분석하다 보면 각각 상황에 맞게 위의 값들이 KeAcquireQueuedSpinLock() 함수의 파라미터로 전달되는 것을 볼 수 있다.
인덱스를 넘기는 이유는 커널이 이미 LockQueueMaimumLock만큼의 SpinLock을 들고 있기 때문이다.
SpinLock의 배열이므로 인덱스로 찾아서 사용하기만 하면 되는 것이다.
시스템 내부적인 목적으로 만들어 놓은 Lock들이므로 개발자들에게 사용하지 말라고 하는 것으로 보인다.
2. QueuedSpinLock에 대해
QueuedSpinLock은 SpinLock의 단점을 보완해 효율적으로 동작한다.
SpinLock의 단점이란 멀티 프로세서 환경에서 어떤 CPU가 SpinLock을 점유할 경우 다른 CPU들이 SpinLock을 체크하기 위해 동기적으로 특정 메모리(SpinLock)에 접근해 시스템 버스에 부하를 주는 현상을 말한다.
결론부터 간단히 말하면 QueuedSpinLock은 특정 메모리를 동기적으로 접근하지 않고 CPU가 각자 자기만의 영역을 체크하는 방식으로 동작하기 때문에 시스템 버스의 부하를 감소시킨다.
어떻게 이것이 가능한지 살펴보자.
QueuedSpinLock은 pLock, pNext라는 두 가지 속성을 가진다.
pLock은 SpinLock의 주소가 저장되고 pNext는 이 스핀락을 대기하는 다른 CPU의 QueuedSpinLock의 주소를 저장하는 링크를 의미한다.
다른 CPU의 QueuedSpinLock 주소라는 말을 이해하려면 CPU마다 각각 가지고 있는 PCR(Processor Control Region)에 LockQueueMaimumLock만큼의 QueuedSpinLock 배열이 정의되어 있다는 내용을 이해해야 한다.
[CPU1의 PCR 내부]
...
QueuedSpinLock[LockQueuePfnLock].pLock = &PfnLock;
QueuedSpinLock[LockQueuePfnLock].pNext = NULL;
QueuedSpinLock[LockQueueSystemSpaceLock].pLock = &SystemSpaceLock;
QueuedSpinLock[LockQueueSystemSpaceLock].pNext = NULL;
...
[CPU2의 PCR 내부]
...
QueuedSpinLock[LockQueuePfnLock].pLock = &PfnLock;
QueuedSpinLock[LockQueuePfnLock].pNext = NULL;
QueuedSpinLock[LockQueueSystemSpaceLock].pLock = &SystemSpaceLock;
QueuedSpinLock[LockQueueSystemSpaceLock].pNext = NULL;
...
이와 같이 CPU 마다 QueuedSpinLock에 대한 정보를 가지고 있는 것이다.
(설명의 편의를 위해 인덱스로 배열을 참조하는 부분은 생략한다)
1. CPU1이 KeAcquireQueuedSpinLock() 함수를 호출
- QueuedSpinLock 획득
2. CPU2가 KeAcquireQueuedSpinLock() 함수를 호출
- 이미 스핀락이 점유되어 있으므로 대기를 위해
- CPU2의 PCR->QueuedSpinLock[x].pLock 최하위 비트를 셋팅해 대기중임을 표시 (이 비트가 풀릴 때까지 무한 루프)
- CPU1의 PCR->QueuedSpinLock[x].pNext에 CPU2의 PCR->QueuedSpinLock[x] 주소를 큐잉
3. CPU1이 KeReleaseQueuedSpinLock() 함수를 호출
- 큐잉된 CPU2의 PCR->QueuedSpinLock[x].pLock을 찾아가 최하위 비트를 클리어(CPU2의 대기가 풀림)
- CPU1의 PCR->QueuedSpinLock[x].pNext를 클리어
즉, 어떤 CPU라도 QueuedSpinLock을 대기하는 경우는 자신의 PCR에 저장된 QueuedSpinLock[x].pLock 필드의 특정 비트만 체크하면서 스핀하고 있는 것이므로 메모리를 동기적으로 접근할 필요가 없어진다.
그리고 여러 CPU가 QueuedSpinLock을 요청하는 경우라도 pNext 필드에 링크로 연결되어 있으므로 먼저 요청한 CPU가 먼저 받아가는 합리적인 배분(FIFO)도 이루어 진다.
알고보니 재미있는 트릭이다. ^^