分类:
2009-10-06 12:01:29
IRQ:
每个设备至少有一条IRQ设备线,每条线都会连接到该设备的PIC(中断控制器),是多对一的关系。
PIC的功能是:
1) PIC监视该设备的IRQ线,如果IRQ有请求,则PIC选择请求队列中,最低编号的那个IRQ
2) PIC将该IRQ线的请求信号组合成一个中断向量
3) PIC将该向量存放在PIC的IO端口里,以便CPU检测到
4) 做好这些准备工作后,PIC向CPU的INTR发消息,通知中断发生
5) 等待CPU往PIC的某个IO端口写结束标志,然后将INTR信号清理
6) 返回到第一步的循环
对于特定的每条IRQ设备线,PIC可以选择是否启动它,如果该IRQ线被disable,则中断控制器PIC不会将此中断提交到CPU,但是会保留此中断,一旦IRQ线被enable,则PIC立刻将此中断提交到CPU处理。
使能enable和无效disable IRQ线,与通常意义上的屏蔽中断有些区别。对于X86来说,和屏蔽有关的寄存器为eflags的IF位,如果该位清零,则所有被PIC提交至CPU的中断都被忽略。对于powerpc e500来说,相应的寄存器为MSR。
通常情况下,在单CPU系统中,PIC可以直接连接到CPU的INTR pin。如果是多CPU系统,PIC应该怎么连接?
对于多CPU来说,PIC被替换成另外一种更高级的中断控制器,对X86系列,是APIC;对于Powerpc来说,是MPIC。
每个CPU有一个自己的local APIC,local APIC连接到所有CPU公有的external APIC。
每个local APIC具有2条IRQ中断线LINT0和LINT1,而external APIC有24条IRQ中断线,以及一个具有24个entry的中断定向表(Interrupt Redirection Table),该定向表的作用是,当外部中断到达external APIC时,由中断表来确定发送到哪个local APIC。
对于外部中断的分发,有两种方式:
1) 静态分发。IRQ信号根据定向表中的设定,往特定的CPU发放
2) 动态分发。IRQ信号根据每个CPU的运行的进程优先级来决定发送至哪个CPU,一般该CPU正在执行的进程,优先级越低的,会更容易被发送到。该CPU运行的进程优先级,是由该CPU的local APIC里的TPR来保存的。这个优先级的控制,是由软件,也就是内核来设置的。假定某个时刻,两个CPU执行的进程优先级一样,则再比较该CPU的arbitration priority register,选取优先级高的那个CPU,注意,这里按我的理解,应该是选取较高优先级的那个。另外,关于arbitration priority的计算,是按照round-robin来计算的,该机制是由硬件来实现。每个CPU被分配一个0到15的优先级,(可以看出,这个算法最多支持16核),当某个CPU被分配到去处理一个中断后,它的LOCAL APIC里的arbitration priority会被置0,然后其他的CPU相应加1,如果其他CPU已经是15了,则该CPU的arbitration被置为上一次获得中断的CPU的arbitration值加1,-----说的这么复杂,其实就一句话,如果是15,就被置1。这样,一定不会有冲突。
Ps:核间中断也是通过local APIC和extrenal APIC发送的。
Interrupt Descriptor Table (IDT )即是存放中断向量的数据结构,理论上可以存放在内存的任意位置。每个IDT的entry由8字节组成,也最多可以有256个entry。Entry的类型有三种:
1) TASK gate
2) Interrupt gate 处理中断
3) Trap gate 处理异常
中断,异常的硬件处理过程
CPU的control unit负责在一条指令结束后,判断是否在上一条指令执行时产生了中断或者异常,如果有,则进行如下操作:
1) 判断当前中断号(0到255,前面说过,IDT最多256个entry)
2) 找到第i个entry,根据这个entry的selector字段,再到GDT里取出相应的段描述符
3) 检查中断的有效性,即,先从cs寄存器的CPL字段(Current Priviledge Level),是否小于等于GDT的entry—segment descriptor里的DPL字段,如果不是,则发一个“保护”异常。注意,这段内容是在深入理解内核一书中摘抄来的,该书的表述有误,原文是DPL大于CPL会出异常。解释:此时,cs寄存器是代码段基地址,eip是偏移,eip指向下一条指令。CPL的值是“用户态”的特权级,而DPL是中断程序的特权级。
4) 如果是编程异常,还会检查CPL和IDT中的DPL,看是否小于IDT中描述符的特权级。为什么要这么设计?
5) 检查是否发生特权级变化,就是说,特权级提升。如果是的话,要建立新的堆栈
a) 通过读tr寄存器,获得当前进程TSS段,TSS段的作用,个人理解是,在中断处理过程中,CPU通过该段得到内核堆栈的地址;
b) 用中断栈的地址替换当前ss和esp指针;
c) 在新栈中压入之前的ss和esp的值。
6) 保留eflags,cs以及eip的值到栈里,(即应该执行的下一条用户指令地址)
7) 用IDT表中的段选择符和偏移分别赋给cs和eip寄存器。注意,是用IDT的选择符来替换cs,因为代码寄存器中,存放的都是段选择符。
最后一步7,实际就是跳转到中断处理程序,处理完后,执行ret,该指令的作用是,用保存在栈上的原cs,eip,eflags替换现有寄存器(返回到用户态),如果用户态处理程序的优先级和当前中断处理一样,则ret结束,否则,从栈中再恢复ss,esp,返回到用户态处理程序栈,检查所有段寄存器,如果DPL小于CPL(用户态特权大于中断特权),则清零。这样做,是为了保证用户态程序不会提高优先级。
关于中断嵌套,书里讲的不多,只说了中断嵌套的好处:
a) 当设备给CPU发送中断后,就会阻塞,一直等到CPU回应。由于允许内核的中断嵌套,所以内核可以及时的发送回应给设备
b) 中断没有优先级。所有的中断一来,就打断上一个中断,这样设计可以保证内核的简单,然而可能会出现饿死等情况。
关于中断门的初始化:
在X86的setup_idt函数里,将IDT的256个entry全部置为ignore_int地址(代码段选择符eax+偏移ignore_init edx)
setup_idt:
lea ignore_int, %edx
movl $(_ _KERNEL_CS << 16), %eax
movw %dx, %ax /* selector = 0x0010 = cs */
movw $0x8e00, %dx /* interrupt gate, dpl=0, present */
lea idt_table, %edi
mov $256, %ecx
rp_sidt:
movl %eax, (%edi)
movl %edx, 4(%edi)
addl $8, %edi
dec %ecx
jne rp_sidt
ret
程序对异常的处理过程:
1) 保存当前寄存器到内核态堆栈
2) 调用高级C异常处理函数
3) 从处理程序返回
类似于:handler_name:
pushl $0
pushl $do_handler_name
jmp error_code
其中,error_code函数,最终调用do_handler_name执行异常处理
在do_handler_name里,会将当前进程控制块current的异常向量等字段,设置为当前向量(什么意思??)然后往当前进程发信号(什么信号?)
current->thread.error_code = error_code;
current->thread.trap_no = vector;
force_sig(sig_number, current);
最后do_handler_name调用ret_from_exception,从异常返回。