Linux内核_软件定时器
1 内核节拍
1.1 系统节拍
Linux内核通过CONFIG_HZ
来配置系统节拍频率,打开文件 include/asm-generic/param.h,有如下内容:
#ifndef __ASM_GENERIC_PARAM_H
#define __ASM_GENERIC_PARAM_H
#include <uapi/asm-generic/param.h>
# undef HZ
# define HZ CONFIG_HZ /* Internal kernel timer frequency */
# define USER_HZ 100 /* some user interfaces are */
# define CLOCKS_PER_SEC (USER_HZ) /* in "ticks" like times() */
#endif /* __ASM_GENERIC_PARAM_H */
其中CONFIG_HZ
表示系统节拍。
1.2 全局jiffies变量
Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0,jiffies按照CONFIG_HZ
设定的频率计数,表示内核的节拍数,jiffies 定义在文件 include/linux/jiffies.h 中,定义如下:
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;
iffies_64 和 jiffies 其实是同一个东西,jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统。 为了兼容不同的硬件,jiffies 其实就是 jiffies_64 的低 32 位。
Linux内核提供的处理时间绕回:
函数 | 描述 |
---|---|
time_after(unkown, known) | unkown在known之后,则返回真 |
time_before(unkown, known) | |
time_after_eq(unkown, known) | |
time_before_eq(unkown, known) |
Linux 内核提供了几个 jiffies 和 ms、us、ns 之间的转换函数如下:
函数 | 描述 |
---|---|
int jiffies_to_msecs(const unsigned long j) | 将毫秒转换为 jiffies 类型。 |
int jiffies_to_usecs(const unsigned long j) | 将微秒转换为 jiffies 类型。 |
u64 jiffies_to_nsecs(const unsigned long j) | 将纳秒转换为 jiffies 类型。 |
long msecs_to_jiffies(const unsigned int m) | 将 jiffies 类型的参数 j 分别转换为对应的毫秒。 |
long usecs_to_jiffies(const unsigned int u) | 将 jiffies 类型的参数 j 分别转换为对应的微秒。 |
unsigned long nsecs_to_jiffies(u64 n) | 将 jiffies 类型的参数 j 分别转换为对应的纳秒。 |
1.3 深入系统滴答
在开发板执行以下命令,可以看到 CPU0 下有一个数值变化特别快,它就是滴答中断:
# cat /proc/interrupts
CPU0
16: 2532 GPC 55 Level i.MX Timer Tick
19: 22 GPC 33 Level 2010000.ecspi
20: 384 GPC 26 Level 2020000.serial
21: 0 GPC 98 Level sai
- 以 100ASK_IMX6ULL 为例,滴答中断名字就是“i.MX Timer Tick”。在 Linux内核源码目录下执行以下命令:
$ grep "i.MX Timer Tick" * -nr
drivers/clocksource/timer-imx-gpt.c:319: act->name = "i.MX Timer Tick";
- 打开 timer-imx-gpt.c 319 行左右,可得如下源码:
act->name = "i.MX Timer Tick";
act->flags = IRQF_TIMER | IRQF_IRQPOLL;
act->handler = mxc_timer_interrupt;
act->dev_id = ced;
return setup_irq(imxtm->irq, act);
- mxc_timer_interrupt 应该就是滴答中断的处理函数,代码如下:
static irqreturn_t mxc_timer_interrupt(int irq, void *dev_id)
{
struct clock_event_device *ced = dev_id;
struct imx_timer *imxtm = to_imx_timer(ced);
uint32_t tstat;
tstat = readl_relaxed(imxtm->base + imxtm->gpt->reg_tstat);
imxtm->gpt->gpt_irq_acknowledge(imxtm);
ced->event_handler(ced);
return IRQ_HANDLED;
}
对于jiffies
的累加,是在ced->event_handler(ced)
中进行的,最终调用的是tick_handle_periodic
函数。
2 内核定时器
2.1 timer_list结构
定时器是一个很常用的功能,需要周期性处理的工作都要用到定时器。Linux 内核定时器 采用系统时钟来实现。内核定时器并不是周期性运行的,超时以后就会自动关闭。 Linux 内核使用 timer_list 结构体表示内核定时器,timer_list 定义在文件include/linux/timer.h 中:
struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct list_head entry;
unsigned long expires;
struct tvec_base *base;
void (*function)(unsigned long);
unsigned long data;
int slack;
#ifdef CONFIG_TIMER_STATS
int start_pid;
void *start_site;
char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
- 在 add_timer 之前,直接修改:
timer.expires = jiffies + xxx; // xxx 表示多少个滴答后超时,也就是 xxx*10ms
timer.expires = jiffies + 2*HZ; // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒
- 在 add_timer 之后,使用 mod_timer 修改:
mod_timer(&timer, jiffies + xxx); // xxx 表示多少个滴答后超时,也就是 xxx*10ms
mod_timer(&timer, jiffies + 2*HZ); // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒
要使用内核定时器首先要先定义一个 timer_list 变量,表示定时器:
-
tiemr_list 结构体的expires 成员变量表示超时时间,单位为节拍数。比如我们现在需要定义一个周期为 2 秒的定时器,那么这个定时器的超时时间就是 jiffies+(2HZ),因此 expires=jiffies+(2HZ);
-
function 就是定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定时处理函数。
2.1 内核定时器相关API
-
init_timer (4.15之后好像弃用,改用timer_setup)
初始化定时器,定义一个
timer_list
类型的变量之后需要通过init_timer
对其初始化:
void init_timer(struct timer_list *timer)
- add_timer
add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行,函数原型如下:
void add_timer(struct timer_list *timer)
- del_timer
del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。del_timer 函数原型如下:
int del_timer(struct timer_list * timer)
返回值:0,定时器还没被激活;1,定时器已经激活。
- del_timer_sync
del_timer_sync
函数是 del_timer
函数的同步版,会等待其他处理器使用完定时器再删除,del_timer_sync
不能使用在中断上下文中。del_timer_sync 函数原型如下所示:
int del_timer_sync(struct timer_list *timer)
返回值:0,定时器还没被激活;1,定时器已经激活。
- mod_timer
mod_timer 函数用于修改定时值,如果定时器还没有激活的话,mod_timer 函数会激活定时 器!函数原型如下:
int mod_timer(struct timer_list *timer, unsigned long expires)
函数参数和返回值含义如下: timer:要修改超时时间(定时值)的定时器。 expires:修改后的超时时间。 返回值:0,调用 mod_timer 函数前定时器未被激活;1,调用 mod_timer 函数前定时器已被激活。
-
timer_setup
初始化定时器,定义一个
timer_list
类型的变量之后需要通过init_timer
对其初始化:
void timer_setup(struct timer_list *timer, void (*callback)(struct timer_list *), unsigned int flags);
2.2 定时器的内部机制
定时器就是通过软件中断来实现的,它属于 TIMER_SOFTIRQ 软中断。对于 TIMER_SOFTIRQ 软中断,初始化代码如下:
void __init init_timers(void)
{
/* ensure there are enough low bits for flags in timer->base pointer */
BUILD_BUG_ON(__alignof__(struct tvec_base) & TIMER_FLAG_MASK);
init_timer_cpus();
init_timer_stats();
timer_register_cpu_notifier();
open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}
当发生硬件中断时,硬件中断处理完后,内核会调用软件中断的处理函数。对于 TIMER_SOFTIRQ,会调用 run_timer_softirq,它的函数如下:
static void run_timer_softirq(struct softirq_action *h)
{
struct tvec_base *base = __this_cpu_read(tvec_bases);
hrtimer_run_pending();
if (time_after_eq(jiffies, base->timer_jiffies))
__run_timers(base);
}
在TIMER_SOFTIRQ
的处理函数中,会从链表中把这些超时的timer取出来,执行其中的函数。
2.3 内核定时器使用流程
struct timer_list timer;
void timer_function(unsigned long arg) {
/** 处理代码 */
/**
如果需要周期性运行,则使用mod_timer重新设置超时时间并启动定时器。
*/
mod_timer(&timer, jiffies + msces_to_jiffies(2000));
}
void init(void) {
init_timer(&timer); // 初始化定时器
timer.function = function; // 设置定时处理函数
timer.expires = jiffies + msecs_to_jiffiess(2000); // 设置超时时间
add_timer(&timer); /// 启动定时器
}
void exit(void) {
del_timer(&timer); // 删除定时器
/** 或者使用 */
del_timer_sync(&timer);
}
3 内核短延时函数
函数 | 描述 |
---|---|
void ndelay(unsigned long nsecs) | 纳秒延时 |
void udelay(unsigned long usecs) | 微妙延时 |
void mdelay(unsigned long msecs) | 毫秒延时 |