简介
这篇文章主要记录我在试图解决如何尽可能准确地在某个特定的时间间隔执行某项具体任务时的思路历程,并在后期对相关的API进展的归纳和总结,以备参考。
问题引出
很多时候,我们会有类似“每隔多长时间执行某项任务〞的需求,乍看这个问题并不难解决,实那么并不容易,有很多隐含条件需要考虑,诸如:时间精度是多少?时间是否允许出现偏差,允许的偏差是多少,偏差之后如何处理?系统的负载如何?这个程序允许占用的系统资源是否有?这个程序运行的硬件平台如何?为了便于分析,我们锁定题目为“每隔2妙打印当前的系统时间(距离UNIX纪元的秒数〕〞。
基于sleep的朴素解法 看到这个题目,我想大家的想法和我一样,都是首先想到类似这样的解法: #include #include . . . . . >= 199309L 既然有了能准确到纳秒的nanosleep可用,上面的较低精度的函数也就可以休息了。实际上在Linux系统下,sleep和usleep就是通过一个系统调用nanosleep实现的。 用带有超时功能的API变相实现睡眠 如果开发者不知道有usleep和nanosleep,这个时候他可能会联想到select类的系统调用: According to POSIX.1-2001 */ #include #include #include 2 / 6 . . . . . 除了基于sleep的实现外,还有基于能用信号进展异步提醒的定时器实现: #include . . . . . nt which, const struct itimerval *value, struct itimerval *ovalue); 细心的你应该已经注意到了,ualarm和setitimer都额外提供了间隔时间的设置以便于间隔定时器用SIGALRM周期性的唤醒进程,这对于我们的需求有什么意义呢?请听我慢慢道来。一般来说,需要定时执行的任务所消耗的时间都很短,至少都会少于间隔时间,否那么这个需求就是无法实现的。我们前面的程序实现,都是假设任务消耗时间为0,实际上的任务并不总是像打印当前系统时间这么简单,即便它们持续的时间真的短到相对来说可以忽略不计,如果这些小的忽略不计累积起来,也还是可能会造成长时间后的大偏差,所以我们有必要将这段时间计算进来。一种补救的措施是在任务执行的前后执行gettimeofday得到系统的时间,然后做差得到任务消耗时间并在接下来的“sleep〞中将其扣除。问题看似解决了,但是我们毕竟没有将系统进展上下文切换的时间和计算消耗时间的时间考虑进来,这样的话,还是会存在较大的误差。另一种计算量相对小些的算法是:直接通过时间间隔计算下一次超时的绝对时间,然后根据当前的绝对时间算出需要等待的时间并睡眠。但是,这也只是修修补补而已,并没有从根本上解决问题。间隔定时器的出现从根本上解决了上面所提的问题,它自身就提供周期唤醒的功能,从而防止了每次都计算的负担。因为ualarm已经被放弃,所以用setitimer再次改写代码: #include . . . . . timerid); int timer_getoverrun(timer_t timerid); int timer_gettime(timer_t timerid, struct itimerspec *value); int timer_settime(timer_t timerid, int flags, const struct itimerspec *restrict value, struct itimerspec *restrict ovalue); 它实际上就是进程间隔定时器的增强版,除了可以定制时钟源〔nanosleep也存在能定制时钟源的版本:clock_nanosleep〕和时间精度提高到纳秒外,它还能通过将evp->sigev_notify设定为如下值来定制定时器到期后的行为: • SIGEV_SIGNAL: 发送由evp->sigev_sino指定的信号到调用进程,evp->sigev_value的值将被作为siginfo_t结构体中si_value的值。 SIGEV_NONE:什么都不做,只提供通过timer_gettime和timer_getoverrun查询超时信息。 SIGEV_THREAD: 以evp->sigev_notification_attributes为线程属性创建一个线程,在新建的线程部以evp->sigev_value为参数调用evp->sigev_notification_function。 • • • SIGEV_THREAD_ID:和SIGEV_SIGNAL类似,不过它只将信号发送到线程号为evp->sigev_notify_thread_id的线程,注意:这里的线程号不一定是POSIX线程号,而是线程调用gettid返回的实际线程号,并且这个线程必须实际存在且属于当前的调用进程。 更新后的程序如下〔需要连接实时扩展库: -lrt〕: #include . . . . . return 0;} 至于时钟源为什么是CLOCK_MONOTONIC而不是CLOCK_REALTIME,主要是考虑到系统的实时时钟可能会在程序运行过程中更改,所以存在一定的不确定性,而CLOCK_MONOTONIC那么不会,较为稳定。至此为止,我们已经找到了目前Linux提供的精度最高的定时器API,它应该能满足大多数情况的要求了。 其它问题 传统信号的不可靠性 传统UNIX信号是不可靠的,也就是说如果当前的信号没有被处理,那么后续的同类信号将被丢失,而不是被排队,而实时信号那么没有这个问题,它是被排队的。联系到当前应用,如果信号丢失,那么是因为任务消耗了过多的处理器时间,而这个不确定性是那个任务带来的,需要改良的应该是那个任务。 系统负载过高 如果系统的负载过高,使得我们的程序因为不能得到与时的调度导致时间精度降低,我们不妨通过nice提高当前程序的优先级,必要时可以通过 sched_setscheduler将当前进程切换成优先级最高的实时进程已确保得到与时调度。 硬件相关的问题 硬件配置也极大的影响着定时器的精度,有的比拟老的遗留系统可能没有比拟准确的硬件定时器,那样的话我们就无法期待它能提供多高的时钟精度了。相反,如果系统的配置比拟高,比如说对称多处理系统,那么即使有的处理器负载比拟高,我们也能通过将一个处理器单独分配出来处理定时器来提高定时器的精度。 更高的时间精度 虽然,Linux的API暗示它能够提供纳秒级的时间精度,但是,由于种种不确定因素,它实际上并不能提供纳秒级的精度,比拟脆弱。如果你需要更高强度的实时性,请考虑采用软实时系统、硬实时系统、专有系统,甚至是专业硬件。注意:为了简便,以上所有代码都没有出错处理,请读者在现实的应用中自行参加出错处理,以提高程序的健壮性。尤其注意sleep类的返回值,它们可能没到期就返回,这个时候你应该手动计算需要再睡眠多长才能满足原始的睡眠时间要求,如果该API并没有返回剩余的时间的话。 6 / 6 因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- sarr.cn 版权所有 赣ICP备2024042794号-1
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务