Skip to content

필사 모드: Linux 스케줄러 완전 가이드 2025: CFS, EEVDF, Real-Time, Deadline — 프로세스는 어떻게 CPU를 나눠 쓰는가

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며: 4코어 서버에서 1000 프로세스가 돌아가는 법

간단한 관찰

`htop`을 실행해 보자. 수백 개의 프로세스가 보인다. 그러나 CPU는 4개, 혹은 8개뿐이다. 어떻게 이 모든 프로세스가 "동시에" 실행되는가?

답: **아무도 실제로 동시에 실행되지 않는다**. Linux 커널은 수십 ms 단위로 CPU를 프로세스들 사이에 **시간 분할**한다. 너무 빠르게 전환되어 사용자에겐 마치 **동시 실행처럼 보인다**.

이 마법의 주인공이 **Linux 스케줄러**다. 언제, 어떤 프로세스에게 CPU를 줄지를 결정하는 커널의 심장.

좋은 스케줄러의 조건

스케줄러는 여러 상충하는 목표를 동시에 만족해야 한다:

1. **공정성(Fairness)**: 모든 프로세스가 합리적 시간을 받음.

2. **응답성(Responsiveness)**: 인터랙티브 작업은 빨리 반응.

3. **처리량(Throughput)**: CPU가 놀지 않음.

4. **에너지 효율**: 배터리 오래.

5. **확장성**: 수백 코어에서 선형 성능.

6. **우선순위 존중**: 중요한 작업 우선.

7. **Real-time 지원**: 데드라인 맞추기.

이 목표들은 때로 충돌한다. 공정성과 응답성, 처리량과 에너지 효율. 스케줄러의 역사는 이 균형을 찾는 긴 여정이다.

이 글에서 다룰 것

1. **스케줄링의 기본**: Time slice, preemption, context switch.

2. **O(1) 스케줄러**: 2.4~2.6 초기.

3. **CFS**: 2007년 혁명.

4. **EEVDF**: 2023년 CFS의 후계자.

5. **Real-time 클래스**: FIFO, RR, Deadline.

6. **Nice와 가중치**: 우선순위 메커니즘.

7. **cgroup과 그룹 스케줄링**.

8. **실전 튜닝과 관찰**.

1. 스케줄링의 기본 개념

Process States

Linux 프로세스는 여러 상태를 가진다:

- **R (Running)**: 현재 CPU에서 실행 중 또는 실행 대기열에 있음.

- **S (Sleeping)**: I/O 대기 등으로 잠자는 중 (interruptible).

- **D (Uninterruptible sleep)**: 디스크 I/O 등으로 잠자는 중. 시그널도 못 깨움.

- **T (Stopped)**: SIGSTOP 등으로 중단됨.

- **Z (Zombie)**: 종료됐지만 부모가 수확 안 함.

스케줄러는 **R 상태 프로세스**들 사이에서 누구를 실행할지 결정한다.

Context Switch

한 프로세스를 중단하고 다른 프로세스로 전환하는 과정:

1. 현재 프로세스의 **레지스터, 스택 포인터 저장** (kernel stack).

2. **Address space 전환** (`mm_struct`, CR3 레지스터).

3. **TLB flush** (필요 시).

4. 새 프로세스의 **레지스터 로드**.

5. 새 프로세스 실행.

**비용**: 수 μs (대략 1~10 μs). 너무 자주 하면 오버헤드 폭발. 너무 드물면 응답성 나쁨.

Time Slice (Quantum)

프로세스가 CPU를 한 번에 잡을 수 있는 최대 시간. 이 시간이 지나면 강제로 양보:

- **너무 짧으면**: 너무 많은 context switch → 오버헤드.

- **너무 길면**: 다른 프로세스가 기다림 → 응답성 나쁨.

전형적 값: 1~10 ms.

Preemption

프로세스가 time slice를 다 쓰기 전에도 **강제로 중단**될 수 있다. 다음 경우:

1. **Time slice 만료**: 정해진 시간 다 씀.

2. **더 높은 우선순위 프로세스가 ready**: 현재 것 중단.

3. **프로세스가 block**: I/O 대기 등.

4. **자발적 양보**: `sched_yield()`.

Linux 2.6부터 **kernel preemption** (커널 코드 실행 중에도 preempt 가능) 지원.

Run Queue

각 CPU는 **run queue**를 가진다. Ready 상태 프로세스들의 리스트:

CPU 0 rq: [P1, P5, P9, P12, ...]

CPU 1 rq: [P2, P6, P10, ...]

CPU 2 rq: [P3, P7, ...]

CPU 3 rq: [P4, P8, ...]

각 CPU가 자기 큐에서 다음 프로세스를 선택. **이는 확장성 핵심**이다 (전역 락 없음).

Load Balancing

프로세스를 CPU들에 **균등 분산**하는 작업. 주기적으로 실행:

- 한 CPU의 큐가 다른 것보다 훨씬 길면 프로세스 이동.

- **NUMA 인식**: 메모리 locality 고려.

- **Cache affinity**: 프로세스를 기존 CPU에 붙이려는 경향 (캐시 히트율).

2. O(1) 스케줄러: 2.6 초기의 답

배경

Linux 2.4의 스케줄러는 O(N)이었다. 프로세스 수가 증가하면 스케줄링 오버헤드도 증가. 수천 프로세스면 병목.

O(1)의 핵심 아이디어

2002년 Ingo Molnar가 **O(1) 스케줄러**를 도입했다. 상수 시간에 다음 프로세스를 선택.

**구조**:

- 각 CPU마다 **140개의 우선순위 큐** (100 real-time + 40 일반).

- **Bitmap**으로 비어 있지 않은 큐 빠르게 추적.

- `__ffs()` (find first set) 명령으로 O(1) 탐색.

인터랙티브 휴리스틱

O(1)의 복잡한 부분: **인터랙티브 프로세스 감지**. "사용자와 상호작용하는 프로세스는 우선순위를 올려주자"는 의도.

- Sleep 시간 측정.

- Sleep이 많으면 인터랙티브로 판단 → bonus.

- 계산 집약적이면 penalty.

**문제**: 이 휴리스틱이 매우 복잡하고 때때로 틀렸다. Ingo Molnar 본인도 만족하지 못했다.

O(1)의 한계

1. **휴리스틱 블랙박스**: 왜 이 프로세스가 저 프로세스보다 우선인지 이해하기 어려움.

2. **코너 케이스**: 특정 워크로드에서 이상한 동작.

3. **Mobile/Interactive**: 데스크톱에선 응답성 문제.

Linux 커뮤니티는 더 우아한 해결책을 찾기 시작했다. 그 답이 **CFS**였다.

3. CFS: Completely Fair Scheduler

등장

2007년 Linux 2.6.23에 **CFS (Completely Fair Scheduler)** 가 도입되었다. 개발자는 **Ingo Molnar**.

철학: "이상적인 공정성"

CFS의 핵심 아이디어:

> **"이상적으로는 모든 ready 프로세스가 동시에 CPU를 N분의 1씩 쓴다."**

N개 프로세스가 있으면 각자 1/N의 CPU를 받아야 한다. 그러나 실제론 한 번에 하나만 실행 가능. 그래서 CFS는 **"가장 덜 실행된 프로세스"** 를 다음에 실행한다.

vruntime: 가상 실행 시간

각 프로세스는 **vruntime** (virtual runtime)을 가진다. 프로세스가 CPU를 사용한 "공정한" 양.

- CPU를 쓰는 동안 vruntime이 증가.

- 가중치(nice)에 따라 증가 속도가 다름.

- **가장 낮은 vruntime 프로세스가 다음에 실행**.

이는 **"모든 프로세스의 vruntime을 동기화"** 하려는 시도와 같다.

Red-Black Tree

CFS는 프로세스들을 **red-black tree**로 정렬한다. 키는 vruntime:

[P5: vr=100]

/ \

[P3: vr=50] [P8: vr=150]

/ \ / \

[P1: vr=25] [P4: vr=75] [P7: vr=125] [P9: vr=200]

- **삽입/삭제**: O(log N).

- **최솟값 찾기**: O(log N) (또는 캐시된 leftmost 노드로 O(1)).

- **다음 실행**: 가장 왼쪽 노드.

Nice와 가중치

Unix의 **nice** 값 (-20 ~ +19):

- **낮음 (-20)**: 높은 우선순위. 더 많은 CPU.

- **높음 (+19)**: 낮은 우선순위. 적은 CPU.

CFS는 nice를 **가중치**로 변환한다:

nice 0 → weight 1024 (기준)

nice -1 → weight 1277 (~1.25배)

nice +1 → weight 820 (~0.8배)

nice -20 → weight 88761 (~87배)

nice +19 → weight 15 (~0.014배)

각 단계마다 약 1.25배씩 변한다. 10단계 차이는 ~10배 차이.

가중치 적용

vruntime 증가 속도가 가중치에 반비례:

실제 CPU 시간 Δt 사용 시

vruntime 증가 = Δt × (NICE_0_LOAD / weight)

- Nice 0: `vruntime += Δt`.

- Nice -5: 가중치 높음 → vruntime 덜 증가 → 더 자주 선택됨.

- Nice +10: 가중치 낮음 → vruntime 많이 증가 → 덜 선택됨.

결과: **CPU 시간이 가중치에 비례**해서 분배된다. 이것이 "공정한" 분배다.

예시 계산

nice 0 프로세스 3개가 있다면:

- 각자 동일한 weight 1024.

- 총 weight: 3072.

- 각자 몫: 1024/3072 = **1/3**.

nice 0과 nice -5 (weight 약 3906) 프로세스가 있으면:

- 총 weight: 1024 + 3906 = 4930.

- nice 0: 1024/4930 ≈ **21%**.

- nice -5: 3906/4930 ≈ **79%**.

Target Latency와 Minimum Granularity

CFS에는 **목표 레이턴시** 개념이 있다:

- `sched_latency_ns`: 모든 프로세스가 한 번씩 실행되는 목표 주기 (기본 24 ms).

- N개 프로세스면 각자 `24ms / N` 몫.

그러나 프로세스가 많으면 time slice가 너무 작아진다. 그래서:

- `sched_min_granularity_ns`: 최소 time slice (기본 3 ms).

if N ≤ 8: 각자 24/N ms

if N > 8: 각자 3 ms, 총 사이클 3N ms

장점

1. **우아한 알고리즘**: 단일 메커니즘으로 공정성과 우선순위 처리.

2. **예측 가능**: Nice 값과 CPU 배분이 직관적 관계.

3. **휴리스틱 없음**: 인터랙티브 휴리스틱 제거, 단순화.

4. **확장성**: 프로세스 수에 O(log N).

단점과 EEVDF로의 전환

CFS는 2007년부터 2023년까지 16년간 Linux의 기본이었다. 그러나 시간이 지나며 문제가 드러났다:

1. **Latency-sensitive 작업**: 게임, 오디오 등은 평균 공정성보다 **지연 상한**이 중요.

2. **Sleep에 대한 보너스**: Sleep에서 깨어난 프로세스는 vruntime이 낮아 즉시 선점됨 → 복잡한 보정 로직.

3. **WAKE_UP_PREEMPT**: 깨어난 프로세스가 현재 프로세스를 즉시 preempt할지 결정이 까다로움.

CFS의 코드는 점점 복잡해졌고 코너 케이스가 늘어났다. 새로운 접근이 필요했다.

4. EEVDF: 2023년의 새 시대

EEVDF란

**EEVDF (Earliest Eligible Virtual Deadline First)** 는 1995년 Stoica와 Abdel-Wahab이 발표한 알고리즘이다. 2023년 Linux 6.6에서 **CFS를 대체**했다. Peter Zijlstra가 구현.

새로운 아이디어: Deadline

CFS는 "과거의 공정성"에 집중했다 (vruntime은 과거 실행량). EEVDF는 "미래의 deadline"에 집중한다.

각 프로세스는 **virtual deadline**을 가진다:

vdeadline = 최근 ready 시점 + (time_slice / weight)

스케줄러는 **가장 이른 deadline을 가진 적격 프로세스**를 선택한다.

Eligible (적격)

"적격"이라는 개념이 EEVDF의 핵심:

- 프로세스가 지금까지 공정한 몫보다 **덜** 받았으면 eligible.

- 더 받았으면 not eligible (잠시 양보).

이는 **과거 공정성**과 **미래 deadline**을 결합한다. "빚이 있는" 프로세스만 deadline 경쟁에 참여.

장점

1. **Latency 제어**: deadline 기반이라 최악 지연 제한.

2. **Time slice 유연성**: 프로세스별로 slice 요청 가능.

3. **단순성**: CFS의 복잡한 휴리스틱 제거.

4. **수학적 증명**: 공정성과 지연이 수학적으로 보장됨.

실전 영향

대부분의 워크로드에선 **사용자가 차이를 느끼지 못한다**. EEVDF는 CFS와 거의 같은 동작을 하되 코너 케이스가 개선됐다.

구체적 개선:

- **오디오/비디오 재생** 더 안정.

- **게임 프레임 시간** 일관성 향상.

- **대량 task 환경**에서 공정성 더 정확.

현재 상태

Linux 6.6+ 시스템은 기본적으로 EEVDF를 쓴다. 대부분의 문서와 도구는 아직 "CFS"라고 부르지만 내부는 EEVDF다.

5. Scheduling Classes

Linux는 **여러 스케줄링 클래스**를 계층적으로 운영한다. 우선순위 순서대로:

stop_sched_class (최고: 정지 태스크)

dl_sched_class (SCHED_DEADLINE)

rt_sched_class (SCHED_FIFO, SCHED_RR)

fair_sched_class (CFS / EEVDF - 대부분 프로세스)

idle_sched_class (최저: idle task)

상위 클래스에 runnable 태스크가 있으면 항상 먼저 실행된다.

SCHED_NORMAL (CFS/EEVDF)

일반 프로세스. `nice`로 조정.

SCHED_BATCH

CFS와 유사하지만:

- **선점을 덜 함** (응답성보다 처리량 우선).

- 긴 계산 작업에 적합.

- 에너지 효율이 약간 더 좋음.

chrt -b -p 0 <pid>

SCHED_IDLE

가장 낮은 우선순위. 다른 작업이 없을 때만 실행.

chrt -i -p 0 <pid>

백그라운드 큰 계산 (백업, 인덱싱)에 유용.

6. Real-Time Classes

Real-time 스케줄링은 **엄격한 타이밍 요구**가 있는 경우를 위한 것:

SCHED_FIFO

**"First-In-First-Out"**. 특성:

- **우선순위 레벨 1~99** (99가 가장 높음).

- 높은 우선순위가 **항상 선점**.

- **Time slice 없음**: 스스로 양보하거나 block할 때까지 실행.

- 같은 우선순위끼리는 FIFO 순서.

struct sched_param param = { .sched_priority = 50 };

sched_setscheduler(pid, SCHED_FIFO, &param);

**주의**: 무한 루프 RT 태스크는 **시스템 동결 가능**. RT 태스크는 반드시 I/O로 block하거나 sleep해야 한다.

SCHED_RR (Round Robin)

SCHED_FIFO와 비슷하지만 **time slice 있음**:

- 같은 우선순위끼리 **시간 분할**.

- 기본 time slice: 100 ms.

- 더 안전.

SCHED_DEADLINE

가장 엄격한 real-time. **"이 시점까지 이만큼 CPU 필요"** 를 명시:

struct sched_attr attr = {

.sched_policy = SCHED_DEADLINE,

.sched_runtime = 10 * 1000 * 1000, // 10 ms

.sched_deadline = 30 * 1000 * 1000, // 30 ms

.sched_period = 30 * 1000 * 1000, // 30 ms

};

sched_setattr(0, &attr, 0);

의미: "매 30 ms 주기마다 10 ms의 CPU를 30 ms 이내에 끝내줘".

EDF (Earliest Deadline First) + CBS (Constant Bandwidth Server) 기반. 수학적 보장 제공.

**사용처**: 오디오 처리, 로봇 제어, 산업 자동화.

RT Throttling

RT 태스크가 **영원히 CPU를 잡을 위험** 방지:

/proc/sys/kernel/

sched_rt_runtime_us = 950000 # 950 ms

sched_rt_period_us = 1000000 # 1000 ms

기본적으로 RT는 **매 1초 중 950 ms**만 쓸 수 있음. 나머지 50 ms는 일반 태스크에 양보. 이 덕분에 RT 태스크 버그로 시스템이 멈추지 않는다.

PREEMPT_RT Patch

표준 Linux 커널의 real-time 지원은 제한적이다. **PREEMPT_RT** 패치는 거의 모든 커널 섹션을 preemptable로 만들어 **실시간 성능**을 제공한다.

Linux 6.12 (2024)부터 PREEMPT_RT가 **메인라인에 병합**되었다. 산업용 Linux의 큰 이정표.

용도: 자동차 제어, 공장 자동화, 전자 증권 거래, 로봇, 우주선.

7. Nice와 ionice

nice 명령어

프로세스 CPU 우선순위 조정:

nice 10으로 실행 (낮은 우선순위)

nice -n 10 ./heavy_script.sh

실행 중 프로세스의 nice 변경

renice 15 -p 1234

사용자의 모든 프로세스

renice 10 -u alice

ionice 명령어

**디스크 I/O 우선순위** 조정 (스케줄러가 CPU뿐 아니라 I/O 스케줄링도 관리):

Idle class (최저)

ionice -c 3 -p 1234

Best-effort with priority 7 (낮음)

ionice -c 2 -n 7 -p 1234

- **Class 1**: Real-time.

- **Class 2**: Best-effort (기본, priority 0-7).

- **Class 3**: Idle (다른 I/O 없을 때만).

백업 스크립트에서 흔히 사용:

ionice -c 3 nice -n 19 ./backup.sh

"CPU와 디스크 모두 idle 우선순위로". 다른 작업을 방해하지 않음.

8. cgroup과 그룹 스케줄링

문제

`nice` 값은 **프로세스 단위**. 하지만 복잡한 시나리오:

- 사용자 Alice가 10개 프로세스 실행.

- 사용자 Bob이 1000개 프로세스 실행.

- Bob이 총 CPU의 99%를 가져간다 (많은 프로세스 덕분).

이는 공정하지 않다. **Alice와 Bob에게 50%씩** 주고 싶다.

cgroup의 그룹 스케줄링

cgroup v2의 **cpu controller**가 이를 해결한다. 프로세스를 그룹으로 묶어 **그룹 단위 공정성**:

mkdir /sys/fs/cgroup/alice

echo "500" > /sys/fs/cgroup/alice/cpu.weight

mkdir /sys/fs/cgroup/bob

echo "500" > /sys/fs/cgroup/bob/cpu.weight

Alice와 Bob 프로세스를 각 그룹에 할당

echo <pid_alice_1> > /sys/fs/cgroup/alice/cgroup.procs

echo <pid_bob_1> > /sys/fs/cgroup/bob/cgroup.procs

이제 **그룹 수준에서 CFS**가 적용되고, 그 안에서 다시 프로세스 수준에서 CFS. 이중 공정성.

cpu.max: 절대 제한

최대 50% CPU

echo "50000 100000" > /sys/fs/cgroup/alice/cpu.max

"100 ms 주기 중 50 ms 사용 가능"

cpu.weight.nice: Nice 스타일

일반 nice 값과 매핑

echo "10" > /sys/fs/cgroup/alice/cpu.weight.nice

실전: Kubernetes

Kubernetes의 CPU `requests`와 `limits`가 바로 cgroup으로 구현된다:

resources:

requests:

cpu: "500m" # 최소 500 milli-cores (공정 몫)

limits:

cpu: "1000m" # 최대 1 core

kubelet이 이를 cgroup 파일로 변환:

- `requests.cpu: 500m` → `cpu.weight = ...`

- `limits.cpu: 1000m` → `cpu.max = "100000 100000"`

9. 멀티 CPU 스케줄링

Per-CPU Run Queue

앞서 설명했듯 각 CPU가 자기 run queue를 가진다. 이는 **확장성의 기반**이다. 수백 코어에서도 한 CPU의 스케줄링이 다른 CPU에 영향 없음.

Load Balancing

그러나 때때로 **균형이 필요**하다:

- 한 CPU에 3개, 다른 CPU에 0개면 불공평.

- 주기적으로 커널이 **load balance** 실행.

Load balance 트리거:

- 주기적 (수 ms 단위).

- CPU가 idle이 되면 (`nohz_idle_balance`).

- Exec/fork 시 (새 프로세스 CPU 선택).

- Wake up 시 (깨운 프로세스 CPU 선택).

Scheduling Domain

모든 CPU가 같은 "비용"으로 접근되지 않는다:

- **같은 코어의 하이퍼스레드**: 매우 저렴 (캐시 공유).

- **같은 소켓의 코어**: 저렴 (L3 공유).

- **다른 소켓 (NUMA)**: 비쌈 (원격 메모리 접근).

Linux는 **scheduling domain**을 계층적으로 구성:

NUMA Node 0

├── Socket 0

│ ├── Core 0

│ │ ├── CPU 0 (HT 0)

│ │ └── CPU 1 (HT 1)

│ └── Core 1

│ ├── CPU 2

│ └── CPU 3

└── Socket 1

├── Core 2

│ ...

NUMA Node 1

...

Load balance는 **작은 도메인부터** 먼저 시도. NUMA 노드 간 이동은 최후의 수단.

CPU Affinity

특정 프로세스를 특정 CPU에 **고정**:

PID 1234를 CPU 2, 3에만 실행

taskset -pc 2,3 1234

새 명령어 실행

taskset -c 0-3 ./my_app

효과:

- **캐시 효율**: 항상 같은 L1/L2 cache 재사용.

- **NUMA 최적**: 메모리가 있는 노드의 CPU에 고정.

- **간섭 격리**: 중요한 워크로드를 특정 코어 전용.

Isolcpus: 코어 격리

부팅 시 일부 CPU를 스케줄러로부터 **완전히 분리**:

GRUB에 추가

isolcpus=4,5,6,7 nohz_full=4,5,6,7 rcu_nocbs=4,5,6,7

이 CPU들은:

- 일반 태스크가 자동으로 안 들어감.

- 명시적으로 `taskset`으로 할당한 태스크만 실행.

- `nohz_full`: tick interrupt도 안 들어감 (거의 진짜 bare metal).

**용도**: 초저지연 트레이딩, 고성능 네트워킹(DPDK), 실시간 처리.

10. 성능 관찰과 분석

top, htop

가장 기본. CPU 사용률, 우선순위, nice 값 확인.

top

htop # 더 예쁨

ps로 스케줄링 클래스 확인

ps -eLo pid,tid,class,rtprio,pri,nice,comm

↑ ↑ ↑ ↑

클래스 rt우선순위 실시간우선순위 nice

- `TS`: SCHED_NORMAL (CFS)

- `FF`: SCHED_FIFO

- `RR`: SCHED_RR

- `IDL`: SCHED_IDLE

- `DLN`: SCHED_DEADLINE

/proc/[pid]/sched

각 프로세스의 스케줄러 통계:

cat /proc/1234/sched

nr_switches, nr_voluntary_switches, run_delay, sum_exec_runtime, ...

**run_delay**: CPU 대기 시간. 높으면 스케줄링 부담.

/proc/schedstat

커널 전체 스케줄링 통계. 거의 원시 데이터라 해석 어려움.

perf sched

가장 강력한 도구. 스케줄링 이벤트 트레이스:

몇 초간 기록

sudo perf sched record -- sleep 10

분석

sudo perf sched latency # latency 통계

sudo perf sched map # 시각적 실행 맵

sudo perf sched timehist # 시간 히스토리

**출력 예시**:

Task Runtime ms Switches Avg delay Max delay

sshd:1234 5.2 12 0.102 0.342

chrome:5678 123.4 1543 1.234 8.901

Max delay가 크면 (수 ms+) 응답성 문제.

bcc/bpftrace

커널 내부 이벤트 추적:

스케줄링 지연 히스토그램

sudo runqlat 10

대기 시간 분포

Context switch 빈도

sudo cpudist -p 1234 10

vmstat 1

r b swpd free ...

3 0 0 1GB ... ← r: runnable, b: blocked

`r` (runnable)이 **CPU 수보다 계속 크면** 스케줄링 경합.

11. 실전 튜닝

인터랙티브 응답성 향상

/etc/sysctl.conf

kernel.sched_migration_cost_ns = 5000000

프로세스 이동 최소 비용. 높이면 캐시 locality 유지.

kernel.sched_autogroup_enabled = 1

데스크톱 autogroup (터미널 세션 단위 공정성)

서버 처리량 우선

선점 덜 자주

kernel.sched_min_granularity_ns = 10000000 # 10 ms

kernel.sched_wakeup_granularity_ns = 15000000 # 15 ms

Autogroup 끔

kernel.sched_autogroup_enabled = 0

낮은 지연 (DB, Real-time)

선점 더 자주

kernel.sched_min_granularity_ns = 1000000 # 1 ms

kernel.sched_wakeup_granularity_ns = 2000000 # 2 ms

Isolcpus + RT priority (부팅 옵션)

isolcpus=4-7 nohz_full=4-7

배치 작업: nice 활용

CI 빌드 스크립트

nice -n 19 ionice -c 3 ./build.sh

CPU와 디스크 모두 최저 우선순위. 다른 작업 방해 없음.

Kubernetes: Guaranteed QoS

Pod에 **integer CPU**를 요청하면 **static CPU manager**가 활성화되어 전용 코어가 할당된다:

resources:

requests:

cpu: "2" # 정수만!

memory: "4Gi"

limits:

cpu: "2" # requests == limits

memory: "4Gi"

낮은 지연이 필요한 워크로드에 유용.

12. 흔한 문제와 해결

문제 1: 프로세스가 "가끔" 느림

**증상**: 평균은 빠른데 가끔 수 ms pause.

**원인**:

- 다른 CPU 집약적 프로세스의 경합.

- Context switch 오버헤드.

- Load balancing migration.

**진단**:

perf sched record -- sleep 10

perf sched latency

**해결**:

- `taskset`으로 affinity 설정.

- `isolcpus` 로 코어 격리.

- Real-time priority 상승.

문제 2: RT 태스크가 시스템 동결

**증상**: 시스템이 응답 안 함.

**원인**: SCHED_FIFO 무한 루프.

**해결**:

- `sched_rt_runtime_us` 설정 (기본 950000). 이미 기본적으로 RT throttling.

- RT 코드 검증 (sleep 또는 I/O 포함).

**예방**: RT 태스크는 반드시 **주기적으로 양보** 또는 **I/O block**.

문제 3: Load average 높은데 CPU 여유

**증상**: `uptime` load average가 높지만 `top`의 CPU 사용률은 낮음.

**원인**: **D state (uninterruptible sleep)** 프로세스. Load average에 포함되지만 CPU 안 씀.

**진단**:

ps -eo state,pid,cmd | grep "^D"

**해결**: 디스크 I/O 또는 네트워크 block 원인 찾기. 실제 CPU 스케줄링 문제 아님.

문제 4: 특정 프로세스 starvation

**증상**: 특정 프로세스가 CPU 거의 못 받음.

**원인**:

- Nice 값 높음 (+19).

- 너무 많은 동등한 우선순위 프로세스.

- I/O 병목으로 ready 시간 짧음.

**해결**:

renice -5 -p <pid>

또는

chrt -r -p 50 <pid> # SCHED_RR

문제 5: 컨테이너 CPU throttling

**증상**: Kubernetes Pod이 latency spike.

**원인**: `limits.cpu` 100ms 주기 내 소진 → 나머지 강제 sleep.

**해결**:

- CPU limit 높이거나 제거.

- `requests`만 설정 (공정 몫만 확보).

- Static CPU manager (Guaranteed QoS).

- `cpu.cfs_period_us` 축소.

퀴즈로 복습하기

**A.**

**버린 것: 복잡한 인터랙티브 휴리스틱**

O(1) 스케줄러는 "이 프로세스가 인터랙티브인가?"를 판단하는 복잡한 규칙이 있었다:

- Sleep 시간을 측정해서 많이 자면 보너스.

- 긴 계산 작업은 페널티.

- 특정 threshold, 특정 scaling factor 등 마법 숫자 가득.

결과: 코너 케이스가 많았고, "왜 이 프로세스가 저 프로세스보다 우선인가?"에 답하기 어려웠다.

**얻은 것: 수학적으로 단순한 공정성 모델**

CFS는 하나의 개념만 쓴다: **vruntime**.

- 모든 프로세스의 vruntime을 "동시에" 증가시키려 함.

- 현재 가장 vruntime이 낮은 것(= 가장 덜 실행된 것)을 선택.

- Nice 값은 vruntime 증가 속도만 조절 (가중치).

이 모델의 우아함:

1. **휴리스틱 없음**: 인터랙티브/batch 구분 불필요. Sleep이 많은 프로세스는 자연스럽게 vruntime이 낮아서 우선.

2. **예측 가능**: nice 값이 가중치를 결정하고, CPU 시간은 가중치에 비례.

3. **증명 가능**: 수학적으로 공정성 보장.

**대가**: Red-black tree 사용으로 O(log N) (O(1)에서 열화). 그러나 실전에선 차이 무시할 정도.

**교훈**: 좋은 설계는 복잡한 휴리스틱을 **하나의 우아한 원리**로 대체한다. CFS는 이 철학의 모범 사례다. Ingo Molnar가 O(1)을 본인이 만들었음에도 CFS로 전환한 것은 "설계의 단순성"이 "성능 최적화"보다 장기적으로 가치 있다는 증거다.

**A.** CFS의 철학은 **"과거 공정성"** 이다. vruntime이 가장 낮은 것(= 가장 적게 실행된 것)을 선택한다. 이는 평균적으로 좋지만 **최악 지연 보장이 없다**.

**CFS의 문제**:

- 많은 프로세스가 있으면 한 바퀴 도는 데 오래 걸림.

- 인터랙티브 프로세스가 wake up 할 때 즉시 preempt되지 않을 수 있음.

- Wake-up 보너스 로직이 복잡하고 완벽하지 않음.

**EEVDF의 해결**:

각 프로세스에 **virtual deadline**을 부여한다:

vdeadline = 현재 + (요청한 time slice / 가중치)

스케줄러는 **가장 이른 deadline**을 가진 적격(eligible) 프로세스를 선택한다.

**결과**:

1. **최악 지연 상한**: deadline이 설정되므로 그 시간 내에 반드시 실행됨.

2. **Slice 유연성**: 프로세스가 "나는 2 ms만 필요해"를 요청 가능.

3. **공정성 유지**: eligible 개념이 과거 공정성도 보장.

**실전 효과**:

- **오디오 재생**: 버퍼 언더런 감소.

- **게임**: 프레임 타이밍 일관성.

- **Interactive shell**: 반응 시간 더 균일.

- **VoIP**: 패킷 처리 지연 감소.

**대부분 사용자는 차이를 못 느낀다**. 워크로드가 그리 까다롭지 않으면 CFS와 EEVDF는 비슷하게 동작한다. 그러나 특정 latency-sensitive 워크로드에선 EEVDF가 명확히 낫다.

또 하나 중요한 점: **EEVDF는 수학적 보장**이 있다. CFS는 경험적으로 잘 동작했지만 특정 코너 케이스에서의 보장이 없었다. EEVDF는 1995년 논문에서 공정성과 지연의 상한을 수학적으로 증명했다.

Linux 6.6 (2023년)에서 기본 스케줄러가 되었고, 앞으로 수년간 Linux의 주력이 될 것이다. CFS의 진화는 멈추지 않는다.

**A.** 둘 다 real-time 스케줄링 클래스지만 접근이 완전히 다르다.

**SCHED_FIFO**: **우선순위 기반**

- 1~99의 우선순위 레벨.

- 높은 우선순위가 낮은 것을 **항상 선점**.

- 같은 우선순위끼리는 FIFO 순서.

- **Time slice 없음**: 스스로 양보하거나 block까지 계속 실행.

- 사용: 우선순위 관계가 명확한 경우.

**문제점**:

- 여러 RT 태스크가 있을 때 "누가 몇 % 받아야 하는가"를 우선순위로만 표현하기 어려움.

- 실수로 무한 루프면 낮은 우선순위 태스크가 starvation.

- Rate monotonic analysis 등 수학적 도구 필요.

**SCHED_DEADLINE**: **공약(budget) 기반**

세 가지 값으로 표현:

- **Runtime**: 이만큼 CPU가 필요.

- **Deadline**: 이 시점까지.

- **Period**: 이 주기마다 반복.

예: "매 30 ms 주기마다, 10 ms의 CPU를, 30 ms 이내에 끝내줘".

**이점**:

1. **수학적 보장**: EDF + CBS 알고리즘으로 공정한 스케줄링.

2. **자원 격리**: 한 태스크의 런타임 초과가 다른 태스크에 영향 없음.

3. **스케줄 가능성 검증**: 커널이 admission control 수행 (받을지 거절할지 판단).

4. **Real-time 이론**과 일치: Liu-Layland 같은 고전 이론 직접 적용.

**사용 예**:

- **SCHED_FIFO**: 오래된 real-time 애플리케이션, 단순 제어 루프.

- **SCHED_DEADLINE**: 오디오 처리 (주기적), 비디오 디코딩, 로봇 제어, 산업 자동화.

**주의**:

- SCHED_DEADLINE은 root 권한 필요.

- admission control이 거부하면 설정 실패.

- 다른 SCHED_DEADLINE 태스크와의 관계 고려 필요.

현대 real-time 시스템은 SCHED_DEADLINE을 선호한다. 더 체계적이고 예측 가능하기 때문이다. SCHED_FIFO는 legacy 또는 단순한 경우에만.

**A.** **약 12배**다.

**계산**:

CFS는 nice 값을 가중치로 변환한다. 변환 테이블:

nice -5 → weight ~3121

nice 0 → weight 1024

nice +5 → weight ~335

두 프로세스만 있을 때:

- 총 weight: 3121 + 335 = 3456.

- nice -5 몫: 3121 / 3456 ≈ **90.3%**.

- nice +5 몫: 335 / 3456 ≈ **9.7%**.

**비율**: 90.3 / 9.7 ≈ **9.3배**.

조금 다르게 계산하면 10단계 차이마다 **약 10배**다. (1.25^10 ≈ 9.3)

실제 커널 테이블:

static const int sched_prio_to_weight[40] = {

/* -20 */ 88761, 71755, 56483, 46273, 36291,

/* -15 */ 29154, 23254, 18705, 14949, 11916,

/* -10 */ 9548, 7620, 6100, 4904, 3906,

/* -5 */ 3121, 2501, 1991, 1586, 1277,

/* 0 */ 1024, 820, 655, 526, 423,

/* 5 */ 335, 272, 215, 172, 137,

/* 10 */ 110, 87, 70, 56, 45,

/* 15 */ 36, 29, 23, 18, 15,

};

**Nice 값의 유용성**:

- **nice -5**: 중요한 작업에 약 3배 더 많은 CPU (다른 것과 동시일 때).

- **nice +5**: 백그라운드 작업에 약 1/3.

- **nice +19**: 거의 idle만.

- **nice -20**: 거의 독점.

**실전 권장**:

- 배치/백업: `nice +10~+19`.

- 일반: `nice 0`.

- 대화형 shell: `nice 0` 또는 `-5`.

- 중요한 데몬: `nice -5 ~ -10`.

- **극단 값 (-20, +19)**: 특별한 경우만.

**주의**:

1. **CPU가 충분하면 nice 효과 없음**: 모두가 CPU를 받으니까.

2. **경쟁 상황**에서만 의미: 여러 CPU 집약적 작업 있을 때.

3. **cgroup과 함께**: cgroup이 우선적 메커니즘. Nice는 그 안에서.

**가중치의 기하급수성**: 1.25배씩 증가가 **절대적이 아니라 상대적**이다. nice -5는 nice 0 대비 3배지만, nice 0이 nice +5 대비해서도 3배다. 균등한 인상/하락 경험을 제공.

이것이 "10단계 차이면 10배"라는 Linux 규칙의 수학적 근거다. 엔지니어링의 아름다운 간결성.

**A.** 세 가지 주요 이유가 있다: **캐시 locality, NUMA locality, 간섭 감소**.

**1. 캐시 Locality**

CPU는 L1/L2/L3 캐시를 가진다. L1은 각 코어 전용, L3는 보통 공유.

Core 0 (L1: 32KB, L2: 256KB)

Core 1 (L1: 32KB, L2: 256KB)

Shared L3: 8MB

프로세스가 Core 0에서 실행되면 그 워킹셋이 Core 0의 L1/L2에 로드된다. 같은 프로세스가 다음에 **다른 코어로 이동하면** (스케줄러의 load balancing) 새 코어의 캐시는 비어 있어서 **모든 데이터를 다시 로드**. 캐시 miss 폭발 → 느려짐.

`taskset`으로 CPU를 고정하면 **항상 같은 캐시 사용** → 높은 hit rate → 빠른 실행.

**효과**: 캐시 민감 워크로드 (in-memory DB, 해시테이블 등)에서 **10~30% 향상** 가능.

**2. NUMA Locality**

멀티 소켓 서버에서 메모리는 NUMA 노드별로 분산되어 있다:

Node 0: [CPUs 0-15][RAM 128GB]

Node 1: [CPUs 16-31][RAM 128GB]

Core 0이 Node 0 RAM에 접근하면 로컬 → 빠름.

Core 0이 Node 1 RAM에 접근하면 원격 → 1.5~2배 느림.

프로세스가 Node 0 RAM에 할당되었는데 스케줄러가 Core 16으로 이동시키면 모든 메모리 접근이 원격. 재앙.

`numactl` + `taskset`으로 **프로세스와 메모리를 같은 노드에 고정**:

numactl --cpunodebind=0 --membind=0 ./myapp

**효과**: 큰 메모리 워크로드에서 **최대 2배 향상**.

**3. 간섭 감소**

일반 스케줄러는 "중요한 작업"과 "백그라운드 작업"을 구분하지 않고 섞는다. 중요한 작업이 실행 중 갑자기 선점되면 지연 발생.

**격리 기법**:

1. `isolcpus=4-7`로 CPU 4-7을 **커널 스케줄러로부터 제외**.

2. `taskset -c 4-7 ./important_app`으로 명시적 할당.

3. 이 코어에선 오직 `important_app`만 실행.

**효과**:

- p99 latency 극적 감소.

- 트레이딩 시스템의 마이크로초 수준 지연 일관성.

- DPDK 네트워킹의 라인레이트 달성.

**실전 사용**:

캐시 locality만 필요

taskset -c 0-3 ./my_cache_sensitive_app

NUMA locality 추가

numactl --cpunodebind=0 --membind=0 ./my_memory_app

완전 격리 (부팅 isolcpus=4-7 전제)

taskset -c 4-7 ./my_isolated_app

**함정**:

1. **Over-affinity**: 너무 엄격하게 고정하면 load balancing이 안 돼 다른 CPU가 idle이어도 활용 못 함.

2. **코어 수보다 많은 프로세스**: affinity 있어도 자기들끼리 경합.

3. **측정 필수**: 벤치마크로 확인. 이론만으론 효과 예측 어려움.

**요약**: `taskset`은 "스케줄러를 믿지 않고 내가 직접 관리"하는 도구다. 일반 워크로드엔 불필요하지만, **성능이 극도로 중요한** 시스템(DB, 트레이딩, 실시간 처리)에서 **필수적**이다. Linux 스케줄러가 똑똑해도, 특정 워크로드의 요구사항을 완전히 아는 건 개발자뿐이다.

마치며: 보이지 않는 조율자

핵심 정리

1. **CFS (2007~)**: 공정성 기반 스케줄러. vruntime으로 우아하게.

2. **EEVDF (2023+)**: CFS의 후계자. deadline 기반 latency 제어.

3. **Real-time**: SCHED_FIFO, SCHED_RR, SCHED_DEADLINE.

4. **Nice**: 프로세스 우선순위. 10단계 = 10배.

5. **cgroup**: 그룹 단위 공정성. Kubernetes의 기반.

6. **CPU affinity**: 캐시/NUMA locality 확보.

7. **isolcpus**: 극한 격리. 실시간 워크로드.

왜 이 지식이 가치 있는가

스케줄러는 **눈에 안 보이지만 모든 성능의 근원**이다. 이해 없이는:

- 왜 특정 프로세스가 느린지 설명 못함.

- CPU 제한이 어떻게 작동하는지 모름.

- Kubernetes CPU throttling에 당황.

- Real-time 요구사항 만족 못함.

- 서버 튜닝이 감에 의존.

이 글의 지식은 **수십 년간 수천 명의 커널 개발자가 쌓아 올린 공학**이다. 그것을 이해하면:

- **적절한 nice 값** 선택.

- **언제 taskset 쓸지** 판단.

- **cgroup 설정**을 의식적으로.

- **RT 클래스** 활용 여부 결정.

- **스케줄링 관련 문제** 논리적 디버깅.

마지막 교훈

O(1) → CFS → EEVDF. 20년의 진화는 **"좋은 해결책은 계속 발견된다"** 는 교훈을 준다. 각 단계는 이전 단계의 한계를 직시하고 새로운 원리로 해결했다. Ingo Molnar는 자기가 만든 O(1)을 버리고 CFS를 만들었고, 이제 CFS도 EEVDF에게 자리를 내줬다.

이는 소프트웨어 엔지니어링의 본질이다. **"영원한 최적"은 없다**. 시스템이 진화하고, 워크로드가 변하고, 하드웨어가 바뀌면 답도 바뀐다. 그리고 매번 더 우아하고 더 정확한 해결책이 등장한다.

당신이 다음에 `top`을 볼 때, 잠시 멈추고 생각해 보자: **이 모든 프로세스가 공평하게(?) 실행되도록 수천 번의 스케줄링 결정이 매 초마다 내려지고 있다**. 그 복잡한 연산이 당신의 컴퓨터를 "반응하는 살아있는 기계"로 만든다. 스케줄러는 조용한 조율자이며, 그 존재를 알아챌 때 비로소 당신은 시스템을 **진짜로 이해**하는 것이다.

참고 자료

- [CFS Scheduler Design Documentation](https://www.kernel.org/doc/html/latest/scheduler/sched-design-CFS.html)

- [EEVDF: An earliest eligible virtual deadline first scheduler (LWN)](https://lwn.net/Articles/925371/)

- [Linux Kernel Scheduler Documentation](https://www.kernel.org/doc/html/latest/scheduler/index.html)

- [Stoica & Abdel-Wahab: Earliest Eligible Virtual Deadline First (1995)](https://ieeexplore.ieee.org/document/6593073)

- [Con Kolivas' BFS scheduler debates (historical)](https://kolivas.org/) - 스케줄러 대안 역사

- [Robert Love: Linux Kernel Development, Ch.4 Process Scheduling](https://www.oreilly.com/library/view/linux-kernel-development/9780672329463/)

- [Understanding the Linux Kernel, Ch.7](https://www.oreilly.com/library/view/understanding-the-linux/0596005652/) - 전통적 설명

- [LWN: An EEVDF CPU scheduler for Linux](https://lwn.net/Articles/925371/)

- [The PREEMPT_RT real-time Linux project](https://wiki.linuxfoundation.org/realtime/start)

- [Brendan Gregg: Systems Performance Ch.6 CPUs](https://www.brendangregg.com/systems-performance-2nd-edition-book.html)

현재 단락 (1/541)

`htop`을 실행해 보자. 수백 개의 프로세스가 보인다. 그러나 CPU는 4개, 혹은 8개뿐이다. 어떻게 이 모든 프로세스가 "동시에" 실행되는가?

작성 글자: 0원문 글자: 18,751작성 단락: 0/541