Linux Workqueue到CMWQ的技术演进

基础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上。我们会在下一篇文章介绍。

发布了234 篇原创文章 · 获赞 78 · 访问量 23万+

猜你喜欢

转载自blog.csdn.net/rikeyone/article/details/100710920
今日推荐