中断和异常
1.1中断的由来及实质
Linux内核要管理计算机上的硬件设备,首先要和他们通信。而处理器的速度跟外围硬件设备的速度往往不在一个数量级上,因此,如果内核采取让处理器向硬件发出一个请求,然后专门等待回应的办法,显然差强人意。既然硬件的响应这么慢,那么内核就应该在此期间处理其他事务,等到硬件真正完成了请求的操作之后,再回过头来对它进行处理。想要实现这种功能,轮询(polling)可能会是一种解决办法。可以让内核定期对设备的状态进行查询,然后做出相应的处理。不过这种方法很可能会让内核做不少无用功,因为无论硬件设备是正在忙碌着完成任务还是已经大功告成,轮询总会周期性地重复执行。更好的办法是由我们来提供一种机制,让硬件在需要的时候再向内核发出信号(变内核主动为硬件主动)。这就是中断机制。
由中断或异常处理程序执行的代码不是一个进程,它是一个内核控制路径,代表中断发生时正在运行的进程执行,内核控制路径比一个进程要“轻“。
1.2异常和中断
中断(interrupt)通常被定为一个事件,该事件改变处理器执行的指令顺序。
中断通常分为同步中断和异步中断:
l 同步中断是当指令执行时由CPU控制单元产生的,称为同步,是因为只有在一条指令执行完成后CPU 才会发生中断。
l 异步中断是由其他硬件设备依照CPU始终信号随机产生的。
同步中断又称为异常,异常是由程序的错误产生的(例如除0),或者是由内核必须处理的异常条件(例如缺页)产生的。前一种情况下,必须通知应用程序出现了异常,内核通过发送一个每个Unix程序员熟悉的信号来处理异常,后一种必须借助于内核才能修复,内核执行恢复异常需要的所有步骤。
异常和异步中断的相同点:如果CPU当前不处于核心态,则发起从用户态到内核态的转变,接下来。内核中执行一个专门的中断服务例程(ISR interrupt service routine)。
异常和异步中断的不同点:产生源的区别,产生原因的区别,及发生时间的区别。还有一方面,许多中断可以禁用,但有些不行。此后本文所说中断默认指的都是异步中断。
(注意,这里是Intel划分的,并不是Linux,Linux有自己的划分方法)更进一步,Intel文档中又把中断和异常继续进行了细分:
l 中断:
可屏蔽中断: I/O设备发出的所有的中断请求(IRQ)都产生可屏蔽中断。可屏蔽中断产生两种状态:屏蔽的(masked)或非屏蔽的(unmasked);当中断被屏蔽,则CPU控制单元就忽略它。
非可屏蔽中断:总是由CPU辨认。只有几个危急事件引起非屏蔽中断。
l 异常:
1.处理器探测到得异常:
1)故障(fault)
通常可以纠正;一旦纠正,程序可以在不失连贯性的情况下重新开始。 保存在eip中的值是引起故障的指令地址。因此,当异常处理程序停止时,那条指令会重新执行
2)陷阱(trap)
在陷阱指令执行后立即报告;内核把控制权返回给程序后就可以继续它的执行而不失连贯性。保存在 eip中的值是一个随后要执行的指令地址。只有当没有必要重新执行已中止的指令时,才触发陷阱。陷阱的主要用途是为了调试程序。
3)异常终止(abort)
发生一个严重错误;CPU控制单元出了问题,不能在eip寄存器中保存引起异常指令所在的确切位置。这个异常中止处理程序除了强制中止受影响的进程中止外,没有别的选择。
2.编程异常(programmed exception)
在编程者发出请求时发生。是由int或int3指令触发的。控制单元把编程异常作为陷阱来处理。编程异常也叫软中断。用途:执行系统调用及给调试程序通报一个特定的事件。
综上所述,从广义上讲,中断可分为四类:中断、故障、陷阱、终止。这些类别之间的同点请参看表 1。
表 1 中断的类别(此表摘自《深入理解计算机系统》)
类别 | 原因 | 异步/同步 | 返回行为 |
中断 | 来自 I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
在可能的情况下,内核试图避免禁用中断,因为禁用会损害系统性能(比如禁用键盘,就交互不友好了),但有些场合禁用中断是必要的,在处理第一个中断时,如果发生第二个中断,内核会发生严重的问题。如果内核在禁用中断的情况下,花费过多时间处理一个ISR(中断服务例程),会丢失一些系统正确运作不可必要的中断。并且因为中断随时可能发生,即中断处理程序随时可能执行,必须保证中断处理程序快速执行,这样才能保证尽可能快的恢复中断代码的执行,中断处理很重要,但对系统其它部分而言,中断处理程序尽可能短时间内处理完同样重要。为了解决这些问题,一般把中断处理且为两个部分。中断处理程序是上半部,接收到中断立即开始执行,但只做有严格时限的工作,例如对中断的应答,这些工作是在所有中断被禁止的情况下完成的,能够被允许稍后完成的会推迟到下半部去。此后,在合适的时机,下半部会被开中断执行。
1.2中断控制器
每个能发出中断请求的硬件设备控制器都有一条IRQ(Interrupt Request)输出线,它连接到可编程中断控制器(PIC),是PIC将中断请求转发到CPU的中断输入,外部设备不能直接发出中断,是通过PIC请求中断,所以中断更正确的叫法是IRQ,中断请求。IRQ线是从0开始顺序编号,第一条IRQ线表示成IRQ0,与IRQn关联度额Intel的缺省向量是n+32,这是因为0~31号是Intel保留为异常使用的。X86计算机的CPU为中断只提供了两条外接引脚:NMI和INTR。其中NMI是不可屏蔽中断,它通常用于电源掉电和物理存储器奇偶校验;INTR是可屏蔽中断,可以通过设置中断屏蔽位来进行中断屏蔽,它主要用于接受外部硬件的中断信号,这些信号由中断控制器传递给 CPU。
常见的中断控制器有两种:
- 可编程中断控制器8259A
传统的PIC是由两片8259A风格的外部芯片以“级联”的方式连接在一起。每个芯片可处理多达8个不同的IRQ输入线。因为从PIC的INT输出线连接到主PIC的IRQ2引脚,所以可用IRQ线的个数限制为15 。
- 高级可编程中断控制器(APIC)
8259A只适合单CPU的情况,为了充分挖掘SMP体系结构的并行性,能够把中断传递给系统中的每个CPU至关重要。基于此理由,Intel引入了一种名位I/O高级可编程控制器的新组件,用以替代老式的8259A可编程中断控制器。此外,Intel当前所有的CPU都含有一个本地 APIC。每个本地APIC 都有32位的寄存器,一个内部时钟,一个本地定时设备及为本地中断保留的两条额外的IRQ线 LINT0 和 LINT1。所有本的APIC都连接到一个外部 I/O APIC,形成一个多APIC的系统。
目前大部分单处理器系统都包含一个I/O APIC芯片,可以通过以下两种方式来对这种芯片进行配置:
1) 作为一种标准 8259A方式的外部 PIC 连接到 CPU。本地 APIC 被禁止,两条 LINT0和 LINT1 分别连接到 INTR 和 NMI引脚。
2) 作为一种标准外部I/O APIC。本地APIC被激活,且所有的外部中断都通过I/O APIC接收。
由于Intel公司保留 0~31 号中断向量用来处理异常事件。因此,硬中断必须设在 31 以后,Linux则在实模式下初始化时把 8259A的IRQ0~IRQ15 设在 0x20~0x2f(INT32~INT47)。既然0~31号中断向量被保留,就剩下32~255共224个中断向量可用。这224个中断向量又是怎么分配的呢?除了0x80(SYSCALL_VECTOR)(INT128)用作系统调用总入口外,其他都用在外部硬件中断源上,如可编程中断控制器 8259A的15个IRQ。事实上,当没有定义CONFIG_X86_IO_APIC时,其他223个(除 0x80 外)中断向量,只利用了从32号开始的15个(与 8259A中的15个IRQ相对应),其他208个都空着,具体分布情况请参看下表:
向量范围 | 用途 |
0~19(0x0~0x13) | 非屏蔽中断和异常 |
21~31(0x14~0x1f) | Intel 保留 |
32~127(0x20~0x7f) | 外部中断(IRQ) |
128(0x80) | 系统调用(重点) |
129~238(0x81~0xee) | 外部中断(IRQ) |
239(0xef) | 本地 APIC 时钟中断(重点) |
240~250(0xf0~0xfa) | 由 Linux 留做将来使用 |
251~255(0xfb~0xff) | 处理器间中断(必须是SMP机器) |
1.3中断门
Intel在实现保护模式时,对CPU中断响应机制作了很大修改,中断向量表的表项变成了类似于入口地址加PSW(能够切换CPU运行模式及优先级)并且更复杂的项,称为“门”。只要想切换CPU的运行状态,即优先级别,就需要通过一道门,中断处理也是。X86 CPU中一共有四种门,即任务门、中断门、陷阱门以及调用门。其中调用门不与中断向量表相联系,即中断向量表上只有前三种门。
Linux使用不同的分类:
关于陷阱的:
1) 系统门(system gate)
用户态的进程可以访问的一个Intel陷阱门(门的DPL字段是3)。通过系统门激活三个Linux异常处理程序,向量分别是4,5,128(即int 0x80)。
2) 陷阱门(trap gate)
用户态的进程不能访问的一个Intel陷阱门(门的DPL字段是0)。
关于中断的:
1) 系统中断门:
能够被用户态进程访问的Intel中断门(门的DPL字段是3)。向量是3
2) 中断门:
用户态进程不能访问的Intel中断门(门的DPL字段是0)。所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态。
任务门(task gate):
不能被用户态进程访问的任务门(门的DPL字段是0)。Linux对”Double fault”异常处理程序是通过任务门激活的。
2中断处理内幕
在此,在2.6以后,Linux抽象出一个与平台无关的中断系统,代码放在kernel中。而与各平台相关的部分分散在各部分架构中。先来看一下抽象部分,kernel/irq/Makefile如下:
obj-y := handle.o manage.o spurious.o resend.o chip.o
obj-$(CONFIG_GENERIC_IRQ_PROBE) += autoprobe.o
obj-$(CONFIG_PROC_FS) += proc.o
obj-$(CONFIG_GENERIC_PENDING_IRQ) += migration.o
其中spurious.c是处理伪中断的,resend.c是重发中断的,在此我们关注handle.c manage.c和chip.c。在IA-32上与平台相关的内核代码文件有arch/i386/kernel/irq.c、arch/i386/kernel/apic.c、arch/i386/kernel/entry.S、arch/i386/kernel/i8259.c 以及include/asm-i386/hw_irq.c等。
2.1数据结构
2.1.1中断处理程序描述符irqaction(include/linux/interrupt.h)
struct irqaction {
irqreturn_t (*handler)(int, void *, struct pt_regs *);
unsigned long flags;
cpumask_t mask;
const char *name;
void *dev_id;
struct irqaction *next;
int irq;
struct proc_dir_entry *dir;
};
handler:函数指针。指向设备的中断响应函数,它在系统初始化时被置入,当中断发生时,系统将自动调用该函数。注意,handler函数的原型是特定的—它接收三个参数,并有一个类型为irqreturn_t的返回值。
flags:标志。指明中断类型,如正常中断、快速中断等
mask: 中断屏蔽字
name:中断设备名。这个宁子会被/proc/irq和proc/interrupt文件使用
dev_id: 中断设备id,主要用于共享中断线。当一条中断线是被多个中断设备共享的时候。在中断处理时,内核会按照dev_id逐个调用处理程序,并检查时不时本设备发出的中断。在中断处理程序释放时,dev_id提供唯一的标志信息,以便从共享中断线的诸多中断处理程序中删除指定的一个。
irq:表示要分配的终端号,对某些设备,如传统PC设备上的系统时钟或键盘是预定死的。对于大多数设备来说,要么是通过探测(probe)获取,要不通过编程动态确定。
next:指向下一个irqaction。
static irqreturn_t (*handler)(int, void *dev_id, struct pt_regs *regs);
第一个参数irq是中断线号,dev_id是一个通用指针,用来区分共享同一中断处理程序的多个设备,比如两个一样的硬盘。对于设备而言,设备结构是唯一的,通常把设备结构传递给dev_id。第三个参数regs是一个指向结构的指针,包含处理中断之前处理器的寄存器和状态。返回值的类型是reqreturn_t。中断处理程序可能返回两个特殊值:IRQ_NONE和IRQ_HANDLED。当中断处理程序检测到一个中断,但该中断对应的设备不是注册处理函数时指定的源时,返回IRQ_NONE,正确调用,返回IRQ_HANDLED。使用宏IRQ_RETVAL(x),x为非0,宏返回IRQ_HANDLED;否则,返回IRQ_NONE。利用这些值,内核可以知道设备发出的是否是一种虚假的中断。中断处理程序一般是static,因为它不被别的文件中代码直接调用,static表明只在本代码文件可用。
2.1.2IRQ控制器抽象irq_chip(include/linux/irq.h)
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);
void (*set_affinity)(unsigned int irq, cpumask_t 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);
#ifdef CONFIG_IRQ_RELEASE_METHOD
void (*release)(unsigned int irq, void *dev_id);
#endif
const char *typename;
};
name:用于标识硬件控制器,在IA-32系统上可能的值是“XTPIC”和“IO-APIC”。
startup:指向一个函数,用于第一次初始化一个IRQ。大多情况下,初始化工作仅限于启用该IRQ,因而实际上就是将工作转给enable。
shutdown:完全关闭一个中断源。如果是NULL的话默认为 disable
enable:激活一个IRQ。执行IRQ由禁用状态到启用状态的转换。
disable:禁用IRQ,而shutdown是完全关闭一个中断源
ack:响应一个中断,与中断控制器硬件密切相关。在某些模型中,IRQ请求的到达必须显示的确认,后续的请求才能进行处理。
mask:屏蔽中断源。
mask_ack:确认一个中断,并在接下来屏蔽该中断
unmask:unmask中断源
eoi:在处理中断时需要一个到硬件的回调,由eoi提供。eoi表示end of interrupt,即中断结束
end:调用end标记中断处理在电流层次的约束。如果一个中断在中断处理期间被禁用,那么该函数负责重新启用此类中断
set_type:设置中断触发方式IRQ_TYPE_LEVEL,在x86上没有该方法
大多数控制方法都是重复的,基本上只要有中断响应、中断屏蔽、中断开启、中断触发类型设置等方法就可以满足要求了。其他各种方法基本上和这些相同。
2.1.3IRQ描述符irq_desc(include/linux/irq.h)
对于每个IRQ中断线,Linux都用一个irq_desc_t数据结构来描述,我们把它叫做IRQ描述符,NR_IRQS个IRQ形成一个全局数组irq_desc[],其定义在/include/linux/irq.h中:
struct irq_desc {
void fastcall (*handle_irq)(unsigned int irq,
struct irq_desc *desc,
struct pt_regs *regs);
struct irq_chip *chip;
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 int irqs_unhandled;
spinlock_t lock;
#ifdef CONFIG_SMP
cpumask_t affinity;
unsigned int cpu;
#endif
#if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE)
cpumask_t pending_mask;
unsigned int move_irq; /* need to re-target IRQ dest */
#endif
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
} ____cacheline_aligned;
extern struct irq_desc irq_desc[NR_IRQS];
handle_irq:上层的通用中断处理函数指针,如果未设置则默认为__do_IRQ()。通常针对电平触发或者边沿触发有不同的处理函数。每个中断线可分别设置,该函数负责使用chip中提供的特定于控制器的方法,进行处理终端所必须的一些底层操作。
chip:指向irq_chip的指针,初始化默认是no_irq_chip;
handler_data:附加参数,用于handle_irq,特定于处理程序;
chip_data:平台相关的附加参数,用于chip;
action:指向 struct irqaction 结构组成的队列的头,正常情况下每个irq只有一个操作,因此链表的正常长度是1或0。但是,如果IRQ被两个或多个设备所共享,那么这个队列就有多个操作了
status:中断线状态;
depth:如果启用这条IRQ中断线,depth则为0;如果禁用这条IRQ中断线不止一次,则为一个正数。如果depth等于0,每当调用一次disable_irq( ),该函数就对这个域的值加1,同时该函数就禁用这条IRQ中断线。相反,每当调用enable_irq( )函数时,该函数就对这个域的值减1;如果depth变为0,该函数就启用这条IRQ中断线。
lock:用于串行访问IRQ描述符和PIC的自旋锁
irq_count: 统计IRQ线上发生中断的次数(诊断时使用)
irq_unhandled:对在IRQ线上无法处理的中断进行计数(仅在诊断时使用)。当100000次中断产生时,如果意外中断次数超过99900,内核禁用这条IRQ线。
“____cacheline_aligned”表示这个数据结构的存放按32字节(高速缓存行的大小)进行对齐,以便于将来存放在高速缓存并容易存取
2.1.4数据结构的定义
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned = {
[0 ... NR_IRQS-1] = {
.status = IRQ_DISABLED,
.chip = &no_irq_chip,
.handle_irq = handle_bad_irq,
.depth = 1,
.lock = SPIN_LOCK_UNLOCKED,
#ifdef CONFIG_SMP
.affinity = CPU_MASK_ALL
#endif
}
};
此处,IRQ线默认是禁用的,depth为1,即启用一次即可激活,irq_chip *chip默认是没有中断控制器。handle_bad_irq()函数处理意外和伪中断。no_irq_chip如下:
struct irq_chip no_irq_chip = {
.name = "none",
.startup = noop_ret,
.shutdown = noop,
.enable = noop,
.disable = noop,
.ack = ack_bad,
.end = noop,
};
2.2中断初始化
2.2.1保护模式下的初始化
在调用 start_kernel()函数进行内核初始化时,将会调用 trap_init()和 init_IRQ()函数对中断进行第二次初始化,其中 trap_init()只初始化系统将用到的 CPU 异常处理程序。这部分已经运行于保护模式下。
文件名:init/main.c
asmlinkage void __init start_kernel(void)
{
……
trap_init();//异常初始化
rcu_init();
init_IRQ();//外部中断初始化
}
为了设计方便,为了完成门描述符的设定,Linux提供了两层函数,底层调用_set_gate()来完成共有的操作,高层函数set_intr_gate()(中断门)、set_trap_gate()(陷阱门)、set_system_gate()(系统门)、set_task_gate()(任务门)和 set_system_intr_gate()(系统中断门)函数均调用_set_gate()来完成门描述符的设定。其中_set_gate()完成的动作与在保护模式下的 setup_idt汇编代码设定的任务完全相同。
void __init trap_init(void)
{
#ifdef CONFIG_EISA //如果配置了 EISA
void __iomem *p = ioremap(0x0FFFD9, 4);
if (readl(p) == 'E'+('I'<<8)+('S'<<16)+('A'<<24)) {
EISA_bus = 1;
}
iounmap(p);
#endif
#ifdef CONFIG_X86_LOCAL_APIC //如果配置了本地APIC,就要对其初始化
init_apic_mappings();
#endif
//0~19号中断向量。
set_trap_gate(0,÷_error);
set_intr_gate(1,&debug);
set_intr_gate(2,&nmi);
set_system_intr_gate(3, &int3); /3号,看上面的系统中断门哦,用户态的
set_system_gate(4,&overflow);
set_trap_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_intr_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
#ifdef CONFIG_X86_MCE
set_trap_gate(18,&machine_check);
#endif
set_trap_gate(19,&simd_coprocessor_error);
if (cpu_has_fxsr) {
struct fxsrAlignAssert {
int _:!(offsetof(struct task_struct,
thread.i387.fxsave) & 15);
};
printk(KERN_INFO "Enabling fast FPU save and restore... ");
set_in_cr4(X86_CR4_OSFXSR);
printk("done.\n");
}
if (cpu_has_xmm) {
printk(KERN_INFO "Enabling unmasked SIMD FPU exception "
"support... ");
set_in_cr4(X86_CR4_OSXMMEXCPT);
printk("done.\n");
}
set_system_gate(SYSCALL_VECTOR,&system_call);//设置系统调用,其中SYSCALL定、、//义在include/asm-i386/mach-default/irq_vetors.h里面 #define SYSCALL_VECTOR //0x80
cpu_init();
trap_init_hook();
}
从以上代码可以看出,trap_init()函数只初始化了 0~19 号中断向量及 128(0x80)号中断(20~31 号是系统保留的),那么其他的中断是怎么被初始化的呢?答案就是 init_IRQ()。
arch/i386/kernel/i8259.c
void __init init_IRQ(void)
{
int i;
/* all the set up before the call gates are initialised */
pre_intr_init_hook();
/*
* Cover the whole vector space, no vector can escape
* us. (some of these will be overridden and become
* 'special' SMP interrupts)
*/
//其中include/asm-i386/mach-default/irq_vectors.h 此文件包含i386架构使用所有关于中断向量(不是中断请求)的统计量,比如FIRST_EXTERNAL_VECTOR(0x20),SYSCALL_VECTOR(0x80)和NR_IRQS,FIRST_SYSTEM_VECTOR=0xef(239),NR_VECTORS=256,NR_IRQS的定义在include/asm-i386/mach-default/irq_vectors_limits.h 配置了apic 的话,NR_IRQS=224,如果没有,则NR_IRQS=16,就是8259A的级联。其实在LINUX中物理需要的IRQ为238-32=206 ,其中0x20~0x2f属于8259A管理,0x30~0xee是属于APIC管理。224是OS系统能用到的最大的中断向量个数,除掉了INTEL CPU保留的异常部分。这224个中断向量中有一些是不需要IRQ的(0xef后都不需要,例如0xef是本地APIC时钟中断),不过用224就起到了最大值,没漏掉一个中断向量,对于不需要IRQ的,到时候再来另外处理,也难说将来不用。 NR_IRQ_VECTORS:就是将IRQ分配到中断向量,注意,不用IRQ的到时再作处理,这里取个224最大值,防止了将来的变动,而无须大改。
for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (i >= NR_IRQS)
break;
//排除系统调用,因为在 trap_init()函数中已经初始化了
if (vector != SYSCALL_VECTOR)
//设置中断门,其中 interrupt[i]为中断处理函数,
set_intr_gate(vector, interrupt[i]);
}
/* setup after call gates are initialised (usually add in
* the architecture specific gates)
*/
// 在include/asm-i386/mach-default/setup.c中定义,#ifdef CONFIG_X86_LOCAL_APIC
apic_intr_init();if (!acpi_ioapic)setup_irq(2, &irq2); 8259A必须连接到IRQ2线,用做PIC级联。
intr_init_hook();
/*
* Set the clock to HZ Hz, we already have a valid
* vector now:
*/
//初始化8253 芯片
setup_pit_timer();
/*
* External FPU? Set up irq13 if so, for
* original braindamaged IBM FERR coupling.
*/
if (boot_cpu_data.hard_math && !cpu_has_fpu)
setup_irq(FPU_IRQ, &fpu_irq);
irq_ctx_init(smp_processor_id());
}
调用了 pre_intr_init_hook()函数来初始化 irq_desc_t结构体。具体代码如下
文件名:arch/i386/mach-default/setup.c
void __init pre_intr_init_hook(void)
{
init_ISA_irqs();//初始化irq_desc_t
}
文件(arch\i386\kernel\i8259.c)
void __init init_ISA_irqs (void)
{
int i;
#ifdef CONFIG_X86_LOCAL_APIC
init_bsp_APIC();
#endif
//初始化8259A芯片
init_8259A(0);
//初始化 irq_desc_t 结构体
for (i = 0; i < NR_IRQS; i++) {
irq_desc[i].status = IRQ_DISABLED;
irq_desc[i].action = NULL;
irq_desc[i].depth = 1;
//头16 个(即 IRQ0~IRQ15)中的hw_interrupt_type(即irq_chip)结构体初始化为 i8259A_irq_type,其余的初始化为no_irq_type
if (i < 16) {
irq_desc[i].chip = &i8259A_irq_type;
} else {
irq_desc[i].chip = &no_irq_type;
}
}
}
其中,i8259A_irq_type 的值为(arch\i386\kernel\i8259.c):
static struct hw_interrupt_type i8259A_irq_type = {
.typename = "XT-PIC",
.startup = startup_8259A_irq,
.shutdown = shutdown_8259A_irq,
.enable = enable_8259A_irq,
.disable = disable_8259A_irq,
.ack = mask_and_ack_8259A,
.end = end_8259A_irq,
};
no_irq_type 值为(kernel\irq\handle.c),已经在上面出现过,如下
struct irq_chip no_irq_chip = {
.name = "none",
.startup = noop_ret,
.shutdown = noop,
.enable = noop,
.disable = noop,
.ack = ack_bad,
.end = noop,
};
从以上分析可以看出,hw_interrupt_type、irq_desc_t、irqaction 三个结构具有如下的关系:
在 init_IRQ()函数中,将32~256 号的中断服务例程设置为 interrupt[i]的偏移地址,那么这个 interrupt[i]为何物,其实 interrupt[i]不是中断服务函数,而是所有中断的一个共同操作,具体的中断服务例程是由硬件的驱动程序所设定的。interrupt[i]是在 entry.S中所设定的,由汇编代码实现。它使得从31号以后的中断都跳到了 common_interrupt,到这里怎么区分具体的中断处理程序,难道都执行同一个吗?上面所讲述的 irqaction 三个结构体,与具体的中断有什么联系?请读者继续往下看
2.2.2注册中断处理程序
在 IDT 表的初始化完成之初,每个中断服务队列(irqaction)都是空的。即使开了中断,并且产生了中断,也只不过是让它在 common_interrupt 中空跑一趟。所以,真正的中断服务要到具体硬件设备的初始化程序将其中断服务程序通过 request_irq()向系统“登记”,挂入某个中断服务队列(irqaction)以后才会发生。
中断函数注册信息就保留在irq_desc_t结构中,系统所有的中断信息构成了一个由224个 irq_desc_t结构组成的全局描述符结构数组irq_desc[]。request_irq()的作用就是注册一个中断并启用。如下图:是注册中断处理程序的流程图:
文件名:kernel/irq/manage.c
//irq 为中断请求号,handler 为中断处理程序,irqflags 为中断类型标志,dev_id 用来共享中断号
int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags, const char *devname, void *dev_id)
{
struct irqaction *action;
int retval;
#ifdef CONFIG_LOCKDEP//希望是原子操作,则设置SA_INTERRUPT标志。
/*
* Lockdep wants atomic interrupt handlers:
*/
irqflags |= SA_INTERRUPT;
#endif
/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*/
//如果设置了共享标志,但 dev_id 为空,则出错
if ((irqflags & IRQF_SHARED) && !dev_id)
return -EINVAL;
//中断请求号超过 224,出错
if (irq >= NR_IRQS)
return -EINVAL;
if (irq_desc[irq].status & IRQ_NOREQUEST)
return -EINVAL;
//中断处理函数为空,出错
if (!handler)
return -EINVAL;
//为 irqaction结构体申请空间
action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC);
if (!action)
return -ENOMEM;
//设置参数
action->handler = handler;
action->flags = irqflags;
cpus_clear(action->mask);
action->name = devname;
action->next = NULL;
action->dev_id = dev_id;
select_smp_affinity(irq);
//将该 irqaction 结构体挂入中断服务队列中
retval = setup_irq(irq, action);
if (retval)//如果成功会返回0,不成功,则释放内存
kfree(action);
return retval;
}
2.2.2.1 irqaction->irqflags和 irq_desc->status
在此先总结一下irqflags和status,以及这两个的区别
因为irqflags属于中断处理程序描述符irqaction的标志位,所以它属于中断标志位,代码在/include/linux/interrupt.h中,其中IRQF表示IRQ的flags。
#define IRQF_TRIGGER_NONE 0x00000000//IRQF表示中断请求Flags
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010
/*
* These flags used only by the kernel as part of the
* irq handling routines.
*
* IRQF_DISABLED - keep irqs disabled when calling the action handler
* IRQF_SAMPLE_RANDOM - irq is used to feed the random generator
* IRQF_SHARED - allow sharing the irq among several devices
* IRQF_PROBE_SHARED - set by callers when they expect sharing mismatches to occur
* IRQF_TIMER - Flag to mark this interrupt as timer interrupt
*/
#define IRQF_DISABLED 0x00000020//调用此中断处理程序的时候禁止中断
#define IRQF_SAMPLE_RANDOM 0x00000040//作为随机数的来源
#define IRQF_SHARED 0x00000080//允许共享
#define IRQF_PROBE_SHARED 0x00000100
#define IRQF_TIMER 0x00000200//时钟中断
#define IRQF_PERCPU 0x00000400//是SMP标志
/*
* Migration helpers. Scheduled for removal in 1/2007
* Do not use for new code !
*/
#define SA_INTERRUPT IRQF_DISABLED
#define SA_SAMPLE_RANDOM IRQF_SAMPLE_RANDOM
#define SA_SHIRQ IRQF_SHARED
#define SA_PROBEIRQ IRQF_PROBE_SHARED
#define SA_PERCPU IRQF_PERCPU
#define SA_TRIGGER_LOW IRQF_TRIGGER_LOW
#define SA_TRIGGER_HIGH IRQF_TRIGGER_HIGH
#define SA_TRIGGER_FALLING IRQF_TRIGGER_FALLING
#define SA_TRIGGER_RISING IRQF_TRIGGER_RISING
#define SA_TRIGGER_MASK IRQF_TRIGGER_MASK
SA_INTERRUPT:此标志表明给定的中断处理程序是一个快速中断处理程序。在本地处理器上,快速中断处理程序在禁止所有终端的情况下运行。而默认情况(未设置)情况下,除了正在运行的中断处理程序对应的中断线被屏蔽外,其余所有中断都是激活的。
SA_SHIRQ:此标志表明可以在多个中断处理程序之间共享中断线。(每个中断都有一个编号,若中断号n分配给一个网卡而不是SCSI控制器,那么内核可以区分两个设备。但是遗憾的是,由于特别设计,只有少数的编号可用于硬件中断,所以必须及格设备共享一个编号,在IA-32的处理器上,硬件中断的最大数目是15,这个叫中断共享)这个标志表明可以在多个处理程序之间共享中断线,在同一条线上注册的每个处理程序必须制定这个标志,否则,每条线上只有一个处理程序。
共享的处理程序与非共享的中断处理程序的不同:
l request_irq()的参数flags必须设置SA_SHIRQ标志。
l 对每个注册的中断处理程序来说,dev_id参数必须唯一
l 中断处理程序必须能够区分它的设备是否真的产生了中断,这既需要硬件的支持,也需要中断处理程序中有相应的处理逻辑。
所有共享中断线的驱动程序都必须满足以上要求,只要有任何一个设备没有按规则进行共享,那么中断线就无法共享了。指定SA_SHIRQ标志以调用request_irq()时,只有以下两种情况才可能成功:中断线当前未被注册,或者在该线上的所有已注册处理程序都指定了SA_SHIRQ。
/*
* IRQ line status.
* Bits 0-16 are reserved for the IRQF_* bits in linux/interrupt.h
* IRQ types
*/这些都是与中断电流触发方式有关
#define IRQ_TYPE_NONE 0x00000000 /* Default, unspecified type */
#define IRQ_TYPE_EDGE_RISING 0x00000001 /* Edge rising type */
#define IRQ_TYPE_EDGE_FALLING 0x00000002 /* Edge falling type */
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH 0x00000004 /* Level high type */
#define IRQ_TYPE_LEVEL_LOW 0x00000008 /* Level low type */
#define IRQ_TYPE_SENSE_MASK 0x0000000f /* Mask of the above */
#define IRQ_TYPE_PROBE 0x00000010 /* Probing in progress */
/* Internal flags */中断线的状态
#define IRQ_INPROGRESS 0x00010000 /* IRQ handler active - do not enter! */
#define IRQ_DISABLED 0x00020000 /* IRQ disabled - do not enter! */
#define IRQ_PENDING 0x00040000 /* IRQ pending - replay on enable */
#define IRQ_REPLAY 0x00080000 /* IRQ has been replayed but not acked yet */
#define IRQ_AUTODETECT 0x00100000 /* IRQ is being autodetected */
#define IRQ_WAITING 0x00200000 /* IRQ not yet seen - for autodetection */
#define IRQ_LEVEL 0x00400000 /* IRQ level triggered */
#define IRQ_MASKED 0x00800000 /* IRQ masked - shouldn't be seen again */
#define IRQ_PER_CPU 0x01000000 /* IRQ is per CPU */
#ifdef CONFIG_IRQ_PER_CPU
# define CHECK_IRQ_PER_CPU(var) ((var) & IRQ_PER_CPU)
#else
# define CHECK_IRQ_PER_CPU(var) 0
#endif
#define IRQ_NOPROBE 0x02000000 /* IRQ is not valid for probing */
#define IRQ_NOREQUEST 0x04000000 /* IRQ cannot be requested */
#define IRQ_NOAUTOEN 0x08000000 /* IRQ will not be enabled on request irq
#define IRQ_DELAYED_DISABLE 0x10000000 /* IRQ disable (masking) happens delayed.
#define IRQ_WAKEUP 0x20000000 /* IRQ triggers system wakeup */
IRQ_INPROGRESS IRQ的一个处理程序正在执行
IRQ_DISABLED 禁用IRQ线
IRQ_PENDING 悬挂,其实就是有中断到达,已对PIC做出应答,但由于某种情况未处理,先标记上
IRQ_REPLAY IRQ线已被禁用,但是前一个出现的IRQ还没对PIC做出应答
IRQ_AUTODETECT 内核在执行硬件设备探测时使用IRQ线
IRQ_WAITING 内核在执行硬件设备探测时使用IRQ线,此外,相应的中断还没有产生
IRQ_MASKED 屏蔽此中断线上的中断请求
2.2.2.1 setup( )函数
在request_irq()中调用函数setup_irq()来完成建立中断的具体操作。在全局中断描述符结构数组(irq_destc[])中,如果该中断号被使用,并且新旧中断都设置了共享,就把新中断加载就中断的后面;如果中断号没有被使用,就直接加在中断描述符结构数组中,并设置中断使能,具体函数分析如下:
/*
* Internal function to register an irqaction - typically used to
* allocate special interrupts that are part of the architecture.
*/
int setup_irq(unsigned int irq, struct irqaction *new)
{
//从全局中断描述符结构数组中得到对应中断号的中断描述结构
struct irq_desc *desc = irq_desc + irq;
struct irqaction *old, **p;
unsigned long flags;
int shared = 0;
if (irq >= NR_IRQS)
return -EINVAL;
if (desc->chip == &no_irq_chip)
return -ENOSYS;
/*
* Some drivers like serial.c use request_irq() heavily,
* so we have to be careful not to interfere with a
* running system.
*/
if (new->flags & IRQF_SAMPLE_RANDOM) {
rand_initialize_irq(irq);
}
//下面这段代码必须原子执行,不能被打断
spin_lock_irqsave(&desc->lock, flags);
p = &desc->action;
old = *p;
if (old) {
/*双方声明为共享中断且触发类型一致*/
if (!((old->flags & new->flags) & IRQF_SHARED) ||
((old->flags ^ new->flags) & IRQF_TRIGGER_MASK))
goto mismatch;
#if defined(CONFIG_IRQ_PER_CPU)
/* All handlers must agree on per-cpuness */
if ((old->flags & IRQF_PERCPU) !=
(new->flags & IRQF_PERCPU))
goto mismatch;
#endif
/* 找到IRQ队列的尾部,并取得最后一个 irqaction 的指针赋值给 p
do {
p = &old->next;
old = *p;
} while (old);
shared = 1;
}
//将新中断挂在 p 上
*p = new;
#if defined(CONFIG_IRQ_PER_CPU)
if (new->flags & IRQF_PERCPU)
desc->status |= IRQ_PER_CPU;
#endif
//如果该中断没有设置共享,即第一此给该中断号设置中断处理程序,则设置该 irq_desc_t
//结构体的值
if (!shared) {
irq_chip_set_defaults(desc->chip);
/* 若指定了触发类型则进行配置*/在i386 架构下没有设置
if (new->flags & IRQF_TRIGGER_MASK) {
if (desc->chip && desc->chip->set_type)
desc->chip->set_type(irq,
new->flags & IRQF_TRIGGER_MASK);
else
printk(KERN_WARNING "No IRQF_TRIGGER set_type "
"function for IRQ %d (%s)\n", irq,
desc->chip ? desc->chip->name :
"unknown");
} else
compat_irq_chip_set_default_handler(desc);//只是检测电流处理程序是否被重写
desc->status &= ~(IRQ_AUTODETECT | IRQ_WAITING |
IRQ_INPROGRESS);
if (!(desc->status & IRQ_NOAUTOEN)) {
desc->depth = 0;
desc->status &= ~IRQ_DISABLED;
//中断使能
if (desc->chip->startup)
desc->chip->startup(irq);
else
desc->chip->enable(irq);
} else
/* Undo nested disables: */
desc->depth = 1;
}
spin_unlock_irqrestore(&desc->lock, flags);
//以下两个函数在/proc/irq文件创建一个与中断相对应的项
new->irq = irq;
register_irq_proc(irq); //更新该中断的proc记录
new->dir = NULL;
register_handler_proc(irq, new);
return 0;
mismatch:
spin_unlock_irqrestore(&desc->lock, flags);
if (!(new->flags & IRQF_PROBE_SHARED)) {
printk(KERN_ERR "IRQ handler type mismatch for IRQ %d\n", irq);
dump_stack();
}
return -EBUSY;
}
在内核中,设备驱动程序一般都要通过 request_irq()向系统登记其中断服务程序。有一点很重要,初始化硬件和注册中断处理程序的顺序必须正确,以防止中断处理程序在设备初始化完成之前就开始执行。注意 request_irq()函数可能睡眠,因此,不能在中断上下文或其他不允许阻塞的代码中调用该函数。
当所有的硬件设备初始化完成后,IDT 表也就完成了所有的初始化动作,现在就可以开始响应中断,并进行中断处理了。
2.2.2.1 free_irq( )函数
kernel/irq/manage.c
void free_irq(unsigned int irq, void *dev_id)
{
struct irq_desc *desc;
struct irqaction **p;
unsigned long flags;
WARN_ON(in_interrupt());
if (irq >= NR_IRQS)
return;
//找到中断号对应的irq_desc_t
desc = irq_desc + irq;
spin_lock_irqsave(&desc->lock, flags);
p = &desc->action;//irqaction链表头
for (;;) {//直到找到对应的dev_id才结束
struct irqaction *action = *p;
//如果还没到irqaction链表的结尾
if (action) {
struct irqaction **pp = p;
p = &action->next;
if (action->dev_id != dev_id)//如果还不是对应的dev_id,那么继续循环
continue;
/* Found it - now remove it from the list of entries */
*pp = action->next;
/* Currently used only by UML, might disappear one day.*/
#ifdef CONFIG_IRQ_RELEASE_METHOD
if (desc->chip->release)
desc->chip->release(irq, dev_id);//释放对应的中断处理程序
#endif
//如果链表里面已经没有了irqaction的话,那么禁用并关闭中断线
if (!desc->action) {
desc->status |= IRQ_DISABLED;
if (desc->chip->shutdown)
desc->chip->shutdown(irq);
else
desc->chip->disable(irq);
}
spin_unlock_irqrestore(&desc->lock, flags);
unregister_handler_proc(irq, action);
/* Make sure it's not being used on another CPU */
synchronize_irq(irq);
kfree(action);
return;
}
printk(KERN_ERR "Trying to free already-free IRQ %d\n", irq);
spin_unlock_irqrestore(&desc->lock, flags);
return;
}
}
free_irq()函数做的就是找到对应的中断处理程序,然后删掉它的中断处理程序,如果在这条中断线上已经没有中断的话,那么禁用这条中断线。
2.3中断电流处理
2.3.1设置控制器硬件
文件include/linux/irq.h中
int set_irq_chip(unsigned int irq, struct irq_chip *chip);
void set_irq_handler(unsigned int irq, irq_flow_handler_t handle);
void set_irq_chained_handler(unsigned int irq, irq_flow_handler_t handle)
void set_irq_chip_and_handler(unsigned int irq, struct irq_chip *chip,irq_flow_handler_t handle);
void set_irq_chip_and_handler_name(unsigned int irq, struct irq_chip *chip,irq_flow_handler_t handle, const char *name);
static inline void
set_irq_handler(unsigned int irq,
void fastcall (*handle)(unsigned int, struct irq_desc *,
struct pt_regs *))
{
__set_irq_handler(irq, handle, 0);
}
static inline void
set_irq_chained_handler(unsigned int irq,
void fastcall (*handle)(unsigned int, struct irq_desc *,
struct pt_regs *))
{
__set_irq_handler(irq, handle, 1);
}
set_irq_chip将一个IRQ芯片以irq_chip实例的形式关联到某个特定的中断,除了从irq_desc选取适当的成员并设置chip指针之外,如果没有提供特定于芯片的实现,该函数将设置默认的处理程序,如果chip指针为NULL,将使用通用的irq_chip实例no_irq_chip,该实现只提供了空操作。源码如下:
int set_irq_chip(unsigned int irq, struct irq_chip *chip)
{
struct irq_desc *desc;
unsigned long flags;
if (irq >= NR_IRQS) {
printk(KERN_ERR "Trying to install chip for IRQ%d\n", irq);
WARN_ON(1);
return -EINVAL;
}
if (!chip)//如果chip为NULL,那么使用通用的no_irq_chip
chip = &no_irq_chip;
desc = irq_desc + irq;
spin_lock_irqsave(&desc->lock, flags);
irq_chip_set_defaults(chip);
desc->chip = chip;
spin_unlock_irqrestore(&desc->lock, flags);
return 0;
}
kernel/irq/chip.c中
void
__set_irq_handler(unsigned int irq,
void fastcall (*handle)(unsigned int, irq_desc_t *,
struct pt_regs *),
int is_chained)
{
struct irq_desc *desc;
unsigned long flags;
if (irq >= NR_IRQS) {
printk(KERN_ERR
"Trying to install type control for IRQ%d\n", irq);
return;
}
//找出对应的irq_desc_t
desc = irq_desc + irq;
//如果handle为NULL
if (!handle)
handle = handle_bad_irq;
//如果chip未指定,还是默认的no_irq_chip
if (desc->chip == &no_irq_chip) {
printk(KERN_WARNING "Trying to install %sinterrupt handler "
"for IRQ%d\n", is_chained ? "chained " : " ", irq);
desc->chip = &dummy_irq_chip;
}
spin_lock_irqsave(&desc->lock, flags);
//下面的需要原子操作
//如果handle未指定,还是默认的handle_bad_irq,禁用它
if (handle == handle_bad_irq) {
if (desc->chip != &no_irq_chip) {
desc->chip->mask(irq);
desc->chip->ack(irq);
}
desc->status |= IRQ_DISABLED;
desc->depth = 1;
}
desc->handle_irq = handle;//设置irq_desc_t的handle选项
//如果这个是非共享的中断线
if (handle != handle_bad_irq && is_chained) {
desc->status &= ~IRQ_DISABLED;
desc->status |= IRQ_NOREQUEST | IRQ_NOPROBE;
desc->depth = 0;
desc->chip->unmask(irq);
}
spin_unlock_irqrestore(&desc->lock, flags);
}
2.3.1电流处理
不同的硬件需要不同的电流处理方式,例如,边沿触发和电平触发需要不同的处理。内核对各种类型提供了几个默认的电流处理程序,它们有一个共同点,每个电流处理程序在工作结束后,都要负责调用高层ISR。Handle_IRQ_event()负责激活高层的处理程序。
现在硬件大部分采用的是边沿触发中断默认处理程序是handle_edge_irq()。
在处理边沿触发中断的IRQ时无须屏蔽,这使得SMP系统:在一个CPU上处理一个IRQ时,另一个同样编号的IRQ可以出现在另一个CPU上,使得两个CPU可能同时调用一个IRQ中断处理程序。内核想避免这种情况:在handle_edge_irq()中,如果设置了IRQ_INPROGRESS标志,说明该IRQ中断处理程序正在被调用,通过设置IRQ_PENDING标志,内核记录还有一个IRQ需要处理。屏蔽该IRQ并通过mask_and_ack()向控制器发送一个确认后,处理过程放弃。注意:如果IRQ被禁用,或没有可用的ISR处理程序,都会放弃处理。
代码流程图如下:
void fastcall
handle_edge_irq(unsigned int irq, struct irq_desc *desc, struct pt_regs *regs)
{
const unsigned int cpu = smp_processor_id();
spin_lock(&desc->lock);
//设置一些标志
desc->status &= ~(IRQ_REPLAY | IRQ_WAITING);
/*
如果没有中断处理程序,或者当前irq在运行或者被禁用,那么屏蔽并设置IRQ_PENEDING标志位,并取消处理。
*/
if (unlikely((desc->status & (IRQ_INPROGRESS | IRQ_DISABLED)) ||
!desc->action)) {
desc->status |= (IRQ_PENDING | IRQ_MASKED);
mask_ack_irq(desc, irq);
goto out_unlock;
}
kstat_cpu(cpu).irqs[irq]++;
//ack应答中断
desc->chip->ack(irq);
//标识中断在处理中
desc->status |= IRQ_INPROGRESS;
do {
struct irqaction *action = desc->action;
irqreturn_t action_ret;
if (unlikely(!action)) {//unlickly只是GCC对代码进行优化,没什么含义
desc->chip->mask(irq);
goto out_unlock;
}
//如果当我们处理中断的时候,另一个中断请求到了,我们需要屏蔽它。现在要接触对irq //的屏蔽,如果它在此期间没有被禁用的话。
if (unlikely((desc->status &
(IRQ_PENDING | IRQ_MASKED | IRQ_DISABLED)) ==
(IRQ_PENDING | IRQ_MASKED))) {
desc->chip->unmask(irq);
desc->status &= ~IRQ_MASKED;
}
desc->status &= ~IRQ_PENDING;
spin_unlock(&desc->lock);
action_ret = handle_IRQ_event(irq, regs, action);//调用高层ISR
if (!noirqdebug)
note_interrupt(irq, desc, action_ret, regs);
spin_lock(&desc->lock);
} while ((desc->status & (IRQ_PENDING | IRQ_DISABLED)) == IRQ_PENDING);
desc->status &= ~IRQ_INPROGRESS;
out_unlock:
spin_unlock(&desc->lock);
}
IRQ的处理时在一个循环中的,假定我们刚好出于handle_IRQ_event()之后的位置上,在第一个IRQ的ISR处理程序运行时,可能同时有第二个IRQ请求发送过来,会通过设置IRQ_PENDING表示,如果设置了该标志,那么有另一个IRQ正在等待处理。循环将从头再次开始。但这种情况下IRQ已经被屏蔽,那么必须用chip->unmask接触IRQ的屏蔽,并清除IRQ_MASKED标志。这确保在handle_IRQ_event执行期间只能发生一个中断。
2.4 IRQ处理
在注册了IRQ处理程序后,每次发生中断时将执行处理程序例程。每次先切换到核心态,它是基于每个中断之后由处理器自动执行的汇编语言代码,实现在arch/i386/kernel/entry.s中,只有必要的操作会直接在汇编语言代码中执行,内核试图尽快到额返回到C代码。在C语言中调用函数时,大多数平台上,控制流会传递到C函数do_IRQ。arch/i386/kernel/irq.c中,do_IRQ做了两件事,一是切换内核栈,二是调用了__do_IRQ()。
fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
{
//得到全局描述符结构数组的下标
struct irq_desc *desc = irq_desc + irq;
struct irqaction *action;
unsigned int status;
kstat_this_cpu.irqs[irq]++;
//因为 irq_desc[]数组中,每个 CPU 占一个元素,这里的 desc 就是本 CPU 数据,所以此处不需要加锁。
if (CHECK_IRQ_PER_CPU(desc->status)) {
irqreturn_t action_ret;
/*
* No locking required for CPU-local interrupts:
*/
//中断处理器在将中断请求“上报”到 CPU后,期待 CPU给它一个确认(ACK) ,表示“我
//已经在处理”,也就是给8259A或 APIC 芯片一个应答信号
if (desc->chip->ack)
desc->chip->ack(irq);
//该函数将会调用中断处理程序
action_ret = handle_IRQ_event(irq, regs, desc->action);
//对中断控制器执行一次“结束中断服务操作”
desc->chip->end(irq);
return 1;
}
//对于多 CPU的情况,进行加锁处理
spin_lock(&desc->lock);
if (desc->chip->ack)
desc->chip->ack(irq);
/*
* REPLAY is when Linux resends an IRQ that was dropped earlier
* WAITING is used by probe to mark irqs that are being tested
*/
//从desc->status中清除清IRQ_REPLAY和 IRQ_WAITING 位,同时设置 IRQ_PENDING 位
status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
status |= IRQ_PENDING; /* we _want_ to handle it */
/*
* If the IRQ is disabled for whatever reason, we cannot
* use the action we have.
*/
action = NULL;
//如果设置了IRQ_DISABLED位 ,或者 IRQ_INPROGRESS 位,即当前中断被关闭
//(IRQ_DISABLED 为 1 表示关闭),或者已经在其他 CPU 上运行(IRQ_INPROGRESS 为
// 1 表示运行),则退出(此时 action = NULL)。反之执行 if语句内的内容。
if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
//取得中断服务程序队列头的指针
action = desc->action;
//清除 IRQ_PENDING 位,即设置 IRQ_PENDING 位为 0,同时设置 IRQ_INPROGRESS 位为1。//其中IRQ_INPROGRESS 位是为多处理器设置的,表示我正在执行,你就不要执行了。对于 //IRQ_PENDING 位的作用下面就可以看到
status &= ~IRQ_PENDING; /* we commit to handling */
status |= IRQ_INPROGRESS; /* we are handling it */
}
//保存当前中断号的状态
desc->status = status;
/*
* If there is no IRQ handler or it was disabled, exit early.
* Since we set PENDING, if another processor is handling
* a different instance of this same irq, the other processor
* will take care of it.
*/
//如果 action队列为空(还包含上面的两种情况),做相关处理后,将退出
if (unlikely(!action))
goto out;
/*
* Edge triggered interrupts need to remember
* pending events.
* This applies to any hw interrupts that allow a second
* instance of the same irq to arrive while we are in do_IRQ
* or in the handler. But the code here only handles the _second_
* instance of the irq, not the third or fourth. So it is mostly
* useful for irq hardware that does not mask cleanly in an
* SMP environment.
*/
for (;;) {
irqreturn_t action_ret;
spin_unlock(&desc->lock);
//执行中断处理程序
action_ret = handle_IRQ_event(irq, regs, action);
spin_lock(&desc->lock);
if (!noirqdebug)
note_interrupt(irq, desc, action_ret, regs);
//在进入 for循环时,IRQ_PENDING 位已经为 0。如果该位此时还为 0,结束 for循环。//如 果为 1,表示该中断又发生了一次,回到循环再调用中断服务程序一次。这样就将本//来可能发生在同一通道上的中断嵌套化解为一个循环。
if (likely(!(desc->status & IRQ_PENDING)))
break;
//清除 IRQ_PENDING 位,即设置 IRQ_PENDING 位为 0。
desc->status &= ~IRQ_PENDING;
}
//清 IRQ_INPROGRESS 位,表示可以继续响应下一次中断服务
desc->status &= ~IRQ_INPROGRESS;
out:
/*
* The ->end() handler has to deal with interrupts which got
* disabled while the handler was running.
*/
//对中断控制器执行一次“结束中断服务操作”
desc->chip->end(irq);
spin_unlock(&desc->lock);
return 1;
}
从以上分析可知,如果中断关闭(IRQ_DISABLED,含义请参看表 5),或者中断处理程序正在执行(IRQ_INPROGRESS) ,或者中断服务程序队列(action)为空,都将无法往下执行。但此时 IRQ_PENDING 是置位的。所以在以下两种情况下,程序看到此标志位置位时,将补上一次中断服务。
1. 如果 action队列为空
2. 在多 CPU 中,一个 CPU 正在执行中断服务,而另一个 CPU 又进入了 do_IRQ(),这时候由于 IRQ_INPROGRESS 位为 1,程序退出。
在这两种情况下,IRQ_PENDING 标志位都为 1,都将会使程序再次执行相应的中断处理程序。大家可能会感到疑问的是对于第二种情况,不是进入__do_irq()时,已经采用了自旋锁吗?怎么会出现第二种情况呢?请仔细看代码,在执行中断处理程序前,已经释放自旋锁了,所以出现了第二种情况。
我们不难看出 IRQ_PENDING 标志的作用,就是用于判断中断是否又发生了一次。从以上代码可以看出,修改 desc->status的值是在加锁的情况下进行的,主要是基于 SMP 的考虑。
代码中的 handle_IRQ_event()函数才是真正的执行中断处理程序代码。
文件名:kernel\irq\handle.c
irqreturn_t handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
struct irqaction *action)
{
irqreturn_t ret, retval = IRQ_NONE;
unsigned int status = 0;
handle_dynamic_tick(action);
//一般来说,中断服务程序都是在关闭中断(排除 NMI 情况)的条件下执行的,这也是 CPU
//在穿越中断门时自动关闭中断的原因。但是在调用 request_irq()函数注册中断服务程//序时,允许设置 SA_INTERRUPT 位为 0,表示该服务程序应该在开中断的情况下执行。
if (!(action->flags & IRQF_DISABLED))
local_irq_enable_in_hardirq();
//依次调用 action 队列中所有的中断服务子程序。其实这个过程并不需要很长的时间。因//为每个具体的中断服务程序中都会一开始检查各自的中断源,一般是读相应设备的中断状//态寄存器,看是否有来自该设备的中断请求,如果没有则马上退出,这个过程一般只需要//几个指令;其次,每个 action 队列的中断服务程序的数量一般也不会很大。所以,不会//有显著的影响。
do {
ret = action->handler(irq, action->dev_id, regs);
if (ret == IRQ_HANDLED)
status |= action->flags;
retval |= ret;
action = action->next;
} while (action);
if (status & IRQF_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
local_irq_disable();
return retval;
}
此时,执行完成后,将返回到 do_IRQ()函数,执行 irq_exit()里面的代码。这个函数主要是还原 preempt_count 的值,并执行软中断。执行完软中断后,将会退栈处理(对应前面的切换内核栈)。
3 附录
3.1中断控制
Linux内核提供了一组接口用于操作机器上的中断状态,这些接口为我们提供了能够尽职当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力。
一般来说,控制中断系统的原因归根结底是需要提供同步。通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码,禁止中断还可以禁止内核抢占。然而,不管是禁止中断还是禁止内核抢占,都没有提供任何保护机制来防止来自其他处理器的并发访问。Linux支持多处理器。因此,内核代码一般都需要获取某种锁,防止来自其他处理器对共享数据的并发访问。获取这些锁的同时伴随着禁止本地中断。锁提供保护机制,防止来自其他处理器的并发访问,而禁止中断提供保护机制,则是防止来自其他中断处理程序的并发访问。
禁止本地CPU中断是确保一组内核语句被当作一个临界区处理的主要机制。这个机制的意义是:即使当硬件设备产生了一个IRQ信号时,中断禁止也让内核控制路径继续执行,因此,这就提供了一种有效的方式,确保内核控制路径中的一些中断处理程序能访问的数据结构也受到保护。
宏local_irq_disable()使用cli汇编语言指令关闭本地CPU上的中断(x86体系的禁止本地中断位于include/linux/Irqflags.h):
#define local_irq_disable() \
do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)
include/asm-i386/Irqflags.h
static inline void raw_local_irq_disable(void)
{
__asm__ __volatile__("cli" : : : "memory");
}
宏local_irq_enable()使用sti汇编语言指令打开被关闭的中断(同样在include/linux/Irqflags.h):
#define local_irq_enable() \
do { trace_hardirqs_on(); raw_local_irq_enable(); } while (0)
include/asm-i386/Irqflags.h
static inline void raw_local_irq_enable(void)
{
__asm__ __volatile__("sti" : : : "memory");
}
汇编语言指令cli和sti分别清除和设置eflags控制寄存器的IF标志。如果eflags寄存器的IF标志被清0,宏irqs_disabled()产生等于1的值;如果IF标志被设置,该宏也产生为1的值。
有时候,需要防止中断处理程序对eflags的寄存器内容进行破坏,需要另一种机制来保护临界资源。保存和恢复eflags的内容是分别通过宏 local_irq_save和local_irq_restore来实现的。local_irq_save宏把eflags寄存器的内容拷贝到一个局部 变量中,随后用cli汇编语言指令把IF标志清0(同样在include/linux/Irqflags.h):
#define local_irq_save(flags) \
do { raw_local_irq_save(flags); trace_hardirqs_off(); } while (0)
include/asm-i386/Irqflags.h
#define raw_local_irq_save(flags) \
do { (flags) = __raw_local_irq_save(); } while (0)
static inline unsigned long __raw_local_irq_save(void)
{
unsigned long flags = __raw_local_save_flags();
raw_local_irq_disable();
return flags;
}
static inline unsigned long __raw_local_save_flags(void)
{
unsigned long flags;
__asm__ __volatile__(
"pushfl ; popl %0"
: "=g" (flags)
: /* no input */
);
return flags;
}
在临界区的末尾,宏local_irq_restore恢复eflags原来的内容:
#define local_irq_restore(flags) \
do { \
if (raw_irqs_disabled_flags(flags)) { \
raw_local_irq_restore(flags); \
trace_hardirqs_off(); \
} else { \
trace_hardirqs_on(); \
raw_local_irq_restore(flags); \
} \
} while (0)
static inline int raw_irqs_disabled_flags(unsigned long flags)
{
return !(flags & (1 << 9));
}
static inline void raw_local_irq_restore(unsigned long flags)
{
__asm__ __volatile__(
"pushl %0 ; popfl"
: /* no output */
:"g" (flags)
:"memory", "cc"
);
}
因此,只是在这个控制路径发出cli汇编语言指令之前,即raw_local_irq_disable()之前中断被激活的情况下,中断才处于打开状态。
禁止本地CPU中断是确保一组内核语句被当作一个临界区处理的主要机制。这个机制的意义是:即使当硬件设备产生了一个IRQ信号时,中断禁止也让内核控制路径继续执行,因此,这就提供了一种有效的方式,确保内核控制路径中的一些中断处理程序能访问的数据结构也受到保护。
1 禁止本地中断
然而,禁止本地中断并不保护运行在另一个CPU上的中断处理程序对该数据结构的并发访问,因此,在多处理器系统上,禁止本地中断经常与自旋锁结合使用。
宏local_irq_disable()使用cli汇编语言指令关闭本地CPU上的中断(x86体系的禁止本地中断位于include/linux/Irqflags.h):
#define local_irq_disable() /
do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)
//include/asm-i386/Irqflags.h
static inline void raw_local_irq_disable(void)
{
__asm__ __volatile__("cli" : : : "memory");
}
宏local_irq_enable()使用sti汇编语言指令打开被关闭的中断(同样在include/linux/Irqflags.h):
#define local_irq_enable() /
do { trace_hardirqs_on(); raw_local_irq_enable(); } while (0)
//include/asm-i386/Irqflags.h
static inline void raw_local_irq_enable(void)
{
__asm__ __volatile__("sti" : : : "memory");
}
汇编语言指令cli和sti分别清除和设置eflags控制寄存器的IF标志。如果eflags寄存器的IF标志被清0,宏irqs_disabled()产生等于1的值;如果IF标志被设置,该宏也产生为1的值。
有时候,需要防止中断处理程序对eflags的寄存器内容进行破坏,需要另一种机制来保护临界资源。保存和恢复eflags的内容是分别通过宏local_irq_save和local_irq_restore来实现的。local_irq_save宏把eflags寄存器的内容拷贝到一个局部变量中,随后用cli汇编语言指令把IF标志清0(同样在include/linux/Irqflags.h):
#define local_irq_save(flags) /
do { raw_local_irq_save(flags); trace_hardirqs_off(); } while (0)
//include/asm-i386/Irqflags.h
#define raw_local_irq_save(flags) /
do { (flags) = __raw_local_irq_save(); } while (0)
static inline unsigned long __raw_local_irq_save(void)
{
unsigned long flags = __raw_local_save_flags();
raw_local_irq_disable();
return flags;
}
static inline unsigned long __raw_local_save_flags(void)
{
unsigned long flags;
__asm__ __volatile__(
"pushfl ; popl %0"
: "=g" (flags)
: /* no input */
);
return flags;
}
在临界区的末尾,宏local_irq_restore恢复eflags原来的内容:
#define local_irq_restore(flags) /
do { /
if (raw_irqs_disabled_flags(flags)) { /
raw_local_irq_restore(flags); /
trace_hardirqs_off(); /
} else { /
trace_hardirqs_on(); /
raw_local_irq_restore(flags); /
} /
} while (0)
static inline int raw_irqs_disabled_flags(unsigned long flags)
{
return !(flags & (1 << 9));
}
static inline void raw_local_irq_restore(unsigned long flags)
{
__asm__ __volatile__(
"pushl %0 ; popfl"
: /* no output */
:"g" (flags)
:"memory", "cc"
);
}
因此,只是在这个控制路径发出cli汇编语言指令之前,即raw_local_irq_disable()之前中断被激活的情况下,中断才处于打开状态。
2 禁止下半部(可延迟函数)
在中断处理专题的“下半部分”一节,我们说明了可延迟函数可能在不可预知的时间执行(实际上是在硬件中断处理程序结束时)。因此,必须保护可延迟函数访问的数据结构使其避免竞争条件。
禁止可延迟函数在单个CPU上执行的一种简单方式就是禁止在那个CPU上的中断。因为没有中断处理程序被激活,因此,软中断操作就不能异步地开始。
然而,内核有时需要只禁止可延迟函数而不禁止中断。这种情况是需要通过操纵当前thread_info描述符preempt_count字段中存放的软中断计数器,可以在本地CPU上激活或禁止可延迟函数。
回忆一下,如果软中断计数器是正数,do_softirq()函数就不会执行软中断,而且,因为tasklet在软中断之前被执行,把这个计数器设置为大于0的值,由此禁止了在给定CPU上的所有可延迟函数和软中断的执行。
宏local_bh_disable给本地CPU的软中断计数器加1,而函数local_bh_enable()从本地CPU的软中断计数器中减掉1。
void local_bh_disable(void) //kernel/Softirq.c
{
__local_bh_disable((unsigned long)__builtin_return_address(0));
}
static inline void __local_bh_disable(unsigned long ip)
{
add_preempt_count(SOFTIRQ_OFFSET);
barrier();
}
#define SOFTIRQ_OFFSET (1UL << SOFTIRQ_SHIFT) //0x00000100 -> 512
#define SOFTIRQ_SHIFT (PREEMPT_SHIFT + PREEMPT_BITS)
#define PREEMPT_SHIFT 0
#define PREEMPT_BITS 8
# define add_preempt_count(val) do { preempt_count() += (val); } while (0)
内核因此能使用几个嵌套的local_bh_disable调用,只有宏local_bh_enable与第一个local_bh_disable调用相匹配,可延迟函数才再次被激活(do_softirq):
void local_bh_enable(void)
{
#ifdef CONFIG_TRACE_IRQFLAGS
unsigned long flags;
WARN_ON_ONCE(in_irq());
#endif
WARN_ON_ONCE(irqs_disabled());
#ifdef CONFIG_TRACE_IRQFLAGS
local_irq_save(flags);
#endif
/*
* Are softirqs going to be turned on now:
*/
if (softirq_count() == SOFTIRQ_OFFSET)
trace_softirqs_on((unsigned long)__builtin_return_address(0));
/*
* Keep preemption disabled until we are done with
* softirq processing:
*/
sub_preempt_count(SOFTIRQ_OFFSET - 1);
if (unlikely(!in_interrupt() && local_softirq_pending()))
do_softirq();
dec_preempt_count();
#ifdef CONFIG_TRACE_IRQFLAGS
local_irq_restore(flags);
#endif
preempt_check_resched();
}
递减软中断计数器之后,local_bh_enable()执行两个重要的操作以有助于保证适时地执行长时间等待的线程:
1. 检查本地CPU的preempt_count字段中硬中断计数器和软中断计数器,如果这两个计数器的值都等于0而且有挂起的软中断要执行,就调用do_softirq()来激活这些软中断(见中断专题“下半部”博文)。
2. 调用preempt_check_resched函数检查本地CPU的TIF_NEED_RESCHED标志是否被设置,如果是,说明进程切换请求是挂起的,因此调用preempt_schedule()函数(参见专题前面的“内核抢占”博文)。
#define preempt_check_resched() /
do { /
if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) /
preempt_schedule(); /
} while (0)
linux中断--中断原理分析
中断之原理篇
前言:
中断是计算机发展中一个重要的技术,它的出现很大程度上解放了CPU,提高了CPU的执行效率。
在中断出现之前,CPU对IO采用的是轮询的方式进行服务,这使的CPU纠结在某一个IO上,一直在等待它的响应,如果它不响应,CPU就在原地一直的等下去。这样就导致了其他IO口也在等待CPU的服务,如果某个IO出现了important or emergency affairs,CPU也抽不出身去响应这个IO。
为了解决这个纠结的问题就------>出现了中断
中断控制的主要优点是只有在IO接口需要服务时才去响应它,使得CPU很淡定的做它自己的事情,只有IO口有需求的时候才去响应它。同时中断中也设计了中断优先级,来处理一些很紧急的事件。
一.中断的基本知识
1.中断的概念:
所谓中断,是指CPU在正常运行程序时,由于程序的预先安排或内外部事件,引起CPU中断正在运行的程序,而转到发生中断事件程序中。这些引起程序中断的事件称为中断源。
其实从物理学的角度看,中断是一种电信号,由硬件设备产生,并直接送入中断控制器(如 8259A)的输入引脚上,然后再由中断控制器向处理器发送相应的信号。处理器一经检测到该信号,便中断自己当前正在处理的工作,转而去处理中断。此后,处理器会通知 OS 已经产生中断。这样,OS 就可以对这个中断进行适当的处理。不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标识,这些值通常被称为中断请求线。
2.那么当产生一个中断时,CPU是如何识别的呢?
在Intel X86中可以支持256中向量中断,为了使处理器能使别每种中断源,给它们进行了编号----->叫做中断向量
3.这些中断向量在Linux中是如何分配的:
编号0~31的向量对应于异常和非屏蔽中断
编号32~47的向量(即由IO设备引起的中断)分配给屏蔽中断。
编号48~255的向量用来标示软中断。Linux用其中的128或0x80来实现系统调用
非屏蔽中断的向量和异常的向量是固定的。
4.异常和中断的区别:
1>异常:是指CPU内部出现的中断,即在CPU执行特定指令时出现的非法情况。同时异常也称为同步中断,因此只有在一条指令执行后才会发出中断 ,不可能在指令执行期间发生异常。
a.产生的原因:
程序的错误产生的(eg:除数为0)
内核必须处理的异常条件产生的(eg:缺页)
b.异常又分为故障和陷阱,它们都不使用中断控制器,也不能被屏蔽
C.X86处理处理器中大约有20中异常。Linux内核必须为每种异常提供一个专门的异常处理程序。
2>中断:也称为异步中断。因此它是由其他硬件设备依照 CPU 时钟信号随机产生,即意味着中断能在指令之间发生。
a.中断又分为外部可屏蔽中断(INTR)和外部非屏蔽中断(NMI)
所用I0设备产生的中断请求均引起可屏蔽中断
硬件故障引起的故障则产生非屏蔽中断。
John说明:
在CPU执行一个异常处理程序时,就不再响应其他异常和中断请求服务.那么如果此时发生了一个异常,CPU不能去响应它,又不能把它的信息丢失该怎么办呢?
这是就用到了堆栈,把所有的信息压入栈。等当前异常处理后,才从堆栈中取出信息再响应刚才的异常。(当产生多个非屏蔽中断时,CPU的处理方法同上)
二APIC和8259A
中断的实现也需要硬件上的支持的,那么硬件上是如何支持中断的?
1.在X86计算机的 CPU 为中断只提供了两条外接引脚:NMI 和 INTR。
NMI 是不可屏蔽中断,它通常用于电源掉电和物理存储器奇偶校验;
INTR是可屏蔽中断,可以通过设置中断屏蔽位来进行中断屏蔽,它主要用于接受外部硬件的中断信号,这些信号由中断控制器传递给 CPU。
2.中断控制器
目前常见的中断控制器有可编程中断控制器8259A和高级可编程中断控制器(APIC)
1>8259A
PIC(Programmable Interrupt Controller)是由两片 8259A 的外部芯片以“级联”的方式连接在一起。每个芯片可处理多达 8 个不同的 IRQ。因为从 PIC 的 INT 输出线连接到主 PIC 的 IRQ2 引脚,所以可用 IRQ 线的个数达到 15 个。
我们来看一个图:(进行看图说明)
a.第一级8259A是主中断控制器,它的第二个中断请求输入端与第二级8259A的中断输出端INT相连。
b.与中断控制器相连的每条线叫做中断线。要使用中断线,就要进行中断线的申请,即IRQ。
那么这条线的名字是啥勒----》中断号。
IRQ线是从0开始顺序编号的,所以第一条IRQ线就是IRQ0。
C.那么该中断号于我们上面所说的中断向量有什么关系呢
中断向量=中断号+32。
从此等式可以看出,第一个中断线(IR0)所对应的中中断向量是32.
由此可以得出:
(1)异常和非屏蔽向量是CPU 内部引起的中断
(2)向量32-47对应的是外部中断。
d.并不是每个设备都可以向中断线上发中断信号,只有对某一条确定的中断线拥有了控制权后,才可以向这条中断线上发送信号。
e.8259A中还有一个很重要的寄存器->8位的中断屏蔽寄存器->这个寄存器的作用是屏蔽中断。
8位的中断屏蔽寄存每一位对应8259A中的一条中断线,如果要禁用某条中断线,则把中断屏蔽寄存器的相应位置1,要启用则置0。
John哥说明:
屏蔽中断也可以从CPU的角度考虑,即清除eflag的中断标志位(IF),当IF位为0时禁止任何外部IO的中断请求,即关中断;
f.共享中断(一个很重要的概念,后面程序中会涉及到它)
由于计算机的外部设备越来越多,所以15条中断线已经不够用了。中断线是很宝贵的资源,为了更好的利用它,只有当设备需要中断的时候才申请占用一个IRQ,并且为了让更多的设备使用中断采取了在申请IRQ时采用共享中断的方式。
2>高级可编程中断控制器(APIC)
先看图再说:
1.8259A 只适合单 CPU 的情况,为了充分挖掘 SMP 体系结构的并行性,能够把中断传递给系统中的每个 CPU 至关重要。基于此理由,Intel 引入了一种名为 I/O 高级可编程控制器的新组件,来替代老式的 8259A 可编程中断控制器。该组件包含两大组成部分:一是“本地 APIC”,主要负责传递中断信号到指定的处理器;举例来说,一台具有三个处理器的机器,则它必须相对的要有三个本地 APIC。另外一个重要的部分是 I/O APIC,主要是收集来自 I/O 装置的 Interrupt 信号且在当那些装置需要中断时发送信号到本地 APIC,系统中最多可拥有 8 个 I/O APIC。
2.每个本地 APIC 都有 32 位的寄存器,一个内部时钟,一个本地定时设备以及为本地中断保留的两条额外的 IRQ 线 LINT0 和 LINT1。所有本地 APIC 都连接到 I/O APIC,形成一个多级 APIC 系统。
那么我们如何知道我们机子上使用的是那种中断控制器呢?
我们可以通过在终端出入命令:cat /proc/interrupts来查看
a.若看到列表中有IO-APIC,说明您的系统正在使用 APIC。
若看到 XT-PIC,意味着您的系统正在使用 8259A 芯片。
16位实地址模式的中断机制和32位保护模式的中断机制的最本质差别就是在保护模式心爱引入了中断描述表
在单处理器的系统中,第一列是中断号,第二列是CPU产生该中断的次数。最后一列是于这个中断相关的俄设备名字。这个名字是通过参数devname提供给函数request_irq()(下篇文章会对它讲解)
三.中断描述表
1.为什么引入
在实地址模式中,CPU把内存中从0开始的1kb空间作为一个中断向量表。表中的每个表项占四个字节,由两个字节的段地址和两个字节的偏移量组成,这样构成的地址就是相应中断处理程序的入口地址。
但是在保护模式下,由4个字节的表项构成的中断向量表已经不能满足要求了。在保护模式下,中断向量表中的表项由8个字节组成。此时他也有了新的名字---->中断描述表(Interrupt Descriptor Table,IDT),其中的每个表项叫做一个门描述符(great descriptor)
先来看图在说明:
1>DPL:段描述符的特权级
2>偏移量:入口函数地址的偏移量
3>P:表示段是否在内存中的标志
4>段选择符:入口函数所处代码段的选择符
5>D:标志位,1表示32位,0标示16位
6>xxx:3位门类型码
门类型符主要分为
a.中断门(interrupt gate):其类型码为110,中断门包含了一个中断或异常处理程序所在段的选择符和段内偏移量。
当控制权通过中断门进入中断处理程序时,处理器清IF标志即关中断这样就避免了中断嵌套的发生。
中断门中的DPL(请求特权级)为0,因此用户态中的进程不能访问中断门。所用的中断处理程序都由中断门激活,并全部限制在内核态。
b..陷阱门(tap gate)其类型码为111。它与中断门类似,唯一的区别是控制权通过陷阱门进入处理程序时保持IF标志位不变,即不关中断。
c.系统门(system gate):Linux内核特别设置的,用来让用户态的进程访问Intel的陷阱门。
系统门的DPL为3。系统调用就是通过系统门进入内核的。
2.在保护模式下,中断描述符表在内存的位置不再局限于从地址0开始的位置,而是可以放在内存的任何位置。
1>为了实现这个功能--->CPU中设计了一个中断描述符表寄存器IDTR,用来存放中断描述符表在内存的起始位置。
2>中断描述表寄存器是一个48位的寄存器。它的低16为保存中断描述符表的大小,高32位保存中断描述表的基址。
3>看下图:
我们知道了中断描述表的功能和基本设置后,那么系统是是在何时给它初始化以及是如何给它初始化的呢?
首先Linux内核在系统的初始化阶段对中断进行初始化,其中包括有:初始化可编程控制器8259A;将中断描述符表的起始地址装入IDTR中,并初始化表中的每一项。
3.中断的初始化
1>用户进程可以通过INT指令发出一个中断请求,其中断请求向量在0~255之间。
那么如何防止用户使用INT指令模拟非法的中断和异常?
此时DPL就起作用了->将DPL置为0就可以了。
2>但是,有时候必须让用户进程能够使用内核所提供的功能(比如系统调用)也就是从用户态进入内核态,此时就可以通过把中断门或陷阱门的DPL置为3来实现。
3>当计算机在实模式时,中断描述符表被初始化,并由BIOS使用。
but,在进入了Linux内核时,中断描述符表就被移到内存的另一个区域,并为进入保护模式进行预初始化:
用汇编指令LIDT对中断描述符表寄存器IDTR进行初始化,即把IDTR置为0,然后把中断描述符表IDT的起始地址装入IDTR。
4>中断描述表的初始化
a.第一次初始化:用setup_idt()函数填充中断描述符表中的256个表项,填充时使用一个空的中断处理程序。因为现在还是在初始化阶段,还没有任何中断处理程序,因此,用这个空的中断处理程序来填充每个表项。
b.第二此初始化:内核在启用分页功能后对IDT进行第二此初始化。
此时,使用实际的陷阱和中断处理程序替换这个空的处理程序。一旦这个过程完成后,对于每个异常,IDT都包含一个专门的陷阱门或系统门,而对每个外部中断,IDT都包含专门的中断门。
上面提到了对IDT的初始化,那么我们就递归深入下来看看系统是如何对IDT表项进行设置的
4.IDT表项的设置
IDT表项的设置是通过_set_gate()函数来实现的。
1>插入一个中断门
调用 set_intr_gate(n,addr)函数来实现
此函数的功能是在IDT的第n个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为中断处理程序的地址addr,DPL字段设置为0.
分析下的形参:
n:表示在第几个表项中插入一个中断门。
addr:表示偏移量,此处偏移量设置为中断处理程序的地址addr.
现在我们迭代深入,看下它的内部是如何实现的
- 330static inline void set_intr_gate(unsigned int n, void *addr)
- 331{
- 332 BUG_ON((unsigned)n > 0xFF);
- 333 _set_gate(n, GATE_INTERRUPT, addr, 0, 0, __KERNEL_CS);
- 334}
我们可以看到它里面有封装了两个函数:我门继续迭代深入。
a.我们先来看看第一个函数的原型和功能:
- 19#if (_MIPS_ISA > _MIPS_ISA_MIPS1)
- 20
- 21static inline void __BUG_ON(unsigned long condition)
- 22{
- 23 if (__builtin_constant_p(condition)) {
- 24 if (condition)
- 25 BUG();
- 26 else
- 27 return;
- 28 }
- 29 __asm__ __volatile__("tne $0, %0, %1"
- 30 : : "r" (condition), "i" (BRK_BUG));
- 31}
- 32
- 33#define BUG_ON(C) __BUG_ON((unsigned long)(C))
- 34
(1)我们可以看到BUG_ON()函数是一函数宏,系统最终调用的是__BUG_ON((unsigned long)(C))
函数:
__BUG_ON((unsigned long)(C))它的作用是判断n是否大于255,若大于255则出错。
此处的n表示在第n个表项中插入一个中断门(因为表项总共有255个,所以当n大于255时出错)。
(2) _set_gate(n, GATE_INTERRUPT, addr, 0, 0, __KERNEL_CS);
我们来分析下它的形参:
第一个形参 n:表示在那个表项中插入一个中断门。
第二个形参 表示门描述符的类型,此处是插入一个中断门,所以它的形参是GATE_INTERRUPT。
第三个形参表示偏移量,设置为中断处理程序的地址addr。
第四个形参表示DPL,我们可以看到给它传递的是0,即避免中断嵌套的发生。
第五个形参表示IST(Interrupt Stack Table)共 3 位,表达 IST1 - IST7 共 7 个 Stack pointer第六个形参表示门中的段选择符。此处设置成内核代码的段选择符。
我们还可以继续深入到 _set_gate(n, GATE_INTERRUPT, addr, 0, 0, __KERNEL_CS)函数中看它到底是如何实现功能的,但由于篇幅有限。占时不在继续深入。
2.插入一个陷阱门
调用 set_trap_gate(n,addr)函数来实现。
此函数的功能是:在IDT的第n个表项中插入一个陷阱门。门中的段选择符设置成内核代码的段选择符,偏移量设置为异常处理程序的地址addr,DPL字段设置为0。
分析下的形参:
n:表示在第几个表项中插入一个中断门。
addr:表示偏移量,此处偏移量设置异常处理程序的地址addr.
现在我们迭代深入,看下它的内部是如何实现的
- 371static inline void set_trap_gate(unsigned int n, void *addr)
- 372{
- 373 BUG_ON((unsigned)n > 0xFF);
- 374 _set_gate(n, GATE_TRAP, addr, 0, 0, __KERNEL_CS);
- 375}
我们可以看到它内部同样封装了两个函数BUG_ON()和_set_gate()函数,它们的功能和实现方法都在上面具体分析过,再次不在分析。
3.插入一个系统门
调用 set_system_gate(n,addr)函数来实现。
此函数的功能是在IDT的第n个表项插入一个系统门。门中的段选择符设置成内核代码的段选择符,偏移量设置为异常理程序的地址addr,DPL字段设置为3.(所以,系统调用在用户态下可以通过INT 0X80顺利通过系统门。从而进入内核态)
分析形参:
n:表示在第几个表项中插入一个中断门。
addr:表示偏移量,此处偏移量设置为异常处理程序的地址addr.
现在我们迭代深入,看下它的内部是如何实现的
- 365static inline void set_system_trap_gate(unsigned int n, void *addr)
- 366{
- 367 BUG_ON((unsigned)n > 0xFF);
- 368 _set_gate(n, GATE_TRAP, addr, 0x3, 0, __KERNEL_CS);
- 369}
- 370
我们可以看到它的内部实现还是和上面类似。唯一不同的是在_set_gate()函数中的两个参数不同。
一个参数是DPL被设置为03,还有一个是门类型的类型是GATE_TRAP