Showing posts with label os. Show all posts
Showing posts with label os. Show all posts

Tuesday, November 4, 2025

CPU 스케줄링의 핵심 원리: FCFS, SJF, RR 알고리즘 비교 분석

컴퓨터 시스템의 심장이라 할 수 있는 중앙 처리 장치(CPU)는 한정된 자원입니다. 현대 운영체제는 수십, 수백 개의 프로세스가 동시에 실행되는 것처럼 보이게 하는 멀티태스킹 환경을 제공하지만, 실제로 특정 순간에 하나의 CPU 코어에서 실행될 수 있는 프로세스는 단 하나뿐입니다. 그렇다면 운영체제는 어떤 기준으로, 어떤 순서로 수많은 프로세스에게 이 소중한 CPU 자원을 배분하는 것일까요? 바로 이 질문에 대한 해답이 CPU 스케줄링입니다.

CPU 스케줄링은 단순히 프로세스를 줄 세우는 작업이 아닙니다. 이는 시스템 전체의 성능과 사용자 경험을 좌우하는 매우 정교하고 중요한 결정 과정입니다. 어떤 스케줄링 알고리즘을 선택하느냐에 따라 시스템의 처리율이 극대화될 수도 있고, 특정 프로세스가 무한정 대기 상태에 빠지거나, 사용자가 프로그램의 반응이 느리다고 느끼게 될 수도 있습니다. 따라서 스케줄링의 목표는 명확합니다. CPU를 최대한 효율적으로 사용하면서도, 모든 프로세스에게 공평한 기회를 제공하고, 사용자가 원하는 작업을 지연 없이 처리하는 것입니다. 이 글에서는 가장 기본적이면서도 모든 현대 스케줄링 알고리즘의 근간이 되는 세 가지 핵심 알고리즘, 즉 FCFS, SJF, Round Robin에 대해 심층적으로 분석하고, 각각의 장단점과 그 이면에 숨겨진 철학을 탐구해 보겠습니다.

스케줄링 알고리즘 평가의 척도

특정 알고리즘이 얼마나 '좋은지'를 판단하려면 객관적인 기준이 필요합니다. CPU 스케줄링에서는 주로 다음과 같은 지표를 사용하여 성능을 평가합니다.

  • CPU 이용률 (CPU Utilization): 전체 시간 중 CPU가 실제로 작업을 처리한 시간의 비율입니다. 시스템이 유휴 상태에 빠지지 않고 최대한 바쁘게 일하게 만드는 것이 목표입니다. 100%에 가까울수록 이상적입니다.
  • 처리율 (Throughput): 단위 시간당 완료된 프로세스의 개수입니다. 시스템이 얼마나 많은 작업을 처리할 수 있는지를 나타내는 지표로, 높을수록 좋습니다.
  • 총 처리 시간 (Turnaround Time): 프로세스가 시스템에 제출된 순간부터 실행을 완전히 마칠 때까지 걸린 총 시간입니다. 이는 실제 CPU 사용 시간과 대기 시간을 모두 포함합니다. 사용자 관점에서 작업이 완료되기까지 걸리는 시간이므로 짧을수록 좋습니다. (총 처리 시간 = 종료 시간 - 도착 시간)
  • 대기 시간 (Waiting Time): 프로세스가 준비 큐(Ready Queue)에서 CPU 할당을 기다리며 보낸 시간의 총합입니다. 이 시간 동안 프로세스는 아무 작업도 하지 못하고 대기만 하므로, 짧을수록 효율적입니다.
  • 응답 시간 (Response Time): 프로세스가 준비 큐에 들어온 후 처음으로 CPU를 할당받기까지 걸린 시간입니다. 대화형 시스템(Interactive System)에서 사용자 입력에 대한 시스템의 '반응성'을 측정하는 중요한 지표이며, 짧을수록 사용자는 시스템이 빠르다고 느낍니다. (응답 시간 = 첫 실행 시간 - 도착 시간)

이러한 지표들은 종종 상충 관계(trade-off)에 있습니다. 예를 들어, 처리율을 극대화하려는 시도가 특정 프로세스의 대기 시간을 무한정 늘릴 수 있고, 모든 프로세스의 응답 시간을 줄이려다 보면 잦은 문맥 교환(Context Switching)으로 인해 전체적인 CPU 이용률이 떨어질 수도 있습니다. 좋은 스케줄링 알고리즘은 시스템의 목적(예: 일괄 처리 시스템, 대화형 시스템)에 맞게 이 지표들 사이에서 최적의 균형점을 찾는 것입니다.


1. 선입 선처리 (First-Come, First-Served, FCFS)

FCFS 알고리즘은 이름 그대로 가장 단순하고 직관적인 스케줄링 방식입니다. "먼저 온 손님부터 서비스한다"는 원칙에 따라, 준비 큐에 먼저 도착한 프로세스에게 CPU를 먼저 할당합니다. 이 알고리즘은 비선점형(Non-preemptive)으로, 일단 한 프로세스가 CPU를 점유하면 해당 프로세스가 자발적으로 CPU를 놓아주거나 (I/O 요청 등), 실행을 완전히 마칠 때까지 다른 프로세스는 CPU를 빼앗을 수 없습니다.

구현이 매우 간단하다는 장점이 있지만, FCFS는 치명적인 약점을 가지고 있습니다. 바로 호위 효과(Convoy Effect)입니다.

호위 효과: FCFS의 아킬레스건

호위 효과란 실행 시간이 매우 긴 프로세스 하나가 CPU를 독점하게 되어, 그 뒤에 대기하는 수많은 짧은 프로세스들이 하염없이 기다려야 하는 현상을 말합니다. 마치 고속도로 1차선을 느린 트럭 한 대가 막고 있어서 뒤따르는 수많은 승용차들이 거대한 정체 행렬을 이루는 것과 같습니다.

예를 들어, 다음과 같은 세 개의 프로세스가 거의 동시에 도착했다고 가정해 봅시다.

  • P1: CPU Burst Time = 24ms
  • P2: CPU Burst Time = 3ms
  • P3: CPU Burst Time = 3ms

만약 P1, P2, P3 순서로 큐에 도착했다면, FCFS 스케줄링에 따른 간트 차트(Gantt Chart)는 다음과 같습니다.

  P1 (24ms)                     | P2 (3ms) | P3 (3ms)
+-------------------------------+----------+----------+
0                               24         27         30

이 경우 각 프로세스의 대기 시간은 다음과 같이 계산됩니다.

  • P1의 대기 시간: 0ms (도착 즉시 실행)
  • P2의 대기 시간: 24ms (P1이 끝날 때까지 대기)
  • P3의 대기 시간: 27ms (P1과 P2가 끝날 때까지 대기)

평균 대기 시간: (0 + 24 + 27) / 3 = 17ms

하지만 만약 프로세스들이 P2, P3, P1 순서로 도착했다면 어떻게 될까요?

  P2 (3ms) | P3 (3ms) | P1 (24ms)
+----------+----------+-------------------------------+
0          3          6                               30

이때의 대기 시간은 크게 달라집니다.

  • P2의 대기 시간: 0ms
  • P3의 대기 시간: 3ms
  • P1의 대기 시간: 6ms

평균 대기 시간: (0 + 3 + 6) / 3 = 3ms

단지 도착 순서가 바뀌었을 뿐인데, 시스템의 평균 대기 시간이 17ms에서 3ms로 극적으로 감소했습니다. 이 예시는 FCFS가 도착 순서에 따라 성능이 매우 불안정하며, 특히 긴 작업이 짧은 작업들을 막는 호위 효과에 얼마나 취약한지를 명확하게 보여줍니다. 이 때문에 FCFS는 현대적인 시분할 시스템에서는 단독으로 거의 사용되지 않으며, 다른 알고리즘과 결합하는 형태로만 그 명맥을 유지하고 있습니다.


2. 최단 작업 우선 (Shortest-Job-First, SJF)

SJF 알고리즘은 FCFS의 비효율성을 해결하기 위한 아이디어에서 출발합니다. "가장 빨리 끝날 수 있는 작업부터 처리하자"는 이 단순한 전략은, 주어진 프로세스 집합에 대해 평균 대기 시간을 최소화할 수 있다는 것이 수학적으로 증명된 최적의 알고리즘입니다. SJF는 준비 큐에 있는 프로세스들 중에서 다음 CPU 버스트(burst) 시간이 가장 짧은 것을 선택하여 실행합니다.

SJF는 두 가지 버전으로 나눌 수 있습니다.

  1. 비선점형 (Non-preemptive) SJF: FCFS와 마찬가지로, 일단 한 프로세스가 CPU를 할당받으면 자신의 CPU 버스트가 끝날 때까지 누구도 이를 빼앗을 수 없습니다.
  2. 선점형 (Preemptive) SJF: 현재 실행 중인 프로세스보다 더 짧은 CPU 버스트 시간을 가진 새로운 프로세스가 준비 큐에 도착하면, 운영체제는 즉시 현재 프로세스를 중단시키고 새로운 짧은 프로세스에게 CPU를 할당합니다. 이 선점형 SJF는 특별히 최소 잔여 시간 우선(Shortest-Remaining-Time-First, SRTF) 알고리즘이라고도 부릅니다.

SJF의 동작 예시 (비선점형 vs 선점형)

다음과 같은 프로세스들이 있다고 가정해 봅시다.

프로세스 도착 시간 (Arrival Time) 실행 시간 (Burst Time)
P1 0 7ms
P2 2 4ms
P3 4 1ms
P4 5 4ms

가) 비선점형 SJF

시간의 흐름에 따라 스케줄링을 추적해 봅시다.

  • 시간 0: P1이 도착합니다. 준비 큐에는 P1만 있으므로 P1이 실행을 시작합니다.
  • 시간 2: P2가 도착하지만, P1이 비선점형으로 실행 중이므로 P2는 대기해야 합니다.
  • 시간 4: P3가 도착하지만, 여전히 P1이 실행 중이므로 P3도 대기합니다.
  • 시간 5: P4가 도착하고, P1은 아직 실행 중이므로 P4도 대기합니다.
  • 시간 7: P1의 실행이 종료됩니다. 이제 준비 큐에는 P2(4ms), P3(1ms), P4(4ms)가 있습니다. 이 중 실행 시간이 가장 짧은 P3가 선택되어 실행됩니다.
  • 시간 8: P3의 실행이 종료됩니다. 이제 큐에는 P2(4ms)와 P4(4ms)가 남았습니다. 실행 시간이 같을 경우 FCFS 원칙에 따라 먼저 도착한 P2가 실행됩니다.
  • 시간 12: P2의 실행이 종료되고, 마지막으로 남은 P4가 실행됩니다.
  • 시간 16: 모든 프로세스가 종료됩니다.

간트 차트는 다음과 같습니다.

  P1 (7ms)    | P3 (1ms) | P2 (4ms)    | P4 (4ms)
+-------------+----------+-------------+-------------+
0             7          8             12            16

평균 대기 시간 = ((7-0-7) + (12-2-4) + (8-4-1) + (16-5-4)) / 4 = (0 + 6 + 3 + 7) / 4 = 4ms

나) 선점형 SJF (SRTF)

같은 예시를 선점형 방식으로 실행하면 결과가 달라집니다.

  • 시간 0: P1이 도착하여 실행을 시작합니다. (남은 시간: 7ms)
  • 시간 2: P2가 도착합니다. P2의 실행 시간(4ms)이 P1의 남은 시간(5ms)보다 짧으므로, P1은 선점되고 P2가 실행을 시작합니다.
  • 시간 4: P3가 도착합니다. P3의 실행 시간(1ms)이 현재 실행 중인 P2의 남은 시간(2ms)보다 짧으므로, P2는 선점되고 P3가 실행을 시작합니다.
  • 시간 5: P3가 실행을 마칩니다. P4가 도착합니다. 이제 준비 큐에는 P1(남은 시간 5ms), P2(남은 시간 2ms), P4(실행 시간 4ms)가 있습니다. 이 중 남은 시간이 가장 짧은 P2가 다시 실행됩니다.
  • 시간 7: P2가 실행을 마칩니다. 큐에는 P1(5ms)과 P4(4ms)가 있습니다. 남은 시간이 더 짧은 P4가 실행됩니다.
  • 시간 11: P4가 실행을 마칩니다. 마지막으로 남은 P1이 실행됩니다.
  • 시간 16: 모든 프로세스가 종료됩니다.

간트 차트는 다음과 같습니다.

  P1 | P2 | P3 | P2 | P4 (4ms)    | P1 (5ms)
+----+----+----+----+-------------+-------------+
0    2    4    5    7             11            16

평균 대기 시간 = ((16-0-7) + (7-2-4) + (5-4-1) + (11-5-4)) / 4 = (9 + 1 + 0 + 2) / 4 = 3ms

선점형 방식(SRTF)이 비선점형 방식보다 더 나은 평균 대기 시간을 보여줍니다. 이는 짧은 작업이 시스템에 들어왔을 때 즉각적으로 반응할 수 있기 때문입니다.

SJF의 현실적 한계: 예언가의 딜레마

SJF는 이론적으로는 최적이지만, 현실 세계에서는 치명적인 문제점을 안고 있습니다. 바로 "프로세스의 다음 CPU 버스트 시간을 어떻게 알 수 있는가?"라는 질문입니다. 운영체제는 프로세스가 앞으로 얼마나 오래 CPU를 사용할지 미리 알 수 없습니다. 이는 마치 점쟁이가 미래를 예측해야 하는 것과 같습니다.

이 문제를 해결하기 위해 실제 시스템에서는 과거의 CPU 버스트 기록을 바탕으로 미래를 '예측'하는 기법을 사용합니다. 가장 대표적인 것이 지수 평균(Exponential Averaging)입니다. 다음과 같은 공식을 사용합니다.

τn+1 = α * tn + (1 - α) * τn

  • τn+1: 다음에 예측할 CPU 버스트 시간
  • tn: 가장 최근에 실제로 측정한 CPU 버스트 시간
  • τn: 이전에 예측했던 CPU 버스트 시간
  • α: 가중치 (0 ≤ α ≤ 1), 최근의 실제 측정값에 얼마나 비중을 둘지 결정

이 예측 기법은 어느 정도 효과적이지만 완벽할 수는 없습니다. 또한, SJF는 또 다른 문제인 기아 상태(Starvation)를 유발할 수 있습니다. 시스템에 짧은 작업들이 계속해서 들어온다면, 실행 시간이 긴 작업은 자신의 차례가 영원히 오지 않고 무한정 대기할 수 있습니다. 이러한 한계 때문에 순수한 SJF/SRTF 역시 현대 운영체제에서 그대로 사용되지는 않습니다.


3. 라운드 로빈 (Round Robin, RR)

라운드 로빈은 현대적인 시분할(Time-sharing) 시스템을 위해 설계된 가장 대표적인 스케줄링 알고리즘입니다. FCFS의 단순함과 선점형 방식의 이점을 결합한 형태로, 모든 프로세스가 공평하게 CPU를 나누어 사용하도록 하는 데 초점을 맞춥니다. RR은 각 프로세스에게 시간 할당량(Time Quantum 또는 Time Slice)이라는 작은 CPU 사용 시간을 할당합니다. 프로세스는 이 시간 할당량만큼 실행된 후, 작업이 끝나지 않았더라도 CPU를 내어놓고 준비 큐의 맨 뒤로 돌아가야 합니다.

RR의 성능은 이 '시간 할당량'의 크기에 매우 민감하게 좌우됩니다.

  • 시간 할당량이 너무 큰 경우: 예를 들어 시간 할당량이 1000ms인데 대부분의 프로세스가 10ms 안에 끝난다면, 모든 프로세스는 할당량을 다 쓰기 전에 작업을 마칠 것입니다. 이 경우, 선점은 거의 일어나지 않으며 RR은 사실상 FCFS처럼 동작하게 됩니다.
  • 시간 할당량이 너무 작은 경우: 예를 들어 시간 할당량이 1ms라면, 프로세스들은 매우 자주 교체됩니다. 이는 응답 시간을 단축시키는 효과가 있지만, 심각한 오버헤드를 유발합니다. 프로세스를 교체하는 문맥 교환(Context Switching)에는 레지스터 상태 저장 및 복원, 메모리 맵 변경 등 상당한 비용이 듭니다. 시간 할당량이 문맥 교환 시간과 비슷할 정도로 작아지면, CPU는 실제 작업보다 프로세스 교체에 더 많은 시간을 낭비하게 되어 시스템 전체의 효율성이 급격히 떨어집니다.

따라서 적절한 시간 할당량을 설정하는 것이 RR 스케줄링의 핵심 과제입니다. 일반적으로 대부분의 CPU 버스트가 한 번의 시간 할당량 안에 처리될 수 있도록 설정하는 것이 이상적입니다 (보통 10ms ~ 100ms 범위).

라운드 로빈의 동작 예시

FCFS 예시에서 사용했던 프로세스들을 시간 할당량(q) = 4ms로 RR 스케줄링해 봅시다.

  • P1 (24ms), P2 (3ms), P3 (3ms)

간트 차트는 다음과 같이 그려집니다.

  P1 | P2 | P3 | P1 | P1 | P1 | P1 | P1
+----+----+----+----+----+----+----+----+
0    4    7    10   14   18   22   26   30

실행 흐름은 다음과 같습니다.

  1. P1이 4ms 실행 (남은 시간 20ms), 큐의 맨 뒤로 이동. 큐: [P2, P3, P1]
  2. P2가 3ms 실행 (작업 완료).
  3. P3가 3ms 실행 (작업 완료).
  4. P1이 4ms 실행 (남은 시간 16ms), 다시 뒤로 이동.
  5. 이 과정을 P1이 작업을 마칠 때까지 반복합니다.

RR의 평균 대기 시간은 보통 SJF보다 길지만, 응답 시간이 매우 빠르다는 결정적인 장점이 있습니다. 위 예시에서 P2는 4ms만에, P3는 7ms만에 첫 응답을 받습니다. FCFS의 첫 번째 예시에서 P2와 P3가 24ms, 27ms를 기다려야 했던 것과 비교하면 엄청난 개선입니다. 이러한 빠른 응답성 덕분에 RR은 여러 사용자가 동시에 상호작용하는 시스템에 가장 적합한 알고리즘으로 평가받습니다.

알고리즘 종합 비교 분석

이제 세 가지 알고리즘을 동일한 조건 하에 비교하여 그 특성을 명확히 해보겠습니다. SJF 예시에서 사용했던 프로세스 집합을 다시 가져와 모든 알고리즘에 적용해 보겠습니다.

프로세스 도착 시간 (Arrival Time) 실행 시간 (Burst Time)
P1 0 7ms
P2 2 4ms
P3 4 1ms
P4 5 4ms

1. FCFS 결과

  P1 (7ms)    | P2 (4ms)    | P3 (1ms) | P4 (4ms)
+-------------+-------------+----------+-------------+
0             7             11         12            16
  • 총 처리 시간: P1(7), P2(9), P3(8), P4(11) -> 평균 8.75ms
  • 대기 시간: P1(0), P2(5), P3(7), P4(7) -> 평균 4.75ms

2. 비선점형 SJF 결과

  P1 (7ms)    | P3 (1ms) | P2 (4ms)    | P4 (4ms)
+-------------+----------+-------------+-------------+
0             7          8             12            16
  • 총 처리 시간: P1(7), P2(10), P3(4), P4(11) -> 평균 8ms
  • 대기 시간: P1(0), P2(6), P3(3), P4(7) -> 평균 4ms

3. 선점형 SJF (SRTF) 결과

  P1 | P2 | P3 | P2 | P4 (4ms)    | P1 (5ms)
+----+----+----+----+-------------+-------------+
0    2    4    5    7             11            16
  • 총 처리 시간: P1(16), P2(5), P3(1), P4(6) -> 평균 7ms
  • 대기 시간: P1(9), P2(1), P3(0), P4(2) -> 평균 3ms

4. 라운드 로빈 (q=3ms) 결과

  P1 | P2 | P3 | P1 | P4 | P2 | P1
+----+----+----+----+----+----+----+
0    3    6    7    10   13   14   16
  • 총 처리 시간: P1(16), P2(12), P3(3), P4(8) -> 평균 9.75ms
  • 대기 시간: P1(9), P2(8), P3(2), P4(4) -> 평균 5.75ms

(RR의 계산은 문맥 교환이 빈번하여 복잡하므로, 각 프로세스가 큐에서 대기한 시간을 모두 합산하여 계산합니다.)

비교 요약표

지표 FCFS SJF (비선점) SRTF (선점 SJF) Round Robin
평균 대기 시간 나쁨 (17ms/4.75ms) 좋음 (4ms) 최적 (3ms) 보통 (5.75ms)
평균 응답 시간 매우 나쁨 나쁨 좋음 매우 좋음
기아 상태 (Starvation) 없음 발생 가능성 높음 없음
구현 복잡도 매우 쉬움 어려움 (버스트 시간 예측 필요) 보통 (타이머 인터럽트 필요)
시스템 종류 일괄 처리 시스템 일괄 처리, 특수 목적 시스템 대화형, 시분할 시스템

결론: 완벽한 알고리즘은 없다

지금까지 살펴본 것처럼, 모든 상황에 완벽한 '최고의' CPU 스케줄링 알고리즘은 존재하지 않습니다. 각 알고리즘은 서로 다른 철학과 목표를 가지고 있으며, 특정 환경에서 강점을 보이는 대신 다른 환경에서는 약점을 드러냅니다.

  • FCFS는 공평성과 단순함을 추구하지만, 효율성을 희생합니다.
  • SJF/SRTF는 시스템 전체의 평균 대기 시간을 최소화하여 최고의 효율성을 추구하지만, 기아 상태를 유발하고 미래를 예측해야 하는 비현실적인 가정을 전제로 합니다.
  • Round Robin은 모든 프로세스에게 공평한 기회와 빠른 응답성을 제공하여 사용자 경험을 극대화하지만, 잦은 문맥 교환으로 인한 오버헤드 때문에 총 처리 시간 면에서는 손해를 볼 수 있습니다.

현대의 범용 운영체제(Linux, Windows, macOS 등)는 이러한 기본 알고리즘들을 그대로 사용하지 않습니다. 대신, 이들의 장점을 결합하고 단점을 보완한 훨씬 더 정교하고 복잡한 스케줄링 기법을 사용합니다. 예를 들어, 다단계 큐 스케줄링(Multilevel Queue Scheduling)이나 다단계 피드백 큐 스케줄링(Multilevel Feedback Queue Scheduling)은 프로세스의 특성(대화형 작업, 백그라운드 작업 등)에 따라 여러 개의 큐를 두고 각 큐마다 다른 스케줄링 알고리즘(예: 전면 큐는 RR, 후면 큐는 FCFS)을 적용합니다. 또한, 우선순위를 동적으로 조정하여 기아 상태를 방지하는 '노화(Aging)' 기법 등을 도입합니다.

결국 CPU 스케줄링의 역사는 시스템의 효율성과 사용자의 편의성이라는 두 마리 토끼를 잡기 위한 끊임없는 트레이드오프의 역사입니다. FCFS, SJF, RR이라는 세 가지 고전적인 알고리즘을 깊이 이해하는 것은 이러한 복잡한 현대 스케줄러의 근간을 파악하고, 우리가 매일 사용하는 컴퓨터 시스템이 어떻게 수많은 작업을 조화롭게 처리해내는지에 대한 근본적인 통찰을 얻는 첫걸음이 될 것입니다.

Understanding CPU Scheduling Through Core Algorithms

In the intricate world of modern operating systems, the Central Processing Unit (CPU) acts as the brain, executing billions of instructions per second. However, this powerful resource can only execute one instruction from one process at a time. Yet, we seamlessly run dozens of applications simultaneously—a web browser, a music player, a code editor, and numerous background services—all appearing to operate in parallel. This illusion of concurrency is masterfully managed by a core component of the operating system: the CPU scheduler. Its fundamental job is to decide which of the many ready-to-run processes gets to use the CPU next, and for how long. The choice of scheduling algorithm is not merely a technical detail; it profoundly impacts the system's performance, responsiveness, and fairness, defining the user experience itself.

Choosing the right algorithm involves a delicate balancing act. Do we prioritize finishing as many tasks as possible in the shortest amount of time (maximizing throughput)? Do we aim to provide the quickest response to a user's keystroke or mouse click (minimizing response time)? Or do we ensure that every process gets a fair share of the CPU, preventing any single process from being neglected for too long? There is no single "best" algorithm. The optimal choice is entirely dependent on the system's intended purpose, whether it's a high-performance computing cluster, a desktop operating system, or a real-time embedded device. In this exploration, we will delve into three foundational algorithms—First-Come, First-Served (FCFS), Shortest-Job-First (SJF), and Round Robin (RR)—to understand not just how they work, but the deeper truths and trade-offs they represent in the complex art of process management.

The Ground Rules: How We Measure Scheduler Performance

Before comparing algorithms, we must establish a common language for evaluating them. An operating system designer considers several key metrics when analyzing a scheduler's efficiency. Understanding these criteria is essential to appreciating the subtle strengths and critical weaknesses of each approach.

  • CPU Utilization: This metric represents the percentage of time the CPU is busy executing processes rather than being idle. In an ideal world, we want to keep the CPU as busy as possible, maximizing the return on our hardware investment. A high utilization rate generally indicates an efficient system, though 100% utilization can sometimes signal a bottleneck.
  • Throughput: This is the number of processes completed per unit of time. For batch systems or servers processing large numbers of jobs, high throughput is often the primary goal. It's a measure of raw processing power and efficiency over a longer period.
  • Turnaround Time: Defined as the total time taken from a process's submission to its completion. This includes the time spent waiting to get into memory, waiting in the ready queue, executing on the CPU, and doing I/O. From a user's perspective, this is the total time they have to wait for their job to finish.
  • Waiting Time: This is the sum of the periods a process spends waiting in the ready queue. Turnaround time is essentially the waiting time plus the actual execution time. Since the execution time is fixed for a given process, minimizing the waiting time is a primary target for many scheduling algorithms.
  • Response Time: In interactive systems, this is arguably the most important metric. It is the time from the submission of a request until the first response is produced. For a user clicking an icon, it's the time until the application window appears, not the time until the entire application has loaded. A scheduler that provides a low response time feels "snappy" and responsive to the user, even if its overall turnaround time is longer.

These metrics are often in conflict. For instance, an algorithm that optimizes for throughput might result in poor response time for interactive users. The genius of operating system design lies in finding an algorithm, or a combination of algorithms, that strikes the right balance for the system's specific goals.

First-Come, First-Served (FCFS): The Illusion of Simplicity and Fairness

The First-Come, First-Served algorithm is the most straightforward scheduling policy imaginable. As its name suggests, processes are allocated the CPU in the order they arrive in the ready queue. It is a non-preemptive algorithm, meaning that once a process is given the CPU, it holds onto it until it either terminates or voluntarily enters a waiting state (for example, to perform an I/O operation). Its implementation is simple, managed with a basic FIFO (First-In, First-Out) queue.

Let's consider a simple scenario to see FCFS in action. Suppose we have three processes arriving at the same time (time 0) with the following CPU burst times:

  • Process P1: 24 milliseconds (ms)
  • Process P2: 3 ms
  • Process P3: 3 ms

If they arrive in the order P1, P2, P3, the FCFS scheduler will execute them in that exact sequence. We can visualize this with a Gantt chart:

   P1                | P2  | P3  |
|--------------------|-----|-----|
0                   24    27    30

Now, let's calculate the waiting time for each process:

  • P1 waits for 0 ms (it starts immediately).
  • P2 waits for 24 ms (it has to wait for P1 to finish).
  • P3 waits for 27 ms (it has to wait for both P1 and P2).

The average waiting time is (0 + 24 + 27) / 3 = 17 ms. This result exposes the fundamental flaw of FCFS.

The Convoy Effect: A Critical Weakness

The "truth" behind FCFS is a phenomenon known as the convoy effect. Imagine being in a grocery store line behind someone with a cart piled high with items, while you're only holding a single carton of milk. This is precisely what happens in FCFS. A long process (P1 in our example) can monopolize the CPU, forcing many short processes (P2, P3) to wait for an unnecessarily long time. The CPU-bound process creates a "convoy" of I/O-bound processes waiting behind it.

Let's see what happens if the same processes arrive in a different order: P2, P3, P1.

 P2 | P3 | P1                |
|----|----|-------------------|
0    3    6                  30

The waiting times now become:

  • P1 waits for 6 ms.
  • P2 waits for 0 ms.
  • P3 waits for 3 ms.

The average waiting time plummets to (6 + 0 + 3) / 3 = 3 ms. This is a dramatic improvement! The same set of processes with the same algorithm yields wildly different performance based solely on arrival order. This variability and the punishing nature of the convoy effect make FCFS a poor choice for most modern, time-sharing operating systems. It is simple to implement, but its performance is often unacceptable, especially when there is a mix of long and short processes.

Shortest-Job-First (SJF): The Optimal but Unattainable Ideal

The Shortest-Job-First (SJF) scheduling algorithm approaches the problem from a different angle. The core idea is simple: when the CPU is available, it is assigned to the process that has the smallest next CPU burst. If two processes have the same length, FCFS can be used to break the tie. This approach is provably optimal in terms of minimizing the average waiting time for a given set of processes.

SJF can exist in two forms: non-preemptive and preemptive.

Non-Preemptive SJF

In the non-preemptive version, once the CPU is given to a process, it cannot be taken away until the process completes its CPU burst. The scheduler simply looks at all processes in the ready queue and selects the one with the shortest burst time.

Let's consider a set of processes with their arrival times and burst times:

Process Arrival Time Burst Time
P1 0 7
P2 2 4
P3 4 1
P4 5 4

Here's how non-preemptive SJF would schedule them:

  1. At time 0, only P1 is in the queue. P1 starts executing.
  2. Since it's non-preemptive, P1 runs to completion at time 7. While P1 was running, P2 (at time 2), P3 (at time 4), and P4 (at time 5) all arrived.
  3. At time 7, the scheduler looks at the ready queue, which contains P2, P3, and P4. Their burst times are 4, 1, and 4 respectively. P3 has the shortest burst, so it is selected.
  4. P3 runs from time 7 to 8.
  5. At time 8, the ready queue contains P2 and P4. They both have a burst time of 4. Using FCFS as a tie-breaker, P2 is selected (as it arrived earlier).
  6. P2 runs from time 8 to 12.
  7. Finally, P4 runs from time 12 to 16.

The Gantt chart looks like this:

   P1      | P3 |  P2  |  P4  |
|----------|----|------|------|
0          7    8      12     16

The average waiting time is ((7-4) + (8-2) + (12-5) + (0-0)) / 4 = (3 + 6 + 7 + 0) / 4 = 4 ms. (Wait Time = Start Time - Arrival Time). This is a significant improvement over what FCFS would likely produce.

Preemptive SJF (Shortest-Remaining-Time-First, SRTF)

The preemptive version, also known as Shortest-Remaining-Time-First (SRTF), takes this a step further. If a new process arrives in the ready queue with a CPU burst length that is less than the remaining time of the currently executing process, the scheduler preempts the current process and allocates the CPU to the new, shorter process. This can lead to even better average waiting times.

Let's use the same example with SRTF:

  1. At time 0, P1 starts (remaining time = 7).
  2. At time 2, P2 arrives (burst time = 4). The remaining time for P1 is 5 (7 - 2). Since P2's burst (4) is less than P1's remaining time (5), P1 is preempted. P2 starts executing.
  3. At time 4, P3 arrives (burst time = 1). The remaining time for P2 is 2 (4 - 2). Since P3's burst (1) is less than P2's remaining time (2), P2 is preempted. P3 starts executing.
  4. At time 5, P3 is still running. P4 arrives (burst time = 4). P3's remaining time is 0 (it will finish at time 5). P3 completes.
  5. At time 5, the ready queue contains P1 (remaining time = 5), P2 (remaining time = 2), and P4 (burst time = 4). P2 has the shortest remaining time, so it resumes execution.
  6. P2 runs to completion from time 5 to 7.
  7. At time 7, the ready queue has P1 (remaining = 5) and P4 (remaining = 4). P4 is shorter, so it starts.
  8. P4 runs from time 7 to 11.
  9. Finally, P1 resumes and runs from time 11 to 16.

The Gantt chart for SRTF is more fragmented:

 P1 | P2 | P3 | P2 |  P4  |   P1    |
|----|----|----|----|------|---------|
0    2    4    5    7      11        16

This looks more complex, but the results are impressive. Let's calculate the waiting time:

  • P1: Waited from 2 to 11. Total wait = (11-2) = 9.
  • P2: Waited from 4 to 5. Total wait = (5-4) = 1.
  • P3: Waited 0. It started immediately upon arrival.
  • P4: Waited from 5 to 7. Total wait = (7-5) = 2.
The average waiting time is (9 + 1 + 0 + 2) / 4 = 3 ms. This is even better than non-preemptive SJF.

The Great Challenge: Predicting the Future

SJF is provably optimal, but it carries a monumental, often fatal flaw: there is no way to know the exact length of the next CPU burst. A process's future behavior is unknown. The scheduler can't see into the future. This is the "truth" that makes pure SJF an unimplemented theoretical benchmark rather than a practical algorithm. Operating systems can only attempt to predict the next burst length. A common technique is to use exponential averaging of the process's previous CPU bursts. The formula might look something like this:

τn+1 = α * tn + (1 - α) * τn

Where τn+1 is the predicted length for the next burst, tn is the actual length of the most recent burst, τn was the previous prediction, and α is a weighting factor between 0 and 1. By adjusting α, the system can give more weight to recent behavior or historical averages. However, this is still just an educated guess. A process that has been I/O-bound (with short CPU bursts) could suddenly become CPU-bound (with a long burst), and the prediction would be wildly inaccurate, undermining the "optimality" of the algorithm.

Furthermore, SJF suffers from a severe risk of starvation. A long process might be ready to run, but if a steady stream of shorter processes keeps arriving, the long process might be postponed indefinitely, never getting a chance to execute. This is unacceptable in any system that requires a guarantee of forward progress for all processes.

Round Robin (RR): A Fair Share for Everyone

Round Robin (RR) is one of the most common and important scheduling algorithms, designed specifically for time-sharing systems. It addresses the major shortcomings of both FCFS (poor response time) and SJF (starvation and the need to predict the future). The core principle of RR is not to let any single process run for too long. It is a preemptive algorithm, similar to FCFS but with the addition of a time limit.

The RR scheduler maintains a FIFO queue of ready processes. When a process is dispatched, it is given a small unit of CPU time called a time quantum or time slice, typically in the range of 10 to 100 milliseconds. When the quantum expires, the scheduler generates an interrupt, preempts the running process, and places it at the end of the ready queue. The scheduler then dispatches the next process from the front of the queue for its own time quantum. If a process finishes its burst before the quantum expires, it voluntarily releases the CPU, and the next process can start immediately.

Let's revisit our first example (P1=24, P2=3, P3=3) with a time quantum of 4 ms.

 P1 | P2 | P3 | P1 | P1 | P1 | P1 | P1 |
|----|----|----|----|----|----|----|----|
0    4    7    10   14   18   22   26   30
  1. P1 runs for 4 ms. Timer interrupts. P1 is moved to the back of the queue.
  2. P2 runs. It only needs 3 ms, so it completes at time 7.
  3. P3 runs. It only needs 3 ms, so it completes at time 10.
  4. P1 is now at the front and runs for another 4 ms. This repeats until P1 completes its total of 24 ms.

The average waiting time for RR is often quite high. In this case, P1 waits in chunks (4-0, 10-4, etc.), P2 waits from 0-4, and P3 waits from 0-7. But its key advantage is excellent response time. P2 and P3 get to start running very quickly, instead of having to wait for the entire duration of P1. This makes the system feel responsive to the user.

The Double-Edged Sword: The Time Quantum

The performance of the Round Robin algorithm is critically dependent on the size of the time quantum. This choice represents a fundamental trade-off in operating system design.

  • If the quantum is too large: The algorithm's behavior approaches that of FCFS. If the quantum is longer than the longest CPU burst of any process, every process will finish its burst before the timer interrupts, effectively making the scheduling order First-Come, First-Served. The benefit of responsiveness is lost.
  • If the quantum is too small: The algorithm provides excellent response time, as processes are switched very frequently. However, this comes at a significant cost. Every time the scheduler preempts one process and dispatches another, it performs a context switch. This is not a free operation. The OS must save the state of the current process (CPU registers, program counter, memory map) into its Process Control Block (PCB) and then load the state of the next process from its PCB. This process takes time and consumes CPU cycles that could have been used for actual work. If the quantum is, for example, 1 ms and a context switch takes 0.1 ms, then 10% of the CPU's time is wasted on the overhead of scheduling itself! This dramatically reduces system throughput.

Therefore, the "truth" of Round Robin is that a balance must be found. The time quantum must be long enough to minimize the relative overhead of context switching but short enough to provide a good response time for interactive users. A common rule of thumb is to set the quantum so that about 80% of CPU bursts are shorter than the quantum, which allows most processes to finish in a single time slice while still preempting the longer ones.

A Deeper Look at Context Switch Overhead

The cost of a context switch is more than just saving and loading registers. Modern CPUs rely heavily on multiple levels of caching (L1, L2, L3) to speed up memory access. When a context switch occurs, the data for the outgoing process in these caches becomes largely useless for the incoming process. The new process will suffer a series of "cache misses" as it starts, having to fetch data from the much slower main memory. This "cache pollution" and subsequent warming up of the cache is a significant, often hidden, component of context switch overhead. A very small time quantum can lead to a state of "cache thrashing," where the CPU spends more time refilling its caches than performing useful computations, severely degrading overall performance.

Comparative Analysis: A Strategic Overview

Each of these foundational algorithms serves a different master. Choosing between them requires a clear understanding of the system's priorities. Let's summarize their characteristics in a comparative table.

Criterion First-Come, First-Served (FCFS) Shortest-Job-First (SJF/SRTF) Round Robin (RR)
CPU Overhead Very Low (minimal scheduler intervention) Moderate (requires calculation/prediction and, for SRTF, preemption checks) High (frequent timer interrupts and context switches)
Primary Goal Simplicity and predictability Minimize average waiting time Minimize response time and provide fairness
Starvation Risk No (every process eventually runs) High (long processes can be starved by a stream of short ones) No (every process is guaranteed a time slice within a bounded time)
Preemption Non-preemptive Can be both (Non-preemptive SJF or Preemptive SRTF) Preemptive (at the end of a time quantum)
Best For Simple batch systems where job order is fixed. Batch systems where turnaround time is critical and burst times are well-known. General-purpose, interactive, time-sharing systems (e.g., desktops, servers).
Key Weakness The Convoy Effect leads to poor average waiting time. Inability to know future CPU burst times makes it impractical. Performance is highly sensitive to the time quantum size.

Beyond the Basics: Scheduling in Modern Operating Systems

While FCFS, SJF, and RR provide the conceptual building blocks, no modern operating system like Linux, Windows, or macOS uses any of them in their pure form. Real-world schedulers are far more sophisticated, employing hybrid strategies to balance the conflicting goals of throughput, responsiveness, and fairness. They often use variants of Priority Scheduling and Multilevel Queue Scheduling.

In a priority scheduling scheme, each process is assigned a priority number, and the CPU is allocated to the process with the highest priority. SJF can be viewed as a special case of priority scheduling where the priority is the inverse of the predicted next CPU burst. The main problem with simple priority schemes is, once again, starvation. A low-priority process might never run.

The common solution to this is aging. Aging is a technique where the priority of a process is gradually increased the longer it waits in the ready queue. Eventually, even the lowest-priority process will age enough to have its priority raised to a level where it will be scheduled, thus defeating starvation.

Multilevel Feedback Queue Scheduling

The most common approach in modern systems is the Multilevel Feedback Queue (MLFQ). This scheduler separates the ready queue into several different queues, each with its own scheduling algorithm and priority level. For instance:

  • Queue 0 (Highest Priority): Uses Round Robin with a very short time quantum (e.g., 8 ms). Intended for highly interactive processes.
  • Queue 1 (Medium Priority): Uses Round Robin with a longer time quantum (e.g., 16 ms).
  • Queue 2 (Lowest Priority): Uses FCFS. Intended for long-running batch processes.

The "feedback" mechanism is the most important part. The scheduler observes the behavior of processes and moves them between queues:

  • A new process enters the highest-priority queue (Queue 0).
  • If it uses its entire time quantum without blocking for I/O, the scheduler assumes it is a CPU-bound process and demotes it to the next lower-priority queue (Queue 1).
  • If it uses its entire quantum in Queue 1, it is demoted again to Queue 2.
  • Conversely, if a process in a lower-priority queue has been waiting for a very long time (a form of aging), it might be promoted to a higher-priority queue to prevent starvation.

This approach is beautifully adaptive. An interactive process that needs quick bursts of CPU (like a text editor responding to a keystroke) will stay in the high-priority queues and receive excellent response time. A CPU-intensive process (like a video encoder) will quickly filter down to the low-priority queues, where it can run for longer periods without interfering with interactive performance. The MLFQ is complex to configure, but it provides a robust framework for satisfying the diverse needs of modern applications.

Conclusion: The Art of the Possible

The journey from the simple queue of FCFS to the adaptive complexity of the MLFQ reveals the core truth of CPU scheduling: it is a discipline of carefully managed trade-offs. There is no perfect algorithm, only the right algorithm for a specific set of goals. FCFS teaches us about the danger of the convoy effect. SJF shows us the theoretical peak of efficiency but also the practical impossibility of predicting the future and the moral hazard of starvation. Round Robin demonstrates how to achieve responsiveness for interactive users but at the cost of context-switching overhead, hinging its entire performance on the delicate choice of a time quantum.

Understanding these fundamental algorithms is not just an academic exercise. It provides the essential vocabulary and conceptual framework for comprehending why our computers behave the way they do—why a system under heavy load can still feel responsive, and how an operating system juggles countless competing demands for the single, precious resource of the CPU. The elegant dance of the scheduler, hidden deep within the kernel, is what transforms a piece of silicon into a dynamic, multitasking powerhouse.

CPUスケジューリングの核心 FCFS, SJF, RR方式を徹底比較

現代のコンピュータシステムにおいて、オペレーティングシステム(OS)が果たす役割は計り知れません。その中でも、最も根幹的かつ重要な機能の一つが「CPUスケジューリング」です。私たちの目には見えませんが、OSは常にどのプログラムにCPUという貴重な計算資源を割り当てるかという、複雑な意思決定をミリ秒単位で行っています。この意思決定のルールこそが、CPUスケジューリングアルゴリズムです。

このアルゴリズムの選択は、システムのパフォーマンス、応答性、そしてユーザー体験に直接的な影響を及ぼします。例えば、動画を再生しながら文書を作成し、バックグラウンドでファイルをダウンロードするといったマルチタスク環境がスムーズに動作するのは、優れたスケジューリングアルゴリズムのおかげです。もしこの選択が不適切であれば、システムは頻繁にフリーズしたり、アプリケーションの反応が著しく遅くなったりするでしょう。

本記事では、CPUスケジューリングの世界への第一歩として、最も古典的でありながら基本となる3つのアルゴリズム「FCFS(First-Come, First-Served)」「SJF(Shortest Job First)」「ラウンドロビン(Round Robin)」に焦点を当てます。これらのアルゴリズムがどのように動作し、それぞれがどのような長所と短所を持つのかを、具体的な例を交えながら深く掘り下げていきます。単なる定義の羅列ではなく、なぜそのアルゴリズムが特定状況下で有効または非効率なのか、その「真実」に迫ることを目指します。

CPUスケジューリングを理解するための基礎知識

具体的なアルゴリズムの解説に入る前に、スケジューリングの性能を評価するための共通の指標や用語を理解しておくことが不可欠です。これらの概念は、各アルゴリズムの特性を比較分析する際の基盤となります。

  • プロセス(Process): 実行中のプログラムのインスタンス。OSはこれらのプロセスを管理し、CPU時間を割り当てます。
  • CPUバースト時間(CPU Burst Time): 1つのプロセスが、I/O待ちなどが発生するまでにCPUを連続して使用する時間。実行時間とも呼ばれます。
  • 到着時刻(Arrival Time): プロセスが実行可能状態になり、スケジューラのキュー(待ち行列)に追加された時刻。
  • ターンアラウンドタイム(Turnaround Time): プロセスがキューに到着してから、その実行が完全に終了するまでの総時間。これは「待ち時間」と「実行時間」の合計に等しくなります。(ターンアラウンドタイム = 終了時刻 - 到着時刻)
  • 待ち時間(Waiting Time): プロセスが実行可能キューで、CPUが割り当てられるのを待っている時間の合計。(待ち時間 = ターンアラウンドタイム - CPUバースト時間)
  • 応答時間(Response Time): プロセスがキューに到着してから、初めてCPUが割り当てられ、何らかの応答を開始するまでの時間。インタラクティブなシステムにおいて特に重要な指標です。
  • スループット(Throughput): 単位時間あたりに完了したプロセスの数。システムの処理能力を示す指標です。
  • コンテキストスイッチ(Context Switch): CPUが現在実行しているプロセスを中断し、別のプロセスの実行を開始するために、プロセスの状態(レジスタ情報など)を保存・復元する処理。これにはオーバーヘッド(時間的コスト)が伴います。

これらの指標は、しばしばトレードオフの関係にあります。例えば、あるアルゴリズムが平均待ち時間を劇的に短縮できたとしても、コンテキストスイッチの回数が増加し、システム全体のスループットが低下する可能性があります。スケジューリングアルゴリズムの設計とは、まさにこれらの指標間の最適なバランスを見つける挑戦なのです。

1. FCFS(First-Come, First-Served)アルゴリズム:公平という名の罠

FCFSは、その名の通り「先に来たものから順に処理する」という、最もシンプルで直感的なスケジューリングアルゴリズムです。これは、スーパーのレジ待ち行列や銀行の窓口と全く同じ原理で動作します。OSは、実行可能キューに到着したプロセスを、到着した順番通りにFIFO(First-In, First-Out)キューで管理します。

FCFSの動作原理

実装は非常に簡単です。OSは単純なキューデータ構造を一つ用意し、新しいプロセスが到着するたびにキューの末尾に追加します。CPUが空くと、キューの先頭からプロセスを取り出して実行を開始します。そのプロセスが完了するか、I/O待ちでブロックされるまで、CPUを占有し続けます。

プロセス到着順: P1 -> P2 -> P3

実行可能キュー:
[ P1 | P2 | P3 ]
  ↑
 次に実行

1. CPUがP1の実行を開始。
2. P1が完了。
3. CPUがP2の実行を開始。
4. P2が完了。
5. CPUがP3の実行を開始。
...

具体例で見るFCFS

以下の3つのプロセスが、ほぼ同時に到着(到着時刻を0とする)した場合を考えてみましょう。

プロセス CPUバースト時間 (ms)
P1 24
P2 3
P3 3

FCFSでは、到着順(P1, P2, P3)で処理されるため、実行の様子は以下のガントチャートで表せます。

ガントチャート:

|<----------- P1 (24ms) ----------->|<-- P2 (3ms) -->|<-- P3 (3ms) -->|
0 24 27 30 (ms)

この場合の各プロセスの待ち時間を計算してみましょう。

  • P1の待ち時間: 0 ms (到着後すぐに実行開始)
  • P2の待ち時間: 24 ms (P1が終了するまで待機)
  • P3の待ち時間: 27 ms (P1とP2が終了するまで待機)

平均待ち時間 = (0 + 24 + 27) / 3 = 17 ms

FCFSの長所と深刻な短所

長所

  • 実装が容易: FIFOキューを管理するだけでよいため、アルゴリズム自体は非常にシンプルです。
  • 公平性: 到着順という、誰にとっても分かりやすい基準で処理されるため、直感的な公平感があります。

短所:コンボイ効果(Convoy Effect)

FCFSの最大の欠点は、コンボイ効果として知られています。これは、CPUバースト時間が非常に長いプロセスが一つキューの先頭にあると、その後ろに続く多数の短いプロセスが、その長いプロセスが終わるまで延々と待たされてしまう現象です。先ほどの例がまさにこれを示しています。

もし、プロセスの到着順がP2, P3, P1だったらどうなっていたでしょうか?

ガントチャート (P2, P3, P1の順):

|<-- P2 (3ms) -->|<-- P3 (3ms) -->|<----------- P1 (24ms) ----------->|
0 3 6 30 (ms)

この場合の待ち時間は、

  • P2の待ち時間: 0 ms
  • P3の待ち時間: 3 ms
  • P1の待ち時間: 6 ms

平均待ち時間 = (0 + 3 + 6) / 3 = 3 ms

驚くべきことに、プロセスの到着順が少し変わっただけで、平均待ち時間は17msから3msへと劇的に改善しました。このことから、FCFSはプロセスの到着順に性能が大きく依存し、特にインタラクティブなシステム(短い応答時間が求められるシステム)には全く向いていないことが分かります。ユーザーがマウスをクリックするという短い処理が、裏で動いている重いバッチ処理の完了を待たなければならない状況を想像してみてください。それは非常にストレスの溜まる体験となるでしょう。

2. SJF(Shortest Job First)アルゴリズム:理想と現実の狭間

SJFアルゴリズムは、FCFSのコンボイ効果の問題を解決するために考案されました。そのアイデアは非常にシンプルで、「次に実行するプロセスとして、CPUバースト時間が最も短いものを選択する」というものです。これにより、短いプロセスが長いプロセスによってブロックされるのを防ぎ、システム全体の平均待ち時間を最小化しようとします。

SJFの種類:非プリエンプティブとプリエンプティブ

SJFには、大きく分けて2つのバリエーションが存在します。

  • 非プリエンプティブSJF (Non-Preemptive SJF): CPUが空いた時点で、実行可能キュー内にいるプロセスの中から最もCPUバースト時間が短いものを選択し、実行を開始します。一度実行が始まったプロセスは、自ら終了するかI/O待ちに入るまで中断されることはありません。
  • プリエンプティブSJF (Preemptive SJF): こちらはSRTF(Shortest Remaining Time First)アルゴリズムとも呼ばれます。現在あるプロセスが実行中であっても、それより「残りの」実行時間が短い新しいプロセスがキューに到着した場合、OSは現在のプロセスを強制的に中断(プリエンプション)し、新しく到着した、より短いプロセスにCPUを割り当てます。

具体例で見るSJF

ここでは、到着時刻が異なるプロセス群を例に、非プリエンプティブSJFとプリエンプティブSJF(SRTF)の動作を比較してみましょう。

プロセス 到着時刻 (ms) CPUバースト時間 (ms)
P1 0 7
P2 2 4
P3 4 1
P4 5 4

非プリエンプティブSJFの場合

  1. 時刻0: P1が到着。キューにはP1のみなので、P1の実行が開始されます。
  2. 時刻2: P2が到着。しかし、P1は非プリエンプティブなので実行を続けます。
  3. 時刻4: P3が到着。P1はまだ実行中です。
  4. 時刻5: P4が到着。P1はまだ実行中です。
  5. 時刻7: P1が実行を終了。この時点でキューにはP2(4ms), P3(1ms), P4(4ms)がいます。最もバースト時間が短いのはP3なので、P3の実行が開始されます。
  6. 時刻8: P3が終了。キューにはP2(4ms)とP4(4ms)がいます。バースト時間が同じなので、ここではFCFSルールを適用し、先に到着したP2を実行します。
  7. 時刻12: P2が終了。最後にP4を実行します。
  8. 時刻16: P4が終了。

ガントチャート (非プリエンプティブSJF):

|<---- P1 (7ms) ---->|<P3(1)>|<--- P2 (4ms) --->|<--- P4 (4ms) --->|
0 7 8 12 16 (ms)

平均待ち時間 = ((7-0-7) + (12-2-4) + (8-4-1) + (16-5-4)) / 4 = (0 + 6 + 3 + 7) / 4 = 4 ms

プリエンプティブSJF(SRTF)の場合

  1. 時刻0: P1が到着し、実行を開始。(残り時間: 7ms)
  2. 時刻2: P2が到着。P2のバースト時間(4ms)は、P1の残り時間(5ms)より短い。よってP1は中断され、P2の実行が開始されます。
  3. 時刻4: P3が到着。P3のバースト時間(1ms)は、P2の残り時間(2ms)より短い。よってP2は中断され、P3の実行が開始されます。
  4. 時刻5: P3が実行を終了。この時点でキューにはP1(残り5ms)とP2(残り2ms)、そして新たに到着したP4(4ms)がいます。残りの時間が最も短いのはP2なので、P2の実行を再開します。
  5. 時刻7: P2が実行を終了。キューにはP1(残り5ms)とP4(4ms)がいます。残りの時間が短いP4の実行を開始します。
  6. 時刻11: P4が実行を終了。最後に残ったP1の実行を再開します。
  7. 時刻16: P1が実行を終了。

ガントチャート (プリエンプティブSJF / SRTF):

|<P1(2)>|<P2(2)>|<P3(1)>|<P2(2)>|<--- P4 (4ms) --->|<---- P1 (5ms) ---->|
0 2 4 5 7 11 16 (ms)

平均待ち時間 = ((16-0-7) + (7-2-4) + (5-4-1) + (11-5-4)) / 4 = (9 + 1 + 0 + 2) / 4 = 3 ms

このように、プリエンプティブ版であるSRTFは、非プリエンプティブ版よりもさらに平均待ち時間を短縮できることが分かります。実際、SRTFは平均待ち時間を最小化するという観点において、理論上最適なアルゴリズムであることが証明されています。

SJFの長所と致命的な問題点

長所

  • 最適な平均待ち時間: 上述の通り、特にSRTFは平均待ち時間を最小化する上で最も効率的なアルゴリズムです。これによりシステム全体のスループットが向上します。

短所

  • CPUバースト時間の予測問題: SJFの最大の欠点であり、現実世界での直接的な適用を困難にしているのがこの問題です。「未来の」CPUバースト時間を正確に知ることは不可能です。OSは過去の実行履歴から次のバースト時間を予測するしかありません。一般的な予測方法として、過去の実績値に重みをつけた指数平均法などがあります。
    τn+1 = α * tn + (1 - α) * τn
    (τn+1: 次の予測値, tn: n回目の実績値, τn: n回目の予測値, α: 重み係数)
    しかし、この予測は完璧ではなく、予測が外れた場合のスケジューリングは最適とは言えなくなります。
  • 飢餓状態(Starvation): SJF、特にSRTFでは、CPUバースト時間が長いプロセスが、後から到着する短いプロセスに割り込まれ続け、いつまでたっても実行されない「飢餓状態」に陥る可能性があります。システムの公平性を著しく損なう危険性があります。

SJFは理論的には魅力的ですが、この「未来予測の不可能性」と「飢餓状態」という2つの大きな壁により、そのままの形では現代の汎用OSで採用されることは稀です。しかし、その思想は後述するより高度なアルゴリズムに取り入れられています。

3. ラウンドロビン(Round Robin, RR)アルゴリズム:対話的システムの寵児

FCFSがバッチ処理には向いていても対話型システムには不向きであり、SJFが理論上最適でも現実には適用が難しいという問題がありました。そこで登場するのが、現代のタイムシェアリングシステム(TSS)の基礎となっているラウンドロビン(RR)アルゴリズムです。

RRの基本的な考え方は「全てのプロセスに公平に少しずつCPU時間を与える」ことです。これにより、どのプロセスも長時間待たされることなく、短い応答時間を得ることができます。

ラウンドロビンの動作原理

RRは、FCFSと同様にFIFOキューでプロセスを管理しますが、1つのプロセスがCPUを独占するのを防ぐために「タイムクォンタム(Time Quantum)」または「タイムスライス(Time Slice)」と呼ばれる短い時間単位を導入します。

  1. CPUが空くと、実行可能キューの先頭からプロセスを取り出し、タイマーをタイムクォンタムに設定して実行を開始します。
  2. 以下のいずれかのイベントが発生すると、プロセスは中断されます。
    • a) プロセスがタイムクォンタムを使い切る前に終了またはI/O待ちになった場合: プロセスはCPUを解放し、OSはキューの次のプロセスを実行します。
    • b) タイムクォンタムを使い切ったが、プロセスがまだ完了していない場合: OSはタイマー割り込みによってプロセスを強制的に中断(プリエンプション)し、そのプロセスを実行可能キューの「末尾」に戻します。そして、キューの先頭にいる次のプロセスにCPUを割り当てます。

この仕組みにより、全てのプロセスが定期的にCPU時間を割り当てられ、あたかも複数のプロセスが同時に実行されているかのように見せかけることができます。

タイムクォンタム = 4ms

実行可能キュー: [ P1 | P2 | P3 ]

1. P1が実行開始 (4ms)。
   - 4ms後、P1はまだ終わらない -> キューの末尾へ移動。
   - キュー: [ P2 | P3 | P1 ]

2. P2が実行開始 (4ms)。
   - 途中でP2が完了 -> CPU解放。

3. P3が実行開始 (4ms)。
   ...

具体例で見るラウンドロビン

FCFSの例で使ったプロセス群を、タイムクォンタムを4msとしてRRで実行してみましょう。

プロセス CPUバースト時間 (ms)
P1 24
P2 3
P3 3

ガントチャート (RR, タイムクォンタム=4ms):

|<P1(4)>|<P2(3)>|<P3(3)>|<P1(4)>|<P1(4)>|<P1(4)>|<P1(4)>|<P1(4)>|
0 4 7 10 14 18 22 26 30 (ms)

この場合の各プロセスのターンアラウンドタイムと待ち時間を計算します。

  • P1: 終了時刻は30ms。待ち時間は (30 - 24) = 6ms。しかし、これは単純な計算です。実際には、P1は (4-4) + (10-7) + (14-10) + ... と細切れに待っています。P1は合計で (7-4) + (10-7) = 6ms待たされました。ターンアラウンドタイムは30msです。
  • P2: 終了時刻は7ms。待ち時間は (7 - 3) = 4ms。
  • P3: 終了時刻は10ms。待ち時間は (10 - 3) = 7ms。

平均待ち時間 = (6 + 4 + 7) / 3 = 5.67 ms

FCFSの17msと比較すると大幅に改善されています。しかし、FCFSの順序を最適化したケースの3msよりは長くなっています。これがRRの特性です。RRは平均待ち時間を必ずしも最小化しませんが、応答時間を劇的に改善します。この例では、P2は4ms後、P3は7ms後には応答を開始できています。FCFSの最悪ケースではP3の応答は27ms後でした。

ラウンドロビンの長所とタイムクォンタムのジレンマ

長所

  • 優れた応答時間: 全てのプロセスが短時間内にCPUを割り当てられるため、ユーザーの操作に対する反応が速く、対話型システムに非常に適しています。
  • 飢餓状態が発生しない: どんなに実行時間が長いプロセスでも、少なくともタイムクォンタムごとに一度は実行される機会が保証されているため、飢餓状態に陥りません。公平性が高いと言えます。

短所:タイムクォンタムの大きさ

RRのパフォーマンスは、タイムクォンタムの長さに極めて大きく依存します。この設定は非常にデリケートな問題です。

  • タイムクォンタムが大きすぎる場合: 例えば、タイムクォンタムがシステム内のどのプロセスのバースト時間よりも長い場合、RRは実質的にFCFSと同じ動作になります。プリエンプションがほとんど発生せず、RRの利点である応答性の良さが失われます。
  • タイムクォンタムが小さすぎる場合: 頻繁にプロセスの切り替え(コンテキストスイッチ)が発生します。コンテキストスイッチにはCPU時間を消費するオーバーヘッドが伴います。もしタイムクォンタムが小さすぎると、OSがプロセスの切り替え作業にばかり時間を費やし、本来のプロセスの処理がほとんど進まないという本末転倒な事態に陥ります。これにより、システム全体のスループットが著しく低下します。

タイムクォンタムとコンテキストスイッチオーバーヘッドの関係

^ コンテキストスイッチ

| のオーバーヘッド

高 | /

| /

| /

| /

| /

| /

| /

低 |/_______________________> タイムクォンタムの長さ

小 大

したがって、タイムクォンタムは、コンテキストスイッチのオーバーヘッドが無視できる程度には長く、かつ、ユーザーが待たされていると感じない程度には短く設定する必要があります。一般的に、10msから100msの範囲で設定されることが多いです。

アルゴリズム比較まとめと実践的な考察

ここまで見てきた3つの基本的なアルゴリズムの特性を、評価指標に基づいて一覧表にまとめてみましょう。

評価指標 FCFS (先着順) SJF (最短ジョブ優先) Round Robin (ラウンドロビン)
平均待ち時間 悪い。コンボイ効果により大きく変動。 理論上最適(最短)。 FCFSより良いがSJFよりは悪い。比較的長い。
応答時間 非常に悪い。短いジョブも待たされる。 非プリエンプティブ版は悪い。プリエンプティブ版は良い。 非常に良い。対話的システムに最適。
スループット コンボイ効果により低い傾向。 短いジョブを優先するため高い傾向。 コンテキストスイッチのオーバーヘッドによりSJFよりは低い。
飢餓状態のリスク なし。いつかは実行される。 あり。長いジョブが実行されない可能性がある。 なし。全プロセスに公平に時間が与えられる。
実装の複雑さ 非常に容易。 困難。バースト時間の予測が必要。 比較的容易。タイマー割り込みの管理が必要。

どのアルゴリズムが「最善」か?

この問いに対する単純な答えはありません。「最善」のアルゴリズムは、システムの目的によって異なります。

  • バッチ処理システムで、ユーザーとの対話がなく、スループットを最大化したいのであれば、予測精度が高い場合の非プリエンプティブSJFが有効な選択肢となり得ます。
  • 汎用のデスクトップOSやサーバOSのように、多数のユーザーやプロセスが対話的にシステムを利用し、応答性が最重要視される環境では、ラウンドロビンが基本となります。
  • FCFSは、その単純さから他のアルゴリズムの構成要素として使われることはあっても、単体でメインのスケジューラとして採用されることは現代のOSではほとんどありません。

現代のOSスケジューラへの道

実際のところ、Windows, macOS, Linuxといった現代のオペレーティングシステムは、これら単一のアルゴリズムをそのまま使用しているわけではありません。これらの古典的なアルゴリズムのアイデアを組み合わせ、さらに洗練させた、より複雑なスケジューリング方式を採用しています。

その代表例が多段フィードバックキュー(Multilevel Feedback Queue)です。これは、プロセスをその特性(対話的か、バッチ処理的かなど)に応じて複数のキューに分類し、キューごとに異なるスケジューリングアルゴリズム(例えば、優先度の高いキューではRR、低いキューではFCFS)を適用します。さらに、プロセスの振る舞いに応じてキュー間を移動させることで、SJFの飢餓状態の問題を緩和しつつ、RRの応答性の良さを両立しようとします。

例えば、最初はRRのキューに入れられたプロセスが、何度もタイムクォンタムを使い切る(=CPUバースト時間が長い)ようであれば、優先度の低いFCFSのキューに降格させます。逆に、I/O待ちを頻繁に起こす(=対話的な)プロセスは、高い優先度のキューに昇格させます。このようにして、システムの状況に動的に適応する、より賢いスケジューリングが実現されています。

結論

CPUスケジューリングは、オペレーティングシステムの中核をなす、深く、そして興味深い分野です。今回取り上げたFCFS、SJF、ラウンドロビンは、その広大な世界への入り口に過ぎません。FCFSは「公平」の単純な定義がいかに危険かを示し、SJFは「最適」の達成がいかに困難であるかを教えてくれます。そしてラウンドロビンは、パフォーマンスと応答性の間の絶妙なトレードオフの重要性を浮き彫りにしました。

これらの基本的なアルゴリズムの原理と限界を理解することは、コンピュータシステムが内部でどのように動作しているかを深く知るための重要な一歩です。そしてそれは、より効率的で応答性の高いソフトウェアを開発するための洞察にも繋がるでしょう。CPUスケジューリングの世界は、限られた資源をいかに賢く分配するかという、コンピュータサイエンスにおける永遠の課題への挑戦なのです。

操作系统核心解密 CPU调度算法如何影响性能

在数字世界的每一个角落,从我们口袋里的智能手机到支撑全球互联网的庞大服务器,操作系统(Operating System)都扮演着沉默而关键的指挥官角色。它的核心职责之一,便是管理和分配计算机最宝贵的资源——中央处理器(CPU)时间。这项任务被称为CPU调度。选择不同的CPU调度算法,就像为一座繁忙的城市选择交通管理系统,一个微小的决策差异就可能导致流畅通行与严重拥堵的天壤之别。本文将深入探讨几种基础且极具影响力的CPU调度算法:先来先服务(FCFS)、短作业优先(SJF)以及时间片轮转(RR),揭示它们背后的设计哲学、内在权衡,以及它们如何从根本上决定了我们与计算机交互的体验。

在我们深入算法的细节之前,理解CPU调度的根本目的至关重要。在一个现代多任务操作系统中,通常有数十甚至数百个进程(Programs in execution)同时处于“就绪”状态,它们都渴望获得CPU的执行权。然而,对于一个单核CPU来说,在任何一个瞬间,它只能执行一个进程的指令。CPU调度的本质,就是制定一套规则,决定在众多嗷嗷待哺的进程中,下一个应该由谁来享用CPU。一个优秀的调度算法旨在平衡多个看似矛盾的目标:最大化CPU利用率、提高系统吞吐量、缩短进程的等待和周转时间,并为交互式用户提供快速的响应。这些目标之间充满了张力,而不同的调度算法正是对这些目标进行不同侧重和取舍的产物。

CPU调度的基础:关键概念与评价指标

要客观地比较各种调度算法的优劣,我们必须先建立一个统一的评判标准。这套标准由一系列关键的性能指标构成,它们从不同维度量化了调度算法的效率和公平性。

首先,我们需要理解进程在生命周期中经历的不同状态。一个进程在其生命周期中会不断地在以下几个状态之间转换:

  • 新建(New):进程正在被创建。
  • 就绪(Ready):进程已经准备好运行,正在等待被分配给CPU。所有就绪的进程通常在一个就绪队列中等待。
  • 运行(Running):进程的指令正在被CPU执行。
  • 等待(Waiting):进程正在等待某个事件的发生,例如I/O操作完成或接收到某个信号。
  • 终止(Terminated):进程已完成执行。

调度程序(Scheduler)的主要工作就是从就绪队列中挑选一个进程,并把CPU的使用权交给它。这个过程的效率和公平性,可以通过以下几个核心指标来衡量:

      +-------+       I/O or Event Wait       +---------+
New ->| Ready |------------------------------>| Waiting |
      +-------+       <---------------------- +---------+
         ^  |         I/O or Event Complete       ^
         |  |                                     |
    Dispatch|  |Preempt                            |
         |  v                                     |
      +---------+                                 |
      | Running |---------------------------------+
      +---------+
          |
          v
      +------------+
      | Terminated |
      +------------+

上图描绘了进程在其生命周期中的状态转换图。CPU调度主要关注从“就绪”状态到“运行”状态的转换。

  • CPU利用率(CPU Utilization):衡量CPU在总时间里处于“忙碌”状态(即执行用户进程或操作系统内核代码)的百分比。理想情况下,我们希望CPU利用率尽可能接近100%,但这在现实中很难达到,因为总会有一些空闲时间。
  • 吞吐量(Throughput):单位时间内完成的进程数量。这个指标衡量了系统的“生产力”。对于批处理系统(Batch Systems),这是一个非常重要的指标。
  • 周转时间(Turnaround Time):指一个进程从提交到完成所花费的总时间。它包含了进程在就绪队列中等待、在CPU上运行、以及进行I/O操作的所有时间。即: 周转时间 = 完成时间 - 提交时间
  • 等待时间(Waiting Time):指进程在就绪队列中等待被分配到CPU所花费的时间总和。这个指标直接反映了调度算法的效率,因为它只计算了“无意义”的等待时间,不包括进程实际运行或进行I/O的时间。
  • 响应时间(Response Time):对于交互式系统(Interactive Systems)而言,这是一个至关重要的指标。它指从用户提交一个请求到系统首次产生响应所花费的时间。例如,你点击一个图标,到应用程序窗口出现,这段时间就是响应时间。它不要求任务完成,只要求开始有反馈。

理解了这些指标,我们就有了一把尺子,可以去度量接下来要讨论的各种调度算法了。需要强调的是,这些指标之间往往存在着此消彼长的关系。例如,过分追求低响应时间可能会增加上下文切换的开销,从而降低系统的总吞吐量。因此,评估一个调度算法,必须结合具体的应用场景和系统目标。

一、先来先服务(First-Come, First-Served, FCFS):简单之下的隐患

FCFS是最直观、最简单的CPU调度算法。它的规则就像我们在银行排队一样:谁先到达就绪队列,谁就先获得CPU。一旦某个进程获得了CPU,它就会一直运行,直到它自愿放弃(例如,执行完成或请求I/O),或者被阻塞。因此,FCFS是一种非抢占式(Non-preemptive)的调度算法。

让我们通过一个具体的例子来感受FCFS的运作方式。假设有三个进程P1, P2, P3几乎同时到达,它们的CPU执行时间(CPU Burst Time)如下:

进程信息
进程 到达时间 CPU执行时间 (ms)
P1 0 24
P2 0 3
P3 0 3

如果进程按照P1, P2, P3的顺序到达,那么CPU的执行序列(甘特图)如下所示:

Gantt Chart (P1, P2, P3 order)
|------------------------|---|---|
0                       24  27  30
        P1               P2  P3

现在我们来计算各个进程的等待时间:

  • P1的等待时间 = 0 ms (它立刻获得了CPU)
  • P2的等待时间 = 24 ms (它必须等待P1完成)
  • P3的等待时间 = 27 ms (它必须等待P1和P2都完成)

因此,平均等待时间 = (0 + 24 + 27) / 3 = 17 ms。

护航效应(Convoy Effect)

现在,让我们看看如果这三个进程的到达顺序稍有不同,情况会发生怎样的戏剧性变化。假设它们的到达顺序是P2, P3, P1。

Gantt Chart (P2, P3, P1 order)
|---|---|------------------------|
0   3   6                       30
 P2  P3           P1

在这种情况下,等待时间变为:

  • P2的等待时间 = 0 ms
  • P3的等待时间 = 3 ms
  • P1的等待时间 = 6 ms

平均等待时间 = (0 + 3 + 6) / 3 = 3 ms。

对比两次结果,17ms与3ms,差异是惊人的!这暴露了FCFS算法最致命的弱点——护航效应。当一个CPU执行时间很长的进程(“重型卡车”)先于许多CPU执行时间很短的进程(“小轿车”)到达时,这些短进程将被迫长时间排队等待,即使它们只需要CPU片刻。这就像在一条单行道上,一辆缓慢的卡车堵住了一长串急于通行的小汽车。这种情况会极大地拉高系统的平均等待时间和周转时间,并严重影响系统的响应性。

护航效应在CPU密集型进程和I/O密集型进程混合的系统中尤为突出。一个CPU密集型进程会长时间占用CPU,导致许多需要频繁进行短暂CPU计算和大量I/O操作的I/O密集型进程无谓地等待,使得I/O设备也处于空闲状态,从而同时降低了CPU和I/O设备的利用率。

尽管FCFS实现简单,代码开销极小,但在现代多任务、交互式操作系统中,由于其非抢占性和严重的护航效应,它几乎不被单独用作主要的CPU调度算法。然而,它的思想——公平地按到达顺序处理——在其他更复杂的算法中,例如在多级队列调度的某个队列内部,仍然可能被采用。

二、短作业优先(Shortest-Job-First, SJF):理论上的最优与现实的困境

为了解决FCFS算法中长作业阻塞短作业的问题,SJF算法应运而生。它的核心思想非常直接:当CPU空闲时,将它分配给就绪队列中下一次CPU执行时间最短的那个进程。这个“作业”在这里指的是进程的下一个CPU执行区间(CPU burst),而非整个进程的执行时间。

SJF算法有两种变体:

  1. 非抢占式SJF:一旦一个进程获得了CPU,它就会一直运行,直到其当前的CPU执行区间结束。在此期间,即使有更短的作业到达就绪队列,也不能中断当前正在运行的进程。
  2. 抢占式SJF:也被称为最短剩余时间优先(Shortest-Remaining-Time-First, SRTF)算法。如果一个新到达的进程,其需要的CPU时间比当前正在运行的进程剩余的CPU时间还要短,那么调度程序会中断当前进程(即抢占),将CPU分配给这个新来的、更短的进程。

让我们用一个例子来对比这两种模式。假设有以下四个进程:

进程信息 (SJF/SRTF 示例)
进程 到达时间 CPU执行时间 (ms)
P1 0 8
P2 1 4
P3 2 9
P4 3 5

非抢占式SJF的调度过程:

  • 时刻0:只有P1到达,P1开始运行。由于是非抢占式,P1将一直运行8ms。
  • 时刻1:P2到达就绪队列。
  • 时刻2:P3到达就绪队列。
  • 时刻3:P4到达就绪队列。
  • 时刻8:P1完成。此时就绪队列中有P2(4ms), P3(9ms), P4(5ms)。调度程序选择最短的P2运行。
  • 时刻12:P2完成。就绪队列中有P3(9ms), P4(5ms)。选择最短的P4运行。
  • 时刻17:P4完成。只剩下P3,P3开始运行。
  • 时刻26:P3完成。

甘特图如下:

|--------|----|-----|---------|
0        8    12    17        26
    P1      P2    P4      P3

平均等待时间 = ((8-1) + (12-3) + (17-2) - 8) ... 不,让我们这样计算等待时间: 等待时间 = 开始执行时间 - 到达时间

  • P1 等待时间 = 0 - 0 = 0
  • P2 等待时间 = 8 - 1 = 7
  • P3 等待时间 = 17 - 2 = 15
  • P4 等待时间 = 12 - 3 = 9

平均等待时间 = (0 + 7 + 15 + 9) / 4 = 31 / 4 = 7.75 ms。

抢占式SJF (SRTF) 的调度过程:

  • 时刻0:P1到达,开始运行。剩余时间为8ms。
  • 时刻1:P2到达,其执行时间为4ms。此时P1的剩余时间为7ms (8-1)。因为 4 < 7,所以P1被抢占,P2开始运行。P1回到就绪队列。
  • 时刻2:P3到达,其执行时间为9ms。此时P2的剩余时间为3ms (4-1)。因为 9 > 3,P2继续运行。
  • 时刻3:P4到达,其执行时间为5ms。此时P2的剩余时间为2ms (3-1)。因为 5 > 2,P2继续运行。
  • 时刻5:P2完成。此时就绪队列中有P1(剩余7ms), P3(9ms), P4(5ms)。选择剩余时间最短的P4运行。
  • 时刻10:P4完成。就绪队列中有P1(剩余7ms), P3(9ms)。选择剩余时间最短的P1运行。
  • 时刻17:P1完成。只剩下P3,P3开始运行。
  • 时刻26:P3完成。

甘特图如下:

|-|----|-----|-------|---------|
0 1    5     10      17        26
P1  P2    P4      P1        P3

计算等待时间: 等待时间 = (完成时间 - 到达时间) - 执行时间

  • P1 等待时间 = (17 - 0) - 8 = 9
  • P2 等待时间 = (5 - 1) - 4 = 0
  • P3 等待时间 = (26 - 2) - 9 = 15
  • P4 等待时间 = (10 - 3) - 5 = 2

平均等待时间 = (9 + 0 + 15 + 2) / 4 = 26 / 4 = 6.5 ms。

通过对比,我们可以清晰地看到,抢占式的SRTF算法获得了更低的平均等待时间。事实上,可以从数学上证明,SJF(特别是SRTF)算法对于给定的进程集合,能够获得最小的平均等待时间。这使它在理论上成为“最优”的调度算法。

理论与现实的鸿沟:预测未来

SJF算法的美好只存在于理论中。它最大的、也是致命的挑战在于:无法预知一个进程下一个CPU执行区间的确切长度。操作系统不是先知,它无法在进程运行前就知道它会运行多久。这就像一个急诊室的医生,他无法准确知道下一个病人需要抢救1分钟还是1小时。

在实际应用中,只能通过历史数据来“预测”未来。一种常用的方法是指数平均法(Exponential Averaging)。假设我们用 τn+1 表示对第 n+1 个CPU执行区间的预测值,用 tn 表示第 n 个CPU执行区间的实际值。预测公式如下:

τn+1 = α * tn + (1 - α) * τn

其中,α 是一个权重因子 (0 ≤ α ≤ 1),它决定了历史数据和最近一次实际值在预测中所占的比重。

  • 如果 α = 0,那么 τn+1 = τn,预测值将永远不变,完全忽略最近的实际表现。
  • 如果 α = 1,那么 τn+1 = tn,预测值完全由最近一次的实际值决定,历史数据被完全抛弃。
  • 通常,α 会被设置为 0.5,这样近期和远期的历史数据就能被平衡地考虑进来。

尽管指数平均法提供了一种可行的近似方案,但预测终究是预测,总会存在误差。这种不确定性使得SJF在现实系统中的实现变得非常困难且效果难以保证。任何预测失误都可能导致其性能偏离理论最优值。

长作业的饥饿问题

SJF算法的另一个严重问题是饥饿(Starvation)。如果系统中持续不断地有短作业到达,那么一个执行时间较长的作业可能永远也得不到CPU。它会一直在就绪队列中等待,眼睁睁地看着那些比它晚到但“更短”的作业一个个插队到它前面执行。在极端情况下,长作业可能会被“饿死”。这个问题在SRTF中尤为明显。为了解决饥饿问题,通常会引入“老化”(Aging)机制,即一个进程在就绪队列中等待的时间越长,它的优先级就越高,最终总有机会被调度。

三、时间片轮转(Round-Robin, RR):为交互性而生的公平主义

与SJF追求最短等待时间不同,时间片轮转(RR)算法的设计核心是公平性响应时间。它是专门为分时系统和交互式系统设计的,也是现代操作系统中最常用、最重要的调度算法之一。RR算法可以被看作是FCFS的抢占式版本。

RR的规则很简单:

  1. 将所有就绪进程组织成一个FIFO队列(即FCFS队列)。
  2. 调度程序从队列头部取出一个进程,让它在CPU上运行。
  3. 但这个进程的运行时间是有限制的,这个限制被称为一个时间片(Time Quantum or Time Slice),通常为10到100毫秒。
  4. 如果在时间片结束前,进程已经完成或因I/O请求而阻塞,它会主动放弃CPU。
  5. 如果时间片用完了,但进程还未结束,定时器会产生一个中断,操作系统内核会介入,执行一次上下文切换(Context Switch),将该进程放回就绪队列的尾部,然后调度程序从队列头部选择下一个进程执行。

这种“轮流坐庄”的方式确保了每个就绪进程都能在一段不太长的时间内获得CPU的响应,从而极大地改善了系统的响应时间。

       +-----------------------------------------+
       |                                         |
       |       Ready Queue (FIFO)                |
       |  +----+   +----+   +----+   +----+       |
       |->| P4 |-->| P1 |-->| P7 |-->| P2 |-------+
       |  +----+   +----+   +----+   +----+       |
       |    ^                                     |
       +----|-------------------------------------+
            | (Process returns after time slice expires)
            |
            v
          +-----+
          | CPU |
          +-----+

上图展示了RR调度的基本模型:一个环形的就绪队列。

时间片大小的艺术

RR算法的性能表现,极度依赖于时间片大小的设定。这是一个非常微妙的权衡:

  • 时间片过大:假设时间片设置得比系统中最长的CPU执行区间还要长,那么每个进程都能在自己的第一个时间片内完成。在这种情况下,定时器中断永远不会发生,RR算法就退化成了FCFS算法。系统的响应时间会变得很差,失去了RR的优势。
  • 时间片过小:假设时间片设置得极小,例如接近一次上下文切换所需的时间。这会使得系统看起来像是每个进程都在一个速度为1/N的独立处理器上运行(N是就绪进程数),响应时间会变得非常好。但是,这会带来巨大的性能开销。

上下文切换的开销(Overhead of Context Switching)是RR算法必须正视的现实。每次切换,操作系统都需要:

  1. 保存当前进程的CPU寄存器状态、程序计数器、内存管理信息等到其进程控制块(PCB)中。
  2. 从就绪队列中选择下一个进程。
  3. 加载下一个进程的PCB信息到CPU的寄存器中。
  4. 刷新内存管理单元(如TLB)。

这个过程本身并不产生任何有价值的用户工作,是纯粹的管理开销。如果时间片为10ms,而上下文切换需要1ms,那么系统就有 1 / (10 + 1) ≈ 9% 的时间被浪费在了上下文切换上。如果时间片缩短到1ms,切换时间还是1ms,那么开销就飙升到了 1 / (1 + 1) = 50%!CPU一半的时间都在做“切换”这个动作,而不是真正地执行程序。这会导致系统总的吞吐量急剧下降。

因此,选择一个合适的时间片大小是关键。经验法则是,时间片应该比80%的CPU执行区间都要长。这样,大部分进程都能在一个时间片内完成它们的CPU工作,从而减少不必要的上下文切换,同时又保证了足够短的时间片,以维持良好的响应性。

RR算法的周转时间

RR算法在响应时间上表现出色,但在平均周转时间上通常比SJF要差。考虑一个简单的例子,三个进程P1, P2, P3都需要10ms的CPU时间,时间片为1ms。在SJF(或FCFS)下,它们的完成时间分别是10, 20, 30ms。但在RR下,它们会交替执行,直到大约30ms时才几乎同时完成。这使得它们的周转时间都很长。RR以牺牲总周转时间为代价,换取了所有进程都能得到快速响应的公平性。

综合比较与现代操作系统的选择

现在,我们可以将这三种基础算法放在一起进行一个全面的比较。

调度算法比较
算法 CPU调度方法 平均周转时间 平均等待时间 响应时间 抢占性 饥饿问题
FCFS 根据到达时间 较长,受长作业影响大 较长,有护航效应 非抢占式
SJF (SRTF) 根据(剩余)CPU执行时间 最优 最优 在非抢占式下较差 可选 (SRTF为抢占式) 可能 (特别是长作业)
RR 时间片轮转 较长 较长 优秀 抢占式

从表格中可以清晰地看出,没有一个算法是完美的。每种算法都是在特定目标下的优化产物:

  • FCFS 追求实现的简单性,适用于不在乎响应时间的批处理系统。
  • SJF 追求最高的效率(最短的平均等待/周转时间),但面临预测未来的难题和饿死长作业的风险。
  • RR 追求交互环境下的公平与快速响应,但以牺牲一定的系统总吞吐量和周转时间为代价。

走向更高级的调度:多级队列

现代通用操作系统,如Windows, Linux, macOS,面对的是一个极其复杂的进程混合环境:既有需要快速响应的前台交互式进程(如文字处理器、浏览器),又有在后台运行、对响应时间不敏感但计算量大的批处理任务(如视频编码、科学计算)。为了应对这种复杂性,它们通常采用更高级的调度策略,其中最常见的就是多级队列调度(Multilevel Queue Scheduling)

这种方法将就绪队列分割成多个独立的队列,每个队列有自己的调度算法。例如:

  • 前台队列(Foreground Queue):存放所有交互式进程。这个队列可以采用RR算法,以保证出色的响应时间。它的优先级通常设置得非常高。
  • 后台队列(Background Queue):存放所有批处理进程。这个队列可以采用FCFS算法,因为它简单且适合处理长作业。它的优先级较低。

调度器首先处理前台队列中的所有进程。只有当前台队列为空时,它才会去处理后台队列中的进程。这种固定的优先级划分解决了不同类型任务的需求,但不够灵活。如果一个任务的性质发生了变化(例如,一个后台任务需要与用户交互),它无法在队列间移动。

终极进化:多级反馈队列调度(Multilevel Feedback Queue Scheduling)

为了增加灵活性,多级反馈队列调度被提了出来。它允许进程在不同队列之间移动,这是它与多级队列调度的根本区别。其设计思想是:

  1. 设置多个就绪队列,每个队列有不同的优先级。优先级越高的队列,分配的时间片通常越短。
  2. 新创建的进程首先进入最高优先级的队列。
  3. 如果一个进程在它的时间片内完成了,它就离开系统。
  4. 如果它在一个队列中用完了时间片但仍未完成,它就会被降级到下一个较低优先级的队列。
  5. 在较低优先级队列中等待时间过长的进程,可能会被升级到较高优先级的队列,以防止饥饿(这是一种“老化”机制的实现)。

这种机制能够非常智能地分离不同类型的进程。一个短的交互式任务会在高优先级队列中用很短的时间片快速完成,获得极佳的响应。而一个CPU密集型的长任务会不断地用完时间片,逐渐“下沉”到低优先级、长时聞片的队列中,不会干扰到前台任务。这种算法是迄今为止最为复杂,但也是最为灵活和通用的一种调度算法,被许多现代操作系统所采纳或借鉴。

现实世界中的调度:以Linux为例

理论是灰色的,而生命之树常青。观察现实世界中的操作系统调度器,能让我们更好地理解这些理论是如何被实践和演进的。以Linux为例,它的调度器经历了多次重大变革。

早期的Linux内核使用的是一种类似于多级反馈队列的调度器。而在2.6.23内核之后,引入了革命性的完全公平调度器(Completely Fair Scheduler, CFS)。CFS的目标不再是追求某个固定的时间片,而是致力于为每个任务提供“公平”的CPU时间。它将理想的、无限快的CPU处理能力公平地分配给每个任务。

CFS的核心思想是维护一个按“虚拟运行时间”(vruntime)排序的红黑树。每个任务都有一个vruntime,它记录了该任务已经获得的、经过加权的CPU时间。CFS每次总是选择红黑树中最左侧的节点——即vruntime最小的那个任务——来运行。当一个任务运行时,它的vruntime会随之增加。这样,那些vruntime较小的(即之前运行得较少的)任务,会自然地被调度器优先选择,从而达到一种动态的、近乎完美的公平。

CFS的设计哲学体现了从“管理时间片”到“管理公平性”的深刻转变,它巧妙地平衡了响应时间与吞吐量,是调度算法理论在现代复杂系统中的一次精彩实践。

结论:没有银弹,只有权衡

从简单的FCFS,到理论最优的SJF,再到为交互而生的RR,直至复杂的现代调度器如CFS,我们看到CPU调度算法的演进史,就是一部在各种相互冲突的目标之间不断寻求最佳平衡的历史。没有任何一种算法可以在所有场景下都表现最优。一个好的调度策略,必然是深入理解系统目标(是服务器的高吞吐量,还是桌面的低延迟,亦或是嵌入式系统的实时性)和工作负载特征之后,做出的明智权衡。

对开发者而言,虽然我们通常不直接编写调度算法,但理解其工作原理能帮助我们写出更“调度器友好”的程序。例如,将一个长任务分解成多个小任务,或者在适当的时候让出CPU(例如通过I/O),都可以帮助操作系统更好地进行调度,从而提升整个系统的性能和响应性。CPU调度是操作系统的心跳,理解它的节律,就是理解现代计算的脉搏。