L11 内核级线程
MMU 映射表
并行:同时触发,同时执行
进程是内核态的,因为它要分配硬件资源
多处理器有多个映射表,多核使用一个映射表。
多线程要到内核中才能利用多核。所以要使用能够进入内核的核心级线程(用户态是核心看不到的所以无法分配硬件资源)
在用户级线程中是通过TCB管理2个用户栈完成的切换
而在核心级中,除了要使用用户栈外,还要进行函数调用进入内核态
所以两个栈不够,要两套栈
用户栈到内核栈,用的仍然是中断
INT时,用户栈进入内核栈,并压入信息(当前用户栈的信息,方便IRET时找到当前用户栈)
IRET时,内核栈返回用户栈,弹栈
switch完成内核态中的线程切换(TCB切换)
重点!!!!
内核线程切换的全过程
- 中断入口,线程S的用户态进入内核态
- 中断处理,在内核态中发生阻塞(IO或其他操作)
- 阻塞导致切换,switch to(cur,next),切换TCB
- 新的TCB找到新线程T的内核态
- 中断返回(IRET),内核态返回用户态
申请内存作为TCB, 都是cs:0f,
tcb.esp=krlstack内核栈关联用户栈。esp指向包含中断出口,然后把上面栈东西该弹出去的都弹出去了。然后就开始指向500的代码。
L14 CPU调度策略
这一章解决的问题:
PID1处于阻塞,需要切换,此时PID2和PID3都就绪,该选择哪一个?
操作系统在调度的时候,关键是折中,需要综合考虑。
吞吐量和响应时间之间有矛盾:
响应时间小,是切换次数多,是因为要很快切回来。切换时间就是内耗,内耗大,吞吐量就小。
常见的调度算法:
- 先来先服务(FCFS)
平均周转时间偏大
怎么缩短平均周转时间?
将短作业提前 - 短作业优先
短作业优先保证了平均周转时间最短
然而导致了响应时间过长 - 轮转调度(时间片)
按时间片来轮转调度,能够控制响应时间
但是时间片的大小不能简单的设置
时间片大:会导致响应时间长
时间片小:虽然响应时间小,但导致吞吐量小
这三个基本的调度算法都不完美,所以要写一个程序来综合这三个算法,从而实现调度
L15 一个实际的schedule 函数
schedule的目的是找到下一个进程,给switch_to去切换
counter代表优先级,每次schedule都是找到PCB中counter最大的就绪进程,然后给switch_to去执行
counter还是时间片
所以schedule既是基于counter的优先级算法,又是基于counter的轮转算法
在上图中,for循环出现的情况是所有counter都等于0,意味着所有就绪态的进程的counter都用完了。
当出现这种情况,执行for循环内部的代码。
这行代码将所有counter(时间片)为0的就绪态进程的counter(时间片)重新设置为初值(0/2+初值)
这行代码将所有未就绪的进程的时间片增加,使他们的counter(时间片/优先级)大于就绪态进程(未就绪进程的counter有初值不为0,因此counter/2+初值一定大于初值)
ps: (*p)->counter>>1(移位操作,意思是counter的数值除2)
这样做恢复了时间片,且让阻塞态恢复就绪态的counter比直接等待转就绪的大
counter能动态调整优先级
在阻塞队列里待的越久,counter(优先级)越大,所以阻塞的进程(I/O)再就绪后优先级高于非阻塞进程
问题:
时间片轮转调度,能够控制响应时间,那么counter现在在不停的变化,能否仍然保证响应时间的界限(有上限)?
回答:
可以,c(t) = c(t-1)/2 +p 是收敛的,一定会小于等于2p
使用counter的schedule方法:
- 保证了响应时间的界
- 经过IO以后,counter就会变大,IO越长,counter越大,照顾了IO进程
- 后台进程一直按照counter轮转,近似SJF(短作业优先)
- 每个进程只用一个counter变量维护
L16 进程同步与信号量
多进程同步就是进程之间的合作变得合理有序,是通过信号量实现进程的同步
让“进程走走停停”来保证多进程合作的合理有序
问题:
如果缓冲区满之后,进入了两个生产者,这两个生产者都会sleep。消费者执行一次循环,唤醒P1,消费者再次循环,P2无法被唤醒
解决办法:
信号升级为信号量
信号量的引入
引入一个值sem来记录有多少个进程在睡眠
sem = -2 指有两个进程在睡眠
sem = 2 指有两个资源可供进程使用
多个进程合作:
多个进程有时会合作完成一件事。
多个进程在合作过程中,推进必须合理有序。
怎么合理有序?
进程在执行到一定程度的时候需要停下来等待。
靠什么来等待?
进程中需要引入一个叫信号量的变量来控制该进程执行或等待。如果信号量的值为0或负值,则需要等待。
其他的进程同样参考信号量的值,如果信号量的值为0或负值,则唤醒。
进程之间的同步就是走走停停,合理有序。进程间参考信号量的值,睡眠或唤醒
信号量的实现
semphore 结构体,代表信号量
value 信号量的值
queue 阻塞队列,存放睡眠的进程
P(semphore) 消费资源:值减1,若值小于0,睡眠
V(semphore)产生资源(与P操作类似)
P,V都是系统调用
mutex互斥信号量,就是表示一次只能一个人用。要么是一个生产,要么一个消费。
当有人在共享缓冲区里,就会阻塞
L17 信号量临界区保护
共享数据不保护,就会出错,因为程序执行顺序是未知的
所以信号量需要临界区进行保护
直观的想法是,对共享变量empty上锁(原子操作)
P1在执行修改共享变量empty的时候,不允许P2执行对共享变量empty进行修改的代码
引入临界区
修改信号量的代码必须保证语意正确(程序执行顺序正确),所以要加以保护,因此修改信号量的代码必须是进程中的临界区代码
临界区一次最多只能让一个进程进入,因此在一个进程修改信号量时,不允许其他进程执行修改信号量的代码
如何找出进程中的临界区代码?(进入前,退出后,都需要操作)
临界区原则:
- 互斥进入(基本原则)
一个进程在临界区中执行,其他进程不允许进入 - 有空让进
有进程要求进入临界区时,尽快让进程进入 - 有限等待
进程从发出进入请求到允许进入,不能无限等待
多进程临界区算法:(面包店算法)
每个进程都获得一个序号,序号最小的进入
进程离开时序号为0
L17 死锁处理
什么情况会出现死锁
什么是死锁?
多个进程由于互相等待对方持有的资源而造成的谁都无法执行的情况叫死锁
死锁造成的情况:
进程无法向前推进,cpu没有程序去执行,计算机不工作,CPU利用率低
死锁的成因:
资源互斥使用,一旦占有别人无法使用
进程占有了一些资源,又不去释放,再去申请其他资源
各自占有的资源和互相申请的资源形成了环路等待
死锁的4个必要条件:
- 互斥使用(资源的固有特性)
- 不可抢占(资源只能自愿放弃,也是资源的固有特性)
- 请求和保持(进程必须占有资源,再去申请)
- 循环等待(在资源分配图中存在一个环路)
死锁的处理方式:
(1) 死锁预防
破坏死锁出现的条件(上述的后两个条件)
(2) 死锁避免
检测每个资源请求,如果会造成死锁就拒绝
(3) 死锁检测+恢复
检测到死锁出现时,让一些操作回滚,让出资源
(4) 死锁忽略
出现死锁可能性很低,在一些经常重启的PC上不做死锁处理
死锁预防方法:
- 在进程执行前,一次性申请所有资源
缺点:
- 需要预知未来,编程困难
- 许多资源分配后很长时间后才使用,资源利用率低
- 对资源类型进行排序,资源申请必须按序进行,不会出现环路等待
缺点:
- 造成资源浪费
死锁避免:
判断此次请求是否会引起死锁?
如果系统中的所有进程存在一个可完成的执行序列P1,…Pn,则称系统处于安全状态
银行家算法可以求出安全序列
T(n)=O(mn^2)
请求出现时,首先假装分配,然后调用银行家算法,看看会不会死锁,如果死锁,则拒绝这次申请
死锁避免能有效避免死锁,但是代价过大
死锁检测+恢复
每次申请都执行银行家算法,效率低下。因此发现问题再处理,能提升效率
但是进程回滚比较复杂,目前存在的问题有:
- 选择哪个进程回滚?
- 如何回滚?
死锁忽略
效率高,且并不容易出现