内核定时器
- Linux 内核定时器是一种在特定时间点触发执行某个函数的机制。它允许内核开发者在指定的延迟时间后执行某个回调函数。
- 定时器广泛应用于设备驱动程序中,用于处理延迟任务、周期性操作或定时检查等场景。
- 内核定时器是软定时器(software timer),在内核的软中断上下文中执行。因此,它不适合执行长时间的任务,也不允许阻塞或睡眠操作。
jiffies变量
- Linux 内核中使用 jiffies 作为时间计数单位,jiffies 是一个全局变量,它的值随着时钟中断的发生不断累加。在每次时钟中断发生时,jiffies 值会递增。
- jiffies 是内核中的一个全局变量,用于表示系统启动后经历的时间,以 jiffies 为单位。它从系统启动时设置为 0,随着系统运行而不断增加。
- 每次时钟中断发生时,jiffies 的值会增加 1。
- 由于 jiffies 与时钟中断频率直接相关,因此它能够间接表示系统运行了多长时间。
HZ宏
- HZ 是一个内核宏,表示一秒钟中 jiffies 增加的次数,通常称为时钟中断频率。
- 不同平台的 HZ 值不同,例如:
x86 平台 HZ=1000:jiffies 每秒增加 1000 次。
ARM9 平台 HZ=100:jiffies 每秒增加 100 次。
ARMv7 平台 HZ=200:jiffies 每秒增加 200 次。 - 内核中使用 HZ 统一处理时间相关的计算,这样程序编写可以跨平台运行,而无需关心底层时钟频率的差异。
jiffies和时间的转换
- 由于不同平台的 HZ 值不同,Linux 提供了多个宏和函数来方便开发者将 jiffies 转换为秒、毫秒等时间单位。
- 具体示例
函数名 | 功能描述 | 示例使用 |
---|
jiffies_to_msecs() | 将 jiffies 转换为毫秒 | unsigned long ms = jiffies_to_msecs(jiffies); |
msecs_to_jiffies() | 将毫秒转换为 jiffies | unsigned long j = msecs_to_jiffies(1000); |
jiffies_to_timeval() | 将 jiffies 转换为 timeval 结构体 | struct timeval tv = jiffies_to_timeval(jiffies); |
jiffies_to_timespec() | 将 jiffies 转换为 timespec 结构体 | struct timespec ts = jiffies_to_timespec(jiffies); |
time_before() | 判断一个 jiffies 时间是否早于另一个 | if (time_before(j1, j2)) {...} |
time_after() | 判断一个 jiffies 时间是否晚于另一个 | if (time_after(j1, j2)) {...} |
time_in_range() | 判断 jiffies 时间是否在某个范围内 | if (time_in_range(j, start, end)) {...} |
get_jiffies_64() | 获取系统启动以来的 jiffies 64位值 | u64 j = get_jiffies_64(); |
jiffies_to_usecs() | 将 jiffies 转换为微秒 | unsigned long us = jiffies_to_usecs(jiffies); |
usecs_to_jiffies() | 将微秒转换为 jiffies | unsigned long j = usecs_to_jiffies(1000000); |
使用流程
- 定义定时器对象
- 使用 struct timer_list 结构体来表示定时器。它包含定时器的超时时间(expires)、回调函数(function),以及私有数据(data)。
struct global_struct {
struct timer_list timer_obj;
int xxx;
};
struct global_struct gstruct;
---------------timer_list结构体-----------
struct timer_list {
unsigned long expires;
void (*function)(unsigned long data);
unsigned long data;
};
- 初始化定时器
- 在模块初始化函数中,通过 init_timer() 初始化定时器,并指定回调函数、超时时间和私有数据。
int mod_init(void)
{
init_timer(&gstruct.timer_obj);
gstruct.timer_obj.function = timer_function;
gstruct.timer_obj.data = (long)&gstruct;
gstruct.timer_obj.expires = jiffies + HZ;
gstruct.xxx = 10086;
add_timer(&gstruct.timer_obj);
printk("%s-%d\n", __func__, __LINE__);
return 0;
}
- 定时器回调函数
- 当定时器超时时,内核会自动调用指定的回调函数。该回调函数在软中断上下文中执行,因此不能进行阻塞操作。
- 示例中的回调函数 timer_function 执行任务后,还会重新启动定时器,形成一个重复定时器。
void timer_function(unsigned long data)
{
struct global_struct *pt_gstruct = (struct global_struct *)data;
printk("%s-%d gstruct.xxx=%d\n", __func__, __LINE__, pt_gstruct->xxx);
pt_gstruct->timer_obj.expires = jiffies + HZ/2;
add_timer(&pt_gstruct->timer_obj);
}
- 交给内核去管理定时器
- add_timer(&gstruct.timer_obj); // 向内核添加定时器
- 当定时器到期时,内核会执行:
timer_obj.function(timer_obj.data);
- 删除定时器
- 在模块退出时,调用 del_timer() 函数删除定时器,防止在模块卸载后定时器仍然运行导致内存访问错误
void mod_exit(void)
{
printk("%s-%d\n", __func__, __LINE__);
del_timer(&gstruct.timer_obj);
return;
}
- 注意事项
- 不能阻塞:定时器的回调函数在软中断上下文中执行,因此不能进行阻塞操作(如 msleep() 或 schedule())。
- 重复定时器:通过在回调函数中重新调用 add_timer(),可以实现重复触发的定时器。
- 删除定时器:在模块卸载时必须删除定时器,防止在模块卸载后定时器回调函数访问已释放的内存。
- 时间单位:定时器的时间单位是 jiffies,不同平台的 HZ 值不同,需要根据具体平台调整定时器的触发间隔。
- 综合代码
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
struct global_struct {
struct timer_list timer_obj;
int xxx;
};
struct global_struct gstruct;
void timer_function(struct timer_list *timer)
{
struct global_struct *pt_gstruct = from_timer(pt_gstruct, timer, timer_obj);
printk("%s-%d gstruct.xxx=%d\n", __func__, __LINE__, pt_gstruct->xxx);
pt_gstruct->timer_obj.expires = jiffies + HZ / 2;
add_timer(&pt_gstruct->timer_obj);
}
int mod_init(void)
{
timer_setup(&gstruct.timer_obj, timer_function, 0);
gstruct.timer_obj.expires = jiffies + HZ;
gstruct.xxx = 10086;
add_timer(&gstruct.timer_obj);
printk("%s-%d\n", __func__, __LINE__);
return 0;
}
void mod_exit(void)
{
printk("%s-%d\n", __func__, __LINE__);
del_timer(&gstruct.timer_obj);
return;
}
module_init(mod_init);
module_exit(mod_exit);
MODULE_LICENSE("GPL");
- 编译载入后使用demsg | grep timer查看打印信息。
内核堆栈
- 栈
- 栈是系统为每个进程自动分配的一块内存,用来存储局部变量、函数参数、返回地址等。栈内存是以“后进先出”(LIFO)方式管理的,函数调用时会将数据压入栈,函数返回时则从栈中弹出数据。
- 用户空间中的栈
- 在用户空间中,每个进程通常拥有 8MB 的栈空间。用户空间栈主要用于保存局部变量、函数调用链等信息。
- 内核中的栈
- 当进程从用户态通过系统调用进入内核态时,它会开始消耗 内核栈。与用户空间栈不同,内核为每个进程分配的栈空间非常有限,一般只有 4KB 或 8KB,具体大小取决于架构和内核配置。
- 由于内核栈空间有限,编写内核代码时需要特别注意栈的使用,避免造成栈溢出,这可能会导致内核崩溃(kernel panic)。
- 内核中编写代码时需要注意的栈问题
- 避免定义大块局部变量: 例如,不要在栈上定义大数组,尤其是在内核栈空间只有 4KB/8KB 时。这种情况下,大型局部变量可能会耗尽栈空间。
char aa[4096]; // 4KB 大小的数组,可能导致栈溢出
- 多使用内核提供的宏: 内核代码中经常使用一些宏来处理数据,它们能帮助简化操作,并且通常更高效。
- 避免过深的函数调用层级,禁止递归: 深层次的函数调用会不断消耗栈空间,递归调用尤其危险,因为递归可能会无限制地增加栈的使用。递归容易导致栈溢出,应尽量避免在内核代码中使用。
- 堆
- 堆是用于动态分配内存的区域,程序可以在运行时请求内存,分配的内存需要手动释放。
- 在用户空间中,常见的动态内存分配方法是使用 malloc() 和 free() 函数。
- 内核空间的堆
- 内核空间中不能直接使用用户空间的 malloc() 函数,内核提供了自己的内存分配函数。
- kmalloc() / kfree()
- kmalloc() 是内核中最常用的内存分配函数,类似于用户态的 malloc(),但它只适用于较小的内存分配,并且分配的内存是连续的物理地址。
void *kmalloc(size_t size, gfp_t flags)
size:要分配的内存大小。
flags:内存分配的标志,用于指定是否允许睡眠等待等条件。常见的标志有:
GFP_KERNEL:表示可以在内存不足时让进程睡眠等待分配内存,这是内核中最常见的分配标志。
GFP_ATOMIC:表示不允许睡眠,如果内存不足立即返回 NULL,用于中断上下文等不能睡眠的场合。
void kfree(void *ptr):释放由 kmalloc() 分配的内存。
- vmalloc() / vfree()
- vmalloc() 用于分配较大的内存块,它在虚拟地址空间中分配连续的地址,但这些地址在物理上不必是连续的。vmalloc() 通常用于需要大块内存的情况。
- void *vmalloc(size_t size):分配指定大小的内存,返回指向该内存区域的指针。
- void vfree(void *ptr):释放 vmalloc() 分配的内存。
- kmalloc() 与 vmalloc() 的区别
特性 | kmalloc() | vmalloc() |
---|
是否可睡眠 | 可睡眠(GFP_KERNEL ),也可不睡眠(GFP_ATOMIC ) | 只能在可睡眠环境下使用 |
申请空间范围 | 适用于较小的内存分配(一般为 32B ~ 128KB) | 适用于较大的内存分配 |
物理内存连续性 | 物理内存必须是连续的 | 虚拟地址连续,但物理地址可以不连续 |
性能 | 由于物理内存连续,性能较好 | 由于物理内存不连续,性能相对较差 |
MMU (Memory Management Unit) 内存管理单元
- MMU(内存管理单元)是处理器中的一个硬件模块,用于管理内存的访问,提供虚拟内存地址到物理内存地址的转换,并实现内存保护。
- MMU 对内存的高效管理是操作系统内存虚拟化技术的核心,它将操作系统和应用程序看到的虚拟地址空间映射到实际的物理内存地址上。
- 主要功能
- 管理物理内存
- 隔离物理地址,保护系统安全
- 为用户提供虚拟地址空间
- 实现虚拟内存
- RAM的管理
- 物理内存(RAM)由 MMU 和内核联合管理,操作系统通过 MMU 的页表映射机制使用 RAM。
- ioremap 的作用:
- 用于将物理地址空间的特定内存区域(如硬件寄存器)映射到内核虚拟地址空间,使得内核可以访问和操作硬件资源。