Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
165 changes: 165 additions & 0 deletions 03-OperatingSystem/08-리눅스_스케줄링/cfs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# CFS

## CFS 이전의 시대

- **초기 리눅스 스케줄러 (O(n))**:

- 리눅스 2.4 커널까지 사용된 스케줄러는 실행 대기 중인 모든 프로세스를 연결 리스트로 관리함
- 다음에 실행할 프로세스를 찾기 위해 리스트 전체를 순회하며 우선순위를 계산했기 때문에, 프로세스 수($n$)가 늘어날수록 스케줄링 비용이 선형적으로 증가함
- 이는 대규모 서버 환경에서 심각한 성능 저하를 초래함

- **O(1) 스케줄러 (리눅스 2.6.0 ~ 2.6.22)**
- 각 CPU마다 'Active'와 'Expired'라는 두 개의 우선순위 배열을 두어, 프로세스 선택 시간을 상수 시간($O(1)$)으로 단축함
- 그러나 이 스케줄러는 공정성을 보장하고 대화형 프로세스의 응답성을 높이기 위해 복잡한 휴리스틱에 의존함
- **휴리스틱의 문제**
- 스케줄러는 프로세스의 수면 시간을 측정하여 이것이 대화형 작업인지 배치 작업인지 '추측'하는 방식
- 추측이 빗나갈 경우 대화형 프로세스가 기아 상태에 빠지거나 시스템이 버벅거리는 현상이 발생했음

## CFS (Completely Fair Scheduler)

### 이상적인 멀티태스킹 CPU 모델

- 만약 $N$개의 실행 가능한 프로세스가 있다면, 무한한 병렬성을 가진 하드웨어는 각각의 프로세스에게 정확히 $1/N$의 물리적 CPU 파워를 동시에 제공어야 함
- 100% 성능의 CPU에서 두 개의 프로세스가 실행된다면, 각각 50%의 속도로 끊김 없이 병렬 실행되어야 함
- 하지만 실제 하드웨어는 한 순간에 하나의 프로세스만 실행할 수 있음
- 따라서 CFS는 시간을 잘개 쪼개어 프로세스들을 번갈이 실행함으로써 병렬 실행의 환상을 만들어 냄

### 가상 런타임 (Virtual Runtime, `vruntime`)

**정의**

- 프로세스의 실제 실행 시간을 프로세스의 가중치(우선순위)로 정규화한 값

**계산**

> $$vruntime += \text{actual\_runtime} \times \frac{\text{weight}_{0}}{\text{weight}_{task}}$$

- actual_runtime: 프로세스가 물리적으로 CPU를 점유한 시간(나노초 단위)
- weight_0: 기본 우선순위(Nice 0)를 가진 프로세스의 가중치 (일반적으로 1024)
- weight_task: 해당 프로세스의 실제 가중치

**수식의 의미**

- **Nice 0 (기본 우선순위)**
- 가중치 비율이 1이므로, 실제 시간 1초가 흐르면 vruntime도 1초 증가함
- **Nice -10 (고우선순위, 가중치가 큼)**
- 분모가 커지므로 실제 시간 1초가 흘러도 vruntime은 0.1초 정도만 증가함
- vruntime이 천천히 증가하므로, CPU를 더 많이 사용할 수 있음
- **Nice +10 (저우선순위, 가중치가 작음)**
- 실제 시간 1초 동안 vruntime은 10초나 증가함
- vruntime이 빠르게 증가하므로, CPU를 조금만 사용할 수 있음

### 스케줄링 결정 로직

- 현재 실행 가능한 프로세스 중 `vruntime`이 가장 작은 프로세스를 선택하여 실행함
- `vruntime`이 작다는 것은 이상적인 모델에 비해 CPU를 조금 할당받았다는 의미기에 공정성을 위해 CPU를 할당 받아야 함
- 프로세스가 실행되면 `vruntime`이 증가하며 결국 다른 대기 중인 프로세스의 `vruntime`보다 커지게 되어 CPU를 반납해야 됨
- 이 과정이 반복되면서 모든 프로세스의 `vruntime`은 균형을 맞추게 됨

## 레드-블랙 트리 (Red-Black Tree)

**정의**

- 시간 순서로 정렬된 트리

**사용 이유**

- 스케줄러 환경에서 프로세스가 자발적으로 수면 상태로 가거나 종료되는 빈도가 매우 높아 삽입과 삭제 모두 안정적인 성능이 보장되어야 함
- 자가 균형 이진 탐색 트리인 레드-블랙 트리는 삽입, 삭제, 탐색, 모든 연산에서 최악의 경우에서도 $O(\log n)$의 시간 복잡도를 보장함

**트리 구조**

- **노드 (Node)**
- 실행 가능한 작업을 나타냄
- **키 (Key)**
- `vruntime`을 기준으로 정렬함
- **배치**
- 트리의 왼쪽으로 갈수록 `vruntime`이 작은 작업들이, 오른쪽으로 갈수록 `vruntime`이 큰 작업들이 배치됨

**동작 흐름**

- 스케줄러는 `vruntime`이 최소인 트리의 가장 왼쪽 노드를 선택함
- 선택된 작업이 실행되면 `vruntime`이 증가함
- 작업이 선점되거나 타임 슬라이스를 소진하면, 갱신된 `vruntime`을 가지고 트리에 재삽입됨
- `vruntime`이 커졌으므로, 이 작업은 트리의 오른쪽으로 이동하게 되고 새로운 가장 왼쪽 노드가 다음 실행 대상으로 선택됨

**최적화**

- 리눅스 커널은 트리의 루트뿐만 아니라 가장 왼쪽 노드를 가리키는 포인터를 별도로 캐싱함
- 그로 인해 다음에 실행될 프로세스를 선택하는 연산은 사실상 $O(1)$의 속도로 수행될 수 있음

## 커널 구현 분석

### 주요 구조체

- **task_struct**
- 리눅스에서 프로세스를 표현하는 구조체
- **sched_entity**
- CFS가 스케줄링하는 단위
- `vruntime`, 가중치, 레드-블랙 트리 노드 등이 정의되어 있음
- **csf_rq**
- 각 CPU별 CFS 런큐
- 레드-블랙 트리의 루트와 가장 왼쪽 노드 포인트를 관리함

### 스케줄링 실행 흐름

- **트리거**
- 타이머 인터럽트가 발생하거나, 프로세스가 I/O 대기를 위해 `sleep`을 호출하면 메인 스케줄러 함수인 `__schedule()`이 호출됨
- **스케줄링 클래스 순회**
- `pick_next_task()` 함수는 우선순위가 높은 스케줄링 클래스부터 순회함
- 리얼타임 클래스에 실행할 작업이 있는지 먼저 확인하고, 없다면 CFS 클래스의 함수를 호출합니다.
- **pick_next_task_fair() 호출**
- `cfs_rq`에서 `rb_leftmost`(가장 왼쪽 노드) 포인터를 참조하여 `vruntime`이 가장 작은 `sched_entity`를 가져옴
- 가져온 엔티티가 `task_struct`로 변환되어 반환됨
- **컨텍스트 스위치**
- CPU 레지스터가 저장되고 새로운 프로세스의 실행이 시작됨

### 그룹 스케줄링 (Group Scheduling)

- CFS는 단순히 개별 프로세스뿐만 아니라 사용자나 컨테이너 단위의 공정성을 보장할 수 있음
- 사용자 A가 99개의 프로세스를 실행하고 사용자 B가 1개의 프로세스를 실행할 때, 프로세스 단위로 공정한 경우 사용자 A가 CPU의 99%를 가져감
- 그룹 스케줄링은 사용자 A와 B에게 각각 50% CPU를 할당하고, 사용자 A의 50% 내부에서 99개 프로세스가 다시 자원을 나눔
- `sched_entity`는 계층적 구조를 가질 수 있으며, 런큐 안에 또 다른 런큐가 존재하는 형태가 됨

## CFS의 한계

- **공정성**에 집중하지만, 공정성이 **응답성**을 의미하지는 않음
- 전체 CPU 사용량은 적더라도 필요할 때 즉시 CPU를 받아야 하는 경우에 문제가 발생함
- CFS에서는 우선순위를 높일 수 있지만, 더 많은 CPU 시간을 달라는 요청이지 더 빨리 CPU를 달라는 요청과는 다름
- **Bursty Task**
- 오랫동안 실행되지 않던 프로세스가 깨어나면 `vruntime`이 매우 작아 트리의 가장 왼쪽에 위치함
- 즉시 실행은 보장하지만, 너무 오랫동안 실행권을 독점하여 다른 프로세스의 레이턴시를 급증시킬 수 있음
- CFS는 이를 막기 위해 `vruntime`을 현재 최솟값 근처로 강제 조정하는 휴리스틱을 사용했지만 이는 공정성을 해치고 예측 불가능한 레이턴시 스파이크를 유발함

## EEVDF (Earliest Eligible Virtual Deadline First)의 등장

- **Lag(지연)** 과 **Deadline(마감 시간)** 이라는 두 가지 새로운 개념을 도입하여 CFS의 한계를 수학적으로 해결함

**Lag**

- 각 프로세스가 받아야 할 이상적인 시간과 실제 받은 시간의 차이
- $Lag > 0$
- 받아야 할 시간보다 덜 받음 (CPU를 받을 자격이 있음, Eligible)
- $Lag < 0$
- 몫보다 더 많이 씀 (당분간 실행 자격 없음)

**Virtual Deadline**

- 실행 자격이 있는 프로세스들 중에서 가상 마감 시간이 가장 빠른 프로세스를 선택함
- 프로세스는 자신의 타임 슬라이스 크기를 통해 데드라인을 조절할 수 있음

**Latency Nice**

- 전체 CPU 할당량은 그대로 유지하면서, 스케줄링 빈도를 높여 응답성을 극대화할 수 있음
- **Latency Nice를 낮추면 (급함)**
- 스케줄러가 프로세스에게 할당하는 1회 실행 시간을 짧게 쪼갬
- 할당량이 작으니 금방 처리해야 할 작업으로 인식되어 가상 마감 시간이 앞당겨짐
- 프로세스가 **더 자주, 더 빨리** 선택됨
- **Latency Nice를 높이면 (안 급함)**
- 실행 시간을 길게 뭉쳐서 제공함 (문맥 전환 오버헤드 감소)
- 마감 시간이 뒤로 밀리므로, 스케줄러가 천천리 처리함

## 출처

- https://developer.ibm.com/tutorials/l-completely-fair-scheduler/
- https://opensource.com/article/19/2/fair-scheduling-linux
2 changes: 2 additions & 0 deletions 03-OperatingSystem/08-리눅스_스케줄링/desktop.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[LocalizedFileNames]
08_CFS.pdf=@08_CFS.pdf,0