Linux内核-中断-中断向量表和中断请求队列的初始化

Linux内核-中断-中断向量表和中断请求队列的初始化
⼀、中断
中断通常被定义为⼀个事件,该事件改变处理器执⾏的指令顺序。中断有两种,⼀种是由CPU外部产⽣的,对于执⾏中的软件来说,这种中断的发⽣完全是“异步”的,根本⽆法预料此类中断会在什么时候发⽣,⼀般由其他硬件设备产⽣(例如键盘中断);另⼀种是由CPU本⾝在执⾏程序的过程中产⽣的,例如X86中的“INT n”。Intel⼿册中分别将这两种中断称为中断和异常。Intel⽂档中,中断⼜可分为可屏蔽中断和⾮屏蔽中断,异常分为:故障、陷阱、异常中⽌和编程异常。不管是哪种中断,CPU的相应过程基本上⼀致,即:在执⾏完当前指令以后,或者在执⾏当前指令中途,根据中断源提供的“中断向量”,在内存中到相应的服务程序⼊⼝并调⽤该服务程序。外部中断的向量是由软件或硬件设置好了的,陷阱的向量是在“⾃陷”指令中发出的(INT n中的n),⽽各种异常的向量则是CPU的硬件结构中预先规定好的。系统调⽤⼀般是通过INT指令实现的,所以也与中断密切相关。
Intel X86 CPU⽀持256个不同的中断向量,早期X86 CPU的中断响应机制是⾮常简单的,内存中从0开始的1K字节作为⼀个中断向量表,表中每个表项占四个字节,由两字节的段地址和两字节的位移组成,构成的地址便是相应中断服务程序的⼊⼝地址。由于中断发⽣时,有可能涉及⽤户态和内核态的切换,那种简单的表项⽆法实现这种运⾏模式的切换,所以在Intel实现保护模式时,中断向量表中的表项从单
纯的⼊⼝地址改成了更为复杂的描述项,称为“门”,当中断发⽣时必须通过这些门,才能进⼊相应的服务程序。按不同的⽤途和⽬
的,CPU中⼀共有四种门:任务门、中断门、陷阱门和系统门(除了系统门外,其它三种都是⽤户态进程⽆法访问的中断门)。其中除任务门外其它三种门的结构基本相同,先看任务门,其⼤⼩为64位,结构如下图:
当中断发⽣时,CPU在中断向量表中到相应表项,如果是⼀个任务门,CPU就会将当前任务的运⾏现场保存在相应的TSS中,并将任务门所指向的TSS作为当前任务,将其内存装⼊CPU中的各个寄存器,从⽽完成⼀次任务切换;
其它三种门⼤⼩也是64位,如下图:
三种门之间的区别为3位的类型码。与任务门相⽐,不同之处主要在于:任务门中不需要段内位移,因为任务门指向⼀个段;⽽其它门则指向⼀个⼦程序,所以必须结合使⽤段选择码和段内位移。
前⾯讲过,内核通过门描述符来实现运⾏模式的切换,这是怎么实现的呢?答案就在描述符的DPL字段,CPU先根据中断向量到⼀扇门描述项,然后,将这个门的DPL与CPU的CPL相⽐,CPL必须⼩于或等于DPL,也就是优先级别不低于DPL,才能穿过这扇门。不过,如果中断是由外部产⽣或是因CPU异常⽽产⽣的话,就免去这⼀层检验。所以通过将系统门的DPL设为3,⽽其它的门的DPL设为0,就可以使⽤户态进程只能访问系统门,从⽽防⽌⽤户通过int指令模拟⾮法的中断和异常。
进⼊中断服务程序时,如果中断服务程序的运⾏级别,也就是⽬标代码段的DPL,与中断发⽣时的CPL不同,那就要引起堆栈的更换。Linux中是这样更换堆栈的:如果进程thread_union结构⼤⼩为8K
B,则当前进程的内核栈被⽤于所有类型的内核控制路径;相反,如果thread_union结构⼤⼩为4KB,内核使⽤三种类型的内核栈:
异常栈:⽤于处理异常(包括系统调⽤)。这个栈包括在每个进程的thread_union数据结构中。
硬中断请求栈:⽤于处理中断。系统中每个CPU都有⼀个硬中断请求栈,⽽且每个栈占⽤⼀个单独的页框。
软中断请求栈:⽤于处理可延迟函数(软中断或tasklet)。系统中每个CPU都有⼀个软中断请求栈,⽽且每个栈占⽤⼀个单独的页框。
⼆、中断向量表的初始化
Linux内核在初始化阶段完成了页式虚存管理的初始化以后,便调⽤trap_init()和init_IRQ两个函数进⾏中断机制的初始化。其中trap_init()中主要是对⼀些系统保留的中断向量的初始化,⽽init_IRQ()则主要是⽤于外设的中断,这两个函数源码如下:
1. trap_init()是在arch/i386/kelnel/traps.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
/*
* APIC为⾼级可编程中断控制器
*/
#ifdef CONFIG_X86_LOCAL_APIC
init_apic_mappings();
#endif
/*
* 程序中先设置中断向量表开头的19个陷阱门,这些中断向量都是CPU保留⽤于
* 异常处理的,例如中断向量14就是为页⾯异常保留的
*/
set_trap_gate(0,÷_error);
独眼 喙鼻set_intr_gate(1,&debug);
set_intr_gate(2,&nmi);
set_system_intr_gate(3, &int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_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);
/*
* 然后是对系统调⽤的初始化,常数SYSCALL_VECTOR定义为0x80
*/
set_system_gate(SYSCALL_VECTOR,&system_call);
/
*
/*
* Should be a barrier for any external CPU state.
*/
cpu_init();
trap_init_hook();
}
从程序中可以看到,这⾥⽤了三个函数来进⾏这些表项的初始化,分别为set_trap_gate()、set_system_gate()和set_intr_gate(),在同⽂件中定义如下:
/*
* 陷阱门
*/
static void __init set_trap_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,0,addr,__KERNEL_CS);
}
/*
* 系统门
*/
static void __init set_system_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,3,addr,__KERNEL_CS);
}
/
*
* 中断门
*/
void set_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,14,0,addr,__KERNEL_CS);
}
/*
* _set_gate函数如下,代码的理解需要结合上⾯门描述符的格式
* %i为输出输⼊的变量,依次编号,%0与参数gate_addr结合,%1与(gate_addr+1)结合⼆者为内存单元
* %2与局部变量__d0结合,存放在寄存器%%eax中,%3与局部变量__d1结合,存放在寄存器%%edx中
* 输⼊部中第⼀个变量为%4,后⾯的两个变量等价于输出部的%3和%2,即存放在edx和eax中
#define _set_gate(gate_addr,type,dpl,addr,seg) \
张炜你在高原
do { \
int __d0, __d1; \
__asm__ __volatile__ (
/*
* 将edx设为addr,eax设为(__KERNEL_CS << 16),edx的低16位移⼊eax的低16位,这样,在
* %%eax中就形成了所需要的中断门的第⼀个长整数,其⾼16位为__KERNEL_CS,低16位为addr的低16位
*/
"movw %%dx,%%ax\n\t" \
/*
* 将(0x8000+(dpl<<13)+(type<<8)))装⼊%%edx的低16位,这样,%%edx中⾼16位为addr⾼16位,
* ⽽低16位的P位为1,DPL位段为dpl(因为dpl<<13),D位加上类型位段为type,其余各位都为0,这就是中断门的第⼆个长整数
*/
"movw %4,%%dx\n\t" \
/*
* 将%%eax写⼊*gate_addr
*/
"movl %%eax,%0\n\t" \
/*
* 将%%edx写⼊*(gate_addr+1)
*/
*/
"movl %%edx,%1" \
:"=m" (*((long *) (gate_addr))), \
"=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1) \
:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"3" ((char *) (addr)),"2" ((seg) << 16)); \
} while (0)
2. 系统初始化时,在trap_init()中设置了⼀些为CPU保留的专⽤的IDT表项以及系统调⽤所⽤的陷阱门以后,就要进⼊init_IRQ()设置⼤
量⽤于外设的通⽤中断门了,函数init_IRQ()源码在arch/i386/kernel/i8259.c中:
void __init init_IRQ(void)
{
int i;
pre_intr_init_hook();
for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (i >= NR_IRQS)
break;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]);
}
cd11/
* setup after call gates are initialised (usually add in
* the architecture specific gates)
*/
intr_init_hook();
/*
* Set the clock to HZ Hz, we already have a valid
* vector now:唐若昕
*/
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()函数相当于⼀下代码:
#ifndef CONFIG_X86_VISWS_APIC
init_ISA_irqs();
#else
init_VISWS_APIC_irqs();
#endif
void __init init_ISA_irqs (void)
{
int i;
#ifdef CONFIG_X86_LOCAL_APIC
init_bsp_APIC();
#endif
init_8259A(0);
for (i = 0; i < NR_IRQS; i++) {
irq_desc[i].status = IRQ_DISABLED;
irq_desc[i].action = NULL;
irq_desc[i].depth = 1;
if (i < 16) {
/*
* 16 old-style INTA-cycle interrupts:
*/
irq_desc[i].handler = &i8259A_irq_type;
} else {
/*
* 'high' PCI IRQs filled in on demand
*/
irq_desc[i].handler = &no_irq_type;
}
}
}
init_ISA_irqs()和init_VISWS_APIC_irqs()分别是对8259A中断控制器和⾼级可编程中断控制器的初始化,并且初始化⼀个结构数组irq_desc[]。为什么要有这么⼀个结构数组呢?i386系统结构⽀持256个中断向量,除去CPU本⾝保留的,很难说剩下的这些向量是否够⽤。⽽且,很多外部设备由于各种原因本来就不得不共⽤中断向量,所以,Linux通过IRQ共享和IRQ动态分配。因此,系统中为每个中断向量设置⼀个队列,根据每个中断源所使⽤的中断向量,将其中断服务程序挂到相应的队列中去,⽽irq_desc[]中的每个元素则是这样⼀个队列头部及控制结构。当中断发⽣时,⾸先执⾏与中断向量相对应的⼀段总服务程序,根据具体的中断源的设备号在其所属队列中到特定的服务程序加以执⾏,该结构在下⽂再详细分析。
回到init_IRQ()函数,接下来的循环从FIRST_EXTERNAL_VECTOR开始,设⽴NR_IRQS个中断向量的IDT表项常数
FIRST_EXTERNAL_VECTOR为0x20,这⾥设置的服务程序⼊⼝地址都来⾃⼀个函数指针interrupt[],该函数指针数组定义如下(arch/i386/kernel/entry.S):
.data
ENTRY(interrupt)
.text
vector=0
ENTRY(irq_entries_start)
.rept NR_IRQS
ALIGN
1:  pushl $vector-256
jmp common_interrupt
.data
.long1b
.
text
vector=vector+1
.endr
数组包括NR_IRQS个元素,这个宏产⽣的数为224或16,当内核⽀持I/O APIC芯⽚时,为224,当内核⽀持8259A时,为16。可以看出,数组中索引为n元素把中断号减256的结果保存在栈中(原因:内核⽤负数表⽰所有中断,正数⽤来表⽰系统调⽤,⽤汇编指令可以很容易判断正负)。由此可以看出,实际上由外设产⽣的中断处理全部进⼊⼀段公共的程序common_interrupt中,在同⼀⽂件中定义如下:
common_interrupt:
SAVE_ALL
movl %esp,%eax
call do_IRQ
jmp ret_from_intr
/* SAVE_ALL如下 */
#define SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \

本文发布于:2024-09-21 00:48:01,感谢您对本站的认可!

本文链接:https://www.17tex.com/xueshu/349599.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:中断   中断向量   相应
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议