07-进程管理
07-进程管理
一、进程(process)的描述
1.1 进程定义
进程:一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程
1.2 进程的组成
一个进程应该包括
- 程序的代码
- 程序处理的数据
- 程序计数器中的值,指示下一条将运行的指令
- 一组通用寄存器的当前值、堆、栈
- 一组系统资源(如打开的文件)
总之,进程包含了正在运行的一个程序的所有状态信息
进程与程序的联系
- 程序是产生进程的基础
- 程序的每次运行构成不同的进程
- 进程是程序功能的体现
- 通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包括多个程序
进程与程序的区别
- 进程是动态的,程序是静态的:程序是有序代码的集合;进程是程序的执行,进程有和心态、用户态
- 进程是暂时的,程序时永久的:进程是一个状态变化的过程,程序可长久保存
- 进程与程序的组成不同:进程的组成包括程序、数据和进程控制块(即进程状态信息)
进程和程序的关系(类比)
有一个计算机科学家,想亲手给女儿做一个生日蛋糕。所以他就找了一本有关做蛋糕的食谱,买了一些原料,面粉,鸡蛋,糖,香料等等,然后边看边学边做
食谱=程序;科学家=CPU;原料=数据;做蛋糕=进程;
这时小儿子哭着跑进来了,说手被蜜蜂蛰了。教授只好把蛋糕先放在一遍,他在食谱上做了个标记,把状态信息记录了起来。
然后又去找了一本医疗手册,查到了相关的内容,按照上面的指令一步步地执行。当伤口处理完之后,又回到厨房继续做蛋糕
cpu从一个进程(做蛋糕)切换到另一个进程(医疗救护)
1.3 进程的特点
- 动态性:可动态地创建、结束进程
- 并发性:进程可以被独立调度并占用处理机运行(什么是并发?在一段时间内有多个进程在运行。什么是并行?在某个时刻有多个进程在运行,并行只有在有多个CPU的时候才能实现)
- 独立性:不同进程的工作不相互影响
- 制约性:因访问共享数据/资源或进程间同步而产生制约
问题:如果你要设计一个OS, 怎么样来实现其中的进程管理机制?
程序 = 算法 + 数据结构
描述进程的数据解雇:进程控制块(Process Control Block, PCB)
操作系统为每个进程都维护了一个PCB,用来保存与该进程有关的各种状态信息
1.4 进程控制结构
进程控制块:操作系统管理控制进程运行所用的信息集合。操作系统用PCB来描述进程的基本情况以及运行变化的过程,PCB是进程存在的唯一标志。
使用进程控制块
- 进程的创建:为该进程生成一个PCB
- 进程的终止:回收它的PCB
- 进程的组织管理:通过对PCB的组织管理来实现
PCB具体包含什么信息?如何组织的?进程的状态转换?
PCB含有以下三大类信息
(一)进程标识信息。如本进程的标识,本进程的产生者标识(父进程标识);用户标识
(二)处理机状态信息保存区。保存进程的运行现场信息:
用户可见寄存器:用户程序可以使用的数据,地址等寄存器
控制和状态寄存器:如程序计数器(PC),程序状态字(PSW)
栈指针:过程调用/系统调用/中断处理和返回时需要用到它
(三)进程控制信息
调度和状态信息:用于操作系统调度进程并占用处理机使用
进程间通信信息:为支持进程间的与通信相关的各种标识、信号、信件等,这些信息存在接收方的进程控制块中
存储管理信息:包含有指向本进程映像存储空间的数据结构
进程所用资源:说明由进程打开,使用的系统资源,如打开的文件等
有关数据结构连接信息:进程可以连接到一个进程队列中,或连接到相关的其他进程的PCB
PCB的组织方式
链表:同一状态的进程其PCB成一链表,多个状态对应多个不同的链表(各状态的进程形成不同的链表:就绪链表,阻塞链表)
索引表:同一状态的进程归入一个index表(由index指向PCB), 多个状态对应多个不同的index表)(各状态的进程形成不同的索引表:就绪索引表,阻塞索引表)
二、进程状态(State)
1. 进程的生命周期管理
进程创建
引起进程创建的3个主要事件:
系统初始化时
用户请求创建一个新进程
正在运行的进程执行了创建进程的系统调用
进程运行
内核选择一个就绪的进程,让它占用处理机并执行
为何选择?
如何选择?
涉及到进程调度相关的知识
进程等待
在以下情况下,进程等待(阻塞):
- 请求并等待系统服务,无法马上完成
- 启动某种操作,无法马上完成
- 需要的数据没有到达
进程只能自己阻塞自己,因为只有进程自身才能知道何时需要等待某种事件的发生
进程唤醒
唤醒进程的原因:
- 被阻塞进程需要的资源可被满足
- 被阻塞进程等待的事件到达
- 将该进程的PCB插入到就绪队列
进程只能被别的进程或操作系统唤醒
进程结束
在以下四种情形下,进程结束:
- 正常退出 (自愿的)
- 错误退出(资源的)
- 致命错误(强制性的)
- 被其他进程所杀(强制性的)
2. 进程状态变化模型
进程的三种基本状态:
进程在生命结束前处于且仅处于三种基本状态之一
不同系统设置的进程状态数目不相同
运行状态(Running): 当一个进程正在处理机上运行时
就绪状态(Ready): 一个进程获得了除处理机之外的一切所需资源,一旦得到处理机即可运行
等待状态(又称阻塞状态Blocked):一个进程正在等待某一事件而暂停运行时。如等待某资源,等待输入/输出完成
进程其他的基本状态:
创建状态(New): 一个
结束状态(Exit): 一个进程正在从系统中消失的状态,这是因为进程结束或由于其他原因所导致的
可能的状态变化:
NULL->New:一个新进程被产生出来执行一个程序
New->Ready:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态。这个状态变化的时间是否会持续很久?不会,因为仅仅是PCB 进程控制块的初始化过程,会比较快
Ready->Running: 处于就绪状态的进程被进程调度系统选中后,就分配到处理机上来运行
Running->Exit: 当进程表示它已经完成或者因出错,当前运行进程会由操作体统作结束处理
Running->Ready: 处于运行状态的进程在其运行过程中,由于分配给它的处理机时间片用完而让出处理机。这个动作是由操作系统完成的
Running->Blocked: 当进程请求某样东西且必须等待时
Blocking->Ready: 当进程要等待某事件到来时,它从阻塞状态变到就绪状态
3. 进程挂起模型
为什么会发生进程挂起?为了合理且充分地利用系统资源
进程在挂起状态时,意味着进程没有占用内存空间,处在挂起状态的进程映像在磁盘上
挂起状态
阻塞挂起状态(Blocked-suspend): 进程在外存并等待某时间的出现
就绪挂起状态(Ready-suspend):进程在外存,但只要进入内存,即可运行
与挂起相关的状态转换
挂起(Suspend): 把一个进程从内存转到外存;可能有一下几种情况:
- 阻塞到阻塞挂起:没有进程处于就绪状态或就绪进程要求更多内存资源时,会进行这种转换,以提交新进程或运行就绪进程
- 就绪到就绪挂起:当有高优先级阻塞进程(系统认为会很快就绪的进程)和低优先级就绪进程时,系统会选择挂起低优先级就绪进程
- 运行到就绪挂起:对抢先式分时系统,当有高优先级阻塞挂起进程因事件出现而进入就绪挂起时,系统可能会把运行进程转到就绪挂起状态
在外存时的状态转换: - 阻塞挂起到就绪挂起:当有阻塞挂起进程因相关事件出现时,系统会把阻塞挂起进程转换为就绪挂起进程。
解挂/激活(Activate):把一个进程从外存转到内存;可能有以下几种情况: - 就绪挂起到就绪:没有就绪进程或挂起就绪进程优先级高于就绪进程时,会进行这种转换
- 阻塞挂起到阻塞:当一个进程释放足够内存时,系统会把一个高优先级阻塞挂起进程(系统认为会很快出现所等待的事件)转换为阻塞状态
用进程的观点来看待OS: 用户胡进程、磁盘管理进程、终端进程……
以进程为基本结构的OS
最底层为CPU调度程序(包括中断处理等);
上面一层为一组各式各样的进程;
状态队列
- 由操作系统来维护一组队列,用来表示系统当中所有进程的当前状态;
- 不同的状态分别用不同的队列来表示(就绪队列、各种类型的阻塞队列);
- 每个进程的PCB都根据它的状态加入到相应的队列当中,当一个进程的状态发生变化时,它的PCB从一个状态队列中脱离出来,加入到另一个队列
小结
- 进程描述
进程定义
进程的特点和组成 - 进程状态
进程的生命周期管理
进程状态变化模型
进程挂起模型
三、线程(thread)
进程当中的一条执行流程
从两个方面来重新理解过程
- 从资源组合的角度:进程把一组相关的资源组合起来,构成了一个资源平台(环境),包括地址空间(代码段、数据段)、打开的文件等各种资源;
- 从运行的角度:代码在这个资源平台上的一条执行流程(线程)
线程 = 进程-共享资源
线程的优点:
- 一个进程中可以同时存在多个线程
- 各个线程之间可以并发地执行
- 各个线程之间可以共享地址空间和文件等资源
线程的缺点:
一个线程彭奎,会导致其所属进程的所有线程崩溃
进程和线程的比较
- 进程是资源分配单位,线程是CPU调度单位
- 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈
- 线程同样具有就绪、阻塞和执行三种基本状态,同样具有状态之间的转换关系
- 线程能减少并发执行的时间和空间开销
- 线程的创建时间比进程短
- 线程的终止时间比进程短
- 同一进程的线程切换时间比进程短
- 由于同一进程的各线程间共享内存和文件资源,可直接进行不通过内核的通信
线程的实现
主要有三种线程的实现方式:
用户线程:在用户空间实现;
POSIX Pthreads,Math C-threads,Solaris threads
内核线程:在内核中实现
Windows, Solaris, Linux
轻量级进程:在内核中实现,支持用户线程
Solaris(LightWeight Process)
用户线程
在用户空间实现的线程机制,它不依赖于操作系统的内核,由一组用户级的线程库函数来完成线程的管理,包括进程的创建、终止、同步和调度
- 由于用户线程的维护由相应进程来完成(通过线程库函数),不需要操作系统内核了解用户线程的存在,可用于不支持线程技术的多进程操作系统
- 每个进程都需要它自己私有的线程控制块(TCB)列表,用来跟踪记录它的各个线程的状态信息(PC、栈指针、寄存器),TCB由线程库函数来维护
- 用户线程的切换也是由线程库函数来完成,无需用户态、核心态切换、所以速度特别快
- 允许每个进程拥有自定义的线程调度算法
用户线程缺点
- 阻塞性的系统调用如何实现?如果一个线程发起系统调用而阻塞,则整个进程在等待
- 当一个线程开始运行后,除非它主动地交出CPU的使用权,否则它所在的进程当中的其他线程将无法运行
- 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会较慢
内核线程
是指在操作系统的内核当中实现的一种线程机制,由操作系统的内核来完成线程的创建、终止和管理
- 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息(PCB和TCB)
- 线程的创建、终止和切换都是通过系统调用、内核函数的方式来进行,由内核来完成,因此系统开销较大
- 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行
- 时间片分配给线程,多线程的进程获得更多的CPU时间
- Windows NT和Windows 2000/XP支持内核线程
轻量级进程(LightWeight Process)
它是内核支持的用户线程,一个进程可有一个或多个轻量级进程,每个轻量级进程由一个单独的内核线程来支持(Solaris/Linux)
上下文切换
停止当前运行进程(从运行状态改变成其他状态)并且调度其他进程(转变成运行状态)
- 必须在切换之前存储许多部分的进程上下文
- 必须能够在之后恢复他们,所以进程不能显示它曾经被暂停过
- 必须快速(上下文转换时非常频繁的)
需要存储什么上下文?
寄存器(PC,SP,...),CPU状态,...
一些时候可能会费时,所以我们应该尽可能避免
进程控制
操作系统为活跃进程准备了进程控制块(PCB)
操作系统将进程控制块(PCB)放置在一个合适的队列里
- 就绪队列
- 等待I/O队列(每个设备的队列)
- 僵尸队列
创建进程
Windows进程创建API: CreateProcess(filename)
- 创建时关闭所有在子进程里的文件描述符 CreateProcess(filename,CLOSE_FD)
- 创建时改变子进程的环境 CreateProcess(filename,CLOSE_FD, new_envp)
- 等等
Unix进程创建系统调用 fork/exec - fork()把一个进程复制成二个进程 parent(old PID), child(new PID)
- exec()用新程序来重写当前进程 PID没有改变
用fork和exec创建进程的示例
int pid = fork(); // 创建子进程
if(pid == 0) { // 子进程在这里继续
// Do anything (unmap memory, close net connections…)
exec(“program”, argc, argv0, argv1, …);
}
fork()创建一个继承的子进程
- 复制父进程的所有变量和内存
- 复制父进程的所有CPU寄存器(有一个寄存器例外)
fork()的返回值 - 子进程的fork()返回0
- 父进程的fork()返回子进程标识符
- fork()返回值可方便后续使用,子进程可使用getpid()获取PID
加载和执行进程
- exec()调用允许一个进程“加载”一个不同的程序并且在main开始执行
- 它允许一个进程指定参数的数量(argc)和它字符串参数数组(argv)
- 如果调用成功,它是相同的进程,但是它运行了一个不同的程序
- 代码, stack和heap重写
fork的简单实现:
- 对子进程分配内存
- 复制父进程的内存和CPU寄存器到子进程里
- 开销昂贵
在99%的情况里,我们在调用fork()之后调用exec()
- 在fork()操作中内存复制是没有作用的
- 子进程可能会关闭打开的文件和连接
- 开销因此是高的
- 为什么不能结合它们在一个调用中(OS/2, Windows)
vfork() - 一个创建进程的系统调用,不需要创建一个通向的内存映像
- 一些时候称为轻量级fork()
- 子进程应该几乎立即调用exec()
- 使用copyOnWrite(COW)技术
等待和终止进程
wait()系统调用是被父进程用来等待子进程的结束
- 一个子进程向父进程返回一个值,多义父进程必须接收这个值并处理
- wait()系统调用担任这个要求
- 它使父进程去睡眠来等待子进程的结果
- 当一个子进程调用exit()的时候,操作系统解锁父进程,并且将通过exit()传递得到的返回值作为wait()调用的一个结果(联通子进程的pid一起)如果这里没有子进程存货,wait()立刻返回
- 当然,如果这里有为父进程的僵尸等待,wait()立即返回其中的一个值(并且解除僵尸状态)
进程结束执行后,它调用exit()
这个系统调用:
- 将这程序的“结果”作为一个参数
- 关闭所有打开的文件,连接等等
- 释放内存
- 释放大部分支持进程的操作系统结构
- 检查是否父进程是存活着的:
如果存活:它保留结果的值直到父进程需要它;在这种情况里,进程没有真正死亡,但是他进入了僵尸状态(zombie/defunct)
如果非存活状态,它释放所有的数据结构,这个进程死亡 - 清理所有的僵尸进程
进程终止是最终的垃圾收集(资源回收)
fork()之后进入new()状态,exit()之后进入Zombie状态 exec()之后进入Running状态或Blocked状态