目录
[进程的特征, 概念](#进程的特征, 概念)
进程
进程的特征, 概念
我们下面先简单介绍一下什么是进程
首先你看到的qq.exe就是一个存储在磁盘里面的可执行文件, 它是一系列指令的集合. 而所谓的进程是动态的, 是程序的一次执行过程.
例如我有一个qq.exe文件, 那么双击这个文件, 救护让操作系统去执行这个指令集合, 那么执行的过程就是进程, 也就是你在任务管理器中看到的一个个进程:
那么很多个执行文件一起执行, 那么就会产生很多进程, 为了管理这些进程, 操作系统会为他在们执行前分配一个唯一的进程id.
当然, 他不只是分配了进程id, 操作系统背后还会记录各个进程的其他信息, 例如:
- 还会记录 给这个进程分配了多少内存
- 这些进程正在使用哪些IO设备.
- 进程正在使用哪些文件
- CPU使用时间, 磁盘使用情况, 网络流量使用等等.
为了统一管理这些信息, 操作系统会将各个进程的这些信息, 组织成为一种数据结构, PCB中, PCB也就是process control block, 中文翻译为进程控制块.
PCB是操作系统级别的, 也就是说PCB是由操作系统管理的信息, 进程本身的指令集合和运行时候产生的数据才是自己的, 如下:
- 程序段: 也就是我们所说的 程序的代码 (最后还是会被编译成机器可以识别的指令序列)
- 数据段: 也就是一个程序在运行期间产生的各种数据(例如 程序中定义的变量, 我们需要再内存中给他分配空间)
接下来看看一个程序的运行过程
我们在学习c语言的时候肯定知道, 我们写好c语言的代码之后, 但是计算机是不认识这个代码的, 我们需要将其翻译成机器可以识别的二进制指令(一条高级语言代码翻译成机器指令可能会有多条) .
来源: RGSS 中执行机器码 - 知乎
首先我们在编写程序的时候, 肯定会写这样的一些代码, 如int a = 10; a++; 之类的代码, 通过编译和链接,最终形成一个.exe的可执行文件,这个可执行文件是存放在硬盘中的。它里面内容就是机器可以识别的指令序列。
如果要运行这个可执行文件,就需要将其加载到内存中去(这里啰嗦一句,这个过程就好比如你有一块电池,你想要用它,那么你就需要将其装载到遥控器中去使用它是一个道理):
在加载到内存中的时候,操作系统会为改进程创建对应的PCB来存放进程的信息,然后将其指令序列读取到内存,(除此之外,在执行指令序列的时候,可能会产生额外的数据,例如我们创建的变量int i = 10;)然后cpu就从内存中读取这些指令来执行。
完整的过程如下:
进程的组成
进程的组成既是:
- PCB : 存储进程的信息
- 程序段: 程序的指令序列
- 数据段:进程在运行过程中 产生的各种数据
进程是系统进行资源分配和调度的一个独立单位,PCB是进程存在的唯一标志。
进程的状态和转换
进程的状态
我们首先来看看进程的状态有哪些(你可以类比一下java里面线程的状态有哪些)。
从上面进程的概念中, 我们得知:
- 首先 磁盘或者硬盘中的一个可执行文件,需要先被加载到内存中去
- 然后操作系统为其创建PCB,然后分配内存空间并初始化PCB,这个创建进程的状态就称为创建态。
- 这个时候,进程就创建完毕,此后该进程就进入就绪态,等待cpu来运行其指令
- cpu空闲的时候,就会为这些在就绪态的进程,让它上cpu运行
- 此时的进程就为运行态 (正在执行指令序列)
- 但是也不会一直都让一个进程在cpu上一直运行,如果一个进程在运行态的某个时刻,需要某个系统资源,但是这个资源正在被其他进程使用,但是当前进程没有这个资源又运行不了,那么在继续呆在cpu又什么都不干,操作系统就认为它在消耗cpu资源,就让他下去了,并让新的进程上cpu,此时下去的进程就为阻塞态
- 这个时候,如果这个进程需要的系统资源被释放了,那么这个进程又可以重新上cpu执行了,上了cpu之后,又重新变为运行态,直到指令集合执行完毕,此时该进程会发起exit系统调用,也就是请求操作系统终止当前进程。此时进程进入终止态,操作系统会让起下cpu,并回收资源。
省流: 可执行文件,创建态,就绪态,运行态/ 阻塞态,终止态。
进程状态的转换
进程的组织方式
上面我们了解了, 进程的几个状态, 这些状态在其pcb中 会存在一个变量来表示进程的当前的状态, 例如1表示创建态. 2表示就绪态, 3表示运行态 等等, 然后操作系统将各个进程的pcb管理起来, 如何将进程的pcb组织起来? 有两种方式.
链式存储:
- 对于状态为创建态的进程, 我们使用一个指针来指向他, 然后如果后续还有进程处于创建态的时候, 就将其插入到这个指针所维护的创建态进程链表中.
- 对于就绪进程和阻塞队列也是如此.
- 其中阻塞队列的链表可能会因为阻塞的原因不同而分为多个不同类型的阻塞队列
索引存储:
进程控制
实习进程的状态转换
进程的控制主要是对系统中所有的进程实施有效的管理, 它有创建新进程, 撤销已有进程, 实现进程状态转换等功能.
如何实现进程控制
操作系统中使用原语来实现对进程的控制, 原语 就是一种特殊的程序, 它的执行具有原子性, 也就是说, 这段程序的运行必须一气呵成, 不可以中断.
为什么进程控制的过程需要一气呵成?
我们知道, 进程的PCB中存在一个变量state来表示进程当前所处的状态, 然后操作系统通过链式方法来管理这些PCB的state, 如下:
现在有这样一个案例, 现在有一个进程处于阻塞态, 它等待被占用的资源被释放, 此时它的state为阻塞态, 如果这个资源被释放之后, 那么他就应该从阻塞态到就绪态.
此时应该要经历这些东西:
- 该进程的在阻塞队列中的结点的state值应该变为 "就绪态" 所对应的值
- 然后将此结点从阻塞队列 移出, 然后搬入就绪队列中.
这两步缺一不可, 这里考虑 他们其中一个步骤失效
- 首先在执行第一步的时候, 也就是将这个想要进入就绪态的进程的pcb的state设置为 "就绪态" 所对应的值, 但是设置完成之后, 就收到了操作系统的中断信号, 此时它的state为就绪态, 但是 目前还是在阻塞队列中, 这就不符合状态组织的逻辑
所以需要使用原语来让其一气呵成.
进程控制的实现
原语的执行具有原子性, 执行期间不能被中断, 可以使用 关中断指令 和 开中断指令 这两个特权指令来实现
cpu在运行的时候, 会一条一条的执行指令, 如下:
假如如果执行到 指令2的时候, cpu收到了外部的中断信号, 这个时候, cpu就会暂停执行当前的指令, 转而运行 中断处理程序, 等这个终端处理程序处理完成之后, 就继续往下面执行指令.
那执行到关中断时候, 会发生什么呢?
是的. 早cpu执行了关中断指令 之后, 就不再检查中断信号 , 知道执行到开中断指令之后, 才会执行检查. 执行了关中断指令之后, 然后执行指令a和指令b, 一直到执行开中断指令之后才会检查中断指令.
如果这个指令a和指令b中间执行过程中出现了中断信号, 在执行完开中断指令之后, cpu才会去运行对应的中断处理程序, 此时指令a和指令b全部已经执行完了, 就保证了其执行的原子性./
为什么关中断和开中断是特权指令? 特权指令只能由内核程序执行, 内核程序运行在内核态, 比较安全, 这个时候如果可以给用户态的程序执行开中断和关中断指令的话, 那么其就会一只霸占cpu浪费资源, 影响系统安全/
原语类型和创建过程
前面提到我们使用原语来保证指令执行的原子性, 那么原语有哪些类型呢?
- 进程的创建
- 创建原语
- 申请空白PCB
- 为新进程分配所需资源
- 初始化PCB
- 将PCB插入到就需队列
- 引起进程创建的事件 的例子
- 用户登录 : 分时系统中,用户登录成功,系统会建立为其建立一个新的进程
- 作业调度 : 多道批处理系统中,有新的作业放入内存时,会为其建立一个新的进程
- 提供服务 : 用户向操作系统提出某些请求时,会新建一个进程处理该请求
- 应用请求 : 由用户进程主动请求创建一个子进程
- 创建原语
- 进程的终止
- 撤销原语:
- 从PCB集合中找到其终止进程的PCB(找到其PCB)
- 如果程序正在运行, 那么久立刻剥夺其cpu执行权, 将cpu资源分配给其他进程.
- 同时操作系统在杀死一个进程的时候, 也会同时杀死其子进程(父子进程关系为一种树形结构)
- 将该进程拥有的资源归还给父进程或者是操作系统
- 然后删除PCB
- 案例 :
- 正常结束 : 进程正常结束, 自己请求终止
- 异常结束 : 整数除以0, 非法使用特权指令, 被操作系统强行杀死
- 外界干预 : 用户手动杀死进程
- 撤销原语:
- 进程的阻塞和唤醒:
- 进程的切换:
- 切换原语:
- 将运行环境信息存入PCB
- PCB移入对应的队列
- 选择另外一个进程执行, 更新其PCB
- 根据PCB恢复这个进程所需的环境
- 案例:
- 进程时间片到了, 就会下处理机, 让另外一个进程上cpu运行
- 有更高优先级的进程到达, cpu优先执行这个高优先级进程, 那么就会引起进程切换
- 当前进程主动阻塞之后, 操作系统认为其长时间浪费系统资源, 让其下cpu, 让其他进程上cpu运行
- 当前进程异常终止, 或者是主动请求终止, 这个进程都会下cpu, 此时会有一个新的进程上cpu运行.
- 切换原语:
进程间通信
什么是进程间通信
一个正在运行的操作系统, 难免会有很多个进程在同时并行, 他们之间也难免需要进行一些通信, 这种通信, 就叫做进程间通信(IPC), 指的是两个进程之间产生了数据交互.
便于理解, 这里列举一个例子:
我们在使用微博的时候看到一篇文章, 突然我们很像分享给朋友看, 于是就点了右上角的 ... 然后选择了微信分享:
然后:
那么这个过程, 就从微博, 将微信唤起, 然后将微博文章的链接分享到了微信
这个操作是需要操作系统的支持的.
为什么进程通信需要操作系统支持?
进程是分配系统资源的最基本单位(包括内存地址空间), 因此各个进程都拥有相互独立的地址内存空间. 所以进程a在运行的过程中是不能访问进程b的空间, 那么如何进行进程间通信?
那就需要操作系统来帮忙了.
那么操作系统提供了几种让进程可以相互进行数据交换的方式 :
共享存储
- 基于存储区的共享
- 基于数据结构的共享
一个进程申请一个共享存储区, 其他进程想要和此进程进行通信的时候, 只需要将数据写入这个内存共享区域即可:
然后另外一个进程就可以读取另外一个进程写入的数据.
但是会出现一个问题:
两个进程同时往共享存储区域中的相同的地方进行写入, 那么就有可能会发生一个进程写入的数据将另外一个进程写入的数据给覆盖掉.
那么就需要保证, 他们对这个共享存储区域的写操作是互斥的, 互斥的对象是进程对象. 比如说, 进程a在访问这个共享存储区的时候, 其他的进程是无法向这个共享内存区域中写入数据的.
那么如何进行互斥操作? 操作系统提供了很多同步互斥工具: p,v操作
基于存储区的共享:操作系统在内存中划出一块共享存储区,数据的形式、存放位置都由通信进程控制,而不是操作系统。这种共享方式速度很快,是一种高级通信方式。
当然我们也可以在共享区域的前提下, 将内存空间进行结构化, 例如共享区域只能存放一个长度为10的数组, 这种方式就叫做基于数据结构的共享 , 但是通信的速度就变慢了, 因此基于数据结构的共享是一种低级通信.
总结:
- 基于存储区的共享: 速度快, 灵活性高, 高级通信
- 基于数据结构的共享: 速度慢. 灵活性低, 低级通信
消息传递
进程间的数据交换以格式化的消息(Message)为单位。进程通过操作系统提供的"发送消息/接收消息"两个原语进行数据交换。
格式化的消息由两部分组成:
- 消息头
- 消息体
消息的传递又分为两种:
- 直接通信方式 : 进程要指明接收进程的id
- 间接通信方式 : 通过"信箱" 间接的进行通信(又称信箱通信)
下面来看看直接通信方式是如何通信的:
- 每个进程的PCB中存在着一个类似于消息队列的结构, 用来接收其他进程的消息
- 进程A将自己要发送的信息的消息体和消息头, 然后通过操作系统提供的发送原语, 并指明消息的接受者,
- 操作系统就会将消息挂在对应进程的消息队列中
- 然后假设是进程B接收到这个消息, 那么进程B使用接收原语, 指明要接受哪个进程的消息, 操作系统就会到进程B对应的PCB中去寻找这条消息. 然后将消息复制到B的地址空间中
接下来是间接通信方式:
- 进程A和进程B都申请了一块空间, 用来当做接收消息的信箱
- 进程A在自己的地址空间中填充好消息体和消息头
- 然后进程A使用发送原语来指明要发送到哪个信箱
- 假设此时进程A指明要发送给信箱A发送消息
- 然后进程B指明要在信箱A中接受这条消息, 那么就完成了消息的传递
管道通信
这个懂得都懂, 可以简单理解为一个队列.
其实这个"管道" 是一个特殊的共享文件, 又名为pipe, 也就是在内存中开辟了一个大小固定的内存缓冲区, 然后两个进程可以往其中读写数据, 但是遵循一个先进先出原则.
那这个和共享内存有什么区别? 共享内存是不限制进程在哪块位置读和写, 不可避免的会存在覆盖写的问题, 但是管道, 也就是这个队列, 需要遵循队列的特性,
如果要实现两个进程的自由读写, 那么就需要提供两个队列, 因为队列是单向读取的. 各个进程还需要互斥的访问信道, 不然会出现类似于一个小消息被消费两次的情况
当管道满了之后, 那么如果继续写的话, 那么就会阻塞, 直到有数据被消费了之后, 那么就会唤醒写进程, 相同, 如果读的时候为空, 那么读进程就会被阻塞, 直到有数据写入.