0. 前言
在linux系统中定时器有分为软定时和硬件定时器。硬件定时器一般指的是CPU的一种底层寄存器,它负责按照固定时间频率产生中断信号,形成信号源。基于硬件提供的信号源,系统就可以按照信号中断来计数,计数在固定频率下对应固定的时间,根据预设的时间参数即可产生定时中断信号,这就是软定时。
本文主要整理 Linux 系统开发中常使用的软定时器,而硬件定时器涉及到硬件手册这里略过。
本文会在持续更新过程中将常用定时器逐一整理出来。
1. alarm()
#include <unistd.h>
unsigned int alarm(unsigned int __seconds);
当时间到达 __seconds秒后,进程会受到一个 SIGALRM 的信号。当 __seconds 设置为0时,当前的 alarm定时器将退出。
返回值是一个无符号整型类型。返回之前闹钟的剩余秒数,如果之前未设闹钟则返回0。
注意:
-
每个进程只允许设置一个闹钟,重复设置会覆盖前一个闹钟;
-
当经过指定的 __seconds 之后,信号由内核产生;
可以通过函数signal注册该信号的回调处理函数callback_fun:
#include <signal.h>
typedef void (*sighandler_t)(int);
sig_t signal(int signum, sighandler_t handler);
举例:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
/*闹钟信号处理函数*/
void sig_handler(int signal) {
printf("hello world: %d\n", signal);
}
/*主函数*/
int main() {
int i;
signal(SIGALRM, sig_handler);
alarm(5);
for (i = 0; i < 8; i++)
{
printf(" sleep % d ... \n", i);
sleep(1);
}
return 0;
}
该例子中,首先通过 signale() 捕捉 SIGALRM 信号,并通过 sig_handler() 进行处理,接着通过函数 alarm() 注册了一个 5s 的定时器,然后通过一个 for 循环进行睡眠等待 SIGALRM 到来。当信号 SIGALRM 到来后进程会转到sig_handler() 处执行,执行完该函数后才会回到 main() 中继续执行。
执行结果如下:
sleep 0 ...
sleep 1 ...
sleep 2 ...
sleep 3 ...
sleep 4 ...
hello world: 14
sleep 5 ...
sleep 6 ...
sleep 7 ...
其他信号相关的信息可以查看:《进程间通信——信号(Signal)》
2. setitimer()
setitimer() 类似于 alarm(),同样是通过闹钟,只不过该函数可以精确到微秒。
#include <sys/time.h>
int getitimer(int which, struct itimerval* current_value);
int setitimer(int which, const struct itimerval* new_value, struct itimerval* old_value);
参数:
-
which 定时器计时的方式:
-
ITIMER_REAL: 真实时间,时间到达时发送 SIGALRM;
-
ITIMER_VIRTUAL: 用户时间,时间到达时发送 SIGVTALRM;
-
ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达时发送 SIGPROF信号;
-
new_value 定时器新的定时值;
-
old_value 定时器旧的定时值,最开始设置为 NULL;
返回时:
-
设置成功时,返回0;
-
设置失败时,返回-1,并设置 errno;
下面来看下数据结构 itimerval:
#include <linux/time.h>
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
struct itimerval {
struct timeval it_interval;
struct timeval it_value;
};
如果it_value 中两个值都为0,表示关闭定时器;如果it_value 中至少一个不为0,则表示打开定时器;
如果it_interval中两个值都为0,表示定时器只执行1次;如果 it_interval 中至少一个不为0,则表示定时器是周期性工作;
将上面的实例进行简单的修改:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>
/*闹钟信号处理函数*/
void sig_handler(int signal) {
printf("hello world: %d\n", signal);
}
/*主函数*/
int main() {
int i;
signal(SIGALRM, sig_handler);
struct itimerval new_timer;
new_timer.it_interval.tv_sec = 1;
new_timer.it_interval.tv_usec = 0;
new_timer.it_value.tv_sec = 2;
new_timer.it_value.tv_usec = 0;
setitimer(ITIMER_REAL, &new_timer, NULL);
for (i = 0; i < 8; i++)
{
printf(" sleep % d ... \n", i);
sleep(1);
}
return 0;
}
该例子中,首先通过 signale() 捕捉 SIGALRM 信号,并通过 sig_handler() 进行处理,接着通过函数 setitimer() 注册了一个 1s 的定时器,2s后开始执行,此处与 alarm() 不同,alarm() 指定的定时器触发时进行 sig_handler() 处理,处理完后回到 main() 中,而此处 setitimer() 设定完定时器的时间后就回到了 main() 函数中等待定时器触发。
执行结果如下:
sleep 0 ...
sleep 1 ...
hello world: 14
sleep 2 ...
hello world: 14
sleep 3 ...
hello world: 14
sleep 4 ...
hello world: 14
sleep 5 ...
hello world: 14
sleep 6 ...
hello world: 14
sleep 7 ...
hello world: 14
2s 后定时器触发,开始处理 sig_handler() 且每个 1s 触发一次,而main函数中 for 循环也会同步执行。
3. timer_create()
另外一种依赖信号的定时器为 timer_create(),较 setitimer() 更加灵活,且时间可以精确到纳秒。
#include <time.h>
int timer_create(clockid_t clockid, struct sigevent* event, timer_t* timer_ptr);
int timer_delete(timer_t timer);
int timer_settime(timer_t timer, int flags,
const struct itimerspec* new_value,
struct itimerspec* old_value);
int timer_gettime(timer_t timer, struct itimerspec* ts);
3.1 timer_create()
timer_create() 用以创建一个 POSIX 内部定时器,将定时器的标识 ID 存放到 timer_ptr 中。
参数:
-
clockid定义了定时器计时的方法,有如下几个值 (定义在 linux/time.h 中):
-
CLOCK_REALTIME : 可设置的系统范围的实时时钟;
-
CLOCK_MONOTONIC : 单调递增的时钟,系统启动后不会被改变;
-
CLOCK_PROCESS_CPUTIME_ID : 用于测量当前进程(包括所有线程)CPU占用时间,包含用户调用和系统调用;
-
CLOCK_THREAD_CPUTIME_ID : 用于测量当前线程CPU占用时间,包含用户调用和系统调用;
-
event 指出该如何通知调用者定时器超时信息,详细的数据结构如下。
-
sigev_notify 指定定时器超时处理的方式;
-
SIGEV_NONE : 定时器超时后不使用异步通知,可能的情况是使用timer_gettime来监控定时器;
-
SIGEV_SIGNAL : 一旦超时,产生一个信号,任何时候,至多只有一个信号会发送到队列里面,可以使用timer_getoverrun来获取超时次数;
-
SIGEV_THREAD : 新建一个线程去处理,该线程执行sigev_notif_function为入口函数;
-
SIGEV_THREAD_ID : linux独有,发出一个信号,和SIG_NAL类似,只不过该信号发送到指定的线程,如果 sigev_notify 设置该值时,需要同时指定 _sigev_un._tid 的值,例如使用 gettid();
-
sigev_signo 用以指定定时器超时时发出的信号的值;例如当sigev_notify 指定 SIGEV_THREAD_ID时,需要有信号发出进行处理,此时 sigev_signo 可以设定 SIGALRM
-
timer_ptr 定时器的标识 ID;
注意:如果event被设置为NULL,相当于SIGEV_SIGNAL,信号是SIGALRM;
返回值:
-
创建成功时,返回0;
-
创建失败时,返回-1,并设置 errno;
下面时 sigevent 数据结构:
#include <uapi/asm-generic/siginfo.h>
typedef struct sigevent {
sigval_t sigev_value;
int sigev_signo;
int sigev_notify;
union {
int _pad[SIGEV_PAD_SIZE];
int _tid;
struct {
void (*_function)(sigval_t);
void *_attribute; /* really pthread_attr_t */
} _sigev_thread;
} _sigev_un;
} sigevent_t;
3.2 timer_settime()
timer_settime() 用以启动或停止一个定时器。
参数:
-
timer,由 timer_create() 创建,为定时器的唯一标识;
-
flags,设置定时器时间标识
-
0,启动一个相对定时器,基于当前时间 + 指定的 new_value;
-
TFD_TIMER_ABSTIME,使用绝对时间的定时器,由参数 new_value 决定;
-
TFD_TIMER_CANCEL_ON_SET,如果实时时钟发生改变,退出绝对时间定时器;
-
new_value,定时器新的定时值,根据flags 不同有不同的含义;
-
old_value,定时器旧的定时值,最开始设置为NULL,如果为非NULL,表示之前设置过;
返回值:
-
创建成功时,返回0;
-
创建失败时,返回-1,并设置 errno;
下面来看下数据结构 itimerspec:
#include <uapi/linux/time.h>
struct timespec {
time_t tv_sec;
long tv_nsec;
};
struct itimerspec {
struct timespec it_interval; /*定时器的时间周期,interval*/
struct timespec it_value; /*定时器的时间值,timeout*/
};
如果it_value 中两个值都为0,表示关闭定时器;如果it_value 中至少一个不为0,则表示打开定时器;
如果it_interval中两个值都为0,表示定时器只执行1次;如果 it_interval 中至少一个不为0,则表示定时器是周期性工作;
3.3 timer_delete()
用以删除定时器。参数时 timer_create() 时创建的 timer 唯一标识;
3.4 timer_gettime()
timer_gettime() 用于查询 timer 对应定时器设定的当前时间值。
参数:
-
timer,由 timer_create() 创建,为定时器的唯一标识;
-
ts,返回的当前时间值;
返回值:
-
查询成功时,返回0;
-
查询失败时,返回-1,并设置 errno;
3.5 举例
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <errno.h>
#include <string.h>
/*闹钟信号处理函数*/
void timer_process(int signal) {
printf("hello world: %d\n", signal);
}
timer_t timer_;
bool create_timer(sigset_t *sigset) {
struct sigevent sevent;
sigemptyset(sigset);
sigaddset(sigset, SIGALRM);
if (sigprocmask(SIG_BLOCK, sigset, NULL)) {
printf("sigprocmask failed: %s\n", strerror(errno));
return false;
}
sevent.sigev_notify = SIGEV_SIGNAL;
sevent.sigev_signo = SIGALRM;
if (timer_create(CLOCK_MONOTONIC, &sevent, &timer_)) {
printf("timer_create failed: %s\n", strerror(errno));
return false;
}
return true;
}
bool start() {
struct itimerspec new_timer;
new_timer.it_value.tv_sec = 2;
new_timer.it_value.tv_nsec = 0;
new_timer.it_interval.tv_sec = 1;
new_timer.it_interval.tv_nsec = 0;
if (timer_settime(timer_, 0, &new_timer, NULL)) {
printf("timer_settime failed: %s\n", strerror(errno));
return false;
}
return true;
}
/*主函数*/
int main() {
sigset_t sigset;
int signum;
if (!create_timer(&sigset)) {
printf("timer creation failed!\n");
return 0;
}
start();
while (true) {
if (sigwait(&sigset, &signum) == -1) {
printf("sigwait failed: %s\n", strerror(errno));
}
timer_process(signum);
}
return 0;
}
该例子核心处理有三个地方:
-
create_timer() 封装了创建 timer 的过程,这里使用信号集指定 SIGALRM 信号,接着使用 timer_create() 创建定时器;
-
start() 封装了定时器时间的设定,主要是调用 timer_settime() 启动定时器;
-
while() 循环利用sigwait() 进行 SIGALRM 等待,如果捕获到信号,则会调用 timer_process() 进行处理流程;
4. timerfd
这是以文件描述符的形式监听时间变化,通常跟select/poll/epoll 配合使用。timerfd 涉及三个接口函数:
#include <sys/timerfd.h>
int timerfd_create(clockid_t clockid, int flags);
int timerfd_settime(int fd, int flags,
const struct itimerspec* new_value,
struct itimerspec* old_value);
int timerfd_gettime(int fd, struct itimerspec* current_value);
4.1 timerfd_create()
timerfd_create() 用以创建一个定时器描述符。
参数:
-
clockid定义了定时器计时的方法,有如下几个值 (定义在 linux/time.h 中):
-
CLOCK_REALTIME : 可设置的系统范围的实时时钟;
-
CLOCK_MONOTONIC : 单调递增的时钟,系统启动后不会被改变;
-
CLOCK_PROCESS_CPUTIME_ID : 用于测量当前进程(包括所有线程)CPU占用时间,包含用户调用和系统调用;
-
CLOCK_THREAD_CPUTIME_ID : 用于测量当前线程CPU占用时间,包含用户调用和系统调用;
-
flags 描述符创建标识
-
TFD_NONBLOCK 以非阻塞形式打开描述符,节约额外调用 fcntl() 函数;
-
TFD_CLOEXEC 为新打开的描述符设置 close-on-exec 选项,在 fork + exec后新进程自动关闭该 fd。同样的可以节约额外 open() 调用;
返回值:
-
创建成功时,返回新的 fd;
-
创建失败时,返回-1,并设置 errno;
4.2 timerfd_settime()
timerfd_settime() 用以启动或停止一个定时器。
参数:
-
fd,由 timerfd_create() 创建,为定时器的文件描述符;
-
flags,设置定时器时间标识
-
0,启动一个相对定时器,基于当前时间 + 指定的 new_value;
-
TFD_TIMER_ABSTIME,使用绝对时间的定时器,由参数 new_value 决定;
-
TFD_TIMER_CANCEL_ON_SET,如果实时时钟发生改变,退出绝对时间定时器;
-
new_value,定时器新的定时值,根据flags 不同有不同的含义;
-
old_value,定时器旧的定时值,最开始设置为NULL,如果为非NULL,表示之前设置过;
返回值:
-
创建成功时,返回0;
-
创建失败时,返回-1,并设置 errno;
下面来看下数据结构 itimerspec:
#include <uapi/linux/time.h>
struct timespec {
time_t tv_sec;
long tv_nsec;
};
struct itimerspec {
struct timespec it_interval; /*定时器的时间周期,interval*/
struct timespec it_value; /*定时器的时间值,timeout*/
};
如果it_value 中两个值都为0,表示关闭定时器;如果it_value 中至少一个不为0,则表示打开定时器;
如果it_interval中两个值都为0,表示定时器只执行1次;如果 it_interval 中至少一个不为0,则表示定时器是周期性工作;
4.3 timerfd_gettime()
timerfd_gettime() 用于查询 fd对应定时器设定的当前时间值。
参数:
-
fd,由 timerfd_create() 创建,为定时器的文件描述符;
-
current_value,返回的当前时间值;
返回值:
-
查询成功时,返回0;
-
查询失败时,返回-1,并设置 errno;
4.4 read() 和 close()
timerfd 归根就是一个文件描述符,当配合 poll/epoll 收到监听定时器超时,需要通过read() 读取文件描述符中的buffer,该buffer 是一个无符号 8bytes 的整型数(uint64_t),表示该定时器超时的次数。如果没有超时,read() 将会进行阻塞,阻塞到下一次定时器超时。另外,如果提供的buffer 大小 < 8bytes,read() 将返回EINVAL,read()成功则返回 8.
在不需要定时器的时候,记得通过 close() 进行关闭。
4.5 举例
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/eventfd.h>
#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <unistd.h>
#define EPOLL_SIZE_HINT 128
int mEpollFd = -1;
int fd_process()
{
struct epoll_event eventItems[EPOLL_SIZE_HINT];
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_SIZE_HINT, -1);
int timerFd = -1;
int eventIndex = 0;
uint64_t readCounter;
if (eventCount < 0) {
printf("Poll failed with an unexpected error: %s\n", strerror(errno));
return -1;
}
for (; eventIndex < eventCount; ++eventIndex) {
timerFd = eventItems[eventIndex].data.fd;
int retRead = read(timerFd, &readCounter, sizeof(uint64_t));
if (retRead < 0) {
printf("read %d failed...\n", timerFd);
continue;
} else {
printf("SUCCESS.....\n");
}
}
return 0;
}
/*主函数*/
int main()
{
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
int fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK);
if (fd < 0) {
printf("Could not create timer fd: %s\n", strerror(errno));
return 0;
}
itimerspec timerSet;
timerSet.it_interval.tv_sec = 1;
timerSet.it_interval.tv_nsec = 0;
timerSet.it_value.tv_sec = 2;
timerSet.it_value.tv_nsec = 0;
if (timerfd_settime(fd, 0, &timerSet, NULL) != 0) {
printf("timerfd_settime failed: %s\n", strerror(errno));
close(fd);
return 0;
}
struct epoll_event eventItem;
memset(&eventItem, 0, sizeof(epoll_event));
eventItem.events = EPOLLIN | EPOLLET;
eventItem.data.fd = fd;
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, fd, &eventItem);
if (result != 0) {
printf("Could not add timer fd(%d) to epoll instance: %s\n", fd, strerror(errno));
}
while(true) {
if (fd_process() < 0)
break;
}
return 0;
}
在main() 函数中创建了epoll,通过 timerfd_create() 创建了 timerfd,并通过 timerfd_settime() 创建定时器,定时器的周期为1s,2s 后工作。最后通过 while 循环等待定时器工作 fd_process(),执行结果为:
SUCCESS.....
SUCCESS.....
SUCCESS.....
SUCCESS.....
SUCCESS.....
SUCCESS.....
SUCCESS.....
因为定时器周期为 1s,所以每隔 1s 会通过 read() 读取到定时器超时。
详细的 epoll 使用原理可以查看:《Linux 中的 epoll 原理及使用》
参考:文章来源:https://www.toymoban.com/news/detail-754923.html
http://t.zoukankan.com/houjun-p-4885148.html文章来源地址https://www.toymoban.com/news/detail-754923.html
到了这里,关于Linux 中的几种定时器的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!