일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 핀토스 프로젝트 3
- alarm clock
- 글루민
- 황금 장미
- 핀토스 프로젝트 2
- 핀토스 프로젝트
- 끝
- 핀토스 프로젝트 1
- 글리치
- 노가다
- botw
- 가테
- 자 이제 시작이야
- 마일섬
- 내일부터
- 아직도 실험 중
- 핀토스 프로젝트 4
- Project 1
- PINTOS
- 파란 장미
- 제발흰박
- 바빠지나?
- 일지 시작한 지 얼마나 됐다고
- 셋업
- multi-oom
- Today
- Total
거북이의 쉼터
(2022.04.09) Concurrency and thread 본문
계속 생각하는 거지만 수업 순서가 이상해.... 일단 오늘은 주로 쓰레드에 대해 정리할 것이다.
1. Bottleneck이란?
일반적으로 병목, bottleneck이라고 하면 전체 시스템의 성능이나 용량이 하나의 구성 요소로 인해 제한을 받는 현상 또는 이러한 현상을 일으키는 구성 요소를 지칭한다. 시스템 설계자는 이러한 병목 현상을 최대한 방지하려고 하며, 현존하는 병목을 찾아내고 향상시키려고 한다.
2. I/O bottleneck과 그 해결법
예전에는 CPU의 처리 속도가 메모리나 스토리지에 비해 느렸기 때문에 CPU 속도가 전체 I/O에서의 bottleneck이었다. 그러나 점차 발전한 CPU에 비해 메모리나 스토리지의 속도 발전이 더뎠기에 지금은 완전히 반대가 되었다. 때문에 I/O 요청을 한 CPU가 해당 I/O가 끝날 때까지 idle해지는 것이 문제로 부상했다.
이를 I/O bottleneck 현상이라고 불리며 이를 해결하기 위해 큰 용량의 cache와 더 좋은 관리 알고리즘을 통해 I/O bottleneck을 극복하려는 시도가 있었다. 그러나 근본적으로 CPU의 idle함을 해결하기 위해, 낭비되는 시간에 CPU가 다른 작업을 진행하게 해서 CPU를 바쁜 상태로 유지시키는 방법이 제기되었다. 예전에는 Uniprogramming으로 한 번에 하나만 실행되도록 했지만, CPU의 idle함을 해결하기 위해 여러 프로그램이 한번에 실행되도록 하는 방법을 고안한 것이다. 이렇게 동시에 여러 프로그램이 실행되도록 하는 방법을 Multiprogramming이라고 부르며, 현존하는 대부분의 OS는 여러 프로그램이 동시에 돌아갈 수 있도록 만들어진다.
3. Concurrency와 쓰레드
Multiprogramming에 있어서 중요한 것은 "동시"에 여러 프로그램을 실행하는 것이다. 물론 정확히는 동시에 실행되는 것처럼 "보이게" 하는 것이지만 말이다. 해당 개념을 Concurrency, 병행성이라고 하며, 정확한 정의는 여러 계산을 동시에 수행하는 시스템의 특성이라고 한다. 동시에 여러 작업이 수행될 때 이들을 올바르게 관리하는 것은 OS 개발자들에게 중요한 과제 중 하나이다.
Concurrent한 OS를 만들기 위해 OS 설계자들은 새로운 abstraction을 고안했다. 바로 thread(쓰레드)이다. 쓰레드는 일련의 실행을 나열한 것으로서, 쓰레드 각각이 개별적으로 schedule될 수 있는 작업을 의미한다. 일련의 실행을 나열한 것은 말 그대로 program counter를 따라서 실행되는 일련의 명령어들의 나열을 의미한다. 또한 OS는 하나의 쓰레드를 언제든지 실행하거나, 미룰 수 있기에 schedule의 단위가 되는 것이다.
그래서 machine의 추상화였던 프로세스가 Protection Unit이었던 것과 대조적으로, 쓰레드에는 Protection이 보장되지 않는다. 대신 쓰레드는 실행의 단위, 즉 Execution Unit으로서 CPU의 추상화라고도 할 수 있다. CPU는 machine에 포함이 되어 있기 때문에 쓰레드는 프로세스에 귀속되어야 한다.
4. 프로세스와 쓰레드의 비교
더 구체적으로 프로세스와 쓰레드를 비교해보자.
평가 척도 | 프로세스 | 쓰레드 |
Switch Overhead | 높다 (메모리/IO state 교체 비용) | 낮다 (CPU 상태만 바꾸면 됨) |
생성 비용 | 높다 | 낮다 |
Protection | CPU와 Memory/IO에 모두 적용 | CPU에만 적용, Memory/IO에는 미적용 |
Sharing Overhead | 높다 (최소 Context Switch 요구) | 낮다 (쓰레드 Switch는 낮은 overhead) |
쓰레드를 도입한 이후에는 동일한 프로세스 내부에서 쓰레드가 전환될 때는 CPU state만 바꾸면 되기 때문에 프로세스에서는 높게 측정되던 overhead를 낮출 수 있다. 단, 여러 쓰레드가 같은 메모리에 접근할 수 있는 특성상 메모리에 대한 보호는 이루어지지 않는다.
쓰레드는 실행의 단위이기 때문에 각각 고유한 스택을 갖게 된다. 스택은 calling convention과 local variable 등 실행에도 사용되는 것이기 때문이다. 그러나 스택을 제외한 나머지 heap, global variable, code 등의 메모리는 실행에는 직접적인 필요가 없기 때문에 같은 프로세스 내의 쓰레드 간에는 공유된다.
프로세스 간 통신 (Inter Process Communication, IPC)는 상당한 비용을 초래했다. System Call로 커널에 정보를 전송한 뒤, Context Switch를 하고, 커널에서 필요한 정보를 가져와야 했기 때문이다. 쓰레드는 메모리를 공유하는 특성에 의해 쓰레드 간 통신은 저렴하게 일어날 수 있다. 이를 저렴하게 하는 대신 보호를 포기했기 때문이다.
한 프로세스 내에서 여러 쓰레드는 어떤 순서로 실행될 지 알 수 없다. 쓰레드의 입장에서는 어떠한 스케줄러가 쓰레드를 스케줄링하는지 알 필요가 없으며, 알 필요가 없어야 한다. 허나 이 때문에 어떤 쓰레드가 현재 CPU에서 실행이 되어야 하는지는 알 수 없다. 이러한 실행 순서의 자유로움 때문에 쓰레드의 행동 예측은 쉽지 않으며, 이 때문에 공유 자원을 다루는 쓰레드들에서는 추후에 다루게 될 동기화 문제가 발생하는 것이다. 동기화 문제를 해결한다면 쓰레드의 실행 순서를 예측할 수 있게 된다.
요약하면, 아래와 같이 정리할 수 있다.
- 쓰레드는 한 개의 프로세스에 귀속되어 있다.
- 프로세스는 쓰레드가 실행될 때의 컨테이너 같은 개념이다.
- 한 프로세스는 여러 쓰레드를 가질 수 있다.
- 메모리 공유로 인해 쓰레드 간 정보 공유는 저렴하다.
- 쓰레드는 스케줄링의 단위이다.
5. 쓰레드의 생애 주기
핀토스를 짠 사람들에게는 익숙할 것인 도표.. 이런 방식으로 쓰레드는 생성되며 실행되고 소멸된다.
6. 쓰레드의 관리 주체에 따른 분류
마지막으로 쓰레드를 누가 관리하는가에 따른 분류를 살펴보자. 프로세스에 관한 정보를 저장하는 PCB (Process Control Block)의 경우 커널 영역에 저장이 됐다. 그럼 Thread Control Block, 즉 TCB는 어디에 저장되어야 할까?
당연히 이것도 커널이어야 할 것 같지만, 놀랍게도 유저 영역과 커널 영역 모두 가능하다. 이는 쓰레드에 Protection이 제공되지 않아 유저 영역에서 관리가 되어도 문제가 없기 때문이다. 따라서 TCB의 저장 영역은 누가 쓰레드를 생성해서 관리하는지의 여부에 달린 것이며, 유저 레벨 쓰레드와 커널 레벨 쓰레드라는 개념이 분리된 것도 이 때문이다.
OS가 쓰레드의 생성과 관리를 담당한다면 커널 레벨 쓰레드로 운영되는 OS이다. 모든 쓰레드와 관련된 작업은 반드시 커널을 거쳐서 실행되어야 하며, 이는 create, exit, join, synchronize, switch 등 모든 것에 적용된다. OS 설계자들이 쓰레드와 관련된 사항들을 설계했기 때문에 OS와의 통합성은 비교적 높다고 볼 수 있으나, 모든 경우에 대해서 대비를 한 보편적인 구조로 동작해야 하기 때문에 (One-size fits all) 필요하지 않은 일부 기능성과 커널 내부의 고정된 크기의 스택 생성 등으로 인해 (Heavy weight memory requirement) 쓰레드가 무거워질 수 있다.
이와 반대로 프로그램에 링크된 라이브러리의 유저 레벨 함수가 관여한다면 유저 레벨 쓰레드가 되는 것이다. 커널 쓰레드에 비해 커스텀 방식으로 필요한 사항을 채택할 수 있기 때문에 lightweight하며, 커널을 거치지 않기 때문에 생성, 삭제가 빠르고, 컨텍스트 스위칭에 들어가는 overhead를 줄일 수 있다. 얼핏 보면 커널 쓰레드는 생성에 syscall이 필요해 syscall 없이 관리될 수 있는 유저 쓰레드보다 비효율적일 것으로 보인다. 실제로도 쓰레드를 생성하거나 없애는 것은 유저 레벨 쓰레드의 속도가 더 빠르다. 그런데 현대의 OS는 주로 커널 쓰레드 방식을 채택한다. 이는 왜일까.
유저 레벨 쓰레드를 채택할 경우, 여러 유저 쓰레드에 연결된 하나의 커널 레벨 쓰레드는 유저 레벨 쓰레드의 존재를 알 수 없다. 쓰레드의 관리는 모두 유저 영역에서 일어나기 때문이다. 이로 인해 어떤 유저 레벨 쓰레드를 스케줄링할지 또한 유저 영역에서 처리할 문제이다. 이 때문에 유저 레벨의 쓰레드 스케줄러가 필요하다. 허나 개별적인 스케줄러를 사용하더라도 문제는 잔존한다.
멀티 코어 상황에서 한 유저 레벨 쓰레드가 IO 요청을 보낸 상황을 생각해보자. 해당 요청을 받아 커널 쓰레드는 IO 요청이 완료되기를 기다릴 것이다. 이러면 해당 커널 쓰레드에 묶여있는 나머지 유저 쓰레드들도 요청하지도 않은 IO가 끝나기를 기다려야 한다. 결과적으로 멀티 코어에서 한 코어가 idle 해지는 문제가 발생하는 것이다. 이 때문에 대부분의 최신 OS가 유저 레벨 쓰레드 방식을 채택하지 않는 것이다.
정리하자면 OS와의 통합성을 기대하기는 어렵고, OS가 자원을 관리함에 있어 좋지 못한 결정을 내릴 가능성이 있다는 것이 유저 쓰레드 방식의 큰 문제인 것이다. 물론 해당 문제도 해결이 가능하긴 하다. 바로 idle 상태가 된 커널 쓰레드가 본인이 놀게 되었다는 사실을 유저 레벨의 스케줄러에게 Upcall을 사용해서 통보하는 것이다. 해당 방식을 채택하면 커널 쓰레드가 idle하지 않은 상태를 유지할 수 있다. 실제로 윈도우 8에서는 커널과 유저 레벨 쓰레드 관리자 간의 통신 방식을 채택하고 있다고 한다. 물론 커널 쓰레드 방식이 만능인 것은 아니다. 그래도, 보편적으로 커널 쓰레드 방식이 더 많이 사용된다.
지금까지 커널 쓰레드 방식과 유저 쓰레드 방식의 장단점을 보았다. 그럼 과연 둘 다 쓰는 방식은 없을까? 물론 있다. 여러개의 유저 쓰레드를 여러 개의 커널 쓰레드로 multiplex해서 사용하는 것이다. 일례로, JVM에서 Java 쓰레드는 유저 쓰레드이며, 생성된 여러 Java 쓰레드를 처리하기 위해 복수의 커널 쓰레드가 할당되어 이들을 multiplexing한다. 이를 n:m 쓰레딩 방식이라고도 부르며, 이 방식에 맞춰 기존 쓰레딩 방식을 설명하자면 커널 쓰레드는 1:1, 유저 쓰레드는 n:1 방식이었던 것이다. 물론 해당 방식에도 단점은 존재한다. 유저 쓰레드와 커널 쓰레드가 동시에 돌아가는 만큼, 유저 쪽과 커널 쪽 모두에 스케줄러가 존재해야 하는데, 이 두 개가 맞춰서 돌아가는 상황을 쉽게 맞추기는 어렵다.
'코딩 삽질 > OS 요약 정리' 카테고리의 다른 글
(22.04.11) Synchronization (0) | 2022.04.11 |
---|---|
(2022.04.10) Scheduling (0) | 2022.04.10 |
(2022.04.09) Programming Interface (0) | 2022.04.09 |
(2022.04.09) Kernel Abstraction (2) (0) | 2022.04.09 |
(2022.04.08) Kernel Abstraction (1) (0) | 2022.04.08 |