【基础学习】操作系统学习笔记 - 进程与线程:实现内核级线程切换、CPU调度策略、调度算法实例

本文详细介绍了操作系统中CPU调度的过程,从早期的单进程到多进程、用户级多线程再到内核级多线程的演变。讨论了如何通过栈和TCB(线程控制块)管理线程切换,并分析了如何利用时钟中断实现调度算法。最后,文章探讨了不同的调度策略,如FIFO、SJF和优先级调度,以及Linux0.11的调度算法,强调了调度策略在平衡响应时间和吞吐量中的作用。
摘要由CSDN通过智能技术生成

在中国大学MOOC上学习操作系统
希望看视频可以直接点击 哈工大-操作系统课程MOOC

操作系统的那棵“树”(The Tree of OS)

ideas
  1. CPU运转,需要将一段程序然后设好PC交给CPU去执行。将CPU用于执行程序
    在这里插入图片描述
  2. 但是CPU并没有好好运转,因为它遇到I/O的阻塞。
    在这里插入图片描述
  3. 解决这个问题,有了多进程,我们让PC跳转,就可以并发地让CPU执行程序。多进程出现了
    在这里插入图片描述
  4. 我们使用栈结构来完成跳转的操作
    在这里插入图片描述
  5. 结果遇到了一个栈 + Yield 造成混乱的问题,怎么让PC回到204代码段?
    在这里插入图片描述
  6. 使用两个栈+两个用户级的TCB,就可以解决跳转混乱的问题。用户级多线程出现了
    在这里插入图片描述
  7. 一直在用户态,没有深入到多核心,当内核上出现了I/O阻塞,CPU依然在空等。
    在这里插入图片描述
  8. 所以我们需要在内核级别实现内核栈的切换。内核级多线程出现了
    在这里插入图片描述
    1. 线程1从A开始,调用B,并压104代码段入线程1用户栈
    2. 线程1执行B,调用read,内核被阻塞,准备切换内核,进入内核态,并将压204代码段入线程1内核栈,线程1用户栈的两个指针同时也被保存在线程1内核栈中,并最终将信息保存在线程1TCB中。
    3. 进入线程2,调用D,并压304代码段入线程2的用户栈。
    4. 执行D,调用read,内核被阻塞,准备切换内核,将404代码段压入线程2内核栈,并保存各种信息,准备切换内核。
    5. 当线程1或线程2完成读取操作后,CPU立刻拉进来它们的TCB,继续执行直到结束后切换至另一个TCB,最后两个任务都完成。
  9. 至此,从让CPU执行一直到内核级多线程的idea都有了。
实现

尝试使用代码实现交替在屏幕上打出A和B(这也是Linux 0.01 版本实现的内容)

  1. 先实现用户级的A、B交替
    在这里插入图片描述
  2. 使用代码表达上面的内容
    在这里插入图片描述
    1. 将__NR_fork内容放进%eax
    2. 使用int 0x80指令中断,进入100号地址
    3. 将%eax内容(也就是int 0x80返回的值)放入res
    4. 比较res和0
    5. 不等于时(父进程),就去执行208号地址指令;等于时,就去执行200号地址的指令,打印A,并跳转至200(实现循环打印)。
  3. INT进入内核
    在这里插入图片描述
    1. 进入内核后,形成内核栈和用户栈
    2. 执行sys_call(系统调用)
  4. 开始sys_fork
    在这里插入图片描述
  5. 开始copy_process
    做出一个新的PCB在这里插入图片描述
    把父进程的PCB大部分都写进新PCB里,然后eip(PC)设为100(也就是我们要打印A的那部分指令存的位置),并保存当前栈顶指针到TSS,TSS中eax置为0。
  6. 开始返回
    创建好了PCB后,copy_process返回,上层的sys_fork向外返回,直到system_call(意味着sys_call_table语句,也就是上面使用fork以后,system_call会查表调用fork的系统调用)。system_call开始根据当前PCB是否就绪,如果就绪,开始调度,调度后中断返回,然后退回至用户级线程,此时int 0x80也结束。PC跳回100号地址开始打印A
    在这里插入图片描述
  7. 又fork了一个打印B的线程,再fork、再int 0x80、再产生一个PCB和内核栈。
  8. 出现了两个进程/线程以后,就绪队列就有了两个可以执行的任务。
    在这里插入图片描述
  9. 父进程(main)调用wait()以后两个任务按照调度规则交替进行。因为wait系统调用中使用了schedule函数
    在这里插入图片描述
  10. schedule代码可以根据自己的理解写。比如这里就是FIFO调度。
    在这里插入图片描述
  11. 开始实现切换进程/线程的逻辑
    在这里插入图片描述
    实际上就是把现在的CPU状况“照一张照片”(TSS),然后将新的“照片”(TSS)给CPU,完成切换。
  12. 根据刚刚A的代码,可以看到首先访问100号地址,进行一系列的比较之后进入200号地址,然后就一直开始循环打印A。
    在这里插入图片描述
  13. 问题来了,怎么让B打印出来?
    我们必须想办法让schedule算法再次执行一下,以便切换到线程B。
    schedule是一个系统调用,我们要想办法进入内核态。
    看来,中断是一个不错的办法。
    那么应该用那种中断?
    时钟中断。
    在这里插入图片描述
    do_schedule:
    当定时器大于0时,不做处理,直接返回。
    当定时器减到0时,就会触发调度
    调度
  14. schedule + switch_to
    在这里插入图片描述
  15. 调度完成
    在这里插入图片描述
    A的时间片用完了,所以在100号地址的指令中,res不再等于0(进入了非就绪态),跳到208号地址,一直循环打印B。
  16. 接着B又执行了一段时间,再一次schedule+switch_to
    自此,A和B开始交替打印。

CPU调度策略

当进程阻塞时,CPU该如何选择切换的那个进程,切换的策略实际上就是CPU的调度策略。

直观想法
  1. FIFO:
    1. 谁先来就先调度谁:简单有效,比如食堂、银行这样的排队
    2. 但是如果只是去银行简单地询问业务的人会不会吃亏?
  2. Priority
    1. 任务短的d可以适当优先
    2. 但是这个人询问的时间越来越长怎么办?降低优先级!
    3. 如果时间很长是因为客户需要填写一个很长的表(长I/O)该怎么办?
如何设计好调度算法?

在这里插入图片描述
可以总结出调度算法的评价策略:

  1. 周转时间(执行时间)
  2. 响应时间(等待时间)
  3. 系统吞吐量(完成任务数量)
如何做到合理?

在这里插入图片描述

  1. 吞吐量和响应时间之间有矛盾
    响应时间小意味着切换次数多,切换次数多意味着CPU的效率降低了,将造成吞吐量小
  2. 前台任务和后台任务的关注点不同
    前台任务更关注响应时间(用户需要快速反馈),后台任务更关注周转时间(不能死等着)
  3. I/O约束型任务和CPU约束型任务有各自的特点
    一般让I/O先执行,因为I/O基本上就占用一小段CPU时间,然后就要去被I/O阻塞,CPU控制权就交给了其他任务。其实一般的前台任务都是I/O约束型任务,后台任务一般都是CPU约束型任务。
  4. 算法实现要尽量“漂亮”(老师后来提到的)
    算法应当用最轻量的数据结构和最快速的逻辑实现,不能调用一下这个算法就占用很大的内存和CPU时间。
FCFS(First Come First Serve)

先来先服务,最简单的调度算法
在这里插入图片描述

SJF(Shortest Job First)与RR(Roud-robin)

最短任务优先在这里插入图片描述
吞吐量变大,响应时间变小,平均周转时间维持不变。
在这里插入图片描述
RR:轮询调度(时间片轮转),是FCFS和SJF的折中,降低了响应时间。
当时间片太长时:比如给一个无限大的时间片,那RR就是FCFS算法,响应时间又变长了。
当时间片太小时:比如给0.0000001ms的时间片,那每个任务就光切换了,吞吐量变低。
所以在上图的任务中,可以折中地将时间片调到10-100ms,达到一个不错的效果。

PSA(Priority Scheduling Algorithm)

优先级调度
在这里插入图片描述
当响应时间和周转时间同时有要求该怎么办?
直观想法:定义两个队列,一个前台任务队列,一个后台任务队列,前台RR后台SJF,并优先执行前台任务。
再次推广一下,我们引入了优先级的概念,也就是前台任务优先级更高,后台任务优先级低一些。
在这里插入图片描述
静态优先级的使用(比如一直存在前台任务),会造成后台任务饥饿,当后台任务已经因为饥饿而超出我们可以接受的时间时,这个后台任务就饿死了。
显然我们应该使用动态的优先级来处理优先级调度的问题。但依然有问题没有解决:

  1. 后台任务优先级动态变高,但后台任务一旦执行,前台的响应时间又拉长了。
  2. 前后台都是用RR,那我们何必要设计优先级调度呢?后台如何实现SJF?前台任务不还是拉长了响应时间?
  3. 怎么知道哪些是前台任务,哪些是后台任务?fork时通知吗?
  4. gcc也需要交互,ctrl+c中断执行怎么实现?word也依然会批处理,比如ctrl+f搜索,这些问题怎么解决?
  5. SJF算法如何判断作业的长度?

调度算法实例

Linux0.11的调度算法
在这里插入图片描述

  1. 设置一些变量 c为-1,next为0,i指向NR_TASKS的末尾
  2. p指向PCB数组的末尾
  3. p借助i指针,获取当前PCB的数据
  4. 开始遍历PCB数组
    1. 如果PCB的状态为就绪(TASK_RUNNING)并且PCB的counter>c,c就等于此时PCB的counter,next指向i。就是求最大的counter和其关联的PCB
    2. 如果这个counter不为0,就跳出循环,即找到了这个进程。
    3. 否则执行修改所有进程的counter的:所有counter除以2(右移一位)并加上counter的初始值(priority)
  5. 根据next直接切换

可见counter就起到了优先级和时间片的作用。而修改counter的步骤,会使得阻塞后的进程一定会优先执行,因为时间片和优先级都增加了(初值+当前的一半)。

为什么说counter起了时间片作用

在这里插入图片描述
在前面看过的时钟中断代码里,我们知道do_timer会不断减小当前进程的counter值,最终将current->couter置为0,显然这个counter就是个时间片。

为什么说counter起了优先级作用

在这里插入图片描述
显然在schedule代码中,程序是通过比对counter来找到那个counter最大的PCB并最终切换,这正符合着优先级调度的定义。
并且在程序中,程序会动态地调整这个counter值使得阻塞后的进程优先级一定大于所有的进程(因为在当前时间片一半的基础上又加了一个完整时间片),这里就运用了动态优先级调度策略。
为什么要对阻塞的进程调整高优先级呢?因为前台任务的特征正是I/O密集型任务,这样做保证了前台任务的优先执行,并且随着阻塞的时间越长,优先级越高。

counter起了哪些作用在这里插入图片描述
  1. counter保证了响应时间的边界
    在最开始的时候counter被设置成了priority
    c0 = p
    c1 = c0 + p = (1/2)p + p = (3/2)p
    c2 = c1 + p = (3/4)p + p = (7/4)p

    cn收敛于2p,保证了最长周转时间有界
    以上公式来自于这段代码:
    (*p)->counter = ((*p)->counter>>1) + priority
    使用右移代替除以2,运算更快
  2. 照顾了前台进程
    当I/O结束以后,可能这时CPU已经调度了很多次了,根据上面算法CPU每调度一次,每个counter都会加上counter的一半再加一个counter,counter会以这种方式开始增长。换句话说,当I/O阻塞的时间越长,它的counter就越大,极大照顾了I/O阻塞的任务,而前台任务才是I/O密集型的,这样就照顾了前台进程的响应时间。
  3. 后台进程一直按照counter轮转,这个算法的周转时间近似于SJF的周转时间
  4. 每个进程只需要维护一个轻量的counter变量,简单且高效。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值