一个操作系统的实现:第六篇——进程

汇编知识:

iretd

当一个中断服务程序执行完毕时,CPU将恢复被中断的现场,返回到引起中断的程序中。为了实现此项功能,指令系统提供了一条专用的中断返回指令。

该指令执行的过程基本上是INT指令的逆过程,具体如下:
◆从栈顶弹出内容送入IP;
◆再从新栈顶弹出内容送入CS;
◆再从新栈顶弹出内容送入标志寄存器;
对80386及其以后的CPU,指令IRETD从栈顶弹出32位内容送入EIP。

mov和lea的区别:

mov ax,BUFF ;是把BUFF这个内存单元中的数据放入到ax寄存器中
而 lea ax,BUFF ;是把BUFF这个内存单元的地址放入到ax寄存器中
两者区别就是一个传递的是内容,一个传递的是地址。

PUSHF 标志传送指令
本指令可以把标志寄存器的内容保存到堆栈中去

 

进程:

狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。

广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

从宏观来看,它有自己的目标,或者说功能,同时又能受控于进程调度模块(类似于工作受控于人);从微观来看,它可以利用系统的资源,有自己的代码(类似于做事的方法)和数据,同时拥有自己的堆栈(数据和堆栈类似于做事需要的资源和工具);进程需要被调度,就好比一个人轮换着做不同的工作。进程示意如图所示。

我们需要一个数据结构记录一个进程的状态,在进程要被挂起的时候,进程信息就被写入这个数据结构,等到进程重新启动的时候,这个信息重新被读出来。

进程状态:

进程自己是不知道什么时候被挂起,什么时候又被启动的,诱发进程切换的原因不只一种,比较典型的情况是发生了时钟中断。当时钟中断发生时,中断处理程序会将控制权交给进程调度模块。这时,如果系统认为应该进行进程切换,进程切换就发生了,当前进程的状态会被保存起来,队列中的下一个进程将被恢复执行。下图表示了单CPU系统中进程切换的情况,黑色条表示进程处在运行态,白色条表示进程处在休息态。在同一时刻,只能有一个进程处在运行态。进程切换的操作者是操作系统的进程调度模块。

进程切换:

我们把这个过程按照时间顺序整理如下:
1. 进程A运行中。
2. 时钟中断发生,ring1→ring0,时钟中断处理程序启动。
3. 进程调度,下一个应运行的进程(假设为进程B)被指定。
4. 进程B被恢复,ring0→ring1。
5. 进程B运行中。

我们先来分析一下,以进程A到进程B切换为例,其中有哪些关键技术需要解决。

1、 进程的哪些状态需要被保存

只有可能被改变的才有保存的必要。我们的进程要运行,不外乎CPU和内存在相互协作,而不同进程的内存互不干涉(我们考虑最简单的情况,假设内存足够大),但是我们提到过,CPU只有一个,不同进程共用一个CPU的一套寄存器。所以,我们要把寄存器的值统统保存起来,准备进程被恢复执行时使用。

2、进程的状态需要何时以及怎样被保存

为了保证进程状态完整,不被破坏,我们当然希望在进程刚刚被挂起时保存所有寄存器的值。你一定在想,保存寄存器我已经很拿手,push就可以了,没错,push就够了。不过,Intel想得更周到,不但有push,更有pushad,一条指令可以保存许多寄存器值。而这些代码,我们应该把它写在时钟中断例程的最顶端,以便中断发生时马上被执行。

3、如何恢复进程B的状态

不用说,你一定早就想到了,保存是为了恢复,既然保存用的是push,恢复一定用pop了。等所有寄存器的值都已经被恢复,执行指令iretd,就回到了进程B。

4、进程表的引入

进程的状态无疑是非常重要的,它关系到每一次进程挂起和恢复。可以预见,我们今后将多次提到它,对于这样重要的数据结构,我们总不能在每次提到时叫它“保存进程状态的那个东西”,总要有个名字。还好,前人已经替我们起过了,那就是“进表”(有的书中称之为进程控制块,也即PCB)。
进程表相当于进程的提纲,纲举目张,通过进程表,我们可以非常方便地进行进程管理。
从代码编写这个角度来看,除中断处理的部分内容我们不得不使用汇编之外,我们还是要用C来编写大部分进程管理的内容。如果把进程表定义成一个结构体的话,对它的操作将会是非常方便的。
毫无疑问,我们会有很多个进程,所以我们会有很多个进程表,形成一个进程表数组。进程表数组如图所示。

进程表是用来描述进程的,所以它必须独立于进程之外。所以,当我们把寄存器值压到进程表内的时候,已经处在进程管理模块中了。

5、进程栈和内核栈

当寄存器的值已经被保存到进程表内,进程调度模块就开始执行了。但这时有一个很重要的问题容易被忽视,就是esp现在指向何处。
毫无疑问,我们在进程调度模块中会用到堆栈,而寄存器被压到进程表之后,esp是指向进程表某个位置的。这就有了问题,如果接下来进行任何的堆栈操作,都会破坏掉进程表的值,从而在下一次进程恢复时产生严重的错误。
为解决这个问题,避免错误的出现,一定要记得将esp指向专门的内核栈区域。这样,在短短的进程切换过程中,esp的位置出现在3个不同的区域(下图是整个过程的示意)。

其中:
进程栈──进程运行时自身的堆栈。
进程表──存储进程状态信息的数据结构。
内核栈──进程调度模块运行时使用的堆栈。
在具体编写代码的过程中,一定要清楚当前使用的是哪个堆栈,以免破坏掉不应破坏的数据。

6、特权级变换::ring1→ring0

对于有特权级变换的转移,如果由外层向内层转移时,需要从TSS中取得从当前TSS中取出内层ss和esp作为目标代码的ss和esp。所以,我们必须事先准备好TSS。由于每个进程相对独立,我们把涉及到的描述符放在局部描述符表LDT中,所以,我们还需要为每个进程准备LDT。

7、特权级变换::ring0→ring1

在我们刚才的分析过程中,我们假设的初始状态是“进程A运行中”。可是我们知道,到目前为止我们的代码完全运行在ring0。所以,可以预见,当我们准备开始第一个进程时,我们面临一个从ring0到ring1的转移,并启动进程A。这跟我们从进程B恢复的情形很相似,所以我们完全可以在准备就绪之后跳转到中断处理程序的后半部分,“假装”发生了一次时钟中断来启动进程A,利用iretd来实现ring0到ring1的转移。

进程表及相关数据结构对应关系示意:

由上图所知进程不外乎4个要素:进程表、进程体、GDT、TSS。它们之间的关系大致分为三个部分:
1. 进程表和GDT。进程表内的LDT Selector对应GDT中的一个描述符,而这个描述符所指向的内存空间就存在于进程表内。
2. 进程表和进程。进程表是进程的描述,进程运行过程中如果被中断,各个寄存器的值都会被保存进进程表中。但是,在我们的第一个进程开始之前,并不需要初始化太多内容,只需要知道进程的入口地址就足够了。另外,由于程序免不了用到堆栈,而堆栈是不受程序本身控制的,所以还需要事先指定esp。
3. GDT和TSS。GDT中需要有一个描述符来对应TSS,需要事先初始化这个描述符。

第一个进程的启动过程示意如下图所示:

增加一个任务需要的步骤:

1. 在task_table中增加一项(global.c)。
2. 让NR_TASKS加1(proc.h)。
3. 定义任务堆栈(proc.h)。
4. 修改STACK_SIZE_TOTAL(proc.h)。
5. 添加新任务执行体的函数声明(proto.h)。

当前系统运行流程:

Boot.asm -> Loader.asm -> Kernel.asm -> cstart(start.c) -> init_prot(protect.c) -> Kernel.asm -> kernel_main(main.c) -> restart(Kernel.asm)

系统调用:
操作系统实现提供的所有系统调用所构成的集合即程序接口或应用编程接口(Application Programming Interface,API)。是应用程序同系统之间的接口。
换句话说,应用程序的能力是有限的,很多事情做不了,只能交给操作系统来做。系统调用就是告诉操作系统:“我有一件事,请你来帮我来完成。”所以,一件事情就可能是应用程序做一部分,操作系统做一部分。这样,问题就又涉及特权级变换。

 8253/8254 PIT:

时钟中断实际上它是由一个被称做PIT(Programmable Interval Timer)的芯片来触发的。在IBM XT中,这个芯片用的是Intel 8253,在AT以及以后换成了Intel 8254。8254功能更强一些,但对于增强的功能,我们并不一定涉及,在下面的陈述中,我们只称呼它8253。
8253有3个计数器(Counter),它们都是16位的,各有不同的作用,如表所示。

从上表中看到,时钟中断实际上是由8253的Counter 0产生的。
计数器的工作原理是这样的:它有一个输入频率,在PC上是1193180Hz。在每一个时钟周期(CLK cycle),计数器值会减1,当减到0时,就会触发一个输出。由于计数器是16位的,所以最大值是65535,因此,默认的时钟中断的发生频率就是1193180/65536≈18.2Hz。
我们可以通过编程来控制8253。因为如果改变计数器的计数值,那么中断产生的时间间隔也就相应改变了。
比如,如果想让系统每10ms产生一次中断,也就是让输出频率为100Hz,那么需要为计数器赋值为1193180/100≈11931。

8253的端口情况,如下表所示:

从上表我们知道,改变Counter 0计数值需要操作端口40h。但是这个操作稍微有一点复杂,因为我们需要先通过端口43h写8253模式控制寄存器。先来看一下它的数据格式,如下图所示。

计数器模式位如下表所示:

注意: 模式的选择不是唯一的,Minix和Linux就分别使用不同的模式。

读/写/锁(Read/Write/Latch)位如下表所示:

注意: 锁住(Latch)当前计数器值并不是让计数停止,而仅仅是为了便于读取。相反,如果不锁住直接读取会影响计数。

计数器选择位如下表所示:

进程调度:

。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值