1. 程式人生 > >linux定時器

linux定時器

簡介
這篇文章主要記錄我在試圖解決如何儘可能精確地在某個特定的時間間隔執行某項具體任務時的思路歷程,並在後期對相關的API進行的歸納和總結,以備參考。
問題引出
很多時候,我們會有類似“每隔多長時間執行某項任務”的需求,乍看這個問題並不難解決,實則並不容易,有很多隱含條件需要考慮,諸如:時間精度是多少?時間是否允許出現偏差,允許的偏差是多少,偏差之後如何處理?系統的負載如何?這個程式允許佔用的系統資源是否有限制?這個程式執行的硬體平臺如何?

為了便於分析,我們鎖定題目為“每隔2妙列印當前的系統時間(距離UNIX紀元的秒數)”。
基於sleep的樸素解法
看到這個題目,我想大家的想法和我一樣,都是首先想到類似這樣的解法:


#include <stdio.h>
int main(int argc, char *argv[]) { while (1) { printf("%d\n", time(NULL)); sleep(2); } return 0; } 如果對時間精度要求不高,以上程式碼確實能工作的很好。因為sleep的時間精度只能到1s#include <unistd.h> unsigned int sleep(unsigned int seconds); 所以對於更高的時間精度(比如說毫秒)來說,sleep
就不能奏效了。如果沿著這個思路走下去,還分別有精確到微妙和納秒的函式usleep和nanosleep可用: #include <unistd.h> int usleep(useconds_t usec); Feature Test Macro Requirements for glibc (see feature_test_macros(7)): usleep(): _BSD_SOURCE || _XOPEN_SOURCE >= 500 #include <time.h> int nanosleep(const struct timespec *req
, struct timespec *rem); Feature Test Macro Requirements for glibc (see feature_test_macros(7)): nanosleep(): _POSIX_C_SOURCE >= 199309L 既然有了能精確到納秒的nanosleep可用,上面的較低精度的函式也就可以休息了。實際上在Linux系統下,sleep和usleep就是通過一個系統呼叫nanosleep實現的。 用帶有超時功能的API變相實現睡眠 如果開發者不知道有usleep和nanosleep,這個時候他可能會聯想到select類的系統呼叫: According to POSIX.1-2001 */ #include <sys/select.h> /* According to earlier standards */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); #include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); #include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask); 從函式原型和相關手冊來看,poll和epoll_wait能提供的時間精度為毫秒,select比他們兩個略勝一籌,為微秒,和前述的usleep相當。但是,果真如此麼?這需要我們深入到Linux的具體實現,在核心裡,這幾個系統呼叫的超時功能都是通過核心中的動態定時器實現的,而動態定時器的時間精度是由當前核心的HZ數決定的。如果核心的HZ是100,那麼動態定時器的時間精度就是1/HZ=1/100=10毫秒。目前,X86系統的HZ最大可以定義為1000,也就是說X86系統的動態定時器的時間精度最高只能到1毫秒。由此來看,select用來指示超時的timeval資料結構,只是看起來很美,實際上精度和poll/epoll_wait相當。 基於定時器的實現 除了基於sleep的實現外,還有基於能用訊號進行非同步提醒的定時器實現: #include <stdio.h> #include <signal.h> int main(int argc, char *argv[]) { sigset_t block; sigemptyset(&block); sigaddset(&block, SIGALRM); sigprocmask(SIG_BLOCK, &block, NULL); while (1) { printf("%d\n", time(NULL)); alarm(2); sigwaitinfo(&block, NULL); } return 0; } 顯然,上面的程式碼並沒有利用訊號進行非同步提醒,而是通過先阻塞訊號的傳遞,然後用sigwaitinfo等待並將訊號取出的方法將非同步化同步。這樣做的目的是為了儘可能減少非必要的訊號呼叫消耗,因為這個程式只需要執行這個簡單的單一任務,所以非同步除了帶來消耗外,並無任何好處。 讀者可能已經發現上面的程式碼無非是把最初的程式碼中的sleep換成了alarm和sigwaitinfo兩個呼叫,除了複雜了程式碼之外,好像並沒有什麼額外的好處。alarm的時間精度只能到1s,並且alarm和sigwaitinfo的確也可以看成是sleep的一種實現,實際上有的sleep確實是透過alarm來實現的,請看sleep的手冊頁: BUGS sleep() may be implemented using SIGALRM; mixing calls to alarm(2) and sleep() is a bad idea. Using longjmp(3) from a signal handler or modifying the handling of SIGALRM while sleeping will cause undefined results. 但是,這只是表象,本質他們是不同的,sleep是撥了一個臨時實時定時器並等待定時器到期,而alarm是用程序唯一的實時定時器來定時喚醒等待訊號到來的程序執行。 如果需要更高的時間精度,可以採用精度為微秒的alarm版本ualarm: #include <unistd.h> useconds_t ualarm(useconds_t usecs, useconds_t interval); Feature Test Macro Requirements for glibc (see feature_test_macros(7)): ualarm(): _BSD_SOURCE || _XOPEN_SOURCE >= 500 或者是直接用setitimer操縱程序的實時定時器: #include <sys/time.h> int getitimer(int which, struct itimerval *value); int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue); 細心的你應該已經注意到了,ualarm和setitimer都額外提供了間隔時間的設定以便於間隔定時器用SIGALRM週期性的喚醒程序,這對於我們的需求有什麼意義呢?請聽我慢慢道來。一般來說,需要定時執行的任務所消耗的時間都很短,至少都會少於間隔時間,否則這個需求就是無法實現的。我們前面的程式實現,都是假設任務消耗時間為0,實際上的任務並不總是像列印當前系統時間這麼簡單,即便它們持續的時間真的短到相對來說可以忽略不計,如果這些小的忽略不計累積起來,也還是可能會造成長時間後的大偏差,所以我們有必要將這段時間計算進來。一種補救的措施是在任務執行的前後執行gettimeofday得到系統的時間,然後做差得到任務消耗時間並在接下來的“sleep”中將其扣除。問題看似解決了,但是我們畢竟沒有將系統進行上下文切換的時間和計算消耗時間的時間考慮進來,這樣的話,還是會存在較大的誤差。另一種計算量相對小些的演算法是:直接通過時間間隔計算下一次超時的絕對時間,然後根據當前的絕對時間算出需要等待的時間並睡眠。但是,這也只是修修補補而已,並沒有從根本上解決問題。間隔定時器的出現從根本上解決了上面所提的問題,它自身就提供週期喚醒的功能,從而避免了每次都計算的負擔。因為ualarm已經被放棄,所以用setitimer再次改寫程式碼: #include <stdio.h> #include <signal.h> #include <sys/time.h> int main(int argc, char *argv[]) { sigset_t block; struct itimerval itv; sigemptyset(&block); sigaddset(&block, SIGALRM); sigprocmask(SIG_BLOCK, &block, NULL); itv.it_interval.tv_sec = 2; itv.it_interval.tv_usec = 0; itv.it_value = itv.it_interval; setitimer(ITIMER_REAL, &itv, NULL); while (1) { printf("%d\n", time(NULL)); sigwaitinfo(&block, NULL); } return 0; } 程序的間隔計時器能夠提供的時間精度為微秒,對於大多數的應用來說,應該已經足夠,如果需要更高的時間精度,或者需要多個定時器,那麼每個程序一個的實時間隔定時器就無能為力了,這個時候我們可以選擇POSIX實時擴充套件中的定時器: #include <signal.h> #include <time.h> int timer_create(clockid_t clockid, struct sigevent *restrict evp, timer_t *restrict timerid); int timer_getoverrun(timer_t timerid); int timer_gettime(timer_t timerid, struct itimerspec *value); int timer_settime(timer_t timerid, int flags, const struct itimerspec *restrict value, struct itimerspec *restrict ovalue); 它實際上就是程序間隔定時器的增強版,除了可以定製時鐘源(nanosleep也存在能定製時鐘源的版本:clock_nanosleep)和時間精度提高到納秒外,它還能通過將evp->sigev_notify設定為如下值來定製定時器到期後的行為: SIGEV_SIGNAL: 傳送由evp->sigev_sino指定的訊號到呼叫程序,evp->sigev_value的值將被作為siginfo_t結構體中si_value的值。 SIGEV_NONE:什麼都不做,只提供通過timer_gettime和timer_getoverrun查詢超時資訊。 SIGEV_THREAD: 以evp->sigev_notification_attributes為執行緒屬性建立一個執行緒,在新建的執行緒內部以evp->sigev_value為引數呼叫evp->sigev_notification_function。 SIGEV_THREAD_ID:和SIGEV_SIGNAL類似,不過它只將訊號傳送到執行緒號為evp->sigev_notify_thread_id的執行緒,注意:這裡的執行緒號不一定是POSIX執行緒號,而是執行緒呼叫gettid返回的實際執行緒號,並且這個執行緒必須實際存在且屬於當前的呼叫程序。 更新後的程式如下(需要連線實時擴充套件庫: -lrt): #include <stdio.h> #include <signal.h> #include <time.h> #include <errno.h> #include <sched.h> int main(int argc, char *argv[]) { timer_t timer; struct itimerspec timeout; sigset_t block; struct sched_param param; sigemptyset(&block); sigaddset(&block, SIGALRM); sigprocmask(SIG_BLOCK, &block, NULL); timer_create(CLOCK_MONOTONIC, NULL, &timer); timeout.it_interval.tv_sec = 2; timeout.it_interval.tv_nsec = 0; timeout.it_value = timeout.it_interval; timer_settime(timer, 0, &timeout, NULL); while (1) { fprintf(stderr, "%d\n", time(NULL)); sigwaitinfo(&block, NULL); } return 0; } 至於時鐘源為什麼是CLOCK_MONOTONIC而不是CLOCK_REALTIME,主要是考慮到系統的實時時鐘可能會在程式執行過程中更改,所以存在一定的不確定性,而CLOCK_MONOTONIC則不會,較為穩定。 至此為止,我們已經找到了目前Linux提供的精度最高的定時器API,它應該能滿足大多數情況的要求了。 其它問題 傳統訊號的不可靠性 傳統UNIX訊號是不可靠的,也就是說如果當前的訊號沒有被處理,那麼後續的同類訊號將被丟失,而不是被排隊,而實時訊號則沒有這個問題,它是被排隊的。聯絡到當前應用,如果訊號丟失,則是因為任務消耗了過多的處理器時間,而這個不確定性是那個任務帶來的,需要改進的應該是那個任務。 系統負載過高 如果系統的負載過高,使得我們的程式因為不能得到及時的排程導致時間精度降低,我們不妨通過nice提高當前程式的優先順序,必要時可以通過sched_setscheduler將當前程序切換成優先順序最高的實時程序已確保得到及時排程。 硬體相關的問題 硬體配置也極大的影響著定時器的精度,有的比較老的遺留系統可能沒有比較精確的硬體定時器,那樣的話我們就無法期待它能提供多高的時鐘精度了。相反,如果系統的配置比較高,比如說對稱多處理系統,那麼即使有的處理器負載比較高,我們也能通過將一個處理器單獨分配出來處理定時器來提高定時器的精度。 更高的時間精度 雖然,Linux的API暗示它能夠提供納秒級的時間精度,但是,由於種種不確定因素,它實際上並不能提供納秒級的精度,比較脆弱。如果你需要更高強度的實時性,請考慮採用軟實時系統、硬實時系統、專有系統,甚至是專業硬體。 注意: 為了簡便,以上所有程式碼都沒有出錯處理,請讀者在現實的應用中自行加入出錯處理,以提高程式的健壯性。尤其注意sleep類的返回值,它們可能沒到期就返回,這個時候你應該手動計算需要再睡眠多長才能滿足原始的睡眠時間要求,如果該API並沒有返回剩餘的時間的話。