間隔定時器
#includeint setitimer(int which,const struct itimerval* new_value,struct itimerval* old_value);
setitimer() 創(chuàng)建一個間隔式定時器,這種定時器會在未來某個時間點到期,并于此后(可選擇地)每間隔一段時間到期一次
which 可以指定以下值:
ITIMER_REAL :創(chuàng)建以真實時間倒計時的定時器,到期會產(chǎn)生 SIGALARM 信號并發(fā)送給進程
ITIMER_VIRTUAL:創(chuàng)建以進程虛擬時間(用戶模式下的 CPU 時間) 倒計時的定時器,到期時會產(chǎn)生信號 SIGVTALRM
ITIMER_PROF:創(chuàng)建一個 profiling 定時器,以進程時間(用戶態(tài)與內(nèi)核態(tài) CPU 時間的總和)倒計時,到期時,則會產(chǎn)生 SIGPROF 信號
針對所有這些信號的默認處置均會終止進程,除非真地期望如此,否則就需要針對這些定時器信號創(chuàng)建處理器函數(shù)。
struct itimerval{ struct timeval it_interval; /* Interval for periodic timer */ struct timeval it_value; /* Current value(time until next expiration) */ }; struct timeval{ time_t tv_sec; /* Seconds */ suseconds_t tv_usec; /* Microseconds */ };
new_value 下屬的 it_value 指定了距離定時器到期的延遲時間,it_interval 則說明該定時器是否是周期性定時器,如果 it_interval 的兩個字段都是 0,那么該定時器屬于 it_value 所指定的時間間隔后到期的一次性定時器,只要 it_interval 中的任一字段非0,那么在每次定時器到期之后,都會將定時器重置為在指定間隔后再次到期
進程只能擁有上述3種定時器的一種,當?shù)诙握{(diào)用 settimer() 時,修改已有定時器的屬性要符合參數(shù) which 中的類型,如果調(diào)用 setitimer() 時將 new_value.it_value 的兩個字段均設置為 0,那么會屏蔽任何已有的定時器
若 old_value 不為 NULL,則以其所指向的 itimerval 結(jié)構來返回定時器的前一設置:
如果 old_value.it_value 的兩個字段值均為 0,那么該定時器之前被設置處于屏蔽狀態(tài)
如果 old_value.it_interval 的兩個字段值均為 0,那么該定時器之前被設置為歷經(jīng) old_value.it_value 指定時間到期的一次性定時器
對需要在新定時器到期后將其還原的情況而言,獲取定時器的前一設置就很重要,如果不關心定時器的前一設置,可以將 old_value 設置為 NULL
定時器會從初始值 it_value 倒計時一直到 0 為止,遞減為 0 時,會將相應信號發(fā)送給進程,隨后,如果時間間隔值 it_interval 非0,那么會再次將 it_value 加載到定時器,重新開始向 0 倒計時
可以在任何時刻調(diào)用 getitimer(),以了解定時器的當前狀態(tài),距離下次到期的剩余時間:
#includeint getitimer(int which,struct itimerval* curr_value);
getitimer() 返回由 which 指定定時器的當前狀態(tài),并置于 curr_value 指向的緩沖區(qū)中
使用 setitimer() 和 alarm() 創(chuàng)建的定時器可以跨越 exec() 調(diào)用而得以保存,但由 fork() 創(chuàng)建的子進程并不繼承該定時器。
更為簡單的定時器接口:alarm()
#includeunsigned int alarm(unsigned int seconds);
seconds 表示定時器到期的秒數(shù),到期時向調(diào)用進程發(fā)送 SIGALRM 信號
調(diào)用 alarm() 會覆蓋對定時器的前一個設置,調(diào)用 alarm(0) 可以屏蔽現(xiàn)有定時器
返回值是定時器前一設置距離到期的剩余描述,如果之前并無設置,則返回 0
setitimer() 和 alarm() 之間的交互
Linux 中 alarm() 和 setitimer() 針對同一進程共享一個實時定時器,無論調(diào)用兩者之中的哪個完成了對定時器的前一設置,同樣可以調(diào)用二者中的任一函數(shù)來改變這一設置。
程序設置實時定時器時,最好選用二者之一。
定時器的調(diào)度和精度
內(nèi)核配置項 CONFIG_HIGH_RES_TIMERS 可以支持高分辨率定時器,使得定時器的精度不受軟件時鐘周期的影響,可以達到底層硬件所支持的精度,在現(xiàn)代硬件平臺上,精度達到微秒級別是司空見慣的。
為阻塞操作設置超時
實時定時器的用途之一就是為某個阻塞系統(tǒng)調(diào)用設置其處于阻塞狀態(tài)的時間上限。
例如,處理 read() 操作:
調(diào)用 sigaction() 創(chuàng)建 SIGALRM 信號的處置函數(shù),排除 SA_RESTART 標志以確保系統(tǒng)調(diào)用不會重新啟動
調(diào)用 alarm() 或者 setitimer() 創(chuàng)建定時器,設置超時時間
執(zhí)行阻塞的系統(tǒng)調(diào)用
系統(tǒng)調(diào)用返回,再次調(diào)用 alarm() 或 setitimer() 屏蔽定時器
檢查系統(tǒng)調(diào)用失敗是否設置 errno 為 EINTR ,即系統(tǒng)調(diào)用遭到中斷
暫停運行一段固定時間
低分辨率休眠:sleep()
#includeunsigned int sleep(unsigned int seconds);
sleep() 可以暫停調(diào)用進程執(zhí)行 seconds 秒,或者在捕獲信號后恢復進程的執(zhí)行
如果休眠正常結(jié)束,返回0,如果因信號中斷休眠,返回剩余的秒數(shù)
考慮到一致性,應該避免 sleep() 和 alarm() 以及 setitimer() 之間的混用,Linux 將 sleep() 實現(xiàn)為對 nanosleep() 的調(diào)用,而有些老系統(tǒng)使用 alarm() 和 SIGALRM 信號處理函數(shù)實現(xiàn) sleep()
高分辨率休眠 nanosleep()
#includeint nanosleep(const struct timespec *req, struct timespec *rem);
nanosleep() 與 sleep() 相似,但是分辨率更高
struct timespec:
struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ };
規(guī)范規(guī)定不得使用信號實現(xiàn)該函數(shù),這意味著 nanosleep() 與 alarm() 和 setitimer() 混用,也不會危及程序的可移植性
盡管 nanosleep() 沒有使用信號,但還是可以通過信號處理器函數(shù)將其中斷,此時將返回 -1,并設置錯誤 EINTR,如果 remain 不為 NULL,則該指針所指向的緩沖區(qū)將返回剩余的休眠時間,可以利用這個返回值重啟該系統(tǒng)調(diào)用以完成休眠,但是由于返回的 remain 時間未必是軟件時鐘間隔的整數(shù)倍,故而每次重啟都會遭受取整,其結(jié)果是,每次重啟后的休眠時間都要長于前一調(diào)用返回的 remain 值,在信號接收頻率很高的情況下,進程的休眠可能永遠也結(jié)束不了,使用 TIMER_SBSTIME 選項的 clock_nanosleep() 可以避免這個問題
POSIX 時鐘
Linux 中需要使用 realtime,實時函數(shù)庫,需要鏈接 librt 即需要加入 -lrt 選項。
獲取時鐘的值
#includeint clock_getres(clockid_t clk_id, struct timespec *res); int clock_gettime(clockid_t clk_id, struct timespec *tp);
clock_gettime() 針對參數(shù) clk_id 所指定的時鐘返回時間,返回的時間,置于 tp 指向的結(jié)構中
clockid_t 是 SUSv3 規(guī)范定義的數(shù)據(jù)類型,用于表示時鐘標識符:
CLOCK_REALTIME 時鐘是一種系統(tǒng)級時鐘,用于度量真實時間,它的設置是可以變更的
CLOCK_MONOTONIC 時鐘對時間的度量始于"未予規(guī)范的過去某一時間點",系統(tǒng)啟動后就不會改變它,Linux 上,這種時鐘對時間的測量始于系統(tǒng)啟動
CLOCK_PROCESS_CPUTIME_ID 時鐘測量調(diào)用進程所消耗的用戶和系統(tǒng) CPU 時間
CLOCK_THREAD_CPUTIME_ID 時鐘測量調(diào)用線程所消耗的用戶和系統(tǒng) CPU 時間
設置時鐘的值
#define _POSIX_C_SOURCE 199309L #includeint clock_settime(clockid_t clk_id, const struct timespec *tp);
clock_settime() 利用 tp 指向緩沖區(qū)中的時間來設置由 clockid 指定的時鐘
如果 tp 指向的時間并非 clock_getres() 所返回的時鐘分辨率的整數(shù)倍,時間會向下取整
特權級進程可以設置 CLOCK_REALTIME 時鐘,該時鐘的初始值通常自 Epoch 以來的時間,其他時鐘類型不可更改
獲取特定進程或線程的時鐘 ID
要測量特定進程或線程消耗的 CPU 時間,首先要獲取其時鐘 ID:
#define _XOPEN_SOURCE 600 #includeint clock_getcpuclockid(pid_t pid, clockid_t *clock_id);
clock_getcpuclockid 將隸屬于 pid 進程的 CPU 時間時鐘的標識符置于 clock_id 指針指向的緩沖區(qū)中
#define _XOPEN_SOURCE 600 #include#include int pthread_getcpuclockid(pthread_t thread, clockid_t *clock_id);
pthread_getcpuclockid() 是 clock_getcpuclockid() 的 POSIX 線程版,返回的標識符所標識的時鐘用于度量調(diào)用進程中指定線程消耗的 CPU 時間
高分辨率休眠的改進版
#define _XOPEN_SOURCE 600 #includeint clock_nanosleep(clockid_t clock_id, int flags,const struct timespec *request,struct timespec *remain);
默認情況下,flags 是0,request 指定的休眠間隔時間是相對時間,如果 flags 設置為 TIMER_ABSTIME ,request 則表示 clock_id 所測量的絕對時間,這個特性對于需要精確休眠一段指定時間的應用程序至關重要,以相對時間進行休眠,進程可能執(zhí)行到一半就被占先了,結(jié)果休眠的時間要比預期的久
對于那些被信號處理器中斷并使用循環(huán)重啟休眠的進程來說,"嗜睡" 問題尤其明顯,如果以高頻接收信號,那么按相對時間休眠的進程在休眠時間上會有較大誤差,可以通過如下方式避免嗜睡:
先調(diào)用 clock_gettime() 獲取時間,再加上期望休眠的時間量
再以 TIMER_ABSTIME 標志調(diào)用 clock_nanosleep() 函數(shù),指定了 TIME_ABSTIME 時,不需要使用參數(shù) remain
信號處理器程序中斷了 clock_nanosleep() 調(diào)用,再次調(diào)用該函數(shù)來重啟休眠時,request 參數(shù)不變
struct timespec request; if(clock_gettime(CLOCK_REALTIME,&request) == -1) errExit("clock_gettime"); request.tv_sec += 20; /* sleep for 20 seconds from now*/ s = clock_nanosleep(CLOCK_REALTIME,TIMER_ABSTIME,&request,NULL); if(s != 0) { if(s == EINTR) printf("Interrupted by signal handler "); else errExit("clock_nanosleep"); }
POSIX 間隔式定時器
使用 settimer() 來設置經(jīng)典 UNIX 間隔式定時器,會收到如下制約:
針對 ITIMER_REAL,ITIMER_VIRTUAL 和 ITIMER_PROF 這 3 類定時器,每種智能設置一個
只能通過發(fā)送信號的方式通知定時器到期,另外也不能改變到期時產(chǎn)生的信號
如果一個間隔式定時器到期多次,且相應信號遭到阻塞時,那么會只調(diào)用一次信號處理器函數(shù),換言之,無從知曉是否出現(xiàn)定時器溢出的情況
定時器的分辨率只能達到微秒級
POSIX.1b 定義了一套 API 來突破這些限制,主要包含如下幾個階段:
timer_create() 創(chuàng)建一個新的定時器,并定義其到期時對進程的通知方法
timer_settime() 啟動或者停止一個定時器
timer_delete() 刪除不再需要使用的定時器
fork() 創(chuàng)建的子進程不會繼承 POSIX 定時器,調(diào)用 exec() 期間亦或是進程終止時將停止并刪除定時器。
使用 POSIX 定時器的 API 程序編譯時需要使用 -lrt 選項。
創(chuàng)建定時器
#define _POSIX_C_SOURCE 199309L #include#include int timer_create(clockid_t clockid, struct sigevent *sevp,timer_t *timerid);
timer_create() 創(chuàng)建一個新的定時器,并以 clockid 指定的時鐘進行時間度量
clockid 可以是 SUSv3 規(guī)范定義的類型,也可以采用 clock_getcpuclockid() 或 pthread_getcpuclockid() 返回的 clockid 值
函數(shù)返回時,timerid 指向的緩沖區(qū)放置定時器句柄,供后續(xù)調(diào)用中指代該定時器之用
sevp 可決定定時器到期時,對應應用程序的通知方式,指向 struct sigevent:
union sigval { /* Data passed with notification */ int sival_int; /* Integer value */ void *sival_ptr; /* Pointer value */ }; struct sigevent { int sigev_notify; /* Notification method */ int sigev_signo; /* Notification signal */ union sigval sigev_value; /* Data passed with notification */ void (*sigev_notify_function) (union sigval); /* Function used for thread notification (SIGEV_THREAD) */ void *sigev_notify_attributes; /* Attributes for notification thread (SIGEV_THREAD) */ pid_t sigev_notify_thread_id; /* ID of thread to signal (SIGEV_THREAD_ID) */ };
sigev_notify 字段的值:
SIGEV_NONE:不提供定時器的到期通知,進程可以使用 timer_gettime() 來監(jiān)控定時器的運轉(zhuǎn)情況
SIGEV_SIGNAL:定時器到期時,為進程生成指定于 sigev_signo 中的信號,如果 sigev_signal 為實時信號,那么 sigev_value 字段則指定了信號的伴隨數(shù)據(jù),通過 siginfo_t 結(jié)構的 si_value 可獲取這一數(shù)據(jù)
SIGEV_THREAD:定時器到期時,會調(diào)用由 sigev_notify_function 字段指定的函數(shù),調(diào)用該函數(shù)類似于調(diào)用新線程的啟動函數(shù)
SIGEV_THREAD_ID:與 SIGEV_THREAD 相類似,只是發(fā)送信號的目標線程 ID 要與 sigev_notify_thread_id 相匹配
配備和解除定時器
#define _POSIX_C_SOURCE 199309L #includeint timer_settime(timer_t timerid, int flags,const struct itimerspec *new_value,struct itimerspec *old_value);
timer_settime() 的參數(shù) timerid 是一個定時器句柄,由之前對 timer_create() 的調(diào)用返回
new_value 包含定時器的新設置,old_value 返回定時器的前一設置,如果對前一個設置不感興趣,可以設置為 NULLL
struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds */ }; struct itimerspec { struct timespec it_interval; /* Timer interval */ struct timespec it_value; /* Initial expiration */ };
it_value 指定了定時器首次到期的時間,it_interval 任意一個字段非0,那么就是一個周期性定時器,如果都是0,那么這個定時器將只到期一次
flags 如果是0,會將 value.it_value 視為始于 timer_settime() 調(diào)用時間點的相對值,如果 flags 設為 TIMER_ABSTIME,那么 value.it_value 則是一個絕對時間
為了啟動定時器,需要調(diào)用函數(shù) timer_settime(),并將 value.it_value 的一個或者全部字段設置為非0,如果之前曾經(jīng)配備過定時器,則 timer_settime() 會將之前的設置值替換掉
如果定時器的值和間隔時間并非對應時鐘分辨率的整數(shù)倍,那么會對這些值向上取整
要解除定時器,需要調(diào)用 timer_settime(),并將 value.it_value 的所有字段設置為 0
獲取定時器的當前值
#define _POSIX_C_SOURCE 199309L #includeint timer_gettime(timer_t timerid, struct itimerspec *curr_value);
timer_gettime() 返回由 timerid 指定的 POSIX 定時器的間隔以及剩余時間
如果返回結(jié)構 curr_value.it_value 的兩個字段都是0,表示定時器處于停止狀態(tài),如果 curr_value.it_interval 的兩個字段都是0,那么該定時器僅在 curr_value.it_value 給定的時間到期過一次
刪除定時器
每個 POSIX 定時器都會消耗少量的系統(tǒng)資源,一旦使用完畢,應當及時釋放這些資源:
#define _POSIX_C_SOURCE 199309L #includeint timer_delete(timer_t timerid);
對于已啟動的定時器,會在移除之前自動將其停止
進程終止時,會自動刪除所有定時器
通過信號發(fā)出通知
如果選擇通過信號來接收定時器通知,那么處理這些信號時既可以采用信號處理器函數(shù),也可以調(diào)用 sigwaitinfo() 或是 sigtimerdwait()。接收進程借助于這兩種方法可以獲取一個 siginfo_t 結(jié)構:
si_signo:包含由定時器產(chǎn)生的信號
si_code:置為 SI_TIMER,表示這是因為 POSIX 定時器到期而產(chǎn)生的信號
si_value:設置為以 timer_create()創(chuàng)建定時器在 evp.sigev_value 中提供的值
為 evp.sigev_value 指定不同的值,可以將到期時發(fā)送同類信號的不同定時器區(qū)分開。
Linux 還為 siginfo_t 結(jié)構提供了如下非標準字段:
si_overrun:包含了定時器溢出個數(shù)
定時器溢出
假設已經(jīng)選擇通過信號傳遞的方式來接收定時器到期的通知。在捕獲或接收相關信號之前定時器到期多次,或者不論直接調(diào)用 sigprocmask() 還是在信號處理器函數(shù)中暗中處理,也都有可能堵塞相關信號的發(fā)送,那如何知道這些定時器溢出?
接收到定時器信號之后,有兩種方法可以獲取定時器的溢出值:
調(diào)用 timer_getoverrun()
使用隨信號一同返回的結(jié)構 siginfo_t 中的 si_overrun 字段值,這種方法可以避免 timer_getoverrun() 調(diào)用開銷,但是這種方法是 Linux 擴展方法,無法移植
#define _POSIX_C_SOURCE 199309L #includeint timer_getoverrun(timer_t timerid);
每次收到定時器信號后,都會重置定時器溢出計數(shù),若自處理或接收定時器信號之后,定時器僅到期一次,則溢出計數(shù)為 0
返回由參數(shù) timerid 指定的定時器的溢出值
timer_getoverrun() 是異步信號安全的函數(shù),故而在信號處理器函數(shù)內(nèi)部調(diào)用也是安全的
通過線程來通知
SIGEV_THREAD 標志允許程序從一個獨立的線程中調(diào)用函數(shù)來獲取定時器到期通知。
利用文件描述符進行通知的定時器
Linux 內(nèi)核特有的創(chuàng)建定時器的 timerfd API,可從文件描述符中讀取其所創(chuàng)建定時器的到期通知。
#includeint timerfd_create(int clockid, int flags);
timerfd_create() 創(chuàng)建一個新的定時器對象,并返回一個指代該對象的文件描述符
clockid 的值,可以是:CLOCK_REALTIME 或者 CLOCK_MONOTONIC
flags 最初必須設置為0現(xiàn)在支持:
TFD_CLOEXEC:為新的文件描述符設置運行時關閉標志 FD_CLOEXEC 與 open() 的 O_CLOEXEC 適用于相同的情況
TFD_NONBLOCK:為底層的打開文件描述符設置 O_NONBLOCK 標志,隨后的讀操作將是非阻塞的
timerfd_create() 創(chuàng)建的定時器使用完畢后,應該調(diào)用 close() 關閉相應的文件描述符,以便內(nèi)核釋放相應的資源
#includeint timerfd_settime(int fd, int flags,const struct itimerspec *new_value,struct itimerspec *old_value);
timerfd_settime() 可以啟動或解除由文件描述符 fd 指代的定時器
new_value 為指定的新設置,old_value 為前一設置,如果不關心前一個設置可以將其設置為 NULL
flags 參數(shù)可以是0,此時將 new_value.it_value 的值視為相對于調(diào)用 timerfd_settime() 的相對時間點,也可以設置為 TFD_TIMER_ABSTIME 將其視為從時鐘0點開始測量的絕對時間點
#includeint timerfd_gettime(int fd, struct itimerspec *curr_value);
timerfd_gettime() 返回文件描述符 fd 所標識的定時器間隔和剩余時間
如果返回的 curr_value.it_value 字段都是0,那么該定時器已經(jīng)被解除,如果返回的結(jié)構 curr_value.it_interval 中的兩個字段都是0,那么定時器只會到期一次,到期時間在 curr_value.it_value 中給出
timerfd 與 fork() 以及 exec() 之間的交互
調(diào)用 fork() 期間,子進程會繼承 timerfd_create() 所創(chuàng)建的文件描述符的拷貝。
timerfd_create() 創(chuàng)建額度文件描述符能夠跨越 exec() 得以保存,除非將描述符設置為運行時關閉,已配備的定時器在 exec() 之后會繼承生成到期通知。
從 timerfd 文件描述符讀取
一旦以 timer_settime() 啟動了定時器,就可以從相應文件描述符中調(diào)用 read() 來讀取定時器的到期信息,處于這一目的,傳給 read() 的緩沖區(qū)必須滿足容納一個 uint64_t 類型的要求。
在上次使用 timerfd_settime() 修改設置以后,或者是最后一次執(zhí)行 read() 后,如果發(fā)生了一起或多起定時器到期時間,那么 read() 立即返回,返回的緩沖區(qū)中包含了到期的次數(shù)。
如果并無定時器到期,read() 將會阻塞至下一個到期。
也可以執(zhí)行 fcntl() 設置 O_NONBLOCK 標志,這時的讀動作將是非阻塞的,如果沒有定時器到期,則返回,設置錯誤 EAGAIN。
可以使用 select(),poll() 和 epoll() 對 timerfd 文件描述符進行監(jiān)控,如果定時器到期,則將對應的文件描述符標記為可讀。
審核編輯:劉清
-
處理器
+關注
關注
68文章
19160瀏覽量
229115 -
定時器
+關注
關注
23文章
3237瀏覽量
114467 -
Linux開發(fā)
+關注
關注
0文章
33瀏覽量
6894
原文標題:Linux應用開發(fā)之定時器
文章出處:【微信號:嵌入式應用研究院,微信公眾號:嵌入式應用研究院】歡迎添加關注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論