【Linux】定时器发出的SIGALRM信号与sleep、usleep、select、poll等函数冲突的解决办法


在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.

猜你喜欢

转载自blog.csdn.net/spiremoon/article/details/107459814