C++实现简单定时器――Unix版
本文介绍了如何在Unix平台使用面向对象的设计方法实现一个简单的定时器的过程,以及介绍了与实现定时器功能相关的Unix信号的捕捉和定时发送SIGALRM信号的技术基础知识;最后对所实现的CSimpleTimer类进行了测试。
在软件开发中,我们经常遇到这样的需求:要求系统每隔一段时间自动执行一系列的任务。例如:foxmail每隔一段时间收取一次邮件、笨笨钟定时提醒功能、企业在下班后要自动进行日结算业务等等。这些要求系统自动执行一些列的任务无一不与定时器相关。
一般来说,一台计算机只要少量的硬件定时器,操作系统会使用硬件定时器来虚拟出很多软件定时器。在Unix平台,系统就可以为每一个进程通过异步信号的方式,虚拟出一个简单的定时器功能,即,在进程内部,使用定时发送SIGALRM信号和对该信号的捕捉实现定时器的功能。由于这种方式的定时器是基于信号的,所以,每一个进程只有一个定时器。当然,我们也可以基于这个定时器再虚拟出更多的定时器,那将更为复杂。
本文只根据最基本的异步信号和对异步信号的捕捉来实现一个简单的定时器,该定时器在一个进程内只能有一个实例!
本章介绍实现定时器所依据的基本知识。
要相捕捉某一个信号,我们需要为该信号注册一个信号处理程序,那么在进程内当发生该信号时,系统会自动调用信号处理程序对该信号作出响应,实现对该信号的捕捉和处理。
sigaction函数允许调用程序指定与某一信号相关的处理程序。函数原型为:
#include<signal.h>
int sigaction(intsig, const struct sigaction* restrict act, struct sigaction * restrict oact);
参数sig表示要设置处理程序的信号值;参数act是用来说明信号处理程序等信息的sigaction结构体指针;参数oact如果为非空(not NULL),则sigaction函数将sig所指定的信号的原处理程序等信息通过oact参数返回给sigaction的调用程序。
在本文中我们将使用该函数为SIGALRM信号注册一个信号处理程序,即上述的参数sig值为SIGALRM。
sigaction结构体包含四个成员,具体如下:
struct sigaction
{
void(*sa_handler)(int); //SIG_DFL、SIG_IGN或者指向函数的指针
sigset_tsa_mask; //处理程序要阻塞的信号
intsa_flags; //标志和选项
void(*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序
};
在sigaction结构中,sa_handler可以保存SIG_DFL、SIG_IGN或者指向函数的指针,SIG_DFL表示使用系统默认的信号处理程序,SIG_IGN表示进程忽略由sigaction函数设置的信号,如果指向一个用户定义的函数指针,那么,当进程内发生了该信号时,则采用该指针所指向的函数作为其处理函数,不过这是有一个前提的,接下来会陈述;信号掩码sa_mask用来指定需要阻塞的信号,在设置sa_mask前,需要初始化sa_mask字段,请参考sigemptyset函数的Unix帮助;如果sa_flags字段的标志位SA_SIGINFO没有被设置,那么,sa_handler将作为信号的处理函数,如果sa_flags字段的标志位SA_SIGINFO被设置,那么,sa_sigaction将作为信号的处理函数;sa_sigaction提供额外信息的信号处理程序指针。
在“3.1.2sigaction结构体”一节中,初步介绍了sa_handler和sa_sigaction字段,其函数原型如下:
voidsa_handler(int signo);
voidsa_sigaction(int signo, siginfo_t * info, void context);
参数signo表示被捕获的信号码。
参数info是siginfo_t结构体变量,其成员如下:
structsiginfo_t
{
int si_signo; //信号码
int si_code; //引发信号的原因
union sigval
{
int sival_int;
void * sival_ptr;
} si_value;
}
参数info中包含的si_signo字段与函数参数signo相同;si_code字段,POSIX定义了如下几个值:SI_USER、SI_QUEUE、SI_TIMER、SI_ASYNCIO、SI_MESGQ;在信号发生时,si_value被设置,可以作为参数传递给信号处理程序。具体参考一下Unix手册。
参数context,目前POSIX标准还没有定义该参数的值。
POSIX:XSI提供了三种用户间隔定时器
1、ITIMER_REAL:真实时间定时器,在定时器到期时产生SIGALRM信号;
2、ITIMER_VIRTUAL:虚拟时间定时器,即进程使用CPU时间,定时器到期时产生SIGVTALRM信号;
3、ITIMER_PROF:根据虚拟时间和进程的系统时间,定时器到期时产生SIGPROF信号。
itimerval结构体包含两个成员,具体如下:
struct itimerval
{
structtimeval it_value; //到下一次到期为止的剩余时间
structtimeval it_interval; //重新装载定时器的值
};
其中,timeval也是一个结构体,定义如下:
struct timeval
{
time_ttv_sec; //秒
time_ttv_usec; //微秒
};
setitimer函数用于启动和终止用户定时器。函数原型如下:
#include<sys/time.h>
intsetitimer(int which, const struct itimerval *restrict value, struct itimerval*restrict ovalue);
参数witch用于指定定时器类型,其值如“3.2.1间隔定时器”所描述的三类之一;value为指定的定时器时间间隔,ovalue如果不为空,则返回which类型的定时器先前所设置的时间间隔值。
一个完整的定时器功能的程序需要以下几个步骤:
1、设置定时器处理程序;
2、设置定时器定时执行时间间隔;
3、启动定时器;
4、停止定时器;
因此,我们将围绕这四个步骤,来设计CSimpleTimer类。
根据“4.1基本思想”中的四个步骤,我们知道,我们需要给定时器类留出至少四个接口,即SetTimerProc(timer事件处理程序)、Timer(n)(n大于0,设置、启动和停止定时器)、Timer(0)(停止定时器)。
实际上,上面的Timer只是一个接口而已,利用不同的参数实现不同的功能。之所以这样设置,是遵循各种语言中的timer定时器的设计。
面向对象设计,不能忘记构造和析构函数,我们要充分利用这两个函数。
我们可以使用构造函数实现设置timer时间处理程序、设置定时器时间间隔、启动定时器的功能;同样,也可以使用析构函数实现推出时停止定时器的功能。
根据“4.2.1方法的设计”一节,我们可以知道,类必须提供能够保存时间处理程序和时间间隔参数的变量。
索性,我们根据“3.1.2sigaction结构体”一节和“3.2.2itimerval结构体”一节,设计两个成员变量:
sigactionmsa_action; //用于设置时间处理函数
itimervalmit_value; //用于设置定时器时间间隔
通过上述两个变量我们完全可以实现对需要数据的保存。
通过“4.2.1方法的设计”和“4.2.2属性的设计”我们可以总结目标结构如下:
属性:
sigaction msa_action; //用于设置时间处理函数
itimervalmit_value; //用于设置定时器时间间隔
方法:
CSimpleTimer();
CSimpleTimer(void(*sa_sigaction)(int, siginfo_t *, void *),const int interval);
SetTimerProc(void(*sa_sigaction)(int, siginfo_t *, void *));
Timer(intinterval);
~CSimpleTimer();
通过对“3预备知识”一章的学习,我们掌握了如何注册信号以及如何发送定时器信号。根据普遍意义上的定时器,本文所实现的定时器采用SIGALRM信号和ITIMER_REAL定时器。即,定时器的定时间隔是实际时间的流逝,而不是虚拟时间,而实际时间所产生的信号恰恰是SIGALRM信号。
所以,在类的实现过程中,我们没有必要将信号类型、信号掩码、action的标志字段、定时器类型等信息提供给用户接口;我们只需要提供上述信息默认值即可。
#include <sys/time.h>
#include <signal.h>
class CSimpleTimer
{
protected:
structsigaction msa_action; //用于设置时间处理函数
itimervalmit_value; //用于设置定时器时间间隔
voidInitialize(); //初始化成员变量,设置默认值
public:
CSimpleTimer();
CSimpleTimer(void(*timerproc)(int, siginfo_t *, void *),const int interval);
intSetTimerProc(void (*timerproc)(int, siginfo_t *, void *));
intTimer(const int interval);
~CSimpleTimer();
};
虽然itimerval数据结构支持时间分辨率达到了微秒级,但是,CSimpleTimer类仅使用秒级的分辨率,即,设置定时器时的参数类型为int型,仅表示秒。可以通过修改Timer方法的参数为double,来使定时器支持更高的分辨率,即整数部分表示秒,小数部分可以表示小于秒的分辨率。
CSimpleTimer的实现代码如下:
#include "SimpleTimer.h"
CSimpleTimer::CSimpleTimer()
{
Initialize();
}
CSimpleTimer::CSimpleTimer(void (*timerproc)(int,siginfo_t *, void *),const int interval)
{
Initialize();
SetTimerProc(timerproc);
Timer(interval);
}
void CSimpleTimer::Initialize()
{
//初始化msa_action成员变量
msa_action.sa_flags=SA_SIGINFO;//使用sa_sigaction信号处理程序
sigemptyset(&msa_action.sa_mask);//出错时返回-1此处未进行错误判断
//由于此时还不知道sa_sigaction将会是什么值,所以初始化为NULL
//sa_sigaction值通过SetTimerProc方法来设置
msa_action.sa_sigaction=NULL;
//此时不知道使用者将使用的定时器时间间隔,所以初始化为0
//mit_value值通过SetInterval方法来设置
mit_value.it_interval.tv_sec=0;
mit_value.it_interval.tv_usec=0;
mit_value.it_value=mit_value.it_interval;
}
int CSimpleTimer::SetTimerProc(void (*timerproc)(int,siginfo_t *, void *))
{
msa_action.sa_sigaction=timerproc;
returnsigaction(SIGALRM,&msa_action,NULL);
}
int CSimpleTimer::Timer(const int interval)
{
mit_value.it_interval.tv_sec=interval;
mit_value.it_interval.tv_usec=0; mit_value.it_value=mit_value.it_interval;
returnsetitimer(ITIMER_REAL,&mit_value,NULL);
}
CSimpleTimer::~CSimpleTimer()
{
Timer(0);
}
由于定时器功能单一,所以,测试程序的编写也相对简单。
测试程序设计如下:
1、定时器事件处理程序(信号处理程序)
作为回调函数的函数,必须是全局函数或者是类的静态成员函数。本测试程序采用全局函数。
2、定时器时间间隔从命令行获取
定时器时间间隔从命令行输入。
3、pause等待信号
pause函数等待一个信号发生,如果信号注册了信号处理程序,pause函数将在信号处理程序执行完毕后返回。
4、Ctrl+C推出程序
#include "SimpleTimer.h"
#include <iostream>
using namespace std;
//事件/信号处理函数,全局函数
void TimerProcess(int sig,siginfo_t * info, void *context)
{
time_tlt=time(NULL);
cout<<"TimerProcessrecieved a signal: "<<sig<<" at"<<lt<<endl;
}
//主程序函数
int main(int argc, char * argv[])
{
//命令行参数判断
if(argc<2)
{
cout<<"usage:TestSimpleTimer n"<<endl;
return0;
}
//创建一个简单定时器类实例
CSimpleTimertimer;
//设置事件/信号处理程序
if(timer.SetTimerProc(TimerProcess)<0)
cout<<"error:SetTimerProc"<<endl;
//启动定时器
if(timer.Timer(atoi(argv[1]))<0)
cout<<"Error:Timer"<<endl;
for(;;)
{
cout<<"For..."<<endl;
pause();
}
return 0;
}
//g++ TestSimpleTimer.cpp SimpleTimer.cpp -oTestSimpleTimer
测试环境为Solaris9、g++ 3.4.2。
编译命令:g++ TestSimpleTimer.cppSimpleTimer.cpp -o TestSimpleTimer
以下各图为测试截图。
命令:TestSimpleTimer 3
命令:TestSimpleTimer 2
本文所涉及的知识比较丰富,归纳起来有如下几个要点:
1、要捕获并处理某个信号,必须设置信号处理程序;
2、信号处理程序使用sigaction函数设置
3、sigaction结构体
4、间隔定时器分为三种:ITIMER_REAL、ITIMER_VIRTUAL、ITIMER_PROF;
5、setitimer函数发送定时器信号
6、itimerval结构体
7、回调函数
本文的定时器是一个简单的定时器,在一个进程中只能有一个这样的定时器,但是,我们可以通过一个定时器模拟出更多的定时器。事实上,操作系统就是使用少量的硬件定时器模拟出了更多的定时器。
通过本文,扩展思路,我们可以处理更多的信号,不外乎以下几个步骤:
1、编写信号处理函数
2、为指定信号注册信号处理程序
3、在某个时机发送信号(可以是人为也可以使系统)
《Unix系统编程》