Real-Time Linux (PREEMPT_RT)

PREEMPT_RT는 Linux 커널을 낮은 지터의 실시간(Real-time) 시스템으로 운용하기 위한 핵심 기반입니다. 이 문서는 스핀락(Spinlock)의 rtmutex 전환(sleeping spinlock), IRQ 스레딩, 우선순위 상속(PI) 체인, SCHED_FIFO/RR/DEADLINE 운용, raw_spinlock_t·local_lock·RCU_PREEMPT, 고해상도 타이머(hrtimer)와 주기 태스크(Task) 설계, cyclictest/ftrace/osnoise 계측, CPU 격리(Isolation)와 운영 안정성 점검 포인트, 실전 RT 애플리케이션 설계 패턴, 산업용 사례까지 종합적으로 다룹니다.

전제 조건: 동기화 기초스케줄러(Scheduler) 문서를 먼저 읽으세요. 스핀락, 뮤텍스(Mutex), 세마포어(Semaphore)의 기본 개념과 CFS/RT 스케줄링 클래스(Scheduling Class) 이해가 필수적입니다. 메모리 배리어(Memory Barrier), RCU 문서도 함께 보시면 좋습니다.
일상 비유: 일반 리눅스 커널은 혼잡한 사거리와 비슷합니다 — 신호등(스핀락)이 바뀔 때까지 모든 차가 멈춰 기다려야 합니다. PREEMPT_RT는 이 사거리를 입체 교차로(인터체인지)로 바꿉니다 — 긴급 차량(고우선순위 태스크)은 별도의 고가도로를 타고 지체 없이 통과할 수 있고, 일반 차량은 우선순위(Priority)에 따라 양보(Yield)합니다. 우선순위 상속(PI)은 마치 긴급 차량이 앞을 막는 차에게 “잠시 비켜달라”는 사이렌을 울려 그 차도 일시적으로 우선 통행권을 얻는 것과 같습니다.

핵심 요약

PREEMPT_RT는 리눅스 커널의 선점(Preemption) 불가 구간을 최소화하여 최악-케이스 지연(worst-case latency)을 수십 마이크로초 이하로 보장하는 실시간 확장입니다. Ingo Molnar와 Thomas Gleixner가 2004년부터 개발을 시작했으며, 20년간의 점진적 메인라인 병합 끝에 Linux 6.12(2024)에서 완전 통합되었습니다.
  • sleeping spinlockspinlock_t가 RT 환경에서 rt_mutex 기반으로 변환되어, 락 대기 시 busy-wait 대신 sleep합니다.
  • raw_spinlock_t — 진짜 하드웨어 스핀락이 필요한 극소수 경로(인터럽트(Interrupt) 디스크립터, 스케줄러 런큐(Runqueue) 등)에만 사용합니다.
  • threaded IRQ — 하드웨어 인터럽트 핸들러(Handler)를 커널 스레드(Kernel Thread)로 실행하여, IRQ 처리 중에도 고우선순위 RT 태스크가 선점할 수 있습니다.
  • PI chain — 우선순위 역전(priority inversion)을 방지하기 위해, 락 소유자의 우선순위를 대기자 중 최고 우선순위로 임시 상승시킵니다.
  • SCHED_DEADLINE — EDF(Earliest Deadline First) 기반 스케줄링으로, 주기적 RT 태스크에 대역폭(Bandwidth)을 보장합니다.
PREEMPT_RT 핵심 변환 요약
일반 커널 구성 요소RT 커널에서의 변환이유
spinlock_trt_mutex 기반 sleeping lock선점 불가 구간 제거
rwlock_trt_mutex 기반 sleeping lockwriter starvation 방지 + 선점
local_bh_disable()local_lock + migrate_disablesoftirq 직렬화(Serialization)를 per-CPU lock으로
hardirq handlerthreaded IRQ (커널 스레드)IRQ 컨텍스트에서 선점 가능
rcu_read_lock()선점 가능 RCU (RCU_PREEMPT)RCU 읽기 구간 중 선점 허용
softirq커널 스레드(ksoftirqd)에서 실행softirq 지연 제거

단계별 이해

  1. 선점 모델 이해
    PREEMPT_NONE → PREEMPT_VOLUNTARY → PREEMPT (FULL) → PREEMPT_RT 순서로 선점성이 강화됩니다. 각 모델의 차이를 먼저 파악하세요.
  2. Sleeping Spinlock 개념
    RT 커널에서 spin_lock()이 호출되면 실제로는 rt_mutex_lock()이 수행됩니다. 이로 인해 락을 잡지 못하면 태스크가 sleep 상태로 전환되어 다른 태스크가 실행될 수 있습니다.
  3. 우선순위 상속 체인
    태스크 A(높은 우선순위)가 태스크 B(낮은 우선순위)가 보유한 락을 기다리면, B의 우선순위가 A 수준으로 임시 상승합니다. 이 체인은 여러 단계로 전파될 수 있습니다.
  4. Threaded IRQ
    하드웨어 인터럽트가 발생하면 최소한의 top-half만 하드 IRQ에서 실행하고, 나머지는 커널 스레드(bottom-half)에서 처리합니다. 이 스레드(Thread)에는 RT 우선순위를 부여할 수 있습니다.
  5. 측정과 튜닝
    cyclictest, osnoise, hwlat_detector로 최악-케이스 지연을 측정하고, CPU isolation, IRQ affinity, RT throttling 파라미터를 조정합니다.
  6. 애플리케이션 설계
    mlockall(), sched_setattr(), stack prefault, 메모리 풀 사전 할당 등의 패턴으로 사용자 공간(User Space) RT 애플리케이션을 작성합니다.

PREEMPT_RT 개요와 메인라인 병합 역사

배경: 왜 실시간 리눅스인가

전통적으로 리눅스 커널은 처리량(throughput) 최적화에 초점을 맞췄습니다. 그러나 산업 자동화, 로봇 제어, 오디오/비디오 처리, 자동차 ADAS 등의 영역에서는 결정론적 응답 시간이 필수적입니다. “평균 1ms”가 아니라 “최악의 경우에도 반드시 50us 이내”를 보장해야 합니다.

이전에는 RTLinux, RTAI, Xenomai 같은 듀얼-커널(dual-kernel) 접근이 주류였습니다. 이들은 소형 실시간 마이크로커널 위에 리눅스를 비실시간 태스크로 실행하는 방식입니다. 그러나 이 접근은 리눅스 API를 실시간 태스크에서 직접 사용할 수 없는 심각한 제약이 있었습니다.

PREEMPT_RT의 철학: 단일 커널 접근

PREEMPT_RT는 리눅스 커널 자체를 실시간 가능하게 만드는 단일 커널(single-kernel) 접근을 취합니다. 모든 POSIX API, 드라이버, 네트워크 스택(Network Stack)을 그대로 실시간 태스크에서 사용할 수 있어, 듀얼-커널 대비 개발 생산성이 크게 향상됩니다.

메인라인 병합 타임라인

PREEMPT_RT 메인라인 병합 주요 이정표
연도커널 버전병합된 구성 요소주요 기여자
20042.6.x (패치(Patch))PREEMPT_RT 패치셋 최초 공개Ingo Molnar
20052.6.13Generic IRQ layer (genirq)Thomas Gleixner, Ingo Molnar
20062.6.18High-resolution timers (hrtimer)Thomas Gleixner
20062.6.18PREEMPT_RCUPaul McKenney
20092.6.30Threaded IRQ handler (request_threaded_irq)Thomas Gleixner
20092.6.31RT mutex 기반 futex PIThomas Gleixner
20133.10SCHED_DEADLINEJuri Lelli, Luca Abeni
20205.8local_lock APIThomas Gleixner, Sebastian Siewior
20215.15Printk 스레드화 (부분)John Ogness
20226.1PREEMPT_DYNAMIC (부트 타임 선점 모델 선택)Peter Zijlstra
20246.12PREEMPT_RT 완전 메인라인 병합Thomas Gleixner, Sebastian Siewior, Peter Zijlstra
20년의 여정: PREEMPT_RT는 리눅스 역사상 가장 오래 걸린 메인라인 병합 프로젝트 중 하나입니다. 핵심 구성 요소들(hrtimer, genirq, threaded IRQ, preemptible RCU, local_lock, SCHED_DEADLINE 등)이 개별적으로 먼저 병합되어 메인라인 코드 품질을 향상시켰고, 최종적으로 6.12에서 CONFIG_PREEMPT_RT 옵션이 공식적으로 추가되었습니다.
2004 RT 패치 공개 2006 hrtimer genirq 2009 threaded IRQ 2013 SCHED_DEADLINE 2020 local_lock 2022 PREEMPT_DYNAMIC 2024 6.12 병합! 범례: 인프라 IRQ 스케줄러 락/선점 최종 병합

핵심 소스 파일 맵

PREEMPT_RT 관련 핵심 커널 소스 파일
파일역할주요 함수/구조체(Struct)
kernel/locking/rtmutex.cRT mutex 핵심 구현rt_mutex_lock(), rt_mutex_slowlock(), PI chain
kernel/locking/rtmutex_common.hRT mutex 내부 구조체rt_mutex_base, rt_mutex_waiter
kernel/locking/rtmutex_api.cRT mutex 공개 APIrt_mutex_lock(), rt_mutex_unlock()
kernel/locking/spinlock_rt.cRT spinlock 구현rt_spin_lock(), rt_spin_unlock()
include/linux/spinlock_types.hspinlock_t RT 재정의spinlock_trt_mutex_base
include/linux/local_lock.hlocal_lock APIlocal_lock(), local_unlock()
kernel/irq/manage.cIRQ 스레드화irq_setup_forced_threading()
kernel/sched/core.c스케줄러 코어__schedule(), PI boost 연동
kernel/rcu/tree_plugin.hPreemptible RCUrcu_read_lock_nesting
kernel/softirq.csoftirq RT 처리invoke_softirq(), ksoftirqd
kernel/time/hrtimer.c고해상도 타이머hrtimer_interrupt()
kernel/locking/ww_mutex.hWound/Wait mutex (RT 호환)ww_mutex_lock()

PREEMPT_RT가 변환하지 않는 것들

중요: PREEMPT_RT는 모든 것을 변환하지 않습니다. 다음은 변경되지 않는 항목입니다:
  • raw_spinlock_t — 항상 진짜 busy-wait spinlock
  • mutex — 이미 sleeping lock이므로 변경 불필요 (단, PI futex와 연동)
  • semaphore — 변경 없음 (이미 sleeping)
  • completion — 변경 없음
  • atomic_t — 하드웨어 atomic 명령어 기반, 변경 불필요
  • NMI(Non-Maskable Interrupt) 핸들러 — 스레드화 불가능

RT 패치 연대기 상세

PREEMPT_RT의 역사는 2004년 Ingo Molnar의 최초 패치셋 공개로 시작됩니다. 이후 20년에 걸쳐 핵심 인프라가 하나씩 메인라인에 병합되었으며, 각 단계마다 커널 전체의 코드 품질과 선점성이 향상되었습니다.

PREEMPT_RT 연대기 상세 (2004-2024)
연도이벤트상세 내용영향
2004최초 패치셋 공개Ingo Molnar가 voluntary-preempt, PREEMPT_RT 패치셋 발표실시간 리눅스의 새 패러다임 제시
2005genirq 병합IRQ 서브시스템 재설계, 아키텍처 독립적 IRQ 관리 레이어threaded IRQ의 기반 마련
2006hrtimer 병합기존 timer wheel 보완, 나노초 해상도 타이머(Timer)정밀한 주기 실행 가능
2006PREEMPT_RCUPaul McKenney의 선점 가능 RCU 구현RCU 읽기 구간 중 선점 허용
2008ftrace 병합Steven Rostedt의 함수 트레이싱 프레임워크irqsoff/preemptoff 트레이서로 RT 디버깅(Debugging) 혁신
2009threaded IRQrequest_threaded_irq() API 공식 도입IRQ 핸들러 스레드화의 표준 인터페이스
2009PI futexRT mutex 기반 futex 우선순위 상속사용자 공간 PI 동기화
2013SCHED_DEADLINEEDF + CBS 기반 데드라인 스케줄링대역폭 보장 스케줄링
2015OSADL 장기 테스트 시작수백 개 시스템에서 24/7 cyclictest 모니터링RT 커널 안정성 입증 데이터 축적
2017Linux Foundation CII 지원PREEMPT_RT 개발에 공식 재정 지원개발 지속성 보장
2020local_lock 병합per-CPU 데이터 보호를 위한 새 APIRT에서 preempt_disable 대체
2021printk 스레드화 (부분)John Ogness의 printk ringbuffer + console 스레드화printk 중 선점 불가 구간 제거 시작
2022PREEMPT_DYNAMIC부트 타임 선점 모델 선택배포판 단일 커널로 다중 모델 지원
2023printk 완전 스레드화console_lock RT 호환 리팩토링마지막 주요 RT 장벽 제거
2024Linux 6.12 완전 병합CONFIG_PREEMPT_RT 메인라인 공식 옵션20년 프로젝트 완성
PREEMPT_RT 메인라인 병합 상세 연대기 2004 — RT 패치 공개 (Ingo Molnar) voluntary-preempt → sleeping spinlock 개념 도입 2006 — hrtimer + genirq + PREEMPT_RCU 핵심 인프라 3종 병합 → RT 기반 구축 완료 2009 — threaded IRQ + PI futex IRQ 스레드화 + 사용자 공간 PI 동기화 확보 2013 — SCHED_DEADLINE EDF+CBS 대역폭 보장 스케줄링 2020 — local_lock API per-CPU 보호의 RT 호환 인터페이스 확립 2022 — PREEMPT_DYNAMIC 부트 타임 선점 모델 선택 → 배포판 통합 용이 2024 — Linux 6.12 완전 병합! CONFIG_PREEMPT_RT 공식 메인라인 옵션 추가 핵심 기여자 Thomas Gleixner — 총괄 메인테이너 Ingo Molnar — 최초 개발자 Sebastian Siewior — RT 코어 개발 Steven Rostedt — ftrace, RT 인프라 Paul McKenney — PREEMPT_RCU Peter Zijlstra — 스케줄러, PREEMPT_DYNAMIC John Ogness — printk 스레드화 Juri Lelli / Luca Abeni — SCHED_DEADLINE

핵심 기여자와 역할

PREEMPT_RT 핵심 기여자 상세
기여자주요 역할기여 구성 요소소속
Thomas GleixnerRT 총괄 메인테이너genirq, hrtimer, threaded IRQ, RT mutex, printk RT, local_lock 감독Linutronix GmbH
Ingo Molnar최초 개발자voluntary-preempt, 초기 sleeping spinlock, CFS 스케줄러Red Hat
Sebastian SiewiorRT 코어 개발자local_lock, softirq RT 변환, per-CPU 락 변환, RT 패치 유지보수Linutronix GmbH
Steven Rostedtftrace 메인테이너ftrace, function tracer, irqsoff/preemptoff tracer, trace-cmd, RT 테스트Google (VMware 출신)
Paul McKenneyRCU 메인테이너PREEMPT_RCU, RCU_BOOST, Tree RCU, RCU nocbsMeta (IBM/Linux Technology Center 출신)
Peter Zijlstra스케줄러/락 코어PREEMPT_DYNAMIC, RT mutex 리팩토링, lockdep, perfIntel
John Ognessprintk RT화printk ringbuffer, console 스레드화, nbcon (non-blocking console)Linutronix GmbH
Juri LelliSCHED_DEADLINEEDF 스케줄러, CBS, GRUB(Greedy Reclamation of Unused Bandwidth)Red Hat

PREEMPT_RT vs Xenomai vs RTAI vs RTLinux

실시간 리눅스에는 크게 두 가지 접근 방식이 있습니다: 단일 커널(PREEMPT_RT)과 듀얼 커널(Xenomai, RTAI, RTLinux). 각 접근은 지연, API 호환성, 유지보수성에서 뚜렷한 트레이드오프를 가집니다.

단일 커널 vs 듀얼 커널 아키텍처 비교 PREEMPT_RT (단일 커널) 사용자 공간 (POSIX API) RT 태스크 + 일반 태스크 (동일 API) Linux 커널 (RT 패치 적용) sleeping spinlock + threaded IRQ 모든 드라이버 + 모든 시스콜 하드웨어 Xenomai (듀얼 커널) RT 태스크 Alchemy/POSIX skin 일반 태스크 POSIX API Cobalt 코어 RT 마이크로커널 (최고 우선순위) Linux 커널 (idle 태스크로 실행) I-pipe / Dovetail (인터럽트 가상화) 하드웨어 핵심 차이점 PREEMPT_RT: 모든 POSIX API 사용 가능, 드라이버 100% 호환, 메인라인 지원 Xenomai: 1-10us 지연 가능, 별도 API(skin) 학습 필요, 외부 패치 유지보수 부담 RTAI: 최소 지연 가능하나 유지보수 중단, x86 전용, 프로덕션 비권장 RTLinux: Wind River 상용화 → 오픈소스 중단, 역사적 의미만 존재
실무 선택 기준: 2024년 기준으로 PREEMPT_RT의 메인라인 병합으로 인해, 새 프로젝트에서는 PREEMPT_RT를 먼저 검토하는 것이 권장됩니다. Xenomai는 sub-10us 하드 데드라인이 반드시 필요한 EtherCAT 마스터, CNC 보간 등 극소수 영역에서만 고려하세요. RTAI는 기존 레거시 시스템 유지보수 외에는 권장하지 않습니다.
# 각 RT 솔루션의 지연 비교 테스트 (cyclictest 기반)
# PREEMPT_RT 커널
cyclictest -m -S -p 90 -i 1000 -d 0 -l 1000000
# 일반적 결과: Avg 3-5us, Max 15-50us

# Xenomai 3.x (latency 도구)
/usr/xenomai/bin/latency -T 60 -p 1000 -H
# 일반적 결과: Avg 1-3us, Max 5-15us

# 비교 포인트: PREEMPT_RT의 Max가 50us 이하면
# 대부분의 산업용도에 충분합니다

실시간 시스템 특성

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

선점 모델 비교

리눅스 커널은 컴파일 타임(또는 6.1 이후 부트 타임)에 선택할 수 있는 여러 선점 모델을 제공합니다. 각 모델은 처리량과 지연 시간 사이의 트레이드오프를 다르게 설정합니다.

리눅스 커널 선점 모델 비교
모델CONFIG 옵션선점 지점최악-케이스 지연용도
NONEPREEMPT_NONE시스템 콜(System Call) 반환, 인터럽트 반환 시에만수십~수백 ms서버, 처리량 극대화
VOLUNTARYPREEMPT_VOLUNTARYNONE + might_sleep() 체크 지점수 ms ~ 수십 ms데스크톱 (기본값)
FULLPREEMPT커널 코드 어디서든 (spin_unlock 후 등)수백 us ~ 수 ms저지연 데스크톱, 일부 임베디드
RTPREEMPT_RTFULL + sleeping spinlock + threaded IRQ< 100 us (tuned)산업 RT, 로봇, 오디오, 자동차

PREEMPT_DYNAMIC (Linux 6.1+)

CONFIG_PREEMPT_DYNAMIC이 활성화되면, 부트 파라미터 preempt=로 런타임에 선점 모델을 선택할 수 있습니다. 이는 배포판이 하나의 커널 바이너리로 서버(NONE)와 데스크톱(FULL) 모두를 지원할 수 있게 합니다.

# 부트 파라미터로 선점 모델 선택
# /etc/default/grub 또는 부트로더 설정
GRUB_CMDLINE_LINUX="preempt=full"

# 런타임에 현재 모델 확인
cat /sys/kernel/debug/sched/preempt
# 출력: full (또는 none, voluntary)
주의: PREEMPT_DYNAMIC은 NONE/VOLUNTARY/FULL 사이에서만 전환 가능합니다. PREEMPT_RT는 별도의 CONFIG_PREEMPT_RT로 컴파일해야 하며, 동적 전환 대상이 아닙니다.
선점 모델별 커널 실행 구간과 선점 가능 지점 NONE VOL FULL RT 커널 코드 실행 (선점 불가) 반환 might_sleep() 체크 지점 spin_unlock()/preempt_enable() 후 선점 raw_spinlock만 비선점 — 나머지 전부 선점 가능 선점 불가 구간 선점 가능 지점

PREEMPT_NONE 상세 (서버/HPC)

PREEMPT_NONE은 커널 코드 실행 중 절대로 선점하지 않습니다. 시스템 콜이나 인터럽트 핸들러가 완료된 후 사용자 공간으로 복귀하는 시점에서만 스케줄링 결정이 이루어집니다. 이 모델은 처리량을 극대화하므로 데이터베이스 서버, HPC(고성능 컴퓨팅), 파일 서버에 적합합니다.

/* PREEMPT_NONE에서의 커널 실행 흐름 */
/*
 * syscall_entry → 커널 모드 진입
 *   → 긴 파일시스템 연산 (수 ms 가능)
 *   → 선점 체크 없음
 *   → 연산 완료
 * syscall_exit → 여기서만 need_resched 체크
 *   → 필요시 schedule() 호출
 *
 * 결과: 최악 케이스 = 가장 긴 시스콜 실행 시간
 *       (ext4 journal commit: 수십~수백 ms)
 */

/* kernel/entry/common.c */
static void syscall_exit_to_user_mode_prepare(struct pt_regs *regs)
{
    /* PREEMPT_NONE: 여기서만 reschedule */
    if (unlikely(tif_need_resched()))
        schedule();  /* 선점 지점 */
}

PREEMPT_VOLUNTARY 상세 (데스크톱)

PREEMPT_VOLUNTARY은 PREEMPT_NONE에 might_sleep()/cond_resched() 체크 지점을 추가합니다. 커널 소스 전체에 약 500여 개의 cond_resched() 호출이 분포되어 있어, 장시간 실행되는 커널 경로에서도 주기적으로 선점 기회를 제공합니다.

/* PREEMPT_VOLUNTARY의 핵심: cond_resched() */
/* include/linux/sched.h */
#ifdef CONFIG_PREEMPT_VOLUNTARY
static inline int cond_resched(void)
{
    if (should_resched(0)) {
        schedule();  /* 자발적 선점 */
        return 1;
    }
    return 0;
}
#else
/* PREEMPT_NONE에서는 아무것도 안 함 */
static inline int cond_resched(void) { return 0; }
#endif

/* 사용 예: 긴 루프에서 주기적 체크 */
/* fs/ext4/inode.c 등에서 흔히 볼 수 있음 */
for (i = 0; i < huge_count; i++) {
    process_block(i);
    cond_resched();  /* 필요시 양보 */
}

PREEMPT_FULL 상세 (범용)

PREEMPT(FULL)에서는 커널 코드 실행 중에도 preempt_count가 0이 되는 모든 지점(예: spin_unlock(), preempt_enable())에서 선점이 가능합니다. preempt_count는 선점 비활성화 중첩 카운터, softirq 카운터, hardirq 카운터를 합친 값입니다.

/* preempt_count 구조 (include/linux/preempt.h) */
/*
 * PREEMPT_MASK:    0x000000ff  (선점 비활성화 카운트)
 * SOFTIRQ_MASK:    0x0000ff00  (softirq 비활성화 카운트)
 * HARDIRQ_MASK:    0x000f0000  (hardirq 카운트)
 * NMI_MASK:        0x00100000  (NMI)
 * PREEMPT_NEED_RESCHED: 0x80000000  (재스케줄 필요 플래그)
 */

/* preempt_enable() → 선점 체크 */
static inline void preempt_enable(void)
{
    /* preempt_count를 1 감소 */
    if (unlikely(preempt_count_dec_and_test())) {
        /* preempt_count가 0이 됨 → 선점 가능 */
        if (unlikely(tif_need_resched()))
            __preempt_schedule();  /* 즉시 선점! */
    }
}

/* spin_unlock()이 선점 지점이 되는 이유 */
static inline void spin_unlock(spinlock_t *lock)
{
    raw_spin_unlock(&lock->rlock);
    preempt_enable();  /* ← 여기서 선점 가능 */
}

PREEMPT_RT 상세 (실시간)

PREEMPT_RT는 PREEMPT_FULL의 모든 선점 지점에 더해, spinlock_t를 sleeping lock으로 변환하고 IRQ 핸들러를 스레드화하여 비선점(Non-preemptive) 구간을 극소화합니다. 남아있는 비선점 구간은 오직 raw_spinlock_t로 보호되는 극히 짧은 경로(스케줄러 런큐, hrtimer 베이스, IRQ 디스크립터)뿐입니다.

비선점 구간 크기 비교:
  • PREEMPT_NONE: 최대 수백 ms (가장 긴 시스콜 전체)
  • PREEMPT_VOLUNTARY: 최대 수 ms ~ 수십 ms (cond_resched 간격)
  • PREEMPT_FULL: 최대 수백 us ~ 수 ms (가장 긴 spinlock 보유 시간)
  • PREEMPT_RT: 최대 수 us ~ 수십 us (가장 긴 raw_spinlock 보유 시간)

PREEMPT_LAZY (Linux 6.13+)

Linux 6.13에서 Thomas Gleixner가 제안한 PREEMPT_LAZY는 새로운 선점 모델입니다. 기존 PREEMPT_VOLUNTARY을 대체하여, “게으른 선점(lazy preemption)” 개념을 도입합니다. 커널 코드에서 선점이 가능하지만, 즉시 선점하지 않고 다음 적절한 지점(틱 인터럽트 등)까지 지연시킵니다.

/* PREEMPT_LAZY 개념 (Linux 6.13+) */
/*
 * 기존 모델의 문제:
 * - PREEMPT_NONE/VOLUNTARY: 선점 기회가 부족 → 지연 큼
 * - PREEMPT_FULL: 모든 unlock에서 선점 → 캐시 thrashing
 *
 * PREEMPT_LAZY 해결책:
 * - 선점 "가능"하지만, TIF_NEED_RESCHED_LAZY 플래그 사용
 * - LAZY 플래그: 다음 틱에서 선점 (보통 1-4ms 이내)
 * - 일반 TIF_NEED_RESCHED: 즉시 선점 (RT 태스크용)
 *
 * → 일반 태스크: 게으른 선점으로 처리량 유지
 * → RT 태스크: 즉시 선점으로 저지연 보장
 */

/* 의사 코드 */
void check_preempt_lazy(void)
{
    if (tif_need_resched()) {
        schedule();  /* 즉시 선점 (RT 태스크 대기 시) */
    } else if (tif_need_resched_lazy()) {
        /* 다음 틱에서 선점 예약 */
        /* 현재 작업 계속 진행 */
    }
}
PREEMPT_LAZY의 의의: 이 모델이 성숙하면, 향후 리눅스 커널은 PREEMPT_NONE, PREEMPT_VOLUNTARY, PREEMPT_FULL의 세 모델 대신 PREEMPT_LAZY(일반 워크로드)와 PREEMPT_RT(실시간)의 두 모델로 단순화될 수 있습니다. Peter Zijlstra와 Thomas Gleixner가 이 방향으로 작업 중입니다.

PREEMPT_DYNAMIC 런타임 전환 메커니즘

PREEMPT_DYNAMIC은 컴파일 타임이 아닌 부트 타임에 선점 모델을 선택합니다. 내부적으로 static key(jump label)를 사용하여 오버헤드(Overhead) 없이 코드 경로를 전환합니다.

/* kernel/sched/core.c - PREEMPT_DYNAMIC 구현 */
#ifdef CONFIG_PREEMPT_DYNAMIC

/* static key로 선점 모델 제어 */
DEFINE_STATIC_KEY_TRUE(preempt_dynamic_full);
DEFINE_STATIC_KEY_FALSE(preempt_dynamic_none);

/* __cond_resched()의 동적 선택 */
int __sched __cond_resched(void)
{
    /* PREEMPT_NONE: cond_resched가 NOP */
    if (static_branch_unlikely(&preempt_dynamic_none))
        return 0;

    /* PREEMPT_VOLUNTARY: should_resched 체크 후 양보 */
    if (should_resched(0)) {
        schedule();
        return 1;
    }
    return 0;
}

/* 부트 파라미터 파싱 */
static int __init setup_preempt_mode(char *str)
{
    if (!strcmp(str, "none"))
        preempt_dynamic_mode = preempt_dynamic_none;
    else if (!strcmp(str, "voluntary"))
        preempt_dynamic_mode = preempt_dynamic_voluntary;
    else if (!strcmp(str, "full"))
        preempt_dynamic_mode = preempt_dynamic_full;
    return 0;
}
__setup("preempt=", setup_preempt_mode);

#endif /* CONFIG_PREEMPT_DYNAMIC */
# PREEMPT_DYNAMIC 사용법
# 부트 파라미터 설정
GRUB_CMDLINE_LINUX="preempt=full"

# 런타임 확인
cat /sys/kernel/debug/sched/preempt
# 출력: full

# sysfs로 런타임 전환 (6.x 일부 버전)
# 주의: 모든 커널에서 지원되지는 않음
echo voluntary > /sys/kernel/debug/sched/preempt

# static key 상태 확인
cat /sys/kernel/debug/sched/debug | grep preempt
선점 모델별 상세 비교
모델최대 지연처리량 영향컨텍스트 스위치/초주 사용 사례
PREEMPT_NONE수백 ms최대 (기준)~100-500데이터베이스, HPC, 웹 서버
PREEMPT_VOLUNTARY수 ms ~ 수십 ms-1~3%~500-2000범용 데스크톱, 배포판 기본
PREEMPT_FULL수백 us ~ 수 ms-3~5%~2000-10000저지연 데스크톱, 게이밍, 오디오
PREEMPT_RT10-50 us-5~15%~5000-20000산업 RT, 로봇, 자동차, 의료
PREEMPT_LAZY (6.13+)수 ms (일반) / 수십 us (RT)-1~3%가변통합 모델 (미래)

Sleeping Spinlock

PREEMPT_RT의 가장 근본적인 변환은 spinlock_t를 sleeping lock으로 바꾸는 것입니다. 일반 커널에서 spin_lock()은 busy-wait하며 선점을 비활성화합니다. RT 커널에서는 이것이 rt_mutex 기반의 sleeping lock으로 변환되어, 락을 획득하지 못하면 태스크가 sleep 상태로 전환됩니다.

변환 메커니즘

/* include/linux/spinlock_types.h (RT 커널) */

#ifdef CONFIG_PREEMPT_RT
/* spinlock_t는 rt_mutex 기반으로 재정의 */
typedef struct {
    struct rt_mutex_base  lock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map    dep_map;
#endif
} spinlock_t;

/* spin_lock() → rt_spin_lock() */
static inline void spin_lock(spinlock_t *lock)
{
    rt_spin_lock(&lock->lock);
    /* rt_spin_lock은 내부적으로 rt_mutex_lock()을 호출
     * - 락 소유자가 없으면 즉시 획득
     * - 소유자가 있으면 PI chain을 설정하고 sleep
     * - 선점 비활성화 하지 않음! */
}
#else
/* 일반 커널: 기존 busy-wait spinlock */
static inline void spin_lock(spinlock_t *lock)
{
    raw_spin_lock(&lock->rlock);
    /* preempt_disable() + busy-wait */
}
#endif

RT 환경에서의 spin_lock_irqsave

/* RT 커널에서 spin_lock_irqsave()의 변환 */

#ifdef CONFIG_PREEMPT_RT
/* IRQ를 실제로 비활성화하지 않음!
 * 대신 rt_spin_lock()만 호출 */
#define spin_lock_irqsave(lock, flags)    \
do {                                       \
    flags = 0;  /* 사용되지 않음 */         \
    rt_spin_lock(&(lock)->lock);           \
} while (0)

/* spin_unlock_irqrestore도 단순히 rt_spin_unlock */
#define spin_unlock_irqrestore(lock, flags) \
    rt_spin_unlock(&(lock)->lock)
#else
/* 일반 커널: 실제로 IRQ 비활성화 */
#define spin_lock_irqsave(lock, flags)    \
do {                                       \
    local_irq_save(flags);                 \
    raw_spin_lock(&(lock)->rlock);         \
} while (0)
#endif
핵심 통찰: RT 커널에서 spin_lock_irqsave()는 실제로 인터럽트를 비활성화하지 않습니다. 인터럽트 핸들러가 이미 스레드화되어 있으므로, 인터럽트 비활성화 없이도 동기화가 보장됩니다. 이것이 RT 커널의 지연 시간을 극적으로 줄이는 핵심 메커니즘입니다.
spinlock_t 변환: 일반 커널 vs RT 커널 일반 커널 (PREEMPT) spin_lock(&lock) preempt_disable() busy-wait (CPU 소비) 선점 불가 구간! RT 커널 (PREEMPT_RT) spin_lock(&lock) rt_spin_lock() sleep (CPU 양보) 선점 가능! + PI 지원 vs

rwlock_t의 RT 변환

rwlock_t도 RT 커널에서는 rt_mutex 기반으로 변환됩니다. 일반 커널에서 rwlock은 다수 reader가 동시에 접근할 수 있지만, RT 커널에서는 PI를 지원해야 하므로 단일 owner가 있는 mutex로 변환됩니다. 이는 읽기 동시성을 희생하지만, 우선순위 역전을 방지합니다.

/* RT 커널에서의 rwlock_t */
#ifdef CONFIG_PREEMPT_RT
typedef struct {
    struct rt_mutex_base  lock;
    /* reader/writer 구분 없이 단일 rt_mutex */
} rwlock_t;

/* read_lock()도 rt_mutex_lock() 호출 */
static inline void read_lock(rwlock_t *lock)
{
    rt_read_lock(lock);
    /* 내부적으로 rt_mutex 계열 연산 수행
     * 복수 reader 동시 진입 불가
     * 대신 PI chain 보장 */
}
#endif

sleeping spinlock의 성능 영향

일반 커널 vs RT 커널 락 성능 비교 (마이크로벤치마크, 비경합)
연산일반 커널 (ns)RT 커널 (ns)오버헤드 비율비고
spin_lock/unlock (비경합)~15~302xRT: rt_mutex fast-path cmpxchg
spin_lock/unlock (경합(Contention))~수천 (busy-wait)~100 (sleep/wake)0.01x~0.1xRT가 경합 시 훨씬 효율적
spin_lock_irqsave/restore~25~301.2xRT: IRQ 실제 비활성화 안 함
raw_spin_lock/unlock~15~151x동일 (변환 없음)
mutex_lock/unlock (비경합)~20~201x이미 sleeping lock
실전 영향: sleeping spinlock의 비경합 오버헤드(~15ns 추가)는 대부분의 워크로드에서 무시할 수 있는 수준입니다. 반면 경합 상황에서는 busy-wait 대신 sleep하므로 CPU 사이클 낭비가 크게 줄어듭니다. 처리량 감소는 일반적으로 5~15% 범위이며, 이는 결정론적 지연이라는 이점으로 충분히 상쇄됩니다.

Wound/Wait Mutex와 RT

ww_mutex(Wound/Wait Mutex)는 GPU 드라이버(DRM/TTM)에서 다중 버퍼(Buffer) 잠금(Lock)에 사용됩니다. RT 커널에서 ww_mutex도 rt_mutex 기반으로 변환되며, PI 체인을 지원합니다.

/* kernel/locking/ww_mutex.h */
#ifdef CONFIG_PREEMPT_RT
/* ww_mutex는 내부적으로 rt_mutex를 사용
 * Wound/Wait 데드락 회피 프로토콜은 그대로 유지 */
struct ww_mutex {
    struct rt_mutex_base  base;
    struct ww_acquire_ctx *ctx;
};
#endif

/* 사용 패턴 (DRM 드라이버 예시) */
struct ww_acquire_ctx ctx;
ww_acquire_init(&ctx, &reservation_ww_class);

/* 여러 버퍼를 순서대로 잠금 */
ret = ww_mutex_lock(&bo1->lock, &ctx);
if (ret == -EDEADLK) {
    /* Wound/Wait 프로토콜: 양보하고 재시도 */
    ww_mutex_unlock(&bo0->lock);
    ww_mutex_lock_slow(&bo1->lock, &ctx);
    /* 처음부터 재시도 */
}
ww_acquire_done(&ctx);

rt_spin_lock 내부 구현 상세

rt_spin_lock()은 RT 커널에서 spin_lock()의 실제 구현입니다. 내부적으로 rt_mutex의 fast path(cmpxchg)를 먼저 시도하고, 실패하면 slow path로 진입하여 PI chain을 설정하고 sleep합니다.

/* kernel/locking/spinlock_rt.c */
void __sched rt_spin_lock(struct rt_mutex_base *lock)
{
    struct task_struct *owner;

    might_sleep();  /* 디버그: sleeping 가능 컨텍스트 확인 */

    /* Fast path: lock->owner가 NULL이면 cmpxchg로 즉시 획득 */
    if (likely(rt_mutex_cmpxchg_acquire(lock, NULL, current))) {
        /* 성공! 선점 비활성화 없이 락 획득 */
        return;
    }

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

static void __sched rt_spin_lock_slowlock(struct rt_mutex_base *lock)
{
    struct rt_mutex_waiter waiter;
    unsigned long flags;

    /* wait_lock은 raw_spinlock_t (진짜 spinlock) */
    raw_spin_lock_irqsave(&lock->wait_lock, flags);

    /* 한번 더 시도: 다른 CPU가 방금 해제했을 수 있음 */
    if (try_to_take_rt_mutex(lock, current, NULL)) {
        raw_spin_unlock_irqrestore(&lock->wait_lock, flags);
        return;
    }

    /* waiter를 RB-tree에 삽입 (우선순위 순) */
    rt_mutex_init_waiter(&waiter);
    task_blocks_on_rt_mutex(lock, &waiter, current,
                            RT_MUTEX_MIN_CHAINWALK);

    /* PI chain 설정 완료 → sleep 진입 */
    for (;;) {
        set_current_state(TASK_RTLOCK_WAIT);
        raw_spin_unlock_irqrestore(&lock->wait_lock, flags);

        schedule_rtlock();  /* RT 전용 sleep */

        raw_spin_lock_irqsave(&lock->wait_lock, flags);

        /* 깨어남: 락을 획득했는지 확인 */
        if (try_to_take_rt_mutex(lock, current, &waiter))
            break;
    }

    /* waiter 정리 */
    fixup_rt_mutex_waiters(lock, true);
    raw_spin_unlock_irqrestore(&lock->wait_lock, flags);
}
rt_spin_lock() 내부 경로: Fast Path vs Slow Path spin_lock(&lock) rt_spin_lock() cmpxchg(owner, NULL, current)? 성공 Fast Path (~15ns) 즉시 락 획득, 리턴 실패 (경합) rt_spin_lock_slowlock() PI chain 설정 + waiter RB-tree 삽입 schedule_rtlock() → sleep 소유자 해제 → 깨어남 → 락 획득 비경합 비율: ~95%+ (일반 워크로드) Fast path가 성능의 핵심

rwlock_t RT 변환 (rwbase_rt)

RT 커널에서 rwlock_t의 변환은 spinlock_t보다 복잡합니다. Linux 5.15부터 rwbase_rt 인프라가 도입되어, reader/writer를 구분하면서도 PI를 지원하는 구현이 사용됩니다.

/* kernel/locking/rwbase_rt.c (Linux 5.15+) */
/*
 * RT rwlock은 세 가지 상태를 가짐:
 * 1. UNLOCKED: 아무도 보유하지 않음
 * 2. READER_BIAS: reader가 보유 (복수 reader 가능)
 * 3. WRITER: writer가 보유 (배타적)
 *
 * 핵심: reader도 "ownerless wait" PI를 받을 수 있음
 * → writer가 대기 시, 모든 reader에 PI boost 전파
 */

struct rwbase_rt {
    atomic_t            readers;    /* reader 카운트 */
    struct rt_mutex_base rtmutex;   /* writer용 rt_mutex */
};

/* read_lock() RT 변환 */
static int __sched rwbase_read_lock(struct rwbase_rt *rwb,
                                     unsigned int state)
{
    /* Fast path: writer 없으면 reader count 증가 */
    if (rwbase_read_trylock(rwb))
        return 0;

    /* Slow path: writer 대기 중 → rt_mutex 경합 */
    return rwbase_read_lock_slowpath(rwb, state);
}

/* write_lock() RT 변환 */
static int __sched rwbase_write_lock(struct rwbase_rt *rwb,
                                      unsigned int state)
{
    /* rt_mutex 획득 (PI 지원) */
    rt_mutex_slowlock(&rwb->rtmutex, ...);

    /* 모든 reader가 해제될 때까지 대기 */
    if (atomic_read(&rwb->readers) != 0)
        rwbase_write_lock_wait(rwb, state);

    return 0;
}
RT rwlock의 읽기 동시성: 초기 RT 커널에서는 rwlock이 단순히 mutex로 변환되어 읽기 동시성이 완전히 사라졌습니다. Linux 5.15의 rwbase_rt 리팩토링으로, RT 커널에서도 복수 reader가 동시에 읽기 구간에 진입할 수 있게 되었습니다. 다만 writer 대기 시 PI chain이 모든 reader에 전파되어야 하므로 오버헤드는 일반 rwlock보다 높습니다.

spin→sleep 변환의 성능 영향 측정

sleeping spinlock의 실제 성능 영향은 워크로드에 따라 크게 달라집니다. 다음은 대표적인 벤치마크 결과입니다.

워크로드별 PREEMPT_RT 처리량 오버헤드 (PREEMPT_FULL 대비)
워크로드오버헤드병목(Bottleneck) 원인개선 방법
커널 빌드 (make -j$(nproc))3-8%파일시스템(Filesystem) 락 경합tmpfs 사용으로 최소화
Redis (GET/SET 벤치마크)5-10%네트워크 softirq 스레드화ksoftirqd 우선순위 조정
PostgreSQL OLTP8-15%WAL 쓰기 + 락 경합per-CPU 배치 처리
nginx 정적 파일3-5%소켓(Socket) 락SO_REUSEPORT로 분산
cyclictest (RT 지연)N/AN/AMax 15-50us (목표 달성)
hackbench (스케줄러)10-20%런큐 경합 + 컨텍스트 스위치CPU isolation으로 완화
네트워크 스루풋 (iperf3)5-12%NAPI/softirq 스레드화threaded NAPI 활용
# 성능 비교 벤치마크 스크립트
#!/bin/bash
# bench_rt_overhead.sh - RT vs non-RT 커널 성능 비교

echo "=== 커널 빌드 벤치마크 ==="
time make -j$(nproc) -s vmlinux 2>/dev/null

echo "=== hackbench (스케줄러 부하) ==="
hackbench -s 4096 -l 1000 -g 10 -f 20

echo "=== Redis 벤치마크 ==="
redis-benchmark -t get,set -n 1000000 -c 50 -q

echo "=== cyclictest (RT 지연) ==="
cyclictest -m -S -p 80 -i 1000 -d 0 -l 100000 -q

echo "=== 네트워크 스루풋 ==="
iperf3 -c localhost -t 30 -P 4 --json | jq '.end.sum_sent.bits_per_second'

Threaded IRQ와 동기화

PREEMPT_RT의 두 번째 핵심 변환은 하드웨어 인터럽트 핸들러를 커널 스레드로 실행하는 것입니다. 일반 커널에서 하드웨어 IRQ 핸들러는 모든 선점과 인터럽트를 비활성화한 상태에서 실행되므로, 그 동안 어떤 RT 태스크도 실행될 수 없습니다. RT 커널에서는 IRQ 핸들러가 스레드로 실행되어 우선순위 기반 스케줄링의 대상이 됩니다.

request_threaded_irq API

/* include/linux/interrupt.h */
int request_threaded_irq(unsigned int irq,
                         irq_handler_t handler,      /* top-half (hard IRQ) */
                         irq_handler_t thread_fn,     /* bottom-half (thread) */
                         unsigned long irqflags,
                         const char *devname,
                         void *dev_id);

/* 예시: 일반적인 threaded IRQ 드라이버 패턴 */
static irqreturn_t my_hardirq(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;

    /* 최소한의 작업만: ACK + 데이터 읽기 */
    if (!my_dev_irq_pending(dev))
        return IRQ_NONE;

    my_dev_irq_ack(dev);
    return IRQ_WAKE_THREAD;  /* 스레드 핸들러 깨우기 */
}

static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;

    /* 이 코드는 커널 스레드에서 실행됨
     * → mutex_lock(), sleeping spinlock 사용 가능
     * → RT 태스크에 의해 선점 가능 */
    mutex_lock(&dev->data_lock);
    process_received_data(dev);
    mutex_unlock(&dev->data_lock);

    return IRQ_HANDLED;
}

/* 등록 */
ret = request_threaded_irq(dev->irq, my_hardirq, my_thread_fn,
                           IRQF_SHARED, "my_dev", dev);

IRQ 스레드 우선순위

IRQ 스레드는 기본적으로 SCHED_FIFO 정책, 우선순위 50으로 생성됩니다. 관리자는 chrt 명령이나 /proc/irq/N/smp_affinity로 조정할 수 있습니다.

# IRQ 스레드 확인
ps -eo pid,cls,rtprio,ni,comm | grep irq/
#  PID CLS RTPRIO  NI COMM
#   67  FF     50   - irq/24-xhci_hc
#   68  FF     50   - irq/25-nvme0q0
#   72  FF     50   - irq/26-nvme0q1

# 특정 IRQ 스레드의 우선순위 변경
chrt -f -p 80 67   # irq/24-xhci_hc를 FIFO 우선순위 80으로

# IRQ affinity 설정 (CPU 2,3에만 바인딩)
echo c > /proc/irq/24/smp_affinity
IRQF_NO_THREAD: 일부 인터럽트(타이머, IPI 등)는 IRQF_NO_THREAD 플래그로 스레드화를 방지합니다. 이들은 RT 커널에서도 하드 IRQ 컨텍스트에서 실행되며, raw_spinlock_t만 사용해야 합니다.
Threaded IRQ 실행 흐름 (PREEMPT_RT) HW Interrupt 발생 top-half (hard IRQ) ACK + IRQ_WAKE_THREAD IRQ 스레드 깨우기 wake_up_process() 스케줄러 (우선순위 비교) SCHED_FIFO prio 50 IRQ 스레드 (thread_fn) mutex_lock() 가능, 선점 가능 RT 태스크 (prio 80) IRQ 스레드를 선점! 비교: 일반 커널에서는... 전체 IRQ 핸들러가 hard IRQ 컨텍스트에서 실행 → 어떤 태스크도 선점 불가 지연 시간 = IRQ 핸들러 실행 시간 전체가 최악 케이스에 포함

RT 커널의 강제 스레드화

CONFIG_PREEMPT_RT에서는 커널 부트 파라미터 threadirqs가 자동으로 활성화됩니다. request_irq()로 등록된 핸들러도 자동으로 스레드화됩니다 (IRQF_NO_THREAD 플래그가 없는 경우).

/* kernel/irq/manage.c */
static int irq_setup_forced_threading(struct irqaction *new)
{
    /* RT 커널에서는 IRQF_NO_THREAD가 아닌 모든 핸들러를
     * 강제로 스레드화 */
    if (!force_irqthreads())  /* CONFIG_PREEMPT_RT에서 항상 true */
        return 0;
    if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT))
        return 0;

    /* 기존 handler를 thread_fn으로 이동 */
    new->thread_fn = new->handler;
    new->handler = irq_default_primary_handler;
    /* irq_default_primary_handler는 단순히 IRQ_WAKE_THREAD 반환 */
    return 0;
}

IRQF 플래그와 RT 동작

주요 IRQF 플래그와 RT 커널 영향
플래그일반 커널 동작RT 커널 동작스레드화 여부
IRQF_SHARED여러 디바이스 IRQ 공유동일 (스레드에서 공유)스레드화됨
IRQF_NO_THREAD스레드화 금지하드 IRQ에서 실행 유지스레드화 안 됨
IRQF_ONESHOT스레드 완료까지 IRQ 마스킹동일이미 스레드 패턴
IRQF_PERCPUper-CPU 인터럽트하드 IRQ 유지스레드화 안 됨
IRQF_TIMER타이머 인터럽트하드 IRQ 유지 (NO_THREAD 포함)스레드화 안 됨
IRQF_NOBALANCINGIRQ 밸런싱 제외동일스레드화됨

IRQ 스레드 우선순위 설계 가이드

우선순위 설계 원칙: IRQ 스레드의 우선순위는 그 인터럽트를 사용하는 RT 태스크의 우선순위보다 높거나 같아야 합니다. 일반적인 우선순위 계층은 다음과 같습니다:
권장 RT 우선순위 계층 (높은 순)
우선순위 범위할당 대상예시
99watchdog[watchdog/N]
95-98하드웨어 제약 IRQ타이머, 전원 관리(Power Management)
85-94RT 태스크의 IRQ 스레드irq/N-sensor
80-89주요 RT 태스크제어 루프, 센서 처리
50-79일반 IRQ 스레드 (기본값)irq/N-xhci, irq/N-nvme
10-49보조 RT 태스크로깅, 모니터링
1-9RCU boost, ksoftirqd (선택)[rcuog/N]
# IRQ 스레드 우선순위 일괄 설정 스크립트
#!/bin/bash
# set_irq_priorities.sh

# 센서 IRQ (사용하는 디바이스의 IRQ 번호 확인)
SENSOR_IRQ=$(cat /proc/interrupts | grep my_sensor | awk '{print $1}' | tr -d ':')
SENSOR_THREAD=$(pgrep -f "irq/${SENSOR_IRQ}-")
chrt -f -p 90 $SENSOR_THREAD

# 네트워크 IRQ (낮은 우선순위)
for pid in $(pgrep -f "irq/.*-eth"); do
    chrt -f -p 50 $pid
done

# 확인
ps -eo pid,cls,rtprio,comm | grep irq/ | sort -k3 -rn

IRQ thread 생성 과정

커널은 request_threaded_irq()가 호출되면 전용 커널 스레드를 생성합니다. 스레드 이름은 irq/N-devname 형식이며, SCHED_FIFO 정책으로 실행됩니다.

/* kernel/irq/manage.c - IRQ 스레드 생성 */
static int __setup_irq(unsigned int irq, struct irq_desc *desc,
                        struct irqaction *new)
{
    /* thread_fn이 있으면 커널 스레드 생성 */
    if (new->thread_fn && !nested) {
        struct task_struct *t;

        /* kthread_create()로 스레드 생성 */
        t = kthread_create(irq_thread, new,
                           "irq/%d-%s", irq, new->name);
        if (IS_ERR(t))
            return PTR_ERR(t);

        /* SCHED_FIFO, 우선순위 50으로 설정 */
        sched_setscheduler_nocheck(t, SCHED_FIFO,
                                    &(struct sched_param){.sched_priority = 50});

        new->thread = t;

        /* CPU affinity는 IRQ affinity를 따름 */
        set_cpus_allowed_ptr(t, irq_get_affinity_mask(irq));
    }
    ...
}

/* IRQ 스레드의 메인 루프 */
static int irq_thread(void *data)
{
    struct irqaction *action = data;

    while (!kthread_should_stop()) {
        /* top-half가 IRQ_WAKE_THREAD를 반환할 때까지 대기 */
        wait_event_interruptible(action->thread_waitq,
                                  irq_thread_should_run(action));

        /* thread_fn 실행 (여기서 sleeping lock 사용 가능) */
        action->thread_fn(action->irq, action->dev_id);
    }
    return 0;
}

강제 스레드화 (threadirqs 부트 파라미터)

CONFIG_PREEMPT_RT에서는 threadirqs가 항상 활성화됩니다. 일반 커널에서도 threadirqs 부트 파라미터로 강제 스레드화를 활성화할 수 있습니다. 이는 PREEMPT_RT 없이도 IRQ 지연을 줄이는 간단한 방법입니다.

# 일반 커널에서도 강제 스레드화 사용 가능
GRUB_CMDLINE_LINUX="threadirqs"

# 스레드화된 IRQ 확인
ps -eo pid,cls,rtprio,comm | grep 'irq/'
# 출력: irq/N-devname 스레드들이 SCHED_FIFO로 실행됨

# 특정 IRQ의 스레드화 여부 확인
cat /proc/irq/24/actions
# → 스레드 핸들러가 있는지 확인
IRQF_NO_THREAD 강제 해제 불가: IRQF_NO_THREAD 플래그가 설정된 인터럽트는 threadirqs 파라미터로도 스레드화되지 않습니다. 이 플래그를 사용하는 대표적 예:
  • IRQF_TIMER — 타이머 인터럽트 (hrtimer 동작에 필수)
  • IRQF_PERCPU — per-CPU 인터럽트 (IPI 등)
  • 아키텍처별 NMI-like 인터럽트

IRQ thread CPU 친화도(Affinity) 관리

# IRQ thread의 CPU 친화도는 IRQ 자체의 affinity를 따름
# smp_affinity는 비트마스크, smp_affinity_list는 CPU 목록

# CPU 0,1에만 IRQ 24 바인딩
echo 3 > /proc/irq/24/smp_affinity      # 비트마스크 (0b11)
echo 0-1 > /proc/irq/24/smp_affinity_list  # CPU 목록

# managed_irq 사용 시 커널이 자동 관리
# isolcpus=managed_irq,2-3 → IRQ가 CPU 2,3에 할당되지 않음

# IRQ 밸런스 데몬 비활성화 (RT 시스템 권장)
systemctl stop irqbalance
systemctl disable irqbalance

# 수동 IRQ 분배 스크립트
#!/bin/bash
# distribute_irqs.sh - IRQ를 housekeeping CPU에만 배치
for irq in $(ls /proc/irq/ | grep -E '^[0-9]+$'); do
    # CPU 0,1만 허용 (housekeeping)
    echo 0-1 > /proc/irq/$irq/smp_affinity_list 2>/dev/null
done

primary handler 역할 (hardirq context)

threaded IRQ에서 primary handler(top-half)는 하드 IRQ 컨텍스트에서 실행됩니다. 이 핸들러에서는 반드시 최소한의 작업만 수행해야 하며, sleeping lock 사용이 불가합니다.

primary handler vs thread handler 비교
속성primary handler (top-half)thread handler (bottom-half)
실행 컨텍스트하드 IRQ (인터럽트 비활성화)커널 스레드 (프로세스 컨텍스트)
sleeping lock사용 불가사용 가능 (spinlock_t, mutex)
raw_spinlock_t사용 가능사용 가능
실행 시간 목표< 1us제한 없음 (우선순위 기반)
선점 가능불가가능 (더 높은 우선순위에 의해)
전형적 작업ACK, 상태 읽기, wake thread데이터 처리, DMA 완료, 사용자 알림
/* 잘 설계된 primary handler 예시 */
static irqreturn_t sensor_hardirq(int irq, void *dev_id)
{
    struct sensor_dev *sdev = dev_id;
    u32 status;

    /* 1. 인터럽트 상태 읽기 (MMIO) - ~100ns */
    status = readl(sdev->regs + SENSOR_STATUS);

    /* 2. 이 디바이스의 인터럽트인지 확인 */
    if (!(status & SENSOR_IRQ_PENDING))
        return IRQ_NONE;  /* 공유 IRQ에서 다른 디바이스 */

    /* 3. 인터럽트 ACK (재발생 방지) - ~100ns */
    writel(status, sdev->regs + SENSOR_IRQ_CLEAR);

    /* 4. 긴급 데이터를 임시 버퍼에 저장 (선택적) */
    sdev->raw_value = readl(sdev->regs + SENSOR_DATA);

    /* 5. 스레드 핸들러 깨우기 */
    return IRQ_WAKE_THREAD;
    /* 전체 실행 시간: ~300-500ns */
}

우선순위 상속 체인 (PI chain)

우선순위 역전(priority inversion)은 실시간 시스템의 가장 위험한 문제입니다. 1997년 NASA의 Mars Pathfinder 임무에서 이 문제가 실제로 발생하여 시스템이 반복적으로 리셋되었습니다. PREEMPT_RT는 우선순위 상속(Priority Inheritance, PI) 프로토콜로 이를 해결합니다.

우선순위 역전 시나리오

우선순위 역전 예시 (PI 없음)
시점Low (prio 10)Mid (prio 50)High (prio 90)문제
t0lock(L) 획득--
t1임계영역 실행-lock(L) 대기High가 Low 대기
t2선점됨!실행 중 (무한 루프 등)lock(L) 대기무기한 역전!
t3실행 불가계속 실행계속 대기High가 Mid보다 늦게 실행

PI 프로토콜로 해결

/* kernel/locking/rtmutex.c - PI chain 핵심 로직 */

/*
 * rt_mutex_adjust_prio_chain() — PI 체인을 따라가며 우선순위 전파
 *
 * waiter의 우선순위가 변경될 때 호출됨.
 * lock owner → owner가 대기 중인 lock의 owner → ... 순으로 체인 추적.
 */
static int __sched 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;

    /* 체인 길이 제한 (데드락 감지) */
    if (++detect_deadlock > max_lock_depth) {
        /* 데드락 또는 과도한 체인 깊이 */
        return -EDEADLK;
    }

    /* 1. 현재 태스크가 대기 중인 lock 찾기 */
    waiter = task->pi_blocked_on;
    if (!waiter)
        return 0;  /* 체인 끝 */

    lock = waiter->lock;

    /* 2. lock의 top_waiter (최고 우선순위 대기자) 갱신 */
    top_waiter = rt_mutex_top_waiter(lock);

    /* 3. lock 소유자의 PI 리스트 갱신 */
    rt_mutex_enqueue_pi(task, waiter);

    /* 4. 소유자의 유효 우선순위 조정 */
    rt_mutex_adjust_prio(task);

    /* 5. 소유자가 다른 lock을 대기 중이면 체인 계속 추적 */
    next_lock = task_blocked_on_lock(task);
    if (next_lock)
        return rt_mutex_adjust_prio_chain(task, chwalk,
                                          orig_lock, next_lock, ...);
    return 0;
}

rt_mutex_waiter 구조체

/* kernel/locking/rtmutex_common.h */
struct rt_mutex_waiter {
    struct rb_node      tree_entry;    /* lock의 waiters RB-tree에 삽입 */
    struct rb_node      pi_tree_entry; /* owner의 PI waiters RB-tree에 삽입 */
    struct task_struct  *task;         /* 대기 중인 태스크 */
    struct rt_mutex_base *lock;        /* 대기 중인 lock */
    unsigned int        wake_state;
    int                 prio;          /* 대기자의 우선순위 */
    u64                 deadline;      /* SCHED_DEADLINE용 */
};
PI Chain 전파 과정 Task High (prio 90) Lock A 대기 중 Task Mid (prio 50) Lock A 소유, Lock B 대기 Task Low (prio 10) Lock B 소유 Lock A Lock B 대기 소유 대기 소유 PI 전파 (prio 90이 체인을 따라 전파) High: prio 90 (변경 없음) PI Mid: 50 → 90 (부스트됨!) PI Low: 10 → 90 (부스트됨!) Low가 prio 90으로 실행 → Lock B 해제 → Mid가 Lock B 획득 → Lock A 해제 → High 실행

PI와 futex: 사용자 공간 동기화

POSIX pthread_mutex는 커널의 futex 시스콜로 구현됩니다. PTHREAD_PRIO_INHERIT 속성을 설정하면 PI futex가 활성화되어, 사용자 공간에서도 우선순위 상속이 동작합니다.

/* PI futex를 사용하는 pthread_mutex 설정 */
#include <pthread.h>

pthread_mutex_t pi_mutex;
pthread_mutexattr_t attr;

/* 뮤텍스 속성 초기화 */
pthread_mutexattr_init(&attr);

/* 우선순위 상속 프로토콜 설정 */
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);

/* 강건한(robust) 뮤텍스 (소유자 crash 시 자동 해제) */
pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST);

/* 뮤텍스 생성 */
pthread_mutex_init(&pi_mutex, &attr);

/* 사용 */
pthread_mutex_lock(&pi_mutex);
/* 이 lock 대기 중에 커널의 PI chain이 동작 */
critical_section();
pthread_mutex_unlock(&pi_mutex);

pthread_mutexattr_destroy(&attr);
PI chain 깊이 제한: max_lock_depth(기본 1024)를 초과하면 PI chain 추적이 중단되고 -EDEADLK가 반환됩니다. 실전에서 PI chain 깊이가 5를 초과하면 설계를 재검토해야 합니다. 깊은 PI chain은 지연 시간 예측을 어렵게 만듭니다.

PI 동작의 성능 비용

PI chain 연산 비용
연산비용빈도설명
waiter 삽입 (RB-tree)O(log n)매 경합마다n = 해당 lock의 waiter 수
PI chain 추적O(d)매 경합마다d = chain 깊이 (보통 1-3)
우선순위 조정O(log m)chain 노드마다m = owner의 PI waiter 수
스케줄러 재배치(Relocation)O(1) ~ O(log n)부스트/디부스트 시런큐 RB-tree 재삽입

rt_mutex_adjust_prio_chain() 알고리즘 상세

PI chain 전파 알고리즘은 rt_mutex_adjust_prio_chain()에 구현되어 있습니다. 이 함수는 재귀적으로(실제로는 반복문으로) 체인을 따라가며 우선순위를 조정합니다.

PI chain 추적 알고리즘:

  1. 현재 태스크(task)가 대기 중인 lock을 찾습니다
  2. lock의 waiter 리스트에서 top_waiter를 갱신합니다
  3. lock 소유자의 PI 리스트를 갱신합니다
  4. 소유자의 유효 우선순위를 재계산합니다
  5. 소유자가 다른 lock을 대기 중이면 → 4단계로 이동합니다 (체인 추적)
  6. 체인 끝에 도달하면 종료합니다

시간 복잡도는 O(d * log n)입니다. d는 체인 깊이(보통 1-3, 최대 max_lock_depth), n은 각 lock의 waiter 수(RB-tree 연산)입니다. 체인 추적 중 원래 태스크를 다시 만나면 순환 의존으로 판단하여 EDEADLK를 반환합니다.

핵심 데이터 구조 관계:

task->pi_blocked_on → rt_mutex_waiter → lock (대기 중인 lock)
lock->owner → task (소유자)
task->pi_waiters → RB-tree of rt_mutex_waiter (PI 대기자 목록)

PI chain:
High_Task --blocked_on--> Lock_A
                          Lock_A --owner--> Mid_Task
                                            Mid_Task --blocked_on--> Lock_B
                                                                     Lock_B --owner--> Low_Task
PI Chain 단계별 전파 과정 Step 1: High(90)가 Lock A 요청 High (90) 대기 Lock A 소유 Mid (50) Lock B Low (10) Step 2: waiter 삽입 + PI chain 시작 High (90) PI! Mid 50→90 (부스트!) Low (10) Step 3: Mid가 Lock B 대기 중 → chain 계속 High (90) Mid (90) PI chain 전파! Low 10→90 (부스트!) Step 4: Low(90)가 즉시 실행 → Lock B 해제 Low: Lock B 해제 Lock B → Mid 획득 Step 5: Mid가 Lock B 획득 → Lock A 해제 → High 실행 Mid: Lock A 해제 Lock A → High 획득 Step 6: 부스트 해제, 원래 우선순위 복원 High (90) ✓ Mid (50) ✓ Low (10) ✓ 총 지연 = Low의 Lock B 잔여 실행 시간 + Mid의 Lock A 잔여 실행 시간 (최소화됨) PI 없이는 Low가 Mid(50)에 선점되어 무기한 지연 가능!

PI 체인 깊이 제한과 데드락 감지

PI chain 추적은 max_lock_depth(기본 1024) 제한이 있습니다. 이 깊이를 초과하면 데드락으로 간주하고 -EDEADLK를 반환합니다. 실제 데드락도 이 과정에서 감지됩니다.

/* kernel/locking/rtmutex.c - 데드락 감지 */
static int __sched rt_mutex_adjust_prio_chain(
    struct task_struct *task, ...)
{
    int detect_deadlock = 0;

    for (;;) {
        /* 깊이 제한 체크 */
        if (++detect_deadlock > max_lock_depth) {
            /* PI chain이 너무 깊음 → 비정상 */
            static int prev_max;
            if (prev_max != max_lock_depth) {
                prev_max = max_lock_depth;
                printk(KERN_WARNING "Maximum lock depth %d reached "
                       "task: %s (%d)\n",
                       max_lock_depth, task->comm, task->pid);
            }
            return -EDEADLK;
        }

        /* 순환 감지: 체인을 따라가다 원래 태스크를 만남 */
        if (task == top_task) {
            /* 데드락! A→B→A 순환 의존 */
            return -EDEADLK;
        }

        /* 체인 계속 추적... */
    }
}

/* max_lock_depth 조정 (sysctl) */
/* /proc/sys/kernel/max_lock_depth = 1024 (기본) */
PI chain 설계 가이드: 실전에서 PI chain 깊이가 3을 초과하면 설계를 재검토해야 합니다. 깊은 PI chain은 다음과 같은 문제를 야기합니다:
  • chain 추적 오버헤드 증가 (각 단계마다 raw_spinlock 획득)
  • 최악-케이스 지연 예측이 어려움 (chain 깊이 × 단계별 부스팅 시간)
  • 디버깅 난이도 급증
해결책: 락 계층을 평탄화하거나, lock-free 알고리즘으로 대체하세요.

PI futex의 커널 내부 동작

사용자 공간에서 FUTEX_LOCK_PI 시스템 콜이 호출되면, 커널은 futex_lock_pi()를 통해 cmpxchg fast path를 먼저 시도하고, 경합 시 rt_mutex_slowlock()으로 진입하여 PI 프로토콜을 활성화합니다.

상세 코드: futex_lock_pi()의 전체 흐름(fast path cmpxchg, find_task_by_vpid, alloc_pi_state, rt_mutex_slowlock)은 Futex: FUTEX_LOCK_PI 상세에서 코드 레벨로 분석합니다.

RT Mutex 내부 구현

RT mutex는 PREEMPT_RT의 핵심 동기화 원시 타입입니다. 우선순위 상속을 지원하며, kernel/locking/rtmutex.c에 구현되어 있습니다. 일반 mutex와 달리 대기자 큐가 우선순위 기반 RB-tree로 정렬됩니다.

rt_mutex_base 구조체

/* kernel/locking/rtmutex_common.h */
struct rt_mutex_base {
    raw_spinlock_t      wait_lock;    /* waiters 보호용 (진짜 spinlock) */
    struct rb_root_cached waiters;    /* 우선순위 순 RB-tree */
    struct task_struct  *owner;       /* 현재 소유자 */
};

/* 전체 RT mutex (사용자 공간 노출용) */
struct rt_mutex {
    struct rt_mutex_base rtmutex;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map   dep_map;
#endif
};

Fast path / Slow path

/* kernel/locking/rtmutex_api.c */
void __sched rt_mutex_lock(struct rt_mutex *lock)
{
    might_sleep();  /* 디버그: sleeping 가능 컨텍스트 확인 */
    rt_mutex_lock_killable(&lock->rtmutex, TASK_KILLABLE);
}

/* kernel/locking/rtmutex.c */
static int __sched rt_mutex_slowlock(
    struct rt_mutex_base *lock,
    struct ww_acquire_ctx *ww_ctx,
    unsigned int state)
{
    struct rt_mutex_waiter waiter;
    int ret;

    raw_spin_lock_irq(&lock->wait_lock);

    /* Fast-path: lock이 비어있으면 즉시 획득 */
    if (try_to_take_rt_mutex(lock, current, NULL)) {
        raw_spin_unlock_irq(&lock->wait_lock);
        return 0;
    }

    /* Slow-path: waiter 설정하고 sleep */
    set_current_state(state);

    /* waiter를 RB-tree에 삽입 (우선순위 순) */
    ret = task_blocks_on_rt_mutex(lock, &waiter, current, ...);

    if (likely(!ret)) {
        /* PI chain 설정 완료, sleep 진입 */
        ret = rt_mutex_slowlock_block(lock, ww_ctx, state,
                                       NULL, &waiter);
    }

    /* 깨어남: waiter 정리 */
    fixup_rt_mutex_waiters(lock, true);
    raw_spin_unlock_irq(&lock->wait_lock);
    return ret;
}
RT Mutex 획득/해제 상태 머신 UNLOCKED LOCKED CONTENDED PI BOOSTED cmpxchg 성공 unlock (대기자 없음) 2번째 태스크 대기 고우선순위 대기자 owner 해제 → top_waiter 획득 마지막 대기자 깨움 waiters RB-tree: 우선순위순 정렬 → O(log n) 삽입/삭제 top_waiter: 항상 최고 우선순위 대기자 → PI boost 대상 결정 wait_lock: raw_spinlock_t (RT에서도 진짜 spinlock, 선점 불가)
데드락 감지: RT mutex는 PI chain을 추적하면서 동시에 데드락을 감지합니다. 체인 깊이가 max_lock_depth(기본 1024)를 초과하면 -EDEADLK를 반환합니다. CONFIG_DEBUG_RT_MUTEXES를 활성화하면 더 상세한 디버그 정보를 얻을 수 있습니다.

rt_mutex_base 구조체 상세 필드 분석

/* rt_mutex_base 필드별 상세 분석 */
struct rt_mutex_base {
    raw_spinlock_t      wait_lock;
    /*
     * wait_lock: raw_spinlock_t를 사용하는 이유
     * → rt_mutex 자체의 보호에 sleeping lock을 쓰면 무한 재귀
     * → 부트스트랩 문제(bootstrap problem) 해결
     * → 이 락의 보유 시간은 매우 짧음 (수 us 이하)
     */

    struct rb_root_cached waiters;
    /*
     * waiters: RB-tree (Red-Black Tree)
     * → 우선순위 순으로 정렬 (FIFO: prio 값, DEADLINE: deadline 값)
     * → rb_root_cached: leftmost 노드를 캐싱하여 top_waiter를 O(1)로 접근
     * → 삽입/삭제: O(log n)
     */

    struct task_struct  *owner;
    /*
     * owner: 현재 소유자 태스크
     * → 하위 비트를 플래그로 활용:
     *   bit 0: RT_MUTEX_HAS_WAITERS (대기자 존재)
     *   나머지: task_struct 포인터
     * → cmpxchg로 atomic 갱신
     */
};

/* owner 필드의 비트 활용 */
#define RT_MUTEX_HAS_WAITERS    1UL

static inline struct task_struct *rt_mutex_owner(struct rt_mutex_base *lock)
{
    unsigned long owner = (unsigned long)READ_ONCE(lock->owner);
    return (struct task_struct *)(owner & ~RT_MUTEX_HAS_WAITERS);
}

fast path (cmpxchg) 상세

RT mutex의 fast path는 비경합 상황에서 단일 cmpxchg 명령어로 락을 획득합니다. 이는 약 10-20ns로 완료되며, 커널 코드 경로를 최소화합니다.

/* Fast path: lock->owner가 NULL이면 즉시 획득 */
static inline bool rt_mutex_cmpxchg_acquire(
    struct rt_mutex_base *lock,
    struct task_struct *old,
    struct task_struct *new)
{
    /*
     * cmpxchg(&lock->owner, NULL, current)
     * → lock->owner == NULL이면 current로 설정하고 true 반환
     * → lock->owner != NULL이면 false 반환 (경합)
     *
     * acquire 의미론: 이 연산 이후의 메모리 접근이
     * 이 연산 이전으로 재정렬되지 않음 보장
     */
    return try_cmpxchg_acquire(&lock->owner, &old, new);
}

/* Fast path unlock: owner를 NULL로 설정 */
static inline bool rt_mutex_cmpxchg_release(
    struct rt_mutex_base *lock,
    struct task_struct *old,
    struct task_struct *new)
{
    /*
     * 대기자가 없으면 (HAS_WAITERS 비트 없음):
     * cmpxchg(&lock->owner, current, NULL) → 즉시 해제
     *
     * 대기자가 있으면:
     * cmpxchg 실패 → slow path로 진입하여 top_waiter 깨움
     */
    return try_cmpxchg_release(&lock->owner, &old, new);
}

slow path (대기/부스팅) 상세

/* kernel/locking/rtmutex.c - slow path 핵심 */
static int __sched rt_mutex_slowlock_block(
    struct rt_mutex_base *lock,
    struct ww_acquire_ctx *ww_ctx,
    unsigned int state,
    struct hrtimer_sleeper *timeout,
    struct rt_mutex_waiter *waiter)
{
    /* 이 함수에서 실제 sleep이 발생 */
    for (;;) {
        /* 시그널/타임아웃 체크 */
        if (timeout && !timeout->task) {
            return -ETIMEDOUT;
        }
        if (signal_pending_state(state, current)) {
            return -EINTR;
        }

        raw_spin_unlock_irq(&lock->wait_lock);

        /* 여기서 실제 sleep! 스케줄러가 다른 태스크 실행 */
        if (state == TASK_RTLOCK_WAIT)
            schedule_rtlock();  /* RT 전용 스케줄링 */
        else
            schedule();

        raw_spin_lock_irq(&lock->wait_lock);

        /* 깨어남: 락을 획득했는지 확인 */
        set_current_state(state);
        if (__rt_mutex_waiter_is_first(lock, waiter) &&
            try_to_take_rt_mutex(lock, current, waiter))
            break;  /* 성공! */
    }
    __set_current_state(TASK_RUNNING);
    return 0;
}

RT mutex 디버깅 (CONFIG_DEBUG_RT_MUTEXES)

# RT mutex 디버깅 활성화
CONFIG_DEBUG_RT_MUTEXES=y
CONFIG_DEBUG_SPINLOCK=y
CONFIG_DEBUG_LOCK_ALLOC=y
CONFIG_PROVE_LOCKING=y       # lockdep: 데드락 패턴 감지
CONFIG_LOCK_STAT=y           # 락 통계 수집

# lockdep으로 RT mutex 잠재적 데드락 감지
# 커널 로그에서 확인:
dmesg | grep -i "possible\|deadlock\|lockdep"

# 락 통계 확인 (경합 빈도, 대기 시간)
cat /proc/lock_stat | head -40
# contentions: 경합 횟수
# waittime-total: 총 대기 시간
# holdtime-total: 총 보유 시간

# RT mutex 관련 tracepoint
echo 1 > /sys/kernel/debug/tracing/events/lock/contention_begin/enable
echo 1 > /sys/kernel/debug/tracing/events/lock/contention_end/enable
cat /sys/kernel/debug/tracing/trace
프로덕션 vs 개발 설정: CONFIG_DEBUG_RT_MUTEXESCONFIG_PROVE_LOCKING은 상당한 오버헤드를 유발합니다 (각각 ~10%, ~30% 성능 저하). 개발 중에 활성화하여 잠재적 문제를 조기에 발견하고, 프로덕션 빌드에서는 반드시 비활성화하세요.

SCHED_FIFO / SCHED_RR / SCHED_DEADLINE과 동기화

리눅스 커널은 세 가지 실시간 스케줄링 정책을 제공합니다. 각 정책은 RT mutex의 우선순위 상속 메커니즘과 긴밀하게 연동됩니다.

실시간 스케줄링 정책 비교
정책알고리즘우선순위 범위타임 슬라이스대역폭 보장PI 동작
SCHED_FIFO고정 우선순위 FIFO1-99없음 (양보 시만 전환)없음우선순위 값으로 비교
SCHED_RR고정 우선순위 Round-Robin1-99100ms (기본)없음우선순위 값으로 비교
SCHED_DEADLINEEDF (Earliest Deadline First)deadline 기반runtime/periodCBS 보장deadline 값으로 비교

SCHED_DEADLINE 설정

/* SCHED_DEADLINE 태스크 설정 예시 */
#include <sched.h>
#include <linux/sched.h>

struct sched_attr {
    __u32 size;
    __u32 sched_policy;
    __u64 sched_flags;
    __s32 sched_nice;
    __u32 sched_priority;
    /* SCHED_DEADLINE 전용 */
    __u64 sched_runtime;   /* 나노초 단위 실행 예산 */
    __u64 sched_deadline;  /* 나노초 단위 상대 데드라인 */
    __u64 sched_period;    /* 나노초 단위 주기 */
};

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

/* sched_setattr() 시스콜로 설정 */
ret = syscall(SYS_sched_setattr, 0, &attr, 0);
SCHED_DEADLINE: CBS(Constant Bandwidth Server) 동작 시간 0 10ms 20ms 30ms runtime 2ms deadline 8ms period 10ms deadline deadline 대역폭 = runtime / period = 2ms / 10ms = 20% CPU 보장
PI와 SCHED_DEADLINE: SCHED_DEADLINE 태스크가 RT mutex를 통해 PI chain에 참여하면, 소유자의 유효 우선순위는 deadline 값으로 비교됩니다. EDF에서는 더 이른 deadline이 더 높은 우선순위를 의미합니다. 이를 DWRR(Deadline-based Weighted Round Robin)이라고도 합니다.

SCHED_FIFO 상세

SCHED_FIFO는 가장 단순한 실시간 정책입니다. 같은 우선순위의 태스크끼리는 FIFO(먼저 큐에 들어간 태스크가 먼저 실행) 순서를 따르며, 더 높은 우선순위 태스크가 도착하면 즉시 선점됩니다. FIFO 태스크는 자발적으로 양보(sched_yield())하거나 블록되기 전까지 무한히 실행됩니다.

/* SCHED_FIFO 설정 (사용자 공간) */
#include <sched.h>

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

/* 현재 프로세스를 SCHED_FIFO로 설정 */
if (sched_setscheduler(0, SCHED_FIFO, &param) == -1) {
    perror("sched_setscheduler");
    /* EPERM: root 권한 또는 CAP_SYS_NICE 필요 */
}

/* 또는 sched_setattr()로 더 세밀한 제어 */
struct sched_attr attr = {
    .size = sizeof(attr),
    .sched_policy = SCHED_FIFO,
    .sched_priority = 80,
    .sched_flags = SCHED_FLAG_RESET_ON_FORK,  /* 자식 프로세스는 SCHED_OTHER로 리셋 */
};
syscall(SYS_sched_setattr, 0, &attr, 0);

SCHED_RR 상세

SCHED_RR은 SCHED_FIFO에 타임 슬라이스를 추가한 정책입니다. 같은 우선순위의 여러 태스크가 있으면 각각 타임 슬라이스(기본 100ms)만큼 실행 후 라운드로빈합니다.

# SCHED_RR 타임 슬라이스 확인/설정
cat /proc/sys/kernel/sched_rr_timeslice_ms
# 100 (기본값: 100ms)

# 조정 (25ms로 변경)
echo 25 > /proc/sys/kernel/sched_rr_timeslice_ms

# SCHED_RR로 프로세스 실행
chrt -r 50 ./my_app    # SCHED_RR, 우선순위 50

# FIFO와 RR 비교 확인
chrt -p $PID
# pid N's current scheduling policy: SCHED_RR
# pid N's current scheduling priority: 50

SCHED_DEADLINE 상세 (CBS, EDF)

SCHED_DEADLINE은 EDF(Earliest Deadline First) 알고리즘을 사용하며, CBS(Constant Bandwidth Server)로 대역폭을 관리합니다. SCHED_DEADLINE 태스크는 SCHED_FIFO/RR보다 항상 높은 우선순위를 가집니다.

/* SCHED_DEADLINE 상세 설정 */
struct sched_attr attr = {
    .size = sizeof(attr),
    .sched_policy = SCHED_DEADLINE,
    .sched_flags = 0,

    /* 3개 파라미터: runtime ≤ deadline ≤ period */
    .sched_runtime  =  2000000,   /* 2ms: 매 주기 최대 실행 시간 */
    .sched_deadline =  5000000,   /* 5ms: 상대 데드라인 */
    .sched_period   = 10000000,   /* 10ms: 주기 */

    /*
     * CBS 대역폭 = runtime / period = 2ms / 10ms = 20%
     * → 이 태스크는 CPU의 20%를 보장받음
     *
     * EDF 우선순위: deadline 값으로 결정
     * → 더 이른 deadline을 가진 태스크가 먼저 실행
     */
};

int ret = syscall(SYS_sched_setattr, 0, &attr, 0);
if (ret < 0) {
    if (errno == EBUSY) {
        /* admission control 실패:
         * 시스템의 총 DEADLINE 대역폭이 한도 초과 */
        fprintf(stderr, "DEADLINE admission control failed\n");
    }
}
SCHED_DEADLINE CBS 대역폭 관리 Task A: rt=2ms, dl=5ms, period=10ms (BW=20%) 0 10ms 20ms dl 5ms Task B: rt=3ms, dl=8ms, period=20ms (BW=15%) dl 8ms Admission Control (입장 제어) 총 대역폭 = Task A (20%) + Task B (15%) = 35% 한도: (M - 0.01) × 100% = 약 95% (M = CPU 수, 단일 CPU 시) 35% < 95% → 허용! 두 태스크 모두 데드라인 보장 수식: Σ(runtime_i / period_i) ≤ M × (1 - sched_rt_runtime_us/sched_rt_period_us)

DEADLINE 대역폭 계산과 관리

# SCHED_DEADLINE 대역폭 확인
cat /proc/sys/kernel/sched_deadline_bandwidth
# 기본: 0 (비활성화)

# GRUB(Greedy Reclamation of Unused Bandwidth) 활성화
# 사용하지 않는 대역폭을 다른 DEADLINE 태스크가 재활용
echo GRUB > /sys/kernel/debug/sched/features

# 현재 DEADLINE 태스크의 대역폭 사용량 확인
cat /proc/sched_debug | grep -A5 "dl_rq"

# DEADLINE 태스크를 chrt로 실행
chrt -d --sched-runtime 2000000 --sched-deadline 5000000 \
        --sched-period 10000000 0 ./my_deadline_app

RT 그룹 스케줄링 (cgroup)

# cgroup v1에서 RT 대역폭 분배
# 루트 cgroup의 총 RT 대역폭
cat /sys/fs/cgroup/cpu/cpu.rt_runtime_us   # 950000 (950ms)
cat /sys/fs/cgroup/cpu/cpu.rt_period_us    # 1000000 (1s)

# RT 태스크 그룹 생성
mkdir /sys/fs/cgroup/cpu/rt_control
echo 500000 > /sys/fs/cgroup/cpu/rt_control/cpu.rt_runtime_us
echo 1000000 > /sys/fs/cgroup/cpu/rt_control/cpu.rt_period_us
# 이 그룹의 RT 태스크에 50% CPU 대역폭 할당

mkdir /sys/fs/cgroup/cpu/rt_sensor
echo 200000 > /sys/fs/cgroup/cpu/rt_sensor/cpu.rt_runtime_us
echo 1000000 > /sys/fs/cgroup/cpu/rt_sensor/cpu.rt_period_us
# 이 그룹에 20% CPU 대역폭 할당

# 태스크 배치
echo $CONTROL_PID > /sys/fs/cgroup/cpu/rt_control/cgroup.procs
echo $SENSOR_PID > /sys/fs/cgroup/cpu/rt_sensor/cgroup.procs

# cgroup v2에서는 cpu.max로 통합 관리
# (RT 전용 인터페이스는 아직 제한적)
RT 스케줄링 정책 선택 가이드
상황권장 정책이유
단일 RT 태스크, 최고 우선순위SCHED_FIFO가장 단순, 오버헤드 최소
같은 우선순위의 여러 RT 태스크SCHED_RR공정한 CPU 분배
주기적 태스크, 대역폭 보장 필요SCHED_DEADLINECBS로 대역폭 격리, admission control
혼합 환경 (RT + 일반)FIFO + RT throttling일반 태스크 기아(Starvation) 방지
EtherCAT/산업 프로토콜SCHED_DEADLINE주기 보장이 핵심

raw_spinlock_t vs spinlock_t

RT 커널에서 spinlock_t가 sleeping lock으로 변환되므로, 절대로 sleep하면 안 되는 경로에는 raw_spinlock_t를 사용해야 합니다. 이 구분은 RT 커널 개발에서 가장 중요한 설계 결정 중 하나입니다.

raw_spinlock_t vs spinlock_t 비교
속성spinlock_traw_spinlock_t
일반 커널 동작busy-wait + preempt_disablebusy-wait + preempt_disable
RT 커널 동작rt_mutex (sleeping)busy-wait + preempt_disable (변경 없음)
선점 비활성화RT에서 안 함항상 함
PI 지원아니오
인터럽트 컨텍스트RT에서 사용 불가사용 가능
임계 구간 길이길어도 됨매우 짧아야 함
사용 예대부분의 드라이버 락스케줄러 런큐, IRQ 디스크립터, hrtimer, printk

선택 기준: Decision Tree

spinlock_t vs raw_spinlock_t 선택 결정 트리 hard IRQ 또는 NMI 컨텍스트에서 사용? Yes raw_spinlock_t No 스케줄러/타이머 코어 경로? Yes raw_spinlock_t No 임계 구간이 매우 짧고 고정적? Yes 검토 후 raw 고려 (드문 경우) No spinlock_t 사용! 기본 원칙: 의심스러우면 spinlock_t를 사용하세요. raw_spinlock_t는 정당한 이유가 있을 때만.

커널 소스에서의 사용 예

/* raw_spinlock_t 사용 사례 */

/* 1. 스케줄러 런큐 (kernel/sched/core.c) */
struct rq {
    raw_spinlock_t  __lock;
    /* 스케줄러 자체가 spinning해야 하므로 sleeping 불가 */
};

/* 2. hrtimer 베이스 (kernel/time/hrtimer.c) */
struct hrtimer_cpu_base {
    raw_spinlock_t      lock;
    /* 타이머 인터럽트 핸들러에서 접근, sleeping 불가 */
};

/* 3. IRQ 디스크립터 (kernel/irq/internals.h) */
struct irq_desc {
    raw_spinlock_t      lock;
    /* 하드 IRQ 핸들러에서 접근 */
};

/* 4. RT mutex의 wait_lock 자체도 raw_spinlock */
struct rt_mutex_base {
    raw_spinlock_t      wait_lock;
    /* 부트스트랩 문제: 이것까지 sleeping이면 무한 재귀 */
};

raw_spinlock_t를 써야 하는 경우

raw_spinlock_t는 다음 세 가지 조건 중 하나 이상을 만족할 때만 사용해야 합니다:

raw_spinlock_t 사용 조건:
  1. 하드 IRQ / NMI 컨텍스트에서 접근 — sleeping lock 사용 불가
  2. 스케줄러/타이머 코어 경로 — schedule() 호출 자체에 필요한 락
  3. RT mutex 인프라 자체를 보호 — 부트스트랩 문제 방지
주요 커널 서브시스템의 raw_spinlock 사용 목록
서브시스템구조체raw_spinlock 이름이유
스케줄러struct rq__lockschedule() 자체에 필수
hrtimerstruct hrtimer_cpu_baselock타이머 인터럽트 핸들러에서 접근
IRQ 서브시스템struct irq_desclock하드 IRQ 핸들러에서 접근
RT mutexstruct rt_mutex_basewait_lock부트스트랩 (sleeping lock의 보호)
printkstruct consolelockNMI에서 출력 가능해야 함
페이지(Page) 할당struct zonelockatomic 할당 컨텍스트
RCUstruct rcu_nodelockgrace period 관리 (인터럽트에서 접근)
시간 관리struct timekeeperlock타이머 인터럽트에서 시간 갱신

spinlock_t ↔ raw_spinlock_t 변환 가이드

/* spinlock_t → raw_spinlock_t 변환이 필요한 경우 */

/* 변환 전: 드라이버에서 IRQ 핸들러와 공유하는 락 */
/* 만약 이 핸들러가 IRQF_NO_THREAD로 등록되었다면: */
struct my_device {
    spinlock_t      lock;  /* 문제: RT에서 sleeping lock이 됨 */
    /* → hard IRQ 핸들러에서 sleep 불가! */
};

/* 변환 후: */
struct my_device {
    raw_spinlock_t  lock;  /* raw: hard IRQ에서 안전 */
};

/* 변환 시 API 변경 */
/* 변환 전 */
spin_lock_init(&dev->lock);
spin_lock_irqsave(&dev->lock, flags);
spin_unlock_irqrestore(&dev->lock, flags);

/* 변환 후 */
raw_spin_lock_init(&dev->lock);
raw_spin_lock_irqsave(&dev->lock, flags);
raw_spin_unlock_irqrestore(&dev->lock, flags);

/* 주의: raw_spinlock 보호 구간은 반드시 짧아야 함! */
/* 긴 처리는 워크큐나 threaded IRQ로 이동 */
변환 실수 방지: spinlock_traw_spinlock_t로 변환할 때 흔한 실수:
  • 보호 구간 내에서 kmalloc(GFP_KERNEL) 호출 — raw_spinlock 하에서 sleep 불가!
  • 보호 구간이 지나치게 긴 경우 — RT 지연의 직접적 원인이 됨
  • 불필요한 raw 사용 — 일반 spinlock_t로 충분한 경우가 대부분
커널 커뮤니티에서는 raw_spinlock_t 추가를 매우 신중하게 리뷰합니다.

Local Lock과 migrate_disable

local_lock은 Linux 5.8에서 도입된 API로, per-CPU 데이터를 보호하면서 RT 커널에서도 올바르게 동작하도록 설계되었습니다. 일반 커널에서는 preempt_disable()과 동일하지만, RT 커널에서는 migrate_disable() + sleeping spinlock으로 변환됩니다.

local_lock API

/* include/linux/local_lock.h */
typedef struct {
#ifdef CONFIG_PREEMPT_RT
    spinlock_t      lock;  /* RT: sleeping spinlock (per-CPU) */
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
    struct task_struct *owner;
#endif
} local_lock_t;

/* 사용 패턴 */
static DEFINE_PER_CPU(local_lock_t, my_local_lock);

void my_function(void)
{
    local_lock(&my_local_lock);
    /* 일반 커널: preempt_disable() → 현재 CPU에 고정
     * RT 커널: migrate_disable() + spin_lock()
     *   → CPU 마이그레이션 비활성화 + sleeping lock 획득
     *   → 선점은 가능! (같은 CPU에서 다른 태스크가 실행될 수 있음)
     */

    /* per-CPU 데이터 안전하게 접근 */
    this_cpu_inc(my_counter);

    local_unlock(&my_local_lock);
}

migrate_disable vs preempt_disable

migrate_disable vs preempt_disable 비교
속성preempt_disable()migrate_disable()
선점비활성화활성 상태 유지
CPU 마이그레이션불가 (선점 꺼짐)불가 (명시적 비활성화)
다른 태스크 실행불가가능 (같은 CPU에서)
sleeping lock 사용불가가능
RT 환경 적합성최소한으로 사용per-CPU 보호에 적합
local_bh_disable() → local_lock: RT 커널에서 local_bh_disable()은 더 이상 softirq를 직접 비활성화하지 않습니다. 대신 local_lock으로 변환되어, softirq 처리를 보호하면서도 선점을 허용합니다. 이것이 RT 커널에서 softirq 지연이 크게 감소하는 핵심 메커니즘입니다.

local_lock API 상세

/* local_lock 전체 API */

/* 선언 및 초기화 */
static DEFINE_PER_CPU(local_lock_t, my_lock);

/* 기본 lock/unlock */
local_lock(&my_lock);
/* 일반 커널: preempt_disable() */
/* RT 커널: migrate_disable() + spin_lock(per-CPU) */
local_unlock(&my_lock);

/* IRQ 보호 변형 */
local_lock_irq(&my_lock);
/* 일반: local_irq_disable() + preempt_disable() */
/* RT: migrate_disable() + spin_lock() (IRQ는 비활성화 안 함!) */
local_unlock_irq(&my_lock);

/* IRQ 상태 보존 변형 */
unsigned long flags;
local_lock_irqsave(&my_lock, flags);
/* 일반: local_irq_save(flags) + preempt_disable() */
/* RT: migrate_disable() + spin_lock() */
local_unlock_irqrestore(&my_lock, flags);

/* softirq 보호 변형 */
local_lock_nested_bh(&my_lock);
/* 일반: local_bh_disable() */
/* RT: migrate_disable() + spin_lock() */
local_unlock_nested_bh(&my_lock);

migrate_disable/enable 내부 동작

/* kernel/sched/core.c - migrate_disable 구현 */
void migrate_disable(void)
{
    struct task_struct *p = current;

    if (p->migration_disabled) {
        p->migration_disabled++;  /* 중첩 가능 */
        return;
    }

    preempt_disable();
    p->migration_disabled = 1;
    p->cpus_ptr = cpumask_of(smp_processor_id());
    /* CPU affinity를 현재 CPU로 제한
     * → 스케줄러가 이 태스크를 다른 CPU로 이동시키지 않음
     * → 하지만 선점은 여전히 가능! */
    preempt_enable();
}

void migrate_enable(void)
{
    struct task_struct *p = current;

    if (p->migration_disabled > 1) {
        p->migration_disabled--;
        return;
    }

    preempt_disable();
    p->migration_disabled = 0;
    p->cpus_ptr = &p->cpus_mask;  /* 원래 CPU affinity 복원 */

    /* 대기 중인 마이그레이션 요청이 있으면 처리 */
    if (p->migration_pending)
        complete_all(p->migration_pending);

    preempt_enable();
}

커널 내 local_lock 사용 사례

커널 서브시스템별 local_lock 사용 사례
서브시스템파일local_lock 이름보호 대상
페이지 할당mm/page_alloc.cpagesets_lockper-CPU 페이지 캐시 (PCP lists)
SLAB 할당mm/slub.cs_lockper-CPU slab 캐시(Cache)
네트워크net/core/dev.cnetdev_lockper-CPU 네트워크 통계
swapmm/swap.cswapvec_lockper-CPU LRU 배치 리스트
VFSfs/namespace.cvfsmount_lockper-CPU 마운트(Mount) 참조 카운트(Reference Count)
randomdrivers/char/random.cbatched_entropy_lockper-CPU 엔트로피 배치

RCU와 RT (RCU_PREEMPT, RCU_BOOST)

일반 커널의 RCU(CONFIG_TREE_RCU)에서는 rcu_read_lock() 구간이 선점 불가능합니다. 이는 RT 커널에서 허용할 수 없는 지연을 유발하므로, CONFIG_PREEMPT_RCU가 활성화되어 RCU 읽기 임계 구간 중에도 선점이 가능합니다.

RCU_PREEMPT 동작

/* RT 커널에서의 rcu_read_lock() */
#ifdef CONFIG_PREEMPT_RCU
static inline void rcu_read_lock(void)
{
    /* 선점 비활성화 하지 않음!
     * 대신 current->rcu_read_lock_nesting 증가 */
    __rcu_read_lock();
    /* 이 사이에서 선점 가능:
     * - 더 높은 우선순위 태스크가 실행될 수 있음
     * - 다만 grace period는 이 태스크가 rcu_read_unlock()
     *   할 때까지 완료되지 않음 */
}

static inline void __rcu_read_lock(void)
{
    current->rcu_read_lock_nesting++;
    /* 선점되더라도 nesting 카운터가 0이 아니면
     * 이 태스크는 RCU 읽기 구간에 있다고 추적됨 */
    barrier();
}
#endif

RCU_BOOST

RCU 읽기 구간에 있는 낮은 우선순위 태스크가 grace period 완료를 지연시키면, CONFIG_RCU_BOOST가 그 태스크의 우선순위를 임시로 상승시킵니다.

# RCU boost 관련 커널 파라미터
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata
# CPU별 RCU 상태 확인

# RCU boost 우선순위 (기본값 1, SCHED_FIFO)
cat /sys/module/rcutree/parameters/kthread_prio
# sysctl로 조정 가능
RCU 모드별 비교
RCU 모드CONFIG 옵션읽기 구간 선점grace periodRT 적합성
Classic RCUTREE_RCU불가빠름부적합
Preemptible RCUPREEMPT_RCU가능추적 필요적합
+ RCU BoostRCU_BOOST가능 + 부스트부스트로 가속필수

RCU 관련 커널 스레드

RT 커널에서 RCU 처리를 담당하는 여러 커널 스레드가 있습니다. rcu_nocbs 파라미터로 RCU 콜백(Callback)을 특정 CPU에서 분리할 수 있습니다.

RCU 관련 커널 스레드
스레드역할격리 CPU 영향설정
rcu_preemptRCU grace period 관리CPU 0에서 실행자동
rcuog/NRCU 콜백 오프로드 실행housekeeping CPU로 이동rcu_nocbs=
rcuop/NRCU 콜백 실행 (이전 방식)housekeeping CPU로 이동rcu_nocbs=
rcub/NRCU boost 스레드필요 시에만 깨움RCU_BOOST=y
# RCU 스레드 확인
ps -eo pid,cls,rtprio,ni,comm | grep rcu
#   11  TS      -   - rcu_preempt
#   12  FF      1   - rcub/0
#   13  TS      -   - rcuog/0
#   14  TS      -   - rcuog/1

# RCU boost 우선순위 설정
echo 2 > /sys/module/rcutree/parameters/kthread_prio
# rcub/* 스레드가 SCHED_FIFO 우선순위 2로 실행

# RCU nocbs 확인
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata
# CPU별 RCU 콜백 상태 표시

SRCU와 RT

SRCU(Sleepable RCU)는 원래 읽기 구간에서 sleep이 가능한 RCU 변형입니다. RT 커널에서 일반 RCU도 선점 가능하지만, SRCU는 도메인별 분리가 필요한 경우 (예: 블록 I/O, 드라이버 모델)에 여전히 사용됩니다. SRCU의 grace period는 전역 RCU와 독립적이므로, 특정 서브시스템의 성능 격리에 유용합니다.

/* SRCU 사용 예시 (드라이버) */
DEFINE_SRCU(my_srcu);

/* 읽기 구간 */
int idx = srcu_read_lock(&my_srcu);
/* 이 사이에서 sleep 가능 (일반 RCU와 달리) */
data = rcu_dereference(my_data);
process(data);
srcu_read_unlock(&my_srcu, idx);

/* 업데이트 (쓰기) */
old_data = rcu_dereference_protected(my_data, lockdep_is_held(&my_lock));
rcu_assign_pointer(my_data, new_data);
synchronize_srcu(&my_srcu);  /* 이 SRCU 도메인의 grace period만 대기 */
kfree(old_data);

local_lock 사용 패턴: 실제 커널 사례

/* mm/page_alloc.c - per-CPU page allocator 보호 */
static DEFINE_PER_CPU(struct per_cpu_pages, boot_pageset);
static DEFINE_PER_CPU(local_lock_t, pagesets_lock) =
    INIT_LOCAL_LOCK(pagesets_lock);

struct page *rmqueue_pcplist(struct zone *preferred_zone,
                             struct zone *zone, ...)
{
    struct per_cpu_pages *pcp;
    struct page *page;

    /* local_lock으로 per-CPU 데이터 보호
     * 일반 커널: preempt_disable()
     * RT 커널: migrate_disable() + sleeping lock */
    local_lock(&pagesets_lock);

    pcp = this_cpu_ptr(zone->per_cpu_pageset);
    page = __rmqueue_pcplist(zone, pcp, ...);

    local_unlock(&pagesets_lock);
    return page;
}

/* net/core/dev.c - 네트워크 per-CPU 통계 보호 */
static DEFINE_PER_CPU(local_lock_t, netdev_lock) =
    INIT_LOCAL_LOCK(netdev_lock);

void dev_queue_xmit_nit(struct sk_buff *skb, ...)
{
    local_lock_nested_bh(&netdev_lock);
    /* per-CPU 통계 업데이트 */
    __this_cpu_inc(softnet_data.processed);
    local_unlock_nested_bh(&netdev_lock);
}

local_lock의 IRQ 변형

local_lock API 변형
API일반 커널RT 커널용도
local_lock()preempt_disable()migrate_disable() + spin_lock()프로세스(Process) 컨텍스트
local_lock_irq()local_irq_disable() + preempt_disable()migrate_disable() + spin_lock()IRQ 보호 필요
local_lock_irqsave()local_irq_save() + preempt_disable()migrate_disable() + spin_lock()IRQ 상태 보존
local_lock_nested_bh()local_bh_disable()migrate_disable() + spin_lock()softirq 보호

RCU_PREEMPT 구현 상세

/* kernel/rcu/tree_plugin.h - Preemptible RCU 내부 */

/*
 * Preemptible RCU의 핵심: rcu_read_lock_nesting 카운터
 *
 * 일반 RCU (TREE_RCU):
 *   rcu_read_lock() → preempt_disable() → 선점 불가
 *   → grace period는 모든 CPU가 "quiescent state"를 지나면 완료
 *
 * Preemptible RCU (PREEMPT_RCU):
 *   rcu_read_lock() → nesting++ (선점 비활성화 안 함!)
 *   → 태스크가 선점되면 "blocked RCU reader" 리스트에 등록
 *   → grace period는 모든 blocked reader가 unlock할 때까지 대기
 */

void __rcu_read_lock(void)
{
    current->rcu_read_lock_nesting++;
    /* 선점 비활성화 하지 않음! */
    /* 대신 스케줄러가 선점 시 rcu_note_context_switch()에서
     * blocked reader를 추적 */
    barrier();  /* 컴파일러 배리어만 */
}

void __rcu_read_unlock(void)
{
    if (--current->rcu_read_lock_nesting == 0) {
        barrier();
        /* 선점되었다가 복귀한 경우 특별 처리 */
        if (unlikely(READ_ONCE(current->rcu_read_unlock_special.s)))
            __rcu_read_unlock_special(current);
    }
}

/* 선점 시 blocked reader 등록 */
void rcu_note_context_switch(void)
{
    struct task_struct *t = current;

    if (t->rcu_read_lock_nesting > 0) {
        /* 이 태스크는 RCU 읽기 구간에서 선점됨 */
        t->rcu_blocked_node = rnp;  /* RCU 노드에 등록 */
        list_add(&t->rcu_node_entry, &rnp->blkd_tasks);
        /* grace period는 이 태스크가 unlock할 때까지 완료 안 됨 */
    }
}

RCU_BOOST 동작 (rcub 스레드 우선순위)

RCU_BOOST는 RCU 읽기 구간에서 선점된 저우선순위 태스크가 grace period를 과도하게 지연시키는 것을 방지합니다. 일정 시간(RCU_BOOST_DELAY) 후 해당 태스크의 우선순위를 부스트합니다.

/* kernel/rcu/tree_plugin.h - RCU boost 로직 */
static void rcu_initiate_boost(struct rcu_node *rnp)
{
    /* blocked reader가 있고, boost 지연 시간이 경과했으면 */
    if (!list_empty(&rnp->blkd_tasks) &&
        time_after(jiffies, rnp->boost_time)) {

        /* rcub/N 스레드 깨우기 */
        wake_up_process(rnp->boost_kthread_task);
    }
}

/* rcub 스레드가 blocked reader를 부스트 */
static int rcu_boost_kthread(void *arg)
{
    struct rcu_node *rnp = arg;

    for (;;) {
        /* 부스트 필요할 때까지 대기 */
        wait_event_interruptible(rnp->boost_wq,
                                  rcu_boost_needed(rnp));

        /* blocked reader의 우선순위를 RT로 부스트 */
        struct task_struct *t;
        list_for_each_entry(t, &rnp->blkd_tasks, rcu_node_entry) {
            /* SCHED_FIFO prio 1로 부스트 (또는 kthread_prio) */
            rt_mutex_setprio(t, kthread_prio);
        }
    }
}
# RCU_BOOST 설정 확인 및 조정
# boost 우선순위 확인
cat /sys/module/rcutree/parameters/kthread_prio
# 1 (기본값: SCHED_FIFO 우선순위 1)

# boost 지연 시간
cat /sys/module/rcutree/parameters/blimit
# 10 (콜백 배치 크기)

# RCU stall 감지 타임아웃
cat /sys/module/rcutree/parameters/rcu_cpu_stall_timeout
# 21 (초)

# RCU boost 우선순위를 5로 높이기
echo 5 > /sys/module/rcutree/parameters/kthread_prio

RT 환경에서 RCU stall 원인과 해결

RT 환경 RCU stall 원인별 진단과 해결
원인증상진단해결
고우선순위 RT 태스크가 CPU 독점RCU stall warning (21초)dmesg에서 stall 메시지sched_rt_runtime_us 또는 DEADLINE 사용
RCU 읽기 구간에서 긴 처리grace period 지연rcudata 파일에서 blocked reader읽기 구간 분할, RCU_BOOST 활성화
nohz_full CPU에서 RCU 콜백 누적메모리 증가rcudata에서 콜백 카운트rcu_nocbs=로 콜백 오프로드
rcuog 스레드가 실행 못 함콜백 처리 지연ps에서 rcuog 상태rcuog 우선순위 조정
RT 환경의 RCU stall 주의: RT 태스크가 rcu_read_lock() 구간에서 장시간 처리하면 grace period가 지연되어 메모리 누수(RCU 콜백 누적)가 발생할 수 있습니다. RCU 읽기 구간은 가능한 짧게 유지하고, 긴 처리가 필요하면 SRCU 사용을 고려하세요.

Softirq 직렬화와 RT

일반 커널에서 softirq는 인터럽트 반환 시점에 실행되며, local_bh_disable()로 보호됩니다. 이는 RT 환경에서 두 가지 문제를 야기합니다:

RT 커널의 softirq 처리

/* RT 커널에서 softirq는 전용 커널 스레드(ksoftirqd)에서 실행 */

/* kernel/softirq.c (RT 경로) */
#ifdef CONFIG_PREEMPT_RT
/*
 * RT에서는 softirq를 인터럽트 반환 시점에서 직접 실행하지 않고,
 * ksoftirqd/N 커널 스레드를 깨워서 처리.
 * → softirq 처리 중에도 고우선순위 RT 태스크가 선점 가능
 */
static inline void invoke_softirq(void)
{
    if (!force_irqthreads() || !__this_cpu_read(ksoftirqd))
        __do_softirq();
    else
        wakeup_softirqd();  /* ksoftirqd 깨우기 */
}
#endif

/* ksoftirqd 스레드 확인 */
/* ps -eo pid,cls,rtprio,ni,comm | grep ksoftirqd
 *   8  TS      -  19 ksoftirqd/0
 *  16  TS      -  19 ksoftirqd/1
 * → nice 19로 실행 (낮은 우선순위)
 * → 필요시 chrt로 RT 우선순위 부여 가능
 */

네트워크 softirq와 RT

네트워크 서브시스템의 NET_RX_SOFTIRQNET_TX_SOFTIRQ는 softirq 중 가장 실행 시간이 긴 경우가 많습니다. RT 커널에서는 이것이 ksoftirqd에서 처리되므로, 네트워크 트래픽이 폭주해도 RT 태스크의 지연에 영향을 미치지 않습니다.

주의: ksoftirqd의 우선순위가 너무 낮으면 네트워크 처리가 지연될 수 있습니다. 고처리량 네트워크 + RT 환경에서는 ksoftirqd의 우선순위를 적절히 조정하거나, NAPI threaded mode를 활용하세요.

Softirq 종류와 RT 영향

리눅스 softirq 종류와 RT 환경 영향
softirq번호일반 커널 실행 위치RT 커널 실행 위치RT 영향도
HI_SOFTIRQ0IRQ 반환 시ksoftirqd낮음
TIMER_SOFTIRQ1IRQ 반환 시ksoftirqd중간
NET_TX_SOFTIRQ2IRQ 반환 시ksoftirqd높음 (대량 전송 시)
NET_RX_SOFTIRQ3IRQ 반환 시ksoftirqd높음 (패킷(Packet) 폭주 시)
BLOCK_SOFTIRQ4IRQ 반환 시ksoftirqd중간
IRQ_POLL_SOFTIRQ5IRQ 반환 시ksoftirqd낮음
TASKLET_SOFTIRQ6IRQ 반환 시ksoftirqd드라이버 의존
SCHED_SOFTIRQ7IRQ 반환 시ksoftirqd중간 (로드 밸런싱)
HRTIMER_SOFTIRQ8IRQ 반환 시ksoftirqd높음 (타이머 정밀도)
RCU_SOFTIRQ9IRQ 반환 시ksoftirqd (rcuog)중간

tasklet의 RT 문제와 대안

tasklet은 RT 환경에서 여러 문제를 야기합니다. softirq 컨텍스트에서 실행되며 직렬화 보장이 불분명하고, sleeping lock 사용이 불가합니다. RT 커널 개발자들은 tasklet 사용을 비권장하며, threaded IRQ 또는 워크큐로 대체할 것을 권합니다.

/* 비권장: tasklet 사용 */
/* tasklet은 RT 환경에서 지연 원인이 될 수 있음 */
DECLARE_TASKLET(my_tasklet, my_tasklet_fn);

void my_hardirq_handler(void)
{
    tasklet_schedule(&my_tasklet);  /* softirq에서 실행됨 */
}

/* 권장: threaded IRQ로 대체 */
static irqreturn_t my_thread_fn(int irq, void *data)
{
    /* 커널 스레드에서 실행 → sleeping lock 사용 가능
     * → 우선순위 제어 가능 → RT 친화적 */
    process_data(data);
    return IRQ_HANDLED;
}

request_threaded_irq(irq, my_quick_check, my_thread_fn,
                     IRQF_ONESHOT, "my_dev", dev);

ksoftirqd 스레딩과 우선순위

# ksoftirqd 스레드 상태 확인
ps -eo pid,cls,rtprio,ni,comm | grep ksoftirqd
#   8  TS      -  19 ksoftirqd/0
#  16  TS      -  19 ksoftirqd/1
#  24  TS      -  19 ksoftirqd/2
#  32  TS      -  19 ksoftirqd/3

# 기본: SCHED_OTHER, nice 19 (가장 낮은 우선순위)
# → RT 태스크를 절대 방해하지 않음
# → 단, 네트워크/블록 softirq 처리가 지연될 수 있음

# 네트워크 처리량이 필요한 경우 우선순위 조정
chrt -o -p 0 8    # nice 0으로 변경 (SCHED_OTHER)
# 또는 RT 우선순위 부여 (주의 필요)
chrt -f -p 10 8   # SCHED_FIFO 우선순위 10

# CPU별 ksoftirqd 대기 시간 모니터링
cat /proc/softirqs
# 각 softirq 유형별 처리 횟수 확인

softirq 유형별 RT 영향

softirq별 RT 환경 영향도:
  • NET_RX/TX_SOFTIRQ — 가장 큰 지연 유발 가능. NAPI가 한 번에 수백 패킷 처리. 해결: threaded NAPI (Linux 5.13+), busy polling, XDP로 우회
  • TIMER_SOFTIRQ — 일반 타이머 콜백 실행. hrtimer는 별도 경로. 영향: 많은 타이머 콜백이 쌓이면 지연 증가
  • BLOCK_SOFTIRQ — 블록 I/O 완료 처리. NVMe IRQ 스레드와 연동. 영향: 대량 I/O 시 지연
  • SCHED_SOFTIRQ — 로드 밸런싱. nohz_full CPU에서는 비활성화됨
  • RCU_SOFTIRQ — RCU 콜백 처리. rcu_nocbs로 오프로드 가능

tasklet 대체 패턴 (threaded work)

/* 비권장: tasklet 사용 (RT 환경에서 문제) */
DECLARE_TASKLET(old_tasklet, old_tasklet_fn);

void old_irq_handler(void)
{
    /* tasklet은 softirq에서 실행
     * → RT 커널에서는 ksoftirqd에서 실행
     * → 우선순위 제어 불가
     * → sleeping lock 사용 불가 */
    tasklet_schedule(&old_tasklet);
}

/* 대안 1: threaded IRQ (가장 권장) */
request_threaded_irq(irq, quick_check, thread_handler,
                     IRQF_ONESHOT, "my_dev", dev);

/* 대안 2: 워크큐 */
static DECLARE_WORK(my_work, my_work_fn);

void new_irq_handler(void)
{
    /* 워크큐는 커널 스레드에서 실행
     * → sleeping lock 사용 가능
     * → WQ_HIGHPRI로 우선순위 제어 가능 */
    queue_work(system_highpri_wq, &my_work);
}

/* 대안 3: 전용 kthread + waitqueue */
static DECLARE_WAIT_QUEUE_HEAD(my_wq);
static atomic_t my_event = ATOMIC_INIT(0);

int my_kthread(void *data)
{
    while (!kthread_should_stop()) {
        wait_event_interruptible(my_wq,
            atomic_read(&my_event) || kthread_should_stop());
        atomic_set(&my_event, 0);
        /* RT 우선순위로 처리 */
        process_data();
    }
    return 0;
}

타이머/hrtimer와 RT 정밀도

PREEMPT_RT의 전신 중 하나인 high-resolution timer(hrtimer)는 나노초 해상도의 타이머를 제공합니다. RT 환경에서 hrtimer는 정확한 주기적 실행의 기반이 됩니다.

hrtimer 아키텍처

/* 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;
    u8                          is_rel;
    u8                          is_soft;      /* soft vs hard 구분 */
    u8                          is_hard;
};

/* RT 환경에서의 hrtimer 사용 */
static enum hrtimer_restart my_timer_callback(struct hrtimer *timer)
{
    struct my_data *data = container_of(timer, struct my_data, timer);

    /* 이 콜백은 hard IRQ 컨텍스트에서 실행됨 (is_hard=1)
     * 또는 softirq 컨텍스트에서 실행됨 (is_soft=1)
     * RT 커널에서 softirq hrtimer는 ksoftirqd에서 실행 */

    do_periodic_work(data);

    /* 다음 주기 설정 */
    hrtimer_forward_now(timer, ns_to_ktime(NSEC_PER_MSEC)); /* 1ms 주기 */
    return HRTIMER_RESTART;
}

hrtimer와 CLOCK_MONOTONIC

RT 애플리케이션에서는 CLOCK_MONOTONIC을 사용하여 NTP 보정에 영향받지 않는 단조 증가 시간을 기준으로 타이머를 설정합니다. clock_nanosleep()과 결합하면 정밀한 주기적 실행이 가능합니다.

/* 사용자 공간: 정밀 주기 실행 패턴 */
#include <time.h>

void periodic_task(long period_ns)
{
    struct timespec next;
    clock_gettime(CLOCK_MONOTONIC, &next);

    while (running) {
        /* 다음 주기 계산 */
        next.tv_nsec += period_ns;
        while (next.tv_nsec >= 1000000000L) {
            next.tv_nsec -= 1000000000L;
            next.tv_sec++;
        }

        /* 절대 시간까지 대기 (드리프트 없음) */
        clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);

        /* 주기적 작업 수행 */
        do_realtime_work();
    }
}

hrtimer soft vs hard mode

Linux 5.4부터 hrtimer는 softhard 모드를 구분합니다. Hard hrtimer는 하드 IRQ 컨텍스트에서 콜백이 실행되고, soft hrtimer는 softirq(RT에서는 ksoftirqd) 컨텍스트에서 실행됩니다.

hrtimer soft vs hard 모드 비교
속성Hard hrtimerSoft hrtimer
실행 컨텍스트하드 IRQ (NMI에 가까움)softirq (RT: ksoftirqd)
sleeping lock 사용불가가능 (RT 커널)
정밀도최고 (하드웨어 한계)softirq 지연 추가
선점 가능불가가능 (RT 커널)
사용 예스케줄러 틱, watchdogPOSIX 타이머, nanosleep
/* hrtimer 초기화 시 soft/hard 모드 선택 */
/* Hard mode (하드 IRQ 컨텍스트 콜백) */
hrtimer_init(&my_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_HARD);
my_timer.function = my_hard_callback;
/* → raw_spinlock_t만 사용 가능 */

/* Soft mode (softirq 컨텍스트 콜백, 기본) */
hrtimer_init(&my_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_timer.function = my_soft_callback;
/* → RT 커널에서 spinlock_t(sleeping) 사용 가능 */

POSIX 타이머와 RT

/* 사용자 공간에서 POSIX 타이머 + 시그널 기반 주기 실행 */
#include <signal.h>
#include <time.h>

static void timer_handler(int sig, siginfo_t *si, void *uc)
{
    /* SIGRTMIN 핸들러에서 RT 작업 수행
     * 주의: 시그널 핸들러 내에서 async-signal-safe 함수만 사용 */
    do_periodic_rt_work();
}

void setup_posix_timer(void)
{
    struct sigevent sev;
    struct itimerspec its;
    timer_t timerid;
    struct sigaction sa;

    /* 시그널 핸들러 설정 */
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = timer_handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGRTMIN, &sa, NULL);

    /* 타이머 생성 (CLOCK_MONOTONIC 사용) */
    sev.sigev_notify = SIGEV_SIGNAL;
    sev.sigev_signo = SIGRTMIN;
    sev.sigev_value.sival_ptr = &timerid;
    timer_create(CLOCK_MONOTONIC, &sev, &timerid);

    /* 1ms 주기 타이머 시작 */
    its.it_value.tv_sec = 0;
    its.it_value.tv_nsec = 1000000;  /* 1ms */
    its.it_interval.tv_sec = 0;
    its.it_interval.tv_nsec = 1000000;
    timer_settime(timerid, 0, &its, NULL);
}
clock_nanosleep vs POSIX 타이머: RT 애플리케이션에서는 clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, ...)을 사용하는 것이 POSIX 시그널(Signal) 타이머보다 권장됩니다. 이유: (1) 시그널 핸들러의 async-signal-safe 제약이 없고, (2) 절대 시간 지정으로 드리프트가 없으며, (3) 코드가 더 간결합니다.

HRTIMER_MODE_SOFT vs HARD

/* hrtimer 모드별 동작 차이 */

/* HRTIMER_MODE_REL_HARD: 하드 IRQ 컨텍스트에서 콜백 */
hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_HARD);
/*
 * 콜백 제약: raw_spinlock_t만 사용 가능
 * 정밀도: 하드웨어 타이머 해상도 (~1ns)
 * 용도: 스케줄러 틱, watchdog, 매우 정밀한 타이밍
 * RT 영향: 비선점 구간 생성 (짧아야 함)
 */

/* HRTIMER_MODE_REL (= _SOFT): softirq 컨텍스트에서 콜백 */
hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
/*
 * 콜백 제약: sleeping lock 사용 가능 (RT 커널)
 * 정밀도: softirq 처리 지연 추가 (~수 us)
 * 용도: POSIX 타이머, nanosleep, 일반 커널 타이머
 * RT 영향: ksoftirqd에서 실행되어 선점 가능
 */

/* 커널 5.4+ softirq hrtimer 마이그레이션 */
/*
 * 이전: 모든 hrtimer가 하드 IRQ에서 실행
 * 이후: 대부분의 hrtimer가 soft 모드로 전환
 * → RT 커널의 비선점 구간 대폭 감소
 *
 * 커널 소스에서 변환 예:
 * - itimer_setup() → HRTIMER_MODE_REL (soft)
 * - posix_timer → HRTIMER_MODE_ABS (soft)
 * - sched_tick → HRTIMER_MODE_REL_PINNED_HARD (hard, 필수)
 */

softirq→hardirq 모드 마이그레이션

일부 타이머는 soft 모드로 시작했다가 특정 조건에서 hard 모드로 전환되어야 합니다. 예를 들어, 고해상도 타이머 서브시스템 자체의 틱 타이머는 hard 모드여야 합니다.

커널 서브시스템별 hrtimer 모드
서브시스템hrtimer 모드이유
스케줄러 틱HARD + PINNED스케줄러 코어 경로, sleeping 불가
watchdogHARDNMI와 연동, 최고 정밀도 필요
POSIX timer (timer_create)SOFT사용자 공간 시그널 전달
nanosleepSOFT프로세스 컨텍스트, sleeping OK
TCP 재전송(Retransmission) 타이머SOFTsoftirq 컨텍스트
perf eventsHARDPMU 인터럽트 핸들러에서 접근

clock_nanosleep 정밀도

/* clock_nanosleep 정밀도 측정 */
#include <time.h>
#include <stdio.h>

void measure_nanosleep_precision(void)
{
    struct timespec start, end, req;
    long long delta_ns;
    long long max_jitter = 0, total_jitter = 0;
    const int iterations = 100000;
    const long period_ns = 1000000;  /* 1ms */

    clock_gettime(CLOCK_MONOTONIC, &req);

    for (int i = 0; i < iterations; i++) {
        /* 다음 깨어날 시간 계산 */
        req.tv_nsec += period_ns;
        if (req.tv_nsec >= 1000000000L) {
            req.tv_nsec -= 1000000000L;
            req.tv_sec++;
        }

        /* 절대 시간까지 대기 */
        clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &req, NULL);

        /* 실제 깨어난 시간 측정 */
        clock_gettime(CLOCK_MONOTONIC, &end);

        /* 지터 계산 (기대 시간과 실제 시간의 차이) */
        delta_ns = (end.tv_sec - req.tv_sec) * 1000000000LL
                 + (end.tv_nsec - req.tv_nsec);

        if (delta_ns > max_jitter) max_jitter = delta_ns;
        total_jitter += delta_ns;
    }

    printf("Iterations: %d, Period: %ldns\n", iterations, period_ns);
    printf("Avg jitter: %lldns\n", total_jitter / iterations);
    printf("Max jitter: %lldns\n", max_jitter);
    /* RT 커널 결과: Avg ~2000ns, Max ~20000ns
     * 일반 커널 결과: Avg ~5000ns, Max ~500000ns+ */
}

CPU Isolation (RT 워크로드 격리)

최소 지연을 달성하려면 RT 태스크 전용 CPU를 다른 모든 커널 활동으로부터 격리해야 합니다. 세 가지 핵심 커널 부트 파라미터를 조합합니다:

# RT 전용 CPU 2,3을 완전 격리하는 GRUB 예시
isolcpus=managed_irq,domain,2-3 nohz_full=2-3 rcu_nocbs=2-3
파라미터역할RT 효과
isolcpus=domain스케줄링 도메인(Scheduling Domain)에서 제외일반 태스크 마이그레이션 차단
nohz_full틱 인터럽트 억제1-tick 주기 지터 제거
rcu_nocbsRCU 콜백 오프로드RCU softirq 지연 제거
managed_irq관리 IRQ 격리디바이스 IRQ 방지
# RT 태스크를 격리된 CPU에 바인딩
taskset -c 2 chrt -f 80 ./rt_app

# IRQ를 하우스키핑 CPU로 이동
for irq in /proc/irq/*/smp_affinity_list; do
    echo 0-1 > "$irq" 2>/dev/null
done

# 격리 상태 확인
cat /sys/devices/system/cpu/isolated     # 2-3
cat /sys/devices/system/cpu/nohz_full    # 2-3
💡

BIOS/하드웨어 튜닝: RT 격리 효과를 극대화하려면 Hyper-Threading 비활성화, C-state를 C1으로 제한(intel_idle.max_cstate=1), SMI 최소화를 BIOS에서 설정하세요.

ℹ️

CPU 격리 심층: cpuset.cpus/cpuset.mems 제약, NUMA 배치, HPC 워크로드 격리, housekeeping CPU 분리 전략은 cpusets & CPU Isolation 페이지를 참고하세요.

Latency 측정 도구

cyclictest

cyclictest는 PREEMPT_RT 지연 측정의 표준 도구입니다. 주기적으로 타이머를 설정하고, 실제 깨어난 시각과 예상 시각의 차이(jitter)를 측정합니다.

# 기본 cyclictest (RT 커널에서 실행)
cyclictest --mlockall --smp --priority=80 \
    --interval=1000 --distance=0 \
    --duration=60 --histogram=200

# 옵션 설명:
# --mlockall    : 모든 페이지 락 (페이지 폴트 방지)
# --smp         : 모든 CPU에 스레드 생성
# --priority=80 : SCHED_FIFO 우선순위 80
# --interval=1000: 1ms 주기
# --duration=60 : 60초 실행
# --histogram=200: 200us까지 히스토그램

# 출력 예:
# T: 0 ( 1234) P:80 I:1000 C: 60000 Min:   1 Act:   4 Avg:   3 Max:  23
# T: 1 ( 1235) P:80 I:1000 C: 60000 Min:   1 Act:   3 Avg:   3 Max:  19
# → Max가 핵심! 최악-케이스 지연

osnoise / hwlat_detector

# osnoise 트레이서 (Linux 5.14+)
# OS 레벨 노이즈를 측정 (NMI, SMI, IRQ, softirq, 스레드)
echo osnoise > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 10
cat /sys/kernel/debug/tracing/trace

# hwlat_detector (하드웨어 지연 감지)
# SMI(System Management Interrupt) 같은 하드웨어 레벨 지연 측정
echo hwlat > /sys/kernel/debug/tracing/current_tracer
echo 1000000 > /sys/kernel/debug/tracing/tracing_thresh  # 1ms 임계값
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 30
cat /sys/kernel/debug/tracing/trace
cyclictest 지연 히스토그램 (RT 커널 vs 일반 커널) 빈도 (샘플 수) 지연 시간 (us) 1 5 10 50 100 500 1000+ RT 커널 (Max ~25us) 일반 커널 (Max ~1000+us)
cyclictest 결과 해석:
  • Min: 최소 지연 (보통 1-2us, 하드웨어 최소 타이머 해상도)
  • Avg: 평균 지연 (RT 커널에서 보통 2-5us)
  • Max: 가장 중요! 최악-케이스 지연 (RT 커널에서 10-50us, 튜닝 시 <20us)
테스트는 최소 24시간, 가능하면 72시간 이상 실행하여 최악 케이스를 충분히 포착해야 합니다.

cyclictest 상세 옵션과 해석

# cyclictest 전체 옵션 가이드

# 기본 테스트 (1분, 모든 CPU)
cyclictest -m -S -p 80 -i 1000 -l 60000

# 프로덕션 검증 테스트 (24시간)
cyclictest \
    --mlockall \                  # 메모리 페이지 락
    --smp \                       # 모든 CPU에 스레드
    --priority=90 \               # SCHED_FIFO 90
    --interval=1000 \             # 1ms 주기 (us 단위)
    --distance=0 \                # 모든 스레드 같은 주기
    --duration=$((24*3600)) \     # 24시간
    --histogram=200 \             # 200us까지 히스토그램
    --histfile=hist.txt \         # 히스토그램 파일 출력
    --quiet                       # 최종 결과만 출력

# 격리 CPU에서만 테스트
cyclictest -m -S -p 90 -i 1000 -l 1000000 \
    --affinity=2-3 \              # CPU 2,3에서만 실행
    --breaktrace=50 \             # 50us 초과 시 ftrace 트리거
    --tracemark                   # trace_marker에 기록

# 결과 해석
# T: 0 (1234) P:90 I:1000 C:1000000 Min:1 Act:3 Avg:3 Max:18
# T: 스레드 번호
# (1234): PID
# P:90: 우선순위
# I:1000: 주기 (us)
# C:1000000: 반복 횟수
# Min/Act/Avg/Max: 최소/현재/평균/최대 지연 (us)
cyclictest 결과 해석 가이드 양호한 결과 (RT 커널 + 튜닝) Min: 1-2 us (하드웨어 해상도) Avg: 2-5 us (정상 범위) Max: 10-30 us (목표: < 50us) 히스토그램: 99.99%가 20us 이내 테스트 시간: 최소 24시간 권장 안전 마진: Max × 2 = 허용 데드라인 문제 있는 결과 (조치 필요) Max > 100us: CPU isolation 재검토 Max > 200us: SMI 또는 C-state 확인 Max > 1000us: 심각! 커널 설정 확인 주기적 스파이크: timer tick (nohz_full?) 불규칙 스파이크: SMI, 전원 관리 점진적 증가: 메모리 누수/RCU stall Max가 높을 때 진단 순서 1. hwlat 실행 2. osnoise 실행 3. irqsoff tracer 4. wakeup_rt SMI/하드웨어? OS 노이즈 원인? 긴 IRQ-off 구간? 스케줄링 지연? hwlat에서 지연이 발견되면 → BIOS 설정 확인 (SMI, C-state) osnoise에서 IRQ 노이즈 → IRQ affinity 재설정, managed_irq 확인

osnoise tracer 상세

# osnoise tracer: OS 레벨 노이즈 원인 분류
cd /sys/kernel/debug/tracing

echo osnoise > current_tracer

# 파라미터 설정
echo 1000000 > osnoise/period_us      # 측정 주기: 1초
echo 10 > osnoise/stop_tracing_us     # 10us 초과 시 트레이싱 중단
echo 2-3 > osnoise/cpus               # 측정 CPU

echo 1 > tracing_on
sleep 60
echo 0 > tracing_on

# 결과 분석
cat trace
# 출력 형식:
# osnoise/CPU:2 ... noise:15us max_single:8us
#   hw:2us  nmi:0us  irq:5us  softirq:3us  thread:5us
#
# hw: 하드웨어 지연 (SMI 등)
# nmi: NMI 처리 시간
# irq: 하드 IRQ 처리 시간
# softirq: softirq 처리 시간
# thread: 다른 스레드에 의한 선점

rtla 도구 (timerlat, osnoise)

# rtla: Real-Time Linux Analysis (Linux 5.17+)
# rt-tests 패키지에 포함

# timerlat: 타이머 기반 지연 측정 (osnoise 발전형)
rtla timerlat top -c 2-3 -d 60 -p 90
# CPU별 IRQ/thread 지연을 분리하여 표시
#
# 출력 예:
#          IRQ Timer Latency (us)    Thread Timer Latency (us)
# CPU     count  min   avg   max    count  min   avg   max
# 2       60000    1     2    12    60000    2     3    18
# 3       60000    1     2    10    60000    2     3    15

# osnoise CLI
rtla osnoise top -c 2-3 -d 60
# OS 노이즈를 실시간 모니터링

# 히스토그램 출력
rtla timerlat hist -c 2-3 -d 60 -b 100 -E 50
# -b 100: 100개 버킷
# -E 50: 50us 초과 시 ftrace 스냅샷

지연 히스토그램 분석 방법

# cyclictest 히스토그램 데이터 생성
cyclictest -m -S -p 90 -i 1000 -l 1000000 \
    --histogram=200 --histfile=hist.txt

# gnuplot으로 히스토그램 시각화
cat <<'EOF' > plot_hist.gp
set terminal png size 800,400
set output 'latency_histogram.png'
set xlabel 'Latency (us)'
set ylabel 'Count'
set title 'Cyclictest Latency Histogram'
set logscale y
set grid
plot 'hist.txt' using 1:2 with boxes title 'CPU 0', \
     'hist.txt' using 1:3 with boxes title 'CPU 1'
EOF
gnuplot plot_hist.gp

# Python으로 백분위수 계산
python3 -c "
import sys
latencies = []
with open('hist.txt') as f:
    for line in f:
        parts = line.strip().split()
        if len(parts) >= 2 and parts[0].isdigit():
            us = int(parts[0])
            count = int(parts[1])
            latencies.extend([us] * count)

latencies.sort()
n = len(latencies)
print(f'P50: {latencies[int(n*0.50)]}us')
print(f'P90: {latencies[int(n*0.90)]}us')
print(f'P99: {latencies[int(n*0.99)]}us')
print(f'P99.9: {latencies[int(n*0.999)]}us')
print(f'P99.99: {latencies[int(n*0.9999)]}us')
print(f'Max: {latencies[-1]}us')
"

ftrace를 이용한 RT 지연 분석

ftrace는 커널 내장 트레이싱 프레임워크로, RT 지연의 원인을 정밀하게 분석할 수 있습니다. 여러 특화된 트레이서가 RT 디버깅에 활용됩니다.

ftrace RT 트레이서 비교
트레이서측정 대상CONFIG 옵션설명
irqsoffIRQ 비활성화 구간IRQSOFF_TRACER가장 긴 IRQ-off 구간과 콜 스택 기록
preemptoff선점 비활성화 구간PREEMPT_TRACER가장 긴 preempt-off 구간 기록
preemptirqsoffIRQ+선점 비활성화두 옵션 모두irqsoff + preemptoff 결합
wakeup_rtRT 태스크 깨우기(Wakeup) 지연SCHED_TRACERwakeup → 실제 실행까지 지연 추적
function_graph함수 호출 그래프FUNCTION_GRAPH_TRACER함수 진입/종료 시간 기록

irqsoff 트레이서 사용

# irqsoff 트레이서 활성화
cd /sys/kernel/debug/tracing

echo irqsoff > current_tracer
echo 1 > tracing_on

# 부하 유발 (예: 디스크 I/O)
dd if=/dev/zero of=/tmp/test bs=1M count=1000

echo 0 > tracing_on
cat trace

# 출력 예:
# irqsoff latency trace v1.1.5 on 6.12.0-rt
# --------------------------------------------------------------------
# latency: 42 us, #4/4, CPU#0 | (M:preempt VP:0, KP:0, SP:0 HP:0)
#    -----------------
#    | task: kworker/0:1-126 (uid:0 nice:0 policy:0 rt_prio:0)
#    -----------------
#  => started at: _raw_spin_lock_irqsave
#  => ended at:   _raw_spin_unlock_irqrestore
#
#                  _------=> CPU#
#                 / _-----=> irqs-off
#                | / _----=> need-resched
#                || / _---=> hardirq/softirq
#                ||| / _--=> preempt-depth
#                |||| /
#   cmd  pid     ||||| time  |   caller
#   ...  ...     d....  0us  : _raw_spin_lock_irqsave
#   ...  ...     d....  42us : _raw_spin_unlock_irqrestore

wakeup_rt 트레이서

# RT 태스크의 wakeup 지연 추적
echo wakeup_rt > current_tracer
echo 1 > tracing_on

# RT 애플리케이션 실행
taskset -c 2 chrt -f 90 ./my_rt_app &

sleep 10
echo 0 > tracing_on
cat trace

# wakeup_rt는 가장 높은 우선순위 RT 태스크가
# 깨어난 시점부터 실제 CPU에서 실행되는 시점까지의
# 최악 케이스를 추적합니다.

function_graph 트레이서로 함수별 지연 분석

# function_graph 트레이서: 특정 함수의 실행 시간 추적
cd /sys/kernel/debug/tracing

echo function_graph > current_tracer

# 특정 함수만 필터링 (예: 스케줄러 관련)
echo '__schedule' > set_graph_function
echo 'schedule' >> set_graph_function
echo 'try_to_wake_up' >> set_graph_function

# 최대 깊이 제한 (출력 양 조절)
echo 5 > max_graph_depth

echo 1 > tracing_on
sleep 5
echo 0 > tracing_on

# 결과 확인
cat trace | head -50
# 출력 예:
#  2)               |  __schedule() {
#  2)   0.350 us    |    rq_clock_task();
#  2)   0.280 us    |    update_rq_clock();
#  2)               |    pick_next_task_rt() {
#  2)   0.190 us    |      _raw_spin_lock();
#  2)   0.280 us    |      dequeue_pushable_task();
#  2)   1.050 us    |    }
#  2)               |    context_switch() {
#  2)   2.500 us    |    }
#  2)   8.200 us    |  }

trace-cmd와 KernelShark

# trace-cmd: ftrace의 명령줄 프론트엔드
# 설치: apt install trace-cmd

# wakeup_rt 지연 기록
trace-cmd record -p wakeup_rt -C mono sleep 30

# 결과 분석
trace-cmd report | grep "wakeup" | sort -k4 -rn | head -10

# KernelShark GUI로 시각적 분석
# 설치: apt install kernelshark
kernelshark trace.dat

# osnoise 트레이서와 결합
trace-cmd record -p osnoise -O stop_on_total=100 \
    -O osnoise:threshold_us=10 sleep 60
trace-cmd report
트레이싱 오버헤드: ftrace 자체도 지연을 추가합니다. function 트레이서는 ~10-30ns/호출, function_graph는 ~50-100ns/호출의 오버헤드를 유발합니다. 프로덕션 RT 시스템에서는 평상 시 트레이서를 비활성화하고, 문제 분석 시에만 활성화하세요. osnoisehwlat는 오버헤드가 매우 낮아 상시 사용 가능합니다.

irqsoff tracer 상세

irqsoff 트레이서는 인터럽트가 비활성화된 구간 중 가장 긴 것을 기록합니다. RT 커널에서도 raw_spinlock_t가 IRQ를 비활성화하므로, 이 트레이서로 최악의 비선점 구간을 찾을 수 있습니다.

# irqsoff 트레이서 사용
cd /sys/kernel/debug/tracing

# 이전 트레이스 초기화
echo 0 > tracing_max_latency
echo irqsoff > current_tracer

# 특정 함수만 필터링 (출력 줄이기)
echo '_raw_spin_lock*' > set_ftrace_filter
echo 'schedule' >> set_ftrace_filter

# 임계값 설정 (10us 이상만 기록)
echo 10 > tracing_thresh

echo 1 > tracing_on

# 부하 유발
stress-ng --cpu 4 --io 2 --vm 2 --timeout 60

echo 0 > tracing_on

# 최대 지연 확인
cat tracing_max_latency
# 42 (42us)

# 상세 트레이스 확인
cat trace
# 가장 긴 IRQ-off 구간의 스택 트레이스 표시

preemptoff tracer 상세

# preemptoff 트레이서: 선점 비활성화 구간 추적
echo preemptoff > current_tracer
echo 0 > tracing_max_latency
echo 1 > tracing_on

# 부하 유발
dd if=/dev/urandom of=/dev/null bs=4k count=100000

echo 0 > tracing_on
cat tracing_max_latency
# RT 커널에서는 이 값이 매우 작아야 함 (< 50us)

# preemptirqsoff: IRQ-off와 preempt-off 결합
echo preemptirqsoff > current_tracer
# 둘 중 하나라도 비활성화된 총 구간을 추적

wakeup_rt tracer 상세

# wakeup_rt: RT 태스크의 wakeup→실행 지연 추적
cd /sys/kernel/debug/tracing

echo wakeup_rt > current_tracer
echo 0 > tracing_max_latency

# 우선순위 필터 (선택적)
echo 80 > tracing_thresh  # 80us 이상만

echo 1 > tracing_on

# RT 애플리케이션 실행
taskset -c 2 chrt -f 90 ./my_rt_app &

sleep 60
echo 0 > tracing_on

# 결과 분석
cat trace
# 출력 예:
# wakeup_rt latency trace v1.1.5 on 6.12.0-rt
# latency: 12 us, #8/8, CPU#2 | (M:preempt VP:0, KP:0, SP:0 HP:0)
# -----------------
# | task: my_rt_app-5678 (uid:0 nice:0 policy:1 rt_prio:90)
# -----------------
# => started at: try_to_wake_up
# => ended at:   __schedule
#
# 12us: wakeup → 실제 실행까지 12us 소요
# 이 값이 높으면: CPU에 더 높은 우선순위 태스크 존재
# 또는 IRQ/softirq가 CPU를 점유 중

trace-cmd RT 분석 실전

# trace-cmd: ftrace의 강력한 CLI 프론트엔드

# 1. RT 지연 원인 종합 분석
trace-cmd record -p function_graph \
    -g __schedule -g try_to_wake_up -g rt_mutex_slowlock \
    -C mono -b 40000 \
    sleep 30

# 2. 결과 분석
trace-cmd report --cpu 2 | head -100

# 3. 특정 지연 이벤트만 필터링
trace-cmd report --filter "duration > 10" | head -50

# 4. osnoise + trace-cmd 결합
trace-cmd record -p osnoise \
    -O osnoise:stop_tracing_us=20 \
    -O osnoise:cpus=2-3 \
    sleep 60

# 5. 멀티 이벤트 동시 수집
trace-cmd record \
    -e sched:sched_switch \
    -e sched:sched_wakeup \
    -e irq:irq_handler_entry \
    -e irq:irq_handler_exit \
    -e lock:contention_begin \
    -e lock:contention_end \
    -C mono sleep 30

trace-cmd report | grep -E "contention|wakeup" | head -30

KernelShark 시각화

KernelShark 활용: kernelshark trace.dat으로 트레이스 데이터를 시각적으로 분석할 수 있습니다.
  • CPU별 태스크 실행 타임라인
  • RT 태스크의 wakeup → 실행 구간을 시각적으로 확인
  • IRQ 핸들러 실행 시간과 빈도
  • 선점/IRQ-off 구간 하이라이트
  • 특정 시간 범위 확대/축소
  • 필터링: 특정 CPU, 태스크, 이벤트만 표시
# KernelShark 설치 및 사용
# Ubuntu/Debian
apt install kernelshark

# trace.dat 생성 후 시각화
trace-cmd record -e sched -e irq -C mono sleep 10
kernelshark trace.dat

# SSH 원격 서버에서 수집 후 로컬에서 분석
scp remote:/tmp/trace.dat .
kernelshark trace.dat

RT Throttling과 /proc/sys/kernel/sched_rt_*

RT 태스크가 CPU를 100% 점유하면 시스템이 응답 불능 상태에 빠질 수 있습니다. 커널은 RT throttling 메커니즘으로 이를 방지합니다.

핵심 파라미터

# RT 태스크의 CPU 대역폭 제한
cat /proc/sys/kernel/sched_rt_runtime_us
# 950000 (기본값: 950ms)

cat /proc/sys/kernel/sched_rt_period_us
# 1000000 (기본값: 1000ms = 1초)

# 의미: 1초 중 최대 950ms만 RT 태스크 실행 가능
# 나머지 50ms는 SCHED_OTHER 태스크에 보장

# RT throttling 비활성화 (주의: 시스템 행 위험)
echo -1 > /proc/sys/kernel/sched_rt_runtime_us
# → RT 태스크가 무제한 CPU 사용 가능
RT throttling 파라미터
파라미터기본값범위설명
sched_rt_period_us1000000 (1s)1 ~ INT_MAXRT 대역폭 측정 주기
sched_rt_runtime_us950000 (950ms)-1 ~ INT_MAX주기 당 최대 RT 실행 시간 (-1: 무제한)
프로덕션 RT 시스템에서: sched_rt_runtime_us = -1로 설정하는 것이 일반적입니다. RT throttling이 활성화되면 RT 태스크가 예기치 않게 일시 정지될 수 있어 데드라인 위반이 발생합니다. 대신 SCHED_DEADLINE의 CBS로 대역폭을 관리하거나, RT 태스크가 CPU를 독점하지 않도록 애플리케이션 레벨에서 보장해야 합니다.

cgroup을 이용한 RT 대역폭 제어

# cgroup v1의 RT 대역폭 제어
mkdir /sys/fs/cgroup/cpu/rt_group
echo 200000 > /sys/fs/cgroup/cpu/rt_group/cpu.rt_runtime_us
echo 1000000 > /sys/fs/cgroup/cpu/rt_group/cpu.rt_period_us
# 이 그룹의 RT 태스크는 1초 중 200ms만 실행 가능

# 태스크 추가
echo $PID > /sys/fs/cgroup/cpu/rt_group/cgroup.procs

커널 빌드 및 설정 가이드 (CONFIG_PREEMPT_RT)

커널 소스 준비

# Linux 6.12+ (PREEMPT_RT 메인라인 포함)
git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
git checkout v6.12

# 또는 RT 전용 브랜치 (6.12 이전 커널)
git clone https://git.kernel.org/pub/scm/linux/kernel/git/rt/linux-stable-rt.git
cd linux-stable-rt
git checkout v6.6-rt15

필수 CONFIG 옵션

# RT 커널 설정
make menuconfig

# General setup → Preemption Model → Fully Preemptible Kernel (Real-Time)
CONFIG_PREEMPT_RT=y

# 고해상도 타이머 (필수)
CONFIG_HIGH_RES_TIMERS=y
CONFIG_NO_HZ_FULL=y

# RCU 설정
CONFIG_PREEMPT_RCU=y     # 자동 선택됨
CONFIG_RCU_BOOST=y
CONFIG_RCU_BOOST_DELAY=500

# 디버깅 (개발 중에만)
CONFIG_DEBUG_RT_MUTEXES=y
CONFIG_DEBUG_PREEMPT=y
CONFIG_IRQSOFF_TRACER=y
CONFIG_PREEMPT_TRACER=y
CONFIG_SCHED_TRACER=y
CONFIG_FTRACE=y

# 프로덕션에서는 디버깅 비활성화
# CONFIG_DEBUG_RT_MUTEXES is not set
# CONFIG_DEBUG_PREEMPT is not set
# CONFIG_PROVE_LOCKING is not set
# CONFIG_LOCK_STAT is not set

빌드 및 설치

# 빌드
make -j$(nproc)
make modules_install
make install

# GRUB 업데이트
update-grub

# 부트 파라미터 추가 (/etc/default/grub)
GRUB_CMDLINE_LINUX="isolcpus=managed_irq,domain,2-3 \
    nohz_full=2-3 rcu_nocbs=2-3 irqaffinity=0,1 \
    nosoftlockup tsc=reliable"

# 재부팅 후 확인
uname -a
# Linux host 6.12.0-rt ... PREEMPT_RT ...

# RT 확인
cat /sys/kernel/realtime
# 1
핵심 CONFIG 옵션 요약
옵션설명RT 필수 여부
CONFIG_PREEMPT_RTPREEMPT_RT 활성화필수
CONFIG_HIGH_RES_TIMERS나노초 해상도 타이머필수
CONFIG_NO_HZ_FULL틱리스(Tickless) 커널 (격리 CPU용)권장
CONFIG_RCU_BOOSTRCU 읽기 구간 부스트권장
CONFIG_CPU_FREQ_DEFAULT_GOV_PERFORMANCECPU 주파수 고정 (성능 모드)권장
CONFIG_FTRACEftrace 트레이싱디버깅용
CONFIG_IRQSOFF_TRACERIRQ-off 지연 추적디버깅용

필수 CONFIG 옵션 상세

PREEMPT_RT 관련 CONFIG 옵션 종합
카테고리옵션기본값설명RT 필수 여부
선점CONFIG_PREEMPT_RTnPREEMPT_RT 활성화필수
선점CONFIG_PREEMPT_DYNAMICy (배포판)부트 타임 선점 선택비호환 (RT와 별도)
타이머CONFIG_HIGH_RES_TIMERSy고해상도 hrtimer필수
타이머CONFIG_NO_HZ_FULLn격리 CPU 틱 제거강력 권장
RCUCONFIG_PREEMPT_RCU자동선점 가능 RCU자동 (RT 시)
RCUCONFIG_RCU_BOOSTnRCU 읽기 구간 부스트강력 권장
RCUCONFIG_RCU_BOOST_DELAY500ms부스트 지연권장
전원CONFIG_CPU_FREQ_DEFAULT_GOV_PERFORMANCEnCPU 주파수 고정강력 권장
메모리CONFIG_TRANSPARENT_HUGEPAGEy투명 대형 페이지비활성화 권장 (compaction 지연)
디버그CONFIG_DEBUG_RT_MUTEXESnRT mutex 디버깅개발용
디버그CONFIG_DEBUG_PREEMPTn선점 카운터 검증개발용
디버그CONFIG_PROVE_LOCKINGnlockdep 활성화개발용
트레이싱CONFIG_IRQSOFF_TRACERnIRQ-off 지연 추적디버깅 권장
트레이싱CONFIG_PREEMPT_TRACERn선점-off 지연 추적디버깅 권장
트레이싱CONFIG_SCHED_TRACERnwakeup_rt 트레이서디버깅 권장
트레이싱CONFIG_OSNOISE_TRACERnOS 노이즈 트레이서상시 권장
트레이싱CONFIG_HWLAT_TRACERn하드웨어 지연 감지상시 권장

디버그 CONFIG 옵션

프로덕션 빌드 주의: 디버그 옵션은 상당한 성능 오버헤드를 유발합니다. 반드시 개발/테스트 빌드와 프로덕션 빌드를 분리하세요.
  • CONFIG_PROVE_LOCKING: ~30% 성능 저하 (lockdep)
  • CONFIG_DEBUG_RT_MUTEXES: ~10% (추가 검증 코드)
  • CONFIG_LOCK_STAT: ~15% (통계 수집)
  • CONFIG_DEBUG_PREEMPT: ~5% (preempt_count 검증)

배포판 RT 커널 (Ubuntu RT, RHEL RT, Debian)

# Ubuntu RT 커널 설치
sudo apt install linux-image-rt-amd64 linux-headers-rt-amd64
# 또는 lowlatency 커널 (RT가 아닌 PREEMPT_FULL)
sudo apt install linux-image-lowlatency

# RHEL/CentOS RT 커널
sudo dnf install kernel-rt kernel-rt-devel
# RT 커널로 부팅
sudo grubby --set-default /boot/vmlinuz-*rt*
# tuned 프로파일 적용
sudo tuned-adm profile realtime

# Fedora RT 커널
sudo dnf install kernel-rt

# Yocto Project RT 커널
# conf/local.conf에 추가:
PREFERRED_PROVIDER_virtual/kernel = "linux-yocto-rt"
# 또는 bitbake 커맨드
bitbake linux-yocto-rt

빌드 단계별 가이드

#!/bin/bash
# build_rt_kernel.sh - RT 커널 빌드 완전 가이드

# 1. 빌드 의존성 설치
sudo apt install build-essential bc kmod cpio flex \
    libncurses-dev libelf-dev libssl-dev bison

# 2. 커널 소스 다운로드 (6.12+ 권장)
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.tar.xz
tar xf linux-6.12.tar.xz
cd linux-6.12

# 3. 현재 커널 설정을 기반으로 시작
cp /boot/config-$(uname -r) .config

# 4. RT 설정 적용
scripts/config --enable PREEMPT_RT
scripts/config --enable HIGH_RES_TIMERS
scripts/config --enable NO_HZ_FULL
scripts/config --enable RCU_BOOST
scripts/config --set-val RCU_BOOST_DELAY 500
scripts/config --enable CPU_FREQ_DEFAULT_GOV_PERFORMANCE
scripts/config --enable IRQSOFF_TRACER
scripts/config --enable SCHED_TRACER
scripts/config --enable OSNOISE_TRACER
scripts/config --enable HWLAT_TRACER

# 프로덕션: 디버그 비활성화
scripts/config --disable DEBUG_RT_MUTEXES
scripts/config --disable DEBUG_PREEMPT
scripts/config --disable PROVE_LOCKING
scripts/config --disable LOCK_STAT

# THP 비활성화 (RT 환경에서 compaction 지연 방지)
scripts/config --disable TRANSPARENT_HUGEPAGE

# 5. 설정 완료
make olddefconfig

# 6. 빌드
make -j$(nproc) bindeb-pkg LOCALVERSION=-rt-custom
# deb 패키지 생성됨

# 7. 설치
cd ..
sudo dpkg -i linux-image-6.12.0-rt-custom_*.deb
sudo dpkg -i linux-headers-6.12.0-rt-custom_*.deb

# 8. 부트 파라미터 설정
sudo sed -i 's/GRUB_CMDLINE_LINUX=""/GRUB_CMDLINE_LINUX="isolcpus=managed_irq,domain,2-3 nohz_full=2-3 rcu_nocbs=2-3 irqaffinity=0,1 nosoftlockup tsc=reliable"/' /etc/default/grub
sudo update-grub

# 9. 재부팅 후 확인
sudo reboot
# ...
cat /sys/kernel/realtime     # 1
uname -r                     # 6.12.0-rt-custom

배포판별 RT 커널 패키지

주요 배포판의 RT 커널 지원
배포판패키지기반 커널설치 방법
Ubuntulinux-image-rt6.x-rtapt install linux-image-rt-amd64
RHEL/CentOSkernel-rt5.14-rt (RHEL 9)dnf install kernel-rt
Fedorakernel-rt6.x-rtdnf install kernel-rt
Debianlinux-image-rt-amd646.x-rtapt install linux-image-rt-amd64
openSUSEkernel-rt6.x-rtzypper install kernel-rt
Yoctolinux-yocto-rt커스텀레시피에 PREFERRED_PROVIDER_virtual/kernel = "linux-yocto-rt"

RT 커널 검증 스크립트

#!/bin/bash
# verify_rt_kernel.sh - RT 커널 설정 검증

echo "=== RT 커널 검증 ==="

# 1. RT 커널 확인
if [ -f /sys/kernel/realtime ]; then
    RT=$(cat /sys/kernel/realtime)
    if [ "$RT" = "1" ]; then
        echo "[OK] RT 커널 활성화됨"
    else
        echo "[FAIL] /sys/kernel/realtime = $RT"
    fi
else
    echo "[FAIL] /sys/kernel/realtime 파일 없음 (RT 커널 아님)"
fi

# 2. 커널 버전 확인
echo "[INFO] 커널: $(uname -r)"

# 3. 선점 모드 확인
if [ -f /sys/kernel/debug/sched/preempt ]; then
    echo "[INFO] 선점 모드: $(cat /sys/kernel/debug/sched/preempt)"
fi

# 4. CPU 격리 확인
ISOLATED=$(cat /sys/devices/system/cpu/isolated 2>/dev/null)
echo "[INFO] 격리 CPU: ${ISOLATED:-없음}"

# 5. nohz_full 확인
NOHZ=$(cat /sys/devices/system/cpu/nohz_full 2>/dev/null)
echo "[INFO] nohz_full CPU: ${NOHZ:-없음}"

# 6. RT throttling 확인
RT_RUNTIME=$(cat /proc/sys/kernel/sched_rt_runtime_us)
if [ "$RT_RUNTIME" = "-1" ]; then
    echo "[OK] RT throttling 비활성화됨"
else
    echo "[WARN] RT throttling 활성화: ${RT_RUNTIME}us / $(cat /proc/sys/kernel/sched_rt_period_us)us"
fi

# 7. CPU 주파수 governor 확인
for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
    if [ -f "$cpu" ]; then
        GOV=$(cat "$cpu")
        CPU_NUM=$(echo "$cpu" | grep -o 'cpu[0-9]*')
        if [ "$GOV" != "performance" ]; then
            echo "[WARN] $CPU_NUM governor: $GOV (performance 권장)"
        fi
    fi
done

# 8. IRQ 스레드 확인
IRQ_THREADS=$(ps -eo comm | grep -c '^irq/')
echo "[INFO] IRQ 스레드 수: $IRQ_THREADS"

echo "=== 검증 완료 ==="

실전 패턴: RT 애플리케이션 설계

RT 커널 위에서 실시간 보장을 받으려면 사용자 공간 애플리케이션도 특별한 설계 패턴을 따라야 합니다. 가장 중요한 원칙은 “초기화 시 모든 자원을 할당하고, 실시간 루프에서는 할당/해제하지 않습니다”입니다.

RT 애플리케이션 라이프사이클 Phase 1: 초기화 mlockall(MCL_CURRENT | MCL_FUTURE) 메모리 풀 사전 할당 스택 prefault sched_setattr(FIFO) Phase 2: RT 루프 clock_nanosleep() 센서 읽기 / 계산 액추에이터 출력 malloc/free 금지! printf/로깅 금지! Phase 3: 정리 RT 스케줄링 해제 자원 해제 로그 플러시 RT 루프에서 금지되는 작업 malloc() / free() / new / delete 파일 I/O (open, read, write, close) printf() / std::cout / syslog() 네트워크 소켓 (TCP/IP) 시스템 콜 (페이지 폴트 유발 가능) 뮤텍스 대기 (예측 불가 지연) 동적 라이브러리 로딩 스레드 생성/종료

mlockall 상세와 주의점

/* mlockall() 상세 동작 */
#include <sys/mman.h>

/*
 * MCL_CURRENT: 현재 매핑된 모든 페이지를 물리 메모리에 고정
 * MCL_FUTURE:  향후 매핑될 페이지도 즉시 고정
 * MCL_ONFAULT: 페이지 폴트 시에만 고정 (Linux 4.4+)
 *
 * 주의사항:
 * 1. MCL_FUTURE는 모든 malloc()이 즉시 물리 페이지를 소비
 *    → 메모리 사용량 예측이 어려움
 * 2. 스택도 고정됨 → 스택 크기(ulimit -s) 만큼 즉시 물리 메모리 소비
 * 3. 라이브러리 매핑도 고정 → glibc, libm 등의 페이지도
 * 4. munlockall()로 해제 가능하지만, RT 루프 중에는 호출 금지
 */

/* 올바른 순서 */
int setup_memory(void)
{
    /* 1. 먼저 필요한 메모리를 모두 할당 */
    data_pool = malloc(POOL_SIZE);
    work_buffer = malloc(BUFFER_SIZE);

    /* 2. 모든 페이지를 touch하여 물리 메모리에 매핑 */
    memset(data_pool, 0, POOL_SIZE);
    memset(work_buffer, 0, BUFFER_SIZE);

    /* 3. 그 후 mlockall() 호출 */
    if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
        perror("mlockall");
        /* ENOMEM: 물리 메모리 부족
         * EPERM: CAP_IPC_LOCK 권한 없음
         * → /etc/security/limits.conf에서 memlock 설정 */
        return -1;
    }

    /* 4. 스택 prefault (아래 참조) */
    prefault_stack();

    return 0;
}

/* mlockall 없이 mlock으로 선택적 고정 */
void selective_lock(void)
{
    void *rt_data = malloc(RT_DATA_SIZE);
    memset(rt_data, 0, RT_DATA_SIZE);

    /* RT에서 사용하는 메모리만 선택적으로 고정 */
    mlock(rt_data, RT_DATA_SIZE);

    /* 나머지 메모리는 swap 가능하게 유지 */
}

스택 pre-fault 기법

/* 스택 prefault: 스택 페이지를 미리 물리 메모리에 매핑 */

/* 방법 1: 큰 지역 변수로 스택 touch */
static void prefault_stack(void)
{
    unsigned char dummy[MAX_STACK_SIZE];
    memset(dummy, 0, sizeof(dummy));
    /* 컴파일러 최적화 방지 */
    asm volatile("" : : "r"(dummy) : "memory");
}

/* 방법 2: 스레드 생성 시 스택 크기 명시 */
void create_rt_thread(void)
{
    pthread_attr_t attr;
    pthread_t thread;
    size_t stack_size = 8 * 1024 * 1024;  /* 8MB */

    pthread_attr_init(&attr);

    /* 스택 크기 설정 */
    pthread_attr_setstacksize(&attr, stack_size);

    /* 스케줄링 속성 */
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
    struct sched_param param = { .sched_priority = 80 };
    pthread_attr_setschedparam(&attr, &param);
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

    pthread_create(&thread, &attr, rt_thread_func, NULL);
    pthread_attr_destroy(&attr);
}

/* 방법 3: mmap으로 스택 직접 할당 (고급) */
void *create_prefaulted_stack(size_t size)
{
    void *stack = mmap(NULL, size,
                        PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK,
                        -1, 0);
    if (stack == MAP_FAILED) return NULL;

    /* 모든 페이지 touch */
    memset(stack, 0, size);

    /* mlock으로 고정 */
    mlock(stack, size);

    return stack;
}

RT용 메모리 풀 설계

RT 애플리케이션 메모리 관리 전략 초기화 단계 (비실시간) 1. malloc() → 풀 메모리 할당 2. memset() → 페이지 touch 3. mlockall() → 물리 메모리 고정 4. prefault_stack() → 스택 매핑 5. sched_setscheduler() → RT 전환 시스콜 허용, 지연 무관 실시간 루프 pool_alloc() → O(1) pool_free() → O(1) ring_push() → lock-free malloc()/free() 금지! mmap()/munmap() 금지! brk() 시스콜 유발 금지! 정리 단계 RT 정책 해제 pool_destroy() munlockall() free() 메모리 풀 구조 free free used free used ... pool_alloc(): free_list에서 마지막 포인터 pop → O(1), 시스콜 없음 pool_free(): free_list에 포인터 push → O(1), 시스콜 없음 고갈 대책: pool 크기를 최악 케이스 + 마진(2x)으로 사전 할당

Lock-free IPC 패턴

RT IPC 선택 가이드:
  • RT→비RT 단방향: SPSC 링 버퍼 (가장 간단, 가장 빠름)
  • 양방향 제어: 두 개의 SPSC 링 (요청/응답)
  • 상태 공유: 원자적(Atomic) 변수 + seqlock 패턴
  • 다중 producer: MPSC 링 버퍼 (CAS 기반)
  • 반드시 피해야 할 것: pipe, socket, pthread_cond, SysV IPC (모두 시스콜 유발)

RT 환경 로깅 전략 (non-blocking)

/* RT 루프에서의 non-blocking 로깅 */

/* 방법 1: SPSC 링 버퍼 + 별도 로깅 스레드 */
struct log_entry {
    uint64_t timestamp;
    uint32_t event_id;
    int32_t  value;
};

/* RT 태스크에서 (비블로킹) */
void rt_log(uint32_t event_id, int32_t value)
{
    struct log_entry entry = {
        .timestamp = get_monotonic_ns(),
        .event_id = event_id,
        .value = value,
    };

    /* lock-free 링 버퍼에 push (실패해도 무시) */
    ring_push(&log_ring, &entry);
    /* → printf/write/syslog 절대 사용 금지! */
}

/* 비RT 로깅 스레드에서 (블로킹 OK) */
void *logger_thread(void *arg)
{
    struct log_entry entry;
    FILE *fp = fopen("rt_log.csv", "w");

    while (running) {
        if (ring_pop(&log_ring, &entry) == 0) {
            fprintf(fp, "%lu,%u,%d\n",
                    entry.timestamp, entry.event_id, entry.value);
        } else {
            usleep(1000);  /* 비RT이므로 sleep OK */
        }
    }
    fclose(fp);
    return NULL;
}

/* 방법 2: 공유 메모리 + trace_marker */
void rt_trace_log(const char *msg)
{
    static int trace_fd = -1;
    if (trace_fd == -1)
        trace_fd = open("/sys/kernel/debug/tracing/trace_marker", O_WRONLY);
    if (trace_fd >= 0)
        write(trace_fd, msg, strlen(msg));
    /* write()는 커널에서 즉시 처리되어 비교적 빠름
     * 다만 프로덕션에서는 SPSC 방식이 더 안전 */
}

메모리 관리(Memory Management) 주의사항

RT 루프에서의 메모리:
  • malloc()/free()는 내부적으로 brk()/mmap() 시스콜을 호출하여 페이지 폴트(Page Fault)를 유발할 수 있습니다.
  • 페이지 폴트 처리는 수십~수백 us의 비결정론적 지연을 야기합니다.
  • mlockall(MCL_FUTURE)미래에 할당될 페이지도 즉시 물리 메모리(Physical Memory)에 고정합니다. 하지만 할당 자체의 지연(glibc allocator 내부 락)은 방지하지 못합니다.
  • RT 루프에서 사용할 모든 메모리는 초기화 단계에서 사전 할당하고 touch(memset)해야 합니다.
/* 메모리 풀 패턴: 초기화 시 풀 생성, RT 루프에서 풀에서 할당 */
struct mem_pool {
    void **free_list;
    size_t elem_size;
    size_t capacity;
    size_t count;
};

/* 초기화 단계에서 호출 (비실시간) */
struct mem_pool *pool_create(size_t elem_size, size_t count)
{
    struct mem_pool *pool = malloc(sizeof(*pool));
    pool->free_list = malloc(count * sizeof(void *));
    pool->elem_size = elem_size;
    pool->capacity = count;
    pool->count = count;

    for (size_t i = 0; i < count; i++) {
        pool->free_list[i] = malloc(elem_size);
        memset(pool->free_list[i], 0, elem_size);  /* 페이지 touch */
    }
    return pool;
}

/* RT 루프에서 호출: O(1) 할당, 시스콜 없음 */
void *pool_alloc(struct mem_pool *pool)
{
    if (pool->count == 0) return NULL;
    return pool->free_list[--pool->count];
}

/* RT 루프에서 호출: O(1) 해제, 시스콜 없음 */
void pool_free(struct mem_pool *pool, void *ptr)
{
    if (pool->count >= pool->capacity) return;
    pool->free_list[pool->count++] = ptr;
}

완전한 RT 애플리케이션 템플릿

/* rt_app_template.c - 최소 RT 애플리케이션 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sched.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>

#define STACK_PREFAULT_SIZE  (8 * 1024 * 1024)  /* 8MB */
#define RT_PRIORITY          80
#define PERIOD_NS            1000000  /* 1ms */

static volatile int running = 1;

/* 1단계: 스택 prefault */
static void prefault_stack(void)
{
    unsigned char dummy[STACK_PREFAULT_SIZE];
    memset(dummy, 0, sizeof(dummy));
    /* 페이지 폴트를 미리 발생시켜 페이지 테이블 구축 */
}

/* 2단계: RT 스케줄링 설정 */
static int setup_rt_scheduling(int priority, int cpu)
{
    struct sched_param param = { .sched_priority = priority };
    cpu_set_t cpuset;

    /* SCHED_FIFO 설정 */
    if (sched_setscheduler(0, SCHED_FIFO, &param) == -1) {
        perror("sched_setscheduler");
        return -1;
    }

    /* CPU affinity 설정 */
    CPU_ZERO(&cpuset);
    CPU_SET(cpu, &cpuset);
    if (sched_setaffinity(0, sizeof(cpuset), &cpuset) == -1) {
        perror("sched_setaffinity");
        return -1;
    }

    return 0;
}

int main(void)
{
    struct timespec next;
    long long overruns = 0;

    /* ======= Phase 1: 초기화 (비실시간) ======= */

    /* 모든 현재+미래 메모리 페이지 락 */
    if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
        perror("mlockall");
        return 1;
    }

    /* 스택 prefault */
    prefault_stack();

    /* 메모리 풀 사전 할당 (예시) */
    void *data_pool = malloc(1024 * 1024);  /* 1MB 풀 */
    if (!data_pool) {
        perror("malloc");
        return 1;
    }
    memset(data_pool, 0, 1024 * 1024);  /* touch all pages */

    /* RT 스케줄링 설정 (격리된 CPU 2에서 FIFO prio 80) */
    if (setup_rt_scheduling(RT_PRIORITY, 2) == -1)
        return 1;

    printf("RT app started: SCHED_FIFO prio %d, CPU 2\n", RT_PRIORITY);

    /* ======= Phase 2: 실시간 루프 ======= */
    clock_gettime(CLOCK_MONOTONIC, &next);

    while (running) {
        /* 다음 주기 계산 */
        next.tv_nsec += PERIOD_NS;
        if (next.tv_nsec >= 1000000000L) {
            next.tv_nsec -= 1000000000L;
            next.tv_sec++;
        }

        /* 절대 시간까지 대기 */
        if (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME,
                            &next, NULL) != 0) {
            if (errno == EINTR) continue;
            break;
        }

        /* === 실시간 작업 (여기서만 수행) === */
        /* 센서 읽기, 제어 알고리즘 계산, 액추에이터 출력 */
        do_realtime_work(data_pool);
    }

    /* ======= Phase 3: 정리 ======= */
    free(data_pool);
    printf("RT app finished. Overruns: %lld\n", overruns);
    return 0;
}

RT 애플리케이션 디버깅 체크리스트

RT 지연 원인 진단 체크리스트
증상가능한 원인진단 도구해결 방법
주기적 지터 (1ms 간격)타이머 틱perf stat -e irq:*nohz_full= 부트 파라미터
불규칙 스파이크 (100-500us)SMI (System Management Interrupt)hwlat_detectorBIOS에서 SMI 비활성화
페이지 폴트 스파이크mlockall() 누락 또는 동적 할당perf stat -e page-faultsmlockall(MCL_CURRENT|MCL_FUTURE)
커널 스핀락 경합다른 코어에서 공유 락 보유ftrace + lock:*per-CPU 데이터 사용, 락 분리
IRQ 간섭격리 CPU에 IRQ 도착/proc/interruptsirqaffinity= + managed_irq
RCU 콜백 간섭격리 CPU에서 RCU cb 실행rcu/*/rcudatarcu_nocbs=
wakeup 지연 > 50us선점 지연wakeup_rt 트레이서우선순위 검토, CPU isolation
RT throttlingsched_rt_runtime_us 초과/proc/sched_debugsched_rt_runtime_us=-1

Lock-free IPC 패턴

RT 태스크와 비RT 태스크 사이의 통신에는 lock-free 자료구조를 사용하는 것이 이상적입니다. 잠금 없이 데이터를 전달하면 우선순위 역전이 원천적으로 발생하지 않습니다.

/* SPSC(Single-Producer, Single-Consumer) 링 버퍼 예시 */
#include <stdatomic.h>

#define RING_SIZE 1024  /* 2의 거듭제곱 */
#define RING_MASK (RING_SIZE - 1)

struct spsc_ring {
    _Alignas(64) atomic_uint head;  /* producer가 쓰기 */
    _Alignas(64) atomic_uint tail;  /* consumer가 쓰기 */
    void *buffer[RING_SIZE];
};

/* RT 태스크 (producer): 센서 데이터 전달 */
int ring_push(struct spsc_ring *ring, void *item)
{
    unsigned int head = atomic_load_explicit(&ring->head,
                                             memory_order_relaxed);
    unsigned int tail = atomic_load_explicit(&ring->tail,
                                             memory_order_acquire);
    if (head - tail >= RING_SIZE)
        return -1;  /* 가득 참 */

    ring->buffer[head & RING_MASK] = item;
    atomic_store_explicit(&ring->head, head + 1,
                          memory_order_release);
    return 0;
}

/* 비RT 태스크 (consumer): 데이터 처리/로깅 */
void *ring_pop(struct spsc_ring *ring)
{
    unsigned int tail = atomic_load_explicit(&ring->tail,
                                             memory_order_relaxed);
    unsigned int head = atomic_load_explicit(&ring->head,
                                             memory_order_acquire);
    if (tail == head)
        return NULL;  /* 비어 있음 */

    void *item = ring->buffer[tail & RING_MASK];
    atomic_store_explicit(&ring->tail, tail + 1,
                          memory_order_release);
    return item;
}
RT 태스크 통신 아키텍처 격리된 CPU (isolcpus) RT 제어 태스크 SCHED_FIFO 90 RT 센서 태스크 SCHED_FIFO 80 SPSC Lock-free Ring 공유 메모리 (mmap) Housekeeping CPU 로깅 태스크 SCHED_OTHER 네트워크 I/O SCHED_OTHER SPSC Ring (consumer) Lock-free 전달 공유 메모리 (설정/상태)

성능 비교

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
웹 서버
(처리량 중심)
우수
높은 처리량
낮은 처리량
~5-10% 감소
일반 커널 권장
데이터베이스 우수
높은 처리량
낮은 처리량
트랜잭션(Transaction) 속도 감소
일반 커널 권장

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, 빌드) → 일반 커널 권장

트러블슈팅

일반적인 문제와 해결

문제 원인 해결 방법
레이턴시 스파이크
(100+ μs)
SMI (System Management Interrupt)
BIOS 레벨 인터럽트
BIOS에서 SMI 비활성화
hwlatdetect로 SMI 감지
최신 펌웨어(Firmware) 업데이트
예상보다 높은 레이턴시 CPU 전력 관리 (C-states)
주파수 스케일링(Frequency Scaling)
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 적용

진단 체크리스트

# 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

산업용 사례

로봇 ROS2 + RT 상세

# ROS 2 RT 환경 설정 (Ubuntu 22.04 + Humble)
# 1. RT 커널 설치
sudo apt install linux-image-rt-amd64

# 2. ROS 2 설치
sudo apt install ros-humble-desktop

# 3. ROS 2 RT 관련 패키지
sudo apt install ros-humble-realtime-support

# 4. RT 실행 환경 설정
# /etc/security/limits.conf에 추가
# @realtime    -    rtprio    99
# @realtime    -    memlock   unlimited

# 5. RT 컨트롤러 실행
ros2 launch my_robot rt_controller.launch.py \
    --ros-args -p use_sim_time:=false

# 6. ROS 2 DDS RT 설정 (cyclonedds.xml)
# <CycloneDDS>
#   <Domain>
#     <Internal>
#       <SocketReceiveBufferSize>1MB</SocketReceiveBufferSize>
#     </Internal>
#     <General>
#       <NetworkInterfaceAddress>eth0</NetworkInterfaceAddress>
#     </General>
#   </Domain>
# </CycloneDDS>

로봇 제어 (ROS 2 + PREEMPT_RT)

ROS 2(Robot Operating System 2)는 DDS(Data Distribution Service) 미들웨어 위에 구축되며, 실시간 제어 루프에 PREEMPT_RT 커널을 권장합니다. 일반적인 로봇 제어 주기는 1ms(서보 모터)에서 10ms(모바일 로봇)입니다.

로봇 제어 시스템의 RT 요구사항
제어 대상제어 주기허용 지터비고
서보 모터 (산업용 로봇팔)250us ~ 1ms< 50usEtherCAT 기반, SCHED_DEADLINE 사용
모바일 로봇 네비게이션10ms ~ 50ms< 1msLiDAR/IMU 센서 퓨전
드론 비행 제어1ms ~ 4ms< 100usPX4/ArduPilot 기반
CNC 공작기계1ms< 20usLinuxCNC + PREEMPT_RT

프로 오디오 (JACK / PipeWire)

프로 오디오 처리에서는 48kHz 샘플링 시 약 21us(1/48000)마다 샘플을 처리해야 합니다. 버퍼 크기에 따라 실질적 주기가 결정됩니다.

# JACK 오디오 서버 RT 설정
jackd -R -d alsa -p 64 -n 2 -r 48000
# -R: 실시간 모드
# -p 64: 프레임 버퍼 크기 (64/48000 ≈ 1.33ms 지연)
# → 2 버퍼 = 약 2.67ms 왕복 지연

# PipeWire RT 설정 (/etc/pipewire/pipewire.conf)
# context.properties = {
#     default.clock.rate      = 48000
#     default.clock.quantum   = 64
#     default.clock.min-quantum = 32
# }

자동차 (AUTOSAR Adaptive Platform)

AUTOSAR Adaptive Platform은 리눅스 기반의 자동차 소프트웨어 플랫폼입니다. ADAS(Advanced Driver Assistance Systems)와 자율 주행 소프트웨어는 PREEMPT_RT 커널 위에서 실행되며, 안전 무결성(Integrity) 수준(ASIL)에 따라 지연 요구사항이 다릅니다.

자동차 RT 요구사항 (ASIL 등급별)
ASIL 등급최대 허용 지연해당 기능RT 설정
ASIL-D< 10us긴급 제동(AEB), 조향 보조전용 마이크로컨트롤러 (리눅스 아님)
ASIL-B< 1ms적응형 순항 제어, 차선 유지PREEMPT_RT + CPU isolation
QM< 100ms인포테인먼트, OTA 업데이트일반 커널 충분
산업 추세: Tesla, Continental, Bosch 등 주요 자동차 Tier-1 업체들이 PREEMPT_RT 커널을 자율 주행 플랫폼에 채택하고 있습니다. Linux 6.12의 메인라인 병합은 자동차 산업에서의 RT 리눅스 채택을 더욱 가속할 것으로 예상됩니다. Linux Foundation의 ELISA(Enabling Linux In Safety Applications) 프로젝트는 안전 인증(ISO 26262)을 위한 리눅스 커널 문서화 및 테스트를 진행하고 있습니다.

통신 인프라 (5G RAN, O-RAN)

5G 기지국의 RAN(Radio Access Network) 처리에서는 1ms 이하의 지연이 요구됩니다. O-RAN Alliance의 개방형 아키텍처에서 PREEMPT_RT 리눅스가 DU(Distributed Unit) 플랫폼으로 채택되고 있습니다.

5G RAN RT 요구사항
구성 요소지연 요구사항주기RT 설정
L1 PHY (LDPC/FFT)< 100usTTI (0.5ms)DPDK + CPU isolation + hugepage
L2 MAC 스케줄러< 500us1msPREEMPT_RT + SCHED_FIFO
Fronthaul (eCPRI)< 100us125us (7.2x split)PTP + CPU isolation
CU-UP (PDCP/SDAP)< 1ms가변일반 커널 + DPDK 가능

의료 기기

의료 로봇(수술 로봇, 재활 로봇)과 환자 모니터링 시스템에서 PREEMPT_RT가 사용됩니다. IEC 62304(의료 기기 소프트웨어) 인증에서는 소프트웨어의 결정론적 동작을 요구하며, PREEMPT_RT의 메인라인 병합은 인증 과정을 크게 단순화합니다.

프로 오디오/JACK 상세

# 프로 오디오 RT 설정 체크리스트

# 1. RT 커널 확인
cat /sys/kernel/realtime  # 1

# 2. 오디오 그룹 추가 (rtprio/memlock 권한)
sudo usermod -a -G audio $USER

# 3. /etc/security/limits.d/audio.conf
# @audio  -  rtprio    95
# @audio  -  memlock   unlimited
# @audio  -  nice      -19

# 4. JACK RT 모드 실행
jackd -R -d alsa -p 32 -n 2 -r 48000
# -R: 실시간 모드 (SCHED_FIFO)
# -p 32: 32 프레임 (32/48000 = 0.67ms 버퍼)
# 왕복 지연: 0.67ms × 2 = 1.33ms

# 5. PipeWire RT 설정
# /etc/pipewire/pipewire.conf.d/99-lowlatency.conf
# context.properties = {
#     default.clock.rate          = 48000
#     default.clock.quantum       = 32
#     default.clock.min-quantum   = 16
#     default.clock.max-quantum   = 256
# }

# 6. IRQ 우선순위 조정 (오디오 인터페이스 IRQ)
AUDIO_IRQ=$(cat /proc/interrupts | grep snd | awk '{print $1}' | tr -d ':')
AUDIO_PID=$(pgrep -f "irq/${AUDIO_IRQ}")
chrt -f -p 90 $AUDIO_PID

자동차 AUTOSAR Adaptive + RT

/* AUTOSAR Adaptive Platform + PREEMPT_RT 구조 예시 */

/* AUTOSAR Adaptive의 Execution Management가
 * RT 태스크의 스케줄링을 관리 */

/* ara::exec 기반 RT 컨트롤러 */
class AdaptiveCruiseController : public ara::exec::Application
{
public:
    void Initialize() override
    {
        /* 초기화 단계: 메모리 할당, 센서 초기화 */
        mlockall(MCL_CURRENT | MCL_FUTURE);

        /* SCHED_DEADLINE 설정 */
        struct sched_attr attr = {
            .size = sizeof(attr),
            .sched_policy = SCHED_DEADLINE,
            .sched_runtime  = 1000000,   /* 1ms */
            .sched_deadline = 5000000,   /* 5ms */
            .sched_period   = 10000000,  /* 10ms */
        };
        syscall(SYS_sched_setattr, 0, &attr, 0);
    }

    void Run() override
    {
        while (IsRunning()) {
            /* RT 루프 */
            auto sensor_data = ReadRadar();
            auto control = ComputeControl(sensor_data);
            SendToActuator(control);

            sched_yield();  /* DEADLINE: 예산 소진 시 양보 */
        }
    }
};

5G/통신 RT 활용

O-RAN DU 소프트웨어 스택: O-RAN Alliance의 DU(Distributed Unit)는 PREEMPT_RT 커널 위에서 L2 MAC 스케줄러를 실행합니다. FlexRAN(Intel), PHY 가속기(FPGA/GPU)와 연동하여 5G NR TTI(0.5ms) 타이밍을 맞춥니다.
  • L1 PHY: DPDK + 전용 CPU (격리, 폴링(Polling) 모드)
  • L2 MAC: PREEMPT_RT + SCHED_FIFO 90
  • Fronthaul: PTP 동기화 + eCPRI
  • O1/O2 관리: 별도 비RT CPU에서 실행

의료기기 RT

의료 로봇(da Vinci 수술 로봇 등)과 환자 모니터링 시스템에서 PREEMPT_RT가 활용됩니다. IEC 62304(의료 기기 소프트웨어 수명 주기) 인증에서는 소프트웨어의 결정론적 동작을 요구합니다.

의료기기 RT 요구사항
기기 유형안전 등급최대 지연주기인증
수술 로봇 (마스터-슬레이브)Class III< 1ms1kHzFDA 510(k), IEC 62304
재활 로봇 (외골격)Class II< 5ms200HzIEC 62304, ISO 13482
환자 모니터 (ECG/SpO2)Class II< 100ms가변IEC 62304
인공호흡기 제어Class III< 10ms100HzFDA, IEC 60601

산업용 PLC/EtherCAT

# LinuxCNC + PREEMPT_RT (CNC 기계 제어)
# 1. RT 커널에서 LinuxCNC 실행
linuxcnc my_machine.ini

# 2. LinuxCNC RT 지연 테스트
latency-test
# Base thread: 25us (기본, 스텝 펄스 생성)
# Servo thread: 1ms (서보 루프)
# Max jitter가 25us 이하여야 안정적 동작

# EtherCAT 마스터 (IgH EtherCAT Master)
# PREEMPT_RT 커널에서 EtherCAT DC(Distributed Clock) 동기화
# 주기: 1ms (표준) ~ 250us (고성능)

# EtherCAT 마스터 RT 태스크 설정
chrt -f 90 ethercatctl start

# EtherCAT 지터 확인
ethercat master
# Jitter: 주기 대비 ±5us 이내 목표

PREEMPT_RT vs 듀얼-커널 비교

PREEMPT_RT vs Xenomai/RTAI 비교
항목PREEMPT_RTXenomai (듀얼-커널)RTAI
아키텍처단일 커널듀얼 커널 (Cobalt + Linux)듀얼 커널 (RTAI HAL + Linux)
최악-케이스 지연10-50us (튜닝 시)1-10us1-5us
리눅스 API 사용모든 POSIX API제한적 (Alchemy/POSIX skin)전용 API만
메인라인 여부6.12에서 완전 메인라인외부 패치외부 패치 (유지보수 중단)
커널 업그레이드자동 (메인라인)패치 포팅 필요패치 포팅 필요
드라이버 호환성모든 리눅스 드라이버RTDM 드라이버 별도 필요전용 드라이버
학습 곡선낮음 (표준 리눅스)중간 (Xenomai API 학습)높음
프로덕션 지원Linux Foundation 지원커뮤니티사실상 중단
선택 가이드:
  • 최악-케이스 < 50us면 → PREEMPT_RT (대부분의 산업 용도에 충분)
  • 최악-케이스 < 10us이 반드시 필요하면 → Xenomai 고려 (CNC, EtherCAT 마스터)
  • 최악-케이스 < 1us면 → 전용 RTOS 또는 FPGA 필요
관련 문서: PREEMPT_RT의 이해를 심화하려면 다음 문서들을 함께 참고하세요.
외부 자료:
  • 커널 문서: Documentation/locking/rt-mutex-design.rst
  • 커널 문서: Documentation/locking/rt-mutex.rst
  • 커널 문서: Documentation/scheduler/sched-rt-group.rst
  • 커널 문서: Documentation/trace/ftrace.rst
  • 커널 문서: Documentation/locking/locktypes.rst — PREEMPT_RT에서 변경되는 락 타입별 동작을 설명합니다.
  • 커널 문서: Documentation/locking/seqlock.rst — RT 환경에서의 seqlock 동작 차이를 다룹니다.
  • 커널 문서: Documentation/scheduler/sched-deadline.rst — SCHED_DEADLINE 정책의 CBS/EDF 알고리즘을 설명합니다.
  • 커널 문서: Documentation/timers/hrtimers.rst
  • 커널 문서: Documentation/timers/no_hz.rst — nohz_full 및 adaptive-ticks 모드의 설정과 동작을 설명합니다.
  • 커널 소스: kernel/sched/deadline.c
  • 커널 소스: kernel/locking/rtmutex.c
  • 커널 소스: kernel/locking/spinlock_rt.c — PREEMPT_RT에서 spinlock이 sleeping lock으로 변환되는 구현입니다.
  • OSADL (Open Source Automation Development Lab): Realtime Linux 프로젝트
  • OSADL 지연 시간 측정: QA Farm 지연 시간 차트 — 수백 대의 시스템에서 장기간 수집한 RT 지연 통계를 제공합니다.
  • rt-tests 도구: git://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git
  • PREEMPT_RT Patches: cdn.kernel.org/pub/linux/kernel/projects/rt/
  • Linux Foundation Real-Time Wiki: wiki.linuxfoundation.org/realtime — RT 프로젝트의 공식 위키입니다.
  • RT Wiki — HOWTO: rt-tests 사용법 — cyclictest, hackbench 등 벤치마크 도구 사용법을 안내합니다.
  • RT Wiki — 시스템 튜닝: CPU 파티셔닝 가이드 — isolcpus, irqaffinity 등 RT 시스템 튜닝 방법을 설명합니다.
  • RT Application Best Practices: Application Base HowTo — RT 애플리케이션 개발 시 메모리 잠금, 스택 사전 할당 등 필수 패턴을 다룹니다.
  • LWN.net: “A realtime preemption overview” (2005) — Ingo Molnar의 PREEMPT_RT 패치 초기 설계를 소개하는 기사입니다.
  • LWN.net: “The ongoing realtime effort” (2017) — RT 패치 메인라인 병합 과정의 기술적 과제를 분석합니다.
  • LWN.net: “RT and dependencies” (2017) — printk, memory allocator 등 RT 병합의 핵심 의존성을 설명합니다.
  • LWN.net: “Realtime mainlining status” (2021) — printk 스레드화, 강제 인터럽트 스레드화 등 메인라인 진행 상황을 보고합니다.
  • LWN.net: “A realtime kernel at last” (2024) — Linux 6.12에서 PREEMPT_RT가 메인라인에 최종 병합된 소식을 전합니다.
  • LWN.net: “SCHED_DEADLINE” — EDF 기반 데드라인 스케줄링의 설계와 사용법을 다룹니다.
  • LWN.net: “Priority inheritance in the kernel” — PI mutex의 커널 구현과 동작 원리를 설명합니다.
  • kernel.org 공식 문서: RT mutex design — RT mutex의 우선순위 상속 체인 구현을 상세히 설명합니다.
  • kernel.org 공식 문서: RT group scheduling — RT 대역폭 제한(rt_runtime_us/rt_period_us) 설정을 안내합니다.
  • ELISA (Enabling Linux In Safety Applications): elisa.tech — 안전 필수 시스템에서 Linux 사용을 위한 표준화 프로젝트입니다.
  • LinuxCNC: linuxcnc.org — PREEMPT_RT 기반 CNC 제어 시스템의 대표적인 실제 적용 사례입니다.
  • Thomas Gleixner “Latency Debugging”: ftrace 공식 문서 — function_graph, irqsoff, preemptoff 트레이서를 활용한 지연 분석 방법을 설명합니다.

주요 용어 정리

PREEMPT_RT 관련 용어집
용어설명
WCETWorst-Case Execution Time. 최악-케이스 실행 시간. 실시간 시스템 설계의 핵심 지표.
Jitter주기적 태스크의 실행 시간 편차. 이상적으로 0에 가까워야 함.
Deterministic결정론적. 동일 입력에 대해 항상 동일한 시간 내에 완료됨을 보장.
PI (Priority Inheritance)우선순위 상속. 락 소유자의 우선순위를 대기자 수준으로 임시 상승.
Priority Inversion우선순위 역전. 고우선순위 태스크가 저우선순위 태스크에 의해 간접적으로 블록됨.
Grace PeriodRCU에서 모든 기존 읽기 구간이 완료되는 시점. RT에서는 선점 가능 RCU로 추적.
CBSConstant Bandwidth Server. SCHED_DEADLINE에서 대역폭을 보장하는 알고리즘.
EDFEarliest Deadline First. 가장 이른 deadline을 가진 태스크를 먼저 실행하는 알고리즘.
nohz_fullAdaptive-ticks 모드. 유일한 실행 태스크만 있으면 타이머 틱을 중단.
SMISystem Management Interrupt. BIOS가 발생시키는 인터럽트로 커널이 감지 불가.
OSADLOpen Source Automation Development Lab. RT 리눅스 지연 측정 데이터를 장기 수집하는 프로젝트.