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 { |
其中 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, |
第一次看到 timer_settime 的参数列表或许会令人觉得费解。先来看看 new_value 和 old_value,它们都是 struct itimerspec 数据结构。
struct itimerspec |
启动和停止 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); |
获得 ThreadID 的系统调用尚未被 GLibC 标准化,因此这里直接调用 syscall。
然后,主线程初始化创建 Timer 所需要的数据结构:
清单 12,设置通知方式
se.sigev_notify = SIGEV_THREAD; |
这里将通知方式设为 SIGEV_THREAD,timer_thread 为线程入口函数。
然后主线程设置定时器间隔,并启动 Timer:
清单 13,启动 Timer
ts.it_value.tv_sec = 5; |
此后主线程进入一个循环,在循环中等待线程条件变量:
清单 14,主程序中的循环
while (counter < 5) { |
条件变量 cond 将在 timer_thread() 处理函数中触发,这样每 5 秒钟,定时器将调用 timer_thread() 处理函数,并唤醒主线程等待的条件变量一次。5 次之后测试程序退出。
现在我们看看 timer_thread() 函数:
清单 15,timer_thread 函数
void timer_thread (void *arg) |
在整个程序中我们都没有使用信号,定时器到期时,将启动新的线程运行 timer_thread。因此在该函数中,我们还打印了当前的线程号以便可以看出它们确实在不同线程中运行。
这里是运行该程序的一个输出:
-bash-3.2$ gcc threadtimer.c -lrt -lpthread -o test |
可以看到每次 Timer 都运行在不同的线程中。