资讯专栏INFORMATION COLUMN

ucore操作系统实验笔记 - 重新理解中断

张利勇 / 2573人阅读

摘要:在上一篇文章操作系统实验笔记中,我已经比较详细地记录了中断的使用。这个栈就是即将运行的中断服务程序要使用的栈。这意味着先前的程序被暂停执行,中断服务程序正式开始工作。会会根据中的内容,对中断进行相应的处理。

在上一篇文章ucore操作系统实验笔记 - Lab1中,我已经比较详细地记录了中断的使用。那篇文章关于中断的重点是如何使用IDT、中断描述符和中断向量表等。这篇文章我将把重点放到另外一个地方,也就是中断的过程中如何保存和恢复现场。

CPU接收到中断信号后会做什么

CPU在执行完当前程序的每一条指令后,都会去确认在执行刚才的指令过程中中断控制器(如:8259A)是否发送中断请求过来,如果有那么CPU就会在相应的时钟脉冲到来时从总线上读取中断请求对应的中断向量;

CPU根据得到的中断向量(以此为索引)到IDT中找到该向量对应的中断描述符,中断描述符里保存着中断服务例程的段选择子;

CPU使用IDT查到的中断服务例程的段选择子从GDT中取得相应的段描述符,段描述符里保存了中断服务例程的段基址和属性信息,此时CPU就得到了中断服务例程的起始地址,并跳转到该地址;

CPU会根据CPL和中断服务例程的段描述符的DPL信息确认是否发生了特权级的转换。比如当前程序正运行在用户态,而中断程序是运行在内核态的,则意味着发生了特权级的转换,这时CPU会从当前程序的TSS信息(该信息在内存中的起始地址存在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和Error Code(如果有的话)。之所以我说第3步应该在第5步后,原因就在这里,如果先跳到了ISR,那么压入的EIP就是ISR中的EIP了,并不是中断前的EIP,因此我们应该在第3步前完成步骤4和5。

Trapframe和ISR

除了CPU要压入的各种寄存器,我们还需要压入其他一些寄存器用于保存现场和提供给ISR中断信息。在ucore中,我们使用结构体trapframe来将保存的寄存器传给ISR。下面就先来看看trapframe:

/* registers as pushed by pushal */
struct pushregs {
    uint32_t reg_edi;
    uint32_t reg_esi;
    uint32_t reg_ebp;
    uint32_t reg_oesp;          /* Useless */
    uint32_t reg_ebx;
    uint32_t reg_edx;
    uint32_t reg_ecx;
    uint32_t reg_eax;
};

struct trapframe {
    struct pushregs tf_regs;
    uint16_t tf_gs;
    uint16_t tf_padding0;
    uint16_t tf_fs;
    uint16_t tf_padding1;
    uint16_t tf_es;
    uint16_t tf_padding2;
    uint16_t tf_ds;
    uint16_t tf_padding3;
    uint32_t tf_trapno;
    /* below here defined by x86 hardware */
    uint32_t tf_err;
    uintptr_t tf_eip;
    uint16_t tf_cs;
    uint16_t tf_padding4;
    uint32_t tf_eflags;
    /* below here only when crossing rings, such as from user to kernel */
    uintptr_t tf_esp;
    uint16_t tf_ss;
    uint16_t tf_padding5;
} __attribute__((packed));

其中pushregs中的寄存器都是pushal中需要压入栈的所有寄存器。有了这个数据结构后,我们就可以在中断后获取中断的信息,并将它传给ISR,ISR会根据传入的trapframe来进行相应的操作。
下面我们来看看如何给trapframe赋值,如何将trapframe传给ISR:

.globl vector2
vector2:
  pushl $0
  pushl $2
  jmp __alltraps

上面这段代码是中断向量2,在第6步时CPU会执行这里的指令。它首先压入0和2,0是error code(对于没有error code的中断,ISR会压入0作为error code;如果中断有error code,这里就不会压入0),2是中断向量号。注意,在这之前,CPU已经压入了EFLAGS,CS,EIP和Error Code(如果有的话)。在压入error code和中断向量号后,CPU跳到__alltraps,__alltraps会将所有中断需要保存的寄存器存到内核栈,然后将此时栈顶的地址($esp)作为参数传给trap(),trap()会将此时栈中压入的各种寄存器整体当成trapframe来处理。trap()会会根据trapframe中的内容,对中断进行相应的处理。

.text
.globl __alltraps
__alltraps:
    # push registers to build a trap frame
    # therefore make the stack look like a struct trapframe
    pushl %ds
    pushl %es
    pushl %fs
    pushl %gs
    pushal

这段代码将所有中断需要保存的寄存器压入内核栈。

 # load GD_KDATA into %ds and %es to set up data segments for kernel
    movl $GD_KDATA, %eax
    movw %ax, %ds
    movw %ax, %es

这段代码将此时的数据段和附加段设置为内核的数据段(ISR是位于kernel的)。

 # push %esp to pass a pointer to the trapframe as an argument to trap()
    pushl %esp

    # call trap(tf), where tf=%esp
    call trap

这段代码先将%esp的值压入内核栈,%esp的值将作为函数trap()的参数,然后我们再call trap。通过向栈中压入各种寄存器的信息并且将栈顶的地址作为trapframe的地址,我们完成了对trapframe的赋值。trap()函数接收到trapframe后就可以根据中断类型做出相应处理了。我们来看看此时栈中的情况:

因为栈是从高地址向低地址生长的,因此,栈中蓝色部分EFLAGS地址最高,EDI地址最低。这个和trapframe中的元素也是吻合的,tf_eflags地址最高(如何不考虑tf_esp, tf_ss),而reg_edi地址最低。因此我们可以通过Old ESP这个地址,把栈中蓝色部分当成trapframe来处理。

# pop the pushed stack pointer
    popl %esp

    # return falls through to trapret...
.globl __trapret
__trapret:
    # restore registers from stack
    popal

    # restore %ds, %es, %fs and %gs
    popl %gs
    popl %fs
    popl %es
    popl %ds

    # get rid of the trap number and error code
    addl $0x8, %esp
    iret

当trap()运行结束后,我们需要将寄存器恢复到中断前的状态。在这里,我们只需要将内核栈中的内容分别弹出,并保存到相应的寄存器即可。最后,通过调用iret指令来恢复EIP,CS和EFLAGS。如果还存在特权级的转化,我们还需要弹出之前保存的SS和ESP。到此为止,整个中断的过程就结束了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/10011.html

相关文章

  • ucore操作系统实验笔记 - Lab2

    摘要:操作系统课程笔记光就实验而言并不难,但实验外的东西还是很值得研究的。系统内存的探测中断与参数在我们分配物理内存空间前,我们必须要获取物理内存空间的信息比如哪些地址空间可以使用,哪些地址空间不能使用等。 操作系统课程笔记 - Lab2 Lab2光就实验而言并不难, 但实验外的东西还是很值得研究的。指导书上也说了,Lab1和Lab2对于初次接触这门课的同学来说是一道坎,只要搞懂了这两Lab...

    CODING 评论0 收藏0
  • ucore操作系统实验笔记 - Lab1

    摘要:最近一直都在跟清华大学的操作系统课程,这个课程最大的特点是有一系列可以实战的操作系统实验。这个主要是为了熟悉以及如何生成操作系统的镜像文件。总之,和之间没有优先级之分,仅仅是在处理中断时有不同的方法,供操作系统在实现时根据需要进行选择。 最近一直都在跟清华大学的操作系统课程,这个课程最大的特点是有一系列可以实战的操作系统实验。这些实验总共有8个,我在这里记录实验中的一些心得和总结。 T...

    阿罗 评论0 收藏0
  • Java 并发学习笔记

    摘要:方法可以将当前线程放入等待集合中,并释放当前线程持有的锁。此后,该线程不会接收到的调度,并进入休眠状态。该线程会唤醒,并尝试恢复之前的状态。 并发 最近重新复习了一边并发的知识,发现自己之前对于并发的了解只是皮毛。这里总结以下Java并发需要掌握的点。 使用并发的一个重要原因是提高执行效率。由于I/O等情况阻塞,单个任务并不能充分利用CPU时间。所以在单处理器的机器上也应该使用并发。为...

    DrizzleX 评论0 收藏0
  • Java多线程笔记(一):JMM与基础关键字

    摘要:当线程执行完后进入状态,表示线程执行结束。其中和表示两个线程。但要注意,让出并不表示当前线程不执行了。关键字其作用是防止指令重排和使线程对一个对象的修改令其他线程可见。 JMM特性一览 Java Memory Model的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。因此我们首先需要来了解这些概念。 原子性(Atomicity) 原子性是指一个操作是不可中断的。即使是在多...

    cyixlq 评论0 收藏0
  • 从Java视角理解系统结构 (一) CPU上下文切换

    摘要:本文是从视角理解系统结构连载文章在高性能编程时经常接触到多线程起初我们的理解是多个线程并行地执行总比单个线程要快就像多个人一起干活总比一个人干要快然而实际情况是多线程之间需要竞争设备或者竞争锁资源,导致往往执行速度还不如单个线程在这里有一个 本文是从Java视角理解系统结构连载文章 在高性能编程时,经常接触到多线程. 起初我们的理解是, 多个线程并行地执行总比单个线程要快, 就像多个...

    yuxue 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<