一、前言
在内核驱动中,常常见到 工作队列(workqueue)。对于熟悉内核或者驱动的工程师来说,这个机制应该是比较熟悉的,经常出现在 中断上下文 中,用于执行中断后的操作。随着内核发展,驱动遇到越多越多的场景,而 工作队列 也逐渐发展,现在我们常用的 工作队列 称为 并发管理工作队列(concurrency managed workqueue),本文对 工作队列 进行简单的介绍和用法说明,希望能够帮助各位读者熟悉其使用
二、workqueue介绍
2.1 workqueue说明
工作队列 常常在内核驱动中会被使用到,而内核中也有对于其描述的文档,其路径为 Documentation/core-api/workqueue.rst:
下面引用原文来看看 workqueue 的描述
There are many cases where an asynchronous process execution context is needed and the workqueue (wq) API is the most commonly used mechanism for such cases.
When such an asynchronous execution context is needed, a work item describing which function to execute is put on a queue.
An independent thread serves as the asynchronous execution context.
The queue is called workqueue and the thread is called worker.
While there are work items on the workqueue the worker executes the functions associated with the work items one after the other.
When there is no work item left on the workqueue the worker becomes idle.
When a new work item gets queued, the worker begins executing again.
简单来说,工作队列 提供了一种 异步执行任务 的机制。一般有以下几个概念:
work:指需要异步执行的任务
woker:指处理 work 的 异步执行上下文,通产可以理解为一个线程
workqueue:多个 work 以节点的形式链接起来形成 workqueue,其可以通过调度让 woker 来执行 work
2.2 为什么使用workqueue
中断的 bottom half机制 比如 tasklet 都是在 中断上下文(softirq) 中执行,而在 中断上下文 中通常是不能执行 休眠 操作中,假如有某些特殊的 bottom half 需要休眠,此时则不能使用 task。
workqueue 是运行在进程上下文中的,因而可以执行休眠操作,这是和其他 bottom half机制 有本质的区别,大大方便了在驱动中处理中断。
2.3 旧版本Workqueue和Concurrency Managed Workqueue
在驱动中我们常常使用 workqueue 来提供 异步执行环境,此时我们定义 work 并链入 workqueue,由 woker 不断处理 work。处理完毕后 worker 进入休眠。
其操作流程看起来简单,但这里面有不少细节需要跟读者们说明一下,以让各位好了解 旧版本workqueue 的不足之处。
我们可以先看看内核原文:
In the original wq implementation, a multi threaded (MT) wq had one
worker thread per CPU and a single threaded (ST) wq had one worker
thread system-wide. A single MT wq needed to keep around the same
number of workers as the number of CPUs. The kernel grew a lot of MT
wq users over the years and with the number of CPU cores continuously
rising, some systems saturated the default 32k PID space just booting
up.
Although MT wq wasted a lot of resource, the level of concurrency
provided was unsatisfactory. The limitation was common to both ST and
MT wq albeit less severe on MT. Each wq maintained its own separate
worker pool. An MT wq could provide only one execution context per CPU
while an ST wq one for the whole system. Work items had to compete for
those very limited execution contexts leading to various problems
including proneness to deadlocks around the single execution context.
内核线程数量太多。在错误使用 workqueue机制 的情况下,容易消耗尽 PID space,而扩大 PID space 则会导致系统 task 过多而造成性能的负面影响。
并发性不足。如果是 single threaded workqueue,则没有并发的概念,任何的 work 都是按顺序排队执行,没有执行到的 work 只能原地等待。如果是多核的 per-CPU multi threaded workqueue,虽然创建了 thread pool(即多个worker),但是其数目是固定的。且每个 oneline cpu 上运行一个 严格绑定的woker,从而到了每个线程都只能严格运行在绑定的 CPU 上,这就造成了无法并发处理的情况。例如 cpu0 上的 worker 进入阻塞状态,那么由该 worker 处理的 workqueue 中的 其他work 都会被阻塞,不能转移到 其他CPU 去执行。
死锁问题。举个例子:比如驱动中存在 work A 和 work B,且 work A 依赖 work B 的执行结果。如果这两个 work 都被同一个 CPU 的同一个 worker 调度并执行的时候会出现问题。由于 worker 不能并发的执行 work A 和 work B,因此该驱动模块会发生 死锁。
二元化的线程池机制。workqueue 创建的线程数目要么是 1,要么是 number of CPU。无法自主设置,不够灵活。
而 Concurrency Managed Workqueue (cmwq) 为了解决这些问题实现了以下几个功能:
兼容旧版本的 workqueue API
让所有的 workqueue 共享 worker pool,以按需提供并发处理并节省资源
自动调节 并发性能,用户无需关心细节
三、并发管理工作队列(cmwq)
3.1、与传统workqueue的区别
对于 workqueue 的使用方法而言,前端的操作包括 2 种:
创建 workqueue
将指定的 work 添加到 workqueue。
在 传统workqueue 中,工作线程(worker thread) 和 workqueue 是密切联系的。
对于 single thread workqueue,创建单个 系统范围内的worker thread。而对于 multi thread workqueue,则创建 per-CPU worker thread,也就是每个 worker thread 对应一个 CPU,一切都是固定死的。
一般情况下,workqueue 是提供一个 异步执行的环境,如果把 创建workqueue 和 创建worker thread 这 2 个操作固定在一起会大大限定了资源的使用。用户并不关心后台如何处理 work 、释放后使用多线程等具体细节。
在 cmwq 中,打破了 workqueue 和 worker thread的绑定关系。其使用了 worker pool 的概念(可以理解为线程池)。在系统中已经存在若干个不同类型的 worker pool,且它们不和 特定的workqueue 绑定,所有的 workqueue 都可以根据自身的需要选择使用其中的 worker pool 。
用户可以在创建 workqueue时通过指定 flag 来约束在该 workqueue 上的 work 的处理方式。workqueue 会根据指定的 flag 将 work 交付给系统中对应的 worker pool 去处理。
通过这样的方式让所有的 workqueue 共享系统中的 worker pool,即减少了由于创建 worker thread 带来的资源的浪费,又因为 worker pool 可以根据自身的情况选择是否 减少或增加woker thread 从而提高了灵活性和并发性。
3.2、cmwq如何解决问题
我们前面提到了,传统的 workqueue 有几个问题,下面我们看看如何解决
3.2.1、如何解决线程数目过多的问题?
在 cmwq 中,用户可以根据需求来创建 workqueue,但一般情况下不需要创建 worker thread,而且是否需要 新woker 也由 woker thread pool 本身决定。也就是说 创建workqueue 已经和 后端的线程池操作没有关系 了。
系统中的 woker pool 包括 2大种:
和特定CPU绑定的线程池:也称为 per CPU worker pool,这类 woker pool 也细分为 2 种:
normal thread pool:用于 管理普通优先级的worker thread 和 处理普通优先级的work
high priority thread pool:用于 管理高优先级的worker thread 和 处理高优先级的work
此类 worker pool 的数量是 固定的,主要和 CPU数量 的数目。假设了 N个online CPU,则会创建 2N个 per CPU worker pool。也就是每个 CPU 都有 2 个绑定的 worker pool,分别为 normal thread pool 和 high priority thread pool。
unbound线程池:也就是 非绑定worker pool,此类 worker pool 的 worker thread 可以运行在 任意的CPU 上。这种 wokrer pool 可以动态创建。如果系统中已经 有了相同属性的worker pool则不需要创建新的线程池,否则需要创建。worker pool属性 包括该worker pool 中的 worker thread的优先级、可以运行的CPU链表 等。
基于上面的讲述,worker thread pool 创建 worker thread 的策略是怎样呢?是否会导致 worker thread 数目过多呢?
在默认情况下,创建 worker thread pool 的时候会创建 一个worker thread 来处理 work。thread worker pool 会 根据work的提交以及work的执行情况动态创建 worker thread。
cmwq 使用了一个比较简单的策略:
当 worker thread pool 中 处于运行状态的worker thread 的数量等于 0,并且有 需要处理的work 的时候,thread worker pool 就会创建 新的worker线程。
当 worker线程处于idle的时候,不会立刻销毁它,而是保持一段时间。如果这时候有创建 新的worker 的需求候,那么直接唤醒 idle worker 即可。
如果 一段时间内过去仍然没有事情处理,那么销毁 worker thread 。
3.2.2、如何解决并发问题?
假设有 A、B、C、D 四个work 在某个 CPU 上运行。在默认情况下,worker thread pool 会创建 一个worker 来处理这四个 work 。在 传统workqueue 中,这四个 work 会在 CPU 上 串行执行。也就是假如 work B阻塞了,那么 work C、work D 无法执行下去,需要一直等到 work B 解除阻塞并执

本文介绍了Linux内核工作队列(workqueue)的使用和原理,包括工作队列的基本概念、为什么使用工作队列、旧版与并发管理工作队列的对比以及并发管理工作队列如何解决问题。重点讲述了并发管理工作队列如何通过线程池提高并发性和资源利用率,并提供了相关API的使用说明。
最低0.47元/天 解锁文章
513

被折叠的 条评论
为什么被折叠?



