← Architecture 비교

Linux 6.18.37 LTS · Architecture comparison 14/21

Timer와 clocksource: architected timer, TSC와 RISC-V time

kernel이 단조 증가 시간을 읽고 다음 hrtimer deadline을 hardware comparator에 설정하며 CPU별 clock event를 처리하는 방식을 비교합니다.

비교 대상
arm64 / x86-64 / RISC-V
실제 원본
파일 3개 · 원문 143줄
기준 태그
Linux v6.18.37
분석 축
상태 · 접근 순서 · 객체 수명 · 지연 시간

01 · QUESTION

무엇을 확인할 것인가

cycle counter를 nanosecond로 변환할 때 overflow 없이 monotonicity를 유지하고, idle CPU의 다음 deadline을 어떻게 깨우는가?

clocksource는 연속 counter read와 mult/shift 변환을 제공하고 clockevent는 one-shot deadline interrupt를 제공한다. timekeeping seqlock이 base cycle과 base ns를 일관되게 읽으며 hrtimer가 가장 이른 만료를 device에 program한다.

counter read, timekeeper update, per-CPU clockevent program, interrupt handler와 timer callback을 나눈다. callback은 hardirq 또는 softirq context이므로 sleep 가능 여부가 다르다.

지연 시간 관점deadline error는 counter read/program 비용, hardware minimum delta, interrupt masking, firmware trap과 callback backlog의 합이다. tickless idle에서는 너무 먼 deadline과 counter wrap도 확인한다.

02 · CONTRACT

공통 규약과 아키텍처별 구현

architecture핵심 mechanism실패 형태확인할 상태
arm64CNTVCT/CNTPCT counter와 CNTV_TVAL/CVAL comparatorCNTFRQ 불일치, unstable counter erratum 또는 comparator program 뒤 즉시 만료 race는 시간 역행과 interrupt storm을 만든다.CNTFRQ, CNTVCT, CVAL/TVAL, control enable/mask/status, PPI affinity와 clocksource rating을 본다.
x86-64TSC clocksource와 local APIC TSC-deadline/one-shot timersocket 간 TSC offset, virtualization migration, 잘못된 calibration은 monotonicity 위반과 timer 조기/지연 만료를 만든다.CPUID invariant bit, tsc_khz, clocksource watchdog, IA32_TSC_DEADLINE, APIC LVT와 per-CPU offset을 확인한다.
RISC-VTIME CSR/SBI와 Sstc STIMECMP comparatorfirmware trap latency, 잘못된 DT timebase-frequency/ACPI RHCT time_base_freq 또는 RV32 comparator write race가 긴 wakeup 지연이나 즉시 재interrupt를 만든다.DT timebase-frequency 또는 ACPI RHCT time_base_freq, TIME, STIMECMP/SBI call, SIE.STIE, timer pending과 clockevent min_delta를 본다.

03 · DIAGRAMS

세 그림으로 먼저 읽기

그림 1. 같은 기능을 구현하는 서로 다른 방식각 명령어 집합(ISA)에서 추적할 상태와 확인할 항목을 나란히 정리했습니다.

arm64

mechanism
CNTVCT/CNTPCT counter와 CNTV_TVAL/CVAL comparator
state
architected timer frequency CNTFRQ를 기준으로 virtual 또는 physical counter를 clocksource로 등록한다. per-CPU PPI가 comparator 만료를 알리고 TVAL 또는 CVAL에 다음 event를 쓴다.
checkpoint
CNTFRQ, CNTVCT, CVAL/TVAL, control enable/mask/status, PPI affinity와 clocksource rating을 본다.

x86-64

mechanism
TSC clocksource와 local APIC TSC-deadline/one-shot timer
state
invariant/nonstop TSC가 충분히 신뢰되면 RDTS(C/P)가 clocksource가 되고 CPUID/calibration으로 frequency를 구한다. local APIC timer는 TSC-deadline MSR 또는 divide-count mode로 per-CPU event를 만든다.
checkpoint
CPUID invariant bit, tsc_khz, clocksource watchdog, IA32_TSC_DEADLINE, APIC LVT와 per-CPU offset을 확인한다.

RISC-V

mechanism
TIME CSR/SBI와 Sstc STIMECMP comparator
state
일반 S-mode 경로에서 time_init()은 ACPI를 쓰지 않으면 DT /cpus의 timebase-frequency, ACPI를 쓰면 RHCT의 time_base_freq를 riscv_timebase에 저장한다. timer_probe()가 시작한 RISC-V timer driver는 이 값으로 clocksource와 sched_clock을 각각 등록한다. clockevent는 Sstc가 있으면 STIMECMP에 직접 쓰고, 없으면 SBI set_timer를 요청한다.
checkpoint
DT timebase-frequency 또는 ACPI RHCT time_base_freq, TIME, STIMECMP/SBI call, SIE.STIE, timer pending과 clockevent min_delta를 본다.
그림 1a. RISC-V 시간 주파수의 Device Tree·ACPI 분기
ACPI 비활성 · DT
/cpus/timebase-frequency를 읽습니다.
ACPI 사용 · RHCT
time_base_freq를 읽습니다.
두 경로 → riscv_timebase → timer_probe() → RISC-V timer driver의 clocksource·sched_clock 등록과 CPU별 clockevent 설정
그림 2. 공통 커널 코드와 아키텍처별 함수의 역할공통 커널 코드와 아키텍처별 코드가 각각 담당하는 일을 구분했습니다.
리눅스 공통 규약clocksource는 연속 counter read와 mult/shift 변환을 제공하고 clockevent는 one-shot deadline interrupt를 제공한다. timekeeping seqlock이 base cycle과 base ns를 일관되게 읽으며 hrtimer가 가장 이른 만료를 device에 program한다.
arm64CNTVCT/CNTPCT counter와 CNTV_TVAL/CVAL comparatorISB로 counter read ordering을 맞추고 erratum workaround가 alternative read path를 선택할 수 있다.
x86-64TSC clocksource와 local APIC TSC-deadline/one-shot timerRDTSCP/LFENCE ordering, TSC synchronization과 frequency scaling invariant 여부를 검증해야 한다.
RISC-VTIME CSR/SBI와 Sstc STIMECMP comparatorRV32에서 64-bit time/comparator를 읽고 쓸 때 high-low-high 또는 safe write sequence로 torn value를 막아야 한다.
객체 수명 관리registered clocksource와 clock_event_device는 사용 CPU가 online인 동안 유지된다. CPU hotplug와 suspend는 device shutdown, counter continuity와 resume correction을 조정해야 한다.
그림 3. 상태 변경과 다른 코드에서 읽는 순서상태를 준비한 뒤, 다른 코드가 읽기 전에 아키텍처에서 요구하는 메모리 접근 순서를 지켜야 합니다.
arm64상태 준비→ISB로 counter read ordering을 맞추고 erratum workaround가 alternative read path를 선택할 수 있다.→관찰: CNTFRQ, CNTVCT, CVAL/TVAL, control enable/mask/status, PPI affinity와 clocksource rating을 본다.
x86-64상태 준비→RDTSCP/LFENCE ordering, TSC synchronization과 frequency scaling invariant 여부를 검증해야 한다.→관찰: CPUID invariant bit, tsc_khz, clocksource watchdog, IA32_TSC_DEADLINE, APIC LVT와 per-CPU offset을 확인한다.
RISC-V상태 준비→RV32에서 64-bit time/comparator를 읽고 쓸 때 high-low-high 또는 safe write sequence로 torn value를 막아야 한다.→관찰: DT timebase-frequency 또는 ACPI RHCT time_base_freq, TIME, STIMECMP/SBI call, SIE.STIE, timer pending과 clockevent min_delta를 본다.

04 · SOURCE

Linux 6.18.37 원본 코드와 주요 구문 해설

소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 원문 전체 발췌를 보존하고, 개별 해설은 근거가 있는 구문에만 붙였습니다.

arm64 · Linux 6.18.37

CNTVCT/CNTPCT counter와 CNTV_TVAL/CVAL comparator

architected timer frequency CNTFRQ를 기준으로 virtual 또는 physical counter를 clocksource로 등록한다. per-CPU PPI가 comparator 만료를 알리고 TVAL 또는 CVAL에 다음 event를 쓴다.

원본 코드: arch/arm64/kernel/time.c:49-73

49	arch_stack_walk(profile_pc_cb, &prof_pc, current, regs);
50 
51	return prof_pc;
52}
53EXPORT_SYMBOL(profile_pc);
54 
55void __init time_init(void)
56{
57	u32 arch_timer_rate;
58 
59	of_clk_init(NULL);
60	timer_probe();
61 
62	tick_setup_hrtimer_broadcast();
63 
64	arch_timer_rate = arch_timer_get_rate();
65	if (!arch_timer_rate)
66		panic("Unable to initialise architected timer.\n");
67 
68	/* Calibrate the delay loop directly */
69	lpj_fine = arch_timer_rate / HZ;
70 
71	pv_time_init();
72}
73 

주요 구문 해설

원본 25줄은 그대로 표시하며, 검토한 13개 구문에 설명을 붙였습니다.

L49 arch_stack_walk(profile_pc_cb, &prof_pc, current, regs);

저장된 regs에서 현재 태스크의 호출 스택을 따라가며 profile_pc_cb로 각 PC를 검사합니다. 콜백은 잠금 함수 내부 주소를 건너뛰고 첫 외부 주소를 prof_pc에 담아, 프로파일 표본을 실제 작업 위치에 가깝게 잡습니다.

L51 return prof_pc;

잠금 함수들을 건너뛰어 찾은 표본 PC를 반환합니다. 적절한 프레임을 찾지 못했다면 초기값 0이 남습니다.

L53EXPORT_SYMBOL(profile_pc);

profile_pc를 다른 커널 모듈이 참조할 수 있도록 심볼 표에 공개합니다. 프로파일링을 여기서 실행하는 호출이 아니라, 스택에서 표본 PC를 고르는 함수를 외부에서도 쓸 수 있게 하는 선언입니다.

L55void __init time_init(void)

부팅 중 clock 공급자와 타이머를 탐색하고 시간·지연 계산에 필요한 자료를 준비합니다. __init 코드는 초기화 이후 해제할 수 있습니다.

L57 u32 arch_timer_rate;

architectural timer의 초당 카운터 증가 횟수를 담을 32비트 부호 없는 값입니다. Hz 단위이며 아래 지연 보정의 기준이 됩니다.

L59 of_clk_init(NULL);

devicetree의 platform clock provider를 먼저 초기화해 timer 외 장치가 사용할 clock tree를 준비한다.

L60 timer_probe();

DT/ACPI에서 architected timer 등 clocksource/clockevent driver를 탐색하고 등록한다.

L62 tick_setup_hrtimer_broadcast();

설정이 지원하면 hrtimer를 이용한 broadcast clock-event 장치를 준비합니다. 깊은 idle에서 자체 타이머가 멈추는 CPU 등에도 필요한 tick을 전달할 공통 타이머 경로를 마련합니다.

L64 arch_timer_rate = arch_timer_get_rate();

초기화된 Arm architectural timer의 카운터 주파수(Hz)를 읽습니다. 뒤에서 0이면 초기화 실패로 중단하고, 정상 값이면 HZ로 나누어 지연 루프 보정값을 계산합니다.

L65 if (!arch_timer_rate)

타이머 주파수를 얻지 못해 0인지 검사합니다. 이 상태로는 카운터 값을 시간으로 환산할 수 없습니다.

L66 panic("Unable to initialise architected timer.\n");

필수 architectural timer를 초기화하지 못했으므로 이유를 출력하고 부팅을 중단합니다.

L69 lpj_fine = arch_timer_rate / HZ;

초당 카운터 횟수를 초당 tick 수 HZ로 나누어 한 tick에 해당하는 지연 보정값을 얻습니다. 타이머 기준으로 바쁜 대기 시간을 계산할 준비입니다.

L71 pv_time_init();

가상화 환경이 제공하는 stolen time 기능을 확인하고 지원되면 연결합니다. vCPU가 실행 기회를 받지 못한 시간을 일반 태스크 실행 시간과 구분해 계산하도록 하며, 지원되지 않으면 그대로 넘어갑니다.

x86-64 · Linux 6.18.37

TSC clocksource와 local APIC TSC-deadline/one-shot timer

invariant/nonstop TSC가 충분히 신뢰되면 RDTS(C/P)가 clocksource가 되고 CPUID/calibration으로 frequency를 구한다. local APIC timer는 TSC-deadline MSR 또는 divide-count mode로 per-CPU event를 만든다.

원본 코드: arch/x86/kernel/tsc.c:1519-1597

1519 
1520	snp_secure_tsc_init();
1521 
1522	if (!determine_cpu_tsc_frequencies(true))
1523		return;
1524	tsc_enable_sched_clock();
1525}
1526 
1527void __init tsc_init(void)
1528{
1529	if (!cpu_feature_enabled(X86_FEATURE_TSC)) {
1530		setup_clear_cpu_cap(X86_FEATURE_TSC_DEADLINE_TIMER);
1531		return;
1532	}
1533 
1534	/*
1535	 * native_calibrate_cpu_early can only calibrate using methods that are
1536	 * available early in boot.
1537	 */
1538	if (x86_platform.calibrate_cpu == native_calibrate_cpu_early)
1539		x86_platform.calibrate_cpu = native_calibrate_cpu;
1540 
1541	if (!tsc_khz) {
1542		/* We failed to determine frequencies earlier, try again */
1543		if (!determine_cpu_tsc_frequencies(false)) {
1544			mark_tsc_unstable("could not calculate TSC khz");
1545			setup_clear_cpu_cap(X86_FEATURE_TSC_DEADLINE_TIMER);
1546			return;
1547		}
1548		tsc_enable_sched_clock();
1549	}
1550 
1551	cyc2ns_init_secondary_cpus();
1552 
1553	if (!no_sched_irq_time)
1554		enable_sched_clock_irqtime();
1555 
1556	lpj_fine = get_loops_per_jiffy();
1557 
1558	check_system_tsc_reliable();
1559 
1560	if (unsynchronized_tsc()) {
1561		mark_tsc_unstable("TSCs unsynchronized");
1562		return;
1563	}
1564 
1565	if (tsc_clocksource_reliable || no_tsc_watchdog)
1566		tsc_disable_clocksource_watchdog();
1567 
1568	clocksource_register_khz(&clocksource_tsc_early, tsc_khz);
1569	detect_art();
1570}
1571 
1572#ifdef CONFIG_SMP
1573/*
1574 * Check whether existing calibration data can be reused.
1575 */
1576unsigned long calibrate_delay_is_known(void)
1577{
1578	int sibling, cpu = smp_processor_id();
1579	int constant_tsc = cpu_has(&cpu_data(cpu), X86_FEATURE_CONSTANT_TSC);
1580	const struct cpumask *mask = topology_core_cpumask(cpu);
1581 
1582	/*
1583	 * If TSC has constant frequency and TSC is synchronized across
1584	 * sockets then reuse CPU0 calibration.
1585	 */
1586	if (constant_tsc && !tsc_unstable)
1587		return cpu_data(0).loops_per_jiffy;
1588 
1589	/*
1590	 * If TSC has constant frequency and TSC is not synchronized across
1591	 * sockets and this is not the first CPU in the socket, then reuse
1592	 * the calibration value of an already online CPU on that socket.
1593	 *
1594	 * This assumes that CONSTANT_TSC is consistent for all CPUs in a
1595	 * socket.
1596	 */
1597	if (!constant_tsc || !mask)

주요 구문 해설

원본 79줄은 그대로 표시하며, 검토한 36개 구문에 설명을 붙였습니다.

L1520 snp_secure_tsc_init();

SEV-SNP Secure TSC를 사용하는 게스트라면 secrets 페이지의 보정 계수와 게스트 TSC 주파수로 신뢰할 TSC 주파수를 준비합니다. 해당 값으로 CPU·TSC 보정 함수를 연결한 뒤 일반 주파수 판별로 진행합니다.

L1522 if (!determine_cpu_tsc_frequencies(true))

초기 부팅에서 사용할 수 있는 방법만으로 CPU·TSC 주파수를 구합니다. true는 early 모드이며 실패하면 이후 단계에서 다시 시도할 수 있습니다.

L1523 return;

초기 단계에서 주파수를 못 구했으므로 아직 TSC sched_clock을 활성화하지 않고 돌아갑니다. 전체 부팅을 실패시키는 반환은 아닙니다.

L1524 tsc_enable_sched_clock();

초기 부팅 단계에서 TSC 주파수를 구했으므로 TSC 기반 지연 루프와 sched_clock 사용을 준비합니다. 부트 CPU의 사이클→나노초 환산 상태를 만들고 TSC 사용 분기를 활성화합니다.

L1527void __init tsc_init(void)

TSC의 정식 초기화 단계입니다. 주파수 보정을 보완하고 CPU별 시간 환산, 신뢰성 검사, clocksource 등록을 수행합니다.

L1529 if (!cpu_feature_enabled(X86_FEATURE_TSC)) {

사용 가능한 TSC 기능이 없는지 검사합니다. 없으면 TSC deadline도 끄고 TSC 초기화 경로를 끝냅니다.

L1530 setup_clear_cpu_cap(X86_FEATURE_TSC_DEADLINE_TIMER);

사용 가능한 TSC가 없으므로 그 카운터를 기한으로 사용하는 TSC deadline 타이머 기능도 제거합니다. 이후 타이머 선택 코드가 사용할 수 없는 deadline 방식을 고르지 않게 합니다.

L1531 return;

TSC가 없으므로 나머지 TSC 보정·등록 작업을 수행하지 않습니다. 다른 시간원 선택은 공통 시간 초기화가 담당합니다.

L1538 if (x86_platform.calibrate_cpu == native_calibrate_cpu_early)

플랫폼 보정 함수가 아직 초기 부팅용 기본 함수인지 확인합니다. 별도로 등록한 플랫폼 전용 함수는 덮어쓰지 않습니다.

L1539 x86_platform.calibrate_cpu = native_calibrate_cpu;

초기 단계에서 불가능했던 측정 방법도 사용할 수 있는 정식 CPU 주파수 보정 함수로 바꿉니다.

L1541 if (!tsc_khz) {

앞선 초기화에서 TSC 주파수를 얻지 못했는지 검사합니다. 이미 유효한 값이 있으면 중복 측정을 건너뜁니다.

L1543 if (!determine_cpu_tsc_frequencies(false)) {

early 제한을 풀고 주파수 판별을 다시 시도합니다. 그래도 실패하면 TSC와 deadline 타이머를 신뢰 가능한 기준으로 사용하지 않습니다.

L1544 mark_tsc_unstable("could not calculate TSC khz");

다시 시도해도 TSC 주파수를 구하지 못했으므로 TSC를 불안정한 시간원으로 표시하고 이유를 남깁니다. 사이클을 시간으로 환산할 근거가 없는 상태에서 신뢰 가능한 clocksource처럼 쓰지 않게 합니다.

L1545 setup_clear_cpu_cap(X86_FEATURE_TSC_DEADLINE_TIMER);

TSC 주파수 보정에 실패한 경로이므로 TSC deadline 타이머 지원 표시도 지웁니다. 정확한 기한을 환산할 수 없는 타이머를 사용하지 않도록 제한합니다.

L1546 return;

주파수를 구하지 못한 오류 상태를 표시한 뒤 초기화를 끝냅니다. 아래 clocksource 등록까지 진행하지 않습니다.

L1548 tsc_enable_sched_clock();

초기 시도에서는 구하지 못했던 TSC 주파수를 이번 보정에서 얻었으므로 TSC 지연 루프·sched_clock을 활성화합니다. 부트 CPU의 사이클 환산 자료도 이때 준비합니다.

L1551 cyc2ns_init_secondary_cpus();

보조 CPU들이 사용할 TSC 사이클→나노초 환산 상태를 초기화합니다. CPU가 달라져도 각 CPU의 sched_clock 계산이 준비된 환산 자료를 사용하게 합니다.

L1553 if (!no_sched_irq_time)

IRQ 시간 계산을 금지하는 no_sched_irq_time이 설정되지 않았는지 확인합니다. 허용된 경우 sched_clock으로 인터럽트 시간을 계산하게 합니다.

L1554 enable_sched_clock_irqtime();

no_sched_irq_time으로 금지하지 않았다면 sched_clock 기반의 IRQ 실행 시간 계산을 켭니다. 인터럽트 처리에 쓴 시간을 태스크 자체 실행 시간과 구분해 스케줄러 통계에 반영하도록 합니다.

L1556 lpj_fine = get_loops_per_jiffy();

알려진 TSC 주파수와 HZ를 바탕으로 한 tick 길이의 지연 루프 보정값을 구해 lpj_fine에 둡니다. 공통 지연 보정 코드가 타이머를 기다리는 별도 측정을 줄일 수 있게 합니다.

L1558 check_system_tsc_reliable();

platform/CPU 특성을 반영해 clocksource watchdog 없이 신뢰할 수 있는지 판단한다.

L1560 if (unsynchronized_tsc()) {

CPU 간 TSC가 동기화되어 있지 않은지 확인합니다. CPU 이동에 따른 시간 불연속 위험이 있으면 정상 등록 경로를 중단합니다.

L1561 mark_tsc_unstable("TSCs unsynchronized");

CPU 사이의 TSC가 동기화되지 않았다고 판단했으므로 TSC를 불안정한 시간원으로 표시합니다. 태스크가 CPU를 옮겼을 때 시간 값이 어긋날 수 있어, 아래의 정상 TSC clocksource 등록 경로를 중단합니다.

L1562 return;

동기화되지 않은 TSC로 표시한 뒤 반환합니다. 아래의 early TSC clocksource 등록과 ART 탐지를 수행하지 않습니다.

L1565 if (tsc_clocksource_reliable || no_tsc_watchdog)

플랫폼이 TSC 신뢰성을 보장했거나 사용자가 watchdog을 끈 경우인지 검사합니다. 해당하면 다른 clocksource와의 주기적 대조를 생략합니다.

L1566 tsc_disable_clocksource_watchdog();

플랫폼이 TSC를 신뢰할 수 있다고 보장하거나 watchdog 검사를 끈 경우 주기적인 clocksource 대조 검사를 비활성화합니다. TSC 카운터 자체를 정지시키는 처리가 아닙니다.

L1568 clocksource_register_khz(&clocksource_tsc_early, tsc_khz);

변환 mult/shift를 계산할 수 있도록 TSC frequency와 clocksource를 timekeeping core에 등록한다.

L1569 detect_art();

조건이 맞는 CPU에서 Always Running Timer와 TSC의 주파수 비율·오프셋을 찾아 기록합니다. CPUID의 비율과 TSC_ADJUST를 확인하고 ART 지원을 표시해 서로 다른 하드웨어 시간 기준을 연결할 수 있게 합니다.

L1572#ifdef CONFIG_SMP

SMP 구성에서는 현재 CPU의 TSC 특성과 형제 CPU의 보정 결과를 보고 delay loop 보정값을 재사용할 수 있는지 확인합니다. 모든 CPU가 무조건 같은 값을 쓰는 것이 아니라 타이머 특성을 확인하는 최적화입니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L1576unsigned long calibrate_delay_is_known(void)

현재 CPU의 지연 루프를 새로 측정하지 않고 기존 보정값을 쓸 수 있는지 판단합니다. 사용 가능한 값이 없으면 0을 돌려 공통 보정을 계속하게 합니다.

L1578 int sibling, cpu = smp_processor_id();

지연 보정값을 재사용할 수 있는지 판단하기 위해 현재 CPU 번호를 cpu에 저장합니다. sibling은 아래에서 같은 패키지의 기존 보정값을 제공할 CPU를 찾는 데 사용합니다.

L1579 int constant_tsc = cpu_has(&cpu_data(cpu), X86_FEATURE_CONSTANT_TSC);

현재 CPU의 기능 정보에서 CONSTANT_TSC 비트를 검사합니다. TSC 주파수가 CPU 동작 주파수 변화에 따라 달라지지 않는지 판단해, 다른 CPU의 지연 보정값을 재사용할 조건으로 삼습니다.

L1580 const struct cpumask *mask = topology_core_cpumask(cpu);

현재 CPU와 같은 패키지에 속하는 CPU 마스크를 얻습니다. 아래에서는 자기 자신을 제외한 CPU를 찾아 이미 알려진 loops_per_jiffy를 가져올 수 있는지 확인합니다. 이름에 core가 있지만 한 코어의 SMT 스레드만 고르는 마스크는 아닙니다.

L1586 if (constant_tsc && !tsc_unstable)

TSC 주파수가 일정하고 불안정 표시도 없는지 확인합니다. 이 경우 CPU0의 보정값을 다른 CPU에도 재사용합니다.

L1587 return cpu_data(0).loops_per_jiffy;

CPU0에서 얻은 loops_per_jiffy를 반환하여 이 CPU의 지연 측정을 줄입니다. 위에서 일정한 TSC와 안정성을 확인한 경로입니다.

L1597 if (!constant_tsc || !mask)

TSC 주파수 일정성이 없거나 같은 패키지 CPU 마스크를 얻지 못했으면 재사용을 포기합니다. 다음 줄의 0은 새 보정이 필요하다는 뜻입니다.

RISC-V · Linux 6.18.37

TIME CSR/SBI와 Sstc STIMECMP comparator

일반 S-mode 경로에서 time_init()은 ACPI를 쓰지 않으면 DT /cpus의 timebase-frequency, ACPI를 쓰면 RHCT의 time_base_freq를 riscv_timebase에 저장한다. timer_probe()가 시작한 RISC-V timer driver는 이 값으로 clocksource와 sched_clock을 각각 등록한다. clockevent는 Sstc가 있으면 STIMECMP에 직접 쓰고, 없으면 SBI set_timer를 요청한다.

위 주파수 분기는 표시한 arch/riscv/kernel/time.c:27–46에서 확인한다. 별도 드라이버의 riscv_timer_init_common()은 clocksource_register_hz()와 sched_clock_register()를 각각 호출한다. Sstc와 SBI clockevent 선택은 같은 드라이버의 riscv_clock_next_event()에 있다.

원본 코드: arch/riscv/kernel/time.c:14-52

14#include <asm/timex.h>
15#include <asm/paravirt.h>
16 
17unsigned long riscv_timebase __ro_after_init;
18EXPORT_SYMBOL_GPL(riscv_timebase);
19 
20void __init time_init(void)
21{
22	struct device_node *cpu;
23	struct acpi_table_rhct *rhct;
24	acpi_status status;
25	u32 prop;
26 
27	if (acpi_disabled) {
28		cpu = of_find_node_by_path("/cpus");
29		if (!cpu || of_property_read_u32(cpu, "timebase-frequency", &prop))
30			panic("RISC-V system with no 'timebase-frequency' in DTS\n");
31 
32		of_node_put(cpu);
33		riscv_timebase = prop;
34		of_clk_init(NULL);
35	} else {
36		status = acpi_get_table(ACPI_SIG_RHCT, 0, (struct acpi_table_header **)&rhct);
37		if (ACPI_FAILURE(status))
38			panic("RISC-V ACPI system with no RHCT table\n");
39 
40		riscv_timebase = rhct->time_base_freq;
41		acpi_put_table((struct acpi_table_header *)rhct);
42	}
43 
44	lpj_fine = riscv_timebase / HZ;
45 
46	timer_probe();
47 
48	tick_setup_hrtimer_broadcast();
49 
50	pv_time_init();
51}
52 

주요 구문 해설

원본 39줄은 그대로 표시하며, 검토한 26개 구문에 설명을 붙였습니다.

L14#include <asm/timex.h>

RISC-V 시간 카운터를 읽는 저수준 정의를 가져옵니다. timebase 주파수와 함께 커널의 시간 측정·delay 보정에 사용할 공통 기반입니다. 전처리 단계에서 헤더 내용을 포함하며, CPU가 실행 중 헤더를 호출하는 동작은 없습니다.

L15#include <asm/paravirt.h>

가상화 환경에서 호스트가 제공하는 시간·steal-time 관련 초기화 정의를 가져옵니다. 일반 timebase 준비 뒤 paravirtual 시간 정보를 연결할 수 있게 합니다. 전처리 단계에서 헤더 내용을 포함하며, CPU가 실행 중 헤더를 호출하는 동작은 없습니다.

L17unsigned long riscv_timebase __ro_after_init;

RISC-V 시간 카운터의 주파수를 보관하는 전역값입니다. __ro_after_init은 부팅에서 값을 채운 뒤 읽기 전용 보호 대상으로 만들겠다는 표식입니다.

L18EXPORT_SYMBOL_GPL(riscv_timebase);

시간 카운터의 초당 증가 횟수인 riscv_timebase를 GPL 호환 모듈에 공개합니다. 변수 내보내기 선언이며, 카운터를 읽거나 주파수를 측정하는 실행문은 아닙니다.

L20void __init time_init(void)

부팅 중 DT 또는 ACPI에서 시간 카운터 주파수를 읽고 타이머 드라이버와 지연 보정을 준비합니다.

L22 struct device_node *cpu;

Device Tree의 /cpus 노드 참조를 담습니다. timebase-frequency 속성을 읽은 뒤 참조를 반환합니다.

L23 struct acpi_table_rhct *rhct;

ACPI가 제공하는 RHCT 테이블 주소를 담습니다. 이 테이블의 time_base_freq가 DT 속성 대신 주파수를 제공합니다.

L24 acpi_status status;

ACPI 테이블 조회가 성공했는지 검사할 상태값입니다. Linux 음수 errno와는 별도의 ACPI 상태 형식입니다.

L25 u32 prop;

Device Tree에서 읽은 32비트 주파수 속성을 임시 보관합니다. 읽기가 성공하면 전역 riscv_timebase에 옮깁니다.

L27 if (acpi_disabled) {

ACPI를 사용하지 않는 부팅인지 구분합니다. 이 경우 Device Tree에서 주파수를 읽고, 반대 경로는 RHCT를 사용합니다.

L28 cpu = of_find_node_by_path("/cpus");

Device Tree의 /cpus 노드를 찾아 참조를 얻습니다. 이 노드의 timebase-frequency 속성이 RISC-V 시간 카운터의 주파수를 제공하므로, 아래에서 해당 값을 읽기 위한 준비입니다.

L29 if (!cpu || of_property_read_u32(cpu, "timebase-frequency", &prop))

/cpus 노드가 없거나 timebase-frequency를 32비트 값으로 읽지 못했는지 검사합니다. 둘 중 하나라도 실패하면 시간 기준을 확보할 수 없습니다.

L30 panic("RISC-V system with no 'timebase-frequency' in DTS\n");

필수 Device Tree 주파수 정보를 찾지 못했으므로 원인을 출력하고 부팅을 중단합니다.

L32 of_node_put(cpu);

timebase-frequency를 읽은 뒤 /cpus 노드에 대해 얻었던 참조를 내려놓습니다. Device Tree 노드를 삭제하는 것이 아니라, 검색 중 확보한 참조의 수명을 끝내는 처리입니다.

L33 riscv_timebase = prop;

Device Tree에서 얻은 Hz 단위 주파수를 공통 전역값에 보관합니다. 이후 코드는 DT와 ACPI를 구분하지 않고 이 값을 사용합니다.

L34 of_clk_init(NULL);

timer와 주변 장치가 참조할 devicetree clock provider를 초기화한다.

L35 } else {

ACPI를 사용하는 부팅의 경로입니다. Device Tree 속성 대신 RHCT 테이블에서 시간 기준을 읽습니다.

L36 status = acpi_get_table(ACPI_SIG_RHCT, 0, (struct acpi_table_header **)&rhct);

ACPI의 첫 번째 RHCT 테이블을 찾아 rhct 포인터로 받습니다. RISC-V Hart Capabilities Table에 있는 시간 기준 주파수를 사용하려는 것이며, 반환된 status로 테이블 조회 실패를 검사합니다.

L37 if (ACPI_FAILURE(status))

RHCT 조회의 ACPI 상태가 실패인지 검사합니다. 유효하지 않은 테이블 포인터에서 주파수를 읽지 않게 합니다.

L38 panic("RISC-V ACPI system with no RHCT table\n");

ACPI 부팅에 필요한 RHCT가 없으므로 시간 초기화를 진행하지 않고 부팅을 중단합니다.

L40 riscv_timebase = rhct->time_base_freq;

RHCT의 time_base_freq를 전역 카운터 주파수로 보관합니다. 단위는 초당 카운터 증가 횟수입니다.

L41 acpi_put_table((struct acpi_table_header *)rhct);

RHCT의 시간 기준 주파수를 복사했으므로 테이블 조회로 확보한 참조를 반환합니다. 이후에는 riscv_timebase에 저장한 값만 필요하므로 ACPI 테이블을 계속 붙잡아 둘 이유가 없습니다.

L44 lpj_fine = riscv_timebase / HZ;

카운터 주파수를 HZ로 나누어 한 tick에 해당하는 지연 보정값을 준비합니다. 주파수의 제공자가 DT인지 ACPI인지는 이 계산에 영향을 주지 않습니다.

L46 timer_probe();

Sstc 또는 SBI 기반 RISC-V timer driver를 찾아 clocksource/clockevent로 등록한다.

L48 tick_setup_hrtimer_broadcast();

설정이 지원하면 hrtimer 기반 broadcast clock-event 장치를 등록합니다. 자신의 로컬 타이머를 사용할 수 없는 idle CPU에도 tick을 전달할 수 있도록 공통 타이머 경로를 준비합니다.

L50 pv_time_init();

지원되는 가상화 환경에서 vCPU가 실행되지 못한 stolen time을 읽는 기능을 연결합니다. CPU online·offline 콜백과 시간 읽기 함수를 등록해 스케줄러가 그 시간을 구분하도록 합니다.

05 · WORKED EXAMPLE

숫자로 검산하기

01

cycle을 ns로 바꾸고 deadline을 program하는 계산

counter frequency 24MHz, 현재 cycle 9,600,000, 2.5ms 뒤 hrtimer를 가정한다.

  1. now9,600,000 / 24,000,000 = 0.4초이므로 base가 0이면 400,000,000ns다.
  2. delta2.5ms는 0.0025 x 24,000,000 = 60,000 cycle이다.
  3. deadlineabsolute comparator 방식은 9,660,000을 쓰고 relative TVAL 방식은 60,000을 쓴다.
  4. lateness실제 handler가 cycle 9,666,000에서 시작하면 6,000/24MHz = 250us 늦었다.

결론timer 정확도는 tick 단위가 아니라 counter frequency, program 방식과 interrupt 시작 지연을 분리해 계산한다.

06 · DEEP DIVE

주요 동작 상세 분석

01

공통 커널 코드와 아키텍처별 함수의 역할 구분

clocksource는 연속 counter read와 mult/shift 변환을 제공하고 clockevent는 one-shot deadline interrupt를 제공한다. timekeeping seqlock이 base cycle과 base ns를 일관되게 읽으며 hrtimer가 가장 이른 만료를 device에 program한다.

counter read, timekeeper update, per-CPU clockevent program, interrupt handler와 timer callback을 나눈다. callback은 hardirq 또는 softirq context이므로 sleep 가능 여부가 다르다.

02

arm64: CNTVCT/CNTPCT counter와 CNTV_TVAL/CVAL comparator

architected timer frequency CNTFRQ를 기준으로 virtual 또는 physical counter를 clocksource로 등록한다. per-CPU PPI가 comparator 만료를 알리고 TVAL 또는 CVAL에 다음 event를 쓴다.

ISB로 counter read ordering을 맞추고 erratum workaround가 alternative read path를 선택할 수 있다. 디버깅할 때는 CNTFRQ, CNTVCT, CVAL/TVAL, control enable/mask/status, PPI affinity와 clocksource rating을 본다.

03

x86-64: TSC clocksource와 local APIC TSC-deadline/one-shot timer

invariant/nonstop TSC가 충분히 신뢰되면 RDTS(C/P)가 clocksource가 되고 CPUID/calibration으로 frequency를 구한다. local APIC timer는 TSC-deadline MSR 또는 divide-count mode로 per-CPU event를 만든다.

RDTSCP/LFENCE ordering, TSC synchronization과 frequency scaling invariant 여부를 검증해야 한다. 디버깅할 때는 CPUID invariant bit, tsc_khz, clocksource watchdog, IA32_TSC_DEADLINE, APIC LVT와 per-CPU offset을 확인한다.

04

RISC-V: TIME CSR/SBI와 Sstc STIMECMP comparator

일반 S-mode 경로에서 time_init()은 ACPI를 쓰지 않으면 DT /cpus의 timebase-frequency, ACPI를 쓰면 RHCT의 time_base_freq를 riscv_timebase에 저장한다. timer_probe()가 시작한 RISC-V timer driver는 이 값으로 clocksource와 sched_clock을 각각 등록한다. clockevent는 Sstc가 있으면 STIMECMP에 직접 쓰고, 없으면 SBI set_timer를 요청한다.

RV32에서 64-bit time/comparator를 읽고 쓸 때 high-low-high 또는 safe write sequence로 torn value를 막아야 한다. 디버깅할 때는 DT timebase-frequency 또는 ACPI RHCT time_base_freq, TIME, STIMECMP/SBI call, SIE.STIE, timer pending과 clockevent min_delta를 본다.

05

객체를 사용하고 해제하는 주체와 시점을 확인한다

registered clocksource와 clock_event_device는 사용 CPU가 online인 동안 유지된다. CPU hotplug와 suspend는 device shutdown, counter continuity와 resume correction을 조정해야 한다.

주소나 레지스터 값만 확인하면 갱신되지 않은 상태를 놓칠 수 있다. 상태를 만드는 코드, 다른 코드에 전달하는 시점, 사용하는 코드, 해제하는 시점을 함께 기록한다.

06

최대 지연 시간은 명령 하나의 실행 시간만으로 결정되지 않는다

deadline error는 counter read/program 비용, hardware minimum delta, interrupt masking, firmware trap과 callback backlog의 합이다. tickless idle에서는 너무 먼 deadline과 counter wrap도 확인한다.

평균값뿐 아니라 인터럽트를 비활성화한 시간, 다른 CPU의 응답 시간, 펌웨어 호출 시간, 재시도 횟수를 따로 확인해야 최대 지연의 원인을 찾을 수 있다.

07 · FAILURE

실패를 어떤 증거로 나눌 것인가

분류관찰되는 결과첫 확인값
arm64CNTFRQ 불일치, unstable counter erratum 또는 comparator program 뒤 즉시 만료 race는 시간 역행과 interrupt storm을 만든다.CNTFRQ, CNTVCT, CVAL/TVAL, control enable/mask/status, PPI affinity와 clocksource rating을 본다.
x86-64socket 간 TSC offset, virtualization migration, 잘못된 calibration은 monotonicity 위반과 timer 조기/지연 만료를 만든다.CPUID invariant bit, tsc_khz, clocksource watchdog, IA32_TSC_DEADLINE, APIC LVT와 per-CPU offset을 확인한다.
RISC-Vfirmware trap latency, 잘못된 DT timebase-frequency/ACPI RHCT time_base_freq 또는 RV32 comparator write race가 긴 wakeup 지연이나 즉시 재interrupt를 만든다.DT timebase-frequency 또는 ACPI RHCT time_base_freq, TIME, STIMECMP/SBI call, SIE.STIE, timer pending과 clockevent min_delta를 본다.

08 · LAB

재현과 계측 절차

  1. clocksource read와 clockevent handler 시작 cycle을 동시에 기록해 program-to-interrupt latency histogram을 만든다.
  2. CPU idle, frequency scaling, VM migration 전후에 clocksource watchdog과 monotonicity를 확인한다.
  3. 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
  4. 소스만 읽고 끝내지 않고 최종 vmlinux의 objdump -dr, readelf -SW 결과로 선택된 alternative와 section 배치를 확인한다.

09 · REFERENCES

원문 위치

Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.