이 글은 윤성우의 TCP/IP 소켓 프로그래밍 책을 참고하였습니다.

순서

I. 커널 오브젝트, 스레드 생성

II. 커널 오브젝트 상태, 스레드 종료

III. 유저 모드와 커널 모드

IV. 유저 모드 동기화

V. 커널 모드 동기화
1. Mutex
2. Semephore
3. Event


그림 1. Window Thread 전체 함수정리

 


I. 커널 오브젝트, 스레드 생성


1. 커널 오브젝트

우리가 다양한 리소스들(프로세스, 스레드, 파일, 뮤텍스 등등..)을 사용할 때
함수로 운영체제에 요청을 한다.

그러면 운영체제는 이를 관리하기 위한 데이터 블럭들을 생성하는데,

이 데이터 블럭들을 "커널 오브젝트"라고 한다.

이 커널 오브젝트는 운영체제의 소유이기 때문에
"커널 오브젝트의 생성, 관리 그리고 소멸시점을 결정하는 것 모두
운영체제의 몫이다!"


2. 스레드 생성 및 종료

다시 한번 떠올리자면 Window는 파일 디스크립터(fd)가 아닌
소켓 핸들로 관리가 되었다.

소켓 핸들은 커널 오브젝트의 구분자 역할을 하는, 정수로 표현되는 값이다.

스레드 생성 함수 1
CreateThread()

#include <windows.h>

HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes,
                     SIZE_T dwStackSize,
                     LPTHREAD_START_ROUTINE lpStartAddress,
                     LPVOID lpParameter,
                     DWORD dwCreationFlags,
                     LPDWORD lpThreadId);
    --> 성공 시 스레드 핸들, 실패 시 NULL 반환
    
    ㄴ. lpThreadAttributes : 스레드의 보안관련 정보, NULL 하면 기본설정
    ㄴ. dwStackSize        : 스레드에 할당할 스택의 크기, 0 하면 기본 크기
    
    ㄴ. lpStartAddress     : 스레드의 main 함수정보 전달
    ㄴ. lpParameter        : 위 main 함수에 전달할 인자
    
    ㄴ. dwCreationFlags    : 스레드 생성 이후 행동 결정, 0 하면 생성과 동시에 실행가능
    ㄴ. lpThreadld         : 스레드 ID 저장을 위한 변수의 주소 값
                     

위의 인자에서 lpStartAddress와 lpParameter만 신경 쓰고 나머지는 0 또는 NULL 하면 된다.

스레드 생성 함수 2
_beginthreadex()

#include <process.h>

uintptr_t _beginthreadex( void *security,
                          unsigned stack_size,
                          unsigned (*start_address) (void*),
                          void* arglist,
                          unsigned initflag,
                          unsigned* thrdaddr
                         );
    --> 성공 시 스레드 핸들, 실패 시 0 반환
    
HANDLE hThread;
unsigned threadID;
int param=5;

hThread = (HANDLE)_beginthreadex(NULL,0, UserThreadFunc, (void*) &param, 0 ,&threadID);

// 위 함수의 호출 규약을 지키기 위해 WINAPI를 삽입한 것이다.
unsigned WINAPI UserThreadFunc(void*arg)
{
    // mycode;
}

위 함수와 순서는 모두 같다.
차이점은, 이름과 매개변수의 자료형이다.

마지막의 함수 호출 모양을 잘 확인하고 참고하자.
반환형이 uintptr_t이므로 HANDLE로 캐스팅해주었다.

또한 C/C++ 표준 함수를 호출하려면 바로 위의 함수를 이용해야 한다.
이전의 "CreateThread"는 C/C++ 표준 함수에 대해 불안정하기 때문이다.

 


II. 커널 오브젝트 상태, 스레드 종료


1. 커널 오브젝트 상태

우리는 커널 오브젝트가 프로세스, 스레드 등을 관리하는 것이라고 알고 있다.

그렇다면 이 커널 오브젝트의 "상태"는 어떻게 알 수 있을까?
이 상태는 "signaled" 상태, "non-signaled" 상태로 두 가지가 있다.

왜 두 개 밖에 없을까?
사실, 커널 오브젝트의 큰 관심사"종료 여부"이기 때문이다.
프로세스가 언제 종료될지, 스레드가 언제 종료될지...

따라서 운영체제는 다음과 같이 약속하고 있다.

"프로세스나 스레드가 종료되면 해당 커널 오브젝트를 signaled 상태로 변경해 놓겠다!"

 

2. 스레드 종료

다음 두 함수는 커널 오브젝트의 signal상태를 알려주는 함수로 스레드의 종료를 대기한다.

#include <windows.h>

// 하나의 커널 오브젝트
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
    --> 성공 시 이벤트 정보, 실패 시 WAIT_FAILED 반환
    
    ㄴ. hHandle        : 상태확인의 대상이 되는 커널 오브젝트의 핸들
    ㄴ. dwMilliseconds : ms단위의 타임아웃 지정, INFINITE전달 시 signaled 무한대기
    
    ㄴ. 반환 값        : signaled 상태로 인한 반환 시 WAIT_OBJECT_0 반환,
                        타임아웃 시 WAIT_TIMEOUT 반환  
 

// 다수의 커널 오브젝트
DWORD WaitForMultipleObjects(DWORD nCount, const HANDLE* lpHandles,
                             BOOL bWaitAll, DWORD dwMilliseconds);

    --> 성공 시 이벤트 정보, 실패 시 WAIT_FAILED 반환
    
    ㄴ. nCount         : 검사할 커널 오브젝트의 수
    ㄴ. lpHandles      : 핸들정보를 담고 있는 배열의 주소 값
    ㄴ. bWaitAll       : TRUE시 모든 대상의 signaled 기다림
                         FALSE시 하나라도 signaled면 반환
    ㄴ. dwMilliseconds : 위 함수와 동일


예제 코드

<hide/>
#include <stdio.h>
#include <windows.h>
#include <process.h>

unsigned WINAPI UserThreadFun(void *arg) // 유저 정의필요

int main()
{
    HANDLE hThread;
    DWORD wr;
    unsigned threadID;
    int param = 5;
    
    hThread = (HANDLE)_beginthreadex(NULL, 0, UserThreadFunc, (void*) &param, &threadID);
    
    wr = WaitForSingleObject(hThread, INFINITE); // signaled 무한대기
    
    if(wr == WAIT_FAILED) return -1;
    
    return 0;
}

 


III. 유저모드와 커널모드

윈도우 운영체제의 연산방식(프로그램 실행방식)은
안정성을 위한 "이중모드 연산"인데 여기서 이중모드는 다음과 같다.

1. 유저모드 (응용 프로그램의 실행)
응용 프로그램이 실행되는 기본모드
물리적인 영역으로의 접근 허용X, 메모리 접근에도 제한있음.

2. 커널모드 (운영체제의 실행)
운영체제가 실행될 때의 모드로, 메모리 및 하드웨어의 접근에 허용O

하지만 독립적인 것이 아니라, 우리가 유저모드에서 스레드 함수만 호출하면
내부 커널모드에서 생성 되는것처럼 서로 전환된다.

따라서 스레드와 같은 커널 오브젝트의 생성을 동반하는 리소스의 생성을 위해서는

1. 리소스의 생성을 위해 : 유저모드 → 커널모드
2. 응용 프로그램 나머지 이어서 실행위해 : 커널모드 
→ 유저모드

와 같은 과정을 거친다.

그림 2. 각 모드의 동기화 차이점

Dead-lock (데드락)이란?

임계영역으로의 진입을 대기중인, 블로킹 상태에 놓인 스레드가 빠져나오지 못하는 상황.

주로 임계영역 전 블로킹 상태가 동시에 걸려 발생한다.

 


IV. 유저 모드 동기화

CRITICAL_SECTION(이하 CS) 오브젝트를 생성해 활용한다.
이는 커널 오브젝트가 아니고, 일종의 Key(열쇠)이다.

아래 함수는 CS오브젝트의 초기화 및 해제관련 함수들이다.

#include <windows.h>

void InitializerCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
    ㄴ. lpCriticalSection : Init시 초기화 할 CS오브젝트 주소 값
                            Del 시 해제할 CS 오브젝트 주소 값
                            
자료형 LPCRITICAL_SECTION은 CS의 포인터 형이다.

주의!! Del 함수는 CS 오브젝트 소멸이 아닌, CS 오브젝트가 사용하던 리소스를 소멸시키는 함수이다.


아래 함수는 CS오브젝트의 소유 및 반납관련 함수들이다.

#include <windows.h>

void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
    ㄴ. lpCriticalSection : 획득 및 반납할 CS 오브젝트의 주소 값 전달

Linux의 mutex와 굉장히 비슷한 모습을 가진다.

예제 코드

<hide/>
#include <stdio.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 100

unsigned WINAPI thread1(void* arg);
unsigned WINAPI thread2(void* arg);

CRITICAL_SECTION cs;

int sharenum{0};

int main()
{
    // CS오브젝트 생성
    InitializerCriticalSection(&cs);
    
    // 스레드 시작
    1. beginthreadex()
    2. WaitForMultipleObjects()
    
    // CS오브젝트 리소스 소멸
    DeleteCriticalSection(&cs);
    
    return 0;
}

unsigned WINAPI thread1(void *arg)
{
    EnterCriticalSection(&cs);
    // Your Share Var Code1
    LeaveCriticalSection(&cs);
}

unsigned WINAPI thread2(void *arg)
{
    EnterCriticalSection(&cs);
    // Your Share Var Code2
    LeaveCriticalSection(&cs);
}

 


V. 커널 모드 동기화


1. Mutex

Mutex도 위의 CS 오브젝트 기반의 동기화와 유사하게 열쇠의 개념으로 접근하면 편하다.

뮤텍스 관련 함수들은 다음과 같다.

#include <windows.h>

[뮤텍스를 생성하는 함수]
HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes,
                    BOOL bInitialOwner,
                    LPCTSTR lpName)
    --> 성공 시 생성된 Mutex 오브젝트의 핸들, 실패 시 NULL 반환
    
    ㄴ. lpMutexAttributes : 보안관련 특성 정보의 전달, 디폴트 보안설정을 위해 NULL 전달.
    ㄴ. bInitialOwner     : True 전달 시, 이 함수를 호출한 스레드의 소유로 non-signaled 상태
                            False 전달 시 , 소유자가 존재하지 않고 signaled상태로 생성
    ㄴ. lpName            : Mutex 오브젝트에 이름 부여시 사용, NULL전달 시 이름없는 Mutex 오브젝트.
    

[뮤텍스 커널오브젝트의 소멸]
BOOL CloseHandle(HANDLE hObject)
    --> 성공 시 TRUE, 실패 시 FALSE 반환
    ㄴ. hObject : 소멸하고자 하는 커널 오브젝트의 핸들 전달

[뮤텍스의 소유]
: 위의 WaitForSingleObject로 signal 대기하는 방법

[뮤텍스의 반납]
BOOL ReleaseMutex(HANDLE hMutex)
    --> 성공 시 TRUE, 실패 시 FALSE 반환
    ㄴ. hMutex 반납할, 소유를 해제할 Mutex 오브젝트 핸들 전달

소유자가 존재하지 않고 반납된 Mutex의 상태가 signaled이다. 따라서 위의
"스레드 종료"에서 보았던 signaled상태를 대기하는 "WaitForSingleObject"와 사용한다.

예제코드

<hide/>
#include <stdio.h>
#include <windows.h>
#include <process.h>

unsigned WINAPI userFunc1(void* arg);
unsigned WINAPI userFunc2(void* arg);

HANDLE hMutex;

int main()
{
    hMutex=CreateMutex(NULL, FALSE, NULL);
    
    1. _beginthreadex();
    2. WaitForMultipleObjects(); // 스레드 종료대기
    
    CloseHandle(hMutex);
    return 0;
}



unsigned WINAPI userFunc1(void* arg)
{
    WaitForSingleObject(hMutex,NULL)
    // your code
    ReleaseMutex(hMutex);
    return 0;
}

unsigned WINAPI userFunc2(void* arg)
{
    WaitForSingleObject(hMutex,NULL)
    // your code
    ReleaseMutex(hMutex);
    return 0;
}

userFunc를 보면 어떻게 뮤텍스를 사용하는지 볼 수 있다.

WaitForSingleObject는 signaled일 때 블로킹이 풀리고, non_signaled로 변경시켜주며
다른 스레드에서의 접근을 막아준다.

또한 ReleaseMutex로 signaled로 변경시키며 소유권을 반납한다.


2. Semephore

리눅스의 세마포어와 유사하게 "세마포어 값"으로 불리는 정수를 기반으로 동기화가 이루어진다.

물론 윈도우의 세마포어 값은 커널 오브젝트에 등록된다.

세마포어 관련 함수들은 다음과 같다.

#include <windows.h>

[세마포어 생성]
HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAtributes,
                        LONG lInitialCount,
                        LONG lMaximumCount,
                        LPCTSTR lpName);
    --> 성공 시 생성된 Semaphore 오브젝트의 핸들, 실패 시 NULL 반환
    
    ㄴ. lpSemaphoreAttributes : 보안관련 정보의 전달, 디폴트 보안설정을 위해 NULL
    ㄴ. lInitialCount : 세마포어 초기값 지정, 0 이상 3번째 인자 이하
    ㄴ. lMaximumCount : 최대 세마포어 값 지정, 1을 전달하면 Binary Semaphore
    ㄴ. lpName        : Semaphore 커널 오브젝트에 이름을 부여할 때 사용 NULL하면 이름 X
    
    
[세마포어 반납]
BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount);
    --> 성공 시 TRUE, 실패 시 FALSE 반환
    
    ㄴ. hSemaphore      : 반납할 Semaphore 오브젝트의 핸들 전달
    ㄴ. lReleaseCount   : 반납은 세마포어 값의 증가를 의미한다.
                          이 인자를 통해 증가되는 값의 크기 지정 가능
                          이로인해 최대 세마포어값을 넘으면 FALSE반환
    ㄴ. lpPreviousCount : 변경 이전의 세마포어 값 저장을 위한 변수의 주소 값, 불필요하면 NULL 

세마포어의 값은 0보다 큰 경우에 signaled상태가 되고, 0인 경우에 non-signaled가 된다.

뮤텍스와 같이 WaitForSinggleObject()로 진입 및 non-signaled로 변경시키고
ReleaseSemaphore()로 반납 및 signaled로 변경.
이 사이에 임계영역의 코드를 작성한다.


3. Event

이벤트 오브젝트 기반 동기화는 앞선 2가지와 차이점이 있다.

기존 2가지는 WaitForSingleObject()함수 호출 시 자동으로 non-signal로 돌아갔다.

하지만 Event 오브젝트는 자동으로 non-signal로 돌아가게할지, 수동으로 할지 결정이 가능하다.

#include <windows.h>

HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes,
                    BOOL bManualReset,
                    BOOL bInitialState,
                    LPCTSTR lpName)
    --> 성공 시 생성된 Event 오브젝트의 핸들, 실패 시 NULL 반환
    
    ㄴ. lpEventAttributes : 보안관련 정보의 전달, 디폴트 NULL
    ㄴ. bManualReset      : TRUE전달 시 manual-reset모드
                            FALSE전달 시 auto-reset모드
    ㄴ. bInitialState     : TRUE전달 시 signal 상태의 Event 오브젝트 생성
                            FALSE전달 시 non-signal 상태의 Event 오브젝트 생성
    ㄴ. lpName            : Event 오브젝트에 이름을 부여할 때 사용, NULL이면 이름없음
    
    
    
BOOL ResetEvent(HANDLE hEvent); // to the non-signaled
BOOL SetEvent(HANDLE hEvent);   // to the signaled

2번 째 인자를 보게되면, 어떤 모드로 생성할지 결정할 수 있다.

만약 auto-reset 모드라면, 자동으로 Reset이 되므로, ResetEvent는 사용하지 않아도 된다.

하지만 manual-reset모드라면, 자동으로 Reset이 되지 않아, ResetEvent()를 사용해주어야한다. 

+ Recent posts