Linux v6.6 · 개념과 코드 읽기

hrtimer: 늦게 처리된 주기 타이머의 다음 기한 계산하기

이 코드는 어떤 문제를 푸나요?

주기 타이머가 늦게 실행되었을 때 현재 시각에서 무조건 한 주기를 더하면 원래 주기가 계속 밀릴 수 있습니다. hrtimer_forward는 기존 만료 시각을 기준으로 필요한 주기 수만큼 앞으로 옮깁니다. 콜백을 밀린 횟수만큼 실행하는 함수와는 다릅니다.

읽을 범위: v6.6 · kernel/time/hrtimer.c · hrtimer_forward 1040–1072행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.

먼저 알아둘 개념

만료 시각과 주기

만료 시각은 한 번의 사건이 예정된 시점이고 interval은 사건 사이의 간격입니다. 다음 기한을 계산할 때 어느 시점을 기준으로 더하는지가 주기 안정성에 영향을 줍니다.

overrun 수

여기서 반환하는 수는 기존 기한을 몇 주기 앞으로 옮겼는지를 나타냅니다. 실제 콜백을 그 횟수만큼 호출했다는 의미가 아닙니다.

큐에 들어 있는 타이머

정렬된 타이머 큐에 등록된 상태에서 만료 시각만 임의로 바꾸면 큐 순서와 맞지 않을 수 있습니다. 이 함수는 enqueue 상태를 검사합니다.

처음 읽을 때

기존 기한 10 ms, 주기 5 ms, 현재 23 ms라면 다음 기한은 25 ms가 되어야 합니다. 이 예시는 3주기 전진이며 28 ms로 새 주기를 시작하는 것과 다릅니다.

더 깊이 살펴볼 때

현재 시각이 기존 기한 또는 정확한 다음 주기 경계와 같을 때도 새 기한이 현재보다 뒤가 되는지 계산해 보세요. 큐 재등록은 호출자의 역할입니다.

그림으로 보는 변화

hrtimer: 늦게 처리된 주기 타이머의 다음 기한 계산하기의 단계별 개념 그림
각 단계에 화살표 의미와 생략 범위를 표시했습니다. 주소·숫자 예제는 실제 장치 값을 뜻하지 않습니다.
1단계 설명

1단계 고정

GIF 원본 열기

1. 얼마나 늦었는지 계산

delta = now - 기존 만료 시각

화살표는 시간 계산 순서입니다. 예시는 만료 10 ms, now 23 ms, interval 5 ms입니다.

2. 지나간 주기만큼 전진

13 / 5 = 2, 만료를 20 ms로 변경

정수 나눗셈 결과로 한꺼번에 전진합니다. 콜백을 두 번 실행하는 그림이 아닙니다.

3. 현재보다 뒤의 기한 확보

한 주기 더해 25 ms, 반환 3

화살표는 만료 시각의 이동을 뜻합니다. 타이머 큐에 재등록하는 동작은 별도입니다.

hrtimer_forward를 한 줄씩 읽기

줄 번호는 v6.6 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.

u64 hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval)
{
	u64 orun = 1;
	ktime_t delta;

	delta = ktime_sub(now, hrtimer_get_expires(timer));

	if (delta < 0)
		return 0;

	if (WARN_ON(timer->state & HRTIMER_STATE_ENQUEUED))
		return 0;

	if (interval < hrtimer_resolution)
		interval = hrtimer_resolution;

	if (unlikely(delta >= interval)) {
		s64 incr = ktime_to_ns(interval);

		orun = ktime_divns(delta, incr);
		hrtimer_add_expires_ns(timer, incr * orun);
		if (hrtimer_get_expires_tv64(timer) > now)
			return orun;
		/*
		 * This (and the ktime_add() below) is the
		 * correction for exact:
		 */
		orun++;
	}
	hrtimer_add_expires(timer, interval);

	return orun;
}
u64 hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval)

타이머의 기존 만료 시각을 now보다 뒤로 전진시킵니다. interval은 반복 간격이고 반환값은 전진한 주기 수입니다.

	u64 orun = 1;

이미 만료한 정상 경로에서 적어도 한 주기 전진할 기본값을 둡니다.

	ktime_t delta;

현재 시각과 기존 만료 시각의 차이를 저장합니다.

	delta = ktime_sub(now, hrtimer_get_expires(timer));

now에서 기존 만료 시각을 빼 지연량을 구합니다. 음수이면 아직 기한 전입니다.

	if (delta < 0)

기한이 아직 오지 않았는지 확인합니다.

		return 0;

아직 이르면 상태를 바꾸지 않고 0을 반환합니다.

	if (WARN_ON(timer->state & HRTIMER_STATE_ENQUEUED))

정렬 큐에 등록된 타이머의 기한을 이 방식으로 바꾸려는 잘못된 사용인지 확인하고 경고합니다.

		return 0;

enqueue된 상태라면 기한을 변경하지 않고 반환합니다.

	if (interval < hrtimer_resolution)

요청 간격이 현재 hrtimer 해상도보다 짧은지 확인합니다.

		interval = hrtimer_resolution;

표현 가능한 최소 해상도에 맞춰 interval을 올립니다.

	if (unlikely(delta >= interval)) {

한 주기 이상 밀린 경우 한 번에 여러 주기를 더하는 경로로 들어갑니다. unlikely는 예상 빈도 힌트이며 분기 의미는 바뀌지 않습니다.

		s64 incr = ktime_to_ns(interval);

주기를 나노초 정수로 변환해 나눗셈과 곱셈에 사용합니다.

		orun = ktime_divns(delta, incr);

지연량을 주기로 나눈 정수 몫으로 지나간 주기 수를 구합니다.

		hrtimer_add_expires_ns(timer, incr * orun);

그 수만큼 기존 만료 시각에 더합니다. 현재 now를 새 기준으로 덮어쓰지 않습니다.

		if (hrtimer_get_expires_tv64(timer) > now)

전진한 기한이 이미 now보다 뒤인지 확인합니다. 필요한 보정 여부를 결정합니다.

			return orun;

충분히 앞으로 갔다면 현재 주기 수를 반환합니다.

		orun++;

기한이 아직 now 이하라면 한 주기를 더 전진할 것이므로 반환할 횟수도 하나 늘립니다.

	hrtimer_add_expires(timer, interval);

한 주기를 더해 최종 만료 시각을 now보다 뒤로 옮깁니다. 지연이 한 주기 미만인 경우에도 이 줄을 사용합니다.

	return orun;

전진한 주기 수를 반환합니다. 호출자는 필요하면 overrun 계상 등에 사용할 수 있습니다.

함께 생각해 볼 질문

늦은 횟수만큼 콜백을 실행하나요?

이 함수는 만료 시각을 전진시키고 주기 수를 반환합니다. 콜백 실행이나 큐 재등록 자체를 반복하는 함수가 아닙니다.

아직 기한 전이면 어떻게 되나요?

delta가 음수이므로 만료 시각을 바꾸지 않고 0을 반환합니다.

interval이 너무 작으면 어떻게 되나요?

hrtimer_resolution보다 작으면 그 해상도까지 올립니다. 하드웨어·모드가 표현할 수 없는 간격을 그대로 쓰지 않습니다.

출처와 읽은 범위

Linux stable v6.6 · kernel/time/hrtimer.c

해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고

맨 위로 ↑