Thread와 Process, 그리고 Scheduling

생각보다 자주 언급되고 자주 사용되는 개념이다.
OS 관점에서 쓰레드와 프로세스는 어떤 차이가 있을까?

프로세스는 프로그램의 인스턴스이며 쓰레드는 프로세스 내에서의 작업 단위이다.
만일 프로그램에 단일 작업 단위(쓰레드)만 허용된다면 싱글 쓰레드, 다수의 작업 단위가 혀용된다면 멀티 쓰레드 프로그램이 된다.
이는 곧 OS에서의 자원 할당은 프로세스 단위로 이루어지며 자원 사용은 쓰레드 단위로 이루어진다는 것을 의미한다.
프로세스는 프로그램의 인스턴스이기때문에 독립적인 자원(Program Control Block, PCB)을 할당받으나 쓰레드의 경우 그렇지 않다는 것이 아래의 그림으로 설명된다.

전공자에겐 너무 당연한 이야기라 굳이 프로세스와 쓰레드의 장단점같은 것을 논하진 않을 것이다.
다만, 그래서 OS는 프로세스와 쓰레드를 어떻게 처리하는데? 에 대한 점은 충분히 다룰 여지가 있다.

Kernel Thread와 User-level Thread

가장 바닥에서부터 시작해보자.
OS는 커널 쓰레드를 스케줄링 단위로 취급한다.
그렇기에 커널 단에서 만들어진 모든 쓰레드는 각기 독립된 개체로서 스케줄링된다.

이제 사용자 수준에서 프로세스가 동작할 때를 생각해보자.
사용자 수준의 프로세스가 OS에 의해 스케줄링되기 위해서는 커널에 존재하는 스케줄링 작업단위, 즉 커널 쓰레드에 매핑되어야 한다.
그렇다면 이 때 사용자 수준의 프로세스가 여러 개의 쓰레드를 가지고있다면 어떻게 될까?
이것이 바로 사용자 수준의 쓰레드를 OS 관점에서 어떻게 처리해야할지에 대한 설계 이슈이다.

가장 중요한 것은 OS가 사용자 수준의 쓰레드를 어떻게 인식할지이다.
기본적으로 OS는 사용자 수준의 프로세스를 자원할당의 관점에서만 바라보기때문에 해당 프로세스에서 몇 개의 쓰레드가 돌아가고있는지는 파악하지도 파악할 필요도 없다.
이 견지대로라면 커널 쓰레드는 사용자수준의 프로세스와 매핑되어 결국 커널 쓰레드와 사용자 수준 쓰레드는 1:N의 매핑관계를 가지게 된다.
하지만 만약 커널 쓰레드가 사용자 수준의 쓰레드와 매핑된다면 커널 쓰레드와 사용자 수준 쓰레드는 1:1 관계를 가지게 된다.

1:1 vs. 1:N mapping

만약 커널 쓰레드와 사용자 수준 쓰레드가 1:1 매핑관계를 가진다면 사실상 커널의 스케줄링 작업단위는 사용자수준의 쓰레드가 될 것이다.
하지만 1:N 매핑관계에서는 사용자 수준 쓰레드가 얼마나 많더라도 OS 입장으로는 하나의 커널 쓰레드(스케줄링 작업단위)로 인식하기 때문에 OS의 스케줄링 혜택을 받지는 못한다.

별거 아닌 것 같지만 이 차이는 상당히 많은 것을 시사한다.
우선 커널 쓰레드에서 작업이 스케줄링된다는 것은 사용자 모드에서 커널 모드로의 모드 전환(Mode Change)과 쓰레드 간 컨텍스트 전환(Context Switching)이 발생함을 의미한다.

예를 들어 사용자 수준에서 동작하는 두 프로세스 A, B에 대해 A가 실행되고있는 도중, A대신 더 높은 우선순위를 가진 B가 실행될 때를 생각해보자.
이 과정에서 커널 쓰레드의 컨텍스트 전환을 위해 A는 모드 전환에 들어가게 되며 커널 단에서 컨텍스트 전환을 통해 프로세스 B의 컨텍스트를 로드하게 될 것이다.
그 이후 프로세스 B는 사용자 수준에서 작업을 재개하기 위해 다시 모드 전환에 들어가게 될 것이다.
다시 말해, 사용자 수준의 프로세스 A -> 커널로의 모드 전환 -> 커널 단에서 프로세스 B에 대응하는 커널 쓰레드로의 컨텍스트 전환 -> 사용자 수준으로의 모드 전환 -> 사용자 수준의 프로세스 B 과정을 거치게 될 것이다.

1:1 매핑관계에서는 여기에 추가로 고려할 것이 없지만, 1:N 관계의 경우 여기에 사용자 수준의 쓰레드 스케줄링과 관련된 추가 이슈가 발생하게 된다.
1:1 매핑관계에서는 스케줄링할 것이 오로지 커널 쓰레드밖에 없지만, 1:N 관계의 경우 사용자 수준의 쓰레드 또한 스케줄링되어야 한다.
물론 OS 관점에서는 이러한 스케줄링 과정을 전혀 인지하지 않고 하나의 작업으로 보게되지만 말이다.
중요한 점은 1:N 매핑관계에서는 사용자 수준의 쓰레드 간 스케줄링에 의해 컨택스트 전환이 발생해도 모드 전환이 일어나지 않는다는 점이다.
애초에 커널과 관련이 없는 작업이기 때문에 커널로의 진입 자체가 필요하지 않은 것이다.
하지만 그러한 이점이 있다고 하더라도 1:1 매핑관계에 비하면 1:N 관계에서의 사용자 수준 쓰레드는 보다 적은 자원(시간 등)을 할당받을 수밖에 없다는 문제가 있다. (1:1 관계에서 할당받는 자원을 N개의 쓰레드가 나눠먹기 때문)

M:N Mapping

다대다 매핑은 이에 대한 절충안이다.
과거에 Solaris(유닉스)에서 채택했던 방식이며 리눅스의 pthread에서 한 때 지원하고자 했던 방식이나 성능상의 이슈로 요즘은 사용하지 않는다.

보통은 사융자 수준에서 별도의 쓰레드 라이브러리가 돌아가며 M:N 매핑관계를 지원한다.
Solaris(유닉스)에서는 이에 대한 구현을 경량화 프로세스(Light-weight Process, LWP)라 정의하였는데, 리눅스에서는 볼 일이 없는데도 생각보다 LWP에 대한 언급이 많다.
M:N 관계를 지원하는 쓰레드 라이브러리, 혹은 LWP는 커널 쓰레드와 사용자 수준 쓰레드를 중계하는 역할을 하며, 중계 과정에서 1:1 또는 1:N의 매핑을 실현한다
이는 아래의 그림에 잘 설명되어있다.
첫번째 그림은 리눅스에서의 쓰레드 구현 방식에 대한 개요이며 (c)에서 M:N 관계를 나타내고 있다.
두번째 그림은 Solaris에서 LWP를 통해 M:N 관계를 나타낸 것이다.

*
플로리다 주립대 강의자료 – COP4610: Operating Systems & Concurrent Programming, Threads [링크]
*
플로리다 주립대 강의자료 – COP4610: Operating Systems & Concurrent Programming, Threads [링크]

Implementation

리눅스에서 일반적으로 많이 사용되는 쓰레드 라이브러리로는 pthread가 있다.
pthread는 POSIX thread의 약어이며 API 명세에 불과하기 때문에 pthread가 어떤 형태의 쓰레드 방식을 지원하는지는 알 수 없다.
하지만 pthread의 구현체(Implementation) 후보로서 LinuxThreads, NGTL, NPTL 등이 대두되었고
그 결과, 현재 GNU Libc에 포함된 최후의 승리자는 Native POSIX Thread Library(NPTL)가 되었다.
복잡해보이지만 평소에 리눅스에서 쓰던 pthread가 모두 이것이라 보면 된다.
NPTL은 1:1 관계의 구현체이다.

윈도우에서는 Win32 Thread를 사용하며, 이 또한 1:1 관계의 구현체이다.

댓글 남기기