首先感谢原文作者 LoyenWang 的分享,可以点击章节阅读原作者原文,或者查看本文的转载地址,再次感谢原作者分享,已经在公众号上征得作者同意。
说明:
- Kernel版本:4.14
- ARM64处理器,Contex-A53,双核
- 使用工具:Source Insight 3.5, Visio
名词
GIC:
中断控制器Generic Interrupt Controller
- SoC:系统级芯片 System on Chip
SGI
:软件产生的中断(software-generated interrupts)
;PPI
:私有外设中断(Private Peripheral Interrupt)
;SPI
:共享外设中断(Shared Peripheral Interrupt)
;
目录
Linux中断子系统(一)-中断控制器及驱动分析
1. 概述
从这篇文章开始,来聊一聊中断子系统。中断是处理器用于异步处理外围设备请求的一种机制,可以说中断处理是操作系统管理外围设备的基石,此外系统调度、核间交互等都离不开中断,它的重要性不言而喻。
来一张概要的分层图:
- 硬件层:最下层为硬件连接层,对应的是具体的外设与SoC的物理连接,中断信号是从外设到中断控制器,由中断控制器统一管理,再路由到处理器上;
- 硬件相关层:这个层包括两部分代码,一部分是架构相关的,比如ARM64处理器处理中断相关,另一部分是中断控制器的驱动代码;
- 通用层:这部分也可以认为是框架层,是硬件无关层,这部分代码在所有硬件平台上是通用的;
- 用户层:这部分也就是中断的使用者了,主要是各类设备驱动,通过中断相关接口来进行申请和注册,最终在外设触发中断时,进行相应的回调处理;
中断子系统系列文章,会包括硬件相关、中断框架层、上半部与下半部、Softirq、Workqueue等机制的介绍,本文会先介绍硬件相关的原理及驱动,前戏结束,直奔主题。
2. GIC硬件原理
-
ARM公司提供了一个通用的中断控制器
GIC(Generic Interrupt Controller)
,GIC
的版本包括V1 ~ V4
,由于本人使用的SoC(系统级芯片 System on Chip)中的中断控制器是V2
版本,本文将围绕GIC-V2
来展开介绍;
来一张功能版的框图:
GIC-V2
从功能上说,除了常用的中断使能、中断屏蔽、优先级管理等功能外,还支持安全扩展、虚拟化等;GIC-V2
从组成上说,主要分为Distributor
和CPU Interface
两个模块,Distributor
主要负责中断源的管理,包括优先级的处理,屏蔽、抢占等,并将最高优先级的中断分发给CPU Interface
,CPU Interface
主要用于连接处理器,与处理器进行交互;
Virtual Distributor
和Virtual CPU Interface
都与虚拟化相关,本文不深入分析;
再来一张细节图看看Distributor
和CPU Interface
的功能:
GIC-V2
支持三种类型的中断:
SGI(software-generated interrupts)
:软件产生的中断,主要用于核间交互,内核中的IPI:inter-processor interrupts
就是基于SGI
,中断号ID0 - ID15
用于SGI
;PPI(Private Peripheral Interrupt)
:私有外设中断,每个CPU都有自己的私有中断,典型的应用有local timer
,中断号ID16 - ID31
用于PPI
;SPI(Shared Peripheral Interrupt)
:共享外设中断,中断产生后,可以分发到某一个CPU上,中断号ID32 - ID1019
用于SPI
,ID1020 - ID1023
保留用于特殊用途;
Distributor
功能:
- 全局开关控制
Distributor
分发到CPU Interface
; - 打开或关闭每个中断;
- 设置每个中断的优先级;
- 设置每个中断将路由的CPU列表;
- 设置每个外设中断的触发方式:电平触发、边缘触发;
- 设置每个中断的Group:Group0或Group1,其中Group0用于安全中断,支持FIQ和IRQ,Group1用于非安全中断,只支持IRQ;
- 将
SGI
中断分发到目标CPU上; - 每个中断的状态可见;
- 提供软件机制来设置和清除外设中断的pending状态;
CPU Interface
功能:
- 使能中断请求信号到CPU上;
- 中断的确认;
- 标识中断处理的完成;
- 为处理器设置中断优先级掩码;
- 设置处理器的中断抢占策略;
- 确定处理器的最高优先级pending中断;
中断处理的状态机如下图:
Inactive
:无中断状态;Pending
:硬件或软件触发了中断,但尚未传递到目标CPU,在电平触发模式下,产生中断的同时保持pending
状态;Active
:发生了中断并将其传递给目标CPU,并且目标CPU可以处理该中断;Active and pending
:发生了中断并将其传递给目标CPU,同时发生了相同的中断并且该中断正在等待处理;
GIC检测中断流程如下:
- GIC捕获中断信号,中断信号assert,标记为pending状态;
Distributor
确定好目标CPU后,将中断信号发送到目标CPU上,同时,对于每个CPU,Distributor
会从pending信号中选择最高优先级中断发送至CPU Interface
;CPU Interface
来决定是否将中断信号发送至目标CPU;- CPU完成中断处理后,发送一个完成信号
EOI(End of Interrupt)
给GIC;
3. GIC驱动分析
3.1 设备信息添加
ARM平台的设备信息,都是通过Device Tree
设备树来添加,设备树信息放置在arch/arm64/boot/dts/
下
下图就是一个中断控制器的设备树信息:
compatible
字段:用于与具体的驱动来进行匹配,比如图片中arm, gic-400
,可以根据这个名字去匹配对应的驱动程序;interrupt-cells
字段:用于指定编码一个中断源所需要的单元个数,这个值为3。比如在外设在设备树中添加中断信号时,通常能看到类似interrupts = <0 23 4>;
的信息,第一个单元0,表示的是中断类型(1:PPI,0:SPI
),第二个单元23表示的是中断号,第三个单元4表示的是中断触发的类型;reg
字段:描述中断控制器的地址信息以及地址范围,比如图片中分别制定了GIC Distributor(GICD)
和GIC CPU Interface(GICC)
的地址信息;interrupt-controller
字段:表示该设备是一个中断控制器,外设可以连接在该中断控制器上;- 关于设备数的各个字段含义,详细可以参考
Documentation/devicetree/bindings
下的对应信息;
设备树的信息,是怎么添加到系统中的呢?Device Tree
最终会编译成dtb
文件,并通过Uboot传递给内核,在内核启动后会将dtb
文件解析成device_node
结构。关于设备树的相关知识,本文先不展开,后续再找机会补充。来一张图,先简要介绍下关键路径:
- 设备树的节点信息,最终会变成
device_node
结构,在内存中维持一个树状结构; - 设备与驱动,会根据
compatible
字段进行匹配;
3.2 驱动流程分析
GIC驱动的执行流程如下图所示:
- 首先需要了解一下链接脚本
vmlinux.lds
,脚本中定义了一个__irqchip_of_table
段,该段用于存放中断控制器信息,用于最终来匹配设备; - 在GIC驱动程序中,使用
IRQCHIP_DECLARE
宏来声明结构信息,包括compatible
字段和回调函数,该宏会将这个结构放置到__irqchip_of_table
字段中; - 在内核启动初始化中断的函数中,
of_irq_init
函数会去查找设备节点信息,该函数的传入参数就是__irqchip_of_table
段,由于IRQCHIP_DECLARE
已经将信息填充好了,of_irq_init
函数会根据arm,gic-400
去查找对应的设备节点,并获取设备的信息。中断控制器也存在级联的情况,of_irq_init
函数中也处理了这种情况; or_irq_init
函数中,最终会回调IRQCHIP_DECLARE
声明的回调函数,也就是gic_of_init
,而这个函数就是GIC驱动的初始化入口函数了;- GIC的工作,本质上是由中断信号来驱动,因此驱动本身的工作就是完成各类信息的初始化,注册好相应的回调函数,以便能在信号到来之时去执行;
set_smp_process_call
设置__smp_cross_call
函数指向gic_raise_softirq
,本质上就是通过软件来触发GIC的SGI中断
,用于核间交互;cpuhp_setup_state_nocalls
函数,设置好CPU进行热插拔时GIC的回调函数,以便在CPU热插拔时做相应处理;set_handle_irq
函数的设置很关键,它将全局函数指针handle_arch_irq
指向了gic_handle_irq
,而处理器在进入中断异常时,会跳转到handle_arch_irq
执行,所以,可以认为它就是中断处理的入口函数了;- 驱动中完成了各类函数的注册,此外还完成了
irq_chip
,irq_domain
等结构体的初始化,这些结构在下文会进一步分析; - 最后,完成GIC硬件模块的初始化设置,以及电源管理相关的注册等工作;
3.3 数据结构分析
先来张图:
- GIC驱动中,使用
struct gic_chip_data
结构体来描述GIC控制器的信息,整个驱动都是围绕着该结构体的初始化,驱动中将函数指针都初始化好,实际的工作是由中断信号触发,也就是在中断来临的时候去进行回调; struct irq_chip
结构,描述的是中断控制器的底层操作函数集,这些函数集最终完成对控制器硬件的操作;struct irq_domain
结构,用于硬件中断号和Linux IRQ中断号(virq,虚拟中断号)之间的映射;
还是上一下具体的数据结构代码吧,关键注释如下:
struct irq_chip {
struct device *parent_device; //指向父设备
const char *name; // /proc/interrupts中显示的名字
unsigned int (*irq_startup)(struct irq_data *data); //启动中断,如果设置成NULL,则默认为enable
void (*irq_shutdown)(struct irq_data *data); //关闭中断,如果设置成NULL,则默认为disable
void (*irq_enable)(struct irq_data *data); //中断使能,如果设置成NULL,则默认为chip->unmask
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); //在SMP中设置CPU亲和力
int (*irq_retrigger)(struct irq_data *data); //重新发送中断到CPU
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); //同步释放慢速总线芯片的锁
void (*irq_cpu_online)(struct irq_data *data);
void (*irq_cpu_offline)(struct irq_data *data);
void (*irq_suspend)(struct irq_data *data);
void (*irq_resume)(struct irq_data *data);
void (*irq_pm_shutdown)(struct irq_data *data);
void (*irq_calc_mask)(struct irq_data *data);
void (*irq_print_chip)(struct irq_data *data, struct seq_file *p);
int (*irq_request_resources)(struct irq_data *data);
void (*irq_release_resources)(struct irq_data *data);
void (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg);
void (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg);
int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state);
int (*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state);
int (*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info);
void (*ipi_send_single)(struct irq_data *data, unsigned int cpu);
void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);
unsigned long flags;
};
struct irq_domain {
struct list_head link; //用于添加到全局链表irq_domain_list中
const char *name; //IRQ domain的名字
const struct irq_domain_ops *ops; //IRQ domain映射操作函数集
void *host_data; //在GIC驱动中,指向了irq_gic_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; //IRQ domain支持中断数量的最大值
unsigned int revmap_direct_max_irq;
unsigned int revmap_size; //线性映射的大小
struct radix_tree_root revmap_tree; //Radix Tree映射的根节点
unsigned int linear_revmap[]; //线性映射用到的查找表
};
struct irq_domain_ops {
int (*match)(struct irq_domain *d, struct device_node *node,
enum irq_domain_bus_token bus_token); // 用于中断控制器设备与IRQ domain的匹配
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); //用于硬件中断号与Linux中断号的映射
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); //通过device_node,解析硬件中断号和触发方式
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
/* extended V2 interfaces to support hierarchy irq_domains */
int (*alloc)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs, void *arg);
void (*free)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs);
void (*activate)(struct irq_domain *d, struct irq_data *irq_data);
void (*deactivate)(struct irq_domain *d, struct irq_data *irq_data);
int (*translate)(struct irq_domain *d, struct irq_fwspec *fwspec,
unsigned long *out_hwirq, unsigned int *out_type);
#endif
};
3.3.1 IRQ domain
IRQ domain用于将硬件的中断号,转换成Linux系统中的中断号(virtual irq, virq
),来张图:
- 每个中断控制器都对应一个IRQ Domain;
- 中断控制器驱动通过
irq_domain_add_*()
接口来创建IRQ Domain; - IRQ Domain支持三种映射方式:linear map(线性映射),tree map(树映射),no map(不映射);
- linear map:维护固定大小的表,索引是硬件中断号,如果硬件中断最大数量固定,并且数值不大,可以选择线性映射;
- tree map:硬件中断号可能很大,可以选择树映射;
- no map:硬件中断号直接就是Linux的中断号;
三种映射的方式如下图:
-
图中描述了三个中断控制器,对应到三种不同的映射方式;
-
各个控制器的硬件中断号可以一样,最终在Linux内核中映射的中断号是唯一的;
4. Arch-speicific代码分析
- 中断也是异常模式的一种,当外设触发中断时,处理器会切换到特定的异常模式进行处理,而这部分代码都是架构相关的;ARM64的代码位于
arch/arm64/kernel/entry.S
。 - ARM64处理器有四个异常级别Exception Level:0~3,EL0级对应用户态程序,EL1级对应操作系统内核态,EL2级对应Hypervisor,EL3级对应Secure Monitor;
- 异常触发时,处理器进行切换,并且跳转到异常向量表开始执行,针对中断异常,最终会跳转到
irq_handler
中;
代码比较简单,如下:
/*
* Interrupt handling.
*/
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1
irq_stack_exit
.endm
来张图:
- 中断触发,处理器去异常向量表找到对应的入口,比如EL0的中断跳转到
el0_irq
处,EL1则跳转到el1_irq
处; - 在GIC驱动中,会调用
set_handle_irq
接口来设置handle_arch_irq
的函数指针,让它指向gic_handle_irq
,因此中断触发的时候会跳转到gic_handle_irq
处执行; gic_handle_irq
函数处理时,分为两种情况,一种是外设触发的中断,硬件中断号在16 ~ 1020
之间,一种是软件触发的中断,用于处理器之间的交互,硬件中断号在16以内;- 外设触发中断后,根据
irq domain
去查找对应的Linux IRQ中断号,进而得到中断描述符irq_desc
,最终也就能调用到外设的中断处理函数了;
GIC和Arch相关的介绍就此打住,下一篇文章会接着介绍通用的中断处理框架,敬请期待。
参考
ARM Generic Interrupt Controller Architecture version 2.0
Linux中断子系统(二)-通用框架处理
1. 概述
《Linux中断子系统(一)-中断控制器及驱动分析》
讲到了底层硬件GIC驱动,以及Arch-Specific的中断代码,本文将研究下通用的中断处理的过程,属于硬件无关层。当然,我还是建议你看一下上篇文章。
这篇文章会解答两个问题:
- 用户是怎么使用中断的(
中断注册
)? - 外设触发中断信号时,最终是怎么调用到中断handler的(
中断处理
)?
2. 数据结构分析
先来看一下总的数据结构,核心是围绕着struct irq_desc
来展开:
-
Linux内核的中断处理,围绕着中断描述符结构
struct irq_desc
展开,内核提供了两种中断描述符组织形式:-
打开
CONFIG_SPARSE_IRQ
宏(中断编号不连续),中断描述符以radix-tree
来组织,用户在初始化时进行动态分配,然后再插入radix-tree
中; -
关闭
CONFIG_SPARSE_IRQ
宏(中断编号连续),中断描述符以数组的形式组织,并且已经分配好; -
不管哪种形式,都可以通过
linux irq
号来找到对应的中断描述符;
-
- 图的左侧灰色部分,主要在中断控制器驱动中进行初始化设置,包括各个结构中函数指针的指向等,其中
struct irq_chip
用于对中断控制器的硬件操作,struct irq_domain
与中断控制器对应,完成的工作是硬件中断号到Linux irq
的映射; - 图的上侧灰色部分,中断描述符的创建(这里指
CONFIG_SPARSE_IRQ
),主要在获取设备中断信息的过程中完成的,从而让设备树中的中断能与具体的中断描述符irq_desc
匹配; - 图中剩余部分,在设备申请注册中断的过程中进行设置,比如
struct irqaction
中handler
的设置,这个用于指向我们设备驱动程序中的中断处理函数了;
中断的处理主要有以下几个功能模块:
- 硬件中断号到
Linux irq
中断号的映射,并创建好irq_desc
中断描述符; - 中断注册时,先获取设备的中断号,根据中断号找到对应的
irq_desc
,并将设备的中断处理函数添加到irq_desc
中; - 设备触发中断信号时,根据硬件中断号得到
Linux irq
中断号,找到对应的irq_desc
,最终调用到设备的中断处理函数;
上述的描述比较简单,更详细的过程,往下看吧。
3. 流程分析
3.1 中断注册
这一次,让我们以问题的方式来展开:先来让我们回答第一个问题:用户是怎么使用中断的?
-
熟悉设备驱动的同学应该都清楚,经常会在驱动程序中调用
request_irq()
接口或者request_threaded_irq()
接口来注册设备的中断处理函数; -
request_irq()/request_threaded_irq
接口中,都需要用到irq
,也就是中断号,那么这个中断号是从哪里来的呢?它是Linux irq
,它又是如何映射到具体的硬件设备的中断号的呢?
先来看第二个问题:设备硬件中断号到
Linux irq
中断号的映射
- 硬件设备的中断信息都在设备树
device tree
中进行了描述,在系统启动过程中,这些信息都已经加载到内存中并得到了解析; - 驱动中通常会使用
platform_get_irq
或irq_of_parse_and_map
接口,去根据设备树的信息去创建映射关系(硬件中断号到linux irq
中断号映射); - 《Linux中断子系统(一)-中断控制器及驱动分析》提到过
struct irq_domain
用于完成映射工作,因此在irq_create_fwspec_mapping
接口中,会先去找到匹配的irq domain
,再去回调该irq domain
中的函数集,通常irq domain
都是在中断控制器驱动中初始化的,以ARM GICv2
为例,最终回调到gic_irq_domain_hierarchy_ops
中的函数; - 如果已经创建好了映射,那么可以直接进行返回
linux irq
中断号了,否则的话需要irq_domain_alloc_irqs
来创建映射关系; irq_domain_alloc_irqs
完成两个工作:- 针对
linux irq
中断号创建一个irq_desc
中断描述符; - 调用
domain->ops->alloc
函数来完成映射,在ARM GICv2
驱动中对应gic_irq_domain_alloc
函数,这个函数很关键,所以下文介绍一下;
- 针对
gic_irq_domain_alloc
函数如下:
gic_irq_domain_translate
:负责解析出设备树中描述的中断号和中断触发类型(边缘触发、电平触发等);gic_irq_domain_map
:将硬件中断号和linux中断号绑定到一个结构中,也就完成了映射,此外还绑定了irq_desc
结构中的其他字段,最重要的是设置了irq_desc->handle_irq
的函数指针,这个最终是中断响应时往上执行的入口,这个是关键,下文讲述中断处理过程时还会提到;- 根据硬件中断号的范围设置
irq_desc->handle_irq
的指针,共享中断入口为handle_fasteoi_irq
,私有中断入口为handle_percpu_devid_irq
;
上述函数执行完成后,完成了两大工作:
- 硬件中断号与Linux中断号完成映射,并为Linux中断号创建了
irq_desc
中断描述符; - 数据结构的绑定及初始化,关键的地方是设置了中断处理往上执行的入口;
再看第一个问题:中断是怎么来注册的?
设备驱动中,获取到了irq
中断号后,通常就会采用request_irq/request_threaded_irq
来注册中断,其中request_irq
用于注册普通处理的中断,request_threaded_irq
用于注册线程化处理的中断;
在讲具体的注册流程前,先看一下主要的中断标志位:
#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的用户共享中断时,执行本设备的中断处理函数
request_irq
也是调用request_threaded_irq
,只是在传参的时候,线程处理函数thread_fn
函数设置成NULL;- 由于在硬件中断号和Linux中断号完成映射后,
irq_desc
已经创建好,可以通过irq_to_desc
接口去获取对应的irq_desc
; - 创建
irqaction
,并初始化该结构体中的各个字段,其中包括传入的中断处理函数赋值给对应的字段; __setup_irq
用于完成中断的相关设置,包括中断线程化的处理:- 中断线程化用于减少系统关中断的时间,增强系统的实时性;
- ARM64默认开启了
CONFIG_IRQ_FORCED_THREADING
,引导参数传入threadirqs
时,则除了IRQF_NO_THREAD
外的中断,其他的都将强制线程化处理; - 中断线程化会为每个中断都创建一个内核线程,如果中断进行共享,对应
irqaction
将连接成链表,每个irqaction
都有thread_mask
位图字段,当所有共享中断都处理完成后才能unmask
中断,解除中断屏蔽;
3.2 中断处理
当完成中断的注册后,所有结构的组织关系都已经建立好,剩下的工作就是当信号来临时,进行中断的处理工作。
来回顾一下《Linux中断子系统(一)-中断控制器及驱动分析》
中的Arch-specific
处理流程:
-
中断收到之后,首先会跳转到异常向量表的入口处,进而逐级进行回调处理,最终调用到
generic_handle_irq
来进行中断处理。
generic_handle_irq
处理如下图:
generic_handle_irq
函数最终会调用到desc->handle_irq()
,这个也就是对应到上文中在建立映射关系的过程中,调用irq_domain_set_info
函数,设置好了函数指针,也就是handle_fasteoi_irq
和handle_percpu_devid_irq
;handle_fasteoi_irq
:处理共享中断,并且遍历irqaction
链表,逐个调用action->handler()
函数,这个函数正是设备驱动程序调用request_irq/request_threaded_irq
接口注册的中断处理函数,此外如果中断线程化处理的话,还会调用__irq_wake_thread()
唤醒内核线程;handle_percpu_devid_irq
:处理per-CPU中断处理,在这个过程中会分别调用中断控制器的处理函数进行硬件操作,该函数调用action->handler()
来进行中断处理;
来看看中断线程化处理后的唤醒流程吧__handle_irq_event_percpu->__irq_wake_thread
:
__handle_irq_event_percpu->__irq_wake_thread
将唤醒irq_thread
中断内核线程;irq_thread
内核线程,将根据是否为强制中断线程化对函数指针handler_fn
进行初始化,以便后续进行调用;irq_thread
内核线程将while(!irq_wait_for_interrupt)
循环进行中断的处理,当满足条件时,执行handler_fn
,在该函数中最终调用action->thread_fn
,也就是完成了中断的处理;irq_wait_for_interrupt
函数,将会判断中断线程的唤醒条件,如果满足了,则将当前任务设置成TASK_RUNNING
状态,并返回0,这样就能执行中断的处理,否则就调用schedule()
进行调度,让出CPU,并将任务设置成TASK_INTERRUPTIBLE
可中断睡眠状态;
3.3 总结
中断的处理,总体来说可以分为两部分来看:
- 从上到下:围绕
irq_desc
中断描述符建立好连接关系,这个过程就包括:中断源信息的解析(设备树),硬件中断号到Linux中断号的映射关系、irq_desc
结构的分配及初始化(内部各个结构的组织关系)、中断的注册(填充irq_desc
结构,包括handler处理函数)等,总而言之,就是完成静态关系创建,为中断处理做好准备; - 从下到上,当外设触发中断信号时,中断控制器接收到信号并发送到处理器,此时处理器进行异常模式切换,并逐步从处理器架构相关代码逐级回调。如果涉及到中断线程化,则还需要进行中断内核线程的唤醒操作,最终完成中断处理函数的执行。
Linux中断子系统(三)-softirq和tasklet
1. 概述
中断子系统中有一个重要的设计机制,那就是Top-half和Bottom-half
,将紧急的工作放置在Top-half
中来处理,而将耗时的工作放置在Bottom-half
中来处理,这样确保Top-half
能尽快完成处理,那么为什么需要这么设计呢?看一张图就明白了:
- ARM处理器在进行中断处理时,处理器进行异常模式切换,此时会将中断进行关闭,处理完成后再将中断打开;
- 如果中断不分上下半部处理,那么意味着只有等上一个中断完成处理后才会打开中断,下一个中断才能得到响应。当某个中断处理处理时间较长时,很有可能就会造成其他中断丢失而无法响应,这个显然是难以接受的,比如典型的时钟中断,作为系统的脉搏,它的响应就需要得到保障;
- 中断分成上下半部处理可以提高中断的响应能力,在上半部处理完成后便将中断打开(通常上半部处理越快越好),这样就可以响应其他中断了,等到中断退出的时候再进行下半部的处理;
- 中断的
Bottom-half
机制,包括了softirq
、tasklet
、workqueue
、以及前文中提到过的中断线程化处理等,其中tasklet
又是基于softirq
来实现的,这也是本文讨论的主题;
在中断处理过程中,离不开各种上下文的讨论,了解不同上下文的区分有助于中断处理的理解,所以,还是来一张老图吧:
task_struct
结构体中的thread_info.preempt_count
用于记录当前任务所处的context
状态;PREEMPT_BITS
:用于记录禁止抢占的次数,禁止抢占一次该值就加1,使能抢占该值就减1;SOFTIRQ_BITS
:用于同步处理,关掉下半部的时候加1,打开下半部的时候减1;HARDIRQ_BITS
:用于表示处于硬件中断上下文中;
前戏结束了,直奔主题吧。
2. softirq - bottom half的一种实现
2.1 初始化
softirq
不支持动态分配,Linux kernel提供了静态分配,关键的结构体描述如下,可以类比硬件中断来理解:
/* 支持的软中断类型,可以认为是软中断号, 其中从上到下优先级递减 */
enum
{
HI_SOFTIRQ=0, /* 最高优先级软中断 */
TIMER_SOFTIRQ, /* Timer定时器软中断 */
NET_TX_SOFTIRQ, /* 发送网络数据包软中断 */
NET_RX_SOFTIRQ, /* 接收网络数据包软中断 */
BLOCK_SOFTIRQ, /* 块设备软中断 */
IRQ_POLL_SOFTIRQ, /* 块设备软中断 */
TASKLET_SOFTIRQ, /* tasklet软中断 */
SCHED_SOFTIRQ, /* 进程调度及负载均衡的软中断 */
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on thenumbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq, RCU相关的软中断 */
NR_SOFTIRQS
};
/* 软件中断描述符,只包含一个handler函数指针 */
struct softirq_action {
void (*action)(struct softirq_action *);
};
/* 软中断描述符表,实际上就是一个全局的数组 */
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
/* CPU软中断状态描述,当某个软中断触发时,__softirq_pending会置位对应的bit */
typedef struct {
unsigned int __softirq_pending;
unsigned int ipi_irqs[NR_IPI];
} ____cacheline_aligned irq_cpustat_t;
/* 每个CPU都会维护一个状态信息结构 */
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
/* 内核为每个CPU都创建了一个软中断处理内核线程 */
DEFINE_PER_CPU(struct task_struct *, ksoftirqd);
来一张图吧:
softirq_vec[]
数组,类比硬件中断描述符表irq_desc[]
,通过软中断号可以找到对应的handler
进行处理,比如图中的tasklet_action
就是一个实际的handler
函数;- 软中断可以在不同的CPU上并行运行,在同一个CPU上只能串行执行;
- 每个CPU维护
irq_cpustat_t
状态结构,当某个软中断需要进行处理时,会将该结构体中的__softirq_pending
字段或上1UL << XXX_SOFTIRQ
;
2.2 流程分析
2.2.1 软中断注册
中断处理流程中设备驱动通过request_irq/request_threaded_irq
接口来注册中断处理函数,而在软中断处理流程中,通过open_softirq
接口来注册,由于它实在是太简单了,我忍不住想把代码贴上来:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
也就是将软中断描述符表中对应描述符的handler
函数指针指向对应的函数即可,以便软中断到来时进行回调。
那么,问题来了,什么时候进行软中断函数回调呢?
2.2.2 软中断执行之一:中断处理后
先看第一种情况,用图片来回答问题:
《Linux中断子系统(二)-通用框架处理》
文章中讲述了整个中断处理流程,在接收到中断信号后,处理器进行异常模式切换,并跳转到异常向量表处进行执行,关键的流程为:el0_irq->irq_handler->handle_arch_irq(gic->handle_irq)->handle_domain_irq->__handle_domain_irq
;- 在
__handle_domain_irq
函数中,irq_enter
和irq_exit
分别用于来标识进入和离开硬件中断上下文处理,这个从preempt_count_add/preempt_count_sub
来操作HARDIRQ_OFFSET
可以看出来,这也对应到了上文中的Context描述图; - 在离开硬件中断上下文后,如果
!in_interrupt() && local_softirq_pending
为真,则进行软中断处理。这个条件有两个含义:1)!in_interrupt()
表明不能处在中断上下文中,这个范围包括in_nmi
、in_irq
、in_softirq(Bottom-half disable)
、in_serving_softirq
,凡是处于这几种状态下,软中断都不会被执行;2)local_softirq_pending
不为0,表明有软中断处理请求;
软中断执行的入口就是invoke_softirq
,继续分析一波:
invoke_softirq
函数中,根据中断处理是否线程化进行分类处理,如果中断已经进行了强制线程化处理(中断强制线程化,需要在启动的时候传入参数threadirqs
),那么直接通过wakeup_softirqd
唤醒内核线程来执行,否则的话则调用__do_softirq
函数来处理;- Linux内核会为每个CPU都创建一个内核线程
ksoftirqd
,通过smpboot_register_percpu_thread
函数来完成,其中当内核线程运行时,在满足条件的情况下会执行run_ksoftirqd
函数,如果此时有软中断处理请求,调用__do_softirq
来进行处理;
上图中的逻辑可以看出,最终的核心处理都放置在__do_softirq
函数中完成:
local_softirq_pending
函数用于读取__softirq_pending
字段,可以类比于设备驱动中的状态寄存器,用于判断是否有软中断处理请求;- 软中断处理时会关闭
Bottom-half
,处理完后再打开; 软中断处理时,会打开本地中断,处理完后关闭本地中断
,这个地方对应到上文中提到的Top-half
和Bottom-half
机制,在Bottom-half
处理的时候,是会将中断打开的,因此也就能继续响应其他中断,这个也就意味着其他中断也能来打断当前的Bottom-half
处理;while(softirq_bit = ffs(pending))
,循环读取状态位,直到处理完每一个软中断请求;- 跳出
while
循环之后,再一次判断是否又有新的软中断请求到来(由于它可能被中断打断,也就意味着可能有新的请求到来),有新的请求到来,则有三个条件判断,满足的话跳转到restart
处执行,否则调用wakeup_sotfirqd
来唤醒内核线程来处理:time_before(jiffies, MAX_SOFTIRQ_TIME)
,软中断处理时间小于两毫秒;!need_resched
,当前没有进程调度的请求;max_restart = MAX_SOFTIRQ_RESTART
,跳转到restart
循环的次数不大于10次;这三个条件的判断,是基于延迟和公平的考虑,既要保证软中断尽快处理,又不能让软中断处理一直占据系统,正所谓trade-off
的艺术;
__do_softirq
既然可以在中断处理过程中调用,也可以在ksoftirqd
中调用,那么softirq
的执行可能有两种context,插张图吧:
让我们来思考最后一个问题:硬件中断触发的时候是通过硬件设备的电信号,那么软中断的触发是通过什么呢?答案是通过raise_softirq
接口:
- 可以在中断处理过程中调用
raise_softirq
来进行软中断处理请求,处理的实际也就是上文中提到过的irq_exit
退出硬件中断上下文之后再处理; raise_softirq_irqoff
函数中,最终会调用到or_softirq_pending
,该函数会去读取本地CPU的irq_stat
中__softirq_pending
字段,然后将对应的软中断号给置位,表明有该软中断的处理请求;raise_softirq_irqoff
函数中,会判断当前的请求的上下文环境,如果不在中断上下文中,就可以通过唤醒内核线程来处理,如果在中断上下文中处理,那就不执行;- 多说一句,在软中断整个处理流程中,会经常看到
in_interrupt()
的条件判断,这个可以确保软中断在CPU上的串行执行,避免嵌套;
2.2.3 软中断执行之二:Bottom-half Enable后
第二种软中断执行的时间点,在Bottom-half
使能的时候,通常用于并发处理,进程空间上下文中进行调用:
- 在讨论并发专题的时候,我们谈到过
Bottom-half
与进程之间能产生资源争夺的情况,如果在软中断和进程之间有临界资源(软中断上下文优先级高于进程上下文),那么可以在进程上下文中调用local_bh_disable/local_bh_enable
来对临界资源保护; - 图中左侧的函数,都是用于打开
Bottom-half
的接口,可以看出是spin_lock_bh/read_lock_bh/write_lock_bh
等并发处理接口的变种形式调用; __local_bh_enable_ip
函数中,首先判断调用该本接口时中断是否是关闭的,如果已经关闭了再操作BH接口就会告警;preempt_count_sub
需要与preempt_count_add
配套使用,用于操作thread_info->preempt_count
字段,加与减的值是一致的,而在__local_bh_enable_ip
接口中,将cnt
值的减操作分成了两步:preempt_count_sub(cnt-1)
和preempt_count_dec
,这么做的原因是执行完preempt_count_sub(cnt-1)
后,thread_info->preempt_count
字段的值保留了1,把抢占给关闭了,当do_softirq
执行完毕后,再调用preempt_count_dec
再减去剩下的1,进而打开抢占;- 为什么在使能
Bottom-half
时要进行软中断处理呢?在并发处理时,可能已经把Bottom-half
进行关闭了,如果此时中断来了后,软中断不会被处理,在进程上下文中打开Bottom-half
时,这时候就会检查是否有软中断处理请求了;
3. tasklet
从上文中分析可以看出,tasklet
是软中断的一种类型,那么两者有啥区别呢?先说结论吧:
- 软中断类型内核中都是静态分配,不支持动态分配,而
tasklet
支持动态和静态分配,也就是驱动程序中能比较方便的进行扩展; - 软中断可以在多个CPU上并行运行,因此需要考虑可重入问题,而
tasklet
会绑定在某个CPU上运行,运行完后再解绑,不要求重入问题,当然它的性能也就会下降一些;
3.1 数据结构
DEFINE_PER_CPU(struct tasklet_head, tasklet_vec)
为每个CPU都分配了tasklet_head
结构,该结构用来维护struct tasklet_struct
链表,需要放到该CPU上运行的tasklet
将会添加到该结构的链表中,内核中为每个CPU维护了两个链表tasklet_vec
和tasklet_vec_hi
,对应两个不同的优先级,本文以tasklet_vec
为例;struct tasklet_struct
为tasklet
的抽象,几个关键字段如图所示,通过next
来链接成链表,通过state
字段来标识不同的状态以确保能在CPU上串行执行,func
函数指针在调用task_init()
接口时进行初始化,并在最终触发软中断时执行;
3.2 流程分析
tasklet
本质上是一种软中断,所以它的调用流程与上文中讨论的软中断流程是一致的;- 调度
tasklet
运行的接口是tasklet_schedule
,如果tasklet
没有被调度则进行调度处理,将该tasklet
添加到CPU对应的链表中,然后调用raise_softirq_irqoff
来触发软中断执行; - 软中断执行的处理函数是
tasklet_action
,这个在softirq_init
函数中通过open_softirq
函数进行注册的; tasklet_action
函数,首先将该CPU上tasklet_vec
中的链表挪到临时链表list
中,然后再对这个list
进行遍历处理,如果满足执行条件则调用t->func()
执行,并continue
跳转遍历下一个节点。如果不满足执行条件,则继续将该tasklet
添加回原来的tasklet_vec
中,并再次触发软中断;
3.3 接口
简单贴一下接口吧:
/* 静态分配tasklet */
DECLARE_TASKLET(name, func, data)
/* 动态分配tasklet */
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
/* 禁止tasklet被执行,本质上是增加tasklet_struct->count值,以便在调度时不满足执行条件 */
void tasklet_disable(struct tasklet_struct *t);
/* 使能tasklet,与tasklet_diable对应 */
void tasklet_enable(struct tasklet_struct *t);
/* 调度tasklet,通常在设备驱动的中断函数里调用 */
void tasklet_schedule(struct tasklet_struct *t);
/* 杀死tasklet,确保不被调度和执行, 主要是设置state状态位 */
void tasklet_kill(struct tasklet_struct *t);
收工!
Linux中断子系统(四)-Workqueue
1. 概述
Workqueue
工作队列是利用内核线程来异步执行工作任务的通用机制;Workqueue
工作队列可以用作中断处理的Bottom-half
机制,利用进程上下文来执行中断处理中耗时的任务,因此它允许睡眠,而Softirq
和Tasklet
在处理任务时不能睡眠;
来一张概述图:
-
在中断处理过程中,或者其他子系统中,调用
workqueue
的调度或入队接口后,通过建立好的链接关系图逐级找到合适的worker
,最终完成工作任务的执行;
2. 数据结构
2.1 总览
此处应有图:
先看看关键的数据结构:
work_struct
:工作队列调度的最小单位,work item
;workqueue_struct
:工作队列,work item
都挂入到工作队列中;worker
:work item
的处理者,每个worker
对应一个内核线程;worker_pool
:worker
池(内核线程池),是一个共享资源池,提供不同的worker
来对work item
进行处理;pool_workqueue
:充当桥梁纽带的作用,用于连接workqueue
和worker_pool
,建立链接关系;
下边看看细节吧:
2.2 work
struct work_struct
用来描述work
,初始化一个work
并添加到工作队列后,将会将其传递到合适的内核线程来进行处理,它是用于调度的最小单位。
关键字段描述如下:
struct work_struct {
atomic_long_t data; //低比特存放状态位,高比特存放worker_pool的ID或者pool_workqueue的指针
struct list_head entry; //用于添加到其他队列上
work_func_t func; //工作任务的处理函数,在内核线程中回调
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
图片说明下data
字段:
2.3 workqueue
内核中工作队列分为两种:
- bound:绑定处理器的工作队列,每个
worker
创建的内核线程绑定到特定的CPU上运行; - unbound:不绑定处理器的工作队列,创建的时候需要指定
WQ_UNBOUND
标志,内核线程可以在处理器间迁移;
内核默认创建了一些工作队列(用户也可以创建):
system_mq
:如果work item
执行时间较短,使用本队列,调用schedule[_delayed]_work[_on]()
接口就是添加到本队列中;system_highpri_mq
:高优先级工作队列,以nice值-20来运行;system_long_wq
:如果work item
执行时间较长,使用本队列;system_unbound_wq
:该工作队列的内核线程不绑定到特定的处理器上;system_freezable_wq
:该工作队列用于在Suspend时可冻结的work item
;system_power_efficient_wq
:该工作队列用于节能目的而选择牺牲性能的work item
;system_freezable_power_efficient_wq
:该工作队列用于节能或Suspend时可冻结目的的work item
;
struct workqueue_struct
关键字段介绍如下:
struct workqueue_struct {
struct list_head pwqs; /* WR: all pwqs of this wq */ //所有的pool_workqueue都添加到本链表中
struct list_head list; /* PR: list of all workqueues */ //用于将工作队列添加到全局链表workqueues中
struct list_head maydays; /* MD: pwqs requesting rescue */ //rescue状态下的pool_workqueue添加到本链表中
struct worker *rescuer; /* I: rescue worker */ //rescuer内核线程,用于处理内存紧张时创建工作线程失败的情况
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 */ //Per-CPU都创建pool_workqueue
struct pool_workqueue __rcu *numa_pwq_tbl[]; /* PWR: unbound pwqs indexed by node */ //Per-Node创建pool_workqueue
...
};
2.4 worker
- 每个
worker
对应一个内核线程,用于对work item
的处理; worker
根据工作状态,可以添加到worker_pool
的空闲链表或忙碌列表中;worker
处于空闲状态时并接收到工作处理请求,将唤醒内核线程来处理;- 内核线程是在每个
worker_pool
中由一个初始的空闲工作线程创建的,并根据需要动态创建和销毁;
关键字段描述如下:
struct worker {
/* on idle list while idle, on busy hash table while busy */
union {
struct list_head entry; /* L: while idle */ //用于添加到worker_pool的空闲链表中
struct hlist_node hentry; /* L: while busy */ //用于添加到worker_pool的忙碌列表中
};
struct work_struct *current_work; /* L: work being processed */ //当前正在处理的work
work_func_t current_func; /* L: current_work's fn */ //当前正在执行的work回调函数
struct pool_workqueue *current_pwq; /* L: current_work's pwq */ //指向当前work所属的pool_workqueue
struct list_head scheduled; /* L: scheduled works */ //所有被调度执行的work都将添加到该链表中
/* 64 bytes boundary on 64bit, 32 on 32bit */
struct task_struct *task; /* I: worker task */ //指向内核线程
struct worker_pool *pool; /* I: the associated pool */ //该worker所属的worker_pool
/* L: for rescuers */
struct list_head node; /* A: anchored at pool->workers */ //添加到worker_pool->workers链表中
/* A: runs through worker->node */
...
};
2.5 worker_pool
-
worker_pool
是一个资源池,管理多个worker
,也就是管理多个内核线程; -
针对绑定类型的工作队列,
worker_pool
是Per-CPU创建,每个CPU都有两个worker_pool
,对应不同的优先级,nice值分别为0和-20; -
针对非绑定类型的工作队列,
worker_pool
创建后会添加到unbound_pool_hash
哈希表中; -
worker_pool
管理一个空闲链表和一个忙碌列表,其中忙碌列表由哈希管理;
关键字段描述如下:
struct worker_pool {
spinlock_t lock; /* the pool lock */
int cpu; /* I: the associated cpu */ //绑定到CPU的workqueue,代表CPU ID
int node; /* I: the associated node ID */ //非绑定类型的workqueue,代表内存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 */ //pending状态的work添加到本链表
int nr_workers; /* L: total number of workers */ //worker的数量
/* 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 */ //处于IDLE状态的worker添加到本链表
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); //工作状态的worker添加到本哈希表中
/* 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 */ //worker_pool管理的worker添加到本链表中
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 */ //用于添加到unbound_pool_hash中
...
} ____cacheline_aligned_in_smp;
2.6 pool_workqueue
-
pool_workqueue
充当纽带的作用,用于将workqueue
和worker_pool
关联起来;
关键字段描述如下:
struct pool_workqueue {
struct worker_pool *pool; /* I: the associated pool */ //指向worker_pool
struct workqueue_struct *wq; /* I: the owning workqueue */ //指向所属的workqueue
int nr_active; /* L: nr of active works */ //活跃的work数量
int max_active; /* L: max active works */ //活跃的最大work数量
struct list_head delayed_works; /* L: delayed works */ //延迟执行的work挂入本链表
struct list_head pwqs_node; /* WR: node on wq->pwqs */ //用于添加到workqueue链表中
struct list_head mayday_node; /* MD: node on wq->maydays */ //用于添加到workqueue链表中
...
} __aligned(1 << WORK_STRUCT_FLAG_BITS);
2.7 小结
再来张图,首尾呼应一下:
3. 流程分析
3.1 workqueue子系统初始化
workqueue
子系统的初始化分成两步来完成的:workqueue_init_early
和workqueue_init
。
3.1.1 workqueue_init_early
workqueue
子系统早期初始化函数完成的主要工作包括:
- 创建
pool_workqueue
的SLAB缓存,用于动态分配struct pool_workqueue
结构; - 为每个CPU都分配两个
worker_pool
,其中的nice值分别为0和HIGHPRI_NICE_LEVEL
,并且为每个worker_pool
从worker_pool_idr
中分配一个ID号; - 为unbound工作队列创建默认属性,
struct workqueue_attrs
属性,主要描述内核线程的nice值,以及cpumask值,分别针对优先级以及允许在哪些CPU上执行; - 为系统默认创建几个工作队列,这几个工作队列的描述在上文的数据结构部分提及过,不再赘述;
从图中可以看出创建工作队列的接口为:alloc_workqueue
,如下图:
alloc_workqueue
完成的主要工作包括:
- 首先当然是要分配一个
struct workqueue_struct
的数据结构,并且对该结构中的字段进行初始化操作; - 前文提到过
workqueue
最终需要和worker_pool
关联起来,而这个纽带就是pool_workqueue
,alloc_and_link_pwqs
函数就是完成这个功能:1)如果工作队列是绑定到CPU上的,则为每个CPU都分配pool_workqueue
并且初始化,通过link_pwq
将工作队列与pool_workqueue
建立连接;2)如果工作队列不绑定到CPU上,则按内存节点(NUMA,参考之前内存管理的文章)来分配pool_workqueue
,调用get_unbound_pool
来实现,它会根据wq属性先去查找,如果没有找到相同的就创建一个新的pool_workqueue
,并且添加到unbound_pool_hash
哈希表中,最后也会调用link_pwq
来建立连接; - 创建工作队列时,如果设置了
WQ_MEM_RECLAIM
标志,则会新建rescuer worker
,对应rescuer_thread
内核线程。当内存紧张时,新创建worker
可能会失败,这时候由rescuer
来处理这种情况; - 最终将新建好的工作队列添加到全局链表
workqueues
中;
3.1.2 workqueue_init
workqueue
子系统第二阶段的初始化:
-
主要完成的工作是给之前创建好的
worker_pool
,添加一个初始的worker
; -
create_worker
函数中,创建的内核线程名字为kworker/XX:YY
或者kworker/uXX:YY
,其中XX
表示worker_pool
的编号,YY
表示worker
的编号,u
表示unbound
;
workqueue
子系统初始化完成后,基本就已经将数据结构的关联建立好了,当有work
来进行调度的时候,就可以进行处理了。
3.2 work调度
3.2.1 schedule_work
以schedule_work
接口为例进行分析:
schedule_work
默认是将work
添加到系统的system_work
工作队列中;queue_work_on
接口中的操作判断要添加work
的标志位,如果已经置位了WORK_STRUCT_PENDING_BIT
,表明已经添加到了队列中等待执行了,否则,需要调用__queue_work
来进行添加。注意了,这个操作是在关中断的情况下进行的,因为工作队列使用WORK_STRUCT_PENDING_BIT
位来同步work
的插入和删除操作,设置了这个比特后,然后才能执行work
,这个过程可能被中断或抢占打断;workqueue
的标志位设置了__WQ_DRAINING
,表明工作队列正在销毁,所有的work
都要处理完,此时不允许再将work
添加到队列中,有一种特殊情况:销毁过程中,执行work
时又触发了新的work
,也就是所谓的chained work
;- 判断
workqueue
的类型,如果是bound
类型,根据CPU来获取pool_workqueue
,如果是unbound
类型,通过node号来获取pool_workqueue
; get_work_pool
获取上一次执行work
的worker_pool
,如果本次执行的worker_pool
与上次执行的worker_pool
不一致,且通过find_worker_executing_work
判断work
正在某个worker_pool
中的worker
中执行,考虑到缓存热度,放到该worker
执行是更合理的选择,进而根据该worker
获取到pool_workqueue
;- 判断
pool_workqueue
活跃的work
数量,少于最大限值则将work
加入到pool->worklist
中,否则加入到pwq->delayed_works
链表中,如果__need_more_worker
判断没有worker
在执行,则唤醒worker
内核线程执行;
总结:
schedule_work
完成的工作是将work
添加到对应的链表中,而在添加的过程中,首先是需要确定pool_workqueue
;pool_workqueue
对应一个worker_pool
,因此确定了pool_workqueue
也就确定了worker_pool
,进而可以将work
添加到工作链表中;pool_workqueue
的确定分为三种情况:1)bound
类型的工作队列,直接根据CPU号获取;2)unbound
类型的工作队列,根据node号获取,针对unbound
类型工作队列,pool_workqueue
的释放是异步执行的,需要判断refcnt
的计数值,因此在获取pool_workqueue
时可能要多次retry
;3)根据缓存热度,优先选择正在被执行的worker_pool
;
3.2.2 worker_thread
work
添加到工作队列后,最终的执行在worker_thread
函数中:
- 在创建
worker
时,创建内核线程,执行函数为worker_thread
; worker_thread
在开始执行时,设置标志位PF_WQ_WORKER
,调度器在进行调度处理时会对task进行判断,针对workerqueue worker
有特殊处理;worker
对应的内核线程,在没有处理work
的时候是睡眠状态,当被唤醒的时候,跳转到woke_up
开始执行;woke_up
之后,如果此时worker
是需要销毁的,那就进行清理工作并返回。否则,离开IDLE
状态,并进入recheck
模块执行;recheck
部分,首先判断是否需要更多的worker
来处理,如果没有任务处理,跳转到sleep
地方进行睡眠。有任务需要处理时,会判断是否有空闲内核线程以及是否需要动态创建,再清除掉worker
的标志位,然后遍历工作链表,对链表中的每个节点调用process_one_worker
来处理;sleep
部分比较好理解,没有任务处理时,worker
进入空闲状态,并将当前的内核线程设置成睡眠状态,让出CPU;
总结:
- 管理
worker_pool
的内核线程池时,如果有PENDING
状态的work
,并且发现没有正在运行的工作线程(worker_pool->nr_running == 0
),唤醒空闲状态的内核线程,或者动态创建内核线程; - 如果
work
已经在同一个worker_pool
的其他worker
中执行,不再对该work
进行处理;
work
的执行函数为process_one_worker
:
-
work
可能在同一个CPU上不同的worker
中运行,直接退出; -
调用
worker->current_func()
,完成最终work
的回调函数执行;
3.3 worker动态管理
3.3.1 worker状态机变换
worker_pool
通过nr_running
字段来在不同的状态机之间进行切换;worker_pool
中有work
需要处理时,需要至少保证有一个运行状态的worker
,当nr_running
大于1时,将多余的worker
进入IDLE状态,没有work
需要处理时,所有的worker
都会进入IDLE状态;- 执行
work
时,如果回调函数阻塞运行,那么会让worker
进入睡眠状态,此时调度器会进行判断是否需要唤醒另一个worker
; - IDLE状态的
worker
都存放在idle_list
链表中,如果空闲时间超过了300秒,则会将其进行销毁;
Running->Suspend
-
当
worker
进入睡眠状态时,如果该worker_pool
没有其他的worker
处于运行状态,那么是需要唤醒一个空闲的worker
来维持并发处理的能力;
Suspend->Running
睡眠状态可以通过wake_up_worker
来进行唤醒处理,最终判断如果该worker
不在运行状态,则增加worker_pool
的nr_running
值;
3.3.2 worker的动态添加和删除
动态删除
worker_pool
初始化时,注册了timer的回调函数,用于定时对空闲链表上的worker
进行处理,如果worker
太多,且空闲时间太长,超过了5分钟,那么就直接进行销毁处理了;
动态添加
内核线程执行worker_thread
函数时,如果没有空闲的worker
,会调用manage_workers
接口来创建更多的worker
来处理工作;
参考
Documentation/core-api/workqueue.rst
http://kernel.meizu.com/linux-workqueue.html
Linux虚拟化KVM-Qemu分析(六)之中断虚拟化
见:《Linux虚拟化KVM-Qemu分析(六)之中断虚拟化》
https://rtoax.blog.csdn.net/article/details/109909292