2024. 4. 13. 21:46ㆍCS/Operating System
Locks
lock은 말 그대로 자물쇠 역할이다. 크리티컬 섹션에는 한 명만 들어가야 하는데, 들어갈 때 lock 하고, 나올 때 unlock 하는 방식으로 동기화 문제를 방지한다.
- acquire() : lock이 풀릴 때 까지 기다렸다가 풀리면 잡는다. 풀려있다면 그냥 잡는다.
- 대기하다 보면 여러 쓰레드가 대기할 수 있다. 여러 쓰레드들을 다 고려해서 acquire()하는 메커니즘을 고려해야 한다.
- release() : unlock
Lock은 초기에 열려있다. 크리티컬 섹션에 들어갈 때 acquire을 부르고 나올 때 release 한다.
acquire과 release 사이에 쓰레드는 락을 잡고 있어야 한다. 또, 최대 하나의 쓰레드만이 락을 잡고 있어야 한다.
아래의 그림을 잘 이해하자!
Lock은 spin할 수 있어서 spinlock이라 불리고, block할 수 있어서 mutex라 불린다.
Implementing Locks
오른쪽 아래의 코드를 실행하는 두 개의 쓰레드 T1, T2를 생각해 보자.
- T1은 held값을 1로 만들고 코드를 실행한다.
- T2는 held값이 1이라서 0으로 바뀔 때까지 while() 문을 돌면서 대기한다.
- T1이 release를 호출해서 held 값을 0으로 만들면 그때부터 T2가 크리티컬 섹션을 들어갈 수 있다.
Problem
사실 이 코드는 문제가 있다. 코드 자체에도 크리티컬 섹션이 있어서 동기화 문제의 위험성이 있다.
T1, T2가 글로벌 변수 held에 의존하고 있어서 그런 문제가 발생한다.
acqurie, release는 Atomic operation하게 설계해야 한다.
- 코드가 여러 줄이 있다면 Context Switching이 일어나지 않고 한 번에 실행을 하던가 아예 실행을 못하게 설계되는 것을 Atomic operation이라 한다.
- Atomic operation은 어셈블리어의 레벨로 생각해야 한다. count+=1;이 어셈블리어로는 세 줄인 것처럼, CPU 한 클락에 코드가 한 번에 다 실행되어야 한다.
Solutions
- 소프트웨어적으로 잘 짜는 방법
- 하드웨어를 설계할 때부터 Atomic operation을 넣어서 그것의 도움을 받는 법.
- 하드웨어적으로 지원하는 인터럽트에 대한 disable/reenable를 사용한다. 인터럽트를 받지 않는다는 말은 타이머 인터럽트를 받지 않는다는 것이고, 그럼 context switching이 일어날 리가 없다.
Software-only Algorithms
이런 식으로 acquire, release를 호출하는 쓰레드의 id로 부르는 방법이 있다.
이 알고리즘 역시 문제를 가지고 있다.
빨간색 화살표의 위치에서 contextSwitch가 일어나면 어떻게 될까? T1도, T2도 interested [other] 값이 1이 되어서 무한 루프를 돌게 된다.
progress를 지키지 못한 알고리즘이다.
Perterson’s Algorithm
어디서 context switch가 일어나던 무한 루프를 돌지 않는다. turn이 글로벌 변수여서 context switch와는 무관하게 잘 동작한다는 것이 포인트이다. 잘 이해해 보자. turn 위아래로 context를 넣어서 직접 시뮬레이션해보자.
Hardware Atomic Instructions
하드웨어적인 도움을 받는 해결책이다.
Test-and-Set이라는 인스럭션이 있는데 예를 들어 C로 바꾸면 이렇게 동작할 것이다. 당연히 실제로는 하드웨어적으로 동작한다.
위의 인스트럭션이 어셈블리어 레벨에서 Atomic 하게 작동하기에 아래와 같이 사용할 수 있다.
기존에 잘못된 알고리즘에서는 l.value를 확인하고 값을 바꾸는 두 과정을 거쳐서 잘못된 부분이 생겼는데, 하드웨어적으로 도움을 받아서 CPU 한 클락에 TestAndSet Atomic 하게 실행하기에 그사이에 컨텍스트 스위칭이 일어날 수 없다.
Swap도 Atomic하게 어셈블리 인스트럭션을 제공한다.
사용하는 예시도 잘 이해해야 한다.
C로 된 모든 예제는 멀티 쓰레드로 시뮬레이션해보면서 잘 기억하자. 하드웨어의 도움을 받으면 알고리즘이 간단해진다.
Problems with Spinlocks
lock을 생각해 보면 사실 Spinlocks이라 불린다.
- 사실 spin은 CPU cycle을 끔찍하게 많이 낭비한다.
- 가장 큰 문제는 acquire에서 while문을 무한히 돈다는 것이다. 그 말은 CPU를 계속 사용한다는 것이다.
- 크리티컬 섹션이 길어지면 대기하는 쓰레드는 그만큼 CPU 사이클을 더 많이 낭비하고 spin이 많아진다.
- lock을 가지고 있는 쓰레드가 갑자기 죽거나, context switch가 lock을 가지고 있지 않은 쓰레드, 혹은 아예 다른 프로세스로 넘어간다면 낭비가 더 심해진다.
따라서 spinlock은 high level의 동기화 구성을 구성할 때만 기본적인 요소로 사용하는 게 좋다.
Disable Interrupts
acquire()에서 cli()를 불러서 인터럽트를 무시하기 시작할 수 있다.
그리고 release()에서 sti()를 불러서 이제 인터럽트를 받겠다고 알려줄 수 있다.
인터럽트를 무시하면 당연히 타이머 인터럽트도 받지 않게 되고, 당연히 context switch가 일어나지 않는다.
이 방법도 문제를 가지고 있다.
- 인터럽트를 켜고 끄는 건 당연히 커널만 가능하다.
- 애플리케이션이 임의로 cli()를 부를 수 있으면 그 프로세스가 CPU를 독점하기에 위험하다.
- 멀티프로세서 CPU에서 여러 CPU가 cli()를 호출하면 문제가 생긴다.
- 크리티컬 섹션이 길다면 아무 인터럽트도 받지 못해서 문제가 생길 수 있다.
spinlock와 마찬가지로 high-level 동기화를 구현하기 위한 기본적인 방법으로만 사용하자.
'CS > Operating System' 카테고리의 다른 글
[OS] CPU 스케줄링의 기초적인 방식, FIFO, SJF, SRTF, RR, Priority scheduling (1) | 2024.05.12 |
---|---|
[OS] 세마포어와 모니터를 통해 동기화 문제를 해결해보자! - 데드락, Starvation (0) | 2024.04.29 |
[OS] Synchronization & Critical sections & Synchronization Problem (0) | 2024.04.13 |
[OS] User-level 쓰레드 VS 커널 쓰레드, Go Language (1) | 2024.04.12 |
[OS] 프로세스와 쓰레드의 비교, pthreads, Signal handling (0) | 2024.04.11 |