모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!
# 유저모드와 커널모드
- 윈도우 운영체제의 연산방식(프로그램 실행방식)을 가리켜 "이중 모드연산" 방식이라 함.
- 유저모드 : 응용 프로그램이 실행되는 기본모드. 물리적인 영역으로의 접근이 허용되지 않으며,
접근할 수 있는 메모리의 영역에도 제한이 따른다.
- 커널모드 : 운영체제가 실행될 때의 모드. 메모리뿐만 아니라, 하드웨어의 접근에도 제한이 따르지 않는다.
- 그러나, 응용 프로그램의 실행과정에서 항상 유저모드에만 머무는 것이 아니라, 유저모드와 커널모드를 오가며 실행하게 됨.
- 두 가지 모드를 정의하는 이유 : "안전성을 높이기 위함"
- 쓰레드와 같이 커널 오브젝트의 생성을 동반하는 리소스의 생성을 위해서는,
유저모드(응용 프로그램에서 리소스 생성을 요청) => 커널모드(리소스의 생성) => 유저모드(응용 프로그램의 나머지 부분을 이어서 실행)
위와 같은 모드 변환의 과정을 기본적으로 거쳐야 함.
- 리소스의 생성 뿐만 아니라 커널 오브젝트와 관련된 모든 일은 커널모드에서 진행됨.
- 모드의 변환 역시 시스템에 부담이 되는 일이기에, 빈번한 모드의 변환은 성능에 영향을 줄 수 있음.
# 유저모드 동기화
- 유저모드상에서 진행되는 동기화
- 운영체제의 도움 없이 응용 프로그램 상에서 진행되는 동기화
- 속도가 빠르고(커널모드로의 전환이 불필요하기 때문), 사용방법도 간단
- 운영체제의 도움을 받지 않는 동기화 기법이기에 기능이 제한적
# 커널모드 동기화
- 커널모드상에서 진행되는 동기화
- 유저모드 동기화에 비해 제공되는 기능이 더 많고, Dead-lock에 걸리지 않도록 타임아웃의 지정이 가능
- 서로 다른 프로세스에 포함되어 있는 두 쓰레드간의 동기화도 가능
(운영체제가 소유하고 관리하는 커널 오브젝트를 기반으로 하기 때문)
- 모드간의 빈번한 변환이 불가피하기 때문에 성능에 있어서의 제약이 따름
# Dead-lock
- 임계영역으로의 진입을 대기중인, 블로킹 상태에 있는 쓰레드가 이를 빠져나오지 못하는 상황
- 매우 다양한 상황에서 발생함
- 대부분의 경우, 원인의 파악조차 쉽지 않을 정도로 애매한 상황에서 Dead-lock은 발생함
# CRITICAL_SECTION 기반의 동기화(유저모드 동기화 기법 중 하나)
- "CRITICAL_SECTION 오브젝트(이하 CS 오브젝트)"라는 것을 생성해서 이를 동기화에 활용함
- 이는 커널 오브젝트가 아니며, 대부분의 다른 동기화 오브젝트와 마찬가지로
이는 임계영역의 진입에 필요한 일종의 Key(열쇠)로 이해할 수 있음
- 임계영역의 진입을 위해서는 CS 오브젝트를 얻어야 하고,
반대로 임계영역을 빠져나갈 때에는 얻었던 CS 오브젝트를 반납해야 함
# CS 오브젝트의 초기화 및 소멸과 관련된 함수
#include <windows.h>
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
- lpCriticalSection : init... 함수에서는 초기화 할 CRITICAL_SECTION 오브젝트의 주소값 전달, 반면 Del... 함수에서는 해제할 CRITICAL_SECTION 오브젝트의 주소값 전달
- LPCRITICAL_SECTION 매개변수 : CRITICAL_SECTION의 포인터형
- DeleteSectionCriticalSection 함수는 CRITICAL_SECTION 오브젝트를 소멸하는 함수가 아니라, CRITICAL_SECTION 오브젝트가 사용하던 리소스를 소멸시키는 함수
# CS 오브젝트의 획득(소유) 및 반납에 관련된 함수
#include <windows.h>
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
- lpCriticalSection : 획득(소유) 및 반납할 CRITICAL_SECTION 오브젝트의 주소값 전달
# CS 오브젝트를 이용한 동기화 예시
Sync_CS_win.c
#include <stdio.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 50
/* thread의 main 함수 */
unsigned WINAPI threadInc(void* arg);
unsigned WINAPI threadDes(void* arg);
long long num=0; // thread들이 접근하는 전역변수
CRITICAL_SECTION cs; // CS 오브젝트
int main(int argc, char* argv[])
{
HANDLE tHandles[NUM_THREAD]; // thread의 커널 오브젝트의 핸들
int i;
InitializeCriticalSection(&cs); // CS 초기화
for(i=0;i<NUM_THREAD;++i) // NUM_THREAD 만큼 쓰레드 생성 및 실행
{
if(i%2)
tHandles[i]=(HANDLE)_beginthreadex(NULL,0,threadInc,NULL,0,NULL);
else
tHandles[i]=(HANDLE)_beginthreadex(NULL,0,threadDes,NULL,0,NULL);
}
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INTINITE); // 모든 쓰레드기 종료할때까지 블로킹
DeleteCriticalSection(&cs); // CS 관련 리소스 소멸
printf("result: %lld \n", num);
return 0;
}
unsigned WINAPI threadInc(void* arg)
{
int i;
EnterCriticalSection(&cs); // CS 획득
for(i=0;i<50000000;++i)
num+=1;
LeaveCriticalSection(&cs); // CS 반납
return 0;
}
unsigned WINAPI threadDes(void* arg)
{
int i;
EnterCriticalSection(&cs); // CS 획득
for(i=0;i<50000000;++i)
num-=1;
LeaveCriticalSection(&cs); // CS 반납
return 0;
}
- CS 획득 및 반납의 과정은 생각보다 시간이 걸리는 작업이므로, 항상 상황에 따라 적절한 임계영역 설정이 필요하다.(예시에서는 빠른 결과 확인을 위해 반복문 전체를 임계영역으로 잡았다.)
# 커널모드 동기화 기법 : Event, Semaphore, Mutex 커널 오브젝트 기반의 동기화가 존재
1. Mutex(Mutual Exclusion) 커널 오브젝트 기반 동기화
- CS 오브젝트 기반의 동기화와 유사
- Mutex 커널 오브젝트 역시 열쇠에 비유해서 이해할 수 있음
# Mutex 커널 오브젝트의 생성에 관련된 함수
#include <windows.h>
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName);
// 성공시 생성된 Mutex 커널 오브젝트의 핸들, 실패시 NULL 반환
- lpMutexAttributes : 보안관련 특성 정보의 전달, 디폴트 보안설정을 위해서 NULL 전달
- bInitialOwner : TRUE 전달시, 생성되는 Mutex 커널 오브젝트는 이 함수를 호출한 쓰레드의 소유가 되면서 non-signaled 상태가 된다. 반면 FALSE 전달시, 생성되는 Mutex 커널 오브젝트는 소유자가 존재하지 않으며 signaled 상태로 생성된다.
- lpName : Mutex 커널 오브젝트에 이름을 부여할 때 사용된다. NULL을 전달하면 이름없는 Mutex 커널 오브젝트가 생성된다.
- Mutex 커널 오브젝트는 소유자가 없는 경우에 signaled 상태가 된다.
# Mutex는 커널 오브젝트이기 때문에 다음 함수의 호출을 통해서 소멸이 이뤄진다.
#include <windows.h>
BOOL CloseHandle(HANDLE hObject);
// 성공시 TRUE, 실패시 FALSE 반환
- hObject : 소멸하고자 하는 커널 오브젝트의 핸들 전달
- 위 함수는 커널 오브젝트를 소멸하는 함수이기 때문에, Semaphore와 Event의 소멸에도 사용됨.
# Mutex의 획득과 반납에 관한 함수
- 획득은 WaitForSingleObject의 함수호출을 통해서 이루어짐
#include <windows.h>
BOOL ReleaseMutex(HANDLE hMutex);
// 성공시 TRUE, 실패시 FALSE 반환
- hMutex : 반납할, 다시 말해서 소유를 해제할 Mutex 커널 오브젝트의 핸들 전달
- Mutex는 소유되었을 때 non-signaled 상태가 되고, 반납되었을 때(소유되지 않았을 때) signaled 상태가 되는 커널 오브젝트
- WaitForSingleObject 함수는 커널 오브젝트의 상태가 signaled 상태인지 확인하는 함수이므로, 이를 통해 Mutex의 소유여부를 확인할 수 있음
- WaitForSingleObject 함수의 호출결과는
1) 호출 후 블로킹 상태 : Mutex 커널 오브젝트가 다르 쓰레드에게 소유되어서 현재 non-signaled 상태인 경우
2) 호출 후 반환된 상태 : Mutex 커널 오브젝트의 소유가 해제되었거나 소유되지 않아서 signaled 상태인 경우
- Mutex는 WaitForSingleObject 함수가 반환될 때, 자동으로 non-signaled 상태가 되는 'auto-reset 모드' 커널 오브젝트 => WaitForSingleObject 함수는 결과적으로 Mutex를 획득(소유)할 때 호출하는 함수가 됨
# Mutex 기반의 임계영역 보호를 위한 코드
WaitForSingleObject(hMutex, INFINITE);
// 임계영역의 시작
....
// 임계영역의 끝
ReleaseMutex(hMutex);
1) Mutex가 해제되었거나 소유되지 않아서 signaled 상태인 경우, WaitForSingleObject 함수가 반환을 하므로 임계영역으로 진입. 이 때, Mutex는 non-signaled 상태로 바뀜(auto-reset 모드) => Mutex가 소유됨
2) ReleaseMutex 함수 호출에 의해 Mutex를 반납. 이 때, Mutex는 signaled 상태로 바뀜.
# Mutex 커널 오브젝트를 이용한 동기화 예시
SyncMutex_win.c
#include <stdio.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 50
/* thread의 main 함수 */
unsigned WINAPI threadInc(void* arg);
unsigned WINAPI threadDes(void* arg);
long long num=0; // thread들이 접근하는 전역변수
HANDLE hMutex; // Mutex 커널 오브젝트의 핸들
int main(int argc, char* argv[])
{
HANDLE tHandles[NUM_THREAD]; // thread의 커널 오브젝트의 핸들
int i;
hMutex=CreateMutex(NULL,FALSE,NULL); // Mutex 커널 오브젝트를 소유자 없이 signaled 상태로 생성
for(i=0;i<NUM_THREAD;++i) // NUM_THREAD 만큼 쓰레드 생성 및 실행
{
if(i%2)
tHandles[i]=(HANDLE)_beginthreadex(NULL,0,threadInc,NULL,0,NULL);
else
tHandles[i]=(HANDLE)_beginthreadex(NULL,0,threadDes,NULL,0,NULL);
}
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INTINITE); // 모든 쓰레드가 종료할때까지 블로킹
CloseHandle(hMutex); // Mutex 커널 오브젝트 소멸
printf("result: %lld \n", num);
return 0;
}
unsigned WINAPI threadInc(void* arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE); // Mutex 획득
for(i=0;i<50000000;++i)
num+=1;
ReleaseMutex(hMutex); // Mutex 반납
return 0;
}
unsigned WINAPI threadDes(void* arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE); // Mutex 획득
for(i=0;i<50000000;++i)
num-=1;
ReleaseMutex(hMutex); // Mutex 반납
return 0;
}
# Semaphore 오브젝트 기반 동기화
- 리눅스의 세마포어와 유사
- 둘 다 "세마포어 값"이라 불리는 정수를 기반으로 동기화가 이뤄지고, 이 값이 0보다 작아질 수 없음
- 윈도우의 세마포어 값은 세마포어 커널 오브젝트에 등록이 됨
# Semaphore 오브젝트의 생성에 사용되는 함수
- 소멸은 CloseHandle 함수를 이용해서 진행함
#include <windows.h>
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount,
LPCTSTR lpName);
// 성공시 생성된 Semaphore 커널 오브젝트의 핸들, 실패시 NULL 전달
- lpSemaphoreAttributes : 보안관련 정보의 전달, 디폴트 보안설정을 위해서 NULL 전달
- lIntialCount : 세마포어의 초기값 지정, 매개변수 lMaximumCount에 전달된 값보다 크면 안되고, 0 이상이어야 한다.
- lMaximumCount : 최대 세마포어 값을 지정한다. 1을 전달하면 세마포어값이 0, 또는 1이 되는 바이너리 세마포어가 구성된다.
- lpName : Semaphore 커널 오브젝트에 이름을 부여할 때 사용한다. NULL을 전달하면 이름없는 Semaphore 오브젝트가 생성된다.
- 세마포어 값이 0인 경우 non-signaled 상태
- 세마포가 값이 0보다 큰 경우 signaled 상태
- 매개변수 lIntialCount에 0이 전달되면 non-signaled 상태로 Semaphore 커널 오브젝트 생성
# 세마포어 커널 오브젝트의 반납에 사용되는 함수
#include <windows.h>
BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount);
// 성공시 TRUE, 실패시 FALSE 반환
- hSemaphore : 반납할 Semaphore 커널 오브젝트의 핸들 전달
- lReleaseCount : 반납은 세마포어 값의 증가를 의미하는데, 이 매개변수를 통해서 증가되는 값의 크기를 지정할 수 있다. 그리고 이로 인해서 세마포어의 최대 값을 넘어서게 되면, 값은 증가하지 않고 FALSE가 반환된다.
- lpPreviousCount : 변경 이전의 세마포어 값 저장을 위한 변수의 주소값 전달, 불필요하다면 NULL 전달
- WaitForSingleObject 함수가 호출되면, 세마포어 값이 0보다 큰 경우(signaled 상태) 반환을 한다.
이렇게 반환이 되면 세마포어 값을 1 감소시킨다. 세마포어 값이 0이 되면 non-signaled 상태가 된다.
- 임계영역의 보호 코드
WaitForSingleObject(hSemaphore, INFINITE);
// 임계영역의 시작
....
// 임계영역의 끝
ReleaseSemaphore(hSemephore, 1, NULL);
# Semaphore 커널 오브젝트를 이용한 동기화 예시
SyncSema_win.c
#include <stdio.h>
#include <windows.h>
#include <process.h>
/* thread의 main 함수 */
unsigned WINAPI Read(void* arg);
unsigned WINAPI Accu(void* arg);
/* 세마포어 커널 오브젝트의 핸들 */
static HANDLE semOne;
static HANDLE semTwo;
static int num;
int main(int argc, char* argv[])
{
HANDLE hThread1, hThread2; // 쓰레드의 핸들
semOne=CreateSemaphore(NULL,0,1,NULL); // non-signaled 상태의 바이너리 세마포어 생성
semTwo=CreateSemaphore(NULL,1,1,NULL); // signaled 상태의 바이너리 세마포어 생성
/*
이를 통해 hThread1 => hThread2 순서대로 임계영역에 진입되게 함
*/
/* 쓰레드 생성 및 실행 */
hThread1=(HANDLE)_beginthreadex(NULL,0,Read,NULL,0,NULL);
hThread2=(HANDLE)_beginthreadex(NULL,0,Accu,NULL,0,NULL);
/* 각 쓰레드가 종료할 때까지 블로킹 */
WaitForSingleObject(hThread1,INFINITE);
WaitForSingleObject(hThread2,INFINITE);
/* 세마포어 소멸 */
CloseHandle(semOne);
CloseHandle(semTwo);
return 0;
}
unsigned WINAPI Read(void* arg)
{
int i;
for(i=0;i<5;++i)
{
fputs("Input num: ",stdout);
WaitForSingleObject(semTwo,INFINITE); // semTwo를 non-signaled로 변경
scanf("%d",&num);
ReleaseSemaphore(semOne,1,NULL); // semOne을 signaled로 변경
}
return 0;
}
unsigned WINAPI Accu(void* arg)
{
int sum=0, i;
for(i=0;i<5;++i)
{
WaitForSingleObject(semOne, INFINITE); // semOne을 non-signaled로 변경
sum+=num;
ReleaseSemaphore(semTwo,1,NULL); // semTwo를 signaled로 변경
}
printf("Result: %d \n",sum);
return 0;
}
# Event 커널 오브젝트 기반 동기화
- Event 커널 오브젝트의 생성과정에서 자동으로 non-signaled 상태로 돌아가는 auto-reset 모드와 그렇지 않은 manual-reset 모드 중 하나를 선택할 수 있다는 점이 다른 동기화 오브젝트와 비교됨.
# Event 커널 오브젝트의 생성에 사용되는 함수
#include <windows.h>
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset,
BOOL bInitialState, LPCTSTR lpName);
// 성공시 생성된 Event 커널 오브젝트의 핸들, 실패시 NULL 반환
- lpEventAttributes : 보안관련 정보의 전달, 디폴트 보안설정을 위해서 NULL 전달
- bManualRest : TRUE 전달시 mamual-reset 모드 Event, FALSE 전달시 auto-reset 모드 Event 생성
- bInitialState : TRUE 전달시 signaled 상태의 Event 커널 오브젝트 생성, FALSE 전달시 non-signaled 상태의 Event 커널 오브젝트 생성
- lpName : Event 커널 오브젝트에 이름을 부여할 때 사용된다. NULL을 전달하면 이름없는 Event 커널 오브젝트가 생성된다.
# CreateEvent 함수의 두번째 매개변수에 TRUE가 전달되면 manual-reset 모드로 Event 커널 오브젝트가 생성되므로, WaitForSingleObject 함수가 반환한다고 해서, Event 커널 오브젝트가 자동으로 non-signaled 상태로 되돌려지지 않는다. 따라서 이러한 경우에는 다음 두 함수를 이용해서 명시적으로 오브젝트의 상태를 변경해야 한다.
#include <windows.h>
BOOL ResetEvent(HANDLE hEvent); // to the non-signaled
BOOL SetEvent(HANDLE hEvent); // to the signaled
// 성공시 TRUE, 실패시 FALSE 반환
# Event 커널 오브젝트를 이용한 동기화 예시
SyncEvent_win.c
#include <stdio.h>
#include <windows.h>
#include <process.h>
#define STR_LEN 100
/* thread의 main 함수 */
unsigned WINAPI NumberOfA(void* arg);
unsigned WINAPI NumberOfOthers(void* arg);
static char str[STR_LEN];
static HANDLE hEvent; // Event 커널 오브젝트의 핸들
int main(int argc, char* argv[])
{
HANDLE hThread1, hThread2; // 쓰레드의 핸들
hEvent=CreateEvent(NULL,TRUE,FALSE,NULL); // manual-reset 모드의 non-signaled 상태의 Event 커널 오브젝트 생성
/* 쓰레드 생성 및 실행 */
hThread1=(HANDLE)_beginthreadex(NULL,0,NumberOfA,NULL,0,NULL);
hThread2=(HANDLE)_beginthreadex(NULL,0,NumberOfOthers,NULL,0,NULL);
/*
문자열을 입력받은 후에,
Event를 signaled 상태로 변경
*/
fputs("Input string: ",stdout);
fgets(str,STR_LEN,stdin);
SetEvent(hEvent);
/* 각 쓰레드가 종료할 때까지 블로킹 */
WaitForSingleObject(hThread1,INFINITE);
WaitForSingleObject(hThread2,INFINITE);
ResetEvent(hEvent); // Event를 non-signaled 상태로 변경
CloseHandle(hEvent); // Event 소멸
return 0;
}
unsigned WINAPI NumberOfA(void* arg)
{
int i, cnt=0;
WaitForSingleObject(hEvent, INFINITE); // 문자열을 입력 받은 후에야 함수를 반환
for(i=0;str[i]!='\0';++i)
{
if(str[i]=='A')
++cnt;
}
printf("Num of A: %d \n",cnt);
return 0;
}
unsigned WINAPI NumberOfOthers(void* arg)
{
int i, cnt=0;
WaitForSingleObject(hEvent, INFINITE); // 문자열을 입력 받은 후에야 함수를 반환
for(i=0;str[i]!='\0';++i)
{
if(str[i]!='A')
++cnt;
}
printf("Num of others: %d \n",cnt-1); // 문자열에 개행문자도 포함되어 있으므로 -1
return 0;
}
- 간단하게나마 둘 이상의 쓰레드가 동시에 대기상태를 빠져 나와야 하는 상황을 보임
- 이러한 상황에서 manual-reset 모드로 생성 가능한 Event 오브젝트가 좋은 선택이 될 수 있음
[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어
'Programming > 열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)' 카테고리의 다른 글
Ch 21. Asynchronous Notification IO 모델 (0) | 2020.08.15 |
---|---|
Ch 20. 내용 확인문제 (0) | 2020.08.15 |
Ch 19. 내용 확인문제 (0) | 2020.08.14 |
Ch 19. Windows에서의 쓰레드 사용 (0) | 2020.08.14 |
Ch 24. 내용 확인문제 (0) | 2020.08.14 |