[TOC]

drivers/rtc 实时时钟(Real Time Clock) 驱动框架与硬件驱动集合

历史与背景

这项技术是为了解决什么特定问题而诞生的?

RTC(Real Time Clock)驱动框架和相关驱动是为了解决计算机系统在**主电源关闭后维持和提供真实世界时间(“wall-clock time”)**这一根本问题而诞生的。

  • 时间持久化:计算机系统的主处理器和内存是易失性的,一旦断电,所有状态信息都会丢失。然而,系统需要一个持久化的时钟来记录日期和时间,以便在下次启动时能够知道当前的时间,这对于文件系统时间戳、日志记录、证书验证和网络协议至关重要。
  • 低功耗运行:这个持久化的时钟必须功耗极低,因为它通常仅由一块小型的纽扣电池供电,需要维持数年的运行。
  • 定时唤醒:除了计时,RTC硬件通常还具备闹钟(Alarm)功能。这允许系统进入深度休眠或关机状态,并由RTC在预设的时间点产生一个硬件中断信号,从而唤醒系统执行预定任务(如定时备份、系统维护等),这是实现高级电源管理的关键。

它的发展经历了哪些重要的里程碑或版本迭代?

Linux RTC子系统的发展反映了Linux从单一PC平台向多样化硬件平台演进的过程。

  • 早期PC时代 (rtc-cmos.c):最初,Linux的RTC支持主要围绕PC架构中的摩托罗拉MC146818兼容芯片。驱动 (rtc-cmos.c) 通过传统的I/O端口 (inb/outb) 直接访问硬件。用户空间接口也比较原始,只有一个固定的 /dev/rtc 设备。
  • RTC Class框架的诞生:随着Linux被广泛用于各种嵌入式系统,出现了成百上千种通过I2C、SPI或其他总线连接的RTC芯片。为每种芯片维护一套独立的用户空间接口是不可行的。因此,一个重要的里程碑是**RTC Class框架 (drivers/rtc/class.c)**的建立。它提供了一个统一的、抽象的接口给用户空间(如 /dev/rtc0, /dev/rtc1, …),并定义了一套标准的操作函数集 (struct rtc_class_ops),供底层硬件驱动来实现和注册。
  • Sysfs接口的引入:为了方便脚本和简单应用访问,RTC Class框架增加了sysfs接口(位于 /sys/class/rtc/rtcX/)。通过读写这些文件(如 date, time, wakealarm),可以无需复杂的ioctl调用就能获取时间或设置闹钟。
  • 与设备树(Device Tree)的集成:在现代嵌入式Linux中,RTC硬件的描述(如I2C地址、中断引脚等)都通过设备树来传递给内核,使得硬件驱动本身可以更加通用,无需硬编码平台相关信息。

目前该技术的社区活跃度和主流应用情况如何?

RTC子系统是Linux内核中一个非常成熟和稳定的部分。其核心Class框架改动很少,社区的活跃度主要体现在为层出不穷的新SoC或外围芯片添加新的硬件驱动。
该技术无处不在,是所有现代计算设备的基础组件,包括:

  • PC和服务器:用于在启动时初始化系统时钟。
  • 智能手机和移动设备:维持准确时间,并用于定时唤醒以处理通知等。
  • 嵌入式和物联网设备:在网络连接不可用时提供可靠的时间源,并用于低功耗场景下的定时唤醒。

核心原理与设计

它的核心工作原理是什么?

drivers/rtc 目录下的工作原理可以分为两个层面:框架层驱动层

  • 框架层 (class.c, interface.c)

    1. 提供统一接口:它向用户空间提供了标准化的接口,主要是字符设备 /dev/rtcX 和sysfs目录 /sys/class/rtc/rtcX/
    2. 定义操作集:它定义了 struct rtc_class_ops 结构体,其中包含了一系列函数指针,如 read_time, set_time, read_alarm, set_alarm, ioctl 等。这构成了硬件驱动必须遵守的“合同”。
    3. 管理和分发:当一个硬件驱动通过 rtc_device_register() 注册自己时,框架会为其分配一个rtc编号(如rtc0),创建相应的设备节点,并保存其提供的rtc_class_ops。当用户空间对 /dev/rtc0 进行操作(如 ioctl(fd, RTC_RD_TIME, ...))时,框架会捕获该请求,并调用 rtc0 对应硬件驱动注册的 read_time 函数来完成实际的硬件操作。
  • 驱动层 (各种 rtc-*.c 文件)

    1. 硬件通信:每个驱动文件都针对一款特定的RTC芯片。它包含了与该芯片通信的逻辑,例如通过I2C总线发送命令、通过SPI读写寄存器、或直接进行内存映射I/O。
    2. 实现操作集:驱动程序会实现 rtc_class_ops 中定义的函数。例如,它的 read_time 函数会包含一系列I2C传输指令,用于从芯片中读取年、月、日、时、分、秒等寄存器的值,并将其转换为内核标准的 struct rtc_time 格式。
    3. 注册与探测:驱动通过总线驱动模型(如I2C driver的.probe函数)被内核调用。在探测到对应的硬件设备后,它会初始化硬件,填充rtc_class_ops,并调用 rtc_device_register() 将自己“挂载”到RTC Class框架上。

它的主要优势体现在哪些方面?

  • 抽象与解耦:用户空间应用程序(如 hwclock 命令)使用完全相同的接口与任何RTC硬件交互,无需关心底层硬件是I2C接口还是SPI接口,是哪家厂商的芯片。
  • 模块化与可扩展性:支持新的RTC硬件变得非常简单。开发者只需编写一个新的驱动文件来实现 rtc_class_ops,而无需修改任何核心框架或上层应用代码。
  • 标准化:提供了统一的时间数据结构 (struct rtc_time) 和标准的 ioctl 命令,简化了应用开发。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 精度和分辨率有限:硬件RTC为了追求极低的功耗,通常使用32.768 kHz的晶振。这导致其精度相对较低,每天可能会有数秒的漂移。其分辨率通常也只有1秒,不适合进行高精度的时间测量。
  • 非单调性:RTC表示的是“墙上时钟”,它可以被用户或NTP服务任意修改,甚至可以向后调整(例如,当时区或夏令时改变时)。因此,它绝不能用于测量时间间隔。
  • 接口复杂性:传统的 ioctl 接口虽然功能强大,但对于编程者来说使用起来较为繁琐。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

  • 系统时钟初始化:这是RTC最核心的场景。系统启动时,内核会读取RTC的时间来初始化系统的“墙上时钟”(CLOCK_REALTIME)。如果没有RTC,系统时钟将从1970年1月1日开始,直到NTP等网络时间服务同步为止。
  • 计划性系统唤醒:这是RTC的另一个关键功能。rtcwake 命令行工具就是其典型用户。例如,执行 rtcwake -m freeze -s 60 会让系统进入低功耗的冻结状态,并设置RTC闹钟在60秒后唤醒系统。这被广泛用于定时执行维护任务或在物联网设备中定时采集数据。
  • 无网络环境下的时间维持:在无法接入互联网的嵌入式设备或特定场景中,RTC是维持时间准确性的唯一可靠手段。

是否有不推荐使用该技术的场景?为什么?

  • 高精度时间间隔测量:当需要测量代码执行时间或事件间隔时,应使用内核的单调时钟(CLOCK_MONOTONIC)。该时钟从系统启动开始单调递增,不受墙上时钟变化的影响,且具有更高的分辨率。
  • 高精度时间同步:对于需要亚秒级时间精度的应用(如金融交易、科学计算),RTC只能作为初始参考。必须使用NTP(网络时间协议)或PTP(精确时间协议)来持续校准系统时钟,以补偿RTC和本地晶振的漂移。

对比分析

请将其 与 其他相似技术 进行详细对比。

Linux内核中有多种时间来源,RTC是其中特殊的一种。

特性 RTC (Real Time Clock) System Wall Clock (CLOCK_REALTIME) Monotonic Clock (CLOCK_MONOTONIC) 硬件高精度定时器 (HPET, TSC)
本质 独立的、电池供电的低功耗硬件。 内核维护的软件时钟,表示“墙上时间”。 内核维护的软件时钟,表示自启动以来的流逝时间。 CPU或主板上的高频硬件计数器。
持久性 断电后保持 断电后丢失。 断电后丢失。 断电后丢失。
单调性 (可任意设置,可回退)。 (可被date命令, NTP等改变)。 (保证永不回退)。 (通常情况下)。
分辨率 低 (通常为1秒)。 高 (通常为纳秒级)。 高 (通常为纳秒级)。 非常高 (与硬件频率相关)。
主要用途 在系统断电时维持时间;为系统启动提供初始时间;定时唤醒系统。 为应用程序和文件系统提供标准的日期和时间。 测量时间间隔、设置超时。 作为内核时钟子系统的基础,驱动CLOCK_REALTIMECLOCK_MONOTONIC的运转。

drivers/rtc/dev.c

drivers/rtc/class.c

RTC 子系统核心:硬件时钟抽象和管理

本代码片段是Linux内核RTC(Real-Time Clock)子系统的核心层。其主要功能是提供一个通用的、硬件无关的框架,用于管理系统中的RTC设备。它负责处理所有RTC设备的共性任务,包括:分配和管理设备ID;通过sysfs和字符设备节点向用户空间暴露标准接口;实现可选的、将硬件时钟同步到系统时钟的功能(hctosys);以及处理系统挂起/恢复期间的时间校准。具体的RTC硬件驱动(如针对STM32H750内部RTC的驱动)会在此框架之上构建,只需实现一组定义好的硬件操作回调函数(rtc_class_ops)。

实现原理分析

该RTC核心框架是一个典型的设备类驱动模型,它将设备驱动的实现与内核的通用接口解耦。

  1. 设备类与生命周期管理:

    • rtc_init函数注册一个名为rtcstruct class。这会在/sys/class/rtc/下为所有RTC设备提供一个统一的视图。
    • rtc_allocate_device是RTC设备的“构造函数”。它负责分配rtc_device结构体,并初始化所有内部组件,如互斥锁、自旋锁、定时器队列、工作队列等。这是一个纯软件的初始化,不涉及硬件。
    • __devm_rtc_register_device是注册的核心。它接收一个已分配并由具体驱动填充了opsrtc_device,然后执行以下操作:
      a. 通过ida_alloc从一个全局ID分配器中获取一个唯一的rtcX编号。
      b. 创建一个字符设备(cdev_device_add),这使得用户空间可以通过/dev/rtcX设备节点访问该RTC。
      c. 在/proc/driver/rtc下创建诊断文件。
    • rtc_device_release是“析构函数”,在设备的最后一个引用被释放时调用。它负责清理所有定时器、取消工作队列、释放ID、销毁锁并释放内存。
    • devm_*系列函数(如devm_rtc_allocate_device)是资源管理的封装,它们将设备的分配和释放与驱动程序的生命周期(probe/remove)绑定,极大地简化了驱动的错误处理和清理逻辑。
  2. 硬件到系统时钟同步 (hctosys):

    • 这是一个可选的、在启动时执行的功能,由CONFIG_RTC_HCTOSYS_DEVICE宏控制。
    • rtc_hctosys函数会读取指定RTC硬件的时间(通过rtc_read_time,它会调用具体驱动的.read_time回调),将RTC的tm格式时间转换为Unix时间戳,然后调用do_settimeofday64来设置系统(wall clock)时间。
    • 它包含一个重要的细节:由于RTC通常只存储整数秒,它在设置系统时间时会加上半秒(NSEC_PER_SEC >> 1),作为对被截断的亚秒部分的一个最佳猜测补偿。
  3. 挂起/恢复处理:

    • rtc_suspend: 在系统进入休眠前,它会记录下当前的系统时间和RTC时间。它还实现了一个巧妙的“delta-of-deltas”算法来补偿多次休眠/唤醒循环可能引入的微小漂移。
    • rtc_resume: 在系统从休眠中唤醒后,它再次读取当前的系统时间和RTC时间。通过比较休眠前后的两个时间快照,它可以精确地计算出系统“睡眠”了多长时间(sleep_time)。然后,它调用timekeeping_inject_sleeptime64将这段丢失的时间“注入”回内核的时间保持子系统,从而校准系统时间,防止因休眠导致的时间变慢。

代码分析

设备生命周期与hctosys

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// rtc_device_release: RTC设备的release回调,在最后一个引用消失时调用。
static void rtc_device_release(struct device *dev)
{
struct rtc_device *rtc = to_rtc_device(dev);
// ... 清理定时器队列 ...
// 取消同步等待的IRQ工作队列。
cancel_work_sync(&rtc->irqwork);

ida_free(&rtc_ida, rtc->id); // 释放设备ID。
mutex_destroy(&rtc->ops_lock); // 销毁互斥锁。
kfree(rtc); // 释放rtc_device结构体内存。
}

// rtc_hctosys: 将硬件时钟(HC)同步到系统时钟(SYS)。
static void rtc_hctosys(struct rtc_device *rtc)
{
int err;
struct rtc_time tm;
struct timespec64 tv64 = { // 初始化纳秒部分为0.5秒。
.tv_nsec = NSEC_PER_SEC >> 1,
};

// 通过ops回调读取硬件时间。
err = rtc_read_time(rtc, &tm);
if (err) { /* ... 错误处理 ... */ }

// 将rtc_time结构转换为time64_t (Unix时间戳)。
tv64.tv_sec = rtc_tm_to_time64(&tm);

// 调用内核核心函数来设置系统时间。
err = do_settimeofday64(&tv64);

dev_info(rtc->dev.parent, "setting system clock to %ptR UTC (%lld)\n",
&tm, (long long)tv64.tv_sec);

err_read:
rtc_hctosys_ret = err;
}

电源管理 (挂起/恢复)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// rtc_suspend: 系统挂起前的回调。
static int rtc_suspend(struct device *dev)
{
// ... 检查是否应跳过,以及是否是主hctosys设备 ...

// 记录挂起前的RTC时间和系统时间快照。
err = rtc_read_time(rtc, &tm);
ktime_get_real_ts64(&old_system);
old_rtc.tv_sec = rtc_tm_to_time64(&tm);

// ... "delta-of-deltas" 漂移补偿逻辑 ...
return 0;
}

// rtc_resume: 系统恢复后的回调。
static int rtc_resume(struct device *dev)
{
// ... 检查 ...

// 记录恢复后的RTC时间和系统时间快照。
ktime_get_real_ts64(&new_system);
err = rtc_read_time(rtc, &tm);
new_rtc.tv_sec = rtc_tm_to_time64(&tm);

// ... 检查时间倒流 ...

// 计算RTC时间经过了多久,即真实的睡眠时间。
sleep_time = timespec64_sub(new_rtc, old_rtc);

// ... 减去suspend和resume函数执行期间的内核运行时间,以提高精度 ...

// 将计算出的睡眠时间注入内核时间保持子系统。
if (sleep_time.tv_sec >= 0)
timekeeping_inject_sleeptime64(&sleep_time);
rtc_hctosys_ret = 0;
return 0;
}

设备分配与注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// rtc_allocate_device: 分配并初始化一个rtc_device结构体(不注册)。
static struct rtc_device *rtc_allocate_device(void)
{
struct rtc_device *rtc;
rtc = kzalloc(sizeof(*rtc), GFP_KERNEL);
// ... 初始化device, class, release回调 ...
// ... 初始化锁、等待队列、定时器、工作队列等内部成员 ...
return rtc;
}

// devm_rtc_allocate_device: 资源管理的rtc_allocate_device版本。
struct rtc_device *devm_rtc_allocate_device(struct device *dev)
{
// ... 获取ID, 调用rtc_allocate_device, 并使用devm_add_action_or_reset注册清理动作 ...
}
EXPORT_SYMBOL_GPL(devm_rtc_allocate_device);

// __devm_rtc_register_device: 资源管理的RTC设备注册核心函数。
int __devm_rtc_register_device(struct module *owner, struct rtc_device *rtc)
{
// ... 检查ops指针, 根据ops能力设置/清除RTC特性位 ...

// 准备字符设备。
rtc_dev_prepare(rtc);
// 添加字符设备,使其在/dev下可见。
err = cdev_device_add(&rtc_char_dev, &rtc->dev);
// ...

// 添加procfs接口。
rtc_proc_add_device(rtc);

dev_info(rtc->dev.parent, "registered as %s\n",
dev_name(&rtc->dev));

#ifdef CONFIG_RTC_HCTOSYS_DEVICE
// 如果是主RTC,则执行hctosys。
if (!strcmp(dev_name(&rtc->dev), CONFIG_RTC_HCTOSYS_DEVICE))
rtc_hctosys(rtc);
#endif

// 注册一个devm动作,以便在驱动卸载时自动注销设备。
return devm_add_action_or_reset(rtc->dev.parent,
devm_rtc_unregister_device, rtc);
}
EXPORT_SYMBOL_GPL(__devm_rtc_register_device);

// rtc_init: RTC子系统的总初始化函数。
static int __init rtc_init(void)
{
// 注册 "rtc" 设备类。
err = class_register(&rtc_class);
// 初始化字符设备。
rtc_dev_init();

return 0;
}
subsys_initcall(rtc_init);

drivers/rtc/interface.c

RTC 工具函数集 (rtc_timer_init, rtc_tm_to_ktime, rtc_bound_alarmtime)

本代码片段包含一组 RTC(实时时钟)子系统的核心工具函数。它们提供了基础的数据结构初始化、时间格式转换以及硬件能力适配等功能。rtc_timer_init 用于初始化一个 rtc_timer 结构体,rtc_tm_to_ktime 负责将 RTC 硬件常用的日历时间格式转换为内核统一的 ktime_t 时间格式,而 rtc_bound_alarmtime 则用于将一个通用的闹钟请求时间调整到特定 RTC 硬件所能支持的最大范围内。

实现原理分析

  1. 时间基准的转换 (rtc_tm_to_time64): 该函数的核心是 mktime64,它实现了一个关键的算法:将格里高利历(公历)日期(年月日时分秒)转换为自 Unix 纪元(1970年1月1日 00:00:00 UTC)以来所经过的线性秒数。这个转换涉及到对年月日进行加权计算,并精确处理闰年的情况。函数在调用 mktime64 之前,对 tm->tm_year 加 1900 和 tm->tm_mon 加 1 进行了调整,这是因为 struct rtc_time 遵循 C 标准库 struct tm 的约定,即年份是从1900年起的偏移量,月份是 0-11。

  2. 硬件能力适配技巧 (rtc_bound_alarmtime): 此函数是一个典型的硬件抽象层适配技巧。它允许上层软件(如 alarmtimer_suspend)以统一的方式请求一个任意时长的闹钟,而无需关心底层硬件的具体限制。RTC 驱动在初始化时,可以根据其硬件手册,在 rtc->alarm_offset_max 字段中声明其能支持的最大闹钟偏移时长(以毫秒为单位)。rtc_bound_alarmtime 在设置闹钟前,会检查请求的时长是否超出了这个硬件限制。如果超出,它不会返回错误,而是“优雅降级”,将闹钟时长缩减到硬件所能支持的最大值。这种设计提高了系统的健壮性,确保即使在请求超出硬件能力时,系统仍能尽力设置一个最早可能被唤醒的闹钟。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// rtc_timer_init: 初始化一个 rtc_timer 结构体。
// @timer: 指向需要被初始化的 rtc_timer 结构体的指针。
// @f: 一个函数指针,当定时器触发时,该函数将被调用。其参数为触发该定时器的 rtc_device 指针。
// @rtc: 指向与此定时器关联的 rtc_device 结构体的指针。
void rtc_timer_init(struct rtc_timer *timer, void (*f)(struct rtc_device *r),
struct rtc_device *rtc)
{
// 初始化 timer 中的 timerqueue_node 成员,使其可以被添加到定时器队列中。
timerqueue_init(&timer->node);
// 将定时器的使能状态标志初始化为0(禁用)。
timer->enabled = 0;
// 将定时器触发时要执行的回调函数指针指向传入的函数 f。
timer->func = f;
// 将定时器关联的 rtc_device 指针指向传入的设备 rtc。
timer->rtc = rtc;
}

// rtc_tm_to_time64: 将 rtc_time 结构体表示的日历时间转换为 time64_t 类型的秒数。
// 该函数将公历日期转换为自1970年1月1日00:00:00 UTC以来的总秒数。
// @tm: 指向包含年月日时分秒信息的 rtc_time 结构体的指针。
// 返回值: time64_t 类型的秒数。
time64_t rtc_tm_to_time64(struct rtc_time *tm)
{
// 调用 mktime64 函数执行转换。
// tm->tm_year 是从1900年起的年数,故需加上1900。
// tm->tm_mon 是0-11的月份,故需加1以符合1-12的常规表示。
return mktime64(((unsigned int)tm->tm_year + 1900), tm->tm_mon + 1,
tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
}
// 将 rtc_tm_to_time64 函数导出,使其可以被其他内核模块调用。
EXPORT_SYMBOL(rtc_tm_to_time64);

// rtc_tm_to_ktime: 将 rtc_time 结构体表示的日历时间转换为内核标准的 ktime_t 时间格式。
// @tm: 包含年月日时分秒信息的 rtc_time 结构体(值传递)。
// 返回值: ktime_t 类型的时间值。
ktime_t rtc_tm_to_ktime(struct rtc_time tm)
{
// 首先调用 rtc_tm_to_time64 将日历时间转换为秒数,
// 然后使用 ktime_set 将秒数和0纳秒结合,创建一个 ktime_t 值。
return ktime_set(rtc_tm_to_time64(&tm), 0);
}
// 将 rtc_tm_to_ktime 函数导出,使其可以被其他内核模块调用。
EXPORT_SYMBOL_GPL(rtc_tm_to_ktime);

// rtc_bound_alarmtime: 根据RTC硬件的能力限制,调整并返回一个有效的闹钟时长。
// @rtc: 指向 rtc_device 结构体的指针。
// @requested: 上层软件请求的闹钟超时时长(ktime_t 格式)。
// 返回值: 一个不超过RTC硬件最大支持时长的 ktime_t 值。
static inline ktime_t rtc_bound_alarmtime(struct rtc_device *rtc,
ktime_t requested)
{
// 检查RTC驱动是否设置了最大闹钟偏移时长(alarm_offset_max,单位为毫秒)。
// 并且,检查请求的时长(转换为毫秒)是否大于硬件支持的最大时长。
if (rtc->alarm_offset_max &&
rtc->alarm_offset_max * MSEC_PER_SEC < ktime_to_ms(requested))
// 如果请求的时长超出了硬件限制,则返回硬件所能支持的最大时长。
return ms_to_ktime(rtc->alarm_offset_max * MSEC_PER_SEC);

// 如果未超出硬件限制,或者硬件没有设置限制,则直接返回原始请求的时长。
return requested;
}

RTC 定时器启动与入队 (rtc_timer_start, rtc_timer_enqueue)

本代码片段展示了 Linux 内核中 RTC(实时时钟)子系统用于启动和管理硬件定时器的核心逻辑。rtc_timer_start 提供了一个高层次的接口用于设置一个基于 RTC 的定时器,而 rtc_timer_enqueue 则是底层的实现函数,负责将定时器高效地加入管理队列,并根据需要对 RTC 硬件闹钟进行编程。

实现原理分析

此实现中包含了若干关键的技术和算法,以确保 RTC 定时器的高效、精确和健壮。

  1. 高效的定时器管理 (timerqueue): rtc_timer_enqueue 函数使用 timerqueue 数据结构来管理所有活动的 RTC 定时器。timerqueue 内部是基于红黑树实现的,这使得添加(timerqueue_add)和删除定时器的操作时间复杂度为 O(log N),而获取下一个即将到期的定时器(timerqueue_getnext)的操作时间复杂度为 O(1)。对于可能需要管理大量定时器的系统,这是一个至关重要的高效设计。

  2. 最小化硬件交互的优化: 在 rtc_timer_enqueue 函数中,if (!next || ktime_before(timer->node.expires, next->expires)) 语句是一个核心优化。它首先检查在添加新定时器 之前,队列中最早的定时器是哪一个(next)。只有当新加入的定时器比原来最早的定时器还要早到期时,它才会去调用 __rtc_set_alarm 来重新编程硬件。这种策略极大地减少了对 RTC 硬件寄存器的写操作次数,因为只有在更新“全局最早到期时间”时才需要与硬件交互,从而降低了系统开销和功耗。

  3. 过期闹钟的健壮处理 (-ETIME Erorr Handling): 当 __rtc_set_alarm 返回 -ETIME 错误时,意味着尝试设置的闹钟时间点已经过去。此时,代码并没有简单地返回失败,而是采取了一套恢复机制:

    • pm_stay_awake(): 调用此函数来创建一个唤醒锁,暂时阻止系统进入睡眠。
    • schedule_work(&rtc->irqwork): 将 RTC 的中断处理工作调度到一个工作队列(workqueue)中立即执行。
    • 这套机制确保了即使由于调度延迟等原因导致闹钟设置时间点已过,该闹钟事件也不会被丢失,而是会立即通过软件方式进行模拟触发和处理,保证了定时任务的可靠执行。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// rtc_timer_start: 设置一个RTC定时器,使其在未来的某个时间点触发。
// @rtc: 指向要使用的 rtc_device 结构体的指针。
// @timer: 指向需要被设置的 rtc_timer 结构体的指针。
// @expires: 定时器到期的绝对时间(ktime_t 格式)。
// @period: 定时器循环触发的周期(ktime_t 格式),如果为0则不循环。
// 返回值: 成功则返回0,失败则返回负数错误码。
int rtc_timer_start(struct rtc_device *rtc, struct rtc_timer *timer,
ktime_t expires, ktime_t period)
{
// 初始化返回值为0。
int ret = 0;

// 获取与RTC设备操作相关的互斥锁,以保护对定时器队列的访问。
mutex_lock(&rtc->ops_lock);
// 检查此定时器是否已经被启用。
if (timer->enabled)
// 如果已启用,则先将其从队列中移除。
rtc_timer_remove(rtc, timer);

// 设置定时器的到期时间。
timer->node.expires = expires;
// 设置定时器的循环周期。
timer->period = period;

// 调用内部函数,将配置好的定时器加入队列并设置硬件闹钟。
ret = rtc_timer_enqueue(rtc, timer);

// 释放互斥锁。
mutex_unlock(&rtc->ops_lock);
// 返回操作结果。
return ret;
}

// rtc_timer_enqueue: 将一个RTC定时器添加到设备的定时器队列中。
// @rtc: 指向 rtc_device 结构体的指针。
// @timer: 指向需要被添加的 rtc_timer 结构体的指针。
// 该函数在执行期间必须持有 rtc->ops_lock 锁。
// 返回值: 成功则返回0,失败则返回负数错误码。
static int rtc_timer_enqueue(struct rtc_device *rtc, struct rtc_timer *timer)
{
// 获取当前定时器队列中下一个(即最早)将到期的节点。
struct timerqueue_node *next = timerqueue_getnext(&rtc->timerqueue);
// 声明一个用于存储RTC硬件时间的 rtc_time 结构体。
struct rtc_time tm;
// 声明一个用于存储当前时间的 ktime_t 变量。
ktime_t now;
// 声明一个用于保存错误码的整型变量。
int err;

// 读取当前RTC硬件的精确时间。
err = __rtc_read_time(rtc, &tm);
// 如果读取失败,则直接返回错误码。
if (err)
return err;

// 将定时器的使能状态标志设为1。
timer->enabled = 1;
// 将读取到的RTC时间转换为内核标准的 ktime_t 格式。
now = rtc_tm_to_ktime(tm);

// 遍历并跳过队列中所有已经到期的定时器节点。
while (next) {
if (next->expires >= now)
break;
next = timerqueue_iterate_next(next);
}

// 将新的定时器节点添加到红黑树实现的定时器队列中。
timerqueue_add(&rtc->timerqueue, &timer->node);
// 记录定时器入队的追踪事件。
trace_rtc_timer_enqueue(timer);
// 检查新加入的定时器是否成为了队列中最早到期的那一个。
// (条件1: !next, 队列原为空;条件2: 新定时器的到期时间早于原队列中最早的那个)
if (!next || ktime_before(timer->node.expires, next->expires)) {
// 如果是,则需要重新设置RTC硬件闹钟。
struct rtc_wkalrm alarm;

// 将新定时器的ktime_t到期时间转换为RTC硬件所需的rtc_time格式。
alarm.time = rtc_ktime_to_tm(timer->node.expires);
// 使能闹钟。
alarm.enabled = 1;
// 调用底层函数,将闹钟时间编程到RTC硬件中。
err = __rtc_set_alarm(rtc, &alarm);
// 如果设置闹钟返回-ETIME,表示目标时间已经过去。
if (err == -ETIME) {
// 创建一个唤醒锁,暂时阻止系统进入睡眠状态。
pm_stay_awake(rtc->dev.parent);
// 调度RTC的中断处理工作队列,以软件方式立即处理这个“已到期”的闹钟。
schedule_work(&rtc->irqwork);
} else if (err) { // 如果是其他类型的错误。
// 从定时器队列中移除刚刚添加的节点,进行回滚。
timerqueue_del(&rtc->timerqueue, &timer->node);
// 记录定时器出队的追踪事件。
trace_rtc_timer_dequeue(timer);
// 将定时器的使能状态重新设为0。
timer->enabled = 0;
// 返回设置硬件闹钟时遇到的错误。
return err;
}
}
// 如果一切顺利,返回0表示成功。
return 0;
}

RTC 硬件闹钟设置与时间偏移校正 (__rtc_set_alarm, rtc_subtract_offset)

本代码片段揭示了 Linux 内核向 RTC(实时时钟)硬件编程闹钟的核心底层逻辑。__rtc_set_alarm 函数负责执行设置闹钟的完整流程,包括时间校验、防止设置过去时间、以及通过驱动操作硬件。rtc_subtract_offset 则是一个关键的辅助函数,用于处理那些时间表示范围有限的 RTC 硬件,通过软件偏移量实现对整个 time64_t 时间范围的“虚拟化”支持。

实现原理分析

此代码段包含了确保闹钟设置既健壮又兼容多种硬件的关键技术。

  1. 过去时间闹钟的预防与竞态窗口: __rtc_set_alarm 的一个核心设计原则是严禁设置一个已经过去的时间点作为闹钟。它通过以下步骤实现:
    a. 读取当前 RTC 的精确时间 now
    b. 将请求的闹钟时间 schedulednow 比较。
    c. 如果 scheduled <= now,则立即返回 -ETIME 错误。
    然而,代码注释(XXX - ... race window ...)明确指出了此机制的一个固有缺陷:在软件检查通过(scheduled > now)到真正通过 rtc->ops->set_alarm 写入硬件寄存器的这段极短时间内,真实时间可能恰好跨过下一秒的边界,使得 scheduled 变为过去时间。这是一个典型的竞态条件,虽然发生概率很低,但它揭示了在非原子操作中检查时间的理论局限性。

  2. 硬件时间范围的虚拟化 (rtc_subtract_offset): 许多(尤其是旧的)RTC 硬件芯片无法表示完整的 Unix 时间戳范围(例如,年份只用两位数表示)。rtc_subtract_offset 和与之配对的 rtc_add_offset 提供了一套精巧的软件解决方案来克服此限制:

    • 问题: 硬件只能表示例如从 1980 年到 2079 年的时间。
    • 解决方案: RTC 驱动在初始化时,可以声明其硬件的有效时间范围 [range_min, range_max],并计算出一个大的软件偏移量 offset_secs(例如,1980-01-01 相比 1970-01-01 的秒数)。
    • 写操作: 当 __rtc_set_alarm 准备向硬件写入一个时间 tm 时,它会调用 rtc_subtract_offset。该函数首先检查 tm 对应的秒数是否已经落在硬件的 [range_min, range_max] 范围内。如果是,则无需任何操作(这是一个优化)。如果超出了范围(例如,请求设置 2090 年的闹钟),函数就会从该时间的秒数中减去 offset_secs,将一个“绝对时间”转换为一个硬件可以理解的“相对时间”,然后再写入硬件。
    • 读操作: 反之,当从硬件读出时间时,rtc_add_offset 会被调用,将 offset_secs 加回去,从而将硬件的“相对时间”还原为内核使用的“绝对时间”。
      这套机制对上层是透明的,使其能够无差别地处理所有 RTC 硬件,无论其物理时间范围有多大。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// __rtc_set_alarm: 一个内部辅助函数,用于向RTC硬件设置一个唤醒闹钟。
// @rtc: 指向 rtc_device 结构体的指针。
// @alarm: 指向 rtc_wkalrm 结构体的指针,包含了要设置的闹钟时间和使能状态。
// 返回值: 成功则返回0,失败则返回负数错误码。
static int __rtc_set_alarm(struct rtc_device *rtc, struct rtc_wkalrm *alarm)
{
struct rtc_time tm; // 用于存储当前RTC时间的临时变量。
time64_t now, scheduled; // 用于存储当前时间和计划闹钟时间的64位秒数。
int err; // 用于存储错误码。

// 首先校验要设置的闹钟时间(alarm->time)是否是一个有效的日历时间。
err = rtc_valid_tm(&alarm->time);
if (err)
return err;

// 将要设置的闹钟时间转换为自Unix纪元以来的总秒数。
scheduled = rtc_tm_to_time64(&alarm->time);

// 关键检查:确保我们不会设置一个已经过去时间的闹钟。
// 读取当前RTC硬件的精确时间。
err = __rtc_read_time(rtc, &tm);
if (err)
return err;
// 将当前硬件时间也转换为总秒数。
now = rtc_tm_to_time64(&tm);

// 如果计划的闹钟时间早于或等于当前时间,则返回-ETIME错误。
if (scheduled <= now)
return -ETIME;
/*
* XXX - 这里存在一个竞态条件窗口:我们刚刚检查了闹钟时间不在过去,
* 但是,如果闹钟被设置为下一秒触发,而系统恰好在此时(在设置硬件之前)
* 发生了秒的翻转,那么设置的仍然会是一个过去的时间。
*/

// 如果RTC硬件的时间范围有限,则对闹钟时间应用一个软件偏移量进行校正。
rtc_subtract_offset(rtc, &alarm->time);

// 检查RTC设备的操作函数集是否存在。
if (!rtc->ops)
err = -ENODEV;
// 检查RTC设备的能力位图,确认其是否声明支持闹钟功能。
else if (!test_bit(RTC_FEATURE_ALARM, rtc->features))
err = -EINVAL;
else
// 调用底层硬件驱动提供的 set_alarm 函数来实际编程硬件闹钟。
err = rtc->ops->set_alarm(rtc->dev.parent, alarm);

// 记录设置闹钟的追踪事件,参数为闹钟时间的秒数和操作结果。
trace_rtc_set_alarm(rtc_tm_to_time64(&alarm->time), err);
// 返回最终的操作结果。
return err;
}

// rtc_subtract_offset: 在设置RTC时间之前,减去一个软件维护的偏移量。
// 这是为了兼容那些硬件时间表示范围有限的RTC芯片。
// @rtc: 指向 rtc_device 结构体的指针。
// @tm: 指向 rtc_time 结构体的指针,其时间值将被(可能)修改。
static void rtc_subtract_offset(struct rtc_device *rtc, struct rtc_time *tm)
{
time64_t secs; // 用于存储时间的64位秒数。

// 如果没有配置偏移量,则直接返回,不执行任何操作。
if (!rtc->offset_secs)
return;

// 将待处理的 rtc_time 转换为总秒数。
secs = rtc_tm_to_time64(tm);

/*
* 如果要设置的时间值已经落在RTC硬件的有效范围内,
* 那么在设置时间到RTC设备时就不需要减去偏移量。
* 否则,我们需要减去偏移量,以使时间值对于RTC硬件设备是有效的。
*/
if (secs >= rtc->range_min && secs <= rtc->range_max)
return;

// 如果时间超出了硬件的直接表示范围,则减去偏移量,
// 然后将新的秒数转换回 rtc_time 格式,更新传入的 tm 结构体。
rtc_time64_to_tm(secs - rtc->offset_secs, tm);
}

RTC 时间与闹钟的读取 (__rtc_read_time, rtc_read_alarm)

本代码片段包含两个 RTC(实时时钟)子系统的核心读取函数。__rtc_read_time 是一个内部函数,负责通过底层驱动从 RTC 硬件中读取当前的日历时间,并进行有效性校验。rtc_read_alarm 则是一个导出的外部接口,用于从 RTC 子系统的软件状态中查询当前设置的硬件闹钟信息。

实现原理分析

这两个函数展示了 Linux 设备驱动模型中的关键设计原则:硬件抽象和软件状态缓存。

  1. 驱动模型与硬件抽象 (__rtc_read_time): __rtc_read_time 的核心是通过 rtc->ops->read_time 这个函数指针来调用具体硬件驱动提供的功能。rtc->ops 指向一个 rtc_class_ops 结构体,其中包含了所有标准 RTC 操作的函数指针。这种设计将通用的 RTC 核心逻辑与特定硬件的实现完全解耦。RTC 核心不需要知道如何与某个具体的 RTC 芯片通信,它只需要调用这个标准的 read_time 接口。此外,函数在读取硬件数据后,会调用 rtc_valid_tm 进行严格的有效性检查(例如,确保月份在 0-11 之间,小时在 0-23 之间等),这增强了系统的健壮性,防止无效的硬件数据污染整个内核时间系统。

  2. 软件状态缓存与性能优化 (rtc_read_alarm): 与 __rtc_read_time 不同,rtc_read_alarm 并不直接调用硬件驱动的 read_alarm 函数来读取硬件寄存器。相反,它直接从 rtc->aie_timer 这个软件结构体中读取闹钟信息。aie_timer (Alarm Interrupt Enable timer) 是 RTC 核心层维护的一个软件副本,它代表了当前被编程到硬件中的那个闹钟。这种设计的原理是状态缓存:硬件只在必要时(即当一个新设置的闹钟比所有现有闹钟都更早到期时)才被更新。当外部需要查询闹钟时,直接读取这个软件缓存即可,避免了昂贵的、可能需要加锁的硬件I/O操作。这是一种重要的性能优化策略。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// __rtc_read_time: 一个内部辅助函数,用于从RTC硬件读取当前时间。
// @rtc: 指向 rtc_device 结构体的指针。
// @tm: 指向用于存储读取结果的 rtc_time 结构体的指针。
// 返回值: 成功则返回0,失败则返回负数错误码。
static int __rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm)
{
int err; // 声明一个用于存储错误码的整型变量。

// 检查RTC设备的操作函数集是否存在。
if (!rtc->ops) {
err = -ENODEV; // 如果不存在,则表示设备或驱动未正确初始化。
} else if (!rtc->ops->read_time) { // 检查具体的read_time函数指针是否存在。
err = -EINVAL; // 如果不存在,则表示该驱动不支持读取时间操作。
} else {
// 将目标结构体清零,以确保没有残留数据。
memset(tm, 0, sizeof(struct rtc_time));
// 调用底层硬件驱动提供的 read_time 函数来实际读取硬件时间。
err = rtc->ops->read_time(rtc->dev.parent, tm);
// 检查驱动函数的返回值。
if (err < 0) {
// 如果读取失败,记录一条调试信息。
dev_dbg(&rtc->dev, "read_time: fail to read: %d\n",
err);
// 返回驱动报告的错误码。
return err;
}

// 对从硬件读取的时间应用一个软件偏移量(如果已配置)。
rtc_add_offset(rtc, tm);

// 校验读取并调整后的时间是否有效(例如,小时数是否在0-23之间)。
err = rtc_valid_tm(tm);
if (err < 0)
// 如果时间无效,记录一条调试信息。
dev_dbg(&rtc->dev, "read_time: rtc_time isn't valid\n");
}
// 返回最终的操作结果。
return err;
}

// rtc_read_alarm: 读取当前设置的RTC硬件闹钟信息。
// @rtc: 指向 rtc_device 结构体的指针。
// @alarm: 指向用于存储闹钟信息的 rtc_wkalrm 结构体的指针。
// 返回值: 成功则返回0,失败则返回负数错误码。
int rtc_read_alarm(struct rtc_device *rtc, struct rtc_wkalrm *alarm)
{
int err; // 声明一个用于存储错误码的整型变量。

// 以可被信号中断的方式获取与RTC设备操作相关的互斥锁。
err = mutex_lock_interruptible(&rtc->ops_lock);
// 如果获取锁时被中断,则直接返回。
if (err)
return err;
// 检查RTC设备的操作函数集是否存在。
if (!rtc->ops) {
err = -ENODEV; // 如果不存在,则表示设备或驱动未正确初始化。
// 检查RTC设备的能力位图,确认其是否支持闹钟功能。
} else if (!test_bit(RTC_FEATURE_ALARM, rtc->features)) {
err = -EINVAL; // 如果不支持,则返回无效参数错误。
} else {
// 将目标结构体清零。
memset(alarm, 0, sizeof(struct rtc_wkalrm));
// 从RTC核心维护的软件状态(aie_timer)中读取闹钟的使能状态。
alarm->enabled = rtc->aie_timer.enabled;
// 从软件状态中读取闹钟的到期时间,并将其从ktime_t格式转换为rtc_time格式。
alarm->time = rtc_ktime_to_tm(rtc->aie_timer.node.expires);
}
// 释放互斥锁。
mutex_unlock(&rtc->ops_lock);

// 记录读取闹钟的追踪事件,参数为闹钟时间的秒数和操作结果。
trace_rtc_read_alarm(rtc_tm_to_time64(&alarm->time), err);
// 返回操作结果。
return err;
}
// 将 rtc_read_alarm 函数导出,使其可以被其他内核模块调用。
EXPORT_SYMBOL_GPL(rtc_read_alarm);

RTC 定时器取消与硬件闹钟禁用 (rtc_timer_cancel, rtc_timer_remove)

本代码片段提供了 Linux 内核中用于安全地取消和移除 RTC(实时时钟)定时器的核心功能。rtc_timer_cancel 是一个高层次的外部接口,它通过加锁来保证操作的原子性。其核心逻辑由内部函数 rtc_timer_remove 实现,该函数不仅将指定的定时器从软件管理队列中移除,还智能地判断是否需要重新编程或禁用硬件闹钟,以确保系统状态的正确性。

实现原理分析

此代码段最关键的设计思想在于最小化硬件交互的优化策略,这对于降低系统功耗和开销至关重要。

  1. 条件性硬件重编程 (rtc_timer_remove): rtc_timer_remove 的核心在于 if (next == &timer->node) 这个判断。在移除一个定时器之前,它首先通过 timerqueue_getnext 获取了当前队列中最早到期的定时器节点 next。如果被移除的定时器 timer 正是这个 next 节点,这意味着被取消的正是当前已编程到硬件中的那个闹钟。只有在这种情况下,代码才会继续执行后续的硬件操作:

    • 查找下一个替代者: 它会再次调用 timerqueue_getnext 来找出队列中新的、最早的定时器。
    • 更新硬件: 如果找到了一个新的最早定时器,它就会调用 __rtc_set_alarm 将这个新的时间点编程到硬件中。
    • 禁用硬件: 如果移除后队列变空(!next),则调用 rtc_alarm_disable 彻底禁用硬件闹钟中断。
      如果被移除的定时器不是队列中最早的那个,那么硬件中编程的闹钟(即原先的 next)仍然是正确的,因此无需任何硬件操作,函数直接返回。这个策略避免了每次取消定时器都去访问硬件,极大地提升了效率。
  2. 状态一致性管理: timer->enabled 标志位虽然简单,但对于维护状态一致性至关重要。rtc_timer_cancel 在加锁后首先检查此标志,避免了对一个已经被取消的定时器执行重复的移除操作,防止了可能导致的队列损坏或内核错误。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// rtc_timer_cancel: 一个供外部调用的接口,用于停止一个正在运行的RTC定时器。
// @rtc: 指向要使用的 rtc_device 结构体的指针。
// @timer: 指向需要被取消的 rtc_timer 结构体的指针。
void rtc_timer_cancel(struct rtc_device *rtc, struct rtc_timer *timer)
{
// 获取与RTC设备操作相关的互斥锁,以确保对定时器队列的独占访问。
mutex_lock(&rtc->ops_lock);
// 检查此定时器当前是否处于启用状态。
if (timer->enabled)
// 如果已启用,则调用内部函数将其从队列中移除并处理硬件状态。
rtc_timer_remove(rtc, timer);
// 释放互斥锁。
mutex_unlock(&rtc->ops_lock);
}

// rtc_timer_remove: 从设备的定时器队列中移除一个RTC定时器。
// @rtc: 指向 rtc_device 结构体的指针。
// @timer: 指向需要被移除的 rtc_timer 结构体的指针。
// 该函数在执行期间必须持有 rtc->ops_lock 锁。
static void rtc_timer_remove(struct rtc_device *rtc, struct rtc_timer *timer)
{
// 获取当前定时器队列中下一个(即最早)将到期的节点。
struct timerqueue_node *next = timerqueue_getnext(&rtc->timerqueue);

// 从红黑树实现的定时器队列中删除指定的定时器节点。
timerqueue_del(&rtc->timerqueue, &timer->node);
// 记录定时器出队的追踪事件。
trace_rtc_timer_dequeue(timer);
// 将定时器的使能状态标志清零。
timer->enabled = 0;
// 关键优化:检查被移除的定时器是否就是刚才队列中最早的那一个。
if (next == &timer->node) {
// 如果是,则意味着硬件闹钟需要被重新编程或禁用。
struct rtc_wkalrm alarm;
int err;

// 再次获取移除后队列中新的最早到期节点。
next = timerqueue_getnext(&rtc->timerqueue);
// 如果移除后队列为空。
if (!next) {
// 调用函数禁用RTC硬件闹钟。
rtc_alarm_disable(rtc);
return;
}
// 如果队列不为空,则用新的最早到期时间来设置硬件闹钟。
// 将新的最早到期时间从ktime_t格式转换为rtc_time格式。
alarm.time = rtc_ktime_to_tm(next->expires);
// 使能闹钟。
alarm.enabled = 1;
// 调用底层函数,将新的闹钟时间编程到RTC硬件中。
err = __rtc_set_alarm(rtc, &alarm);
// 如果设置闹钟返回-ETIME,表示目标时间已经过去。
if (err == -ETIME) {
// 创建一个唤醒锁,暂时阻止系统进入睡眠。
pm_stay_awake(rtc->dev.parent);
// 调度RTC的中断处理工作队列,以软件方式立即处理这个“已到期”的闹钟。
schedule_work(&rtc->irqwork);
}
}
}

// rtc_alarm_disable: 禁用RTC设备的硬件闹钟中断。
// @rtc: 指向 rtc_device 结构体的指针。
static void rtc_alarm_disable(struct rtc_device *rtc)
{
// 检查RTC设备的操作函数集、闹钟功能支持位以及具体的alarm_irq_enable函数指针是否存在。
if (!rtc->ops || !test_bit(RTC_FEATURE_ALARM, rtc->features) || !rtc->ops->alarm_irq_enable)
// 如果有任何一个不存在,则直接返回,不做任何操作。
return;

// 调用底层硬件驱动提供的 alarm_irq_enable 函数,传入 false 以禁用中断。
rtc->ops->alarm_irq_enable(rtc->dev.parent, false);
// 记录禁用闹钟中断的追踪事件。
trace_rtc_alarm_irq_enable(0, 0);
}

drivers/rtc/lib.c RTC 日期与时间转换及校验工具集 (rtc_time64_to_tm, rtc_valid_tm)

本代码片段是 Linux 内核 RTC 子系统的核心工具库,提供了一系列不依赖于具体硬件的、纯粹的日期和时间计算函数。其主要功能包括:在内核标准的 time64_t(自 Unix 纪元以来的秒数)和 RTC 硬件常用的 struct rtc_time(年月日时分秒的日历格式)之间进行精确转换;校验 struct rtc_time 的有效性;以及提供辅助计算(如计算某月天数)。

实现原理分析

此代码段的核心在于其高效且精确的日历算法,特别是从线性秒数到公历日期的转换。

  1. Neri and Schneider 历法转换算法 (rtc_time64_to_tm): 此函数没有采用传统的循环或试错法来确定年月日,而是实现了一个基于欧几里得仿射函数的精密算法。

    • 计算历法: 算法的核心思想是使用一个“计算历法”,其中年份从3月1日开始。这样做在数学上非常便利,因为闰年的额外一天(2月29日)被置于一年的末尾,从而使得在计算一年中的某一天时,无需预先判断当年是否为闰年。
    • 时间基准偏移: 算法首先将输入的 Unix 时间戳(从1970-01-01起)加上一个 719468 * 86400 秒的偏移量。719468 是从 0000-03-01 到 1970-01-01 的天数,这个操作将时间基准转换到了“计算历法”的起点。
    • 纯算术分解: 接下来,算法通过一系列精心选择的整数乘法、除法和取模运算,直接从总天数中解析出世纪、世纪中的年份、年内日、月和日。例如,146097 是一个400年周期(公历的完整闰年周期)的总天数 (97*366 + 303*365)。2939745ULL2141 等是根据月份长度分布推导出的“魔术数字”,用于通过乘法和移位来高效地计算出月份和日期。
    • 结果转换: 在计算历法下得到年月日后,算法最后通过简单的加减法将其转换回标准的公历表示。
  2. 查找表优化: rtc_month_daysrtc_year_days 函数使用了静态常量数组(rtc_days_in_monthrtc_ydays)作为查找表。这避免了复杂的 if-elseswitch 分支,使得计算某月天数或某日在一年中的序数成为一次高效的数组索引操作。

  3. 纳秒向上取整 (rtc_ktime_to_tm): 在将 ktime_t 转换为 rtc_time 时,代码中存在 if (ts.tv_nsec) ts.tv_sec++; 的逻辑。这是一个重要的取整策略:只要时间戳中包含任何非零的纳秒部分,就将其归入下一秒。这对于闹钟设置至关重要,它确保了闹钟的触发时间点不会早于请求的时间点,保证了行为的正确性。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// rtc_days_in_month: 一个静态常量数组,存储非闰年情况下每个月的天数(月份从0开始索引)。
static const unsigned char rtc_days_in_month[] = {
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};

// rtc_ydays: 一个二维静态常量数组,用作查找表,存储每年中每个月第一天之前累计的总天数。
// 第一行用于非闰年,第二行用于闰年。
static const unsigned short rtc_ydays[2][13] = {
/* 普通年份 */
{ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 },
/* 闰年 */
{ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 }
};

static inline bool is_leap_year(unsigned int year)
{
return (!(year % 4) && (year % 100)) || !(year % 400);
}

// rtc_month_days: 计算给定年份和月份的总天数。
// @month: 月份 (0-11)。
// @year: 完整的年份 (例如 2024)。
// 返回值: 该月的总天数。
int rtc_month_days(unsigned int month, unsigned int year)
{
// 从查找表中获取该月的基本天数,并判断:如果年份是闰年且月份是1(二月),则天数加1。
return rtc_days_in_month[month] + (is_leap_year(year) && month == 1);
}
// 将 rtc_month_days 函数导出,使其可以被其他内核模块调用。
EXPORT_SYMBOL(rtc_month_days);

// rtc_year_days: 计算给定的日期是该年中的第几天 (从1开始计数)。
// @day: 该月的第几天 (1-31)。
// @month: 月份 (0-11)。
// @year: 完整的年份 (例如 2024)。
// 返回值: 该年中的第几天 (1-366)。
int rtc_year_days(unsigned int day, unsigned int month, unsigned int year)
{
// 根据是否为闰年选择正确的查找表行,获取该月之前所有月份的总天数,然后加上当月的天数,最后减1(因为day是从1开始的)。
return rtc_ydays[is_leap_year(year)][month] + day - 1;
}
// 将 rtc_year_days 函数导出,使其可以被其他内核模块调用。
EXPORT_SYMBOL(rtc_year_days);

// rtc_time64_to_tm: 将 time64_t 类型的秒数转换为 rtc_time 结构体表示的日历时间。
// @time: 自1970年1月1日00:00:00 UTC以来的总秒数。
// @tm: 指向用于存储结果的 rtc_time 结构体的指针。
void rtc_time64_to_tm(time64_t time, struct rtc_time *tm)
{
int secs; // 用于存储一天内的秒数。

u64 u64tmp; // 64位无符号整型临时变量,用于防止乘法溢出。
// 32位无符号整型临时变量,及用于存储计算结果的变量。
u32 u32tmp, udays, century, day_of_century, year_of_century, year,
day_of_year, month, day;
// 用于存储布尔状态的变量。
bool is_Jan_or_Feb, is_leap_year;

// 将时间基准从1970-01-01转换到0000-03-01,以简化后续的历法计算。
// 719468 是从 0000-03-01 到 1970-01-01 的天数。
time += (u64)719468 * 86400;

// 从总秒数中计算出总天数(udays)和当天剩余的秒数(secs)。
udays = div_s64_rem(time, 86400, &secs);

// 计算星期几。0000-03-01 在公历前推中是星期三,所以加3。
tm->tm_wday = (udays + 3) % 7;

// 以下是 Neri and Schneider 算法,用于将总天数转换为年月日。
// 该算法在一个以3月为年首的“计算历法”中进行。

// 计算一个中间值,用于后续分解。
u32tmp = 4 * udays + 3;
// 计算从0年3月1日起的世纪数。146097是400年的总天数。
century = u32tmp / 146097;
// 计算在当前400年周期内的天数(除以4是因为每4年有1个闰日)。
day_of_century = u32tmp % 146097 / 4;

// 计算另一个中间值。
u32tmp = 4 * day_of_century + 3;
// 使用一个大的64位乘法来同时计算年和年内日,这是一种高效的算术技巧。
// 2939745ULL 是一个用于此计算的“魔术数字”。
u64tmp = 2939745ULL * u32tmp;
// 64位乘法结果的高32位是世纪内的年份。
year_of_century = upper_32_bits(u64tmp);
// 低32位通过一系列运算得到年内日。
day_of_year = lower_32_bits(u64tmp) / 2939745 / 4;

// 计算完整的公历年份(在计算历法下)。
year = 100 * century + year_of_century;
// 判断计算出的年份是否为闰年。
is_leap_year = year_of_century != 0 ?
year_of_century % 4 == 0 : century % 4 == 0;

// 再次使用算术技巧从年内日计算出月份和日期。
u32tmp = 2141 * day_of_year + 132377;
// 结果的高16位代表月份。
month = u32tmp >> 16;
// 低16位除以一个常数得到日期。
day = ((u16) u32tmp) / 2141;

// 在计算历法中,1月1日是第306天。判断当前日期是否属于计算年的“年尾”(即公历的1月或2月)。
is_Jan_or_Feb = day_of_year >= 306;

// 将计算历法下的年月日转换回标准的公历表示。
// 如果是1月或2月,公历年份需要加1。
year = year + is_Jan_or_Feb;
// 月份进行相应调整。
month = is_Jan_or_Feb ? month - 12 : month;
// 日期加1(因为计算结果是从0开始的)。
day = day + 1;

// 计算在公历下的年内日。
day_of_year = is_Jan_or_Feb ?
day_of_year - 306 : day_of_year + 31 + 28 + is_leap_year;

// 将计算结果填充到 rtc_time 结构体中。
// tm_year 是自1900年起的年份。
tm->tm_year = (int) (year - 1900);
tm->tm_mon = (int) month;
tm->tm_mday = (int) day;
// tm_yday 是从1开始计数的年内日。
tm->tm_yday = (int) day_of_year + 1;

// 从当天剩余的秒数中解析出时、分、秒。
tm->tm_hour = secs / 3600;
secs -= tm->tm_hour * 3600;
tm->tm_min = secs / 60;
tm->tm_sec = secs - tm->tm_min * 60;

// 默认不考虑夏令时。
tm->tm_isdst = 0;
}
// 将 rtc_time64_to_tm 函数导出。
EXPORT_SYMBOL(rtc_time64_to_tm);

// rtc_valid_tm: 校验一个 rtc_time 结构体中的日期和时间是否有效。
// @tm: 指向待校验的 rtc_time 结构体的指针。
// 返回值: 如果有效则返回0,否则返回-EINVAL。
int rtc_valid_tm(struct rtc_time *tm)
{
// 检查年月日时分秒的各个字段是否在其合法范围内。
// tm->tm_year 从70年 (Unix纪元) 开始有效。
if (tm->tm_year < 70 ||
tm->tm_year > (INT_MAX - 1900) || // 防止年份溢出。
((unsigned int)tm->tm_mon) >= 12 || // 月份必须是 0-11。
tm->tm_mday < 1 || // 日期必须大于等于1。
// 日期不能超过当月的实际天数。
tm->tm_mday > rtc_month_days(tm->tm_mon,
((unsigned int)tm->tm_year + 1900)) ||
((unsigned int)tm->tm_hour) >= 24 || // 小时必须是 0-23。
((unsigned int)tm->tm_min) >= 60 || // 分钟必须是 0-59。
((unsigned int)tm->tm_sec) >= 60) // 秒数必须是 0-59。
return -EINVAL;

return 0;
}
// 将 rtc_valid_tm 函数导出。
EXPORT_SYMBOL(rtc_valid_tm);

// rtc_tm_to_time64: 将 rtc_time 结构体表示的日历时间转换为 time64_t 类型的秒数。
// @tm: 指向包含日历时间信息的 rtc_time 结构体的指针。
// 返回值: time64_t 类型的秒数。
time64_t rtc_tm_to_time64(struct rtc_time *tm)
{
// 调用内核提供的 mktime64 函数来执行转换。
// 注意,tm->tm_mon 需要加1来匹配 mktime64 的参数要求 (1-12)。
return mktime64(((unsigned int)tm->tm_year + 1900), tm->tm_mon + 1,
tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
}
// 将 rtc_tm_to_time64 函数导出。
EXPORT_SYMBOL(rtc_tm_to_time64);

// rtc_tm_to_ktime: 将 rtc_time 结构体转换为内核标准的 ktime_t 时间格式。
// @tm: 包含日历时间信息的 rtc_time 结构体(值传递)。
// 返回值: ktime_t 类型的时间值。
ktime_t rtc_tm_to_ktime(struct rtc_time tm)
{
// 首先将日历时间转换为秒数,然后使用 ktime_set 将其包装为 ktime_t 类型(纳秒部分为0)。
return ktime_set(rtc_tm_to_time64(&tm), 0);
}
// 将 rtc_tm_to_ktime 函数导出。
EXPORT_SYMBOL_GPL(rtc_tm_to_ktime);

// rtc_ktime_to_tm: 将 ktime_t 时间格式转换为 rtc_time 结构体。
// @kt: ktime_t 类型的时间值。
// 返回值: 转换后的 rtc_time 结构体。
struct rtc_time rtc_ktime_to_tm(ktime_t kt)
{
struct timespec64 ts; // 用于存储转换后的秒和纳秒。
struct rtc_time ret; // 用于存储最终的 rtc_time 结果。

// 将 ktime_t 转换为 timespec64 结构。
ts = ktime_to_timespec64(kt);
// 向上取整:如果纳秒部分不为零,则秒数加1。
// 这确保了闹钟等事件不会早于请求的时间触发。
if (ts.tv_nsec)
ts.tv_sec++;
// 调用 rtc_time64_to_tm 将总秒数转换为 rtc_time 格式。
rtc_time64_to_tm(ts.tv_sec, &ret);
return ret;
}
// 将 rtc_ktime_to_tm 函数导出。
EXPORT_SYMBOL_GPL(rtc_ktime_to_tm);