操作系统之进程管理——进程概览
1. 进程的概念、特征和状态转换
- 推荐阅读:进程详解
- [定义]进程:进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。[理解]进程类似一个实例化的程序,是“执行中的程序”,是一个过程。
- 进程实体:由程序段、相关数据段和PCB(进程控制块,由链表实现,进程的唯一标识,进程创建时,OS为其生成一个PCB,进程终止时回收PCB)三部分构成。
- 进程的特征:动态性、并发性、独立性、异步性、结构性:
- 动态性:进程实体不运行就不叫进程,一个没有被调用的进程实体也不叫进程。具有着创建、活动、暂停、终止等过程。具有一定的生命周期,所以是动态的。
- 并发性:引入进程就是为了使程序能够与其他进程的程序并发执行,用以提高资源利用率。
- 独立性:进程实体拥有着独立的资源(程序段和数据段),是能够独立地接受调度并独立运行的基本单位(因为PCB的存在)。
- 异步性:由于进程的相互制约,使进程具有执行的间断性,即进程按照各自独立地、不可预知的速度向前推进。但异步会导致执行结果的不可再现性(类似数据库的一致性),为此,OS必须配置相应的进程同步机制(为解决访问顺序和共享变量的冲突问题,推荐阅读:进程间同步机制)。
- 结构性:一个进程配置一个PCB进行描述,按进程实体定义的三部分组成。
- 进程的状态转换:三种基本状态(就绪状态、执行状态、阻塞/等待状态),某些系统的附加状态(挂起状态),另外两种常见状态(创建状态、终止状态)。
- 就绪状态:进程拥有除了CPU外的一切所需资源;
- 执行状态:运行在CPU上,一般单CPU系统某一时刻下最多运行一个进程;
- 阻塞/等待状态:进程在等待某一事件(不包括CPU)而暂停运行。
- 挂起状态:出于种种原因,我们让进程处于静止状态(即让进程不能立即执行),OS把这些进程换到磁盘中的挂起队列,推荐阅读:进程的挂起状态详细分析。
- 创建状态:正在被创建,尚未进入就绪状态。创建步骤:申请一个空白PCB,向PCB中写入控制信息,然后OS为进程分配资源,最后转入就绪状态。
- 终止状态:进程由于正常结束/其他原因中断运行,OS设置进程为结束状态,然后进一步处理资源释放和回收工作。
- [注]运行状态 -> 阻塞状态是进程的主动过程,但由阻塞状态 -> 就绪状态是一个被动的行为,需要其他相关进程的协助(见本博客第4节内容)。
事件 | 导致进程挂起的事件说明 |
---|---|
交换 | 操作系统需要释放足够的内存空间,以调入并执行处于就绪状态的进程 |
其他OS原因 | 操作系统可能挂起后台进程或工具程序进程,或者被怀疑导致问题的进程 |
交互式用户请求 | 用户可能希望挂起一个程序的执行,目的是为了调试或与一个资源的使用进行连接 |
定时 | 一个进程可能会周期性地执行(例如记账或系统监视进程),而且可能在等待下一个时间间隔时被挂起 |
父进程请求 | 父进程可能会希望挂起后代进程的执行,以检查或修改挂起的进程,或者协调不同后代进程之间的行为 |
2. 进程的创建
在OS中,用户登录、作业调度、系统提供服务、用户程序的应用请求都会引起进程的创建。
以Linux为例,说明创建步骤:
- ① 首先在内存中为新进程创建一个task_struct结构(Linux下对PCB定义的结构体);
- ② 然后将父进程的task_struct内容复制其中,再修改部分数据。
- ③ 分配新的内核堆栈、新的PID,再将task_struct这个node添加到PCB链表中。
- 进程的以上创建步骤是单个原语,不允许分割,其他的撤销已有进程、实现进程状态的转换等都是原语。
- 因为绝大多数信息是从父进程拷贝/克隆来的,系统有必要记录这种亲属关系,使进程之间的协作更加方便(例如kill信号或通信信号)。初始化进程init中的task_struct结构是进程树的根(init进程是所有进程的祖先进程)。推荐阅读:Linux - PCB之task_struct结构体
- 子进程刚开始时,内核并没有为它分配物理内存,而是以只读的方式共享父进程内存,只有当子进程写时,才复制,即“copy on write”。
3. 进程的终止
- 终止的事件有:正常结束(main中的return,调exit,调_exit或_EXIT)、异常结束(存储区越界、保护错、非法指令、特权指令错、I/O故障等,调abort等)、外界干预(人为干预、OS干预、父进程干预)
- 终止步骤:
- 根据被终止进程的标识符,检索PCB,读出该进程的状态;
- 若被终止进程处于执行状态,立即终止该进程的执行,将CPU分配给其他进程;
- 若该进程还有子进程,应将所有子进程终止;
- 将该进程所拥有的所有资源,归还给父进程或OS;
- 将PCB从链表中删除;
4. 进程的阻塞和唤醒
- 重点:只有获得CPU的进程才能够转为阻塞状态,这种转换是主动的,方式为执行阻塞原语(Block)。
- 当资源或数据等条件得到满足后,由合作进程或其他相关进程执行唤醒原语(Wakeup),唤醒该被阻塞的进程,进入就绪队列。
- 引起阻塞和唤醒的事件:
- 向系统申请资源时失败。如申请打印机,但打印机被其他进程占用。
- 等待某种操作。如进程A启动了某I/O,只有等完成该I/O任务后进程A才能执行,则进程A启动I/O后自动阻塞。
- 新数据尚未到达。如相互合作的进程,没等到另一个进程的数据之前,先进入阻塞状态。
- 等待新任务的到达。进程完成了自己的任务后将自己阻塞起来,等待新任务到来才将其唤醒。
- Block的执行步骤:
- 找到该进程的PID对应的PCB;
- 查看PCB中进程的状态,若是运行状态,则保护现场,将状态改为阻塞状态,停止运行;
- 把该PCB插入到相应事件的阻塞队列中。
- Wakeup的执行步骤:
- 在该事件的等待队列中找到相应进程的PCB;
- 将其从等待队列中移出,并置为就绪状态;
- 将PCB插入就绪队列中,等待调度程序调度。
5. 进程的切换
- 推荐阅读:进程概念,进程切换,上下文切换,虚拟地址空间
- 进程的切换步骤:
- OS保存处理的上下文到PCB,包括PC(program counter)、PSW(program status word)及其他寄存器,保存到tast_struct中的thread结构体中(把寄存器中的值赋给thread中的各变量);
- 更新PCB信息;
- 把进程PCB移入相应的队列;
- 选择另一个进程执行,并更新其PCB;
- 更新内存管理的数据结构(如地址页表、进程表、进程打开的文件表等);
- 恢复CPU上下文。
- 切换发生的时机:
- 阻塞式系统调用、虚拟地址异常。导致进入等待态。
- 时间片中断、I/O中断后更改优先级进程。导致进入就绪态。
- 终止用系统调用、不能继续执行的异常。导致进入终止态。
- 虽然进程切换一定发生在中断/异常/系统调用的处理过程中,但并不意味所有的中断/异常都会引起进程切换(有些时候OS处理完中断,会立即恢复原进程的处理)。
- 进程切换只会发生在内核态。
- 一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文
- 用户级上下文: 正文、数据、用户堆栈以及共享存储区;
- 寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
- 系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈
系统调用进行的是模式切换(mode switch)。模式切换与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是进程寄存器上下文的切换。
当进程调用系统调用或者发生中断时,CPU从用户态切换成内核态,此时,无论是系统调用程序还是中断服务程序,都处于当前进程的上下文中,并没有发生进程上下文切换。
当系统调用或中断处理程序返回时,CPU要从内核态切换回用户态,此时会执行操作系统的调用程序。如果发现就绪队列中有比当前进程更高的优先级的进程,则会发生进程切换:当前进程信息被保存,切换到就绪队列中的那个高优先级进程;否则,直接返回当前进程的用户模式,不会发生上下文切换
当进行系统调用的时候,当前进程的时间片就停止了,由内核去完成系统调用,当我们系统调用返回的时候,时间片就可以重新开始计时,无论怎么样,系统调用以后,一定是有时间让内核把信息,返回给用户,系统调用的结果会保存在eax寄存器中。
如果系统刚调用完毕,准备返回用户空间,就在此时,发生了进程切换,那么别的进程就会改掉eax的值,那么当我下次进行恢复的时候,还得重新调用系统调用,这样子的话,对于系统的资源就太浪费了;所以说进行系统调用的返回值一定是返回给用户的,当用户把eax寄存器的值保存起来以后,标志着系统调用完毕,接下来切不切换进程就没有啥影响了。
6. 进程间的通信
PV操作(P、V原语,属于信号量机制)是通过硬件实现的低级通信方式;缺点:效率低、通信对用户不透明。高级通信方式有三种:共享存储、消息传递和管道通信(优点:隐藏了实现细节):
共享存储:使用同步互斥工具,对一块可直接访问的共享空间进行读写控制,要通过特殊的系统调用来实现(因为一般进程空间是独立地,不会访问其他进程的空间)。
消息传递:以格式化的消息为单位(计网中称为报文)进行传递。系统提供了“发送消息(Send)”和“接受消息(Receive)”两个原语,应用广泛,能很好地支持多处理机系统、分布式系统和计算机网络。
- 直接通信方式:发送进程将消息挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息;
- 间接通信方式,又称为信箱通信方式:发送进程将消息发送到某一个中间实体中,接收进程从中间实体(被称为信箱)取得消息(消息在信箱中可以安全的保存,只允许核准的目标用户随时读取)。采用间接通信方式可实现实时通信,又可实现非实时通信。分为三种信箱:
- 私有信箱。由用户创建信箱,并作为该进程的一部分,其他用户只能向该信箱发消息,而无法取消息。可采用单向通信链路的信箱来实现,当该信箱的进程结束时,进程PCB消失,信箱随之消失。
- 公用信箱。由OS创建,提供给OS核准的用户使用,采用双向通信链路的信箱来实现。通常公用信箱在系统运行期间始终存在。
- 共享信箱。创建时或创建后指明为共享的,同时需指出共享进程(用户)的名字。
管道通信:用于连接一个读进程和一个写进程以实现通信的一个共享文件,即pipe文件。推荐阅读:【Linux0.11 内核源码剖析】进程间通信——管道(pipe)。管道机制必须提供三方面能力:
- ① 互斥。当一个进程正在对pipe进行读/写操作时,另一个进程必须等待。
- ② 同步。当写(输入)进程把一定数量数据写入pipe后,便去睡眠等待,直到读(输出)进程取走数据后,再把它唤醒。当读进程读到一空pipe时,也应睡眠等待,直至写进程将数据写入管道后,才将它唤醒。
- ③ 确定对方的存在。只有确定对方已存在时,才能进行通信。
- 管道有一个固定的缓冲区,所以需要限制管道的大小;
- 管道读数据是一次性操作,一旦数据被读取,它就要从管道中被抛弃,释放空间以便写更多的数据;
- 管道只能采用半双工通信,某一时刻只能单向传输。若要实现父子通信,必须定义两个管道。