關於Linux的應用層定時器
使用定時器的目的無非是為了週期性的執行某一任務,或者是到了一個指定時間去執行某一個任務。要達到這一目的,一般有兩個常見的比較有效的方法。一個是用 Linux 內部的三個定時器;另一個是用 sleep 或 usleep 函式讓程序睡眠一段時間;其實,還有一個方法,那就是用 gettimeofday、difftime 等自己來計算時間間隔,然後時間到了就執行某一任務,但是這種方法效率低,所以不常用。
1、alarm
如果不要求很精確的話,用 alarm() 和 signal() 就夠了
unsigned int alarm(unsigned int seconds)
專門為SIGALRM訊號而設,在指定的時間seconds秒後,將向程序本身傳送SIGALRM訊號,又稱為鬧鐘時間。程序呼叫alarm後,任何以前的alarm()呼叫都將無效。如果引數seconds為零,那麼程序內將不再包含任何鬧鐘時間。如果呼叫alarm()前,程序中已經設定了鬧鐘時間,則返回上一個鬧鐘時間的剩餘時間,否則返回0。
示例:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigalrm_fn(int sig)
{
printf("alarm!\n");
alarm(2);
return;
}
int main(void)
{
signal(SIGALRM, sigalrm_fn);
alarm(2);
while(1) pause();
}
2、setitimer
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));
int getitimer(int which, struct itimerval *value);
strcut timeval
{
long tv_sec; /*秒*/
long tv_usec; /*微秒*/
};
struct itimerval
{
struct timeval it_interval; /*時間間隔*/
struct timeval it_value; /*當前時間計數*/
};
setitimer() 比 alarm() 功能強大,支援3種類型的定時器:
① ITIMER_REAL
② ITIMER_VIRTUAL:給定一個時間間隔,當程序執行的時候才減少計數,時間間隔為0的時候發出SIGVTALRM訊號。
③ ITIMER_PROF:給定一個時間間隔,當程序執行或者是系統為程序排程的時候,減少計數,時間到了,發出SIGPROF訊號。
setitimer() 第一個引數 which 指定定時器型別(上面三種之一);第二個引數是結構 itimerval 的一個例項;第三個引數可不做處理。
下面是關於setitimer呼叫的一個簡單示範,在該例子中,每隔一秒發出一個SIGALRM,每隔0.5秒發出一個SIGVTALRM訊號::
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
int sec;
void sigroutine(int signo){
switch (signo){
case SIGALRM:
printf("Catch a signal -- SIGALRM \n");
signal(SIGALRM, sigroutine);
break;
case SIGVTALRM:
printf("Catch a signal -- SIGVTALRM \n");
signal(SIGVTALRM, sigroutine);
break;
}
return;
}
int main()
{
struct itimerval value, ovalue, value2;
sec = 5;
printf("process id is %d ", getpid());
signal(SIGALRM, sigroutine);
signal(SIGVTALRM, sigroutine);
value.it_value.tv_sec = 1;
value.it_value.tv_usec = 0;
value.it_interval.tv_sec = 1;
value.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &value, &ovalue);
value2.it_value.tv_sec = 0;
value2.it_value.tv_usec = 500000;
value2.it_interval.tv_sec = 0;
value2.it_interval.tv_usec = 500000;
setitimer(ITIMER_VIRTUAL, &value2, &ovalue);
for(;;);
}
該例子的執行結果如下:
localhost:~$ ./timer_test
process id is 579
Catch a signal – SIGVTALRM
Catch a signal – SIGALRM
Catch a signal – SIGVTALRM
Catch a signal – SIGVTALRM
Catch a signal – SIGALRM
Catch a signal –GVTALRM
注意:Linux訊號機制基本上是從Unix系統中繼承過來的。早期Unix系統中的訊號機制比較簡單和原始,後來在實踐中暴露出一些問題,因此,把那些建立在早期機制上的訊號叫做”不可靠訊號”,訊號值小於SIGRTMIN(Red hat 7.2中,SIGRTMIN=32,SIGRTMAX=63)的訊號都是不可靠訊號。這就是”不可靠訊號”的來源。它的主要問題是:程序每次處理訊號後,就將對訊號的響應設定為預設動作。在某些情況下,將導致對訊號的錯誤處理;因此,使用者如果不希望這樣的操作,那麼就要在訊號處理函式結尾再一次呼叫 signal(),重新安裝該訊號。
3、用 sleep 以及 usleep 實現定時執行任務
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
static char msg[] = "I received a msg.\n";
int len;
void show_msg(int signo)
{
write(STDERR_FILENO, msg, len);
}
int main()
{
struct sigaction act;
union sigval tsval;
act.sa_handler = show_msg;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(50, &act, NULL);
len = strlen(msg);
while ( 1 )
{
sleep(2); /*睡眠2秒*/
/*向主程序傳送訊號,實際上是自己給自己發訊號*/
sigqueue(getpid(), 50, tsval);
}
return 0;
}
看到了吧,這個要比上面的簡單多了,而且你用秒錶測一下,時間很準,指定2秒到了就給你輸出一個字串。所以,如果你只做一般的定時,到了時間去執行一個任務,這種方法是最簡單的。
4、通過自己計算時間差的方法來定時
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
static char msg[] = "I received a msg.\n";
int len;
static time_t lasttime;
void show_msg(int signo)
{
write(STDERR_FILENO, msg, len);
}
int main()
{
struct sigaction act;
union sigval tsval;
act.sa_handler = show_msg;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(50, &act, NULL);
len = strlen(msg);
time(&lasttime);
while ( 1 )
{
time_t nowtime;
/*獲取當前時間*/
time(&nowtime);
/*和上一次的時間做比較,如果大於等於2秒,則立刻傳送訊號*/
if (nowtime - lasttime >= 2)
{
/*向主程序傳送訊號,實際上是自己給自己發訊號*/
sigqueue(getpid(), 50, tsval);
lasttime = nowtime;
}
}
return 0;
}
這個和上面不同之處在於,是自己手工計算時間差的,如果你想更精確的計算時間差,你可以把 time 函式換成 gettimeofday,這個可以精確到微妙。
5、使用 select 來提供精確定時和休眠
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
n 指監視的檔案描述符範圍,通常設為所要select的fd+1;readfds,writefds 和 exceptfds分別是讀,寫和異常檔案描述符集;timeout 為超時時間。
可能用到的關於檔案描述符集操作的巨集有:
FD_CLR(int fd, fd_set *set); // 清除fd
FD_ISSET(int fd, fd_set *set); // 測試fd是否設定
FD_SET(int fd, fd_set *set); //設定fd
FD_ZERO(fd_set *set); //清空描述符集
我們此時用不到這些巨集,因為我們並不關心檔案描述符的狀態,我們關心的是select超時。所以我們需要把 readfds,writefds 和 exceptfds 都設為 NULL,只指定 timeout 時間就行了。至於 n 我們可以不關心,所以你可以把它設為任何非負值。實現程式碼如下:
int msSleep(long ms)
{
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = ms;
return select(0, NULL, NULL, NULL, &tv);
}
怎麼樣,是不是很簡單? setitimer 和 select 都能實現程序的精確休眠,這裡給出了一個簡單的基於 select 的實現。我不推薦使用 setitimer,因為 Linux 系統提供的 timer 有限(每個程序至多能設3個不同型別的 timer),而且 setitimer 實現起來沒有 select 簡單。
6、高精度硬體中斷定時器 hrtimer
需要在 kernel 中開啟 “high resolution Timer support”,驅動程式中 hrtimer 的初始化如下:
hrtimer_init(&m_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_PINNED);
m_timer.function = vibrator_timer_func;
hrtimer_start(&m_timer, ktime_set(0, 62500), HRTIMER_MODE_REL_PINNED);
定時函式 vibrator_timer_func 如下:
static enum hrtimer_restart vibrator_timer_func(struct hrtimer *timer)
{
gpio_set_value(gpio_test, 1);
gpio_set_value(gpio_test, 0);
hrtimer_forward_now(&m_timer,ktime_set(0, 62500));
return HRTIMER_RESTART;
}
其中 gpio_test 為輸出引腳,為了方便輸出檢視。但是用示波器檢視引腳波形時,發現雖然設定的週期為62.5us,但是輸出總是為72us左右,而且偶爾會有兩個波形靠的很近(也就是說週期突然變為10us以下)。我將週期設到40us的話,就會出現72us和10us經常交替出現,無法實現精確的40us的波形,如果設定到100us時,則波形就是100us了,而且貌似沒有看到有10us以下的週期出現。
7、高精度定時器 posix_timer
最強大的定時器介面來自POSIX時鐘系列,其建立、初始化以及刪除一個定時器的行動被分為三個不同的函式:timer_create()
(建立定時器)、timer_settime()
(初始化定時器)以及 timer_delete()
(銷燬它)。
建立一個定時器:
int timer_create(clockid_t clock_id, struct sigevent *evp, timer_t *timerid)
程序可以通過呼叫 timer_create()
建立特定的定時器,定時器是每個程序自己的,不是在 fork 時繼承的。clock_id
說明定時器是基於哪個時鐘的,*timerid 裝載的是被建立的定時器的 ID。該函式建立了定時器,並將他的 ID 放入timerid指向的位置中。引數evp指定了定時器到期要產生的非同步通知。如果evp為 NULL,那麼定時器到期會產生預設的訊號,對 CLOCK_REALTIMER
來說,預設訊號就是SIGALRM。如果要產生除預設訊號之外的其它訊號,程式必須將 evp->sigev_signo
設定為期望的訊號碼。struct sigevent 結構中的成員 evp->sigev_notify
說明了定時器到期時應該採取的行動。通常,這個成員的值為SIGEV_SIGNAL
,這個值說明在定時器到期時,會產生一個訊號。程式可以將成員 evp->sigev_notify
設為SIGEV_NONE
來防止定時器到期時產生訊號。
如果幾個定時器產生了同一個訊號,處理程式可以用 evp->sigev_value來區分是哪個定時器產生了訊號。要實現這種功能,程式必須在為訊號安裝處理程式時,使用struct sigaction的成員sa_flags中的標誌符SA_SIGINFO。
clock_id取值為以下:
CLOCK_REALTIME :Systemwide realtime clock.
CLOCK_MONOTONIC:Represents monotonic time. Cannot be set.
CLOCK_PROCESS_CPUTIME_ID :High resolution per-process timer.
CLOCK_THREAD_CPUTIME_ID :Thread-specific timer.
CLOCK_REALTIME_HR :High resolution version of CLOCK_REALTIME.
CLOCK_MONOTONIC_HR :High resolution version of CLOCK_MONOTONIC.
struct sigevent
{
int sigev_notify; //notification type
int sigev_signo; //signal number
union sigval sigev_value; //signal value
void (*sigev_notify_function)(union sigval);
pthread_attr_t *sigev_notify_attributes;
}
union sigval
{
int sival_int; //integer value
void *sival_ptr; //pointer value
}
通過將evp->sigev_notify設定為如下值來定製定時器到期後的行為:
SIGEV_NONE:什麼都不做,只提供通過timer_gettime和timer_getoverrun查詢超時資訊。
SIGEV_SIGNAL: 當定時器到期,核心會將sigev_signo所指定的訊號傳送給程序。在訊號處理程式中,si_value會被設定會sigev_value。
SIGEV_THREAD: 當定時器到期,核心會(在此程序內)以sigev_notification_attributes為執行緒屬性建立一個執行緒,並且讓它執行sigev_notify_function,傳入sigev_value作為為一個引數。
啟動一個定時器:
timer_create()所建立的定時器並未啟動。要將它關聯到一個到期時間以及啟動時鐘週期,可以使用timer_settime()。
int timer_settime(timer_t timerid, int flags, const struct itimerspec *value, struct itimerspect *ovalue);
struct itimespec{
struct timespec it_interval;
struct timespec it_value;
};
如同settimer(),it_value用於指定當前的定時器到期時間。當定時器到期,it_value的值會被更新成it_interval 的值。如果it_interval的值為0,則定時器不是一個時間間隔定時器,一旦it_value到期就會回到未啟動狀態。timespec的結構提供了納秒級解析度:
struct timespec{
time_t tv_sec;
long tv_nsec;
};
如果flags的值為TIMER_ABSTIME,則value所指定的時間值會被解讀成絕對值(此值的預設的解讀方式為相對於當前的時間)。這個經修改的行為可避免取得當前時間、計算“該時間”與“所期望的未來時間”的相對差額以及啟動定時器期間造成競爭條件。
如果ovalue的值不是NULL,則之前的定時器到期時間會被存入其所提供的itimerspec。如果定時器之前處在未啟動狀態,則此結構的成員全都會被設定成0。
獲得一個活動定時器的剩餘時間:
int timer_gettime(timer_t timerid,struct itimerspec *value);
取得一個定時器的超限執行次數:
有可能一個定時器到期了,而同一定時器上一次到期時產生的訊號還處於掛起狀態。在這種情況下,其中的一個訊號可能會丟失。這就是定時器超限。程式可以通過呼叫timer_getoverrun來確定一個特定的定時器出現這種超限的次數。定時器超限只能發生在同一個定時器產生的訊號上。由多個定時器,甚至是那些使用相同的時鐘和訊號的定時器,所產生的訊號都會排隊而不會丟失。
int timer_getoverrun(timer_t timerid);
執行成功時,timer_getoverrun()會返回定時器初次到期與通知程序(例如通過訊號)定時器已到期之間額外發生的定時器到期次數。舉例來說,在我們之前的例子中,一個1ms的定時器運行了10ms,則此呼叫會返回9。如果超限執行的次數等於或大於DELAYTIMER_MAX,則此呼叫會返回DELAYTIMER_MAX。
執行失敗時,此函式會返回-1並將errno設定會EINVAL,這個唯一的錯誤情況代表timerid指定了無效的定時器。
刪除一個定時器:
int timer_delete (timer_t timerid);
一次成功的timer_delete()呼叫會銷燬關聯到timerid的定時器並且返回0。執行失敗時,此呼叫會返回-1並將errno設定會 EINVAL,這個唯一的錯誤情況代表timerid不是一個有效的定時器。
例1:
void handle()
{
time_t t;
char p[32];
time(&t);
strftime(p, sizeof(p), "%T", localtime(&t));
printf("time is %s\n", p);
}
int main()
{
struct sigevent evp;
struct itimerspec ts;
timer_t timer;
int ret;
evp.sigev_value.sival_ptr = &timer;
evp.sigev_notify = SIGEV_SIGNAL;
evp.sigev_signo = SIGUSR1;
signal(SIGUSR1, handle);
ret = timer_create(CLOCK_REALTIME, &evp, &timer);
if( ret )
perror("timer_create");
ts.it_interval.tv_sec = 1;
ts.it_interval.tv_nsec = 0;
ts.it_value.tv_sec = 3;
ts.it_value.tv_nsec = 0;
ret = timer_settime(timer, 0, &ts, NULL);
if( ret )
perror("timer_settime");
while(1);
}
例2:
void handle(union sigval v)
{
time_t t;
char p[32];
time(&t);
strftime(p, sizeof(p), "%T", localtime(&t));
printf("%s thread %lu, val = %d, signal captured.\n", p, pthread_self(), v.sival_int);
return;
}
int main()
{
struct sigevent evp;
struct itimerspec ts;
timer_t timer;
int ret;
memset (&evp, 0, sizeof (evp));
evp.sigev_value.sival_ptr = &timer;
evp.sigev_notify = SIGEV_THREAD;
evp.sigev_notify_function = handle;
evp.sigev_value.sival_int = 3; //作為handle()的引數
ret = timer_create(CLOCK_REALTIME, &evp, &timer);
if( ret)
perror("timer_create");
ts.it_interval.tv_sec = 1;
ts.it_interval.tv_nsec = 0;
ts.it_value.tv_sec = 3;
ts.it_value.tv_nsec = 0;
ret = timer_settime(timer, TIMER_ABSTIME, &ts, NULL);
if( ret )
perror("timer_settime");
while(1);
}