코어 수는 증가하는 반면, CPU의 속도는 증가하기 어렵게 되자 concurrency를 위한 수단이 필요해졌다. 많은 코어를 사용할 수 있는 어플리케이션이 필요해진 것이다.
처음에는 그 만큼 프로세스 수를 늘렸다. 쉽고, 보안성도 확실했기 때문이다. 그러나 프로세스 간의 통신은 큰 overhead이며 context switching 비용도 만만치 않다.
이런 배경에서 등장한 것이 thread이다. 프로세스와 비슷하지만, 같은 프로세스에서 나온 thread는 address space를 공유한다. 매우 큰 일을 나눠서 해결하며 shared address space를 통해 communication을 하기 때문에 비용이 적게 든다는 장점이 있다.
이와 같은 장점으로 인해 multi-threaded program이 유행하였고 그 특징은 다음과 같다.
- Producer/consumer 역할 나누기
- Task 쪼개기
- Non-critical work in background thread
왼쪽부터 CPU1, CPU2, RAM이라고 하면, 각 thread는 page table만 다르고 code 같은 영역은 공유할 수 있으므로 서로 다른 stack 공간만 잡아주면 된다.
Thread vs Process
프로세스와 다르게 thread는 공유하는 부분이 있다.
SHARE | NO-SHARE |
PID Address space (Code, data(heap)) Open file descriptors Current working directory User, group id |
Thread ID (TID) Registers (program counter, stack pointer) Stack (local variables, return address) |
Thread API
다양한 thread system이 있고, 공통적으로 구현된 기능은 다음과 같다.
- Create
- Exit
- Join
Many-to-one thread mapping
User level에서 thread를 create, schedule, synchronize할 수 있게 구현하는 것이다. OS는 user의 thread를 알지 못한다. OS는 오직 단일 kernel 수준에서 관리할 뿐이다. 몇 시스템에서는 이 방법을 사용한다.
- 장점
- OS support가 필요 없음
- 어플리케이션에 적합한 thread scheduling이 가능함
- Thread로 인한 overhead가 작음
- 단점
- Multiprocessors 사용 불가능
- 하나의 thread가 차단되면, 프로세스가 차단되버림
One-to-one thread mapping
Kernel level에서 thread를 관리하는 방식이다. Linux 등 최근의 OS는 모두 이 방식을 사용하고 있다.
- 장점
- Kernel-level thread이기 때문에, multiprocessor를 parallel하게 사용 가능
- Thread가 block되도, 다른 thread 실행 가능
- 단점
- Thread operations(create, join, 등) overhead가 큼
- OS가 thread 수를 잘 scale해야함
Thread scheduling
Thread의 context switching은 shared address space를 가지기 때문에 많은 것을 생각해 봐야 한다. 가령 다음처럼 같은 데이터를 참조하려고 하는 상황을 생각해 보자.
Thread 1과 Thread 2가 순차적으로 실행되어 각 thread에서 문제 없이 동작할 수 있다. 하지만, thread의 목표였던 동시성을 생각해보면, thread의 동작 순서는 따로 atomic하게 정해진 것이 없다. 따라서 다음과 같은 문제가 발생할 수 있다.
Thread 1에서 0x19d의 instruction을 수행해 T1의 %eax에 저장된 계산된 결과를 0x9cd4로 저장하려 했지만, 중간에 context switch가 발생하여 T2가 0x9cd4의 data를 불러와 계산을 수행하게 된다. 이렇게 되면, T1, T2의 순으로 실행되었다면 0x9cd4엔느 102가 저장되어야 하는데, 이 경우에는 101만이 저장되게 된다.
스케쥴링을 통해 해결할 수 있겠지만, 스케쥴링은 공격당할 위험이 있으며, 스케쥴링에 상관없이 이런 경우가 발생할 수 있다. 결국 원하는 것은 몇 instruction이 atomic하게 동작하는 것이고, 그렇기에 스케쥴링과 무관하게 race condition이 발생하지 않는 수단이 필요했다.
다시 말해, mutual exclusion(mutex) for critical section이 필요해졌다. 이를 위해 2가지 목표가 제안되었다.
- Synchronization
- Locks
Synchronization
Operation이 올바른 순서로 실행되는 것을 OS에서 보장할 필요가 있게 되었고, 몇가지 atomic operation을 만들게 되었다.
- Uniprocessor 환경에서의 code
- Load, store
- Special HW instructions
- Test & Set
- Compare & Swap
Lock
Atomic한 수행을 원할 때, 다른 프로세스를 배제하는 mutual exclusion을 구현하기 위한 방법이다. Mutex라고도 한다. 이를 위해서는 3가지가 필요하다.
- Allocate, initialize lock
- Acquire
- exclusion access를 얻는 과정
- 누가 lock을 가지고 있으면, wait
- Wait 중에는 CPU를 포기한다.
- Release
- 가지고 있던 lock을 놓는다.
여러 구현 방식이 있겠지만, 다음처럼 코드로 간단하게 구현해볼 수 있다.
그러나 이런 방식으로는 lock을 제대로 구현할 수 없다. 위의 예시처럼 race condition에서 충돌이 발생할 수 있기 때문이다.
Summary
Concurrency는 높은 성능을 얻기 위해 필수적이게 되었고 이를 위해 thread를 만들었다. 그러나 critical section에서의 context switch로 인해 bug(race condition)가 발생하게 되었고 이를 위해 mutual exclusion을 위한 lock을 제안하게 되었다,
'Study > Operating System' 카테고리의 다른 글
8. Locks (0) | 2023.12.09 |
---|---|
7. Stack (0) | 2023.12.08 |
5. Virtual Memory (0) | 2023.12.08 |
4. Paging (0) | 2023.12.05 |
3. Memory Virtualization (0) | 2023.12.01 |