전체 흐름
hrtimer_start_range_ns는 타이머를 CPU별 큐에 등록하고 돌아온다. 만료 실행은 별도의 진입점이다. 고해상도 모드의 clockevent IRQ에서는 HARD 큐를 실행하고, SOFT 만료는 별도 softirq에 넘긴다.
- 호출자 →
hrtimer_start_range_ns - 기존 큐 항목 제거, REL 변환·slack 설정·CPU base 선택
clock_base.active에 timerqueue 노드 삽입- 필요한 경우만 다음 clockevent 재설정 검토
- clockevent IRQ →
hrtimer_interrupt - SOFT 만료 시
HRTIMER_SOFTIRQ알림 - 현재 IRQ에서 HARD 큐 콜백 실행
- 다음 이벤트 계산·요청; SOFT 콜백은 softirq에서 실행
두 진입점은 큐 상태를 공유하지만 등록 함수가 만료 콜백을 직접 호출하는 체인은 아니다. 고해상도 모드가 활성화되지 않으면 hrtimer_run_queues의 주기적 검사 경로가 있다(1897–1929행).
소스 코드 위치와 범위
아래 발췌와 행 번호는 보유 중인 Linux v6.6 kernel/time/hrtimer.c 파일을 기준으로 한다. 출처 링크는 공식 v6.6 태그의 해당 파일로 연결한다. 일반 타이머의 kernel/time/timer.c와 hrtimer 구현을 혼동하지 않는다. 세 연속 발췌 89행의 일치는 보관 소스를 기준으로 검증했으며, 실제 장비의 지연이나 런타임 동작을 측정한 결과는 아니다.
- kernel/time/hrtimer.c · 등록, 큐 실행, clockevent 요청
- include/linux/hrtimer.h · hrtimer 객체와 모드 정의
- kernel/time/timer.c · 일반 타이머와 비교할 때만 참고
대표 코드: 등록, 실행, 재설정
1. 등록 내부 · __hrtimer_start_range_ns
kernel/time/hrtimer.c 1244–1271행 · 함수 내부의 줄 생략 없는 연속 발췌
remove_hrtimer(timer, base, true, force_local);
if (mode & HRTIMER_MODE_REL)
tim = ktime_add_safe(tim, base->get_time());
tim = hrtimer_update_lowres(timer, tim, mode);
hrtimer_set_expires_range_ns(timer, tim, delta_ns);
/* Switch the timer base, if necessary: */
if (!force_local) {
new_base = switch_hrtimer_base(timer, base,
mode & HRTIMER_MODE_PINNED);
} else {
new_base = base;
}
first = enqueue_hrtimer(timer, new_base, mode);
if (!force_local)
return first;
/*
* Timer was forced to stay on the current CPU to avoid
* reprogramming on removal and enqueue. Force reprogram the
* hardware by evaluating the new first expiring timer.
*/
hrtimer_force_reprogram(new_base->cpu_base, 1);
return 0;
기존 큐에서 제거한 뒤 REL 만료를 절대 시각으로 환산하고 slack 범위를 기록한다. CPU base를 고른 뒤 enqueue한다. 반환된 first는 wrapper의 조건부 hrtimer_reprogram으로 이어지고, 기존 선두를 로컬에 고정한 경우에는 hrtimer_force_reprogram을 호출한다(1283–1305행).
2. 활성 큐의 만료 검사 · __hrtimer_run_queues
kernel/time/hrtimer.c 1720–1757행 · 줄 생략 없는 연속 발췌
static void __hrtimer_run_queues(struct hrtimer_cpu_base *cpu_base, ktime_t now,
unsigned long flags, unsigned int active_mask)
{
struct hrtimer_clock_base *base;
unsigned int active = cpu_base->active_bases & active_mask;
for_each_active_base(base, cpu_base, active) {
struct timerqueue_node *node;
ktime_t basenow;
basenow = ktime_add(now, base->offset);
while ((node = timerqueue_getnext(&base->active))) {
struct hrtimer *timer;
timer = container_of(node, struct hrtimer, node);
/*
* The immediate goal for using the softexpires is
* minimizing wakeups, not running timers at the
* earliest interrupt after their soft expiration.
* This allows us to avoid using a Priority Search
* Tree, which can answer a stabbing query for
* overlapping intervals and instead use the simple
* BST we already have.
* We don't add extra wakeups by delaying timers that
* are right-of a not yet expired timer, because that
* timer will have to trigger a wakeup anyway.
*/
if (basenow < hrtimer_get_softexpires_tv64(timer))
break;
__run_hrtimer(cpu_base, base, timer, &basenow, flags);
if (active_mask == HRTIMER_ACTIVE_SOFT)
hrtimer_sync_wait_running(cpu_base, flags);
}
}
}
선택한 HARD 또는 SOFT 활성 base에서 가장 이른 노드를 살피고, base->offset을 반영한 현재 시각이 soft 만료에 이르면 콜백 실행 helper로 넘긴다. 이 함수는 clockevent 장치를 직접 설정하지 않는다.
3. IRQ 끝의 다음 clockevent · hrtimer_interrupt
kernel/time/hrtimer.c 1808–1830행 · 함수 중간의 연속 발췌
if (!ktime_before(now, cpu_base->softirq_expires_next)) {
cpu_base->softirq_expires_next = KTIME_MAX;
cpu_base->softirq_activated = 1;
raise_softirq_irqoff(HRTIMER_SOFTIRQ);
}
__hrtimer_run_queues(cpu_base, now, flags, HRTIMER_ACTIVE_HARD);
/* Reevaluate the clock bases for the [soft] next expiry */
expires_next = hrtimer_update_next_event(cpu_base);
/*
* Store the new expiry value so the migration code can verify
* against it.
*/
cpu_base->expires_next = expires_next;
cpu_base->in_hrtirq = 0;
raw_spin_unlock_irqrestore(&cpu_base->lock, flags);
/* Reprogramming necessary ? */
if (!tick_program_event(expires_next, 0)) {
cpu_base->hang_detected = 0;
return;
}
SOFT 만료는 softirq에 알리고 HARD 큐는 현재 IRQ에서 실행한다. 이후 다음 시각을 계산하고 tick_program_event를 요청한다. 예외·재시도 분기는 발췌 뒤에 이어지므로 이 조각만으로 인터럽트 전체가 성공했다고 단정하지 않는다.
함수별 분석
등록 함수의 큐 삽입, 만료 함수의 콜백 호출, 장치의 다음 이벤트 요청은 서로 다른 경계다. 같은 타이머를 다시 등록할 때에는 큐 선두 여부와 CPU 이동 조건 때문에 재설정이 달라진다.
enqueue_hrtimer가 좌단 여부 반환first이면 wrapper가 hrtimer_reprogram 호출; force_local이면 helper가 재계산wrapper 호출이 실제 장치 프로그램을 보장하지는 않는다. hrtimer_reprogram은 원격 CPU, 기존보다 늦은 만료, 진행 중인 IRQ 등을 검사하고, 고해상도 비활성 상태에서도 장치 요청이 다르다(810–869행, 657–684행).
| 진입점 | 역할 | v6.6 근거 |
|---|---|---|
hrtimer_start_range_ns | base 잠금, 내부 등록 helper 호출, 필요한 경우 재설정 | 1283–1305행 |
enqueue_hrtimer | ENQUEUED 상태와 hard 만료순 timerqueue 갱신 | 1083–1095행 |
__hrtimer_run_queues | 현재 만료된 HARD 또는 SOFT 타이머 검사와 콜백 실행 위임 | 1720–1757행 |
hrtimer_interrupt | SOFT 알림, HARD 실행, 다음 clockevent 요청 | 1784–1829행 |
CPU별 큐와 만료 범위
CPU별 hrtimer_cpu_base 안에 시계 기준·실행 문맥별 hrtimer_clock_base가 있다. 각 clock base의 active timerqueue가 개별 타이머의 node를 보유한다. 객체 포함 관계와 실행 순서를 혼동하면 안 된다. 아래의 소문자 soft/hard 만료 시각은 slack 범위의 두 경계이고, 그림 4의 대문자 HARD/SOFT는 콜백 실행 문맥을 뜻한다.
hrtimer_cpu_baseclock_base[] · 시계 기준과 HARD/SOFT baseclock_base.active · 만료순 timerqueuehrtimer.node · 큐에 삽입되는 노드실행 가능다른 wakeup에 함께 처리할 수 있는 범위25 mshard 만료
다음 이벤트 계산 기준
20 ms에 별도 IRQ가 반드시 발생하는 것은 아니다. 검사 시각과 시스템 지연에 따라 실제 콜백은 더 늦을 수도 있다.
hrtimer_set_expires_range_ns가 범위를 설정한다(1251행). 다음 clockevent 후보는 큐의 hard 만료값으로 계산하고(526행), 만료 검사는 soft 만료를 사용한다(1749행). 숫자는 설명용이다.
| 객체·필드 | 읽어야 할 역할 |
|---|---|
cpu_base->lock | CPU별 hrtimer 큐 상태의 잠금; 콜백 호출 전에는 해제한다(1684행). |
clock_base.active | 정렬된 timerqueue; enqueue_hrtimer가 timer->node를 추가한다(1094행). |
base->offset | 현재 시각을 해당 clock base 기준으로 맞춰 만료를 비교한다(1730행). |
| soft / hard 만료 | 콜백 가능 시각과 다음 wakeup 후보의 기준이 다르다(1749행, 526행). |
timer->function | 잠금 해제 후 __run_hrtimer가 호출하는 콜백(1669행, 1684–1688행). |
HARD IRQ와 SOFT softirq
고해상도 모드에서 hrtimer_interrupt는 HARD 큐를 그 IRQ 안에서 실행한다. SOFT 만료는 HRTIMER_SOFTIRQ를 올리고, hrtimer_run_softirq가 별도로 SOFT 큐를 실행한다. v6.6의 PREEMPT_RT에서는 HARD로 명시하지 않은 타이머를 초기화할 때 SOFT 쪽으로 옮긴다(1551–1558행).
hrtimer_interrupt__hrtimer_run_queues(..., HRTIMER_ACTIVE_HARD)__run_hrtimer에서 콜백hrtimer_update_next_event뒤tick_program_event
- SOFT 만료 감지 →
raise_softirq_irqoff hrtimer_run_softirq진입__hrtimer_run_queues(..., HRTIMER_ACTIVE_SOFT)- 만료 콜백 실행과 SOFT 다음 시각 갱신
SOFT 알림은 SOFT 콜백 완료가 아니다. 콜백이 HRTIMER_RESTART를 반환해도 다른 문맥이 이미 enqueue했다면 다시 넣지 않는다. 콜백은 재시작할 다음 만료값을 자체적으로 갱신해야 한다(1684–1705행).
| 관찰 상태 | v6.6 코드 경계 | 피할 단정 |
|---|---|---|
| ENQUEUED | timer->state와 timerqueue 갱신(1091–1094행) | 등록과 콜백 실행은 같은 호출 체인이다 |
| 콜백 실행 중 | base->running 설정 뒤 큐에서 제거하고 잠금 해제(1657–1688행) | 콜백 내내 base lock이 잡혀 있다 |
| SOFT pending | IRQ에서 softirq 발생, 별도 hrtimer_run_softirq(1808–1812행, 1759–1775행) | 모든 콜백이 HARD IRQ 안에서 실행된다 |
| 다음 clockevent | 다음 이벤트 계산 뒤 tick_program_event(1816–1829행) | __hrtimer_run_queues가 장치를 직접 프로그램한다 |
경계 조건과 취소
새 큐 선두 여부와 기존 선두의 로컬 유지 여부를 나눠 본다. 재설정 helper가 호출돼도 원격 CPU나 비활성 고해상도 모드 등에서는 장치 프로그램을 단정할 수 없다(1261–1271행, 810–869행).
콜백 실행 전 base lock을 풀기 때문에 다른 문맥의 등록이 가능하다. 콜백 반환만으로 최종 큐 상태를 단정할 수 없다(1684–1705행).
상대 CLOCK_REALTIME은 초기화 때 CLOCK_MONOTONIC base로 바뀐다(1565–1570행). HARD/SOFT와 PREEMPT_RT 설정을 구분해 콜백의 실행 문맥을 판단한다.
hrtimer_try_to_cancel은 콜백 실행 중 -1을 반환할 수 있고, hrtimer_cancel은 이를 기다리며 재시도한다(1319–1344행, 1441–1453행). 소유자가 동시에 재등록을 막는 규칙까지 대신해 주지는 않는다.
계측과 검증
timer trace 이벤트를 짧게 켜서 등록과 콜백 진입·종료를 대조한다. hrtimer_interrupt는 필요하면 함수 추적 대상으로 지정한다. ftrace:hrtimer_interrupt라는 이벤트 이름으로 가정하지 않는다.
| 관찰 지점 | 확인할 사실 |
|---|---|
timer:hrtimer_start | 등록 시점; 만료나 콜백 완료 시점은 아니다. |
timer:hrtimer_expire_entry/exit | 콜백 앞뒤를 비교한다(1685행, 1691행). |
hrtimer_interrupt 함수 추적 | 고해상도 IRQ 경로에 진입했는지 본다(1784행). |
cyclictest | 지연 측정 도구이며 개별 타이머의 큐 상태 추적을 대신하지 않는다. |
(
cd /sys/kernel/tracing || exit 1
command -v timeout >/dev/null || exit 1
start=events/timer/hrtimer_start/enable
expire=events/timer/hrtimer_expire_entry/enable
start_before=$(cat "$start") || exit 1
expire_before=$(cat "$expire") || exit 1
restore() {
printf '%s\n' "$start_before" > "$start"
printf '%s\n' "$expire_before" > "$expire"
}
trap restore EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
printf '1\n' > "$start" || exit 1
printf '1\n' > "$expire" || exit 1
timeout 5 cat trace_pipe
)쓰기 권한과 tracefs 마운트가 필요하다. 다른 계측 작업과 같은 tracefs 설정·버퍼를 공유하지 않는 환경에서만 실행한다. trace_pipe는 읽은 레코드를 소비하며, 강제 종료(SIGKILL) 때는 trap도 실행되지 않으므로 저장한 설정을 별도로 복원해야 한다. 이 예시는 장비에서 실행 검증하지 않았다.
코드에서 다시 확인할 질문
- 같은 타이머를 다시 등록할 때
force_local과first는 각각 어떤 재설정 결정을 만드는가? - slack 5 ms 예시에서 soft 만료 검사와 다음 clockevent 후보는 왜 다른 시각을 보는가?
- SOFT 만료 알림과 실제 콜백 진입은 trace에서 어떻게 구분할 수 있는가?
- 상대
CLOCK_REALTIME타이머가 시각 조정의 영향을 받지 않도록 v6.6 초기화는 무엇을 바꾸는가?