프로세스(Process) 스케줄러(Scheduler)
Linux 커널 프로세스 스케줄러의 전체 아키텍처를 sched_class 계층 기준으로 추적합니다. CFS/EEVDF 선택 로직, RT/DEADLINE 우선순위(Priority) 규칙, per-CPU 런큐(Runqueue)와 PELT 부하 추적, sched_domain 로드 밸런싱, BPF 기반 sched_ext 확장, 선점(Preemption) 모델 차이, 관측·튜닝·디버깅(Debugging) 절차까지 포괄적으로 분석합니다.
핵심 요약
- CFS / EEVDF — 일반 프로세스용 공정 스케줄러. 가상 런타임(vruntime)으로 공평성을 유지합니다.
- sched_class — 스케줄링 정책의 플러그인 구조. dl → rt → fair → idle 우선순위입니다.
- 런큐(runqueue) — 각 CPU마다 하나씩 존재하며, 실행 대기 중인 태스크를 관리합니다.
- 선점(preemption) — 커널이 현재 실행 중인 태스크를 강제로 중단하고 다른 태스크를 실행합니다.
- 로드 밸런싱 — CPU 간에 태스크를 이동시켜 부하를 균등하게 분배합니다.
단계별 이해
- 스케줄러의 역할 — "다음에 어떤 프로세스를 실행할까?"를 결정합니다.
schedule()함수가 핵심입니다.타이머 인터럽트마다 현재 태스크의 시간을 업데이트하고 선점 여부를 판단합니다.
- CFS 동작 이해 — 각 태스크의 vruntime(가상 실행 시간)을 추적하여, vruntime이 가장 작은 태스크를 다음에 실행합니다.
Red-Black 트리로 관리되어 O(log N)에 다음 태스크를 선택합니다.
- 우선순위 확인 —
nice(-20~19) 값으로 프로세스 우선순위를 조정합니다. 낮을수록 더 많은 CPU 시간을 받습니다.ps -eo pid,ni,comm으로 현재 프로세스의 nice 값을 확인할 수 있습니다. - 실시간 스케줄링 —
SCHED_FIFO/SCHED_RR은 일반 태스크보다 항상 우선합니다.chrt -f 50 ./app으로 실시간 우선순위를 지정할 수 있습니다.
스케줄러 개요 (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) |
커널 스케줄러의 핵심 진입점(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을 반환하면 다음 클래스로 넘어갑니다.
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 스케줄러를 참고하세요.
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 설계 덕분에 대부분의 스케줄링 결정이 로컬 락만으로 가능합니다.
/* 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), 스택 포인터)를 저장하고 다음 태스크의 상태를 복원하는 과정입니다.
/* 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는 커널 4.17에서 도입된 에너지 효율 인식 스케줄링 프레임워크입니다. 전통적인 로드 밸런싱이 "모든 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 스케줄러는 태스크 개별이 아닌 그룹 단위로 공정성을 보장합니다. 컨테이너, 가상화, 멀티테넌트(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 누적 허용량 (버스트 허용) | echo 100000 > cpu.max.burst |
cpu.pressure |
읽기 전용 | 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) 멀티스레드 애플리케이션은 모든 스레드가 quota를 공유하므로, 스레드 수 × 필요 CPU 시간을 고려해야 합니다. (3) cpu.max.burst를 설정하면 유휴 기간에 축적된 quota를 버스트 시 사용할 수 있어 일시적 부하 처리에 유리합니다. (4) Kubernetes에서 CPU limit은 cpu.max로 구현되며, limit이 너무 낮으면 쓰로틀링으로 인한 성능 저하가 발생합니다.
__schedule() 핵심 경로
커널 스케줄링의 진입점은 schedule() 함수이며, 실제 스케줄링 결정은 __schedule()에서 이루어집니다. 이 함수는 kernel/sched/core.c에 정의되어 있으며, 다음 태스크를 선택하고 컨텍스트 스위치를 수행하는 전체 과정을 통제합니다.
호출 체인 (Call Chain)
스케줄링이 발생하는 전체 호출 경로는 다음과 같습니다:
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행선점이 아닌 자발적 스케줄링에서 이전 태스크의 상태를 확인합니다. 시그널이 있으면 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 스케줄러에서 각 태스크(또는 태스크 그룹)의 스케줄링 상태를 추적하는 핵심 구조체입니다. 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에서 실행 중인 태스크를 다른 태스크로 교체하는 과정입니다. 이 과정은 주소 공간 전환과 레지스터/스택 전환의 두 단계로 나뉘며, 커널에서 가장 성능에 민감한 경로 중 하나입니다.
호출 체인
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()는 페이지 테이블 기저 레지스터(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 엔트리 무효화 — 성능 저하 원인 |
| 커널 스레드 | flush 불필요 | 커널 공간만 사용, 이전 mm의 유저 매핑을 차용(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은 현재 활성화된 메모리 맵으로, 커널 스레드도 유저 공간 페이지 테이블을 차용할 수 있습니다. - 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 참조 카운트 감소 등.
perf stat -e context-switches,cs ./app으로 빈도를 측정하고, perf sched latency로 전환 지연을 분석할 수 있습니다. 일반적으로 유저→유저 전환(TLB flush 포함)이 스레드→스레드 전환(같은 mm)보다 수 배 느립니다.
Wake-up 경로
태스크 깨우기(Wake-up)는 sleep 상태의 태스크를 런큐에 다시 넣고, 필요하면 현재 실행 중인 태스크를 선점하는 과정입니다. I/O 완료, 시그널 전달, 뮤텍스 해제 등 다양한 경로에서 호출됩니다.
호출 체인
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(우선순위 상속 락)을 획득하여 동시에 여러 경로에서 같은 태스크를 깨우는 경합을 방지합니다. - 11–12행태스크의 현재 상태가 요청된
state마스크와 일치하는지 확인합니다. 이미 RUNNING이거나 다른 상태이면 깨우기를 건너뜁니다. - 15행
select_task_rq()가 부하 균형, 캐시 친화성, 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에서 활성화합니다. 원격 경로는 런큐 락 경합을 줄이기 위해 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로 시각화 분석. 각 단계에서 병목을 좁혀가며, 스케줄러 파라미터 조정은 원인을 정확히 파악한 후에 수행합니다.
참고 자료
- 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 주파수 거버너 문서입니다.
- 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 유휴 진입 로직입니다.
- 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 도입 과정을 설명하는 기사입니다.
- man sched(7) — 리눅스 스케줄링 정책과 우선순위에 대한 매뉴얼 페이지입니다.
- man sched_setscheduler(2) — 스케줄링 정책 및 파라미터 설정 시스템 콜 매뉴얼입니다.
- man sched_setattr(2) — SCHED_DEADLINE 등 확장 스케줄링 속성 설정 시스템 콜 매뉴얼입니다.
관련 문서
스케줄러와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.