linux 中断子系统 - 工作队列简介以及使用
c程序员
工作队列简介
工作队列(workqueue)是在 2.6 版本引用的下半部机制,相对于 tasklet 和软中断而言,工作队列有一个本质上的区别:它运行在内核线程上下文中。而tasklet 和软中断只能在中断上下文中运行(特殊情况下可能运行在内核线程上下文),不支持睡眠和调度,这对某些阻塞式和需要同步执行的任务是不友好的,比如磁盘的访问。
尽管工作队列可以运行在进程上下文,但是它并不和任何用户进程相关联,它也不能访问任何用户程序空间,因为工作队列是由内核线程执行的,它只会运行在内核中。
工作队列的使用
发展到4.x 甚至 5.x 的内核版本,工作队列的底层实现变得更加复杂(在后续的文章中我们会详细讨论),但是它的应用接口却是始终的简单:初始化和调度执行。
当然,这个调度执行并不是直接在中断中调用工作函数,而是以设置就绪状态的方式,告诉内核线程,某个工作已经准备好了,它需要在将来的某一时刻执行,这里所说的将来,通常是以 ms 为单位的时间,这个时间具体是多少,没有人知道。
(注:这里的设置就绪状态并不是设置某个标志位,实际所做的工作是将该工作加入到工作线程然后唤醒线程,设置就绪状态只是一个通用的说法)
但是有一点可以确定的是:通常情况下,一旦工作队列处于就绪状态,工作函数肯定是越快执行越好,这样系统也就拥有更好的响应性能,自 2.6 内核以来,工作队列的优化工作也是主要针对这一部分。
相关的实现细节我们就在本系列文章的后续章节讨论,接下来我们先来看看工作队列是如何使用的,毕竟这才是一个驱动工程师首要关心的问题。
相关结构体
在这里需要区分两个概念:工作队列和工作。
工作队列是内核提供的一套实现机制,同时也通常对应内核中的一个软件实体组合,因为现在的工作队列不是一个单纯的队列或链表实现的,为了讨论方便,或者是基于抽象的理念,在讨论应用层面时,我们就把它当成一个队列即可,这一个软件实体组合可以简单地认为由 struct workqueue_struct 描述。
而工作是实际需要被执行的用户工作,主体是一个工作函数。对于一个驱动工程师而言,需要做的所有事情就是创建工作,然后将其添加到工作队列中等待执行。
常用的工作分为两种:普通工作和延迟工作,延迟工作仅仅是添加了一个参数:延时时间,单位是 jiffies,工作队列会 在延时时间过后被设置为就绪状态。
普通工作结构体
普通工作的结构体由 struct work_struct 描述:
struct work_struct {
atomic_long_t data; //相关标志位
struct list_head entry; //链表节点
work_func_t func; //工作函数
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map; //死锁检测
#endif
};
结构成员非常简单:
- atomic_long_t data:看到回调函数和 data 的组合就很容易想到 data 是用户传入的私有数据,但是这里并不是这样实现的,由于工作队列运行在进程上下文,所以可以通过其它方式进行传参,这里的 data 是一组标志位的集合,通常不需要驱动开发者关心。
- struct list_head entry:工作队列将会以链表的形式管理所有加入的工作,该工作通过这个节点将工作链接到工作队列中。
- work_func_t func:回调函数,也就是该工作需要执行的工作,函数原型是:typedef void (work_func_t)(struct work_struct work);
- struct lockdep_map lockdep_map:lock_dep 是内核中的一个死锁检测模块,用于死锁的检测,这部分不予讨论。
延迟工作结构体
延迟工作的结构体由 struct delayed_work 描述:
struct delayed_work {
struct work_struct work;
struct timer_list timer;
/* target workqueue and CPU ->timer uses to queue ->work */
struct workqueue_struct *wq;
int cpu;
};
延迟工作的结构成员要复杂一些:
- struct work_struct work:普通工作结构体
- struct timer_list timer:内核定时器,用于计时,延时指定的时间就是由内核定时器实现的。
- struct workqueue_struct *wq:当前工作相关的工作队列。
- int cpu:工作队列机制的实现和 smp (对称多处理器,也可以理解为多核处理器,有多个 CPU 节点) 结构是强相关的,该参数表示当前工作与哪个 cpu 相关。
工作的初始化
内核提供两种类型的接口对工作队列进行初始化:
DECLARE_WORK(n, f)
INIT_WORK(_work, _func)
分别提供静态和动态的初始化,静态方法的第一个参数只需要提供一个工作名,比如:
DECLARE_WORK(my_work, work_callback)
系统就会创建一个 struct work_struct my_work 结构并对其成员。
动态初始化需要自定义一个 struct work_struct 类型结构,传入到 INIT_WORK() 中,传入的参数为指针,比如:
struct work_struct my_work;
INIT_WORK(&my_work,work_callback);
需要注意的是,上述代码如果出现在同一个函数中,这是一个错误实现,因为 my_work 的生命周期限于函数之内,尽管这在某些情况下可以得到正确的结果,但是永远不要这么做,这是编程者常犯的一个错误。相对于此,还有一个常犯的错误:
struct work_struct *my_work;
INIT_WORK(my_work,work_callback);
在还没有为指针分配内存空间时就使用它,通常编译器会提出警告,某些检查严格的编译器甚至会报错,但是不应该指望编译器来守护你的系统安全。
而对于延迟工作队列的初始化,也是提供两个接口:
DECLARE_DELAYED_WORK(n, f)
INIT_DELAYED_WORK(_work, _func)
不难猜到,延迟工作队列的初始化其实就是比普通工作队列多了一项:内核定时器的初始化
...
__setup_timer(&(_work)->timer, delayed_work_timer_fn, \
(unsigned long)(_work), \
(_tflags) | TIMER_IRQSAFE);
...
对于工作的初始化,比较简单,建议你也去看看源代码。
添加工作队列
将初始化完成的工作添加到工作队列中,工作就会在将来的某个时刻被调度执行,linux 内核已经封装了相应的接口:
schedule_work(work);
schedule_delayed_work(delayed_work,jiffies);
对于驱动开发者而言,工作的配置就已经完成了,work_struct->func 将会由系统在将来的某个时刻调度执行。
其中,schedule_work 的实现是这样的:
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}
该接口调用了 queue_work,将传入的 work 加入到系统预定义的工作队列 system_wq 中,当然,驱动开发人员也可以直接使用 queue_work() 接口,将当前工作队列添加到其他的工作队列。
而 schedule_delayed_work 的实现是这样的:
static inline bool schedule_delayed_work(struct delayed_work *dwork,
unsigned long delay)
{
//将 work 同样添加到 system_wq 中
return queue_delayed_work(system_wq, dwork, delay);
}
static inline bool queue_delayed_work(struct workqueue_struct *wq,
struct delayed_work *dwork,
unsigned long delay)
{
return queue_delayed_work_on(WORK_CPU_UNBOUND, wq, dwork, delay);
}
bool queue_delayed_work_on(int cpu, struct workqueue_struct *wq,
struct delayed_work *dwork, unsigned long delay)
{
struct work_struct *work = &dwork->work;
...
__queue_delayed_work(cpu, wq, dwork, delay);
...
}
return ret;
}
static void __queue_delayed_work(int cpu, struct workqueue_struct *wq,
struct delayed_work *dwork, unsigned long delay)
{
struct timer_list *timer = &dwork->timer;
struct work_struct *work = &dwork->work;
dwork->wq = wq;
dwork->cpu = cpu;
timer->expires = jiffies + delay;
...
add_timer(timer);
}
可以从源码中看到,经过 queue_delayed_work->queue_delayed_work_on->__queue_delayed_work() 函数之间的层层调用,最后在 __queue_delayed_work 中开启了定时器,而定时器的超时值就是用户传入的 delay 值,同样的,该接口默认地将当前工作队列添加到系统提供的 system_wq 中。
定时器一旦超时,将会进入到定时器回调函数中,这个回调函数在初始化的时候被设置:
void delayed_work_timer_fn(unsigned long __data)
{
struct delayed_work *dwork = (struct delayed_work *)__data;
__queue_work(dwork->cpu, dwork->wq, &dwork->work);
}
同样是调用了 __queue_work 接口,将 struct work_struct 类型的工作加入到指定工作队列中,至于 __queue_work 的具体实现,因为涉及到比较多的预备知识,我们将在后续的文章中讨论。
工作队列
在上文的工作添加操作中,直接使用系统的接口都会将该工作提交到默认的工作队列 system_wq 中,至于工作队列,内核中系统提供的工作队列分为以下几种:
- 独立于 cpu 的工作队列
- per cpu 的普通工作队列。
- per cpu 的高优先级工作队列
- 可冻结的工作队列
- 节能型的工作队列
除了系统提供的工作队列,用户还可以使用 alloc_workqueue 接口自行创建工作队列,对于这些工作队列的特性,同样将会在后续的文章中继续讨论。
等待执行完成
如果需要确定指定的 work 是否执行完成,可以使用内核提供的接口:
bool flush_work(struct work_struct *work)
这个接口在 work 未完成时会被阻塞直到 work 执行完成,返回 true,但是如果指定的 work 进入了 idle 状态,会返回 false。
需要注意的是:一个 work 在执行期间可能会被添加到多个工作队列中,flush_work 将会等待所有 work 执行完成。
针对延迟工作而言,内核接口使用 flush_delayed_work:
bool flush_delayed_work(struct delayed_work *dwork)
同时需要注意的是,在延迟工作对象上调用 flush 将会取消 delayed_work 的延时时间,也就是会将 delayed_work 立即添加到工作队列并调度执行。
取消工作
另一个类似的接口是:
bool cancel_work_sync(struct work_struct *work)
这个接口会取消指定的工作,如果该工作已经在运行,该函数将会阻塞直到它完成,对于其它添加到工作队列的工作,将会取消它们。
示例代码
以下是一个简单的 workqueue 示例代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/workqueue.h>
MODULE_LICENSE("GPL");
struct wq_priv{
struct work_struct work;
struct delayed_work dl_work;
};
static void work_func(struct work_struct *work){
printk("exec work queue!\n");
}
static void dl_work_func(struct work_struct *work){
printk("exec delayed work queue!\n");
}
static struct wq_priv priv;
static int __init workqueue_init(void)
{
printk("hello world!!!\n");
//初始化 workqueue
INIT_WORK(&priv.work,work_func);
INIT_DELAYED_WORK(&priv.dl_work,dl_work_func);
//调度 workqueue
if(0 == schedule_work(&priv.work)){
printk("Failed to run workqueue!\n");
}
if(0 == schedule_delayed_work(&priv.dl_work,3*HZ)){
printk("Failed to run workqueue!\n");
}
return 0;
}
static void __exit workqueue_exit(void)
{
//退出 workqueue
cancel_work_sync(&priv.work);
cancel_delayed_work_sync(&priv.dl_work);
}
module_init(workqueue_init);
module_exit(workqueue_exit);
将编译后的 .ko 文件加载到内核中,打印信息如下:
[17065.062558] exec work queue!
[17068.102369] exec delayed work queue!
可以看到,delayed workqueue 的执行时间在 workqueue 之后的 3s,因为设置的 delay 时间就是 3s。
参考
4.14 内核代码
linux中断子系统 - 工作队列原理概述
c程序员
在驱动工程师的印象中,工作队列就是将中断中不方便执行的任务延迟执行,将工作挂到内核某个队列上,在将来的某个阶段内核执行该工作。
最简单的实现就是:内核启动时创建并维护一个工作队列,该队列由内核线程实现,没有任务执行时就陷入睡眠。在用户调用 schedule_work 时,将 work 挂到该工作队列的链表或者队列中,唤醒该内核线程并执行该 work。在有必要的时候,用户也可以自己创建一个 workqueue 来使用。
如果涉及到 SMP 架构,事情可能就要复杂一点,考虑到 cpu 缓存的问题,如果多个 cpu 共用同一个队列,会导致过多的缓存失效从而带来效率上的问题。那么,就让每个 cpu 维护一个队列,cpu 之间的队列互不干涉,用户的工作被均匀分配给各个 cpu 的队列。
同时,采用优先级机制,将用户的工作区分优先级,同样的也创建不同的优先级队列,这样就可以照顾到一些实时性较高的工作。
上述这种实现就是传统的 workqueue 实现方式,出现在 2.6 之前,这种实现在通常情况下是可以接受的,但是也有一些缺陷所在:
- 每个 workqueue 上连接的 work 都是串行化执行的,如果某一个 work 需要长时间执行甚至睡眠,后续的 work 将被迫等待前面的 work 执行完成,由此还可能造成死锁问题。
- 无法限制用户创建 workqueue 的数量,从而无法控制内核线程的数量,内核线程过多将导致内核执行效率降低。
针对传统 workqueue 的限制,linux 在 2.6 版本的基础上对其进行了更新,引入了 CMWQ(Concurrency Managed Workqueue),即并发管理的工作队列。
传统 workqueue 出现的问题大部分都是由于工作队列的排队机制引起的,即在同一个工作队列中,后就绪的 work 需要等待前面的 work 执行完才能执行,要解决这个问题,很容易想到的一个方法,就是支持动态地将工作进行迁移,当前面的工作执行有延迟或者阻塞时,将后面的工作动态迁移到其他的工作队列上。但是这样也有一个问题,一个阻塞的工作将占用一个工作队列,系统中添加的工作负担比较重的时候,还是会出现堵塞的问题,而且,用户创建自定义的工作队列导致内核线程过多的问题也得不到解决。
另一个方法,也就是 linux 当前使用的方法:将工作队列和线程分离,每一个工作队列并不对应特定的内核线程,而是引入一个 worker_pool 的东西,worker 用来描述一个内核线程,顾名思义 worker_pool 就是一个 worker 池,这个 worker_pool 最大的特点是其伸缩性。
当一个 workqueue 中有多个 work 需要执行时,理论上它们在工作 workqueue 中是顺序执行的,当前面的 work 阻塞时,workqueue 可以检测到并且动态地开启一个新的线程来执行后续的 work,如果是非常耗时的计算工作,由用户设置相应的标志位,该 workqueue 也将会腾出专门一个内核线程来执行这个 work。
对于用户创建工作队列而言,因为工作队列的创建不再严格对应内核线程的创建(需要就创建,不需要就删除),所以也解决了这个内核线程过多的问题。
新的工作队列实现机制较传统的版本实现更为复杂,其源代码在 kernel/workqueue.c 中,目前(4.14)将近 6000 行代码,相比于传统的实现(800多行)多了数倍。
关键结构体
每一个子模块都会涉及到一个或几个结构体来描述整个模块,包括数据、操作,workqueue 自然也不例外,我们来看看 workqueue 中几个主要的结构体:
- struct workqueue_struct:主要用于描述一个工作队列,在创建工作队列时返回该结构。
- struct work_stuct : 描述需要执行的工作
- struct worker_pool:工作队列池
- struct worker:描述真正执行工作的内核线程
- struct pool_workqueue:起一个连接作用,负责关联 workqueue_struct 和 worker_pool。
当我第一次打开 workqueue 的源代码,看到上述几个结构体以及他们相互交织以实现 workqueue 的功能,也是非常头疼,但是既然源代码都在眼前,唯一要做的就是不断地深入分析,经过一段时间的死磕,对于 workqueue 这几个核心结构体,博主有了一些自己的总结,他们之间的对应关系是这样的:
- work_pool 存在多个,通常是对应 CPU 而存在的,系统默认会为每个 CPU 创建两个 worker_pool,一个普通的一个高优先级的 pool,这些 pool 是和对应 CPU 严格绑定的,同时还会创建两个 ubound 类型的 pool,也就是不与 CPU 绑定的 pool。
- worker 用于描述内核线程,由 worker pool 产生,一个 worker pool 可以产生多个 worker。
- workqueue_struct:属于上层对外的接口,一个 workqueue_struct 描述一个工作队列。如果把整个工作队列看成是一个黑盒子。
- pool_workqueue:属于 worker_pool 和 workqueue_struct 之间的中介,负责将 workqueue_struct 和 worker_pool 联系起来,一个 workqueue_struct 对应多个 pool_workqueue。
- work:由用户添加,可以是多个,一个 workqueue 对应多个 work
上图只是围绕一个 workqueue 的结构体联系,实际上内核中可以创建多个 workqueue 工作队列,每个工作队列都遵循这个结构。
上述的描述还是很抽象,我知道你还是没有搞清楚他们之间的关系,为了更方便地理解它们之间的关系,我们以一个简单的例子来讲解它们之间的联系:
workqueue_struct 相当于公司的销售人员,对外揽活儿,揽进来的活儿呢,就是 work,销售人员可以有多个,活也有很多,一个销售人员可以接多个活,而一个活只能由一个销售人员经手,所以 workqueue_struct 和 work 是一对多的关系。
销售人员揽进来的活自己是干不了的,那这个分给谁干呢?当然是技术部门来做,交给哪个技术部门呢?自然是销售目前所对接的技术部门,技术部门负责分配技术员来做这个活,如果一个技术员搞不定,就需要分配多个技术员。相对应的,这个技术部门就是 woker_pool,而这个技术员就是 woker,表示内核线程。
当开发者调用 schedule_work 将 work 添加到工作队列时,实际上是添加到了当前执行这段代码的 CPU 的 worker_pool->work_list 成员中(通常情况下),这时候 worker_pool 就派出 worker 开始干活了,如果发现一个 worker 干不过来,就会派出另一个 worker 进行协助,活干完了就释放 worker。
这个活干得好不好,进度怎么样,是否需要技术支持,这就需要一个项目管理来进行协调,向上连接销售人员,向下连接技术部门,这个项目管理起到一个联系作用,它就是 pool_workqueue。
传统的模式是:一个销售(workqueue_struct)固定绑定一个或多个技术员(worker),来了活儿技术员直接干,活儿太多也得排队等着,如果活儿太少,技术人员就自己在那儿玩,多招入一个销售,就得相对应地多分配出一个技术人员。
CMWQ 模式是:一个销售人员(workqueue_struct)并不固定对应哪个技术部门(worker_pool),也不对应哪个技术员(worker),有事做的时候才请求技术部门(worker_pool)分配技术人员(worker),没事做的时候就不需要,活儿多的时候就可以申请更多地技术人员(worker),这种动态申请的方式无疑是提高了资源的利用率。
结构体源码
下面我们来看看这几个核心数据结构中几个主要的结构体成员:
workqueue_struct
struct workqueue_struct {
struct list_head pwqs; //链表头,该链表中挂着所有与当前 workqueue_struct 相关的 pool_workqueue
struct list_head list; //链表节点,通过该链表节点将该 workqueue_struct 链接到全局链表(workqueues)中,内核通过该全局链表统一管理 workqueue_struct。
struct mutex mutex; //内核互斥锁,保护全局数据的操作
...
struct list_head maydays; //需要 rescue 执行的工作链表
struct worker *rescuer; // rescue 内核线程,这是一个独立的内核线程
struct workqueue_attrs *unbound_attrs; // ubound 类型 workqueue_struct 的属性,该属性定义了一个 workqueue 的 nice 值、numa 节点等,只针对 unbound 类型。
struct pool_workqueue *dfl_pwq; //同样只针对 unbound 类型,pool_workqueue。
#ifdef CONFIG_SYSFS
struct wq_device *wq_dev; //针对 sysfs 的属性,是否导出接口到 sysfs 中,通常是 /sys 目录
#endif
char name[WQ_NAME_LEN]; // 名称
unsigned int flags ____cacheline_aligned; //针对不同情况下的标志位,后续文章中将会讲到
struct pool_workqueue __percpu *cpu_pwqs; // 指向 percpu 类型的 pool_workqueue
struct pool_workqueue __rcu *numa_pwq_tbl[]; //指向 pernode 类型的 pool_workqueue
};
pool_workqueue
struct pool_workqueue {
struct worker_pool *pool; //当前 pool_workqueue 对应的 worker pool。
struct workqueue_struct *wq; //当前 pool_workqueue 对应的 workqueue_struct。
int refcnt; //引用计数,内核的回收机制
int nr_active; //正处于活动状态的 work 数量。
int max_active; //处于活动状态的 work 最大值
struct list_head delayed_works; // delayed_work 链表,delayed_work 因为有个延时,需要记录起来
struct list_head pwqs_node; //节点,用于将当前的 pool_workqueue 结构链接到 wq->pwqs 中。
struct list_head mayday_node; //当前节点会被链接到 wq->maydays 链表中。
struct work_struct unbound_release_work;
} __aligned(1 << WORK_STRUCT_FLAG_BITS);
worker_pool
struct worker_pool {
spinlock_t lock; //该 worker_pool 的自旋锁
int cpu; //该 worker_pool 对应的 cpu
int node; //对应的 numa node
int id; //当前的 pool ID.
unsigned int flags; //标志位
unsigned long watchdog_ts; //看门狗,主要用作超时检测
struct list_head worklist; //核心的链表结构,当 work 添加到队列中时,实际上就是添加到了当前的链表节点上。
int nr_workers; //当前 worker pool 上工作的数量
int nr_idle; //休眠状态下的数量
struct list_head idle_list; //链表,记录所有处于 idle 的 worker。
struct timer_list idle_timer; //内核定时器用于记录 idle worker 的超时
struct timer_list mayday_timer; // SOS timer
DECLARE_HASHTABLE(busy_hash, BUSY_WORKER_HASH_ORDER); //记录正运行的 worker
struct worker *manager; // 管理线程
struct list_head workers; // 当前 worker pool 上的 worker 线程。
struct completion *detach_completion; //管理所有 worker 的 detach
struct workqueue_attrs *attrs; // worker 的属性
int refcnt; //引用计数
} ____cacheline_aligned_in_smp;
worker
struct worker {
union {
struct list_head entry; // 如果当前 worker 处于 idle 状态,就使用这个节点链接到 worker_pool 的 idle 链表
struct hlist_node hentry; // 如果当前 worker 处于 busy 状态,就使用这个节点链接到 worker_pool 的 busy_hash 中。
};
struct work_struct *current_work; //当前需要执行的 worker
work_func_t current_func; //当前执行 worker 的函数
struct pool_workqueue *current_pwq; //当前 worker 对应的 pwq
struct list_head scheduled; //链表头节点,当准备或者运行一个 worker 的时候,将 work 连接到当前的链表中
struct task_struct *task; // 内核线程对应的 task_struct 结构
struct worker_pool *pool; // 该 worker 对应的 worker_pool
unsigned long last_active; //上一个活动 worker 时设置的 timestamp
unsigned int flags; //运行标志位
int id; //worker id
char desc[WORKER_DESC_LEN]; // work 的描述信息,主要是在 debug 时使用
struct workqueue_struct *rescue_wq; // 针对 rescue 使用的 workqueue。
};
在这一章节中,我们主要讲解了五个核心的结构体以及他们之间的关系,理清楚了这些关键的数据结构,我们再来一步步解析 workqueue 的实现流程。
参考
4.14 内核代码
linux 中断子系统 - 工作队列的初始化0
c程序员
workqueue 初始化的开始有两个函数:
- workqueue_init_early
- workqueue_init
workqueue_init_early 直接在 start_kernel 中被调用,而 workqueue_init 的调用路径为:
start_kernel
rest_init
kernel_init
kernel_init_freeable
workqueue_init
从函数名可以看出,workqueue_init_early 负责前期的初始化工作:
int __init workqueue_init_early(void)
{
int std_nice[NR_STD_WORKER_POOLS] = { 0, HIGHPRI_NICE_LEVEL };
int i, cpu;
WARN_ON(__alignof__(struct pool_workqueue) < __alignof__(long long));
BUG_ON(!alloc_cpumask_var(&wq_unbound_cpumask, GFP_KERNEL));
cpumask_copy(wq_unbound_cpumask, cpu_possible_mask);
pwq_cache = KMEM_CACHE(pool_workqueue, SLAB_PANIC);
for_each_possible_cpu(cpu) {
struct worker_pool *pool;
i = 0;
for_each_cpu_worker_pool(pool, cpu) {
BUG_ON(init_worker_pool(pool));
pool->cpu = cpu;
cpumask_copy(pool->attrs->cpumask, cpumask_of(cpu));
pool->attrs->nice = std_nice[i++];
pool->node = cpu_to_node(cpu);
mutex_lock(&wq_pool_mutex);
BUG_ON(worker_pool_assign_id(pool));
mutex_unlock(&wq_pool_mutex);
}
}
/* create default unbound and ordered wq attrs */
for (i = 0; i < NR_STD_WORKER_POOLS; i++) {
struct workqueue_attrs *attrs;
BUG_ON(!(attrs = alloc_workqueue_attrs(GFP_KERNEL)));
attrs->nice = std_nice[i];
unbound_std_wq_attrs[i] = attrs;
BUG_ON(!(attrs = alloc_workqueue_attrs(GFP_KERNEL)));
attrs->nice = std_nice[i];
attrs->no_numa = true;
ordered_wq_attrs[i] = attrs;
}
system_wq = alloc_workqueue("events", 0, 0);
system_highpri_wq = alloc_workqueue("events_highpri", WQ_HIGHPRI, 0);
system_long_wq = alloc_workqueue("events_long", 0, 0);
system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND,
WQ_UNBOUND_MAX_ACTIVE);
system_freezable_wq = alloc_workqueue("events_freezable",
WQ_FREEZABLE, 0);
system_power_efficient_wq = alloc_workqueue("events_power_efficient",
WQ_POWER_EFFICIENT, 0);
system_freezable_power_efficient_wq = alloc_workqueue("events_freezable_power_efficient",
WQ_FREEZABLE | WQ_POWER_EFFICIENT,
0);
BUG_ON(!system_wq || !system_highpri_wq || !system_long_wq ||
!system_unbound_wq || !system_freezable_wq ||
!system_power_efficient_wq ||
!system_freezable_power_efficient_wq);
return 0;
}
第一阶段的初始化分为以下几个主要部分:
- 创建 pool_workqueue 的高速缓存池
- 初始化 percpu 类型的 worker pool
- 创建各种类型的工作队列
创建高速缓存池
在内核中,通常使用 kmalloc 进行内存的分配,这种分配方式小巧灵活:需要用到的时候就向系统申请,不需要使用了就调用对应的 free 函数,系统将其回收,内核针对物理页面的管理使用 buddy 子系统,而针对小内存的管理使用 slub 分配器,slub 分配器采用创建缓存块的方式对页面进行管理,内核初始化阶段就创建了不同大小的缓存块以适应不同 size 的内存分配需求,而调用 kmalloc 时 slub 返回的就是满足要求的最接近分配大小的缓存块,不难看出,如果 kmalloc 需要申请的 size 没有刚好适配内核最初创建的缓存块大小,就会造成内存的浪费。
因此,对于频繁申请的同一类型数据,为了不造成这种浪费,需要为其创建刚好适配的 slub 缓存对象。
这种缓存的做法在内核中以 kmem_cache 的方式提供,主要提供以下接口:
struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t offset,
unsigned long flags, void (*ctor)(void *))
kmem_cache_create 用于创建一个指定 size 的缓存对象,这个 size 通过对应需要使用的数据结构的大小,这个缓存是一片保留内存,通常并不直接使用,而是在当前缓存的基础上调用 kmem_cache_alloc,返回内存指针,再对该指针进行读写操作。
void *kmem_cache_alloc(struct kmem_cache *, gfp_t flags)
在当前初始化函数中,使用 KMEM_CACHE 接口创建缓存结构,这个接口是基于 kmem_cache_create 的封装:
#define KMEM_CACHE(__struct, __flags) kmem_cache_create(#__struct,\
sizeof(struct __struct), __alignof__(struct __struct),\
(__flags), NULL)
初始化 percpu 类型的 worker pool
在当前文件的头部,静态地创建了 percpu 类型的 worker pool 结构:
static DEFINE_PER_CPU_SHARED_ALIGNED(struct worker_pool [NR_STD_WORKER_POOLS], cpu_worker_pools);
其中,NR_STD_WORKER_POOLS 的值默认为 2,也就是系统初始化时每个 cpu 定义两个静态的 worker pool,上文中贴出的初始化的代码简化出来就是:
for_each_possible_cpu{ //遍历 cpu 操作
for_each_cpu_worker_pool{ //遍历 worker pool 操作
init_worker_pool(pool); //初始化 worker pool
pool->cpu = cpu; //绑定 worker pool 和 cpu
cpumask_copy(pool->attrs->cpumask, cpumask_of(cpu)); //获取 cpu mask
pool->node = cpu_to_node(cpu); //获取 numa node 节点
worker_pool_assign_id(pool); //为当前 worker pool 分配 id 号
}
}
其中,前面的两个 for_each 嵌套表示:以下的操作针对每个 cpu 下的每个 worker_pool。
init_worker_pool 负责初始化 worker_pool:
static int init_worker_pool(struct worker_pool *pool)
{
pool->id = -1;
pool->cpu = -1;
pool->node = NUMA_NO_NODE;
pool->flags |= POOL_DISASSOCIATED;
pool->watchdog_ts = jiffies;
INIT_LIST_HEAD(&pool->worklist);
INIT_LIST_HEAD(&pool->idle_list);
hash_init(pool->busy_hash);
setup_deferrable_timer(&pool->idle_timer, idle_worker_timeout,
(unsigned long)pool);
setup_timer(&pool->mayday_timer, pool_mayday_timeout,
(unsigned long)pool);
INIT_LIST_HEAD(&pool->workers);
ida_init(&pool->worker_ida);
INIT_HLIST_NODE(&pool->hash_node);
pool->refcnt = 1;
pool->attrs = alloc_workqueue_attrs(GFP_KERNEL);
}
按照阅读内核代码的经验来说,init 操作通常就是对结构体中的成员进行初始化,init_worker_pool 也不例外,其中值得注意的有几个成员:
- pool->node: NUMA node,这涉及到 numa 架构的概念,numa 存在于 SMP 系统中,通俗地说就是:
在 SMP 系统中,多个 cpu 核同时访问系统中共用的内存会出现总线争用和高速缓存效率的问题,所以将整片内存分开连接到不同的 cpu,每个 cpu 核尽可能使用 local ram,不同的 local ram 之间也可以交换数据,这种方式成为了一种解决方案,应用这种方案的系统就称为 numa 系统,这里的 cpu 核与内存组成了一个 numa node。当然,SMP 结构中也可以不使用 noma 架构。 - pool->attrs = alloc_workqueue_attrs(GFP_KERNEL):初始化 pool atrrs(属性),该属性包含三个部分:
- nice:优先级
- cpumask:cpu 掩码
- no_numa:标记是否处于 numa 架构。
初始化完成之后,就是对pool->cpu、pool->node 、cpumask 等成员进行赋值,同时 调用 worker_pool_assign_id 分配 worker_pool 的 id,该 id 由 idr 数据结构进行管理。
创建各种类型的工作队列
初始化完成 worker_pool 之后,内核通过传入不同的 flag 多次调用 alloc_workqueue 创建了大量的工作队列:
system_wq = alloc_workqueue("events", 0, 0);
system_highpri_wq = alloc_workqueue("events_highpri", WQ_HIGHPRI, 0);
system_long_wq = alloc_workqueue("events_long", 0, 0);
system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND,WQ_UNBOUND_MAX_ACTIVE);
system_freezable_wq = alloc_workqueue("events_freezable",WQ_FREEZABLE, 0);
system_power_efficient_wq = alloc_workqueue("events_power_efficient",WQ_POWER_EFFICIENT, 0);
system_freezable_power_efficient_wq = alloc_workqueue("events_freezable_power_efficient",WQ_FREEZABLE | WQ_POWER_EFFICIENT,0);
不同的 flag 代表工作队列不同的属性,比如 : 0 表示普通的工作队列,system_wq 就是普通类型,不知道你还记不记得,开发者调用 schedule_work(work) 时,默认将 work 加入到当前工作队列中。
- WQ_HIGHPRI 表示高优先级的工作队列
- WQ_UNBOUND 表示不和 cpu 绑定的工作队列
- WQ_FREEZABLE 表示可在挂起的时候,该工作队列也会被冻结
- WQ_POWER_EFFICIENT 表示节能型的工作队列
- WQ_MEM_RECLAIM 表示可在内存回收时调用
- WQ_CPU_INTENSIVE 表示 cpu 消耗型
- WQ_SYSFS 表示将当前工作队列导出到 sysfs 中
如果你的 work 有特殊的需求,除了调用 schedule_work(),同样可以调用 queue_work() 以指定将 work 加入到上述的某个工作队列中。
alloc_workqueue 实现
紧接着,我们来看看 alloc_workqueue 的源码实现
#define alloc_workqueue(fmt, flags, max_active, args...) \
__alloc_workqueue_key((fmt), (flags), (max_active), \
NULL, NULL, ##args)
alloc_workqueue 调用了 __alloc_workqueue_key:
struct workqueue_struct *__alloc_workqueue_key(const char *fmt,unsigned int flags,
int max_active,struct lock_class_key *key,const char *lock_name, ...)
{
struct workqueue_struct *wq;
struct pool_workqueue *pwq;
...
//申请一个 workqueue_struct 结构。
wq = kzalloc(sizeof(*wq) + tbl_size, GFP_KERNEL);
//如果是 unbound 类型的,就申请默认的 attrs,如果不是,就会使用 percpu worker_pool 的 attrs???。
if (flags & WQ_UNBOUND) {
wq->unbound_attrs = alloc_workqueue_attrs(GFP_KERNEL);
}
//初始化 wq 的成员
wq->flags = flags;
wq->saved_max_active = max_active;
mutex_init(&wq->mutex);
atomic_set(&wq->nr_pwqs_to_flush, 0);
INIT_LIST_HEAD(&wq->pwqs);
INIT_LIST_HEAD(&wq->flusher_queue);
INIT_LIST_HEAD(&wq->flusher_overflow);
INIT_LIST_HEAD(&wq->maydays);
INIT_LIST_HEAD(&wq->list);
//申请并绑定 pwq 和 wq
alloc_and_link_pwqs(wq);
//如果设置了 WQ_MEM_RECLAIM 标志位,需要创建一个 rescue 线程。
if (flags & WQ_MEM_RECLAIM) {
struct worker *rescuer;
rescuer = alloc_worker(NUMA_NO_NODE);
if (!rescuer)
goto err_destroy;
rescuer->rescue_wq = wq;
rescuer->task = kthread_create(rescuer_thread, rescuer, "%s",
wq->name);
if (IS_ERR(rescuer->task)) {
kfree(rescuer);
goto err_destroy;
}
wq->rescuer = rescuer;
kthread_bind_mask(rescuer->task, cpu_possible_mask);
wake_up_process(rescuer->task);
}
//如果设置了 WQ_SYSFS,将当前 wq 导出到 sysfs 中。
if ((wq->flags & WQ_SYSFS) && workqueue_sysfs_register(wq))
goto err_destroy;
//将当前 wq 添加到全局 wq 链表 workqueues 中。
list_add_tail_rcu(&wq->list, &workqueues);
}
从以上源码中可以看到,alloc_workqueue 主要包括几个部分:
- unbound 类型的 wq 需要做的一些额外设置,因为是 unbound 的类型,所以不能使用特定 cpu 的参数。
- wq 主要成员的初始化
- 申请 pwq 并将其和 wq 绑定。
- 如果设置了 WQ_MEM_RECLAIM 标志,需要多创建一个 rescuer 线程。
- 如果设置了 WQ_SYSFS 标志,导出到 sysfs
- 将 wq 链接到全局链表中。
其中需要讲解的有两部分:
- rescue 线程
- 申请 pwq 并绑定。
rescue 线程
在上一章结构体的介绍中,经常可以看到 mayday 和 rescue 的身影,从字面意思来看,这是用来"救命"的东西,至于救谁的命呢?
结合 WQ_MEM_RECLAIM 标志以及它的注释可以了解到,当 linux 进行内存回收时,可能导致工作队列运行得没那么顺利,这时候需要有一个线程来保证 work 的持续运行,但是并不建议开发者无理由地添加这个 flag,因为这会实实在在地创建一个内核线程,正如前面的章节所说,过多的内核线程将会影响系统的执行效率。
但是实际上内核中大部分驱动程序的实现都使用了这个标志位。
申请 pwq 并绑定
这一部分由函数 : alloc_and_link_pwqs 完成,
static int alloc_and_link_pwqs(struct workqueue_struct *wq)
{
bool highpri = wq->flags & WQ_HIGHPRI;
int cpu, ret;
if (!(wq->flags & WQ_UNBOUND)) {
wq->cpu_pwqs = alloc_pe
rcpu(struct pool_workqueue);
if (!wq->cpu_pwqs)
return -ENOMEM;
for_each_possible_cpu(cpu) {
struct pool_workqueue *pwq =
per_cpu_ptr(wq->cpu_pwqs, cpu);
struct worker_pool *cpu_pools =
per_cpu(cpu_worker_pools, cpu);
init_pwq(pwq, wq, &cpu_pools[highpri]);
mutex_lock(&wq->mutex);
link_pwq(pwq);
mutex_unlock(&wq->mutex);
}
return 0;
} else if (wq->flags & __WQ_ORDERED) {
ret = apply_workqueue_attrs(wq, ordered_wq_attrs[highpri]);
WARN(!ret && (wq->pwqs.next != &wq->dfl_pwq->pwqs_node ||
wq->pwqs.prev != &wq->dfl_pwq->pwqs_node),
"ordering guarantee broken for workqueue %s\n", wq->name);
return ret;
} else {
return apply_workqueue_attrs(wq, unbound_std_wq_attrs[highpri]);
}
}
该函数主要包括两个部分:
- 针对非 unbound 类型的 wq,对每个 CPU 申请一个 pwq 结构,如果设置了 WQ_HIGHPRI(高优先级) 标志位,则将其与 percpu 的高优先级 worker_pool 进行绑定,否则与普通的 worker_pool 进行绑定,worker_pool 的创建在上文中有相应介绍。
初始化和绑定的过程也比较简单,主要是以下几个部分: 设置 pwq->pool 为当前 CPU 的 worker_pool,设置 pwq->wq 为当前被创建的 work_queue_struct. 初始化各种链表头、引用计数置为 1 * 将 pwq->pwqs_node 链接到 wq->pwqs 中,建立索引关系。 - 针对 unbound 类型的 wq,只需要设置属性即可。
小结
第一阶段的初始化主要的工作是:
- 为每个 CPU 创建两个工作队列,同时创建两个 unbound 类型的工作队列。
- 内核启动时默认创建多个对应不同属性的工作队列,在创建工作队列的过程中,同时会创建 percpu 类型的 pool_workqueue,主要工作就是将工作队列与 percpu 的 worker_pool 进行连接。
需要注意区分的是:worker_pool 和 pwq 是 percpu 类型的,而 workqueue_struct 并非 percpu 类型的。
从目前的源码实现来看,CPU 数量为 n 时,创建一个工作队列的同时会创建 n 个 pwq ,n 个 pwq 会将 n 个 worker_pool 连接到这一个工作队列中,如果创建的是高优先级的工作队列,就会连接高优先级的 worker_pool,反之亦然。
参考
4.14 内核代码
linux 中断子系统 - 工作队列的初始化1
c程序员
通过上一章:工作队列初始化0的讨论,我们了解了工作队列初始化的第一阶段创建了 worker_pool 和系统默认的 workqueue,在初始化的第二阶段做了哪些事呢?
我们继续来看另一个初始化函数:workqueue_init(),我们先看一下这个函数的总体实现,然后再进行局部分析:
int __init workqueue_init(void)
{
struct workqueue_struct *wq;
struct worker_pool *pool;
int cpu, bkt;
// NUMA 节点的初始化
wq_numa_init();
//设置 NUMA node
for_each_possible_cpu(cpu) {
for_each_cpu_worker_pool(pool, cpu) {
pool->node = cpu_to_node(cpu);
}
}
list_for_each_entry(wq, &workqueues, list)
wq_update_unbound_numa(wq, smp_processor_id(), true);
//对每个 CPU 的 worker_pool ,创建 worker 线程
for_each_online_cpu(cpu) {
for_each_cpu_worker_pool(pool, cpu) {
pool->flags &= ~POOL_DISASSOCIATED;
BUG_ON(!create_worker(pool));
}
}
//创建 unbound 线程
hash_for_each(unbound_pool_hash, bkt, pool, hash_node)
BUG_ON(!create_worker(pool));
...
return 0;
}
第二阶段的初始化分为以下的几个主要部分:
- numa 的初始化
- 为每个 cpu 和 unbound 类型的 worker_pool ,创建默认的 worker 线程
numa 的初始化
在初始化的第一阶段中,就涉及到了 NUMA 节点的处理,事实上,在初始化的第一阶段就做这件事是比较简单且合适的,但是对于某些架构而言,比如 power 和 arm64,NUMA 节点映射在初始化的第一阶段之后,所以在第二阶段依旧需要对 numa 节点进行初始化以及更新,主要是针对每个 worker_pool 和 unbound 类型的 wq。
numa 架构相关的不过多讨论。
为 worker_pool 创建 worker 线程
通过系统提供的接口:create_worker(pool) 为每一个 worker_pool 构建 worker 并创建内核线程,传入的参数为 worker_pool.
接下来自然要看看 create_worker 的实现:
static struct worker *create_worker(struct worker_pool *pool)
{
struct worker *worker = NULL;
int id = -1;
char id_buf[16];
// id 用作确定内核线程的名称
id = ida_simple_get(&pool->worker_ida, 0, 0, GFP_KERNEL);
//申请一个 worker 结构
worker = alloc_worker(pool->node);
//
worker->pool = pool;
worker->id = id;
//确定内核线程的名称
if (pool->cpu >= 0)
snprintf(id_buf, sizeof(id_buf), "%d:%d%s", pool->cpu, id,
pool->attrs->nice < 0 ? "H" : "");
else
snprintf(id_buf, sizeof(id_buf), "u%d:%d", pool->id, id);
//创建内核线程
worker->task = kthread_create_on_node(worker_thread, worker, pool->node,
"kworker/%s", id_buf);
set_user_nice(worker->task, pool->attrs->nice);
kthread_bind_mask(worker->task, pool->attrs->cpumask);
worker_attach_to_pool(worker, pool);
worker->pool->nr_workers++;
worker_enter_idle(worker);
//开始执行内核线程
wake_up_process(worker->task);
return worker;
}
create_worker 分成以下的几个步骤:
- 申请一个 worker 结构
- 确定内核线程名称
- 创建内核线程
申请 worker 结构
worker 结构体用于描述 workqueue 相关的内核线程,需要使用时要向系统申请并初始化一个 worker 结构,该操作由 alloc_worker 这个接口完成,参数 node 表示 NUMA node,如果是在 NUMA 系统上,则会在 pool->node 节点上进行申请,然后进行一些成员的初始化。
确定内核线程名称并绑定
在 worker_pool 的创建时,使用 worker_pool_assign_id 接口为每个 worker_pool 分配了唯一 id 号,这个 id 号在这里被用于内核线程名称的确定。
在使用 linux 系统时,当我们使用 ps 命令进程,经常可以看到类似下面的这种条目:
root 5715 2 0 Mar12 ? 00:01:28 [kworker/2:2]
...
进程的名称为 kworker/%d:%d ,通常会有多个,这些就是内核默认的工作队列所产生的线程,而这些线程的就是在通过上述 create_worker 接口创建的。
我们可以在系统终端使用下面的指令来指定查看系统中正在运行的工作线程:
ps -ef | grep "kworker*"
系统的输出通常是这样的:
...
root 9099 2 0 Mar15 ? 00:01:00 [kworker/1:2]
root 9679 2 0 Feb24 ? 00:02:27 [kworker/0:1]
root 21980 2 0 08:20 ? 00:00:00 [kworker/0:0H]
root 22033 2 0 08:27 ? 00:00:00 [kworker/1:0H]
root 22127 2 0 08:44 ? 00:00:02 [kworker/u8:1]
...
其中:
"/" 后的第一个字节表示 cpu id,而 u 则代表 unbound 类型的 worker,":"后的数字则表示该线程对应的 worker_pool 的 id,后缀为 "H" 表示为高优先级 worker_pool 的 worker。
创建内核线程并绑定
创建内核线程和绑定主要是下面这两个接口:
worker->task = kthread_create_on_node(worker_thread, worker, pool->node,"kworker/%s", id_buf);
worker_attach_to_pool(worker, pool);
使用 kthread_create_on_node 接口创建内核线程以兼容 NUMA 系统,将返回的 task_struct 赋值给 worker,由 worker 对线程进行管理。worker_attach_to_pool 接口会将 worker 绑定到 worker_pool 上,其实就是执行下面这一段代码:
list_add_tail(&worker->node, &pool->workers);
通过 worker->node 节点链接到 worker_pool 的 workers 链表上。
当前 thread 的 thread_func 为 worker_thread ,参数为当前 worker,这是内核线程的执行主体部分,同样的,我们来看看它的源码实现,看看工作线程到底做了哪些事:
static int worker_thread(void *__worker)
{
struct worker *worker = __worker;
struct worker_pool *pool = worker->pool;
worker->task->flags |= PF_WQ_WORKER;
woke_up:
spin_lock_irq(&pool->lock);
//在必要的时候删除 worker,退出当前线程。
if (unlikely(worker->flags & WORKER_DIE)) {
spin_unlock_irq(&pool->lock);
WARN_ON_ONCE(!list_empty(&worker->entry));
worker->task->flags &= ~PF_WQ_WORKER;
set_task_comm(worker->task, "kworker/dying");
ida_simple_remove(&pool->worker_ida, worker->id);
worker_detach_from_pool(worker, pool);
kfree(worker);
return 0;
}
worker_leave_idle(worker);
recheck:
//管理 worker 线程
if (!need_more_worker(pool))
goto sleep;
if (unlikely(!may_start_working(pool)) && manage_workers(worker))
goto recheck;
//执行 work
do {
struct work_struct *work =
list_first_entry(&pool->worklist,
struct work_struct, entry);
pool->watchdog_ts = jiffies;
if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
process_one_work(worker, work);
if (unlikely(!list_empty(&worker->scheduled)))
process_scheduled_works(worker);
} else {
move_linked_works(work, &worker->scheduled, NULL);
process_scheduled_works(worker);
}
} while (keep_working(pool));
worker_set_flags(worker, WORKER_PREP);
sleep:
//处理完成,陷入睡眠
worker_enter_idle(worker);
__set_current_state(TASK_IDLE);
spin_unlock_irq(&pool->lock);
schedule();
goto woke_up;
}
整个 worker_thread 函数实现比较多,这里同样只是截取了主体部分,该函数主要包括以下的几个主要部分:
- 管理 worker
- 执行 work
管理 worker
从 worker 中获取与其绑定的 worker_pool,在 worker_pool 的基础上对运行在其上的 worker 进行管理,主要分为两个部分:检查删除和检查创建。
检查删除主要是检查 worker->flags & WORKER_DIE 标志位是否被置位,如果该位被置位,将释放当前的 worker ,当前线程返回,回收相应的资源。这个标志位是通过 destroy_worker 函数置位,删除的逻辑就是当前 worker_pool 中有冗余的 worker,长时间进入 idle 状态没有被使用。
检查创建主要由 manage_workers 实现,当 worker_pool 的负载增高时,需要动态地新创建 worker 来执行 work,每个 pool 至少保证有一个 idle worker 以响应即将到来的 work。
worker 的创建和删除完全取决于需要执行 work 数量,而这也是整个 work queue 新框架的核心部分。
执行 work
worker 用于管理线程,而线程自然是用于执行具体的工作,从源码中不难看到,当前 worker 的线程被唤醒后将会从第一个 worker_pool->worklist 中的链表元素,也就是挂入当前 worker_pool 的 work 开始,取出其中的 work,然后执行,执行的接口使用:
move_linked_works(work, &worker->scheduled, NULL);
process_scheduled_works(worker);
move_linked_works 将会在执行前将 work 添加到 worker->scheduled 链表中,该接口和 list_add_tail 不同的是,这个接口会先删除链表中存在的节点并重新添加,保证不会重复添加,且始终添加到最后一个节点。
然后调用 process_scheduled_works 函数正式执行 work,该函数会遍历 worker->scheduled 链表,执行每一个 work,执行之前会做一些必要的检查,比如在同一个 cpu 上,一个 worker 不能在多个 worker 线程中被并发执行(这里的并发执行指的是同时加入到 schedule 链表),是否需要唤醒其它的 worker 来协助执行(碰到 cpu 消耗型的 work 需要这么做),执行 work 的方式就是调用 work->func,。
当执行完 worker_pool->worklist 中所有的 work 之后,当前线程就会陷入睡眠。
小结
第二阶段的初始化主要是为每个 worker_pool 创建 worker,并创建对应的内核线程,这些内核线程负责处理 work。
参考
4.14 内核代码
linux 中断子系统 - 工作队列 schedule_work 的实现
c程序员
了解了 workqueue 的初始化以及实现原理,再来看 schedule_work 的实现原理,事情就会变得轻松很多,经过前面章节的铺垫,不难猜到:不管是 schedule_work 还是 schedule_delayed_work,都是将 work 添加到 worker_pool->worklist 中,然后由 worker 对应的内核线程执行,但是还有两个问题需要给出答案:
- 如何判断将当前的 work 添加到哪个 worker_pool 中?
- 这个添加的过程中,pwq 和 workqueue_struct、worker_pool 是如何进行交互的?
带着这两个疑问,我们来看看它的源码实现。
schedule_work
在 workqueue 的使用篇就讲到,schedule_work 会将 work 添加到默认的工作队列也就是 system_wq 中,如果需要添加到指定的工作队列,可以调用 queue_work(wq,work) 接口,第一个参数就是指定的 workqueue_struct 结构。
schedule_work 其实就是基于 queue_work 的一层封装:
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}
接着查看 queue_work 的实现:
static inline bool queue_work(struct workqueue_struct *wq,
struct work_struct *work)
{
return queue_work_on(WORK_CPU_UNBOUND, wq, work);
}
继续调用 queue_work_on ,增加一个参数 WORK_CPU_UNBOUND,这个参数并不是指将当前 work 绑定到 unbound 类型的 worker_pool 中,只是说明调用者并不指定将当前 work 绑定到哪个 cpu 上,由系统来分配 cpu.当然,调用者也可以直接使用 queue_work_on 接口,通过第一个参数来指定当前 work 绑定的 cpu。
接着看 queue_work_on 的源码实现:
bool queue_work_on(int cpu, struct workqueue_struct *wq,
struct work_struct *work)
{
...
if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
__queue_work(cpu, wq, work);
ret = true;
}
...
return ret;
}
如果没有为当前的 work 设置 WORK_STRUCT_PENDING_BIT 标志位,当当前 work 已经被添加到某个工作队列时,该标志位被置位,与 tasklet 和 softirq 的区别在于,softirq 支持多 cpu 上的并发执行,所以要求执行函数可重入,而 tasklet 不允许多 cpu 上的并发执行,编程相对简单。workqueue 机制中,不允许同一个 work 同时被加入到一个或多个工作队列中,只有当 work 正在执行或者已经执行完成,才能重新添加该 work 到工作队列中,所以也不存在多 cpu 并发执行的问题。
继续调用 __queue_work 函数:
static void __queue_work(int cpu, struct workqueue_struct *wq,
struct work_struct *work)
{
...
//获取 cpu 相关参数
if (req_cpu == WORK_CPU_UNBOUND)
cpu = wq_select_unbound_cpu(raw_smp_processor_id());
...
//检查当前的 work 是不是在这之前被添加到其他 worker_pool 中,如果是,就让它继续在原本的 worker_pool 上运行
last_pool = get_work_pool(work);
if (last_pool && last_pool != pwq->pool) {
struct worker *worker;
spin_lock(&last_pool->lock);
worker = find_worker_executing_work(last_pool, work);
if (worker && worker->current_pwq->wq == wq) {
pwq = worker->current_pwq;
} else {
/* meh... not running there, queue here */
spin_unlock(&last_pool->lock);
spin_lock(&pwq->pool->lock);
}
} else {
spin_lock(&pwq->pool->lock);
}
//如果超过 pwq 支持的最大的 work 数量,将work添加到 pwq->delayed_works 中,否则就添加到 pwq->pool->worklist 中。
if (likely(pwq->nr_active < pwq->max_active)) {
trace_workqueue_activate_work(work);
pwq->nr_active++;
worklist = &pwq->pool->worklist;
if (list_empty(worklist))
pwq->pool->watchdog_ts = jiffies;
} else {
work_flags |= WORK_STRUCT_DELAYED;
worklist = &pwq->delayed_works;
}
//添加 work 到队列中。
insert_work(pwq, work, worklist, work_flags);
}
__queue_work 主要由几个部分组成:
- 获取 cpu 参数
- 检查冲突
- 添加 work 到队列。
获取 cpu 参数
回顾开篇所提出的第一个问题:如何判断将当前的 work 添加到哪个 worker_pool 中?
在这个函数中就可以得到答案:通过 raw_smp_processor_id() 函数获取当前 cpu 的 id,通过 per_cpu_ptr(pwq,cpu) 接口获取当前 cpu 的 pwq,因为 pwq 向上连接了 workqueue_struct,向下连接了 worker_pool,所以因此也可以获取到当前 cpu 的 worker_pool。
如果指定添加到 unbound 类型的 workqueue_struct 上,就使用 unbound 类型的 pwq 和 worker_pool。
在特殊情况下,当前的 cpu 上没有初始化 worker_pool 和 pwq,就会找到下一个可用的 cpu。
检查冲突
检查当前的 work 是不是在这之前被添加到其他 worker_pool 中,如果是,就让它继续在原本的 worker_pool 上运行,这时候找到的 pwq 指向另一个 cpu 的 pwq 结构。
内核这样设计的初衷应该是出于 cpu 高速缓存的考虑,某个 cpu 曾经执行过该 work,所以将该 work 放到之前的 cpu 上执行可能因为缓存命中而提高执行效率,但是这也只是可能。除非两次 work 执行间隔非常小,高速缓存才有可能会保留。
添加 work 到队列
将 work 添加到队列的函数是 insert_work(pwq, work, worklist, work_flags),传入的参数中有 work 和 worklist,如果超过 pwq 支持的最大的 work 数量,将work添加到 pwq->delayed_works 中,否则就添加到 pwq->pool->worklist 中。
insert_work 的源码实现也比较简单:
static void insert_work(struct pool_workqueue *pwq, struct work_struct *work,
struct list_head *head, unsigned int extra_flags)
{
struct worker_pool *pool = pwq->pool;
//设置 work 的 pwq 和 flag。
set_work_pwq(work, pwq, extra_flags);
//将 work 添加到 worklist 链表中
list_add_tail(&work->entry, head);
//为 pwq 添加引用计数
get_pwq(pwq);
//添加内存屏障,防止 cpu 将指令乱序排列
smp_mb();
//唤醒 worker 对应的内核线程
if (__need_more_worker(pool))
wake_up_worker(pool);
}
简单地说,就是将 work 插入到 worker_pool->worklist 中。
尽管在前面的章节中有说明,但是博主觉得在这里有必要再强调一次:每一个通过 alloc_workqueue 创建的 wq 都会对应 percpu 的 pwq 和 percpu 的 worker_pool,比如在一个四核系统中,一个 alloc_workqueue 对应 4 个 pwq 和 4 个 worker_pool。
将 work 添加到 wq 的哪个 worker 呢? 就是执行当前 schedule_work 代码那个,添加完之后,就会唤醒 worker_pool 中第一个处于 idle 状态 worker->task 内核线程,work 就会进入到待处理状态。
对于开篇的第二个问题:这个添加的过程中,pwq 和 workqueue_struct、worker_pool 是如何进行交互的?
答案其实也很简单,获取当前 cpu 的 id,通过该 cpu id 获取当前被使用的 wq 绑定的 pwq,通过 pwq 就可以找到对应的 worker_pool,在这里 pwq 相当于 worker_pool 和 wq 之间的媒介。
参考
4.14 内核代码