操作系统中最核心的概念就是进程:这是对正在运行程序的一个抽象。本文主要从进程组成、组织方式、特征、管理等角度进行阐述。

进程模型

一个进程就是一个正在执行程序的实例,包括程序计数器、寄存器和变量的当前值。程序段、数据段、PCB三部分组成了进程实体(进程映象)。一般情况下,我们把进程实体简称为进程,所谓创建进程,实质上是创建进程实体中的PCB;而撤销进程,本质上是撤销进程实体中的PCB,PCB是进程存在的唯一标志。进程是进程实体的运行过程,是系统进行资源分配和调度的独立单元。

  • PCB组成
进程描述信息 进程标识符PID;
用户标识符UID
进程控制与管理信息 进程当前状态;
进程优先级
资源分配清单 程序段指针;
数据段指针;
键盘;
鼠标
处理器相关信息 各种寄存器值

进程的创建

进程的创建是进程的开始,有四种主要事件会导致进程的创建:

  • 系统初始化
  • 正在运行的程序执行了创建进程的系统调用
  • 用户请求创建一个新进程
  • 一个批处理作业的初始化

创建的流程如下。传统的fork()系统调用直接把所有的资源复制给新创建的进程。Linux的fork采用写时复制(copy-on-write)页实现,写时复制是一种可以可以推迟甚至免除复制数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个复制。

fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。

stateDiagram 进程1 --> 子进程1 : 1.fork 进程1 --> 子进程1: 2.exec

其中exec负责读取可执行文件并将载入地址空间开始运行。

Linux实际是通过clone()系统调用实现fork(),涉及到fork()vfork()__clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_task()do_task()完成了创建中的大部分工作,do_task()调用copy_process()函数,然后让进程运行,copy_process()的工作流程如下

stateDiagram fork() --> 创建一个内核栈、thread_info结构和task_struct : dup_task_struct() 创建一个内核栈、thread_info结构和task_struct --> 检查并确保新创建子进程后的进程号是否超出限制 检查并确保新创建子进程后的进程号是否超出限制 --> 子进程修改自己与父进程区别开来 : 进程描述符内许多称为被清0或设置为初始值 子进程修改自己与父进程区别开来 --> 子进程状态设置为`task_uninterruptible`,保证不会投入运行 子进程状态设置为`task_uninterruptible`,保证不会投入运行 --> `copy_process()`调用`copy_flags()`以更新task_struct和flags成员 `copy_process()`调用`copy_flags()`以更新task_struct和flags成员 --> 调用alloc_pid为新进程分配一个有效的PID 调用alloc_pid为新进程分配一个有效的PID --> 根据传递给clone()的参数标志,`copy_process()`处理相关内容 根据传递给clone()的参数标志,`copy_process()`处理相关内容 --> `copy_process()`做扫尾工作并返回一个子进程指针

进程的终止

当一个进程终止时,内核必须释放他所占有的所有资源并且通知父进程。进程的终止发生在调用exit()系统调用时,该调用的大部分都靠do_exit()来完成。

进程的层次结构

每个进程都有一个父进程,初始的内核级进程通常是自己的父进程。当子进程终止时,父进程得到通知并取得子进程的退出状态。

Unix/Linux使用进程号(PID)用以标识系统中的某个进程,PID是一个正数,而进程的父进程为PPID。下面为获取pid和ppid的方法。

1
2
3
4
5
#include<unistd.h>

pid_t getpid(void);

pid_t getppid(void);

每个进程的父进程号属性反映了系统上所有进程间的树状关系。每个进程的父进程又有自己的父进程,以此类推,回溯到1号进程————init进程,即为所有进程的始祖。使用pstree(1)命令可以查看家族树

当子进程的父进程终止,则子进程就会变成孤儿,init进程随即收养该进程,子进程的后续对getppid()调用将返回进程号1.下图为进程之间的关系。

stateDiagram 初始进程init --> 子进程1 : fork 子进程1 --> 孙进程2: fork 子进程1 --> 孙进程3 : fork

进程的状态

进程被划分为三种基本状态,运行态(running)、就绪态(ready)、阻塞态(waiting/blocked,又称为等待态)

  • 运行态:占用CPU,并在CPU上运行
  • 就绪态:已经具备运行条件,但是由于没有空间CPU,而暂时不能运行
  • 阻塞态:因等待某一事件而暂时不能运行

除了基本状态以外,还有创建态(New)、终止态(Terminated)

  • 创建态:进程正在被创建,操作系统被进程分配资源、初始化PCB
  • 终止态:进程正在从系统中撤销。操作系统会回收进程拥有的资源、撤销PCB

下图为进程五种状态之间的转换

stateDiagram 创建态 --> 就绪态 : 完成创建进程的一系列工作 就绪态 --> 运行态 : 进程被调度 运行态 --> 就绪态 : 时间片到,或处理器被抢占 运行态 --> 阻塞态 : 请求等待 阻塞态 --> 就绪态 : 等待的事件完成 运行态 --> 终止态

进程的组织方式

  • 链接方式:按照进程状态将PCB分为多个队列(就绪队列、阻塞队列);操作系统持有各个队列的指针
  • 索引方式:根据进程状态不同,建立几张索引表;操作系统持有指向各个索引表的指针

进程特征

  • 动态性:进程是程序的一次执行过程,是动态地产生、变化和消亡的
  • 并发性:内存中有多个进程实体,各进程可并发执行
  • 独立性:进程是能独立运行,独立获得资源、独立接受调度的基本单位
  • 异步性:各个进程按各自独立的,不可预知的速度向前推进,操作系统要提供“进程同步机制”来解决异步问题
  • 结构性:每个进程都会配置一个PCB,结构上看,进程由程序段、数据段、PCB组成。

进程控制

用原语实现进程控制。原语的特点是执行期间不允许中断,只能一气呵成,这种不可被终端的操作即为原子操作。原语采用的是关中断指令开中断指令实现,原语对进程状态的转换做的事情如下三类

  • 更新PCB的信息
    • 所有进程控制原语一定都会修改进程状态标志
    • 剥夺当前运行进程的CPU使用权必然需要保存其运行环境
    • 某进程开始运行前必然要回复其运行环境
  • 将PCB插入合适的队列
  • 分配与回收资源

进程创建原语流程如下图所示,引起进程创建的事件有用户登录作业调度提供服务应用请求

stateDiagram direction LR 申请空白PCB --> 为新进程分配所需资源 为新进程分配所需资源 --> 初始化PCB 初始化PCB --> 将PCB插入就绪队列

进程撤销原语流程如下图,引起进程终止的事件有正常结束异常结束外接干预

stateDiagram direction LR 从PCB集合中找到终止进程的PCB --> 若进程正在运行,立即剥夺CPU,将CPU分配给其他进程 若进程正在运行,立即剥夺CPU,将CPU分配给其他进程 --> 终止其所有子进程 终止其所有子进程 --> 将该进程拥有的所有资源归还给父进程或操作系统 将该进程拥有的所有资源归还给父进程或操作系统 --> 删除PCB

进程阻塞,引起进程阻塞的事件分为需要等待系统分配某种资源需要等待相互合作的其他进程完成工作

stateDiagram direction LR 找到要阻塞的进程对应的PCB --> 保护进程运行现场,将PCB状态信息设置为“阻塞态”,暂时停止进程运行 保护进程运行现场,将PCB状态信息设置为“阻塞态”,暂时停止进程运行 --> 将PCB插入相应事件的等待队列

进程唤醒如下所示,引起进程唤醒的事件为等待事件发生

stateDiagram direction LR 在时间等待队列中找到PCB --> 将PCB从等待队列转移,设置进程为就绪态 将PCB从等待队列转移,设置进程为就绪态 --> 将PCB插入就绪队列,等待被调度

进程的切换原语如下,引起进程切换的事件有当前进程时间片到有更高优先级的进程到达更前进程主动阻塞当前进程终止

stateDiagram direction LR 运行环境信息存入PCB --> PCB移入相应队列 PCB移入相应队列 --> 选择另一个进程执行,并更新其PCB 选择另一个进程执行,并更新其PCB --> 根据PCB恢复新进程所需的运行环境