进程管理的故事
这次我们来将一个关于进程管理的故事。。。
什么是进程管理
操作系统是替我们管理硬件的软件,所以与其说是进程管理,倒不如说是CPU管理来的贴切。
为什么要对CPU进行管理呢?
1.屏蔽CPU进行编程, 市面上的cpu那么多,我们每次换一块cpu执行就要更改我们的代码,很不方便。
2.对cpu的管理复杂且不安全, 我们希望能直接调用一个接口而不去管其中的细节。
所以,我们需要操作系统提供一个安全且可靠的机制来帮我们执行我们的程序。
一个简单的版本:直接执行
作为一个最简单且直接的思路,我们每次接收到用户的系统调用,我们就让cpu执行该代码,执行的时候不允许其他代码进入,执行完成后将结果返回。
怎么实现?
这个版本有一个前提,那就是我们需要一个唯一的标识来识别我们的程序,这就是进程的由来。
进程是一段代码在一个数据集上的一次执行
这是进程的定义,注意,同一段代码执行两次也算不同的进程。
进程是一个抽象的概念,那么进程在系统中是如何定义的呢?
我们通过进程控制模块(processor control module),又叫PCB模块来实现我们的进程。
在这个版本,我们的PCB
只需要包含进程描述信息就可以了:
pcb:
1.进程描述信息:进程唯一标识符、用户标识符
有什么问题?
最为明显的问题就是我们每次只能执行一个程序,在此之间其他的程序只能等待,用户体验极差,且当一个程序正在执行I\O等费时操作,我们的CPU是空闲的,造成了资源的浪费。
版本升级:多道程序执行
经过大脑的一番研究,我们发现可以让程序在执行I\O等操作时把cpu让出来让其他程序执行,I\O完成之后再发出中断请求执行。
怎么实现?
要实现进程之间的切换,
首先我们得为进程设计一个状态,来判断进程是否处于执行中,
另外,我们需要将切换出来的进程信息保存下来,以便下次执行时能继续,
最后,我们切换进程时接下来调用哪一个进程呢?我们需要一个调度算法来选择进程执行。
进程状态设计
作为最基本的状态,我们至少需要5个状态来描述:
- 进程创建
- 进程就绪
- 进程执行
- 进程阻塞
- 进程结束
首先,我们创建了进程,该进程肯定是可以执行的,但是cpu同时只能执行一个进程,所以该进程需要进入就绪状态等待cpu的调用。
好不容易等到了cpu,于是我们开始运行,此时处于运行状态,可是突然需要进行I\O操作,没办法,为了大家的效率,我们需要把cpu让出来,此时属于阻塞状态,等到I\O完成了,我们也不能直接抢占cpu执行,而是进入就绪状态继续等待cpu,终于,我们把程序执行完了,达到了结束状态。
可是,根据我们之前学习的内存管理,进程在被创建之后就从磁盘加载到了内存中,而当我们处于就绪状态和阻塞状态时,我们的进程是没有在执行的,这不就造成了内存空间的浪费了吗?
于是我们还需要两个状态:
- 就绪挂起
- 阻塞挂起
当我们处于就绪或阻塞状态时,我们可以选择将该进程的内存换到磁盘中,此时处于就绪挂起/阻塞挂起中,等到一定条件后,可以激活该进程,是其返回到就绪/挂起状态。
另外,我们的PCB也需要保存该进程的状态信息了:
pcb:
1.进程描述信息:进程唯一标识符、用户标识符
2.进程的状态
进程上下文保存
要让切换回来的进程能继续执行,我们需要保存哪些信息呢?
首先,我们需要知道该改代码运行到哪了,所以需要保存程序计数器的值,
另外,学了内存管理的我们知道,每个进程都有自己的段表和页表,而段表和页表的地址都保存在各自的寄存器中,所以我们也需要将各个寄存器的值保存下来。
这样,我们的pcb又多了一些信息:
pcb:
1.进程描述信息:进程唯一标识符、用户标识符
2.进程的状态
3.各个寄存器的值
进程的调度
进程的调度算法有很多,
先来先服务、最短作业优先、高响应比优先、时间片轮转、优先级调度等等。
各有利弊,网上的资料也很多,我们这里不讨论了,总之,我们需要从就绪队列中拿出一个进程来执行,这就是我们的目的。
并且,我们如果有优先级的时候,我们还需要再pcb中保存这个优先级。
所以pcb,你又要更新了:
pcb:
1.进程描述信息:进程唯一标识符、用户标识符
2.进程的状态
3.各个寄存器的值
4.进程优先级
有什么问题?
我们知道,进程之间是相互隔离的互不干扰的,但是如果我们需要多个进程合作来完成一个任务呢?
换句话说,我们怎样让两个进程进行通信呢?
再升级:让进程通信
如何让进程通信?我们可以通过多种方式来实现。
怎么实现?
管道
管道就是一个缓冲区,发送方把信息发送到缓冲区里,接收方从管道中取信息,这样就完成了进程的通信。‘
管道包含匿名管道和命名管道
匿名管道在父子进程中可以使用,比如我们运行
ps -ef | grep java
ps是父进程,他将结果发送给了子进程grep
命名管道可以在非亲缘关系的进程中进行,但是我们需要预先定义。
消息队列
消息由链表实现,和管道不同的是,消息队列具有类型,而不是只发送字节流,并且消息队列的空间大于管道
信号量
信号量是一个全局的计数器,进程之间可以通过这个全局的变量来进行同步
共享内存
共享内存即让两个进程的一片虚拟内存映射到同一片物理内存上,这样就可以感知对方进程的信息。
socket
socket 可以完成不同主机上的进程的通信。
有什么问题?
有没有感觉进程间的通信十分麻烦呢?作为速度最快的共享内存方式,我们还需要进行内存的映射,那我们为什么不让一个进程完成这所有的任务呢?反正我一个进程内的内存都是共享的嘛。
再再升级:引入线程
为了能让一个进程能处理多个任务,我们需要再引入一个抽象的概念:线程。
引入了线程,我们任务间的通信就方便了许多,并且,线程的切换不需要切换虚拟空间,这也减少了上下文切换的消费。
但是引入了线程,我们的程序也变得越来越复杂,编程的难度也大大增高。