Chinaunix首页 | 论坛 | 博客
  • 博客访问: 66154
  • 博文数量: 21
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 170
  • 用 户 组: 普通用户
  • 注册时间: 2015-10-15 14:23
个人简介

-计算机-每天学习一点专业知识

文章分类

全部博文(21)

文章存档

2019年(1)

2018年(15)

2015年(5)

我的朋友

分类: 其他UNIX

2018-08-30 15:02:26

原文http://blog.sina.com.cn/s/blog_4dff87120100x1lj.html#cmt_5381CC5D-7F000001-B73453F6-8DA-8A0
    这篇文章算是还一个月之前的债吧……我是标题党哈哈哈。

大树当时提出问题:MINIX3基于什么?我认为基于进程调度。我的意见是:MINIX3基于中断,中断提供操作系统调度进程的能力。(照这么说,基本上任何现代操作系统都基于中断)

中断的本质在于把处理正常程序流意料之外情况的代码,与正常程序流代码分离开。这种思想在计算机各个层次都有:CPU的中断处理、进程间的信号机制、编程语言的异常控制……它们做的事情都是一样的。但对操作系统和CPU而言,中断还有另外一层意义:提供一种灵活的、CPU响应外设以及优先请求的方法。于CPU,中断提供响应外设的机制;于操作系统,中断提供调度进程的能力。

 

MINIX3自称机制与策略分离,今天重点讲策略。MINIX3运行在保护模式,由于涉及到特权级跳转以及中断重入,MINIX3的中断过程稍微有点复杂。但由于MINIX3阻止了大部分中断重入的发生,因此今天不讨论。(另外MINIX3的中断策略适用于任何运行在x86平台的系统,如果你也想写一个的话)

一个中断(Interrupt)过程无非三件事:1保护现场;2、处理中断;3、恢复程序。在一个中断过程中,SSESP会在进程栈、进程表和内核栈之间来回跳转,我就以一个时钟中断为线、以堆栈的切换和程序计数器的跳转为索来详解MINIX3中断过程。

 

前世(中断发生):

假设系统正在运行进程PMINIX3的进程中栈段跟数据段是同一段,且栈是从高地址向低地址生长)。

MINIX3中断过程超级详详解

 

这时,啪!,时钟发出了一个中断……

CPU首先关闭可屏蔽中断,并关闭了8259A对中断信号响应,然后根据TR指向的TSS,随后把SS:ESP的内容设定成TSSSS0ESP0的值(进程表在内核数据区,特权级Ring0)。

TSS结构:

MINIX3中断过程超级详详解

 

TSSSS0ESP0指向了哪儿?指向了P进程表项的栈帧顶部。SSESP就指向了进程表中P的表项的栈帧,这个表项正是被中断进程的表项,SS完成了特权级的转换。

MINIX3中断过程超级详详解

栈帧的结构是什么样的?

MINIX3栈帧定义(src/kernel/type.h

MINIX3中断过程超级详详解

 

(为了美观,也因为我还不能处理好代码在文章中的显示,下面的代码全部贴图……)

 从栈帧的定义看出这显然就是用于保存寄存器值,再次强调栈的生长方向是从高地址到低地址。随后CPU把程序运行最重要的5个寄存器值PUSH到栈帧中。

MINIX3中断过程超级详详解

 

发现什么问题了吗?

CPU先把SS:ESP指向了进程P的栈帧,但随后又把P被中断前的栈指针压栈。所以我猜CPU里头肯定有某个看不见的寄存器在SS:ESP转到Ring0之前先保存了进程PSS:ESP,否则不可能做到SS:ESP切换到进程表之后还能把原来的栈指针保存起来。

MINIX3只用了一个TSS,主要原因大概是为了可移植性吧(Windows也一样,只用了一个TSS),且系统启动以后,TSS始终指向当前进程的进程表项中的栈帧顶部。但由于TSS是共享的,所以MINIX3只用了其中的SS0ESP0,由其指向被中断进程的栈帧保存寄存器,而其他字段未使用。也由于中断发生后栈指针立即指向内核区,所以P本身的进程栈未发生任何改变,减小了出错几率和栈溢出的可能性。

 

然后根据中断门把CS:EIP指向中断处理程序的入口,此处我又做了一个猜测,即CPU先完成了CS:EIP的压栈之后再进行跳转。(中断门包含了CS的特权级跳转)

MINIX3中断过程超级详详解

此时CS:EIPSS:ESP都转入到内核区,优先级也由Ring3变成了Ring0,第一个难题特权级跳转也解决了(关键在于TSS和栈帧)。以上操作全部由硬件(CPU、时钟定时器、8259A)完成,程序员看不到,但必须知道发生了什么:

中断发生根据TSS切换栈指针→5个寄存器压栈根据中断门修改程序计数器。

 

今生(中断处理):

一、保存现场

来到中断处理程序,现在控制权把握在程序员手中了。因为无论是什么中断都有相似的处理过程,所以MINIX3的中断程序被写成了汇编宏。

MINIX3中断处理程序(src/kernel/mpx386.s)

MINIX3中断过程超级详详解

汇编宏如下,时钟中断对应8295A的0号中断,所以irq=0

 

MINIX3中断处理程序宏定义(src/kernel/mpx386.s)

MINIX3中断过程超级详详解

 

中断处理的第一件事是call savesave如其名,就是保存程序状态的子程序。注意此时栈指针还指着P的栈帧,call指令是会把下一条指令地址压栈的,压到哪儿了?retaddr。所以此时retaddr压入了push (_irq_handlers+4*irq)的地址。随即来到了save

 

MINIX3中断处理save程序(src/kernel/mpx386.s)MINIX3中断过程超级详详解

 

save子程序主要做了三件事:

一、保存状态

二、判断中断是否重入

三、根据中断是否重入决定是否切换栈以及接下来的程序流

324L~329L把通用寄存器和段寄存器全部压入到栈帧,然后把SS的值赋给DSES,供内核函数使用。如注释所写“ss is kernel data segment”333LESP的值赋给EAX,这一步用EAX暂存了ESP的值,下面要使用。ESP现在指向哪儿?此时所有寄存器都保存完毕,所以EAXESP都指向P进程表项栈帧的起始位置。

保存完毕并设置了段寄存器后,save_k_reenter变量加了1_k_reenter是个全局变量,用来帮助判断是否发生了中断重入。初始化的时候是0,恢复或启动一个进程(restart子程序,下面会讲到)的时候它的值减1,发生一次中断的时候它的值加。所以一个进程运行没有中断发生的时候,它的值应该是-1

334L_k_reenter1后,335L随即进行判断跳转。如果_k_reenter此时的值是0,则没有发生中断重入,程序顺序执行;否则跳转到342L。二者的区别稍后再说,为了不徒增复杂度,此时继续以一个时钟中断为例子,没有重入,因此程序顺序执行。

来到336L,这一条指令至关重要,栈指针此时第二次进行切换,切到了内核栈区的栈顶(k_stktopkernel stack top,是存放内核栈顶地址的常量)。然后把restart子程序的入口地址_restart压栈,并清空EBP,最后通过一个jmp指令离开save

MINIX3中断过程超级详详解

jmp到的地址是RETADR-P_STACKBASE(eax)RETADRP_STACKBASE都是宏定义,这里的意思就是跳转到栈帧中retaddr的地址。由于EAX已指向P栈帧的初始位置,所以RETADR-P_STACKBASE(eax)就是P栈帧中的retaddr。回想刚才压入retaddr的内容,就是call save下一条指令的地址,所以jmp实际上回到了hwint_master。此处之所以通过jmp而不是ret返回,是因为ret会改变栈而jmp不会,而栈中已经保存了一个_restart留着将来使用,因此不能用ret返回。

二、处理中断

状态保存完毕,就可以开始处理中断了。这篇文章的重点不是中断处理,而是中断过程。因此这一部分一笔带过。

Tanenbaum老大在此处做出了一个niubility的决定:整个中断过程期间,禁止可屏蔽中断重入,这也就几乎消除了中断重入。好处很明显,提高了系统稳定性,增加程序流可控性,减小栈溢出的可能性;缺点就是可能出现MINIX处理中断的时候没法响应键盘之类的情况。但这就是Tanenbaum的个人观点,也是MINIX的设计哲学:操作系统最重要的是正确和稳定,性能什么的靠边站。注意只是可屏蔽中断不会发生重入,并不是说所有中断重入都不发生。

程序执行到218L压入了一个与时钟中断响应的irq_hook_t*作为intr_handler函数的参数,接下来就调用intr_handler处理时钟中断,然后清理堆栈(符合函数调用约定:堆栈由函数调用者清理)。至此,中断处理完毕。

P.S. 218L~220L调用了MINIX3的通用中断处理函数intr_handler,根据218L压入的参数再调用相应的特定中断处理函数,这其中又有中断链式响应等机制,但你只需要知道一点:这三行处理了中断。

来世(中断返回):

中断处理完之后,就要着手恢复进程的运行了。 第一件事就是判断intr_handler的返回值,但intr_handler并不是直接返回给hwint_master,而是把中断处理的结果放在irq_actids数组中。如果这个结果是非0值,就顺序执行223L~225L,屏蔽相应的IRQx(比如此处就关闭IRQ0,停止8259A对时钟中断的响应)。如果是0,则直接跳转到标号0226L

假设此时irq_actids[0]=0,程序直接跳到226Lhwint_master首先对8259A发送EOI,恢复了8259A对中断信号的响应,但CPU还不响应可屏蔽中断。然后执行retret?怎么不是iret?别着急~由我慢慢讲。

ret指令用是从栈中取出地址去修改程序计数器,此时的栈指针指到哪儿了?(分析中断过程中要不断问自己这个问题,要时刻把握栈指针的位置才能理清思绪)

上一次修改栈是218L,为了调用intr_handler压入了一个参数,但随后就清理了,栈的情况与之前一样。再上一次修改栈是什么时候?是337L,在save子程序中压入了一个_restart。这个ret正是跳到了_restart,也就是之前提到的restart子程序。

 

MINIX3中断进程恢复(src/kernel/mpx386.s

MINIX3中断过程超级详详解

 

restart并没有马上恢复寄存器的值,而是一开始做了个cmp,比较_next_ptr的值是不是0MINIX3进程调度的秘密就在于这个_next_ptr_next_ptr是个指向进程表项的指针,如果_next_ptr的值不是0,那么它就指向了下一个将要被执行的进程的进程表,于是它的值将被赋给_proc_ptr,之后再被设为0_proc_ptr也是个指针,指向当前正在运行的进程的进程表。_next_ptr是在哪儿被修改的呢?就是在处理中断的时候。

此处机制和策略实现了一个完美的分离,你进程怎么调度我不管,只要设置好_next_ptr就行,我只管切换。

391L之后的代码恢复一个进程的运行。首先由_proc_ptr决定要恢复哪一个进程,假设要是进程X。然后把ESP对准进程X的栈帧顶部,此时栈指针再次切换,从内核栈转回进程表项的栈帧。接着把ESP的值赋给EAXEAX的值加上一个偏移量之后(相加的结果指向了栈帧底部)赋给TSS,用于下一次中断。

到了396L_k_reenter1(前面提到过的),表示要离开这次中断了。然后从X栈帧内恢复一系列寄存器,恢复进程X被中断前的现场。

MINIX3中断过程超级详详解

ESP跳过一个retaddr,来到最后一步IRETIRET指令主要就做了一件事:将栈顶(栈指针此时指向哪儿?)五个值弹出并分别存入EIP,CS,ELFAG,ESP,SS,完成程序最后的恢复。此时又出现那个奇怪的小情况:ESP的值明明已经被设置成进程X被中断时的值了,但随后又正常恢复了SS,所以CPU里肯定有某处看不见的寄存器先保存了SSESP的内容。EFLAG是程序被中断前的状态,IF=1(否则不会被中断),因此此时可屏蔽中断又重新可以被响应了。

至此,时钟中断结束,恢复到进程X。

 

---------------------------------------------------------------------------------

如果你看完了还有疑问,那是我的错,没有写清楚;如果你看完了没有疑问,那还是我的错,没有留给你思考的余地。

以上です。

Sina blog显示的代码太挫了,我迟早要搬家……

阅读(1978) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~