The period is set to 100 ms, but the actual time per round is about 130 ms; after cancellation returns, the callback appears again. The former is the time base, and the latter is the lifetime protocol.
Target
Reading Location
Timing and event hardware
Sections 1–3
Cancellation races and period drift
Sections 4–5
tick and timerfd experiments
Sections 6–7
The clock tells the kernel where time has reached, the timer records what should happen in the future, and the scheduler decides when the woken-up tasks actually run. The latency of these three stages must be separated; you cannot guarantee that an application will wake up with nanosecond error just because clock_getres returns nanosecond resolution.
1. Clocksource and clockevent
clocksource provides a readable counter. The kernel updates time based on the counter difference, mask, and conversion parameters; the counter frequency does not necessarily equal the current CPU execution frequency.
clockevent is responsible for generating notifications in the future. set_next_event() receives the delta in the device's corresponding count units, not all devices uniformly receive nanoseconds; interfaces like set_next_ktime() can use direct time values. The core is responsible for conversion and constraints such as device minimum/maximum ranges.
The same hardware device may support both responsibilities, or they may be handled by different hardware. For example, a readable TSC does not directly equal a traditional APIC timer; mechanisms like TSC deadline provide event capabilities based on corresponding counts. When a CPU is deeply idle, local event devices may need a broadcast mechanism for assistance. See timekeeping hardware abstraction.
2. Jiffies, HZ, and Wraparound
jiffies is the kernel's coarse-grained time counter, and HZ specifies the related tick scale. A tickless system still maintains logical time and does not require each CPU to receive exactly HZ timer interrupts per second.
getconf CLK_TCK returns the timekeeping unit for the user-space interface; you cannot use it to directly read CONFIG_HZ. When HZ=250, one jiffy is 4 ms, which does not mean all hrtimers can only be programmed in 4 ms increments.
With a 32-bit unsigned long, 2^32 counts wrap around in approximately 497 days at HZ=100, and in approximately 49.7 days at HZ=1000. Expiry comparisons should use macros like time_after/time_before, and adhere to the corresponding distance ranges; you cannot use jiffies > expires as a portable timeout check. You also cannot infinitely rely on half-range difference comparisons for very distant future intervals.
Use APIs like msecs_to_jiffies() for millisecond conversions to avoid truncating short delays to zero via manual integer division, or ignoring overflow and upper limits.
3. Why Timer Wheel and hrtimer Coexist
Mechanism
Organization and Purpose
Time Semantics
timer_list / timer wheel
Hierarchical buckets, suitable for scenarios with many timeouts and frequent early cancellations
jiffies scale; distant expiries can use coarser granularity
hrtimer
Organized by expiry value for high-precision time events
ktime/ns representation; actual precision depends on event hardware and configuration
Timers are not "appointments that must execute precisely at expiry." IRQ disabling, CPU busy, thread priority, and VM suspension can all cause processing delays; high precision mainly improves the ability to schedule expiries, it does not directly provide hard real-time upper bounds.
Standard timer callbacks usually execute under TIMER_SOFTIRQ semantics and cannot sleep arbitrarily. The hard/soft mode of hrtimers and the PREEMPT_RT configuration affect the actual callback context; you cannot uniformly say "hrtimer is always in HRTIMER_SOFTIRQ" or "always in hardirq."
If blocking work needs to be executed, the callback can schedule an appropriate standard workqueue or wake up a task. schedule_hrtimeout() is an interface for the caller to wait; it does not convert a callback already in atomic context into a sleepable function. See hrtimer design.
4. Lifetime Issues in Initialization, Cancellation, and Rearming
New code should use timer_setup() to initialize timer_list; the current kernel provides hrtimer_setup() to initialize hrtimers, while older targets may still require legacy initialization interfaces, so you must check the target headers.
Action
What it Solves
What it Doesn't Solve
Delete pending timer
Revokes queued expiry events
The callback may have already started
Synchronous delete
Waits for current activity to end
Other paths may still rearm
Prevent rearming
Closes future trigger sources
The object still needs to stay alive until the last accessor exits
Therefore, the exit protocol must coordinate queued state, current callbacks, and future accessors simultaneously.
Modern timer_list uses timer_shutdown_sync() for exit scenarios requiring synchronous stop and prevention of rearming, especially when timers and work reschedule each other. The names del_timer/del_timer_sync in old code and the current timer_delete series should be checked against the target version; do not comment standard del_timer as "synchronous delete."
Even with shutdown, you must prevent future paths from accessing the released timer object; "rearming a live but shut-down timer is ignored" does not mean "you can call APIs on a dangling timer pointer." hrtimer_cancel also needs to coordinate with publishers/rearmer.
Do not hold locks required by the callback during cancellation
If a thread holds lock L and calls synchronous cancellation, and the callback has already started and is waiting for L, both sides will wait for each other. The exit should first stop new submission paths, synchronously wait without holding mutually dependent locks, and then release the timer, work, and their shared context.
5. Which Base to Use for Periodic Tasks
Assume a period of 100 ms and a processing time of 30 ms per round, comparing the start times of previous rounds:
Scheduling Method
Round 1
Round 2
Round 3
Wait 100 ms after each round ends
0
130 ms
260 ms
Schedule according to t0 + n × 100 ms
0
100 ms
200 ms
Here, other scheduling delays are ignored. An absolute base avoids repeatedly adding working time into the period, but after an actual timeout, you still need to decide whether to catch up, skip, or merge.
Interfaces like hrtimer_forward help advance the expiry point and report crossed periods; they only restart according to the corresponding rules when HRTIMER_RESTART is returned; they do not automatically execute a full business callback for each missed period.
User-space can also use clock_nanosleep(..., TIMER_ABSTIME, ...) or timerfd absolute time mode to reduce relative sleep drift. Choosing MONOTONIC, BOOTTIME, or REALTIME determines how time synchronization and suspension affect deadlines.
hrtimer_forward advances deadlines and reports overruns; HRTIMER_RESTART requests restarting under its rules. It does not execute one business callback for every missed period. Userspace can use TIMER_ABSTIME with clock_nanosleep or timerfd; the clock choice determines suspend and wall-clock adjustment semantics.
tnrelative=n(P+C+J)+J,tnabsolute=nP+J
n starts at zero; P is the period, C callback work, and J a fixed dispatch delay. This parameter range does not overload the absolute schedule; real jitter need not be constant.
Preparing the visual
How does a small extra delay become cumulative drift?
Waiting a period after completion repeatedly adds work and delay to the next baseline. Fixed absolute phase avoids accumulating those terms, but each release can still be late.
{"id":"timer-phase-and-drift","title":"How does a small extra delay become cumulative drift?","summary":"Waiting a period after completion repeatedly adds work and delay to the next baseline. Fixed absolute phase avoids accumulating those terms, but each release can still be late.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"work\" data-i18n=\"work\"></label><output for=\"work\" id=\"work-value\"></output><input id=\"work\" type=\"range\" min=\"0\" max=\"40\" value=\"30\" step=\"1\"></div><div class=\"control\"><label for=\"delay\" data-i18n=\"delay\"></label><output for=\"delay\" id=\"delay-value\"></output><input id=\"delay\" type=\"range\" min=\"0\" max=\"40\" value=\"0\" step=\"1\"></div><p class=\"caption\" data-i18n=\"relative\"></p><div class=\"plot\"><div class=\"plot-row\"><i id=\"relative-0\"></i><b id=\"relative-label-0\"></b></div><div class=\"plot-row\"><i id=\"relative-1\"></i><b id=\"relative-label-1\"></b></div><div class=\"plot-row\"><i id=\"relative-2\"></i><b id=\"relative-label-2\"></b></div><div class=\"plot-row\"><i id=\"relative-3\"></i><b id=\"relative-label-3\"></b></div><div class=\"plot-row\"><i id=\"relative-4\"></i><b id=\"relative-label-4\"></b></div><div class=\"plot-row\"><i id=\"relative-5\"></i><b id=\"relative-label-5\"></b></div></div><div class=\"axis\"><span>0</span><span>500</span><span>1000 ms</span></div><p class=\"caption\" data-i18n=\"absolute\"></p><div class=\"plot\"><div class=\"plot-row\"><i id=\"absolute-0\"></i><b id=\"absolute-label-0\"></b></div><div class=\"plot-row\"><i id=\"absolute-1\"></i><b id=\"absolute-label-1\"></b></div><div class=\"plot-row\"><i id=\"absolute-2\"></i><b id=\"absolute-label-2\"></b></div><div class=\"plot-row\"><i id=\"absolute-3\"></i><b id=\"absolute-label-3\"></b></div><div class=\"plot-row\"><i id=\"absolute-4\"></i><b id=\"absolute-label-4\"></b></div><div class=\"plot-row\"><i id=\"absolute-5\"></i><b id=\"absolute-label-5\"></b></div></div><div class=\"axis\"><span>0</span><span>500</span><span>1000 ms</span></div><div class=\"metrics\"><div><span data-i18n=\"relError\"></span><strong id=\"relError\"></strong></div><div><span data-i18n=\"absError\"></span><strong id=\"absError\"></strong></div></div><div class=\"formula\" id=\"arithmetic\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.plot-row b{font-size:10px;padding:2px 4px;line-height:1.4;position:absolute;right:3px;top:6px}.plot-row:nth-child(odd){background:var(--accent-soft)}#arithmetic{min-height:5em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}function draw(){const w=+$('#work').value,j=+$('#delay').value;set('work-value',w+' ms');set('delay-value',j+' ms');for(let n=0;n<6;n++){for(const [k,start]of [['relative',n*(100+w+j)+j],['absolute',n*100+j]]){const e=$('#'+k+'-'+n);e.style.left=start/10+'%';e.style.width=w/10+'%';e.style.opacity=w===0?.45:1;set(k+'-label-'+n,n+': '+start+' ms');}}set('relError',5*(w+j)+j);set('absError',j);set('arithmetic','s₅ − 5P = 5 × ('+w+' + '+j+') + '+j+' = '+(5*(w+j)+j)+' ms\\na₅ − 5P = '+j+' ms');set('explanation',t('note'));}$('#work').oninput=$('#delay').oninput=draw;draw();","audio":false,"strings":{"absError":"Absolute schedule phase error / ms","absolute":"Fixed phase nP: same scale","delay":"Start delay J each release / ms","heading":"How does a small extra delay become cumulative drift?","intro":"Adjust work time W and per-release start delay J. Compare six start times on a common scale: the background is the time range, and solid bars show actual work.","note":"Both schedules include start delay J. The difference is whether the next baseline is the original phase or the previous completion; absolute timers do not eliminate scheduling delay.","relError":"Sixth release phase error / ms","relative":"Wait P after completion: one release per row","reset":"Reset","scope":"P=100 ms, n=0…5, t0=0. Each release has fixed delay J and work W, with W+J<P, avoiding overrun policy ambiguity. Relative starts sₙ=n(P+W+J)+J; absolute starts aₙ=nP+J. These are not measured latency or Linux upper bounds.","scopeLabel":"Model scope and assumptions","work":"Work time W / ms"}}
6. NO_HTZ Reduces Ticks, But Does Not Eliminate All Interrupts
NO_HZ_IDLE stops periodic scheduling ticks under suitable idle conditions, waking up based on the next event that needs processing. NO_HZ_FULL further reduces ticks during running tasks on selected CPUs and under load conditions that meet requirements, but there are still maintenance tasks, RCU, timers, and device interrupts.
"Only one runnable task" is one of the relevant conditions; it does not guarantee that there will never be ticks under any configuration. Housekeeping CPU and task/IRQ affinity must also be coordinated; nohz_full is not a complete CPU isolation configuration.
cpuidle's state*/disable indicates whether certain idle states are allowed; you cannot directly judge the current tick mode. You should combine kernel configuration, boot parameters, and actual tracing. See NO_HZ documentation.
7. POSIX timer, itimer, and timerfd
Wall-clock POSIX timers like CLOCK_MONOTONIC/REALTIME and CPU-time timers have different timekeeping sources; you cannot claim that all timer_create calls directly correspond to the same type of hrtimer. Notification mechanisms such as SIGEV_SIGNAL, SIGEV_NONE, and library-involved SIGEV_THREAD also differ.
setitimer's REAL times based on elapsed time, VIRTUAL based on user CPU time, and PROF based on user and kernel CPU time; CPU-time timers should not be treated as standard sleep timeouts. Signal merging, overrun, and processing delays will also affect application observation.
timerfd notifies via fd readability; read gets a 64-bit cumulative expiry count, not a timestamp, and does not guarantee that each poll readability corresponds to exactly one expiry. The following program only creates a MONOTONIC timer for this process, waits for about 75 ms, and then reads the cumulative count for a 20 ms period.
cc -std=c11 -O2 -Wall -Wextra -Werror timer-count.c -o timer-count./timer-count
The typical result is 3, but it may be higher due to scheduling delays; you cannot assert that it is always exactly 3. Closing the fd reclaims the timer; this example does not modify the system clock. See timerfd for details.