Linux kernel workqueue机制分析

CMWQ(Concurrency Managed Workqueue)机制重新设计了Linux内核中的workqueue机制,旨在提高资源利用率并保证并发处理能力。它引入了统一的worker池管理机制,允许使用者通过API指定CPU核心、可重复性、并发限制和优先级等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Linux kernel workqueue机制分析

在内核编程中,workqueue机制是最常用的异步处理方式。本文主要基于linux kernel 3.10.108的workqueue文档分析其基本原理和使用API。

概览

Workqueue(WQ)机制是Linux内核中最常用的异步处理机制。Workqueue机制的主要概念包括:work用于描述放到队列里即将被执行的函数;worker表示一个独立的线程,用于执行异步上下文处理;workqueue用于存放work的队列。
当workqueue上有work条目时,worker线程被触发来执行work对应的函数。如果有多个work条目在队列,worker会按顺序处理所有work。

CMWQ概述

在最初的WQ实现中,多线程WQ(MTWQ)在每个CPU上都有一个worker线程,单线程WQ(STWQ)则总共只有一个worker线程。一个MTWQ的worker个数和CPU核数相同,多年来,MTWQ大量使用使得线程数量大量增加,甚至超过了某些系统对PID空间默认32K的限制。
尽管MTWQ浪费大量资源,但其提供的并发水平还是不能让人满意。并发的限制在STWQ和MTWQ上都存在,虽然MT相对来说不那么严重。MTWQ在每个CPU上提供了一个上下文执行环境,STWQ则在整个系统提供一个上下文执行环境。work任务需要竞争这些有限的执行环境资源,从而导致死锁等问题。
并发和资源之间的紧张关系使得一些使用者不得不做出一些不必要的折中,比如libata的polling PIOs选择STWQ,这样就无法有两个polling PIOs同时进行处理。因为MTWQ并不能提供高并发能力,因此async和fscache不得不实现自己的线程池来提供高并发能力。

Concurrency Managed Workqueue (CMWQ)重新设计了WQ机制,并实现如下目标:

  1. 保持原workqueue API的兼容;
  2. 使用per-CPU统一的worker池,为所有WQ共享使用并提供灵活的并发级别,同时不浪费不必要的资源;
  3. 自动调整worker池和并发级别,让使用者不用关心这些细节。

CMWQ设计思想

一个work是一个简单的结构体,保存一个函数指针用于异步执行。任何驱动或者子系统想要一个函数被异步执行,都需要设置一个work指向该函数并将其放入workqueue队列。然后worker线程从队列上获取work并执行对应的函数,如果队列里没有work,则worker线程处于空闲状态。这些worker线程用线程池机制来管理。

CMWQ设计时将面向用户的workqueue机制和后台worker线程池管理机制进行了区分。后台的workqueue被称为GCWQ(推测可能是Global Concurrency Workqueuq),在每个CPU上存在一个GCWQ,用于处理该CPU上所有workqueue的work。每个GCWQ有两个线程池:一个用于普通work处理,另一个用于高优先级work处理。

内核子系统和驱动程序通过workqueue API创建和调度work,并可以通过设置flags来指定CPU核心、可重复性、并发限制,优先级等。当work放入workqueue时,通过队列参数和属性决定目标GCWQ和线程池,work最终放入对应线程池的共享worklist上。通过如果没有特别设定,work会被默认放入当前运行的CPU核上的GCWQ线程池的worklist上。

GCWQ的线程池在实现时同时考虑了并发能力和资源占用,仅可能占用最小的资源并提供足够的并发能力。每个CPU上绑定的线程池通过hook到CPU调度机制来实现并发管理。当worker被唤醒或者进入睡眠都会通知到线程池,线程池保持对当前可以运行的worker个数的跟踪。通常我们不期望一个work独占CPU和运行很多个CPU周期,因此维护刚好足够的并发以防止work处理的速度降低是最优的。当CPU上有一个或多个runnalbe的worker,线程池不会启动新的work任务。当上一个running的work转入睡眠,则立即调度一个新的worker。这样当有work在pending的时候,CPU一直保持干活的状态。这样来保证用最小的worker个数同时足够的执行带宽。

维持idle状态的worker只是消耗部分kthreads的内存,因此CMWQ在杀掉idle的worker之前一段时间让其活着。

unbound的WQ并不使用上述机制,而是用pseudo unbound CPU的线程池去尽快处理所有work。CMWQ的使用者来控制并发级别,并可以设置一个flag来忽略并发管理机制。

CMWQ通过创建更多的worker以及rescue-worker来保证任务按时处理。所有可能在内存回收的代码路径上执行的work必须放到特定的workqueue,该workqueue上有一个rescue-worker可以在内存压力下执行,这样避免在内存回收时出现死锁。

API

alloc_workqueue()
alloc_workqueue()用于分配一个WQ。原来的create_workqueue()系列接口已经弃用并计划删除。alloc_workqueue()有三个入参:@name, @flags, @max_active。name是workqueue的名字并也用于rescuer-thread(如果有的话)名称。flags和max_active用于控制work分配执行环境、调度和执行。

flags
WQ_NON_REENTRANT
默认一个WQ保证在同一个CPU上不会有重入性,即WQ上多个work不会再同一个CPU上并发执行,但会在多个CPU上并发执行。该flag标识在多个CPU上也不能重入,在整个系统级别都只有一个work在执行。

WQ_UNBOUND
该flag设定的WQ不绑定到CPU,其work将被一个特殊的CGWQ进行服务,该CGWQ上的worker不绑定任何CPU。unbound WQ牺牲了CPU亲和性,主要用于下场景:

  1. 并发级别需求的波动非常大,如果使用bound WQ则会在不同CPU上创建大量的worker,并且这些worker大部分时间都是空闲的。
  2. 长时间运行的CPU密集型工作可以由系统调度程序更好的管理。

WQ_FREEZABLE
可冻结的WQ在系统suspend操作的freeze阶段,暂停新的work执行直到解冻。

WQ_MEM_RECLAIM
可能用于内存回收路径的WQ必须设置该flag。在内存紧张的时候也会保证至少有一个可执行的上下文用于该WQ。

WQ_HIGHPRI
高优先级的WQ的work会被放入GCWQ的高优先级线程池。高优先级的线程池的线程拥有高nice级别。普通的线程池和高优先级的线程池彼此独立,互相不影响。

WQ_CPU_INTENSIVE
设置为CPU密集型的WQ的work不会影响并发级别,即CPU密集型的work执行时并不会阻止同一个线程池里其他WQ的work的执行。这对希望独占CPU周期的work非常有用,由系统调度程序调度他们的执行。如果不设置该标记,则独占CPU周期的work会导致同一个线程池里其他WQ的work得不到执行。
由于同一由CMWQ的并发管理进行调度,当非密集型的WQ的work运行过程中,也会导致密集型的WQ的work被推迟。该flag仅适用于bound的WQ,对unbound的WQ无效。

max_active
max_active用于指定WQ在每个CPU上最大的执行上下文个数,即并发处理的work个数。目前对于bound WQ,max_active最大可以设置为512,如果max_active入参为0,则使用默认值256。对于unbound WQ,最大值为512和4*cpu核数两个里面较大的值。
对于希望使用STWQ的使用者,可以设置max_active为1,并且设置WQ_UNBOUND标识。这样整个系统里只有一个该WQ上的work正在执行。

struct workqueue_struct
函数alloc_workqueue()返回一个指向struct workqueue_struct的指针,代表一个workqueue。如下所示:

struct workqueue_struct *wq = alloc_workqueue("wq-name", WQ_NON_REENTRANT | WQ_MEM_RECLAIM, 0);

struct work_struct
数据结构struct work_struct定义了一个work,通过通过INIT_WORK系列宏定义初始化work,设置执行的函数。如下所示:

struct work_struct work;
void worker_func(struct work_struct *work);
INIT_WORK(&work, worker_func);

struct delayed_work
数据结构struct delayed_work定义了一个延迟work,延迟work通过设置定时器的方式延迟将work放入队列。如下所示为其数据结构定义:

struct delayed_work {
    struct work_struct work;
    struct timer_list timer;

    /* target workqueue and CPU ->timer uses to queue ->work */
    struct workqueue_struct *wq;
    int cpu;
};

可以看到delayed_work由一个work和一个定时器组成。delayed_work通过INIT_DELAYED_WORK系列宏定义进行初始化。如下所示:

struct delayed_work work;
void worker_func(struct work_struct *work);
INIT_DELAYED_WORK(&work, worker_func);

queue_work_on()
函数queue_work_on()将work放入workqueue队列,其定义如下:

extern bool queue_work_on(int cpu, struct workqueue_struct *wq,
            struct work_struct *work);

函数queue_delayed_work将delayed_work在延迟delay个jiffies之后放入workqueue队列,其定义如下:

static inline bool queue_delayed_work(struct workqueue_struct *wq,
                      struct delayed_work *dwork,
                      unsigned long delay)

调试

因为work是由通用的工作线程执行的,因此需要一些技巧来定位workqueue使用者的一些错误行为。
worker线程通过ps可以看到:

root      5671  0.0  0.0      0     0 ?        S    12:07   0:00 [kworker/0:1]
root      5672  0.0  0.0      0     0 ?        S    12:07   0:00 [kworker/1:2]
root      5673  0.0  0.0      0     0 ?        S    12:12   0:00 [kworker/0:0]
root      5674  0.0  0.0      0     0 ?        S    12:13   0:00 [kworker/1:0]

如果某个kworker疯了,占用CPU非常高,可能有如下两种原因:

  1. 大量的work正在正在提交调度;
  2. 某个work占用过多CPU;

第1种原因可以通过tracing机制来跟踪:

$ echo workqueue:workqueue_queue_work > /sys/kernel/debug/tracing/set_event
$ cat /sys/kernel/debug/tracing/trace_pipe > out.txt

如果某个worker忙于循环将大量work进行调度,通过输出的work里的函数可以找到谁提交了大量的work。

第2种原因可以通过打印出worker的堆栈空间来分析是哪个work的函数正在处理:

$ cat /proc/THE_OFFENDING_KWORKER/stack

总结

在内核中直接使用kthread创建自己的线程进行异步处理带来一定的复杂度以及资源浪费,而workqueue机制为内核模块提供了简单的接口来实现异步函数处理。CMWQ机制在使用尽量少的资源的同时保证了并发处理能力。

参考资料

Linux/Documentation/workqueue.txt

转载于:https://www.cnblogs.com/jimbo17/p/8885814.html

<think>我们正在讨论Linux内核中的workqueue(工作队列)。根据引用内容,workqueue是一种将任务推迟执行的内核机制,特别适用于中断上下文之外的任务执行。下面我将详细介绍workqueue的作用和使用场景。 ### Workqueue的作用 1. **延迟任务处理**:Workqueue允许将任务推迟到稍后执行,这在中断处理中尤为重要,因为中断处理程序要求快速执行,不能进行长时间操作[^1]。 2. **在进程上下文执行**:Workqueue中的任务在进程上下文中执行,因此可以睡眠(调用调度器),而中断上下文则不能睡眠[^5]。 3. **并发管理**:现代workqueue(并发管理工作队列)能够自动管理并发级别,根据系统负载动态调整工作者线程的数量,提高系统效率[^5]。 ### Workqueue的使用场景 1. **中断下半部处理**:在中断上半部(中断处理程序)中,如果存在耗时操作,可以将这些操作放入workqueue中,由下半部在进程上下文执行[^1][^5]。 2. **定时任务**:结合延时工作队列(Delayed Workqueue),可以实现定时任务,例如在特定时间后执行某项操作[^4]。 3. **周期性任务**:通过重新提交工作项(work item)到队列中,可以实现周期性任务[^4]。 4. **需要睡眠的任务**:当任务需要执行可能睡眠的操作(如获取互斥锁、分配内存等)时,必须使用workqueue,因为在中断上下文(如软中断或tasklet)中不允许睡眠[^5]。 ### 示例代码 引用[2]提供了一个简单的workqueue使用示例: ```c #include <linux/module.h> #include <linux/init.h> #include <linux/workqueue.h> // 定义工作结构体 static struct work_struct my_work; // 工作处理函数 static void work_handler(struct work_struct *work) { printk(KERN_INFO "Work handler is running.\n"); } static int __init my_init(void) { // 初始化工作项,绑定处理函数 INIT_WORK(&my_work, work_handler); // 将工作项提交到系统默认的工作队列(events) schedule_work(&my_work); return 0; } static void __exit my_exit(void) { // 等待工作项完成(如果正在执行) flush_scheduled_work(); } module_init(my_init); module_exit(my_exit); ``` ### Workqueue的优缺点 **优点**: - 允许任务睡眠,适合复杂任务。 - 自动并发管理,提高资源利用率。 - 支持延时执行(使用`schedule_delayed_work`)[^4]。 **缺点**: - 相对于tasklet或软中断,有更高的延迟(因为任务可能被调度器抢占)[^5]。 - 创建大量工作项可能导致系统产生过多的工作者线程,增加开销(但现代并发管理工作队列已优化此问题)[^5]。 ### Tasklet的比较 - **Tasklet**:在软中断上下文中运行,不能睡眠,但执行延迟低,适合对延迟敏感且不睡眠的任务[^5]。 - **Workqueue**:在进程上下文中运行,可以睡眠,适合需要长时间执行或需要睡眠的任务[^5]。 ### 总结 WorkqueueLinux内核中一种重要的延迟任务执行机制,尤其适用于中断下半部处理、需要睡眠的任务以及定时/周期性任务。通过将任务放入工作队列,可以确保中断处理程序的快速执行,同时完成复杂的后续操作。 §§相关问题§§ 1. Workqueue中的任务在什么上下文中执行?为什么可以睡眠? 2. 如何创建一个延时工作队列任务? 3. Workqueue和tasklet的主要区别是什么? 4. 如何避免workqueue中的任务执行时间过长影响系统性能?</think>### Linux Workqueue 的作用使用场景 #### 一、Workqueue 的核心作用 1. **异步任务处理** 允许将任务推迟到**进程上下文**执行(区别于中断上下文),适用于需要睡眠、阻塞或长时间运行的操作[^5]。 2. **中断下半部处理** 在中断处理中,将非紧急任务(如数据处理、日志记录)从**中断上半部**转移到工作队列,确保中断快速响应[^1][^5]。 3. **并发管理** 现代 workqueue(CMWQ)自动管理**工作者线程的并发级别**,根据 CPU 负载动态调整线程数量[^5]。 4. **延时任务支持** 通过 `schedule_delayed_work()` 实现定时任务(如硬件状态轮询、超时处理)[^4]。 #### 二、典型使用场景 1. **中断上下文延伸** ```c // 中断处理中提交任务 irq_handler() { struct work_struct *work = &my_work; schedule_work(work); // 将耗时操作移交到进程上下文 } ``` **场景**:网络中断后数据包处理、块设备 I/O 完成后的数据校验[^1][^5]。 2. **内核定时任务** ```c // 设置延时任务(2秒后执行) schedule_delayed_work(&delayed_work, HZ * 2); ``` **场景**:硬件状态轮询(如传感器读取)、连接超时检测[^4]。 3. **需要睡眠的操作** **场景**: - 文件系统操作(需要磁盘 I/O 等待) - 内存分配(可能触发页错误) - 获取互斥锁(可能阻塞) 这些操作**不能在中断上下文执行**,必须通过 workqueue[^5]。 4. **资源解耦** **场景**:驱动模块卸载时,确保队列中的任务完成后再释放资源(通过 `flush_workqueue()`)[^2]。 #### 三、Workqueue 实现原理 1. **核心组件** - **工作项**(`struct work_struct`):封装待执行函数 - **工作者线程**(`kworker`):执行任务的线程池 - **工作队列**(`struct workqueue_struct`):任务容器 2. **执行流程** ```mermaid graph LR A[驱动提交 work] --> B(workqueue) B --> C[工作者线程 kworker] C --> D{是否可运行?} D -->|是| E[执行 work->func] D -->|否| F[进入睡眠状态] ``` 引用[3]描述了初始化流程: > `kernel_init → workqueue_init → create_worker → worker_thread → process_one_work`[^3] #### 四、 Tasklet 的对比 | **特性** | **Workqueue** | **Tasklet** | |-------------------|-----------------------------------|---------------------------| | 执行上下文 | 进程上下文(可睡眠) | 软中断上下文(不可睡眠) | | 并发性 | 支持多核并行执行 | 同一 tasklet 串行执行 | | 延迟 | 较高(受线程调度影响) | 极低(软中断优先) | | 适用场景 | 耗时操作/需阻塞 | 高频小任务/原子操作 | #### 五、优缺点分析 ✅ **优点**: - 支持睡眠和长时间操作 - 自动负载均衡(CMWQ) - 灵活的延时任务机制[^4] ❌ **缺点**: - 延迟高于 tasklet - 线程切换带来额外开销 - 不当使用可能导致线程爆炸(需合理设计队列) > 关键设计原则:**"在中断中只做最紧急的事,其余交给 workqueue"**[^5] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值