윈도우즈 시스템 프로그래밍 13장 - 커널 모드 동기화(Synchronization In Kernel Mode)
윈도우즈 시스템 프로그래밍이라는 책과 해당 책의 저자이신 윤성우님의 강의를 통해 공부한 내용을 정리하는 글입니다.
뮤텍스(Mutex) 기반의 동기화
뮤텍스 기반의 동기화 기법에서 열쇠로 비유할 수 있는 것이 뮤텍스 오브젝트이고, 아래 함수를 통해 만들어진다.
HANDLE CreateMutex (
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
// If the function fails, the return value is NULL.
⦁ lpMutexAttributes : 뮤텍스도 커널 오브젝트이므로 보안 속성을 통해 상속 여부를 결정한다.
⦁ bInitialOwner : 뮤텍스는 뮤텍스를 생성하는 쓰레드에게 기회를 먼저 줄 수 있다. FALSE를 전달할 경우 먼저 차지하는 쓰레드가 기회를 얻을 수 있고, TRUE를 전달할 경우 뮤텍스를 생성한 쓰레드가 먼저 기회를 얻을 수도 있다.
⦁ lpName : 뮤텍스에 이름을 붙여주기 위한 인자이다. 이름은 널문자로 끝나는 문자열로 지정하면 된다. 이름을 주었을 때 생성되는 뮤텍스를 이름있는 뮤텍스(Named Mutex)라고 한다.
뮤텍스는 열쇠에 비유되므로, 누군가가 열쇠를 취득했을 때 Non-Signaled 상태가 되고, 취득한 열쇠를 반환했을 때 Signaled 상태가 되는 특성을 이용해 동기화를 한다.
즉, 누군가에 의해 획득이 가능할 때 Signaled 상태가 된다.
그러므로 WaitForSingleObject 함수를 임계 영역 진입을 위한 뮤텍스 획득 용도로 사용이 가능하다.반환할 때는 아래 함수를 사용한다.
BOOL ReleaseMutex (
HANDLE hMutex
);
// If the function fails, the return value is zero.
⦁ hMutex : 반환할 뮤텍스의 핸들을 인자로 전달한다. Non-Signaled 상태에 있는 뮤텍스 오브젝트는 Signaled 상태로 된다.
WaitForSingleObject 함수는 커널 오브젝트가 Signaled 상태가 되어 반환할 경우, 해당 커널 오브젝트의 상태를 Non-Signaled 상태로 변경하므로 다른 쓰레드들은 임계 영역으로의 진입이 제한된다.
임계 영역에서 나오면서 ReleaseMutex 함수를 호출해서 Signaled 상태(다른 누군가에게 획득 가능한 상태)가 되어서 쓰레드 진입을 허용한다.
리소스 해제는 CloseHandle로 처리하면 된다.
⦁ CriticalSectionSyncMutex.cpp
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#include <process.h>
#define NUM_OF_GATE 6
LONG gTotalCount = 0;
HANDLE hMutex;
void IncreaseCount()
{
WaitForSingleObject(hMutex, INFINITE);
gTotalCount++;
ReleaseMutex(hMutex);
}
unsigned int WINAPI ThreadProc(LPVOID lpParam)
{
for (DWORD i = 0; i < 1000; i++)
{
IncreaseCount();
}
return 0;
}
int _tmain(int argc, TCHAR** argv)
{
DWORD dwThreadId[NUM_OF_GATE];
HANDLE hThread[NUM_OF_GATE];
hMutex = CreateMutex(NULL, FALSE, NULL);
if (hMutex == NULL)
{
_tprintf(_T("CreateMutex error : %d \n"), GetLastError());
}
for (DWORD i = 0; i < NUM_OF_GATE; i++)
{
hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, NULL, CREATE_SUSPENDED, (unsigned*)&dwThreadId[1]);
if (hThread[i] == NULL)
{
_tprintf(_T("Thread creation fault! \n"));
return -1;
}
}
for (DWORD i = 0; i < NUM_OF_GATE; i++)
{
ResumeThread(hThread[i]);
}
WaitForMultipleObjects(NUM_OF_GATE, hThread, TRUE, INFINITE);
_tprintf(_T("total count: %d \n"), gTotalCount);
for (DWORD i = 0; i < NUM_OF_GATE; i++)
{
CloseHandle(hThread[i]);
}
CloseHandle(hMutex);
return 0;
}
이 경우에는 WaitForSingleObject 함수를 래핑(Wrapping)해서 사용하면 코드의 의미를 명확히 할 수 있다.
DWORD AcquireMutex(HANDLE mutex)
{
return WaitForSingleObject(mutex,INFINITE);
}
세마포어(Semaphore) 기반의 동기화
⦁ 세마포어에는 카운트 기능이 존재한다. 즉, 임계 영역에 접근 가능한 쓰레드 개수를 조절하는 기능이 있다.
⦁ 임계 영역의 접근 허용 쓰레드 개수를 하나로 제한하기 위해 사용되는 세마포어를 가리켜 바이너리 세마포어라고 한다. 그렇다면 바이너리 세마포어는 뮤텍스와 동일한 기능을 제공한다.
⦁ 세마포어를 생성하는 함수 CraeteSemaphore
HANDLE CreateSemaphore (
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
);
//If the function fails, the return value is NULL.
⦁ lpSemaphoreAttributes : 보안 속성을 지정. 상속을 결정 짓는다.
⦁ lInitialCount : 세마포어는 이 값을 기반으로 임계 영역에 접근 가능한 쓰레드 수를 제한한다.
⦁ lMaximumCount : 세마포어가 지닐 수 있는 값의 최대 크기를 지정한다. 1일 경우 바이너리 세마포어가 구성된다. lInitialCount 보다 커야한다.
⦁ lpName : 세마포어에 이름을 붙이기 위해 사용된다.
lInitialCount에 의해 초기 카운트가 결정된다. 0인 경우 Non-Signaled 상태가 되고, 1이상일 경우 Signaled 상태가 된다.
세마포어의 핸들을 인자로 전달하면서 WaitForSingleObject 함수를 호출할 경우, 그 값이 하나씩 감소하며 함수를 반환한다.
즉, 세마포어를 생성할 때 초기 카운트를 10으로 지정하면, WaitForSingleObject 함수가 총 열 번 호출될 때까지 카운트가 하나씩 감소하며 함수를 반환한다. 열한 번째 호출되면 세마포어 카운트가 0이므로 블로킹 상태가 된다.
세마포어는 임계 영역에 접근하는 열쇠를 여러 개 둘 수 있다. 두 번째 인자가 열쇠의 개수이다.
즉, 세마포어 카운트가 열쇠의 개수이다.
임계 영역을 빠져나오면 열쇠를 반환해야 하므로 ReleaseSemaphore 함수를 호출해야한다.
BOOL ReleaseSemaphore (
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
//If the function fails, the return value is zero.
⦁ hSemaphore : 반환할 세마포어의 핸들
⦁ lReleaseCount : 세마포어 카운트 값을 증가시키는 값, 특별한 경우가 아니면 1을 전달. 최대 카운트 값을 넘어서 증가시킬 것을 요구하면 FALSE가 반환된다.
⦁ lpPreviousCount : 변경되기 전 세마포어 카운트 값(이 함수 호출 전의 값)을 저장할 변수 지정, 필요 없으면 NULL을 전달
⦁ MyongDongKyoJa.cpp
#include <stdio.h>
#include <tchar.h>
#include <time.h>
#include <windows.h>
#include <process.h>
#define NUM_OF_CUSTOMER 50
#define RANGE_MIN 10
#define RANGE_MAX (30-RANGE_MIN)
#define TABLE_CNT 10
HANDLE hSemaphore;
DWORD randTimeArr[50];
void TakeMeal(DWORD time)
{
WaitForSingleObject(hSemaphore, INFINITE);
_tprintf(_T("Enter Customer %d~ \n"), GetCurrentThreadId());
_tprintf(_T("Customer %d having launch~ \n"), GetCurrentThreadId());
Sleep(1000 * time);
ReleaseSemaphore(hSemaphore, 1, NULL);
_tprintf(_T("Out Customer %d~ \n\n"), GetCurrentThreadId());
}
unsigned int WINAPI ThreadProc(LPVOID lpParam)
{
TakeMeal((DWORD)lpParam);
return 0;
}
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadIDs[NUM_OF_CUSTOMER];
HANDLE hThreads[NUM_OF_CUSTOMER];
srand((unsigned)time(NULL));
for (int i = 0; i < NUM_OF_CUSTOMER; i++)
{
randTimeArr[i] = (DWORD)(((double)rand() / (double)RAND_MAX * RANGE_MAX + RANGE_MIN));
}
hSemaphore = CreateSemaphore(NULL, TABLE_CNT, TABLE_CNT, NULL);
if (hSemaphore == NULL)
{
_tprintf(_T("CreateSemaphore error : %d \n"), GetLastError());
}
for (int i = 0; i < NUM_OF_CUSTOMER; i++)
{
hThreads[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, (void*)randTimeArr[i], CREATE_SUSPENDED, (unsigned*)&dwThreadIDs[i]);
if (hThreads[i] == NULL)
{
_tprintf(_T("Thread creation fault! \n"));
return -1;
}
}
for (int i = 0; i < NUM_OF_CUSTOMER; i++)
{
ResumeThread(hThreads[i]);
}
WaitForMultipleObjects(NUM_OF_CUSTOMER, hThreads, TRUE, INFINITE);
_tprintf(_T("----END---------\n"));
for (int i = 0; i < NUM_OF_CUSTOMER; i++)
{
CloseHandle(hThreads[i]);
}
CloseHandle(hSemaphore);
return 0;
}
⦁ 시뮬레이션 제한요소
테이블이 총 10개이고, 동시에 총 10명의 손님만 받을 수 있다.
오늘 점심시간에 식사하러 올 예상 손님 수는 총 50명이다.
각 손님들이 식사하는 시간은 대락 10분~30분이다.
이름있는 뮤텍스(Named Mutex) 기반의 프로세스 동기화
뮤텍스는 커널 오브젝트이므로 커널이 관리하는 오브젝트이다. 그러므로 프로세스 A의 요청으로 생성되었다 한들 프로세스 B도 접근이 가능하다. 하지만 핸들 테이블은 프로세스 별로 독립적이기 때문에 B 프로세스의 핸들 테이블에는 뮤텍스의 정보가 없으므로 접근이 불가능하다. 이럴때 뮤텍스에 이름을 붙여주면 핸들 정보를 얻을 수 있으므로 문제가 해결된다.
⦁ NamedMutex.cpp
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#include <process.h>
HANDLE hMutex;
DWORD dwWaitResult;
void ProcessBaseCriticalSection()
{
dwWaitResult = WaitForSingleObject(hMutex, INFINITE);
switch (dwWaitResult)
{
case WAIT_OBJECT_0:
_tprintf(_T("thread got mutex ! \n"));
break;
case WAIT_TIMEOUT:
_tprintf(_T("timer expired ! \n"));
return;
case WAIT_ABANDONED:
return;
}
for (DWORD i = 0; i < 5; i++)
{
_tprintf(_T("Thread Running ! \n"));
Sleep(10000);
}
ReleaseMutex(hMutex);
}
int _tmain(int argc, TCHAR* argv[])
{
#if 0
hMutex = CreateMutex(NULL, FALSE, _T("NamedMutex"));
#else
hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, _T("NamedMutex"));
#endif
if (hMutex == NULL)
{
_tprintf(_T("CreateMutex error : %d \n"), GetLastError());
return - 1;
}
ProcessBaseCriticalSection();
CloseHandle(hMutex);
return 0;
}
#if 1로 바꿔서 CreateMutex.exe를 만들고, #if 0인 코드를 OpenMutex.exe로 만들어 전자를 먼저 실행시킨 뒤, 후자를 실행시켜서 실행 결과를 확인하자.
CreateMutex의 뮤텍스가 반환되고 나면 OpenMutex가 이어서 실행을 한다. 서로 다른 프로세스 영역에 존재하는 쓰레드들이 동기화가 가능한 것을 확인한 것이다.
⦁ OpenMutex
HANDLE OpenMutex (
DWORD dwDesireAccess,
BOOL bInheritHandle,
LPCTSTR lpName
);
//If the function fails, the return value is NULL.
⦁ dwDesireAccess : 이름있는 뮤텍스로의 접근권한 지정, MUTEX_ALL_ACCESS를 전달해서 권한을 요청
⦁ bInheritHandle : 핸들의 상속 유무를 결정
⦁ lpName : 얻고자 하는 핸들 정보의 커널 오브젝트 이름을 전달. 일치하는 이름의 뮤텍스가 존재하면 해당 뮤텍스의 핸들이 반환된다. 핸들 테이블에 이 정보도 추가된다.
뮤텍스의 소유와 WAIT_ABANDONED
⦁ 뮤텍스는 획득한 쓰레드가 직접 반환하는 것이 원칙이다. 그러나 세마포어와 그 외의 동기화 오브젝트들은 다른 쓰레드가 대신 반환해도 문제가 되지 않는다.
⦁ MUTEX_WAIT_ABANDONED.cpp
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#include <process.h>
LONG gTotalCount = 0;
HANDLE hMutex;
unsigned int WINAPI IncreaseCountOne(LPVOID lpParam)
{
WaitForSingleObject(hMutex, INFINITE);
gTotalCount++;
return 0;
}
unsigned int WINAPI IncreaseCountTwo(LPVOID lpParam)
{
DWORD dwWaitResult = 0;
dwWaitResult = WaitForSingleObject(hMutex, INFINITE);
switch (dwWaitResult)
{
case WAIT_OBJECT_0:
ReleaseMutex(hMutex);
break;
case WAIT_ABANDONED:
_tprintf(_T("WAIT_ABANDONED \n"));
break;
}
gTotalCount++;
ReleaseMutex(hMutex);
return 0;
}
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadIDOne;
DWORD dwThreadIDTwo;
HANDLE hThreadOne;
HANDLE hThreadTwo;
hMutex = CreateMutex(NULL, FALSE, NULL);
if (hMutex == NULL)
_tprintf(_T("CreateMutex error: %d\n"), GetLastError());
hThreadOne = (HANDLE)_beginthreadex(NULL, 0, IncreaseCountOne, NULL, 0, (unsigned*)&dwThreadIDOne);
hThreadTwo = (HANDLE)_beginthreadex(NULL, 0, IncreaseCountTwo, NULL, 0, (unsigned*)&dwThreadIDTwo);
Sleep(1000);
ResumeThread(hThreadTwo);
WaitForSingleObject(hThreadTwo, INFINITE);
_tprintf(_T("total count: %d \n"), gTotalCount);
CloseHandle(hThreadOne);
CloseHandle(hThreadTwo);
CloseHandle(hMutex);
return 0;
}
⦁ 실행결과
WAIT_ABANDONED가 반환되었다고 해서 오류가 발생한 것은 아니다. Windows가 뮤텍스 소유 관련해서 문제가 발생한 것을 적절히 처리해 주었기 때문이다. WAIT_ABANDONED을 얻은 쓰레드가 뮤텍스를 소유하게 되었다.
이 코드는 대부분 디버그 관련해서 삽입되곤 한다.