Linux时间管理之hardware(1)


一直以来对Linux下的时间管理知之不详,GFree_wind在微博发起过几次Linux下时钟的讨论,和Godbach这些大牛比,我完全插不上话,因为不懂。近来闲暇时间研究了下Linux下的时间管理,分享出来,请大家指正。

从我们大白话的角度想,时间管理其实分成两部分,就像我们小时候学习物理的时候物理老师不断强调时间和时刻的区别。一个是时刻,比如现在是20:44:37秒,指的是时刻,我们手机上看时间,指的也是时刻。另一块是时间,比如说,我每天工作八小时,再比如说,半小时之后,我要出门了,结束时间指向的是未来,但是仍然是一段时间。OK。无论是时刻还是时望间,都是需要硬件支持的,你手里只有一块最小刻度只有1秒的手表,就不要指用这块手表给百米大赛度量成绩了,何哉,硬件太挫。Linux也是如此,之所以Linux启动之后,可以精确的计时,那是因为Linux的下面有相应的硬件为依托。  

RTC


RTC,real time clock,实时时钟和其他的硬件是不同的,RTC吐出来的是时刻,而其他硬件时钟吐出来的是时间。也就是说,RTC能告诉我们,当前是2013年9月12日,21:49:38,但是其他的硬件如TSC,PIT,HPET只能告诉我们,我应该走过了XX个cycle,按照我的频率,已经过去了10分钟了。

为啥RTC这么牛X,可以告诉我们当前时刻,哪怕用户关了机?以X86为例,RTC是主板上的一块CMOS芯片,哪怕你的Linux关了机,她也可以依赖主板上的电池维持时钟的准确。当然了,在Linux下,RTC存储的是UTC时间,而不会考虑timezone。

所以,Linux启动的时候,一定会拜访RTC来获得当前的时刻值,尽管精度不高精确到秒)。When and How?

首先回答When。Linux启动的时候,start_kernel有四大time相关的函数调用:

从RTC中读取当前的UTC时间是timekeeping_init中做的事情,调用路径如下:

 timekeeping_init

|___________read_persistent_clock    (arch/x86/kernel/rtc.c)

          |_____x86_platform.get_wallclock()

          |_____mach_get_cmos_time  (arch/x86/kernel/x86_init.c)

  1. /************arch/x86/kernel/rtc.c*****************/ 
  2. void read_persistent_clock(struct timespec *ts) 
  3.     unsigned long retval; 
  4.   
  5.     retval = x86_platform.get_wallclock(); 
  6.   
  7.     ts->tv_sec = retval
  8.     ts->tv_nsec = 0
  9.   
  10. /*****************arch/x86/kernel/x86_init.c ****************/ 
  11.   
  12. struct x86_platform_ops x86_platform = { 
  13.     .calibrate_tsc = native_calibrate_tsc
  14.     .wallclock_init = wallclock_init_noop
  15.     .get_wallclock = mach_get_cmos_time
  16.     .set_wallclock = mach_set_rtc_mmss
  17.     .iommu_shutdown = iommu_shutdown_noop
  18.     .is_untracked_pat_range = is_ISA_range
  19.     .nmi_init = default_nmi_init
  20.     .get_nmi_reason = default_get_nmi_reason
  21.     .i8042_detect = default_i8042_detect
  22.     .save_sched_clock_state = tsc_save_sched_clock_state
  23.     .restore_sched_clock_state = tsc_restore_sched_clock_state

对于我们而言,我们要读的function是mach_get_cmos_time

  1. unsigned long mach_get_cmos_time(void) 
  2.     unsigned int status, year, mon, day, hour, min, sec, century = 0
  3.     unsigned long flags; 
  4.   
  5.     spin_lock_irqsave(&rtc_lock, flags); 
  6.   
  7.     /* 
  8.      * If UIP is clear, then we have >= 244 microseconds before 
  9.      * RTC registers will be updated. Spec sheet says that this 
  10.      * is the reliable way to read RTC - registers. If UIP is set 
  11.      * then the register access might be invalid. 
  12.      */ 
  13.     while ((CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP)) 
  14.         cpu_relax(); 
  15.   
  16.     sec = CMOS_READ(RTC_SECONDS); 
  17.     min = CMOS_READ(RTC_MINUTES); 
  18.     hour = CMOS_READ(RTC_HOURS); 
  19.     day = CMOS_READ(RTC_DAY_OF_MONTH); 
  20.     mon = CMOS_READ(RTC_MONTH); 
  21.     year = CMOS_READ(RTC_YEAR); 
  22.   
  23. #ifdef CONFIG_ACPI 
  24.     if (acpi_gbl_FADT.header.revision >= FADT2_REVISION_ID && 
  25.      acpi_gbl_FADT.century) 
  26.         century = CMOS_READ(acpi_gbl_FADT.century); 
  27. #endif 
  28.   
  29.     status = CMOS_READ(RTC_CONTROL); 
  30.     WARN_ON_ONCE(RTC_ALWAYS_BCD && (status & RTC_DM_BINARY)); 
  31.   
  32.     spin_unlock_irqrestore(&rtc_lock, flags); 
  33.   
  34.     if (RTC_ALWAYS_BCD || !(status & RTC_DM_BINARY)) { 
  35.         sec = bcd2bin(sec); 
  36.         min = bcd2bin(min); 
  37.         hour = bcd2bin(hour); 
  38.         day = bcd2bin(day); 
  39.         mon = bcd2bin(mon); 
  40.         year = bcd2bin(year); 
  41.     } 
  42.   
  43.     if (century) { 
  44.         century = bcd2bin(century); 
  45.         year += century * 100; 
  46.         printk(KERN_INFO "Extended CMOS year: %d\n", century * 100); 
  47.     } else 
  48.         year += CMOS_YEARS_OFFS; 
  49.   
  50.     return mktime(year, mon, day, hour, min, sec); 

这一段代码已经牵扯到了硬件相关的编程,我们其实并不关心驱动,对于硬件比较感兴趣的筒子,可以访问http://xenyinzen.wikidot.com/reship:080225-2

获得更多更详细的信息。mktime是将年月日时分秒组装成1970年1月1日00:00:00这个UNIX基准时间以来的秒数。我们在Linux下可以通过一下方式获得这个值:

  1. root@manu:/sys/class/rtc/rtc0# date +%s ;cat /sys/class/rtc/rtc0/since_epoch  
  2. 1379081060 
  3. 1379081060 

既然Linux上电的时候,可以从RTC中读出当前的时间,我们也可以设置时间,并且写入到RTC。用户层也可以操作RTC硬件时钟,通过ioctl。下面给出一个样例: 

  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. #include <linux/rtc.h>  
  4. #include <fcntl.h> 
  5. #include <sys/ioctl.h> 
  6.   
  7. int main(int argc,char *argv[]) 
  8.     int retval,fd; 
  9.     struct rtc_time rtc_tm; 
  10.   
  11.     fd=open("/dev/rtc",O_RDONLY);  
  12.     if(fd==-1) 
  13.     { 
  14.         perror("error open /dev/rtc"); 
  15.         return -1; 
  16.     } 
  17.   
  18.     retval=ioctl(fd,RTC_RD_TIME,&rtc_tm); 
  19.     if(retval==-1) 
  20.     { 
  21.         perror("error exec RTC_RD_TIME ioctl"); 
  22.         return -2; 
  23.     } 
  24.     printf("RTC time is %d-%d-%d %d:%d:%d \n", 
  25.            rtc_tm.tm_year+1900,rtc_tm.tm_mon,rtc_tm.tm_mday, 
  26.            rtc_tm.tm_hour,rtc_tm.tm_min,rtc_tm.tm_sec); 
  27.   
  28.     close(fd); 
  29.   
  30.     return 0; 

输出如下:   

  1. root@manu:~/code/c/self/rtc# ./rtc_test  
  2. RTC time is 2013-8-14 15:46:2 

对于set RTC 也可以通过ioctl RTC_SET_TIME参数来实现,我就不多说了,对这部分感兴趣的,可以自行man rtc,或者Kernel的Documentation/rtc.txt有一个示例代码,比较详细。

已经说过了,RTC在时间相关的硬件中是个独树一帜的奇葩,作用和其他的硬件不同。而其他的硬件只是以一定的频率产生时钟中断,帮助OS完成计时。前面我也提到过,你手里拿着个手表,就不要指望给百米大赛计时,原因就是精度太低。硬件也是如此,有精度高的有精度低的。Linux操作系统抽象出了clocksource时钟源)来管理这些硬件。Linux会在所有的硬件时钟中选择出精度最高作为当前在用的时钟源。

如何查看当前所有的可用的时钟源已经当前在用的时钟源呢? 

  1. manu@manu:/$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource  
  2. tsc hpet acpi_pm  
  3. manu@manu:/$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource  
  4. tsc 

我们分别介绍hpet,acpi_pm,tsc,不过在这之前,先介绍一个PIT


相关内容