llinux 定时器 转载自 http://blog.chinaunix.net/uid-11848011-id-96374.html

这篇文章主要记录我在试图解决如何尽可能精确地在某个特定的时间间隔执行某项具体任务时的思路历程,并在后期对相关的API进行的归纳和总结,以备参考。

问题引出

很多时候,我们会有类似“每隔多长时间执行某项任务”的需求,乍看这个问题并不难解决,实则并不容易,有很多隐含条件需要考虑,诸如:时间精度是多少?时间是否允许出现偏差,允许的偏差是多少,偏差之后如何处理?系统的负载如何?这个程序允许占用的系统资源是否有限制?这个程序运行的硬件平台如何?

为了便于分析,我们锁定题目为“每隔2妙打印当前的系统时间(距离UNIX纪元的秒数)”。

基于sleep的朴素解法

看到这个题目,我想大家的想法和我一样,都是首先想到类似这样的解法:



#include <stdio.h>

int main(int argc, char *argv[])
{
        while (1) {
                printf("%d\n", time(NULL));
                sleep(2);
        }

return 0;
}

如果对时间精度要求不高,以上代码确实能工作的很好。因为sleep的时间精度只能到1s:


       #include <unistd.h>

unsigned int sleep(unsigned int seconds);

所以对于更高的时间精度(比如说毫秒)来说,sleep就不能奏效了。如果沿着这个思路走下去,还分别有精确到微妙和纳秒的函数usleep和nanosleep可用:


      #include <unistd.h>

int usleep(useconds_t usec);

Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

usleep(): _BSD_SOURCE || _XOPEN_SOURCE >= 500


      #include <time.h>

int nanosleep(const struct timespec *req, struct timespec *rem);

Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

nanosleep(): _POSIX_C_SOURCE >= 199309L

既然有了能精确到纳秒的nanosleep可用,上面的较低精度的函数也就可以休息了。实际上在Linux系统下,sleep和usleep就是通过一个系统调用nanosleep实现的。

用带有超时功能的API变相实现睡眠

如果开发者不知道有usleep和nanosleep,这个时候他可能会联想到select类的系统调用:


       According to POSIX.1-2001 */
       #include <sys/select.h>

/* According to earlier standards */
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);


      #include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);


       #include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
       int epoll_pwait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout,
                      const sigset_t *sigmask);


函数原型和相关手册来看,poll和epoll_wait能提供的时间精度为毫秒,select比他们两个略胜一筹,为微秒,和前述的usleep相当。
但是,果真如此么?这需要我们深入到Linux的具体实现,在内核里,这几个系统调用的超时功能都是通过内核中的动态定时器实现的,而动态定时器的时间精
度是由当前内核的HZ数决定的。如果内核的HZ是100,那么动态定时器的时间精度就是1/HZ=1/100=10毫秒。目前,X86系统的HZ最大可以
定义为1000,也就是说X86系统的动态定时器的时间精度最高只能到1毫秒。由此来看,select用来指示超时的timeval数据结构,只是看起来
很美,实际上精度和poll/epoll_wait相当。

基于定时器的实现

除了基于sleep的实现外,还有基于能用信号进行异步提醒的定时器实现:


#include <stdio.h>
#include <signal.h>

int main(int argc, char *argv[])
{
        sigset_t block;

sigemptyset(&block);
        sigaddset(&block, SIGALRM);
        sigprocmask(SIG_BLOCK, &block, NULL);

while (1) {
                printf("%d\n", time(NULL));
                alarm(2);
                sigwaitinfo(&block, NULL);
        }

return 0;
}


然,上面的代码并没有利用信号进行异步提醒,而是通过先阻塞信号的传递,然后用sigwaitinfo等待并将信号取出的方法将异步化同步。这样做的目的
是为了尽可能减少非必要的信号调用消耗,因为这个程序只需要执行这个简单的单一任务,所以异步除了带来消耗外,并无任何好处。

读者可能已
经发现上面的代码无非是把最初的代码中的sleep换成了alarm和sigwaitinfo两个调用,除了复杂了代码之外,好像并没有什么额外的好处。
alarm的时间精度只能到1s,并且alarm和sigwaitinfo的确也可以看成是sleep的一种实现,实际上有的sleep确实是透过
alarm来实现的,请看sleep的手册页:

BUGS
       sleep()  may be implemented using SIGALRM; mixing calls to alarm(2) and
       sleep() is a bad idea.

Using longjmp(3) from a signal handler or  modifying  the  handling  of
       SIGALRM while sleeping will cause undefined results.

但是,这只是表象,本质他们是不同的,sleep是拨了一个临时实时定时器并等待定时器到期,而alarm是用进程唯一的实时定时器来定时唤醒等待信号到来的进程执行。

如果需要更高的时间精度,可以采用精度为微秒的alarm版本ualarm:


       #include <unistd.h>

useconds_t ualarm(useconds_t usecs, useconds_t interval);

Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

ualarm(): _BSD_SOURCE || _XOPEN_SOURCE >= 500

或者是直接用setitimer操纵进程的实时定时器:


      #include <sys/time.h>

int getitimer(int which, struct itimerval *value);
       int setitimer(int which, const struct itimerval *value,
                     struct itimerval *ovalue);


心的你应该已经注意到了,ualarm和setitimer都额外提供了间隔时间的设置以便于间隔定时器用SIGALRM周期性的唤醒进程,这对于我们的
需求有什么意义呢?请听我慢慢道来。一般来说,需要定时执行的任务所消耗的时间都很短,至少都会少于间隔时间,否则这个需求就是无法实现的。我们前面的程
序实现,都是假设任务消耗时间为0,实际上的任务并不总是像打印当前系统时间这么简单,即便它们持续的时间真的短到相对来说可以忽略不计,如果这些小的忽
略不计累积起来,也还是可能会造成长时间后的大偏差,所以我们有必要将这段时间计算进来。一种补救的措施是在任务执行的前后执行gettimeofday
得到系统的时间,然后做差得到任务消耗时间并在接下来的“sleep”中将其扣除。问题看似解决了,但是我们毕竟没有将系统进行上下文切换的时间和计算消
耗时间的时间考虑进来,这样的话,还是会存在较大的误差。另一种计算量相对小些的算法是:直接通过时间间隔计算下一次超时的绝对时间,然后根据当前的绝对
时间算出需要等待的时间并睡眠。但是,这也只是修修补补而已,并没有从根本上解决问题。间隔定时器的出现从根本上解决了上面所提的问题,它自身就提供周期
唤醒的功能,从而避免了每次都计算的负担。因为ualarm已经被放弃,所以用setitimer再次改写代码:



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

int main(int argc, char *argv[])
{
        sigset_t block;
        struct itimerval itv;

sigemptyset(&block);
        sigaddset(&block, SIGALRM);
        sigprocmask(SIG_BLOCK, &block, NULL);

itv.it_interval.tv_sec = 2;
        itv.it_interval.tv_usec = 0;
        itv.it_value = itv.it_interval;
        setitimer(ITIMER_REAL, &itv, NULL);

while (1) {
                printf("%d\n", time(NULL));
                sigwaitinfo(&block, NULL);
        }

return 0;
}

进程的间隔计时器能够提供的时间精度为微秒,对于大多数的应用来说,应该已经足够,如果需要更高的时间精度,或者需要多个定时器,那么每个进程一个的实时间隔定时器就无能为力了,这个时候我们可以选择POSIX实时扩展中的定时器:


      #include <signal.h>
       #include <time.h>

int timer_create(clockid_t clockid, struct sigevent *restrict evp,
              timer_t *restrict 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 <stdio.h>
#include <signal.h>
#include <time.h>
#include <errno.h>
#include <sched.h>

int main(int argc, char *argv[])
{
        timer_t timer;
        struct itimerspec timeout;
        sigset_t block;
        struct sched_param param;

sigemptyset(&block);
        sigaddset(&block, SIGALRM);
        sigprocmask(SIG_BLOCK, &block, NULL);

timer_create(CLOCK_MONOTONIC, NULL, &timer);
        timeout.it_interval.tv_sec = 2;
        timeout.it_interval.tv_nsec = 0;
        timeout.it_value = timeout.it_interval;
        timer_settime(timer, 0, &timeout, NULL);

while (1) {
                fprintf(stderr, "%d\n", time(NULL));
                sigwaitinfo(&block, NULL);
        }

return 0;
}

至于时钟源为什么是CLOCK_MONOTONIC而不是CLOCK_REALTIME,主要是考虑到系统的实时时钟可能会在程序运行过程中更改,所以存在一定的不确定性,而CLOCK_MONOTONIC则不会,较为稳定。

至此为止,我们已经找到了目前Linux提供的精度最高的定时器API,它应该能满足大多数情况的要求了。

其它问题

传统信号的不可靠性

传统UNIX信号是不可靠的,也就是说如果当前的信号没有被处理,那么后续的同类信号将被丢失,而不是被排队,而实
时信号则没有这个问题,它是被排队的。联系到当前应用,如果信号丢失,则是因为任务消耗了过多的处理器时间,而这个不确定性是那个任务带来的,需要改进的
应该是那个任务。

系统负载过高

如果系统的负载过高,使得我们的程序因为不能得到及时的调度导致时间精度降低,我们不妨通过nice提高当前程序的优先级,必要时可以通过sched_setscheduler将当前进程切换成优先级最高的实时进程已确保得到及时调度。

硬件相关的问题

硬件配置也极大的影响着定时器的精度,有的比较老的遗留系统可能没有比较精确的硬件定时器,那样的话我们就无法期待它能
提供多高的时钟精度了。相反,如果系统的配置比较高,比如说对称多处理系统,那么即使有的处理器负载比较高,我们也能通过将一个处理器单独分配出来处理定
时器来提高定时器的精度。

更高的时间精度

虽然,Linux的API暗示它能够提供纳秒级的时间精度,但是,由于种种不确定因素,它实际上并不能提供纳秒级的精度,比较脆弱。如果你需要更高强度的实时性,请考虑采用软实时系统、硬实时系统、专有系统,甚至是专业硬件。

注意:

为了简便,以上所有代码都没有出错处理,请读者在现实的应用中自行加入出错处理,以提高程序的健壮性。尤其注意sleep类的返回值,它们可能没到期就返回,这个时候你应该手动计算需要再睡眠多长才能满足原始的睡眠时间要求,如果该API并没有返回剩余的时间的话。

时间: 2024-10-05 11:59:43

llinux 定时器 转载自 http://blog.chinaunix.net/uid-11848011-id-96374.html的相关文章

Win32消息循环机制等【转载】http://blog.csdn.net/u013777351/article/details/49522219

Dos的过程驱动与Windows的事件驱动 在讲本程序的消息循环之前,我想先谈一下Dos与Windows驱动机制的区别: DOS程序主要使用顺序的,过程驱动的程序设计方法.顺序的,过程驱动的程序有一个明显的开始,明显的过程及一个明显的结束,因此程序能直接控制程序事件或过程的顺序.虽然在顺序的过程驱动的程序中也有很多处理异常的方法,但这样的异常处理也仍然是顺序的,过程驱动的结构. 而Windows的驱动方式是事件驱动,就是不由事件的顺序来控制,而是由事件的发生来控制,所有的事件是无序的,所为一个程

转载http://lhflinux.blog.51cto.com

一.实验环境部署 主服务器 192.168.18.42 端口3306  ==> 从服务器 192.168.18.44 端口 3306    主服务器 192.168.18.43 端口3306  ==> 从服务器 192.168.18.44 端口 3307 ##数据库,已经安装mysql服务,安装部分略.从服务器上的多个mysql实例,请看另一篇帖子<用mysql_multi 实现一台机器跑多台mysql > 二.部署服务器 1.在两台主服务器上赋予从机权限,有多台丛机,就执行多次(

目前 完整的 Blog 在phanx.blog.chinaunix.net

正在从 ChinaUnix往 GitHub 上搬. 目前要查看完整的Blog,还是请访问 phanx.blog.chinaunix.net 新的Blog会发在 GitHub 上. http://www.phanx.com 可直接访问.

推荐一些C#相关的网站、资源和书籍 (转载自http://blog.csdn.net/chinacsharper/article/details/17514923)

一.网站 1.http://msdn.microsoft.com/zh-CN/ 微软的官方网站,C#程序员必去的地方.那里有API开发文档,还有各种代码.资源下载. 2.http://social.msdn.microsoft.com/Forums/zh-CN/home 微软msdn论坛.定位于微软技术的传播和技术问题的解决,是学习微软技术的好去处. 3.http://www.codeproject.com/ 国外著名的社区网站,面向世界的程序员.有很多不错的程序都可以从那里下载到. 4.htt

Oracle RAC 全局等待事件 gc current block busy 和 gc cr multi block request 说明--转载(http://blog.csdn.net/tianlesoftware/article/details/7777511)

一.RAC 全局等待事件说明 在RAC环境中,和全局调整缓存相关的最常见的等待事件是global cache cr request,global cache busy和equeue. 当一个进程访问需要一个或者多个块时,Oracle会首先检查自己的Cache是否存在该块,如果发现没有,就会先通过global cache赋予这些块共享访问的权限,然后再访问.假如,通过global cache发现这些块已经在另一个实例的Cache里面,那么这些块就会通过Cache Fusion,在节点之间直接传递,

分享一篇文章C语言字节对齐问题(适用于C++)转载至http://blog.csdn.net/21aspnet/article/details/6729724

文章最后本人做了一幅图,一看就明白了,这个问题网上讲的不少,但是都没有把问题说透. 一.概念    对齐跟数据在内存中的位置有关.如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐.比如在32位cpu下,假设一个整型变量的地址为0x00000004,那它就是自然对齐的.   二.为什么要字节对齐   需要字节对齐的根本原因在于CPU访问数据的效率问题.假设上面整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x000000

C++ 运算符的重载(转载自http://blog.csdn.net/insistgogo/article/details/6626952)

(转载自http://blog.csdn.net/insistgogo/article/details/6626952) 什么是运算符的重载? 运算符与类结合,产生新的含义. 为什么要引入运算符重载? 作用:为了实现类的多态性(多态是指一个函数名有多种含义) 怎么实现运算符的重载? 方式:类的成员函数 或 友元函数(类外的普通函数) 规则:不能重载的运算符有 .  和 .* 和 ?: 和 ::  和 sizeof 友元函数和成员函数的使用场合:一般情况下,建议一元运算符使用成员函数,二元运算符使

同步与异步的概念(转自http://blog.chinaunix.net/uid-21411227-id-1826898.html)

今天看到一篇博客介绍同步异步的,看的挺舒服的,转过来,谢谢原作者zhenhuaqin(http://blog.chinaunix.net/uid-21411227-id-1826898.html). 进程同步用来实现程序并发执行时候的可再现性. 一.进程同步及异步的概念 1.进程同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回.也就是必须一件一件事做,等前一件做完了才能做下一件事.就像早上起床后,先洗涮,然后才能吃饭,不能在洗涮没有完成时,就开始吃饭.按照这个定义,其实绝大多数

SmartRF Flash Programmer 弹不出那个界面--转载http://blog.chinaunix.net/uid-24343357-id-3594874.html

SmartRF Flash Programmer 在我烧写几次HEX文件以后,老是缩在任务栏,你怎么点击,它都弹不出那个界面了,如下图,很郁闷的.我卸载一下,然后重装也不行.没有办法了,换了一台计算机,开始是好的,烧写几个hex以后,又出现同样的问题了,看来不是我计算机的问题了.怎么解决呢,打开任务管理器,按照下图操作. 点击最大化以后,界面是全屏,关闭,然后出现打开SmartRF Flash Programmer 就好了啊.莫名其妙的,软件有这样的问题,实在无话可说了.