Windows 커널 & 드라이버 시리즈 — 15화
스핀락과 동기화 기법 — 멀티코어 환경에서 안전하게 데이터 공유하기
오늘날 서버는 수십 개의 CPU 코어가 동시에 돌아가고, 드라이버 코드는 여러 코어에서 동시에 실행될 수 있어요. 그 말은 "공유 데이터를 동시에 두 코어가 건드리는" 상황이 언제든 생길 수 있다는 뜻이에요. 이번 화에서는 커널 레벨 동기화 기법들을 살펴보겠습니다.
왜 동기화가 필요한가
아래 코드를 보면 문제가 보일 거예요:
// 전역 카운터 (공유 데이터) LONG g_RequestCount = 0; // 두 코어가 동시에 이 코드를 실행하면? NTSTATUS DispatchRead(PDEVICE_OBJECT DevObj, PIRP Irp) { g_RequestCount++; // 1) 읽기 → 2) +1 → 3) 쓰기 (3단계!) // ... }g_RequestCount++는 소스 코드 한 줄이지만 어셈블리로는 읽기→증가→쓰기의 세 단계예요. 두 코어가 동시에 "읽기"를 실행하고 둘 다 같은 값을 읽으면, 두 번 증가했어야 할 카운터가 한 번만 증가하는 버그가 생겨요. 이걸 경쟁 조건(Race Condition)이라고 합니다.
스핀락 (KSPIN_LOCK)
스핀락은 DISPATCH_LEVEL 이상에서도 사용할 수 있는 동기화 기법이에요. 락을 얻지 못한 쪽은 계속 "됐어?" 하고 확인하면서 대기(spin)합니다.
// 드라이버 전역 또는 DeviceExtension KSPIN_LOCK g_Lock; LONG g_RequestCount = 0; // 초기화 (DriverEntry 또는 DeviceExtension 초기화 시) KeInitializeSpinLock(&g_Lock); // 사용 패턴 NTSTATUS DispatchRead(PDEVICE_OBJECT DevObj, PIRP Irp) { KIRQL oldIrql; // 락 획득 (IRQL이 DISPATCH_LEVEL로 올라감) KeAcquireSpinLock(&g_Lock, &oldIrql); g_RequestCount++; // 이제 안전! // 락 해제 (IRQL이 원래대로 복귀) KeReleaseSpinLock(&g_Lock, oldIrql); // ... }
⚠️ 스핀락 보유 중에는 Paged Pool 접근 금지
KeAcquireSpinLock이 IRQL을 DISPATCH_LEVEL로 올리기 때문에, 락을 보유한 동안에는 14화에서 배운 DISPATCH_LEVEL 제약이 그대로 적용돼요. 가능한 한 짧게 보유하고, 그 사이에 복잡한 작업은 하지 마세요.
IRQL이 이미 DISPATCH_LEVEL 이상이면?
DPC 루틴처럼 이미 DISPATCH_LEVEL에서 실행 중이라면, IRQL 조작 없이 SpinLock만 획득하는 버전을 씁니다:
// 이미 DISPATCH_LEVEL 이상인 경우 KeAcquireSpinLockAtDpcLevel(&g_Lock); // ... 짧은 임계 구역 KeReleaseSpinLockFromDpcLevel(&g_Lock);단순 카운터는 인터락 연산으로
카운터 하나를 보호하려고 스핀락을 쓰는 건 좀 무거워요. 원자적 연산(Interlocked)을 쓰면 더 가볍게 해결할 수 있어요:
LONG g_RequestCount = 0; // 원자적 증가 (락 불필요!) InterlockedIncrement(&g_RequestCount); // 원자적 감소 InterlockedDecrement(&g_RequestCount); // 원자적 교환 LONG old = InterlockedExchange(&g_RequestCount, 0); // 원자적 비교-교환 (CAS) LONG expected = 5; InterlockedCompareExchange(&g_RequestCount, 10, expected); // g_RequestCount == 5이면 10으로 바꾸고, 아니면 그대로 둠PASSIVE_LEVEL에서 쓸 수 있는 동기화 기법
스핀락은 대기 중에 CPU를 계속 점유하기 때문에 비효율적이에요. PASSIVE_LEVEL(일반 드라이버 루틴)에서는 더 효율적인 방법들을 쓸 수 있습니다.
KMUTEX — 상호 배제
KMUTEX g_Mutex; KeInitializeMutex(&g_Mutex, 0); // 락 획득 (대기 가능 — PASSIVE_LEVEL에서만!) KeWaitForSingleObject(&g_Mutex, Executive, KernelMode, FALSE, NULL); // ... 임계 구역 // 락 해제 KeReleaseMutex(&g_Mutex, FALSE);KEVENT — 신호 메커니즘
KEVENT g_Event; // 알림 이벤트 (모든 대기 스레드를 깨움) vs 동기 이벤트 (하나만 깨움) KeInitializeEvent(&g_Event, NotificationEvent, FALSE); // 이벤트 대기 (PASSIVE_LEVEL에서) KeWaitForSingleObject(&g_Event, Executive, KernelMode, FALSE, NULL); // 이벤트 발생시키기 (다른 스레드/DPC에서) KeSetEvent(&g_Event, IO_NO_INCREMENT, FALSE); // 이벤트 리셋 KeClearEvent(&g_Event);ERESOURCE — 읽기/쓰기 락
읽기는 여럿이 동시에 해도 되고, 쓰기만 단독으로 해야 할 때 씁니다. 데이터베이스의 RW 락과 같은 개념이에요:
ERESOURCE g_Resource; ExInitializeResourceLite(&g_Resource); // 읽기 락 (여러 스레드 동시 획득 가능) ExAcquireResourceSharedLite(&g_Resource, TRUE); // ... 읽기 작업 ExReleaseResourceLite(&g_Resource); // 쓰기 락 (단독 접근) ExAcquireResourceExclusiveLite(&g_Resource, TRUE); // ... 쓰기 작업 ExReleaseResourceLite(&g_Resource); // 정리 ExDeleteResourceLite(&g_Resource);
📌 ERESOURCE는 PASSIVE_LEVEL 전용
ERESOURCE는 내부적으로 대기 연산을 사용하므로 반드시 PASSIVE_LEVEL에서만 사용해야 해요. 읽기가 많고 쓰기가 드문 데이터(설정 값, 디바이스 목록 등)를 보호하는 데 이상적입니다.
동기화 기법 선택 가이드
| 상황 | 권장 기법 | 사용 가능 IRQL |
|---|---|---|
| 단순 카운터 증감 | InterlockedIncrement/Decrement | 모든 레벨 |
| 짧은 임계 구역, DPC에서 공유 | KSPIN_LOCK | ≤ DISPATCH_LEVEL |
| 상호 배제, 대기 가능 | KMUTEX | PASSIVE_LEVEL |
| 스레드 간 신호 전달 | KEVENT | PASSIVE_LEVEL (대기 시) |
| 읽기 많고 쓰기 드문 공유 데이터 | ERESOURCE | PASSIVE_LEVEL |
데드락 방지 원칙
- 여러 락을 획득해야 한다면 항상 같은 순서로 획득하세요. A→B 순서로 획득했으면, 어디서든 A→B 순서를 지켜야 해요. 반대로 하면 데드락이 생겨요.
- 락을 보유한 채로 외부 코드(콜백 등)를 호출하지 마세요. 그 코드가 같은 락을 요청할 수도 있어요.
- 스핀락은 가능한 한 짧게 보유하세요. 복잡한 계산은 락 밖에서 하세요.
✅ 15화 요약
- KSPIN_LOCK: DISPATCH_LEVEL 이상에서 사용 가능. 대기 중 CPU를 점유(spin)합니다.
- Interlocked 연산: 단순 정수 연산에 락 없이 원자성을 보장합니다.
- KMUTEX, KEVENT, ERESOURCE: PASSIVE_LEVEL에서 사용. 스레드를 슬립 상태로 대기시킵니다.
- 데드락 방지를 위해 항상 같은 순서로 락을 획득하세요.
다음 화
16화 — 커널 메모리 풀 관리: NonPaged Pool vs Paged Pool, 올바른 할당과 해제 →
#SpinLock #KMUTEX #KEVENT #동기화 #RaceCondition
'Programming > 7. Device Driver' 카테고리의 다른 글
| 드라이버 개발 핵심 - 17화 MDL (0) | 2026.06.05 |
|---|---|
| 드라이버 개발 핵심 - 16화 커널 메모리 풀 관리 (0) | 2026.06.04 |
| 드라이버 개발 핵심 - 14화 IRQL 완전 정복 (0) | 2026.06.02 |
| 드라이버 개발 핵심 - 13화 IRP 디스패치 루틴 구현 (0) | 2026.06.01 |
| 드라이버 개발 핵심 - 11화 디바이스 오브젝트와 디바이스 스택 (0) | 2026.05.29 |