Linux usleep不准问题排查


  最近在工作中遇到了一个应用程序usleep不准的问题,排查过程中了解了一下usleep的内核实现,简单的讲一下低精度模式下的usleep机制。
  先把最终结论贴出来,内核使能 CONFIG_HIGH_RES_TIMERS选项,且平台支持高精度定时器模式,即可解决该问题。
  下面主要来分析为什么在未使能高精度定时器的情况下,usleep不准的问题。

参考文章

可参考链接中系列时间子系统介绍文章
Linux时间子系统之一:clock source(时钟源)

测试代码

  测试代码如下:

#include <sys/time.h>
#include <unistd.h>
#include <stdio.h>

#include <sys/time.h>
#include <unistd.h>
#include <stdio.h>

int main(void)
{
	int sleepTime = 0;
	int sleepus = 0;

	struct timeval t1;
	struct timeval t2;

	while(1) {
		gettimeofday(&t1, NULL);
		printf("start time %d(s).%06d(us)\n", (int)t1.tv_sec, (int)t1.tv_usec);
		usleep(10000);
		gettimeofday(&t2, NULL);
		printf("end   time %d(s).%06d(us)\n", (int)t2.tv_sec, (int)t2.tv_usec);
		sleepTime = (int)((t2.tv_sec - t1.tv_sec) * 1000 + (t2.tv_usec - t1.tv_usec) / 1000);
		sleepus = (int)((t2.tv_usec - t1.tv_usec) % 1000);
		printf("sleep cost %d(ms).%d(us)\n", sleepTime, sleepus);
	}

	return 0;
}

  测试结果为:
1)在代码中usleep10 ms,实际测试下来会变成20 ms,usleep 5ms会变成10ms。usleep 9900us(9.9ms),实际测试会变成10ms。
2)屏蔽代码中的while(1)循环,编译为单次执行,会发现usleep 5ms会随机分布在5-15 ms之间。usleep 10ms会随机分布在10-19ms之间。

系统调用

  usleep经过libc库封装,最终内核系统调用为nanosleep,位于内核kernel/time/hrtimer.c中代码如下:

SYSCALL_DEFINE2(nanosleep, struct timespec __user *, rqtp,
		struct timespec __user *, rmtp)
{
	struct timespec64 tu;

	if (get_timespec64(&tu, rqtp))
		return -EFAULT;

	if (!timespec64_valid(&tu))
		return -EINVAL;

	current->restart_block.nanosleep.type = rmtp ? TT_NATIVE : TT_NONE;
	current->restart_block.nanosleep.rmtp = rmtp;
	return hrtimer_nanosleep(&tu, HRTIMER_MODE_REL, CLOCK_MONOTONIC);
}

long hrtimer_nanosleep(const struct timespec64 *rqtp,
		       const enum hrtimer_mode mode, const clockid_t clockid)
{
	struct restart_block *restart;
	struct hrtimer_sleeper t;
	int ret = 0;
	u64 slack;

	slack = current->timer_slack_ns;
	if (dl_task(current) || rt_task(current))
		slack = 0;

	hrtimer_init_on_stack(&t.timer, clockid, mode);
	hrtimer_set_expires_range_ns(&t.timer, timespec64_to_ktime(*rqtp), slack);
	ret = do_nanosleep(&t, mode);
	if (ret != -ERESTART_RESTARTBLOCK)
		goto out;

	/* Absolute timers do not update the rmtp value and restart: */
	if (mode == HRTIMER_MODE_ABS) {
		ret = -ERESTARTNOHAND;
		goto out;
	}

	restart = &current->restart_block;
	restart->fn = hrtimer_nanosleep_restart;
	restart->nanosleep.clockid = t.timer.base->clockid;
	restart->nanosleep.expires = hrtimer_get_expires_tv64(&t.timer);
out:
	destroy_hrtimer_on_stack(&t.timer);
	return ret;
}

  当应用程序调用usleep时,实际会调用内核的高精度定时器。但是当内核未使能CONFIG_HIGH_RES_TIMERS选项时,虽然会调到nanosleep,会创建对应的高精度定时器,内核也会按照定精度定时器的模式进行处理。即在系统节拍到来时处理(系统HZ)。
在这里插入图片描述
  这里我的平台是100HZ。即每秒每个cpu的clock_even定时器会产生100次中断,10ms一次。低精度定时器在定时中断中处理。
  未使能CONFIG_HIGH_RES_TIMERS选项时,nanosleep创建的高精度定时器也会在此中断服务函数中处理。即高精度定时器工作在低精度模式下。
  某个CPU的clock_event会被选中作为系统节拍维护者,即负责jiffies增加的工作。

clock_event中断服务函数

  clock_event_device注册时,会设置中断服务函数,具体调用关系如下:
clockevents_register_device->tick_check_new_device->tick_setup_device

static void tick_setup_device(struct tick_device *td,
			      struct clock_event_device *newdev, int cpu,
			      const struct cpumask *cpumask)
{
	......
	if (td->mode == TICKDEV_MODE_PERIODIC)
		tick_setup_periodic(newdev, 0);
	else
		tick_setup_oneshot(newdev, handler, next_event);
}

  这里会设置周期模式。即调用tick_setup_periodic设置中断服务函数。
tick_setup_device->tick_setup_periodic->tick_set_periodic_handler

void tick_set_periodic_handler(struct clock_event_device *dev, int broadcast)
{
	if (!broadcast)
		dev->event_handler = tick_handle_periodic;
	else
		dev->event_handler = tick_handle_periodic_broadcast;
}

  所以最终的中断服务函数为tick_handle_periodic,每个CPU 的clockevent来临时,会调用该函数。
  而中断服务函数中,最终执行定时器的调用关系为:tick_handle_periodic->tick_periodic->update_process_times->run_local_timers。顾名思义,运行当前cpu上的定时器。代码如下:

void run_local_timers(void)
{
	struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);

	hrtimer_run_queues();									//查找到期的高精度定时器,并处理回调函数
	/* Raise the softirq only if required. */
	if (time_before(jiffies, base->clk)) {
		if (!IS_ENABLED(CONFIG_NO_HZ_COMMON))
			return;
		/* CPU is awake, so check the deferrable base. */
		base++;
		if (time_before(jiffies, base->clk))
			return;
	}
	raise_softirq(TIMER_SOFTIRQ);							//唤醒低精度定时器的软中断。
}

  nanosleep插入的高精度定时器,到期时会在hrtimer_run_queues被查找,并执行回调函数,唤醒应用程序进程。具体实现太复杂,就不展开了。

usleep不准问题说明

流程梳理

  梳理下整个流程。
1)执行usleep,触发系统调用
2)系统调用中创建高精度定时器,并将进程设置成睡眠模式。
3)高精度定时器到期,执行定时器回调,唤醒对应进程。
  usleep不准问题主要就出在步骤3上,进程什么时候被唤醒。

原因分析

  假设当前,内核节拍时间轴为0ms、10ms、20ms、30ms。依次类推。
1)单次运行时:命令行中执行可执行文件时,此时在0-10ms之间,假设4ms时插入,代码循环中usleep 10ms,则内核10ms时clock_event中断来临时,检查定时器,此时定时器尚未到期。本次中断中不会执行该定时器回调函数。14ms时,定时器到期,但是没有中断来处理该定时器,需要等到下一个clock_event中断才能执行该定时器的回调函数。即需要在20ms时,才能唤醒睡眠进程。此种情况耗时为16ms+。多次运行后,会发现,时间会在10~19ms之间分布。
2)while(1)循环运行时,命令行中执行该可执行文件,第一次循环费时和单次运行一致。时间随机在10~19ms。当第一次运行完后,进程在20ms时被唤醒执行,就绪队列中可能有其他进程在排队,且系统调用存在开支,while(1)循环中还有别的逻辑代码。则第二次循环调用usleep时间轴为20ms+,此时睡眠10ms,则会错误30ms时间轴时的clock_event中断,需要等待下一次clock_event中断。所以后面每次usleep 10ms都会变成接近20ms。可以修改代码,睡眠时间改为usleep 9ms时,则demo中每次循环睡眠时间会变为10ms。系统HZ决定睡眠误差。

解决方案

  上策:使能内核CONFIG_HIGH_RES_TIMERS选项,高精度定时器使能后,clock_event会根据定时器设置,精准设置下次中断到来的时间,精度可以达到us、ns级别。当然前提是,你使用的芯片原厂驱动支持该模式。
  下策:修改系统节拍。比如当前节拍100HZ,则误差为10ms。当需要睡眠的事件很短,如10ms,则误差会导致睡眠时间在10-20ms之间偏差,严重影响精度。当系统节拍改为1000HZ时,同样睡眠10ms,则睡眠的时间会在10-11ms之间,相比上面的情况,误差就更容易让人接受。当然,修改系统节拍存在坏处,会增加系统的负载。不建议修改。

  • 8
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值