4.6.7 中断描述符表
中断描述符表(Interrupt Descriptor Table,IDT)将每个异常或中断向量分别与它们的处理过程联系起来。与GDT和LDT表类似,IDT也是由8字节长描述符组成的一个数组。与GDT 不同的是,表中第1项可以包含描述符。为了构成IDT表中的一个索引值,处理器把异常或中断的向量号乘以8。因为最多只有256个中断或异常向量,所以 IDT无需包含多于256个描述符。IDT中可以含有少于256个描述符,因为只有可能发生的异常或中断才需要描述符。不过IDT中所有空描述符项应该设置其存在位(标志)为0。
IDT表可以驻留在线性地址空间的任何地方,处理器使用IDTR寄存器来定位IDT表的位置。这个寄存器中含有IDT表32位的基地址和16位的长度(限长)值,如图4-26所示。IDT表基地址应该对齐在8字节边界上以提高处理器的访问效率。限长值是以字节为单位的IDT表的长度。
图4-26 中断描述符表IDT和寄存器IDTR
LIDT和SIDT指令分别用于加载和保存IDTR寄存器的内容。LIDT指令用于把内存中的限长值和基地址操作数加载到IDTR寄存器中。该指令仅能由当前特权级CPL是0的代码执行,通常被用于创建IDT时的操作系统初始化代码中。SIDT指令用于把IDTR中的基地址和限长内容复制到内存中。该指令可在任何特权级上执行。
如果中断或异常向量引用的描述符超过了IDT的界限,处理器会产生一个一般保护性异常。
4.6.8 IDT描述符
IDT表中可以存放3种类型的门描述符:中断门(Interrupt gate)描述符、陷阱门(Trap gate)描述符、任务门(Task gate)描述符。
图4-27给出了这三种门描述符的格式。中断门和陷阱门含有一个长指针(即段选择符和偏移值),处理器使用这个长指针把程序执行权转移到代码段中异常或中断的处理过程中。这两个段的主要区别在于处理器操作EFLAGS寄存器IF标志上。IDT中任务门描述符的格式与GDT和LDT中任务门的格式相同。任务门描述符中含有一个任务TSS段的选择符,该任务用于处理异常或中断。
图4-27 中断门、陷阱门和任务门描述符格式
4.6.9 异常与中断处理
处理器对异常和中断处理过程的调用操作方法与使用CALL指令调用程序过程和任务的方法类似。当响应一个异常或中断时,处理器使用异常或中断的向量作为IDT表中的索引。如果索引值指向中断门或陷阱门,则处理器使用与CALL指令操作调用门类似的方法调用异常或中断处理过程。如果索引值指向任务门,则处理器使用与CALL指令操作任务门类似的方法进行任务切换,执行异常或中断的处理任务。
异常或中断门引用运行在当前任务上下文中的异常或中断处理过程,如图4-28所示。门中的段选择符指向GDT或当前LDT中的可执行代码段描述符。门描述符中的偏移字段指向异常或中断处理过程的开始处。
(点击查看大图)图4-28 中断过程调用
当处理器执行异常或中断处理过程调用时会进行以下操作:
(1)如果处理过程将在高特权级(如0级)上执行时就会发生堆栈切换操作。堆栈切换过程如下:
处理器从当前执行任务的TSS段中得到中断或异常处理过程使用的堆栈的段选择符和栈指针(例如tss.ss0、tss.esp0)。然后处理器会把被中断程序(或任务)的栈选择符和栈指针压入新栈中,如图4-29所示。
图4-29 转移到中断处理过程时堆栈的使用方法
接着处理器会把EFLAGS、CS和EIP寄存器的当前值也压入新栈中。
如果异常会产生一个错误号,那么该错误号也会被最后压入新栈中。
(2)如果处理过程将在被中断任务同一个特权级上运行,那么:
处理器把EFLAGS、CS和EIP寄存器的当前值保存在当前堆栈上。
如果异常会产生一个错误号,那么该错误号也会被最后压入新栈中。
为了从中断处理过程中返回,处理过程必须使用IRET指令。IRET指令与RET指令类似,但IRET还会把保存的寄存器内容恢复到EFLAGS 中。不过只有当CPL是0时才会恢复EFLAGS中的IOPL字段,并且只有当CPL不大于IOPL时,IF标志才会被改变。 如果当调用中断处理过程时发生了堆栈切换,那么在返回时IRET指令会切换到原来的堆栈。
(1)异常和中断处理过程的保护
异常和中断处理过程的特权级保护机制与通过调用门调用普通过程类似。处理器不允许把控制转移到比CPL更低特权级代码段的中断处理过程中,否则将产生一个一般保护性异常。另外,中断和异常的保护机制在以下方面与一般调用门过程不同:
因为中断和异常向量没有RPL,因此在隐式调用异常和中断处理过程时不会检查RPL。
只有当一个异常或中断是由INT n、INT 3或INT 0指令产生时,处理器才会检查中断或陷阱门中的DPL。此时CPL必须小于或等于门的DPL。这个限制可以防止运行在特权级3的应用程序使用软件中断访问重要的异常处理过程,例如页错误处理过程,假设这些处理过程已被存放在更高特权级的代码段中。对于硬件产生的中断和处理器检测到的异常,处理器会忽略中断门和陷阱门中的DPL。
因为异常和中断通常不会定期发生,因此这些有关特权级的规则有效地增强了异常和中断处理过程能够运行的特权级限制。我们可以利用以下技术之一来避免违反特权级保护:
异常或中断处理程序可以存放在一个一致性代码段中。这个技术可以用于只需访问堆栈上数据的处理过程(如除出错异常)。如果处理程序需要数据段中的数据,那么特权级3必须能够访问这个数据段。但这样一来就没有保护可言了。
处理过程可以放在具有特权级0的非一致代码段中。这种处理过程总是可以执行的,与被中断程序或任务的当前特权级CPL无关。
(2)异常或中断处理过程的标志使用方式
当通过中断门或陷阱门访问一个异常或中断处理过程时,处理器会在把EFLAGS寄存器内容保存到堆栈上之后清除EFLAGS中的TF标志。清除TF标志可以防止指令跟踪影响中断响应。而随后的IRET指令会用堆栈上的内容恢复EFLAGS的原TF标志。
中断门与陷阱门唯一的区别在于处理器操作EFLAGS寄存器IF标志的方法。当通过中断门访问一个异常或中断处理过程时,处理器会复位IF标志以防止其他中断干扰当前中断处理过程。随后的IRET指令则会用保存在堆栈上的内容恢复EFLAGS寄存器的IF标志。而通过陷阱门访问处理过程并不会影响 IF标志。
(3)执行中断处理过程的任务
当通过IDT表中任务门访问异常或中断处理过程时,就会导致任务切换。从而可以在一个专用任务中执行中断或异常处理过程。IDT表中的任务门引用 GDT中的TSS描述符。切换到处理过程任务的方法与普通任务切换一样。由于本书讨论的Linux操作系统没有使用这种中断处理方式,因此这里不再赘述。
4.6.10 中断处理任务
当通过IDT中任务门来访问异常或中断处理过程时就会导致任务切换。使用单独的任务来处理异常或中断有如下好处:
被中断程序或任务的完整上下文会被自动保存。
在处理异常或中断时,新的TSS可以允许处理过程使用新特权级0的堆栈。在当前特权级0的堆栈已毁坏时如果发生了一个异常或中断,那么在为中断过程提供一个新特权级0的堆栈条件下,通过任务门访问中断处理过程能够防止系统崩溃。
通过使用单独的LDT给中断或异常处理任务独立的地址空间,可以把它与其他任务隔离开来。
使用独立任务处理异常或中断的不足之处是:在任务切换时必须对大量机器状态进行保存,使得它比使用中断门的响应速度要慢,导致中断延时增加。
IDT中的任务门会引用GDT中的TSS描述符,如图4-30所示。切换到句柄任务的过程与普通任务切换过程相同。到被中断任务的反向链接会被保存在句柄任务TSS的前一任务链接字段中。如果一个异常会产生一个出错码,则该出错码会被复制到新任务堆栈上。
图4-30 中断处理任务切换
当异常或中断句柄任务用于操作系统中时,实际上有两种分派调度任务的机制:操作系统软件调度和处理器中断机制的硬件调度。使用软件调度方法时需要考虑到中断开启时采用中断处理任务。
- 中断信号的作用.
- 使CPU转而去运行正常控制流之外的代码.为了它.就要在内核态堆栈保存程序计数器的当前值(eip和cs寄存器).并把与中断类型相关的一个地址放在程序计数器.
- 中断处理与进程切换的差异:由中断或异常处理程序执行的代码不是一个进程,而是内核控制路径.代表中断发生时正在运行的进程执行.其比进程"轻".
- 中断和异常
- 中断:
- 可屏蔽中断(maskable): I/O设备发出的中断请求(irq)都属于.可处于两种状态:屏蔽的/非屏蔽的.
- 非屏蔽中断(nonmaskable): 只有几个危急事件才引起.总是由CPU辨认.
- 异常:
- 处理器探测异常:当CPU执行指令时探测到一个反常条件所产生的异常. 根据保存在eip寄存器中的值,分为3种; 1)故障(fault):通常可以被纠正.eip中保存的是引起故障的指令地址.纠正后会重新执行该条指令. ; 2)陷阱(trap):在陷阱指令执行后立刻报告.内核把控制器返回给程序后可以继续他的执行而不失连贯性. eip保存的是随后要执行的指令地址.只有当没有必要重新执行已终止的指令时(通常为了调试程序)时才触发陷阱. ; 3)异常中止(abort):不能在eip中保存引起异常的指令所在的确切位置.用于报告严重的错误.异常中止处理程序会强制受影响的进程终止.
- 编程异常: 在编程者发出请求时发生.将其作为陷阱来处理.也叫软中断.用途:1)执行系统调用.2)给调试程序通报一个特定的事件.
- 每个中断和异常由0~255之间的一个数来标示.称为向量(vector).只有可屏蔽中断的向量可以通过编程改变.其余都是固定的.
- IRQ和中断:能发出中断的设备都有一个IRQ的输出线.所有IRQ线都与一个可编程中断控制器(PIC)的硬件电路的输入引脚相连.可以有选择地禁止每条IRQ线.可对PIC编程从而禁止IRQ.禁止的中断不丢失.一旦激活,PIC就把他们发送到CPU.该特性运行中断处理程序逐次处理同一类型的IRQ.
- 为了发挥SMP体系的并行性,能够把中断传递给每个CPU很重要.所以引入了I/O高级可编程控制器(I/O APIC)的组件.所有CPU都含有一个本地APIC,通过APIC总线(在系统总线上)连接到外部的I/O APIC.还支持CPU产生处理器间中断(IPI),可以利用它来在CPU之间交换消息.
- 中断描述符表:IDT是一个系统表,它与每一个中断或异常向量相联系.每一个向量在表中有相应的中断或异常处理程序的入口地址.最多需要256*8=2048字节来存放IDT. idtr寄存器指定IDT的线性基地址及其最大长度,从而使IDT可以位于内存中的任何地方.分为3种类型: 1)任务门:信号发生时,必须取代当前进程的那个进程的TSS选择符存放在任务门中. ;2)中断门:包含段选择符和处理程序的段内偏移量.当CPU控制权转移到一个适当的断后,清除IF标志来关闭将来会发生的可屏蔽中断 ;3)陷阱门:与中断门相似,但控制权传递到一个适合的CPU时不修改IF标志. Linux利用中断门处理中断,利用陷阱门处理异常.注意: "Double fault"异常是唯一由任务们处理的异常.表示一种内核错误.
- 中断和异常处理程序的嵌套执行.
- 必须保证中断处理程序永不阻塞,即中断处理程序运行期间不能发生进程切换.因为嵌套的内核控制路径恢复执行时需要的数据都存放在当前线程的内核态堆栈上.
- 一个中断处理程序可以抢占其他的中断处理程序和异常处理程序.异常处理程序从不抢占中断处理程序.
- 初始化中断描述符表
- 过程: 1)在初始化系统时把IDT表的初始地址装入idtr寄存器,并初始化表中的每一项.; 2)int指令用于在用户态进程发出一个中断信号,为了防止模拟非法的中断,将门描述符的DPL=0.; 3)在用户态进程必须要能够发出一个编程异常时,将门的SPL=3。
- Linux中的分类: 中断门(DPL=0,所有LInux中断处理程序都通过中断门激活,并限制在内核态); 系统门(DPL=3,用来激活3个linux异常处理程序); 系统中断门(DPL=3,激活int3的异常处理程序); 陷阱门(DPL=0,激活大部分的异常处理程序); 任务门(DPL=0,"Double Fault的异常处理).
- idt的初始化分为两步:1)将256个表项用同一个中断门(即指向ignore_int()中断处理程序:其是一个空的处理程序)来填充.2)用有意义的陷阱和中断处理程序来代替空处理程序.
- 异常处理
- 大部分异常都解释成为出错条件.当异常发生时,内核向引起异常的进程发送一个信号向它通知一个反常条件.
- 特殊情况: 1)"Device not availeble" 2)"Page Fault"该异常推迟给进程分配新的页框,直到不能再推迟位置.
- 异常处理程序的标准结构: 1)在内核态堆栈中保存大多数寄存器的内容; 2)用C函数处理异常; 3)通过ret_from_exception函数从异常处理程序退出.
- 中断处理
- 由于一个进程被挂起好久后中断才到达,因此一个完全无关的进程可能正在运行.所以发送信号给当前进程是无用的.
- 中断处理依赖于中断类型:1)I/O中断(查询设备以确定适当的操作过程); 2)时钟中断(该中断告诉内核一个固定的时间间隔已经过去,作为I/O中断处理) ;3)处理器间中断.
- I/O中断处理:要能给多个设备同时提供服务.实现: 1)IRQ共享(每个ISR(中断服务例程)是一个与单独设备(共享IRQ线)相关的函数,因为无法预知那个特定的设备产生IRQ,所以,中断处理程序执行多个ISR,以验证它的设备是否需要关注,如果是,当设备产生中断时就执行所需的操作); 2)IRQ动态分配(一条IRQ线在可能的最后时刻才与一个设备相关联.这样,即使几个设备并不共享IRQ线,但同一IRQ向量也可以由这几个设备在不同时刻使用).
- 一个中断处理程序正在执行时,相应的IRQ线上发出的信号被暂时忽略; 中断处理程序所代表的进程必须是出于Task_Running态的; 其不能执行任何阻塞过程.
- 中断要执行的操作: 1)紧急的(在禁止可屏蔽中断下立即执行); 2)非紧急的(在开中断下立即执行); 3)非紧急可延迟的(由独立的函数执行).
- 步骤:1)在内核态堆栈中保存IRQ的值和寄存器的内容; 2)为正在给IRQ线服务的PIC发送一个应答来允许PIC进一步发出中断; 3)执行共享这个IRQ的所有设备的ISR; 4)跳到ret_from_intr()的地址后终止.
- IRQ数据结构: 1)意外中断:中断内核没有处理的中断.原因是与某个IRQ线相关的ISR不存在或者与某个中断线相关的所有例程都识别不出来.当一条IRQ线上的意外中断次数过多时,就禁用这条IRQ线. 2)
- IRQ在多CPU系统上的分发:对称多处理器模型(SMP).一般情况下,内核能够公平地在CPU间分发中断.但是在某些情况下,Linxu需要使用kirqd的内核线程来纠正IRQ的自动分配.其利用了CPU的IRQ亲和力:通过修改I/O APIC的中断重定向表表项,可以吧中断信号发送到某个特定的CPU上.
- CPU间中断处理:IPI不通过IRQ线传输,而是作为信号直接放在连接所有CPU本地APIC总线上. 类型: 1)Call_Function_Vector(发往不包含发送者的所有CPU,强制这些CPU运行发送者传递过来的函数). 2)Reschedule_Vector(从中断返回后,所有的重新调度都自动运行); 3)Invalidate_TLB_Vector(强制TLB无效,来刷新CPU的TLB).
- 软中断及tasklet:
- 可延迟中断可以在开中断的情况下执行.把可延迟中断从中断处理程序中抽出来有助于使内核保持较短的响应时间.Linux通过两种非紧迫,可中断内核函数:1)可延迟函数;2)通过工作队列来执行的函数.
- tasklet是在软中断之上实现的.软中断的分配是静态的,tasklet的分配和初始化是在运行时.软中断是可重入函数并使用自旋锁来保护数据,其实可以并行在多CPU上执行的,tasklet总是串行执行,但是不同类型的tasklet可以并发执行,其不必是可重入的.四种操作:1)初始化;2)激活;3)屏蔽;4)执行.激活和执行被绑定在一个CPU上,虽然可以更好地利用CPU的Cache,但是有潜在的危险性(一个CPU很忙,但其他的很闲).
- 软中断:使用下标(共6个)来表示优先级.softirq_action[32] softirq_vec数组.优先级是下标,所以只有前6元素个有效.另外,thread_info中有一个preempt_count字段来跟踪内核抢占和内核控制路径的嵌套.
- 每个CPu都有自己的ksoftirqd/n内核线程.其为了解决以下问题:软中断函数可以重新激活自己,软中断的连续高流量可能会产生问题.不然就要选择以下两种之一的策略:1)忽略do_softirq()运行时新出现的软中断,此种情况的等待是不可接受的;2)不断地重新检查挂起的软中断,这种情况下,do_softirq()函数就会一直不返回,用户态程序实际上停止执行.解决:do_softirq()函数确定哪些软中断是挂起的,并执行他们的韩素华.如果已经执行的软中断又被激活,则do_softirq()唤醒内核线程并终止.内核线程有较低的优先级,因此用户程序有就会运行.但是,如果机器空闲(没有用户态程序需要运行时),挂起的软中断就很快被执行.
- tasklet:是I/O驱动程序中实现可延迟函数的首选.
- 工作队列
- 用来代替任务队列.允许内核函数被激活,而且稍后由一种叫做工作者线程的特殊内核线程来执行.
- 可延迟函数运行在中断上下文中,而工作队列中的函数运行在进程上下文中.执行可阻塞函数的方式是在进程上下文中运行,因为在中断上下文中不可能发生进程切换.两者都不能访问进程的用户态地址空间.