RT Mutex (우선순위 상속 뮤텍스)
실시간(Real-time) 시스템에서 치명적인 Priority Inversion 문제를 해결하기 위한 Priority Inheritance 프로토콜 기반 뮤텍스(Mutex)인 rt_mutex를 분석합니다. Mars Pathfinder 사례부터 struct rt_mutex 내부 구조, rb-tree 기반 대기자 관리, PI Chain 전파 알고리즘, Boosting/Deboosting 메커니즘, 데드락 감지, PREEMPT_RT에서의 spinlock_t 변환, PI Futex를 통한 사용자 공간(User Space) 연결까지 커널 소스 기반으로 포괄합니다.
핵심 요약
- Priority Inversion 해결 — 낮은 우선순위 태스크(Task)가 높은 우선순위 태스크의 실행을 무한정 지연(Latency)시키는 문제를 Priority Inheritance로 해결합니다.
- PI 프로토콜 — 락 보유자의 우선순위를 대기자 중 가장 높은 우선순위로 일시 부스팅하여 임계 영역(Critical Section)을 빠르게 빠져나가게 합니다.
- rb-tree 대기열 — 대기자를 우선순위 순으로 rb-tree에 정렬하여 O(log n) 삽입/삭제와 O(1) 최고 우선순위 조회를 보장합니다.
- PI Chain 전파 — 중첩된 락 보유 관계를 따라 우선순위를 연쇄적으로 전파하며, 순환 감지로 데드락을 탐지합니다.
- PREEMPT_RT 핵심 — PREEMPT_RT 커널에서 모든
spinlock_t가 내부적으로 rt_mutex로 변환되어 ~10,000개 이상의 커널 락이 PI를 지원합니다.
단계별 이해
- Priority Inversion 문제 이해
고/중/저 우선순위 태스크 간 상호작용에서 발생하는 unbounded priority inversion을 파악합니다. - Priority Inheritance 프로토콜 학습
락 보유자 우선순위를 대기자 수준으로 일시 부스팅하는 메커니즘을 이해합니다. - rt_mutex 자료구조 분석
wait_lock, waiters rb-tree, owner 포인터의 역할과 상호 관계를 파악합니다. - PI Chain 전파 추적
rt_mutex_adjust_prio_chain()이 중첩된 락 체인을 따라 우선순위를 전파하는 과정을 추적합니다. - 실전 적용과 디버깅(Debugging)
PREEMPT_RT 환경, PI Futex, /proc/lock_stat 활용법을 익힙니다.
이론적 배경: 우선순위 역전(Priority Inversion)과 우선순위 상속(Priority Inheritance)
실시간 시스템에서 태스크 간 자원 공유는 우선순위 역전(Priority Inversion)이라는 근본적인 문제를 야기합니다. 우선순위 기반 선점형 스케줄러에서 높은 우선순위 태스크가 낮은 우선순위 태스크가 보유한 락을 기다릴 때, 중간 우선순위 태스크가 낮은 우선순위 태스크를 선점(Preemption)하면 높은 우선순위 태스크의 실행이 무한정 지연될 수 있습니다.
역사적 배경: Mars Pathfinder (1997)
1997년 NASA의 Mars Pathfinder 탐사선은 착륙 후 반복적인 시스템 리셋 현상을 겪었습니다. 원인은 VxWorks RTOS에서 발생한 priority inversion이었습니다:
| 태스크 | 우선순위 | 역할 | 문제 |
|---|---|---|---|
| Bus Task | 높음 | 정보 버스(Bus) 관리 | 뮤텍스 대기 중 watchdog 타임아웃 → 리셋 |
| Communication Task | 중간 | 지구 통신 | Bus Task를 간접적으로 차단 |
| Meteorological Task | 낮음 | 기상 데이터 수집 | 뮤텍스를 보유한 채 중간 우선순위에 선점됨 |
해결책은 VxWorks의 mutexOptions에 SEM_INVERSION_SAFE 플래그를 활성화하여 priority inheritance를 켜는 것이었습니다. 이 사건은 실시간 동기화에서 PI 프로토콜의 필수성을 증명한 대표 사례입니다.
해결 접근법 비교
| 기법 | 원리 | 장점 | 단점 |
|---|---|---|---|
| Priority Inheritance (PI) | 보유자를 대기자 최고 우선순위로 부스팅 | 구현 간단, 범용적 | 체인 전파 비용, 비최적 부스팅 |
| Priority Ceiling (PCP) | 락 획득 시 사전 정의된 천장 우선순위로 즉시 부스팅 | 데드락 방지, 체인 불필요 | 모든 접근 태스크 우선순위를 사전에 파악 필요 |
| Random Boosting | Windows: 무작위로 보유자 부스팅 | 구현 단순 | 확률적, 보장 없음 |
| Interrupt Disable | 임계 영역 동안 인터럽트(Interrupt) 비활성화 | 단순 | 단일 CPU만, 지연 시간 증가 |
Linux 커널은 PI 프로토콜을 채택했습니다. PCP는 모든 락에 접근하는 태스크 집합을 정적으로 분석해야 하므로 범용 운영체제에 적합하지 않습니다.
Priority Inversion 문제 상세
Priority Inversion에는 두 가지 유형이 있습니다:
Bounded Priority Inversion
높은 우선순위 태스크 H가 낮은 우선순위 태스크 L이 보유한 락을 기다릴 때, L이 임계 영역을 완료할 때까지만 대기하는 경우입니다. 이 역전 시간은 L의 임계 영역 길이로 상한이 존재하므로 bounded입니다.
/* Bounded Priority Inversion */
/* 역전 시간 = L의 임계 영역 실행 시간 (유한) */
시간 →
L: ═══[lock]══════[unlock]═══
H: ├─대기─┤실행
/* 대기 시간의 상한이 존재: L의 임계 영역 길이 */
Unbounded Priority Inversion
중간 우선순위 태스크 M이 L을 선점하면, L이 임계 영역을 빠져나올 수 없어 H의 대기가 무한정 길어집니다. M이 여러 개 존재하면 역전은 사실상 상한이 없습니다:
/* 3개 태스크: H(높음), M(중간), L(낮음) */
/* L이 뮤텍스 X를 보유하고 임계 영역 실행 중 */
/* t1: L이 뮤텍스 X 획득 */
L: mutex_lock(&X);
/* t2: M이 도착하여 L을 선점 (M > L 이므로) */
M: /* CPU 사용 — 긴 연산 */
/* t3: H가 도착하지만 X를 기다려야 함 */
H: mutex_lock(&X); /* BLOCKED: L이 보유 중 */
/* 문제: M이 L보다 우선순위가 높아 계속 실행됨 */
/* → L은 CPU를 받지 못해 X를 해제할 수 없음 */
/* → H는 무한정 대기 (M의 수만큼 지연 누적) */
Priority Inheritance 프로토콜
Priority Inheritance Protocol(PIP)의 핵심 규칙은 단순합니다:
- 락 획득 시: 태스크가 락을 성공적으로 획득하면, 자신의 원래 우선순위를 기억합니다.
- 대기 시: 태스크가 이미 보유된 락을 요청하면, 보유자의 effective priority를 자신의 우선순위와 비교하여 더 높은 값으로 부스팅합니다.
- 전파: 보유자가 부스팅되면, 그 보유자가 다른 락을 기다리고 있는 경우 해당 락의 보유자도 연쇄적으로 부스팅합니다 (PI Chain).
- 해제 시: 락을 해제하면 보유자의 우선순위를 원래 값(또는 다른 보유 락의 최고 대기자 우선순위)으로 복원합니다 (Deboosting).
/* PI 프로토콜 의사 코드 */
void pi_lock(struct pi_mutex *lock, struct task_struct *task)
{
if (try_acquire(lock, task))
return; /* fast path: 비경합 */
/* slow path: 이미 보유자가 있음 */
struct task_struct *owner = lock->owner;
/* 대기자 등록 (우선순위 순 rb-tree) */
enqueue_waiter(lock, task);
/* PI 부스팅: owner 우선순위를 task 수준으로 */
if (task->prio < owner->prio) /* 낮은 값 = 높은 우선순위 */
boost_prio(owner, task->prio);
/* PI Chain 전파: owner가 다른 락을 기다리는 경우 */
if (owner->pi_blocked_on)
propagate_pi_chain(owner);
schedule(); /* 슬립 */
}
void pi_unlock(struct pi_mutex *lock, struct task_struct *task)
{
/* 대기자가 있으면 최고 우선순위 대기자를 깨움 */
struct task_struct *next = dequeue_top_waiter(lock);
/* Deboosting: 다른 보유 락의 최고 대기자로 복원 */
deboost_prio(task);
if (next)
wake_up_process(next);
}
struct rt_mutex 분석
Linux 커널의 struct rt_mutex는 kernel/locking/rtmutex_common.h에 정의됩니다:
/* include/linux/rtmutex.h */
struct rt_mutex {
struct rt_mutex_base rtmutex;
};
/* kernel/locking/rtmutex_common.h */
struct rt_mutex_base {
raw_spinlock_t wait_lock; /* 대기자 리스트 보호 */
struct rb_root_cached waiters; /* 우선순위 순 rb-tree */
struct task_struct *owner; /* 현재 보유자 + 플래그 비트 */
};
| 필드 | 타입 | 역할 |
|---|---|---|
wait_lock | raw_spinlock_t | waiters rb-tree와 owner 필드 변경 시 원자성 보호. raw_spinlock_t이므로 PREEMPT_RT에서도 진정한 스핀락(Spinlock)을 사용합니다. |
waiters | rb_root_cached | 대기 중인 태스크를 우선순위 순으로 정렬한 rb-tree. rb_root_cached는 leftmost 노드를 캐싱하여 최고 우선순위 대기자를 O(1)에 접근합니다. |
owner | task_struct * | 현재 락 보유자의 task_struct 포인터. 하위 비트에 플래그를 인코딩합니다 (RT_MUTEX_HAS_WAITERS 등). |
owner 필드 플래그 비트
owner 포인터의 하위 비트는 task_struct의 정렬 보장(최소 4바이트) 덕분에 플래그로 활용됩니다:
/* kernel/locking/rtmutex_common.h */
#define RT_MUTEX_HAS_WAITERS 1UL /* bit 0: 대기자 존재 */
#define RT_MUTEX_OWNER_MASKALL 1UL /* 마스크: 순수 포인터 추출 */
/* owner 포인터에서 실제 task_struct * 추출 */
static inline struct task_struct *rt_mutex_owner(struct rt_mutex_base *lock)
{
unsigned long val = (unsigned long)lock->owner;
return (struct task_struct *)(val & ~RT_MUTEX_OWNER_MASKALL);
}
struct rt_mutex_waiter와 rb-tree
락을 기다리는 각 태스크는 struct rt_mutex_waiter로 표현되어 rb-tree에 삽입됩니다:
/* kernel/locking/rtmutex_common.h */
struct rt_mutex_waiter {
struct rb_node tree_entry; /* rb-tree 노드 */
struct rb_node pi_tree_entry; /* owner의 PI 대기자 트리 */
struct task_struct *task; /* 대기 중인 태스크 */
struct rt_mutex_base *lock; /* 대기 중인 락 */
unsigned int wake_state; /* 웨이크업 상태 */
int prio; /* 대기 시점의 우선순위 */
u64 deadline; /* SCHED_DEADLINE용 */
};
rb-tree 정렬 기준
대기자는 (prio, deadline) 쌍으로 정렬됩니다. 우선순위 값이 작을수록 높은 우선순위이므로, leftmost 노드가 항상 가장 높은 우선순위의 대기자입니다:
/* 정렬 비교 함수 (개념적) */
static bool rt_mutex_waiter_less(struct rt_mutex_waiter *a,
struct rt_mutex_waiter *b)
{
/* 1차: 우선순위 (낮은 값 = 높은 우선순위) */
if (a->prio < b->prio)
return true;
if (a->prio > b->prio)
return false;
/* 2차: SCHED_DEADLINE — 빠른 deadline 우선 */
if (dl_prio(a->prio))
return dl_time_before(a->deadline, b->deadline);
return false;
}
이중 rb-tree 구조
각 대기자는 두 개의 rb-tree에 동시 참여합니다:
| rb-tree | 소속 | 목적 |
|---|---|---|
tree_entry | lock->waiters | 특정 락의 모든 대기자를 우선순위 순으로 관리 |
pi_tree_entry | owner->pi_waiters | 태스크가 보유한 모든 락의 최상위 대기자를 관리 (PI 계산용) |
task_struct의 PI 관련 필드:
/* include/linux/sched.h (관련 필드 발췌) */
struct task_struct {
/* ... */
struct rb_root_cached pi_waiters; /* 이 태스크를 기다리는 최상위 대기자들 */
struct rt_mutex_waiter *pi_blocked_on; /* 이 태스크가 기다리는 waiter */
/* ... */
};
RT Mutex API 레퍼런스
| 함수 | 설명 | 컨텍스트 |
|---|---|---|
rt_mutex_init() | rt_mutex 초기화 | 프로세스 |
rt_mutex_lock() | 락 획득 (슬립(Sleep) 가능, 비인터럽트) | 프로세스 |
rt_mutex_lock_interruptible() | 락 획득 (시그널(Signal)에 의해 중단 가능) | 프로세스 |
rt_mutex_lock_killable() | 락 획득 (치명적 시그널에 의해 중단 가능) | 프로세스 |
rt_mutex_trylock() | 비차단(Non-blocking) 락 시도 | 프로세스 |
rt_mutex_unlock() | 락 해제 | 프로세스 |
rt_mutex_is_locked() | 락 상태 조회 | 모두 |
/* 사용 예시 */
#include <linux/rtmutex.h>
static DEFINE_RT_MUTEX(my_rt_lock);
void my_function(void)
{
rt_mutex_lock(&my_rt_lock);
/* 임계 영역 — PI에 의해 보호됨 */
rt_mutex_unlock(&my_rt_lock);
}
/* 동적 초기화 */
struct rt_mutex dynamic_lock;
rt_mutex_init(&dynamic_lock);
raw_spinlock_t를 사용해야 합니다.
rt_mutex_trylock()만 유일하게 인터럽트 컨텍스트에서 사용 가능합니다 (실패 시 즉시 반환).
rt_mutex_lock() 구현 분석
rt_mutex_lock()의 내부 경로는 fast path와 slow path로 나뉩니다:
/* kernel/locking/rtmutex_api.c */
void __sched rt_mutex_lock(struct rt_mutex *lock)
{
might_sleep();
mutex_acquire(&lock->dep_map, 0, 0, _RET_IP_);
__rt_mutex_lock(&lock->rtmutex, TASK_UNINTERRUPTIBLE);
}
/* kernel/locking/rtmutex.c */
static int __sched __rt_mutex_lock(struct rt_mutex_base *lock,
unsigned int state)
{
if (likely(rt_mutex_cmpxchg_acquire(lock, NULL, current)))
return 0; /* Fast path: CAS로 즉시 획득 */
return rt_mutex_slowlock(lock, NULL, state);
}
Fast Path
락이 비어있으면(owner == NULL) cmpxchg로 현재 태스크를 owner에 원자적(Atomic)으로 설정합니다. 경합(Contention)이 없는 경우 이 경로만 실행되므로 오버헤드(Overhead)가 매우 낮습니다.
Slow Path 개요
/* kernel/locking/rtmutex.c — slowlock 핵심 흐름 */
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;
rt_mutex_init_waiter(&waiter);
raw_spin_lock_irq(&lock->wait_lock);
/* 1. waiter를 lock->waiters rb-tree에 삽입 */
ret = task_blocks_on_rt_mutex(lock, &waiter, current, ww_ctx, state);
if (likely(!ret))
/* 2. 슬립 루프: 락을 획득할 때까지 반복 */
ret = rt_mutex_slowlock_block(lock, ww_ctx, state, NULL, &waiter);
/* 3. 정리: waiter 제거 또는 락 획득 완료 */
if (unlikely(ret))
remove_waiter(lock, &waiter);
raw_spin_unlock_irq(&lock->wait_lock);
return ret;
}
Slow Path: PI Chain 워크
task_blocks_on_rt_mutex()는 slow path의 핵심으로, 대기자 등록과 PI 부스팅을 수행합니다:
/* kernel/locking/rtmutex.c (핵심 로직 요약) */
static int task_blocks_on_rt_mutex(struct rt_mutex_base *lock,
struct rt_mutex_waiter *waiter,
struct task_struct *task, ...)
{
struct task_struct *owner;
struct rt_mutex_waiter *top_waiter;
int chain_walk = 0;
/* 1. waiter 초기화: 우선순위, deadline 설정 */
waiter->task = task;
waiter->lock = lock;
waiter->prio = task->prio;
waiter->deadline = task->dl.deadline;
/* 2. lock->waiters rb-tree에 삽입 */
rt_mutex_enqueue(lock, waiter);
/* 3. 현재 태스크의 pi_blocked_on 설정 */
task->pi_blocked_on = waiter;
owner = rt_mutex_owner(lock);
/* 4. top waiter가 변경되었는지 확인 */
top_waiter = rt_mutex_top_waiter(lock);
if (waiter == top_waiter) {
/* 새 대기자가 최고 우선순위 → owner의 pi_waiters 갱신 */
rt_mutex_enqueue_pi(owner, waiter);
rt_mutex_dequeue_pi(owner, /* previous top */);
rt_mutex_adjust_prio(owner);
chain_walk = 1; /* PI chain 전파 필요 */
}
if (chain_walk)
rt_mutex_adjust_prio_chain(owner, ...);
return 0;
}
PI Chain 전파 알고리즘
rt_mutex_adjust_prio_chain()은 중첩된 락 보유 관계를 따라 우선순위를 연쇄적으로 전파합니다. 이 함수는 RT Mutex 구현의 가장 복잡한 부분입니다:
/* PI Chain 예시:
* T1(p=0) --waits--> Lock_A --held_by--> T2(p=10)
* T2(p=10) --waits--> Lock_B --held_by--> T3(p=20)
*
* T1이 Lock_A를 요청하면:
* 1. T2를 prio=0으로 부스팅
* 2. T2가 Lock_B를 기다리고 있으므로
* 3. T3를 prio=0으로 부스팅
* → 체인 끝까지 전파
*/
/* kernel/locking/rtmutex.c (핵심 로직 요약) */
static int rt_mutex_adjust_prio_chain(
struct task_struct *task,
enum rtmutex_chainwalk chwalk,
struct rt_mutex_base *orig_lock,
struct rt_mutex_base *next_lock,
struct rt_mutex_waiter *orig_waiter,
struct task_struct *top_task)
{
struct rt_mutex_waiter *waiter, *top_waiter;
struct rt_mutex_base *lock;
int ret = 0, depth = 0;
again:
/* 데드락 방지: 체인 깊이 제한 */
if (++depth > max_lock_depth) {
/* 순환 감지 또는 과도한 깊이 */
ret = -EDEADLK;
goto out;
}
/* 현재 태스크의 우선순위 조정 */
waiter = task->pi_blocked_on;
if (!waiter)
goto out; /* 체인 끝: 더 이상 기다리는 락이 없음 */
lock = waiter->lock;
/* waiter의 우선순위 갱신 */
if (waiter->prio != task->prio) {
rt_mutex_dequeue(lock, waiter);
waiter->prio = task->prio;
rt_mutex_enqueue(lock, waiter);
}
/* top waiter가 변경되었는지 확인 */
top_waiter = rt_mutex_top_waiter(lock);
if (top_waiter != waiter)
goto out; /* 최고 우선순위가 변하지 않으면 전파 중단 */
/* 다음 owner로 이동하여 체인 계속 */
task = rt_mutex_owner(lock);
rt_mutex_adjust_prio(task);
goto again; /* 다음 단계로 (반복) */
out:
return ret;
}
Priority Boosting 메커니즘
실제 우선순위 변경은 rt_mutex_setprio()를 통해 스케줄러에 반영됩니다:
/* kernel/locking/rtmutex.c */
static void rt_mutex_adjust_prio(struct task_struct *p)
{
struct task_struct *pi_task;
/* pi_waiters에서 최고 우선순위 대기자 확인 */
pi_task = task_top_pi_waiter(p);
/* effective priority 계산:
* max(자신의 normal_prio, pi_waiters의 최고 우선순위) */
rt_mutex_setprio(p, pi_task);
}
/* kernel/sched/core.c */
void rt_mutex_setprio(struct task_struct *p,
struct task_struct *pi_task)
{
int prio;
struct rq *rq;
const struct sched_class *prev_class;
rq = __task_rq_lock(p, ...);
/* 새 effective priority 결정 */
prio = __rt_effective_prio(pi_task, p->normal_prio);
if (prio == p->prio && ...)
goto out; /* 변경 없음 */
/* 스케줄러 클래스 변경이 필요할 수 있음
* (CFS → RT 전환) */
prev_class = p->sched_class;
/* runqueue에서 제거 → 우선순위 변경 → 재삽입 */
dequeue_task(rq, p, ...);
p->prio = prio;
p->sched_class = /* 새 우선순위에 맞는 클래스 */;
enqueue_task(rq, p, ...);
check_preempt_curr(rq, p, ...); /* 선점 확인 */
}
sched_class가 fair_sched_class에서 rt_sched_class로 전환됩니다. 디부스팅 시 원래 클래스로 복원됩니다.
Deboosting: 우선순위 복원
락을 해제할 때 보유자의 우선순위를 적절히 복원하는 것이 deboosting입니다:
/* rt_mutex_slowunlock 핵심 흐름 */
static void rt_mutex_slowunlock(struct rt_mutex_base *lock)
{
struct rt_mutex_waiter *waiter;
raw_spin_lock_irq(&lock->wait_lock);
/* 최고 우선순위 대기자를 깨움 */
waiter = rt_mutex_top_waiter(lock);
/* owner의 pi_waiters에서 제거 */
rt_mutex_dequeue_pi(current, waiter);
/* 우선순위 복원 (deboosting) */
rt_mutex_adjust_prio(current);
/* → pi_waiters가 비어있으면 normal_prio로 복원
* → 다른 락의 대기자가 남아있으면 그 중 최고로 유지 */
/* 새 owner 설정 및 웨이크업 */
mark_wakeup_next_waiter(...);
raw_spin_unlock_irq(&lock->wait_lock);
rt_mutex_postunlock(...); /* wake_up_process() */
}
다중 락 보유 시 Deboosting
태스크가 여러 rt_mutex를 동시에 보유할 때, 하나의 락을 해제해도 다른 락의 대기자 우선순위로 유지될 수 있습니다:
/* 시나리오: Task O가 Lock A, Lock B를 보유 */
/* Lock A 대기: T1(prio=0), T2(prio=5) */
/* Lock B 대기: T3(prio=3) */
O의 effective_prio = min(T1.prio, T3.prio) = 0 /* T1 기준 */
/* O가 Lock A를 해제하면: */
/* → pi_waiters에서 T1 제거 */
/* → pi_waiters 남은 최고: T3(prio=3) */
O의 effective_prio = 3 /* T3 기준으로 유지 */
/* O가 Lock B도 해제하면: */
/* → pi_waiters 비어있음 */
O의 effective_prio = normal_prio /* 원래 우선순위 복원 */
데드락 감지 (PI Chain 순환)
PI chain walk 중 순환이 감지되면 데드락입니다. rt_mutex_adjust_prio_chain()은 두 가지 방법으로 데드락을 감지합니다:
| 감지 방법 | 조건 | 반환값 |
|---|---|---|
| 깊이 초과 | depth > max_lock_depth | -EDEADLK |
| 순환 감지 | chain walk 중 orig_lock을 다시 만남 | -EDEADLK |
/* 데드락 시나리오:
* T1 --waits--> Lock_A --held_by--> T2
* T2 --waits--> Lock_B --held_by--> T1 ← 순환!
*
* T1이 Lock_A를 요청 → chain walk:
* T2 → Lock_B → T1 (orig task!) → EDEADLK
*/
/* max_lock_depth: /proc/sys/kernel/max_lock_depth */
/* 기본값: 1024 */
/* 정당한 중첩 깊이의 상한 — 이를 초과하면 설계 결함 의심 */
rt_mutex_lock()(TASK_UNINTERRUPTIBLE)은 데드락을 감지해도 반환할 수 없으므로, 커널 경고만 출력합니다. 데드락 감지와 복구가 필요한 경우 rt_mutex_lock_interruptible() 또는 rt_mutex_lock_killable()을 사용해야 합니다.
PREEMPT_RT: spinlock_t → rt_mutex 변환
PREEMPT_RT 패치(Patch)의 가장 급진적인 변경은 커널의 거의 모든 spinlock_t를 rt_mutex 기반 sleeping lock으로 변환하는 것입니다:
/* PREEMPT_RT가 비활성화된 일반 커널 */
typedef struct {
arch_spinlock_t raw_lock;
} spinlock_t;
/* PREEMPT_RT가 활성화된 커널 */
typedef struct {
struct rt_mutex_base lock; /* spinlock이 rt_mutex로! */
} spinlock_t;
/* raw_spinlock_t는 변환되지 않음 — 진정한 스핀 */
typedef struct {
arch_spinlock_t raw_lock; /* 그대로 유지 */
} raw_spinlock_t;
API 매핑(Mapping)
| 일반 커널 API | PREEMPT_RT 내부 구현 | 비고 |
|---|---|---|
spin_lock() | rt_mutex_lock() | 슬립 가능, PI 지원 |
spin_lock_irqsave() | rt_mutex_lock() + migrate_disable | IRQ는 비활성화하지 않음 |
spin_lock_bh() | rt_mutex_lock() | softirq도 스레드화 |
raw_spin_lock() | arch_spin_lock() | 진정한 busy-wait 유지 |
raw_spin_lock_irqsave() | arch_spin_lock() + IRQ 비활성화 | 그대로 유지 |
변환의 의미
/* PREEMPT_RT에서의 변환 규모 */
커널 내 spinlock_t 사용처: ~10,000+ 곳
raw_spinlock_t 사용처: ~100곳 (타이머, 스케줄러, 인터럽트 핵심)
/* 변환 효과 */
1. 모든 spinlock 보유자가 PI를 지원
2. spinlock 보유 중 선점 가능 → 지연 시간 감소
3. spinlock 보유 중 슬립 가능 → 코드 제약 완화
4. IRQ 핸들러도 스레드화 → spinlock 경합 시 스레드 스케줄링
/* raw_spinlock_t가 필요한 곳 */
- 스케줄러 runqueue (rq->lock)
- 타이머 인터럽트 핵심 경로
- PI chain walk 자체의 보호 (wait_lock)
- printk, 콘솔 출력 경로
PI Futex: 사용자 공간 연결
사용자 공간의 pthread_mutex에도 PI를 적용하기 위해 Linux는 PI Futex를 제공합니다:
/* 사용자 공간: pthread에서 PI 뮤텍스 사용 */
#include <pthread.h>
pthread_mutex_t lock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&lock, &attr);
/* 이후 pthread_mutex_lock/unlock은 내부적으로
* FUTEX_LOCK_PI / FUTEX_UNLOCK_PI 시스템 콜 사용 */
PI Futex 시스템 콜(System Call)
| 시스템 콜 | 동작 |
|---|---|
FUTEX_LOCK_PI | 사용자 공간 futex 변수를 커널 rt_mutex와 연결하고, PI를 적용하여 락 획득 |
FUTEX_UNLOCK_PI | 커널 rt_mutex를 해제하고, 다음 대기자에게 소유권 전달 |
FUTEX_TRYLOCK_PI | 비차단 PI 락 시도 |
FUTEX_CMP_REQUEUE_PI | condvar 구현을 위한 PI-aware requeue |
/* 커널 내부: PI Futex 구현 구조 */
/* kernel/futex/pi.c */
/* FUTEX_LOCK_PI 핵심 흐름:
* 1. 사용자 공간 futex 변수에서 owner TID 읽기
* 2. TID가 0이면 → fast path: CAS로 현재 TID 설정
* 3. TID가 non-zero → slow path:
* a. futex_hash()로 해시 버킷 찾기
* b. rt_mutex 구조체와 연결 (또는 생성)
* c. rt_mutex_lock()과 동일한 PI chain walk
* d. 커널에서 우선순위 부스팅 수행
*/
/* glibc pthread_mutex 내부:
* PTHREAD_PRIO_INHERIT 설정 시
* __pthread_mutex_lock() → FUTEX_LOCK_PI
* __pthread_mutex_unlock() → FUTEX_UNLOCK_PI */
rt_mutex vs mutex 비교
| 특성 | struct mutex | struct rt_mutex |
|---|---|---|
| Priority Inheritance | 없음 | 있음 (핵심 기능) |
| 대기 구조 | FIFO 연결 리스트 (wait_list) | 우선순위 순 rb-tree |
| Optimistic Spinning | 있음 (MCS/osq_lock) | 없음 (RT에서 의미 없음) |
| HANDOFF 메커니즘 | 있음 (starvation 방지) | 불필요 (우선순위 순 보장) |
| Fast Path | CAS(owner) | CAS(owner) |
| 잠금(Lock) 오버헤드 (비경합) | ~20 cycles | ~25 cycles |
| 잠금 오버헤드 (경합) | spinning + sleep | PI chain walk + sleep |
| 데드락 감지 | lockdep (디버그) | PI chain 순환 감지 (런타임) |
| 사용 시나리오 | 일반 커널 동기화 | RT 시스템, 우선순위 보장 필요 시 |
| 구조체(Struct) 크기 (64비트) | ~32 bytes | ~40 bytes |
mutex가 적합합니다. rt_mutex를 직접 사용하는 경우는 드물며, 주로 PREEMPT_RT 커널이나 PI futex를 통해 간접적으로 사용됩니다. 직접 사용이 필요한 경우는 실시간 제약이 있는 드라이버나 서브시스템에 한정됩니다.
실전 사용 패턴
패턴 1: 기본 PI 보호
/* RT 태스크 간 공유 자원 보호 */
static DEFINE_RT_MUTEX(shared_data_lock);
static struct shared_data data;
/* RT 태스크에서 호출 */
void rt_update_data(int value)
{
rt_mutex_lock(&shared_data_lock);
data.field = value;
data.timestamp = ktime_get();
rt_mutex_unlock(&shared_data_lock);
}
패턴 2: 인터럽트 가능 락
/* 사용자 요청 처리 — 시그널로 취소 가능 */
int rt_process_request(struct request *req)
{
int ret;
ret = rt_mutex_lock_interruptible(&resource_lock);
if (ret == -EINTR)
return -ERESTARTSYS;
if (ret == -EDEADLK) {
pr_warn("deadlock detected!\n");
return -EDEADLK;
}
/* 임계 영역 */
process(req);
rt_mutex_unlock(&resource_lock);
return 0;
}
패턴 3: RT 드라이버에서의 사용
/* 실시간 하드웨어 제어 드라이버 */
struct rt_hw_device {
struct rt_mutex hw_lock; /* 하드웨어 접근 보호 */
raw_spinlock_t irq_lock; /* IRQ 핸들러에서 사용 */
void __iomem *regs;
wait_queue_head_t irq_wq;
};
/* 프로세스 컨텍스트: RT 태스크에서 호출 */
int rt_hw_write(struct rt_hw_device *dev, u32 val)
{
rt_mutex_lock(&dev->hw_lock);
writel(val, dev->regs + DATA_REG);
writel(CMD_START, dev->regs + CMD_REG);
/* IRQ 완료 대기 */
wait_event(dev->irq_wq, hw_done(dev));
rt_mutex_unlock(&dev->hw_lock);
return 0;
}
/* IRQ 핸들러: raw_spinlock 사용 (슬립 불가) */
irqreturn_t rt_hw_irq(int irq, void *data)
{
struct rt_hw_device *dev = data;
unsigned long flags;
raw_spin_lock_irqsave(&dev->irq_lock, flags);
/* IRQ 처리 */
raw_spin_unlock_irqrestore(&dev->irq_lock, flags);
wake_up(&dev->irq_wq);
return IRQ_HANDLED;
}
안티패턴
안티패턴 1: 인터럽트 컨텍스트에서 rt_mutex 사용
/* 절대 하지 말 것 — BUG() 발생 */
irqreturn_t bad_irq_handler(int irq, void *data)
{
rt_mutex_lock(&lock); /* BUG: might_sleep() in IRQ! */
/* ... */
rt_mutex_unlock(&lock);
return IRQ_HANDLED;
}
/* 올바른 접근: raw_spinlock_t 사용 */
irqreturn_t good_irq_handler(int irq, void *data)
{
raw_spin_lock(&raw_lock);
/* ... */
raw_spin_unlock(&raw_lock);
return IRQ_HANDLED;
}
안티패턴 2: 불필요한 중첩 락 (PI chain 비용)
/* 과도한 중첩 — PI chain walk 비용 증가 */
void bad_nested(void)
{
rt_mutex_lock(&lock_a);
rt_mutex_lock(&lock_b);
rt_mutex_lock(&lock_c);
rt_mutex_lock(&lock_d); /* chain depth 4 */
/* ... */
rt_mutex_unlock(&lock_d);
rt_mutex_unlock(&lock_c);
rt_mutex_unlock(&lock_b);
rt_mutex_unlock(&lock_a);
}
/* 개선: 락 세분화 또는 단일 락으로 통합 */
안티패턴 3: 비-RT 태스크에서 불필요한 rt_mutex
/* CFS 태스크 간에는 일반 mutex가 더 효율적 */
/* rt_mutex의 rb-tree 오버헤드가 불필요 */
/* 비효율: */
static DEFINE_RT_MUTEX(cfs_lock); /* PI 불필요한 곳에 사용 */
/* 효율: */
static DEFINE_MUTEX(cfs_lock); /* optimistic spinning 활용 */
디버깅과 /proc/lock_stat
CONFIG_LOCK_STAT
CONFIG_LOCK_STAT=y 활성화 시 /proc/lock_stat에서 rt_mutex 경합 통계(con-bounces, contentions, waittime)를 확인할 수 있습니다. grep rt_lock /proc/lock_stat으로 rt_mutex 관련 항목을 필터링하세요. 필드 설명과 상세 사용법은 lockdep의 /proc/lock_stat 섹션을 참고하세요.
ftrace로 PI chain 추적
# rt_mutex 관련 함수 추적
echo function > /sys/kernel/tracing/current_tracer
echo 'rt_mutex_*' > /sys/kernel/tracing/set_ftrace_filter
echo 'task_blocks_on_rt_mutex' >> /sys/kernel/tracing/set_ftrace_filter
echo 'rt_mutex_adjust_prio_chain' >> /sys/kernel/tracing/set_ftrace_filter
echo 1 > /sys/kernel/tracing/tracing_on
# 추적 결과 확인
cat /sys/kernel/tracing/trace
# PI boosting 이벤트 추적
echo 1 > /sys/kernel/tracing/events/sched/sched_pi_setprio/enable
cat /sys/kernel/tracing/trace_pipe
# 출력 예: sched_pi_setprio: comm=worker pid=1234 oldprio=120 newprio=49
lockdep과의 상호작용
rt_mutex는 lockdep과 완전히 통합되어, 잘못된 순서의 중첩 잠금(ABBA 패턴)을 런타임에 탐지합니다. CONFIG_PROVE_LOCKING=y 활성화 시 커널 로그에 순환 의존성 경고가 출력됩니다. 경고 메시지 형식과 해석 방법은 lockdep — 경고 메시지 해석을 참고하세요.
성능 특성
| 시나리오 | 비용 | 세부 |
|---|---|---|
| 비경합 락/언락 | ~25 cycles | CAS 1회 (fast path) |
| 경합 — 단일 대기자 | ~500–1,000 cycles | rb-tree 삽입 + PI boost + schedule() |
| 경합 — PI chain depth=1 | ~1,000–2,000 cycles | chain walk 1단계 + 스케줄러 호출 |
| 경합 — PI chain depth=N | ~1,000 * N cycles | 각 단계마다 lock/unlock 쌍 + 우선순위 조정 |
| rb-tree 삽입 | O(log n) | n = 대기자 수 |
| top waiter 조회 | O(1) | rb_root_cached의 leftmost |
mutex vs rt_mutex 성능 비교
/* 벤치마크 조건: x86_64, 4 CPU, 10M iterations */
mutex rt_mutex
비경합 (1 thread): 18 ns 22 ns /* +22% */
경합 (2 threads): 145 ns 180 ns /* +24% */
경합 (4 threads): 320 ns 410 ns /* +28% */
/* rt_mutex는 rb-tree 오버헤드로 인해
* 일반 mutex보다 20-30% 느리지만,
* PI 보장이라는 결정론적 이점을 제공합니다.
*
* 실시간 시스템에서는 평균 성능보다
* 최악 지연 시간(worst-case latency)이 중요하므로
* 이 오버헤드는 허용됩니다. */
커널 설정
| CONFIG 옵션 | 설명 | 기본값 |
|---|---|---|
CONFIG_RT_MUTEXES | rt_mutex 지원 활성화 | y (대부분 배포판) |
CONFIG_PREEMPT_RT | PREEMPT_RT 패치 — spinlock_t→rt_mutex 변환 | n |
CONFIG_DEBUG_RT_MUTEXES | rt_mutex 디버그 검사 활성화 | n |
CONFIG_RT_MUTEX_TESTER | rt_mutex 테스트 모듈 | n |
CONFIG_FUTEX | futex 시스템 콜 (PI futex 포함) | y |
CONFIG_FUTEX_PI | PI futex 전용 지원 | y (RT_MUTEXES 의존) |
# PREEMPT_RT 커널 빌드 최소 설정
CONFIG_PREEMPT_RT=y
CONFIG_RT_MUTEXES=y # PREEMPT_RT가 자동 선택
CONFIG_HIGH_RES_TIMERS=y # RT에 필수
CONFIG_NO_HZ_FULL=y # 틱 없는 모드 권장
# 디버그 빌드 추가
CONFIG_DEBUG_RT_MUTEXES=y
CONFIG_PROVE_LOCKING=y
CONFIG_LOCK_STAT=y
# /proc/sys/kernel 런타임 파라미터
max_lock_depth = 1024 # PI chain 최대 깊이
rt_mutex_lock() 소스 심층 분석
rt_mutex_lock()은 fast path와 slow path로 나뉩니다. fast path는 cmpxchg로 owner를 원자적으로 설정하는 단일 연산이며, 경합이 발생하면 slow path인 rt_mutex_slowlock()으로 진입합니다.
/* kernel/locking/rtmutex_api.c */
void __sched rt_mutex_lock(struct rt_mutex *lock)
{
might_sleep();
mutex_acquire(&lock->dep_map, 0, 0, _RET_IP_);
__rt_mutex_lock(&lock->rtmutex, TASK_UNINTERRUPTIBLE);
}
/* kernel/locking/rtmutex.c */
static int __sched __rt_mutex_lock(
struct rt_mutex_base *lock,
unsigned int state)
{
/* Fast path: cmpxchg(owner, NULL, current) */
if (likely(rt_mutex_cmpxchg_acquire(lock, NULL, current)))
return 0;
/* Slow path: 경합 시 */
return rt_mutex_slowlock(lock, NULL, state);
}
rt_mutex_slowlock() 상세
slow path는 wait_lock을 잡고, waiter를 rb-tree에 삽입하고, PI chain을 구축한 뒤 슬립합니다:
/* 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;
rt_mutex_init_waiter(&waiter);
raw_spin_lock_irq(&lock->wait_lock);
/* 1단계: 즉시 획득 시도 */
if (try_to_take_rt_mutex(lock, current, NULL)) {
raw_spin_unlock_irq(&lock->wait_lock);
return 0;
}
/* 2단계: waiter 등록 + PI chain 구축 */
ret = task_blocks_on_rt_mutex(lock, &waiter,
current, ww_ctx,
RT_MUTEX_FULL_CHAINWALK);
if (likely(!ret))
rt_mutex_slowlock_block(lock, ww_ctx,
state, NULL, &waiter);
/* 3단계: 깨어난 후 정리 */
if (unlikely(ret)) {
__set_current_state(TASK_RUNNING);
remove_waiter(lock, &waiter);
rt_mutex_handle_deadlock(ret, ...);
}
raw_spin_unlock_irq(&lock->wait_lock);
debug_rt_mutex_free_waiter(&waiter);
return ret;
}
task_blocks_on_rt_mutex() 분석
이 함수는 현재 태스크를 대기자로 등록하고 PI 관계를 설정하는 핵심입니다:
/* kernel/locking/rtmutex.c */
static int task_blocks_on_rt_mutex(
struct rt_mutex_base *lock,
struct rt_mutex_waiter *waiter,
struct task_struct *task,
struct ww_acquire_ctx *ww_ctx,
enum rtmutex_chainwalk chwalk)
{
struct task_struct *owner;
struct rt_mutex_waiter *top_waiter;
int chain_walk = 0;
/* waiter 초기화: 현재 태스크의 우선순위 설정 */
waiter->task = task;
waiter->lock = lock;
waiter_update_prio(waiter, task);
/* rb-tree에 삽입 (우선순위 순) */
rt_mutex_enqueue(lock, waiter);
/* 현재 태스크의 pi_blocked_on 설정 */
task->pi_blocked_on = waiter;
owner = rt_mutex_owner(lock);
/* 새 대기자가 top waiter가 되었으면 PI 갱신 필요 */
top_waiter = rt_mutex_top_waiter(lock);
if (waiter == top_waiter) {
rt_mutex_dequeue_pi(owner, top_waiter);
rt_mutex_enqueue_pi(owner, waiter);
rt_mutex_adjust_prio(owner);
chain_walk = 1;
}
/* PI chain walk 필요 시 수행 */
if (chain_walk)
return rt_mutex_adjust_prio_chain(
owner, chwalk, lock, NULL,
waiter, task);
return 0;
}
try_to_take_rt_mutex() 상세
try_to_take_rt_mutex()는 락이 해제되었거나 현재 태스크가 top waiter일 때 소유권을 탈취합니다:
/* 핵심 판단 로직 */
static int try_to_take_rt_mutex(
struct rt_mutex_base *lock,
struct task_struct *task,
struct rt_mutex_waiter *waiter)
{
/* Case 1: owner가 NULL → 바로 획득 */
if (!rt_mutex_owner(lock)) {
goto takeit;
}
/* Case 2: 현재 태스크가 top waiter + owner가 방금 해제 */
if (waiter == rt_mutex_top_waiter(lock)) {
/* owner가 unlock 과정에서 HAS_WAITERS만 남긴 상태 */
rt_mutex_dequeue(lock, waiter);
goto takeit;
}
return 0; /* 획득 실패 */
takeit:
/* PI 관계 정리 후 owner 설정 */
rt_mutex_set_owner(lock, task);
return 1;
}
rt_mutex_slowlock_block()은 내부에서 schedule() 루프를 돌며 대기합니다. 깨어날 때마다 try_to_take_rt_mutex()를 다시 시도하며, 성공할 때까지 반복합니다. 이는 spurious wakeup에 대한 방어입니다.
wait_lock은 raw_spinlock_t입니다. PREEMPT_RT에서도 실제 스핀락으로 동작하므로, rt_mutex_slowlock() 내부에서 wait_lock을 잡고 있는 시간은 최소화해야 합니다. schedule() 호출 전에 반드시 해제합니다.
PI Chain Walk 소스 분석
rt_mutex_adjust_prio_chain()은 RT Mutex 구현에서 가장 복잡한 함수입니다. 중첩된 락 보유 관계를 재귀 없이 반복문으로 순회하면서 우선순위를 전파하고, 동시에 데드락을 감지합니다.
전체 흐름 상세 분석
/* kernel/locking/rtmutex.c — 전체 흐름 */
static int rt_mutex_adjust_prio_chain(
struct task_struct *task,
enum rtmutex_chainwalk chwalk,
struct rt_mutex_base *orig_lock,
struct rt_mutex_base *next_lock,
struct rt_mutex_waiter *orig_waiter,
struct task_struct *top_task)
{
struct rt_mutex_waiter *waiter, *top_waiter, *prereload_top_waiter;
struct rt_mutex_base *lock;
int ret = 0, depth = 0;
bool detect_deadlock;
detect_deadlock = (chwalk == RT_MUTEX_FULL_CHAINWALK);
again:
/*
* max_lock_depth 검사 — 과도한 깊이 → -EDEADLK
* 기본값 1024, /proc/sys/kernel/max_lock_depth로 조정
*/
if (++depth > max_lock_depth) {
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_nr(task));
}
put_task_struct(task);
return -EDEADLK;
}
/*
* 락 안 잡은 상태에서 task 참조 — 경쟁 조건 처리
* task가 중간에 상태 변경할 수 있으므로 retry 필요
*/
raw_spin_lock_irq(&task->pi_lock);
/* 태스크가 더 이상 대기하지 않으면 종료 */
waiter = task->pi_blocked_on;
if (!waiter)
goto out_unlock_pi;
/* 데드락 감지: 자기 자신이 원래 lock에 도달 */
if (detect_deadlock && waiter->lock == orig_lock) {
ret = -EDEADLK;
goto out_unlock_pi;
}
lock = waiter->lock;
raw_spin_lock(&lock->wait_lock);
/* waiter 위치 재조정 (우선순위 변경 반영) */
prereload_top_waiter = rt_mutex_top_waiter(lock);
rt_mutex_dequeue(lock, waiter);
waiter_update_prio(waiter, task);
rt_mutex_enqueue(lock, waiter);
/* top waiter 변경 여부 확인 */
top_waiter = rt_mutex_top_waiter(lock);
/* top waiter 불변이면 전파 중단 (최적화) */
if (top_waiter != waiter &&
top_waiter == prereload_top_waiter) {
if (!detect_deadlock)
goto out_unlock;
}
/* 다음 owner로 이동 */
task = rt_mutex_owner(lock);
get_task_struct(task); /* 참조 카운트 증가 */
/* owner의 pi_waiters 갱신 */
raw_spin_lock(&task->pi_lock);
rt_mutex_dequeue_pi(task, prereload_top_waiter);
rt_mutex_enqueue_pi(task, top_waiter);
rt_mutex_adjust_prio(task);
raw_spin_unlock(&task->pi_lock);
raw_spin_unlock(&lock->wait_lock);
/* 다음 단계 — 반복문으로 재귀 대체 */
goto again;
out_unlock:
raw_spin_unlock(&lock->wait_lock);
out_unlock_pi:
raw_spin_unlock_irq(&task->pi_lock);
put_task_struct(task);
return ret;
}
락 순서 규칙
PI chain walk 중 여러 락을 동시에 잡아야 하므로 엄격한 순서가 필수입니다:
| 순서 | 락 | 보호 대상 |
|---|---|---|
| 1 | task->pi_lock | pi_waiters, pi_blocked_on |
| 2 | lock->wait_lock | waiters rb-tree, owner |
| 3 | next_task->pi_lock | 다음 owner의 PI 정보 |
task->pi_lock을 해제하고 lock->wait_lock을 잡는 구간에서 경쟁 조건(Race Condition)이 발생할 수 있습니다. 이를 위해 커널은 get_task_struct()/put_task_struct()로 태스크 참조를 보호하고, 재시작(Reboot) 로직을 포함합니다.
Chain Walk 모드
enum rtmutex_chainwalk {
RT_MUTEX_MIN_CHAINWALK, /* top_waiter 불변 시 즉시 중단 */
RT_MUTEX_FULL_CHAINWALK, /* 데드락 감지 — 끝까지 순회 */
};
MIN_CHAINWALK는 deboost 경로에서 사용되며, top waiter가 변하지 않으면 전파를 조기 중단합니다. FULL_CHAINWALK는 새 대기자 등록 시 사용되어 순환(데드락)을 완전히 검사합니다.
lock->wait_lock을 잡기 전에 task->pi_lock을 먼저 잡는 순서를 반드시 지킵니다. 이 순서가 깨지면 AB-BA 데드락이 발생합니다. lockdep이 이 순서를 검증합니다.
SCHED_DEADLINE과 RT Mutex 연동
SCHED_DEADLINE 태스크는 고정 우선순위가 아닌 절대 데드라인(absolute deadline) 기반으로 스케줄링됩니다. 이러한 태스크가 rt_mutex를 사용할 때 PI 메커니즘은 특별한 처리가 필요합니다.
DL 태스크의 PI 기본 원리
SCHED_DEADLINE에서는 "우선순위"가 절대 데드라인의 임박도로 결정됩니다. 가장 가까운 데드라인을 가진 태스크가 가장 높은 우선순위입니다 (Earliest Deadline First):
/* kernel/locking/rtmutex_common.h */
static inline bool rt_mutex_waiter_less(
struct rt_mutex_waiter *left,
struct rt_mutex_waiter *right)
{
/* 1차: 우선순위 비교 (DL > RT > CFS) */
if (left->prio < right->prio)
return 1;
if (left->prio > right->prio)
return 0;
/* 2차: 같은 우선순위면 deadline 비교 (DL task) */
if (dl_prio(left->prio))
return dl_time_before(left->deadline,
right->deadline);
return 0;
}
/* SCHED_DEADLINE의 prio 값 */
#define MAX_DL_PRIO 0 /* DL 태스크는 항상 최고 prio 0 */
데드라인 상속 메커니즘
DL 태스크가 rt_mutex에서 블록되면, 락 보유자에게 데드라인 정보까지 전파됩니다:
/* kernel/sched/core.c — rt_mutex_setprio() 내부 */
void rt_mutex_setprio(struct task_struct *p,
struct task_struct *pi_task)
{
struct sched_dl_entity *dl_se;
/* DL 태스크로부터 PI가 온 경우 */
if (dl_prio(prio)) {
/* 부스팅 대상이 원래 DL이 아니어도
* dl_sched_class로 전환 */
p->sched_class = &dl_sched_class;
/* PI 소스의 deadline 정보를 상속 */
dl_se = &p->dl;
dl_se->dl_boosted = 1;
dl_se->dl_deadline = pi_task->dl.dl_deadline;
dl_se->dl_period = pi_task->dl.dl_period;
}
}
| 시나리오 | 대기자 스케줄링 | 보유자 변경 | PI 전파 |
|---|---|---|---|
| DL(50ms) → CFS 보유자 | SCHED_DEADLINE | CFS → DL 클래스 전환 + 데드라인 상속 | deadline 값 복사 |
| DL(50ms) → RT(prio=0) 보유자 | SCHED_DEADLINE | RT → DL 클래스 전환 | deadline 값 복사 |
| DL(100ms) → DL(50ms) 보유자 | SCHED_DEADLINE | 보유자가 이미 더 임박한 DL | 전파 불필요 |
| RT(prio=0) → DL 보유자 | SCHED_FIFO | DL이 이미 최고 → 변경 없음 | 전파 불필요 |
대역폭(Bandwidth) 상속 (Bandwidth Inheritance)
SCHED_DEADLINE은 대역폭 제어(admission control)를 수행합니다. PI로 인한 DL 부스팅은 대역폭 검사를 우회합니다:
/* PI 부스팅된 DL 태스크의 대역폭 처리 */
if (dl_se->dl_boosted) {
/*
* 부스팅된 태스크는 admission control 대상이 아님.
* 원래 태스크의 대역폭 예약을 사용하지 않고,
* PI 소스 태스크의 대역폭에 의존.
*
* 이는 부스팅 해제 시 원래 스케줄링 파라미터로
* 복원되므로 대역폭 초과가 일시적임.
*/
enqueue_dl_entity(dl_se, flags);
return;
}
dl_prio()는 prio < MAX_DL_PRIO를 검사합니다. SCHED_DEADLINE 태스크는 항상 prio=0이므로 RT 태스크(prio=0~99)와 같은 prio 값을 가질 수 있습니다. 이 경우 rt_mutex_waiter_less()에서 deadline 값으로 추가 비교합니다.
futex2/futex_waitv와 RT Mutex
PI Futex는 사용자 공간 뮤텍스에 커널의 Priority Inheritance를 연결하는 다리입니다. Linux 5.16에서 도입된 futex_waitv() 시스템 콜은 여러 futex를 동시에 대기할 수 있으며, PI 시맨틱과의 결합에 새로운 고려사항이 있습니다.
FUTEX_LOCK_PI 내부
/* kernel/futex/pi.c — futex_lock_pi() 핵심 흐름 */
int futex_lock_pi(u32 __user *uaddr,
unsigned int flags,
ktime_t *time, int trylock)
{
struct hrtimer_sleeper timeout;
struct futex_pi_state *pi_state;
struct futex_hash_bucket *hb;
struct futex_q q = futex_q_init;
u32 uval;
int ret;
retry:
/* 1단계: 사용자 공간 futex 값 읽기 */
ret = get_futex_value_locked(&uval, uaddr);
/* 2단계: uncontended (값 == 0) → cmpxchg로 TID 설정 */
if (!(uval & FUTEX_TID_MASK)) {
/* 경합 없음: user-space cmpxchg */
ret = futex_lock_pi_atomic(uaddr, hb, &q.key,
&pi_state, current,
&exiting, 0);
if (ret == 1) /* 성공 */
return 0;
}
/* 3단계: contended → futex_pi_state 연결 */
/*
* futex_pi_state는 사용자 공간 futex와
* 커널 rt_mutex를 연결하는 중간 객체:
* user futex (uaddr) ←→ pi_state ←→ rt_mutex
*/
/* FUTEX_WAITERS 비트 설정 */
uval |= FUTEX_WAITERS;
cmpxchg_futex_value_locked(uaddr, uval, newval);
/* 4단계: 커널 rt_mutex에서 대기 */
ret = rt_mutex_futex_trylock(&q.pi_state->pi_mutex);
if (!ret)
ret = __rt_mutex_slowlock(
&q.pi_state->pi_mutex,
TASK_INTERRUPTIBLE, ...);
return ret;
}
futex_pi_state 구조
futex_pi_state는 사용자 공간 futex 주소와 커널 rt_mutex를 연결하는 핵심 중간 객체입니다:
/* kernel/futex/futex.h */
struct futex_pi_state {
struct list_head list; /* owner task의 pi_state_list */
struct rt_mutex_base pi_mutex; /* 커널 rt_mutex — PI 핵심 */
struct task_struct *owner; /* 현재 소유자 태스크 */
refcount_t refcount; /* 참조 카운트 */
union futex_key key; /* futex hash key */
};
/* 연결 관계:
* user-space futex (uaddr)
* → futex_hash_bucket → futex_q → futex_pi_state
* → pi_mutex (rt_mutex_base)
* → waiters rb-tree → PI chain
*
* owner task_struct
* → pi_state_list → futex_pi_state (여러 개 가능)
*/
FUTEX_LOCK_PI2 확장
Linux 5.14에서 도입된 FUTEX_LOCK_PI2는 CLOCK_MONOTONIC 타임아웃을 지원합니다:
/* 기존 FUTEX_LOCK_PI: CLOCK_REALTIME만 지원 */
syscall(SYS_futex, uaddr, FUTEX_LOCK_PI, 0,
&timeout_realtime, NULL, 0);
/* FUTEX_LOCK_PI2: CLOCK_MONOTONIC 지원 */
syscall(SYS_futex, uaddr, FUTEX_LOCK_PI2, 0,
&timeout_monotonic, NULL, 0);
/* futex_waitv — 여러 futex 동시 대기 (Linux 5.16+)
* 주의: futex_waitv는 PI를 직접 지원하지 않음 */
struct futex_waitv waitv[2] = {
{ .uaddr = (unsigned long)futex1,
.val = expected1,
.flags = FUTEX_32 | FUTEX_PRIVATE_FLAG },
{ .uaddr = (unsigned long)futex2,
.val = expected2,
.flags = FUTEX_32 | FUTEX_PRIVATE_FLAG },
};
syscall(SYS_futex_waitv, waitv, 2,
0, &timeout, CLOCK_MONOTONIC);
futex_waitv()는 PI 시맨틱을 지원하지 않습니다. 여러 PI futex를 동시에 대기해야 하는 경우, 각 futex에 대해 별도 스레드(Thread)에서 FUTEX_LOCK_PI를 사용하거나, 설계를 재고해야 합니다.
벤치마크: PI 오버헤드 측정
PI 메커니즘은 정확성을 보장하지만 오버헤드가 있습니다. 다음은 실제 측정 결과와 벤치마크 방법론을 분석합니다.
비경합(Uncontended) 경로 비용
/* rt_mutex vs mutex 비경합 비용 비교
* 측정 환경: x86_64, Intel Xeon, CONFIG_SMP=y
*
* 핵심 차이: fast path의 cmpxchg는 동일하지만
* rt_mutex는 추가 lockdep 어노테이션이 있음 */
/* mutex: cmpxchg_acquire(owner, 0, current) */
/* 약 15-25ns (L1 캐시 히트 시) */
/* rt_mutex: cmpxchg_acquire(owner, NULL, current)
* + might_sleep() 검사
* + lockdep mutex_acquire()
* 약 20-35ns (L1 캐시 히트 시) */
경합(Contended) 경로 비용
| 연산 | mutex | rt_mutex | 오버헤드 원인 |
|---|---|---|---|
| 비경합 lock | 15-25ns | 20-35ns | lockdep, might_sleep |
| 비경합 unlock | 10-20ns | 15-25ns | HAS_WAITERS 검사 |
| 경합 lock (대기자 1명) | ~1-5us | ~2-8us | PI chain walk (depth=1) |
| 경합 lock (대기자 10명) | ~2-10us | ~5-15us | rb-tree O(log n) 삽입 + PI |
| PI chain depth=3 | N/A | ~10-30us | 3단계 락 순회 + 스케줄러 갱신 |
| PI chain depth=10 | N/A | ~50-100us | 10단계 순회 + 다중 lock 잡기/풀기 |
벤치마크 도구와 방법
# rt_mutex 경합 측정 — cyclictest (rt-tests)
# RT 태스크 간 PI 지연 측정
cyclictest -p 80 -t 4 -n -i 1000 -l 100000 \
--policy=fifo -a 0-3
# pi_stress — PI 전용 스트레스 테스트
# rt-tests 패키지에 포함
pi_stress --duration=60 --groups=4 --rr
# ftrace로 rt_mutex 내부 추적
echo 1 > /sys/kernel/debug/tracing/events/lock/enable
echo "rt_mutex_*" > /sys/kernel/debug/tracing/set_ftrace_filter
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 실행 후 결과 확인
cat /sys/kernel/debug/tracing/trace
lock_stat으로 경합 분석
# CONFIG_LOCK_STAT=y 빌드 필요
echo 0 > /proc/lock_stat # 통계 초기화
# 워크로드 실행 후 결과 확인
cat /proc/lock_stat | head -50
# 출력 예시:
# class name con-bounces contentions waittime-min ...
# &lock->wait_lock: 15234 15234 0.42 125.30 1847.20
# rt_mutex_slowlock+0x4c/0x120 15234 []
# perf로 락 프로파일링
perf lock record -- sleep 10
perf lock report
max_lock_depth의 기본값 1024에 도달하는 경우는 거의 데드락입니다. 실시간 시스템에서는 깊이 3 이하를 설계 목표로 합니다.
메모리 순서: rt_mutex 배리어
rt_mutex는 락 시맨틱을 보장하기 위해 정확한 메모리 순서(memory ordering)를 구현합니다. 특히 owner 필드의 읽기/쓰기에서 acquire/release 시맨틱이 핵심입니다.
Fast Path: cmpxchg_acquire
/* kernel/locking/rtmutex.c */
/* Lock 획득: acquire 시맨틱
* - cmpxchg 이후의 모든 메모리 접근이
* 이 연산 이후로 순서가 보장됨
* - 임계 영역의 코드가 락 획득 전으로
* 재배치되지 않음 */
static inline bool rt_mutex_cmpxchg_acquire(
struct rt_mutex_base *lock,
struct task_struct *old,
struct task_struct *new)
{
return try_cmpxchg_acquire(&lock->owner, &old, new);
/*
* ARM64: CASA (Compare-And-Swap with Acquire)
* 또는 LDAXR + STXR 루프
* x86: LOCK CMPXCHG (full barrier이므로 acquire 포함)
* RISC-V: LR.W.AQ + SC.W
*/
}
/* Unlock: release 시맨틱
* - 이 연산 이전의 모든 메모리 접근이
* 이 연산 전에 완료됨
* - 임계 영역의 코드가 락 해제 후로
* 재배치되지 않음 */
static inline bool rt_mutex_cmpxchg_release(
struct rt_mutex_base *lock,
struct task_struct *old,
struct task_struct *new)
{
return try_cmpxchg_release(&lock->owner, &old, new);
/*
* ARM64: CASL (Compare-And-Swap with Release)
* 또는 STLXR
* x86: LOCK CMPXCHG (full barrier)
* RISC-V: LR.W + SC.W.RL
*/
}
Slow Path 배리어
/* owner 설정: slow path에서는 wait_lock 보호 하에 실행
* wait_lock이 이미 배리어 역할을 하므로 relaxed 가능한 부분도 있지만,
* 커널은 보수적으로 release 시맨틱을 사용 */
/* rt_mutex_set_owner — slow path에서 owner 설정 */
static inline void rt_mutex_set_owner(
struct rt_mutex_base *lock,
struct task_struct *owner)
{
unsigned long val = (unsigned long)owner;
if (rt_mutex_has_waiters(lock))
val |= RT_MUTEX_HAS_WAITERS;
/* WRITE_ONCE: 단일 store 보장
* wait_lock 해제 시 release 배리어가 전파 */
WRITE_ONCE(lock->owner, (struct task_struct *)val);
}
/* rt_mutex_owner 읽기 — 락 없이도 호출 가능 */
static inline struct task_struct *rt_mutex_owner(
struct rt_mutex_base *lock)
{
unsigned long val = (unsigned long)READ_ONCE(lock->owner);
return (struct task_struct *)(val & ~RT_MUTEX_OWNER_MASKALL);
}
rb-tree 연산의 배리어
rb-tree 삽입/삭제는 wait_lock(raw_spinlock_t) 보호 하에 이루어지므로, 별도의 배리어가 불필요합니다:
| 연산 | 배리어 | 이유 |
|---|---|---|
| fast path lock | cmpxchg_acquire | 임계 영역 진입 시점 보장 |
| fast path unlock | cmpxchg_release | 임계 영역 종료 시점 보장 |
| slow path lock | raw_spin_lock_irq (implicit) | wait_lock이 full barrier 제공 |
| slow path unlock | raw_spin_unlock_irq (implicit) | wait_lock 해제 시 release |
| owner 읽기 | READ_ONCE | torn read 방지, 순서 보장(Ordering) 없음 |
| owner 쓰기 | WRITE_ONCE | torn write 방지 |
| waiter prio 갱신 | wait_lock 내부 | rb-tree 조작은 항상 락 하에 |
| pi_waiters 갱신 | pi_lock 내부 | 태스크의 PI 정보 보호 |
LOCK CMPXCHG는 full memory barrier이므로 acquire/release 구분이 성능에 영향을 주지 않습니다. ARM64에서는 LDAXR(acquire)/ STLXR(release)로 acquire와 release가 분리되어 불필요한 배리어를 피합니다.
rt_mutex_owner()가 반환하는 값은 READ_ONCE로만 보호됩니다. 반환 시점에 owner가 이미 변경되었을 수 있습니다. 정확한 owner 확인이 필요하면 반드시 wait_lock을 잡고 확인해야 합니다.
PREEMPT_RT 심층: spinlock_t→rt_mutex
PREEMPT_RT 커널에서 가장 근본적인 변환은 spinlock_t를 rt_mutex 기반의 sleeping lock으로 바꾸는 것입니다. 이 절에서는 이 변환의 내부 메커니즘을 심층 분석합니다.
PREEMPT_RT spinlock_t 구조
/* include/linux/spinlock_types.h */
/* 일반 커널 */
#ifndef CONFIG_PREEMPT_RT
typedef struct {
struct raw_spinlock rlock; /* 진정한 스핀 */
} spinlock_t;
#else
/* PREEMPT_RT 커널 */
typedef struct {
struct rt_mutex_base lock; /* sleeping lock! */
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} spinlock_t;
#endif
/* raw_spinlock_t는 항상 진정한 스핀 */
typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
} raw_spinlock_t;
API 매핑
| 일반 커널 API | PREEMPT_RT 구현 | 동작 |
|---|---|---|
spin_lock() | rt_spin_lock() | sleeping lock (선점/슬립 가능) |
spin_lock_irq() | rt_spin_lock() | IRQ 비활성화 안 함! |
spin_lock_irqsave() | rt_spin_lock() + migrate_disable | IRQ 비활성화 안 함 |
spin_lock_bh() | rt_spin_lock() + local_bh_disable | softirq는 스레드화 |
raw_spin_lock() | raw_spin_lock() | 진정한 스핀 (변환 안 됨) |
raw_spin_lock_irq() | raw_spin_lock_irq() | 진정한 스핀 + IRQ 비활성화 |
rt_spin_lock() 내부
/* kernel/locking/spinlock_rt.c */
void __sched rt_spin_lock(spinlock_t *lock)
{
struct rt_mutex_base *rtm = &lock->lock;
/* migrate_disable: CPU 마이그레이션 방지
* (per-CPU 데이터 보호를 위해 필요) */
migrate_disable();
/* rt_mutex와 동일한 fast path */
if (likely(rt_mutex_cmpxchg_acquire(rtm, NULL, current)))
return;
/* slow path: 슬립 대기 + PI */
rt_spin_lock_slowlock(rtm);
}
void __sched rt_spin_unlock(spinlock_t *lock)
{
struct rt_mutex_base *rtm = &lock->lock;
if (likely(rt_mutex_cmpxchg_release(rtm, current, NULL))) {
migrate_enable();
return;
}
rt_spin_lock_slowunlock(rtm);
migrate_enable();
}
local_lock: per-CPU 데이터 보호
PREEMPT_RT에서 local_lock은 spinlock_t(→ rt_mutex)로 변환되어 per-CPU 데이터를 보호합니다:
/* include/linux/local_lock_internal.h */
#ifdef CONFIG_PREEMPT_RT
typedef struct {
spinlock_t lock; /* rt_mutex 기반 */
struct task_struct *owner;
int nestcnt; /* 중첩 카운트 */
} local_lock_t;
/* 사용 패턴 */
DEFINE_PER_CPU(struct my_data, mydata);
static DEFINE_PER_CPU(local_lock_t, mydata_lock);
void update_my_data(void)
{
local_lock(&mydata_lock);
/* per-CPU 데이터 접근 — 선점 가능하지만
* migrate_disable로 다른 CPU 이동 방지 */
this_cpu_ptr(&mydata)->counter++;
local_unlock(&mydata_lock);
}
#else
/* 일반 커널에서 local_lock은 preempt_disable과 동일 */
typedef struct { } local_lock_t;
#endif
raw_spinlock_t 사용 기준
PREEMPT_RT에서도 변환되지 않는 raw_spinlock_t는 다음과 같은 경우에만 사용합니다:
| 사용 위치 | 이유 |
|---|---|
스케줄러 runqueue (rq->__lock) | rt_mutex 내부에서 스케줄러를 호출하므로 순환 의존 방지 |
rt_mutex 내부 (wait_lock) | 자기 자신을 보호하는 데 자신을 사용할 수 없음 |
| 인터럽트 핸들러 (hard IRQ) | PREEMPT_RT에서도 hard IRQ는 비선점적 |
| NMI 핸들러 | 어떤 상황에서도 슬립 불가 |
| 아키텍처 초기화 코드 | 스케줄러 초기화 전에 실행 |
| 타이머(Timer)/hrtimer 인터럽트 | hard IRQ 컨텍스트 |
spin_lock_irqsave()는 실제로 IRQ를 비활성화하지 않습니다. 이는 의도된 동작으로, PREEMPT_RT는 거의 모든 인터럽트를 스레드화(threaded IRQ)하여 인터럽트 핸들러도 선점 가능하게 만듭니다. IRQ 비활성화가 진정으로 필요한 곳은 raw_spin_lock_irqsave()를 사용해야 합니다.
서브시스템: 커널 내부 rt_mutex 사용
rt_mutex는 커널의 여러 서브시스템에서 직접 사용되며, PI Futex를 통해 사용자 공간까지 확장됩니다. 주요 사용처를 분석합니다.
glibc pthread의 PI Mutex
/* glibc nptl — pthread_mutex PI 설정 */
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_t pi_mutex;
pthread_mutex_init(&pi_mutex, &attr);
/* 내부 동작:
* 1. pthread_mutex_lock() → glibc lll_lock
* 2. 경합 시 → futex(FUTEX_LOCK_PI)
* 3. 커널: futex_lock_pi() → rt_mutex 대기
* 4. PI chain walk → 보유자 우선순위 부스팅 */
SunRPC (NFS)
NFS 서버는 RPC 요청 처리에서 rt_mutex를 사용합니다:
/* net/sunrpc/svc.c — RPC 서비스 동기화 */
/*
* NFS 서버 스레드가 특정 클라이언트 요청을 처리할 때
* PI가 필요한 이유:
* - 높은 우선순위 NFS I/O가 낮은 우선순위 스레드가
* 보유한 클라이언트 상태 락에 의해 블록될 수 있음
* - rt_mutex를 사용하면 PI로 inversion 방지
*/
/* PREEMPT_RT 커널에서는 모든 spinlock_t가
* rt_mutex 기반이므로, NFS 내부의 일반 spinlock도
* 자동으로 PI를 지원하게 됨 */
블록 I/O 레이어
/* block/blk-mq.c — 블록 멀티큐 */
/*
* 블록 레이어에서 RT 태스크의 I/O 요청이
* 낮은 우선순위 태스크가 보유한 큐 락에 의해
* 지연될 수 있음.
*
* PREEMPT_RT에서:
* - blk-mq의 spinlock_t → rt_mutex
* - I/O 스케줄러 큐 락 → rt_mutex
* - 요청 할당 세마포어 → PI 지원
*
* 결과: RT 태스크의 I/O가 PI로 보호됨
*/
/* 예시: 블록 디바이스 큐 처리 */
void blk_mq_run_hw_queue(struct blk_mq_hw_ctx *hctx,
bool async)
{
/* PREEMPT_RT에서 이 spinlock은 rt_mutex 기반
* → RT 태스크 I/O 시 PI 적용 */
spin_lock(&hctx->lock);
/* 큐 처리 ... */
spin_unlock(&hctx->lock);
}
타이머와 Softirq
/* PREEMPT_RT에서 softirq 스레드화 */
/*
* 일반 커널: softirq는 인터럽트 컨텍스트에서 실행
* PREEMPT_RT: softirq는 커널 스레드 (ksoftirqd)에서 실행
*
* 결과:
* - softirq 내부 spinlock_t → rt_mutex
* - 타이머 콜백도 선점 가능
* - PI가 softirq 처리에도 적용
*
* 예외: hrtimer는 hard IRQ 컨텍스트에서 실행
* → raw_spinlock_t 사용
*/
/* kernel/softirq.c — PREEMPT_RT softirq 스레드 */
static void run_ksoftirqd(unsigned int cpu)
{
/* 스레드 컨텍스트에서 실행
* → 선점 가능, 슬립 가능
* → 내부 spinlock은 rt_mutex 기반 */
local_irq_disable();
if (local_softirq_pending()) {
__do_softirq();
}
local_irq_enable();
}
rt_mutex를 사용하지 않더라도, 일반 spinlock_t를 사용하는 모든 서브시스템이 자동으로 PI를 지원하게 됩니다. 이것이 PREEMPT_RT의 핵심 가치입니다.
커널 버전별 진화
RT Mutex는 2006년 커널 2.6.18에서 처음 도입된 이후 꾸준히 발전해왔습니다. 주요 마일스톤을 추적합니다.
주요 버전별 변경 사항
| 커널 버전 | 연도 | 변경 사항 | 핵심 커밋 |
|---|---|---|---|
| v2.6.18 | 2006 | rt_mutex 최초 도입 (Ingo Molnar) | PI 프로토콜 + PI futex |
| v2.6.22 | 2007 | rb-tree 기반 waiter 관리로 전환 | 기존 plist에서 rb-tree로 교체 |
| v3.0 | 2011 | PI chain walk 최적화 | MIN_CHAINWALK 도입 |
| v3.14 | 2014 | SCHED_DEADLINE PI 지원 | DL 태스크 우선순위 상속 |
| v4.0 | 2015 | PI chain walk 리팩토링 | lockless owner 접근 최적화 |
| v4.7 | 2016 | rt_mutex_waiter 정렬 개선 | rb_root_cached 도입 (leftmost 캐싱) |
| v5.0 | 2019 | ww_mutex RT 지원 | wound-wait + PI 통합 |
| v5.14 | 2021 | FUTEX_LOCK_PI2 도입 | CLOCK_MONOTONIC 타임아웃 지원 |
| v5.15 | 2021 | PREEMPT_RT 최초 mainline 머지 | 20년 개발 끝에 공식 포함 |
| v5.17 | 2022 | rt_mutex_base 분리 | spinlock_t 변환 경량화 |
| v6.0 | 2022 | PI futex exit 처리 개선 | exit_pi_state_list 리팩토링 |
| v6.6 LTS | 2023 | PREEMPT_RT 완전 통합 | config 정리, 문서화 |
| v6.12 | 2024 | RT 성능 최적화 | chain walk 캐시(Cache) 친화성 개선 |
v5.17: rt_mutex_base 분리
v5.17에서 rt_mutex_base가 rt_mutex에서 분리된 것은 중요한 구조적 변화입니다:
/* v5.17 이전: 모든 기능이 rt_mutex에 */
struct rt_mutex {
raw_spinlock_t wait_lock;
struct rb_root_cached waiters;
struct task_struct *owner;
#ifdef CONFIG_DEBUG_RT_MUTEXES
/* 디버그 필드 */
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
/* v5.17 이후: base + wrapper 분리 */
struct rt_mutex_base { /* 핵심만 — 크기 최소화 */
raw_spinlock_t wait_lock;
struct rb_root_cached waiters;
struct task_struct *owner;
};
struct rt_mutex { /* 사용자용 — 디버그 포함 */
struct rt_mutex_base rtmutex;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
/* 이점:
* 1. spinlock_t (PREEMPT_RT)에 rt_mutex_base만 포함
* → sizeof(spinlock_t) 감소
* 2. futex_pi_state에 rt_mutex_base만 포함
* → lockdep 오버헤드 제거
* 3. 커널 내부 사용과 사용자 API 분리 */
실전 디버깅 시나리오
rt_mutex 관련 문제는 주로 Priority Inversion, 데드락, 과도한 PI chain depth로 나타납니다. 각 시나리오별 진단 방법을 상세히 다룹니다.
시나리오 1: Priority Inversion 탐지
RT 태스크의 응답 지연이 비정상적으로 증가하면 priority inversion을 의심합니다:
# 1단계: RT 태스크 상태 확인
ps -eo pid,cls,rtprio,stat,wchan:30,comm | grep -E "FF|RR"
# 출력 예시:
# PID CLS RTPRIO STAT WCHAN COMMAND
# 1234 FF 80 S rt_mutex_slowlock audio_thread
# 5678 TS - R - compilation
# 2단계: 어떤 락에서 블록되었는지 확인
cat /proc/1234/stack
# [] rt_mutex_slowlock+0x4c/0x120
# [] rt_mutex_lock+0x38/0x60
# [] do_something_critical+0x20/0x80
# 3단계: 락 보유자 추적
cat /proc/lock_stat | grep "something_critical"
# 4단계: ftrace로 PI 동작 확인
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_pi_setprio/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 워크로드 실행 후
cat /sys/kernel/debug/tracing/trace | grep sched_pi_setprio
# sched_pi_setprio: comm=low_prio_task pid=5678
# oldprio=120 newprio=19
# → CFS(prio=120)가 RT(prio=19)로 부스팅됨
시나리오 2: Unbounded Priority Inversion 진단
PI가 올바르게 작동하지 않는 경우, 또는 PI가 적용되지 않는 락(일반 mutex, semaphore)이 문제인 경우:
# Unbounded inversion 패턴:
# 1. RT 태스크(prio=80)가 일반 mutex에 블록
# 2. mutex 보유자(CFS)가 중간 우선순위 태스크에 의해 선점
# 3. RT 태스크가 무한정 대기
#
# 진단: 일반 mutex 사용 여부 확인
# lockdep 경고 확인
dmesg | grep -i "priority"
dmesg | grep -i "rt_mutex"
# RT 태스크의 블록 지점이 rt_mutex인지 확인
cat /proc/1234/stack
# rt_mutex_slowlock → PI 적용됨 (정상)
# mutex_lock_slowpath → PI 미적용! (문제)
# 해결: 해당 코드 경로에서 rt_mutex 사용으로 변경
# 또는 PREEMPT_RT 커널 사용 (spinlock_t → rt_mutex 자동 변환)
시나리오 3: lockdep + rt_mutex 진단
# lockdep이 rt_mutex 관련 경고를 출력하는 경우
dmesg | grep -A 20 "BUG: lock"
# 일반적인 lockdep 경고 패턴:
# 1. AB-BA 데드락 감지
# ======================================================
# WARNING: possible circular locking dependency detected
# 6.6.0 #1 Not tainted
# ------------------------------------------------------
# Thread A: rt_mutex_A → rt_mutex_B
# Thread B: rt_mutex_B → rt_mutex_A
# 2. sleep-in-atomic 경고
# BUG: sleeping function called from invalid context
# → raw_spinlock 보호 구간에서 rt_mutex 사용 시
# lockdep annotation 확인
# rt_mutex는 자동으로 lockdep 클래스가 할당됨
# 중첩 사용 시 명시적 어노테이션 필요:
/* lockdep 어노테이션 예시 */
static struct lock_class_key my_rt_mutex_key;
void init_my_rt_mutex(struct rt_mutex *lock)
{
rt_mutex_init(lock);
lockdep_set_class(&lock->dep_map, &my_rt_mutex_key);
}
/* 중첩 락 사용 시 서브클래스 지정 */
rt_mutex_lock_nested(lock, SINGLE_DEPTH_NESTING);
시나리오 4: ftrace로 PI chain 추적
# ftrace PI 관련 이벤트 활성화
cd /sys/kernel/debug/tracing
# PI 관련 이벤트
echo 1 > events/sched/sched_pi_setprio/enable
echo 1 > events/sched/sched_switch/enable
echo 1 > events/sched/sched_wakeup/enable
# rt_mutex 내부 함수 추적
echo "rt_mutex_slowlock rt_mutex_slowunlock" > set_ftrace_filter
echo "rt_mutex_adjust_prio_chain" >> set_ftrace_filter
echo function_graph > current_tracer
# 필터: 특정 PID만
echo 1234 > set_ftrace_pid
echo 1 > tracing_on
# ... 워크로드 실행 ...
echo 0 > tracing_on
# 결과 분석
cat trace
# 출력 예시:
# audio-1234 | 2.340us | rt_mutex_slowlock() {
# audio-1234 | 0.450us | task_blocks_on_rt_mutex();
# audio-1234 | 1.230us | rt_mutex_adjust_prio_chain() {
# audio-1234 | 0.120us | rt_mutex_adjust_prio();
# audio-1234 | 0.080us | rt_mutex_setprio();
# audio-1234 | 1.230us | }
# audio-1234 | | schedule() {
# ...
# audio-1234 | 15.670us | } ← 전체 슬립 시간 포함
시나리오 5: max_lock_depth 초과
# 커널 로그에서 max_lock_depth 경고 확인
dmesg | grep "Maximum lock depth"
# Maximum lock depth 1024 reached task: worker (pid=9876)
# 원인 분석:
# 1. 실제 데드락 (순환 의존)
# 2. 과도하게 깊은 락 중첩 (설계 문제)
# 3. 동적으로 생성되는 락 체인 (드문 경우)
# 현재 설정 확인
cat /proc/sys/kernel/max_lock_depth
# 1024
# 임시 조정 (디버깅용)
echo 2048 > /proc/sys/kernel/max_lock_depth
# lock_stat으로 깊은 체인 확인
cat /proc/lock_stat | sort -k2 -rn | head -20
# 근본 해결: 락 설계 개선
# - 락 순서 정리 (lockdep 활용)
# - 세밀한 락 분할 (coarse-grained → fine-grained)
# - lock-free 알고리즘 검토
CONFIG_DEBUG_RT_MUTEXES=y는 개발/디버깅 전용입니다. 프로덕션에서는 비활성화하세요. 매 lock/unlock마다 추가 검증을 수행하여 상당한 성능 저하를 유발합니다. 마찬가지로 CONFIG_PROVE_LOCKING=y(lockdep)도 프로덕션에서는 비활성화가 권장됩니다.
참고 자료
커널 공식 문서
- RT-mutex implementation design — PI chain 전파, boosting, 설계 문서
- RT-mutex subsystem with PI support — rt_mutex API 레퍼런스
- Lock types and their rules — PREEMPT_RT에서 spinlock→rt_mutex 변환
- Runtime locking correctness validator — rt_mutex 데드락 검증
LWN.net 심층 기사
- A realtime preemption overview (2006) — PREEMPT_RT와 rt_mutex의 관계
- The PREEMPT_RT patchset (2018) — PREEMPT_RT 메인라인 통합과 rt_mutex 역할
- The mutex API (2013) — mutex와 rt_mutex의 비교
- Lockdep: how to read its cryptic output (2013) — PI chain 관련 lockdep 경고 해석
학술 자료 및 외부 참고
- Sha, Rajkumar & Lehoczky — "Priority Inheritance Protocols: An Approach to Real-Time Synchronization" (IEEE Trans. Computers, 1990) — PI 프로토콜 원본 논문
- Linux Foundation Real-Time Wiki — PREEMPT_RT 프로젝트 공식 위키
- Paul McKenney — "Is Parallel Programming Hard?" — 실시간 동기화와 우선순위 역전
- 커널 소스:
kernel/locking/rtmutex.c,kernel/locking/rtmutex_api.c,include/linux/rtmutex.h - PI Futex:
kernel/futex/pi.c— 사용자 공간 PI 동기화 구현
관련 문서
rt_mutex와 관련된 다른 동기화 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.