操作系统—进程(Progess)& 线程(Thread)
1. 进程(Process)描述
进程定义
实际操作系统中的进程
- 进程:一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。
进程的组成
一个进程应该包括:
- 程序的代码;
- 程序处理的数据;
- 程序计数器中的值,指示下一条将运行的指令;
- 一组通用的寄存器的当前值,堆,栈;
- 一组系统资源(如打开的文件)
总之,进程包含了正在运行的一个程序的所有状态信息。
进程和程序的联系
- 程序是产生进程的基础
- 程序的每次运行构成不同的进程
- 进程是程序功能的体现
- 通过多次执行,一个程序可以对应多个进程;通过调用关系,一个进程可包括多个程序。
进程和程序的区别
- 进程是动态的, 程序是静态的:程序是有序代码的集合。 进程是程序的执行,进程有核心态 / 用户态。
- 进程是暂时的,程序是永久的。 进程是一个状态变化的过程,程序可以长久保存。
- 进程和程序的组成不同:进程的组成包括程序,数据和进程控制块(进程状态信息)
进程的特点
-
动态性:可动态地创建,结果进程;
-
并发性:进程可以被独立调度并占用处理机运行;(并发:一段,并行:一时刻)
-
独立性:不同进程的工作不相互影响;(页表是保障措施之一)
-
制约性:因访问共享数据 / 资源或进程间同步而产生制约。
问题:如果你要设计一个OS,怎么样来实现其中的进程管理机制?
进程控制结构
描述进程的数据结构:进程控制块 (Process Control Block)
操作系统为每个进程都维护了一个PCB,用来保存与该进程有关的各种状态信息。
-
进程控制块: 操作系统管理控制进程运行所用的信息集合。
操作系统用PCB来描述进程的基本情况以及运行变化的过程,PCB是进程存在的唯一标志。
-
使用进程控制块
➢ 进程的创建:为该进程生成一个PCB
➢ 进程的终止: 回收它的PCB
➢ 进程的组织管理: 通过对PCB的组织管理来实现
(PCB具体包含什么信息? 如何组织的? 进程的状态转换?)
PCB含有以下三大类信息:
-
① 进程标志信息。如本进程的标志,本进程的产生者标志(父进程标志);用户标志
-
② 处理机状态信息保存区。保存进程的运行现场信息:
➢ 用户可见寄存器; 用户程序可以使用的数据,地址等寄存器。
➢ 控制和状态寄存器; 如程序计数器(PC),程序状态字(PSW)。
➢ 栈指针;过程调用,系统调用,中断处理和返回时需要用到它。
-
③ 进程控制信息
➢ 调度和状态信息;用于操作系统调度进程并占用处理机使用。
➢ 进程间通信信息;为支持进程间与通信相关的各种标志,信号,信件等,这些信息都存在接收方的进程控制块中。
➢ 存储管理信息;包含有指向本进程映像存储空间的数据结构。
➢ 进程所用资源;说明由进程打开,使用的系统资源。 如打开的文件等。
➢ 有关数据结构的链接信息;进程可以连接到一个进程队列中,或连接到相关的其他进程的PCB。
PCB的组织方式
-
链表:同一状态的进程其PCB成一链表,多个状态对应多个不同的链表。
各状态的进程形成不同的链表:就绪链表,阻塞链表
-
索引表:同一状态的进程归入一个index表(由index指向PCB),多个状态对应多个不同的index表
各状态的进行形成不同的索引表:就绪索引表,阻塞索引表
2. 进程状态(State)
进程的生命期原理
进程创建
引起进程创建的3个主要事件:
- 系统初始化;
- 用户请求创建一个新进程;
- 正在运行的进程执行了创建进程的系统调用。
进程运行
内核选择一个就绪的进程,让它占用处理机并执行(后续的调度算法)
- 为何选择?
- 如何选择?
进程等待
在以下情况下,进程等待(阻塞):
- 请求并等待系统服务,无法马上完成
- 启动某种操作,无法马上完成
- 需要的数据没有到达
进程只能自己阻塞自己,因为只有进程自身才能知道何时需要等待某种事件的发生。
进程唤醒
唤醒进程的原因:
- 被阻塞进程需要的资源可被满足
- 被阻塞进程等待的事件到达
- 将该进程的PCB插入到就绪队列
进程只能被别的进程或操作系统唤醒。
进程结束
在以下四种情况下,进程结束:
- 正常退出(自愿的)
- 错误退出(自愿的)
- 致命错误(强制性的)
- 被其他进程所杀(强制性的)
sleep()系统调用对应的进程状态变化
gif
进程切换
进程状态变化模型
进程的三种基本状态:
进程在生命结束前处于三种基本状态之一。
不同系统设置的进程状态数目不同。
- 运行状态(Running):当一个进程正在处理机上运行时
- 就绪状态(Ready):一个进程获得了除处理机之外的一切所需资源,一旦得到处理机即可运行
- 等待状态(阻塞状态 Blocked):一个进程正在等待某一时间而暂停运行时。 如等待某资源,等待输入 / 输出完成。
进程其它的基本状态:
-
创建状态(New):一个进程正在被创建,还没被转到就绪状态之前的状态。
-
结束状态(Exit): 一个进程正在从系统中消失时的状态,这是因为进程结束或由于其它原因所导致。
可能的状态变化如下:
NULL → New:一个新进程被产生出来执行一个程序。
New → Ready: 当进程创建完成并初始化后,一切就绪准备运行时,变为就绪状态。是否会持续很久?很快。
Ready → Running :处于就绪态的进程被进程调度程序选中后,就分配到处理机上来运行。(怎么选中取决于后面的调度算法)
Running → Exit :当进程表示它已经完成或者因出错,当前运行进程会由操作系统作结束处理。
Running → Ready :处于运行状态的进程在其运行过程中,由于分配它的处理机时间片用完而让出处理机。谁完成?OS。
Running → Blocked: 当进程请求某样东西且必须等待时。例如?等待一个计时器的到达、读 / 写文件 比较慢等。
Blocked → Ready :当进程要等待某事件到来时,它从阻塞状态变到就绪状态。例如?事件到达等。谁完成?OS。
进程挂起模型
Why?为了合理且充分地利用系统资源。
进程在挂起状态时,意味着进程没有占用内存空间,处在挂起状态的进程映像在磁盘上。(把进程放到磁盘上)
挂起状态
- 阻塞挂起状态(Blocked-suspend):进程在外存并等待某事件的出现。
- 就绪挂起状态(Ready-suspend):进程在外存,但只要进入内存,即可运行。
与挂起相关的状态转换
挂起(Suspend): 把一个进程从内存转到外存,可能有以下几种情况:
- 阻塞到阻塞挂起:没有进程处于就绪状态或就绪进程要求更多内存资源时,会进行这种转换,以提交新进程或运行时就绪进程。
- 就绪到就绪挂起:当有高优先级阻塞(系统认为会很快就绪的)进程和低优先级就绪进程时,系统会选择挂起低优先级就绪进程。
- 运行到就绪挂起:对抢先式分时系统,当有高优先级阻塞挂起进程因事件出现而进入就绪挂起时,系统可能会把运行进程转导就绪挂起状态。
在外存时的状态转换:
- 阻塞挂起到就绪挂起:当有阻塞挂起因相关事件出现时,系统会把阻塞挂起进程转换为就绪挂起进程。
解挂 / 激活: 把一个进程从外存转到内存;可能有以下几种情况:
- 就绪挂起到就绪:没有就绪进程或挂起就绪进程优先级高于就绪进程时,会进行这种转换。
- 阻塞挂起到阻塞:当一个进程释放足够内存时,系统会把一个高优先级阻塞挂起(系统认为会很快出现所等待的事件)进程转换为阻塞进程。
问题:OS怎么通过PCB和定义的进程状态来管理PCB,帮助完成进程的调度过程?
3. 线程(Thread)
为什么使用线程?
【案例】编写一个MP3播放软件。
核心功能模块有三个:
(1) 从MP3音频文件中读取数据;
(2) 对数据进行解压缩;
(3) 把解压缩后的音频数据播放出来。
单进程的实现方法
main()
{
while (TRUE)
{
Read(); // I/O
Decompress(); // CPU
Play();
}
}
Read() {
...}
Decompress() {
...}
Play() {
...}
/*
问题:
播放出来的声音能否连贯?
各个函数之间不是并发执行,影响资源的使用效率。
*/
多进程的实现方法
// 程序1
main()
{
while (TRUE)
{
Read();
}
}
Read() {
...}
// 程序2
main()
{
while (TRUE)
{
Decompress();
}
}
Decompress() {
...}
// 程序3
main()
{
while (TRUE)
{
Play();
}
}
Play() {
...}
// 问题:进程之间如何通信,共享数据?另外,维护进程的系统开销较大;
// 创建进程时,分配资源,建立PCB;撤销进程时,回收资源,撤销PCB;进程切换时,保存当前进程的状态信息
怎么来解决这些问题?
需要提出一种新的实体,满足以下特征:
- 实体之间可以并发执行;
- 实体之间共享相同的地址空间。
这实体就是线程(Thread)。
什么是线程
Thread:进程当中的一条执行流程。
从两个方面重新理解进程:
- 从资源组合的角度:进程把一组相关的资源组合起来,构成了一个资源平台(环境),包括地址空间(代码段、数据段)、打开的文件等各种资源;
- 从运行的角度:代码在这个资源平台上的一条执行流程(线程)。
线程 = 进程 - 共享资源
线程的优点也是线程的缺点,由于共享资源,安全性得不到保障。
使用线程:很强调性能、执行代码相对统一的高性能计算(天气预报、水利、空气动力);
使用进程:Chrome浏览器,一个进程打开一个网页,某一个网页崩溃之后不会影响到其他进程网页的浏览。
进程和线程有各自的特点,需要根据应用具体情况选择合适的模式设计程序。
线程和进程的比较
-
进程是资源分配单位,线程是CPU调度单位;
-
进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
-
线程同样具有就绪,阻塞和执行三种基本状态,同样具有状态之间的转换关系;
-
线程能减少并发执行的时间和空间开销:
➢ 线程的创建时间比进程短;(直接利用所属进程的一些状态信息)
➢ 线程的终止时间比进程短;(不需要考虑资源的释放问题)
➢ 同一进程内的线程切换时间比进程短;(同一进程不同线程的切换不需要切换页表)
➢ 由于同一进程的各线程之间共享内存和文件资源,可直接进行不通过内核的通信。(直接通过内存地址数据传递)
线程的实现
主要有三种线程的实现方式:
-
用户线程:在用户空间实现;
POSIX Pthreads,Mach C-threads,Solaris threads
-
内核线程:在内核中实现;
Windows,Solaris,Linux
-
轻量级进程:在内核中实现,支持用户线程。
Solaris(LightWeight Process)
用户线程
操作系统只能看到进程,看不到线程,线程的TCB在线程库中实现;
在用户空间实现的线程机制,它不依赖于操作系统的内核,由一组用户级的线程库函数来完成线程的管理,包括进程的创建、终止、同步和调度等。
- 由于用户线程的维护由相应的进程来完成(通过线程库函数),不需要操作系统内核了解用户进程的存在,可用于不支持线程技术的多进程操作系统;
- 每个进程都需要它自己私有的线程控制块(TCB)列表,用来跟踪记录它的各个线程的状态信息(PC,栈指针,寄存器),TCB由线程库函数来维护;
- 用户线程的切换也是由线程库函数来完成,无需用户态 / 核心态切换,所以速度特别快;
- 允许每个进程拥有自定义的线程调度算法。
用户线程的缺点:
- 阻塞性的系统调用如何实现?如果一个线程发起系统调用而阻塞,则整个进程在等待;
- 当一个线程开始运行时,除非它主动地交出CPU的使用权,否则它所在的进程当中的其他线程将无法运行;
- 由于时间片分配给进程,所以与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会较慢。
内核线程
操作系统能够看到进程也可能看到线程,线程在内核中实现;
内核线程是指在操作系统的内核当中实现的一种线程机制,由操作系统的内核来完成线程的创建,终止和管理。
一个PCB会管理一个TCB的list。
- 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息(PCB和TCB);
- 线程的创建,终止和切换都是通过系统调用 / 内核函数的方式来进行,由内核来完成,因此系统开销较大;
- 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;
- 时间片分配给线程,多线程的进程获得更多CPU时间;
- Windows NT 和 Windows 2000/XP 支持内核线程。
轻量级进程(LightWeight Process)
它是内核支持的用户线程。一个进程可以有一个或多个轻量化进程,每个轻量级进程由一个单独的内核线程来支持。(Solaris / Linux)
4. 进程切换
上下文切换(Context switch)
停止当前运行进程(从运行状态变成其他状态),并且调度其他进程(转变为运行状态)
- 必须在切换之前存储许多部分的进程上下文
- 必须能够在之后恢复他们,所以进程不能显示它曾经被暂停过
- 必须快速(上下文切换时非常频繁)
需要存储什么上下文?
- 寄存器(PC,SP…),CPU状态,…
- 一些时候可能会费时,所以我们应该尽可能避免
-
操作系统为活跃进程准备了进程控制块(PCB)
-
操作系统将进程控制块(PCB)放置在一个合适的队列里
➢ 就绪队列
➢ 等待I/O队列(每个设备的队列)
➢ 僵尸队列
5. 进程控制
创建进程
fork()的地址空间复制
加载和执行进程
系统调用exec()
加载程序取代当前运行的进程。
In the parent process:
main()
...
int pid = fork(); // 创建子进程
if (pid == 0) {
// 子进程
exec_status = exec("calc", argc, argv0, argv1, ...);
printf("Why would I execute?");
} else {
// 父进程。合理设计:else if (pid > 0)
printf("Whose your daddy?");
...
child_status = wait(pid);
}
if (pid < 0) {
/* error occurred */
在shell中调用fork()
后加载计算器的图示
执行完exec()
后,pid的id变化了,open files的路径也改变了
在实际内存中的布局图:
执行完exec()
后,PCB中的代码段完全被新的程序calc所替换,且执行地址发生了变化
-
exec()
调用允许一个进程”加载“一个不同的程序并且在main开始执行(事实上 _start) -
它允许一个进程指定参数的数量(argc)和它字符串参数数组(argv)
-
如果调用成功
➢ 它是相同的进程…
➢ 但是它运行了一个不同的程序!!
-
代码,stack & heap 重写
fork()
的简单实现:
- 对子进程分配内存
- 复制父进程的内存和CPU寄存器到子进程(有一个寄存器例外)
- 开销昂贵!!
在99%的情况下,我们在调用fork()
之后调用exec()
- 在
fork()
操作中内存复制是没有作用的 - 子进程将可能关闭打开的文件和连接
- 开销因此是最高的
- 为什么不能结合它们在一个调用中(OS/2,windows)?
vfork()
-
一个创建进程的系统调用,不需要创建一个同样的内存映像
-
一些时候称为轻量级
fork()
-
子进程应该几乎立即调用
exec()
-
现在不再使用(虚fork,早期的Unix系统提供的一种手段,只复制一小部分内容)
➢ 目前使用 Copy on Write(COW)技术 参考
➢ Java中也有Copy on Write技术,相关实现在并发包
java.util.concurrent
中:
等待和终止进程
wait()
系统调用是被父进程用来等待子进程的结束
-
一个子进程向父进程返回一个值,所以父进程必须接受这个值并处理 (子进程无法释放掉自己的PCB,父进程在子进程执行结束后,接收返回值,帮助子进程释放内存中的PCB等资源)
-
wait()
系统调用担任这个要求➢ 它使父进程去睡眠来等待子进程的结束
➢ 当一个子进程调用
exit()
的时候,操作系统解锁父进程,并且将通过exit()
传递得到的返回值作为wait()
调用的一个结果(连同子进程的pid一起)如果这里没有子进程存活,wait()
立刻返回。➢ 当然,如果这里有为父进程的僵尸等待,
wait()
立即返回其中一个值(并且解除僵尸状态) -
进程结束执行之后,它调用
exit()
-
这个系统调用:
➢ 将这程序的 ”结果“ 作为一个参数
➢ 关闭所有打开的文件,连接等等
➢ 释放内存
➢ 释放大部分支持进程的操作系统结构
➢ 检查是否父进程是存活着的:
√ 如果是的话,它保留结果的值直到父进程需要它;在这种情况里,进程没有真正死亡,但是它进入了僵尸(zombie/defunct)状态
√ 如果没有,它释放所有的数据结构,这个进程死亡(Root进程会定期扫描僵尸队列 判断僵尸状态的子进程的父进程是否存在)
➢ 清理所有等待的僵尸进程
-
进程终止是最终的垃圾收集(资源回收)
执行exec()
时,进程可能处于不同的状态。
整理自 【清华大学】 操作系统