linux 定时器

原文地址:

计时器的使用

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

清单 8,sleep 加循环
while(condtion)
{
 //do something
 sleep(interval);
}

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

setitimer

Linux 为每一个进程提供了 3 个 setitimer 间隔计时器:

  • ITIMER_REAL:减少实际时间,到期的时候发出 SIGALRM 信号。
  • ITIMER_VIRTUAL:减少有效时间 (进程执行的时间),产生 SIGVTALRM 信号。
  • ITIMER_PROF:减少进程的有效时间和系统时间 (为进程调度用的时间)。这个经常和上面一个使用用来计算系统内核时间和用户时间。产生 SIGPROF 信号。

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

VIRTUAL 时间是进程执行的时间,Linux 是一个多用户多任务系统,在过去的 1 分钟内,指定进程实际在 CPU 上的执行时间往往并没有 1 分钟,因为其他进程会被 Linux 调度执行,在那些时间内,虽然自然时间在流逝,但指定进程并没有真正的运行。VIRTUAL 时间就是指定进程真正的有效执行时间。比如 5 点 10 分开始的 1 分钟内,进程 P1 被 Linux 调度并占用 CPU 的执行时间为 30 秒,那么 VIRTUAL 时间对于进程 P1 来讲就是 30 秒。此时自然时间已经到了 5 点 11 分,但从进程 P1 的眼中看来,时间只过了 30 秒。

PROF 时间比较独特,对进程 P1 来说从 5 点 10 分开始的 1 分钟内,虽然自己的执行时间为 30 秒,但实际上还有 10 秒钟内核是在执行 P1 发起的系统调用,那么这 10 秒钟也被加入到 PROF 时间。这种时间定义主要用于全面衡量进程的性能,因为在统计程序性能的时候,10 秒的系统调用时间也应该算到 P1 的头上。这也许就是 PROF 这个名字的来历吧。

使用 setitimer Timer 需要了解下面这些接口 API:

int getitimer(int which,struct itimerval *value); 
int setitimer(int which,struct itimerval *newval, 
struct itimerval *oldval);

itimerval 的定义如下:

struct itimerval { 
struct timeval it_interval; 
struct timeval it_value; 
}

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

setitimer 函数设置间隔计时器的时间值为 newval. 并将旧值保存在 oldval 中;which 表示使用三个计时器中的哪一个。

itimerval 结构中的 it_value 是第一次调用后触发定时器的时间,当这个值递减为 0 时,系统会向进程发出相应的信号。此后将以 it_internval 为周期定时触发定时器。

给出一个具体的例子:

清单 9,setitmer 例子
void print_info(int signo) 
{ 
 printf(“timer fired\n”); //简单的打印,表示 timer 到期
} 

void init_sigaction(void) 
{ 
 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; 
 value.it_value.tv_usec=0; 
 value.it_interval=value.it_value; 
 setitimer(ITIMER_PROF,&value,NULL); //初始化 timer,到期发送 SIGPROF 信号
} 

int main() 
{ 
 len=strlen(prompt); 
 init_sigaction(); 
 init_time(); 
 while(1); 
 exit(0); 
}

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

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

POSIX Timer

间隔定时器 setitimer 有一些重要的缺点,POSIX Timer 对 setitimer 进行了增强,克服了 setitimer 的诸多问题:

首先,一个进程同一时刻只能有一个 timer。假如应用需要同时维护多个 Interval 不同的计时器,必须自己写代码来维护。这非常不方便。使用 POSIX Timer,一个进程可以创建任意多个 Timer。

setitmer 计时器时间到达时,只能使用信号方式通知使用 timer 的进程,而 POSIX timer 可以有多种通知方式,比如信号,或者启动线程。

使用 setitimer 时,通知信号的类别不能改变:SIGALARM,SIGPROF 等,而这些都是传统信号,而不是实时信号,因此有 timer overrun 的问题;而 POSIX Timer 则可以使用实时信号。

setimer 的精度是 ms,POSIX Timer 是针对有实时要求的应用所设计的,接口支持 ns 级别的时钟精度。

表 2. POSIX Timer 函数
函数名 功能描述
timer_create  创建一个新的 Timer;并且指定定时器到时通知机制
timer_delete 删除一个 Timer
timer_gettime Get the time remaining on a POSIX.1b interval timer
timer_settime 开始或者停止某个定时器。
timer_getoverrun 获取丢失的定时通知个数。

使用 Posix Timer 的基本流程很简单,首先创建一个 Timer。创建的时候可以指定该 Timer 的一些特性,比如 clock ID。

clock ID 即 Timer 的种类,可以为下表中的任意一种:

表 3. POSIX Timer clock ID
Clock ID 描述
CLOCK_REALTIME Settable system-wide real-time clock;
CLOCK_MONOTONIC Nonsettable monotonic clock
CLOCK_PROCESS_CPUTIME_ID Per-process CPU-time clock
CLOCK_THREAD_CPUTIME_ID Per-thread CPU-time clock

CLOCK_REALTIME 时间是系统保存的时间,即可以由 date 命令显示的时间,该时间可以重新设置。比如当前时间为上午 10 点 10 分,Timer 打算在 10 分钟后到时。假如 5 分钟后,我用 date 命令修改当前时间为 10 点 10 分,那么 Timer 还会再等十分钟到期,因此实际上 Timer 等待了 15 分钟。假如您希望无论任何人如何修改系统时间,Timer 都严格按照 10 分钟的周期进行触发,那么就可以使用 CLOCK_MONOTONIC。

CLOCK_PROCESS_CPUTIME_ID 的含义与 setitimer 的 ITIMER_VIRTUAL 类似。计时器只记录当前进程所实际花费的时间;比如还是上面的例子,假设系统非常繁忙,当前进程只能获得 50%的 CPU 时间,为了让进程真正地运行 10 分钟,应该到 10 点 30 分才允许 Timer 到期。

CLOCK_THREAD_CPUTIME_ID 以线程为计时实体,当前进程中的某个线程真正地运行了一定时间才触发 Timer。

设置到期通知方式

timer_create 的第二个参数 struct sigevent 用来设置定时器到时时的通知方式。该数据结构如下:

清单 10,结构 sigevent
 struct sigevent {
 int sigev_notify; /* Notification method */
 int sigev_signo; /* Notification signal */
 union sigval sigev_value; /* Data passed with
 notification */
 void (*sigev_notify_function) (union sigval);
 /* Function used for thread
 notification (SIGEV_THREAD) */
 void *sigev_notify_attributes;
 /* Attributes for notification thread
 (SIGEV_THREAD) */
 pid_t sigev_notify_thread_id;
 /* ID of thread to signal (SIGEV_THREAD_ID) */
 };

其中 sigev_notify 表示通知方式,有如下几种:

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

如果采用 SIGEV_NONE 方式,使用者必须调用timer_gettime 函数主动读取定时器已经走过的时间。类似轮询。

如果采用 SIGEV_SIGNAL 方式,使用者可以选择使用什么信号,用 sigev_signo 表示信号值,比如 SIG_ALARM。

如果使用 SIGEV_THREAD 方式,则需要设置 sigev_notify_function,当 Timer 到期时,将使用该函数作为入口启动一个线程来处理信号;sigev_value 保存了传入 sigev_notify_function 的参数。sigev_notify_attributes 如果非空,则应该是一个指向 pthread_attr_t 的指针,用来设置线程的属性(比如 stack 大小,detach 状态等)。

SIGEV_THREAD_ID 通常和 SIGEV_SIGNAL 联合使用,这样当 Timer 到期时,系统会向由 sigev_notify_thread_id 指定的线程发送信号,否则可能进程中的任意线程都可能收到该信号。这个选项是 Linux 对 POSIX 标准的扩展,目前主要是 GLibc 在实现 SIGEV_THREAD 的时候使用到,应用程序很少会需要用到这种模式。

启动定时器

创建 Timer 之后,便可以调用 timer_settime() 函数指定定时器的时间间隔,并启动该定时器了。

 int timer_settime(timer_t timerid, int flags,
 const struct itimerspec *new_value,
 struct itimerspec * old_value);

第一次看到 timer_settime 的参数列表或许会令人觉得费解。先来看看 new_value 和 old_value,它们都是 struct itimerspec 数据结构。

struct itimerspec
{
 struct timespec it_interval; //定时器周期值
 struct timespec it_value; //定时器到期值
};

启动和停止 Timer 都可以通过设置 new_value 来实现:

new_value->it_interval 为定时器的周期值,比如 1 秒,表示定时器每隔 1 秒到期;

new_value->it_value 如果大于 0,表示启动定时器,Timer 将在 it_value 这么长的时间过去后到期,此后每隔 it_interval 便到期一次。如果 it_value 为 0,表示停止该 Timer。

有些时候,应用程序会先启动用一个时间间隔启动定时器,随后又修改该定时器的时间间隔,这都可以通过修改 new_value 来实现;假如应用程序在修改了时间间隔之后希望了解之前的时间间隔设置,则传入一个非 NULL 的 old_value 指针,这样在 timer_settime() 调用返回时,old_value 就保存了上一次 Timer 的时间间隔设置。多数情况下我们并不需要这样,便可以简单地将 old_value 设置为 NULL,忽略它。

下面给出一个使用 posix timer 的例子程序。最传统的例子就是创建通知方式为 SIGEV_SIGNAL 的 Timer。这样当定时器到期时,将产生信号通知,主程序需要定义自己的信号处理函数,来处理信号到期事件。这种例子比比皆是,我打算在这里写一个采用通知方式为 SIGEV_THREAD 的例子。该例子程序从 main 函数开始主线程,在开始的时候打印出主线程的进程 ID 和线程 ID。

清单 11,打印 TID
 pid_t tid = (pid_t) syscall (SYS_gettid);
 printf("start program in PID:[%d]TID:[%d]\n",getpid(),tid);

获得 ThreadID 的系统调用尚未被 GLibC 标准化,因此这里直接调用 syscall。

然后,主线程初始化创建 Timer 所需要的数据结构:

清单 12,设置通知方式
 se.sigev_notify = SIGEV_THREAD;
 se.sigev_value.sival_ptr = &timer_id;
 se.sigev_notify_function = timer_thread;
 se.sigev_notify_attributes = NULL;
 status = timer_create(CLOCK_REALTIME, &se, &timer_id);

这里将通知方式设为 SIGEV_THREAD,timer_thread 为线程入口函数。

然后主线程设置定时器间隔,并启动 Timer:

清单 13,启动 Timer
 ts.it_value.tv_sec = 5;
 ts.it_value.tv_nsec = 0;
 ts.it_interval.tv_sec = 5;
 ts.it_interval.tv_nsec = 0;
 status = timer_settime(timer_id, 0, &ts, 0);

此后主线程进入一个循环,在循环中等待线程条件变量:

清单 14,主程序中的循环
 while (counter < 5) {
 status = pthread_cond_wait (&cond, &mutex);
}

条件变量 cond 将在 timer_thread() 处理函数中触发,这样每 5 秒钟,定时器将调用 timer_thread() 处理函数,并唤醒主线程等待的条件变量一次。5 次之后测试程序退出。

现在我们看看 timer_thread() 函数:

清单 15,timer_thread 函数
void timer_thread (void *arg)
{
 status = pthread_mutex_lock (&mutex);
 if (++counter >= 5) {
 status = pthread_cond_signal (&cond);
 }
 status = pthread_mutex_unlock (&mutex);
 pid_t tid = (pid_t) syscall (SYS_gettid);
 printf ("Timer %d in PID:[%d]TID:[%d]\n", counter,getpid(),tid);
}

在整个程序中我们都没有使用信号,定时器到期时,将启动新的线程运行 timer_thread。因此在该函数中,我们还打印了当前的线程号以便可以看出它们确实在不同线程中运行。

这里是运行该程序的一个输出:

-bash-3.2$ gcc threadtimer.c -lrt -lpthread -o test
-bash-3.2$ ./test
start program in PID:[21483]TID:[21483]
Timer 1 in PID:[21483]TID:[21498]
Timer 2 in PID:[21483]TID:[21510]
Timer 3 in PID:[21483]TID:[21534]

可以看到每次 Timer 都运行在不同的线程中。


你可能感兴趣的:(linux)