1.概述
工作队列(workqueue)是除了软中断softirq和小任务tasklet以外最常用的一种中断下半部分机制,由内核统一管理。工作队列把推迟执行的任务交给内核线程来执行,其运行在进程上下文,允许重新调度、睡眠。工作队列解决了软中断和tasklet执行时间过长导致系统实时性下降的问题,同时避免了驱动模块自身创建线程导致内核线程过多的问题。
早期的工作队列设计的比较简单,由多线程(Multi threaded,每个CPU默认一个工作线程)和单线程(Single threaded,用户自行创建的工作线程)组成,在使用中,出现了如下问题:
(1)内核线程数量太多。虽然系统中默认有一套工作线程,但有很多工程师系统喜欢自行创建工作线程,对于CPU数量比较多的机器,系统启动完可能就耗尽了PID资源。
(2)并发性比较差。Multi threaded的工作线程和CPU是一一绑定的,某个本地CPU上的工作任务是串行执行的。如某个线程上的工作任务发生了睡眠,之后的工作任务只能等待,前一个任务没有执行完成,则后一个任务永远无法得到执行,也不能迁移到其他空闲CPU上执行。
(3)死锁问题。如果有很多的工作任务运行在系统默认的工作队列上,并且他们有一些数据依赖关系,那么很有可能产生死锁问题。
为了解决上述问题,Linux内核引入了concurrency-managed workqueues(CWMQ),和旧的workqueue接口兼容,明确划分了workqueue的前端接口和后端实现机制。CWMQ提出了工作线程池(worker_pool)概念,不和特定的工作队列关联。工作线程池有两种。一种和具体CPU绑定,为Per-CPU类型,有两个线程池,一个给高优先级的work使用,另一个给低优先级的work使用。另外一种不和具体CPU绑定,可以运行在任意CPU上,工作线程池中的线程是动态分配和管理,线程数量不固定,缺省情况下会创建一个线程来处理工作任务。用户在使用workqueue时,无需关心放在哪个CPU上执行,也无需再创建额外的线程,只需要设置相关flag和优先级,内核会将工作任务放在合适的线程池中执行。当某个工作任务阻塞时,CWMQ会唤醒或创建新的线程执行后续的工作任务,以提高并发效率。
2.工作队列数据结构
工作队列的工作任务用work_struct
描述,是工作队列处理工作任务的最小单位。data
的存放内容由WORK_STRUCT_PWQ
标志位决定。entry
是串联工作任务的链表指针。func
是处理工作任务的回调函数。
关于工作任务work的data成员设置场景(32位CPU):当调用schedule_work
函数将work插入到工作队列中时,将WORK_STRUCT_PENDING
和WORK_STRUCT_PWQ
标记设置到data的低8位,同时将pool_workqueue的指针设置到data的高24位,pool_workqueue结构体按256字节对其,低8位地址可以忽略;当调用process_one_work
函数执行work的回调函数之前,将低8位的标记清除,即清除WORK_STRUCT_PENDING
和WORK_STRUCT_PWQ
标记,将worker_pool
的id设置到data的高27位,低5位用来存放标记。当调用cancel_work_sync
删除work时,清除WORK_STRUCT_PENDING
和WORK_STRUCT_PWQ
标记,将WORK_STRUCT_NO_POOL
标记设置到data中。WORK_STRUCT_PENDING
标记同步work的插入和删除。
[include/linux/workqueue.h]
struct work_struct {
// 低比特部分存放work的标志位,剩余的存放上一次运行worker_pool的ID号或pool_workqueue的指针
atomic_long_t data;
struct list_head entry; // 串联工作任务的指针
work_func_t func; // 工作任务的回调函数
......
};
// 工作任务的回调函数类型
typedef void (*work_func_t)(struct work_struct *work);
处理工作任务的工作线程用worker
描述,每个worker
都对应一个内核线程。worker
根据工作状态,可以添加到worker_pool
的空闲链表和忙碌链表中。处于空闲状态的workr
收到工作处理请求后,将唤醒workr
描述的内核线程。
[kernel/workqueue_internal.h]
struct worker {
union {
/* 如果worker处于idle状态,则将entry挂到worker_pool的idle_list链表中 */
struct list_head entry;
/* 如果worker处于busy状态,则将hentry添加到worker_pool的busy_hash哈希表中 */
struct hlist_node hentry;
};
struct work_struct *current_work; /* 当前正在处理的工作 */
work_func_t current_func; /* 当前正在执行的work回调函数 */
struct pool_workqueue *current_pwq; /* 当前work所属的pool_workqueue */
bool desc_valid; /* 字符数组desc是否有效 */
/* 所有被调度并正准备执行的work都挂入该链表中,只要挂入此链表中的工作任务会被worker处理 */
struct list_head scheduled;
struct task_struct *task; /* 该工作线程的task_struct结构体,调度的实体 */
struct worker_pool *pool; /* 该工作线程所属的worker_pool */
struct list_head node; /* 挂到worker_pool->workers链表中 */
/* 最近一次运行的时间戳,用于判定该工作者线程是否可以被destory时使用 */
unsigned long last_active;
unsigned int flags; /* 标志位 */
int id; /* 工作线程的ID号,用ps命令在用户空间可以看到具体的值 */
char desc[WORKER_DESC_LEN];/* 工作线程的描述说明*/
struct workqueue_struct *rescue_wq; /* I: the workqueue to rescue */
};
工作线程池使用worker_pool
描述,管理多个worker
描述的内核线程。针对和CPU绑定的工作队列,worker_pool
是Per-CPU类型,每个CPU都有两个worker_pool
,对应不同的优先级,nice值分别为0和-20。针对不和CPU绑定的工作队列,worker_pool
创建后会添加到unbound_pool_hash
哈希表中。worker_pool
管理一个空闲链表和忙碌链表,其中忙碌链表由哈希表管理。DEFINE_PER_CPU_SHARED_ALIGNED
定义了2个Per-CPU类型的worker_pool
,NR_STD_WORKER_POOLS
为2。
[kernel/workqueue.c]
struct worker_pool {
spinlock_t lock; /* 保护worker_pool的自旋锁 */
int cpu; /* 绑定类型-绑定的CPU ID,非绑定类型为-1 */
int node; /* 非绑定类型-表示该worker_pool所属内存节点的ID编号 */
int id; /* I: worker_pool的ID号 */
unsigned int flags; /* X: flags */
unsigned long watchdog_ts; /* L: watchdog timestamp */
struct list_head worklist; /* pending状态的工作任务挂到此链表中 */
int nr_workers; /* 线程(worker)数量 */
int nr_idle; /* 空闲线程数量 */
struct list_head idle_list; /* idle状态的线程挂到此链表中 */
struct timer_list idle_timer; /* L: 任务的空闲超时时间 */
struct timer_list mayday_timer; /* L: SOS timer for workers */
DECLARE_HASHTABLE(busy_hash, BUSY_WORKER_HASH_ORDER); /* busy状态的线程添加到本哈希表中 */
struct mutex manager_arb; /* manager arbitration */
struct worker *manager; /* L: purely informational */
struct mutex attach_mutex; /* worker绑定和解绑的互斥锁 */
struct list_head workers; /* worker_pool管理的所有worker添加到本链表中 */
struct completion *detach_completion; /* all workers detached */
struct workqueue_attrs *attrs; /* 工作线程的属性 */
struct hlist_node hash_node; /* 用于添加到unbound_pool_hash中 */
// 用于管理worker线程的创建和销毁,表示正在运行中的worker数量
atomic_t nr_running ____cacheline_aligned_in_smp;
} ____cacheline_aligned_in_smp // SMP系统中高速缓存对齐
// 静态定义和CPU绑定的worker_pool,每个CPU有两个,名称为cpu_worker_pools
static DEFINE_PER_CPU_SHARED_ALIGNED(struct worker_pool [NR_STD_WORKER_POOLS], cpu_worker_pools);
pool_workqueue
数据结构充当纽带作用,用于将workqueue_struct
和worker_pool
连接起来。pool_workqueue
结构体分配内存时按256字节对齐,内存地址的低8位可以存放其他内容。
[kernel/workqueue.c]
struct pool_workqueue {
struct worker_pool *pool; /* 指向worker_pool的指针 */
struct workqueue_struct *wq; /* 指向所属的工作队列 */
int work_color; /* L: current color */
int flush_color; /* L: flushing color */
int refcnt; /* 引用计数,若为0,则说明此pool_workqueue将被释放 */
int nr_in_flight[WORK_NR_COLORS]; /* L: nr of in_flight works */
int nr_active; /* 活跃的works数量 */
int max_active; /* 活跃的works最大数量 */
struct list_head delayed_works; /* 延迟的works可以挂入该链表 */
struct list_head pwqs_node; /* WR: node on wq->pwqs */
struct list_head mayday_node; /* MD: node on wq->maydays */
struct work_struct unbound_release_work;
struct rcu_head rcu;
} __aligned(1 << WORK_STRUCT_FLAG_BITS) // 256字节对齐
系统中所有不和CPU绑定的工作队列,包括系统默认的工作队列,例如system_wq
、system_highpri_wq
等,以及驱动开发者新创建的工作队列,共享一组worker_pool
。对于绑定CPU的工作队列,每个CPU拥有两个线程池,每个线程池可以对应多个工作队列。工作队列由workqueue_struct
描述。
[kernel/workqueue.c]
struct workqueue_struct {
struct list_head pwqs; /* 所有的pool-workqueue数据结构都挂入该链表 */
struct list_head list; /* 系统定义了全局的链表workqueue,所有workqueue挂入该链表 */
struct list_head maydays; /* 所有rescue状态下的pool-workqueue挂入该链表 */
/* rescue内核线程。内存紧张时创建新的工作线程可能会失败,如果创建workqueue时设置了WQ_MEM_RECLAIM,
那么rescuer线程会接管这种情况 */
struct worker *rescuer;
struct workqueue_attrs *unbound_attrs; /* 未绑定CPU工作队列属性 */
struct pool_workqueue *dfl_pwq; /* 指向未绑定CPU工作队列的pool_workqueue */
char name[WQ_NAME_LEN]; /* 工作队列的名字 */
/* 标志位经常被不同的CPU访问,因此要和cache line对其。标志位包括WQ_UNBOUND、
WQ_HIGHPRI、WQ_FREEZABLE等 */
unsigned int flags ____cacheline_aligned; /* WQ: WQ_* flags */
struct pool_workqueue __percpu *cpu_pwqs; /* 指向Per-CPU类型的pool_workqueue */
};
BOUND类型的工作队列数据结构关系如下图所示。
UNBOUND类型的工作队列数据结构关系如下图所示,工作线程池、工作线程、工作任务之间关系和上图类似。
3.初始化工作队列
Linux内核使用init_workqueues
初始化工作队列。首先遍历系统中可用的CPU,设置CPU的两个线程池worker_pool,一个为普通优先级,线程nice值为0,另一个为高优先级,线程nice值为-20,同时为每个线程池worker_pool分配ID。接着针对每个online CPU(可用的CPU)的线程池创建内核线程并设置线程池标志为~POOL_DISASSOCIATED
,即此线程池内的线程是和CPU绑定的。然后创建UNBOUND工作队列的属性,工作队列属性workqueue_attrs
主要描述内核线程的nice值、cpumask值(允许在哪些CPU上执行)及NUMA系统的亲和性,Per-CPU类型的工作队列只能在一个CPU上执行,nice值采用WQ_HIGHPRI或0来设置,因此Per-CPU类型的工作队列不需要设置workqueue_attrs的属性。最后使用alloc_workqueue
宏创建了系统默认的工作队列。
[include/linux/cpumask.h]
// 遍历系统中可能(cpu_possible_mask)的CPU(最多可以有多少个CPU,包含可热插拔的CPU)
#define for_each_possible_cpu(cpu) for_each_cpu((cpu), cpu_possible_mask)
#define for_each_cpu(cpu, mask) \
for ((cpu) = -1; \
(cpu) = cpumask_next((cpu), (mask)), \
(cpu) < nr_cpu_ids;)
[kernel/workqueue.c]
NR_STD_WORKER_POOLS = 2, /* # standard pools per cpu */
HIGHPRI_NICE_LEVEL = MIN_NICE,
#define MIN_NICE -20
// 遍历和CPU绑定的Per-CPU类型的worker_pool,每个CPU对用两个worker_pool,
// cpu_worker_pools由宏DEFINE_PER_CPU_SHARED_ALIGNED静态定义
#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)++)
init_workqueues // 系统的workqueues的初始化函数
std_nice[NR_STD_WORKER_POOLS] = { 0, HIGHPRI_NICE_LEVEL } // 设置优先级
->alloc_cpumask_var(&wq_unbound_cpumask, GFP_KERNEL) // 分配CPU掩码位图内存
// 将cpu_possible_mask拷贝到wq_unbound_cpumask指向的内存中
->cpumask_copy(wq_unbound_cpumask, cpu_possible_mask)
->KMEM_CACHE // 创建一个pool_workqueue数据结构的slab缓存对象
// 注册CPU事件的通知链,主要用于处理CPU热插拔时候,将该CPU上的工作队列迁移到online的CPU上
// 在CMWQ中,将这种机制叫做trustee
->cpu_notifier(workqueue_cpu_up_callback, CPU_PRI_WORKQUEUE_UP)
->wq_numa_init // 处理NUMA系统的情况
for_each_possible_cpu(cpu) { // 遍历系统中可用(cpu_possible_mask)的CPU
struct worker_pool *pool;
i = 0;
// 遍历并设置Per-CPU类型的worker_pool,一个CPU对应两个worker-pool
for_each_cpu_worker_pool(pool, cpu) {
// 初始化worker-pool,设置工作线程空闲超时时执行的函数为idle_worker_timeout
// 线程空闲的超时时间为300秒
BUG_ON(init_worker_pool(pool));
pool->cpu = cpu; // 设置关联的CPU
cpumask_copy(pool->attrs->cpumask, cpumask_of(cpu)); // 拷贝CPU掩码
// 设置worker-pool线程的nice属性,第一个为0(低优先级),第二个为-20(高优先级)
pool->attrs->nice = std_nice[i++];
pool->node = cpu_to_node(cpu); // 将CPU编号转换为节点
/* 分配worker-pool的ID */
mutex_lock(&wq_pool_mutex);
BUG_ON(worker_pool_assign_id(pool));
mutex_unlock(&wq_pool_mutex);
}
}
// 对每个online CPU的两个线程池分别创建一个线程worker,创建线程的函数为create_worker
for_each_online_cpu(cpu) {
struct worker_pool *pool;
for_each_cpu_worker_pool(pool, cpu) {
// 设置worker_pool的标志,表示此worker_pool和CPU绑定
pool->flags &= ~POOL_DISASSOCIATED;
// 给Per-CPU类型的worker_pool创建内核线程
create_worker(pool)
}
}
// 创建UNBOUND工作队列workqueue_attrs的属性,主要描述内核线程的nice值、cpumask值
// (允许在哪些CPU上执行)及NUMA系统的亲和性。Per-CPU类型的工作队列只能在一个CPU上执行,
// nice值采用WQ_HIGHPRI来设置,因此Per-CPU类型的工作队列不需要设置workqueue_attrs的属性。
// UNBOUND类型工作队列属性包含两种,分别为默认属性和ordered属性,ordered属性的
// workqueue严格按照顺序执行,不存在并发问题。
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; // unbound工作队列默认属性
BUG_ON(!(attrs = alloc_workqueue_attrs(GFP_KERNEL)));
attrs->nice = std_nice[i];
attrs->no_numa = true;
ordered_wq_attrs[i] = attrs; // unbound工作队列ordered属性
}
// 下面开始分配系统默认的工作队列workqueue,创建workqueue的函数为alloc_workqueue
// 普通优先级类型的工作队列system_wq,名称为events,schedule[_delayed]_work[_on]
// 函数添加的work就是填到此工作队列中,system_wq工作队列要求添加的work运行时间不宜过长
system_wq = alloc_workqueue("events", 0, 0);
// system_highpri_wq和system_wq类似,只是添加的work优先级较高
system_highpri_wq = alloc_workqueue("events_highpri", WQ_HIGHPRI, 0);
// system_long_wq和system_wq类似,只是添加的work运行时间较长
system_long_wq = alloc_workqueue("events_long", 0, 0);
// system_unbound_wq是UNBOUND类型的工作队列
system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND,
WQ_UNBOUND_MAX_ACTIVE);
//system_freezable_wq和system_wq类似,可用于工作队列suspend时可冻结的work
system_freezable_wq = alloc_workqueue("events_freezable", WQ_FREEZABLE, 0);
// system_power_efficient_wq可用于节能而牺牲性能的work,当wq_power_efficient使能后,
// system_power_efficient_wq可转为WQ_UNBOUND类型以节省电力消耗
// WQ_POWER_EFFICIENT标记的工作队列默认是Per-CPU类型的
system_power_efficient_wq = alloc_workqueue("events_power_efficient",
WQ_POWER_EFFICIENT, 0);
// 可用于节能和工作队列Suspend时可冻结的work
// 当WQ_POWER_EFFICIENT禁止后,system_freezable_power_efficient_wq和system_wq相同
system_freezable_power_efficient_wq = alloc_workqueue(
"events_freezable_power_efficient", WQ_FREEZABLE | WQ_POWER_EFFICIENT, 0);
->wq_watchdog_init // 初始化工作队列的看门狗
create_worker
用来创建内核线程worker。首先根据线程池类型,设置线程池内线程的名称;接着在本地内存节点分配一个task_struct
结构体,并将线程的入口函数设置为worker_thread
;接着将创建的内核线程worker和线程池worker_pool
绑定;最后设置线程数量信息。
[kernel/workqueue.c]
create_worker(pool)
->ida_simple_get // 通过IDA子系统获取ID号并设置到worker_ida中,可用于内核线程名称
->alloc_worker // 在worker_pool的node节点中分配一个worker结构体
worker->pool = pool // 使worker的pool指向worker属于的worker_pool
// pool->cpu >= 0表示BOUND类型的工作线程。worker的名字一般是"kworker/+CPU_ID
// +:+worker_id",如果属于高优先级类型的wirkqueue(nice值小于0),末尾还要加上H。
// pool->cpu < 0表示UNBOUND类型的工作线程(UNBOUND类型的工作线程的pool->cpu设置为-1),
// worker的名字一般是"kworker/u+CPU_ID+:+worker_id"
if (pool->cpu >= 0) // 给和CPU绑定的worker_pool的worker设置线程名称
snprintf(id_buf, sizeof(id_buf), "%d:%d%s", pool->cpu, id,
pool->attrs->nice < 0 ? "H" : "");
else // 给不和CPU绑定的worker_pool的worker设置线程名称
snprintf(id_buf, sizeof(id_buf), "u%d:%d", pool->id, id);
// 在本地内存节点分配一个task_struct结构体并创建一个内核线程,线程的入口函数为worker_thread
->kthread_create_on_node
->set_user_nice // 根据worker_pool的attrs属性设置创建线程的nice值
->kthread_bind_mask // 根据worker_pool的attrs属性设置创建线程的cpumask
->worker_attach_to_pool // 将内核线程worker绑定到worker_pool
->mutex_lock // 互斥锁attach_mutex加锁
// 设置此线程和CPU的亲和性,若cpumask中没有任何online CPU,则设置会失败
->set_cpus_allowed_ptr(worker->task, pool->attrs->cpumask)
// POOL_DISASSOCIATED是worker_pool内部使用的标志,一个worker_pool可以是associated状态
// 或者disassociated状态。associated状态表示worker_pool绑定了某个CPU,disassociated状态
// 表示没有绑定某个CPU,也有可能是绑定的CPU被offline了,因此可以在任意CPU上运行,此处应是
// associated状态
if (pool->flags & POOL_DISASSOCIATED)
worker->flags |= WORKER_UNBOUND;
// 将worker的node挂到worker_pool的workers链表中,实现了worker绑定到worker_pool上
->list_add_tail(&worker->node, &pool->workers)
->mutex_unlock // 互斥锁attach_mutex解锁
->spin_lock_irq(&pool->lock) // 自旋锁lock加锁
worker->pool->nr_workers++ // worker的数量加1
->worker_enter_idle // 将worker设置为idle状态
worker->flags |= WORKER_IDLE // 设置WORKER_IDLE标志
pool->nr_idle++ // worker_pool空闲线程数量加1
worker->last_active = jiffies // 设置最近一次运行的时间戳
// 将空闲的worker挂到worker_pool的idle_list链表中
->list_add(&worker->entry, &pool->idle_list)
// too_many_workers返回true的条件是:空闲线程数量大于2且空闲线程数量减2乘以4大于等于
// 忙碌线程数量。timer_pending返回true的条件是:有定时处于pending状态
if (too_many_workers(pool) && !timer_pending(&pool->idle_timer))
// 修正idle_timer定时的时间,IDLE_WORKER_TIMEOUT为300*HZ,如HZ为100,则时间为5分钟
mod_timer(&pool->idle_timer, jiffies + IDLE_WORKER_TIMEOUT);
->wake_up_process // 唤醒创建的内线线程worker
->spin_unlock_irq(&pool->lock) // 自旋锁lock解锁
创建工作队列workqueue
的函数很多,并且基本上和旧版本的workqueue兼容。最通用的一个函数是alloc_workqueue
,分别有3个参数,分别是name
、flags
和max_active
,其他函数都是对workqueue
的封装,主要区别是flags
不同。主要的flags
如下:
(1)WQ_UNBOUND:工作任务work会加入UNBOUND线程池中,UNBOUND工作队列线程没有和具体的CPU绑定,不需要额外的同步措施,但会损失因局部性原理带来的性能提升。比较适合以下场景:
(a)某些内核组件会在不同的CPU上运行,如果创建BOUND类型工作队列,会创建很多线程。
(b)长时间运行的内核组件(标记WQ_CPU_INTENSIVE标志位)通常会创建UNBOUND类型的工作队列,内核调度器会自动选择此工作队列内核线程运行的CPU。
(2)WQ_FREEZABLE:电源管理相关的标志,此类工作队列会参与到系统的suspend过程,系统suspend后,工作线程处理完当前所有的work后冻结。一旦冻结完成,不会再执行新的work,直到解冻。
(3)WQ_MEM_RECLAIM:当内存紧张时,在此类工作队列内创建新的工作线程可能会失败,此时系统的rescuer内核线程会接管这种情况。
(4)WQ_HIGHPRI:此工作队列内的线程优先级较高。
(5)WQ_CPU_INTENSIVE:此类工作队列中的内核线程执行特别消耗CPU资源的work,这类work的执行线程由调度器进行调度。排在这类work后面non-CPU-intensive类型的work会推迟执行。
(6)__WQ_ORDERED:此类工作队列中的线程同一时间只能执行一个work。
参数max_active
决定了每个CPU最多可以有多少个work挂入一个工作队列中。例如max_active
=16,则说明每个CPU最多可以有16个work挂入到工作队列中执行。对于BOUND类型的工作队列,max_active
最大可以是512,如果max_active
被设置为0,则表示指定max_active
为256.对于UNBOUND类型工作队列,max_active
最大值为512和4*num_possible_cpus
之间的最大值。通常建议将max_active
设为0,如希望work被串行执行,则考虑使用max_active
=1和WQ_UNBOUND的组合。
[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...) \
// `max_active`为1
alloc_workqueue(fmt, WQ_UNBOUND | __WQ_ORDERED | (flags), 1, ##args)
#define create_workqueue(name) \
alloc_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, 1, (name))
#define create_freezable_workqueue(name) \
alloc_workqueue("%s", __WQ_LEGACY | WQ_FREEZABLE | WQ_UNBOUND | \
WQ_MEM_RECLAIM, 1, (name))
#define create_singlethread_workqueue(name) \
alloc_ordered_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, name)
下面重点分析一下__alloc_workqueue_key
。WQ_POWER_EFFICIENT
标志位和wq_power_efficient
共同影响系统的功耗。当系统定义CONFIG_WQ_POWER_EFFICIENT_DEFAULT
选项时,wq_power_efficient
为1。BOUND类型的线程池worker_pool内的线程只会在一个CPU上执行,由于局部性原理,性能会得到提高,但会增加能量消耗。UNBOUND类型线程池中的work由那个CPU执行,由调度器决定,调度器调度的时候会考虑CPU的状态,尽量使空闲的CPU(idle)保持空闲状态,以降低功耗。对于工作队列workqueue,如果工作队列中有WQ_UNBOUND标记,则说明此workqueue是Per-CPU类型的,运行在那个CPU上初始化时就指定了,不由调度器控制。工作队列workqueue
要在性能和能耗之间平衡,既要获得更好的性能(尽量让Per-CPU的线程池处理worker),也要降低能耗(让空闲的CPU继续保持空闲,而不是反复在空闲、忙碌状态中切换)。
[include/linux/workqueue.h]
WQ_MAX_ACTIVE = 512, /* I like 512, better ideas? */
WQ_DFL_ACTIVE = WQ_MAX_ACTIVE / 2,
[kernel/workqueue.c]
__alloc_workqueue_key
// 如果flags中包含了WQ_POWER_EFFICIENT,且系统配置了CONFIG_WQ_POWER_EFFICIENT_DEFAULT,
// 则向workqueue中的flags设置WQ_UNBOUND标记
if ((flags & WQ_POWER_EFFICIENT) && wq_power_efficient)
flags |= WQ_UNBOUND;
// 如果是UNBOUND类型,则还需要分配额外的空间用来保存NUMA节点信息,SMP系统中nr_node_ids为1
if (flags & WQ_UNBOUND)
tbl_size = nr_node_ids * sizeof(wq->numa_pwq_tbl[0]);
->kzalloc // 分配workqueue_struct结构体占用的内存
// 如果有WQ_UNBOUND标志,则说明是UNBOUND类型的workqueue,则需要分配属性内存空间
if (flags & WQ_UNBOUND) {
wq->unbound_attrs = alloc_workqueue_attrs(GFP_KERNEL);
if (!wq->unbound_attrs)
goto err_free_wq;
}
// 如果max_active为0,则设置max_active为256
max_active = max_active ?: WQ_DFL_ACTIVE;
// 如果是UNBOUND类型,max_active最大值为512和4*num_possible_cpus之间的最大值
max_active = wq_clamp_max_active(max_active, flags, wq->name);
wq->flags = flags // 设置workqueue的标志
wq->saved_max_active = max_active // 设置workqueue的最大in-flight work数量
->alloc_and_link_pwqs
bool highpri = wq->flags & WQ_HIGHPRI // workqueue是否为高优先级
// 如果不含有WQ_UNBOUND标志,则说明此workqueue为BOUND类型
if (!(wq->flags & WQ_UNBOUND)) {
// 对于BOUND类型的workqueue,则对每一个CPU分配一个pool_workqueue结构体
// cpu_pwqs是一个Per-CPU类型的指针
wq->cpu_pwqs = alloc_percpu(struct pool_workqueue);
if (!wq->cpu_pwqs)
return -ENOMEM;
// 对于每个CPU,
for_each_possible_cpu(cpu) {
// 获取CPU对应的pool_workqueue指针
struct pool_workqueue *pwq = per_cpu_ptr(wq->cpu_pwqs, cpu);
// 获取CPU对应的worker_pool指针,cpu_worker_pools是静态定义Per-CPU
// 类型的worker_pool
struct worker_pool *cpu_pools = per_cpu(cpu_worker_pools, cpu);
init_pwq(pwq, wq, &cpu_pools[highpri]);
->memset(pwq, 0, sizeof(*pwq)) // 将pool_workqueue清零
pwq->pool = pool // 使pool_workqueue中的pool指针指向worker_pool
pwq->wq = wq // 使pool_workqueue中的wq指针指向workqueue_struct
// 初始化pool_workqueue中的unbound_release_work成员,回调函数指向
// pwq_unbound_release_workfn
INIT_WORK(&pwq->unbound_release_work, pwq_unbound_release_workfn)
mutex_lock(&wq->mutex);
link_pwq(pwq); // 将pool_workqueue添加到workqueue_struct的pwqs链表中
mutex_unlock(&wq->mutex);
}
return 0;
} else if (wq->flags & __WQ_ORDERED) {
ret = apply_workqueue_attrs(wq, ordered_wq_attrs[highpri]);
return ret;
} else {
return apply_workqueue_attrs(wq, unbound_std_wq_attrs[highpri]);
}
->alloc_worker // 如果定义WQ_MEM_RECLAIM标志,则分配一个worker
->kthread_create // 创建一个线程,线程执行函数为rescuer_thread
->wake_up_process // 唤醒创建的线程
对于ORDERED和UNBOUND类型的workqueue,都通过调用apply_workqueue_attrs
来分配workqueue,只是第二个参数不同,ORDERED传入的是ordered_wq_attrs
,UNBOUND传入的是unbound_std_wq_attrs
。ORDERED和UNBOUND类型的workqueue关联的worker_pool是从系统定义的哈希表unbound_pool_hash
根据属性匹配的,如匹配不到,则重新分配和初始化一个,并添加到unbound_pool_hash
中.在init_pwq
函数中,使pool_workqueue中的pool指针指向worker_pool,使pool_workqueue中的wq指针指向workqueue_struct,使pool_workqueue中的unbound_release_work
回调函数指向pwq_unbound_release_workfn
。最后在link_pwq
,将pool_workqueue添加到workqueue_struct的pwqs链表中。
[kernel/workqueue.c]
// 静态定义的unbound_pool_hash哈希表
static DEFINE_HASHTABLE(unbound_pool_hash, UNBOUND_POOL_HASH_ORDER);
apply_workqueue_attrs
->apply_wqattrs_lock // 互斥锁wq_pool_mutex加锁
->apply_workqueue_attrs_locked
->apply_wqattrs_prepare
->kzalloc // 分配一个struct apply_wqattrs_ctx临时对象
->alloc_workqueue_attrs // 分配workqueue属性空间
->alloc_workqueue_attrs // 分配workqueue临时属性空间
->copy_workqueue_attrs // 将传入的属性参数拷贝到分配的属性空间中
->copy_workqueue_attrs // 将属性拷贝到临时属性空间中
->alloc_unbound_pwq
// 根据属性在unbound_pool_hash中查找worker_pool,如果没有,那就重新分配和初始化一个,并添加到
// unbound_pool_hash中
// 系统定义了一个哈希表unbound_pool_hash,用于管理系统中所有的UNBOUND类型的worker_pool,通过
// wqattrs_equal判断系统中是否已经有了类型相关的worker_pool。
->get_unbound_pool
->kmem_cache_alloc_node // 从pwq_cache中分配一个struct pool_workqueue结构体
// 使pool_workqueue中的pool指针指向worker_pool
// 使pool_workqueue中的wq指针指向workqueue_struct
// 使struct pool_workqueue中的unbound_release_work回调函数指向pwq_unbound_release_workfn
->init_pwq
->apply_wqattrs_commit
->mutex_lock // 互斥锁mutex加锁
->copy_workqueue_attrs // 将属性拷贝到unbound_attrs
->link_pwq // 将pool_workqueue添加到workqueue_struct的pwqs链表中
->mutex_unlock // 互斥锁mutex解锁
->apply_wqattrs_cleanup
->apply_wqattrs_unlock // 互斥锁wq_pool_mutex解锁
4.添加工作任务
内核默认定义了7个工作队列workqueue,一般情况下足够使用,不需要额外创建新的工作队列。要使用工作队列,需要添加工作任务work。在添加工作队任务之前,需要先初始化工作任务work,内核提供了INIT_WORK
宏来初始化工作任务work。这里重点关注data
成员的初始化。data
成员被划分为两个位域,低比特位域存放work相关的flags,高比特位域用于存放上次执行该work的worker_pool的worker_pool的ID号或保存上一次pool_workqueue数据结构指针。可通过get_work_pool
函数获取执行该work的线程池worker_pool的指针。
[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(); \ // 初始化data成员
INIT_LIST_HEAD(&(_work)->entry); \ // 初始化链表节点
(_work)->func = (_func); \ // 设置回调函数
} while (0)
#define WORK_DATA_INIT() ATOMIC_LONG_INIT(WORK_STRUCT_NO_POOL)
以32bit的CPU来说,当data
字段包含WORK_STRUCT_PWQ
标志,表示高比特位域保存着上一次pool_workqueue
数据结构的指针,这时低8位存放标志。当data
字段没有包含WORK_STRUCT_PWQ
标志,表示高比特位域存放上次执行该work的worker_pool的ID号,低5位用于存放标志。
enum {
// 带_BIT的表示是第几位
WORK_STRUCT_PENDING_BIT = 0,
WORK_STRUCT_DELAYED_BIT = 1,
WORK_STRUCT_PWQ_BIT = 2,
WORK_STRUCT_LINKED_BIT = 3,
WORK_STRUCT_COLOR_SHIFT = 4,
WORK_STRUCT_COLOR_BITS = 4,
/* 该work正在等待执行 */
WORK_STRUCT_PENDING = 1 << WORK_STRUCT_PENDING_BIT,
/* 该work被延迟执行了 */
WORK_STRUCT_DELAYED = 1 << WORK_STRUCT_DELAYED_BIT,
/* data的高比特位域保存着指向pool_workqueue结构的指针,
pool_workqueue需要按照256字节对其,低8位可以忽略 */
WORK_STRUCT_PWQ = 1 << WORK_STRUCT_PWQ_BIT,
/* 下一个work连接到该work上 */
WORK_STRUCT_LINKED = 1 << WORK_STRUCT_LINKED_BIT,
......
}
初始化完工作任务work后,可以调用schedule_work
或schedule_work_on
函数将工作任务work挂入系统默认的workqueue中。schedule_work
和schedule_work_on
函数将work挂入系统默认BOUND类型的工作队列system_wq
中,不同的是schedule_work_on
可以指定挂入工作队列所在的CPU。cancel_work_sync
取消一个工作任务。flush_scheduled_work
函数将system_wq
工作队列中的所有工作任务销毁。如果创建了新的工作队列,则可使用queue_work
或queue_work_on
向创建的工作队列中加入work。
[include/linux/workqueue.h]
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}
static inline bool schedule_work_on(int cpu, struct work_struct *work)
{
return queue_work_on(cpu, system_wq, work);
}
bool cancel_work_sync(struct work_struct *work)
{
return __cancel_work_timer(work, false);
}
static inline void flush_scheduled_work(void)
{
flush_workqueue(system_wq);
}
schedule_work
函数最后调用了queue_work_on
函数,queue_work_on
函数有三个参数,第一个参数cpu
为绑定处理器的编号,如不绑定特定的处理器,则传入WORK_CPU_UNBOUND
,使用本地CPU。第二个参数wq
为要挂入的工作队列的指针,第三个参数work
为工作任务结构体指针。若work中设置了WORK_STRUCT_PENDING
标记,说明work已经在工作队列中,还未得到处理,不能再次添加。首先根据工作队列的类型,查找对应的pool_workqueue,接着判断该work上一次运行的worker_pool和本次要运行的worker_pool是否一致,若不一致则选择上一次运行的worker_pool,同时更新对应的pool_workqueue。若pool_workqueue活跃的work数量,如果少于最高限制,就加入pending链表worklist,否则加入链表delayed_works。最后调用insert_work
执行插入操作,插入时向work的data成员设置了pool_workqueue指针、WORK_STRUCT_PENDING
标记和WORK_STRUCT_PWQ
标记,WORK_STRUCT_PENDING
表示work已加入工作队列但处于PENDING状态,等待被处理,WORK_STRUCT_PWQ
表示work的data成员是pool_workqueue的指针。若没有工作线程运行,则唤醒第一个空闲的线程。schedule_work
只是将work加入到工作队列workqueue对应的worker_pool链表中,并没有得到执行。schedule_work
函数的关键是找到合适的pool_workqueue。
[kernel/workqueue.c]
schedule_work
->queue_work
->queue_work_on
->local_irq_save // 禁止本地中断
// 如果work设置了WORK_STRUCT_PENDING,则说明work已经在工作队列中了,
// 不需要额外添加,直接返回false,反之则需要添加,添加成功返回true
if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work)))
->__queue_work(cpu, wq, work)
// 如果设置__WQ_DRAINING标记,说明工作队列要销毁,则不再添加新的工作任务,直接返回
if (unlikely(wq->flags & __WQ_DRAINING) && WARN_ON_ONCE(!is_chained_work(wq)))
return;
// 如果cpu的参数为WORK_CPU_UNBOUND,则直接选择本地CPU
if (req_cpu == WORK_CPU_UNBOUND)
cpu = wq_select_unbound_cpu(raw_smp_processor_id());
// 根据工作队列的标记是否含有WQ_UNBOUND选择pool_workqueue
// 含有WQ_UNBOUND,说明是BOUND类型的工作队列,则从Per-CPU变量cpu_pwqs中获取
// 不含有WQ_UNBOUND,说明是UNBOUND类型的工作队列,则从numa_pwq_tbl变量中获取,
// numa_pwq_tbl变量是一个指针数组,索引为node节点,元素为此node节点的pool_workqueue指针
if (!(wq->flags & WQ_UNBOUND))
pwq = per_cpu_ptr(wq->cpu_pwqs, cpu);
else
pwq = unbound_pwq_by_node(wq, cpu_to_node(cpu));
// 获取上一次运行的worker_pool
->get_work_pool
// last_pool为上次一运行work的work_pool,pwq->pool为本次运行该work的work_pool,
// 若两者不一致,则应添加到上一次运行work的work_pool,这样可以利用缓存热度
if (last_pool && last_pool != pwq->pool) {
struct worker *worker;
spin_lock(&last_pool->lock);
// 判断work_pool是否正在运行此work并返回执行此work的线程worker
worker = find_worker_executing_work(last_pool, work);
// 获取运行此worker对应的pool_workqueue
if (worker && worker->current_pwq->wq == wq) {
pwq = worker->current_pwq;
} else {
spin_unlock(&last_pool->lock);
spin_lock(&pwq->pool->lock); // 1
} else { // 如果work是第一次加入,则直接加入本地CPU或者本地node节点的工作队列,
// 无需考虑上一次在哪里执行
spin_lock(&pwq->pool->lock); // 2
}
// 判断当前的pool_workqueue活跃的work数量,如果少于最高限制,就加入pending链表worklist,
// 否则加入delayed_works
if (likely(pwq->nr_active < pwq->max_active)) {
pwq->nr_active++;
worklist = &pwq->pool->worklist; // 获取worklist
if (list_empty(worklist))
pwq->pool->watchdog_ts = jiffies;
} else {
work_flags |= WORK_STRUCT_DELAYED; // 设置延迟执行的标志
worklist = &pwq->delayed_works; // 获取delayed_works
}
->insert_work(pwq, work, worklist, work_flags)
->set_work_pwq
// 向work中的data成员设置pool_workqueue指针、WORK_STRUCT_PENDING、
// WORK_STRUCT_PWQ标记, WORK_STRUCT_PENDING表示work处于PENDING状态,等待被处理,
// WORK_STRUCT_PWQ表示work的data成员是pool_workqueue的指针
->set_work_data
->list_add_tail // 将work加入worklist链表
->get_pwq // 增加pool_workqueue的引用计数,和put_pwq成对使用
->smp_mb // SMP系统内存屏障,确保前面的代码都执行完毕
// 如果worker_pool中没有运行的线程,则唤醒第一个空闲的线程
if (__need_more_worker(pool))
wake_up_worker(pool);
->spin_unlock(&pwq->pool->lock) // 和1或2处的spin_lock配对
->local_irq_restore // 开启本地中断
5.执行工作任务
在初始化工作队列的函数init_workqueues
中,使用create_worker
函数创建了工作线程,执行了工作线程的入口函数为worker_thread
,下面分析一下worker_thread
的执行流程。首先判断此工作线程是否要退出,即是否设置了WORKER_DIE
标志,若设置了,则线程退出;接着退出空闲状态;然后判断是否需要创建额外的线程,需要的话调用manage_workers
创建线程,这里会循环创建,直到不需要创建线程或者有空闲的线程才停止创建线程;最后在开始处理工作任务,如果work后面没有再连接work,则调用process_one_work
处理此work,反之则调用move_linked_works
将所有待执行的work迁移到工作线程的scheduled
链表中,然后调用process_scheduled_works
一并处理,线程保持运行(keep_working
)的条件是线程池中的worker_pool的worklist非空(即还有work要处理)和线程池中正在运行的线程数量小于等于1,当不满足keep_working
的条件,工作线程会睡眠。keep_working
可以动态管理活跃的线程数量。
[kernel/workqueue.c]
worker_thread(void *__worker)
// 设置PF_WQ_WORKER标志,告诉调度器这是一个工作队列的工作线程
->worker->task->flags |= PF_WQ_WORKER
woke_up:
->spin_lock_irq(&pool->lock) // 加锁
// 如果设置了WORKER_DIE标志,说明此工作线程要退出了
if (unlikely(worker->flags & WORKER_DIE)) {
spin_unlock_irq(&pool->lock);
worker->task->flags &= ~PF_WQ_WORKER; // 清除PF_WQ_WORKER标记
set_task_comm(worker->task, "kworker/dying");
ida_simple_remove(&pool->worker_ida, worker->id);
worker_detach_from_pool(worker, pool); // 将worker和worker_pool分离
kfree(worker); // 释放wirker的内存
return 0; // 返回后此线程退出
}
// 创建此线程时被设置成空闲状态,现在退出空闲状态
->worker_leave_idle
->worker_clr_flags(worker, WORKER_IDLE) // 清除WORKER_IDLE标志
pool->nr_idle-- // 空闲线程数量减1
->list_del_init // 从空闲链表entry中删除
recheck:
// 如果worker_pool的worklist链表为空且有线程在运行,则跳转到sleep中睡眠
if (!need_more_worker(pool))
goto sleep;
// 如果没有空闲的线程,则调用manage_workers函数创建工作线程,创建成功后跳转到recheck处
// 这里是循环创建线程,直到不需要创建线程或者有空闲的线程才停止创建线程
// 也就是循环创建会额外创建一个空闲的线程
if (unlikely(!may_start_working(pool)) && manage_workers(worker))
goto recheck;
// 清除WORKER_PREP和WORKER_REBOUND标志,表示马上要执行work的回调函数了
// WORKER_PREP:工作线程worker准备运行,WORKER_REBOUND:工作线程worker重新绑定
// 对于UNBOUND类型的workqueue还会增加work_pool->nr_running的引用计数
->worker_clr_flags(worker, WORKER_PREP | WORKER_REBOUND)
do {
// 遍历 worker_pool的worklist链表,拿出工作任务work
struct work_struct *work = list_first_entry(&pool->worklist, struct work_struct, entry);
pool->watchdog_ts = jiffies;
// 如果work后面没有连接work时,调用process_one_work处理work
// WORK_STRUCT_LINKED:表示work后面还连接着work
if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
// 如果该work正在本地CPU的其他线程执行,则将此work加入到执行的线程的scheduled链表后返回,
// 反之则执行该work(只执行一个work)
process_one_work(worker, work);
// 如果该线程的scheduled链表中不为空,即还有需要执行的work,
// 则调用process_scheduled_works处理所有work
if (unlikely(!list_empty(&worker->scheduled)))
->process_scheduled_works(worker);
// 遍历scheduled链表,调用process_one_work处理所有work
while (!list_empty(&worker->scheduled)) {
struct work_struct *work = list_first_entry(&worker->scheduled,
struct work_struct, entry);
->process_one_work(worker, work);
}
} else {
// 如果work后面还连接着work,则使用move_linked_works将work迁移到scheduled链表中,
// 然后调用process_scheduled_works一并处理所有work
move_linked_works(work, &worker->scheduled, NULL);
process_scheduled_works(worker);
}
// 工作线程worker继续运行的条件有两个(keep_working返回true的条件):
// 1: 线程池中的worker_pool的worklist非空,即还有work要处理
// 2: 线程池中正在运行的线程数量小于等于1
} while (keep_working(pool));
sleep:
->worker_enter_idle // 工作线程worker进入idle状态
->__set_current_state(TASK_INTERRUPTIBLE) // 设置线程的状态,可以被中断、信号唤醒
->spin_unlock_irq(&pool->lock) // pool->lock解锁
->schedule // 将此线程调度出CPU
goto woke_up // 线程被唤醒后跳转到woke_up标号处执行
manage_workers
函数是动态管理创建工作线程的函数。manager_arb
保护创建线程的临界区,确保只有一个线程进入创建线程的临界区。真正创建线程的工作在manager_arb
函数中完成,在while
循环中调用create_worker
创建工作线程,创建成功或者不需要创建工作线程时跳出循环。
[kernel/workqueue.c]
// 管理工作线程的函数
static bool manage_workers(struct worker *worker)
{
struct worker_pool *pool = worker->pool;
// 获取管理工作线程的互斥锁
if (!mutex_trylock(&pool->manager_arb))
return false;
// 获取锁成功,则将此工作线程worker设置到worker_pool中
pool->manager = worker;
// 如果需要,则创建新的线程
maybe_create_worker(pool);
pool->manager = NULL; // 处理完管理工作,将manager设置为空
mutex_unlock(&pool->manager_arb); // 解锁管理工作线程的互斥锁
return true;
}
MAYDAY_INITIAL_TIMEOUT = HZ / 100 >= 2 ? HZ / 100 : 2, // 一般为20毫秒
static void maybe_create_worker(struct worker_pool *pool)
{
restart:
spin_unlock_irq(&pool->lock); // 解锁
/* 修正mayday_timer的时间,超时时间为 */
mod_timer(&pool->mayday_timer, jiffies + MAYDAY_INITIAL_TIMEOUT);
while (true) {
// 调用create_worker创建工作线程,创建成功则退出,need_to_create_worker用来
// 判断是否还需要创建线程,不需要退出
if (create_worker(pool) || !need_to_create_worker(pool))
break;
// 创建工作线程失败且还需要创建线程,则休眠一段时间
schedule_timeout_interruptible(CREATE_COOLDOWN);
if (!need_to_create_worker(pool))
break;
}
// 删除定时器
del_timer_sync(&pool->mayday_timer);
spin_lock_irq(&pool->lock);
// 如果需要创建工作线程,则继续跳转到restart处
if (need_to_create_worker(pool))
goto restart;
}
process_one_work
是执行work的核心函数。首先调用find_worker_executing_work
判断当前要执行的work是否在本地CPU上其他线程中执行,若是,则将此work加入到执行线程的scheduled
链表中,然后返回,并不会执行此work;反之则将此work加入到工作线程的忙碌链表hentry
中,将工作线程worker加入到busy_hash
哈希表中,准备执行此work;执行之前需要设置当前工作线程执行的工作任务、回调函数、相关标志等,比较重要的是将work从worklist链表中删除,调用set_work_pool_and_clear_pending
函数,该函数清除了调用schedule_work
时设置的WORK_STRUCT_PENDING
和WORK_STRUCT_PWQ
标记,同时将执行线程所属线程池worker_pool的id设置到data成员中,此后work处于空闲状态;执行完成之后清除设置的当前工作线程执行的工作任务、回调函数等。
从ARMv7指令集开始,ARM提供3条内存屏障指令:
(1)数据存储屏障( Data Memory Barrier,DMB)
数据存储器隔离。DMB指令保证仅当所有在它前面的存储器访问操作都执行完毕后,才提交在它后面的存取访问操作指令。当位于此指令前的所有内存访问均完成时,DMB指令才会完成。
(2)数据同步屏障( Data synchronization Barrier,DSB)
数据同步隔离。比DMB要严格一些,仅当所有在它前面的存储访问操作指令都执行完毕后,才会执行在它后面的指令。即任何指令都要等待DSB前面的存储访问完成。位于此指令前的所有缓存,如分支预测和TLB维护操作全部完成。
(3)指令同步屏障( Instruction synchronization Barrier,ISB)。
指令同步隔离。它最严格,会冲洗流水线,ISB前面的指令执行完毕后才会从cache或者内存中预取ISB指令之后的指令。ISB通常用来保证上下文切换的效果,例如更改ASID、TLB维护操作和C15寄存器的修改等。
[kernel/workqueue.c]
process_one_work
->get_work_pwq // 获取工作线程对应的pool_workqueue
// 此工作队列是否是CPU敏感类型的
bool cpu_intensive = pwq->wq->flags & WQ_CPU_INTENSIVE
// 检查此work是否在同一个CPU上的不同线程中运行,同一个work,不允许在单个CPU上多个线程中运行
->collision = find_worker_executing_work
// 遍历worker_pool的busy_hash表中的忙碌工作线程,忙碌的工作线程都在busy_hash哈希表中
hash_for_each_possible(pool->busy_hash, worker, hentry,(unsigned long)work)
// 比较工作任务work的地址和回调函数func的地址是否一致,一致返回工作线程worker的地址
if (worker->current_work == work && worker->current_func == work->func)
return worker;
return NULL; // 遍历完没找到,则返回空
// 如果此work在同一个CPU上的不同线程worker中运行,要将此work迁移到运行的线程worker上
if (unlikely(collision)) {
// 迁移work
move_linked_works(work, &collision->scheduled, NULL);
return; // 迁移完work后返回
}
// 将此工作线程worker加入到busy_hash哈希表中,表示此工作线程要处理工作任务work了
->hash_add(pool->busy_hash, &worker->hentry, (unsigned long)work)
// 设置current_work和current_func,软件锁存,当清除WORK_STRUCT_PENDING标记后,work就处于idle
// 状态,可以被取消
worker->current_work = work; // 保存现在执行的work
worker->current_func = work->func; // 保存现在执行work的回调函数
worker->current_pwq = pwq; // 设置此worker对应的pool_workqueue
list_del_init // 从entry链表中删除准备执行的work
// 如果是CPU敏感型的工作,则向worker中设置WORKER_CPU_INTENSIVE标记,WORKER_CPU_INTENSIVE标记的
// 线程调度由调度器负责,不再由工作队列进行并发管理,
if (unlikely(cpu_intensive))
worker_set_flags(worker, WORKER_CPU_INTENSIVE);
// 是否需要更多的工作线程worker,如果需要,则唤醒线程池work_pool中的线程
if (need_more_worker(pool))
wake_up_worker(pool);
// 清除work中的WORK_STRUCT_PENDING和WORK_STRUCT_PWQ标记并将此次执行线程
// 所属线程池的ID设置的work的data成员中,清除WORK_STRUCT_PENDING标记后
// work处于idle状态
->set_work_pool_and_clear_pending(work, pool->id)
->smp_wmb // SMP系统内存屏障,通过ARM特有的DMB指令实现,确保前面访问内存的指令执行完毕
->set_work_data
->smp_mb // SMP系统内存屏障
->spin_unlock_irq // 解锁pool->lock,执行工作任务的回调函数时释放自旋锁
->worker->current_func(work) // 执行work的回调函数
->spin_lock_irq // 加锁pool->lock
// 清除WORKER_CPU_INTENSIVE标记
if (unlikely(cpu_intensive))
worker_clr_flags(worker, WORKER_CPU_INTENSIVE);
hash_del(&worker->hentry) // 从工作线程的忙碌链表hentry中删除被执行完的work
worker->current_work = NULL
worker->current_func = NULL
worker->current_pwq = NULL
下面的函数是上面用到的一些功能性函数。
[kernel/workqueue.c]
// 判断是否需要更多的工作线程worker,need_more_worker返回true需要同时满足两个条件:
// 1: worklist不为空,即还有work要处理
// 2: nr_running为0,即没有运行的线程
// 如不满足上面的两个条件,则不会创建工作线程worker
static bool need_more_worker(struct worker_pool *pool)
{
return !list_empty(&pool->worklist) && __need_more_worker(pool);
}
static bool __need_more_worker(struct worker_pool *pool)
{
return !atomic_read(&pool->nr_running); // 读取运行的线程数量
}
// 唤醒线程池中第一个空闲的线程
static void wake_up_worker(struct worker_pool *pool)
{
struct worker *worker = first_idle_worker(pool);
if (likely(worker))
wake_up_process(worker->task);
}
// 工作线程worker继续运行要同时满足如下两个条件:
// 1: 线程池中的worker_pool的worklist非空,即还有work要处理
// 2: 线程池中正在运行的线程数量小于等于1
// 如不满足,则线程将进入休眠状态
static bool keep_working(struct worker_pool *pool)
{
return !list_empty(&pool->worklist) && atomic_read(&pool->nr_running) <= 1;
}
当worker长时间处于空闲状态会怎么样呢?前面说到过,在工作线程池初始化函数init_worker_pool
中,设置了空闲定时器idle_timer
的回调函数为idle_worker_timeout
, 在创建worker后worker进入空闲状态(create_worker
->worker_enter_idle
)时设置超时时间为300秒。下面分析一下工作线程空闲超时后会怎么处理。当工作线程空闲的时间超过300秒后,会触发软中断定时器,在定时器的回调函数中设置线程死亡标志WORKER_DIE
,然后唤醒线程,线程运行时检测到WORKER_DIE
标志,会自动退出。
[kernel/workqueue.c]
static void idle_worker_timeout(unsigned long __pool)
{
struct worker_pool *pool = (void *)__pool;
spin_lock_irq(&pool->lock);
// 判断是否有更多的线程
while (too_many_workers(pool)) {
struct worker *worker;
unsigned long expires;
// 获取空闲线程链表的第一个元素
worker = list_entry(pool->idle_list.prev, struct worker, entry);
// 计算超时的时间戳
expires = worker->last_active + IDLE_WORKER_TIMEOUT;
// 使用现在的时间戳jiffies和超时时间戳expires相比,若jiffies < expires
// 则修正定时器的超时时间为expires
if (time_before(jiffies, expires)) {
mod_timer(&pool->idle_timer, expires);
break;
}
// 若jiffies >= expires,则空闲超时时间已到,则销毁工作线程
destroy_worker(worker);
pool->nr_workers-- // 线程池线程数量减1
pool->nr_idle-- // 线程池空闲线程数量减1
list_del_init // 从工作线程的空闲链表entry中删除此worker
worker->flags |= WORKER_DIE // 设置工作线程死亡的标志
// 唤醒工作线程,工作线程唤醒后检测到WORKER_DIE标志后会自己退出
->wake_up_process
}
spin_unlock_irq(&pool->lock);
}
MAX_IDLE_WORKERS_RATIO = 4
static bool too_many_workers(struct worker_pool *pool)
{
// 管理线程的锁是否被锁住
bool managing = mutex_is_locked(&pool->manager_arb);
// 如果有线程获取了manager_arb锁,可认为此线程是空闲的,因此要计算到空闲线程的数量中
int nr_idle = pool->nr_idle + managing; /* manager is considered idle */
// 计算忙碌线程数量
int nr_busy = pool->nr_workers - nr_idle;
// 返回true表示空闲线程太多,返回true的条件如下:
// 1: 空闲线程数量大于2
// 2: 空闲线程数量减2乘以4大于等于忙碌的线程数量
return nr_idle > 2 && (nr_idle - 2) * MAX_IDLE_WORKERS_RATIO >= nr_busy;
}
工作线城池中工作线程可以动态管理,管理规则如下:
(1)工作线程有3中状态:空闲(idle)、运行(running)和挂起(suspend)。空闲是指没有执行工作,运行是指正在执行工作,挂起是指执行工作的过程中睡眠。
(2)如果工作线程池中有工作任务要处理,则至少要有一个处于运行状态的线程来处理。
(3)如果处于运行状态的工人在执行工作的过程中进入了挂起状态,为了保证其他工作的执行,需要唤醒空闲的线程处理。
(4)如果有工作任务需要执行,并且处在运行状态的工作线程的数量大于1,会让多余的工作线程进入空闲状态(进程进行上下文切换时,在__schedule
函数中会判断此进程是否是工作队列的工作线程,如是则会调用wq_worker_sleeping
函数判断是否需要唤醒其他工作线程,如需要则调用try_to_wake_up_local
尝试唤醒)。
(5)如果没有工作任务需要执行,则让所有的工作线程进入空闲状态。
(6)如果工作线程的空闲时间超过300秒,则自会被销毁。
6.取消工作任务
使用cancel_work_sync
删除一个work,cancel_work_sync
是一个同步的接口,若要删除的work正在执行,则会等待此work执行完毕,然后删除此work。删除work就是把work从调用schedule_work
时插入的链表中删除。cancel_work_sync
不能用于delayed_work
,cancel_delayed_work_sync
可用于delayed_work
。
首先定义了静态类型的等待队列cancel_waitq
;接着调用try_to_grab_pending
尝试从worklist链表中删除work,若try_to_grab_pending
的返回值小于0,说明删除失败,需要再次调用尝试删除,当返回值是-ENOENT,说明其他线程正在删除此work,需要睡眠等待;跳出循环说明删除成功,调用mark_work_canceling
向data中设置worker_pool的id、WORK_OFFQ_CANCELING
和WORK_STRUCT_PENDING
标记,表示work正在被删除;接着调用flush_work
等待该work的回调函数被工作线程执行完毕,执行完毕后调用clear_work_data
清理work的data变量;最后,如果有线程在cancel_waitq
队列中等待,则唤醒其中的一个。
[kernel/workqueue.c]
cancel_work_sync
->__cancel_work_timer(work, false) // false表示不是delayed_work
static DECLARE_WAIT_QUEUE_HEAD // 申明一个等待队列cancel_waitq
do {
// 尝试从工作队列中取出work,若work处于idle状态(WORK_STRUCT_PENDING标记被清除),
// 则可直接取出,若work处于PENDING(WORK_STRUCT_PWQ没有被清除)状态,
// 正要被处理(WORK_STRUCT_PWQ标记被清除)或者work被其他线程
// 正在取出(设置WORK_OFFQ_CANCELING标记),则稍后取出。
// try_to_grab_pending返回值意义如下:
// 1: work未被处理,处于PENDING状态,手动将work从worklist链表中删除
// 0: work已被处理并且从worklist中删除,此时work处于idle状态
// -ENOENT: work被正在被其他线程删除
// -EAGAIN: 需要稍后再调用try_to_grab_pending再尝试删除work
ret = try_to_grab_pending(work, is_dwork, &flags);
// 如果该work正在被其他线程取出,则会等待其他线程完成
if (unlikely(ret == -ENOENT)) {
struct cwt_wait cwait;
init_wait(&cwait.wait);
cwait.wait.func = cwt_wakefn;
cwait.work = work;
prepare_to_wait_exclusive(&cancel_waitq, &cwait.wait, TASK_UNINTERRUPTIBLE);
if (work_is_canceling(work))
schedule();
finish_wait(&cancel_waitq, &cwait.wait);
}
// try_to_grab_pending返回值为负数时会一直循环尝试取出work
} while (unlikely(ret < 0));
->mark_work_canceling
->get_work_pool_id // 获取worker_pool的ID
pool_id <<= WORK_OFFQ_POOL_SHIFT
// 向data成员设置pool_id和WORK_OFFQ_CANCELING、WORK_STRUCT_PENDING标记
// WORK_OFFQ_CANCELING表示work正在被删除
->set_work_data(work, pool_id | WORK_OFFQ_CANCELING, WORK_STRUCT_PENDING)
->local_irq_restore // 恢复中断
// 等待work被执行完,前面只是从worklist中删除了work,
// 并没有等待worker执行完work的回调函数
->flush_work
->clear_work_data // 将data设置为WORK_STRUCT_NO_POOL,清除标记位
->smp_mb // SMP系统内存屏障
// 如果有线程在队列cancel_waitq上等待,则唤醒其中一个
if (waitqueue_active(&cancel_waitq))
__wake_up(&cancel_waitq, TASK_NORMAL, 1, work);
try_to_grab_pending
尝试从worklist链表中删除work。首先关闭中断,防止在中断中又调用schedule_work
插入work或调用cancel_work_sync
删除work;接着设置并测试WORK_STRUCT_PENDING
位,若WORK_STRUCT_PENDING
位已被清除(见process_one_work
函数),说明work已被从worklist链表中删除,处于idle状态,直接返回0,若test_and_set_bit
没有被清除,说明work正在等待被处理,处于PENDING状态,需要手动删除,删除后返回1;使用get_work_pwq
获取pool_workqueue的指针,若获取成功,则手动删除work,否则判断是否是别的线程正在删除work,若是返回-ENOENT,若不是返回-EAGAIN。try_to_grab_pending
的返回值意义如下:
1: work未被处理,处于PENDING状态,手动将work从worklist链表中删除
0: work已被处理并且从worklist中删除,此时work处于idle状态
-ENOENT: work被正在被其他线程取出
-EAGAIN: 需要稍后再调用try_to_grab_pending再尝试删除work
// is_dwork表示是否是delayed_work,true表示delayed_work,false表示不是
static int try_to_grab_pending(struct work_struct *work, bool is_dwork,
unsigned long *flags)
{
struct worker_pool *pool;
struct pool_workqueue *pwq;
local_irq_save(*flags); // 禁止本地中断
// 如果是delayed_work,则将delayed_work的定时器关闭
if (is_dwork) {
struct delayed_work *dwork = to_delayed_work(work);
// del_timer返回1表示定时器处于active状态,返回0表示定时器处于inactive状态
if (likely(del_timer(&dwork->timer)))
return 1;
}
// 设置WORK_STRUCT_PENDING标记并返回原来标记的值;若原来WORK_STRUCT_PENDING
// 标记被清除,说明work处于空闲状态,设置WORK_STRUCT_PENDING标记并返回0;
// 若没有被清除,说明work正在等待被处理,处于PENDING状态,需要手动删除
if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work)))
return 0;
// 通过data成员获取work对应的worker_pool指针,如果获取失败,跳转到fail处
pool = get_work_pool(work);
if (!pool)
goto fail;
spin_lock(&pool->lock);
// 获取work对应的pool_workqueue,若获取成功,说明work处于PENDING状态,手动删除work,
// 否则不能删除work
pwq = get_work_pwq(work);
if (pwq && pwq->pool == pool) { // 进入if分支,说明成功取到pool_workqueue
// delayed_work不能直接取出,需要将work激活并挂到worklist链表中
// 并将活跃work计数nr_active加1
if (*work_data_bits(work) & WORK_STRUCT_DELAYED)
pwq_activate_delayed_work(work);
// 将work从worklist链表中删除并重新初始化entry链表
list_del_init(&work->entry);
// 向data中设置WORK_STRUCT_PENDING标记和worker_pool的id
set_work_pool_and_keep_pending(work, pool->id);
spin_unlock(&pool->lock); // 解锁
return 1; // 返回1,说明成功删除了work
}
spin_unlock(&pool->lock); // 解锁
fail:
local_irq_restore(*flags);
// 判断此work是否被其他线程取出,如果是返回-ENOENT
if (work_is_canceling(work))
return -ENOENT;
cpu_relax();
// 删除失败,需要等一会再尝试删除
return -EAGAIN;
}
// 从工作任务的data成员中获取工作线程池worker_pool的指针
static struct worker_pool *get_work_pool(struct work_struct *work)
{
// 原子的读取data成员
unsigned long data = atomic_long_read(&work->data);
int pool_id;
assert_rcu_or_pool_mutex();
// 若data含有WORK_STRUCT_PWQ标记,说明data的高24位保存了pool_workqueue的指针,
// 则根据pool_workqueue直接返回worker_pool的指针
if (data & WORK_STRUCT_PWQ)
return ((struct pool_workqueue *)
(data & WORK_STRUCT_WQ_DATA_MASK))->pool;
// 若data不含有WORK_STRUCT_PWQ标记,说明data的高27位保存的是worker_pool的id
pool_id = data >> WORK_OFFQ_POOL_SHIFT;
if (pool_id == WORK_OFFQ_POOL_NONE) // 若id无效,则返回空
return NULL;
// 根据有效id,从worker_pool_idr查找此id对应的worker_pool的指针
return idr_find(&worker_pool_idr, pool_id);
}
// worker_pool的所有id静态定义在worker_pool_idr变量中
static DEFINE_IDR(worker_pool_idr);
// 获取工作任务work对用的pool_workqueue指针
static struct pool_workqueue *get_work_pwq(struct work_struct *work)
{
// 原子的读取data成员
unsigned long data = atomic_long_read(&work->data);
// 若data含有WORK_STRUCT_PWQ标记,说明data的高24位保存了pool_workqueue的指针
if (data & WORK_STRUCT_PWQ)
// 屏蔽低8位,返回pool_workqueue的指针
return (void *)(data & WORK_STRUCT_WQ_DATA_MASK);
else // 否则返回空
return NULL;
}
// 成功删除work后设置WORK_OFFQ_CANCELING标记,起到通知其他线程此work已被删除的作用,
// 设置WORK_STRUCT_PENDING标记的目的是阻止将此work再次添加到工作队列中
static void mark_work_canceling(struct work_struct *work)
{
unsigned long pool_id = get_work_pool_id(work);
pool_id <<= WORK_OFFQ_POOL_SHIFT;
set_work_data(work, pool_id | WORK_OFFQ_CANCELING, WORK_STRUCT_PENDING);
}
// 判断此work是否正在被其他线程删除,是返回true,不是返回false
static bool work_is_canceling(struct work_struct *work)
{
unsigned long data = atomic_long_read(&work->data);
return !(data & WORK_STRUCT_PWQ) && (data & WORK_OFFQ_CANCELING);
}
flush_work
等待work的回调函数执行完毕,返回true表示执行完成,返回false表示work的回调函数已经执行完毕,无需等待。start_flush_work
是实现等待功能的核心,通过定义一个额外的work,将work插到要删除的work后面,当插入的work被执行时,说明要删除work的回调函数已经执行完毕。wq_barrier_func
为插入work的回调函数,当次函数执行时,将会唤醒在完成量done
上等待的线程。
[kernel/workqueue.c]
struct wq_barrier {
struct work_struct work;
struct completion done;
struct task_struct *task; /* purely informational */
};
bool flush_work(struct work_struct *work)
{
struct wq_barrier barr;
......
// start_flush_work返回true说明要等待,则线程在完成量done上等待
if (start_flush_work(work, &barr)) {
wait_for_completion(&barr.done);
destroy_work_on_stack(&barr.work);
return true;
} else {
return false;
}
}
static bool start_flush_work(struct work_struct *work, struct wq_barrier *barr)
{
struct worker *worker = NULL;
struct worker_pool *pool;
struct pool_workqueue *pwq;
might_sleep();
local_irq_disable();
// 获取worker_pool
pool = get_work_pool(work);
if (!pool) {
local_irq_enable();
// work的回调函数已经执行完毕
return false;
}
spin_lock(&pool->lock);
// 获取pool_workqueue
pwq = get_work_pwq(work);
if (pwq) {
if (unlikely(pwq->pool != pool))
goto already_gone;
} else {
// 说明此work的回调函数正在被工作线程执行,查找执行此work的工作线程
worker = find_worker_executing_work(pool, work);
if (!worker)
goto already_gone;
pwq = worker->current_pwq;
}
check_flush_dependency(pwq->wq, work);
insert_wq_barrier(pwq, barr, work, worker);
// 初始化barr的work成员,work的回调函数为wq_barrier_func
INIT_WORK_ONSTACK(&barr->work, wq_barrier_func)
// 设置WORK_STRUCT_PENDING_BIT标记
->__set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(&barr->work))
->init_completion(&barr->done) // 初始化完成量
barr->task = current // task为本线程
// 获取work的插入节点位置
if (worker)
head = worker->scheduled.next;
else {
unsigned long *bits = work_data_bits(target);
head = target->entry.next;
linked = *bits & WORK_STRUCT_LINKED;
__set_bit(WORK_STRUCT_LINKED_BIT, bits);
}
// 将barr的work插入到要等待的work后面,当执行到barr的work的回调函数,
// 说明等待的work已经执行完成
->insert_work
spin_unlock_irq(&pool->lock);
......
return true;
already_gone:
spin_unlock_irq(&pool->lock);
// work的回调函数已经执行完毕
return false;
}
// barr中work的回调函数,
static void wq_barrier_func(struct work_struct *work)
{
struct wq_barrier *barr = container_of(work, struct wq_barrier, work);
complete(&barr->done); // 唤醒等待在完成量done上的线程
}
参考资料
- Linux kernel V4.6版本源码
- 《奔跑吧 Linux内核:基于Linux 4.x内核源代码问题分析》
- 《Linux内核深度解析》
- http://www.wowotech.net/irq_subsystem/cmwq-intro.html
- https://www.cnblogs.com/LoyenWang/p/13185451.html