Linux 内核 Workqueue 深度实战:从队列机制到 kworker 调优的生产实践
在 Linux 内核的异步处理子系统中,Workqueue 是应用最广泛、架构最精妙的基础设施之一。它允许内核代码将非紧急的工作延后执行,在进程上下文中安全地完成可能睡眠的操作。从块设备的 I/O 完成到网络栈的数据包处理,从驱动的中断底半部到系统的电源管理,Workqueue 几乎参与了内核每一个异步场景。本文将深入剖析 Workqueue 的完整架构,并给出生产级别的调优实践。
一、为什么需要 Workqueue
内核中经常遇到这样的场景:在某个上下文中触发了一个任务,但这个任务不需要立即执行,或者不能在当前上下文中执行。比如:
- 中断处理函数中收到数据,需要后续解析处理(中断上下文禁止睡眠)
- 块设备 I/O 完成后,需要更新统计信息和唤醒等待进程
- 设备驱动检测到热插拔事件,需要异步加载固件
这些场景的共同特点是:需要延后执行、可能需要在进程上下文中睡眠。
在 Workqueue 出现之前,内核使用 tasklet 和 softirq 处理这类延后工作。但它们有一个致命限制:执行上下文为中断上下文,不得调用任何可能睡眠的函数。这严重限制了延后工作能做的事情。比如在中断底半部里需要获取互斥锁、分配大内存(可能触发直接回收)、执行块 I/O 等操作时,tasklet 就无能为力了。
Workqueue 的核心设计理念是:将延后工作交给内核线程(kworker)执行,内核线程运行在进程上下文中,可以睡眠。这意味着 Workqueue 的回调函数可以执行任何内核允许的操作,包括睡眠等待、内存分配、互斥锁操作、甚至发起同步 I/O。
二、Workqueue 的架构演进
2.1 旧时代:单线程与多队列 Workqueue
Linux 2.6 早期,Workqueue 的实现相对简单。它提供了两种预定义的工作队列:
- 单线程队列(single-threaded):整个系统只有一个内核线程负责一个队列,实现简单但吞吐受限
- 单 CPU 线程队列(multi-threaded):每个 CPU 上创建一条内核线程,性能好但线程数量随 CPU 激增
旧接口通过 schedule_work()、queue_work() 等函数提交工作,开发者无法控制工作在哪条线程上执行。这导致两个严重问题:在服务器级系统(64+ CPU)上创建了大量的内核线程,资源浪费严重;同时缺乏 NUMA 感知能力,工作可能被调度到远端 NUMA 节点执行,导致缓存失效和延迟抖动。
2.2 现代架构:Concurrency-Managed Workqueue(cmwq)
从 Linux 2.6.36 开始,cmwq 全面重构了 Workqueue 子系统。其核心设计思想是:工作队列不绑定具体的 kworker 线程,而是通过工作池(worker pool)按需调度并发执行。
关键数据结构关系:
workqueue_struct // 工作队列对象(逻辑抽象)
└── pool[NR_STD_WORK_QUEUE_POOLS] // 每个 NUMA 节点对应两个池
├── worker_pool[BOUND] // 绑核的 worker pool
└── worker_pool[UNBOUND] // 不绑核的 worker pool(跨 NUMA 均衡)
└── worker_list // 活跃的 kworker 线程链表
```
cmwq 的革新体现在以下几点:
**1. 动态并发控制**
cmwq 的"并发管理"本质是一种反馈调度机制。它维护每个 worker pool 上的活跃 worker 数量,当发现工作积压且没有 worker 正在处理时(`need_more_worker()`),就唤醒新的 kworker 线程;当 worker 空闲超过阈值(默认 30 秒)时,线程会自动退出。这避免了旧方案中线程冗余的问题。
**2. NUMA 感知**
cmwq 为每个 NUMA 节点分别维护 worker pool。默认情况下,Work 提交到特定 CPU 时,会在该 CPU 所属 NUMA 节点的 pool 中调度执行。对于 unbound workqueue,线程可以在所有允许的 CPU 上执行,内核通过 CPU 亲和性和负载均衡优化远端 NUMA 访问。
**3. 工作窃取与负载均衡**
对于 unbound workqueue,cmwq 实现了跨 NUMA 节点的工作窃取(work stealing)。当某个 NUMA 节点的 local pool 处于空闲状态时,会从相邻节点的 busy pool 中窃取工作,充分利用系统空闲计算能力。
**4. 优先级隔离**
cmwq 通过 kworker 线程的 nice 级别实现优先级隔离。系统预定义队列包含 `system_highpri_wq`,其 kworker 运行在 `MIN_NICE` 优先级(nice=-20),确保高优先级工作不被低优先级工作饿死。
---
## 三、核心 API 与使用模式
### 3.1 预定义工作队列
内核内置了 5 个预定义 workqueue,覆盖了绝大多数场景:
| 队列 | 标志 | kworker nice | 说明 |
|------|------|-------------|------|
| `system_wq` | `WQ_UNBOUND` × 旧 | 0(普通) | 通用的 bound workqueue,每个 CPU 一个 pool |
| `system_highpri_wq` | `WQ_HIGHPRI` | -20(高优先级) | 高优先级工作队列 |
| `system_long_wq` | `WQ_UNBOUND` | 0 | 长时间运行的 unbound 工作 |
| `system_unbound_wq` | `WQ_UNBOUND` | 0 | 不绑定 CPU 的 unbound 工作,可跨 NUMA 均衡 |
| `system_freezable_wq` | `WQ_FREEZABLE` | 0 | 系统休眠/冻结时可冻醒的工作队列 |
提交到这些队列的方式有多种:
```c
// 方式1:schedule_work/schedule_delayed_work(提交到 system_wq)
static DECLARE_WORK(my_work, my_work_func);
schedule_work(&my_work);
schedule_delayed_work(&my_dwork, msecs_to_jiffies(100));
// 方式2:显式指定队列提交
queue_work(system_highpri_wq, &my_work);
queue_delayed_work(system_unbound_wq, &my_dwork, msecs_to_jiffies(500));
// 方式3:取消工作
cancel_work_sync(&my_work);
cancel_delayed_work_sync(&my_dwork);
```
### 3.2 创建自定义工作队列
对于有特殊需求的场景(如需要自定义并发级别、nice 值、UNBOUND 属性),可以创建专用队列:
```c
#include <linux/workqueue.h>
static struct workqueue_struct *my_wq;
static void create_my_workqueue(void)
{
// 只允许 4 个并发,nice=-5,不可休眠(no power management)
my_wq = alloc_workqueue("my_wq",
WQ_UNBOUND | WQ_HIGHPRI, // 标志位
4); // max_active(仅对 bound wq 有效)
}
static void destroy_my_workqueue(void)
{
destroy_workqueue(my_wq);
}
```
### 3.3 WORK vs TASKLET 选择决策树
在新代码编写时,常面临 Workqueue 与 Tasklet 的选择问题。以下是一个实用的决策框架:
```
需要延后处理的工作?
├── 是否可能睡眠?
│ ├── 是 → 只能用 Workqueue
│ └── 否(纯中断上下文)→ Tasklet 或 Workqueue
│ ├── 对延迟极度敏感(<1ms)→ Tasklet
│ └── 普通延迟要求(允许几 ms)→ Workqueue(更可预测的调度)
│
└── 是否需要自定义并发策略?
└── 是 → 创建自定义 workqueue(普通或 unbound)
```
现代内核开发社区的趋势是:**优先使用 Workqueue**。原因在于 Tasklet 有一些令人不适的特性——
- 同一 tasklet 不能在多个 CPU 上并行执行(`TASKLET_STATE_RUN` 保护)
- Tasklet 不能在模块卸载时安全地取消(不像 `cancel_work_sync()` 那样会等待完成)
- Tasklet 没有 Workqueue 丰富的调优接口和调试工具
---
## 四、kworker 线程内部机制
### 4.1 kworker 线程的生命周期
kworker 线程由 workqueue 模块在初始化时按需创建,其核心循环在 `worker_thread()` 中实现。简化后的逻辑如下:
```c
static int worker_thread(void *__worker)
{
struct worker *worker = __worker;
struct worker_pool *pool = worker->pool;
// frozen 检查:系统休眠时自动冻结线程
if (pool->flags & POOL_DISASSOCIATING)
worker_leave_idle(worker);
// 工作循环入口
while (!kthread_should_stop()) {
// 尝试从 pool->worklist 中取工作
struct work_struct *work = list_first_entry(...);
if (likely(work)) {
worker->current_work = work;
worker->current_func = work->func;
worker->current_pwq = ...; // 当前所属的 pool_workqueue
// 执行工作者函数
worker->current_func(work);
// 标记完成,允许下一个 worker 接手
...
} else {
// 没有工作时进入空闲状态
worker_enter_idle(worker);
schedule();
}
}
}
```
### 4.2 工作状态机
每个 `work_struct` 在提交到队列时经历以下状态机转换:
```
[提交] WORK_STRUCT_PENDING_BIT 置位 → 加入 worklist
↓
[被 kworker 拾取] WORK_STRUCT_RUNNING_BIT 置位(通过 worker->current_func)
↓
[执行完成] WORK_STRUCT_RUNNING_BIT 清除
↓
[可能再次提交] 或 [彻底销毁]
```
`WORK_STRUCT_PENDING_BIT` 的巧妙之处在于它内嵌在 `work_struct->data` 的低位比特中,避免了额外的内存分配。很多开发者在使用 Workqueue 时踩的坑都涉及对这个标志位的误解:
```c
// 错误使用:直接首次使用 INIT_WORK
static struct work_struct my_work;
INIT_WORK(&my_work, my_work_func);
schedule_work(&my_work);
// 如果 my_work 是全局变量且之前被其他代码使用过,
// PENDING_BIT 可能未清除 → 工作丢失或触发 WARNING
```
正确的做法是确保 work_struct 完全初始化后再提交,或使用 `INIT_WORK` 一次后通过 `schedule_work()`/`queue_work()` 重新提交(此时内部会自动检查 PENDING_BIT)。
### 4.3 kworker 线程数配置
cmwq 限制了每个 CPU 可以同时活跃的 kworker 数量。关键参数:
```c
// Workqueue 头文件中
#define WQ_MAX_ACTIVE 512 // 每个 workqueue 的最大并发
#define WQ_MAX_UNACTIVE 512 // unbound wq 最大活跃 worker
// kworker 线程长度命名格式
kworker/<cpu>:<pool_id>[u][<nice>][H]
// 示例:kworker/0:2H → CPU0 pool2 高优先级(nice=-20)
// kworker/u:3b → Unbound pool3 (nice=-20)
```
在 Linux 6.x 内核中,每个 NUMA 节点的每个 workqueue type(normal/highpri/unbound)维护一个 worker pool。每个 pool 的最大并发数默认为 `WQ_MAX_ACTIVE / 2 = 256`,同时根据 CPU 数量动态调整实际可用的并发数。
---
## 五、生产实践:Workqueue 调优
### 5.1 kworker CPU 占用过高问题
生产系统中常见的"元问题"是:`top` 显示某个 kworker 线程持续占用高 CPU。诊断流程如下:
**第一步:定位哪个 workqueue 的 kworker**
```bash
// 查看所有 kworker 线程的 CPU 占用和绑定的 CPU
ps -eo pid,comm,psr,pcpu,args | grep kworker | sort -k4 -rn | head -20
// 输出示例:
// 1234 kworker/0:2H 0 97.0 [kworker/0:2H]
// 显示该线程绑定在 CPU0,属于高优先级 pool(H 标识)
```
**第二步:使用 ftrace 追踪具体工作函数**
```bash
// 启用 function tracer
echo function > /sys/kernel/debug/tracing/current_tracer
// 过滤 kworker 的当前工作函数
echo '*schedule_work* *queue_work* *worker_thread*' > /sys/kernel/debug/tracing/set_ftrace_filter
// 启用 trace
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 10
cat /sys/kernel/debug/tracing/trace | grep -E 'kworker|work' | less
```
**第三步:通过 perf 火焰图分析热点**
```bash
// 采样 kworker 线程
perf record -g -p <pid> sleep 30
// 生成火焰图
perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flame.pl > kworker-flame.svg
```
典型原因包括:某个驱动或子系统频繁提交 work 导致工作堆积;workqueue 的回调函数内部死循环或阻塞时间过长(饥饿后面队列的工作);NUMA 远端内存访问延迟大导致 kworker 长时间处于运行态。
### 5.2 自定义 unbound workqueue 的并发控制
默认的 `system_unbound_wq` 使用全局 kworker 池,在大量提交短工作时可能引发"惊群效应"——瞬间唤醒大量线程,线程切换开销剧增。解决方案:
```c
#include <linux/workqueue.h>
// 创建具有严格并发限制的 unbound 队列
static struct workqueue_struct *limited_wq;
int __init my_init(void)
{
// max_active=2:最多允许 2 个 worker 同时执行
// WQ_UNBOUND:不绑核,跨 NUMA 均衡
// WQ_MEM_RECLAIM:允许在内存回收路径中使用
limited_wq = alloc_ordered_workqueue("my_limited_wq",
WQ_UNBOUND | WQ_MEM_RECLAIM, 2);
return 0;
}
// 内部使用:提交工作时自然受 max_active 限制
void submit_work(void)
{
// 如果当前已有 2 个活跃 worker,新的工作会在 worklist 排队等待
queue_work(limited_wq, &my_work);
}
```
`max_active` 在不同类型的 workqueue 上含义不同:
- **bound workqueue(如 system_wq)**:每个 CPU 独立计算,max_active 同时控制单个 CPU 上活跃 worker 数量
- **unbound workqueue**:全局控制,所有 NUMA 节点上的 worker 总数不超过 max_active
### 5.3 workqueue 与实时性
在实时内核(PREEMPT_RT)中,kworker 线程被转换为**可以睡眠的 kthread_worker**,使用优先级继承(priority inheritance)机制防止优先级反转。关键变化:
```c
// PREEMPT_RT 下的 kworker 优先级管理
// 1. 默认 kworker 使用 SCHED_OTHER(nice=-5 对 unbound)
// 2. workqueue 支持为特定工作设置优先级:
typedef void (*work_func_t)(struct work_struct *work);
void set_user_nice(struct task_struct *p, long nice);
// 内核框架优先级映射
// WQ_HIGHPRI → nice=-20
// 普通 workqueue → nice=0
// power-efficient → nice=-5(节省功耗)
```
在 PREEMPT_RT 系统中,不建议在 workqueue 回调中进行大量计算,因为实时线程可能因此被饿死。最佳实践是将 workqueue 与 threaded IRQ 结合使用:
```c
// threaded IRQ 允许中断处理在可睡眠的线程中执行
// (priority 受 IRQ thread 的调度策略控制)
request_threaded_irq(irq, irq_handler, irq_thread_fn, IRQF_ONESHOT, "name", dev);
```
### 5.4 NUMA 亲和性优化
在 NUMA 架构服务器上,默认的 bound workqueue 可能导致频繁的跨 NUMA 节点访问。比如:
- 网卡中断绑定在 NUMA node 0 的 CPU
- 数据提交到 `system_wq`(bound),工作也在 node 0 执行
- 但数据本身在 NUMA node 1 的用户进程缓冲区
跨 NUMA 访问内存的延迟大约是本地访问的 2-3 倍。在高速网络(100Gbps+)场景下,这会成为显著的性能瓶颈。
优化方案:使用 unbound workqueue 并限制工作只在数据所在的 NUMA 节点执行。
```c
// 使用 NUMA 节点亲和性的 unbound wq
static struct workqueue_struct *numa_affinity_wq;
int __init my_driver_init(void)
{
struct workqueue_attrs attrs;
// 设置 NUMA 节点亲和性
memset(&attrs, 0, sizeof(attrs));
attrs.numa_node_id = 1; // 只在 NUMA node 1 的 CPU 上运行
numa_affinity_wq = alloc_workqueue("numa_wq", WQ_UNBOUND, 8);
// 应用属性
apply_workqueue_attrs(numa_affinity_wq, &attrs);
return 0;
}
```
Linux 6.x 引入了更精细的 workqueue sysfs 接口,可以直接通过 sysfs 修改现有 workqueue 的参数:
```bash
// 查看 workqueue 参数
cat /sys/devices/virtual/workqueue/system_unbound_wq/nice
cat /sys/devices/virtual/workqueue/system_unbound_wq/max_active
cat /sys/devices/virtual/workqueue/system_unbound_wq/numa
// 修改参数
echo -5 > /sys/devices/virtual/workqueue/system_unbound_wq/nice
echo 1 > /sys/devices/virtual/workqueue/system_unbound_wq/numa // 启用 NUMA affinity(0=禁用,1=node local)
```
---
## 六、高级特性与陷阱
### 6.1 电源管理:Freezeable 与 Power-Efficient
现代 SoC(尤其是移动设备和笔记本)对功耗极为敏感。Workqueue 的电源管理特性确保:
**Freezeable Workqueue(`WQ_FREEZABLE`)**:在系统休眠/唤醒过程中,内核会冻结 workqueue 的执行。冻结后的 workqueue 不再允许新的 kworker 线程被唤醒,已在执行的 worker 也会在下一个调度点被挂起。相关 API:
```c
// 冻结所有 freezable workqueue
freeze_workqueues_begin();
// 恢复 workqueue
thaw_workqueues();
```
**Power-Efficient Workqueue(`WQ_POWER_EFFICIENT`)**:这类 workqueue 的 kworker 线程会被标记为功耗敏感,内核的 EAS(Energy Aware Scheduler)会尝试将这些线程调度到高效核(LITTLE core)上执行,避免 wake-up 大核(big core)。
**需要注意的陷阱**:如果你的 workqueue 回调中调用了 `usleep_range()` 或 `wait_event_*` 函数(这在进程上下文中是合法的),但在系统设计中需要保证"X 时间内必须执行完成"的 deadline,那么 freezeable 的挂起可能是致命的。在 Android 和 Linux 嵌入式系统中,这类问题导致的唤醒延迟(resume latency)往往在数十毫秒到数秒量级。
### 6.2 工作嵌套与死锁
Workqueue 回调中再次提交 Workqueue 是常见的编码模式,但也容易导致死锁:
```c
// 危险模式:同一 workqueue 上重新提交自身
void my_work_func(struct work_struct *work)
{
// 清理资源...
// 死锁陷阱:如果 workqueue 的 max_active=1,
// 自己等待自己取消 → 永远阻塞
cancel_work_sync(work); // 永远等待自己完成,死锁!
// 解决方案:在回调完成后再重新提交
schedule_work(work); // 正确:此时旧工作已完成
}
```
另一个经典的死锁场景涉及多依赖工作:
```c
// 死锁场景
void func_a(struct work_struct *work_a)
{
// 尝试取消自己的依赖工作
cancel_work_sync(&work_b); // 如果 work_b 也在尝试取消 work_a → 死锁
}
```
### 6.3 RCUs 与 Workqueue 的协同
RCU 回调和 Workqueue 是两种常见的延后处理方式。它们各有适用场景:
| 对比维度 | RCU 回调 | Workqueue |
|---------|----------|-----------|
| 执行上下文 | Softirq(不睡眠) | 进程上下文(可睡眠) |
| 延迟保证 | 等待 grace period 结束 | 排队到 kworker 执行 |
| 适用场景 | 立即执行、小开销的对象清理 | 需要睡眠的复杂清理 |
| 吞吐量 | 极高(批量处理) | 受 kworker 并发限制 |
在块设备层,RCU 常用于 `bio` 完成后的快速统计更新,而 Workqueue 用于需要同步元数据的复杂清理(如 SSD 的磨损均衡数据维护)。
---
## 七、调试与观测工具
### 7.1 内核暴露的接口
Workqueue 通过 sysfs 和 debugfs 提供了丰富的观测接口:
```bash
// /sys/devices/virtual/workqueue/ 目录
/sys/devices/virtual/workqueue/
├── system_wq
│ ├── nice ← kworker 的 nice 值
│ ├── max_active ← 最大并发
│ └── numa ← NUMA affinity 使能状态
├── system_unbound_wq
│ ├── nice
│ └── ...
└── ...
// debugfs 接口(需挂载 debugfs)
/sys/kernel/debug/workqueue/
├── stats ← 查看所有 workqueue 的统计信息
├── ...
```
### 7.2 perf 分析 workqueue 延迟
```bash
// 使用 perf 分析 kworker 的执行延迟
// 启用 workqueue tracepoint
ls /sys/kernel/debug/tracing/events/workqueue/
// workqueue_activate_work workqueue_execute_start workqueue_execute_end workqueue_queue_work
// 追踪 work 从提交到执行的时间差
echo 1 > /sys/kernel/debug/tracing/events/workqueue/workqueue_queue_work/enable
echo 1 > /sys/kernel/debug/tracing/events/workqueue/workqueue_execute_start/enable
cat /sys/kernel/debug/tracing/trace
```
### 7.3 drgn:实时查看 workqueue 状态
drgn(debugger with a rust-like language)可以实时检查 Workqueue 状态:
```python
from drgn import prog
# 查看 system_wq 的 pending 工作数量
wq = prog['system_wq']
for pwq in list_for_each_entry('struct pool_workqueue', wq.pwqs_list.address_of_(), 'pwq_list):
if pwq.pool:
active = pwq.nr_active
pending = len(pwq.delayed_works) # 延迟等待的工作
print(f"pool {pwq.pool.cpu}: active={active}, pending={pending}")
```
---
## 八、实战案例:优化块设备驱动的 I/O 完成处理
某 NVMe SSD 驱动在生产环境中出现间歇性延迟毛刺,`iostat` 显示 `%util` 周期性达到 100%,但吞吐量并未显著提升。分析发现,I/O 完成路径中使用的 `system_wq` 的 kworker 线程在 I/O 高峰期被大量唤醒,线程切换开销成为瓶颈。
优化前代码:
```c
// 原始版本:使用 system_wq 处理 I/O 完成
static struct work_struct io_complete_work;
static DECLARE_COMPLETION(io_done);
static void io_complete_func(struct work_struct *work)
{
struct nvme_device *dev = container_of(work, struct nvme_device, work);
// 处理 I/O 完成:解锁页面、通知等待者
blk_mq_end_request(dev->req, BLK_STS_OK);
// 可能执行内存释放(可能引起直接回收)
kfree(dev->dma_buf);
// 唤醒等待 I/O 完成的进程
blk_mq_end_request(dev->req, BLK_STS_OK);
}
static irqreturn_t nvme_irq_handler(int irq, void *dev_id)
{
// 中断处理:将完成处理提交到 system_wq
schedule_work(&io_complete_work);
return IRQ_HANDLED;
}
```
优化方案:创建专用 unbound workqueue,限制并发数等于磁盘并行队列数(避免过度唤醒),并启用 NUMA 局部性亲和。
```c
// 优化版本:专用 unbound workqueue
static struct workqueue_struct *nvme_wq;
static int __init nvme_init(void)
{
// 创建专用队列:CPU 亲和,并发数=NUM_HW_QUEUES
nvme_wq = alloc_workqueue("nvme_wq",
WQ_UNBOUND | WQ_MEM_RECLAIM, NUM_HW_QUEUES);
// 设置 NUMA 亲和(绑定到网卡/磁盘所在 NUMA 节点)
apply_workqueue_attrs(numa_node_attrs(nvme_wq));
return 0;
}
// 中断处理中提交工作
static irqreturn_t nvme_irq_handler(int irq, void *dev_id)
{
queue_work(nvme_wq, &io_complete_work);
return IRQ_HANDLED;
}
```
优化结果:P99 I/O 延迟从 850μs 降低到 120μs,kworker 相关的 CPU 使用率下降 60%,线程切换次数减少 75%。
---
## 九、总结
Workqueue 作为 Linux 内核中最重要的延后处理基础设施,其设计哲学——以进程上下文执行延后工作、动态并发控制、NUMA 感知调度——体现了现代操作系统对性能与灵活性的极致追求。理解 Workqueue 的内部机制,对于驱动开发、内核调优、性能诊断都至关重要。
关键要点回顾:
- Workqueue 允许回调函数在进程上下文中睡眠,这是与 Tasklet/Softirq 的本质区别
- cmwq(Concurrency-Managed Workqueue)通过工作池和动态线程管理实现了高效的并发控制
- `WQ_UNBOUND`、`WQ_FREEZABLE`、`WQ_HIGHPRI` 等标志位设置了 workqueue 的行为属性
- kworker CPU 占用过高通常源于工作过频提交或回调函数耗时过长,应使用 ftrace/perf 精确定位
- 在 NUMA 系统中,正确设置 workqueue 的 NUMA 亲和性可以显著降低跨节点访问延迟
- Workqueue 回调中避免嵌套调用 `cancel_work_sync()` 防止自死锁
推荐进一步阅读:`kernel/workqueue.c`(内核源码)、`Documentation/core-api/workqueue.rst`(内核文档)、以及 Brendan Gregg 关于 kworker 分析的博客文章。

发表评论 取消回复