요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
Clockevent는 미래 시점에 interrupt를 만든다
timekeeping.rst:85-107Clockevent device는 periodic 또는 one-shot mode로 다음 event를 programming합니다. Per-CPU local timer가 있으면 scheduler tick과 hrtimer deadline을 각 CPU에 전달하고, broadcast device는 deep idle에서 local timer가 멈춘 CPU를 대신 깨웁니다.
Clocksource가 시간의 자 역할이라면 clockevent는 alarm입니다. 같은 hardware block이 두 기능을 제공할 수 있어도 kernel abstraction과 failure 조건은 분리됩니다.
sched_clock의 빠른 timestamp
timekeeping.rst:108-159sched_clock()은 scheduler와 tracing이 짧은 interval ordering을 기록하는 빠른 nanosecond timestamp입니다. 반드시 wall clock 정확도를 제공하는 API는 아니며 architecture에 따라 CPU 간 동기화와 wrap 특성이 다를 수 있습니다.
Scheduler는 이 값을 runtime accounting에 사용하므로 read overhead가 작아야 합니다. Trace 분석에서 서로 다른 CPU timestamp를 비교할 때 clock mode와 synchronization 조건을 확인합니다.
Delay timer는 calibration을 돕는다
timekeeping.rst:160-181일부 architecture는 delay loop 대신 constant-frequency hardware timer를 사용하여 udelay 정확도를 높입니다. Delay timer는 free-running read가 빠르고 interrupt나 dynamic frequency 변화의 영향을 덜 받아야 합니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
===========================================================
Clock sources, Clock events, sched_clock() and delay timers
===========================================================
This document tries to briefly explain some basic kernel timekeeping
abstractions. It partly pertains to the drivers usually found in
drivers/clocksource in the kernel tree, but the code may be spread out
across the kernel.
If you grep through the kernel source you will find a number of architecture-
specific implementations of clock sources, clockevents and several likewise
architecture-specific overrides of the sched_clock() function and some
delay timers.
To provide timekeeping for your platform, the clock source provides
the basic timeline, whereas clock events shoot interrupts on certain points
on this timeline, providing facilities such as high-resolution timers.
sched_clock() is used for scheduling and timestamping, and delay timers
provide an accurate delay source using hardware counters.
Clock sources
-------------
The purpose of the clock source is to provide a timeline for the system that
tells you where you are in time. For example issuing the command 'date' on
a Linux system will eventually read the clock source to determine exactly
what time it is.
Typically the clock source is a monotonic, atomic counter which will provide
n bits which count from 0 to (2^n)-1 and then wraps around to 0 and start over.
It will ideally NEVER stop ticking as long as the system is running. It
may stop during system suspend.
The clock source shall have as high resolution as possible, and the frequency
shall be as stable and correct as possible as compared to a real-world wall
clock. It should not move unpredictably back and forth in time or miss a few
cycles here and there.
It must be immune to the kind of effects that occur in hardware where e.g.
the counter register is read in two phases on the bus lowest 16 bits first
and the higher 16 bits in a second bus cycle with the counter bits
potentially being updated in between leading to the risk of very strange
values from the counter.
When the wall-clock accuracy of the clock source isn't satisfactory, there
are various quirks and layers in the timekeeping code for e.g. synchronizing
the user-visible time to RTC clocks in the system or against networked time
servers using NTP, but all they do basically is update an offset against
the clock source, which provides the fundamental timeline for the system.
These measures does not affect the clock source per se, they only adapt the
system to the shortcomings of it.
The clock source struct shall provide means to translate the provided counter
into a nanosecond value as an unsigned long long (unsigned 64 bit) number.
Since this operation may be invoked very often, doing this in a strict
mathematical sense is not desirable: instead the number is taken as close as
possible to a nanosecond value using only the arithmetic operations
multiply and shift, so in clocksource_cyc2ns() you find:
ns ~= (clocksource * mult) >> shift
You will find a number of helper functions in the clock source code intended
to aid in providing these mult and shift values, such as
clocksource_khz2mult(), clocksource_hz2mult() that help determine the
mult factor from a fixed shift, and clocksource_register_hz() and
clocksource_register_khz() which will help out assigning both shift and mult
factors using the frequency of the clock source as the only input.
For real simple clock sources accessed from a single I/O memory location
there is nowadays even clocksource_mmio_init() which will take a memory
location, bit width, a parameter telling whether the counter in the
register counts up or down, and the timer clock rate, and then conjure all
necessary parameters.
Since a 32-bit counter at say 100 MHz will wrap around to zero after some 43
seconds, the code handling the clock source will have to compensate for this.
That is the reason why the clock source struct also contains a 'mask'
member telling how many bits of the source are valid. This way the timekeeping
code knows when the counter will wrap around and can insert the necessary
compensation code on both sides of the wrap point so that the system timeline
remains monotonic.
Clock events
------------
Clock events are the conceptual reverse of clock sources: they take a
desired time specification value and calculate the values to poke into
hardware timer registers.
Clock events are orthogonal to clock sources. The same hardware
and register range may be used for the clock event, but it is essentially
a different thing. The hardware driving clock events has to be able to
fire interrupts, so as to trigger events on the system timeline. On an SMP
system, it is ideal (and customary) to have one such event driving timer per
CPU core, so that each core can trigger events independently of any other
core.
You will notice that the clock event device code is based on the same basic
idea about translating counters to nanoseconds using mult and shift
arithmetic, and you find the same family of helper functions again for
assigning these values. The clock event driver does not need a 'mask'
attribute however: the system will not try to plan events beyond the time
horizon of the clock event.
sched_clock()
-------------
In addition to the clock sources and clock events there is a special weak
function in the kernel called sched_clock(). This function shall return the
number of nanoseconds since the system was started. An architecture may or
may not provide an implementation of sched_clock() on its own. If a local
implementation is not provided, the system jiffy counter will be used as
sched_clock().
As the name suggests, sched_clock() is used for scheduling the system,
determining the absolute timeslice for a certain process in the CFS scheduler
for example. It is also used for printk timestamps when you have selected to
include time information in printk for things like bootcharts.
Compared to clock sources, sched_clock() has to be very fast: it is called
much more often, especially by the scheduler. If you have to do trade-offs
between accuracy compared to the clock source, you may sacrifice accuracy
for speed in sched_clock(). It however requires some of the same basic
characteristics as the clock source, i.e. it should be monotonic.
The sched_clock() function may wrap only on unsigned long long boundaries,
i.e. after 64 bits. Since this is a nanosecond value this will mean it wraps
after circa 585 years. (For most practical systems this means "never".)
If an architecture does not provide its own implementation of this function,
it will fall back to using jiffies, making its maximum resolution 1/HZ of the
jiffy frequency for the architecture. This will affect scheduling accuracy
and will likely show up in system benchmarks.
The clock driving sched_clock() may stop or reset to zero during system
suspend/sleep. This does not matter to the function it serves of scheduling
events on the system. However it may result in interesting timestamps in
printk().
The sched_clock() function should be callable in any context, IRQ- and
NMI-safe and return a sane value in any context.
Some architectures may have a limited set of time sources and lack a nice
counter to derive a 64-bit nanosecond value, so for example on the ARM
architecture, special helper functions have been created to provide a
sched_clock() nanosecond base from a 16- or 32-bit counter. Sometimes the
same counter that is also used as clock source is used for this purpose.
On SMP systems, it is crucial for performance that sched_clock() can be called
independently on each CPU without any synchronization performance hits.
Some hardware (such as the x86 TSC) will cause the sched_clock() function to
drift between the CPUs on the system. The kernel can work around this by
enabling the CONFIG_HAVE_UNSTABLE_SCHED_CLOCK option. This is another aspect
that makes sched_clock() different from the ordinary clock source.
Delay timers (some architectures only)
--------------------------------------
On systems with variable CPU frequency, the various kernel delay() functions
will sometimes behave strangely. Basically these delays usually use a hard
loop to delay a certain number of jiffy fractions using a "lpj" (loops per
jiffy) value, calibrated on boot.
Let's hope that your system is running on maximum frequency when this value
is calibrated: as an effect when the frequency is geared down to half the
full frequency, any delay() will be twice as long. Usually this does not
hurt, as you're commonly requesting that amount of delay *or more*. But
basically the semantics are quite unpredictable on such systems.
Enter timer-based delays. Using these, a timer read may be used instead of
a hard-coded loop for providing the desired delay.
This is done by declaring a struct delay_timer and assigning the appropriate
function pointers and rate settings for this delay timer.
This is available on some architectures like OpenRISC or ARM.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
Kernel timekeeping의 네 구성 요소
1-19이 문서는 clocksource, clock event, sched_clock(), delay timer라는 기본 kernel timekeeping abstraction을 설명한다. 관련 driver는 주로 drivers/clocksource에 있지만 code는 kernel 전반에 흩어져 있을 수 있고 architecture별 구현과 override가 많다.
- Clocksource는 system의 기본 time line을 제공한다.
- Clock event는 그 time line의 특정 시점에 interrupt를 발생시켜 high-resolution timer 같은 기능을 가능하게 한다.
- sched_clock()은 scheduling과 timestamp에 사용한다.
- Delay timer는 hardware counter를 이용해 정확한 delay source를 제공한다.
Clocksource counter의 요구 사항
22-52Clocksource는 system이 현재 time line의 어디에 있는지 알려 준다. 예를 들어 date command가 보여 주는 시각도 최종적으로 clocksource를 읽은 결과를 바탕으로 계산한다.
일반적인 clocksource는 0부터 2^n-1까지 증가한 뒤 0으로 wrap하는 n-bit monotonic atomic counter다. System이 실행되는 동안 이상적으로 절대 멈추지 않아야 하지만 suspend 중에는 멈출 수 있다.
Resolution은 가능한 한 높고 frequency는 실제 wall clock과 비교해 안정적이고 정확해야 한다. 예측할 수 없이 앞뒤로 이동하거나 cycle을 누락해서는 안 된다.
Hardware bus가 counter register의 아래 16 bit와 위 16 bit를 서로 다른 cycle에 읽고 그 사이 counter가 갱신되어 비정상적인 조합이 만들어지는 문제 같은 read tearing에도 안전해야 한다.
Clocksource의 wall-clock 정확도가 부족하면 timekeeping layer가 RTC 또는 NTP server에 맞춰 사용자에게 보이는 time offset을 조정한다. 이는 fundamental timeline을 제공하는 clocksource 자체를 바꾸는 것이 아니라 단점을 보상하는 것이다.
Cycle을 nanosecond로 변환하는 mult와 shift
54-74Clocksource structure는 counter 값을 unsigned long long, 즉 unsigned 64-bit nanosecond 값으로 변환할 수 있어야 한다. 매우 자주 호출되므로 매번 정확한 division을 수행하지 않고 multiply와 shift만으로 가능한 한 가까운 값을 계산한다.
ns ~= (clocksource * mult) >> shift
clocksource_khz2mult()와 clocksource_hz2mult()는 고정된 shift에서 mult를 구한다. clocksource_register_hz()와 clocksource_register_khz()는 clocksource frequency만 입력받아 shift와 mult를 모두 정하는 등록을 돕는다.
하나의 I/O memory location에서 읽는 단순한 clocksource는 clocksource_mmio_init()을 사용할 수 있다. Memory address, bit width, counter가 up/down 중 어느 방향으로 세는지, timer clock rate를 받아 필요한 parameter를 구성한다.
Counter wrap과 mask
76-82100 MHz의 32-bit counter는 약 43초 뒤 0으로 wrap한다. Clocksource code는 이를 보정해야 하므로 structure의 mask member에 source에서 유효한 bit 수를 기록한다. Timekeeping code는 wrap 시점을 알고 경계 전후에 필요한 보정을 적용해 system timeline을 계속 monotonic하게 유지한다.
Clock event device
85-105Clock event는 개념적으로 clocksource의 반대다. 원하는 시각을 입력받아 hardware timer register에 기록할 값을 계산한다.
Clock event와 clocksource는 서로 독립적인 abstraction이다. 같은 hardware와 register 범위를 사용할 수 있어도 역할은 다르다. Clock event hardware는 system timeline에서 event를 발생시키도록 interrupt를 만들 수 있어야 한다.
SMP system에서는 CPU core마다 하나의 event-driving timer를 두어 각 core가 다른 core와 독립적으로 event를 발생시키는 구성이 이상적이며 일반적이다.
Clock event도 mult와 shift arithmetic으로 counter와 nanosecond를 변환하며 같은 helper family를 사용한다. 다만 system이 clock event의 시간 범위를 넘어서는 event를 계획하지 않으므로 mask attribute는 필요하지 않다.
sched_clock()의 속도와 monotonicity
108-144sched_clock()은 system 시작 뒤 경과한 nanosecond를 반환하는 weak function이다. Architecture가 자체 구현을 제공할 수 있고 제공하지 않으면 system jiffy counter를 사용한다.
이 값은 CFS scheduler에서 process의 absolute timeslice를 계산하는 등 scheduling에 쓰이고, printk에 time 정보를 넣었을 때 bootchart 같은 log timestamp에도 사용한다.
Scheduler가 매우 자주 호출하므로 sched_clock()은 clocksource보다 훨씬 빨라야 한다. Clocksource 수준의 정확도와 속도를 동시에 얻을 수 없다면 정확도를 일부 희생할 수 있지만 monotonic해야 한다는 기본 성질은 유지해야 한다.
sched_clock()은 unsigned long long의 64-bit 경계에서만 wrap할 수 있다. Nanosecond 단위라면 약 585년 뒤이므로 실용적인 system에서는 사실상 wrap하지 않는다.
Architecture 구현이 없어 jiffy로 fallback하면 최대 resolution은 1/HZ가 되어 scheduling 정확도가 떨어지고 benchmark에도 나타날 수 있다. sched_clock source는 suspend나 sleep 중 멈추거나 0으로 reset되어도 scheduling 목적에는 문제가 없지만 printk timestamp가 특이하게 보일 수 있다.
sched_clock()은 어떤 context에서도 호출할 수 있어야 하며 IRQ-safe, NMI-safe하고 항상 타당한 값을 반환해야 한다.
Architecture별 counter와 SMP synchronization
146-157일부 architecture는 사용할 time source가 제한적이고 64-bit nanosecond 값을 직접 만들 counter가 없다. ARM은 16-bit 또는 32-bit counter에서 sched_clock nanosecond base를 제공하는 helper를 두며 clocksource와 같은 counter를 사용하기도 한다.
SMP 성능을 위해 sched_clock()은 synchronization 비용 없이 각 CPU에서 독립적으로 호출할 수 있어야 한다. x86 TSC 같은 hardware에서는 CPU 사이 값이 drift할 수 있다. Kernel은 CONFIG_HAVE_UNSTABLE_SCHED_CLOCK을 enable해 이를 보정할 수 있으며 이 점도 일반 clocksource와의 차이다.
CPU frequency 변화와 hardware delay timer
160-180CPU frequency가 변하는 system에서는 kernel delay() function이 예상과 다르게 동작할 수 있다. 전통적인 delay는 boot 때 보정한 lpj, 즉 loops per jiffy를 이용해 일정 횟수의 hard loop를 실행한다.
LpJ를 최대 frequency에서 보정한 뒤 CPU frequency가 절반으로 낮아지면 같은 delay()가 두 배 오래 걸린다. 보통 요청한 시간 이상 기다리는 동작이라 치명적이지는 않지만 semantic은 예측하기 어렵다.
Timer-based delay는 고정 loop 대신 hardware timer를 읽어 원하는 delay를 제공한다. struct delay_timer를 선언하고 적절한 function pointer와 rate를 설정해 사용한다. 이 방식은 OpenRISC와 ARM 같은 일부 architecture에서 제공된다.
Clocksource는 현재 시간을 읽는다
timekeeping.rst:1-84Clocksource는 일정한 주기로 증가하는 free-running counter를 읽어 경과 시간을 계산합니다. Driver는 read callback, mask, frequency에서 계산한 mult와 shift, rating과 stability flag를 등록합니다. Timekeeping core는 cycle delta를 nanosecond로 변환하고 wraparound 이전에 주기적으로 값을 누적합니다.
좋은 clocksource는 monotonic하고 CPU·power state 변화에 안정적이며 모든 CPU에서 일관되게 읽혀야 합니다. Rating이 높아도 watchdog 비교에서 불안정하다고 판정되면 다른 source로 전환될 수 있습니다.