GIC,Generic Interrupt Controller。是ARM公司提供的一个通用的中断控制器。主要作用为:接受硬件中断信号,并经过一定处理后,分发给对应的CPU进行处理。
当前GIC 有四个版本,GIC v1~v4, 本文主要介绍GIC v3控制器。
GICv3定义了以下中断类型:
中断类型 | 硬件中断号 |
---|---|
SGI | 0-15 |
PPI | 16-31 |
SPI | 32-1019 |
reserved | ...... |
LPI | 8192-MAX |
GICv3 控制器由以下三部分组成:
GICv3 控制器内部模块和各中断类型的关系如下图所示:
GICv3 使用 hierarchy 来标识一个具体的 core, 如下图是一个四层的结构(aarch64):
用 ... 的形式组成一个 PE 的路由。每一个 core 的 affnity 值可以通过 MPDIR_EL1 寄存器获取, 每一个 affinity 占用8bit。配置对应 core 的 MPIDR 值,可以将中断路由到该 core 上。
各个 affinity 的定义是根据 SOC 自己的定义,比如:
. ..
...
中断亲和性的设置的通用函数为 irq_set_affinity,后面会做详细介绍。
中断处理的状态机如下图:
这里主要分析 linux kernel 中 GIC v3 中断控制器的代码(drivers/irqchip/irq-gic-v3.c)。
先来看下一个中断控制器的设备树信息:
gic: interrupt-controller@51a00000 {
compatible = "arm,gic-v3";
reg = <0x0 0x51a00000 0 0x10000>, /* GIC Dist */
<0x0 0x51b00000 0 0xC0000>, /* GICR */
<0x0 0x52000000 0 0x2000>, /* GICC */
<0x0 0x52010000 0 0x1000>, /* GICH */
<0x0 0x52020000 0 0x20000>; /* GICV */
#interrupt-cells = <3>;
interrupt-controller;
interrupts = ;
interrupt-parent = <&gic>;
};
关于设备数的各个字段含义,详细可以参考 Documentation/devicetree/bindings 下的对应信息。
1. irq chip driver 的声明:
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);
定义 IRQCHIP_DECLARE 之后,相应的内容会保存到 __irqchip_of_table 里边:
#define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)
#define OF_DECLARE_2(table, name, compat, fn)
_OF_DECLARE(table, name, compat, fn, of_init_fn_2)
#define _OF_DECLARE(table, name, compat, fn, fn_type)
static const struct of_device_id __of_table_##name
__used __section(__##table##_of_table)
= { .compatible = compat,
.data = (fn == (fn_type)NULL) ? fn : fn }
__irqchip_of_table 在链接脚本 vmlinux.lds 里,被放到了 __irqchip_begin 和 __irqchip_of_end 之间,该段用于存放中断控制器信息:
#ifdef CONFIG_IRQCHIP
#define IRQCHIP_OF_MATCH_TABLE()
. = ALIGN(8);
VMLINUX_SYMBOL(__irqchip_begin) = .;
*(__irqchip_of_table)
*(__irqchip_of_end)
#endif
在内核启动初始化中断的函数中,of_irq_init 函数会去查找设备节点信息,该函数的传入参数就是 __irqchip_of_table 段,由于 IRQCHIP_DECLARE 已经将信息填充好了,of_irq_init 函数会根据 “arm,gic-v3” 去查找对应的设备节点,并获取设备的信息。or_irq_init 函数中,最终会回调 IRQCHIP_DECLARE 声明的回调函数,也就是 gic_of_init,而这个函数就是 GIC 驱动的初始化入口。
2. gic_of_init 流程:
static int __init gic_of_init(struct device_node *node, struct device_node *parent)
{
......
dist_base = of_iomap(node, 0); ------(1)
if (!dist_base) {
pr_err("%pOF: unable to map gic dist registersn", node);
return -ENXIO;
}
err = gic_validate_dist_version(dist_base); ------(2)
if (err) {
pr_err("%pOF: no distributor detected, giving upn", node);
goto out_unmap_dist;
}
if (of_property_read_u32(node, "#redistributor-regions", &nr_redist_regions)) ------(3)
nr_redist_regions = 1;
rdist_regs = kzalloc(sizeof(*rdist_regs) * nr_redist_regions, GFP_KERNEL);
if (!rdist_regs) {
err = -ENOMEM;
goto out_unmap_dist;
}
for (i = 0; i < nr_redist_regions; i++) { ------(4)
struct resource res;
int ret;
ret = of_address_to_resource(node, 1 + i, &res);
rdist_regs[i].redist_base = of_iomap(node, 1 + i);
if (ret || !rdist_regs[i].redist_base) {
pr_err("%pOF: couldn't map region %dn", node, i);
err = -ENODEV;
goto out_unmap_rdist;
}
rdist_regs[i].phys_base = res.start;
}
if (of_property_read_u64(node, "redistributor-stride", &redist_stride)) ------(5)
redist_stride = 0;
err = gic_init_bases(dist_base, rdist_regs, nr_redist_regions, ------(6)
redist_stride, &node->fwnode);
if (err)
goto out_unmap_rdist;
gic_populate_ppi_partitions(node); ------(7)
gic_of_setup_kvm_info(node);
return 0;
......
return err;
}
static int __init gic_init_bases(void __iomem *dist_base,
struct redist_region *rdist_regs,
u32 nr_redist_regions,
u64 redist_stride,
struct fwnode_handle *handle)
{
......
typer = readl_relaxed(gic_data.dist_base + GICD_TYPER); ------(1)
gic_data.rdists.id_bits = GICD_TYPER_ID_BITS(typer);
gic_irqs = GICD_TYPER_IRQS(typer);
if (gic_irqs > 1020)
gic_irqs = 1020;
gic_data.irq_nr = gic_irqs;
gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops, ------(2)
&gic_data);
gic_data.rdists.rdist = alloc_percpu(typeof(*gic_data.rdists.rdist));
gic_data.rdists.has_vlpis = true;
gic_data.rdists.has_direct_lpi = true;
......
set_handle_irq(gic_handle_irq); ------(3)
gic_update_vlpi_properties(); ------(4)
if (IS_ENABLED(CONFIG_ARM_GIC_V3_ITS) && gic_dist_supports_lpis())
its_init(handle, &gic_data.rdists, gic_data.domain); ------(5)
gic_smp_init(); ------(6)
gic_dist_init(); ------(7)
gic_cpu_init(); ------(8)
gic_cpu_pm_init(); ------(9)
return 0;
......
}
当早期的系统只存在一个中断控制器,而且中断数目也不多的时候,一个很简单的做法就是一个中断号对应到中断控制器的一个号,可以说是简单的线性映射:
但当一个系统中有多个中断控制器,而且中断号也逐渐增加的时候。linux 内核为了应对此问题,引入了 irq_domain 的概念。
irq_domain 的引入相当于一个中断控制器就是一个 irq_domain。这样一来所有的中断控制器就会出现级联的布局。利用树状的结构可以充分的利用 irq 数目,而且每一个 irq_domain 区域可以自己去管理自己 interrupt 的特性。
每一个中断控制器对应多个中断号, 而硬件中断号在不同的中断控制器上是会重复编码的, 这时仅仅用硬中断号已经不能唯一标识一个外设中断,因此 linux kernel 提供了一个虚拟中断号的概念。
接下来我们看下硬件中断号是如何映射到虚拟中断号的。
在看硬件中断号映射到虚拟中断号之前,先来看下几个比较重要的数据结构。
struct irq_desc 描述一个外设的中断,称之中断描述符。
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
......
struct irqaction *action;
......
} ____cacheline_internodealigned_in_smp;
struct irq_data 包含中断控制器的硬件数据。
struct irq_data {
u32 mask;
unsigned int irq;
unsigned long hwirq;
struct irq_common_data *common;
struct irq_chip *chip;
struct irq_domain *domain;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_data *parent_data;
#endif
void *chip_data;
};
struct irq_chip 用于对中断控制器的硬件操作。
struct irq_chip {
struct device *parent_device;
const char *name;
unsigned int (*irq_startup)(struct irq_data *data);
void (*irq_shutdown)(struct irq_data *data);
void (*irq_enable)(struct irq_data *data);
void (*irq_disable)(struct irq_data *data);
void (*irq_ack)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data);
void (*irq_mask_ack)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*irq_eoi)(struct irq_data *data);
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
int (*irq_retrigger)(struct irq_data *data);
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
int (*irq_set_wake)(struct irq_data *data, unsigned int on);
void (*irq_bus_lock)(struct irq_data *data);
void (*irq_bus_sync_unlock)(struct irq_data *data);
......
};
struct irq_domain 与中断控制器对应,完成硬件中断号 hwirq 到 virq 的映射。
struct irq_domain {
struct list_head link;
const char *name;
const struct irq_domain_ops *ops;
void *host_data;
unsigned int flags;
unsigned int mapcount;
/* Optional data */
struct fwnode_handle *fwnode;
enum irq_domain_bus_token bus_token;
struct irq_domain_chip_generic *gc;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_domain *parent;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file;
#endif
/* reverse map data. The linear map gets appended to the irq_domain */
irq_hw_number_t hwirq_max;
unsigned int revmap_direct_max_irq;
unsigned int revmap_size;
struct radix_tree_root revmap_tree;
unsigned int linear_revmap[];
};
struct irq_domain_ops 是 irq_domain 映射操作函数集。
struct irq_domain_ops {
int (*match)(struct irq_domain *d, struct device_node *node,
enum irq_domain_bus_token bus_token);
int (*select)(struct irq_domain *d, struct irq_fwspec *fwspec,
enum irq_domain_bus_token bus_token);
int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);
void (*unmap)(struct irq_domain *d, unsigned int virq);
int (*xlate)(struct irq_domain *d, struct device_node *node,
const u32 *intspec, unsigned int intsize,
unsigned long *out_hwirq, unsigned int *out_type);
......
};
struct irqaction 主要是用来存设备驱动注册的中断处理函数。
struct irqaction {
irq_handler_t handler;
void *dev_id;
......
unsigned int irq;
unsigned int flags;
......
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;
这里,我们用一张图来汇总下上面的数据结构:
上面的结构体 struct irq_desc 是设备驱动加载的过程中完成的,让设备树中的中断能与具体的中断描述符 irq_desc 匹配,其中 struct irqaction 保存着设备的中断处理函数。右边框内的结构体主要是在中断控制器驱动加载的过程中完成的,其中 struct irq_chip 用于对中断控制器的硬件操作,struct irq_domain 用于硬件中断号到 Linux irq 的映射。
下面我们结合代码看下中断控制器驱动和设备驱动是如何创建这些结构体,并且硬中断和虚拟中断号是如何完成映射的。
通过 __irq_domain_add 初始化 irq_domain 数据结构,然后把 irq_domain 添加到全局的链表 irq_domain_list 中。
设备的驱动在初始化的时候可以调用 irq_of_parse_and_map 这个接口函数进行该 device node 中和中断相关的内容的解析,并建立映射关系
最后,我们可以通过 /proc/interrupts 下的值来看下它们的关系:
现在,我们已经知道内核为硬件中断号与 Linux 中断号做了映射,相关数据结构的绑定及初始化,并且设置了中断处理函数执行的入口。接下来我们再看下设备的中断是怎么来注册的?
设备驱动中,获取到了 irq 中断号后,通常就会采用 request_irq/request_threaded_irq 来注册中断,其中 request_irq 用于注册普通处理的中断。request_threaded_irq 用于注册线程化处理的中断,线程化中断的主要目的把中断上下文的任务迁移到线程中,减少系统关中断的时间,增强系统的实时性。
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
其中 irq 是 linux 中断号,handler 是中断处理函数,flags 是中断标志位,name 是中断的名字。在讲具体的注册流程前,先看一下主要的中断标志位:
#define IRQF_SHARED 0x00000080 //多个设备共享一个中断号,需要外设硬件支持
#define IRQF_PROBE_SHARED 0x00000100 //中断处理程序允许sharing mismatch发生
#define __IRQF_TIMER 0x00000200 //时钟中断
#define IRQF_PERCPU 0x00000400 //属于特定CPU的中断
#define IRQF_NOBALANCING 0x00000800 //禁止在CPU之间进行中断均衡处理
#define IRQF_IRQPOLL 0x00001000 //中断被用作轮训
#define IRQF_ONESHOT 0x00002000 //一次性触发的中断,不能嵌套,1)在硬件中断处理完成后才能打开中断;2)在中断线程化中保持关闭状态,直到该中断源上的所有thread_fn函数都执行完
#define IRQF_NO_SUSPEND 0x00004000 //系统休眠唤醒操作中,不关闭该中断
#define IRQF_FORCE_RESUME 0x00008000 //系统唤醒过程中必须强制打开该中断
#define IRQF_NO_THREAD 0x00010000 //禁止中断线程化
#define IRQF_EARLY_RESUME 0x00020000 //系统唤醒过程中在syscore阶段resume,而不用等到设备resume阶段
#define IRQF_COND_SUSPEND 0x00040000 //与NO_SUSPEND的用户共享中断时,执行本设备的中断处理函数
创建完成后,通过 ps 命令可以查看系统中的中断线程,注意这些线程是实时线程 SCHED_FIFO:
# ps -A | grep "irq/"
root 1749 2 0 0 irq_thread 0 S [irq/433-imx_drm]
root 1750 2 0 0 irq_thread 0 S [irq/439-imx_drm]
root 1751 2 0 0 irq_thread 0 S [irq/445-imx_drm]
root 1752 2 0 0 irq_thread 0 S [irq/451-imx_drm]
root 2044 2 0 0 irq_thread 0 S [irq/279-isl2902]
root 2192 2 0 0 irq_thread 0 S [irq/114-mmc0]
root 2199 2 0 0 irq_thread 0 S [irq/115-mmc1]
root 2203 2 0 0 irq_thread 0 S [irq/322-5b02000]
root 2361 2 0 0 irq_thread 0 S [irq/294-4-0051]
当完成中断的注册后,所有结构的组织关系都已经建立好,剩下的工作就是当信号来临时,进行中断的处理工作。这里我们站在前面知识点的基础上,把中断触发,中断处理等整个流程走一遍。
假设当前在 EL0 运行一个应用程序,触发了一个 EL0 的 irq 中断,则处理器会做如下的操作:
先会跳到 arm64 对应的异常向量表:
/*
* Exception vectors.
*/
.pushsection ".entry.text", "ax"
.align 11
SYM_CODE_START(vectors)
......
kernel_ventry 1, sync // el1 下的同步异常,例如指令执行异常、缺页中断等
kernel_ventry 1, irq // el1 下的异步异常,硬件中断。1代表异常等级
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error // Error EL1h
kernel_ventry 0, sync // el0 下的同步异常,例如指令执行异常、缺页中断(跳转地址或者取地址)、系统调用等
kernel_ventry 0, irq // el0 下的异步异常,硬件中断。0代表异常等级
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error // Error 64-bit EL0
......
#endif
SYM_CODE_END(vectors)
arm64 的异常向量表 vectors 中设置了各种异常的入口。kernel_ventry 展开后,可以看到有效的异常入口有两个同步异常 el0_sync,el1_sync 和两个异步异常 el0_irq,el1_irq,其他异常入口暂时都 invalid。中断属于异步异常。
通过上图,我们可以看出中断的处理分为三个部分,保护现场,中断处理,恢复现场。其中 el0_irq 和 el1_irq 的具体实现略有不同,但处理流程大致是相同的。接下来我们以 el0_irq 为例对上面三个步骤进行梳理。
kernel_entry 0,其中 kernel_entry 是一个宏,此宏会将 CPU 寄存器按照 pt_regs 结构体的定义将第一现场保存到栈上。
.macro kernel_entry, el, regsize = 64
.if regsize == 32
mov w0, w0 // zero upper 32 bits of x0
.endif
stp x0, x1, [sp, #16 * 0]
stp x2, x3, [sp, #16 * 1]
stp x4, x5, [sp, #16 * 2]
stp x6, x7, [sp, #16 * 3]
stp x8, x9, [sp, #16 * 4]
stp x10, x11, [sp, #16 * 5]
stp x12, x13, [sp, #16 * 6]
stp x14, x15, [sp, #16 * 7]
stp x16, x17, [sp, #16 * 8]
stp x18, x19, [sp, #16 * 9]
stp x20, x21, [sp, #16 * 10]
stp x22, x23, [sp, #16 * 11]
stp x24, x25, [sp, #16 * 12]
stp x26, x27, [sp, #16 * 13]
stp x28, x29, [sp, #16 * 14]
.if el == 0
clear_gp_regs
mrs x21, sp_el0
ldr_this_cpu tsk, __entry_task, x20
msr sp_el0, tsk
enable_da_f 是关闭中断。
/* IRQ is the lowest priority flag, unconditionally unmask the rest. */
.macro enable_da_f
msr daifclr, #(8 | 4 | 1)
.endm
总之,保存现场主要是下面三个操作:
保存过现场后,即将跳入中断处理 irq_handler。
/*
* Interrupt handling.
*/
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry //进入中断栈
blr x1 //执行 handle_arch_irq
irq_stack_exit //退出中断栈
.endm
这里主要做了三个动作:
中断栈用来保存中断的上下文,中断发生和退出的时候调用 irq_stack_entry 和 irq_stack_exit 来进入和退出中断栈。中断栈是在内核启动时就创建好的,内核在启动过程中会去为每个 CPU 创建一个 per cpu 的中断栈:start_kernel->init_IRQ->init_irq_stacks
那中断控制器的 handle_arch_irq 又指向哪里呢?其实上面我们有讲到,在内核启动过程中初始化中断控制器时,设置了具体的 handler,gic_init_bases->set_handle_irq 将 handle_arch_irq 指针指向 gic_handle_irq 函数。代码如下:
void __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
{
if (handle_arch_irq)
return;
handle_arch_irq = handle_irq;
}
static int __init gic_init_bases(void __iomem *dist_base,
struct redist_region *rdist_regs,
u32 nr_redist_regions,
u64 redist_stride,
struct fwnode_handle *handle)
{
set_handle_irq(gic_handle_irq);
}
所以,中断处理最终会进入 gic_handle_irq:
static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u32 irqnr;
do {
irqnr = gic_read_iar(); ------(1)
if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) { ------(2)
int err;
if (static_key_true(&supports_deactivate))
gic_write_eoir(irqnr);
else
isb();
err = handle_domain_irq(gic_data.domain, irqnr, regs); ------(3)
if (err) {
WARN_ONCE(true, "Unexpected interrupt received!n");
if (static_key_true(&supports_deactivate)) {
if (irqnr < 8192)
gic_write_dir(irqnr);
} else {
gic_write_eoir(irqnr);
}
}
continue;
}
if (irqnr < 16) { ------(4)
gic_write_eoir(irqnr);
if (static_key_true(&supports_deactivate))
gic_write_dir(irqnr);
#ifdef CONFIG_SMP
/*
* Unlike GICv2, we don't need an smp_rmb() here.
* The control dependency from gic_read_iar to
* the ISB in gic_write_eoir is enough to ensure
* that any shared data read by handle_IPI will
* be read after the ACK.
*/
handle_IPI(irqnr, regs); ------(5)
#else
WARN_ONCE(true, "Unexpected SGI received!n");
#endif
continue;
}
} while (irqnr != ICC_IAR1_EL1_SPURIOUS);
}
中断控制器中断处理的主体,如下所示:
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
bool lookup, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
unsigned int irq = hwirq;
int ret = 0;
irq_enter(); ------(1)
#ifdef CONFIG_IRQ_DOMAIN
if (lookup)
irq = irq_find_mapping(domain, hwirq); ------(2)
#endif
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (unlikely(!irq || irq >= nr_irqs)) {
ack_bad_irq(irq);
ret = -EINVAL;
} else {
generic_handle_irq(irq); ------(3)
}
irq_exit(); ------(4)
set_irq_regs(old_regs);
return ret;
}
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
desc->handle_irq(desc);
}
调用 desc->handle_irq 指向的回调函数
irq_domain_set_info 根据硬件中断号的范围设置 irq_desc->handle_irq 的指针,共享中断入口为 handle_fasteoi_irq,私有中断入口为 handle_percpu_devid_irq。如下所示:
handle_percpu_devid_irq:处理私有中断处理,在这个过程中会分别调用中断控制器的处理函数进行硬件操作,该函数调用 action->handler() 来进行中断处理
handle_fasteoi_irq:处理共享中断,并且遍历 irqaction 链表,逐个调用 action->handler() 函数,这个函数正是设备驱动程序调用 request_irq/request_threaded_irq 接口注册的中断处理函数,此外如果中断线程化处理的话,还会调用 __irq_wake_thread 唤醒内核线程。
SYM_CODE_START_LOCAL(ret_to_user)
disable_daif //D A I F 分别为PSTAT中的四个异常屏蔽标志位,此处屏蔽这4中异常
gic_prio_kentry_setup tmp=x3
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_off
#endif
ldr x19, [tsk, #TSK_TI_FLAGS] //获取 thread_info 中的flags变量的值
and x2, x19, #_TIF_WORK_MASK
cbnz x2, work_pending
finish_ret_to_user:
user_enter_irqoff
/* Ignore asynchronous tag check faults in the uaccess routines */
clear_mte_async_tcf
enable_step_tsk x19, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
bl stackleak_erase
#endif
kernel_exit 0 //恢复 pt_regs 中的寄存器上下文
主要分三步:
上面讲了中断控制器和设备驱动的初始化。包括从设备树获取中断源信息的解析,硬件中断号到 Linux 中断号的映射关系,irq_desc 等各个结构的分配及初始化、中断的注册等等,总而言之,就是完成静态关系创建,为中断处理做好准备。
当外设触发中断信号时,中断控制器接收到信号并发送到处理器,此时处理器进行异常模式切换,如果涉及到中断线程化,则还需要进行中断内核线程的唤醒操作,最终完成中断处理函数的执行。
最后,用一张图来汇总中断控制器和设备驱动的来龙去脉:
workqueue 是除了 softirq 和 tasklet 以外最常用的下半部机制之一。workqueue 的本质是把 work 交给一个内核线程,在进程上下文调度的时候执行。因为这个特点,所以 workqueue 允许重新调度和睡眠,这种异步执行的进程上下文,能解决因为 softirq 和 tasklet 执行时间长而导致的系统实时性下降等问题。
当驱动程序在进程上下文中有异步执行的工作任务时,可以用 work 来描述工作任务。把 work 添加到一个链表 worklist 中,然后由一个内核线程 worker 遍历链表,串行地执行挂入 worklist 中的所有 work。如果 worklist 中没有 work,那么内核线程 worker 就会变成 IDLE 状态;如果有 work,则执行 work 中的回调函数。
关于 workqueue 中几个概念都是 work 相关的数据结构非常容易混淆,大概可以这样来理解:
工作。初始化一个 work 并添加到工作队列后,将会将其传递到合适的内核线程来进行处理,它是用于调度的最小单位。
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
工作的集合。workqueue 和 work 是一对多的关系。内核中工作队列分为两种:
struct workqueue_struct {
struct list_head pwqs; /* WR: all pwqs of this wq */
struct list_head list; /* PR: list of all workqueues */
struct list_head maydays; /* MD: pwqs requesting rescue */
struct worker *rescuer; /* I: rescue worker */
struct pool_workqueue *dfl_pwq; /* PW: 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 */
struct pool_workqueue __rcu *numa_pwq_tbl[]; /* PWR: unbound pwqs indexed by node */ //Per-Node创建pool_workqueue
...
};
中间人 / 中介,负责建立起 workqueue 和 worker_pool 之间的关系。workqueue 和 pool_workqueue 是一对多的关系。
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 list_head pwqs_node; /* WR: node on wq->pwqs */
struct list_head mayday_node; /* MD: node on wq->maydays */ //用于添加到workqueue链表中
...
} __aligned(1 << WORK_STRUCT_FLAG_BITS);
工人的集合。pool_workqueue 和 worker_pool 是一对一的关系,worker_pool 和 worker 是一对多的关系。
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 */
unsigned long watchdog_ts; /* L: watchdog timestamp */
struct list_head worklist; /* L: list of pending works */
int nr_workers; /* L: total number of workers */
/* nr_idle includes the ones off idle_list for rebinding */
int nr_idle; /* L: currently idle ones */
struct list_head idle_list; /* X: list of idle workers */
struct timer_list idle_timer; /* L: worker idle timeout */
struct timer_list mayday_timer; /* L: SOS timer for workers */
/* a workers is either on busy_hash or idle_list, or the manager */
DECLARE_HASHTABLE(busy_hash, BUSY_WORKER_HASH_ORDER); /* L: hash of busy workers */
/* see manage_workers() for details on the two manager mutexes */
struct worker *manager; /* L: purely informational */
struct mutex attach_mutex; /* attach/detach exclusion */
struct list_head workers; /* A: attached workers */
struct completion *detach_completion; /* all workers detached */
struct ida worker_ida; /* worker IDs for task name */
struct workqueue_attrs *attrs; /* I: worker attributes */
struct hlist_node hash_node; /* PL: unbound_pool_hash node */
...
} ____cacheline_aligned_in_smp;
工人。在代码中 worker 对应一个 work_thread() 内核线程。
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 */
/* 64 bytes boundary on 64bit, 32 on 32bit */
struct task_struct *task; /* I: worker task */
struct worker_pool *pool; /* I: the associated pool */
/* L: for rescuers */
struct list_head node; /* A: anchored at pool->workers */ //添加到worker_pool->workers链表中
/* A: runs through worker->node */
...
};
可以用下图来总结:
内核在启动的时候会对 workqueue 做初始化,workqueue 的初始化包含两个阶段,分别是 workqueue_init_early 和 workqueue_init。
这里主要完成的工作是给之前创建好的 worker_pool,添加一个初始的 worker,然后利用函数 create_worker,创建名字为 kworker/XX:YY 或者 kworker/uXX:YY 的内核线程。其中 XX 表示 worker_pool 的编号,YY 表示 worker 的编号,u 表示unbound。
经过上面两个阶段的初始化,workqueue 子系统基本就已经将数据结构的关联建立好了,当有 work 来进行调度的时候,就可以进行处理了。
内核推荐驱动开发者使用默认的 workqueue,而不是新建 workqueue。要使用系统默认的 workqueue,首先需要初始化 work,内核提供了相应的宏 INIT_WORK。
#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)
初始化 work 后,就可以调用 shedule_work 函数把 work 挂入系统默认的 workqueue 中。
worker 内核线程的执行函数是 worker_thread。