본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 28. · 2 Views
CPU 스케줄링 알고리즘 (2)
Round Robin부터 MLFQ까지, 현대 운영체제가 사용하는 핵심 CPU 스케줄링 알고리즘들을 실무 예제와 함께 알아봅니다. 초급 개발자도 쉽게 이해할 수 있도록 스토리텔링 방식으로 설명합니다.
목차
- Round Robin 스케줄링
- 타임 퀀텀 선택
- Priority 스케줄링
- Multi-Level Queue (MLQ)
- Multi-Level Feedback Queue (MLFQ)
- 실제 OS의 스케줄링
1. Round Robin 스케줄링
김개발 씨는 오늘 운영체제 스터디에서 새로운 주제를 발표해야 합니다. 지난 시간에 FCFS와 SJF를 배웠는데, 이번에는 Round Robin이라는 생소한 이름의 알고리즘을 맡게 되었습니다.
"라운드 로빈이라니, 무슨 새 이름 같은데?" 김개발 씨는 자료를 찾아보기 시작했습니다.
Round Robin은 모든 프로세스에게 동일한 시간만큼 CPU를 사용할 기회를 주는 스케줄링 방식입니다. 마치 놀이공원에서 모든 아이들이 같은 시간만큼 놀이기구를 타고 내려오는 것과 같습니다.
이 방식은 특히 시분할 시스템에서 공정성을 보장하기 위해 널리 사용됩니다.
다음 코드를 살펴봅시다.
from collections import deque
def round_robin(processes, burst_times, time_quantum):
n = len(processes)
remaining = burst_times.copy()
queue = deque(range(n))
time = 0
while queue:
i = queue.popleft()
# 타임 퀀텀만큼 또는 남은 시간만큼 실행
exec_time = min(time_quantum, remaining[i])
time += exec_time
remaining[i] -= exec_time
# 아직 작업이 남았으면 큐의 끝으로
if remaining[i] > 0:
queue.append(i)
else:
print(f"{processes[i]} 완료: {time}ms")
round_robin(['P1', 'P2', 'P3'], [10, 5, 8], 3)
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 오늘 스터디 자료를 준비하면서 Round Robin이라는 이름의 유래가 궁금해졌습니다.
검색해보니 원래 라운드 로빈은 모든 참가자가 돌아가며 한 번씩 경기하는 방식을 뜻하는 용어였습니다. 선배 개발자 박시니어 씨가 옆에서 지나가다 김개발 씨의 화면을 봅니다.
"아, Round Robin 공부하고 있구나. 이거 정말 중요한 알고리즘이야." 그렇다면 Round Robin이란 정확히 무엇일까요?
쉽게 비유하자면, Round Robin은 마치 뷔페 식당에서 줄을 서는 것과 같습니다. 모든 손님이 한 번에 원하는 만큼 음식을 가져가면 뒷사람은 한참을 기다려야 합니다.
그래서 "한 사람당 3분만 음식을 담을 수 있습니다"라는 규칙을 정하는 것이죠. 시간이 지나면 뒤로 가서 다시 줄을 서야 합니다.
Round Robin이 없던 시절, 즉 FCFS 방식에서는 어떤 문제가 있었을까요? Convoy Effect라는 현상이 대표적입니다.
앞에 실행 시간이 긴 프로세스가 있으면, 뒤에 있는 짧은 프로세스들이 오랫동안 기다려야 했습니다. 사용자 입장에서는 키보드를 눌러도 한참 뒤에 반응이 오는 답답한 상황이 발생했습니다.
바로 이런 문제를 해결하기 위해 Round Robin이 등장했습니다. Round Robin에서는 각 프로세스가 타임 퀀텀이라고 불리는 고정된 시간만큼만 CPU를 사용합니다.
시간이 다 되면 현재 프로세스는 레디 큐의 맨 뒤로 가고, 다음 프로세스가 CPU를 차지합니다. 이렇게 하면 어떤 프로세스도 오래 기다리지 않아도 됩니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 deque를 사용하여 레디 큐를 구현합니다.
큐의 앞에서 프로세스를 꺼내고, 작업이 남으면 뒤에 다시 넣어야 하기 때문입니다. 핵심은 exec_time = min(time_quantum, remaining[i]) 부분입니다.
타임 퀀텀과 남은 실행 시간 중 작은 값만큼만 실행합니다. 실제 현업에서는 어떻게 활용할까요?
대부분의 시분할 운영체제가 Round Robin을 기반으로 합니다. 여러분이 지금 사용하는 컴퓨터에서 음악을 들으면서 웹 브라우저를 사용하고 문서 작업을 하는 것, 이 모든 것이 Round Robin 덕분에 가능합니다.
각 프로그램이 조금씩 번갈아 실행되기 때문에 동시에 돌아가는 것처럼 느껴지는 것입니다. 하지만 주의할 점도 있습니다.
Round Robin의 성능은 타임 퀀텀 크기에 크게 의존합니다. 너무 크면 FCFS와 다를 바 없고, 너무 작으면 컨텍스트 스위칭 오버헤드가 커집니다.
적절한 균형점을 찾는 것이 중요합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
자료를 정리한 김개발 씨는 고개를 끄덕였습니다. "Round Robin이 공정성을 위한 알고리즘이었구나!"
실전 팁
💡 - 타임 퀀텀은 보통 10~100ms 사이로 설정합니다
- 컨텍스트 스위칭 시간의 10배 이상으로 타임 퀀텀을 설정하는 것이 좋습니다
2. 타임 퀀텀 선택
스터디 발표를 마친 김개발 씨에게 동료 이주니어 씨가 질문했습니다. "그런데 타임 퀀텀은 얼마로 정해야 해?" 김개발 씨는 순간 말문이 막혔습니다.
그냥 적당히 정하면 되는 줄 알았는데, 생각보다 깊은 주제였습니다.
타임 퀀텀 선택은 Round Robin 스케줄링의 성능을 좌우하는 핵심 요소입니다. 너무 크면 응답성이 떨어지고, 너무 작으면 오버헤드가 증가합니다.
마치 요리할 때 불 세기를 조절하는 것처럼, 상황에 맞는 적절한 값을 찾아야 합니다.
다음 코드를 살펴봅시다.
def analyze_time_quantum(processes, burst_times, quantum):
n = len(processes)
context_switch_time = 0.1 # 컨텍스트 스위칭 비용 (ms)
total_switches = 0
remaining = burst_times.copy()
# 총 컨텍스트 스위칭 횟수 계산
for burst in burst_times:
switches = (burst - 1) // quantum
total_switches += switches
overhead = total_switches * context_switch_time
total_burst = sum(burst_times)
efficiency = total_burst / (total_burst + overhead) * 100
print(f"타임 퀀텀: {quantum}ms")
print(f"컨텍스트 스위칭: {total_switches}회")
print(f"CPU 효율: {efficiency:.1f}%")
# 다양한 타임 퀀텀으로 테스트
for q in [1, 5, 10, 50]:
analyze_time_quantum(['P1', 'P2', 'P3'], [20, 15, 30], q)
김개발 씨는 타임 퀀텀에 대해 더 깊이 알아보기로 했습니다. 박시니어 씨에게 조언을 구하자, 그는 화이트보드에 그림을 그리기 시작했습니다.
"타임 퀀텀 선택은 마치 피자 배달과 같아." 박시니어 씨의 비유가 시작되었습니다. "만약 배달 기사가 한 집에 피자를 다 전달할 때까지 기다렸다가 다음 집에 간다면, 그건 FCFS야.
반면에 각 집에 피자 한 조각씩만 주고 다음 집으로 간다면? 모든 집이 조금씩은 받겠지만, 기사는 계속 왔다 갔다 해야 하지." 타임 퀀텀이 너무 작으면 어떤 일이 벌어질까요?
프로세스가 의미 있는 작업을 하기도 전에 CPU를 빼앗깁니다. 그리고 매번 프로세스를 교체할 때마다 컨텍스트 스위칭이 발생합니다.
현재 프로세스의 상태를 저장하고, 다음 프로세스의 상태를 복원하는 작업이죠. 이 과정에서 CPU는 실제 작업이 아닌 관리 작업에 시간을 소비합니다.
반대로 타임 퀀텀이 너무 크면 어떨까요? 극단적으로 타임 퀀텀이 무한대라면, 그것은 그냥 FCFS와 똑같습니다.
먼저 온 프로세스가 끝날 때까지 다른 프로세스는 기다려야 합니다. Round Robin의 장점인 공정성과 응답성이 사라지는 것입니다.
위의 코드는 타임 퀀텀에 따른 CPU 효율을 계산합니다. 컨텍스트 스위칭 횟수는 (burst - 1) // quantum으로 계산됩니다.
예를 들어 burst time이 20ms이고 quantum이 5ms라면, 3번의 스위칭이 발생합니다. 각 스위칭마다 0.1ms의 오버헤드가 있다고 가정하면, 전체 효율을 계산할 수 있습니다.
실제 운영체제에서는 어떤 값을 사용할까요? 리눅스의 경우 일반적으로 10ms에서 200ms 사이의 값을 사용합니다.
정확한 값은 시스템의 성격에 따라 다릅니다. 서버 시스템은 처리량을 위해 큰 값을, 데스크톱 시스템은 응답성을 위해 작은 값을 선호합니다.
경험적으로 좋은 타임 퀀텀은 80% 이상의 CPU burst가 타임 퀀텀 내에 완료될 수 있는 크기입니다. 이렇게 하면 대부분의 프로세스가 한 번의 할당으로 작업을 마칠 수 있어서, 불필요한 컨텍스트 스위칭을 줄일 수 있습니다.
물론 이것은 워크로드의 특성을 잘 파악해야 가능한 일입니다. 김개발 씨는 이제 이해가 되었습니다.
"결국 트레이드오프의 문제군요. 응답성과 효율 사이에서 균형을 찾아야 하는 거네요."
실전 팁
💡 - 일반적으로 컨텍스트 스위칭 시간보다 100배 이상 큰 타임 퀀텀을 권장합니다
- 대화형 시스템은 작은 타임 퀀텀, 배치 시스템은 큰 타임 퀀텀이 적합합니다
3. Priority 스케줄링
다음 주 스터디 주제는 Priority 스케줄링입니다. 김개발 씨는 "우선순위라면 뭐 당연히 높은 거부터 하는 거 아냐?"라고 생각했습니다.
하지만 자료를 찾아보니, 생각보다 고려할 점이 많았습니다.
Priority 스케줄링은 각 프로세스에 우선순위를 부여하고, 가장 높은 우선순위의 프로세스를 먼저 실행하는 방식입니다. 마치 병원 응급실에서 중증 환자를 먼저 치료하는 것과 같습니다.
단순하지만 기아 현상이라는 치명적인 문제를 안고 있습니다.
다음 코드를 살펴봅시다.
import heapq
def priority_scheduling(processes):
# (우선순위, 도착시간, 이름, 실행시간) 형태
# 숫자가 작을수록 높은 우선순위
heap = []
for name, arrival, burst, priority in processes:
heapq.heappush(heap, (priority, arrival, name, burst))
time = 0
while heap:
priority, arrival, name, burst = heapq.heappop(heap)
time = max(time, arrival)
time += burst
print(f"{name}(우선순위:{priority}) 완료: {time}ms")
# (이름, 도착시간, 실행시간, 우선순위)
processes = [
('System', 0, 5, 1), # 시스템 프로세스 - 최고 우선순위
('User1', 0, 10, 3), # 사용자 프로세스
('Background', 0, 3, 5) # 백그라운드 - 낮은 우선순위
]
priority_scheduling(processes)
김개발 씨는 회사에서 재미있는 경험을 했습니다. 열심히 코드를 작성하고 있는데, 갑자기 팀장님이 급한 버그 수정을 요청했습니다.
하던 일을 멈추고 버그부터 고쳤죠. 이것이 바로 Priority 스케줄링의 실생활 예시입니다.
박시니어 씨가 설명을 덧붙입니다. "운영체제에서도 마찬가지야.
모든 프로세스가 같은 중요도를 가지진 않거든." Priority 스케줄링에서는 각 프로세스에 우선순위 번호가 부여됩니다. 관례적으로 숫자가 작을수록 높은 우선순위를 의미하는 경우가 많습니다.
예를 들어 리눅스에서 nice 값 -20은 가장 높은 우선순위, 19는 가장 낮은 우선순위입니다. 반대로 숫자가 클수록 높은 우선순위인 시스템도 있으니 주의해야 합니다.
우선순위는 어떻게 정해질까요? 정적 우선순위는 프로세스 생성 시 고정됩니다.
시스템 프로세스는 높은 우선순위, 백그라운드 작업은 낮은 우선순위를 받습니다. 반면 동적 우선순위는 실행 중에 변할 수 있습니다.
오래 기다린 프로세스의 우선순위를 올려주는 방식이죠. 위의 코드에서는 힙 자료구조를 사용합니다.
힙은 항상 가장 작은 값(높은 우선순위)을 먼저 꺼낼 수 있어서, Priority 스케줄링 구현에 안성맞춤입니다. heapq.heappop()을 호출할 때마다 가장 우선순위가 높은 프로세스가 선택됩니다.
하지만 Priority 스케줄링에는 큰 문제가 있습니다. 바로 **기아 현상(Starvation)**입니다.
높은 우선순위 프로세스가 계속 들어오면, 낮은 우선순위 프로세스는 영원히 CPU를 얻지 못할 수 있습니다. 마치 응급실에서 계속 중환자가 들어오면 감기 환자는 집에 갈 수 없는 것과 같습니다.
이 문제의 해결책이 **에이징(Aging)**입니다. 대기 시간이 길어질수록 우선순위를 점진적으로 높여주는 방식입니다.
아무리 낮은 우선순위로 시작해도, 충분히 오래 기다리면 결국 실행될 기회를 얻습니다. 이것이 공정성을 보장하는 핵심 메커니즘입니다.
김개발 씨가 고개를 끄덕입니다. "단순히 우선순위만 따지면 안 되고, 기아 현상도 방지해야 하는군요!"
실전 팁
💡 - 기아 현상 방지를 위해 에이징 기법을 함께 사용하세요
- 선점형과 비선점형 Priority 스케줄링의 차이를 이해하세요
4. Multi-Level Queue (MLQ)
김개발 씨는 회사에서 업무를 처리할 때 긴급, 중요, 일반으로 나눠서 관리합니다. 문득 "운영체제도 이렇게 하면 효율적이지 않을까?"라는 생각이 들었습니다.
알고 보니 이미 그런 방식이 존재했습니다. 바로 Multi-Level Queue입니다.
**MLQ(Multi-Level Queue)**는 프로세스를 여러 개의 큐로 분류하여 관리하는 스케줄링 방식입니다. 마치 공항에서 퍼스트 클래스, 비즈니스, 이코노미 승객을 다른 줄로 안내하는 것과 같습니다.
각 큐는 자체적인 스케줄링 알고리즘을 가질 수 있습니다.
다음 코드를 살펴봅시다.
class MultiLevelQueue:
def __init__(self):
# 3개의 우선순위 큐 생성
self.system_queue = [] # 최고 우선순위
self.interactive_queue = [] # 중간 우선순위
self.batch_queue = [] # 낮은 우선순위
def add_process(self, name, proc_type, burst):
if proc_type == 'system':
self.system_queue.append((name, burst))
elif proc_type == 'interactive':
self.interactive_queue.append((name, burst))
else:
self.batch_queue.append((name, burst))
def schedule(self):
# 높은 우선순위 큐부터 처리
for queue_name, queue in [
('시스템', self.system_queue),
('대화형', self.interactive_queue),
('배치', self.batch_queue)
]:
while queue:
process = queue.pop(0)
print(f"[{queue_name}] {process[0]} 실행")
mlq = MultiLevelQueue()
mlq.add_process('kernel', 'system', 5)
mlq.add_process('browser', 'interactive', 10)
mlq.add_process('backup', 'batch', 20)
mlq.schedule()
어느 날 박시니어 씨가 김개발 씨에게 물었습니다. "지금까지 배운 스케줄링 알고리즘들, 뭔가 부족하지 않아?" 김개발 씨가 잠시 생각했습니다.
"음... 모든 프로세스를 똑같이 취급하는 게 좀 그렇긴 하죠.
시스템 프로세스랑 게임이랑 같은 대우를 받으면 안 될 것 같은데요." 박시니어 씨가 미소를 지었습니다. "바로 그거야.
그래서 MLQ가 나온 거지." MLQ는 프로세스를 성격에 따라 분류합니다. 보통 시스템 프로세스, 대화형 프로세스, 배치 프로세스로 나눕니다.
시스템 프로세스는 운영체제의 핵심 기능을 담당하므로 가장 높은 우선순위를 받습니다. 대화형 프로세스는 사용자와 직접 상호작용하므로 빠른 응답이 필요합니다.
배치 프로세스는 백그라운드에서 돌아가므로 늦어도 괜찮습니다. 각 큐는 자체적인 스케줄링 알고리즘을 가질 수 있습니다.
예를 들어 대화형 프로세스 큐는 빠른 응답을 위해 Round Robin을 사용하고, 배치 프로세스 큐는 효율성을 위해 FCFS를 사용할 수 있습니다. 이렇게 하면 각 프로세스 유형의 특성에 맞는 최적의 스케줄링이 가능합니다.
큐 간의 스케줄링은 어떻게 할까요? 가장 단순한 방법은 절대적 우선순위입니다.
높은 우선순위 큐가 비어야만 낮은 우선순위 큐를 처리합니다. 하지만 이러면 기아 현상이 발생할 수 있어서, 타임 슬라이스 방식을 사용하기도 합니다.
예를 들어 시스템 큐에 50%, 대화형 큐에 40%, 배치 큐에 10%의 CPU 시간을 할당하는 식입니다. 위 코드에서는 3개의 리스트로 각 큐를 구현했습니다.
add_process 메서드에서 프로세스 유형에 따라 적절한 큐에 배치합니다. schedule 메서드에서는 높은 우선순위 큐부터 순서대로 비웁니다.
실제 구현에서는 각 큐에 다른 스케줄링 알고리즘을 적용할 수 있습니다. MLQ의 큰 단점은 프로세스가 큐 간 이동이 불가능하다는 점입니다.
한번 배치 큐에 들어간 프로세스는 아무리 기다려도 대화형 큐로 올라갈 수 없습니다. 이 문제를 해결한 것이 바로 다음에 배울 MLFQ입니다.
김개발 씨는 MLQ의 구조가 마치 회사의 업무 처리 시스템과 비슷하다고 느꼈습니다.
실전 팁
💡 - MLQ는 프로세스 특성이 명확히 구분될 때 효과적입니다
- 큐 간 CPU 시간 배분 비율이 전체 성능에 큰 영향을 미칩니다
5. Multi-Level Feedback Queue (MLFQ)
김개발 씨는 MLQ의 단점을 알게 되었습니다. "프로세스가 한번 낮은 큐에 들어가면 영원히 못 나오는 건 불공평하지 않나요?" 박시니어 씨가 고개를 끄덕였습니다.
"그래서 피드백이 추가된 MLFQ가 있어."
**MLFQ(Multi-Level Feedback Queue)**는 프로세스가 큐 사이를 이동할 수 있는 진화된 스케줄링 방식입니다. 프로세스의 행동을 관찰하여 동적으로 우선순위를 조정합니다.
마치 학교에서 성적에 따라 반 배정이 바뀌는 것과 같습니다.
다음 코드를 살펴봅시다.
class MLFQ:
def __init__(self, num_queues=3):
self.queues = [[] for _ in range(num_queues)]
# 각 큐의 타임 퀀텀 (낮은 큐일수록 큼)
self.time_quantums = [4, 8, 16]
def add_process(self, name, burst):
# 새 프로세스는 최상위 큐에서 시작
self.queues[0].append({'name': name, 'remaining': burst})
def run_one_cycle(self):
for level, queue in enumerate(self.queues):
if not queue:
continue
proc = queue.pop(0)
quantum = self.time_quantums[level]
exec_time = min(quantum, proc['remaining'])
proc['remaining'] -= exec_time
if proc['remaining'] > 0:
# 타임 퀀텀 다 쓰면 한 단계 낮은 큐로
next_level = min(level + 1, len(self.queues) - 1)
self.queues[next_level].append(proc)
print(f"{proc['name']}: 큐{level}에서 {exec_time}ms 실행 -> 큐{next_level}로 이동")
else:
print(f"{proc['name']}: 완료!")
return True
return False
MLFQ는 현대 운영체제에서 가장 널리 사용되는 스케줄링 알고리즘 중 하나입니다. 김개발 씨는 이 알고리즘의 핵심 아이디어가 궁금했습니다.
박시니어 씨가 설명을 시작했습니다. "MLFQ의 핵심은 프로세스를 관찰하고 학습하는 거야." 새로운 프로세스가 시스템에 들어오면, 우리는 이 프로세스가 어떤 성격인지 모릅니다.
CPU를 많이 쓰는 프로세스일까요, 아니면 I/O가 많은 대화형 프로세스일까요? MLFQ는 일단 최고 우선순위를 주고 지켜봅니다.
프로세스가 타임 퀀텀을 다 소진하면 어떻게 될까요? "아, 이 프로세스는 CPU를 많이 쓰는 놈이구나"라고 판단하고, 한 단계 낮은 큐로 내려보냅니다.
반면에 타임 퀀텀을 다 쓰기 전에 I/O를 요청하면? "이 프로세스는 대화형이구나"라고 판단하고, 높은 우선순위를 유지합니다.
이 방식의 장점은 사전 정보 없이도 최적의 스케줄링이 가능하다는 것입니다. SJF처럼 실행 시간을 미리 알 필요가 없습니다.
프로세스의 실제 행동을 보고 적응적으로 반응하기 때문입니다. 짧은 작업은 높은 큐에서 빨리 끝나고, 긴 작업은 천천히 낮은 큐로 내려가면서 실행됩니다.
위 코드에서 주목할 부분은 타임 퀀텀의 증가입니다. 낮은 큐일수록 타임 퀀텀이 큽니다.
왜 그럴까요? 이미 낮은 큐에 있다는 것은 CPU를 많이 쓰는 프로세스라는 뜻입니다.
이런 프로세스에게 작은 타임 퀀텀을 주면 컨텍스트 스위칭만 많아지고 비효율적입니다. MLFQ에도 주의할 점이 있습니다.
영악한 프로세스가 타임 퀀텀이 끝나기 직전에 의도적으로 I/O를 요청하면 어떻게 될까요? 높은 우선순위를 계속 유지하면서 사실상 CPU를 독점할 수 있습니다.
이를 게이밍(Gaming) 문제라고 합니다. 해결책으로 주기적인 우선순위 부스팅을 사용합니다.
일정 시간마다 모든 프로세스를 최상위 큐로 올려버리는 것입니다. 이렇게 하면 낮은 큐에 오래 머문 프로세스도 다시 기회를 얻고, 게이밍을 시도하는 프로세스도 초기화됩니다.
김개발 씨는 감탄했습니다. "정말 똑똑한 알고리즘이네요.
관찰하고 학습하다니!"
실전 팁
💡 - 주기적인 우선순위 부스팅으로 기아 현상과 게이밍을 방지하세요
- 각 큐의 타임 퀀텀은 보통 2배씩 증가시킵니다
6. 실제 OS의 스케줄링
스터디를 마친 김개발 씨가 집에 돌아와 노트북을 켰습니다. 문득 궁금해졌습니다.
"내가 쓰는 리눅스랑 윈도우는 어떤 스케줄링을 쓰는 걸까?" 검색창에 손가락이 움직이기 시작했습니다.
실제 운영체제는 이론에서 배운 알고리즘들을 조합하고 발전시켜 사용합니다. 리눅스는 **CFS(Completely Fair Scheduler)**를, 윈도우는 다단계 피드백 큐를 기반으로 합니다.
각 OS의 설계 철학에 따라 세부 구현이 다릅니다.
다음 코드를 살펴봅시다.
# 리눅스 CFS 개념 시뮬레이션
class CFSScheduler:
def __init__(self):
self.processes = {} # name: vruntime
def add_process(self, name, nice=0):
# nice 값에 따른 가중치 (-20~19, 기본값 0)
weight = 1024 * (1.25 ** -nice) # 간소화된 가중치 계산
self.processes[name] = {'vruntime': 0, 'weight': weight}
def select_next(self):
# vruntime이 가장 작은 프로세스 선택
if not self.processes:
return None
return min(self.processes, key=lambda p: self.processes[p]['vruntime'])
def run_process(self, name, actual_time):
# 가중치에 반비례하여 vruntime 증가
weight = self.processes[name]['weight']
self.processes[name]['vruntime'] += actual_time * (1024 / weight)
print(f"{name} 실행 {actual_time}ms, vruntime: {self.processes[name]['vruntime']:.1f}")
cfs = CFSScheduler()
cfs.add_process('important', nice=-5) # 높은 우선순위
cfs.add_process('normal', nice=0) # 기본
cfs.add_process('background', nice=10) # 낮은 우선순위
김개발 씨는 자신이 매일 사용하는 운영체제가 실제로 어떻게 스케줄링하는지 알아보기로 했습니다. 이론과 실제는 어떻게 다를까요?
먼저 리눅스의 CFS를 살펴봅시다. CFS는 "Completely Fair Scheduler", 즉 완전 공정 스케줄러라는 이름을 가지고 있습니다.
2007년 리눅스 커널 2.6.23부터 도입되었습니다. 핵심 아이디어는 간단합니다.
모든 프로세스가 CPU 시간을 공평하게 나눠 가져야 한다는 것입니다. CFS는 **가상 실행 시간(vruntime)**이라는 개념을 사용합니다.
각 프로세스는 실제로 CPU를 사용한 시간을 기록합니다. 하지만 단순히 시간만 기록하는 것이 아니라, 우선순위에 따라 가중치를 적용합니다.
높은 우선순위 프로세스는 같은 시간을 실행해도 vruntime이 덜 증가하고, 낮은 우선순위는 더 빨리 증가합니다. 스케줄러는 항상 vruntime이 가장 작은 프로세스를 선택합니다.
이렇게 하면 자연스럽게 공정한 분배가 이루어집니다. vruntime이 작다는 것은 상대적으로 CPU를 덜 받았다는 뜻이니까요.
CFS는 이 과정을 효율적으로 하기 위해 레드-블랙 트리라는 자료구조를 사용합니다. 윈도우는 어떨까요?
윈도우는 0부터 31까지 32개의 우선순위 레벨을 사용합니다. 0은 시스템 유휴 스레드, 115는 일반 우선순위, 1631은 실시간 우선순위입니다.
일반 사용자 프로세스는 보통 6~10 범위에서 시작합니다. 윈도우의 특징은 동적 우선순위 조정입니다.
포그라운드 윈도우의 스레드는 우선순위가 올라갑니다. 사용자가 현재 사용 중인 프로그램이 더 빠르게 반응하도록 하기 위해서입니다.
또한 I/O 완료 후에도 일시적으로 우선순위가 올라갑니다. macOS도 살펴볼까요?
macOS는 Mach 커널 기반으로, 스레드를 여러 밴드로 나눕니다. 실시간 스레드, 시스템 스레드, 사용자 스레드 등으로 구분됩니다.
특히 멀티미디어 작업을 위한 실시간 스레드 지원이 특징적입니다. 김개발 씨는 각 운영체제가 자신만의 철학을 가지고 있다는 것을 깨달았습니다.
리눅스는 공정성을, 윈도우는 응답성을, macOS는 멀티미디어 성능을 중시합니다. "결국 완벽한 스케줄러는 없고, 상황에 맞는 최선의 선택이 있을 뿐이군요." 박시니어 씨가 스터디 마무리 발언을 합니다.
"맞아. 그래서 운영체제를 이해하면 프로그램 최적화에도 도움이 되는 거야.
내 프로세스가 어떻게 스케줄링되는지 알면, 더 효율적인 코드를 작성할 수 있거든."
실전 팁
💡 - 리눅스에서 nice 명령어로 프로세스 우선순위를 조정할 수 있습니다
- 실시간 처리가 필요하면 SCHED_FIFO나 SCHED_RR 정책을 고려하세요
- 윈도우에서는 작업 관리자로 프로세스 우선순위를 변경할 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
교착 상태 (Deadlock) 완벽 가이드
운영체제에서 발생하는 교착 상태의 개념부터 예방, 회피, 탐지까지 완벽하게 이해할 수 있는 가이드입니다. 실무에서 마주칠 수 있는 동시성 문제를 해결하는 핵심 지식을 담았습니다.
동기화 도구 완벽 가이드
멀티스레드 프로그래밍에서 핵심이 되는 동기화 도구들을 알아봅니다. 뮤텍스, 세마포어, 모니터 등 운영체제의 동기화 메커니즘을 실무 예제와 함께 쉽게 이해할 수 있습니다.
프로세스 동기화 완벽 가이드
멀티스레드 환경에서 여러 프로세스가 공유 자원에 안전하게 접근하는 방법을 알아봅니다. Race Condition부터 Peterson 알고리즘까지 동기화의 핵심 개념을 실무 예제와 함께 쉽게 설명합니다.
CPU 스케줄링 알고리즘 완벽 가이드 (1)
운영체제의 핵심인 CPU 스케줄링 알고리즘을 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. FCFS부터 HRRN까지, 각 알고리즘의 동작 원리와 장단점을 실무 스토리와 함께 배워봅니다.
CPU 스케줄링 기초 완벽 가이드
운영체제의 핵심인 CPU 스케줄링을 초급 개발자도 이해할 수 있도록 쉽게 설명합니다. 선점형과 비선점형의 차이부터 다양한 성능 지표까지, 실무에서 꼭 알아야 할 개념을 다룹니다.