2020-02-22
关键字:alarm()、setitimer()、拦截定时器信号、定时器信号有效范围
在 Linux 中实现定时器功能的比较简单且好用的系统内置的方法有两种:
1、alarm() 函数
2、setitimer() 函数
这两种定时方式都是通过信号(signal)来通知定时到期的。
1、alarm() 函数实现定时功能
alarm()函数的签名如下:
#include <unistd.h> unsigned int alarm(unsigned int seconds);
参数1 是你想定时的秒数。
返回值是在你调用这个函数的时刻,上一次的 alarm() 所剩余的秒数。例如,你上一次调用了 alarm(10),7秒钟以后你再次调用了alarm(6),那么在 alarm(6) 时返回的值就是 3 。
alarm() 的定时是一次性的,若想实现循环定时功能,则需要手动在本次定时结束时再次启动 alarm() 定时。alarm() 在定时到时后会发出一个 SIGALRM 信号,所以还需要我们拦截这一信号才能接收到定时回调结果。
以下贴出通过 alarm() 实现定时功能的示例源码:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <signal.h> static int counter; void sig_alarm_handler(int); int main() { printf("hello world.\n"); printf("pid number:%d\n", getpid()); //拦截定时器信号。 sighandler_t *pre = signal(SIGALRM, sig_alarm_handler); printf("signal() return ret address:%p,my sig_alm_handler:%p\n", pre, sig_alarm_handler); //pre应该是空才对。 //设定定时器。 int remaing = alarm(1); printf("alarm remaing:%d\n", remaing);//remaing 应该是0才对。 counter = 0; while(counter < 7) { usleep(500); } //主动关闭定时器。 alarm(0); printf("bye\n"); return 0; } void sig_alarm_handler(int sig_num) { printf("%s, signal number:%d, counter:%d\n", __FUNCTION__, sig_num, counter); if(sig_num = SIGALRM) { counter++; int remaing = alarm(1); printf("re-alarm remaing:%d\n", remaing); } }
这一程序的运行结果打印如下:
root@xxx:/system/bin # ./myblog hello world. pid number:4120 signal() return ret address:0x0,my sig_alm_handler:0xb6fca47d alarm remaing:0 sig_alarm_handler, signal number:14, counter:0 re-alarm remaing:0 sig_alarm_handler, signal number:14, counter:1 re-alarm remaing:0 sig_alarm_handler, signal number:14, counter:2 re-alarm remaing:0 sig_alarm_handler, signal number:14, counter:3 re-alarm remaing:0 sig_alarm_handler, signal number:14, counter:4 re-alarm remaing:0 sig_alarm_handler, signal number:14, counter:5 re-alarm remaing:0 sig_alarm_handler, signal number:14, counter:6 re-alarm remaing:0 bye root@xxx:/system/bin #
2、setitimer() 函数实现定时功能
setitimer()函数的签名如下所示:
#include <sys/time.h> int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
参数1 表示要启动的定时器类型,setitimer 方式为每个进程提供了三种类型的定时器:1、ITIMER_REAL;2、ITIMER_VIRTUAL;3、ITIMER_PROF;第1种定时器一旦启动立即执行定时计时,定时结束后产生一个 SIGALRM 信号并可根据启动前的配置决定是否自动重启下一轮定时。第2种定时器在启动后将只在进程处于运行态时会工作,当进程处于非运行态时,定时工作也将暂停。且这种定时器在定时结束后会产生一个 SIGVTALRM 信号。第3种定时器类型暂且不理。
参数2 是用于描述定时器下一轮要执行的规则的,说白了就是你想定时多久就在这里设置。结构体 struct itimerval 的原型如下图所示:
参数3 一般不用理会。
返回值为0表示启动定时器成功,失败时返回-1并设置相应的错误信息。
setitimer() 实现定时的方式会稍微复杂一点,但同样它的功能相较于 alarm() 也更强大一点。
alarm() 方式的定时精度只能达到“秒级”,但 setitimer() 却可以达到 “微秒级”。同时,setitimer() 还可以根据进程的不同运行状态来控制定时功能的运行状态。setitimer() 是一个可以在本轮定时任务完成后自动重启下一轮定时的定时机制,当然是否自动重启取决于启动定时器时参数2的值。在上面函数参数2的释义中,struct itimerval 中的 it_value 成员表示定时器当前的值,它可以设置一个秒值及微秒值,其实就是你想让这个定时器在多少时间以后启动,立即启动则将 it_value 的值全设成0。而 it_interval 则表示你想定时多少时间。假如我们给 it_value 设置了一个值,但 it_interval 却全设成0,就表示定时器将在 it_value 设置的时间以后发出一个定时到时信号,此后就不会再自动重启下一轮定时任务了。
取消一个正在运行的 itimerval 定时器的方法是调用以下函数:
//方式一 setitimer(type, NULL, NULL);//type为 ITIMER_REAL 或 ITIMER_VIRTUAL 或 ITIMER_PROF //方式二 struct itimerval itv; itv.it_interval.tv_sec = 0; itv.it_interval.tv_usec = 0; itv.it_value.tv_sec = 0; itv.it_value.tv_usec = 0; setitimer(type, &itv, NULL);
如果想查询正在运行着的 itimerval 定时器的状态信息,比如当前运行到什么状态了,还有多少时间定时结束,下一轮定时时间被设成多少,则可以调用以下函数:
#include <sys/time.h> int getitimer(int which, struct itimerval *curr_value);
参数1 是定时器的类型,即前面描述的三种类型之一。
参数2 是一个用于保存结果的结构体对象,需要注意这里不能传空指针,否则将查询不到结果。
返回值为0表示查询成功,可以通过 curr_value 查看结果。返回值为-1则表示查询失败。
以下是一个通过 setitimer() 函数实现的定时器功能的示例源码:
#include <stdio.h> #include <signal.h> #include <sys/time.h> void sig_alm_handler(int sig_num); static int counter; int main() { printf("hello world.\n"); //拦截定时器信号。 sighandler_t *pre = signal(SIGALRM, sig_alm_handler); printf("pre-sighandler address:%p\n", pre); //pre应该是NULL. struct itimerval olditv; struct itimerval itv; itv.it_interval.tv_sec = 1; //定时周期为1秒钟。 itv.it_interval.tv_usec = 0; itv.it_value.tv_sec = 3; //定时器启动以后将在3秒又500微秒以后正式开始计时。 itv.it_value.tv_usec = 500; setitimer(ITIMER_REAL, &itv, &olditv); while(counter < 7) { usleep(500); } //try to cancle the timer. int counter2 = 0; while(counter2 < 7) { /* 通过这个while可以监测到定时器是否成功被停止, 若未停止,则仍旧可以在每秒看到定时信号回调函数中的打印。 通过这个while可以确保定时器是真的被我们的设置而停掉的 而非因为程序结束才强制停止的。 */ sleep(1); //try to stop the timer. setitimer(ITIMER_REAL, NULL, NULL); printf("try cancled!,counter2:%d\n", counter2); counter2++; } printf("\nBye.\n"); return 0; } void sig_alm_handler(int sig_num) { printf("%s, signal number:%d, counter:%d\n", __FUNCTION__, sig_num, counter); if(sig_num = SIGALRM) { counter++; } }
这段程序运行的结果打印如下:
root@xxx:/system/bin # ./myblog hello world. pre-sighandler address:0x0 sig_alm_handler, signal number:14, counter:0 sig_alm_handler, signal number:14, counter:1 sig_alm_handler, signal number:14, counter:2 sig_alm_handler, signal number:14, counter:3 sig_alm_handler, signal number:14, counter:4 sig_alm_handler, signal number:14, counter:5 sig_alm_handler, signal number:14, counter:6 sig_alm_handler, signal number:14, counter:7 try cancled!,counter2:0 try cancled!,counter2:1 try cancled!,counter2:2 try cancled!,counter2:3 try cancled!,counter2:4 try cancled!,counter2:5 try cancled!,counter2:6 Bye. root@xxx:/system/bin #
3、番外篇
这两种定时器的的有效范围都仅在本进程内。不必担心调用系统定时器并产生的 SIGALRM 信号会被其它进程通过拦截定时信号而接收到。以下有相应的源码与打印可以证明。
证明的原理也不难,就是在两种定时方式将定时器启动以后 fork() 一个子进程出来,并在定时信号回调函数中打印出当前进程号,查看这个回调是哪一个进程中调用的。同时在 fork() 出的子进程中主动发送一个定时信号以证明在子进程中定时信号拦截也是生效的。
以下是 alarm() 方式的源码与打印:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <signal.h> static int counter; static int is_sub_process; void sig_alarm_handler(int); int main() { printf("hello world.\n"); printf("pid number:%d\n", getpid()); is_sub_process = 0; //拦截定时器信号。 sighandler_t *pre = signal(SIGALRM, sig_alarm_handler); printf("signal() return ret address:%p,my sig_alm_handler:%p\n", pre, sig_alarm_handler); //pre应该是空才对。 //设定定时器。 alarm(1); counter = 0; pid_t pno = fork(); printf("pno:%d\n", pno); is_sub_process = !pno; if(is_sub_process) { raise(SIGALRM);//子进程中主动发一个定时结束信号。 } while(counter < 3) { usleep(500); } //主动关闭定时器。 alarm(0); printf("bye\n"); return 0; } void sig_alarm_handler(int sig_num) { printf("%s, signal number:%d, counter:%d,pid:%d\n", __FUNCTION__, sig_num, counter, getpid()); if(sig_num = SIGALRM) { counter++; if(!is_sub_process) { alarm(1); } } }
打印如下:
root@xxx:/system/bin # ./myblog hello world. pid number:5272 signal() return ret address:0x0,my sig_alm_handler:0xb6efe4d5 pno:5273 pno:0 sig_alarm_handler, signal number:14, counter:0,pid:5273 sig_alarm_handler, signal number:14, counter:0,pid:5272 sig_alarm_handler, signal number:14, counter:1,pid:5272 sig_alarm_handler, signal number:14, counter:2,pid:5272 bye root@xxx:/system/bin #
通过打印可以很明显地发现,主、子进程都有在拦截 SIGALRM 信号,并且只有主进程能接收到定时信号。且在主进程定时结束程序退出以后,通过 ps 仍能发现子进程在不断运行当中,如下图:
子进程不退出的原因就是因为子进程没有定时信号,counter 变量一直不增加,导致 while(1) 一直无法退出。
以下是 setitimer() 方式的源码与打印:
#include <stdio.h> #include <signal.h> #include <unistd.h> #include <sys/time.h> void sig_alm_handler(int sig_num); static int counter; static int is_sub_process; int main() { printf("hello world.\n"); is_sub_process = 0; //拦截定时器信号。 sighandler_t *pre = signal(SIGALRM, sig_alm_handler); printf("pre-sighandler address:%p\n", pre); //pre应该是NULL. pid_t pno = fork(); printf("pno:%d\n", pno); if(pno >= 0) { is_sub_process = !pno; if(is_sub_process) { //子进程模拟一个SIGALRM信号出来。 raise(SIGALRM); } else { //仅主进程启动定时器。 struct itimerval itv; itv.it_interval.tv_sec = 1; //定时周期为1秒钟。 itv.it_interval.tv_usec = 0; itv.it_value.tv_sec = 3; //定时器启动以后将在3秒又500微秒以后正式开始计时。 itv.it_value.tv_usec = 500; setitimer(ITIMER_REAL, &itv, NULL); } } while(counter < 3) { usleep(500); } if(!is_sub_process) { //仅主进程才需要停止 struct itimerval curitv; memset(&curitv, 0, sizeof(struct itimerval)); setitimer(ITIMER_REAL, NULL, &curitv);//定时器停止之前的定时计数值状态将被保存在 curitv 中。 printf("The status before timer cancle is sec:%d, usec:%d, isec:%d, iusec:%d\n", curitv.it_value.tv_sec, curitv.it_value.tv_usec, curitv.it_interval.tv_sec, curitv.it_interval.tv_usec); } printf("\nBye.\n"); return 0; } void sig_alm_handler(int sig_num) { printf("%s, signal number:%d, counter:%d,pid:%d\n", __FUNCTION__, sig_num, counter, getpid()); if(sig_num = SIGALRM) { counter++; } }
setitimer() 的验证机制与 alarm() 的稍有不同,它是在主进程中才去启动定时器,但得到的结果却与 alarm() 的方式完全一样。这两份代码在某种程度上可以认为是一个双盲对照实验了,由此可以充分证明 alarm() 与 setitimer() 所启动的定时器及产生的定时信号均只在本进程内有效。
以下是 setitimer() 方式验证的打印:
root@xxx:/system/bin # ./myblog hello world. pre-sighandler address:0x0 pno:5323 pno:0 sig_alm_handler, signal number:14, counter:0,pid:5323 sig_alm_handler, signal number:14, counter:0,pid:5322 sig_alm_handler, signal number:14, counter:1,pid:5322 sig_alm_handler, signal number:14, counter:2,pid:5322 The status before timer cancle is sec:0, usec:999417, isec:1, iusec:0 Bye. root@xxx:/system/bin #
在主进程因定时结束而退出以后仍能通过 ps 发现子进程卡在 while(1) 中退不出来,如下图所示: