CPU 스케줄링
CPU 스케줄링은 Ready Queue에 있는 프로세스 중 어느 것을 CPU에 올릴지 결정하는 OS의 핵심 기능입니다. 좋은 스케줄링은 CPU 이용률을 높이고, 응답 시간·대기 시간·반환 시간을 최소화합니다. 알고리즘에 따라 공정성과 효율 사이의 트레이드오프가 달라집니다.
핵심 개념
스케줄링 성능 지표
CPU 이용률(Utilization), 처리량(Throughput), 반환 시간(Turnaround Time = 완료 - 도착), 대기 시간(Waiting Time = Ready Queue 대기 합계), 응답 시간(Response Time = 첫 응답까지)으로 알고리즘 성능을 비교합니다.
핵심 개념
FCFS (First-Come, First-Served)
도착 순서대로 CPU를 할당합니다. 구현이 가장 단순하지만 긴 프로세스 뒤에 짧은 프로세스가 오래 기다리는 호위 효과(Convoy Effect)가 발생합니다. 비선점형입니다.
핵심 개념
SJF (Shortest Job First)
CPU 버스트 시간이 가장 짧은 프로세스를 먼저 실행합니다. 버스트 길이를 정확히 안다면 평균 대기 시간이 최소인 최적 알고리즘이지만 다음 CPU 버스트 시간을 미리 알 수 없어 실제 구현은 예측값(지수 평균)을 사용합니다.
핵심 개념
Round Robin (RR)
각 프로세스에 타임 퀀텀(Time Quantum, q)만큼 CPU를 할당하고 시간이 지나면 선점합니다. 공정하고 응답 시간이 짧지만 q가 너무 작으면 컨텍스트 스위치 오버헤드가 커집니다. 시분할(Time-sharing) 시스템의 기반이며, 현대 OS의 스케줄러도 RR의 선점·시간 분할 개념을 발전시킨 형태입니다(Linux CFS/EEVDF는 비례 공정 분배, POSIX SCHED_RR은 같은 우선순위의 실시간 스레드끼리 RR).
핵심 개념
선점 vs 비선점
비선점(Non-preemptive)은 프로세스가 CPU를 자발적으로 반납할 때까지 실행합니다. 선점(Preemptive)은 OS가 강제로 CPU를 빼앗습니다. Round Robin·SRTF는 선점형, FCFS·SJF(기본)는 비선점형입니다.
핵심 개념
디스패처 (Dispatcher)
스케줄러가 선택한 프로세스를 실제로 CPU에 올리는 모듈입니다. 컨텍스트 스위치 + 유저 모드 전환 + 선택된 PC 주소로 점프를 수행합니다. 디스패처 지연(Dispatch Latency)은 스케줄링 오버헤드의 핵심입니다.
스케줄러는 무엇을 고르는가
프로세스는 CPU를 쓰는 구간(CPU 버스트)과 입출력을 기다리는 구간을 번갈아 가집니다. CPU가 놀지 않게 하려고 운영체제는 여러 프로세스를 메모리에 두고, Ready 큐에서 다음에 CPU를 줄 프로세스를 고릅니다. 이 선택 기준이 스케줄링 알고리즘이고, 기준에 따라 평균 대기 시간, 응답 시간, 공정성이 달라집니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
FCFS, SJF, Round Robin을 비교해 주세요.
FCFS는 단순하지만 긴 작업 뒤에 짧은 작업이 기다리는 호위 효과가 있습니다. SJF는 평균 대기 시간이 최소이지만 버스트 예측이 필요하고 기아가 생길 수 있습니다. Round Robin은 퀀텀마다 돌아가며 응답 시간이 짧고 공정하지만 전환 비용이 있습니다.
면접 질문
기아는 어떻게 해결하나요?
대기 시간에 비례해 우선순위를 올리는 에이징을 씁니다. 일정 시간이 지나면 낮은 우선순위 작업도 결국 높은 우선순위가 되어 실행됩니다.
면접 질문
선점형과 비선점형의 차이는 무엇인가요?
비선점형은 실행 중인 프로세스가 CPU를 스스로 놓을 때까지 기다리고, 선점형은 타이머 인터럽트나 더 높은 우선순위의 도착 같은 사건으로 운영체제가 CPU를 빼앗을 수 있습니다.
면접 질문
리눅스는 어떤 스케줄링을 쓰나요?
일반 프로세스는 가중치로 CPU 시간을 나누는 공정 스케줄러를 씁니다. 오랫동안 CFS였고 리눅스 6.6부터 EEVDF로 바뀌었습니다. 실시간 정책으로 SCHED_FIFO와 SCHED_RR이 따로 있습니다.
실제 시스템에서는
nice와 renice로 일반 프로세스의 우선순위 값을 바꾸고, chrt로 스케줄링 정책(SCHED_FIFO, SCHED_RR 등)을 지정합니다. 정책의 설명은 sched(7) 매뉴얼에 있습니다.
sched_rr_get_interval()이나 Linux 3.9부터 /proc/sys/kernel/sched_rr_timeslice_ms로 볼 수 있으며 기본값이 100ms라고 설명합니다.
Thread.setPriority 값이 소속 스레드 그룹의 최대 우선순위로 제한되고, 가상 스레드는 항상 NORM_PRIORITY라서 값이 무시된다고 설명합니다. 우선순위로 실행 순서를 보장하려 하지 않습니다.
함께 보면 좋은 토픽
참고 자료
C — Round Robin 스케줄러 시뮬레이션
#include <stdio.h> #include <string.h> #define N 4 #define QUANTUM 2 typedef struct { char id[4]; int arrival; // 도착 시간 int burst; // CPU 버스트 시간 int remaining; // 남은 실행 시간 int finish; // 완료 시간 } Process; int main() { Process procs[N] = { { "P1", 0, 6, 6, 0 }, { "P2", 1, 3, 3, 0 }, { "P3", 2, 4, 4, 0 }, { "P4", 3, 2, 2, 0 }, }; int queue[64]; // Ready Queue (원형 배열 대신 단순 배열로 시뮬레이션) int head = 0, tail = 0; int enqueued[N]; memset(enqueued, 0, sizeof(enqueued)); int t = 0; int done = 0; while (done < N) { // 현재 시각에 도착한 프로세스를 Ready Queue에 삽입 for (int i = 0; i < N; i++) { if (!enqueued[i] && procs[i].arrival <= t && procs[i].remaining > 0) { queue[tail++] = i; enqueued[i] = 1; } } if (head == tail) { t++; continue; } // CPU 유휴 int idx = queue[head++]; // Dequeue int run = (procs[idx].remaining < QUANTUM) ? procs[idx].remaining : QUANTUM; printf("T=%2d ~ T=%2d : %s 실행\n", t, t + run, procs[idx].id); t += run; procs[idx].remaining -= run; // 실행 후 도착한 프로세스 먼저 삽입 (선점 직후 도착 처리) for (int i = 0; i < N; i++) { if (!enqueued[i] && procs[i].arrival <= t && procs[i].remaining > 0) { queue[tail++] = i; enqueued[i] = 1; } } if (procs[idx].remaining > 0) { queue[tail++] = idx; // 아직 남아 있으면 Ready Queue 끝에 재삽입 } else { procs[idx].finish = t; done++; } } printf("\n%-4s %7s %7s %7s %7s %11s\n", "PID", "도착", "버스트", "완료", "대기", "반환시간"); float sumW = 0, sumT = 0; for (int i = 0; i < N; i++) { int tat = procs[i].finish - procs[i].arrival; int waiting = tat - procs[i].burst; sumW += waiting; sumT += tat; printf("%-4s %7d %7d %7d %7d %11d\n", procs[i].id, procs[i].arrival, procs[i].burst, procs[i].finish, waiting, tat); } printf("평균 대기: %.1f 평균 반환: %.1f\n", sumW / N, sumT / N); return 0; }
Round Robin은 Ready Queue를 FIFO로 관리합니다. 프로세스가 퀀텀을 다 소진하면 큐 끝으로 이동하고, 아직 남은 프로세스가 있으면 선점됩니다.
선점 직후 새로 도착한 프로세스를 먼저 삽입한 뒤 현재 프로세스를 재삽입하는 순서가 중요합니다. 순서가 바뀌면 기아나 예상치 못한 실행 순서가 발생합니다.
반환 시간(TAT) = 완료 시각 − 도착 시각, 대기 시간 = TAT − CPU 버스트 시간입니다.
Java — SJF (비선점) 스케줄러 시뮬레이션
import java.util.*; public class SJFScheduler { record Process(String id, int arrival, int burst) {} public static void main(String[] args) { List<Process> procs = List.of( new Process("P1", 0, 6), new Process("P2", 1, 3), new Process("P3", 2, 4), new Process("P4", 3, 2) ); // PriorityQueue: 도착한 프로세스 중 burst 최소 → 우선순위 큐 PriorityQueue<Process> ready = new PriorityQueue<>( Comparator.comparingInt(Process::burst) .thenComparingInt(Process::arrival) ); List<Process> remaining = new ArrayList<>(procs); Map<String, Integer> finishMap = new LinkedHashMap<>(); int t = 0; while (!remaining.isEmpty() || !ready.isEmpty()) { // 현재 시각에 도착한 프로세스를 Ready Queue에 투입 Iterator<Process> it = remaining.iterator(); while (it.hasNext()) { Process p = it.next(); if (p.arrival() <= t) { ready.add(p); it.remove(); } } if (ready.isEmpty()) { // CPU 유휴: 다음 도착 시각으로 점프 t = remaining.stream() .mapToInt(Process::arrival).min().orElse(t + 1); continue; } // 가장 짧은 버스트 프로세스를 선택 (비선점: 완료까지 실행) Process sel = ready.poll(); System.out.printf("T=%2d ~ T=%2d : %s 실행%n", t, t + sel.burst(), sel.id()); t += sel.burst(); finishMap.put(sel.id(), t); // 실행하는 동안 도착한 프로세스도 큐에 추가 remaining.removeIf(p -> { if (p.arrival() <= t) { ready.add(p); return true; } return false; }); } printStats(procs, finishMap); } static void printStats(List<Process> procs, Map<String, Integer> finishMap) { System.out.printf("%n%-4s %6s %6s %6s %6s%n", "PID", "Arr", "Burst", "Wait", "TAT"); double sumW = 0, sumT = 0; for (Process p : procs) { int finish = finishMap.get(p.id()); int tat = finish - p.arrival(); int wait = tat - p.burst(); sumW += wait; sumT += tat; System.out.printf("%-4s %6d %6d %6d %6d%n", p.id(), p.arrival(), p.burst(), wait, tat); } System.out.printf("평균 대기: %.1f 평균 반환: %.1f%n", sumW / procs.size(), sumT / procs.size()); } }
비선점 SJF는 CPU가 비면 그 시점까지 도착한 프로세스 중 버스트가 가장 짧은 것을 선택합니다. PriorityQueue를 사용하면 O(log n)으로 최솟값을 꺼낼 수 있습니다.
실행 도중 새로 도착한 프로세스는 비선점이므로 즉시 CPU를 빼앗지 않고 다음 기회를 기다립니다.
실제 OS에서는 미래의 CPU 버스트 시간을 알 수 없으므로 지수 평균(Exponential Average)으로 예측합니다: τ(n+1) = α·t(n) + (1−α)·τ(n), 보통 α=0.5를 사용합니다.
Q1
FCFS(First-Come First-Served) 스케줄링의 특징은?
Q2
SJF(Shortest Job First)가 평균 대기시간 측면에서 유리한 이유는?
Q3
Round Robin(RR) 스케줄링에서 타임 퀀텀(time quantum)이 너무 작으면?