Windows OS - 스레드(Thread) 1편
아주 기본적인 스레드의 개념.
'프로세스는 왜 존재하는가' 를 생각해본다.왜 존재하는가? 간단하다.
프로세스는 프로그램 실행을 위해 존재한다.
프로그램 실행은 어떻게 이루어지는가?
프로그램은 명령어의 집합이다.
기계어 명령어를 파일로 만들면 그게 우리가 실행파일이라고 부르는것이 된다.
기계어 명령어가 CPU로 전달되면 CPU는 그것을 이해하고 실행한다.
이게 프로그램의 실행이다.
아무튼, 프로세스는 프로그램 실행을 위해 존재한다고 했다.
여기서 스레드라는 개념이 등장한다.
스레드라는 개념은 프로세스의 개념보다는 비교적 늦게 등장했다.
프로세스가 가지는 프로그램 실행의 개념을 스레드에게 100% 이식했다.
간단한 비유를 하자면 부서가 분리된 것이다.
이 스레드라는 것이 프로그램을 실행한다.
프로세스와 스레드의 관계는 ... 마치 어항과 물고기 같다고 볼 수 있다.
물고기가 없는 어항은 필요가 없다.
스레드가 모두 종료되면 OS가 프로세스를 파괴한다.
어항이 깨지면 물고기는 모두 죽는다.
프로세스가 종료되면 스레드도 모두 종료된다.
아주 간단하고도 본질적인 것부터 생각해보도록한다.
'스레드가 프로그램 실행을 합니다' 라고 이야기한다면,무슨일을 하는지는 알아도, 이게 실제적으로 어떻게 구현된것인지 모를것이다.아마도 스레드라는 것을 굉장히 비밀스러운 것으로 여길수도 있겠다.
Windows는 Thread라는 개념을 어떻게 구현했는가?
1. OS가 관리할 스레드 커널 오브젝트라는 데이터 블럭이건 커널 메모리 안에 생성되고, OS에 의해 관리된다.이 커널 오브젝트라는 블럭안에는 OS가 관리를 위해넣어둔 많은 정보가 있다.그 중에서도 레지스터 정보를 저장해두는 공간이 있는데,이것이 잘 알려진 Context 이다.
2. 프로그램 실행시 사용할 스택이 있어야한다.
이 스택은 프로세스의 것으로 할당된 메모리를 떼어와서 사용한다.
보통 스레드 스택이라고 불리는것이 이것이다.
간단한 Hello World 를 출력하는 프로그램을 실행하는 상황을 생각해보자.
우리가 바탕화면에 있는 HelloWorld.exe 파일을 더블클릭했다고 하자.
그러면 프로그램이 실행되고, 화면에 Hello World가 출력된다.
이때에 많은 서적들이 'ROM에 있는 프로그램이 RAM으로 올라간다'고
표현한다.
프로세스와 스레드의 개념은 그 다음에 일어나는 일을 알게해준다.
HelloWorld.exe 실행을 위해서 OS는 프로세스를 하나 만든다.
프로세스에 속한 스레드가 실행을 한다고 했다.그래서 스레드는 하나가 자동적으로 생성된다.익히 알고있는 주 스레드(Primary Thread)가 그것이다.
'ROM에 있는 프로그램이 RAM으로 올라간다' 는 사실 축약된 의미이고,
다음과 같은 일들이 일어난다.
커널 메모리 안에 프로세스 커널 오브젝트가 생성된다.커널 메모리 안에 스레드 커널 오브젝트가 생성된다.유저 메모리 안에 HelloWorld.exe를 위한 주소공간이 할당된다.
스레드 커널 오브젝트안에 있는 Context에는레지스터 SP(Stack Pointer)에 저장될 값과레지스터 IP(Instruction Pointer)에 저장될 값이 들어가 있다.이제 감이 올 것이다.
CPU 가 Context를 읽어들이고, 프로그램 코드를 하나씩 쭉쭉 실행해 나가면 그것이 곧 프로그램의 실행이 되는것이다.
이제 프로그램이 실행시 어떤일이 일어나는지 간략하게는 이해가 될 것이다.
스레드 커널 오브젝트
스레드 커널 오브젝트 안에는 많은 정보가 있다.당연히 커널 메모리 안에 위치한다.
스레드의 우선순위, 컨텍스트, 상태, 정지상태플래그 등이 여기 들어간다.
당연히 스레드 커널 오브젝트가 생성되면 OS는 핸들의 형태로 준다.
이 핸들을 잘 닫아야 스레드 커널 오브젝트가 삭제된다.스레드 종료와 잘 구분하자.스레드 커널 오브젝트가 삭제되는건 어디까지나 사용카운트가 0이 되었을 때이다.보통 스레드를 생성함수 호출 이후 closehandle을 바로 호출하는 것을 볼 수있다.스레드 커널 오브젝트를 OS가 정리하는 시점을 스레드 종료와 맞추기 위한 것이다.스레드를 생성요청한 프로세스가 참조하고 있으니 생성직후의 사용카운트는 2이다.1이 아니다. 이걸 잘 기억한다.
Context Switching
스레드에 대해서 자주 이야기하는 것 중 하나가 바로컨텍스트 스위칭(Context Switching) 이다.
'프로세스와 스레드의 차이' 를 가장 쉽게 이해할수 있는 부분이기도 하다.
같은 프로세스내의 스레드는 스택을 제외하고 코드,데이터,힙 영역에 대한 가상메모리 매핑을 공유한다.
따라서 같은 프로세스내의 스레드가 컨텍스트 스위칭시 더 빠르게 진행될 수 있다.
C/C++ 프로그램의 스레드 생성
스레드의 생성은 CreateThread 를 통해 진행한다.
실질적으로 스레드를 생성할 수 있는 방법은 CreateThread 호출 뿐이다.std::thread 또한 CreateThread 를 호출한다.
_beginthreadex 등의 CRT 함수들도 모두 CreateThread 를 호출한다.
스레드를 생성한다고 알려진 많은 방법이
윈도우즈에서는 결국 내부적으로 CreateThread를 호출한다는 것이다.
CreateThread 가 호출되면, 시스템은 스레드 커널 오브젝트를 생성한다.그리고 적절한 값으로 초기화를 한다.그리고 스레드 스택을 하나 만든다.상술했듯이 스레드 스택은 프로세스가 받은 주소공간내에서 떼어서 만든다.OS는 이렇게 만든 스레드 스택에 pvParam과 lpStartAddres으로 들어온 값을 차례대로 쓴다.
그리고 스레드 컨텍스트 내의 IP를 RtlUserThreadStart라는 함수의 시작주소로 세팅하고,
SP를 lpStartAddress가 저장된 주소공간, 즉 스레드 스택으로 세팅한다.
그런다음, CREATE_SUSPENDED 플래그가 세팅되어 있지 않으면,정지 카운트를 1에서 0으로 바꾼다.이제 CPU에 스케쥴링 될 수 있는 상태가 되는 것이다.이제부터 새로운 스레드가 실행되는 상태가 된다.Instruction Pointer 는 RtlUserThreadStart로 설정되어 있다.스레드의 진입점이 RtlUserThreadStart부터 이루어지는 것이다.
RtlUserThreadStart 함수는 SEH를 세팅하고,
스레드 내부에서 예외가 발생하면 프로세스를 종료하도록 구현되어있다.
RtlUserThreadStart는 대략적으로 다음과 같이 구현되어있다.
VOID RtlUserThreadStart(PHTREAD_START_ROUTINE pfnStartAddr, PVOID pvParam)
{
__try
{
ExitThread( (pfnStartAddr)(pvParam) );
}
__except( UnhandledExceptionFilter( GetExceptionInformation() ) )
{
ExitProcess(GetExceptionCode());
}
}
스레드 함수를 실행하고, 반환값으로 스레드의 종료코드를 설정한다.
종료코드로 전달된 정수 값은 스레드 커널 오브젝트에 세팅될 것이다.
또한 위에서 보이듯이 RtlUserThreadStart 는 어떠한 일이 있어도 반환 되지 않는다.Why?스레드를 새로 만들지 않았는가? 리턴문을 실행해도 돌아갈곳의 주소가 스택에 없기때문이다.
_beginthreadex 를 사용하자.
_beginthreadex 또한 내부적에서 CreateThread를 호출한다.단, _beginthreadex는 인자로 전달된 start_address를 CreateThread에 전달하지 않는다.
_beginthreadex 를 호출하면,
CreateThread를 호출하기 전에,
만들어질 스레드가 사용할 데이터블럭을 하나 만든다.
이걸 PTD(per-thread-data) 라고 부른다.
당연히 데이터블럭은 힙에 만들어야 한다. 동적할당이다.
왜 힙에 만들어야 하는지 잘 이해가 안간다면,
상술된 CreateThread를 호출하면 일어나는 일에 대해 다시 한번 기억을 되짚어 본다.
CreateThread를 호출한 스레드는 그냥 스레드 커널 오브젝트에 대한 핸들을 받는다.
OS는 뭘한다? OS는 스레드 커널 오브젝트를 초기화하고,
RtlUserThreadStart의 주소를 스레드 커널 오브젝트 안에 있는 컨텍스트 내의 IP로 설정한다.
그리고 CPU를 사용할 수 있도록 스케쥴링 되면,
RtlUserThreadStart부터 차근차근 실행되는 것이다.
PTD 구조체에 값들을 채워넣는다.
pMyParam과 pMyFunc1 을 인자로 넣었다고 하자.
이 ptd 구조체 안에 pMyParam과 pMyFunc을 넣는다.
또한 PTD 구조체안에 CRT의 함수들이 사용하게 될 전역변수를 저장시킨다.
멀티스레드 사용시에 발생할 데이터 레이스를 고려한 것이다.
PTD 세팅이 끝났다면, CreateThread에 PTD 를 pvParam으로, threadstartex를 스레드의 진입점으로 전달한다.그리고 나오는 CreateThread 가 리턴하는 핸들을 리턴한다.
threadstartex는, 함수의 종료시에 동적할당 받았던 PTD구조체를 할당해제 하도록 되어있다.
그런데 ! CreateThread 를 할때에도 CRT관련된 전역변수를 힙에 생성하는 기능이 생겨버렸다.이경우, MyFunc 내에서 _endthreadex를 호출해야 한다. 그렇지 않으면 꼼짝없이 데이터 누수이다.
그러니 속 편하게 _beginthreadex를 사용하자.
스레드의 종료
스레드가 종료되면, 다음과 같은 일이 일어난다.
1. 스레드가 소유하고 있던 오브젝트 핸들이 삭제된다.이건 윈도우창과 , 윈도우 훅만 해당된다.그러니까 윈도우 창은 스레드가 종료될때 같이 꺼지는 것.
윈도우창과 윈도우 훅을 제외한 오브젝트에 대해서는,
스레드가 오브젝트 생성에 대한 요청을 OS에 하면,
해당 스레드가 속한 프로세스가 소유함으로 간주한다.
2. 당연히 스레드 커널 오브젝트의 사용카운트가 1 감소한다.
3. 스레드 커널 오브젝트의 종료코드가 STILL_ALIVE에서 설정된 종료코드로 바뀐다.
프로세스와 마찬가지로 TerminateThread등의 방법으로 종료는 바람직 하지 못하다.
반드시 스레드 함수에서 종료코드를 리턴하는 방식으로 종료하자.
'Windows OS' 카테고리의 다른 글
Windows OS - Windows 제공 Reader/Writer lock SRWLock (0) | 2022.02.20 |
---|---|
Windows OS - CriticalSection Spinlock으로 사용하기 (0) | 2022.02.20 |
Windows OS - 스레드(Thread) 2편 (0) | 2022.02.01 |
Windows OS - C/C++ App과 Process(프로세스) 기본 (0) | 2022.01.02 |
Windows OS - Kernel Object(커널 오브젝트) (0) | 2022.01.02 |
댓글
이 글 공유하기
다른 글
-
Windows OS - CriticalSection Spinlock으로 사용하기
Windows OS - CriticalSection Spinlock으로 사용하기
2022.02.20 -
Windows OS - 스레드(Thread) 2편
Windows OS - 스레드(Thread) 2편
2022.02.01 -
Windows OS - C/C++ App과 Process(프로세스) 기본
Windows OS - C/C++ App과 Process(프로세스) 기본
2022.01.02 -
Windows OS - Kernel Object(커널 오브젝트)
Windows OS - Kernel Object(커널 오브젝트)
2022.01.02