进程、系统调用和进程调度
进程、系统调用和进程调度
进程是操作系统最重要的概念之一,实际上,实现进程之前的部分都不能被称为操作系统。进程的切换和调度等内容是和保护模式的相关技术紧密相连的,而这也是本书作者要花大量篇幅先介绍保护模式的原因,但是,对于一个只学过微机原理和C语言的小白(比如我)来说,理解起来还是太难了。为此,我特意先学习了保护模式相关的知识,具体书籍和路径前面已经介绍过。
“对于进程的概念,只有在有了基于具体平台的感性认识之后,才有可能对形而上的理论有更踏实的理解”,对作者的这句话我深以为然。其实不单单是进程的概念,对于计算机领域的无数理论概念来说都一样,各种抽象的理论和概念没有落脚点,朦胧的理解就会很快被遗忘,而这个落脚点其实就是具体的实现,这和那句著名的“源码之下,了无秘密”的道理其实是一致的。“有了一种机型的经验,不但有利于在学习理论时形成形象思维,更有触类旁通的能力,面对任何类型的机器和操作系统都能够胸有成竹。”
进程介绍
作者的这段关于进程的讲解也十分符合他的理念,将进程涉及的代码数据和堆栈以及对进程的调度这些抽象的概念与具体的人做工作的行为对应起来,给抽象的概念以具体的例子支撑。看过很多对进程的讲解都是为了举例子而举例子,看完还是无法将进程理解清楚。
多进程的必要准备
多进程下,处于内存中的进程数总是大于CPU数的
graph LR
进程数大于CPU数-->进程有运行和挂起等不同状态
进程有运行和挂起等不同状态-->内核要对各个进程进行调度在不同状态间切换
内核要对各个进程进行调度在不同状态间切换 -->切换时需要保存和恢复进程运行状态
切换时需要保存和恢复进程运行状态--> 采用数据结构来保存进程状态
几个设计点明确 最简单的进程(包含涉及的关键技术) 进程调度过程 进程A运行时钟中断发生,ring1->ring0,进入中断处理程序调用进程调度模块,获取切换的目标进程B切换到(恢复)进程B,ring0->ring1进程B运行 关键技术 先进行ring0->ring1——启动第一个用户进程
准备工作有:进程表,进程体,GDT和TSS
进程表和GDT的创建和设置:这里因为LDT也是进程的一部分,所以方便起见也把LDT放在了进程表PCB里,同时还放了LDT的选择子。然后在GDT里放置进程的LDT描述符。进程表和进程。进程表是进程的描述,储存了进程的各个寄存器的值,暂时为了简单起见,只放必须的,比如进程的入口地址,另外,由于堆栈是由内核设置的,不受程序本身控制,所以还需要指定esp。GDT和TSS。在GDT里创建进程对应TSS的描述符。 注意点
一定要记得以下几点:
进程启动过程总结
第二步,开始使用时钟中断——通过中断回到内核ring0 多进程
前面已经为多进程打好了基础,可以从内核跳转到第一个进程运行,运行的进程可以被中断,中断完成后还能返回到原来的进程恢复运行。
添加新的进程体
在main.c函数里加一个TestB()函数。
实现批量准备进程4要素的相关变量和宏 进程表初始化代码扩充(用for批量处理)
每次循环从TASK中读取不同任务的入口地址、堆栈栈顶和进程名(这里进程和任务等价)
两个注意点:
批量生成进程的LDT
在()里用for循环批量
修改中断处理程序——添加进程切换代码
想要在中断处理程序中恢复不同的进程只需要将esp指向不同的进程表即可。
mov esp, []就是设置切换的目标进程。是指向进程表的指针,所以我们只需在这条语句前面通过进程调度算法,得到调度的目标进程,然后将指向目标进程的进程表即可。
添加任务步骤总结
一个值得注意的点
中断重入的处理。之所以允许中断嵌套,是为了在进程调度的时候允许其他的一些需要及时响应的中断比如键盘中断等,而一旦允许了中断嵌套,时钟中断也就有可能嵌套本身,这可能造成时钟中断的无限嵌套,为了解决这个问题,我们才使用全局变量来识别当前是否是重入的时钟中断,如果是则直接返回。而对于其他类型的中断则没有影响。
后面仿照minix的中断处理整理代码
总结来说,minx的中断处理和我们前面的实现总体是一致的,只不过minx更加模块化,代码的耦合性更低,并将()部分和中断处理程序后半部分这两个一致的部分合并成一个,让代码更简洁。
最后,再将这个中断处理格式写成多行宏的形式,拓展到每一个中断通用的格式,并将其中原来的也用函数指针数组来代替,根据中断向量号来调用不同的中断处理程序。至此,进程调度的框架已经搭好,甚至同时为整个中断系统都搭好了基本框架,这个框架解决了中断重入问题,可以保证中断处理程序部分才能被重入,而且不能被当前中断程序本身嵌套,只能被别的中断嵌套。
整理代码总结
目前为止整个系统运行过程:
系统调用
系统调用是应用程序和操作系统的桥梁,运行在低特权级的应用程序能够通过它来间接地安装操作系统的规范来使用其不能直接使用的指令或者内存区域。
这章主要就是在实现一个简单的系统调用的同时也搭建好创建系统调用的框架,这样后面自己添加新的系统调用的时候就很方便了。
实现一个简单的系统调用
ticks全局变量代表发生时钟中断的次数,就是获取当前发生时钟中断的次数。
我们的目标是实现进程能够随时通过系统调用来获取ticks值,既然是要能够随时,那自然就想到使用中断来实现。
发生中断的时候,处理程序从何得知调用的是哪个系统调用函数以及其参数呢?首先自然想到可以用堆栈,因为ring0是可以直接读取ring1的堆栈的,也无需切换堆栈。这是可以的,但是这里暂时无需传入参数,只要传入一个标号来标识调用的是哪个系统调用即可,所有系统调用只占用一个中断向量号,各个系统调用的函数指针组成一个函数指针数组[]类似。后面一旦要实现有很多参数的复杂系统调用,用寄存器来传递的话就不现实了,还是得用堆栈,这里暂且先这样。当然,要能够通过int + 中断向量号来触发种中断处理程序也是需要在idt中初始化相应的中断门(描述符)才行类似宏来实现,先save保存现场,然后调用相应的函数(这才是真正的系统调用函数)。但是这里有个问题就是,原来的eax是没有用来传东西的,所以save里用了eax,这里把它换成esi就可以解决。这样save就可以直接用了,无需另外再写。实际的系统调用函数()放在哪里? 由于ticks看上去是和进程相关的东西,所以这里就单独建一个文件proc.c来放它(主要是因为后面ticks在进程调度里有很多应用)实现正确的调用系统调用后,就要实现的实际功能:也就是设置全局变量ticks,并在里更新它,最后在里获取并返回它。 的应用——更精准的延时函数
利用相对精准的时钟中断周期来实现更精准的延时函数
8253/8254 PIT可编程周期计数器/定时器
这部分其实算是复习了,因为之前微机原理学习过。8254就是比8253功能强一些。
8253有3个计数器()它们都是16位的。
计数器作用
0
输出到IRQ0, 以便每隔一段时间让系统产生一次时钟中断
1
通常被设为18,以便每15us做一次RAM刷新
2
连接PC喇叭
进程调度
这一小节主要是利用刚刚的ticks来让进程运行的时间不一,从而在某种意义上实现优先级的概念。但是这里的调度算法比较简单和操作系统理论书里的很多调度算法不一样。不过,本着简洁高效的原则,只要能够体现一定的优先级,并对不同优先级的进程实现不同的调度即可。
初始调度策略 改进的调度策略
就是一直调度值最大的进程,直到其ticks为0,再调度下一个第二的进程。三个进程都结束后,再重置它的ticks为初值。这样就能够实现按照优先级,优先级值高的进程先被调度,而且执行每次调度执行时间也和优先级成正比。
可以看到,运行结果如图,打印出的字符的比例和优先级比例15:5:3很接近
总结
最后,作者提到,这个最后的调度策略其实是从linux中借鉴来的,“轻重缓急”四个字可以概况这个策略,优先级高的进程"更早地处理"也被处理“更多的时间”。
Minix中,进程被分成Task、和User(任务、服务和用户进程)三种,进程调度也为此设置了三个不同的优先级队列来分别调度。