linux時間函數gettimeofday解析

我們在程序中會頻繁地取當前時間,例如處理一個http請求時,兩次調用gettimeofday取差值計算出處理該請求消耗了多少秒。這樣的調用無處不在,所以我們有必要詳細瞭解下,gettimeofday這個函數做了些什麼?內核1ms一次的時鐘中斷處理真的可以支持tv_usec字段達到微秒精度嗎?它的調用成本在i386/x86_64體系架構上代價一樣嗎?如果在系統繁忙時,頻繁的調用它有問題嗎?

gettimeofday是C庫提供的函數(不是系統調用),它封裝了內核裏的sys_gettimeofday系統調用,就是說,歸根到底是系統調用。

但是,內核對於x86_64體系結構下,除了普通的系統調用外,還提供了sysenter和vsyscall方式來獲取內核態的數據。目前我們使用的操作系統大都是x86_64體系的,如果我們用strace命令跟蹤,就會發現gettimeofday命令實際上沒有執行系統調用(i386體系會有),這是因爲:x86_64體系上,使用vsyscall實現了gettimeofday這個系統調用。具體就是,創建了一個共享的內存頁面,它是在內核態的,它的數據由內核來維護,但是,用戶態也有權限訪問這個內核頁面,由此,不通過中斷gettimeofday也就拿到了系統時間。

接下來,我來詳細回答以上4個問題。

一、gettimeofday做了些什麼?

它把內核保存的牆上時間和jiffies綜合處理後返回給用戶。解釋下牆上時間和jiffies是什麼:1、牆上時間就是實際時間(1970/1/1號以來的時間),它是由我們主板電池供電的(裝過PC機的同學都瞭解)RTC單元存儲的,這樣即使機器斷電了時間也不用重設。當操作系統啓動時,會用這個RTC來初始化牆上時間,接着,內核會在一定精度內根據jiffies維護這個牆上時間。2、jiffies就是操作系統啓動後經過的時間,它的單位是節拍數。有些體系架構,1個節拍數是10ms,但我們常用的x86體系下,1個節拍數是1ms。也就是說,jiffies這個全局變量存儲了操作系統啓動以來共經歷了多少毫秒。我們來看看gettimeofday是如何做的。首先它調用了sys_gettimeofday系統調用。

[cpp]
  1. asmlinkage long sys_gettimeofday(struct timeval __user *tv, struct timezone __user *tz)  
  2. {  
  3.     if (likely(tv != NULL)) {  
  4.         struct timeval ktv;  
  5.         do_gettimeofday(&ktv);  
  6.         if (copy_to_user(tv, &ktv, sizeof(ktv)))  
  7.             return -EFAULT;  
  8.     }  
  9.     if (unlikely(tz != NULL)) {  
  10.         if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))  
  11.             return -EFAULT;  
  12.     }  
  13.     return 0;  
  14. }  
大家看到,它調用do_gettimeofday函數取到當前時間存儲到局部變量ktv上,www.linuxidc.com 然後調用copy_to_user把結果複製到用戶空間。每個體系都有自己的實現,我這裏就簡單列下x86_64體系下do_gettimeofday的實現:

[cpp]
  1. void do_gettimeofday(struct timeval *tv)  
  2. {  
  3.     unsigned long seq, t;  
  4.     unsigned int sec, usec;  
  5.   
  6.     do {  
  7.         seq = read_seqbegin(&xtime_lock);  
  8.   
  9.         sec = xtime.tv_sec;  
  10.         usec = xtime.tv_nsec / 1000;  
  11.   
  12.         /* i386 does some correction here to keep the clock  
  13.            monotonous even when ntpd is fixing drift. 
  14.            But they didn't work for me, there is a non monotonic 
  15.            clock anyways with ntp. 
  16.            I dropped all corrections now until a real solution can 
  17.            be found. Note when you fix it here you need to do the same 
  18.            in arch/x86_64/kernel/vsyscall.c and export all needed 
  19.            variables in vmlinux.lds. -AK */   
  20.   
  21.         t = (jiffies - wall_jiffies) * (1000000L / HZ) +  
  22.             do_gettimeoffset();  
  23.         usec += t;  
  24.   
  25.     } while (read_seqretry(&xtime_lock, seq));  
  26.   
  27.     tv->tv_sec = sec + usec / 1000000;  
  28.     tv->tv_usec = usec % 1000000;  
  29. }  

大家看到,只是把xtime加以jiffies修正後返回給用戶而已。而xtime變量和jiffies的維護更新頻率,就決定了時間精度,上面說了,每10或者1ms才處理一次時鐘中斷,難道精度只到1ms嗎?繼續往下看。

二、內核1ms一次的時鐘中斷真的可以支持tv_usec字段達到微秒精度嗎?
可以,因爲這個時間還會由High Precision Event Timer來維護,這個模塊會處理微秒級的中斷,並更新xtime和jiffies變量。我們看下x86_64體系結構下的維護代碼:

[cpp]
  1. static struct irqaction irq0 = {  
  2.     timer_interrupt, SA_INTERRUPT, CPU_MASK_NONE, "timer", NULL, NULL  
  3. };  

這個timer_interrupt函數會處理HPET時間中斷,來更新xtime變量。

三、它的調用成本在所有的操作系統上代價一樣嗎?如果在系統繁忙時,1毫秒內調用多次有問題嗎?

最上面已經說了,對於x86_64系統來說,這是個虛擬系統調用vsyscall!所以,這裏它不用發送中斷!速度很快,成本低,調用一次的成本大概不到一微秒!

對於i386體系來說,這就是系統調用了!最簡單的系統調用都有無法避免的成本:陷入內核態。當我們調用gettimeofday時,將會向內核發送軟中斷,然後將陷入內核態,這時內核至少要做下列事:處理軟中斷、保存所有寄存器值、從用戶態複製函數參數到內核態、執行、將結果複製到用戶態。這些成本至少在1微秒以上!


四、關於jiffies值得一提的兩點

先看看它的定義:

[cpp]
  1. volatile unsigned long __jiffies;  
只談兩點。

1、它用了一個C語言裏比較罕見的關鍵字volatile,這個關鍵字用於解決併發問題。C語言編譯器很喜歡做優化的,它不清楚某個變量可能會被併發的修改,例如上面的jiffies變量首先是0,如果首先一個CPU修改了它的值爲1,緊接着另一個CPU在讀它的值,例如 __jiffies = 0; while (__jiffies == 1),那麼在內核的C代碼中,如果不加volatile字段,那麼第二個CPU裏的循環體可能不會被執行到,因爲C編譯器在對代碼做優化時,生成的彙編代碼不一定每次都會去讀內存!它會根據代碼把變量__jiffies設爲0,並一直使用下去!www.linuxidc.com 而加了volatile字段後,就會要求編譯器,每次使用到__jiffies時,都要到內存裏真實的讀取這個值。


2、它的類型是unsigned long,在32位系統中,最大值也只有43億不到,從系統啓動後49天就到達最大值了,之後就會清0重新開始。那麼jiffies達到最大值時的迴轉問題是怎麼解決的呢?或者換句話說,我們需要保證當jiffies迴轉爲一個小的正數時,例如1,要比幾十秒毫秒前的大正數大,例如4294967290,要達到jiffies(1)>jiffies(4294967290)這種效果。

內核是通過定義了兩個宏來解決的:

[cpp]
  1. #define time_after(a,b)     \   
  2.     (typecheck(unsigned long, a) && \  
  3.      typecheck(unsigned long, b) && \  
  4.      ((long)(b) - (long)(a) < 0))  
  5. #define time_before(a,b)    time_after(b,a)  
很巧妙的設計!僅僅把unsigned long轉爲long類型後相減比較,就達到了jiffies(1)>jiffies(4294967290)效果,簡單的解決了jiffies的迴轉問題。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章