스레드의 이해
스레드란 무엇인가?
멀티 프로세스 기반 프로그램
둘 이상의 서로 다른 프로그램 실행을 위해서 둘 이상의 프로세스를 생성하는 것은 지극히 당연한 일이다.
음악을 듣기 위해 MP3 플레이어를 실행시키고, 문서 작업을 위해 WORD 프로그램을 실행시키면 기본적으로 두 개의 프로세스는 생성된다. 그러나 하나의 프로그램이 두 가지 이상의 일(흐름)을 동시에 처리하기 위해서도 둘 이상의 프로세스가 필요하다.(여기서의 흐름은 코드 실행을 말한다)
멀티 프로세스 운영체제 기반 프로그램의 문제점과 새로운 제안
두 가지 이상의 일을 동시에 처리하기 위해서, 혹은 둘 이상의 실행 흐름이 필요해서 추가적으로 프로세스를 생성하는 작업은 상당히 부담스럽다. 많은 수의 프로세스 생성은 빈번한 컨텍스트 스위칭으로 이어져 성능에 영향을 미치기 때문이다.
빈번하게 발생하는 컨텍스트 스위칭에 소요되는 시간을 줄이기 위해서는 저장하고 복원하는 컨텍스트 정보의 개수를 줄여주면 된다. 컨텍스트 정보란 프로세스의 상태 정보와 관련이 있으므로 결국은 프로세스 상태 정보를 줄여야 한다는 결론이 나온다.
해결책 스레드
MP3 플레이어와 WORD 프로그램 관계처럼 완전히 별개의 프로그램을 동시 실행하기 위해서는 어쩔 수 없이 독립된 프로세스 구조가 필요하다. 하지만 하나의 프로그램 내에서 둘 이상의 실행 흐름을 두기 위해서 모든 것을 독립시키는 구조로 갈 필요는 없어 보인다. 이러한 생각과 고민 끝에 등장한 것이 스레드이다. 중요한 것은 프로세스와 스레드의 차이점이다.
프로세스는 완전히 독립된 두 개의 프로그램 실행을 위해서 사용된다. 그러나 스레드는 하나의 프로그램 내에서 둘 이상의 흐름을 만들어 내기 위해서 디자인된 것이다. 그리고 프로세스와 달리 스레드 간에는 공유하는 상태 정보들이 있다. 이것이 스레드의 컨텍스트 스위칭을 빠르게 하는 요인이 된다. 코드 영역은 공유하고 스택을 별도로 가져가면 굳이 자식 프로세스를 만들 필요가 없다고 생각해서 만들어진 게 스레드이다.
- 스레드는 하나의 프로그램 내에서 여러 개의 실행 흐름을 두기 위한 모델이다.
- 스레드는 프로세스처럼 완벽히 독립적인 구조가 아니다. 스레드들 사이에는 공유하는 요소들이 있다.
- 스레드는 공유하는 요소가 있는 관계로 컨텍스트 스위칭에 걸리는 시간이 프로세스보다 짧다.
메모리 구조 관점에서 본 프로세스와 스레드
프로세스와 스레드의 차이점을 이해하는 가장 좋은 방법은 메모리 구조 관점에서 비교하는 것이다.
아래 그림은 하나의 프로세스 내에서 두 개의 자식 프로세스를 생성했을 때의 메모리 구조를 보여준다.
자식 프로세스가 생성되고 난 다음에는 모든 것이 부모 프로세스와 독립적이다.
물론 부모 프로세스는 자신이 가지고 있는 핸들 테이블을 상속하는 등 부모로서의 역할에 충실하지만, 이는 어디까지나 생성 과정에서 발생하는 일이다. 메모리 구조상에서 보면 생성 이후에는 아무런 관계가 없다. 이러한 메모리 구조를 지녔기 때문에 프로세스 간에 데이터를 주고받기 위해서 IPC라는 메커니즘이 필요한 것이다.
위 그림은 하나의 프로세스 내에서 두 개의 프로세스 대신에 스레드를 생성했을 때의 메모리 구조를 보여준다.
위 그림에서 보여주듯이 스레드를 생성할 때마다 해당 스레드만을 위한 스택을 생성할 뿐 그 외의 영역은 부모 프로세스 영역을 공유하고 있다. (사실 스레드는 자식 프로세스가 아니므로 부모 프로세스라는 표현은 맞지 않다. )
스레드의 특성 1 : 스레드마다 스택을 독립적으로 할당해 준다.
프로세스와 마찬가지로 스레드도 독립적으로 스택을 할당한다.
스택은 함수 호출 시 전달되는 인자, 되돌아갈 주소값 및 함수 내에서 선언하는 변수 등을 저장하기 위한 메모리 공간이다. 간단히 말해서 함수 호출 시 필요한 메모리 영역이다.
따라서 이 메모리 공간이 독립적이라는 뜻은 추가적인 실행 흐름을 만들 수 있다는 의미가 된다.
다시 말하면 실행 흐름의 추가를 위한 최소 조건이 독립된 스택의 제공이다.
스레드의 특성 2 : 코드 영역을 공유한다.
프로세스의 경우 100% 독립된 구조이기 때문에 위와 같은 형태의 실행이 불가능하다.
그러나 스레드는 자신을 생성한 프로세스가 가지고 있는 함수를 호출할 수 있다.
왜냐하면 코드 영역을 공유하기 때문이다.
위 그림은 코드 영역을 공유한다는 것에 대한 본질을 설명하고 있다.
프로세스이므로 프로그램 흐름의 첫 시작인 main 함수가 있다.
그리고 이 영역에 또 다른 실행 흐름을 의미하는 스레드의 main 함수가 있다.
이 스레드 main 함수는 코드 영역에 존재하는 모든 함수를 호출할 수 있다. 프로세스 main 함수처럼 말이다.
위 그림에서 보면 총 3개의 main 함수가 존재한다(프로세스 main 하나, 스레드 main 둘). 결과적으로 프로그램의 흐름은 총 세 개가 된다.
“그렇다면 스레드가 있는 프로그램은 시작과 동시에 총 3개의 흐름을 형성하게 되나요?”
그건 아니다! 마치 자식 프로세스를 생성하는 부모 프로세스처럼, 스레드 생성을 위해 마련된 함수 호출을 통해서 스레드를 생성하게 된다. 즉 특정 함수 호출을 통해 스레드를 생성하는 거지 아무 함수나 호출한다고 해서 스레드가 생성돼 흐름이 나뉘는 게 아니다.
스레드의 특성 3 : 데이터 영역과 힙을 공유한다.
IPC가 필요 없어졌다. 스레드 간에 힙과 데이터 영역을 공유하기 때문에 스레드 A가 접근할 수 있는 힙과 데이터 영역은 스레드 B도 접근할 수 있다. 따라서 힙이나 데이터 영역에 메모리 공간을 할당해서 서로 통신하는 것이 가능하다. IPC와 같은 복잡한 통신기법은 필요가 없다. 좀 더 직관적으로 설명하면, 전역변수와 malloc 함수를 통해서 동적 할당된 메모리 공간은 공유가 가능하다.
Windows에서의 프로세스와 스레드
스케줄러라고 표현을 하면 프로세스 스케줄러를 의미하는 것으로 이해했을 것이다.
마찬가지로 스레드도 CPU의 시간을 할당받아서 실행되기 때문에 스레드 스케줄러의 존재도 추측해 볼 수 있다.
과연 이들의 관계는 어떻게 될까?
내부 커널의 구현 원리 따라서 나뉘게 된다. 이 책에서는 Windows에 한정적으로 이야기를 한다.
결론부터 얘기하면 Windows 입장에서 프로세스는 단순히 스레드를 담는 상자에 지나지 않는다.
때문에 실제 프로그램의 흐름을 형성하는 것은 스레드이다.
그렇다면 앞서 언급했던 프로세스 스케줄러나 프로세스 상태 정보들은 어떻게 이해해야 하는가?
사실 Windows 운영체제에 있어서 프로세스는 상태(Running , Ready , Blocked)를 지니지 않는다.
상태를 지니는 것은 프로세스가 아니라 스레드이다.
뿐만 아니라, 스케줄러가 실행의 단위로 선택하는 것도 프로세스가 아닌 스레드이다.
즉 Windows에 있어서 실행의 중심에 있는 것은 프로세스가 아닌 스레드이다.
그렇다면 프로세스 컨텍스트 스위칭도 존재하지 않는 것일까?
위 그림을 보자 프로세스 B안에 두 개의 스레드가 존재한다.
이 둘은 하나의 프로세스 내에 존재하므로 별개의 스레드가 아니다. 이 둘 사이에서 발생하는 컨텍스트 스위칭은 우리가 알고 있는 프로세스의 컨텍스트 스위칭에 비교해서 그 속도가 훨씬 빠르다. 그러나 프로세스 A의 스레드가 실행되는 도중에 프로세스 B의 스레드로 실행을 옮기는 과정에서 발생하는 컨텍스트 스위칭은 우리가 알고 있는 프로세스 컨텍스트 스위칭과 다를 바가 없다. 자신이 속해 있는 프로세스 영역이 다르면 공유하는 영역이 있을 수 없기 때문이다. 즉 윈도우에서 프로세스 컨텍스트 스위칭의 의미는 서로 다른 프로세스 내에 존재하는 스레드들 사이에서 발생하는 컨텍스트 스위칭을 의미한다.
스레드 구현 모델에 따른 구분
지금까지는 스레드의 특성에 대해서 이야기했다면, 이번에는 스레드의 구현 원리에 대해서 이야기하고자 한다.
스레드의 구현 원리? 쉽게 말해서 스레드가 어떻게 만들어지느냐 하는 것이다.
지금까지 설명했던 스레드! 과연 누구에 의해서 어떻게 만들어지는 것인가?
커널 레벨 스레드와 유저 레벨 스레드
첫 번째 경우로, 스레드를 생성해 주는 대상은 커널일 수 있다.
이러한 경우 운영체제가 제공하는 시스템 함수 호출을 통해서 스레드 생성을 요구해야 한다.
그러면 운영체제는 해당 스레드를 생성 및 관리하면서 새로운 흐름을 형성하도록 도와준다.
프로그래머 요청에 따라 스레드를 생성 및 스케줄링하는 주체가 커널인 경우, 이를 가리켜 커널 레벨 스레드라 한다.
커널 레벨에서 스레드가 지원된다는 뜻이다.
커널 레벨 스레드 모델
위 그림은 커널 레벨 스레드의 특성을 보여주고 있다.
그림에 나타난 유저 영역은 사용자에 의해서 할당되는 메모리 공간을 의미한다. 하나의 프로세스에 할당되는 메모리 공간이 4G라고 가정해 보자. 그러면 이 중에서 일부 영역은 프로그램 코드를 올리는 데 사용되고 또 일부는 실행하는 과정에서 변수 선언이나 메모리의 동적 할당 등의 용도로 사용된다. 이렇게 프로그램이 동작하기 위해 사용되는 메모리 공간을 가리켜 유저 영역이라 한다. 코드영역, 데이터 영역, 스택 및 힙 영역을 가리켜 유저 영역이라 한다.
그러면 커널 영역은 또 무엇인가?
하나의 프로세스에게 할당된 총 메모리 공간 중에서 유저 영역을 제외한 나머지 영역을 커널 영역이라 한다.
운영체제가 실행되기 위해서는 운영체제 역시도 메모리에 올라가야 하고, 또 일반 프로그램처럼 실행되는 과정에서 변수 선언도 하고 메모리를 동적할당하기도 한다. 이렇게 운영체제라는 하나의 소프트웨어를 실행시키기 위해서 필요한 메모리 공간을 커널 영역이라 한다.
일반 프로그램을 실행시키기 위해서 필요한 공간과 운영체제의 실행을 위한 메모리 공간을 분리시켜 놓지 않으면 관리하는 측면에서 엄청난 곤란을 겪을 수 있다. 따라서 유저 영역과 커널 영역을 분리하는 것이다.
스레드에게 일을 시키기 위한 프로그램 코드는 프로그래머가 개발하므로 스레드 A, B, C의 실행 코드는 유저 영역에 존재할 것이다. 그러나 스케줄러와 스레드 정보(스케줄링을 하는데 필요한 스레드 정보)는 커널 영역에 존재한다. 이것이 바로 커널 레벨 스레드의 유형이다. 오늘날 대부분의 운영체제(Linux, Windows)는 커널 레벨 스레드를 기반으로 스레드 모델을 지원한다.
두 번째 경우로, 유저 레벨 스레드 모델이다.
멀티 프로세스 운영체제라고 해서 커널이 기본적으로 스레드를 지원하는 것은 아니다. 커널이 스레드 모델을 제공하지 않을 경우 커널에 의존적이지 않은 형태로 스레드의 기능을 제공하는 라이브러리를 활용할 수 있는데 이러한 방식으로 제공되는 스레드가 유저 레벨 스레드이다. 커널에서 제공하는 기능이 아니므로 당연히 실행 시 유저 영역에서 실행된다. 그래서 유저 레벨 스레드라 한다.
유저 레벨 스레드 모델
스레드를 지원하지 않는 운영체제에서 유저 레벨 스레드 모델을 적용한 상황 스레드를 지원하지 않기 때문에 스케줄러가 스케줄링하는 대상은 프로세스이다. 그리고 스레드를 스케줄링하는 스케줄러는 유저 영역에서 실행된다. 아래 커널 영역을 보면 커널에는 스레드에 대한 아무런 정보도 존재하지 않음을 알 수 있다. 유저 레벨 스레드 모델을 적용할 경우, 운영체제는 스레드의 존재를 알지도 확인하지도 못한다.
커널 모드와 유저 모드
이제 유저 레벨 스레드와 커널 레벨 스레드의 장, 단점을 이야기할 차례인데 이를 이해하기 위해서는 windows 운영체제가 동작하는 두 가지 모드에 대해서 알아야 한다. Windows는 동작할 때 커널 모드와 유저 모드 중 한 가지 모드로 동작한다. 메모리는 활용 대상에 따라서 유저 영역과 커널 영역으로 나뉜다.
유저 영역은 사용자가 구현한 프로그램 동작 시 사용하게 되는 메모리 영역이고, 커널 영역은 운영체제 동작 시 사용하게 되는 메모리 영역이다. 그리고 커널이 스레드를 지원할 경우 스레드 관리가 커널 영역에서 이뤄지기 때문에 커널 레벨 스레드 모델이라 하고, 커널이 지원하지 않을 경우에 라이브러리를 통해서 제공받아야 하는데 이러한 경우에는 유저 영역에서 스레드의 관리가 이뤄지기 때문에 유저 레벨 스레드 모델이라 한다. 커널 영역은 유저 영역에 비해서 상대적으로 중요하다. 유저 영역에서 메모리 참조 오류가 발생한다면 기껏해야 실행 중인 프로그램에만 영향을 미치게 되지만, 커널 영역은 커널의 코드가 실행되는 영역이므로 시스템 전체에 문제를 일으킬 수도 있는 일이다.
그런데 우리가 구현하거나 사용하는 프로그램은 유저 영역에서 실행되므로 실제로 커널 영역에서 문제를 일으킬 일은 없어 보인다. 그러나 C 언어의 특성상 메모리 참조가 용이하기 때문에 C언어로 프로그램을 구현할 경우 이에 대한 보장을 할 수 없게 된다. 이에 등장한 것이 커널 모드와 유저 모드이다. 일반적인 프로그램은 기본적으로 유저 모드에서 동작한다. 그러다가 Windows 커널이 실행되어야 하는 경우에는 커널 모드로의 전환이 일어난다. 다시 말하면, 커널 영역에서 실행이 이뤄져야 할 경우에는 커널 모드로의 전환이 일어나는 것이다.
이제 확인을 위한 질문을 하나 드리겠다. 여러 개의 프로세스들이 실행 중에 있다. 이제 정해진 타임 슬라이스가 지나서 스케줄러가 동작하려 한다. 커널 모드로의 전환이 일어나겠는가?
당연히 일어난다. 왜냐하면 스케줄러는 커널의 일부에 해당하기 때문이다.
이러한 스케줄러가 동작하려면 커널 모드로의 전환이 필수이다.
그렇다면 커널 모드와 유저 모드의 차이점은 무엇인가?
아주 단순하다. 프로세스가 유저 모드에서 동작할 때에는 커널 영역으로의 접근이 금지된다. 즉 뜻하지 않게 유저 모드에서 실행 중인 프로그램이 커널 영역으로 접근을 시도하면 시스템에서 오류가 발생했음을 알리고 접근을 원천적으로 봉쇄한다. 반면에 커널 모드에서 동작할 때에는 모든 영역의 접근이 허용된다.
중요한 점은 “모드의 전환(커널 모드 유저 모드)은 시스템에 부담을 주는 일이다”를 기억하자.
정리
커널 모드/유저 모드: CPU의 실행 권한을 설명한다.
커널 모드는 CPU가 모든 자원에 접근할 수 있는 상태이고, 유저 모드는 제한된 권한으로 실행된다.
커널 영역/유저 영역: 메모리 공간의 구분을 설명한다.
커널 영역은 운영체제 커널이 사용하는 메모리 공간이고, 유저 영역은 일반 프로세스가 사용하는 메모리 공간이다.
커널 레벨 스레드와 유저 레벨 스레드의 장점 및 단점
- 커널 레벨 스레드의 장점 및 단점
- 장점 :커널에서 직접 제공해 주기 때문에 안전성과 다양한 기능성이 제공된다.
- 단점 : 커널에서 제공해 주는 기능이기 때문에 유저 모드에서 커널 모드로의 전환이 빈번하게 일어난다. 따라서 이는 성능의 저하로 이어지게 된다.
- 유저 레벨 스레드의 장점 및 단점
- 장점 : 커널은 스레드의 존재조차 모른다. 오로지 유저 모드로 동작하기 때문에 유저 모드에서 커널 모드로의 전환이 필요 없다. 때문에 성능이 좋다.
- 단점 : 하나의 프로세스 내에 총 3개의 스레드 A, B, C가 있다고 가정해 보자 이 중에서 A 스레드가 시스템 함수를 호출했는데, 커널에 의해서 블로킹되었다. 이럴 경우 B, C도 실행되지 않는다. 운영체제는 프로세스의 존재만 알지 스레드의 존재를 모른다. 때문에 A 스레드가 속해 있는 프로세스 전부가 블로킹되는 문제를 안고 있다. 이 문제를 해결하기 위한 방법들이 있지만, 결국은 프로그래밍이 어려워지고 커널 레벨 스레드에 비해서 결과 예측이 어렵게 된다.
'운영체제 > 뇌를 자극하는 윈도우즈 시스템 프로그래밍' 카테고리의 다른 글
뇌를 자극하는 윈도우즈 시스템 프로그래밍 13장 (1) | 2024.09.21 |
---|---|
뇌를 자극하는 윈도우즈 시스템 프로그래밍 12장 (0) | 2024.09.07 |
뇌를 자극하는 윈도우즈 시스템 프로그래밍 10장 (0) | 2024.09.07 |
뇌를 자극하는 윈도우즈 시스템 프로그래밍 9장 (0) | 2024.09.07 |
뇌를 자극하는 윈도우즈 시스템 프로그래밍 8장 (0) | 2024.09.05 |