内核定时器1-普通定时器
定时器与中断关系
软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ 软中断,运行当前处理器上到期的所有定时器。
内核的时钟中断(例如,周期性的时钟中断)通常是由硬件定时器(如 PIT、TSC 或 HPET)触发的,通过中断控制器(如GIC)将中断发送给CPU。这些硬中断为内核提供了时钟信号,并更新系统时间(jiffies),从而使内核能够计算时间间隔,并决定何时触发定时器。
linux中查看可用硬件时钟源和当前时钟源:
~$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc kvm-clock acpi_pm
~$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
tsc
lscpu
查看CPU是否支持TSC
$ lscpu | grep -i tsc
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 cx16 sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx rdrand hypervisor lahf_lm abm 3dnowprefetch fsgsbase avx2 invpcid rdseed clflushopt md_clear flush_l1d arch_capabilities
通过内核配置,查看内核是否启用了HPET、TSC或PIT等定时器
$ grep CONFIG_HPET /boot/config-$(uname -r)
CONFIG_HPET_TIMER=y
CONFIG_HPET_EMULATE_RTC=y
CONFIG_HPET=y
CONFIG_HPET_MMAP=y
CONFIG_HPET_MMAP_DEFAULT=y
内核定时器
Linux 内核提供内核定时器机制,其核心是由硬件产生中断来追踪时间的流动情况。内核中很多部分的工作都高度依赖于时间数据信息,比如周期性的调度程序、延时程序,对于驱动开发说最常用的就是定时器。Linux 内核提供不同的定时器以支持忙等或睡眠等待等时间的相关服务。
在Linux设备驱动编程中,可以利用Linux内核中提供的一组函数和数据结构来完成定时触发工作或者完成某周期性的事务。这组函数和数据结构使得驱动工程师在多数情况下不用关心具体的软件定时器究竟对应着怎样的内核和硬件行为。
Jiffies和HZ
在 Linux 内核中,jiffies
和 HZ
是密切相关的两个概念,它们用于表示和管理时间的流逝。它们是定时器的基础,先介绍这两个概念。
Jiffies
jiffies定义
jiffies
是内核中用于表示时间的一个全局变量,通常用于衡量系统自启动以来的时间流逝。它的作用类似于一个时钟计数器,在每个时钟中断时递增。具体来说,jiffies 用于表示经过的“滴答数”,每一滴答对应一个固定的时间间隔。
jiffies
是一个全局变量,类型通常为unsigned long
,它记录的是自系统启动以来的时钟滴答数。- 每个硬件时钟中断(或称为时钟滴答)都会增加
jiffies
的值。内核定时器、调度等功能依赖于jiffies
来计算时间。
jiffies
定义代码如下。 对于 64 位系统,jiffies_64
是扩展的 64 位版本,用于避免溢出。
/*
* The 64-bit value is not atomic - you MUST NOT read it
* without sampling the sequence number in jiffies_lock.
* get_jiffies_64() will do this for you as appropriate.
*/
extern u64 __cacheline_aligned_in_smp jiffies_64;
extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies;
如何计算jiffies的值
jiffies
的值依赖于系统的时钟频率,也就是 HZ
(后面会讲到)。假设 HZ
的值为 1000,那么系统每秒钟会产生 1000 个时钟滴答。在这种情况下,jiffies
会每隔 1/1000 秒(1 毫秒)增加 1。
jiffies
的值在每次时钟中断时更新:
- 通过硬件定时器(如 APIC 或 GIC)触发中断。
- 中断服务例程中,
jiffies
被递增。
如何使用jiffies
jiffies
用于内核内部的定时器、延迟操作、定时任务等。例如,内核会定期检查 jiffies
的值来判断定时器是否到期。
查看jiffies
:~$ cat /proc/timer_list | grep jiffies
.idle_jiffies : 4423611186
.last_jiffies : 4423611187
jiffies: 4423611187
.idle_jiffies : 4423611177
.last_jiffies : 4423611187
jiffies: 4423611187
.idle_jiffies : 4423611182
.last_jiffies : 4423611187
jiffies: 4423611187
.idle_jiffies : 4423611148
.last_jiffies : 4423611187
jiffies: 4423611187
.idle_jiffies : 4423611183
.last_jiffies : 4423611188
jiffies: 4423611188
.idle_jiffies : 4423611188
.last_jiffies : 4423611188
jiffies: 4423611188
HZ
HZ
是内核中定义的一个常量,用来表示系统时钟中断的频率。简单来说,HZ
表示每秒钟时钟滴答的数量,也就是每秒钟内核会产生多少次硬件时钟中断。
HZ的作用
- 控制时钟中断频率:
HZ
决定了jiffies
每秒增加多少次。例如,如果HZ
的值是 1000,那么每秒钟jiffies
会增加 1000 次。 - 调度粒度:
HZ
还控制着内核调度的粒度,即内核每隔多少时间检查一次是否需要切换进程。如果HZ
较大,进程切换的粒度较小,调度更加频繁,反之亦然。
常见的HZ
值
- 在传统的 32 位 x86 系统上,通常
HZ
为 100 或 1000,表示每秒钟内核产生 100 次或 1000 次时钟中断。 - 在高性能系统(如实时操作系统)中,
HZ
可能设置得更高,以提供更精细的时间管理。
如何设置HZ
HZ
的值在内核编译时通过配置文件设置。在现代的 Linux 系统中,HZ
值的选择通常是根据硬件平台和性能需求来设定的。常见的 HZ
值有:
- 100:适用于一些低功耗设备或嵌入式系统。
- 1000:在桌面或服务器上比较常见。
定义位置
HZ
的定义代码如下:
# undef HZ
# define HZ CONFIG_HZ /* Internal kernel timer frequency */
# define USER_HZ 100 /* some user interfaces are */
HZ
的设置影响到系统的时钟精度、进程调度的精细度以及定时器的精度。较高的 HZ
值提供更高的精度,但也会增加系统的开销。
查看HZ
$ grep HZ /boot/config-$(uname -r)
# CONFIG_HZ_100 is not set
CONFIG_HZ_250=y
CONFIG_HZ=250
jiffies与HZ的关系
jiffies
表示系统自启动以来的时钟滴答数,而HZ
定义了每秒钟系统产生多少个时钟滴答。- 如果
HZ
是 1000,则每秒钟jiffies
增加 1000 次,表示每个jiffies
间隔为 1 毫秒。 - 如果
HZ
是 100,那么每秒钟jiffies
增加 100 次,表示每个jiffies
间隔为 10 毫秒。
通过 HZ
,内核可以通过时钟中断来更新 jiffies
,并基于此来调度任务、处理定时器等。
总结
**jiffies**
:是内核中表示时间的计数器,每次硬件时钟中断时递增,用于记录系统自启动以来的时间流逝。**HZ**
:是系统时钟中断的频率,表示每秒钟产生的时钟中断的次数,进而决定了jiffies
的增速。HZ
控制着内核的时间管理、调度粒度和定时器精度。
这两个概念紧密关联,HZ
决定了 jiffies
的增速,而 jiffies
提供了内核处理定时任务、调度和时间管理的基础。
内核定时器API
Linux内核所提供的用于操作定时器的数据结构和函数如下。
struct timer_list
struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct hlist_node entry; // 定时器链表
unsigned long expires; // 定时器过期时间
void (*function)(unsigned long); // 回调函数
unsigned long data;
u32 flags;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
当定时器期满后,其中第8行的function()成员将被执行,而第9行的data成员则是传入其中的参
数,第7行的expires则是定时器到期的时间(jiffies)。
定时器使用链表管理,为什么不使用红黑树?
这里的list,管理的是同一个expire下的所有timer。如果timer数目较多的情况,使用list确实比较低效。内核在高分辨率定时器中,使用红黑树进行hrtimer的管理。
对于整个内核普通定时器,使用的是timer wheel进行管理的。
timer wheel(时间轮)
timer wheel
的设计,借鉴了钟表的原理:分小时、分钟、秒 3个层级对时间进行管理。以下是Linux内核中timer wheel
的说明图,它展示了多级时间轮的层级结构和工作原理:
Timer Wheel:Linux内核使用多级时间轮(Timer Wheel)来管理定时器,图中分为4个层级(Tier),内核中实际层级更多。
工作流程
在Linux内核的Timer Wheel(时间轮)中,只有当定时器到达最低层级(Tier 1)时才会真正触发(fire)。其他层级(Tier 2、Tier 3、Tier 4)中的定时器不会直接触发,而是会在时间轮转动时逐步迁移到更低层级,直到最终到达 Tier 1 时才触发。
详细解释:
- Tier 1(最低层级):
- Tier 1 的定时器是直接触发的,当系统时钟周期(tick)到达定时器的到期时间时,内核会执行该定时器的回调函数。
- Tier 1 的槽位覆盖了最近的 256 个 ticks(通常是 0-255 个时钟周期)。
- Tier 2、Tier 3、Tier 4(更高层级):
- 更高层级的定时器不会直接触发,它们的作用是管理较长时间的定时器。
- 当一个定时器被添加到 Timer Wheel 时,如果它的到期时间超出了当前层级的时间范围,它会被分配到更高层级。
- 随着时间的推移,系统时钟每经过一个层级的时间范围(例如 Tier 1 的 256 ticks),Timer Wheel 会进行一次“迁移(cascade)”操作,将定时器从高一层级迁移到低一层级。
- 最终,当定时器迁移到 Tier 1 并到达其到期时间时,才会触发。
示例:
假设我们有一个定时器的到期时间是 1000 ticks:
- 初始分配:由于 1000 ticks 超出了 Tier 1 的范围(0-255 ticks),因此它会被分配到 Tier 2(256-65535 ticks)。
- 逐步迁移:随着时间的推移,当系统时钟经过 256 ticks 时,Timer Wheel 会进行一次迁移操作,将 Tier 2 中的定时器移动到 Tier 1。
- 触发:当定时器最终迁移到 Tier 1 并且到期时间到达时,定时器才会触发(fire),执行其回调函数。
代码
内核中相关代码如下:
/* Size of each clock level */
#define LVL_BITS 6
#define LVL_SIZE (1UL << LVL_BITS)
/* Level depth */
#if HZ > 100
# define LVL_DEPTH 9
# else
# define LVL_DEPTH 8
#endif
#define WHEEL_SIZE (LVL_SIZE * LVL_DEPTH)
struct timer_base {
spinlock_t lock;
struct timer_list *running_timer;
unsigned long clk;
unsigned long next_expiry;
unsigned int cpu;
bool migration_enabled;
bool nohz_active;
bool is_idle;
DECLARE_BITMAP(pending_map, WHEEL_SIZE);
struct hlist_head vectors[WHEEL_SIZE];
} ____cacheline_aligned;
时间轮的优势
- 效率:时间轮可以以 O(1) 的时间复杂度插入、删除定时器,而传统的线性结构(如链表)需要 O(n) 的时间复杂度。
- 内存优化:通过ring buffer避免了过多的内存分配和清理。
- 高效的定时器管理:即使是有大量定时器的场景,时间轮仍能高效地管理和触发定时器。
内核定时器处理流程
- 定时器注册与设置: 当内核定时器被创建并设置时,它会被添加到内核的定时器队列中。定时器结构中包含了定时器到期的时间点(
expires
),并与内核时钟(jiffies
)进行比较。 - 定时器的检查: 内核定时器并不会实时地轮询或执行,而是依赖于内核周期性地检查定时器队列。在每次时钟中断(硬中断触发时)时,内核会检查当前时间与已注册定时器的到期时间是否匹配。如果某个定时器到期,内核会将它的回调函数排入待处理队列。
- 软中断与工作队列: 一旦定时器到期,内核通常不会立即在硬中断上下文中执行定时器回调函数,而是将其放入软中断队列(TIMER_SOFTIRQ)或者工作队列中。然后,内核会在合适的时机(通常是在退出硬中断后)调度这些回调函数的执行。
初始化定时器
#define init_timer(timer) \
__init_timer((timer), 0)
它的原型等价于:
void init_timer(struct timer_list * timer);
上述init_timer()函数是timer进行初始化,初始化timer_list的entry的pprev为NULL。
增加定时器
void add_timer(struct timer_list *timer);
上述函数用于注册内核定时器,将定时器加入到内核动态定时器链表中。
删除定时器
int del_timer(struct timer_list * timer);
上述函数用于删除定时器。
修改定时器的expire
int mod_timer(struct timer_list *timer, unsigned long expires);
上述函数用于修改定时器的到期时间,在新的被传入的expires到来后才会执行定时器函数。
内核定时器使用sample
下面代码给出了一个完整的内核定时器使用sample,在大多数情况下,设备驱动都如这个sample那样使用定时器。
/* xxx设备结构体 */
struct xxx_dev {
struct cdev cdev;
timer_list xxx_timer; /* 设备要使用的定时器 */
...
};
/* xxx驱动中的某函数 */
void xxx_func1(...)
{
struct xxx_dev *dev = filp->private_data;
...
/* 初始化定时器 */
init_timer(&dev->xxx_timer);
dev->xxx_timer.function = &xxx_do_timer;
dev->xxx_timer.data = (unsigned long)dev;
/* 设备结构体指针作为定时器处理函数参数 */
dev->xxx_timer.expires = jiffies + delay;
/* 添加(注册)定时器 */
add_timer(&dev->xxx_timer);
...
}
/* xxx驱动中的某函数 */
void xxx_func2(...)
{
...
/* 删除定时器 */
del_timer(&dev->xxx_timer);
...
}
/* 定时器处理函数 */
static void xxx_do_timer(unsigned long arg)
{
struct xxx_device *dev = (struct xxx_device *)(arg);
...
/* 调度定时器再执行 */
dev->xxx_timer.expires = jiffies + delay;
add_timer(&dev->xxx_timer);
...
}
第18、39行可以看出,定时器的到期时间往往是在目前jiffies的基础上添加一个时延,若
为Hz,则表示延迟1s。
在定时器处理函数中,在完成相应的工作后,往往会延后expires并将定时器再次添加到内核定时器链
表中,以便定时器能再次被触发。
高精度定时器
上面介绍的定时器, 精度受内核 HZ 值的限制,例如 HZ 为 1000 时,精度为 1 毫秒。 那么内核如何支持更高精度的定时器呢?
下一篇定时器的文章,将主要介绍高精度定时器。
用户空间定时器
用户空间的定时器,也是基于内核定时器的机制。关于用户应用程序使用定时器的方法,会在另一篇文章中介绍。
参考资料
- Professional Linux Kernel Architecture,Wolfgang Mauerer
- Linux内核深度解析,余华兵
- Linux设备驱动开发详解,宋宝华