3. workqueue工作队列

  • 0 历史和原理概述
  • 1 初始化工作队列
    • 1.1 工作任务struct work_struct
    • 1.2 工作线程struct worker
    • 1.3 工作线程池struct worker_pool
    • 1.4 连接workqueue(工作队列)和worker-pool(工作线程池)的桥梁struct pool_workqueue
    • 1.5 工作队列struct workqueue_struct
    • 1.6 数据结构关系图
    • 1.7 系统初始化几个默认的workqueue
      • 1.7.1 create_worker()创建工作线程
  • 2 创建工作队列workqueue
    • 2.1 __alloc_workqueue_key创建工作队列
      • 2.1.1 pool_workqueue分配以及初始化函数alloc_and_link_pwqs()
        • 2.1.1.1 BOUND类型的workqueue
        • 2.1.1.2 ORDERED类型和UNBOUND类型的workqueue
  • 3 调度一个work
    • 3.1 初始化一个work
    • 3.2 schedule_work()调度work
    • 3.3 工作线程处理函数worker_thread()
  • 4 取消一个work
  • 5 和调度器的交互
  • 6 小结
    • 6.1 背景和原理
    • 6.2 数据结构

思考题:

  • workqueue是运行在中断上下文,还是进程上下文?其回调函数允许睡眠吗?
  • 旧版本(Linux 2.6.25)的 workqueue机制在实际过程中遇到了哪些问题和挑战?
  • CMWQ机制如何动态管理工作线程池的线程呢?
  • 如果有多个work挂入一个工作线程中执行,当某个work的回调函数执行了阻塞操作,那么剩下的work该怎么办?

0 历史和原理概述

工作队列机制(workqueue)是除了软中断和tasklet以外最常用的一种下半部机制。工作队列的基本原理是把work(需要推迟执行的函数)交由一个内核线程来执行,它总是在进程上下文中执行。工作队列的优点是利用进程上下文来执行中断下半部操作,因此工作队列允许重新调度睡眠,是异步执行的进程上下文,另外它还能解决软中断tasklet执行时间过长导致系统实时性下降等问题。

当驱动程序或者内核子系统在进程上下文中有异步执行的工作任务时,可以使用work item来描述工作任务,包括该工作任务的执行回调函数,把work item添加到一个队列中,然后一个内核线程会去执行这个工作任务的回调函数。这里work item被称为工作队列被称为workqueue,即工作队列,内核线程被称为worker

工作队列最早是在Linux 2.5.x内核开发期间被引入的机制,早期的工作队列的设计比较简单,由多线程(Multi threaded,每个CPU默认一个工作线程)和单线程(Single threaded, 用户可以自行创建工作线程)组成。在长期测试中发现如下问题:

  • 内核线程数量太多。虽然系统中有默认的一套工作线程(kevents),但是有很多驱动和子系统喜欢自行创建工作线程,例如调用create_workqueue()函数,这样在大型系统(CPU数量比较多的机器)中可能内核启动结束之后就耗尽了系统PID资源。
  • 并发性比较差。Multi threaded的工作线程和CPU是一一绑定的,例如CPU0上的某个工作线程有A 、B 和 C 三个work。假设执行work A上回调函数时发生了睡眠和调度,CPU0就会调度出去执行其他的进程,对 于 B 和 C 来说,它们只能等待CPU0重新调度执行该工作线程,尽管其他CPU比较空闲,也没有办法迁移到其他CPU上执行。
  • 死锁问题。系统有一个默认的工作队列kevents, 如果有很多work运行在默认的工作队列kevents上,并且它们有一些数据上依赖关系,那么很有可能会产生死锁。解决办法是为每一个有可能产生死锁的work创建一个专职的工作线程,这样又回到问题1了。

为此社区专家Tejun Heo在Linux 2.6.36中提出了一套解决方案concurrency-managed workqueues(CMWQ)。

执行work任务的线程称为worker工作线程工作线程串行化地执行挂入到队列中所有的work。如果队列中没有work, 那么该工作线程就会变成idle状态

为了管理众多工作线程,CMWQ提出了工作线程池(worker-pool)概念,worker-pool有两种,一是BOUND类型的,可以理解为Per-CPU类型,每个CPU都有worker-pool; 另一种是UNBOUND类型的,即不和具体CPU绑定。这两种worker-pool都会定义两个线程池,一个给普通优先级的work使用,另一个给高优先级的work使用。这些工作线程池中的线程数量动态分配和管理的,而不是固定的。当工作线程睡眠时,会去检查是否需要唤醒更多的工作线程,如有需要,会去唤醒同一个工作线程池中idle状态的工作线程。

1 初始化工作队列

1.1 工作任务struct work_struct

workqueue机制最小的调度单元是work item, 有的书中称为工作任务,由struct work_struct数据结构来抽象和描述,本章简称为work或工作任务。

[include/linux/workqueue.h]
struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;
};

struct work_struct数据结构定义比较简单。

  • data成员包括两部分低比特位部分是work的标志位剩余的比特位通常用于存放上一次运行的worker_poolID号pool_workqueue的指针,存放的内容由WORK_STRUCT_PWQ标志位来决定
  • func是工作任务的处理函数
  • entry用于把work挂到其他队列上。

1.2 工作线程struct worker

work运行在内核线程中,这个内核线程在代码中被称为worker, 类似流水线中的工人,work类似工人的工作,本章简称为工作线程或worker

工作线程struct worker数据结构来描述:

[kernel/workqueue_internal.h]
struct worker {
	/* on idle list while idle, on busy hash table while busy */
	union {
		struct list_head	entry;	/* L: while idle */
		struct hlist_node	hentry;	/* L: while busy */
	};

	struct work_struct	    *current_work;	/* L: work being processed */
	work_func_t		        current_func;	/* L: current_work's fn */
	struct pool_workqueue	*current_pwq;   /* L: current_work's pwq */
	struct list_head	    scheduled;	    /* L: scheduled works */
    struct list_head	    node;		    /* A: anchored at pool->workers */
						                    /* A: runs through worker->node */
	struct task_struct	    *task;		    /* I: worker task */
	struct worker_pool	    *pool;		    /* I: the associated pool */
	int			            id;		        /* I: worker id */
    ...
};
  • current_work: 当前正在处理的work
  • current_func: 当前正在执行的work回调函数
  • current_pwq:当前work所属的pool_workqueue
  • scheduled: 所有被调度并正准备执行的work都挂入该链表中。
  • task: 该工作线程的task_struct数据结构。
  • pool: 该工作线程所属的worker_pool
  • id: 工作线程的ID号
  • node: 可以把该worker挂入到worker_pool->workers链表中。

1.3 工作线程池struct worker_pool

CMWQ提出了工作线程池概念,代码中使用struct worker_pool数据结构来抽象和描述,本章简称worker-pool或者工作线程池。

简化后的struct worker_pool数据结构如下:

[kernel/workqueue.c]
struct worker_pool {
	spinlock_t		lock;		/* the pool lock */
	int			    cpu;		/* I: the associated cpu */
	int			    node;		/* I: the associated node ID */
	int			    id;		    /* I: pool ID */
	unsigned int	flags;		/* X: flags */

	struct list_head	worklist;	/* L: list of pending works */
	int			        nr_workers;	/* L: total number of workers */
	int			        nr_idle;	/* L: currently idle ones */

	struct list_head	idle_list;	/* X: list of idle workers */
	struct list_head	workers;	/* A: attached workers */
	struct workqueue_attrs	*attrs;		/* I: worker attributes */
	atomic_t		nr_running ____cacheline_aligned_in_smp;
	struct rcu_head		rcu;
	...
} ____cacheline_aligned_in_smp;
  • lock: 用于保护worker-pool的自旋锁
  • cpu: 对应BOUND类型workqueue来说,cpu表示绑定的CPU ID, 对应UNBOUND类型,该值为-1
  • node: 对于UNBOUND类型的workqueue,node表示该worker-pool所属内存节点的ID编号。
  • id: 该worker-pool的ID号
  • worklist: pending状态work会挂入该链表中。
  • nr_workers: 工作线程的数量
  • nr_idle: 处于idle状态工作线程的数量
  • idle_list: 处于idle状态工作线程(!!!)会挂入该链表中。
  • workers: 该worker-pool管理的工作线程会挂入该链表中。
  • attrs: 工作线程的属性
  • nr_running: 统计计数,用于管理worker创建和销毁,表示正在运行中的worker数量。在进程调度器唤醒进程时(try_to_wake_up()),其他CPU有可能会同时访问该成员,该成员频繁在多核之间读写,因此让该成员独占一个缓冲行(!!!),避免多核CPU读写该成员时引发其他临近的成员“颠簸”现象,这也是所谓的“缓存行伪共享”的问题。
  • rcu: RCU锁。

worker-pool是Per-CPU概念,每个CPU都有worker-pool, 准确来说每个CPU有两个worker-pool, 一个用于普通优先级的工作线程,另一个用于高优先级的工作线程

[kernel/workqueue.c]
/* the per-cpu worker pools */
static DEFINE_PER_CPU_SHARED_ALIGNED(struct worker_pool [NR_STD_WORKER_POOLS],
				     cpu_worker_pools);

1.4 连接workqueue(工作队列)和worker-pool(工作线程池)的桥梁struct pool_workqueue

CMWQ还定义了一个pool_workqueue的数据结构,它是连接workqueue和worker-pool的枢纽

[kernel/workqueue.c]
struct pool_workqueue {
	struct worker_pool	    *pool;		/* I: the associated pool */
	struct workqueue_struct *wq;		/* I: the owning workqueue */
	int			            nr_active;	/* L: nr of active works */
	int			            max_active;	/* L: max active works */
	struct list_head	    delayed_works;	/* L: delayed works */
	struct rcu_head		    rcu;
	...
} __aligned(1 << WORK_STRUCT_FLAG_BITS);

其中,WORK_STRUCT_FLAG_BITS为8, 因此pool_workqueue数据结构是按照256Byte对齐的,这样方便把该数据结构指针的bit[8:31]位存放到work->data中,work->data字段的低8位用于存放一些标志位,见set_work_pwq()和get_work_pwq()函数。

  • pool: 指向worker-pool指针
  • wq: 指向所属的工作队列
  • nr_active: 活跃的work数量
  • max_active: 活跃的work最大数量
  • delayed_works: 链表头,被延迟执行的works可以挂入该链表
  • rcu: rcu锁。

1.5 工作队列struct workqueue_struct

系统中所有的工作队列,包括系统默认的工作队列,例如system_wq或system_highpri_wq等,以及驱动开发者新创建的工作队列,它们共享一组worker-pool。而对于BOUND类型的工作队列每个CPU只有两个工作线程池每个工作线程池可以和多个workqueue对应,每个workqueue只能对应这几个工作线程池

工作队列由struct workqueue_struct数据结构来描述:

[kernel/workqueue.c]
struct workqueue_struct {
	struct list_head	pwqs;		/* WR: all pwqs of this wq */
	struct list_head	list;		/* PL: list of all workqueues */

	struct list_head	maydays;	/* MD: pwqs requesting rescue */
	struct worker		*rescuer;	/* I: rescue worker */

	struct workqueue_attrs	*unbound_attrs;	/* WQ: only for unbound wqs */
	struct pool_workqueue	*dfl_pwq;	/* WQ: only for unbound wqs */

	char			name[WQ_NAME_LEN]; /* I: workqueue name */

	/* hot fields used during command issue, aligned to cacheline */
	unsigned int		flags ____cacheline_aligned; /* WQ: WQ_* flags */
	struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */
	...
};
  • pwqs: 所有的pool-workqueue数据结构都挂入链表中。
  • list: 链表节点。系统定义一个全局的链表workqueues所有的workqueue挂入该链表
  • maydays: 所有rescue状态下的pool-workqueue数据结构挂入该链表
  • rescuer: rescue内核线程内存紧张创建新的工作线程可能会失败,如果创建workqueue时设置了WQ_MEM_RECLAIM标志位,那么rescuer线程会接管这种情况
  • unbound attrs: UNBOUND类型属性
  • dfl_pwq: 指向UNBOUND类型的pool_workqueue.
  • name: 该workqueue的名字
  • flags: 标志位经常被不同CPU访问,因此要和cache line对齐。标志位包括WQ_UNBOUND、WQ_HIGHPRI、WQ_FREEZABLE等。
  • cpu_pwqs: 指向Per-CPU类型pool workqueue

1.6 数据结构关系图

一个work挂入workqueue中,最终还要通过worker-pool中的工作线程来处理其回调函数,worker-pool是系统共享的(!!!),因此workqueue需要查找到一个合适的worker-pool,然后从worker-pool中分派一个合适的工作线程,pool_workqueue数据结构在其中起到桥梁作用。这有些类似IT类公司的人力资源池的概念,具体关系如图5.7所示。

3. workqueue工作队列_第1张图片

3. workqueue工作队列_第2张图片

  • work_struct结构体代表的是一个任务,它指向一个待异步执行的函数,不管驱动还是子系统什么时候要执行这个函数,都必须把work加入到一个workqueue

  • worker结构体代表一个工作者线程(worker thread),它主要一个接一个的执行挂入到队列中的work,如果没有work了,那么工作者线程就挂起,这些工作者线程被worker-pool管理。

对于驱动和子系统的开发人员来说,接触到的只有work,而背后的处理机制是管理worker-pool和处理挂入的work。

  • worker_pool结构体用来管理worker,对于每一种worker pool都分两种情况:一种是处理普通work,另一种是处理高优先级的work

  • workqueue_struct结构体代表的是工作队列,工作队列分unbound workqueuebound workqueue。bound workqueue就是绑定到cpu上的,挂入到此队列中的work只会在相对应的cpu上运行。unbound workqueue不绑定到特定的cpu,而且后台线程池的数量也是动态的,具体workqueue关联到哪个worker pool,这是由workqueue_attrs决定的。

1.7 系统初始化几个默认的workqueue

总结:

(1) 创建一个pool_workqueue结构的slab缓存对象, workqueue针对NUMA系统做一些初始化

(2) 为所有可用CPU(!!!包括离线的!!!)创建两个工作线程池struct worker_pool(普通优先级的和高优先级的)并初始化

(3) 为每个在线CPU(!!!)的每个工作线程池(每个CPU有两个)分别创建一个工作线程(调用create_worker(), 详细见下面)

(4) 创建UNBOUND类型ordered类型workqueue属性, 分别是两个, 对应普通优先级高优先级, 可以供后续使用

(5) 调用alloc_workqueue(), 创建几个默认的workqueue

系统启动时,会通过init_workqueues()函数来初始化几个系统默认的workqueue

[kernel/workqueue.c]
// per cpu的pool数目
NR_STD_WORKER_POOLS	= 2,		/* # standard pools per cpu */

/* I: attributes used when instantiating standard unbound pools on demand */
static struct workqueue_attrs *unbound_std_wq_attrs[NR_STD_WORKER_POOLS];

/* I: attributes used when instantiating ordered pools on demand */
static struct workqueue_attrs *ordered_wq_attrs[NR_STD_WORKER_POOLS];

static int __init init_workqueues(void)
{
	int std_nice[NR_STD_WORKER_POOLS] = { 0, HIGHPRI_NICE_LEVEL };
	int i, cpu;
    // 位置1
	pwq_cache = KMEM_CACHE(pool_workqueue, SLAB_PANIC);

	cpu_notifier(workqueue_cpu_up_callback, CPU_PRI_WORKQUEUE_UP);
	hotcpu_notifier(workqueue_cpu_down_callback, CPU_PRI_WORKQUEUE_DOWN);
    // 位置2
	wq_numa_init();

	/* initialize CPU pools */
	// 位置3
	for_each_possible_cpu(cpu) {
		struct worker_pool *pool;

		i = 0;
		// 位置4
		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);

			/* alloc pool ID */
			mutex_lock(&wq_pool_mutex);
			BUG_ON(worker_pool_assign_id(pool));
			mutex_unlock(&wq_pool_mutex);
		}
	}

	/* create the initial worker */
	// 位置5
	for_each_online_cpu(cpu) {
		struct worker_pool *pool;

		for_each_cpu_worker_pool(pool, cpu) {
			pool->flags &= ~POOL_DISASSOCIATED;
			BUG_ON(!create_worker(pool));
		}
	}
    // 位置6
	/* 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;
	}
    // 位置7
	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);
	return 0;
}
early_initcall(init_workqueues);

位置1, 创建一个pool_workqueue数据结构的slab缓存对象

位置2, workqueue考虑了NUMA系统情况的一些特殊处理。

位置3, 为系统中所有可用的CPU(cpu_possible_mask) 分别创建struct worker_pool数据结构.

位置4, for_each_cpu_worker_pool()为每个CPU创建两个worker_pool, 一个是普通优先级的工作线程池, 另一个是高优先级的工作线程池, init_worker_pool()函数用于初始化一个worker_pool. 这里初始化的都是BOUND类型的worker_pool, 所以worker_pool->cpu都相应设置了; node都设置成为了当前cpu所属的内存节点

注意位置4的for_each_cpu_worker_pool遍历CPU中两个worker_pool:

[kernel/workqueue.c]
#define for_each_cpu_worker_pool(pool, cpu)				\
	for ((pool) = &per_cpu(cpu_worker_pools, cpu)[0];		\
	     (pool) < &per_cpu(cpu_worker_pools, cpu)[NR_STD_WORKER_POOLS]; \
	     (pool)++)

位置5, 为系统每一个在线(online)CPU中的每个worker_pool分别创建一个工作线程

位置6, 创建UNBOUND类型ordered类型的workqueue属性ordered类型workqueue表示同一个时刻只能有一个work item在运行(!!!)。

位置7到最后, 创建系统默认的workqueue,这里使用创建工作队列的API函数alloc_workqueue().

  • 普通优先级BOUND类型工作队列system_wq, 名称为“events”,可以理解为默认工作队列
  • 高优先级BOUND类型的工作队列system_highpri_wq ,名称为“events_highpri”。
  • UNBOUND类型的工作队列system_unbound_wq,名称为“system_unbound_wq”。
  • Freezable类型的工作队列system_freezable_wq,名称为“events_freezable”。
  • 省电类型的工作队列system_freezable_wq,名称为 “events_power_efficient”。

1.7.1 create_worker()创建工作线程

总结:

(1) 获取一个ID

(2) 工作线程池对应的内存节点分配一个worker

(3) 在工作线程池对应的内存节点上创建一个内核线程给分配的worker, 执行函数为worker_thread, 参数为worker(struct worker), 内核线程名字是"kworker/u + CPU_ID + : + worker_idH", 高优先级的才有H, UNBOUND类型的才有u

(4) 设置线程(worker->task->flags)的PF_NO_SETAFFINITY标志位, 防止修改CPU亲和性

(5) 将创建的worker挂到worker_pool: 线程池如果没有绑定到某个CPU, 那么设置worker不绑定CPU, 可在任意CPU上运行; 将worker加到工作线程池的workers链表

(6) 使worker进入idle状态

(7) 唤醒worker的内核线程

(8) 返回该worker

上面位置5, 会为每个online的CPU每个worker_pool分别创建一个工作线程.

下面来看create_worker()函数是如何创建工作线程的。

[init_workqueues()->create_worker()]
static struct worker *create_worker(struct worker_pool *pool)
{
	struct worker *worker = NULL;
	int id = -1;
	char id_buf[16];
    // 位置1
	/* ID is needed to determine kthread name */
	id = ida_simple_get(&pool->worker_ida, 0, 0, GFP_KERNEL);
    // 位置2
	worker = alloc_worker(pool->node);

	worker->pool = pool;
	worker->id = id;
    // 位置3
	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);
    // 位置4
	worker->task = kthread_create_on_node(worker_thread, worker, pool->node,
					      "kworker/%s", id_buf);

	set_user_nice(worker->task, pool->attrs->nice);

	/* prevent userland from meddling with cpumask of workqueue workers */
	// 位置5
	worker->task->flags |= PF_NO_SETAFFINITY;

	/* successful, attach the worker to the pool */
	// 位置6
	worker_attach_to_pool(worker, pool);

	/* start the newly created worker */
	spin_lock_irq(&pool->lock);
	// 位置7
	worker->pool->nr_workers++;
	// 位置8
	worker_enter_idle(worker);
	// 位置9
	wake_up_process(worker->task);
	spin_unlock_irq(&pool->lock);

	return worker;
}

位置1, 通过IDA子系统获取一个ID号

位置2, 在worker_pool对应的内存节点中分配一个worker数据结构。

位置3到位置4之间,pool->cpu >= 0, 表示BOUND类型的工作线程。worker的名字一般是 “kworker/ + CPU_ID + worker_id”,如果属于高优先级类型的workqueue,即nice值小于 0,那么还要加上“H”。 pool->cpu < 0,表示UNBOUND类型的工作线程,名字为“kworker/u + CPU_ID + worker_id”。

位置4,通过kthread_create_on_node()函数在工作线程池对应的node!!!创建一个内核线程用于worker,在这个内存节点上分配该内核线程相关的struct task_struct等数据结构。

注意, 线程执行函数为worker_thread!!!worker(struct worker)是执行函数的参数, 在工作线程池对应的node上创建, 线程名是位置3设置的!!!

位置5,设置工作线程(task的flags!!!)的PF_NO_SETAFFINITY标志位,防止用户程序修改其CPU亲和性。在位置6代码中会设置这个worker允许运行的cpumask(!!!)。

位置6,worker_attach_to_pool()函数把刚分配的工作线程挂入worker_pool中。

[create_worker() ->worker_attach_to_pool()]
static void worker_attach_to_pool(struct worker *worker,
				   struct worker_pool *pool)
{
	mutex_lock(&pool->attach_mutex);
	set_cpus_allowed_ptr(worker->task, pool->attrs->cpumask);

	if (pool->flags & POOL_DISASSOCIATED)
		worker->flags |= WORKER_UNBOUND;

	list_add_tail(&worker->node, &pool->workers);
	mutex_unlock(&pool->attach_mutex);
}

worker_attach_to_pool()函数最主要的工作是将该worker工作线程加入worker_pool->workers链表中。

POOL_DISASSOCIATEDworker-pool(工作线程池使用的!!!)内部使用的标志位一个线程池可以是associated状态或disassociated状态。associated状态的线程池表示有绑定到某个CPU上, disassociated状态的线程池表示没有绑定某个CPU, 也有可能是绑定的CPU被offline(!!!)了,因此可以在任意CPU上运行(!!!)。

回到create_worker()函数中,位置7代码中的nr_workers统计该worker_pool中的工作线程的个数。注意这里nr_workers变量需要用spinlock锁来保护,因为每个worker_pool定义了一个timer,用于动态删除过多的空闲的worker(!!!),见idle_worker_timeout()函数。

位置8,worker_enter_idle()函数让该工作线程进入idle状态

位置9, wake_up_process()函数唤醒该工作线程

2 创建工作队列workqueue

创建工作队列API有很多,并且基本上和旧版本的workqueue兼容。

[include/linux/workqueue.h]
#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_workqueue(name)						\
	alloc_workqueue("%s", WQ_MEM_RECLAIM, 1, (name))
#define create_freezable_workqueue(name)				\
	alloc_workqueue("%s", WQ_FREEZABLE | WQ_UNBOUND | WQ_MEM_RECLAIM, \
			1, (name))
#define create_singlethread_workqueue(name)				\
	alloc_ordered_workqueue("%s", WQ_MEM_RECLAIM, name)

最常见是alloc_workqueue(), 有3个参数, 分别是name, flags和max_active. 其他API和该API类似, 只是调用的flags不相同(!!!).

(1) WQ_UNBOUND: 工作任务work会加入UNBOUND工作队列中,UNBOUND工作队列的工作线程没有绑定到具体的CPU上。UNBOUND类型的work不需要额外的同步管理,UNBOUND工作线程池会尝试尽快执行它的work。这类work会牺牲一部分性能(局部原理带来的性能提升),但是比较适用于如下场景。

  • 一些应用在不同的CPU上跳跃,这样如果创建Bound类型的工作队列,会创建很多没用的工作线程
  • 长时间运行CPU消耗类型的应用(标记WQ_CPU_INTENSIVE标志位)通常会创建UNBOUND类型的workqueue, 进程调度器会管理这类工作线程在哪个CPU上运行。

(2) WQ_FREEZABLE: 一个标记着WQ_FREEZABLE的工作队列会参与到系统的suspend过程中,这会让工作线程处理完成当前所有的work才完成进程冻结,并且这个过程不会再新开始一个work的执行,直到进程被解冻

(3) WQ_MEM_RECLAIM: 当内存紧张时,创建新的工作线程可能会失败,系统还有一个rescuer内核线程会去接管这种情况。

(4) WQ_HIGHPRI: 属于高优先级的worker-pool, 即比较低的nice值

(5) WQ_CPU_INTENSIVE: 属于特别消耗CPU资源的一类work, 这类work的执行会得到系统进程调度器的监管。排在这类work后面的non-CPU-intensive类型的work可能会推迟执行

(6) __WQ_ORDERED: 表示同一个时间只能执行一个work item

参数max_active也值得关注,它决定每个CPU最多可以有多少个work挂入一个工作队列中。例如max_active=16,说明每个CPU最多可以有16个work挂入到工作队列中执行(!!!)。

  • 通常对于BOUND类型的工作队列,max_active最大可以是512,如果max_active参数传入0,则表示指定为256
  • 对于UNBOUND类型工作队列,max_active可以取5124 * num_possible_cpus()之间的最大值

通常建议驱动开发者使用max_active=0作为参数,有些驱动开发者希望使用一个严格串行执行工作队列alloc_ordered_workqueue()API可以满足这方面的需求,这里使用max_active=1WQ_UNBOUND的组合,同一时刻只有一个work可以执行

2.1 __alloc_workqueue_key创建工作队列

总结:

(1) 分配一个workqueue_struct并初始化

(2) 对于UNBOUND类型, 创建一个UNBOUND类型的workqueue属性

(3) BOUND类型的workqueue,

[kernel/workqueue.c]
struct workqueue_struct *__alloc_workqueue_key(const char *fmt,
					       unsigned int flags,
					       int max_active,
					       struct lock_class_key *key,
					       const char *lock_name, ...)
{
	size_t tbl_size = 0;
	va_list args;
	struct workqueue_struct *wq;
	struct pool_workqueue *pwq;

	/* see the comment above the definition of WQ_POWER_EFFICIENT */
	// 位置1
	if ((flags & WQ_POWER_EFFICIENT) && wq_power_efficient)
		flags |= WQ_UNBOUND;

	/* allocate wq and format name */
	if (flags & WQ_UNBOUND)
		tbl_size = nr_node_ids * sizeof(wq->numa_pwq_tbl[0]);
    // 位置2
	wq = kzalloc(sizeof(*wq) + tbl_size, GFP_KERNEL);
	if (!wq)
		return NULL;
    // 位置3
	if (flags & WQ_UNBOUND) {
		wq->unbound_attrs = alloc_workqueue_attrs(GFP_KERNEL);
		if (!wq->unbound_attrs)
			goto err_free_wq;
	}

	va_start(args, lock_name);
	vsnprintf(wq->name, sizeof(wq->name), fmt, args);
	va_end(args);

	max_active = max_active ?: WQ_DFL_ACTIVE;
	max_active = wq_clamp_max_active(max_active, flags, wq->name);

	/* init 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);

	lockdep_init_map(&wq->lockdep_map, lock_name, key, 0);
	INIT_LIST_HEAD(&wq->list);

	if (alloc_and_link_pwqs(wq) < 0)
		goto err_free_wq;

位置1, WQ_POWER_EFFICIENT标志位考虑系统的功耗问题。

  • 对于BOUND类型的workqueue, 它是Per-CPU类型的,会利用cache的局部性原理来提高性能。也就是说,它不会从这个CPU迁移到另外一个CPU, 也不希望进程调度器来打扰它们。
  • 设置成UNBOUND类型的workqueue后,究竟选择哪个CPU上唤醒交由进程调度器决定

Per-CPU类型workqueue会让idle状态的CPU从idle状态唤醒,从而增加了功耗。如果系统配置了CONNG_WQ_POWER_EFFICIENT_DEFAULT选项,那么创建workqueue会把标记了WQ_POWER_EFFIOENTworkqueue设置成UNBOUND类型,这样进程调度器就可以参与选择CPU来执行.

[kernel/workqueue.c]
#ifdef CONFIG_WQ_POWER_EFFICIENT_DEFAULT
static bool wq_power_efficient = true;
#else
static bool wq_power_efficient;
#endif

位置2以及后面, 是分配一个workqueue_struct数据结构并初始化。

位置3, 对于UNBOUND类型, 创建一个UNBOUND类型的workqueue属性

2.1.1 pool_workqueue分配以及初始化函数alloc_and_link_pwqs()

接下来看pool_workqueue分配以及初始化

2.1.1.1 BOUND类型的workqueue

总结:

(1) 给每个CPU分配一个Per-CPUpool_workqueue

(2) 遍历每个CPU, 通过CPU的这个pool_workqueue系统静态定义的Per-CPU类型的高优先级worker_pool(也就是init_workqueues()初始化的)传入的workqueue连接起来, 并将这个pool_workqueue添加到传入的workqueue->pwqs链表.

[alloc_workqueue() ->alloc_and_link_pwqs()]
[kernel/workqueue.c]
static int alloc_and_link_pwqs(struct workqueue_struct *wq)
{
	bool highpri = wq->flags & WQ_HIGHPRI;
	int cpu, ret;
    // 位置1
	if (!(wq->flags & WQ_UNBOUND)) {
	    // 为每个CPU分配一个Per-CPU类型的pool_workqueue
		wq->cpu_pwqs = alloc_percpu(struct pool_workqueue);
		// 遍历所有可用CPU
		for_each_possible_cpu(cpu) {
		    // 得到当前CPU的pool_workqueue
			struct pool_workqueue *pwq =
				per_cpu_ptr(wq->cpu_pwqs, cpu);
			// 得到当前CPU的worker_pool
			struct worker_pool *cpu_pools =
				per_cpu(cpu_worker_pools, cpu);
            // 通过当前CPU的pool_workqueue将当前CPU的高优先级worker_pool和传入的workqueue连接起来
			init_pwq(pwq, wq, &cpu_pools[highpri]);

			mutex_lock(&wq->mutex);
			// 将当前CPU的pool_workqueue加到传入的workqueue->pwqs链表
			link_pwq(pwq);
			mutex_unlock(&wq->mutex);
		}
		return 0;
	// 位置2
	} else if (wq->flags & __WQ_ORDERED) {
		ret = apply_workqueue_attrs(wq, ordered_wq_attrs[highpri]);
		/* there should only be single pwq for ordering guarantee */
		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;
	// 位置3
	} else {
		return apply_workqueue_attrs(wq, unbound_std_wq_attrs[highpri]);
	}
}

位置1的整个if, 处理BOUND类型的workqueue。给每个CPU分配一个Per-CPUpool_workqueue, 遍历每个CPU, 通过CPU的这个pool_workqueue系统静态定义的Per-CPU类型的高优先级的worker_pool(也就是init_workqueues()初始化的)传入的workqueue连接起来, 并将这个pool_workqueue添加到传入的workqueue->pwqs链表.

cpu_pwqs是一个Per-CPU类型的指针,alloc_percpu()为每个CPU分配一个Per-CPU类型的pool_workqueue数据结构。

cpu_worker_pools系统静态定义的Per-CPU类型worker_pool数据结构wq->cpu_pwqs动态分配Per-CPU类型的pool_workqueue数据结构。

init_pwq()函数把这两个数据结构连接起来,即pool_workqueue->pool指向worker_pool数据结构,pool_workqueue->wq指向workqueue_struct数据结构。

link_pwq()函数主要是把pool_workqueue添加到workqueue_struct->pwqs链表中。

位置2和位置3处理ORDERED类型UNBOUND类型的workqueue

2.1.1.2 ORDERED类型和UNBOUND类型的workqueue

总结:

都通过调用apply_workqueue_attrs()函数来实现, 出入的workqueue_attrs属性参数不同, 一个是ordered_wq_attrs[highpri], 一个是unbound_std_wq_attrs[highpri], 这两个不同之处在于属性里面的no_numa在ordered中是true, 这些都是在系统初始化init_workqueues()阶段完成

(1) 通过系统全局哈希表unbound_pool_hash(管理所有UNBOUND类型的work_pool)根据属性查找worker_pool, 找到将其引用计数加1, 并返回, 没有的话重新分配并初始化一个(创建pool, 为pool创建一个工作线程worker<会唤醒线程>), 将新pool加入哈希表

(2) 分配一个pool_workqueue

(3) 初始化该pwq, 将worker_pool和workqueue_struct连接起来, 为pool_workqueue初始化一个工作work(通过INIT_WORK()), 回调函数是pwq_unbound_release_workfn(), 该work执行: 从work中找到相应的pwq, 该work只对UNBOUND类型的workqueue有效, 如果work->pwq->wq->pwqs(所有pool_workqueue都在这个链表)中当前pool_workqueue是最后一个, 释放pool_workqueue相关结构

执行代码片段如下:

[alloc_workqueue() -> alloc_and_link_pwqs() -> apply_workqueue_attrs()]
int apply_workqueue_attrs(struct workqueue_struct *wq,
			  const struct workqueue_attrs *attrs)
{
	struct workqueue_attrs *new_attrs, *tmp_attrs;
	struct pool_workqueue **pwq_tbl, *dfl_pwq;
	int node, ret;
    // 分配pool_workqueue
	pwq_tbl = kzalloc(nr_node_ids * sizeof(pwq_tbl[0]), GFP_KERNEL);

	mutex_lock(&wq_pool_mutex);
    // 查找或新建一个pool_workqueue
	dfl_pwq = alloc_unbound_pwq(wq, new_attrs);

	for_each_node(node) {
		dfl_pwq->refcnt++;
		pwq_tbl[node] = dfl_pwq;
	}

	mutex_unlock(&wq_pool_mutex);

    mutex_lock(&wq->mutex);

	/* save the previous pwq and install the new one */
	for_each_node(node)
		pwq_tbl[node] = numa_pwq_tbl_install(wq, node, pwq_tbl[node]);

	/* @dfl_pwq might not have been used, ensure it's linked */
	link_pwq(dfl_pwq);
	swap(wq->dfl_pwq, dfl_pwq);

	mutex_unlock(&wq->mutex);

	/* put the old pwqs */
	for_each_node(node)
	    // 位置1
		put_pwq_unlocked(pwq_tbl[node]);
	put_pwq_unlocked(dfl_pwq);

	put_online_cpus();
	ret = 0;
	return ret;
}

首先分配一个pool_workqueue数据结构, 然后调用alloc_unbound_pwq()来查找或新建一个pool_workqueue.

[apply_workqueue_attrs() ->alloc_unbound_pwq()]
[kernel/workqueue.c]
static struct pool_workqueue *alloc_unbound_pwq(struct workqueue_struct *wq,
					const struct workqueue_attrs *attrs)
{
	struct worker_pool *pool;
	struct pool_workqueue *pwq;
    // 查找相同属性的worker_pool
	pool = get_unbound_pool(attrs);
    // 给pool_workqueue 分配内存
	pwq = kmem_cache_alloc_node(pwq_cache, GFP_KERNEL, pool->node);
	init_pwq(pwq, wq, pool);
	// 返回pool_workqueue
	return pwq;
}

首先通过get_unbound_pool()去系统中查找有没有相同属性的worker_pool

[kernel/workqueue.c]
static struct worker_pool *get_unbound_pool(const struct workqueue_attrs *attrs)
{
    // 根据attrs计算对应的散列值
	u32 hash = wqattrs_hash(attrs);
	struct worker_pool *pool;
	int node;

	/* do we already have a matching pool? */
	// 从全局散列表unbound_pool_hash中,根据attrs对比找到存在的worker_pool,找到就返回
	hash_for_each_possible(unbound_pool_hash, pool, hash_node, hash) {
		if (wqattrs_equal(pool->attrs, attrs)) {
			pool->refcnt++;
			return pool;
		}
	}
    // 走到这一步说明没有找到存在的worker_pool,那么下面就得新创建一个
	/* nope, create a new one */
	// 创建一个worker_pool
	pool = kzalloc(sizeof(*pool), GFP_KERNEL);
    
    lockdep_set_subclass(&pool->lock, 1);   /* see put_pwq() */
    // copy属性给worker_pool
    copy_workqueue_attrs(pool->attrs, attrs);

    /*
     * no_numa isn't a worker_pool attribute, always clear it.  See
     * 'struct workqueue_attrs' comments for detail.
     */
    pool->attrs->no_numa = false;

	if (worker_pool_assign_id(pool) < 0)
		goto fail;

	/* create and start the initial worker */
	// 创建工作者线程
	if (!create_worker(pool))
		goto fail;

	/* install */
	// 加入到全局unbound_pool_hash散列表中
	hash_add(unbound_pool_hash, &pool->hash_node, hash);

	return pool;
fail:
	if (pool)
		put_unbound_pool(pool);
	return NULL;
}

系统定义了一个哈希表unbound_pool_hash(!!!),用于管理系统中所有的UNBOUND类型的worker_pool,通过wqattrs_equal()判断系统中是否己经有了类型相关的worker_pool, wqattrs_equal()函数首先会比较nice值,然后比较cpumask位图是否一致

如果哈希表有worker_pool, 那将该pool引用计数加1, 并返回该pool

如果哈希表没有,那就重新分配和初始化一个(创建pool, 为该pool创建一个worker<会唤醒线程>, 将当前pool加入到哈希表), 最后返回新建的pool

回到alloc_unbound_pwq()函数中,找到worker_pool还需要一个连接器pool_workqueue,最后通过init_pwq()函数把worker_poolworkqueue_struct串联起来。

回到apply_workqueue_attrs()函数中的numa_pwq_tbl_install()函数。

[kernel/workqueue.c]
static struct pool_workqueue *numa_pwq_tbl_install(struct workqueue_struct *wq,
						   int node,
						   struct pool_workqueue *pwq)
{
	struct pool_workqueue *old_pwq;

	lockdep_assert_held(&wq->mutex);

	/* link_pwq() can handle duplicate calls */
	link_pwq(pwq);

	old_pwq = rcu_access_pointer(wq->numa_pwq_tbl[node]);
	rcu_assign_pointer(wq->numa_pwq_tbl[node], pwq);
	return old_pwq;
}

link_pwq()把找到的pool_workqueue添加到workqueue_struct->pwqs链表中。

接下来利用RCU锁机制来保护pool_workqueue数据结构,首先old_pwq和pwq_tbl[node]指向wq->numa_pwq_tbl[node]中旧的数据,rcu_assign_pointer()之后wq->numa_pwq_tbl[node]指针指向新的数据。那RCU什么时候会删除旧数据呢?看apply_workqueue_attrs()函数位置1处的代码,其中参数pwq_tbl[node]指向旧数据

[put_pwq_unlocked() ->put_pwq()]
[kernel/workqueue.c]
static void put_pwq(struct pool_workqueue *pwq)
{
	lockdep_assert_held(&pwq->pool->lock);
	if (likely(--pwq->refcnt))
		return;
	if (WARN_ON_ONCE(!(pwq->wq->flags & WQ_UNBOUND)))
		return;

	schedule_work(&pwq->unbound_release_work);
}

pool_workqueue->refcnt成员计数小于0时,会通过schedule_work()调度一个系统默认的work每个pool_workqueue有初始化一个work!!!,见init_pwq()函数

[kernel/workqueue.c]
static void init_pwq(struct pool_workqueue *pwq, struct workqueue_struct *wq,
		     struct worker_pool *pool)
{
	BUG_ON((unsigned long)pwq & WORK_STRUCT_FLAG_MASK);

	memset(pwq, 0, sizeof(*pwq));

	pwq->pool = pool;
	pwq->wq = wq;
	pwq->flush_color = -1;
	pwq->refcnt = 1;
	INIT_LIST_HEAD(&pwq->delayed_works);
	INIT_LIST_HEAD(&pwq->pwqs_node);
	INIT_LIST_HEAD(&pwq->mayday_node);
	INIT_WORK(&pwq->unbound_release_work, pwq_unbound_release_workfn);
}

直接看该work的回调函数pwq_unbound_release_workfn()

[put_pwq_unlocked() ->put_pwq() ->pwq_unbound_release_workfn()]
[kernel/workqueue.c]
static void pwq_unbound_release_workfn(struct work_struct *work)
{
	struct pool_workqueue *pwq = container_of(work, struct pool_workqueue,
						  unbound_release_work);
	struct workqueue_struct *wq = pwq->wq;
	struct worker_pool *pool = pwq->pool;
	bool is_last;

	if (WARN_ON_ONCE(!(wq->flags & WQ_UNBOUND)))
		return;

	mutex_lock(&wq->mutex);
	list_del_rcu(&pwq->pwqs_node);
	is_last = list_empty(&wq->pwqs);
	mutex_unlock(&wq->mutex);

	mutex_lock(&wq_pool_mutex);
	put_unbound_pool(pool);
	mutex_unlock(&wq_pool_mutex);

	call_rcu_sched(&pwq->rcu, rcu_free_pwq);

	/*
	 * If we're the last pwq going away, @wq is already dead and no one
	 * is gonna access it anymore.  Free it.
	 */
	if (is_last) {
		free_workqueue_attrs(wq->unbound_attrs);
		kfree(wq);
	}
}

首先从work中找到pool_workqueue数据结构指针pwq,注意该work只对UNBOUND类型的workqueue有效。当有需要释放pool_workqueue数据结构时,会调用call_rcu_sched()来对旧数据进行保护,让所有访问该旧数据的读临界区都经历过了Grace Period之后才会释放旧数据。

3 调度一个work

3.1 初始化一个work

初始化一个work: 宏INIT_WORK(work, func)

Linux内核推荐驱动开发者使用默认的workqueue, 而不是新创建workqueue。要使用系统默认的workqueue,首先需要初始化一个work, 内核提供了相应的宏INIT_WORK()。

[include/linux/workqueue.h]
#define INIT_WORK(_work, _func)						\
	__INIT_WORK((_work), (_func), 0)

#define __INIT_WORK(_work, _func, _onstack)				\
	do {								\
		__init_work((_work), _onstack);				\
		(_work)->data = (atomic_long_t) WORK_DATA_INIT();	\
		INIT_LIST_HEAD(&(_work)->entry);			\
		(_work)->func = (_func);				\
	} while (0)

struct work_struct数据结构不复杂,主要是对data、entry和回调函数func的赋值。data成员被划分成两个域低比特位域用于存放work相关的flags, 高比特位域用于存放上次执行该work的worker_pool的ID号保存上一次pool_workqueue数据结构指针

[include/linux/workqueue.h]
enum {
	WORK_STRUCT_PENDING_BIT	= 0,	/* work item is pending execution */
	WORK_STRUCT_DELAYED_BIT	= 1,	/* work item is delayed */
	WORK_STRUCT_PWQ_BIT	= 2,	/* data points to pwq */
	WORK_STRUCT_LINKED_BIT	= 3,	/* next work is linked to this one */
	WORK_STRUCT_COLOR_SHIFT	= 4,	/* color for workqueue flushing */
	WORK_STRUCT_COLOR_BITS	= 4,
    ...
	WORK_OFFQ_FLAG_BITS	= 1,
	...
};

以32bit的CPU来说,当data字段包含WORK_STRUCT_PWQ_BIT标志位时,表示高比特位域保存着上一次pool_workqueue数据结构指针,这时低8位用于存放一些标志位。当data字段没有包含WORK_STRUCT_PWQ_BIT标志位时,表示其高比特位域存放上次执行该work的worker_pool的ID号低5位用于存放一些标志位,见get_work_pool()函数。

常见的标志位如下。

  • WORK_STRUCT_PENDING_BIT: 表示该work正在pending执行, 已经在工作队列中。
  • WORK_STRUCT_DELAYED_BIT: 表示该work被延迟执行了。
  • WORK_STRUCT_PWQ_BIT: 表示work的data成员指向pwqs数据结构的指针,其中pwqs需要按照256Byte对齐,这样pwqs指针的低8位可以忽略,只需要其余的比特位就可以找回pwqs指针。 struct pool_workqueue数据结构按照256Byte对齐。
  • WORK_STRUCT_LINKED_BIT:表示下一个work连接到该work上。

3.2 schedule_work()调度work

总结:

调度一个work: schedule_work(work_struct), 将work挂入系统默认的BOUND类型的workqueue工作队列system_wq

(2) 关闭本地中断

(3) work已经有WORK_STRUCT_PENDING_BIT标志位, 说明该work正在pending执行, 已经在队列中, 不用重复添加, 恢复本地中断并返回, 否则设置该标志位, 继续

(4) 找到一个合适的pool_workqueue. 优先选择本地CPU本地CPU的node节点对应的pool_workqueue,如果该work上次执行的worker_pool和刚选择的pwq->pool不等, 并且该work正在其上次执行的工作线程池运行,而且运行这个work的worker对应的pwq对应的workqueue等于调度传入的workqueue(worker->current_pwq->wq == wq), 则优先选择这个正在运行的worker->current_pwq. 利用其缓存热度.

(5) 判断当前pool_workqueue活跃的work数量, 少于最高限值, 加入pwq->pool->worklist(pool的pending链表), 否则加入pwq->delayed_works(pwq的被延迟执行的works链表)

(6) 当前pwq->pool工作线程池存在pending状态的work并且pool中正运行的worker数量为0的话, 找到pool中第一个idle的worker并唤醒worker->task

初始化完一个work后,就可以调用schedule_work()函数来把work挂入系统的默认的workqueue中。

[include/linux/workqueue.h]
static inline bool schedule_work(struct work_struct *work)
{
	return queue_work(system_wq, work);
}

schedule_work()函数把work挂入系统默认BOUND类型的工作队列system_wq中,该工作队列是在init_workqueues()时创建的。

[schedule_work() - >queue_work()]
[include/linux/workqueue.h]
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()有3个参数,其中WORK_CPU_UNBOUND表示不绑定到任何CPU上,建议使用本地CPU。WORK_CPU_UNBOUND宏容易让人产生混淆,其定义为NR_CPUS。wq指工作队列work是新创建的工作

[schedule_work() ->queue_work() ->queue_work_on()]
[kernel/workqueue.c]
bool queue_work_on(int cpu, struct workqueue_struct *wq,
		   struct work_struct *work)
{
	bool ret = false;
	unsigned long flags;

	local_irq_save(flags);

	if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
		__queue_work(cpu, wq, work);
		ret = true;
	}

	local_irq_restore(flags);
	return ret;
}
EXPORT_SYMBOL(queue_work_on);

work加入工作队列中是在关闭本地中断下运行的。如果开中断,那么有可能在处理中断返回时调度其他进程其他进程有可能调用cancel_delayed_work()把PENDING位偷走,这种情况在稍后介绍cancel_delayed_work()时再详细描述。

如果该work己经设置WORK_STRUCT_PENDING_BIT标志位,说明该work己经在工作队列中,不需要重复添加。

test_and_set_bit()函数设置WORK_STRUCT_PENDING_BIT标志位并返回旧值

[schedule_work() ->queue_work() ->queue_work_on() ->__queue_work()]
[kernel/workqueue.c]
static void __queue_work(int cpu, struct workqueue_struct *wq,
			 struct work_struct *work)
{
	struct pool_workqueue *pwq;
	struct worker_pool *last_pool;
	struct list_head *worklist;
	unsigned int work_flags;
	unsigned int req_cpu = cpu;
    // 位置1
	WARN_ON_ONCE(!irqs_disabled());

	debug_work_activate(work);

	/* if draining, only works from the same workqueue are allowed */
	if (unlikely(wq->flags & __WQ_DRAINING) &&
	    WARN_ON_ONCE(!is_chained_work(wq)))
		return;

位置1代码要判断当前运行状态是否处于关中断状态,为什么__queue_work()要运行在关中断的状态下呢?读者可以先思考一下,这个问题稍后讲述cancel_work_sync()函数时再详细介绍。

__WQ_DRAINING标志位表示要销毁workqueue,那么挂入workqueue中所有的work都要处理完毕才能把这个workqueue销毁。在销毁过程中, —般不允许再有新的work加入队列中,有一种特例情况是正在清空work时又触发了一个queue work操作,这种情况被称为chained work。

[__queue_work() ]
retry:
	if (req_cpu == WORK_CPU_UNBOUND)
		cpu = raw_smp_processor_id();

	/* pwq which will be used unless @work is executing elsewhere */
	// 位置4
	if (!(wq->flags & WQ_UNBOUND))
		pwq = per_cpu_ptr(wq->cpu_pwqs, cpu);
	else
		pwq = unbound_pwq_by_node(wq, cpu_to_node(cpu));
    // 位置2
	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);
	}
    // 位置3
	if (unlikely(!pwq->refcnt)) {
		if (wq->flags & WQ_UNBOUND) {
			spin_unlock(&pwq->pool->lock);
			cpu_relax();
			goto retry;
		}
		/* oops */
		WARN_ONCE(true, "workqueue: per-cpu pwq for %s on cpu%d has 0 refcnt",
			  wq->name, cpu);
	}

pool_workqueue数据结构是桥梁枢纽,想把work加入到workqueue中,首先需要找到一个合适的pool_workqueue枢纽。对于BOUND类型workqueue,直接使用本地CPU对应的pool_workqueue枢纽; 如果是UNOUND类型的workqueue,调用unbound_pwq_by_node()函数来寻找本地node节点对应的UNBOUND类型的pool_workqueue.

[kernel/workqueue.c]
static struct pool_workqueue *unbound_pwq_by_node(struct workqueue_struct *wq,
						  int node)
{
	return rcu_dereference_raw(wq->numa_pwq_tbl[node]);
}

对于UNBOUND类型workqueueworkqueue_struct数据结构中的numa_pwq_tbl[]数组存放着每个系统node节点(!!!)对应的UNBOUND类型的pool_workqueue枢纽。

位置2整块代码,每个work_struct数据结构的data成员可以用于记录worker_pool的ID号,那么get_work_pool()函数可以用于查询该work上一次是在哪个worker_pool中运行的。

[kernel/workqueue.c]
static struct worker_pool *get_work_pool(struct work_struct *work)
{
	unsigned long data = atomic_long_read(&work->data);
	int pool_id;

	pool_id = data >> WORK_OFFQ_POOL_SHIFT;
	if (pool_id == WORK_OFFQ_POOL_NONE)
		return NULL;

	return idr_find(&worker_pool_idr, pool_id);
}

位置2代码,返回该work上一次运行的worker_pool。这里有一种情况,就是发现上一次运行的worker_pool这一次运行该work的pwq->pool不一致。例如上一次是在CPU0对应的worker_pool,这一次是在CPU1上的worker_pool,这种情况下就要考查work是不是正运行在CPU0的worker_pool中的某个工作线程里。如果,那么这次work应该继续添加到CPU0上的worker_pool上。find_worker_executing_work()判断一个work是否在某个worker_pool上正在运行,如果是,则返回这个正在执行的工作线程,这样可以利用其缓存热度

static struct worker *find_worker_executing_work(struct worker_pool *pool,
						 struct work_struct *work)
{
	struct worker *worker;

	hash_for_each_possible(pool->busy_hash, worker, hentry,
			       (unsigned long)work)
		if (worker->current_work == work &&
		    worker->current_func == work->func)
			return worker;

	return NULL;
}

到了位置3代码处,这时pool_workqueue应该已确定,要么是位置4代码通过本地CPU或node节点找到了pool_workqueue; 要么是上一次的last pool_workqueue。但是对于UNBOUND类型workqueue来说,对UNBOUND类型的pool_workqueue的释放是异步的,因此这里有一个refcnt计数成员,当pool_workqueue->refcnt减少到0时,说明该pool_workqueue己经被释放,那么只能跳转到retry标签处重新选择pool_workqueue。接下来继续看__queue_work()函数。

[__queue_work()]
    // 位置5
	if (likely(pwq->nr_active < pwq->max_active)) {
		trace_workqueue_activate_work(work);
		pwq->nr_active++;
		worklist = &pwq->pool->worklist;
	} else {
		work_flags |= WORK_STRUCT_DELAYED;
		worklist = &pwq->delayed_works;
	}

	insert_work(pwq, work, worklist, work_flags);

	spin_unlock(&pwq->pool->lock);
}

位置5代码,判断当前的pool_workqueue活跃的work数量,如果少于最高限值,就加入pending链表worker_pool->worklist中,否则加入pool_workqueue->delayed_works链表(被延迟执行的works)中。

[__queue_work() -> 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;

	/* we own @work, set data and link */
	// 位置1
	set_work_pwq(work, pwq, extra_flags);
	// 位置2
	list_add_tail(&work->entry, head);
	// 位置3
	get_pwq(pwq);
    // 位置4
	smp_mb();
    // 位置5
	if (__need_more_worker(pool))
		wake_up_worker(pool);
}

位置1, set_work_pwq()是设置work_struct数据结构中的data成员,把pwq指针的值和一些flags设置到data成员中,方便下一次再调用queue_work()函数把该work重新加入时,可以很方便地知道本次使用哪个pool_workqueue, 见get_work_pwq()函数。

位置2, 将 work加入worker_pool相应的链表中。

位置3代码,get_pwq()增加pool_workqueue ->refcnt成员引用计数,它和put_pwq()是配对使用的。

位置4代码,smp_mb()内存屏障指令保证wake_up_worker()唤醒worker时,在__schedule()->wq_worker_sleeping()函数中看到这里的list_add_tail()添加链表己经完成。另外也保证位置5代码的__need_more_worker()函数去读取worker_pool->nr_running成员时,list_add_tail()添加链表己经完成。

位置5, 判断当前pwq->pool工作线程池是否需要更多工作线程worker(pool中存在pending状态的work并且pool中正在运行的worker数量为0), 是的话, 找到pool中第一个idle的worker并唤醒worker->task

至此,驱动开发者调用schedule_work()函数己经把work加入workqueue中,虽然函数名叫作schedule_work,但并没有开始实质调度work执行,它只是把work加入workqueuePENDING链表中而己。

  • 加入workqueue的 PENDING链表是关中断的环境下进行的。
  • 设置work->data成员的WORK_STRUCT_PENDING_BIT标志位
  • 寻找合适的pool_workqueue。优先选择本地CPU本地CPU的node节点对应的pool_workqueue,如果该work上次执行的worker_pool和刚选择的pwq->pool不等, 并且该work正在其上次执行的工作线程池运行,而且运行这个work的worker对应的pwq对应的workqueue等于调度传入的workqueue(worker->current_pwq->wq == wq), 则优先选择这个正在运行的worker->current_pwq. 利用其缓存热度.
  • 找到pool_workqueue,也就找到对应的worker_pool对应的PENDING链表
  • 小心处理SMP并发情况。

3.3 工作线程处理函数worker_thread()

接下来看工作线程是如何处理work的。

........

worker_thread()中第39行中的keep_eorking()函数, 其实是控制活跃工作线程数量的.

static bool keep_working(struct worker_pool *pool)
{
	return !list_empty(&pool->worklist) &&
		atomic_read(&pool->nr_running) <= 1;
}

这里判断条件比较简单,如果pool->worklist还有工作需要处理工作线程池活跃的线程小于等于1,那么保持当前工作线程继续工作,此功能可以防止工作线程泛滥。为什么限定活跃的工作线程数量小于等于1呢?在一个CPU上限定一个活跃工作线程的方法比较简单,当然这里没有考虑CPU上线程工作池的负载情况(例如一个CPU上有5个任务,假设它们的权重都是1024,其中3个work类型任务,那么这3个work分布在3个线程和在1个线程中运行,哪种方式能够最快执行完成?)。

简化后的代码逻辑如下:

worker_thread()
{
recheck:
    if(不需要更多的工作线程?)
        goto 睡眠;
        
    if(需要创建更多的工作线程? && 创建线程)
        goto recheck;
        
    do{
        处理工作;
    }(还有工作待完成 && 活跃的工作线程 <= 1)
    
睡眠:
    schedule();
}

至此一个work的执行过程已介绍完毕,对工作线程worker总结如下。

  • 动态地创建管理一个工作线程池中的工作线程。假如发现有PENDING的work当前工作池没有正在运行的工作线程(worker_pool->nr_running = 0),那就唤醒idle状态的线程,否则就动态创建一个工作线程
  • 如果发现一个work己经在同一个工作池另外一个工作线程执行了,那就不处理该work
  • 动态管理活跃工作线程数量,见keep_working()函数。

4 取消一个work

5 和调度器的交互

CMWQ机制动态地调整一个线程池中工作线程的执行情况不会因为某一个work回调函数执行了阻塞操作影响到整个线程池中其他work的执行。

假设某个work的回调函数func()中执行了睡眠操作,例如调用wait_event_interruptible()函数去睡眠,在wait_event_interruptible()函数中会设置当前进程的state为TASK_INTERRUPTIBLE,然后执行schedule()切换进程。

[kernel/sched/core.c]
static void __sched __schedule(void)
{
    ...
	if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
		if (unlikely(signal_pending_state(prev->state, prev))) {
			prev->state = TASK_RUNNING;
		} else {
			deactivate_task(rq, prev, DEQUEUE_SLEEP);
			prev->on_rq = 0;

			/*
			 * If a worker went to sleep, notify and ask workqueue
			 * whether it wants to wake up a task to maintain
			 * concurrency.
			 */
			// 位置1
			if (prev->flags & PF_WQ_WORKER) {
				struct task_struct *to_wakeup;
                // 重点
				to_wakeup = wq_worker_sleeping(prev, cpu);
				if (to_wakeup)
					try_to_wake_up_local(to_wakeup);
			}
		}
		switch_count = &prev->nvcsw;
	}
	...
}

在__schedule()函数中,prev指当前进程,即执行work的工作线程,它的state状态为TASK_INTERRUPTIBLE(其值为1), 另外这次调度不是中断返回前的抢占调度,preempt_count也没有设置PREEMPT_ACTIVE,因此会运行到位置1代码处。

一个工作线程要被调度器换出时,调用wq_worker_sleeping()看看是否需要唤醒同一个线程池中的其他内核线程

[kernel/workqueue.c]
struct task_struct *wq_worker_sleeping(struct task_struct *task, int cpu)
{
	struct worker *worker = kthread_data(task), *to_wakeup = NULL;
	struct worker_pool *pool;

	pool = worker->pool;

	if (atomic_dec_and_test(&pool->nr_running) &&
	    !list_empty(&pool->worklist))
		to_wakeup = first_idle_worker(pool);
	return to_wakeup ? to_wakeup->task : NULL;
}

当前的工作线程马上要被换出(睡眠),因此先把worker_pool->nr_running引用计数减1,然后判断该计数是否为0, 为0则说明当前线程池也没有活跃的工作线程没有活跃的工作线程当前线程池的等待队列中还有work需要处理,那么就必须要去找一个idle的工作线程来唤醒它。first_idle_worker()函数比较简单,从pool->idle_list链表中取一个idle的工作线程即可。

找到一个idle工作线程,调用try_to_wake_up_local()去唤醒idle工作线程

在唤醒一个工作线程时,需要增加worker_pool-> nr_running引用计数来告诉workqueue机制现在有一个工作线程要被唤醒了。

[__schedule() ->try_to_wake_up_local() ->ttwu_activate()]
[kernel/sched/core.c]
static void ttwu_activate(struct rq *rq, struct task_struct *p, int en_flags)
{
	activate_task(rq, p, en_flags);
	p->on_rq = TASK_ON_RQ_QUEUED;

	/* if a worker is waking up, notify workqueue */
	if (p->flags & PF_WQ_WORKER)
		wq_worker_waking_up(p, cpu_of(rq));
}

wq_worker_waking_up()函数增加pool->nr_running引用计数,表示有一个工作线程马上就会被唤醒,可以投入工作了。

[kernel/workqueue.c]
void wq_worker_waking_up(struct task_struct *task, int cpu)
{
	struct worker *worker = kthread_data(task);

	if (!(worker->flags & WORKER_NOT_RUNNING)) {
		WARN_ON_ONCE(worker->pool->cpu != cpu);
		atomic_inc(&worker->pool->nr_running);
	}
}

worker_pool->nr_running引用计数在workqueue机制中起到非常重要的作用,它是workqueue机制和进程调度器之间的桥梁枢纽。下面来看引用计数:

[kernel/workqueue.c]
struct worker_pool {
    atomic_t		nr_running ____cacheline_aligned_in_smp;
	...
}____cacheline_aligned_in_smp;

worker_pool数据结构按照cacheline对齐,而nr_running成员也是要求和cacheline对齐,因为系统上每个CPU都有可能访问到这个变量,例如前面看到的schedule()函数和try_to_wake_up()函数,把这个成员放到单独一个cacheline中,有利于提高效率。

  • 工作线程进入执行时会增加nr_running 计数,见worker_thread()-〉worker_clr_flags()函数。
  • 工作线程退出执行时会减少nr_running 计数,见worker_thread()-〉worker_set_flags()函数。
  • 工作线程进入睡眠时会减少nr_running计数,见__schedule()函数。
  • 工作线程被唤醒时会增加nr_running计数,见ttwu_activate()函数。

6 小结

6.1 背景和原理

工作队列的基本原理是把work(需要推迟执行的函数)交由一个内核线程来执行,它总是在进程上下文中执行。工作队列的优点是利用进程上下文来执行中断下半部操作,因此工作队列允许重新调度睡眠,是异步执行的进程上下文,另外它还能解决软中断tasklet执行时间过长导致系统实时性下降等问题。

早起workqueue比较简单, 由多线程(Multi threaded,每个CPU默认一个工作线程)和单线程(Single threaded, 用户可以自行创建工作线程)组成. 容易导致①内核线程数量太多 ②并发性差(工作线程和CPU是绑定的) ③死锁(同一个队列上的数据有依赖容易死锁)

concurrency-managed workqueues(CMWQ): BOUND类型(Per-CPU, 每个CPU一个)和UNBOUND类型, 每种都有两个工作线程池(worker-pool): 普通优先级的工作(work)使用和高优先级的工作(work)使用. 工作线程池(worker-pool)中线程数量是动态管理的. 工作线程睡眠时, 检查是否需要唤醒更多工作线程, 有需要则唤醒同一个工作线程池中idle状态的工作线程.

6.2 数据结构

工作任务struct work_struct

[include/linux/workqueue.h]
struct work_struct {
    //低比特位是标志位, 剩余存放上一次运行的worker_pool的ID或pool_workqueue的指针(由WORK_STRUCT_PWQ标志位来决定)
	atomic_long_t data;
	// 把work挂到其他队列上
	struct list_head entry;
	// 工作任务处理函数
	work_func_t func;
};

工作线程struct worker

[kernel/workqueue_internal.h]
struct worker {
	/* on idle list while idle, on busy hash table while busy */
	union {
		struct list_head	entry;	/* L: while idle */
		struct hlist_node	hentry;	/* L: while busy */
	};
    // 正在被处理的work
	struct work_struct	    *current_work;	/* L: work being processed */
	// 正在执行的work回调函数
	work_func_t		        current_func;	/* L: current_work's fn */
	// 当前work所属的pool_workqueue
	struct pool_workqueue	*current_pwq;   /* L: current_work's pwq */
	// 所有被调度并正准备执行的work都挂入该链表
	struct list_head	    scheduled;	    /* L: scheduled works */
    // 挂入到worker_pool->workers链表
    struct list_head	    node;		    /* A: anchored at pool->workers */
						                    /* A: runs through worker->node */
	// 工作线程的task_struct
	struct task_struct	    *task;		    /* I: worker task */
	// 该工作线程所属的worker_pool
	struct worker_pool	    *pool;		    /* I: the associated pool */
	// 该工作线程的ID号
	int			            id;		        /* I: worker id */
    ...
};

工作线程池struct worker_pool

[kernel/workqueue.c]
struct worker_pool {
    // 保护worker-pool的自旋锁
	spinlock_t		lock;		/* the pool lock */
	// BOUND类型的workqueue,cpu表示绑定的CPU ID; UNBOUND类型,该值为-1
	int			    cpu;		/* I: the associated cpu */
	// UNBOUND类型的workqueue,表示该worker-pool所属内存节点的ID编号
	int			    node;		/* I: the associated node ID */
	// ID号
	int			    id;		    /* I: pool ID */
	unsigned int	flags;		/* X: flags */
    // pending状态的work会挂入该链表
	struct list_head	worklist;	/* L: list of pending works */
	// 工作线程的数量
	int			        nr_workers;	/* L: total number of workers */
	// idle状态的工作线程的数量
	int			        nr_idle;	/* L: currently idle ones */
    // idle状态的工作线程挂入该链表
	struct list_head	idle_list;	/* X: list of idle workers */
	// 被管理的工作线程会挂入该链表
	struct list_head	workers;	/* A: attached workers */
	// 工作线程的属性
	struct workqueue_attrs	*attrs;		/* I: worker attributes */
	// 正在运行中的worker数量
	atomic_t		nr_running ____cacheline_aligned_in_smp;
	// rcu锁
	struct rcu_head		rcu;
	...
} ____cacheline_aligned_in_smp;

连接workqueue(工作队列)和worker_pool(工作线程池)的桥梁struct pool_workqueue

[kernel/workqueue.c]
struct pool_workqueue {
    // worker_pool指针
	struct worker_pool	    *pool;		/* I: the associated pool */
	// 工作队列
	struct workqueue_struct *wq;		/* I: the owning workqueue */
	// 活跃的work数量
	int			            nr_active;	/* L: nr of active works */
	// 活跃的work最大数量
	int			            max_active;	/* L: max active works */
	// 被延迟执行的works挂入该链表
	struct list_head	    delayed_works;	/* L: delayed works */
	struct rcu_head		    rcu;
	...
} __aligned(1 << WORK_STRUCT_FLAG_BITS);

工作队列struct workqueue_struct

[kernel/workqueue.c]
struct workqueue_struct {
    // 所有的pool-workqueue数据结构都挂入链表
	struct list_head	pwqs;		/* WR: all pwqs of this wq */
	// 链表节点。当前workqueue挂入全局的链表workqueues
	struct list_head	list;		/* PL: list of all workqueues */
    // 所有rescue状态下的pool-workqueue数据结构挂入该链表
	struct list_head	maydays;	/* MD: pwqs requesting rescue */
	// rescue内核线程. 创建workqueue时设置WQ_MEM_RECLAIM, 那么内存紧张而创建新的工作线程失败会被该线程接管
	struct worker		*rescuer;	/* I: rescue worker */
    // UNBOUND类型属性
	struct workqueue_attrs	*unbound_attrs;	/* WQ: only for unbound wqs */
	// UNBOUND类型的pool_workqueue
	struct pool_workqueue	*dfl_pwq;	/* WQ: only for unbound wqs */
    // workqueue的名字
	char			name[WQ_NAME_LEN]; /* I: workqueue name */

	/* hot fields used during command issue, aligned to cacheline */
	unsigned int		flags ____cacheline_aligned; /* WQ: WQ_* flags */
	//Per-CPU类型的pool_workqueue
	struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */
	...
};

关系图:

一个work挂入workqueue中,最终还要通过worker-pool中的工作线程来处理其回调函数,worker-pool是系统共享的(!!!),因此workqueue需要查找到一个合适的worker-pool,然后从worker-pool中分派一个合适的工作线程,pool_workqueue数据结构在其中起到桥梁作用。这有些类似IT类公司的人力资源池的概念,具体关系如图5.7所示。

3. workqueue工作队列_第3张图片

3. workqueue工作队列_第4张图片

  • work_struct结构体代表的是一个任务,它指向一个待异步执行的函数,不管驱动还是子系统什么时候要执行这个函数,都必须把work加入到一个workqueue

  • worker结构体代表一个工作者线程(worker thread),它主要一个接一个的执行挂入到队列中的work,如果没有work了,那么工作者线程就挂起,这些工作者线程被worker-pool管理。

对于驱动和子系统的开发人员来说,接触到的只有work,而背后的处理机制是管理worker-pool和处理挂入的work。

  • worker_pool结构体用来管理worker,对于每一种worker pool都分两种情况:一种是处理普通work,另一种是处理高优先级的work

  • workqueue_struct结构体代表的是工作队列,工作队列分unbound workqueuebound workqueue。bound workqueue就是绑定到cpu上的,挂入到此队列中的work只会在相对应的cpu上运行。unbound workqueue不绑定到特定的cpu,而且后台线程池的数量也是动态的,具体workqueue关联到哪个worker pool,这是由workqueue_attrs决定的。

系统初始化阶段(init_workqueue()): 为所有CPU(包括离线的)创建两个工作线程池worker_pool(普通优先级和高优先级); 为每个在线CPU的每个工作线程池(每个CPU有两个)创建一个工作线程(create_worker); 创建UNBOUND类型和ordered类型的workqueue属性, 分别两个, 对应普通优先级和高优先级, 供后续使用; alloc_workqueue()创建几个默认的workqueue

  • 普通优先级BOUND类型工作队列system_wq, 名称为“events”,可以理解为默认工作队列
  • 高优先级BOUND类型的工作队列system_highpri_wq ,名称为“events_highpri”。
  • UNBOUND类型的工作队列system_unbound_wq,名称为“system_unbound_wq”。
  • Freezable类型的工作队列system_freezable_wq,名称为“events_freezable”。
  • 省电类型的工作队列system_freezable_wq,名称为 “events_power_efficient”。

创建工作线程worker(参数是worker_pool): 获取一个ID; 工作线程池对应的内存节点分配一个worker; 在工作线程池对应的node创建一个内核线程, 名字("kworker/u + CPU_ID + : + worker_idH", 高优先级的才有H, UNBOUND类型的才有u); 设置线程(worker->task->flags)的PF_NO_SETAFFINITY标志位(防止修改CPU亲和性); 工作线程池没有绑定到CPU上, 那么设置worker标志位不绑定CPU; 将worker加到工作线程池的workers链表; 使worker进入idle状态; 唤醒worker的内核线程; 返回该worker

创建工作队列workqueue: API很多, 3个参数name, flags和max_active.

(1) 分配一个workqueue_struct并初始化, 对于UNBOUND类型, 创建一个UNBOUND类型的workqueue属性

(2) 分配pool_workqueue并初始化alloc_and_link_pwqs()

BOUND类型的workqueue:

  • 每个CPU分配一个Per-CPU的pool_workqueue,
  • 遍历每个CPU, 通过这个pwq将系统静态定义的Per-CPU类型的高优先级的worker_pool(也就是init_workqueues()初始化的)和workqueue连接起来, 并将这个pool_workqueue添加到传入的workqueue->pwqs链表.

UNBOUND类型和ORDERED类型的workqueue: 都是调用apply_workqueue_attrs实现, 不同在于传入的属性一个是ordered_wq_attrs[highpri], 一个是unbound_std_wq_attrs[highpri], 这两个不同在于属性里面的no_numa在ordered中是true, 这两个属性系统初始化阶段完成的.

  • 通过系统全局哈希表unbound_pool_hash(管理所有UNBOUND类型的work_pool)根据属性查找worker_pool, 找到将其引用计数加1, 并返回, 没有的话重新分配并初始化一个(创建pool, 为pool创建一个工作线程worker<会唤醒线程>), 将新pool加入哈希表
  • 分配一个pool_workqueue
  • 初始化该pwq, 将worker_pool和workqueue_struct连接起来, 为pool_workqueue初始化一个工作work(通过INIT_WORK()), 回调函数是pwq_unbound_release_workfn(), 该work执行: 从work中找到相应的pwq, 该work只对UNBOUND类型的workqueue有效, 如果work->pwq->wq->pwqs(所有pool_workqueue都在这个链表)中当前pool_workqueue是最后一个, 释放pool_workqueue相关结构

初始化一个work: 宏INIT_WORK(work, func)

调度一个work: schedule_work(work_struct), 将work挂入系统默认的BOUND类型的workqueue工作队列system_wq, queue_work(workqueue, work)

(1) 关中断

(2) 设置work标志位WORK_STRUCT_PENDING_BIT, 已经有说明正在pending, 已经在队列中, 不用重复添加

(3) 找一个合适的pool_workqueue. 优先本地CPU或本地CPU的node节点对应的pwq, 如果该work上次执行的worker_pool和刚选择的pwq->pool不等, 并且该work正在其上次执行的工作线程池中运行,而且运行这个work的worker对应的pwq对应的workqueue等于调度传入的workqueue(worker->current_pwq->wq == wq), 则优先选择这个正在运行的worker->current_pwq. 利用其缓存热度.

(4) 判断当前pwq活跃的work数量, 少于最高限值, 加入pwq->pool->worklist(pool的pending链表), 否则加入pwq->delayed_works(pwq的被延迟执行的works链表)

(5) 当前pwq->pool工作线程池存在pending状态的work并且pool中正运行的worker数量为0的话, 找到pool中第一个idle的worker并唤醒worker->task

(6) 开中断

工作线程处理函数worker_thread():

worker_thread()
{
recheck:
    if(不需要更多的工作线程?)
        goto 睡眠;
        
    if(需要创建更多的工作线程? && 创建线程)
        goto recheck;
        
    do{
        处理工作;
    }(还有工作待完成 && 活跃的工作线程 <= 1) // 这儿就是keep_working(pool)
    
睡眠:
    schedule();
}
  • 动态地创建和管理一个工作线程池中的工作线程。假如发现有PENDING的work且当前工作池中没有正在运行的工作线程(worker_pool->nr_running = 0),那就唤醒idle状态的线程,否则就动态创建一个工作线程。
  • 如果发现一个work己经在同一个工作池的另外一个工作线程执行了,那就不处理该work。
  • 动态管理活跃工作线程数量,见keep_working()函数。如果pool->worklist中还有工作需要处理且工作线程池中活跃的线程小于等于1,那么保持当前工作线程继续工作,此功能可以防止工作线程泛滥。也就是限定活跃的工作线程数量小于等于1.

和调度器交互:

CMWQ机制会动态地调整一个线程池中工作线程的执行情况,不会因为某一个work回调函数执行了阻塞操作而影响到整个线程池中其他work的执行。

某个work的回调函数func()中执行了睡眠操作(设置当前进程state为TASK_INTERRUPTIBLE, 然后执行schedule()切换), 在schedule()中, 判断进程的flags是否有PF_WQ_WORKER(属于worker线程), 有的话:

(1) 将当前worker的worker_pool中nr_running引用计数减1, 如果为0则说明当前线程池没有活跃的工作线程, 而当前线程池的等待队列worklist有work, 那么从pool->idle_list链表拿一个idle的工作线程

(2) 唤醒该工作线程, 增加worker_pool中nr_running引用计数

  • 工作线程进入执行时会增加nr_running 计数,见worker_thread()-〉worker_clr_flags()函数。
  • 工作线程退出执行时会减少nr_running 计数,见worker_thread()-〉worker_set_flags()函数。
  • 工作线程进入睡眠时会减少nr_running计数,见__schedule()函数。
  • 工作线程被唤醒时会增加nr_running计数,见ttwu_activate()函数。

在驱动开发中使用workqueue是比较简单的,特别是使用系统默认的工作队列system_wq, 步骤如下。

  • 使用INIT_WORK()宏声明一个work和该work的回调函数。
  • 调度一个work: schedule_work()。
  • 取消一个work: cancel_work_sync()

此外,有的驱动程序还自己创建一个workqueue,特别是网络子系统块设备子系统等。

  • 使用alloc_workqueue()创建新的workqueue
  • 使用INIT_WORK()宏声明一个work该work的回调函数
  • 新workqueue调度一个work: queue_work()
  • flush workqueue所有work: flush_workqueue()

Linux内核还提供一个workqueue机制timer机制结合的延时机制delayed_work

理解CMWQ机制,首先要明白旧版本的workqueue机制遇到了哪些问题,其次要清楚CMWQ机制中几个重要数据结构的关系. CMWQ机制workqueue划分为BOUND类型UNBOUND类型

如图5.8所示是BOUND类型workqueue机制的架构图,对于BOUND类型的workqueue归纳如下。

  • 每个新建的workqueue,都有一个struct workqueue_struct数据结构来描述。
  • 对于每个新建的(!!!)workqueue每个CPU一个pool_workqueue(!!!)数据结构来连接workqueueworker_pool.
  • 每个CPU只有两个worker_pool数据结构来描述工作池,一个用于普通优先级工作线程,另一个用于高优先级工作线程
  • worker_pool中可以有多个工作线程,动态管理工作线程。
  • worker_poolworkqueue1:N(!!!)的关系,即一个worker_pool可以对应多个workqueue.
  • pool_workqueue是worker_pool和workqueue之间的桥梁枢纽。
  • worker_pool(!!!)和worker工作线程(!!!)也是1:N(!!!)的关系。

BOUND类型的work是在哪个CPU上运行的呢?有几个API接口可以把一个work添加到workqueue上运行,其中schedule_work()函数倾向于使用本地CPU,这样有利于利用CPU的局部性原理提高效率,而queue_work_on()函数可以指定CPU的。

对于UNBOUND类型的workqueue来说,其工作线程没有绑定到某个固定的CPU上。对于UMA机器,它可以在全系统的CPU内运行;对于NUMA机器,每一个node节点创建一个worker_pool

驱动开发中,UNBOUND类型workqueue不太常用,举一个典型的例子,Linux内核中有一个优化启动时间(boot time)的新接口Asynchronous functioncalls,实现是在kernel/asyn.c文件中。对于一些不依赖硬件时序且不需要串行执行的初始化部分,可以采用这个接口,现在电源管理子系统中有一个选项可以把一部分外设在suspend/resume过程中的操作用异步的方式来实现,从而优化其suspend/resume时间,详见kemel/power/main.c 中关于“pm_async_enabled” 的实现。

对于长时间占用CPU资源的一些负载(标记WQ_CPU_INTENSIVE), Linux内核倾向于使用UNBOUND类型的workqueue, 这样可以利用系统进程调度器来优化选择在哪个CPU上运行,例如drivers/md/raid5.c驱动。

如下动态管理技术值得读者仔细品味。

  • 动态管理工作线程数量,包括动态创建工作线程动态管理活跃工作线程等。
  • 动态唤醒工作线程

你可能感兴趣的:(3. workqueue工作队列)