IRQ / Time · Linux 6.18.37 LTS · source + diagram note

hrtimer

고정 분석 버전: Linux v6.18.37. high-resolution timer의 등록, 만료 처리와 clockevent 재설정을 구분하는 코드 분석 노트입니다.

전체 흐름

hrtimer는 타이머를 CPU별 시계 기준의 큐에 놓는 작업과, 만료된 타이머의 콜백을 실행하는 작업을 나눠서 읽어야 한다. 고해상도 모드에서는 clockevent 인터럽트가 만료 검사를 시작한다. 저해상도 모드의 주기적 검사 경로는 별도다.

그림 1. 등록과 만료는 서로 다른 진입 경로
① 등록 · 재등록
  1. 호출자 → hrtimer_start_range_ns
  2. 기존 큐 항목 제거와 기준 시각 선택
  3. CPU별 clock_base.active에 enqueue
  4. 조건에 맞으면 다음 clockevent 재설정
② 고해상도 만료
  1. clockevent IRQ → hrtimer_interrupt
  2. SOFT 만료 시 softirq를 표시
  3. HARD 큐에서 만료 타이머 실행
  4. 다음 만료를 계산해 clockevent 설정; SOFT 콜백은 별도 실행

두 경로는 같은 큐 상태를 공유하지만 등록이 곧바로 콜백을 실행하지 않는다. 고해상도 동작이 비활성인 경우 hrtimer_run_queues의 주기적 검사 경로를 따른다.

소스 코드 위치와 범위

이 글은 Linux v6.18.37의 kernel/time/hrtimer.c에 있는 등록, 만료 처리, 다음 clockevent 계산을 대조한다. 아래 세 발췌는 보관된 해당 버전 원문에서 줄 번호와 내용을 그대로 옮겼다. 발췌 일치는 보관 소스 기준이며 실제 장비의 지연이나 런타임 동작을 측정한 결과는 아니다. 다른 버전의 같은 함수 이름만으로 흐름을 일반화하지 않는다.

대표 코드: 세 경계

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를 요청한다. 여기서 보인 코드는 고해상도 모드의 인터럽트 경로다.

함수별 분석

등록 함수와 실행 함수는 연달아 호출되는 단일 체인이 아니다. 콜백의 재시작은 만료 처리 도중 일어날 수 있지만, 일반적인 등록 호출은 큐에 미래 사건을 놓고 돌아온다.

그림 2. 등록 중 재설정이 필요한 경우
base lock공개 wrapper 1322행
remove → expires → enqueue1240–1257행
큐의 선두 판단first는 clock base 큐의 새 좌단 여부
조건부 장치 재설정로컬 온라인·선두이면 wrapper에서 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_interruptHARD 실행, SOFT 만료 알림, 다음 이벤트 계산·요청1878–1924행

구조체와 만료 시각

hrtimer_cpu_base는 CPU별 큐와 잠금을 담고, 그 안의 hrtimer_clock_base들이 시계 기준별 활성 큐를 가진다. 개별 hrtimer는 큐 노드와 콜백 함수를 가진다. 필드가 순서대로 함수를 호출하는 관계는 아니다.

그림 3. CPU별 큐의 포함 관계와 만료 범위
객체 관계
  1. hrtimer_cpu_base · CPU별 lock/상태
  2. clock_base[] · 시계 기준과 HARD/SOFT 구분
  3. clock_base.active · 정렬된 timerqueue
  4. hrtimer.node · 큐에 들어가는 타이머
예시: slack 5 ms
20 mssoft 만료 · 실행 가능합쳐 깨울 수 있는 구간25 mshard 만료 · 요청 기한

20 ms에 반드시 별도 IRQ가 난다는 뜻이 아니다. 그 사이 다른 타이머의 clockevent가 발생해 hrtimer 만료 검사 경로가 돌면 함께 처리할 수 있다. 실제 실행 시각은 시스템 지연에 따라 더 늦을 수 있다.

숫자는 원리 설명용이다. 등록 시 hrtimer_set_expires_range_ns(1247행)가 범위를 설정하고, 다음 이벤트 계산은 큐에서 hard 만료 시각을 사용한다(497–540행).

필드·객체이 글에서 읽을 의미
cpu_base->lock같은 CPU의 hrtimer 큐 상태를 보호한다.
clock_base.activetimerqueue; 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행).

그림 4. 고해상도 IRQ의 HARD/SOFT 갈림길
HARD · 현재 IRQ
  1. hrtimer_interrupt
  2. __hrtimer_run_queues(..., HRTIMER_ACTIVE_HARD)
  3. __run_hrtimer가 콜백 호출
  4. 다음 이벤트 계산·clockevent 요청
SOFT · 별도 softirq
  1. SOFT 만료 감지 시 raise_timer_softirq
  2. hrtimer_run_softirq 진입
  3. __hrtimer_run_queues(..., HRTIMER_ACTIVE_SOFT)
  4. 만료 타이머의 콜백 실행

콜백이 HRTIMER_RESTART를 반환해도 이미 다른 호출이 타이머를 enqueue했다면 중복 등록하지 않는다(1797–1799행). HRTIMER_NORESTART만으로 동시 재등록이 없었다고 단정할 수 없다.

관찰 상태코드상 경계잘못된 단정
ENQUEUEDenqueue가 timer->state와 timerqueue를 갱신(1079–1082행)등록 즉시 콜백이 실행된다
콜백 실행 중 (base->running)큐에서 제거한 후 잠금을 풀고 콜백 호출(1750–1786행)콜백 동안 base lock이 계속 잡혀 있다
SOFT pendingIRQ에서 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지연을 측정하는 도구; 개별 타이머 상태 추적을 대신하지 않는다
별도 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는 각각 무엇을 바꾸는가?
  • 20–25 ms slack 예시에서 soft 만료와 다음 clockevent의 기준은 왜 서로 다른가?
  • IRQ가 SOFT 만료를 발견한 시점과 SOFT 콜백이 실제 실행된 시점은 어떻게 구별할 수 있는가?
  • 콜백의 HRTIMER_RESTART와 다른 CPU의 동시 enqueue가 만날 때 중복 등록을 어떤 조건이 막는가?