从定时器到工作队列:用schedule_delayed_work重构你的Linux内核模块

在Linux内核开发中,延迟执行任务是一个常见需求。过去,许多开发者习惯使用内核定时器(timer)来实现这一功能,但随着内核版本的迭代,工作队列(workqueue)机制逐渐成为更优选择。本文将带你深入理解如何将现有内核模块从传统的定时器迁移到基于schedule_delayed_work的现代实现,提升代码的可维护性和性能。

1. 为什么需要重构:定时器与工作队列的对比

1.1 执行上下文的关键差异

内核定时器运行在原子上下文中,这意味着:

  • 不能调用可能睡眠的函数(如kmalloc带GFP_KERNEL标志)
  • 不能访问用户空间内存
  • 执行时间必须尽可能短

而工作队列运行在进程上下文,具有以下优势:

// 工作队列处理函数示例
void work_handler(struct work_struct *work) {
    // 这里可以安全地调用可能睡眠的函数
    memory = kmalloc(size, GFP_KERNEL);
    if (memory) {
        // 处理内存
        kfree(memory);
    }
}

1.2 并发处理能力比较

特性内核定时器工作队列
并发执行同一定时器不能并行可多线程并行处理
可睡眠否是
CPU亲和性无可配置
延迟精度高中等
资源消耗低较高

1.3 典型应用场景分析

适合使用定时器的场景:

  • 对延迟精度要求极高的硬件操作
  • 极短时间内的超时处理
  • 不能接受上下文切换开销的情况

适合使用工作队列的场景:

  • 需要文件I/O或网络操作的任务
  • 可能分配大量内存的操作
  • 需要与用户空间交互的任务
  • 执行时间较长的后台处理

2. 迁移准备:理解schedule_delayed_work机制

2.1 核心数据结构解析

schedule_delayed_work依赖于几个关键数据结构:

struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;
};

struct delayed_work {
    struct work_struct work;
    struct timer_list timer;
};

2.2 工作队列生命周期管理

  1. 初始化阶段:

    INIT_DELAYED_WORK(&my_work, my_work_handler);
    
  2. 调度阶段:

    schedule_delayed_work(&my_work, msecs_to_jiffies(500));
    
  3. 执行阶段:

    • 内核线程从工作队列取出任务
    • 在进程上下文中执行处理函数
  4. 清理阶段:

    cancel_delayed_work_sync(&my_work);
    

2.3 延迟时间处理细节

注意:delay参数的单位是jiffies,建议使用msecs_to_jiffies()转换毫秒时间,避免直接使用魔法数字。

时间转换的推荐做法:

unsigned long delay = msecs_to_jiffies(500); // 500毫秒
schedule_delayed_work(&work, delay);

3. 逐步迁移:从定时器到工作队列

3.1 步骤一:替换数据结构

原定时器实现:

struct my_data {
    struct timer_list my_timer;
    // 其他数据...
};

新工作队列实现:

struct my_data {
    struct delayed_work my_work;
    // 其他数据...
};

3.2 步骤二:重写处理函数

定时器处理函数:

void timer_handler(unsigned long data) {
    // 原子上下文,限制较多
}

工作队列处理函数:

void work_handler(struct work_struct *work) {
    // 进程上下文,可以安全睡眠
    struct my_data *data = container_of(work, struct my_data, work.work);
    // 处理逻辑...
}

3.3 步骤三:修改调度逻辑

原定时器调度:

setup_timer(&data->my_timer, timer_handler, (unsigned long)data);
mod_timer(&data->my_timer, jiffies + msecs_to_jiffies(500));

新工作队列调度:

INIT_DELAYED_WORK(&data->my_work, work_handler);
schedule_delayed_work(&data->my_work, msecs_to_jiffies(500));

3.4 步骤四:处理取消逻辑

原定时器取消:

del_timer_sync(&data->my_timer);

新工作队列取消:

cancel_delayed_work_sync(&data->my_work);

4. 高级优化与问题排查

4.1 自定义工作队列的创建

对于高性能场景,可以创建专用工作队列:

// 创建专用工作队列
struct workqueue_struct *my_wq = alloc_workqueue("my_wq", WQ_MEM_RECLAIM, 1);

// 调度工作
queue_delayed_work(my_wq, &work, delay);

// 销毁工作队列
destroy_workqueue(my_wq);

4.2 常见问题与解决方案

问题1:工作函数执行时间过长

  • 解决方案:将大任务拆分为多个小工作项

问题2:取消工作时的竞争条件

// 不安全的取消方式
if (!cancel_delayed_work(&work)) {
    // 工作可能已经在执行
}

// 安全的取消方式
cancel_delayed_work_sync(&work); // 等待正在执行的工作完成

问题3:内存回收时的死锁

提示:在内存紧张时,工作队列的创建可能失败。应在模块初始化时预先分配所需资源。

4.3 性能调优技巧

  1. 工作者线程配置:

    # 查看系统工作队列线程
    ps -eLo pid,ppid,pgid,comm | grep kworker
    
  2. CPU亲和性设置:

    // 为工作队列设置CPU亲和性
    struct workqueue_attrs *attrs;
    attrs = alloc_workqueue_attrs();
    cpumask_set_cpu(cpu, attrs->cpumask);
    apply_workqueue_attrs(wq, attrs);
    
  3. 电源管理考虑:

    • 使用WQ_MEM_RECLAIM标志确保内存回收时工作不丢失
    • 避免在系统挂起时调度不必要的工作

5. 实际案例:网络驱动中的延迟任务重构

以一个虚拟网络设备驱动为例,展示如何将链路状态检测从定时器迁移到工作队列:

原始定时器实现:

static void link_check_timer(unsigned long arg) {
    struct net_device *dev = (struct net_device *)arg;
    // 检查链路状态
    if (check_link_status(dev)) {
        netif_carrier_on(dev);
    } else {
        netif_carrier_off(dev);
    }
    // 重新调度定时器
    mod_timer(&dev->link_timer, jiffies + HZ);
}

重构后的工作队列实现:

static void link_check_work(struct work_struct *work) {
    struct net_device *dev = container_of(work, struct net_device, link_work.work);
    
    // 可以安全调用可能睡眠的函数
    int status = check_link_status_with_retry(dev);
    
    if (status > 0) {
        netif_carrier_on(dev);
    } else if (status == 0) {
        netif_carrier_off(dev);
    }
    
    // 重新调度工作
    schedule_delayed_work(&dev->link_work, HZ);
}

初始化差异对比:

// 旧方式
setup_timer(&dev->link_timer, link_check_timer, (unsigned long)dev);
mod_timer(&dev->link_timer, jiffies + HZ);

// 新方式
INIT_DELAYED_WORK(&dev->link_work, link_check_work);
schedule_delayed_work(&dev->link_work, HZ);

这个案例展示了工作队列如何使代码更安全、更易维护,特别是当链路状态检查需要更复杂的操作(如重试机制)时。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐