Linux中断子系统之软中断、tasklet和工作队列
一、软中断
软中断使得内核可以延期执行任务。因为它们的运作方式与上文描述的中断类似,但完全是用软件实现的,所以称为软中断(softIRQ)。
1.1 软中断的初始化
在内核中有一个软件中断的数组:
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
softirq_action 这个数据结构用于描述一个软件中断。默认情况下,系统上只能使用 32 个软中断。
struct softirq_action
{
void (*action)(struct softirq_action *);
};
- action是一个指向处理程序例程的指针,在软中断发生时由内核执行该处理程序例程。
软中断必须先注册,内核才能执行软中断。open_softirq 函数即用于该目的,它在softirq_vec 表中指定的位置写入新的软中断:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
软中断的类型是通过枚举静态定义的,当前有10种软中断类型,从0到9。0的优先级最高,当系统调度到软中断时会最先执行。
enum
{
HI_SOFTIRQ=0, /* 高优先级tasklet软中断 */
TIMER_SOFTIRQ, /* 定时器软中断 */
NET_TX_SOFTIRQ, /* 发送网络包软中断 */
NET_RX_SOFTIRQ, /* 接收网络包软中断 */
BLOCK_SOFTIRQ, /* 用于处理块设备「block device」的软中断 */
IRQ_POLL_SOFTIRQ, /* 用于执行IOPOLL的回调函数 */
TASKLET_SOFTIRQ, /* tasklet软中断 */
SCHED_SOFTIRQ, /* 用于进程调度与负载均衡的软中断 */
HRTIMER_SOFTIRQ, /* 用于高精度定时器的软中断 */
RCU_SOFTIRQ, /* 用于RCU服务的软中断 */
NR_SOFTIRQS
};
在 start_kernel 函数会调用 softirq_init();
函数来初始化软件中断:
void __init softirq_init(void)
{
...
/* 注册tasklet相关的两个软中断 */
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
1.2 软中断的触发
系统通过一个per cpu的 irq_cpustat_t数据结构来记录软中断的触发与否,可以将其理解为软中断状态寄存器,该寄存器实际是一个uint32_t的__softirq_pending
变量,使用bitmap的形式,32个bit可以对应32个软中断,当然目前只使用了bit0到bit9这10个。
typedef struct {
unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;
触发软中断其实就是在__softirq_pending
相应的bit位上写1。触发软中断的接口有两个raise_softirq
和raise_softirq_irqoff
,前者在接口内部会关本地中断,而后者需要保证调用时已经处于关中断状态。
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
if (!in_interrupt())
wakeup_softirqd();
}
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
void __raise_softirq_irqoff(unsigned int nr)
{
lockdep_assert_irqs_disabled();
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}
1.3 软中断的执行
因为每个cpu都有一个软中断状态寄存器__softirq_pending
,因此每个CPU可以并行执行自己的软中断队列。软中断的执行时机有3个:
- 触发软中断后(执行
raise_softirq
和raise_softirq_irqoff
时); - 硬中断退出时(irq_exit时);
- 任何调用local_bh_enable打开本地软中断的时机。
有几种方法可开启软中断处理,但这些都归结为调用 do_softirq 函数。
asmlinkage __visible void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())
return;
local_irq_save(flags); /* 保存IF标志的状态值,并禁用本地CPU上的中断 */
pending = local_softirq_pending();
if (pending && !ksoftirqd_running(pending))
do_softirq_own_stack();
local_irq_restore(flags); /* 恢复保存的IF标志的状态值并返回 */
}
in_interrupt()
该函数确认当前不处于中断上下文中(当然,即不涉及硬件中断)。如果处于中断上下文,则立即结束。因为软中断用于执行 ISR 中非时间关键部分,所以其代码本身一定不能在中断处理程序内调用。
通过 local_softirq_pending,确定当前 CPU 软中断位图中所有置位的比特位。如果有软中断等待处理,则调用 __do_softirq。
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
unsigned long old_flags = current->flags;
int max_restart = MAX_SOFTIRQ_RESTART; /* 循环计数器的值初始化为10 */
struct softirq_action *h;
bool in_hardirq;
__u32 pending;
int softirq_bit;
/*
* Mask out PF_MEMALLOC as the current task context is borrowedfor the
* softirq. A softirq handled, such as network RX, might set PF_MEMALLOC
* again if the socket is related to swapping.
*/
current->flags &= ~PF_MEMALLOC;
/* 将本CPU的__softirq_pending变量复制到局部变量中 */
pending = local_softirq_pending();
account_irq_enter_time(current);
/* 关软中断,防止再次进入软中断执行逻辑 */
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
in_hardirq = lockdep_softirq_start();
restart:
/* 将__softirq_pending清零,以便可以激活新的软中断 */
set_softirq_pending(0);
/* 开启硬件中断 */
local_irq_enable();
h = softirq_vec;
while ((softirq_bit = ffs(pending))) {
unsigned int vec_nr;
int prev_count;
h += softirq_bit - 1;
vec_nr = h - softirq_vec;
prev_count = preempt_count();
/* 对softirq的执行次数计数,这是/proc/softirqs的数据来源 */
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr);
/* 某软中断对应的action回调 */
h->action(h);
trace_softirq_exit(vec_nr);
if (unlikely(prev_count != preempt_count())) {
pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
vec_nr, softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count_set(prev_count);
}
h++;
pending >>= softirq_bit;
}
if (__this_cpu_read(ksoftirqd) == current)
rcu_softirq_qs();
/* 处理完一波软中断后,关闭硬件中断 */
local_irq_disable();
/* 读一下当前的__softirq_pending,看这段时间有没有触发新的软中断 */
pending = local_softirq_pending();
/* 有新的软中断需要处理:
* 如果时间没超过2ms && 当前没有进程需要调度 && 这种循环没有超过10次,
* 此时重新跳到while循环处依次执行软中断的action回调;
* 否则唤醒ksoftirqd线程执行软中断处理。
*/
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
wakeup_softirqd();
}
lockdep_softirq_end(in_hardirq);
account_irq_exit_time(current);
/* 离开软中断上下文时,开启软中断 */
__local_bh_enable(SOFTIRQ_OFFSET);
WARN_ON_ONCE(in_interrupt());
current_restore_flags(old_flags, PF_MEMALLOC);
}
__do_softirq函数只做固定时间或固定次数的循环,然后就返回了,避免出现延迟用户进程的执行。如果还有其余挂起的软中断,内核线程ksoftirqd将会在预期时间里处理它们。
1.4 ksoftirqd内核线程
系统中的每个处理器都分配了自身的守护进程,名为 ksoftirqd。
static void wakeup_softirqd(void)
{
/* Interrupts are disabled: no need to stop preemption */
struct task_struct *tsk = __this_cpu_read(ksoftirqd);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
wake_up_process唤醒进程后运行run_ksoftirqd()函数。
static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable();
if (local_softirq_pending()) {
/*
* We can safely run softirq on inline stack, as we are not deep
* in the task stack here.
*/
__do_softirq();
local_irq_enable();
cond_resched();
return;
}
local_irq_enable();
}
二、tasklet
linux内核为什么还要引入tasklet机制呢?主要原因是软中断的pending标志位也就32位,一般情况是不随意增加软中断处理的。而且内核也没有提供通用的增加软中断的接口。其次内,软中断处理函数要求可重入,需要考虑到竞争条件比较多,要求比较高的编程技巧。所以内核提供了tasklet这样的一种通用的机制。
tasklet是I/O驱动程序中实现可延迟函数的首选方法。tasklet建立在两个叫做HI_SOFTIRQ和TASKLET_SOFTIRQ的软中断之上。tasklet和高优先级的tasklet分别存放在tasklet_vec和tasklet_hi_vec数组中。二者都包含类型为tasklet_head的NR_CPUS个元素,每个元素都由指向tasklet描述符链表的指针组成。
各个 tasklet 的数据结构称作 tasklet_struct,定义如下:
struct tasklet_struct
{
struct tasklet_struct *next; /* 将多个tasklet链接成单向循环链表 */
unsigned long state;
atomic_t count; /* 0:激活tasklet 非0:禁用tasklet */
bool use_callback;
union {
void (*func)(unsigned long data); /* 指向tasklet函数指针 */
void (*callback)(struct tasklet_struct *t);
};
unsigned long data; /* 用作函数执行时的参数 */
};
state表示任务的当前状态,有两个标志:
-
TASKLET_STATE_SCHED 表示tasklet是挂起的,等待调度执行时
-
TASKLET_STATE_RUN 表示 tasklet 当前正在执行。
tasklet_schedule处理过程也比较简单,就是把tasklet_struct结构体挂到tasklet_vec链表或者挂接到tasklet_hi_vec链表上,并调度软中断。
static void __tasklet_schedule_common(struct tasklet_struct *t,
struct tasklet_head __percpu *headp,
unsigned int softirq_nr)
{
struct tasklet_head *head;
unsigned long flags;
local_irq_save(flags);
head = this_cpu_ptr(headp);
t->next = NULL;
*head->tail = t;
head->tail = &(t->next);
raise_softirq_irqoff(softirq_nr);
local_irq_restore(flags);
}
tasklet执行过程tasklet_action_common在软中断被调度到后会被执行,从链表中把tasklet_struct结构体都取下来,然后逐个执行。如果t->count的值等于0,说明这个tasklet在调度之后,被disable掉了,所以会将tasklet结构体重新放回到tasklet_vec链表,并重新调度TASKLET_SOFTIRQ软中断,在之后enable这个tasklet之后重新再执行它。
static void tasklet_action_common(struct softirq_action *a,
struct tasklet_head *tl_head,
unsigned int softirq_nr)
{
struct tasklet_struct *list;
local_irq_disable();
list = tl_head->head;
tl_head->head = NULL;
tl_head->tail = &tl_head->head;
local_irq_enable();
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED,
&t->state))
BUG();
if (t->use_callback)
t->callback(t);
else
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = NULL;
*tl_head->tail = t;
tl_head->tail = &t->next;
__raise_softirq_irqoff(softirq_nr);
local_irq_enable();
}
}
因为一个 tasklet 只能在一个处理器上执行一次,但其他的 tasklet 可以并行运行,所以需要特定于 tasklet 的锁。 state 状态用作锁变量。在执行一个 tasklet 的处理程序函数之前,内核使用 tasklet_trylock 检查 tasklet 的状态是否为 TASKLET_STATE_RUN。
三、工作队列
从上面的介绍看以看出,软中断运行在中断上下文中,因此不能阻塞和睡眠,而tasklet使用软中断实现,当然也不能阻塞和睡眠。但如果某延迟处理函数需要睡眠或者阻塞呢?没关系工作队列就可以如您所愿了。
工作队列(work queue)是另外一种将工作推后执行的形式。工作队列可以把工作推后,交由一个内核线程去执行—这个下半部分总是会在进程上下文执行,但由于是内核线程,其不能访问用户空间。最重要特点的就是工作队列允许重新调度甚至是睡眠。实际上,工作队列的本质就是将工作交给内核线程处理,因此其可以用内核线程替换。但是内核线程的创建和销毁对编程者的要求较高,而工作队列实现了内核线程的封装,不易出错,所以也推荐使用工作队列。
3.1 关键数据结构
工作是实际需要被执行的用户工作,主体是一个工作函数。常用的工作分为两种:普通工作和延迟工作,延迟工作仅仅是添加了一个参数:延时时间,单位是 jiffies,工作队列会在延时时间过后被设置为就绪状态。
普通工作的结构体由 struct work_struct 描述:
struct work_struct {
atomic_long_t data; /* 传递给工作函数的参数 */
struct list_head entry; /* 链表结构,链接同一工作队列上的工作 */
work_func_t func; /* 工作函数,用户自定义实现 */
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map; /* 死锁检测 */
#endif
};
延迟工作的结构体由 struct delayed_work描述:
struct delayed_work {
struct work_struct work;
struct timer_list timer; /* 定时器,用于实现延迟处理 */
/* target workqueue and CPU ->timer uses to queue ->work */
struct workqueue_struct *wq; /* 当前工作相关的工作队列 */
int cpu;
};
- workqueue_struct:用于描述一个工作队列,在创建工作队列时返回该结构
struct workqueue_struct {
struct list_head pwqs; /* 链表头,该链表中挂着所有与当前 workqueue_struct 相关的 pool_workqueue */
struct list_head list; /* 链表节点,通过该链表节点将该 workqueue_struct 链接到全局链表 */
struct mutex mutex; /* protects this wq */
int work_color; /* WQ: current work color */
int flush_color; /* WQ: current flush color */
atomic_t nr_pwqs_to_flush; /* flush in progress */
struct wq_flusher *first_flusher; /* WQ: first flusher */
struct list_head flusher_queue; /* WQ: flush waiters */
struct list_head flusher_overflow; /* WQ: flush overflow list */
struct list_head maydays; /* 需要 rescue 执行的工作链表 */
struct worker *rescuer; /* rescue 内核线程,这是一个独立的内核线程 */
int nr_drainers; /* WQ: drain in progress */
int saved_max_active; /* WQ: saved pwq max_active */
struct workqueue_attrs *unbound_attrs; /* PW: only for unbound wqs */
struct pool_workqueue *dfl_pwq; /* PW: only for unbound wqs */
#ifdef CONFIG_SYSFS
struct wq_device *wq_dev; /* I: for sysfs interface */
#endif
#ifdef CONFIG_LOCKDEP
char *lock_name;
struct lock_class_key key;
struct lockdep_map lockdep_map;
#endif
char name[WQ_NAME_LEN]; /* I: workqueue name */
/*
* Destruction of workqueue_struct is RCU protected to allow walking
* the workqueues list without grabbing wq_pool_mutex.
* This is used to dump all workqueues from sysrq.
*/
struct rcu_head rcu;
/* hot fields used during command issue, aligned to cacheline */
unsigned int flags ____cacheline_aligned; /* WQ: WQ_* flags */
struct pool_workqueue __percpu *cpu_pwqs; /* 指向 percpu 类型的 pool_workqueue */
struct pool_workqueue __rcu *numa_pwq_tbl[]; /* 指向 pernode 类型的 pool_workqueue */
};
- pool_workqueue:属于 worker_pool 和 workqueue_struct 之间的中介,负责将 workqueue_struct 和 worker_pool 联系起来,一个 workqueue_struct 对应多个 pool_workqueue。
struct pool_workqueue {
struct worker_pool *pool; //当前 pool_workqueue 对应的 worker_pool。
struct workqueue_struct *wq; //当前 pool_workqueue 对应的 workqueue_struct。
int refcnt; //引用计数,内核的回收机制
int nr_active; //正处于活动状态的 work 数量。
int max_active; //处于活动状态的 work 最大值
struct list_head delayed_works; // delayed_work 链表,delayed_work 因为有个延时,需要记录起来
struct list_head pwqs_node; //节点,用于将当前的 pool_workqueue 结构链接到 wq->pwqs 中。
struct list_head mayday_node; //当前节点会被链接到 wq->maydays 链表中。
struct work_struct unbound_release_work;
} __aligned(1 << WORK_STRUCT_FLAG_BITS);
- struct worker_pool:工作队列池,存在多个,通常是对应 CPU 而存在的,系统默认会为每个 CPU 创建两个 worker_pool,一个普通的一个高优先级的 pool,这些 pool 是和对应 CPU 严格绑定的,同时还会创建两个 ubound 类型的 pool,也就是不与 CPU 绑定的 pool。
struct worker_pool {
spinlock_t lock; //该 worker_pool 的自旋锁
int cpu; //该 worker_pool 对应的 cpu
int node; //对应的 numa node
int id; //当前的 pool ID.
unsigned int flags; //标志位
unsigned long watchdog_ts; //看门狗,主要用作超时检测
struct list_head worklist; //核心的链表结构,当 work 添加到队列中时,实际上就是添加到了当前的链表节点上。
int nr_workers; //当前 worker_pool 上工作的数量
int nr_idle; //休眠状态下的数量
struct list_head idle_list; //链表,记录所有处于 idle 的 worker。
struct timer_list idle_timer; //内核定时器用于记录 idle worker 的超时
struct timer_list mayday_timer; // SOS timer
DECLARE_HASHTABLE(busy_hash, BUSY_WORKER_HASH_ORDER); //记录正运行的 worker
struct worker *manager; // 管理线程
struct list_head workers; // 当前 worker_pool 上的 worker 线程。
struct completion *detach_completion; //管理所有 worker 的 detach
struct workqueue_attrs *attrs; // worker 的属性
int refcnt; //引用计数
} ____cacheline_aligned_in_smp;
- worker 用于描述内核线程,由 worker_pool 产生,一个 worker_pool 可以产生多个 worker。
struct worker {
union {
struct list_head entry; // 如果当前 worker 处于 idle 状态,就使用这个节点链接到 worker_pool 的 idle 链表
struct hlist_node hentry; // 如果当前 worker 处于 busy 状态,就使用这个节点链接到 worker_pool 的 busy_hash 中。
};
struct work_struct *current_work; //当前需要执行的 worker
work_func_t current_func; //当前执行 worker 的函数
struct pool_workqueue *current_pwq; //当前 worker 对应的 pwq
struct list_head scheduled; //链表头节点,当准备或者运行一个 worker 的时候,将 work 连接到当前的链表中
struct task_struct *task; // 内核线程对应的 task_struct 结构
struct worker_pool *pool; // 该 worker 对应的 worker_pool
unsigned long last_active; //上一个活动 worker 时设置的 timestamp
unsigned int flags; //运行标志位
int id; //worker id
char desc[WORKER_DESC_LEN]; // work 的描述信息,主要是在 debug 时使用
struct workqueue_struct *rescue_wq; // 针对 rescue 使用的 workqueue。
};
3.2 初始化
3.2.1 工作的初始化
内核提供两种类型的接口对工作进行初始化:分别提供静态和动态的初始化。静态方法的第一个参数只需要提供一个工作名,系统就会创建一个 struct work_struct my_work 结构并对其成员。
DECLARE_WORK(my_work, work_callback)
动态初始化需要自定义一个 struct work_struct 类型结构,传入到 INIT_WORK() 中,传入的参数为指针,比如:
struct work_struct *my_work;
INIT_WORK(my_work,work_callback);
而对于延迟工作队列的初始化,也是提供两个接口:
DECLARE_DELAYED_WORK(n, f)
INIT_DELAYED_WORK(_work, _func)
延迟工作队列的初始化其实就是比普通工作队列多了一项:内核定时器的初始化。
3.2.2 工作队列的初始化
workqueue 初始化的开始有两个函数:
- workqueue_init_early
- workqueue_init
workqueue_init_early 直接在 start_kernel 中被调用,而 workqueue_init 的调用路径为:
start_kernel
rest_init
kernel_init
kernel_init_freeable
workqueue_init
从函数名可以看出,workqueue_init_early 负责前期的初始化工作:
int __init workqueue_init_early(void)
{
int std_nice[NR_STD_WORKER_POOLS] = { 0, HIGHPRI_NICE_LEVEL };
int i, cpu;
WARN_ON(__alignof__(struct pool_workqueue) < __alignof__(long long));
BUG_ON(!alloc_cpumask_var(&wq_unbound_cpumask, GFP_KERNEL));
cpumask_copy(wq_unbound_cpumask, cpu_possible_mask);
/* 创建 pool_workqueue 的高速缓存池 */
pwq_cache = KMEM_CACHE(pool_workqueue, SLAB_PANIC);
/* 初始化 percpu 类型的 worker_pool */
for_each_possible_cpu(cpu) { //遍历 cpu 操作
struct worker_pool *pool;
i = 0;
for_each_cpu_worker_pool(pool, cpu) {. //遍历 worker_pool 操作
BUG_ON(init_worker_pool(pool)); //初始化 worker_pool
pool->cpu = cpu; //绑定 worker_pool 和 cpu
cpumask_copy(pool->attrs->cpumask, cpumask_of(cpu)); //获取 cpu mask
pool->node = cpu_to_node(cpu); //获取 numa node 节点
worker_pool_assign_id(pool); //为当前 worker_pool 分配 id 号
mutex_lock(&wq_pool_mutex);
BUG_ON(worker_pool_assign_id(pool));
mutex_unlock(&wq_pool_mutex);
}
}
/* 创建各种类型的工作队列 */
for (i = 0; i < NR_STD_WORKER_POOLS; i++) {
struct workqueue_attrs *attrs;
BUG_ON(!(attrs = alloc_workqueue_attrs(GFP_KERNEL)));
attrs->nice = std_nice[i];
unbound_std_wq_attrs[i] = attrs;
BUG_ON(!(attrs = alloc_workqueue_attrs(GFP_KERNEL)));
attrs->nice = std_nice[i];
attrs->no_numa = true;
ordered_wq_attrs[i] = attrs;
}
system_wq = alloc_workqueue("events", 0, 0);
system_highpri_wq = alloc_workqueue("events_highpri", WQ_HIGHPRI, 0);
system_long_wq = alloc_workqueue("events_long", 0, 0);
system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND,
WQ_UNBOUND_MAX_ACTIVE);
system_freezable_wq = alloc_workqueue("events_freezable",
WQ_FREEZABLE, 0);
system_power_efficient_wq = alloc_workqueue("events_power_efficient",
WQ_POWER_EFFICIENT, 0);
system_freezable_power_efficient_wq = alloc_workqueue("events_freezable_power_efficient",
WQ_FREEZABLE | WQ_POWER_EFFICIENT,
0);
BUG_ON(!system_wq || !system_highpri_wq || !system_long_wq ||
!system_unbound_wq || !system_freezable_wq ||
!system_power_efficient_wq ||
!system_freezable_power_efficient_wq);
return 0;
}
- 初始化 percpu 类型的 worker_pool
在当前文件的头部,静态地创建了 percpu 类型的 worker_pool 结构:
static DEFINE_PER_CPU_SHARED_ALIGNED(struct worker_pool [NR_STD_WORKER_POOLS], cpu_worker_pools);
其中,NR_STD_WORKER_POOLS 的值默认为 2,也就是系统初始化时每个 cpu 定义两个静态的 worker_pool。其中,前面的两个 for_each 嵌套表示:以下的操作针对每个 cpu 下的每个 worker_pool。
- init_worker_pool 负责初始化 worker_pool
static int init_worker_pool(struct worker_pool *pool)
{
pool->id = -1;
pool->cpu = -1;
pool->node = NUMA_NO_NODE;
pool->flags |= POOL_DISASSOCIATED;
pool->watchdog_ts = jiffies;
INIT_LIST_HEAD(&pool->worklist);
INIT_LIST_HEAD(&pool->idle_list);
hash_init(pool->busy_hash);
setup_deferrable_timer(&pool->idle_timer, idle_worker_timeout,
(unsigned long)pool);
setup_timer(&pool->mayday_timer, pool_mayday_timeout,
(unsigned long)pool);
INIT_LIST_HEAD(&pool->workers);
ida_init(&pool->worker_ida);
INIT_HLIST_NODE(&pool->hash_node);
pool->refcnt = 1;
pool->attrs = alloc_workqueue_attrs(GFP_KERNEL);
}
- 创建各种类型的工作队列
初始化完成 worker_pool 之后,内核通过传入不同的 flag 多次调用 alloc_workqueue 创建了大量的工作队列:
system_wq = alloc_workqueue("events", 0, 0);
system_highpri_wq = alloc_workqueue("events_highpri", WQ_HIGHPRI, 0);
system_long_wq = alloc_workqueue("events_long", 0, 0);
system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND,WQ_UNBOUND_MAX_ACTIVE);
system_freezable_wq = alloc_workqueue("events_freezable",WQ_FREEZABLE, 0);
system_power_efficient_wq = alloc_workqueue("events_power_efficient",WQ_POWER_EFFICIENT, 0);
system_freezable_power_efficient_wq = alloc_workqueue("events_freezable_power_efficient",WQ_FREEZABLE | WQ_POWER_EFFICIENT,0);
- WQ_HIGHPRI 表示高优先级的工作队列
- WQ_UNBOUND 表示不和 cpu 绑定的工作队列
- WQ_FREEZABLE 表示可在挂起的时候,该工作队列也会被冻结
- WQ_POWER_EFFICIENT 表示节能型的工作队列
- WQ_MEM_RECLAIM 表示可在内存回收时调用
- WQ_CPU_INTENSIVE 表示 cpu 消耗型
- WQ_SYSFS 表示将当前工作队列导出到 sysfs 中
如果你的 work 有特殊的需求,除了调用 schedule_work(),同样可以调用 queue_work() 以指定将 work 加入到上述的某个工作队列中。
- alloc_workqueue实现
#define alloc_workqueue(fmt, flags, max_active, args...) \
__alloc_workqueue_key((fmt), (flags), (max_active), \
NULL, NULL, ##args)
alloc_workqueue 调用了 __alloc_workqueue_key:
struct workqueue_struct *__alloc_workqueue_key(const char *fmt,unsigned int flags,
int max_active,struct lock_class_key *key,const char *lock_name, ...)
{
struct workqueue_struct *wq;
struct pool_workqueue *pwq;
...
//申请一个 workqueue_struct 结构。
wq = kzalloc(sizeof(*wq) + tbl_size, GFP_KERNEL);
//如果是 unbound 类型的,就申请默认的 attrs,如果不是,就会使用 percpu worker_pool 的 attrs。
if (flags & WQ_UNBOUND) {
wq->unbound_attrs = alloc_workqueue_attrs(GFP_KERNEL);
}
//初始化 wq 的成员
wq->flags = flags;
wq->saved_max_active = max_active;
mutex_init(&wq->mutex);
atomic_set(&wq->nr_pwqs_to_flush, 0);
INIT_LIST_HEAD(&wq->pwqs);
INIT_LIST_HEAD(&wq->flusher_queue);
INIT_LIST_HEAD(&wq->flusher_overflow);
INIT_LIST_HEAD(&wq->maydays);
INIT_LIST_HEAD(&wq->list);
//申请并绑定 pwq 和 wq
alloc_and_link_pwqs(wq);
//如果设置了 WQ_MEM_RECLAIM 标志位,需要创建一个 rescue 线程。
if (flags & WQ_MEM_RECLAIM) {
struct worker *rescuer;
rescuer = alloc_worker(NUMA_NO_NODE);
if (!rescuer)
goto err_destroy;
rescuer->rescue_wq = wq;
rescuer->task = kthread_create(rescuer_thread, rescuer, "%s",
wq->name);
if (IS_ERR(rescuer->task)) {
kfree(rescuer);
goto err_destroy;
}
wq->rescuer = rescuer;
kthread_bind_mask(rescuer->task, cpu_possible_mask);
wake_up_process(rescuer->task);
}
//如果设置了 WQ_SYSFS,将当前 wq 导出到 sysfs 中。
if ((wq->flags & WQ_SYSFS) && workqueue_sysfs_register(wq))
goto err_destroy;
//将当前 wq 添加到全局 wq 链表 workqueues 中。
list_add_tail_rcu(&wq->list, &workqueues);
}
- rescue 线程
在上一章结构体的介绍中,经常可以看到 mayday 和 rescue 的身影,从字面意思来看,这是用来"救命"的东西,至于救谁的命呢?
结合 WQ_MEM_RECLAIM 标志以及它的注释可以了解到,当 linux 进行内存回收时,可能导致工作队列运行得没那么顺利,这时候需要有一个线程来保证 work 的持续运行,但是并不建议开发者无理由地添加这个 flag,因为这会实实在在地创建一个内核线程,正如前面的章节所说,过多的内核线程将会影响系统的执行效率。
但是实际上内核中大部分驱动程序的实现都使用了这个标志位。
- 申请 pwq 并绑定
这一部分由函数 : alloc_and_link_pwqs 完成,
static int alloc_and_link_pwqs(struct workqueue_struct *wq)
{
bool highpri = wq->flags & WQ_HIGHPRI;
int cpu, ret;
if (!(wq->flags & WQ_UNBOUND)) {
wq->cpu_pwqs = alloc_pe
rcpu(struct pool_workqueue);
if (!wq->cpu_pwqs)
return -ENOMEM;
for_each_possible_cpu(cpu) {
struct pool_workqueue *pwq =
per_cpu_ptr(wq->cpu_pwqs, cpu);
struct worker_pool *cpu_pools =
per_cpu(cpu_worker_pools, cpu);
init_pwq(pwq, wq, &cpu_pools[highpri]);
mutex_lock(&wq->mutex);
link_pwq(pwq);
mutex_unlock(&wq->mutex);
}
return 0;
} else if (wq->flags & __WQ_ORDERED) {
ret = apply_workqueue_attrs(wq, ordered_wq_attrs[highpri]);
WARN(!ret && (wq->pwqs.next != &wq->dfl_pwq->pwqs_node ||
wq->pwqs.prev != &wq->dfl_pwq->pwqs_node),
"ordering guarantee broken for workqueue %s\n", wq->name);
return ret;
} else {
return apply_workqueue_attrs(wq, unbound_std_wq_attrs[highpri]);
}
}
该函数主要包括两个部分:
-
针对非 unbound 类型的 wq,对每个 CPU 申请一个 pwq 结构,如果设置了 WQ_HIGHPRI(高优先级) 标志位,则将其与 percpu 的高优先级 worker_pool 进行绑定,否则与普通的 worker_pool 进行绑定,worker_pool 的创建在上文中有相应介绍。
-
初始化和绑定的过程也比较简单,主要是以下几个部分: 设置 pwq->pool 为当前 CPU 的 worker_pool,设置 pwq->wq 为当前被创建的 work_queue_struct. 初始化各种链表头、引用计数置为 1 * 将 pwq->pwqs_node 链接到 wq->pwqs 中,建立索引关系。
-
针对 unbound 类型的 wq,只需要设置属性即可。
我们继续来看另一个初始化函数:workqueue_init(),我们先看一下这个函数的总体实现,然后再进行局部分析:
int __init workqueue_init(void)
{
struct workqueue_struct *wq;
struct worker_pool *pool;
int cpu, bkt;
// NUMA 节点的初始化
wq_numa_init();
//设置 NUMA node
for_each_possible_cpu(cpu) {
for_each_cpu_worker_pool(pool, cpu) {
pool->node = cpu_to_node(cpu);
}
}
list_for_each_entry(wq, &workqueues, list)
wq_update_unbound_numa(wq, smp_processor_id(), true);
//对每个 CPU 的 worker_pool ,创建 worker 线程
for_each_online_cpu(cpu) {
for_each_cpu_worker_pool(pool, cpu) {
pool->flags &= ~POOL_DISASSOCIATED;
BUG_ON(!create_worker(pool));
}
}
//创建 unbound 线程
hash_for_each(unbound_pool_hash, bkt, pool, hash_node)
BUG_ON(!create_worker(pool));
...
return 0;
}
- 为 worker_pool 创建 worker 线程
通过系统提供的接口:create_worker(pool) 为每一个 worker_pool 构建 worker 并创建内核线程,传入的参数为 worker_pool。
static struct worker *create_worker(struct worker_pool *pool)
{
struct worker *worker = NULL;
int id = -1;
char id_buf[16];
// id 用作确定内核线程的名称
id = ida_simple_get(&pool->worker_ida, 0, 0, GFP_KERNEL);
//申请一个 worker 结构
worker = alloc_worker(pool->node);
worker->pool = pool;
worker->id = id;
//确定内核线程的名称
if (pool->cpu >= 0)
snprintf(id_buf, sizeof(id_buf), "%d:%d%s", pool->cpu, id,
pool->attrs->nice < 0 ? "H" : "");
else
snprintf(id_buf, sizeof(id_buf), "u%d:%d", pool->id, id);
//创建内核线程
worker->task = kthread_create_on_node(worker_thread, worker, pool->node,
"kworker/%s", id_buf);
set_user_nice(worker->task, pool->attrs->nice);
kthread_bind_mask(worker->task, pool->attrs->cpumask);
worker_attach_to_pool(worker, pool);
worker->pool->nr_workers++;
worker_enter_idle(worker);
//开始执行内核线程
wake_up_process(worker->task);
return worker;
}
- 申请 worker 结构
worker 结构体用于描述 workqueue 相关的内核线程,需要使用时要向系统申请并初始化一个 worker 结构,该操作由 alloc_worker 这个接口完成,参数 node 表示 NUMA node,如果是在 NUMA 系统上,则会在 pool->node 节点上进行申请,然后进行一些成员的初始化。
- 创建内核线程并绑定
创建内核线程和绑定主要是下面这两个接口:
worker->task = kthread_create_on_node(worker_thread, worker, pool->node,"kworker/%s", id_buf);
worker_attach_to_pool(worker, pool);
使用 kthread_create_on_node 接口创建内核线程以兼容 NUMA 系统,将返回的 task_struct 赋值给 worker,由 worker 对线程进行管理。worker_attach_to_pool 接口会将 worker 绑定到 worker_pool 上,其实就是执行下面这一段代码:
list_add_tail(&worker->node, &pool->workers);
通过 worker->node 节点链接到 worker_pool 的 workers 链表上。
当前 thread 的 thread_func 为 worker_thread ,参数为当前 worker,这是内核线程的执行主体部分,同样的,我们来看看它的源码实现,看看工作线程到底做了哪些事:
static int worker_thread(void *__worker)
{
struct worker *worker = __worker;
struct worker_pool *pool = worker->pool;
worker->task->flags |= PF_WQ_WORKER;
woke_up:
spin_lock_irq(&pool->lock);
//在必要的时候删除 worker,退出当前线程。
if (unlikely(worker->flags & WORKER_DIE)) {
spin_unlock_irq(&pool->lock);
WARN_ON_ONCE(!list_empty(&worker->entry));
worker->task->flags &= ~PF_WQ_WORKER;
set_task_comm(worker->task, "kworker/dying");
ida_simple_remove(&pool->worker_ida, worker->id);
worker_detach_from_pool(worker, pool);
kfree(worker);
return 0;
}
worker_leave_idle(worker);
recheck:
//管理 worker 线程
if (!need_more_worker(pool))
goto sleep;
if (unlikely(!may_start_working(pool)) && manage_workers(worker))
goto recheck;
//执行 work
do {
struct work_struct *work =
list_first_entry(&pool->worklist,
struct work_struct, entry);
pool->watchdog_ts = jiffies;
if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
process_one_work(worker, work);
if (unlikely(!list_empty(&worker->scheduled)))
process_scheduled_works(worker);
} else {
move_linked_works(work, &worker->scheduled, NULL);
process_scheduled_works(worker);
}
} while (keep_working(pool));
worker_set_flags(worker, WORKER_PREP);
sleep:
//处理完成,陷入睡眠
worker_enter_idle(worker);
__set_current_state(TASK_IDLE);
spin_unlock_irq(&pool->lock);
schedule();
goto woke_up;
}
- 管理 worker
从 worker 中获取与其绑定的 worker_pool,在 worker_pool 的基础上对运行在其上的 worker 进行管理,主要分为两个部分:检查删除和检查创建。
检查删除主要是检查 worker->flags & WORKER_DIE 标志位是否被置位,如果该位被置位,将释放当前的 worker ,当前线程返回,回收相应的资源。这个标志位是通过 destroy_worker 函数置位,删除的逻辑就是当前 worker_pool 中有冗余的 worker,长时间进入 idle 状态没有被使用。
检查创建主要由 manage_workers 实现,当 worker_pool 的负载增高时,需要动态地新创建 worker 来执行 work,每个 pool 至少保证有一个 idle worker 以响应即将到来的 work。
worker 的创建和删除完全取决于需要执行 work 数量,而这也是整个 work queue 新框架的核心部分。
- 执行 work
worker 用于管理线程,而线程自然是用于执行具体的工作,从源码中不难看到,当前 worker 的线程被唤醒后将会从第一个 worker_pool->worklist 中的链表元素,也就是挂入当前 worker_pool 的 work 开始,取出其中的 work,然后执行,执行的接口使用:
move_linked_works(work, &worker->scheduled, NULL);
process_scheduled_works(worker);
move_linked_works 将会在执行前将 work 添加到 worker->scheduled 链表中,该接口和 list_add_tail 不同的是,这个接口会先删除链表中存在的节点并重新添加,保证不会重复添加,且始终添加到最后一个节点。
然后调用 process_scheduled_works 函数正式执行 work,该函数会遍历 worker->scheduled 链表,执行每一个 work,执行之前会做一些必要的检查,比如在同一个 cpu 上,一个 worker 不能在多个 worker 线程中被并发执行(这里的并发执行指的是同时加入到 schedule 链表),是否需要唤醒其它的 worker 来协助执行(碰到 cpu 消耗型的 work 需要这么做),执行 work 的方式就是调用 work->func,。
当执行完 worker_pool->worklist 中所有的 work 之后,当前线程就会陷入睡眠。
3.3 工作队列中添加工作
schedule_work 会将 work 添加到默认的工作队列也就是 system_wq 中,如果需要添加到指定的工作队列,可以调用 queue_work(wq,work) 接口,第一个参数就是指定的 workqueue_struct 结构。
schedule_work 其实就是基于 queue_work 的一层封装:
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}
接着查看 queue_work 的实现:
static inline bool queue_work(struct workqueue_struct *wq,
struct work_struct *work)
{
return queue_work_on(WORK_CPU_UNBOUND, wq, work);
}
继续调用 queue_work_on ,增加一个参数 WORK_CPU_UNBOUND,这个参数并不是指将当前 work 绑定到 unbound 类型的 worker_pool 中,只是说明调用者并不指定将当前 work 绑定到哪个 cpu 上,由系统来分配 cpu.当然,调用者也可以直接使用 queue_work_on 接口,通过第一个参数来指定当前 work 绑定的 cpu。
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;
}
如果没有为当前的 work 设置 WORK_STRUCT_PENDING_BIT 标志位,当当前 work 已经被添加到某个工作队列时,该标志位被置位,与 tasklet 和 softirq 的区别在于,softirq 支持多 cpu 上的并发执行,所以要求执行函数可重入,而 tasklet 不允许多 cpu 上的并发执行,编程相对简单。workqueue 机制中,不允许同一个 work 同时被加入到一个或多个工作队列中,只有当 work 正在执行或者已经执行完成,才能重新添加该 work 到工作队列中,所以也不存在多 cpu 并发执行的问题。
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;
/*
* While a work item is PENDING && off queue, a task trying to
* steal the PENDING will busy-loop waiting for it to either get
* queued or lose PENDING. Grabbing PENDING and queueing should
* happen with IRQ disabled.
*/
lockdep_assert_irqs_disabled();
/* if draining, only works from the same workqueue are allowed */
if (unlikely(wq->flags & __WQ_DRAINING) &&
WARN_ON_ONCE(!is_chained_work(wq)))
return;
rcu_read_lock();
retry:
/* pwq which will be used unless @work is executing elsewhere */
if (wq->flags & WQ_UNBOUND) {
if (req_cpu == WORK_CPU_UNBOUND)
cpu = wq_select_unbound_cpu(raw_smp_processor_id());
pwq = unbound_pwq_by_node(wq, cpu_to_node(cpu));
} else {
if (req_cpu == WORK_CPU_UNBOUND)
cpu = raw_smp_processor_id();
pwq = per_cpu_ptr(wq->cpu_pwqs, cpu);
}
/* 检查当前的 work 是不是在这之前被添加到其他 worker_pool 中,
如果是,就让它继续在原本的 worker_pool 上运行 */
last_pool = get_work_pool(work);
if (last_pool && last_pool != pwq->pool) {
struct worker *worker;
raw_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 */
raw_spin_unlock(&last_pool->lock);
raw_spin_lock(&pwq->pool->lock);
}
} else {
raw_spin_lock(&pwq->pool->lock);
}
/*
* pwq is determined and locked. For unbound pools, we could have
* raced with pwq release and it could already be dead. If its
* refcnt is zero, repeat pwq selection. Note that pwqs never die
* without another pwq replacing it in the numa_pwq_tbl or while
* work items are executing on it, so the retrying is guaranteed to
* make forward-progress.
*/
if (unlikely(!pwq->refcnt)) {
if (wq->flags & WQ_UNBOUND) {
raw_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);
}
/* pwq determined, queue */
trace_workqueue_queue_work(req_cpu, pwq, work);
if (WARN_ON(!list_empty(&work->entry)))
goto out;
pwq->nr_in_flight[pwq->work_color]++;
work_flags = work_color_to_flags(pwq->work_color);
//如果超过 pwq 支持的最大的 work 数量,将work添加到 pwq->delayed_works 中,否则就添加到 pwq->pool->worklist 中。
if (likely(pwq->nr_active < pwq->max_active)) {
trace_workqueue_activate_work(work);
pwq->nr_active++;
worklist = &pwq->pool->worklist;
if (list_empty(worklist))
pwq->pool->watchdog_ts = jiffies;
} else {
work_flags |= WORK_STRUCT_DELAYED;
worklist = &pwq->delayed_works;
}
debug_work_activate(work);
//添加 work 到队列中。
insert_work(pwq, work, worklist, work_flags);
out:
raw_spin_unlock(&pwq->pool->lock);
rcu_read_unlock();
}
insert_work 的源码实现也比较简单:
static void insert_work(struct pool_workqueue *pwq, struct work_struct *work,
struct list_head *head, unsigned int extra_flags)
{
struct worker_pool *pool = pwq->pool;
//设置 work 的 pwq 和 flag。
set_work_pwq(work, pwq, extra_flags);
//将 work 添加到 worklist 链表中
list_add_tail(&work->entry, head);
//为 pwq 添加引用计数
get_pwq(pwq);
//添加内存屏障,防止 cpu 将指令乱序排列
smp_mb();
//唤醒 worker 对应的内核线程
if (__need_more_worker(pool))
wake_up_worker(pool);
}
这个添加的过程中,pwq 和 workqueue_struct、worker_pool 是如何进行交互的?
答案其实也很简单,获取当前 cpu 的 id,通过该 cpu id 获取当前被使用的 wq 绑定的 pwq,通过 pwq 就可以找到对应的 worker_pool,在这里 pwq 相当于 worker_pool 和 wq 之间的媒介。
等待执行完成
如果需要确定指定的 work 是否执行完成,可以使用内核提供的接口:
bool flush_work(struct work_struct *work)
这个接口在 work 未完成时会被阻塞直到 work 执行完成,返回 true,但是如果指定的 work 进入了 idle 状态,会返回 false。
需要注意的是:一个 work 在执行期间可能会被添加到多个工作队列中,flush_work 将会等待所有 work 执行完成。
针对延迟工作而言,内核接口使用 flush_delayed_work:
bool flush_delayed_work(struct delayed_work *dwork)
同时需要注意的是,在延迟工作对象上调用 flush 将会取消 delayed_work 的延时时间,也就是会将 delayed_work 立即添加到工作队列并调度执行。
取消工作
另一个类似的接口是:
bool cancel_work_sync(struct work_struct *work)
这个接口会取消指定的工作,如果该工作已经在运行,该函数将会阻塞直到它完成,对于其它添加到工作队列的工作,将会取消它们。