리눅스 실시간 스케줄링과 CFS CPU 자원 분배

SCHED_FIFO, SCHED_RR, CFS의 동작 방식과 우선순위 역전, CPU Affinity를 실무 관점에서 정리합니다.

2026-08-14 · 최초 발행 2026-01-25

데드라인이 결과를 바꾸는 작업과 일반 작업의 공존

프로세서 스케줄링은 응답성과 처리량을 좌우한다. 시간 제약이 있는 작업에는 예측 가능한 실행 순서가 필요하고, 다중 사용자 환경에서는 특정 작업이나 그룹이 CPU를 계속 점유하지 않도록 분배 기준도 필요하다.

리눅스에서는 SCHED_FIFO와 SCHED_RR이 실시간 작업을 다루며, 일반 프로세스는 CFS의 공정 분배 방식으로 실행된다.

데드라인 위반의 영향에 따라 달라지는 실시간 시스템

실시간 시스템은 데드라인을 넘겼을 때 결과가 얼마나 심각하게 달라지는지에 따라 구분한다.

실시간 시스템Hard Real-TimeFirm Real-TimeSoft Real-Time데드라인 위반 시스템 실패(예: 항공기 제어, 의료 장비)데드라인 위반 결과 무용(예: 비디오 프레임 처리)데드라인 위반 품질 저하(예: 멀티미디어 스트리밍)

리눅스의 스케줄링 클래스는 우선순위에 따라 다음과 같이 구성된다.

스케줄링 클래스 우선순위 범위 용도 선점 가능
SCHED_DEADLINE 가장 높음 EDF 기반 실시간 불가
SCHED_FIFO 1-99 (실시간) 선입선출 실시간 불가
SCHED_RR 1-99 (실시간) 라운드 로빈 실시간 타임 슬라이스
SCHED_NORMAL (CFS) 100-139 (nice -20~19) 일반 프로세스 가능
SCHED_BATCH 100-139 배치 작업 가능
SCHED_IDLE 가장 낮음 유휴 시간 작업 가능

SCHED_FIFO가 실행권을 넘기는 시점

SCHED_FIFO는 실행 가능한 작업 중 우선순위가 가장 높은 프로세스를 선택한다. 선택된 프로세스는 스스로 양보하거나 자신보다 높은 우선순위의 프로세스가 실행 가능 상태가 될 때까지 계속 실행한다.

Process P3(Priority 90)Process P2(Priority 80)Process P1(Priority 90)CPUProcess P3(Priority 90)Process P2(Priority 80)Process P1(Priority 90)CPU초기 상태: P1 실행 중우선순위 낮음, 대기실행 중 (선점 불가)실행 준비 (우선순위 80)실행 준비 (우선순위 90)선점 (동일 우선순위는 FIFO)실행 시작I/O 대기 (자발적 양보)재개완료실행 시작

프로세스 정책과 우선순위를 설정할 때는 허용 범위를 확인한 뒤 sched_setscheduler를 호출한다.

#include <sched.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>

int set_fifo_scheduling(pid_t pid, int priority) {
    struct sched_param param;

    // 우선순위 범위 확인 (1-99)
    int min_priority = sched_get_priority_min(SCHED_FIFO);
    int max_priority = sched_get_priority_max(SCHED_FIFO);

    printf("FIFO 우선순위 범위: %d - %d\n", min_priority, max_priority);

    if (priority < min_priority || priority > max_priority) {
        fprintf(stderr, "잘못된 우선순위: %d\n", priority);
        return -1;
    }

    param.sched_priority = priority;

    // SCHED_FIFO 정책 설정 (root 권한 필요)
    if (sched_setscheduler(pid, SCHED_FIFO, &param) == -1) {
        perror("sched_setscheduler");
        return -1;
    }

    printf("프로세스 %d를 SCHED_FIFO (우선순위 %d)로 설정\n", pid, priority);
    return 0;
}

// 실시간 프로세스 예시
void realtime_task() {
    // 현재 프로세스를 SCHED_FIFO로 설정
    set_fifo_scheduling(0, 80);  // 0 = 현재 프로세스

    while (1) {
        // 시간 제약적 작업 수행
        perform_critical_operation();

        // 명시적 양보 (같은 우선순위의 다른 프로세스에게)
        sched_yield();
    }
}

FIFO 정책에서는 양보하지 않는 고우선순위 작업이 CPU를 장기간 점유할 수 있다. 다음 예시는 최고 우선순위 스레드가 무한 루프에 머물 때의 위험을 보여 준다.

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* cpu_hog_thread(void* arg) {
    struct sched_param param;
    param.sched_priority = 99;  // 최고 우선순위

    pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);

    printf("CPU 독점 스레드 시작 (우선순위 99)\n");

    // 무한 루프 - 다른 모든 프로세스 차단
    while (1) {
        // sched_yield() 없음 = CPU 영구 독점
        // 시스템이 응답 불가 상태가 될 수 있음
    }

    return NULL;
}

// 해결책: RT Throttling 설정 (리눅스 커널)
// /proc/sys/kernel/sched_rt_runtime_us = 950000 (95%)
// /proc/sys/kernel/sched_rt_period_us = 1000000 (1초)
// 실시간 프로세스가 CPU의 95%만 사용하도록 제한

같은 우선순위의 작업을 교대시키는 SCHED_RR

SCHED_RR은 실시간 우선순위 체계를 사용하지만, 같은 우선순위에 있는 프로세스에는 타임 슬라이스를 배정한다. 따라서 동일 우선순위 작업이 실행 시간을 나누어 사용할 수 있다.

000000000000000000000000000실행 대기 대기 실행 대기 실행 실행 대기 실행 대기 Process AProcess BProcess CSCHED_RR 타임 슬라이스 할당 (우선순위 80)

다음 코드는 현재 타임 슬라이스를 확인하고, 동일한 우선순위의 센서 및 제어 스레드에 SCHED_RR을 적용하는 예시다.

#include <sched.h>
#include <stdio.h>
#include <time.h>

void configure_round_robin() {
    struct sched_param param;
    struct timespec ts;

    // SCHED_RR로 설정
    param.sched_priority = 50;
    sched_setscheduler(0, SCHED_RR, &param);

    // 현재 타임 슬라이스 확인
    sched_rr_get_interval(0, &ts);
    printf("RR 타임 슬라이스: %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);

    // 일반적으로 100ms (커널 설정에 따라 다름)
}

// 멀티스레드 실시간 애플리케이션 예시
void* sensor_thread(void* arg) {
    struct sched_param param = {.sched_priority = 70};
    pthread_setschedparam(pthread_self(), SCHED_RR, &param);

    while (1) {
        read_sensor_data();
        // 타임 슬라이스 내에 작업 완료 가능
        usleep(50000);  // 50ms
    }
}

void* control_thread(void* arg) {
    struct sched_param param = {.sched_priority = 70};
    pthread_setschedparam(pthread_self(), SCHED_RR, &param);

    while (1) {
        update_control_output();
        usleep(50000);
    }
}

FIFO와 RR의 차이는 같은 우선순위에서 실행권을 넘기는 방식에 있다.

import os
import sched
import time

class RealtimeSchedulerDemo:
    """FIFO와 RR 동작 비교"""

    @staticmethod
    def fifo_behavior():
        """SCHED_FIFO: 동일 우선순위에서 양보 없음"""
        tasks = [
            {'name': 'TaskA', 'priority': 80, 'runtime': 300},
            {'name': 'TaskB', 'priority': 80, 'runtime': 200},
            {'name': 'TaskC', 'priority': 80, 'runtime': 100},
        ]

        print("SCHED_FIFO 실행 순서 (도착 순):")
        total_time = 0
        for task in tasks:
            start = total_time
            total_time += task['runtime']
            print(f"  {task['name']}: {start}ms - {total_time}ms")

        # 출력:
        # TaskA: 0ms - 300ms
        # TaskB: 300ms - 500ms
        # TaskC: 500ms - 600ms

    @staticmethod
    def rr_behavior(time_slice=100):
        """SCHED_RR: 타임 슬라이스로 교대 실행"""
        tasks = [
            {'name': 'TaskA', 'remaining': 300},
            {'name': 'TaskB', 'remaining': 200},
            {'name': 'TaskC', 'remaining': 100},
        ]

        print(f"\nSCHED_RR 실행 순서 (타임 슬라이스: {time_slice}ms):")
        current_time = 0

        while any(t['remaining'] > 0 for t in tasks):
            for task in tasks:
                if task['remaining'] > 0:
                    exec_time = min(time_slice, task['remaining'])
                    print(f"  {current_time}ms: {task['name']} 실행 ({exec_time}ms)")
                    task['remaining'] -= exec_time
                    current_time += exec_time

        # 출력:
        # 0ms: TaskA 실행 (100ms)
        # 100ms: TaskB 실행 (100ms)
        # 200ms: TaskC 실행 (100ms)
        # 300ms: TaskA 실행 (100ms)
        # 400ms: TaskB 실행 (100ms)
        # 500ms: TaskA 실행 (100ms)

RealtimeSchedulerDemo.fifo_behavior()
RealtimeSchedulerDemo.rr_behavior()

CFS가 가상 실행 시간을 사용하는 이유

CFS는 이상적인 멀티태스킹 환경에서 각 프로세스가 받아야 할 CPU 시간을 기준으로, 실제 실행 시간과의 차이를 줄이려 한다. 실행 가능 프로세스는 가상 실행 시간인 vruntime에 따라 관리되며, 값이 가장 작은 항목이 다음 실행 대상으로 선택된다.

모든 실행 가능 프로세스Virtual Runtime 계산Red-Black Tree에 삽입최소 vruntime 노드 선택프로세스 실행vruntime 업데이트

nice 값은 가중치에 반영되고, 그 가중치는 vruntime 증가량에 영향을 준다.

// 커널 코드 단순화 버전
struct sched_entity {
    u64 vruntime;           // 가상 실행 시간
    u64 sum_exec_runtime;   // 누적 실행 시간
    struct load_weight load; // 가중치
};

// vruntime 계산 (nice 값 반영)
u64 calculate_vruntime(u64 delta_exec, struct load_weight *load) {
    // delta_exec: 실제 실행 시간
    // load->weight: nice 값에 따른 가중치

    // nice 0 (기본): weight = 1024
    // nice -20 (최고 우선순위): weight = 88761
    // nice 19 (최저 우선순위): weight = 15

    return delta_exec * NICE_0_LOAD / load->weight;
}

// nice 값에 따른 가중치 테이블
const int prio_to_weight[40] = {
    /* -20 */ 88761, 71755, 56483, 46273, 36291,
    /* -15 */ 29154, 23254, 18705, 14949, 11916,
    /* -10 */  9548,  7620,  6100,  4904,  3906,
    /*  -5 */  3121,  2501,  1991,  1586,  1277,
    /*   0 */  1024,   820,   655,   526,   423,
    /*   5 */   335,   272,   215,   172,   137,
    /*  10 */   110,    87,    70,    56,    45,
    /*  15 */    36,    29,    23,    18,    15,
};

다음 시뮬레이터는 vruntime이 작은 프로세스를 먼저 꺼내고, 실행 후 다시 실행 큐에 넣는 흐름을 단순화해 보여 준다.

import heapq
from dataclasses import dataclass

@dataclass
class Process:
    pid: int
    nice: int
    vruntime: float = 0.0
    exec_time: float = 0.0

    def __lt__(self, other):
        return self.vruntime < other.vruntime

    def weight(self):
        """nice 값에서 가중치 계산 (간소화)"""
        return 1024 * (1.25 ** -self.nice)

    def update_vruntime(self, delta_exec):
        """실제 실행 시간을 가상 실행 시간으로 변환"""
        self.exec_time += delta_exec
        # nice 0: vruntime == exec_time
        # nice -10: vruntime < exec_time (우선순위 높음)
        # nice 10: vruntime > exec_time (우선순위 낮음)
        self.vruntime += delta_exec * (1024 / self.weight())

class CFSSimulator:
    def __init__(self):
        self.runqueue = []  # min-heap (Red-Black Tree 대신)

    def enqueue(self, process):
        heapq.heappush(self.runqueue, process)

    def pick_next_task(self):
        """vruntime이 가장 작은 프로세스 선택"""
        if self.runqueue:
            return heapq.heappop(self.runqueue)
        return None

    def schedule(self, time_slice=10):
        """스케줄링 시뮬레이션"""
        total_time = 0

        while self.runqueue:
            task = self.pick_next_task()
            if task is None:
                break

            print(f"T={total_time}ms: PID {task.pid} (nice {task.nice:+d}) "
                  f"vruntime={task.vruntime:.2f} 실행")

            task.update_vruntime(time_slice)
            total_time += time_slice

            # 다시 큐에 삽입 (실제로는 실행 완료 조건 필요)
            if total_time < 100:  # 시뮬레이션 종료 조건
                self.enqueue(task)

# 시뮬레이션 실행
scheduler = CFSSimulator()
scheduler.enqueue(Process(pid=100, nice=0))   # 기본 우선순위
scheduler.enqueue(Process(pid=101, nice=-5))  # 높은 우선순위
scheduler.enqueue(Process(pid=102, nice=5))   # 낮은 우선순위

scheduler.schedule()

# 출력 예시:
# T=0ms: PID 100 (nice +0) vruntime=0.00 실행
# T=10ms: PID 101 (nice -5) vruntime=0.00 실행
# T=20ms: PID 102 (nice +5) vruntime=0.00 실행
# T=30ms: PID 101 (nice -5) vruntime=8.02 실행  <- 가장 빨리 재실행
# T=40ms: PID 100 (nice +0) vruntime=10.00 실행
# ...

cgroup을 사용하면 개별 프로세스뿐 아니라 그룹 단위의 CPU 시간 분배도 다룰 수 있다.

# cgroup을 통한 CPU 공정 분배
# 사용자별 CPU 시간 공정 분배

# 그룹 생성
mkdir /sys/fs/cgroup/cpu/user1
mkdir /sys/fs/cgroup/cpu/user2

# CPU 공유 비율 설정 (기본값: 1024)
echo 2048 > /sys/fs/cgroup/cpu/user1/cpu.shares  # 2배 우선순위
echo 1024 > /sys/fs/cgroup/cpu/user2/cpu.shares  # 기본 우선순위

# 프로세스를 그룹에 할당
echo $PID1 > /sys/fs/cgroup/cpu/user1/cgroup.procs
echo $PID2 > /sys/fs/cgroup/cpu/user2/cgroup.procs

# 결과: user1의 모든 프로세스가 user2의 2배 CPU 시간 확보

락 대기에서 발생하는 우선순위 역전

고우선순위 작업이 락을 기다리고, 그 락을 저우선순위 작업이 보유하면 우선순위 역전이 발생할 수 있다. Priority Inheritance Protocol은 락 보유 스레드가 대기 중인 고우선순위 스레드의 우선순위를 일시적으로 상속하도록 구성하는 방식이다.

#include <pthread.h>
#include <stdio.h>

pthread_mutex_t resource_lock;

void* high_priority_task(void* arg) {
    struct sched_param param = {.sched_priority = 90};
    pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);

    printf("고우선순위 작업: 락 대기\n");
    pthread_mutex_lock(&resource_lock);  // 저우선순위가 점유 중
    // Priority Inversion 발생!
    printf("고우선순위 작업: 락 획득, 실행\n");
    pthread_mutex_unlock(&resource_lock);
    return NULL;
}

void* low_priority_task(void* arg) {
    struct sched_param param = {.sched_priority = 10};
    pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);

    pthread_mutex_lock(&resource_lock);
    printf("저우선순위 작업: 락 획득, 장시간 실행\n");
    sleep(5);  // 고우선순위 작업이 대기
    pthread_mutex_unlock(&resource_lock);
    return NULL;
}

// 해결책: Priority Inheritance Protocol
void configure_priority_inheritance() {
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);

    // 락을 점유한 저우선순위 스레드가 일시적으로
    // 대기 중인 고우선순위 스레드의 우선순위를 상속
    pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
    pthread_mutex_init(&resource_lock, &attr);
}

멀티코어에서 CPU Affinity와 NUMA를 고려하는 방식

멀티코어 환경에서는 특정 프로세스를 특정 CPU에 고정하는 CPU Affinity, 코어 간 부하를 나누는 Load Balancing, 메모리 지역성을 고려하는 NUMA Awareness가 함께 작동한다.

멀티코어 스케줄링CPU AffinityLoad BalancingNUMA Awareness특정 프로세스를특정 CPU에 고정코어 부하 균등 분배메모리 지역성 고려스케줄링캐시 히트율 향상CPU 유휴 시간 최소화메모리 접근 지연 감소

실시간 작업과 일반 작업을 CPU 구획으로 나누는 경우에는 스레드의 Affinity와 스케줄링 정책을 함께 지정할 수 있다.

#define _GNU_SOURCE
#include <sched.h>
#include <pthread.h>

void set_cpu_affinity(int cpu_id) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(cpu_id, &cpuset);

    // 현재 스레드를 특정 CPU에 바인딩
    pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);

    printf("스레드를 CPU %d에 고정\n", cpu_id);
}

// 실시간 작업: CPU 0-3 전용
// 일반 작업: CPU 4-7 전용
void* realtime_worker(void* arg) {
    set_cpu_affinity(0);  // CPU 0에 고정

    struct sched_param param = {.sched_priority = 80};
    pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);

    // 실시간 작업 수행
    while (1) {
        process_realtime_data();
    }
}

워크로드 특성에 맞춘 정책 선택

워크로드 유형 권장 정책 우선순위/Nice 이유
오디오/비디오 처리 SCHED_FIFO 70-80 일정한 지연 시간 필요
네트워크 패킷 처리 SCHED_RR 60-70 공평한 처리 + 실시간성
백그라운드 인덱싱 SCHED_BATCH nice 15-19 CPU 양보, 저전력
대화형 GUI SCHED_NORMAL nice -5-0 빠른 응답, 공정성
과학 계산 SCHED_NORMAL nice 5-10 CPU 집중, 우선순위 낮음

현재 정책과 실행 큐 상태는 다음 명령으로 확인할 수 있다.

# 프로세스별 스케줄링 정책 확인
chrt -p <PID>

# 모든 프로세스의 우선순위 확인
ps -eo pid,class,rtprio,ni,comm

# 실시간 스케줄러 통계
cat /proc/sys/kernel/sched_rt_runtime_us
cat /proc/sys/kernel/sched_rt_period_us

# CPU별 실행 큐 상태 (디버그 옵션 필요)
cat /proc/sched_debug

# 프로세스별 스케줄링 통계
cat /proc/<PID>/sched

실시간 정책은 시간 제약적 작업의 실행을 예측 가능하게 만들지만, 우선순위와 양보 지점을 잘못 설정하면 시스템 응답성 전체에 영향을 준다. CFS는 가상 실행 시간을 통해 일반 작업의 CPU 기회를 조정한다. 락 경합, CPU 독점, 코어 및 메모리 지역성까지 포함해 워크로드에 맞는 정책을 선택해야 한다.

리눅스프로세스 스케줄링실시간 시스템CFS운영체제