http://linux.chinaitlab.com/administer/824246.html
一:引言
在Intel的文档中,把中断分为两种.一种是异常,也叫同步同断.一种称之为中断,也叫异常中断.
同步中断指的是由CPU控制单元产生,之所以称之为同步,是因为只有一条指令执行完毕后才会发出中断.例如除法运算中,除数为零的时候,就会产生一个异常
异步中断是由外部设备按照CPU的时钟随机产生的.例如,网卡检测到一个数据到来就会产生一个中断.
二:x86的中断处理过程
由于中断是开着的,所以当执行完一条指令后,cs和eip这对寄存器中已经包含了下一条将要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否发生了一个中断或异常。如果发生了一个中断和异常,那么控制单元执行下列操作:
1. 确定与中断或异常关联的向量i(0≤ i ≤255)
2. 读由idtr寄存器指向的IDT表中的第i项。
3. 从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符标识的段描述符。这个描述符指定中断或异常处理程序所在的段的基地址。
4. 确信中断是由授权的(中断)发生源发出的。首先将当前特权级CPL(存放在cs寄存器的低两位)与段描述符(存放在GDT中)的描述符特权级DPL比较。如果CPL小于DPL,就产生一个“通常保护”异常,因为中断处理程序的特权级不能低于引起中断的程序的特权。对于编程异常,则做进一步的安全检查:比较CPL与处于IDT中的门描述符的DPL,如果DPL小于CPL,就产生一个“通常保护”异常,这最后
一个检查可以避免用户应用程序访问特殊的陷阱门和中断门。
5. 检查是否发生了特权级的变化,也就是说,CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈,通过执行以下步骤来保证这一点:
A. 读tr寄存器,以访问运行进程的TSS段。
B. 用与新特权级相关的栈段和栈指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到。
C. 在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
6. 如果故障已发生,用引起异常的指令地址装载cs和eip寄存器,从而使得这条指令能再次被执行。
7. 在栈中保存eflag、cs和eip的内容。
8. 如果异常产生了一个硬件出错码,则将它保存在栈中。
9. 装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。
控制单元所执行的最后一步就是跳转到中断或异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。
上面的处理过程的描述摘自<<深入理解linux内核>>,其中有几点值得注意的地方:
1:通过门后,只能提高运行级别.就像上面所述的 “当前特权级CPL(存放在cs寄存器的低两位)与段描述符(存放在GDT中)的描述符特权级DPL比较。如果CPL小于DPL,就产生一个“通常保护”异常”.在中断处理中,通常把IDT中的相应段选择符设为__KERNEL_CS.即最高的运行级别
2:上面C所述:“在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址”,那ss,esp以前的值是如何找到的呢?应该是从TSS中.在中断发生的时候,如果检测到运行级别发生了改了,将寄存器SS,ESP中的值保存进TSS的相应级别位置.再加载新的SS,ESP的值,然后从TSS中取出旧的SS,ESP值,再压栈.
3:堆栈的改变,如下图所示:
从上图中我们可以看到,硬件自动保存的硬件环境是非常少,要在中断后恢复到以前的环境,还需要保存更多的寄存器值,这是由操作系统完成的.这我们在以后的代码分析中可以看到
中断和异常被处理完毕后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程,这将迫使控制单元:
1. 用保存在栈中的值装载cs、eip和eflag寄存器。如果一个硬件出错码曾被压入栈中,并且在eip内容的上面,那么,执行iret指令前必须先弹出这个硬件出错码。
2. 检查处理程序的CPL是否等于cs中的低两位的值。如果是,iret终止返回;否则,转入下一步。
3. 从栈中转载ss和esp寄存器,因此,返回到与旧特权级相关的栈。
4. 检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相关的段寄存器。控制单元这么做是为了禁止用户态的程序利用内核以前所用的段寄存器。如果不清除这些寄存器的话,恶意的用户程序就会利用他们来访问内核地址空间。
注意到4:举例说明一下.如果通过系统调用进入内核态.然后将DS,ES的值赋为__KERNEL_DS(在2.4的内核里),处理完后(调用iret后),恢复CS,EIP的值,此时CS的CPL是3.因为DS,ES被设为了__KERNEL_DS,所以其DPL是0,所以要将DS,ES中的值清除.在2.6内核中,发生中断或异常后,将DS,ES的值设为了__USER_DS,避免了上述的清除过程,提高了效率.
三:重要的数据结构
在深入源代码之前,先把所用到的数据结构分析如下:
Irq_desc[]定义如下:
extern irq_desc_t irq_desc [NR_IRQS]
typedef struct irq_desc {
unsigned int status; /* IRQ的状态;IRQ 是否被禁止了,有关IRQ 的设备当前是否正被自动检测*/
hw_irq_controller *handler;/*指向一个中断控制器的指针*/
c *action; /* 挂在IRQ上的中断处理程序 */
unsigned int depth; /* 为0:该IRQ被启用,如果为一个正数,表示被禁用 */
unsigned int irq_count; /* 该IRQ发生的中断的次数 */
unsigned int irqs_unhandled; /*该IRQ线上没有被处理的IRQ总数*/
spinlock_t lock;
} ____cacheline_aligned irq_desc_t;
Hw_irq_controller定义如下:
struct hw_interrupt_type {
const char * typename; /*中断控制器的名字*/
unsigned int (*startup)(unsigned int irq); /*允许从IRQ线产生中断*/
void (*shutdown)(unsigned int irq); /*禁止从IRQ线产生中断*/
void (*enable)(unsigned int irq); /*enable与disable函数在8259A中与上述的startup shutdown函数相同*/
void (*disable)(unsigned int irq);
void (*ack)(unsigned int irq); /*在IRQ线上产生一个应答*/
void (*end)(unsigned int irq); /*在IRQ处理程序终止时被调用*/
void (*set_affinity)(unsigned int irq, cpumask_t dest); /*在SMP系统中,设置IRQ处理的亲和力*/
}
typedef struct hw_interrupt_type hw_irq_controller;
struct irqaction定义如下:
struct irqaction {
//中断处理例程
irqreturn_t (*handler)(int, void *, struct pt_regs *);
//flags:
//SA_INTERRUPT:中断嵌套
//SA_SAMPLE_RANDOM:这个中断源于物理随机性
//SA_SHIRQ:中断线共享
unsigned long flags;
//在x86平台无用
cpumask_t mask;
//产生中断的硬件名字
const char *name;
//设备ID,一般由厂商指定
void *dev_id;
//下一个irqaction.共享的时候,通常一根中断线对应很多硬件设备的中断处理例程
struct irqaction *next;
}
可以用下图来表示上述数据结构的关系:
四:idt在保护模式下的初始化
有关实模式下的初始化,以后再做专题分析.详情请关注本站更新.
在init/main.c中:
asmlinkage void __init start_kernel(void)
{
……
//设定系统规定的异常/中断
trap_init();
//设置外部IRQ中断
init_IRQ();
……
}
在start_kernel中,调用trap_init()来设置系统规定的异常与中断,调用init_IRQ()来设置外部中断.
void __init trap_init(void)
{
……
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);
set_system_gate(SYSCALL_VECTOR,&system_call); //系统调用
……
}
如上所示,设置了0~19的中断/异常处理程序,这些都是intel所规定的,除些之后设置了系统调用入口(用户空间的 int SYSCALL_VECTOR )
那, set_trap_gate()/set_intr_gate()/set_system_gata()都有一些什么样的区别呢?继续看代码:
void set_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,14,0,addr,__KERNEL_CS);
}
static inline void set_system_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n, 14, 3, addr, __KERNEL_CS);
}
void __init set_trap_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,0,addr,__KERNEL_CS);
}
void __init set_system_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,3,addr,__KERNEL_CS);
}
都是通过统一的接口_set_gate().在i386中,这段代码是用嵌入式汇编完成的,如下所示:
#define _set_gate(gate_addr,type,dpl,addr,seg) \
do { \
int __d0, __d1; \
__asm__ __volatile__ ("movw %%dx,%%ax\n\t" \
"movw %4,%%dx\n\t" \
"movl %%eax,%0\n\t" \
"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)
我们看可以看: _set_gate(gate_addr,type,dpl,addr,seg)中:
Gate_addr:相应IDT项的地址.type:设置IDT项的TYPE字段, 15表示系统门,14表示中断门.dpl:IDT项对应的DPL值,addr:中断处理程序的地址,seg:IDT中对应项的段选择符
由此可以看出,陷阱门与中断门被锁定在内核态(DPL为0),系统门可以从用户态进入.那既然陷阱门与中断门又有什么区别呢?唯一的区别是,通过陷阱门不会改变FLAGES中的中断标志,但是中断门就会改变,即会屏弊中断
接下来看IRQ中断的设置:
void __init init_IRQ(void)
{
int i;
//8259初始化
pre_intr_init_hook();
for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {
//FIRST_EXTERNAL_VECTOR:第一个可用号,前面部份均为系统保留
int vector = FIRST_EXTERNAL_VECTOR + i;
if (i >= NR_IRQS)
break;
//跳过系统调用号
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]);
}
……
……
}
在深入这段代码之前,我们先看下x86的中断的硬件处理机制.x86中断处理系统一般采用两个8259A芯片级连的方式.每个8259A有8根中断信号线,从片有一根信号线连接至了主片,所以,总共可以处理15个IRQ信号.如下图所示:
来看下具体的代码:
pre_intr_init_hook() -à init_ISA_irqs()
void __init init_ISA_irqs (void)
{
int i;
#ifdef CONFIG_X86_LOCAL_APIC
init_bsp_APIC();
#endif
//初始化8259A芯片
init_8259A(0);
//初始化irq_desc[]数组
for (i = 0; i < NR_IRQS; i++) {
//状态:禁用
irq_desc[i].status = IRQ_DISABLED;
//初始化为NULL.表示无中断处理函数,系统初始化完成之后,可以调用request_irq()注册中断处理函数
irq_desc[i].action = NULL;
//depth值为1,表示当前IRQ线被禁用
irq_desc[i].depth = 1;
// 只使用了15根(两块8259A级联)
if (i < 16) {
//将irq_desc[i].handler:设置为8259A的中断控制器处理
irq_desc[i].handler = &i8259A_irq_type;
} else {
//其它的在x86平台被设为no_irq_type.表示无中断控制器
irq_desc[i].handler = &no_irq_type;
}
}
}
在这个函数里,初始化了irq_desc[]数组.随后,调用了set_intr_gate(vector, interrupt[i])为第n条中断线设置的中断处理函数为interrupt[n- FIRST_EXTERNAL_VECTOR].
Interrupt[]数组在哪里定义的呢?接下来往下看:
ENTRY(interrupt)
.previous
vector=0
ENTRY(irq_entries_start)
.rept NR_IRQS
ALIGN
1: pushl $vector-256
jmp common_interrupt
.data
.long 1b
.previous
vector=vector+1
.endr
相当于,interrupt[i]执行下列操作:
Pushl $i-256 //中断号取负再压栈
Jmp common_interrupt //跳转至一段公共的处理函数
至此,保护模式下的中断子系统初始化完成.接下来分析linux如何响应中断
五:中断入口分析
1:IRQ入口分析
如上所述.将中断号取负压栈之后,会跳转一段公共的处理函数,跟进这段函数:
common_interrupt:
SAVE_ALL
call do_IRQ #调用相应的中断处理函数
jmp ret_from_intr #从中断返回
SAVE_ALL定义如下:
#define SAVE_ALL \
__SAVE_ALL; \
__SWITCH_KERNELSPACE; #在没有定义CONFIG_X86_HIGH_ENTRY的情况下,此宏是一个空宏
__SAVE_ALL定义如下:
#define __SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__USER_DS), %edx; \
movl %edx, %ds; \
movl %edx, %es;
相当于把中断发生时,硬件没有保存的寄存器压栈保存下来.把DS.ES设为了__USER_DS是有一定原因的,参考上节所述.
经过SAVE_ALL后.堆栈内容如下所示:
这个图是从<<中断处理源码情景分析>>一文中摘出来的.事实上图中的用户堆栈指针在IRQ处理中是不可能存在的,因为中断处理对应IDT项的PL值为0,所以,不可能是从用户空间发生的
2:异常处理入口分析
异常处理程序也有很多相同的操作,以16号向量对应的中断处理程序为例:
set_trap_gate(16,&coprocessor_error);
ENTRY(coprocessor_error)
pushl $0 #把0入栈.如果异常没有产生一个硬件出错码,就把0入栈
pushl $do_coprocessor_error #相应的异常处理程序
jmp error_code #跳转到error_code
error_code定义如下:
error_code:
pushl %ds
pushl %eax
xorl %eax, %eax //EAX中的值变为零
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
decl %eax # eax = -1
pushl %ecx
pushl %ebx
cld
movl %es, %ecx
movl ORIG_EAX(%esp), %esi # get the error code
movl ES(%esp), %edi # get the function address
movl %eax, ORIG_EAX(%esp)
movl %ecx, ES(%esp)
pushl %esi # push the error code
movl $(__USER_DS), %edx
movl %edx, %ds
movl %edx, %es
/* clobbers edx, ebx and ebp */
__SWITCH_KERNELSPACE
leal 4(%esp), %edx # prepare pt_regs
pushl %edx # push pt_regs
call *%edi
addl $8, %esp
jmp ret_from_exception把
在上述代码在cld指令之前的堆栈内容与IRQ处理的堆栈相比,如下示:
其中大部份的数据都是一样的,经过后续处理,堆栈内容会变成如下所示:
经过调整之后变得跟IRQ处理的堆栈一样的了,栈顶指针指向存放EBX的位置,栈顶向上一个存储单位是ERROR_CODE.这样做是因为异常处理程序一般都有两个参数,一个是struct pt_regs.表示所有保存的寄存器的值,一个是unsigned long,表示出错码
这样,就为中断处理跟异常处理构造了一个统一的堆栈,当然这样做只是为了使用统一的参数类型,即struct pt_regs
3:系统调用入口分析
系统调用的处理函数为: system_call,定义如下:
set_system_gate(SYSCALL_VECTOR,&system_call);
ENTRY(system_call)
pushl %eax # save orig_eax(系统调用号).在发生系统调用的时候,系统调用号都是存放在EAX中的
SAVE_ALL #SAVE_ALL在IRQ处理入中已经分析过了
GET_THREAD_INFO(%ebp) #取得当前进程的task描述符
# system call tracing in operation
testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
jnz syscall_trace_entry #如果进程被TRACE,跳转至syscall_trace_entry,假设进程没有被trace
cmpl $(nr_syscalls), %eax #在系统调用表中取得相应的处理函数
jae syscall_badsys #系统调用号无效
syscall_call:
call *sys_call_table(,%eax,4) #call相应的处理函数
movl %eax,EAX(%esp) # store the return value
syscall_exit:
cli # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
movl TI_flags(%ebp), %ecx
testw $_TIF_ALLWORK_MASK, %cx # current->work
jne syscall_exit_work
restore_all:
RESTORE_ALL
六:中断返回分析
1:IRQ中断返回分析
如上节所述,IRQ中断处理如下:
common_interrupt:
SAVE_ALL
call do_IRQ
jmp ret_from_intr
分析一下执行完相应的中断处理函数之后,如何返回,即恢复硬件环境原状
Ret_from_intr定义如下:
ret_from_intr:
GET_THREAD_INFO(%ebp) # 取得当前过程的task描述符
movl EFLAGS(%esp), %eax # 中断前的EFLAGS中的值存进EAX
movb CS(%esp), %al # 将中断前的CS低16移至AL
#至此EFLAGS 的H16和CS的L16构成了EAX的内容
testl $(VM_MASK | 3), %eax # EFLAGS中有一位表示是否运行在vm86模式中
#CS的最低二位表示当前进程的运行级别
jz resume_kernel # 如果中断前不是在用户空间,且不是在VM86模式下,跳转到resume_kernel
ENTRY(resume_userspace) # 用户模式下
cli #开中断,以防中断丢失
movl TI_flags(%ebp), %ecx #将task->flags成员的值存进ecx
andl $_TIF_WORK_MASK, %ecx # is there any work to be done on 还有事情没做完?
# int/exception return?
jne work_pending #还有事情没有处理完
jmp restore_all #所有事情都处理完了
resume_kernel定义如下:
在没有定义CONFIG_PREEMPT的情况下:
#define resume_kernel restore_all
Restore_all被定义成:
restore_all:
RESTORE_ALL
RESTORE_ALL定义如下:
#define RESTORE_ALL \
__SWITCH_USERSPACE; \ #选择配置项.忽略
__RESTORE_ALL;
转至__RESTORE_ALL:
#define __RESTORE_ALL \
__RESTORE_REGS \ #pop在SAVE_ALL中入栈的寄存器
addl $4, %esp; \ #记否?在SAVE_ALL之前压入了一个中断向量的负值或者是系统调用号
333: iret; #iret中断返回,交给硬件完成中断的返回工作
__RESTORE_REGS被定义成:
#define __RESTORE_REGS \
__RESTORE_INT_REGS; \
#define __RESTORE_INT_REGS \
popl %ebx; \
popl %ecx; \
popl %edx; \
popl %esi; \
popl %edi; \
popl %ebp; \
popl %eax
如果编译内核被配置成允许被抢占的情况:
#ifdef CONFIG_PREEMPT
ENTRY(resume_kernel)
cmpl $0,TI_preempt_count(%ebp) # non-zero preempt_count ?
jnz restore_all #preempt_count为非0,表示此时不能发生抢占,则跳转到restore_all
need_resched:
movl TI_flags(%ebp), %ecx # need_resched set ?
testb $_TIF_NEED_RESCHED, %cl #此时preempt_count=0,如果need_resched标志置位,则发生调度。反之则跳转到restore_all
jz restore_all
#如果是发生了异常,则不会进行抢占调度,此时preempt_count=0,且need_resched置位
testl $IF_MASK,EFLAGS(%esp) # interrupts off (exception path) ?
jz restore_all
#将最大值赋值给preempt_count,表示不允许再次被抢占
movl $PREEMPT_ACTIVE,TI_preempt_count(%ebp)
sti
#调度
call schedule
#preempt_count还原为0
movl $0,TI_preempt_count(%ebp)
cli
#跳转到need_resched,判断是否又需要发生被调度
jmp need_resched
#endif
分析完内核态且不是VM86下的情况,我们接着分析用户态的情况
……
jz resume_kernel # 如果中断前不是在用户空间,且不是在VM86模式下,跳转到resume_kernel
ENTRY(resume_userspace) # 用户模式下
cli #开中断,以防中断丢失
movl TI_flags(%ebp), %ecx #将task->flags成员的值存进ecx
andl $_TIF_WORK_MASK, %ecx # is there any work to be done on 还有事情没做完?
# int/exception return?
jnev #还有事情没有处理完
jmp restore_all #所有事情都处理完了
restore_all的代码我们在上面已经分析完了.我们继续分析如果还有后续事情没有处理的情况:
work_pending的处理:
work_pending:
#在返回用户空间时,只需要判断need_resched是否置位,不需要判断preempt_count,如果
#置位则发生调度,反之则跳转到work_notifysig
testb $_TIF_NEED_RESCHED, %cl
jz work_notifysig
work_resched:
call schedule
cli # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
#继续测试是否还有其他额外的事情要处理,如果没有,则跳转到restore_all
movl TI_flags(%ebp), %ecx
andl $_TIF_WORK_MASK, %ecx # is there any work to be done other
# than syscall tracing?
jz restore_all