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

timekeeping

clocksource cycle을 timekeeper 기준값과 합쳐 시각을 읽는 경로, 그 기준값을 갱신하는 경로, timer 만료 경로를 각각 구분한다.

전체 흐름

clocksource는 경과 cycle을 읽는 시계 소스다. timekeeper는 마지막 기준 cycle과 환산값·기준 시각을 보관한다. ktime_get()은 현재 cycle 차이를 읽어 monotonic 시각을 계산하며, 이 호출이 timer IRQ나 hrtimer 콜백을 실행하는 것은 아니다.

그림 1. 같은 clocksource를 쓰는 두 경로
clocksourceread() → cycle 값. clockevent의 만료 인터럽트와 역할이 다르다.
기준값 갱신update_wall_time() → 누적·보정 → 읽기용 기준값 게시
현재 시각 읽기ktime_get() → seqcount 스냅샷 + 현재 cycle 차이

오른쪽 두 칸은 순차 호출 관계가 아니다. 읽기는 갱신 사이에도 현재 cycle 차이를 더한다.

jiffies는 tick 계수, sched_clock은 스케줄러용 시계, clockevent·hrtimer는 만료 알림 쪽이다. 모두 ktime_get() 내부 단계로 그리면 안 된다.

소스 코드 위치

Linux 6.6의 고정 소스 기준이다. clocksource의 실제 종류와 성능은 보드·설정에 따라 달라진다.

파일확인할 책임
kernel/time/timekeeping.ctimekeeping_init, ktime_get, update_wall_time 및 timekeeper 필드
kernel/time/clocksource.c등록·평가·선택. 초기 기본 clocksource와 이후 선택을 구분
kernel/time/tick-common.ctick/clockevent 연결을 추적할 때 별도로 확인

대표 코드

timekeeping_init

Linux v6.6 kernel/time/timekeeping.c, 1625행 부근의 시작 발췌

/* Flag for if there is a persistent clock on this platform */
static bool persistent_clock_exists;

/*
 * timekeeping_init - Initializes the clocksource and common timekeeping values
 */
void __init timekeeping_init(void)
{
	struct timespec64 wall_time, boot_offset, wall_to_mono;
	struct timekeeper *tk = &tk_core.timekeeper;
	struct clocksource *clock;
	unsigned long flags;

	read_persistent_wall_and_boot_offset(&wall_time, &boot_offset);
	if (timespec64_valid_settod(&wall_time) &&
	    timespec64_to_ns(&wall_time) > 0) {
		persistent_clock_exists = true;
	} else if (timespec64_to_ns(&wall_time) != 0) {
		pr_warn("Persistent clock returned invalid value");
		wall_time = (struct timespec64){0};
	}

	if (timespec64_compare(&wall_time, &boot_offset) < 0)
		boot_offset = (struct timespec64){0};

	/*
	 * We want set wall_to_mono, so the following is true:
	 * wall time + wall_to_mono = boot time
	 */
	wall_to_mono = timespec64_sub(boot_offset, wall_time);

	raw_spin_lock_irqsave(&timekeeper_lock, flags);
	write_seqcount_begin(&tk_core.seq);
	ntp_init();

	clock = clocksource_default_clock();
	if (clock->enable)
		clock->enable(clock);
	tk_setup_internals(tk, clock);

읽는 법: 이 발췌는 함수 중간에서 끝난다. 뒤따르는 tk_set_xtime, timekeeping_update, seqcount 종료까지 읽어야 초기화가 완결된다. clocksource_default_clock()는 최종 최적 시계 선택이 아니다.

fast-timekeeper 래치와 빠른 읽기 보조 코드

Linux v6.6 kernel/time/timekeeping.c, 409행 부근의 중간 발췌
 * Employ the latch technique; see @raw_write_seqcount_latch.
 *
 * So if a NMI hits the update of base[0] then it will use base[1]
 * which is still consistent. In the worst case this can result is a
 * slightly wrong timestamp (a few nanoseconds). See
 * @ktime_get_mono_fast_ns.
 */
static void update_fast_timekeeper(const struct tk_read_base *tkr,
				   struct tk_fast *tkf)
{
	struct tk_read_base *base = tkf->base;

	/* Force readers off to base[1] */
	raw_write_seqcount_latch(&tkf->seq);

	/* Update base[0] */
	memcpy(base, tkr, sizeof(*base));

	/* Force readers back to base[0] */
	raw_write_seqcount_latch(&tkf->seq);

	/* Update base[1] */
	memcpy(base + 1, base, sizeof(*base));
}

static __always_inline u64 fast_tk_get_delta_ns(struct tk_read_base *tkr)
{
	u64 delta, cycles = tk_clock_read(tkr);

	delta = clocksource_delta(cycles, tkr->cycle_last, tkr->mask);
	return timekeeping_delta_to_ns(tkr, delta);
}

static __always_inline u64 __ktime_get_fast_ns(struct tk_fast *tkf)
{
	struct tk_read_base *tkr;
	unsigned int seq;
	u64 now;

	do {

읽는 법: 이 원문은 일반 ktime_get() 본문이 아니다. fast-timekeeper의 래치 복제와 보조 읽기 시작 부분이다. 일반 ktime_get()은 원본의 별도 함수(836행 부근)에서 seqcount 재시도를 사용한다.

함수별 분석

초기화·기준값 갱신·읽기를 하나의 함수 체인으로 연결하지 않는다. 서로 다른 진입점과 보호 규칙이다.

그림 2. 기준값 게시와 잠금 없는 읽기
쓰기·게시timekeeping_init 또는 update_wall_time
spinlock + seqcount 쓰기 구간
읽기ktime_get: base + 현재 cycle 차이
seqcount 시작값 검사·재시도

갱신은 매번 ktime_get()에서 호출되는 선행 단계가 아니다.

함수실제 역할경계
timekeeping_inittimekeeper_lock과 seqcount 쓰기 구간에서 기본 clocksource와 시작 시각을 설정하고 timekeeping_update로 읽기용 자료를 갱신한다.기본 clocksource로 시작하며 나중에 timekeeping_notify로 바뀔 수 있다.
update_wall_timetimekeeping_advance가 전역 shadow에 누적한 뒤 timekeeping_update와 memcpy로 본체를 갱신한다.tick 기반 갱신. NO_HZ에서 여러 cycle interval을 한꺼번에 누적할 수 있다.
ktime_gettkr_mono.base와 timekeeping_get_ns를 같은 seqcount 스냅샷에서 읽는다.read-only monotonic 경로; suspend 중 사용에는 WARN이 있다.

구조체와 필드

전역 tk_core가 일관된 읽기 스냅샷을 제공한다. tk_read_base는 clocksource 참조·기준 cycle·환산 계수를 묶는다.

객체·필드뜻읽기 규칙
tkr_mono.clock, cycle_last시계 소스와 기준 cycleclocksource 교체·누적 때 함께 맞춘다.
tkr_mono.mult, shift, maskcycle 차이를 ns로 환산하고 wrap 처리clocksource 특성과 보정이 반영된다.
tkr_mono.basemonotonic 기준값ktime_get()이 현 cycle 차이를 더한다.
tk_core.seq여러 필드의 일관성일반 읽기는 값이 바뀌면 다시 읽는다.
offs_real, tkr_rawrealtime offset, raw 기준monotonic·realtime·raw의 의미를 구분한다.

실행 단계와 상태 변화

그림 3. Linux 6.6 기준값 갱신 경계
cycle 관찰tk_clock_read·clocksource_delta로 경과량 획득
shadow 누적interval 누적, NTP 보정, 초 경계 정리
읽기용 게시timekeeping_update + memcpy와 seqcount 종료

읽기는 마지막 게시 기준값에 현재 cycle 차이를 더하는 별도 경로다.

v6.6은 전역 shadow/timekeeper lock을 쓴다. v6.18.37은 tk_data 배열에 본체·shadow·lock을 묶고 보조 timekeeper 경로를 갖는다. 두 버전의 저장 구조를 동일하게 묘사하지 않는다.

불변 조건과 실패 사례

조건혼동하면 생기는 문제
cycle 기준·환산 계수·base를 같은 seqcount 스냅샷에서 읽기갱신 전후 필드를 섞으면 시각이 튈 수 있다.
clocksource와 clockevent 분리counter 읽기를 timer IRQ → callback 호출로 잘못 그리게 된다.
monotonic, realtime, raw 구분RTC/NTP·시각 설정의 영향과 단조 경과 시간 보장을 혼동한다.
일반 읽기와 fast/NMI 읽기 구분래치 보조 코드를 일반 ktime_get() 본문으로 오해한다.

clocksource 고장·교체, suspend/resume, 긴 cycle 간격은 별도 경계 조건이다. 고정 소스의 호출 관계는 모든 보드의 정확도 검증 결과가 아니다.

계측과 검증

실기기에서는 /sys/devices/system/clocksource/clocksource0/current_clocksource로 활성 clocksource를 확인한다. 이름만으로 정밀도·안정성을 증명할 수는 없다.

관찰검증할 가설
source: timekeeping_init·timekeeping_notify부팅 기본값과 이후 교체를 구분
source: update_wall_time·timekeeping_advance쓰기에서 cycle 누적·게시 확인
반복 ktime_get() 호출단조 읽기 경로 점검; 측정 비용·가상화 영향도 고려

ftrace 함수 추적 지원은 설정에 따라 다르다. perf stat cycles는 활성 clocksource의 cycle 값을 직접 보여 주는 지표가 아니다.

추가 확인 사항

  • 이 보드의 부팅 기본 clocksource와 이후 선택된 clocksource가 같은가?
  • ktime_get()의 seqcount 재시도와 fast/NMI 래치는 각각 언제 필요한가?
  • 긴 NO_HZ 구간 뒤 update_wall_time()은 얼마의 cycle을 누적하는가?
  • 6.6과 6.18.37의 timekeeper 저장·게시 방식 차이는 어느 필드에 드러나는가?