当前位置: 首页 > Linux

ucore操作系统实验笔记-重新认识中断

时间:2023-04-06 06:35:16 Linux

在之前的文章ucore操作系统实验笔记-Lab1中,我比较详细的记录了中断的使用。中断那篇文章的重点是如何使用IDT、中断描述符和中断向量表等,这篇文章我会重点讲另外一个地方,即中断过程中的场景如何保存和恢复。CPU收到中断信号后会做什么?CPU执行完当前程序的每条指令后,会检查中断控制器(如:8259A)是否在刚才的指令执行过程中发出了中断请求。如果是,CPU就会在相应的时钟脉冲到来时,从总线上读取中断请求对应的中断向量;CPU根据得到的中断向量(这里是索引)在IDT中找到该向量对应的中断描述符,中断描述符中存放着中断服务程序的段选择符;CPU使用IDT找到的中断服务程序的段选择符从GDT中获取对应的段描述符,GDT中存放着中断服务程序的段基地址和属性信息,此时CPU会得到中断服务程序的起始地址中断服务程序并跳转到该地址;CPU会根据中断服务程序段描述符的CPL和DPL信息来确认是否发生了特权级转换。比如当前程序运行在用户态,而中断程序运行在内核态,这意味着发生了特权级转换。TR寄存器)获取程序的内核栈地址,包括内核态ss和esp的值,立即将系统当前使用的栈切换到新的内核栈。该堆栈是要运行的中断服务程序要使用的堆栈。紧接着,将当前程序使用的用户态ss和esp压入新的内核栈并保存;CPU需要开始保存当前被中断程序的场景(即一些寄存器的值),以便以后恢复被中断的程序。中断的程序继续执行。这就需要利用内核栈来保存相关的场景信息,即依次压入被中断程序使用的eflags、cs、eip、errorCode(如果有异常则有错误码)信息;CPU使用中断服务程序的段描述符将其第一条指令的地址加载到cs和eip寄存器中,开始执行中断服务程序。这意味着之前的程序被挂起,中断服务程序开始工作。以上内容直接摘自ucore实验指南。在上一篇文章中,我主要关注前3步和最后一步。在本文中,我将重点介绍步骤4和5。对于特权级转换的检测,个人认为第4、5步应该发生在CPU跳转到ISR(中断服务程序)之前,所以把第3步放在第5步之后比较合适,后面会解释为什么我想是这样。当CPU获取到IDT中的中断描述符后,就会检测到特权级的转换。具体检测如下图所示:当CPU获取到中断描述符后,CPU会根据中断描述符的DPL和当前段选择子的CPL进行比较,判断是否需要进行特权级转换。同时,它也会做一些检测工作。例如,对于硬中断,CPL必须大于或等于DPL,因为特权级别转换为更高的特权级别或相等级别。对于软中断,转换后的特权级不能超过转换前的特权级,这是为了防止用户代码随意触发中断。对于CPL和DPL不同的情况,我们需要使用TSS来切换内核栈。关于TSS的内容我会在后面单独开一篇文章。内核栈的变化第4步和第5步的一个重要作用是将各种寄存器压入内核栈。压入这些寄存器不仅可以挽救现场,还可以让ISR知道中断的各种信息,所以这两步很重要。我们来看看CPU必须将哪些寄存器压入内核栈:这是中断发生和特权级转换发生后栈空间变化的示意图。对于不经历特权级别转换的中断,有两个区别。首先,它只使用了一个栈,也就是说Procedure和Handler使用了同一个栈;其次,CPU不需要推送SS和ESP。除此之外,这两种情况都需要推送CS、EIP和错误代码(如果有)。之所以说step3应该在step5之后,原因就在这里。如果先跳转到ISR,那么按下的EIP就是ISR中的EIP,而不是中断前的EIP,所以我们应该在第三步。在第1步之前完成第4步和第5步。Trapframe和ISR除了CPU要压入的各种寄存器外,我们还需要压入一些其他的寄存器来保存场景和提供ISR中断信息。在ucore中,我们使用结构体trapframe将保存的寄存器传递给ISR。让我们先看一下trapframe:uint32_treg_esi;uint32_treg_ebp;uint32_treg_oesp;/*没用*/uint32_treg_ebx;;structtrapframe{structpushregstf_regs;uint16_ttf_gs;uint16_ttf_padding0;uint16_ttf_fs;uint16_ttf_padding1;uint16_ttf_es;uint16_ttf_padding2;uint16_ttf_ds;uint16_ttf_padding3;uint32_ttf_trapno;/*下面由x86硬件定义*/uint32_ttf_err;uintptr_ttf_eipuint16_ttf_cs;uint16_ttf_padding4;uint32_ttf_eflags;/*下面仅当跨环时,例如从用户到内核*/uintptr_ttf_esp;uint16_ttf_ss;pushal中就是所有需要压栈的寄存器。有了这个数据结构,我们就可以拿到中断后的中断信息,传递给ISR,ISR会根据传入的trapframe进行相应的操作。下面看看trapframe如何赋值以及trapframe如何传递给ISR:.globlvector2vector2:pushl$0pushl$2jmp__alltraps上面的代码是中断向量2,CPU会执行这里的指令在第6步,先压0和2,0是错误码(对于没有错误码的中断,ISR会压0为错误码;如果中断有错误码,这里不会压0),2是中断向量号。请注意,在此之前,CPU已推送EFLAGS、CS、EIP和错误代码(如果有)。压入错误码和中断向量号后,CPU跳转到__alltraps,__alltraps会将中断中需要保存的所有寄存器存入内核栈,然后传递栈顶地址($esp)作为trap()的参数,trap()会将此时压入栈的各个寄存器作为一个trapframe作为一个整体进行处理。trap()会根据trapframe中的内容对中断进行相应的处理。...内核堆栈。#loadGD_KDATAinto%dsand%estosetdatasegmentsforkernelmovl$GD_KDATA,%eaxmovw%ax,%dsmovw%ax,%es这段代码将此时的数据段和附加段设置为内核的Data段(ISR位于内核中)。#push%esp将指向trapframe的指针作为参数传递给trap()pushl%esp#calltr??ap(tf),其中tf=%espcalltr??ap这段代码首先将%esp的值压入内核堆栈,%esp的值将作为函数trap()的参数,然后我们调用trap。通过将各个寄存器的信息压入栈中,并将栈顶的地址作为trapframe的地址,我们完成了trapframe的赋值。trap()函数接收到trapframe后,可以根据中断类型进行相应的处理。我们看一下此时栈中的情况:因为栈是从高地址向低地址增长的,所以栈中蓝色部分的EFLAGS地址最高,EDI地址最低。这也和trapframe中的元素一致,tf_eflags地址最高(如何忽略tf_esp,tf_ss),reg_edi地址最低。因此,我们可以利用OldESP的地址,将栈中的蓝色部分当作trapframe。#弹出压栈指针popl%esp#返回落入trapret....globl__trapret__trapret:#从栈中恢复寄存器popal#恢复%ds,%es,%fsand%gspopl%gspopl%fspopl%espopl%ds#去掉陷阱号和错误码addl$0x8,%espiret当trap()运行结束时,我们需要将寄存器恢复到中断前的状态。这里,我们只需要将内核栈的内容单独弹出,保存到相应的寄存器中即可。最后通过调用iret命令恢复EIP、CS和EFLAGS。如果还有特权转换,我们还需要弹出之前保存的SS和ESP。至此,整个中断流程结束。