在linux中開發引用,timerfd和posix timer是最常用的定時器。timerfd是linux特有的定時器,通過fd來實現定時器,體現了linux"一切皆文件"的思想;posix timer,只要符合posix標準的操作系統,均應支持。
在開發確定性應用時,需要選用確定性的定時器。搜索網絡上的一些資料,往往說這兩種定時器的精度可以達到納秒級。但是在實際測試中,尤其是在壓測時,即使linux打了實時補丁,兩種定時器的抖動也會比較大,可以達到700μs,甚至更大。本文分析在壓力情況下,timerfd和posix timer抖動大的原因。
1timerfd
#include <sys/timerfd.h>
#include <pthread.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <inttypes.h>#define NS_PER_US 1000
#define US_PER_SEC 1000000
#define TIMER_INTERVAL_US 10000
#define SAMPLE_COUNT 1000typedef struct {uint64_t max_delta; // 最大偏差(μs)uint64_t min_delta; // 最小偏差(μs)uint64_t total_delta; // 偏差總和(μs)uint32_t count; // 實際采樣計數
} TimerStats;void* timerfd_monitor_thread(void* arg) {int timer_fd = *(int*)arg;TimerStats stats = {0, UINT64_MAX, 0, 0};struct timespec prev_ts = {0, 0};uint64_t expirations;struct sched_param param;param.sched_priority = 30;pthread_t current_thread = pthread_self();pthread_setschedparam(current_thread, SCHED_FIFO, ¶m);// 第一次讀取(建立基準)if (read(timer_fd, &expirations, sizeof(expirations)) > 0) {clock_gettime(CLOCK_MONOTONIC, &prev_ts);}while (stats.count < SAMPLE_COUNT) {int ret = read(timer_fd, &expirations, sizeof(expirations));if (ret > 0) {struct timespec curr_ts;clock_gettime(CLOCK_MONOTONIC, &curr_ts);uint64_t interval_us = (curr_ts.tv_sec - prev_ts.tv_sec) * US_PER_SEC +(curr_ts.tv_nsec - prev_ts.tv_nsec) / NS_PER_US;if (prev_ts.tv_sec != 0) {int64_t delta = (int64_t)interval_us - TIMER_INTERVAL_US;uint64_t abs_delta = llabs(delta);if (abs_delta > stats.max_delta) stats.max_delta = abs_delta;if (abs_delta < stats.min_delta) stats.min_delta = abs_delta;stats.total_delta += abs_delta;stats.count++;}prev_ts = curr_ts;}}TimerStats* result = (TimerStats *)malloc(sizeof(TimerStats));memcpy(result, &stats, sizeof(TimerStats));return result;
}int main() {int timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);if (timer_fd == -1) {perror("timerfd_create");exit(EXIT_FAILURE);}struct itimerspec timer_spec = {.it_interval = {.tv_sec = 0, .tv_nsec = TIMER_INTERVAL_US * NS_PER_US},.it_value = {.tv_sec = 0, .tv_nsec = 1}};if (timerfd_settime(timer_fd, 0, &timer_spec, NULL) == -1) {perror("timerfd_settime");close(timer_fd);exit(EXIT_FAILURE);}pthread_t monitor_thread;if (pthread_create(&monitor_thread, NULL, timerfd_monitor_thread, &timer_fd)) {perror("pthread_create");close(timer_fd);exit(EXIT_FAILURE);}TimerStats* stats;pthread_join(monitor_thread, (void**)&stats);printf("max: %" PRIu64 "\n", stats->max_delta);printf("min: %" PRIu64 "\n", stats->min_delta);printf("avg: %.2f\n", (double)stats->total_delta / stats->count);free(stats);close(timer_fd);return 0;
}
2posix timer
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <math.h>#define _GNU_SOURCE
#include <sys/types.h>// 全局統計數據結構
volatile struct {long long max_jitter; // 最大抖動(納秒)long long min_jitter; // 最小抖動(納秒)long long total_jitter; // 抖動累計值(納秒)long long count; // 觸發次數統計struct timespec prev_ts; // 上一次觸發時間戳
} stats = { .min_jitter = 100000 };// 信號處理函數
void sig_handler(int a, siginfo_t *b, void *c) {struct timespec curr_ts;clock_gettime(CLOCK_MONOTONIC, &curr_ts);// 第一次觸發時只記錄時間戳不計算抖動if (stats.count > 0) {// 計算實際時間差(納秒)long long actual_interval = (curr_ts.tv_sec - stats.prev_ts.tv_sec) * 1000000000LL+ (curr_ts.tv_nsec - stats.prev_ts.tv_nsec);// 計算抖動(實際間隔 - 設定周期10ms)const long long expected_interval = 10 * 1000000LL; // 10ms in nslong long jitter = actual_interval - expected_interval;// 更新統計值stats.total_jitter += llabs(jitter);if (llabs(jitter) > stats.max_jitter) stats.max_jitter = llabs(jitter);if (llabs(jitter) < stats.min_jitter) stats.min_jitter = llabs(jitter);}// 更新狀態stats.prev_ts = curr_ts;stats.count++;
}void create_timer(int signum, int period_in_ms, void (*cb)(int, siginfo_t*, void*), timer_t* timerid) {struct sigaction sa;sa.sa_flags = SA_SIGINFO;sa.sa_sigaction = cb;sigemptyset(&sa.sa_mask);sigaction(signum, &sa, NULL);sigevent_t event;event.sigev_notify = SIGEV_THREAD_ID;event.sigev_signo = signum;event._sigev_un._tid = syscall(SYS_gettid);event.sigev_value.sival_ptr = NULL;timer_create(CLOCK_MONOTONIC, &event, timerid);struct itimerspec its;its.it_interval.tv_sec = period_in_ms / 1000;its.it_interval.tv_nsec = (period_in_ms % 1000) * 1000000;its.it_value.tv_sec = 0;its.it_value.tv_nsec = 1; // 立即啟動timer_settime(*timerid, 0, &its, NULL);
}void* thread_func(void* arg) {struct sched_param param;param.sched_priority = 30;pthread_t current_thread = pthread_self();pthread_setschedparam(current_thread, SCHED_FIFO, ¶m);timer_t timerid;create_timer(34, 10, sig_handler, &timerid);while(stats.count < 1000) {usleep(20000);}// 取消定時器timer_delete(timerid);// 打印統計結果printf("\n--- Timer Jitter Statistics ---\n");printf("Samples collected: %lld\n", stats.count - 1); // 有效樣本數printf("Max jitter: %lld ns (%.3f ms)\n", stats.max_jitter, stats.max_jitter / 1000000.0);printf("Min jitter: %lld ns\n", stats.min_jitter);printf("Avg jitter: %.2f ns (%.3f ms)\n",(double)stats.total_jitter / (stats.count - 1),(double)stats.total_jitter / (stats.count - 1) / 1000000.0);return NULL;
}int main() {pthread_t thread;pthread_create(&thread, NULL, thread_func, NULL);pthread_join(thread, NULL);return 0;
}
3抖動大的原因
3.1timerfd和posix timer均通過內核的hrtimer來實現
timerfd:
SYSCALL_DEFINE2(timerfd_create, int, clockid, int, flags)
{...hrtimer_setup(&ctx->t.tmr, timerfd_tmrproc, clockid, HRTIMER_MODE_ABS);...return ufd;
}
posix timer:
posix timer創建定時器,最終會調用函數common_timer_create來創建。
static int common_timer_create(struct k_itimer *new_timer)
{hrtimer_setup(&new_timer->it.real.timer, posix_timer_fn, new_timer->it_clock, 0);return 0;
}
3.2hrtimer通過軟中斷來處理
在函數raise_timer_softirq中喚醒軟中斷處理線程。而軟中斷處理線程的優先級是默認優先級,即SCHED_OTHER,nice值為0,這樣在cpu加壓情況下,軟中斷處理線程的抖動就是完全不可預期的,進而引起定時器抖動。
軟中斷處理線程中不僅僅是處理HRTIMER這一種軟中斷,還有NET_RX、NET_TX等,會進一步加劇抖動。
? ? ? ? ? ? ? ? ? ? CPU0 ? ? ? CPU1 ? ? ? CPU2 ? ? ? CPU3 ? ? ? CPU4 ? ? ? CPU5 ? ? ? CPU6 ? ? ? CPU7 ? ? ? CPU8 ? ? ? CPU9 ? ? ? CPU10 ? ? ?CPU11
HI: ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?2 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0
TIMER: ? ?2923684 ? ?1037158 ? ?2312782 ? 15780522 ? ?2292706 ? 12511455 ? ?2021456 ? ?1911912 ? ?2777271 ? ?2135993 ? ? ? ? ?1 ? ? ? ? ?1
NET_TX: ? ? ? ? ?1 ? ? ? ? ?1 ? ? ? ? ?1 ? ? ? ? ?3 ? ? ? ? ?3 ? ? ? ? ?1 ? ? ? ? ?0 ? ? ? ? ?3 ? ? ? ? ?4 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0
NET_RX: ?171182484 ? ? 572625 ? ? 467634 ? ? 576342 ? ? 322601 ? ? 338762 ? ? 298284 ? ? 354496 ? ? 223455 ? ? 165924 ? ? ? ? ?0 ? ? ? ? ?0
BLOCK: ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0
IRQ_POLL: ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0
TASKLET: ? ? 124653 ? ? ?97476 ? ? ?70810 ? ? 143113 ? ? ?50785 ? ? ?83217 ? ? ?65544 ? ? ?75319 ? ? ?20208 ? ? ?15287 ? ? ? ? ?0 ? ? ? ? ?0
SCHED: ? 80111744 ? 12500903 ? ?3502611 ? 13362108 ? ?2612070 ? 10030360 ? ?2319687 ? ?2160967 ? ?2852872 ? ?2370172 ? ? ? ? ?0 ? ? ? ? ?0
HRTIMER: ? ?7245889 ? ?3010058 ? ?2493355 ? ?3392701 ? ?1950261 ? ?1208348 ? ?1013954 ? ?1105595 ? ?1078221 ? ? 983678 ? ? ? ? ?0 ? ? ? ? ?0
RCU: ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ?0
void hrtimer_interrupt(struct clock_event_device *dev)
{...if (!ktime_before(now, cpu_base->softirq_expires_next)) {cpu_base->softirq_expires_next = KTIME_MAX;cpu_base->softirq_activated = 1;raise_timer_softirq(HRTIMER_SOFTIRQ);}....
}
inline void raise_softirq_irqoff(unsigned int nr)
{__raise_softirq_irqoff(nr);/** If we're in an interrupt or softirq, we're done* (this also catches softirq-disabled code). We will* actually run the softirq once we return from* the irq or softirq.** Otherwise we wake up ksoftirqd to make sure we* schedule the softirq soon.*/if (!in_interrupt())wakeup_softirqd();
}
3.3ktimers線程
在linux 6.x中引入了ktimers線程,引入該線程的目的是將HRTIMR這一軟中斷從softirq中隔離出來。使用專門的線程來處理hrtimer軟中斷。ktimers線程的調度策略是SCHED_FIFO實時調度策略,優先級是1。使用專門的線程來處理HRTIMER軟中斷,并且該線程還是實時調度策略,在很大程度上降低了定時器的抖動。
#ifdef CONFIG_IRQ_FORCED_THREADING
static void ktimerd_setup(unsigned int cpu)
{/* Above SCHED_NORMAL to handle timers before regular tasks. */sched_set_fifo_low(current);
}static int ktimerd_should_run(unsigned int cpu)
{return local_timers_pending_force_th();
}void raise_ktimers_thread(unsigned int nr)
{trace_softirq_raise(nr);__this_cpu_or(pending_timer_softirq, BIT(nr));
}static void run_ktimerd(unsigned int cpu)
{unsigned int timer_si;ksoftirqd_run_begin();timer_si = local_timers_pending_force_th();__this_cpu_write(pending_timer_softirq, 0);or_softirq_pending(timer_si);__do_softirq();ksoftirqd_run_end();
}static struct smp_hotplug_thread timer_thread = {.store = &ktimerd,.setup = ktimerd_setup,.thread_should_run = ktimerd_should_run,.thread_fn = run_ktimerd,.thread_comm = "ktimers/%u",
};
#endif
如果條件滿足,則喚醒ktimers線程:
static inline void raise_timer_softirq(unsigned int nr)
{lockdep_assert_in_irq();if (force_irqthreads())raise_ktimers_thread(nr);else__raise_softirq_irqoff(nr);
}
在開啟實時內核,并且打開配置CONFIG_IRQ_FORCED_THREADING的條件下,才會啟用ktimers線程。
#ifdef CONFIG_IRQ_FORCED_THREADING
# ifdef CONFIG_PREEMPT_RT
# define force_irqthreads() (true)
...
在中斷返回函數里,會直接喚醒ktimers線程。
static inline void __irq_exit_rcu(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLEDlocal_irq_disable();
#elselockdep_assert_irqs_disabled();
#endifaccount_hardirq_exit(current);preempt_count_sub(HARDIRQ_OFFSET);if (!in_interrupt() && local_softirq_pending())invoke_softirq();if (IS_ENABLED(CONFIG_IRQ_FORCED_THREADING) && force_irqthreads() &&local_timers_pending_force_th() && !(in_nmi() | in_hardirq()))wake_timersd();tick_irq_exit();
}