CONFIG_HIGH_RES_TIMERS
选项,且平台支持高精度定时器模式,即可解决该问题。
可参考链接中系列时间子系统介绍文章
Linux时间子系统之一:clock source(时钟源)
测试代码如下:
#include
#include
#include
#include
#include
#include
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 = ¤t->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_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被查找,并执行回调函数,唤醒应用程序进程。具体实现太复杂,就不展开了。
梳理下整个流程。
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之间,相比上面的情况,误差就更容易让人接受。当然,修改系统节拍存在坏处,会增加系统的负载。不建议修改。