Linux下的定时器-程序设计

简介
这篇文章主要记录我在试图解决如何尽可能精确地在某个特定的时间间隔执行某项具体任务时的思路历程,并在后期对相关的API进行的归纳和总结,以备参考。
   问题引出
很多时候,我们会有类似“每隔多长时间执行某项任务”的需求,乍看这个问题并不难解决,实则并不容易,有很多隐含条件需要考虑,诸如:时间精度是多少?时间是否允许出现偏差,允许的偏差是多少,偏差之后如何处理?系统的负载如何?这个程序允许占用的系统资源是否有限制?这个程序运行的硬件平台如何?
为了便于分析,我们锁定题目为“每隔2妙打印当前的系统时间(距离UNIX纪元的秒数)”。
基于sleep的朴素解法
看到这个题目,我想大家的想法和我一样,都是首先想到类似这样的解法:


  1. #include <stdio.h>   
  2.    
  3. int main(int argc, char *argv[])  
  4. {  
  5.         while (1) {  
  6.                 printf("%d\n", time(NULL));  
  7.                 sleep(2);  
  8.         }  
  9.    
  10.         return 0;  
  11. }  
#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可用:
 
     
  1.  #include <unistd.h>   
  2.    
  3.   int usleep(useconds_t usec);   
  4.   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):   
  5.   usleep(): _BSD_SOURCE || _XOPEN_SOURCE >= 500  
  6.     
  7. #include <time.h>   
  8.  int nanosleep(const struct timespec *req, struct timespec *rem);  
  9.  Feature Test Macro Requirements for glibc (see feature_test_macros(7)):  
  10.  nanosleep(): _POSIX_C_SOURCE >= 199309L  
 #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类的系统调用:
 
     
  1. //According to POSIX.1-2001 */   
  2.  #include <sys/select.h>   
  3.        
  4.  #include <sys/time.h>   
  5.  #include <sys/types.h>   
  6.   #include <unistd.h>   
  7.   
  8.   int select(int nfds, fd_set *readfds,   
  9.              fd_set *writefds,    
  10.              fd_set *exceptfds,   
  11.              struct timeval *timeout);  
  12.   
  13.   #include <poll.h>   
  14.   
  15.   int poll(struct pollfd *fds, nfds_t nfds, int timeout);  
  16.   
  17.   #include <sys/epoll.h>   
  18.   
  19.   int epoll_wait(int epfd, struct epoll_event *events,    
  20.                  int maxevents, int timeout);  
  21.   int epoll_pwait(int epfd, struct epoll_event *events,     
  22.                  int maxevents, int timeout,     
  23.                  const sigset_t *sigmask);  
  //According to POSIX.1-2001 */
   #include <sys/select.h>
       
   #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的实现外,还有基于能用信号进行异步提醒的定时器实现:
 
  1. #include <stdio.h>   
  2. #include <signal.h>   
  3.    
  4. int main(int argc, char *argv[])  
  5. {  
  6.         sigset_t block;  
  7.    
  8.         sigemptyset(&block);  
  9.         sigaddset(&block, SIGALRM);  
  10.         sigprocmask(SIG_BLOCK, &block, NULL);  
  11.    
  12.         while (1) {  
  13.                 printf("%d\n", time(NULL));  
  14.                 alarm(2);  
  15.                 sigwaitinfo(&block, NULL);  
  16.         }  
  17.    
  18.         return 0;  
  19. }  
  20.    
#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:
 
  1. #include <unistd.h>   
  2.   
  3.  useconds_t ualarm(useconds_t usecs, useconds_t interval);  
  4.   
  5.  Feature Test Macro Requirements for glibc (see feature_test_macros(7)):  
  6.   
  7. ualarm(): _BSD_SOURCE || _XOPEN_SOURCE >= 500  
 #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再次改写代码:
 
 
  1. #include <stdio.h>   
  2. #include <signal.h>   
  3. #include <sys/time.h>   
  4.    
  5. int main(int argc, char *argv[])  
  6. {  
  7.         sigset_t block;  
  8.         struct itimerval itv;  
  9.    
  10.         sigemptyset(&block);  
  11.         sigaddset(&block, SIGALRM);  
  12.         sigprocmask(SIG_BLOCK, &block, NULL);  
  13.         //sigemptyset 函数初始化信号集合set,将set 设置为空.   
  14.         //sigfillset 也初始化信号集合,只是将信号集合设置为所有信号的集合.   
  15.         //sigaddset 将信号signo 加入到信号集合之中,sigdelset 将信号从信号集合中删除.   
  16.         //sigismember 查询信号是否在信号集合之中.s   
  17.         //igprocmask 是最为关键的一个函数.在使用之前要先设置好信号集合set.这个函数的作用是将指定的信号集合set    
  18.         //加入到进程的信号阻塞集合之中去,如果提供了oset 那么当前的进程信号阻塞集合将会保存在oset 里面.参数how 决定函数的操作方式:   
  19.         //SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合之中.   
  20.         //SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合.   
  21.         //SIG_SETMASK:将当前的信号集合设置为信号阻塞集合.   
  22.     
  23.         itv.it_interval.tv_sec = 2;  
  24.         itv.it_interval.tv_usec = 0;  
  25.         itv.it_value = itv.it_interval;  
  26.         setitimer(ITIMER_REAL, &itv, NULL);  
  27.    
  28.         while (1) {  
  29.                 printf("%d\n", time(NULL));  
  30.                 sigwaitinfo(&block, NULL);  
  31.         }  
  32.    
  33.         return 0;  
  34. }  
#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);
        //sigemptyset 函数初始化信号集合set,将set 设置为空.
        //sigfillset 也初始化信号集合,只是将信号集合设置为所有信号的集合.
        //sigaddset 将信号signo 加入到信号集合之中,sigdelset 将信号从信号集合中删除.
        //sigismember 查询信号是否在信号集合之中.s
        //igprocmask 是最为关键的一个函数.在使用之前要先设置好信号集合set.这个函数的作用是将指定的信号集合set 
        //加入到进程的信号阻塞集合之中去,如果提供了oset 那么当前的进程信号阻塞集合将会保存在oset 里面.参数how 决定函数的操作方式:
        //SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合之中.
        //SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合.
        //SIG_SETMASK:将当前的信号集合设置为信号阻塞集合.
  
        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实时扩展中的定时器:
 
  1. #include <signal.h>   
  2. #include <time.h>   
  3.   
  4. int timer_create(clockid_t clockid, struct sigevent *restrict evp,    
  5.                  timer_t *restrict timerid);  
  6. int timer_getoverrun(timer_t timerid);  
  7. int timer_gettime(timer_t timerid, struct itimerspec *value);  
  8. int timer_settime(timer_t timerid, int flags,    
  9.                   const struct itimerspec *restrict value,    
  10.                   struct itimerspec *restrict ovalue);  
       #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):
 
  1. #include <stdio.h>   
  2. #include <signal.h>   
  3. #include <time.h>   
  4. #include <errno.h>   
  5. #include <sched.h>   
  6.    
  7. int main(int argc, char *argv[])  
  8. {  
  9.         timer_t timer;  
  10.         struct itimerspec timeout;  
  11.         sigset_t block;  
  12.         struct sched_param param;  
  13.    
  14.         sigemptyset(&block);  
  15.         sigaddset(&block, SIGALRM);  
  16.         sigprocmask(SIG_BLOCK, &block, NULL);  
  17.    
  18.         timer_create(CLOCK_MONOTONIC, NULL, &timer);  
  19.         timeout.it_interval.tv_sec = 2;  
  20.         timeout.it_interval.tv_nsec = 0;  
  21.         timeout.it_value = timeout.it_interval;  
  22.         timer_settime(timer, 0, &timeout, NULL);  
  23.    
  24.         while (1) {  
  25.                 fprintf(stderr, "%d\n", time(NULL));  
  26.                 sigwaitinfo(&block, NULL);  
  27.         }  
  28.    
  29.         return 0;  
  30. }  
  31.    
#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,它应该能满足大多数情况的要求了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值