컨텍스트 스위칭(Context Switching)은 운영 체제의 멀티태스킹과 밀접하게 연관된 중요한 개념이다. 컴퓨터에서 여러 프로세스 또는 스레드가 동시에 실행되는 것처럼 보이게 하는 메커니즘으로, 실제로는 CPU가 빠르게 여러 작업 사이를 전환하면서 각각의 작업에 조금씩 처리 시간을 할당한다.
1. 실행 중인 프로세스의 현재 상태(컨텍스트)를 PCB 형태로 저장해야 한다. 이 컨텍스트에는 PID, 프로세스의 상태(우선순위 등), 프로세스 카운터(PC), 레지스터 세트, CPU 스케줄링 정보, 메모리 상태 등이 포함된다. 이 정보는 프로세스의 컨텍스트를 구성하며, 프로세스가 다시 CPU에서 실행을 계속할 수 있도록 필요한 모든 정보를 포함한다.
2. 스케줄러가 다음에 실행할 프로세스를 결정한다. 이 결정은 운영 체제의 스케줄링 알고리즘에 따라 달라진다.
3. 이전에 저장된 다음 프로세스의 컨텍스트를 복원한다. 이 과정에서 프로세스 카운터, 레지스터, 메모리 상태 등이 CPU에 로드된다.
4. 복원된 컨텍스트를 바탕으로 새로운 프로세스(또는 이전에 중단되었던 프로세스)가 실행을 시작한다.
컨텍스트 스위칭은 자원을 소모하는 작업이다. 컨텍스트를 저장하고 복원하는 데에 시간이 걸리며, 이로 인해 CPU 사용률이 실제 작업 처리에 사용되는 것보다 낮아질 수 있다. 또한, 캐시 메모리가 무효화되는 경우(캐시 콜드 미스)가 발생할 수 있어, 성능 저하를 초래할 수 있다.
프로세스 대신 스레드를 사용하면 컨텍스트 스위칭 비용을 줄일 수 있다. 스레드는 같은 프로세스 내에서 메모리와 자원을 공유하기 때문에 컨텍스트 스위칭 시 저장해야 할 정보가 적다. 운영 체제의 스케줄링 알고리즘 선택도 성능에 영향을 준다. 예를 들어, 시분할(Time-sharing) 시스템에서는 라운드 로빈(Round Robin) 스케줄링이 자주 사용된다.
비선점형 커널은 한 프로세스가 종료될 때까지 점유하기 때문에 경쟁상황이 발생하지 않는다.
선점형 커널은 계속해서 컨텍스트 스위칭이 일어나기 때문에 경쟁상황이 발생할 수 있는데 이를 피하기 위해 임계구역이라는 것을 만들어줘야 한다.
임계구역이란 파일의 입출력, 공유 데이터 등 원자적으로 실행할 필요가 있는 명령문 또는 코드의 일부 영역이다.
Race condition(경쟁 상황)은 두 개 이상의 프로세스가 공통자원을 병행적으로 읽거나 쓰는 동작을 할 때, 공유 데이터에 대한 접근이 어떤 순서에 따라 이루어 졌는지에 따라 그 실행 결과가 같지 않고 달라지는 상황을 의미한다.
경쟁상황은 불 충분한 동기화, 비 원자적인 작업, 실행 순서가 잘못 되었을 때 일어난다. 여기서 비원자적인 것은 어느한 단계를 걸쳐서 실행하는 큰 함수와도 같은 것들을 말한다. 원자적, 즉 더 이상 쪼갤 수 없는 작업 단위로 컨텍스트 스위칭이 일어나야한다.
두 개 이상의 프로세스의 Race condition이 일어날 때, 서로 동일한 자원을 두고 경쟁을 하는데 이 경쟁이 끝까지 끝나지 않을 경우를 교착 상태라고 한다. 즉, 두 개의 스레드가 하나의 자원을 놓고 서로 사용하지 않고 경쟁하는 상황이다.
Deadlock(교착 상태)은 임계구역에서 둘 이상의 프로세스가 서로 보유하고 있는 자원을 요구하여, 아무도 자신의 작업을 진행할 수 없게 되는 상태를 말한다.
Starvation(기아 상태)는 하나 이상의 프로세스가 자원에 대한 접근을 무한정 기다려야 하는 상황이다. 이는 특정 프로세스가 다른 프로세스에 비해 우선 순위가 낮거나, 자원 분배 알고리즘에 의해 불리하게 작용할 때 발생할 수 있다. 부족한 자원을 여러 프로세스가 점유하려고 경쟁할 때 발생한다.
Starvation은 SJF, SRT, SPN 처럼 우선순위를 길이로 판단하는 스케줄링을 사용했을 때 일어나며, 이를 방지하기 위해 FIFO같은 우선순위가 아닌 요청 순서대로 처리하는 큐를 사용하는게 좋다.
Deadlock은 시스템이 완전히 멈춰버린 상태로, 아무 진전이 없을 수도 있고 즉시 해결해야 하는 긴급한 문제이고, Starvation은 특정 프로세스가 진행할 수 없는 상태지만, 다른 부분은 여전히 작동가능하여 장기적인 관점에서 해결할 수 있는 문제이다.
세마포어(Semaphore): 동시성 프로그래밍에서 공유 자원에 대한 접근을 제어하기 위해 사용되는 변수나 추상 데이터 타입이다. 멀티 스레드나 프로세스 환경에서 여러 스레드 또는 프로세스가 동시에 공유 자원을 사용하려 할 때, 이를 안전하게 조정하기 위한 메커니즘 중 하나이다.
이진 세마포어(Binary Semaphore): 값이 0 또는 1만을 가질 수 있는 세마포어로, 뮤텍스(Mutex)와 유사한 기능을 한다. 이는 공유 자원에 단 하나의 스레드/프로세스만 접근할 수 있게 하여 상호 배제(Mutual Exclusion)를 구현한다.
카운팅 세마포어(Counting Semaphore): 값이 0 이상의 정수를 가질 수 있는 세마포어로, 한 번에 여러 스레드/프로세스가 공유 자원에 접근할 수 있게 한다. 카운팅 세마포어의 값은 동시에 접근할 수 있는 최대 스레드/프로세스 수를 의미한다.
세마포어는 주로 두 가지 기본 연산, wait()(또는 P(), acquire()) 연산과 signal()(또는 V(), release()) 연산을 사용한다.
wait() 연산: 스레드/프로세스가 공유 자원을 사용하기 전에 이 연산을 호출한다. 세마포어의 값이 0보다 크면, 세마포어의 값을 1 감소시키고 실행을 계속한다. 만약 세마포어의 값이 0이면, 공유 자원이 사용 가능해질 때까지 스레드/프로세스를 대기 상태로 만든다.
signal() 연산: 스레드/프로세스가 공유 자원의 사용을 마친 후에 이 연산을 호출한다. 세마포어의 값을 1 증가시키고, 대기 중인 스레드/프로세스 중 하나를 깨워 공유 자원을 사용할 수 있게 한다.
import threading
import time
import random
# 세마포어 생성, 동시에 리소스에 접근할 수 있는 스레드의 최대 수를 2로 설정
semaphore = threading.Semaphore(2)
class MyThread(threading.Thread):
def run(self):
# 세마포어 획득(리소스 접근)
with semaphore:
print(f"{self.name} is now sleeping")
time.sleep(random.randint(1, 5))
print(f"{self.name} is finished")
# 스레드 객체 생성
threads = []
for i in range(4):
threads.append(MyThread())
# 모든 스레드 시작
for t in threads:
t.start()
# 모든 스레드의 종료를 기다림
for t in threads:
t.join()
뮤텍스(Mutex, Mutual Exclusion Object)
동시성 프로그래밍에서 공유 자원에 대한 동시 접근을 방지하기 위해 사용되는 동기화 메커니즘이다. 뮤텍스는 한 번에 하나의 스레드만 공유 자원을 사용할 수 있도록 보장함으로써, 데이터의 일관성과 무결성을 유지한다.
스레드가 공유 자원을 사용하기 전에 뮤텍스를 잠그거나 획득한다. 이 시점에서 해당 뮤텍스는 잠긴 상태가 되며, 다른 스레드는 그 자원을 사용할 수 없다.
뮤텍스를 성공적으로 잠근 스레드는 공유 자원을 안전하게 사용할 수 있다. 스레드가 자원 사용을 마치면 뮤텍스를 해제하거나 방출한다. 이로써 다른 스레드가 해당 자원에 접근할 수 있는 기회를 얻는다.
import threading
import time
import random
mutex = threading.Lock()
class ThreadOne(threading.Thread):
def run(self):
global mutex
# 뮤텍스를 획득
mutex.acquire()
print("The first thread is now sleeping")
time.sleep(random.randint(1, 5)) # 1초에서 5초 사이의 랜덤한 시간 동안 대기
print("First thread is finished")
# 작업이 끝나면 뮤텍스를 해제
mutex.release()
class ThreadTwo(threading.Thread):
def run(self):
global mutex
# 첫 번째 스레드가 뮤텍스를 해제할 때까지 대기
mutex.acquire()
print("The second thread is now sleeping")
# 1초에서 5초 사이의 랜덤한 시간 동안 대기
time.sleep(random.randint(1, 5))
print("Second thread is finished")
# 작업이 끝나면 뮤텍스를 해제
mutex.release()
t1 = ThreadOne()
t2 = ThreadTwo()
t1.start()
t2.start()
t1.join() # 첫 번째 스레드가 종료될 때까지 메인 스레드 대기
t2.join() # 두 번째 스레드가 종료될 때까지 메인 스레드 대기
'CS > 운영체제' 카테고리의 다른 글
하이퍼바이저, 애뮬레이션, QEMU (0) | 2024.03.26 |
---|---|
Pint OS_Project 1 구현 (1) | 2024.03.26 |
CPU Scheduling, 4BSD, nice (1) | 2024.03.25 |
32bit and 64bit, RAX, CPU vs GPU (1) | 2024.03.24 |
ECF, 시스템 콜과 인터럽트 그리고 시그널 (0) | 2024.03.22 |