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 애플리케이션 설계 패턴, 산업용 사례까지 종합적으로 다룹니다.
핵심 요약
- sleeping spinlock —
spinlock_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)을 보장합니다.
| 일반 커널 구성 요소 | RT 커널에서의 변환 | 이유 |
|---|---|---|
spinlock_t | rt_mutex 기반 sleeping lock | 선점 불가 구간 제거 |
rwlock_t | rt_mutex 기반 sleeping lock | writer starvation 방지 + 선점 |
local_bh_disable() | local_lock + migrate_disable | softirq 직렬화(Serialization)를 per-CPU lock으로 |
| hardirq handler | threaded IRQ (커널 스레드) | IRQ 컨텍스트에서 선점 가능 |
rcu_read_lock() | 선점 가능 RCU (RCU_PREEMPT) | RCU 읽기 구간 중 선점 허용 |
| softirq | 커널 스레드(ksoftirqd)에서 실행 | softirq 지연 제거 |
단계별 이해
- 선점 모델 이해
PREEMPT_NONE → PREEMPT_VOLUNTARY → PREEMPT (FULL) → PREEMPT_RT 순서로 선점성이 강화됩니다. 각 모델의 차이를 먼저 파악하세요. - Sleeping Spinlock 개념
RT 커널에서spin_lock()이 호출되면 실제로는rt_mutex_lock()이 수행됩니다. 이로 인해 락을 잡지 못하면 태스크가 sleep 상태로 전환되어 다른 태스크가 실행될 수 있습니다. - 우선순위 상속 체인
태스크 A(높은 우선순위)가 태스크 B(낮은 우선순위)가 보유한 락을 기다리면, B의 우선순위가 A 수준으로 임시 상승합니다. 이 체인은 여러 단계로 전파될 수 있습니다. - Threaded IRQ
하드웨어 인터럽트가 발생하면 최소한의 top-half만 하드 IRQ에서 실행하고, 나머지는 커널 스레드(bottom-half)에서 처리합니다. 이 스레드(Thread)에는 RT 우선순위를 부여할 수 있습니다. - 측정과 튜닝
cyclictest,osnoise,hwlat_detector로 최악-케이스 지연을 측정하고, CPU isolation, IRQ affinity, RT throttling 파라미터를 조정합니다. - 애플리케이션 설계
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)을 그대로 실시간 태스크에서 사용할 수 있어, 듀얼-커널 대비 개발 생산성이 크게 향상됩니다.
메인라인 병합 타임라인
| 연도 | 커널 버전 | 병합된 구성 요소 | 주요 기여자 |
|---|---|---|---|
| 2004 | 2.6.x (패치(Patch)) | PREEMPT_RT 패치셋 최초 공개 | Ingo Molnar |
| 2005 | 2.6.13 | Generic IRQ layer (genirq) | Thomas Gleixner, Ingo Molnar |
| 2006 | 2.6.18 | High-resolution timers (hrtimer) | Thomas Gleixner |
| 2006 | 2.6.18 | PREEMPT_RCU | Paul McKenney |
| 2009 | 2.6.30 | Threaded IRQ handler (request_threaded_irq) | Thomas Gleixner |
| 2009 | 2.6.31 | RT mutex 기반 futex PI | Thomas Gleixner |
| 2013 | 3.10 | SCHED_DEADLINE | Juri Lelli, Luca Abeni |
| 2020 | 5.8 | local_lock API | Thomas Gleixner, Sebastian Siewior |
| 2021 | 5.15 | Printk 스레드화 (부분) | John Ogness |
| 2022 | 6.1 | PREEMPT_DYNAMIC (부트 타임 선점 모델 선택) | Peter Zijlstra |
| 2024 | 6.12 | PREEMPT_RT 완전 메인라인 병합 | Thomas Gleixner, Sebastian Siewior, Peter Zijlstra |
CONFIG_PREEMPT_RT 옵션이 공식적으로 추가되었습니다.
핵심 소스 파일 맵
| 파일 | 역할 | 주요 함수/구조체(Struct) |
|---|---|---|
kernel/locking/rtmutex.c | RT mutex 핵심 구현 | rt_mutex_lock(), rt_mutex_slowlock(), PI chain |
kernel/locking/rtmutex_common.h | RT mutex 내부 구조체 | rt_mutex_base, rt_mutex_waiter |
kernel/locking/rtmutex_api.c | RT mutex 공개 API | rt_mutex_lock(), rt_mutex_unlock() |
kernel/locking/spinlock_rt.c | RT spinlock 구현 | rt_spin_lock(), rt_spin_unlock() |
include/linux/spinlock_types.h | spinlock_t RT 재정의 | spinlock_t → rt_mutex_base |
include/linux/local_lock.h | local_lock API | local_lock(), local_unlock() |
kernel/irq/manage.c | IRQ 스레드화 | irq_setup_forced_threading() |
kernel/sched/core.c | 스케줄러 코어 | __schedule(), PI boost 연동 |
kernel/rcu/tree_plugin.h | Preemptible RCU | rcu_read_lock_nesting |
kernel/softirq.c | softirq RT 처리 | invoke_softirq(), ksoftirqd |
kernel/time/hrtimer.c | 고해상도 타이머 | hrtimer_interrupt() |
kernel/locking/ww_mutex.h | Wound/Wait mutex (RT 호환) | ww_mutex_lock() |
PREEMPT_RT가 변환하지 않는 것들
raw_spinlock_t— 항상 진짜 busy-wait spinlockmutex— 이미 sleeping lock이므로 변경 불필요 (단, PI futex와 연동)semaphore— 변경 없음 (이미 sleeping)completion— 변경 없음atomic_t— 하드웨어 atomic 명령어 기반, 변경 불필요- NMI(Non-Maskable Interrupt) 핸들러 — 스레드화 불가능
RT 패치 연대기 상세
PREEMPT_RT의 역사는 2004년 Ingo Molnar의 최초 패치셋 공개로 시작됩니다. 이후 20년에 걸쳐 핵심 인프라가 하나씩 메인라인에 병합되었으며, 각 단계마다 커널 전체의 코드 품질과 선점성이 향상되었습니다.
| 연도 | 이벤트 | 상세 내용 | 영향 |
|---|---|---|---|
| 2004 | 최초 패치셋 공개 | Ingo Molnar가 voluntary-preempt, PREEMPT_RT 패치셋 발표 | 실시간 리눅스의 새 패러다임 제시 |
| 2005 | genirq 병합 | IRQ 서브시스템 재설계, 아키텍처 독립적 IRQ 관리 레이어 | threaded IRQ의 기반 마련 |
| 2006 | hrtimer 병합 | 기존 timer wheel 보완, 나노초 해상도 타이머(Timer) | 정밀한 주기 실행 가능 |
| 2006 | PREEMPT_RCU | Paul McKenney의 선점 가능 RCU 구현 | RCU 읽기 구간 중 선점 허용 |
| 2008 | ftrace 병합 | Steven Rostedt의 함수 트레이싱 프레임워크 | irqsoff/preemptoff 트레이서로 RT 디버깅(Debugging) 혁신 |
| 2009 | threaded IRQ | request_threaded_irq() API 공식 도입 | IRQ 핸들러 스레드화의 표준 인터페이스 |
| 2009 | PI futex | RT mutex 기반 futex 우선순위 상속 | 사용자 공간 PI 동기화 |
| 2013 | SCHED_DEADLINE | EDF + CBS 기반 데드라인 스케줄링 | 대역폭 보장 스케줄링 |
| 2015 | OSADL 장기 테스트 시작 | 수백 개 시스템에서 24/7 cyclictest 모니터링 | RT 커널 안정성 입증 데이터 축적 |
| 2017 | Linux Foundation CII 지원 | PREEMPT_RT 개발에 공식 재정 지원 | 개발 지속성 보장 |
| 2020 | local_lock 병합 | per-CPU 데이터 보호를 위한 새 API | RT에서 preempt_disable 대체 |
| 2021 | printk 스레드화 (부분) | John Ogness의 printk ringbuffer + console 스레드화 | printk 중 선점 불가 구간 제거 시작 |
| 2022 | PREEMPT_DYNAMIC | 부트 타임 선점 모델 선택 | 배포판 단일 커널로 다중 모델 지원 |
| 2023 | printk 완전 스레드화 | console_lock RT 호환 리팩토링 | 마지막 주요 RT 장벽 제거 |
| 2024 | Linux 6.12 완전 병합 | CONFIG_PREEMPT_RT 메인라인 공식 옵션 | 20년 프로젝트 완성 |
핵심 기여자와 역할
| 기여자 | 주요 역할 | 기여 구성 요소 | 소속 |
|---|---|---|---|
| Thomas Gleixner | RT 총괄 메인테이너 | genirq, hrtimer, threaded IRQ, RT mutex, printk RT, local_lock 감독 | Linutronix GmbH |
| Ingo Molnar | 최초 개발자 | voluntary-preempt, 초기 sleeping spinlock, CFS 스케줄러 | Red Hat |
| Sebastian Siewior | RT 코어 개발자 | local_lock, softirq RT 변환, per-CPU 락 변환, RT 패치 유지보수 | Linutronix GmbH |
| Steven Rostedt | ftrace 메인테이너 | ftrace, function tracer, irqsoff/preemptoff tracer, trace-cmd, RT 테스트 | Google (VMware 출신) |
| Paul McKenney | RCU 메인테이너 | PREEMPT_RCU, RCU_BOOST, Tree RCU, RCU nocbs | Meta (IBM/Linux Technology Center 출신) |
| Peter Zijlstra | 스케줄러/락 코어 | PREEMPT_DYNAMIC, RT mutex 리팩토링, lockdep, perf | Intel |
| John Ogness | printk RT화 | printk ringbuffer, console 스레드화, nbcon (non-blocking console) | Linutronix GmbH |
| Juri Lelli | SCHED_DEADLINE | EDF 스케줄러, CBS, GRUB(Greedy Reclamation of Unused Bandwidth) | Red Hat |
PREEMPT_RT vs Xenomai vs RTAI vs RTLinux
실시간 리눅스에는 크게 두 가지 접근 방식이 있습니다: 단일 커널(PREEMPT_RT)과 듀얼 커널(Xenomai, RTAI, RTLinux). 각 접근은 지연, API 호환성, 유지보수성에서 뚜렷한 트레이드오프를 가집니다.
# 각 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 옵션 | 선점 지점 | 최악-케이스 지연 | 용도 |
|---|---|---|---|---|
| NONE | PREEMPT_NONE | 시스템 콜(System Call) 반환, 인터럽트 반환 시에만 | 수십~수백 ms | 서버, 처리량 극대화 |
| VOLUNTARY | PREEMPT_VOLUNTARY | NONE + might_sleep() 체크 지점 | 수 ms ~ 수십 ms | 데스크톱 (기본값) |
| FULL | PREEMPT | 커널 코드 어디서든 (spin_unlock 후 등) | 수백 us ~ 수 ms | 저지연 데스크톱, 일부 임베디드 |
| RT | PREEMPT_RT | FULL + 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로 컴파일해야 하며, 동적 전환 대상이 아닙니다.
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_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_RT | 10-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
spin_lock_irqsave()는 실제로 인터럽트를 비활성화하지 않습니다.
인터럽트 핸들러가 이미 스레드화되어 있으므로, 인터럽트 비활성화 없이도 동기화가 보장됩니다.
이것이 RT 커널의 지연 시간을 극적으로 줄이는 핵심 메커니즘입니다.
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의 성능 영향
| 연산 | 일반 커널 (ns) | RT 커널 (ns) | 오버헤드 비율 | 비고 |
|---|---|---|---|---|
spin_lock/unlock (비경합) | ~15 | ~30 | 2x | RT: rt_mutex fast-path cmpxchg |
spin_lock/unlock (경합(Contention)) | ~수천 (busy-wait) | ~100 (sleep/wake) | 0.01x~0.1x | RT가 경합 시 훨씬 효율적 |
spin_lock_irqsave/restore | ~25 | ~30 | 1.2x | RT: IRQ 실제 비활성화 안 함 |
raw_spin_lock/unlock | ~15 | ~15 | 1x | 동일 (변환 없음) |
mutex_lock/unlock (비경합) | ~20 | ~20 | 1x | 이미 sleeping lock |
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);
}
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;
}
spin→sleep 변환의 성능 영향 측정
sleeping spinlock의 실제 성능 영향은 워크로드에 따라 크게 달라집니다. 다음은 대표적인 벤치마크 결과입니다.
| 워크로드 | 오버헤드 | 병목(Bottleneck) 원인 | 개선 방법 |
|---|---|---|---|
커널 빌드 (make -j$(nproc)) | 3-8% | 파일시스템(Filesystem) 락 경합 | tmpfs 사용으로 최소화 |
| Redis (GET/SET 벤치마크) | 5-10% | 네트워크 softirq 스레드화 | ksoftirqd 우선순위 조정 |
| PostgreSQL OLTP | 8-15% | WAL 쓰기 + 락 경합 | per-CPU 배치 처리 |
| nginx 정적 파일 | 3-5% | 소켓(Socket) 락 | SO_REUSEPORT로 분산 |
| cyclictest (RT 지연) | N/A | N/A | Max 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 플래그로
스레드화를 방지합니다. 이들은 RT 커널에서도 하드 IRQ 컨텍스트에서 실행되며,
raw_spinlock_t만 사용해야 합니다.
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 동작
| 플래그 | 일반 커널 동작 | RT 커널 동작 | 스레드화 여부 |
|---|---|---|---|
IRQF_SHARED | 여러 디바이스 IRQ 공유 | 동일 (스레드에서 공유) | 스레드화됨 |
IRQF_NO_THREAD | 스레드화 금지 | 하드 IRQ에서 실행 유지 | 스레드화 안 됨 |
IRQF_ONESHOT | 스레드 완료까지 IRQ 마스킹 | 동일 | 이미 스레드 패턴 |
IRQF_PERCPU | per-CPU 인터럽트 | 하드 IRQ 유지 | 스레드화 안 됨 |
IRQF_TIMER | 타이머 인터럽트 | 하드 IRQ 유지 (NO_THREAD 포함) | 스레드화 안 됨 |
IRQF_NOBALANCING | IRQ 밸런싱 제외 | 동일 | 스레드화됨 |
IRQ 스레드 우선순위 설계 가이드
| 우선순위 범위 | 할당 대상 | 예시 |
|---|---|---|
| 99 | watchdog | [watchdog/N] |
| 95-98 | 하드웨어 제약 IRQ | 타이머, 전원 관리(Power Management) |
| 85-94 | RT 태스크의 IRQ 스레드 | irq/N-sensor |
| 80-89 | 주요 RT 태스크 | 제어 루프, 센서 처리 |
| 50-79 | 일반 IRQ 스레드 (기본값) | irq/N-xhci, irq/N-nvme |
| 10-49 | 보조 RT 태스크 | 로깅, 모니터링 |
| 1-9 | RCU 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 플래그가 설정된 인터럽트는 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 (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) 프로토콜로 이를 해결합니다.
우선순위 역전 시나리오
| 시점 | Low (prio 10) | Mid (prio 50) | High (prio 90) | 문제 |
|---|---|---|---|---|
| t0 | lock(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와 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);
max_lock_depth(기본 1024)를 초과하면
PI chain 추적이 중단되고 -EDEADLK가 반환됩니다.
실전에서 PI chain 깊이가 5를 초과하면 설계를 재검토해야 합니다.
깊은 PI chain은 지연 시간 예측을 어렵게 만듭니다.
PI 동작의 성능 비용
| 연산 | 비용 | 빈도 | 설명 |
|---|---|---|---|
| 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 추적 알고리즘:
- 현재 태스크(task)가 대기 중인 lock을 찾습니다
- lock의 waiter 리스트에서 top_waiter를 갱신합니다
- lock 소유자의 PI 리스트를 갱신합니다
- 소유자의 유효 우선순위를 재계산합니다
- 소유자가 다른 lock을 대기 중이면 → 4단계로 이동합니다 (체인 추적)
- 체인 끝에 도달하면 종료합니다
시간 복잡도는 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 체인 깊이 제한과 데드락 감지
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 (기본) */
- chain 추적 오버헤드 증가 (각 단계마다 raw_spinlock 획득)
- 최악-케이스 지연 예측이 어려움 (chain 깊이 × 단계별 부스팅 시간)
- 디버깅 난이도 급증
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;
}
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
CONFIG_DEBUG_RT_MUTEXES와 CONFIG_PROVE_LOCKING은 상당한 오버헤드를 유발합니다
(각각 ~10%, ~30% 성능 저하). 개발 중에 활성화하여 잠재적 문제를 조기에 발견하고,
프로덕션 빌드에서는 반드시 비활성화하세요.
SCHED_FIFO / SCHED_RR / SCHED_DEADLINE과 동기화
리눅스 커널은 세 가지 실시간 스케줄링 정책을 제공합니다. 각 정책은 RT mutex의 우선순위 상속 메커니즘과 긴밀하게 연동됩니다.
| 정책 | 알고리즘 | 우선순위 범위 | 타임 슬라이스 | 대역폭 보장 | PI 동작 |
|---|---|---|---|---|---|
SCHED_FIFO | 고정 우선순위 FIFO | 1-99 | 없음 (양보 시만 전환) | 없음 | 우선순위 값으로 비교 |
SCHED_RR | 고정 우선순위 Round-Robin | 1-99 | 100ms (기본) | 없음 | 우선순위 값으로 비교 |
SCHED_DEADLINE | EDF (Earliest Deadline First) | deadline 기반 | runtime/period | CBS 보장 | 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_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, ¶m) == -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");
}
}
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 태스크, 최고 우선순위 | SCHED_FIFO | 가장 단순, 오버헤드 최소 |
| 같은 우선순위의 여러 RT 태스크 | SCHED_RR | 공정한 CPU 분배 |
| 주기적 태스크, 대역폭 보장 필요 | SCHED_DEADLINE | CBS로 대역폭 격리, 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 커널 개발에서 가장 중요한 설계 결정 중 하나입니다.
| 속성 | spinlock_t | raw_spinlock_t |
|---|---|---|
| 일반 커널 동작 | busy-wait + preempt_disable | busy-wait + preempt_disable |
| RT 커널 동작 | rt_mutex (sleeping) | busy-wait + preempt_disable (변경 없음) |
| 선점 비활성화 | RT에서 안 함 | 항상 함 |
| PI 지원 | 예 | 아니오 |
| 인터럽트 컨텍스트 | RT에서 사용 불가 | 사용 가능 |
| 임계 구간 길이 | 길어도 됨 | 매우 짧아야 함 |
| 사용 예 | 대부분의 드라이버 락 | 스케줄러 런큐, IRQ 디스크립터, hrtimer, printk |
선택 기준: Decision Tree
커널 소스에서의 사용 예
/* 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는 다음 세 가지 조건 중 하나 이상을 만족할 때만 사용해야 합니다:
- 하드 IRQ / NMI 컨텍스트에서 접근 — sleeping lock 사용 불가
- 스케줄러/타이머 코어 경로 — schedule() 호출 자체에 필요한 락
- RT mutex 인프라 자체를 보호 — 부트스트랩 문제 방지
| 서브시스템 | 구조체 | raw_spinlock 이름 | 이유 |
|---|---|---|---|
| 스케줄러 | struct rq | __lock | schedule() 자체에 필수 |
| hrtimer | struct hrtimer_cpu_base | lock | 타이머 인터럽트 핸들러에서 접근 |
| IRQ 서브시스템 | struct irq_desc | lock | 하드 IRQ 핸들러에서 접근 |
| RT mutex | struct rt_mutex_base | wait_lock | 부트스트랩 (sleeping lock의 보호) |
| printk | struct console | lock | NMI에서 출력 가능해야 함 |
| 페이지(Page) 할당 | struct zone | lock | atomic 할당 컨텍스트 |
| RCU | struct rcu_node | lock | grace period 관리 (인터럽트에서 접근) |
| 시간 관리 | struct timekeeper | lock | 타이머 인터럽트에서 시간 갱신 |
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_t를 raw_spinlock_t로 변환할 때 흔한 실수:
- 보호 구간 내에서
kmalloc(GFP_KERNEL)호출 — raw_spinlock 하에서 sleep 불가! - 보호 구간이 지나치게 긴 경우 — RT 지연의 직접적 원인이 됨
- 불필요한 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
| 속성 | preempt_disable() | migrate_disable() |
|---|---|---|
| 선점 | 비활성화 | 활성 상태 유지 |
| CPU 마이그레이션 | 불가 (선점 꺼짐) | 불가 (명시적 비활성화) |
| 다른 태스크 실행 | 불가 | 가능 (같은 CPU에서) |
| sleeping lock 사용 | 불가 | 가능 |
| RT 환경 적합성 | 최소한으로 사용 | per-CPU 보호에 적합 |
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 이름 | 보호 대상 |
|---|---|---|---|
| 페이지 할당 | mm/page_alloc.c | pagesets_lock | per-CPU 페이지 캐시 (PCP lists) |
| SLAB 할당 | mm/slub.c | s_lock | per-CPU slab 캐시(Cache) |
| 네트워크 | net/core/dev.c | netdev_lock | per-CPU 네트워크 통계 |
| swap | mm/swap.c | swapvec_lock | per-CPU LRU 배치 리스트 |
| VFS | fs/namespace.c | vfsmount_lock | per-CPU 마운트(Mount) 참조 카운트(Reference Count) |
| random | drivers/char/random.c | batched_entropy_lock | per-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 모드 | CONFIG 옵션 | 읽기 구간 선점 | grace period | RT 적합성 |
|---|---|---|---|---|
| Classic RCU | TREE_RCU | 불가 | 빠름 | 부적합 |
| Preemptible RCU | PREEMPT_RCU | 가능 | 추적 필요 | 적합 |
| + RCU Boost | RCU_BOOST | 가능 + 부스트 | 부스트로 가속 | 필수 |
RCU 관련 커널 스레드
RT 커널에서 RCU 처리를 담당하는 여러 커널 스레드가 있습니다.
rcu_nocbs 파라미터로 RCU 콜백(Callback)을 특정 CPU에서 분리할 수 있습니다.
| 스레드 | 역할 | 격리 CPU 영향 | 설정 |
|---|---|---|---|
rcu_preempt | RCU grace period 관리 | CPU 0에서 실행 | 자동 |
rcuog/N | RCU 콜백 오프로드 실행 | housekeeping CPU로 이동 | rcu_nocbs= |
rcuop/N | RCU 콜백 실행 (이전 방식) | housekeeping CPU로 이동 | rcu_nocbs= |
rcub/N | RCU 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 변형
| 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 태스크가 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 우선순위 조정 |
rcu_read_lock() 구간에서 장시간 처리하면
grace period가 지연되어 메모리 누수(RCU 콜백 누적)가 발생할 수 있습니다.
RCU 읽기 구간은 가능한 짧게 유지하고, 긴 처리가 필요하면 SRCU 사용을 고려하세요.
Softirq 직렬화와 RT
일반 커널에서 softirq는 인터럽트 반환 시점에 실행되며,
local_bh_disable()로 보호됩니다.
이는 RT 환경에서 두 가지 문제를 야기합니다:
- Softirq 실행 시간이 예측 불가능 (여러 softirq가 연속으로 실행될 수 있음)
local_bh_disable()이 선점을 비활성화함
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_SOFTIRQ와 NET_TX_SOFTIRQ는
softirq 중 가장 실행 시간이 긴 경우가 많습니다.
RT 커널에서는 이것이 ksoftirqd에서 처리되므로, 네트워크 트래픽이 폭주해도
RT 태스크의 지연에 영향을 미치지 않습니다.
Softirq 종류와 RT 영향
| softirq | 번호 | 일반 커널 실행 위치 | RT 커널 실행 위치 | RT 영향도 |
|---|---|---|---|---|
HI_SOFTIRQ | 0 | IRQ 반환 시 | ksoftirqd | 낮음 |
TIMER_SOFTIRQ | 1 | IRQ 반환 시 | ksoftirqd | 중간 |
NET_TX_SOFTIRQ | 2 | IRQ 반환 시 | ksoftirqd | 높음 (대량 전송 시) |
NET_RX_SOFTIRQ | 3 | IRQ 반환 시 | ksoftirqd | 높음 (패킷(Packet) 폭주 시) |
BLOCK_SOFTIRQ | 4 | IRQ 반환 시 | ksoftirqd | 중간 |
IRQ_POLL_SOFTIRQ | 5 | IRQ 반환 시 | ksoftirqd | 낮음 |
TASKLET_SOFTIRQ | 6 | IRQ 반환 시 | ksoftirqd | 드라이버 의존 |
SCHED_SOFTIRQ | 7 | IRQ 반환 시 | ksoftirqd | 중간 (로드 밸런싱) |
HRTIMER_SOFTIRQ | 8 | IRQ 반환 시 | ksoftirqd | 높음 (타이머 정밀도) |
RCU_SOFTIRQ | 9 | IRQ 반환 시 | 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 영향
- 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는 soft와 hard 모드를 구분합니다.
Hard hrtimer는 하드 IRQ 컨텍스트에서 콜백이 실행되고,
soft hrtimer는 softirq(RT에서는 ksoftirqd) 컨텍스트에서 실행됩니다.
| 속성 | Hard hrtimer | Soft hrtimer |
|---|---|---|
| 실행 컨텍스트 | 하드 IRQ (NMI에 가까움) | softirq (RT: ksoftirqd) |
| sleeping lock 사용 | 불가 | 가능 (RT 커널) |
| 정밀도 | 최고 (하드웨어 한계) | softirq 지연 추가 |
| 선점 가능 | 불가 | 가능 (RT 커널) |
| 사용 예 | 스케줄러 틱, watchdog | POSIX 타이머, 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(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 모드 | 이유 |
|---|---|---|
| 스케줄러 틱 | HARD + PINNED | 스케줄러 코어 경로, sleeping 불가 |
| watchdog | HARD | NMI와 연동, 최고 정밀도 필요 |
| POSIX timer (timer_create) | SOFT | 사용자 공간 시그널 전달 |
| nanosleep | SOFT | 프로세스 컨텍스트, sleeping OK |
| TCP 재전송(Retransmission) 타이머 | SOFT | softirq 컨텍스트 |
| perf events | HARD | PMU 인터럽트 핸들러에서 접근 |
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_nocbs | RCU 콜백 오프로드 | 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
- Min: 최소 지연 (보통 1-2us, 하드웨어 최소 타이머 해상도)
- Avg: 평균 지연 (RT 커널에서 보통 2-5us)
- Max: 가장 중요! 최악-케이스 지연 (RT 커널에서 10-50us, 튜닝 시 <20us)
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)
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 디버깅에 활용됩니다.
주요 RT 관련 트레이서
| 트레이서 | 측정 대상 | CONFIG 옵션 | 설명 |
|---|---|---|---|
irqsoff | IRQ 비활성화 구간 | IRQSOFF_TRACER | 가장 긴 IRQ-off 구간과 콜 스택 기록 |
preemptoff | 선점 비활성화 구간 | PREEMPT_TRACER | 가장 긴 preempt-off 구간 기록 |
preemptirqsoff | IRQ+선점 비활성화 | 두 옵션 모두 | irqsoff + preemptoff 결합 |
wakeup_rt | RT 태스크 깨우기(Wakeup) 지연 | SCHED_TRACER | wakeup → 실제 실행까지 지연 추적 |
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
function 트레이서는 ~10-30ns/호출, function_graph는 ~50-100ns/호출의
오버헤드를 유발합니다. 프로덕션 RT 시스템에서는 평상 시 트레이서를 비활성화하고,
문제 분석 시에만 활성화하세요.
osnoise와 hwlat는 오버헤드가 매우 낮아 상시 사용 가능합니다.
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 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 사용 가능
| 파라미터 | 기본값 | 범위 | 설명 |
|---|---|---|---|
sched_rt_period_us | 1000000 (1s) | 1 ~ INT_MAX | RT 대역폭 측정 주기 |
sched_rt_runtime_us | 950000 (950ms) | -1 ~ INT_MAX | 주기 당 최대 RT 실행 시간 (-1: 무제한) |
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
| 옵션 | 설명 | RT 필수 여부 |
|---|---|---|
CONFIG_PREEMPT_RT | PREEMPT_RT 활성화 | 필수 |
CONFIG_HIGH_RES_TIMERS | 나노초 해상도 타이머 | 필수 |
CONFIG_NO_HZ_FULL | 틱리스(Tickless) 커널 (격리 CPU용) | 권장 |
CONFIG_RCU_BOOST | RCU 읽기 구간 부스트 | 권장 |
CONFIG_CPU_FREQ_DEFAULT_GOV_PERFORMANCE | CPU 주파수 고정 (성능 모드) | 권장 |
CONFIG_FTRACE | ftrace 트레이싱 | 디버깅용 |
CONFIG_IRQSOFF_TRACER | IRQ-off 지연 추적 | 디버깅용 |
필수 CONFIG 옵션 상세
| 카테고리 | 옵션 | 기본값 | 설명 | RT 필수 여부 |
|---|---|---|---|---|
| 선점 | CONFIG_PREEMPT_RT | n | PREEMPT_RT 활성화 | 필수 |
| 선점 | CONFIG_PREEMPT_DYNAMIC | y (배포판) | 부트 타임 선점 선택 | 비호환 (RT와 별도) |
| 타이머 | CONFIG_HIGH_RES_TIMERS | y | 고해상도 hrtimer | 필수 |
| 타이머 | CONFIG_NO_HZ_FULL | n | 격리 CPU 틱 제거 | 강력 권장 |
| RCU | CONFIG_PREEMPT_RCU | 자동 | 선점 가능 RCU | 자동 (RT 시) |
| RCU | CONFIG_RCU_BOOST | n | RCU 읽기 구간 부스트 | 강력 권장 |
| RCU | CONFIG_RCU_BOOST_DELAY | 500ms | 부스트 지연 | 권장 |
| 전원 | CONFIG_CPU_FREQ_DEFAULT_GOV_PERFORMANCE | n | CPU 주파수 고정 | 강력 권장 |
| 메모리 | CONFIG_TRANSPARENT_HUGEPAGE | y | 투명 대형 페이지 | 비활성화 권장 (compaction 지연) |
| 디버그 | CONFIG_DEBUG_RT_MUTEXES | n | RT mutex 디버깅 | 개발용 |
| 디버그 | CONFIG_DEBUG_PREEMPT | n | 선점 카운터 검증 | 개발용 |
| 디버그 | CONFIG_PROVE_LOCKING | n | lockdep 활성화 | 개발용 |
| 트레이싱 | CONFIG_IRQSOFF_TRACER | n | IRQ-off 지연 추적 | 디버깅 권장 |
| 트레이싱 | CONFIG_PREEMPT_TRACER | n | 선점-off 지연 추적 | 디버깅 권장 |
| 트레이싱 | CONFIG_SCHED_TRACER | n | wakeup_rt 트레이서 | 디버깅 권장 |
| 트레이싱 | CONFIG_OSNOISE_TRACER | n | OS 노이즈 트레이서 | 상시 권장 |
| 트레이싱 | CONFIG_HWLAT_TRACER | n | 하드웨어 지연 감지 | 상시 권장 |
디버그 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 커널 패키지
| 배포판 | 패키지 | 기반 커널 | 설치 방법 |
|---|---|---|---|
| Ubuntu | linux-image-rt | 6.x-rt | apt install linux-image-rt-amd64 |
| RHEL/CentOS | kernel-rt | 5.14-rt (RHEL 9) | dnf install kernel-rt |
| Fedora | kernel-rt | 6.x-rt | dnf install kernel-rt |
| Debian | linux-image-rt-amd64 | 6.x-rt | apt install linux-image-rt-amd64 |
| openSUSE | kernel-rt | 6.x-rt | zypper install kernel-rt |
| Yocto | linux-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 커널 위에서 실시간 보장을 받으려면 사용자 공간 애플리케이션도 특별한 설계 패턴을 따라야 합니다. 가장 중요한 원칙은 “초기화 시 모든 자원을 할당하고, 실시간 루프에서는 할당/해제하지 않습니다”입니다.
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, ¶m);
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용 메모리 풀 설계
Lock-free 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) 주의사항
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, ¶m) == -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 애플리케이션 디버깅 체크리스트
| 증상 | 가능한 원인 | 진단 도구 | 해결 방법 |
|---|---|---|---|
| 주기적 지터 (1ms 간격) | 타이머 틱 | perf stat -e irq:* | nohz_full= 부트 파라미터 |
| 불규칙 스파이크 (100-500us) | SMI (System Management Interrupt) | hwlat_detector | BIOS에서 SMI 비활성화 |
| 페이지 폴트 스파이크 | mlockall() 누락 또는 동적 할당 | perf stat -e page-faults | mlockall(MCL_CURRENT|MCL_FUTURE) |
| 커널 스핀락 경합 | 다른 코어에서 공유 락 보유 | ftrace + lock:* | per-CPU 데이터 사용, 락 분리 |
| IRQ 간섭 | 격리 CPU에 IRQ 도착 | /proc/interrupts | irqaffinity= + managed_irq |
| RCU 콜백 간섭 | 격리 CPU에서 RCU cb 실행 | rcu/*/rcudata | rcu_nocbs= |
| wakeup 지연 > 50us | 선점 지연 | wakeup_rt 트레이서 | 우선순위 검토, CPU isolation |
| RT throttling | sched_rt_runtime_us 초과 | /proc/sched_debug | sched_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;
}
성능 비교
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% ↓ |
워크로드별 성능 특성
| 워크로드 유형 | 일반 커널 | 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=disablePerformance 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(모바일 로봇)입니다.
| 제어 대상 | 제어 주기 | 허용 지터 | 비고 |
|---|---|---|---|
| 서보 모터 (산업용 로봇팔) | 250us ~ 1ms | < 50us | EtherCAT 기반, SCHED_DEADLINE 사용 |
| 모바일 로봇 네비게이션 | 10ms ~ 50ms | < 1ms | LiDAR/IMU 센서 퓨전 |
| 드론 비행 제어 | 1ms ~ 4ms | < 100us | PX4/ArduPilot 기반 |
| CNC 공작기계 | 1ms | < 20us | LinuxCNC + 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)에 따라 지연 요구사항이 다릅니다.
| ASIL 등급 | 최대 허용 지연 | 해당 기능 | RT 설정 |
|---|---|---|---|
| ASIL-D | < 10us | 긴급 제동(AEB), 조향 보조 | 전용 마이크로컨트롤러 (리눅스 아님) |
| ASIL-B | < 1ms | 적응형 순항 제어, 차선 유지 | PREEMPT_RT + CPU isolation |
| QM | < 100ms | 인포테인먼트, OTA 업데이트 | 일반 커널 충분 |
통신 인프라 (5G RAN, O-RAN)
5G 기지국의 RAN(Radio Access Network) 처리에서는 1ms 이하의 지연이 요구됩니다. O-RAN Alliance의 개방형 아키텍처에서 PREEMPT_RT 리눅스가 DU(Distributed Unit) 플랫폼으로 채택되고 있습니다.
| 구성 요소 | 지연 요구사항 | 주기 | RT 설정 |
|---|---|---|---|
| L1 PHY (LDPC/FFT) | < 100us | TTI (0.5ms) | DPDK + CPU isolation + hugepage |
| L2 MAC 스케줄러 | < 500us | 1ms | PREEMPT_RT + SCHED_FIFO |
| Fronthaul (eCPRI) | < 100us | 125us (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 활용
- 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(의료 기기 소프트웨어 수명 주기) 인증에서는 소프트웨어의 결정론적 동작을 요구합니다.
| 기기 유형 | 안전 등급 | 최대 지연 | 주기 | 인증 |
|---|---|---|---|---|
| 수술 로봇 (마스터-슬레이브) | Class III | < 1ms | 1kHz | FDA 510(k), IEC 62304 |
| 재활 로봇 (외골격) | Class II | < 5ms | 200Hz | IEC 62304, ISO 13482 |
| 환자 모니터 (ECG/SpO2) | Class II | < 100ms | 가변 | IEC 62304 |
| 인공호흡기 제어 | Class III | < 10ms | 100Hz | FDA, 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 | Xenomai (듀얼-커널) | RTAI |
|---|---|---|---|
| 아키텍처 | 단일 커널 | 듀얼 커널 (Cobalt + Linux) | 듀얼 커널 (RTAI HAL + Linux) |
| 최악-케이스 지연 | 10-50us (튜닝 시) | 1-10us | 1-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 필요
관련 문서
- 동기화 기초 — spinlock, mutex, semaphore, completion 등 기본 동기화 원시 타입
- 메모리 배리어 — 메모리 순서 보장(Ordering), barrier(), smp_mb(), READ_ONCE/WRITE_ONCE
- RCU (Read-Copy-Update) — lock-free 읽기 동시성, grace period, SRCU, Tree RCU
- Futex — 사용자 공간 동기화, PI futex (pthread_mutex + PTHREAD_PRIO_INHERIT)
- Lock-free 프로그래밍 — atomic 연산, compare-and-swap, lock-free 큐/스택
- 동시성 디버깅 — lockdep, KCSAN, KASAN, 데드락 감지
- 스케줄러 — CFS, RT 스케줄링 클래스, SCHED_DEADLINE, 런큐 관리
- EEVDF 스케줄러 — Earliest Eligible Virtual Deadline First 알고리즘
- sched_ext — BPF 기반 확장 가능 스케줄러 프레임워크
- cpusets & CPU Isolation — CPU 격리
- 타이머 — High-Resolution Timer 구조
- 인터럽트 — Interrupt Threading
- 커널 문서:
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 트레이서를 활용한 지연 분석 방법을 설명합니다.
주요 용어 정리
| 용어 | 설명 |
|---|---|
| WCET | Worst-Case Execution Time. 최악-케이스 실행 시간. 실시간 시스템 설계의 핵심 지표. |
| Jitter | 주기적 태스크의 실행 시간 편차. 이상적으로 0에 가까워야 함. |
| Deterministic | 결정론적. 동일 입력에 대해 항상 동일한 시간 내에 완료됨을 보장. |
| PI (Priority Inheritance) | 우선순위 상속. 락 소유자의 우선순위를 대기자 수준으로 임시 상승. |
| Priority Inversion | 우선순위 역전. 고우선순위 태스크가 저우선순위 태스크에 의해 간접적으로 블록됨. |
| Grace Period | RCU에서 모든 기존 읽기 구간이 완료되는 시점. RT에서는 선점 가능 RCU로 추적. |
| CBS | Constant Bandwidth Server. SCHED_DEADLINE에서 대역폭을 보장하는 알고리즘. |
| EDF | Earliest Deadline First. 가장 이른 deadline을 가진 태스크를 먼저 실행하는 알고리즘. |
| nohz_full | Adaptive-ticks 모드. 유일한 실행 태스크만 있으면 타이머 틱을 중단. |
| SMI | System Management Interrupt. BIOS가 발생시키는 인터럽트로 커널이 감지 불가. |
| OSADL | Open Source Automation Development Lab. RT 리눅스 지연 측정 데이터를 장기 수집하는 프로젝트. |