개요
우리는 학교 과제를 하기 위해 HWP나 Word, Excel, PPT 등의 응용프로그램을 실행해야 한다. 하지만 단순히 이러한 동작만을 하는 것이 아니라 노래를 듣는 동시에 코드를 짜거나, 이에 더해 PC 톡으로 친구들에게 물어보기도 한다. 우리가 이러한 프로그램들을 실행하면 결과적으로 CPU에서 명령어를 처리하게 된다. 그렇다면 어떻게 CPU가 하나뿐이더라도 동시에 여러 작업이 가능해지는 것일까? 이를 위해 프로세스와 스레드가 어떻게 동작하는지에 대하여 알아보자.
프로그램은 일반적으로 하드 디스크 등에 저장되어 있는 실행코드를 뜻하고, 프로세스는 프로그램을 구동하여 프로그램 자체와 프로그램의 상태가 메모리 상에서 실행되는 작업 단위를 지칭한다. 예를 들어, 하나의 프로그램을 여러 번 구동하면 여러 개의 프로세스가 메모리 상에서 실행된다. 프로세스란 단순히 사용자가 실행시킨 실행 파일만을 필요로 하는 것이 아니라 프로그램을 읽어 들일 메모리와 데이터를 보관할 메모리, CPU의 레지스터 그리고 Windows의 경우 윈도우와 파일 등의 리소스도 사용하여 프로그램을 작동시킨다. 이처럼 프로그램을 관리하기 위한 환경을 묶어서 'Context'라고 한다.
멀티 태스크
어떻게 여러 작업을 동시에 처리할 수 있을까? 위에서 말했다시피 모든 명령어는 CPU에서 처리해주므로 동작하게 된다. CPU가 두 개일 경우 동시에 두 가지의 작업을 각각의 CPU에서 처리할 수 있겠지만, 하나의 CPU만 쓰는 PC에서는 어떻게 이러한 동시 작업이 가능해지는가에 대답은 바로 우리가 그렇게 느끼도록 하는 것이다. 사실 동시에 작업을 한다고는 하지만 정확히 말하자면 아주 짧은 시간마다 작업을 전환하여 모든 작업이 동시에 처리되는 것마냥 느끼도록 한다.
예를 들어 위의 그림과 같이 몇 개의 프로세스가 메모리에 올라와 있다고 하자. 이때 CPU가 처리할 수 있는 것은 하나의 작업뿐이다. 그렇기에 먼저 프로세스 A를 처리하고, 처리가 끝나면 이를 다시 중지하고 프로세스 B를 처리한다. 마찬가지로 B를 처리하다가 다음엔 C를 처리하는 방식으로 동작하게 된다. 이렇게만 이야기하면 당연히 멀티 태스크라는 말이 안 어울리며 단일 태스크인 것 같지만, 아주 빠른 시간 내에 이런 작업이 이루어진다면 사용자가 느끼기에는 충분히 멀티 태스크가 되는 것이다.
그렇다면 태스크의 전환은 어떻게 이루어지는지에 대하여 프로세스를 예로 알아보자. 우선 응용프로그램이 자발적으로 운영체제에게 제어를 넘기는 것이다. 이를 '비선점형 멀티 태스크'라고 하며 응용프로그램의 처리가 끝나서 함수에서 빠져나오면 Windows로 제어권이 반환되고 이 시점에서 다른 응용프로그램으로 전환하는 경우가 예이다. 하지만 이러한 협조 방식에는 문제가 있다. 바로 하나의 응용프로그램에서 제어를 반환하지 않는다면 다른 모든 응용프로그램은 동작할 기회를 갖지 못하게 된다. 운영체제는 제어권을 응용프로그램으로부터 기다려야 하기 때문에 이는 결국 운영체제가 프로세스의 실행을 제어한다고 하기 어렵다고 할 수 있다.
이러한 단점으로 인해 나온 것이 바로 '선점형 멀티 태스크' 구조로 운영체제는 응용프로그램이 제어를 넘겨줄 때까지 기다릴 필요 없이 강제적으로 실행을 중단시켜 다른 응용프로그램으로 전환 시킬 수 있다. 이는 프로세스 실행의 제어권을 운영체제가 가지고 있다고 할 수 있으며, 강제적으로 실행을 중단시키는 방법에 사용되는 것이 바로 'Interrupt'로 이는 하드웨어로부터 제어신호와 CPU 명령 처리 중에 일어나는 여러 가지 이벤트를 계기로 강제로 특정 코드를 실행하도록 하는 CPU 기능의 일종이다. 인터럽트 처리에도 여러 종류가 있지만 그 중 하드웨어 타이머(클럭)에서 정기적으로 인터럽트를 거는 것이 있다. 운영체제는 이를 설정해서 정기적으로 인터럽트를 받고, 필요에 따라 프로세스 전환에 사용한다.
선점형 태스크 전환은 태스크의 작업이 완료되지 않아도 CPU를 선점하므로 이를 위해선 태스크 스위칭이 일어날 때 기존에 실행중인 태스크의 상태를 보존하고 있다가, 이후 재선점 시에 이를 가지고 올 필요가 있다. 이러한 작업 내용 저장 위치는 작업 단위에 따라 달라지게 된다. 프로세스를 하나의 작업 단위로 볼 경우 PCB(Process Control Block)에 대하여 알아야 한다. PCB에는 프로세스에 대한 다양한 정보들이 저장되어 있는데 스위칭이 일어날 때 바로 이 PCB와 관련된 동작을 수행한다.
만약 현재 Process #A가 CPU에서 실행 중인 상태에서 Process #B로 전환이 일어나면 Process #A가 Ready 상태가 되면서 해당 프로세스의 상태나 레지스터 값 등이 Process #A의 PCB에 저장된다. 반대로 Process #B는 Running 상태가 되면서 Process #B의 PCB에 저장된 내용들을 CPU로 적재 시킨다. 이와 같은 작업을 통해 Context Switching을 진행한다.
하지만 이러한 Process의 Context Switching은 상대적으로 많은 내용을 메모리에서 CPU로 옮기고 다시 CPU에서 메모리로 옮기는 작업을 수행해야 한다. 이에 반해 Thread를 작업 단위로 볼 경우 훨씬 용이한 Context Switching가 가능해진다. Thread는 Process 보다 작은 작업 단위임을 위에서 언급하였다. 이는 Process 마다 하나의 PCB와 주소 공간을 가지는 반면에 Thread의 경우 하나의 Process 안에서 많은 내용을 공유한다. 아래의 그림을 보자.
Case #1의 경우 Process A가 Process B를 생성한 것으로 두 프로세스 간에 전혀 공유되는 요소가 없이 각각의 요소를 서로 갖고 있는 것을 확인할 수 있다. 이에 반해 Case #2의 경우 Process C가 Thread #1과 Thread #2를 생성한 경우로 두 Thread 간에 Code, Data, Heap을 Process 안에서 공유하고 있다. 다시 말해 각각의 Thread는 Stack 영역만을 개별적으로 갖는다. 아래는 실제 프로그램의 메모리를 나타낸 것으로 네 개의 Thread가 각각의 Stack 공간을 갖고 있는 것을 확인할 수 있다.
Memory map Address Size Owner Section Contains Type Access Initial >Mapped as 006FE000 00002000 > > Stack of main thread Priv >RW RW 00A8C000 00002000 > > 00A8E000 00002000 > > Stack of thread 2. >Priv >RW RW 00B8D000 00002000 > > 00B8F000 00001000 > > Stack of thread 3. >Priv >RW RW 00C8D000 00002000 > > 00C8F000 00001000 > > Stack of thread 4. >Priv >RW RW |
이에 관련하여 Thread를 생성하는 API인 CreateThread()에도 해당 Thread에게 할당하고자 하는 Stack의 크기를 지정해줄 수가 있다. 이와 같이 Thread릁 통한 태스크 전환은 프로세스 기준 태스크 전환보다 훨씬 교환해야 할 요소가 적어 리소스를 덜 사용하게 된다. Context Switching이 일어나는 동안 CPU는 아무런 일을 하지 못한다는 점을 고려했을 때, 이러한 시간을 줄이는 것은 당연히 중요한 요소가 된다. 따라서 유사한 동작을 하는 두 개의 Task를 만들 경우 굳이 새로운 Process로 하는 것보단 데이터를 공유하는 Thread로 하는 것이 효율적이다.
HANDLE WINAPI CreateThread( _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ SIZE_ dwStackSize, _In_ LPTHREAD_START_ROUTINE lpStartAddress, _In_opt_ LPVOID lpParameter, _In_ DWORD dwCreationFlags, _Out_opt_ LPDWORD lpThreadId ); |
우선 순위
마지막으로 스케줄링 우선 순위에 대하여 알아보자. 여러 개의 응용프로그램을 좋은 효율로 동작하게 하려면 어떤 것을 먼저 어느 만큼 실행할 지가 매우 중요하다. 이러한 조절을 '스케줄링'이라고 하며 이는 운영체제에 따라 조금씩 방식이 다르다. Windows의 경우 Thread 우선 순위에 기반한 방식과 라운드-로빈 방식을 조합한 것이다. 우선 순위는 어떤 Thread를 우선해서 실행할 것인가를 나타내는 정수 값으로 0~31까지의 우선 순위가 있으며, Windows 에서는 Process 마다 기본적인 우선순위를 나타내는 Priority Class를 지정하고 있다. 물론 이러한 Thread 우선순위는 Process와 Thread를 생성할 때 지정되며, 실행 도중 SetThreadPriority API를 호출하여 우선순위를 변경할 수 있다.
상수 기본 우선 순위 REALTIME_PRIORITY_CLASS 24 HIGH_PRIRITY_CLASS 13 ABOVE_NORMAL_PRIORITY_CLASS 10 NORMAL_PRIORITY_CLASS 8 BELOW_NORMAL_PRIORITY_CLASS 6 IDLE_PRIORITY_CLASS 4
Windows는 실행 큐를 우선 순위가 높은 것부터 조사해서 실행 큐의 앞쪽으로 옮긴다. 그리고 CPU에 있는 Thread가 대기 상태로 넘어가게 되면 새로운 우선순위가 높은 실행 큐를 CPU로 가지고 와서 실행한다. 그리고 대기 중인 Thread에 있는 Thread가 실행 준비가 되면 실행 Queue로 옮겨져 CPU에 의해 동작되길 다시 기다린다. 이와 같은 전환은 Thread가 주어진 시간 간격을 사용한 경우 외에도 디스크 접근이나 동기화 등을 위해 자발적인 대가 상태에 들어간 경우와 실행 도중에 우선순위가 더 높은 Thread의 실행 준비가 끝났을 경우 발생한다.
하지만 이처럼 규칙에 충실한 스케줄링이 안 좋을 때가 있다. 예를 들어 우선순위가 높은 Thread가 하나일 경우에는 남겨진 Thread가 영원히 실행되지 않는다. 그래서 실행 준비를 마친 Thread가 일정시간 동안 실행 기회를 얻지 못했을 경우 Windows에서는 그 Thread의 우선순위를 일시적으로 올려주도록 되어 있다. 또한 디스크 접근이 완료되어 실행준비가 되었을 때나 GUI Thread가 동작을 시작했을 때도 그 Thread의 우선순위를 일시적으로 올려준다. 이런 조작을 'Priority Boosts'라고 한다.
Reference
+ [API로 배우는 Windows 구조와 원리] p.33 ~ p.38
+ "컨텍스트 스위칭", http://blog.eairship.kr/257
+ "스레드 다루기 (기초편)", http://www.jiniya.net/wp/archives/7194
+ "x86에서 TSS", http://onestep.tistory.com/31
+ MSDN, " https://msdn.microsoft.com/"
+ "프로세스가 뭐지?", http://bowbowbow.tistory.com/16
'O / S > Window' 카테고리의 다른 글
네트워크 패킷 캡처 (Windows) (0) | 2020.11.25 |
---|---|
Windows Event Message (0) | 2016.07.13 |
Windows Service (0) | 2016.06.21 |
Windows Boot Process (Vista 이상ver 부팅 과정) (0) | 2016.04.13 |
CSIDL 값 (0) | 2016.02.20 |