进程管理
- 进程(Process)描述
- 进程状态(State)
- 线程(Thread)
- 进程间通信(Inter-Process Communication)
- 进程互斥与同步
- 死锁(Deadlock)
1. 进程描述
- 进程定义
- 进程的组成
- 进程的特点
- 进程控制结构
1.1 进程的定义
进程
:一个具有一定独立功能的程序在一个数据集合上的一次动态的执行过程
- 程序通过编译变成了可执行文件,可执行程序包含了代码段、数据段等
- 当前程序是放在文件中,它是静态的,只有当OS将文件调入到内存中使得程序执行起来(通过CPU执行每条指令,对相关数据进行处理。完成特定功能,这是动态过程)
1.2 进程的组成
进程包括 :
- 程序的代码
- 程序处理的数据
- 程序计数器中的值, 指示下一条将运行的指令
- 一组通用的寄存器的当前值, 堆, 栈
- 一组系统资源(如打开的文件)
进程和程序的联系 :
- 程序是产生进程的基础
- 程序的每次运行构成不同的进程
- 进程是程序功能的体现
- 通过多次执行, 一个程序可以对应多个进程, 通过调用关系, 一个进程可包括多个程序.
进程和程序的区别 :
进程是动态的, 程序是静态的 : 程序是有序代码的集合. 进程是程序的执行, 进程有核心态 / 用户态.
- 进程是暂时的, 程序是永久的. 进程是一个状态变化的过程, 程序可以长久保存.
- 进程和程序的组成不同 : 进程的组成包括程序, 数据和进程控制块(进程状态信息
类比
进程在进行过程中有可能被切换,CPU在执行的过程中会动态切换不同的进程,来实现不同的功能(进程的动态)
1.3 进程的特点
动态性
: 可动态地创建, 结果进程;并发性
: 进程可以被独立调度并占用处理机运行; (并发:一段, 并行:一时刻)独立性
: 不同进程的工作不相互影响;(页表是保障措施之一)制约性
: 因访问共享数据, 资源或进程间同步而产生制约.
如果你要设计一个OS, 怎么样来实现其中的进程管理机制?
进程控制结构
- 操作系统也是一个程序只不过它多了算法和数据结构,通过数据结构来管理进程
- 进程控制块用来描述进程在整个过程中的动态的的状态变化,以及进程在运行过程中需要的资源
1.4 进程控制结构
- 进程与PCB是一对一的关系,进程消失则PCB也消失,创建一个进程,必然有会创建唯一PCB与之对应
进程的创建
: 为该进程生成一个PCB进程的终止
: 回收它的PCB进程的组织管理
: 通过对PCB的组织管理来实现
PCB具体包含什么信息? 如何组织的? 进程的状态转换?
(因为内存中可能存在多个进程,需要有效的组织起来,我们希望PCB可以描述进程的状态变化)
PCB含有以下三大类信息
:
(一)进程标志信息:
如本进程的标志, 本进程的产生者标志(父进程标志). 用户标志(二)处理机状态信息保存区
: 保存进程的运行现场信息 :用户可见寄存器
. 用户程序可以使用的数据, 地址等寄存器控制和状态寄存器
: 如程序计数器(PC), 程序状态字(PSW)栈指针
: 过程调用, 系统调用, 中断处理和返回时需要用到它
(三)进程控制信息(前面俩个是执行的状态表示)
调度和状态信息
. 用于操作系统调度进程并占用处理机使用.进程间通信信息
. 为支持进程间与通信相关的各种标志, 信号, 信件等, 这些信息都存在接收方的进程控制块中.存储管理信息
. 包含有指向本进程映像存储空间的数据结构.(进程的的内存管理)进程所用资源
. 说明由进程打开, 使用的系统资源. 如打开的文件等.(一个进程可能打开多个不同文件)有关数据结构的链接信息
. 进程可以连接到一个进程队列中, 或连接到相关的其他进程的PCB.(进程之间有关系,例如父进程,子进程,A创建C,A创建B,此时A是B和C的父进程,可以通过链表管理)
PCB的组织方式
- 进程是动态的,OS在管理进程是一会创建一会结束,应该是动态插入和动态删除
2. 进程的状态
- 进程的生命期管理
- 进程状态变化模型
- 进程挂起模型
2.1 进程的生命期管理
- 进程创建
- 进程运行
- 进程等待
- 进程唤醒
- 进程结束
进程创建
- 开始创建第一个进程称为MIT进程(当用户发出请求时,这个进程后续再创建其他新进程)
引起进程创建的3个主要事件 :
系统初始化;
用户请求创建一个新进程;
正在运行的进程执行了创建进程的系统调用.
进程运行
内核选择一个就绪的进程, 让它占用处理机并执行
- 为何选择?
- 如何选择?
进程等待
在以下情况下, 进程等待(阻塞):
- 请求并等待系统服务, 无法马上完成
- 启动某种操作, 无法马上完成
- 需要的数据没有到达
进程只能自己阻塞自己, 因为只有进程自身才能知道何时需要等待某种事件的发生.
进程唤醒
唤醒进程的原因 :
- 被阻塞进程需要的资源可被满足
- 被阻塞进程等待的事件到达
- 将该进程的PCB插入到就绪队列
进程只能被别的进程或操作系统唤醒
进程结束
在以下四种情况下, 进程结束 :
- 正常退出(自愿)
- 错误退出(自愿)
- 致命错误(强制性)
- 被其他进程杀死(强制性)
2.2 进程状态变化模型
进程的三种基本状态 :
- 进程在生命结束前处于三种基本状态之一.
不同系统设置的进程状态数目不同.
三种基本状态
运行状态(Running)
: 当一个进程正在处理机上运行时就绪状态(Ready)
: 一个进程获得了除处理机之外的一切所需资源, 一旦得到处理机即可运行等待状态(阻塞状态 Blocked)
: 一个进程正在等待某一时间而暂停运行时. 如等待某资源, 等待输入/输出完成.
进程其它的基本状态
创建状态(New)
: 一个进程正在被创建, 还没被转到就绪状态之前的状态结束状态(Exit)
: 一个进程正在从系统中消失时的状态, 这是因为进程结束或由于其它原因所导致.
状态变化图
可能的状态变化如下 :
NULL → New
: 一个新进程被产生出来执行一个程序New → Ready
: 当进程创建完成并初始化后, 一切就绪准备运行时, 变为就绪状态Ready → Running
: 处于就绪态的进程被进程调度程序选中后, 就分配到处理机上来运行Running → Exit
: 当进程表示它已经完成或者因出错, 当前运行进程会由操作系统作结束处理Running → Ready
: 处于运行状态的进程在其运行过程中, 由于分配它的处理机时间片用完而让出处理机Running → Blocked
: 当进程请求某样东西且必须等待时Blocked → Ready
: 当进程要等待某事件到来时, 它从阻塞状态变到就绪状态
2.3 进程挂起(主要就是内存和外存打交道,产生挂起)
- 进程挂起与进程阻塞不一样
- 在虚拟存储时,运行的程序(进程)一部分内存空间会放到硬盘上,腾出更多的空间给需要的进程使用
俩种挂起状态1. 阻塞挂起状态
: 进程在外存并等待某事件的出现2. 就绪挂起状态
: 进程在外存, 但只要进入内存, 即可运行.
与挂起相关的状态转换
挂起 : 把一个进程从内存转到外存, 可能有以下几种情况 :
阻塞到阻塞挂起
: 没有进程处于就绪状态或就绪进程要求更多内存资源时, 会进行这种转换, 以提交新进程或运行时就绪进程.就绪到就绪挂起
: 当有高优先级阻塞(系统认为会很快就绪的)进程和低优先级就绪进程时, 系统会选择挂起低优先级就绪进程.运行到就绪挂起
: 对抢先式分时系统, 当有高优先级阻塞挂起进程因事件出现而进入就绪挂起时, 系统可能会把运行进程转导就绪挂起状态.
在外存时的状态转换 :
阻塞挂起到就绪挂起
: 当有阻塞挂起因相关事件出现时, 系统会把阻塞挂起进程转换为就绪挂起进程.(但是进程本身所有的资源都还在外存中)
解挂, 激活 : 把一个进程从外存转到内存; 可能有以下几种情况 :
就绪挂起到就绪
: 没有就绪进程或挂起就绪进程优先级高于就绪进程时, 会进行这种转换.(当获得资源,直接变成就绪态)阻塞挂起到阻塞
: 当一个进程释放足够内存时, 系统会把一个高优先级阻塞挂起(系统认为会很快出现所等待的事件)进程转换为阻塞进程.
OS怎么通过PCB和定义的进程状态来管理PCB, 帮助完成进程的调度过程?(具体选择哪个程序执行)
状态队列(对于任何状态而言,它存在多个进程,OS要维护一组队列)
- 由操作系统来维护一组队列, 用来表示系统当中所有进程的当前状态;(就绪态都放在就绪队列中等)
- 不同的状态分别用不同的队列来表示(就绪队列, 各种类型的阻塞队列);
- 每个进程的PCB都根据它的状态加入到相应的队列当中, 当一个进程的状态发生变化时, 它的PCB从一个状态中脱离出来, 加入到另外一个队列.
状态表示方法
- 存在多个就绪队列,进程分优先级,就绪队列1的先调度,如果1没有了在调度就绪队列2
- 如果等待事件1只能满足一个进程,那么就一个进程换到就绪态,如果全部都满足,把所有进程换到就绪态
3. 线程
比进程更小的独立运行单位是线程(go语言还有协程)
- 为什么使用线程
- 什么是线程
- 线程的实现
- 多线程编程接口举例
3.1 为什么使用线程
单进程实现方法
//单进程方式
while(1){
Read();
Decompress();
Play();
}
问题: 播放出来的声音能否连贯? (因为读这个文件需要IO操作,所以CPU会等待)各个函数之间不是并发执行, 影响资源的使用效率.
多进程实现方法
//多进程
//进程1
while(1){
Read();
}
//进程2
while(1){
Decompress();
}
//进程3
while(1){
Play();
}
问题: 进程之间如何通信,共享数据?另外,维护进程的系统开销较大:
创建进程时,分配资源,建立PCB;撤销进程时,回收资源,撤销PCB;进程切换时,保存当前进程的状态信息
解决问题
因此需要提出一种新的实体, 满足以下特征:
- 实体之间可以并发执行;
- 实体之间共享相同的地址空间和资源(进程间的地址空间是相互独立的,OS从一个进程换到另一个进程开销大)
这实体就是线程
.
3.2 什么是线程
进程拆开有俩部分组成一个是资源管理一个是线程( 进程 = 线程 + 共享资源)
- 一个进程可以有多个线程,线程共享进程提供的资源
- 线程有自己的线程控制块TCB(进程是PCB)
线程的优点:
- 一个进程中可以同时存在多个线程;
- 各个线程之间可以并发地执行;
- 各个线程之间可以共享地址空间和文件等资源.
线程的缺点:
- 一个线程崩溃, 会导致其所属进程的所有线程崩溃.(安全性低)
线程所需的资源
不同的线程需要独立的寄存器和堆栈, 共享代码,数据和文件等.
3.2.1 线程和进程的比较
- 进程是资源分配单位(资源包括内存、文件、网络), 线程是CPU调度单位(CPU是特殊的资源,执行控制流所需要的信息)
- 进程拥有一个完整的资源平台, 而线程只独享必不可少的资源, 如寄存器和栈;
- 线程同样具有就绪,阻塞和执行三种基本状态,同样具有状态之间的转换关系;
线程能减少并发执行的时间和空间开销:
线程的创建时间比进程短;(直接利用所属进程的一些状态信息)
线程的终止时间比进程短;(不需要考虑把这些状态信息给释放)
同一进程内的线程切换时间比进程短;(同一进程不同线程的切换不需要切换页表)
由于同一进程的各线程之间共享内存和文件资源, 可直接进行不通过内核的通信.(直接通过内存地址读写资源)
进程创建需要创建线程和相关的资源,所以所需时间长,而进程直接就可以使用线程的资源,因此创建线程所需要的时间更少
线程具有同一个地址空间也就是同一个进程的页表,切换时不需要切换内存管理的页表,而对于进程切换时,需要切换页表,开销大(会涉及到访问的地址空间,缓存等都不一样,都是无效需要重新加载)
3.3 线程的实现
主要有三种线程的实现方式:
用户线程
: 在用户空间实现;POSIX Pthreads, Mach C-threads, Solaris threads
内核线程
: 在内核中实现;Windows, Solaris, Linux
轻量级进程:
在内核中实现,支持用户线程;Solaris(LightWeight Process)
用户线程与内核线程的对应关系
- 多对一
- 一对一
- 多对多
3.3.1 用户线程
用户线程库完成了对用户线程的管理,线程控制块(TCB)就在这个库里面实现,对于OS它看不到TCB只能看到进程信息,但是进程里面的线程只有线程管理库可以看到,线程OS看不到
- 所以操作系统不能直接参加线程调度
- 当一个进程处于阻塞中,那么它里面的所有线程也都处于阻塞状态
- 线程的调度有自己的算法(后面说)
用户线程
操作系统看不到的是用户线程,用户线程操作系统看不到由应用线程的库来管理
操作系统只能看到进程, 看不到线程, 线程的TCB在线程库中实现
在用户空间实现的线程机制, 它不依赖于操作系统的内核, 由一组用户级的线程库来完成线程的管理, 包括进程的创建,终止,同步和调度等.
- 由于用户线程的维护由相应的进程来完成(通过线程库函数),不需要操作系统内核了解用户进程的存在,可用于不支持线程技术的多进程操作系统;
- 每个进程都需要它自己私有的线程控制块(TCB)列表,用来跟踪记录它的各个线程的状态信息(PC,栈指针,寄存器),TCB由线程库函数来维护;
- 用户线程的切换也是由线程库函数来完成,无需用户态/核心态切换,所以速度特别快;
- 允许每个进程拥有自定义的线程调度算法.
用户线程的缺点:
- 阻塞性的系统调用如何实现?如果一个用户线程发起系统调用而阻塞,则整个进程在等待
- 当一个线程开始运行时,除非它主动地交出CPU的使用权,否则它所在的进程当中的其他线程将无法运行;
- 由于时间片分配给进程,所以与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会较慢.
理解
1.(因为OS看不到线程,他只能看到进程,当线程因调用发生阻塞,它以为是进程发出的,所以他会阻塞进程,进而导致所有线程都阻塞);
2.用户态的线程库无法主动打断用户线程执行(无特权),但是OS可以,OS会管理中断,一旦产生中断,CPU控制权全部交给OS,OS进行处理,例如时钟中断,OS会强制切换,把当前线程停止下来,执行其他线程
3.3.2 内核线程
内核线程
- 内核线程操作系统可以看到,意味着它的线程控制块(TCB)是在内核中的,所以CPU的调度单位是线程,而不是进程
- 进程主要完成资源管理,一个进程PCB会管理一系列线程(TCB),但是具体调度是由TCB完成的
只要内核线程完成一次切换,就要完成一个从用户态到内核态变化,相比前面用户线程开销大(用户线程都是在用户态进行切换的)
操作系统管理起来的。能够看到的线程是内核线程,内核是由OS管理
- 操作系统能够看到进程也可能看到线程,线程在内核中实现;内核线程是在操作系统的内核当中实现的一种线程机制,由操作系统的内核来完成线程的创建,终止和管理.
- 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息(PCB和TCB);
- 线程的创建,终止和切换都是通过系统调用,内核函数的方式来进行,由内核来完成,因此系统开销较大;
- 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;
- 时间片分配给线程,多线程的进程获得更多CPU时间;
- Windows NT 和 Windows 2000/XP 支持内核线程.
轻量级进程
4. 进程切换(上下文切换)
- 各个进程是共享CPU资源,在不同时刻需要进行切换让不同的进程执行,切换过程就叫上下文切换
停止当前运行进程(从运行状态变成其他状态),并且调度其他进程(转变为运行状态)
必须在切换之前存储许多部分的进程上下文
必须能够在之后恢复他们,所以进程不能显示它曾经被暂停过
必须快速(上下文切换时非常频繁)
需要存储什么上下文?
寄存器(PC,SP...),CPU状态等信息
一些时候可能会费时,所以我们应该尽可能避免
这部分切换都是与硬件联系,所以使用汇编代码写的
5. 进程控制
5.1 创建进程
fork()的简单实现
- 对子进程分配内存
- 复制父进程的内存和CPU寄存器到子进程
- 开销昂贵
在99%的情况下,我们在调用fork()之后调用exec()
- 在fork()操作中内存复制是没有作用的
- 子进程将可能关闭打开的文件和连接
- 开销因此是最高的
- 为什么不能结合它们在一个调用中(OS/2, windows)?
vfork()
- 一个创建进程的系统调用,不需要创建一个同样的内存映像
- 一些时候称为轻量级fork()
- 子进程应该几乎立即调用exec()
- 现在不再使用如果我们使用 copy on write 技术
5.2 加载和执行进程
exec
是让当前进程执行新的程序,所有的地址空间和代码全部变成新的- 当执行完
exec
后当前地址空间的代码段已经覆盖了,则printf
的内容已经被新的程序calc
覆盖了,就不会打印了.如果打印出来,就意味着exec
执行失败 pid > 0
意味着执行空间在父进程,wait(pid)
父进程要等待子进程,wait
返回意味着子进程结束pid = 0
fork失败
int pid = fork(); //创建子进程
if(pid == 0) { //子进程
exec_status = exec("calc", argc, argv0,argv1,...);
printf("Why would I execute?");
} else if(pid > 0) { //父进程
printf("Whose your daddy?");
...
child_status = wait(pid);
}
执行exec代码和数据都复制了一份,原来的代码发生了变化,被新代码覆盖了
内存中的布局图
- 系统调用
exec()
加载程序取代当前运行的进程 exec()
调用允许一个进程"加载"一个不同的程序并且在main
开始执行(事实上_start
)- 它允许一个进程指定参数的数量(
argc
)和它字符串参数数组(argv
) 如果调用成功
它是相同的进程
但是它运行了一个不同的程序
- 代码,
stack
,heap
重写
fork执行开销,因为在执行的过程中会创建很多的子进程所以要使fork效率高
- 之前
fork
是把父进程空间完全复制一份到子进程,内存的大拷贝(代码段和数据段等),但是做exec
时你之前做的拷贝全都被覆盖了(因为要加载新的程序)
解决办法
vfork
:对父进程只进行一小部分复制,一个fork
一个vfork
增加编程人员开销COW技术
:通过虚存管理,写的时候在进行复制(当父进程创建子进程使用的COW技术,则在复制时,只是复制父进程一些源数据(页表),他们指向同一块空间,当父进程或者子进程在写的时候,会触发异常使得这个页复制成俩份,分别不同的地址,如果只读则不需要)
5.3 等待和终止进程
父进程创建子进程后,最重要的一个是等待子进程的结束
问题1:
:当子进程结束直接调用exit
不就完事了,当子进程执行完exit
后是否子进程需要的资源全部回收到OS中
操作系统会对当前退出的子进程,回收地址空间、文件系统及其资源,但是对于当前进程的进程控制块(PCB)不会被操作系统回收
- 当操作系统释放完这些资源,它无法回到用户空间在执行,但是在内核中还有相应的资源例如PCB,它是很难释放的
- 当子进程执行完
exit
最后时会返回,会通过OS通知父进程,如果父进程在执行wait
,那正好这俩个对上,父进程会帮助子进程把它内存中的资源释放掉(PCB)- 父进程
wait
配合子进程的exit
完成对子进程所有地址空间的及其资源的回收
wait()
系统调用是被父进程用来等待子进程的结束
- 一个子进程向父进程返回一个值,所以父进程必须接受这个值并处理
wait()
系统调用担任这个要求它使父进程去睡眠来等待子进程的结束
当一个子进程调用
exit()的时候,操作系统解锁父进程,并且将通过
exit()传递得到的返回值作为
wait调用的一个结果(连同子进程的
pid一起)如果这里没有子进程存活,
wait()立刻返回
-
当然,如果这里有为父进程的僵尸等待,
wait()立即返回其中一个值(并且解除僵尸状态)
- 当子进程执行完
exit
,且父进程还没有执行完wait
的时候(没有把子进程PCB回收这段时间),这中间有一段时间
- 进程结束执行之后,它调用exit()
这个系统调用:
将这程序的"结果"作为一个参数
关闭所有打开的文件,连接等等
释放内存
释放大部分支持进程的操作系统结构
检查是否父进程是存活着的:
- 如果是的话,它保留结果的值直到父进程需要它;在这种情况里,进程没有真正死亡,但是它进入了僵尸状态
- 如果没有,它释放所有的数据结构,这个进程死亡
清理所有等待的僵尸进程
- 进程终止是最终的垃圾收集(资源回收)
僵尸状态exit执行完毕但是wait还没有执行完毕,此时子进程就处于僵尸状态,什么也做不了就只能等待被父进程回收
问题2:父进程先于子进程死亡退出了,那么就不可能有一个父进程在wait了,也就意味着子进程一直处于僵尸状态,无法回收相应的PCB,那么它就会一直留在OS中
>解决办法
进程都会有父子关系,最早的进程称为
root进程,它会定期扫描进程控制块列表,看是否有进程处于僵尸状态,如果有进程处于僵尸状态,他会代替进程的父进程完成回收操作,使得操作系统中不会有过多的僵尸进程存在
执行exec时,进程可能处于不同的状态,例如当从磁盘加载一个新程序时,进程可能从运行到阻塞,然后再运行新程序
exec: 加载新程序和运行新程序