15linux内核时间管理
根据所需执行的工作,定时器需要提供不同的特征,特别是最大可能的分辨率方面。本章讨论Linux内核提供的各种可能方案。
15.1 概述
15.1.1 定时器的类型
在内核版本2.6开发期间,内核的时间子系统有了惊人的发展。在最初的发布版中,定时器子系统只包括现在所谓的低精度定时器。本质上,低精度定时器是围绕时钟周期展开的
,该周期是每隔一定时间定期发生的。可以预定在某个周期激活指定的事件。扩展这个相对简单的框架的压力,主要来自于如下两方面。
- 电力有限的设备(如笔记本电脑、嵌入式设备等)在无事可做时,需要使用尽可能少的电能。如果运行一个周期性的时钟,那么在几乎无事可做时,也仍然必须提供时钟的周期信号。但如果该周期信号没有用户,它本质上就不需要运行。然而,为实现该时钟周期信号,系统需要不时地从较低功耗状态进入到较高功耗状态
- 面向多媒体的应用程序需要非常精确的计时功能,例如,为避免视频中的跳帧,或音频回放中的跳跃。这迫使我们提高现有的分辨率
为找到所有接触时间管理的开发者(和用户)都同意的良好的解决方案(事实上有很多此类方案),要花费很多年,提出很多内核补丁。当前的状态是很不同寻常的,内核目前支持两类差别很大的定时器
经典定时器
(classical timer):在内核的最初版本,就已经提供了此类定时器。其实现位于kernel/timer.c
中。提供的典型分辨率为4毫秒,但实际上取决于计算机时钟中断运行的频率。经典定时器也称作低精度(low-resolution)定时器,或定时器轮(timer wheel)定时器
。- 对许多应用程序来说,特别是面向媒体的应用,几毫秒的定时器分辨率不够用。实际上,最新的硬件提供了精确得多的计时手段,
可以达到纳秒级的分辨率
。在内核版本2.6开发期间,添加了一个额外的定时器子系统,可以利用这样的高精度定时器资源。新的子系统提供的定时器,通常称为高精度定时器
(high-resolution timer)
高精度定时器的一部分代码总是被编译到内核中,但只有设置了编译选项CONFIG_HIGH_RES_TIMERS
,才能提供比低精度定时器更高的精度。高精度定时器引入的框架,由低精度定时器重用(实际上,低精度定时器是基于高精度定时器的机制而实现的)
经典定时器是由固定的栅栏所框定的
,而高精度时钟事件在本质上可以发生在任意时刻
,如下图。除非启用了动态时钟特性,否则很可能时钟周期信号发出,但实际上并没有事件到期。与此相反,高精度时钟信号只在某些事件可能发生时,才会发出
为什么开发者不改进现存的定时器子系统,而开发一个全新的呢?实际上,有人试图采用该策略,但旧的定时器子系统成熟而健壮的结构,使得要在改进的同时维持其效率且不引入新的问题,并不是特别容易。对此问题的一些深入思考,可以在Documentation/hrtimers.txt
中找到
除了分辨率之外,内核在术语上还会区分下面两种定时器:
- 超时:表示将在一定时间之后发生的事件,但可以且通常都会在发生之前取消。例如,考虑网络子系统等待在一定时间内即将到达的分组。为处理这种情况,需要设置一个定时器,在预定的时间期限结束时到期。由于分组通常都会按时到达,定时器在实际过期之前被删除的可能性很大。此外,分辨率对此类定时器来说,不是很关键。在内核允许分组的确认在10秒内发送的时候,它实际上并不在意超时到底是发生在10秒,还是10.001秒
- 定时器:用于实现时序。例如,声卡驱动器可能想要按很短的周期间隔向声卡发送一些数据。此类定时器通常都会到期,而且与超时类定时器相比,需要高得多的分辨率
时间子系统的实现所采用的各种基础组件的概述。因为是概述,所以不是很精确,只是给出了时间测算领域所涉及的各种事项的一个概览,并对各种组件之间的交互方式做了一些说明:
真正的硬件在最底层。每个典型的系统都有几个设备,通常由时钟芯片实现,提供了定时功能,可以用作时钟。实际可用的硬件取决于具体的体系结构。例如,IA-32和AMD64系统有一个PIT(programmable interrupt timer,可编程中断计时器,由8253芯片实现),这是一个经典的时钟源,分辨率和稳定性一般。在上文讨论IRQ处理时提到了CPU局部的APIC(advanced programmable interrupt controller,高级可编程中断控制器),它的分辨率和稳定性要好得多。APIC适合充当高精度时间源,而PIT只适用于低精度定时器
硬件自然需要由体系结构相关代码来编程控制,但时钟源抽象(clock source abstraction)为所有硬件时钟芯片提供了一个通用接口
。本质上,该接口允许读取时钟芯片提供的运行计数器的当前值
周期性的事件不怎么符合上述运行计数器的模型,因而需要另一个抽象。时钟事件(clock event)是周期性事件的基础
。但时钟事件可以发挥更强大的作用。一些定时设备可以提供发生在任意的非规则时刻的事件。与周期性事件设备相对,它们称作单触发设备
(one-shot device)
高精度定时器机制基于时钟事件,而低精度定时器机制利用了周期性事件,而周期性事件可以直接基于低精度时钟,或在高精度时间子系统之上构建
。低精度定时器承担了如下两个重要的任务。
- 处理全局jiffies计数器。该值周期性地增长(或至少从内核的大部分看来,它是在周期性增长),它表示了一种特别简单的时间基准。(对jiffies值的更新,并不容易在低精度和高精度框架之间区分开来,因为根据内核的配置,二者都有可能更新jiffies值)
- 进行各进程统计。这也包括了对经典的低精度定时器的处理,这种定时器可以关联到任意进程。
15.1.2 配置选项
内核中不仅有两个不同(但有关)的定时子系统,而且动态时钟
特性会使情况更为复杂。通常,周期时钟在内核的整个生命周期内都是活动的。这在缺乏电力的系统上可能是浪费,主要的例子如笔记本电脑或便携式计算机。如果有一个周期性事件在活动,那么系统决不会长时间进入省电模式。因而内核允许配置动态时钟(习惯上,也将启用了该配置选项的系统称为无时钟(tickless)系统),它不需要周期信号
。由于这使得定时器的处理复杂化,我们从现在开始假定未启用该特性
内核可以实现4种计时方案
。尽管这个数目听起来不大,但在许多任务根据选定的配置可能以4种方式实现时,理解时间相关代码的工作显然没有被简化
根据两个集合,每个集合两个成员,来计算4种可能性并不复杂。但重要的是意识到,高/低精度和动态/周期时钟的所有组合都是有效的,内核都需要考虑
15.2 低精度定时器的实现
由于低精度定时器
在内核中已经存在多年,用于数百处,我们首先阐述其实现。在下文中,假定内核使用周期时钟
。如果使用了动态时钟,情况会更为复杂,将在15.5节讨论该情形
15.2.1 定时器激活与进程统计
对于定时器的时间基线,内核会使用处理器的时钟中断或其他任何适当的周期性时钟源
。在IA-32与AMD64系统上,PIT或HPET(High Precision Event Timer,高精度事件定时器)可用于该目的。几乎所有比较现代的此类系统都具备HPET,如果HPET可用,则将优先采用。(但可以通过内核的命令行选项hpet=disable来禁用HPET
) 中断将定期发生,刚好是每秒HZ次
。HZ由一个体系结构相关的预处理器符号定义,在<asm-arch/param.h>
头文件中。其值可以在编译时通过配置选项CONFIG_HZ
来设置
HZ = 250用作大多数机器类型的默认值
,特别是在普遍存在的IA-32与AMD64体系结构上
在启用动态时钟时,也定义(并使用)了HZ,因为它是许多计时任务的基本量。在一个繁忙的系统上,总有一些非平凡的工作(不同于idle进程)需要完成,动态和周期时钟在表面上没什么区别。只有在近乎于无事可做,而且可以跳过一些时钟中断时,我们才能看到二者的差别。
通常,较高的HZ值使得系统具有更好的交互性和响应速度
,特别是,每个时钟中断时都会调用调度器
。缺点是,因为定时器例程调用得更频繁,有更多的系统工作需要完成:在HZ增高的同时,内核的一般性开销也会随之增高。这样,较大的HZ值比较适合于桌面系统和多媒体系统,而较低的HZ值更适合于服务器和批处理机器,这种场合下交互性属于次要因素
内核2.6系列的早期版本直接挂钩到时钟中断,来开始定时器的激活和进程的统计,但随着通用时钟框架的引入,在一定程度上又增加了复杂性。IA-32和AMD64机器上情况的概述如下:
其他体系结构的细节有所不同,但原理是一致的。(特定的体系结构上进行处理的方式通常是,在系统启动时调用time_init初始化基本的低精度计时设施
。)周期时钟设置为每秒运行HZ个周期
。IA-32将timer_interrupt注册为中断处理程序,而AMD64使用的是timer_event_interrupt
。这两个函数都通过调用所谓的全局时钟(参见15.3节)的事件处理程序,来通知内核中通用的、体系结构无关的时间处理层。根据使用的计时模型不同,会采用不同的处理程序函数。无论如何,该处理程序都通过调用以下两个函数,使得周期性低精度计时设施开始运作
do_timer
负责全系统范围的、全局性的任务:更新jiffies值,处理进程统计。在多处理器系统上,会选择一个特定的CPU来执行这两个任务,而不涉及其他CPUupdate_process_times
需要由SMP系统上的每个CPU执行。除了进程统计之外,它还激活了所有注册的经典低精度定时器并使之到期,并向调度器提供时间感知。因为这些主题都值得单独进行讨论(而且与本节其余的内容关系不大),所以将在15.8节详细讲述。这里我们只关注定时器的激活和过期,这是通过调用run_local_timers
触发的。该函数又引发了软中断TIMER_SOFTIRQ,而其处理程序函数负责运行低精度定时器
全局变量jiffies_64
(一个整型变量,在所有体系结构上都是64位),记录了系统启动以来时钟中断的准确数目.内核中还有一个历史遗留的变量 jiffies.
内核使用一种技巧,来防止在两个不同的时间基准之间转换时引入精度损失。jiffies和jiffies_64的低32位是重合的,指向同一块内存或同一个寄存器
。为达到这一目的,这两个变量是分别声明的,但用于联编最终的内核二进制映像的链接器脚本中,指定了jiffies等同于jiffies_64的低位4个字节,根据底层体系结构的字节序不同,这可能是jiffies_64的前4个或后4个字节。在64位机器上,这两个变量是同义词
切记:以jiffies为单位指定的时间,和jiffies变量本身,可能的奇异之处
,将在接下来的15.2.2节讨论
-
timer_event_interrupt AMD64时钟中断处理函数
- global_clock_event->event_handler 之后的流程和IA32相同
-
timer_interrupt IA32时钟中断处理函数
- do_timer_interrupt_hook 处理时钟中断
- global_clock_event->event_handler 低精度定时器为tick_handle_periodic
- tick_periodic 时钟更新,触发时钟软中断
- do_timer 负责全系统范围的、全局性的任务:更新jiffies值,处理进程统计。在多处理器系统上,会选择一个特定的CPU来执行这两个任务,而不涉及其他CPU
- 更新 jiffies_64
- update_times 处理每个时钟中断都必须执行的其余操作
- update_wall_time 更新wall time,它指定了系统已经启动并运行了多长时间.wall clock使用了人类可读格式(纳秒)来表示当前时间
- calc_load 更新系统负载统计,确定在前1分钟、5分钟、15分钟内,平均有多少个就绪状态的进程在就绪队列上等待.该状态可以使用w命令输出
- update_process_times 需要由SMP系统上的每个CPU执行。除了进程统计之外,它还激活了所有注册的经典低精度定时器并使之到期,并向调度器提供时间感知。这里我们只关注定时器的激活和过期,这是通过调用run_local_timers触发的。该函数又引发了软中断TIMER_SOFTIRQ来管理运行低精度定时器
- do_timer 负责全系统范围的、全局性的任务:更新jiffies值,处理进程统计。在多处理器系统上,会选择一个特定的CPU来执行这两个任务,而不涉及其他CPU
- tick_periodic 时钟更新,触发时钟软中断
- global_clock_event->event_handler 低精度定时器为tick_handle_periodic
- do_timer_interrupt_hook 处理时钟中断
15.2.2 处理jiffies
jiffies提供了内核中一种简单形式的低精度时间管理方式。尽管概念简单,但在读取该变量的值,或比较按jiffies指定的时间时,有些问题需要注意
由于jiffies_64在32位系统上是一个复合变量,它不能直接读取,而只能用辅助函数 get_jiffies_64 访问
。这确保在所有系统上都能返回正确的值
-
比较时间
为比较事件的时序关系,内核提供了几个辅助函数,使用这些函数替代自行编写的比较函数,可防止所谓的off-by-one错误(a、b、c表示一些事件的jiffie时间值)。//include/linux/jiffies.h //返回true,如果时间a在时间b之后。 #define time_after(a, b) //返回true,如果时间a在时间b之前 #define time_before(a,b) //与 time_after 类似,但在两个时间相等时也返回true #define time_after_eq(a,b) //与 time_before 类似,但在两个时间相等时也返回true #define time_before_eq(a,b) //检查时间a是否包含在[b, c]时间间隔内。范围是包含边界的,因而a等于b或c也会返回true #define time_in_range(a,b,c)
使用这些函数,可以确保正确处理jiffies计数器的回绕问题。通常,内核代码不应该直接比较时间值,而应该使用这些函数。
尽管比较由jiffies_64给出的64位时间问题较少,但内核针对64位时间提供了上述函数。除了time_in_range以外,只要向其他函数名增加_64后缀,即可得到处理64位时间值的函数变体
-
时间换算
就时间间隔而言,jiffies在大多数程序员心里不是首选单位。对较短的时间间隔,更传统的方式是按照毫秒或微秒度量。因而内核提供了一些辅助函数,在这些单位和jiffies之间来回转换:
//include/linux/jiffies.h unsigned int jiffies_to_msecs(const unsigned long j); unsigned int jiffies_to_usecs(const unsigned long j); unsigned long msecs_to_jiffies(const unsigned int m); unsigned long usecs_to_jiffies(const unsigned int u);
这些函数的语义是自明的。15.2.3节还分别给出了
jiffies和struct timeval以及struct timespec
之间的转换函数
15.2.3 数据结构
本节将详细讲述低精度定时器的实现
。处理过程是由run_local_timers
发起的,但在讨论该函数之前,还需要介绍一些数据结构,作为讨论的基础
定时器按链表组织
,以下数据结构表示链表上的一个定时器:
//include/linux/timer.h
//定时器结构体
struct timer_list {
struct list_head entry;//链表元素,到期时间相同的定时器连接起来,链表头为 tvec_t_base_s 的tv1-5
unsigned long expires;//定时器到期的时间,单位是jiffies,可能是时间间隔
void (*function)(unsigned long);//超时函数指针
unsigned long data;//超时函数的参数
struct tvec_t_base_s *base;//指向一个基元素,其中的定时器按到期时间排序.系统中的每个处理器对应于一个基元素,因而可使用base确定定时器在哪个CPU上运行
};
//定义并初始化一个静态的timer_list实例。
#define DEFINE_TIMER(_name, _function, _expires, _data) \
struct timer_list _name = \
TIMER_INITIALIZER(_function, _expires, _data)
时间在内核中以两种格式给出,偏移量或绝对值
。二者都利用了jiffies。在安装一个新定时器时使用了偏移量
,而所有内核数据结构都使用的是绝对值
,因为这样可以与当前jiffies时间轻易进行比较。timer_list的expires成员也使用了绝对时间,而非偏移量
因为在定义时间间隔时程序员习惯于按秒而不是HZ单位来思考,内核提供了一个匹配的数据结构,还可以将其转换为jiffies(当然,还可以反向转换)
//include/linux/time.h
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
完整的时间间隔,通过将指定的秒和微秒的值加起来计算可得。timeval_to_jiffies
和jiffies_to_timeval
函数用于在这种表示和jiffies
值之间转换。这两个函数声明在<jiffies.h>
中,实现在time.c
中
另一种指定时间的可能方法是,使用纳秒而不是微秒:
//include/linux/time.h
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
仍然有辅助函数可以在jiffies
和timespec
之间来回转换:timespec_to_jiffies
和jiffies_to_timespec
15.2.4 动态定时器
内核需要数据结构来管理系统中注册的所有定时器
(这些定时器可能分配给某个进程或内核本身)。该结构必须容许快速而高效地检查到期的定时器,以免消耗太多CPU时间。毕竟,每个时钟中断都必须进行这样的检查
(尽管选择的数据结构与预期目的很适合,但它对高精度定时器来说太低效,后者需要更好的组织)
-
定时器核心算法工作原理
以一个简化的例子来说明定时器管理的原理,因为内核使用的算法比初看起来要更复杂。(复杂性带来了更高的性能,这些是比较简单的算法和数据结构所不能达到的。)数据结构中不仅必须包含管理定时器所需的全部信息,(暂时忽略与进程相关的间隔定时器所需的额外数据)而且它必须能够很容易地进行周期性的扫描,以便执行到期的定时器并删除。下图说明了内核管理定时器的方式
主要的困难在于扫描即将到期和刚刚到期的定时器链表。因为只是将所有timer_list实例简单地串联在一起是不够的,
内核创建了不同的组,根据定时器的到期时间进行分类。分类的基础是一个主数组,有5个数组项,都是数组。主数组的5个位置根据到期时间对定时器进行粗略的分类。第一组是到期时间在0到255(或2^8^-1)个时钟周期之间的所有定时器。第二组包含了到期时间在256和2^8+6^-1 = 2^14^-1个时钟周期之间的所有定时器。第三组中定时器的到期时间范围是从2^14^到2^8+2×6^-1个时钟周期
依次类推。主表中的各项,称为组(group),有时又称为桶(bucket)。下表列出了各个定时器组的时间间隔。这里以普通系统上桶的大小作为计算的基础。在内存较少的小型系统上,时间间隔有所不同每个组本身由一个数组组成,定时器在其中再次排序。第一个组的数组有256个数组项,每个位置表示0到255个时钟周期之间一个可能的到期时间。如果系统中有几个定时器的到期时间相同,它们通过一个标准的双链表连接起来(链表元素为timer_list的entry成员)
其余的组
也由数组组成,但数组项数目较少,是64个
。数组项包含的是timer_list的双链表。但每个数组项包含的timer_list的expires值不再只有一个,而是一个时间间隔
。间隔的长度与组是相关的
。对第二组来说,每个数组项可容许的时间间隔为256 = 28个时钟周期,而对第三组来说是214个时钟周期,对第四组来说是220,对第五组来说是226。在我们考虑定时器是如何随着时间的推移而最终执行以及相关的数据结构如何改变的时候,上述这些时间间隔的意义就很清楚了定时器是如何执行的呢?
内核主要负责关注第一组的定时器
,因为这些定时器都将在稍后到期。为简单起见,我们假定每组都有一个计数器,存储了某个数组位置的编号(实际的内核实现在功能上是等效的,但结构上的清晰程度要差得多)第一组中的索引项指向的数组元素
,保存了稍后即将执行的各定时器的timer_list实例。每当遇到一个时钟中断时,内核都扫描该链表,执行所有定时器函数,并将索引位置加1。刚执行过的定时器则从数据结构移除。下一次发生时钟中断时,将执行新的数组位置上的定时器,并将其从数据结构移除,同样将索引加1,依次类推。在所有项都处理之后,索引值为255。因为这里的加法是模256的,因而索引将恢复到初始位置(位置0)
因为第一组的内容在最多256个时钟周期之后就会耗尽,必须将后续各组的定时器依次前推,重新补足第一组。
在第一组的索引位置恢复到初始位置0之后,会将第二组中一个数组项的所有定时器补充到第一组
。这种做法,解释了为什么各组选择了不同的时间间隔。因为第一组的各数组项可能有256个不同的到期时间,而第二组中一个数组项的数据就足以填充第一组的整个数组。该道理同样适用于后续各组。第三组的一个数组项的数据同样足以填充整个第二组,第四组的一个数组项也足以填充整个第三组,而第五组的一个数组项也足以填充整个第四组。
后续各组的数组位置并非随机选择的,其中的索引项仍然发挥了作用。但索引项的值不再是每个时钟周期加1,而是每256i-1个时钟周期加1,其中i是组的编号(i从0开始)
例子
:从第一组的处理开始已经过了256个jiffies,此时索引重置为0。同时,第二组的第一个数组项的内容将补充到第一组中。我们假定在第一组索引重置时,jiffies系统计时器的值为10 000。在第二组的第一个数组项中,有一个定时器链表,各定时器分别在时钟周期10001、10015、10015、10254到期。这些定时器分别会定位到第一组的1、15和254,在位置15会创建一个链表,包括两个指针,因为这两个定时器同时到期。在复制完成后,将第二组的索引位置加1。
循环接下来重新开始。在每个时钟周期会逐一处理第一组的各个索引位置上的定时器,直至到达索引位置255。接下来,用第二组的第二个数组元素中的所有定时器,来补充第一组。在第二组的索引位置到达63时(从第二组开始,每组只包含64个数组项),则使用第三组第一个数组项的内容来补充第二组。最后,在第三组的索引位置到达最大值时,从第四组取得新的数据;同样的原则,也适用于第五组到第四组的数据传输
为确定哪些定时器已经到期,内核无须扫描一个巨大的定时器链表,处理范围仅限于第一组中的一个数组项。因为该位置通常是空的或仅包含一个定时器,检查可以很快进行。偶尔从后续的各组向前复制数据甚至也不需要多少CPU时间,因为复制可以通过指针操作高效进行(内核无须复制内存块,而只需将指针设置为新值,如同标准链表函数那样)。 -
数据结构
上述各组的内容是通过两个简单的数据结构生成的,其不同之处很少://kernel/timer.c //内核管理定时器的后63组使用的组项数据结构体 typedef struct tvec_s { struct list_head vec[TVN_SIZE];//TVN_SIZE默认为64,缺乏内存的系统可设置配置选项BASE_SMALL,此时TVN_SIZE为16 } tvec_t; //内核管理定时器的第1组使用的组项数据结构体 typedef struct tvec_root_s { struct list_head vec[TVR_SIZE];//TVR_SIZE默认为256,缺乏内存的系统可设置配置选项BASE_SMALL,此时TVR_SIZE为64 } tvec_root_t;
系统中的每个处理器都有自身的数据结构,来管理运行于其上的定时器。下列数据结构的一个各CPU实例,用作根数据项:
//kernel/timer.c //定时器管理结构体,每个cpu定义了一个该结构的实例 struct tvec_t_base_s { spinlock_t lock; struct timer_list *running_timer; //记录了一个时间点(单位为jiffies),该结构中此前到期的定时器都已经执行,如,该值为10 500,那么内核就知道,jiffies值10 499及之前到期的定时器都已经执行过了。通常,timer_jiffies等于jiffies或比jiffies小1。如果内核有一段时间无法执行定时器(系统负荷非常高),二者的差值可能会稍大一点。 unsigned long timer_jiffies; //tv1-5为5组定时器组 tvec_root_t tv1; tvec_t tv2; tvec_t tv3; tvec_t tv4; tvec_t tv5; } ____cacheline_aligned; typedef struct tvec_t_base_s tvec_base_t; //每个CPU一个定时器管理结构体 tvec_base_t boot_tvec_bases; static DEFINE_PER_CPU(tvec_base_t *, tvec_bases) = &boot_tvec_bases;
-
定时器核心算法的实现和处理到期的定时器 __run_timers
对所有定时器的处理都由update_process_times
发起,它会调用run_local_timers
函数该函数将使用raise_softirq(TIMER_SOFTIRQ)
来激活定时器管理软中断,在下一个可能的时机执行(因为软中断不能直接处理,所以也可能经过若干jiffies,此期间内核没有处理任何定时器。因而,有时候定时器可能激活较迟,但决不可能过早激活)。run_timer_softirq
用作该软中断的处理程序函数,它会选择特定于CPU的structtvec_t_base_s实例,并调用__run_timers
-
update_process_times 在 tick_periodic 中被调用,调用源头是硬件时钟中断的处理函数
- run_local_timers
- raise_softirq(TIMER_SOFTIRQ) 触发软中断
- raise_softirq_irqoff
- wakeup_softirqd 唤醒软中断处理守护进程
- raise_softirq_irqoff
- raise_softirq(TIMER_SOFTIRQ) 触发软中断
- run_local_timers
-
ksoftirqd 软中断守护进程处理函数
- do_softirq 处理软中断
- __do_softirq
- h->action(h) 执行具体的软中断函数(这里为 run_timer_softirq 定时器软中断处理函数)
- __do_softirq
- do_softirq 处理软中断
-
run_timer_softirq 定时器软中断处理函数
- __run_timers 处理特定CPU上的定时器调度算法
__run_timers实现了前面描述的算法
。但我们在上面给出的数据结构中,并没有发现算法描述中提到的索引位置!内核并不需要一个显式的变量来记录该信息,所有必要的信息都已经包含在base的timer_jiffies成员中(base指tvec_t_base_s的全局实例,该实例是一个per-CPU变量)。为此定义了下列宏://kernel/timer.c //在小型系统上(通常是嵌入式系统)可以定义配置选项 CONFIG_BASE_SMALL ,为各数组分配较少的数组项,来节省一些空间。定时器实现的其他方面不受该选项的影响 //第一组的索引位置可通过base->timer_jiffies & TVR_MASK计算。int index = base->timer_jiffies & TVR_MASK; #define TVN_BITS (CONFIG_BASE_SMALL ? 4 : 6) #define TVR_BITS (CONFIG_BASE_SMALL ? 6 : 8) #define TVN_SIZE (1 << TVN_BITS) #define TVR_SIZE (1 << TVR_BITS) #define TVN_MASK (TVN_SIZE - 1) #define TVR_MASK (TVR_SIZE - 1) //计算定时器组N的索引值(第二组的N值为0) #define INDEX(N) ((base->timer_jiffies >> (TVR_BITS + (N) * TVN_BITS)) & TVN_MASK)
在小型系统上(通常是嵌入式系统)可以定义配置选项
BASE_SMALL
,为各数组分配较少的数组项,来节省一些空间。定时器实现的其他方面不受该选项的影响第一组的索引位置
可通过base->timer_jiffies & TVR_MASK计算//kernel/timer.c //低精度定时器处理算法 static inline void __run_timers(tvec_base_t *base) { ... int index = base->timer_jiffies & TVR_MASK; ... }
- __run_timers 实现定时器处理算法
- 获取第一组定时器的索引位置
- 处理经过多个 jiffies 到期的数组移动处理 cascade
- 根据索引位置从链表中移出到期要处理的所有定时器
- 执行所有到期的定时器处理函数
-
-
添加新定时器 add_timer
在安装新定时器时,必须区分这是来自内核自身的需求,还是用户空间应用程序的需求。首先讨论内核定时器的机制,因为用户定时器也基于该机制建立add_timer用于将一个完全设置好的timer_list实例插入到上述数据结构中:
//include/linux/timer.h //用于将一个完全设置好的timer_list实例插入到 tvec_base_t 数据结构中 static inline void add_timer(struct timer_list *timer) { BUG_ON(timer_pending(timer)); __mod_timer(timer, timer->expires); }
- add_timer 将一个完全设置好的timer_list实例插入到对应链表中
- __mod_timer
- internal_add_timer 将新定时器放入到数据结构中的正确位置上
- __mod_timer
- add_timer 将一个完全设置好的timer_list实例插入到对应链表中
15.3 通用时间子系统
低精度定时器在很大范围都能发挥作用,对很多可能的情况处理得很好。但这种广泛性,使得对高精度定时器的支持复杂化。多年的开发经验证明很难将其集成到现存的框架中。因而内核支持了另一种定时机制
虽然低精度定时器使用jiffies作为时间的基本单位,但高精度定时器使用人类的时间单位,即纳秒
。这是合理的,因为高精度定时器大多用于用户层应用程序
,而程序员很自然会使用人类的单位来考虑时间问题。而最重要的一点是,1纳秒是一个精确定义的时间间隔,而一个jiffy或时钟周期的长度是根据内核配置而定的
内核的第二个定时器子系统的核心实现
可以在kernel/time/hrtimer.c
中找到。作为高精度定时器集成的通用计时代码
,位于kernel/time
下的几个文件中。我们首先概述所使用的机制,接下来介绍高精度定时器引入的新API,然后详细考察其实现
15.3.1 概述
通用时间系统的概览如下,它是高精度定时器的基础
子系统的各个组件和数据结构涉及3种机制,形成了内核中任何与时间相关的任务的基础
时钟源
(由struct clocksource
定义):时间管理的支柱。本质上每个时钟源都提供了一个单调增加的计数器,通用的内核代码只能进行只读访问。不同时钟源的精度取决于底层硬件的能力时钟事件设备
(由struct clock_event_device
定义):向时钟增加了事件功能,在未来的某个时刻发生。请注意,由于历史原因,这种设备通常也称为时钟事件源
(clock event source)。时钟设备
(由struct tick_device
定义):扩展了时钟事件源的功能,提供一个时钟事件的连续流,各个时钟事件定期触发。但可以使用动态时钟机制,在一定时间间隔内停止周期时钟
内核区分如下两种时钟类型:
全局时钟
(global clock),负责提供周期时钟,主要用于更新jiffies值。在此前的内核版本中,此类型时钟在IA-32系统上是由PIT实现的,在其他体系结构上由类似芯片实现- 每个CPU一个
局部时钟
(local clock),用来进行进程统计、性能剖析和实现高精度定时器
全局时钟的角色,由一个明确选择的局部时钟承担
。请注意,高精度定时器只能工作于提供了各CPU时钟源的系统上
。否则,处理器之间的大量通信将大大降低系统性能,这是高精度定时器的作用所不能弥补的
在AMD64和IA-32(MIPS平台也受到影响)平台
上的问题,使得整体的概念复杂化。SMP系统上的局部时钟基于APIC芯片
。遗憾的是,这种时钟能否正确工作,取决于系统所处的电源模式。对于低功耗模式(确切地说,ACPI模式C3),将停用局部APIC定时器
,因而无法作为时钟源使用
。在这种电源管理模式下,系统全局时钟仍然处于工作状态,将用于周期性地激活信号,使之看起来仍然来自于原来的时钟源
。这种规避方案称为广播机制
,在15.6节阐述。
由于广播需要CPU之间的通信,与专门的局部时钟源相比,这种解决方案要慢,且不那么精确。内核会自动将定时器由高精度模式切换回低精度模式
。
15.3.2 配置选项
定时器实现受到几个配置选项的影响。在编译时间,有如下两种可能的选择:
- 内核在联编时,可以选择支持或不支持动态时钟。如果启用了
动态时钟
,将设置预处理器常数CONFIG_NO_HZ
。 - 可以启用或禁用
高精度定时器
支持。如果要提供支持,将启用预处理器符号CONFIG_HIGH_RES_TIMERS
由于二者彼此独立的,这导致时间和定时器子系统有4种不同配置
另外,每个体系结构都需要进行一些配置选择。这些不受用户影响:
GENERIC_TIME
表示体系结构支持通用时间框架
。GENERIC_CLOCKEVENTS
表示体系结构支持通用时钟事件
。因为二者都是支持动态时钟和高精度定时器的必要前提
,所以我们只考虑提供这两种特性的体系结构(当前正在向通用时钟事件框架迁移的体系结构,可以设置GENERIC_CLOCKEVENTS_MIGR
。这将联编相关的代码,但在运行时不使用)。实际上大多数广泛使用的体系结构都已经更新为支持这两个选项,即使某些体系结构(如SuperH)只对特定的时间模型提供支持CONFIG_TICK_ONESHOT
用于支持时钟事件设备的单触发模式
。如果启用了高精度定时器或动态时钟,会自动选中该选项
- 如果体系结构受困于
省电模式
的问题而需要广播
,那么必须定义GENERIC_CLOCKEVENTS_BROADCAST
。当前该问题只影响IA-32、AMD64和MIPS
15.3.3 时间表示 ktime_t和与其他时间结构体间的转换函数
通用时间框架
使用数据类型ktime_t
来表示时间值。无论在何种底层体系结构下,该类型都是一个64位量
。
为减少32位机器上的工作量,该结构的定义确保两个32位值的排序能够被立即直接解释为一个64位值,显然这需要根据处理器的字节序不同而对字段进行不同的排序
//include/linux/ktime.h
//通用时间框架表示时间值的结构体,64位,32位机上使用两个32位表示来减少工作量
union ktime {
s64 tv64;
#if BITS_PER_LONG != 64 && !defined(CONFIG_KTIME_SCALAR)
struct {
# ifdef __BIG_ENDIAN
s32 sec, nsec;
# else
s32 nsec, sec;
# endif
} tv;
#endif
};
typedef union ktime ktime_t;
如果某个32位体系结构提供了能够高效处理64位量的函数,可以设置配置选项KTIME_SCALAR
,目前只有IA-32利用了这种做法。在这种情况下,不会将该结构划分为两个32位值,而是直接表示为一个64位量
内核定义了几个辅助函数来处理ktime_t对象:
//include/linux/ktime.h
//两个ktime_t变量相加
#define ktime_add(lhs, rhs)
//两个ktime_t变量相减
#define ktime_sub(lhs, rhs)
//向一个ktime_t变量加上给定数量的纳秒
#define ktime_add_ns(kt, nsval)
//向一个ktime_t变量加上给定数量的微秒
static inline ktime_t ktime_add_us(const ktime_t kt, const u64 usec)
//向一个ktime_t变量减去给定数量的微秒
static inline ktime_t ktime_sub_us(const ktime_t kt, const u64 usec)
//向一个ktime_t变量减去给定数量的纳秒
#define ktime_sub_ns(kt, nsval)
//根据指定的秒和纳秒,来创建一个ktime_t变量
static inline ktime_t ktime_set(const long secs, const unsigned long nsecs)
//有各种形如 x_to_y 的函数,可以在x和y两种表示之间进行转换,其中x和y的类型可以是ktime_t、timeval clock_t和timespec
ktime_t timespec_to_ktime(const struct timespec ts)
ktime_t timeval_to_ktime(const struct timeval tv)
struct timespec ktime_to_timespec(const ktime_t kt)
struct timeval ktime_to_timeval(const ktime_t kt)
s64 ktime_to_ns(const ktime_t kt)
s64 ktime_to_us(const ktime_t kt)
//在64位机器上可以直接将ktime_t解释为纳秒数,但这在32位机器上将导致问题。因而提供了ktime_to_ns函数,来正确执行该转换
#define ktime_to_ns(kt) ((kt).tv64)
//判断两个ktime_t是否相等
static inline int ktime_equal(const ktime_t cmp1, const ktime_t cmp2)
15.3.4 用于时间管理的对象
时钟源
、时钟事件设备
和时钟设备
3个对象的数据结构表示
-
时钟源 clocksource
//include/linux/clocksource.h //时钟源结构体,时间管理的支柱。本质上每个时钟源都提供了一个单调增加的计数器,通用的内核代码只能进行只读访问。不同时钟源的精度取决于底层硬件的能力 struct clocksource { char *name;//时钟源的名称 struct list_head list;//链表元素,用于将所有的时钟源连接到一个标准的内核链表上,链表头为 clocksource_list 定义在 clocksource.c 中 /* rating值说明: 1.rating在1至99之间,表示一个质量非常差的时钟源,只能在万不得已时使用,或用于启动期 间,即只能在没有更好的时钟源可用的情况下才能使用。 2.100至199的范围表示时钟源可用于实际应用,但在有更好的时钟源可用时,一般不会使用此 种质量的时钟源。 3.rating在300至399区间,表示时钟源相当快速且准确。 4.完美的时钟源,其rating值在400至499之间 目前最好的时钟源在PowerPC体系结构上,其中有两个rating为400的时钟源。IA-32和AMD64机器上的时间戳计数器(time stamp counter,简称TSC)是这些体系结构上最精确的设备,其rating为300。大多数体系结构上最佳时钟源的rating值是接近的。开发者没有扩大这些设备的性能,并留出了许多空间可供硬件方面改善 */ int rating;//时钟源的质量,并非所有时钟的质量都是相同的,内核显然想要选择其中最好的一个. cycle_t (*read)(void);//用于读取时钟周期的当前计数值 cycle_t mask;//如果时钟不提供64位时间值,那么mask指定了一个位图,用于选择适当的比特位.CLOCKSOURCE_MASK(bits)宏用于针对给定的比特位数构建适当的掩码 //并非所有时钟源的read返回值都使用了统一的计时单位,因而需要分别转换为纳秒值。为此,需要分别使用mult和shift成员来乘/右移返回的时钟周期数,查看 static inline s64 cyc2ns(struct clocksource *cs, cycle_t cycles) 函数 u32 mult;//乘返回的时钟周期数 u32 shift;//右移返回的时钟周期数 unsigned long flags;//标志,CLOCK_SOURCE_IS_CONTINUOUS 表示一个连续时钟,不能丢失周期,如果时钟源要用于高精度定时器,该标志必须置位 ... }; //将不同 clocksource->read 读出来的时间转为统一的纳秒值 static inline s64 cyc2ns(struct clocksource *cs, cycle_t cycles)
在启动期间,如果
计算机确实没有提供更好的选择
(在启动后,决不会如此
),内核提供了一个基于jiffies的时钟
:请注意,如果jiffy时钟用作主时钟源,那么内核将负责通过一些适当的方式来更新jiffies值,例如直接在处理时钟中断时更新。通常,体系结构不会这样做。因而,在无时钟系统上使用该时钟是没有意义的,因为此类系统需要通过时钟源模拟jiffies层。实际上,使用jiffies时钟源是使动态时钟系统崩溃的一种好方法,至少在内核版本2.6.24上是这样的
//kernel/time/jiffies.c //NSEC_PER_JIFFY的定义包含了预处理器符号ACTHZ。虽然HZ表示编译时选择的低精度计时频率,但系统实际上提供的计时频率会因硬件的选择而有轻微差别。ACTHZ存储了时钟实际上运行的频率 #define NSEC_PER_JIFFY ((u32)((((u64)NSEC_PER_SEC)<<8)/ACTHZ)) //在启动期间,如果计算机确实没有提供更好的选择(在启动后,决不会如此),内核提供了一个基于jiffies的时钟 struct clocksource clocksource_jiffies = { .name = "jiffies", .rating = 1, /* lowest valid rating*///整个系统中最差的时钟 .read = jiffies_read, .mask = 0xffffffff, /*32bits*/ //初看起来,先左移JIFFIES_SHIFT然后再右移同样的位数,似乎没什么意义。但由于NTP代码不接受0位的移位操作,所以需要这种奇怪的做法 .mult = NSEC_PER_JIFFY << JIFFIES_SHIFT, /* details above */ .shift = JIFFIES_SHIFT, };
在IA-32和AMD64机器上,时间戳计数器通常提供了最佳时钟:
//arch/x86/kernel/tsc_64.c //IA-32和AMD64机器上的最佳时钟 static struct clocksource clocksource_tsc = { .name = "tsc", .rating = 300, .read = read_tsc, .mask = CLOCKSOURCE_MASK(64), .shift = 22, .flags = CLOCK_SOURCE_IS_CONTINUOUS | CLOCK_SOURCE_MUST_VERIFY, .vread = vread_tsc, };
-
使用时钟源
首先,使用clocksource_register
函数注册到内核。时钟源只是被添加到全局的clocksource_list
(定义在kernel/time/clocksource.c
),其中根据rating对所有可用的时钟源进行排序
。可调用select_clocksource来选择最佳时钟源
。通常该函数将选择rating最大的时钟
,但也可以从用户层通过/sys/devices/system/clocksource/clocksource0/current_clocksource指定优先选择的时钟源
,内核将优先使用。为此提供了如下两个全局变量。current_clocksource
指向当前最佳时钟源next_clocksource
指向一个struct clocksource实例,它比当前使用的时钟源更好。在注册一个新的最佳时钟源时,内核将自动切换到最佳时钟源。
读取时钟计时函数:
//kernel/time/timekeeping.c //读取当前时钟,转换结果保存到 ts 中 static inline void __get_realtime_clock_ts(struct timespec *ts) //如果系统提供了高精度时钟,使用这里定义的getnstimeofday,如果系统没有提供高精度时钟,则使用kernel/time.c中的getnstimeofday,提供的timespec值只能满足低精度计时需求 void getnstimeofday(struct timespec *ts)
-
时钟事件设备 clock_event_device
//include/linux/clockchips.h //时钟事件设备结构体,向时钟增加了事件功能,在未来的某个时刻发生。请注意,由于历史原因,这种设备通常也称为时钟事件源 struct clock_event_device { const char *name;//该事件设备的名称,显示在/proc/timer_list中 unsigned int features;//每位保存了事件设备的特性 如 CLOCK_EVT_FEAT_PERIODIC /*如,假定当前时间为20,min_delta_ns为2,而max_delta_ns为40.那么,下一个事件可以在时间范围[22, 60]内发生,这里的范围包括边界在内*/ unsigned long max_delta_ns;//指定了当前时间和下一次事件的触发时间之间的差值的最大值 unsigned long min_delta_ns;//指定了当前时间和下一次事件的触发时间之间的差值的最小值 unsigned long mult;//乘数,用于在时钟周期数和纳秒值之间进行转换 int shift;//位移数,用于在时钟周期数和纳秒值之间进行转换 int rating;//作用类似于时钟设备中相应的机制,时钟事件设备可以通过标称其精度来进行比较 int irq;//指定了该事件设备使用的IRQ编号。请注意,只有全局设备才需要该编号。各CPU的局部时钟使用不同的硬件机制来发送信号,将irq设置为1即可 cpumask_t cpumask;//指定了该事件设备所服务的CPU。为此使用了一个简单的位图。局部设备通常只负责一个CPU int (*set_next_event)(unsigned long evt, struct clock_event_device *);//设置事件将要发生的时间 void (*set_mode)(enum clock_event_mode mode, struct clock_event_device *);//用来切换所需要的运行方式,即在周期模式和单触发模式之间切换 void (*event_handler)(struct clock_event_device *);//在事件实际发生时调用,函数由硬件接口代码(通常是特定于体系结构的)调用,将时钟事件传递到通用时间子系统层 void (*broadcast)(cpumask_t mask);//是广播实现所需要的成员,它可以规避IA-32和AMD64系统上在省电模式下不工作的局部APIC设备 struct list_head list;//链表元素,链表头为全局链表 clockevent_devices(在clockevents.c中定义) enum clock_event_mode mode;//指定了当前的运行方式,CLOCK_EVT_MODE_ONESHOT 等 ktime_t next_event;//存储了下一个事件的绝对时间 }; //kernel/time/clockevents.c //将时钟事件下次触发的时间转为通用时间子系统使用的纳秒值 unsigned long clockevent_delta2ns(unsigned long latch, struct clock_event_device *evt) /*例如,假定当前时间为20,min_delta_ns为2,而max_delta_ns为40(当然,这些值只是示例,并不表示实际上可能出现的情况)。那么,下一个事件可以在时间范围[22, 60]内发生,这里的范围包括边界在内,clockevent_delta2ns就是用于将 2-40 转为 22-60*/ static LIST_HEAD(clockevent_devices);//全局链表头,链表元素为 时钟事件设备 clock_event_device.list //注册一个新的时钟事件设备 void clockevents_register_device(struct clock_event_device *dev)
时钟事件设备允许注册一个事件
,在未来一个指定的时间点上发生。但与完备的定时器实现相比,它只能存储一个事件
。每个clock_event_device
的关键成员是set_next_event
和event_handler
,其中前者设置事件将要发生的时间,后者在事件实际发生时调用每个事件设备的几个特性都可以根据
features
中存储的一个位串来判定。<clockchips.h>
中提供了若干常数,定义了可能的特性。我们对其中两个比较感兴趣(回想IA-32和AMD64系统上局部APIC暴露的问题:在特定的省电模式下,它们将停止工作。该问题通过设置“特性”CLOCK_EVT_FEAT_C3STOP来向内核报告,但这实际上应该称为“反特性”。)://include/linux/clockchips.h //支持周期性事件(即,事件按周期不断重复,无须通过对设备重新编程来显式激活事件)的时钟事件设备 #define CLOCK_EVT_FEAT_PERIODIC 0x000001 //表示时钟能够发出单触发事件,只发生一次。基本上,这刚好与周期性事件相反 #define CLOCK_EVT_FEAT_ONESHOT 0x000002 //时钟事件设备的模式 enum clock_event_mode { CLOCK_EVT_MODE_UNUSED = 0, CLOCK_EVT_MODE_SHUTDOWN, CLOCK_EVT_MODE_PERIODIC, CLOCK_EVT_MODE_ONESHOT, CLOCK_EVT_MODE_RESUME, };
时钟事件设备设置下一个事件将要发生的时间:
//kernel/time/clockevents.c //设置事件将要发生的时间,expires给出了设备dev的过期时间(绝对值),而now表示当前时间。通常,调用者会将 ktime_get() 的结果传递给该参数 int clockevents_program_event(struct clock_event_device *dev, ktime_t expires, ktime_t now)
在IA-32和AMD64系统上,全局时钟事件设备的角色最初由PIT承担。在HPET初始化之后,将接管该职责。为在x86系统上跟踪用于处理全局时钟事件的设备,采用了全局变量
global_clock_event
,定义在arch/x86/kernel/i8253.c
中。它指向当前使用的全局时钟事件设备的clock_event_device
实例。时钟设备
和时钟事件设备
在数据结构层次上,形式上是没有关联的。但是,一般通过系统中一个特定的硬件设备,提供这两个接口所需的功能需求,因此,内核通常会对每个时钟硬件设备注册一个时钟设备和一个时钟事件设备
。例如,考虑IA-32和AMD64系统上的HPET设备。该设备作为时钟源的功能汇集到clocksource_hpet
,而hpet_clockevent
则是clock_event_device
的一个实例。二者都定义在arch/x86/kernel/hpet.c
中。hpet_init
首先注册时钟源然后注册时钟事件设备。这向内核增加了两个时间管理对象,但只需要一个硬件 -
时钟设备 tick_device,是时钟事件设备的包装
时钟事件设备的一个特别重要的用途是提供周期时钟
,周期时钟的一个用途是用于运作经典的定时器轮。时钟设备是时钟事件设备的一个扩展
//include/linux/tick.h //时钟设备结构体,扩展了时钟事件源的功能,提供一个时钟事件的连续流,各个时钟事件定期触发。但可以使用动态时钟机制,在一定时间间隔内停止周期时钟。 struct tick_device { struct clock_event_device *evtdev;//时钟事件设备 enum tick_device_mode mode;//指定设备的运行模式 }; enum tick_device_mode { TICKDEV_MODE_PERIODIC,//周期模式 TICKDEV_MODE_ONESHOT,//单触发模式 };
周期模式和单触发模式在考虑无时钟系统时,区别会很重要,在15.5介绍.现在,只要将时钟设备视为一种提供时钟事件连续流的机制即可。这些形成了
调度器、经典定时器轮和内核相关组件的基础
内核仍然会区分全局和局部(各CPU)时钟设备。
局部设备
汇集在tick_cpu_device
中(定义在kernel/time/tick-internal.h
中)。请注意,在注册一个新的时钟事件设备时,内核会自动创建一个时钟设备
//kernel/time/tick-internal.h //局部时钟设备集合(每个cpu一个),各CPU链表,包含了系统中每个CPU对应的struct tick_device实例 DECLARE_PER_CPU(struct tick_device, tick_cpu_device); extern ktime_t tick_next_period;//指定了下一个全局时钟事件发生的时间(单位为纳秒) extern ktime_t tick_period;//存储了时钟周期的长度,单位为纳秒。它与HZ相对,后者存储了时钟的频率 extern int tick_do_timer_cpu __read_mostly;//包含了一个CPU编号,该CPU的时钟设备将承担全局时钟设备的角色 //kernel/time/tick-common.c /* 设置一个时钟设备 参数td指定了将要设置的tick_device实例。它将绑定到时钟事件设备newdev。cpu表示该设备 关联的处理器,cpumask是一个位掩码,用于限制只有特定的CPU才能使用该时钟设备 */ static void tick_setup_device(struct tick_device *td, struct clock_event_device *newdev, int cpu, cpumask_t cpumask)
如果
注册了一个新的时钟事件设备,能够利用该设备创建一个比当前更好的时钟设备,那么会自动调用该函数
。虽然将优先选择更高质量的设备,但在新的、更精确的设备不支持单触发模式,而旧设备支持该模式的情况下,不会选择新设备- tick_setup_device 设置一个时钟设备
- 设备为第一次设置时 if (!td->evtdev)
- 如果没有选定时钟设备来承担全局时钟设备的角色,那么将选择当前设备来承担此职责
- 将时钟设备设置为周期模式
- else
- 为时钟设备指定了事件设备之后,如果当前启用了广播模式,则该函数结束 if
- 否则,内核建立一个周期时钟(使用周期模式或单触发模式) else
- 如果当前时钟设备为周期模式,设置为周期模式,调用 tick_setup_periodic
- 如果该时钟设备为单触发模式,设置为单触发模式,调用 tick_setup_oneshot
- 设备为第一次设置时 if (!td->evtdev)
即使时钟设备处于单触发模式,也并不意味着一定启用了动态时钟
!例如,在高精度模式下,时钟总是基于单触发定时器实现的。我们先来考虑,根据所选择的配置,内核所需要处理的情形:
- 没有动态时钟的低精度系统,总是使用周期时钟。该内核不包含任何对单触发操作的支持
- 启用了动态时钟特性的低精度系统,以单触发模式使用时钟设备。
- 高精度系统总是使用单触发模式,无论是否启用了动态时钟特性
所有系统最初都工作于低精度模式,未启用动态时钟
,只有在必要的硬件初始化之后,系统才能切换到不同的特性组合
。因而这里主要考虑低精度、周期时钟的情况。更高级的选项在15.4.5节 和15.5节讨论。在广播模式下,需要进行一些修正,15.6节更详细地阐述了相关内容。在考察不启用动态时钟的低精度情形之前,需要指出,下图给出了可用于
不同特性组合下的时钟处理程序的概述
。请注意,对没有动态时钟特性的系统选择哪个广播函数,取决于底层时钟设备的模式。细节如下给出。tick_setup_periodic函数流程:
这里将专注于
不启用动态时钟的低精度情形
,其他设置将使用不同的处理程序函数- tick_setup_periodic 将时钟设备设置为周期模式
- 将 tick_handle_periodic 设为处理程序函数,调用tick_set_periodic_handler
- 如果时钟事件设备支持周期性事件,将时钟事件设备设置为周期模式,调用clockevents_set_mode(dev, CLOCK_EVT_MODE_PERIODIC);,之后会周期性调用 tick_handle_periodic
- 如果时钟事件设备不支持周期性事件,则使用单触发模式实现时钟设备的周期模式(通过周期性产生单触发事件),之后会周期性调用 tick_handle_periodic
辅助函数
tick_periodic
负责处理给定CPU上的周期时钟信号,CPU通过函数的参数指定://kernel/time/tick-common.c //处理给定CPU上的周期时钟信号 static void tick_periodic(int cpu)
- tick_periodic 处理给定CPU上的周期时钟信号
- 如果当前时钟设备负责全局时钟
- 更新全局jiffies值,处理进程统计,调用do_timer
- update_process_times 需要由SMP系统上的每个CPU执行。除了进程统计之外,它还激活了所有注册的经典低精度定时器并使之到期,并向调度器提供时间感知。这里我们只关注定时器的激活和过期,这是通过调用run_local_timers触发的。该函数又引发了软中断TIMER_SOFTIRQ来管理运行低精度定时器
- profile_tick 用于程序性能剖析
- 如果当前时钟设备负责全局时钟
//kernel/time/tick-common.c //时钟周期模式处理函数 void tick_handle_periodic(struct clock_event_device *dev)
- tick_handle_periodic 时钟周期模式处理函数
- tick_periodic 处理给定CPU上的周期时钟信号
- 如果时钟事件设备以周期模式运行,则函数结束
- 如果时钟事件设备以单触发模式运行,则设置下一个单触发事件以实现周期模式
- tick_setup_device 设置一个时钟设备
15.4 高精度定时器
在讨论了通用时间框架之后,深入到高精度定时器的实现中。这种定时器与低精度定时器相比,有如下两个根本性的不同
:
- 高精度定时器按时间
在红黑树上排序
- 它们
独立于周期时钟。它们不使用基于jiffies的时间规格,而是采用了纳秒时间戳
在经过通常的开发和测试阶段之后,内核版本2.6.16包含了该特性的基本框架,提供了下述特性之外的大部分实现:对高精度定时器的支持……但在该版本中,低精度定时器的经典实现的基础部分已经替换为新的实现。该实现基于高精度定时器框架,尽管支持的分辨率并不比以前好。随后的内核版本提供了对另一类定时器的支持,这些定时器实际上提供了高精度功能。
这种合并策略不仅是出于历史方面的考虑:由于低精度定时器的实现基于高精度机制,即使不启用高精度定时器,内核中也会联编(一部分)对高精度定时器的支持。当然,这种情况下系统只能够提供低精度定时功能。
高精度定时器框架中,并非普遍适用且实际上用于提供高精度功能支持的部分组件由预处理器符号CONFIG_HIGH_RES_TIMERS
控制,只有在编译时通过该选项启用高精度支持的情况下,相关代码才会编译到内核中。而框架的通用部分总是会编译到内核中
这意味着,即使只支持低精度定时器的内核也会包含高精度定时器框架的一部分,有时候这可能导致混淆。
15.4.1 数据结构
高精度定时器可以基于两种时钟
(称为时钟基础,clock base)。单调时钟(CLOCK_MONOTONIC)在系统启动时从0开始。另一种时钟(CLOCK_REALTIME)表示系统的实际时间。后一种时钟的时间可能发生跳跃
,例如在系统时间改变时,但单调时钟始终会单调地运行
对系统中的每个CPU,都提供了一个包含了两种时钟基础的数据结构。每个时钟基础都有一个红黑树,来排序所有待决的高精度定时器
。下图给出了相关情况的图形化概述。每个CPU都提供两个时钟基础(单调时钟和实际时间)。所有定时器都按过期时间在红黑树上排序
,如果定时器已经到期但其处理程序回调函数尚未执行,则从红黑树迁移到一个链表中
时钟基础由以下数据结构定义:
//include/linux/hrtimer.h
//时钟基础结构体,高精度定时器可以基于这个时钟(单调时钟和实际时间都使用这个结构体)
struct hrtimer_clock_base {
struct hrtimer_cpu_base *cpu_base;//指向该时钟基础所属的各CPU时钟基础结构
clockid_t index;//用于区分CLOCK_MONOTONIC(单调时钟)和CLOCK_REALTIME(实际时间,时间可能发生跳跃)
struct rb_root active;//红黑树的根结点,所有活动的定时器都在该树中排序
struct rb_node *first;//指向将第一个到期的定时器
ktime_t resolution;//表示该定时器的分辩率,单位为纳秒
ktime_t (*get_time)(void);//读取细粒度的时间。这对单调时钟是比较简单的(可直接使用由当前时钟源提供的值),但需要进行一些简单的算术操作,才能将该值转换为实际的系统时间
ktime_t (*get_softirq_time)(void);//获取软中断发出的时间
ktime_t softirq_time;//存储了软中断(HRTIMER_SOFTIRQ)发出的时间,如果未启用高精度模式,那么存储的时间将是粗粒度的,高精度定时器的处理,由相关的软中断HRTIMER_SOFTIRQ发起
#ifdef CONFIG_HIGH_RES_TIMERS
ktime_t offset;//在调整实时时钟时,会造成存储在CLOCK_REALTIME时钟基础上的定时器的过期时间值与当前实际时间之间的偏差。offset字段有助于修正这种情况,它表示定时器需要校正的偏移量。由于这只是一种临时效应,很少发生
int (*reprogram)(struct hrtimer *t,
struct hrtimer_clock_base *b,
ktime_t n);//用于对给定的定时器事件重新编程,即修改过期时间
#endif
};
//对每个CPU来说,都会使用以下数据结构建立两个时钟基础
struct hrtimer_cpu_base {
struct hrtimer_clock_base clock_base[HRTIMER_MAX_CLOCK_BASES];//每个cpu有2个时钟基础,对应单调时钟和实时时钟
#ifdef CONFIG_HIGH_RES_TIMERS
ktime_t expires_next;//包含了将要到期的下一个事件的绝对时间
int hres_active;//用作一个布尔变量,表示高精度模式是否已经启用,还是只提供了低精度模式
struct list_head cb_pending;//在定时器到期时,将从红黑树迁移到一个链表中,表头为cb_pending(这要求,允许在软中断的上下文中执行定时器。另外,定时器也可能在时钟硬件的IRQ中直接到期,而不涉及通过过期链表进行的迂回处理)。请注意,该链表上的定时器仍然需要进行处理。这由对应的软中断处理程序完成
unsigned long nr_events;//用于跟踪记录时钟中断的总数
#endif
};
//kernel/hrtimer.c
//各CPU时钟基础,系统初始化为低精度模式
DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases) =
{
.clock_base =
{
{
.index = CLOCK_REALTIME,
.get_time = &ktime_get_real,//通过使用getnstimeofday来获取当前时间
.resolution = KTIME_LOW_RES,//该预处理器常数表示在频率为HZ的周期时钟下,时钟信号间隔的长度,单位为纳秒
},
{
.index = CLOCK_MONOTONIC,
.get_time = &ktime_get,//通过使用getnstimeofday来获取当前时间
.resolution = KTIME_LOW_RES,//该预处理器常数表示在频率为HZ的周期时钟下,时钟信号间隔的长度,单位为纳秒
},
}
};
//include/linux/hrtimer.h
//定时器结构体
struct hrtimer {
struct rb_node node;//用于将定时器维持在红黑树中
ktime_t expires;//到期时间
enum hrtimer_restart (*function)(struct hrtimer *);//定时器到期时调用的回调函数
struct hrtimer_clock_base *base;//指向定时器的基础
unsigned long state;//定时器当前的状态, HRTIMER_STATE_INACTIVE 等
#ifdef CONFIG_HIGH_RES_TIMERS
enum hrtimer_cb_mode cb_mode;//定时器模式
struct list_head cb_entry;//链表元素,可用于将定时器置于回调链表上,其表头为hrtimer_cpu_base->cb_pending
#endif
};
每个定时器都可以指定一些情况,在这些情况下,该定时器可能或必须运行:
//include/linux/hrtimer.h
//定时器模式
enum hrtimer_cb_mode {
HRTIMER_CB_SOFTIRQ,//回调函数必须在软中断上下文运行
HRTIMER_CB_IRQSAFE,//回调函数可能在硬件中断上下文运行
HRTIMER_CB_IRQSAFE_NO_RESTART,//回调函数可能在硬件中断上下文运行,不会重启定时器
HRTIMER_CB_IRQSAFE_NO_SOFTIRQ,//回调函数必须在硬件中断上下文运行用于时钟仿真的特别模式
};
//定时器回调函数返回值
enum hrtimer_restart {
HRTIMER_NORESTART, /* Timer is not restarted *//*定时器无须重启*/
HRTIMER_RESTART, /* Timer must be restarted *//*定时器必须重启*/
};
定时器状态保存在hrtimer->state
中,在很少见的情况下,定时器可能同时处于HRTIMER_STATE_ENQUEUED
和HRTIMER_STATE_CALLBACK
状态。更多的信息,请参见<hrtimer.h>
中的注释
//include/linux/hrtimer.h
#define HRTIMER_STATE_INACTIVE 0x00/*不活动的定时器*/
#define HRTIMER_STATE_ENQUEUED 0x01/*在时钟基础上排队、等待到期的定时器*/
#define HRTIMER_STATE_CALLBACK 0x02/*表示当前正在执行定时器的回调函数*/
#define HRTIMER_STATE_PENDING 0x04/*在定时器已经到期,正在回调链表上等待执行*/
通常,回调函数 hrtimer->function
结束执行时会返回HRTIMER_NORESTART
。在这种情况下,该定时器将从系统消失。但定时器也可以选择重启。这需要在回调函数中执行如下两个步骤
- 回调函数的结果必须是HRTIMER_RESTART
- 定时器的到期时间必须设置为未来的某个时间点。回调函数可以执行上述操作,因为它可以通过函数参数获得一个指向当前运行的定时器hrtimer实例的指针。为简化操作,内核提供了一个辅助函数,将定时器的到期时间向未来推移:
//include/linux/hrtimer.h
//将定时器时间推后,使之在now之后到期(now通常设置为hrtimer_clock_base->get_time()的返回值)。确切的到期时间,需要将旧的到期时间加上interval,通常都是在now之后。该函数的返回值指定了需要在旧的到期时间加上多少个interval,才能使新的到期时间在now之后。如果旧的过期时间是5,now是12,interval为2,那么新的到期时间将是13。返回值为4,因为13 = 5 + 4×2
extern unsigned long
hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval);
高精度定时器的一个常见应用,就是使一个进程睡眠一段比较短的时间
,时间的长度可以指定。内核为此提供了另一个数据结构:
//include/linux/hrtimer.h
//高精度定时器的一个常见应用,就是使一个进程睡眠一段比较短的时间,时间的长度可以指定,使用该结构体,内核使用hrtimer_wakeup作为到期时调用的回调函数,以唤醒睡眠进程。在定时器到期时,可使用container_of机制从hrtimer计算出hrtimer_sleeper实例的地址(请注意,定时器是嵌入到struct hrtimer_sleeper中的),即可唤醒相关的进程
struct hrtimer_sleeper {
struct hrtimer timer;
struct task_struct *task;
};
15.4.2 使用定时器(初始化,开始,取消,重启)
设置一个新的定时器需要如下两步:
- hrtimer_init用于初始化一个hrtimer实例
- hrtimer_start启动定时器
//include/linux/hrtimer.h
//初始化一个hrtimer实例,timer表示受影响的高精度定时器,which_clock是定时器绑定的目标时钟,而mode指定了是使用相对时间值(相对于当前时间)还是绝对时间值
void hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
enum hrtimer_mode mode)
enum hrtimer_mode {
HRTIMER_MODE_ABS, /* Time value is absolute *//*时间值是绝对的*/
HRTIMER_MODE_REL, /* Time value is relative to now *//*时间值是相对于当前时间的*/
};
//启动定时器
int
hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode)
//取消一个设置好的定时器,如果定时器当前正在执行因而无法停止,一直等处理程序执行完毕.如果定时器处于未激活状态,返回0.如果定时器处于活动状态(即状态为HRTIMER_STATE_ENQUEUED或HRTIMER_STATE_PENDING),返回1
int hrtimer_cancel(struct hrtimer *timer)
//取消一个设置好的定时器,如果定时器当前正在执行因而无法停止,返回-1.如果定时器处于未激活状态,返回0.如果定时器处于活动状态(即状态为HRTIMER_STATE_ENQUEUED或HRTIMER_STATE_PENDING),返回1
int hrtimer_try_to_cancel(struct hrtimer *timer)
//重启一个取消的定时器
static inline int hrtimer_restart(struct hrtimer *timer)
15.4.3 高精度定时器机制实现
在介绍了所有必需的数据结构和组件之后,我们填补最后一片缺失的拼图,高精度定时器的到期机制及其回调函数的运行方式
回想前文,可知高精度定时器框架有一部分总是会编译到内核中,即使禁用了对高精度定时器的支持。在这种情况下,高精度定时器的到期是由一个低精度时钟驱动的。这避免了代码复制,因为高精度定时器的用户,在没有高精度计时能力的系统上,无须对时间相关代码提供一个额外的版本。这种情况下,仍然会采用高精度框架,但只以低精度运作。
即使高精度定时器支持已经编译到内核中,但在启动时只提供了低精度计时功能,这与上述情况是相同的。因而,在考察高精度定时器的运行时,需要考虑两种可能性
:基于高精度计时能力的时钟
和具有低精度的时钟
-
高精度模式下的高精度定时器 hrtimer_interrupt
我们首先假定一个高精度时钟已经设置好且正在运行中,而向高精度模式的迁移已经完全完成。这种一般的情形如下图所示:在负责高精度定时器的时钟事件设备引发一个中断时,将调用
hrtimer_interrupt
作为事件处理程序。该函数负责选中所有到期的定时器,或者将其转移到过期链表(如果它们可以在软中断上下文执行),或者直接调用定时器的处理程序函数。在对时钟事件设备重新编程(使得在下一个待决定时器到期时可以引发一个中断)之后,将引发软中断HRTIMER_SOFTIRQ。在该软中断执行时,run_hrtimer_softirq负责执行到期链表上所有定时器的处理程序函数
- timer_interrupt IA32时钟中断处理函数
- do_timer_interrupt_hook 处理时钟中断
- global_clock_event->event_handler 高精度定时器为 hrtimer_interrupt
- 遍历时钟基础(单调时钟和实时时钟)
- 遍历红黑树中到期的结点
- 如果下一个定时器的到期时间是在未来,则离开while循环
- 如果当前定时器已经到期,那么在允许在软中断上下文执行处理程序的情况下(即设置了HRTIMER_CB_SOFTIRQ),会将该定时器移动到回调链表并设置好红黑树中下一个定时器,然后调用 continue处理红黑树中下一个定时器
- 如果不允许在软中断上下文执行定时器的处理程序,那么将直接在硬件中断上下文中执行定时器回调函数 timer->function(timer)
- 定时器回调函数如果返回 HRTIMER_RESTART 则重启定时器 enqueue_hrtimer(timer, base, 0)
- 在已经选择了所有时钟基础的待决定时器之后,内核需要对时钟事件设备重新编程,以便在下一个定时器到期时引发中断。如果下一个定时器的到期时间已经过去,那么重新编程会失败。在定时器的处理花费了太长时间的情况下,会发生这种情况。在这种情况下,会跳转到函数起始处的retry标号,重启整个处理序列
- 如果还有定时器在回调链表上等待,则必须引发HRTIMER_SOFTIRQ软中断
- 遍历红黑树中到期的结点
- 遍历时钟基础(单调时钟和实时时钟)
- global_clock_event->event_handler 高精度定时器为 hrtimer_interrupt
- do_timer_interrupt_hook 处理时钟中断
HRTIMER_SOFTIRQ软中断处理函数(run_hrtimer_softirq)流程图如下:
这里忽略了一种边缘情况,即在定时器回调已经执行后,该定时器在另一个CPU上被重启。如果该定时器是树中第一个到期的,那么可能需要对时钟事件设备重新编程,以设定新的到期时间。
- run_hrtimer_softirq HRTIMER_SOFTIRQ软中断处理函数
- 该函数将遍历所有待决定时器的链表。对每个定时器,都将执行回调处理程序。如果定时器请求重启,那么调用enqueue_hrtimer来完成所需的工作
- timer_interrupt IA32时钟中断处理函数
-
低精度模式下的高精度定时器 hrtimer_run_queues
如果系统没有提供高精度时钟,会怎么样呢?在这种情况下,高精度定时器的到期操作由hrtimer_run_queues
发起,该函数由软中断TIMER_SOFTIRQ调用
(由于软中断处理在这种情况下是基于低精度定时器,因而该机制很自然不能提供任何高精度计时能力)。其代码流程图如下图所示。请注意,这是一个简化的版本
。实际上,该函数要牵涉更多的因素,因为从低精度到高精度模式的切换即由此开始的
。但这些问题现在不会影响到我们,所需的相关扩展将在15.4.5节讨论。
-
timer_interrupt IA32时钟中断处理函数
- do_timer_interrupt_hook 处理时钟中断
- global_clock_event->event_handler 低精度定时器为tick_handle_periodic
- tick_periodic 时钟更新,触发时钟软中断
- do_timer 负责全系统范围的、全局性的任务:更新jiffies值,处理进程统计。在多处理器系统上,会选择一个特定的CPU来执行这两个任务,而不涉及其他CPU
- update_process_times 需要由SMP系统上的每个CPU执行。除了进程统计之外,它还激活了所有注册的经典低精度定时器并使之到期,并向调度器提供时间感知。这里我们只关注定时器的激活和过期,这是通过调用run_local_timers触发的。该函数又引发了软中断TIMER_SOFTIRQ来管理运行低精度定时器
- run_local_timers 使定时器激活和过期,该函数又引发了软中断TIMER_SOFTIRQ,而其处理程序函数负责运行低精度定时器
- raise_softirq(TIMER_SOFTIRQ) 触发TIMER_SOFTIRQ软中断(定时器管理软中断),其处理程序函数负责运行低精度定时器
- run_local_timers 使定时器激活和过期,该函数又引发了软中断TIMER_SOFTIRQ,而其处理程序函数负责运行低精度定时器
- tick_periodic 时钟更新,触发时钟软中断
- global_clock_event->event_handler 低精度定时器为tick_handle_periodic
- do_timer_interrupt_hook 处理时钟中断
-
run_timer_softirq 定时器软中断TIMER_SOFTIRQ处理函数
- hrtimer_run_queues 系统没有提供高精度时钟时,高精度定时器的到期操作
- hrtimer_get_softirq_time 将粗粒度的时间值保存到定时器基础
- run_hrtimer_queue 处理各个队列中的项
- 检查是否有定时器需要处理
- 遍历红黑树中到期的定时器
- 移除到期的定时器,设置红黑树中下一个定时器
- 执行定时器到期回调函数
- 清除HRTIMER_STATE_CALLBACK标志
- 定时器回调函数如果返回 HRTIMER_RESTART 则重启定时器 enqueue_hrtimer(timer, base, 0)
- hrtimer_run_queues 系统没有提供高精度时钟时,高精度定时器的到期操作
-
15.4.4 周期时钟仿真(模拟)(高精度时钟是基于单触发模式实现的,为了模拟周期tick(低精度),仿真层为每个CPU安装一个高精度定时器周期调用) tick_setup_sched_timer
高精度模式下的时钟事件处理程序
是hrtimer_interrupt
。这意味着tick_handle_periodic
不再提供周期时钟信号。因而需要基于高精度定时器提供一个等效的功能。在启用/禁用动态时钟的情况下,实现(几乎)是相同的。动态时钟的通用框架在15.5节讨论,这里只粗略讲一下所需的组件
tick_sched
是用于管理周期时钟相关的所有信息的数据结构,由全局变量tick_cpu_sched
为每个CPU分别提供了一个该结构的实例
在内核切换到高精度模式时
,将调用tick_setup_sched_timer
来激活时钟仿真层。这将为每个CPU安装一个高精度定时器。所需的struct hrtimer
实例保存在各CPU变量tick_cpu_sched
中:
//include/linux/tick.h
//用于管理周期时钟相关的所有信息,动态时钟使用了这个
struct tick_sched {
struct hrtimer sched_timer;//用于实现时钟的定时器,定时器的回调函数选择了tick_sched_timer
...
};
//kernel/time/tick-sched.c
//存周期时钟所有信息,每个CPU一个
static DEFINE_PER_CPU(struct tick_sched, tick_cpu_sched);
-
tick_setup_sched_timer 在内核切换到高精度模式时调用,激活时钟仿真层。这将为每个CPU安装一个高精度定时器
- hrtimer_init 初始化当前cpu的hrtimer实例,定时器到期函数为 tick_sched_timer
- hrtimer_forward 将定时器到期时间推后tick_period时间
- hrtimer_start 启动定时器
-
tick_sched_timer 仿真层注册的CPU定时器到期处理函数
- tick_do_update_jiffies64 更新jiffies64
- update_process_times 需要由SMP系统上的每个CPU执行。除了进程统计之外,它还激活了所有注册的经典低精度定时器并使之到期,并向调度器提供时间感知。这里我们只关注定时器的激活和过期,这是通过调用run_local_timers触发的。该函数又引发了软中断TIMER_SOFTIRQ来管理运行低精度定时器
- run_local_timers 使定时器激活和过期,该函数又引发了软中断TIMER_SOFTIRQ,而其处理程序函数负责运行低精度定时器
- raise_softirq(TIMER_SOFTIRQ) 触发TIMER_SOFTIRQ软中断(定时器管理软中断),其处理程序函数负责运行低精度定时器
- run_local_timers 使定时器激活和过期,该函数又引发了软中断TIMER_SOFTIRQ,而其处理程序函数负责运行低精度定时器
- profile_tick 用于程序性能剖析
- hrtimer_forward(timer, now, tick_period) 重设定时器到期时间
该定时器的回调函数选择了tick_sched_timer
。为避免所有CPU同时运行周期时钟处理程序的情况,内核分配了加速时间
,如下图。回想前文,可知时钟周期的长度(单位为纳秒)保存在tick_period
中。时钟信号将在周期的前一半时间里传播
。假定第一个时钟信号起始于时间0。如果系统包含N个CPU,其余的周期时钟信号分别起始于时间Δ、2Δ、3Δ、… 偏移量Δ由tick_period/(2N)
给出。
用于周期时钟仿真的定时器,其注册类似于其他普通的高精度定时器
。该函数与tick_periodic
有些类似,但要复杂一些。其代码流程图如下:
如果当前执行该定时器的CPU负责提供全局时钟(回想前文可知,在启动时,系统尚处于低精度模式,该职责已经分配到具体的CPU),那么tick_do_update_jiffies64
将计算自上一次更新以来所经过的jiffies数目,在这里该数目总是1,因为现在不考虑动态时钟。此前讨论的函数do_timer用于处理全局定时器的所有职责。回想前文可知,其中就包括了对全局变量jiffies64的更新。
在update_process_times(参见15.8节)和profile_tick中执行各CPU周期时钟任务时,需要计算下一个事件的时间,而hrtimer_forward将据此对定时器进行设置。通过返回HRTIMER_RESTART,定时器将自动重新进入队列,并在下一个时钟到期时激活
15.4.5 切换到高精度定时器 hrtimer_switch_to_hres
最初,高精度定时器并未启用,只有在已经初始化了适当的高精度时钟源并将其添加到通用时钟框架之后,才能启用高精度定时器
。但在最初,低精度时钟就已经提供了。下文将讨论内核如何从低精度模式切换到高精度模式
在低精度定时器活动时,高精度队列由hrtimer_run_queues处理
。在队列运行前,该函数将检查系统中是否存在适用于高精度定时器的时钟事件设备。如果有,则切换到高精度模式
-
timer_interrupt IA32时钟中断处理函数
- do_timer_interrupt_hook 处理时钟中断
- global_clock_event->event_handler 低精度定时器为tick_handle_periodic
- tick_periodic 时钟更新,触发时钟软中断
- do_timer 负责全系统范围的、全局性的任务:更新jiffies值,处理进程统计。在多处理器系统上,会选择一个特定的CPU来执行这两个任务,而不涉及其他CPU
- update_process_times 需要由SMP系统上的每个CPU执行。除了进程统计之外,它还激活了所有注册的经典低精度定时器并使之到期,并向调度器提供时间感知。这里我们只关注定时器的激活和过期,这是通过调用run_local_timers触发的。该函数又引发了软中断TIMER_SOFTIRQ来管理运行低精度定时器
- run_local_timers 使定时器激活和过期,该函数又引发了软中断TIMER_SOFTIRQ,而其处理程序函数负责运行低精度定时器
- raise_softirq(TIMER_SOFTIRQ) 触发TIMER_SOFTIRQ软中断(定时器管理软中断),其处理程序函数负责运行低精度定时器
- run_local_timers 使定时器激活和过期,该函数又引发了软中断TIMER_SOFTIRQ,而其处理程序函数负责运行低精度定时器
- tick_periodic 时钟更新,触发时钟软中断
- global_clock_event->event_handler 低精度定时器为tick_handle_periodic
- do_timer_interrupt_hook 处理时钟中断
-
run_timer_softirq 定时器软中断TIMER_SOFTIRQ处理函数
- hrtimer_run_queues 系统没有提供高精度时钟时,高精度定时器的到期操作
- tick_check_oneshot_change 检查系统中是否存在适用于高精度定时器的时钟事件设备,如果没有可能启动动态时钟
- hrtimer_switch_to_hres 如果有合适的高精度定时器的时钟事件设备,则切换到高精度模式
- tick_init_highres
- tick_switch_to_oneshot(hrtimer_interrupt) 将时钟事件设备设置为单触发模式,将hrtimer_interrupt设置为事件处理程序,下一次硬件时钟中断调用的 global_clock_event->event_handler 就是 hrtimer_interrupt 函数
- tick_setup_sched_timer 激活周期时钟仿真
- tick_init_highres
- hrtimer_run_queues 系统没有提供高精度时钟时,高精度定时器的到期操作
如果有一个支持单触发模式的时钟,而且其精度可以达到高精度定时器所要求的分辨率(即设置了CLOCK_SOURCE_VALID_FOR_HRES标志),那么tick_check_oneshot_change
将通知内核可以使用高精度定时器。实际的切换由hrtimer_switch_to_hres
执行。如下图。
hrtimer_switch_to_hres
是一个包装器函数,它使用 tick_switch_to_oneshot
将时钟事件设备设置为单触发模式。另外,还将hrtimer_interrupt
设置为事件处理程序。然后,正如前文的讨论,用tick_setup_sched_timer
激活周期时钟仿真。由于分辨率现在已经提高,这也需要反映到数据结构中。
15.5 动态时钟(用于使系统进入深度睡眠实现低功耗,停止了时钟)
多年以来,Linux内核中的时间概念都是由周期时钟提供的
。该方法简单而有效,但在很关注耗电量的系统上,有一点不足之处:周期时钟要求系统在一定的频率下,周期性地处于活动状态。因此,长时间的休眠是不可能的
动态时钟改善了这种情况。只有在有些任务需要实际执行时,才激活周期时钟。否则,会临时禁用周期时钟
。对该技术的支持可以在编译时选择,启用此选项的系统也称为无时钟系统
(tickless system)。但这个名称是不完全准确的,因为即使在这种情况下,周期时钟运行的基础频率HZ仍然为时序提供了一个基本的度量工具。由于时钟可以根据当前的需要来激活或停用
,因而“动态时钟”这个术语就很适用
内核如何判定系统当前是否无事可做?回想第2章的内容,其中提到,如果运行队列时没有活动进程,内核将选择一个特别的idle进程来运行。此时,动态时钟机制将开始发挥作用。每当选中idle进程运行时,都将禁用周期时钟,直至下一个定时器即将到期为止
。在经过这样一段时间之后,或者有中断发生时,将重新启用周期时钟。与此同时,CPU可以进入不受打扰的睡眠状态。请注意,只有经典定时器需要考虑此用法。高精度定时器不绑定到时钟频率,也并非基于周期时钟实现
。
在讨论动态时钟的实现之前,我们先要注意,单触发时钟是实现动态时钟的先决条件
。因为动态时钟的一个关键特性是可以根据需要来停止或重启时钟机制,纯粹周期性的定时器根本就不适用于该机制。
下文提到周期时钟时,是指时钟的实现没有使用动态时钟
。这决不能与工作于周期模式的时钟事件设备相混淆。
15.5.1 动态时钟相关数据结构
动态时钟需要根据使用的定时器分辨率高低来采用不同的实现。在两种情况下,其实现都是围绕以下数据结构进行:
//include/linux/tick.h
//用于管理周期时钟相关的所有信息,动态时钟使用了这个
struct tick_sched {
struct hrtimer sched_timer;//用于实现时钟的定时器,定时器的回调函数选择了tick_sched_timer
enum tick_nohz_mode nohz_mode;//当前运作模式
ktime_t idle_tick;//存储在禁用周期时钟之前,上一个时钟信号的到期时间。这对于了解何时再次启用周期时钟是很重要的,因为下一个时钟的到期时间必须与时钟禁用前完全一致,就像是时钟没有禁用一样。准确的时间点可以根据idle_tick中保存的值来计算。然后加上数目足够多的时钟周期,以获得下一个时钟信号的到期时间。
int tick_stopped;//如果周期时钟已经停用,则tick_stopped为1,即当前没有什么基于周期时钟信号的工作要做。否则,其值为0
unsigned long idle_jiffies;//存储了周期时钟禁用时的jiffies值
unsigned long idle_calls;//统计了内核试图停用周期时钟的次数
unsigned long idle_sleeps;//统计了实际上成功停用周期时钟的次数.因为如果下一个时钟即将在一个jiffy之后到期,内核是不会停用时钟的
ktime_t idle_entrytime;//存储了周期时钟上一次禁用的准确时间(使用当前最佳的分辨率)
ktime_t idle_sleeptime;//累计了时钟停用的总的时间
ktime_t sleep_length;//存储了周期时钟将禁用的时间长度,即从时钟禁用起,到预定将发生的下一个时钟信号为止,这一段时间的长度
unsigned long last_jiffies;
unsigned long next_jiffies;//存储了下一个定时器到期时间的jiffy值
ktime_t idle_expires;//存储了下一个将到期的经典定时器的到期时间。与上一个值不同,这个值的分辨率会尽可能高,其单位不是jiffies
};
enum tick_nohz_mode {
NOHZ_MODE_INACTIVE,//周期时钟处于活动状态
NOHZ_MODE_LOWRES,//使用的动态时钟是基于低精度的定时器
NOHZ_MODE_HIGHRES,//使用的动态时钟是基于高精度的定时器
};
//存周期时钟所有信息,每个CPU一个.对时钟的禁用是按CPU指定的,而不是对整个系统指定
static DEFINE_PER_CPU(struct tick_sched, tick_cpu_sched);
15.5.2 低精度系统下的动态时钟
考虑内核不使用高精度定时器,只提供低精度计时功能的情形。这种场景下,如何实现动态时钟?回想上文可知,定时器软中断调用hrtimer_run_queues
来处理高精度定时器队列,即使底层时钟事件设备只提供了低精度,也是如此。当然,要再次强调这并不会为定时器提供更好的分辨率,但这使得可以使用现存的框架,而无须关注时钟的分辨率
-
切换到动态时钟 tick_switch_to_oneshot(tick_nohz_handler)
hrtimer_run_queues
调用tick_check_oneshot_change
来判断是否可以激活高精度定时器。此外,该函数还检查是否可以在低精度系统上启用动态时钟
。在两种情况下,这是可能的。(1) 提供了支持单触发模式的时钟事件设备
(2) 未启用高精度模式如果二者都满足,那么将
调用tick_nohz_switch_to_nohz
来激活动态时钟
。但这没有最终启用动态时钟。如果在编译时禁用了对无时钟系统的支持,那么上述函数只是一个空函数,内核仍将处于周期时钟模式。否则,内核将继续进行处理,如下图:-
timer_interrupt IA32时钟中断处理函数
- do_timer_interrupt_hook 处理时钟中断
- global_clock_event->event_handler 低精度定时器为tick_handle_periodic
- tick_periodic 时钟更新,触发时钟软中断
- do_timer 负责全系统范围的、全局性的任务:更新jiffies值,处理进程统计。在多处理器系统上,会选择一个特定的CPU来执行这两个任务,而不涉及其他CPU
- update_process_times 需要由SMP系统上的每个CPU执行。除了进程统计之外,它还激活了所有注册的经典低精度定时器并使之到期,并向调度器提供时间感知。这里我们只关注定时器的激活和过期,这是通过调用run_local_timers触发的。该函数又引发了软中断TIMER_SOFTIRQ来管理运行低精度定时器
- run_local_timers 使定时器激活和过期,该函数又引发了软中断TIMER_SOFTIRQ,而其处理程序函数负责运行低精度定时器
- raise_softirq(TIMER_SOFTIRQ) 触发TIMER_SOFTIRQ软中断(定时器管理软中断),其处理程序函数负责运行低精度定时器
- run_local_timers 使定时器激活和过期,该函数又引发了软中断TIMER_SOFTIRQ,而其处理程序函数负责运行低精度定时器
- tick_periodic 时钟更新,触发时钟软中断
- global_clock_event->event_handler 低精度定时器为tick_handle_periodic
- do_timer_interrupt_hook 处理时钟中断
-
run_timer_softirq 定时器软中断TIMER_SOFTIRQ处理函数
- hrtimer_run_queues 系统没有提供高精度时钟时,高精度定时器的到期操作
- tick_check_oneshot_change 检查系统中是否存在适用于高精度定时器的时钟事件设备,如果没有可能启动动态时钟
- tick_nohz_switch_to_nohz 激活动态时钟,如果编译选项不支持动态时钟,这将是一个空函数
- tick_switch_to_oneshot(tick_nohz_handler) 将时钟事件设备设置为单触发模式,将tick_nohz_handler设置为事件处理程序,下一次硬件时钟中断调用的 global_clock_event->event_handler 就是 tick_nohz_handler 函数
- ts->nohz_mode = NOHZ_MODE_LOWRES 使用的动态时钟是基于低精度的定时器
- 初始化时钟定时器,编程设置下一个时钟周期信号
- tick_nohz_switch_to_nohz 激活动态时钟,如果编译选项不支持动态时钟,这将是一个空函数
- tick_check_oneshot_change 检查系统中是否存在适用于高精度定时器的时钟事件设备,如果没有可能启动动态时钟
- hrtimer_run_queues 系统没有提供高精度时钟时,高精度定时器的到期操作
迁移到动态时钟模式所需的最重要的改变是
将时钟事件设备设置为单触发模式,并安装一个适当的时钟定时器处理程序
。这是通过调用tick_switch_to_oneshot完成的。新的处理程序是tick_nohz_handler,将在下文考察由于动态时钟模式现在已经激活,struct tick_sched的各CPU实例的nohz_mode字段将改变为NOHZ_MODE_LOWRES。为使该机制进入运转,内核最后需要激活第一个周期时钟定时器,使得该定时器在下一个时钟信号应当出现的时间到期。
-
-
动态时钟处理程序 tick_nohz_handler
新的时钟定时器处理程序tick_nohz_handler需要承担如下两个职责:
- 执行时钟机制所需的所有操作
- 对时钟设备重新编程,使得下一个时钟信号在适当的时候到期
- timer_interrupt IA32时钟中断处理函数
- do_timer_interrupt_hook 处理时钟中断
- global_clock_event->event_handler 低精度动态时钟处理函数为 tick_nohz_handler
- 如果一个CPU要进入比较长时间的休眠,不能继续负责全局时钟,需要撤销其职责。如果是这样,那么接下来如果有哪个CPU的时钟定时器处理程序被调用,该CPU必须承担该职责.也有可能所有处理器都进入睡眠,其时间都长于一个jiffy。内核需要考虑这种情形
- 如果CPU负责提供全局时钟,那么调用tick_do_update_jiffies64更新
- update_process_times 更新局部时钟信息
- profile_tick 用于程序性能剖析
- 如果时钟机制在当前CPU已经停用,则结束函数,不需要重新编程时钟,CPU可以进入完全的睡眠状态 函数结束
- 否则 tick_nohz_reprogram 对时钟设备重新编程,使得下一个时钟信号在适当的时候到期
- global_clock_event->event_handler 低精度动态时钟处理函数为 tick_nohz_handler
- do_timer_interrupt_hook 处理时钟中断
-
全局时钟更新jiffies tick_do_update_jiffies64
全局时钟设备调用
tick_do_update_jiffies64
来更新全局jiffies_64变量,它是低精度定时器处理的基础。在使用周期时钟时,这相对简单,因为每过一个jiffy,都会调用该函数。在启用动态时钟时,可能出现这种情况:系统的所有CPU都处于idel状态,系统处于没有全局时钟的状态
。tick_do_update_jiffies64需要考虑这种情况。我们直接看一下代码是如何处理的- tick_do_update_jiffies64 由选中的某个用于全局时钟的CPU调用,更新全局jiffies64
- 需要判断,从上一次更新以来时间是否已经过了多个jiffy,必须计算当前时间与 last_jiffies_update 之间的差
- 仅当上一次更新是在一个时钟周期之前时,才需要更新jiffies值.更新的jiffies可能多于一个时钟周期.因为系统休眠,动态时钟停止了jiffies更新,系统跳过了多个时钟周期的处理
- do_timer(++ticks) 更新全局jiffies值
- tick_do_update_jiffies64 由选中的某个用于全局时钟的CPU调用,更新全局jiffies64
15.5.3 高精度系统下的动态时钟
由于在内核使用高精度时,时钟事件设备以单触发模式运行
,对动态时钟的支持比低精度情形更为容易实现。回想前文的讨论,周期时钟是通过tick_sched_timer
仿真(模拟)的。该函数也用于实现动态时钟。在15.4.4节的讨论中,省略了实现动态时钟需要的两个要素:
-
由于CPU可能放弃全局时钟的职责,该处理程序需要检查现在是否是这种情况,并承担该职责:
//kernel/time/tick-sched.c tick_sched_timer { #ifdef CONFIG_NO_HZ if (unlikely(tick_do_timer_cpu == -1)) tick_do_timer_cpu = cpu; #endif }
-
在处理程序结束时,通常需要对时钟设备重新编程,使得下一个时钟信号在适当时候发生。如果时钟已经停止,则不必如此
//kernel/time/tick-sched.c tick_sched_timer { /* 如果处于idle循环中,不要重启定时器 */ if (ts->tick_stopped) return HRTIMER_NORESTART; }
在高精度模式下初始化动态时钟模式,只需修改一处。回想前文,可知tick_setup_sched_timer
在高精度下用于初始化时钟仿真层。如果在编译时启用动态时钟,会向该函数添加一小段代码:
kernel/time/tick-sched.c
void tick_setup_sched_timer(void)
{
...
#ifdef CONFIG_NO_HZ
if (tick_nohz_enabled)
ts->nohz_mode = NOHZ_MODE_HIGHRES;
#endif
}
这样就开始在高精度定时器下使用动态时钟了
15.5.4 停止和启动周期时钟
动态时钟提供了暂时延迟周期时钟的框架。但内核仍然需要判断在何时停止和重启时钟
很自然的一种做法是,在调度idle进程时停止时钟
:这表明处理器确实没什么可做。动态时钟框架提供了tick_nohz_stop_sched_tick
,用于停止时钟。请注意,该函数同时适用于低/高精度。如果编译时停用了动态时钟,该函数将替换为空实现
。
idle进程是以特定于体系结构的方式实现的,而并非所有的体系结构都已经更新到支持停用周期时钟。在本书撰写时,ARM、MIPS、PowerPC、SuperH、Sparc64、IA-32、AMD64(以及用户模式Linux,如果读者认为那是一个独立体系结构的话)会在idle进程关闭时钟
集成tick_nohz_stop_sched_tick
的工作相当简单。例如,考虑ARM系统上cpu_idle的实现(在idle进程中运行):
//arch/arm/kernel/process.c
//idle线程
void cpu_idle(void)
- cpu_idle idle线程
- tick_nohz_stop_sched_tick 停止时钟
- while (!need_resched()) 等待有进程需要调度
- tick_nohz_restart_sched_tick 激活时钟
其他体系结构在一些细节方面有所不同,但一般原理上是一致的。在调用tick_nohz_stop_sched_tick
关闭时钟后,系统进入一个无限循环,在该处理器上有一个进程可调度时,循环才结束。时钟接下来就需要使用了,可通过tick_nohz_restart_sched_tick
重激活
睡眠进程会等待一些条件满足,使得该进程切换到可运行状态
。对条件的改变可通过中断通知
,假定进程在等待一些数据到达,而该中断将通知系统数据现在已经可用。由于从内核的角度来看,中断可能发生在随机的时间点上,因而很可能是这样,在关闭时钟的idle循环期间,引发了中断
。因而可能有两种情况需要重启时钟
:
- 一个外部中断使某个进程变为可运行,这要求时钟机制恢复工作(为简化阐述,这里忽略了一种情况:在中断扰动了无时钟间歇期但没有改变系统状态的情况下,此时没有进程变为可运行,irq_exit也会调用tick_nohz_stop_sched_tick。这也简化了对tick_nohz_stop_sched_tick的讨论,因为此后对该函数的多次调用都无须考虑。此外,没有讨论在irq_enter中需要更新jiffies值的情况,如果不更新,中断处理程序会使用一个错误的时间值。负责此工作的函数是tick_nohz_update_jiffies)。在这种情况下,时钟的恢复,比最初计划的时间要早一些
- 下一个时钟信号即将到期,而时钟中断表明到期时间已经到来。在这种情况下,时钟机制的恢复与此前的计划相同
-
停止时钟 tick_nohz_stop_sched_tick
tick_nohz_stop_sched_tick需要执行以下3个任务
- 检查下一个定时器轮事件是否在一个时钟周期之后
- 如果是这样,则重新编程时钟设备,忽略下一个时钟周期信号,直至有必要时才恢复。这将自动忽略所有不需要的时钟信号
- 在tick_sched中更新统计信息
由于许多细节需要关注边边角角的情况,tick_nohz_stop_sched_tick的实际实现相当庞大,下文将考虑一个简化版本
- tick_nohz_stop_sched_tick 停止时钟
- 如果下一个时钟信号至少在一个jiffy以后,时钟设备需要据此重新编程
- 如果当前CPU必须提供全局时钟,那么此职责必须转交给另一CPU
- 对时钟设备重新编程,如果成功,则函数结束
- 否则,编程失败(必然是在处理过程中花费了太多时间,而导致下一个到期时间已经过去),更新jiffies
- 产生软中断 raise_softirq_irqoff(TIMER_SOFTIRQ)
- 如果下一个时钟信号至少在一个jiffy以后,时钟设备需要据此重新编程
-
重启时钟 tick_nohz_restart_sched_tick
同样,该函数的实现被各种技术细节搞得很复杂,但其一般原理是很简单的。首先调用我们熟悉的
tick_do_update_jiffies64
。在正确地统计空闲时间之后,将tick_sched->tick_stopped设置为0,因为时钟现在再次激活了。最后,需要对下一个时钟事件编程。这是必要的,因为外部中断的存在,可能导致空闲时间的结束早于预期
15.6 广播模式(某些体系结构上省电模式启用时时钟事件设备将进入睡眠,这时用另一个可工作的设备来替换停止的设备) tick_handle_periodic_broadcast
在分析Tick模拟层的时候曾经提到过,当系统中没有别的进程需要处理的时候,会将当前CPU切换到NO_HZ状态,不会每一个Tick都收到定时中断,从而达到节电的目的。但此时,当前CPU上的定时事件设备还是打开的,处于工作状态,只不过不产生Tick了。但是,如果当前CPU上的定时事件设备还支持一种叫做C3_STOP的状态的话,有可能当CPU进入某些空闲状态的时候,连为本CPU服务的定时事件设备都会被完全停止掉。这时候,本CPU将完全接收不到任何定时中断,也不会自己把自己唤醒,必须寻求外部设备或其它没有休眠CPU的帮助
,这就是Tick广播层存在的目的
在一些体系结构上,在某些省电模式启用时,时钟事件设备将进入睡眠
。幸好,系统不是只有一个时钟事件设备,因此仍然可以用另一个可工作的设备来替换停止的设备。全局变量tick_broadcast_device
定义在kernel/time/tick-broadcast.c
中,即为用于广播设备的tick_device
实例。
广播模式的概述如下:
在这种情况下,APIC设备是不工作的,但广播事件设备仍然可工作。tick_handle_periodic_broadcast
用作事件处理程序。它可以处理广播设备的周期模式和单触发模式
,我们无须进一步关注。该处理程序在每个tick_period之后都会激活
广播处理程序使用了tick_do_periodic_broadcast
。其代码流程图如下。该函数调用了当前CPU上不工作设备的event_handler
方法。该处理程序不能区分对其调用是来自于时钟中断还是广播设备,因而会像底层事件设备仍然正常工作一样去执行
如果有更多不工作的时钟设备,那么tick_do_broadcast
将利用链表中第一个设备的broadcast
方法(这是可能的,因为目前对所有不工作的设备都会安装同一广播处理程序)。对局部APIC
来说,broadcast方法指向lapic_timer_broadcast
。对所有与不工作时钟设备相关联的CPU,该方法都负责发送处理器间中断(inter-processor interrupt,IPI)LOCAL_TIMER_VECTOR
。该中断的中断向量由内核设置为调用apic_timer_interrupt。其结果就是,时钟事件设备无法区分IPI和真正的中断,因而其效果与设备仍然处于工作状态是相同的。
处理器间中断是很慢的,因而不会提供高精度定时器所需的精度和分辨率。因而如果需要广播,内核总是切换到低精度模式
- timer_interrupt IA32时钟中断处理函数
- do_timer_interrupt_hook 处理时钟中断
- global_clock_event->event_handler 广播处理函数为 tick_handle_periodic_broadcast
- tick_do_periodic_broadcast
- tick_do_broadcast
- td->evtdev->event_handler(td->evtdev) 调用了当前CPU上不工作设备的event_handler方法
- 所有cpu时钟设备都不工作调用 td->evtdev->broadcast(mask) 对局部APIC来说为 lapic_timer_broadcast 函数
- send_IPI_mask(mask, LOCAL_TIMER_VECTOR); 对所有与不工作时钟设备相关联的CPU,该方法都负责发送处理器间中断(inter-processor interrupt,IPI)LOCAL_TIMER_VECTOR
- tick_do_broadcast
- tick_do_periodic_broadcast
- global_clock_event->event_handler 广播处理函数为 tick_handle_periodic_broadcast
- do_timer_interrupt_hook 处理时钟中断
15.7 定时器相关系统调用的实现
15.7.1 时间基准
在使用定时器时,有3个选项可以区分如何计算经过的时间
,或定时器所处的时间基准(通常也称为时间域(time domain))。内核提供了以下形式的时间基准,在超时发生时,会触发各种信号
ITIMER_REAL
测量定时器激活以来实际流逝的时间
,以便在超时时间达到时发出信号。在这种情况下,定时器会继续运转,而不管系统是处于核心态还是用户态,或使用该定时器的应用程序当前是否在运行。在定时器到期时将发出SIGALRM类型的信号ITIMER_VIRTUAL
只在定时器的拥有者进程在用户态消耗的时间
内运行。在这种情况下,在核心态(或处理器忙于另一个应用程序)消耗的时间将忽略。定时器到期通过SIGVTALRM信号表示。ITIMER_PROF
计算进程在用户态和核心态消耗的时间
,在内核代表该进程执行系统调用时,仍然会计算时间的消耗。系统其他进程消耗的时间将忽略。定时器到期时发送的信号是SIGPROF。
顾名思义,该定时器的主要用途是剖析应用程序的性能,以查找程序中计算最密集的片段,并据此进行优化。这是一项重要的考虑,特别是在科学计算或操作系统相关应用中
定时器的类型和时间间隔的长度,都必须在创建间隔定时器时指定。在我们的例子中,使用了TTIMER_REAL来创建一个实时定时器
报警定时器的行为可以用间隔定时器仿真,选择ITIMER_REAL作为定时器类型,在第一次到期后删除定时器即可。因而可以认为,间隔定时器是报警定时器的一种一般形式
15.7.2 alarm和setitimer系统调用
alarm安装ITIMER_REAL类型的定时器(实时定时器)
,而setitimer不仅可用于安装实时定时器,还可以安装虚拟和剖析定时器
。这两个系统调用都结束于do_setitimer
。两个系统调用的实现都依赖于kernel/itimer.c
中定义的一种共同机制。实现围绕struct hrtimer
展开,因而如果高精度支持可用,那么用户层会自动利用相应的优点,而不仅仅只能在内核中利用。请注意,由于alarm使用了ITIMER_REAL类型的定时器,这两个系统调用可能相互干扰
这两个系统调用的起点分别是sys_alarm
和sys_setitimer
函数。这两个函数都使用辅助函数do_setitimer
来实际实现定时器
//kernel/itimer.c
//该函数需要3个参数。which指定了定时器类型,可以是ITIMER_REAL、ITIMER_VIRTUAL或ITIMER_PROF。value包含有关新定时器的所有信息。如果定时器将替换某个现存定时器,那么可使用ovalue来返回此前活动的定时器的描述
int do_setitimer(int which, struct itimerval *value, struct itimerval *ovalue)
// 定时器的属性
struct itimerval {
struct timeval it_interval; /* timer interval *///定时器时间间隔
struct timeval it_value; /* current value *///到定时器下一次到期之前,还剩余的时间长度
};
-
对进程结构task_struct的扩展
每个进程的
task_struct
实例都包含一个指针,指向一个struct signal_struct
的实例,其中包含了几个成员,用于容纳定时器所需的信息//include/linux/sched.h struct task_struct { ... struct signal_struct *signal; ... }; struct signal_struct { ... struct hrtimer real_timer;//将插入到内核的其他数据结构中,用于实现实时定时器。其他类型的两种定时器(虚拟和剖析)的管理无须此数据项 struct task_struct *tsk;//指向设置定时器的进程的task_struct实例。 ktime_t it_real_incr;//实时定时器的间隔在it_real_incr中指定 /* ITIMER_PROF and ITIMER_VIRTUAL timers for the process */ cputime_t it_prof_expires, it_virt_expires;//下一次定时器到期的时间 cputime_t it_prof_incr, it_virt_incr;//定时器在多长时间之后调用 ... };
有两个字段分别为剖析定时器和虚拟定时器保留:
(1) 下一次定时器到期的时间(it_prof_expires
和it_virt_expires
)。
(2) 定时器在多长时间之后调用(it_prof_incr
和it_virt_incr
)因而
每个进程可以有3个不同类型的定时器
,根据现存的数据结构,内核不能用setitimer和alarm机制管理更多的定时器。例如,一个进程可以同时执行一个虚拟定时器和一个实时定时器,但不能是两个实时定时器
POSIX定时器实现在
kernel/posix-timers.c
中,提供了对此方案的一个扩展,它允许更多的定时器,但无须更进一步讨论。虚拟和剖析定时器也是基于此框架实现的 -
实时定时器 do_setitimer实时定时器部分代码
在安装实时定时器(ITIMER_REAL)时,首先必须保存可能存在的旧定时器的属性(在新定时器安装后,这些属性将返回到用户层),并用hrtimer_try_to_cancel取消旧定时器。安装定时器时,会“覆盖”此前的定时器
定时器的时间间隔保存在特定于进程的signal_struct->it_real_incr字段(如果该字段为零,那么该定时器不是周期性的,而只能激活一次),hrtimer_start将启动定时器,该定时器在指定的时间到期
在动态定时器到期时,不会有处理程序例程在用户空间执行。相反,系统会生成一个信号,导致调用信号处理程序,因而间接调用了一个回调函数。内核如何确保该信号会被发送,而如何将定时器设置为周期性的
内核使用了回调处理程序it_real_fn,它将对所有用户空间实时定时器的执行。该函数向安装定时器的进程发送SIGALRM信号,但不会重新设置信号处理程序以使得信号具有周期性
相反,在进程上下文中投递信号时(确切地说,在dequeue_signal中),会重新安装定时器。在用hrtimer_forward前推到期时间之后,用hrtimer_restart重启定时器
在定时器到期之后,内核为何不立即重新激活定时器?更早的内核版本确实选择了这种方法,但在使用高精度定时器时,会出现问题。进程可以选择非常短的重复周期,使得定时器反复到期,这将导致在定时器代码中花费过多时间。说得直接些,这也可以称为一种拒绝服务攻击,而现在的方法避免了这种情况
-
do_setitimer 系统调用
- case ITIMER_REAL 实时定时器部分
- hrtimer_try_to_cancel 如果有旧定时器的情况,取消旧定时器.安装定时器时,会“覆盖”此前的定时器
- 计算定时器周期时间
- 启动定时器 hrtimer_start,处理函数为 it_real_fn
-
it_real_fn 实时定时器到期处理函数
- send_group_sig_info 向安装定时器的进程发送SIGALRM信号,但不会重新设置信号处理程序以使得信号具有周期性
-
dequeue_signal 进程上下文处理信号的函数
- hrtimer_forward 重设定时器到期时间
- hrtimer_restart 重启定时器
-
15.7.3 获取当前时间 sys_adjtimex
有两个理由,使得我们需要获得系统的当前时间。首先,许多操作依赖于时间戳
,例如,内核需要记录文件上次修改的时间或一些日志信息产生的时间。其次,系统的绝对时间,即外界的实际时间,是需要通知用户的
。
然而对第一项用途
来说,只要时序是连续的(即,连续操作的时间戳的顺序应与其实际操作顺序一致),绝对精度不那么重要
。但对第二项用途
来说,精度更重要些
。硬件时钟在精度方面臭名昭著,或快、或慢或者是快慢随机组合。有各种方法可解决该问题,在联网计算机的时代,一个最常见的方法就是与一个可靠的时钟源(例如,一个原子钟)通过NTP同步。由于这纯粹是一个用户层的问题,这里不会进一步讨论
内核提供了两种方法,可用于获取时间信息。
系统调用adjtimex
。有一个同名小实用程序,可用于快速显示该系统调用导出的信息。该系统调用用来读取当前内核内部时间
。其他可能性都记录在相关的手册页adjtimex(2)
中- 设备特殊文件
/dev/rtc
。该时钟源可以运作于各种模式,其中一种模式可向调用者提供当前日期和时间。
adjtimex
实现。入口点照例是sys_adjtimex
,但经过一些准备工作之后,实际工作委托给do_adjtimex。该函数相当冗长,但我们感兴趣的部分还比较短
//kernel/time.c
asmlinkage long sys_adjtimex(struct timex __user *txc_p)
- sys_adjtimex
- do_adjtimex
- do_gettimeofday 使用内核选择的最佳时钟源获得内核内部时间
- do_adjtimex
15.8 管理进程时间
task_struct实例包含了两个与进程时间有关的成员
//include/linux/sched.h
struct task_struct {
...
/*utime stime:进程在用户态,内核态的运行时间*/
cputime_t utime, stime;
...
};
update_process_times
用于管理特定于进程的时间数据,从局部时钟调用
代码流程图:
- update_process_times 更新进程的时间数据
- account_process_tick 使用account_user_time或account_sys_time来更新进程在用户态或核心态消耗的CPU时间,即task_struct中的utime或stime成员。如果进程超出了Rlimit指定的CPU份额限制,那么还会每隔1秒发送SIGXCPU信号
- run_local_timers 使定时器激活和过期,该函数又引发了软中断TIMER_SOFTIRQ,而其处理程序函数负责运行低精度定时器
- scheduler_tick 用于CPU调度器
- run_posix_cpu_timers 使当前注册的POSIX定时器开始运行。这包括运行前述的的间隔定时器,因为其实现基于POSIX定时器
总结
低精度定时器,第一种定时子系统(旧的机制),通过每个数组项记录一个jiffies时间的所有定时器,进行处理
通用时间子系统,第二个定时子系统的核心,新引入的机制,实现高精度时钟的基础。各CPU一个局部时钟设备,提供局部时钟。精度最高的CPU用于全局时钟jiffies。定时器在红黑树中排序。给每个CPU创建一个定时器来模拟低精度定时器。
高精度时钟使用单触发模式,
高精度时钟实现的定时器不依赖系统tick中断,而是基于事件触发
高精度时钟创建一个hrtimer,模拟周期tick中断
动态时钟,用于低功耗(系统深度休眠),停止了时钟
动态时钟必须使用单触发模式的时钟事件设备
低精度时钟时 在 tick_check_oneshot_change 函数中将 事件处理函数设为 tick_nohz_handler 来处理动态时钟
高精度时钟时 在开始模拟低精度时钟时将模式设为 NOHZ_MODE_HIGHRES 就启动了动态时钟
动态时钟在idle进程中关闭周期时钟若干时间,直到有新进程需要调度或被中断打断时重启周期时钟
广播模式,有的设备进入省电模式后时钟设备会睡眠,这时用其他CPU上的时钟设备代替停止的设备,并通过跨处理器的中断来定期调用处理函数(跨处理器中断比较慢会丢失精度),如果需要广播,内核总是切换到低精度模式
=========================================
涉及的命令和配置:
Documentation/hrtimers.txt
CONFIG_HZ
:配置时钟中断每秒HZ次
BASE_SMALL
:小系统使用该配置减少组定时器数组大小
CONFIG_NO_HZ
:启用动态时钟
CONFIG_HIGH_RES_TIMERS
:启用高精度定时器支持
GENERIC_TIME
:支持通用时间框架,动态时钟和高精度定时器的必要前提
GENERIC_CLOCKEVENTS
:支持通用时钟事件,动态时钟和高精度定时器的必要前提
CONFIG_TICK_ONESHOT
:时钟事件设备的单触发模式,高精度定时器或动态时钟,会自动选中该选项
GENERIC_CLOCKEVENTS_BROADCAST
:支持时钟广播机制,用于省电模式
全局变量 jiffies_64
,还有历史遗留的 jiffies
,他们分别声明,但用于联编最终的内核二进制映像的链接器脚本中,指定了jiffies等同于jiffies_64的低位4个字节,根据底层体系结构的字节序不同,这可能是jiffies_64的前4个或后4个字节。在64位机器上,这两个变量是同义词
命令w
显示前1分钟、5分钟、15分钟内,平均有多少个就绪状态的进程在就绪队列上等待
全局变量 boot_tvec_bases
,每个CPU一个定时器管理结构体
全局变量 clocksource_list
,系统中所有注册的时钟源在该链表中,根据rating对所有可用的时钟源进行排序
用户层可以通过/sys/devices/system/clocksource/clocksource0/current_clocksource
指定优先选择的时钟源
tick_sched中收集的统计信息在用户层中的位置/proc/timer_list
全局CPU变量tick_cpu_sched
,提供了一个struct tick_sched实例。这是必须的,因为对时钟的禁用是按CPU指定的,而不是对整个系统指定
全局链表头 clockevent_devices
,存着所有时钟事件设备
全局链表头 tick_cpu_device
,存着局部时钟设备(struct tick_device)集合
全局变量tick_broadcast_device
,存着用于广播设备的tick_device实例