프로세스(Process) 스케줄러(Scheduler)
Linux 커널 프로세스 스케줄러의 전체 아키텍처를 sched_class 계층 기준으로 추적합니다. CFS/EEVDF 선택 로직, RT/DEADLINE 우선순위(Priority) 규칙, per-CPU 런큐(Runqueue)와 PELT 부하 추적, sched_domain 로드 밸런싱, BPF 기반 sched_ext 확장, 선점(Preemption) 모델 차이, 관측·튜닝·디버깅(Debugging) 절차까지 포괄적으로 분석합니다.
왜 스케줄러가 필요한가?
현대 컴퓨터는 웹 브라우저, 음악 플레이어, 터미널, 시스템 데몬 등 수백 개의 프로세스를 동시에 실행하는 것처럼 보입니다. 그러나 CPU 코어 하나는 어느 순간에 정확히 하나의 프로세스만 실행할 수 있습니다.
스케줄러는 이 모순을 해결합니다. 각 프로세스에 매우 짧은 시간(약 1~10ms)의 CPU 시간을 번갈아 주어, 사람 눈에는 모든 프로세스가 동시에 실행되는 것처럼 보이게 합니다. 이를 시분할(Time-Sharing)이라 합니다.
프로세스 상태 전이 (State Machine)
스케줄러를 이해하려면 프로세스가 어떤 상태를 가지는지 알아야 합니다. 프로세스는 언제나 아래 세 가지 상태 중 하나에 있으며, 스케줄러는 READY → RUNNING 전환을 결정합니다.
| 상태 | 의미 | Linux 내부 값 | 런큐 포함? |
|---|---|---|---|
| RUNNING | 현재 CPU에서 코드를 실행 중 | TASK_RUNNING + rq→curr |
런큐에 있음 |
| READY | 실행 가능하지만 CPU를 기다리는 중 | TASK_RUNNING (curr 아님) |
런큐에 있음 |
| BLOCKED | I/O·락·sleep 등으로 이벤트를 기다리는 중 | TASK_INTERRUPTIBLE / TASK_UNINTERRUPTIBLE |
런큐에서 제거 |
단계별 이해
- 스케줄러의 역할 — "다음에 어떤 프로세스를 실행할까?"를 결정합니다.
CPU는 동시에 한 프로세스만 실행합니다. 스케줄러는 약 1~10ms마다 "지금 실행 중인 프로세스를 계속 실행할지, 아니면 다른 프로세스로 전환할지"를 결정합니다. 핵심 함수는
kernel/sched/core.c의__schedule()입니다. - 타이머 인터럽트와 스케줄러 호출 — 스케줄러는 매 타이머 인터럽트(기본 4ms,
CONFIG_HZ=250)마다 깨어납니다.타이머 인터럽트 핸들러(Handler)가
scheduler_tick()을 호출하고, 현재 태스크의 타임슬라이스가 다 되었거나 더 높은 우선순위 태스크가 깨어나면TIF_NEED_RESCHED플래그를 설정합니다. 인터럽트 복귀 시 커널이 이 플래그를 확인하고schedule()을 호출합니다. - CFS와 vruntime — 각 태스크의 가상 실행 시간(vruntime)을 추적하여, 가장 적게 실행된 태스크를 다음에 선택합니다.
nice 0 태스크가 10ms CPU를 쓰면 vruntime도 10ms 증가합니다. nice +5 태스크는 같은 10ms를 써도 vruntime이 더 빠르게 증가하도록 보정됩니다 (불리한 가중치). 결과적으로 nice 0 태스크가 자주 선택되고, nice +5 태스크는 덜 선택됩니다. 실행 가능한 태스크들은 vruntime을 키로 하는 Red-Black 트리에 저장되며, 항상 가장 왼쪽(최소 vruntime) 노드를 다음 실행 대상으로 O(1) 접근합니다.
- 우선순위와 nice 값 —
nice(-20~19) 값으로 프로세스의 CPU 시간 비중을 조정합니다.nice 0이 기본값입니다. 값이 낮을수록 더 많은 CPU 시간을 받습니다 (역직관적이지만, "nice하지 않다 = 욕심쟁이"). nice -5는 nice 0 대비 약 3배 더 많은 CPU 시간을 받습니다.
renice -n -5 -p <PID>로 조정 가능합니다 (root 권한 필요).ps -eo pid,ni,comm으로 현재 프로세스의 nice 값을 확인합니다. - 실시간 스케줄링 —
SCHED_FIFO/SCHED_RR은 일반 태스크보다 항상 우선합니다.실시간 태스크는 rtprio(1~99) 값으로 우선순위를 지정합니다. SCHED_FIFO는 자발적으로 양보(sleep/yield)하지 않으면 다른 태스크가 CPU를 받을 수 없습니다.
chrt -f 50 ./app으로 실시간 우선순위 50으로 프로그램을 실행합니다.SCHED_DEADLINE은 최고 우선순위이며, 데드라인과 실행 예산(runtime)을 명시합니다. - 컨텍스트 스위치 비용 — 태스크를 전환할 때마다 레지스터·메모리 맵 등을 저장/복원하는 비용이 발생합니다.
컨텍스트 스위치 한 번에 수 마이크로초(µs)가 걸립니다.
perf stat -e cs sleep 1으로 초당 컨텍스트 스위치 횟수를 확인할 수 있습니다. 스위치가 너무 잦으면(초당 수만 회) CPU 시간의 상당 부분이 실제 작업 대신 전환 오버헤드에 소모됩니다. 스레드 수를 CPU 코어 수에 맞추거나 배치 크기를 키우는 것이 일반적인 해결책입니다.
핵심 요약
- CFS / EEVDF — 일반 프로세스용 공정 스케줄러. vruntime(가상 실행 시간)으로 공평성을 유지합니다.
- sched_class — 스케줄링 정책의 플러그인 구조. dl → rt → fair → idle 우선순위입니다.
- 런큐(runqueue) — 각 CPU마다 하나씩 존재하며, 실행 대기 중인 태스크를 관리합니다.
- 선점(preemption) — 커널이 현재 실행 중인 태스크를 강제로 중단하고 다른 태스크를 실행합니다.
- 로드 밸런싱 — CPU 간에 태스크를 이동시켜 부하를 균등하게 분배합니다.
- 컨텍스트 스위치 — 태스크 전환 시 레지스터·주소 공간을 교체하는 과정으로, 수 µs의 오버헤드가 발생합니다.
스케줄러 개요 (Scheduler Overview)
Linux 커널 스케줄러는 시스템의 모든 CPU에서 어떤 태스크를 언제, 얼마나 실행할지 결정하는 핵심 서브시스템입니다. 스케줄러의 주요 목표는 다음과 같습니다:
| 목표 | 설명 | 관련 메트릭 |
|---|---|---|
| 공정성 (Fairness) | 모든 태스크가 가중치에 비례하는 CPU 시간을 받도록 보장 | vruntime 편차, lag |
| 응답성 (Responsiveness) | 대화형 태스크의 스케줄링 지연(Latency) 최소화 | wakeup-to-run latency |
| 처리량 (Throughput) | 단위 시간당 최대 작업 완료량 | IPC, context switch 빈도 |
| 실시간 보장 (RT Guarantee) | 실시간 태스크의 데드라인 충족 | worst-case latency |
| 에너지 효율 (Energy Efficiency) | 불필요한 CPU 활성화 최소화 | idle residency, C-state 진입률 |
| 확장성 (Scalability) | 수천 CPU NUMA 시스템에서도 효율적 동작 | lock contention, 밸런싱 오버헤드(Overhead) |
스케줄러 호출 경로 (Trigger Paths)
__schedule()은 직접 호출되지 않으며, 다음 네 가지 경로를 통해 간접적으로 호출됩니다. 각 경로를 이해하면 "언제 CPU가 다른 프로세스로 전환되는가"를 예측할 수 있습니다.
| 경로 | 대표 상황 | 선점 유형 | 즉시성 |
|---|---|---|---|
| ① 타이머 인터럽트 | 타임슬라이스 만료, 주기적 확인 | 비자발적(선점) | 인터럽트 복귀 시 |
| ② 자발적 양보 | sleep(), mutex_lock(), wait_event() |
자발적 | 즉시 |
| ③ 고우선순위 wakeup | I/O 완료, 시그널(Signal), wake_up() |
비자발적(선점) | syscall/IRQ 복귀 시 |
| ④ cond_resched() | 커널 내부 긴 루프 (파일시스템(Filesystem), 메모리 처리) | 협력적 | 즉시 (플래그 확인 후) |
커널 스케줄러의 핵심 진입점(Entry Point)은 kernel/sched/core.c의 __schedule() 함수입니다. 이 함수는 현재 태스크를 중단하고 다음에 실행할 태스크를 선택하여 컨텍스트 스위치를 수행합니다.
/* kernel/sched/core.c — 스케줄러 핵심 진입점 (간략화) */
static void __sched notrace __schedule(unsigned int sched_mode)
{
struct task_struct *prev, *next;
struct rq_flags rf;
struct rq *rq;
int cpu;
cpu = smp_processor_id();
rq = cpu_rq(cpu); /* per-CPU 런큐 획득 */
prev = rq->curr; /* 현재 실행 중인 태스크 */
rq_lock(rq, &rf);
/* 이전 태스크의 상태 갱신 (dequeue 여부 결정) */
if (!(sched_mode & SM_MASK_PREEMPT) && prev_state) {
if (signal_pending_state(prev_state, prev)) {
WRITE_ONCE(prev->__state, TASK_RUNNING);
} else {
prev->sched_contributes_to_load =
(prev_state & TASK_UNINTERRUPTIBLE) &&
!(prev_state & TASK_NOLOAD);
deactivate_task(rq, prev, DEQUEUE_SLEEP);
}
}
/* 다음 태스크 선택 — sched_class 계층을 순회 */
next = pick_next_task(rq, prev, &rf);
if (likely(prev != next)) {
rq->nr_switches++;
rq_set_curr(rq, next);
/* 컨텍스트 스위치 수행 */
rq = context_switch(rq, prev, next, &rf);
}
rq_unlock_irq(rq, &rf);
balance_callback(rq);
}
__schedule()은 다음 경로들에서 호출됩니다: (1) 태스크가 자발적으로 슬립(Sleep)할 때 (schedule()), (2) 인터럽트/시스템 콜(System Call) 복귀 시 TIF_NEED_RESCHED 플래그가 설정된 경우, (3) cond_resched() 호출 시 선점 필요한 경우, (4) 선점형 커널에서 선점 카운터가 0이 될 때.
sched_class 계층 구조
Linux 스케줄러는 모듈화된 클래스 기반 설계를 채택합니다. 각 스케줄링 정책은 struct sched_class를 구현하며, 우선순위가 높은 클래스부터 순서대로 탐색합니다.
/* include/linux/sched.h — sched_class 인터페이스 (주요 콜백) */
struct sched_class {
/* 태스크를 런큐에 삽입/제거 */
void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags);
void (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags);
/* 다음에 실행할 태스크 선택 */
struct task_struct *(*pick_next_task)(struct rq *rq);
/* 현재 태스크가 선점되어야 하는지 검사 */
void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
/* 태스크가 CPU를 양보할 때 */
void (*put_prev_task)(struct rq *rq, struct task_struct *p);
/* 주기적 타이머 틱 처리 */
void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
/* 태스크 웨이크업 시 CPU 선택 */
int (*select_task_rq)(struct task_struct *p, int prev_cpu, int wake_flags);
/* 로드 밸런싱 관련 */
int (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
void (*task_woken)(struct rq *rq, struct task_struct *p);
};
클래스 우선순위 체인
스케줄러가 pick_next_task()를 호출하면, 가장 높은 우선순위의 클래스부터 순차적으로 실행 가능한 태스크를 찾습니다.
| 우선순위 | sched_class | 소스 파일 | 용도 | 정책 |
|---|---|---|---|---|
| 1 (최고) | stop_sched_class |
kernel/sched/stop_task.c |
CPU 핫플러그(Hotplug), active migration | 내부 전용 (사용자 설정 불가) |
| 2 | dl_sched_class |
kernel/sched/deadline.c |
하드 실시간 태스크 | SCHED_DEADLINE (EDF/CBS) |
| 3 | rt_sched_class |
kernel/sched/rt.c |
소프트 실시간 태스크 | SCHED_FIFO, SCHED_RR |
| 4 | fair_sched_class |
kernel/sched/fair.c |
일반 태스크 (대부분의 프로세스) | SCHED_NORMAL, SCHED_BATCH |
| 5 | ext_sched_class |
kernel/sched/ext.c |
BPF 확장 스케줄러 (v6.12+) | SCHED_EXT (sched_ext) |
| 6 (최저) | idle_sched_class |
kernel/sched/idle.c |
CPU idle 태스크 | SCHED_IDLE (per-CPU swapper) |
/* kernel/sched/core.c — pick_next_task 최적 경로 */
static inline struct task_struct *
__pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
const struct sched_class *class;
struct task_struct *p;
/*
* 최적화: 런큐의 모든 태스크가 fair 클래스이면
* (nr_running == cfs_rq.h_nr_running) fair만 검사
*/
if (likely(rq->nr_running == rq->cfs.h_nr_running)) {
p = pick_next_task_fair(rq, prev, rf);
if (unlikely(p == RETRY_TASK))
goto restart;
return p;
}
restart:
put_prev_task_balance(rq, prev, rf);
/* 우선순위 높은 클래스부터 순회 */
for_each_class(class) {
p = class->pick_next_task(rq);
if (p)
return p;
}
/* idle 클래스는 항상 태스크를 반환 (per-CPU idle thread) */
BUG();
}
stop_sched_class → dl_sched_class → rt_sched_class → fair_sched_class → ext_sched_class (v6.12+) → idle_sched_class 순서로 순회합니다. 각 클래스의 pick_next_task()가 NULL을 반환하면 다음 클래스로 넘어갑니다.
스케줄링 정책 선택 가이드
어떤 정책을 써야 할지 모를 때 아래 가이드를 참고하세요. 대부분의 애플리케이션은 기본값(SCHED_NORMAL)으로 충분합니다.
| 사용 사례 | 권장 정책 | 설정 방법 | 주의사항 |
|---|---|---|---|
| 일반 애플리케이션 (브라우저, 서버, DB) | SCHED_NORMAL (nice 0) |
기본값 (별도 설정 불필요) | — |
| CPU를 많이 쓰는 백그라운드 작업 (빌드, 압축, 렌더링) | SCHED_BATCH 또는 nice +10~+19 |
chrt -b 0 make -j8 |
대화형 응답성에 영향 없이 백그라운드로 실행 |
| 대화형 앱의 우선순위를 높이고 싶을 때 | SCHED_NORMAL, nice -5~-10 |
renice -n -5 -p <PID> |
root 권한 필요. 너무 낮추면 다른 앱이 굶주림(starvation) |
| 오디오·비디오 실시간 처리 (PulseAudio, pipewire, 게임 엔진) | SCHED_FIFO rtprio 40~60 또는 SCHED_RR |
chrt -f 50 ./audio_app |
반드시 sleep/yield 포함. 무한루프 시 시스템 멈춤 |
| 엄격한 주기 태스크 (산업용 제어, 로보틱스) | SCHED_DEADLINE (EDF) |
sched_setattr(2) + runtime/deadline/period 지정 |
admission control 통과 필요. PREEMPT_RT 커널 권장 |
| 가장 낮은 우선순위로 자원을 양보하면서 실행 | SCHED_IDLE 또는 nice +19 |
chrt -i 0 ./idle_work |
CPU가 완전히 유휴(idle) 상태일 때만 실행됨 |
| 커스텀 스케줄링 로직이 필요한 경우 (연구·클라우드) | SCHED_EXT (v6.12+) |
BPF 스케줄러 로드 (scx_rusty, scx_layered 등) | 커널 6.12+ 필요. sched_ext 문서 참고 |
/* 스케줄링 정책을 프로그래밍 방식으로 설정하는 예시 */
#include <sched.h>
/* SCHED_FIFO 설정 (rtprio=50) */
struct sched_param sp = { .sched_priority = 50 };
sched_setscheduler(0, SCHED_FIFO, &sp);
/* SCHED_DEADLINE 설정: 10ms 주기에 3ms 실행 예산 */
struct sched_attr attr = {
.size = sizeof(attr),
.sched_policy = SCHED_DEADLINE,
.sched_runtime = 3 * 1000 * 1000, /* 3ms (ns) */
.sched_deadline = 10 * 1000 * 1000, /* 10ms (ns) */
.sched_period = 10 * 1000 * 1000, /* 10ms (ns) */
};
sched_setattr(0, &attr, 0); /* 실패 시 EBUSY (admission 거부) */
/* nice 값 변경 (SCHED_NORMAL) */
setpriority(PRIO_PROCESS, 0, -5); /* nice -5 (root 필요) */
nice(10); /* 현재 nice 값에서 +10 */
CFS (Completely Fair Scheduler)
CFS는 커널 2.6.23에서 Ingo Molnar가 도입한 스케줄러로, 이상적인 멀티태스킹 프로세서(Ideal Multi-Tasking CPU)를 근사(approximate)합니다. 이상적인 프로세서에서는 N개의 태스크가 각각 1/N의 CPU 시간을 동시에 받지만, 실제 하드웨어에서는 한 CPU에 하나의 태스크만 실행할 수 있으므로, CFS는 vruntime(가상 실행 시간)을 사용하여 공정성을 추적합니다.
vruntime (가상 실행 시간)
vruntime은 태스크가 실제로 소비한 CPU 시간을 가중치로 보정한 값입니다. nice 값이 낮은(우선순위 높은) 태스크는 vruntime이 느리게 증가하고, nice 값이 높은(우선순위 낮은) 태스크는 빠르게 증가합니다. update_curr()·calc_delta_fair() 구현 코드와 nice 가중치 테이블은 CFS 스케줄러를 참고하세요.
수치 예제로 이해하는 vruntime
nice 가중치는 kernel/sched/core.c의 sched_prio_to_weight[] 테이블에 정의됩니다.
nice 0의 가중치는 1024이며, nice 값이 1 증가할 때마다 약 1.25배씩 감소합니다.
vruntime 증분 공식은 delta_vruntime = delta_exec × (NICE_0_LOAD / weight)입니다.
| nice 값 | 가중치(weight) | 10ms 실행 시 vruntime 증가량 | CPU 시간 비율 (A:B:C) |
|---|---|---|---|
| -5 (높은 우선순위) | 3121 | 10ms × (1024/3121) ≈ 3.3ms | 3121 : 1024 : 335 ≈ 9.3 : 3 : 1 |
| 0 (기본값) | 1024 | 10ms × (1024/1024) = 10ms | |
| +5 (낮은 우선순위) | 335 | 10ms × (1024/335) ≈ 30.6ms |
위 표의 의미: 세 태스크가 모두 런큐에 있을 때, nice -5 태스크는 nice 0 태스크보다 약 3배, nice +5 태스크보다 약 9배 더 많은 실제 CPU 시간을 받습니다. 같은 10ms 실제 실행 시간이 경과해도 각 태스크의 vruntime 증가폭이 다르므로, 스케줄러가 다음 태스크를 고를 때 항상 최소 vruntime을 가진 nice -5 태스크를 우선 선택합니다.
- 1회 실행(A): A vruntime=3.3ms, B=0, C=0 → 다음은 B(최소)
- 2회 실행(B): A=3.3ms, B=10ms, C=0 → 다음은 C(최소)
- 3회 실행(C): A=3.3ms, B=10ms, C=30.6ms → 다음은 A(최소)
- 4회 실행(A): A=6.6ms, B=10ms, C=30.6ms → 다음은 A(최소)
- 5회 실행(A): A=9.9ms, B=10ms, C=30.6ms → 다음은 B(최소)
- … A가 약 9번 실행될 때 B가 3번, C가 1번 실행됩니다.
Red-Black Tree 기반 태스크 관리
CFS는 모든 실행 가능한(runnable) 태스크를 vruntime을 키로 하는 Red-Black Tree에 관리합니다. 트리의 가장 왼쪽(최소 vruntime) 노드가 다음 실행 대상입니다. 이 구조로 삽입/삭제/탐색이 모두 O(log N)이며, 가장 왼쪽 노드는 rb_leftmost에 캐싱되어 O(1) 접근이 가능합니다.
/* include/linux/sched.h — CFS 런큐 구조 */
struct cfs_rq {
struct load_weight load; /* 런큐 전체 가중치 합 */
unsigned int nr_running; /* 실행 가능 태스크 수 */
u64 min_vruntime; /* 런큐 내 최소 vruntime */
struct rb_root_cached tasks_timeline; /* RB-Tree (leftmost 캐싱) */
struct sched_entity *curr; /* 현재 실행 중인 엔티티 */
struct sched_entity *next; /* 다음 실행 힌트 */
struct sched_entity *skip; /* 건너뛸 엔티티 (yield) */
};
/* include/linux/sched.h — 스케줄 엔티티 */
struct sched_entity {
struct load_weight load; /* nice에 따른 가중치 */
struct rb_node run_node; /* RB-Tree 노드 */
unsigned int on_rq; /* 런큐에 있는지 여부 */
u64 exec_start; /* 현재 실행 시작 시각 */
u64 sum_exec_runtime; /* 누적 실행 시간 */
u64 vruntime; /* 가상 실행 시간 */
u64 prev_sum_exec_runtime; /* 이전 누적값 (preempt 체크용) */
};
타임 슬라이스 계산
CFS는 고정 타임 슬라이스를 사용하지 않습니다. 대신, 타겟 지연(sched_latency_ns)를 런큐의 태스크 수로 나누고 가중치를 반영합니다.
/*
* 타임 슬라이스 계산 공식:
*
* sched_latency_ns * weight_i
* timeslice_i = ─────────────────────────────────
* total_weight
*
* 예: sched_latency = 6ms, 태스크 A(nice 0, w=1024), B(nice 5, w=335)
*
* A의 슬라이스 = 6ms * 1024 / (1024+335) = 4.52ms
* B의 슬라이스 = 6ms * 335 / (1024+335) = 1.48ms
*
* 태스크가 nr_latency(기본 8)개를 초과하면:
* sched_min_granularity_ns(기본 0.75ms) * nr_running 사용
*/
/* kernel/sched/fair.c — sysctl 기본값 */
unsigned int sysctl_sched_latency = 6000000ULL; /* 6ms */
unsigned int sysctl_sched_min_granularity = 750000ULL; /* 0.75ms */
unsigned int sysctl_sched_wakeup_granularity = 1000000ULL; /* 1ms */
next/last 힌트 기반의 임시방편적 wakeup 선점 로직, (3) sched_latency 보장이 확률적(best-effort)이라는 점이었습니다.
EEVDF (Earliest Eligible Virtual Deadline First)
EEVDF는 커널 v6.6에서 Peter Zijlstra에 의해 CFS를 대체한 fair 스케줄링 알고리즘입니다. 1995년 Stoica와 Abdel-Wahab이 제안한 이론적 모델을 기반으로 하며, CFS의 단순한 "최소 vruntime" 선택 대신 적격 시각(eligible time)과 가상 데드라인(virtual deadline)을 사용하여 지연 시간 보장을 강화합니다.
핵심 개념
| 개념 | 기호 | 설명 |
|---|---|---|
| 가상 시간 (Virtual Time) | V(t) | 이상적 프로세서에서 각 태스크가 받았어야 할 시간의 기준점 |
| 적격 시각 (Eligible Time) | ei | 태스크 i가 실행 권리를 갖는 가상 시각. lag ≥ 0이면 적격 |
| 가상 데드라인 (Virtual Deadline) | di | ei + (요청 슬라이스 / 가중치). 작을수록 긴급 |
| 래그 (Lag) | lagi | 이상적 서비스량 - 실제 서비스량. 양수면 서비스 부족(eligible) |
| 요청 슬라이스 (Request/Slice) | ri | 태스크가 요청한 실행 시간 단위 (커널의 slice 필드) |
알고리즘 동작 원리
EEVDF의 태스크 선택 과정은 두 단계입니다:
- 적격성(Eligibility) 필터링:
vruntime ≤ V(t)(즉, lag ≥ 0)인 태스크만 후보로 선별 - 데드라인 기반 선택: 적격 태스크 중
virtual deadline이 가장 빠른 태스크를 선택
/* kernel/sched/fair.c — EEVDF pick_eevdf() (v6.6+, 간략화) */
static struct sched_entity *
pick_eevdf(struct cfs_rq *cfs_rq)
{
struct rb_node *node = cfs_rq->tasks_timeline.rb_root.rb_node;
struct sched_entity *best = NULL;
while (node) {
struct sched_entity *se = __node_2_se(node);
/*
* 적격성 검사: entity의 vruntime이 cfs_rq의 avg_vruntime 이하인지
* (lag >= 0 ⟺ vruntime <= V(t) ⟺ eligible)
*/
if (entity_eligible(cfs_rq, se)) {
/* 적격 태스크 중 deadline이 가장 빠른 것 선택 */
if (!best || deadline_gt(deadline, best, se))
best = se;
node = node->rb_left; /* 더 빠른 vruntime (eligible) 탐색 */
} else {
node = node->rb_right; /* 아직 eligible 아님 → 오른쪽 */
}
}
return best;
}
/* 적격성 판단 (O(1)) */
static int entity_eligible(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
/*
* avg_vruntime은 런큐의 가중 평균 vruntime = V(t)
* se->vruntime <= V(t) 이면 eligible (서비스 부족 상태)
*/
return vruntime_eligible(cfs_rq, se->vruntime);
}
슬라이스와 데드라인
EEVDF에서 각 태스크의 가상 데드라인은 eligible time + slice / weight로 계산됩니다. slice는 태스크가 한 번에 요청하는 실행 시간 단위입니다.
/* kernel/sched/fair.c — 데드라인 갱신 */
static void update_deadline(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
if ((s64)(se->vruntime - se->deadline) >= 0) {
/*
* 현재 슬라이스 소진 → 새 데드라인 설정
* deadline = vruntime + calc_delta_fair(slice, se)
*
* calc_delta_fair: slice를 weight로 보정
* nice 0: deadline = vruntime + slice
* nice -5: deadline = vruntime + slice * 0.328
*/
se->deadline = se->vruntime +
calc_delta_fair(se->slice, se);
/* 선점 검사: 새 태스크의 deadline이 현재보다 빠르면 resched */
if (cfs_rq->nr_running > 1)
resched_curr(rq_of(cfs_rq));
}
}
avg_vruntime (가중 평균 가상 시간)
EEVDF는 런큐의 "현재 가상 시간" V(t)를 추적하기 위해 avg_vruntime을 유지합니다. 이는 모든 실행 가능한 엔티티의 vruntime 가중 평균입니다.
/* kernel/sched/fair.c — 가중 평균 vruntime 계산 */
static u64 avg_vruntime(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
s64 avg = cfs_rq->avg_vruntime;
long load = cfs_rq->avg_load;
if (curr && curr->on_rq) {
unsigned long weight = scale_load_down(curr->load.weight);
avg += entity_key(cfs_rq, curr) * weight;
load += weight;
}
if (load)
avg = div_s64(avg, load);
return cfs_rq->min_vruntime + avg;
}
실시간 스케줄링 (Real-Time Scheduling)
Linux는 POSIX.1b 실시간 스케줄링 정책을 지원합니다. RT 태스크는 fair 클래스보다 항상 높은 우선순위를 가지므로, RT 태스크가 실행 가능한 한 일반 태스크는 CPU를 받지 못합니다.
RT 정책: SCHED_FIFO vs SCHED_RR
| 속성 | SCHED_FIFO | SCHED_RR |
|---|---|---|
| 타임 슬라이스 | 무한 (자발적 양보(Yield)까지 계속 실행) | 고정 (기본 100ms, sched_rr_timeslice_ms) |
| 같은 우선순위 내 동작 | FIFO 순서, 선점 없음 | 라운드 로빈(Round Robin), 슬라이스 소진 시 큐 뒤로 |
| 우선순위 범위 | 1 ~ 99 (99가 최고) | 1 ~ 99 (99가 최고) |
| 선점 | 더 높은 우선순위 RT에 의해서만 | 더 높은 우선순위 + 슬라이스 만료 |
| 용도 | 지연 민감 제어 루프 | 균등 시분할이 필요한 RT 태스크 |
/* RT 스케줄링 설정 예제 (사용자 공간) */
#include <sched.h>
struct sched_param param;
param.sched_priority = 80; /* 1-99, 높을수록 높은 우선순위 */
/* SCHED_FIFO 설정 */
sched_setscheduler(pid, SCHED_FIFO, ¶m);
/* SCHED_RR 설정 (타임 슬라이스 포함) */
sched_setscheduler(pid, SCHED_RR, ¶m);
/* 현재 RR 타임 슬라이스 확인 */
struct timespec ts;
sched_rr_get_interval(pid, &ts); /* 기본 100ms */
RT 스케줄러 내부 구현
RT 스케줄러는 우선순위별 연결 리스트(Linked List) 배열을 사용합니다. 비트맵(Bitmap)으로 비어 있지 않은 우선순위를 O(1)에 찾습니다.
/* kernel/sched/rt.c — RT 런큐 구조 */
struct rt_prio_array {
DECLARE_BITMAP(bitmap, MAX_RT_PRIO + 1); /* 100비트 비트맵 */
struct list_head queue[MAX_RT_PRIO]; /* 우선순위별 리스트 */
};
struct rt_rq {
struct rt_prio_array active; /* 활성 태스크 배열 */
unsigned int rt_nr_running; /* RT 태스크 수 */
unsigned int rr_nr_running; /* SCHED_RR 태스크 수 */
struct {
int curr; /* 현재 최고 우선순위 */
int next; /* 다음 최고 우선순위 */
} highest_prio;
};
/* pick_next_task_rt — O(1) 최고 우선순위 태스크 선택 */
static struct task_struct *
pick_next_task_rt(struct rq *rq)
{
struct rt_rq *rt_rq = &rq->rt;
struct rt_prio_array *array = &rt_rq->active;
int idx;
idx = sched_find_first_bit(array->bitmap); /* 비트맵에서 최고 우선순위 */
if (idx >= MAX_RT_PRIO)
return NULL;
return list_first_entry(&array->queue[idx],
struct sched_rt_entity, run_list);
}
RT 쓰로틀링 (RT Throttling)
RT 태스크가 CPU를 독점하여 일반 태스크가 기아(starvation) 상태에 빠지는 것을 방지하기 위해, 커널은 RT 대역폭(Bandwidth) 제한(RT bandwidth throttling)을 적용합니다.
# RT 쓰로틀링 기본값 확인
cat /proc/sys/kernel/sched_rt_period_us # 1000000 (1초)
cat /proc/sys/kernel/sched_rt_runtime_us # 950000 (950ms)
# 의미: 1초 주기 중 RT 태스크는 최대 950ms만 실행 가능
# 나머지 50ms는 일반(fair) 태스크에 할당
# RT 쓰로틀링 비활성화 (위험! 프로덕션에서 사용 금지)
echo -1 > /proc/sys/kernel/sched_rt_runtime_us
sched_rt_runtime_us = -1), 무한 루프 버그가 있는 RT 태스크가 시스템을 완전히 멈출 수 있습니다. 반드시 개발/테스트 환경에서만 사용하고, 프로덕션 시스템에서는 적절한 RT 대역폭 제한을 유지하십시오.
SCHED_DEADLINE
SCHED_DEADLINE은 커널 3.14에서 도입된 CBS(Constant Bandwidth Server) + EDF(Earliest Deadline First) 기반 스케줄링 정책입니다. RT 클래스보다도 높은 우선순위를 가지며, 태스크에 명시적인 실행 예산(runtime), 주기(period), 데드라인(deadline)을 할당합니다.
- CBS는 태스크별 실행 예산(Q)과 주기(T) 비율로 대역폭을 격리(Isolation)합니다. 예산을 초과한 태스크는 다음 주기까지 실행 불가 상태로 전환되어, 과실행이 다른 태스크의 데드라인에 영향을 주지 않습니다.
- EDF는 런큐에서 절대 데드라인이 가장 빠른 태스크를 선택합니다. CBS가 "얼마나 실행할 수 있는가"를 결정하고, EDF가 "다음에 누가 실행되는가"를 결정합니다.
SCHED_DEADLINE 파라미터
| 파라미터 | 필드 | 설명 |
|---|---|---|
| Runtime (Q) | sched_runtime |
한 주기 내 최대 실행 시간 (ns) |
| Deadline (D) | sched_deadline |
주기 시작부터 runtime을 소진해야 하는 시한 (ns) |
| Period (T) | sched_period |
태스크 활성화 주기 (ns). D ≤ T 필수 |
/* SCHED_DEADLINE 설정 예제 (사용자 공간) */
#include <sched.h>
#include <linux/sched/types.h>
struct sched_attr attr = {
.size = sizeof(attr),
.sched_policy = SCHED_DEADLINE,
.sched_runtime = 10 * 1000 * 1000, /* 10ms 실행 예산 */
.sched_deadline = 30 * 1000 * 1000, /* 30ms 데드라인 */
.sched_period = 50 * 1000 * 1000, /* 50ms 주기 */
};
/* sched_setattr 시스템 콜 사용 (glibc 래퍼 없음 → syscall 직접) */
syscall(SYS_sched_setattr, pid, &attr, 0);
/*
* 대역폭 비율: Q/T = 10ms/50ms = 20%
* 즉, 이 태스크는 CPU의 20%를 보장받으며,
* 매 주기 시작 후 30ms 이내에 10ms를 실행
*/
CBS (Constant Bandwidth Server)
CBS는 각 DEADLINE 태스크를 독립적인 "서버"로 취급하여, 한 태스크의 과도한 실행이 다른 태스크에 영향을 미치지 않도록 격리합니다.
/* kernel/sched/deadline.c — CBS 서버 보충 로직 (간략화) */
static void replenish_dl_entity(struct sched_dl_entity *dl_se)
{
/*
* 새 주기 시작 → 실행 예산(runtime) 보충
* deadline = 현재시각 + relative_deadline
*/
dl_se->runtime = dl_se->dl_runtime; /* Q 보충 */
dl_se->deadline = rq_clock(rq) + dl_se->dl_deadline;
/*
* 과거 deadline이 아직 유효하면 (서버가 idle 상태였으면)
* deadline을 현재 기준으로 리셋하여 다른 태스크에 피해 방지
*/
if (dl_time_before(dl_se->deadline, rq_clock(rq))) {
dl_se->deadline = rq_clock(rq) + dl_se->dl_deadline;
dl_se->runtime = dl_se->dl_runtime;
}
}
/* EDF 기반 태스크 선택: deadline이 가장 빠른 태스크 */
static struct task_struct *
pick_next_task_dl(struct rq *rq)
{
struct dl_rq *dl_rq = &rq->dl;
struct rb_node *left = rb_first_cached(&dl_rq->root);
if (!left)
return NULL;
return dl_task_of(__node_2_dle(left)); /* 최소 deadline */
}
입장 제어 (Admission Control)
SCHED_DEADLINE은 시스템의 총 대역폭이 초과되지 않도록 입장 제어를 수행합니다.
- 입장 제어 조건 (간략화):
- 모든 DEADLINE 태스크 i에 대해:
- Σ(Q_i / T_i)
런큐 (Per-CPU Runqueue)
각 CPU에는 고유한 런큐(struct rq)가 있습니다. 이는 스케줄러의 핵심 자료구조로, 해당 CPU에서 실행 가능한 모든 태스크를 관리합니다. per-CPU 설계 덕분에 대부분의 스케줄링 결정이 로컬 잠금(Lock)만으로 가능합니다.
/* kernel/sched/sched.h — per-CPU 런큐 구조 (주요 필드) */
struct rq {
/* --- 글로벌 상태 --- */
raw_spinlock_t __lock; /* 런큐 락 */
unsigned int nr_running; /* 총 실행 가능 태스크 수 */
unsigned int nr_switches; /* 컨텍스트 스위치 카운터 */
/* --- 현재 실행 중 --- */
struct task_struct *curr; /* 현재 태스크 */
struct task_struct *idle; /* per-CPU idle 태스크 */
struct task_struct *stop; /* stop 태스크 (migration) */
/* --- 클래스별 서브 런큐 --- */
struct cfs_rq cfs; /* CFS/EEVDF 런큐 */
struct rt_rq rt; /* RT 런큐 */
struct dl_rq dl; /* DEADLINE 런큐 */
/* --- 시간 관리 --- */
u64 clock; /* 런큐 시계 (ns) */
u64 clock_task; /* irq 시간 제외 태스크 시계 */
/* --- 로드 밸런싱 --- */
struct sched_domain *sd; /* 스케줄링 도메인 */
unsigned long cpu_capacity; /* CPU 용량 (freq 반영) */
/* --- 선점/리스케줄 --- */
int skip_clock_update; /* 시계 갱신 건너뛰기 */
unsigned long nr_uninterruptible; /* load avg 보정용 */
/* --- NUMA 밸런싱 --- */
unsigned int nr_preferred_running; /* NUMA 선호 CPU 태스크 */
struct cpu_stop_work active_balance_work; /* 능동 밸런싱 */
int active_balance;
int push_cpu;
};
컨텍스트 스위치 경로
컨텍스트 스위치는 현재 태스크의 CPU 상태(레지스터(Register), 스택 포인터)를 저장하고 다음 태스크의 상태를 복원하는 과정입니다.
컨텍스트 스위치 비용 이해
컨텍스트 스위치는 공짜가 아닙니다. 전환 한 번에 다음 두 종류의 비용이 발생합니다.
| 비용 종류 | 내용 | 규모 (x86-64 기준) |
|---|---|---|
| 직접 비용 (Direct) | 레지스터 저장·복원, CR3(페이지 테이블 포인터) 교체, TLB 플러시(Flush) | 1~10 µs |
| 간접 비용 (Indirect) | CPU 캐시(L1/L2/L3) 콜드, TLB 미스, 분기 예측(Branch Prediction)기 초기화 | 수십~수백 µs (워크로드에 따라 가변) |
# 1초 동안의 컨텍스트 스위치 횟수 확인
perf stat -e cs sleep 1
# 출력 예: 1,234 cs (= 1초에 1,234회 컨텍스트 스위치)
# vmstat으로 초당 컨텍스트 스위치(cs) 확인
vmstat 1
# cs 컬럼: 일반적으로 서버에서 수천~수만 회/초가 정상
# 스위치가 많은 프로세스 찾기
pidstat -w 1
# cswch/s: 자발적 스위치, nvcswch/s: 비자발적 스위치
# 스위치 지연 직접 측정 (lmbench)
lat_ctx -s 0 2
# 출력 예: "2 processes: 3.4 microseconds" (2-태스크 전환 지연)
컨텍스트 스위치가 너무 잦을 때의 증상과 해결책:
| 증상 | 원인 | 해결책 |
|---|---|---|
| CPU 사용률은 높지만 처리량(throughput)이 낮음 | 스레드 수 >> CPU 코어 수, 짧은 임계 구간에서 잦은 락 경합 | 스레드 풀 크기를 CPU 코어 수에 맞춤, 락 범위 축소 |
nvcswch/s (비자발적 스위치) 급증 |
타임슬라이스보다 짧은 실행 단위, 고우선순위 태스크가 자주 선점 | sched_min_granularity_ns 증가, 배치 크기 확대 |
| 특정 프로세스의 레이턴시(latency) 급등 | 같은 CPU에서 선점이 잦아 캐시가 자꾸 비워짐 | taskset/cpuset으로 CPU 고정, isolcpus 커널 파라미터 |
/* kernel/sched/core.c — 컨텍스트 스위치 (간략화) */
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
/* 1단계: 메모리 컨텍스트 전환 (mm_struct) */
if (!next->mm) {
/* 커널 스레드 → 이전 태스크의 mm 빌려씀 (lazy TLB) */
enter_lazy_tlb(prev->active_mm, next);
next->active_mm = prev->active_mm;
} else {
/* 사용자 프로세스 → 페이지 테이블 전환 */
switch_mm_irqs_off(prev->active_mm, next->mm, next);
}
/* 2단계: CPU 레지스터/스택 전환 (아키텍처 의존) */
switch_to(prev, next, prev);
/* === 이 시점에서 CPU는 'next' 태스크를 실행 중 === */
return finish_task_switch(prev);
}
switch_to(prev, next, last)는 아키텍처별로 구현됩니다. x86에서는 RSP(스택 포인터)를 교체하고 RIP(명령어 포인터)를 __switch_to_asm에서 전환합니다. 세 번째 인자 last는 스위치 후 "이전에 실행 중이던 태스크"를 반환받기 위한 것으로, A→B→A 순환에서 A가 복귀했을 때 B를 정리하기 위해 필요합니다.
로드 밸런싱 (Load Balancing)
SMP(Symmetric Multi-Processing, 대칭형 다중 처리) 시스템에서 CPU 간 부하를 균등하게 분배하는 것은 성능의 핵심입니다. Linux 스케줄러는 sched_domain 계층 구조를 통해 토폴로지(Topology) 인식 로드 밸런싱을 수행합니다.
sched_domain 계층 구조
스케줄링 도메인(Scheduling Domain)은 CPU의 물리적 토폴로지를 반영합니다. SMT(하이퍼스레딩), MC(멀티코어), NUMA 노드 수준으로 계층화됩니다.
로드 밸런싱 알고리즘
로드 밸런싱은 주기적으로(sched_balance_softirq) 또는 idle CPU가 작업을 찾을 때(idle balancing) 트리거됩니다.
/* kernel/sched/fair.c — 로드 밸런싱 핵심 흐름 (간략화) */
static int sched_balance_rq(struct rq *this_rq,
struct sched_domain *sd,
enum cpu_idle_type idle)
{
struct lb_env env = {
.sd = sd,
.dst_cpu = this_rq->cpu,
.dst_rq = this_rq,
.idle = idle,
};
/* 1단계: 가장 바쁜 그룹 찾기 */
struct sched_group *busiest_group =
find_busiest_group(&env, &sds);
if (!busiest_group)
goto out_balanced;
/* 2단계: 그룹 내 가장 바쁜 런큐 찾기 */
struct rq *busiest =
find_busiest_queue(&env, busiest_group);
if (!busiest)
goto out_balanced;
/* 3단계: 태스크 이주 (busiest → this_rq) */
detach_tasks(&env);
if (env.imbalance && !env.loop_break) {
attach_tasks(&env);
}
return 0;
out_balanced:
return 0;
}
밸런싱 메트릭
| 메트릭 | 설명 | 용도 |
|---|---|---|
cpu_load |
PELT 기반 CPU 부하 (지수 감쇠 평균) | idle balancing 판단 |
runnable_avg |
태스크의 실행 가능 비율 (0~1024) | 태스크 가중치 산출 |
util_avg |
태스크의 실제 CPU 활용도 (0~1024) | CPU 용량 대비 활용도 비교 |
nr_running |
런큐의 실행 가능 태스크 수 | 불균형 감지 |
cpu_capacity |
CPU 용량 (주파수, 열 제한 반영) | 비대칭(big.LITTLE) 밸런싱 |
CPU 마이그레이션
태스크가 한 CPU에서 다른 CPU로 이주하는 경로는 크게 세 가지입니다:
/*
* 1. Pull migration (주기적 밸런싱)
* - 바쁜 CPU에서 idle CPU로 태스크를 "당겨옴"
* - softirq 컨텍스트에서 실행
* - sched_balance_rq() → detach_tasks() → attach_tasks()
*
* 2. Push migration (RT/DL 전용)
* - 높은 우선순위 태스크가 깨어나면 적합한 CPU로 "밀어냄"
* - push_rt_task() / push_dl_task()
*
* 3. Wake-up migration
* - 태스크가 깨어날 때 select_task_rq()에서 최적 CPU 선택
* - 캐시 친화성 vs 부하 분산 트레이드오프
*/
/* kernel/sched/fair.c — select_task_rq_fair (웨이크업 CPU 선택, 간략화) */
static int
select_task_rq_fair(struct task_struct *p, int prev_cpu, int wake_flags)
{
struct sched_domain *sd;
int new_cpu = prev_cpu;
int want_affine = 0;
/* 깨운 CPU가 prev_cpu와 같은 도메인이면 affinity 선호 */
if (cpumask_test_cpu(smp_processor_id(), p->cpus_ptr))
want_affine = 1;
/* 도메인 계층을 올라가며 가장 idle한 CPU/그룹 선택 */
for_each_domain(smp_processor_id(), sd) {
if (want_affine && (sd->flags & SD_WAKE_AFFINE)) {
new_cpu = select_idle_sibling(p, prev_cpu, new_cpu);
break;
}
}
return new_cpu;
}
PELT (Per-Entity Load Tracking)
커널 3.8에서 도입된 PELT는 스케줄러의 부하 측정 기반입니다. 이전의 per-CPU 부하 추적과 달리, 각 스케줄 엔티티(태스크, cgroup)별로 독립적으로 부하를 추적하여 정밀한 로드 밸런싱과 주파수 결정을 가능하게 합니다.
핵심 메트릭: load_avg, runnable_avg, util_avg
| 메트릭 | 범위 | 측정 대상 | 용도 |
|---|---|---|---|
load_avg |
0 ~ ∞ | runnable_avg × weight (nice 반영) | 로드 밸런싱 — CPU 간 부하 비교 |
runnable_avg |
0 ~ 1024 | 태스크가 실행 중 + 런큐 대기 중인 비율 | 런큐 포화도 판단 |
util_avg |
0 ~ 1024 | 태스크가 실제로 CPU에서 실행 중인 비율 | EAS 에너지 계산, schedutil DVFS |
/* kernel/sched/pelt.c — PELT 감쇠 갱신 (간략화) */
/*
* PELT 감쇠 공식:
*
* load_sum = Σ (기여값 × y^n)
*
* 여기서:
* y = (2^32 - 1) / 2^32 ≈ 0.978 (감쇠 계수)
* n = 경과한 1024μs 윈도우 수
*
* 각 1024μs 윈도우에서:
* - running 상태이면 기여값 = 1 (util, runnable, load 모두)
* - runnable(대기) 상태이면 기여값 = 1 (runnable, load만)
* - sleeping 상태이면 기여값 = 0 (기존 값에 감쇠만 적용)
*
* 반감기: y^32 ≈ 0.5 → 약 32ms(32 윈도우)마다 기여도 절반
*/
static u32
accumulate_sum(u64 delta, struct sched_avg *sa,
unsigned long load, unsigned long runnable,
int running)
{
u32 contrib = (u32)delta; /* 현재 윈도우 기여분 */
u64 periods = delta / 1024;
if (periods) {
/* 과거 윈도우에 감쇠 적용 */
sa->load_sum = decay_load(sa->load_sum, periods);
sa->runnable_sum = decay_load(sa->runnable_sum, periods);
sa->util_sum = decay_load(sa->util_sum, periods);
}
/* 현재 윈도우 기여분 추가 */
sa->util_sum += running * contrib; /* 실행 중일 때만 */
sa->runnable_sum += runnable * contrib; /* 실행 중 + 대기 */
sa->load_sum += load * contrib; /* 가중치 반영 */
return periods;
}
/* 평균값 계산: sum / divider (PELT 기하급수 합의 상한) */
static void ___update_load_avg(struct sched_avg *sa,
unsigned long load)
{
u32 divider = LOAD_AVG_MAX - 1024 + sa->period_contrib;
sa->load_avg = div_u64(load * sa->load_sum, divider);
sa->runnable_avg = div_u64(sa->runnable_sum, divider);
sa->util_avg = sa->util_sum / divider;
}
# PELT 메트릭 확인 (debugfs)
cat /proc/<pid>/sched | grep -E 'avg\.'
# se.avg.load_avg : 512
# se.avg.runnable_avg : 680
# se.avg.util_avg : 450
# CPU 전체 PELT (cfs_rq 레벨)
cat /proc/sched_debug | grep -A5 'cfs_rq\[0\]'
# .load_avg = 2048 (CPU 0 CFS 런큐의 총 가중 부하)
# .util_avg = 820 (CPU 0 CFS 런큐의 총 활용도)
# schedutil 연동: PELT util_avg → CPU 주파수 결정
cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor
# schedutil ← PELT 기반 주파수 거버너
# util_avg와 주파수의 관계:
# freq_next = freq_max × (util_avg + margin) / capacity
schedutil cpufreq 거버너는 PELT의 util_avg를 직접 사용하여 CPU 주파수를 결정합니다. util_avg가 높으면 주파수를 올리고, 낮으면 내립니다. 이는 스케줄러가 주파수 결정에 직접 관여하여, 기존 ondemand/conservative 거버너보다 빠르고 정확한 DVFS를 가능하게 합니다. EAS가 schedutil을 필수로 요구하는 이유입니다.
EAS (Energy Aware Scheduling)
EAS는 커널 5.0에서 메인라인에 병합된 에너지 효율 인식 스케줄링 프레임워크입니다 (Android 공통 커널에는 4.x부터 존재, 메인라인 진입은 5.0). 전통적인 로드 밸런싱이 "모든 CPU에 부하를 균등 분배"하는 것을 목표로 하는 반면, EAS는 성능 요구를 충족하면서 에너지 소비를 최소화하는 CPU를 선택합니다. ARM big.LITTLE, DynamIQ, Intel Hybrid(Alder Lake+) 등 비대칭 CPU 토폴로지에서 핵심적인 역할을 합니다.
EAS 핵심 원리
EAS는 태스크를 배치할 때 에너지 모델(Energy Model, EM)을 참조하여 각 CPU에 태스크를 배치했을 때의 예상 에너지 소비를 계산하고, 가장 효율적인 배치를 선택합니다.
| 구성요소 | 설명 | 커널 소스 |
|---|---|---|
| Energy Model (EM) | 각 성능 도메인(Performance Domain)의 OPP별 전력/주파수 테이블 | kernel/power/energy_model.c |
| Performance Domain | 동일 주파수를 공유하는 CPU 그룹 (예: big 클러스터, LITTLE 클러스터) | include/linux/energy_model.h |
| PELT util_avg | 태스크/CPU의 실제 활용도 (0~1024) | kernel/sched/pelt.c |
| schedutil governor | 스케줄러 연동 DVFS — util_avg 기반 주파수 결정 | kernel/sched/cpufreq_schedutil.c |
/* kernel/sched/fair.c — find_energy_efficient_cpu() (간략화) */
static int
find_energy_efficient_cpu(struct task_struct *p, int prev_cpu)
{
struct perf_domain *pd;
unsigned long best_delta = ULONG_MAX;
int best_cpu = -1;
/* 모든 성능 도메인(클러스터)을 순회 */
for_each_pd(pd) {
unsigned long cur_energy, new_energy;
int max_spare_cap_cpu = -1;
/* 도메인 내에서 여유 용량이 가장 큰 CPU 찾기 */
for_each_cpu(cpu, perf_domain_span(pd)) {
unsigned long spare = cpu_cap(cpu) - cpu_util(cpu);
if (spare > max_spare)
max_spare_cap_cpu = cpu;
}
/* 현재 에너지 vs 태스크 배치 후 에너지 비교 */
cur_energy = compute_energy(p, -1, pd); /* 태스크 없이 */
new_energy = compute_energy(p, max_spare_cap_cpu, pd);
unsigned long delta = new_energy - cur_energy;
if (delta < best_delta) {
best_delta = delta;
best_cpu = max_spare_cap_cpu;
}
}
/* 에너지 절감이 6% 미만이면 prev_cpu 유지 (마이그레이션 비용) */
if (best_delta > prev_delta * 94 / 100)
return prev_cpu;
return best_cpu;
}
EAS 활성화 조건
EAS는 다음 조건이 모두 충족될 때만 활성화됩니다:
| 조건 | 설명 | 확인 방법 |
|---|---|---|
| 비대칭 CPU 용량 | 성능 도메인(PD)이 2개 이상이고 CPU 용량이 다름 | cat /sys/devices/system/cpu/cpu*/cpu_capacity |
| Energy Model 등록 | DT(Device Tree) 또는 ACPI CPPC로 EM이 등록됨 | ls /sys/devices/virtual/powercap/dtpm/ |
| schedutil governor | cpufreq governor가 schedutil이어야 함 | cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor |
| overutilized 아님 | 시스템 전체 사용률이 80% 미만 | sched_domain 플래그 SD_ASYM_CPUCAPACITY |
# EAS 활성 상태 확인
# 성능 도메인 확인
cat /proc/schedstat | head -20
# CPU 용량 확인 (비대칭이면 값이 다름)
for cpu in /sys/devices/system/cpu/cpu[0-9]*; do
echo "$(basename $cpu): $(cat $cpu/cpu_capacity)"
done
# schedutil governor 설정 (EAS 필수 조건)
echo schedutil > /sys/devices/system/cpu/cpufreq/policy0/scaling_governor
# overutilized 상태 추적
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_overutilized_tp/enable
cat /sys/kernel/debug/tracing/trace_pipe
overutilized 플래그가 설정되고, 전통적인 로드 밸런싱으로 자동 전환됩니다. 이는 높은 부하에서 에너지 최적화보다 성능 분배가 중요하기 때문입니다.
이기종 CPU 스케줄링 (Heterogeneous Scheduling)
최신 SoC와 프로세서는 성능/전력 특성이 다른 CPU 코어를 혼합하는 이기종 아키텍처를 채택합니다. Linux 스케줄러는 CPU 용량(capacity), 적합성(fitness), 비대칭 패킹(asymmetric packing) 등의 메커니즘으로 이를 지원합니다.
주요 이기종 아키텍처
| 아키텍처 | 구조 | 특징 | Linux 지원 |
|---|---|---|---|
| ARM big.LITTLE | big(Cortex-A7x) + LITTLE(Cortex-A5x) | 클러스터 단위 DVFS, GTS | EAS, SD_ASYM_CPUCAPACITY |
| ARM DynamIQ | big + mid + LITTLE (3클러스터) | 단일 클러스터 내 이기종 가능, L3 공유 | EAS, 3-PD 에너지 모델 |
| Intel Hybrid (ADL+) | P-core(Golden Cove) + E-core(Gracemont) | ITMT(Intel Thread Director), HFI | ITMT, asym_packing, HFI |
| Apple Silicon | Firestorm(P) + Icestorm(E) | macOS AMP 스케줄러 (Linux 미공식) | Asahi Linux EAS 실험적 |
CPU 용량(Capacity)과 적합성(Fitness)
스케줄러는 각 CPU의 용량(capacity)을 0~1024 범위로 정규화합니다. 가장 성능이 높은 CPU가 1024이고, 나머지는 상대적 비율로 표현됩니다.
/* kernel/sched/fair.c — CPU 적합성 검사 */
static inline int
task_fits_cpu(struct task_struct *p, int cpu)
{
unsigned long cap = capacity_of(cpu);
unsigned long util = task_util_est(p);
/*
* 태스크 활용도가 CPU 용량의 80% 이하이면 적합
* 마진 20%: 주파수 변경 지연 + 버스트 대응
*/
return fits_capacity(util, cap);
}
/* capacity 비교 매크로 */
#define fits_capacity(cap, max) ((cap) * 1280 < (max) * 1024)
Intel Hybrid: ITMT와 HFI
Intel 12세대(Alder Lake)부터 P-core와 E-core를 혼합합니다. Linux는 ITMT(Intel Turbo Boost Max Technology 3.0)와 HFI(Hardware Feedback Interface)를 통해 이기종 스케줄링을 지원합니다.
/* arch/x86/kernel/itmt.c — ITMT 우선순위 설정 */
void sched_set_itmt_core_prio(int prio, int cpu)
{
/*
* P-core: 높은 우선순위 (예: 2)
* E-core: 낮은 우선순위 (예: 1)
* → asym_packing 로직이 P-core를 먼저 채움
*/
per_cpu(sched_core_priority, cpu) = prio;
}
/* SD_ASYM_PACKING: P-core를 먼저 채우는 비대칭 패킹 */
/* sched_domain 플래그에 SD_ASYM_PACKING 설정 시
* 우선순위가 높은 CPU에 태스크를 몰아서 배치
* → E-core는 P-core가 바쁠 때만 사용 */
/* drivers/thermal/intel/intel_hfi.c — HFI 콜백 */
/*
* HFI(Hardware Feedback Interface)는 하드웨어가 실시간으로
* 각 CPU의 성능/효율 등급을 보고하는 인터페이스
*
* 열 제한 시 P-core 성능이 E-core 수준으로 떨어지면
* HFI가 스케줄러에 알려 E-core 우선 사용으로 전환
*/
static void intel_hfi_online(unsigned int cpu)
{
struct hfi_cpu_info *info = per_cpu_ptr(&hfi_cpu_info, cpu);
/* perf_cap: 성능 등급 (0-255), ee_cap: 에너지 효율 등급 */
info->perf_cap = hfi_read_perf(cpu);
info->ee_cap = hfi_read_ee(cpu);
}
Misfit 태스크 마이그레이션
작은 용량의 CPU에서 실행 중인 태스크가 해당 CPU의 용량을 초과하면(misfit), 스케줄러는 더 큰 용량의 CPU로 마이그레이션합니다.
/* kernel/sched/fair.c — misfit 태스크 감지 */
static void check_misfit_status(struct rq *rq,
struct task_struct *p)
{
/*
* LITTLE CPU에서 실행 중인 태스크의 util이
* CPU capacity를 초과하면 misfit 플래그 설정
* → active balancer가 big CPU로 이주
*/
if (!task_fits_cpu(p, cpu_of(rq))) {
rq->misfit_task_load = max_t(unsigned long,
task_util_est(p), 1);
} else {
rq->misfit_task_load = 0;
}
}
/* sched_domain 플래그: SD_ASYM_CPUCAPACITY
* 이 플래그가 설정되면 로드 밸런서가
* misfit 태스크를 능동적으로 이주시킴 */
taskset이나 cpuset으로 태스크를 E-core/LITTLE에 고정하면 EAS/misfit 마이그레이션이 작동하지 않습니다. (2) schedutil 외의 cpufreq governor를 사용하면 EAS가 비활성화됩니다. (3) VM 환경에서는 호스트의 이기종 토폴로지가 게스트에 노출되지 않아 EAS가 작동하지 않을 수 있습니다. (4) 에너지 모델이 실제 하드웨어와 일치하지 않으면 오히려 성능/전력 모두 악화될 수 있으므로 DT/ACPI 데이터 정확성이 중요합니다.
NUMA 밸런싱 (Automatic NUMA Balancing)
NUMA(Non-Uniform Memory Access) 시스템에서 메모리 접근 지연은 CPU와 메모리의 거리에 따라 달라집니다. 원격 노드 메모리 접근은 로컬 대비 1.5~3배 느립니다. Linux 커널은 Automatic NUMA Balancing을 통해 태스크를 메모리가 있는 노드로 이주하거나, 메모리를 태스크가 있는 노드로 이동합니다.
NUMA 밸런싱 메커니즘
/* kernel/sched/fair.c — NUMA 밸런싱 핵심 (간략화) */
/* NUMA 폴트 기록 구조 */
struct task_struct {
/* ... */
int numa_preferred_nid; /* 선호 NUMA 노드 */
unsigned long total_numa_faults; /* 총 NUMA 폴트 수 */
/*
* numa_faults[node][type]:
* type 0 = 개인(private) 접근 폴트
* type 1 = 공유(shared) 접근 폴트
* 개인 접근이 많으면 태스크 이주 유리
* 공유 접근이 많으면 페이지 이주 유리
*/
unsigned long *numa_faults;
/* NUMA 스캔 상태 */
unsigned long numa_scan_seq; /* 스캔 시퀀스 번호 */
unsigned long numa_scan_period; /* 스캔 주기 (ms) */
unsigned long numa_scan_offset; /* 현재 스캔 위치 */
};
/* NUMA 선호 노드 결정 */
static void task_numa_placement(struct task_struct *p)
{
int max_nid = NUMA_NO_NODE;
unsigned long max_faults = 0;
/* 각 NUMA 노드별 폴트 수 비교 */
for_each_online_node(nid) {
unsigned long faults = p->numa_faults[nid];
if (faults > max_faults) {
max_faults = faults;
max_nid = nid;
}
}
/* 선호 노드 변경 (이전과 다르면) */
if (max_nid != p->numa_preferred_nid) {
p->numa_preferred_nid = max_nid;
/* select_task_rq_fair()에서 이 노드 우선 선택 */
}
}
# NUMA 밸런싱 상태 확인 및 튜닝
# 활성화 상태 확인 (1=활성)
cat /proc/sys/kernel/numa_balancing
# 1
# 스캔 주기 범위 (ms)
cat /proc/sys/kernel/numa_balancing_scan_delay_ms # 1000 (초기 지연)
cat /proc/sys/kernel/numa_balancing_scan_period_min_ms # 1000 (최소 주기)
cat /proc/sys/kernel/numa_balancing_scan_period_max_ms # 60000 (최대 주기)
cat /proc/sys/kernel/numa_balancing_scan_size_mb # 256 (한 번에 스캔할 크기)
# 프로세스별 NUMA 통계 확인
cat /proc/<pid>/numa_maps
# N0=1024 N1=256 ← 노드별 페이지 분포
# preferred=0 ← 선호 노드
# numastat으로 시스템 전체 NUMA 상태
numastat -p <pid>
# Per-node process memory usage (in MBs)
# Node 0 Node 1 Total
# Heap 512.0 128.0 640.0
# Stack 4.0 0.0 4.0
# NUMA 밸런싱 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/migrate/mm_numa_migrate_ratelimited/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_swap_numa/enable
numactl --membind로 메모리를 특정 노드에 고정하고 자동 밸런싱 비활성화가 유리할 수 있습니다. (2) 빈번한 NUMA 이주(ping-pong)가 관측되면 scan_period_min_ms를 늘려 스캔 빈도를 줄이세요. (3) numa_balancing_promote_rate_limit_MBps(v6.1+)로 페이지 승격 속도를 제한할 수 있습니다. (4) numastat -m으로 노드간 메모리 분포를 확인하고, numa_miss가 높은 노드를 식별하세요.
sched_ext (BPF 기반 확장 스케줄링, v6.12+)
sched_ext는 커널 v6.12에서 도입된 BPF 기반 확장 가능 스케줄링 프레임워크입니다. 커널을 재컴파일하지 않고도 BPF 프로그램을 통해 스케줄링 정책을 커스터마이징할 수 있습니다. ext_sched_class는 fair와 idle 사이에 위치합니다.
아키텍처
/* sched_ext 클래스 위치 (우선순위 순) */
/*
* stop_sched_class (최고)
* dl_sched_class
* rt_sched_class
* fair_sched_class
* ext_sched_class ← sched_ext (v6.12+)
* idle_sched_class (최저)
*/
/* include/linux/sched/ext.h — sched_ext_ops 구조체 (주요 콜백) */
struct sched_ext_ops {
/* 태스크가 런큐에 삽입될 때 */
void (*enqueue)(struct task_struct *p, u64 enq_flags);
/* 태스크가 런큐에서 제거될 때 */
void (*dequeue)(struct task_struct *p, u64 deq_flags);
/* 다음 실행 태스크 선택 (핵심 콜백) */
struct task_struct *(*dispatch)(s32 cpu, struct task_struct *prev);
/* 태스크가 실행을 시작/종료할 때 */
void (*running)(struct task_struct *p);
void (*stopping)(struct task_struct *p, bool runnable);
/* CPU 선택 (wakeup 시) */
s32 (*select_cpu)(struct task_struct *p, s32 prev_cpu, u64 wake_flags);
/* 초기화/종료 */
s32 (*init)(void);
void (*exit)(struct scx_exit_info *info);
/* 스케줄러 이름 */
char name[SCX_OPS_NAME_LEN];
};
sched_ext BPF 스케줄러 예제
/* 간단한 sched_ext BPF 스케줄러 예제 (scx_simple) */
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
/* 글로벌 FIFO 디스패치 큐 사용 */
s32 BPF_STRUCT_OPS(simple_select_cpu,
struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
bool is_idle = false;
s32 cpu;
/* idle CPU가 있으면 직접 디스패치 (fast path) */
cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &is_idle);
if (is_idle) {
scx_bpf_dispatch(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);
}
return cpu;
}
void BPF_STRUCT_OPS(simple_enqueue,
struct task_struct *p, u64 enq_flags)
{
/* 글로벌 FIFO 큐에 삽입 */
scx_bpf_dispatch(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
}
SEC(".struct_ops.link")
struct sched_ext_ops simple_ops = {
.select_cpu = (void *)simple_select_cpu,
.enqueue = (void *)simple_enqueue,
.name = "simple",
};
# sched_ext 스케줄러 로드/관리
# scx_simple 스케줄러 로드 (커널 tools/sched_ext/에 포함)
sudo ./scx_simple
# 현재 활성 sched_ext 스케줄러 확인
cat /sys/kernel/sched_ext/root/ops
# 스케줄러 통계
cat /sys/kernel/sched_ext/root/stats
# sched_ext 비활성화 (fair로 복귀)
# → 스케줄러 프로세스를 종료하면 자동으로 fair 복귀
sched_ext 이벤트 리포팅 (v6.15+)
커널 6.15에서 sched_ext에 내부 이벤트 카운팅 및 리포팅 기능이 추가되었습니다. 이를 통해 BPF 스케줄러가 런타임에 발생하는 주요 이벤트를 추적하고 성능 분석에 활용할 수 있습니다.
# sched_ext 이벤트 카운터 확인 (v6.15+)
cat /sys/kernel/sched_ext/root/events
# 출력 예시:
# select_cpu_fallback: 1284
# dispatch_local_dsq_offline: 0
# dispatch_global_dsq_nr_exiting: 42
# bypass_activate: 0
# bypass_deactivate: 0
선점 모델 (Preemption Models)
Linux 커널의 선점(preemption)은 커널 코드 실행 중 더 높은 우선순위의 태스크로 전환할 수 있는 메커니즘입니다. 유저 공간은 항상 선점 가능하며, 커널 선점 가능 여부는 빌드 시 또는 CONFIG_PREEMPT_DYNAMIC으로 런타임에 설정합니다.
| 모델 | 커널 선점 | 지연 | 처리량 | 용도 |
|---|---|---|---|---|
PREEMPT_NONE | 불가 | 높음 (ms) | 최대 | 서버, HPC |
PREEMPT_VOLUNTARY | 명시적 양보점 | 중간 | 높음 | 범용 서버 |
PREEMPT (FULL) | spin_unlock 시 | 낮음 (μs) | 중간 | 데스크톱, 임베디드 |
PREEMPT_RT | 거의 항상 | 최소 (~μs) | 낮음 | 실시간 시스템 |
PREEMPT_LAZY | 지연 선점 | 낮음 | 높음 | v6.13+ 기본 후보 |
TIF_NEED_RESCHED — 선점 요청 메커니즘
선점의 핵심은 TIF_NEED_RESCHED 플래그입니다. 이 플래그는 "현재 태스크가 CPU를 양보해야 합니다"는 신호로, 다양한 경로에서 설정되고 검사됩니다.
/* include/linux/preempt.h — 선점 관련 핵심 매크로 */
/* 선점 비활성화/활성화 (중첩 가능) */
#define preempt_disable() \
do { \
preempt_count_inc(); \ /* PREEMPT_MASK++ */
barrier(); \
} while (0)
#define preempt_enable() \
do { \
barrier(); \
if (unlikely(preempt_count_dec_and_test())) \
__preempt_schedule(); \ /* count==0 && NEED_RESCHED → schedule() */
} while (0)
/* kernel/sched/core.c — resched_curr(): 선점 요청 설정 */
void resched_curr(struct rq *rq)
{
struct task_struct *curr = rq->curr;
if (test_tsk_need_resched(curr))
return; /* 이미 설정됨 */
/* thread_info->flags에 TIF_NEED_RESCHED 비트 설정 */
set_tsk_need_resched(curr);
/* 원격 CPU의 경우 IPI로 즉시 통보 (idle 상태 깨우기) */
if (cpu != smp_processor_id())
smp_send_reschedule(cpu);
}
선점 모델별 동작 상세
/* kernel/sched/core.c — PREEMPT_LAZY: 이중 NEED_RESCHED (v6.13+) */
/*
* PREEMPT_LAZY는 두 가지 resched 플래그를 사용:
*
* TIF_NEED_RESCHED — 즉시 선점 필요 (RT, DL 등 긴급)
* TIF_NEED_RESCHED_LAZY — 지연 선점 (fair 클래스, 유저 복귀 시 처리)
*
* RT 태스크가 깨어나면 → TIF_NEED_RESCHED (즉시)
* fair 태스크의 vruntime 만료 → TIF_NEED_RESCHED_LAZY (지연)
* 유저 복귀 시 둘 다 검사
*/
/* resched_curr()에서 긴급도에 따라 분기 */
static void set_nr_if_polling(struct task_struct *p)
{
if (is_idle_task(p)) {
/* idle 태스크는 항상 즉시 깨움 */
set_tsk_need_resched(p);
}
}
/* PREEMPT_DYNAMIC: 런타임 선점 모드 전환 (v5.12+) */
/*
* 부트 파라미터: preempt=none|voluntary|full|lazy
*
* 커널 빌드 시 CONFIG_PREEMPT_DYNAMIC=y이면
* 런타임에 선점 모델 변경 가능 (재부팅 없이)
*
* 내부적으로 static_call/static_key를 사용하여
* cond_resched(), preempt_schedule() 등의 동작을 전환
*/
# PREEMPT_DYNAMIC: 런타임 선점 모드 확인 및 변경
# 현재 선점 모드 확인
cat /sys/kernel/debug/sched/preempt
# 출력 예: full (PREEMPT)
# 부트 파라미터로 선점 모드 지정
# GRUB: preempt=none → 서버 최적화
# GRUB: preempt=voluntary → 범용
# GRUB: preempt=full → 데스크톱/임베디드
# GRUB: preempt=lazy → v6.13+ 새 기본 후보
# 선점 관련 커널 설정 확인
zcat /proc/config.gz | grep -i preempt
# CONFIG_PREEMPT_DYNAMIC=y
# CONFIG_PREEMPT_BUILD=y
스케줄러 디버깅 (Scheduler Debugging)
스케줄러의 동작을 분석하고 성능 문제를 진단하기 위한 다양한 도구와 인터페이스가 있습니다.
/proc/sched_debug
# 전체 스케줄러 상태 덤프
cat /proc/sched_debug
# 출력 내용:
# - 글로벌 스케줄러 파라미터 (sched_latency, min_granularity 등)
# - per-CPU 런큐 상태 (nr_running, load, curr 태스크 등)
# - 각 CPU의 CFS 런큐 트리 (vruntime, deadline, weight 등)
# - RT 런큐 상태
# 특정 프로세스의 스케줄링 정보
cat /proc/<pid>/sched
# 출력 예:
# se.vruntime : 12345.678901
# se.sum_exec_runtime : 987654.321098
# se.nr_migrations : 42
# nr_switches : 1234
# nr_voluntary_switches : 987
# nr_involuntary_switches : 247
# prio : 120
# policy : 0 (SCHED_NORMAL)
perf sched
# 스케줄링 이벤트 기록 (10초간)
sudo perf sched record -- sleep 10
# 스케줄링 지연 분석 (wakeup → actually running)
sudo perf sched latency
# CPU별 타임라인 (ASCII art)
sudo perf sched map
# 스케줄링 통계 요약
sudo perf sched timehist
# 각 컨텍스트 스위치의 상세 타임스탬프, wait/run 시간 출력
# 스케줄링 이벤트 기반 통계
sudo perf stat -e 'sched:sched_switch,sched:sched_wakeup' -- sleep 5
perf sched latency 출력 예시:
| Task | Runtime (ms) | Switches | Avg delay (ms) |
|---|---|---|---|
| kworker/0:1-mm_per | 0.293 | 12 | 0.012 |
| bash:1234 | 125.432 | 89 | 0.845 |
perf sched map 해석 예시: 시간축에서 *가 표시된 시점의 CPU별 실행 태스크를 보여줍니다. 예를 들어 0: migration/0, 1: ksoftirqd/1, 2: bash:1234처럼 CPU별 활성 태스크를 매칭해 확인합니다.
ftrace 스케줄러 트레이싱
# ftrace로 스케줄러 이벤트 트레이싱
cd /sys/kernel/debug/tracing
# 사용 가능한 스케줄러 이벤트 확인
ls events/sched/
# sched_switch sched_wakeup sched_wakeup_new sched_migrate_task
# sched_process_fork sched_process_exec sched_process_exit
# sched_stat_wait sched_stat_sleep sched_stat_runtime
# 스케줄러 스위치 이벤트 활성화
echo 1 > events/sched/sched_switch/enable
echo 1 > events/sched/sched_wakeup/enable
# 트레이싱 시작
echo 1 > tracing_on
sleep 2
echo 0 > tracing_on
# 결과 확인
cat trace
# 출력 예:
# bash-1234 [002] 1234.567890: sched_switch: prev_comm=bash prev_pid=1234
# prev_prio=120 prev_state=S ==> next_comm=kworker/2:1 next_pid=567
# next_prio=120
#
# <idle>-0 [001] 1234.567895: sched_wakeup: comm=bash pid=1234
# prio=120 target_cpu=002
# 특정 프로세스의 wakeup 지연 측정 (wakeup latency tracer)
echo wakeup > current_tracer
echo 1 > tracing_on
# ... 워크로드 실행 ...
echo 0 > tracing_on
cat trace
# 최대 wakeup 지연 시간 표시
스케줄러 디버그 기능 (sched_features)
# 현재 활성화된 스케줄러 기능 확인
cat /sys/kernel/debug/sched/features
# 출력 예:
# GENTLE_FAIR_SLEEPERS (슬리퍼에 대한 보상 제한)
# START_DEBIT (새 태스크에 vruntime 불이익)
# NEXT_BUDDY (wakeup 시 next 힌트)
# LAST_BUDDY (yield 시 last 힌트)
# CACHE_HOT_BUDDY (캐시 친화적 wakeup)
# WAKEUP_PREEMPTION (wakeup 시 선점 허용)
# PLACE_LAG (EEVDF: lag 기반 배치)
# PLACE_DEADLINE_INITIAL (EEVDF: 초기 deadline 설정)
# 기능 토글 (런타임)
echo WAKEUP_PREEMPTION > /sys/kernel/debug/sched/features # 활성화
echo NO_WAKEUP_PREEMPTION > /sys/kernel/debug/sched/features # 비활성화
schedstat 통계
# schedstat 활성화 (CONFIG_SCHEDSTATS 필요)
echo 1 > /proc/sys/kernel/sched_schedstats
# CPU별 스케줄링 통계
cat /proc/schedstat
# cpu<N> <domain> <9개 필드>
# 필드: yld_count, sched_switch, sched_goidle, ttwu_count,
# ttwu_local, rq_cpu_time, run_delay, pcount
# 프로세스별 스케줄링 통계
cat /proc/<pid>/schedstat
# 3개 필드: run_time(ns) wait_time(ns) nr_timeslices
# 모든 CPU의 로드 밸런싱 통계 확인
cat /proc/schedstat | grep domain
디버깅 도구 요약
| 도구 | 용도 | 오버헤드 |
|---|---|---|
/proc/sched_debug |
스케줄러 전체 상태 스냅샷 | 낮음 (읽기만) |
/proc/<pid>/sched |
개별 프로세스 스케줄링 정보 | 낮음 |
perf sched |
스케줄링 지연/타임라인 분석 | 중간 |
ftrace sched events |
상세 스케줄링 이벤트 트레이싱 | 중간~높음 |
schedstat |
스케줄링 통계 집계 | 낮음 |
sched_features |
스케줄러 동작 런타임 튜닝 | 없음 |
trace-cmd |
ftrace 프론트엔드 (기록/분석) | 중간 |
kernelshark |
trace-cmd 데이터 GUI 시각화 | 없음 (오프라인) |
주요 커널 설정 옵션
| 설정 | 설명 | 기본값 |
|---|---|---|
CONFIG_PREEMPT_NONE |
서버용 비선점(Non-preemptive) 커널 | - |
CONFIG_PREEMPT_VOLUNTARY |
데스크톱 자발적 선점 | 대부분의 배포판 기본 |
CONFIG_PREEMPT |
저지연 선점형 커널 | - |
CONFIG_PREEMPT_RT |
하드 실시간 선점 | - |
CONFIG_SCHED_EXT |
BPF sched_ext 지원 | n (v6.12+) |
CONFIG_SCHEDSTATS |
스케줄링 통계 수집 | n |
CONFIG_SCHED_DEBUG |
스케줄러 디버그 인터페이스 | y |
CONFIG_NO_HZ_FULL |
단일 태스크 CPU에서 틱 제거 | n |
CONFIG_HZ |
타이머 틱 빈도 (100/250/300/1000) | 250 |
CONFIG_NUMA_BALANCING |
NUMA 자동 메모리/태스크 밸런싱 | y (NUMA 시스템) |
CONFIG_CGROUP_SCHED |
cgroup 기반 스케줄링 지원 | y |
CONFIG_FAIR_GROUP_SCHED |
CFS/EEVDF 그룹 스케줄링 | y |
CONFIG_RT_GROUP_SCHED |
RT 그룹 스케줄링 | n |
sysctl 튜닝 파라미터
# === CFS/EEVDF 파라미터 ===
# 타겟 레이턴시 (ns): 태스크들이 한 번씩 실행되는 목표 주기
sysctl kernel.sched_latency_ns=6000000 # 6ms (기본)
# 최소 그래뉼래리티 (ns): 태스크당 최소 실행 시간
sysctl kernel.sched_min_granularity_ns=750000 # 0.75ms (기본)
# EEVDF 기본 슬라이스 (ns, v6.6+)
sysctl kernel.sched_base_slice_ns=3000000 # 3ms (기본)
# 웨이크업 그래뉼래리티: 깨어난 태스크의 선점 임계값
sysctl kernel.sched_wakeup_granularity_ns=1000000 # 1ms (기본)
# === RT 파라미터 ===
# RT 대역폭 제한 (us)
sysctl kernel.sched_rt_period_us=1000000 # 1초 주기
sysctl kernel.sched_rt_runtime_us=950000 # 주기당 최대 950ms
# === 마이그레이션 비용 ===
sysctl kernel.sched_migration_cost_ns=500000 # 0.5ms (기본)
# 이 시간 이내에 실행된 태스크는 "캐시 핫"으로 간주, 마이그레이션 억제
# === CPU Affinity (명령줄) ===
taskset -c 0,1 ./my_app # CPU 0,1에만 바인딩
taskset -p -c 2-5 1234 # PID 1234를 CPU 2-5로 변경
# === isolcpus (커널 부트 파라미터) ===
# 특정 CPU를 스케줄러에서 격리 (RT 워크로드 전용)
# GRUB: isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3
sched_latency_ns 줄이기, CONFIG_PREEMPT 사용. (2) 서버/배치 워크로드: sched_min_granularity_ns 늘리기, CONFIG_PREEMPT_NONE 사용. (3) RT 워크로드: isolcpus + SCHED_FIFO + CONFIG_PREEMPT_RT. (4) 컨테이너(Container) 환경: cgroup cpu.max로 대역폭 제한, CONFIG_FAIR_GROUP_SCHED 필수.
그룹 스케줄링 (Group Scheduling & cgroup CPU)
Linux는 cgroup(Control Group)을 통해 프로세스 그룹 단위로 CPU 자원을 제어합니다. CONFIG_FAIR_GROUP_SCHED가 활성화되면 CFS/EEVDF 스케줄러는 태스크 개별이 아닌 그룹 단위로 공정성을 보장합니다. 컨테이너, 가상화(Virtualization), 멀티테넌트(Multi-tenant) 환경의 핵심 기술입니다.
계층적 CFS 런큐 구조
그룹 스케줄링이 활성화되면 각 cgroup은 자체 cfs_rq를 가지며, 상위 그룹의 cfs_rq에 sched_entity로 등록됩니다. 이 계층 구조를 통해 그룹 간 공정성이 보장됩니다.
cgroup v2 CPU 컨트롤러
cgroup v2에서 CPU 자원 제어는 cpu 컨트롤러를 통해 수행됩니다. 두 가지 주요 인터페이스가 있습니다:
| 파일 | 형식 | 설명 | 예시 |
|---|---|---|---|
cpu.weight |
1-10000 | CFS 가중치 (기본 100). 그룹 간 CPU 비례 분배 | echo 200 > cpu.weight — 기본의 2배 |
cpu.max |
quota period | 대역폭 제한. period(μs) 중 최대 quota(μs)만 실행 | echo "200000 1000000" > cpu.max — 20% |
cpu.max.burst |
0-∞ (μs) | 미사용 quota 누적 허용량 (버스(Bus)트 허용) | echo 100000 > cpu.max.burst |
cpu.pressure |
읽기 전용(Read-Only) | PSI(Pressure Stall Information) CPU 압력 | some avg10=0.00 avg60=0.00 |
cpu.stat |
읽기 전용 | 사용/대기/쓰로틀링 시간 통계 | usage_usec, nr_throttled 등 |
# === cgroup v2 CPU 제어 실전 설정 ===
# 1. 웹서버 그룹: CPU 가중치 높게 + 대역폭 무제한
mkdir -p /sys/fs/cgroup/webserver
echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control
echo 500 > /sys/fs/cgroup/webserver/cpu.weight # 기본의 5배
echo "max 100000" > /sys/fs/cgroup/webserver/cpu.max # 무제한
echo $$ > /sys/fs/cgroup/webserver/cgroup.procs # 현재 셸 이동
# 2. 배치 작업 그룹: 가중치 낮게 + CPU 30% 제한
mkdir -p /sys/fs/cgroup/batch
echo 50 > /sys/fs/cgroup/batch/cpu.weight # 기본의 절반
echo "300000 1000000" > /sys/fs/cgroup/batch/cpu.max # 30% 상한
echo 100000 > /sys/fs/cgroup/batch/cpu.max.burst # 100ms 버스트 허용
# 3. 통계 확인
cat /sys/fs/cgroup/batch/cpu.stat
# usage_usec 12345678 ← 총 CPU 사용 시간
# user_usec 10000000
# system_usec 2345678
# nr_periods 1000 ← 대역폭 주기 수
# nr_throttled 42 ← 쓰로틀링 발생 횟수
# throttled_usec 5000000 ← 총 쓰로틀링 시간
# nr_bursts 10 ← 버스트 사용 횟수
# burst_usec 500000 ← 총 버스트 시간
# 4. PSI(CPU 압력) 확인
cat /sys/fs/cgroup/batch/cpu.pressure
# some avg10=5.23 avg60=3.10 avg300=1.50 total=98765432
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
CFS 대역폭 제어 (CFS Bandwidth Control)
cpu.max의 내부 구현인 CFS 대역폭 제어는 각 그룹에 주기(period)당 최대 할당량(quota)을 설정합니다. quota를 소진하면 그룹의 모든 태스크가 다음 주기까지 쓰로틀링됩니다.
/* kernel/sched/fair.c — CFS 대역폭 제어 (간략화) */
struct cfs_bandwidth {
ktime_t period; /* 대역폭 주기 (기본 100ms) */
u64 quota; /* 주기당 최대 실행 시간 (ns) */
u64 runtime; /* 남은 실행 시간 */
u64 burst; /* 미사용 quota 누적 허용량 */
s64 hierarchical_quota; /* 계층 반영 실효 quota */
u8 idle; /* 그룹 비활성 상태 */
u8 period_active; /* 주기 타이머 활성 */
struct hrtimer period_timer; /* 주기 만료 타이머 */
struct hrtimer slack_timer; /* 미사용 quota 회수 */
int nr_periods; /* 총 주기 수 (통계) */
int nr_throttled; /* 쓰로틀링 발생 수 */
u64 throttled_time; /* 총 쓰로틀링 시간 */
};
/* 쓰로틀링 판단: runtime 소진 시 cfs_rq 비활성화 */
static bool throttle_cfs_rq(struct cfs_rq *cfs_rq)
{
/* cfs_rq의 모든 태스크를 런큐에서 제거 */
walk_tg_tree_from(cfs_rq->tg, tg_throttle_down, ...);
cfs_rq->throttled = 1;
cfs_rq->throttled_clock = rq_clock(rq);
return true;
}
/* 새 주기 시작 시 quota 보충 + 쓰로틀링 해제 */
static int do_sched_cfs_period_timer(struct cfs_bandwidth *cfs_b)
{
cfs_b->runtime = cfs_b->quota; /* quota 보충 */
distribute_cfs_runtime(cfs_b); /* 쓰로틀된 런큐에 분배 */
unthrottle_cfs_rq(cfs_rq); /* 태스크 재활성화 */
return 0;
}
nr_throttled가 높으면 quota 증가를 검토하세요. (2) 멀티스레드 애플리케이션은 모든 스레드(Thread)가 quota를 공유하므로, 스레드 수 × 필요 CPU 시간을 고려해야 합니다. (3) cpu.max.burst를 설정하면 유휴 기간에 축적된 quota를 버스트 시 사용할 수 있어 일시적 부하 처리에 유리합니다. (4) Kubernetes에서 CPU limit은 cpu.max로 구현되며, limit이 너무 낮으면 쓰로틀링으로 인한 성능 저하가 발생합니다.
__schedule() 핵심 경로
커널 스케줄링의 진입점은 schedule() 함수이며, 실제 스케줄링 결정은 __schedule()에서 이루어집니다. 이 함수는 kernel/sched/core.c에 정의되어 있으며, 다음 태스크를 선택하고 컨텍스트 스위치를 수행하는 전체 과정을 통제합니다.
호출 체인 (Call Chain)
스케줄링이 발생하는 전체 호출 경로는 다음과 같습니다:
/* kernel/sched/core.c — 간략화 */
schedule()
└─→ __schedule(SM_NONE)
├─→ local_irq_disable() /* 인터럽트 비활성화 */
├─→ rq_lock(rq) /* 런큐 락 획득 */
├─→ pick_next_task(rq, prev) /* 다음 태스크 선택 */
│ ├─→ [최적화] fair만 있으면 pick_next_task_fair() 직행
│ └─→ [일반] for_each_class(class) → class->pick_next_task()
├─→ context_switch(rq, prev, next)
│ ├─→ switch_mm_irqs_off() /* 주소 공간 전환 */
│ └─→ switch_to(prev, next, prev) /* 레지스터/스택 전환 */
└─→ rq_unlock(rq) + local_irq_enable()
__schedule() 핵심 코드
/* kernel/sched/core.c — 간략화된 __schedule() */
static void __schedule(unsigned int sched_mode)
{
struct task_struct *prev, *next;
struct rq *rq;
int cpu;
cpu = smp_processor_id();
rq = cpu_rq(cpu);
prev = rq->curr;
/* 1. 이전 태스크 상태 처리 */
if (!(sched_mode & SM_MASK_PREEMPT) &&
prev_state(prev)) {
if (signal_pending_state(prev_state, prev)) {
WRITE_ONCE(prev->__state, TASK_RUNNING);
} else {
deactivate_task(rq, prev, DEQUEUE_SLEEP);
}
}
/* 2. 다음 태스크 선택 */
next = pick_next_task(rq, prev, &rf);
/* 3. need_resched 플래그 초기화 */
clear_tsk_need_resched(prev);
/* 4. 태스크가 변경되었으면 컨텍스트 스위치 */
if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
context_switch(rq, prev, next, &rf);
} else {
rq_unlock_irq(rq, &rf);
}
}
코드 설명
- 9–11행현재 CPU 번호를 가져오고, 해당 CPU의 런큐(runqueue)와 현재 실행 중인 태스크를 저장합니다.
- 14–20행선점이 아닌 자발적 스케줄링에서 이전 태스크의 상태를 확인합니다. 시그널(Signal)이 있으면 TASK_RUNNING 상태로 복원하고, 없으면
deactivate_task()로 런큐에서 제거합니다. - 23행
pick_next_task()가 sched_class 우선순위에 따라 다음 실행할 태스크를 결정합니다. - 26행
TIF_NEED_RESCHED플래그를 초기화하여 불필요한 재스케줄링을 방지합니다. - 29–33행선택된 다음 태스크가 현재 태스크와 다르면
context_switch()를 호출하여 실제 전환을 수행합니다. 동일하면 잠금을 해제하고 그대로 실행을 계속합니다.
schedule()에서 호출하면 SM_NONE(자발적), preempt_schedule()에서 호출하면 SM_PREEMPT(강제 선점)입니다. 선점 모드에서는 이전 태스크를 런큐에서 제거하지 않고 그대로 유지합니다.
struct sched_entity 심층 분석
struct sched_entity는 CFS/EEVDF 스케줄러에서 각 태스크(또는 태스크 그룹)의 스케줄링 상태를 추적하는 핵심 구조체(Struct)입니다. task_struct에 임베딩되어 있으며, 가상 런타임, 부하 가중치, 실행 통계 등을 관리합니다.
주요 필드 분석
| 필드 | 타입 | 용도 |
|---|---|---|
load |
struct load_weight |
태스크의 가중치(weight). nice 값에서 변환되어 CPU 시간 비율을 결정 |
run_node |
struct rb_node |
CFS Red-Black 트리의 노드. vruntime(또는 deadline) 기준 정렬 |
group_node |
struct list_head |
태스크 그룹 내 엔티티 리스트 연결 |
on_rq |
unsigned int |
런큐 등록 여부 (0: 미등록, 1: 등록) |
exec_start |
u64 |
현재 실행 구간의 시작 시각 (나노초) |
sum_exec_runtime |
u64 |
총 누적 실행 시간 (나노초) |
vruntime |
u64 |
가상 런타임. CFS에서 태스크 선택의 핵심 키 |
prev_sum_exec_runtime |
u64 |
이전 스케줄링 시점의 누적 실행 시간. 슬라이스 소진 여부 판단에 사용 |
nr_migrations |
u64 |
CPU 간 마이그레이션 횟수 (디버깅/통계) |
deadline |
u64 |
EEVDF의 가상 데드라인. vruntime + slice/weight로 계산 |
min_vruntime |
u64 |
엔티티가 참조하는 최소 vruntime (그룹 스케줄링에서 사용) |
vlag |
s64 |
EEVDF의 lag 값. 양수면 자원 부족(eligible), 음수면 과다 사용 |
struct sched_class 핵심 오퍼레이션
sched_class는 스케줄링 정책별 동작을 정의하는 함수 포인터 테이블입니다. 각 정책(fair, rt, dl, idle)은 자체 sched_class 인스턴스를 제공합니다.
| 오퍼레이션 | 호출 시점 | 역할 |
|---|---|---|
enqueue_task |
태스크가 실행 가능 상태로 전환될 때 | 런큐(RB-tree/리스트)에 태스크 삽입 |
dequeue_task |
태스크가 sleep/exit할 때 | 런큐에서 태스크 제거 |
yield_task |
sched_yield() 시스템 콜 |
자발적으로 CPU 양보 |
check_preempt_curr |
새 태스크가 깨어날 때 | 현재 태스크 선점 여부 판단 |
pick_next_task |
__schedule() 내부 |
다음 실행할 태스크 선택 |
put_prev_task |
현재 태스크를 내려놓을 때 | 이전 태스크 정리 (통계 갱신, 트리 재삽입) |
set_next_task |
다음 태스크가 선택된 직후 | 선택된 태스크 초기 설정 (exec_start 갱신 등) |
task_tick |
매 타이머 틱(Timer Tick)마다 | vruntime 갱신, 슬라이스 만료 확인, 선점 판단 |
sched_entity 코드 구조
/* include/linux/sched.h — 핵심 필드만 발췌 */
struct sched_entity {
struct load_weight load; /* nice → weight 변환 */
struct rb_node run_node; /* RB-tree 노드 */
unsigned int on_rq; /* 런큐 등록 여부 */
u64 exec_start; /* 현재 실행 시작 시각 */
u64 sum_exec_runtime; /* 총 누적 실행 시간 */
u64 prev_sum_exec_runtime; /* 이전 시점 누적 */
u64 vruntime; /* 가상 런타임 (CFS 키) */
/* EEVDF 관련 (v6.6+) */
u64 deadline; /* 가상 데드라인 */
u64 min_vruntime; /* 참조 최소 vruntime */
s64 vlag; /* lag 값: 자원 편향 */
u64 nr_migrations; /* 마이그레이션 횟수 */
#ifdef CONFIG_FAIR_GROUP_SCHED
struct sched_entity *parent; /* 그룹 계층 부모 */
struct cfs_rq *cfs_rq; /* 소속 cfs_rq */
struct cfs_rq *my_q; /* 소유 cfs_rq (그룹) */
#endif
};
코드 설명
- 3행
load는 nice 값을 가중치(weight)로 변환한 값입니다. nice 0은 weight 1024, nice -20은 88761로 CPU 시간 배분 비율을 결정합니다. - 4행
run_node는 CFS의 Red-Black 트리에 삽입되는 노드입니다. vruntime(EEVDF에서는 deadline)을 키로 정렬됩니다. - 10행
vruntime은 실제 실행 시간을 가중치로 나눈 가상 시간입니다. 가중치가 높을수록(nice가 낮을수록) vruntime이 느리게 증가하여 더 많은 CPU 시간을 받습니다. - 13행
deadline은 EEVDF에서vruntime + slice_length/weight로 계산되며, eligible한 엔티티 중 deadline이 가장 작은 것을 선택합니다. - 15행
vlag는 태스크가 이상적 서비스 대비 얼마나 차이가 있는지 나타냅니다. 양수면 CPU 시간이 부족한 상태(eligible), 음수면 과다 사용 상태입니다. - 19–21행그룹 스케줄링 활성화 시 계층 구조를 형성합니다.
my_q는 그룹 엔티티가 소유하는 하위 cfs_rq이고,parent는 상위 엔티티를 가리킵니다.
CFS pick_next_task_fair() 분석
pick_next_task_fair()는 CFS 런큐에서 다음 실행할 태스크를 선택하는 핵심 함수입니다. 전통적인 CFS에서는 vruntime이 가장 작은 엔티티를 선택했지만, EEVDF(v6.6+)에서는 eligible한 엔티티 중 가상 데드라인이 가장 빠른 엔티티를 선택합니다.
호출 체인
pick_next_task_fair(rq)
└─→ pick_next_entity(cfs_rq)
├─→ [CFS 레거시] __pick_first_entity() ← rb_leftmost (최소 vruntime)
└─→ [EEVDF v6.6+] pick_eevdf(cfs_rq)
├─→ __pick_first_entity() ← 최소 vruntime 후보
└─→ __pick_eevdf() ← eligible + 최소 deadline
Red-Black 트리 leftmost 선택 (레거시 CFS)
전통적인 CFS에서 __pick_first_entity()는 Red-Black 트리의 rb_leftmost 포인터를 사용하여 O(1)에 vruntime이 가장 작은 엔티티를 반환합니다. rb_root_cached가 leftmost를 캐싱하므로 트리 순회가 불필요합니다.
EEVDF pick_eevdf() 로직
EEVDF에서는 단순히 vruntime이 작은 태스크를 선택하는 대신, 두 가지 조건을 동시에 만족하는 태스크를 선택합니다:
- Eligible (적격) —
vlag >= 0, 즉 할당받은 것보다 적게 실행한 태스크 - Earliest Deadline — eligible한 태스크 중 가상
deadline이 가장 빠른 태스크
/* kernel/sched/fair.c — 간략화된 pick_eevdf() */
static struct sched_entity *pick_eevdf(struct cfs_rq *cfs_rq)
{
struct rb_node *node = cfs_rq->tasks_timeline.rb_root.rb_node;
struct sched_entity *best = NULL;
while (node) {
struct sched_entity *se = rb_entry(node, struct sched_entity, run_node);
/* 왼쪽 서브트리에 eligible한 엔티티가 있을 수 있음 */
if (entity_eligible(cfs_rq, se)) {
/* 현재 노드가 eligible이면 best 후보 갱신 */
if (!best || deadline_gt(best, se))
best = se;
/* 왼쪽에 더 좋은 deadline이 있을 수 있으므로 계속 탐색 */
node = node->rb_left;
} else {
/* 현재 ineligible이면 오른쪽에서 eligible 탐색 */
node = node->rb_right;
}
}
if (!best)
best = __pick_first_entity(cfs_rq); /* fallback: leftmost */
return best;
}
코드 설명
- 4행RB-tree의 루트 노드부터 탐색을 시작합니다. 이 트리는 vruntime 기준으로 정렬되어 있습니다.
- 11행
entity_eligible()은vlag >= 0인지 확인합니다. 이상적 서비스보다 적게 실행된 태스크만 적격합니다. - 13–14행eligible한 노드의 deadline을 비교하여 가장 빠른(가장 작은) deadline을 가진 엔티티를 best로 갱신합니다.
- 16행eligible한 경우 왼쪽 서브트리에서 더 작은 deadline을 가진 eligible 엔티티를 찾습니다. vruntime 정렬이므로 왼쪽이 더 eligible할 가능성이 높습니다.
- 19행ineligible한 경우 오른쪽 서브트리로 이동합니다. vruntime이 더 큰 쪽에서 eligible한 엔티티를 탐색합니다.
- 23–24행eligible한 엔티티가 없으면 fallback으로 leftmost(최소 vruntime)를 선택합니다. 이는 모든 태스크가 과다 실행 상태일 때 발생합니다.
Context Switch 내부 구조
컨텍스트 스위치(Context Switch)는 CPU에서 실행 중인 태스크를 다른 태스크로 교체하는 과정입니다. 이 과정은 주소 공간(Address Space) 전환과 레지스터/스택 전환의 두 단계로 나뉘며, 커널에서 가장 성능에 민감한 경로 중 하나입니다.
호출 체인
context_switch(rq, prev, next, rf)
├─→ prepare_task_switch(rq, prev, next) /* 전환 준비 (통계, 알림) */
├─→ arch_start_context_switch(prev) /* 아키텍처 전환 시작 */
├─→ [주소 공간 전환]
│ ├─→ 커널 스레드 → 커널 스레드: 전환 불필요 (active_mm 차용)
│ ├─→ 커널 스레드 → 유저: switch_mm_irqs_off(NULL, next_mm)
│ └─→ 유저 → 유저: switch_mm_irqs_off(prev_mm, next_mm)
├─→ switch_to(prev, next, prev) /* CPU 레지스터 + 스택 전환 */
└─→ finish_task_switch(prev) /* 이전 태스크 정리 */
switch_mm_irqs_off() — 주소 공간 전환
switch_mm_irqs_off()는 페이지 테이블(Page Table) 기저 레지스터(x86: CR3, ARM64: TTBR0_EL1)를 변경하여 가상 주소 공간을 전환합니다. 이때 TLB(Translation Lookaside Buffer) 처리가 핵심입니다:
| 상황 | TLB 동작 | 설명 |
|---|---|---|
| 같은 mm | TLB flush 생략 | 동일 프로세스의 스레드 간 전환 시 주소 공간이 동일 |
| PCID/ASID 지원 | 선택적 flush | x86 PCID 또는 ARM ASID로 TLB 태깅, 전체 flush 회피 |
| PCID/ASID 미지원 | 전체 TLB flush | 모든 TLB 엔트리 무효화(Invalidation) — 성능 저하 원인 |
| 커널 스레드(Kernel Thread) | flush 불필요 | 커널 공간(Kernel Space)만 사용, 이전 mm의 유저 매핑(Mapping)을 차용(lazy TLB) |
switch_to() — 아키텍처별 레지스터 전환
switch_to()는 아키텍처에 따라 다르게 구현되며, CPU 레지스터와 스택 포인터를 물리적으로 전환합니다. 주요 작업:
- FPU/SIMD 상태 저장/복원 — x86에서
XSAVE/XRSTOR명령어로 FPU, SSE, AVX 레지스터를 전환합니다. 지연 FPU 전환(lazy FPU switching)은 최신 커널에서 제거되어, 항상 즉시 저장/복원합니다. - 스택 포인터 전환 — 커널 스택 포인터(x86: RSP, ARM64: SP_EL1)를 다음 태스크의
thread.sp로 교체합니다. - 명령어 포인터 — 다음 태스크가 마지막으로 context switch된 시점의 복귀 주소로 점프합니다.
- per-CPU 변수 갱신 —
current매크로가 다음 태스크를 가리키도록 per-CPU 포인터를 갱신합니다 (x86: GS 세그먼트 베이스).
/* kernel/sched/core.c — 간략화된 context_switch() */
static void context_switch(struct rq *rq,
struct task_struct *prev, struct task_struct *next)
{
struct mm_struct *mm = next->mm;
struct mm_struct *prev_mm = prev->active_mm;
prepare_task_switch(rq, prev, next);
/* 주소 공간 전환 */
if (!mm) {
/* next는 커널 스레드: 이전 mm을 차용 (lazy TLB) */
next->active_mm = prev_mm;
mmgrab(prev_mm);
enter_lazy_tlb(prev_mm, next);
} else {
/* next는 유저 프로세스: mm 전환 */
switch_mm_irqs_off(prev_mm, mm, next);
}
/* 아키텍처별 레지스터 + 스택 전환 */
switch_to(prev, next, prev);
/* 여기서부터는 "next" 태스크의 컨텍스트에서 실행 */
finish_task_switch(prev);
}
코드 설명
- 5–6행
next->mm이 NULL이면 커널 스레드, 아니면 유저 프로세스입니다.active_mm은 현재 활성화된 메모리 맵(Memory Map)으로, 커널 스레드도 유저 공간 페이지 테이블을 차용할 수 있습니다. - 11–15행커널 스레드로 전환할 때 주소 공간 전환 비용을 절약합니다. 이전 태스크의 mm을 차용하고
enter_lazy_tlb()로 TLB flush를 지연합니다. - 18행유저 프로세스로 전환할 때
switch_mm_irqs_off()가 CR3(또는 TTBR0)를 변경하여 새로운 가상 주소 공간을 활성화합니다. - 22행
switch_to()는 실제 CPU 레지스터와 스택을 전환합니다. 이 매크로 실행 후에는 next 태스크의 컨텍스트에서 코드가 실행됩니다. 세 번째 인자로 prev를 다시 전달하여, 깨어난 태스크가 자신이 교체한 태스크를 알 수 있게 합니다. - 25행
finish_task_switch()는 이전 태스크의 후처리를 수행합니다: 상태가 TASK_DEAD이면put_task_struct()로 자원 해제, mm_struct 참조 카운트(Reference Count) 감소 등.
perf stat -e context-switches,cs ./app으로 빈도를 측정하고, perf sched latency로 전환 지연을 분석할 수 있습니다. 일반적으로 유저→유저 전환(TLB flush 포함)이 스레드→스레드 전환(같은 mm)보다 수 배 느립니다.
Wake-up 경로
태스크 깨우기(Wakeup)(Wake-up)는 sleep 상태의 태스크를 런큐에 다시 넣고, 필요하면 현재 실행 중인 태스크를 선점하는 과정입니다. I/O 완료, 시그널 전달, 뮤텍스(Mutex) 해제 등 다양한 경로에서 호출됩니다.
호출 체인
try_to_wake_up(p, state, wake_flags)
├─→ p->__state 확인 (이미 RUNNING이면 즉시 반환)
├─→ select_task_rq(p, ...) /* 태스크를 넣을 CPU 선택 */
│ └─→ sched_class->select_task_rq()
│ └─→ [CFS] select_task_rq_fair() — 부하 기반 CPU 선택
├─→ ttwu_queue(p, cpu, ...)
│ ├─→ [로컬 CPU] ttwu_do_activate(rq, p, ...)
│ └─→ [원격 CPU] ttwu_queue_wakelist() → IPI 전송
└─→ ttwu_do_activate(rq, p, ...)
├─→ activate_task(rq, p, ...)
│ └─→ enqueue_task(rq, p, ...)
│ └─→ sched_class->enqueue_task()
├─→ WRITE_ONCE(p->__state, TASK_RUNNING)
└─→ check_preempt_curr(rq, p, ...)
└─→ sched_class->check_preempt_curr()
└─→ [CFS] resched_curr(rq) (vruntime 비교)
try_to_wake_up() 핵심 코드
/* kernel/sched/core.c — 간략화된 try_to_wake_up() */
static int try_to_wake_up(struct task_struct *p,
unsigned int state, int wake_flags)
{
unsigned long flags;
int cpu, success = 0;
raw_spin_lock_irqsave(&p->pi_lock, flags);
/* 1. 상태 확인: 이미 실행 중이면 반환 */
if (!(p->__state & state))
goto unlock;
/* 2. 태스크를 배치할 CPU 선택 */
cpu = select_task_rq(p, p->wake_cpu, wake_flags);
if (task_cpu(p) != cpu) {
/* CPU 마이그레이션 필요 */
set_task_cpu(p, cpu);
p->se.nr_migrations++;
}
/* 3. 런큐에 태스크 활성화 */
ttwu_queue(p, cpu, wake_flags);
success = 1;
unlock:
raw_spin_unlock_irqrestore(&p->pi_lock, flags);
return success;
}
코드 설명
- 8행
pi_lock(우선순위 상속(Priority Inheritance) 잠금)을 획득하여 동시에 여러 경로에서 같은 태스크를 깨우는 경합(Contention)을 방지합니다. - 11–12행태스크의 현재 상태가 요청된
state마스크와 일치하는지 확인합니다. 이미 RUNNING이거나 다른 상태이면 깨우기를 건너뜁니다. - 15행
select_task_rq()가 부하 균형, 캐시(Cache) 친화성, NUMA 거리 등을 고려하여 최적의 CPU를 선택합니다. CFS의 경우select_task_rq_fair()가 에너지 인지(EAS) 또는 로드 밸런싱 기반으로 결정합니다. - 17–20행선택된 CPU가 태스크의 현재 CPU와 다르면 마이그레이션이 발생합니다.
set_task_cpu()가 태스크의 CPU를 갱신하고,nr_migrations카운터를 증가시킵니다. - 23행
ttwu_queue()가 대상 CPU가 로컬이면 직접 런큐에 추가하고, 원격이면 IPI(Inter-Processor Interrupt)를 보내 원격 CPU에서 활성화합니다. 원격 경로는 런큐 잠금 경합(Lock Contention)을 줄이기 위해 wake list를 사용합니다.
check_preempt_curr()에서 CFS는 깨어난 태스크의 vruntime이 현재 태스크보다 충분히 작으면(차이가 sysctl_sched_wakeup_granularity보다 크면) TIF_NEED_RESCHED 플래그를 설정합니다. 이 플래그는 다음 인터럽트 복귀 또는 선점 지점에서 schedule()을 트리거합니다.
흔한 실수와 실전 트러블슈팅
스케줄러 관련 흔한 실수
| 실수 | 증상 | 원인 | 해결 |
|---|---|---|---|
| RT 태스크 무한루프 | 시스템 완전 멈춤 (watchdog timeout) | SCHED_FIFO 태스크가 yield/sleep 없이 무한 실행 | RT 쓰로틀링 유지 (sched_rt_runtime_us=950000), 코드에 sched_yield() 삽입 |
| Kubernetes CPU 쓰로틀링 | 응답 시간 급증, P99 지연 스파이크 | cpu.max(CPU limit)가 너무 낮아 CFS bandwidth throttling 발생 |
cpu.stat의 nr_throttled 확인, limit 증가 또는 cpu.max.burst 설정 |
| isolcpus + cgroup 충돌 | 격리 CPU에서 태스크 미실행 | isolcpus로 격리한 CPU가 cgroup의 cpuset에서 제외됨 |
격리 CPU를 cpuset.cpus에 명시적으로 포함 |
| NUMA 미인식 메모리 할당 | 원격 메모리 접근으로 성능 저하 | 대규모 할당 시 로컬 노드 메모리 부족 → 원격 할당 | numactl --membind 또는 mbind()로 노드 지정, NUMA 밸런싱 튜닝 |
| 과도한 컨텍스트 스위치 | CPU 사용률 높은데 처리량 낮음 | sched_min_granularity_ns 너무 작게 설정, 또는 과다한 스레드/프로세스 |
perf stat -e cs로 빈도 확인, granularity 증가, 스레드 풀 크기 조정 |
| EAS 미활성 | ARM big.LITTLE에서 에너지 효율 미최적화 | cpufreq governor가 schedutil이 아니거나 EM 미등록 |
governor를 schedutil로 변경, DT에 energy-model 추가 |
| nice 값 오해 | 기대한 CPU 시간 배분과 다름 | nice 1 차이 ≠ 균등 분배. nice 0→1은 약 10% 감소, 0→5는 약 67% 감소 | nice 가중치 테이블 참고 (비선형 지수 관계) |
| sched_ext 스케줄러 크래시 | BPF 스케줄러 에러 후 시스템 불안정 | BPF 프로그램 버그로 dispatch 실패 | 자동 fair 복귀 확인, /sys/kernel/sched_ext/root/stats에서 에러 확인 |
실전 트러블슈팅 레시피
레시피 1: "어떤 프로세스가 CPU를 독점하는지 모르겠습니다"
# 1단계: 실시간 CPU 사용률 상위 프로세스
top -b -n1 | head -20
# 2단계: 스케줄링 정책별 프로세스 확인
ps -eo pid,cls,rtprio,ni,comm --sort=-pcpu | head -20
# CLS: TS(SCHED_NORMAL), FF(FIFO), RR(RR), --(DL)
# 3단계: RT 태스크가 있는지 확인
ps -eo pid,cls,rtprio,comm | grep -E 'FF|RR'
# RT 태스크가 높은 우선순위로 독점 중이면 원인
# 4단계: cgroup 쓰로틀링 확인
for cg in $(find /sys/fs/cgroup -name cpu.stat -type f); do
throttled=$(grep nr_throttled $cg | awk '{print $2}')
[[ $throttled -gt 0 ]] && echo "$cg: nr_throttled=$throttled"
done
레시피 2: "스케줄링 지연이 높습니다 (느린 응답)"
# 1단계: 스케줄링 지연 측정
sudo perf sched record -- sleep 5
sudo perf sched latency --sort max
# Avg delay > 1ms이면 문제 가능
# 2단계: 런큐 길이 확인
sar -q 1 10
# runq-sz가 CPU 수의 2배 이상이면 과부하
# 3단계: 선점 모드 확인
cat /sys/kernel/debug/sched/preempt
# none이면 → voluntary 또는 full로 변경 검토
# 4단계: 특정 프로세스의 wakeup 지연 추적
sudo perf sched timehist -p <pid>
# wait time 컬럼 확인
# 5단계: 스케줄러 파라미터 조정
# 대화형 워크로드이면:
sysctl kernel.sched_latency_ns=4000000 # 6ms → 4ms
sysctl kernel.sched_wakeup_granularity_ns=500000 # 1ms → 0.5ms
레시피 3: "NUMA 시스템에서 성능이 안 나옵니다"
# 1단계: NUMA 메모리 분포 확인
numastat -p <pid>
# Numa_Miss, Numa_Foreign 비율이 높으면 문제
# 2단계: 태스크의 NUMA 선호 노드 확인
cat /proc/<pid>/status | grep -i numa
# Mems_allowed: 어떤 노드에서 메모리 할당 가능한지
# 3단계: NUMA 이주 이벤트 추적
perf stat -e 'migrate:mm_numa_migrate_ratelimited' -- sleep 10
# 4단계: 수동 NUMA 배치 (인메모리 DB 권장)
numactl --cpunodebind=0 --membind=0 ./my_database
# CPU와 메모리를 같은 노드에 고정
레시피 4: "컨테이너에서 CPU 쓰로틀링 진단"
# 1단계: cgroup의 쓰로틀링 통계 확인
# (Kubernetes Pod의 cgroup 경로 찾기)
CGPATH=$(find /sys/fs/cgroup -name "*<container-id>*" -type d | head -1)
cat $CGPATH/cpu.max
# 200000 100000 ← 100ms 주기당 200ms quota (2 CPU 상당)
cat $CGPATH/cpu.stat | grep throttled
# nr_throttled 1542 ← 쓰로틀링 발생 횟수!
# throttled_usec 45678901 ← 총 쓰로틀링 시간 (45초)
# 2단계: 쓰로틀링 비율 계산
# throttled_ratio = nr_throttled / nr_periods
# 5% 이상이면 CPU limit 증가 고려
# 3단계: 버스트 허용으로 완화
echo 200000 > $CGPATH/cpu.max.burst
# 유휴 시 최대 200ms quota 축적, 버스트 시 사용
# 4단계: PSI(Pressure Stall Information) 확인
cat $CGPATH/cpu.pressure
# some avg10=15.2 ← 10초간 15.2%의 시간에서 CPU 부족
perf top으로 핫 함수 식별 → (2) perf sched latency로 스케줄링 지연 측정 → (3) /proc/<pid>/sched로 개별 프로세스 통계 → (4) ftrace sched_switch로 상세 이벤트 추적 → (5) kernelshark로 시각화 분석. 각 단계에서 병목(Bottleneck)을 좁혀가며, 스케줄러 파라미터 조정은 원인을 정확히 파악한 후에 수행합니다.
Linux 6.12 ~ 6.16 스케줄러 동향 요약
2024-2025년은 리눅스 스케줄러 역사에서 가장 큰 지각 변동이 있었던 기간입니다. sched_ext 메인라인 병합, PREEMPT_RT 공식 통합, PREEMPT_LAZY 도입, EEVDF의 지속적 개선이 동시에 진행되면서 전통적인 CFS 시대가 공식 종료되었습니다. 아래 표와 권고 사항은 운영 환경 커널 선택과 튜닝 전략 수립에 참고하세요.
| 커널 | 릴리스 | 스케줄링 관련 주요 변경 | 실무 시사점 |
|---|---|---|---|
| 6.12 (LTS) | 2024-11 | sched_ext 메인라인 병합, PREEMPT_RT 공식 통합, EEVDF DELAY_DEQUEUE 추가 | RT 전용 브랜치 유지 종료. SCX + PREEMPT_RT 조합 가능. 2026-12까지 지원 |
| 6.13 | 2025-01 | PREEMPT_LAZY 모델 메인라인, CONFIG_PREEMPT_RT가 선점 모델과 직교한 옵션으로 분리, EEVDF 엔티티 배치 lag 핫픽스 | 선점 모델을 none / voluntary / full / lazy 4종으로 정리, 서버 기본값 LAZY 검토 |
| 6.14 | 2025-03 | CFS 용어의 공식 문서 폐기, fair_sched_class는 EEVDF 알고리즘 기반으로 정리, 이종 코어(P/E) NUMA 도메인 개선 | 레거시 CFS 튜너블(sched_latency_ns 등)을 전제로 한 스크립트는 모두 재작성 필요 |
| 6.15 | 2025-05 | sched_ext 내부 이벤트 카운터 8종(SCX_EV_*) 노출, fair 계열 cgroup enqueue 정합성 개선 | BPF 스케줄러 관측성을 sysfs/tracepoint/BPF 세 경로에서 확보 |
| 6.16 | 2025-07 | sched_ext에 scx_bpf_select_cpu_and() kfunc 추가, idle CPU 선택 kfunc의 unlocked 컨텍스트 호출 허용, 다중 계층(hierarchical) SCX 기반 작업 | 토폴로지 인식 BPF 스케줄러 작성이 쉬워지고, 향후 nested SCX의 초석 마련 |
| 6.17 | 2025-09 | SMP 코드 무조건 컴파일(단일 프로세서 전용 #ifdef 제거), proxy execution 초기 구현(우선순위 역전 방지), fair scheduler lag·slice parity 개선, sched_ext cgroup 대역폭 제어 인터페이스 추가 | 스케줄러 코드베이스 단순화, RT 우선순위 역전 완화 첫 단계. sched_ext cgroup 연동 기반 마련 |
| 6.18 | 2025-11 | sched_ext에 scx_bpf_cpu_curr()·scx_bpf_locked_rq() BPF 헬퍼 추가, cgroup 동기화를 scx_cgroup_rwsem → cgroup_lock/unlock()으로 전환, 'fairer' NUMA 밸런싱 개선, migrate_enable/disable 인라인 최적화 | BPF 스케줄러가 cgroup 계층을 안전하게 조회 가능. 계층형 SCX의 기반 완성. NUMA 공정성 향상 |
| 6.19 | 2026-02 | NEXT_BUDDY 재도입(waker/wakee 캐시 친화성 기반 wakeup 선점 강화), proportional newidle balance, sched_avg fold se_weight() 누락 회귀 수정(schbench 52.4% 성능 회복), sched_ext lockless DSQ peek | 대화형 워크로드 wakeup latency 개선. 6.19 초기에 schbench 52.4% 회귀가 보고되었으며 수정 패치(Patch)가 빠르게 반영됨 |
| 7.0 | 2026-04 | RSEQ 기반 Time Slice Extension 추가(임계 구간 이탈 방지), 최신 아키텍처(x86_64, arm64, RISC-V 등)에서 선점 모델 구성이 PREEMPT_FULL/LAZY 중심으로 재정리, sched_ext GPU 인식·에너지 인식 추상화 준비 | PREEMPT_LAZY 활용 범위가 넓어지고 있으나, 이를 최신 아키텍처의 일률적 기본값으로 단정하기는 어렵습니다. 실제 기본 모델은 커널 설정과 배포판 정책을 함께 확인해야 합니다. PostgreSQL 등 스핀락 중심 워크로드에서 PREEMPT_LAZY 단독 사용 시 성능 저하가 보고되었고(Huge Pages 활성화 시 미재현), RSEQ 슬라이스 확장으로 임계 구간 선점 완화가 가능합니다. |
2026년 환경별 커널 선택 가이드
| 환경 | 권장 커널 | 선점 모델 | 추가 권고 |
|---|---|---|---|
| 일반 서버 (웹, DB, 컨테이너 호스트) | 6.12 LTS | PREEMPT_LAZY (6.13 이상 사용 시) 또는 PREEMPT_FULL | DELAY_DEQUEUE 활성 확인, cgroup v2 사용. PostgreSQL 등 스핀락(Spinlock) 집약 워크로드는 7.0 업그레이드 전 Huge Pages 활성화 여부 확인 필요 |
| 저지연 서비스 (게임 서버, 미디어) | 6.19+ | PREEMPT_LAZY + latency_nice 조정 | SCX scx_lavd 평가. 6.19의 NEXT_BUDDY 재도입으로 wakeup latency 추가 개선 |
| RT 임베디드 (산업 제어, 오디오) | 6.12 LTS (+ CONFIG_PREEMPT_RT) | PREEMPT_RT | 6.12-rt 브랜치, 2026-12 지원. 7.0에서는 PREEMPT_RT + PREEMPT_LAZY 직교 축 그대로 유지 |
| HPC / 배치 | 6.12 LTS 또는 6.18+ | 7.0 미만: PREEMPT_NONE, 7.0 이상: PREEMPT_LAZY | base_slice_ns를 3ms 이상으로 조정. 7.0+에서는 PREEMPT_LAZY가 HPC 워크로드에도 충분한 처리량 제공 확인 권장 |
| 실험 / 연구 | 최신 stable (7.0+) | PREEMPT_FULL 또는 PREEMPT_LAZY | SCX 스케줄러 교체 평가, RSEQ Time Slice Extension 활용. ML 기반 부하 분산(Load Balancing) scx_rusty 파생 스케줄러 실험 가능 |
fair_sched_class는 EEVDF 구현으로 유지됩니다. (2) 선점 모델과 RT 활성화는 6.13부터 독립 축입니다. (3) sched_ext는 실험 단계가 아니라 배포판 기본 지원에 들어간 프로덕션 기능입니다. (4) 7.0에서 PREEMPT_NONE이 최신 아키텍처에서 사실상 폐기되어 LAZY/FULL 이분화가 완성됩니다. (5) 7.0의 RSEQ Time Slice Extension은 스핀락 집약 워크로드가 임계 구간에서 선점되는 문제를 RSEQ를 통해 해결하는 새로운 접근법입니다.
Linux 7.0 선점 모델 재편 — PREEMPT_NONE 폐기 과정
Peter Zijlstra가 주도한 이 변경은 최신 아키텍처(x86_64, arm64, RISC-V, POWER, LoongArch, s390)에서 선점 모델을 PREEMPT_FULL과 PREEMPT_LAZY 두 가지로 집약합니다. PREEMPT_NONE은 선점을 전혀 지원하지 않는 구형 아키텍처로 범위가 축소되고, PREEMPT_VOLUNTARY는 PREEMPT_LAZY가 없는 아키텍처의 과도기 설정으로만 남습니다. 실무적으로는 PREEMPT_LAZY/FULL 중심으로 문서를 읽는 편이 맞습니다.
이 변경의 직접적인 배경은 PREEMPT_RT 도입 이후 cond_resched() 호출을 커널 전반에 무한 추가해야 하는 문제를 근본적으로 해결하는 것입니다. PREEMPT_LAZY를 기본 모델로 채택하면 커널이 원칙적으로 선점 가능한 상태가 되어 이런 패치워크가 불필요해집니다.
Linux 7.0 — RSEQ Time Slice Extension
약 10년간 개발된 기능이 Linux 7.0에서 마침내 메인라인에 병합되었습니다. RSEQ(Restartable Sequences) Time Slice Extension은 사용자 공간(User Space) 스레드가 임계 구간 진입 시 CPU 타임슬라이스(Time Slice)를 일시적으로 연장 요청할 수 있게 합니다. 실제 우선순위 천장(Priority Ceiling) 프로토콜의 오버헤드 없이 기회적(opportunistic) 선점 방지 효과를 얻는 것이 목표입니다.
/* RSEQ Time Slice Extension 동작 원리 (Linux 7.0+) */
/* 사용자 공간에서 임계 구간 진입 시: */
rseq_cs.start_ip = (uintptr_t)&&critical_start;
rseq_cs.post_commit_offset = &&critical_end - &&critical_start;
rseq_cs.abort_ip = (uintptr_t)&&critical_abort;
/* 슬라이스 확장 요청 (최소 5µs, 최대 50µs) */
/* 커널은 확장된 슬라이스 소진 전까지 SCHED_NORMAL 선점을 억제 */
/* 단, RT/DEADLINE 태스크는 여전히 즉시 선점 */
커널 7.0부터: RSEQ Time Slice Extension이 추가되었습니다. 기본 확장값은 5µs이며 최대 50µs까지 설정 가능합니다. 이 값을 크게 설정하면 최소 스케줄링 지연(minimum scheduling latency)에 영향을 줄 수 있으므로 측정 후 조정해야 합니다.
참고자료
- docs.kernel.org — Scheduler — 커널 공식 스케줄러 문서 인덱스입니다.
- docs.kernel.org — CFS Scheduler Design — CFS 스케줄러 설계 원리를 설명하는 공식 문서입니다.
- docs.kernel.org — Nice Design — nice 값과 가중치 설계 근거를 다룬 공식 문서입니다.
- docs.kernel.org — RT Group Scheduling — 실시간 그룹 스케줄링 설정 및 대역폭 제어 문서입니다.
- docs.kernel.org — SCHED_DEADLINE — CBS/EDF 기반 데드라인 스케줄링 공식 문서입니다.
- docs.kernel.org — Energy Aware Scheduling — 에너지 효율 스케줄링(EAS) 공식 문서입니다.
- docs.kernel.org — Scheduler Domains — 스케줄링 도메인과 로드 밸런싱 토폴로지 문서입니다.
- docs.kernel.org — CPU Capacity — 비대칭(big.LITTLE) 시스템에서의 CPU 용량 처리 문서입니다.
- docs.kernel.org — schedutil — 스케줄러 기반 CPU 주파수 거버너 문서입니다.
- docs.kernel.org — sched_ext (Extensible Scheduler Class) — BPF 프로그램으로 커스텀 스케줄러를 정의하는 sched_ext 공식 문서입니다. CONFIG 옵션, ops 콜백 인터페이스, 안전성 보장 메커니즘을 설명합니다.
- kernel/sched/core.c — Bootlin Elixir — 스케줄러 코어 구현 소스 코드입니다. schedule(), __schedule() 등 핵심 함수를 포함합니다.
- kernel/sched/fair.c — Bootlin Elixir — CFS/EEVDF 공정 스케줄러 클래스 구현 소스 코드입니다.
- kernel/sched/rt.c — Bootlin Elixir — SCHED_FIFO/SCHED_RR 실시간 스케줄러 구현 소스 코드입니다.
- kernel/sched/deadline.c — Bootlin Elixir — SCHED_DEADLINE 스케줄러 구현 소스 코드입니다.
- kernel/sched/idle.c — Bootlin Elixir — idle 스케줄러 클래스 및 CPU 유휴 진입 로직입니다.
- kernel/sched/ext.c — Bootlin Elixir — sched_ext BPF 확장 스케줄러 클래스 구현 소스 코드입니다. Linux 6.12에서 메인라인에 병합되었습니다.
- include/linux/sched.h — Bootlin Elixir — task_struct 및 스케줄러 관련 핵심 자료구조 정의입니다.
- LWN: An EEVDF CPU scheduler for Linux — EEVDF 스케줄러 도입 배경과 설계를 다룬 기사입니다.
- LWN: Completing the EEVDF scheduler — EEVDF 스케줄러 통합 완료 과정을 다룬 후속 기사입니다.
- LWN: Per-entity load tracking — PELT(Per-Entity Load Tracking) 메커니즘을 소개하는 기사입니다.
- LWN: Schedulers — the plot thickens — CFS 스케줄러 초기 설계 논의를 기록한 기사입니다.
- LWN: A look at the EEVDF scheduler — EEVDF 알고리즘의 이론적 배경을 설명하는 기사입니다.
- LWN: Fixing SCHED_IDLE — SCHED_IDLE 정책의 문제점과 개선을 다룬 기사입니다.
- LWN: Deadline scheduling for Linux — SCHED_DEADLINE 도입 과정을 설명하는 기사입니다.
- LWN: The extensible scheduler class — sched_ext의 기초 개념과 BPF struct_ops 기반 스케줄러 구현 메커니즘을 소개하는 기사입니다.
- LWN: sched_ext — BPF-Based Extensible Scheduler for Linux — sched_ext 종합 해설 기사입니다. Meta·Google의 프로덕션 채택 사례와 Dispatch Queue 동작 방식을 설명합니다.
- LWN: What's scheduled for sched_ext — scx_rusty, scx_layered 등 실제 사용 스케줄러 구현 사례와 BPF 제약 사항을 다룬 기사입니다.
- LWN: Extensible scheduler class to be merged for 6.11 — sched_ext 메인라인 병합 결정 과정을 기록한 기사입니다(최종적으로 Linux 6.12에 병합).
- LWN: The realtime preemption end game — for real this time — 20년 개발 끝에 PREEMPT_RT가 Linux 6.12 메인라인에 완전히 병합된 과정을 다룬 기사입니다.
- LWN: The long road to lazy preemption — TIF_NEED_RESCHED_LAZY 플래그 기반 PREEMPT_LAZY 모델의 설계 배경과 Linux 6.13 병합 과정을 설명하는 기사입니다.
- man sched(7) — 리눅스 스케줄링 정책과 우선순위에 대한 매뉴얼 페이지입니다.
- man sched_setscheduler(2) — 스케줄링 정책 및 파라미터 설정 시스템 콜 매뉴얼입니다.
- man sched_setattr(2) — SCHED_DEADLINE 등 확장 스케줄링 속성 설정 시스템 콜 매뉴얼입니다.
- Phoronix: Linux 7.0 Scheduler Updates Land Time Slice Extension, Performance & Scalability Work — RSEQ Time Slice Extension 병합과 스케줄러 확장성 개선을 정리한 기사입니다.
- LWN: Scheduler time slice extension — RSEQ 기반 타임슬라이스 확장 메커니즘의 설계와 구현을 설명하는 기사입니다.
- Phoronix: Linux 7.0 To Focus Just On Full & Lazy Preemption Models For Up-To-Date CPU Archs — 최신 아키텍처에서 PREEMPT_NONE/VOLUNTARY를 제거하는 선점 모델 재편을 설명하는 기사입니다.
- Phoronix: AWS Engineer Reports PostgreSQL Performance Halved By Linux 7.0 — Linux 7.0 선점 모델 변경으로 인한 PostgreSQL 성능 저하 보고 및 분석입니다.
- LWN: Improved load balancing with machine learning — sched_ext 기반 ML 부하 분산 스케줄러의 설계와 동작을 설명하는 기사입니다.
- KernelNewbies: Linux 6.17 — v6.17 릴리스 요약으로 proxy execution 초기 구현과 SMP 무조건 컴파일을 포함합니다.
- KernelNewbies: Linux 6.18 — v6.18 릴리스 요약으로 NUMA 밸런싱 개선, sched_ext cgroup 서브스케줄러 기반 작업을 포함합니다.
- KernelNewbies: Linux 6.19 — v6.19 릴리스 요약으로 NEXT_BUDDY 재도입과 sched_avg 회귀 수정을 포함합니다.
- KernelNewbies: Linux 7.0 — v7.0 릴리스 요약으로 RSEQ Time Slice Extension 및 선점 모델 재편을 포함합니다.
관련 문서
스케줄러와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.