问题背景:
Linux下用C++实现一个定时器。
一、使用 setitimer 实现定时器功能:
GETITIMER(2) Linux Programmer’s Manual GETITIMER(2)
NAME
getitimer, setitimer - get or set value of an interval timer
SYNOPSIS
#include <sys/time.h>
int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
DESCRIPTION
The system provides each process with three interval timers, each decrementing in a distinct time
domain. When any timer expires, a signal is sent to the process, and the timer (potentially)
restarts.
ITIMER_REAL decrements in real time, and delivers SIGALRM upon expiration.
ITIMER_VIRTUAL decrements only when the process is executing, and delivers SIGVTALRM upon expira-
tion.
ITIMER_PROF decrements both when the process executes and when the system is executing on
behalf of the process. Coupled with ITIMER_VIRTUAL, this timer is usually used to
profile the time spent by the application in user and kernel space. SIGPROF is
delivered upon expiration.
Timer values are defined by the following structures:
struct itimerval {
struct timeval it_interval; /* next value */
struct timeval it_value; /* current value */
};
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
The function getitimer() fills the structure pointed to by curr_value with the current setting for
the timer specified by which (one of ITIMER_REAL, ITIMER_VIRTUAL, or ITIMER_PROF). The element
it_value is set to the amount of time remaining on the timer, or zero if the timer is disabled.
Similarly, it_interval is set to the reset value.
The function setitimer() sets the specified timer to the value in new_value. If old_value is non-
NULL, the old value of the timer is stored there.
Timers decrement from it_value to zero, generate a signal, and reset to it_interval. A timer
which is set to zero (it_value is zero or the timer expires and it_interval is zero) stops.
Both tv_sec and tv_usec are significant in determining the duration of a timer.
Timers will never expire before the requested time, but may expire some (short) time afterwards,
which depends on the system timer resolution and on the system load; see time(7). (But see BUGS
below.) Upon expiration, a signal will be generated and the timer reset. If the timer expires
while the process is active (always true for ITIMER_VIRTUAL) the signal will be delivered immedi-
ately when generated. Otherwise the delivery will be offset by a small time dependent on the sys-
tem loading.
setitimer 的工作机制为:对it_value倒计时,当it_value为零时触发信号。 重置为 it_interval,再继续对it_value倒计时,一直循环下去。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
struct itimerval {
struct timeval it_interval; /* next value */
struct timeval it_value; /* current value */
};
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
二、使用sigaction接收信号,并触发行为:
SIGACTION(2) Linux Programmer’s Manual SIGACTION(2)
NAME
sigaction - examine and change a signal action
SYNOPSIS
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
sigaction(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
DESCRIPTION
The sigaction() system call is used to change the action taken by a process on receipt of a spe-
cific signal. (See signal(7) for an overview of signals.)
signum specifies the signal and can be any valid signal except SIGKILL and SIGSTOP.
If act is non-null, the new action for signal signum is installed from act. If oldact is non-
null, the previous action is saved in oldact.
The sigaction structure is defined as something like:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
On some architectures a union is involved: do not assign to both sa_handler and sa_sigaction.
The sa_restorer element is obsolete and should not be used. POSIX does not specify a sa_restorer
element.
sa_handler specifies the action to be associated with signum and may be SIG_DFL for the default
action, SIG_IGN to ignore this signal, or a pointer to a signal handling function. This function
receives the signal number as its only argument.
If SA_SIGINFO is specified in sa_flags, then sa_sigaction (instead of sa_handler) specifies the
signal-handling function for signum. This function receives the signal number as its first argu-
ment, a pointer to a siginfo_t as its second argument and a pointer to a ucontext_t (cast to
void *) as its third argument.
sa_mask specifies a mask of signals which should be blocked (i.e., added to the signal mask of the
thread in which the signal handler is invoked) during execution of the signal handler. In addi-
tion, the signal which triggered the handler will be blocked, unless the SA_NODEFER flag is used.
sa_flags specifies a set of flags which modify the behavior of the signal. It is formed by the
bitwise OR of zero or more of the following:
sigaction 工作机制为:
signum 参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数保存上一次的信号处理方式(不为NULL的话)
sa_mask指定在执行信号处理程序期间应阻塞的信号掩码
sa_flags指定一组修改信号行为的标志
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
实现定时器 —— 每隔1s打印 count :
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
using namespace std;
void notify(int signum)
{
static int count = 0;
cout << count++ << endl;
}
typedef void(*FuncType)(int);
void init_sigaction()
{
struct sigaction act;
act.sa_handler = notify;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGPROF, &act, NULL);
}
void init_time()
{
struct itimerval val;
val.it_value.tv_sec = 1;
val.it_value.tv_usec = 0;
val.it_interval.tv_sec = 1;
val.it_interval.tv_usec = 0;
setitimer(ITIMER_PROF, &val, NULL);
}
int main()
{
init_sigaction();
init_time();
while(1);
return 0;
}
输出结果:
image.png
由输出结果可以看出,现在可以基本满足定时器的要求。但是当执行此程序时,发现此进程的CPU占用率飙高,几乎为100% (〃>目<)
CPU占用率飙到100%的原因,实际上是因为使用了 while(1)。
进程陷入死循环时,会导致此进程的CPU占用率很高。即使Linux系统是时间片算法调度进程,当切换至其他进程时,其他进程不需要消耗这么多时间,继续切换至下一个进程,直到再次切换到死循环的进程中。而此进程因为死循环,再次占满整个时间片。所以,实际上死循环的进程可以理解为耗光了所有空余的时间。
解决方法:
int main()
{
init_sigaction();
init_time();
while(1){
sleep(1);
}
return 0;
}
使用sleep(1) 将程序挂起1s中,即可将进程的CPU占用率大大降低到1%以内。这是因为阻塞1s,对于运算量不大的死循环,相当于让出了整个的CPU运算资源。假如循环一次需要1us,那么1s-1us的CPU占用率都被让了出来。
但是按照上面这种写法,再去运行程序,就会发现,虽然CPU占用率降下来了,但是定时器却用不了了。/(ㄒoㄒ)/~~
为什么使用了 sleep(1) ,会导致定时器用不了了呢?
通过查看setitimer的API文档,会发现:
ITIMER_REAL decrements in real time, and delivers SIGALRM upon expiration.
ITIMER_VIRTUAL decrements only when the process is executing, and delivers SIGVTALRM upon expira-
tion.
ITIMER_PROF decrements both when the process executes and when the system is executing on
behalf of the process. Coupled with ITIMER_VIRTUAL, this timer is usually used to
profile the time spent by the application in user and kernel space. SIGPROF is
delivered upon expiration.
当setitimer的计时方式使用 ITIMER_PROF 时,只有在进行为running状态时计时。使用了sleep,此进程大部分时间都是挂起状态,影响了计时器的正常工作。
所以这里将计时方式换成 "ITIMER_REAL",不管进程的状态如果,计时器都根据真实时间计时。
注意到,ITIMER_REAL产生的信号为 "SIGALRM"。所以,相应的信号处理函数也需要修改。
修改后的代码:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
using namespace std;
void notify(int signum)
{
static int count = 0;
cout << count++ << endl;
}
typedef void(*FuncType)(int);
void init_sigaction()
{
struct sigaction act;
act.sa_handler = notify;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
}
void init_time()
{
struct itimerval val;
val.it_value.tv_sec = 1;
val.it_value.tv_usec = 0;
val.it_interval.tv_sec = 1;
val.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &val, NULL);
}
int main()
{
init_sigaction();
init_time();
while(1)
{
sleep(1);
}
return 0;
}
再次运行代码,会发现计时器又能正常运行 (●ˇ∀ˇ●)
但是,继续查阅资料,会发现sleep()函数,是通过调用alarm()来设定报警时间,调用了sigsuspend()将进程挂起在信号SIGALARM上。这就导致了一旦收到SIGALARM信号,sleep()就会被中断。为了避免 sleep() 和 此定时器中产生的SIGALARM信号产生冲突,可以用nanosleep方式实现进程的挂起。
nanosleep()则是Linux中的系统调用,它是使用定时器来实现的,该调用使调用进程睡眠,并往定时器队列上加入一个timer_list型定时器,time_list结构里包括唤醒时间以及唤醒后执行的函数,通过nanosleep()加入的定时器的执行函数仅仅完成唤醒当前进程的功能。系统通过一定的机制定时检查这些队列(比如通过系统调用陷入核心后,从核心返回用户态前,要检查当前进程的时间片是否已经耗尽,如果是则调用schedule()函数重新调度,该函数中就会检查定时器队列,另外慢中断返回前也会做此检查),如果定时时间已超过,则执行定时器指定的函数唤醒调用进程。当然,由于系统时间片可能丢失,所以nanosleep()精度也不是很高。
将定时器封装成一个类:
#include <iostream>
#include <signal.h>
#include <string.h>
#include <sys/time.h>
using namespace std;
static void notify( int signum ){
static int counter;
if( signum == SIGALRM ){
++ counter;
cout << counter << endl;
}
}
typedef void(*ProcessHandler)(int);
class Timer{
protected:
struct sigaction _act;
struct sigaction _oldact;
struct itimerval _tim_ticks;
void wait( long timeout_ms ){
struct timespec spec;
spec.tv_sec = timeout_ms / 1000;
spec.tv_nsec = (timeout_ms % 1000) * 1000000;
nanosleep(&spec,NULL);
}
public:
Timer(int sec,int usec){
_tim_ticks.it_value.tv_sec = 1;
_tim_ticks.it_value.tv_usec = 0;
_tim_ticks.it_interval.tv_sec = sec;
_tim_ticks.it_interval.tv_usec = usec;
}
void setHandler(ProcessHandler handler){
sigemptyset( &_act.sa_mask );
_act.sa_flags = 0;
_act.sa_handler = handler;
}
bool reset(){
int res = sigaction( SIGALRM, &_act, &_oldact );
if ( res ) {
perror( "Fail to install handle: " );
return false;
}
res = setitimer( ITIMER_REAL, &_tim_ticks, 0 );
if(res){
perror( "Fail to set timer: " );
sigaction( SIGALRM, &_oldact, 0 );
return false;
}
for ( ; ; ){
wait(1000000);
}
}
};
int main( void ){
Timer timer(2,0);
timer.setHandler(notify);
timer.reset();
return 0;
}
~ o( ̄▽ ̄)o 其实上面这个封装好的定时器的类,我是参考 http://blog.sina.com.cn/s/blog_49693bd0010080am.html 这个博客的。 但是,这其中的原理还是要好好理解才能明白为啥要这样干呀~
网友评论