文章目录
- CPU管理的直观想法
- 多进程图像
- 用户级线程
- 内核级线程
- 内核级线程实现
- 操作系统之树
- CPU 调度策略
- 一个实际的schedule 函数
- 进程同步与信号量
- 信号量临界区保护
- 信号量的代码实现
- 死锁处理
CPU管理的直观想法
- CPU的工作原理: 自动的取值执行,给了初始地址,之后CPU就自动取值执行
- IO 指令非常慢,但是计算指令非常快
- 提出问题,假设多条计算指令和 一条IO指令 形成 一段代码,当顺序执行的时候,运行到IO指令,需要大量的时间操作磁盘,这个时候CPU会做什么?是直接等待还是做什么操作?
- 答案是 当运行到 IO操作的时候,CPU会切出去做其他操作,当 IO操作完成之后,发出中断,CPU又会返回接着做剩下的操作,这样的操作可以大大提升CPU的利用率
- 由此可见CPU是 多道程序 交替执行,下面举个例子
- 单道程序只能顺序执行,所以CPU完成两个作业的时间是80
- 多道程序是两个作业交替执行,当操作第三方设备时,CPU可以执行别的作业,当两个作业都需要CPU时,只能顺序执行,所以多道程序完成两个作业的时间是45
- 由此可见CPU多道程序的利用率非常高
- 当多个程序运行时,CPU如何进行并发操作呢?
- 答案是 多个程序同时运行,不过当CPU运行程序1 n个时间 的时候,又去运行程序2 n个时间,这样就是并发操作
- 但是还有个问题,CPU运行程序1 跳到 程序2的时候,如何记录运行到哪个指令,这个时候就需要一个记录数据结构PCB来记录。
- 由此可见,运行的程序跟静态的程序是有一定区别的,还是需要一定的包装,所以这个时候就引入一个新的概念,进程,进程就是进行中的程序
多进程图像
- 用户操作操作系统,本质就是操作多个进程
- 进程的顺序:
- 启动操作系统
- 在main函数中 fork() 创建第一个进程
if(!fork()) {init();}
Shell ( windows桌面 ) - shell()进程里面就可以接着启动其他进程,进程完成后,返回Shell()进程
- 多进程如何组织?
- 核心就是 Process Control Block 结构,用来记录进程信息的数据结构
- 多进程有哪些状态?:
- 就绪态:就绪态 -》运行态
- 运行态:运行态 -》 阻塞态,运行态 -》就绪态
- 阻塞态:阻塞态 -》 就绪态
- 多进程如何切换?看下面的例子
- 多进程调度的优先级又是怎么样的?
- FIFO :先到先得
- Priority:设定优先级,优先级高的优先
- 多进程具体是如何切换?看下面的代码,将进程的信息进行切换
- 但是多个线程在内存中,会存在对同一块资源进行操作的问题,怎么样才能保证资源操作之后准确性的问题呢?
- 限制对一块资源的读写
- 多进程如何进行合作呢?
- 生产者-消费者实例
- 上面是消费者和生产者的运行过程,但是会有问题,下列的问题
- 正常来说 counter应该是5 但是最后得出是4,显然是不对的,所以这时候就需要多进程同步了,合理推进多进程的顺序。
- 生产者-消费者实例
- 核心就是 Process Control Block 结构,用来记录进程信息的数据结构
用户级线程
- 上一节说到进程切换,这一节我们需要讲解线程,首先来想一个问题,如果进程之间只是一个函数进行切换,然后就需要动用太多资源的切换是不是不太值得,有没有可能用一个更小的单位去切换,代价会小一点。
- 所以这个时候就引出了线程的概念,保留了并发的优点,避免了进程切换的代价
- 线程切换,只是指令之间的切换,并不涉及共享资源的之间的切换
- 下面举个例子,来说明如何进行线程之间的切换:GetData线程 和 Show线程
- 其中切换的方法就是 yield 方法,所以线程切换最核心的部分就是 yield
- 下面就是切换举例:
- 首先执行 A 函数,运行到 B 函数的时候,需要将原本的位置保存起来,就是将 104 放到栈中
- 然后开始执行 B 函数,运行到 yield 函数 又要进行跳转,所以将 204 也放到栈中
- 这个时候已经切换到 C 函数,C 函数继续执行,又调用了 D 函数,所以将 304 放到栈中
- 然后开始执行 D 函数,D 函数执行使用 yield 函数,又跳转到 B 函数,所以将 404 放到栈中
- 然后继续执行 B 函数,B 函数执行完之后 ,就会 调用 ret 函数,对应的栈就会弹栈,而栈中的 404 就会弹出,这个时候就不对劲,为什么是 404 弹出呢? 明显不对,但是又怎么解决呢?
- 补充:
- 很显然 A 和 B 函数应该是一个进程 ,C 和 D 函数应该是另一个进程,所以如果两个进程的线程都公用一个栈的话,就会出现刚才的问题,ret 指令 之后,会从另一个进程开始执行
- 解决方案:一个执行序列 就 拥有自己独立的栈,使用TCB 和 栈 相互配合
- 现在重写了yield 函数,将两个指令序列之前的跳转隔离开,但是有个问题,按照上图执行的效果,204是弹不了栈的,因为 执行到 jmp 204 的时候,就会跳转,所以 204 永远不会弹出来。但如果把 jmp 204 删除呢?括号之后就会执行 ret 指令 ,并且现在的栈是 TCB1的栈,204 是可以弹出的,然后 执行会顺着 204 的位置继续执行。
- 现在重写了yield 函数,将两个指令序列之前的跳转隔离开,但是有个问题,按照上图执行的效果,204是弹不了栈的,因为 执行到 jmp 204 的时候,就会跳转,所以 204 永远不会弹出来。但如果把 jmp 204 删除呢?括号之后就会执行 ret 指令 ,并且现在的栈是 TCB1的栈,204 是可以弹出的,然后 执行会顺着 204 的位置继续执行。
- ThreadCreate 函数:创建 线程 的必须品
- malloc :申请一段内存用来给TCB
- 整个流程:
- 什么是用户级线程?
内核级线程
- 用户级线程与内核级线程的区别:
- 用户级线程用的是两个栈,内核级线程是两套栈
- 用户栈和内核栈之间的关联:
- 内核栈中的ss和sp都存着用户栈中的指令地址,所以int的时候进入内核栈,iret就弹出返回用户栈
- 内核栈中的ss和sp都存着用户栈中的指令地址,所以int的时候进入内核栈,iret就弹出返回用户栈
- 下面举个例子:
- switch_to :仍然是通过TCB找到内核栈指针,然后通过ret切到某个内核程序,最后再用cs : pc 切到用户程序
- 各个线程对比:
内核级线程实现
- 故事从一个代码开始
- A 方法 运行到 fork() 的时候,就会压栈,把对应的地址压倒内核栈中,然后等 INT 0X80 指令结束后,就会通过内核栈中的地址返回用户栈对应的位置
- 另一个故事
操作系统之树
- 通过实现一个功能,再整体看看如何设计操作系统程序,实现在页面上打印ABABBBBAAA等数据
- 第一步从用户代码开始,首先执行 fork()函数创一个子进程A
- 用汇编的语言转换就变成下面那样
- 通过 int 0X80 进入到内核
- 然后执行 system_call ,然后执行 sys_fork
- 一直运行,然后执行 coy_process ,在内核中做一个子进程
- 做出一个新的PCB
- 做出一个新的栈
- 执行完 copy_process 就要开始执行 ret 开始回退了
- 继续执行代码,发现又有一个 fork()函数(创建一个子函数),再一次产生一个子进程B(一个PCB + 内核栈)
- 然后父进程继续执行,执行wait()函数,等待
- 将父进程状态置为等待
- 父进程阻塞
- 然后父进程就要执行 schedule()函数,选择子进程函数,通过运行 switch_to() 函数 进行切换
- switch_to 切换 到 A
- 把 CPU 的内容,放置到 父进程的 tss 上
- 把 子进程 A 的 tss 内容,放置到 cpu 上
- 开始执行 子进程A
- 这个时候有个问题,就是屏幕上只会打印A ,不会打印B ,因为子进程没有切换程序,那怎么样才能切换的呢?
- 这时候就需要时钟中断,每次一到时钟中断就切换到B
- 这时候就需要时钟中断,每次一到时钟中断就切换到B
- 这样就会完成 屏幕上交替 执行 A 和 B
CPU 调度策略
- CPU 调度是如何调度的呢?,先看下面的例子
- CPU 调度最直观的想法 就是 就绪队列中有很多进程究竟选择谁来执行呢
- CPU 调度有两种思想:
- FIFO :先到先执行
- Priority :选择优先权大的,先执行
- 面对诸多场景,如何设计调度算法?
- 就是需要让进程满意,如何让进程满意呢?
- 尽快结束任务:周转时间(从任务进入到任务结束)少
- 用户操作尽快响应:响应时间短
- 系统内耗时间少:吞吐量
- 但是想要完成上面那几点需要考虑的因素有很多
- 第一种CPU 的 调度算法:FCFS 先来先服务
- 但是第一种方式 周转时间太高了,假设 将 P3这种短作业的提前,周转时间就会变小,所以这样就引出了优化的 调度算法:SJF 短作业优先
- 第二种CPU 的 调度算法: 用时间片来轮转调度
- 每一定时间片运行一个任务,到了时间就切换
- 每一定时间片运行一个任务,到了时间就切换
一个实际的schedule 函数
- 操作系统的调度函数
- 第一个if 的目的就是 找到最大的 counter 的进程,这就是典型的优先级算法
- 第二个 for 的 目的,就是把阻塞IO的线程优先级提高
- Counter的作用:时间片
- counter 的另一个作用:优先级
- counter 总结:
进程同步与信号量
- 如何使得多个进程执行的合理有序?
- 例子:多个进程共同完成一个任务
- 实例1 : 司机操作完之后,会给一个信号给售票员,售票员收到之后,开始执行操作,执行完一个操作,也发送一个信号给司机,则司机执行下一个操作,通过这个例子可以看出,进程之间执行合理可以通过相对应的信号来沟通
- 实例1 : 司机操作完之后,会给一个信号给售票员,售票员收到之后,开始执行操作,执行完一个操作,也发送一个信号给司机,则司机执行下一个操作,通过这个例子可以看出,进程之间执行合理可以通过相对应的信号来沟通
- 生产者-消费者实例
- 当生产者把缓冲区占满了之后,就要通知消费者开始消费缓冲区的数据
- 但是光通知还不够,还需要让生产者不再往缓冲区里面添加数据,这个时候就需要暂停生产者进程
- 但是只发送信号,还不能解决全部问题,因为信号量太少了,所以还需要更加丰富的信号量来解决
- 通过信号量解决生产者 - 消费者问题
- empty 信号量 是缓冲区
- mutex 信号量 是文件是否有人再操作
信号量临界区保护
- 如果不对信号量进行保护,多个进程进行操作,就有可能发生错误
- 所以这种问题得主要是竞争关系
- 而解决这个竞争条件最直观的想法是给共享资源在写操作的时候上锁
- 临界区:一次只允许一个进程进入得该进程的那一段代码
- 临界区代码的保护原则:
- 互斥性:当一个进程在临界区执行,则其他进程不允许进入
- 互斥性:当一个进程在临界区执行,则其他进程不允许进入
- 临界区代码保护方法:轮换法
- 进程轮流进去
- 满足互斥性
- 问题:如果P0 执行完临界区了以后,turn=1了,但是P1 不执行临界区,所以turn不会等于0,就会导致p0一直是用不了
- 临界区代码保护方法:标记法
* 由轮换法导致问题,我们想到一个新的方法,就是标记法,就是假设P0进程想进去临界区,设置他的状态为True,并且判断P1是否也想进入临界区,如果执行完临界区的代码,则把状态变成fals
* 问题:P0进程判断的时候获取的P1的状态的时候有延迟,则两边都过不去
- 临界区代码保护方法:非对称标记法
- 结合标记和轮转的思想
- 结合标记和轮转的思想
- 临界区代码保护方法:面包店算法
信号量的代码实现
- 对于信号量的代码实现:
死锁处理
- 生产者和消费者会因为信号量的争夺产生死锁
- 实例
- 假设调换位置
- 这样会形成环路,所以我们将这种多个进程由于互相等待对方持有的资源而造成的谁都无法执行的情况叫死锁
- 死锁的4个必要条件:
- 互斥使用
- 不可抢占
- 请求和保持
- 循环等待
- 死锁的处理方法:
- 死锁预防:破坏死锁出现的条件
- 死锁避免:检测每个资源请求,如果造成死锁就拒绝
- 死锁检测+恢复:检测到死锁出现时,让一些进程回滚,让出资源
- 死锁忽略:就好像没有出现死锁一样
- 银行家算法: