Linux時間管理之hardware
一直以來對Linux下的時間管理知之不詳,GFree_wind在微博發(fā)起過幾次Linux下時鐘的討論,和Godbach這些大牛比,我完全插不上話,因為不懂。近來閑暇時間研究了下Linux下的時間管理,分享出來,請大家指正。
從我們大白話的角度想,時間管理其實分成兩部分,就像我們小時候?qū)W習物理的時候物理老師不斷強調(diào)時間和時刻的區(qū)別。一個是時刻,比如現(xiàn)在是20:44:37秒,指的是時刻,我們手機上看時間,指的也是時刻。另一塊是時間,比如說,我每天工作八小時,再比如說,半小時之后,我要出門了,結(jié)束時間指向的是未來,但是仍然是一段時間。OK。無論是時刻還是時望間,都是需要硬件支持的,你手里只有一塊最小刻度只有1秒的手表,就不要指用這塊手表給百米大賽度量成績了,何哉,硬件太挫。Linux也是如此,之所以Linux啟動之后,可以精確的計時,那是因為Linux的下面有相應的硬件為依托。
RTC
RTC,real time clock,實時時鐘和其他的硬件是不同的,RTC吐出來的是時刻,而其他硬件時鐘吐出來的是時間。也就是說,RTC能告訴我們,當前是2013年9月12日,21:49:38,但是其他的硬件如TSC,PIT,HPET只能告訴我們,我應該走過了XX個cycle,按照我的頻率,已經(jīng)過去了10分鐘了。
為啥RTC這么牛X,可以告訴我們當前時刻,哪怕用戶關(guān)了機?以X86為例,RTC是主板上的一塊CMOS芯片,哪怕你的Linux關(guān)了機,她也可以依賴主板上的電池維持時鐘的準確。當然了,在Linux下,RTC存儲的是UTC時間,而不會考慮timezone。
所以,Linux啟動的時候,一定會拜訪RTC來獲得當前的時刻值,盡管精度不高(精確到秒)。When and How?
首先回答When。Linux啟動的時候,start_kernel有四大time相關(guān)的函數(shù)調(diào)用:
- init_timers();
- hrtimers_init();
- timekeeping_init()
- time_init();
從RTC中讀取當前的UTC時間是timekeeping_init中做的事情,調(diào)用路徑如下:
timekeeping_init
|___________read_persistent_clock (arch/x86/kernel/rtc.c) |_____x86_platform.get_wallclock() |_____mach_get_cmos_time (arch/x86/kernel/x86_init.c) |
- /************arch/x86/kernel/rtc.c*****************/
- void read_persistent_clock(struct timespec *ts)
- {
- unsigned long retval;
- retval = x86_platform.get_wallclock();
- ts->tv_sec = retval;
- ts->tv_nsec = 0;
- }
- /*****************arch/x86/kernel/x86_init.c ****************/
- struct x86_platform_ops x86_platform = {
- .calibrate_tsc = native_calibrate_tsc,
- .wallclock_init = wallclock_init_noop,
- .get_wallclock = mach_get_cmos_time,
- .set_wallclock = mach_set_rtc_mmss,
- .iommu_shutdown = iommu_shutdown_noop,
- .is_untracked_pat_range = is_ISA_range,
- .nmi_init = default_nmi_init,
- .get_nmi_reason = default_get_nmi_reason,
- .i8042_detect = default_i8042_detect,
- .save_sched_clock_state = tsc_save_sched_clock_state,
- .restore_sched_clock_state = tsc_restore_sched_clock_state,
- }
對于我們而言,我們要讀的function是mach_get_cmos_time
- unsigned long mach_get_cmos_time(void)
- {
- unsigned int status, year, mon, day, hour, min, sec, century = 0;
- unsigned long flags;
- spin_lock_irqsave(&rtc_lock, flags);
- /*
- * If UIP is clear, then we have >= 244 microseconds before
- * RTC registers will be updated. Spec sheet says that this
- * is the reliable way to read RTC - registers. If UIP is set
- * then the register access might be invalid.
- */
- while ((CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP))
- cpu_relax();
- sec = CMOS_READ(RTC_SECONDS);
- min = CMOS_READ(RTC_MINUTES);
- hour = CMOS_READ(RTC_HOURS);
- day = CMOS_READ(RTC_DAY_OF_MONTH);
- mon = CMOS_READ(RTC_MONTH);
- year = CMOS_READ(RTC_YEAR);
- #ifdef CONFIG_ACPI
- if (acpi_gbl_FADT.header.revision >= FADT2_REVISION_ID &&
- acpi_gbl_FADT.century)
- century = CMOS_READ(acpi_gbl_FADT.century);
- #endif
- status = CMOS_READ(RTC_CONTROL);
- WARN_ON_ONCE(RTC_ALWAYS_BCD && (status & RTC_DM_BINARY));
- spin_unlock_irqrestore(&rtc_lock, flags);
- if (RTC_ALWAYS_BCD || !(status & RTC_DM_BINARY)) {
- sec = bcd2bin(sec);
- min = bcd2bin(min);
- hour = bcd2bin(hour);
- day = bcd2bin(day);
- mon = bcd2bin(mon);
- year = bcd2bin(year);
- }
- if (century) {
- century = bcd2bin(century);
- year += century * 100;
- printk(KERN_INFO "Extended CMOS year: %d\n", century * 100);
- } else
- year += CMOS_YEARS_OFFS;
- return mktime(year, mon, day, hour, min, sec);
- }
這一段代碼已經(jīng)牽扯到了硬件相關(guān)的編程,我們其實并不關(guān)心驅(qū)動,對于硬件比較感興趣的筒子,可以訪問http://xenyinzen.wikidot.com/reship:080225-2
獲得更多更詳細的信息。mktime是將年月日時分秒組裝成1970年1月1日00:00:00這個UNIX基準時間以來的秒數(shù)。我們在Linux下可以通過一下方式獲得這個值:
- root@manu:/sys/class/rtc/rtc0# date +%s ;cat /sys/class/rtc/rtc0/since_epoch
- 1379081060
- 1379081060
既然Linux上電的時候,可以從RTC中讀出當前的時間,我們也可以設置時間,并且寫入到RTC。用戶層也可以操作RTC硬件時鐘,通過ioctl。下面給出一個樣例:
- #include <stdio.h>
- #include <stdlib.h>
- #include <linux/rtc.h>
- #include <fcntl.h>
- #include <sys/ioctl.h>
- int main(int argc,char *argv[])
- {
- int retval,fd;
- struct rtc_time rtc_tm;
- fd=open("/dev/rtc",O_RDONLY);
- if(fd==-1)
- {
- perror("error open /dev/rtc");
- return -1;
- }
- retval=ioctl(fd,RTC_RD_TIME,&rtc_tm);
- if(retval==-1)
- {
- perror("error exec RTC_RD_TIME ioctl");
- return -2;
- }
- printf("RTC time is %d-%d-%d %d:%d:%d \n",
- rtc_tm.tm_year+1900,rtc_tm.tm_mon,rtc_tm.tm_mday,
- rtc_tm.tm_hour,rtc_tm.tm_min,rtc_tm.tm_sec);
- close(fd);
- return 0;
- }
輸出如下:
- root@manu:~/code/c/self/rtc# ./rtc_test
- RTC time is 2013-8-14 15:46:2
對于set RTC 也可以通過ioctl RTC_SET_TIME參數(shù)來實現(xiàn),我就不多說了,對這部分感興趣的,可以自行man rtc,或者Kernel的Documentation/rtc.txt有一個示例代碼,比較詳細。
已經(jīng)說過了,RTC在時間相關(guān)的硬件中是個獨樹一幟的奇葩,作用和其他的硬件不同。而其他的硬件只是以一定的頻率產(chǎn)生時鐘中斷,幫助OS完成計時。前面我也提到過,你手里拿著個手表,就不要指望給百米大賽計時,原因就是精度太低。硬件也是如此,有精度高的有精度低的。Linux操作系統(tǒng)抽象出了clocksource(時鐘源)來管理這些硬件。Linux會在所有的硬件時鐘中選擇出精度最高作為當前在用的時鐘源。
如何查看當前所有的可用的時鐘源已經(jīng)當前在用的時鐘源呢?
- manu@manu:/$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
- tsc hpet acpi_pm
- manu@manu:/$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
- tsc
我們分別介紹hpet,acpi_pm,tsc,不過在這之前,先介紹一個PIT
#p#
PIT
PIT全稱Programmable Interval Timer,是出現(xiàn)比較早的,比較菜的硬件。這種設備有8253/8254,對底層感興趣的可以讀drivers/clocksource/i8253.c,這種硬件的頻率是1MHZ左右:
- #define PIT_TICK_RATE 1193182ul
PIT為啥沒有落在available_clocksource中呢,因為后起之秀HPET的存在。Kernel中發(fā)現(xiàn)可以使用HPET,就不會用PIT作為始終源了。后面我們會分析到。
HPET
PIT 的精度較低,HPET 被設計來替代 PIT 提供高精度時鐘中斷(至少 10MHz)。它是由微軟和 Intel 聯(lián)合開發(fā)的。一個 HPET 包括了一個固定頻率的數(shù)值增加的計數(shù)器以及 3 到 32 個獨立的計時器,這每一個計時器有包涵了一個比較器和一個寄存器(保存一個數(shù)值,表示觸發(fā)中斷的時機)。每一個比較器都比較計數(shù)器中的數(shù)值和寄存器的數(shù)值,相等就會產(chǎn)生中斷。
HPET這個時鐘源的檢測和注冊是在前文提到的四大初始化中的最后一個:time_init
time_init
|________________x86_late_time_init |_________x86_init.timers.timer_init (arch/x86/kernel/x86_init.c) |________hpet_time_init |_____hpet_enable |____hpet_clocksource_register |_____set_default_time_irq |________________tsc_init |________x86_platform.calibrate_tsc (x86_init.c) |______native_calibrate_tsc |___quit_pit_calibrate |
在這個時候我們看到,time_init主要是兩個部分,TSC是一個,HPET(PIT)是一個。
- static struct irqaction irq0 = {
- .handler = timer_interrupt,
- .flags = IRQF_DISABLED | IRQF_NOBALANCING | IRQF_IRQPOLL | IRQF_TIMER,
- .name = "timer"
- };
- void __init setup_default_timer_irq(void)
- {
- setup_irq(0, &irq0);
- }
- /* Default timer init function */
- void __init hpet_time_init(void)
- {
- if (!hpet_enable())
- setup_pit_timer();
- setup_default_timer_irq();
- }
- static __init void x86_late_time_init(void)
- {
- x86_init.timers.timer_init();
- tsc_init(); //TSC part
- }
- /*
- * Initialize TSC and delay the periodic timer init to
- * late x86_late_time_init() so ioremap works.
- */
- void __init time_init(void)
- {
- late_time_init = x86_late_time_init;
- }
從上面加粗的部分可以看到,當HPET可以enable的時候,我們就不用PIT作為時鐘源了,原因是HPET頻率高,精度高。
從我的筆記本Linux的dmesg可看到:
[ 0.664201] hpet0: 8 comparators, 64-bit 14.318180 MHz counter |
我的筆記本的HPET,頻率是14.318180MHz。
講到這里,就不得不講clocksource。Linux將真實的時鐘做了抽象,用數(shù)據(jù)結(jié)構(gòu)clocksource來管理這些硬件時鐘源。
- /**
- * struct clocksource - hardware abstraction for a free running counter
- * Provides mostly state-free accessors to the underlying hardware.
- * This is the structure used for system time.
- *
- * @name: ptr to clocksource name
- * @list: list head for registration
- * @rating: rating value for selection (higher is better)
- * To avoid rating inflation the following
- * list should give you a guide as to how
- * to assign your clocksource a rating
- * 1-99: Unfit for real use
- * Only available for bootup and testing purposes.
- * 100-199: Base level usability.
- * Functional for real use, but not desired.
- * 200-299: Good.
- * A correct and usable clocksource.
- * 300-399: Desired.
- * A reasonably fast and accurate clocksource.
- * 400-499: Perfect
- * The ideal clocksource. A must-use where
- * available.
- * @read: returns a cycle value, passes clocksource as argument
- * @enable: optional function to enable the clocksource
- * @disable: optional function to disable the clocksource
- * @mask: bitmask for two's complement
- * subtraction of non 64 bit counters
- * @mult: cycle to nanosecond multiplier
- * @shift: cycle to nanosecond pisor (power of two)
- * @max_idle_ns: max idle time permitted by the clocksource (nsecs)
- * @maxadj: maximum adjustment value to mult (~11%)
- * @flags: flags describing special properties
- * @archdata: arch-specific data
- * @suspend: suspend function for the clocksource, if necessary
- * @resume: resume function for the clocksource, if necessary
- * @cycle_last: most recent cycle counter value seen by ::read()
- */
- struct clocksource {
- /*
- * Hotpath data, fits in a single cache line when the
- * clocksource itself is cacheline aligned.
- */
- cycle_t (*read)(struct clocksource *cs);
- cycle_t cycle_last;
- cycle_t mask;
- u32 mult;
- u32 shift;
- u64 max_idle_ns;
- u32 maxadj;
- #ifdef CONFIG_ARCH_CLOCKSOURCE_DATA
- struct arch_clocksource_data archdata;
- #endif
- const char *name;
- struct list_head list;
- int rating;
- int (*enable)(struct clocksource *cs);
- void (*disable)(struct clocksource *cs);
- unsigned long flags;
- void (*suspend)(struct clocksource *cs);
- void (*resume)(struct clocksource *cs);
- /* private: */
- #ifdef CONFIG_CLOCKSOURCE_WATCHDOG
- /* Watchdog related data, used by the framework */
- struct list_head wd_list;
- cycle_t cs_last;
- cycle_t wd_last;
- #endif
- } ____cacheline_aligned;
很重要的一個參數(shù)是rating時鐘源分優(yōu)劣,精度越高的時鐘源的,rating值越大。從注釋中我們可以看到:
- 1--99: 不適合于用作實際的時鐘源,只用于啟動過程或用于測試;
- 100--199:基本可用,可用作真實的時鐘源,但不推薦;
- 200--299:精度較好,可用作真實的時鐘源;
- 300--399:很好,精確的時鐘源;
- 400--499:理想的時鐘源,如有可能就必須選擇它作為時鐘源;
- static struct clocksource clocksource_hpet = {
- .name = "hpet",
- .rating = 250,
- .read = read_hpet,
- .mask = HPET_MASK,
- .flags = CLOCK_SOURCE_IS_CONTINUOUS,
- .resume = hpet_resume_counter,
- #ifdef CONFIG_X86_64
- .archdata = { .vclock_mode = VCLOCK_HPET },
- #endif
- };
從rating上HPET的rating是250,已經(jīng)很不錯了,為什么最終選擇了TSC,available_clocksource中的acpi_pm又是什么?
#p#
ACPI_PM
這個是傳說中的ACPI Power Management Time,這個其實我也知之不詳,對這個感興趣的可以去找下CU的彭東,這小子寫OS,應該會經(jīng)常和這種硬件糾纏不清。到了硬件驅(qū)動層,我水平基本溫飽線以下。
- #define PMTMR_TICKS_PER_SEC 3579545
- 66 static struct clocksource clocksource_acpi_pm = {
- .name = "acpi_pm",
- .rating = 200,
- .read = acpi_pm_read,
- .mask = (cycle_t)ACPI_PM_MASK,
- .mult = 0, /*to be calculated*/
- .shift = 22,
- .flags = CLOCK_SOURCE_IS_CONTINUOUS,
- };
- fs_initcall(init_acpi_pm_clocksource)
我們看到了頻率在3Mhz這個級別,rating是200,低于HPET。至于初始化在fs_initcall這一步做。
TSC
TSC是Time Stamp Counter。CPU 執(zhí)行指令需要一個外部振蕩器產(chǎn)生時鐘信號,從 CLK 管腳輸入。x86 提供了一個 TSC 寄存器,該寄存器的值在每次收到一個時鐘信號時加一。比如 CPU 的主頻為 1GHZ,則每一秒時間內(nèi),TSC 寄存器的值將增加 1G 次,或者說每一個納秒加一次。x86 還提供了 rtdsc 指令來讀取該值,因此 TSC 也可以作為時鐘設備。TSC 提供了比 RTC 更高精度的時間,即納秒級的時間精度。這個很牛X,看時鐘頻率是和CPU的頻率一個水平線的。遠遠超過HPET,PIT這些小魚小蝦米??聪挛业墓P記本的TSC 頻率:
- manu@manu:~/code/c/classical/linux-3.4.61$ dmesg |grep Detected
- [ 0.004000] Detected 2127.727 MHz processor.
- manu@manu:~$ cat /proc/cpuinfo
- processor : 0
- vendor_id : GenuineIntel
- cpu family : 6
- model : 37
- model name : Intel(R) Core(TM) i3 CPU M 330 @ 2.13GHz
- 。。。。。
看這時鐘頻率,相當?shù)膰樔?,看下clocksource的rating:
- static struct clocksource clocksource_tsc = {
- .name = "tsc",
- .rating = 300,
- .read = read_tsc,
- .resume = resume_tsc,
- .mask = CLOCKSOURCE_MASK(64),
- .flags = CLOCK_SOURCE_IS_CONTINUOUS |
- CLOCK_SOURCE_MUST_VERIFY,
- #ifdef CONFIG_X86_64
- .archdata = { .vclock_mode = VCLOCK_TSC },
- #endif
- };
TSC的init和register分別在tsc_init和init_tsc_clocksource中進行
time_init
|________________x86_late_time_init |_________x86_init.timers.timer_init (arch/x86/kernel/x86_init.c) |________hpet_time_init |_____hpet_enable |____hpet_clocksource_register |_____set_default_time_irq |________________tsc_init |________x86_platform.calibrate_tsc (x86_init.c) |______native_calibrate_tsc |___quit_pit_calibrate |
- static int __init init_tsc_clocksource(void)
- {
- if (!cpu_has_tsc || tsc_disabled > 0 || !tsc_khz)
- return 0;
- if (tsc_clocksource_reliable)
- clocksource_tsc.flags &= ~CLOCK_SOURCE_MUST_VERIFY;
- /* lower the rating if we already know its unstable: */
- if (check_tsc_unstable()) {
- clocksource_tsc.rating = 0;
- clocksource_tsc.flags &= ~CLOCK_SOURCE_IS_CONTINUOUS;
- }
- /*
- * Trust the results of the earlier calibration on systems
- * exporting a reliable TSC.
- */
- if (boot_cpu_has(X86_FEATURE_TSC_RELIABLE)) {
- clocksource_register_khz(&clocksource_tsc, tsc_khz);
- return 0;
- }
- schedule_delayed_work(&tsc_irqwork, 0);
- return 0;
- }
- /*
- * We use device_initcall here, to ensure we run after the hpet
- * is fully initialized, which may occur at fs_initcall time.
- */
- device_initcall(init_tsc_clocksource);
所以,clocksource 的PK之戰(zhàn)中,TSC技壓群雄,完爆HPET/PIT/ACPI_PM,成為current_clocksource。clocksource之戰(zhàn)如何進行,如何選擇當前時鐘源,新的時鐘源注冊會帶來什么影響。Linux如何根據(jù)時鐘源完成計時,這個就在下一篇clocksource中介紹。