Linux定时器发出的SIGALRM信号与sleepusleepselectpoll等函数冲突的解决办法
Posted 菜老越
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux定时器发出的SIGALRM信号与sleepusleepselectpoll等函数冲突的解决办法相关的知识,希望对你有一定的参考价值。
文章目录
在Linux应用编程时,有需要用到定时器的场合,类似于单片机内的定时器中断——规定时间到了之后执行特定代码。
一、定时器的使用简介
以下为定时器的一个简单例子,初始化定时器后每隔一秒调用一次TimerFuc进行打印。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
void TimerFuc()
time_t tick;
tick = time(NULL);
printf("[Tick:%d]This is TimeFuc\\n",tick);
void InitTimer()
struct itimerval tTimer;
memset((void *)&tTimer,0,sizeof(tTimer));
//初始化1us后启动定时器,定时周期1s
tTimer.it_value.tv_sec = 0;
tTimer.it_value.tv_usec = 1;
tTimer.it_interval.tv_sec = 1;
tTimer.it_interval.tv_usec = 0;
//绑定
signal(SIGALRM, &TimerFuc);
setitimer(ITIMER_REAL, &tTimer, NULL);
int main()
InitTimer();
while(1)
return 0;
在InitTimer初始化定时器函数中,定义struct itimerval结构体,如下阅读其头文件的说明,可知it_interval为定时器截止后的重载值,也可以理解为该定时器的周期;it_value可以理解为倒计时,定时器开启后,将递减it_value的值,当其减到0时发出信号,并将it_value重新赋为it_interval,然后继续递减。这和单片机中的递减定时器非常相似。
signal(SIGALRM, &TimerFuc)则将SIGALRM与“中断函数”TimerFuc绑定到了一起,即每次发出SIGALRM信号,就执行TimerFuc。
/* Type of the second argument to `getitimer' and
the second and third arguments `setitimer'. */
struct itimerval
/* Value to put into `it_value' when the timer expires. */
struct timeval it_interval;
/* Time to the next timer expiration. */
struct timeval it_value;
;
配置好定时器后,如程序中配置,1us后触发一次TimerFuc,之后每1秒触发一次TimerFuc,之后需要通过setitimer来开启定时器。先看看setitimer的原型:
/* Set the timer WHICH to *NEW. If OLD is not NULL,
set *OLD to the old value of timer WHICH.
Returns 0 on success, -1 on errors. */
extern int setitimer (__itimer_which_t __which,
const struct itimerval *__restrict __new,
struct itimerval *__restrict __old) __THROW;
其有三个参数:
1.__which,表示定时器截止后将发出什么信号,这里有三个枚举值:
(1)ITIMER_REAL, //Timers run in real time. 以系统真实的时间来计算,它送出SIGALRM信号;
(2)ITIMER_VIRTUAL,//Timers run only when the process is executing. 以该进程在用户态下花费的时间来计算,即该进程占用CPU的时间,它送出SIGVTALRM信号;
(3)ITIMER_PROF,//Timers run when the process is executing and when the system is executing on behalf of the process. 以该进程在用户态下和内核态下所费的时间来计算。它送出SIGPROF信号。
2.__new: 传入配置好的定时器配置,让定时器工作在配置好的延时、周期下;
3.__old 通常用不上,设置为NULL,它是用来存储上一次setitimer调用时设置的new_value值。
以上,我们开启了定时器,由于setitimer的第一个参数为ITIMER_REAL,故定时器将在规定的截至时间时发出SIGALRM信号,而我们又把SIGALRM绑定到了TimerFuc函数。由此一来,一个简单的“定时器中断”形成了。
运行结果,每隔一秒打印一次:
wy@wy-VirtualBox:~/test/timertest$ ./a.out
[Tick:1595225445]This is TimeFuc
[Tick:1595225446]This is TimeFuc
[Tick:1595225447]This is TimeFuc
[Tick:1595225448]This is TimeFuc
[Tick:1595225449]This is TimeFuc
[Tick:1595225450]This is TimeFuc
[Tick:1595225451]This is TimeFuc
二、使用定时器将带来的问题
由于我们在使用定时器时,大多时候想使周期时间以系统时间为准。故setitimer的第一个参数只能选择ITIMER_REAL,否则定时器周期将会不稳定增长。如我设定1秒的周期,若setitimer的第一个参数为ITIMER_VIRTUAL,那么实际定时器的周期会大于1秒,因为ITIMER_VIRTUAL只在该进程占有CPU时进行定时器递减;而若setitimer的第一个参数为ITIMER_PROF,那么周期仍会略微大于1秒,因为只在该进程占用CPU时,以及运行内核调度时进行定时器递减。
那么既然选择了ITIMER_REAL,定时器递减到0时将发出SIGALRM信号。不要小看这个SIGALRM信号,通过阅读说明、源码看出,linux中的sleep、usleep、select、poll等函数的超时结果均也是使用的SIGALRM作为结束信号。若你的定时器按一定周期一直在发SIGALRM信号,那么以上几个函数均有极大可能被提前打断,当你的定时器周期设的很短,比如10ms时,以上几个函数几乎变得不可使用。
可以通过下面这个实验验证,将刚刚的代码做以下修改。在while(1)中循环打印,并在其中使用sleep函数期望这个打印以一定周期进行。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
void TimerFuc()
time_t tick;
tick = time(NULL);
printf("[Tick:%d]This is TimeFuc\\n",tick);
void InitTimer()
struct itimerval tTimer;
memset((void *)&tTimer,0,sizeof(tTimer));
//初始化1us后启动定时器,定时周期1s
tTimer.it_value.tv_sec = 0;
tTimer.it_value.tv_usec = 1;
tTimer.it_interval.tv_sec = 1;
tTimer.it_interval.tv_usec = 0;
//绑定
signal(SIGALRM, &TimerFuc);
setitimer(ITIMER_REAL, &tTimer, NULL);
int main()
InitTimer();
time_t tick;
while(1)
sleep(2);
tick = time(NULL);
printf("[Tick:%d]This is in while(1)\\n",tick);
return 0;
运行结果如下:
wy@wy-VirtualBox:~/test/timertest$ ./a.out
[Tick:1595226677]This is TimeFuc
[Tick:1595226677]This is in while(1)
[Tick:1595226678]This is TimeFuc
[Tick:1595226678]This is in while(1)
[Tick:1595226679]This is TimeFuc
[Tick:1595226679]This is in while(1)
[Tick:1595226680]This is TimeFuc
[Tick:1595226680]This is in while(1)
[Tick:1595226681]This is TimeFuc
[Tick:1595226681]This is in while(1)
[Tick:1595226682]This is TimeFuc
[Tick:1595226682]This is in while(1)
我们发现,This is in while(1)根本不是按预想的2秒周期打印,为什么,因为他在sleep时,被定时器发出的SIGALRM打断了。那么在sleep周期短于定时器周期时,sleep实际的睡眠时间将变成定时器的周期。同样的usleep、selet、poll这几个重要函数,将也变得无法正常使用。
三、以上问题的解决办法
1.通过多线程解决
可以将定时器初始化移植多线程中,并在主线程中屏蔽SIGALRM信号解决。这样一来,多线程中来了SIGALRM信号该怎么处理怎么处理,主线程中来了SIGALRM信号直接无视,从而就不存在SIGALRM打断sleep的问题了。
实现代码如下:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>
void TimerFuc()
time_t tick;
time(&tick);
printf("[Tick:%d]This is TimeFuc\\n",tick);
void InitTimer()
struct itimerval tTimer;
memset((void *)&tTimer,0,sizeof(tTimer));
//初始化1us后启动定时器,定时周期1s
tTimer.it_value.tv_sec = 0;
tTimer.it_value.tv_usec = 1;
tTimer.it_interval.tv_sec = 1;
tTimer.it_interval.tv_usec = 0;
//绑定
signal(SIGALRM, &TimerFuc);
setitimer(ITIMER_REAL, &tTimer, NULL);
void f_thr_timer()
InitTimer();
while(1);
int main()
time_t tick;
pthread_t thr_timer;
pthread_create(&thr_timer,NULL,f_thr_timer,NULL);
pthread_detach(thr_timer);
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGALRM);
pthread_sigmask(SIG_SETMASK,&set,NULL);
while(1)
sleep(2);
tick = time(NULL);
printf("===================================[Tick:%d]This is in while(1)\\n",tick);
return 0;
主要就是创建了一个线程,将定时器初始化、定时器处理函数移到了多线程中。然乎在主线程中对SIGALRM信号进行了屏蔽。运行程序结果如下:
wy@wy-VirtualBox:~/test/timertest$ ./a.out
[Tick:1595230890]This is TimeFuc
[Tick:1595230891]This is TimeFuc
===================================[Tick:1595230892]This is in while(1)
[Tick:1595230892]This is TimeFuc
[Tick:1595230893]This is TimeFuc
[Tick:1595230894]This is TimeFuc
===================================[Tick:1595230894]This is in while(1)
[Tick:1595230895]This is TimeFuc
[Tick:1595230896]This is TimeFuc
===================================[Tick:1595230896]This is in while(1)
[Tick:1595230897]This is TimeFuc
[Tick:1595230898]This is TimeFuc
===================================[Tick:1595230898]This is in while(1)
[Tick:1595230899]This is TimeFuc
[Tick:1595230900]This is TimeFuc
===================================[Tick:1595230900]This is in while(1)
[Tick:1595230901]This is TimeFuc
[Tick:1595230902]This is TimeFuc
===================================[Tick:1595230902]This is in while(1)
可以看到定时器触发的打印 每一秒打印一次。而主线程的while(1)中的打印也按sleep(2)进行打印,没有受到SIGALRM信号的影响。
由上得出结论:使用定时器使时其移至一个单独的线程,然后在需要调用sleep、usleep、select、poll函数的线程中屏蔽SIGALRM信号即可。
2.通过写延时函数解决
2.1 解决sleep、usleep函数
也可以通过自己写阻塞函数来模拟sleep与usleep。延时函数使用gettimeofday函数实现,代码如下所示:
void usSleep(int nUs)
struct timeval begin;
struct timeval now;
int pastUs = 0;
gettimeofday(&begin,NULL);
now.tv_sec = begin.tv_sec;
now.tv_usec = begin.tv_usec;
while(pastUs < nUs)
gettimeofday(&now,NULL);
pastUs = (now.tv_sec - begin.tv_sec) * 1000000 - begin.tv_usec + now.tv_usec;
这样一来,sleep和usleep均可使用这个函数来实现,由于不再依赖SIGALRM信号,故不会被定时器发出的SIGALRM信号打断,精度达到微秒级,比较实用。
2.2 解决select、poll函数
由于用read读字符终端设备、网络socket、管道时都是默认阻塞的,所以在使用read函数使往往需要配合select、poll函数实现,这两个函数均有一个timeout,当timeout内read的描述符仍无可读数据时,将返回错误,以不至于傻傻的阻塞在read函数上。
既然了解了select、poll的原理,那么我们也可以自己写一个简单的方法来模拟。首先在打开设备时,open的第二个参数要或上O_NONBLOCK,如:
open(ttyName,O_RDWR | O_NONBLOCK)
表示以非阻塞方式打开文件,那么在read时,若不可读则会瞬间返回-1.所以模拟select、poll的方法可如下编写:
int my_poll(int fd,int timeout_ms,unsigned char *des,int len)
int res = 0;
if(timeout_ms <= 0)
return res;
while(timeout_ms--)
res = read(fd,des,len);
if(res > 0)
break;
usSleep(1000);
return res;
四个参数:
1.fd,文件描述符;
2.timeout_ms,等待fd可读的超时时间;
3.des,读的目的地址,即目的buffer;
4.欲读出的长度,一般为buffer的长度;
返回值:
-1:timeout时间内未读出数据;
0:timeout时间内未读出数据;
大于0:读出的数据长度。
四、总结
1.可以使用setitimer实现定时器,但要注意SIGALRM对特殊函数的影响;
2.可利用多线程的方式解决定时器SIGALRM对特殊函数的影响;
3.可通过自己编写延时函数解决定时器SIGALRM对特殊函数的影响;
4.应优先使用方法2.
以上是关于Linux定时器发出的SIGALRM信号与sleepusleepselectpoll等函数冲突的解决办法的主要内容,如果未能解决你的问题,请参考以下文章