Real-Time Linux (PREEMPT_RT)

PREEMPT_RT는 Linux 커널을 낮은 지터의 실시간 시스템으로 운용하기 위한 핵심 기반입니다. 이 문서는 스핀락의 rtmutex 전환, IRQ 스레딩, 우선순위 상속, SCHED_FIFO/RR/DEADLINE 운용, 고해상도 타이머와 주기 태스크 설계, cyclictest/ftrace 계측, CPU 격리와 운영 안정성 점검 포인트까지 상세히 설명합니다.

전제 조건: 프로세스 스케줄러프로세스 문서를 먼저 읽으세요. 실행 단위 관리 주제는 태스크 상태 전이와 큐 정책이 핵심이므로, 스케줄링 기준과 wakeup 경로를 먼저 이해해야 합니다.
일상 비유: 이 주제는 작업장 인력 배치와 호출 순서 관리와 비슷합니다. 긴급 작업, 대기열, 우선순위를 함께 조정하듯이 커널도 태스크 분류와 깨우기 정책이 전체 반응성을 결정합니다.

핵심 요약

  • Threaded IRQ — 인터럽트를 스레드화해 우선순위 제어 가능
  • RT Mutex — priority inheritance로 inversion 완화
  • Preemptible kernel — 비선점 구간 최소화
  • Latency 측정 — cyclictest/ftrace 기반 검증 필수
  • CPU isolation — 실시간 태스크 전용 코어 분리

단계별 이해

  1. 기준 측정
    기존 커널에서 레이턴시 baseline을 먼저 측정합니다.
  2. RT 커널 구성
    PREEMPT_RT 설정과 IRQ 스레딩을 활성화합니다.
  3. 정책 튜닝
    스케줄링 클래스와 CPU 격리, 메모리 잠금 정책을 조정합니다.
  4. 회귀 검증
    stress + trace 기반으로 worst-case latency를 반복 확인합니다.
관련 문서: 프로세스 스케줄러 (실시간 스케줄링), 인터럽트 (인터럽트 처리), 타이머 (고해상도 타이머), cpusets & CPU Isolation (CPU 격리)

개요

Real-Time Linux는 시간 제약이 있는 작업(산업 제어, 로봇, 오디오/비디오, 통신)을 위해 결정론적 응답 시간을 제공합니다.

하드웨어 IRQ 상위 절반(hardirq) 일반 커널 softirq/tasklet 경유 PREEMPT_RT IRQ Thread로 전환 우선순위 스케줄링 결정론적 레이턴시

실시간 시스템 특성

구분 설명
Hard Real-Time Deadline 위반 시 시스템 실패 (예: ABS 브레이크, 의료기기)
Soft Real-Time Deadline 위반 시 성능 저하 (예: 비디오 스트리밍, VoIP)
Firm Real-Time Deadline 위반 결과는 무용지물 (예: 주식 거래)

Preemption 모드

모드 설명 레이턴시
PREEMPT_NONE 서버/배치 워크로드. 명시적 preemption point만 ~수 ms
PREEMPT_VOLUNTARY 데스크탑. 추가 preemption point ~수백 μs
PREEMPT 저지연. 커널 대부분이 선점 가능 ~수십 μs
PREEMPT_RT Hard Real-Time. 인터럽트도 선점 가능 ~수 μs

PREEMPT_RT 패치셋

주요 기능

PREEMPT_RT 설치

# 1. RT 패치 다운로드
$ wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/6.6/patch-6.6-rt15.patch.xz
$ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz

# 2. 커널 소스 압축 해제 및 패치 적용
$ tar xf linux-6.6.tar.xz
$ cd linux-6.6
$ xzcat ../patch-6.6-rt15.patch.xz | patch -p1

# 3. 커널 설정
$ make menuconfig
# General setup → Preemption Model → Fully Preemptible Kernel (Real-Time)

# 4. 빌드 및 설치
$ make -j$(nproc)
$ sudo make modules_install install

Interrupt Threading

PREEMPT_RT는 하드웨어 인터럽트를 커널 스레드로 처리하여 우선순위 기반 스케줄링을 가능하게 합니다.

Threaded IRQ 구조

/* 일반 커널 */
Hardware IRQ → hardirq → softirq → process

/* PREEMPT_RT 커널 */
Hardware IRQ → 최소 hardirq → IRQ thread (스케줄 가능) → process

IRQ 스레드 확인

# IRQ 스레드 목록 (PREEMPT_RT에서)
$ ps aux | grep irq
root         3  0.0  0.0      0     0 ?        S    Jan01   0:00 [irq/9-acpi]
root        12  0.0  0.0      0     0 ?        S    Jan01   0:01 [irq/16-ehci_hcd]
root        14  0.0  0.0      0     0 ?        S    Jan01   0:05 [irq/19-eth0]

# IRQ 스레드 우선순위 설정
$ sudo chrt -f -p 90 $(pgrep -f "irq/19-eth0")

request_threaded_irq() API

#include <linux/interrupt.h>

/* Threaded IRQ 등록 */
int request_threaded_irq(
    unsigned int irq,
    irq_handler_t handler,        /* hardirq handler (빠른 처리) */
    irq_handler_t thread_fn,     /* thread handler (스케줄 가능) */
    unsigned long irqflags,
    const char *devname,
    void *dev_id
);

/* 예시: 네트워크 드라이버 */
static irqreturn_t eth_irq_handler(int irq, void *dev_id)
{
    /* 최소한의 처리: IRQ 승인, 레지스터 읽기 */
    u32 status = read_reg(STATUS_REG);
    if (!(status & IRQ_PENDING))
        return IRQ_NONE;

    /* IRQ 승인 */
    write_reg(STATUS_REG, status);

    return IRQ_WAKE_THREAD;  /* thread_fn 호출 요청 */
}

static irqreturn_t eth_irq_thread(int irq, void *dev_id)
{
    /* 시간이 걸리는 처리: 패킷 수신, 메모리 할당 등 */
    process_rx_packets();
    return IRQ_HANDLED;
}

/* 등록 */
request_threaded_irq(dev->irq, eth_irq_handler, eth_irq_thread,
                      IRQF_SHARED, "eth0", dev);

RT Mutex (Priority Inheritance)

PREEMPT_RT는 spinlock을 rt_mutex로 대체하여 우선순위 역전(Priority Inversion)을 방지합니다.

Priority Inversion 문제

❌ Spinlock (Priority Inversion 발생) t=0 시간 → Low (Lock 보유) Low 완료 Medium 선점 High 대기 (블록됨!) High 실행 H가 M 때문에 지연! ✅ RT Mutex (Priority Inheritance) t=0 Low (Lock 보유) Low ↑ High 우선순위 상승 Medium 대기 High 실행 H 대기 → L 우선순위 상승 High 우선순위 Medium 우선순위 Low 우선순위
/* 시나리오: High(H), Medium(M), Low(L) 우선순위 태스크 */
1. L이 spinlock 획득
2. H가 같은 spinlock 대기 → L 완료까지 블록
3. M이 실행 가능 → M이 L을 선점
4. 결과: H가 M이 끝날 때까지 대기 (우선순위 역전!)

/* Priority Inheritance 해결책 */
1. L이 rt_mutex 획득
2. H가 같은 rt_mutex 대기
3. L의 우선순위가 일시적으로 H 수준으로 상승
4. L이 M을 선점하여 빠르게 완료
5. H가 mutex 획득, L은 원래 우선순위로 복귀

rt_mutex API

#include <linux/rtmutex.h>

struct rt_mutex {
    raw_spinlock_t wait_lock;
    struct rb_root_cached waiters;  /* 우선순위 정렬 대기 큐 */
    struct task_struct *owner;
};

/* rt_mutex 초기화 */
struct rt_mutex lock;
rt_mutex_init(&lock);

/* 획득/해제 */
rt_mutex_lock(&lock);
/* critical section */
rt_mutex_unlock(&lock);

/* trylock */
if (rt_mutex_trylock(&lock)) {
    /* 성공 */
    rt_mutex_unlock(&lock);
}

RT Mutex vs Spinlock 비교

특성 Spinlock RT Mutex
대기 방식 Busy-wait (CPU 점유) Sleep (스케줄 아웃)
Context Atomic (인터럽트 가능) Non-atomic (sleep 가능)
Priority Inversion 발생 가능 ❌ Priority Inheritance로 방지 ✅
Preemption 비활성화 (spinlock_t) 또는 활성화 (raw_spinlock_t) 활성화 (선점 가능)
사용 시기 매우 짧은 임계 구간 (<수십 μs) 긴 임계 구간, 실시간 요구사항
오버헤드 낮음 (단순 busy-wait) 높음 (스케줄링 오버헤드)
PREEMPT_RT 대부분 rt_mutex로 대체됨 기본 락 메커니즘
Spinlock Lock 요청 Busy-Wait Loop (CPU 100% 소비) Lock 획득까지 반복 Critical Section Unlock RT Mutex Lock 요청 대기 큐에 추가 (Sleep, CPU 양보) Wake-up 후 Critical Section Unlock + Wake Next
PREEMPT_RT에서의 변화: 일반 커널의 spinlock_t는 PREEMPT_RT에서 자동으로 rt_mutex로 대체됩니다. 진짜 spinlock이 필요한 경우(예: 인터럽트 핸들러)에만 raw_spinlock_t를 사용하세요.

실시간 스케줄링

실시간 스케줄링 정책

정책 설명 우선순위
SCHED_FIFO First-In First-Out. 같은 우선순위 내 순서대로 1~99 (99 최고)
SCHED_RR Round-Robin. FIFO + Time Slice 1~99
SCHED_DEADLINE EDF (Earliest Deadline First). CBS 기반 동적

SCHED_FIFO 예시

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

void set_realtime_priority(int priority)
{
    struct sched_param param;
    param.sched_priority = priority;  /* 1~99 */

    if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) {
        perror("sched_setscheduler");
    }
}

/* pthread 사용 */
pthread_t thread;
struct sched_param param = { .sched_priority = 80 };
pthread_setschedparam(thread, SCHED_FIFO, ¶m);

SCHED_DEADLINE

SCHED_DEADLINE은 EDF(Earliest Deadline First) 알고리즘과 CBS(Constant Bandwidth Server)를 결합한 스케줄러입니다.

#include <sched.h>
#include <linux/sched.h>

struct sched_attr {
    u32 size;
    u32 sched_policy;     /* SCHED_DEADLINE */
    u64 sched_flags;
    s32 sched_nice;
    u32 sched_priority;

    /* SCHED_DEADLINE 파라미터 */
    u64 sched_runtime;    /* 나노초 단위 실행 시간 */
    u64 sched_deadline;   /* 상대 deadline */
    u64 sched_period;     /* 주기 */
};

/* 예: 10ms마다 3ms 실행 시간, 10ms deadline */
struct sched_attr attr = {
    .size = sizeof(attr),
    .sched_policy = SCHED_DEADLINE,
    .sched_runtime  = 3 * 1000000,   /* 3ms */
    .sched_deadline = 10 * 1000000,  /* 10ms */
    .sched_period   = 10 * 1000000,  /* 10ms */
};

sched_setattr(0, &attr, 0);

High-Resolution Timers

PREEMPT_RT는 나노초 정밀도의 고해상도 타이머를 제공합니다.

hrtimer API

#include <linux/hrtimer.h>

struct hrtimer {
    struct timerqueue_node node;
    ktime_t _softexpires;
    enum hrtimer_restart (*function)(struct hrtimer *);
    struct hrtimer_clock_base *base;
    u8 state;
};

/* hrtimer 초기화 */
struct hrtimer timer;
hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
timer.function = &my_hrtimer_callback;

/* 타이머 시작 (100μs 후) */
ktime_t ktime = ktime_set(0, 100000);  /* 0초 + 100000ns */
hrtimer_start(&timer, ktime, HRTIMER_MODE_REL);

/* 콜백 함수 */
static enum hrtimer_restart my_hrtimer_callback(struct hrtimer *timer)
{
    /* 실시간 작업 수행 */
    return HRTIMER_NORESTART;  /* or HRTIMER_RESTART */
}

/* 타이머 취소 */
hrtimer_cancel(&timer);

레이턴시 측정

cyclictest

cyclictest는 실시간 레이턴시 측정 표준 도구입니다.

# rt-tests 패키지 설치
$ sudo apt install rt-tests

# 기본 테스트 (10분)
$ sudo cyclictest -m -Sp99 -D 10m

# 상세 옵션
$ sudo cyclictest \
    -m              # mlockall() 메모리 고정
    -Sp99           # SCHED_FIFO, 우선순위 99
    -i 1000         # 1ms 간격
    -h 100          # 히스토그램 100μs 범위
    -D 1h           # 1시간 테스트
    -q              # quiet 모드
    --histfile=hist.txt

# 출력 예시
T: 0 (1234) P:99 I:1000 C: 600000 Min:      2 Act:    3 Avg:    3 Max:      12
#       T: 스레드 ID
#       Min: 최소 레이턴시 (μs)
#       Act: 현재 레이턴시
#       Avg: 평균 레이턴시
#       Max: 최대 레이턴시 (중요!)

ftrace 레이턴시 추적

# irqsoff tracer: 인터럽트 비활성화 시간 측정
# echo irqsoff > /sys/kernel/debug/tracing/current_tracer
# echo 1 > /sys/kernel/debug/tracing/tracing_on
# sleep 10
# cat /sys/kernel/debug/tracing/trace_stat/function0

# preemptoff tracer: 선점 비활성화 시간 측정
# echo preemptoff > /sys/kernel/debug/tracing/current_tracer

# wakeup tracer: 스케줄링 레이턴시 측정
# echo wakeup > /sys/kernel/debug/tracing/current_tracer
# echo 1 > /sys/kernel/debug/tracing/tracing_on

성능 비교

PREEMPT_RT 커널과 일반 커널의 레이턴시 및 처리량 비교입니다. 실제 워크로드에 따라 결과는 달라질 수 있습니다.

레이턴시 벤치마크

워크로드 일반 커널 (PREEMPT) PREEMPT_RT 개선율
Worst-case latency ~200 μs ~15 μs 93% ↓
Average latency ~8 μs ~5 μs 37% ↓
Jitter (표준편차) ±25 μs ±3 μs 88% ↓
99.9% percentile ~50 μs ~10 μs 80% ↓
측정 환경: Intel Core i7-8700K, 16GB RAM, cyclictest -m -Sp99 -D 1h, stress-ng 백그라운드 부하

워크로드별 성능 특성

워크로드 유형 일반 커널 PREEMPT_RT 권장사항
산업 제어
(PLC, 모션 컨트롤)
❌ 불안정
레이턴시 스파이크 발생
✅ 안정적
<20 μs 보장
PREEMPT_RT 필수
오디오 처리
(Jack, PipeWire)
⚠️ 가능
버퍼 크기 증가 필요
✅ 우수
64 샘플 버퍼 가능
PREEMPT_RT 권장
네트워크 (low-latency)
(금융, HFT)
⚠️ 제한적
P99 latency 높음
✅ 우수
일관된 레이턴시
PREEMPT_RT + CPU isolation
비디오 스트리밍 ✅ 충분
Soft RT로 처리 가능
✅ 더 안정적
프레임 드롭 감소
일반 커널로 충분
웹 서버
(처리량 중심)
✅ 우수
높은 처리량
⚠️ 낮은 처리량
~5-10% 감소
일반 커널 권장
데이터베이스 ✅ 우수
높은 처리량
⚠️ 낮은 처리량
트랜잭션 속도 감소
일반 커널 권장
컴파일/빌드 ✅ 빠름 ⚠️ 느림
~10-15% 증가
일반 커널 권장

Trade-offs 요약

항목 PREEMPT_RT 장점 PREEMPT_RT 단점
레이턴시 Worst-case 극적 감소 (93% ↓) Average 약간 증가 (스케줄링 오버헤드)
처리량 예측 가능성 향상 전체 처리량 5-15% 감소
전력 소비 - 더 많은 context switch → 전력 증가
복잡도 표준 Linux API 사용 튜닝 복잡 (CPU isolation, IRQ affinity 등)
디버깅 ftrace/perf 도구 사용 가능 타이밍 버그 재현 어려움
선택 기준:
  • Hard RT 필요 (산업, 로봇, 의료) → PREEMPT_RT 필수
  • Low-latency 선호 (오디오, 네트워크) → PREEMPT_RT 권장
  • 처리량 중심 (웹, DB, 빌드) → 일반 커널 권장

CPU Isolation

실시간 워크로드를 방해 없이 실행하려면 CPU를 격리해야 합니다.

isolcpus 커널 파라미터

# GRUB 설정 (/etc/default/grub)
GRUB_CMDLINE_LINUX="isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3"

# isolcpus=2,3      — CPU 2,3을 일반 스케줄러에서 제외
# nohz_full=2,3     — CPU 2,3에서 타이머 틱 비활성화 (adaptive-tick mode)
# rcu_nocbs=2,3     — CPU 2,3의 RCU 콜백을 다른 CPU로 오프로드

$ sudo update-grub
$ sudo reboot

# 격리된 CPU에 태스크 바인딩
$ taskset -c 2 ./realtime_app

IRQ Affinity 설정

# IRQ를 특정 CPU로 제한 (CPU 0,1만 사용)
# echo 3 > /proc/irq/19/smp_affinity  # 0x03 = CPU 0,1

# irqbalance 비활성화 (수동 IRQ 관리 시)
$ sudo systemctl stop irqbalance
$ sudo systemctl disable irqbalance

실시간 시스템 Best Practices

실시간 애플리케이션 체크리스트

실시간 애플리케이션 예시

#include <sched.h>
#include <sys/mman.h>
#include <string.h>

void setup_realtime(void)
{
    /* 1. 메모리 고정 (swap 방지) */
    mlockall(MCL_CURRENT | MCL_FUTURE);

    /* 2. 스택 프리페칭 (page fault 방지) */
    unsigned char dummy[8192];
    memset(dummy, 0, sizeof(dummy));

    /* 3. 실시간 스케줄링 설정 */
    struct sched_param param = { .sched_priority = 90 };
    sched_setscheduler(0, SCHED_FIFO, ¶m);

    /* 4. CPU 어피니티 (격리된 CPU 2) */
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(2, &cpuset);
    sched_setaffinity(0, sizeof(cpuset), &cpuset);
}

void realtime_loop(void)
{
    struct timespec next, interval = { .tv_nsec = 1000000 };  /* 1ms */
    clock_gettime(CLOCK_MONOTONIC, &next);

    while (1) {
        /* 실시간 작업 */
        do_critical_work();

        /* 다음 주기까지 대기 */
        next.tv_nsec += interval.tv_nsec;
        if (next.tv_nsec >= 1000000000) {
            next.tv_sec++;
            next.tv_nsec -= 1000000000;
        }
        clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
    }
}

커널 설정

# Preemption Model
CONFIG_PREEMPT_RT=y                    # Fully Preemptible Kernel (RT)
CONFIG_PREEMPT_RT_FULL=y

# High-Resolution Timers
CONFIG_HIGH_RES_TIMERS=y
CONFIG_NO_HZ_FULL=y                    # Adaptive-tick mode

# CPU Isolation
CONFIG_RCU_NOCB_CPU=y                  # RCU callback offloading
CONFIG_NO_HZ_FULL_ALL=y

# IRQ Threading
CONFIG_IRQ_FORCED_THREADING=y

# Disable for RT
# CONFIG_DEBUG_PREEMPT is not set       # 디버그 오버헤드 제거
# CONFIG_PROVE_LOCKING is not set
# CONFIG_DEBUG_LOCK_ALLOC is not set

디버깅

RT Throttling

SCHED_FIFO/RR 태스크가 CPU를 독점하지 못하도록 제한합니다.

# RT 태스크는 기본적으로 950ms/1초만 사용 가능
# cat /proc/sys/kernel/sched_rt_period_us
1000000

# cat /proc/sys/kernel/sched_rt_runtime_us
950000

# RT 제한 해제 (주의: 시스템 행 가능)
# echo -1 > /proc/sys/kernel/sched_rt_runtime_us

trace-cmd

# trace-cmd 설치
$ sudo apt install trace-cmd

# 레이턴시 추적
$ sudo trace-cmd record -p function_graph -P 1234  # PID 1234 추적
$ sudo trace-cmd report

트러블슈팅

일반적인 문제와 해결

문제 원인 해결 방법
레이턴시 스파이크
(100+ μs)
SMI (System Management Interrupt)
BIOS 레벨 인터럽트
• BIOS에서 SMI 비활성화
hwlatdetect로 SMI 감지
• 최신 펌웨어 업데이트
예상보다 높은 레이턴시 CPU 전력 관리 (C-states)
주파수 스케일링
• C-states 비활성화: processor.max_cstate=1
• 고정 주파수: intel_pstate=disable
• Performance governor 설정
RT 태스크가 실행 안 됨 RT Throttling 제한
(기본 95% CPU)
sched_rt_runtime_us 증가
• 또는 -1로 제한 해제 (주의)
시스템 응답 없음 RT 태스크 무한 루프
일반 태스크 CPU 못 받음
• Magic SysRq 사용: Alt+SysRq+k
• Watchdog 활성화
• RT throttling 유지
mlockall() 실패
(ENOMEM)
RLIMIT_MEMLOCK 제한 ulimit -l unlimited
• /etc/security/limits.conf 설정
• CAP_IPC_LOCK capability 부여
IRQ 스레드 없음 PREEMPT_RT 아닌 커널
또는 드라이버 미지원
ps aux | grep irq 확인
• RT 커널 재빌드
• 드라이버 request_threaded_irq() 사용 확인
cyclictest 높은 latency 백그라운드 프로세스
IRQ affinity 미설정
• 불필요한 서비스 중지
• IRQ affinity 설정
• CPU isolation 적용

레이턴시 스파이크 원인 분석

# 1. SMI (System Management Interrupt) 감지
$ sudo hwlatdetect --duration=60
Samples: 3600
Max latency: 127 us
SMI count: 12  ← SMI가 레이턴시 원인!

# 해결: BIOS에서 SMI 소스 비활성화 (USB legacy, thermal monitoring 등)

# 2. CPU 전력 관리 확인
$ cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
powersave  ← 문제! performance로 변경 필요

$ sudo cpupower frequency-set -g performance

# 3. C-states 확인 및 비활성화
$ cat /sys/devices/system/cpu/cpu0/cpuidle/state*/disable
0
0  ← 모든 C-states 활성화 상태 (문제)

# GRUB 파라미터로 C-states 제한
GRUB_CMDLINE_LINUX="processor.max_cstate=1 intel_idle.max_cstate=0"

# 4. ftrace로 스파이크 원인 추적
$ sudo trace-cmd record -p function_graph \
  -e irq -e sched -e timer \
  -F cyclictest -m -Sp99 -D 1m

$ sudo trace-cmd report | grep "duration > 50"  # 50μs 이상 찾기

# 5. 프로세스별 레이턴시 기여도 확인
$ ps -eLo pid,tid,class,rtprio,pri,nice,cmd | grep FF
  1234  1234 FF      99  139   - ./realtime_app  ← RT 태스크
  5678  5678 FF      50   90   - [irq/19-eth0]    ← IRQ 스레드

진단 체크리스트

# RT 시스템 상태 종합 점검 스크립트

## 1. 커널 버전 및 PREEMPT_RT 확인
$ uname -a | grep PREEMPT_RT
$ cat /sys/kernel/realtime  # 1이면 RT 커널

## 2. CPU Governor 확인
$ cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor | sort -u

## 3. IRQ Threading 확인
$ ps aux | grep "\[irq/" | wc -l  # 0이면 문제

## 4. RT Throttling 설정
$ cat /proc/sys/kernel/sched_rt_runtime_us

## 5. Isolated CPU 확인
$ cat /sys/devices/system/cpu/isolated

## 6. RCU callback offloading
$ cat /sys/devices/system/cpu/nohz_full

## 7. 메모리 잠금 한계
$ ulimit -l  # unlimited 여야 함

## 8. SMI 카운트 (Intel)
$ sudo rdmsr -a 0x34  # SMI_COUNT MSR
주의: RT 시스템에서 디버깅 도구(ftrace, perf)를 사용하면 레이턴시가 증가합니다. 최종 검증 시에는 디버깅 도구 없이 측정하세요.

참고자료

다음 학습:

PREEMPT_RT 아키텍처

PREEMPT_RT 패치셋은 리눅스 커널의 비선점 구간을 체계적으로 제거합니다. 핵심 변환은 세 가지입니다: (1) spinlock_t를 sleeping lock(rt_mutex 기반)으로 교체, (2) 하드웨어 인터럽트를 커널 스레드로 전환(threaded interrupts), (3) softirq를 스레드화하여 선점 가능하게 만듭니다.

PREEMPT_RT 커널 변환 아키텍처 일반 커널 spinlock_t (busy-wait, 비선점) hardirq → softirq (비선점 구간) preempt_disable() 구간 다수 local_irq_disable() 구간 존재 변환 변환 변환 변환 PREEMPT_RT 커널 rt_mutex (sleeping, 선점 가능) IRQ thread + ksoftirqd (선점 가능) migrate_disable() (선점 가능) local_lock (선점 가능) 예외: raw_spinlock_t는 변환되지 않음 스케줄러 내부, 타이머 코어, low-level 아키텍처 코드에서만 사용 Sleeping Spinlock 동작 흐름 spin_lock() 호출 rt_mutex_lock() 내부 변환 contention 시 sleep PI 체인 전파 owner unlock 시 wake_up + 재스케줄 범례: 비선점 구간 선점 가능 구간 완료/해제 변환 경로

Sleeping Spinlocks 상세

PREEMPT_RT에서 spinlock_t는 매크로 수준에서 rt_mutex 기반 sleeping lock으로 재정의됩니다. 이 변환은 커널 소스 수정 없이 include/linux/spinlock_types.h 헤더에서 이루어집니다.

/* include/linux/spinlock_types.h — PREEMPT_RT 빌드 시 */
#ifdef CONFIG_PREEMPT_RT

typedef struct spinlock {
    struct rt_mutex_base lock;  /* rt_mutex 기반 구현 */
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
} spinlock_t;

#else

typedef struct spinlock {
    union {
        struct raw_spinlock rlock;  /* 전통적 busy-wait spinlock */
    };
} spinlock_t;

#endif
raw_spinlock_t 사용 기준: raw_spinlock_t는 PREEMPT_RT에서도 변환되지 않는 진짜 spinlock입니다. 스케줄러 코어(rq->lock), 타이머 인터럽트 코어, 아키텍처 저수준 코드 등 sleep이 절대 불가능한 컨텍스트에서만 사용합니다. 일반 드라이버에서는 사용을 피하세요.

Threaded Interrupts 상세 메커니즘

PREEMPT_RT는 CONFIG_IRQ_FORCED_THREADING을 통해 거의 모든 인터럽트 핸들러를 자동으로 스레드화합니다. 드라이버가 request_irq()로 등록한 핸들러도 내부적으로 threaded IRQ로 전환됩니다.

/* kernel/irq/manage.c — forced threading 핵심 로직 */
static int irq_setup_forced_threading(struct irqaction *new)
{
    /* IRQF_NO_THREAD 플래그가 설정되면 스레딩 건너뜀 */
    if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT))
        return 0;

    /* 기존 hardirq handler를 thread_fn으로 이동 */
    new->thread_fn = new->handler;
    new->handler = irq_default_primary_handler;
    new->flags |= IRQF_ONESHOT;

    return 0;
}
구분 일반 커널 PREEMPT_RT
인터럽트 핸들러 hardirq 컨텍스트 (비선점) 커널 스레드 (선점 가능)
softirq 인터럽트 리턴 시 즉시 실행 ksoftirqd 스레드에서 실행
IRQ 우선순위 모든 IRQ 동일 (FIFO) chrt로 개별 IRQ 스레드 우선순위 조정
Lock 내 sleep BUG (금지) 허용 (rt_mutex 기반)
비선점 구간 spinlock 보유 시 전체 raw_spinlock 보유 시만
# IRQ 스레드 우선순위 확인 및 조정
$ ps -eo pid,cls,rtprio,comm | grep irq/
   12  FF    50 irq/9-acpi
   14  FF    50 irq/16-ehci
   18  FF    50 irq/19-eth0

# 네트워크 IRQ를 최고 우선순위로 설정
$ sudo chrt -f -p 90 18

# IRQF_NO_THREAD 핸들러 확인 (스레딩 제외 IRQ)
$ grep -r "IRQF_NO_THREAD" drivers/ | head -5

우선순위 상속 프로토콜

우선순위 상속(Priority Inheritance, PI)은 우선순위 역전 문제의 핵심 해결책입니다. PREEMPT_RT의 rt_mutex는 PI 체인을 통해 중첩된 락 보유 관계에서도 올바른 우선순위 전파를 보장합니다.

PI 체인 (중첩 우선순위 상속) Task H(prio 99) → Mutex B 대기 → Task M(prio 50) → Mutex A 대기 → Task L(prio 10) Step 1: 초기 상태 Task L prio=10 Mutex A owner=L Step 2: M이 Mutex A 대기 Task L prio=50! Mutex A owner=L Task M prio=50 L의 우선순위가 50으로 상승 Step 3: H가 Mutex B 대기 (PI 체인 전파) Task L prio=99! Mutex A owner=L Task M prio=99! Mutex B owner=M Task H prio=99 PI 체인 전파: H → M → L 전체 우선순위 99로 상승 Step 4: L이 Mutex A 해제 → 우선순위 복원 Task L prio=10 Task M prio=99! Mutex B owner=M Task H prio=99 원래 우선순위 복원 High 우선순위 태스크 Medium 우선순위 태스크 Low 우선순위 태스크 Mutex

PI 체인 워크스루

커널 내부에서 PI 체인 전파는 rt_mutex_adjust_prio_chain() 함수가 담당합니다. 이 함수는 waiter → owner → owner의 waiter 관계를 재귀적으로 순회하며 우선순위를 전파합니다.

/* kernel/locking/rtmutex.c — PI 체인 조정 핵심 */
static int rt_mutex_adjust_prio_chain(
    struct task_struct *task,
    enum rtmutex_chainwalk chwalk,
    struct rt_mutex_base *orig_lock,
    struct rt_mutex_base *next_lock,
    struct rt_mutex_waiter *orig_waiter,
    struct task_struct *top_task)
{
    struct rt_mutex_waiter *waiter, *top_waiter;
    struct rt_mutex_base *lock;

    /* 체인 워크 루프: owner를 따라 올라감 */
again:
    /* 데드락 감지: 최대 깊이 제한 */
    if (++depth > 1024) {
        WARN(1, "PI chain too deep!");
        return -EDEADLK;
    }

    /* task의 최고 우선순위 waiter 확인 */
    waiter = rt_mutex_top_waiter(lock);
    top_waiter = task_top_pi_waiter(task);

    /* 우선순위 전파가 필요한지 확인 */
    if (rt_waiter_node_less(&waiter->tree, &top_waiter->tree))
        rt_mutex_adjust_prio(task);  /* 태스크 우선순위 상승 */

    /* 다음 lock owner로 체인 계속 */
    task = rt_mutex_owner(lock);
    if (task)
        goto again;

    return 0;
}
PI 체인 깊이 제한: PI 체인은 최대 1024 단계까지만 전파됩니다. 이를 초과하면 데드락 가능성으로 판단하고 -EDEADLK를 반환합니다. 실제 시스템에서 3단계 이상의 PI 체인은 설계 결함을 의심해야 합니다.
PI 프로토콜 설명 Linux 구현
Basic PI 직접 owner의 우선순위만 상승 단일 rt_mutex lock/unlock
Transitive PI 체인 전체 우선순위 전파 rt_mutex_adjust_prio_chain()
Nested PI 중첩 락 보유 시 다중 boosting task->pi_waiters RB-tree 관리

cyclictest 방법론

cyclictest는 RT 커널의 worst-case latency를 정량적으로 측정하는 표준 도구입니다. 단순한 실행을 넘어 올바른 방법론으로 측정해야 의미 있는 결과를 얻을 수 있습니다.

cyclictest 내부 동작 원리 시간 축 clock_nanosleep wake latency 측정 clock_nanosleep wake 측정 기대 wakeup 실제 wakeup latency latency = actual_wakeup_time - expected_wakeup_time 이 값이 작을수록 결정론적 시스템 (worst-case가 핵심 지표) 레이턴시 히스토그램 분포 빈도 레이턴시 (us) 평균 부근 worst-case 이상치 Max latency (이 값이 핵심!) 1 5 10 50+

체계적 측정 방법론

# ============================================
# 1단계: 환경 준비 (측정 전 필수)
# ============================================

# CPU governor를 performance로 고정
$ sudo cpupower frequency-set -g performance

# C-states 비활성화
$ sudo sh -c 'for f in /sys/devices/system/cpu/cpu*/cpuidle/state*/disable; do echo 1 > $f; done'

# irqbalance 중지
$ sudo systemctl stop irqbalance

# ============================================
# 2단계: 부하 생성 (worst-case 유도)
# ============================================

# stress-ng로 다양한 부하 생성
$ sudo stress-ng --cpu 4 --io 2 --vm 2 --vm-bytes 256M \
    --fork 4 --timeout 0 &

# 네트워크 부하 (별도 머신에서)
$ iperf3 -c target_ip -t 3600 -P 4

# 디스크 I/O 부하
$ sudo fio --name=randwrite --ioengine=libaio --rw=randwrite \
    --bs=4k --numjobs=4 --size=1G --runtime=3600

# ============================================
# 3단계: cyclictest 실행
# ============================================

# 프로덕션 레벨 측정 (최소 24시간 권장)
$ sudo cyclictest \
    --mlockall \
    --smp \
    --priority=99 \
    --interval=1000 \
    --histogram=200 \
    --duration=24h \
    --histfile=latency_hist.txt \
    --quiet

# ============================================
# 4단계: 결과 분석
# ============================================

# 히스토그램 결과 해석
$ head -20 latency_hist.txt
#   0   1234   5678   3456   ← latency 0us에서의 빈도
#   1  45678  43210  44567   ← latency 1us
#   2  98765  97654  96543   ← latency 2us (최다빈도 구간)
#   3  87654  86543  85432
#   ...
#  15      2      0      1   ← 15us (이상치)

지터(Jitter) 분석

지터는 레이턴시의 변동폭으로, RT 시스템의 예측 가능성을 나타냅니다. 평균 레이턴시보다 지터가 더 중요합니다.

지표 계산 방법 양호 기준 (PREEMPT_RT) 불량 징후
Max Latency cyclictest Max 값 <20 us >50 us (SMI, C-state 의심)
Avg Latency cyclictest Avg 값 <5 us >10 us (설정 문제)
Jitter Max - Min <15 us >30 us (비선점 구간 존재)
P99.9 99.9 백분위 값 <10 us >25 us (간헐적 간섭)
표준편차 히스토그램 기반 계산 <3 us >8 us (불안정)
히스토그램 해석 팁: 정상적인 RT 시스템은 히스토그램이 좁은 피크(sharp peak)를 보입니다. 긴 꼬리(long tail)가 있다면 간헐적 간섭 소스를 조사해야 합니다. gnuplot이나 python matplotlib로 시각화하면 문제 패턴을 빠르게 파악할 수 있습니다.
# gnuplot으로 히스토그램 시각화
$ gnuplot << 'PLOT'
set terminal png size 800,400
set output "latency_histogram.png"
set title "Latency Histogram"
set xlabel "Latency (us)"
set ylabel "Frequency"
set style fill solid 0.5
plot "latency_hist.txt" using 1:2 with boxes title "CPU 0", \
     "" using 1:3 with boxes title "CPU 1"
PLOT

RT 스케줄러 내부

RT 스케줄러(kernel/sched/rt.c)는 고정 우선순위 기반의 결정론적 스케줄링을 제공합니다. 내부적으로 rt_prio_array 비트맵과 pushable_tasks 리스트를 사용하여 O(1) 시간 복잡도로 다음 실행 태스크를 선택합니다.

RT 스케줄러 내부 구조 (rt_rq) rt_prio_array (100 단계) bitmap[0..3]: 1 0 1 0 0 1 ... queue[0]: TaskA(99) TaskB(99) queue[2]: TaskC(97) queue[5]: TaskD(94) queue[99]: (empty) sched_find_first_bit() → O(1) 탐색 pushable_tasks (plist) 다른 CPU로 이동 가능한 RT 태스크 TaskB(prio 99) TaskC(prio 97) TaskD(94) RT 밸런싱 (push/pull) 높은 prio 태스크를 idle/low-prio CPU로 이동 push_rt_task() 현재 CPU에서 밀어냄 pull_rt_task() 다른 CPU에서 가져옴 SMP RT 밸런싱 시나리오 CPU 0 TaskA(99) 실행 중 TaskC(97) 대기 중 CPU 1 TaskE(50) 실행 중 (낮은 우선순위) push TaskC CPU 2 (idle) pull 대상 pull 가능
/* kernel/sched/rt.c — rt_prio_array 구조 */
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;
    unsigned int rt_nr_migratory;      /* 이동 가능 태스크 수 */
    int overloaded;                    /* CPU가 과부하 상태? */
    struct plist_head pushable_tasks;  /* push 가능 태스크 */
};

/* 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;

    /* 비트맵에서 첫 번째 set bit 탐색 (O(1)) */
    idx = sched_find_first_bit(array->bitmap);
    if (idx >= MAX_RT_PRIO)
        return NULL;  /* RT 태스크 없음 */

    /* 해당 우선순위 큐의 첫 번째 태스크 반환 */
    return list_first_entry(&array->queue[idx],
                            struct sched_rt_entity, run_list);
}

Push/Pull 밸런싱

SMP 시스템에서 RT 스케줄러는 push/pull 메커니즘으로 RT 태스크를 CPU 간 이동시킵니다. 높은 우선순위 태스크가 낮은 우선순위 태스크가 실행 중인 CPU로 이동(push)하거나, idle CPU가 과부하 CPU에서 태스크를 가져옵니다(pull).

/* push_rt_task(): 현재 CPU에서 다른 CPU로 RT 태스크 이동 */
static int push_rt_task(struct rq *rq)
{
    struct task_struct *next_task;
    struct rq *lowest_rq;

    /* pushable_tasks에서 가장 높은 우선순위 태스크 선택 */
    next_task = plist_first_entry(&rq->rt.pushable_tasks,
                                   struct task_struct, pushable_tasks);

    /* 가장 낮은 우선순위 태스크가 실행 중인 CPU 탐색 */
    lowest_rq = find_lowest_rq(next_task);

    /* 해당 CPU의 현재 태스크보다 높은 우선순위면 이동 */
    if (task_prio(lowest_rq->curr) > task_prio(next_task)) {
        deactivate_task(rq, next_task, 0);
        activate_task(lowest_rq, next_task, 0);
        resched_curr(lowest_rq);  /* 대상 CPU 재스케줄 */
    }

    return 1;
}
overloaded 플래그: CPU에 2개 이상의 RT 태스크가 있으면 overloaded=1이 설정됩니다. 이 플래그가 설정된 CPU에서만 push 동작이 트리거되며, cpupri 자료구조를 통해 가장 낮은 우선순위 CPU를 O(1)로 찾습니다.

SCHED_DEADLINE 스케줄러

SCHED_DEADLINE은 Linux에서 가장 높은 우선순위의 스케줄링 클래스입니다. EDF(Earliest Deadline First) 알고리즘과 CBS(Constant Bandwidth Server) 메커니즘을 결합하여 주기적 실시간 태스크의 시간 격리(temporal isolation)를 보장합니다.

SCHED_DEADLINE: CBS + EDF 동작 원리 3개 파라미터: runtime(Q) = 실행 시간, deadline(D) = 마감시한, period(T) = 주기 대역폭 = Q/T, 제약 조건: Q <= D <= T EDF 스케줄링 예시 (2개 태스크) t 0 5ms 10ms 15ms 20ms A 2ms 2ms A: Q=2ms, D=T=5ms B 3ms 3ms B: Q=3ms, D=T=10ms EDF: 가장 가까운 deadline을 가진 태스크를 먼저 실행 t=0: A(D=5ms) vs B(D=10ms) → A 먼저 실행 CBS Admission Control (입장 제어) 총 대역폭 검사: sum(Q_i / T_i) <= CPU 수 Task A: 2/5 = 0.4, Task B: 3/10 = 0.3 → 합계 0.7 <= 1.0 (허용) 합계 > 1.0이면 sched_setattr() 호출 시 -EBUSY 반환

SCHED_DEADLINE API 상세

/* SCHED_DEADLINE 태스크 설정 — 커널 내부 구조 */
struct sched_dl_entity {
    struct rb_node rb_node;      /* 데드라인 정렬 RB-tree */
    u64 dl_runtime;               /* 최대 실행 시간 (ns) */
    u64 dl_deadline;              /* 상대 deadline (ns) */
    u64 dl_period;                /* 태스크 주기 (ns) */
    u64 runtime;                  /* 남은 실행 시간 */
    u64 deadline;                 /* 절대 deadline */
    unsigned int dl_throttled : 1; /* budget 소진 시 throttle */
    unsigned int dl_boosted : 1;   /* PI로 boosted? */
};
/* 유저스페이스에서 SCHED_DEADLINE 태스크 설정 예시 */
#include <sched.h>
#include <sys/syscall.h>
#include <unistd.h>

#define SCHED_DEADLINE 6

struct sched_attr {
    __u32 size;
    __u32 sched_policy;
    __u64 sched_flags;
    __s32 sched_nice;
    __u32 sched_priority;
    __u64 sched_runtime;
    __u64 sched_deadline;
    __u64 sched_period;
};

void setup_deadline_task(void)
{
    struct sched_attr attr = {
        .size           = sizeof(attr),
        .sched_policy   = SCHED_DEADLINE,
        .sched_runtime  = 5 * 1000000,   /* 5ms runtime */
        .sched_deadline = 20 * 1000000,  /* 20ms deadline */
        .sched_period   = 20 * 1000000,  /* 20ms period */
    };

    /* sched_setattr() 시스템 콜 (glibc wrapper 없음) */
    int ret = syscall(SYS_sched_setattr, 0, &attr, 0);
    if (ret < 0) {
        perror("sched_setattr");
        /* EBUSY: admission control 실패 (대역폭 초과) */
        /* EPERM: CAP_SYS_NICE 권한 없음 */
    }

    /* 주기적 실행 루프 */
    while (1) {
        do_periodic_work();
        sched_yield();  /* budget 반환, 다음 period까지 sleep */
    }
}
항목 SCHED_FIFO SCHED_DEADLINE
우선순위 정적 (1-99) 동적 (EDF 기반 deadline)
CPU 대역폭 제어 없음 (독점 가능) CBS로 자동 대역폭 제한
Admission Control 없음 sched_setattr() 시 검증
시간 격리 없음 (높은 prio가 독점) 보장 (budget 소진 시 throttle)
SMP 밸런싱 push/pull (cpupri) push/pull (cpudl)
적합 용도 단일 critical 태스크 다중 주기적 RT 태스크
SCHED_DEADLINE vs SCHED_FIFO 선택 기준: 주기적 태스크가 여러 개이고 각 태스크에 독립적인 시간 보장이 필요하면 SCHED_DEADLINE을 사용하세요. 단일 critical path 태스크라면 SCHED_FIFO가 더 단순합니다.

RT 스로틀링

RT 스로틀링은 실시간 태스크가 시스템을 독점하는 것을 방지하는 안전장치입니다. sched_rt_runtime_ussched_rt_period_us 두 파라미터로 RT 태스크의 CPU 사용 비율을 제한합니다.

RT 스로틀링 메커니즘 sched_rt_period_us = 1,000,000 (주기: 1초) sched_rt_runtime_us = 950,000 (RT 허용: 950ms/1초 = 95%) 정상 동작: RT 태스크 95% 사용 RT 태스크 실행 (950ms) CFS 0ms 950ms 1s 스로틀 발동: RT budget 소진 RT 태스크 (950ms budget 사용) 차단 RT THROTTLED! dmesg: sched: RT throttling 스로틀 해제 (위험!) echo -1 > sched_rt_runtime_us 안전한 방법 cgroup RT bandwidth 사용

스로틀링 상세 동작

# 현재 RT 스로틀링 설정 확인
$ cat /proc/sys/kernel/sched_rt_period_us
1000000   # 1초 주기

$ cat /proc/sys/kernel/sched_rt_runtime_us
950000    # 1초 중 950ms까지 RT 허용 (95%)

# 스로틀 발생 확인
$ dmesg | grep "RT throttling"
[  120.456] sched: RT throttling activated

# RT 스로틀 통계
$ cat /proc/sched_debug | grep rt_throttled
  .rt_throttled       : 1

# ============================================
# cgroup v2 RT 대역폭 제어 (안전한 방법)
# ============================================

# RT 태스크 전용 cgroup 생성
$ sudo mkdir /sys/fs/cgroup/rt_tasks

# cpu.max로 대역폭 제한 (500ms per 1000ms = 50%)
$ echo "500000 1000000" | sudo tee /sys/fs/cgroup/rt_tasks/cpu.max

# RT 태스크를 cgroup에 배치
$ echo $PID | sudo tee /sys/fs/cgroup/rt_tasks/cgroup.procs
sched_rt_runtime_us = -1 의 위험성: 이 설정은 RT 스로틀링을 완전히 비활성화합니다. RT 태스크가 무한 루프에 빠지면 일반 태스크(SSH, 로그인 셸 포함)가 CPU를 전혀 받지 못해 시스템이 응답 불능 상태가 됩니다. 프로덕션에서는 반드시 watchdog과 함께 사용하세요.
설정 의미 권장 용도
rt_runtime_us=950000 95% 기본값. 5% CFS 보장 일반 서버
rt_runtime_us=980000 98% 거의 전부 RT에 할당 전용 RT 시스템
rt_runtime_us=-1 100% 스로틀 해제 (위험) watchdog 필수 환경만
cgroup cpu.max 가변 태스크별 세밀한 제어 다중 RT 태스크 환경

지연시간 분석

실시간 시스템에서 지연시간(latency)은 이벤트 발생부터 태스크 실행까지의 전체 경로입니다. 이 경로를 세분화하여 병목을 식별해야 합니다.

Wakeup-to-Running 지연시간 분석 경로 하드웨어 인터럽트 IRQ 전달 지연 IRQ handler 실행 스케줄링 결정 Context Switch + 실행 시작 총 지연시간 (End-to-End Latency) 구간별 지연 원인과 측정 방법 1. IRQ Latency (0.5~3 us) 원인: local_irq_disable() 구간, NMI, SMI | 측정: irqsoff tracer 2. IRQ Handler Time (1~10 us) 원인: hardirq 코드 실행 시간 | 측정: function_graph tracer, irq 이벤트 3. Scheduling Latency (1~5 us) 원인: preempt_disable() 구간, rq lock 경합 | 측정: preemptoff tracer, wakeup tracer 4. Context Switch (1~3 us) 원인: TLB flush, 캐시 미스, FPU 상태 저장 | 측정: perf sched, context_switch 트레이스포인트 PREEMPT_RT 총 지연: 3.5~21 us (typical: 5~10 us)

ftrace Latency Tracers

# ============================================
# irqsoff tracer: 인터럽트 비활성화 최대 시간
# ============================================
$ cd /sys/kernel/debug/tracing

# tracer 설정
# echo irqsoff > current_tracer
# echo 0 > tracing_max_latency   # 기존 최대값 초기화
# echo 1 > tracing_on

# 부하 실행 (10초)
$ sudo stress-ng --cpu 4 --io 2 --timeout 10s

# 최대 irq-off 시간 확인
# cat tracing_max_latency
42                # 42us 동안 인터럽트가 비활성화됨

# 해당 구간의 콜스택 확인
# cat trace
# irqsoff latency trace v1.1.5 on 6.6.0-rt15
# latency: 42 us, #4/4, CPU#2 | (M:preempt VP:0, KP:0, SP:0 HP:0)
#    -----------------
#    | task: stress-ng-1234
#    -----------------
#                  _------=> CPU#
#                 / _-----=> irqs-off
#                | / _----=> need-resched
#                || / _---=> hardirq/softirq
#                ||| / _--=> preempt-depth
#                |||| /
#  cmd     pid   ||||| time  |   caller
#     \   /      |||||  \    |    /
  stress  1234   2d..1    0us : _raw_spin_lock_irqsave
  stress  1234   2d..1   42us : _raw_spin_unlock_irqrestore

# ============================================
# wakeup tracer: RT 태스크 wakeup → 실행 지연
# ============================================
# echo wakeup_rt > current_tracer
# echo 0 > tracing_max_latency
# echo 1 > tracing_on

# cyclictest 실행 (RT wakeup 생성)
$ sudo cyclictest -m -Sp99 -D 30s &

# 최대 wakeup latency 확인
# cat tracing_max_latency
8                 # 8us: wakeup에서 실행까지 최대 지연

# ============================================
# preemptirqsoff: irq-off + preempt-off 결합
# ============================================
# echo preemptirqsoff > current_tracer
# echo 0 > tracing_max_latency
# echo 1 > tracing_on
tracer 선택 가이드:
  • irqsoff: 인터럽트 비활성화 구간 최대값 (하드웨어 인터럽트 차단 시간)
  • preemptoff: 선점 비활성화 구간 최대값 (spinlock 보유 시간)
  • preemptirqsoff: 위 두 가지 결합 (가장 포괄적)
  • wakeup_rt: RT 태스크의 wakeup-to-running 지연 (end-to-end 관점)

rt_mutex 구조체 분석

rt_mutex는 PREEMPT_RT의 핵심 동기화 프리미티브입니다. 우선순위 정렬된 RB-tree 대기 큐와 PI(Priority Inheritance) 체인 관리를 통해 결정론적 락 동작을 보장합니다.

rt_mutex 내부 자료구조 관계 rt_mutex_base wait_lock: raw_spinlock_t waiters: rb_root_cached owner: task_struct * + HAS_WAITERS 비트 waiters (RB-tree) 우선순위 정렬 p50 p80 p30 leftmost = 최고 우선순위 task_struct (owner) pi_waiters: rb_root pi_top_task: waiter * pi_blocked_on: mutex * prio: (boosted 우선순위) owner 포인터 rt_mutex_waiter tree: rb_node (mutex 큐) pi_tree: rb_node (PI 큐) task: task_struct * lock: rt_mutex_base * tree 노드 삽입 pi_tree 노드 삽입 task_struct (waiter) pi_blocked_on → mutex state: TASK_UNINTERRUPTIBLE Lock/Unlock 흐름 1. cmpxchg fast path 2. waiter RB-tree 삽입 3. PI 체인 전파 4. schedule() → sleep 범례: 포인터 참조 RB-tree 삽입
/* include/linux/rtmutex.h — 핵심 구조체 */
struct rt_mutex_base {
    raw_spinlock_t wait_lock;        /* waiter 리스트 보호 */
    struct rb_root_cached waiters;   /* 우선순위 정렬 RB-tree */
    struct task_struct *owner;        /* 현재 owner + 플래그 비트 */
};

struct rt_mutex {
    struct rt_mutex_base rtmutex;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
};

/* 대기자(waiter) 구조체 */
struct rt_mutex_waiter {
    struct rb_node tree;              /* mutex의 waiters RB-tree 노드 */
    struct rb_node pi_tree;           /* owner의 pi_waiters RB-tree 노드 */
    struct task_struct *task;         /* 대기 중인 태스크 */
    struct rt_mutex_base *lock;       /* 대기 중인 mutex */
    unsigned int wake_state;         /* wakeup 상태 */
    struct rt_waiter_node tree_entry; /* 정렬 키: prio, deadline */
    struct rt_waiter_node pi_tree_entry;
};

/* task_struct의 PI 관련 필드 */
struct task_struct {
    /* ... */
    struct rb_root_cached pi_waiters;      /* 이 태스크를 기다리는 waiter들 */
    struct rt_mutex_waiter *pi_top_task;   /* 최고 우선순위 waiter */
    struct rt_mutex_base *pi_blocked_on;  /* 이 태스크가 대기 중인 mutex */
    /* ... */
};

trylock/lock/unlock 코드 워크스루

/* === rt_mutex_trylock() === */
int rt_mutex_trylock(struct rt_mutex *lock)
{
    /* Fast path: owner가 NULL이면 atomic으로 현재 태스크 설정 */
    if (cmpxchg_acquire(&lock->rtmutex.owner, NULL, current))
        return 1;  /* 성공: lock 획득 */
    return 0;      /* 실패: 이미 다른 태스크가 보유 */
}

/* === rt_mutex_lock() 전체 경로 === */
void rt_mutex_lock(struct rt_mutex *lock)
{
    might_sleep();  /* atomic 컨텍스트에서 호출 시 경고 */

    /* Fast path: 비경합 시 cmpxchg로 즉시 획득 */
    if (likely(rt_mutex_cmpxchg_acquire(lock, NULL, current)))
        return;

    /* Slow path: 경합 발생 */
    rt_mutex_slowlock(lock);
}

/* Slow path 상세 */
static int rt_mutex_slowlock_block(struct rt_mutex_base *lock,
                                    struct rt_mutex_waiter *waiter)
{
    for (;;) {
        /* 1. waiter를 mutex의 waiters RB-tree에 삽입 */
        /* 2. waiter를 owner의 pi_waiters RB-tree에 삽입 */
        /* 3. owner의 우선순위 조정 (PI) */
        rt_mutex_adjust_prio_chain(owner, ...);

        /* 4. 현재 태스크를 sleep 상태로 전환 */
        set_current_state(TASK_UNINTERRUPTIBLE);

        /* 5. owner가 unlock할 때까지 대기 */
        if (try_to_take_rt_mutex(lock, current, waiter))
            break;  /* lock 획득 성공 */

        schedule();  /* CPU 양보 */
    }
    return 0;
}

/* === rt_mutex_unlock() === */
void rt_mutex_unlock(struct rt_mutex *lock)
{
    /* Fast path: waiter 없으면 단순 해제 */
    if (likely(rt_mutex_cmpxchg_release(lock, current, NULL)))
        return;

    /* Slow path: waiter가 있으면 */
    /* 1. owner의 pi_waiters에서 이 mutex의 waiter 제거 */
    /* 2. owner의 우선순위 복원 (de-boost) */
    /* 3. 최고 우선순위 waiter를 깨움 */
    rt_mutex_slowunlock(lock);
}
Fast Path vs Slow Path: 비경합 상황(90%+ 경우)에서는 단일 cmpxchg atomic 연산으로 lock/unlock이 완료됩니다. Slow path는 실제 경합이 발생했을 때만 실행되며, PI 체인 조정 비용이 추가됩니다.

ftrace를 이용한 RT 디버깅

ftrace는 PREEMPT_RT 커널의 레이턴시 문제를 진단하는 핵심 도구입니다. preemptirqsoff tracer와 trace-cmd, bpftrace를 조합하면 마이크로초 단위의 지연 원인을 정확히 파악할 수 있습니다.

RT 디버깅 도구 체계 ftrace 커널 인프라 /sys/kernel/debug/tracing/ irqsoff tracer IRQ 비활성화 구간 preemptoff tracer 선점 비활성화 구간 preemptirqsoff 결합 (가장 포괄적) wakeup_rt tracer wakeup→run 지연 사용자 공간 도구 trace-cmd ftrace 래퍼 record + report bpftrace 프로그래밍 가능 필터 히스토그램, 조건 출력 cyclictest RT 레이턴시 측정 히스토그램 생성 hwlatdetect SMI 감지 하드웨어 지연 권장 디버깅 워크플로우 hwlatdetect(SMI 확인) → cyclictest(문제 확인) → trace-cmd(이벤트 분석) → bpftrace(심층 분석)

preemptirqsoff Tracer 실전

# preemptirqsoff: 선점+인터럽트 비활성화 최대 구간 추적
# echo preemptirqsoff > /sys/kernel/debug/tracing/current_tracer
# echo 0 > /sys/kernel/debug/tracing/tracing_max_latency
# echo 1 > /sys/kernel/debug/tracing/tracing_on

# 부하 생성과 동시에 추적
$ sudo stress-ng --cpu 4 --io 2 --vm 2 --timeout 60s

# 최대 지연 확인
# cat /sys/kernel/debug/tracing/tracing_max_latency
23   # 23us: 최대 비선점/irq-off 구간

# 상세 트레이스 확인 (어떤 함수가 23us를 소비했는지)
# cat /sys/kernel/debug/tracing/trace | head -30

cyclictest + trace-cmd 연동

# trace-cmd로 cyclictest 실행 중 이벤트 캡처
$ sudo trace-cmd record \
    -e sched:sched_switch \
    -e sched:sched_wakeup \
    -e irq:irq_handler_entry \
    -e irq:irq_handler_exit \
    -e preemptirq:irq_disable \
    -e preemptirq:irq_enable \
    -e preemptirq:preempt_disable \
    -e preemptirq:preempt_enable \
    -- cyclictest -m -Sp99 -i 1000 -D 30s

# 레이턴시 스파이크 원인 분석
$ sudo trace-cmd report --cpu 2 | grep -A5 "sched_wakeup.*cyclictest"

# wakeup에서 switch까지 시간 차이 계산
$ sudo trace-cmd report | awk '
/sched_wakeup.*cyclictest/ { wakeup_ts = $4 }
/sched_switch.*==> cyclictest/ {
    switch_ts = $4
    latency = switch_ts - wakeup_ts
    if (latency > 0.010) printf("HIGH LATENCY: %.3f ms\n", latency*1000)
}'

# function_graph으로 특정 함수 실행 시간 분석
$ sudo trace-cmd record -p function_graph \
    -g schedule \
    -g try_to_wake_up \
    -P $(pgrep cyclictest) \
    -- sleep 10

$ sudo trace-cmd report | grep -E "^[^|]*[0-9]+\.[0-9]+ us" | sort -rn -k2 | head -20

bpftrace RT 진단 예제

# RT 태스크의 wakeup-to-run 지연 히스토그램 (bpftrace)
$ sudo bpftrace -e '
tracepoint:sched:sched_wakeup
/args->prio < 100/  /* RT 태스크만 (prio 0-99) */
{
    @wakeup[args->pid] = nsecs;
}

tracepoint:sched:sched_switch
/args->next_prio < 100 && @wakeup[args->next_pid]/
{
    $latency = (nsecs - @wakeup[args->next_pid]) / 1000;  /* ns → us */
    @us_hist = hist($latency);
    if ($latency > 50) {
        printf("HIGH LATENCY: pid=%d comm=%s lat=%d us\n",
               args->next_pid, args->next_comm, $latency);
    }
    delete(@wakeup[args->next_pid]);
}

END {
    printf("\nRT Wakeup-to-Run Latency Histogram (us):\n");
    print(@us_hist);
}
'

# IRQ 스레드 실행 시간 측정
$ sudo bpftrace -e '
tracepoint:irq:irq_handler_entry {
    @start[args->irq] = nsecs;
}
tracepoint:irq:irq_handler_exit
/@start[args->irq]/ {
    $dur = (nsecs - @start[args->irq]) / 1000;
    @irq_duration[args->irq] = hist($dur);
    delete(@start[args->irq]);
}
'

# preempt_disable 구간 추적
$ sudo bpftrace -e '
tracepoint:preemptirq:preempt_disable {
    @pd_start[tid] = nsecs;
}
tracepoint:preemptirq:preempt_enable
/@pd_start[tid]/ {
    $dur = (nsecs - @pd_start[tid]) / 1000;
    if ($dur > 20) {
        printf("Long preempt-off: %d us, caller=%s\n", $dur, kstack);
    }
    @pd_hist = hist($dur);
    delete(@pd_start[tid]);
}
'
도구 선택 가이드:
  • ftrace tracer: 커널 빌트인, 오버헤드 최소, 최대값 추적에 최적
  • trace-cmd: ftrace의 사용자 친화적 래퍼, 이벤트 기반 분석
  • bpftrace: 프로그래밍 가능한 필터, 히스토그램, 조건부 출력에 강력
  • perf: 하드웨어 PMU 카운터 기반, 캐시 미스/분기 예측 분석

실시간 튜닝 체크리스트

RT 시스템 튜닝은 커널 빌드 → 부트 파라미터 → 런타임 설정 → 애플리케이션 설정 순서로 체계적으로 진행해야 합니다. 각 단계에서 cyclictest로 개선 효과를 측정하세요.

RT 시스템 튜닝 4단계 체크리스트 1. 커널 빌드 CONFIG_PREEMPT_RT=y CONFIG_HIGH_RES_TIMERS=y CONFIG_NO_HZ_FULL=y CONFIG_RCU_NOCB_CPU=y 디버그 옵션 비활성화 2. 부트 파라미터 isolcpus=N,M nohz_full=N,M rcu_nocbs=N,M processor.max_cstate=1 nmi_watchdog=0 3. 런타임 설정 governor=performance irqbalance 중지 IRQ affinity 설정 THP 비활성화 swap off 4. 애플리케이션 mlockall() SCHED_FIFO 설정 CPU affinity stack prefault malloc 금지 각 단계 후 cyclictest 측정 → 개선 효과 확인 cyclictest -m -Sp99 -i 1000 -D 1h --affinity=N Baseline Max ~200us RT 커널 적용 Max ~50us CPU 격리 적용 Max ~20us 전체 튜닝 완료 Max <10us

커널 부트 파라미터

파라미터 설명 효과
isolcpus 2,3 CPU 2,3을 일반 스케줄러에서 제외 RT 태스크 전용 CPU 확보
nohz_full 2,3 adaptive-tick 모드 (타이머 틱 제거) 주기적 인터럽트 제거
rcu_nocbs 2,3 RCU 콜백을 다른 CPU로 오프로드 RCU 처리 지연 제거
irqaffinity 0,1 IRQ를 CPU 0,1로 제한 격리 CPU의 IRQ 간섭 제거
processor.max_cstate 1 깊은 C-state 진입 방지 wakeup 지연 제거
intel_idle.max_cstate 0 Intel idle 드라이버 C-state 제한 C-state 전환 오버헤드 제거
idle=poll - idle 시 polling 모드 (C0 유지) 최저 지연 (전력 소비 증가)
nosoftlockup - soft lockup 감지 비활성화 긴 RT 태스크 실행 허용
tsc=reliable - TSC를 신뢰할 수 있는 클럭으로 표시 타이머 소스 안정화
nowatchdog - NMI watchdog 비활성화 NMI 지연 제거
nmi_watchdog 0 NMI watchdog 비활성화 (대안) NMI 지연 제거
skew_tick 1 CPU간 타이머 틱 분산 동시 타이머 처리 경합 감소
# /etc/default/grub — 프로덕션 RT 설정 예시
GRUB_CMDLINE_LINUX="isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3 \
irqaffinity=0,1 processor.max_cstate=1 intel_idle.max_cstate=0 \
nosoftlockup tsc=reliable nmi_watchdog=0 skew_tick=1 \
default_hugepagesz=2M hugepagesz=2M hugepages=512"

$ sudo update-grub && sudo reboot

런타임 튜닝

# ============================================
# 1. CPU Governor 고정
# ============================================
$ sudo cpupower frequency-set -g performance

# 또는 모든 CPU에 개별 적용
$ for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
    echo performance | sudo tee $cpu
  done

# ============================================
# 2. IRQ Affinity 설정
# ============================================
# irqbalance 비활성화
$ sudo systemctl stop irqbalance
$ sudo systemctl disable irqbalance

# 모든 IRQ를 CPU 0,1로 제한 (격리 CPU 2,3에서 제외)
$ for irq in /proc/irq/*/smp_affinity; do
    echo 3 | sudo tee $irq 2>/dev/null
  done

# ============================================
# 3. 메모리 관리
# ============================================
# Transparent Huge Pages 비활성화 (예측 불가능 지연 발생)
$ echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled

# swap 비활성화 (page fault 방지)
$ sudo swapoff -a

# vm.stat_interval 증가 (vmstat 업데이트 빈도 감소)
$ sudo sysctl -w vm.stat_interval=120

# ============================================
# 4. 네트워크 튜닝
# ============================================
# Busy polling (네트워크 지연 감소)
$ sudo sysctl -w net.core.busy_read=50
$ sudo sysctl -w net.core.busy_poll=50

# ============================================
# 5. 실시간 그룹 스케줄링
# ============================================
# RT bandwidth 조정
$ sudo sysctl -w kernel.sched_rt_runtime_us=980000  # 98%

# ============================================
# 6. 검증: 튜닝 후 cyclictest
# ============================================
$ sudo cyclictest -m -Sp99 -i 1000 -D 1h \
    --affinity=2 --histogram=200 --histfile=tuned_hist.txt

최종 튜닝 체크리스트

단계 항목 확인 명령 기대 결과
1 RT 커널 확인 uname -a | grep PREEMPT_RT PREEMPT_RT 문자열 포함
2 CPU 격리 확인 cat /sys/devices/system/cpu/isolated 2-3 (격리 CPU 번호)
3 nohz_full 확인 cat /sys/devices/system/cpu/nohz_full 2-3
4 RCU nocbs 확인 cat /sys/kernel/rcu_nocbs 2-3
5 CPU Governor cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor performance
6 IRQ affinity cat /proc/irq/*/smp_affinity 격리 CPU 제외
7 irqbalance 중지 systemctl is-active irqbalance inactive
8 THP 비활성화 cat /sys/.../transparent_hugepage/enabled [never]
9 swap 비활성화 swapon --show 출력 없음
10 SMI 확인 hwlatdetect --duration=60 Max latency <10 us
11 cyclictest 최종 cyclictest -m -Sp99 -D 24h Max <20 us
튜닝 순서 중요: 반드시 커널 빌드 → 부트 파라미터 → 런타임 설정 순서로 진행하세요. 각 단계에서 cyclictest로 효과를 측정한 뒤 다음 단계로 넘어가야 어떤 설정이 실제로 개선을 가져왔는지 파악할 수 있습니다.