CFS 스케줄러 상세

Linux 커널의 CFS(Completely Fair Scheduler)를 vruntime 수식과 실행 큐 동작 기준으로 심층 분석합니다. nice 가중치 기반 시간 분배, Red-Black Tree 스케줄링, wakeup preemption, PELT 부하 추적, cgroup 대역폭 제한, EEVDF와의 연계 변화, 성능 관측 지표와 튜닝 전략까지 실무 관점에서 다룹니다.

전제 조건: 커널 아키텍처 문서를 먼저 읽으세요. CPU, 메모리, 인터럽트의 기본 흐름을 알고 있으면 본 문서를 더 빠르게 이해할 수 있습니다.
일상 비유: 이 개념은 작업 순서표를 따라 문제를 해결하는 과정과 비슷합니다. 핵심 용어를 먼저 잡고, 실제 동작 순서를 단계별로 확인하면 복잡한 커널 내부 동작을 안정적으로 이해할 수 있습니다.

핵심 요약

  • 핵심 객체 — 이 문서의 중심이 되는 자료구조/API를 먼저 파악합니다.
  • 실행 경로 — 요청이 들어와 처리되고 종료되는 흐름을 확인합니다.
  • 병목 지점 — 지연이나 처리량 저하가 발생하는 구간을 점검합니다.
  • 동기화 지점 — 경합과 경쟁 조건이 생길 수 있는 구간을 구분합니다.
  • 운영 포인트 — 관측 지표와 튜닝 항목을 함께 확인합니다.

단계별 이해

  1. 구성요소 확인
    핵심 자료구조와 주요 API를 먼저 식별합니다.
  2. 요청 흐름 추적
    입력부터 완료까지의 호출 경로를 순서대로 따라갑니다.
  3. 예외 경로 점검
    실패 처리, 재시도, 타임아웃 등 경계 조건을 확인합니다.
  4. 성능/안정성 점검
    잠금 경합, 큐 적체, 병목 지점을 측정하고 조정합니다.

개요

CFS(Completely Fair Scheduler)는 2007년 Linux 2.6.23에서 기존 O(1) 스케줄러를 대체하며 도입된 프로세스 스케줄러입니다.

핵심 원칙

O(1) 스케줄러와의 차이

구분O(1) 스케줄러CFS
시간 복잡도O(1)O(log N)
자료구조우선순위별 큐 배열Red-Black Tree
타임슬라이스고정 (5-800ms)동적 조정
대화형 태스크휴리스틱 기반 감지자동 공정 분배
복잡도높음 (~2000 LOC)낮음 (~1000 LOC)

가상 런타임 (vruntime)

vruntime은 CFS의 핵심 메트릭으로, 실제 실행 시간을 nice 값(우선순위)으로 가중한 값입니다.

vruntime 계산

/* kernel/sched/fair.c */
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->vruntime += calc_delta_fair(delta_exec, curr);
    update_min_vruntime(cfs_rq);
}

/* vruntime = 실제시간 * (NICE_0_LOAD / 현재_weight) */
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;
}

Nice 값과 Weight 테이블

NiceWeightvruntime 증가율설명
-20887610.12x최고 우선순위 (1/8 속도로 증가)
-1095480.11x높은 우선순위
010241.0x기본 우선순위
+101109.3x낮은 우선순위
+191568x최저 우선순위 (68배 빠르게 증가)
Nice 값에 따른 Weight와 CPU 시간 분배 Weight 90000 60000 30000 10000 0 Nice 값 88761 -20 최고 9548 -10 높음 1024 0 기본 110 +10 낮음 15 +19 최저 핵심 포인트 • Weight가 높을수록 CPU 시간 많이 할당 • vruntime 증가율 = NICE_0_LOAD / weight • Nice 1 증가 ≈ CPU 시간 10% 감소
Nice 값이 낮을수록 weight가 크고, 같은 실제 실행 시간 대비 vruntime이 천천히 증가하여 CPU를 더 많이 할당받습니다.

vruntime Overflow 처리

vruntime은 u64 타입이지만, 수십 년 실행 시 오버플로우 가능성이 있습니다. CFS는 entity_before() 함수에서 부호 있는 차이 계산으로 오버플로우를 안전하게 처리합니다:

static int entity_before(struct sched_entity *a, struct sched_entity *b)
{
    return (s64)(a->vruntime - b->vruntime) < 0;
}

⚠ Nice 값 설정 주의사항

  • Nice 값은 상대적: nice -5는 "빠름"이 아니라 "nice 0보다 더 많은 CPU"를 의미합니다. 시스템에 프로세스가 1개뿐이면 nice 값은 무의미합니다.
  • 루트 권한 필요: 음수 nice (높은 우선순위)는 CAP_SYS_NICE capability가 필요합니다. 일반 사용자는 자신의 프로세스 nice 값을 증가(우선순위 낮춤)만 가능합니다.
  • 실시간 아님: nice -20도 SCHED_FIFO RT 태스크보다 낮은 우선순위입니다. 엄격한 지연시간 보장이 필요하면 실시간 스케줄링 정책을 사용하세요.
  • I/O 대기는 무관: Nice 값은 CPU 경쟁 시에만 영향을 줍니다. 디스크 I/O나 네트워크 대기 중인 시간은 vruntime에 포함되지 않습니다.

Red-Black Tree 구조

CFS는 각 CPU의 실행 큐(runqueue)에서 vruntime을 키로 하는 Red-Black Tree를 관리합니다.

sched_entity 구조체

/* include/linux/sched.h */
struct sched_entity {
    struct load_weight   load;           /* 우선순위 가중치 */
    unsigned long        runnable_weight;
    struct rb_node        run_node;       /* RB 트리 노드 */
    u64                  exec_start;     /* 마지막 실행 시작 시각 */
    u64                  sum_exec_runtime; /* 누적 실행 시간 */
    u64                  vruntime;       /* 가상 런타임 */
    u64                  prev_sum_exec_runtime;

    struct sched_statistics statistics;
    struct sched_entity   *parent;        /* 그룹 스케줄링용 */
    struct cfs_rq         *cfs_rq;        /* 소속 CFS 런큐 */
    struct cfs_rq         *my_q;          /* 그룹인 경우 자식 런큐 */
};

트리 구조 시각화

CFS Red-Black Tree (vruntime 기준) vruntime 150 vr 80 vr 200 vr 50 vr 120 vr 180 vr 250 leftmost (다음 실행 태스크) CFS 스케줄링 원리 • vruntime이 작을수록 우선순위 높음 • leftmost 노드 = 다음 실행 태스크 • O(log N) 복잡도로 빠른 선택
Red-Black Tree는 vruntime을 기준으로 태스크를 정렬하여 최소 vruntime 태스크를 O(log N)에 찾습니다.

다음 태스크 선택 (pick_next_task)

스케줄러의 핵심 동작은 가장 작은 vruntime을 가진 태스크를 선택하는 것입니다.

pick_next_entity 구현

/* kernel/sched/fair.c */
static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq)
{
    struct sched_entity *se = __pick_first_entity(cfs_rq);
    struct sched_entity *left = __pick_first_entity(cfs_rq);

    /* leftmost 노드 = RB 트리 최좌측 = 최소 vruntime */
    if (left) {
        se = left;
    }

    return se;
}

static struct sched_entity *
__pick_first_entity(struct cfs_rq *cfs_rq)
{
    struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);

    if (!left)
        return NULL;

    return rb_entry(left, struct sched_entity, run_node);
}

스케줄링 흐름

schedule()
  └─ __schedule()
       ├─ pick_next_task()
       │    └─ pick_next_task_fair()  /* CFS */
       │         └─ pick_next_entity()
       │              └─ __pick_first_entity()  /* leftmost */
       └─ context_switch()

타임슬라이스와 지연시간

CFS는 고정 타임슬라이스 대신 동적으로 계산된 시간 할당을 사용합니다.

핵심 파라미터

파라미터기본값설명
sched_latency_ns6ms모든 프로세스가 1회 실행되는 기간 (목표 지연)
sched_min_granularity_ns0.75ms최소 타임슬라이스 (너무 잦은 전환 방지)
sched_wakeup_granularity_ns1ms선점 결정 임계값

타임슬라이스 계산

/* kernel/sched/fair.c */
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    u64 slice = __sched_period(cfs_rq->nr_running);

    /* 프로세스의 가중치 비율로 분배 */
    slice *= se->load.weight;
    do_div(slice, cfs_rq->load.weight);

    return slice;
}

/* 실행 가능한 프로세스 수에 따라 period 조정 */
static u64 __sched_period(unsigned long nr_running)
{
    if (unlikely(nr_running > sched_nr_latency))
        return nr_running * sysctl_sched_min_granularity;
    else
        return sysctl_sched_latency;
}

예제 계산

타임슬라이스 계산 예제

4개 프로세스 (모두 nice 0, weight=1024)가 실행 중일 때:

  • Period = sched_latency = 6ms
  • 각 프로세스 타임슬라이스 = 6ms / 4 = 1.5ms

Nice -10 (weight=9548) 프로세스 1개 + Nice 0 프로세스 3개:

  • 총 가중치 = 9548 + 1024×3 = 12620
  • Nice -10 프로세스: 6ms × (9548/12620) ≈ 4.5ms
  • Nice 0 프로세스 각각: 6ms × (1024/12620) ≈ 0.5ms

타임슬라이스 계산 시뮬레이션 (코드)

/* CFS 타임슬라이스 계산 시뮬레이터 */
#include <stdio.h>

/* 커널 기본 값 */
#define SCHED_LATENCY_NS  6000000    /* 6ms */
#define MIN_GRANULARITY_NS 750000   /* 0.75ms */
#define NICE_0_LOAD        1024

/* Nice 값 → Weight 변환 테이블 (일부) */
const unsigned long sched_prio_to_weight[] = {
    88761,  /* nice -20 */
    71755, 56483, 46273, 36291,
    29154, 23254, 18705, 14949, 11916,  /* -10 */
    9548, 7620, 6100, 4904, 3906,
    3121, 2501, 1991, 1586, 1277,
    1024,   /* nice 0 (기준) */
    820, 655, 526, 423,
    335, 272, 215, 172, 137,
    110, 87, 70, 56, 45,
    36, 29, 23, 18, 15,  /* nice +19 */
};

/* 타임슬라이스 계산 */
void calculate_timeslice(int *nice_values, int nr_tasks)
{
    unsigned long total_weight = 0;
    unsigned long period;
    int i;

    /* 1. 총 가중치 계산 */
    for (i = 0; i < nr_tasks; i++) {
        int nice = nice_values[i];
        total_weight += sched_prio_to_weight[nice + 20]; /* nice -20 ~ +19 → 인덱스 0~39 */
    }

    /* 2. Period 결정 (nr_latency 고려) */
    int nr_latency = SCHED_LATENCY_NS / MIN_GRANULARITY_NS; /* 8 */
    if (nr_tasks > nr_latency)
        period = nr_tasks * MIN_GRANULARITY_NS; /* 태스크 많으면 확장 */
    else
        period = SCHED_LATENCY_NS;

    printf("\\n=== CFS 타임슬라이스 시뮬레이션 ===\\n");
    printf("프로세스 수: %d, Period: %lu ns (%.2f ms)\\n",
           nr_tasks, period, period / 1000000.0);
    printf("총 가중치: %lu\\n\\n", total_weight);

    /* 3. 각 태스크의 타임슬라이스 계산 */
    for (i = 0; i < nr_tasks; i++) {
        int nice = nice_values[i];
        unsigned long weight = sched_prio_to_weight[nice + 20];
        unsigned long slice_ns = (period * weight) / total_weight;

        printf("Task %d: nice=%d, weight=%lu, 타임슬라이스=%lu ns (%.2f ms)\\n",
               i, nice, weight, slice_ns, slice_ns / 1000000.0);
        printf("        → Period 대비 %.1f%% 할당\\n\\n",
               (double)slice_ns * 100 / period);
    }
}

int main(void)
{
    /* 시나리오 1: 모두 nice 0 */
    int scenario1[] = {0, 0, 0, 0};
    calculate_timeslice(scenario1, 4);

    /* 시나리오 2: nice -10, 0, 0, 0 (우선순위 혼재) */
    int scenario2[] = {-10, 0, 0, 0};
    calculate_timeslice(scenario2, 4);

    /* 시나리오 3: 극단적 차이 (nice -20, +19) */
    int scenario3[] = {-20, 19};
    calculate_timeslice(scenario3, 2);

    return 0;
}

/* 출력 예시:
 *
 * === CFS 타임슬라이스 시뮬레이션 ===
 * 프로세스 수: 4, Period: 6000000 ns (6.00 ms)
 * 총 가중치: 4096
 *
 * Task 0: nice=0, weight=1024, 타임슬라이스=1500000 ns (1.50 ms)
 *         → Period 대비 25.0% 할당
 *
 * Task 1: nice=0, weight=1024, 타임슬라이스=1500000 ns (1.50 ms)
 *         → Period 대비 25.0% 할당
 * ...
 *
 * === 시나리오 2 ===
 * Task 0: nice=-10, weight=9548, 타임슬라이스=4539810 ns (4.54 ms)
 *         → Period 대비 75.7% 할당 ← 높은 우선순위!
 *
 * Task 1: nice=0, weight=1024, 타임슬라이스=486730 ns (0.49 ms)
 *         → Period 대비 8.1% 할당
 */

부하 분산 (Load Balancing)

멀티코어 시스템에서 CFS는 CPU 간 태스크를 균등하게 분배합니다.

부하 분산 트리거

load_balance 핵심 로직

/* kernel/sched/fair.c */
static int load_balance(int this_cpu, struct rq *this_rq,
                         struct sched_domain *sd,
                         enum cpu_idle_type idle)
{
    struct rq *busiest;
    struct lb_env env = {
        .sd             = sd,
        .dst_cpu        = this_cpu,
        .dst_rq         = this_rq,
        .idle           = idle,
    };

    /* 1. 가장 바쁜 CPU 찾기 */
    busiest = find_busiest_queue(&env);
    if (!busiest)
        goto out_balanced;

    /* 2. 이동할 태스크 선택 */
    env.src_cpu = busiest->cpu;
    env.src_rq = busiest;
    detach_tasks(&env);

    /* 3. 태스크 이동 */
    if (!list_empty(&env.tasks))
        attach_tasks(&env);

    return 1;

out_balanced:
    return 0;
}

Load Balancing 오버헤드

부하 분산은 성능 향상에 도움이 되지만, 과도한 분산은 오버헤드를 유발합니다:

  • 캐시 미스: 태스크 이동 시 L1/L2 캐시 무효화
  • Lock contention: 여러 CPU의 runqueue lock 경쟁
  • Migration cost: Context switch + TLB flush

고성능 애플리케이션은 taskset이나 CPU affinity 설정으로 부하 분산을 제한할 수 있습니다.

PELT (Per-Entity Load Tracking)

PELT는 각 태스크와 CPU의 부하를 시간 감쇠 평균으로 추적하는 메커니즘입니다.

PELT 메트릭

메트릭의미용도
util_avg평균 활용률 (0-1024)CPU 용량 대비 부하 계산
load_avg평균 부하 (가중치 포함)부하 분산 결정
runnable_avg실행 가능 시간 평균스케줄 지연 추정

감쇠 계산

/* kernel/sched/pelt.c */
/* 32ms 주기로 절반으로 감쇠 (half-life) */
#define LOAD_AVG_PERIOD 32
#define LOAD_AVG_MAX    47742

static void __update_load_avg_se(u64 now, struct sched_entity *se)
{
    u64 delta = now - se->avg.last_update_time;

    /* Geometric series로 감쇠 적용 */
    se->avg.load_sum = decay_load(se->avg.load_sum, delta);
    se->avg.util_sum = decay_load(se->avg.util_sum, delta);

    se->avg.load_avg = se->avg.load_sum / LOAD_AVG_MAX;
    se->avg.util_avg = se->avg.util_sum / LOAD_AVG_MAX;
}
PELT 부하 추적 감쇠 곡선 (Half-life = 32ms) 부하 (util_avg) 100% 75% 50% 25% 0% 시간 (ms) 32 64 96 128 160 192 태스크 실행 50% 25% 12.5% 32ms (half-life) • 태스크가 멈춘 후 32ms마다 부하가 절반으로 감소 • 과거 부하의 영향이 시간이 지나면서 기하급수적으로 감소 → 최근 행동에 더 높은 가중치
PELT는 32ms half-life로 부하를 추적하여, 최근 행동에 더 높은 가중치를 부여하고 오래된 데이터는 지수적으로 감쇠시킵니다.

📊 PELT 메트릭과 부하 분산

PELT (Per-Entity Load Tracking)는 각 태스크와 CPU의 부하를 추적하여 CFS의 부하 분산 결정에 사용됩니다:

  • util_avg: 평균 CPU 활용률 (0~1024). 이 값이 높으면 해당 태스크가 CPU 집약적입니다.
  • load_avg: nice 값으로 가중된 부하. 부하 분산 시 CPU 간 이동 우선순위 결정에 사용
  • runnable_avg: 실행 대기 중인 평균 시간. 런큐 대기 지연 추정에 사용

확인 방법: /proc/<pid>/sched에서 se.avg.util_avg, se.avg.load_avg 필드로 태스크별 PELT 메트릭 확인 가능

grep "avg\." /proc/self/sched
# se.avg.load_avg                  :          512
# se.avg.util_avg                  :          480
# se.avg.runnable_avg              :          490

스케줄러 클래스

CFS는 여러 스케줄러 클래스 중 하나입니다.

스케줄러 클래스 우선순위

순위클래스정책설명
1stop_sched_class-CPU 정지 태스크 (최고 우선순위)
2dl_sched_classSCHED_DEADLINEDeadline 스케줄링 (EDF)
3rt_sched_classSCHED_FIFO, SCHED_RR실시간 스케줄링
4fair_sched_classSCHED_NORMAL, SCHED_BATCHCFS (일반 프로세스)
5idle_sched_classSCHED_IDLE유휴 태스크

sched_class 구조체

/* kernel/sched/sched.h */
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 (*put_prev_task)(struct rq *rq, struct task_struct *p);
    void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
    /* ... */
};

const struct sched_class fair_sched_class = {
    .enqueue_task       = enqueue_task_fair,
    .dequeue_task       = dequeue_task_fair,
    .pick_next_task     = pick_next_task_fair,
    .task_tick          = task_tick_fair,
    /* ... */
};

CFS 그룹 스케줄링

그룹 스케줄링은 프로세스 그룹(cgroup) 단위로 CPU 시간을 공정하게 분배합니다.

그룹 스케줄링 구조

task_group (cgroup)
  ├─ cfs_rq[CPU0]  ← CPU0의 그룹 런큐
  │    ├─ Task A (vruntime=100)
  │    └─ Task B (vruntime=150)
  └─ cfs_rq[CPU1]  ← CPU1의 그룹 런큐
       └─ Task C (vruntime=80)

sched_entity (그룹 대표)
  ├─ vruntime: 그룹 전체의 가상 런타임
  └─ load: 그룹 내 태스크 부하 합

설정 방법

# cgroup v2에서 CPU 시간 제한
mkdir /sys/fs/cgroup/mygroup
echo "100000 1000000" > /sys/fs/cgroup/mygroup/cpu.max
# 100ms / 1000ms = 10% CPU 시간 제한

echo $$ > /sys/fs/cgroup/mygroup/cgroup.procs

선점 메커니즘

CFS는 다음 조건에서 현재 실행 중인 태스크를 선점합니다.

선점 조건

check_preempt_tick 구현

/* kernel/sched/fair.c */
static void check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
    unsigned long ideal_runtime, delta_exec;
    struct sched_entity *se;

    ideal_runtime = sched_slice(cfs_rq, curr);
    delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;

    /* 타임슬라이스 소진 */
    if (delta_exec > ideal_runtime) {
        resched_curr(rq_of(cfs_rq));
        return;
    }

    /* leftmost 태스크와 vruntime 차이 확인 */
    se = __pick_first_entity(cfs_rq);
    delta_exec = curr->vruntime - se->vruntime;

    if (delta_exec > sysctl_sched_wakeup_granularity)
        resched_curr(rq_of(cfs_rq));
}

모니터링

CFS 스케줄러의 동작을 관찰하고 디버깅하는 방법입니다.

🔍 스케줄러 성능 문제 진단 순서

애플리케이션 성능 저하 시 다음 순서로 CFS 관련 문제를 진단하세요:

  1. 컨텍스트 스위치 빈도 확인:
    vmstat 1
    # cs 열이 초당 수만~수십만이면 과도한 전환
  2. 런큐 길이 (load average) 확인:
    uptime
    # load average: 2.50, 3.10, 2.90
    # CPU 4코어 시스템에서 2.5 → 62% 활용 (정상)
  3. 프로세스별 CPU 시간 분포:
    top -H -p <pid>
    # TIME+ 열에서 스레드별 CPU 누적 시간 확인
  4. 비자발적 선점 횟수 (nr_involuntary_switches):
    grep involuntary /proc/<pid>/status
    # 높으면 타임슬라이스 소진으로 자주 선점됨
  5. CPU migration 횟수:
    grep nr_migrations /proc/<pid>/sched
    # 높으면 부하 분산으로 CPU 간 이동 빈번 (캐시 오염)

/proc 인터페이스

# 프로세스별 스케줄링 통계
cat /proc/1234/sched
# se.sum_exec_runtime    : 누적 실행 시간
# se.vruntime            : 가상 런타임
# se.nr_migrations       : CPU 이동 횟수
# nr_voluntary_switches  : 자발적 컨텍스트 스위치
# nr_involuntary_switches: 비자발적 컨텍스트 스위치

# 전체 스케줄러 디버그 정보
cat /proc/sched_debug
# CPU별 런큐 상태, 부하, 태스크 목록

schedstats 활성화

# CONFIG_SCHEDSTATS=y로 커널 빌드 시 사용 가능
cat /proc/schedstat

# 또는 동적 활성화 (커널 4.6+)
echo 1 > /proc/sys/kernel/sched_schedstats

perf 도구

# 스케줄링 이벤트 추적
perf sched record -- sleep 10
perf sched latency

# 출력 예:
#  Task                  |   Runtime ms  | Switches | Avg delay ms | Max delay ms |
#  my_app:1234           |    1245.123   |   512    |    0.123     |    12.456    |

성능 튜닝

CFS 파라미터를 조정하여 워크로드에 맞게 최적화할 수 있습니다.

튜닝 파라미터

파라미터경로효과
sched_latency_ns/proc/sys/kernel/sched_latency_ns↑ 처리량 증가, 지연 증가
sched_min_granularity_ns/proc/sys/kernel/sched_min_granularity_ns↓ 컨텍스트 스위치 감소
sched_wakeup_granularity_ns/proc/sys/kernel/sched_wakeup_granularity_ns↑ 대화형 성능 향상
sched_migration_cost_ns/proc/sys/kernel/sched_migration_cost_ns↑ 캐시 친화성 증가
sched_nr_migrate/proc/sys/kernel/sched_nr_migrate부하 분산 시 이동 태스크 수

워크로드별 권장 설정

대화형 데스크톱

# 낮은 지연, 빠른 응답
echo 2000000 > /proc/sys/kernel/sched_latency_ns          # 2ms
echo 500000 > /proc/sys/kernel/sched_min_granularity_ns   # 0.5ms
echo 500000 > /proc/sys/kernel/sched_wakeup_granularity_ns # 0.5ms

배치 처리 서버

# 높은 처리량, 지연 허용
echo 24000000 > /proc/sys/kernel/sched_latency_ns          # 24ms
echo 3000000 > /proc/sys/kernel/sched_min_granularity_ns   # 3ms
echo 4000000 > /proc/sys/kernel/sched_wakeup_granularity_ns # 4ms

CPU Affinity 설정

# taskset으로 CPU 고정 (부하 분산 비활성화)
taskset -c 0-3 ./my_app

# C API
#include <sched.h>
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);
sched_setaffinity(0, sizeof(cpuset), &cpuset);

💡 CPU Affinity와 CFS 상호작용

CPU affinity는 부하 분산과 상호작용하여 성능에 영향을 줍니다:

  • 캐시 친화성 향상: 태스크를 특정 CPU에 고정하면 L1/L2 캐시 히트율이 증가하여 메모리 접근 속도 향상
  • 부하 불균형 위험: 여러 태스크를 같은 CPU에 고정하면 다른 CPU가 유휴 상태여도 부하 분산이 불가능
  • NUMA 고려: 원격 NUMA 노드의 CPU에 고정하면 메모리 접근 지연 증가 (200ns → 100ns). numactl --cpunodebind로 로컬 노드 CPU만 사용 권장
  • 실시간 워크로드: 지연시간 민감 태스크는 전용 CPU에 고정하고 (isolcpus 커널 파라미터), 해당 CPU에서 일반 태스크 제외

권장 패턴: 일반 애플리케이션은 affinity 설정하지 않고 커널의 부하 분산에 맡기고, 고성능 컴퓨팅이나 실시간 시스템에서만 선택적으로 사용

CFS Bandwidth Control

cgroup을 통해 그룹의 CPU 사용량을 제한할 수 있습니다.

설정 예제

# cgroup v2
mkdir /sys/fs/cgroup/limited
echo "50000 100000" > /sys/fs/cgroup/limited/cpu.max
# quota=50ms, period=100ms → 50% CPU 제한

echo $$ > /sys/fs/cgroup/limited/cgroup.procs

# cgroup v1
echo 100000 > /sys/fs/cgroup/cpu/limited/cpu.cfs_period_us
echo 50000 > /sys/fs/cgroup/cpu/limited/cpu.cfs_quota_us

Throttling 주의사항

CFS bandwidth control이 활성화되면 그룹이 quota를 소진했을 때 throttling됩니다. 이는 지연 시간 민감 애플리케이션에 영향을 줄 수 있습니다:

  • /proc/<pid>/sched에서 nr_throttled 확인
  • /sys/fs/cgroup/cpu.stat에서 throttled_time 확인

🎯 CFS Bandwidth Control 효과적 사용법

CPU quota 설정은 멀티테넌트 환경이나 리소스 격리가 필요한 경우 유용합니다. 다음 원칙을 따르세요:

  • Period는 100ms 권장: 기본값 100ms(100000us)가 대부분 상황에 적합. 더 짧으면 throttling 오버헤드 증가, 더 길면 지연시간 증가
  • Quota는 여유있게: 평균 사용량의 120-150% 설정. 버스트 트래픽을 수용하면서 폭주 방지
    # 평균 30% CPU 사용 → 40-50% quota 설정
    echo "40000 100000" > cpu.max
  • 컨테이너 단위로 적용: 개별 프로세스보다 컨테이너/서비스 단위로 제한하면 관리 용이. Docker/Kubernetes는 --cpus 옵션으로 자동 설정
  • 모니터링 필수: cpu.statnr_throttled, throttled_time을 주기적으로 확인하여 quota 부족 감지
    while true; do
      grep throttled /sys/fs/cgroup/myapp/cpu.stat
      sleep 5
    done
  • 실시간 워크로드는 제외: SCHED_FIFO/SCHED_DEADLINE은 bandwidth control 영향받지 않으므로, 지연시간 민감 태스크는 RT 정책 사용

실제 사례: Kubernetes에서 requests.cpu=0.5 → 50ms quota, limits.cpu=2 → 200ms quota 매핑. limit 초과 시 throttling 발생

다른 스케줄러와 비교

항목CFSO(1)RT (FIFO/RR)SCHED_DEADLINE
용도일반 프로세스일반 프로세스 (구식)실시간 태스크Deadline 보장
알고리즘Fair sharing우선순위 큐고정 우선순위EDF (Earliest Deadline First)
복잡도O(log N)O(1)O(N)O(log N)
지연시간동적 (~ms)고정 (5-800ms)즉시Deadline 내 보장
공정성매우 높음중간없음 (우선순위)Deadline 기반

🎯 실시간 워크로드를 위한 대안

CFS는 공정성을 우선하므로, 지연시간 보장이 필요한 실시간 워크로드에는 적합하지 않습니다. 다음 대안을 고려하세요:

  • SCHED_FIFO / SCHED_RR: 고정 우선순위 실시간 스케줄링. 오디오/비디오 처리, 로봇 제어 등에 적합
    struct sched_param param = { .sched_priority = 50 };
    sched_setscheduler(0, SCHED_FIFO, &param);
  • SCHED_DEADLINE: 주기적 태스크의 데드라인 보장. 산업 제어, 통신 프로토콜 구현에 적합
    struct sched_attr attr = {
        .sched_policy = SCHED_DEADLINE,
        .sched_runtime  = 10 * 1000 * 1000,   /* 10ms */
        .sched_deadline = 30 * 1000 * 1000,   /* 30ms */
        .sched_period   = 30 * 1000 * 1000    /* 30ms */
    };
    sched_setattr(0, &attr, 0);
  • PREEMPT_RT 패치: 전체 커널을 선점 가능하게 만들어 지연시간을 마이크로초 단위로 감소 (산업용 Linux)
  • CPU 격리 (isolcpus): 특정 CPU를 일반 스케줄링에서 제외하여 전용 사용
    # 커널 부트 파라미터: isolcpus=2,3
    # CPU 2, 3은 CFS 부하 분산에서 제외됨

vruntime 타임라인 심화

vruntime은 CFS의 공정성을 보장하는 핵심 메트릭입니다. 이 섹션에서는 min_vruntime의 진행 방식, 새 태스크의 vruntime 초기화, 그리고 타임라인 전체를 시각적으로 추적합니다.

min_vruntime 진행

min_vruntime은 CFS 런큐에서 모든 태스크의 vruntime 중 최솟값을 단조 증가하며 추적하는 변수입니다. 새로 진입하는 태스크나 슬립 후 복귀하는 태스크의 vruntime 초기화 기준점 역할을 합니다.

/* kernel/sched/fair.c */
static void update_min_vruntime(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    struct rb_node *leftmost = rb_first_cached(&cfs_rq->tasks_timeline);
    u64 vruntime = cfs_rq->min_vruntime;

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

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

    /* min_vruntime은 단조 증가만 허용 */
    cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime);
}

새 태스크 vruntime 초기화

fork()로 생성된 새 태스크는 place_entity()에서 vruntime이 초기화됩니다. 기본적으로 min_vruntime을 기준으로 설정하되, sched_child_runs_first 옵션에 따라 부모/자식의 실행 순서가 결정됩니다.

/* kernel/sched/fair.c */
static void place_entity(struct cfs_rq *cfs_rq,
                          struct sched_entity *se, int initial)
{
    u64 vruntime = cfs_rq->min_vruntime;

    if (initial && sched_feat(START_DEBIT))
        /* 새 태스크에 반 주기만큼 패널티 부여 */
        vruntime += sched_vslice(cfs_rq, se);

    /* 슬립 복귀 시 보상: thresh만큼 뒤로 당김 */
    if (!initial) {
        unsigned long thresh = sysctl_sched_latency;
        if (sched_feat(GENTLE_FAIR_SLEEPERS))
            thresh >>= 1;  /* 절반만 보상 */
        vruntime -= thresh;
    }

    /* min_vruntime보다 뒤로 가지 않도록 보장 */
    se->vruntime = max_vruntime(se->vruntime, vruntime);
}
START_DEBIT 패널티: 새 태스크가 즉시 실행되면 기존 태스크의 공정성이 깨집니다. sched_vslice()는 해당 태스크가 한 번의 스케줄링 주기에서 소비할 vruntime을 예측하여 패널티로 부여합니다. 이로써 새 태스크는 기존 태스크들보다 약간 뒤에 위치하게 됩니다.

슬립/웨이크업 시 vruntime 보상

오랫동안 슬립한 태스크가 깨어났을 때, vruntime이 너무 작으면 다른 태스크를 장시간 선점할 수 있습니다. CFS는 GENTLE_FAIR_SLEEPERS 기능으로 이를 제한합니다.

시나리오vruntime 초기화설명
새 태스크 (fork)min_vruntime + sched_vslice()START_DEBIT 패널티 적용
슬립 복귀max(기존, min_vruntime - latency/2)GENTLE_FAIR_SLEEPERS 보상
CPU 마이그레이션원본 CPU의 min_vruntime 기준 보정CPU 간 vruntime 차이 보정
그룹 간 이동대상 cfs_rq의 min_vruntime 기준그룹 스케줄링 공정성 유지
vruntime 타임라인: min_vruntime 진행과 태스크 초기화 시간 vruntime min_vruntime Task A (nice 0) Task B (nice -5, 느림) Task C (nice +5, 빠름) fork() → 새 태스크 D vr = min_vr + vslice Task E 슬립 복귀 vr = min_vr - latency/2 vruntime 증가율 = NICE_0_LOAD / weight 높은 우선순위: 느리게 증가 → CPU 더 할당 낮은 우선순위: 빠르게 증가 → CPU 덜 할당 min_vruntime: 단조 증가 기준선
vruntime은 nice 값에 따라 다른 속도로 증가하며, min_vruntime은 항상 단조 증가하여 새 태스크의 초기화 기준점 역할을 합니다.
min_vruntime과 래핑: min_vruntime이 뒤로 가지 않도록 max_vruntime()으로 보호합니다. 만약 모든 태스크가 디큐되어 런큐가 비면, 다음에 인큐되는 태스크의 vruntime이 min_vruntime으로 설정되어 공정성이 유지됩니다.

그룹 스케줄링 심화

CFS 그룹 스케줄링은 task_group 계층 구조를 통해 CPU 시간을 계층적으로 분배합니다. 각 그룹은 독립적인 sched_entitycfs_rq를 가지며, 상위 그룹의 런큐에 스케줄링 엔티티로 참여합니다.

task_group 계층 구조

/* kernel/sched/sched.h */
struct task_group {
    struct cgroup_subsys_state css;

    /* 각 CPU별 스케줄링 엔티티와 CFS 런큐 */
    struct sched_entity  **se;     /* se[cpu] — 상위 cfs_rq에 참여 */
    struct cfs_rq        **cfs_rq; /* cfs_rq[cpu] — 그룹 내부 런큐 */

    unsigned long        shares;   /* cpu.shares (비율 기반 분배) */
    struct rcu_head       rcu;
    struct list_head      list;
    struct task_group     *parent;
    struct list_head      siblings;
    struct list_head      children;
};

shares 분배 알고리즘

cpu.shares 값은 형제 그룹 간의 상대적 CPU 시간 비율을 결정합니다. 기본값은 1024 (= NICE_0_LOAD)입니다.

/* 그룹 A: shares=2048, 그룹 B: shares=1024인 경우 */
/* 그룹 A는 그룹 B보다 2배의 CPU 시간을 할당받음 */

/* kernel/sched/fair.c — shares → weight 변환 */
static void update_cfs_group(struct sched_entity *se)
{
    struct cfs_rq *gcfs_rq = group_cfs_rq(se);
    unsigned long shares;

    if (!gcfs_rq)
        return;

    shares = calc_group_shares(gcfs_rq);
    reweight_entity(cfs_rq_of(se), se, shares);
}

/* shares 계산: 그룹 내 부하 비율에 따라 CPU별로 분배 */
static long calc_group_shares(struct cfs_rq *cfs_rq)
{
    long tg_weight, tg_shares, load, shares;
    struct task_group *tg = cfs_rq->tg;

    tg_shares = READ_ONCE(tg->shares);
    load = max(scale_load_down(cfs_rq->load.weight), cfs_rq->avg.load_avg);
    tg_weight = atomic_long_read(&tg->load_avg);

    shares = (tg_shares * load) / tg_weight;
    return clamp_t(long, shares, MIN_SHARES, tg_shares);
}
CFS 그룹 스케줄링 계층 구조 루트 cfs_rq (CPU 0) task_group: root 그룹 A (se) — shares=2048 CPU 시간: 2/3 (66.7%) 그룹 B (se) — shares=1024 CPU 시간: 1/3 (33.3%) 그룹 A의 cfs_rq (내부 런큐) Task P1 vr=100 Task P2 vr=150 Task P3 vr=200 그룹 B의 cfs_rq (내부 런큐) Task Q1 vr=80 Task Q2 vr=120 2단계 스케줄링 과정 1단계: 루트 cfs_rq에서 그룹 A vs 그룹 B의 sched_entity 비교 (shares 기반 vruntime) 2단계: 선택된 그룹의 내부 cfs_rq에서 개별 태스크 선택 (일반 vruntime 비교) 그룹 A (shares=2048): 5개 태스크가 있어도 전체 CPU의 66.7%만 사용 그룹 B (shares=1024): 2개 태스크가 전체 CPU의 33.3%를 나눠 사용 핵심: 그룹 내 태스크 수에 관계없이 shares 비율로 CPU 시간이 분배됨
그룹 스케줄링은 2단계로 동작합니다. 먼저 shares 비율로 그룹 간 CPU 시간을 분배하고, 그 다음 각 그룹 내부에서 일반 CFS 방식으로 태스크를 선택합니다.

cgroup v2에서의 그룹 스케줄링 설정

# 그룹 생성 및 shares 설정
mkdir -p /sys/fs/cgroup/webserver
mkdir -p /sys/fs/cgroup/batch

# cpu.weight (cgroup v2, 기본값 100)
echo 200 > /sys/fs/cgroup/webserver/cpu.weight  # 높은 비율
echo 50 > /sys/fs/cgroup/batch/cpu.weight      # 낮은 비율

# 프로세스 배치
echo $$ > /sys/fs/cgroup/webserver/cgroup.procs

# 현재 weight 확인
cat /sys/fs/cgroup/webserver/cpu.weight
# 200
cgroup v1 vs v2 차이: v1의 cpu.shares(기본 1024)는 v2에서 cpu.weight(기본 100)로 변경되었습니다. 내부 동작은 동일하지만, v2는 단일 계층 구조로 관리가 더 직관적입니다. weight = shares * 100 / 1024 공식으로 대략적 변환이 가능합니다.

EEVDF vs CFS 비교

Linux 6.6부터 CFS는 EEVDF(Earliest Eligible Virtual Deadline First) 알고리즘으로 대체되었습니다. EEVDF는 CFS의 공정성 원칙을 유지하면서 지연시간 보장을 추가합니다.

핵심 개념: Eligible과 Virtual Deadline

개념CFSEEVDF
선택 기준최소 vruntimeeligible 중 최소 virtual deadline
공정성vruntime 기반lag 기반 (더 정밀)
지연시간보장 없음O(1) 지연 보장
선점wakeup_granularity 기반deadline 비교
복잡도O(log N)O(log N)
구현 위치kernel/sched/fair.ckernel/sched/fair.c (동일)

Lag (지연) 기반 선택

EEVDF에서 lag은 태스크가 받아야 할 CPU 시간과 실제 받은 CPU 시간의 차이입니다.

/* kernel/sched/fair.c (Linux 6.6+) */
/* lag = 기대 서비스 시간 - 실제 서비스 시간 */
/* lag > 0: CPU를 덜 받은 태스크 (eligible) */
/* lag < 0: CPU를 더 받은 태스크 (not eligible) */

static void update_entity_lag(struct cfs_rq *cfs_rq,
                               struct sched_entity *se)
{
    s64 lag, limit;

    SCHED_WARN_ON(!se->on_rq);
    lag = avg_vruntime(cfs_rq) - se->vruntime;

    limit = calc_delta_fair(max_t(u64,
        2 * se->slice, TICK_NSEC), se);
    se->vlag = clamp(lag, -limit, limit);
}

/* Virtual Deadline 계산 */
/* deadline = vruntime + (slice / weight) * NICE_0_LOAD */
static u64 vruntime_deadline(struct sched_entity *se)
{
    return se->vruntime + calc_delta_fair(se->slice, se);
}

EEVDF 선택 과정

/* pick_eevdf(): eligible 태스크 중 가장 이른 deadline 선택 */
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);

        /* eligible 조건: vruntime <= avg_vruntime */
        if (entity_eligible(cfs_rq, se)) {
            /* deadline이 가장 이른 eligible 태스크 선택 */
            if (!best || deadline_gt(best, se))
                best = se;
            node = node->rb_left;
        } else {
            node = node->rb_right;
        }
    }
    return best;
}
EEVDF: Eligible 판정과 Virtual Deadline 선택 vruntime avg_vruntime (eligible 기준선) Eligible (lag > 0) Not Eligible (lag < 0) A vr=200 dl=350 B vr=300 dl=500 C vr=130 dl=280 ★ D vr=480 E vr=600 다음 실행! CFS vs EEVDF 선택 차이 CFS (Linux < 6.6): → 최소 vruntime 태스크 선택 (Task C, vr=130) 문제: 오래 슬립한 태스크가 큰 burst로 다른 태스크를 선점 EEVDF (Linux 6.6+): → eligible 태스크 중 최소 deadline 선택 (Task C, dl=280) 장점: deadline 기반으로 지연시간 O(1) 보장, sleeper 버스트 방지
EEVDF는 먼저 eligible(lag>0) 여부를 판별한 뒤, eligible 태스크 중 virtual deadline이 가장 이른 태스크를 선택합니다.
마이그레이션 가이드: Linux 6.6 이상에서는 EEVDF가 기본입니다. 기존 CFS 튜닝 파라미터 중 sched_wakeup_granularity_ns는 EEVDF에서 제거되었습니다. 대신 태스크의 slice 값이 deadline 계산에 사용되며, sched_attr.sched_runtime으로 제어할 수 있습니다.

EEVDF 실무 활용

EEVDF에서 태스크의 slice 값을 조정하면 지연시간 특성을 제어할 수 있습니다.

/* EEVDF에서 태스크 slice 설정 (Linux 6.6+) */
#include <sched.h>
#include <linux/sched.h>

struct sched_attr attr = {
    .size = sizeof(struct sched_attr),
    .sched_policy = SCHED_NORMAL,
    .sched_nice = 0,
    /* sched_runtime은 EEVDF의 slice로 해석됨 */
    .sched_runtime = 3000000,  /* 3ms slice → 짧은 deadline */
};

/* 짧은 slice = 짧은 deadline = 더 빠른 스케줄링 */
/* 긴 slice = 긴 deadline = 더 큰 타임슬라이스 */
sched_setattr(0, &attr, 0);
slice 값동작적합한 워크로드
1ms (작은 값)짧은 deadline, 빠른 스케줄링, 잦은 선점대화형 UI, 오디오 렌더링
3ms (기본)균형잡힌 deadline과 타임슬라이스일반 워크로드
10ms (큰 값)긴 deadline, 큰 타임슬라이스, 적은 선점컴파일, 배치 처리
100ms (매우 큰 값)거의 선점되지 않음, 최대 처리량HPC, 과학 컴퓨팅
EEVDF vs CFS 하위 호환성: EEVDF 도입 시 기존 CFS의 sched_latency_nssched_min_granularity_ns는 여전히 존재하지만, 선점 결정에서의 역할이 달라졌습니다. sched_wakeup_granularity_ns는 완전히 제거되어 /proc/sys에서 접근할 수 없습니다. EEVDF에서는 deadline 비교로 선점을 결정하므로 별도의 granularity가 불필요합니다.

CFS 대역폭 제어 심화

CFS Bandwidth Control은 cgroup 단위로 CPU 사용량을 하드 리밋합니다. quotaperiod 파라미터로 동작하며, quota가 소진되면 그룹 전체가 스로틀링됩니다.

대역폭 제어 핵심 구조체

/* kernel/sched/sched.h */
struct cfs_bandwidth {
    raw_spinlock_t   lock;
    ktime_t          period;         /* 주기 (기본 100ms) */
    u64              quota;          /* 주기당 허용 CPU 시간 */
    u64              runtime;        /* 남은 런타임 */
    u64              burst;          /* 버스트 허용량 (Linux 5.14+) */
    s64              hierarchical_quota;

    u8               idle;
    u8               period_active;
    struct hrtimer    period_timer;   /* 주기 리셋 타이머 */
    struct hrtimer    slack_timer;    /* 여유 런타임 회수 */

    struct list_head  throttled_cfs_rq;
    int              nr_periods;     /* 통계: 총 주기 수 */
    int              nr_throttled;   /* 통계: 스로틀 횟수 */
    u64              throttled_time; /* 통계: 스로틀 시간 합 */
};

스로틀링 메커니즘

/* kernel/sched/fair.c — quota 소진 시 스로틀링 */
static bool check_cfs_rq_runtime(struct cfs_rq *cfs_rq)
{
    if (!cfs_bandwidth_used())
        return false;

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

    /* 글로벌 풀에서 런타임 보충 시도 */
    if (cfs_rq_throttled(cfs_rq))
        return true;

    throttle_cfs_rq(cfs_rq);
    return true;
}

/* 스로틀 해제: period 타이머가 런타임 리필 */
static int do_sched_cfs_period_timer(struct cfs_bandwidth *cfs_b,
                                      int overrun)
{
    /* 새 period 시작: quota 리필 */
    cfs_b->runtime = cfs_b->quota;

    /* 스로틀된 cfs_rq에 런타임 분배 */
    while (throttled) {
        distribute_cfs_runtime(cfs_b);
        unthrottle_cfs_rq(cfs_rq);
    }
    return 0;
}
CFS 대역폭 제어: Quota/Period 스로틀링 사이클 시간 Period 1 (100ms) Period 2 (100ms) Period 3 (100ms) 실행 (50ms) 스로틀! (quota 소진) quota: 50ms → 0ms 리필! 실행 (50ms) 스로틀! 리필! 실행 (50ms) 스로틀! cpu.max = "50000 100000" (50ms/100ms = 50% CPU) 1. 각 period(100ms) 시작 시 quota(50ms) 리필 2. 태스크 실행 시 quota 소비 → 소진되면 스로틀 (다음 period까지 대기) 3. 멀티코어: quota는 코어 수 × 시간으로 소비 (2코어 25ms씩 = 50ms quota)
CFS bandwidth control은 period마다 quota를 리필하고, 소진되면 그룹을 스로틀하여 다음 period까지 실행을 중단시킵니다.

멀티코어 환경에서의 Quota 소비

CFS bandwidth의 quota는 모든 CPU에서 공유됩니다. 멀티코어에서 2개 스레드가 동시 실행되면 quota가 2배 속도로 소비됩니다.

# 예: cpu.max = "100000 100000" (100ms quota / 100ms period)
# 1 CPU 사용 시: 100ms 동안 실행 가능 → 100% CPU
# 2 CPU 사용 시: 50ms 동안 실행 → 각 CPU 50ms → 합계 100ms = 100% CPU
# 4 CPU 사용 시: 25ms 동안 실행 → 각 CPU 25ms → 합계 100ms = 100% CPU

# 4 CPU를 모두 활용하려면:
echo "400000 100000" > /sys/fs/cgroup/myapp/cpu.max
# quota=400ms → 4코어 × 100ms = 400% CPU (= 4코어 전부 사용 가능)
Kubernetes CPU 리밋 매핑: Kubernetes의 resources.limits.cpu: "2"는 내부적으로 cpu.max = "200000 100000"으로 변환됩니다. 이는 2코어 분량의 CPU 시간을 의미하며, 단일 코어에서 200ms 실행하거나 2코어에서 100ms씩 실행할 수 있습니다. CPU limit을 정수가 아닌 소수(예: 0.5)로 설정하면 "50000 100000"이 되어 반 코어 분량만 사용 가능합니다.

Burst 기능 (Linux 5.14+)

cpu.max.burst는 사용하지 않은 quota를 누적하여 일시적으로 제한을 초과할 수 있게 합니다.

# 버스트 설정 (미사용 quota를 최대 20ms까지 누적)
echo "50000 100000" > /sys/fs/cgroup/myapp/cpu.max
echo 20000 > /sys/fs/cgroup/myapp/cpu.max.burst

# 통계 확인
cat /sys/fs/cgroup/myapp/cpu.stat
# usage_usec 12345678
# nr_periods 1500
# nr_throttled 42
# throttled_usec 2100000
# nr_bursts 15
# burst_usec 150000

pick_next_task_fair() 코드 워크스루

pick_next_task_fair()는 CFS 스케줄러의 핵심 진입점으로, 다음에 실행할 태스크를 선택합니다. 이 함수는 최적화 경로(simple path)그룹 스케줄링 경로를 분리하여 성능을 극대화합니다.

전체 흐름

/* kernel/sched/fair.c */
static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev,
                    struct rq_flags *rf)
{
    struct cfs_rq *cfs_rq = &rq->cfs;
    struct sched_entity *se;
    struct task_struct *p;
    int new_tasks;

again:
    if (!sched_fair_runnable(rq))
        goto idle;

#ifdef CONFIG_FAIR_GROUP_SCHED
    if (!prev || prev->sched_class != &fair_sched_class)
        goto simple;

    /* 이전 태스크가 CFS였으면 put_prev 처리 */
    do {
        struct sched_entity *curr = cfs_rq->curr;
        if (curr && curr->on_rq)
            update_curr(cfs_rq);
        else
            curr = NULL;

        if (unlikely(check_cfs_rq_runtime(cfs_rq))) {
            cfs_rq = &rq->cfs;
            if (!cfs_rq->nr_running)
                goto idle;
            goto simple;
        }
    } while (cfs_rq);

    p = task_of(se);

simple:
#endif
    if (prev)
        put_prev_task(rq, prev);

    /* rb_first_cached: O(1)으로 leftmost 노드 접근 */
    do {
        se = pick_next_entity(cfs_rq);
        set_next_entity(cfs_rq, se);
        cfs_rq = group_cfs_rq(se);
    } while (cfs_rq);

    p = task_of(se);

done: __maybe_unused;
    return p;

idle:
    /* CFS 태스크 없음 → 다른 클래스 시도 또는 idle */
    new_tasks = newidle_balance(rq, rf);
    if (new_tasks > 0)
        goto again;

    return NULL;
}

skip/next/last 힌트

pick_next_entity()에서는 leftmost 외에 skip, next, last 힌트를 통해 선택을 조정합니다.

/* kernel/sched/fair.c */
static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
    struct sched_entity *left = __pick_first_entity(cfs_rq);
    struct sched_entity *se;

    /* leftmost가 기본 후보 */
    se = left;

    /* skip: 특정 엔티티를 건너뛰라는 힌트 */
    if (cfs_rq->skip == se) {
        struct sched_entity *second = __pick_next_entity(se);
        if (second && wakeup_preempt_entity(second, left) < 1)
            se = second;
    }

    /* last: 마지막으로 깨운 엔티티 (캐시 친화성) */
    if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1)
        se = cfs_rq->last;

    /* next: wakeup으로 다음에 실행해야 할 엔티티 */
    if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1)
        se = cfs_rq->next;

    clear_buddies(cfs_rq, se);
    return se;
}
rb_first_cached: Linux 커널의 rb_first_cached()는 RB-tree의 leftmost 노드를 캐싱하여 O(1)에 접근합니다. enqueue_entity()에서 삽입 시 leftmost 여부를 판별하여 캐시를 갱신합니다. 따라서 pick_next_entity()의 실질적 시간 복잡도는 O(1)입니다 (트리 탐색 없이 즉시 접근).
pick_next_task_fair() 실행 흐름 pick_next_task_fair() nr_running > 0? (CFS 태스크 존재?) NO newidle_balance() 다른 CPU에서 pull YES put_prev_task(rq, prev) pick_next_entity(cfs_rq) leftmost + skip/next/last 힌트 선택 힌트 skip: yield 호출 시 건너뜀 next: wakeup preemption 대상 last: 마지막 깨운 태스크 group_cfs_rq? (그룹 엔티티?) YES 하위 cfs_rq로 재탐색 (반복) NO (개별 태스크) set_next_entity(se) return task_of(se)
pick_next_task_fair()는 CFS 런큐에서 leftmost 엔티티를 선택하고, 그룹 스케줄링인 경우 하위 cfs_rq를 반복 탐색하여 최종 태스크를 결정합니다.

set_next_entity

/* 선택된 엔티티를 현재 실행 엔티티로 설정 */
static void set_next_entity(struct cfs_rq *cfs_rq,
                            struct sched_entity *se)
{
    /* RB-tree에서 제거 (현재 실행 중이므로 트리에 없어도 됨) */
    if (se->on_rq) {
        __dequeue_entity(cfs_rq, se);
        update_load_avg(cfs_rq, se, UPDATE_TG);
    }

    update_stats_curr_start(cfs_rq, se);
    cfs_rq->curr = se;

    se->prev_sum_exec_runtime = se->sum_exec_runtime;
}

로드 밸런싱 심화

멀티코어 시스템에서 CFS의 로드 밸런싱은 sched_domain 계층 구조를 통해 단계적으로 수행됩니다. CPU 토폴로지(SMT, MC, NUMA)에 따라 밸런싱 주기와 비용이 달라집니다.

sched_domain 계층

/* include/linux/sched/topology.h */
struct sched_domain {
    struct sched_domain *parent;   /* 상위 도메인 */
    struct sched_domain *child;    /* 하위 도메인 */
    struct sched_group *groups;    /* 도메인 내 CPU 그룹 */

    unsigned long min_interval;    /* 최소 밸런싱 간격 */
    unsigned long max_interval;    /* 최대 밸런싱 간격 */
    unsigned int  busy_factor;     /* 바쁠 때 간격 확장 배율 */
    unsigned int  imbalance_pct;   /* 불균형 임계값 (%) */
    unsigned int  cache_nice_tries;
    unsigned long flags;
    int           level;           /* 도메인 레벨 */
    char          *name;           /* SMT, MC, NUMA 등 */
};
sched_domain 계층: NUMA > MC > SMT NUMA 도메인 (Level 2) 밸런싱 간격: 64~128ms, imbalance_pct: 125% MC 도메인 (Node 0, Level 1) 밸런싱 간격: 4~8ms MC 도메인 (Node 1, Level 1) 밸런싱 간격: 4~8ms SMT (Core 0) 간격: 1~2ms SMT (Core 1) 간격: 1~2ms SMT (Core 2) 간격: 1~2ms SMT (Core 3) 간격: 1~2ms CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7 Pull Migration (유휴 CPU가 바쁜 CPU에서 태스크 가져옴) 밸런싱 원칙 1. 하위 도메인부터 상위로 단계적 밸런싱 (SMT → MC → NUMA) 2. 같은 도메인 내 이동 비용이 낮으므로 먼저 시도 (캐시 보존) 3. NUMA 간 이동은 비용이 높으므로 imbalance_pct 임계값을 초과해야 발생
sched_domain 계층은 CPU 토폴로지를 반영하여 SMT(하이퍼스레드) → MC(멀티코어) → NUMA 순으로 단계적으로 로드 밸런싱을 수행합니다.

Pull vs Push 마이그레이션

유형트리거동작함수
PullCPU가 유휴 상태 진입바쁜 CPU에서 태스크 가져옴newidle_balance()
Pull주기적 밸런싱 타이머가장 바쁜 그룹에서 태스크 이동load_balance()
Push태스크 wake-up최적 CPU 선택하여 배치select_task_rq_fair()
Push태스크 fork/exec부하 낮은 CPU로 배치wake_up_new_task()

select_task_rq_fair: 최적 CPU 선택

/* kernel/sched/fair.c */
static int select_task_rq_fair(struct task_struct *p,
                               int prev_cpu, int wake_flags)
{
    int new_cpu = prev_cpu;
    int want_affine = 0;
    struct sched_domain *sd;

    /* 1. 친화성: 깨운 CPU와 같은 도메인 선호 */
    if (wake_flags & WF_TTWU) {
        int waker_cpu = smp_processor_id();
        want_affine = cpumask_test_cpu(waker_cpu,
                                         p->cpus_ptr);
    }

    /* 2. sched_domain 탐색: 가장 에너지 효율적인 CPU */
    for_each_domain(prev_cpu, sd) {
        if (want_affine && cpumask_test_cpu(prev_cpu,
                              sched_domain_span(sd)))
            break;
    }

    /* 3. 유휴 CPU가 있으면 우선 선택 */
    new_cpu = find_idlest_cpu(sd, p, prev_cpu, new_cpu);

    return new_cpu;
}
밸런싱 비용: NUMA 노드 간 마이그레이션은 LLC(Last-Level Cache) 전체 무효화와 원격 메모리 접근 지연(100-300ns)을 동반합니다. /proc/sys/kernel/sched_migration_cost_ns(기본 500us)를 높이면 마이그레이션 빈도가 줄어 캐시 친화성이 향상되지만, 부하 불균형 기간이 길어질 수 있습니다.

Energy Aware Scheduling (EAS)

ARM big.LITTLE 같은 이기종 CPU 시스템에서 CFS는 EAS(Energy Aware Scheduling)를 통해 성능과 전력 소비를 최적화합니다.

/* kernel/sched/fair.c — EAS CPU 선택 */
static int find_energy_efficient_cpu(struct task_struct *p,
                                     int prev_cpu)
{
    unsigned long best_delta = ULONG_MAX;
    int best_cpu = -1;
    struct perf_domain *pd;

    /* 각 성능 도메인(big/LITTLE)별로 에너지 계산 */
    for_each_perf_domain(pd) {
        unsigned long cur_delta;
        int cpu;

        for_each_cpu(cpu, perf_domain_span(pd)) {
            /* 태스크 배치 시 에너지 증분 계산 */
            cur_delta = compute_energy(p, cpu, pd);
            if (cur_delta < best_delta) {
                best_delta = cur_delta;
                best_cpu = cpu;
            }
        }
    }
    return best_cpu;
}
시나리오EAS 비활성EAS 활성
경량 태스크 (10% CPU)아무 유휴 CPU 배치LITTLE 코어 우선 배치 (저전력)
중량 태스크 (80% CPU)아무 유휴 CPU 배치big 코어 배치 (고성능)
혼합 워크로드부하 균등 분배에너지 최적 배치
# EAS 활성화 확인
cat /proc/sys/kernel/sched_energy_aware
# 1 (활성), 0 (비활성)

# 성능 도메인 확인
ls /sys/devices/system/cpu/cpu*/cpufreq/
# 각 CPU의 주파수 도메인 확인

# EAS 비활성화 (서버에서는 불필요)
echo 0 > /proc/sys/kernel/sched_energy_aware
EAS 활성화 조건: EAS는 다음 조건을 모두 만족해야 동작합니다: (1) CONFIG_ENERGY_MODEL=y 빌드, (2) Energy Model 등록 (cpufreq 드라이버가 EM 제공), (3) sched_domain이 Overutilized가 아닐 것, (4) sched_energy_aware=1. x86 서버에서는 일반적으로 비활성이며, ARM 모바일/임베디드에서 주로 사용됩니다.

sched_entity/cfs_rq 구조체 분석

CFS 스케줄러의 내부 동작을 이해하려면 sched_entitycfs_rq 구조체의 필드별 역할과 상호 관계를 파악해야 합니다.

sched_entity 상세 분석

/* include/linux/sched.h (Linux 6.x) */
struct sched_entity {
    /* === 부하/가중치 === */
    struct load_weight      load;          /* nice → weight 변환값 */
    struct sched_avg         avg;           /* PELT 메트릭 */

    /* === RB-tree 관련 === */
    struct rb_node           run_node;      /* RB-tree 노드 */
    unsigned int            on_rq;         /* 런큐에 있는지 여부 */

    /* === 실행 시간 추적 === */
    u64                     exec_start;    /* 현재 스케줄 시작 시각 */
    u64                     sum_exec_runtime; /* 누적 실행 시간 */
    u64                     prev_sum_exec_runtime;
    u64                     vruntime;      /* 가상 런타임 */

    /* === EEVDF 관련 (6.6+) === */
    s64                     vlag;          /* 지연 (lag) */
    u64                     deadline;      /* 가상 데드라인 */
    u64                     slice;         /* 요청 슬라이스 */

    /* === 그룹 스케줄링 === */
    struct sched_entity     *parent;       /* 상위 그룹 엔티티 */
    struct cfs_rq           *cfs_rq;       /* 소속 런큐 */
    struct cfs_rq           *my_q;         /* 그룹이면 자식 런큐 */

    int                     depth;         /* 그룹 계층 깊이 */
};

cfs_rq 구조체 분석

/* kernel/sched/sched.h */
struct cfs_rq {
    /* === 부하 정보 === */
    struct load_weight   load;          /* 런큐 내 총 가중치 */
    unsigned int        nr_running;     /* 실행 가능 태스크 수 */
    unsigned int        h_nr_running;   /* 계층적 총 태스크 수 */

    /* === 실행 시간 === */
    u64                 exec_clock;     /* 런큐 누적 실행 시간 */
    u64                 min_vruntime;   /* 최소 vruntime 추적 */

    /* === RB-tree === */
    struct rb_root_cached tasks_timeline; /* 태스크 트리 */

    /* === 현재 실행 엔티티 === */
    struct sched_entity  *curr;          /* 현재 실행 중 */
    struct sched_entity  *next;          /* 다음 실행 힌트 */
    struct sched_entity  *last;          /* 마지막 깨운 엔티티 */
    struct sched_entity  *skip;          /* 건너뛸 엔티티 */

    /* === PELT === */
    struct sched_avg     avg;

    /* === Bandwidth Control === */
    int                 runtime_enabled;
    s64                 runtime_remaining; /* 남은 quota */
    int                 throttled;         /* 스로틀 여부 */
    int                 throttle_count;

    /* === 그룹 스케줄링 === */
    struct task_group    *tg;             /* 소속 task_group */
};

Weight/Load 계산 체계

sched_entity ↔ cfs_rq 관계와 Weight 전파 cfs_rq (CPU 0 루트 런큐) load.weight = 3072 (se1+se2+se3) nr_running = 3 min_vruntime = 50000 tasks_timeline (RB-tree) se1 se2 se3 sched_entity (se1) load.weight = 1024 (nice 0) vruntime = 50000 avg.util_avg = 512 avg.load_avg = 480 on_rq = 1 cfs_rq → 루트 cfs_rq my_q → NULL (개별 태스크) sched_entity (se2) load.weight = 2048 (nice -5) vruntime = 52000 avg.util_avg = 768 avg.load_avg = 900 on_rq = 1 cfs_rq → 루트 cfs_rq my_q → NULL (개별 태스크) load_weight 구조 struct load_weight { unsigned long weight; // nice → sched_prio_to_weight[] u32 inv_weight; // 2^32 / weight (나눗셈 최적화) }; inv_weight로 곱셈+시프트로 나눗셈을 대체하여 성능 최적화
sched_entity는 cfs_rq의 RB-tree에 삽입되며, load_weight의 inv_weight 필드는 비용이 큰 나눗셈 연산을 곱셈+시프트로 대체합니다.
inv_weight 최적화: calc_delta_fair()에서 delta * NICE_0_LOAD / weight 나눗셈을 delta * NICE_0_LOAD * inv_weight >> 32 곱셈+시프트로 변환합니다. 이는 스케줄러 hot path에서 수십 ns의 성능 차이를 만듭니다.

enqueue/dequeue 흐름

태스크가 실행 가능 상태가 되면 enqueue_entity()로 CFS 런큐에 삽입되고, 슬립하거나 종료하면 dequeue_entity()로 제거됩니다.

/* kernel/sched/fair.c — 엔큐 핵심 로직 */
static void enqueue_entity(struct cfs_rq *cfs_rq,
                           struct sched_entity *se, int flags)
{
    bool renorm = !(flags & ENQUEUE_WAKEUP) ||
                  (flags & ENQUEUE_MIGRATED);

    /* 1. vruntime 정규화 (마이그레이션 시) */
    if (renorm && curr)
        se->vruntime += cfs_rq->min_vruntime;

    /* 2. PELT 부하 업데이트 */
    update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH);
    update_cfs_group(se);

    /* 3. 부하 가중치 업데이트 */
    account_entity_enqueue(cfs_rq, se);

    /* 4. vruntime 초기화 (새 태스크/슬립 복귀) */
    if (flags & ENQUEUE_WAKEUP)
        place_entity(cfs_rq, se, 0);

    /* 5. RB-tree에 삽입 */
    if (se != cfs_rq->curr)
        __enqueue_entity(cfs_rq, se);
    se->on_rq = 1;
}

/* RB-tree 삽입: vruntime을 키로 정렬 */
static void __enqueue_entity(struct cfs_rq *cfs_rq,
                              struct sched_entity *se)
{
    struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node;
    struct rb_node *parent = NULL;
    struct sched_entity *entry;
    bool leftmost = true;

    while (*link) {
        parent = *link;
        entry = rb_entry(parent, struct sched_entity, run_node);
        if (entity_before(se, entry)) {
            link = &parent->rb_left;
        } else {
            link = &parent->rb_right;
            leftmost = false; /* leftmost 캐시 갱신 */
        }
    }

    rb_link_node(&se->run_node, parent, link);
    rb_insert_color_cached(&se->run_node,
                           &cfs_rq->tasks_timeline, leftmost);
}
/* 디큐 핵심 로직 */
static void dequeue_entity(struct cfs_rq *cfs_rq,
                           struct sched_entity *se, int flags)
{
    /* 1. vruntime 업데이트 */
    update_curr(cfs_rq);

    /* 2. PELT 부하 업데이트 */
    update_load_avg(cfs_rq, se, UPDATE_TG);

    /* 3. 부하 가중치 제거 */
    account_entity_dequeue(cfs_rq, se);

    /* 4. RB-tree에서 제거 */
    if (se != cfs_rq->curr)
        __dequeue_entity(cfs_rq, se);
    se->on_rq = 0;

    /* 5. vruntime 비정규화 (마이그레이션 대비) */
    if (!(flags & DEQUEUE_SLEEP))
        se->vruntime -= cfs_rq->min_vruntime;

    update_min_vruntime(cfs_rq);
    update_cfs_group(se);
}
vruntime 정규화/비정규화: CPU 간 마이그레이션 시 vruntime은 상대적 값으로 변환(비정규화)되어 전송되고, 도착 CPU에서 해당 CPU의 min_vruntime을 기준으로 다시 절대값(정규화)됩니다. 이로써 서로 다른 CPU의 min_vruntime 차이에 의한 불공정을 방지합니다.

ftrace/perf/bpftrace 스케줄러 분석

CFS 스케줄러의 동작을 실시간으로 분석하는 세 가지 핵심 도구를 다룹니다.

ftrace sched 이벤트

ftrace의 sched 관련 tracepoint를 활용하여 스케줄링 이벤트를 추적합니다.

# 사용 가능한 sched 이벤트 확인
ls /sys/kernel/debug/tracing/events/sched/
# sched_switch        — 컨텍스트 스위치
# sched_wakeup        — 태스크 wake-up
# sched_wakeup_new    — 새 태스크 wake-up
# sched_migrate_task  — CPU 마이그레이션
# sched_stat_runtime  — 실행 시간 통계
# sched_stat_wait     — 런큐 대기 시간

# sched_switch 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
cat /sys/kernel/debug/tracing/trace_pipe | head -20

# 출력 예:
# <idle>-0 [002] 1234.567: sched_switch:
#   prev_comm=swapper/2 prev_pid=0 prev_state=R ==>
#   next_comm=my_app next_pid=1234 next_prio=120

# 특정 PID만 필터링
echo 'common_pid == 1234' > /sys/kernel/debug/tracing/events/sched/sched_switch/filter

# 스케줄 지연 측정 (wakeup → switch 시간)
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_stat_wait/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_stat_runtime/enable

perf sched 분석

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

# 태스크별 지연시간 분석
perf sched latency --sort max
# ─────────────────────────────────────────────────────
# Task            │ Runtime ms │ Switches │ Max delay ms
# ─────────────────────────────────────────────────────
# my_app:1234     │   3456.12  │   2048   │    15.234
# worker:5678     │    123.45  │    512   │     3.456

# CPU별 타임라인 시각화
perf sched timehist --summary
# ─────────────────────────────────────────────────────
# CPU 0: busy 78.5%  idle 21.5%  migrations: 42
# CPU 1: busy 65.2%  idle 34.8%  migrations: 38

# 마이그레이션 추적
perf sched map

bpftrace 런큐 모니터링

# 런큐 대기 시간 히스토그램
bpftrace -e '
tracepoint:sched:sched_switch
{
    @runq_lat[args->next_comm] = hist(nsecs - @start[args->next_pid]);
    @start[args->next_pid] = nsecs;
}

tracepoint:sched:sched_wakeup,
tracepoint:sched:sched_wakeup_new
{
    @start[args->pid] = nsecs;
}

END { clear(@start); }
'

# CFS vruntime 분포 추적 (kprobe)
bpftrace -e '
kprobe:update_curr
{
    $se = (struct sched_entity *)arg1;
    @vruntime = hist($se->vruntime);
}
'

# 컨텍스트 스위치 빈도 (초당)
bpftrace -e '
tracepoint:sched:sched_switch
{
    @switches = count();
}

interval:s:1
{
    printf("cs/s: %d\n", @switches);
    clear(@switches);
}
'

# CPU 마이그레이션 추적
bpftrace -e '
tracepoint:sched:sched_migrate_task
{
    printf("%s[%d] CPU %d -> %d\n",
        args->comm, args->pid,
        args->orig_cpu, args->dest_cpu);
}
'
schedstat 확인: /proc/schedstat에서 CPU별 스케줄링 통계를 확인할 수 있습니다. 각 줄은 cpu<N> yld_count sched_count sched_goidle ttwu_count ttwu_local 형식입니다. sched_goidle이 높으면 해당 CPU가 자주 유휴 상태에 빠지고 있어 부하 불균형을 의심할 수 있습니다.

스케줄러 디버깅 체크리스트

CFS 관련 성능 문제 발생 시 다음 순서로 조사합니다.

단계확인 항목도구정상 기준
1런큐 길이vmstat 1 (r 컬럼)CPU 수 이하
2컨텍스트 스위치vmstat 1 (cs 컬럼)수천~수만/s
3비자발적 선점/proc/PID/status낮을수록 좋음
4마이그레이션 횟수/proc/PID/sched캐시 민감 작업은 0 권장
5스케줄 지연perf sched latency수 ms 이하
6스로틀링cpu.stat (nr_throttled)0이면 정상
7NUMA 미스/proc/vmstat (numa_miss)numa_hit의 5% 이하
8PELT 부하/proc/PID/sched (avg)워크로드 기대치와 일치
CFS 스케줄러 관측 도구 체계 CFS 스케줄러 kernel/sched/fair.c /proc 인터페이스 /proc/PID/sched (태스크 통계) /proc/sched_debug (런큐 상태) /proc/schedstat (CPU별 통계) ftrace Tracepoints sched_switch (컨텍스트 스위치) sched_wakeup (태스크 깨우기) sched_migrate_task (마이그레이션) perf sched perf sched record (기록) perf sched latency (지연 분석) perf sched timehist (타임라인) bpftrace / BPF 런큐 지연 히스토그램 vruntime 분포 추적 kprobe 기반 실시간 분석 cgroup 통계 cpu.stat (스로틀링 횟수/시간) cpu.pressure (PSI 지표) 일반 모니터링: /proc + vmstat → 심층 분석: perf sched → 실시간 커스텀: bpftrace → 지속 관측: cgroup cpu.stat
CFS 스케줄러 분석 도구는 간단한 /proc 확인부터 bpftrace 실시간 추적까지 단계적으로 활용합니다.

PSI (Pressure Stall Information)

Linux 4.20+에서 도입된 PSI는 CPU/메모리/IO 자원 부족으로 태스크가 대기한 시간 비율을 제공합니다.

# 시스템 전체 CPU 압력
cat /proc/pressure/cpu
# some avg10=2.45 avg60=3.12 avg300=2.89 total=12345678
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0

# cgroup별 CPU 압력
cat /sys/fs/cgroup/myapp/cpu.pressure
# some avg10=15.23 avg60=12.45 avg300=10.89 total=98765432

# 해석:
# some: 하나 이상의 태스크가 CPU를 기다리는 시간 비율 (%)
# full: 모든 태스크가 CPU를 기다리는 시간 비율 (CPU는 some만)
# avg10/60/300: 10초/60초/300초 이동 평균

# PSI 기반 알람 설정 (5초간 CPU 압력 25% 초과 시)
echo "some 250000 5000000" > /proc/pressure/cpu
# poll()로 감시 가능 (systemd-oomd, cgroup PSI 모니터링 등)

sysctl 튜닝 파라미터 상세

CFS의 동작을 세밀하게 조정하는 모든 sysctl 파라미터를 정리합니다. 각 파라미터의 기본값, 효과, 워크로드별 권장값을 포함합니다.

핵심 CFS 파라미터

파라미터기본값범위효과
sched_latency_ns6,000,000 (6ms)100,000~1,000,000,000한 스케줄링 주기 (모든 태스크 1회 실행). 증가 시 처리량 증가, 응답 지연 증가
sched_min_granularity_ns750,000 (0.75ms)100,000~1,000,000,000최소 타임슬라이스. 감소 시 공정성 향상, 컨텍스트 스위치 증가
sched_wakeup_granularity_ns1,000,000 (1ms)0~1,000,000,000wake-up 선점 임계값. 감소 시 대화형 응답 향상, 불필요한 선점 증가
sched_migration_cost_ns500,000 (0.5ms)0~100,000,000마이그레이션 비용 추정. 증가 시 캐시 친화성 향상, 부하 분산 지연
sched_nr_migrate320~65535한 번의 밸런싱에서 이동할 최대 태스크 수
sched_child_runs_first00 또는 11이면 fork() 시 자식이 먼저 실행 (COW 최적화)
sched_tunable_scaling10, 1, 2CPU 수에 따라 latency 자동 스케일링 (0=비활성, 1=log2, 2=선형)

워크로드별 튜닝 프로파일

## 프로파일 1: 대화형 데스크톱 (최소 지연)
sysctl -w kernel.sched_latency_ns=3000000           # 3ms
sysctl -w kernel.sched_min_granularity_ns=300000    # 0.3ms
sysctl -w kernel.sched_wakeup_granularity_ns=500000 # 0.5ms
sysctl -w kernel.sched_migration_cost_ns=250000    # 0.25ms

## 프로파일 2: 처리량 최적화 서버 (HPC/배치)
sysctl -w kernel.sched_latency_ns=24000000          # 24ms
sysctl -w kernel.sched_min_granularity_ns=3000000   # 3ms
sysctl -w kernel.sched_wakeup_granularity_ns=4000000 # 4ms
sysctl -w kernel.sched_migration_cost_ns=5000000   # 5ms
sysctl -w kernel.sched_nr_migrate=128               # 대량 마이그레이션

## 프로파일 3: 데이터베이스 서버 (균형)
sysctl -w kernel.sched_latency_ns=12000000          # 12ms
sysctl -w kernel.sched_min_granularity_ns=1500000   # 1.5ms
sysctl -w kernel.sched_wakeup_granularity_ns=2000000 # 2ms
sysctl -w kernel.sched_migration_cost_ns=1000000   # 1ms

## 프로파일 4: 가상화 호스트 (VM 간 공정성)
sysctl -w kernel.sched_latency_ns=10000000          # 10ms
sysctl -w kernel.sched_min_granularity_ns=2000000   # 2ms
sysctl -w kernel.sched_migration_cost_ns=500000    # 0.5ms
CFS 튜닝 트레이드오프 sched_latency_ns 증가 → 낮은 지연 높은 처리량 2-4ms (데스크톱) 12-24ms (서버) sched_min_granularity_ns 증가 → 정밀한 공정성 적은 CS 오버헤드 0.3-0.5ms 2-3ms sched_wakeup_granularity_ns 증가 → 빠른 wakeup 응답 적은 불필요 선점 0.5ms (대화형) 4ms (배치) sched_migration_cost_ns 증가 → 빠른 부하 분산 높은 캐시 친화성 0.25ms 5ms 대화형/데스크톱 프로파일 모든 값 최소 → 빠른 응답, 높은 CS 비용 적합: GUI, 게임, 실시간 오디오 처리량 최적화/서버 프로파일 모든 값 최대 → 높은 처리량, 긴 응답 지연 적합: HPC, 배치, 대규모 병렬 컴퓨팅, 웹서버
CFS 파라미터는 응답 지연과 처리량 사이의 트레이드오프를 조절합니다. 워크로드 특성에 맞는 프로파일을 선택하세요.

sched_autogroup 자동 그룹화

데스크톱 환경에서 CONFIG_SCHED_AUTOGROUP은 TTY 세션별로 자동 task_group을 생성하여, 빌드 같은 CPU 집약 작업이 대화형 세션에 미치는 영향을 최소화합니다.

# autogroup 활성화 여부 확인
cat /proc/sys/kernel/sched_autogroup_enabled
# 1

# 프로세스의 autogroup nice 조정
echo 10 > /proc/self/autogroup  # 현재 세션 그룹 우선순위 낮춤

# autogroup 비활성화 (서버 환경 권장)
sysctl -w kernel.sched_autogroup_enabled=0
튜닝 주의사항: CFS 파라미터 변경은 시스템 전체에 영향을 줍니다. 변경 전 기존 값을 기록하고, 변경 후 perf sched latency로 지연시간 변화를 측정하세요. sched_min_granularity_ns를 너무 낮추면 컨텍스트 스위치 오버헤드가 실행 시간보다 커질 수 있습니다.

sched_tunable_scaling 자동 스케일링

sched_tunable_scaling은 CPU 수가 증가할 때 CFS 파라미터를 자동으로 조정합니다.

모드스케일링 공식설명
0비활성고정값 사용수동 설정된 값 그대로 사용
1log2기본값 × (1 + log2(N))기본값. CPU 수 증가에 따라 완만하게 증가
2선형기본값 × NCPU 수에 비례하여 증가 (대규모 시스템)
# 현재 스케일링 모드 확인
cat /proc/sys/kernel/sched_tunable_scaling
# 1 (log2 모드)

# 64코어 시스템에서의 자동 스케일링 예:
# log2(64) = 6, factor = 1 + 6 = 7
# sched_latency = 6ms × 7 = 42ms
# sched_min_granularity = 0.75ms × 7 = 5.25ms

# 수동 튜닝 시 자동 스케일링 비활성화 권장
echo 0 > /proc/sys/kernel/sched_tunable_scaling

NUMA 밸런싱

CFS의 NUMA 밸런싱은 태스크의 메모리 접근 패턴을 분석하여 태스크를 가장 많이 접근하는 메모리가 있는 NUMA 노드로 자동 마이그레이션합니다.

자동 페이지 마이그레이션 원리

커널은 주기적으로 태스크의 페이지 테이블 엔트리에서 접근 비트(Accessed bit)를 클리어하여, 이후 접근 시 페이지 폴트를 유발합니다. 이 폴트 핸들러에서 접근 통계(numa_faults)를 수집합니다.

/* kernel/sched/fair.c */
struct task_struct {
    /* ... */
    int                      numa_scan_seq;
    unsigned int             numa_scan_period;    /* 스캔 주기 (ms) */
    unsigned int             numa_scan_period_max;
    int                      numa_preferred_nid;  /* 선호 NUMA 노드 */
    unsigned long            numa_migrate_retry;
    u64                      node_stamp;

    /* NUMA 폴트 통계: [노드][CPU/MEM][PRIVATE/SHARED] */
    unsigned long            *numa_faults;
    unsigned long            total_numa_faults;
    struct numa_group        *numa_group;
};

/* NUMA 폴트 핸들러 */
static void task_numa_fault(int last_cpupid,
                            int mem_node, int pages, int flags)
{
    struct task_struct *p = current;
    int cpu_node = task_node(p);
    int priv;

    /* 로컬 vs 원격 폴트 구분 */
    priv = (cpu_node == mem_node);

    /* numa_faults 통계 업데이트 */
    p->numa_faults[task_faults_idx(NUMA_MEM, mem_node, priv)] += pages;
    p->numa_faults[task_faults_idx(NUMA_CPU, cpu_node, priv)] += pages;

    /* 선호 노드 재계산 */
    task_numa_placement(p);
}

NUMA 밸런싱 결정 과정

/* kernel/sched/fair.c — 선호 노드 결정 */
static void task_numa_placement(struct task_struct *p)
{
    int nid, max_nid = NUMA_NO_NODE;
    unsigned long max_faults = 0;

    /* 각 노드별 폴트 수 비교 */
    for_each_online_node(nid) {
        unsigned long faults;
        faults = p->numa_faults[task_faults_idx(NUMA_MEM, nid, 0)]
               + p->numa_faults[task_faults_idx(NUMA_MEM, nid, 1)];

        if (faults > max_faults) {
            max_faults = faults;
            max_nid = nid;
        }
    }

    /* 선호 노드가 변경되면 마이그레이션 트리거 */
    if (max_nid != p->numa_preferred_nid) {
        p->numa_preferred_nid = max_nid;
        p->numa_migrate_retry = jiffies + HZ;
    }
}
NUMA 자동 밸런싱: 폴트 기반 태스크 마이그레이션 NUMA Node 0 로컬 메모리 접근: ~100ns CPU0 CPU1 Task P CPU0에서 실행 numa_faults[P] Node 0: 120 (30%) Node 1: 280 (70%) ★ → 선호 노드: Node 1 NUMA Node 1 로컬 메모리 접근: ~100ns CPU2 CPU3 원격 접근 ~300ns (비효율!) 태스크 마이그레이션 numa_faults 기반 → Node 1 선호 Task P (마이그레이션 후) NUMA 밸런싱 3단계 과정 1. 페이지 테이블 접근 비트 클리어 → 다음 접근 시 NUMA 힌트 폴트 발생 2. 폴트 핸들러에서 접근 노드 기록 (numa_faults[MEM][nid] 증가) 3. 폴트가 많은 노드 = numa_preferred_nid → 해당 노드 CPU로 태스크 마이그레이션 + 페이지 마이그레이션
NUMA 밸런싱은 페이지 폴트를 의도적으로 유발하여 메모리 접근 패턴을 수집하고, 가장 많이 접근하는 노드로 태스크와 페이지를 마이그레이션합니다.

NUMA 밸런싱 제어

# NUMA 밸런싱 활성화/비활성화
echo 1 > /proc/sys/kernel/numa_balancing          # 활성화
echo 0 > /proc/sys/kernel/numa_balancing          # 비활성화

# 스캔 주기 (ms) — 폴트 빈도 조절
cat /proc/sys/kernel/numa_balancing_scan_period_min_ms  # 기본: 1000
cat /proc/sys/kernel/numa_balancing_scan_period_max_ms  # 기본: 60000

# 한 번에 스캔할 페이지 크기 (MB)
cat /proc/sys/kernel/numa_balancing_scan_size_mb        # 기본: 256

# NUMA 통계 확인
numastat -p 1234
# Per-node process memory usage (in MBs)
# Node 0: 512.00    Node 1: 1024.00

# NUMA 폴트 카운터 확인
grep numa /proc/vmstat
# numa_hit 15234567
# numa_miss 234567
# numa_foreign 234567
# numa_interleave 12345
# numa_local 14567890
# numa_other 456789
NUMA 밸런싱 비활성화 시나리오: 다음 경우에는 NUMA 밸런싱을 비활성화하는 것이 유리합니다:
  • numactl --membind로 메모리 정책을 명시적으로 설정한 경우
  • 가상화 환경에서 게스트 OS가 자체 NUMA 밸런싱을 수행하는 경우
  • 메모리 크기가 작아 NUMA 힌트 폴트 오버헤드가 마이그레이션 이득보다 큰 경우
  • 접근 패턴이 균일하여 특정 노드 선호가 없는 경우

numa_group: 그룹 단위 마이그레이션

공유 메모리를 사용하는 태스크들은 numa_group으로 자동 묶여, 그룹 단위로 최적 노드를 결정합니다. 이는 멀티스레드 애플리케이션의 NUMA 성능을 크게 개선합니다.

/* kernel/sched/fair.c */
struct numa_group {
    refcount_t       refcount;
    spinlock_t       lock;
    int              nr_tasks;
    pid_t            gid;           /* 그룹 대표 PID */
    int              active_nodes;  /* 사용 중인 노드 수 */

    /* 그룹 전체의 NUMA 폴트 통계 */
    unsigned long    total_faults;
    unsigned long    max_faults_cpu;
    unsigned long    faults[];      /* [노드 수 * 2] */
};

/* 같은 페이지에 접근하는 태스크를 자동으로 그룹화 */
static void task_numa_group(struct task_struct *p,
                            int cpupid, int flags,
                            int *priv)
{
    struct task_struct *grp_leader = find_task_by_vpid(cpupid);
    if (grp_leader && grp_leader->numa_group) {
        /* 같은 페이지에 접근 → 그룹 합류 */
        do_numa_group_merge(p, grp_leader->numa_group);
    }
}
성능 검증: NUMA 밸런싱 효과를 측정하려면 perf stat -e 'sched:sched_move_numa' -a sleep 60으로 마이그레이션 횟수를, numastat -m으로 노드별 메모리 분포를 확인하세요. numa_miss 대비 numa_hit 비율이 높아져야 효과가 있습니다.

NUMA 밸런싱 파라미터 종합

파라미터경로기본값설명
numa_balancing/proc/sys/kernel/1NUMA 밸런싱 전체 활성화/비활성화
numa_balancing_scan_period_min_ms/proc/sys/kernel/1000최소 스캔 주기 (ms). 작을수록 빠른 감지, 높은 오버헤드
numa_balancing_scan_period_max_ms/proc/sys/kernel/60000최대 스캔 주기 (ms). 접근 패턴 안정 시 자동 증가
numa_balancing_scan_delay_ms/proc/sys/kernel/1000태스크 시작 후 첫 스캔까지 지연
numa_balancing_scan_size_mb/proc/sys/kernel/256한 번에 스캔할 메모리 크기 (MB)

NUMA 밸런싱 실전 사례

# 사례 1: 데이터베이스 서버 — NUMA 밸런싱 최적화
# 빠른 감지 + 작은 스캔 크기 (오버헤드 최소화)
echo 500 > /proc/sys/kernel/numa_balancing_scan_period_min_ms
echo 30000 > /proc/sys/kernel/numa_balancing_scan_period_max_ms
echo 128 > /proc/sys/kernel/numa_balancing_scan_size_mb

# 사례 2: JVM 애플리케이션 — 힙 크기가 큰 경우
# 큰 스캔 크기로 빠른 수렴
echo 2000 > /proc/sys/kernel/numa_balancing_scan_period_min_ms
echo 60000 > /proc/sys/kernel/numa_balancing_scan_period_max_ms
echo 512 > /proc/sys/kernel/numa_balancing_scan_size_mb

# 사례 3: 명시적 NUMA 바인딩 사용 시 — 비활성화
numactl --cpunodebind=0 --membind=0 ./my_app
# 이 경우 자동 밸런싱은 불필요 (명시적 정책 우선)
echo 0 > /proc/sys/kernel/numa_balancing

# NUMA 효과 전후 비교
perf stat -e 'node-loads,node-load-misses,node-stores,node-store-misses' ./benchmark
# node-load-misses가 감소하면 NUMA 밸런싱 효과 있음

# 실시간 NUMA 마이그레이션 모니터링
watch -n 5 'grep -E "numa_(hit|miss|foreign)" /proc/vmstat'
# numa_hit이 증가하고 numa_miss가 감소하면 수렴 중
NUMA 힌트 폴트 오버헤드: NUMA 밸런싱은 의도적으로 페이지 폴트를 유발하므로 오버헤드가 존재합니다. 메모리 사용량이 적은 태스크(수십 MB 이하)에서는 마이그레이션 이득보다 폴트 처리 비용이 클 수 있습니다. numa_balancing_scan_period_min_ms를 높이거나, 해당 태스크에 numactl --interleave를 사용하여 균등 분배하는 것이 효율적입니다.