Linux 應用開發之定時器
來源:嵌入式應用研究院
間隔定時器
#include <sys/time.h>
int setitimer(int which,const struct itimerval* new_value,struct itimerval* old_value);
-
setitimer()
創建一個間隔式定時器,這種定時器會在未來某個時間點到期,並於此後 (可選擇地) 每間隔一段時間到期一次 -
which
可以指定以下值: -
ITIMER_REAL
:創建以真實時間倒計時的定時器,到期會產生SIGALARM
信號併發送給進程 -
ITIMER_VIRTUAL
:創建以進程虛擬時間 (用戶模式下的 CPU 時間) 倒計時的定時器,到期時會產生信號SIGVTALRM
-
ITIMER_PROF
:創建一個profiling
定時器,以進程時間 (用戶態與內核態 CPU 時間的總和) 倒計時,到期時,則會產生SIGPROF
信號
針對所有這些信號的默認處置均會終止進程,除非真地期望如此,否則就需要針對這些定時器信號創建處理器函數。
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 種定時器的一種,當第二次調用
settimer()
時,修改已有定時器的屬性要符合參數which
中的類型,如果調用setitimer()
時將new_value.it_value
的兩個字段均設置爲 0,那麼會屏蔽任何已有的定時器 -
若
old_value
不爲NULL
,則以其所指向的itimerval
結構來返回定時器的前一設置: -
如果
old_value.it_value
的兩個字段值均爲 0,那麼該定時器之前被設置處於屏蔽狀態 -
如果
old_value.it_interval
的兩個字段值均爲 0,那麼該定時器之前被設置爲歷經old_value.it_value
指定時間到期的一次性定時器 -
對需要在新定時器到期後將其還原的情況而言,獲取定時器的前一設置就很重要,如果不關心定時器的前一設置,可以將
old_value
設置爲NULL
-
定時器會從初始值
it_value
倒計時一直到 0 爲止,遞減爲 0 時,會將相應信號發送給進程,隨後,如果時間間隔值it_interval
非 0,那麼會再次將it_value
加載到定時器,重新開始向 0 倒計時
可以在任何時刻調用 getitimer()
,以瞭解定時器的當前狀態,距離下次到期的剩餘時間:
#include <sys/time.h>
int getitimer(int which,struct itimerval* curr_value);
getitimer()
返回由which
指定定時器的當前狀態,並置於curr_value
指向的緩衝區中
使用 setitimer()
和 alarm()
創建的定時器可以跨越 exec()
調用而得以保存,但由 fork()
創建的子進程並不繼承該定時器。
更爲簡單的定時器接口:alarm()
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
-
seconds
表示定時器到期的秒數,到期時向調用進程發送SIGALRM
信號 -
調用
alarm()
會覆蓋對定時器的前一個設置,調用alarm(0)
可以屏蔽現有定時器 -
返回值是定時器前一設置距離到期的剩餘描述,如果之前並無設置,則返回 0
setitimer()
和 alarm()
之間的交互
Linux 中 alarm()
和 setitimer()
針對同一進程共享一個實時定時器,無論調用兩者之中的哪個完成了對定時器的前一設置,同樣可以調用二者中的任一函數來改變這一設置。
程序設置實時定時器時,最好選用二者之一。
定時器的調度和精度
內核配置項 CONFIG_HIGH_RES_TIMERS
可以支持高分辨率定時器,使得定時器的精度不受軟件時鐘週期的影響,可以達到底層硬件所支持的精度,在現代硬件平臺上,精度達到微秒級別是司空見慣的。
爲阻塞操作設置超時
實時定時器的用途之一就是爲某個阻塞系統調用設置其處於阻塞狀態的時間上限。
例如,處理 read()
操作:
-
調用
sigaction()
創建SIGALRM
信號的處置函數,排除SA_RESTART
標誌以確保系統調用不會重新啓動 -
調用
alarm()
或者setitimer()
創建定時器,設置超時時間 -
執行阻塞的系統調用
-
系統調用返回,再次調用
alarm()
或setitimer()
屏蔽定時器 -
檢查系統調用失敗是否設置
errno
爲EINTR
,即系統調用遭到中斷
暫停運行一段固定時間
低分辨率休眠:sleep()
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
-
sleep()
可以暫停調用進程執行seconds
秒,或者在捕獲信號後恢復進程的執行 -
如果休眠正常結束,返回 0,如果因信號中斷休眠,返回剩餘的秒數
-
考慮到一致性,應該避免
sleep()
和alarm()
以及setitimer()
之間的混用,Linux 將sleep()
實現爲對nanosleep()
的調用,而有些老系統使用alarm()
和SIGALRM
信號處理函數實現sleep()
高分辨率休眠 nanosleep()
#include <time.h>
int nanosleep(const struct timespec *req, struct timespec *rem);
-
nanosleep()
與sleep()
相似,但是分辨率更高 -
struct timespec
:
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
-
規範規定不得使用信號實現該函數,這意味着
nanosleep()
與alarm()
和setitimer()
混用,也不會危及程序的可移植性 -
儘管
nanosleep()
沒有使用信號,但還是可以通過信號處理器函數將其中斷,此時將返回 -1,並設置錯誤EINTR
,如果remain
不爲NULL
,則該指針所指向的緩衝區將返回剩餘的休眠時間,可以利用這個返回值重啓該系統調用以完成休眠,但是由於返回的remain
時間未必是軟件時鐘間隔的整數倍,故而每次重啓都會遭受取整,其結果是,每次重啓後的休眠時間都要長於前一調用返回的remain
值,在信號接收頻率很高的情況下,進程的休眠可能永遠也結束不了,使用TIMER_SBSTIME
選項的clock_nanosleep()
可以避免這個問題
POSIX 時鐘
Linux 中需要使用 realtime,實時函數庫,需要鏈接 librt
即需要加入 -lrt
選項。
獲取時鐘的值
#include <time.h>
int clock_getres(clockid_t clk_id, struct timespec *res);
int clock_gettime(clockid_t clk_id, struct timespec *tp);
-
clock_gettime()
針對參數clk_id
所指定的時鐘返回時間,返回的時間,置於tp
指向的結構中 -
clockid_t
是 SUSv3 規範定義的數據類型,用於表示時鐘標識符:
-
CLOCK_REALTIME
時鐘是一種系統級時鐘,用於度量真實時間,它的設置是可以變更的 -
CLOCK_MONOTONIC
時鐘對時間的度量始於 "未予規範的過去某一時間點",系統啓動後就不會改變它,Linux 上,這種時鐘對時間的測量始於系統啓動 -
CLOCK_PROCESS_CPUTIME_ID
時鐘測量調用進程所消耗的用戶和系統 CPU 時間 -
CLOCK_THREAD_CPUTIME_ID
時鐘測量調用線程所消耗的用戶和系統 CPU 時間
設置時鐘的值
#define _POSIX_C_SOURCE 199309L
#include <time.h>
int clock_settime(clockid_t clk_id, const struct timespec *tp);
-
clock_settime()
利用tp
指向緩衝區中的時間來設置由clockid
指定的時鐘 -
如果
tp
指向的時間並非clock_getres()
所返回的時鐘分辨率的整數倍,時間會向下取整 -
特權級進程可以設置
CLOCK_REALTIME
時鐘,該時鐘的初始值通常自 Epoch 以來的時間,其他時鐘類型不可更改
獲取特定進程或線程的時鐘 ID
要測量特定進程或線程消耗的 CPU 時間,首先要獲取其時鐘 ID:
#define _XOPEN_SOURCE 600
#include <time.h>
int clock_getcpuclockid(pid_t pid, clockid_t *clock_id);
clock_getcpuclockid
將隸屬於pid
進程的 CPU 時間時鐘的標識符置於clock_id
指針指向的緩衝區中
#define _XOPEN_SOURCE 600
#include <pthread.h>
#include <time.h>
int pthread_getcpuclockid(pthread_t thread, clockid_t *clock_id);
pthread_getcpuclockid()
是clock_getcpuclockid()
的 POSIX 線程版,返回的標識符所標識的時鐘用於度量調用進程中指定線程消耗的 CPU 時間
高分辨率休眠的改進版
#define _XOPEN_SOURCE 600
#include <time.h>
int clock_nanosleep(clockid_t clock_id, int flags,const struct timespec *request,struct timespec *remain);
- 默認情況下,
flags
是 0,request
指定的休眠間隔時間是相對時間,如果flags
設置爲TIMER_ABSTIME
,request
則表示clock_id
所測量的絕對時間,這個特性對於需要精確休眠一段指定時間的應用程序至關重要,以相對時間進行休眠,進程可能執行到一半就被佔先了,結果休眠的時間要比預期的久
對於那些被信號處理器中斷並使用循環重啓休眠的進程來說,"嗜睡" 問題尤其明顯,如果以高頻接收信號,那麼按相對時間休眠的進程在休眠時間上會有較大誤差,可以通過如下方式避免嗜睡:
-
先調用
clock_gettime()
獲取時間,再加上期望休眠的時間量 -
再以
TIMER_ABSTIME
標誌調用clock_nanosleep()
函數,指定了TIME_ABSTIME
時,不需要使用參數remain
-
信號處理器程序中斷了
clock_nanosleep()
調用,再次調用該函數來重啓休眠時,request
參數不變
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\n");
else
errExit("clock_nanosleep");
}
POSIX 間隔式定時器
使用 settimer()
來設置經典 UNIX 間隔式定時器,會收到如下制約:
-
針對
ITIMER_REAL
,ITIMER_VIRTUAL
和ITIMER_PROF
這 3 類定時器,每種智能設置一個 -
只能通過發送信號的方式通知定時器到期,另外也不能改變到期時產生的信號
-
如果一個間隔式定時器到期多次,且相應信號遭到阻塞時,那麼會只調用一次信號處理器函數,換言之,無從知曉是否出現定時器溢出的情況
-
定時器的分辨率只能達到微秒級
POSIX.1b 定義了一套 API 來突破這些限制,主要包含如下幾個階段:
-
timer_create()
創建一個新的定時器,並定義其到期時對進程的通知方法 -
timer_settime()
啓動或者停止一個定時器 -
timer_delete()
刪除不再需要使用的定時器
fork()
創建的子進程不會繼承 POSIX 定時器,調用 exec()
期間亦或是進程終止時將停止並刪除定時器。
使用 POSIX 定時器的 API 程序編譯時需要使用 -lrt
選項。
創建定時器
#define _POSIX_C_SOURCE 199309L
#include <signal.h>
#include <time.h>
int timer_create(clockid_t clockid, struct sigevent *sevp,timer_t *timerid);
-
timer_create()
創建一個新的定時器,並以clockid
指定的時鐘進行時間度量 -
clockid
可以是 SUSv3 規範定義的類型,也可以採用clock_getcpuclockid()
或pthread_getcpuclockid()
返回的clockid
值 -
函數返回時,
timerid
指向的緩衝區放置定時器句柄,供後續調用中指代該定時器之用 -
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()
來監控定時器的運轉情況 -
SIGEV_SIGNAL
:定時器到期時,爲進程生成指定於sigev_signo
中的信號,如果sigev_signal
爲實時信號,那麼sigev_value
字段則指定了信號的伴隨數據,通過siginfo_t
結構的si_value
可獲取這一數據 -
SIGEV_THREAD
:定時器到期時,會調用由sigev_notify_function
字段指定的函數,調用該函數類似於調用新線程的啓動函數 -
SIGEV_THREAD_ID
:與SIGEV_THREAD
相類似,只是發送信號的目標線程 ID 要與sigev_notify_thread_id
相匹配
配備和解除定時器
#define _POSIX_C_SOURCE 199309L
#include <time.h>
int timer_settime(timer_t timerid, int flags,const struct itimerspec *new_value,struct itimerspec *old_value);
-
timer_settime()
的參數timerid
是一個定時器句柄,由之前對timer_create()
的調用返回 -
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()
調用時間點的相對值,如果flags
設爲TIMER_ABSTIME
,那麼value.it_value
則是一個絕對時間 -
爲了啓動定時器,需要調用函數
timer_settime()
,並將value.it_value
的一個或者全部字段設置爲非 0,如果之前曾經配備過定時器,則timer_settime()
會將之前的設置值替換掉 -
如果定時器的值和間隔時間並非對應時鐘分辨率的整數倍,那麼會對這些值向上取整
-
要解除定時器,需要調用
timer_settime()
,並將value.it_value
的所有字段設置爲 0
獲取定時器的當前值
#define _POSIX_C_SOURCE 199309L
#include <time.h>
int timer_gettime(timer_t timerid, struct itimerspec *curr_value);
-
timer_gettime()
返回由timerid
指定的 POSIX 定時器的間隔以及剩餘時間 -
如果返回結構
curr_value.it_value
的兩個字段都是 0,表示定時器處於停止狀態,如果curr_value.it_interval
的兩個字段都是 0,那麼該定時器僅在curr_value.it_value
給定的時間到期過一次
刪除定時器
每個 POSIX 定時器都會消耗少量的系統資源,一旦使用完畢,應當及時釋放這些資源:
#define _POSIX_C_SOURCE 199309L
#include <time.h>
int timer_delete(timer_t timerid);
-
對於已啓動的定時器,會在移除之前自動將其停止
-
進程終止時,會自動刪除所有定時器
通過信號發出通知
如果選擇通過信號來接收定時器通知,那麼處理這些信號時既可以採用信號處理器函數,也可以調用 sigwaitinfo()
或是 sigtimerdwait()
。接收進程藉助於這兩種方法可以獲取一個 siginfo_t
結構:
-
si_signo
:包含由定時器產生的信號 -
si_code
:置爲SI_TIMER
,表示這是因爲 POSIX 定時器到期而產生的信號 -
si_value
:設置爲以timer_create()
創建定時器在evp.sigev_value
中提供的值
爲 evp.sigev_value
指定不同的值,可以將到期時發送同類信號的不同定時器區分開。
Linux 還爲 siginfo_t
結構提供瞭如下非標準字段:
si_overrun
:包含了定時器溢出個數
定時器溢出
假設已經選擇通過信號傳遞的方式來接收定時器到期的通知。在捕獲或接收相關信號之前定時器到期多次,或者不論直接調用 sigprocmask()
還是在信號處理器函數中暗中處理,也都有可能堵塞相關信號的發送,那如何知道這些定時器溢出?
接收到定時器信號之後,有兩種方法可以獲取定時器的溢出值:
-
調用
timer_getoverrun()
-
使用隨信號一同返回的結構
siginfo_t
中的si_overrun
字段值,這種方法可以避免timer_getoverrun()
調用開銷,但是這種方法是 Linux 擴展方法,無法移植
#define _POSIX_C_SOURCE 199309L
#include <time.h>
int timer_getoverrun(timer_t timerid);
-
每次收到定時器信號後,都會重置定時器溢出計數,若自處理或接收定時器信號之後,定時器僅到期一次,則溢出計數爲 0
-
返回由參數
timerid
指定的定時器的溢出值 -
timer_getoverrun()
是異步信號安全的函數,故而在信號處理器函數內部調用也是安全的
通過線程來通知
SIGEV_THREAD
標誌允許程序從一個獨立的線程中調用函數來獲取定時器到期通知。
利用文件描述符進行通知的定時器
Linux 內核特有的創建定時器的 timerfd API,可從文件描述符中讀取其所創建定時器的到期通知。
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
-
timerfd_create()
創建一個新的定時器對象,並返回一個指代該對象的文件描述符 -
clockid
的值,可以是:CLOCK_REALTIME
或者CLOCK_MONOTONIC
-
flags
最初必須設置爲 0 現在支持: -
TFD_CLOEXEC
:爲新的文件描述符設置運行時關閉標誌FD_CLOEXEC
與open()
的O_CLOEXEC
適用於相同的情況 -
TFD_NONBLOCK
:爲底層的打開文件描述符設置O_NONBLOCK
標誌,隨後的讀操作將是非阻塞的 -
timerfd_create()
創建的定時器使用完畢後,應該調用close()
關閉相應的文件描述符,以便內核釋放相應的資源
#include <sys/timerfd.h>
int timerfd_settime(int fd, int flags,const struct itimerspec *new_value,struct itimerspec *old_value);
-
timerfd_settime()
可以啓動或解除由文件描述符fd
指代的定時器 -
new_value
爲指定的新設置,old_value
爲前一設置,如果不關心前一個設置可以將其設置爲NULL
-
flags
參數可以是 0,此時將new_value.it_value
的值視爲相對於調用timerfd_settime()
的相對時間點,也可以設置爲TFD_TIMER_ABSTIME
將其視爲從時鐘 0 點開始測量的絕對時間點
#include <sys/timerfd.h>
int timerfd_gettime(int fd, struct itimerspec *curr_value);
-
timerfd_gettime()
返回文件描述符fd
所標識的定時器間隔和剩餘時間 -
如果返回的
curr_value.it_value
字段都是 0,那麼該定時器已經被解除,如果返回的結構curr_value.it_interval
中的兩個字段都是 0,那麼定時器只會到期一次,到期時間在curr_value.it_value
中給出
timerfd 與 fork()
以及 exec()
之間的交互
調用 fork()
期間,子進程會繼承 timerfd_create()
所創建的文件描述符的拷貝。
timerfd_create()
創建額度文件描述符能夠跨越 exec()
得以保存,除非將描述符設置爲運行時關閉,已配備的定時器在 exec()
之後會繼承生成到期通知。
從 timerfd 文件描述符讀取
一旦以 timer_settime()
啓動了定時器,就可以從相應文件描述符中調用 read()
來讀取定時器的到期信息,處於這一目的,傳給 read()
的緩衝區必須滿足容納一個 uint64_t
類型的要求。
在上次使用 timerfd_settime()
修改設置以後,或者是最後一次執行 read()
後,如果發生了一起或多起定時器到期時間,那麼 read()
立即返回,返回的緩衝區中包含了到期的次數。
如果並無定時器到期,read()
將會阻塞至下一個到期。
也可以執行 fcntl()
設置 O_NONBLOCK
標誌,這時的讀動作將是非阻塞的,如果沒有定時器到期,則返回,設置錯誤 EAGAIN
。
可以使用 select()
,poll()
和 epoll()
對 timerfd
文件描述符進行監控,如果定時器到期,則將對應的文件描述符標記爲可讀。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/KgnW2WEyAvdpgNTPKapeWw