운영체제 · 5장

CPU 스케줄링 CPU Scheduling

준비 큐에 여러 스레드가 기다린다. 다음에 누구에게 코어를 줄 것인가 — 이 한 번의 결정이 응답성·처리량·공정성·실시간 마감을 모두 좌우한다. 스케줄링은 “단순한 큐 정렬”처럼 보이지만, 그 뒤에는 버스트 예측, 선점의 비용, 멀티코어 캐시 친화성, 그리고 마감을 보장하는 수학이 숨어 있다.
기반: Operating System Concepts 10판 5장 · 대학원 수준으로 확장 · 이론 + 간트차트 + 구현 코드 + 다이어그램

이 장을 마치면

  • CPU–I/O 버스트 사이클로 프로세스 행동을 모델링하고, 선점/비선점 스케줄링이 일어나는 네 가지 상황과 디스패처의 디스패치 지연을 설명할 수 있다.
  • 이용률·처리량·반환시간·대기시간·응답시간의 다섯 기준을 정의하고, 어느 알고리즘이 어느 기준에 유리한지 논증할 수 있다.
  • FCFS·SJF·SRTF·RR·우선순위·MLQ·MLFQ를 간트차트로 직접 그리고, 평균 대기시간을 손으로 계산할 수 있다.
  • PCS vs SCS 스레드 스케줄링과 pthread 경합 범위 API를 이해한다.
  • 다중처리기 스케줄링의 부하 균형(push/pull), 처리기 친화성, NUMA, 이기종(big.LITTLE) 멀티프로세싱을 설명한다.
  • 실시간 스케줄링의 지연(interrupt/dispatch latency)과 Rate-Monotonic·EDF의 스케줄 가능성을 수식과 타임라인으로 분석한다.
  • Linux CFS의 내부 구현(vruntime·레드블랙 트리·nice/weight)과 EEVDF로의 전환, Windows·Solaris 사례를 비교한다.
  • 결정적 모델링·큐잉 모델(Little's law)·시뮬레이션·구현으로 스케줄러를 평가하고, 실무에서 chrt·sched_setaffinity로 튜닝한다.
🏷️ 출처 표시 — 이 페이지 읽는 법

이 자료는 교재(Operating System Concepts 10판 5장) 원문대학원 수준 확장을 더한 것입니다. 절·소절 제목 옆 배지로 출처를 구분했습니다.

📘 OSC 5.x 교재 5장 핵심 내용 ⊕ 교재 외 확장 교재 범위 밖 심화(CFS 내부 구현·EEVDF·스케줄러 역사·실무 튜닝 등)

9절(CFS 내부·EEVDF·튜닝)은 절 전체가 확장입니다. 배지 없는 소절은 교재 본문에 해당합니다. “N.x 절” 참조는 이 페이지 안의 앵커로 이동합니다.

멀티프로그래밍의 목적은 언제나 무언가가 CPU에서 돌게 해 이용률을 최대화하는 것이다. 한 프로세스가 I/O를 기다리며 멈추면, OS는 그 코어를 빼앗아 다른 프로세스에 준다. 누구에게, 언제, 얼마나 줄 것인가 — 이 결정을 내리는 것이 CPU 스케줄러다. 현대 OS에서 스케줄링되는 것은 사실 프로세스가 아니라 커널 수준 스레드지만, 일반 개념을 말할 때는 관례적으로 “프로세스 스케줄링”이라 부른다.

0학습 지도

주제핵심 질문
1기본 개념 — 버스트 사이클·선점·디스패처스케줄링은 정확히 언제 일어나나?
2스케줄링 기준 — 무엇을 최적화하나응답시간과 반환시간은 어떻게 다른가?
3스케줄링 알고리즘 — FCFS부터 MLFQ까지왜 짧은 작업을 먼저 돌리면 평균이 줄까?
4스레드 스케줄링 — PCS vs SCS스레드 우선순위는 누가 정하나?
5다중 처리기 스케줄링한 스레드를 같은 코어에 붙들면 왜 빠른가?
6실시간 CPU 스케줄링 — RMS·EDF마감을 항상 지킬 수 있는지 어떻게 아나?
7OS 사례 — Linux·Windows·Solaris실제 커널은 어떤 정책을 쓰나?
8알고리즘 평가 — 모델링·큐잉·시뮬레이션어느 알고리즘이 더 나은지 어떻게 증명하나?
9CFS 내부·EEVDF·실무 튜닝vruntime은 어떻게 공정성을 만드나?
10오해 정리 · 복습

1기본 개념 — 버스트 사이클·선점·디스패처 📘 OSC 5.1

CPU 스케줄링이 성립하는 근거는 프로세스 실행에 대한 한 가지 관찰이다: 프로세스 실행은 CPU 실행(CPU burst)과 I/O 대기(I/O burst)의 사이클이라는 것이다. 프로세스는 CPU 버스트로 시작해 I/O 버스트, 다시 CPU 버스트… 를 반복하다가 마지막 CPU 버스트가 종료 시스템 콜로 끝난다. 한 프로세스가 I/O를 기다리는 동안 CPU를 놀리지 않고 다른 프로세스에 넘기는 것이 멀티프로그래밍의 본질이다.

1.1 CPU–I/O 버스트 사이클

CPU 버스트의 길이를 대량으로 측정하면 지수(exponential) 또는 하이퍼지수 분포를 따른다: 짧은 버스트가 아주 많고, 긴 버스트는 드물다. I/O 바운드(I/O-bound) 프로그램(예: 대화형 편집기)은 짧은 CPU 버스트가 많고, CPU 바운드(CPU-bound) 프로그램(예: 행렬 곱)은 긴 버스트가 몇 개뿐이다. 이 분포 지식은 스케줄러 설계에 결정적이다 — 짧은 버스트를 우대하면 대화형 응답이 좋아진다.

빈도 (frequency) 버스트 지속시간 (burst duration) 짧은 버스트 多 (I/O-bound) 긴 버스트 少 (CPU-bound)
CPU 버스트 지속시간의 히스토그램. 곡선은 지수/하이퍼지수 형태로, 짧은 버스트가 압도적으로 많다. 이 비대칭이 “짧은 작업 우대” 정책의 근거다.

1.2 CPU 스케줄러와 준비 큐

CPU가 놀게 되면 OS는 준비 큐(ready queue)에서 실행할 프로세스를 하나 골라야 한다. 이 선택을 하는 것이 CPU 스케줄러(단기 스케줄러)다. 중요한 오해 하나: 준비 큐가 반드시 FIFO일 필요는 없다. 알고리즘에 따라 FIFO 큐, 우선순위 큐, 트리(CFS의 레드블랙 트리), 혹은 정렬되지 않은 연결 리스트로 구현될 수 있다. 큐에 들어가는 레코드는 일반적으로 프로세스의 PCB(Process Control Block)다.

1.3 선점형과 비선점형 스케줄링

스케줄링 결정은 다음 네 가지 상황에서만 일어난다.

#상태 전이예시선택 여부
1실행 → 대기(waiting)I/O 요청, wait() 호출선택 없음
2실행 → 준비(ready)인터럽트(타이머 만료 등) 발생선택 있음
3대기 → 준비I/O 완료선택 있음
4종료(terminate)프로세스 종료선택 없음

상황 1·4에서만 스케줄링이 일어나면 비선점형(nonpreemptive) 또는 협력적(cooperative)이라 한다 — 일단 CPU를 받은 프로세스는 자발적으로 놓을 때까지(종료하거나 대기 상태로 전환) CPU를 쥔다. 상황 2·3에서도 스케줄링하면 선점형(preemptive — 실행 중인 프로세스를 강제로 멈추고 CPU를 빼앗음)이다. Windows·macOS·Linux·UNIX 등 거의 모든 현대 OS는 선점형이다.

⚠️ 선점의 대가 — 경쟁 조건과 커널 설계

선점은 공짜가 아니다. 한 프로세스가 공유 데이터를 갱신하던 중 선점되면, 다른 프로세스가 일관성 없는(inconsistent) 상태의 데이터를 읽는 경쟁 조건(race condition)이 생긴다(6장 동기화 주제). 커널 자체도 마찬가지다: 시스템 콜 처리 중 커널 데이터(I/O 큐 등)를 바꾸다 선점되면 혼란이 온다. 비선점 커널은 시스템 콜이 끝나거나 프로세스가 블록될 때까지 문맥 교환을 미뤄 단순하지만, 실시간성에 나쁘다. 선점 커널은 뮤텍스 락으로 공유 커널 구조를 보호한다 — 대부분의 현대 OS는 커널 모드에서도 완전 선점형이다.

1.4 디스패처와 디스패치 지연

스케줄러가 “누구를 돌릴지” 고르면, 실제로 그 프로세스에 코어 제어권을 넘기는 모듈이 디스패처(dispatcher)다. 디스패처가 하는 일은 세 가지다.

디스패처는 모든 문맥 교환마다 호출되므로 최대한 빨라야 한다. 한 프로세스를 멈추고 다른 프로세스를 시작하는 데 걸리는 시간을 디스패치 지연(dispatch latency)이라 한다.

P0 실행 중 상태 저장 → PCB0 상태 복원 ← PCB1 P1 실행 중 디스패치 지연 (저장+복원에 드는 시간)
디스패처의 역할. P0의 상태를 PCB0에 저장하고 PCB1에서 P1의 상태를 복원하는 구간 전체가 디스패치 지연이다. 이 동안 유용한 일은 진행되지 않는다.
심화 자발적 vs 비자발적 문맥 교환

Linux에서 vmstat 1 3은 초당 문맥 교환 수를, /proc/<pid>/status는 프로세스별 통계를 보여준다. 여기서 두 종류가 구분된다. 자발적(voluntary) 문맥 교환은 프로세스가 필요한 자원이 없어(예: I/O 블로킹) 스스로 CPU를 내려놓을 때 일어난다. 비자발적(nonvoluntary) 문맥 교환은 타임 슬라이스가 만료되거나 더 높은 우선순위에 의해 선점되어 CPU를 빼앗길 때 일어난다. nonvoluntary ctxt switches가 비정상적으로 높으면 CPU 경합이 심하다는 신호다.

2스케줄링 기준 — 무엇을 최적화하나 📘 OSC 5.2

알고리즘마다 성질이 다르고, 어떤 기준으로 비교하느냐에 따라 “최선”이 달라진다. 교재가 드는 다섯 가지 기준은 다음과 같다.

기준정의방향측정 도구
CPU 이용률
(utilization)
CPU가 바쁜 시간의 비율최대화 (실전 40~90%)top
처리량
(throughput)
단위 시간당 완료된 프로세스 수최대화
반환시간
(turnaround)
제출~완료까지 총 시간 (대기+실행+I/O)최소화
대기시간
(waiting)
준비 큐에서 기다린 시간의 합최소화
응답시간
(response)
제출~첫 응답이 나오기까지 시간최소화
📐 핵심 구분 — 반환시간 vs 응답시간

반환시간은 작업이 완전히 끝날 때까지의 시간이고, 응답시간첫 반응이 나오기 시작할 때까지의 시간이다. 대화형 시스템에서는 사용자가 결과를 다 받기 전이라도 화면에 무언가가 빨리 뜨는 게 중요하므로 응답시간이 더 적절한 기준이다. 스케줄링 알고리즘은 프로세스가 실제로 실행하거나 I/O하는 시간은 바꿀 수 없고, 오직 준비 큐에서 기다리는 시간(=대기시간)만 바꾼다.

대부분 평균값(average)을 최적화하지만, 때로는 최소/최대값이 더 중요하다. 예를 들어 모든 사용자에게 좋은 서비스를 보장하려면 최대 응답시간을 최소화해야 한다. 또 연구자들은 대화형 시스템에서 평균 응답시간보다 응답시간의 분산(variance)을 줄이는 게 더 중요하다고 본다 — 평균은 빠르지만 들쭉날쭉한 시스템보다, 합리적이고 예측 가능한 시스템이 더 좋게 느껴진다.

💡 이 장의 비교 방법

정확한 비교는 수백 개 버스트를 가진 다수 프로세스를 다뤄야 하지만, 단순화를 위해 이 장의 예제는 프로세스당 CPU 버스트 하나(밀리초 단위)만 본다. 비교 척도는 평균 대기시간이다. 더 정교한 평가는 8절에서 다룬다.

3스케줄링 알고리즘 — FCFS부터 MLFQ까지 📘 OSC 5.3

이 절의 예제는 모두 단일 코어를 가정한다(멀티코어는 5절). 각 알고리즘을 간트차트(Gantt chart — 어느 시각에 어느 프로세스가 CPU를 점유하는지 보여주는 막대 도표)로 그리고 평균 대기시간을 직접 계산한다.

3.1 FCFS (First-Come, First-Served)

가장 단순하다: 먼저 요청한 프로세스가 먼저. FIFO 큐 하나로 구현된다. 다음 세 프로세스가 시각 0에 P1, P2, P3 순으로 도착한다고 하자(버스트: 24, 3, 3).

도착 순서 P1 → P2 → P3 P1 (24) P2 P3 0242730 도착 순서 P2 → P3 → P1 P2 P3 P1 (24) 03630
같은 세 프로세스라도 도착 순서가 다르면 평균 대기시간이 크게 달라진다(17ms vs 3ms). 긴 작업이 앞서면 짧은 작업들이 다 기다린다.
🧮 평균 대기시간 계산

P1→P2→P3: 대기 = P1:0, P2:24, P3:27 → 평균 (0+24+27)/3 = 17ms.
P2→P3→P1: 대기 = P2:0, P3:3, P1:6 → 평균 (0+3+6)/3 = 3ms. 순서만 바꿔도 5배 이상 차이!

FCFS의 두 가지 병폐: ① 호위 효과(convoy effect) — CPU 바운드 프로세스 하나가 CPU를 길게 쥐면, I/O 바운드 프로세스들이 모두 그 뒤에 줄 서고 I/O 장치는 놀게 되어 전체 이용률이 떨어진다. ② 비선점형이라 대화형 시스템에 부적합하다 — 한 프로세스가 CPU를 오래 점유하는 것을 막을 수 없다.

3.2 SJF (Shortest-Job-First)와 버스트 예측

각 프로세스의 다음 CPU 버스트가 가장 짧은 것을 먼저 실행한다(정확히는 최단 다음 버스트). 예제(버스트: P1=6, P2=8, P3=7, P4=3):

P4 (3) P1 (6) P3 (7) P2 (8) 0391624
SJF: 짧은 P4를 먼저 돌린다. 대기 = P1:3, P2:16, P3:9, P4:0 → 평균 (3+16+9+0)/4 = 7ms. 같은 작업을 FCFS로 하면 10.25ms.

SJF는 주어진 집합에 대해 평균 대기시간이 증명상 최적(provably optimal)이다 — 짧은 작업을 긴 작업 앞으로 옮기면, 짧은 작업의 대기 감소가 긴 작업의 대기 증가보다 크기 때문이다. 그러나 치명적 문제: 다음 CPU 버스트의 길이를 미리 알 수 없다. 그래서 과거 버스트로 예측한다. 다음 버스트는 보통 과거 버스트의 지수 평균(exponential average)으로 추정한다.

버스트 예측 — 지수 평균 (수식과 직관)
τ(n+1) = α · t(n) + (1 − α) · τ(n)        0 ≤ α ≤ 1

  t(n)   = n번째 실제 측정 버스트
  τ(n)   = n번째에 대해 예측했던 값
  α      = 최근 측정에 두는 가중치

  α = 0 → τ(n+1) = τ(n)        (최근 무시, 과거가 전부)
  α = 1 → τ(n+1) = t(n)        (과거 무시, 최근이 전부)
  α = ½ → 최근과 과거를 동등 가중 (가장 흔함)

전개하면: τ(n+1) = α·t(n) + (1−α)α·t(n−1) + ... + (1−α)^(n+1)·τ(0)
→ 오래된 항일수록 (1−α)^j 만큼 지수적으로 가중치가 줄어든다.

3.3 SRTF — 선점형 SJF (Shortest-Remaining-Time-First)

SJF는 선점형도 될 수 있다. 실행 중인 프로세스보다 남은 시간(remaining time)이 더 짧은 새 프로세스가 도착하면 선점한다. 이를 SRTF라 한다. 예제(도착시각/버스트: P1=0/8, P2=1/4, P3=2/9, P4=3/5):

P1 P2 (4) P4 (5) P1 (7 남음) P3 (9) 015101726
SRTF: P1이 0에 시작하지만 시각 1에 P2(4ms)가 도착, P1의 남은 7ms보다 짧아 선점된다. 평균 대기 [(10−1)+(1−1)+(17−2)+(5−3)]/4 = 26/4 = 6.5ms (비선점 SJF는 7.75ms).

3.4 라운드로빈 (RR)

FCFS에 선점을 더한 것이다. 타임 퀀텀(time quantum / time slice)이라는 작은 시간 단위(보통 10~100ms)를 정한다. 준비 큐를 원형 큐로 보고, 각 프로세스에 최대 1퀀텀씩 돌아가며 CPU를 준다. 퀀텀 내에 끝나면 자발적으로 반납하고, 넘으면 타이머 인터럽트로 선점되어 큐 꼬리로 간다. 예제(버스트 P1=24, P2=3, P3=3, 퀀텀=4):

P1 P2 P3 P1 P1 P1 P1 P1 047101418222630
RR (퀀텀 4): P1은 4씩 끊겨 다섯 번 돈다. 대기 = P1:6(=10−4), P2:4, P3:7 → 평균 17/3 ≈ 5.66ms. n개·퀀텀 q면 각자 최대 (n−1)×q를 기다린다.

RR 성능은 퀀텀 크기에 크게 좌우된다. 퀀텀이 아주 크면 RR은 FCFS와 같아지고, 아주 작으면(예: 1ms) 문맥 교환이 폭증한다. 경험칙: 문맥 교환 시간이 퀀텀의 10%면 CPU의 10%를 교환에 낭비한다. 또 퀀텀이 너무 작으면 반환시간이 늘고, 80%의 CPU 버스트가 퀀텀보다 짧도록 잡는 것이 좋다.

process time = 10 q=12 → 교환 0회 q=6 → 교환 1회 q=1 → 교환 9회
10단위 작업을 퀀텀별로 나눈 모습. q가 작아질수록 막대(=실행 구간)가 잘게 쪼개지고 그만큼 문맥 교환 오버헤드가 누적된다.

3.5 우선순위 스케줄링 — 기아와 노화

SJF는 “우선순위 = 예측 버스트의 역수”인 우선순위 스케줄링(priority scheduling)의 특수 경우다. 각 프로세스에 우선순위를 주고 가장 높은 것에 CPU를 준다(이 교재는 낮은 숫자 = 높은 우선순위로 가정). 예제(버스트/우선순위: P1=10/3, P2=1/1, P3=2/4, P4=1/5, P5=5/2):

P2 P5 (5) P1 (10) P3 P4 016161819
우선순위 순서 P2(1)→P5(2)→P1(3)→P3(4)→P4(5). 평균 대기시간 8.2ms. 우선순위는 내부(시간·메모리·I/O비율)나 외부(중요도·정책) 기준으로 정한다.
⚠️ 기아(starvation)와 노화(aging)

우선순위 스케줄링의 큰 문제는 무한 블로킹(indefinite blocking) = 기아다 — 높은 우선순위 작업이 끊임없이 들어오면 낮은 우선순위 작업은 영영 못 돈다. (MIT의 IBM 7094를 1973년 끄면서 1967년 제출된 저우선순위 프로세스를 발견했다는 일화가 있다.) 해법은 노화(aging): 오래 기다린 프로세스의 우선순위를 시간이 지날수록 점점 올린다. 또 다른 방법은 같은 우선순위끼리 RR로 돌리는 우선순위+RR 결합이다.

3.6 다단계 큐 (MLQ, Multilevel Queue)

모든 프로세스를 하나의 큐에 두면 최고 우선순위를 찾는 데 O(n) 탐색이 들 수 있다. 실전에서는 우선순위마다 별도 큐를 두는 것이 효율적이다. 더 일반적으로는 프로세스 유형별로 큐를 나눈다: 실시간 → 시스템 → 대화형 → 배치. 각 큐는 자기만의 알고리즘(예: 전경은 RR, 배경은 FCFS)을 쓸 수 있다. 큐 사이의 스케줄링은 보통 고정 우선순위 선점(상위 큐가 비어야 하위 큐 실행)이거나 타임 슬라이스 분배(전경 80%, 배경 20%)다.

최고 우선순위 실시간 프로세스 시스템 프로세스 대화형 프로세스 (RR) 배치 프로세스 (FCFS) 최저 상위 큐가 비어야 하위 실행
다단계 큐: 프로세스가 유형에 따라 영구히 한 큐에 배정된다. 큐 간에는 고정 우선순위, 큐 내에는 큐별 알고리즘을 적용한다.

3.7 다단계 피드백 큐 (MLFQ, Multilevel Feedback Queue)

MLQ는 단순하지만 경직(inflexible)되어 있다 — 프로세스가 큐를 옮기지 못한다. MLFQ는 프로세스가 큐 사이를 이동하게 허용한다. 핵심 아이디어: CPU를 너무 많이 쓰면 하위 큐로 강등한다. 그러면 짧은 버스트의 I/O 바운드·대화형 프로세스가 자연히 상위 큐에 남는다. 또 하위 큐에서 너무 오래 기다린 프로세스는 상위로 승격(노화)해 기아를 막는다.

큐 0 · RR · 퀀텀 8 큐 1 · RR · 퀀텀 16 큐 2 · FCFS 8ms 초과 → 강등 16ms 초과 → 강등 노화 → 승격
MLFQ: 새 프로세스는 큐 0에서 시작한다. 퀀텀 안에 못 끝내면 하위 큐로 강등(긴 작업은 자동으로 큐 2로 가라앉음), 오래 기다리면 노화로 승격. 버스트 ≤8ms 작업이 최고 우선이다.

MLFQ는 다섯 매개변수(큐 개수, 각 큐의 알고리즘, 승격 기준, 강등 기준, 진입 큐 결정)로 정의되어 가장 일반적인 스케줄러다 — 어떤 시스템에도 맞출 수 있다. 그러나 그만큼 가장 복잡하다(매개변수 튜닝이 어렵다).

4스레드 스케줄링 — PCS vs SCS 📘 OSC 5.4

현대 OS가 실제로 스케줄링하는 것은 프로세스가 아니라 커널 수준 스레드다. 사용자 수준 스레드는 스레드 라이브러리가 관리하고 커널은 이를 모른다 — CPU에서 돌려면 결국 커널 스레드에 매핑되어야 한다(LWP를 거칠 수도 있다).

4.1 경합 범위 — PCS와 SCS

PCS (프로세스 내부 경쟁) u1 u2 u3 LWP 라이브러리가 우선순위로 선택 SCS (시스템 전체 경쟁) k1 k2 k3 물리 코어 커널이 전체에서 선택 (Linux·Win)
PCS는 같은 프로세스의 사용자 스레드끼리 LWP를 두고 경쟁(라이브러리가 결정), SCS는 시스템의 모든 커널 스레드가 물리 코어를 두고 경쟁(커널이 결정)한다.

4.2 Pthread 스케줄링 API

POSIX는 스레드 생성 시 경합 범위를 지정하는 두 값을 제공한다: PTHREAD_SCOPE_PROCESS(PCS)와 PTHREAD_SCOPE_SYSTEM(SCS). 단, Linux와 macOS는 PTHREAD_SCOPE_SYSTEM만 허용한다(일대일 모델이므로).

Pthread 경합 범위 지정 — SCS로 설정 (C)
#include <pthread.h>
#include <stdio.h>
#define NUM_THREADS 5

void *runner(void *param) {
    /* ... 작업 ... */
    pthread_exit(0);
}

int main(int argc, char *argv[]) {
    int i, scope;
    pthread_t      tid[NUM_THREADS];
    pthread_attr_t attr;

    pthread_attr_init(&attr);                       /* 기본 속성 */

    /* 현재 경합 범위 조회 */
    if (pthread_attr_getscope(&attr, &scope) != 0)
        fprintf(stderr, "Unable to get scheduling scope\n");
    else if (scope == PTHREAD_SCOPE_PROCESS)
        printf("PTHREAD_SCOPE_PROCESS\n");
    else if (scope == PTHREAD_SCOPE_SYSTEM)
        printf("PTHREAD_SCOPE_SYSTEM\n");

    /* SCS(=PCS 또는 SCS)로 설정 */
    pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

    for (i = 0; i < NUM_THREADS; i++)
        pthread_create(&tid[i], &attr, runner, NULL);
    for (i = 0; i < NUM_THREADS; i++)
        pthread_join(tid[i], NULL);
}

5다중 처리기 스케줄링 📘 OSC 5.5

코어가 여럿이면 부하 공유(load sharing)로 진짜 병렬이 가능해지지만, 스케줄링은 그만큼 복잡해진다. “다중처리기”는 이제 단일 정의가 아니라 멀티코어 CPU · 멀티스레드 코어(SMT) · NUMA 시스템 · 이기종 멀티프로세싱(HMP)을 모두 포괄한다.

5.1 접근법 — 비대칭 vs SMP

(a) 공통 준비 큐 T0 T1 T2 … Tn (공유, 락 필요) core0 core1 core n 경쟁 조건 위험 · 락 경합 병목 (b) 코어별 사설 큐 T0T1 T2T3 T4 core0 core1 core n 경합 없음 · 친화성 공짜 · SMP 표준
공통 큐(a)는 모든 접근에 락이 필요해 병목이 되고, 코어별 사설 큐(b)는 경합이 없고 캐시 친화성을 공짜로 얻는다. 대부분의 현대 SMP는 (b)를 쓴다.

5.2 멀티코어와 멀티스레드 코어 — 메모리 스톨과 CMT

처리기가 메모리에 접근하면 데이터가 올 때까지 상당 시간을 기다리는 메모리 스톨(memory stall)이 생긴다(메모리가 코어보다 훨씬 느리거나 캐시 미스). 이때 코어 시간의 최대 50%가 낭비될 수 있다. 해법: 멀티스레드 코어 — 한 코어에 하드웨어 스레드를 2개 이상 두어, 한 스레드가 스톨하면 다른 스레드로 전환해 실행 유닛을 채운다. 이를 칩 멀티스레딩(CMT) 또는 인텔 용어로 하이퍼스레딩 / SMT(simultaneous multithreading)라 한다.

📝 두 수준의 스케줄링과 멀티스레딩 방식

멀티스레드 멀티코어는 두 수준의 스케줄링이 필요하다: 수준 1은 OS가 어느 소프트웨어 스레드를 어느 하드웨어 스레드(논리 CPU)에 올릴지(이 장의 알고리즘), 수준 2는 코어가 자기 하드웨어 스레드 중 어느 것을 실제로 돌릴지(UltraSPARC T3는 RR, Itanium은 0~7 긴급도(urgency) 값으로 선택). 코어 자원(캐시·파이프라인)은 공유되므로 코어는 한 번에 하나의 하드웨어 스레드만 실행한다. OS가 자원 공유를 인지하면, 두 스레드를 자원을 공유하지 않는 별도 코어에 배치해 더 빠르게 진행시킬 수 있다.
멀티스레딩 방식: 거친(coarse-grained) — 메모리 스톨 같은 긴 지연 이벤트에서 전환(파이프라인 플러시 비용 큼) vs 세밀한(fine-grained) — 명령 경계마다 전환(전환 로직이 하드웨어에 있어 비용 작음).

5.3 부하 균형 — push와 pull 이주

코어별 사설 큐를 쓰면 작업량이 불균등해질 수 있어 부하 균형(load balancing)이 필요하다(공통 큐면 불필요 — 노는 코어가 즉시 꺼내가므로). 두 가지 접근이 있다.

둘은 배타적이지 않고 함께 쓰인다(Linux CFS, FreeBSD ULE 모두 둘 다 구현). “균형 잡힌 부하”의 의미도 다양하다 — 큐별 스레드 수가 같은 것, 우선순위 분포가 같은 것, 혹은 CFS처럼 부하(우선순위+CPU 이용률)의 합이 같은 것.

과부하 코어 A T1T2T3T4 한가한 코어 B (거의 빔) push pull push: A가 B로 밀어냄 pull: B가 A에서 끌어옴 보통 둘 다 병행
부하 균형의 두 메커니즘. push는 과부하 코어가 능동적으로 밀어내고, pull은 노는 코어가 능동적으로 끌어온다. CFS/ULE는 둘을 함께 쓴다.

5.4 처리기 친화성과 NUMA

스레드가 한 코어에서 돌면 그 코어 캐시가 “따뜻해진다(warm cache)”. 다른 코어로 이주하면 첫 코어 캐시를 무효화하고 둘째 코어 캐시를 다시 채워야 하므로 비싸다. 그래서 OS는 스레드를 같은 코어에 붙들려 한다 — 이것이 처리기 친화성(processor affinity)이다. 코어별 사설 큐는 친화성을 공짜로 제공한다(항상 같은 코어에 스케줄되므로).

NUMA(Non-Uniform Memory Access)에서는 각 CPU가 로컬 메모리에 빠르게, 원격 메모리에 느리게 접근한다. OS의 스케줄러와 메모리 배치 알고리즘이 NUMA-aware하면, 스레드를 올린 CPU에 가장 가까운 메모리를 할당해 최선의 접근 속도를 준다.

CPU 0 CPU 1 로컬 메모리 0 로컬 메모리 1 빠름 빠름 느림 (원격) interconnect
NUMA: 같은 물리 주소 공간을 공유하지만 CPU는 로컬 메모리에 빠르게, 인터커넥트 너머 원격 메모리에 느리게 접근한다. 부하 균형과 친화성 사이에는 본질적 긴장이 있다.
🪤 함정 — 부하 균형 ⊥ 친화성

부하 균형과 친화성은 서로 상충한다. 부하를 맞추려 스레드를 옮기면 따뜻한 캐시의 이득이 사라지고, NUMA에서는 더 먼 메모리로 옮겨져 패널티까지 받는다. 그래서 현대 멀티코어 NUMA 스케줄러는 매우 복잡하다 — CFS는 “시스템이 바쁘면 코어 로컬 도메인 너머로는 균형을 잡지 않는다”는 식으로 둘을 절충한다(7절).

5.5 이기종 멀티프로세싱 (HMP) — big.LITTLE

모바일 시스템은 같은 명령 집합을 쓰되 클럭과 전력 관리가 다른 코어를 섞는다 — 이기종 멀티프로세싱(HMP). ARM의 big.LITTLE이 대표로, 고성능 big 코어와 저전력 LITTLE 코어를 결합한다. (비대칭 다중처리와 다르다 — 시스템·사용자 태스크 모두 어느 코어에서나 돌 수 있다.) 목적은 전력 관리: 백그라운드 작업처럼 성능이 덜 필요한 태스크는 LITTLE에, 대화형처럼 짧고 강한 성능이 필요한 태스크는 big에 배치한다. 절전 모드면 big 코어를 끄고 LITTLE만 쓴다. Windows 10도 스레드별 전력 정책 선택으로 HMP를 지원한다.

6실시간 CPU 스케줄링 — RMS·EDF 📘 OSC 5.6

실시간 시스템은 특별한 이슈가 있다. 소프트 실시간(soft real-time)은 임계 작업에 우선권만 보장한다(언제 스케줄될지는 보장 안 함). 하드 실시간(hard real-time)마감(deadline)까지 반드시 서비스해야 한다 — 마감 후의 서비스는 서비스 안 한 것과 같다.

6.1 지연 최소화 — 인터럽트 지연과 디스패치 지연

이벤트 지연(event latency)은 이벤트 발생부터 서비스까지의 시간이다(ABS 브레이크는 3~5ms, 항공기 레이더는 수 초도 허용). 실시간 성능에 영향을 주는 두 지연이 있다.

time 인터럽트 처리 충돌 단계 (conflict) 디스패치 RT 프로세스 디스패치 지연 ① 커널 선점 ② 자원 해제
디스패치 지연은 충돌 단계(커널 선점 + 자원 해제)와 디스패치 단계로 나뉜다. 하드 실시간에서는 보통 수 마이크로초로 측정된다.

6.2 우선순위 기반 스케줄링과 주기 태스크

실시간 OS의 핵심은 선점 + 우선순위 기반 스케줄러다. 그러나 이것만으로는 소프트 실시간만 보장된다. 하드 실시간은 추가 기능이 필요하다. 하드 실시간 분석을 위해 태스크를 주기적(periodic)이라 가정한다: 처리시간 t, 마감 d, 주기 p (관계 0 ≤ t ≤ d ≤ p), 비율(rate) = 1/p. 태스크가 마감을 알리면 입장 제어(admission-control) 알고리즘이 “마감 보장 가능하면 입장, 불가능하면 거부”한다.

6.3 Rate-Monotonic 스케줄링 (RMS)

주기 태스크를 정적(static) 우선순위 + 선점으로 스케줄한다. 주기가 짧을수록 높은 우선순위(CPU를 자주 필요로 하는 태스크 우대). 예: P1(p=50, t=20), P2(p=100, t=35). 이용률 = 20/50 + 35/100 = 0.40 + 0.35 = 0.75. RMS는 P1을 더 높게 둔다.

P1 (p=50, t=20) 우선 · P2 (p=100, t=35) P1 P2 (30) P1 P2 idle P1 020507075100
RMS: P1이 짧은 주기라 우선. P1이 20에 끝나고 P2 시작, 50에 P1이 P2를 선점(P2는 5 남음). P1이 70에 끝나면 P2 재개해 75에 완료 — 둘 다 마감 충족.

RMS는 정적 우선순위 중 최적(이 알고리즘으로 못 짜면 어떤 정적 우선순위로도 못 짠다)이다. 그러나 한계가 있다: 이용률 상한이 있어 항상 100%를 못 쓴다. N개 프로세스의 최악 이용률 경계N(2^(1/N) − 1)로, N→∞면 약 69%로 수렴한다(2개면 ≈83%). 예로 P1(p=50,t=25), P2(p=80,t=35)는 이용률 94%라 RMS로 짜면 P2가 마감(80)을 놓친다(85에 완료).

6.4 Earliest-Deadline-First (EDF)

EDF는 동적(dynamic) 우선순위를 마감에 따라 준다 — 마감이 이를수록 높은 우선순위. 새 프로세스가 실행 가능해지면 마감을 알리고, 우선순위가 재조정된다(RMS는 고정이라는 점이 다르다). 앞서 RMS가 실패한 P1(p=50,t=25), P2(p=80,t=35)를 EDF는 성공시킨다.

EDF: P1 (p=50,t=25) · P2 (p=80,t=35) — 마감 이른 쪽이 우선 P1 (25) P2 (35) P1 (25) P2 P1 P2 0256085100125145
EDF: 시각 50에 RMS는 P1이 P2를 선점하지만, EDF는 P2의 마감(80)이 P1의 다음 마감(100)보다 일러 P2를 계속 돌린다 — 둘 다 마감 충족. 이론상 100% 이용률·주기성 불필요.

EDF는 주기성도, 일정한 버스트도 요구하지 않으며 — 오직 “실행 가능해질 때 마감을 알릴 것”만 요구한다. 이론상 최적(100% 이용률 달성 가능)이지만 실전에서는 문맥 교환·인터럽트 비용 때문에 그 수준은 못 미친다.

6.5 비례 배분 스케줄링과 POSIX 실시간

비례 배분(proportional share): 총 T 셰어를 앱들에 나눠, N 셰어를 받은 앱은 전체의 N/T 시간을 보장받는다. 역시 입장 제어와 함께 동작한다(셰어가 모자라면 거부). POSIX 실시간(POSIX.1b)은 두 클래스를 정의한다: SCHED_FIFO(같은 우선순위 간 타임 슬라이스 없는 FIFO, 종료/블록까지 점유)와 SCHED_RR(같은 우선순위 간 RR). SCHED_OTHER도 있으나 구현은 시스템마다 다르다.

POSIX 실시간 스케줄링 정책 설정 — SCHED_FIFO (C)
#include <pthread.h>
#include <stdio.h>
#define NUM_THREADS 5

int main(int argc, char *argv[]) {
    int i, policy;
    pthread_t      tid[NUM_THREADS];
    pthread_attr_t attr;

    pthread_attr_init(&attr);

    /* 현재 정책 조회 */
    if (pthread_attr_getschedpolicy(&attr, &policy) != 0)
        fprintf(stderr, "Unable to get policy.\n");
    else if (policy == SCHED_OTHER) printf("SCHED_OTHER\n");
    else if (policy == SCHED_RR)    printf("SCHED_RR\n");
    else if (policy == SCHED_FIFO)  printf("SCHED_FIFO\n");

    /* 정책 설정 — FIFO / RR / OTHER */
    if (pthread_attr_setschedpolicy(&attr, SCHED_FIFO) != 0)
        fprintf(stderr, "Unable to set policy.\n");

    for (i = 0; i < NUM_THREADS; i++)
        pthread_create(&tid[i], &attr, runner, NULL);
    for (i = 0; i < NUM_THREADS; i++)
        pthread_join(tid[i], NULL);
}

7운영체제 사례 — Linux · Windows · Solaris 📘 OSC 5.7

7.1 Linux — CFS (Completely Fair Scheduler)

Linux 스케줄러 역사: 2.5 이전의 전통 UNIX 방식 → 2.5의 O(1) 스케줄러(태스크 수와 무관한 상수 시간, SMP 우수했으나 대화형 응답 나쁨) → 2.6.23부터 기본이 된 CFS. Linux는 스케줄링 클래스(scheduling class) 기반이다: (1) 기본 CFS 클래스, (2) 실시간 클래스(POSIX SCHED_FIFO/RR). 스케줄러는 최고 우선순위 클래스의 최고 우선순위 태스크를 고른다.

CFS는 시간 퀀텀을 직접 안 쓰고 각 태스크에 CPU 시간의 비율(proportion)을 준다. 이 비율은 nice 값(−20~+19, 낮을수록 높은 우선순위, 기본 0)으로 정해진다. 또 목표 지연(targeted latency) — 모든 실행 가능 태스크가 최소 한 번은 돌아야 하는 구간 — 을 정의하고 그 안에서 비율로 시간을 배분한다(태스크가 많아지면 목표 지연도 늘어난다).

📝 vruntime — CFS가 우선순위를 “기록”하는 법

CFS는 우선순위를 직접 배정하지 않는다. 대신 각 태스크가 얼마나 돌았는지를 가상 실행 시간(virtual run time, vruntime)으로 기록한다. vruntime은 우선순위 기반 감쇠 인자(decay factor)가 곱해진다: 저우선순위 태스크는 vruntime이 빨리 증가(같은 물리 시간에 더 큰 vruntime), 고우선순위는 천천히. nice=0이면 vruntime = 실제 물리 시간. 스케줄러는 단순히 vruntime이 가장 작은 태스크를 고른다. I/O 바운드 태스크는 짧게 돌고 블록되어 vruntime이 작게 유지되므로 자연히 CPU 바운드보다 우선시된다 → 좋은 대화형 응답.

Linux 실시간은 POSIX(0~99 정적 우선순위), 일반 태스크는 100~139(nice −20→100, +19→139)로 매핑된다 — 전역 우선순위 체계에서 낮은 숫자가 높은 우선순위다. CFS의 부하 균형은 스케줄링 도메인(scheduling domain) 계층(L2 공유 → domain, L3 공유 → NUMA 노드)을 따라 가장 낮은 수준부터 균형을 잡으며, NUMA 노드 간 이주는 심각한 불균형일 때만 한다.

7.2 Windows — 32단계 우선순위 디스패처

Windows는 우선순위 기반·선점형으로 항상 최고 우선순위 스레드를 돌린다(이 부분을 디스패처라 부른다). 32단계: 가변 클래스(1~15)와 실시간 클래스(16~31), 그리고 메모리 관리용 0. 우선순위마다 큐가 있고 높은 데서 낮은 데로 훑는다(없으면 idle 스레드). API의 6개 우선순위 클래스(IDLE~REALTIME)와 상대 우선순위(IDLE~TIME_CRITICAL)의 조합으로 수치 우선순위가 정해진다.

가변 클래스 스레드는 퀀텀이 만료되면 우선순위가 낮아지고(기준 우선순위 밑으로는 안 감 — compute-bound 억제), 대기에서 풀리면 올라간다(키보드 I/O는 크게, 디스크는 중간 — 대화형 응답 강화). 전경(foreground) 프로세스는 퀀텀을 보통 3배로 늘려준다. Windows 7의 UMS(user-mode scheduling)는 커널 개입 없이 사용자 모드에서 스레드를 스케줄(과거 fiber의 한계 극복), ConcRT 같은 라이브러리가 그 위에 얹힌다.

7.3 Solaris — 6개 스케줄링 클래스 → 전역 우선순위

Solaris는 우선순위 기반 스레드 스케줄링으로, 스레드가 6개 클래스 중 하나에 속한다: TS(time sharing, 기본), IA(interactive), RT(real time), SYS(system), FSS(fair share), FP(fixed priority). TS/IA는 MLFQ로 동적 우선순위·가변 퀀텀을 쓴다 — 우선순위와 퀀텀은 역관계(우선순위 높으면 퀀텀 작음). 디스패치 테이블이 “퀀텀 만료 시 새 우선순위(낮춤)”와 “sleep 복귀 시 우선순위(50~59로 부스트)”를 정의해 대화형 응답을 좋게 한다. 각 클래스의 클래스별 우선순위를 전역 우선순위(global priority)로 변환해 최고를 고른다(인터럽트 스레드 160~169가 최상위).

높은 전역 우선순위 (먼저) 인터럽트 스레드 (160–169) 실시간 RT (100–159) 시스템 SYS (60–99) FSS · FPTS (time-share)IA (interactive)(0–59) 낮은 전역 우선순위 (나중)
Solaris: 6개 클래스가 전역 우선순위로 매핑된다. 같은 우선순위면 RR 큐를 쓴다. Solaris 9부터 다대다에서 일대일 모델로 전환했다.

8알고리즘 평가 — 모델링·큐잉·시뮬레이션 📘 OSC 5.8

특정 시스템에 어느 알고리즘을 쓸지 어떻게 고를까? 먼저 기준의 상대적 중요도를 정한다(예: “최대 응답 300ms 제약 하에 CPU 이용률 최대화”). 그다음 네 가지 평가 방법을 쓴다.

8.1 결정적 모델링 (Deterministic Modeling)

특정한 사전 결정 워크로드를 두고 각 알고리즘의 성능을 정확한 숫자로 계산하는 해석적 방법이다. 예제(모두 시각 0 도착, 버스트 P1=10, P2=29, P3=3, P4=7, P5=12)를 FCFS·SJF·RR(q=10)로 비교:

FCFS → 평균 대기 28ms P1 P2 (29) P3 P4 P5 01039424961 SJF (비선점) → 평균 대기 13ms P3 P4 P1 P5 P2 (29) 0310203261 RR (q=10) → 평균 대기 23ms P1 P2 P3 P4 P5 P2 P5 P2 01020233040505261
같은 워크로드를 세 알고리즘으로: SJF(13ms)가 FCFS(28ms)의 절반 미만, RR(23ms)은 그 중간. 결정적 모델링은 정확하지만 그 입력에만 적용된다.

8.2 큐잉 모델과 Little's Law

매일 프로세스가 달라지면 정적 집합이 없다. 대신 CPU/I/O 버스트의 분포(보통 지수 분포)와 도착 분포를 측정해 평균 처리량·이용률·대기시간을 계산한다 — 큐잉 네트워크 분석. 시스템을 “큐를 가진 서버들의 네트워크”로 본다(CPU는 준비 큐를 가진 서버, I/O는 장치 큐를 가진 서버).

Little's Law — 정상 상태의 큐 길이·대기·도착률 관계
           n = λ × W

  n = 평균 큐 길이 (서비스 중인 것 제외)
  λ = 평균 도착률 (예: 초당 7개 프로세스)
  W = 큐에서의 평균 대기시간

직관: 프로세스가 W 시간 기다리는 동안 λ×W 개가 새로 도착한다.
정상 상태(steady state)면 들어오는 수 = 나가는 수 → n = λ·W.

예: λ = 7 프로세스/초, n = 14 → W = n/λ = 14/7 = 2초.
임의의 스케줄링 알고리즘·도착 분포에 모두 성립(범용).

큐잉 분석은 알고리즘 비교에 유용하지만, 다룰 수 있는 알고리즘·분포 클래스가 제한적이고, 수학적 편의를 위해 비현실적 가정을 하므로 결과가 근사에 그칠 수 있다.

8.3 시뮬레이션과 구현

추적 파일실제 실행 기록 시뮬레이션 FCFS 시뮬레이션 SJF 시뮬레이션 RR (q=14) 성능 통계 (FCFS) 성능 통계 (SJF) 성능 통계 (RR)
추적 파일 기반 평가: 동일한 실제 입력 시퀀스를 여러 스케줄러 시뮬레이션에 넣어 공정하게 성능을 비교한다.

9CFS 내부·EEVDF·스케줄러 역사·실무 튜닝 ⊕ 교재 외 확장

교재는 CFS를 “vruntime이 작은 태스크를 고른다” 수준으로 소개한다. 여기서는 그 내부 자료구조와 수학, 그리고 2023년의 후계자 EEVDF, 역사적 맥락, 실무 튜닝을 더 깊이 본다.

9.1 vruntime · nice · weight — 공정성의 산술

CFS의 “공정”은 가중 공정(weighted fair)이다. 각 태스크의 nice 값은 weight로 변환된다(커널 테이블 sched_prio_to_weight[], nice 0 = 1024). vruntime은 실제 실행 시간에 1024/weight를 곱해 누적된다 — 고우선순위(큰 weight)는 vruntime이 천천히 자라 더 자주 선택된다. nice 한 단계 차이는 약 1.25배의 CPU 비율 차이를 만든다.

CFS의 vruntime 누적 — 개념 의사코드 (C 유사)
/* nice → weight: 한 단계마다 약 1.25배. nice 0 → 1024 */
static const int prio_to_weight[40] = {
    /* -20 */ 88761, 71755, 56483, 46273, 36291,
    /* ...  */ /* ... 중략 ... */
    /*   0 */ 1024,  820,   655,   526,   423,
    /* +19 */ 15
};

/* 태스크가 delta_exec(ns)만큼 실제로 돈 뒤 호출 */
void update_vruntime(struct sched_entity *se, u64 delta_exec) {
    /* 가중 보정: weight가 클수록(고우선순위) vruntime이 천천히 증가 */
    se->vruntime += delta_exec * NICE_0_WEIGHT / se->load.weight;
    /*               (= delta_exec * 1024 / weight)               */
}

/* 다음에 돌릴 태스크: 레드블랙 트리의 최좌측(min vruntime) */
struct sched_entity *pick_next(struct cfs_rq *rq) {
    return rq->rb_leftmost;     /* O(1): 캐시된 최좌측 노드 */
}

9.2 레드블랙 트리 — O(log N) 선택과 O(1) 캐시

CFS는 준비 큐를 레드블랙 트리(자가 균형 이진 탐색 트리)로 유지한다. 키는 vruntime. 실행 가능해진 태스크는 트리에 삽입, 블록되면 제거된다. vruntime이 작은(덜 돈) 태스크가 왼쪽에 모이므로, 최좌측 노드 = 다음에 돌릴 태스크다. 트리가 균형이라 탐색은 O(log N)이지만, 커널은 최좌측을 rb_leftmost캐시해 실제 선택은 O(1)이다.

T0 T1 T2 T3 T4 T5 T6 T7 최소 vruntime → 최대 vruntime ← rb_leftmost
CFS 레드블랙 트리: 키는 vruntime. 왼쪽일수록 덜 돈(우선순위 높은) 태스크다. 최좌측 노드(T7)가 다음에 돌 태스크이며 rb_leftmost에 캐시되어 O(1)로 얻는다.

9.3 EEVDF — CFS의 후계자 (6.6+)

2023년 말 Linux 6.6은 CFS를 EEVDF(Earliest Eligible Virtual Deadline First)로 대체했다. CFS의 약점은 지연(latency) 보장이 없다는 것이었다 — vruntime 공정성은 “장기적으로 공평”하지만 “지금 당장 얼마나 빨리 돌지”를 표현하지 못했다. EEVDF는 각 태스크에 가상 마감(virtual deadline)을 주고, 자격(eligible)이 있는 태스크 중 가상 마감이 가장 이른 것을 고른다 — 6절 EDF의 정신을 공정 스케줄러에 이식한 셈이다. lag(받아야 할 시간 대비 실제 받은 시간의 차)으로 자격을 판정하고, 태스크별 sched_attrlatency-nice로 지연 민감도를 표현한다. nice·weight·레드블랙 트리 골격은 그대로 재사용한다.

심화 스케줄러의 역사적 계보

Linux 스케줄러 변천: O(n)(~2.4, 매번 전체 큐 순회) → O(1)(2.6 초기, 활성/만료 배열로 상수 시간이나 대화형 휴리스틱이 복잡) → CFS(2.6.23, vruntime+레드블랙 트리) → EEVDF(6.6). 그 사이 비주류로 Con Kolivas의 BFS(Brain Fuck Scheduler)와 그 후속 MuQSS가 있었다 — 데스크톱 응답성에 초점을 둔 단일 런큐 설계로, 메인라인에는 안 들어갔지만 “대화형 지연이 처리량만큼 중요하다”는 문제의식을 부각해 CFS/EEVDF 개선을 자극했다. 교훈: 스케줄러에는 유일한 정답이 없고, 워크로드(서버 처리량 vs 데스크톱 지연 vs 모바일 전력)가 설계를 가른다.

9.4 실무 튜닝 — chrt · taskset · nice

이론을 실제 시스템에 적용하는 도구들이다.

Linux 스케줄링 실무 튜닝 (shell)
# 1) nice — 일반(CFS/EEVDF) 태스크의 우선순위 비율 조정 (-20~19)
nice -n 10 ./batch_job          # 낮은 우선순위로 시작
renice -n -5 -p 1234            # 실행 중 PID 1234 우선순위 올림

# 2) chrt — 실시간 정책 지정 (SCHED_FIFO / SCHED_RR / SCHED_OTHER)
chrt -f 80 ./audio_engine       # SCHED_FIFO, 우선순위 80 (저지연 오디오)
chrt -r 50 ./control_loop       # SCHED_RR, 우선순위 50
chrt -p 1234                    # PID 1234의 현재 정책/우선순위 조회

# 3) taskset / sched_setaffinity — 하드 친화성 (CPU 핀닝)
taskset -c 2,3 ./latency_app    # CPU 2,3에만 고정 → 따뜻한 캐시 유지
taskset -p 0x1 1234             # PID 1234를 CPU0에 핀

# 4) cgroup v2 cpu 컨트롤러 — 그룹 단위 비례 배분 (컨테이너)
echo 200 > /sys/fs/cgroup/myapp/cpu.weight     # 기본 100의 2배 비율
echo "50000 100000" > .../cpu.max               # 100ms당 50ms 상한(쿼터)
하드 친화성 — sched_setaffinity() 직접 호출 (C, Linux)
#define _GNU_SOURCE
#include <sched.h>
#include <pthread.h>

void pin_to_cpu(int cpu) {
    cpu_set_t set;
    CPU_ZERO(&set);
    CPU_SET(cpu, &set);                          /* cpu 하나만 허용 집합에 */
    /* 현재 스레드(0)를 지정 CPU에만 스케줄되도록 고정 */
    if (sched_setaffinity(0, sizeof(set), &set) != 0)
        perror("sched_setaffinity");
    /* pthread 버전: pthread_setaffinity_np(pthread_self(), ...) */
}
🪤 함정 — 실시간 우선순위로 시스템을 굶기다

SCHED_FIFO는 같은/낮은 우선순위 태스크에 타임 슬라이스를 주지 않는다. 무한 루프를 도는 SCHED_FIFO 스레드는 그 코어를 영원히 독점해 커널 스레드(마이그레이션·RCU)까지 굶긴다 → 시스템 행. 그래서 Linux는 sched_rt_runtime_us(기본 950000/1000000 = RT에 95% 상한)로 안전판을 둔다. 실시간 우선순위는 경계 있는 짧은 작업에만 쓰고, CPU 핀닝과 함께 격리(isolation)하는 것이 정석이다.

10오해 정리 · 한 장 요약 · 복습 📘 OSC 5.9

10.1 흔한 오해 바로잡기

10.2 한 장 정리

🎯 5장 핵심

  • CPU 스케줄링 = 준비 큐에서 하나를 골라 디스패처가 CPU를 준다. 결정은 네 상황(실행→대기/준비, 대기→준비, 종료)에서만 일어나고, 2·3에서도 하면 선점형.
  • 기준 다섯: 이용률·처리량(최대화) / 반환·대기·응답(최소화). 응답시간 ≠ 반환시간(첫 반응 vs 완료).
  • 알고리즘: FCFS(단순·호위효과), SJF/SRTF(평균 최적이나 예측 필요), RR(퀀텀이 관건), 우선순위(기아→노화), MLQ(고정 큐), MLFQ(가장 일반적, 큐 이동).
  • 스레드: PCS(프로세스 내, 라이브러리) vs SCS(시스템 전체, 커널). Linux/Win은 SCS만.
  • 다중처리기: 비대칭 vs SMP(코어별 사설 큐 표준). push/pull 부하균형처리기 친화성·NUMA. 이기종은 big.LITTLE로 전력 관리.
  • 실시간: 인터럽트·디스패치 지연 최소화. RMS(정적, 주기 짧을수록 우선, 경계 ≈69%) vs EDF(동적, 마감 이를수록 우선, 이론상 100%). POSIX SCHED_FIFO/RR.
  • 사례: Linux CFS(vruntime·레드블랙 트리·nice→weight), Windows(32단계 디스패처·전경 부스트), Solaris(6클래스→전역 우선순위).
  • 평가: 결정적 모델링(정확하나 입력 한정) · 큐잉(Little's law n=λW) · 시뮬레이션(추적 파일) · 구현(가장 정확·가장 비쌈).
  • 확장: CFS 내부(weight 산술·O(1) 캐시) → EEVDF(가상 마감으로 지연 보장). 실무는 nice·chrt·taskset·cgroup으로 튜닝.

10.3 복습 — 답을 가리고

Q1. 같은 세 프로세스(버스트 24,3,3)인데 FCFS 평균 대기시간이 17ms에서 3ms로 바뀐다. 무엇이 그 차이를 만드나?

도착(서비스) 순서다. 긴 P1이 앞서면 짧은 둘이 다 기다려 (0+24+27)/3=17ms, 짧은 둘을 앞세우면 (0+3+6)/3=3ms. FCFS는 비선점이라 호위 효과에 취약하다.

Q2. SJF가 평균 대기시간 최적인데도 CPU 스케줄러에 못 쓰는 이유와, 그 대안은?

다음 CPU 버스트의 길이를 알 수 없기 때문. 대안은 과거 버스트의 지수 평균 τ(n+1)=α·t(n)+(1−α)·τ(n)으로 예측해 가장 짧을 것 같은 프로세스를 고르는 것(근사 SJF).

Q3. RR 퀀텀을 1ms로 극단적으로 줄이면 무슨 일이 일어나나?

문맥 교환이 폭증한다. 교환 시간이 퀀텀의 10%면 CPU의 10%를 교환에 낭비하고, 반환시간도 늘어난다. 경험칙은 “80%의 CPU 버스트가 퀀텀보다 짧게” 잡는 것.

Q4. RMS와 EDF의 우선순위 부여 방식 차이를 한 문장씩.

RMS는 정적 — 주기가 짧을수록 높은 우선순위(영구 고정). EDF는 동적 — 마감이 이를수록 높은 우선순위(실행 가능해질 때마다 재조정). 그래서 RMS는 94% 이용률에서 마감을 놓치지만 EDF는 지킨다.

Q5. 처리기 친화성이 왜 성능에 중요하고, 부하 균형과 어떻게 충돌하나?

스레드를 같은 코어에 두면 따뜻한 캐시를 재사용해 메모리 접근이 빠르다(NUMA에선 로컬 메모리 지역성까지). 그러나 부하 균형은 스레드를 다른 코어로 옮겨 이 이득을 무효화하고 NUMA 패널티까지 줄 수 있다 — 본질적 긴장 관계.

Q6. CFS는 우선순위를 어떻게 “기록”하며, 다음 태스크를 어떻게 O(1)로 고르나?

각 태스크의 vruntime을 weight로 보정해 누적한다(고우선순위는 천천히 증가). 준비 큐를 vruntime 키의 레드블랙 트리로 유지하고, 최좌측(최소 vruntime) 노드를 rb_leftmost에 캐시해 선택을 O(1)로 만든다.

Q7. Little's law n=λW로, 초당 7개가 도착하고 큐에 평균 14개가 있으면 평균 대기시간은?

W = n/λ = 14/7 = 2초. 이 공식은 임의의 스케줄링 알고리즘·도착 분포에 성립하는 정상 상태 항등식이다.

Q8. SCHED_FIFO로 무한 루프 스레드를 돌리면 왜 시스템이 멈출 수 있고, Linux는 어떻게 막나?

SCHED_FIFO는 같은/낮은 우선순위에 타임 슬라이스를 안 줘 그 코어를 독점, 커널 스레드까지 굶긴다. Linux는 sched_rt_runtime_us(기본 95% 상한)로 RT가 CPU를 100% 못 쓰게 막아 일반 태스크가 숨 쉴 틈을 보장한다.

10.4 연관 자료 · 더 깊이

🚧 이 코스의 다른 장에 대해

본문의 “N.x 절” 참조(예: 6.2·5.5.4)는 이 페이지 안의 앵커(#s5 등)나 인접 장을 가리킨다. “N장” 참조 — 4장 스레드·6장 동기화 도구 등 — 는 이 운영체제 코스에 순차적으로 추가되는 페이지를 가리킨다. 현재 4장이 공개되어 있고 6장은 준비 중이다. 동기화·경쟁 조건의 상세는 6장이, 캐시·NUMA·메모리 순서의 하드웨어 측면은 위의 메모리 코스 링크가 보강한다.

심화 참고 문헌

Silberschatz·Galvin·Gagne, Operating System Concepts 10e, Ch.5 · Liu & Layland, “Scheduling Algorithms for Multiprogramming in a Hard-Real-Time Environment” (1973, RMS·EDF의 원전) · Molnar, CFS 설계 노트(Documentation/scheduler) · Zijlstra, EEVDF 패치 시리즈(LWN.net 해설) · Stoica et al., “Earliest Eligible Virtual Deadline First” (1996) · man 7 sched · Con Kolivas, BFS/MuQSS 설계 문서.