linux内核架构-中断处理机制

本文对中断系统进行了全面的分析与探讨,主要包括中断控制器、中断分类、中断亲和力、中断线程化与 SMP 中的中断迁徙等。首先对中断工作原理进行了简要分析,接着详细探讨了中断亲和力的实现原理,最后对中断线程化与非线程化中断之间的实现机理进行了对比分析。在每一部分都对linux3.2.0内核中arm架构的中断机理进行了研究对比。

1 什么是中断?

Linux 内核需要对连接到计算机上的所有硬件设备进行管理,毫无疑问这是它的份内事。如果要管理这些设备,首先得和它们互相通信才行,一般有两种方案可实现这种功能:

轮询(polling 让内核定期对设备的状态进行查询,然后做出相应的处理;

中断(interrupt 让硬件在需要的时候向内核发出信号(变内核主动为硬件主动)。

轮询因其周期性的执行,影响效率,故而采用中断方式管理所有外设。


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

软中断(SoftIRQ):用于有效实现内核中的延期操作。


中断工作流程:

从物理学的角度看,中断是一种电信号,由硬件设备产生,并直接送入中断控制器(如 8259A)的输入引脚上,然后再由中断控制器向处理器发送相应的信号。处理器一经检测到该信号,便中断自己当前正在处理的工作,转而去处理中断。此后,处理器会通知 OS 已经产生中断。这样,OS 就可以对这个中断进行适当的处理。不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标识,这些值通常被称为中断请求线。


2 APIC vs 8259A

X86计算机的 CPU 为中断只提供了两条外接引脚:NMI 和 INTR。其中 NMI 是不可屏蔽中断,它通常用于电源掉电和物理存储器奇偶校验;INTR是可屏蔽中断,可以通过设置中断屏蔽位来进行中断屏蔽,它主要用于接受外部硬件的中断信号,这些信号由中断控制器传递给 CPU。

常见的中断控制器有两种:

2.1 可编程中断控制器8259A

传统的 PIC(Programmable Interrupt Controller)是由两片 8259A 风格的外部芯片以“级联”的方式连接在一起。每个芯片可处理多达 8 个不同的 IRQ。因为从 PIC 的 INT 输出线连接到主 PIC 的 IRQ2 引脚,所以可用 IRQ 线的个数达到 15 个,如图 1 所示。

2.2 高级可编程中断控制器(APIC)

8259A 只适合单 CPU 的情况,为了充分挖掘 SMP 体系结构的并行性,能够把中断传递给系统中的每个 CPU 至关重要。基于此理由,Intel 引入了一种名为 I/O 高级可编程控制器的新组件,来替代老式的 8259A 可编程中断控制器。该组件包含两大组成部分:一是“本地 APIC”,主要负责传递中断信号到指定的处理器;举例来说,一台具有三个处理器的机器,则它必须相对的要有三个本地 APIC。另外一个重要的部分是 I/O APIC,主要是收集来自 I/O 装置的 Interrupt 信号且在当那些装置需要中断时发送信号到本地 APIC,系统中最多可拥有 8 个 I/O APIC。

每个本地 APIC 都有 32 位的寄存器,一个内部时钟,一个本地定时设备以及为本地中断保留的两条额外的 IRQ 线 LINT0 和 LINT1。所有本地 APIC 都连接到 I/O APIC,形成一个多级 APIC 系统,如图 2 所示。

目前大部分单处理器系统都包含一个 I/O APIC 芯片,可以通过以下两种方式来对这种芯片进行配置:

1) 作为一种标准的 8259A 工作方式。本地 APIC 被禁止,外部 I/O APIC 连接到 CPU,两条 LINT0 和 LINT1 分别连接到 INTR 和 NMI 引脚。

2) 作为一种标准外部 I/O APIC。本地 APIC 被激活,且所有的外部中断都通过 I/O APIC 接收。

辨别一个系统是否正在使用 I/O APIC,可以在命令行输入如下命令:

user@UserComputer:~$ cat /proc/interrupts 
           CPU0       CPU1       
  0:         44          0   IO-APIC-edge      timer
  1:          3          0   IO-APIC-edge      i8042
  6:       1528       2111   IO-APIC-edge      floppy
  7:          0          0   IO-APIC-edge      parport0
  8:          0          1   IO-APIC-edge      rtc0
  9:          2          1   IO-APIC-fasteoi   acpi
 12:          1          3   IO-APIC-edge      i8042
 14:          0          0   IO-APIC-edge      ata_piix
 15:          0          0   IO-APIC-edge      ata_piix
 ......

果输出结果中列出了 IO-APIC,说明您的系统正在使用 APIC。如果看到 XT-PIC,意味着您的系统正在使用 8259A 芯片。

2.3 ARM Cortex-A8中断研究

ARM Cortex-A8处理器是第一款基于ARMv7架构的产品。Sitara家族AM335x系列 是基于ARM ® Cortex-A8 微处理器。中断控制器能够控制多达 128个中断请求。微处理器的ARM中断控制器(AINTC)负责优化系统外设发送的服务请求,产生nIRQ 或nFIQ,发送给cpu。中断类型和中断输入优先级是可编程的。AINTC 和 ARM 微处理器通过AXI2OCP桥相连,遵循AXI协议,运行速率是微处理器的一半。

AINTC的通用特点:
• 多达128个中断请求
• 每个中断输入都可定制优先级
• 每个中断输入能被指定为nFIQ 或 nIRQ
• nFIQ 和 nIRQ具有独立的优先级排序

注意:AXI(Advanced eXtensible Interface)是一种总线协议,该协议是ARM公司提出的AMBA(Advanced Microcontroller Bus Architecture)3.0协议中最重要的部分,是一种面向高性能、高带宽、低延迟的片内总线。它的地址/控制和数据相位是分离的,支持不对齐的数据传输,同时在突发传输中,只需要首地址,同时分离的读写数据通道、并支持Outstanding传输访问和乱序访问,并更加容易进行时序收敛。AXI 是AMBA 中一个新的高性能协议。AXI 技术丰富了现有的AMBA 标准内容,满足超高性能和复杂的片上系统(SoC)设计的需求。

2.4 ARM Cortex-A8 和 x86架构的中断硬件比较

ARM Cortex-A8一般使用ARM中断控制器(AINTC)与MPU相连,连接方式与X86使用8259A的方式相似,只是遵循的协议不同。


3 中断分类

中断可分为同步(synchronous)中断和异步(asynchronous)中断:

3.1. 同步中断是当指令执行时由 CPU 控制单元产生,之所以称为同步,是因为只有在一条指令执行完毕后 CPU 才会发出中断,而不是发生在代码指令执行期间,比如系统调用。

3.2. 异步中断是指由其他硬件设备依照 CPU 时钟信号随机产生,即意味着中断能够在指令之间发生,例如键盘中断。

根据 Intel 官方资料,同步中断称为异常(exception),异步中断被称为中断(interrupt)。

中断可分为可屏蔽中断(Maskable interrupt)和非屏蔽中断(Nomaskable interrupt)。异常可分为故障(fault)、陷阱(trap)、终止(abort)三类。


从广义上讲,中断可分为四类:中断故障陷阱终止。这些类别之间的异同点请参看 表 1。

表 1:中断类别及其行为
类别原因异步/同步返回行为
中断来自I/O设备的信号异步总是返回到下一条指令
陷阱有意的异常同步总是返回到下一条指令
故障潜在可恢复的错误同步返回到当前指令
终止不可恢复的错误同步不会返回

X86 体系结构的每个中断都被赋予一个唯一的编号或者向量(8 位无符号整数)。非屏蔽中断和异常向量是固定的,而可屏蔽中断向量可以通过对中断控制器的编程来改变。

4 Linux 2.6 中断处理原理简介

中断描述符表(Interrupt Descriptor Table,IDT)是一个系统表,它与每一个中断或异常向量相联系,每一个向量在表中存放的是相应的中断或异常处理程序的入口地址。内核在允许中断发生前,也就是在系统初始化时,必须把 IDT 表的初始化地址装载到 idtr 寄存器中,初始化表中的每一项。

当处于实模式下时,IDT 被初始化并由 BIOS 程序所使用。然而,一旦 Linux 开始接管,IDT 就被移到 ARM 的另一个区域,并进行第二次初始化,因为 Linux 不使用任何 BIOS 程序,而使用自己专门的中断服务程序(例程)(interrupt service routine,ISR)。中断和异常处理程序很像常规的 C 函数,有三个主要的数据结构包含了与 IRQ 相关的所有信息:hw_interrupt_typeirq_desc_tirqaction,图3 解释了它们之间是如何关联的。

在 X86 系统中,对于 8259A 和 I/O APIC 这两种不同类型的中断控制器,hw_interrupt_type 结构体被赋予不同的值,具体区别参见表 2。

表 2:8259A 和 I/O APIC PIC 的区别
8259AI/O APIC
static struct hw_interrupt_type i8259A_irq_type = { 
"XT-PIC", 
startup_8259A_irq, 
shutdown_8259A_irq, 
enable_8259A_irq, 
disable_8259A_irq, 
mask_and_ack_8259A, 
end_8259A_irq, 
NULL }; 
static struct hw_interrupt_type ioapic_edge_type ={
.typename = "IO-APIC-edge",
.startup = startup_edge_ioapic,
.shutdown = shutdown_edge_ioapic,
.enable = enable_edge_ioapic,
.disable = disable_edge_ioapic,
.ack = ack_edge_ioapic,
.end = end_edge_ioapic,
.set_affinity = set_ioapic_affinity,};
static struct hw_interrupt_type ioapic_level_type = {
.typename = "IO-APIC-level",
.startup = startup_level_ioapic,
.shutdown = shutdown_level_ioapic,
.enable = enable_level_ioapic,
.disable = disable_level_ioapic,
.ack = mask_and_ack_level_ioapic,
.end = end_level_ioapic,
.set_affinity = set_ioapic_affinity,};

在中断初始化阶段,调用 hw_interrupt_type 类型的变量初始化 irq_desc_t 结构中的handle 成员。在早期的系统中使用级联的8259A,所以将用i8259A_irq_type 来进行初始化,而对于SMP系统来说,要么以ioapic_edge_type,或以ioapic_level_type 来初始化handle 变量。

对于每一个外设,要么以静态(声明为 static 类型的全局变量)或动态(调用 request_irq 函数)的方式向 Linux 内核注册中断处理程序。不管以何种方式注册,都会声明或分配一块irqaction 结构(其中handler 指向中断服务程序),然后调用setup_irq() 函数,将irq_desc_tirqaction 联系起来。

当中断发生时,通过中断描述符表 IDT 获取中断服务程序入口地址,对于 32≤ i ≤255(i≠128) 之间的中断向量,将会执行push $i-256,jmp common_interrupt 指令。随之将调用do_IRQ() 函数,以中断向量为irq_desc[] 结构的下标,获取action 的指针,然后调用handler 所指向的中断服务程序。

从以上描述,我们不难看出整个中断的流程,如图 4 所示:

4.2 linux-3.2.0-m3352/arch/arm/include/asm$

4.2.1 IRQ控制器抽象定义

/**
 * struct irq_desc - interrupt descriptor
 * @irq_data:		per irq and chip data passed down to chip functions
 * @timer_rand_state:	pointer to timer rand state struct
 * @kstat_irqs:		irq stats per cpu
 * @handle_irq:		highlevel irq-events handler
 * @preflow_handler:	handler called before the flow handler (currently used by sparc)
 * @action:		the irq action chain
 * @status:		status information
 * @core_internal_state__do_not_mess_with_it: core internal status information
 * @depth:		disable-depth, for nested irq_disable() calls
 * @wake_depth:		enable depth, for multiple irq_set_irq_wake() callers
 * @irq_count:		stats field to detect stalled irqs
 * @last_unhandled:	aging timer for unhandled count
 * @irqs_unhandled:	stats field for spurious unhandled interrupts
 * @lock:		locking for SMP
 * @affinity_hint:	hint to user space for preferred irq affinity
 * @affinity_notify:	context for notification of affinity changes
 * @pending_mask:	pending rebalanced interrupts
 * @threads_oneshot:	bitfield to handle shared oneshot threads
 * @threads_active:	number of irqaction threads currently running
 * @wait_for_threads:	wait queue for sync_irq to wait for threaded handlers
 * @dir:		/proc/irq/ procfs entry
 * @name:		flow handler name for /proc/interrupts output
 */
struct irq_desc {
	struct irq_data		irq_data;
	struct timer_rand_state *timer_rand_state;
	unsigned int __percpu	*kstat_irqs;
	irq_flow_handler_t	handle_irq;
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
	irq_preflow_handler_t	preflow_handler;
#endif
	struct irqaction	*action;	/* IRQ action list */
	unsigned int		status_use_accessors;
	unsigned int		core_internal_state__do_not_mess_with_it;
	unsigned int		depth;		/* nested irq disables */
	unsigned int		wake_depth;	/* nested wake enables */
	unsigned int		irq_count;	/* For detecting broken IRQs */
	unsigned long		last_unhandled;	/* Aging timer for unhandled count */
	unsigned int		irqs_unhandled;
	raw_spinlock_t		lock;
	struct cpumask		*percpu_enabled;
#ifdef CONFIG_SMP
	const struct cpumask	*affinity_hint;
	struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
	cpumask_var_t		pending_mask;
#endif
#endif
	unsigned long		threads_oneshot;
	atomic_t		threads_active;
	wait_queue_head_t       wait_for_threads;
#ifdef CONFIG_PROC_FS
	struct proc_dir_entry	*dir;
#endif
	struct module		*owner;
	const char		*name;
} ____cacheline_internodealigned_in_smp;


/**
 * struct irq_data - per irq and irq chip data passed down to chip functions
 * @irq:        中断号
 * @hwirq:        硬件中断号, local to the interrupt domain
 * @node:        node index useful for balancing
 * @state_use_accessors: status information for irq chip functions.
 *            Use accessor functions to deal with it
 * @chip:        low level interrupt hardware access
 * @domain:        Interrupt translation domain; responsible for mapping
 *            between hwirq number and linux irq number.
 * @handler_data:    per-IRQ data for the irq_chip methods
 * @chip_data:        platform-specific per-chip private data for the chip
 *            methods, to allow shared chip implementations
 * @msi_desc:        MSI descriptor
 * @affinity:        IRQ affinity on SMP
 *
 * The fields here need to overlay the ones in irq_desc until we
 * cleaned up the direct references and switched everything over to
 * irq_data.
 */


struct irq_data {
	unsigned int		irq;
	unsigned long		hwirq;
	unsigned int		node;
	unsigned int		state_use_accessors;
	struct irq_chip		*chip;
	struct irq_domain	*domain;
	void			*handler_data;
	void			*chip_data;
	struct msi_desc		*msi_desc;
#ifdef CONFIG_SMP
	cpumask_var_t		affinity;
#endif
};

/**
 * struct irq_chip - hardware interrupt chip descriptor
 *
 * @name:     在/proc/interrupts中显示的名称,用于标识硬件控制器。在IA-32系统上可能值是“XTPIC”和“IO-APIC”,在AMD64系统上大多数时候也会使用后者。具体依据系统。
 * @irq_startup:    指向一个函数,用于第一次初始化一个IRQ。初始化工作仅限于启用该IRQ。因而,startup函数实际上就是将工作转给enable(默认情况下为enable)
 * @irq_shutdown:    关闭中断 (defaults to ->disable if NULL)
 * @irq_enable:        激活一个IRQ。也就是IRQ从禁用状态到启动状态的转换 (defaults to chip->unmask if NULL)
 * @irq_disable:    与enable相对
 * @irq_ack:        start of a new interrupt
 * @irq_mask:        mask an interrupt source
 * @irq_mask_ack:    ack and mask an interrupt source
 * @irq_unmask:        unmask an interrupt source
 * @irq_eoi:                   end of interrupt
 * @irq_set_affinity:    set the CPU affinity on SMP machines
 * @irq_retrigger:    resend an IRQ to the CPU
 * @irq_set_type:    set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ
 * @irq_set_wake:    enable/disable power-management wake-on of an IRQ
 * @irq_bus_lock:    function to lock access to slow bus (i2c) chips
 * @irq_bus_sync_unlock:function to sync and unlock slow bus (i2c) chips
 * @irq_cpu_online:    configure an interrupt source for a secondary CPU
 * @irq_cpu_offline:    un-configure an interrupt source for a secondary CPU
 * @irq_suspend:                    function called from core code on suspend once per chip
 * @irq_resume:                     function called from core code on resume once per chip
 * @irq_pm_shutdown:         function called from core code on shutdown once per chip
 * @irq_print_chip:                optional to print special chip info in show_interrupts
 * @flags:                                 chip specific flags
 *
 * @release:        release function solely used by UML
 */
struct irq_chip {
	const char	*name;
	unsigned int	(*irq_startup)(struct irq_data *data);
	void		(*irq_shutdown)(struct irq_data *data);
	void		(*irq_enable)(struct irq_data *data);
	void		(*irq_disable)(struct irq_data *data);

	void		(*irq_ack)(struct irq_data *data);
	void		(*irq_mask)(struct irq_data *data);
	void		(*irq_mask_ack)(struct irq_data *data);
	void		(*irq_unmask)(struct irq_data *data);
	void		(*irq_eoi)(struct irq_data *data);

	int		(*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
	int		(*irq_retrigger)(struct irq_data *data);
	int		(*irq_set_type)(struct irq_data *data, unsigned int flow_type);
	int		(*irq_set_wake)(struct irq_data *data, unsigned int on);

	void		(*irq_bus_lock)(struct irq_data *data);
	void		(*irq_bus_sync_unlock)(struct irq_data *data);

	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_print_chip)(struct irq_data *data, struct seq_file *p);

	unsigned long	flags;

	/* Currently used only by UML, might disappear one day.*/
#ifdef CONFIG_IRQ_RELEASE_METHOD
	void		(*release)(unsigned int irq, void *dev_id);
#endif
};
中断控制器芯片实现的一个特定例子,就是AMD64系统上的IO-APIC(代码位置linux/arch/x86/kernel/apic/io_apic.c)。有下列定义给出:

static struct irq_chip ioapic_chip __read_mostly = {
	.name			= "IO-APIC",
	.irq_startup		= startup_ioapic_irq,
	.irq_mask		= mask_ioapic_irq,
	.irq_unmask		= unmask_ioapic_irq,
	.irq_ack		= ack_apic_edge,
	.irq_eoi		= ack_apic_level,
#ifdef CONFIG_SMP
	.irq_set_affinity	= ioapic_set_affinity,
#endif
	.irq_retrigger		= ioapic_retrigger_irq,
};

基于AXI的ARM中断控制器的数据结构的实例化。

static struct irq_chip gic_chip = {
	.name			= "GIC",
	.irq_mask		= gic_mask_irq,
	.irq_unmask		= gic_unmask_irq,
	.irq_eoi		= gic_eoi_irq,
	.irq_set_type		= gic_set_type,
	.irq_retrigger		= gic_retrigger,
#ifdef CONFIG_SMP
	.irq_set_affinity	= gic_set_affinity,
#endif
	.irq_set_wake		= gic_set_wake,
};

处理程序函数用到的数据结构体如下所示,在<linux/include/linux/interrupt.h>中定义:

/**
 * struct irqaction - per interrupt action descriptor
 * @handler:	interrupt handler function
 * @flags:	flags (see IRQF_* above)
 * @name:	name of the device
 * @dev_id:	cookie to identify the device
 * @percpu_dev_id:	cookie to identify the device
 * @next:	pointer to the next irqaction for shared interrupts
 * @irq:	interrupt number
 * @dir:	pointer to the proc/irq/NN/name entry
 * @thread_fn:	interrupt handler function for threaded interrupts
 * @thread:	thread pointer for threaded interrupts
 * @thread_flags:	flags related to @thread
 * @thread_mask:	bitmask for keeping track of @thread activity
 */
struct irqaction {
	irq_handler_t		<span style="background-color: rgb(255, 255, 102);"><span style="color:#FF0000;">handler;</span></span>
	unsigned long		flags;
	void			*dev_id;
	void __percpu		*percpu_dev_id;
	struct irqaction	*next;
	int			irq;
	irq_handler_t		thread_fn;
	struct task_struct	*thread;
	unsigned long		thread_flags;
	unsigned long		thread_mask;
	const char		*name;
	struct proc_dir_entry	*dir;
} ____cacheline_internodealigned_in_smp;

该结构中最重要的成员便是处理程序函数本身,即handler成员,这是一个函数指针,位于结构的起始处。在设备请求一个系统中断,而中断控制器通过引发中断将该请求转发到处理器的时候,将调用该处理程序函数。

name和dev_id唯一地标示一个指针,指向在所有内核数据结构中唯一表示了该设备的数据结构实例,例如网卡的net_device实例。如果几个设备共享一个IRQ,那么IRQ编号自身不能标识该设备,此时,在删除处理程序函数时,将需要上述信息。

flags是一个标志变量,通过位图描述了IRQ(和相关的中断)的一些特性,位图中的各个标志位照例可以通过预定义的常数访问。<interrupt.h>



5 中断绑定——中断亲和力(IRQ Affinity)

在 SMP 体系结构中,我们可以通过调用系统调用和一组相关的宏来设置 CPU 亲和力(CPU affinity),将一个或多个进程绑定到一个或多个处理器上运行。中断在这方面也毫不示弱,也具有相同的特性。中断亲和力是指将一个或多个中断源绑定到特定的 CPU 上运行。中断亲和力最初由 Ingo Molnar 设计并实现。

/proc/irq 目录中,对于已经注册中断处理程序的硬件设备,都会在该目录下存在一个以该中断号命名的目录 IRQ#IRQ# 目录下有一个smp_affinity 文件(SMP 体系结构才有该文件),它是一个 CPU 的位掩码,可以用来设置该中断的亲和力, 默认值为0xffffffff,表明把中断发送到所有的 CPU 上去处理。如果中断控制器不支持IRQ affinity,不能改变此默认值,同时也不能关闭所有的 CPU 位掩码,即不能设置成0x0

我们以网卡(eth1,中断号 44 )为例,在具有 8 个 CPU 的服务器上来设置网卡中断的亲和力(以下数据出自内核源码 Documentation\IRQ-affinity.txt):

[root@moon 44]# cat smp_affinity
ffffffff
[root@moon 44]# echo 0f > smp_affinity
[root@moon 44]# cat smp_affinity
0000000f
[root@moon 44]# ping -f h
PING hell (195.4.7.3): 56 data bytes
...
--- hell ping statistics ---
6029 packets transmitted, 6027 packets received, 0% packet loss
round-trip min/avg/max = 0.1/0.1/0.4 ms
[root@moon 44]# cat /proc/interrupts | grep 44:
 44:   0   1785   1785   1783   1783   1   1   0   IO-APIC-level   eth1
[root@moon 44]# echo f0 > smp_affinity
[root@moon 44]# ping -f h
PING hell (195.4.7.3): 56 data bytes
..
--- hell ping statistics ---
2779 packets transmitted, 2777 packets received, 0% packet loss
round-trip min/avg/max = 0.1/0.5/585.4 ms
[root@moon 44]# cat /proc/interrupts | grep 44:
 44:  1068  1785  1785  1784   1784   1069   1070   1069   IO-APIC-level  eth1
[root@moon 44]#

在上例中,我们首先只允许在 CPU0~3 上处理网卡中断,接着运行 ping 程序,不难发现在 CPU4~7 上并没有对网卡中断进行处理。然后只在 CPU4~7 上对网卡中断进行处理, CPU0~3 不对网卡中断进行任何处理,运行 ping 程序之后,再次查看/proc/interrupts 文件时,不难发现 CPU4~7 上的中断次数明显增加,而 CPU0~3 上的中断次数没有太大的变化。

在探讨中断亲和力的实现原理之前,我们首先来了解 I/O APIC 中的组成。

I/O APIC 由一组 24 条 IRQ 线,一张 24 项的中断重定向表(Interrupt Redirection Table),可编程寄存器,以及通过 APIC 总线发送和接收 APIC 信息的一个信息单元组成。其中与中断亲和力息息相关的是中断重定向表,中断重定向表表中的每一项都可以被单独编程以指明中断向量和优先级、目标处理器及选择处理器的方式

通过表 2,不难发现 8259A 和 APIC 中断控制器最大不同点在于 hw_interrupt_type 类型变量的最后一项。对于 8259A 类型,set_affinity 被置为NULL,而对于 SMP 的 APIC 类型,set_affinity 被赋值为set_ioapic_affinity

在系统初始化期间,对于 SMP 体系结构,将会调用 setup_IO_APIC_irqs() 函数来初始化 I/O APIC 芯片,芯片中的中断重定向表的 24 项被填充。在系统启动期间,所有的 CPU 都执行setup_local_APIC() 函数,完成本地的 APIC 初始化。当有中断被触发时,将相应的中断重定向表中的值转换成一条消息,然后,通过 APIC 总线把消息发送给一个或多个本地 APIC 单元,这样,中断就能立即被传递给一个特定的 CPU,或一组 CPU,或所有的 CPU,从而来实现中断亲和力。

当我们通过 cat 命令将 CPU 掩码写进 smp_affinity 文件时,此时的调用路线图为:write() ->sys_write() ->vfs_write() ->proc_file_write() ->irq_affinity_write_proc() ->set_affinity() ->set_ioapic_affinity()->set_ioapic_affinity_irq()->io_apic_write();其中在调用set_ioapic_affinity_irq() 函数时,以中断号和 CPU 掩码作为参数,接着继续调用io_apic_write(),修改相应的中断重定向中的值,来完成中断亲和力的设置。当执行 ping 命令时,网卡中断被触发,产生了一个中断信号,多 APIC 系统根据中断重定向表中的值,依照仲裁机制,选择 CPU0~3 中的某一个 CPU,并将该信号传递给相应的本地 APIC,本地 APIC 又中断它的 CPU,整个事件不通报给其他所有的 CPU。

6 新特性展望——中断线程化(Interrupt Threads)

在嵌入式领域,业界对 Linux 实时性的呼声越来越高,对中断进行改造势在必行。在 Linux 中,中断具有最高的优先级。不论在任何时刻,只要产生中断事件,内核将立即执行相应的中断处理程序,等到所有挂起的中断和软中断处理完毕后才能执行正常的任务,因此有可能造成实时任务得不到及时的处理。中断线程化之后,中断将作为内核线程运行而且被赋予不同的实时优先级,实时任务可以有比中断线程更高的优先级。这样,具有最高优先级的实时任务就能得到优先处理,即使在严重负载下仍有实时性保证。

目前较新的 Linux 2.6.17 还不支持中断线程化。但由 Ingo Molnar 设计并实现的实时补丁,实现了中断线程化。最新的下载地址为:

http://people.redhat.com/~mingo/realtime-preempt/patch-2.6.17-rt9

下面将对中断线程化进行简要分析。

在初始化阶段,中断线程化的中断初始化与常规中断初始化大体上相同,在 start_kernel() 函数中都调用了 trap_init()init_IRQ() 两个函数来初始化irq_desc_t 结构体,不同点主要体现在内核初始化创建init 线程时,中断线程化的中断在init() 函数中还将调用init_hardirqs(kernel/irq/manage.c(已经打过上文提到的补丁)),来为每一个 IRQ 创建一个内核线程,最高实时优先级为 50,依次类推直到 25,因此任何 IRQ 线程的最低实时优先级为 25。

void __init init_hardirqs(void)
{
……
	for (i = 0; i < NR_IRQS; i++) {
		irq_desc_t *desc = irq_desc + i;
		if (desc->action && !(desc->status & IRQ_NODELAY))
			desc->thread = kthread_create(do_irqd, desc, "IRQ %d", irq);
    ……
	}
}
static int do_irqd(void * __desc)
{
    ……
	/*
	 * Scale irq thread priorities from prio 50 to prio 25
	 */
	param.sched_priority = curr_irq_prio;
	if (param.sched_priority > 25)
		curr_irq_prio = param.sched_priority - 1;
   ……
}

如果某个中断号状态位中的 IRQ_NODELAY 被置位,那么该中断不能被线程化。

在中断处理阶段,两者之间的异同点主要体现在:两者相同的部分是当发生中断时,CPU 将调用 do_IRQ() 函数来处理相应的中断,do_IRQ() 在做了必要的相关处理之后调用__do_IRQ()。两者最大的不同点体现在__do_IRQ() 函数中,在该函数中,将判断该中断是否已经被线程化(如果中断描述符的状态字段不包含IRQ_NODELAY 标志,则说明该中断被线程化了),对于没有线程化的中断,将直接调用handle_IRQ_event() 函数来处理。

fastcall notrace unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
{
……
	if (redirect_hardirq(desc))
		goto out_no_end;
……
action_ret = handle_IRQ_event(irq, regs, action);
……
}
int redirect_hardirq(struct irq_desc *desc)
{
……
	if (!hardirq_preemption || (desc->status & IRQ_NODELAY) || !desc->thread)
		return 0;
……
	if (desc->thread && desc->thread->state != TASK_RUNNING)
		wake_up_process(desc->thread);
……
}

对于已经线程化的情况,调用 wake_up_process() 函数唤醒中断处理线程,并开始运行,内核线程将调用 do_hardirq() 来处理相应的中断,该函数将判断是否有中断需要被处理,如果有就调用handle_IRQ_event() 来处理。handle_IRQ_event() 将直接调用相应的中断处理函数来完成中断处理。

不难看出,不管是线程化还是非线程化的中断,最终都会执行 handle_IRQ_event() 函数来调用相应的中断处理函数,只是线程化的中断处理函数是在内核线程中执行的。

并不是所有的中断都可以被线程化,比如时钟中断,主要用来维护系统时间以及定时器等,其中定时器是操作系统的脉搏,一旦被线程化,就有可能被挂起,这样后果将不堪设想,所以不应当被线程化。如果某个中断需要被实时处理,它可以像时钟中断那样,用SA_NODELAY 标志来声明自己非线程化,例如:

static struct irqaction irq0 = {
	timer_interrupt, SA_INTERRUPT | SA_NODELAY, CPU_MASK_NONE, "timer", NULL, NULL
};

其中,SA_NODELAYIRQ_NODELAY 之间的转换,是在 setup_irq() 函数中完成的。

7 中断负载均衡—SMP体系结构下的中断

中断负载均衡的实现主要封装在 arch\ arch\i386\kernel\io-apic.c 文件中。如果在编译内核时配置了 CONFIG_IRQBALANCE 选项,那么 SMP 体系结构中的中断负载均衡将以模块的形式存在于内核中。

late_initcall(balanced_irq_init);
#define late_initcall(fn)		module_init(fn)  //include\linux\init.h

balanced_irq_init() 函数中,将创建一个内核线程来负责中断负载均衡:

static int __init balanced_irq_init(void)
{   ……
	printk(KERN_INFO "Starting balanced_irq\n");
	if (kernel_thread(balanced_irq, NULL, CLONE_KERNEL) >= 0) 
		return 0;
	else 
		printk(KERN_ERR "balanced_irq_init: failed to spawn balanced_irq");
    ……
}

balanced_irq() 函数中,每隔 5HZ=5s 的时间,将调用一次 do_irq_balance() 函数,进行中断的迁徙。将重负载 CPU 上的中断迁移到较空闲的CPU上进行处理。

8 总结

随着中断亲和力和中断线程化的相继实现,Linux 内核在 SMP 和实时性能方面的表现越来越让人满意,完全有理由相信,在不久的将来,中断线程化将被合并到基线版本中。本文对中断线程化的分析只是起一个抛砖引玉的作用,当新特性发布时,不至于让人感到迷茫。

  • 注释 1:轮询也不是毫无用处,比如NAPI,就是轮询与中断相结合的经典案例。
9 参考资料:
http://www.ibm.com/developerworks/cn/linux/l-cn-linuxkernelint/


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值