第二章 进程与线程
2.1进程
进程的模型(概念以及目的,PCB)
(进程的特征)
1.进程控制的流程
进程的创建
-
创建原语
分配标识号、申请空白PCB
分配资源
初始化PCB
将新进程插入就绪队列 -
引起进程创建的事件
用户登陆(系统初始化)
作业调度(批处理作业的初始化)
提供服务(执行了创建进程的系统调用)
应用请求(用户请求创建一个新进程)
进程的终止
-
撤销原语
根据标识符,从PCB集合中找到终止进程的PCB
若进程正在运行,立即终止该进程的执行,将CPU分配给其他进程
终止其所有子进程
将进程拥有的全部资源归还给其父资源或OS
将PCB删除 -
引起进程终止的事件
正常结束
自己请求终止(exit系统调用)
异常结束
非法使用特权指令、非法指令、算术运算错(整数除以零)、储区越界、保护错、I/O故障、等待超时、I/O故障、运行超时
外界干预
用户选择杀掉进程、父进程请求、父进程终止
进程的唤醒
- 唤醒原语
在事件等待队列找到PCB
将PCB从等待队列移出,设置其为就绪态
将PCB插入就绪队列 - 引起进程唤醒的事件
等待的事件发生
因何事阻塞就应由何事唤醒
进程的阻塞
- 阻塞原语
找到要被阻塞的进程标识号对应的PCB
保护进程现场(若为运行态),将其状态转换为阻塞态,停止运行
将PCB插入相应事件的等待队列 - 引起进程阻塞的事件
需要等待系统分配某种资源
需要等待互相合作的进程完成其他工作
2.进程的创建 4种分别对应的情况
- 总结都是已经存在的线程执行系统调用(Unix,Win创建时父子进程逻辑不同)
3.进程的终止 4种
进程控制的具体操作
进程的层次结构Unix保存了父子关系,Win没有
4.进程的五状态模型(之后加上挂起)以及状态之间的转换
- 创建
- 就绪
- 运行
- 阻塞
- 终止
5.进程的实现
- 进程切换的过程(中断的处理)
2.2 线程
0.使用线程的原因
-
程序需要并发执行多项功能,其中部分功能可能会发生阻塞,所以需要独立出来
-
线程比进程更轻量,切换开销更小
-
重叠I/O和计算型任务
1.线程使用的案例
- 写word的同时编排格式,保存信息
- web端处理请求查询缓存和磁盘IO的三种形式
2.线程模型
- 进程分配资源,CPU直接调用线程实例
- 线程概念是为了实现共享一组资源的多个线程的执行能力
- 进程共享部分:
- 线程独立部分:
- 线程依赖自身内部调度
- 多线程带来的复杂性:资源共享,同步操作,执行的优先级别
3.POSIX 线程
4.在用户空间实现线程
-
整个线程包放用户空间(用户级线程)
- 优点:线程管理在用户空间,上下文切换迅速,定制调度算法
- 缺点:单个线程持续独占进程分配到的单个CPU,难以只阻塞单个线程,缺页所导致的阻塞整个进程。多个线程不能并行的在CPU上运行。
-
在内核中实现线程
- 优点:某线程阻塞不影响其他线程
- 缺点:系统开销大影响程序性能
-
混合实现
7. 调度程序激活机制
- 上行调用(UNIX信号)原理
- 常规流程:用户级线程被阻塞后整个进程都将被阻塞(单用用户线程包的情况)
- 改进:~线程A被阻塞后,内核通知该进程的运行时系统,传递被阻塞的线程编号和原因。接着运行时系统再次调用该进程,该进程标记线程A被阻塞再从线程表中另选线程B执行。等到A可执行后再由内核通知该进程,之后由进程选择处理线程A的方式。
8. 弹出式线程
- 优点:没有历史包(不需要恢复之前存储在寄存器or堆栈之类的数据)从全新开始,所以创建速度快=》消息到达和开始处理之间的反应时间短
- 内核的~:优点权限大,容易访问所有的表格和I/O设备。缺点:出错代价大。用途:可以用在中断处理
9. 单线程代码多线程化
- 全局变量和局部变量的读写权限
- 给每个线程创建一个全局变量的副本
- 库的过程不是可重入的
- 一些方法有固定的执行顺序,在未完成第一次执行之前不能开始第二次执行(例如网络传输把数据写到缓冲区)
- malloc维护内存表格的时候切换线程所导致不一致的状态
- 处理方法:重写库
- 加锁(为每个过程添加个包装器,包装器中设置个flag表示某个库正在被使用)=》并行性下降
- 用户级(线程包内)线程向内核发送的信号产生覆盖问题(alarm闹钟函数会覆盖),以及内核给的通用性(入键盘输入)消息由哪个线程处理
- 堆载的管理
- 进程中的每个线程都需要有自己独立的堆栈(栈是先进先出,多线程并发显然不符合栈规则)但是内核不能意识到用户包内的线程(即用户包内的线程对内核透明)
2.3进程间通信
- 三个问题
- 进程间的信息传递
- 进程执行关键任务不能交叉(关键操作的事务)
- 进程正确的执行顺序(打印机打印文件)
1.竞争条件
- 多个线程同时读写某些共享数据,而最后结果取决于进程运行的精确时序(类似pc指针这样指向下个地址然后具体内容会被覆盖)
2.临界区
- 定义:
- 解决竞争条件的方案的四个需要满足的点
- 线程互斥读写共享数据:任何两个进程不能同时进入临界区
- 适应广泛的CPU:不对CPU的速度和数量做限制
- 避免无效等待:临界区外的进程不得阻塞其他进程
- 不能导致饥饿:不能让进程无限期等待进去临界区
3.忙等待的互斥
-
屏蔽中断
- 给用户进程的权限太大
- 只能屏蔽当前CPU的中断信号,不适合多核机器
-
锁变量
- 只加个flag标志位来记录是否有线程使用临界区(标记和进入不具备被原子性)
- 具体操作是因为多线程可能会导致跳过对方的一步检测,Peterson方法用了两步检测一定会检测到
- 只加个flag标志位来记录是否有线程使用临界区(标记和进入不具备被原子性)
-
严格轮询
- 自旋锁持续占据CPU(时间非常短可用)
- 只能线程交替进入,违反了避免无效等待的原则三
-
Peterson解法
- 把标记分为两步,欲望标记,和进入标记。只有当欲望和进入都标记上后才能进入临界区
-
TSL指令
- 使用TSL指令
- 原理:读取设置锁,自旋判断,锁住内存总线其他CPU无法访问内存(不同于屏蔽中断信号),访问完临界区调用mov修改锁
- 限制:互斥行为实际执行效果取决于进程的是否合作。如果一个进程有欺诈行为则互斥一定会失败
- 使用TSL指令
4.睡眠与唤醒
- 自旋锁和互斥锁的对比
- 忙等待(自旋锁)浪费CPU时间在等待时间短时适用(竞争小)。而且会造成优先级反转问题。
- 睡眠(阻塞)会切换内核态,所以在等待时间长的时候适用(竞争激烈)。
- 生产者和消费者例子出现竞争条件导致对未睡眠的线程使用wakeup导致信号丢失
5.信号量
- 定义:用一个整形来累计唤醒次数
- P/V操作这里称做down/up(为一般化(原子操作)后的sleep/wakeup)
6.互斥量
- 作用:实现容易有效所以常在用户空间的线程包
- 定义:只有加锁和解锁两种状态
-
利用互斥量实现用户级线程互斥
- 问题:即使内核级线程调用自旋锁持续忙等待,最终也会因为CPU的时间超时而调度其他线程=》之后锁会被释放。但是用户级线程忙等待即使被调度,进程下次上CPU还是该线程持续蛮等待,可能会造成死锁。
- 解决:用户级线程查询设置互斥变量,如果已经被锁则调用thread_yield放弃CPU并给其他线程
-
问题:进程间需要访问些共享内存来共享信号量以实现进程间通讯
- 可以放内核中每次通过系统调用查询
- 在UNIX和Windows都能让线程间共享部分地址空间(具体通过,缓冲区》其他数据结果》共享文件)
-
进程和线程的差别
- 进程打开文件,定时器,其他一些特性仍然独立,而线程则共享所有特性
- 进程共享的效率低于线程
-
快速用户区互斥量futex
- 目的:减少内核切换、
- 实现:futex包含内核服务和用户库两个部分。没有竞争的时候可以完全在用户空间工作
-
pthread中的互斥量
- 增加了互斥变量的调用方式,可以选择互斥变量已经被锁后执行阻塞还是返回错误信息继续执行该线程内的操作
- 增加了条件变量,用于在线程需要的条件不满足时阻塞,之后满足时通知
- 条件变量和互斥变量的关系、
- 互斥变量用处是只让一个线程访问资源的临界区。
- 两者常常结合使用
- 条件变量不会储存在内存中,如果没有线程在等待则会丢失信号
7.管程
- 特性:管程中只能由一个活跃进程(这一特性完成互斥)
- sleep和wakeup失败的原因是A进程准备休眠时B线程唤醒A导致信号丢失。而管程的互斥性规避了这点
- 问题:实现互斥访问后还需要让线程在无法继续运行时阻塞
- 解决:引入条件变量以及wait和signal
- 由于管程的互斥性,A唤醒B后A该如何?
- A挂起(舍弃)
- A需要以唤醒为最后一条语句并且立即退出管程(概念简单,实现容易)
- 其他方案:A继续运行等A退出时再让B进入管程
- 由于管程的互斥性,A唤醒B后A该如何?
- 问题:(在没有管程的语言中)在多CPU中可以用TSL之类来控制,避免竞争。如果在分布式系统中有多个CPU,每个又都有私有内存的时候该怎么实现多线程的互斥性
- 结论:信号量太低级,需要使用进程间通讯
8.消息传递
- 原理:进程间通讯是用原语send和receive,是系统调用不是语言特性(类似信号而不是管程)
- 过程:A给B发一条消息,B从某个源取一个消息。如果没有消息可能阻塞等待,也可能直接带错误码返回。
- 设计要点
- 问题:如何防止消息丢失
- 接收方收到消息后回复确认,如果发送方没收到回复则重新发送消息
- 问题:如果接收方回复的确认丢失,发送方将重复发送消息如何区分
- 每条原始消息中嵌入一个连续序号
- 问题:进程命名
- send和receive指定的进程没有二义性
- 问题:身份认证
- 问题:如何防止消息丢失
- 消息传递解决生产者-消费者问题
- 假设所有消息一样大小,开始由操作系统在缓冲区放入空消息,生产者取走一条新消息并填回一条消息。好处是所有消息都可以放在事先确定好数量的内存中
- 生成/消费者执行生产/消费行为,直到缓冲区全满/全空后线程被阻塞
- 对消息进行编址
- 为每个进程分配个唯一地址
- 引入信箱数据结构
- 经典:设置一个信箱对消息进行缓冲
- 消息直接从发送方到接受方,send和receive先后顺序决定了哪方被阻塞。
9.屏障
- 目的:过程被划分为多个阶段,并且只有当一组进程全部到达这一阶段后才能执行下一阶段。
- 实现:在每个阶段结尾安放屏障。用于同步一组线程
10.避免锁:读-复制-更新
- 某项情况下可以同时读写,只要确保每个读进程要么全读旧版本,要么全读新版本。
- 要在确定没有线程对相关数据进行访问时才可以删除。需要一个”宽限期“常常设置为所有的线程执行一次上下文切换。
2.4调度
- 调度程序:竞争CPU的进程多余空闲CPU的数量,所以CPU要选择其中一个执行。其中使用的算法就是调度算法。(进程和线程调度的处理方法类似)
1. 调度简介
1.进程行为
-
CPU密集型和I/O密集型
-
(I/O活动:CPU等待外部的设备完成工作)
2.何时调度
- 新建进程后决定运行旧线程还是新线程
- 在进程退出时
- 线程阻塞时(等待I/O,信号量)
- 发生I/O中断时(中断来自I/O设备,则说明该设备已经完成作业)
3. 调度算法分类
- 抢占和非抢占
- 批处理-减少进程切换,改善了性能。(不需要抢占)
- 交互式-(需要抢占)
- 实时
- 实时和交互式的区别:实时运行的程序是确定的(只运行推进现有应用的程序),而交互式系统可以运行任意程序(包括非协作和恶意程序)
4.调度算法的目标
- 总的来说:公平,强制执行,平衡(各部分都忙碌)
- 批处理系统:吞吐量,周转时间,CPU利用率(次)
- 交互系统:相应时间,均衡性(满足用户期望)
- 实时系统:满足截止时间,可预测性(用于影音)
2.批处理中的调度
- 先来先服务
- 优点:简单易用
- 缺点:不能均衡I/O和CPU型
- 最短作业优先(非抢占)
- 优点:周转时间最短(所有作业同时到达)
- 缺点:可能导致饥饿
- 最短剩余时间优先(抢占)
3. 交互式系统中的调度
- 轮转调度(时间片)
- 优点:简单,公平
- 特点:时间片的长度影响效果
- 优先级调度
- 优先级可以是静态的也可以是动态的
- 可以把I/O密集型的优先级设置为 运行时间/时间片
- 缺点:有可能产生饥饿
- 优先级可以是静态的也可以是动态的
- 多级队列
- 最短进程优先
- 问题:如何找出最短的进程
- 可以根据"老化"来预测(对前第几次的时间乘上2的几次方再求和)
- 保证调度
- 目标:对进程做出明确的性能保证
- 原理:根据CPU运行时间来分配CPU
- 实现:进程已分配时间/进程应分配时间。算出后值小的先上 (2秒/5秒=0.4)
- 彩票制度
- 目的:既能拟合预测结果,又能简单实现
- 原理:按条件给每个进程若干张彩票,每次CPU切换则抽奖。抽到谁就把CPU分配给谁
- 优点:
- 方便加权(多给彩票)
- 反应迅速(更改彩票数在下次分配CPU时就能实现)
- 进程间协作可以通过交换or给予彩票来增加
- 加权颗粒度比队列小
- 公平分享调度
- 和5.保证调度类似。公平分享的对象是用户,保证调度的对象是进程
4.实时系统中的调度
- 实时系统是以时间为主导
- 硬实时:必须满足绝对的截止时间。软实时:偶尔错过时间可以容忍
- 特点:把程序划分为一组进程,每个进程都可预测和提前掌握并且寿命短执行速度快。
- 调度:目的为了满足所以进程的截止时间
- 事件分为周期性和非周期性
- 可调度:可以处理负载的条件是每个进程的周期/执行时间的和≤1(忽略了上下文的切换带来的消耗)
- 静态调度算法,只有提前掌握所以的工作以及截止时间等全部信息才能运行,并且在系统开始运行之前就会做出调度决策。动态调度没有这些限制,在运行过程中进行调度决策
5.策略和机制
- 隐含的前提假设:所有进程属于不同用户,且相互竞争资源。
- 问题:主进程完全可能掌握某些子线程的重要程度的信息,但是以上调度算法都不能从用户进程处接受到有关调度决策的信息。导致调度程序很少做出最优的选择
- 解决:将调度机制和调度算法分离。提供系统调用给用户进程让其可以传入优先级之类的参数。这时调度机制位于内核,但是调度策略是由用户线程决定。
6.线程调度
- 存在两个层次的并行:线程和进程。其中用户级线程和内核级线程在调度处理上有本质差别
- 用户级线程:对内核调度来说与进程一致,具体调度受线程包控制。对其他进程没有影响。缺点:没有时钟中断运行时间过长的线程,容易造成一个线程持续占用CPU时间。
- 内核级线程:调度同进程
- 二者区别:效率上用户级线程高。内核级线程不会因为某个线程阻塞而导致整个进程阻塞。用户级线程可以使用专门为程序定制的线程调度程序(调度机制与调度算法分离)
- 效率高:因为不用切换系统上下文,不修改内存,使高速缓存失效(cache)
- 同理,切换同个进程内的线程效率高,所有内核倾向切换到同进程的另一线程
2.5经典IPC问题(进程间通讯)
1.哲学家就餐问题(互斥访问有限资源的竞争问题)
- 关键:不能死锁
- 解决方案一:拿起左手边的筷子再检测右手边,如果不可以则放弃左手边
- 问题:如果每个同时拿起左手边再检测右手边的筷子再放下还是会死锁(每个过程间等待个随机时长。这个可以用但不够安全)
- 解决方案二:一个互斥信号量=1,想吃就down,吃完再up
- 问题:有性能上的局限,只能一位用餐。理论上能有两位同时用餐
- 解决方案三:建一个5位数组,思考,想吃,在吃(0,1,2)如果哲学家想吃就把自己位上的信号标为想吃(1)。标完检测左右两个信号。只有两个信号为思考(0)则哲学家开始拿筷子,其他情况则阻塞。哲学家吃完时调用带上左右各自的号检测。
2.读者和写者(为数据库的访问建立模型)
- 关键:平衡读写进程
- 解决方案一:加信号量D=0,每个读者来时都up走时Ddown。写者来时先检查信号量是否为0 。不为0则持续等待。
- 问题:可能造成写者饥饿
- 解决方案二:在一之上再加个信号量X=0。写者来时up,只要读者来时检测到X不为0则阻塞。
- 问题:并发度和效率较低