- 论坛徽章:
- 0
|
本帖最后由 71v5 于 2014-07-05 17:13 编辑
先简单描述一下中断和异常相关的基本概念:
中断是一种机制,这种机制使I/O设备能够打断cpu当前正在进行的计算,这种机制使cpu能够同时进行计算和I/O操作。
异常,当cpu在执行指令的时候探测到一个错误条件,就会产生一个异常,比如被零除,访问的数据当前不在内存中等。
不管是中断还是异常,内核是通过中断向量号来识别的,cpu将中断向量号做为中断描述表的索引,相应的表项中包含了
中断处理程序或者异常处理程序的地址。
这里将不讨论I/O设备产生的中断,因为其牵扯的内容多且有点复杂,留作以后分析。
因为异常是cpu内部识别的,所以其对应的中断向量号是固定的,处理期间中断对应的中断向量号也是固定的,将从这些
中断和异常里面摘取几个典型简要分析一下,看看在能处理异常和中断前,freebsd9.2所完成的初始化工作以及
IA32提供的中断处理机制。
[IA32:高级可编程中断控制器(APIC)]-对于APIC,下面简单介绍一下相关概念以及用来发送处理器间中断的寄存器,详细信息
可以参考"Intel 64 and IA-32 ArchitecturesSoftware Developer’s Manual Volume 3A"第十章,将处理器间中断简称为IPI。
在IA32中,APIC分为local APIC和I/O APIC两种:
local APIC,主要用来完成:
1:从cpu的处理器引脚,或者外部中断源,或者外部的I/O APIC接收中断,并将中断发送给处理器核心进行处理。
2:在SMP系统中,local APIC从系统总线上接收其它cpu发送的IPI消息或者将IPI消息通过系统总线发送给其它cpu,IPI消息
也可以用来在系统中的cpu间分发中断。
I/O APIC:主要用来接收I/O设备产生的中断,并且将中断转换为中断消息发送给local APIC,关于I/O 1PIC将不会去深入分析,
知道其作用就行;作为一个实例,大家可以参考82093AA I/O APIC这个中断控制器芯片的详细说明。
local APIC和I/O APIC的关系图如下所示:
local APIC包含了一组寄存器,在local APIC能正确工作之前,要初始化这些寄存器,其中有些寄存器是只读的,在SMP系统的配置
中,这组寄存器被映射到一个大小为4KB的连续物理地址空间中,起始物理地址为FEE00000H,当然也可以通过修改IA32_APIC_BASE MSR
这个寄存器来更改起始物理地址。
下面简单描述一下两个local APIC寄存器:Local APIC ID Register和Interrupt Command Register。
[Local APIC ID Register]:
在加电时,系统硬件会给每个local APIC分配一个唯一的APIC ID,系统硬件基于系统拓扑以及编码插槽位置信息等获取一个APIC ID。
在SMP系统中,BIOS和操作系统使用local APIC ID 作为cpu的id,和前面描述的logical cpu id不同,该寄存器如下图所示。
[Interrupt Command Register]-该寄存器是64bit,主要用来发送IPI,为了发送一个IPI,必须要正确的构造这个寄存器,向低32位
执行一个写操作,将导致一个IPI被发送,寄存器详细格式如下图所示,正确构造这个寄存器就是用正确的值来初始化其中的字段:
只关心下面的字段:
Vector:中断向量号。
Delivery Mode:指定了IPI发送类型,Fixed之外的类型大部分在多处理器启动阶段使用,用来启动AP等。
Delivery Status:指示IPI的发送状态。0:IPI已经成功发送出去;1:还没有完成IPI的发送。
Destination:IPI的目的地,只有当Destination Shorthand字段为00时,才使用,此时该字段的值为目的cpu的local APIC ID。
Destination Shorthand:使用速记符号指定IPI的目的地,编码如下:
00:IPI目的地在Destination字段中指定。
01:IPI的目的地仅包括发送此IPI的cpu。
10:IPI的目的地为系统中全部cpu,包括发送此IPI的cpu。
11:IPI的目的地为系统中除发送此IPI的cpu以外的cpu。
[freebsd9.2:怎么访问local APIC寄存器组]:- /*************************************************************************************************************
- * typedef struct LAPIC lapic_t;
- lapic:一个虚拟线性地址,通过该地址可以访问local APIC寄存器组。
- lapic_paddr:local APIC寄存器组的物理地址。
- volatile lapic_t *lapic;
- vm_paddr_t lapic_paddr;
- 在freebsd9.2中,通过变量lapic访问local APIC寄存器组。
- 关于变量lapic的初始化过程,大概说明一下,需要一点ACPI规范方面的知识,ACPI规范定义了一个
- 名为Multiple APIC Description Table的表,该表包含了一个32bit的物理地址(这里假设为apic_phyaddr),
- 系统中所有cpu都可以通过apic_phyaddr访问自己的local APIC寄存器组。在系统初始化的最初阶段,初始化函数
- apic_init除了获取系统中每个cpu的local APIC ID外,还要获取Multiple APIC Description Table的物理地址
- 并将这个物理地址保存到变量madt_physaddr中;之后的初始化函数lapic_init会从Multiple APIC Description Table
- 获取apic_phyaddr,将apic_phyaddr赋值给lapic_paddr,同时建立一个到内核地址空间的映射,将apic_phyaddr开始
- 的这段物理内存映射到内核地址空间,返回的内核虚拟线性地址保存到lapic变量中。
- 关于struct LAPIC类型,这里只列出两个成员,描述Local APIC ID Register和Interrupt Command Register:
- ********************************************************************/
- 127 struct LAPIC {
- 。。。。。。。。。。。。。。。。。。。。。。。。。。
- 130 u_int32_t id; PAD3;
- 。。。。。。。。。。。。。。。。。。。。。。。。。。
- 176 u_int32_t icr_lo; PAD3;
- 177 u_int32_t icr_hi; PAD3;
- 。。。。。。。。。。。。。。。。。。。。。。。。。。
- 192 };
复制代码 [IA32:中断向量号,中断描述表(IDT)]:
中断向量号:
为了帮助处理中断和异常,IA32给需要由处理器特殊处理的异常和中断条件分配了唯一的标识号,这个唯一的标识号就是中断向量号;
中断向量号的范围是0-255,IA32将0-31之间的中断向量号保留给其定义的内部异常和中断。
中断描述符表(IDT):
IDT可以看成是一个门描述符(大小为8Byte)的数组,cpu使用中断向量号作为IDT的索引,数组元素数目为256,IDT中的门描述符的格式和GDT
中段描述符的格式基本相同,只不过其中某些字段取值不一样,IDT中门描述符的标准格式如下图所示:
这里简单描述下面三个字段:
Segment Selector:
中断处理程序或者异常处理程序所在段的Segment Selector,因为中断处理程序或者异常处理程序一般都位于内核地址空间中,
所以在freebsd9.2中,描述符中该字段都设置为KCSEL,即内核代码段的Segment Selector。
Offset:
中断处理程序或者异常处理程序的地址。
DPL:
门描述符的特权级;根据IA32的保护机制,如果中断处理程序或者异常处理程序在一个较低特权级(数值大)的代码段中,那么cpu将不会执行
一个控制转移,试图违背这个保护机制的话,就会产生一个general-protection exception;如果中断或者异常是通过INT n指令产生的,
那么当前当前CS段寄存器中RPL中的值一定要小于或等于门描述符中DPL字段中的值,否则就会产生一个general-protection exception。
Interrupt Descriptor Table Register(IDTR)-用来访问IDT,lidt指令用来加载IDTR,格式如下:- Interrupt Descriptor Table Register(IDTR)-用来访问IDT,lidt指令用来加载IDTR,格式如下:
- bit47 bit0
- ***************************************************************************
- * * *
- * 32-bit Linear Base Address * 16-Bit Table Limit *
- ***************************************************************************
- Base Address指定了IDT的线性基地址,Table Limit指定了IDT的大小字(节数)。
复制代码 [IDTR和IDT的关系如下图所示]:
[freebsd9.2:建立IDT]-相关数据结构:
[struct gate_descriptor]-用来描述IDT中的表项:- /****************************************************************************
- * #define NIDT 256
- *
- * static struct gate_descriptor idt0[NIDT];
- struct gate_descriptor *idt = &idt0[0];
-
- 数组idt0就是IDT,变量idt是为了方便setidt函数的操作。
- 因为IA32中IDT的最大数目为256,所以这里数组idt0跟IA32保持一致。
- 对于struct gate_descriptor对象中的成员,和上面图中所示的门描述
- 中的字段一一对应,这里忽略成员gd_stkcpy,gd_xx,因为这两个成员
- 在门描述符对应的字段没有使用,一般设置为0:
- gd_looffset:中断或异常处理程序的低16bit。
- gd_selector:中断或异常处理程序所在段的segment selector。
- gd_type:门描述符的类型,一般情况下为中断门或者陷阱门,当通过
- 中断门访问中断或异常处理程序时,cpu清EFLAGS寄存器中的
- IF标志,而通过陷阱门访问中断或异常处理程序时,不会
- 清这个标志。
- gd_dpl:门描述符的特权级,一般情况下,这个字段都设置为0;有
- 一个例外,就是系统调用处理异常,该异常的中断向量号为0x80,
- IDT中该向量号对应的门描述符类型是一个陷阱门,而且门描述符
- 的DPL字段的值为3,这样在用户态,才能够正常执行一个int 0x80
- 软中断指令陷入内核。
- gd_p:段描述符是否在内存中,始终设置为1。
-
- gd_hioffset:中断或异常处理程序的高16bit。
- 和GDT不同,freebsd9.2中只有一个IDT。
- ********************************************************/
- 88 struct gate_descriptor {
- 89 unsigned gd_looffset:16 ; /* gate offset (lsb) */
- 90 unsigned gd_selector:16 ; /* gate segment selector */
- 91 unsigned gd_stkcpy:5 ; /* number of stack wds to cpy */
- 92 unsigned gd_xx:3 ; /* unused */
- 93 unsigned gd_type:5 ; /* segment type */
- 94 unsigned gd_dpl:2 ; /* segment descriptor priority level */
- 95 unsigned gd_p:1 ; /* segment descriptor present */
- 96 unsigned gd_hioffset:16 ; /* gate offset (msb) */
- 97 } ;
复制代码 [struct region_descriptor]-加载IDTR使用:- /**************************************************************************************
- * struct region_descriptor r_idt;
-
- struct region_descriptor类型的数据对象只在使用lidt指令加载IDTR时使用:
- rd_limit:IDT的大小。
- rd_base:IDT的线性基地址。
- 在初始化函数init386中,会对r_idt的成员做如下设置,在这之前已经调用函数setidt建立了
- 部分异常号对应的门描述符:
-
- r_idt.rd_limit = sizeof(idt0) - 1;
- r_idt.rd_base = (int) idt;
- lidt(&r_idt);
- lidt指令加载BSP的IDTR。
- *****************************************/
- 164 struct region_descriptor {
- 165 unsigned rd_limit:16; /* segment extent */
- 166 unsigned rd_base:32 __packed; /* base address */
- 167 };
复制代码 [函数setidt]-建立IDT中的门描述符:- /*************************************************************************************
- * 函数setidt比较简单,基本是一些赋值操作,下面描述一下参数:
- idx: 中断向量号。
- func:中断或异常处理程序地址。
- type:要建立门描述符的类型。
- dpl: 要建立门描述符的特权级
- selec:中断或异常处理程序所在段的segment selector
- 该函数就是使用参数idx作为IDT的索引,确定相应的表项,然后使用参数建立相应的
- 门描述符。
- *****************************************/
- 1919 void
- 1920 setidt(idx, func, typ, dpl, selec)
- 1921 int idx;
- 1922 inthand_t *func;
- 1923 int typ;
- 1924 int dpl;
- 1925 int selec;
- 1926 {
- 1927 struct gate_descriptor *ip;
- 1928
- 1929 ip = idt + idx;
- 1930 ip->gd_looffset = (int)func;
- 1931 ip->gd_selector = selec;
- 1932 ip->gd_stkcpy = 0;
- 1933 ip->gd_xx = 0;
- 1934 ip->gd_type = typ;
- 1935 ip->gd_dpl = dpl;
- 1936 ip->gd_p = 1;
- 1937 ip->gd_hioffset = ((int)func)>>16 ;
- 1938 }
复制代码 有了前面描述做准备,下面看两个比较典型的中断或者异常如何建立门描述符的。
1:系统调用异常,对应的异常向量号是0x80,即在IDT中的索引为128,系统调用异常处理程序在初始化函数init386中注册到IDT中,如下所示:- 系统调用异常对应的异常处理程序为Xint0x80_syscall,通过readelf读取内核符号表,该符号的值为c0f36640。
- setidt(IDT_SYSCALL, &IDTVEC(int0x80_syscall), SDT_SYS386TGT, SEL_UPL,GSEL(GCODE_SEL, SEL_KPL));
-
- 结合上面的setidt函数,相应的门描述符个字段取值如下:
- idt[128].gd_looffset = 6640;// 系统调用异常处理程序地址低16位
- idt[128].gd_selector = KCSEL;// 内核代码段的segment selector
- idt[128].gd_stkcpy = 0;
- idt[128].gd_xx = 0;
- idt[128].gd_type = SDT_SYS386TGT;// 15 即为一个陷阱门
- idt[128].gd_dpl = SEL_UPL;// 3,注意这里是最低特权级3,只有这样用户程序才能执行int 0x80发出一个软中断。
- idt[128].gd_p = 1;
- idt[128].gd_hioffset = c0f3 ;// 系统调用异常处理程序地址高16位
复制代码 2:中断向量号为IPI_BITMAP_VECTOR的中断,宏IPI_BITMAP_VECTOR的值为249,对应的中断处理程序在初始化函数cpu_mp_start中注册到IDT中,如下所示:- 中断向量号为IPI_BITMAP_VECTOR的中断处理程序为Xipi_intr_bitmap_handler,通过readelf读取内核符号表,该符号的值为c0f36cb0。
- setidt(IPI_BITMAP_VECTOR, IDTVEC(ipi_intr_bitmap_handler),SDT_SYS386IGT, SEL_KPL, GSEL(GCODE_SEL, SEL_KPL));
- 结合上面的setidt函数,相应的门描述符各个字段取值如下:
- idt[249].gd_looffset = 6cb0;// 中断处理程序地址低16位
- idt[249].gd_selector = KCSEL;// 内核代码段的segment selector
- idt[249].gd_stkcpy = 0;
- idt[249].gd_xx = 0;
- idt[249].gd_type = SDT_SYS386IGT;// 14 即为一个中断门
- idt[249].gd_dpl = SEL_KPL;// 0,注意这里是最高特权级0。
- idt[249].gd_p = 1;
- idt[249].gd_hioffset = c0f3 ;// 中断处理程序地址高16位
复制代码 下面看看再将控制传递到异常处理程序或者中断处理程序之前,cpu硬件单元所做的处理,这里以收到一个中断向量号为IPI_BITMAP_VECTOR的IPI为例,假设
此时正在运行线程intr_thread,并且cpu处于用户态,其内核栈如下所示,在进行线程切换时,TSS的tss_esp0字段已经设置为线程intr_thread内核栈的栈顶,
TSS的tss_ss0成员为KDSEL即内核数据段的segment selector:- ******************************* 内核栈顶部 高地址方向
- * *
- * struct pcb对象 *
- * *
- * *
- ******************************* <--struct thread的td_pcb成员指向这里
- * *
- * 16bytes for vm *
- ******************************* TSS.esp0指向这里,第6步执行完后
- * SS *
- *******************************
- * ESP *
- ******************************* 第7步执行完后,ESP指向这里
- * EFLAGS *
- *******************************
- * CS *
- *******************************
- * EIP *
- ******************************* 第8步执行完后,ESP指向这里
- * *
- * *
- * *
- * *
- * *
- * *
- * *
- *******************************------ 内核栈底部 低地址方向----
复制代码 那么cpu硬件单元将做:
1:
获取对应的中断向量号,这里为249。
2:
从IDTR中获取IDT的基地址,读IDT的第249项,这里就为数组元素idt[249]的内容,即中断向量号249对应的门描述符。
3:
从GDTR中获取GDT的基地址,因为门描述符idt[249]中的Segment Selector为KCSEL,所以这里读取内核代码段对应
的段描述符。
4
使用第3步获取的段描述符中DPL字段在TSS中选择一个新栈(segment selector和栈顶指针);因为此时中断处理程序Xipi_intr_bitmap_handler在内核代码段中,
内核代码段段描述中DPL字段的值为0,所以此时就选择tss_esp0和tss_ss0。
5:
在cpu内部临时保存当前ESP和SS寄存器的值,这两个寄存器的值为线程intr_thred的用户态硬件上下文的一部分。
6:
用tss_esp0加载ESP寄存器,tss_ss0加载SS寄存器。期间cpu硬件单元还要进行很多的权限检查,一旦违例,就会产生一个异常。
7:
将第5步临时保存的ESP和SS寄存器的值压入栈中。
8:
将寄存器EFLAGS,CS,EIP的值压入栈中,这三个寄存器的值也为线程intr_thread的用户态硬件上下文的一部分。
9:
用内核代码段的Segment Selector加载CS寄存器,用第2步获取的门描述符中offset字段的值加载EIP寄存器,这里将用
KCSEL加载CS寄存器,用中断处理程序Xipi_intr_bitmap_handler的地址c0f36cb0加载EIP寄存器。
第9步执行完后,因为CS和EIP寄存器已被更改,所以将开始执行中断向量号为IPI_BITMAP_VECTOR对应的中断处理程序。 |
|