IRQ / Time · Linux v6.6 · source + diagram note

hrtimer

Linux v6.6에서 hrtimer 등록, CPU별 만료 큐, clockevent IRQ와 콜백의 실행 문맥을 소스 기준으로 구분한다.

전체 흐름

hrtimer_start_range_ns는 타이머를 CPU별 큐에 등록하고 돌아온다. 만료 실행은 별도의 진입점이다. 고해상도 모드의 clockevent IRQ에서는 HARD 큐를 실행하고, SOFT 만료는 별도 softirq에 넘긴다.

그림 1. 타이머 등록과 고해상도 만료의 두 진입점
등록 · 재등록
  1. 호출자 → hrtimer_start_range_ns
  2. 기존 큐 항목 제거, REL 변환·slack 설정·CPU base 선택
  3. clock_base.active에 timerqueue 노드 삽입
  4. 필요한 경우만 다음 clockevent 재설정 검토
만료 · 고해상도 모드
  1. clockevent IRQ → hrtimer_interrupt
  2. SOFT 만료 시 HRTIMER_SOFTIRQ 알림
  3. 현재 IRQ에서 HARD 큐 콜백 실행
  4. 다음 이벤트 계산·요청; SOFT 콜백은 softirq에서 실행

두 진입점은 큐 상태를 공유하지만 등록 함수가 만료 콜백을 직접 호출하는 체인은 아니다. 고해상도 모드가 활성화되지 않으면 hrtimer_run_queues의 주기적 검사 경로가 있다(1897–1929행).

소스 코드 위치와 범위

아래 발췌와 행 번호는 보유 중인 Linux v6.6 kernel/time/hrtimer.c 파일을 기준으로 한다. 출처 링크는 공식 v6.6 태그의 해당 파일로 연결한다. 일반 타이머의 kernel/time/timer.c와 hrtimer 구현을 혼동하지 않는다. 세 연속 발췌 89행의 일치는 보관 소스를 기준으로 검증했으며, 실제 장비의 지연이나 런타임 동작을 측정한 결과는 아니다.

대표 코드: 등록, 실행, 재설정

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 이동 조건 때문에 재설정이 달라진다.

그림 2. 등록 중 clockevent 재설정 판단
base 잠금공개 wrapper 1299행
제거 → 만료 범위 → enqueue내부 helper 1244–1261행
새 선두 여부enqueue_hrtimer가 좌단 여부 반환
재설정 검토first이면 wrapper가 hrtimer_reprogram 호출; force_local이면 helper가 재계산

wrapper 호출이 실제 장치 프로그램을 보장하지는 않는다. hrtimer_reprogram은 원격 CPU, 기존보다 늦은 만료, 진행 중인 IRQ 등을 검사하고, 고해상도 비활성 상태에서도 장치 요청이 다르다(810–869행, 657–684행).

진입점역할v6.6 근거
hrtimer_start_range_nsbase 잠금, 내부 등록 helper 호출, 필요한 경우 재설정1283–1305행
enqueue_hrtimerENQUEUED 상태와 hard 만료순 timerqueue 갱신1083–1095행
__hrtimer_run_queues현재 만료된 HARD 또는 SOFT 타이머 검사와 콜백 실행 위임1720–1757행
hrtimer_interruptSOFT 알림, HARD 실행, 다음 clockevent 요청1784–1829행

CPU별 큐와 만료 범위

CPU별 hrtimer_cpu_base 안에 시계 기준·실행 문맥별 hrtimer_clock_base가 있다. 각 clock base의 active timerqueue가 개별 타이머의 node를 보유한다. 객체 포함 관계와 실행 순서를 혼동하면 안 된다. 아래의 소문자 soft/hard 만료 시각은 slack 범위의 두 경계이고, 그림 4의 대문자 HARD/SOFT는 콜백 실행 문맥을 뜻한다.

그림 3. 큐의 포함 관계와 slack의 두 경계
CPU별 hrtimer_cpu_base
clock_base[] · 시계 기준과 HARD/SOFT base
clock_base.active · 만료순 timerqueue
hrtimer.node · 큐에 삽입되는 노드
예시: slack 5 ms
20 mssoft 만료
실행 가능
다른 wakeup에 함께 처리할 수 있는 범위25 mshard 만료
다음 이벤트 계산 기준

20 ms에 별도 IRQ가 반드시 발생하는 것은 아니다. 검사 시각과 시스템 지연에 따라 실제 콜백은 더 늦을 수도 있다.

hrtimer_set_expires_range_ns가 범위를 설정한다(1251행). 다음 clockevent 후보는 큐의 hard 만료값으로 계산하고(526행), 만료 검사는 soft 만료를 사용한다(1749행). 숫자는 설명용이다.

객체·필드읽어야 할 역할
cpu_base->lockCPU별 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행).

그림 4. 고해상도 만료 뒤 실행 문맥의 분기
HARD · 현재 IRQ
  1. hrtimer_interrupt
  2. __hrtimer_run_queues(..., HRTIMER_ACTIVE_HARD)
  3. __run_hrtimer에서 콜백
  4. hrtimer_update_next_event 뒤 tick_program_event
SOFT · 알림 뒤 별도 실행
  1. SOFT 만료 감지 → raise_softirq_irqoff
  2. hrtimer_run_softirq 진입
  3. __hrtimer_run_queues(..., HRTIMER_ACTIVE_SOFT)
  4. 만료 콜백 실행과 SOFT 다음 시각 갱신

SOFT 알림은 SOFT 콜백 완료가 아니다. 콜백이 HRTIMER_RESTART를 반환해도 다른 문맥이 이미 enqueue했다면 다시 넣지 않는다. 콜백은 재시작할 다음 만료값을 자체적으로 갱신해야 한다(1684–1705행).

관찰 상태v6.6 코드 경계피할 단정
ENQUEUEDtimer->state와 timerqueue 갱신(1091–1094행)등록과 콜백 실행은 같은 호출 체인이다
콜백 실행 중base->running 설정 뒤 큐에서 제거하고 잠금 해제(1657–1688행)콜백 내내 base lock이 잡혀 있다
SOFT pendingIRQ에서 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지연 측정 도구이며 개별 타이머의 큐 상태 추적을 대신하지 않는다.
별도 tracefs 사용 환경의 짧은 계측 예시 · 기존 이벤트 설정 복원
(
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 초기화는 무엇을 바꾸는가?