1. 程式人生 > >關於Linux的應用層定時器

關於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

:給一個指定的時間間隔,按照實際的時間來減少這個計數,當時間間隔為0的時候發出SIGALRM訊號。
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);
}