一、Linux内核定时器
1.1. 内核定时器实现原理
软件定时器本质上依赖于硬件定时器中断对系统节拍进行维护并触发软件定时器中断处理,软件定时器中断处理再检查定时器列表中已到期的定时器,执行定时器到期处理函数。
具体到Linux内核,硬件定时器中断产生后会更新系统节拍数jiffies/jiffies_64,同时触发软件定时器软中断TIMER_SOFTIRQ,由软中断运行当前处理器上所有到期的定时器处理函数。
但硬件定时器中断是Machine层的编码,Linux在Machine层和通用框架层之间定义了clock_event_device结构,以便将硬件接口和软件框架独立,硬件平台只需要实现给定的接口。参考:Linux时间子系统之四:定时器的引擎:clock_event_device-优快云博客
1.2. 内核定时器使用
1.2.1. 节拍与配置
硬件定时器中断的频率就是系统频率,也叫做节拍率(tick rate) 。
系统节拍率是可以设置的,单位是 Hz,对应的内核宏值为HZ,其定义在/linux-4.1.15/include/asm-generic/param.h。如果修改了头文件里的HZ值,则必须使用新的值重新编译内核以及所有模块。
4 #include <uapi/asm-generic/param.h>
5
6 # undef HZ
7 # define HZ CONFIG_HZ /* Internal kernel timer frequency */
8 # define USER_HZ 100 /* some user interfaces are */
9 # define CLOCKS_PER_SEC (USER_HZ) /* in "ticks" like times() */
10 #endif /* __ASM_GENERIC_PARAM_H */
系统节拍频率高低的优缺点?
系统节拍频率越高,定时和延时支持的精度就越高,同时,硬件中断触发的次数也越多,对处理器的负担也就越大。
1.2.2. 低精度定时器
低精度定时器在内核中以链表数组的形式分组,分组的依据是定时器超时时间expires与当前cpu定时器所经历过的jiffies数的差值,并以expires的各个位段作为下标,方便系统以当前jiffies的各个位段值作为索引批量处理。参考:Linux时间子系统之五:低分辨率定时器的原理和实现-优快云博客
timer_list 定义在文件include/linux/timer.h 中:
struct timer_list {
// 定时器在内核定时器链表中的节点,用于链接到内核的定时器管理链表中
struct list_head entry;
// 定时器超时时间,单位是节拍数(jiffies)。当系统节拍数达到此值时,定时器触发
unsigned long expires;
// 指向定时器所属的基准时钟(tvec_base)结构的指针。基准时钟管理一组定时器,用于高效处理大量定时器的超时
struct tvec_base *base;
// 定时器触发时要调用的处理函数。该函数接受一个无符号长整型参数
void (*function)(unsigned long);
// 要传递给上面提到的 function 函数的参数。这允许定时器处理函数根据这个参数执行不同的操作
unsigned long data;
// 松弛时间(slack time),用于调整定时器的精度和减少系统负载。当设置为非零值时,允许定时器在到期时间后的一段时间内触发,以优化性能
int slack;
#ifdef CONFIG_TIMER_STATS
// 定时器启动时进程的PID(进程标识符)。这个字段用于统计和调试目的,帮助跟踪定时器的来源
int start_pid;
// 指向定时器启动位置(代码地址)的指针。这有助于调试和跟踪定时器的设置位置
void *start_site;
// 存储启动定时器的进程的名称。这同样有助于调试和跟踪
char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
// 锁依赖映射。这是Linux内核锁调试功能的一部分,用于跟踪和检测可能的锁顺序错误
struct lockdep_map lockdep_map;
#endif
};
定时器定义、注册、调整、删除:
// 初始化
DEFINE_TIMER(_name,_function,_expires,_data);
setup_timer(_timer,_fn,_data,_flags);
void init_timer(struct timer_list *timer);
// 添加定时器
void add_timer(struct timer_list *timer);
// 删除定时器
void del_timer(struct timer_list *timer);
// 同步删除,在删除时会等待定时器处理完毕, 不可在中断上下文中调用
void del_timer_sync(struct timer_list *timer);
// 调整定时器
int mod_timer (struct timer_list *timer,unsigned long expires);
expires设置:
expires = jiffies + HZ; // 定时1s, HZ即系统1s节拍数
expires = jiffies + msecs_to_ jiffies(1); // 定时1ms
expires = jiffies + usecs_to_ jiffies(1); // 定时1us
注:在使用内核定时器的时候要注意,内核定时器并不是周期性运行的,超时就会关闭。因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
1.2.3. 高精度定时器
高精度定时器hrtimer由独立的一套子系统实现,由红黑树维护所有已注册的高精度定时器,按照超时时间排序,最快超时的hrtimer位于红黑树的左下叶子节点。参考:Linux时间子系统之六:高精度定时器(HRTIMER)的原理和实现_时间子系统之高精度 timer 实现-优快云博客
高精度定时器定义:
<include/linux/hrtimer.h>
struct hrtimer {
struct timerqueue_node node; // 定时器存储结构, 红黑树的进一步封装
ktime_t _softexpires; // 软过期时间
enum hrtimer_restart (*function)(struct hrtimer *); // 定时器到期时调用的回调函数
struct hrtimer_clock_base *base; // 所属时间基准系统指针
unsigned long state; // 定时器状态
......
};
/* 1.定时器存储数据结构 */
struct timerqueue_node {
struct rb_node node; // 红黑树的节点
ktime_t expires; // 该节点代表队hrtimer的到期时间,与hrtimer结构中的_softexpires稍有不同
};
struct timerqueue_head {
struct rb_root head; // 红黑树的根节点
struct timerqueue_node *next; // 该红黑树中最早到期的节点,也就是最左下的节点
};
/* 2.回调函数返回值 */
// 定时器一旦到期,function字段指定的回调函数会被调用,该函数的返回值为一个枚举值,它决定了该hrtimer是否需要被重新激活
enum hrtimer_restart {
HRTIMER_NORESTART, /* Timer is not restarted */
HRTIMER_RESTART, /* Timer must be restarted */
};
/* 3.时基系统 */
// clock_base数组为每种时间基准系统都定义了一个hrtimer_clock_base结构,它的定义如下:
struct hrtimer_cpu_base {
......
struct hrtimer_clock_base clock_base[HRTIMER_MAX_CLOCK_BASES];
};
struct hrtimer_clock_base {
struct hrtimer_cpu_base *cpu_base; // 指向所属cpu的hrtimer_cpu_base结构
......
struct timerqueue_head active; // 红黑树,包含了所有使用该时间基准系统的hrtimer
ktime_t resolution; // 时间基准系统的分辨率
ktime_t (*get_time)(void); // 获取该基准系统的时间函数
ktime_t softirq_time;// 当用jiffies
ktime_t offset; //
};
// 枚举定义高精度定时器(hrtimer)可以基于的不同时间基准
enum hrtimer_base_type {
// 相对于系统启动点的不间断递增时间,不受系统时间调整影响
HRTIMER_BASE_MONOTONIC,
// 实时时钟时间,通常与墙上时钟同步,可以受到系统时间调整(如用户手动设置时间)的影响
HRTIMER_BASE_REALTIME,
// 自系统启动以来的时间,包括系统睡眠时间,即考虑了系统休眠和唤醒的时间
HRTIMER_BASE_BOOTTIME,
// 国际原子时(TAI),是一个连续递增的时间标准,不受闰秒调整影响
HRTIMER_BASE_TAI,
// 基于MONOTONIC的软时钟,用于实现基于monotonic时间的延迟执行,但精度可能不如直接基于MONOTONIC的定时器
HRTIMER_BASE_MONOTONIC_SOFT,
// 基于REALTIME的软时钟,用于实现基于realtime时间的延迟执行,但精度可能不如直接基于REALTIME的定时器
HRTIMER_BASE_REALTIME_SOFT,
// 基于BOOTTIME的软时钟,用于实现基于系统启动以来的时间(包括休眠时间)的延迟执行,但精度可能不如直接基于BOOTTIME的定时器
HRTIMER_BASE_BOOTTIME_SOFT,
// 基于TAI的软时钟,用于实现基于TAI时间的延迟执行,但精度可能不如直接基于TAI的定时器
HRTIMER_BASE_TAI_SOFT,
// 枚举的最大值,用于内部计算或作为数组大小等,不代表一个有效的时间基准
HRTIMER_MAX_CLOCK_BASES,
};
/* 4.状态字段 */
// state字段用于表示hrtimer当前的状态,有几下几种位组合:
#define HRTIMER_STATE_INACTIVE 0x00 // 定时器未激活
#define HRTIMER_STATE_ENQUEUED 0x01 // 定时器已经被排入红黑树中
#define HRTIMER_STATE_CALLBACK 0x02 // 定时器的回调函数正在被调用
#define HRTIMER_STATE_MIGRATE 0x04 // 定时器正在CPU之间做迁移
高精度定时器使用:
/**
* hrtimer_init - 初始化高精度定时器
* @clock_id: 指定定时器使用的时钟源,例如CLOCK_REALTIME、CLOCK_MONOTONIC等
* @mode: 定时器的工作模式,例如HRTIMER_MODE_REL(相对时间)或HRTIMER_MODE_ABS(绝对时间)
*/
void hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
enum hrtimer_mode mode);
/**
* hrtimer_start - 启动高精度定时器
*/
void hrtimer_start(struct hrtimer *timer, ktime_t tim,
const enum hrtimer_mode mode);
/**
* hrtimer_start_range_ns - 启动高精度定时器,并指定一个时间范围
* @delta_ns: 允许定时器到期时间有一个纳秒级的偏差范围
*/
void hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,
u64 delta_ns, const enum hrtimer_mode mode);
/**
* hrtimer_forward_now - 将定时器的到期时间向前调整到当前时间之后的某个时间点
* @tim: 新的到期时间点,使用ktime_t类型表示,相对于当前时间
* 此函数用于将定时器的到期时间向前调整,确保它不会在过去的时间点触发。
* 这通常用于在定时器回调函数中重新安排定时器时,确保新的到期时间在当前时间之后。
*/
void hrtimer_forward_now(struct hrtimer *timer, ktime_t tim);
/**
* hrtimer_cancel - 取消高精度定时器
*/
int hrtimer_cancel(struct hrtimer *timer);
enum hrtimer_mode {
/* 有以下5中基本模式,其他都是基本模式的组合 */
HRTIMER_MODE_ABS = 0x00, /* 定时器到期时间是绝对值 */
HRTIMER_MODE_REL = 0x01, /* 定时器到期时间是相对值 */
HRTIMER_MODE_PINNED = 0x02, /* 定时器需要绑定到某个CPU上 */
HRTIMER_MODE_SOFT = 0x04, /* 定时器是“软”的,即该定时器的回调函数在软中断上下文执行 */
HRTIMER_MODE_HARD = 0x08, /* 定时器是“硬”的,即该定时器的回调函数在硬中断上下文执行即使是PREEMPT_RT打开的情况 */
HRTIMER_MODE_ABS_PINNED = HRTIMER_MODE_ABS | HRTIMER_MODE_PINNED,
HRTIMER_MODE_REL_PINNED = HRTIMER_MODE_REL | HRTIMER_MODE_PINNED,
HRTIMER_MODE_ABS_SOFT = HRTIMER_MODE_ABS | HRTIMER_MODE_SOFT,
HRTIMER_MODE_REL_SOFT = HRTIMER_MODE_REL | HRTIMER_MODE_SOFT,
HRTIMER_MODE_ABS_PINNED_SOFT = HRTIMER_MODE_ABS_PINNED | HRTIMER_MODE_SOFT,
HRTIMER_MODE_REL_PINNED_SOFT = HRTIMER_MODE_REL_PINNED | HRTIMER_MODE_SOFT,
HRTIMER_MODE_ABS_HARD = HRTIMER_MODE_ABS | HRTIMER_MODE_HARD,
HRTIMER_MODE_REL_HARD = HRTIMER_MODE_REL | HRTIMER_MODE_HARD,
HRTIMER_MODE_ABS_PINNED_HARD = HRTIMER_MODE_ABS_PINNED | HRTIMER_MODE_HARD,
HRTIMER_MODE_REL_PINNED_HARD = HRTIMER_MODE_REL_PINNED | HRTIMER_MODE_HARD,
};
1.2.4. 动态时钟
为了降低系统功耗,在系统没有活动的进程时由idle进程判断是否可以停止周期时钟。
这部分较为复杂,只学了皮毛,可以参考:Linux时间子系统之八:动态时钟框架(CONFIG_NO_HZ、tickless)_linux时间片tick config-优快云博客
二、Linux延时
2.1. 内核延时任务
延时任务是对工作队列和定时器功能的封装,结构为delayed_work:
struct delayed_work{
struct work_struct work; // 任务结构
struct timer_list timer; // 定时器结构
struct workqueue_struct *wq; // 所属任务队列
int cpu;
};
delayed_work相关接口:
// 在指定的delay后, delayed_work结构体中的work成员中, work_func_t成员会被执行
// delay的单位是jiffies, 常见用法schedule_delayed_work(&work, msecs_to_jiffies(mdelay));
// 如果要实现周期性执行任务, 则在work_func_t函数中再次调用schedule_delayed_work
int schedule_delayed_work(struct delayed_work *work, unsigned long delay);
// 取消delayed_work
int cancel_delayed_work(struct delayed_work *work);
int cancel_delayed_work_sync(struct delayed_work *work);
2.2. 内核延时接口
2.2.1. 忙等待延时
忙等待延时本质上是执行一定次数的循环,以耗费延时时间。(会耗费CPU资源!!!)
内核在启动时,会运行一个延迟循环校准,得到每次循环对应的jiffy数量 (Loops Per Jiffy)。后续延时就根据lpj来判断执行循环多少次。
// 以时间为单位延时
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
// 以jiffies为单位延时
unsigned long delay = jiffies + Hz;
// 注:内核会把jiffies变量定义为volatile, 防止编译优化将两次读取合并, 造成死循环
while (time_before(jiffies, delay)); // 延时1s
2.2.2. 睡眠延时
睡眠延时是使当前进程睡眠一段时间后再唤醒,睡眠期间不会占用CPU。
schedule_timeout()可以使当前任务休眠指定的jiffies之后重新被调度执行。msleep()本质上是对schedule_timeout()的封装实现。schedule_timeout()会向系统添加一个定时器, 在定时器处理函数中唤醒对应的进程。受中断以及进程调度影响, msleep()的精度有限。
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
还有一种睡眠延时实现是将当前进程添加到等待队列中,从而在等待队列上睡眠,当超时发生时,进程被唤醒。
sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);