1. 程式人生 > >Linux時間子系統(三) 用戶空間接口函數

Linux時間子系統(三) 用戶空間接口函數

因此 delet inter 擁有 基本 數據 關系 and sse

一、前言

從應用程序的角度看,內核需要提供的和時間相關的服務有三種:

1、和系統時間相關的服務。例如,在向數據庫寫入一條記錄的時候,需要記錄操作時間(何年何月何日何時)。

2、讓進程睡眠一段時間

3、和timer相關的服務。在一段指定的時間過去後,kernel要alert用戶進程

本文主要描述和時間子系統相關的用戶空間接口函數知識。

二、和系統時間相關的服務

1、秒級別的時間函數:time和stime

time和stime函數的定義如下:

#include <time.h>

time_t time(time_t *t);

int stime(time_t *t);

time函數返回了當前時間點到linux epoch的秒數(內核中timekeeper模塊保存了這個值,timekeeper->xtime_sec)。stime是設定當前時間點到linux epoch的秒數。對於linux kernel,設定時間的進程必須擁有CAP_SYS_TIME的權利,否則會失敗。

linux kernel用系統調用sys_time和sys_stime來支持這兩個函數。實際上,在引入更高精度的時間相關的系統調用之後(例如:sys_gettimeofday),上面這兩個系統調用可以用新的系統調在用戶空間實現time和stime函數。在kernel中,只有定義了__ARCH_WANT_SYS_TIME這個宏,系統才會提供上面這兩個系統調用。當然,提供這樣的系統調用多半是為了兼容舊的應用軟件。

配合上面的接口函數還有一系列將當前時間點到linux epoch的秒數轉換成適合人類閱讀的接口函數,例如asctime, ctime, gmtime, localtime, mktime, asctime_r, ctime_r, gmtime_r, localtime_r ,這些函數主要用來將time_t類型的時間轉換成break-down time或者字符形式。

2、微秒級別的時間函數:gettimeofday和settimeofday

#include <sys/time.h>

int gettimeofday(struct timeval *tv, struct timezone *tz);

int settimeofday(const struct timeval *tv, const struct timezone *tz);

這兩個函數和上一小節秒數的函數類似,只不過時間精度可以達到微秒級別。gettimeofday函數可以獲取從linux epoch到當前時間點的秒數以及微秒數(在內核態,這個時間值仍然是通過timekeeper模塊獲得的,具體接口是getnstimeofday64,該接口的時間精度是納秒級別的,不過沒有關系,除以1000就獲得微秒級別的精度了),settimeofday則是設定從linux epoch到當前時間點的秒數以及微秒數。同樣的,設定時間的進程必須擁有CAP_SYS_TIME的權利,否則會失敗。tz參數是由於歷史原因而存在,實際上內核並沒有對timezone進行支持。

顯然,sys_gettimeofday和sys_settimeofday這兩個系統調用是用來支持上面兩個函數功能的,值得一提的是:這些系統調用在新的POSIX標準中 gettimeofday和settimeofday接口函數被標註為obsolescent,取而代之的是clock_gettime和clock_settime接口函數

3、納秒級別的時間函數:clock_gettime和clock_settime

#include <time.h>

int clock_getres(clockid_t clk_id, struct timespec *res);

int clock_gettime(clockid_t clk_id, struct timespec *tp);

int clock_settime(clockid_t clk_id, const struct timespec *tp);

如果不是clk_id這個參數,clock_gettime和clock_settime基本上是不用解釋的,其概念和gettimeofday和settimeofday接口函數是完全類似的,除了精度是納秒。clock就是時鐘的意思,它記錄了時間的流逝。clock ID當然就是識別system clock(系統時鐘)的ID了,定義如下:

CLOCK_REALTIME
CLOCK_MONOTONIC
CLOCK_MONOTONIC_RAW
CLOCK_PROCESS_CPUTIME_ID
CLOCK_THREAD_CPUTIME_ID

根據應用的需求,內核維護了幾個不同系統時鐘。大家最熟悉的當然就是CLOCK_REALTIME這個系統時鐘,因為它表示了真實世界的墻上時鐘(前面兩節的接口函數沒有指定CLOCK ID,實際上獲取的就是CLOCK_REALTIME的時間值)。CLOCK_REALTIME這個系統時鐘允許用戶對其進行設定(當然要有CAP_SYS_TIME權限),這也就表示在用戶空間可以對該系統時鐘進行修改,產生不連續的時間間斷點。除此之外,也可以通過NTP對該時鐘進行調整(不會有間斷點,NTP調整的是local oscillator和上遊服務器頻率誤差而已)。

僅僅從名字上就可以看出CLOCK_MONOTONIC的系統時鐘應該是單調遞增的,此外,該時鐘也是真實世界的墻上時鐘,只不過其基準點不一定是linux epoch(當然也可以是),一般會把系統啟動的時間點設定為其基準點。隨後該時鐘會不斷的遞增。除了可以通過NTP對該時鐘進行調整之外,其他任何程序不允許設定該時鐘,這樣也就保證了該時鐘的單調性。

CLOCK_MONOTONIC_RAW具備CLOCK_MONOTONIC的特性,除了NTP調整。也就是說,clock id是CLOCK_MONOTONIC_RAW的系統時鐘是一個完全基於本地晶振的時鐘。不能設定,也不能對對晶振頻率進行調整。

在調用clock_gettime和clock_settime接口函數時,如果傳遞clock id參數是CLOCK_REALTIME的話,那麽這兩個函數的行為和前兩個小節描述的一致,除了是ns精度。讀到這裏,我詳細廣大人民群眾不免要問:為何要有其他類型的系統時鐘呢?MONOTONIC類型的時鐘相對比較簡單,如果你設定事件A之後5秒進行動作B,那麽用MONOTONIC類型的時鐘是一個比較好的選擇,如果使用REALTIME的時鐘,當用戶在事件A和動作B之間插入時間設定的操作,那麽你設定事件A之後5秒進行動作B將不能觸發。此外,用戶需要了解系統啟動時間,這個需求需要使用MONOTONIC類型的時鐘的時鐘。需要指出的是MONOTONIC類型的時鐘不是絕對時間的概念,多半是計算兩個采樣點之間的時間,並且保證采樣點之間時間的單調性。MONOTONIC_RAW是一個底層工具,一般而言程序員不會操作它,使用MONOTONIC類型的時鐘就夠用了,當然,一些高級的應用場合,例如你想使用另外的方法(不是NTP)來調整時間,那麽就可以使用MONOTONIC_RAW了。

有些應用場景使用real time的時鐘(墻上時鐘)是不合適的,例如當我們進行系統中各個應用程序的性能分析和統計的時候。正因為如此,kernel提供了基於進程或者線程的系統時鐘,也就是CLOCK_PROCESS_CPUTIME_ID和CLOCK_THREAD_CPUTIME_ID了。當我們打算使用基於進程或者線程的系統時鐘的時候,需要首先獲取clock id:

#include <time.h>

int clock_getcpuclockid(pid_t pid, clockid_t *clock_id);

如果是線程的話,需要調用pthread_getcpuclockid接口函數:

#include <pthread.h>
#include <time.h>

int pthread_getcpuclockid(pthread_t thread, clockid_t *clock_id);

雖然這組函數接口的精度可以達到ns級別,但是實際的系統可以達到什麽樣的精度是實現相關的,因此,clock_getres用來獲取系統時鐘的精度。

4、系統時鐘的調整

設定系統時間是一個比較粗暴的做法,一旦修改了系統時間,系統中的很多依賴絕對時間的進程會有各種奇奇怪怪的行為。正因為如此,系統提供了時間同步的接口函數,可以讓外部的精準的計時服務器來不斷的修正系統時鐘。

(1)adjtime接口函數

int adjtime(const struct timeval *delta, struct timeval *olddelta);

該函數可以根據delta參數緩慢的修正系統時鐘(CLOCK_REALTIME那個)。olddelta返回上一次調整中尚未完整的delta。

(2)adjtimex

#include <sys/timex.h>

int adjtimex(struct timex *buf);

RFC 1305定義了更復雜,更強大的時間調整算法,因此linux kernel通過sys_adjtimex支持這個算法,其用戶空間的接口函數就是adjtimex。由於這個算法過去強大,這裏就不再贅述,等有時間、有興趣之後再填補這裏的空白吧。

Linux內核提供了sys_adjtimex系統調用來支持上面兩個接口函數。此外,還提供了sys_clock_adjtime的系統調用來支持POSIX clock tunning。

三、進程睡眠

1、秒級別的sleep函數:sleep

#include <unistd.h>

unsigned int sleep(unsigned int seconds);

調用該函數會導致當前進程sleep,seconds之後(基於CLOCK_REALTIME)會返回繼續執行程序。該函數的返回值說明了進程沒有進入睡眠的時間。例如如果我們想要睡眠8秒,但是由於siganl中斷了睡眠,只是sleep了5秒,那麽返回值就是3,表示有3秒還沒有睡。

2、微秒級別的sleep函數:usleep

#include <unistd.h>

int usleep(useconds_t usec);

概念上和sleep一樣,不過返回值的定義不同。usleep返回0表示執行成功,返回-1說明執行失敗,錯誤碼在errno中獲取。

3、納秒級別的sleep函數:nanosleep

#include <time.h>

int nanosleep(const struct timespec *req, struct timespec *rem);

usleep函數已經是過去式,不建議使用,取而代之的是nanosleep函數。req中設定你要sleep的秒以及納秒值,然後調用該函數讓當前進程sleep。返回0表示執行成功,返回-1說明執行失敗,錯誤碼在errno中獲取。EINTR表示該函數被signal打斷。rem參數是remaining time的意思,也就是說還有多少時間沒有睡完。

linux kernel並沒有提供sleep和usleep對應的系統調用,sleep和usleep的實現位於c lib。在有些系統中,這些實現是依賴信號的,也有的系統使用timer來實現的,對於GNU系統,sleep和usleep和nanosleep函數一樣,都是通過kernel的sys_nanosleep的系統調用實現的(底層是基於hrtimer)。

4、更高級的sleep函數:clock_nanosleep

#include <time.h>

int clock_nanosleep(clockid_t clock_id, int flags,
const struct timespec *request,
struct timespec *remain);

clock_nanosleep接口函數需要傳遞更多的參數,當然也就是意味著它功能更強大。clock_id說明該接口函數不僅能基於real time clock睡眠,還可以基於其他的系統時鐘睡眠。flag等於0或者1,分別指明request參數設定的時間值是相對時間還是絕對時間。

四、和timer相關的服務

1、alarm函數

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

alarm函數是使用timer最簡單的接口。在指定秒數(基於CLOCK_REALTIME)的時間過去後,向該進程發送SIGALRM信號。當然,調用該接口的程序需要設定signal handler。

2、Interval timer函數

#include <sys/time.h>

int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);

Interval timer函數的行為和alarm函數類似,不過功能更強大。每個進程支持3種timer,不同的timer定義了如何計時以及發送什麽樣的信號給進程,which參數指明使用哪個timer:

(1)ITIMER_REAL。基於CLOCK_REALTIME計時,超時後發送SIGALRM信號,和alarm函數一樣。

(2)ITIMER_VIRTUAL。只有當該進程的用戶空間代碼執行的時候才計時,超時後發送SIGVTALRM信號。

(3)ITIMER_PROF。只有該進程執行的時候才計時,不論是執行用戶空間代碼還是陷入內核執行(例如系統調用),超時後發送SIGPROF信號。

struct itimerval定義如下:

struct itimerval {
struct timeval it_interval; /* next value */
struct timeval it_value; /* current value */
};

兩個成員分別指明了本次和下次(超期後如何設定)的時間值。通過這樣的定義,interval timer可以實現one shot類型的timer和periodic的timer。例如current value設定為5秒,next value設定為3秒,設定這樣的timer後,it_value值會不斷遞減,直到5秒後觸發,而隨後it_value的值會被重新加載(使用it_interval的值),也就是等於3秒,之後會按照3為周期不斷的觸發。

old_value返回上次setitimer函數的設定值。getitimer函數獲取當前的Interval timer的狀態,其中的it_value成員可以得到當前時刻到下一次觸發點的世時間信息,it_interval成員保持不變,除非你重新調用setitimer重新設定。

雖然interval timer函數也是POSIX標準的一部分,不過在新的POSIX標準中,interval timer接口函數被標註為obsolescent,取而代之的是POSIX timer接口函數。

3、更高級,更靈活的timer函數

上一節介紹的Interval timer函數還是有功能不足之處:例如一個進程只能有ITIMER_REAL、ITIMER_VIRTUAL和ITIMER_PROF三個timer,如果連續設定其中一種timer(例如ITIMER_REAL),這會導致第一個設定被第二次設定覆蓋。此外,超時處理永遠是用信號的方式,而且發送的signal不能修改。當mask信號處理的時候,雖然timer多次超期,但是signal handler只會調用一次,無法獲取更詳細的信息。最後一點,Interval timer函數精度是微秒級別,精度有進一步提升的空間。正因為傳統的Interval timer函數的不足之處,POSIX標準定義了更高級,更靈活的timer函數,我們稱之POSIX (interval)Timer。

(1)創建timer

#include <signal.h>
#include <time.h>

int timer_create(clockid_t clockid, struct sigevent *sevp, timer_t *timerid);

在這個接口函數中,clock id相信大家都很熟悉了, timerid一看就是返回的timer ID的句柄,就像open函數返回的文件描述符一樣。因此,要理解這個接口函數重點是了解struct sigevent這個數據結構:

union sigval { /* Data passed with notification */
int sival_int; /* Integer value */
void *sival_ptr; /* Pointer value */
};

typedef struct sigevent {
sigval_t sigev_value;
int sigev_signo;
int sigev_notify;
union {
int _pad[SIGEV_PAD_SIZE];
int _tid;

struct {
void (*_function)(sigval_t);
void *_attribute; /* really pthread_attr_t */
} _sigev_thread;
} _sigev_un;
} sigevent_t;

sigev_notify定義了當timer超期後如何通知該進程,可以設定:

(a)SIGEV_NONE。不需要異步通知,程序自己調用timer_gettime來輪詢timer的當前狀態

(b)SIGEV_SIGNAL。使用sinal這樣的異步通知方式。發送的信號由sigev_signo定義。如果發送的是realtime signal,該信號的附加數據由sigev_value定義。

(c)SIGEV_THREAD。創建一個線程執行timer超期callback函數,_attribute定義了該線程的屬性。

(d)SIGEV_THREAD_ID。行為和SIGEV_SIGNAL類似,不過發送的信號被送達進程內的一個指定的thread,這個thread由_tid標識。

(2)設定timer

#include <time.h>

int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value,
struct itimerspec * old_value);
int timer_gettime(timer_t timerid, struct itimerspec *curr_value);

timerid就是上一節中通過timer_create創建的timer。new_value和old_value這兩個參數類似setitimer函數,這裏就不再細述了。flag等於0或者1,分別指明new_value參數設定的時間值是相對時間還是絕對時間。如果new_value.it_value是一個非0值,那麽調用timer_settime可以啟動該timer。如果new_value.it_value是一個0值,那麽調用timer_settime可以stop該timer。

timer_gettime函數和getitimer類似,可以參考上面的描述。

(3)刪除timer

#include <time.h>

int timer_delete(timer_t timerid);

有創建就有刪除,timer_delete用來刪除指定的timer,釋放資源。

Linux時間子系統(三) 用戶空間接口函數