Concurrency Managed Workqueue
内容参考:
内核文档: linux-4.4.23/documentation/workqueue.txt
工作队列(workqueue, wq)通常适用于需要异步处理流程的场景。
当需要一个异步执行上下文时,只需定义一个work(指定了异步处理函数),并将其加入工作队列中,内核就会有一个独立的线程(worker)处理该需要异步执行的上下文。
worker线程会一个接一个的执行wq中每一个work对应的异步处理函数,当所有work都执行完了后,worker线程进入idle状态。当一个新的work加入wq后,worker线程又被重新唤醒继续执行。
在最初的wq实现中,一个multi threaded(MT)类型的wq需要在每一个CPU上创建一个worker线程,而single threaded(ST)类型的wq在系统内只创建一个worker线程。每一个MT工作队列都要在系统内创建与CPU核数相同数量的worker线程。随着内核中使用MT工作队列的增多以及CPU核数的持续增加,有些系统在刚启动后就达到了线程个数的上限。
虽然MT工作队列消耗了大量的资源,但是并没有取得惊人满意的并发效果。每一个wq维护其独立的worker线程池。一个MT工作队列在每个CPU核上只能提供一个可执行上下文,一个ST工作队列在整个系统中只能提供一个可执行上下文。这就要求work必须在有限的可执行上下文中完成处理,很容易导致在单可执行上下文中常见的死锁等问题。
关于并发效果:
对于ST wq,这种情况完全没有并发的概念,任何的work都是串行排队执行,如果正在执行的work很慢,那么队列中的其他work除了等待别无选择。
对于MT的wq,虽然创建了线程池,但是线程池的数目是固定的:每个online的cpu上运行一个,而且是严格的绑定关系。也就是说本来线程池是一个很好的概念,但是传统的wq上的线程池却分割了每个线程,线程之间不能互通有无。例如,cpu0上的worker线程由于处理work而进入阻塞状态,那么该worker线程处理的wq上其他的work都被阻塞住,不能转移到其他cpu的worker线程中去,更有甚者,cpu0上随后挂入的work也有同样的命运(在某个cpu上schedule的work一定会运行在那个cpu上),不能到其他空闲的worker线程上执行。
关于死锁:
假设某个驱动模块比较复杂,使用了两个work,分别为A 和 B,如果A依赖B的执行结果,那么,当这两个work都调度到一个worker线程上的时候就会出现问题,由于worker线程不能并发执行A和B,因此该驱动模块可能会死锁。MT的工作队列能减轻这个问题,但不能从根本上解决,毕竟work A和work B还是有可能被调度到一个CPU上执行。造成这些问题的根本原因是众多的work竞争一个执行上下文导致的。
这种消耗了大量资源,但是没有提供良好并发性能的问题,导致了很多使用者不得不做出一些不必要的权衡。如libata模块中使用了ST wq来轮询(poll) PIO,就会有不能同时轮询(poll)两个PIO的限制。由于MT没有提供良好的并发效果,需要支持高并发的使用者(如async和fscache)不得不实现自己的一套线程池。
cmwq是一套重构的wq机制,它的实现聚焦于以下目标:
与之前的wq API兼容
在不浪费大量资源的要求下,每个CPU使用统一的、对所有wq共享的worker线程池保证了灵活的并发性。
自动调节worker线程池和并发度,这样,API的使用者就不需要关注具体的细节。
为了减少函数的异步执行,对work进行了异步抽象。
work是一个简单的结构体(work_struct),它保存了一个待异步执行函数的指针。当驱动或子系统需要一个流程异步执行时,它必须创建一个work_struct,将异步调用函数指针保存其中,然后将work_struct加入wq中。
worker线程从wq中取出work并执行其中的异步处理函数。当wq中没有work后,worker线程进入idle状态。所有worker线程被集中管理,称为worker-pool。
cmwq设计明确区分了面向用户的前端工作队列接口和后端worker线程池的管理机制。
cmwq的worker线程分为两种:
与cpu绑定的线程池,这种线程池又分为普通优先级和高优先级两种。
未与cpu绑定的线程池
这些线程池的数量是动态变化的。
驱动和子系统可以通过API接口选择合适的方式创建work然后将其加入wq。用户可以通过创建wq并设置其flag来约束挂入该wq上work的处理方式。这些wq的flag包括cpu locality, 并发限制,优先级等。具体信息可以查看下面的alloc_workqueue的接口描述。
当一个work加入wq后,会根据wq的参数,属性和共享方式等将其交给系统中的某个work-pool进行处理。例如,除非进行了特殊设置,一个设置了绑定类型的wq中的work,将会交由当前cpu的普通或高优先级的work-pool进行处理。
对于所有worker线程池的实现,并发度的管理(多少个可执行上下文是active的)都是一个很重要的事情。cmwq尝试保持最小且足够用的并发度。本质上这是一个需要在并发性和系统资源消耗上进行平衡的问题。
每一个绑定到真实cpu上work-pool依赖调度器进行并发管理。当一个active的worker线程被唤醒或睡眠时,都会通知worker-pool,同时worker-pool会记录当前可运行(runnable)的worker的数量。通常情况下,work都不会设计为长时间占用cpu,这意味着维护能够预防work长时间处理且足够够用的并发是最佳选择。只要CPU上有一个或多个runnable的worker线程在运行,worker-pool就不会启动新的work执行动作,但是当最后一个running的worker线程睡眠后,worker-pool会立即调度一个新的worker线程,这样cpu就不会在有处于pending状态的work时进入idle状态。这样就会保持一直使用最小数量的worker线程但又不会丢失可执行线程带宽。
一直保持空闲的worker线程会消耗kthread的内存空间,当worker线程处于idle状态时,不会立即销毁它,而是保持一段时间,如果这是有新的work需要执行时,那么直接wakeup处于idle的worker线程即可。一段时间后仍然没有事情可做时,该worker线程会被销毁。
对于未绑定cpu的wq,线程池的数量的动态的。unbound的wq可以使用apply_workqueue_attrs()来指定属性。wq会自动创建匹配对应属性的work-pool。调节并发度的职责在使用者这边。对于bound的wq也可以通过设置flag来让内核忽略并发度的管理,详细信息参考API章节。
wq机制前端处理依赖于当需要更多的可执行上下文时能够及时创建worker线程。当不能及时创建worker线程时,就需要通过rescue worker线程来处理。在内存不足的情况下,所有可能释放内存的work都会被加入到rescue-worker线程中。另外,对于会解除worker-pool中死锁等待的work也会做同样的操作。
alloc_workqueue函数用于创建wq(workqueue_struct)。原来的crear_*workqueue函数已经被弃用并计划删除了。alloc_workqueue函数有三个参数:name,flags和max_active。
name参数表示wq的名字,如果wq用于rescuer,那么此参数还表示rescuer-worker线程的名字
max_active表示每个cpu上该wq在后台最多有多少个worker线程与之绑定。比如,如果max_active的值为16,那么表示该工作队列最多有16个work可以在同一个CPU上运行。
对于一个绑定的wq,max_active的最大值为512,如果该值被设置为0则使用默认值256。对于非绑定的wq,最大值为512和4*num_possible_cpus两个里面的较大的那个。
通常情况下,wq中的active的work的数量由wq的使用者控制的,更具体的说,是使用者同一时间放入wq中work的数量。除非有特殊的要求需要调整active worker的数量,否则建议将max_active设置为0。
有一些使用者需要使用ST的wq且需要依赖严格的执行顺序,此时,设置max_active=1且flags=WQ_UNBOUND就可以满足要求。
flag
WQ_FREEZABLE 这是一个和电源管理相关的标志。在系统Hibernation或者suspend的时候,有一个步骤就是冻结用户空间的进程以及部分(标注freezable的)内核线程(包括workqueue的worker thread)。标记WQ_FREEZABLE的workqueue需要参与到进程冻结的过程中,worker thread被冻结的时候,会处理完当前所有的work,一旦冻结完成,那么就不会启动新的work的执行,直到进程被解冻。
WQ_MEM_RECLAIM 所有用于内存回收的任务必须设置这个标志。这个标志保证了即是在内存紧张的情况下,也至少有一个可执行上下文。
WQ_HIGHPRI 挂入该workqueue的work是属于高优先级的work,需要高优先级(比较低的nice value)的worker thread来处理。 注意普通优先级和高优先级的worker-pool是相互独立的,它们都维护各自的线程池和并发度管理。对于unbound的wq,该标记无意义。
WQ_CPU_INTENSIVE CPU密集型队列中任务对并发度的提高是没有帮助的。所以,要确保可运行的CPU密集型工作任务不能阻止其他任务被运行。这个标记对于在绑定CPU的工作队列上,并且预期会占用较多CPU周期的任务来说是有用的,设置了该标记后,系统调度就能控制CPU密集型work的执行,保证其他work不会一直都得不到cpu。 该标记对于unbound的wq是无意义的。
下面用一个例子来说明在不同的配置条件下,cmwq的运行方式
现在有w0, w1 和w2 三个work,这三个work都加入到绑定cpu的wq q0中。w0执行时会占用5ms的cpu,然后睡眠10ms,然后再占用5ms的cpu。w1和w2执行时会先占用5ms的cpu然后睡眠10ms。
忽略其他所有任务 和 CPU 的情况下,假定采用简单的FIFO的调度方式。
在原来的wq机制下,简化后的执行顺序大概如下:
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 |
采用cmwq机制,且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 |
采用cmwq机制,且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 |
假设w0加入wq q0,w1和w2加入wq q1中,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 |
当处理一个涉及内存回收的work的,不要忘记使用WQ_MEM_RECLAIM标志。每一个设置了该标志的wq都有一个预留的可执行上下文。
除非严格依赖执行顺序,否则没有必要使用ST wq
除非有特殊的需求,创建wq时,建议max_active的值设置为0
当work不涉及内存回收、flushed或其他特殊属性时,可以使用系统定义的wq。系统定义的wq和自定义的wq在执行时没有区别。
除非work需要占用大量的CPU资源,否则建议使用bound的wq,bound的wq在并发度管理和cache缓冲方面有优势
worker 线程的呈现方式如下:
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]
当kworker占用大量cpu时,通常原因为一下两个:
某些任务被连续快速的调度
某个任务执行时需要消耗大量的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
第二个问题可以通过检查对应线程的栈信息确认
$ cat /proc/THE_OFFENDING_KWORKER/stack
module_platform_driver宏 定义在头文件 platform_device.h 中
/* module_platform_driver() - Helper macro for drivers that don't do
* anything special in module init/exit. This eliminates a lot of
* boilerplate. Each module may only use this macro once, and
* calling it replaces module_init() and module_exit()
*/
#define module_platform_driver(__platform_driver) \
module_driver(__platform_driver, platform_driver_register, \
platform_driver_unregister)
modlue_driver宏 定义在头文件 device.h 中
/**
* module_driver() - Helper macro for drivers that don't do anything
* special in module init/exit. This eliminates a lot of boilerplate.
* Each module may only use this macro once, and calling it replaces
* module_init() and module_exit().
*
* @__driver: driver name
* @__register: register function for this driver type
* @__unregister: unregister function for this driver type
* @...: Additional arguments to be passed to __register and __unregister.
*
* Use this macro to construct bus specific macros for registering
* drivers, and do not use it on its own.
*/
#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \
{ \
return __register(&(__driver) , ##__VA_ARGS__); \
} \
module_init(__driver##_init); \
static void __exit __driver##_exit(void) \
{ \
__unregister(&(__driver) , ##__VA_ARGS__); \
} \
module_exit(__driver##_exit);
因此 module_platform_driver(xxx);的展开后结果为:
static int __init xxx_init(void)
{
return platform_driver_register(&(xxx));
}
module_init(xxx_init);
static void __exit xxx_exit(void)
{
platform_driver_unregister(&(xxx));
}
module_exit(xxx_exit);
主机(Host)和sd卡之间的通信都是由主机Host(master)来控制的。主机发送的命令有两种:
Broadcast commands,广播命令。发送给所有挂在SD总线上的SD卡,有一些广播命令需要SD卡做出响应。
Addressed(point-to-point) commands,点对点寻址命令。寻址命令只发给具有相应地址的卡,并需要返回一个响应。
SD Memory Card system(host and cards)定义了两种操作模式:
Card identification mode:
主机(Host)在被重置(reset)或主机查找SD总线上的新卡时,处于卡识别模式。
卡(Card)在被重置(reset)后处于卡识别模式,直到接收到SEND_RCA(CMD3)命令。
Data transfer mode:
主机(Host)在识别完总线上的所有卡之后进入数据传输模式。
卡(Card)在第一次发布(publish)RCA后进入数据传输模式。
当处于该模式时,主机(Host)会重置所有处于卡识别模式的卡,确认操作电压范围,识别卡,请求卡发送(publish)相对卡地址(Relative Card Address, RCA)。这些操作都是在各自的CMD线上完成的。所有的通信都仅仅使用了CMD线。
During the card identification process, the card shall operate in the SD clock frequecy of the identification clock rate fod.
卡识别模式的状态转换图
在SD模式下,命令GO_IDLE_STATE(CMD0)是软复位命令,该命令会设置卡进入空闲状态(idle state)。处于非活动状态(inactive state)的卡接受到该命令时无响应。
所有的卡在被主机执行power-on之后都会进入 空闲状态(idle state),即使是以前处于非活动状态的卡。
空闲状态的SD卡的CMD线处于输入状态,等待主机下发下一个命令。SD卡的RCA默认初始化为0x0000。
在Host和card进行通信之前,Host可能不清楚card支持的电压范围。此时Host首先使用card可能支持的电压发送一条CMD0命令,紧接着发送一条CMD8命令获取SD卡支持的工作电压范围数据。
SEND_IF_COND(CMD8)命令通常用于确认SD卡的工作条件。SD卡通过解析命令的argument域中的数据(VHS)确认Host当前使用的操作条件(工作电压)的有效性。Host通过CMD8的响应来确认SD卡是否能够在所给的电压下工作。
当卡能够在Host给定的电压下工作时,卡会给Host发送响应回填CMD8命令中的argument域中的数据。
当卡不能在Host给定的电压下工作时,SD卡不会发送响应给主机,并保持处于idle状态。
SD_SEND_OP_COND(ACMD41)命令提供了一种机制来确认SD卡是否可以在Host给定的Vdd范围下工作。如果SD卡无法在给定的VDD范围内工作,则进入inactive state。需要注意的是,ACMD41命令是appliction-specific 命令,每次发送ACMD41命令之前都要先发送APP_CMD(CMD55)。在空闲状态CMD55命令使用默认的卡相对地址RCA=0x0000。
SD卡的初始化开始于接收到ACMD41命令之后。
如果ACMD41中的HCS(Host Capacity Support)域被设置为1,表示Host支持SDHC/SDXC卡,否则表示Host不支持。
如果主机发送的ACMD41命令中HCS被置位0,当SDHC或SDXC卡接受到该命令时,会一直返回busy。
ACMD41的响应为OCR寄存器的内容,其中的busy bit被SD卡用于通知Host,ACMD41命令是否处理完成,当busy bit被设置为0,表示sd卡仍在处理ACMD41命令,当busy bit被置位1,表示sd卡处理ACMD41命令完成。
主机Host会重复发送ACMD41指令,直到返回的busy bit为1 或 连续发送时间超过1秒为止。在此期间,Host不能发送除了CMD0之外的其他命令。
Host会对所有卡执行相同的流程,与Host不兼容的卡会进入inactive state.
随后Host会发送All_SEND_CID(CMD2)来获取各个卡的CID,SD卡在发送完CID后,进入识别状态(identificaiont state)。
Host发送SEND_RELATIVE_ADDR(CMD3)命令要求各个SD卡更新相对卡地址信息。RCA信息发送完后,SD卡进入stand-by state。
数据传输模式状态转换图
在SD卡结束识别模式之前,Host一直保持Fod的工作频率,在数据传输模式中,Host的工作频率会切换到Fpp。
Host发送SEND_CSD(CMD9)命令来获取SD卡的CSD寄存器信息,如块长度,卡容量信息等。
广播命令SET_DSR(CMD4)用于配置所有已识别卡的Driver Stages,它设置DSR寄存器中的bus layout(length),卡的数量和数据传输频率。时钟频率也在此时被转换为Fpp。SET_DSR命令对于Host和卡都是可选的。
CMD7命令用于选择一个卡,并将其设置为Transfer State。在任何时间,只能有一张卡处于传输状态。如果选择的卡当前已经处于传输状态时,再对其发送CMD7命令会将其转换到Stand-by 状态。
当CMD7以保留地址0x0000发送时,所有的卡都被设置为stand-by状态。这个功能可以别用于识别新卡同时不重置其他已经注册的卡。处于stand-by状态且已经有RCA地址的卡不都会响应识别命令(CMD2, CMD3 ACMD41)。
所有的数据读命令在任意时刻都可以被停止命令(CMD12)终止。 数据传输会终止,同时SD卡会返回Transfer State。 读命令有:块读操作(CMD17)、多块读操作(CMD18)、发送写保护(CMD30)、发送scr(ACMD51)以及读模式下的普通命令(CMD56)。
所有的数据写命令在任意时刻都可以被停止命令(CMD12)终止。 写命令应该在取消选择命令(CMD7)之前停止。 写命令有:块写操作(CMD24,CMD25)、编程命令(CMD27)、锁定/解锁命令(CMD42)以及写模式下的普通命令(CMD56)。
数据传输一旦完成,SD卡会退出数据写状态,进入Programming状态(传输成功)或者Transfer状态(传输失败)
如果块写操作被停止,但是写操作包含的最后一个块的长度和CRC校验是正确的话,数据会被编程到SD卡(从缓存写入到Flash)。
卡可能提供块写缓冲。 这意味着在前一块数据被操作时,下一块数据可以传送给卡。如果所有卡写缓冲已满, 只要卡在 Programming State, DAT0 将保持低电平(BUSY)。
写CSD、CID、写保护和擦除时没有缓冲。这表明当卡在处理这些命令时,不再接收其他数据传输命令。 在卡处于busy 且处于Programming State时,DAT0 保持低电平。 实际上如果SD卡的 CMD 和 DAT0 线分离,而且主机占有的忙 DAT0 线与其他卡的 DAT0 线没有连接时,主机可以访问其他卡。
在卡被编程(programming)时,不允许接收参数设置命令。参数设置命令包括:设置块长度(CMD16),擦除块开始(CMD32)和擦除块结束(CMD33)。
在卡被编程(programming)时,不允许接收读命令
使用 CMD7 指令把另一个卡从 Stand-by 状态转移到 Transfer 状态不会中止擦除和编程(programming)操作。卡将切换到 Disconnect 状态并释放 DAT 线。
使用 CMD7 指令可以选中处于 Disconnect 状态的卡。卡将进入 Programming 状态,重新激活忙指示。
使用 CMD0 或 CMD15 重置卡将中止所有挂起和活动的编程(programming)操作。这可能会破坏卡上的数据内容,需要主机保证避免这样的操作。
CMD34-37 CMD50,CMD57保留。
标准大小的SD卡的外形和接口
引脚说明
内部结构
Operating Conditions Register(OCR)
这个32bits的寄存器保存了VDD电压(非UNS-II模式)/VDD1电压(UNS-II模式)信息。除此之外,该寄存器还保存了一些状态信息位。
Bit31: Card power up status bit, 当sd卡的上电流程完成后,该标志位被设置
Bit30: Card Capacity status bit
该bit位在卡上电完成且bit31位被设置后才有效。主机(HOST)通过读该bit来确认sd卡是SDSC还是SDHC/SDXC。
Bit7: 该标志为是为Dual Voltage Card新定义的,默认值为0。 当Dual Voltage Card没有接收到CMD8时,该标志位为0. 当Dual Voltage Card接收到CMD8后,该标志被置位1.
当SD卡不支持某个电压范围时,对应的bit位被置位LOW。 当卡的状态为busy时,bit31被设置为LOW。
Card Identification Register(CID)
该寄存器中保存了卡认证阶段(identification phase)需要的ID信息,所有的读写卡都有一个唯一标示的ID号。
Card Specific Data Register(CSD)
该寄存器有两个版本,当CSD_STRUCTURE中的值为0表示版本1.0,对应标准容量的SD卡(SDSC)。 当CSD_STRUCTURE中的值为1时表示版本2.0,对应高容量和超高容量的SD卡(SDHC/SDXC)。
R = readable, W(1) = writable once, W = multiple writable
Relative Card Address(RCA)
在SD总线模式下,该寄存器中保存了卡在认证阶段(identification)发布的器件地址信息。该地址用于认证完成后的主机与卡之间的通信。它的默认值为0x0000。
在UHS-II模式下,该寄存器中保存的是Node ID。
Driver Stage Register(DSR)
该寄存器是可选的。
如果选择使用该寄存器,通常用于扩展总线的操作。 It can be optionally used to improve the bus performance for extended operating conditions(depending on parameters like bus length, transfer rate or number of cards).
CSD寄存器中有标志位保存DSR寄存器是否使用的信息。DSR寄存器的默认值为0x404
SD Card Configuration Register(SCR)
SCR保存了SD卡支持的一些特殊特性信息,该寄存器的内容由制造商在生产过程中设置。