EEVDF 스케줄러(Scheduler)

Earliest Eligible Virtual Deadline First(EEVDF) 스케줄러는 Linux 6.6에서 CFS의 핵심 pick 로직을 대체한 공정 스케줄링 알고리즘입니다. 1996년 Stoica와 Abdel-Wahab이 제안한 이론적 모델을 Peter Zijlstra가 커널에 구현한 과정, eligible 조건 판정, virtual deadline 계산, lag 추적, slice 기반 선점(Preemption) 메커니즘, augmented RB-Tree 구조, 그룹 스케줄링 상호작용, latency nice 확장, CFS 대비 성능 특성과 튜닝 전략을 커널 소스 기반으로 심층 분석합니다.

전제 조건: 프로세스(Process) 스케줄러, CFS 스케줄러 상세 문서를 먼저 읽으세요. EEVDF는 CFS의 vruntime, sched_entity, cfs_rq 구조를 그대로 활용하므로, CFS의 기본 개념을 먼저 이해해야 합니다.
일상 비유: CFS가 "지금까지 가장 적게 서비스받은 고객"을 다음으로 선택하는 방식이라면, EEVDF는 "마감 기한(Deadline)이 가장 임박한 고객 중에서 자격이 있는 사람"을 선택하는 방식입니다. 식당에서 예약 시간(deadline)이 가장 빠른 손님을 우선 안내하되, 아직 입장 시간(eligible time)이 되지 않은 손님은 대기시키는 것과 같습니다.

핵심 요약

  • EEVDF = Eligible + Earliest Virtual Deadline First — 자격 조건(eligible)을 통과한 태스크(Task) 중 virtual deadline이 가장 이른 태스크를 선택합니다.
  • Lag 기반 공정성(Fairness) — 각 태스크의 lag(이상적 서비스 - 실제 서비스)를 추적하여 양의 lag를 가진 태스크(덜 받은 태스크)가 eligible 자격을 얻습니다.
  • Slice 기반 선점 — CFS의 sched_min_granularity 대신 요청 기반 slice를 사용하여 태스크가 자신의 실행 시간을 예측 가능하게 합니다.
  • Augmented RB-Tree — 기존 vruntime 정렬 RB-Tree에 min_deadline 보조 키를 추가하여 O(log N)에 최적 후보를 찾습니다.
  • Latency Nice 확장 — nice 값과 독립적인 latency_nice(-20~19)로 응답 지연(Latency)을 세밀하게 제어할 수 있습니다.
  • CFS 호환 유지 — vruntime, PELT, cgroup 대역폭(Bandwidth), sched_domain 로드 밸런싱 등 CFS 인프라를 그대로 활용합니다.
  • 커널 6.6 도입 — Peter Zijlstra가 2023년 메인라인에 머지, 이후 6.7~6.12에서 지속 개선 중입니다.

단계별 이해

  1. CFS 복습
    vruntime 기반 공정 분배와 RB-Tree 스케줄링의 기본 원리를 확인합니다.
  2. EEVDF 이론 파악
    eligible 조건, virtual deadline, lag의 수학적 정의를 이해합니다.
  3. 커널 구현 추적
    pick_next_entity(), place_entity(), update_curr()의 EEVDF 변경점을 소스 코드로 따라갑니다.
  4. 데이터 구조 이해
    augmented RB-Tree의 min_deadline 전파 메커니즘을 파악합니다.
  5. 실전 튜닝과 관측
    sched_base_slice_ns, latency_nice 설정과 ftrace/perf 기반 성능 분석을 적용합니다.
관련 논문: Stoica, I. & Abdel-Wahab, H. "Earliest Eligible Virtual Deadline First: A Flexible and Accurate Mechanism for Proportional Share Resource Allocation" (Technical Report TR-95-22, Old Dominion University, 1996) — EEVDF 원논문. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

EEVDF 논문과 역사

EEVDF(Earliest Eligible Virtual Deadline First)는 1996년 Ion Stoica와 Hussein Abdel-Wahab이 발표한 비례 공유 자원 할당 알고리즘입니다. 원래 네트워크 패킷(Packet) 스케줄링(WFQ, WF2Q)의 맥락에서 제안되었으며, CPU 스케줄링에도 적용 가능한 범용 프레임워크입니다.

논문에서 커널까지의 타임라인

연도이벤트의미
1993WFQ(Weighted Fair Queueing) 제안GPS(Generalized Processor Sharing)의 패킷 근사
1996Stoica/Abdel-Wahab EEVDF 논문eligible 조건 + virtual deadline으로 지연 보장 개선
2007Linux CFS 도입 (v2.6.23)Ingo Molnar의 vruntime 기반 공정 스케줄러
2023-05Peter Zijlstra EEVDF 패치(Patch)셋 v5CFS 위에 EEVDF pick 로직 구현
2023-08Linux v6.6-rc1 머지EEVDF가 CFS의 기본 pick 알고리즘으로 교체
2023-10Linux v6.6 정식 릴리스EEVDF 첫 안정 버전
2024-01v6.7 — slice 튜닝 개선sched_base_slice_ns 기본값 조정
2024-03v6.8 — place_entity 리팩토링wakeup 시 lag 보존 정밀화
2024-05v6.9 — latency_nice 통합slice 크기에 latency_nice 반영
2024-09v6.11 — eligible 최적화augmented RB-Tree 순회 개선
2024-11v6.12 — NUMA 밸런싱 연동EEVDF + NUMA 마이그레이션 개선

논문 이론 vs 커널 구현 차이

항목원논문 (1996)Linux 구현 (6.6+)
대상 자원범용 (CPU, 네트워크 등)CPU 시간 전용
eligible 판정V(t) ≥ eligible_timeentity_eligible() — avg_vruntime 기반
virtual time V(t)연속 GPS 시뮬레이션cfs_rq->min_vruntime (이산 근사)
weight 표현실수 비율 (0~1)nice → sched_prio_to_weight[] 정수 테이블
request size명시적 요청sched_base_slice_ns 기반 자동 계산
deadline 계산VD = VE + r/wVD = VE + slice/weight (커널 단위)
자료구조명시하지 않음Augmented RB-Tree (min_deadline 보조 키)
그룹 스케줄링미다룸task_group → cfs_rq 계층적 적용
선점 정책즉시 선점 가정tick/wakeup 기반 선점 판단
왜 27년 만에 도입되었나: CFS 도입 당시(2007) EEVDF는 구현 복잡성 대비 이점이 불명확했습니다. 그러나 CFS의 sched_min_granularity 기반 선점이 대화형 워크로드에서 지연 보장을 제공하지 못하는 문제가 커지면서, Peter Zijlstra가 EEVDF의 deadline 기반 선점이 이 문제를 우아하게 해결한다는 것을 증명했습니다.
/*
 * kernel/sched/fair.c — EEVDF 도입 커밋 메시지 요약 (2023)
 *
 * sched/fair: Add EEVDF scheduling
 *
 * EEVDF는 eligible 조건과 virtual deadline을 통해
 * CFS보다 더 나은 지연 보장을 제공합니다.
 *
 * 핵심 변경:
 * - pick_next_entity()가 leftmost(min vruntime) 대신
 *   eligible 태스크 중 earliest deadline을 선택
 * - sched_entity에 deadline, slice 필드 추가
 * - RB-Tree에 min_deadline augmentation 추가
 * - place_entity()에서 lag 기반 배치
 */

CFS와의 핵심 차이점

EEVDF는 CFS의 인프라(vruntime, sched_entity, cfs_rq, PELT, cgroup bandwidth)를 그대로 활용하면서 태스크 선택 로직만 근본적으로 변경했습니다. CFS가 "가장 작은 vruntime"을 선택했다면, EEVDF는 "eligible한 태스크 중 가장 이른 deadline"을 선택합니다.

비교 항목CFS (v2.6.23~v6.5)EEVDF (v6.6+)
선택 기준min vruntime (leftmost RB node)eligible & earliest virtual deadline
공정성 척도vruntime 편차 최소화lag (이상적 - 실제 서비스) 최소화
선점 판단sched_min_granularity / buddy heuristicdeadline 비교 (새 태스크 VD < 현재 VD)
실행 시간 단위sched_latency / nr_runningslice (sched_base_slice_ns 기반)
지연 보장보장 없음 (best-effort)O(log N) 보장 (eligible + deadline)
대화형 최적화sleeper credit (wakeup preemption)wakeup 시 lag 보존 → 자연스러운 우선 처리
튜닝 파라미터sched_latency_ns, sched_min_granularity_nssched_base_slice_ns, latency_nice
RB-Tree 키vruntime 단일 키vruntime + min_deadline (augmented)
next/last buddy캐시(Cache) 친화도(Affinity) 힌트제거됨 (deadline 기반으로 불필요)
skip buddyyield_to() 구현유지 (yield 시 deadline 무시)
CFS vs EEVDF: 태스크 선택 로직 비교 CFS (v2.6.23~v6.5) RB-Tree (vruntime 정렬) leftmost = min vruntime buddy 힌트 확인 (next/last) sched_min_granularity 확인 선택: min vruntime 태스크 문제: 지연 보장 없음 buddy가 공정성 왜곡 가능 EEVDF (v6.6+) Augmented RB-Tree (vruntime + min_deadline) 1. Eligible 필터링 (lag >= 0) 2. Earliest Deadline 선택 deadline 비교 선점 판단 선택: earliest deadline 태스크 장점: O(log N) 지연 보장 buddy 불필요, 더 결정적
CFS는 단순 min vruntime을 선택하지만, EEVDF는 eligible 필터링 후 earliest deadline을 선택합니다
/* CFS의 pick_next_entity (v6.5 이전) — 단순화 */
static struct sched_entity *__pick_next_entity(struct sched_entity *se)
{
    struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);
    return rb_entry(left, struct sched_entity, run_node);
    /* → 항상 min vruntime (leftmost) 반환 */
}

/* EEVDF의 pick_eevdf (v6.6+) — 단순화 */
static struct sched_entity *pick_eevdf(struct cfs_rq *cfs_rq)
{
    /* eligible 조건을 만족하면서 deadline이 가장 이른 태스크 */
    struct sched_entity *best = NULL;
    struct rb_node *node = cfs_rq->tasks_timeline.rb_root.rb_node;

    while (node) {
        struct sched_entity *se = __node_2_se(node);

        if (entity_eligible(cfs_rq, se)) {
            best = se;  /* 더 이른 deadline 후보 탐색 */
            node = node->rb_left;
        } else {
            node = node->rb_right;
        }
    }
    return best;
}
buddy 제거: CFS에서 캐시 친화도를 위해 사용하던 next, last buddy가 EEVDF에서는 완전히 제거되었습니다. EEVDF의 deadline 기반 선택이 더 결정적이므로 buddy 힌트가 불필요하며, buddy가 공정성을 왜곡하는 문제도 함께 해결됩니다.

가상 시간 체계

EEVDF의 가상 시간 체계는 세 가지 핵심 요소로 구성됩니다: 가상 실행 시간(vruntime), 가상 마감 시간(virtual deadline), 자격 시간(eligible time). 이 세 값의 관계가 EEVDF의 스케줄링 결정을 완전히 결정합니다.

정의

개념수식 (논문)커널 표현의미
V(t)Σ w_i · s_i(t) / Σ w_icfs_rq->min_vruntime (근사)시스템 가상 시간 — 모든 태스크의 가중 평균 진행
S(t)태스크 i의 실제 서비스 시간se->vruntime태스크의 가상 실행 시간
VEeligible time = V(t_last_end)se->vruntime 기반 계산자격 시간 — 이 시점부터 선택 대상
VDVE + request / weightse->deadline가상 마감 시간 — 서비스 완료 기한
lagS_ideal(t) - S_actual(t)entity_lag() 계산이상적 서비스와 실제 서비스 차이
EEVDF 핵심 개념: eligible time, virtual deadline, lag V(t) 가상 시간 VE eligible time (자격 시작) VD virtual deadline (마감 기한) slice / weight V(t) 현재 ELIGIBLE (V(t) >= VE) Lag 계산 lag = S_ideal(t) - S_actual(t) lag > 0 : 덜 받음 → eligible lag < 0 : 더 받음 → ineligible lag = 0 : 정확히 공정 * eligible iff vruntime <= avg_vruntime EEVDF 선택 규칙 1. eligible한 태스크만 후보 2. 후보 중 VD 가장 작은 태스크 선택 3. 동률 시 vruntime 작은 쪽 우선 결과: 덜 받은 태스크가 긴급한 것부터 처리 → 공정성 + 지연 보장
EEVDF의 세 핵심 값: eligible time(자격 시작), virtual deadline(마감 기한), lag(공정성 척도)
/* kernel/sched/fair.c — avg_vruntime: 시스템 가상 시간의 가중 평균 */
static s64 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 avg;
}

/* avg_vruntime = Σ(key_i × weight_i) / Σ(weight_i)
 * 여기서 key_i = se->vruntime - cfs_rq->min_vruntime
 * 이 값이 EEVDF의 V(t)에 해당 */
/* entity_key: RB-Tree 키 = vruntime - min_vruntime */
static s64 entity_key(struct cfs_rq *cfs_rq,
                       struct sched_entity *se)
{
    return (s64)(se->vruntime - cfs_rq->min_vruntime);
}

Eligible 조건 판정

EEVDF에서 태스크가 실행 대상이 되려면 먼저 eligible(자격) 조건을 충족해야 합니다. 이는 해당 태스크가 공정한 몫보다 적게 서비스를 받았는지(양의 lag) 판단하는 것입니다.

수학적 정의

논문에서 태스크 i가 eligible한 조건은:

V(t) >= VE_i    (시스템 가상 시간이 태스크의 eligible time 이상)

여기서:
  V(t) = 시스템 가상 시간 (모든 태스크의 가중 평균 vruntime)
  VE_i = 태스크 i의 마지막 서비스 종료 시점의 V(t)

동치 조건:
  lag_i = S_ideal_i(t) - S_actual_i(t) >= 0

즉, 공정한 몫보다 적게 받은 태스크만 eligible

커널 구현: entity_eligible()

/* kernel/sched/fair.c — EEVDF eligible 조건 판정
 *
 * 태스크가 eligible ↔ vruntime <= avg_vruntime
 *
 * avg_vruntime = Σ(key_i × w_i) / Σ(w_i)
 * key_i = se->vruntime - min_vruntime
 *
 * 정수 연산 최적화:
 * se->vruntime <= avg_vruntime
 * ↔ (se->vruntime - min_vruntime) × Σ(w_i) <= Σ(key_i × w_i)
 * ↔ entity_key(se) × load <= avg_vruntime_sum
 */
static int entity_eligible(struct cfs_rq *cfs_rq,
                           struct sched_entity *se)
{
    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;
    }

    return entity_key(cfs_rq, se) * load <= avg;
}
코드 설명
  • avg, load 초기화cfs_rq->avg_vruntimeΣ(key_i × w_i), avg_loadΣ(w_i)를 저장합니다. 이 값들은 avg_vruntime_add()/avg_vruntime_sub()에서 enqueue/dequeue 시 증분 갱신됩니다.
  • curr 보정curr는 RB-Tree에 없지만 실행 중이므로, avg 계산에 수동으로 포함시킵니다. entity_key(cfs_rq, curr)curr->vruntime - cfs_rq->min_vruntime입니다.
  • eligible 판정entity_key(se) * load <= avg는 수학적으로 se->vruntime <= avg_vruntime()과 동치입니다. 나눗셈 대신 곱셈으로 변환하여 커널에서 비용이 큰 정수 나눗셈을 완전히 제거합니다.
정수 나눗셈 회피: 순진한 구현은 se->vruntime <= avg / load로 나눗셈이 필요합니다. 커널은 양변에 load를 곱하여 key * load <= avg로 변환함으로써 비싼 나눗셈을 완전히 제거합니다.
Eligible 판정 흐름 entity_eligible(cfs_rq, se) avg = cfs_rq->avg_vruntime curr on_rq? Yes avg += key(curr) * w(curr) No key(se) * load <= avg? Yes ELIGIBLE No INELIGIBLE eligible ↔ vruntime <= avg_vruntime ↔ lag >= 0 ↔ "공정 몫보다 덜 받음"
entity_eligible()은 나눗셈 없이 정수 곱셈만으로 eligible 여부를 판정합니다

avg_vruntime 유지

/* avg_vruntime은 태스크 enqueue/dequeue 시 증분 업데이트 */
static void avg_vruntime_add(struct cfs_rq *cfs_rq,
                              struct sched_entity *se)
{
    unsigned long weight = scale_load_down(se->load.weight);
    s64 key = entity_key(cfs_rq, se);

    cfs_rq->avg_vruntime += key * weight;
    cfs_rq->avg_load += weight;
}

static void avg_vruntime_sub(struct cfs_rq *cfs_rq,
                              struct sched_entity *se)
{
    unsigned long weight = scale_load_down(se->load.weight);
    s64 key = entity_key(cfs_rq, se);

    cfs_rq->avg_vruntime -= key * weight;
    cfs_rq->avg_load -= weight;
}

/* min_vruntime 업데이트 시에도 avg_vruntime 보정 필요:
 * key_i = se->vruntime - min_vruntime이므로
 * min_vruntime이 변하면 모든 key가 변함
 * → Σ(key_i × w_i) 보정 */
static void avg_vruntime_update(struct cfs_rq *cfs_rq, s64 delta)
{
    /* min_vruntime이 delta만큼 전진 →
     * 모든 key가 -delta → Σ(key×w)가 -delta×Σw */
    cfs_rq->avg_vruntime -= cfs_rq->avg_load * delta;
}

Virtual Deadline 계산

Virtual deadline은 태스크의 서비스 완료 기한을 나타냅니다. weight가 큰(우선순위(Priority) 높은) 태스크일수록 더 짧은 간격(이른 deadline)을 갖게 되어 자주 선택됩니다.

계산 공식

VD = VE + slice / weight

여기서:
  VD    = virtual deadline
  VE    = eligible time (= 현재 vruntime, 대략적으로)
  slice = 요청 실행 시간 (sched_base_slice_ns 기반)
  weight = nice → sched_prio_to_weight[] 변환 값

예시 (sched_base_slice_ns = 3ms = 3,000,000 ns):
  nice 0 (weight 1024):  VD = VE + 3,000,000 / 1024 = VE + 2,929
  nice -5 (weight 3121): VD = VE + 3,000,000 / 3121 = VE + 961
  nice 5 (weight 335):   VD = VE + 3,000,000 / 335  = VE + 8,955

→ nice -5 태스크는 VD가 VE에 가까워 더 자주/먼저 선택됨
/* kernel/sched/fair.c — deadline 설정 (slice/weight 계산) */
static void update_deadline(struct cfs_rq *cfs_rq,
                            struct sched_entity *se)
{
    if ((s64)(se->vruntime - se->deadline) >= 0) {
        /* 이전 deadline을 넘겼으면 새 deadline 설정 */
        se->deadline = se->vruntime +
                       calc_delta_fair(se->slice, se);
        /*
         * calc_delta_fair(delta, se) = delta * NICE_0_LOAD / se->load.weight
         * → slice를 weight로 나눈 가상 시간 단위
         * → weight가 클수록 deadline이 가까워짐
         */
    }
}

/* se->slice: 실제 실행 시간 단위 (nanoseconds)
 * 기본값 = sysctl_sched_base_slice (기본 3ms)
 * latency_nice에 의해 조정 가능 */
코드 설명
  • 조건 검사(s64)(se->vruntime - se->deadline) >= 0은 현재 vruntime이 이전 deadline을 넘었는지 확인합니다. signed 비교를 사용하여 u64 래핑(wrap-around)을 안전하게 처리합니다.
  • deadline 계산calc_delta_fair(slice, se)slice * NICE_0_LOAD / se->load.weight를 계산합니다 (kernel/sched/fair.c). weight가 클수록(높은 우선순위) 가상 시간 간격이 짧아져 deadline이 가까워지고, 더 자주 선택됩니다.
  • slice 기본값se->slice는 실제 실행 시간(ns) 단위이며, 기본값 3ms는 sysctl_sched_base_slice로 설정됩니다. CFS의 sched_latency / nr_running 동적 계산과 달리 태스크별 고정값입니다.
deadline 갱신 시점: deadline은 현재 vruntime이 이전 deadline을 넘었을 때만 갱신됩니다. 이는 태스크가 slice 시간을 다 쓰기 전에는 동일 deadline을 유지하도록 보장합니다. CFS의 sched_latency 기반 동적 타임슬라이스(Time Slice)와 달리, EEVDF의 slice는 태스크별로 고정됩니다.
nice 값weightslice (3ms 기준) 가상 시간상대적 CPU 비율
-2088761VE + 33.8~30.5x (nice 0 대비)
-109548VE + 314.2~3.3x
-53121VE + 961.2~1.1x
01024VE + 2929.71.0x (기준)
5335VE + 8955.2~0.33x
10110VE + 27272.7~0.11x
1915VE + 200000.0~0.015x
nice 값별 Virtual Deadline 간격 비교 V(t) 가상 시간 VE nice=-20 w=88761 VD=VE+34 nice=-5 w=3121 VD=VE+961 nice=0 (기준) w=1024 VD=VE+2930 nice=5 w=335 VD=VE+8955 nice=19 w=15 VD=VE+200000 (→ 매우 멀리) VD 간격이 짧을수록 • deadline이 더 빨리 도래 • pick_eevdf()에서 더 자주 선택 • 결과: 더 많은 CPU 시간 할당 VD = VE + slice × NICE_0_LOAD / weight
weight가 클수록(nice 작을수록) VD 간격이 짧아져 스케줄러가 더 자주 선택합니다
직관적 이해: nice=-20 태스크의 deadline 간격(34)은 nice=19 태스크(200,000)의 약 1/6000입니다. 이는 nice=-20 태스크가 같은 시간 동안 약 6,000배 더 자주 스케줄링된다는 것이 아니라(실제 CPU 비율은 weight 비율인 ~5917:1), deadline 경쟁에서 거의 항상 이긴다는 의미입니다.

Lag 추적 메커니즘

Lag는 EEVDF의 공정성 척도입니다. 각 태스크의 이상적 서비스(ideal service)실제 서비스(actual service)의 차이를 추적합니다.

Lag 정의

lag_i(t) = S_ideal_i(t) - S_actual_i(t)

S_ideal_i(t) = w_i / W × (t - t_0)    (공정 비율 × 경과 시간)
S_actual_i(t) = se->vruntime 기반      (실제 받은 서비스)

lag > 0 : 태스크가 공정 몫보다 적게 받음 → eligible (우선 서비스)
lag = 0 : 정확히 공정하게 받음
lag < 0 : 태스크가 공정 몫보다 많이 받음 → ineligible (대기)

EEVDF의 보장:
  |lag_i(t)| <= r_max    (r_max = 최대 요청 크기)
  → lag가 무한히 커지지 않음, 기아(starvation) 방지
Lag 추적: 양/음 lag와 eligible 상태 시간 lag=0 lag > 0: ELIGIBLE (덜 받음) lag < 0: INELIGIBLE (더 받음) 태스크 A (CPU-bound) 태스크 B (interactive) sleep에서 깨어남 → lag 축적 → eligible 실행 후 lag 감소 → ineligible interactive 태스크는 sleep 동안 lag 축적 → 깨어나면 즉시 eligible
lag의 부호가 eligible 상태를 결정: 양(+)이면 eligible, 음(-)이면 ineligible
/* kernel/sched/fair.c — lag 계산 */
static s64 entity_lag(u64 avruntime, struct sched_entity *se)
{
    s64 vlag, limit;

    /* lag = avg_vruntime - se->vruntime
     * 양수 = 평균보다 뒤처짐 = 덜 받음 = eligible
     * 음수 = 평균보다 앞섬 = 더 받음 = ineligible */
    vlag = avruntime - se->vruntime;

    /* lag 상한 제한: |lag| <= slice */
    limit = calc_delta_fair(se->slice, se);
    return clamp(vlag, -limit, limit);
}
코드 설명
  • lag 정의avruntime - se->vruntime으로 계산됩니다. 양수면 해당 태스크가 평균보다 뒤처져(덜 받아) eligible 상태이고, 음수면 평균보다 앞서서(더 받아) ineligible 상태입니다.
  • 상한 제한calc_delta_fair(slice, se)로 계산된 가상 시간 단위의 slice만큼으로 lag를 제한합니다. 이는 오래 sleep한 태스크가 과도한 lag를 축적하여 다른 태스크를 기아(starvation) 상태로 만드는 것을 방지합니다.
  • 호출 경로dequeue_entity()entity_lag()로 호출되어 se->vlag에 저장됩니다. 이후 enqueue_entity()place_entity()에서 이 값을 복원하여 공정성을 유지합니다.
lag 보존의 중요성: 태스크가 sleep했다가 wakeup할 때 lag를 정확히 보존하지 않으면 공정성이 깨집니다. CFS는 sleeper에게 vruntime 보너스를 주는 휴리스틱을 사용했지만, EEVDF는 lag 자체를 보존하여 수학적으로 정확한 공정성을 유지합니다.

pick_next_entity() 구현

EEVDF의 핵심 함수인 pick_eevdf()는 eligible한 태스크 중 가장 이른 virtual deadline을 가진 태스크를 O(log N)에 찾습니다. 이를 위해 augmented RB-Tree의 min_deadline 보조 키를 활용합니다.

pick_eevdf() 동작 흐름 pick_eevdf(cfs_rq) 호출 node = RB-Tree root while (node != NULL) entity_eligible (cfs_rq, se)? Yes (eligible) best = se node = node->rb_left (더 이른 VD 탐색) No (ineligible) node = node->rb_right (vruntime 큰 쪽 탐색) min_deadline 서브트리 탐색 return best (earliest VD)
pick_eevdf()는 RB-Tree를 순회하며 eligible한 태스크 중 earliest 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 *se = __pick_first_entity(cfs_rq);
    struct sched_entity *curr = cfs_rq->curr;
    struct sched_entity *best = NULL;

    /* cfs_rq->curr가 eligible하고 deadline이 빠르면 유지 */
    if (curr && curr->on_rq &&
        entity_eligible(cfs_rq, curr))
        best = curr;

    /* leftmost가 eligible이면 이것이 최선일 가능성 높음 */
    if (se && entity_eligible(cfs_rq, se) &&
        (!best || deadline_gt(best, se)))
        best = se;

    if (best) {
        /* best의 deadline이 서브트리 min보다 작으면 탐색 불필요 */
        if (!node || deadline_gt(__node_2_se(node), best))
            goto found;
    }

    /* Augmented RB-Tree 탐색: eligible + earliest deadline */
    while (node) {
        struct sched_entity *se = __node_2_se(node);

        /* 왼쪽 서브트리에 더 이른 min_deadline이 있으면 왼쪽으로 */
        if (node->rb_left &&
            vruntime_eligible(cfs_rq,
                __node_2_se(node->rb_left)->min_deadline)) {
            node = node->rb_left;
            continue;
        }

        if (entity_eligible(cfs_rq, se)) {
            if (!best || deadline_gt(best, se))
                best = se;
        }

        node = node->rb_right;
    }

found:
    return best;
}
코드 설명
  • curr 검사현재 실행 중인 cfs_rq->curr가 eligible하면 best 후보로 설정합니다. 불필요한 컨텍스트 스위치를 방지하는 최적화입니다.
  • leftmost 검사RB-Tree의 leftmost 노드(가장 작은 vruntime)는 eligible일 가능성이 높습니다. eligible이면서 best보다 이른 deadline이면 갱신합니다.
  • early terminationbest의 deadline이 루트 노드의 min_deadline보다 이르면, 서브트리 전체를 탐색할 필요가 없으므로 즉시 반환합니다. 이 최적화로 대부분의 경우 O(1)에 가깝게 동작합니다.
  • augmented 탐색왼쪽 자식의 min_deadline이 eligible 범위에 있으면 왼쪽으로 내려갑니다. vruntime_eligible()은 해당 deadline의 vruntime이 avg_vruntime 이하인지 확인합니다. 최대 O(log N) 노드만 방문합니다.
/* pick_next_entity — EEVDF를 사용하는 최상위 선택 함수 */
static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq)
{
    /* skip이 설정된 경우 (yield) 건너뛰기 */
    if (cfs_rq->skip)
        return pick_eevdf(cfs_rq);  /* skip 처리 포함 */

    return pick_eevdf(cfs_rq);
}
O(log N) 보장: RB-Tree 높이가 O(log N)이므로, pick_eevdf()의 while 루프는 최대 O(log N) 노드만 방문합니다. CFS의 leftmost O(1)보다 느리지만, 지연 보장이라는 훨씬 강한 속성을 제공합니다. 실제로는 early termination 최적화로 대부분 O(1)~O(log N) 사이에서 완료됩니다.

슬라이스 메커니즘

EEVDF에서 slice는 태스크가 한 번에 실행할 수 있는 시간 단위입니다. CFS의 동적 타임슬라이스(sched_latency / nr_running)와 달리, EEVDF의 slice는 태스크별로 고정되며 deadline 계산의 기초가 됩니다.

파라미터CFSEEVDF
실행 시간 단위sched_latency / nr_runningsched_base_slice_ns (태스크별 고정)
최소 단위sched_min_granularity_ns해당 없음 (slice 자체가 최소)
선점 기준delta_exec > ideal_runtimevruntime > deadline
nr_running 영향타임슬라이스 = latency / nr_runningslice 크기 불변, deadline 간격만 변화
latency 보장없음 (n이 커지면 지연 증가)O(log N) 보장
/* kernel/sched/fair.c — slice 기반 deadline 및 선점 */

/* sched_base_slice_ns: 기본 실행 슬라이스 */
unsigned int sysctl_sched_base_slice = 3000000ULL;  /* 3ms */

/* 태스크의 slice 초기화 */
static void init_entity_runnable_average(struct sched_entity *se)
{
    se->slice = sysctl_sched_base_slice;
    /* latency_nice가 설정되면 slice 조정 */
}

/* slice 소진 확인 — tick에서 호출 */
static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    u64 now = rq_clock_task(rq_of(cfs_rq));
    u64 delta_exec;

    delta_exec = now - curr->exec_start;
    curr->exec_start = now;

    curr->sum_exec_runtime += delta_exec;
    curr->vruntime += calc_delta_fair(delta_exec, curr);

    /* EEVDF: vruntime이 deadline을 넘으면 새 deadline 설정 */
    update_deadline(cfs_rq, curr);

    update_min_vruntime(cfs_rq);
}
CFS 동적 타임슬라이스 vs EEVDF 고정 슬라이스 CFS: 동적 타임슬라이스 nr_running = 4일 때: timeslice = sched_latency(6ms) / 4 = 1.5ms A:1.5ms B:1.5ms C:1.5ms nr_running = 20일 때: timeslice = 6ms / 20 = 0.3ms → min_granularity(0.75ms)로 클램핑! 20개 × 0.75ms = 총 스케줄링 주기 15ms (지연 증가!) CFS 문제점 • 태스크 수 증가 → 타임슬라이스 감소 • min_granularity 클램핑 → 지연 비례 증가 • 보장: 없음 (O(n) 지연) EEVDF: 고정 슬라이스 nr_running = 4일 때: slice = sched_base_slice(3ms) — 고정 A:3ms B:3ms C:3ms nr_running = 20일 때: slice = 3ms — 여전히 고정! → deadline 경쟁이 자동 조절 각 태스크 3ms slice, eligible+deadline으로 공정 분배 EEVDF 장점 • 태스크 수와 무관한 고정 slice • deadline이 자연스럽게 순서 결정 • 보장: O(log N) 지연 핵심 차이: 태스크 수(N) 증가 시 CFS: timeslice = max(latency/N, min_gran) → N 커지면 지연 O(N) 증가 EEVDF: slice 고정, deadline 간격만 조밀해짐 → 지연 O(log N) 보장
CFS는 태스크 수에 비례하여 지연이 증가하지만, EEVDF는 고정 slice와 deadline 경쟁으로 O(log N) 지연을 보장합니다
slice vs 타임슬라이스: CFS의 타임슬라이스는 sched_latency / nr_running으로 태스크 수에 반비례하여 줄어들었습니다. EEVDF의 slice는 고정이며, 대신 deadline이 자연스럽게 스케줄링 주기를 결정합니다. 태스크 수가 많으면 eligible 후보도 많아지고, deadline 경쟁이 자연스럽게 시간 분배를 조절합니다.

EEVDF 선점 규칙

EEVDF에서 선점(preemption)은 두 가지 경로로 발생합니다: tick 기반 선점(현재 태스크의 slice 소진)과 wakeup 기반 선점(새로 깨어난 태스크의 deadline이 더 이름).

선점 판단 규칙

선점 발생 조건:

1. Tick 선점:
   curr->vruntime >= curr->deadline
   → 현재 태스크가 자신의 slice를 소진

2. Wakeup 선점:
   new->deadline < curr->deadline
   AND entity_eligible(cfs_rq, new)
   → 새 태스크가 eligible하고 deadline이 더 이름

3. CFS에서 제거된 조건:
   - buddy 기반 선점 (next/last buddy) → 제거
   - sched_min_granularity 기반 최소 실행 보장 → 제거
   - wakeup_gran 기반 보너스 → 제거
EEVDF 선점 판단 흐름 (tick / wakeup) Tick 선점 경로 scheduler_tick() 호출 update_curr(cfs_rq) vruntime >= deadline? Yes No 선점 안 함 (계속 실행) resched_curr(rq) → TIF_NEED_RESCHED 설정 schedule() → pick_eevdf() Wakeup 선점 경로 check_preempt_wakeup_fair() entity_eligible (cfs_rq, new)? Yes No 선점 안 함 new->deadline < curr->deadline? Yes No 선점 안 함 resched_curr(rq) → 선점! schedule() → pick_eevdf()
EEVDF의 두 가지 선점 경로: tick에서는 deadline 초과, wakeup에서는 deadline 비교
/* kernel/sched/fair.c — tick 기반 선점 확인 */
static void
check_preempt_tick(struct cfs_rq *cfs_rq,
                   struct sched_entity *curr)
{
    /* EEVDF: curr의 vruntime이 deadline을 넘었으면 선점 */
    if ((s64)(curr->vruntime - curr->deadline) >= 0) {
        resched_curr(rq_of(cfs_rq));
        return;
    }
}
/* kernel/sched/fair.c — wakeup 기반 선점 확인 */
static void
check_preempt_wakeup_fair(struct rq *rq,
                          struct task_struct *p, int wake_flags)
{
    struct sched_entity *se = &p->se;
    struct sched_entity *curr_se = &rq->curr->se;

    /* 같은 cfs_rq 레벨까지 탐색 */
    while (se && curr_se) {
        if (se == curr_se)
            break;
        /* ... hierarchy walk ... */
    }

    /* EEVDF: new가 eligible하고 deadline이 curr보다 이르면 선점 */
    if (entity_eligible(cfs_rq, se) &&
        (s64)(curr_se->deadline - se->deadline) > 0)
        resched_curr(rq);
}
코드 설명
  • check_preempt_tickentity_tick()에서 호출되며, (s64)(curr->vruntime - curr->deadline) >= 0이면 현재 태스크가 slice를 소진한 것이므로 resched_curr()로 스케줄링 플래그(TIF_NEED_RESCHED)를 설정합니다. CFS의 ideal_runtime 비교보다 단순합니다.
  • check_preempt_wakeup_fairtry_to_wake_up()check_preempt_curr() 경로로 호출됩니다. 깨어난 태스크(se)가 eligible하면서 현재 태스크(curr_se)보다 이른 deadline을 가지면 선점합니다.
  • 계층 탐색while (se && curr_se) 루프는 그룹 스케줄링 환경에서 두 태스크가 같은 cfs_rq 레벨에 도달할 때까지 계층을 올라갑니다. 같은 레벨에서만 deadline 비교가 의미를 가집니다.
  • CFS와의 차이CFS는 wakeup_gran(기본 1ms) 버퍼를 두어 잦은 선점을 방지했지만, EEVDF는 순수 deadline 비교만 사용합니다. 이로써 대화형 태스크의 응답 지연이 줄어듭니다.
CFS 선점과의 차이: CFS는 wakeup_gran(기본 1ms)을 사용하여 현재 태스크에게 최소 실행 보장을 제공했습니다. EEVDF에서는 이 파라미터가 제거되었으며, deadline 비교만으로 선점을 판단합니다. 이로 인해 대화형 태스크의 응답성이 개선되지만, 매우 잦은 선점이 발생할 수 있습니다.

Augmented RB-Tree

EEVDF의 효율적 구현을 위해 기존 CFS의 RB-Tree에 min_deadline 보조 키가 추가되었습니다. 이 augmentation을 통해 서브트리 내 최소 deadline을 O(1)에 조회할 수 있어, pick_eevdf()가 O(log N)에 동작할 수 있습니다.

Augmented RB-Tree: vruntime 키 + min_deadline 보조 키 태스크 C vruntime=1000, VD=1800 min_deadline=900 태스크 A vruntime=500, VD=900 min_deadline=900 태스크 E vruntime=1500, VD=2100 min_deadline=1200 태스크 B vr=300, VD=1100 min_dl=1100 태스크 D vr=1200, VD=1200 min_dl=1200 태스크 F vr=1800, VD=2500 min_dl=2500 Augmentation 규칙 min_deadline(node) = min(node->deadline, min_deadline(left), min_deadline(right)) Root의 min_deadline=900은 전체 트리에서 가장 이른 deadline (태스크 A) pick_eevdf()는 min_deadline을 따라 eligible + earliest deadline을 O(log N)에 탐색
각 노드의 min_deadline은 자신과 하위 서브트리 중 최소 deadline을 저장합니다
/* kernel/sched/fair.c — RB-Tree augmentation: min_deadline 전파 */

/* 노드의 min_deadline을 자식으로부터 갱신 */
static inline bool
__update_min_deadline(struct sched_entity *se, struct rb_node *node)
{
    struct sched_entity *rse = __node_2_se(node);
    if (deadline_gt(se, rse)) {
        se->min_deadline = rse->min_deadline;
        return true;
    }
    return false;
}

/* RB-Tree 콜백: 회전/삽입/삭제 시 augmentation 업데이트 */
RB_DECLARE_CALLBACKS(static, min_deadline_cb,
    struct sched_entity, run_node, min_deadline,
    min_deadline_update);

static void min_deadline_update(struct sched_entity *se,
                                struct rb_node *node)
{
    /* se->min_deadline = 자기 자신의 deadline으로 초기화 */
    se->min_deadline = se->deadline;

    /* 왼쪽 자식의 min_deadline과 비교 */
    if (node->rb_left)
        __update_min_deadline(se, node->rb_left);

    /* 오른쪽 자식의 min_deadline과 비교 */
    if (node->rb_right)
        __update_min_deadline(se, node->rb_right);
}
RB_DECLARE_CALLBACKS: 리눅스 커널의 rbtree augmented API는 RB_DECLARE_CALLBACKS 매크로(Macro)를 통해 회전(rotation) 시 자동으로 보조 키를 갱신합니다. 이 매크로가 rb_augment_callbacks를 생성하여 RB-Tree 연산(insert, erase, rotate)에서 min_deadline 일관성을 자동 유지합니다.

sched_entity EEVDF 관련 필드 상세

EEVDF 도입으로 struct sched_entity에 여러 필드가 추가되었습니다.

/* include/linux/sched.h — sched_entity EEVDF 관련 필드 */
struct sched_entity {
    /* === CFS 기존 필드 === */
    struct load_weight    load;           /* nice → weight 변환 */
    struct rb_node        run_node;       /* RB-Tree 노드 */
    unsigned int         on_rq;          /* enqueued 여부 */
    u64                  exec_start;     /* 현재 실행 시작 시각 */
    u64                  sum_exec_runtime; /* 총 실행 시간 */
    u64                  prev_sum_exec_runtime;
    u64                  vruntime;       /* 가상 실행 시간 */

    /* === EEVDF 추가 필드 === */
    u64                  deadline;       /* virtual deadline */
    u64                  min_deadline;   /* augmented: 서브트리 최소 deadline */
    unsigned int         slice;          /* 요청 실행 시간 (ns) */

    /* === 공통 === */
    s64                  vlag;           /* lag 값 (place_entity용) */
    u64                  migrate_deadline; /* 마이그레이션 deadline */

    /* ... PELT, cgroup 필드 생략 ... */
};
코드 설명
  • loadnice 값을 weight로 변환한 결과를 저장합니다. calc_delta_fair()에서 vruntime 증가율을 결정하는 핵심 값입니다 (include/linux/sched.h).
  • vruntime가상 실행 시간으로, CFS와 동일하게 update_curr()에서 매 tick 갱신됩니다. EEVDF에서는 eligible 판정(entity_eligible())과 RB-Tree 정렬 키로 사용됩니다.
  • deadlineEEVDF 추가 필드로, vruntime + calc_delta_fair(slice, se)로 계산됩니다. pick_eevdf()가 eligible 태스크 중 가장 이른 deadline을 선택하는 기준입니다.
  • min_deadlineAugmented RB-Tree의 보조 키로, 해당 노드의 서브트리 내 최소 deadline 값을 캐시합니다. pick_eevdf()에서 O(log N) 탐색을 가능하게 합니다.
  • slice태스크가 요청한 실행 시간(나노초 단위)입니다. 기본값은 sysctl_sched_base_slice(3ms)이며, latency_nice로 조정 가능합니다.
  • vlagdequeue_entity()에서 저장된 lag 값입니다. place_entity()에서 wakeup/migrate 시 vruntime을 복원하는 데 사용되어 공정성을 보존합니다.
필드타입용도갱신 시점
vruntimeu64가상 실행 시간 (CFS 호환)update_curr() — 매 tick
deadlineu64virtual deadlineupdate_deadline() — vruntime >= deadline 시
min_deadlineu64서브트리 최소 deadline (augmented)RB-Tree 삽입/삭제/회전 시
sliceunsigned int요청 실행 시간 (ns)fork/exec 시 초기화, latency_nice로 조정
vlags64저장된 lag (dequeue/migrate 시)place_entity() — enqueue 시 복원
/* cfs_rq의 EEVDF 관련 필드 */
struct cfs_rq {
    /* === 기존 === */
    struct load_weight    load;
    unsigned int         nr_running;
    u64                  min_vruntime;
    struct rb_root_cached tasks_timeline;

    /* === EEVDF 추가 === */
    s64                  avg_vruntime;   /* Σ(key_i × w_i) */
    u64                  avg_load;       /* Σ(w_i) */

    /* ... */
};

place_entity() 분석

place_entity()는 태스크가 enqueue될 때(fork, wakeup, migrate) vruntime과 deadline을 적절히 배치하는 핵심 함수입니다. EEVDF에서는 lag 보존이 핵심 원칙입니다.

배치 시나리오별 동작

시나리오CFS 동작EEVDF 동작
새 태스크 (fork)vruntime = min_vruntimevruntime = avg_vruntime, lag=0
wakeup (짧은 sleep)vruntime = max(vruntime, min_vruntime - sched_latency/2)lag 보존 → vruntime = avg_vruntime - vlag
wakeup (긴 sleep)동일 + sleeper creditlag 클램핑 → 과도한 boost 방지
migrate (다른 CPU)vruntime 보정 (min_vruntime 차이)vlag 보존 + 새 cfs_rq의 avg_vruntime 기반 복원
place_entity() 배치 로직 place_entity(cfs_rq, se, flags) ENQUEUE_INITIAL? (새 태스크) Yes vruntime = avg_vruntime vlag = 0 (neutral start) No lag 복원 vruntime = avg_vruntime - se->vlag lag 클램핑: |vlag| <= slice/weight update_deadline(cfs_rq, se) deadline = vruntime + slice/weight
새 태스크는 avg_vruntime에서 시작하고, wakeup 태스크는 저장된 lag를 복원합니다
/* kernel/sched/fair.c — place_entity (EEVDF, 단순화) */
static void
place_entity(struct cfs_rq *cfs_rq,
             struct sched_entity *se, int flags)
{
    u64 vruntime = avg_vruntime(cfs_rq);
    s64 lag = 0;

    if (!(flags & ENQUEUE_INITIAL)) {
        /* wakeup/migrate: 저장된 lag 복원 */
        lag = se->vlag;

        /* lag 상한 제한: 과도한 boost 방지 */
        s64 limit = calc_delta_fair(se->slice, se);
        lag = clamp(lag, -limit, limit);
    }

    /* vruntime 배치: avg에서 lag만큼 뒤로 */
    se->vruntime = vruntime - lag;

    /* 새 deadline 설정 */
    se->deadline = se->vruntime +
                   calc_delta_fair(se->slice, se);
}
코드 설명
  • vruntime 기준점avg_vruntime(cfs_rq)는 현재 시스템의 가중 평균 vruntime을 반환합니다. 이를 기준점으로 사용하여 새로 enqueue되는 태스크가 과거나 미래에 치우치지 않도록 배치합니다.
  • lag 복원ENQUEUE_INITIAL이 아닌 경우(wakeup/migrate), dequeue_entity()에서 저장했던 se->vlag를 복원합니다. clamp(lag, -limit, limit)으로 slice 범위 내로 제한하여 과도한 boost를 방지합니다.
  • vruntime 배치vruntime - lag로 설정합니다. 양수 lag(덜 받은 상태)면 vruntime이 avg보다 뒤로 배치되어 eligible 상태가 되고, 음수 lag(더 받은 상태)면 앞으로 배치되어 ineligible 상태가 됩니다.
  • deadline 재설정배치된 vruntime에서 calc_delta_fair(slice, se)를 더해 새 deadline을 설정합니다. 이는 kernel/sched/fair.cupdate_deadline()과 동일한 계산입니다.
/* dequeue 시 lag 저장 — 나중에 place_entity에서 복원 */
static void
dequeue_entity(struct cfs_rq *cfs_rq,
               struct sched_entity *se, int flags)
{
    /* ... */

    /* 현재 lag를 vlag에 저장 */
    se->vlag = entity_lag(avg_vruntime(cfs_rq), se);

    __dequeue_entity(cfs_rq, se);
    /* ... */
}
lag 보존의 수학적 의미: 태스크가 sleep 전 lag=+500이었다면, wakeup 시에도 "500만큼 덜 받은 상태"로 복원됩니다. 이는 CFS의 sched_child_runs_first 같은 ad-hoc 휴리스틱 없이도 자연스러운 대화형 응답성을 제공합니다.

update_curr() 변경점

update_curr()는 현재 실행 중인 태스크의 vruntime을 갱신하는 핵심 함수로, EEVDF에서 deadline 갱신 로직이 추가되었습니다.

/* kernel/sched/fair.c — update_curr (EEVDF 변경점 포함) */
static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    u64 now = rq_clock_task(rq_of(cfs_rq));
    u64 delta_exec;

    if (unlikely(!curr))
        return;

    delta_exec = now - curr->exec_start;
    if (unlikely((s64)delta_exec <= 0))
        return;

    curr->exec_start = now;
    curr->sum_exec_runtime += delta_exec;

    /* 1. vruntime 갱신 (CFS와 동일) */
    curr->vruntime += calc_delta_fair(delta_exec, curr);

    /* 2. EEVDF: deadline 갱신 (추가됨) */
    update_deadline(cfs_rq, curr);

    /* 3. min_vruntime 갱신 */
    update_min_vruntime(cfs_rq);

    /* 4. avg_vruntime은 min_vruntime 변화에 의해 암시적 갱신 */

    /* 5. PELT 부하 갱신 */
    account_cfs_rq_runtime(cfs_rq, delta_exec);
}
코드 설명
  • delta_exec 계산rq_clock_task()로 현재 시각을 가져와 exec_start와의 차이를 구합니다. unlikely((s64)delta_exec <= 0) 검사는 시계 역행(clock skew) 등 비정상 상황을 방어합니다.
  • vruntime 갱신calc_delta_fair(delta_exec, curr)delta_exec * NICE_0_LOAD / weight를 계산합니다. weight가 높은(우선순위 높은) 태스크일수록 vruntime 증가가 느려 더 오래 실행됩니다. CFS와 완전히 동일한 로직입니다.
  • EEVDF 추가분update_deadline(cfs_rq, curr) 호출이 EEVDF에서 유일하게 추가된 부분입니다. vruntime이 deadline을 초과하면 새 deadline을 할당합니다.
  • 호출 체인scheduler_tick()task_tick_fair()entity_tick()update_curr() 경로로 매 tick(보통 1ms~4ms)마다 호출됩니다. 또한 pick_next_task(), put_prev_task() 등에서도 호출됩니다.
변경 최소화: EEVDF는 update_curr()에 update_deadline() 호출 한 줄만 추가했습니다. CFS의 기존 vruntime 갱신, PELT, cgroup bandwidth 등의 로직은 완전히 보존됩니다. 이는 EEVDF가 CFS의 "드롭인 대체"로 설계되었음을 보여줍니다.

update_deadline() 상세

/* deadline은 slice를 다 쓸 때만 갱신됨 */
static void update_deadline(struct cfs_rq *cfs_rq,
                            struct sched_entity *se)
{
    /* vruntime이 deadline을 넘었는가? */
    if ((s64)(se->vruntime - se->deadline) >= 0) {
        /*
         * 이전 slice를 소진 → 새 deadline 할당
         * deadline = vruntime + calc_delta_fair(slice, se)
         *          = vruntime + slice * NICE_0_LOAD / weight
         *
         * 높은 weight → 짧은 가상 시간 간격 → 이른 deadline
         * 낮은 weight → 긴 가상 시간 간격 → 늦은 deadline
         */
        se->deadline = se->vruntime +
                       calc_delta_fair(se->slice, se);

        /* RB-Tree에 있으면 min_deadline augmentation 갱신 */
        if (se->on_rq)
            update_min_deadline(se);
    }
}

tick 기반 선점 판단

EEVDF의 tick 기반 선점은 CFS보다 단순합니다. CFS는 ideal_runtime 계산과 sched_min_granularity 확인이 필요했지만, EEVDF는 단순히 deadline 초과 여부만 확인합니다.

/* kernel/sched/fair.c — entity_tick (단순화) */
static void
entity_tick(struct cfs_rq *cfs_rq,
            struct sched_entity *curr, int queued)
{
    /* 1. 현재 태스크의 vruntime 갱신 */
    update_curr(cfs_rq);

    /* 2. 다른 runnable 태스크가 있으면 선점 확인 */
    if (cfs_rq->nr_running > 1)
        check_preempt_tick(cfs_rq, curr);
}

static void
check_preempt_tick(struct cfs_rq *cfs_rq,
                   struct sched_entity *curr)
{
    /* EEVDF: slice 소진 = deadline 초과 */
    if ((s64)(curr->vruntime - curr->deadline) >= 0) {
        resched_curr(rq_of(cfs_rq));
        return;
    }

    /* 최소 실행 보장: HZ에 기반한 최소 tick (선택적) */
    if (delta_exec < sysctl_sched_min_granularity)
        return;
}

wakeup 선점 판단

Wakeup 선점은 sleep 상태의 태스크가 깨어날 때 현재 실행 중인 태스크를 선점할지 결정합니다. EEVDF에서는 deadline 비교가 핵심입니다.

/* kernel/sched/fair.c — wakeup 선점 (EEVDF) */
static void
check_preempt_wakeup_fair(struct rq *rq,
                          struct task_struct *p,
                          int wake_flags)
{
    struct task_struct *curr = rq->curr;
    struct sched_entity *se = &p->se;
    struct sched_entity *pse = &curr->se;
    struct cfs_rq *cfs_rq;

    /* RT/DL 클래스가 FAIR보다 높으면 바로 리턴 */
    if (unlikely(p->sched_class != &fair_sched_class))
        return;

    /* 같은 그룹 레벨까지 올라감 */
    find_matching_se(&se, &pse);
    cfs_rq = cfs_rq_of(pse);

    update_curr(cfs_rq);

    /* EEVDF 선점 조건:
     * 1. 새 태스크가 eligible
     * 2. 새 태스크의 deadline < 현재 태스크의 deadline */
    if (entity_eligible(cfs_rq, se) &&
        (s64)(pse->deadline - se->deadline) > 0)
        resched_curr(rq);
}
interactive 태스크의 자연스러운 우선 처리: interactive 태스크는 자주 sleep하므로 양의 lag를 축적합니다. 깨어날 때 lag가 복원되어 eligible 조건을 쉽게 충족하고, 짧은 slice로 인해 이른 deadline을 갖게 됩니다. 이로 인해 별도의 대화형 감지 휴리스틱 없이도 자연스럽게 우선 처리됩니다.

그룹 스케줄링과 EEVDF

리눅스 커널의 그룹 스케줄링(cgroup cpu controller)은 EEVDF에서도 동일하게 동작합니다. 각 task_group은 자체 cfs_rq를 가지며, 그룹 수준과 태스크 수준에서 독립적으로 EEVDF 알고리즘이 적용됩니다.

EEVDF 그룹 스케줄링 계층 Root cfs_rq (CPU 0) EEVDF: pick eligible + earliest VD Group A: sched_entity (w=2048) VD=1200, lag=+300 Group B: sched_entity (w=1024) VD=1500, lag=-100 Group A의 내부 cfs_rq (독립 EEVDF) Task A1 VD=800, eligible Task A2 VD=1100, eligible Group B의 내부 cfs_rq (독립 EEVDF) Task B1 VD=600, eligible Task B2 VD=900, ineligible 2단계 EEVDF: Root에서 Group A(VD=1200) 선택 → A 내부에서 Task A1(VD=800) 선택 Group B는 lag=-100으로 root에서 ineligible → Task B1(VD=600)이 있어도 선택 안 됨
그룹 스케줄링에서 EEVDF는 각 계층에서 독립적으로 eligible + deadline 선택을 수행합니다
/* 그룹 스케줄링 + EEVDF: 계층적 pick */
static struct task_struct *
pick_next_task_fair(struct rq *rq)
{
    struct cfs_rq *cfs_rq = &rq->cfs;
    struct sched_entity *se;

    do {
        /* 각 레벨에서 EEVDF pick */
        se = pick_next_entity(cfs_rq);
        if (!se)
            return NULL;

        /* 태스크가 아닌 그룹이면 내부 cfs_rq로 진입 */
        cfs_rq = group_cfs_rq(se);
    } while (cfs_rq);

    return task_of(se);
}

/* 그룹의 weight는 cpu.weight (또는 cpu.shares) 설정에 의해 결정
 * 예: A 그룹 weight=2048, B 그룹 weight=1024
 * → A가 B의 2배 CPU 시간 할당 */
그룹 간 공정성: EEVDF의 lag 추적은 그룹 수준에서도 동작합니다. 그룹 A가 더 많이 실행되면 그룹 A의 sched_entity에 음의 lag가 쌓이고, root cfs_rq에서 ineligible이 됩니다. 이를 통해 그룹 간 CPU 비율이 weight에 따라 정확히 분배됩니다.

CFS 대역폭 제한과 EEVDF

cgroup v2의 cpu.max (또는 cgroup v1의 cpu.cfs_quota_us)를 통한 대역폭 제한은 EEVDF에서도 동일하게 동작합니다. 쓰로틀링은 EEVDF의 pick 로직과 독립적으로 cfs_rq 수준에서 적용됩니다.

/* cpu.max 설정 예시: 50ms 주기 중 20ms 허용 */
/* → 최대 40% CPU 사용 */

/* kernel/sched/fair.c — 대역폭 쓰로틀링 */
static bool
check_cfs_rq_runtime(struct cfs_rq *cfs_rq)
{
    if (!cfs_bandwidth_used())
        return false;

    if (likely(!cfs_rq->runtime_enabled))
        return false;

    if (cfs_rq->runtime_remaining > 0)
        return false;

    /* 할당량 소진 → 쓰로틀링
     * EEVDF의 pick과 무관하게 cfs_rq 전체를 비활성화 */
    throttle_cfs_rq(cfs_rq);
    return true;
}
# cgroup v2 CPU 대역폭 설정 예시
echo "20000 50000" > /sys/fs/cgroup/mygroup/cpu.max  # 20ms/50ms = 40%
echo "100000 100000" > /sys/fs/cgroup/mygroup/cpu.max # 100ms/100ms = 100% (제한 없음)
echo "max 100000" > /sys/fs/cgroup/mygroup/cpu.max    # 무제한

# EEVDF에서 동작 확인
cat /proc/sched_debug | grep -A5 "cfs_rq\[mygroup\]"
EEVDF와 대역폭의 독립성: 대역폭 제한(throttling)은 account_cfs_rq_runtime()에서 cfs_rq의 runtime_remaining을 차감하고, 소진되면 cfs_rq 전체를 dequeue합니다. EEVDF의 eligible/deadline 판정은 쓰로틀링과 완전히 독립적이므로, 두 메커니즘이 서로 간섭하지 않습니다.

sysctl 튜닝 파라미터

EEVDF 도입 후 일부 CFS 파라미터가 제거되고 새 파라미터가 추가되었습니다.

파라미터기본값EEVDF 상태설명
sched_base_slice_ns3,000,000 (3ms)신규기본 실행 슬라이스, deadline 간격 결정
sched_latency_ns6,000,000 (6ms)유지 (참조용)EEVDF에서는 직접 사용하지 않음
sched_min_granularity_ns750,000 (0.75ms)유지 (최소 실행 보장)tick 선점 최소 간격
sched_wakeup_granularity_ns제거됨제거EEVDF에서는 deadline 비교로 대체
sched_child_runs_first제거됨제거EEVDF의 lag 보존으로 불필요
sched_nr_latency제거됨제거EEVDF에서 타임슬라이스 동적 계산 불필요
# EEVDF 튜닝 파라미터 확인
sysctl kernel.sched_base_slice_ns
# kernel.sched_base_slice_ns = 3000000

# slice 줄이기: 더 짧은 deadline → 더 빠른 응답, 더 잦은 선점
sysctl -w kernel.sched_base_slice_ns=1000000  # 1ms

# slice 늘리기: 더 긴 deadline → 더 나은 처리량, 더 적은 선점
sysctl -w kernel.sched_base_slice_ns=10000000  # 10ms

# 현재 EEVDF 관련 sched 디버그 정보
cat /proc/sched_debug | head -30
sched_base_slice_ns 영향: 이 값을 줄이면 deadline 간격이 짧아져 선점 빈도가 증가합니다. 대화형 응답은 좋아지지만 컨텍스트 스위칭(Context Switching) 오버헤드(Overhead)도 증가합니다. 서버 워크로드에서는 기본값(3ms) 또는 더 높은 값이 적합하고, 데스크톱에서는 1~2ms가 좋을 수 있습니다.

Latency Nice

Latency nice는 EEVDF와 함께 도입된 새로운 태스크 속성으로, nice 값(CPU 비율)과 독립적으로 응답 지연(latency)을 제어합니다.

매핑(Mapping) 규칙

latency_nice 범위: -20 ~ 19 (nice와 동일 범위)

매핑:
  latency_nice → latency_offset

  latency_nice = -20 → 가장 짧은 slice → 가장 이른 deadline → 최소 지연
  latency_nice =   0 → 기본 slice (sched_base_slice_ns)
  latency_nice =  19 → 가장 긴 slice → 가장 늦은 deadline → 최대 지연 허용

관계:
  nice: CPU 시간 "비율" 제어 (weight)
  latency_nice: 응답 "속도" 제어 (slice → deadline)

예시:
  nice=0, latency_nice=-10 → 공정한 CPU 비율이지만 빠른 응답 (짧은 deadline)
  nice=0, latency_nice=+10 → 공정한 CPU 비율이지만 느린 응답 허용 (배치 워크로드)
Latency Nice: slice 크기와 deadline 관계 latency_nice=-20 -10 0 (기본) +19 slice 크기 최소 (~0.75ms) 최대 (~24ms) deadline 간격 짧음 (자주 선택) 김 (덜 선택) 낮은 latency_nice 오디오, 게임, UI 렌더링 짧은 slice → 빠른 응답 더 자주 스케줄링됨 기본 (0) 일반 프로세스 기본 slice (3ms) 높은 latency_nice 컴파일, 인코딩, 백업 긴 slice → 캐시 효율 덜 자주 but 오래 실행
latency_nice는 nice(CPU 비율)와 독립적으로 응답 지연을 제어합니다
/* kernel/sched/core.c — latency_nice → slice 변환 */

/* latency_nice 설정 API */
void set_latency_nice(struct task_struct *p, long nice)
{
    int old_prio = p->latency_prio;

    p->latency_prio = NICE_TO_PRIO(nice);

    if (old_prio != p->latency_prio)
        set_latency_offset(p);
}

/* latency_offset 계산 — slice에 반영 */
static void set_latency_offset(struct task_struct *p)
{
    long nice = PRIO_TO_NICE(p->latency_prio);

    /* nice=-20 → 매우 짧은 slice
     * nice=19  → 매우 긴 slice
     * 기본 slice에 가중치를 곱하여 조정 */
    p->se.slice = sysctl_sched_base_slice *
                  sched_latency_to_slice[nice + 20];
}
# latency_nice 설정 (sched_setattr 시스템 콜)
# chrt 명령으로는 아직 미지원, schedtool 또는 직접 syscall

# C 코드 예시:
# struct sched_attr attr = {
#     .size = sizeof(attr),
#     .sched_flags = SCHED_FLAG_UTIL_CLAMP | SCHED_FLAG_LATENCY_NICE,
#     .sched_latency_nice = -10,  // 빠른 응답
# };
# sched_setattr(pid, &attr, 0);

# 확인
cat /proc//sched | grep latency
nice vs latency_nice 분리의 이점: 기존에는 "CPU를 적게 쓰지만 빠르게 응답해야 하는" 태스크(예: 오디오 데몬)를 nice로 표현하기 어려웠습니다. nice를 낮추면 CPU 비율이 너무 높아지고, nice를 높이면 응답이 느려졌습니다. latency_nice는 이 두 차원을 분리합니다.

워크로드별 성능 분석

EEVDF는 워크로드 유형에 따라 CFS와 다른 성능 특성을 보입니다.

대화형 워크로드

특성CFSEEVDF영향
wakeup 레이턴시wakeup_gran에 의해 제한deadline 비교로 즉시 선점 가능EEVDF 유리 (더 빠른 응답)
짧은 burst 처리buddy 힌트로 캐시 친화lag 보존으로 eligible 우선EEVDF 유리 (수학적 공정성)
선점 빈도wakeup_gran + min_granularitydeadline 비교만EEVDF에서 더 잦을 수 있음

배치 워크로드

특성CFSEEVDF영향
처리량(Throughput)buddy로 캐시 재사용 유도deadline 기반 엄격 공정CFS 약간 유리 (캐시 효율)
공정성vruntime 편차 존재lag bounded by r_maxEEVDF 유리 (수학적 보장)
컨텍스트 스위치min_granularity로 최소화slice로 제어비슷 (slice 튜닝으로 조절 가능)

혼합 워크로드

시나리오: 4 CPU-bound + 1 interactive (주기적 1ms burst)

CFS:
  - interactive가 wakeup 시 wakeup_gran(1ms) 후에야 선점
  - next/last buddy로 CPU-bound가 캐시 재사용
  - interactive 응답 시간: ~2-5ms (변동)

EEVDF:
  - interactive가 wakeup 시 즉시 eligible (lag > 0)
  - deadline 비교로 확정적 선점
  - interactive 응답 시간: ~0.5-1ms (안정적)

→ 혼합 워크로드에서 EEVDF가 더 예측 가능한 지연을 제공
혼합 워크로드: CFS vs EEVDF 스케줄링 타임라인 시나리오: CPU-bound 3개 + interactive 1개 (주기적 1ms burst) CFS (v6.5) 시간(ms) CPU-A CPU-B CPU-C wakeup! 대기 CPU-A Int wakeup! ~2-5ms 지연 CFS: wakeup_gran(1ms) + buddy 힌트 → interactive 지연 변동 크고 예측 불가 EEVDF (v6.6+) 시간(ms) CPU-A B wakeup! Int wakeup! Int ~0.5ms 즉시 선점 EEVDF: interactive의 lag>0 → eligible, deadline 비교로 즉시 선점 → 안정적 ~0.5-1ms 응답 CPU-A CPU-B CPU-C Interactive 대기 시간
EEVDF는 interactive 태스크가 wakeup하면 lag 기반 eligible + deadline 비교로 즉시 선점하여 안정적인 응답을 제공합니다
실제 벤치마크 결과: Peter Zijlstra의 패치셋에서 보고된 바에 따르면, EEVDF는 hackbench에서 CFS와 동등하거나 약간 나은 성능을, schbench에서 현저히 개선된 지연 특성을 보여주었습니다.

벤치마크 비교

CFS와 EEVDF의 성능을 다양한 벤치마크로 비교합니다.

벤치마크측정 항목CFS (v6.5)EEVDF (v6.6)차이
hackbench처리량 (msg/s)~100,000~102,000+2% (오차 내)
schbench (p50)지연 (us)~85~42-50%
schbench (p99)지연 (us)~350~120-66%
tbench처리량~5,200~5,150-1% (오차 내)
sysbench oltpTPS~15,000~15,200+1%
cyclictest (avg)지연 (us)~12~8-33%
compile kernel시간 (s)~300~298-0.7%
CFS vs EEVDF 벤치마크 시각화 처리량 (높을수록 좋음) hackbench CFS: 100K EEVDF: 102K (+2%) tbench CFS: 5,200 EEVDF: 5,150 (-1%) kernel build CFS: 300s EEVDF: 298s (-0.7%) → 처리량: CFS ≈ EEVDF (동등) 지연 (낮을수록 좋음) schbench p50 CFS: 85μs EEVDF: 42μs (-50%!) schbench p99 CFS: 350μs EEVDF: 120μs (-66%!) cyclictest avg CFS: 12μs EEVDF: 8μs (-33%) → 지연: EEVDF 현저히 개선! CFS (v6.5) EEVDF (v6.6) 핵심 관찰 1. 처리량(Throughput): CFS와 EEVDF는 거의 동일 (±2% 오차 범위) 2. 지연(Latency): EEVDF가 p50에서 50%, p99에서 66% 개선 — 특히 tail latency 대폭 감소 3. 의미: EEVDF는 "처리량을 희생하지 않고 지연을 개선"하는 Pareto 개선 4. 원인: deadline 기반 선택이 buddy 휴리스틱보다 더 결정적이고 공정한 스케줄링을 제공
EEVDF는 처리량을 유지하면서 지연(특히 tail latency)을 크게 개선하는 Pareto 개선입니다
벤치마크 해석 주의: 위 수치는 특정 하드웨어/설정에서의 대략적 경향이며, 절대값은 환경에 따라 크게 달라질 수 있습니다. 핵심 관찰은: (1) 처리량은 CFS와 동등, (2) 지연(특히 tail latency)은 EEVDF가 현저히 개선.
# 직접 벤치마크 실행하기

# hackbench: IPC 처리량
hackbench -g 20 -l 10000 --pipe

# schbench: 스케줄러 지연 측정
schbench -m 2 -t 8 -r 30  # 2 메시지 그룹, 8 스레드, 30초

# tbench: 처리량
tbench_srv &
tbench -t 60 $(nproc) 127.0.0.1

# cyclictest: 실시간 지연
cyclictest -m -p 80 -i 1000 -l 100000

# perf로 스케줄러 이벤트 측정
perf sched record -- sleep 10
perf sched latency

ftrace/perf로 EEVDF 관측

EEVDF의 동작을 런타임에 관측하기 위한 도구와 방법을 소개합니다.

관련 tracepoint

tracepoint용도EEVDF 특이점
sched:sched_switch태스크 전환prev/next의 deadline 확인 가능
sched:sched_wakeup태스크 wakeupwakeup 선점 여부 관측
sched:sched_stat_runtime실행 시간 통계slice 소진 패턴 분석
sched:sched_migrate_taskCPU 마이그레이션lag 보존 확인
# ftrace로 스케줄러 이벤트 기록
cd /sys/kernel/debug/tracing
echo 0 > tracing_on
echo sched_switch sched_wakeup > set_event

# 특정 프로세스만 추적
echo $PID > set_ftrace_pid

echo 1 > tracing_on
sleep 5
echo 0 > tracing_on
cat trace | head -50

# perf로 스케줄러 지연 분석
perf sched record -- sleep 10
perf sched latency --sort max

# perf로 EEVDF 관련 함수 프로파일링
perf probe --add 'pick_eevdf'
perf stat -e 'probe:pick_eevdf' -- sleep 10

# bpftrace로 deadline 분포 관측
bpftrace -e '
kprobe:pick_eevdf {
    @pick_count = count();
}
kretprobe:pick_eevdf {
    @pick_lat = hist(nsecs - @start[tid]);
}
'
# /proc/sched_debug에서 EEVDF 정보 확인
cat /proc/sched_debug | grep -A20 "cfs_rq\[0\]"

# 출력 예시 (EEVDF 관련 필드):
# cfs_rq[0]:/
#   .nr_running                    : 3
#   .min_vruntime                  : 1234567.890123
#   .avg_vruntime                  : 456789
#   .avg_load                      : 3072

# 특정 태스크의 스케줄링 정보
cat /proc/$PID/sched | grep -E "vruntime|deadline|slice|lag"

# 출력 예시:
# se.vruntime                      : 1234567.890123
# se.deadline                      : 1234570.819846
# se.slice                         : 3000000
# se.vlag                          : 0
EEVDF 디버깅(Debugging) 팁: /proc/PID/sched에서 se.deadline - se.vruntime 값이 양수이면 태스크가 아직 slice를 소진하지 않은 상태이고, 0 이하이면 다음 tick에서 선점 대상입니다. se.vlag가 큰 양수이면 해당 태스크가 오래 sleep했다가 깨어난 것입니다.

디버깅 시나리오

시나리오 1: 태스크 기아(Starvation) 의심

# 1. 대상 태스크의 스케줄링 정보 확인
cat /proc/$PID/sched

# 2. lag 확인: vlag가 큰 양수이면 덜 받고 있음
cat /proc/$PID/sched | grep vlag

# 3. eligible 여부 간접 확인:
#    vruntime < avg_vruntime → eligible
#    vruntime > avg_vruntime → ineligible

# 4. EEVDF에서 starvation은 이론적으로 불가능
#    (lag bounded by r_max)
#    실제로 발생하면 버그이므로 커널 로그 확인
dmesg | grep -i sched

시나리오 2: 높은 스케줄링 레이턴시

# 1. perf로 wakeup-to-run 레이턴시 측정
perf sched record -- sleep 30
perf sched latency --sort max | head -20

# 2. slice가 너무 큰지 확인
sysctl kernel.sched_base_slice_ns
# 3ms 이상이면 줄여보기
sysctl -w kernel.sched_base_slice_ns=1000000

# 3. cgroup bandwidth throttling 확인
cat /sys/fs/cgroup/*/cpu.max
cat /sys/fs/cgroup/*/cpu.stat | grep throttled

# 4. RT 태스크가 FAIR를 밀어내고 있는지 확인
ps -eo pid,cls,pri,comm | grep -E "RR|FF"

# 5. bpftrace로 pick_eevdf 호출 빈도 및 레이턴시
bpftrace -e 'kprobe:pick_eevdf { @start[tid] = nsecs; }
kretprobe:pick_eevdf /@start[tid]/ {
    @us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

시나리오 3: 불공정 CPU 분배

# 1. CPU 사용률 확인 (동일 nice 태스크가 동일 비율인지)
pidstat -u 1 10

# 2. /proc/sched_debug에서 vruntime 편차 확인
cat /proc/sched_debug | grep -A50 "cfs_rq\[0\]"

# 3. NUMA 밸런싱이 불공정을 유발하는지
numastat -p $PID
cat /proc/$PID/numa_maps

# 4. EEVDF에서는 |lag| <= r_max 보장이므로
#    불공정이 r_max를 넘으면 구현 버그
cat /proc/$PID/sched | grep -E "sum_exec|vruntime|deadline"
EEVDF 버그 의심 시: EEVDF는 수학적으로 |lag| <= slice를 보장합니다. 관측된 불공정이 이 범위를 크게 넘으면 커널 버그일 수 있습니다. CONFIG_SCHED_DEBUG=y로 빌드하고 /proc/sched_debug의 상세 정보를 수집하여 버그 리포트에 포함하세요.

관련 커널 설정 옵션

# EEVDF 관련 커널 설정 (make menuconfig)

# 기본 활성화 (v6.6+ 기본)
CONFIG_SCHED_CLASS_FAIR=y           # FAIR 스케줄러 클래스

# 디버깅
CONFIG_SCHED_DEBUG=y                # /proc/sched_debug, /proc/PID/sched
CONFIG_SCHEDSTATS=y                 # 스케줄러 통계 수집

# 선점 모델 (EEVDF와 독립적이지만 동작에 영향)
CONFIG_PREEMPT_NONE=y               # 서버 (최소 선점)
CONFIG_PREEMPT_VOLUNTARY=y          # 데스크톱 (중간, 기본)
CONFIG_PREEMPT=y                    # 실시간 (최대 선점)
CONFIG_PREEMPT_RT=y                 # PREEMPT_RT (실시간)

# 그룹 스케줄링 (EEVDF + cgroup 연동)
CONFIG_CGROUP_SCHED=y               # cgroup 스케줄러 지원
CONFIG_FAIR_GROUP_SCHED=y           # FAIR 그룹 스케줄링
CONFIG_CFS_BANDWIDTH=y              # cpu.max 대역폭 제한

# PELT (EEVDF와 함께 동작)
CONFIG_SMP=y                        # 멀티프로세서 (PELT 필요)

# sched_ext (BPF 확장)
CONFIG_SCHED_CLASS_EXT=y            # BPF 스케줄러 확장
설정기본값EEVDF 영향
CONFIG_SCHED_DEBUGy/proc/sched_debug에 EEVDF 필드 출력
CONFIG_SCHEDSTATSn활성화 시 성능 통계 수집 (약간의 오버헤드)
CONFIG_FAIR_GROUP_SCHEDycgroup별 독립 EEVDF 인스턴스
CONFIG_CFS_BANDWIDTHycpu.max 쓰로틀링 (EEVDF와 독립)
CONFIG_PREEMPT_VOLUNTARYyEEVDF 선점 판단은 유지, 실행 시점만 다름

CFS→EEVDF 마이그레이션 가이드

커널 6.5 이전(CFS)에서 6.6 이후(EEVDF)로 업그레이드할 때 고려해야 할 사항들입니다.

CFS → EEVDF 전환 타임라인 v6.5 마지막 순수 CFS 커널 buddy, wakeup_gran v6.6 EEVDF 도입! pick_eevdf() deadline, slice buddy 제거 v6.7 slice 튜닝 기본값 조정 안정화 v6.8-6.9 place_entity 리팩토링 latency_nice v6.11-6.12 eligible 최적화 NUMA 연동 성능 안정화 전환 체크리스트 1. sched_wakeup_granularity_ns 관련 sysctl 스크립트 제거 2. sched_child_runs_first 관련 설정 제거 3. sched_base_slice_ns 확인 (기본 3ms, 워크로드에 맞게 조정) 4. schbench로 지연 특성 비교 검증 5. latency_nice 도입 검토 (오디오/게임/배치 분리)
v6.6에서 EEVDF 도입 후 지속적으로 개선 중입니다

마이그레이션 체크리스트

[ ] 1. 제거된 sysctl 파라미터 확인
      - sched_wakeup_granularity_ns → 제거 (EEVDF deadline으로 대체)
      - sched_child_runs_first → 제거 (lag 기반으로 불필요)
      - sched_nr_latency → 제거 (동적 타임슬라이스 불필요)
      → 이 파라미터를 설정하는 스크립트/설정 정리

[ ] 2. sched_base_slice_ns 설정
      - 기본 3ms
      - 데스크톱: 1~2ms (더 나은 응답)
      - 서버/HPC: 3~10ms (더 나은 처리량)

[ ] 3. 벤치마크 비교
      - 기존 워크로드로 CFS vs EEVDF 성능 비교
      - 특히 tail latency (p99, p999) 개선 확인

[ ] 4. buddy 의존성 제거
      - CFS의 next/last buddy에 의존하는 워크로드 패턴 확인
      - EEVDF에서는 deadline이 결정적이므로 buddy 불필요

[ ] 5. latency_nice 도입 검토
      - 오디오/영상 처리: latency_nice=-10~-20
      - 일반: latency_nice=0
      - 배치/빌드: latency_nice=10~19

[ ] 6. cgroup 설정 확인
      - cpu.weight, cpu.max는 변경 없이 동작
      - EEVDF와 bandwidth throttling은 독립적

[ ] 7. 모니터링 갱신
      - /proc/PID/sched에서 deadline, slice, vlag 필드 확인
      - perf sched latency로 정기 측정
호환성 주의: CFS 시절의 sched_wakeup_granularity_ns=0 설정(aggressive preemption)은 EEVDF에서 불필요하며, 설정 자체가 존재하지 않습니다. 이 설정을 시도하면 sysctl: cannot stat 에러가 발생합니다. 시스템 초기화 스크립트에서 이 파라미터를 제거해야 합니다.

Virtual Time 타임라인 상세

여러 태스크의 vruntime과 virtual deadline이 시간에 따라 어떻게 진행되는지를 시각화합니다.

EEVDF Virtual Time 타임라인 (3 태스크) 실제 시간 가상 시간 (vruntime) avg_vruntime A (nice=0) B (nice=-5) C (nice=5) t1 t2 t3 t4 weight 클수록(nice 작을수록) vruntime 느리게 증가 B(nice=-5): vruntime 느림 → 자주 eligible → 더 많은 CPU C(nice=5): vruntime 빠름 → 자주 ineligible → 더 적은 CPU
weight가 큰 태스크(nice=-5)는 vruntime이 느리게 증가하여 자주 eligible이 됩니다
/* vruntime 증가율과 weight의 관계 */
/*
 * calc_delta_fair(delta, se):
 *   return delta * NICE_0_LOAD / se->load.weight
 *
 * 예시 (1ms 실제 실행):
 *   nice 0  (w=1024): vruntime += 1ms * 1024/1024 = 1ms
 *   nice -5 (w=3121): vruntime += 1ms * 1024/3121 = 0.328ms
 *   nice 5  (w=335):  vruntime += 1ms * 1024/335  = 3.057ms
 *
 * → 높은 weight = 느린 vruntime 증가 = avg 아래 유지 = eligible
 * → 낮은 weight = 빠른 vruntime 증가 = avg 위로 = ineligible
 */

static u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
    if (unlikely(se->load.weight != NICE_0_LOAD))
        delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
    return delta;
}

내부 구현 세부 사항

min_vruntime 갱신

/* kernel/sched/fair.c — min_vruntime 갱신
 * EEVDF에서도 CFS와 동일한 min_vruntime 메커니즘 사용
 * avg_vruntime 계산의 기준점 역할 */
static void update_min_vruntime(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    struct rb_node *leftmost = rb_first_cached(&cfs_rq->tasks_timeline);
    u64 vruntime = cfs_rq->min_vruntime;

    if (curr) {
        if (curr->on_rq)
            vruntime = curr->vruntime;
        else
            curr = NULL;
    }

    if (leftmost) {
        struct sched_entity *se = __node_2_se(leftmost);
        if (!curr)
            vruntime = se->vruntime;
        else
            vruntime = min_vruntime(vruntime, se->vruntime);
    }

    /* min_vruntime은 단조 증가만 허용 */
    u64 delta = vruntime - cfs_rq->min_vruntime;
    if ((s64)delta > 0) {
        avg_vruntime_update(cfs_rq, delta);
        cfs_rq->min_vruntime = vruntime;
    }
}

enqueue/dequeue 흐름

/* enqueue_entity — EEVDF 필드 초기화 포함 */
static void
enqueue_entity(struct cfs_rq *cfs_rq,
               struct sched_entity *se, int flags)
{
    /* 1. EEVDF: vruntime/deadline 배치 */
    if (!(flags & ENQUEUE_WAKEUP) || !(flags & ENQUEUE_MIGRATED))
        se->vlag = 0;

    place_entity(cfs_rq, se, flags);

    /* 2. RB-Tree 삽입 */
    __enqueue_entity(cfs_rq, se);

    /* 3. avg_vruntime 업데이트 */
    avg_vruntime_add(cfs_rq, se);

    se->on_rq = 1;
    cfs_rq->nr_running++;
}
/* __enqueue_entity — RB-Tree 삽입 + augmentation */
static void __enqueue_entity(struct cfs_rq *cfs_rq,
                              struct sched_entity *se)
{
    /* vruntime 기반 정렬 삽입 */
    rb_add_augmented_cached(&se->run_node,
                            &cfs_rq->tasks_timeline,
                            __entity_less,
                            &min_deadline_cb);
    /* augmented 삽입이므로 min_deadline이 자동 전파됨 */
}

yield_to()의 EEVDF 구현

/* yield — 현재 태스크의 deadline을 무시하고 양보 */
static void yield_task_fair(struct rq *rq)
{
    struct sched_entity *se = &rq->curr->se;
    struct cfs_rq *cfs_rq = cfs_rq_of(se);

    /* EEVDF: skip 설정으로 pick_eevdf에서 건너뛰기 */
    if (cfs_rq->nr_running > 1) {
        cfs_rq->skip = se;
        set_skip_buddy(se);
    }
}

/* pick_eevdf에서 skip 처리:
 * skip이 설정된 태스크는 eligible + earliest deadline이어도
 * 다음 후보를 선택 */

NUMA 밸런싱과 EEVDF

NUMA(Non-Uniform Memory Access) 시스템에서 태스크가 CPU 간 마이그레이션(Migration)될 때, 각 CPU의 cfs_rq는 독립적인 avg_vruntimemin_vruntime을 가집니다. EEVDF는 vlag(lag 값)을 보존하여 마이그레이션 후에도 공정성을 유지합니다.

NUMA 마이그레이션: vlag 보존을 통한 공정성 유지 CPU 0 (NUMA Node 0) avg_vruntime = 10,000,000 min_vruntime = 8,000,000 Task P: vruntime=9,200,000 lag = avg - vruntime = 800,000 → "800,000만큼 덜 받은 상태" migrate! ① dequeue(CPU 0) vlag = 800,000 저장 ② enqueue(CPU 3) CPU 3 (NUMA Node 1) avg_vruntime = 25,000,000 min_vruntime = 22,000,000 Task P: vruntime=24,200,000 vruntime = avg - vlag = 25M - 800K → lag 동일하게 보존! (800,000) CFS 방식 (문제점) vruntime_new = vruntime - min_vr(old) + min_vr(new) 문제: min_vruntime 차이가 클 때 정밀도 손실 → 불공정 발생 가능 특히 CPU 간 부하 비대칭 시 악화 sleeper credit이 reset될 수 있음 EEVDF 방식 (해결) vlag = entity_lag(avg_old, se) vruntime = avg_new - vlag 장점: lag 값 자체를 보존 → min_vruntime 차이와 무관 → 공정성 정확히 유지 → NUMA 불균형에도 강건
EEVDF는 마이그레이션 시 절대 vruntime이 아닌 상대적 lag(vlag)를 보존하여 CPU 간 공정성을 유지합니다
/* NUMA 마이그레이션 시 lag 보존
 * 태스크가 CPU A → CPU B로 이동할 때:
 * 1. dequeue: vlag = entity_lag(avg_vruntime_A, se)
 * 2. enqueue: place_entity에서 vlag를 CPU B의 avg_vruntime에 적용
 *
 * 이를 통해 NUMA 마이그레이션 후에도 공정성이 유지됨 */

/* v6.12: migrate_deadline 필드 추가 */
static void migrate_task_rq_fair(struct task_struct *p, int new_cpu)
{
    struct sched_entity *se = &p->se;

    /* lag 저장 (dequeue에서 이미 설정됨) */
    /* se->vlag는 이미 dequeue_entity에서 설정 */

    /* deadline 상대값 보존 */
    se->migrate_deadline = se->deadline - se->vruntime;
}
EEVDF의 NUMA 이점: CFS는 NUMA 마이그레이션 시 min_vruntime 차이 보정이 필요했고, 이 과정에서 vruntime이 부정확해질 수 있었습니다. EEVDF는 lag 값 자체를 보존하므로, CPU 간 가상 시간 차이와 무관하게 정확한 공정성을 유지합니다.

sched_ext와 EEVDF의 관계

Linux 6.12에서 도입된 sched_ext(BPF 확장 스케줄러)는 EEVDF와 독립적으로 동작하는 별도의 sched_class입니다.

/* sched_class 우선순위 (높은 것이 먼저 선택):
 * stop_sched_class > dl_sched_class > rt_sched_class
 *   > fair_sched_class (EEVDF) > ext_sched_class > idle_sched_class
 *
 * sched_ext는 fair(EEVDF)보다 낮은 우선순위
 * → EEVDF 태스크가 있으면 sched_ext는 실행되지 않음
 * → sched_ext는 자체 BPF 프로그램으로 pick 로직 구현
 */

/* 그러나 sched_ext가 FAIR 클래스의 태스크를 "가져오면"
 * EEVDF 대신 BPF 로직이 적용됨 */
sched_ext vs EEVDF: sched_ext를 사용하면 EEVDF를 완전히 우회하고 BPF 프로그램이 스케줄링을 제어합니다. 그러나 대부분의 프로덕션 환경에서는 EEVDF가 충분하며, sched_ext는 실험적 스케줄링 정책이나 특수 워크로드에 적합합니다.

EEVDF의 한계와 알려진 이슈

이슈설명상태
O(log N) pick 비용CFS의 O(1) leftmost보다 느림early termination으로 완화
cache thrashingbuddy 제거로 캐시 친화도 감소 가능대부분 체감 차이 없음
큰 nr_running태스크 수 많으면 RB-Tree 깊이 증가O(log N)이므로 실무상 문제 없음
latency_nice 미완일부 커널에서 API 불완전v6.9+ 점진 안정화
cgroup 상호작용그룹 간 EEVDF 동작 미세 차이v6.11+ 개선 중
/* EEVDF pick 비용 분석:
 *
 * 최선: O(1) — leftmost가 eligible + earliest deadline
 * 평균: O(log N) — RB-Tree 루트에서 한 경로 탐색
 * 최악: O(log N) — 트리 전체 높이 탐색
 *
 * 실측 (4 CPU, 100 태스크):
 *   CFS pick:   ~30ns (O(1) rb_first_cached)
 *   EEVDF pick:  ~80ns (O(log 100) ≈ O(7))
 *
 * 차이: ~50ns — 3ms slice 대비 0.002% 오버헤드
 * → 사실상 무시 가능 */
감지된 회귀: 초기 v6.6에서 일부 게임/VR 워크로드에서 미세한 프레임 시간 변동이 보고되었습니다. 이는 buddy 제거로 인한 캐시 패턴 변화와 관련이 있었으며, v6.7에서 slice 기본값 조정으로 대부분 해소되었습니다. 여전히 문제가 있다면 sched_base_slice_ns를 줄여보세요.

EEVDF vs SCHED_DEADLINE

EEVDF와 SCHED_DEADLINE은 모두 "deadline" 개념을 사용하지만, 목적과 구현이 완전히 다릅니다.

비교 항목EEVDF (SCHED_NORMAL)SCHED_DEADLINE
sched_classfair_sched_classdl_sched_class (더 높은 우선순위)
deadline 의미가상 시간(vruntime 공간)실제 시간(wall clock)
보장 수준비례 공유 (best-effort 지연)하드 실시간 (EDF 보장)
입력 파라미터nice, latency_niceruntime, deadline, period
대상일반 워크로드실시간 워크로드 (오디오, 산업 제어)
기아 방지lag 바운드admission control
CPU 독점불가능 (공정 분배)가능 (예약된 시간 보장)
/* sched_class 우선순위 계층에서의 위치:
 *
 * dl_sched_class (SCHED_DEADLINE)
 *   ↑ 더 높은 우선순위
 *   |
 * rt_sched_class (SCHED_FIFO, SCHED_RR)
 *   |
 * fair_sched_class (SCHED_NORMAL — EEVDF)
 *   |
 *   ↓ 더 낮은 우선순위
 * idle_sched_class (SCHED_IDLE)
 *
 * EEVDF의 virtual deadline은 fair_sched_class 내에서만 유효
 * SCHED_DEADLINE 태스크가 있으면 항상 먼저 실행됨
 */
EEVDF vs SCHED_DEADLINE: deadline의 두 가지 의미 EEVDF (SCHED_NORMAL) 가상 시간(vruntime) 공간 V(t) VE VD(가상) slice/w • deadline = 가상 시간 내 마감 • 목적: 비례 공유 (공정성) • 입력: nice, latency_nice • 보장: |lag| ≤ slice (상대적 공정) • 기아 방지: lag 자동 바운드 • 관리자 권한 불필요 적합한 워크로드 ✓ 데스크톱 앱, 웹 서버, DB ✓ 컨테이너, 클라우드 워크로드 ✓ 일반적인 대화형 응답성 ✓ 배치 컴파일, 데이터 처리 SCHED_DEADLINE (dl_sched_class) 실제 시간(wall clock) 공간 t runtime deadline(실제) period • deadline = 실제 시간 마감 • 목적: 하드 실시간 보장 • 입력: runtime, deadline, period • 보장: deadline 내 runtime 완료 • 기아 방지: admission control • CAP_SYS_NICE 또는 root 필요 적합한 워크로드 ✓ 실시간 오디오 (JACK, PipeWire) ✓ 산업 제어 시스템 (PLC) ✓ 로봇 제어, 모터 PWM ✓ 네트워크 패킷 처리 (XDP) DL > RT > FAIR(EEVDF) > IDLE
EEVDF의 virtual deadline은 공정성을 위한 가상 시간 마감이고, SCHED_DEADLINE의 deadline은 실시간 보장을 위한 실제 시간 마감입니다
언제 SCHED_DEADLINE을 쓰나: 실시간 오디오 처리(JACK), 산업 제어 시스템 등 절대적 시간 보장이 필요한 경우에만 SCHED_DEADLINE을 사용합니다. 일반적인 대화형 응답성 개선은 EEVDF의 latency_nice로 충분합니다.

전체 코드 워크스루

태스크가 wakeup되어 선점하고, 실행되고, 다시 schedule되는 전체 과정을 EEVDF 관점에서 추적합니다.

EEVDF 태스크 생명주기 흐름도 ① Wakeup & Enqueue try_to_wake_up(p) place_entity(): lag 복원 vruntime = avg - vlag RB-Tree 삽입 + min_dl 전파 ② 선점 판단 check_preempt_wakeup_fair() eligible? VD < curr VD? Yes resched_curr() → 선점! No 대기 ③ 선택 & 실행 schedule() → pick_eevdf() eligible + earliest VD 선택 context_switch(prev, next) 태스크 실행 시작! ④ 실행 중 (매 Tick) scheduler_tick() update_curr(): vruntime ↑ update_deadline() vruntime >= deadline? Yes: 선점 No: 계속 실행 ⑤ Sleep & Dequeue (또는 선점) dequeue_entity() vlag = entity_lag() ← lag 저장! RB-Tree 제거 + avg_vruntime_sub() SLEEPING (vlag 보존됨) 다음 wakeup 시 ①로 돌아감 핵심: sleep 시 lag 저장 → wakeup 시 lag 복원 → 수학적 공정성 순환
EEVDF 태스크 생명주기: wakeup(lag 복원) → 선점 판단 → 실행 → tick(deadline 확인) → sleep(lag 저장) 순환
/*
 * 전체 EEVDF 경로 워크스루:
 *
 * 1. try_to_wake_up(p)
 *    → ttwu_queue(p, cpu)
 *    → ttwu_do_activate(rq, p)
 *    → activate_task(rq, p, ENQUEUE_WAKEUP)
 *    → enqueue_task_fair(rq, p, ENQUEUE_WAKEUP)
 *    → enqueue_entity(cfs_rq, se, ENQUEUE_WAKEUP)
 *    → place_entity(cfs_rq, se, ENQUEUE_WAKEUP)
 *       ↳ vruntime = avg_vruntime - se->vlag  (lag 복원)
 *       ↳ deadline = vruntime + slice/weight
 *    → __enqueue_entity(cfs_rq, se)  (RB-Tree 삽입 + min_deadline 전파)
 *    → avg_vruntime_add(cfs_rq, se)  (avg_vruntime 갱신)
 *
 * 2. check_preempt_wakeup_fair(rq, p)
 *    → entity_eligible(cfs_rq, se)   (se의 vruntime <= avg?)
 *    → deadline 비교: se->deadline < curr->deadline?
 *    → resched_curr(rq)              (선점 예약)
 *
 * 3. schedule() → __schedule()
 *    → pick_next_task_fair(rq)
 *    → pick_next_entity(cfs_rq)
 *    → pick_eevdf(cfs_rq)
 *       ↳ eligible 필터링 + earliest deadline 선택
 *    → context_switch(rq, prev, next)
 *
 * 4. 실행 중: scheduler_tick()
 *    → task_tick_fair(rq, p)
 *    → entity_tick(cfs_rq, curr)
 *    → update_curr(cfs_rq)
 *       ↳ vruntime += calc_delta_fair(delta_exec, curr)
 *       ↳ update_deadline(cfs_rq, curr)
 *    → check_preempt_tick(cfs_rq, curr)
 *       ↳ vruntime >= deadline? → resched_curr
 *
 * 5. 태스크 sleep: dequeue_task_fair()
 *    → dequeue_entity(cfs_rq, se)
 *    → se->vlag = entity_lag(avg_vruntime, se)  (lag 저장!)
 *    → __dequeue_entity(cfs_rq, se)
 *    → avg_vruntime_sub(cfs_rq, se)
 */
핵심 흐름 요약: wakeup 시 place_entity()에서 lag를 복원하고 deadline을 설정한 뒤, check_preempt_wakeup_fair()에서 eligible + deadline 비교로 선점을 결정합니다. 실행 중에는 update_curr()update_deadline()이 slice 소진을 추적합니다.

수치 시뮬레이션 예제

EEVDF의 스케줄링 결정 과정을 구체적 숫자로 추적합니다. 3개 태스크(A: nice=0, B: nice=-5, C: nice=5)가 동시에 실행 대기 중인 상황에서 EEVDF가 어떻게 선택하는지 단계별로 시뮬레이션합니다.

초기 조건

시스템 설정:
  sched_base_slice_ns = 3,000,000 (3ms)
  NICE_0_LOAD = 1024

태스크 초기 상태 (모두 동시에 enqueue):
  A: nice=0,  weight=1024, vruntime=0, slice=3ms
  B: nice=-5, weight=3121, vruntime=0, slice=3ms
  C: nice=5,  weight=335,  vruntime=0, slice=3ms

VD(Virtual Deadline) 계산:
  VD = vruntime + slice × NICE_0_LOAD / weight

  A: VD = 0 + 3,000,000 × 1024/1024 = 3,000,000
  B: VD = 0 + 3,000,000 × 1024/3121 = 984,300   ← 가장 이른 deadline
  C: VD = 0 + 3,000,000 × 1024/335  = 9,171,642

avg_vruntime 초기값:
  avg = Σ(key_i × w_i) / Σ(w_i)
  = (0×1024 + 0×3121 + 0×335) / (1024+3121+335)
  = 0

Step 1: 최초 pick (t=0)

eligible 판정 (vruntime <= avg_vruntime?):
  A: 0 <= 0 → eligible ✓
  B: 0 <= 0 → eligible ✓
  C: 0 <= 0 → eligible ✓

earliest deadline 선택:
  A.VD=3,000,000  B.VD=984,300  C.VD=9,171,642
                      ↑ 최소!

→ pick_eevdf() 선택: 태스크 B (nice=-5)

Step 2: B가 1ms 실행 후 (t=1ms)

update_curr() — B의 vruntime 갱신:
  delta_exec = 1,000,000 ns (1ms 실제 실행)
  vruntime 증가 = delta × NICE_0_LOAD / weight
                = 1,000,000 × 1024 / 3121
                = 328,100 (가상 시간)

  B.vruntime = 0 + 328,100 = 328,100

avg_vruntime 갱신:
  avg = (0×1024 + 328,100×3121 + 0×335) / (1024+3121+335)
      = 1,023,840,100 / 4,480
      = 228,536

eligible 판정:
  A: 0 <= 228,536 → eligible ✓ (vruntime이 avg보다 낮음 = 덜 받음)
  B: 328,100 > 228,536 → INELIGIBLE ✗ (avg보다 높음 = 더 받음)
  C: 0 <= 228,536 → eligible ✓

B는 아직 slice(VD=984,300)를 소진하지 않았지만 (vruntime=328,100 < VD),
여전히 ineligible이므로 선점 검사에서 다른 태스크가 선택될 수 있음.

그러나 tick 선점은 vruntime >= deadline 일 때만 발생하므로,
B는 계속 실행 (tick에서 deadline 미초과)

Step 3: B가 3ms(slice) 실행 완료 후 (t=3ms)

update_curr() — B의 vruntime 갱신:
  B.vruntime = 3,000,000 × 1024 / 3121 = 984,300

deadline 확인: B.vruntime(984,300) >= B.VD(984,300) → 같음!
→ update_deadline(): B.VD = 984,300 + 984,300 = 1,968,600
→ resched_curr() — 선점 예약!

avg_vruntime:
  avg = (0×1024 + 984,300×3121 + 0×335) / 4,480
      = 3,072,060,300 / 4,480
      = 685,728

eligible 판정:
  A: 0 <= 685,728 → eligible ✓
  B: 984,300 > 685,728 → INELIGIBLE ✗
  C: 0 <= 685,728 → eligible ✓

pick_eevdf() — eligible 태스크 중 earliest VD:
  A.VD = 3,000,000
  C.VD = 9,171,642

→ 선택: 태스크 A (VD=3,000,000이 더 이름)

Step 4: A가 3ms 실행 후 (t=6ms)

A.vruntime = 3,000,000 × 1024/1024 = 3,000,000
A.vruntime(3,000,000) >= A.VD(3,000,000) → deadline 도달!
→ A.VD = 3,000,000 + 3,000,000 = 6,000,000

avg_vruntime:
  avg = (3,000,000×1024 + 984,300×3121 + 0×335) / 4,480
      = (3,072,000,000 + 3,072,060,300) / 4,480
      = 6,144,060,300 / 4,480
      = 1,371,442

eligible:
  A: 3,000,000 > 1,371,442 → INELIGIBLE ✗
  B: 984,300 <= 1,371,442 → eligible ✓ (다시 eligible!)
  C: 0 <= 1,371,442 → eligible ✓

pick_eevdf():
  B.VD = 1,968,600
  C.VD = 9,171,642

→ 선택: 태스크 B (VD=1,968,600이 더 이름)
  B가 다시 선택됨 — weight가 높아 자주 스케줄링!

누적 실행 시간 (실제):
  A: 3ms (1회), B: 3ms (1회), C: 0ms (0회)
  이상적 비율: A=1024, B=3121, C=335 → A:22.8%, B:69.7%, C:7.5%
  실제 비율: A:50%, B:50%, C:0% → 아직 수렴 전

Step 5: B가 다시 3ms 실행 후 (t=9ms)

B.vruntime = 984,300 + 984,300 = 1,968,600
B.VD = 1,968,600 + 984,300 = 2,952,900

avg_vruntime ≈ 2,057,171

eligible:
  A: 3,000,000 > 2,057,171 → INELIGIBLE ✗
  B: 1,968,600 <= 2,057,171 → eligible ✓ (아직 eligible!)
  C: 0 <= 2,057,171 → eligible ✓

pick_eevdf():
  B.VD = 2,952,900
  C.VD = 9,171,642

→ 선택: 태스크 B (VD가 더 이름)

... B가 한 번 더 실행됩니다.

Step 6: B 세 번째 slice 완료 후 (t=12ms)

B.vruntime = 1,968,600 + 984,300 = 2,952,900
B.VD = 2,952,900 + 984,300 = 3,937,200

avg_vruntime ≈ 2,742,899

eligible:
  A: 3,000,000 > 2,742,899 → INELIGIBLE ✗
  B: 2,952,900 > 2,742,899 → INELIGIBLE ✗ (드디어 ineligible!)
  C: 0 <= 2,742,899 → eligible ✓ (유일한 eligible)

→ 선택: 태스크 C (유일한 eligible 태스크)

  C가 드디어 실행됩니다!

누적 실행 (실제): A=3ms, B=9ms, C=0ms → 비율 25:75:0
이상적 비율: 22.8:69.7:7.5
→ C가 0ms 받은 상태 → lag가 크게 양수 → eligible 우선 처리
수치 시뮬레이션: 실행 순서 타임라인 0ms 3ms 6ms 9ms 12ms 15ms 18ms B (nice=-5) A (nice=0) B B C (nice=5) B 누적 실행 비율 (18ms 시점) B(w=3121): 12ms (66.7%) — 이상적: 69.7% A(w=1024): 3ms (16.7%) — 이상적: 22.8% C(w=335): 3ms (16.7%) — 이상적: 7.5% 시간이 지나면 실제 비율이 이상적 비율(weight 비례)로 수렴합니다. EEVDF 보장: 모든 시점에서 |lag| ≤ slice, 즉 불공정 한도가 제한됩니다. B가 많이 실행되는 이유: weight(3121)가 가장 크므로 VD 간격이 짧아 자주 선택됨
weight가 큰 태스크 B가 자주 선택되고, 시간이 지나면 실행 비율이 weight 비율에 수렴합니다

Interactive 태스크 시뮬레이션

시나리오: A(CPU-bound, nice=0), I(Interactive, nice=0, 주기적 1ms burst)
  둘 다 weight=1024

상태: A가 10ms 동안 실행 중, I는 sleep 중

t=10ms: I가 wakeup! (이전에 sleep 상태로 dequeue됨)

  1. dequeue 시 저장된 I.vlag = entity_lag(avg, I)
     I가 10ms 동안 sleep → lag가 크게 양수 (덜 받음)
     I.vlag ≈ 5,000,000 (약 5ms 분량의 lag)

  2. place_entity(cfs_rq, I, ENQUEUE_WAKEUP):
     I.vruntime = avg_vruntime - I.vlag
               = 10,000,000 - 5,000,000
               = 5,000,000

     I.VD = 5,000,000 + 3,000,000 = 8,000,000

  3. check_preempt_wakeup_fair():
     entity_eligible(I): I.vruntime(5M) <= avg(10M) → eligible ✓
     I.VD(8,000,000) < A.VD(12,000,000) → deadline 비교 통과!
     → resched_curr() → I가 A를 즉시 선점!

  4. I가 1ms burst 실행 후 자발적 sleep:
     I.vruntime += 1,000,000 × 1024/1024 = 1,000,000
     I.vruntime = 6,000,000
     dequeue 시 I.vlag = avg(≈10.5M) - 6M = 4,500,000
     → 다음 wakeup에서도 lag 보존!

핵심: I는 별도의 대화형 휴리스틱 없이
      lag 보존만으로 자연스럽게 빠른 응답을 얻습니다.
수치 시뮬레이션의 교훈: EEVDF는 단 세 가지 규칙(eligible 판정, earliest deadline 선택, lag 보존)만으로 복잡한 워크로드 시나리오를 수학적으로 정확하게 처리합니다. CFS에서 필요했던 sleeper credit, buddy 힌트, wakeup_gran 등의 ad-hoc 휴리스틱이 모두 불필요해진 이유입니다.

향후 개발 방향

영역현재 상태 (v6.12)예상 개선
latency_nice API기본 동작, sched_setattr 필요cgroup 수준 latency_nice, systemd 통합
pick_eevdf 최적화early termination더 나은 프루닝, 캐싱
NUMA 연동vlag 보존NUMA 토폴로지(Topology) 인식 deadline
Energy AwarePELT 기반 EASEEVDF deadline 인식 EAS
sched_ext 연동독립적EEVDF 위에 BPF 확장 가능성
per-task slicesched_base_slice 기반워크로드 특성 자동 감지
장기 비전: EEVDF는 CFS의 ad-hoc 휴리스틱을 수학적 기반으로 대체하는 첫 단계입니다. 장기적으로 latency_nice의 cgroup 통합, 워크로드 자동 분류, EAS 연동이 진행되면 Linux 스케줄러의 예측 가능성과 튜닝 가능성이 크게 향상될 것입니다.

참고 자료

EEVDF 스케줄러와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.