操作系统的核心功能就是管理计算机硬件,而CPU就是计算机中最核心的硬件。而通过学习笔记3的简史回顾,操作系统通过多进程图像实现对CPU的管理。所以多进程图像是操作系统的核心图像。
参考资料:
- 课程:哈工大操作系统(本部分对应 L8 && L9)
- 实验:操作系统原理与实践_Linux - 蓝桥云课 (lanqiao.cn)
- 笔记:操作系统学习导引 · 语雀 (yuque.com)
1. 从使用CPU开始直观理解CPU管理
要想管理CPU,就要知道如何使用CPU。
CPU的工作原理已经很熟悉:
- 取指执行
- 程序存放在内存中,每段指令对应一个地址
- CPU发出取指命令,将想去地址通过地址总线传到PC
- 内存根据地址取出对应地址的指令
- 从总线传回,CPU解释执行
所以,管理CPU最直观的方法就是,设置PC的初值,CPU就能按照规则依次执行下去。
这样做有什么问题?
来看下面一段程序
int main(int argc,char* argv[]){ int i,to,*fp,sum=0; to = atoi(agv[1]); for(i = 1; i <=to; i++){ sum = sum + i; fprintf(fp,"%d",sum); } }
如果要让CPU工作,就是要让PC指向这段程序的起始地址。
但是!程序和程序之间是不一样的。例如将
fprintf()
替换为其他计算语句替换前后的运行时长进行比较,则前者:后者≈10:1
而假设我们遇到一种程序,有10个计算指令,然后一条IO指令,如果还是按照上面所说的设置PC初值,让其自动执行,那么对于CPU来说,其忙碌的计算指令只占到了总时长的一半(另一半在等待IO),利用率不高。
怎么办?
2. CPU管理的核心:并发
举一个烧水的例子,首先往烧水壶里倒水,然后放在插座上,然后就可以去做别的事情了,等烧水壶响了,这就是中断,这时我们就可以来用烧水壶里的热水了,烧水的过程就类似IO
所以解决方案为:多道程序交替执行,一个CPU上交替执行多个程序,即并发
可见,上图两个程序A、B充分利用了CPU的计算资源,总时长从80降到了45.
如何实现并发呢?
即控制 PC 进行切换
适当的时候修改PC,使得PC指向另一个程序的指令,但是只修改PC会有问题
例如下图左右两个程序,当PC按照逻辑切换回地址53继续程序1的执行,那么ax和bx寄存器应当存储什么值?
很显然,如果要继续程序1,当然应当为1 和 1,而不是 10 和 10.
所以当程序切换时,除了切换PC,还要切换很多内容
我们需要记录 切换前的上下文,保护现场。
每个程序有一个存放信息的结构:PCB,process control block,进程控制块。
这样,我们实际运行过程中的程序,就跟我们单纯汇编得到的代码不一样了。即运行程序和静态程序不一样。
而程序 + 所有这些不一样 ---> 进程
如何描述这种不同呢?
!进程! 这个概念就用来刻画运行中的程序。比如上图中的程序1 和程序2,就是两个进程。
3. 简单总结1
到这里,我们进程描述CPU的管理:
使用CPU:启动一个进程,让CPU去执行这个进程;
更高效的使用CPU:启动多个进程,让CPU去执行多个进程;
跑多个程序/进程的样子,就是CPU管理的核心样子。
这就是多进程图像。
4. 多进程图像
前文讲到,为了让CPU更好的工作,我们需要让CPU执行多进程,而这个过程如何表征呢?
- 对于用户而言
- 就是一个个 PID 进程号;
- 可供用户查看各进程运行情况;
- 对于下层操作系统而言
- 负责管理 各个进程;具体为记录情况、按照合理的次序推进;
- 分配资源、进行调度;
多进程图像从开机一直存在到关机结束。
4.1 开机到关机过程中的多进程图像
系统启动时,最后启动的 main.c 中最后执行了
fork()
if(!fork()){init();} // fork,启动进程的接口
代码意思是:启动一个进程,执行
init()
,即执行 shell,接下来就能再 shell 里操作,这就是计算机提供给用户使用的界面(初代版本)。可以理解为,操作系统要让用户使用计算机,需要创建一个初始化的进程。
shell 再根据用户输入启动其他进程,执行用户的命令也是在创建进程;
// shell 的核心代码 int main(){ while(1){ scanf("%s",cmd); if(!fork()){ exec(cmd); wait(); } } }
此后,计算机每执行一个任务,就开启一个进程。
4.2 查看当前进程情况 | 任务管理器
在 win10 以上版本中,Ctrl + Shift + Esc 就可看到任务管理器。
- 其中Explorer是整个Windows的文件系统,如果关掉整个进程,就只能看见背景了。
- 如果感觉计算机特别慢,就可以打开任务管理器,查看占用CPU资源比例大的进程。
- 操作系统就是通过管理进程,来管理用户对计算机的使用。
4.3 操作系统如何实现多进程图像
为了实现多进程图像,操作系统都应该解决哪些问题?
多进程如何组织?也即多进程如何存放?
操作系统感知进程依赖于PCB,组织和存放进程也靠PCB,通过PCB形成一些数据结构(队列),来组织多进程;如下图:
组织好多进程,才能合理推进多进程。
如何推进多进程?
一个进程正在执行
另一些进程在排队(就绪队列)等待执行
还有一些在等待触发事件,即使排到也不能调度执行
总结:多进程对应的PCB分别放在不同的地方,执行不同的处理。
把进程通过状态区分开来,通过操作系统对进程状态的转移控制,多进程就向前推进了。
多进程如何交替/切换?
情境:一个进程启动磁盘读写,等待时进行切换。
下图展示了关键代码,代码注释见图中红色字体;
schedule()函数是重点,即调度函数;
下图中的
getNext
从就绪队列中挑出下一个需要占用CPU的进程;switch_to
就是用 PCB 进行进程上下文的切换,pCur
、pNew
分别指当前进程的 PCB 和调度得到的下一个进程的 PCB ,即进行执行现场的更替。
进程如何调度?
- 这里先讲两个基础调度算法。
- FIFO,First In First Out.
- 显然是比较公平的策略,但是没有考虑进程执行的任务轻重缓急;
- Priority.
- 对进程赋予优先级,但如何赋予也是个问题。
- FIFO,First In First Out.
- 这里先讲两个基础调度算法。
切换进程
调度找到下一个占用CPU的进程后,就要进行切换;
这个过程需要精细控制,所以需要 汇编代码,下图为伪代码;
做的事情也不难想象,先把将要停下的进程信息保存到PCB1中(将当前CPU的各种信息(寄存器等)保存到pCur中),
再从将要进行的进程的PCB2中取出信息赋到对应寄存器/位置(将pNew中的寄存器等信息恢复到CPU中)
多进程交替时,如何相互影响?
多进程看似不打照面,但实际上它们同时在一个内存来存放。
多个进程交替执行会相互影响,包括正面的多进程合作,负面的内存地址冲突等等
比如,进程1中,修改了某个地址的值,而这个地址,正好时进程2 包含的地址,这时就会引起进程2崩溃。
如何解决进程间矛盾?
限制对进程2地址的读写。即:!内存映射!
通过一个映射表,将真实物理地址转化为虚拟存储地址;
两个进程的100内存地址,是虚拟逻辑地址,会映射到不同的物理内存;下图中展示了两个进程的100地址分别映射到了物理地址780和1260
还有一些时候,进程之间需要进行合作,如何进行进程间合作?
举例1(浅显):
- 不同的应用程序提交打印任务,打印任务会被放到“待打印文件队列”
- 打印进程会从“待打印文件队列”中一个接一个的取出打印任务,控制打印机打印
- 如果对存入打印进程的任务不进行管理,如任务1没放完,任务2就开始放,后面切换时就会出现顺序执行所不会遇到的乱序问题。
举例2(稍深):生产者-消费者实例
、
生产者和消费者通过共享数据
buffer[]
进行合作如果缓冲区满了,就不应该再放了,
用
counter
记录,如果==buffer_size,说明满了,死循环;没满则counter++
;如果多个进程都在内存中交替执行,counter可能就会出错。
下面是个具体的例子:
初始counter=5,生产者执行counter++,消费者执行counter--,在寄存器层面将会是:
// 生产者P register = counter; register = register+1; counter = register; // 消费者C register = counter; register = register -1; counter = register;
当生产者的程序执行到中间切换到消费者,可能的代码序列如右上角所示,counter 直接乱了。后续合作就也会乱套。
解决合作问题(合作各方的合理推进顺序)的核心在于 !进程同步!
给 counter 上锁,即写 counter 时阻断其他进程访问 counter.
5. 简单总结2
理解CPU管理的基本想法
直观感受了操作系统的多进程
具体了解了多进程图像,探讨了操作系统如何实现多进程图像。
这时多进程图像的大致轮廓,后续会一一展开: