Linux中时间子系统初始化time_init函数
时间子系统初始化time_init
void __init time_init(void)
{
#ifdef CONFIG_HPET_TIMERif (is_hpet_capable()) {/** HPET initialization needs to do memory-mapped io. So, let* us do a late initialization after mem_init().*/late_time_init = hpet_time_init;return;}
#endifxtime.tv_sec = get_cmos_time();xtime.tv_nsec = (INITIAL_JIFFIES % HZ) * (NSEC_PER_SEC / HZ);set_normalized_timespec(&wall_to_monotonic,-xtime.tv_sec, -xtime.tv_nsec);cur_timer = select_timer();printk(KERN_INFO "Using %s for high-res timesource\n",cur_timer->name);time_init_hook();
}
1. 函数功能概述
time_init
函数负责初始化内核的时间系统,包括硬件时钟读取、时间源选择、系统时间基准设置等,为内核提供时间管理的基础设施
2. 代码逐段分析
2.1. 函数定义
void __init time_init(void)
{
void
:函数没有返回值__init
:宏标记,表示该函数只在内核初始化阶段使用,初始化完成后内存会被释放
2.2. HPET 定时器支持(条件编译)
#ifdef CONFIG_HPET_TIMERif (is_hpet_capable()) {/** HPET initialization needs to do memory-mapped io. So, let* us do a late initialization after mem_init().*/late_time_init = hpet_time_init;return;}
#endif
#ifdef CONFIG_HPET_TIMER
:条件编译,只在支持 HPET 的内核中包含if (is_hpet_capable())
:检查系统是否支持 HPET(高精度事件定时器)- HPET 初始化需要内存映射IO,所以在
mem_init()
之后进行延迟初始化 late_time_init = hpet_time_init
:设置延迟初始化函数指针return
:如果使用 HPET,直接返回,后续初始化在延迟阶段进行
2.3. 读取 CMOS 时钟时间
xtime.tv_sec = get_cmos_time();
xtime
:内核全局变量,存储系统当前时间(从 1970-01-01 开始的秒数)xtime.tv_sec
:时间的秒数部分get_cmos_time()
:函数调用,从 CMOS 实时时钟芯片读取当前时间
2.4. 计算纳秒部分
xtime.tv_nsec = (INITIAL_JIFFIES % HZ) * (NSEC_PER_SEC / HZ);
xtime.tv_nsec
:时间的纳秒部分INITIAL_JIFFIES
:内核启动时的 jiffies 初始值HZ
:系统时钟频率(通常 100, 250, 1000 等)NSEC_PER_SEC
:每秒的纳秒数(1,000,000,000)- 计算原理:
(jiffies % HZ)
得到当前秒内已过的时钟滴答数,乘以每个滴答的纳秒数
2.5. 设置单调时间基准
set_normalized_timespec(&wall_to_monotonic,-xtime.tv_sec, -xtime.tv_nsec);
set_normalized_timespec()
:规范化时间规格函数&wall_to_monotonic
:全局变量,存储墙钟时间到单调时间的偏移量-xtime.tv_sec, -xtime.tv_nsec
:取负值,表示从墙钟时间减去当前时间得到单调时间起点- 作用:建立单调时间基准,确保
CLOCK_MONOTONIC
从系统启动开始计时
2.6. 选择定时器硬件
cur_timer = select_timer();
cur_timer
:全局指针,指向当前使用的定时器操作结构select_timer()
:函数调用,自动检测并选择可用的高精度定时器硬件- 选择顺序:通常 HPET > ACPI PMT > PIT > 其他
2.7. 打印定时器信息
printk(KERN_INFO "Using %s for high-res timesource\n",cur_timer->name);
printk(KERN_INFO ...)
:内核信息打印cur_timer->name
:所选定时器的名称字符串- 作用:在启动日志中显示使用的高分辨率时间源
2.8. 调用架构特定初始化
time_init_hook();
}
time_init_hook()
:函数指针,指向架构特定的时间初始化函数- 作用:允许不同 CPU 架构进行特定的时间系统设置
读取硬件时间get_cmos_time
unsigned long get_cmos_time(void)
{unsigned long retval;spin_lock(&rtc_lock);if (efi_enabled)retval = efi_get_time();elseretval = mach_get_cmos_time();spin_unlock(&rtc_lock);return retval;
}
inline unsigned long __init efi_get_time(void)
{efi_status_t status;efi_time_t eft;efi_time_cap_t cap;status = phys_efi_get_time(&eft, &cap);if (status != EFI_SUCCESS)printk("Oops: efitime: can't read time status: 0x%lx\n",status);return mktime(eft.year, eft.month, eft.day, eft.hour,eft.minute, eft.second);
}
static inline unsigned long mach_get_cmos_time(void)
{unsigned int year, mon, day, hour, min, sec;int i;/* The Linux interpretation of the CMOS clock register contents:* When the Update-In-Progress (UIP) flag goes from 1 to 0, the* RTC registers show the second which has precisely just started.* Let's hope other operating systems interpret the RTC the same way.*//* read RTC exactly on falling edge of update flag */for (i = 0 ; i < 1000000 ; i++) /* may take up to 1 second... */if (CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP)break;for (i = 0 ; i < 1000000 ; i++) /* must try at least 2.228 ms */if (!(CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP))break;do { /* Isn't this overkill ? UIP above should guarantee consistency */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);} while (sec != CMOS_READ(RTC_SECONDS));if (!(CMOS_READ(RTC_CONTROL) & RTC_DM_BINARY) || RTC_ALWAYS_BCD){BCD_TO_BIN(sec);BCD_TO_BIN(min);BCD_TO_BIN(hour);BCD_TO_BIN(day);BCD_TO_BIN(mon);BCD_TO_BIN(year);}if ((year += 1900) < 1970)year += 100;return mktime(year, mon, day, hour, min, sec);
}
1. 函数功能概述
这些函数负责从硬件时钟(RTC 或 EFI)读取当前时间,并将其转换为从 1970-01-01 开始的秒数,为系统提供初始时间基准
2. get_cmos_time
函数分析
2.1. 函数定义和变量声明
unsigned long get_cmos_time(void)
{unsigned long retval;
unsigned long
:返回值类型,表示从 1970-01-01 开始的秒数retval
:存储返回的时间值
2.2. 获取 RTC 锁
spin_lock(&rtc_lock);
spin_lock(&rtc_lock)
:获取实时时钟的自旋锁- 作用:防止多CPU同时访问RTC硬件,保证读操作的原子性
2.3. EFI 时间获取分支
if (efi_enabled)retval = efi_get_time();
efi_enabled
:检查系统是否启用了 EFI(可扩展固件接口)efi_get_time()
:如果使用 EFI,调用 EFI 时间获取函数
2.4. 传统 CMOS 时间获取分支
elseretval = mach_get_cmos_time();
mach_get_cmos_time()
:传统 BIOS 系统,调用 CMOS RTC 时间获取函数
2.5. 释放锁并返回
spin_unlock(&rtc_lock);return retval;
}
spin_unlock(&rtc_lock)
:释放 RTC 锁return retval
:返回获取的时间值
3. efi_get_time
函数分析
3.1. 函数定义
inline unsigned long __init efi_get_time(void)
{
inline
:内联函数,减少函数调用开销__init
:只在初始化阶段使用
3.2. 变量声明
efi_status_t status;efi_time_t eft;efi_time_cap_t cap;
efi_status_t
:EFI 操作状态类型efi_time_t
:EFI 时间结构体efi_time_cap_t
:EFI 时间能力结构体
3.3. 调用 EFI 获取时间
status = phys_efi_get_time(&eft, &cap);
phys_efi_get_time()
:物理 EFI 调用,从固件获取时间&eft
:传递时间结构体指针接收时间数据&cap
:传递能力结构体指针
3.4. 错误处理
if (status != EFI_SUCCESS)printk("Oops: efitime: can't read time status: 0x%lx\n",status);
- 检查 EFI 操作状态
- 如果失败,打印错误信息但继续执行
3.5. 时间转换
return mktime(eft.year, eft.month, eft.day, eft.hour,eft.minute, eft.second);
}
mktime()
:将年月日时分秒转换为从 1970-01-01 开始的秒数
4. mach_get_cmos_time
函数分析
4.1. 函数定义和变量声明
static inline unsigned long mach_get_cmos_time(void)
{unsigned int year, mon, day, hour, min, sec;int i;
- 声明时间分量变量和循环计数器
4.2. 等待 UIP 标志置位
/* read RTC exactly on falling edge of update flag */for (i = 0 ; i < 1000000 ; i++) /* may take up to 1 second... */if (CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP)break;
- 循环等待 UIP(更新进行中)标志置位
- 最多等待 100 万次,可能长达 1 秒
CMOS_READ(RTC_FREQ_SELECT)
:读取 CMOS 频率选择寄存器RTC_UIP
:更新进行中标志位
4.3. 等待 UIP 标志清除
for (i = 0 ; i < 1000000 ; i++) /* must try at least 2.228 ms */if (!(CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP))break;
- 等待 UIP 标志清除(从 1 变 0)
- 必须在 UIP 下降沿后至少尝试 2.228 毫秒
- 这是读取稳定时间数据的关键点
4.4. 读取时间寄存器(带重试)
do { /* Isn't this overkill ? UIP above should guarantee consistency */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);} while (sec != CMOS_READ(RTC_SECONDS));
- 循环读取所有时间寄存器
- 条件:直到秒数读取一致(防止在秒变更时读取)
4.5. BCD 到二进制转换
if (!(CMOS_READ(RTC_CONTROL) & RTC_DM_BINARY) || RTC_ALWAYS_BCD){BCD_TO_BIN(sec);BCD_TO_BIN(min);BCD_TO_BIN(hour);BCD_TO_BIN(day);BCD_TO_BIN(mon);BCD_TO_BIN(year);}
- 检查 RTC 控制寄存器是否使用 BCD(二进制编码十进制)格式
- 如果使用 BCD 或强制 BCD,将所有分量转换为二进制
BCD_TO_BIN
:宏,将 BCD 码转换为二进制数
4.6. 年份调整
if ((year += 1900) < 1970)year += 100;
- 年份加上 1900(RTC 通常存储从 1900 开始的偏移)
- 如果年份仍小于 1970,再加 100(处理 2000 年问题)
- 早期的 CMOS RTC 芯片只有 2 位数字存储年份(00-99)
- 硬件设计时假设是 20 世纪(1900-1999)
- 当时间进入 2000 年时,年份会从 99 回滚到 00
4.7. 时间转换
return mktime(year, mon, day, hour, min, sec);
}
- 调用
mktime
将时间分量转换为 Unix 时间戳
5. 关键技术细节
5.1. RTC 更新机制
RTC 更新周期:
1. UIP 置位: 表示更新开始
2. 更新寄存器: RTC内部更新所有时间寄存器
3. UIP 清除: 更新完成,数据稳定
4. 读取窗口: 在UIP下降沿后读取可获得准确时间
我来详细解释这个 BCD 到二进制转换的宏定义,这是处理硬件时钟数据的核心算法。
6. BCD宏定义解析
#define BCD_TO_BIN(val) ((val)=BCD2BIN(val))
#define BCD2BIN(val) (((val) & 0x0f) + ((val)>>4)*10)
6.1. BCD2BIN 宏 - 实际转换逻辑
#define BCD2BIN(val) (((val) & 0x0f) + ((val)>>4)*10)
(val) & 0x0f
:取低4位(个位数)(val) >> 4
:取高4位(十位数)((val) >> 4) * 10
:十位数乘以10- 两者相加得到最终的十进制数
6.2. BCD_TO_BIN 宏 - 赋值包装
#define BCD_TO_BIN(val) ((val)=BCD2BIN(val))
- 调用
BCD2BIN(val)
进行转换 - 将转换结果赋值回原变量
(val) = ...
- 直接修改传入的变量值
6.3. BCD(二进制编码十进制)基础
BCD 编码原理
BCD 用4位二进制表示1位十进制数(0-9)十进制 23 的 BCD 表示:
十位数: 2 → 二进制 0010
个位数: 3 → 二进制 0011
组合: 0010 0011 = 0x23
为什么需要 BCD?
- 硬件设计简单:直接对应数码管显示
- 避免二进制到十进制的转换开销
- 早期硬件计算能力有限
6.4. 转换过程详细演示
示例1:BCD 0x23 → 十进制 23
值: 0x23 = 0010 0011 二进制步骤1: (val) & 0x0f 0x23 & 0x0f = 0x03 = 3 ← 个位数步骤2: (val) >> 40x23 >> 4 = 0x02 = 2 ← 十位数步骤3: ((val) >> 4) * 102 * 10 = 20 ← 十位数值步骤4: 相加3 + 20 = 23 ← 最终结果
示例2:BCD 0x59 → 十进制 59
值: 0x59 = 0101 1001 二进制步骤1: 0x59 & 0x0f = 0x09 = 9
步骤2: 0x59 >> 4 = 0x05 = 5
步骤3: 5 * 10 = 50
步骤4: 9 + 50 = 59