14内核活动

14内核活动(中断,tasklet,完成量,等待队列)

所有支持Linux的平台都采用了中断(interrupt)的概念,以便(因种种原因)引入周期性的中断。需要区分两种类型的中断

  1. 硬件中断(hardware interrupt):由系统自身和与之连接的外设自动产生。它们用于支持更高效地实现设备驱动程序,也用于引起处理器自身对异常或错误的关注,这些是需要与内核代码进行交互的
  2. 软中断(SoftIRQ):用于有效实现内核中的延期操作

内核经常需要一些机制,将某些活动延迟到未来的某个时间执行,或将活动置于某个队列上,在时间充裕时进行后续处理。在本章中,将仔细地考察其实现。

14.1 中断

在内核版本2.6的开发期间引入了一个用于中断和IRQ的通用框架

14.1.1 中断类型

通常,各种类型的中断可分为如下两个类别

  1. 同步中断和异常。这些由CPU自身产生,针对当前执行的程序。异常可能因种种原因触发:由于运行时发生的程序设计错误(典型的例子是除0),或由于出现了异常的情况或条件,致使处理器需要“外部”的帮助才能处理.
  2. 异步中断。这是经典的中断类型,由外部设备产生,可能发生在任意时间。不同于同步中断,异步中断并不与特定进程关联。它们可能发生在任何时间,而不牵涉系统当前执行的活动。网卡通过发出一个相关的中断来报告新分组的到达。因为数据可能在任意时刻到达系统,所以当前执行的很可能是与数据无关的某个进程或其他东西。为避免损害该进程,内核必须确保中断能够尽快处理完毕(通过缓冲数据),使得CPU时间能够返还给当前进程。这也是内核需要延期操作机制的原因

如果CPU当前不处于核心态,则发起从用户态到核心态的切换。接
下来,在内核中执行一个专门的例程,称为中断服务例程(interrupt service routine,简称ISR)或中断处理程序(interrupt handler)。处理异常

许多中断可以禁用,但有些不行。后一类就包括了因硬件故障或其他系统关键事件而发出的中断

在可能的情况下,内核试图避免禁用中断,因为这显然会损害系统性能。但有些场合禁用中断是必要的,在处理第一个中断时,如果发生第二个中断,内核中可能发生严重的问题。如果内核在处理关键代码时发生了中断。在最坏情况下,这可能引起内核死锁,致使整个系统变得不可用

如果内核容许在禁用中断的情况下,花费过多时间处理一个ISR,那么可能(也必将)会丢失一些对系统正确运作必不可少的中断。内核为解决该问题,将中断处理程序划分为两个部分,性能关键的前一部分在禁用中断时执行,而不那么重要的后一部分延期执行,进行所有次要的操作。早期的内核版本也包含了一种同名机制,用于将操作延期一段时间执行。但该机制已经被更高效的机制取代

每个中断都有一个编号。如果中断号n分配给一个网卡而m≠ n分配给SCSI控制器,那么内核即可区分两个设备,并在中断发生时调用对应的ISR来执行特定于设备的操作。当然,同样的原则也适应
于异常,不同的异常指派了不同的编号。遗憾的是,由于特别设计(通常是历史上的)的“特性”(IA-32体系结构就是一个恰当的特例),情况并不总是像描述的那样简单。因为只有很少的编号可用于硬件中断,所以必须由几个设备共享一个编号。在IA-32处理器上,硬件中断的最大数目通常是15,这个值可不怎么大,还有考虑到有些中断编号已经永久性地分配给了标准的系统组件(键盘、定时器,等等),因而限制了可用于其他外部设备的中断编号数目

这个过程称为中断共享(interrupt sharing)。但必须硬件和内核同时支持才能使用该技术,因为必须要识别出中断来源于哪个设备。

14.1.2 硬件IRQ

中断借助于一个称为中断控制器(interrupt controller)的标准组件来请求,该组件存在于每个系统中

外部设备(或其槽位),会有电路连接到用于向中断控制器发送中断请求的组件。控制器在执行了各种电工任务之后,将中断请求转发到CPU的中断输入。因为外部设备不能直接发出中断,而必须通过上述组件请求中断,所以这种请求更正确的叫法是IRQ,或中断请求(interrupt request)

对大多数CPU来说,都只是从可用于处理硬件中断的整个中断号范围抽取一小部分使用。抽取出的范围通常位于所有中断号序列的中部,例如,IA-32 CPU总共提供了16个中断号,从32到47

如果读者曾经在IA-32系统上配置过I/O扩展卡,或研究过/proc/interrupts的内容,那么就会了解到,扩展卡的IRQ编号从0开始,到15结束,当然,前提是使用了典型的中断控制器8256A。这意味着这里同样有16个不同的选项,但数值不同。中断控制器除了负责IRQ信号的电工处理之外,还会对IRQ编号和中断号进行一个“转换”在IA-32系统上,加32即可。如果设备发出IRQ 9,CPU将产生中断41,在安装中断处理程序时必须考虑到这一点。

14.1.3 处理中断

在CPU得知发生中断后,它将进一步的处理委托给一个软件例程,该例程可能会修复故障、提供专门的处理或将外部事件通知用户进程。由于每个中断和异常都有唯一的编号,内核使用一个数组,数组项是指向处理程序函数的指针。相关的中断号根据数组项在数组中的位置判断,如下:
在这里插入图片描述

  1. 进入和退出任务
    如下图,中断处理划分为3部分。首先,必须建立一个适当的环境,使得处理程序函数能够在其中执行,接下来调用处理程序自身,最后将系统复原(在当前程序看来)到中断之前的状态。调用中断处理程序前后的两部分,分别称为进入路径(entry path)和退出路径(exit path)。
    在这里插入图片描述

    进入路径的一个关键任务是,从用户态栈切换到核心态栈。进入路径必须保存用户应用程序当前的寄存器状态,以便在中断活动结束后恢复。这与调度期间用于上下文切换的机制是相同的。在进入核心态时,只保存整个寄存器集合的一部分。内核并不使用全部寄存器。举例来说,内核代码中不使用浮点操作(只有整数计算),因而并不保存浮点寄存器。浮点寄存器的值在执行内核代码时不会改变。平台相关的数据结构pt_regs列出了核心态可能修改的所有寄存器,它的定义考虑到了不同的CPU之间的差别。在汇编语言编写的底层例程负责填充该结构。

    在退出路径中,内核会检查下列事项

    • 调度器是否应该选择一个新进程代替旧的进程。
    • 是否有信号必须投递到原进程

    然后还原寄存器集合、切换到用户态栈、切换到适用于用户应用程序的适当的处理器状态,或切换到一个不同的保护环

    代码位于arch/arch/kernel/entry.S

    在中断到达时,处理器可能处于用户态或核心态,这使得中断的进入和退出路径的不同(有可能无须切换核心态栈和用户态栈,也有可能无须检查是否要调用调度器或投递信号。)

    中断处理程序(interrupt handler)用于指代CPU对ISR(中断服务程序)的调用,包括了进入/退出路径和ISR本身。当然,如果只指代在进入路径和退出路径之间执行、由C语言实现的例程,将更为准确

  2. 中断处理程序
    在中断处理时产生中断可能会有问题。尽管可以通过在处理程序执行期间禁用中断来防止,但这会引起其他问题,如遗漏重要的中断。屏蔽(Masking,这个术语用于表示选择性地禁用一个或多个中断)因而只能短时间使用

    因此ISR必须满足如下两个要求

    1. 实现(特别是在禁用其他中断时)必须包含尽可能少的代码,以支持快速处理
    2. 可以在其他ISR执行期间调用的中断处理程序例程,不能彼此干扰

    并非ISR的每个部分都同等重要。通常,每个处理程序例程都可以划分为3个部分,具有不同的意义

    1. 关键操作必须在中断发生后立即执行。否则,无法维持系统的稳定性,或计算机的正确运作。在执行此类操作期间,必须禁用其他中断
    2. 非关键操作也应该尽快执行,但允许启用中断(因而可能被其他系统事件中断)
    3. 可延期操作不是特别重要,不必在中断处理程序中实现。内核可以延迟这些操作,在时间充裕时进行。

    内核提供了tasklet,用于在稍后执行可延期操作

14.1.4 数据结构

中断技术上的实现有两方面:汇编语言代码,与处理器高度相关,用于处理特定平台上相关的底层细节;抽象接口,是设备驱动程序及其他内核代码安装和管理IRQ处理程序所需的。

为响应外部设备的IRQ,内核必须为每个潜在的IRQ提供一个函数。该函数必须能够动态注册和注销。

IRQ相关信息管理的关键点是一个全局数组,每个数组项对应一个IRQ编号。因为数组位置和中断号是相同的,很容易定位与特定的IRQ相关的数组项:IRQ 0在位置0,IRQ 15在位置15,等等。IRQ最终映射到哪个处理器中断,在这里是不相关的

数组定义如下:

//kernel/irq/handle.c
//所有IRQ中断全局数组
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
	[0 ... NR_IRQS-1] = {
		.status = IRQ_DISABLED,
		.chip = &no_irq_chip,
		.handle_irq = handle_bad_irq,
		.depth = 1,
		.lock = __SPIN_LOCK_UNLOCKED(irq_desc->lock),
#ifdef CONFIG_SMP
		.affinity = CPU_MASK_ALL
#endif
	}
};

尽管各个数组项使用的是一个体系结构无关的数据类型,但IRQ的最大可能数目是通过一个平台相关的常数NR_IRQS指定的。大多数体系结构下,该常数定义在处理器相关的头文件include/asm-arch/irq.h中(在IA-32体系结构下,使用/include/asm-x86/mach-type/irq_vectors_limits.h).不同处理器间及同一处理器家族内,该常数的值变化都很大,主要取决于辅助CPU管理IRQ的辅助芯片。Alpha计算机在小型系统上可支持32个中断,而在Wildfire主板上可支持2048个中断。IA-64处理器的中断数目总是256。IA-32系统连同经典的8256A控制器,只提供了16个IRQ。如果使用IO-APIC(advanced programmable interrupt controller,高级可编程中断控制器)扩展,中断数目可增加到224个。该特性在所有多处理器系统都支持,也可以部署到单处理器系统。最初,所有中断槽位都使用handle_bad_irq作为处理程序,该函数只对没有安装处理程序的中断进行确认。

内核在2.6之前的版本包含了大量平台相关代码来处理IRQ,在许多地方是相同的。因而,在内核版本2.6开发期间,引入了一个新的通用的IRQ子系统。它能够以统一的方式处理不同的中断控制器和不同类型的中断。基本上,它由3个抽象层组成,如下图所示
在这里插入图片描述

  1. 高层ISR(high-level interrupt service routines,高层中断服服务例程)针对设备驱动程序端(或其他内核组件)的中断,执行由此引起的所有必要的工作。例如,如果设备使用中断通知一些数据已经到达,那么高层ISR的工作应该是将数据复制到适当的位置
  2. 中断电流处理(interrupt flow handling):处理不同的中断电流类型之间的各种差别,如边沿触发(edge-triggering)和电平触发(level-triggering)。边沿触发意味着硬件通过感知线路上的电位差来检测中断。在电平触发系统中,根据特定的电势值检测中断,与电势是否改变无关.从内核的角度来看,电平触发更为复杂,因为在每个中断后,都需要将线路明确设置为一个特定的电势,表示“没有中断”
  3. 芯片级硬件封装(chip-level hardware encapsulation):需要与在电子学层次上产生中断的底层硬件直接通信。该抽象层可以视为中断控制器的某种“设备驱动程序”

IRQ描述符的结构定义如下(用于支持MSI(message signaled interrupt)特性的成员也被忽略了。MSI是PCI标准的一个可选扩展,是PCI Express标准的必需组件。该扩展特性允许不使用硬件的物理针脚,而通过PCI总线上的“消息”来发送中断。因为现代处理器可用针脚数目是有限的,而又需要用于许多其他目的,因此针脚实际上是一种稀有资源。因而硬件设计师在寻找发送中断的替换方法,MSI机制就是其中之一。在未来,该特性的重要性会逐渐增长。内核源代码中的Documentation/MSI-HOWTO.txt包含了有关该机制的更多信息):

//include/linux/irq.h
/*IRQ描述符的结构定义*/
struct irq_desc {
	irq_flow_handler_t	handle_irq;//电流中断处理函数(边沿触发或电平触发),中断发生时被特定体系结构的代码调用,该函数使用 chip 提供的特定于控制器的函数集,进行处理中断所必须的一些底层操作
	struct irq_chip		*chip;//特定于控制器的函数集合
	struct msi_desc		*msi_desc;
	void			*handler_data;//中断函数对应的数据
	void			*chip_data;//与chip相关的数据
	/*struct irqaction链表表头,action提供了一个操作链,在处理共享中断时,内核就通过这种方式来确保中断发生时调用处理程序的顺序与其注册顺序相同,需要在中断发生时执行。由中断通知的设备驱动程序,可以将与之相关的处理程序函数放置在此处。有一个专门的数据结构用于表示这些操作,电流处理(电平触发或边沿触发)和芯片相关操作被封装在chip中*/
	struct irqaction	*action;	/* IRQ action list */
	//描述IRQ电路的当前状态 IRQ_DISABLED 等,每个常数表示一位
	unsigned int		status;		/* IRQ status */

	//depth有两个任务.可用于确定IRQ电路是启用的还是禁用,正值表示禁用,而0表示启用。为什么用正值表示禁用的IRQ呢?因为这使得内核能够区分启用和禁用的IRQ电路,以及重复禁用同一中断的情形。这个值相当于一个计数器,内核其余部分的代码每次禁用某个中断,则将对应的计数器加1;每次中断被再次启用,则将计数器减1。在depth归0时,硬件才能再次使用对应的IRQ。这种方法能够支持对嵌套禁用中断的正确处理
	unsigned int		depth;		/* nested irq disables */

	/*irq_count 和 irq_unhandled 字段提供了一些统计量,可用于检测停顿和未处理,但持续发生的中断。后者通常称作假中断(spurious interrupt)*/
	unsigned int		irq_count;	/* For detecting broken IRQs */
	unsigned int		irqs_unhandled;
...
	const char		*name;//电流层处理程序的名称,将在/proc/interrupts中显示.对边沿触发中断,通常是“edge”,对电平触发中断,通常是“level”
} ____cacheline_internodealigned_in_smp;

#define hw_interrupt_type	irq_chip
typedef struct irq_chip		hw_irq_controller;

通用的IRQ子系统3个抽象层在结构体中的表示:

  1. 电流层ISR由handle_irq提供handler_data可以指向任意数据,该数据可以是特定于IRQ或处理程序的。每当发生中断时,特定于体系结构的代码都会调用handle_irq。该函数负责使用chip中提供的特定于控制器的方法,进行处理中断所必需的一些底层操作。用于不同中断类型的默认函数由内核提供

  2. action提供了一个操作链,需要在中断发生时执行。由中断通知的设备驱动程序,可以将与之相关的处理程序函数放置在此处

  3. 电流处理和芯片相关操作被封装在chip中。为此引入了一个专门的数据结构.chip_data指向可能与chip相关的任意数据

  4. IRQ控制器抽象 struct irq_chip

    //include/linux/irq.h
    /*IRQ控制器抽象*/
    struct irq_chip {
        const char	*name;//用于标识硬件控制器。在IA-32系统上可能的值是“XTPIC”和“IO-APIC”,在AMD64系统上大多数情况下也会使用后者。在其他系统上有各种各样的值,因为有许多不同的控制器类型,其中很多类型都得到了广泛应用
        unsigned int	(*startup)(unsigned int irq);//用于第一次初始化一个IRQ。大多数情况下,初始化工作仅限于启用该IRQ。因而,startup函数实际上就是将工作转给enable
        void		(*shutdown)(unsigned int irq);//完全关闭一个中断源。如果不支持该特性,那么这个函数实际上是disable的别名
        void		(*enable)(unsigned int irq);//激活一个IRQ。换句话说,它执行IRQ由禁用状态到启用状态的转换。为此,必须向I/O内存或I/O端口中硬件相关的位置写入特定于硬件的数值
        void		(*disable)(unsigned int irq);//用于禁用IRQ
    
        void		(*ack)(unsigned int irq);//与中断控制器的硬件密切相关。在某些模型中,IRQ请求的到达(以及在处理器的对应中断)必须显式确认,后续的请求才能进行处理。如果芯片组没有这样的要求,该指针可以指向一个空函数,或NULL指针
        void		(*mask)(unsigned int irq);
        void		(*mask_ack)(unsigned int irq);//确认一个中断,并在接下来屏蔽该中断
        void		(*unmask)(unsigned int irq);
        void		(*eoi)(unsigned int irq);//现代的中断控制器不需要内核进行太多的电流控制,控制器几乎可以管理所有事务。在处理中断时需要一个到硬件的回调,由eoi提供,eoi表示end of interrupt,即中断结束
    
        void		(*end)(unsigned int irq);//标记中断处理在电流层次的结束。如果一个中断在中断处理期间被禁用,那么该函数负责重新启用此类中断。
        void		(*set_affinity)(unsigned int irq, cpumask_t dest);//在多处理器系统中,可使用set_affinity指定CPU来处理特定的IRQ。这使得可以将IRQ分配给某些CPU(通常,SMP系统上的IRQ是平均发布到所有处理器的)。该方法在单处理器系统上没用,可以设置为NULL指针
        ...
        int		(*set_type)(unsigned int irq, unsigned int flow_type);//设置IRQ的电流类型。该方法主要使用在ARM、PowerPC和SuperH机器上,其他系统不需要该方法,可以将set_type设置为NULL
        ...
    };
    
  5. 处理程序函数的表示 struct irqaction

    //include/linux/interrupt.h
    /*中断处理程序函数的对应的结构体*/
    struct irqaction {
        irq_handler_t handler;//在设备请求一个系统中断,而中断控制器通过引发中断将该请求转发到处理器的时候,内核将调用该处理程序函数
        unsigned long flags;//flags是一个标志变量,通过位图描述了IRQ(和相关的中断)的一些特性,位图中的各个标志位照例可通过预定义的常数访问 IRQF_SHARED 等
        //name和dev_id唯一地标识一个中断处理程序,如果几个设备共享一个IRQ,那么IRQ编号自身不能标识该设备,此时,在删除处理程序函数时,将需要上述信息
        const char *name;//用于标识设备(例如,“e100”、“ncr53c8xx”,等等)
        void *dev_id;//指向在所有内核数据结构中唯一标识了该设备的数据结构实例,例如网卡的net_device实例
        struct irqaction *next;//链表元素,表头为struct irq_desc.action,用于实现共享的IRQ处理程序。几个irqaction实例聚集到一个链表中。链表的所有元素都必须处理同一IRQ编号(处理不同编号的实例,位于irq_desc数组中不同的位置)。在发生一个共享中断时,内核扫描该链表找出中断实际上的来源设备。特别是在单芯片(只有一个中断)上集成了许多不同的设备(网络、USB、FireWire、声卡等)的笔记本电脑中,此类处理程序链表可能包含大约5个元素。但我们预期的情况是,每个IRQ下都注册一个设备
    };
    

    irqaction数据结构的一个概览.因为通常在一个系统上只有一种类型的中断控制器会占据支配地位(当然,并没有什么约束条件阻止多个控制器并存),所有irq_desc的handler成员都指向irq_chip的同一实例:
    在这里插入图片描述

14.1.5 中断电流处理

  1. 设置控制器硬件

    用于注册irq_chip和设置电流处理程序的函数:

    //include/linux/irq.h
    extern int set_irq_chip(unsigned int irq, struct irq_chip *chip);
    static inline void
    set_irq_handler(unsigned int irq, irq_flow_handler_t handle);
    static inline void
    set_irq_chained_handler(unsigned int irq,
                irq_flow_handler_t handle)
    extern void
    set_irq_chip_and_handler(unsigned int irq, struct irq_chip *chip,
                irq_flow_handler_t handle);
    extern void
    set_irq_chip_and_handler_name(unsigned int irq, struct irq_chip *chip,
                    irq_flow_handler_t handle, const char *name);
    
  2. 电流处理

    电流处理程序类型(边沿触发和电平触发不同):

    //include/linux/irq.h
    typedef	void fastcall (*irq_flow_handler_t)(unsigned int irq,
    				    struct irq_desc *desc);
    

    内核对各种类型提供了几个默认的电流处理程序。它们有一个共同点:每个电流处理程序在其工作结束后,都要负责调用高层ISR。handle_IRQ_event负责激活高层的处理程序

    • 边沿触发中断 handle_edge_irq
      现在的硬件大部分采用的是边沿触发中断,默认处理程序实现在handle_edge_irq中,边沿触发中断在处理时不屏蔽

      在这里插入图片描述

      在处理边沿触发的IRQ时无须屏蔽,这与电平触发IRQ是相反的。在SMP系统中,当在一个CPU上处理一个IRQ时,另一个同样编号的IRQ可以出现在另一个CPU上,即同时处理两个相同编号的中断,第二个要放弃

      函数流程:

      1. 处理一个中断在多个CPU上执行(处理一个中断时,同一个中断又触发)
      2. 循环调用高层的ISR处理程序(handle_IRQ_event).循环中处理了刚调用高层的ISR处理程序后马上又有第二个IRQ请求的情况
      3. 清除 IRQ_INPROGRESS 标志
    • 电平触发中断 handle_level_irq
      电平触发中断在处理时必须屏蔽,因此需要完成的第一件事就是调用mask_ack_irq。该辅助函数通过调用chip->mask_ack屏蔽并确认IRQ.
      在这里插入图片描述

      函数流程:

      1. 屏蔽中断,处理多核竞态
      2. 触发了高层ISR handle_IRQ_event
      3. 清除 IRQ_INPROGRESS 标志,解除屏蔽
    • 其他中断类型
      除了边沿触发和电平触发IRQ,还可能有一些不那么常见的电流类型。内核也对它们提供了默认处理程序

      • 现代IRQ硬件只需要极少的电流处理工作。只需在IRQ处理结束之后调用一个芯片相关的函数chip->eoi。此类型的默认处理程序是handle_fasteoi_irq。它基本上等同于handle_level_irq,除了只需在最后与控制器芯片交互
      • 非常简单,根本不需要电流控制的中断由handle_simple_irq管理。如果调用者想要自行处理电流,也可以使用该函数
      • 各CPU IRQ,即IRQ只能发送到多处理器系统的一个特定的CPU,由handle_percpu_irq处理。该函数在接收之后确认IRQ,在处理之后调用eoi例程。其实现非常简单,因为不需要锁,根据定义代码只能在一个CPU上运行

14.1.6 初始化和配置IRQ

  1. 注册IRQ request_irq

    //kernel/irq/manage.c
    //中断注册函数
    int request_irq(unsigned int irq, irq_handler_t handler,
            unsigned long irqflags, const char *devname, void *dev_id)
    

    在这里插入图片描述

    函数流程:

    1. 申请 irqaction ,并初始化.最后调用 setup_irq
      1. 判断该中断是否对内核熵池有所贡献,并加入对应数据结构
      2. 将 irqaction 实例添加到所属IRQ编号对应的例程链表尾部irq_desc[NUM]->action
      3. 如果安装的处理程序是该IRQ编号对应链表中的第一个,则调用handler->startup初始化函数
      4. register_irq_proc在proc文件系统中建立目录/proc/irq/NUM。而register_handler_proc生成proc/irq/NUM/name
  2. 释放IRQ free_irq

    函数流程:

    1. 通过硬件相关的函数chip->shutdown通知中断控制器该IRQ已经删除
    2. 释放中断信息 irqaction

    如果释放的是共享中断中的一个,则只从链表中删除,不需要执行上面两个过程

  3. 注册中断(异常和用户触发的软中断)
    前面讲述的机制只适用于由系统外设的中断请求所引发的中断。但内核还必须考虑由处理器本身或者用户进程中的软件机制所引发的中断。与IRQ相比,内核无需提供接口,供此类中断动态注册处理程序。因为,所使用的编号在初始化时就是已知的,此后不会改变。中断和异常的注册在内核初始化时进行,其分配在运行时并不改变
    平台相关的内核源代码基本上没有共同点.不同平台间的具体实现差别很大。
    各个平台之间最大的相似性就是文件名。arch/arch/kernel/traps.c包含了用于中断处理程序注册的系统相关的实现

    所有实现的结果都是这样:在中断发生时自动调用对应的处理程序函数。因为系统中断不支持中断共享,只需要建立中断号和函数指针之间的关联

    通常,内核以下述两种方式之一来响应中断:

    1. 向当前用户进程发送一个信号,通知有错误发生。举例来说,在IA-32和AMD64系统上,除0操作通过中断0通知。自动调用的汇编语言例程divide_error,会向用户进程发送SIGPFE信号
    2. 内核自动修复错误,这对用户进程不可见。例如,在IA-32系统上,中断14用于表示缺页异常,内核自动修复该错误

14.1.7 处理IRQ

各个平台上的中断操作都由3部分组成。进入路径从用户态切换到核心态,接下来执行实际的处理程序例程,最后从核心态切换回用户态。尽管涉及大量的汇编语言代码,至少有一些C代码片段在所有平台上都是相似的

  1. 切换到核心态
    到核心态的切换,是基于每个中断之后由处理器自动执行的汇编语言代码的。其实现可以在arch/arch/kernel/entry.S中找到,其中通常定义了各个入口点,在中断发生时处理器可以将控制流转到这些入口点

    在C语言中调用函数时,需要将所需的数据(返回地址和参数)按一定的顺序放到栈上。在用户态和核心态之间切换时,还需要将最重要的寄存器保存到栈上,以便以后恢复。这两个操作由平台相关的汇编语言代码执行。在大多数平台上,控制流接下来传递到C函数do_IRQ,其实现也是平台相关的,但情况仍然得到了很大的简化。 根据平台不同,该函数的参数或者是处理器寄存器集合,或者是中断号和指向处理器寄存器集合的指针

    //arch/arch/kernel/irq.c
    fastcall unsigned int do_IRQ(struct pt_regs regs)
    
    unsigned int do_IRQ(int irq, struct pt_regs *regs)
    

    pt_regs用于保存内核使用的寄存器集合。各个寄存器的值被依次压栈(通过汇编语言代码), 在C函数调用之前,一直保存在栈上。
    pt_regs的定义可以确保栈上的各个寄存器项与该结构的各个成员相对应。这些值并不是仅仅保存用于后续的使用,C代码也可以读取这些值。如下图:
    在这里插入图片描述

    struct pt_regs的定义是平台相关的,因为不同的处理器提供了不同的寄存器集合。pt_res中包含了内核使用的寄存器。其中不包括的寄存器,可能只能由用户态应用程序使用。在IA-32系统上,pt_regs通常定义如下:

    //include/asm-x86/ptrace.h
    //内核使用的寄存器,用于用户态和内核态切换时堆栈的保存和恢复
    struct pt_regs {
        long ebx;
        long ecx;
        long edx;
        long esi;
        long edi;
        long ebp;
        long eax;
        int  xds;
        int  xes;
        int  xfs;
        /* int  xgs; */
        long orig_eax;//这个字段包含了一个额外的值。其用途是,在进入核心态时存储eax寄存器中传递的系统调用编号。因为eax寄存器也用于向用户空间传输结果,它在系统调用过程中必定会修改。但即使修改之后,也能通过orig_eax确定系统调用的编号(例如,在使用ptrace跟踪进程时,就可能需要这样做)。
        long eip;
        int  xcs;
        long eflags;
        long esp;
        int  xss;
    };
    

    在IA-32系统上,被引发中断的编号保存在orig_eax的高8位中。其他体系结构使用其他的位置。如上所述,一些平台甚至将中断号放置在栈上,作为一个直接参数

  2. IRQ栈
    只有在内核使用内核栈来处理IRQ的情况下,上面描述的情形才是正确的。但不一定总是如此。IA-32体系结构提供了配置选项CONFIG_4KSTACKS。如果启用该配置,内核栈的长度由8 KiB缩减到4 KiB。由于IA-32计算机上页面大小是4 KiB,实现内核栈所需的页数目由2个减少到1个。由于单个内存页比两个连续的内存页更容易分配(前文讨论过),在系统中有大量活动进程(或线程)时,这使得虚拟内存子系统的工作会稍微容易些。遗憾的是,对常规的内核工作以及IRQ处理例程所需的空间来说,4 KiB并不总是够用,因而引入了另外两个栈

    • 用于硬件IRQ处理的栈
    • 用于软件IRQ处理的栈

    常规的内核栈对每个进程都会分配,而这两个额外的栈是针对各CPU分别分配的。在硬件中断发生时(或处理软中断时),内核需要切换到适当的栈

    下列数组提供了指向额外的栈的指针:

    //arch/x86/kernel/irq_32.c
    //用于硬件IRQ处理的栈,对各CPU分别分配
    static union irq_ctx *hardirq_ctx[NR_CPUS] __read_mostly;
    //用于软件IRQ处理的栈,对各CPU分别分配
    static union irq_ctx *softirq_ctx[NR_CPUS] __read_mostly;
    

    属性__read_mostly不是指栈本身,而是指这里的指针,也就是栈在内存中的地址。只有在最初分配这些栈时才会操作这些指针,而后在系统运行期间都只是读取

    用作栈的数据结构如下:

    //arch/x86/kernel/irq_32.c
    //用于栈的数据结构
    union irq_ctx {
        struct thread_info      tinfo;//用于存储中断发生之前所运行线程的有关信息
        u32                     stack[THREAD_SIZE/sizeof(u32)];//提供了栈空间
    };
    
  3. 调用电流处理程序例程
    电流处理程序例程的调用方式,因体系结构而不同,接下来将讨论AMD64和IA-32平台的调用方式。此外,还将考察IRQ子系统重写之前所使用的旧处理程序机制,该机制现在仍然用于某些地方

    • AMD64系统上的处理
      在这里插入图片描述

      //arch/x86/kernel/irq_64.c
      asmlinkage unsigned int do_IRQ(struct pt_regs *regs)
      

      函数流程:

      1. set_irq_regs 寄存器集合的值保存在一个全局变量中(中断发生之前,变量中保存的旧指针会保留下来,供后续使用)。需要访问寄存器集合的中断处理程序,可以从该变量中访问
      2. irq_enter 更新一些统计量.对于具备动态时钟周期特性的系统,如果系统已经有很长一段时间没有发生时钟中断,则更新全局计时变量jiffies
      3. 调用对所述IRQ注册的ISR的任务委托给体系结构无关的函数generic_handle_irq,它调用irq_desc[irq]->handle_irq来激活电流控制处理程序
      4. 接下来irq_exit负责记录一些统计量,另外还要调用(假定内核此时已经不再处于中断状态,即此前处理的不是嵌套中断)do_softirq来处理任何待决的软件IRQ
      5. 再次调用set_irq_regs,将指向struct pt_regs的指针恢复到上一次调用之前的值。这确保嵌套的处理程序能够正确工作
    • IA-32系统上的处理
      在这里插入图片描述

      与AMD64相比多了判断栈切换的步骤

      函数流程:

      1. set_irq_regs 寄存器集合的值保存在一个全局变量中(中断发生之前,变量中保存的旧指针会保留下来,供后续使用)。需要访问寄存器集合的中断处理程序,可以从该变量中访问
      2. irq_enter 更新一些统计量.对于具备动态时钟周期特性的系统,如果系统已经有很长一段时间没有发生时钟中断,则更新全局计时变量jiffies
      3. 判断是否要切换IRQ栈并调用中断处理函数(使用8k的栈不用切换,使用4k的栈(内核配置了CONFIG_4KSTACKS)需要切换)
        1. 如果进程已经在使用IRQ栈,因为是在处理嵌套的IRQ。在这种情况下,内核不需要做什么,所有的设置都已经完成。可以调用irq_desc[irq]->handle_irq来激活保存在IRQ数据库中的ISR
        2. 如果当前栈不是IRQ栈(curctx != irqctx),需要在二者之间切换。在这种情况下,内核执行所需的底层汇编语言操作来切换栈,然后调用irq_desc[irq]->handle_irq,最后再将栈切换回去
      4. 接下来irq_exit负责记录一些统计量,另外还要调用(假定内核此时已经不再处于中断状态,即此前处理的不是嵌套中断)do_softirq来处理任何待决的软件IRQ
      5. 再次调用set_irq_regs,将指向struct pt_regs的指针恢复到上一次调用之前的值。这确保嵌套的处理程序能够正确工作
    • 旧式的处理

      //include/linux/irq.h
      static inline void generic_handle_irq(unsigned int irq)
      {
          struct irq_desc *desc = irq_desc + irq;
      
      //现代的代码定义了该宏,重写了IRQ子系统,分离了电流处理和ISR处理
      #ifdef CONFIG_GENERIC_HARDIRQS_NO__DO_IRQ
          desc->handle_irq(irq, desc);
      #else
          if (likely(desc->handle_irq))
              desc->handle_irq(irq, desc);
          else
              //在重写IRQ子系统之前,内核混合使用了各种体系结构相关方法来处理IRQ。最重要的是,这种方法不可能分离电流处理和ISR处理:这两项任务都在同一个体系结构相关的例程中执行,通常称为__do_IRQ。
              __do_IRQ(irq);
      #endif
      }
      

      在重写IRQ子系统之前,内核混合使用了各种体系结构相关方法来处理IRQ。最重要的是,这种方法不可能分离电流处理和ISR处理:这两项任务都在同一个体系结构相关的例程中执行,通常称为__do_IRQ。
      现代的代码应该启用配置选项GENRIC_HARDIRQS_NO__DO_IRQ,使用重写后的方法来实现电流处理。在这种情况下,generic_handle_irq实际上只调用irq_desc[irq]->handle_irq。
      如果不设置该选项,内核提供了一个__do_IRQ的默认实现,包含了所有中断类型的电流处理,并调用了所需的ISR(在引入通用IRQ框架之前,实现是基于IA-32系统上使用的版本的)。基本上,该函数的用法和电流处理的实现有如下3种可能性

      1. 对一些IRQ使用通用电流处理程序,其他IRQ的处理程序不定义。对这些IRQ,采用__do_IRQ来完成电流处理和高层处理两项任务。这样的话,就需要从do_IRQ调用generic_handle_IRQ
      2. 从do_IRQ直接调用__do_IRQ。这完全忽略了对电流处理的分离。一些非主流体系结构如M32R、H8300、SuperH和Cris仍然使用这种方法
      3. 以完全体系结构相关的方法处理IRQ,不重用现存的任何框架。

      这里不讨论__do_IRQ

  4. 调用高层ISR handle_IRQ_event调用前面注册的某个中断号中的irqaction中的中断处理函数,如果是共享中断一个中断号中会有多个irqaction

    不同的电流处理程序例程都有一个共同点:采用handle_IRQ_event来激活与特定IRQ相关的高层ISR。该函数需要IRQ编号和操作链作为参数

    //kernel/irq/handle.c
    //不同的电流处理程序都调用该函数激活与特定IRQ相关的高层ISR
    irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
    

    函数流程:

    1. 如果第一个处理程序函数中没有设置IRQF_DISABLED,则用local_irq_enable_in_hardirq启用(当前CPU的)中断。换句话说,该处理程序可以被其他IRQ中断。但根据电流类型,也可能一直屏蔽刚处理的IRQ
    2. 逐一调用所注册的IRQ处理程序的action函数
    3. 如果对该IRQ设置了IRQF_SAMPLE_RANDOM,则调用add_interrupt_randomness,将事件的时间作为熵池的一个源(如果中断的发生是随机的,那么它们是理想的源)。
    4. local_irq_disable禁用中断。因为中断的启用和禁用是不嵌套的,与中断在处理开始时是否启用是不相关的。handle_IRQ_event在调用时禁用中断,在退出时仍然预期禁用中断

    在共享IRQ时,内核无法找出引发中断请求的设备。该工作完全留给处理程序例程.内核总是依次执行所有处理程序例程,处理程序例程会识别出是否为需要执行当前函数,不是目标的处理函数在检测后会快速结束

    但内核总可以检查是否有负责该IRQ的处理程序。irqreturn_t定义为处理程序函数的返回类型,它只是一个简单的整型变量。可以接收IRQ_NONEIRQ_HANDLED两个值,这取决于处理程序是否处理了该IRQ
    在执行所有处理程序例程期间,内核将返回结果用逻辑“或”操作合并起来。内核最后可以据此判断IRQ是否被处理

  5. 实现处理程序例程

    在实现处理程序例程时,必须要注意一些要点。这些会极大地影响系统的性能和稳定性。

    • 限制
      在实现ISR时,主要的问题是它们在所谓的中断上下文(interrupt context)中执行。内核代码有时在常规上下文运行,有时在中断上下文运行。为区分这两种不同情况并据此设计代码,内核提供了in_interrupt函数,用于指明当前是否在处理中断

      中断上下文与普通上下文的不同之处主要有如下3点:

      1. 中断是异步执行的。换句话说,它们可以在任何时间发生。因而从用户空间来看,处理程序例程并不是在一个明确定义的环境中执行。这种环境下,禁止访问用户空间,特别是与用户空间地址之间来回复制内存数据的行为
        例如,对网络驱动程序来说,不能将接收的数据直接转发到等待的应用程序。毕竟,内核无法确定等待数据的应用程序此时是否在运行(事实上,这种可能性很低)。
      2. 中断上下文中不能调用调度器。因而不能自愿地放弃控制权。
      3. 处理程序例程不能进入睡眠状态。只有在外部事件导致状态改变并唤醒进程时,才能解除睡眠状态。但中断上下文中不允许中断,进程睡眠后,内核只能永远等待下去。因为也不能调用调度器,不能选择进程来执行

      当然,只确保处理程序例程的直接代码不进入睡眠状态,这是不够的。其中调用的所有过程和函数(以及被这些函数/过程调用的函数/过程,依此类推)都不能进入睡眠状态。对此进行的检查并不简单,必须非常谨慎,特别是在控制路径存在大量分支时

    • 实现处理程序
      ISR函数的原型如下:

      //include/linux/interrupt.h
      typedef irqreturn_t (*irq_handler_t)(int, void *);
      

      irq指定了IRQ编号,dev_id是注册处理程序时传递的设备ID。irqreturn_t是另一个typedef,实际上只是整数

      ISR的原型在内核版本2.6.19开发期间发生了改变!此前,处理程序例程的参数还包括一个指向保存的寄存器集合的指针:

      //include/linux/interrupt.h
      irqreturn_t (*handler)(int irq, void *dev_id, struct pt_regs *regs);
      

      中断处理程序显然是所谓的“热”代码路径,耗费的处理时间是非常关键的。尽管大多数处理程序都不需要寄存器状态,但仍然需要花费时间和栈空间来向每个ISR传递一个指针。因而从原型删除该指针是个好主意

      需要访问寄存器集合的处理程序也仍然可以访问。内核定义了一个全局的各CPU数组来保存寄存器集合,而include/asm-generic/irq_regs.h中的get_irq_regs可用于获取一个指向pt_regs实例的指针。该实例包含了切换到核心态时活动的寄存器集合的状态。普通的设备驱动程序不使用该信息,但有时候对调试内核问题有用

      中断处理程序只能使用两种返回值:如果正确地处理了IRQ则返回IRQ_HANDLED,如果ISR不负责该IRQ则返回IRQ_NONE

      为处理共享中断,处理程序例程首先必须检查IRQ是否是针对该例程的。如果相关的外部设备设计得比较现代,那么硬件会提供一个简单的方法来执行该检查,通常是通过一个专门的设备寄存器。如果是该设备引起中断,则寄存器值设置为1。在这种情况下,处理程序例程必须将设备寄存器恢复默认值(通常是0),接下来开始正常的中断处理。如果例程发现设备寄存器值为0,它可以确信所管理的设备不是中断源,因而可以将控制返回到高层代码

      如果设备没有此类状态寄存器,还在使用手工轮询的方案。每次发生一个中断时,处理程序都检查相关设备是否有数据可用。倘若如此,则处理数据。否则,例程结束

      可能有一个处理程序例程同时负责多个设备的情况,例如同一类型的两块网卡。如果收到一个IRQ,则对两块卡执行同样的代码,因为两个处理程序函数指向内核代码中同一位置。如果两个设备使用不同的IRQ编号,那么处理程序例程可以区分二者。如果二者共享同一个IRQ,仍然可以根据设备相关的dev_id字段来唯一地标识各个卡

14.2 软中断(处理函数为do_softirq,两种方式调用,1.直接调用,2.在守护进程ksoftirqd中调用)

软中断使得内核可以延期执行任务。因为它们的运作方式与上文描述的中断类似,但完全是用软件实现的,所以称为软中断(software interrupt)或softIRQ

内核借助于软中断来获知异常情况的发生,而该情况将在稍后由专门的处理程序例程解决。如上所述,内核在do_IRQ末尾处理所有待决软中断,因而可以确保软中断能够定期得到处理

可以将软中断描述为一种延迟到稍后时刻执行的内核活动。

软中断机制的核心部分是一个表,包含32个softirq_action类型的数据项。该数据类型结构非常简单,只包含两个成员

//include/linux/interrupt.h
//软中断机制结构体
struct softirq_action
{
	void	(*action)(struct softirq_action *);//指向处理函数的指针,软中断发生时内核执行
	void	*data;//指向处理函数私有数据的指针
};

该数据结构的定义是体系结构无关的,而软中断机制的整个实现也是如此。除了处理的激活之外,没有利用处理器相关的功能或特性,这与普通的中断是完全相反的

软中断必须先注册,然后内核才能执行软中断。open_softirq函数即用于该目的。它在softirq_vec表中指定的位置写入新的软中断.软中断充当实现其他延期执行机制的基础,而且也很适合设备驱动程序的需要.下文将讨论相应的技术(tasklet、工作队列和内核定时器)

只有中枢的内核代码才使用软中断。软中断只用于少数场合,这些都是相对重要的情况:

//include/linux/interrupt.h
//使用软中断的场合
enum
{
	HI_SOFTIRQ=0,//用来实现tasklet,优先级比下面的tasklet更高,因为在这个枚举中是第一个
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ,//用于网络的发送,这是软中断机制的来源和其最重要的应用
	NET_RX_SOFTIRQ,//用于网络的接收,这是软中断机制的来源和其最重要的应用
	BLOCK_SOFTIRQ,//用于块层,实现异步请求完成
	TASKLET_SOFTIRQ,//用来实现tasklet
	SCHED_SOFTIRQ,//用于调度器,以实现SMP系统上周期性的负载均衡
#ifdef CONFIG_HIGH_RES_TIMERS
	HRTIMER_SOFTIRQ,//用于高分辨率定时器
#endif
};

其中两个用来实现tasklet(HI_SOFTIRQ、TASKLET_SOFTIRQ),两个用于网络的发送和接收操作(NET_TX_SOFTIRQ和NET_RX_SOFTIRQ,这是软中断机制的来源和其最重要的应用),一个用于块层,实现异步请求完成(BLOCK_SOFTIRQ),一个用于调度器(SCHED_SOFTIRQ),以实现SMP系统上周期性的负载均衡。在启用高分辨率定时器时,还需要一个软中断(HRTIMER_SOFTIRQ)

软中断的编号形成了一个优先顺序,这并不影响各个处理程序例程执行的频率或它们相当于其他系统活动的优先级,但定义了多个软中断同时活动或待决时处理例程执行的次序

raise_softirq(int nr)用于引发一个软中断(类似普通中断)。软中断的编号通过参数指定

该函数设置各CPU变量irq_stat[smp_processor_id].__softirq_pending中的对应比特位。该函数将相应的软中断标记为执行,但这个执行是延期执行。通过使用特定于处理器的位图,内核确保几个软中断(甚至是相同的)可以同时在不同的CPU上执行

如果不在中断上下文调用raise_softirq,则调用wakeup_softirqd来唤醒软中断守护进程,这是开启软中断处理的两个可选方法之一

14.2.1 软中断处理 do_softirq

  • do_IRQ 硬件中断触发后由汇编调用

    • irq_exit 注册的硬件中断函数处理完后调用该函数
      • invoke_softirq 这个函数就是 do_softirq 函数的别名,处理软中断
        • __do_softirq
          • wakeup_softirqd 如果软中断触发的太多执行太久后调用该函数唤醒软中断守护进程
  • ksoftirqd 软中断守护进程

    • do_softirq 处理软中断

有几种方法可处理软中断,但这些最终都调用do_softirq函数

在这里插入图片描述

函数流程:

  1. 确认当前不处于中断上下文中(当然,即不涉及硬件中断),如果在中断上下文则直接结束 in_interrupt
  2. 通过判断软中断的bit位是否置位,确定执行的软中断 local_softirq_pending
    1. 如果有软中断等待处理,则调用 __do_softirq
      1. 清除软中断位图,在(当前处理器上)禁用中断的情况下执行,以防其他进程对位图的修改造成干扰 set_softirq_pending
      2. 开启中断,这使得在软中断处理程序执行期间的任何时刻,都可以修改原来的位图 local_irq_enable
      3. 获取全局软中断向量表,在while中执行被置位的软中断函数
      4. 在处理了所有标记出的软中断之后,内核检查在此期间是否有新的软中断标记到位图中。要求在前一轮循环中至少有一个没有处理的软中断,而重启的次数没有超过MAX_SOFTIRQ_RESTART(通常设置为10)。如果是这样,则再次按序处理标记的软中断。这操作会一直重复下去,直至在执行所有处理程序之后没有新的未处理软中断为止
      5. 如果在MAX_SOFTIRQ_RESTART次重启处理过程之后,仍然有未处理的软中断,内核将调用wakeup_softirqd唤醒软中断守护进程

14.2.2 软中断守护进程 ksoftirqd

软中断守护进程的任务是,与其余内核代码异步执行软中断。为此,系统中的每个处理器都分配了自身的守护进程,名为ksoftirqd

内核中有两处调用wakeup_softirqd唤醒了该守护进程

  1. 在do_softirq中
  2. 在raise_softirq_irqoff末尾。该函数由raise_softirq在内部调用,如果内核当前停用了中断,也可以直接使用

wakeup_softirqd函数流程:

  1. 借助宏,从一个各CPU变量读取指向当前CPU软中断守护进程的task_struct的指针。 *tsk = __get_cpu_var(ksoftirqd);
  2. 如果该进程当前的状态不是TASK_RUNNING,则通过wake_up_process将其放置到就绪进程的列表末尾。尽管这并不会立即开始处理所有待决软中断,但只要调度器没有更好的选择,就会选择该守护进程(优先级为19)来执行

在系统启动时用initcall机制调用init不久,即创建了系统中的软中断守护进程。在初始化之后,各个守护进程都执行无限循环

ksoftirqd函数流程:

  1. 每次被唤醒时,守护进程首先检查是否有标记出的待决软中断,否则明确地调用调度器,将控制转交到其他进程 local_softirq_pending
  2. 如果有标记出的软中断,那么守护进程接下来将处理软中断。进程在一个while循环中重复调用两个函数do_softirq和cond_resched,直至没有标记出的软中断为止。cond_resched确保在对当前进程设置了TIF_NEED_RESCHED标志的情况下调用调度器。这是可能的,因为所有这些函数执行时都启用了硬件中断

14.3 tasklet

软中断是将操作推迟到未来时刻执行的最有效的方法。但该延期机制处理起来非常复杂。因为多个处理器可以同时且独立地处理软中断,同一个软中断的处理程序例程可以在几个CPU上同时运行。对软中断的效率来说,这是一个关键,多处理器系统上的网络实现显然受惠于此。但处理程序例程的设计必须是完全可重入且线程安全的。另外,临界区必须用自旋锁保护(或其他IPC机制),而这需要大量审慎的考虑。

tasklet和工作队列是延期执行工作的机制,tasklet实现基于软中断,但它们更易于使用,因而更适合于设备驱动程序(以及其他一般性的内核代码)

由于历史原因,术语下半部(bottom half)通常指代两个不同的东西;首先,它是指ISR代码的下半部,负责执行非时间关键操作。遗憾的是,早期内核版本中使用的操作延期执行机制,也称为下半部,因而使用的术语经常是含糊不清的。在此期间,下半部不再作为内核机制存在。它们在内核版本2.5开发期间被废弃,被tasklet代替,这是一个好得多的替代品。

tasklet是“小进程”,执行一些迷你任务,对这些任务使用全功能进程可能比较浪费

14.3.1 创建tasklet

tasklet的数据结构称作tasklet_struct

//include/linux/interrupt
//tasklet机制中枢数据结构,tasklet和工作队列是延期执行工作的机制,其实现基于软中断,tasklet是“小进程”,执行一些迷你任务,对这些任务使用全功能进程可能比较浪费
struct tasklet_struct
{
	struct tasklet_struct *next;//建立链表,排队执行,表头为tasklet_vec,每个cpu一个表头
	unsigned long state;//任务的当前状态,类似于真正的进程。但只有两个选项,分别由state中的一个比特位表示,这也是二者可以独立设置/清除的原因,TASKLET_STATE_SCHED(表示在tasklet注册到内核,等待调度执行时,设置)和TASKLET_STATE_RUN(表示tasklet当前正在执行,在SMP系统上有用,用于保护tasklet在多个处理器上并行执行)
	atomic_t count;//引用计数,用于禁用已经调度的tasklet.如果其值不等于0,在接下来执行所有待决的tasklet时,将忽略对应的tasklet。
	void (*func)(unsigned long);//指向要延期执行的函数
	unsigned long data;//函数的参数
};

14.3.2 注册tasklet tasklet_schedule

tasklet_schedule 将一个tasklet注册到系统中

//include/linux/interrupt
//将一个tasklet注册到 tasklet_vec 中
static inline void tasklet_schedule(struct tasklet_struct *t)

如果设置了TASKLET_STATE_SCHED标志位,则结束注册过程,因为该tasklet此前已经注册了。否则,将该tasklet置于一个链表的起始,其表头是特定于CPU的变量tasklet_vec。该链表包含了所有注册的tasklet,使用next成员作为链表元素。

在注册了一个tasklet之后,tasklet链表即标记为即将进行处理

14.3.3 执行tasklet

tasklet的生命周期中最重要的部分就是其执行。因为tasklet基于软中断实现,它们总是在处理软中断时执行

tasklet关联到TASKLET_SOFTIRQ软中断。因而,调用raise_softirq(TASKLET_SOFTIRQ),即可在下一个适当的时机执行当前处理器的tasklet。内核使用tasklet_action作为该软中断的action函数。

  • tasklet_action TASKLET_SOFTIRQ软中断处理函数
    • 从当前cpu的tasklet链表中获取要执行的tasklet函数,在while中执行链表中的每个tasklet函数
    • 执行完所有tasklet函数期间如果新增了tasklet函数,则再次触发该软中断,在下一次软中断中处理

因为一个tasklet只能在一个处理器上执行一次,但其他的tasklet可以并行运行,所以需要特定于tasklet的锁。state 状态用作锁变量。在执行一个tasklet 的处理程序函数之前,内核使用tasklet_trylock 检查tasklet的状态是否为 TASKLET_STATE_RUN 。换句话说,它是否已经在系统的另一个处理器上运行.如果对应比特位尚未设置,则设置该比特位

  • 更高优先级的tasklet软中断为 HI_SOFTIRQ,相关的软中断处理函数 action 是 tasklet_hi_action
  • 注册的tasklet在CPU相关的变量tasklet_hi_vec中排队。使用tasklet_hi_schedule注册的
  • “较高优先级”是指该软中断的处理程序HI_SOFTIRQ在所有其他处理程序之前执行,尤其是在构成了软中断活动主体的网络处理程序之前执行.当前,大部分声卡驱动程序都利用了这一选项,因为操作延迟时间太长可能损害音频输出的音质。而用于高速传输的网卡也可以得益于该机制

14.4 等待队列和完成量

等待队列(wait queue)用于使进程等待某一特定事件发生,而无须频繁轮询。进程在等待期间睡眠,在事件发生时由内核自动唤醒。完成量(completion)机制基于等待队列,内核利用该机制等待某一操作结束。这两种机制使用得都比较频繁,主要用于设备驱动程序

14.4.1 等待队列

  1. 数据结构

    //include/linux/wait.h
    //等待队列头结构体
    struct __wait_queue_head {
        spinlock_t lock;//自旋锁
        struct list_head task_list;//链表头,实现队列,队列的成员数据结构为struct __wait_queue
    };
    typedef struct __wait_queue_head wait_queue_head_t;
    
    //等待队列头中的成员
    struct __wait_queue {
        unsigned int flags;//值为WQ_FLAG_EXCLUSIVE(表示等待进程想要被独占地唤醒),或者为0
    #define WQ_FLAG_EXCLUSIVE	0x01
        void *private;//指向等待进程的task_struct实例。该变量本质上可以指向任意的私有数据,但内核中只有很少情况下才这么用
        wait_queue_func_t func;//调用func,唤醒等待进程
        struct list_head task_list;//用作一个链表元素,用于将wait_queue_t实例放置到等待队列头中(__wait_queue_head.task_list)
    };
    typedef struct __wait_queue wait_queue_t;
    typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int sync, void *key);
    

    使用:

    1. 首先调用 wait_event 或等价函数,将进程放入等待队列.进程释放调度器,并进入休眠.如在向块设备发出传输数据的请求后,调用该函数。因为传输不会立即发生,而在此期间又没有其他事情可做,所以进程可以睡眠,将CPU时间让给系统中的其他进程
    2. 在其他地方调用 wake_up 函数或等价函数,唤醒等待队列中的睡眠进程.如块设备的数据到达后调用

    在使用wait_event使进程睡眠之后,必须确保在内核中另一处有一个对应的wake_up调用

  2. 使进程睡眠

    将等待队列项加入等待队列的函数:

    //kernel/wait.c
    //将一个进程增加到等待队列,通常不直接使用,更常用的是wait_event
    void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
    //将一个进程增加到等待队列尾部
    void fastcall add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
    //将一个进程增加到等待队列
    void fastcall
    prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
    //将一个进程增加到等待队列尾部
    void fastcall
    prepare_to_wait_exclusive(wait_queue_head_t *q, wait_queue_t *wait, int state)
    

    初始化等待队列项:

    //include/linux/wait.h
    //初始化一个动态分配的wait_queue_t实例
    static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
    
    //创建wait_queue_t的静态实例,并初始化
    #define DEFINE_WAIT(name)						\
        wait_queue_t name = {						\
            .private	= current,				\
            .func		= autoremove_wake_function,/*尝试唤醒进程,并将所属等待队列成员从等待队列删除*/		\
            .task_list	= LIST_HEAD_INIT((name).task_list),	\
        }
    

    将当前进程加入等待队列头并休眠,等待条件满足后从队列移除并将进程设为运行态:

    //include/linux/wait.h
    //将当前进程置于等待队列中,进程状态不可中断,使当前进程在一个等待队列中睡眠,进程进入睡眠,将控制权释放给调度器.在使用wait_event使进程睡眠之后,必须确保在内核中另一处有一个对应的wake_up调用,wq:等待队列头,condition:等待的条件,条件满足(>0)则不等待
    #define wait_event(wq, condition) 					\
    do {									\
        if (condition)	 						\
            break;							\
        __wait_event(wq, condition);					\
    } while (0)
    
    //将当前进程置于等待队列中,使用的进程状态为TASK_INTERRUPTIBLE。因而睡眠进程可以通过接收信号而唤醒
    #define wait_event_interruptible(wq, condition)				\
    ({									\
        int __ret = 0;							\
        if (!(condition))						\
            __wait_event_interruptible(wq, condition, __ret);	\
        __ret;								\
    })
    
    //将当前进程置于等待队列中,等待满足指定的条件,但如果等待时间超过了指定的超时限制(按jiffies指定)则停止。这防止了进程永远睡眠
    #define wait_event_timeout(wq, condition, timeout)			\
    ({									\
        long __ret = timeout;						\
        if (!(condition)) 						\
            __wait_event_timeout(wq, condition, __ret);		\
        __ret;								\
    })
    
    //将当前进程置于等待队列中,使进程睡眠,但可以通过接收信号唤醒。它也注册了一个超时限制,超时唤醒
    #define wait_event_interruptible_timeout(wq, condition, timeout)	\
    ({									\
        long __ret = timeout;						\
        if (!(condition))						\
            __wait_event_interruptible_timeout(wq, condition, __ret); \
        __ret;								\
    })
    
  3. 唤醒进程

    内核定义了一系列宏,可用于唤醒等待队列中的进程。它们基于同一个函数:

    //include/linux/wait.h
    //唤醒等待队列中的睡眠进程,全部基于同一个函数
    #define wake_up(x)			__wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 1, NULL)
    #define wake_up_nr(x, nr)		__wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, nr, NULL)
    #define wake_up_all(x)			__wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 0, NULL)
    #define wake_up_interruptible(x)	__wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
    #define wake_up_interruptible_nr(x, nr)	__wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
    #define wake_up_interruptible_all(x)	__wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
    
    • __wake_up
      • __wake_up_common 唤醒等待队列中的睡眠进程.遍历等待队列头中的所有项,并调用每一项中的唤醒进程函数

14.4.2 完成量

完成量与信号量有些相似,但是基于等待队列实现的。我们感兴趣的是完成量的接口。场景中有两个参与者:一个在等待某操作完成,而另一个在操作完成时发出声明。实际上,这已经被简化过了:可以有任意数目的进程等待操作完成。为表示进程等待的即将完成的“某操作”,内核使用了下述数据结构

//include/linux/completion.h
/*完成量数据结构,实现基于等待队列*/
struct completion {
	unsigned int done;/*可能在某些进程开始等待之前,事件就已经完成,done用来处理这种情形,每次调用complete时,该计数器都加1,仅当done等于0时,wait_for系列函数才会使调用进程进入睡眠.complete_all的工作方式类似,但它会将计数器设置为最大可能值(UINT_MAX/2,这是无符号整数最大值的一半,因为计数器也可能取负值),这样,在事件完成后调用wait_系列函数的进程将永远不会睡眠*/
	wait_queue_head_t wait;/*等待队列头,等待进程在队列上睡眠*/
};

//初始化一个动态分配的完成量completion实例
static inline void init_completion(struct completion *x)

/*定义一个完成量并初始化*/
#define DECLARE_COMPLETION(work) \
	struct completion work = COMPLETION_INITIALIZER(work)

//进程可以用wait_for_completion添加到等待队列,进程在其中等待(以独占睡眠状态),直至请求被内核的某些部分处理,进程在等待事件的完成时处于不可中断状态
extern void wait_for_completion(struct completion *);
extern int wait_for_completion_interruptible(struct completion *x);
extern unsigned long wait_for_completion_timeout(struct completion *x,
						   unsigned long timeout);
extern unsigned long wait_for_completion_interruptible_timeout(
			struct completion *x, unsigned long timeout);

//在请求由内核的另一部分处理之后,必须调用complete或complete_all来唤醒等待的进程
extern void complete(struct completion *);
extern void complete_all(struct completion *);

//kernel/exit.c
//唤醒完成量等待进程,并调用do_exit结束内核线程
NORET_TYPE void complete_and_exit(struct completion *comp, long code)

14.4.3 工作队列

工作队列是将操作延期执行的另一种手段。因为它们是通过守护进程在用户上下文执行,函数可以睡眠任意长的时间,这与内核是无关的。在内核版本2.5开发期间,设计了工作队列,用以替换此前使用的keventd机制。

每个工作队列都有一个数组,数组项的数目与系统中处理器的数目相同。每个数组项都列出了将延期执行的任务

对每个工作队列来说,内核都会创建一个新的内核守护进程,延期任务使用上文描述的等待队列机制,在该守护进程的上下文中执行

创建新的工作队列:

//include/linux/workqueue.h
//创建新的工作队列,在所有CPU上都创建一个工作线程
#define create_workqueue(name) __create_workqueue((name), 0, 0)
//创建新的工作队列,创建能够与系统休眠进行良好协作的工作队列,与电源管理相关
#define create_freezeable_workqueue(name) __create_workqueue((name), 1, 1)
//创建新的工作队列,只在系统的第一个CPU上创建一个线程
#define create_singlethread_workqueue(name) __create_workqueue((name), 1, 0)

工作队列的工作任务结构体

//include/linux/workqueue.h
//工作队列的工作任务结构体,所有推送到工作队列上的任务,都必须打包为work_struct结构的实例
struct work_struct {
	atomic_long_t data;//不用void*而用atomic_long_t的原因,内核使用了一点小技巧,显然有点近乎于“肮脏”,以便将更多信息放入该结构,而又不付出更多代价。因为指针在所有支持的体系结构上都对齐到4字节边界,而前两个比特位保证为0。因而可以“滥用”这两个比特位,将其用作标志位。剩余的比特位照旧保存指针的信息。下面三个宏定义用于屏蔽标志位,请注意,将data设置为原子数据类型,确保对该比特位的修改不会带来并发问题
#define WORK_STRUCT_PENDING 0		/*用来查找当前是否有待决(该标志位置位)的可延迟工作项。辅助宏work_pending(work)用来检查该标志位*//* T if work item pending execution */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
	struct list_head entry;//链表元素,用于将几个work_struct实例群集到一个链表中,链表头为workqueue_struct.list
	work_func_t func;//指向将延期执行的函数,函数的参数是指针,指向用于提交该工作的work_struct实例,这使得工作函数可以获得work_struct的data成员,该成员可以指向与work_struct相关的任意数据。

};

struct work_struct;
typedef void (*work_func_t)(struct work_struct *work);

//向一个现存的work_struct实例提供一个延期执行函数。如果需要data成员,则需要稍后设置
#define INIT_WORK(_work, _func)						\
	do {								\
		(_work)->data = (atomic_long_t) WORK_DATA_INIT();	\
		INIT_LIST_HEAD(&(_work)->entry);			\
		PREPARE_WORK((_work), (_func));				\
	} while (0)

将工作队列任务加入工作队列

//include/linux/workqueue.h
//向一个工作队列添加work_struct实例
int fastcall queue_work(struct workqueue_struct *wq, struct work_struct *work)
//向一个工作队列添加delayed_work实例,这是一个work_struct扩展结构体,增加了一个定时器,确保在延期工作执行之前,至少会经过由delay指定的一段时间(以jiffies为单位)
int fastcall queue_delayed_work(struct workqueue_struct *wq,
			struct delayed_work *dwork, unsigned long delay)

//工作队列延时,确保排队的工作项将在提交后指定的一段时间内执行的扩展work_struct结构体,添加一个定时器
struct delayed_work {
	struct work_struct work;
	struct timer_list timer;
};

内核创建了一个标准的工作队列,在 init_workqueues 函数中,称为events。内核的各个部分中,凡是没有必要创建独立的工作队列者,均可使用该队列,全局变量为keventd_wq

向标准工作队列添加工作队列任务函数:

//kernel/workqueue.c
//将新的工作添加标准工作队列(event)
int fastcall schedule_work(struct work_struct *work)
//将新的工作添加标准工作队列(event)
int fastcall schedule_delayed_work(struct delayed_work *dwork,
					unsigned long delay)

总结

软中断处理函数有两种方式被调用

  1. 直接调用,在硬件中断处理完最后调用,这部分不能被抢占还在中断中,当处理超过时间或超过10次时,剩余的软中断会开启软中断进程来进行处理
  2. 在守护进程ksoftirqd中调用,当硬件中断最后处理软中断次数过多时会唤醒该线程并退出硬件中断函数

底半步 tasklet(延后处理,用于驱动) 实现基于软中断,不能休眠

等待队列(用于同步,进程间同步,如read,一个进程等另一个进程) 条件不满足就调度进程

完成量(用于同步,轻量级同步,提供了更为精细的同步控制,一个执行单元等待另一个执行单元,是信号量的补充) 实现基于 等待队列

工作队列(延后执行,内核模块或驱动) 实现基于 等待队列.每个工作队列创建一个进程来执行工作,进程在等待队列上等待条件满足,可休眠

=========================================

涉及的命令和配置:

PCI的MSI扩展说明Documentation/MSI-HOWTO.txt

中断控制器类型(和系统中分配的所有IRQ)可以在/proc/interrupts中看到

wolfgang@meitner> cat /proc/interrupts 
    CPU0 CPU1  CPU2  CPU3
0:    48    1     0     0 IO-APIC-edge timer
1:     1    0     1     0 IO-APIC-edge i8042
4:     3    0     0     3 IO-APIC-edge
8:     0    0     0     1 IO-APIC-edge rtc
9:     0    0     0     0 IO-APIC-fasteoi acpi
16:   48   48 96720 50082 IO-APIC-fasteoi libata, uhci_hcd:usb1
18:    1    0     2     0 IO-APIC-fasteoi uhci_hcd:usb3,uhci_hcd:usb6, 
ehci_hcd:usb7
19:    0    0     0     0 IO-APIC-fasteoi uhci_hcd:usb5
21:    0    0     0     0 IO-APIC-fasteoi uhci_hcd:usb2
22: 407287 370858 1164 1166 IO-APIC-fasteoi libata, libata, HAD Intel 
23: 0 0 0 0 IO-APIC-fasteoi uhci_hcd:usb4,
ehci_hcd:usb8
NMI: 0 0 0 0 Non-maskable interrupts
LOC: 2307075 2266433 2220704 2208597 Local timer interrupts 
RES: 22037 18253 33530 35156 Rescheduling interrupts 
CAL: 363 373 394 184 function call interrupts 
TLB: 3355 3729 1919 1630 TLB shootdowns 
TRM: 0 0 0 0 Thermal event interrupts 
THR: 0 0 0 0 Threshold APIC interrupts 
SPU: 0 0 0 0 Spurious interrupts
ERR: 0

芯片名称之后是电流处理程序的名称,导致出现的名称诸如“IO-APIC-edge”。除了列出所有注册的IRQ之外,该文件尾部还提供了一些统计信息

查看内核中注册的中断 proc/irq/NUM
查看内核中具体的中断 proc/irq/NUM/name

全局指针数组static union irq_ctx *hardirq_ctx[NR_CPUS] __read_mostly;,用于硬件IRQ处理的栈,对各CPU分别分配
全局指针数组static union irq_ctx *softirq_ctx[NR_CPUS] __read_mostly;,用于软件IRQ处理的栈,对各CPU分别分配

全局软中断向量表static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;

全局变量static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec) = { NULL };,tasklet表头,每个cpu一个表头
全局变量static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec) = { NULL };,tasklet表头,每个cpu一个表头,优先级比tasklet更高

全局变量static struct workqueue_struct *keventd_wq __read_mostly;内核创建的标准工作队列,称为events。内核的各个部分中,凡是没有必要创建独立的工作队列者,均可使用该队列

硬件中断延后处理的底半部实现的三种方法:

  1. tasklet 基于软中断,不能休眠
  2. 工作队列 工作队列是创建内核守护进程来执行的,基于等待队列,可以休眠
  3. 软中断
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值