산타는 없다

Window via C/C++ 6장 - 스레드의 기본 본문

프로그래밍 서적/Window via C++

Window via C/C++ 6장 - 스레드의 기본

LEDPEAR 2021. 11. 7. 18:00
반응형
  • 0. 개요
    • 모든 프로세스는 적어도 하나 이상의 스레드를 사용한다
    • 운영체제가 스레드를 다루기 위해 사용하는 스레드 커널 오브젝트, 스레드 커널 오브젝트는 시스템이 스레드에 대한 통계 정보를 저장하는 공간이기도 하다
    • 스레드가 코드를 수행할 때 함수의 매개변수와 지역변수를 저장하기 위한 스레드 스택
    • 프로세스는 스스로 수행될 수 없고 단순히 생각한다면 스레드의 저장소로 볼 수도 있다.
    • 프로세스는 자신만의 주소공간을 가지기 때문에 스레드에 비해 더 많은 시스템 리소스를 사용한다. 프로세스별로 가상 주소 공간을 생성하는 것은 매우 많은 시스템 리소스를 필요로 한다. 특히 개별 프로세스는 상당량의 정보를 시스템 내부에 저장해 두어야 하기 때문에 메모리를 많이 필요로 한다.
  • 1. 스레드를 생성해야 하는 이유
    • 스레드는 프로세스 내의 수행 흐름을 의미한다.
    • 프로세스의 초기화가 진행되는 동안에 시스템은 주스레드를 생성한다.
    • 애플리케이션이 마이크로소프트 C/C++ 컴파일러로 작성된 경우라면, 주 스레드는 C/C++ 런타임 라이브러리의 시작 코드를 수행하는 것으로 시작된다. 이후 진입점 함수(_tmain이나 _tWinMain)를 호출하고 이 함수가 반환될 때까지 수행을 계속하다가 함수가 반환되면 C/C++ 런타임 라이브러리의 시작 코드가 ExitProcess를 호출하여 수행을 종료한다.
    • CPU를 유휴 상태로 놔둬야 할 필요는 없으며, 계속해서 CPU가 작업을 수행할 수 있도록 해 주는 것이 좋다.
    • 각 스레드별로 전용 CPU를 할당하는 것이 가능하다.
  • 2. 스레드를 생성하지 말아야 하는경우
    • 스레드는 상당히 유용하지만. 이전의 방식으로 풀던 문제를 다수의 스레드를 이용하여 해결하려 하면 새로운 문제를 일으키는 경우도 있다(프린트 출력과, 문서 수정을 동시에 하는 경우)
    • 일반적으로, 애플리케이션은 모든 윈도우를 생성하고 GetMessage 루프를 가지고 있는 단일 사용자 인터페이스 스레드를 가지고 있다. 
    • 윈도우 탐색기는 각각의 폴더 윈도우에 대해 서로 다른 스레드를 생성한다. 이를 통해 파일을 복사하거나 다른 폴더를 계속 탐색할 수 있다.
    • 다수의 스레드를 이용하여 사용자 인터페이스를 다루는 방식은 매우 신중하게 사용되어야 하며 가능한 한 사용하지 않는것이 좋다.
  • 3. 처음으로 작성하는 스레드 함수
    • 모든 스레드는 수행을 시작할 진입점 함수를 반드시 가져야 한다.
    • 주 스레드의 진입점 함수는 _tmain이나 _tWinMain이다.
    • 프로세스 내에 두 번째 스레드를 만들려면 새로 생성되는 스레드는 아래와 같은 형태의 진입점 함수를 반드시 가져야 한다. 
    • DWORD WINAPI ThreadFunc(PVOID pvParam) { DWORD dwResult = 0; ... return (dwResult); }​
    • 스레드 함수가 반환되는 시점에 스레드는 수행을 멈추고 스레드가 사용하던 스택도 반환된다. 또한 스레드 커널 오브젝트의 사용 카운트도 감소한다. 이 값이 0이 되면 스레드 커널 오브젝트는 파괴된다. 프로세스 커널 오브젝트와 마찬가지로 스레드 커널 오브젝트 또한 이를 통해 관리되는 스레드만큼 살아 있으며 스레드 종료 이후에도 여전히 살아 있을 수 있다.
    • 스레드 함수에 대해 몇가지 중요한 점
      • 주 스레드의 진입점 함수의 이름이 기본적으로 main, wmain, WinMain, 또는 wWinMain( 예외적으로 링커의 /ENTRY: 옵션을 사용하면 진입점 함수의 이름을 임의로 변경할 수 있다)이어야 하는것과 다르게 스레드 함수는 어떠한 이름이라도 사용될 수 있다. 실제로 애플리케이션에 여러개의 스레드 함수가 필요하다면 각각은 서로 다른 이름으로 명명되어야만 한다.
      • 주 스레드의 진입점 함수에는 문자열 매개변수가 전달되어야 하기 때문에 ANSI 버전과 유니코드 버전의 진입점 함수가 main/wmain, WinMain/wWinMain처럼 각각 존재하게 된다. 하지만 스레드 함수는 하나의 매개변수만 전달할 수 있고, 매개변수의 의미는 운영체제가 아니라 사용자에 의해 정의되기 때문에 반드시 ANSI와 유니코드 버전을 각기 구성해야하는 것은 아니다.
      • 스레드 함수는 반드시 값을 반환해야 한다. 이 값은 나중에 스레드의 종료 코드가 된다. 이것은 C/C++ 런터임 라이브러리가 주 스레드의 종료 코드를 프로세스의 종료 코드로 사용하는 것과 유사하다.
      • 스레드 함수는 가능한 한 함수로 전도리된 매개변수와 지역 변수만을 사용하도록 작성되는 것이 좋다. 만일 정적 변수나 전역 변수를 사용하게 되면 다수의 스레드가 동시에 변수에 접근할 수 있게 되며(Thread Safe하지 않게 된다), 이는 변수의 값이 잘못 변경되는 원인이 되기도 한다. 하지만 함수의 매개변수와 지역변수는 스레드의 스택에 유지되기 때문에 다른 스레드에 의해 내용이 변경될 가능성이 거의 없다.
  • 4. CreateThread 함수
    • HANDLE CreateThread(
      	PSECURITY_ATTRIBUTES pas,
          DWORD cbStackSize,
          PTHREAD _START_ROUTINE pfnStartAddr,
          PVOID pvParam,
          DWORD dwCreateFlags,
          PDWORD pdwThreadID);
    • 윈도우가 제공하는 CreateThread 함수는 스레드를 생성하는 함수이다. 하지만 C/C++로 코드를 작성하는 경우에는 CreateThread를 사용해서는 안되고, 마이크로소프트 C/C++ 런타임 라이브러리에서 제공하는 _begin-threadex 함수를 사용해야 한다. 다른 컴파일러에서도 CreateThread 함수를 대체할 만한 함수를 제공할 것이며, 반드시 컴파일러에 의해 제공되는 다른 함수를 이용해야 한다.
    • CreateThread가 호출되면 시스템은 스레드 커널 오브젝트를 생성한다. 이 스레드 커널 오브젝트는 스레드 자체는 아니며, 운영체제가 스레드를 다루기 위한 조그만 데이터 구조체이다.
    • 다음으로, 시스템은 스레드가 사용할 스택을 확보한다. 새로운 스레드는 스레드를 생성한 프로세스와 동일한 컨텍스트 내에서 수행된다. 따라서 새로운 스레드는 프로세스의 모든 커널 오브젝트 핸들뿐만 아니라 프로세스에 있는 모든 메모리, 그리고 같은 프로세스에 있는 다른 모든 스레드의 스택에 조차 접근이 가능하다.
    • 다음은 CreateThread 함수의 각 매개변수들에 대한 설명이다.
      • psa
        • SECURITY_ATTRIBUTES 구조체를 가리키는 포인터다.
        • 스레드 커널 오브젝트에 대해 기본 보안 특성을 사용할 것이라면 이 매개변수로 NULL을 전달하면 된다(대부분 그렇게 사용)
        • 만일 차일드 프로세스로 해당 스레드 커널 오브젝트 핸들을 상속하로독 하려면 SECURITY_ATTRIBUTES 구초제의 bInhertHandle 멤버를 TRUE로 초기화하여 그 포인터를 전달하면 된다.
      • cbStackSize
        • 스레드가 자신의 스택을 위해 얼마만큼의 주소 공간을 사용할지를 지정한다.
        • CreateProcess를 호출하여 프로세스가 시작되면 내부적으로는 CreateThread 함수를 호출하여 프로세스의 주 스레드를 초기화하는데, 이때 CreateProcess는 실행 파일 내부에 저장되어 있는 값을 이용하여 cbStackSize 매개변수의 값을 결정한다. 실행 파일 내에 저장되어 스택의 크기를 변경하기 위해서는 링커의 /STACK 스위치를 사용하면 된다.
        • /STACK:[ reserve] [ , commit]
        • reserve 인자는 시스템이 스레드 스택을 위해 지정된 크기만큼의 주소 공간을 예약하게 한다. 기본값은 1MB다.
        • commit 인자는 스택으로 예약된 주소 공간에 커밋된 물리적 저장소의 초기 크기를 나타낸다. 기본값은 한 페이지 크기다.
        • 스레드가 코드를 수행함에 따라 한 페이지 이상의 스택을 필요로 할 수도 있을 텐데, 이경우 스레드의 스택 오버플로 예외를 발생시키게 된다.
        • 시스템은이러한 예외가발생하며 추가적인 페이지를 예약된 주소 공간상에 커밋해 준다. 이러한 방식으로 스레드가 사용하는 스택을 필요 시 동적으로 커지게 된다.
        • cbStackSize에 0 이외의 값을 지정하면 지정된 크기의 메모리를 에약하고 커밋까지 수행한다.
        • 스택에서 사용할 예약된 영역은 /STACK 링커 스위치와 cbStackSize에 지정된 값 중 큰 값을 이용한다. 
        • 커밋할 저장소의 크기는 항시 cbStackSize 값을 따른다. 하지만 cbStackSize 매개변수로 0을 전달하면 CreateThread는 /STACK 링커 스위치를 이용하여 실행 파일 내에 포함된 커밋된 물리적 저장소의 초기 크기를 따르게 된다.
        • 예약된 영역의 크기는 스택으로 사용할 수 있는 공간의 최대 크기를 지정하는 것이기 때문에 재귀 호출을 끝없이 반복하게 되는 경우 예외가 발생할 수 있다.
      • pfnStartAddr과 pvParam
        • pfnStartAddr : 새로이 생성되는 스레드가 호출할 스레드 함수의 주소를 가리킨다. 이 스레드 함수의 pvParam 매개변수로는 CreateThread 함수의 pvParam 매개변수로 전달한 값이 그래도 전달된다.
        • 이 매개변수는 스레드 함수에 초기 값을 전달하는 용도로 사용될 수 있다. 전달되는 값은 단순 숫자 값일 수도 있고, 다양한 형태의 값을 포함하는 데이터 구조체의 포인터가 될 수도 있다.
        • 다수의 스레드가 동일한 스레드의 함수 주소를 진입점을 ㅗ사용하는 것은 매우 유용한 방법이다.
        • 윈도우는 선점형 멀티스레딩 시스템이다. 때문에 다수의 스레드가 동시에 수행되면 새로운 문제가 발생할 수 있다.
      • dwCreateFlags
        • 스레드를 생성할 때 세부적인 제어를 수행하기 위한 추가적인 플래그를 지정하는 데 사용한다.
        • 이 값으로 0을 전달하면 스레드는 생성되는 즉시 CPU에 의해 스케줄 가능하게 된다.
        • CREATE_SUSPENDED를 전달하면 시스템은 스레드를 생성하고 초기화를 완료한 이후 CPU에 의해 바로 스케줄되지 않도록 일시 정지 상태를 유지하게 된다.
        • CREATE_SUSPENDED 플래그를 사용하면 애플리케이션이 새로 생성되는 스레드가 코드를 수행하기 전에 스레드와 관련된 값들을 변경할 수 있는 기회를 가지게 된다.
      • pdwThreadID
        • 새로운 스레드에 할당되는 스레드 ID 값을 저장할 DWORD 변수를 가리키는 주소를 지정하면 된다.
        • 이 매개변수 값으로 NULL을 전달할 수도 있는데(보통 이렇게 한다.) 이렇게 하면 스레드의 ID에 대해서는 관심이 없다고 함수에게 알려주게 된다.
  • 5. 스레드의 종료
    • 스레드는 4가지 방법으로 종료될 수 있다.
      • 스레드 함수가 반환된다. (이 방법을 강력히 추천한다)
      • 스레드 함수 내에서 ExitTheard 함수를 호출한다. (이 방법은 피하는 것이 좋다.)
      • 동일한 프로세스나 다른 프로세스에서 TerminateThread 함수를 호출한다 (이 방법도 피하는 것이 좋다)
      • 스레드가 포함된 프로세스가 종료된다 (이 방법도 피하는 것이 좋다)
    • 스레드 함수 반환
      • 스레드를 종료하려는 경우 항상 스레드 함수가 반환되도록 설계하는 것이 좋다. 이것은 스레드가 사용한 자원을 적절하게 정리할 수 있는 유일한 방법이다..
      • 스레드 함수가 반환되면 다음과 같은 작업을 수행한다
        • 스레드 함수 내에서 생성한 모든 C++ 오브젝트들은 파괴자를 통해 적절히 제거된다.
        • 운영체제는 스레드 스택으로 사용하엿던 메모리를 반환한다.
        • 시스템은 스레드의 종료 코드를 스세드 함수의 반환 값으로 설정한다 (이 값은 스레드 커널 오브젝트 내에 저장된다.)
        • 시스템은 스레드 커널 오브젝트의 사용 카운트를 감소시킨다.
    • ExitThread 함수
      • ExitThread 함수를 호출하여 스레드를 강제로 종료할 수 있다.
      • VOID ExitThread (DWORD dwExitCode);
      • 이 함수는 스레드를 강제로 종료하고 운영체제가 스레드에서 사용했던 모든 운영체제 리소스를 정리하도록 한다. 하지만 C/C++ 리소스(C++ 클ㄹ래스 오브젝트와 같은)는 정리되지 않느다.
      • 따라서 ExitThread함수를 호출하기 보다는 스레드 함수를 반환하도록 코드를 작성하는 것이 더 좋다.
      • 물론 ExitThread 함수의 dwExitCode 매개변수를 이용하여 스레드의 종료 코드를 설정할 수 있다. ExitThread 함수는 반환되지 않는 함수이기 때문에 이후에 나온느 코드는 수행되지 않는다.
      • C/C++로 코드를 작성하는 경우라면 ExitTheard를 사용하는 대신 _endthreadex 함수는 사용하는 것이 좋고, 다른 컴파일러를 사용한다면 해당 컴파일러가 제공하는 대체 함수를 사용하는 것이 좋다.
    • TerminateThread 함수
      • BOOL TerminateThread ( HANDLE hThread, DWORD dwExitCode);
      • ExitThread 함수가 이 함수를 호출하는 스레드를 종료하는 것과는 다르게 TerminateThread 함수는 어떠한 스레드라도 종료할 수 있다
      • hThread 매개변수는 종료할 스레드의 핸들을 가리킨다.
      • 스레드가 종료되면 스레드의 종료 코드는 dwExiteCode 매개변수로 전달한  값으로 설정되고 스레드 커널 오브젝트의 사용 카운트는 감소한다.
      • TerminateThread 함수는 비동기 함수이므로 함수가 반환되기 이전에 해당 스레드가 종료되엇음을 보장할 수없다. 만일 정확히 스레드가 종료되는 시점을 알고 싶다면 WaitForSingleObject나 이와 유사한 함수에 스레드 핸들을 전달하면 된다.
      • 스레드가 반환을 통해 혹은 ExitThread 함수를 호출해서 종료되는 경우라면 스레드가 사용하던 스택이 정상적으로 정리되지만 TherminateThread 함수를 사용하면 시스템은 종료된 스레드를 소유하고 있던 프로세스가 살아있는 동안 그 스레드가 사용하였던 스레드 스택을 정리하지 않는다.
      • 또한 일반적으로 스레드가 종료되면 DLL은 스레드 종료 통지를 받게 되지만, TerminateThread를 사용하여 스레드를 강제 종료하면 DLL은 어떻나 통지도 전달받지 못하기 때문에 적절한 정리 작업을 수행하지 못할 수도 있다.
    • 프로세스가 종료되면
      • ExitProcess와 TerminateProcess 함수를 호출하는 경우에도 스레드는 종료된다. 차이점이라면 이러한 함수들을 호출하면 프로세스가 소유하고 있던 모든 스레드가 종료된다는 것이다.
      • 전체 프로세스가 종료되기 때문에 프로세스가 사용하던 리소스들도 모두 정리된다. 따라서 스레드들이 사용하던 스택들도 정리될 것이다. 프로세스를 강제 종료하면 프로세스 내에 남아 있는 스레드들에 대해 각각 TerminateThread 함수가 호출된다. 이와 같이 프로세스를 종료하게 되면 C++ 파괴자가 호출되지도 못하고 디스크로 자료를 저장하는 등의 적절한 정리 작업도 수행되지 못한다.
      • 애플리케이션의 진입점 함수가 반환되면 C/C++ 런타임 라이브러리의 시작 코드는 ExitProcess를 호출하게 된다. 만일 애플리케이션 내에서 여러개의 스레드가 동시 수행 중이었다면, 주 스레드가 종료되기 전에 각각의 스레드들에 대해 적절한 정리 작업이 수행되어야 할 것이다. 그렇지 않으면 수행 중인 다른 스레드들이 갑작스럽게 그리고 조용히 종료될 것이다.
    • 스레드가 종료되면
      • 스레드가 종료되면 다음과 같은 작업들이 수행된다.
        • 스레드가 소유하고 있던 모든 유저 오브젝트 핸들이 삭제된다. 윈도우에서는 대부분의 오브젝트들이 스레드에 의해서 생성되지만 (오브젝트를 생성한 스레드를 포함하고 있는) 프로세스에 의해 소유된다. 그런데 윈도우와 윈도우 훅 두 개의 사용자 오브젝트는 스레드에 의해 소유도니다. 스레드가 종료되면 시스템은 자동적으로 해당 스레드가 생성한 윈도우를 파괴하고, 설치한 윈도우 훅을 제거한다. 다른 형태의 오브젝트들은 모두 소유하고 있는 프로세스가 종료되는 시점에 파괴된다.
        • 스레드의 종료 코드는 STILL_ACTIVE에서 ExitThread나 TerminateThread에서 지정한 종료 코드로 변경된다.
        • 스레드 커널 오브젝트의 상태가 시그널 상태로 변경된다.
        • 종료되는 스레드가 프로세스 내의 마지막 활성 스레드라면 시스템은 프로세스도 값이 종료되어야 하는 것으로 간주한다.
        • 스레드 커널 오브젝트의 사용 카운트가 1만큼 감소한다.
      • 스레드가 종료된다 하더라도 스레드 커널 오브젝트는 핸들을 삭제하지 않는 이상 자동적으로 파괴되지 않는다.
      • BOOL GetExitCodeThread( HANDLE hThread, PDWORD pdwExitCode);
        종료된 스레드의 종료 코드를 획득해 오는 함수. 종료 코드는 pdwExitCode가 가리키는 DWORD 포인터를 통해 값을 반환한다.
      • 미처 종료되지 않았다면 STILL_ACTIVE, 함수가 성공적으로 호출되면 TRUE의 값이 반환될 것이다.
  • 6. 스레드의 내부
    • 스레드는 자신만의 가상 메모리 공간을 가지지 않으므로 스택으로 활용할 메모리는 프로세스의 주소 공간으로부터 할당된다.
    • 스레드 스택은 항상 상위 메모리로부터 하위 메모리 순으로 사용된다.
    • 각 스레드는 자신만의 CPU 레지스터 세트를 가지는데, 이를 스레드 컨텍스트라고 부른다. 이러한 컨텍스트는 스레드가 마지막으로 수행되엇을 당시의 스레드의 CPU 레지스터 값을 가지고 있다.
    • 스레드의 CPU 레지스터 세트는 CONTEXT 구조체(WinNT.h 헤더 파일에 정의되어 있다) 형태로 스레드 커널 오브젝트 내에 저장된다.
    • IP 레지스터와 SP 레지스터는 스레드 컨텍스트에 저장되는 값 중에서 가장 중요한 레지스터다. 스레드는 항상 프로세스의 컨텍스트 내부에서 수행된다. 두 레지스터의 값은 프로세스 메모리 공간 상의 특정위치를 가리키고 있다.
    • SP (stack pointer 레지스터) : pfnStartAddr를 저장
    • IP (instruction pointer 레지스터) : NTDLL.dll 모듈이 익스포트하고 있는 RtlUserThreadStart라는 문서화되지 않은 함수의 주소를 가리킨다.
    • 새로운 스레드가 RtlUserThreadStart 함수를 호출하면 다음과 같은 작업이 수행된다.
      • 스레드 함수 내에서 예외가 발생했을 경우 시스템이 제공하는 기본적인 예외 처리 코드를 수행할 수 있도록 구조적 예외 처리(structured exception handing / SEH) 프레임이 설정된다.
      • 시스템은 CreateThread 함수 호출 시 전달한 pvParam 매개변수로 스레드 함수를 호출한다.
      • 스레드 함수가 반환되면 RtlUserThreadStart 함수는 스레드 함수가 반환한 값을 인자로 ExitThread 함수를 호출한다. 스레드 커널 오브젝트의 사용 카운트는 점차 감소되고, 스레드는 수행을 종료한다.
      • 만일 스레드가 예외를 유발하고 이러한 예외가 처리되지 않으면 RTLUserThreadStart 함수가 설정한 SEH 프레임이 예외를 처리하게 된다. 이때 사용자에게 메시지  박스를 출력하는데, 사용자가 프로그램 닫기를 선택하면 RtlUserThreadStart는 ExitProcess를 호출하여 예외를 유발한 스레드뿐만 아니라 전체 프로세스를 종료시켜 버린다.
    • RtlUserThreadStart 함수는 C/C++ 런타임 라이브러리의 시작 코드를 호출하여 각종 초기화를 진행하과, _tmain이나 _tWinMain과 같은 진입점 함수를 호출한다. 진입점 함수가 반환되면 C/C++ 런타임 라이브러리 시작 코드는 ExitProcess를 호출한다. 따라서 C/C++ 애플리케이션의 주 스레드는 RtlUserThreadStart 함수로 절대 반환되지 않는다.
  • 7. C/C++ 런타임 라이브러리에 대한 고찰
    • 라이브러리 파일명 설명
      LibCMt.lib 릴리즈 버전의 정적 링크 라이브러리
      LibCMtD.lib 디버그 버전의 정적 링크 라이브러리
      MSVCRt.lib MSVCR80.dll에 대한 동적 링크를 위한 릴리즈 버전의 임포트 라이브러리
      MSVCRtD.lib MSVCR80D.dll에 대한 동적 링크를 위한 디버그 버전의 임포트 라이브러리
      MSVCMRt.lib 매니지드와 네이티브 코드가 섞여 있는 경우에 사용하는 임포트 라이브러리
      MSVCURt.lib 100% MSIL 코드로 컴파일된 임포트 라이브러리
    • 멀티스레드 애플리케이션에서 전통적인 C 런타임 라이브러리를 사용하였을 때 나타나는 문제점 : 스레드가 다른 스레드의 전역변수 값을 변경해버릴 수 있는 문제가 나타남
    • 멀티스레드 기반의 C/C++ 프로그램이 정상적으로 동작하려면 C/C++ 런타임 라이브러리 함수들은 사용하는 각 스레드별로 적절한 구조의 데이터 블록을 생성해야 한다. 또한 C/C++ 런타임 라이브러리 함수는 다른 스레드들로부터 영향을 받지 않도록 자신을 호출한 스레드의 데이터 블록에만 접근 가능해야한다.
    • 아래에 _beginthreadex 구현부에서 주목해야 할 부분을 나타냈다.
      • 각 스레드는 C/C+ 런타임 라이브러리 힙에 _tiddata 메모리 블록을 가진다.
      • _beginthreadex 함수에 전달된 스레드 함수의 주소는 _tiddata 메모리 블록내에 저장된다. 스레드 함수에 전달할 매개변수 또한 _tiddata 메모리 블록에 저장된다.
      • CreateThread가 호출되면 _beginthreadex의 pfnStartAddr 매개변수로 전달한 스레드 함수가 아니라 _threadstartex라는 함수가 수행하게 된다. 또한 스레드 함수로 전달할 매개변수도 _beginthreadex에 전달한 pvParam이 아니라 _tiddata 구조체의 주소다
      • 정상적인 경우 _beginthreadex는 CreateThread와 동일하게 스레드 핸들을 반환한다. 만일 문제가 발생하면 0을 반환한다.
    • 아래에 _threadstartex에서 주목해야 할 부분을 나타내었다.
      • 새로 생성된 스레드는 RtlUserThreadStart(NTDLL.dll 내에 있는)를 호출하고 곧 _threadstartex로 진입한다.
      • _threadstartex로는 새로 생성된 스레드의 _tiddata 블록을 가리키는 주소가 매개변수로 전달된다.
      • TlsSetValue는 이 함수를 호출하는 스레드와 매개변수로 전달되는 값을 연계시키는 운영체제 함수다. 이러한 값이 저장되는 공간을 스레드 지역 저장소(TLS)라고 한다. _threadstartex 함수는 새로 생성된 스레드와 _tiddata 블록을 연계시킨다.
      • 아무런 인자도 전달받지 않는 _callthreadstartex 함수 내에서는 사용자가 지정한 스레드 함수의 호출부를 둘러싸는 SEH 프레임을 구성한다. 이 프레임은 런타임 라이브러리와 관련된 많은 작업들을 수행하는데, 예를 들어 런타임 에러(처리되지 않은 C++ 예외)와 C/C++런타임 라이브러리의 signal 함수가 정상 동작하도록 작업을 수행한다. 이 부분은 매우 중요한데, 만일 CreateThread 함수를 이용하여 스레드를 생성한 후 C/C++ 런타임 라이브러리가 제공하는 signal 함수를 호출하게 되면 이 함수는 정상 동작하지 않는다.
      • 사용자 정의 스레드 함수가 사용자가 전달한 매개변수 값으로 호출된다. 스레드 함수의 주소와 매개변수 값은 _beginthreadex 함수 내에서 TLS에 저장하였던 _tiddata 블록을 _callthreadstartex 함수내에서 가져와서 사용한다.
      • 사용자가 지정한 스레드 함수의 반환 값을 스레드의 종료 코드가 된다. _callthreadstartex는 단순히 _threadstartex로 반환되고, 계속해서 RtlUserThreadStart로 반환되는 구조가 아님에 주목할 필요가 있다. 만일 그렇게 되면 스레드는 종료되고, 스레드의 종료 코드는 올바르게 설정될지 모르겠지만, 이러한 메모리 누수를 일으키지 않기 위해 _endthreadex라는 C/C++ 런타임 라이브러리 함수가 제공되며, 이 함수는 매개변수로 스레드 종료코드를 전달받는다.
    • _endthreadex에 대해 주목해야할 사항을 아래에 나타냈다.
      • C/C++ 런타임 라이브러릴 함수인 _getpth_noexit 함수는 이 함수를 호출하는 스레드의 _tiddata 메모리 블록을 가져오기 위해 내부적으로 운영체제의 TlsGetValue 함수를 호출한다.
      • _tiddata 블록이 삭제되고 운영체제의 ExitThread 함수가 호출되어 스레드를 실제로 파괴한다. 물론 이 과정에서 종료 코드가 전달되고 올바르게 설정된다.
    • ① 실수로 _beginthreadex 대신 CreateThread를 호출했다
      • 스레드 내에서 C/C++ 런타임 라이브러리 함수를 호출하려면 _tiddata 구조체가 필요한데, 바로 이것이 문제다 (대부분의 C/C++ 런타임 라이브러리 함수들은 스레드 안전하며 _tiddata 구조체가 필요하지 않다.)
      • 스레드의 데이터 블록을 가져오려고 시도하지만 NULL이 반환될 것이다. 그러면 C/C++ 런타임 함수는 호출하는 스레드에서 사용할 _tiddata 블록을 새로 할당하고 초기화하여 사용한다.
      • 제약 사항 없이 수행될 수 있는 것 처럼보이지만 문제가 있다
        • 첫째, 스레드가 C/C++ 런타임 라이브러리가 제공하는 signal 함수를 사용하는 경우 구조적 예외 처리 프레임이 준비되지 않았기 때문에 이 함수를 호출하면 프로세스가 종료되어 버린다.
        • 둘째, 스레드가 _endthreadex를 호출하지 않고 종료되어 버리면, 데이터 블록을 삭제되지 않아 메모리 누수가 발생한다.
    • ② 절대 호출하지 말아야 하는 C/C++ 런타임 라이브러리 함수
      • _beginthread, _endthread
      • 매개변수의 개수가 조금 부족하기 때문에 모든기능을 제공하지 못한다
      • _beginthread는 보안 특성을 가진 스레드를 생성할 수 없으며, 일시 정지된 상태의 스레드로 생성할 수 없고, 스레드의 ID 값을 얻을 수 없다.
      • _endthread는 어떠한 매개변수도 가지지않기 때문에 스레드의 종료 코드는 항상 0이 된다.
      • 스레드 함수가 반환되면 _beginthread 함수가 _endthread를 호출하는 것과 같이 _beginthreadex 는 _endthreadex를 호출한다
  • 8. 자신의 구분자 얻기
    • 스레드가 자신의(또는 자신이 속한 프로세스의) 수행 환경을 변경하는 것은 매우 일반적인 작업이기 때문에 윈도우 운영체제는 스레드가 자신을 소유하는 프로세스의 커널 오브젝트나 자신을 나타는 커널 오브젝트를 손쉽게 얻을 수 있는 함수를 제공하고 있다.
    • HANDLE GetCurrentProcess();
      HANDLE GetCurrentThread();
      이 두개의 함수는 해당 함수를 호출한 스레드를 소유하고 있는 프로세스나 스레드 자신을 나타내는 스레드 커널 오브젝트의 허위 핸들(pseudohandle)을 반환한다.
    • ① 허위 핸들을 실제 핸들로 변경하기
      • 실제 핸들이란 특정 스레드를 대표할 수 있는 고유의 핸들 값을 의미한다.
      • DuplicateHandle 함수를 이용하면 변경할 수 있다.
반응형
Comments