프로세스 스케줄러 심화 (Process Scheduler)

Linux 커널 프로세스 스케줄러의 전체 아키텍처를 심층적으로 다룹니다. sched_class 계층 구조, CFS와 EEVDF 알고리즘, 실시간 스케줄링(SCHED_FIFO/RR/DEADLINE), per-CPU 런큐, 로드 밸런싱, BPF 기반 sched_ext, 선점 모델, 그리고 스케줄러 디버깅 기법까지 포괄적으로 분석합니다.

관련 표준: POSIX.1-2017 (스케줄링 정책 SCHED_FIFO/RR/OTHER), IEEE 1003.1b (실시간 확장), Linux SCHED_DEADLINE (CBS/EDF 알고리즘) — 커널 스케줄러가 준수하는 핵심 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 프로세스 관리(task_struct, 프로세스 상태)와 커널 아키텍처(컨텍스트 스위치)를 먼저 읽으세요.
일상 비유: 스케줄러는 놀이공원의 줄 관리 시스템과 같습니다. 놀이기구(CPU)는 한정되어 있고 손님(프로세스)은 많습니다. CFS(Completely Fair Scheduler)는 "모든 손님에게 공평한 대기 시간"을 보장하려 하고, 실시간 스케줄러(SCHED_FIFO)는 "VIP 손님을 항상 먼저" 태웁니다. 선점(preemption)은 "타임아웃! 다음 손님 차례"라고 강제로 교체하는 것입니다.

핵심 요약

  • CFS / EEVDF — 일반 프로세스용 공정 스케줄러. 가상 런타임(vruntime)으로 공평성을 유지합니다.
  • sched_class — 스케줄링 정책의 플러그인 구조. dl → rt → fair → idle 우선순위입니다.
  • 런큐(runqueue) — 각 CPU마다 하나씩 존재하며, 실행 대기 중인 태스크를 관리합니다.
  • 선점(preemption) — 커널이 현재 실행 중인 태스크를 강제로 중단하고 다른 태스크를 실행합니다.
  • 로드 밸런싱 — CPU 간에 태스크를 이동시켜 부하를 균등하게 분배합니다.

단계별 이해

  1. 스케줄러의 역할 — "다음에 어떤 프로세스를 실행할까?"를 결정합니다. schedule() 함수가 핵심입니다.

    타이머 인터럽트마다 현재 태스크의 시간을 업데이트하고 선점 여부를 판단합니다.

  2. CFS 동작 이해 — 각 태스크의 vruntime(가상 실행 시간)을 추적하여, vruntime이 가장 작은 태스크를 다음에 실행합니다.

    Red-Black 트리로 관리되어 O(log N)에 다음 태스크를 선택합니다.

  3. 우선순위 확인nice(-20~19) 값으로 프로세스 우선순위를 조정합니다. 낮을수록 더 많은 CPU 시간을 받습니다.

    ps -eo pid,ni,comm으로 현재 프로세스의 nice 값을 확인할 수 있습니다.

  4. 실시간 스케줄링SCHED_FIFO/SCHED_RR은 일반 태스크보다 항상 우선합니다.

    chrt -f 50 ./app으로 실시간 우선순위를 지정할 수 있습니다.

스케줄러 개요 (Scheduler Overview)

Linux 커널 스케줄러는 시스템의 모든 CPU에서 어떤 태스크를 언제, 얼마나 실행할지 결정하는 핵심 서브시스템입니다. 스케줄러의 주요 목표는 다음과 같습니다:

목표 설명 관련 메트릭
공정성 (Fairness) 모든 태스크가 가중치에 비례하는 CPU 시간을 받도록 보장 vruntime 편차, lag
응답성 (Responsiveness) 대화형 태스크의 스케줄링 지연 최소화 wakeup-to-run latency
처리량 (Throughput) 단위 시간당 최대 작업 완료량 IPC, context switch 빈도
실시간 보장 (RT Guarantee) 실시간 태스크의 데드라인 충족 worst-case latency
에너지 효율 (Energy Efficiency) 불필요한 CPU 활성화 최소화 idle residency, C-state 진입률
확장성 (Scalability) 수천 CPU NUMA 시스템에서도 효율적 동작 lock contention, 밸런싱 오버헤드

커널 스케줄러의 핵심 진입점은 kernel/sched/core.c__schedule() 함수입니다. 이 함수는 현재 태스크를 중단하고 다음에 실행할 태스크를 선택하여 컨텍스트 스위치를 수행합니다.

/* kernel/sched/core.c — 스케줄러 핵심 진입점 (간략화) */
static void __sched notrace __schedule(unsigned int sched_mode)
{
    struct task_struct *prev, *next;
    struct rq_flags rf;
    struct rq *rq;
    int cpu;

    cpu = smp_processor_id();
    rq  = cpu_rq(cpu);           /* per-CPU 런큐 획득 */
    prev = rq->curr;               /* 현재 실행 중인 태스크 */

    rq_lock(rq, &rf);

    /* 이전 태스크의 상태 갱신 (dequeue 여부 결정) */
    if (!(sched_mode & SM_MASK_PREEMPT) && prev_state) {
        if (signal_pending_state(prev_state, prev)) {
            WRITE_ONCE(prev->__state, TASK_RUNNING);
        } else {
            prev->sched_contributes_to_load =
                (prev_state & TASK_UNINTERRUPTIBLE) &&
                !(prev_state & TASK_NOLOAD);
            deactivate_task(rq, prev, DEQUEUE_SLEEP);
        }
    }

    /* 다음 태스크 선택 — sched_class 계층을 순회 */
    next = pick_next_task(rq, prev, &rf);

    if (likely(prev != next)) {
        rq->nr_switches++;
        rq_set_curr(rq, next);

        /* 컨텍스트 스위치 수행 */
        rq = context_switch(rq, prev, next, &rf);
    }

    rq_unlock_irq(rq, &rf);
    balance_callback(rq);
}
스케줄링 트리거: __schedule()은 다음 경로들에서 호출됩니다: (1) 태스크가 자발적으로 슬립할 때 (schedule()), (2) 인터럽트/시스템 콜 복귀 시 TIF_NEED_RESCHED 플래그가 설정된 경우, (3) cond_resched() 호출 시 선점 필요한 경우, (4) 선점형 커널에서 선점 카운터가 0이 될 때.

sched_class 계층 구조

Linux 스케줄러는 모듈화된 클래스 기반 설계를 채택합니다. 각 스케줄링 정책은 struct sched_class를 구현하며, 우선순위가 높은 클래스부터 순서대로 탐색합니다.

/* include/linux/sched.h — sched_class 인터페이스 (주요 콜백) */
struct sched_class {
    /* 태스크를 런큐에 삽입/제거 */
    void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags);
    void (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags);

    /* 다음에 실행할 태스크 선택 */
    struct task_struct *(*pick_next_task)(struct rq *rq);

    /* 현재 태스크가 선점되어야 하는지 검사 */
    void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);

    /* 태스크가 CPU를 양보할 때 */
    void (*put_prev_task)(struct rq *rq, struct task_struct *p);

    /* 주기적 타이머 틱 처리 */
    void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);

    /* 태스크 웨이크업 시 CPU 선택 */
    int (*select_task_rq)(struct task_struct *p, int prev_cpu, int wake_flags);

    /* 로드 밸런싱 관련 */
    int  (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
    void (*task_woken)(struct rq *rq, struct task_struct *p);
};

클래스 우선순위 체인

스케줄러가 pick_next_task()를 호출하면, 가장 높은 우선순위의 클래스부터 순차적으로 실행 가능한 태스크를 찾습니다.

우선순위 sched_class 소스 파일 용도 정책
1 (최고) stop_sched_class kernel/sched/stop_task.c CPU 핫플러그, active migration 내부 전용 (사용자 설정 불가)
2 dl_sched_class kernel/sched/deadline.c 하드 실시간 태스크 SCHED_DEADLINE (EDF/CBS)
3 rt_sched_class kernel/sched/rt.c 소프트 실시간 태스크 SCHED_FIFO, SCHED_RR
4 fair_sched_class kernel/sched/fair.c 일반 태스크 (대부분의 프로세스) SCHED_NORMAL, SCHED_BATCH
5 (최저) idle_sched_class kernel/sched/idle.c CPU idle 태스크 SCHED_IDLE (per-CPU swapper)
/* kernel/sched/core.c — pick_next_task 최적 경로 */
static inline struct task_struct *
__pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
    const struct sched_class *class;
    struct task_struct *p;

    /*
     * 최적화: 런큐의 모든 태스크가 fair 클래스이면
     * (nr_running == cfs_rq.h_nr_running) fair만 검사
     */
    if (likely(rq->nr_running == rq->cfs.h_nr_running)) {
        p = pick_next_task_fair(rq, prev, rf);
        if (unlikely(p == RETRY_TASK))
            goto restart;
        return p;
    }

restart:
    put_prev_task_balance(rq, prev, rf);

    /* 우선순위 높은 클래스부터 순회 */
    for_each_class(class) {
        p = class->pick_next_task(rq);
        if (p)
            return p;
    }

    /* idle 클래스는 항상 태스크를 반환 (per-CPU idle thread) */
    BUG();
}
for_each_class 매크로: 이 매크로는 stop_sched_classdl_sched_classrt_sched_classfair_sched_classidle_sched_class 순서로 순회합니다. 각 클래스의 pick_next_task()가 NULL을 반환하면 다음 클래스로 넘어갑니다.

CFS (Completely Fair Scheduler)

CFS는 커널 2.6.23에서 Ingo Molnar가 도입한 스케줄러로, 이상적인 멀티태스킹 프로세서(Ideal Multi-Tasking CPU)를 근사(approximate)합니다. 이상적인 프로세서에서는 N개의 태스크가 각각 1/N의 CPU 시간을 동시에 받지만, 실제 하드웨어에서는 한 CPU에 하나의 태스크만 실행할 수 있으므로, CFS는 vruntime(가상 실행 시간)을 사용하여 공정성을 추적합니다.

vruntime (가상 실행 시간)

vruntime은 태스크가 실제로 소비한 CPU 시간을 가중치로 보정한 값입니다. nice 값이 낮은(우선순위 높은) 태스크는 vruntime이 느리게 증가하고, nice 값이 높은(우선순위 낮은) 태스크는 빠르게 증가합니다.

/* kernel/sched/fair.c — vruntime 갱신 (CFS, v6.5 이전) */
static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    u64 now = rq_clock_task(rq_of(cfs_rq));
    u64 delta_exec;

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

    /*
     * vruntime 계산:
     * delta_vruntime = delta_exec * (NICE_0_LOAD / weight)
     *
     * nice 0 (weight 1024): delta_vruntime = delta_exec * 1.0
     * nice -5 (weight 3121): delta_vruntime = delta_exec * 0.328
     * nice 5 (weight 335):  delta_vruntime = delta_exec * 3.057
     */
    curr->vruntime += calc_delta_fair(delta_exec, curr);

    /* 런큐의 min_vruntime 갱신 */
    update_min_vruntime(cfs_rq);
}

Red-Black Tree 기반 태스크 관리

CFS는 모든 실행 가능한(runnable) 태스크를 vruntime을 키로 하는 Red-Black Tree에 관리합니다. 트리의 가장 왼쪽(최소 vruntime) 노드가 다음 실행 대상입니다. 이 구조로 삽입/삭제/탐색이 모두 O(log N)이며, 가장 왼쪽 노드는 rb_leftmost에 캐싱되어 O(1) 접근이 가능합니다.

/* include/linux/sched.h — CFS 런큐 구조 */
struct cfs_rq {
    struct load_weight  load;         /* 런큐 전체 가중치 합 */
    unsigned int        nr_running;   /* 실행 가능 태스크 수 */
    u64                 min_vruntime; /* 런큐 내 최소 vruntime */

    struct rb_root_cached tasks_timeline; /* RB-Tree (leftmost 캐싱) */

    struct sched_entity *curr;  /* 현재 실행 중인 엔티티 */
    struct sched_entity *next;  /* 다음 실행 힌트 */
    struct sched_entity *skip;  /* 건너뛸 엔티티 (yield) */
};

/* include/linux/sched.h — 스케줄 엔티티 */
struct sched_entity {
    struct load_weight  load;       /* nice에 따른 가중치 */
    struct rb_node      run_node;   /* RB-Tree 노드 */
    unsigned int        on_rq;      /* 런큐에 있는지 여부 */

    u64  exec_start;                 /* 현재 실행 시작 시각 */
    u64  sum_exec_runtime;            /* 누적 실행 시간 */
    u64  vruntime;                    /* 가상 실행 시간 */
    u64  prev_sum_exec_runtime;       /* 이전 누적값 (preempt 체크용) */
};

타임 슬라이스 계산

CFS는 고정 타임 슬라이스를 사용하지 않습니다. 대신, 타겟 레이턴시(sched_latency_ns)를 런큐의 태스크 수로 나누고 가중치를 반영합니다.

/*
 * 타임 슬라이스 계산 공식:
 *
 *                  sched_latency_ns * weight_i
 * timeslice_i = ─────────────────────────────────
 *                     total_weight
 *
 * 예: sched_latency = 6ms, 태스크 A(nice 0, w=1024), B(nice 5, w=335)
 *
 * A의 슬라이스 = 6ms * 1024 / (1024+335) = 4.52ms
 * B의 슬라이스 = 6ms * 335  / (1024+335) = 1.48ms
 *
 * 태스크가 nr_latency(기본 8)개를 초과하면:
 * sched_min_granularity_ns(기본 0.75ms) * nr_running 사용
 */

/* kernel/sched/fair.c — sysctl 기본값 */
unsigned int sysctl_sched_latency            = 6000000ULL; /* 6ms */
unsigned int sysctl_sched_min_granularity     = 750000ULL;  /* 0.75ms */
unsigned int sysctl_sched_wakeup_granularity  = 1000000ULL; /* 1ms */
CFS의 한계: CFS는 v6.6에서 EEVDF로 대체되었습니다. CFS의 주요 문제점은: (1) 새로 깨어난(wakeup) 태스크에 대한 과도한 보너스로 인한 불공정, (2) next/last 힌트 기반의 임시방편적 wakeup 선점 로직, (3) sched_latency 보장이 확률적(best-effort)이라는 점이었습니다.

EEVDF (Earliest Eligible Virtual Deadline First)

EEVDF는 커널 v6.6에서 Peter Zijlstra에 의해 CFS를 대체한 fair 스케줄링 알고리즘입니다. 1995년 Stoica와 Abdel-Wahab이 제안한 이론적 모델을 기반으로 하며, CFS의 단순한 "최소 vruntime" 선택 대신 적격 시각(eligible time)가상 데드라인(virtual deadline)을 사용하여 지연 시간 보장을 강화합니다.

핵심 개념

개념 기호 설명
가상 시간 (Virtual Time) V(t) 이상적 프로세서에서 각 태스크가 받았어야 할 시간의 기준점
적격 시각 (Eligible Time) ei 태스크 i가 실행 권리를 갖는 가상 시각. lag ≥ 0이면 적격
가상 데드라인 (Virtual Deadline) di ei + (요청 슬라이스 / 가중치). 작을수록 긴급
래그 (Lag) lagi 이상적 서비스량 - 실제 서비스량. 양수면 서비스 부족(eligible)
요청 슬라이스 (Request/Slice) ri 태스크가 요청한 실행 시간 단위 (커널의 slice 필드)

알고리즘 동작 원리

EEVDF의 태스크 선택 과정은 두 단계입니다:

  1. 적격성(Eligibility) 필터링: vruntime ≤ V(t) (즉, lag ≥ 0)인 태스크만 후보로 선별
  2. 데드라인 기반 선택: 적격 태스크 중 virtual deadline이 가장 빠른 태스크를 선택
/* kernel/sched/fair.c — EEVDF pick_eevdf() (v6.6+, 간략화) */
static struct sched_entity *
pick_eevdf(struct cfs_rq *cfs_rq)
{
    struct rb_node *node = cfs_rq->tasks_timeline.rb_root.rb_node;
    struct sched_entity *best = NULL;

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

        /*
         * 적격성 검사: entity의 vruntime이 cfs_rq의 avg_vruntime 이하인지
         * (lag >= 0 ⟺ vruntime <= V(t) ⟺ eligible)
         */
        if (entity_eligible(cfs_rq, se)) {
            /* 적격 태스크 중 deadline이 가장 빠른 것 선택 */
            if (!best || deadline_gt(deadline, best, se))
                best = se;
            node = node->rb_left;   /* 더 빠른 vruntime (eligible) 탐색 */
        } else {
            node = node->rb_right;  /* 아직 eligible 아님 → 오른쪽 */
        }
    }

    return best;
}

/* 적격성 판단 (O(1)) */
static int entity_eligible(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    /*
     * avg_vruntime은 런큐의 가중 평균 vruntime = V(t)
     * se->vruntime <= V(t) 이면 eligible (서비스 부족 상태)
     */
    return vruntime_eligible(cfs_rq, se->vruntime);
}

슬라이스와 데드라인

EEVDF에서 각 태스크의 가상 데드라인은 eligible time + slice / weight로 계산됩니다. slice는 태스크가 한 번에 요청하는 실행 시간 단위입니다.

/* kernel/sched/fair.c — 데드라인 갱신 */
static void update_deadline(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    if ((s64)(se->vruntime - se->deadline) >= 0) {
        /*
         * 현재 슬라이스 소진 → 새 데드라인 설정
         * deadline = vruntime + calc_delta_fair(slice, se)
         *
         * calc_delta_fair: slice를 weight로 보정
         * nice 0: deadline = vruntime + slice
         * nice -5: deadline = vruntime + slice * 0.328
         */
        se->deadline = se->vruntime +
            calc_delta_fair(se->slice, se);

        /* 선점 검사: 새 태스크의 deadline이 현재보다 빠르면 resched */
        if (cfs_rq->nr_running > 1)
            resched_curr(rq_of(cfs_rq));
    }
}
EEVDF vs CFS 핵심 차이: CFS는 "가장 적게 실행된 태스크"를 무조건 선택하지만, EEVDF는 "서비스가 부족하면서(eligible) 데드라인이 가장 빠른 태스크"를 선택합니다. 이로써 short-sleep 태스크(대화형)가 깨어나면 낮은 vruntime 덕분에 즉시 eligible이 되고, 짧은 slice로 인해 deadline이 빠르므로 우선 선택됩니다. 반면 CPU-bound 태스크는 긴 slice를 사용하여 컨텍스트 스위치를 줄입니다.

avg_vruntime (가중 평균 가상 시간)

EEVDF는 런큐의 "현재 가상 시간" V(t)를 추적하기 위해 avg_vruntime을 유지합니다. 이는 모든 실행 가능한 엔티티의 vruntime 가중 평균입니다.

/* kernel/sched/fair.c — 가중 평균 vruntime 계산 */
static u64 avg_vruntime(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    s64 avg = cfs_rq->avg_vruntime;
    long load = cfs_rq->avg_load;

    if (curr && curr->on_rq) {
        unsigned long weight = scale_load_down(curr->load.weight);
        avg += entity_key(cfs_rq, curr) * weight;
        load += weight;
    }

    if (load)
        avg = div_s64(avg, load);

    return cfs_rq->min_vruntime + avg;
}

실시간 스케줄링 (Real-Time Scheduling)

Linux는 POSIX.1b 실시간 스케줄링 정책을 지원합니다. RT 태스크는 fair 클래스보다 항상 높은 우선순위를 가지므로, RT 태스크가 실행 가능한 한 일반 태스크는 CPU를 받지 못합니다.

RT 정책: SCHED_FIFO vs SCHED_RR

속성 SCHED_FIFO SCHED_RR
타임 슬라이스 무한 (자발적 양보까지 계속 실행) 고정 (기본 100ms, sched_rr_timeslice_ms)
같은 우선순위 내 동작 FIFO 순서, 선점 없음 라운드 로빈, 슬라이스 소진 시 큐 뒤로
우선순위 범위 1 ~ 99 (99가 최고) 1 ~ 99 (99가 최고)
선점 더 높은 우선순위 RT에 의해서만 더 높은 우선순위 + 슬라이스 만료
용도 지연 민감 제어 루프 균등 시분할이 필요한 RT 태스크
/* RT 스케줄링 설정 예제 (사용자 공간) */
#include <sched.h>

struct sched_param param;
param.sched_priority = 80;  /* 1-99, 높을수록 높은 우선순위 */

/* SCHED_FIFO 설정 */
sched_setscheduler(pid, SCHED_FIFO, ¶m);

/* SCHED_RR 설정 (타임 슬라이스 포함) */
sched_setscheduler(pid, SCHED_RR, ¶m);

/* 현재 RR 타임 슬라이스 확인 */
struct timespec ts;
sched_rr_get_interval(pid, &ts);  /* 기본 100ms */

RT 스케줄러 내부 구현

RT 스케줄러는 우선순위별 연결 리스트 배열을 사용합니다. 비트맵으로 비어 있지 않은 우선순위를 O(1)에 찾습니다.

/* kernel/sched/rt.c — RT 런큐 구조 */
struct rt_prio_array {
    DECLARE_BITMAP(bitmap, MAX_RT_PRIO + 1); /* 100비트 비트맵 */
    struct list_head queue[MAX_RT_PRIO];     /* 우선순위별 리스트 */
};

struct rt_rq {
    struct rt_prio_array  active;        /* 활성 태스크 배열 */
    unsigned int         rt_nr_running; /* RT 태스크 수 */
    unsigned int         rr_nr_running; /* SCHED_RR 태스크 수 */
    struct {
        int  curr;         /* 현재 최고 우선순위 */
        int  next;         /* 다음 최고 우선순위 */
    } highest_prio;
};

/* pick_next_task_rt — O(1) 최고 우선순위 태스크 선택 */
static struct task_struct *
pick_next_task_rt(struct rq *rq)
{
    struct rt_rq *rt_rq = &rq->rt;
    struct rt_prio_array *array = &rt_rq->active;
    int idx;

    idx = sched_find_first_bit(array->bitmap); /* 비트맵에서 최고 우선순위 */
    if (idx >= MAX_RT_PRIO)
        return NULL;

    return list_first_entry(&array->queue[idx],
                             struct sched_rt_entity, run_list);
}

RT 쓰로틀링 (RT Throttling)

RT 태스크가 CPU를 독점하여 일반 태스크가 기아(starvation) 상태에 빠지는 것을 방지하기 위해, 커널은 RT 대역폭 제한(RT bandwidth throttling)을 적용합니다.

# RT 쓰로틀링 기본값 확인
cat /proc/sys/kernel/sched_rt_period_us   # 1000000 (1초)
cat /proc/sys/kernel/sched_rt_runtime_us  # 950000 (950ms)

# 의미: 1초 주기 중 RT 태스크는 최대 950ms만 실행 가능
# 나머지 50ms는 일반(fair) 태스크에 할당

# RT 쓰로틀링 비활성화 (위험! 프로덕션에서 사용 금지)
echo -1 > /proc/sys/kernel/sched_rt_runtime_us
주의: RT 쓰로틀링을 비활성화하면(sched_rt_runtime_us = -1), 무한 루프 버그가 있는 RT 태스크가 시스템을 완전히 멈출 수 있습니다. 반드시 개발/테스트 환경에서만 사용하고, 프로덕션 시스템에서는 적절한 RT 대역폭 제한을 유지하십시오.

SCHED_DEADLINE

SCHED_DEADLINE은 커널 3.14에서 도입된 CBS(Constant Bandwidth Server) + EDF(Earliest Deadline First) 기반 스케줄링 정책입니다. RT 클래스보다도 높은 우선순위를 가지며, 태스크에 명시적인 실행 예산(runtime), 주기(period), 데드라인(deadline)을 할당합니다.

SCHED_DEADLINE 파라미터

파라미터 필드 설명
Runtime (Q) sched_runtime 한 주기 내 최대 실행 시간 (ns)
Deadline (D) sched_deadline 주기 시작부터 runtime을 소진해야 하는 시한 (ns)
Period (T) sched_period 태스크 활성화 주기 (ns). D ≤ T 필수
/* SCHED_DEADLINE 설정 예제 (사용자 공간) */
#include <sched.h>
#include <linux/sched/types.h>

struct sched_attr attr = {
    .size           = sizeof(attr),
    .sched_policy   = SCHED_DEADLINE,
    .sched_runtime  = 10 * 1000 * 1000,   /* 10ms 실행 예산 */
    .sched_deadline = 30 * 1000 * 1000,   /* 30ms 데드라인 */
    .sched_period   = 50 * 1000 * 1000,   /* 50ms 주기 */
};

/* sched_setattr 시스템 콜 사용 (glibc 래퍼 없음 → syscall 직접) */
syscall(SYS_sched_setattr, pid, &attr, 0);

/*
 * 대역폭 비율: Q/T = 10ms/50ms = 20%
 * 즉, 이 태스크는 CPU의 20%를 보장받으며,
 * 매 주기 시작 후 30ms 이내에 10ms를 실행
 */

CBS (Constant Bandwidth Server)

CBS는 각 DEADLINE 태스크를 독립적인 "서버"로 취급하여, 한 태스크의 과도한 실행이 다른 태스크에 영향을 미치지 않도록 격리합니다.

/* kernel/sched/deadline.c — CBS 서버 보충 로직 (간략화) */
static void replenish_dl_entity(struct sched_dl_entity *dl_se)
{
    /*
     * 새 주기 시작 → 실행 예산(runtime) 보충
     * deadline = 현재시각 + relative_deadline
     */
    dl_se->runtime  = dl_se->dl_runtime;   /* Q 보충 */
    dl_se->deadline = rq_clock(rq) + dl_se->dl_deadline;

    /*
     * 과거 deadline이 아직 유효하면 (서버가 idle 상태였으면)
     * deadline을 현재 기준으로 리셋하여 다른 태스크에 피해 방지
     */
    if (dl_time_before(dl_se->deadline, rq_clock(rq))) {
        dl_se->deadline = rq_clock(rq) + dl_se->dl_deadline;
        dl_se->runtime  = dl_se->dl_runtime;
    }
}

/* EDF 기반 태스크 선택: deadline이 가장 빠른 태스크 */
static struct task_struct *
pick_next_task_dl(struct rq *rq)
{
    struct dl_rq *dl_rq = &rq->dl;
    struct rb_node *left = rb_first_cached(&dl_rq->root);

    if (!left)
        return NULL;

    return dl_task_of(__node_2_dle(left));  /* 최소 deadline */
}

입장 제어 (Admission Control)

SCHED_DEADLINE은 시스템의 총 대역폭이 초과되지 않도록 입장 제어를 수행합니다.

/*
 * 입장 제어 조건 (간략화):
 *
 * 모든 DEADLINE 태스크 i에 대해:
 *   Σ(Q_i / T_i) <= total_bw
 *
 * total_bw = 각 CPU의 (sched_rt_runtime / sched_rt_period)
 * 기본값: 950ms / 1000ms = 0.95 (95%)
 *
 * 즉, 새 DEADLINE 태스크 추가 시
 * 기존 대역폭 합 + 새 대역폭이 95%를 초과하면
 * sched_setattr()은 -EBUSY를 반환
 */
SCHED_DEADLINE vs RT: DEADLINE은 CBS로 대역폭을 격리하므로, 한 태스크의 과도한 실행이 다른 태스크의 데드라인을 침해하지 않습니다. RT(FIFO/RR)는 고정 우선순위 기반이라 우선순위 역전과 기아 문제에 취약합니다. 하드 실시간 요구사항이 있다면 SCHED_DEADLINE이 적합합니다.

런큐 (Per-CPU Runqueue)

각 CPU에는 고유한 런큐(struct rq)가 있습니다. 이는 스케줄러의 핵심 자료구조로, 해당 CPU에서 실행 가능한 모든 태스크를 관리합니다. per-CPU 설계 덕분에 대부분의 스케줄링 결정이 로컬 락만으로 가능합니다.

/* kernel/sched/sched.h — per-CPU 런큐 구조 (주요 필드) */
struct rq {
    /* --- 글로벌 상태 --- */
    raw_spinlock_t  __lock;        /* 런큐 락 */
    unsigned int    nr_running;    /* 총 실행 가능 태스크 수 */
    unsigned int    nr_switches;   /* 컨텍스트 스위치 카운터 */

    /* --- 현재 실행 중 --- */
    struct task_struct  *curr;     /* 현재 태스크 */
    struct task_struct  *idle;     /* per-CPU idle 태스크 */
    struct task_struct  *stop;     /* stop 태스크 (migration) */

    /* --- 클래스별 서브 런큐 --- */
    struct cfs_rq   cfs;           /* CFS/EEVDF 런큐 */
    struct rt_rq    rt;            /* RT 런큐 */
    struct dl_rq    dl;            /* DEADLINE 런큐 */

    /* --- 시간 관리 --- */
    u64             clock;         /* 런큐 시계 (ns) */
    u64             clock_task;    /* irq 시간 제외 태스크 시계 */

    /* --- 로드 밸런싱 --- */
    struct sched_domain *sd;       /* 스케줄링 도메인 */
    unsigned long   cpu_capacity;  /* CPU 용량 (freq 반영) */

    /* --- 선점/리스케줄 --- */
    int             skip_clock_update; /* 시계 갱신 건너뛰기 */

    unsigned long   nr_uninterruptible; /* load avg 보정용 */

    /* --- NUMA 밸런싱 --- */
    unsigned int    nr_preferred_running; /* NUMA 선호 CPU 태스크 */

    struct cpu_stop_work active_balance_work; /* 능동 밸런싱 */
    int             active_balance;
    int             push_cpu;
};

컨텍스트 스위치 경로

컨텍스트 스위치는 현재 태스크의 CPU 상태(레지스터, 스택 포인터)를 저장하고 다음 태스크의 상태를 복원하는 과정입니다.

/* kernel/sched/core.c — 컨텍스트 스위치 (간략화) */
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
               struct task_struct *next, struct rq_flags *rf)
{
    /* 1단계: 메모리 컨텍스트 전환 (mm_struct) */
    if (!next->mm) {
        /* 커널 스레드 → 이전 태스크의 mm 빌려씀 (lazy TLB) */
        enter_lazy_tlb(prev->active_mm, next);
        next->active_mm = prev->active_mm;
    } else {
        /* 사용자 프로세스 → 페이지 테이블 전환 */
        switch_mm_irqs_off(prev->active_mm, next->mm, next);
    }

    /* 2단계: CPU 레지스터/스택 전환 (아키텍처 의존) */
    switch_to(prev, next, prev);

    /* === 이 시점에서 CPU는 'next' 태스크를 실행 중 === */

    return finish_task_switch(prev);
}
switch_to 매크로: switch_to(prev, next, last)는 아키텍처별로 구현됩니다. x86에서는 RSP(스택 포인터)를 교체하고 RIP(명령어 포인터)를 __switch_to_asm에서 전환합니다. 세 번째 인자 last는 스위치 후 "이전에 실행 중이던 태스크"를 반환받기 위한 것으로, A→B→A 순환에서 A가 복귀했을 때 B를 정리하기 위해 필요합니다.

로드 밸런싱 (Load Balancing)

SMP 시스템에서 CPU 간 부하를 균등하게 분배하는 것은 성능의 핵심입니다. Linux 스케줄러는 sched_domain 계층 구조를 통해 토폴로지 인식 로드 밸런싱을 수행합니다.

sched_domain 계층 구조

스케줄링 도메인은 CPU의 물리적 토폴로지를 반영합니다. SMT(하이퍼스레딩), MC(멀티코어), NUMA 노드 수준으로 계층화됩니다.

예: 2-NUMA-node, 각 4코어 8스레드 시스템

NUMA Domain (SD_NUMA)
├── Node 0: MC Domain (SD_MC)
│   ├── Core 0: SMT Domain (SD_SMT)
│   │   ├── CPU 0 (thread 0)
│   │   └── CPU 1 (thread 1)
│   ├── Core 1: SMT Domain
│   │   ├── CPU 2
│   │   └── CPU 3
│   ├── Core 2: SMT Domain
│   │   ├── CPU 4
│   │   └── CPU 5
│   └── Core 3: SMT Domain
│       ├── CPU 6
│       └── CPU 7
└── Node 1: MC Domain (SD_MC)
    ├── Core 4: SMT Domain
    │   ├── CPU 8
    │   └── CPU 9
    ├── Core 5: SMT Domain
    │   ├── CPU 10
    │   └── CPU 11
    ├── Core 6: SMT Domain
    │   ├── CPU 12
    │   └── CPU 13
    └── Core 7: SMT Domain
        ├── CPU 14
        └── CPU 15

로드 밸런싱 알고리즘

로드 밸런싱은 주기적으로(sched_balance_softirq) 또는 idle CPU가 작업을 찾을 때(idle balancing) 트리거됩니다.

/* kernel/sched/fair.c — 로드 밸런싱 핵심 흐름 (간략화) */
static int sched_balance_rq(struct rq *this_rq,
                              struct sched_domain *sd,
                              enum cpu_idle_type idle)
{
    struct lb_env env = {
        .sd         = sd,
        .dst_cpu    = this_rq->cpu,
        .dst_rq     = this_rq,
        .idle       = idle,
    };

    /* 1단계: 가장 바쁜 그룹 찾기 */
    struct sched_group *busiest_group =
        find_busiest_group(&env, &sds);
    if (!busiest_group)
        goto out_balanced;

    /* 2단계: 그룹 내 가장 바쁜 런큐 찾기 */
    struct rq *busiest =
        find_busiest_queue(&env, busiest_group);
    if (!busiest)
        goto out_balanced;

    /* 3단계: 태스크 이주 (busiest → this_rq) */
    detach_tasks(&env);
    if (env.imbalance && !env.loop_break) {
        attach_tasks(&env);
    }

    return 0;

out_balanced:
    return 0;
}

밸런싱 메트릭

메트릭 설명 용도
cpu_load PELT 기반 CPU 부하 (지수 감쇠 평균) idle balancing 판단
runnable_avg 태스크의 실행 가능 비율 (0~1024) 태스크 가중치 산출
util_avg 태스크의 실제 CPU 활용도 (0~1024) CPU 용량 대비 활용도 비교
nr_running 런큐의 실행 가능 태스크 수 불균형 감지
cpu_capacity CPU 용량 (주파수, 열 제한 반영) 비대칭(big.LITTLE) 밸런싱

CPU 마이그레이션

태스크가 한 CPU에서 다른 CPU로 이주하는 경로는 크게 세 가지입니다:

/*
 * 1. Pull migration (주기적 밸런싱)
 *    - 바쁜 CPU에서 idle CPU로 태스크를 "당겨옴"
 *    - softirq 컨텍스트에서 실행
 *    - sched_balance_rq() → detach_tasks() → attach_tasks()
 *
 * 2. Push migration (RT/DL 전용)
 *    - 높은 우선순위 태스크가 깨어나면 적합한 CPU로 "밀어냄"
 *    - push_rt_task() / push_dl_task()
 *
 * 3. Wake-up migration
 *    - 태스크가 깨어날 때 select_task_rq()에서 최적 CPU 선택
 *    - 캐시 친화성 vs 부하 분산 트레이드오프
 */

/* kernel/sched/fair.c — select_task_rq_fair (웨이크업 CPU 선택, 간략화) */
static int
select_task_rq_fair(struct task_struct *p, int prev_cpu, int wake_flags)
{
    struct sched_domain *sd;
    int new_cpu = prev_cpu;
    int want_affine = 0;

    /* 깨운 CPU가 prev_cpu와 같은 도메인이면 affinity 선호 */
    if (cpumask_test_cpu(smp_processor_id(), p->cpus_ptr))
        want_affine = 1;

    /* 도메인 계층을 올라가며 가장 idle한 CPU/그룹 선택 */
    for_each_domain(smp_processor_id(), sd) {
        if (want_affine && (sd->flags & SD_WAKE_AFFINE)) {
            new_cpu = select_idle_sibling(p, prev_cpu, new_cpu);
            break;
        }
    }

    return new_cpu;
}
PELT (Per-Entity Load Tracking): 커널 3.8에서 도입된 PELT는 각 스케줄 엔티티(태스크, cgroup)의 부하를 개별적으로 추적합니다. 1024us 윈도우를 기준으로 지수 감쇠(y = 1/2^(32/1024))를 적용하여, 최근 부하에 더 높은 가중치를 부여합니다. 이는 CPU freq 거버너(schedutil)와 로드 밸런싱 모두에 사용됩니다.

sched_ext (BPF 기반 확장 스케줄링, v6.12+)

sched_ext는 커널 v6.12에서 도입된 BPF 기반 확장 가능 스케줄링 프레임워크입니다. 커널을 재컴파일하지 않고도 BPF 프로그램을 통해 스케줄링 정책을 커스터마이징할 수 있습니다. ext_sched_class는 fair와 idle 사이에 위치합니다.

아키텍처

/* sched_ext 클래스 위치 (우선순위 순) */
/*
 * stop_sched_class     (최고)
 * dl_sched_class
 * rt_sched_class
 * fair_sched_class
 * ext_sched_class      ← sched_ext (v6.12+)
 * idle_sched_class     (최저)
 */

/* include/linux/sched/ext.h — sched_ext_ops 구조체 (주요 콜백) */
struct sched_ext_ops {
    /* 태스크가 런큐에 삽입될 때 */
    void (*enqueue)(struct task_struct *p, u64 enq_flags);

    /* 태스크가 런큐에서 제거될 때 */
    void (*dequeue)(struct task_struct *p, u64 deq_flags);

    /* 다음 실행 태스크 선택 (핵심 콜백) */
    struct task_struct *(*dispatch)(s32 cpu, struct task_struct *prev);

    /* 태스크가 실행을 시작/종료할 때 */
    void (*running)(struct task_struct *p);
    void (*stopping)(struct task_struct *p, bool runnable);

    /* CPU 선택 (wakeup 시) */
    s32 (*select_cpu)(struct task_struct *p, s32 prev_cpu, u64 wake_flags);

    /* 초기화/종료 */
    s32 (*init)(void);
    void (*exit)(struct scx_exit_info *info);

    /* 스케줄러 이름 */
    char name[SCX_OPS_NAME_LEN];
};

sched_ext BPF 스케줄러 예제

/* 간단한 sched_ext BPF 스케줄러 예제 (scx_simple) */
#include <scx/common.bpf.h>

char _license[] SEC("license") = "GPL";

/* 글로벌 FIFO 디스패치 큐 사용 */
s32 BPF_STRUCT_OPS(simple_select_cpu,
                    struct task_struct *p,
                    s32 prev_cpu, u64 wake_flags)
{
    bool is_idle = false;
    s32 cpu;

    /* idle CPU가 있으면 직접 디스패치 (fast path) */
    cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &is_idle);
    if (is_idle) {
        scx_bpf_dispatch(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);
    }
    return cpu;
}

void BPF_STRUCT_OPS(simple_enqueue,
                    struct task_struct *p, u64 enq_flags)
{
    /* 글로벌 FIFO 큐에 삽입 */
    scx_bpf_dispatch(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
}

SEC(".struct_ops.link")
struct sched_ext_ops simple_ops = {
    .select_cpu = (void *)simple_select_cpu,
    .enqueue    = (void *)simple_enqueue,
    .name       = "simple",
};
# sched_ext 스케줄러 로드/관리
# scx_simple 스케줄러 로드 (커널 tools/sched_ext/에 포함)
sudo ./scx_simple

# 현재 활성 sched_ext 스케줄러 확인
cat /sys/kernel/sched_ext/root/ops

# 스케줄러 통계
cat /sys/kernel/sched_ext/root/stats

# sched_ext 비활성화 (fair로 복귀)
# → 스케줄러 프로세스를 종료하면 자동으로 fair 복귀
sched_ext 활용 사례: (1) scx_rusty: Rust로 작성된 NUMA-aware 스케줄러, (2) scx_lavd: 게이밍 워크로드 최적화, (3) scx_bpfland: 대화형 워크로드 최적화. Meta, Google 등에서 프로덕션 워크로드에 맞춤형 스케줄러를 실험하는 데 활용하고 있습니다.

선점 모델 (Preemption Models)

Linux 커널의 선점(preemption)은 실행 중인 커널 코드를 중단하고 더 높은 우선순위의 태스크로 전환할 수 있는 메커니즘입니다. 유저 공간에서 실행 중인 프로세스는 항상 선점 가능하지만, 커널 공간에서의 선점 가능 여부는 커널 설정에 따라 달라집니다. 선점 모델은 응답성(latency)과 처리량(throughput) 사이의 근본적인 트레이드오프를 결정하는 핵심 설계 선택입니다.

유저 선점 vs 커널 선점: 유저 선점(User Preemption)은 모든 선점 모델에서 동일하게 동작합니다 — 시스템 콜 복귀 또는 인터럽트 복귀 시 TIF_NEED_RESCHED가 설정되어 있으면 schedule()이 호출됩니다. 선점 모델 간의 차이는 오직 커널 선점(Kernel Preemption)에서 발생합니다: 커널 코드를 실행하는 도중 스케줄러가 개입할 수 있는 시점이 달라집니다.

선점 모델 비교

모델 CONFIG 옵션 커널 선점 시점 특성
PREEMPT_NONE CONFIG_PREEMPT_NONE 시스템 콜/인터럽트 복귀 시에만 최대 처리량, 서버 워크로드에 적합
PREEMPT_VOLUNTARY CONFIG_PREEMPT_VOLUNTARY 명시적 선점 포인트 (might_sleep(), cond_resched()) 데스크톱 기본, 적절한 응답성과 처리량 균형
PREEMPT (Full) CONFIG_PREEMPT preempt_count == 0인 모든 구간 저지연 데스크톱, 오디오/게이밍 워크로드
PREEMPT_RT CONFIG_PREEMPT_RT 거의 모든 커널 코드에서 (spinlock도 선점 가능) 하드 실시간, 결정론적 지연 시간

비선점 모드 상세 (PREEMPT_NONE)

PREEMPT_NONE은 커널 코드 실행 중 선점을 허용하지 않는 가장 보수적인 모델입니다. 커널에 진입한 태스크는 자발적으로 CPU를 양보하거나, 유저 공간으로 복귀하거나, 명시적으로 schedule()을 호출할 때까지 계속 실행됩니다.

/* PREEMPT_NONE에서의 선점 동작 흐름 */

/* 시나리오: 태스크 A가 시스템 콜 처리 중, 태스크 B(높은 우선순위)가 깨어남 */

/* 1. 타이머 tick이 발생하여 TIF_NEED_RESCHED 설정 */
scheduler_tick() → task_tick_fair()
    → set_tsk_need_resched(curr);  /* 플래그만 설정, 즉시 선점 안 함 */

/* 2. 태스크 A는 시스템 콜 처리를 계속 진행 (수 밀리초~수십 밀리초) */
/*    → PREEMPT_NONE에서는 커널 코드 실행 중 선점 검사를 하지 않음 */

/* 3. 시스템 콜이 완료되고 유저 공간으로 복귀하는 시점에서야 선점 발생 */
syscall_exit_to_user_mode()
    → exit_to_user_mode_prepare()
        → if (test_thread_flag(TIF_NEED_RESCHED))
              schedule();  /* 여기서야 비로소 태스크 B로 전환 */

비선점의 장점:

비선점의 단점:

서버 환경에서 PREEMPT_NONE 선호 이유: 대부분의 서버 배포판(RHEL, Ubuntu Server, Debian)은 기본적으로 PREEMPT_NONE 또는 PREEMPT_VOLUNTARY를 사용합니다. 서버 워크로드는 대화형 응답성보다 초당 요청 처리 수(throughput)가 중요하며, 선점 오버헤드를 제거하면 CPU-bound 작업에서 2~5%의 성능 이득을 얻을 수 있습니다.

자발적 선점 포인트 (Voluntary Preemption Points)

PREEMPT_NONEPREEMPT_VOLUNTARY 모델에서는 커널 코드 내 명시적으로 배치된 자발적 선점 포인트가 유일한 커널 내 스케줄링 기회입니다. 커널 개발자는 긴 실행 경로에 이러한 포인트를 적절히 배치해야 합니다.

함수 동작 사용 조건 비고
cond_resched() TIF_NEED_RESCHED 설정 시 schedule() 호출 선점 비활성화 구간 밖, 슬립 가능 컨텍스트 가장 일반적인 자발적 양보 포인트
cond_resched_lock() spinlock을 해제하고 스케줄링 후 다시 획득 spinlock 보유 중 긴 루프에서 spinlock 보유 시간 제한
might_sleep() PREEMPT_VOLUNTARY에서 선점 포인트로 동작, 디버그 빌드에서 atomic 컨텍스트 검증 슬립 가능 컨텍스트 CONFIG_DEBUG_ATOMIC_SLEEP에서 BUG 검출
schedule() 명시적으로 스케줄러 호출, CPU 즉시 양보 슬립 가능 컨텍스트 대기 루프, 커널 스레드 메인 루프에서 사용
yield() 현재 태스크를 런큐 맨 뒤로 이동 유저/커널 모두 비권장: 예측 불가능한 동작, sched_yield(2)
/* 자발적 선점 포인트 사용 패턴 */

/* 1. cond_resched() — 긴 커널 루프에서 주기적 양보 */
static int do_large_scan(struct address_space *mapping)
{
    struct folio *folio;
    unsigned long index = 0;

    while ((folio = find_get_folio(mapping, index++))) {
        process_folio(folio);
        folio_put(folio);

        /* 긴 루프에서 주기적으로 CPU 양보 기회 제공 */
        if (need_resched())
            cond_resched();  /* TIF_NEED_RESCHED 설정 시 schedule() 호출 */
    }
    return 0;
}

/* 2. cond_resched_lock() — spinlock 보유 중 양보 */
static void flush_all_entries(struct list_head *head, spinlock_t *lock)
{
    struct entry *e, *tmp;

    spin_lock(lock);
    list_for_each_entry_safe(e, tmp, head, node) {
        list_del(&e->node);
        process_entry(e);
        cond_resched_lock(lock);  /* lock 해제 → schedule() → lock 재획득 */
    }
    spin_unlock(lock);
}

/* 3. might_sleep() — 슬립 가능 컨텍스트 검증 */
void *kmalloc(size_t size, gfp_t flags)
{
    if (flags & __GFP_DIRECT_RECLAIM)
        might_sleep();  /* atomic 컨텍스트에서 GFP_KERNEL 사용 시 경고 */
    /* ... */
}
yield() 사용을 피해야 하는 이유: yield()는 현재 태스크를 런큐 맨 뒤로 보내지만, 다른 실행 가능한 태스크가 없으면 즉시 다시 실행됩니다. CFS에서는 vruntime 기반 스케줄링으로 인해 yield()의 동작이 직관적이지 않을 수 있습니다. 커널 코드에서는 cond_resched()를, 유저 공간에서는 적절한 동기화 프리미티브(mutex, futex, condition variable)를 사용하세요.

preempt_count 중첩 동작 상세

preempt_count는 각 태스크(스레드)에 연결된 32비트 정수로, 현재 실행 컨텍스트와 선점 비활성화 상태를 추적합니다. 이 값이 0이 아니면 커널 선점이 불가합니다.

preempt_count 32비트 필드 레이아웃 bit 31 RESCHED bits 22-30 Reserved bit 21 LAZY bit 20 NMI bits 16-19 HARDIRQ (4 bits) bits 8-15 SOFTIRQ (8 bits) bits 0-7 PREEMPT (8 bits) TIF_NEED_RESCHED 비트 반전 저장 PREEMPT_LAZY용 (커널 6.12+) NMI 컨텍스트 중첩 불가 하드 IRQ 중첩 횟수 irq_enter/exit 조작 local_bh_disable 횟수 softirq 처리 중 증가 preempt_disable 횟수 spin_lock 등으로 증가 컨텍스트 검사 매크로 in_irq() / in_hardirq() → hardirq count > 0 (하드 인터럽트 핸들러 내부) in_softirq() → softirq count > 0 (softirq 또는 bh_disable 구간) in_interrupt() → hardirq + softirq + NMI 중 하나라도 > 0 in_nmi() → NMI 비트 설정 (Non-Maskable Interrupt 컨텍스트) preemptible() → preempt_count == 0 && !irqs_disabled() (선점 가능 여부)
/* include/linux/preempt.h — preempt_count 필드 정의 */
#define PREEMPT_BITS      8
#define SOFTIRQ_BITS      8
#define HARDIRQ_BITS      4
#define NMI_BITS          1

#define PREEMPT_SHIFT     0
#define SOFTIRQ_SHIFT     (PREEMPT_SHIFT + PREEMPT_BITS)    /* 8 */
#define HARDIRQ_SHIFT     (SOFTIRQ_SHIFT + SOFTIRQ_BITS)    /* 16 */
#define NMI_SHIFT         (HARDIRQ_SHIFT + HARDIRQ_BITS)    /* 20 */

/* 중첩 동작 예시 */
spin_lock(&lock_a);         /* preempt_count: 0 → 1 (PREEMPT 필드) */
  spin_lock(&lock_b);       /* preempt_count: 1 → 2 (중첩 가능) */
    local_bh_disable();     /* preempt_count: 2 → 2 + 0x100 (SOFTIRQ 필드 증가) */
    local_bh_enable();      /* preempt_count: SOFTIRQ 필드 감소 */
  spin_unlock(&lock_b);     /* preempt_count: 2 → 1 */
spin_unlock(&lock_a);       /* preempt_count: 1 → 0 → 선점 검사! */

/* 선점 가능 여부 확인 */
#define preemptible()     (preempt_count() == 0 && !irqs_disabled())

선점 메커니즘

/* include/linux/preempt.h — 선점 비활성화/활성화 */
#define preempt_disable() \
    do { preempt_count_inc(); barrier(); } while (0)

#define preempt_enable() \
    do { \
        barrier(); \
        if (unlikely(preempt_count_dec_and_test())) \
            __preempt_schedule();  /* count==0 && need_resched → schedule() */ \
    } while (0)

/* TIF_NEED_RESCHED 설정 (스케줄러가 재스케줄 요청) */
static inline void set_tsk_need_resched(struct task_struct *tsk)
{
    set_tsk_thread_flag(tsk, TIF_NEED_RESCHED);
}

/* preempt_schedule — 커널 선점 진입점 (CONFIG_PREEMPT) */
asmlinkage void __preempt_schedule(void)
{
    if (likely(!preemptible()))
        return;
    do {
        preempt_disable();
        __schedule(SM_PREEMPT);
        preempt_enable_no_resched();
    } while (need_resched());
}

선점 흐름 비교 다이어그램

아래 다이어그램은 동일한 시나리오에서 세 가지 선점 모델의 동작 차이를 보여줍니다. 태스크 A(일반 우선순위)가 긴 시스템 콜을 처리하는 도중, 태스크 B(높은 우선순위)가 깨어나는 상황입니다.

선점 모델별 태스크 전환 타이밍 비교 태스크 A (실행 중) 태스크 B (실행 중) 태스크 B (대기 중) PREEMPT_NONE t 태스크 A: 긴 시스템 콜 처리 (커널 모드) B wakeup + TIF_NEED_RESCHED 태스크 B: 대기 (선점 불가) syscall 복귀 → schedule() 태스크 B 실행 지연: 길다 (수 ms) PREEMPT_VOLUNTARY t 태스크 A: 시스템 콜 처리 중 B wakeup + TIF_NEED_RESCHED B 대기 cond_resched() → schedule() 태스크 B 실행 지연: 보통 (~1ms) PREEMPT (Full) t 태스크 A B wakeup + TIF_NEED_RESCHED preempt_enable() → schedule() 태스크 B 실행 지연: 짧다 (~100μs) 커널 선점 검사 경로 (3가지) 1. 시스템 콜 / 인터럽트 복귀 syscall_exit_to_user_mode() 모든 모델에서 동작 2. 자발적 선점 포인트 cond_resched() / might_sleep() VOLUNTARY 이상에서 동작 3. preempt_enable() 경로 spin_unlock() / preempt_enable() PREEMPT / PREEMPT_RT에서 동작 NONE ✓ | VOLUNTARY ✓ | FULL ✓ NONE ✗ | VOLUNTARY ✓ | FULL ✓ NONE ✗ | VOLUNTARY ✗ | FULL ✓ 선점 검사 포인트 증가 → 지연시간 감소 ↓ | 처리량 감소 ↓ | 오버헤드 증가 ↑

CONFIG_PREEMPT_DYNAMIC (런타임 전환)

커널 5.12에서 도입되고 6.x에서 안정화된 CONFIG_PREEMPT_DYNAMIC재부팅 없이 런타임에 선점 모델을 전환할 수 있게 합니다. 내부적으로 static_call 메커니즘을 사용하여 함수 포인터 오버헤드 없이 선점 포인트의 동작을 전환합니다.

# 현재 선점 모델 확인
cat /sys/kernel/debug/sched/preempt
# 출력 예: "full" 또는 "voluntary" 또는 "none"

# 런타임 선점 모델 전환 (root 필요)
echo none > /sys/kernel/debug/sched/preempt
echo voluntary > /sys/kernel/debug/sched/preempt
echo full > /sys/kernel/debug/sched/preempt

# 부팅 파라미터로 초기 선점 모델 지정
# GRUB_CMDLINE_LINUX에 추가:
preempt=none        # 서버 워크로드
preempt=voluntary   # 데스크톱 기본
preempt=full         # 저지연 워크로드
/* kernel/sched/core.c — PREEMPT_DYNAMIC 구현 원리 */

/* static_call로 선점 포인트 동작을 런타임에 전환 */
DEFINE_STATIC_CALL(preempt_schedule, __preempt_schedule_func);
DEFINE_STATIC_CALL(cond_resched, __cond_resched_func);

/* preempt=none 모드: cond_resched()와 preempt_schedule() 모두 NOP */
static int __cond_resched_none(void) { return 0; }
static void __preempt_schedule_none(void) { }

/* preempt=voluntary 모드: cond_resched()만 활성화 */
static int __cond_resched_voluntary(void)
{
    if (need_resched()) {
        preempt_schedule_common();
        return 1;
    }
    return 0;
}

/* preempt=full 모드: cond_resched() + preempt_enable() 경로 모두 활성화 */
/* → __preempt_schedule()이 실제 schedule() 호출 */

/* 모델 전환 시 static_call 업데이트 */
void sched_dynamic_update(int mode)
{
    switch (mode) {
    case preempt_dynamic_none:
        static_call_update(cond_resched, __cond_resched_none);
        static_call_update(preempt_schedule, __preempt_schedule_none);
        break;
    case preempt_dynamic_voluntary:
        static_call_update(cond_resched, __cond_resched_voluntary);
        static_call_update(preempt_schedule, __preempt_schedule_none);
        break;
    case preempt_dynamic_full:
        static_call_update(cond_resched, __cond_resched_voluntary);
        static_call_update(preempt_schedule, __preempt_schedule_func);
        break;
    }
}
PREEMPT_DYNAMIC의 이점: 배포판 커널을 CONFIG_PREEMPT_DYNAMIC=y로 빌드하면, 동일한 커널 바이너리로 서버(none)와 데스크톱(voluntary/full) 환경을 모두 최적화할 수 있습니다. static_call 메커니즘 덕분에 런타임 오버헤드는 사실상 0입니다 — 함수 포인터 대신 직접 호출(direct call) 명령어가 패치됩니다.

PREEMPT_LAZY (커널 6.12+)

커널 6.12에서 도입된 PREEMPT_LAZY는 Full Preemption과 Voluntary Preemption 사이의 새로운 중간 모델입니다. 핵심 아이디어는 선점 요청을 즉시 처리하지 않고, 다음 자연적 선점 포인트(타이머 tick, 시스템 콜 복귀 등)까지 지연하는 것입니다.

/* PREEMPT_LAZY 핵심 메커니즘 */

/* 기존 선점: TIF_NEED_RESCHED만 사용 */
/* PREEMPT_LAZY: 두 가지 플래그 사용 */

#define TIF_NEED_RESCHED       3   /* 즉시 선점 필요 */
#define TIF_NEED_RESCHED_LAZY  4   /* 지연 선점 (다음 자연 포인트에서 처리) */

/*
 * PREEMPT_LAZY 동작 흐름:
 *
 * 1. scheduler_tick() → resched_curr() 호출 시:
 *    - 즉시 선점 대신 TIF_NEED_RESCHED_LAZY만 설정
 *    - preempt_enable() 경로에서는 이 플래그를 검사하지 않음
 *
 * 2. 자연적 선점 포인트(tick, syscall return)에서:
 *    - TIF_NEED_RESCHED_LAZY 검사 → TIF_NEED_RESCHED 설정 → schedule()
 *
 * 3. 긴급 선점이 필요한 경우 (RT 태스크 wakeup 등):
 *    - TIF_NEED_RESCHED를 직접 설정 → 즉시 선점
 */

/* 일반 wakeup: lazy 선점 */
static void resched_curr_lazy(struct rq *rq)
{
    struct task_struct *curr = rq->curr;

    if (test_tsk_need_resched(curr))
        return;  /* 이미 즉시 선점 예약됨 */

    if (test_tsk_thread_flag(curr, TIF_NEED_RESCHED_LAZY))
        return;  /* 이미 lazy 선점 예약됨 */

    set_tsk_thread_flag(curr, TIF_NEED_RESCHED_LAZY);
    /* preempt_enable()에서는 검사 안 함 → 다음 tick에서 처리 */
}

/* 긴급 wakeup (RT 태스크 등): 즉시 선점 */
static void resched_curr(struct rq *rq)
{
    set_tsk_need_resched(rq->curr);  /* TIF_NEED_RESCHED 직접 설정 */
}
구분 PREEMPT (Full) PREEMPT_LAZY
일반 선점 요청 preempt_enable() 시 즉시 선점 다음 tick/syscall 복귀까지 지연
RT 태스크 wakeup 즉시 선점 즉시 선점 (동일)
처리량 기준 ~5% 향상 (벤치마크 의존)
일반 지연시간 낮음 약간 높음 (최대 1 tick)
RT 지연시간 낮음 동일 (즉시 선점)
장점 최소 지연시간 처리량 향상 + RT 지연시간 유지
PREEMPT_LAZY의 설계 철학: 대부분의 선점 요청은 "즉시" 처리할 필요가 없습니다. 일반 CFS 태스크 간의 전환은 1 tick(1~4ms) 지연해도 사용자가 체감하지 못합니다. 반면 RT 태스크의 선점은 긴급하므로 즉시 처리합니다. 이 구분을 통해 불필요한 컨텍스트 스위칭을 줄이면서도 실시간 응답성은 유지할 수 있습니다.

PREEMPT_RT 상세

PREEMPT_RT 패치(v5.15부터 메인라인 통합 진행, 6.x에서 대부분 완료)는 Linux 커널을 하드 실시간 시스템으로 변환합니다. 최악 지연 시간(worst-case latency)을 100μs 이하로 보장하기 위해 커널의 동기화 메커니즘을 근본적으로 변경합니다.

/* PREEMPT_RT에서의 주요 변경점 */

/*
 * 1. spinlock → rt_mutex로 대체
 *    - spin_lock()이 실제로는 슬립 가능한 rt_mutex 획득
 *    - 우선순위 상속(priority inheritance) 자동 지원
 *    - 선점 가능 → 장시간 lock 보유가 다른 태스크를 블록하지 않음
 */
typedef struct rt_mutex spinlock_t;  /* CONFIG_PREEMPT_RT일 때 */

/*
 * 2. softirq의 스레드화
 *    - softirq가 전용 커널 스레드(ksoftirqd)에서 실행
 *    - softirq 처리 중에도 선점 가능
 */

/*
 * 3. 하드 인터럽트의 스레드화 (threaded IRQ)
 *    - 인터럽트 핸들러가 커널 스레드로 실행
 *    - 인터럽트 처리 중 선점 및 스케줄링 가능
 *    - 최악 지연 시간 100μs 이하 달성
 */

Priority Inheritance (우선순위 상속) 프로토콜

우선순위 역전(Priority Inversion) 문제는 높은 우선순위 태스크가 낮은 우선순위 태스크가 보유한 락을 기다리는 동안, 중간 우선순위 태스크가 낮은 우선순위 태스크를 선점하여 간접적으로 높은 우선순위 태스크를 지연시키는 현상입니다. PREEMPT_RT의 rt_mutex는 이를 자동으로 해결합니다.

/* rt_mutex의 Priority Inheritance 동작 */

/* 시나리오: 태스크 H(높은 prio), M(중간), L(낮은)
 *
 * 1. L이 rt_mutex 획득
 * 2. H가 같은 rt_mutex를 기다림
 *    → L의 우선순위를 H 수준으로 일시적으로 상승 (boost)
 * 3. M이 L을 선점하려 하지만, L의 부스트된 우선순위가 더 높으므로 실패
 * 4. L이 rt_mutex 해제 → H가 즉시 실행
 *    → L의 우선순위 원복 (deboost)
 */

struct rt_mutex {
    raw_spinlock_t      wait_lock;
    struct rb_root_cached waiters;    /* 대기자 RB 트리 (우선순위 정렬) */
    struct task_struct   *owner;      /* 현재 소유자 */
};

/* 우선순위 상속 체인: 중첩된 락에서도 전파 */
/* H → mutex_A(소유: M) → M → mutex_B(소유: L) → L
 * → L은 H의 우선순위로 부스트됨 (체인 전파) */

raw_spinlock vs spinlock 사용 가이드라인

타입 PREEMPT_RT 동작 사용처
spinlock_t rt_mutex (슬립 가능, 선점 가능) 대부분의 커널 코드
raw_spinlock_t 진정한 스핀락 (슬립 불가, 선점 불가) 스케줄러, 인터럽트 코드, 타이머 코어
local_lock_t per-CPU 데이터 보호 (RT에서 슬립 가능) per-CPU 데이터 접근 시
/* local_lock — per-CPU 데이터 보호 (PREEMPT_RT 호환) */

/* 비-RT 커널: local_lock은 preempt_disable/enable으로 최적화 */
/* RT 커널: per-CPU spinning lock으로 변환 (마이그레이션 방지) */

DEFINE_PER_CPU(struct local_lock, mydata_lock);
DEFINE_PER_CPU(struct mydata, mydata);

void update_mydata(void)
{
    local_lock(&mydata_lock);      /* 비-RT: preempt_disable(), RT: spin_lock */
    this_cpu_inc(mydata.counter);
    local_unlock(&mydata_lock);    /* 비-RT: preempt_enable(), RT: spin_unlock */
}
PREEMPT_RT와 raw_spinlock: PREEMPT_RT에서도 raw_spinlock_t는 진정한 스핀락으로 남습니다. 인터럽트 컨텍스트나 스케줄러 자체에서 사용하는 락에 필요합니다. raw_spin_lock()으로 보호되는 크리티컬 섹션은 가능한 짧게 유지해야 합니다. 일반 드라이버에서는 raw_spinlock 대신 spinlock_t를 사용하세요.

인터럽트 컨텍스트와 선점

인터럽트 컨텍스트에서의 선점 동작은 선점 모델에 따라 크게 달라집니다. 하드 인터럽트 핸들러 실행 중에는 preempt_count의 hardirq 비트가 설정되어 있으므로 선점이 불가합니다.

/* 인터럽트 진입/탈출 시 preempt_count 조작 */

/* 하드 인터럽트 진입 */
void irq_enter(void)
{
    preempt_count_add(HARDIRQ_OFFSET);  /* hardirq 카운터 +1 */
    /* → in_hardirq() == true, 선점 불가 */
}

/* 하드 인터럽트 탈출 */
void irq_exit(void)
{
    preempt_count_sub(HARDIRQ_OFFSET);  /* hardirq 카운터 -1 */

    if (!in_interrupt() && local_softirq_pending())
        invoke_softirq();  /* 보류된 softirq 처리 */

    /* CONFIG_PREEMPT: hardirq count가 0이 되면 선점 검사 */
    preempt_check_resched();
}

/* 인터럽트 복귀 시 선점 검사 (arch 코드) */
/*
 * 비-RT 커널:
 *   인터럽트 → 하드웨어 핸들러(hardirq context) → irq_exit
 *   → softirq 처리(softirq context) → 선점 검사
 *
 * PREEMPT_RT 커널:
 *   인터럽트 → 최소한의 top-half만 → threaded IRQ 핸들러(프로세스 context)
 *   → softirq도 ksoftirqd 스레드에서 처리
 *   → 모두 스케줄링 가능한 프로세스 컨텍스트에서 실행
 */
컨텍스트 비-RT 커널 PREEMPT_RT
하드 IRQ 선점 불가 (hardirq context) top-half: 선점 불가 (최소 코드만)
threaded handler: 선점 가능
softirq 선점 불가 (softirq context) ksoftirqd 스레드에서 실행, 선점 가능
tasklet 선점 불가 (softirq에서 실행) 선점 가능 (스레드화)
타이머 콜백 softirq context에서 실행 별도 스레드에서 실행, 선점 가능

성능 영향 분석

선점 모델 선택은 시스템의 지연시간(latency)처리량(throughput)에 직접적인 영향을 미칩니다. 워크로드 특성에 맞는 모델을 선택하는 것이 중요합니다.

선점 모델 스케줄링 지연 처리량 컨텍스트 스위칭 빈도 권장 워크로드
PREEMPT_NONE ~10ms (최악) 최고 (기준) 최소 서버, 데이터베이스, HPC, 배치 처리
PREEMPT_VOLUNTARY ~2ms (최악) 높음 (~98%) 낮음 데스크톱, 범용 서버, 개발 환경
PREEMPT_LAZY ~1ms (일반), ~100μs (RT) 높음 (~97%) 보통 혼합 워크로드 (서버 + 실시간)
PREEMPT ~500μs (최악) 보통 (~95%) 높음 오디오/비디오 제작, 게이밍
PREEMPT_RT <100μs (보장) 낮음 (~85-90%) 최대 산업 제어, 로봇, 의료 장비, 금융
성능 측정 도구:
  • cyclictest --mlockall -t1 -p80 -i250 -l10000 — 스케줄링 지연시간 측정 (RT 환경 표준)
  • hackbench -s512 -l200 -g15 -f25 — 스케줄러 처리량 벤치마크
  • latencytop — 프로세스별 지연 원인 분석
  • perf sched latency — 스케줄링 지연시간 히스토그램
  • perf sched timehist — 시간별 스케줄링 이벤트 기록

선점 관련 디버깅

선점 동작 문제를 디버깅하기 위한 커널 내장 도구들입니다. 주로 ftrace 기반 트레이서를 사용하여 선점/인터럽트 비활성화 구간을 추적합니다.

# === preemptoff tracer: 선점 비활성화 구간 추적 ===
# 가장 긴 선점 비활성화 구간을 찾아 보고

echo preemptoff > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 워크로드 실행 후:
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace

# 출력 예시:
# # tracer: preemptoff
# # => started at: _raw_spin_lock_irqsave
# # => ended at:   _raw_spin_unlock_irqrestore
# #  latency: 152 us, #4/4, CPU#0

# === irqsoff tracer: 인터럽트 비활성화 구간 추적 ===
echo irqsoff > /sys/kernel/debug/tracing/current_tracer

# === preemptirqsoff: 선점 + 인터럽트 비활성화 모두 추적 ===
echo preemptirqsoff > /sys/kernel/debug/tracing/current_tracer

# === 최대 지연시간 리셋 ===
echo 0 > /sys/kernel/debug/tracing/tracing_max_latency
# === 프로세스별 자발적/비자발적 컨텍스트 스위칭 통계 ===
cat /proc/<PID>/sched | grep nr_switches
# nr_voluntary_switches: 1523     ← 자발적 (sleep, wait)
# nr_involuntary_switches: 47     ← 비자발적 (선점)

# 비율로 선점 빈도 파악:
# involuntary가 높으면 → 선점이 빈번 → PREEMPT 모델 또는 높은 경쟁
# voluntary가 높으면 → 정상적인 I/O 대기 패턴

# === perf를 이용한 컨텍스트 스위칭 분석 ===
perf stat -e context-switches,cpu-migrations -- <command>

# === ftrace로 선점 이벤트 추적 ===
echo sched_switch > /sys/kernel/debug/tracing/set_event
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 워크로드 실행
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace | head -50

# 출력에서 prev_state 필드로 선점 여부 판별:
# prev_state=R  → Running 상태에서 선점됨 (비자발적)
# prev_state=S  → Sleep 상태로 전환 (자발적)
디버깅 트레이서 사용 시 주의: preemptoff, irqsoff, preemptirqsoff 트레이서는 CONFIG_PREEMPTIRQ_TRACEPOINTS=y 또는 CONFIG_IRQSOFF_TRACER=y가 활성화된 커널에서만 사용 가능합니다. 이 트레이서들은 약간의 오버헤드를 추가하므로, 프로덕션 환경에서는 측정 후 즉시 비활성화하세요.

스케줄러 디버깅 (Scheduler Debugging)

스케줄러의 동작을 분석하고 성능 문제를 진단하기 위한 다양한 도구와 인터페이스가 있습니다.

/proc/sched_debug

# 전체 스케줄러 상태 덤프
cat /proc/sched_debug

# 출력 내용:
# - 글로벌 스케줄러 파라미터 (sched_latency, min_granularity 등)
# - per-CPU 런큐 상태 (nr_running, load, curr 태스크 등)
# - 각 CPU의 CFS 런큐 트리 (vruntime, deadline, weight 등)
# - RT 런큐 상태

# 특정 프로세스의 스케줄링 정보
cat /proc/<pid>/sched

# 출력 예:
# se.vruntime                         :         12345.678901
# se.sum_exec_runtime                 :        987654.321098
# se.nr_migrations                    :               42
# nr_switches                         :             1234
# nr_voluntary_switches               :              987
# nr_involuntary_switches             :              247
# prio                                :              120
# policy                              :                0  (SCHED_NORMAL)

perf sched

# 스케줄링 이벤트 기록 (10초간)
sudo perf sched record -- sleep 10

# 스케줄링 지연 분석 (wakeup → actually running)
sudo perf sched latency
# 출력:
#  Task                  |   Runtime ms  | Switches | Avg delay ms |
# ------------------------------------------------------------------
#  kworker/0:1-mm_per    |      0.293 ms |       12 | avg:  0.012 ms |
#  bash:1234             |    125.432 ms |       89 | avg:  0.845 ms |

# CPU별 타임라인 (ASCII art)
sudo perf sched map
# 출력:
#  *A0          0.000 ms | 0: migration/0
#   A0   *B0    0.105 ms | 1: ksoftirqd/1
#   A0    B0 *C0 0.230 ms | 2: bash:1234

# 스케줄링 통계 요약
sudo perf sched timehist
# 각 컨텍스트 스위치의 상세 타임스탬프, wait/run 시간 출력

# 스케줄링 이벤트 기반 통계
sudo perf stat -e 'sched:sched_switch,sched:sched_wakeup' -- sleep 5

ftrace 스케줄러 트레이싱

# ftrace로 스케줄러 이벤트 트레이싱
cd /sys/kernel/debug/tracing

# 사용 가능한 스케줄러 이벤트 확인
ls events/sched/
# sched_switch  sched_wakeup  sched_wakeup_new  sched_migrate_task
# sched_process_fork  sched_process_exec  sched_process_exit
# sched_stat_wait  sched_stat_sleep  sched_stat_runtime

# 스케줄러 스위치 이벤트 활성화
echo 1 > events/sched/sched_switch/enable
echo 1 > events/sched/sched_wakeup/enable

# 트레이싱 시작
echo 1 > tracing_on
sleep 2
echo 0 > tracing_on

# 결과 확인
cat trace
# 출력 예:
# bash-1234 [002] 1234.567890: sched_switch: prev_comm=bash prev_pid=1234
#   prev_prio=120 prev_state=S ==> next_comm=kworker/2:1 next_pid=567
#   next_prio=120
#
# <idle>-0 [001] 1234.567895: sched_wakeup: comm=bash pid=1234
#   prio=120 target_cpu=002

# 특정 프로세스의 wakeup 지연 측정 (wakeup latency tracer)
echo wakeup > current_tracer
echo 1 > tracing_on
# ... 워크로드 실행 ...
echo 0 > tracing_on
cat trace
# 최대 wakeup 지연 시간 표시

스케줄러 디버그 기능 (sched_features)

# 현재 활성화된 스케줄러 기능 확인
cat /sys/kernel/debug/sched/features
# 출력 예:
# GENTLE_FAIR_SLEEPERS  (슬리퍼에 대한 보상 제한)
# START_DEBIT           (새 태스크에 vruntime 불이익)
# NEXT_BUDDY            (wakeup 시 next 힌트)
# LAST_BUDDY            (yield 시 last 힌트)
# CACHE_HOT_BUDDY       (캐시 친화적 wakeup)
# WAKEUP_PREEMPTION     (wakeup 시 선점 허용)
# PLACE_LAG             (EEVDF: lag 기반 배치)
# PLACE_DEADLINE_INITIAL (EEVDF: 초기 deadline 설정)

# 기능 토글 (런타임)
echo WAKEUP_PREEMPTION > /sys/kernel/debug/sched/features   # 활성화
echo NO_WAKEUP_PREEMPTION > /sys/kernel/debug/sched/features # 비활성화

schedstat 통계

# schedstat 활성화 (CONFIG_SCHEDSTATS 필요)
echo 1 > /proc/sys/kernel/sched_schedstats

# CPU별 스케줄링 통계
cat /proc/schedstat
# cpu<N> <domain> <9개 필드>
# 필드: yld_count, sched_switch, sched_goidle, ttwu_count,
#       ttwu_local, rq_cpu_time, run_delay, pcount

# 프로세스별 스케줄링 통계
cat /proc/<pid>/schedstat
# 3개 필드: run_time(ns) wait_time(ns) nr_timeslices

# 모든 CPU의 로드 밸런싱 통계 확인
cat /proc/schedstat | grep domain

디버깅 도구 요약

도구 용도 오버헤드
/proc/sched_debug 스케줄러 전체 상태 스냅샷 낮음 (읽기만)
/proc/<pid>/sched 개별 프로세스 스케줄링 정보 낮음
perf sched 스케줄링 지연/타임라인 분석 중간
ftrace sched events 상세 스케줄링 이벤트 트레이싱 중간~높음
schedstat 스케줄링 통계 집계 낮음
sched_features 스케줄러 동작 런타임 튜닝 없음
trace-cmd ftrace 프론트엔드 (기록/분석) 중간
kernelshark trace-cmd 데이터 GUI 시각화 없음 (오프라인)

주요 커널 설정 옵션

설정 설명 기본값
CONFIG_PREEMPT_NONE 서버용 비선점 커널 -
CONFIG_PREEMPT_VOLUNTARY 데스크톱 자발적 선점 대부분의 배포판 기본
CONFIG_PREEMPT 저지연 선점형 커널 -
CONFIG_PREEMPT_RT 하드 실시간 선점 -
CONFIG_SCHED_EXT BPF sched_ext 지원 n (v6.12+)
CONFIG_SCHEDSTATS 스케줄링 통계 수집 n
CONFIG_SCHED_DEBUG 스케줄러 디버그 인터페이스 y
CONFIG_NO_HZ_FULL 단일 태스크 CPU에서 틱 제거 n
CONFIG_HZ 타이머 틱 빈도 (100/250/300/1000) 250
CONFIG_NUMA_BALANCING NUMA 자동 메모리/태스크 밸런싱 y (NUMA 시스템)
CONFIG_CGROUP_SCHED cgroup 기반 스케줄링 지원 y
CONFIG_FAIR_GROUP_SCHED CFS/EEVDF 그룹 스케줄링 y
CONFIG_RT_GROUP_SCHED RT 그룹 스케줄링 n

sysctl 튜닝 파라미터

# === CFS/EEVDF 파라미터 ===
# 타겟 레이턴시 (ns): 태스크들이 한 번씩 실행되는 목표 주기
sysctl kernel.sched_latency_ns=6000000        # 6ms (기본)

# 최소 그래뉼래리티 (ns): 태스크당 최소 실행 시간
sysctl kernel.sched_min_granularity_ns=750000 # 0.75ms (기본)

# EEVDF 기본 슬라이스 (ns, v6.6+)
sysctl kernel.sched_base_slice_ns=3000000     # 3ms (기본)

# 웨이크업 그래뉼래리티: 깨어난 태스크의 선점 임계값
sysctl kernel.sched_wakeup_granularity_ns=1000000  # 1ms (기본)

# === RT 파라미터 ===
# RT 대역폭 제한 (us)
sysctl kernel.sched_rt_period_us=1000000      # 1초 주기
sysctl kernel.sched_rt_runtime_us=950000      # 주기당 최대 950ms

# === 마이그레이션 비용 ===
sysctl kernel.sched_migration_cost_ns=500000 # 0.5ms (기본)
# 이 시간 이내에 실행된 태스크는 "캐시 핫"으로 간주, 마이그레이션 억제

# === CPU Affinity (명령줄) ===
taskset -c 0,1 ./my_app                      # CPU 0,1에만 바인딩
taskset -p -c 2-5 1234                       # PID 1234를 CPU 2-5로 변경

# === isolcpus (커널 부트 파라미터) ===
# 특정 CPU를 스케줄러에서 격리 (RT 워크로드 전용)
# GRUB: isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3
성능 튜닝 팁: (1) 대화형 워크로드(데스크톱): sched_latency_ns 줄이기, CONFIG_PREEMPT 사용. (2) 서버/배치 워크로드: sched_min_granularity_ns 늘리기, CONFIG_PREEMPT_NONE 사용. (3) RT 워크로드: isolcpus + SCHED_FIFO + CONFIG_PREEMPT_RT. (4) 컨테이너 환경: cgroup cpu.max로 대역폭 제한, CONFIG_FAIR_GROUP_SCHED 필수.