vlambda博客
学习文章列表

Linux工作队列workqueue源码分析(二)

前文我们讲了USB Hub使用工作队列的例子,一个模块通过创建队列,初始化任务,任务入队列三个接口就可以使用它。这篇文章我们分析一下工作队列接口的实现及相关对象之间的关系。
1、创建队列
工作队列(workqueue_struct)保存了相关的参数,列表,及与线程池之间的关系。通过alloc_workqueue宏创建,代码如下图所示:
Linux工作队列workqueue源码分析(二)
alloc_workqueue宏最终调用__alloc_workqueue_key()实现:
Linux工作队列workqueue源码分析(二)
这个函数的代码比较长,如果每一行都解析不知道如何下手,也没有必要,所以我只显示了骨干代码,其他的都折叠起来了,这样有助于我们理解主要流程。在创建workqueue_struct的的时候,主要实现分配空间及初始化、分配pool_workqueue对象并连接到线程池。
分配空间及初始化
3853-3879行,分配workqueue_struct对象空间,分配的时候指定GFP_KERNEL参数,说明内存不足的时候有可能会造成睡眠,所以不宜在中断上下文中使用。分配成功后,设置相关的属性,初始化pwq列表,mayday列表等,这些参数都是在任务调度的时候用到的,调度的细节我们另文分析。
分配pool_workqueue对象并连接到线程池
pool_workqueue对象是连接工作队列(workqueue_struct)与线程池(worker_pool)的对象。我们先说一下线程池,在Linux内核中,worker_pool主要分为两种,一种和CPU绑定,另一种不和CPU绑定。和CPU绑定中的线程只在绑定的CPU上运行,这对CPU亲缘性及局部热缓存有帮助;不和CPU绑定的线程管理比较灵活,可以在任何一个CPU中运行,对平衡CPU性能有帮助,但在切换线程上下文的时候会引起缓存失效。内核启动时候给每个CPU创建两个线程池,一个高优先级,一个低优先级,然后再创建两个非绑定的线程池。pool_workqueue负责把工作队列和线程池连接起来。
Linux工作队列workqueue源码分析(二)
alloc_and_link_pwqs()创建pool_workqueue对象并与线程池连接。
连接到绑定CPU的线程池
3783行,cpu_pwqs是__percpu类型的变量,这种类型的变量为每一个CPU都分配一个副本,CPU根据自己的索引计算偏移量取出自己的副本来使用,这样可以有效降低锁的使用,提高cache利用率。
3788-3791行,取出每个CPU副本对象指针。
3793-3797行,根据队列的优先级选择相应的线程池并绑定起来。

连接到非绑定线程池
非绑定线程池又分为有无__WQ_ORDERED标志,用于保障顺序调度,实现逻辑基本相同。
3800-3809行,非绑定线程池是通过队列属性来区分的,也就是说不同属性的队列会有相应的线程池进行调度。
2、任务初始化
任务(work_struct)对象在使用前要先初始化,代码如下所示:
Linux工作队列workqueue源码分析(二)
203-213行,如果定义了CONFIG_LOCKDEP,则定义多一些数据用于线程死锁检测。我们暂时不去分析死锁检测原理, 且CONFIG_LOCKDEP默认也是关闭的。
215-220行,INIT_WORK的具体实现,首先用一个do…while(0)把宏括起来,这是常见的宏定义语法,目的让宏代码更好的嵌入到使用场景中而不至于混淆展开后的代码。
217行,WORK_DATA_INIT宏把work的data成员设成枚举类型WORK_STRUCT_NO_POOL的值,表示当前还没有加入到任何线程池里。
218行,初始化entry链表,工作者线程通过此链表遍历任务列表。
219行,设置任务执行的函数,任务调度一次此函数执行一次,如果需要反复执行应在外层重复的提交任务,不能在函数内做无限循环处理,这样会堵塞线程调度影响其他任务执行。

3、任务入队列
我们先看一下入队列的接口
Linux工作队列workqueue源码分析(二)
queue_work是一个inline函数,内核中很多接口采用这种用法,在头文件中定义一个inline函数包装一下实际业务的函数,这样过度一下可以有效的降低代码的耦合度。
实际执行的函数是queue_work_on,代码如下:
Linux工作队列workqueue源码分析(二)
1455行,1462行,关闭/打开本地中断,防止work的data并发设置。
1457行,设置work->data的WORK_STRUCT_PENDING_BIT,表示任务已经在处理了,完成之前不能重复提交。
1458行,调用入队列函数。
__queue_work函数比较长,同样折叠了部分代码,如下:
1365-1368行,这一部分主要获得pool_workqueue对象,跟据work_struct的标志有没有指定WQ_UNBOUND获取相应的pwq指针。
1424-1431行,判断pwq中已经激活的线程数是否小于最大线程数,如果是则加入任务队列调度执行;否则,说明线程都在忙碌的工作中,应该把任务加到延迟工作队列中,之后再调度执行。
工作队列和每一个线程池都会连接在一起,但任务在一个时刻只会插入到一个pwq对象中去,这样在执行调度的时候可以更方便的管理。

以上就是工作队列三个接口的主要实现及对象间的关系,后续我们再另文分析线程池的管理及线程的调度。(armv8, kernel4.4)


相关文章