轉自:
http://blog.chinaunix.net/space.php?uid=317451&do=blog&id=92667簡介
這篇文章主要記錄我在試圖解決如何盡可能精確地在某個特定的時間間隔執行某項具體任務時的思路歷程,并在后期對相關的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并沒有返回剩余的時間的話。
posted on 2011-08-01 11:32
老馬驛站 閱讀(1648)
評論(0) 編輯 收藏 引用 所屬分類:
linux