전체 흐름
hrtimer는 타이머를 CPU별 시계 기준의 큐에 놓는 작업과, 만료된 타이머의 콜백을 실행하는 작업을 나눠서 읽어야 한다. 고해상도 모드에서는 clockevent 인터럽트가 만료 검사를 시작한다. 저해상도 모드의 주기적 검사 경로는 별도다.
- 호출자 →
hrtimer_start_range_ns - 기존 큐 항목 제거와 기준 시각 선택
- CPU별
clock_base.active에 enqueue - 조건에 맞으면 다음 clockevent 재설정
- clockevent IRQ →
hrtimer_interrupt - SOFT 만료 시 softirq를 표시
- HARD 큐에서 만료 타이머 실행
- 다음 만료를 계산해 clockevent 설정; SOFT 콜백은 별도 실행
두 경로는 같은 큐 상태를 공유하지만 등록이 곧바로 콜백을 실행하지 않는다. 고해상도 동작이 비활성인 경우 hrtimer_run_queues의 주기적 검사 경로를 따른다.
소스 코드 위치와 범위
이 글은 Linux v6.18.37의 kernel/time/hrtimer.c에 있는 등록, 만료 처리, 다음 clockevent 계산을 대조한다. 아래 세 발췌는 보관된 해당 버전 원문에서 줄 번호와 내용을 그대로 옮겼다. 발췌 일치는 보관 소스 기준이며 실제 장비의 지연이나 런타임 동작을 측정한 결과는 아니다. 다른 버전의 같은 함수 이름만으로 흐름을 일반화하지 않는다.
- kernel/time/hrtimer.c · 구현과 아래 발췌
- include/linux/hrtimer.h · 구조체와 모드 정의
- kernel/time/timer.c · 일반 타이머와 비교할 때만 참고
대표 코드: 세 경계
1. 등록 내부 · __hrtimer_start_range_ns
kernel/time/hrtimer.c 1240–1273행 · 아래는 줄 생략 없는 연속 발췌입니다.
was_armed = remove_hrtimer(timer, base, true, force_local);
if (mode & HRTIMER_MODE_REL)
tim = ktime_add_safe(tim, __hrtimer_cb_get_time(base->clockid));
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, was_armed);
/*
* If the hrtimer interrupt is running, then it will reevaluate the
* clock bases and reprogram the clock event device.
*/
if (new_base->cpu_base->in_hrtirq)
return false;
if (!force_local) {
/*
* If the current CPU base is online, then the timer is
* never queued on a remote CPU if it would be the first
* expiring timer there.
*/
if (hrtimer_base_is_online(this_cpu_base))
return first;
remove_hrtimer는 이미 대기 중인 항목을 먼저 다룬다. 상대 시각을 절대 시각으로 바꾸고 만료 범위를 설정한 뒤 새 base에 enqueue한다. 이 조각의 반환값은 공개 wrapper가 hrtimer_reprogram을 부를지 결정한다(1306–1328행). 인터럽트가 이미 실행 중이면 그 인터럽트가 다시 계산하므로 이 자리에서 재설정하지 않는다.
2. 만료 검사 · __hrtimer_run_queues
kernel/time/hrtimer.c 1814–1851행 · 아래는 줄 생략 없는 연속 발췌입니다.
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);
}
}
}
활성 clock base를 돌며 base->offset을 적용하고, 각 큐의 첫 타이머가 soft 만료 시각에 이르렀는지 확인한 뒤 __run_hrtimer를 호출한다. 이 함수 자체가 clockevent 장치를 설정하지 않는다.
3. 인터럽트 끝 · 다음 이벤트 결정
kernel/time/hrtimer.c 1900–1924행 · 아래는 줄 생략 없는 연속 발췌입니다.
cpu_base->expires_next = KTIME_MAX;
if (!ktime_before(now, cpu_base->softirq_expires_next)) {
cpu_base->softirq_expires_next = KTIME_MAX;
cpu_base->softirq_activated = 1;
raise_timer_softirq(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 만료는 HRTIMER_SOFTIRQ를 깨우고, HARD 큐는 이 인터럽트에서 실행한다. 그 뒤 다음 만료 시각을 계산하고 tick_program_event로 다음 clockevent를 요청한다. 여기서 보인 코드는 고해상도 모드의 인터럽트 경로다.
함수별 분석
등록 함수와 실행 함수는 연달아 호출되는 단일 체인이 아니다. 콜백의 재시작은 만료 처리 도중 일어날 수 있지만, 일반적인 등록 호출은 큐에 미래 사건을 놓고 돌아온다.
first는 clock base 큐의 새 좌단 여부hrtimer_reprogram; force_local이면 helper에서 강제 재계산; 인터럽트 진행 중이면 그쪽에 위임네 단계는 같은 타이머를 다시 등록할 때의 코드 순서다. 모든 등록이 장치를 다시 프로그램하는 것은 아니다.
| 경로 | 실제 역할 | 확인할 원문 |
|---|---|---|
hrtimer_start_range_ns | 모드 확인·base 잠금·내부 helper 호출·필요할 때 재설정 | 1306–1328행 |
__hrtimer_start_range_ns | 기존 큐 처리, 만료 범위/base 선택, enqueue, 재설정 판단 | 1204–1295행 |
__hrtimer_run_queues | 활성 큐의 만료 조건 확인과 콜백 실행 위임 | 1814–1851행 |
hrtimer_interrupt | HARD 실행, SOFT 만료 알림, 다음 이벤트 계산·요청 | 1878–1924행 |
구조체와 만료 시각
hrtimer_cpu_base는 CPU별 큐와 잠금을 담고, 그 안의 hrtimer_clock_base들이 시계 기준별 활성 큐를 가진다. 개별 hrtimer는 큐 노드와 콜백 함수를 가진다. 필드가 순서대로 함수를 호출하는 관계는 아니다.
hrtimer_cpu_base· CPU별 lock/상태clock_base[]· 시계 기준과 HARD/SOFT 구분clock_base.active· 정렬된 timerqueuehrtimer.node· 큐에 들어가는 타이머
20 ms에 반드시 별도 IRQ가 난다는 뜻이 아니다. 그 사이 다른 타이머의 clockevent가 발생해 hrtimer 만료 검사 경로가 돌면 함께 처리할 수 있다. 실제 실행 시각은 시스템 지연에 따라 더 늦을 수 있다.
숫자는 원리 설명용이다. 등록 시 hrtimer_set_expires_range_ns(1247행)가 범위를 설정하고, 다음 이벤트 계산은 큐에서 hard 만료 시각을 사용한다(497–540행).
| 필드·객체 | 이 글에서 읽을 의미 |
|---|---|
cpu_base->lock | 같은 CPU의 hrtimer 큐 상태를 보호한다. |
clock_base.active | timerqueue; enqueue_hrtimer가 타이머 노드를 넣는다(1064–1082행). |
base->offset | 현재 시각을 해당 clock base의 시계 영역으로 바꿔 만료를 비교한다(1824행). |
| soft / hard 만료 시각 | 콜백 검사 기준과 다음 wakeup 계산 기준을 구분한다(1843행, 519행). |
timer->function | __run_hrtimer가 잠금을 푼 뒤 호출하는 콜백(1762–1786행). |
만료 처리와 콜백의 분기
고해상도 모드에서 clockevent IRQ가 왔을 때 HARD 타이머는 IRQ 경로에서 검사한다. SOFT 타이머의 만료가 감지되면 softirq를 깨우며 실제 SOFT 큐 실행은 hrtimer_run_softirq(1853–1869행) 경로다. 비 RT 설정의 기본 타이머는 HARD이며, PREEMPT_RT에서는 HARD를 명시하지 않은 타이머가 SOFT 만료로 이동한다(1620–1645행).
hrtimer_interrupt__hrtimer_run_queues(..., HRTIMER_ACTIVE_HARD)__run_hrtimer가 콜백 호출- 다음 이벤트 계산·clockevent 요청
- SOFT 만료 감지 시
raise_timer_softirq hrtimer_run_softirq진입__hrtimer_run_queues(..., HRTIMER_ACTIVE_SOFT)- 만료 타이머의 콜백 실행
콜백이 HRTIMER_RESTART를 반환해도 이미 다른 호출이 타이머를 enqueue했다면 중복 등록하지 않는다(1797–1799행). HRTIMER_NORESTART만으로 동시 재등록이 없었다고 단정할 수 없다.
| 관찰 상태 | 코드상 경계 | 잘못된 단정 |
|---|---|---|
| ENQUEUED | enqueue가 timer->state와 timerqueue를 갱신(1079–1082행) | 등록 즉시 콜백이 실행된다 |
콜백 실행 중 (base->running) | 큐에서 제거한 후 잠금을 풀고 콜백 호출(1750–1786행) | 콜백 동안 base lock이 계속 잡혀 있다 |
| SOFT pending | IRQ에서 softirq 표시, 별도 큐 실행(1902–1908행, 1853–1869행) | 모든 콜백이 HARD IRQ에서 실행된다 |
| 다음 이벤트 | hrtimer_update_next_event 뒤 tick_program_event(1910–1924행) | __hrtimer_run_queues가 장치를 직접 설정한다 |
읽을 때 지켜야 할 경계
새 선두 만료가 생기거나 기존 선두를 옮긴 경우 등에서 재설정을 고려한다. 인터럽트 중에는 그 경로가 다음 이벤트를 다시 계산한다(1257–1294행).
콜백 전 base lock을 풀기 때문에 다른 실행 문맥이 같은 타이머를 재등록할 수 있다. 반환값 하나만 보고 최종 큐 상태를 단정하지 않는다(1778–1799행).
HARD/SOFT 모드와 PREEMPT_RT 설정을 나눠 본다. 콜백에서 수면 가능 여부를 타이머 이름만으로 일반화하지 않는다(1306–1320행, 1620–1645행).
hrtimer_try_to_cancel은 실행 중인 타이머에 -1을 반환할 수 있다. hrtimer_cancel의 대기는 동시 재등록까지 금지하는 소유권 규약을 대신하지 않는다. -1 반환 설명은 1332–1370행에서 확인한다.
계측과 검증
tracefs에 실제로 있는 timer 이벤트를 확인한 뒤 필요한 것만 짧게 켠다. 이벤트 하나가 찍혔다고 콜백 완료나 장치 재설정을 단정하지 말고 start, expire entry/exit와 코드상 상태를 대응시킨다.
| 관찰 지점 | 해석 |
|---|---|
timer:hrtimer_start | 등록 시점; debug_activate의 tracepoint(474–477행) |
timer:hrtimer_expire_entry/exit | 콜백 전후(1779행, 1785행) |
hrtimer_interrupt 함수 추적 | 선택적인 function tracer 대상; 같은 이름의 ftrace:hrtimer_interrupt 이벤트가 아니다 |
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는 각각 무엇을 바꾸는가? - 20–25 ms slack 예시에서 soft 만료와 다음 clockevent의 기준은 왜 서로 다른가?
- IRQ가 SOFT 만료를 발견한 시점과 SOFT 콜백이 실제 실행된 시점은 어떻게 구별할 수 있는가?
- 콜백의
HRTIMER_RESTART와 다른 CPU의 동시 enqueue가 만날 때 중복 등록을 어떤 조건이 막는가?