并发管理工作队列(kernel译文[1])

来源:互联网 发布:nginx 模型 编辑:程序博客网 时间:2024/04/27 18:03

并发管理工作队列(Concurrency Managed Workqueue)

9月,2010

目录
介绍
为什么是cmwq?
设计
API
运行情景举例
指南
调试

  • 并发管理工作队列Concurrency Managed Workqueue
    • 介绍
    • 为什么是cmwq
    • 设计
    • 应用程序接口API
    • 一些执行的场景
    • 指南
    • 调试

介绍

       异步进程的执行在很多情况下会发生,而workqueue的API就是针对这些情况最经常使用的机制。
当需要异步执行时,会将一个描述哪个函数将要执行的工作项(workitem)放入一个队列(queue)当中。一个独立的线程会作为异步可执行的上下文。该队列被称为工作队列(workqueue),该线程则被称为工作者(worker)
当在工作队列(workqueue)中有很多工作项(workitem)时,工作者(worker)会一个个的执行与工作项相关联的函数。

为什么是cmwq?

       在原始的wq的实现中,多线程(MT)的wq每一个cpu都会有一个工作者线程,而单线程(ST)的wq在整个系统中只有一个工作者线程。一个多线程wq需要维系和CPU数量等同的工作者数。内核这几年增加了很多MT wq的使用者,并且随着cpu核的数量的不断增加,一些系统在刚启动后默认的32K的pid空间就已经饱和了。

       虽然MT wq浪费了很多的资源,但是所提供的并发的级别仍然没有得到满足。尽管单线程wq的这种情况没有多线程wq严重,但是这种限制在两者上都是普遍发生的。每一个wq都会维护其自己独立的工作者线程池。MT wq会为每一个cpu提供一个可执行上下文(就是worker thread),而ST wq则是在整个系统中维护一个可执行上下文。多个工作项不得不为了那些有限制的可执行上文来进行竞争,而这种竞争则导致了各种各样的问题,这些问题包括在单个可执行上下文中极易造成死锁的问题。

       并发水平和资源使用的矛盾造成的担忧致使它们的使用者做了很多不必要的权衡工作。譬如libata选择使用ST wq来polling PIOs,同时意味着其接受了在同一时间没有两个polling PIOs同时进行的事实。因为MT wq并不能提供更好的并发,那么需要更高并发水平的用户(如async,fscache)不得不实现他们自己的线程池。

       而cmwq则是对之前wq的一种重新实现,而这种实现则主要为了实现下面这些目标:
* 保持和之前的工作队列API的兼容
* 所有的wq使用基于per-CPU的统一的工作者线程池,从而提供更灵活的按需的并发水平,并且不会浪费很多资源
* 自动调节工作者线程池和并发水平之间的矛盾,不需要用户担心太多的其中的细节

设计

       ​为了减轻函数异步执行的压力,推出了一个新的抽象概念“工作项(work item)”。

       工作项只是一个简单的struct结构,这个结构包含了一个指向将要异步执行的函数指针。无论什么时候,当一个驱动或是子系统模块想要异步执行一个函数,那么这个模块必须要建立这样的一个结构,然后将该函数指针指向将要执行的函数,并将work item放入工作队列中。

       一些被称为工作者线程(worker thread)的特殊线程,他们会从工作队列中将工作项一个个的取出来执行。如果队列空了,那么该线程就会进入idle状态。这些工作者线程被统一管理起来,就是所谓的工作者线程池(worker-pools)。

       cmwq的设计思想中,做了两部分区分,分别是面向用户的前端设计,包括驱动程序或子系统如何放入和取出工作项的接口;以及管理工作者线程并处理工作者项的后端机制。
在每个可能的CPU上,有两个工作者线程池,一个是为普通的工作项准备的,另一个是为高优先级的工作项准备的,还有一些额外的工作者线程池,这些线程池是为那些放入unbound队列的工作项准备的。这些后台线程池的数量是动态变化的。

       子系统模块可以通过 API函数接口来创建工作项,并将其放入相应队列中。它们还可以通过对工作队列设置一些标记来影响在其上执行的工作项的行为方式。这些标记包括如cpu locality,并发限制(concurrency limits),优先级等。想要获取更多的细节,请参考对下面alloc_workqueue()函数接口的描述。

       当工作项被放入工作队列的时候,目标工作者线程池就已经被确定了并且被添加到共享的worklist中。确定条件就是放入队列参数,队列属性。譬如,除非被覆盖,否则一个bound队列中的工作项会被添加到对应的队列中,而相应的线程池要么是normal,要么是highpri的线程池,并且相应的cpu也是该工作项运行的那个cpu。

       对于任何一种工作者线程的实现来说,如何管理好并发水平(就是多少可执行上下文是激活状态)都是一件非常重要的事情。cmwq试图保持最小的并发状态,并且在这种状态下对于系统来说还能够实现最大的工作效率。

       每一个绑定到CPU上的线程池实现并发管理都是和进程调用器(scheduler)挂钩的。当一个工作者线程被唤醒,或是进入睡眠状态的时候,该工作者线程池就会被通知到,而且该线程池会跟踪当前可以运行的工作者线程的数量。一般的,工作项不应该独占CPU并消耗许多cpu时间周期。这就意味着如何维持足够的并发而又能防止工作项的运行有延迟是需要被调优的。只要在一个cpu上有一个或多个可运行的工作者,那么线程池就不会再启动新的工作,但是,当最后一个运行的工作者进入休眠状态后,系统会立刻调度一个新的工作者使得该CPU不会因为闲置而阻塞工作项。这样的话就能够既使用了最小数量的工作者,又不会损失执行带宽。

       对于kthreads来说,使工作者线程处于idle状态并不会消耗更多除内存外的其他资源,所以cmwq会在杀死这些线程之前使他们保持一段idle状态。

       对于unbound的工作队列,后台的线程池的数量是动态的。unbound的工作队列可以使用apply_workqueue_attrs()接口函数自定义属性,并且该队列会自动的创建符合该属性的后台工作者线程池。调节并发的责任在使用者。对于bound wq来说,也有一个flag来标记让其忽略并发管理。更多细节请参考API部分。

       当更多的可执行上下文是必要的时候,工作者线程可以被创建,这个机制是进程能够进行下去的保障。反过来说,我们可以通过救援工作者线程(rescue worker)来确保进程的顺利执行。举个例子,当所有的需要处理内存回收的工作项都被放到队列中时,就需要有一个救援工作者在内存压力大的情况下执行这些工作项。否则,就可能会造成线程池的死锁,因为它正在等执行上下文去释放内存。

应用程序接口(API)

       alloc_workqueue()分配一个工作队列。原先的create_*workqueue()函数已经过时了,并且以后会在kernel中被移除。alloc_workqueue()需要三个参数 —— @name,@flag和@max_active。@name是wq的名称,并且会被当做相应救援线程的名称。

       工作队列已经不再对执行资源进行管理了,但是它作为域保证了进程的运行,flush,以及工作项的属性。@flag和@max_active用来控制到底有多少工作项被分配了执行资源,被调度和执行。

@flag:

WQ_UNBOUND    unbound队列中的工作项会被相应的工作者线程池处理,这些线程池管理的工作者不会和任何制定的CPU绑定。这样的话,该队列的行为就相当于是提供了一个简单的执行上下文,而不会做并发管理。未绑定的线程池只要有可能就会试图启动工作项的执行。未绑定的队列牺牲了cpu的相关性,但是有一下用处。* 我们期盼并发需求的宽度浮动,并且如果使用bound的队列会因为在不同的cpu上运行  导致在多个cpu上产生很多不用的工作者线程。* 长时间运行的CPU负载能够更好的被系统调度程序管理。 WQ_FREEZABLE冻结队列因为参与了系统的挂起操作流程,在该队列上的工作项会被排出(**drained**),直到被解冻之前都不会有新的工作项被执行。WQ_MEM_RECLAIM所有有可能运行在内存回收流程中的工作队列都需要设置该标记。这样能够确保不管是否在内存压力比较大的情况下都能有至少一个执行上下文能够运行。WQ_HIGHPRIhighpri的队列中的工作项会被被指定的cpu上的工作者线程池来处理。highpri工作者线程池中的线程具有较高的nice level。注意,normal和highpri工作者线程池不会相互之间进行交互。每一个只是单独的维护去自己池中的工作者,并且对其进行并发管理工作。WQ_CPU_INTENSIVECPU密集型工作队列中的工作项并不会对并发级别产生贡献。换句话说,可以运行的CPU密集型工作项不会阻止相同工作者线程池上的工作项的运行。对于绑定的并且希望独占CPU周期的工作项,这个flag是有用的,因为它们的运行可以被系统进程调度程序调节。虽然CPU密集型的工作项不对并发级别做出贡献,但是它们的执行仍然会被并发管理所调节,因为可以运行的那些非CPU密集型工作项可以延迟CPU密集型工作项的运行。这个标记对于unbound工作队列没有什么意义。    注意,标记WQ_NON_REENTRANT不再存在了,因为当前所有的工作队列都是non-reentrant的 - 任何工作项从系统的角度来说,在任何时候最多只会由一个工作者运行。

@max_active:

       @max_active决定了分配到工作队列的工作项的每个CPU上的执行上下文的最大数量。例如,如果@max_active是16,那么同一个CPU上同一时刻最大只有16工作项可以工作。

       目前,对于bound工作队列,@max_active的最大限制值是512,并且默认的值为0的时候表示为256。 对于unbound工作队列,这两个的限制(maximum limit & default value)分别是512和4* num_possible_cpus()。之所以会选择一个这么高的值,是为了在系统在失控的时候提供保护的情况下,它们不会成为关键的限制因素。

       一个队列中的激活状态的工作项的数量一般来说是由队列的用户来进行调节的,更具体的说,是用户在同一时刻能够将多少工作项放入队列的数量是有用户自己决定的。除非一些特殊的要求需要限制工作项的数量,一般推荐默认的‘0’。

       一些用户需要依赖ST wq的严格的顺序执行的特点。那么将@max_active设置成1并且将flag设置成WQ_UNBOUND被用来实现这个行为。在这中wq上的工作项总是会被放入unbound的工作者线程池,并且在任意时刻都只有一个工作项被激活。这样就能够实现类似ST wq的相同顺序执行的特性。

一些执行的场景

       下面的一些执行场景用来说明cmwq在不同的配置下的行为。

       工作项w0,w1,w2被放入同一个CPU上的一个bound队列中。w0消耗了CPU 5ms的时间然后进入休眠10ms,然后再次消耗CPU 5ms。w1和w2消耗cpu 5ms然后进入休眠10ms。

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

TIME IN MSECS EVENT 0 w0 starts and burns CPU 5 w0 sleeps 15 w0 wakes up and burns CPU 20 w0 finishes 20 w1 starts and burns CPU 25 w1 sleeps 35 w1 wakes up and finishes 35 w2 starts and burns CPU 40 w2 sleeps 50 w2 wakes up and finishes

如果设置@max_active 大于等于3

TIME IN MSECS EVENT 0 w0 starts and burns CPU 5 w0 sleeps 5 w1 starts and burns CPU 10 w1 sleeps 10 w2 starts and burns CPU 15 w2 sleeps 15 w0 wakes up and burns CPU 20 w0 finishes 20 w1 wakes up and finishes 25 w2 wakes up and finishes

如果@max_active等于2

TIME IN MSECS EVENT 0 w0 starts and burns CPU 5 w0 sleeps 5 w1 starts and burns CPU 10 w1 sleeps 15 w0 wakes up and burns CPU 20 w0 finishes 20 w1 wakes up and finishes 20 w2 starts and burns CPU 25 w2 sleeps 35 w2 wakes up and finishes

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

TIME IN MSECS EVENT 0 w0 starts and burns CPU 5 w0 sleeps 5 w1 and w2 start and burn CPU 10 w1 sleeps 15 w2 sleeps 15 w0 wakes up and burns CPU 20 w0 finishes 20 w1 wakes up and finishes 25 w2 wakes up and finishes

指南

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

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

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

  • 工作队列可以看做下面几个方面的域:进程运行的保障,WQ_MEM_RECLEAIM,flush以及工作项的属性。当一个工作项不需要进行内存回收,不需要作为一组工作项中的一部分被flush,不需要特殊的属性的时候,就可以使用系统的wq。在执行特性上,使用一个特定的wq和使用一个系统wq并没有什么不同。

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

调试

因为工作项的执行函数是被通用的工作者线程执行,有一些小技巧可以对于队列用户的不当行为进行分辨。

工作者线程在进程列表中显示如下:

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]

If kworkers are going crazy (using too much cpu), there are two types of possible problems:
如果kworkers线程“疯了”(使用了太多的CPU),那么有两种可能的问题:

1. 有什么东东在不断的快速的被调度2. 其中一个工作项消耗了很多CPU周期时间

第一种可能可以使用如下的方法进行跟踪:

$ echo workqueue:workqueue_queue_work > /sys/kernel/debug/tracing/set_event$ cat /sys/kernel/debug/tracing/trace_pipe > out.txt(wait a few secs)^C

如果有什么在不断循环的忙于工作项的进出队列,那么这个动作的log会充斥着output的log,而引起此问题的罪魁祸首可以通过工作项函数被确认出来

对于第二类问题,通过跟踪检查该工作者线程的堆栈是有可能查出问题的。

$ cat /proc/THE_OFFENDING_KWORKER/stack

该工作项的函数应该在该stack trace中是可见的。

0 0