1. 程式人生 > >[轉]淺析 Linux 中的時間程式設計和實現原理,第 1 部分: Linux 應用層的時間程式設計

[轉]淺析 Linux 中的時間程式設計和實現原理,第 1 部分: Linux 應用層的時間程式設計

引子

我們都生活在時間中,但卻無法去思考它。什麼是時間呢?似乎這是一個永遠也不能被回答的問題。然而作為一個程式設計師,在工作中,總有那麼幾次我必須思考什麼是時間。比如,需要知道一段程式碼運行了多久;要在 log 檔案中記錄事件發生時的時間戳;再比如需要一個定時器以便能夠定期做某些計算機操作。我發現,在計算機世界中,時間在不同場合也往往有不同的含義,讓試圖思考它的人感到迷茫。但值得慶幸的是,Linux 中的時間終究是可以理解的。因此我打算討論一下有關時間的話題,嘗試著深入理解 Linux 系統中 C 語言程式設計中的時間問題。主要內容如下:

  • 第 1 部分是應用程式中的時間問題。有三個方面:程式計時需要;獲取當前時間;定時器。
  • 第 2 部分包括時間硬體簡介和 GlibC 實現時間函式的原理。
  • 第 3 和第 4 部分是 Linux 核心對時間的支援和實現原理。

現在開始第 1 部分,探討應用開發中的時間程式設計問題。在這一部分中,所有的例子程式碼都在 GlibC 2.14,核心 2.6.33 的 Linux 系統下編譯並驗證執行過。讀者如果使用低版本的 GlibC 和 Linux 核心有可能無法正確執行。

時間的獲取

在程式當中, 我們經常要輸出系統當前的時間,比如日誌檔案中的每一個事件都要記錄其產生時間。在 C 語言中獲取當前時間的方法有以下幾種,它們所獲得的時間精度從秒級到納秒,各有所不同。

表 1. C 時間函式
function 定義 含義 返回值 精度
time() time 函式獲得從 1970 年 1 月 1 日 0 點到當前的秒數,儲存在time_t結構之中。 time_t
gettimeofday() gettimeofday 函式返回從 1970 年 1 月 1 日 0 點以來,到現在的時間。用 timeval 資料結構表示。 struct timeval
{
time_t tv_sec;
long int tv_usec;
};
微秒
clock_gettime() clock_gettime 函式返回從 1970 年 1 月 1 日 0 點以來,到現在的時間。用 timespec 資料結構表示。
支援不廣泛。屬於實時擴充套件。
struct timespec
{
time_t tv_sec;
long int tv_nsec;
};
納秒
ftime() 函式返回從 1970 年 1 月 1 日 0 點以來,到現在的時間。用timeb資料結構表示。
已經過時, 被 time() 替代。儘量不使用。
struct timeb {
time_t time;
unsigned short
millitm;
short timezone;
short dstflag;
};
毫秒

GUN/Linux 提供了三個標準的 API 用來獲取當前時間,time()/gettimeofday()/clock_gettime(),它們的區別僅在於獲取的時間精度不同,您可以根據需要選取合適的呼叫。ftime() 是老的一些系統中的時間呼叫,很多 Linux 版本雖然支援它,但僅僅是為了向前相容性,新開發的軟體不建議使用 ftime() 來獲得當前時間。

時間顯示和轉換

目前我們得到的時間是一個數字,無論精度如何,它代表的僅是一個差值。比如精度為秒的 time() 函式,返回一個 time_t 型別的整數。假設當前時間為 2011 年 12 月 7 日下午 20 點 29 分 51 秒,那麼 time_t 的值為:1323318591。即距離 1970 年 1 月 1 日零點,我們已經過去了 1323318591 秒。(這裡的 1970 年 1 月 1 日零點是格林威治時間,而不是北京時間。)我們下面討論的時間如果不特別說明都是格林威治時間,也叫 GMT 時間,或者 UTC 時間。

字串“1323318591 秒”對於多數人都沒有太大的意義,我們更願意看到“2011 年 12 月 7 日”這樣的顯示。因此當我們得到秒,毫秒,甚至納秒錶示的當前時間之後,往往需要將這些數字轉換為人們所熟悉的時間表示方法。

由於國家,習慣和時區的不同,時間的表示方法並沒有一個統一的格式。為了滿足各種時間顯示的需求,標準 C 庫提供了許多時間格式轉換的函式。這些函式的數量眾多,容易讓人迷惑,記住它們的用法十分不易。在這裡我借用 Michael Kerrisk 在《Linux Programming Interface》一書中的插圖,來對這些標準 C 函式進行一個總體的概覽。

圖 1. 各種時間顯示格式轉換函式關係圖

從上圖可以看到,time()/gettimeofday() 從核心得到當前時間之後,該當前時間值可以被兩大類函式轉換為更加容易閱讀的顯示格式:

  • 固定格式轉換
  • 使用者指定格式轉換函式。

固定格式轉換

用 ctime() 函式轉換出來的時間格式是系統固定的,呼叫者無法改動,因此被稱為固定格式轉換。如果您對日期格式沒有特殊的要求,那麼用它基本上就可以了,簡單,不用記憶很多的引數。

使用者指定格式轉換

典型的 ctime() 格式如下:

Wed Dec 7 20:45:43 PST 2011

有些人覺得這個格式太長,類似 Wed,星期三這樣的資訊很多情況下都沒有啥用途。人們可能更喜歡其他格式:比如2011-12-07 20:45。在這種情況下,就需要進行時間顯示格式轉換。做法為:先把從核心得到的時間值轉換為 struct tm 型別的值,然後呼叫 strftime() 等函式來輸出自定義的時間格式字串。

下面我列舉一些例項,以便讀者更清晰地理解眾多的時間轉換函式的用法。

各標準 C 時間轉換函式的解釋和舉例

char *ctime(const time_t *clock);

使用函式 ctime 將秒數轉化為字串. 這個函式的返回型別是固定的:一個可能值為”Thu Dec 7 14:58:59 2000”。這個字串的長度和顯示格式是固定的。

清單 1,time 的使用
#include <time.h>
int main ()
{
 time_t time_raw_format;
 time ( &time_raw_format ); //獲取當前時間
 printf (" time is [%d]\n", time_raw_format);
 //用 ctime 將時間轉換為字串輸出
 printf ( "The current local time: %s", ctime(&time_raw_format));
 return 0;
}

自定義格式轉換

為了更靈活的顯示,需要把型別 time_t 轉換為 tm 資料結構。tm 資料結構將時間分別儲存到代表年,月,日,時,分,秒等不同的變數中。不再是一個令人費解的 64 位整數了。這種資料結構是各種自定義格式轉換函式所需要的輸入形式。

清單 2,資料結構 tm
struct tm {
int tm_sec; /* Seconds (0-60) */
int tm_min; /* Minutes (0-59) */
int tm_hour; /* Hours (0-23) */
int tm_mday; /* Day of the month (1-31) */
int tm_mon; /* Month (0-11) */
int tm_year; /* Year since 1900 */
int tm_wday; /* Day of the week (Sunday = 0)*/
int tm_yday; /* Day in the year (0-365; 1 Jan = 0)*/
int tm_isdst; /* Daylight saving time flag
 > 0: DST is in effect;
 = 0: DST is not effect;
 < 0: DST information not available */
};

可以使用 gmtime() 和 localtime() 把 time_t 轉換為 tm 資料格式,其中 gmtime() 把時間轉換為格林威治時間;localtime 則轉換為當地時間。

清單 3,時間轉換函式定義
#include <time.h>
struct tm *gmtime(const time_t *timep);
struct tm *localtime(const time_t *timep);

使用 tm 來表示時間,您就可以呼叫 asctime() 和 strftime() 將時間轉換為字串了。asctime() 的輸出格式固定,和 ctime() 相同。strftime() 則類似我們最熟悉的 printf() 函式,您可以通過輸入引數自定義時間的輸出格式。

size_t strftime(char *outstr, size_t maxsize, const char *format,
 const struct tm *timeptr);
清單 4,時間顯示轉換
int main ()
{
	time_t time_raw_format;
	struct tm * time_struct;
	char buf [100];
	time ( &time_raw_format );
	time_struct = localtime ( &time_raw_format );
	strftime (buf,100,"It is now: %I:%M%p.",time_struct);
	puts (buf);
	return 0;
}

該例子程式的輸出結果如下:

It is now: 02:45PM.

從以上的例子可以看到,利用從 time() 得到的時間值,可以呼叫各種轉換函式將其轉換成更方便人們閱讀的形式。

此外從前面的總結中我們也瞭解到,還有兩個 C 函式可以獲得當前時間,gettimeofday() 以及 clock_gettime(),它們分別返回 struct timeval 或者 timespec 代表的高精度的時間值。在目前的 GLibC 中,還沒有直接把 struct timeval/timespec 轉換為 struct tm 的函式。一般的做法是將 timeval 中的 tv_sec 轉換為 tm,使用上面所述的方法轉換為字串,最後在顯示的時候追加上 tv_usec,比如下面的例子程式碼:

清單 5,更多時間顯示轉換
struct timeval tv;
time_t nowtime; 
struct tm *nowtm; 
char tmbuf[64], buf[64]; 
gettimeofday(&tv, NULL); //獲取當前時間到 tv
nowtime = tv.tv_sec; //nowtime 儲存了秒級的時間值
nowtm = localtime(&nowtime); //轉換為 tm 資料結構
//用 strftime 函式將 tv 轉換為字串,但 strftime 函式只能達到秒級精度
strftime(tmbuf, sizeof tmbuf, "%Y-%m-%d %H:%M:%S", nowtm);
//將毫秒值追加到 strftime 轉換的字串末尾 
snprintf(buf, sizeof buf, "%s.%06d", tmbuf, tv.tv_usec);

時間的測量

有時候我們要計算某段程式執行的時間,比如需要對演算法進行時間分析。基本的實現思路為在被測試程式碼的開始和結束的地方獲取當時時間,相減後得到相對值,即所需要的統計時間。為了實現高精度的時間測量,必須使用高精度的時間獲取方式,一般有兩種方法:

  • 系統呼叫 gettimeofday
  • 彙編指令 RDTSC。

gettimeofday

可以使用 gettimeofday() 函式進行時間測量,其精度在 us 級別,可以用來做一般的時間分析。

gettimeofday() 將時間儲存在結構 tv 之中。gettimeofday() 的第二個引數代表時區,在 Linux 中已經廢棄不用,只能用 NULL 傳入。一個典型的例子程式如下:

清單 6,gettimeofday 例子程式
void function() 
{ 
 unsigned int i,j; 
 double y; 
 for(i=0;i<1000;i++) 
 for(j=0;j<1000;j++) 
 y=sin((double)i); //耗時操作
} 

main() 
{ 
 struct timeval tpstart,tpend; 
 float timeuse; 

 gettimeofday(&tpstart,NULL); //記錄開始時間戳
 function(); 
 gettimeofday(&tpend,NULL); //記錄結束時間戳
 timeuse = 1000000*(tpend.tv_sec-tpstart.tv_sec)+ 
tpend.tv_usec-tpstart.tv_usec; //計算差值
 timeuse /= 1000000; 
 printf("Used Time:%f\n",timeuse); 
 exit(0); 
}

這個程式輸出函式的執行時間,我們可以使用這個來進行系統性能的測試,或者是函式演算法的效率分析。在我個人機器上的輸出結果是:Used Time:0.556070

RDTSC

gettimeofday() 是一個系統呼叫,在某些場合下頻繁呼叫它是不合適的。比如效能要求很高的程式碼段內。因為 gettimeofday() 需要使用者態/核心態切換,開銷較大。Intel X86 處理器提供了 TSC 硬體,並且可以用非特權指令 rdtsc 來讀取該硬體的時間值,這就避免了過度的核心使用者態切換。

如何使用 RDTSC

參考下面的例子程式碼,採用 GCC 的彙編擴充套件,定義 rdtsc 的函式,它返回當前時間戳。

#define rdtsc(low,high) __asm__ \
 __volatile__("rdtsc" : "=a" (low), "=d" (high))

在 C 程式碼中使用 rdtsc 十分簡單。比如:

清單 7,RDTSC 例子程式
unsigned long long get_cycles()
{
	unsigned low, high;
	unsigned long long val;
	rdtsc(low,high);
	val = high;
val = (val << 32) | low; //將 low 和 high 合成一個 64 位值
	return val;
}

double get_cpu_mhz(void)
{
	FILE* f;
	char buf[256];
	double mhz = 0.0;

f = fopen("/proc/cpuinfo","r"); //開啟 proc/cpuinfo 檔案
	if (!f)
		return 0.0;
	while(fgets(buf, sizeof(buf), f)) {
		double m;
		int rc;
rc = sscanf(buf, "cpu MHz : %lf", &m); //讀取 cpu MHz
		if (mhz == 0.0) {
			mhz = m;
			break;
		}
	}
	fclose(f);
return mhz; //返回 HZ 值
}

int main()
{
	double mhz;
	mhz = get_cpu_mhz();
	cycles_t c1, c2;

	for(;;)
	{
		c1 = get_cycles(); 
		sleep(1);
		c2 = get_cycles();
 //c2 和 c1 的差值應該為 1000000us,即 1 秒
		printf("1 sec = %g usec\n", (c2 - c1) / mhz); 
	}
}

函式 get_cycles 將返回 64 位整數,代表當前時間,單位是 CPU 的 cycle 數。函式 get_cpu_mhz 獲得當前 CPU 的工作頻率。用兩個 CPU cycle 的差值除以 CPU 頻率,就是微妙。

但 RDTSC 只能在 IA 系列處理器上使用。而且由於處理器的亂序執行,RDTSC 有些情況下並不準確,在 SMP 下使用 RDTSC 也有一定的問題。但這些問題只有在需要極高時間精度的情況下才會出現,對於一般的時間測量要求,採用 RDTSC 是一個可以考慮的選擇。

計時器的使用

有時我們需要定時完成一些任務。簡單的方法是使用 while 迴圈加 sleep。比如每隔 1 分鐘檢查連結情況的 heartbeat 任務等。

清單 8,sleep 加迴圈
while(condtion)
{
 //do something
 sleep(interval);
}

這可以滿足很多程式的定時需要,但假如您不希望程式“偷懶”,即上例中 sleep 的時候您還是希望程式做些有用的工作,那麼使用定時器是通常的選擇。Linux 系統上最常用的定時器是 setitmer 計時器。

setitimer

Linux 為每一個程序提供了 3 個 setitimer 間隔計時器:

  • ITIMER_REAL:減少實際時間,到期的時候發出 SIGALRM 訊號。
  • ITIMER_VIRTUAL:減少有效時間 (程序執行的時間),產生 SIGVTALRM 訊號。
  • ITIMER_PROF:減少程序的有效時間和系統時間 (為程序排程用的時間)。這個經常和上面一個使用用來計算系統核心時間和使用者時間。產生 SIGPROF 訊號。

所謂 REAL 時間,即我們人類自然感受的時間,英文計算機文件中也經常使用 wall-clock 這個術語。說白了就是我們通常所說的時間,比如現在是下午 5 點 10 分,那麼一分鐘的 REAL 時間之後就是下午 5 點 11 分。

VIRTUAL 時間是程序執行的時間,Linux 是一個多使用者多工系統,在過去的 1 分鐘內,指定程序實際在 CPU 上的執行時間往往並沒有 1 分鐘,因為其他程序會被 Linux 排程執行,在那些時間內,雖然自然時間在流逝,但指定程序並沒有真正的執行。VIRTUAL 時間就是指定程序真正的有效執行時間。比如 5 點 10 分開始的 1 分鐘內,程序 P1 被 Linux 排程並佔用 CPU 的執行時間為 30 秒,那麼 VIRTUAL 時間對於程序 P1 來講就是 30 秒。此時自然時間已經到了 5 點 11 分,但從程序 P1 的眼中看來,時間只過了 30 秒。

PROF 時間比較獨特,對程序 P1 來說從 5 點 10 分開始的 1 分鐘內,雖然自己的執行時間為 30 秒,但實際上還有 10 秒鐘核心是在執行 P1 發起的系統呼叫,那麼這 10 秒鐘也被加入到 PROF 時間。這種時間定義主要用於全面衡量程序的效能,因為在統計程式效能的時候,10 秒的系統呼叫時間也應該算到 P1 的頭上。這也許就是 PROF 這個名字的來歷吧。

使用 setitimer Timer 需要了解下面這些介面 API:

int getitimer(int which,struct itimerval *value); 
int setitimer(int which,struct itimerval *newval, 
struct itimerval *oldval);

itimerval 的定義如下:

struct itimerval { 
struct timeval it_interval; 
struct timeval it_value; 
}

getitimer 函式得到間隔計時器的時間值,儲存在 value 中。

setitimer 函式設定間隔計時器的時間值為 newval. 並將舊值儲存在 oldval 中;which 表示使用三個計時器中的哪一個。

itimerval 結構中的 it_value 是第一次呼叫後觸發定時器的時間,當這個值遞減為 0 時,系統會向程序發出相應的訊號。此後將以 it_internval 為週期定時觸發定時器。

給出一個具體的例子:

清單 9,setitmer 例子
void print_info(int signo) 
{ 
 printf(“timer fired\n”); //簡單的列印,表示 timer 到期
} 

void init_sigaction(void) 
{ 
 struct sigaction act; 
 act.sa_handler= print_info; 
 act.sa_flags=0; 
 sigemptyset(&act.sa_mask); 
 sigaction(SIGPROF,&act,NULL); //設定訊號 SIGPROF 的處理函式為 print_info
} 

void init_time() 
{ 
 struct itimerval value; 
 value.it_value.tv_sec=2; 
 value.it_value.tv_usec=0; 
 value.it_interval=value.it_value; 
 setitimer(ITIMER_PROF,&value,NULL); //初始化 timer,到期傳送 SIGPROF 訊號
} 

int main() 
{ 
 len=strlen(prompt); 
 init_sigaction(); 
 init_time(); 
 while(1); 
 exit(0); 
}

這個程式使用 PROF 時間,每經過兩秒 PROF 時間之後就會列印一下 timer fired 字串。

需要指出:setitimer 計時器的精度為 ms,即 1000 分之 1 秒,足以滿足絕大多數應用程式的需要。但多媒體等應用可能需要更高精度的定時,那麼就需要考慮使用下一類定時器:POSIX Timer。

POSIX Timer

間隔定時器 setitimer 有一些重要的缺點,POSIX Timer 對 setitimer 進行了增強,克服了 setitimer 的諸多問題:

首先,一個程序同一時刻只能有一個 timer。假如應用需要同時維護多個 Interval 不同的計時器,必須自己寫程式碼來維護。這非常不方便。使用 POSIX Timer,一個程序可以建立任意多個 Timer。

setitmer 計時器時間到達時,只能使用訊號方式通知使用 timer 的程序,而 POSIX timer 可以有多種通知方式,比如訊號,或者啟動執行緒。

使用 setitimer 時,通知訊號的類別不能改變:SIGALARM,SIGPROF 等,而這些都是傳統訊號,而不是實時訊號,因此有 timer overrun 的問題;而 POSIX Timer 則可以使用實時訊號。

setimer 的精度是 ms,POSIX Timer 是針對有實時要求的應用所設計的,介面支援 ns 級別的時鐘精度。

表 2. POSIX Timer 函式
函式名 功能描述
timer_create 建立一個新的 Timer;並且指定定時器到時通知機制
timer_delete 刪除一個 Timer
timer_gettime Get the time remaining on a POSIX.1b interval timer
timer_settime 開始或者停止某個定時器。
timer_getoverrun 獲取丟失的定時通知個數。

使用 Posix Timer 的基本流程很簡單,首先建立一個 Timer。建立的時候可以指定該 Timer 的一些特性,比如 clock ID。

clock ID 即 Timer 的種類,可以為下表中的任意一種:

表 3. POSIX Timer clock ID
Clock ID 描述
CLOCK_REALTIME Settable system-wide real-time clock;
CLOCK_MONOTONIC Nonsettable monotonic clock
CLOCK_PROCESS_CPUTIME_ID Per-process CPU-time clock
CLOCK_THREAD_CPUTIME_ID Per-thread CPU-time clock

CLOCK_REALTIME 時間是系統儲存的時間,即可以由 date 命令顯示的時間,該時間可以重新設定。比如當前時間為上午 10 點 10 分,Timer 打算在 10 分鐘後到時。假如 5 分鐘後,我用 date 命令修改當前時間為 10 點 10 分,那麼 Timer 還會再等十分鐘到期,因此實際上 Timer 等待了 15 分鐘。假如您希望無論任何人如何修改系統時間,Timer 都嚴格按照 10 分鐘的週期進行觸發,那麼就可以使用 CLOCK_MONOTONIC。

CLOCK_PROCESS_CPUTIME_ID 的含義與 setitimer 的 ITIMER_VIRTUAL 類似。計時器只記錄當前程序所實際花費的時間;比如還是上面的例子,假設系統非常繁忙,當前程序只能獲得 50%的 CPU 時間,為了讓程序真正地執行 10 分鐘,應該到 10 點 30 分才允許 Timer 到期。

CLOCK_THREAD_CPUTIME_ID 以執行緒為計時實體,當前程序中的某個執行緒真正地運行了一定時間才觸發 Timer。

設定到期通知方式

timer_create 的第二個引數 struct sigevent 用來設定定時器到時時的通知方式。該資料結構如下:

清單 10,結構 sigevent
 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 表示通知方式,有如下幾種:

表 3. POSIX Timer 到期通知方式
通知方式 描述
SIGEV_NONE 定時器到期時不產生通知。。。
SIGEV_SIGNAL 定時器到期時將給程序投遞一個訊號,sigev_signo 可以用來指定使用什麼訊號。
SIGEV_THREAD 定時器到期時將啟動新的執行緒進行需要的處理
SIGEV_THREAD_ID(僅針對 Linux) 定時器到期時將向指定執行緒傳送訊號。

如果採用 SIGEV_NONE 方式,使用者必須呼叫timer_gettime 函式主動讀取定時器已經走過的時間。類似輪詢。

如果採用 SIGEV_SIGNAL 方式,使用者可以選擇使用什麼訊號,用 sigev_signo 表示訊號值,比如 SIG_ALARM。

如果使用 SIGEV_THREAD 方式,則需要設定 sigev_notify_function,當 Timer 到期時,將使用該函式作為入口啟動一個執行緒來處理訊號;sigev_value 儲存了傳入 sigev_notify_function 的引數。sigev_notify_attributes 如果非空,則應該是一個指向 pthread_attr_t 的指標,用來設定執行緒的屬性(比如 stack 大小,detach 狀態等)。

SIGEV_THREAD_ID 通常和 SIGEV_SIGNAL 聯合使用,這樣當 Timer 到期時,系統會向由 sigev_notify_thread_id 指定的執行緒傳送訊號,否則可能程序中的任意執行緒都可能收到該訊號。這個選項是 Linux 對 POSIX 標準的擴充套件,目前主要是 GLibc 在實現 SIGEV_THREAD 的時候使用到,應用程式很少會需要用到這種模式。

啟動定時器

建立 Timer 之後,便可以呼叫 timer_settime() 函式指定定時器的時間間隔,並啟動該定時器了。

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

第一次看到 timer_settime 的引數列表或許會令人覺得費解。先來看看 new_value 和 old_value,它們都是 struct itimerspec 資料結構。

struct itimerspec
{
 struct timespec it_interval; //定時器週期值
 struct timespec it_value; //定時器到期值
};

啟動和停止 Timer 都可以通過設定 new_value 來實現:

new_value->it_interval 為定時器的週期值,比如 1 秒,表示定時器每隔 1 秒到期;

new_value->it_value 如果大於 0,表示啟動定時器,Timer 將在 it_value 這麼長的時間過去後到期,此後每隔 it_interval 便到期一次。如果 it_value 為 0,表示停止該 Timer。

有些時候,應用程式會先啟動用一個時間間隔啟動定時器,隨後又修改該定時器的時間間隔,這都可以通過修改 new_value 來實現;假如應用程式在修改了時間間隔之後希望瞭解之前的時間間隔設定,則傳入一個非 NULL 的 old_value 指標,這樣在 timer_settime() 呼叫返回時,old_value 就儲存了上一次 Timer 的時間間隔設定。多數情況下我們並不需要這樣,便可以簡單地將 old_value 設定為 NULL,忽略它。

下面給出一個使用 posix timer 的例子程式。最傳統的例子就是建立通知方式為 SIGEV_SIGNAL 的 Timer。這樣當定時器到期時,將產生訊號通知,主程式需要定義自己的訊號處理函式,來處理訊號到期事件。這種例子比比皆是,我打算在這裡寫一個採用通知方式為 SIGEV_THREAD 的例子。該例子程式從 main 函式開始主執行緒,在開始的時候打印出主執行緒的程序 ID 和執行緒 ID。

清單 11,列印 TID
 pid_t tid = (pid_t) syscall (SYS_gettid);
 printf("start program in PID:[%d]TID:[%d]\n",getpid(),tid);

獲得 ThreadID 的系統呼叫尚未被 GLibC 標準化,因此這裡直接呼叫 syscall。

然後,主執行緒初始化建立 Timer 所需要的資料結構:

清單 12,設定通知方式
 se.sigev_notify = SIGEV_THREAD;
 se.sigev_value.sival_ptr = &timer_id;
 se.sigev_notify_function = timer_thread;
 se.sigev_notify_attributes = NULL;
 status = timer_create(CLOCK_REALTIME, &se, &timer_id);

這裡將通知方式設為 SIGEV_THREAD,timer_thread 為執行緒入口函式。

然後主執行緒設定定時器間隔,並啟動 Timer:

清單 13,啟動 Timer
 ts.it_value.tv_sec = 5;
 ts.it_value.tv_nsec = 0;
 ts.it_interval.tv_sec = 5;
 ts.it_interval.tv_nsec = 0;
 status = timer_settime(timer_id, 0, &ts, 0);

此後主執行緒進入一個迴圈,在迴圈中等待執行緒條件變數:

清單 14,主程式中的迴圈
 while (counter < 5) {
 status = pthread_cond_wait (&cond, &mutex);
}

條件變數 cond 將在 timer_thread() 處理函式中觸發,這樣每 5 秒鐘,定時器將呼叫 timer_thread() 處理函式,並喚醒主執行緒等待的條件變數一次。5 次之後測試程式退出。

現在我們看看 timer_thread() 函式:

清單 15,timer_thread 函式
void timer_thread (void *arg)
{
 status = pthread_mutex_lock (&mutex);
 if (++counter >= 5) {
 status = pthread_cond_signal (&cond);
 }
 status = pthread_mutex_unlock (&mutex);
 pid_t tid = (pid_t) syscall (SYS_gettid);
 printf ("Timer %d in PID:[%d]TID:[%d]\n", counter,getpid(),tid);
}

在整個程式中我們都沒有使用訊號,定時器到期時,將啟動新的執行緒執行 timer_thread。因此在該函式中,我們還列印了當前的執行緒號以便可以看出它們確實在不同執行緒中執行。

這裡是執行該程式的一個輸出:

-bash-3.2$ gcc threadtimer.c -lrt -lpthread -o test
-bash-3.2$ ./test
start program in PID:[21483]TID:[21483]
Timer 1 in PID:[21483]TID:[21498]
Timer 2 in PID:[21483]TID:[21510]
Timer 3 in PID:[21483]TID:[21534]

可以看到每次 Timer 都執行在不同的執行緒中。

小結

至此,希望我已經講述了 Linux 系統提供的大多數關於時間的程式設計方法。使用這些方法我們可以:

  • 獲得當前時間,並轉換為合適的顯示方式;
  • 衡量程式執行經過的時間;
  • 使用定時器完成周期性的任務;

另外不知道您是否和我一樣,對於 Linux 系統如何實現這些機制十分好奇。計算機畢竟是一個機器,底層硬體提供了怎樣的功能,作業系統和 C 庫如何協同工作才可以提供這些一絲不苟的,優美的方法呢?我將在後續的部分試圖探討這個話題。