有时我们需要定时完成一些任务。简单的方法是使用 while 循环加 sleep。比如每隔 1 分钟检查链接情况的 heartbeat 任务等。比如:

while(condtion)
{
 //do something
 sleep(interval);
}

这可以满足很多程序的定时需要,但假如您不希望程序“偷懒”,即上例中 sleep 的时候您还是希望程序做些有用的工作,那么使用定时器是通常的选择。Linux 系统上最常用的定时器是 setitmer 计时器。

setitimer

Linux 系统为每个进程提供三个间隔计时器,每个计时器在不同的时间域递减。当任何计时器
到期时,向进程发送一个信号,然后计时器(可能)重新启动。

ITIMER_REAL:减少实际时间,到期的时候发出 SIGALRM 信号。

REAL 时间,即我们人类自然感受的时间,英文计算机文档中也经常使用 wall-clock 这个术语。说白了就是我们通常所说的时间,比如现在是中午12 点 10 分,那么一分钟的 REAL 时间之后就是中午 12点 11 分。

ITIMER_VIRTUAL:减少有效时间 (进程执行的时间),产生 SIGVTALRM 信号。

VIRTUAL 时间是进程执行的时间,Linux 是一个多用户多任务系统,在过去的 1 分钟内,指定进程实际在 CPU 上的执行时间往往并没有 1 分钟,因为其他进程会被 Linux 调度执行,在那些时间内,虽然自然时间在流逝,但指定进程并没有真正的运行。

ITIMER_PROF:减少进程的有效时间和系统时间 (为进程调度用的时间)。这个经常和上面一个使用用来计算系统内核时间和用户时间。产生 SIGPROF 信号。

PROF 时间比较独特,对进程 P1 来说从 12点 10 分开始的 1 分钟内,虽然自己的执行时间为 30 秒,但实际上还有 10 秒钟内核是在执行 P1 发起的系统调用,那么这 10 秒钟也被加入到 PROF 时间。

setitimer Timer 需要的API

#include <sys/time.h>

int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,
              struct itimerval *old_value);

计时器值由以下结构定义:

struct itimerval {
    struct timeval it_interval; /* next value */
    struct timeval it_value;    /* current value */
};

struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};

setitimer() 以 new_value 设置特定的定时器,如果 old_value 非空,则它返回 which 类型时间间隔定时器的前一个值。定时器从 it_value 递减到零,然后产生一个信号,并重新设置为 it_interval,如果此时 it_interval 为零,则该定时器停止。任何时候,只要 it_value 设置为零,该定时器就会停止。

getitimer 函数得到间隔计时器的时间值,保存在 value 中。

来看看下面的例子。

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


//简单打印信息,定时器触发函数
void print_info(int signo){
    printf("timer fired\n");
}

void init_sigaction(){
    struct sigaction act;
    act.sa_handler = print_info;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask); 
    sigaction(SIGPROF,&act,NULL); //设置信号 SIGPROF 的处理函数为 print_info

}


void init_time() { 
    struct itimerval value; 
    value.it_value.tv_sec=2; //定时器启动后,每隔2秒将执行相应的函数
    value.it_value.tv_usec=0; 
    value.it_interval=value.it_value; 
    setitimer(ITIMER_PROF,&value,NULL); //初始化 timer,到期发送 SIGPROF 信号
} 

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

    init_sigaction(); 
    init_time(); 
    while(1); 

    return 0;
}

在这里插入图片描述

这个程序使用 PROF 时间,每经过两秒 PROF 时间之后就会打印一下 timer fired 字符串。

需要指出:setitimer 计时器的精度为 ms,即 1000 分之 1 秒,足以满足绝大多数应用程序的需要。但多媒体等应用可能需要更高精度的定时,那么就需要考虑使用下一类定时器:POSIX Timer。

POSIX Timer

最强大的定时器接口来自POSIX时钟系列,其创建、初始化以及删除一个定时器的行动被分为三个不同的函数:timer_create()(创建定时器)、timer_settime()(初始化定时器)以及 timer_delete()(销毁它)。POSIX Timer 对 setitimer 进行了增强,克服了 setitimer 的诸多问题:

1、一个进程同一时刻只能有一个 timer。假如应用需要同时维护多个 Interval 不同的计时器,必须自己写代码来维护。这非常不方便。使用 POSIX Timer,一个进程可以创建任意多个 Timer。
2、setitmer 计时器时间到达时,只能使用信号方式通知使用 timer 的进程,而 POSIX timer 可以有多种通知方式,比如信号,或者启动线程。
3、使用 setitimer 时,通知信号的类别不能改变:SIGALARM,SIGPROF 等,而这些都是传统信号,而不是实时信号,因此有 timer overrun 的问题;而 POSIX Timer 则可以使用实时信号。
4、 setimer 的精度是 ms,POSIX Timer 是针对有实时要求的应用所设计的,接口支持 ns 级别的时钟精度。

POSIX Timer 函数

//创建一个定时器
int timer_create(clockid_t clock_id, struct sigevent *evp, timer_t *timerid)

进程可以通过调用 timer_create() 创建特定的定时器,定时器是每个进程自己的,不是在 fork 时继承的。

参数:

clock_id 说明定时器是基于哪个时钟的,*timerid 装载的是被创建的定时器的 ID。该函数创建了定时器,并将他的 ID 放入timerid指向的位置中。参数evp指定了定时器到期要产生的异步通知。

如果evp为 NULL,那么定时器到期会产生默认的信号,对 CLOCK_REALTIMER来说,默认信号就是SIGALRM。如果要产生除默认信号之外的其它信号,程序必须将 evp->sigev_signo设置为期望的信号码。

struct sigevent 结构中的成员 evp->sigev_notify说明了定时器到期时应该采取的行动。通常,这个成员的值为SIGEV_SIGNAL,这个值说明在定时器到期时,会产生一个信号。程序可以将成员 evp->sigev_notify设为SIGEV_NONE来防止定时器到期时产生信号。

clock_id取值为以下:

CLOCK_REALTIME :Systemwide realtime clock. //时间是系统保存的时间,即可以由 date 命令显示的时间,该时间可以重新设置。
CLOCK_MONOTONIC:Represents monotonic time. Cannot be set.
CLOCK_PROCESS_CPUTIME_ID :High resolution per-process timer.  //CLOCK_PROCESS_CPUTIME_ID 的含义与 setitimer 的 ITIMER_VIRTUAL 类似。
CLOCK_THREAD_CPUTIME_ID :Thread-specific timer.// CLOCK_THREAD_CPUTIME_ID 以线程为计时实体,当前进程中的某个线程真正地运行了一定时间才触发 Timer。
CLOCK_REALTIME_HR :High resolution version of CLOCK_REALTIME. 
CLOCK_MONOTONIC_HR :High resolution version of CLOCK_MONOTONIC.
//启动一个定时器
int timer_settime(timer_t timerid, int flags, const struct itimerspec *value, struct itimerspect *ovalue);

struct itimespec{
    struct timespec it_interval; 
    struct timespec it_value;   
}; 

timer_create()所创建的定时器并未启动。要将它关联到一个到期时间以及启动时钟周期,可以使用timer_settime()。

如同settimer(),it_value用于指定当前的定时器到期时间。当定时器到期,it_value的值会被更新成it_interval 的值。如果it_interval的值为0,则定时器不是一个时间间隔定时器,一旦it_value到期就会回到未启动状态。timespec的结构提供了纳秒级分辨率:

struct timespec{
    time_t tv_sec;
    long tv_nsec;  
};

参数:

如果flags的值为TIMER_ABSTIME,则value所指定的时间值会被解读成绝对值(此值的默认的解读方式为相对于当前的时间)。这个经修改的行为可避免取得当前时间、计算“该时间”与“所期望的未来时间”的相对差额以及启动定时器期间造成竞争条件。 如果ovalue的值不是NULL,则之前的定时器到期时间会被存入其所提供的itimerspec。如果定时器之前处在未启动状态,则此结构的成员全都会被设定成0。

//获得一个活动定时器的剩余时间
int timer_gettime(timer_t timerid,struct itimerspec *value);
//取得一个定时器的超限运行次数
int timer_getoverrun(timer_t timerid);

有可能一个定时器到期了,而同一定时器上一次到期时产生的信号还处于挂起状态。在这种情况下,其中的一个信号可能会丢失。这就是定时器超限。程序可以通过调用timer_getoverrun来确定一个特定的定时器出现这种超限的次数。定时器超限只能发生在同一个定时器产生的信号上。由多个定时器,甚至是那些使用相同的时钟和信号的定时器,所产生的信号都会排队而不会丢失。

执行成功时,timer_getoverrun()会返回定时器初次到期与通知进程(例如通过信号)定时器已到期之间额外发生的定时器到期次数。

//删除一个定时器
int timer_delete (timer_t timerid);

一次成功的timer_delete()调用会销毁关联到timerid的定时器并且返回0。执行失败时,此调用会返回-1并将errno设定会 EINVAL,这个唯一的错误情况代表timerid不是一个有效的定时器。

POSIX Timer 到期通知方式

SIGEV_NONE 	定时器到期时不产生通知。。。
SIGEV_SIGNAL 	定时器到期时将给进程投递一个信号,sigev_signo 可以用来指定使用什么信号。
SIGEV_THREAD 	定时器到期时将启动新的线程进行需要的处理
SIGEV_THREAD_ID(仅针对 Linux) 	定时器到期时将向指定线程发送信号。 

设置通知方式:

evp.sigev_value.sival_ptr = &timer;
evp.sigev_notify = SIGEV_THREAD;
evp.sigev_notify_function = handle;
evp.sigev_value.sival_int = 3;   //作为handle()的参数
ret = timer_create(CLOCK_REALTIME, &evp, &timer);

这里将通知方式设为 SIGEV_THREAD,handle为入口函数。然后设置定时器间隔,并启动 Timer:

ts.it_interval.tv_sec = 1;
ts.it_interval.tv_nsec = 0;
ts.it_value.tv_sec = 3;
ts.it_value.tv_nsec = 0;
ret = timer_settime(timer, TIMER_ABSTIME, &ts, NULL);
#include <stdio.h>
#include <sys/time.h>
#include <string.h>
#include <signal.h>
#include <time.h> 


void  handle(union sigval v){
    time_t t;
    char p[32];
    time(&t);
    strftime(p, sizeof(p), "%T", localtime(&t));
    printf("%s thread %lu, val = %d, signal captured.\n", p, pthread_self(), v.sival_int);
    return;
}

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

    struct sigevent evp;
    struct itimerspec ts;
    timer_t timer;
    int ret;
    memset   (&evp, 0, sizeof(evp));
    evp.sigev_value.sival_ptr = &timer;
    evp.sigev_notify = SIGEV_THREAD;
    evp.sigev_notify_function = handle;
    evp.sigev_value.sival_int = 3;   //作为handle()的参数
    ret = timer_create(CLOCK_REALTIME, &evp, &timer);
    if( ret){
        perror("timer_create");
    }
   
    ts.it_interval.tv_sec = 1;
    ts.it_interval.tv_nsec = 0;
    ts.it_value.tv_sec = 3;
    ts.it_value.tv_nsec = 0;
    ret = timer_settime(timer, TIMER_ABSTIME, &ts, NULL);
    if( ret )
    {
        perror("timer_settime");
    }

    while(1);

}

编译输出:

在这里插入图片描述

总结

使用定时器的目的无非是为了周期性的执行某一任务,或者是到了一个指定时间去执行某一个任务。本文讲述了 Linux 系统提供的大多数关于时间的编程方法。 但Linux 系统是如何实现这些机制是否十分好奇。但这得了解到一些硬件时钟设备的简单知识了。

参考:https://www.ibm.com/developerworks/cn/linux/1307_liuming_linuxtime1/index.html?ca=drs-

在这里插入图片描述

欢迎关注微信公众号【程序猿编码】,添加本人微信号(17865354792),回复:领取学习资料。或者回复:进入技术交流群。网盘资料有如下:

在这里插入图片描述

Logo

更多推荐