目录
为什么会出现进程这个概念
进程切换会产生什么影响
哪些状态需要被保护呢
如何保存这些状态呢
如何调度进程
时钟中断
中断重入
为什么会出现进程这个概念
如果没有进程这个概念,程序是如何执行的呢?那只能运行一个程序,运行完一个程序,才能接着运行下一个。哪怕上个程序中间要等待某件事十天半个月,也是没有办法的事情了。
如果有进程这个概念呢?那么操作系统看到有个进程闲了,就不把cpu分给它了,这就是进程切换的概念。这样cpu利用率就上来了。此时还要考虑一个情况,要是一个进程一直忙,难道其他进程就只能在旁边等着吗?又规定每个进程最多能连续使用多久的cpu时间,到时间必须让给别人,这就是所谓的进程时间片。
而由于进程的出现,可能会引发这样一个问题,某一个进程崩溃了,大家都一起玩完了。为了避免这样的情况出现,intel引入了保护模式的概念。一个进程挂了,给操作系统一个机会,清除这个进程,而不是和它一起灭亡。
在保护模式,intel规定了不同的特权级。linux系统用0和3两个特权级。关于特权级,见保护模式下处理器特性那篇文章。咱们这个实践过程中用了三个特权级,0、1、3,比linux多用了一个1特权级,这是因为作者用的微内核,对于一些系统的功能模块给了1这个特权级,比内核低,比用户进程高。
进程切换会产生什么影响
不同的进程执行的是不同的代码,同一个进程不同的特权级这两个问题又带来了堆栈切换的问题。因为不同的特权级、不同的进程,如果共用一个堆栈,谁也不能保证再切回去的时候堆栈没有被破坏。所以我们必须保护好切换前的执行环境。
哪些状态需要被保护呢。
上帝的归上帝,撒旦的归撒旦。进程的当然归进程自己了。
又需要一个账本了:
- 记录每个进程还剩多少时间
- 记录进程此时用到的各个寄存器,比如说eip,esp这些比较重要的数据,等下次切换回来接着用
- 再就是联系上文件系统,还有打开的文件描述符数组
- 还有保护模式下LDT的选择子
- 还有自己的状态
- 进程自己的代码段和数据段描述符
这些就完了吗?肯定不可能一次考虑周全,只能随着工作的推进,我们的考虑才能越来越完善。
那么这些东西保存在哪里呢?如果随便保存在一个地方,系统还需要额外做个记录,既然每个进程都独有一份不同的记录,并且所需的空间是固定的,不会一会儿大一会小。这样的话,最好就是放在进程表里,既免去了系统要做的额外记录,而且进程本身如果要对这些值做修改是非常方便的(例如exec加载一个新的镜像)。
如何保存这些状态呢
保存的地方有了,似乎就应该可以切换了。来看看有几种切换方式。
- 从内核态切换到用户态
- 从一个进程的内核态切换到另一个进程的内核态
- 从用户态切到内核态
前两种状态比较好处理,都是按照保存在进程表中的寄存器顺序依次弹出即可。
单单是从用户态到内核态有点麻烦,因为用户态的时候你不能访问内核态的数据,那么你怎么会知道你的进程表在哪里呢?那么用户态的执行环境就没法保存了。
intel从硬件上给予了支持。在从用户态切换到内核态的时候需要用到tss段了。当因为某些信号从用户态切换到内核态时候,cpu会把ss、esp暂时保存在cpu中,然后从tr寄存器中拿到选择子,从GDT中得到tss的段基地址,然后将tss.ss0给ss寄存器,tss.esp0给esp寄存器,这两个值就相当于从用户态切换到内核态时候的堆栈指针。再把刚才保存的ss、esp加上EFLAG、cs、eip放到esp0指向的栈上。
这就要求进程在离开内核态回到用户态的时候,必须要把tss段中的ss0和esp0的值设置为自己进程表中保存用户执行环境的起始地址。
保护进程的现场,好像又引出了不少东西。GDT、LDT、TSS。其实它们几个都是内存中的一张表。网上都有说明。
说一下以前自己容易忽略的LDT寻址的细节。
当TI=1时表示段描述符在LDT中:
- 还是先从GDTR寄存器中获得GDT基址。
- 从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。而LDTR的内容是用lldt加载的,这个时候就清楚了代码中从内核返回的时候为什么要加载进程各自的LDT选择子了。
- 以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。LDT表是作为一个段存在的。从这可以看出来GDT是总目录,大家都要从要作为起点来寻址。
- 用段选择器高13位位置索引值从LDT段中得到段描述符。(此时是段选择子,一般是cs)
- 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。(cpu来完成)
似乎进程这个值得费笔墨的知识点真的没有什么东西可以说的了。
也难怪,伟大的东西似乎都是简单的。
如何调度进程
那进程切换的时机呢?总不能让进程自己决定什么时候不用cpu了吧?万一有个无赖怎么办?
时钟中断
这个时候,中断就是一个非常好的仲裁者了,依靠物理特性,准时准点的报时。系统只需要在石英晶体报时的时候做一些计算就可以了。
在时钟中断里面,我们可以来判断当前进程是不是把时间片用完了,那系统从准备好的进程队列中挑选一个进程来执行。当然,这个挑选的过程可以简单也可以复杂,看实现者想做什么了。目前我们要做的比较简单,每个进程分配一些时间片,每次时钟中断里面把当前进程的时间片减1,等于0的时候,被系统拿掉cpu的使用权,让下一个进程使用cpu。
中断重入
时钟中断还是比较简单的,设置好8259A就可以响应中断信号了。但是在书中,作者又让我了解到中断重入的概念。以前看linux 0.12的时候时钟中断的代码中却是没有重入的概念。
来看一下cpu响应中断时候会发生什么:
CPU响应中断后,输出中断响应信号,自动将状态标志寄存器的内容压入堆栈保护起来,然后将状态标志寄存器中的中断标志位IF与陷阱标志位TF清零,从而自动关闭外部硬件中断。因为CPU刚进入中断时要保护现场,主要涉及堆栈操作,此时不能再响应中断,否则将造成系统混乱。
书中给出的实例是保存了寄存器现场后用sti指令打开了中断,所以允许中断重入,而linux 0.12中进入中断后没有做额外的处理,所以不用理会中断的重入。
处理中断重入也不算难,需要定义一个全局变量,并初始化为-1。进入中断对这个变量加1,如果本次的计算结果不等于0,说明是重入的中断,直接结束本次中断。