X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务

目录

1. 中断和异常概述

1.1 中断的分类

1.1.1 中断(Interrupt)

1.1.2 异常(Exception)

1.2 异常的分类

1.2.1 按异常的来源分类

1.2.2 按异常的性质和严重性分类

2. 保护模式中断处理机制

2.1 中断向量的分配

2.2 中断描述符表

2.2.1 实模式下的中断向量表

2.2.2 中断描述符表的构成

2.2.3 中断门与陷阱门

2.3 保护模式下中断控制转移过程

2.4 中断和异常发生时的特权级检查

2.4.1 不检查RPL

2.4.2 门级检查

2.4.3 段级检查

2.5 中断和异常发生时的栈切换过程

2.6 Linux 0.11中断门与陷阱门的部署

2.6.1 生成门描述符

2.6.2 中断门的部署

2.6.3 陷阱门的部署

2.6.4 系统门的部署

3. 保护模式中断设置

3.1 关闭中断

3.2 创建IDT

3.3 安装RTC中断门

3.4 加载中断描述符表寄存器IDTR

3.5 重新设置8259A主片中断向量

3.5.1 重新设置的原因

3.5.2 8259A初始化命令字

3.5.3 8259A初始化示例代码

3.6 设置RTC硬件

4. 抢占式多任务切换的实现

4.1 示例代码任务构成

4.2 在中断处理过程中实施任务切换

4.2.1 RTC中断处理函数

4.2.2 任务切换过程分析

4.2.3 在中断中切换任务状态分析

4.2.4 Linux中断上下文调度分析


1. 中断和异常概述

1.1 中断的分类


广义的中断包含硬件中断、软件中断和处理器内部异常,为了明确中断的性质,一般将中断和异常分开说明

1.1.1 中断(Interrupt)


中断包括硬件中断和软中断,


1.1.1.1 硬件中断


① 由外围硬件设备发出的中断信号引发,以请求处理器提供服务


② 硬件中断一般被中断控制器(e.g. 8259A或I/O APIC)收集,并发送到处理器


③ 硬件中断完全是随机产生的,与处理器执行不同步


④ 当中断发生时,处理器先执行完当前的指令,然后处理中断


中断号由中断控制器发送给处理器


1.1.1.2 软中断


① 由int n指令引发


中断号由软中断指令提供

1.1.2 异常(Exception)


异常是处理器内部产生的中断,表示在指令执行的过程中遇到了错误的状况,异常有如下2种分类方式

1.2 异常的分类

1.2.1 按异常的来源分类


1.2.1.1 指令执行异常


① 处理器在执行指令的过程中,检测到程序中的错误,并由此引发的异常


② 例如执行非法指令、在执行除法指令div/idiv时除数为0


1.2.1.2 程序调试异常


① 由into、int3和bound指令主动发起


② 这些指令允许在指令流的当前点上检查实施异常处理的条件是否满足,例如into指令在执行时将检查EFLAGS寄存器的OF标志位


说明:bound(Check Array Index Against Bounds)指令


bound指令用于检查数组的索引是否在边界之内,指令格式为,

;目的操作数是寄存器,包含数组的索引
;源操作数指向内存位置,那里存储了两个成对出现的字或双字
;分别是数组索引的下限和上限
bound r16, m16
bound r32, m32


如果数组的索引超限,则产生异常


1.2.1.3 机器检查异常


① 这种异常与处理器型号相关


② 处理器提供了一种对芯片内部和总线处理进行检查的机制,当检测到有错误发生时,将引发此类异常

1.2.2 按异常的性质和严重性分类


1.2.2.1 故障(Faults)


① 故障通常可以纠正,而纠正的过程在中断处理程序中进行


因为故障是可以纠正的,所以我们希望异常纠正后,可以重新执行引起故障的指令


③ 为了实现这一要求,当故障发生时,处理器压入栈中的CS & EIP是指向引起故障的那条指令(而不是像中断那样,指向下一条指令)


说明:故障使用示例


虚拟内存管理就基于故障实现,处理流程如下,


① 当处理器执行一条访问内存的指令时,如果发现目标段或者页不在内存中(P位为0),则触发异常


② 在异常处理程序中分配内存或进行磁盘的换入换出操作,同时将P位置为1


③ 当异常处理程序返回时,重新执行引起故障的内存访问指令,此时访问成功


1.2.2.2 陷阱(Traps)


① 陷阱通常在执行了截获陷阱条件的指令之后立即产生(如果陷阱条件成立的话)


② 陷阱通常用于调试目的,比如单步中断指令int3和溢出检测指令into


③ 陷阱中断允许任务从中断处理程序返回后继续进行,因此,当异常发生时,处理器压入栈中的CS & EIP指向截获陷阱指令的下一条指令的地址


1.2.2.3 终止(Aborts)


① 终止标志着严重的错误,例如硬件错误、系统表(e.g. GDT,LDT)中的数据无效


② 这类异常总是无法精确报告引起错误的指令的位置,在这种错误发生时,任务不可能重新启动


说明1:对于终止异常的处理


由于无法继续执行引起终止异常的任务,因此操作系统只能将该任务从系统中删除。一种方案是在终止异常对应的中断描述符位置部署一个任务门,切换到内核管理任务,然后在该任务中将引起终止异常的任务删除


我们分析一下Linux 0.11中对于终止异常的处理,以双重故障为例,


① 在中断处理函数中,会调用die函数

 


② 在die函数中会打印导致异常的任务信息(也就是当前任务),并调用do_exit函数

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第1张图片 


③ 在do_exit函数中,会终止当前任务,释放其资源,通知其父进程,并调用schedule切换到其他任务执行

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第2张图片 


可见对终止异常处理的核心是将引发异常的任务删除(不会也无法再返回了),并调度其他任务执行


说明2:对于某些异常,处理器在转入异常处理程序之前,会在当前栈中压入一个称为错误代码的数值,帮助程序进一步诊断异常产生的位置和原因

2. 保护模式中断处理机制

2.1 中断向量的分配


Intel处理器在保护模式下对异常向量的分配如下表所示,

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第3张图片


说明1:对于中断、故障、陷阱和终止这些概念上的分类,映射到处理器的处理流程中,体现在如下3点,


① 中断向量的来源(处理器内部生成 / 中断控制器提供 / 指令提供)


② 中断发生时,处理器压栈的CS & EIP指向(引起中断的指令 / 被中断的下一条指令)


③ 是否有错误代码压栈


说明2:中断向量32 ~ 255供外部中断和软中断使用,因此需要设置中断控制器,使其生成的中断向量在此范围中

2.2 中断描述符表

2.2.1 实模式下的中断向量表X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第4张图片


我们首先来回顾一下实模式下的中断向量表IVT(Interrupt Vector Table),


① 共256个表项,每个表项4B,共1KB内存


② 每个表项4B,包括中断处理程序的16位段地址和16位段内偏移量


③ 存储在内存零地址处

2.2.2 中断描述符表的构成

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第5张图片

在保护模式下,处理器对中断的管理是类似的,只是使用的是中断描述符表IDT(Interrupt Descriptor Table)


① 也是256个表项,但是每个描述符8B,因此需要2KB内存


② 每个表项是一个门描述符,可以是中断门、陷阱门或任务门


③ 可以存储在内存中的任何位置,因为有IDTR寄存器保存其线性地址与表界限(与GDT类似)


说明1:中断门和陷阱门描述符只允许安装在IDT内,任务门描述符可以安装在GDT、LDT和IDT中


说明2:中断描述符表寄存器IDTR

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第6张图片

① IDTR中存储的是IDT的起始线性地址和表界限,16位表界限最大为64KB,但实际最大只使用2KB


② 系统复位时,IDTR的基地址部分为0,表界限部分为0xFFFF


③ 与GDT类似,系统中只有一个IDT


④ 为了提高高速缓存使处理器的工作性能最大化,建议IDTR的基地址8B对齐


说明3:在IDT中安装任务门,可以执行硬件任务切换。但是在64位X86处理器中,不再支持硬件任务切换和任务门


说明4:IDT中部署任务门的注意事项


① 因中断和异常而发起任务切换时,不再保存CS & EIP。但是如果该中断或异常有错误代码的话,在任务切换完成后,处理器会将错误代码压入新任务的栈中


② 任务是不可重入的,因此在进入中断任务之后和执行iretd指令之前,必须关中断,以防止因相同中断再次发生而产生常规保护异常#GP


③ 和对中断门、陷阱门的保护一样,只对通过int3、int n和into指令发起的任务切换进行门级检查,只有满足如下条件才允许发起任务切换

CPL <= 任务门描述符的DPL

2.2.3 中断门与陷阱门


中断门和陷阱门格式类似,只有1比特的差异

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第7张图片

 X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第8张图片

说明:中断门和陷阱门的区别不大,区别在于通过门进入中断处理程序时对EFLAGS寄存器的处理


通过中断门进入中断处理程序时,EFLAGS寄存器的IF位被处理器自动清零,以禁止嵌套的中断。当中断返回时,将从栈中恢复EFLAGS寄存器的原始状态


陷阱中断的优先级较低,当通过陷阱门进入中断处理程序时,EFLAGS寄存器的IF位保持不变,以允许其他中断优先处理


需要注意的是,EFLAGS寄存器的IF位仅影响硬件中断,对NMI、异常和int n形式的软中断不起作用

2.3 保护模式下中断控制转移过程

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第9张图片


① 将中断向量 * 8,结合IDTR中的基地址访问IDT,找到对应的中断门或陷阱门描述符


② 从中断门或陷阱门描述符中取出中断处理程序的代码段选择子和偏移量,也就是中断处理程序的入口点(CS + EIP)


③ 根据代码段选择子,从GDT或LDT中取出代码段描述符,并从中取出目标代码段的基地址


④ 通过目标代码段的基地址 + 中断门或陷阱门描述符中的偏移量,得到中断处理程序的入口地址,就可以转移到中断处理函数运行


说明:如果使用中断向量访问IDT时超出了IDT的范围,则产生通用保护异常#GP

2.4 中断和异常发生时的特权级检查


当中断和异常发生时,在进行中断处理的过程中,要实施特权级检查与保护。但是中断和异常的处理机制中的特权级检查比较特殊,体现在如下3个方面

2.4.1 不检查RPL


当中断发生时,处理器从软中断指令、中断控制器或处理器内部取得中断向量,然后使用该向量从IDT中取出中断门、陷阱门或任务门


但是中断向量只是一个代码中断编号的数字,没有表指示器TI和请求特权级RPL部分,所以当中断和异常发生时,不检查RPL字段


说明:此处附上选择子的构成,就一目了然了,中断向量相当于只有描述符索引号部分

2.4.2 门级检查


① 除了int n、int3和into指令触发中断,其余情况下不检查门的DPL


② 当使用int n、int3和into指令触发中断时,要求当前特权级CPL必须高于或等于门的描述符特权级DPL,即在数值上有

CPL <= 门描述符的DPL


说明:上述检查主要是为了防止低特权级的软件通过软中断指令访问一些只为内核服务的例程(e.g. 通过int 14触发缺页异常)

2.4.3 段级检查


目标代码段的描述符特权级DPL必须高于或等于当前特权级CPL,即在数值上有

CPL >= 目标代码段的DPL


也就是说,通过中断也只能将控制从低特权级代码段转移到高特权级代码段


说明:违反该约束条件,不允许将控制转移到中断或异常处理程序,并引发常规保护异常#GP

2.5 中断和异常发生时的栈切换过程


因中断导致的控制转移也需要用栈保护现场,至于使用哪个栈,则取决于目标代码段的特权级


② 如果处理中断的过程中发生特权级切换,则也需要切换栈,处理器会自动将栈切换到与目标代码段相同的特权级


说明1:切换栈的来源


虽然中断和异常是随机产生的,但是总是发生在某个任务内,是在某个任务正在执行时产生的,即使整个系统内只有一个任务


因此切换栈的来源,就是被中断打断的当前任务的TSS段,这和通过调用门进行特权级转移是一样的


说明2:中断压栈状态


① 无特权级切换


X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第10张图片


② 有特权级切换

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第11张图片


说明3:异常错误代码


有些异常发生时,处理器会在栈中压入一个错误代码,通常这意味着异常和特定的段选择子或中断向量有关


压入栈中的错误代码是32位的,构成如下,


① EXT(External Event):异常是否由外部事件引发


1:异常由NMI、硬件中断等引发


0:异常为处理器内部引发


② IDT:指示描述符的位置


1:段选择子的索引部分(错误代码的bit [15:3])指向中断描述符表


0:段选择子的索引部分指向GDT或LDT


③ TI:表指示器,在IDT位为0时才有意义


1:段选择子的索引部分指向LDT


0:段选择子的索引部分指向GDT


④ 段选择子索引:用于指示描述符


说明4:异常错误代码需要中断处理函数弹出


通过iret / iretd指令从中断处理过程返回时,处理器不会自动从栈中弹出错误代码(也没法弹,因为有的异常有,有的异常没有)。因此,如果栈中有错误代码,必须在执行iret / iretd指令之前,先从栈中将其弹出


说明5:对于外部中断(比如通过处理器引脚触发的异常),以及用软中断指令int n触发的异常,处理器不会压入错误代码,即使他原本是一个有错误代码的异常


分配给外部中断的向量号是32 ~ 255,但是出于特殊目的,中断控制器或软中断指令可能会给出一个0 ~ 19的向量号。在这种情况下,处理器并不会压入错误代码

2.6 Linux 0.11中断门与陷阱门的部署


我们以一款实际操作系统来分析中断门与陷阱门的部署方式

2.6.1 生成门描述符

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第12张图片


代码中共有3个生成IDT中门描述符的宏,根据门的类型与门描述符的DPL分类如下,


① 中断门:中断门类型 + DPL = 0


② 陷阱门:陷阱门类型 + DPL = 0


③ 系统门:陷阱门类型 + DPL = 3


可见系统门就是门描述符特权级DPL为3的陷阱门,这是实现系统调用的关键

2.6.2 中断门的部署


代码中将如下中断定义为中断门

set_intr_gate(0x2E,&hd_interrupt); // 硬盘中断
set_intr_gate(0x24,rs1_interrupt); // 串行口1中断
set_intr_gate(0x23,rs2_interrupt); // 串行口2中断
set_intr_gate(0x20,&timer_interrupt); // 定时器中断

说明:定时器中断是系统调度的基础

2.6.3 陷阱门的部署


代码中将如下中断定义为陷阱门

set_trap_gate(0x21,&keyboard_interrupt); // 键盘中断
set_trap_gate(0x26,&floppy_interrupt); // 软盘中断
set_trap_gate(45,&irq13);
set_trap_gate(39,¶llel_interrupt);
// 下面是一组系统异常
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
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_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);

2.6.4 系统门的部署


代码中将如下中断定义为系统门

set_system_gate(0x80,&system_call); // 系统调用
set_system_gate(3,&int3); // 单步调试中断
set_system_gate(4,&overflow); // into指令中断
set_system_gate(5,&bounds); // bounds指令中断


说明:由于系统门是门描述符特权级DPL=3的陷阱门,所以有如下特性,


① 可以在用户态通过指令触发


e.g. int 0x80 / int3 / into / bounds


② 中断处理过程中不会关中断

3. 保护模式中断设置


下面根据本章示例代码,分析使用保护模式下的中断需要进行哪些设置

3.1 关闭中断

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第13张图片


在MBR进入保护模式之前,已经调用cli指令关闭了中断,因为此时保护模式下的中断处理机制尚未准备完毕


说明:在保护模式中断处理机制未准备好的情况下,都不能打开中断。这里要特别注意不要调用那些含有sti指令的过程,在示例代码中就是要注意不能调用put_string函数


这是因为在目前的实现中,put_string函数的打印过程通过开关中断进行了保护

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第14张图片


之所以要在put_string函数中关中断,是因为在抢占式多任务的环境中,多个任务均会调用put_string函数进行打印。如果在打印过程中发生RTC中断,系统将进行任务切换,那么不同任务的打印内容交错,就会出现混乱,而且对光标位置的维护也会错乱


当然,出现这个问题的原因是目前没有做开关中断的嵌套保护

3.2 创建IDT

 X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第15张图片


创建IDT的关键就是填充其中的表项,示例代码填充如下,


① 0 ~ 19号中断


填充中断门,门描述符DPL = 0,异常处理函数为general_interrupt_handler


② 20 ~ 255号中断


填充中断门,门描述符DPL = 0,中断处理函数为general_interrupt_handler


说明1:general_exception_handler函数


通用异常处理函数只是打印信息,并调用hlt指令休眠。在目前的示例代码中,所有异常使用同一函数处理。在实际操作系统中,需要根据异常逐个处理


说明2:general_interrupt_handler函数

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第16张图片


通用中断处理函数只是向8259A发送EOI命令,之后调用iretd返回。其实20 ~ 255号中断并非都是由外围设备触发,此处只是一个处理模板

3.3 安装RTC中断门


之前是为所有中断部署了通用的中断处理函数,下面为RTC中断部署中断门

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第17张图片


① RTC中断使用rtm_0x70_interrupt_handle函数处理,我们就是在该函数中实现任务切换,进而达到抢占式多任务的目的,详见下文分析


② 使用新生成的中断门描述符替换换来IDT中的中断门描述符,其中0x70为RTC中断的中断向量

3.4 加载中断描述符表寄存器IDTR


在设置好IDT之后,需要将IDTR的起始线性地址和表界限加载到IDTR寄存器,IDT才能生效


加载IDTR使用lidt指令,格式如下,

lidt m48

在m48指向的内存中,需要按如下布局存储IDT表界限和线性地址

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第18张图片


示例代码中实现如下,


说明1:lidt指令不影响任何标志位


说明2:和lgdt指令一样,lidt指令也可以在实模式下执行,以便于在进入保护模式之前就做好与中断相关的准备

3.5 重新设置8259A主片中断向量

3.5.1 重新设置的原因


根据之前介绍实模式中断的笔记,BIOS为8259A芯片设置的默认中断向量如下图所示,

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第19张图片


其中主片的中断向量为0x08 ~ 0x0F,这和Intel保护模式下的中断向量分配是冲突的,因此需要重新设置

3.5.2 8259A初始化命令字


① 8259A芯片是可编程的,并通过IO端口进行。其中,主片端口号为0x20和0x21,从片端口号为0xA0和0xA1


② 对8259A芯片编程需要使用初始化命令字(Initialize Command Word,ICW),以设置他的工作方式


③ 共有4个初始化命令字,分别是ICW1 ~ ICW4


说明1:ICW1


ICW1用于设置中断请求的触发方式,以及级联的芯片数量

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第20张图片


① 从0x20 / 0xA0端口接收到ICW1后,意味着一个新的初始化过程开始了。之后8259A芯片期待从0x21 / 0xA1端口接收ICW2,至于是否期待ICW3和ICW4,就看ICW1中的设置


② 如果ICW1中设置有级联,则有ICW3;如果ICW1中设置需要ICW4,则有ICW4


说明2:ICW2


设置每个芯片的中断向量


① 对中断向量的设置是以芯片为单位的,此处设置的是芯片的起始中断向量


② ICW2的低3位为0,也就是说,设置的起始中断向量要能被8整除


说明3:ICW3


设置用哪个引脚实现芯片级联

说明4:ICW4


设置芯片的工作模式


对于8259A单片使用的场合,采用自动结束方式较为方便;但多片级联的场合,应当采用非自动结束方式

3.5.3 8259A初始化示例代码

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第21张图片


经过设置后,8259A主片的中断向量为0x20 ~ 0x27,从片中断向量为0x70 ~ 0x77

3.6 设置RTC硬件

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第22张图片


经过设置后,RTC更新完成中断即开始工作,此时可以调用sti指令打开中断

4. 抢占式多任务切换的实现

4.1 示例代码任务构成


示例代码中共有3个任务,其中1个内核任务,2个用户任务


① 内核任务


内核任务在启动用户任务之后,进入死循环并打印信息

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第23张图片

② 用户任务1


用户任务1在启动后,进入死循环并打印信息

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第24张图片

③ 用户任务2


用户任务2在启动后,也是进入死循环并打印信息

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第25张图片

说明1:内核任务和2个用户任务在创建后都被加入TCB链表,以便在任务切换时调度。需要注意的是,在append_to_tcb_link函数中,在对TCB链表进行操作时需要关中断

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第26张图片


这是因为在RTC中断中也会操作TCB链表以调度任务,因此需要互斥


说明2:TCB中的任务状态

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第27张图片


在我们使用的TCB中,偏移量为4处存储的是任务状态,我们进行如下约定,


① 任务状态 = 0xFFFF,系统中的忙任务,也就是当前正在执行的任务


② 任务状态 = 0x0000,系统中的空闲任务


在创建内核任务时,将任务状态设置为0xFFFF;在创建用户任务时,将任务状态设置为0x0000。后续任务状态,由RTC中断处理函数根据任务调度状态修改

4.2 在中断处理过程中实施任务切换

4.2.1 RTC中断处理函数

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第28张图片


RTC中断处理函数完成2个任务,


① 在中断控制器 & 中断源端清中断


② 进行任务切换

4.2.2 任务切换过程分析


我们首先来分析一下在RTC中断中进行任务切换的流程,


① 遍历TCB链表,查找状态值为0xFFFF的节点,也就是当前忙任务。如果找不到(在示例代码中,就是链表为空),则iret中断返回


② 如果找到忙任务,将此节点移动到表尾。因为该任务刚刚执行完,将其移动至表尾可以使其被调度的优先级最低


③ 再次遍历TCB链表,寻址第一个状态为空闲的任务,如果没有(在示例代码中,就是系统中只有一个任务),则iret中断返回


④ 如果同时找到忙和空闲的任务,修改任务状态,将忙置为空闲,将空闲置为忙


⑤ 使用jmp far指令从当前任务切换到空闲任务


⑥ 当任务再次切换回来时,则iret中断返回


下面我们来看一下代码实现,

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第29张图片

说明:查找到忙任务后,并不立即修改他的任务状态,而是要同时找到空闲任务时一起修改。这是因为系统中可能只有1个任务,此时不会发生任务切换

4.2.3 在中断中切换任务状态分析


在RTC中断处理函数中进行任务切换,处理器中断状态是怎样的 ?


当处理器通过中断门进入RTC中断处理函数时,会将EFLAGS中的IF位清零,即中断是关闭的。也就是说是在关中断的情况下切换到另一个任务,这会影响后续的中断嘛 ? 因为我们在Linux中不是强调在中断上下文中不能调用可能睡眠(也就是会导致调度)的函数嘛 ?


在我们这里,答案肯定是不会影响的 !!! 我们来分析一下原因


对于中断的发生与处理,包括中断源、中断控制器、处理器3个环节,我们对照代码逐一说明

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第30张图片


① 中断源端


在切换任务前,已经在中断源端清中断,也就是RTC的更新完成中断会继续产生


② 中断控制器端


在切换任务前,也已经在中断控制器端清中断,也就是中断处理器会继续处理后续产生的RTC更新完成中断


③ 处理器端


虽然当前EFLAGS中的IF位为0,但是任务切换后,会使用目标任务TSS中的EFLAGS值恢复EFLAGS寄存器,而目标任务TSS中的EFLAGS值中,一定是开中断的


为什么说目标TSS中的EFLAGS值中一定是开中断的呢 ?


a. 在创建任务时,设置到TSS中的EFLAGS是开中断的,所以首次切换到该任务时,EFLAGS一定是开中断的


b. 如果任务是在运行过程中被切换走,则当时EFLAGS寄存器一定是开中断的,否则不会在RTC中断中被切换走

4.2.4 Linux中断上下文调度分析


那么问题就来了,在Linux中,为什么强调在中断上下文中不能触发调度呢 ?


这个问题话分两头,不同时代的Linux处理是不同的,Linux 2.4版本是不可以的


,但是Linux 0.11版本就是在timer中断中进行定期调度(与示例代码的实现类似)


4.2.4.1 Linux 2.4 + i386

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第31张图片

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第32张图片


我们关注其中的核心步骤,


① 调用中断处理器的ack函数响应中断


② 调用request_irq注册的中断处理函数,也就是我们常说的中断顶半部


③ 调用中断处理器的end函数结束中断


那么核心就是ack & end函数的实现, 我们以8259A芯片的实现为例进行说明


说明1:mask_and_ack_8259A函数


在arch/i386/kernel/i8259.c中,handler->ack设置的回调函数是mask_and_ack_8259A

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第33张图片


该函数会向中断控制器发送EOI信号,同时会在中断控制器端屏蔽当前正在处理的中断


说明2:end_8259A_irq函数


在arch/i386/kernel/i8259.c中,handler->end设置的回调函数是end_8259A_irq

 X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第34张图片


该函数会在中断控制器端重新使能之前处理的中断


至此,情况就比较明确了,Linux 2.4在即将执行中断顶半部时,中断情况如下,


① 中断源端尚未清中断(一般在中断顶半部中清除中断源端中断)


② 中断控制器端该中断被清中断,同时屏蔽(由mask_and_ack_8259A函数设置)


③ 处理器端EFLAGS寄存器关中断(通过中断门时处理器设置)


如果在这种情况下,在中断顶半部中直接或间接调用schedule函数进行任务切换,虽然可以恢复EFLAGS寄存器,在处理器端开中断。但是在中断控制器端,该中断仍然是被屏蔽的,后续相同的中断将无法触发


因此,在schedule函数中,就对在中断上下文中发起调度进行了判断

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第35张图片


4.2.4.2 Linux 0.11 + i386


首先,在Linux 0.11中,系统就是在定时器中断处理函数中进行任务定期调度的

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第36张图片


之所以在Linux 0.11中可以这么做,是因为在执行该函数前,已经完成了对中断处理器的操作

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第37张图片


因此,虽然在执行do_timer函数时,处理器端仍然是关中断的(EFLAGS寄存器中IF位被清除),但是通过调用schedule切换任务,当使用目标任务TSS中的EFLAGS值恢复EFLAGS寄存器后,处理器端中断将打开


说明:定时器中断门设置


定时器中断门设置如下,

set_intr_gate(0x20,&timer_interrupt);

在系统中,0x20是8259A主片IRQ0的中断向量号,该引脚连接的正式系统时钟

X86汇编语言从实模式到保护模式18:中断和异常的处理与抢占式多任务_第38张图片

​​​​​​​

你可能感兴趣的:(计算机体系结构,计算机体系结构)