linux cmwq介绍

一.简介

在很多情况下,我们需要一个异步的执行上下文。比如对于linux中断而言,我们可以把紧急的任务放在中断服务函数中进行,而把不紧急的任务延后执行。延后执行的方式有很多,比如 tasklet和软中断。但是tasklet和软中断的缺点是,虽然是延后执行,但是其执行期间还是属于中断上下文,中断上下文最大的问题是不能执行可能导致睡眠的函数,比如kmalloc。针对这个问题,内核提供了工作队列(wq)机制,能够使得延后执行的任务在进程上下文执行,因为工作队列中的工作项是由内核线程来执行的。

当我们使用工作队列机制时,描述具体工作函数的工作项被放在一个队列上。一个独立的内核线程用来提供这个异步的执行上下文。这里的队列,我们称之为工作队列(workqueue),而这个线程,我们称之为工作者线程(worker)。

当工作队列上存在工作项时,工作者线程就一个一个地执行工作项关联的工作函数。当工作队列上没有工作项时,工作者线程的状态就变为闲置(idle)。当有新的工作项入队列时,工作者线程就再次运行。

二.为什么使用并发管理的工作队列(Concurrency Managed Workqueue,cmwq)?

在内核原始的实现中,工作队列(workqueue)有两种实现形式:

1. 多线程式工作队列:该类型的工作队列,为每个CPU都分配了一个工作线程,因此其工作线程的数量等于CPU的数量。

2. 单线程式工作队列:该类型的工作队列拥有一个系统范围内的工作线程。

随着内核中越来越多的使用多线程式工作队列,以及系统中CPU的数量越来越多,在某些系统中,默认的32K范围的PID空间显得越来越捉襟见肘。

多线程式的工作队列即使浪费了许多系统资源,其并发性也并不能让人满意,更不用提单线程式的工作队列了。一个多线程式的工作队列只能为你每个CPU提供一个执行上下文,而单线程式的工作队列甚至需要为整个系统提供一个执行上下文。这些有限的执行上下文导致了诸多问题,包括容易在单执行上下文发生的死锁。

并发性和系统资源之间的矛盾越来越突出,使得用户不得不在使用中做出一些让步。比如,在单线程式的工作队列中,不能同时出现两个对PIO进行轮询的工作项;由于多线程式的工作队列并不能提供更好的并发性,对于对并发要求高的用户,只能自己实现线程池(thread pool)。

并发管理工作队列(cmwq)是对工作队列(wq)的重新实现,并专注于以下目标:

1. 保持与原始工作队列API的兼容性。

2. 使用所有等待队列共享的每CPU统一工作者线程池来提供灵活的按需并发级别,而不会浪费很多资源。

3. 自动调节工作者线程池和并发级别,以便API用户无需担心此类细节。

三.设计

首先引入工作项(work item)的概念,以简化对异步函数的执行。

一个工作项(work item)其实就是一个简单的结构体,该结构体包含了需要被异步执行的函数。当驱动工程师想让一个函数异步执行,他就必须创建一个工作项,并包含异步执行的函数的函数指针,最后将该工作项扔进一个工作队列中。

工作者线程(worker threads)专门用于一个接一个地处理这些工作项。当所有地工作项处理完成后,工作者线程就自动变为idle状态。所谓地工作者线程池(worker-pools)专门用于管理这些工作者线程。

在前端,面向用户的工作队列(workqueues)用于将驱动或者子系统的工作项项管理成队列;在后端,线程池负责处理这些排成队的工作项。新的cmwq在设计上将前端的工作队列和后端的线程管理机制区分开来。

有两种工作者线程池:

1.Bound类型:该类线程池是每CPU的,系统中每个CPU都有拥有两个线程池,一个是高优先级线程池,还有一种是普通优先级线程池。

2.Unbound 类型:该类型的线程池用于处理不绑定特定CPU的工作队列(unbound workqueues)上的工作项,并且该类型的线程池的数目是动态变化的。

子系统和驱动程序可以使用适合的工作队列API函数来创建工作项,并将这里工作项添加进工作队列。在初始化工作队列时,可以通过设置不同的标志(flags),就能影响该工作队列上工作项被执行的方式。这些标志能够影响很多,包括工作项与哪个CPU绑定,并发的上限,执行优先级等方面。

当一个工作项被塞进一个工作队列后,具体使用哪个工作者线程池,是由队列参数和工作队列属性决定的。比如,通常情况下,bound类型的工作队列上的工作项,由发起者所占用的CPU上的两个线程池之一来执行。

对于任何工作者线程池的实现,管理并发级别(处于活跃的执行上下文的个数)是一个重要的话题。在满足系统需求的前提下,cmwq尽量将并发级别控制在最低,以降低对系统资源的占用。

绑定特定CPU的工作者线程池通过与系统调度器挂钩的方式来实现并发管理。

每当活动的工作者线程被唤醒或睡眠时,都会通知其所属的工作者线程池,工作者线程池以跟踪当前可运行的工作者线程的数量。

通常来说,工作项一般不会占用CPU太长时间。这就意味着,最优的方式就是只要维护足够的并发量,保证对任务的执行不会停下来就行。只要CPU上有一个或者多个可执行的工作者线程存在,工作者线程池就不会开始执行新的工作项;但是,当最后一个工作线程睡下去后,就立即调度新的工作线程,以保证当有挂起的工作项时,CPU不会进入idle。这种策略能够保证我们能够在不损失执行带宽的前提下,使用最少的量的工作者线程。

将idle态的工作者线程暂时保留,仅仅会占用内存空间而不会有其他开销,所有cmwq在将idle线程销毁之前,会保留它们一段时间。

对于unbund类型的的工作队列来说,其使用的工作者线程池的数量是变化的。使用``apply_workqueue_attrs()``函数可以指定给定工作队列的属性,并同时自动创建能够匹配这些属性的工作线程池。调节并发等级的权力和责任都交给了用户。当然,对于bound类型的工作队列,也有一个标志,表示忽略并发管理。可以参考API小节。

处理工作项进度的保证,依赖于可以在需要更多执行上下文时创建工作线程,而这又可以通过使用救援线程(rescue-worker)来保证。所有可能在内存回收的代码路径上执行的工作项必须放到特定的工作队列上,该工作队列上有一个rescue-worker可以在内存紧张的情况下执行,这样避免在内存回收时出现死锁。

四.API

4.1 alloc_workqueue()

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

4.2  flags

WQ_UNBOUND:

在unbound类型的工作队列上排队的工作项,将由unbound类型的工作者线程池处理。这一点使得工作队列充当执行上下文的提供者,而不用管理并发。unbound类型的工作者线程池会尽快开始执行这些工作项。虽然牺牲了cache局部性的优势,损失了一定的性能,但是对于下列几种情况是有用的:

1.并发级别需求的波动非常大,如果使用bound 类型的工作队列,则会在不同CPU上创建大量的worker,并且这些worker大部分时间都是空闲的。

2.需要长时间运行的CPU密集型工作的工作项,这样可以由系统调度程序更好的管理,而不占用某个CPU过长时间。

WQ_FREEZABLE:

可冻结的工作队列参与系统suspend操作的freeze阶段,并暂停对新的工作项的执行直到解冻。

WQ_MEM_RECLAIM:

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

WQ_HIGHPRI:

高优先级的工作队列的工作项会被放入目标CPU的高优先级线程池。高优先级的线程池的线程拥有高nice级别。普通的线程池和高优先级的线程池彼此独立,互相不影响。

WQ_CPU_INTENSIVE:

设置为CPU密集型的工作队列上的工作项不会影响并发级别,即CPU密集型的work执行时并不会阻止同一个线程池里其他WQ的work的执行。这对希望独占CPU周期的work非常有用,因为系统调度程序调度他们的执行。如果不设置该标记,则独占CPU周期的work会导致同一个线程池里其他WQ的work得不到执行。

由于统一由CMWQ的并发管理进行调度,在非密集型的WQ的work运行过程中,会导致密集型的WQ的work被推迟。该flag仅适用于bound的工作队列,对unbound的工作队列无效。

4.3 max_active

max_active用于指定工作队列在每个CPU上最大的执行上下文个数,即并发处理的work个数。

例如,如果将 max_active设置为16,则每个CPU可以同时处理16个该队列上的工作项。

目前对于bound类型的工作队列,max_active最大可以设置为512,如果max_active入参为0,则使用默认值256。对于unbound 工作队列,最大值为512和4*cpu核数两个里面较大的值。

对于希望使用单线程式的工作队列的使用者,可以设置max_active为1,并且设置WQ_UNBOUND标识。这样整个系统里只有一个该工作队列上的work正在执行。在当前的实现中,以上的配置只能保证在一个给定的NUMA节点中是单线程的行为。想取得系统范围内的单线程行为,需要使用“alloc_ordered_queue()”。

五. 执行场景举例

以下执行场景举例将说明cmwq在不同的配置下将如何工作。

工作项w0 ,w1,w2,在bound类型的工作队列 q0上排队等待被同一个 cpu执行。w0消耗了cpu 5ms,然后睡眠了10ms,然后又消耗了5ms cpu,最后结束。w1,w2也都消耗了cpu 5ms,然后睡眠10ms。

 忽略所有的其他任务,工作和处理开销, 假设就只是简单的FIFO调度,那么下面的列表是一个高度简化的事件可能发生的顺序的版本。

时间(ms)事件
0w0开始消耗CPU
5w0睡眠
15w0唤醒,继续消耗CPU
20w0结束,w1开始消耗CPU
25 w1睡眠
35w1醒来并结束运行,w2开始消耗CPU
40w2睡眠
50w2醒来并结束

如果设置max_active参数大于等于3:

时间(ms)事件
0w0开始消耗CPU
5w0睡眠,w1开始消耗CPU
10w1睡眠,w2开始消耗CPU
15w2睡眠,w0唤醒并消耗CPU
20w0结束运行,w1醒来并结束运行
25w2醒来并结束运行

如果设置max_active参数等于2:

时间(ms)事件
0w0开始消耗CPU
5w0睡眠,w1开始运行
10w1睡眠
15w0唤醒,继续消耗CPU
20w0结束运行;w1运行结束;w2开始运行,消耗CPU
25w2睡眠
35w2结束

现在我们假设w1和w2被放入了另一个队列q1中,并且该队列的WQ_CPU_INTENSIVE标记被设置。

时间(ms)事件
0w0开始消耗CPU
5w0睡眠,w1,w2开始运行
10 w1睡眠
15w2 睡眠,w0醒来继续执行
20w0运行结束,w1运行结束
25w2醒来并运行结束

六. 指南

1.如果一个队列可能会处理进行内存回收的工作项,那么不要忘记使用WQ_MEM_RECLAIM标记。每一个带有WQ_MEM_RECAIM的队列都有一个保留的执行上下文。如果在内存回收的过程中,各个工作项之间有依赖关系,那么它们必须要被放入不同的队列中,而这些队列也要带有WQ_MEM_RECLAIM标记。

2.除非是需要严格的顺序执行条件,否则不需要使用ST wq.

3.除非有特殊需要,否则使用默认推荐的@max_active的值0。在一般的用户场景中,并发级别通常在该默认值下都能运行的很好。

4.对于wq的操作以及工作项的执行来说,随着cpu cache的日益增长,除非工作项要消耗大量的CPU时间,否则最好使用bound队列.

 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值