在c、c++开发中,如果使用定时器,我们经常会使用posix timer。posix timer使用较为灵活,本文介绍posix timer的使用。
1example
如下是使用posix timer的一个例子。主要使用了3个api:timer_create用于创建一个timer,但是timer创建之后,并没有启动;使用timer_settime可以启动定时器;使用timer_delete可以删除定时器。
使用posix timer编译的时候要带-lrt。
struct itimerspec {
struct timespec it_value; // 定时器启动之后,第一次执行的时间
struct timespec it_interval; // 如果设置为0,那么定时器只执行一次;大于0,则是定时器的周期
};
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>void timer_cb(union sigval sigev_value) { printf("timer callback\n"); }int main() {struct sigevent sev;struct itimerspec its;timer_t timer_id;sev.sigev_notify = SIGEV_THREAD;sev.sigev_notify_function = timer_cb;sev.sigev_notify_attributes = NULL;timer_create(CLOCK_BOOTTIME, &sev, &timer_id);its.it_interval.tv_sec = 1;its.it_interval.tv_nsec = 0;its.it_value.tv_sec = 1;its.it_value.tv_nsec = 0;timer_settime(timer_id, 0, &its, NULL);sleep(10);timer_delete(timer_id);return 0;
}
2时钟源
timer_create的第一个参数是clockid,表示时钟的类型。
int timer_create(clockid_t clockid, struct sigevent *sevp,
timer_t *timerid);
linux中的时钟id比较多,在定时器中经常使用CLOCK_BOOTTIME或CLOCK_MONOTONIC。
CLOCK_REALTIME | 从1970年1月1日8点开始的时间,这个就是系统时间,也叫墙上时间,比如2024年10月10日10时10分10秒。 如果系统的时间会变化,比如系统开启了NTP网络时钟同步,这样的话系统时间就会不断变化调整。 这种时钟不适合用在定时器中,因为时钟变化会影响时钟。适用于获取当前时间。 |
CLOCK_BOOTTIME | 系统启动之后开始计时,适合用在定时器中。 |
CLOCK_MONOTONIC | 系统启动之后开始计时,与CLOCK_BOOTTIME的区别是,如果系统休眠了,那么CLOCK_BOOTTIME也会继续计时;而CLOCK_MONOTONIC不会。 |
CLOCK_PROCESS_CPUTIME_ID | 用于计量进程实际消耗的cpu时间。 |
CLOCK_THREAD_CPUTIE_ID | 用于计量线程实际消耗的cpu时间。 |
3定时器触发方式
触发方式指的是定时器超时之后,以什么样的方式来触发调用定时器回调函数。posix timer中提供了如下 4 种方式。
信号通知方式 | 说明 |
SIGEV_NONE | 超时之后什么都不做,需要用户调用函数timer_gettime来确定定时器是不是超时。 |
SIGEV_SIGNAL | 当定时器超时,向进程发信号。 信号对整个进程都是有效的。也就是说,对于多线程程序来说,如果线程中有 sleep 或者其它阻塞的话,那么信号同样也会将这些阻塞唤醒,所以就需要对信号有额外的控制,控制起来比较复杂。 |
SIGEV_THREAD | posix内部会创建一个线程用来执行用户注册的定时器回调函数 |
SIGEV_THREAD_ID | 需要用户自己创建一个线程,该线程会调用定时器回调函数 |
在工作中,经常使用SIGEV_THREAD和SIGEV_THREAD_ID。SIGEV_THREAD,每个定时器超时需要执行的时候都会创建一个线程,执行完毕,线程销毁,线程有底层库实现,不需要用户关心,使用方便,但是每次都要创建并销毁线程,对性能不友好;SIGEV_THREAD_ID中使用的线程需要由用户自己创建,使用稍复杂,但是用户可以进行更精确的控制。SIGEV_THREAD_ID在实际使用中,往往一个线程要使用一个线程 。
SIGEV_THREAD和SIGEV_THREAD_ID,针对每一个定时器都需要创建一个线程。如果我们想要使用多个定时器共用一个线程呢,这个时候,我们可以使用SIGEV_NONE,这种方式,我们可以使用一个线程,每隔一定时间(假设为T)检查所有定时器是不是超时,超时则执行回调函数,这里的T就是定时器的精度。
3.1SIGEV_NONE
如下是一个简单的例子,使用一个线程管理3个定时器。
(1)使用its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0来判断定时器是不是超时
(2)it_interval需要设置为0,timer_gettime获取的是定时器剩余的时间,如果不设置为0的话,那么定时器超时之后,会再一次进行计时,那么线程很难捕获到its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0条件满足的情况。
struct itimerspec its;
its.it_value.tv_sec = 3;
its.it_value.tv_nsec = 0;
its.it_interval.tv_sec = 0;
its.it_interval.tv_nsec = 0;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>#define NUM_TIMERS 3
typedef void (*CB)();
CB timer_cb[NUM_TIMERS];
timer_t timer_id[NUM_TIMERS];void cb1() {printf("timer 1\n");
}void cb2() {printf("timer 2\n");
}void cb3() {printf("timer 3\n");
}int create_timer(int seq, CB cb) {timer_t timerid;struct sigevent sev;sev.sigev_notify = SIGEV_NONE;sev.sigev_value.sival_ptr = &timerid;if (timer_create(CLOCK_BOOTTIME, &sev, &timerid) == -1) {perror("timer_create");return -1;}struct itimerspec its;its.it_value.tv_sec = 3;its.it_value.tv_nsec = 0;its.it_interval.tv_sec = 0;its.it_interval.tv_nsec = 0;if (timer_settime(timerid, 0, &its, NULL) == -1) {perror("timer_settime");return -1;}printf("seq %d, cb %p\n", seq, cb);cb();timer_cb[seq] = cb;timer_id[seq] = timerid;return 0;
}void* timer_manager() {while (1) {for (int i = 0; i < NUM_TIMERS; i++) {struct itimerspec its;if (timer_gettime(timer_id[i], &its) == -1) {perror("timer_gettime");continue;}if (its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0) {printf("call timer call back, cb %p\n", timer_cb[i]);timer_cb[i]();its.it_value.tv_sec = 3;its.it_value.tv_nsec = 0;timer_settime(timer_id[i], 0, &its, NULL);}}usleep(4 * 1000);}return NULL;
}int main() {create_timer(0, cb1);create_timer(1, cb2);create_timer(2, cb3);pthread_t thread_id;if (pthread_create(&thread_id, NULL, timer_manager, NULL) != 0) {perror("pthread_create");exit(EXIT_FAILURE);}sleep(60);for (int i = 0; i < NUM_TIMERS; i++) {timer_delete(timer_id[i]);}return 0;
}
3.2SIGEV_THREAD
如下是使用SIGEV_THREAD触发方式,创建了两个定时器,周期分别为5s和3s,定时器回调函数分别为timer_cb5和timer_cb3,在回调函数中打印了线程id。
如下是运行时的打印,从打印可以看出来,定时器函数每次调用,都会创建一个新的线程。
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>#define _GNU_SOURCE
#include <unistd.h>
#include <sys/types.h>void timer_cb5(union sigval sigev_value) { printf("timer callback 5, tid %d\n", gettid()); }
void timer_cb3(union sigval sigev_value) { printf("timer callback 3, tid %d\n", gettid()); }void create_timer(int period_in_second, void *cb) {struct sigevent sev;struct itimerspec its;timer_t timer_id;sev.sigev_notify = SIGEV_THREAD;sev.sigev_notify_function = cb;sev.sigev_notify_attributes = NULL;timer_create(CLOCK_BOOTTIME, &sev, &timer_id);its.it_interval.tv_sec = period_in_second;its.it_interval.tv_nsec = 0;its.it_value.tv_sec = period_in_second;its.it_value.tv_nsec = 0;timer_settime(timer_id, 0, &its, NULL);
}int main() {create_timer(5, timer_cb5);create_timer(3, timer_cb3);sleep(100);return 0;
}
3.3SIGEV_THREAD_ID
SIGEV_THREAD中的线程是posix库内部创建的线程,并且定时器的吗诶个周期都是创建一个线程,销毁一个线程。
SIGEV_THREAD_ID使用的是用户创建的线程,这样便于用户进行精确控制。在实际使用中,定时器是有精度误差的,特别是在linux这种非确定的操作系统中,用户态的定时器还受调度精度影响,并且误差不可预测。如果我们对某个定时器的精度要求高,那么可以将定时器线程设置为实时调度策略,来优化精度表现。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <time.h>
#include <unistd.h>#define _GNU_SOURCE
#include <sys/types.h>void sig_handler(int a, siginfo_t *b, void *c) {printf("signal handler, tid %d, signal %d\n", gettid(), a);return;
}void create_timer(int signum, int period_in_second, void *cb) {struct sigaction sa;sa.sa_flags = 0;sa.sa_sigaction = cb;sigemptyset(&sa.sa_mask);sigaction(signum, &sa, NULL);sigevent_t event;timer_t timerid;event.sigev_notify = SIGEV_THREAD_ID;event.sigev_signo = signum;event._sigev_un._tid = gettid();event.sigev_value.sival_ptr = NULL;timer_create(CLOCK_BOOTTIME, &event, &timerid);struct itimerspec its;its.it_interval.tv_sec = period_in_second;its.it_interval.tv_nsec = 0;its.it_value.tv_sec = period_in_second;its.it_value.tv_nsec = 0;timer_settime(timerid, 0, &its, NULL);
}void *thread_func(void *arg) {create_timer(34, 3, sig_handler);create_timer(34, 5, sig_handler);while(1);return NULL;
}int main() {pthread_t thread;pthread_create(&thread, NULL, thread_func, NULL);sleep(100);return 0;
}