-
2021-11-24 14:54:16
一、中断处理程序是什么?
中斷處理程序是管理硬件驅動的驅動程序的組成部分,如果設備使用中斷,那麼相應的驅動程序就註冊一箇中斷處理程序。二、驱动中如何注册中断处理程序?
在驅動程序中,通常使用request_irq()來註冊中斷處理程序。該函數在文件<include/linux/interrupt.h>中聲明:
extern int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev);
三、中断发生了,硬件+操作系统如何处理中断?1、device通过到中断控制器引脚电平变化通知中断控制器device中断产生了。
2、中断控制器根据自身电平变化的引脚产生中断号。
3、中断控制器通过到CPU中断引脚电平变化通知CPU中断产生,并通过其他预定义引脚将
更多相关内容 -
Linux内核中断系统处理机制-详细分析
2018-08-23 23:09:24Linux内核中断 一、中断概述 中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再...日期 内核版本 架构 作者 内容 2018-8-23 Linux-2.6.32 X86
Bystander Linux内核中断 -
一、中断概述
中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。
1.1中断类型
同步中断由CPU本身产生,又称为内部中断。这里同步是指中断请求信号与代码指令之间的同步执行,在一条指令执行完毕后,CPU才能进行中断,不能在执行期间。所以也称为异常(exception)。
异步中断是由外部硬件设备产生,又称为外部中断,与同步中断相反,异步中断可在任何时间产生,包括指令执行期间,所以也被称为中断(interrupt)。
异常又可分为可屏蔽中断(Maskable interrupt)和非屏蔽中断(Nomaskable interrupt)。而中断可分为故障(fault)、陷阱(trap)、终止(abort)三类。
从广义上讲,中断又可分为四类:中断、故障、陷阱、终止。这些类别之间的异同点请参考 表 1。
表 1:中断类别及其行为
类别
原因
异步/同步
返回行为
中断
来自I/O设备的信号
异步
总是返回到下一条指令
陷阱
有意的异常
同步
总是返回到下一条指令
故障
潜在可恢复的错误
同步
返回到当前指令
终止
不可恢复的错误
同步
不会返回
有些参考资料中按照中断来源进行分类,如下图所示(个人建议不采用这种方式):
图1-1
1.2区分中断号与中断向量
I/O设备把中断信号发送给中断控制器(8259A)时与之相关联的是一个中断号,当中断控制器把中断信号发送给CPU时与之关联的是一个中断向量。换个角度分析就是中断号是从中断控制器层面划分,中断向量是从CPU层面划分,所以中断号与中断向量之间存在一对一映射关系。在Intel X86中最大支持256种中断,从0到255开始编号,这个8位的编号就是中断向量。其中将0到31保留用于异常处理和不可屏蔽中断。
-
二、中断数据处理结构
Linux内核中处理中断主要有三个数据结构,irq_desc,irq_chip和irqaction。
在\include\linux\ irq.h中定义了
1)irq_desc用于描述IRQ线的属性与状态,被称为中断描述符。
/** * struct irq_desc - interrupt descriptor * @irq: interrupt number for this descriptor * @timer_rand_state: pointer to timer rand state struct * @kstat_irqs: irq stats per cpu * @irq_2_iommu: iommu with this irq * @handle_irq: highlevel irq-events handler [if NULL, __do_IRQ()] * @chip: low level interrupt hardware access * @msi_desc: MSI descriptor * @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 * @action: the irq action chain * @status: status information * @depth: disable-depth, for nested irq_disable() calls * @wake_depth: enable depth, for multiple 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: IRQ affinity on SMP * @node: node index useful for balancing * @pending_mask: pending rebalanced interrupts * @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{ unsigned int irq; struct timer_rand_state *timer_rand_state; unsigned int *kstat_irqs; #ifdef CONFIG_INTR_REMAP struct irq_2_iommu *irq_2_iommu; #endif irq_flow_handler_t handle_irq; struct irq_chip *chip; struct msi_desc *msi_desc; void *handler_data; void *chip_data; struct irqaction *action; /* IRQ action list */ unsigned int status; /* IRQ status */ 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; spinlock_t lock; #ifdef CONFIG_SMP cpumask_var_t affinity; unsigned int node; #ifdef CONFIG_GENERIC_PENDING_IRQ cpumask_var_t pending_mask; #endif #endif atomic_t threads_active; wait_queue_head_t wait_for_threads; #ifdef CONFIG_PROC_FS struct proc_dir_entry *dir; #endif const char *name; }
2)irq_chip用于描述不同类型的中断控制器。
/** * struct irq_chip - hardware interrupt chip descriptor * * @name: name for /proc/interrupts * @startup: start up the interrupt (defaults to ->enable if NULL) * @shutdown: shut down the interrupt (defaults to ->disable if NULL) * @enable: enable the interrupt (defaults to chip->unmask if NULL) * @disable: disable the interrupt (defaults to chip->mask if NULL) * @ack: start of a new interrupt * @mask: mask an interrupt source * @mask_ack: ack and mask an interrupt source * @unmask: unmask an interrupt source * @eoi: end of interrupt - chip level * @end: end of interrupt - flow level * @set_affinity: set the CPU affinity on SMP machines * @retrigger: resend an IRQ to the CPU * @set_type: set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ * @set_wake: enable/disable power-management wake-on of an IRQ * * @bus_lock: function to lock access to slow bus (i2c) chips * @bus_sync_unlock: function to sync and unlock slow bus (i2c) chips * * @release: release function solely used by UML * @typename: obsoleted by name, kept as migration helper */ struct irq_chip { const char *name; unsigned int (*startup)(unsigned int irq); void (*shutdown)(unsigned int irq); void (*enable)(unsigned int irq); void (*disable)(unsigned int irq); void (*ack)(unsigned int irq); void (*mask)(unsigned int irq); void (*mask_ack)(unsigned int irq); void (*unmask)(unsigned int irq); void (*eoi)(unsigned int irq); void (*end)(unsigned int irq); int (*set_affinity)(unsigned int irq, const struct cpumask *dest); int (*retrigger)(unsigned int irq); int (*set_type)(unsigned int irq, unsigned int flow_type); int (*set_wake)(unsigned int irq, unsigned int on); void (*bus_lock)(unsigned int irq); void (*bus_sync_unlock)(unsigned int irq); /* Currently used only by UML, might disappear one day.*/ #ifdef CONFIG_IRQ_RELEASE_METHOD void (*release)(unsigned int irq, void *dev_id); #endif /* * For compatibility, ->typename is copied into ->name. * Will disappear. */ const char *typename; }
在\include\linux\ interrupt.h中定义了 irqaction用来描述特定设备所产生的中断描述符。
/** * 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 * @next: pointer to the next irqaction for shared interrupts * @irq: interrupt number * @dir: pointer to the proc/irq/NN/name entry * @thread_fn: interupt handler function for threaded interrupts * @thread: thread pointer for threaded interrupts * @thread_flags: flags related to @thread */ struct irqaction { irq_handler_t handler; unsigned long flags; const char *name; void *dev_id; struct irqaction *next; int irq; struct proc_dir_entry *dir; irq_handler_t thread_fn; struct task_struct *thread; unsigned long thread_flags; };
-
三、Linux中断机制
Linux中断机制由三部分组成:
- 中断子系统初始化:内核自身初始化过程中对中断处理机制初始化,例如中断的数据结构以及中断请求等。
- 中断或异常处理:中断整体处理过程。
- 中断API:为设备驱动提供API,例如注册,释放和激活等。
3.1中断子系统初始化
3.1.1中断描述符表(IDT)初始化
中断描述符表初始化需要经过两个过程:
- 第一个过程在内核引导过程。由两个步骤组成,首先给分配IDT分配2KB空间(256中断向量,每个向量由8bit组成)并初始化;然后把IDT起始地址存储到IDTR寄存器中。
- 第二个过程内核在初始化自身的start_kernal函数中使用trap_init初始化系统保留中断向量,使用init_IRQ完成其余中断向量初始化。
3.1.2中断请求队列初始化
init_IRQ调用pre_intr_init_hook,进而最终调用init_ISA_irqs初始化中断控制器以及每个IRQ线的中断请求队列。
3.2中断或异常处理
中断处理过程:设备产生中断,并通过中断线将中断信号送往中断控制器,如果中断没有被屏蔽则会到达CPU的INTR引脚,CPU立即停止当前工作,根据获得中断向量号从IDT中找出门描述符,并执行相关中断程序。
异常处理过程:异常是由CPU内部发生所以不会通过中断控制器,CPU直接根据中断向量号从IDT中找出门描述符,并执行相关中断程序。
图3-1
中断控制器处理主要有5个步骤:1.中断请求 2.中断响应 3.优先级比较 4.提交中断向量 5.中断结束。这里不再赘述5个步骤的具体流程。
CPU处理流程主要有6个步骤:1.确定中断或异常的中断向量 2.通过IDTR寄存器找到IDT 3.特权检查 4.特权级发生变化,进行堆栈切换 5.如果是异常将异常代码压入堆栈,如果是中断则关闭可屏蔽中断 6.进入中断或异常服务程序执行。这里不再赘述6个步骤的具体流程。
3.3中断API
内核提供的API主要用于驱动的开发。
注册IRQ:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
释放IRQ:
void free_irq(unsigned int, void *);
注:IRQ线资源非常宝贵,我们在使用时必须先注册,不使用时必须释放IRQ资源。
激活当前CPU中断:
local_irq_enable();
禁止当前CPU中断:
local_irq_disable();
激活指定中断线:
void enable_irq(unsigned int irq);
禁止指定中断线:
void disable_irq(unsigned int irq);
禁止指定中断线:
void disable_irq_nosync(unsigned int irq);
注:此函数调用irq_chip中disable禁止指定中断线,所以不会保证中断线上执行的中断服务程序已经退出。
3.4中断机制划分
由于中断会打断内核中进程的正常调度运行,所以要求中断服务程序尽可能的短小精悍;但是在实际系统中,当中断到来时,要完成工作往往进行大量的耗时处理。因此期望让中断处理程序运行得快,并想让它完成的工作量多,这两个目标相互制约,诞生——顶/底半部机制。
中断处理程序是顶半部——接受中断,它就立即开始执行,但只有做严格时限的工作。能够被允许稍后完成的工作会推迟到底半部去,此后,在合适的时机,底半部会被开终端执行。顶半部简单快速,执行时禁止一些或者全部中断。
底半部稍后执行,而且执行期间可以响应所有的中断。这种设计可以使系统处于中断屏蔽状态的时间尽可能的短,以此来提高系统的响应能力。顶半部只有中断处理程序机制,而底半部的实现有软中断,tasklet和工作队列实现。
图3-2 注:登记中断,将底半部处理程序挂到该设备的低半部执行队列中。
3.4.1顶/底半部划分原则:
1) 如果一个任务对时间非常敏感,将其放在顶半部中执行;
2) 如果一个任务和硬件有关,将其放在顶半部中执行;
3) 如果一个任务要保证不被其他中断打断,将其放在顶半部中执行;
4) 其他所有任务,考虑放置在底半部执行。
3.4.2底半部实现机制
图3-3
软中断:
软中断作为下半部机制的代表,是随着SMP(share memory processor)的出现应运而生的,它也是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。软中断一般是“可延迟函数”的总称,有时候也包括了tasklet(请读者在遇到的时候根据上下文推断是否包含tasklet)。它的出现就是因为要满足上面所提出的上半部和下半部的区别,使得对时间不敏感的任务延后执行,软中断执行中断处理程序留给它去完成的剩余任务,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。它的特性包括:
a)产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。
b)可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。
内核中定义了几种软中断的用途:
enum { HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ NR_SOFTIRQS };
Tasklet:
tasklet是通过软中断实现的,所以它本身也是软中断。
软中断用轮询的方式处理。假如正好是最后一种中断,则必须循环完所有的中断类型,才能最终执行对应的处理函数。显然当年开发人员为了保证轮询的效率,于是限制中断个数为32个。
为了提高中断处理数量,顺道改进处理效率,于是产生了tasklet机制。
Tasklet采用无差别的队列机制,有中断时才执行,免去了循环查表之苦。Tasklet作为一种新机制,显然可以承担更多的优点。正好这时候SMP越来越火了,因此又在tasklet中加入了SMP机制,保证同种中断只能在一个cpu上执行。在软中断时代,显然没有这种考虑。因此同一种软中断可以在两个cpu上同时执行,很可能造成冲突。
总结下tasklet的优点:
(1)无类型数量限制;
(2)效率高,无需循环查表;
(3)支持SMP机制;
它的特性如下:
1)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
2)多个不同类型的tasklet可以并行在多个CPU上。
3)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
工作队列:
上面我们介绍的可延迟函数运行在中断上下文中,于是导致了一些问题,说明它们不可挂起,也就是说软中断不能睡眠、不能阻塞,原因是由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。而且由于是串行执行,因此只要有一个处理时间较长,则会导致其他中断响应的延迟。为了完成这些不可能完成的任务,于是出现了工作队列,它能够在不同的进程间切换,以完成不同的工作。
工作队列能运行在进程上下文,它将工作给一个内核线程,作为中断守护线程来使用。多个中断可以放在一个线程中,也可以每个中断分配一个线程。我们用结构体workqueue_struct表示工作者线程,工作者线程是用内核线程实现的。而工作者线程是如何执行被推后的工作——有这样一个链表,它由结构体work_struct组成,而这个work_struct则描述了一个工作,一旦这个工作被执行完,相应的work_struct对象就从链表上移去,当链表上不再有对象时,工作者线程就会继续休眠。因为工作队列是线程,所以我们可以使用所有可以在线程中使用的方法。
如何选择下半部机制:
- 软中断和tasklet运行在中断上下文,工作队列运行在进程上下文。如果需要休眠则选择工作队列,否则选择tasklet;如果对性能要求较高则选择软中断。
- 从易用性考虑,首选工作队列,然后tasklet,最后是软中断,因为软中断需要静态创建。
- 从代码安全考虑,如果对底半部代码保护不够安全,则选择tasklet,因为相对软中断,tasklet对锁要求低,上面也简述它们工作方式以及运用场景。
四、多处理器系统中断相关概念
4.1处理器间中断
在多处理器系统中,操作系统需要在多个处理器中协调操作,所以需要处理器中断(Inter-Processor-Interrupt,IPI)实现,IPI是一种特殊硬件中断,由CPU送出,其他CPU接收,处理CPU之间通信和同步操作。以下是x86中SMP定义的IPI,中断向量号用十六进制表示:
SPURIOUS_APIC_VECTOR 0xff ERROR_APIC_VECTOR 0xfe RESCHEDULE_VECTOR 0xfd CALL_FUNCTION_VECTOR 0xfc CALL_FUNCTION_SINGLE_VECTOR 0xfb THERMAL_APIC_VECTOR 0xfa THRESHOLD_APIC_VECTOR 0xf9 REBOOT_VECTOR 0xf8 INVALIDATE_TLB_VECTOR_END 0xf7 INVALIDATE_TLB_VECTOR_START 0xf0
4.2中断亲和力
将一个或多个中断服务程序绑定到特定的CPU上处理,这就是中断亲和力(SMP IRQ affinity)。我们可以使用中断亲和力来均衡各个CPU的负载,提高系统处理能力。
以上便是本人对Linux中断的理解,若有纰漏欢迎指正!
-
-
Linux中断机制(转)
2019-02-13 15:22:33... Linux 中断机制 中断控制器 中断描述符 中断数据结构 中断的初始化 内核接口 中断处理过程 CPU 的中断处理流程 保存中断信息 处理中断 从中断中返回 编写中断处理...本文转自博客:https://www.cnblogs.com/sky-heaven/p/5640961.html
目录
- 为什么要有中断
- 中断的作用
- 中断的处理原则
- Linux 中断机制
- 中断控制器
- 中断描述符
- 中断数据结构
- 中断的初始化
- 内核接口
- 中断处理过程
- CPU 的中断处理流程
- 保存中断信息
- 处理中断
- 从中断中返回
- 编写中断处理程序
- 软中断、tasklet与工作队列
- 上半部与下半部
- 软中断
- tasklet
- 工作队列
1 为什么要有中断
1.1 中断的作用
处理器的运算速度一般要比外部硬件快很多。以读取硬盘为例,如果是简单的顺序执行,CPU 必须等待很长时间,不停地轮询硬盘是否读取完毕,这会浪费很多 CPU 时间。中断提供了这样一种机制,使得读取硬盘这样的操作可以交给硬件来完成,CPU 挂起当前进程,将控制权转交给其他进程,待硬件处理完毕后通知 CPU,操作系统把当前进程设为活动的,从而允许该进程继续执行,处理读取硬盘的结果。
另一方面,有些事件不是程序本身可预见的,需要硬件以某种方式告诉进程。例如时钟中断为定时器提供了基础,如果没有时钟中断,程序只能每执行几条指令就检查一下当前系统时间,这在效率上是不可接受的。
从广义上说,中断是改变 CPU 处理指令顺序的硬件信号。分为两类:
- 异步的:在程序执行的任何时刻都可能产生,如时钟中断
- 同步的:在特殊或错误指令执行时由 CPU 控制单元产生,称为异常
1.2 中断的处理原则
中断处理的基本原则就是“快”。如果反应慢了,数据可能丢失或被覆盖。例如键盘按键中断,所按下的键的 keycode 放在 KBDR 寄存器中,如果在中断被处理之前用户又按了一个键,则 KBDR 的值被新按下的键的 keycode 覆盖,早先按下的键对应的数据就丢失了。
当一个中断信号到达时,CPU 必须停止当前所做的事,转而处理中断信号。为了尽快处理中断并为接收下一个中断做好准备,内核应尽快处理完一个中断,将更多的处理向后推迟。
为达到“快”这一目标,内核允许不同类型的中断嵌套发生,即在中断处理的临界区之外可以接受新的中断。这样,更多的 I/O 设备将处于忙状态。
2 Linux 中断机制
2.1 中断控制器
中断控制器是连接设备和 CPU 的桥梁,一个设备产生中断后,需要经过中断控制器的转发,才能最终到达 CPU。时代发展至今,中断控制器经历了 PIC(Programmable Interrupt Controller,可编程中断控制器) 和 APIC (Advanced Programmable Interrupt Controller,高级可编程中断控制器) 两个阶段。前者在 UP(Uni-processor,单处理器) 上威震四方,随着 SMP (Symmetric Multiple Processor,对称多处理器) 的流行,APIC 已广为流行并将最终取代 PIC。
8259A (PIC) 管脚图
上图中的管脚说明:
- IR0~IR7 (Interrupt Request0~7,用于连接设备)
- INT (连接 CPU,当有中断请求时,拉高该管脚以通知 CPU 中断的到来)
- INTA (连接 CPU,CPU 通过该管脚应答中断请求,并通知 PIC 提交中断的 vector 到数据线)
- CS (片选,用于将两个 8259A 串联成可连接 15 个设备的 PIC)
8259A 中的寄存器:
- ICW: Initialization Command Word,初始化命令寄存器,用于初始化 8259A
- OCW: Operation Command Word,操作命令字,用于控制 8259A
- IRR: Interrupt Request Register,中断请求寄存器,共 8bit,对应 IR0~IR7 八个中断管脚。当某个管脚的中断请求到来后,若该管脚没有被屏蔽,IRR 中对应的 bit 被置1。表示 PIC 已经收到设备的中断请求,但还未提交给 CPU。
- ISR: In Service Register,服务中寄存器,共 8bit,每 bit 意义同上。当 IRR 中的某个中断请求被发送给 CPU 后,ISR 中对应的 bit 被置1。表示中断已发送给 CPU,但 CPU 还未处理完。
- IMR: Interrupt Mask Register,中断屏蔽寄存器,共 8bit,每 bit 意义同上。用于屏蔽中断。当某 bit 置1时,对应的中断管脚被屏蔽。
arch/x86/kernel/i8259_32.c 中通过位运算来开启和关闭中断。
63 void disable_8259A_irq(unsigned int irq) 64 { 65 unsigned int mask = 1 << irq; 66 unsigned long flags; 67 68 spin_lock_irqsave(&i8259A_lock, flags); // 用 spinlock 锁住 69 cached_irq_mask |= mask; // 将 IRQ 的相应位置1,屏蔽中断 70 if (irq & 8) 71 outb(cached_slave_mask, PIC_SLAVE_IMR); // IR2 管脚负责 8259A 的级联(见下图),为0时使用主片,为1时使用从片 72 else 73 outb(cached_master_mask, PIC_MASTER_IMR); 74 spin_unlock_irqrestore(&i8259A_lock, flags); // 解开自旋锁 75 }
77 void enable_8259A_irq(unsigned int irq) 78 { 79 unsigned int mask = ~(1 << irq); 80 unsigned long flags; 81 82 spin_lock_irqsave(&i8259A_lock, flags); // 用 spinlock 锁住 83 cached_irq_mask &= mask; // 将 IRQ 的相应位置0,开启中断 84 if (irq & 8) 85 outb(cached_slave_mask, PIC_SLAVE_IMR); // IR2 管脚负责 8259A 的级联(见下图),为0时使用主片,为1时使用从片 86 else 87 outb(cached_master_mask, PIC_MASTER_IMR); 88 spin_unlock_irqrestore(&i8259A_lock, flags); // 解开自旋锁 89 }
PIC 的每个管脚具有优先级,连接号码较小的设备具有较高的中断优先级。
在 PIC 默认的 Full Nested 模式下,通过 PIC 发起中断的流程如下:
- 一个或多个 IR 管脚上产生电平信号,若对应的中断没有被屏蔽,IRR 中相应的 bit 被置1。
- PIC 拉高 INT 管脚通知 CPU 中断发生。
- CPU 通过 INTA 管脚应答 PIC,表示中断请求收到。
- PIC 收到 INTA 应答后,将 IRR 中具有最高优先级的 bit 清零,并设置 ISR 中对应的 bit。
- CPU 通过 INTA 管脚第二次发出脉冲,PIC 收到后计算最高优先级中断的 vector,并将它提交到数据线上。
- 等待 CPU 写 EOI (End of Interrupt)。收到 EOI 后,ISR 中最高优先级的 bit 被清零。如果 PIC 处于 AEOI 模式,当第二个 INTA 脉冲收到后,ISR 中最高优先级的 bit 自动清零。
PIC 还有优先级轮转模式,即 PIC 在服务完一个管脚之后将其优先级临时降低,并升高未服务管脚的优先级,以实现类似轮询的模式,避免一个管脚持续发出中断导致其他设备“饿死”。
下图是一个典型的 PIC 中断分配,管脚基本上都被古董级设备占据了。
arch/x86/kernel/i8259_32.c 中 8259A 引脚的分配(function init_8259A)
292 outb_pic(0x11, PIC_MASTER_CMD); /* ICW1: select 8259A-1 init */ 293 outb_pic(0x20 + 0, PIC_MASTER_IMR); /* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */ 294 outb_pic(1U << PIC_CASCADE_IR, PIC_MASTER_IMR); /* 8259A-1 (the master) has a slave on IR2 */ 295 if (auto_eoi) /* master does Auto EOI */ 296 outb_pic(MASTER_ICW4_DEFAULT | PIC_ICW4_AEOI, PIC_MASTER_IMR); 297 else /* master expects normal EOI */ 298 outb_pic(MASTER_ICW4_DEFAULT, PIC_MASTER_IMR); 299 300 outb_pic(0x11, PIC_SLAVE_CMD); /* ICW1: select 8259A-2 init */ 301 outb_pic(0x20 + 8, PIC_SLAVE_IMR); /* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */ 302 outb_pic(PIC_CASCADE_IR, PIC_SLAVE_IMR); /* 8259A-2 is a slave on master's IR2 */ 303 outb_pic(SLAVE_ICW4_DEFAULT, PIC_SLAVE_IMR); /* (slave's support for AEOI in flat mode is to be investigated) */
从上图可见,PIC 能接的设备数量实在太少了,而且不支持多处理器。
为了使用 8259A 级联连接较多的设备,可以采用两种方式:
- IRQ 共享:中断处理程序执行多个中断服务程序(ISR),每个 ISR 是一个与共享 IRQ 线相关的函数。
IRQ 共享需要满足两个条件:
- 每个 ISR 都愿意共享 IRQ,即 request_irq() 时指定了 IRQF_SHARED
- 所有 ISR 具有相同的触发条件(电平触发或边沿触发、高低电平或上下边沿)
-
IRQ 动态分配:在可能的最后时刻,才把 IRQ 线分配给一个设备。
当然,APIC 是现代的解决方案。即使是 APIC,也需要使用 IRQ 共享。
I/O APIC 的组成为:一组 24 条 IRQ 线,一张 24 项的中断重定向表,可编程寄存器,通过 APIC 总线发送和接收 APIC 信息的一个信息单元。
与 8259A 不同,中断优先级不与引脚号相关联,中断重定向表中的每一项都可以被单独编程以指明中断向量和优先级、目标处理器和选择处理器的方式。
来自外部硬件设备的中断以两种方式在可用 CPU 之间分发:
- 静态分发
- 动态分发
2.2 中断描述符
Intel 提供了三种类型的中断描述符:任务门、中断门及陷阱门描述符。
Linux 使用与 Intel 稍有不同的分类,把中断描述符分为五类:
- 中断门(interrupt gate):用户态的进程不能访问Intel中断门(门的DPL字段为0)。所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态。
set_intr_gate(n,addr)
上述系统调用在 IDT 的第 n 个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为中断处理程序的地址 addr,DPL 字段设置为0。
- 系统门(system gate):用户态的进程可以访问Intel陷阱门(门的DPL字段为3)。通过系统门来激活三个Linux异常处理程序,它们的向量是4,5及128,因此,在用户态下,可以发布into、bound及int $0x80三条汇编语言指令。
set_system_gate(n,addr)
- 系统中断门(system interrupt gate):能够被用户态进程访问的Intel中断门(门的DPL字段为3)。与向量3相关的异常处理程序是由系统中断门激活的,因此,在用户态可以使用汇编语言指令int3。
set_system_intr_gate(n,addr)
- 陷阱门(trap gate):用户态的进程不能访问的一个Intel陷阱门(门的DPL字段为0)。大部分Linux异常处理程序都通过陷阱门来激活。
set_trap_gate(n,addr)
- 任务门(task gate):不能被用户态进程访问的Intel任务门(门的DPL字段为0)。Linux对“Double fault”异常的处理程序是由任务门激活的。
set_task_gate(n,gdt)
门中的段选择符中存放一个TSS的全局描述符表的指针,该TSS中包含要被激活的函数。
在 IDT 中插入门的函数定义在 include/asm-x86/desc.h 中。
这些函数以不同的参数调用内部函数 _set_gate()。_set_gate 调用两个内部函数
- pack_gate: 设置门的数据结构:中断号、门类型、处理函数地址、DPL、ist、目录段寄存器
38 static inline void pack_gate(gate_desc *gate, unsigned type, unsigned long func, 39 unsigned dpl, unsigned ist, unsigned seg) 40 { 41 gate->offset_low = PTR_LOW(func); // 处理函数低内存偏移 42 gate->segment = __KERNEL_CS; // 内核代码段 43 gate->ist = ist; // ist 44 gate->p = 1; 45 gate->dpl = dpl; // DPL 46 gate->zero0 = 0; 47 gate->zero1 = 0; 48 gate->type = type; // 门类型(宏定义) 49 gate->offset_middle = PTR_MIDDLE(func); // 处理函数中内存偏移 50 gate->offset_high = PTR_HIGH(func); // 处理函数高内存偏移 51 }
- write_idt_entry: 宏定义为 native_write_idt_entry,用 memcpy 将设置好的门写入 IDT。
2.3 中断数据结构
在 Linux 中,中断描述符的核心数据结构是 include/linux/irq.h 中的 irq_desc 结构体。每个 irq_desc 实例描述一条中断线。
153 struct irq_desc { 154 irq_flow_handler_t handle_irq; // 中断事件处理函数,下面会介绍 155 struct irq_chip *chip; // irq_chip 指针,描述了一些硬件信息,下面会介绍 156 struct msi_desc *msi_desc; 157 void *handler_data; // chip 中使用的数据 158 void *chip_data; // chip 中使用的数据 159 struct irqaction *action; /* IRQ action list */ // irqaction 指针,下面会介绍 160 unsigned int status; /* IRQ status */ // IRQ 线状态标志 161 162 unsigned int depth; /* nested irq disables */ 163 unsigned int wake_depth; /* nested wake enables */ 164 unsigned int irq_count; /* For detecting broken IRQs */ // 中断计数 165 unsigned int irqs_unhandled; // 无法处理的中断计数 166 unsigned long last_unhandled; /* Aging timer for unhandled count */ 167 spinlock_t lock; // 自旋锁 168 #ifdef CONFIG_SMP 169 cpumask_t affinity; // 多处理器中的处理器亲和性 170 unsigned int cpu; 171 #endif 172 #if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE) 173 cpumask_t pending_mask; 174 #endif 175 #ifdef CONFIG_PROC_FS 176 struct proc_dir_entry *dir; // 在 /proc 文件系统中的目录 177 #endif 178 const char *name; // 中断名称 179 } ____cacheline_internodealigned_in_smp;
irq_desc 在 kernel/irq/handle.c 中被使用,此文件是 IRQ 机制的核心入口,描述了各中断线。
50 struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = { 51 [0 ... NR_IRQS-1] = { 52 .status = IRQ_DISABLED, // 默认屏蔽中断 53 .chip = &no_irq_chip, // 没有与 chip 相关联 // 未知(坏的)IRQ 处理程序,输出 IRQ 信息供调试,更新 CPU IRQ 次数计数器,回应 IRQ。 54 .handle_irq = handle_bad_irq, 55 .depth = 1, // 默认是第一层(没有嵌套中断) 56 .lock = __SPIN_LOCK_UNLOCKED(irq_desc->lock), // 还没有自旋锁 57 #ifdef CONFIG_SMP 58 .affinity = CPU_MASK_ALL // 处理器亲和性未定义 59 #endif 60 } 61 };
下面介绍 irq_desc 中的主要数据成员。
handle_irq
handle_irq 是函数指针,指向 kernel/irq/chip.c 中的中断事件处理函数。
- handle_simple_irq
- handle_level_irq
- handle_fasteoi_irq
- handle_edge_irq
- handle_percpu_irq
这个函数指针是由 kernel/irq/chip.c 中的 __set_irq_handler() 设置的。
chip
chip 是 irq_chip 结构体指针,include/linux/irq.h 中的 irq_chip 结构体定义了对每根中断线的底层硬件操作:
99 struct irq_chip { 100 const char *name; // 中断线名称 101 unsigned int (*startup)(unsigned int irq); // 初始化中断的函数指针 102 void (*shutdown)(unsigned int irq); // 停止中断的函数指针 103 void (*enable)(unsigned int irq); // 启用中断的函数指针 104 void (*disable)(unsigned int irq); // 关闭中断的函数指针 105 106 void (*ack)(unsigned int irq); // 确认中断的函数指针 107 void (*mask)(unsigned int irq); // 屏蔽中断的函数指针 108 void (*mask_ack)(unsigned int irq); // 确认并屏蔽中断的函数指针 109 void (*unmask)(unsigned int irq); // 取消屏蔽中断的函数指针 110 void (*eoi)(unsigned int irq); // 中断处理结束的函数指针 111 112 void (*end)(unsigned int irq); 113 void (*set_affinity)(unsigned int irq, cpumask_t dest); // 设置处理器亲和性 114 int (*retrigger)(unsigned int irq); // 重新出发中断 // 设置中断触发类型,根据 IRQ_TYPE 宏定义,包括上边沿、下边沿、边沿、高电平、低电平等 115 int (*set_type)(unsigned int irq, unsigned int flow_type); 116 int (*set_wake)(unsigned int irq, unsigned int on); // 唤醒中断 117 118 /* Currently used only by UML, might disappear one day.*/ 119 #ifdef CONFIG_IRQ_RELEASE_METHOD 120 void (*release)(unsigned int irq, void *dev_id); 121 #endif 122 /* 123 * For compatibility, ->typename is copied into ->name. 124 * Will disappear. 125 */ 126 const char *typename; 127 };
action
action 是 irqaction 结构体指针,指向一个 irqaction 链表。irqaction 在 include/linux/interrupt.h 中定义,每个结构体描述一个中断处理程序。
60 struct irqaction { 61 irq_handler_t handler; // 中断处理程序的函数指针 62 unsigned long flags; 63 cpumask_t mask; // 处理器亲和性 64 const char *name; // 中断处理程序名称,显示在 /proc/interrupts 中 65 void *dev_id; // 设备 ID 66 struct irqaction *next; // 指向链表中的下一个 irqaction 结构体 67 int irq; // 中断通道号 68 struct proc_dir_entry *dir; // 在 /proc 文件系统中的目录 69 };
status
status 是描述 IRQ 线状态的一组标志。在同一文件中宏定义:
49 #define IRQ_INPROGRESS 0x00000100 /* IRQ handler active - do not enter! */ 50 #define IRQ_DISABLED 0x00000200 /* IRQ disabled - do not enter! */ 51 #define IRQ_PENDING 0x00000400 /* IRQ pending - replay on enable */ 52 #define IRQ_REPLAY 0x00000800 /* IRQ has been replayed but not acked yet */ 53 #define IRQ_AUTODETECT 0x00001000 /* IRQ is being autodetected */ 54 #define IRQ_WAITING 0x00002000 /* IRQ not yet seen - for autodetection */ 55 #define IRQ_LEVEL 0x00004000 /* IRQ level triggered */ 56 #define IRQ_MASKED 0x00008000 /* IRQ masked - shouldn't be seen again */ 57 #define IRQ_PER_CPU 0x00010000 /* IRQ is per CPU */
综上所述,内核中的中断描述符表是一个 irq_desc 数组,数组的每一项描述一根中断线的信息,包括芯片中断处理程序、底层硬件操作函数、注册的中断处理程序链表等。
中断向量表可以通过 /proc/interrupts 查看:
[boj@~]$ cat /proc/interrupts CPU0 CPU1 0: 3652701 2 IO-APIC-edge timer 1: 34517 0 IO-APIC-edge i8042 8: 1 0 IO-APIC-edge rtc0 9: 48512 19 IO-APIC-fasteoi acpi 12: 12 0 IO-APIC-edge i8042 14: 29337 0 IO-APIC-edge ata_piix 15: 38002 0 IO-APIC-edge ata_piix 16: 263352 1 IO-APIC-fasteoi uhci_hcd:usb5, yenta, i915 18: 0 0 IO-APIC-fasteoi uhci_hcd:usb4 19: 105769 0 IO-APIC-fasteoi uhci_hcd:usb3 21: 34677 0 IO-APIC-fasteoi eth0 22: 151 0 IO-APIC-fasteoi firewire_ohci 23: 2 0 IO-APIC-fasteoi ehci_hcd:usb1, uhci_hcd:usb2, mmc0 42: 360215 0 PCI-MSI-edge iwl3945 43: 656 0 PCI-MSI-edge hda_intel NMI: 0 0 Non-maskable interrupts LOC: 253429 2025163 Local timer interrupts SPU: 0 0 Spurious interrupts PMI: 0 0 Performance monitoring interrupts IWI: 0 0 IRQ work interrupts RES: 1063515 1286501 Rescheduling interrupts CAL: 3762 2967 Function call interrupts TLB: 13274 13115 TLB shootdowns TRM: 0 0 Thermal event interrupts THR: 0 0 Threshold APIC interrupts MCE: 0 0 Machine check exceptions MCP: 32 32 Machine check polls ERR: 0 MIS: 0
负责打印 /proc/interrupts 的代码位于 arch/x86/kernel/irq_32.c。
242 int show_interrupts(struct seq_file *p, void *v)
2.4 中断的初始化
中断机制的初始化分为三步:
- arch/x86/kernel/head_32.S 中 setup IDT,在内核引导分析报告中已经阐述。
- init/main.c 的 start_kernel() 中的 trap_init()
- init/main.c 的 start_kernel() 中的 init_IRQ()
trap_init()
trap_init() 定义于 arch/x86/kernel/traps_32.c,作用是设置中断向量。
- 初始化 APIC 映射表
- 调用 set_trap_gate、set_intr_gate、set_task_gate、set_system_gate 等,初始化中断描述符表。
- 调用 set_system_gate,初始化系统调用
- 将已设置的中断向量置保留位
- 将已设置的系统调用置保留位
- 初始化 CPU 作为屏障
- 执行 trap_init 的钩子函数
init_IRQ
init_IRQ() 定义于 arch/x86/kernel/paravirt.c,由 paravirt_ops.init_IRQ() 和 native_init_IRQ() 二者组成。
native_init_IRQ() 定义于 arch/x86/kernel/i8259.c。该函数主要将 IDT 未初始化的各项初始化为中断门。
-
pre_intr_init_hook() 调用 init_ISA_irqs(),初始化 irq_desc 数组、status、action、depth。
-
在循环中,对于所有在 FIRST_EXTERNAL_VECTOR(0x20) 与 NR_VECTOR(0x100)之间的、不是系统中断的 256 - 32 - 1 = 223 项,调用 set_intr_gate(),初始化为中断门。
-
现在我们关心的是,这些中断门的中断处理程序是什么?在 x86 体系结构下没找到 interrupt 数组的定义,因此使用 64 位体系结构的做说明:
// arch/x86/kernel/i8259_64.c 80 static void (*__initdata interrupt[NR_VECTORS - FIRST_EXTERNAL_VECTOR])(void) = { 81 IRQLIST_16(0x2), IRQLIST_16(0x3), 82 IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7), 83 IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb), 84 IRQLIST_16(0xc), IRQLIST_16(0xd), IRQLIST_16(0xe), IRQLIST_16(0xf) 85 };
- 以上是 interrupt 数组的定义。在下面的代码中,## 是将字符串连接起来,这样宏定义的函数 IRQ(0x4,6) 就是 IRQ0x46_interrupt,生成 224 个这样的函数填入数组。
// arch/x86/kernel/i8259_64.c 70 #define IRQ(x,y) \ 71 IRQ##x##y##_interrupt 72 73 #define IRQLIST_16(x) \ 74 IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \ 75 IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \ 76 IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \ 77 IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)
- 那么这 224 个函数在哪里呢?通过下面的宏可以生成一个汇编函数,它调用了 common_interrupt 函数:
// include/asm/hw_irq_64.h 155 #define IRQ_NAME2(nr) nr##_interrupt(void) 156 #define IRQ_NAME(nr) IRQ_NAME2(IRQ##nr) 162 #define BUILD_IRQ(nr) \ 163 asmlinkage void IRQ_NAME(nr); \ 164 asm("\n.p2align\n" \ 165 "IRQ" #nr "_interrupt:\n\t" \ 166 "push $~(" #nr ") ; " \ 167 "jmp common_interrupt");
- common_interrupt 是汇编函数,这个函数最终调用了 do_IRQ,这是我们下章要介绍的核心中断处理函数。
// arch/x86/kernel/entry_32.S 613 common_interrupt: 614 SAVE_ALL 615 TRACE_IRQS_OFF 616 movl %esp,%eax 617 call do_IRQ 618 jmp ret_from_intr 619 ENDPROC(common_interrupt)
- 看来,只需要调用 BUILD_IRQ 就能生成中断处理函数了。Linux Kernel 正是这样做的:
// arch/x86/kernel/i8259_64.c 37 #define BI(x,y) \ 38 BUILD_IRQ(x##y) 39 40 #define BUILD_16_IRQS(x) \ 41 BI(x,0) BI(x,1) BI(x,2) BI(x,3) \ 42 BI(x,4) BI(x,5) BI(x,6) BI(x,7) \ 43 BI(x,8) BI(x,9) BI(x,a) BI(x,b) \ 44 BI(x,c) BI(x,d) BI(x,e) BI(x,f) .................................................... 61 BUILD_16_IRQS(0x2) BUILD_16_IRQS(0x3) 62 BUILD_16_IRQS(0x4) BUILD_16_IRQS(0x5) BUILD_16_IRQS(0x6) BUILD_16_IRQS(0x7) 63 BUILD_16_IRQS(0x8) BUILD_16_IRQS(0x9) BUILD_16_IRQS(0xa) BUILD_16_IRQS(0xb) 64 BUILD_16_IRQS(0xc) BUILD_16_IRQS(0xd) BUILD_16_IRQS(0xe) BUILD_16_IRQS(0xf)
- 不得不发表一下感慨,Linux Kernel 对 C 语言宏的运用真是炉火纯青。如果是我写代码,很可能不得不先写个代码生成器,用它生成 224 个函数的源码。但 Linux Kernel Source 里出现几百个几乎一模一样的函数太不优雅了,于是利用 C 语言的预处理机制,采用二级宏定义,尽可能降低代码量。
2.5 内核接口
中断处理程序不是编译内核时就完全确定的,因此要为开发者留下编程接口。
2.6.17 内核引入了 generic IRQ 机制,支持 i386、x86-64 和 ARM 三个体系结构。generic IRQ 层的引入,是为了剥离 IRQ flow 和 IRQ chip 过于紧密的耦合。为驱动开发者提供通用的 API 来 request/enable/disable/free 中断,而不需要知道任何底层的中断控制器细节。
这些中断 API 是在内核中用 EXPORT_SYMBOL 导出的。
请求中断
kernel/irq/manage.c 中的 request_irq:
536 int request_irq(unsigned int irq, irq_handler_t handler, 537 unsigned long irqflags, const char *devname, void *dev_id)
参数
- irq: 中断通道号,无符号整数
- handler:中断处理程序的函数指针 (irq_return_t isr_func(int irq, void *dev_id))
- irq_flags:标志位
- IRQF_SHARED: 共享中断通道
- IRQF_DISABLED: 中断处理程序执行时关中断
- IRQF_SAMPLE_RANDOM: 随机发生中断,可用于产生随机数
- IRQF_TRIGGER_LOW:2.6.26 中没有,低电平有效
- IRQF_TRIGGER_HIGH: 2.6.26 中没有,高电平有效
- IRQF_TRIGGER_RISING: 2.6.26 中没有,上升沿有效
- IRQF_TRIGGER_FALLING: 2.6.26 中没有,下降沿有效
- dev_name:名称,显示在 /proc/interrupts 中
- dev_id:设备 ID,区分不同的设备
内部机制:
- 检查输入数据的合法性
- 为临时变量 irqaction 分配内存空间,初始化 irqaction 数据结构
- 如果是调试模式,测试是否运行正常
- 进入工作函数 setup_irq(unsigned int irq, struct irqaction *new)
- 如果是 IRQF_SAMPLE_RANDOM 模式,随机初始化 irq
- 上自旋锁
- 如果希望共享中断通道,所有中断处理程序需要有相同的触发特性标识、PERCPU 特性
- 把新 irqaction 结构体挂在链表尾部
- 如果设置了 IRQF_TRIGGER_MASK,初始化触发特性
- 初始化 irq 状态、嵌套深度
- 启动(enable)此 IRQ
- 释放自旋锁
- 调用 /kernel/irq/proc.c 中的 register_irq_proc() 和 register_handler_proc(),建立 /proc 文件系统中的相关数据结构
- 返回成功(0)
- 如果出错,输出内核调试信息,释放自旋锁,返回错误
- 释放 irqaction 的内存空间,返回 setup_irq 的返回值
清除中断
kernel/irq/manage.c 中的 free_irq:
435 void free_irq(unsigned int irq, void *dev_id)
参数
- irq: 中断通道号,无符号整数
- dev_id: 请求中断时指定的设备 ID
内部机制:
- 不能在中断上下文中调用
- 上自旋锁
- 循环,沿链表查找要删除的中断处理程序
- 如果发现是已经释放的,则输出内核调试信息,释放自旋锁
- 如果 dev_id 不对,沿着 irqaction 链表继续向下寻找
- 如果找到了,从链表中移除这个 irqaction
- 关闭此 IRQ,关闭硬件,释放自旋锁,从 /proc 文件系统中删除对应目录
- 同步 IRQ 以防正在其他 CPU 上运行
- 如果是调试模式,测试驱动程序是否知道此共享 IRQ 已移除
- 释放内存空间,返回
启用中断
kernel/irq/manage.c 中的 enable_irq:
153 static void __enable_irq(struct irq_desc *desc, unsigned int irq)
内部调用了 __enable_irq,首先上自旋锁,找到 irq_desc 结构体指针,判断嵌套深度,刷新 IRQ 状态,释放自旋锁。
参数
- desc: 指向 irq_desc 结构体的指针
- irq: 中断通道号
关闭中断
kernel/irq/manage.c 中的 disable_irq:
140 void disable_irq(unsigned int irq)
参数
- irq: 中断通道号
关闭中断 (无等待)
disable_irq 会保证存在的 IRQ handler 完成操作,而 disable_irq_nosync 立即关中断并返回。事实上,disable_irq 首先调用 disable_irq_nosync,然后调用 synchronize_irq 同步。
111 void disable_irq_nosync(unsigned int irq)
同步中断 (多处理器)
30 void synchronize_irq(unsigned int irq)
设置 IRQ 芯片
kernel/irq/chip.c: set_irq_chip()
93 int set_irq_chip(unsigned int irq, struct irq_chip *chip)
设置 IRQ 类型
kernel/irq/chip.c: set_irq_type()
122 int set_irq_type(unsigned int irq, unsigned int type)
设置 IRQ 数据
kernel/irq/chip.c: set_irq_data()
150 int set_irq_data(unsigned int irq, void *data)
设置 IRQ 芯片数据
kernel/irq/chip.c: set_irq_chip_data()
202 int set_irq_chip_data(unsigned int irq, void *data)
3 中断处理流程
3.1 CPU的中断处理流程
本节摘自参考文献之 中断的硬件环境
每个能够发出中断请求的硬件设备控制器都有一条名为 IRQ 的输出线。所有现有的 IRQ 线都与一个名为可编程中断控制器(PIC)的硬件电路的输入引脚相连,可编程中断控制器执行下列动作:
- 监视 IRQ 线,检查产生的信号。如果有两条以上的 IRQ 线上产生信号,就选择引脚编号较小的 IRQ 线。
- 如果一个引发信号出现在 IRQ 线上:
- 把接收到的引发信号转换成对应的向量号
- 把这个向量存放在中断控制器的一个 I/O 端口(0x20、0x21),从而允许 CPU 通过数据总线读此向量。
- 把引发信号发送到处理器的 INTR 引脚,即产生一个中断。
- 等待,直到 CPU 通过把这个中断信号写进可编程中断控制器的一个 I/O 端口来确认它;当这种情况发生时,清 INTR 线。
- 返回第1步。
当执行了一条指令后,CS和eip这对寄存器包含下一条将要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否已经发生了一个中断或异常。如果发生了一个中断或异常,那么控制单元执行下列操作:
- 确定与中断或异常关联的向量i (0 ≤ i ≤ 255)。
- 读由idtr寄存器指向的 IDT表中的第i项(在下面的分析中,我们假定IDT表项中包含的是一个中断门或一个陷阱门)。
- 从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符所标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址。
- 确信中断是由授权的(中断)发生源发出的。首先将当前特权级CPL(存放在cs寄存器的低两位)与段描述符(存放在GDT中)的描述符特权级DPL比较,如果CPL小于DPL,就产生一个“General protection”异常,因为中断处理程序的特权不能低于引起中断的程序的特权。对于编程异常,则做进一步的安全检查:比较CPL与处于IDT中的门描述符的DPL,如果DPL小于CPL,就产生一个“General protection”异常。这最后一个检查可以避免用户应用程序访问特殊的陷阱门或中断门。
- 检查是否发生了特权级的变化,也就是说,CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈。通过执行以下步骤来做到这点:
- 读tr寄存器,以访问运行进程的TSS段。
- 用与新特权级相关的栈段和栈指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到(参见第三章的“任务状态段”一节)
- 在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
- 如果故障已发生,用引起异常的指令地址装载CS和eip寄存器,从而使得这条指令能再次被执行。
- 在栈中保存eflags、CS及eip的内容。
- 如果异常产生了一个硬件出错码,则将它保存在栈中。
- 装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。
控制单元所执行的最后一步就是跳转到中断或者异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。
中断或异常被处理完后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程,这将迫使控制单元:
- 用保存在栈中的值装载CS、eip或eflags寄存器。如果一个硬件出错码曾被压入栈中,并且在eip内容的上面,那么,执行iret指令前必须先弹出这个硬件出错码。
- 检查处理程序的CPL是否等于CS中最低两位的值(这意味着被中断的进程与处理程序运行在同一特权级)。如果是,iret终止执行;否则,转入下一步。
- 从栈中装载ss和esp寄存器,因此,返回到与旧特权级相关的栈。
- 检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相应的段寄存器。控制单元这么做是为了禁止用户态的程序(CPL=3)利用内核以前所用的段寄存器(DPL=0)。如果不清这些寄存器,怀有恶意的用户态程序就可能利用它们来访问内核地址空间。
3.2 保存中断信息
Linux 内核的中断处理机制自始至终贯穿着 “重要的事马上做,不重要的事推后做” 的思想。
中断处理程序首先要做:
- 将中断号压入栈中,以便找到对应的中断服务程序
- 将当前寄存器信息压入栈中,以便中断退出时恢复上下文
显然, 这两步都是不可重入的。因此在进入中断服务程序时,CPU 已经自动禁止了本 CPU 上的中断响应。
上章中断初始化过程的分析中,已经介绍了 interrupt 数组的生成过程,其中索引为 n 的元素中存放着下列指令的地址:
pushl n-256 jmp common_interrupt
执行结果是将中断号 - 256 保存在栈中,这样栈中的中断都是负数,而正数用来表示系统调用。这样,系统调用和中断可以用一个有符号整数统一表示。
现在重述一下 common_interrupt 的定义:
// arch/x86/kernel/entry_32.S 613 common_interrupt: 614 SAVE_ALL 615 TRACE_IRQS_OFF 616 movl %esp,%eax # 将栈顶地址放入 eax,这样 do_IRQ 返回时控制转到 ret_from_intr() 617 call do_IRQ # 核心中断处理函数 618 jmp ret_from_intr # 跳转到 ret_from_intr()
其中 SAVE_ALL 宏将被展开成:
cld push %es # 保存除 eflags、cs、eip、ss、esp (已被 CPU 自动保存) 外的其他寄存器 push %ds pushl %eax pushl %ebp pushl %edi pushl %edx pushl %ecx pushl %ebx movl $ _ _USER_DS, %edx movl %edx, %ds # 将用户数据段选择符载入 ds、es movl %edx, %es
3.3 处理中断
前面汇编代码的实质是,以中断发生时寄存器的信息为参数,调用 arch/x86/kernel/irq32.c 中的 do_IRQ 函数。
我们注意到 unlikely 和 unlikely 宏定义,它们的含义是
#define likely(x) __builtin_expect((x),1) #define unlikely(x) __builtin_expect((x),0)
__builtin_expect 是 GCC 的内部机制,意思是告诉编译器哪个分支条件更有可能发生。这使得编译器把更可能发生的分支条件与前面的代码顺序串接起来,更有效地利用 CPU 的指令流水线。
do_IRQ 函数流程:
- 保存寄存器上下文
- 调用 irq_enter:
// kernel/softirq.c 281 void irq_enter(void) 282 { 283 #ifdef CONFIG_NO_HZ // 无滴答内核,它将在需要调度新任务时执行计算并在这个时间设置一个时钟中断,允许处理器在更长的时间内(几秒钟)保持在最低功耗状态,从而减少了电能消耗。 284 int cpu = smp_processor_id(); 285 if (idle_cpu(cpu) && !in_interrupt()) 286 tick_nohz_stop_idle(cpu); // 如果空闲且不在中断中,则停止空闲,开始工作 287 #endif 288 __irq_enter(); 289 #ifdef CONFIG_NO_HZ 290 if (idle_cpu(cpu)) 291 tick_nohz_update_jiffies(); // 更新 jiffies 292 #endif 293 }
// include/linux/hardirq.h 135 #define __irq_enter() \ /* 在宏定义函数中,do { ... } while(0) 结构可以把语句块作为一个整体,就像函数调用,避免宏展开后出现问题 */ 136 do { \ 137 rcu_irq_enter(); \ 138 account_system_vtime(current); \ 139 add_preempt_count(HARDIRQ_OFFSET); \ /* 程序嵌套数量计数器递增1 */ 140 trace_hardirq_enter(); \ 141 } while (0)
- 如果可用空间不足 1KB,可能会引发栈溢出,输出内核错误信息
- 如果 thread_union 是 4KB 的,进行一些特殊处理
- 调用 desc->handle_irq(irq, desc),调用 __do_IRQ() (kernel/irq/handle.c)
- 取得中断号,获取对应的 irq_desc
- 如果是 CPU 内部中断,不需要上锁,简单处理完就返回了
- 上自旋锁
- 应答中断芯片,这样中断芯片就能开始接受新的中断了。
- 更新中断状态。
IRQ_REPLAY:如果被禁止的中断管脚上产生了中断,这个中断是不会被处理的。当这个中断号被允许产生中断时,会将这个未被处理的中断转为 IRQ_REPLAY。
IRQ_WAITING:探测用,探测时会将所有没有中断处理函数的中断号设为 IRQ_WAITING,只要这个中断管脚上有中断产生,就把这个状态去掉,从而知道哪些中断管脚上产生过中断。
IRQ_PENDING、IRQ_INPROGRESS 是为了确保同一个中断号的处理程序不能重入,且不能丢失这个中断的下一个处理程序。具体地说,当内核在运行某个中断号对应的处理程序时,状态会设置成 IRQ_INPROGRESS。如果发现已经有另一实例在运行了,就将这下一个中断标注为 IRQ_PENDING 并返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,是则再次执行一遍。
- 如果链表上没有中断处理程序,或者中断被禁止,或者已经有另一实例在运行,则进行收尾工作。
- 循环:
- 释放自旋锁
- 执行函数链:handle_IRQ_event()。其中主要是一个循环,依次执行中断处理程序链表上的函数,并根据返回值更新中断状态。如果愿意,可以参与随机数采样。中断处理程序执行期间,打开本地中断。
- 上自旋锁
- 如果当前中断已经处理完,则退出;不然取消中断的 PENDING 标志,继续循环。
- 取消中断的 INPROGRESS 标志
- 收尾工作:有的中断在处理过程中被关闭了,->end() 处理这种情况;释放自旋锁。
- 执行 irq_exit(),在 kernel/softirq.c 中:
- 递减中断计数器
- 检查是否有软中断在等待执行,若有则执行软中断。
- 如果使用了无滴答内核看是不是该休息了。
- 恢复寄存器上下文,跳转到 ret_from_intr (跳转点早在 common_interrupt 中就被指定了)
在中断处理过程中,我们反复看到对自旋锁的操作。在单处理器系统上,spinlock 是没有作用的;在多处理器系统上,由于同种类型的中断可能连续产生,同时被几个 CPU 处理(注意,应答中断芯片是紧接着获得自旋锁后,位于整个中断处理流程的前部,因此在中断处理流程的其余部分,中断芯片可以触发新的中断并被另一个 CPU 开始处理),如果没有自旋锁,多个 CPU 可能同时访问 IRQ 描述符,造成混乱。因此在访问 IRQ 描述符的过程中需要有 spinlock 保护。
3.4 从中断中返回
上面的中断处理流程中隐含了一个问题:整个处理过程是持续占有CPU的(除开中断情况下可能被新的中断打断外),这样
- 连续的低优先的中断可能持续占有 CPU, 而高优先的某些进程则无法获得 CPU
- 中断处理的这几个阶段中不能调用可能导致睡眠的函数
对于第一个问题,较新的 linux 内核增加了 ksoftirqd 内核线程,如果持续处理的软中断超过一定数量,则结束中断处理过程,唤醒 ksoftirqd,由它来继续处理。
对于第二个问题,linux 内核提供了 workqueue(工作队列)机制,定义一个 work 结构(包含了处理函数),然后在上述的中断处理的几个阶段的某一步中调用 schedule_work 函数,work 便被添加到 workqueue 中,等待处理。
工作队列有着自己的处理线程, 这些 work 被推迟到这些线程中去处理。处理过程只可能发生在这些工作线程中,不会发生在内核中断处理路径中,所以可以睡眠。下章将简要介绍这些中断机制。
3.5 编写中断处理程序
本节编写一个简单的中断处理程序 (catchirq) 作为内核模块,演示捕获网卡中断。
- catchirq.c
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/interrupt.h> #include <linux/timer.h> #define DEBUG #ifdef DEBUG #define MSG(message, args...) printk(KERN_DEBUG "catchirq: " message, ##args) #else #define MSG(message, args...) #endif MODULE_LICENSE("GPL"); MODULE_AUTHOR("boj"); int irq; char *interface; // module_param(name, type, perm) module_param(irq, int, 0644); module_param(interface, charp, 0644); int irq_handle_function(int irq, void *device_id) { static int count = 1; MSG("[%d] Receive IRQ at %ld\n", count, jiffies); count++; return IRQ_NONE; } int init_module() { if (request_irq(irq, irq_handle_function, IRQF_SHARED, interface, (void *)&irq)) { MSG("[FAILED] IRQ register failure.\n"); return -EIO; } MSG("[OK] Interface=%s IRQ=%d\n", interface, irq); return 0; } void cleanup_module() { free_irq(irq, &irq); MSG("IRQ is freed.\n"); }
- Makefile(编写说明参见 Documentation/kbuild/)
obj-m := catchirq.o KERNELDIR := /lib/modules/$(shell uname -r)/build default: make -C $(KERNELDIR) M=$(shell pwd) clean: make -C $(KERNELDIR) M=$(shell pwd) clean
- 命令:make
-
[boj@~/int]$ ls built-in.o catchirq.c catchirq.ko catchirq.mod.c catchirq.mod.o catchirq.o Makefile modules.order Module.symvers
- 查看 /proc/interrupts(前面章节已经贴出来了),获知我们想截获的网卡(eth0)是 21 号中断。通过 insmod 的 interface 和 irq 指定模块加载参数(源文件中的 module_params 指定的)
sudo insmod catchirq.ko interface=eth1 irq=21
- 成功插入一个内核模块:
[boj@~]$ lsmod | grep catchirq catchirq 12636 0
- 我们看到,/proc/interrupts 的 21 号中断增加了一个中断处理程序:eth1
[boj@~/int]$ cat /proc/interrupts CPU0 CPU1 0: 23443709 27 IO-APIC-edge timer 1: 205319 0 IO-APIC-edge i8042 8: 1 0 IO-APIC-edge rtc0 9: 170665 80 IO-APIC-fasteoi acpi 12: 12 0 IO-APIC-edge i8042 14: 135310 0 IO-APIC-edge ata_piix 15: 205712 1 IO-APIC-edge ata_piix 16: 1488409 29 IO-APIC-fasteoi uhci_hcd:usb5, yenta, i915 18: 0 0 IO-APIC-fasteoi uhci_hcd:usb4 19: 477290 5 IO-APIC-fasteoi uhci_hcd:usb3 21: 107049 0 IO-APIC-fasteoi eth0, eth1 22: 806 0 IO-APIC-fasteoi firewire_ohci 23: 2 0 IO-APIC-fasteoi ehci_hcd:usb1, uhci_hcd:usb2, mmc0 42: 1803270 2 PCI-MSI-edge iwl3945 43: 11783 0 PCI-MSI-edge hda_intel NMI: 0 0 Non-maskable interrupts LOC: 2013602 12644870 Local timer interrupts SPU: 0 0 Spurious interrupts PMI: 0 0 Performance monitoring interrupts IWI: 0 0 IRQ work interrupts RES: 6046340 7106551 Rescheduling interrupts CAL: 20110 14839 Function call interrupts TLB: 33385 36028 TLB shootdowns TRM: 0 0 Thermal event interrupts THR: 0 0 Threshold APIC interrupts MCE: 0 0 Machine check exceptions MCP: 172 172 Machine check polls ERR: 0 MIS: 0
- dmesg 中可以看到大量如下形式的内核信息。这恰好是我们在源码中的 DEBUG 模式通过 printk 输出的。
// [Time] module_name: [count] Receive IRQ at jiffies [51837.231505] catchirq: [499] Receive IRQ at 12884307 [51837.232803] catchirq: [500] Receive IRQ at 12884308 [51837.232849] catchirq: [501] Receive IRQ at 12884308 [51837.269587] catchirq: [502] Receive IRQ at 12884317 [51844.585799] catchirq: [503] Receive IRQ at 12886146 [51844.586724] catchirq: [504] Receive IRQ at 12886146
- 演示完毕,卸载内核模块:
sudo rmmod catchirq
- 根据 dmesg,catchirq 模块输出了最后一句话,被正常卸载。从 /proc/interrupts 看到,中断处理程序表恢复原状。
[52413.797952] catchirq: [2245] Receive IRQ at 13028449 [52413.815899] catchirq: [2246] Receive IRQ at 13028453 [52413.815990] catchirq: [2247] Receive IRQ at 13028453 [52413.841763] catchirq: IRQ is freed.
4 软中断、tasklet与工作队列
4.1 上半部与下半部
软中断、tasklet和工作队列并不是Linux内核中一直存在的机制,而是由更早版本的内核中的“下半部”(bottom half)演变而来。下半部的机制实际上包括五种,但2.6版本的内核中,下半部和任务队列的函数都消失了,只剩下了前三者。
上半部指的是中断处理程序,下半部则指的是一些虽然与中断有相关性但是可以延后执行的任务。举个例子:在网络传输中,网卡接收到数据包这个事件不一定需要马上被处理,适合用下半部去实现;但是用户敲击键盘这样的事件就必须马上被响应,应该用中断实现。
两者的主要区别在于:中断不能被相同类型的中断打断,而下半部依然可以被中断打断;中断对于时间非常敏感,而下半部基本上都是一些可以延迟的工作。由于二者的这种区别,所以对于一个工作是放在上半部还是放在下半部去执行,有一些参考标准:
- 如果一个任务对时间非常敏感,将其放在中断处理程序中执行。
- 如果一个任务和硬件相关,将其放在中断处理程序中执行。
- 如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断处理程序中执行。
- 其他所有任务,考虑放在下半部去执行。
4.2 软中断
软中断作为下半部机制的代表,是随着 SMP 的出现应运而生的,它也是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。软中断一般是“可延迟函数”的总称,有时候也包括了tasklet(请读者在遇到的时候根据上下文推断是否包含tasklet)。它的出现就是因为要满足上面所提出的上半部和下半部的区别,使得对时间不敏感的任务延后执行,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。特性是:
- 产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。
- 可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。
4.3 tasklet
由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。它具有以下特性:
- 一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
- 多个不同类型的tasklet可以并行在多个CPU上。
- 软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择。
一般而言,在可延迟函数上可以执行四种操作:初始化/激活/执行/屏蔽。屏蔽我们这里不再叙述,前三个则比较重要。下面将软中断和tasklet的三个步骤分别进行对比介绍。
-
初始化
初始化是指在可延迟函数准备就绪之前所做的所有工作。一般包括两个大步骤:首先是向内核声明这个可延迟函数,以备内核在需要的时候调用;然后就是调用相应的初始化函数,用函数指针等初始化相应的描述符。
如果是软中断则在内核初始化时进行,其描述符定义如下:
struct softirq_action { void (*action)(struct softirq_action *); void *data; };
kernel/softirq.c 中的软中断描述符数组:static struct softirq_action softirq_vec[32]
前 6 个已经被内核注册使用:
- tasklet 使用的 HI_SOFTIRQ
- tasklet 使用的 TASKLET_SOFTIRQ
- 网络协议栈使用的 NET_TX_SOFTIRQ
- 网络协议栈使用的 NET_RX_SOFTIRQ
- SCSI 存储
- 系统计时器
其余的软中断描述符可以由内核开发者使用。
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
例如网络子系统通过以下两个函数初始化软中断(net_tx_action/net_rx_action是两个函数):
open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action);
当内核中产生 NET_TX_SOFTIRQ 软中断之后,就会调用 net_tx_action 这个函数。
tasklet 则可以在运行时定义,例如加载模块时。定义方式有两种:
- 静态声明
DECLARE_TASKET(name, func, data) DECLARE_TASKLET_DISABLED(name, func, data)
- 动态声明
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data)
其参数分别为描述符,需要调用的函数和此函数的参数。初始化生成的就是一个实际的描述符,假设为 my_tasklet。
-
激活
激活:标记一个可延迟函数为挂起(pending)状态,表示内核可以调用这个可延迟函数。类似处于 TASK_RUNNING 状态的进程,处在这个状态的进程只是准备好了被 CPU 调度,但并不一定马上就会被调度。
软中断使用 raise_softirq() 函数激活,接收的参数就是上面初始化时用到的数组索引 nr。
tasklet 使用 tasklet_schedule() 激活,该函数接受 tasklet 的描述符作为参数,例如上面生成的 my_tasklet:
tasklet_schedule(&my_tasklet)
-
执行
执行就是内核运行可延迟函数的过程,但是执行只发生在某些特定的时刻。
每个CPU上都有一个32位的掩码__softirq_pending,表明此CPU上有哪些挂起(已被激活)的软中断。此掩码可以用local_softirq_pending()宏获得。所有的挂起的软中断需要用do_softirq()函数的一个循环来处理。
对于 tasklet,软中断初始化时设置了发生 TASKLET_SOFTIRQ 或 HI_SOFTIRQ 软中断时所执行的函数:
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL); open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
tasklet_action 和 tasklet_hi_action 内部实现不同,软中断和 tasklet 因此具有了不同的特性。
4.4 工作队列
上面的可延迟函数运行在中断上下文中(如上章所述,软中断的一个检查点就是 do_IRQ 退出的时候),于是导致了一些问题:软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。
因此在 2.6 版的内核中出现了在内核态运行的工作队列(替代了 2.4 内核中的任务队列)。它也具有一些可延迟函数的特点(需要被激活和延后执行),但是能够能够在不同的进程间切换,以完成不同的工作。
参考文献
- 陈香兰老师《Linux内核源代码导读》讲义
- Understanding the Linux Kernel, Third Edition
- Interrupt in Linux (硬件篇)
- Linux 中断处理浅析
- 超强的 Linux 中断分析
- 中断描述符表
- 中断的硬件环境
- 软中断/tasklet/工作队列
- 为什么要有中断
-
优化Linux网络处理性能的软中断处理机制.pdf
2021-09-06 20:29:22优化Linux网络处理性能的软中断处理机制.pdf -
Linux中断处理原理分析
2021-01-20 14:47:58近有时间了,于是试着整理一下linux的中断处理机制,目的是起码从原理上能够说得通。 一、简单的中断机制 简单的中断机制是像芯片手册上讲的那样,在中断向量表中填入跳转到对应处理函数的指令,然后在处理... -
一种Linux嵌入式系统的中断处理机制.pdf
2021-09-06 20:30:44一种Linux嵌入式系统的中断处理机制.pdf -
Linux中断处理驱动程序编写
2018-10-22 12:40:21中断就是CPU正常运行期间,由于内、外部事件引起的CPU暂时停止正在运行的程序,去执行该内部事件或外部事件的引起的服务中去,服务执行完毕后再返回断点处继续执行的情形。这样的中断机制极大的提高了CPU运行效率。 -
深入剖析Linux中断机制
2014-08-29 17:24:37Linux 处理异常和中断的基本流程, 在此基础上分析了中断处理的详细流程, 包括保存现场、 中断处理、中断退出时的软中断执行及中断返回时的进程切换等问题。最后介绍了中断相关 的 API,包括中断注册和释放、中断... -
Linux中断机制:硬件处理,初始化和中断处理
2020-12-23 21:41:41硬件处理最近解决一个关于Linux中断的问题,把相关机制整理了一遍,记录在此。不同的外部设备、不同的体系结构、不同的OS其中断实现机制都有差别,本文对应的OS为linux3.4版本,外部...硬件处理
最近解决一个关于Linux中断的问题,把相关机制整理了一遍,记录在此。
不同的外部设备、不同的体系结构、不同的OS其中断实现机制都有差别,本文对应的OS为linux3.4版本,外部设备为PCI设备、系统为X86。
概览
中断让外设能够通知CPU他需要获得服务(让CPU执行指定的中断服务例程ISR)。为了达到这个目的,首先要为中断执行做好准备,完成初始化相关的操作。包括:
1、 初始化中断控制器等相关器件(OS初始化过程中完成);
2、 配置并使能外部设备(比如使用pci_enable_msix),得到irq号;在这个操作过程中,内核需要完成的大致操作是:1、 确定该中断的执行CPU,并在对应CPU上建立vector和irq号的对应关系(利用全局per-cpu变量vector_irq),配置中断控制器(I/OAPIC、PIR等),可能还需要设置外部设备(比如设置MSI
Capacity registers);
2、 为对应的irq_desc初始化正确的handle_irq接口(通用逻辑接口);
3、 为对应的irq_desc初始化正确的底层chip操作接口。
3、 使用request_irq号为该中断号指定一个服务例程;
完成了以上的初始化操作,在外设中断到来的时候,为该中断指定的ISR(Interrupt Service Routines)就能得到执行,这个执行过程大致如下:
1、 外设根据各自的配置,产生中断信号或者中断消息(MSI,INT# message)。
2、 中断控制器从外设获取中断电信号或者中断消息,把它翻译为vector(CPU使用这个参数来决定是谁发生了中断,要如何处理)并提交到CPU。
3、 对X86系统,CPU利用从中断控制器获取到的vector为索引,查询IDT (interrupt descriptor table)得到该中断的处理接口(对linux,是在entry_64.s中定义的函数common_interrupt接口)并执行。
4、 在linux定义的common_interrupt接口中,执行完中断执行环境建立后,会进入generic interrupt layer执行,其首先通过vector查找到irq和对应的irq_desc结构,并执行该结构的handle_irq接口,这个接口就是generic interrupt layer的通用逻辑接口,比如handle_edge_irq/handle_level_irq等;在中断执行的通用逻辑接口中,会通过irq_desc::action调用外设指定的ISR。
在linux中可以通过/proc/interrupts查看当前系统中所有中断的统计信息,在/proc/irq/xxx(中断号)下面,可以看到该中断的详细信息。中断相关硬件
这里的描述很多来自INTEL的文档《Intel Software developer’s Manual, system programming guide》和《PCI Express System Architecture》
中断控制器
中断控制器的功能是:把外设的中断信号,转换成CPU能够明白的vector,并完成中断执行控制,确保在合适的时机把中断提交给CPU执行。对这部分内容,《interrupt in linux》有详细的描述。
1、 8259A:
每个8259A有8个管脚,每个管脚对应其连接的CPU的IDT中的一个vector,单独使用8259A,其硬件连线就决定了对设备vector的使用。典型的场景是使用两个8259A级联,理论最多16个中断号(就是ISA IRQs),实际能提供对15个中断线的处理(master的IRQ2用于连接slave),其具体的分配见下图。
2、 PIR:
用于完成输入的信号到输出信号的映射。在下图中PIR被用于完成多个PCI设备的INT#信号到8259A对应引脚的路由。对应这种连接方式,在PCI设备初始化的时候,OS会根据BISO提供的信息设置PIR,把INT#路由到O0-O3中正确的管脚,从而体现到8259A的正确管脚(对应了vector),这样INT#信号就被转换为vector并提交到CPU。由于可能有较多的PCI设备,而PIR的输入/出错管脚有限,所以连接到相同输入关键的INT#会共享一个中断。3、 I/O APIC
每个I/O APIC提供24个管脚,能够和外部设备的中断线连接,每个管脚都可以通过配RTE(Redirection table entry)配置对应的vector。其功能是:把外部设备的中断请求,翻译为local APIC的interrupt message,并按照配置的vector,发送给指定的local APIC处理(在SMP系统,存在多个CPU,也就有多个local APIC)。通常的配置方式是:第一个I/O APIC的前16个管脚,配置来处理之前的ISA IRQs,其它外设比如PCI设备,则直接使用其他管脚连接。
4、 local APIC
其负责处理IPI(inter-process interrupt)、直接连接的中断处理、接收和处理interrupt message,每个CPU有自己的local APIC。
对应I/O APIC和local APIC的组合,其连接方式见下图针对X86中断控制器硬件和linux对这些硬件的初始化,在《interrupt in linux》中有很详细的描述。
X86对中断的处理
Local APIC的处理过程
每个local APIC对应了一个CPU。其处理interrupt message的过程如下:
1、 判断该中断的destination是否为当前APIC,如果不是则忽略,否则继续处理
2、 如果是SMI/NMI/INIT/ExtINT, or SIPI(这些中断都负责特殊的系统管理任务,外设一般不会使用)被直接送到CPU执行,否则执行下一步。
3、 设置Local APIC 的IRR寄存器的对应bit位。
4、 如果该中断优先级高于当前CPU正在执行的中断,且当前CPU没有屏蔽中断(按照X86和LINUX的实现,这时是屏蔽了中断的),则该高优先级中断会中断当前正在执行的中断(置ISR位,并开始执行),低优先级中断会在高优先级中断完成后继续执行,否则只有等到当前中断执行完成(写了EOI寄存器)后才能开始执行下一个中断。
5、 在CPU可以处理下一个中断的时候,从IRR中选取最高优先级的中断,清0 IRR中的对应位,并设置ISR中的对应位,然后ISR中最高优先级的中断被发送到CPU执行(如果其它优先级和屏蔽检查通过)。
6、 CPU执行中断处理例程,在合适的时机(在IRET指令前)通过写EOI寄存器来确认中断处理已经完成,写EOI寄存器会导致local APIC清理ISR的对应bit,对于level trigged中断,还会向所有的I/O APIC发送EOI message,通告中断处理已经完成。说明:
1、 关于Local APIC的IRR和ISR
寄存器interrupt request register (IRR) 和 in-service register (ISR),都是256bit寄存器,每个bit对应一个中断(其中[0-15]不能使用,SMI/NMI/INIT/ExtINT/SIPI的发送和执行不经过ISR和IRR) 。IRR中保存的是已经被local APIC接纳但是还没有开始执行的中断;ISR中保持的是当前正在执行但是还没有完成的中断。
2、 中断优先级
对应通过local APIC发送到CPU的中断,按照其vector进行优先级排序:
优先级=vector/16
数值越大,优先级越高。由于local APIC允许的vector范围为[16,255],而X86系统预留了[0,31]作为系统保留使用的vector,实际的用户定义中断的优先级的取值范围为[2,15],在每个优先级内部,vector的值越大,优先级越高。
Local APIC中还有一个关于中断优先级的寄存器TPR(task priority register)寄存器:用于确定打断线程执行需要的中断优先级级别,只有优先级高于设置值的中断才会被CPU执行 (SMI/NMI/INIT/ExtINT, or SIPI不受限制),也就是除了特殊中断外,优先级低于TPR指定值的中断将被忽略。
3、 中断的pending
对于同一个vector,如果有多次中断请求,可能IRR和ISR对应的bit位都被置位,也就是对同一个vector,local APIC可以pending两个中断,其后的即使有多处,也会被合并为一个执行。
4、 中断执行时机
中断的执行总是在指令边界开始(只有一个特殊的exception:abort在外,出现了这个中断,系统基本上也就完蛋了),也就是中断不可能打断指令的执行。CPU对中断和异常的处理
相关概念
1、 vector(中断向量)
vector是一个整数,在X86CPU上,使用vector对中断(interrupt,外部设备产生)和异常(exception,CPU在程序执行中产生)统一编号,每个CPU核心内部,中断/异常和vector所以一一对应的;但是在各个不同的CPU核心上,相同的vector可以对应不同的中断(至少对于linux的设置,异常还是使用相同的vector)。
vector的取值范围为[0,255],其中[0,31]被系统保留使用(多数作为异常的vector),其余的可供外设中断使用(系统设备比如local APIC也占用了部分[32,255]这个范围的vector)。
2、 IDT(interrupt descriptor table)
X86 CPU采用一个有256个元素的数组来描述中断/异常,该数组的index为vector;其内容包括了三种gate descriptor,用于描述一个中断/异常的处理接口;这个数组就是IDT,CPU在收到中断请求的时候,就利用vector获取到对应的中断处理接口描述并执行。
3、 可屏蔽中断
通过CPU INTR管脚/local APIC接收到的中断是可屏蔽中断,这些中断能够通过清零EFLAGS的IF来屏蔽(CLI指令)。通过INT n指令生成的中断即使使用了和外部中断一样的vector,也是不可屏蔽的;同样CPU运行过程中同步产生的trap、fault、abort等异常也是不可屏蔽的。
4、 NMI
NMI是不可屏蔽中断(不可通过IF标志屏蔽),是通过CPU的NMI管脚发出的中断或者通过delivery mode为NMI的方式提交的中断。NMI中断在执行前,CPU不仅会屏蔽其它中断,也会屏蔽NMI中断,直到NMI中断处理执行完成(IRET指令被执行)。使用INT 2指令虽然能执行NMI中断处理函数,但是相关硬件不会介入,也就是没有相关的屏蔽NMI中断的操作。CPU执行中断的过程
1、 利用vector,查IDT得到中断描述符;
2、 如果中断发生在用户态,会首先执行stack switch切换到内核态执行;
3、 依次保存EFLAGS CS IP到当前栈,如果需要(有error code的异常),把error code PUSH到当前栈。并把IF/TF位清零屏蔽可屏蔽中断;至此,CPU完成了中断处理程序执行环境的建立。
4、 执行中断描述符定义的中断处理入口(IDT中指定地址的代码);
5、 根据环境执行不同的中断退出方式,比如执行现场调度操作(retint_careful和retint_kernel),最终都会执行IRET指令;至此,中断执行完成。
异常的执行过程类似,只不过异常在执行前不会把IF位清零,只清零TF位。PCI设备的中断
本部分的很多内容来自《PCI Interrupts for x86 Machines under FreeBSD》和《PCI Express®
Base Specification Revision 3.0》和《PCI Express System Architecture》。
PCI设备的中断有两种模式:一种是INT#模式,一种是MSI模式。INT#模式
每个PCI设备用四个中断信号,对应INTA#、INTB# INTC#、INTD#,这些中断信号采用level trigger 的方式并且为低电平有效,PCI设备通过拉低对应的信号来assert对应的中断,并在ISR访问PCI设备的指定寄存器deassert该中断。中断线和X86系统的连接
这里存在两种常见连接模式,一种是使用老的8259A+PIR的系统,一种是使用新的I/O APIC的系统。
对于使用8259A的系统:PCI的中断线连接到一个可编程的PIR设备,再通过该设备连接到8259A(见X86中断控制器一章的图);对于采用I/OAPIC的系统,可以使用以下的连接方式,同样这里只画出了一个中断线,同时根据不同的系统配置可能存在多个I/OAPIC。除了采用直接的中断引脚连接,PCI还支持virtual INT#,使用INT# message(Assert INT# message和deassert INT# message)的方式来使用INT#信号。NT#模式的局限
1、 中断数量有限且不方便扩展:每个物理的PCI设备,最多只有4个中断但是至少能支持8个function,且系统中可能存在多个PCI设备,不得不使用中断共享的模式,影响使用性能。
2、 同步问题:由于INT#中断采用的是side channel,中断信号和数据本身存在不同步的问题:可能在中断到达的时候,对应的数据没有达到,为了处理这个问题,一般采用“读刷新”的做法,也就是在使用该设备写入到X86的数据之前,ISR先对这个设备进行一次读操作来确保相关数据已经写入完成,比如读PCI设备的中断状态寄存器等。MSI/MSI-X模式
在这种模式下,PCI设备通过和数据DMA一样的通道来完成中断处理,通过向特定地址空间(系统FSB Interrupt存储器空间)发起一个写操作来发起中断。该写操作的地址和数据信息在PCI设备初始化MSI功能的时候已经填写到MSI Capacity registers(MSI模式)/MSI-X table(MSI-X)中(对X86,这个地址空间是FEE00000H开始的地址空间,其实就是local APIC寄存器映射的地址空间),地址信息保存在Message address register,其中包含了目标CPU信息和FSB Interrupt存储器空间;数据中包含了该MSI中断对应的vector,保存在Message data register中。MCH(memory control hub)截获这个写操作,转换为FSB interrupt message并向各个CPU核心广播,local APIC接收并处理这个消息,最终触发CPU的中断处理过程。使用这种机制,中断的数量不受PIR/ IOAPIC等各种器件管脚数量的限制,MSI可以支持32个中断,而MSI-X可以达到2048个;中断的传递相当直接,省略了中断路由的过程;并且能直接从interrupt message中获取vector信息,减少了交互过程。初始化
相关概念和关键数据结构
1、 irq号:在当前系统中全局唯一,对应内核数据结构struct irq_desc,每个外设的中断有一个irq号(体系结构预留的中断,是没有对应的irq_desc结构和irq号的),该irq在该中断的生命周期内都不会改变,且和该中断的中断处理函数关联;内核使用一个bitmap allocated_irqs来标识当前系统已经分配的irq;irq号的管理与底层中断设备和配置无关,属于Generic Interrupt Layer;对于irq号分布集中的情况,不配置CONFIG_SPARSE_IRQ,内核采用数组直接管理,数组下标就是irq号;而对于irq号比较分散的,设置CONFIG_SPARSE_IRQ,内核采用radix tree来管理所有的irq号。
2、 vector号:内核使用全局bitmap used_vectors来标识那些vector被系统预留,不能被外设分配使用。
3、 irq号和vector号的关联:内核中使用per-cpu变量vector_irq来描述irq号和vector号的关联,对每个CPU,vector_irq是一个数组,在X86架构下成员数量为256,其数组的index为vector,值为irq,如果为-1则表示该CPU上的这个vector尚未分配。
4、 struct irq_desc结构,用来描述一个中断,是内核generic interrupt layer的关键数据结构,其包含了中断的大部分信息,并连接了driver层和物理中断设备层,每个irq号对应一个该结构,共享相同irq号的中断共享该结构。它的关键成员包括:a) irq_data :为该中断对应的物理中断设备层相关的数据。
b) handle_irq:为该该中断使用的通用逻辑接口。
c) action:为driver层提供的ISR信息,其为一个单向链表结构,所有共享该中断的设备的ISR都链接在这里。
内核关键数据结构和相关初始化
对X86 CPU,Linux内核使用全局idt_table来表达当前的IDT,该变量定义在traps.c
gate_desc idt_table[NR_VECTORS] __page_aligned_data = { { { { 0, 0 } } }, };//初始化为全0。
对中断相关的初始化,内核主要有以下工作:
1、 设置used_vectors,确保外设不能分配到X86保留使用的vector(预留的vector范围为[0,31],另外还有其他通过apic_intr_init等接口预留的系统使用的vector);
2、 设置X86CPU保留使用的vector对应的IDT entry;这些entry使用特定的中断处理接口;
3、 设置外设 (包括ISA中断)使用的中断处理接口,这些中断处理接口都一样。
4、 设置ISA IRQ使用的irq_desc;
5、 把IDT的首地址加载到CPU的IDTR(Interrupt Descriptor Table Register);
6、 初始化中断控制器(下一章描述)
以上工作主要在以下函数中完成:可以看到,这个过程会完成每个中断vector对应的idt entry的初始化,系统把这些中断vector分成以下几种:
1、X86保留vector,这些vector包括[0,0x1f]和APIC等系统部件占用的vector,对这些vector,会记录在bitmap used_vectors中,确保不会被外设分配使用;同时这些vector都使用各自的中断处理接口,其中断处理过程相对简单(没有generic interrupt layer的参与,CPU直接调用到各自的ISR)。
2、ISA irqs,对这些中断,在初始化过程中已经完成了irq_desc、vector_irq、以及IDT中对应entry的分配和设置,同时可以发现ISA中断,在初始化的时候都被设置为运行在0号CPU。
3、其它外设的中断,对这些中断,在初始化过程中仅设置了对应的IDT,和ISA中断一样,其中断处理接口都来自interrupt数组。中断处理接口interrupt数组
interrupt数组是内核中外设中断对应的IDT entry,其在entry_64.S中定义,定义如下:这段汇编的效果是:在代码段,生成了一个符号irq_entries_start,该符号对应的内容是一组可执行代码,一共(NR_VECTORS-FIRST_EXTERNAL_VECTOR+6)/7组,每组为7个中断入口,为:
每组的最后一个中断入口不需要jmp 2f是因为其pushq_cfi(就是pushq咯)下面就
是2f这个标号的地址了。(不明白的是:为什么不在jmp 2f的地方直接写上jmp common_interrupt?非要jmp 2f,2f的地方再次jmp common_interrupt? )而interrupt是一个数组,该数组在初始化完成后释放,其每个数组项都是一个地址,是对应的“pushq_cfi”代码的地址(每个代表中断入口的标号)。系统在初始化的时候,对外设使用interrupt数组作中断处理接口,就是在中断发生时,执行代码段:
初始化中断控制器
对中断控制器的使用基本上有三种机制:
1、 中断路由表 $PIR
struct irq_routing_table,该结构用于使用PIR和8259A的系统,在微软的文献《PCI IRQ Routing Table Specification》中描述了该结构详细信息。其描述了一个PCI设备的INT#是如何连接到PIR设备的输入端口的。其关键数据是一个可变长的struct irq_info数组,每个struct irq_info描述了一个PCI物理设备的4个INT#相关的中断路由信息和对应可用的ISA IRQs的bitmap。BIOS根据相关设备的物理连接填写该数据结构,OS在设备初始化过程中使用这些信息为使用INT#的设备分配对应的vector和irq。
2、 MP table
struct mpc_intsrc,该数据结构用于使用I/O APIC的系统中,描述系统中所有PCI设备4个INT#信号和I/O APIC输入引脚的对应关系。该数据结构的srcbus成员为对应PCI设备的bus id;srcbusirq描述了一个INT#信号,其bit0-bit1用于描述是INTA#–INTD#中的哪一个(对应值为0-3),bit2-bit6描述该PCI设备的slot id。dstapic为该描述对应的I/O APIC的ID。dstirq描述srcbus和srcbusirq确定的INT#对应的irq号信息(具体的解析有多种情况)。在系统中有一个以该数据结构为成员的全局数组mp_irqs,用于管理系统中所有的硬件中断信号和irq之间的关联。对MP table及其使用的更加详细的描述,见《Multiprocessor Specification v1.4》
3、 ACPI(Advanced Configuration and PowerInterface)机制
这种机制为I/O APIC机制和中PIR机制提供统一的管理界面,该机制使用struct acpi_prt_entry描述INT#和GSI(能和vector、irq对应)的关系,系统中所有的struct acpi_prt_entry由OS从BIOS提供的信息中获取,并保存在链表acpi_prt_list中。
注:对GSI的说明,GSI(global system interrupt)表示的是系统中中断控制器的每个输入管脚的唯一编号,在使用ACPI模式管理中断控制器的时候使用。对使用8259A的系统,GSI和ISA IRQ是一一对应的。对于使用APIC的,每个I/O APIC会由BISO分配一个基址,这个base+对应管脚的编号(从0开始)就是对应的GSI。通常是基址为0的I/O APIC的前16个管脚用于ISA IRQS,对GSI更加详细的描述,见《Advanced Configuration and Power Interface Revision 2.0》除了中断路由表,其它两种机制的初始化(包括相关中断路由信息的初始化)的在《interrupt in linux》中有很详细的描述。这些初始化操作都在内核初始化的时候完成。
为PCI设备配置中断
为PCI设备配置中断,分为两个步骤,
步骤一:为设备分配irq号(对MSIX,会有多个),为该中断分配执行CPU和它使用的vector,并通过对中断控制器的设置,确保对应的中断信号和vector匹配。对于使用INT#类型的中断,通常通过pci_enable_device/pci_enable_device_mem/pci_enable_device_io中对函数pcibios_enable_device的调用来完成(只有在没有开启MSI/MSIX的时候才会为INT#做配置),而要配置MSI/MSIX中断要使用的是pci_enable_msix。
步骤二:request_irq为该设备的irq指定对应的中断处理例程,把irq号和驱动定义ISR关联。pcibios_enable_device
该接口用于使能PCI设备INT#模式的中断。其主要功能由pcibios_enable_irq(dev)完成,pcibios_enable_irq是一个函数指针,对于ACPI模式,其在上电过程中被设置为acpi_pci_irq_enable,其它情况被设置为pirq_enable_irq。
对ACPI模式,其执行过程为:
1、 acpi_pci_irq_enable:其先根据设备的管脚信息获取一个GSI(可以认为有了GSI,就有了irq号,gsi_to_irq可以完成其转换),有了gsi/irq,要完成设置还必须有vector并且把它们关联起来,因此如果GSI获取成功,会使用acpi_register_gsi来完成后续操作。
2、 acpi_register_gsi:其主要功能由__acpi_register_gsi来完成,该函数指针在ACPI模式下被设置为acpi_register_gsi_ioapic,acpi_register_gsi_ioapic的执行过程如下:
mp_register_gsi===>io_apic_set_pci_routing===>io_apic_set_pci_routing===>io_apic_setup_irq_pin_once===>io_apic_setup_irq_pin===>setup_ioapic_irq,在setup_ioapic_irq中,就会利用assign_irq_vector为该irq选择对应的执行CPU,并分配该CPU上的vector,同时还把该vector等配置写入到I/O APIC对应管脚的RTE,从而完成整个中断的配置。这样在该INT#信号到来的时候,I/O APIC就能根据对应管脚的RTE,把该信号翻译为一个vector,并通过中断消息发送到local APIC。同时在setup_ioapic_irq中,还通过ioapic_register_intr===>irq_set_chip_and_handler_name为得到的irq号对应的irq_desc设置了->irq_data.chip和handle_irq函数指针(对level触发的,为handle_fasteoi_irq,否则为handle_edge_irq)对其它模式,其通过pcibios_lookup_irq完成执行:
在配置了I/O APIC的场景,pirq_enable_irq通过IO_APIC_get_PCI_irq_vector获取到irq号,然后和ACPI模式一样,通过io_apic_set_pci_routing完成对I/O APIC的配置。而对没有配置I/O APIC的场景,主要通过pcibios_lookup_irq来完成相关操作:
1、 pcibios_lookup_irq通过读取BIOS提供的中断路由表 ($PIR表,irq_routing_table)信息和当前irq分配情况(pirq_penalty数组),在考虑均衡的前提下为当前设备分配一个可用的irq。
2、 根据当前PIR的相关信息,决定最终的irq号选择,相关代码行如下也就是:如果是硬链接(INT#直接连接到了8259A,没有经过PIR),直接获取irq号,如果PIR中已经有该输入线的配置,使用已有的值,否则利用刚刚分配的可用irq,并写入到PIR,以便能够完成中断信号到irq号的转换。
注意:
1、这里的r,也就是pirq_router,代表一种PIR硬件,全局配置pirq_routers中描述了当前支持的PIR,并在初始化的时候通过pirq_find_router获取了对应当前配置的PIR对应的描述。
2、这里没有分配vector,是因为这里使用的irq号范围为0-16,是ISA IRQs,其与vector的对应关系简单:vector = IRQ0_VECTOR + irq,并在系统初始化过程中,已经通过early_irq_init中分配了irq_desc结构,通过init_IRQ设置了vector_irq(只运行于CPU0上),然后通过x86_init.irqs.intr_init(native_init_IRQ)===> x86_init.irqs.pre_vector_init(init_ISA_irqs)设置了->irq_data.chip(i8259A_chip)和handle_irq函数指针(handle_level_irq)。Pci_enable_msix
该函数完成MSIX中断相关的设置。
msix_capability_init中实现中断初始化的是arch_setup_msi_irqs,对于X86系统,其为x86_setup_msi_irqs,x86_setup_msi_irqs中直接调用了native_setup_msi_irqs,该函数是X86系统中实现MSIX中断初始化的关键函数,对于没有启用interrupt remap的系统,其实现如下:
该函数中有两个关键函数,分别是create_irq_nr和setup_msi_irq,其中create_irq_nr是分配一个vector给当前的中断,分配vector的同时,也为该中断指定了执行CPU。setup_msi_irq则负责把相关配置信息写入到PCIE配置区,并设置irq_desc的数据,其中关键的是irq_desc的handle_irq被设置为handle_edge_irq。
create_irq_nr的实现如下:其中__assign_irq_vector负责分配vector,并和中断在CPU上的调度相关,其实现如下
从实现中可以看到,该函数从FIRST_EXTERNAL_VECTOR(外设中断的起始vector号,通常是0x20) 到first_system_vector(外部中断结束vector号,通常是254,255被系统作为保留的SPURIOUS_APIC_VECTOR使用)的范围中,为当前中断分配一个vector,要求该vector在对应的cpu上均可用,该vector按照系统配置的要求和对应的cpu核心绑定,并在要求的cpu中没有被其它中断使用。需要说明的是,在setup_msi_irq中会再次通过msi_compose_msg再次调用__assign_irq_vector,但是由于这时已经存在满足CPU绑定要求的vector,不会多次分配。
从以上分析可以得到MSI-X中断的一个绑定特征:根据当前APIC配置,每个中断都有对应的可以运行的cpu,pci_enable_msix在这些要求的cpu核心上建立了vector (APIC的配置由数据结构struct apic来抽象,其vector_allocation_domain用于决定需要在那些cpu核心上为该中断建立vector),当前我的系统使用的是apic_physflat,对每个MSI中断,其只在一个cpu核心上建立vector,对应的MSI-X中断事实上被绑定到该cpu核心上。在用户通过echo xxx > /proc/irq/xxx/affinity来调整中断的绑定属性时,内核会重新为该中断分配一个新的在对应核心上可用的vector,但是irq号不会改变。绑定属性调整的调用路径大致为irq_affinity_proc_fops===>irq_affinity_proc_write===> write_irq_affinity===>irq_set_affinity===>__irq_set_affinity_locked===>chip->irq_set_affinity(msi_set_affinity)。也就是最终通过msi_set_affinity来实现,在该函数中首先通过 __ioapic_set_affinity在绑定属性要求的cpu中选择空闲vector,然后通过__write_msi_msg把配置写入PCIE配置区。需要说明的是:该irq最终可以运行的cpu数量并不完全由用户指定,还与apic的模式相关,对于apic_physflat,实际上只为该irq分配了一个cpu核心,该irq只能运行在用户指定的cpu中的一个,而不是全部。
附:关于全局变量apic
该全局变量为local apic的抽象,在不同的系统配置下,有不同的选择,其最终的选择结果,由内核的config(反应在/arch/x86/kernel/apic/Makefile)和硬件配置等来决定。
1、 定义各种apic driver
首先,每种apic配置都会使用apic_driver/ apic_drivers来定义,apic_driver的定义如下这个定义的目的是把sym的地址写入到名为” .apicdrivers”的段中。
2、 定义全局符号__apicdrivers和__apicdrivers_end
在linker script vmlinux.lds.S中,定义了__apicdrivers为” .apicdrivers”段的开始地址,而__apicdrivers_end为结束地址。” .apicdrivers”段中是各个不同的apic配置对应的struct apic。3、 apic的probe
在初始化过程(start_kernel)中,会调用default_setup_apic_routing(probe_64.c中定义)来完成apic的probe,该函数会按照各个struct apic结构在.apicdrivers中的顺序,依次调用其probe接口,第一个调用返回非0的struct apic结构就被初始化到全局变量apic。也就是:如果有多个apic结构可用,最终会选择在.apicdrivers段中出现的第一个;所以makefile文件中各个.o出现的顺序也会觉得最终的apic probe结果。request_irq
该函数把irq和用户指定的中断处理函数关联。用户指定的每个处理函数对应于一个struct irqaction结构,这些处理函数构成一个链表,保存在struct irq_desc::action成员中。详细见request_irq===>request_threaded_irq中的处理。
中断的执行
在内核代码中,对X86平台中断执行的基本过程是:
1、 通过IDT中的中断描述符,调用common_interrupt;
2、 通过common_interrupt,调用do_IRQ,完成vector到irq_desc的转换,进入Generic interrupt layer(调用处理函数generic_handle_irq_desc);
3、 调用在中断初始化的时候,按照中断特性(level触发,edge触发等、simple等)初始化的irq_desc:: handle_irq,执行不同的通用处理接口,比如handle_simple_irq;
4、 这些通用处理接口会调用中断初始化的时候注册的外部中断处理函数;完成EOI等硬件相关操作;并完成中断处理的相关控制。common_interrupt
按照之前CPU执行中断过程的描述,X86 CPU在准备好了中断执行环境后,会调用中断描述符定义的中断处理入口;根据中断相关初始化过程我们知道,对于用户自定义中断,中断处理入口都是(对系统预留的,就直接执行定义的接口了):
就是在把vector入栈后,执行common_interrupt,common_interrupt在entry_64.S中定义,其中关键步骤为:调用do_IRQ,完成后会根据环境判断是否需要执行调度,最后执行iretq指令完成中断处理,iret指令的重要功能就是回复中断函数前的EFLAGS(执行中断入口前被入栈保存,并清零IF位关中断),并恢复执行被中断的程序(这里不一定会恢复到之前的执行环境,可能执行软中断处理,或者执行调度)。
do_IRQ
do_IRQ的基本处理过程如下,其负责中断执行环境建立、vector到irq的转换等
Generic interrupt layer
该层负责的是平台无关/设备无关的中断通用逻辑,对这部分,在《Linux generic IRQ handling》中有详细描述。其负责完成中断处理的接口是generic_handle_irq_desc,该接口会执行irq_desc::handle_irq;Generic interrupt layer根据中断特性的不同,把中断分成几类,包括:level type(handle_level_irq)、edge type(handle_edge_irq)、simple type(handle_simple_irq)等,这些中断类型对应的处理函数是都在kernel/irq/chip.c中定义,并入前面的描述,在相关中断初始化的时候,被赋值给irq_desc::handle_irq;对于PCI设备,只用了两种,level type(INT#模式)、edge type(MSI/MSI-X模式)。
edge 触发中断的基本处理过程:
电压跳变触发中断===>中断控制器接收中断,记IRR寄存器===>中断控制器置ISR寄存器===>CPU屏蔽本CPU中断===>CPU处理中断,发出EOI===>中断控制器确认可以处理下一次中断===>ISR清中断源,电压归位===>中断源可以发起下一次中断===>CPU中断处理完成,执行完现场处理后执行IRET,不再屏蔽本CPU中断。
edge触发的特点:
a) 中断不会丢
如果中断触发时中断被屏蔽,那么中断控制器会记录下该中断,在屏蔽取消的时候会再执行。
b) edge触发的缺点是完成共享不方便:
比如A和B两个中断源共享一个中断,每次ISR先检查A再检查B,如果B先发生中断,在ISR检查完A,检查B的过程中,A发生中断。那么在ISR处理开始的时候,A会告诉ISR,不是它干的,然后ISR处理B的中断,完成后通过清理中断源把B的电压归位,但是由于A的中断没有得到处理,电压没有归位,这个共享的中断就不能得到再次触发了。
edge触发对应的通用逻辑接口level 触发:
这种模式下,外设通过把电压保持到某个门限值来完成触发中断,在处理完成(EOI)后,如果电压还在门限值,就会再次触发中断的执行。
level触发的特点:
a) 方便中断共享
b) 对中断触发时中断被屏蔽的情况,如果中断屏蔽解除后仍然引脚电压仍然在门限值,就执行该中断的ISR,否则不执行。
需要说明的是:对于使用local APIC的系统,level触发和edge触发需要配置local APIC的Local Vector Table。
4、 level触发对应的通用逻辑接口level触发和edge触发在通用逻辑层最大的不同就是当其他CPU正在处理该中断的时候,系统的行为,对edge触发,会把该中断记录下来,当前处理结束后再次执行,而level直接退出。产生这种差异的原因是:level触发不怕丢?
无论是那种触发方式,都会调用handle_irq_event处理中断,该函数中会遍历irq_desc::action链表,执行action->handler,也就是驱动在中断初始化的时候,通过request_irq注册的中断处理接口。
总结
中断的使能状态
1、 在local APIC层次(当前CPU),一个中断正在处理的时候,不会有相同的中断或者优先级低于该中断的其它中断来打断当前中断的执行;但是高优先级中断可以打断低优先级中断。
2、 在X86 CPU层次(当前CPU),从中断执行开始到IRET,IF位都被清零,也就是只有不可屏蔽中断能够打断当前中断的执行。
3、 在Generic interrupt layer层次,如果一个中断已经在系统中执行,会阻止该中断在其它CPU上的执行。
4、 在外设/驱动中断处理函数层次往往也有中断使能的功能,比如启用了NAPI的网卡,在中断处理函数开始执行的时候,往往会通过硬件功能关闭该中断,要在对应的软中断完成处理后才通过硬件功能使能该中断。
注:NMI中断虽然称为不可屏蔽中断,也有一个例外:NMI中断执行过程中,该CPU屏蔽了后来的NMI中断。中断的执行CPU
通过中断初始化过程我们知道:中断在那个CPU上执行,取决于在那个CPU上申请了vector并配置了对应的中断控制器(比如local APIC)。如果想要改变一个中断的执行CPU,必须重新申请vector并配置中断控制器。一般通过echo xxx > /proc/irq/xxx/affinity来完成调整,同时irq_balance一类软件可以用于完成中断的均衡。
-
Linux 中断之中断处理浅析
2019-01-17 14:15:091. 中断的概念 ...Linux中通常分为外部中断(又叫硬件中断)和内部中断(又叫异常)。 软件对硬件进行配置后,软件期望等待硬件的某种状态(比如,收到了数据),这里有两种方式,一种是轮询(pol... -
嵌入式Linux的中断处理与实时调度的实现机制.pdf
2021-09-07 00:49:48嵌入式Linux的中断处理与实时调度的实现机制.pdf -
LINUX irq中断机制介绍.docx
2020-07-27 14:23:19描述了linux中断的上半部即中断的注册、中断处理。中断的下半部机制,包括软中断、tasklet和工作队列 -
Linux中断——request_irq
2021-04-06 11:17:30前面一篇文章已经对中断做了一些简单介绍《关于Linux中断一些思考》,但是中断是怎么告诉内核(kernel)的呢?这里我们带着问题去了解一下irq的调用。 所谓中断它其实是硬件产生了一个信号,然后告诉内核要去处理它... -
Linux内核设计与实现---中断和中断处理程序
2022-03-21 15:21:23中断和中断处理程序1 中断2 中断处理程序3 注册中断处理程序4 编写中断处理程序共享的中断处理程序中断处理程序实例5 中断上下文6 中断处理机制的实现7 中断控制禁止和激活中断禁止指定中断线中断系统的状态 ... -
LINUX 中断处理机制的学习
2013-07-29 14:54:05linux中的中断处理机制是linux必不可少的一种工作机制,由于linux系统的特殊性,把linux的中断分为上半部分中断和下半部分中断 linux上半部分中断的机制解析: linux 中断和中断处理程序 linux下半部分的中断... -
Linux内核中断trace机制log解析工具
2018-08-14 16:29:54用于将通过cat /sys/kernel/debug/tracing/trace命令获取的log文件,进行解析,和统计,分类:哪一号中断,在哪一号CPU上运行,总共运行时间多久,最大单次处理时间,最小单次处理时间,平均处理时间,相同中断发生... -
Linux 软中断机制分析
2018-09-12 11:57:37软中断分析 ... 编写驱动的时候,一个中断产生之后,内核在中断处理函数中可能需要完成很多工作。但是中断处理函数的处理是关闭了中断的。也就是说在响应中断时,系统不能再次响应外部的其它中断... -
Linux中断处理“下半部”机制
2017-04-08 12:54:44转载自http://blog.csdn.net/myarrow/article/details/92871691. 中断处理“下半部”机制 中断服务程序一般都是在中断请求关闭的条件下执行的,以避免嵌套而使中断控制复杂化。但是,中断是一个随机事件,它随时会... -
Linux中断机制分析
2017-01-08 16:27:00中断处理机制 中断的类型 中断一般分为异步中断(一般由硬件引起)和同步中断(一般由处理器本身引起)。 异步中断:CPU 处理中断的时间过长,所以先将硬件复位,使硬件可以继续做自己的工作。然后再适当... -
Linux中的中断处理机制
2018-03-08 11:27:23转自:http://www.uml.org.cn/embeded/201304021.asp 与Linux设备驱动中中断处理相关的首先是申请与释放IRQ的API request_irq()和free_irq(),request_irq()的原型为: int request_irq(unsigned int irq,void (*... -
Linux内核同步机制
2021-01-20 14:49:52Linux内核同步机制,挺复杂的一个东西,常用的有自旋锁,信号量,互斥体,原子操作,顺序锁,RCU,内存屏障等。这里说说它们的特点和基本用法。 自旋锁 :通用的 和读写的 特点: 1. 处理的时间很短。 2... -
linux 中断机制的处理过程
2018-10-19 09:10:21一、中断的概念 中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或...Linux中通常分为外部中断(又叫硬件中断)和内部中断(又叫异... -
Linux 中断(包括:中断背景,定义,处理过程,类型,上下部分,中断上下文等)
2020-12-15 11:51:44中断1. 中断 定义1.1 操作系统中断举例1.2 官方定义中断1.3 分类2. 中断的作用 1. 中断 定义 中断的汉语解释是半中间发生阻隔、停顿或故障而断开。 中断通常被定义为一个事件,该事件能够改变处理器执行指令的顺序。... -
MSI和MSI-X中断机制.docx
2021-06-30 17:24:56MSI和MSI-X中断机制.docx -
细说Linux内核中断机制(详)
2018-07-20 23:04:21本文着重介绍Linux内核中中断处理的始末流程,因此对一些基本的概念应该有所了解。 2.硬件支持 我们知道,CPU有一个INTR引脚,用于接收中断请求信号。 而中断控制器用于提供中断向量,即第几号中断。 3.内核需要... -
Linux中的中断处理
2019-02-08 09:19:27Linux中的中断处理 -
Linux中断机制之一:硬件处理
2015-04-18 20:44:23最近解决一个关于Linux中断的问题,把相关机制整理了一遍,记录在此。不同的外部设备、不同的体系结构、不同的OS其中断实现机制都有差别,本文对应的OS为linux3.4版本,外部设备为PCI设备、系统为X86。概览中断让... -
linux中断处理程序架构--上半部与下半部
2019-04-02 14:35:20中断处理程序架构 设备的中断会打断内核中进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽可能地短小精悍。但是,这个良好的愿望往往与现实并不吻合。在大多数真实的系统中,当中断到来时,要... -
linux 中断和中断处理
2018-07-30 15:47:16这就是中断机制。 从物理学角度看,中断是一种电信号,中断是硬件发出,送入中断控制器的输入引脚中,中断控制器是个简单的电子芯片,其作用是将多路中断管线,采用复用技术只通过一个和处理器连接的管线与处理器...