当前位置: 首页 > news >正文

linux下timerfd和posix timer为什么存在较大的抖动?

在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, &param);// 第一次读取(建立基准)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, &param);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();
}
http://www.dtcms.com/a/336387.html

相关文章:

  • USB-A 3.2 和 USB-A 2.0的区别
  • 集成电路学习:什么是ORB方向性FAST和旋转BRIEF
  • 外贸电商选品方案的模型
  • 天地图应用篇: 增加缩放、比例尺控件
  • 集运业务突围:三大关键问题的智能化解决方案
  • 【数据结构与算法-Day 16】队列的应用:广度优先搜索(BFS)的基石与迷宫寻路实战
  • vulnhub-lampiao靶机渗透
  • 002.Redis 配置及数据类型
  • 安装pytorch3d后报和本机cuda不符
  • LLM、RAG、Agent知识点思维导图
  • 简单了解BeanFactory和FactoryBean的区别
  • AMBA-AXI and ACE协议详解(八)
  • Critic-V: VLM Critics Help Catch VLM Errors in Multimodal Reasoning(CVPR 2025)
  • C++零拷贝网络编程实战:从理论到生产环境的性能优化之路
  • Word和Excel的一些功能记录
  • PHP现代化全栈开发:测试驱动开发与持续交付实践
  • 重温k8s基础概念知识系列二(Pod)
  • 腾讯开源:视频生成框架Hunyuan-GameCraft
  • 说一下事件传播机制
  • LeeCode 39.组合总和
  • 第4章 鼎鼎大名的Reactor模式
  • Redis--day7--黑马点评--优惠券秒杀
  • steam_api64.dll丢失?steam_api64.dll修复工具
  • 837. 新 21 点
  • C语言基础:(十六)深入理解指针(6)
  • 在鸿蒙里优雅地处理网络错误:从 Demo 到实战案例
  • 基于粒子群优化算法优化支持向量机的数据回归预测 PSO-SVM
  • Java实战:数字转中文大写金额的完整实现与优化技巧
  • 偏最小二乘结构方程(PLS-SEM)_TomatoSCI分析日记
  • bash shell 入门