Linux内核中如何在定时器中调用可睡眠的函数

在Linux内核中,定时器回调函数(如通过timer_listhrtimer注册的函数)通常运行在中断上下文中,而中断上下文的关键限制是不能调用任何可能引发睡眠的函数(例如使用互斥锁、内存分配GFP_KERNEL或阻塞操作)。为了解决这一问题,必须将可睡眠的操作迁移到进程上下文中执行。以下是实现这一目标的步骤和示例代码:


方法:使用工作队列(Workqueue)

工作队列(Workqueue)是内核中用于在进程上下文中执行任务的机制。通过将任务提交到工作队列,可以安全地调用可睡眠函数。

实现步骤:

  1. 定义工作队列和定时器
    声明一个工作队列结构体和一个定时器结构体。

  2. 初始化工作队列和定时器
    在模块初始化时,绑定工作队列的处理函数,并配置定时器的回调。

  3. 在定时器回调中调度工作队列
    定时器触发时,在中断上下文中调度工作队列任务,而非直接执行可睡眠操作。

  4. 在工作队列处理函数中执行睡眠操作
    工作队列的处理函数在进程上下文中运行,允许调用可睡眠函数。

  5. 清理资源
    在模块卸载时,确保取消定时器和工作队列的任务。


示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/timer.h>
#include <linux/workqueue.h>

static struct timer_list my_timer;
static struct work_struct my_work;

// 工作队列处理函数(进程上下文)
static void my_work_handler(struct work_struct *work) {
printk(KERN_INFO "Workqueue: Entering process context.\n");

// 示例:调用可能睡眠的函数
msleep(1000); // 允许睡眠
printk(KERN_INFO "Workqueue: Slept for 1 second.\n");

// 其他可能睡眠的操作(如 mutex_lock、GFP_KERNEL 内存分配等)
}

// 定时器回调函数(中断上下文)
static void my_timer_callback(struct timer_list *timer) {
printk(KERN_INFO "Timer: Scheduling workqueue.\n");

// 调度工作队列到进程上下文
schedule_work(&my_work);

// 重新激活定时器(可选)
mod_timer(&my_timer, jiffies + msecs_to_jiffies(2000));
}

// 模块初始化
static int __init my_module_init(void) {
printk(KERN_INFO "Module initializing.\n");

// 初始化定时器
timer_setup(&my_timer, my_timer_callback, 0);
mod_timer(&my_timer, jiffies + msecs_to_jiffies(2000)); // 2秒后触发

// 初始化工作队列
INIT_WORK(&my_work, my_work_handler);

return 0;
}

// 模块卸载
static void __exit my_module_exit(void) {
printk(KERN_INFO "Module exiting.\n");

// 删除定时器并等待可能的运行中的回调
del_timer_sync(&my_timer);

// 刷新工作队列,确保所有任务完成
flush_work(&my_work);
}

module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");

关键点解释

  1. 工作队列的作用

    • schedule_work(&my_work) 将任务提交到内核的全局工作队列(system_wq),该队列由内核线程管理,任务在进程上下文中执行。
    • 使用 INIT_WORK 初始化工作项,并绑定处理函数 my_work_handler
  2. 定时器的限制

    • 定时器回调函数 my_timer_callback 仅负责快速调度工作队列,不执行任何可能阻塞的操作。
  3. 进程上下文的优势

    • 在工作队列处理函数中,可以安全调用如 msleep()mutex_lock()kmalloc(GFP_KERNEL) 等函数。

替代方案

  1. 内核线程(Kernel Thread)
    手动创建内核线程并通过信号量或等待队列同步任务。灵活性更高,但实现更复杂。

  2. Tasklet 或 SoftIRQ
    不适用,因为它们同样运行在中断上下文。

  3. 延迟工作队列(Delayed Workqueue)
    使用 schedule_delayed_work() 实现延迟执行,适用于需要延时的场景。


注意事项

  • 资源竞争:确保工作队列处理函数是可重入的,或在并发访问时使用锁机制。
  • 内存安全:在模块卸载时,通过 flush_work() 等待工作队列任务完成,防止访问已释放的内存。
  • 定时器重置:若需周期性任务,可在定时器回调中调用 mod_timer() 重新激活。

通过上述方法,可以安全地在定时器中间接调用可睡眠函数,避免内核崩溃或不可预测的行为。