基础workqueue实现
Linux kernel 2.6.36版本之前,内核已经实现了workqueue的功能。
- 工作线程创建
支持single-thread和Per-CPU thread两种形式的workqueue,对于singlethread workqueue,内核会创建单个kthread用于执行work任务,并且该kthread不会绑定到某个CPU上;对于Per-CPU thread workqueue,内核会在每个CPU上针对该workqueue创建一个kthread,每个kthread绑定到一个CPU上
- work的管理
针对每个workqueue,会创建percpu的cpu_workqueue_struct结构,用来实际管理每个加入到该workqueue的work。
- work的调度
对于Per-CPU thread类型的workqueue,每个CPU上都针对该workqueue创建了thread,那么每个work应该被哪个thread执行呢?实际上就是执行queue_work的CPU执行的,因为这个函数会把work加入到本CPU的cpu_workqueue_struct结构中,因此在本CPU的thread中会检测到对应cpu_workqueue_struct结构体中有work存在就会执行该work。
static struct cpu_workqueue_struct *wq_per_cpu(struct workqueue_struct *wq, int cpu)
{
if (unlikely(is_single_threaded(wq)))
cpu = singlethread_cpu;
return per_cpu_ptr(wq->cpu_wq, cpu);
}
- work防止重入
work是不会重入的,也就是说当一个work加入到一个workqueue之后还没有来得及执行,那么此时再次加入将不会生效。
int queue_work(struct workqueue_struct *wq, struct work_struct *work)
{
int ret = 0;
if (!test_and_set_bit(WORK_STRUCT_PENDING, work_data_bits(work))) {
__queue_work(wq_per_cpu(wq, get_cpu()), work);
put_cpu();
ret = 1;
}
return ret;
}
总结一下,它的特点如下:
1.支持单线程和多线程workqueue
2.单线程不绑定CPU,多线程每个CPU绑定一个线程
3.多线程workqueue中,在某个CPU上进行queue_work的work,一定会在该CPU上执行
4.queue_work时如果检测到该work处于pending状态,那么不会重复加入队列,防止重入
Concurrency Managed Workqueue (CMWQ)
Linux kernel 2.6.36版本之后,内核加入了CMWQ,并发管理任务队列,内核已经实现了workqueue,为什么还要CMWQ呢?当然是为了解决旧的workqueue遇到的一些缺点,那么旧的实现都有哪些问题呢?
- 内核线程过多
旧的workqueue实现,每个workqueue都会创建一系列per cpu的thread,这会大大增加系统中的进程数量,导致系统性能的降低
- 并发性较差
对于single thread类型的workqueue,其中的work是顺序执行,如果一个work阻塞,那么后面的都将收到影响,对于per cpu thread workqueue,情况会稍微好些,但是依然不够好,当一个CPU thread中已经加入了一个work时,由于queue_work只会在当前CPU上运行,后面再加入到该CPU thread的work依然需要等待,即使其他CPU thread是空闲的。
- 可能出现的死锁问题
在旧版本的workqueue中,已经加入到一个CPU上的work是不能转移到另一个CPU上的,那么假设有两个work 1和work 2,work1 依赖于work2,但是work1和work2被顺序加入到了同一个CPU thread上,那么work1将永远无法等到work2执行结束,这就形成了死锁问题。
CMWQ就是为了解决以上问题而引入内核的,它对workqueue的实现进行了优化,使得work的调度更加的灵活,为了兼容旧版本workqueue,接口基本保持了不变。
CMWQ把work的生产和消费分为了两个部分,对于生产者使用workqueue来管理产生的work,消费者是使用thread pool的形式来执行work。worker pool和workqueue是一对多的关系。worker pool和pwq(struct pool_workqueue)是一对一的关系。
- 生产者
内核在启动时会创建一些系统级的workqueue,比如system_wq,除了内核自带的一些workqueue,各个模块驱动可以自行创建workqueue,这些workqueue可以通过参数来选择用什么类型的work pool来运行work。
- 消费者
CMWQ中,把用于工作的线程叫做worker,并且建立两种worker pool,也就是thread pool线程池。第一种为绑定CPU的worker pool;第二种为unbound worker pool。系统初始化时会针对每个CPU创建绑定的worker pool,每个CPU创建两个类型的worker pool,一个是普通优先级的normal worker pool,另一个是highpri worker pool。每个worker pool中包含的worker线程数量是不定的。系统也会创建unbound worker pool,也有两个类型分别是noraml和highpri。
NR_STD_WORKER_POOLS = 2, /* # standard pools per cpu */
/* the per-cpu worker pools */
static DEFINE_PER_CPU_SHARED_ALIGNED(struct worker_pool [NR_STD_WORKER_POOLS], cpu_worker_pools);
可以通过ps查看系统中每个CPU对应的worker的数量:
root 214 2 0 9月09 ? 00:00:01 [kworker/0:1H]
root 232 2 0 9月09 ? 00:00:00 [kworker/1:1H]
root 233 2 0 9月09 ? 00:00:01 [kworker/3:1H]
root 234 2 0 9月09 ? 00:00:03 [kworker/4:1H]
root 239 2 0 9月09 ? 00:00:01 [kworker/2:1H]
root 674 2 0 9月09 ? 00:00:00 [kworker/5:1H]
root 699 2 0 9月09 ? 00:00:00 [kworker/7:1H]
root 747 2 0 9月09 ? 00:00:00 [kworker/6:1H]
root 7177 2 0 9月09 ? 00:00:00 [kworker/7:2]
root 8737 2 0 9月09 ? 00:00:00 [kworker/6:0]
root 19106 2 0 07:35 ? 00:00:00 [kworker/2:0]
root 19259 2 0 07:41 ? 00:00:00 [kworker/3:0]
root 22001 2 0 10:14 ? 00:00:00 [kworker/7:0]
root 22062 2 0 10:15 ? 00:00:00 [kworker/0:2]
root 22135 2 0 10:16 ? 00:00:00 [kworker/4:1]
root 22435 2 0 10:19 ? 00:00:00 [kworker/6:1]
root 25411 2 0 11:28 ? 00:00:03 [kworker/u16:3]
root 25973 2 0 11:40 ? 00:00:00 [kworker/3:1]
root 26212 2 0 11:55 ? 00:00:00 [kworker/4:0]
root 26419 2 0 12:10 ? 00:00:00 [kworker/0:0]
root 26438 2 0 12:12 ? 00:00:02 [kworker/u16:1]
root 27110 2 0 13:00 ? 00:00:01 [kworker/u16:2]
root 27327 2 0 13:17 ? 00:00:00 [kworker/u16:4]
可以通过ps查看系统中的kworker情况,其中数字的含义分别是 “kworker/cpu id:worker id”,H表示高优先级的worker pool,不带H的表示普通优先级的worker pool。如果在进程名中包含一个字母u表示的就是unbound worker,比如上面的“kworker/u16:3”表示的就是unbound worker pool id为16的第3个worker线程。
每个worker pool中的worker线程数量是可以动态调整的,所以不会出现创建大量无用线程的情况,假设一个worker中要运行3个work时,发现其中第1个work运行是发生了阻塞,那么会检测到当前worker pool中处于可运行状态的worker线程为0,那么就会创建一个worker来运行其他2的work。如果执行完毕后,发现当前处于idle状态的worker线程有多个,那么会保持一段时间,如果一直都为idle,就接着销毁该idle worker thread。这种动态调整优化了kworker的资源和性能平衡。
CMWQ接口
CMWQ和之前的workqueue相比,接口上最大的差异就是创建workqueue的函数:
#define alloc_workqueue(fmt, flags, max_active, args...) \
__alloc_workqueue_key((fmt), (flags), (max_active), NULL, NULL, ##args)
#define alloc_ordered_workqueue(fmt, flags, args...) \
alloc_workqueue(fmt, WQ_UNBOUND | __WQ_ORDERED | (flags), 1, ##args)
#define create_freezable_workqueue(name) \
alloc_workqueue("%s", WQ_FREEZABLE | WQ_UNBOUND | WQ_MEM_RECLAIM, 1, (name))
#define create_workqueue(name) \
alloc_workqueue("%s", WQ_MEM_RECLAIM, 1, (name))
#define create_singlethread_workqueue(name) \
alloc_ordered_workqueue("%s", WQ_MEM_RECLAIM, name)
CMWQ是使用alloc_workqueue来创建workqueue的,并且按照传入的参数不同,表示该workqueue管理的worker类型的不同:
WQ_UNBOUND说明其work的处理不需要绑定在特定的CPU上执行
WQ_FREEZABLE表示本work的执行是可以被冻住的
WQ_MEM_RECLAIM这种类型的workqueue会创建备份worker,当发生了内存回收导致worker创建失败时就使用备用的worker来进行调度
WQ_HIGHPRI说明挂入该workqueue的work是属于高优先级的work
WQ_CPU_INTENSIVE说明挂入该workqueue的work是属于特别消耗cpu的那一类,所以当系统检测到该work正在运行时,那么如果还有其他work在排队,那么会假设本worker pool已经没有可用的线程了,需要创建多余worker线程去处理其他的work。
加入work到workqueue的接口:
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, 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是被加入到哪种类型的worker pool中运行,WORK_CPU_UNBOUND表示的就是unbound类型的worker pool来处理该work,如果参数传入某一个CPU的id,那么就会指定对应CPU的worker pool来执行work。但这并不是绝对的,如果加入一个work时,发现该work正在其他CPU上执行,那么为了防止重入,会把该work加入上次执行的CPU上。我们会在下一篇文章介绍。