Linux中断子系统

首先感谢原文作者 LoyenWang 的分享,可以点击章节阅读原作者原文,或者查看本文的转载地址,再次感谢原作者分享,已经在公众号上征得作者同意。

说明:

  1. Kernel版本:4.14
  2. ARM64处理器,Contex-A53,双核
  3. 使用工具:Source Insight 3.5, Visio

名词

  1. GIC:中断控制器 Generic Interrupt Controller
  2. SoC:系统级芯片 System on Chip
  3. SGI:软件产生的中断(software-generated interrupts)
  4. PPI:私有外设中断(Private Peripheral Interrupt)
  5. SPI:共享外设中断(Shared Peripheral Interrupt)

目录

Linux中断子系统(一)-中断控制器及驱动分析

Linux中断子系统(二)-通用框架处理

Linux中断子系统(三)-softirq和tasklet

Linux中断子系统(四)-Workqueue

Linux虚拟化KVM-Qemu分析(六)之中断虚拟化


 

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从组成上说,主要分为DistributorCPU Interface两个模块
    • Distributor主要负责中断源的管理,包括优先级的处理,屏蔽、抢占等,并将最高优先级的中断分发给CPU Interface
    • CPU Interface主要用于连接处理器,与处理器进行交互;
  • Virtual DistributorVirtual CPU Interface都与虚拟化相关,本文不深入分析;

再来一张细节图看看DistributorCPU Interface的功能:

GIC-V2支持三种类型的中断:

  1. SGI(software-generated interrupts):软件产生的中断,主要用于核间交互,内核中的IPI:inter-processor interrupts就是基于SGI,中断号ID0 - ID15用于SGI
  2. PPI(Private Peripheral Interrupt):私有外设中断,每个CPU都有自己的私有中断,典型的应用有local timer,中断号ID16 - ID31用于PPI
  3. SPI(Shared Peripheral Interrupt):共享外设中断,中断产生后,可以分发到某一个CPU上,中断号ID32 - ID1019用于SPIID1020 - ID1023保留用于特殊用途;

Distributor功能:

  1. 全局开关控制Distributor分发到CPU Interface
  2. 打开或关闭每个中断;
  3. 设置每个中断的优先级;
  4. 设置每个中断将路由的CPU列表;
  5. 设置每个外设中断的触发方式:电平触发、边缘触发;
  6. 设置每个中断的Group:Group0或Group1,其中Group0用于安全中断,支持FIQ和IRQ,Group1用于非安全中断,只支持IRQ;
  7. SGI中断分发到目标CPU上;
  8. 每个中断的状态可见;
  9. 提供软件机制来设置和清除外设中断的pending状态;

CPU Interface功能:

  1. 使能中断请求信号到CPU上;
  2. 中断的确认;
  3. 标识中断处理的完成;
  4. 为处理器设置中断优先级掩码;
  5. 设置处理器的中断抢占策略;
  6. 确定处理器的最高优先级pending中断;

中断处理的状态机如下图:

  • Inactive:无中断状态;
  • Pending:硬件或软件触发了中断,但尚未传递到目标CPU,在电平触发模式下,产生中断的同时保持pending状态;
  • Active:发生了中断并将其传递给目标CPU,并且目标CPU可以处理该中断;
  • Active and pending:发生了中断并将其传递给目标CPU,同时发生了相同的中断并且该中断正在等待处理;

GIC检测中断流程如下:

  1. GIC捕获中断信号,中断信号assert,标记为pending状态;
  2. Distributor确定好目标CPU后,将中断信号发送到目标CPU上,同时,对于每个CPU,Distributor会从pending信号中选择最高优先级中断发送至CPU Interface
  3. CPU Interface来决定是否将中断信号发送至目标CPU;
  4. 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_chipirq_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的中断代码,本文将研究下通用的中断处理的过程,属于硬件无关层。当然,我还是建议你看一下上篇文章。

这篇文章会解答两个问题:

  1. 用户是怎么使用中断的(中断注册)?
  2. 外设触发中断信号时,最终是怎么调用到中断handler的(中断处理)?

 

2. 数据结构分析


先来看一下总的数据结构,核心是围绕着struct irq_desc来展开:

  • Linux内核的中断处理,围绕着中断描述符结构struct irq_desc展开,内核提供了两种中断描述符组织形式:

    1. 打开CONFIG_SPARSE_IRQ宏(中断编号不连续),中断描述符以radix-tree来组织,用户在初始化时进行动态分配,然后再插入radix-tree中;

    2. 关闭CONFIG_SPARSE_IRQ宏(中断编号连续),中断描述符以数组的形式组织,并且已经分配好;

    3. 不管哪种形式,都可以通过linux irq号来找到对应的中断描述符;

  • 图的左侧灰色部分,主要在中断控制器驱动中进行初始化设置,包括各个结构中函数指针的指向等,其中struct irq_chip用于对中断控制器的硬件操作,struct irq_domain与中断控制器对应,完成的工作是硬件中断号到Linux irq的映射;
  • 图的上侧灰色部分,中断描述符的创建(这里指CONFIG_SPARSE_IRQ),主要在获取设备中断信息的过程中完成的,从而让设备树中的中断能与具体的中断描述符irq_desc匹配;
  • 图中剩余部分,在设备申请注册中断的过程中进行设置,比如struct irqactionhandler的设置,这个用于指向我们设备驱动程序中的中断处理函数了;

中断的处理主要有以下几个功能模块:

  1. 硬件中断号到Linux irq中断号的映射,并创建好irq_desc中断描述符;
  2. 中断注册时,先获取设备的中断号,根据中断号找到对应的irq_desc,并将设备的中断处理函数添加到irq_desc中;
  3. 设备触发中断信号时,根据硬件中断号得到Linux irq中断号,找到对应的irq_desc,最终调用到设备的中断处理函数;

上述的描述比较简单,更详细的过程,往下看吧。

 

3. 流程分析


3.1 中断注册


这一次,让我们以问题的方式来展开:先来让我们回答第一个问题:用户是怎么使用中断的?

  1. 熟悉设备驱动的同学应该都清楚,经常会在驱动程序中调用request_irq()接口或者request_threaded_irq()接口来注册设备的中断处理函数;

  2. request_irq()/request_threaded_irq接口中,都需要用到irq,也就是中断号,那么这个中断号是从哪里来的呢?它是Linux irq,它又是如何映射到具体的硬件设备的中断号的呢?

先来看第二个问题:设备硬件中断号到Linux irq中断号的映射

  • 硬件设备的中断信息都在设备树device tree中进行了描述,在系统启动过程中,这些信息都已经加载到内存中并得到了解析;
  • 驱动中通常会使用platform_get_irqirq_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

上述函数执行完成后,完成了两大工作:

  1. 硬件中断号与Linux中断号完成映射,并为Linux中断号创建了irq_desc中断描述符;
  2. 数据结构的绑定及初始化,关键的地方是设置了中断处理往上执行的入口;

再看第一个问题:中断是怎么来注册的?

设备驱动中,获取到了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_irqhandle_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 总结


中断的处理,总体来说可以分为两部分来看:

  1. 从上到下:围绕irq_desc中断描述符建立好连接关系,这个过程就包括:中断源信息的解析(设备树),硬件中断号到Linux中断号的映射关系、irq_desc结构的分配及初始化(内部各个结构的组织关系)、中断的注册(填充irq_desc结构,包括handler处理函数)等,总而言之,就是完成静态关系创建,为中断处理做好准备;
  2. 从下到上,当外设触发中断信号时,中断控制器接收到信号并发送到处理器,此时处理器进行异常模式切换,并逐步从处理器架构相关代码逐级回调。如果涉及到中断线程化,则还需要进行中断内核线程的唤醒操作,最终完成中断处理函数的执行。

 

Linux中断子系统(三)-softirq和tasklet


1. 概述


中断子系统中有一个重要的设计机制,那就是Top-half和Bottom-half将紧急的工作放置在Top-half中来处理,而将耗时的工作放置在Bottom-half中来处理,这样确保Top-half能尽快完成处理,那么为什么需要这么设计呢?看一张图就明白了:

  • ARM处理器在进行中断处理时,处理器进行异常模式切换,此时会将中断进行关闭,处理完成后再将中断打开;
  • 如果中断不分上下半部处理,那么意味着只有等上一个中断完成处理后才会打开中断,下一个中断才能得到响应。当某个中断处理处理时间较长时,很有可能就会造成其他中断丢失而无法响应,这个显然是难以接受的,比如典型的时钟中断,作为系统的脉搏,它的响应就需要得到保障;
  • 中断分成上下半部处理可以提高中断的响应能力,在上半部处理完成后便将中断打开(通常上半部处理越快越好),这样就可以响应其他中断了,等到中断退出的时候再进行下半部的处理;
  • 中断的Bottom-half机制,包括了softirqtaskletworkqueue、以及前文中提到过的中断线程化处理等,其中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_enterirq_exit分别用于来标识进入和离开硬件中断上下文处理,这个从preempt_count_add/preempt_count_sub来操作HARDIRQ_OFFSET可以看出来,这也对应到了上文中的Context描述图;
  • 在离开硬件中断上下文后,如果!in_interrupt() && local_softirq_pending为真,则进行软中断处理。这个条件有两个含义:1)!in_interrupt()表明不能处在中断上下文中,这个范围包括in_nmiin_irqin_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-halfBottom-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_vectasklet_vec_hi,对应两个不同的优先级,本文以tasklet_vec为例;
  • struct tasklet_structtasklet的抽象,几个关键字段如图所示,通过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机制,利用进程上下文来执行中断处理中耗时的任务,因此它允许睡眠,而SoftirqTasklet在处理任务时不能睡眠;

来一张概述图:

  • 在中断处理过程中,或者其他子系统中,调用workqueue的调度或入队接口后,通过建立好的链接关系图逐级找到合适的worker,最终完成工作任务的执行;

 

2. 数据结构


2.1 总览


此处应有图:

先看看关键的数据结构:

  1. work_struct:工作队列调度的最小单位,work item
  2. workqueue_struct:工作队列,work item都挂入到工作队列中;
  3. workerwork item的处理者,每个worker对应一个内核线程;
  4. worker_poolworker池(内核线程池),是一个共享资源池,提供不同的worker来对work item进行处理;
  5. pool_workqueue:充当桥梁纽带的作用,用于连接workqueueworker_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


内核中工作队列分为两种:

  1. bound:绑定处理器的工作队列,每个worker创建的内核线程绑定到特定的CPU上运行;
  2. unbound:不绑定处理器的工作队列,创建的时候需要指定WQ_UNBOUND标志,内核线程可以在处理器间迁移;

内核默认创建了一些工作队列(用户也可以创建):

  1. system_mq:如果work item执行时间较短,使用本队列,调用schedule[_delayed]_work[_on]()接口就是添加到本队列中;
  2. system_highpri_mq:高优先级工作队列,以nice值-20来运行;
  3. system_long_wq:如果work item执行时间较长,使用本队列;
  4. system_unbound_wq:该工作队列的内核线程不绑定到特定的处理器上;
  5. system_freezable_wq:该工作队列用于在Suspend时可冻结的work item
  6. system_power_efficient_wq:该工作队列用于节能目的而选择牺牲性能的work item
  7. 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充当纽带的作用,用于将workqueueworker_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_earlyworkqueue_init

 

3.1.1 workqueue_init_early


workqueue子系统早期初始化函数完成的主要工作包括:

  1. 创建pool_workqueue的SLAB缓存,用于动态分配struct pool_workqueue结构;
  2. 为每个CPU都分配两个worker_pool,其中的nice值分别为0和HIGHPRI_NICE_LEVEL,并且为每个worker_poolworker_pool_idr中分配一个ID号;
  3. 为unbound工作队列创建默认属性,struct workqueue_attrs属性,主要描述内核线程的nice值,以及cpumask值,分别针对优先级以及允许在哪些CPU上执行;
  4. 为系统默认创建几个工作队列,这几个工作队列的描述在上文的数据结构部分提及过,不再赘述;

从图中可以看出创建工作队列的接口为:alloc_workqueue,如下图:

alloc_workqueue完成的主要工作包括:

  1. 首先当然是要分配一个struct workqueue_struct的数据结构,并且对该结构中的字段进行初始化操作;
  2. 前文提到过workqueue最终需要和worker_pool关联起来,而这个纽带就是pool_workqueuealloc_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来建立连接;
  3. 创建工作队列时,如果设置了WQ_MEM_RECLAIM标志,则会新建rescuer worker,对应rescuer_thread内核线程。当内存紧张时,新创建worker可能会失败,这时候由rescuer来处理这种情况;
  4. 最终将新建好的工作队列添加到全局链表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获取上一次执行workworker_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内核线程执行;

总结:

  1. schedule_work完成的工作是将work添加到对应的链表中,而在添加的过程中,首先是需要确定pool_workqueue
  2. pool_workqueue对应一个worker_pool,因此确定了pool_workqueue也就确定了worker_pool,进而可以将work添加到工作链表中;
  3. 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;

总结:

  1. 管理worker_pool的内核线程池时,如果有PENDING状态的work,并且发现没有正在运行的工作线程(worker_pool->nr_running == 0),唤醒空闲状态的内核线程,或者动态创建内核线程;
  2. 如果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_poolnr_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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值