workqueue
-
有些时候内核需要一个异步的进程执行上下文,而工作 队列(workqueue)可以满足这种需求。
工作队列中的每一个元素都是一个工作项(work item), 有一个函数与工作项相关,这个函数就是工作项所要处 理的任务。
内核中有一个专门的线程——被称作worker,来依次执行 工作队列中的每一个工作项对应的函数,当工作队列为 空时,这个worker就变为空闲状态(idle),当有新的 工作项加入到工作队列时,worker又重新开始执行。
-
在最早的实现中包括两种实现方式,一种是整个系统只 有一个worker(single thread,ST),另一种是每个 CPU包含一个worker(multiple thread,MT),每一个 CPU包含一个属于自己的worker pool 。
这两种实现都引起了系统中对于这种异步上下文的竞争, 只不过是MT方式的竞争可能更小一些。
因此,内核人员对workqueue作了重新实现,新的实现 被称作concurrency managed workqueue(cmwq),新的 实现的特点如下:
-
与之前的API兼容
-
实现了统一的每CPU worker pool,减少了资源的浪 费,提高了并发的灵活性
-
可以自动调节worker pool和并发的级别
-
-
为了简化执行这种异步上下文 ,引入了work item,它 是一个简单的结构体,包含一个函数指针,这个指针指 向的函数就是需要在异步上文中执行的函数。
当一个内核子系统或者驱动程序需要在异步上下文中执 行一个函数时,它首先需要创建一个work item结构体, 然后将这个结构体加入到工作队列中。
工作者线程(worker thread)负责从工作对列中取出work item并执行,直到工作队列中的work item为空。工作 者线程是由worker-pool管理的。
cmwq的实现在用户接口(即子系统或者驱动程序的使用) 和后台支持上(即如何管理worker pool以及处理work item)有差别。
每一个CPU上都有两个worker pool,一个是用来处理普 通的work item,另一个是用来处理高优先级的work item。 另外还有一些worker pool用来处理未添加到绑定到CPU 的wq上的work item,这些worker pool的数量是动态的。
可以通过修改工作队列的属性来改变添加到其上的work item的执行行为,例如在哪个CPU上执行、并发限制、优 先级等。
当一个work item添加到workqueue时,根据函数的参数 以及要添加到的工作队列的属性就可以确定将由哪一个 worker pool来执行,并且会将该work item添加到该 worker pool共享的工作列表中。例如,当一个work item被添加到一个workqueue时,它要么被添加到普通 的worker-pool的工作列表,要么被添加到高优先级的 worker-pool的工作列表,这里的两个worker-pool对应 于添加work item到工作队列的CPU。
管理一个工作者线程池的并发度一直是工作者线程池面 临的一个重要的问题。cmwq保持了最小的并发度,但是 又让CPU不会空闲,充分利用了CPU资源。
每一个绑定到CPU的线程池通过在调度器中添加钩子实 现了并发的控制。当一个工作项被唤醒或者睡眠的时候, 工作者线程池都会接到通知,以此来追踪当前并发的工 作者线程数。一般来说,当一个CPU上有一个或者多个 工作者线程在执行的时候,与该CPU绑定的工作者线程 池不会再起动其他的工作者线程,当该CPU上最后一个 工作者线程睡眠后,立刻启动一个新的工作,来保证CPU 不会空转。
对于未绑定到CPU的工作队列来说,它的线程池数量是 动态的。可以通过apply_workqueue_attrs函数来设置这 个未绑定的工作队列的参数,系统会自动生成与这些参 数对应的工作者线程池。另外对于绑定的工作队列可以 设置某些参数,让某个工作队列忽略并发的限制。
任何需要多个工作者线程同时执行的子系统或者驱动程 序都需要使用有急救工作者线程(rescuer worker)的 工作队列。例如,在内存回收的时候,回收内存的工作 项往往需要同时执行,因此如果没有使用这种工作队列 的话,就会造成死锁,因为后执行的工作者线程会等待 前面的工作者线程被释放。
-
应用程序接口(API)
alloc_workqueue负责创建一个工作队列,之前的create_*workqueue 之类的接口已经被丢弃了。该函数有三个参数@name, @flags以及@max_active,@name表示该工作队列的名字, 如果急救工作者线程也存在的话,那么@name也是该急救 工作者线程的名字。
工作队列已经不再管理执行资源,但是它作为一个域用 来管理工作项,例如保证工作项往前执行、flush以及 工作项的属性。@flags和@max_active参数控制工作项 的执行资源分配,如何被调度以及如何执行。
@flags:
WQ_UNBOUND 未绑定的工作队列对应的工作者线程池数是动态的。该 工作者线程池未绑定到任何的CPU,因此相当于是一个简 单的执行上下文的提供者,它会立即执行添加到该工作 队列工作项。一般在下面两种情况会用到:
-
工作者线程的并发级别波动很大,且将工作项添加到 工作队列的发起者会在不同的CPU之间来回执行,因 此如果不使用未绑定工作队列的话,就会造成在各个 CPU的工作者线程池上创建了许多不会使用的工作者 线程。
-
当CPU的负担很重时,最好将工作项添加到位绑定的工 作队列,由调度器来进行调度。
WQ_FREEZABLE 当系统暂停时,工作队列会进入到冻结状态。并且会清 空该工作队列中的工作项,直到被解冻才可以执行新的 工作。
WQ_MEM_RECLAIM 任何可能在内存回收路径上使用的工作队列都必须设置 该标记,它保证了不管内存压力有多大,都至少有一个 执行上下文与之对应。
WQ_HIGHPRI 高优先级工作队列的工作项被加入到相应CPU的高优先 级工作者线程池。高优先级工作者线程池由提高了nice 级别的线程来服务。
普通的工作者线程池和高优先级的工作者线程池是隔离 的,他们之间不进行任何的交互。对于并发级别的控制 也是各自独立进行控制的。
WQ_CPU_INTENSIVE 这种工作队列中的工作项并不受并发级别的控制,因为 该类型的工作项通常会需要很多的CPU使用量,因此最 好的办法就是由系统调度器来进行调度。
另外,并发级别的限制会影响密集型工作项的执行,例 如当前正在运行的非密集型的工作会延迟密集型工作的 执行。
该标记对未绑定的工作队列是无效的。
@max_active:
表示每个工作队列同时最多能有几个工作项在同一个CPU 上运行。例如,max_active=16,表示同时最多能有16个 该工作队列的工作项在每一个CPU上运行。
对于绑定的工作队列,@max_active的最大值为512,默 认情况下,该参数传值为0,此时它的最大值为256。对 于未绑定的工作队列,max_active的最大值要大于512。
工作队列的同时处于活跃状态的工作项的数目是由用户 调节的,例如用户同时添加到工作队列中的工作项的数 目。除非有需求调节活跃工作项的数目,否则,推荐该 参数赋值为0。
有些需要依赖ST工作队列的顺序,因此可以使WQ_UNBOUND 和@max_active为1。这样就模拟了ST,每一个该类型的 工作项都应该被添加到未绑定的工作队列中,且一次只 能有一个工作项执行就限定了执行的顺序。
-
-
指导方针
-
如果工作队列中的工作项可能用在内存回收代码路径 中,一定要设置WQ_MEM_RECLAIM,每一个该类型的队 列都有一个保留的执行上下文。另外,如果在内存回 收路径中有多个工作项相互依赖的话,应该将这些工 作项添加到不同的工作对列中。
-
如果没有特殊要求的话,推荐@max_active参数的值 为0。
-
一般如果没有执行次序要求的话,不会用到ST。
-
工作队列作为工作项一个域,用来WQ_MEM_RECLAIM, flush以及某些工作项的共有属性。如果工作项不会 用到上面的任何一个特性,可以使用系统工作队列。
-
除非一个工作项要耗费很多的CPU,一般将工作项添 加到绑定的工作队列中。
-
ref
1. Documentation/workqueue.txt