EEVDF 스케줄러(Scheduler)

Earliest Eligible Virtual Deadline First(EEVDF) 스케줄러는 Linux 6.6에서 CFS의 핵심 pick 로직을 대체한 공정 스케줄링 알고리즘입니다. 1995년 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의 기본 개념을 먼저 이해해야 합니다.

왜 EEVDF가 필요했나

CFS(Completely Fair Scheduler)는 2007년 Linux 2.6.23에 도입된 이래 오랫동안 리눅스 기본 스케줄러로 사용되었습니다. CFS의 핵심 아이디어는 단순합니다. 모든 태스크(Task)에 vruntime(가상 실행 시간)을 부여하고, 항상 vruntime이 가장 작은, 즉 지금까지 CPU를 가장 적게 받은 태스크를 다음 실행 대상으로 선택합니다. 이렇게 하면 장기적으로 각 태스크가 자신의 비중에 맞는 CPU 시간을 균등하게 나눠 받습니다.

그런데 실제 현장에서는 CFS에 세 가지 고질적인 문제가 드러났습니다.

  • 지연 보장 부재: CFS는 "오래 기다린 태스크를 먼저"라는 원칙만 있고, 특정 태스크가 언제까지는 반드시 CPU를 받는다는 보장이 없습니다. 실행 중인 태스크 수(nr_running)가 늘어날수록 각 태스크의 타임슬라이스(timeslice)가 짧아지고, 원하는 시간 안에 응답하지 못할 수 있습니다.
  • 선점 판단의 불확실성: CFS는 sched_min_granularity_ns와 next/last buddy 힌트를 조합해 선점 여부를 결정했습니다. 이 휴리스틱(heuristic)은 대화형 워크로드(workload)에서 예측하기 어려운 동작을 일으키거나, 공정성을 미묘하게 왜곡시키는 원인이 되었습니다.
  • 응답성과 공정성의 충돌: 대화형 태스크의 응답성을 높이려면 스케줄러가 자주 선점해야 합니다. 하지만 선점이 잦아질수록 컨텍스트 스위칭(context switching) 비용이 늘고 공정성이 흔들립니다. CFS는 이 두 목표 사이에서 완벽한 균형을 찾지 못했습니다.

EEVDF는 이 세 문제를 하나의 알고리즘으로 해결합니다. 각 태스크에 "언제까지 서비스를 완료해야 하는가"를 뜻하는 virtual deadline(VD)을 부여하고, "지금 서비스를 받을 자격이 있는가"를 뜻하는 eligible(자격) 조건을 도입합니다. 스케줄러는 eligible 조건을 충족하는 태스크 중에서 deadline이 가장 이른 것을 선택합니다. 이 단순한 규칙 하나가 지연 보장과 공정성을 동시에 달성합니다.

일상 비유: CFS가 "지금까지 가장 적게 서비스받은 고객"을 다음으로 선택하는 방식이라면, EEVDF는 "마감 기한(Deadline)이 가장 임박한 고객 중에서 자격이 있는 사람"을 선택하는 방식입니다. 병원 응급실에 비유하자면, 접수 순서(vruntime)만 보는 것이 아니라 환자 상태(eligible)와 처치 기한(deadline)을 함께 고려해 가장 시급한 환자를 먼저 진료하는 것과 같습니다.

핵심 요약

  • EEVDF = Eligible + Earliest Virtual Deadline First — 자격 조건(eligible)을 통과한 태스크(Task) 중 virtual deadline이 가장 이른 태스크를 선택합니다.
  • Lag 기반 공정성(Fairness) — 각 태스크의 lag(이상적 서비스 - 실제 서비스)를 추적하여 양의 lag를 가진 태스크(덜 받은 태스크)가 eligible 자격을 얻습니다.
  • Slice 기반 선점 — CFS의 sched_min_granularity 대신 요청 기반 slice를 사용하여 태스크가 자신의 실행 시간을 예측 가능하게 합니다.
  • Augmented RB-Tree — deadline 기준으로 정렬한 RB-Tree에 min_vruntime·min_slice·max_slice 보조 키를 유지하여 O(log N)에 최적 후보를 찾습니다.
  • Latency Nice 확장(제안) — nice 값과 독립적인 latency_nice(-20~19)로 응답 지연(Latency)을 세밀하게 제어하는 제안이 있습니다. 현재 메인라인에는 병합되지 않았으며, slice 조정(debugfs base_slice_ns, 태스크별 sched_runtime)과 cgroup min_slice로 일부 대체합니다.
  • CFS 호환 유지 — vruntime, PELT, cgroup 대역폭(Bandwidth), sched_domain 로드 밸런싱 등 CFS 인프라를 그대로 활용합니다.
  • 커널 6.6 도입 — Peter Zijlstra가 2023년 메인라인에 머지, 이후 6.7~v7.x에서 지속 개선 중입니다. 6.19에서는 NEXT_BUDDY가 재도입되었으나 성능 회귀로 인해 곧 디스애크되어 현재 기본 비활성화 상태입니다.

단계별 이해

  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_vruntime·min_slice 전파 메커니즘을 파악합니다.
  5. 실전 튜닝과 관측
    base_slice_ns(debugfs), sched_runtime 설정과 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, 1995) — EEVDF 원논문. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

EEVDF 논문과 역사

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

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

연도이벤트의미
1993WFQ(Weighted Fair Queueing) 제안GPS(Generalized Processor Sharing)의 패킷 근사
1995Stoica/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 — 안정화v6.6 대비 fair.c의 함수 구성은 그대로이며, slice 튜닝과 버그 수정이 중심이 된 시기입니다.
2024-03~09v6.8~v6.11 — lag·place_entity 보정wakeup 시 lag 보존과 cgroup 계층 정합성 수정이 이어졌습니다. 이 시기 소스는 avg_vruntime_add() 기반 누적과 단일 min_vruntime 키를 유지합니다.
2024-11v6.12 — 지연 디큐(DELAY_DEQUEUE)sched_delayed 기반 지연 디큐, PLACE_REL_DEADLINE, min_slice와 RB 증강 콜백(Callback)이 도입되었습니다.
2025 상반기v6.13~v6.15 — 지연 선점 경로resched_curr_lazy() 계열의 지연 선점 경로와 선점 판정 세분화가 들어왔습니다(v6.12에는 없고 v6.16에는 존재).
2025-07v6.16 — update_min_vruntime() 도입zero_vruntime 기반 min_vruntime 갱신이 함수로 분리되었고, DL server 헬퍼와 마이그레이션 목적지 적격성 판정(task_is_ineligible_on_dst_cpu())이 추가되었습니다.
2025-09v6.17 — Proxy Execution · run-to-parity 정비단일 run-queue 프록시 실행 기반이 머지되고 SMP 전용으로 컴파일됩니다. vlag 임시 저장(HACK)이 전용 vprot 필드로 대체되고, run-to-parity 보호 구간이 enqueue된 엔티티의 최소 slice로 제한되었습니다.
2025-11v6.18 — sum_w_vruntime 3키 증강avg_vruntime_add()가 제거되고 sum_w_vruntime·sum_weight·zero_vruntime 기반의 min_vruntime·min_slice·max_slice 증강이 들어왔습니다. 가중치가 바뀔 때 vprot도 함께 재스케일링(rescale_entity())합니다.
2026-02v6.19 — NEXT_BUDDY 재도입 후 비활성화wakeup 캐시 친화성 개선 시도가 MySQL/SPECjbb/DayTrader 회귀로 인해 6.19-rc7에서 기본 비활성화되었습니다.
2026-10v7.3.1 — 현재 안정 버전가중 평균에 계층 부하(h_load.weight) 사용, protect_slice/vprot와 PICK_BUDDY 정교화
2026-11v7.4-rc1 — 메인라인7.4 개발 주기 시작(2026-11-01)

논문 이론 vs 커널 구현 차이

항목원논문 (1995)Linux 구현 (v7.3 기준)
대상 자원범용 (CPU, 네트워크 등)CPU 시간 전용
eligible 판정V(t) ≥ eligible_timeentity_eligible() → vruntime_eligible() — 가중 평균 V(t)와 vruntime 비교
virtual time V(t)연속 GPS 시뮬레이션cfs_rq->zero_vruntime 기준 가중 평균 (이산 근사)
weight 표현실수 비율 (0~1)nice → sched_prio_to_weight[] 정수 테이블
request size명시적 요청sched_base_slice 기반 자동 계산, 태스크별 sched_runtime으로 재정의 가능
deadline 계산VD = VE + r/wVD = VE + calc_delta_fair(slice, se)
자료구조명시하지 않음Augmented RB-Tree (min_vruntime·min_slice·max_slice 보조 키)
그룹 스케줄링미다룸task_group → cfs_rq 계층적 적용
선점 정책즉시 선점 가정tick/wakeup 기반 선점 판단
왜 28년 만에 도입되었나: 1995년 논문 발표에서 2023년 v6.6 머지까지 28년이 걸렸습니다. 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보다 더 나은 지연 보장을 제공합니다. 핵심 변경 사항은 다음과 같습니다.

  1. pick_next_entity()가 leftmost(min vruntime) 대신 eligible 태스크 중 earliest deadline을 선택합니다.
  2. sched_entity에 deadline, slice 필드가 추가되었습니다.
  3. RB-Tree에 deadline 정렬과 min_vruntime·min_slice·max_slice augmentation이 추가되었습니다.
  4. 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 heuristiceligible + deadline 비교, vprot로 slice 보호
실행 시간 단위sched_latency / nr_runningslice (sched_base_slice, 기본 0.7ms)
지연 보장보장 없음 (best-effort)lag 상한 -r_max < lag < max(r_max, q)
대화형 최적화sleeper credit (wakeup preemption)wakeup 시 lag 보존 → 자연스러운 우선 처리
튜닝 파라미터sched_latency_ns, sched_min_granularity_nsbase_slice_ns(debugfs), sched_runtime, latency_nice(제안)
RB-Tree 키vruntime 단일 키deadline 정렬 + min_vruntime·min_slice·max_slice (augmented)
next/last buddy캐시(Cache) 친화도(Affinity) 힌트PICK_BUDDY(기본 on)로 제한적 유지, NEXT_BUDDY는 기본 off
skip buddyyield_to() 구현유지 (yield 시 deadline 무시)
CFS (v2.6.23~v6.5) RB-Tree (vruntime 정렬) leftmost = min vruntime buddy 힌트 + wakeup granularity 확인 선택: buddy(근접 시) 또는 leftmost 지연 보장 없음 (best-effort) buddy가 공정성을 왜곡할 수 있음 EEVDF (v6.6+) Augmented RB-Tree (deadline 정렬) 1. Eligible 필터링 (lag >= 0) 2. vprot: 현재 slice 보호 중이면 유지 3. 선택: earliest deadline O(log N) 선택, lag/deadline 기반 공정성 vprot로 실행 안정성 확보
CFS는 min vruntime(leftmost)을 기준으로 하되 buddy 힌트로 일부 보정하는 반면, EEVDF는 eligible 필터링과 vprot 보호를 거쳐 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) 반환 */
}

/* kernel/sched/fair.c — v7.3.1 pick_eevdf(), 핵심 경로만 발췌 */
static struct sched_entity *pick_eevdf(struct cfs_rq *cfs_rq, bool protect)
{
    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;

    /* 엔티티가 하나뿐이면 eligible 검사 생략 */
    if (cfs_rq->h_nr_queued == 1)
        return curr && curr->on_rq ? curr : se;

    /* PICK_BUDDY: yield_to_task()/cgroup 등이 지정한 next 후보 */
    if (sched_feat(PICK_BUDDY) && protect &&
        cfs_rq->next && entity_eligible(cfs_rq, cfs_rq->next))
        return cfs_rq->next;

    if (curr && (!curr->on_rq || !entity_eligible(cfs_rq, curr)))
        curr = NULL;

    /* vprot: 현재 태스크가 보호 구간 안이면 계속 실행 */
    if (curr && protect && protect_slice(curr))
        return curr;

    /* leftmost가 eligible이면 즉시 선택 (fast path) */
    if (se && entity_eligible(cfs_rq, se)) {
        best = se;
        goto found;
    }

    /* min_vruntime augmentation으로 eligible 서브트리만 탐색 */
    while (node) {
        struct rb_node *left = node->rb_left;

        if (left && vruntime_eligible(cfs_rq,
                    __node_2_se(left)->min_vruntime)) {
            node = left;
            continue;
        }

        se = __node_2_se(node);

        /* 왼쪽 서브트리에 eligible이 없으면 현재 노드 검사 */
        if (entity_eligible(cfs_rq, se)) {
            best = se;   /* deadline 순서상 가장 이른 eligible */
            break;
        }

        node = node->rb_right;
    }
found:
    if (!best || (curr && entity_before(curr, best)))
        best = curr;

    return best;
}
buddy 힌트의 축소: EEVDF는 CFS의 last buddy를 제거했지만, next buddy(cfs_rq->next)는 남아 있습니다. NEXT_BUDDY는 기본 false라 wakeup 시 buddy 지명은 하지 않지만, PICK_BUDDY가 기본 true이므로 yield_to_task(), cgroup dequeue/pick 등이 설정한 next는 eligible일 때 우선 선택됩니다. 즉 buddy는 "공정성 판단을 대체"하지 않고, eligible 범위 안에서 캐시 지역성만 보정합니다.

가상 시간 체계

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

가상 시간이란 무엇인가

스케줄러가 "공정하게" CPU를 나누려면 서로 다른 가중치(priority)를 가진 태스크들의 진행 속도를 하나의 공통 척도로 비교할 방법이 필요합니다. 실제 물리적 시간(wall-clock time)을 쓰면, 가중치가 높은 태스크는 CPU를 더 많이 받으므로 단순 비교가 불가능합니다.

가상 시간(virtual time)은 이 문제를 해결하는 정규화된 시계입니다. 각 태스크는 CPU를 실제로 1ms 사용해도, 자신의 가중치에 반비례해 vruntime이 증가합니다. 가중치가 2배인 태스크는 같은 CPU 시간을 써도 vruntime이 절반만 늘어납니다. 이 덕분에 스케줄러는 vruntime을 비교하는 것만으로 "누가 자신의 공정한 몫보다 적게 받았는가"를 즉시 판단할 수 있습니다.

핵심 관계: lag > 0 ↔ eligible ↔ se->vruntime ≤ V(t) — 이 세 표현은 모두 동치입니다. 커널은 부동소수점 연산을 피하기 위해 vruntime_eligible(cfs_rq, se->vruntime) 정수 비교를 사용합니다.

수식 정의

개념수식 (논문)커널 표현의미
V(t)Σ w_i · s_i(t) / Σ w_izero_vruntime + sum_w_vruntime/sum_weight시스템 가상 시간 — 모든 태스크의 가중 평균 진행
S(t)태스크 i의 실제 서비스 시간se->vruntime태스크의 가상 실행 시간
VEeligible time = V(t_last_end)se->vruntime (가상 시간 좌표)자격 시간 — 이 시점부터 선택 대상
VDVE + request / weightse->deadline가상 마감 시간 — 서비스 완료 기한
lagS_ideal(t) - S_actual(t)se->vlag (entity_lag()가 max_slice + TICK_NSEC 기준으로 클램프)이상적 서비스와 실제 서비스 차이
slice / weight = calc_delta_fair(slice, se) eligible 구간 가상 시간 VE eligible time (자격 시작) V(t) 시스템 가상 시간 VD virtual deadline (서비스 마감) lag = 이상적 서비스 - 실제 서비스 lag > 0: 덜 받음 → eligible lag < 0: 더 받음 → ineligible lag = 0: 정확히 공정 eligible ⇔ vruntime ≤ V(t) EEVDF 선택 규칙 1. eligible한 태스크만 후보 2. 후보 중 VD가 가장 이른 태스크 선택 3. 동률이면 vruntime이 작은 쪽 우선 덜 받은 태스크를 급한 순서로 처리
EEVDF의 세 핵심 값: eligible time(자격 시작), virtual deadline(마감 기한), lag(공정성 척도)
/* kernel/sched/fair.c — v7.3.1 avg_vruntime() */
u64 avg_vruntime(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    long weight = cfs_rq->sum_weight;
    s64 delta = 0;

    if (curr && !curr->on_rq)
        curr = NULL;

    if (weight) {
        s64 runtime = cfs_rq->sum_w_vruntime;

        if (curr) {
            unsigned long w = avg_vruntime_weight(cfs_rq,
                                                curr->h_load.weight);

            runtime += entity_key(cfs_rq, curr) * w;
            weight += w;
        }

        /* 음수 구간에서 내림/올림 방향 보정 */
        if (runtime < 0)
            runtime -= (weight - 1);

        delta = div64_long(runtime, weight);
    }

    update_zero_vruntime(cfs_rq, delta);

    return cfs_rq->zero_vruntime;
}

/* V(t) = Σ((v_i - v0) × w_i) / Σw_i + v0
 *   v0 = cfs_rq->zero_vruntime
 *   Σ((v_i - v0) × w_i) = cfs_rq->sum_w_vruntime
 *   Σw_i               = cfs_rq->sum_weight
 * v7.3.1은 w_i로 계층 부하(h_load.weight)를 사용한다 */
/* entity_key: 가상 시간 좌표를 zero_vruntime 기준 상대값으로 변환 */
static inline s64 entity_key(struct cfs_rq *cfs_rq,
                       struct sched_entity *se)
{
    return vruntime_op(se->vruntime, "-", cfs_rq->zero_vruntime);
}

Eligible 조건 판정

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

왜 Eligible 조건이 필요한가

EEVDF는 "deadline이 가장 이른 태스크를 선택"하는 알고리즘입니다. 그런데 eligible 조건 없이 무조건 deadline만 보면 어떤 문제가 생길까요?

예를 들어, 동일 가중치(weight)의 태스크 A, B, C 세 개가 있다고 합시다. B가 짧은 I/O 대기 후 막 깨어났고, 그동안 A와 C는 계속 CPU를 받았습니다. 깨어난 B는 새 deadline을 받는데, 이 값이 A나 C의 deadline보다 이를 수 있습니다. eligible 조건이 없다면 B는 깨어나자마자 A와 C를 밀어내고 CPU를 독점하게 됩니다. 이는 자다 깨어났다는 이유만으로 높은 우선순위를 얻는 셈이므로 공정하지 않습니다.

eligible 조건은 이 문제를 차단합니다. "지금까지 공정한 몫보다 실제로 적게 받은 태스크만" 선택 후보가 될 수 있습니다. B가 I/O 대기 중에는 CPU를 전혀 못 받았으므로 lag가 크게 양수가 되어 당연히 eligible입니다. 그러나 B가 이미 충분히 CPU를 받은 상황(lag < 0)이라면 deadline이 빨라도 선택되지 않습니다.

한 줄 요약: Eligible = "나는 아직 내 몫을 다 못 받았다" 는 확인 도장입니다. 이 도장이 없으면 deadline 경쟁에 참가할 수 없습니다.

수학적 정의

논문에서 태스크 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 — v7.3.1 vruntime_eligible() */
static int vruntime_eligible(struct cfs_rq *cfs_rq, u64 vruntime)
{
    struct sched_entity *curr = cfs_rq->curr;
    s64 key, avg = cfs_rq->sum_w_vruntime;
    long load = cfs_rq->sum_weight;

    if (curr && curr->on_rq) {
        unsigned long weight = avg_vruntime_weight(cfs_rq,
                                                curr->h_load.weight);

        avg += entity_key(cfs_rq, curr) * weight;
        load += weight;
    }

    key = vruntime_op(vruntime, "-", cfs_rq->zero_vruntime);

    /* avg/load ≥ key ⇔ vruntime ≤ V(t) */
#ifdef CONFIG_64BIT
#ifdef CONFIG_ARCH_SUPPORTS_INT128
    return avg >= (__int128)key * load;
#else
    s64 rhs;

    /* 곱셈 오버플로 시 key 부호가 정답을 결정한다 */
    if (check_mul_overflow(key, load, &rhs))
        return key <= 0;

    return avg >= rhs;
#endif
#else /* 32bit */
    return avg >= key * load;
#endif
}

int entity_eligible(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    return vruntime_eligible(cfs_rq, se->vruntime);
}
코드 설명
  • avg, load 초기화cfs_rq->sum_w_vruntime은 Σ((v_i - v0) × w_i), sum_weight는 Σ(w_i)를 저장합니다. 이 값들은 enqueue/dequeue 시 sum_w_vruntime_add()/sum_w_vruntime_sub()에서 증분 갱신됩니다.
  • curr 보정curr는 RB-Tree에 없지만 실행 중이므로 avg 계산에 수동으로 포함시킵니다. 가중치는 curr->h_load.weight(계층 부하)를 사용합니다.
  • eligible 판정avg >= key * load는 수학적으로 vruntime <= V(t)와 동치입니다. 64비트에서는 __int128 곱셈으로 오버플로를 피하고, 미지원 아키텍처에서는 check_mul_overflow() 후 key 부호로 판정합니다.
정수 나눗셈 회피: 순진한 구현은 vruntime <= avg / load로 나눗셈이 필요합니다. 커널은 양변에 load를 곱하여 avg >= key * load로 변환함으로써 비싼 나눗셈을 제거합니다. 64비트에서 곱셈이 s64를 넘을 수 있으므로 __int128 또는 check_mul_overflow()를 사용합니다.
entity_eligible(): 나눗셈 없는 자격 판정 entity_eligible(cfs_rq, se) avg = sum_w_vruntime, load = sum_weight curr가 on_rq인가? Yes avg += entity_key(curr) × weight load += weight(curr) No avg >= key × load ? Yes ELIGIBLE No INELIGIBLE avg >= key × load ⇔ vruntime ≤ V(t) ⇔ lag >= 0 64비트 곱셈 오버플로는 __int128 또는 check_mul_overflow()로 처리
entity_eligible()은 나눗셈 없이 정수 곱셈만으로 eligible 여부를 판정합니다

avg_vruntime 유지

/* kernel/sched/fair.c — v7.3.1, 간략화 */
static void sum_w_vruntime_add(struct cfs_rq *cfs_rq,
                                 struct sched_entity *se)
{
    unsigned long weight = avg_vruntime_weight(cfs_rq,
                                                se->h_load.weight);
    s64 key = entity_key(cfs_rq, se);

    cfs_rq->sum_w_vruntime += key * weight;
    cfs_rq->sum_weight += weight;
}

static void sum_w_vruntime_sub(struct cfs_rq *cfs_rq,
                                 struct sched_entity *se)
{
    unsigned long weight = avg_vruntime_weight(cfs_rq,
                                                se->h_load.weight);
    s64 key = entity_key(cfs_rq, se);

    cfs_rq->sum_w_vruntime -= key * weight;
    cfs_rq->sum_weight -= weight;
}

/* PARANOID_AVG=true이면 sum_shift를 올려 재계산하는
 * sum_w_vruntime_add_paranoid() 경로를 사용한다 (기본 false) */

Virtual Deadline 계산

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

계산 공식

VD = VE + calc_delta_fair(slice, se)
   = VE + slice × NICE_0_LOAD / weight

여기서:
  VD     = virtual deadline
  VE     = eligible time (= 현재 vruntime)
  slice  = 요청 실행 시간 (sysctl_sched_base_slice, 기본 0.7ms)
  weight = nice → sched_prio_to_weight[] 변환 값

예시 (sched_base_slice = 700,000 ns):
  nice 0  (weight 1024): VD = VE + 700,000 × 1024 / 1024 = VE + 700,000
  nice -5 (weight 3121): VD = VE + 700,000 × 1024 / 3121 = VE + 229,669
  nice 5  (weight 335):  VD = VE + 700,000 × 1024 / 335  = VE + 2,139,701

→ weight가 클수록 VD 간격이 짧아져 더 자주/먼저 선택됨
/* kernel/sched/fair.c — v7.3.1 update_deadline() */
static bool update_deadline(struct cfs_rq *cfs_rq,
                            struct sched_entity *se)
{
    if (vruntime_cmp(se->vruntime, "<", se->deadline))
        return false;

    /* 기본 slice는 sysctl, sched_runtime으로 지정한 값은 유지 */
    if (!se->custom_slice)
        se->slice = sysctl_sched_base_slice;

    /* EEVDF: vd_i = ve_i + r_i / w_i */
    se->deadline = se->vruntime + calc_delta_fair(se->slice, se);
    avg_vruntime(cfs_rq);

    /* 요청한 slice를 다 썼으므로 재스케줄링 필요 */
    return true;
}

/* se->slice: 실제 실행 시간 단위 (nanoseconds)
 * 기본값 = sysctl_sched_base_slice (v7.3.1: 0.7ms)
 * sched_setattr()의 sched_runtime으로 태스크별 재정의 가능
 *   → se->custom_slice = 1, 0.1ms ~ 100ms로 clamp */
코드 설명
  • 조건 검사vruntime_cmp(se->vruntime, "<", se->deadline)은 현재 vruntime이 아직 deadline에 도달하지 않았으면 false를 반환합니다. 부호 있는 래핑 안전 비교(vruntime_op 계열)를 사용합니다.
  • deadline 계산calc_delta_fair(slice, se)는 slice * NICE_0_LOAD / se->load.weight를 계산합니다. weight가 클수록 가상 시간 간격이 짧아져 deadline이 가까워지고, 더 자주 선택됩니다.
  • slice 기본값se->slice는 실제 실행 시간(ns) 단위이며, 기본값 0.7ms는 sysctl_sched_base_slice로 설정됩니다. custom_slice가 설정된 태스크는 sched_runtime 값을 유지합니다.
  • 반환값deadline을 갱신했으면 true를 반환하고, 호출자(update_curr())가 resched_curr_lazy()로 재스케줄링을 요청합니다.
deadline 갱신 시점: deadline은 현재 vruntime이 이전 deadline에 도달했을 때만 갱신됩니다. 이는 태스크가 slice 시간을 다 쓰기 전에는 동일 deadline을 유지하도록 보장합니다. CFS의 sched_latency 기반 동적 타임슬라이스(Time Slice)와 달리, EEVDF의 slice는 태스크별 고정값(기본 0.7ms)입니다.
nice 값weightslice(0.7ms) 기준 가상 시간 간격 (ns)상대적 CPU 비율
-2088761VE + 8,075~86.7x (nice 0 대비)
-109548VE + 75,073~9.3x
-53121VE + 229,669~3.1x
01024VE + 700,0001.0x (기준)
5335VE + 2,139,701~0.33x
10110VE + 6,516,363~0.11x
1915VE + 47,786,666~0.015x
nice 값별 Virtual Deadline 간격 VD = VE + slice × NICE_0_LOAD / weight (slice = 0.7ms) ← 간격 짧음 (자주 선택) 간격 김 (드물게 선택) → nice=-20 w=88761 8,075 ns nice=-10 w=9548 75,073 ns nice=-5 w=3121 229,669 ns nice=0 (기준) w=1024 700,000 ns nice=5 w=335 2,139,701 ns nice=10 w=110 6,516,363 ns nice=19 w=15 47,786,666 ns 0.01x 0.1x 1x 10x 100x VD 간격 (nice 0 대비 배율, 로그 축) weight가 클수록 VD 간격이 짧아져 deadline 경쟁에서 더 자주 선택됩니다
weight가 클수록(nice 작을수록) VD 간격이 짧아져 스케줄러가 더 자주 선택합니다 (가로 위치는 nice 0 대비 배율의 로그 축, 수치는 실제 가상 시간(ns))
직관적 이해: nice=-20 태스크의 deadline 간격(8,075ns)은 nice=19 태스크(47,786,666ns)의 약 1/5917입니다. 이 간격 비율은 두 태스크의 weight 비율(88761:15 ≈ 5917:1)과 일치합니다 — deadline 간격이 weight에 반비례하기 때문입니다. 간격이 짧을수록 deadline 경쟁에서 더 자주 이겨, 결국 weight 비율만큼(≈5917배)의 CPU 시간을 얻습니다. 즉 "더 자주 스케줄링"과 "더 많은 CPU 비율"은 같은 원인(weight)의 두 결과입니다.

Lag 추적 메커니즘

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

A/B 두 프로세스로 이해하는 Lag

추상적인 수식보다 구체적인 예시로 먼저 lag의 직관을 잡겠습니다. 동일한 가중치(weight = 1024, nice = 0)를 가진 프로세스 A와 B 두 개가 실행 중인 상황을 가정합니다. 물리 CPU가 하나뿐이므로 이상적인 공정 분배라면 A와 B 각각이 50%씩 CPU를 받아야 합니다. 계산을 단순하게 보이기 위해 한 번의 slice를 3ms로 가정합니다(실제 기본값은 0.7ms이며, 비율 관계는 동일합니다).

시간실제 실행A의 lagB의 lageligible 태스크설명
t = 0msA 실행 시작00A, B 모두초기 상태, 둘 다 eligible
t = 3msA가 3ms 실행-1.5ms+1.5msB만A는 이상(1.5ms)보다 1.5ms 더 받았고, B는 1.5ms 덜 받음
t = 6msB가 3ms 실행00A, B 모두공정하게 균형 회복
t = 9msA가 3ms 실행-1.5ms+1.5msB만다시 A 과잉, B 부족

이 예시에서 lag가 하는 역할을 명확히 볼 수 있습니다.

A/B Lag 타임라인 (동일 가중치, slice = 3ms) 0 3ms 6ms 9ms 12ms CPU A 실행 B 실행 A 실행 B 실행 +1.5ms lag 0 -1.5ms 구간 종료 시점의 eligible 태스크 t=3: B만 t=6: 둘 다 t=9: B만 t=12: 둘 다 A lag B lag 실행 중 lag 감소, 대기 중 lag 증가 lag의 부호가 바뀌면 eligible 상태도 바뀝니다 — 양수면 eligible, 음수면 ineligible
동일 가중치 A, B 두 프로세스의 lag 변화: 실행 중인 태스크는 lag가 감소(음수 방향), 대기 중인 태스크는 lag가 증가(양수 방향)하며 eligible 상태가 교대로 전환됩니다

이제 가중치가 다른 경우도 생각해 봅시다. A의 가중치가 B의 2배라면, A는 CPU의 2/3을, B는 1/3을 받아야 공정합니다. 1ms 동안 A가 실행되면 A의 vruntime은 상대적으로 더 느리게 증가하고, B의 lag는 1/3 × 1ms = 0.33ms만 증가합니다. 가중치가 반영된 비율로 lag가 쌓이므로, 가중치가 다르더라도 동일한 eligible 판정 공식이 공정하게 작동합니다.

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 변화와 eligible 상태 (3ms slice 가정) lag = 이상적 서비스 − 실제 서비스, |lag| ≤ r_max eligible ineligible r_max 0 -r_max 0 3ms 6ms 9ms 12ms A lag B lag lag > 0: 덜 받음 → eligible · lag < 0: 더 받음 → ineligible lag의 부호가 eligible 상태를 결정하며, 크기는 r_max로 제한됩니다
lag의 부호가 eligible 상태를 결정: 양(+)이면 eligible, 음(-)이면 ineligible
/* kernel/sched/fair.c — entity_lag() (v7.3.1) */
static s64 entity_lag(struct cfs_rq *cfs_rq,
                        struct sched_entity *se, u64 avruntime)
{
    u64 max_slice = cfs_rq_max_slice(cfs_rq) + TICK_NSEC;
    s64 vlag, limit;

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

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

pick_next_entity() 구현

EEVDF의 핵심 함수인 pick_eevdf(cfs_rq, protect)는 eligible한 태스크 중 가장 이른 virtual deadline을 가진 태스크를 O(log N)에 찾습니다. RB-Tree는 deadline 순서로 정렬되어 있고, 각 노드는 서브트리의 min_vruntime을 유지하므로 eligible 후보가 없는 서브트리를 통째로 가지치기(pruning)할 수 있습니다.

pick_eevdf(cfs_rq, protect) 동작 흐름 (v7.3.1) pick_eevdf(cfs_rq, protect) fast path (트리 순회 생략, 즉시 return) h_nr_queued == 1 → curr (없거나 !on_rq면 se) PICK_BUDDY && protect && next eligible → next curr eligible && protect && protect_slice(curr) → curr while (node) 왼쪽 서브트리의 min_vruntime이 eligible? Yes node = node->rb_left continue (더 이른 VD) No 현재 노드가 eligible? Yes best = se break (earliest VD) No node = node->rb_right continue node == NULL found: if (!best || (curr && entity_before(curr, best))) best = curr return best
fast path를 지나면 deadline 순으로 정렬된 RB-Tree를 내려갑니다. 왼쪽 서브트리의 min_vruntime이 eligible이면 왼쪽으로, 아니면 현재 노드를 검사합니다. 현재 노드가 eligible이면 deadline 순서상 첫 후보이므로 break하고, 아니면 오른쪽 서브트리로 이동합니다. eligible을 찾지 못한 채 node가 NULL이 되면 curr과 비교해 최종 후보를 정합니다. (v7.3.1 pick_eevdf() 기준)
/* kernel/sched/fair.c — v7.3.1 pick_eevdf()의 서브트리 pruning */
static struct sched_entity *pick_eevdf(struct cfs_rq *cfs_rq, bool protect)
{
    /* ... h_nr_queued == 1, PICK_BUDDY, curr/protect_slice fast path 생략 ... */

    /* 트리는 deadline 순서이므로, in-order에서 처음 만난
     * eligible 엔티티가 곧 earliest deadline이다 */
    while (node) {
        struct rb_node *left = node->rb_left;

        /* 왼쪽 서브트리의 최소 vruntime이 eligible이면
         * 그 안에 eligible 엔티티가 반드시 존재한다 */
        if (left && vruntime_eligible(cfs_rq,
                    __node_2_se(left)->min_vruntime)) {
            node = left;
            continue;
        }

        se = __node_2_se(node);

        /* 왼쪽에 eligible이 없으면 현재 노드 검사 */
        if (entity_eligible(cfs_rq, se)) {
            best = se;
            break;
        }

        node = node->rb_right;
    }

    /* eligible을 찾지 못하면 curr과 비교해 결정 */
    if (!best || (curr && entity_before(curr, best)))
        best = curr;

    return best;
}
코드 설명
  • min_vruntime pruning트리는 deadline 순서로 정렬되지만 eligible 여부는 vruntime으로 판정합니다. 서브트리의 최소 vruntime을 보조 키로 유지하면, 왼쪽 서브트리의 min_vruntime이 eligible인 순간 그 안에 eligible 엔티티가 최소 하나 존재함이 보장됩니다. 따라서 왼쪽으로 내려가면 됩니다.
  • 현재 노드 검사왼쪽 서브트리에 eligible이 없으면 현재 노드를 검사합니다. eligible이면 deadline 순서상 처음 만난 후보이므로 곧 earliest deadline이고, 더 볼 필요 없이 break합니다.
  • 오른쪽 이동현재 노드도 eligible이 아니면 오른쪽 서브트리로 이동합니다. 이 pruning 덕분에 모든 노드를 보지 않고 deadline 순서를 따라 내려갑니다.
  • 폴백eligible 후보를 찾지 못하면 curr과 entity_before()로 비교합니다. 이 경로는 lag가 음수인 태스크만 남았을 때(과잉 서비스 상태) 발생합니다.
/* kernel/sched/fair.c — v7.3.1 */
static struct sched_entity *
pick_next_entity(struct rq *rq, bool protect)
{
    struct cfs_rq *cfs_rq = &rq->cfs;
    struct sched_entity *se;

    se = pick_eevdf(cfs_rq, protect);
    if (se->sched_delayed) {
        /* DELAY_DEQUEUE로 지연된 태스크는 지금 꺼낸다 */
        __dequeue_task(rq, task_of(se),
                       DEQUEUE_SLEEP | DEQUEUE_DELAYED);
        return NULL;
    }
    return se;
}
탐색 비용: RB-Tree 높이는 O(log N)이므로 pruning 루프의 최악 비용도 O(log N)입니다. 다만 leftmost가 eligible이면 fast path로 즉시 끝나고, h_nr_queued == 1이면 루프 자체를 생략합니다. CFS의 leftmost 선택이 O(1)인 것과 달리 최악의 경우 로그 시간이 걸리지만, 그 대가로 lag 기반 지연 보장을 얻습니다.

슬라이스 메커니즘

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

CFS의 문제: 태스크가 늘수록 길어지는 지연

CFS에서는 타임슬라이스가 sched_latency_ns / nr_running으로 계산됩니다. 기본 sched_latency_ns가 6ms이고 실행 가능한 태스크가 6개라면 각 태스크의 타임슬라이스는 1ms입니다. 여기까지는 좋습니다. 그런데 태스크가 60개라면 어떻게 될까요?

CFS 타임슬라이스 = sched_latency_ns / nr_running

nr_running =  6: 타임슬라이스 = 6ms / 6  = 1.0ms
nr_running = 20: 타임슬라이스 = 6ms / 20 = 0.3ms  (sched_min_granularity_ns 하한 발동)
nr_running = 60: 타임슬라이스 = 6ms / 60 = 0.1ms  → 하한 클램핑 → 실제 지연 증가

결과: 태스크 60개 × 하한 0.75ms = 45ms 주기 → 1회 서비스 대기 최대 44ms

태스크 수가 늘면 한 바퀴 돌아오는 데 걸리는 시간(스케줄링 지연)이 그에 비례해 늘어납니다. 이는 대화형 태스크나 실시간성이 필요한 워크로드에서 응답 지연이 예측 불가하게 커지는 원인이 됩니다.

EEVDF의 해법: 고정 slice + deadline 기반 선점

EEVDF에서 slice는 태스크 수와 무관하게 고정됩니다 (기본값 sysctl_sched_base_slice = 700,000ns = 0.7ms). 선점 기준도 "타임슬라이스 소진"이 아니라 "현재 태스크의 deadline을 더 이른 deadline을 가진 eligible 태스크가 추월"하는 시점입니다.

EEVDF 선점 조건 (wakeup):
  새 태스크의 VD  <  현재 실행 중 태스크의 VD  (deadline 역전)
  AND 새 태스크가 eligible

태스크 수에 따른 변화:
  nr_running =  6: 각 태스크 slice = 0.7ms (고정)
  nr_running = 60: 각 태스크 slice = 0.7ms (고정) ← CFS와 다름

결과: 태스크 수가 늘어도 slice 크기 불변.
      지연은 lag 상한(-r_max < lag < max(r_max, q))과
      deadline 간격으로 제한되며, 태스크 수에 선형 비례하지 않음
실용적 차이: 서버에 60개의 동시 요청 처리 스레드(thread)가 있다고 가정합니다. CFS에서는 각 스레드가 CPU를 받기까지 최대 44ms 이상 기다릴 수 있습니다. EEVDF에서는 스레드마다 독립적인 deadline이 있고, eligible한 스레드 중 가장 시급한 것이 즉시 선택됩니다. 응답 지연이 태스크 수에 선형으로 비례하지 않습니다.
파라미터CFSEEVDF
실행 시간 단위sched_latency / nr_runningsched_base_slice (태스크별 고정, 기본 0.7ms)
최소 단위sched_min_granularity_ns해당 없음 (slice 자체가 최소)
선점 기준delta_exec > ideal_runtimevruntime ≥ deadline, 또는 wakeup 시 vprot 초과
nr_running 영향타임슬라이스 = latency / nr_runningslice 크기 불변, deadline 간격만 변화
latency 보장없음 (n이 커지면 지연 증가)lag 상한(-r_max < lag < max(r_max, q))
/* kernel/sched/fair.c — v7.3.1 */
unsigned int sysctl_sched_base_slice = 700000ULL;
static unsigned int normalized_sysctl_sched_base_slice = 700000ULL;

/* __setscheduler_params(): 태스크별 slice 결정 */
if (attr->sched_runtime) {
    se->custom_slice = 1;
    se->slice = clamp_t(u64, attr->sched_runtime,
                        100 * NSEC_PER_USEC,      /* 100us */
                        100 * NSEC_PER_MSEC);     /* 100ms */
} else {
    se->custom_slice = 0;
    se->slice = sysctl_sched_base_slice;
}

/* update_curr(): 실행 시간 회계 + deadline 갱신 + 선점 요청 */
static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->h_curr;
    struct rq *rq = rq_of(cfs_rq);
    s64 delta_exec;
    bool resched;

    if (unlikely(!curr))
        return;

    delta_exec = update_se(rq, curr);   /* exec_start 갱신 */
    if (unlikely(delta_exec <= 0))
        return;

    account_cfs_rq_runtime(cfs_rq, delta_exec);

    if (!entity_is_task(curr))
        return;

    cfs_rq = &rq->cfs;

    curr->vruntime += calc_delta_fair(delta_exec, curr);
    resched = update_deadline(cfs_rq, curr);

    dl_server_update(&rq->fair_server, delta_exec);

    if (cfs_rq->h_nr_queued == 1)
        return;

    /* deadline을 넘었거나 보호 구간을 벗어나면 지연 선점 요청 */
    if (resched || !protect_slice(curr)) {
        resched_curr_lazy(rq);
        clear_buddies(cfs_rq, curr);
    }
}
CFS 동적 타임슬라이스 vs EEVDF 고정 slice CFS: 동적 타임슬라이스 timeslice = sched_latency_ns / nr_running sched_latency_ns = 6ms, sched_min_granularity_ns = 0.75ms N = 4 → 6ms / 4 = 1.5ms 라운드 = 6ms N = 20 → 6ms / 20 = 0.3ms → 0.75ms로 클램핑 라운드 = 15ms • 한 바퀴에 N × timeslice가 소요 • 큰 N에서는 min_granularity 하한으로 클램핑 • 재실행 대기 = 라운드 길이 → N에 비례해 증가 EEVDF: 고정 slice + VD 순 선택 slice = sched_base_slice = 0.7ms (고정) sched_runtime(100us~100ms)으로 태스크별 재정의 가능 entity vruntime VD A 10.2 12.4 B 10.4 13.1 C 10.1 11.8 ← eligible 중 earliest VD D 10.6 14.0 • slice 크기는 N과 무관하게 0.7ms • 실행 순서는 고정 라운드가 아니라 VD가 결정 • lag 상한(-r_max < lag < max(r_max, q))으로 편차 제한 핵심 차이 CFS: 라운드 = N × timeslice → 지연이 N에 비례 (큰 N에서는 min_granularity가 하한) EEVDF: slice 고정 + VD 순 선택 → 지연이 고정 라운드에 묶이지 않고 lag 상한으로 편차 제한 선택 연산은 min_vruntime 증강으로 O(log N) — 이는 지연 보장과 별개의 비용
CFS는 태스크 수가 늘면 타임슬라이스가 줄고 하한에 클램핑되어 라운드가 N에 비례해 길어집니다. EEVDF는 slice를 고정하고 VD 순서와 lag 상한으로 지연을 제한합니다.
slice vs 타임슬라이스: CFS의 타임슬라이스는 sched_latency / nr_running으로 태스크 수에 반비례하여 줄어들었습니다. EEVDF의 slice는 고정이며, 대신 deadline이 자연스럽게 스케줄링 주기를 결정합니다. 태스크 수가 많으면 eligible 후보도 많아지고, deadline 경쟁이 자연스럽게 시간 분배를 조절합니다.

EEVDF 선점 규칙

EEVDF에서 선점은 두 가지 경로로 발생합니다: tick 기반 선점(현재 태스크의 deadline 도달)과 wakeup 기반 선점(새로 깨어난 태스크가 더 이른 deadline을 가짐). v7.3.1 기준으로 tick 경로는 update_curr() → update_deadline()이 담당하고, wakeup 경로는 wakeup_preempt_fair()가 담당합니다. CFS의 check_preempt_tick()과 wakeup_gran 기반 판정은 더 이상 존재하지 않습니다.

선점 판단 규칙

선점 발생 조건 (v7.3.1):

1. Tick 경로 (update_curr → update_deadline):
   vruntime >= deadline
   → deadline을 새로 계산하고 resched_curr_lazy(rq) 요청
   단, cfs_rq->h_nr_queued == 1이면 경쟁자가 없으므로 생략
   RUN_TO_PARITY && protect_slice(curr)이면 선점을 보류
   CONFIG_SCHED_HRTICK이면 slice 경계에 맞춰 tick을 미리 설정

2. Wakeup 경로 (wakeup_preempt_fair):
   - p 또는 donor가 fair 클래스가 아니면 NONE
   - WAKEUP_PREEMPTION이 꺼져 있으면 NONE
   - idle entity ↔ non-idle entity 교체는 즉시 preempt
   - PREEMPT_SHORT && pse->slice < se->slice → PREEMPT_WAKEUP_SHORT
   - WF_FORK wakeup은 wakee 선점에서 제외
   - NEXT_BUDDY && set_preempt_buddy() → PICK (WF_SYNC면 RESCHED)
   - pick_next_entity(rq, protect) 결과가 pse이면 즉시 preempt
   - PREEMPT_SHORT && pse가 eligible이면 protection을 취소하고 preempt

3. CFS에서 제거된 조건:
   - check_preempt_tick()의 ideal_runtime 비교
   - sched_wakeup_granularity(wakeup_gran) 버퍼
   - sched_min_granularity 기반 최소 실행 보장
   (최소 실행 보장은 base_slice + protect_slice()/vprot로 대체)
EEVDF 선점 판단 흐름 (v7.3.1) Tick 경로 — update_curr() scheduler_tick() → entity_tick() update_curr() vruntime += calc_delta_fair(delta_exec, curr) resched = update_deadline(cfs_rq, curr) h_nr_queued == 1? Yes → return resched || !protect_slice(curr)? (update_curr 내부 판단) Yes resched_curr_lazy(rq) clear_buddies(cfs_rq, curr) schedule() → pick_eevdf() 복귀 경계에서 다음 후보 선택 No: protect_slice(curr)면 계속 실행 HRTICK: hrtick 만료(queued)면 resched_curr() 즉시 RUN_TO_PARITY: vprot 도달까지 실행 보장 h_nr_queued == 1이면 선점 요청 생략 Wakeup 경로 — wakeup_preempt_fair() try_to_wake_up() → wakeup_preempt_fair() 선행 조건 검사 fair 클래스 && WAKEUP_PREEMPTION (아니면 NONE) idle ↔ non-idle 교체 → 즉시 preempt PREEMPT_SHORT && pse->slice < se->slice → PREEMPT_WAKEUP_SHORT WF_FORK → wakee 선점 제외 NEXT_BUDDY && WF_SYNC → preempt_sync() pick_next_entity(rq, protect) protect = (preempt_action != SHORT) nse == pse || (SHORT && eligible)? No → update Yes cancel_protect_slice(se) resched_curr_lazy(rq) SHORT: set_short_buddy() 후 선점 No 경로는 update_protect_slice() 후 종료 두 경로 모두 resched_curr_lazy(rq)로 선점을 요청하고, 복귀 경계에서 pick_eevdf()가 다음 후보를 고릅니다.
tick 경로는 deadline 도달 여부로, wakeup 경로는 preempt_action과 pick_next_entity() 결과로 선점을 결정합니다. 두 경로 모두 최종적으로 resched_curr_lazy()를 사용합니다.
/* kernel/sched/fair.c — tick 경로 (v7.3.1, 단순화) */
static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
    update_curr(cfs_rq);
    update_load_avg(cfs_rq, curr, UPDATE_TG);
    update_cfs_group(curr);

#ifdef CONFIG_SCHED_HRTICK
    if (queued) {
        resched_curr(rq_of(cfs_rq));
        return;
    }
#endif
}

/* update_curr()의 선점 판단 부분 */
    curr->vruntime += calc_delta_fair(delta_exec, curr);
    resched = update_deadline(cfs_rq, curr);

    /* ... fair_server 회계 ... */

    if (cfs_rq->h_nr_queued == 1)
        return;

    if (resched || !protect_slice(curr)) {
        resched_curr_lazy(rq);
        clear_buddies(cfs_rq, curr);
    }
/* kernel/sched/fair.c — wakeup_preempt_fair() (v7.3.1, 단순화) */
static void wakeup_preempt_fair(struct rq *rq, struct task_struct *p, int wake_flags)
{
    enum preempt_wakeup_action preempt_action = PREEMPT_WAKEUP_PICK;
    struct task_struct *donor = rq->donor;
    struct sched_entity *nse, *se = &donor->se, *pse = &p->se;
    struct cfs_rq *cfs_rq = &rq->cfs;

    if (p->sched_class != &fair_sched_class ||
        donor->sched_class != &fair_sched_class)
        return;

    if (!sched_feat(WAKEUP_PREEMPTION))
        return;

    update_curr_fair(rq);

    /* idle ↔ non-idle 교체는 즉시 선점 */
    if (se_is_idle(se) && !se_is_idle(pse))
        goto preempt;

    /* p의 slice가 더 짧고 eligible이면 curr의 slice 보호를 무시 */
    if (sched_feat(PREEMPT_SHORT) && (pse->slice < se->slice)) {
        preempt_action = PREEMPT_WAKEUP_SHORT;
        goto pick;
    }

    if (wake_flags & WF_FORK)
        goto update;

    if (sched_feat(NEXT_BUDDY) && set_preempt_buddy(cfs_rq, pse)) {
        if (wake_flags & WF_SYNC)
            preempt_action = preempt_sync(rq, wake_flags, pse, se);
    }

pick:
    if (cfs_rq->h_nr_queued) {
        nse = pick_next_entity(rq, preempt_action != PREEMPT_WAKEUP_SHORT);
        if (unlikely(!nse))
            goto pick;
        if (nse == pse)
            goto preempt;
    }

    if (preempt_action == PREEMPT_WAKEUP_SHORT && entity_eligible(cfs_rq, pse))
        goto preempt;
update:
    if (sched_feat(RUN_TO_PARITY))
        update_protect_slice(cfs_rq, se);
    return;

preempt:
    cancel_protect_slice(se);
    if (preempt_action == PREEMPT_WAKEUP_SHORT)
        set_short_buddy(cfs_rq, pse);
    resched_curr_lazy(rq);
}
코드 설명
  • tick 경로entity_tick()은 update_curr()만 호출합니다. update_curr()는 update_deadline()이 true(vruntime이 deadline 도달)를 반환하거나 protect_slice(curr)가 거짓이면 resched_curr_lazy()로 선점을 요청합니다. 경쟁자가 하나뿐이면(h_nr_queued == 1) 선점할 대상이 없으므로 생략합니다.
  • HRTICKCONFIG_SCHED_HRTICK이 켜져 있으면 slice 경계에 맞춰 hrtimer를 설정합니다. queued 인자는 이미 hrtick이 만료되어 재스케줄이 필요함을 뜻하며, 이때는 resched_curr()를 즉시 호출합니다.
  • preempt_actionwakeup_preempt_fair()는 PREEMPT_WAKEUP_NONE/SHORT/PICK/RESCHED 중 하나를 고릅니다. PREEMPT_SHORT는 wakee의 slice가 더 짧을 때 curr의 slice 보호를 무시하고, NEXT_BUDDY는 wakee를 buddy로 지정해 pick_next_entity()가 우선 고려하게 합니다. WF_SYNC는 preempt_sync()를 통해 동기 선점(RESCHED)으로 이어질 수 있습니다.
  • pick_next_entitypick_next_entity(rq, protect)는 pick_eevdf()로 실제 다음 후보를 구합니다. 후보가 wakee(pse)이면 즉시 preempt로 이동합니다. protect 인자는 PREEMPT_WAKEUP_SHORT일 때만 false가 되어 curr의 slice 보호를 해제합니다.
  • protect_slice / vprotset_protect_slice()는 vprot = se->deadline을 저장하고, RUN_TO_PARITY이면 cfs_rq_min_slice()로 보호 구간을 줄입니다. protect_slice()는 현재 태스크가 아직 vprot에 도달하지 않았는지 검사합니다. 이 메커니즘이 CFS의 sched_min_granularity를 대체합니다.
  • resched_curr_lazy즉시 TIF_NEED_RESCHED를 설정하는 resched_curr()와 달리, resched_curr_lazy()는 커널/유저 복귀 경계에서 선점을 지연시켜 불필요한 컨텍스트 스위치를 줄입니다.
CFS 선점과의 차이: CFS는 check_preempt_tick()에서 ideal_runtime과 sched_min_granularity를 비교하고, wakeup 시 wakeup_gran(기본 1ms) 버퍼(Buffer)로 잦은 선점을 막았습니다. v7.3.1 EEVDF에는 이 두 함수/파라미터가 없습니다. 대신 update_deadline()의 deadline 도달 판정과 protect_slice()/vprot, PREEMPT_SHORT, NEXT_BUDDY가 선점 빈도와 응답성을 조율합니다.

Augmented RB-Tree

EEVDF의 효율적 구현을 위해 기존 CFS의 RB-Tree에 세 가지 보조 키가 추가되었습니다: min_vruntime, min_slice, max_slice. 트리는 deadline 순서로 정렬되지만, "eligible 여부"는 vruntime으로 판정하므로 서브트리의 최소 vruntime을 알면 eligible 후보가 존재하는지 O(1)에 판단할 수 있습니다. min_slice/max_slice는 cfs_rq_min_slice()와 slice 보호(protect_slice) 계산에 사용됩니다.

Augmented RB-Tree: deadline 정렬 + min_vruntime 증강 C VD=1800 vruntime=1000, slice=1.5ms min_vruntime=300 A VD=900 vruntime=500, slice=2.0ms min_vruntime=300 E VD=2100 vruntime=1500, slice=1.0ms min_vruntime=1200 B VD=800 vruntime=300, slice=1.0ms min_vruntime=300 D VD=1900 vruntime=1200, slice=2.5ms min_vruntime=1200 F VD=2500 vruntime=1800, slice=1.5ms min_vruntime=1800 in-order 순회 = deadline 오름차순: B(800) → A(900) → C(1800) → D(1900) → E(2100) → F(2500) min_vruntime(node) = min(자신, 왼쪽 서브트리, 오른쪽 서브트리) V(t)=400일 때: 루트 min_vruntime=300 ≤ V(t) → 왼쪽으로 이동, B의 300이 eligible → B 선택 (굵은 경로) A의 min_vruntime도 300이므로 A를 거쳐 B까지 내려가며, 오른쪽 서브트리(E, D, F)는 탐색하지 않습니다. min_slice/max_slice도 같은 방식으로 전파 — cfs_rq_min_slice()와 protect_slice() 계산에 사용됩니다.
각 노드의 min_vruntime은 자신과 하위 서브트리 중 최소 vruntime을 저장합니다. min_slice/max_slice도 같은 방식으로 전파됩니다.
/* kernel/sched/fair.c — v7.3.1 RB-Tree augmentation */

/* 자식의 보조 키와 비교해 더 작은 값으로 갱신 */
static inline void __min_vruntime_update(struct sched_entity *se,
                                            struct rb_node *node)
{
    if (node) {
        struct sched_entity *rse = __node_2_se(node);

        if (vruntime_cmp(se->min_vruntime, ">", rse->min_vruntime))
            se->min_vruntime = rse->min_vruntime;
    }
}

/* se->min_vruntime = min(se->vruntime, {left,right}->min_vruntime) */
static inline bool min_vruntime_update(struct sched_entity *se, bool exit)
{
    u64 old_min_vruntime = se->min_vruntime;
    u64 old_min_slice = se->min_slice;
    u64 old_max_slice = se->max_slice;
    struct rb_node *node = &se->run_node;

    se->min_vruntime = se->vruntime;
    __min_vruntime_update(se, node->rb_right);
    __min_vruntime_update(se, node->rb_left);

    se->min_slice = se->slice;
    __min_slice_update(se, node->rb_right);
    __min_slice_update(se, node->rb_left);

    se->max_slice = se->slice;
    __max_slice_update(se, node->rb_right);
    __max_slice_update(se, node->rb_left);

    return se->min_vruntime == old_min_vruntime &&
           se->min_slice == old_min_slice &&
           se->max_slice == old_max_slice;
}

RB_DECLARE_CALLBACKS_MULTI(static, min_vruntime_cb, struct sched_entity,
                          run_node, min_vruntime_copy, min_vruntime_update);
RB_DECLARE_CALLBACKS_MULTI: 리눅스 커널의 augmented rbtree API는 RB_DECLARE_CALLBACKS_MULTI 매크로(Macro)로 회전(rotation) 시 보조 키를 자동 갱신합니다. min_vruntime_copy()는 복사 시 세 키를 함께 옮기고, min_vruntime_update()는 삽입/삭제/회전 후 자식 값으로 재계산합니다. 세 키가 모두 이전과 같으면 true를 반환해 상위 전파를 조기 종료합니다.

sched_entity EEVDF 관련 필드 상세

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

/* include/linux/sched.h — sched_entity EEVDF 관련 필드 (v7.3.1) */
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_vruntime;   /* augmented: 서브트리 최소 vruntime */
    u64                  min_slice;      /* augmented: 서브트리 최소 slice */
    u64                  max_slice;      /* augmented: 서브트리 최대 slice */
    unsigned char        sched_delayed;  /* 지연 dequeue 상태 */
    unsigned char        rel_deadline;   /* deadline이 상대값인 동안 1 */
    unsigned char        custom_slice;   /* sched_runtime으로 slice 재정의 */

    /* === 공통 === */
    s64                  vlag;           /* 가상 lag (dequeue 시 저장) */
    u64                  vprot;          /* slice 보호용 virtual deadline */
    u64                  slice;          /* 요청 실행 시간 (ns) */

    /* ... 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_vruntime / min_slice / max_sliceAugmented RB-Tree의 보조 키입니다. 트리는 deadline 순서로 정렬되지만, pick_eevdf()는 서브트리의 min_vruntime으로 eligible 후보가 있는지 O(1)에 판정해 탐색을 pruning합니다. min_slice/max_slice는 cfs_rq_min_slice()와 slice 보호 계산에 쓰입니다.
  • slice / custom_slice태스크가 요청한 실행 시간(나노초)입니다. custom_slice가 0이면 sysctl_sched_base_slice(v7.3.1 기본 0.7ms)를 쓰고, sched_setattr()의 sched_runtime으로 재정의하면 1로 설정되어 slice를 직접 유지합니다.
  • vlagupdate_entity_lag()가 dequeue 시 저장하는 가상 lag입니다. place_entity()에서 wakeup/migrate 시 vruntime을 복원하는 데 사용되어 공정성을 보존합니다.
  • vprotset_protect_slice()가 저장하는 보호용 virtual deadline입니다. protect_slice()는 현재 태스크가 이 지점까지는 선점되지 않도록 pick_eevdf()의 fast path에서 사용합니다.
  • sched_delayed / rel_deadlinesched_delayed는 DELAY_DEQUEUE로 트리에 남아 있는 지연 dequeue 상태, rel_deadline은 마이그레이션 시 deadline을 상대값으로 보존하는 동안 1입니다.
필드타입용도갱신 시점
vruntimeu64가상 실행 시간 (CFS 호환)update_curr() — 매 tick
deadlineu64virtual deadlineupdate_deadline() — vruntime >= deadline 시
min_vruntimeu64서브트리 최소 vruntime (augmented)RB-Tree 삽입/삭제/회전 시
min_slice / max_sliceu64서브트리 최소/최대 slice (augmented)RB-Tree 삽입/삭제/회전 시
vprotu64slice 보호용 virtual deadlineset_protect_slice() / update_protect_slice()
sched_delayedunsigned char지연 dequeue 상태dequeue/requeue 경로
custom_sliceunsigned charsched_runtime 재정의 여부sched_setattr()
sliceu64요청 실행 시간 (ns)enqueue/update_deadline() — 기본 0.7ms
vlags64저장된 lag (dequeue/migrate 시)place_entity() — enqueue 시 복원
/* kernel/sched/sched.h — cfs_rq의 EEVDF 관련 필드 (v7.3.1) */
struct cfs_rq {
    /* === 기존 === */
    struct load_weight    load;
    unsigned int         nr_queued;
    struct rb_root_cached tasks_timeline;

    /* === EEVDF 추가 === */
    s64                  sum_w_vruntime; /* Σ((v_i - v0) × w_i) */
    u64                  sum_weight;     /* Σ(w_i) */
    u64                  zero_vruntime;  /* 가중 평균의 기준점 v0 */
    unsigned int         sum_shift;      /* 정밀도 보정 시프트 */

    /* ... */
};

place_entity() 분석

place_entity()는 태스크가 enqueue될 때(fork, wakeup, migrate) vruntime과 deadline을 배치하는 핵심 함수입니다. EEVDF에서는 lag 보존이 핵심 원칙이며, v7.3.1 구현은 lag 팽창(lag inflation), 상대 deadline 마이그레이션, 새 태스크의 반 slice 시작을 모두 반영합니다.

배치 시나리오별 동작

시나리오CFS 동작EEVDF 동작 (v7.3.1)
새 태스크 (fork)vruntime = min_vruntimevruntime = avg_vruntime, lag=0, deadline은 vslice/2 (PLACE_DEADLINE_INITIAL)
wakeup (짧은 sleep)vruntime = max(vruntime, min_vruntime - sched_latency/2)PLACE_LAG: vlag를 (W+w_i)/W로 팽창해 복원 → vruntime = V - lag
wakeup (긴 sleep)동일 + sleeper creditdequeue 시 entity_lag()가 max_slice + TICK_NSEC로 클램프 → 과도한 boost 방지
migrate (다른 CPU)vruntime 보정 (min_vruntime 차이)PLACE_REL_DEADLINE: deadline을 상대값으로 옮긴 뒤 새 vruntime에 더함
place_entity() 배치 로직 (v7.3.1) place_entity(cfs_rq, se, flags) slice 확정 !se->custom_slice → se->slice = sched_base_slice vslice = calc_delta_fair(se->slice, se) PLACE_LAG && nr_queued && se->vlag? Yes lag 팽창 (가중 평균 희석 보정) lag = vlag × (W + w_i) / W W = sum_weight (+ curr 가중치) weight > load → update_zero_vruntime() No → lag = 0 se->vruntime = V - lag V = avg_vruntime(cfs_rq) PLACE_REL_DEADLINE && se->rel_deadline? Yes 마이그레이션 — 즉시 종료 deadline += vruntime rel_deadline = 0; return No PLACE_DEADLINE_INITIAL && ENQUEUE_INITIAL? Yes vslice /= 2 fork/exec은 반 slice로 경쟁 참여 No se->deadline = se->vruntime + vslice weight가 크면 vslice가 짧아 더 이른 deadline dequeue 시 entity_lag()가 vlag를 저장(max_slice + TICK_NSEC로 클램프) → 다음 enqueue에서 위 흐름으로 복원합니다.
place_entity()는 저장된 vlag를 가중 평균 희석 효과만큼 팽창시켜 복원하고, 상대 deadline 마이그레이션이면 deadline만 옮깁니다. 새 태스크는 반 slice로 경쟁에 참여합니다.
/* kernel/sched/fair.c — place_entity() (v7.3.1, 단순화) */
static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    u64 vslice, vruntime = avg_vruntime(cfs_rq);
    unsigned int nr_queued = cfs_rq->h_nr_queued;
    bool update_zero = false;
    s64 lag = 0;

    if (!se->custom_slice)
        se->slice = sysctl_sched_base_slice;
    vslice = calc_delta_fair(se->slice, se);

    if (flags & ENQUEUE_QUEUED)
        nr_queued -= 1;

    /* PLACE_LAG: 새 엔티티가 가중 평균을 끌어내리는 효과를 보정 */
    if (sched_feat(PLACE_LAG) && nr_queued && se->vlag) {
        long load, weight;

        lag = se->vlag;

        load = cfs_rq->sum_weight;
        if (curr && curr->on_rq)
            load += avg_vruntime_weight(cfs_rq, curr->h_load.weight);
        if (WARN_ON_ONCE(!load))
            load = 1;
        weight = avg_vruntime_weight(cfs_rq, se->h_load.weight);

        /* lag_i = (W + w_i) * vlag_i / W */
        lag += div64_long(lag * weight, load);

        if (weight > load)
            update_zero = true;
    }

    se->vruntime = vruntime - lag;

    if (update_zero)
        update_zero_vruntime(cfs_rq, -lag);

    /* 마이그레이션: deadline을 상대값으로 옮긴 뒤 종료 */
    if (sched_feat(PLACE_REL_DEADLINE) && se->rel_deadline) {
        se->deadline += se->vruntime;
        se->rel_deadline = 0;
        return;
    }

    /* 새 태스크는 평균적으로 slice 중간에서 시작하므로 반 slice */
    if (sched_feat(PLACE_DEADLINE_INITIAL) && (flags & ENQUEUE_INITIAL))
        vslice /= 2;

    se->deadline = se->vruntime + vslice;
}
코드 설명
  • 기준점avg_vruntime(cfs_rq)는 zero_vruntime + sum_w_vruntime/sum_weight로 가중 평균 V를 구합니다. 새 엔티티는 이 V를 기준으로 배치됩니다.
  • lag 팽창단순히 vruntime = V - vlag로 두면, 새 엔티티가 평균에 더해지면서 lag가 w_i/(W+w_i)만큼 희석됩니다. 이를 상쇄하려고 vlag *= (W+w_i)/W로 팽창시킵니다. div64_long()은 곱셈 오버플로를 피하기 위한 64비트 나눗셈입니다.
  • vruntime 배치양수 lag(덜 받은 상태)면 vruntime이 V보다 작아져 eligible이 되고, 음수 lag(더 받은 상태)면 V보다 커져 ineligible이 됩니다.
  • 상대 deadlinePLACE_REL_DEADLINE은 마이그레이션 시 deadline을 절대값이 아니라 현재 vruntime 기준 상대값으로 유지합니다. rel_deadline이 1이면 deadline += vruntime으로 새 CPU의 vruntime에 맞춰 복원하고 종료합니다.
  • 초기 반 sliceENQUEUE_INITIAL(fork/exec)이면 vslice /= 2로 deadline을 절반만 앞당깁니다. 기존 태스크들이 평균적으로 slice 중간에 있으므로 새 태스크가 과도하게 앞서지 않도록 하는 장치입니다.
  • deadline 계산vd_i = ve_i + r_i/w_i에 따라 calc_delta_fair(slice, se)를 더합니다. weight가 큰 태스크는 가상 시간 간격이 짧아 더 이른 deadline을 얻습니다.
/* dequeue 시 lag 저장 — 다음 enqueue의 place_entity()에서 복원 */
static bool __dequeue_task(struct rq *rq, struct task_struct *p, int flags)
{
    struct sched_entity *se = &p->se;
    struct cfs_rq *cfs_rq = &rq->cfs;

    clear_buddies(cfs_rq, se);
    update_curr_eevdf(cfs_rq);
    update_entity_lag(cfs_rq, se);   /* se->vlag = entity_lag(...) */
    /* ... DELAY_DEQUEUE 판단 ... */
}

/* entity_lag(): max_slice + TICK_NSEC를 상한으로 클램프 */
static s64 entity_lag(struct cfs_rq *cfs_rq,
                        struct sched_entity *se, u64 avruntime)
{
    u64 max_slice = cfs_rq_max_slice(cfs_rq) + TICK_NSEC;
    s64 vlag, limit;

    vlag = avruntime - se->vruntime;
    limit = calc_delta_fair(max_slice, se);

    return clamp(vlag, -limit, limit);
}
lag 보존의 수학적 의미: 태스크가 sleep 전 vlag=+500이었다면, wakeup 시 가중 평균 희석을 보정한 뒤 "500만큼 덜 받은 상태"로 복원됩니다. 상한은 개별 se->slice가 아니라 cfs_rq_max_slice() + TICK_NSEC이므로, slice가 짧은 태스크도 충분한 lag를 유지할 수 있습니다.

update_curr() 변경점

update_curr()는 현재 실행 중인 태스크의 vruntime을 갱신하고, deadline 도달 시 선점을 요청하는 핵심 함수입니다. v7.3.1에서는 cfs_rq->curr 대신 cfs_rq->h_curr를 사용하고, update_se()·protect_slice()·resched_curr_lazy()가 추가되었습니다.

/* kernel/sched/fair.c — update_curr() (v7.3.1) */
static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->h_curr;
    struct rq *rq = rq_of(cfs_rq);
    s64 delta_exec;
    bool resched;

    if (unlikely(!curr))
        return;

    delta_exec = update_se(rq, curr);
    if (unlikely(delta_exec <= 0))
        return;

    account_cfs_rq_runtime(cfs_rq, delta_exec);

    if (!entity_is_task(curr))
        return;

    cfs_rq = &rq->cfs;

    curr->vruntime += calc_delta_fair(delta_exec, curr);
    resched = update_deadline(cfs_rq, curr);

    dl_server_update(&rq->fair_server, delta_exec);

    if (cfs_rq->h_nr_queued == 1)
        return;

    if (resched || !protect_slice(curr)) {
        resched_curr_lazy(rq);
        clear_buddies(cfs_rq, curr);
    }
}
코드 설명
  • h_currcfs_rq->h_curr는 실제로 실행하기로 선택된 엔티티(rq->donor.se)입니다. proxy-exec 때문에 rq->curr.se와 다를 수 있으므로 EEVDF는 h_curr를 기준으로 회계합니다.
  • update_seupdate_se()는 exec_start와 sum_exec_runtime을 갱신하고 실행 시간 delta를 반환합니다. 시계 역행 등으로 delta가 0 이하이면 즉시 반환합니다.
  • deadline 갱신update_deadline()은 vruntime이 deadline에 도달했으면 새 deadline을 할당하고 true를 반환합니다. 이 반환값이 tick 선점의 근거가 됩니다.
  • fair_serverdl_server_update()는 fair 대역폭 서버(rq->fair_server)에 실행 시간을 회계합니다. 태스크가 fair_server를 대신해 실행 중이든 아니든 동일하게 적용됩니다.
  • protect_slice경쟁자가 2개 이상일 때, deadline에 도달했거나(resched) 현재 태스크의 slice 보호가 끝났으면(!protect_slice) resched_curr_lazy()로 선점을 요청합니다. clear_buddies()는 이전 buddy 선택을 무효화(Invalidation)합니다.
  • 호출 경로scheduler_tick() → task_tick_fair() → entity_tick() → update_curr() 외에도 put_prev_task(), pick_next_task(), enqueue/dequeue 경로에서 호출됩니다.
CFS와의 관계: vruntime 갱신(calc_delta_fair), PELT, cgroup bandwidth 회계는 CFS와 동일합니다. EEVDF가 추가한 것은 update_deadline() 호출과 protect_slice()/resched_curr_lazy() 기반 선점 판단입니다.

update_deadline() 상세

/* kernel/sched/fair.c — update_deadline() (v7.3.1) */
static bool update_deadline(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    /* 아직 deadline에 도달하지 않았으면 그대로 둔다 */
    if (vruntime_cmp(se->vruntime, "<", se->deadline))
        return false;

    /* custom_slice가 아니면 base slice로 재설정 */
    if (!se->custom_slice)
        se->slice = sysctl_sched_base_slice;

    /* EEVDF: vd_i = ve_i + r_i / w_i */
    se->deadline = se->vruntime + calc_delta_fair(se->slice, se);

    /* zero_vruntime를 최신 상태로 갱신 (반환값은 사용하지 않음) */
    avg_vruntime(cfs_rq);

    /* slice를 소진했으므로 재스케줄 필요 */
    return true;
}
코드 설명
  • 도달 판정vruntime_cmp(se->vruntime, "<", se->deadline)는 래핑을 고려한 비교입니다. vruntime이 deadline보다 작으면 아직 요청량을 다 쓰지 않았으므로 false를 반환합니다.
  • slice 재설정custom_slice가 0이면 sysctl_sched_base_slice(v7.3.1 기본 0.7ms)를 사용합니다. sched_runtime으로 지정한 태스크는 custom_slice=1이라 기존 slice를 유지합니다.
  • deadline 계산calc_delta_fair(slice, se)는 slice × NICE_0_LOAD / weight입니다. weight가 높을수록 가상 시간 간격이 짧아 더 이른 deadline을 받습니다.
  • avg_vruntime 호출반환값을 쓰지 않지만, avg_vruntime() 내부에서 zero_vruntime을 최신 vruntime에 맞춰 갱신하는 부수 효과가 있습니다.

tick 기반 선점 판단

v7.3.1의 tick 경로에는 CFS의 check_preempt_tick()이 없습니다. task_tick_fair() → entity_tick() → update_curr()가 실행되고, update_deadline()이 deadline 도달을 알리면 resched_curr_lazy()가 선점을 요청합니다.

/* kernel/sched/fair.c — task_tick_fair() / entity_tick() (v7.3.1, 단순화) */
static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
    struct sched_entity *se = &curr->se;

    if (se->on_rq) {
        struct cfs_rq *cfs_rq;

        for_each_sched_entity(se) {
            cfs_rq = cfs_rq_of(se);
            entity_tick(cfs_rq, se, queued);
        }
        /* ... reweight_eevdf() ... */
    }
}

static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
    update_curr(cfs_rq);
    update_load_avg(cfs_rq, curr, UPDATE_TG);
    update_cfs_group(curr);

#ifdef CONFIG_SCHED_HRTICK
    if (queued) {
        resched_curr(rq_of(cfs_rq));
        return;
    }
#endif
}
코드 설명
  • update_currtick마다 update_curr()가 vruntime과 deadline을 갱신합니다. deadline에 도달하면 resched_curr_lazy()가 호출됩니다.
  • HRTICKCONFIG_SCHED_HRTICK이면 slice 경계에 hrtimer를 걸어 두므로, queued가 1이면 이미 slice가 만료된 상태입니다. 이때는 resched_curr()로 즉시 재스케줄합니다.
  • PELT/cgroupupdate_load_avg()와 update_cfs_group()은 CFS와 동일하게 tick마다 부하와 그룹 가중치를 갱신합니다.

wakeup 선점 판단

wakeup 경로는 wakeup_preempt_fair()가 담당합니다. CFS의 wakeup_gran 버퍼 대신 PREEMPT_SHORT, NEXT_BUDDY, WF_SYNC, slice 보호(protect_slice())가 선점 여부를 결정합니다.

/* kernel/sched/fair.c — wakeup_preempt_fair() (v7.3.1, 단순화) */
static void wakeup_preempt_fair(struct rq *rq, struct task_struct *p, int wake_flags)
{
    enum preempt_wakeup_action preempt_action = PREEMPT_WAKEUP_PICK;
    struct task_struct *donor = rq->donor;
    struct sched_entity *nse, *se = &donor->se, *pse = &p->se;
    struct cfs_rq *cfs_rq = &rq->cfs;

    if (p->sched_class != &fair_sched_class ||
        donor->sched_class != &fair_sched_class)
        return;

    if (!sched_feat(WAKEUP_PREEMPTION))
        return;

    update_curr_fair(rq);

    /* idle ↔ non-idle 교체는 즉시 선점 */
    if (se_is_idle(se) && !se_is_idle(pse))
        goto preempt;

    /* wakee의 slice가 더 짧고 eligible이면 curr의 보호를 무시 */
    if (sched_feat(PREEMPT_SHORT) && (pse->slice < se->slice)) {
        preempt_action = PREEMPT_WAKEUP_SHORT;
        goto pick;
    }

    if (wake_flags & WF_FORK)
        goto update;

    if (sched_feat(NEXT_BUDDY) && set_preempt_buddy(cfs_rq, pse)) {
        if (wake_flags & WF_SYNC)
            preempt_action = preempt_sync(rq, wake_flags, pse, se);
    }

pick:
    if (cfs_rq->h_nr_queued) {
        nse = pick_next_entity(rq, preempt_action != PREEMPT_WAKEUP_SHORT);
        if (unlikely(!nse))
            goto pick;
        if (nse == pse)
            goto preempt;
    }

    if (preempt_action == PREEMPT_WAKEUP_SHORT && entity_eligible(cfs_rq, pse))
        goto preempt;

update:
    if (sched_feat(RUN_TO_PARITY))
        update_protect_slice(cfs_rq, se);
    return;

preempt:
    cancel_protect_slice(se);
    if (preempt_action == PREEMPT_WAKEUP_SHORT)
        set_short_buddy(cfs_rq, pse);
    resched_curr_lazy(rq);
}
코드 설명
  • 클래스/기능 검사wakee와 donor가 모두 fair 클래스여야 하며, WAKEUP_PREEMPTION이 꺼져 있으면 선점하지 않습니다.
  • idle 교체현재가 idle 엔티티이고 wakee가 non-idle이면 즉시 preempt로 이동합니다. 반대 방향(idle wakee)은 별도 처리가 없습니다.
  • PREEMPT_SHORTwakee의 slice가 현재보다 짧으면 PREEMPT_WAKEUP_SHORT로 표시하고 pick으로 갑니다. 이후 pick_next_entity(rq, protect=false)로 curr의 slice 보호를 해제하고, wakee가 eligible이면 선점합니다.
  • NEXT_BUDDY / WF_SYNCset_preempt_buddy()는 wakee를 다음 선택 후보로 지정합니다. WF_SYNC(동기 wakeup)이면 preempt_sync()가 PREEMPT_WAKEUP_RESCHED를 반환해 즉시 선점할 수 있습니다.
  • pick_next_entity실제 다음 후보를 구해 wakee이면 선점합니다. 후보가 없으면(!nse) 다시 pick을 시도합니다.
  • protect_slice 갱신선점하지 않는 경우 RUN_TO_PARITY이면 update_protect_slice()로 보호 구간을 현재 태스크의 진행에 맞춰 갱신합니다.
  • 선점 실행선점 시 cancel_protect_slice()로 보호를 해제하고, SHORT 경로면 set_short_buddy()로 wakee를 buddy에 등록한 뒤 resched_curr_lazy()를 호출합니다.
wakeup_gran 제거: CFS는 sysctl_sched_wakeup_granularity(기본 1ms)만큼 deadline이 앞서야 선점했습니다. v7.3.1 EEVDF에는 이 파라미터가 없으며, PREEMPT_SHORT와 protect_slice()/vprot가 잦은 선점과 응답성 사이를 조율합니다.

그룹 스케줄링과 EEVDF

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

EEVDF 그룹 스케줄링: 계층별 pick Root cfs_rq (CPU 0) pick: eligible && earliest VD Group A sched_entity (w=2048) VD=1200, lag=+300 → eligible Group B sched_entity (w=1024) VD=1500, lag=-100 → ineligible 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 Root: A(VD=1200, eligible)와 B(VD=1500, ineligible) 중 A를 선택 — 초록 경로 Group A 내부: A1(VD=800)과 A2(VD=1100) 중 A1을 선택 B1의 VD=600은 전체에서 가장 이르지만, Group B가 root에서 ineligible이므로 후보가 되지 않습니다. weight 2048 : 1024 = 2 : 1 → Group A가 Group B의 두 배 CPU 시간을 받습니다.
그룹 스케줄링에서 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 상태설명
base_slice_ns (debugfs)700,000 (0.7ms)신규기본 실행 슬라이스, deadline 간격 결정
sched_latency_ns제거됨제거EEVDF는 고정 slice를 사용하므로 동적 latency 계산이 없음
sched_min_granularity_ns제거됨제거최소 실행 보장은 protect_slice()/vprot가 담당
sched_wakeup_granularity_ns제거됨제거PREEMPT_SHORT와 deadline 비교로 대체
sched_child_runs_first제거됨제거EEVDF의 lag 보존으로 불필요
sched_nr_latency제거됨제거EEVDF에서 타임슬라이스 동적 계산 불필요
인터페이스 주의: sched_base_slice는 /proc/sys/kernel의 sysctl이 아니라 debugfs 노브입니다. 경로는 /sys/kernel/debug/sched/base_slice_ns이며, 값의 단위는 나노초입니다. CONFIG_SCHED_DEBUG가 켜져 있어야 합니다.
# EEVDF slice 확인 (debugfs, 단위 ns)
cat /sys/kernel/debug/sched/base_slice_ns
# 700000

# slice 줄이기: 더 짧은 deadline → 더 빠른 응답, 더 잦은 선점
echo 1000000 > /sys/kernel/debug/sched/base_slice_ns  # 1ms

# slice 늘리기: 더 긴 deadline → 더 나은 처리량, 더 적은 선점
echo 10000000 > /sys/kernel/debug/sched/base_slice_ns  # 10ms

# sched_debug에서 cfs_rq별 slice/deadline 통계 확인
cat /proc/sched_debug | head -30
base_slice_ns 영향: 이 값을 줄이면 deadline 간격이 짧아져 선점 빈도가 증가합니다. 대화형 응답은 좋아지지만 컨텍스트 스위칭(Context Switching) 오버헤드(Overhead)도 증가합니다. 서버 워크로드에서는 기본값(0.7ms) 또는 더 높은 값이 적합하고, 데스크톱에서는 1~2ms가 좋을 수 있습니다.

Latency Nice

메인라인 미머지 주의: latency_nice는 메인라인 커널에 병합되지 않은 제안(Proposal)입니다. 아래의 매핑 규칙·코드는 커널 메일링 리스트에 게시된 패치셋의 설계 의도를 설명한 것입니다. 실제 커널에는 sched_latency_nice 필드와 SCHED_FLAG_LATENCY_NICE 플래그가 존재하지 않으므로 sched_setattr()으로 설정할 수 없습니다. 응답 지연 제어는 debugfs base_slice_ns, SCHED_BATCH 정책, cgroup min_slice로 대신합니다.

Latency nice는 EEVDF와 함께 제안된 태스크 속성으로, nice 값(CPU 비율)과 독립적으로 응답 지연을 제어하는 것을 목표로 합니다.

매핑(Mapping) 규칙

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

매핑:
  latency_nice → latency_offset

  latency_nice = -20 → 가장 짧은 slice → 가장 이른 deadline → 최소 지연
  latency_nice =   0 → 기본 slice (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: nice와 독립된 응답 지연 축 CPU 비율 축 — nice가 결정 nice -20 ~ 19 weight nice 0 → 1024 CPU 비율 (점유율) 장기 평균 장기 점유율만 결정 (응답 시점과 무관) 응답 지연 축 — latency_nice가 결정 latency_nice -20 ~ 19 latency_prio NICE_TO_PRIO() 변환 latency_offset slice에 반영 slice base_slice × 배율 deadline 응답 시점 두 축은 서로 독립 — 한쪽 값을 바꿔도 다른 쪽 값은 바뀌지 않는다 latency_nice -20 0 (기본) +19 slice 매우 짧음 base_slice_ns 매우 김 deadline 가장 이른 기본 가장 늦은 응답 지연 최소 기본 최대 허용 용도 오디오, 게임, UI 일반 프로세스 컴파일, 인코딩 선점 빈도 잦음 기준 드묾 nice는 그대로 두고 latency_nice만 바꾼 경우 (nice = 0) latency_nice = -10 CPU 비율 동일 · deadline 이른 → 응답 빠름 latency_nice = +10 CPU 비율 동일 · deadline 늦음 → 지연 허용(배치) 제안 단계: latency_nice는 메인라인에 병합되지 않았고 sched_latency_nice 필드도 없습니다. 실제 응답 지연 제어는 debugfs base_slice_ns, SCHED_BATCH, cgroup min_slice로 대체합니다.
RFC 패치셋 기준 설계 개요(개념 수준) — nice는 weight를 거쳐 CPU 비율을, latency_nice는 latency_prio·latency_offset을 거쳐 slice와 deadline으로 응답 지연을 각각 제어하며 두 축은 서로 독립입니다
/* 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/<PID>/sched | grep latency
nice vs latency_nice 분리의 이점: 기존에는 "CPU를 적게 쓰지만 빠르게 응답해야 하는" 태스크(예: 오디오 데몬)를 nice로 표현하기 어려웠습니다. nice를 낮추면 CPU 비율이 너무 높아지고, nice를 높이면 응답이 느려졌습니다. latency_nice는 이 두 차원을 분리합니다.

워크로드별 성능 분석

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

대화형 워크로드

특성CFSEEVDF영향
wakeup 지연wakeup_gran에 의해 제한eligible이면 protect_slice()까지 보류 후 선점EEVDF 유리 (더 빠른 응답)
짧은 burst 처리buddy 힌트로 캐시 친화lag 보존으로 eligible 우선EEVDF 유리 (수학적 공정성)
선점 빈도wakeup_gran + min_granularityupdate_deadline + protect_slice()/PREEMPT_SHORTEEVDF에서 더 잦을 수 있음

배치 워크로드

특성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 4개 + interactive 1개 (1ms burst, 1ms = 60px) CFS (v6.5) CPU 실행 Interactive CPU-A CPU-B CPU-C CPU-D A CPU-B Int CPU-C CPU-D CPU-A CPU-B Int C wakeup wakeup 대기 1ms Int 대기 Int 0 2 4 6 8 10 12 ms wakeup 후 wakeup_gran(≈1ms)을 기다린 뒤 선점 → 지연이 변동적 EEVDF (v6.6+) CPU 실행 Interactive CPU-A CPU-B CPU-C CPU-D A Int CPU-B CPU-C CPU-D A Int CPU-B C wakeup wakeup Int Int 0 2 4 6 8 10 12 ms wakeup 즉시 resched 요청 → 다음 선점 지점에서 전환 (1 tick 이내), 지연이 일정 CPU-A CPU-B CPU-C CPU-D Interactive 대기(지연)
같은 시나리오를 같은 시간 척도로 비교합니다. CFS는 wakeup 후 wakeup_gran만큼 기다리는 반면, EEVDF는 즉시 선점을 요청합니다.
실제 벤치마크 결과: Peter Zijlstra의 패치셋에서 보고된 바에 따르면, EEVDF는 hackbench에서 CFS와 동등하거나 약간 나은 성능을, schbench에서 현저히 개선된 지연 특성을 보여주었습니다.

벤치마크 비교

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

벤치마크측정 항목CFS (v6.5)EEVDF (v6.6)차이
hackbench처리량 (msg/s)기준동등~다소 향상오차 내
schbench (p50)지연 (us)기준크게 감소크게 감소
schbench (p99)지연 (us)기준크게 감소크게 감소
tbench처리량기준동등오차 내
sysbench oltpTPS기준다소 향상다소 향상
cyclictest (avg)지연 (us)기준다소 감소다소 감소
compile kernel시간 (s)기준동등오차 내
CFS vs EEVDF: 정성적 경향 비교 방향성만 표현합니다 — 막대 길이나 절대 수치는 주장하지 않습니다 처리량 (Throughput) EEVDF = CFS (오차 내) hackbench (msg/s) = 동등 tbench (처리량) = 동등 커널 빌드 (시간) = 동등 공정 분배 로직(vruntime·weight)이 동일하기 때문 스케줄링 지연 (Latency) EEVDF < CFS (경향) schbench p50 ↓ 감소 schbench p99 ↓ 감소 cyclictest (avg) ↓ 감소 wakeup 시 deadline 비교로 선점 시점이 확정적 EEVDF는 pick 규칙(eligible + earliest deadline)만 교체하고 vruntime·weight·PELT·cgroup 인프라는 그대로 사용합니다. 따라서 처리량은 유지되고, 지연의 변동 폭이 줄어드는 방향으로 나타납니다. 해석 시 주의 실제 수치는 CPU, 코어 수, HZ, cgroup 설정, 워크로드에 따라 크게 달라집니다. 벤치마크를 인용할 때는 측정 환경과 도구를 함께 명시해야 합니다. 이 그림은 방향성만 표현하며 특정 수치를 주장하지 않습니다.
EEVDF는 처리량을 유지하면서 지연을 개선하는 방향으로 작용하지만, 실제 수치는 측정 환경에 따라 크게 달라집니다
벤치마크 해석 주의: 위 수치는 특정 하드웨어/설정에서의 대략적 경향이며, 절대값은 환경에 따라 크게 달라질 수 있습니다. 핵심 관찰은: (1) 처리량은 CFS와 동등, (2) 지연은 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: 태스크 기아 의심

# 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가 너무 큰지 확인 (debugfs, ns)
cat /sys/kernel/debug/sched/base_slice_ns
# 기본값 700000(0.7ms)보다 크면 줄여보기
echo 700000 > /sys/kernel/debug/sched/base_slice_ns

# 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) leftmost vruntime buddy + wakeup_gran 지연 보장 없음 v6.6 (도입) pick_eevdf() entity_eligible() deadline 정렬 RB-Tree v6.12 (지연 디큐) sched_delayed PLACE_REL_DEADLINE min_slice 증강 v6.18 (3키 증강) sum_w_vruntime sum_weight · zero_vruntime avg_vruntime_add() 제거 v7.x (정합화) protect_slice · vprot PICK_BUDDY 정교화 계층 부하 h_load.weight 전환 체크리스트 1 제거된 sysctl 정리 — sched_wakeup_granularity_ns, sched_child_runs_first, sched_nr_latency 2 sched_latency_ns · sched_min_granularity_ns 전제의 튜닝 스크립트 제거 3 base_slice_ns 확인 (debugfs, 기본 0.7ms) 후 워크로드에 맞게 조정 4 schbench로 지연 특성(p50·p99) 비교 — 측정 환경과 함께 기록 5 buddy 의존 패턴 점검 — EEVDF는 deadline 기반이라 wakeup buddy 힌트가 기본 비활성
v6.6에서 EEVDF가 도입된 뒤 버전별로 데이터 구조와 선점 판정이 확장되었습니다. 버전 표기는 각 버전 소스에서 해당 심볼을 확인한 시점을 기준으로 합니다.

마이그레이션 체크리스트

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

[ ] 2. base_slice_ns 설정 (debugfs)
      - 기본 0.7ms
      - 데스크톱: 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이 시간에 따라 어떻게 진행되는지를 시각화합니다.

vruntime 증가율과 weight A: nice 0 (weight 1024) · B: nice -5 (weight 3121) · C: nice +5 (weight 335) 1ms 실행했을 때 vruntime 증가량 vruntime += 1ms × 1024 / weight 0 1 2 3 vruntime (ms) 실행 시간 1ms A nice 0 → +1.00 B nice -5 → +0.33 C nice +5 → +3.06 기울기 = 1 / weight → 큰 weight는 완만, 작은 weight는 가파름 장기 CPU 점유율 ∝ weight weight 상수 비율로 계산 (합 4480) B nice -5 (3121) ≈ 69.7% A nice 0 (1024) ≈ 22.9% C nice +5 (335) ≈ 7.5% 점유율은 weight 비율의 산술 결과이며, 실제 측정값이 아닙니다 (반올림)
EEVDF는 실행 시간 자체를 weight로 나눠 vruntime을 진행시키므로, weight가 큰 태스크는 같은 실행 시간에도 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 — avg_vruntime() / update_zero_vruntime() (v7.3.1) */
void update_zero_vruntime(struct cfs_rq *cfs_rq, s64 delta)
{
    /* v0를 delta만큼 옮기면 sum_w_vruntime은 -delta*sum_weight만큼 보정 */
    cfs_rq->sum_w_vruntime -= cfs_rq->sum_weight * delta;
    cfs_rq->zero_vruntime += delta;
}

u64 avg_vruntime(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    long weight = cfs_rq->sum_weight;
    s64 delta = 0;

    if (curr && !curr->on_rq)
        curr = NULL;

    if (weight) {
        s64 runtime = cfs_rq->sum_w_vruntime;

        if (curr) {
            unsigned long w = avg_vruntime_weight(cfs_rq, curr->h_load.weight);

            runtime += entity_key(cfs_rq, curr) * w;
            weight += w;
        }

        /* 음수일 때 floor/ceiling을 뒤집어 left bias 확보 */
        if (runtime < 0)
            runtime -= (weight - 1);

        delta = div64_long(runtime, weight);
    } else if (curr) {
        delta = curr->vruntime - cfs_rq->zero_vruntime;
    }

    update_zero_vruntime(cfs_rq, delta);

    return cfs_rq->zero_vruntime;
}

enqueue/dequeue 흐름

/* kernel/sched/fair.c — enqueue_task_fair()의 배치/삽입 순서 (v7.3.1, 단순화) */
static void enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
    struct sched_entity *se = &p->se;
    struct cfs_rq *cfs_rq = &rq->cfs;
    unsigned long weight;
    bool curr;

    update_curr_eevdf(cfs_rq);

    curr = (cfs_rq->curr == se);
    if (curr)
        place_entity(cfs_rq, se, flags);

    if (se->on_rq && se->sched_delayed)
        requeue_delayed_entity(cfs_rq, se);

    weight = enqueue_hierarchy(p, flags);

    if (!curr) {
        reweight_eevdf(cfs_rq, se, weight, false);
        place_entity(cfs_rq, se, flags | ENQUEUE_QUEUED);
        __enqueue_entity(cfs_rq, se);
    }

    add_nr_running(rq, 1);
}

static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    /* PELT/cgroup 가중치 갱신 + 회계. RB-Tree 삽입은 하지 않는다. */
    update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH);
    se_update_runnable(se);
    update_cfs_group(se);
    account_entity_enqueue(cfs_rq, se);
    se->on_rq = 1;
}
/* __enqueue_entity — deadline 정렬 RB-Tree 삽입 + augmentation (v7.3.1) */
static void __enqueue_entity(struct cfs_rq *cfs_rq,
                              struct sched_entity *se)
{
    sum_w_vruntime_add(cfs_rq, se);
    se->min_vruntime = se->vruntime;
    se->min_slice = se->slice;
    se->max_slice = se->slice;

    rb_add_augmented_cached(&se->run_node,
                            &cfs_rq->tasks_timeline,
                            __entity_less,
                            &min_vruntime_cb);
    /* 세 augmented 키가 회전/삽입 시 자동 전파됨 */
}

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_vruntime과 min_vruntime을 가집니다. EEVDF는 vlag(lag 값)을 보존하여 마이그레이션 후에도 공정성을 유지합니다.

NUMA 마이그레이션: vlag 보존 절대 vruntime이 아니라 lag(V(t) − v)를 옮깁니다 — 수치는 개념 설명용 예시입니다 CPU 0 (출발지) V(t) = avg_vruntime = 10,000,000 Task P의 v = 9,200,000 lag = V(t) − v = 800,000 (덜 받은 상태) → eligible: 다른 태스크보다 먼저 선택될 수 있음 migrate CPU 3 (도착지) V(t) = avg_vruntime = 25,000,000 Task P의 v = 24,200,000 (재계산) lag = 25,000,000 − 24,200,000 = 800,000 → 같은 lag: CPU별 가상 시간 기준과 무관 ① dequeue (CPU 0) vlag = V₀ − v₀ = 10,000,000 − 9,200,000 ② 이동 se->vlag = 800,000 유지 vruntime은 아직 갱신하지 않음 ③ enqueue (CPU 3) v = V₁ − vlag = 25,000,000 − 800,000 CFS: vruntime 재기준화 v ← v − min_vruntime(옛 CPU) + min_vruntime(새 CPU) CPU 간 가상 시간 격차가 크면 오차가 누적될 수 있음 sleeper credit 해석이 흔들릴 수 있음 → 공정성 판단이 CPU별 기준값에 의존 EEVDF: lag만 보존 vlag = V(옛 CPU) − v 저장, v = V(새 CPU) − vlag 복원 이동 전후 lag가 같으므로 보정 기준값이 필요 없음 CPU별 V(t) 절대값 차이는 결과에 영향 없음 → 공정성 판단이 상대값(lag)만으로 성립
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가 제안만 되고 메인라인에 병합되지 못함메인라인 미포함 — base_slice_ns(debugfs)·cgroup min_slice로 부분 대체
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~30nsO(1) rb_first_cached
EEVDF pick~80nsO(log 100) ≈ O(7)

이 차이는 slice 길이에 비해 무시할 수 있는 수준이며, 실무적으로 체감되지 않습니다.

감지된 회귀: 초기 v6.6에서 일부 게임/VR 워크로드에서 미세한 프레임 시간 변동이 보고되었습니다. 이는 buddy 제거로 인한 캐시 패턴 변화와 관련이 있었으며, v6.7에서 slice 기본값 조정으로 대부분 해소되었습니다. 여전히 문제가 있다면 debugfs의 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 지연)하드 실시간(Real-time) (EDF 보장)
입력 파라미터nice (+ latency_nice 제안)runtime, deadline, period
대상일반 워크로드실시간 워크로드 (오디오, 산업 제어)
기아 방지lag 바운드admission control
CPU 독점불가능 (공정 분배)가능 (예약된 시간 보장)

sched_class 우선순위 계층에서 EEVDF의 위치는 다음과 같습니다.

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 태스크가 있으면 항상 먼저 실행됩니다.

deadline의 두 가지 의미 EEVDF (fair_sched_class) 가상 시간(vruntime) 축 — 공정성의 마감 slice / weight (가상 시간 단위) V(t) VE VD 목적: 비례 공유 — weight만큼 CPU를 나눠 받음 입력: nice (+ latency_nice는 제안 단계) 보장: lag 상한 −r_max < lag < max(r_max, q) 권한: 일반 사용자 (별도 설정 없음) 실시간 보장은 아님 — 지연은 확률적으로 짧아질 뿐 SCHED_DEADLINE (dl_sched_class) 실제 시간(wall clock) 축 — 하드 실시간 마감 t (실제 시간) runtime deadline period runtime ≤ deadline ≤ period 목적: 하드 실시간 보장 (EDF) 입력: runtime, deadline, period 보장: deadline 안에 runtime 완료 (admission 통과 시) 권한: CAP_SYS_NICE 또는 root 필요 대역폭 초과 시 throttling — 일반 태스크보다 항상 먼저 실행 적합한 워크로드 데스크톱 앱, 웹 서버, DB, 컨테이너, 배치 컴파일 "대체로 빠르면 되는" 지연 민감 워크로드 적합한 워크로드 실시간 오디오(JACK), 산업 제어(PLC), 로봇 제어 "기한을 넘기면 실패"인 결정적 주기 워크로드 DL > RT > FAIR(EEVDF) > IDLE
EEVDF의 virtual deadline은 가상 시간 축의 공정성 마감이고, SCHED_DEADLINE의 deadline은 실제 시간 축에서 runtime 완료를 보장하는 하드 실시간 마감입니다
언제 SCHED_DEADLINE을 쓰나: 실시간 오디오 처리(JACK), 산업 제어 시스템 등 절대적 시간 보장이 필요한 경우에만 SCHED_DEADLINE을 사용합니다. 일반적인 대화형 응답성 개선은 EEVDF의 slice 조정(base_slice_ns)이나 제안된 latency_nice로 충분합니다.

전체 코드 워크스루

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

EEVDF 태스크 생명주기: lag 저장과 복원의 순환 ① Wakeup & Enqueue try_to_wake_up() → ttwu_do_activate() place_entity(): lag 복원 vruntime = avg − vlag, VD 설정 RB-Tree 삽입 + min_dl 전파 ② 선점 판단 wakeup_preempt_fair() eligible && VD < curr VD? Yes → resched_curr_lazy() 예약 No → 선점 없이 계속 실행 선점 예약은 다음 schedule()에서 소비 NEXT_BUDDY·PREEMPT_SHORT·WF_SYNC가 힌트 ③ 선택 & 실행 schedule() → pick_eevdf() eligible 중 earliest VD 선택 context_switch(prev, next) 태스크 실행 시작 ④ 실행 중 (매 Tick) scheduler_tick() update_curr(): vruntime ↑ update_deadline() slice 소진? delta_exec > slice 이면 재선택 요청 No: 계속 실행 Yes: ⑤로 ⑤ Sleep & Dequeue (또는 선점) dequeue_entity() se->vlag = entity_lag() (r_max로 클램프) SLEEPING — vlag 보존 다음 wakeup 시 복귀
EEVDF 태스크 생명주기: wakeup에서 lag를 복원하고, 선점 판단과 선택을 거쳐 실행되며, sleep 시 dequeue에서 lag를 저장해 다음 wakeup으로 순환합니다

전체 EEVDF 경로 워크스루는 다음과 같습니다.

  1. try_to_wake_up(p) — Wakeup 경로
    • ttwu_queue(p, cpu)
    • ttwu_do_activate(rq, p)
    • activate_task(rq, p, ENQUEUE_WAKEUP)
    • enqueue_task_fair(rq, p, ENQUEUE_WAKEUP)
    • reweight_eevdf(cfs_rq, se, weight, false)
    • place_entity(cfs_rq, se, flags | ENQUEUE_QUEUED) — vruntime = avg_vruntime - lag(lag 복원), deadline = vruntime + calc_delta_fair(slice, se)
    • __enqueue_entity(cfs_rq, se) — RB-Tree 삽입 + min_vruntime/min_slice/max_slice 전파
    • enqueue_entity(cfs_rq, se, flags) — PELT/cgroup 회계, on_rq = 1
  2. wakeup_preempt_fair(rq, p, wake_flags) — 선점 판단
    • PREEMPT_SHORT/NEXT_BUDDY/WF_SYNC로 preempt_action 결정
    • pick_next_entity(rq, protect) — pick_eevdf()로 실제 후보 확인
    • resched_curr_lazy(rq) — 선점 예약
  3. schedule() → __schedule() — 태스크 선택
    • pick_next_task_fair(rq)
    • pick_next_entity(rq, protect)
    • pick_eevdf(cfs_rq, protect) — eligible 필터링 + earliest deadline 선택
    • context_switch(rq, prev, next)
  4. scheduler_tick() — 실행 중 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)
    • update_deadline(cfs_rq, curr) — vruntime >= deadline이면 resched_curr_lazy 요청
  5. dequeue_task_fair() — 태스크 sleep
    • dequeue_entity(cfs_rq, se)
    • update_entity_lag(cfs_rq, se) — se->vlag에 lag 저장 (entity_lag()로 클램프)
    • __dequeue_entity(cfs_rq, se)
    • sum_w_vruntime_sub(cfs_rq, se)
핵심 흐름 요약: wakeup 시 place_entity()에서 lag를 복원하고 deadline을 설정한 뒤, wakeup_preempt_fair()가 preempt_action과 pick_next_entity() 결과로 선점을 결정합니다. 실행 중에는 update_curr()의 update_deadline()이 slice 소진을 추적합니다.

수치 시뮬레이션 예제

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

초기 조건

시스템 설정:
  slice = 3ms (계산 편의를 위한 가정, 실제 기본값 0.7ms)
  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 우선 처리
수치 시뮬레이션: 0~12ms 실행 순서 slice = 3ms 가정, A(nice 0, w=1024), B(nice −5, w=3121), C(nice +5, w=335) — 계산 예시 0ms 3ms 6ms 9ms 12ms 실행 B A B B C t=12ms → C 선택 선택 근거 pick: B B.VD = 984,300 A.VD = 3,000,000 C.VD = 9,171,642 pick: A A.VD = 3,000,000 C.VD = 9,171,642 B는 ineligible pick: B (재선택) B.VD = 1,968,600 C.VD = 9,171,642 B가 아직 eligible pick: B (세 번째) B.VD = 2,952,900 C.VD = 9,171,642 C는 계속 대기 t=12ms 누적 실행 (총 12ms) vs 이상적 weight 비율 A (w=1024) 3ms = 25% (이상 22.8%) B (w=3121) 9ms = 75% (이상 69.7%) C (w=335) 0ms = 0% (이상 7.5%) 0 ~12ms 선택 규칙: eligible 필터를 통과한 태스크 중 earliest VD t=12ms에 B의 vruntime(2,952,900)이 avg(≈2,742,899)를 넘어 ineligible이 되고, 유일한 eligible인 C가 선택됩니다
weight가 큰 B가 VD 간격이 짧아 자주 선택되지만, vruntime이 avg를 넘는 순간 ineligible이 되어 대기하던 C가 실행 기회를 얻습니다

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(cfs_rq, I, avg)
     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 휴리스틱이 모두 불필요해진 이유입니다.

Linux 6.12+ EEVDF 개선 사항 (6.12 ~ 6.19)

EEVDF는 Linux 6.6 머지 이후에도 계속 다듬어지고 있습니다. 6.12에서는 짧은 슬립 패턴의 공정성을 보존하는 DELAY_DEQUEUE가 추가되었고, 6.13에서는 PREEMPT_LAZY 모델이 도입되어 EEVDF의 deadline 기반 선점과 새롭게 상호작용합니다. 운영 환경에서 6.6 ~ 6.13 커널을 함께 다루는 경우, 알고리즘 동작의 미묘한 차이를 이해해야 정확한 튜닝이 가능합니다.

DELAY_DEQUEUE — 슬립 직전 lag 보존

기존 EEVDF는 태스크가 슬립하면 즉시 RB-Tree에서 제거하면서 vlag를 마지막에 갱신했습니다. 그런데 짧게 슬립하고 곧 깨어나는 대화형 워크로드(예: GUI 이벤트 루프(Event Loop))는 슬립(Sleep) 동안 avg_vruntime이 진행되어 깨어날 때 큰 양의 lag을 갖게 되고, 결과적으로 다른 태스크보다 부당하게 자주 선택되는 부작용이 있었습니다. Peter Zijlstra가 v6.12에 머지한 DELAY_DEQUEUE는 이 문제를 해결하기 위해 dequeue 자체를 지연시킵니다.

/* kernel/sched/fair.c (v6.12+, 단순화) */
static bool
dequeue_entity(struct cfs_rq *cfs_rq,
               struct sched_entity *se, int flags)
{
    /* DEQUEUE_SLEEP: voluntary sleep 경로 */
    if (sched_feat(DELAY_DEQUEUE) &&
        (flags & DEQUEUE_SLEEP) &&
        entity_eligible(cfs_rq, se)) {
        /* eligible 상태에서 슬립 → 즉시 제거하지 않음 */
        se->sched_delayed = 1;       /* 트리에는 남기되 실행은 안 됨 */
        return false;
    }

    /* not-eligible(lag < 0)이거나 강제 dequeue: 기존 경로 */
    update_curr(cfs_rq);
    update_entity_lag(cfs_rq, se);  /* 마지막 vlag 기록 */
    __dequeue_entity(cfs_rq, se);
    se->on_rq = 0;
    return true;
}

/* wake-up 시: sched_delayed 플래그가 있으면 트리 재삽입 없이 활성화 */
static void
enqueue_entity(struct cfs_rq *cfs_rq,
               struct sched_entity *se, int flags)
{
    if (se->sched_delayed) {
        se->sched_delayed = 0;     /* lag 그대로 보존된 채 재실행 */
        return;
    }
    place_entity(cfs_rq, se, flags);
    __enqueue_entity(cfs_rq, se);
    se->on_rq = 1;
}
핵심 효과: 짧게 슬립한 태스크는 슬립 동안의 시간만큼 vlag가 부당하게 늘어나지 않습니다. 따라서 깨어났을 때 deadline이 비합리적으로 이른 값이 되지 않아, 다른 태스크의 공정성을 침해하지 않습니다. UI 응답성과 백그라운드 처리량의 균형이 동시에 개선됩니다.

DELAY_DEQUEUE 효과 측정

워크로드v6.11 (NO_DELAY_DEQUEUE)v6.12 (DELAY_DEQUEUE)변화
hackbench (혼합)기준동등오차 내
대화형 p99 wakeup latency기준크게 감소크게 감소
백그라운드 컴파일 처리량기준다소 향상다소 향상
schbench RPS기준다소 향상다소 향상
# DELAY_DEQUEUE 활성화 여부 확인 (v6.12+)
cat /sys/kernel/debug/sched/features | tr ' ' '\n' | grep DELAY_DEQUEUE
# 출력: DELAY_DEQUEUE (활성) 또는 NO_DELAY_DEQUEUE (비활성)

# 비활성화하여 회귀 테스트
echo NO_DELAY_DEQUEUE > /sys/kernel/debug/sched/features

# 다시 활성화
echo DELAY_DEQUEUE > /sys/kernel/debug/sched/features

# sched_delayed 상태인 태스크 확인 (디버깅용)
cat /proc/sched_debug | grep -A2 sched_delayed

PREEMPT_LAZY와 EEVDF 선점 상호작용 (Linux 6.13+)

Linux 6.13에서 추가된 PREEMPT_LAZY는 SCHED_NORMAL(EEVDF) 태스크에 대해서는 협력적 선점만 적용하고, RT 태스크에는 즉시 선점을 적용하는 하이브리드 모델입니다. EEVDF의 입장에서 보면, deadline 비교로 다른 태스크가 선택되어도 현재 실행 중 EEVDF 태스크는 다음 schedule() 지점까지 계속 실행됩니다.

/* kernel/sched/fair.c (v6.13+ 단순화) */
static void
check_preempt_wakeup_fair(struct rq *rq,
                          struct task_struct *p, int wake_flags)
{
    struct sched_entity *se = &rq->curr->se;
    struct sched_entity *pse = &p->se;

    /* deadline 비교: 새 태스크가 더 이른 deadline인가? */
    if (pick_eevdf(cfs_rq) != se) {
        /* PREEMPT_LAZY: SCHED_NORMAL은 즉시 선점하지 않고 NEED_RESCHED_LAZY 마킹 */
        resched_curr_lazy(rq);
        /* 다음 syscall 반환, 명시적 cond_resched(), tick 경계에서 선점 */
        return;
    }
}

/* RT 태스크가 깨어나면 PREEMPT_LAZY와 무관하게 즉시 선점 */
static void check_preempt_curr_rt(struct rq *rq, ...)
{
    resched_curr(rq);  /* NEED_RESCHED — 즉시 선점 */
}
같은 판정, 다른 전환 시점: PREEMPT_LAZY vs 즉시 선점 A(vd 300) 실행 중 B(vd 180)가 깨어남 — pick_eevdf()는 두 경우 모두 B를 선택합니다 즉시 선점 PREEMPT_FULL · RT A 실행 (vd=300) B 실행 (vd=180, 더 이른 VD) B 깨어남 (wakeup) resched_curr() → 즉시 전환, wake-to-run 지연 거의 없음 PREEMPT_LAZY SCHED_NORMAL A 실행 (vd=300) A 계속 실행 NEED_RESCHED_LAZY B 실행 tick 경계 (다음 schedule 지점) 항목 즉시 선점 (FULL · RT) PREEMPT_LAZY (EEVDF) 실제 전환 호출 resched_curr() — 즉시 요청 resched_curr_lazy() — 마킹만 전환 시점 wakeup 경로에서 바로 syscall 복귀 · cond_resched() · tick wake-to-run 지연 거의 0 다음 schedule 지점까지 (최대 1 tick) 부수 효과 A의 캐시를 즉시 무효화 A의 캐시 보존, 남은 slice 활용 RT·DL 태스크는 PREEMPT_LAZY와 무관하게 즉시 선점 — EEVDF 판정은 그대로, 전환 시점만 조정됩니다
PREEMPT_LAZY는 EEVDF의 deadline 판정 결과를 그대로 사용하지만, SCHED_NORMAL 태스크의 실제 컨텍스트 스위치를 다음 schedule 지점까지 늦춰 실행 중 태스크의 캐시를 보존합니다

base_slice_ns 디버그 인터페이스 (v6.7+)

EEVDF의 기본 slice 값은 /sys/kernel/debug/sched/base_slice_ns로 조정할 수 있습니다(v6.7부터 노출). 이 값은 sched_attr.sched_runtime을 명시하지 않은 모든 태스크의 deadline 계산에 사용됩니다.

# 현재 base_slice 확인 (HZ=1000 기준 기본 750000ns = 0.75ms)
cat /sys/kernel/debug/sched/base_slice_ns

# 대화형 워크로드 우선: slice 짧게 (500us)
echo 500000 > /sys/kernel/debug/sched/base_slice_ns

# 처리량 우선: slice 길게 (3ms)
echo 3000000 > /sys/kernel/debug/sched/base_slice_ns

# 효과 측정 — 컨텍스트 스위치 빈도가 비례
vmstat 1 5 | awk 'NR>2 {print $12}'
# 짧은 slice → cs/s 증가, 긴 slice → cs/s 감소
base_slice_ns특성적합 워크로드주의점
250µs매우 짧은 deadline, 매우 잦은 선점오디오/저지연 통신cs 오버헤드 큼
750µs (기본)균형일반 데스크톱/서버—
3ms긴 deadline, 캐시 친화컴파일·배치UI 지터 가능
10ms거의 협력적HPC, 단일 워크로드 전용대화형 태스크 영향

Linux 6.14 — cfs_rq.nr_running → nr_queued 개명

Linux 6.14(2025-03)에서 cfs_rq.nr_running이 nr_queued로 개명되었습니다(v6.12 sched.h에는 nr_running만, v6.14에는 nr_queued·h_nr_queued가 존재). 실행 중인 태스크가 아니라 "큐에 올라 대기 중인 엔티티"를 센다는 EEVDF의 의미를 이름에 반영한 변경입니다.

문서도 함께 정비되었습니다. EEVDF 설명 문서(Documentation/scheduler/sched-eevdf.rst)는 v6.12에 추가되었고, 기존 sched-design-CFS.rst는 별도 문서로 계속 유지됩니다. 즉 CFS라는 용어가 삭제된 것이 아니라, EEVDF 문서가 나란히 추가된 것입니다.

# nr_queued 개명 확인 — v6.14부터 nr_running → nr_queued
grep -m5 "nr_queued" /proc/sched_debug
# EEVDF 전환 후 사라진 CFS 튜너블 확인 (목록에 없음)
ls /proc/sys/kernel/sched_latency_ns /proc/sys/kernel/sched_wakeup_granularity_ns 2>/dev/null || echo "CFS tunables removed"

Linux 6.15 — sched_ext 관측성 강화 및 디버그 무조건화

Linux 6.15(2025-05)는 EEVDF와 sched_ext 하이브리드 환경의 관측성을 크게 높입니다. sched_ext에 내부 이벤트 카운터가 추가되어 BPF 스케줄러의 미세한 동작을 /sys/kernel/sched_ext/root/events를 통해 실시간으로 관찰할 수 있게 되었습니다.

또한 CONFIG_SCHED_DEBUG가 무조건 컴파일되도록 변경되어, 릴리스 커널에서도 /proc/sched_debug와 /sys/kernel/debug/sched/의 EEVDF 진단 인터페이스를 항상 사용할 수 있습니다. sched_ext에는 SCX_OPS_ALLOW_QUEUED_WAKEUP 플래그가 추가되어 BPF 스케줄러가 복잡한 하드웨어 토폴로지에서 ttwu_queue를 선택적으로 활성화할 수 있습니다.

# v6.15+: CONFIG_SCHED_DEBUG 옵션이 제거되어 릴리스 커널에서도 항상 사용 가능
grep -m1 "base_slice" /proc/sched_debug

# sched_ext 이벤트 카운터 (v6.15+, sched_ext 활성화 시)
cat /sys/kernel/sched_ext/root/events 2>/dev/null || echo "sched_ext not active"

Linux 6.17 — Proxy Execution, SMP 전용 컴파일, run-to-parity 정비

Linux 6.17(2025-09)은 EEVDF 인프라 측면에서 세 가지 변화를 포함합니다.

첫째, proxy execution의 초기 구현이 추가되었습니다(CONFIG_SCHED_PROXY_EXEC, EXPERT 의존이며 PREEMPT_RT·SCHED_CLASS_EXT와는 함께 켤 수 없음). 뮤텍스(mutex)를 보유한 태스크가 더 높은 우선순위의 waiter를 막고 있을 때, waiter의 스케줄링 컨텍스트(slice, deadline)를 mutex 보유 태스크에 전파하여 우선순위 역전(priority inversion)을 완화합니다. 현재는 단일 run-queue 구성으로 제한됩니다.

둘째, 단일 프로세서 전용 코드 경로가 제거되어 스케줄러가 SMP 전제로만 컴파일됩니다(sched/smp: Make SMP unconditional). 이에 따라 CONFIG_SMP 분기가 줄고 코드베이스가 단순해졌습니다.

셋째, run-to-parity slice 보호가 Vincent Guittot의 패치 시리즈로 정비되었습니다. 6.16까지는 pick 시점의 deadline을 vlag에 임시로 복사해 두었고, 소스 주석에도 "HACK"으로 표기되어 있었습니다. 6.17에서 sched_entity에 전용 vprot 필드가 추가되고, 보호 구간이 "현재 태스크의 slice 전체"가 아니라 enqueue된 엔티티 중 가장 짧은 slice로 제한되었습니다. 짧은 slice를 가진 태스크가 긴 slice 태스크의 보호 구간 때문에 오래 기다리며 lag가 커지는 문제를 줄이는 변경입니다. RUN_TO_PARITY와 PREEMPT_SHORT는 v6.17에서도 기본 활성(SCHED_FEAT(..., true))입니다.

커밋 (v6.17 머지 윈도우)변경 내용
sched/fair: Use protect_slice() instead of direct comparisondeadline 직접 비교를 protect_slice() 헬퍼로 통일합니다.
sched/fair: Fix NO_RUN_TO_PARITY caseRUN_TO_PARITY가 꺼진 경우에도 같은 slice 보호를 재사용하여 태스크마다 최소 실행 quantum을 보장합니다.
sched/fair: Remove spurious shorter slice preemption보호를 취소해도 wakeup 태스크가 실제로 선택되지 않으면 선점하지 않습니다.
sched/fair: Limit run to parity to the min slice of enqueued entitiesrun-to-parity 보호 구간을 enqueue된 엔티티의 최소 slice로 제한합니다.
sched/fair: Fix entity's lag with run to parity선점 없이 enqueue될 때 보호 구간을 갱신하여 lag가 slice + tick을 넘지 않게 합니다.
sched/fair: Always trigger resched at the end of a protected period보호 구간이 끝나면 eligible 여부와 무관하게 resched를 겁니다.
/* kernel/sched/fair.c — v6.17 set_protect_slice() */
static inline void set_protect_slice(struct cfs_rq *cfs_rq,
                                       struct sched_entity *se)
{
    u64 slice = normalized_sysctl_sched_base_slice;
    u64 vprot = se->deadline;

    if (sched_feat(RUN_TO_PARITY))
        slice = cfs_rq_min_slice(cfs_rq);

    slice = min(slice, se->slice);
    if (slice != se->slice)
        vprot = min_vruntime(vprot, se->vruntime +
                            calc_delta_fair(slice, se));

    se->vprot = vprot;
}
검증 출처: 커밋 목록은 v6.16→v6.17 구간에서 kernel/sched/fair.c를 변경한 커밋을 GitHub compare로 추출한 결과이며, set_protect_slice() 본문은 v6.17 태그 소스와 대조했습니다. 프록시 실행의 단일 run-queue 제한과 SMP 전용 컴파일은 LWN "6.17 Merge window, part 1"에서도 확인됩니다.

Linux 6.19 — NEXT_BUDDY 재도입 및 이후 디스애크, sched_avg 회귀 수정

Linux 6.19(2026-02)는 EEVDF에 두 가지 중요한 변화를 도입했습니다.

첫째, NEXT_BUDDY가 재도입되었습니다. EEVDF 초기 구현(6.6)에서는 CFS의 buddy 메커니즘이 제거되었으나, wakeup 선점 시 waker와 wakee가 캐시를 공유하는 경우 마지막 wakee를 더 빨리 스케줄하면 성능이 향상된다는 분석 결과에 따라 NEXT_BUDDY 로직이 EEVDF에 맞게 재설계되어 복귀했습니다. 일부 producer-consumer 패턴의 대화형 워크로드에서 wakeup latency 개선 효과가 보고되었습니다.

NEXT_BUDDY 디스애크: 그러나 NEXT_BUDDY 재도입 후 MySQL client/server(별도 서버 구성) 환경의 성능 회귀, SPECjbb 피크 메트릭 감소, DayTrader 워크로드 회귀가 보고되었습니다. Peter Zijlstra는 tip/sched/urgent 브랜치를 통해 v6.19 정식 릴리스 전 rc 단계(6.19-rc7)에서 기본 비활성화(patched out)했습니다. root cause 분석이 완료될 때까지 일시적으로 디스애크된 상태이며, 향후 EEVDF에 맞는 wakeup 캐시 친화성 힌트로 재도입될 가능성이 있습니다. 출처: Phoronix, LKML (Mel Gorman patch)

둘째, sched_avg fold의 se_weight() 누락 회귀가 수정되었습니다. 스케줄링 엔티티의 평균 이용률(sched_avg) 접기(fold) 과정에서 weight 인수가 빠진 커밋이 포함되어 Intel Kernel Test Robot이 schbench 99.9th percentile latency 52.4% 회귀를 보고했으며, tip/sched/core 브랜치에 큐잉되었습니다.

# NEXT_BUDDY 효과 확인 — wakeup latency 측정 (v6.19+)
perf sched latency -s max | grep "Average"

# sched_avg 정상 여부 확인 — schbench로 99.9th percentile 검증
schbench -m 2 -t 16 -r 30 -p 99.9

v6.6 ~ v6.19 EEVDF 변경 요약

버전릴리스변경 내용실무 영향
6.62023-10EEVDF 메인라인 머지, sched_latency/min_granularity/wakeup_granularity 제거기존 CFS 튜닝 스크립트 재작성 필요
6.72024-01slice 튜닝·안정화(소스 대조상 fair.c 함수 구성 변화 없음)기존 CFS 튜너블은 이미 제거된 상태
6.8~6.112024-03~09lag 보존·place_entity 보정, cgroup 계층 정합성 수정nested cgroup 공정성 개선
6.122024-11DELAY_DEQUEUE(sched_delayed), PLACE_REL_DEADLINE, min_slice·RB 증강 도입슬립 직전 lag 보존으로 대화형 응답성 개선
6.13~6.152025 상반기resched_curr_lazy() 기반 지연 선점 경로, 선점 판정 세분화처리량과 RT 응답성의 균형 조정
6.162025-07update_min_vruntime() 도입, DL server 헬퍼(fair_server_*), task_is_ineligible_on_dst_cpu(), load-balance 불균형 통계NUMA·이종 코어 환경 판정 정교화
6.172025-09Single RunQueue Proxy Execution, SMP 전용 컴파일, vprot 도입 및 run-to-parity 보호를 최소 slice로 제한우선순위 역전 완화 첫 단계, 짧은 slice 태스크의 lag 폭주 완화
6.182025-11avg_vruntime_add() 제거, sum_w_vruntime 3키 증강, 가중치 변경 시 vprot 재스케일링min_vruntime 선택 정확도·보호 구간 일관성 개선
6.192026-02NEXT_BUDDY 재도입 후 즉시 비활성화(MySQL/SPECjbb/DayTrader 회귀), sched_avg fold의 se_weight() 누락 회귀 수정NEXT_BUDDY 기본 비활성화(향후 재도입 가능성 있음), 서버 배치 성능 회귀 해소 완료
7.02026-04RSEQ 기반 Time Slice Extension 추가(kernel/rseq.c: 최소 5µs·최대 50µs, 기본값은 최소), PREEMPT_LAZY 지원 아키텍처에서 선점 모델 기본값이 PREEMPT_LAZY로 변경(kernel/Kconfig.preempt: PREEMPT_VOLUNTARY는 !ARCH_HAS_PREEMPT_LAZY, PREEMPT_NONE은 ARCH_NO_PREEMPT에서만 선택 가능), 선점 판정이 wakeup_preempt_fair()로 통합EEVDF의 선점 판정 결과를 PREEMPT_LAZY가 유연하게 적용. RSEQ Extension으로 스핀락(Spinlock) 집약 워크로드 선점 문제 완화 가능. PostgreSQL 등에서 PREEMPT_LAZY 단독 사용 시 성능 저하 주의(Huge Pages 활성화 또는 RSEQ 확장 적용 권장)
운영 권장 사항 (2026 기준): 6.6~6.11 커널을 사용 중이라면 가능한 한 6.12 이상으로 업그레이드하는 것을 권장합니다. DELAY_DEQUEUE만으로도 대화형 워크로드 응답성이 개선됩니다. 6.13 이후의 PREEMPT_LAZY는 일반 서버 커널의 기본 선점 모델로 검토할 가치가 있습니다. EEVDF 전환 이후 sched_latency_ns·sched_wakeup_granularity_ns 등 이전 CFS 튜너블은 커널에 존재하지 않으므로, 새 튜닝 스크립트는 이를 전제로 작성하지 않아야 합니다. 6.19에서는 sched_avg fold 회귀가 수정되어 배치 워크로드 성능이 회복되었으나, NEXT_BUDDY 재도입은 MySQL/SPECjbb/DayTrader 성능 회귀로 인해 기본 비활성화되었습니다. 7.0으로 업그레이드 시 PREEMPT_LAZY 지원 아키텍처에서는 PREEMPT_VOLUNTARY 선택지가 사라지고 기본값이 PREEMPT_LAZY가 되므로, PostgreSQL 등 스핀락 집약 워크로드는 Huge Pages 활성화 또는 RSEQ Time Slice Extension 도입을 평가해야 합니다.

Linux 7.0 — RSEQ Time Slice Extension과 EEVDF 선점 상호작용

Linux 7.0에서 병합된 RSEQ Time Slice Extension은 EEVDF의 deadline 기반 선점 결정과 직접 상호작용합니다. EEVDF는 현재 실행 중인 태스크의 virtual deadline이 만료되거나 더 이른 deadline을 가진 eligible 태스크가 나타나면 선점을 요청합니다. 그런데 RSEQ Time Slice Extension이 활성화된 임계 구간에서는 SCHED_NORMAL 태스크에 한해 선점이 일시 억제됩니다. RT/DEADLINE 태스크는 여전히 즉시 선점합니다.

이 메커니즘은 약 10년간 개발된 기능으로, rseq(2) 시스템 콜(System Call)에 슬라이스 확장 요청 인터페이스를 추가합니다. 임계 구간에서 기회적(opportunistic) 선점 방지를 얻는 것이 목적이며, 실제 우선순위 천장(Priority Ceiling) 프로토콜과 달리 보장보다는 확률적 개선을 추구합니다.

커널 7.0부터: RSEQ Time Slice Extension이 추가되었습니다. 기본값은 5µs, 최대 50µs입니다. 이 값을 크게 설정할수록 최소 스케줄링 지연에 영향을 주므로 워크로드에 따라 조정이 필요합니다.

Linux 7.0 PostgreSQL 성능 저하 주의: Linux 7.0 계열에서 PREEMPT_LAZY 중심 구성과 관련된 성능 회귀 사례가 보고되었습니다(AWS Graviton4 환경). 다만 이를 "최신 아키텍처의 일률적 기본값 전환"으로 일반화하기는 어렵고, 실제 영향은 커널 설정과 배포판 정책을 함께 봐야 합니다. PostgreSQL 등 스핀락 기반 워크로드에서 처리량이 이전 커널 대비 약 50% 하락한 사례가 있었고, Huge Pages를 활성화하면 회귀가 재현되지 않았습니다. 장기 해결책은 RSEQ Time Slice Extension을 사용하여 버퍼 풀 스핀락 구간의 선점을 억제하는 것입니다. 출처: Phoronix

향후 개발 방향

영역현재 상태 (v7.0)예상 개선
latency_nice API메인라인 미머지(제안 상태), SCHED_FLAG_LATENCY_NICE 미정의머지 시 cgroup 수준 latency_nice, systemd 통합
pick_eevdf 최적화early termination, v6.19 NEXT_BUDDY 재도입(디스애크됨)더 나은 프루닝, 캐싱; wakeup 캐시 친화성 힌트는 EEVDF 전용 설계 후 재검토 중
NUMA 연동vlag 보존, v6.16 per-NUMA idle cpumaskNUMA 토폴로지(Topology) 인식 deadline
Energy AwarePELT 기반 EAS, v6.16 이종 CPU asymmetric priorityEEVDF deadline 인식 EAS, P/E 코어 완전 통합
sched_ext 연동v7.0 GPU 인식·에너지 인식 추상화 준비, ML 기반 부하 분산(Load Balancing)EEVDF 위에 BPF 확장, cgroup 서브스케줄러 완성
per-task slicesched_base_slice 기반, v6.17 lag·parity 개선, v7.0 RSEQ 확장워크로드 특성 자동 감지, proxy execution 완성
우선순위 역전 방지v6.17 proxy execution 초기 구현(CONFIG_EXPERT). v7.0 RSEQ 기반 기회적 선점 억제mutex 보유 태스크에 waiter 우선순위 전파 완성
선점 모델 통합v7.0에서 최신 아키텍처를 PREEMPT_FULL/LAZY로 집약PREEMPT_VOLUNTARY 단계적 제거, 단일 커널 이진 파일 추구
장기 비전: EEVDF는 CFS의 ad-hoc 휴리스틱을 수학적 기반으로 대체하는 첫 단계입니다. 장기적으로 latency_nice의 cgroup 통합, 워크로드 자동 분류, EAS 연동, RSEQ 기반 임계 구간 보호, 그리고 ML 기반 부하 분산이 함께 진행되면 Linux 스케줄러의 예측 가능성과 튜닝 가능성이 크게 향상될 것입니다.

참고자료

커널 공식 문서

LWN 기사

논문과 학술 자료

커널 커밋과 패치 시리즈

소스 코드 탐색

벤치마크와 릴리스 노트

필수 관련 문서: