概述
5.中断与中断处理
从物理上看,中断是硬件设备产生的一种信号,通过中断控制器发送给CPU,CPU检测到该中断信号后通知内核,内核完成后续的处理。
从不同的角度来说,中断可以有三种分类方法。
- 中断可以分为同步中断(synchronous)和异步中断(asynchronous)。
- 中断可分为硬中断和软中断。
- 中断可分为可屏蔽中断(Maskable interrupt)和非屏蔽中断(Nomaskable interrupt)。
同步中断是在指令执行时由CPU主动产生的,受到CPU控制,其执行点是可控的。异步中断是CPU被动接收到的,由外设发出的电信号引起,其发生时间不可预测。同步中断又称为异常(exception),异步中断称为中断(interrupt)。中断可分为可屏蔽中断(Maskable Interrupt)和非可屏蔽中断(Nomaskable Interrupt)。异常可分为故障(fault)、陷阱(trap)和终止(abort)三类。
CPU提供了两条外界引脚用于中断,即NMI和INTR,NMI用于不可屏蔽中断,INTR用于可屏蔽中断。可屏蔽中断收EFLAGS寄存器的中断允许标志位IF控制
5.1 PIC vs APIC
中断控制器分为两种:
- 可编中断程控制器(Programmable Interrupt Controller, PIC),只能用于单处理器(Uni-processor,UP)平台
- 高级可编程中断控制器(Advanced Programmable Interrupt Controller, APIC),用于多处理器(Multiple Processor,MP)平台
1.可编程中断控制器8259A
单个8259A芯片只能管理8个中断源,若需管理更多中断源,则需将多个芯片级联。传统PIC都是采用两个8295A级联的方式,支持15个中断源。第1 级(称主片)的第2 个中断请求输入端,与第2 级8259A(称从片)的中断输出端INT 相连。
中断控制器与其他硬件设备相联接的各条线被称作中断线。由于数目有限,所以中断线是非常宝贵的资源,使用之前,必须进行中断线的申请,就是IRQ(Interrupt Requirement),常把申请一条中断线称为申请一个IRQ或者申请一个中断号。IRQ线是从0开始顺序编号的,因此,第一条IRQ线通常表示成IRQ0。IRQn的缺省向量是n+32;如前所述,IRQ和向量之间的映射可以通过中断控制器端口来修改。
2.高级可编程中断控制器APIC
APIC由两部分组成:本地高级中断控制器(Local APIC,LAPIC),位于CPU中,主要负责传递中断信号到指定的处理器,在MP平台,每个CPU都具有一个自己的LAPIC;I/O高级中断控制器(I/O APIC,IOAPIC)负责收集来自I/O设备的中断信号并分发给LAPIC,系统中最多拥有8个APIC。
5.2 中断向量
Intel x86系列微机共支持256种向量中断,为使处理器较容易地识别每种中断源,将它们从0~255编号,即赋予一个中断类型码n,Interl把这个8位的无符号整数叫做一个向量:中断向量。所有的256种中断可分为两大类:异常和中断。异常又分为故障、陷阱和中止,它们的共同点是既不使用中断控制器,又不能被屏蔽。中断又分为外部可屏蔽中断(INTR)和外部非屏蔽中断(NMI),所有I/O设备产生的中断请求(IRQ)均引起屏蔽中断,而紧急的事件(如硬件故障)引起的故障产生非屏蔽中断。
非屏蔽中断的向量和异常的向量是固定的,而屏蔽中断的向量可以通过对中断控制器的编程来改变,Linux对256个向量的分配如下:
- 0~31的向量对应异常和非屏蔽中断
- 32~47的向量(即由I/O设备引起的中断)分配给屏蔽中断
- 剩余的48~255向量用于软中断。Linux只用了一个(即128或0x80向量)用来实现系统调用。当用户态下的进程执行一条int 0x80汇编指令时,CPU就切换到内核态,并开始执行system_call内核函数。
5.3 中断描述符表
在实地址模式中,CPU把内存中从0开始的1K字节作为一个中断向量表。表中的每个表项占4个字节,由两个字节的段基址和两个字节的偏移量组成,这样构成的地址便是相应的中断处理程序的入口地址。但是在保护模式下,由4个字节的表项构成的中断向量表已经不能满足要求了。在保护模式下,中断向量表中的表项由8个字节组成。中断向量表也改叫做中断描述符表IDT(Interrupt Descriptor Table)。其中每个表项叫做一个门描述符(Gate Descriptor),“门”的含义是当中断发生时必须先通过这些门,然后才能进入相应的处理程序。
其中类型栈3位,表示门描述符的类型,这些描述符如下:
1.任务门 (Task gate)
其类型码为101,门中包含了一个进程的TSS 段选择符,但偏移量部分没有使用,因为TSS本身是作为一个段来对待的,因此,任务门不包含某一个入口函数的地址。TSS 是Intel 所提供的任务切换机制,但是 Linux 并没有采用任务门来进行任务切换。
2.中断门(Interrupt gate)
其类型码为110,中断门包含了一个中断或异常处理程序所在段的选择符和段内偏移量。当控制权通过中断门进入中断处理程序时,处理器清IF 标志,即关中断,以避免嵌套中断的发生。中断门中的DPL(Descriptor Privilege Level)为0,因此,用户态的进程不能访问Intel 的中断门。所有的中断处理程序都由中断门激活,并全部限制在内核态。
3.陷阱门(Trap gate)
其类型码为111,与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时维持IF 标志位不变,也就是说,不关中断。
4.系统(调用)门(System gate)
这是Linux 内核特别设置的,用来让用户态的进程访问Intel 的陷阱门,因此,门描述符的DPL 为3。通过系统门来激活4 个Linux 异常处理程序,它们的向量是3、4、5 及128,也就是说,在用户态下,可以使用int 3、into、bound 及int 0x80 四条汇编指令。
最后,在保护模式下,中断描述符表在内存的位置不再限于从地址0 开始的地方,而是可以放在内存的任何地方。为此,CPU 中增设了一个中断描述符表寄存器IDTR,用来存放中断描述符表在内存的起始地址。中断描述符表寄存器IDTR 是一个48 位的寄存器,其低16位保存中断描述符表的大小,高32 位保存IDT 的基址,如图3.3 所示。
5.4 中断服务程序
中断服务程序必须在设备驱动程序中定义,并在使用request_irq函数申请IRQ线时,关联到所申请到的IRQ线上
/**
*@irq:所申请到的IRQ线
*dev_id : request_irq函数传递进来的参数,具有全局唯一性,通常指向设备的私有数据结构,可以用其判断具体哪个设备产生了中断
*/
typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);
中断服务程序的返回值是一个特殊类型——irqreturn_t,它在include/linux/irqreturn.h文件中定义
typedef int irqreturn_t
#define IRQ_NONE (0)
#define IRQ_HANDLED (1)
#define IRQ_RETVAL(x) ((x)!=0)
5.5 中断相关数据结构
3个主要的数据结构:irq_chip、irq_desc和irqaction。
数据结构irq_desc用于描述IRQ线的属性与状态,又被称为中断描述符。每个IRQ都有它自己的irq_desc对象,所有的irq_desc对象组织在一起形成irq_desc数组,即中断描述符数组。
struct irq_desc {
/*该IRQ线的公共服务程序*/
irq_flow_handler_t handle_irq;
/*该IRQ线所隶属的中断控制器*/
struct irq_chip *chip;
struct msi_desc *msi_desc;
void *handler_data;
void *chip_data;
/*描述该IRQ线所连接的特定中断源产生中断时,需要调用的服务程序,指向该IRQ线中断请求队列的头*/
struct irqaction *action; /* IRQ action list */
/*描述IRQ线状态的标志*/
unsigned int status; /* IRQ status */
/*如果为0,表示该IRQ线被激活,如果为一个正数,则表示该IRQ线被禁止的次数*/
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
/*该IRQ线上发生中断的总次数*/
unsigned int irq_count; /* For detecting broken IRQs */
/*该IRQ线上未处理中断发生的总次数*/
unsigned int irqs_unhandled;
/*上一次发生未处理中断时的jiffies*/
unsigned long last_unhandled; /* Aging timer for unhandled count */
/*irq_desc对象都是全局的,因此需要使用锁进行保护*/
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;
#endif
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
const char *name;
} ____cacheline_internodealigned_in_smp;
/*声明一个中断描述符数组,共有NR_IRQS(通常为224)项,对应NR_IRQS个IRQ线*/
extern struct irq_desc irq_desc[NR_IRQS];
irq_chip用于描述不同类型的中断控制器
struct irq_chip {
/*中断控制器名称*/
const char *name;
/*启用与关闭指定的IRQ线*/
unsigned int (*startup)(unsigned int irq);
void (*shutdown)(unsigned int irq);
/*激活与禁止指定的IRQ线*/
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);
/*应答指定的IRQ线*/
void (*ack)(unsigned int irq);
/*屏蔽指定的IRQ线,阻塞它向CPU发送*/
void (*mask)(unsigned int irq);
void (*mask_ack)(unsigned int irq);
/*解除屏蔽指定的IRQ线*/
void (*unmask)(unsigned int irq);
void (*eoi)(unsigned int irq);
/*通知中继控制器,中断已经处理完毕*/
void (*end)(unsigned int irq);
/*绑定指定的IRQ到特定的CPU上*/
void (*set_affinity)(unsigned int irq, cpumask_t dest);
/*重新产生并发送指定的IRQ*/
int (*retrigger)(unsigned int irq);
int (*set_type)(unsigned int irq, unsigned int flow_type);
/*激活或禁止wake-on-interrupt行为(该IRQ能否唤醒睡眠中的系统)*/
int (*set_wake)(unsigned int irq, unsigned int on);
/* 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;
};
irqaction针对特定设备所产生中断
typedef irqreturn_t (*irq_handler_t)(int, void *);
struct irqaction {
/*特定设备对应的中断服务程序*/
irq_handler_t handler;
/*中断处理的标志*/
unsigned long flags;
cpumask_t mask;
/*设备名*/
const char *name;
/*设备的私有字段,通常用户标识设备或者指向设备驱动程序的数据
驱动程序申请的IRQ时将其作为参数传递给中断处理程序*/
void *dev_id;
/*指向IRQ线中断请求队列的下一个irqaction对象*/
struct irqaction *next;
/*irq线*/
int irq;
struct proc_dir_entry *dir;
};
5.6 中断子系统初始化
内核在自身初始化过程中对中断处理机制的初始化,包括中断描述符表的初始化以及中断请求队列的初始化等内容。
5.6.1 中断描述符表初始化
中断描述符IDT需要经过两个阶段的初始化,第一个阶段初始化发生在内核引导过程,第二个阶段发生在内核初始化过程。
内核引导过程初始化
为IDT分配2KB大小的空间(256个中断向量,每个中断向量对应一个8bit的门描述符)并初始化为默认值;存储IDT的起始地址到IDTR寄存器。
代码实现:
/*代码目录 arch/i386/kernel/head.S*/
call setup_idt /* 初始化IDT */
...
lidt idt_descr /* 存储IDT的起始地址到IDTR寄存器中 */
setup_idt:
/*将默认处理程序ignore_init 函数的地址存储在edx寄存器*/
lea ignore_int,%edx
/*将代码段的段选择符存储在eax寄存器的高16位*/
movl $(__KERNEL_CS << 16),%eax
/*将edx的低16位存储在eax寄存器低16位*/
movw %dx,%ax /* selector = 0x0010 = cs */
/*将0x8E00作为门描述符的默认属性存储在edx寄存器低16位*/
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
/*将IDT的起始地址(即第一个门描述符地址)存储在edi寄存器*/
lea idt_table,%edi
/*将中断向量的数目256存储在ecx寄存器*/
mov $256,%ecx
/*使用edx与eax寄存器作为一个门描述符的高32位与低32位,初始化IDT的每一项*/
rp_sidt:
/*存储eax寄存器的值到门描述符低32位(edi寄存器存储了IDT的地址)*/
movl %eax,(%edi)
/*存储edx寄存器的值到门描述符的高32位*/
movl %edx,4(%edi)
/*使edi寄存器指向下一个门描述符*/
addl $8,%edi
/*ecx寄存器值减1,循环上面步骤,直到IDT的256个门描述符初始化完毕*/
dec %ecx
jne rp_sidt
指令格式:LEA 目的,源 指令功能:取源操作数地址的偏移量,并把它传送到目的操作数所在的单元。
内核初始化过程
内核自身的初始化发生在start_kernel函数中,使用了两个函数trap_init和init_IRQ来完成IDT的第二阶段初始化。
(1)trap_init 完成对系统保留中断向量(异常、非屏蔽中断以及系统调用)的初始化
/* arch/i386/kernel/trap.c */
void __init trap_init(void)
{
#ifdef CONFIG_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
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); /* int3/4 can be called from all */
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) {
/*
* Verify that the FXSAVE/FXRSTOR data will be 16-byte aligned.
* Generates a compile-time "error: zero width for bit-field" if
* the alignment is wrong.
*/
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");
}
/*初始化中断向量0x80,即系统调用*/
set_system_gate(SYSCALL_VECTOR,&system_call);
/*
* Should be a barrier for any external CPU state.
*/
cpu_init();
trap_init_hook();
}
/*设置中断门描述符,n为中断向量号,addr为服务程序地址*/
void set_intr_gate(unsigned int n, void *addr)
{
_set_gate(n, DESCTYPE_INT, addr, __KERNEL_CS);
}
/*
* This routine sets up an interrupt gate at directory privilege level 3.
*/
/*设置特权级为3的中断门描述符(中断门描述符特权级默认为0)*/
static inline void set_system_intr_gate(unsigned int n, void *addr)
{
_set_gate(n, DESCTYPE_INT | DESCTYPE_DPL3, addr, __KERNEL_CS);
}
/*设置陷阱门描述符*/
static void __init set_trap_gate(unsigned int n, void *addr)
{
_set_gate(n, DESCTYPE_TRAP, addr, __KERNEL_CS);
}
/*设置系统门描述符*/
static void __init set_system_gate(unsigned int n, void *addr)
{
_set_gate(n, DESCTYPE_TRAP | DESCTYPE_DPL3, addr, __KERNEL_CS);
}
/*设置任务门描述符*/
static void __init set_task_gate(unsigned int n, unsigned int gdt_entry)
{
_set_gate(n, DESCTYPE_TASK, (void *)0, (gdt_entry<<3));
}
/*
*设置门描述符的底层函数,gate为中断向量,type为门描述符的属性
*addr 为处理程序地址,seg为段选择符
*/
static inline void _set_gate(int gate, unsigned int type, void *addr, unsigned short seg)
{
__u32 a, b;
/*pack_gate函数将参数封装成一个门描述符保存在变量a、b中。a保存低32位,其中低16位为addr的低16位,
高16位为段选择符seg。b保存高32位,其中低16位为type,高16位为addr的高16位*/
pack_gate(&a, &b, (unsigned long)addr, seg, type, 0);
/*将pack_gate获得的门描述符写入IDT,idt_table为第一阶段初始化中预留的IDT地址*/
write_idt_entry(idt_table, gate, a, b);
}
(2) init_IRQ函数完成其余中断向量的初始化
/* arch/i386/kernel/paravirt.c */
void init_IRQ(void)
{
paravirt_ops.init_IRQ();/*初始化为native_init_IRQ函数*/
}
/* arch/i386/kernel/I8259.c */
void __init native_init_IRQ(void)
{
int i;
/* all the set up before the call gates are initialised */
/*初始化中断控制器以及初始化每个IRQ线的中断请求队列*/
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)
*/
/*NR_VECTORS 为256,FIRST_EXTERNAL_VECTOR 为32(表示预留的中断向量0~32),
*NR_IRQS 为224,因此此处的for循环对其余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)
set_intr_gate(vector, interrupt[i]);
}
/* setup after call gates are initialised (usually add in
* the architecture specific gates)
*/
/*主要针对SMP系统进行一些额外的初始化*/
intr_init_hook();
/*
* 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);
/*如果配置内核是,选中了CONFIG_4KSTACKS选项,则为每个CPU分配一个4kb大小的栈,专门用于中断和异常
处理。否则,中断与异常处理时与被中断进程共用一个8kb大小的内核栈*/
irq_ctx_init(smp_processor_id());
}
5.7 中断或异常处理
- 中断处理基本过程:首先设备产生中断,并通过中断线将中断信号送往中断控制器。如果该中断没有被屏蔽,则会被送往CPU的INTR引脚。CPU立即停止当前的工作,根据从中断控制器获取的中断向量号,从IDT中找到相应的门描述符,从而获取中断服务程序的地址并执行。
- 异常处理基本过程:异常不需要经过中断控制器转发电信号,当异常发生时,CPU通过其特定的中断向量号,从IDT中查找相应的门描述符,获取异常服务程序的地址。
5.7.1 中断控制器的工作
主要有3个寄存器用于对中断的响应和处理:
- IRR (Interrupt Request Register) 中断请求控制器,共8bit,对应IR0~IR7八个中断引脚,当某个引脚的中断请求到来后,若该引脚没有被屏蔽,则IRR中对应的bit被置为1,表示已经收到设备的中断请求,但还未提交给CPU.
- ISR (Interrupt Service Register) 中断服务寄存器,当IRR中的某个中断请求被送往CPU后,ISR中对应的bit被置为1,表示中断已提交给CPU,但CPU还未处理完。
- IMR (Interrupt Mask Register) 中断屏蔽寄存器,当某个bit置为1时,对应的中断引脚被屏蔽。
中断请求:设备发起中断时,与其相连的IR引脚上产生电信号,若对应的中断没有被屏蔽,即IMR中相应的位为0,则设置IRR相应为为1,并通过INT引脚向CPU的INTR引脚发出中断请求信号。若已屏蔽,则仅仅丢弃该中断请求。
中断响应
当CPU执行完一条指令后,会去检查INTR管脚是否有信号,即是否有新的中断请求,如果有,还要检查EFLAGS寄存器的中断允许标志IF是否为1,若IF为1 ,则通过INTA引脚应答8259A,表示收到中断请求。
5.7.2 CPU的工作
- 确定中断或异常的中断向量i
- 通过IDTR寄存器找到IDT,读取第i项(即第i个门描述符)
- 特权检查。CPU的当期特权级CPL 小于 门描述符的DPL ,则能通过门,否则产生通用保护异常。通过门之后,门描述符中段选择符所指向代码段的DPL小于CPL,则执行中断处理程序,否则产生一个通用保护异常。
- 若特权级发生变化,则需要进行堆栈的切换。
- 进入中断或异常程序并执行
5.7.3 内核对中断的处理
所有中断(除去不可屏蔽中断)的服务程序在init_IRQ函数中都被初始化为interrupt[i],interrupt数组的每一项指向一个代码段,该代码片除了将中断向量号压入堆栈外,还要调用到一个公共的处理程序common_interrupt。
5.8 中断API
注册释放接口
/**
* @irq:要申请的IRQ号。很多设备使用的IRQ号通常都是预先分配好的,比如系统时钟和键盘。
* @handler : 要注册的中断服务例程
* @irqflags : 中断的类型标识。可取值如下:
* IRQF_SHARED: 可以与其他设备共享同一条中断线。
IRQ_DISABLED: 在本地CPU上,中断处理程序在禁止所有中断的情况下执行,
可以不受其他中断干扰。如果不设置这个标志,则只有该中断处理程序对应的
那条中断线被屏蔽,其他中断都是激活的。
IRQ_SAMPLE_RANDOM : 中断能够用来产生内核熵
@dev_name : 中断源的名称,可以在/proc/interrupts文件中查看
@dev_id : 传递给中断服务程序handler的参数,必须全局唯一,通常用来指向设备的私有数据结构,
以便标识是哪个设备产生了中断。如果不与其他设备共享中断线,则可以指定为NULL
*/
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id);
void free_irq(unsigned int irq, void *dev_id);
request_irq主要任务是为IRQ线的中断请求队列创建irqaction节点。因为中断请求队列在中断子系统的初始化过程中被初始化为空,所以如果设备没有使用request_irq函数为其填充节点,即使设备产生了中断,也得不到任何真正的处理。
内核接受一个中断后,将依次调用在该中断线上注册的每一个中断服务程序。设备驱动程序必须知道它是否应该为中断请求负责。因此,一个中断服务程序如果判断与它相关的设备并没有产生该中断请求,那么它应该立即退出。通常硬件设备都会提供状态寄存器或类似机制,以供中断服务程序进行检查。
转载于:https://my.oschina.net/u/3227348/blog/3026443
最后
以上就是标致冬日为你收集整理的linux内核从菜鸟起步(二)5.中断与中断处理的全部内容,希望文章能够帮你解决linux内核从菜鸟起步(二)5.中断与中断处理所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复