操作系统——线程调度
0.关注博主有更多知识
操作系统入门知识合集
目录
6.1线程调度概念
思考题:
6.2典型调度算法
思考题:
6.3Linux线程调度
6.1线程调度概念
在第四章曾经介绍过,线程是操作系统调度的基本单位,那么本篇就不再以进程的视角去看待调度问题了,读者可以把进程理解为只有一个线程的线程。我们曾经介绍过分时操作系统,那么当一个线程的时间片结束之后,操作系统需要选择一个新的线程来占有CPU。那么线程调度的定义就是:在合适的时候以一定的策略选择一个就绪线程投入运行。
操作系统需要考虑以下几个点:
1.调度时机:操作系统必须选择一个合适的时机去调度线程,这个时机可以是线程的时间片结束、可以是被其他线程抢占、亦或是被迫发生中断......
2.调度策略:操作系统需要选择一个合适的调度策略去调度线程,也可以理解为调度算法。每一种算法都有好有坏,每一种算法都能会对效率产生不同的影响。
3.调度的指标:线程的调度可不是一句话盖过这么轻松,操作系统必须考虑以下几个指标:
1)响应速度尽可能快:这就意味着当要发生调度时,调度的过程必须快。特别是针对用户与计算机交互的操作系统,内部一定会频繁地发生线程调度、切换,操作系统必须有能力让处理调度的时间尽可能短,从而使用户在宏观上感觉不到延迟。
2)线程的处理尽可能快:因为要频繁发生调度,这就意味着线程不能在CPU上待太长时间。也就是说无论是硬件还是操作系统,他们对任务的处理都必须尽可能快。
3)操作系统吞吐量尽可能大:在单位时间内,调度更多的线程,就意味这这款操作系统的吞吐量更大。
4)对所有线程要公平:不能让某个线程频繁被调度,也不能让某个线程迟迟不被调度。也就是说要避免调度带来的饥饿问题。
5)避免饥饿
6)避免死锁
......
这些调度的指标(原则),他们之间是相互存在矛盾的。例如1)和2),在操作系统当中存在成百上千个任务,所以他们之间要频繁地发生调度,就算调度的过程再快,它依然会影响线程的单位处理时间,也就是说本来在单位时间内可以处理1000条指令,但因为调度把单位时间给消耗了,此时只能执行400条指令了。所以在实际操作系统的设计中,会根据用户的习惯来增强某一侧重点:例如用户注重硬实时任务,那么操作系统的设计更加侧重指标1)。
思考题:
1.你认为Windows和Linux重点侧重哪些调度指标?
Windows和Linux都是面向用户的操作系统,它们必须优先考虑用户的需求,所以非常侧重调度的响应速度;其次,Windows是一款多任务多并发的操作系统,所以Windows也要确保具有较高的吞吐量。
6.2典型调度算法
先来先服务调度(First Come First Serve):
1.算法:操作系统按照作业创建的先后来挑选作业,先创建的作业优先被操作系统运行
2.特点:容易实现但效率不高。FCFS算法只考虑了作业的等候时间,而没有考虑运行时间的长短。也就是说一个晚来但是运行时间很短的作业可能要等待很长时间才可能被投入运行。因此FCFS算法不利于短作业。
短作业有限调度算法(Short Job First):
1.算法:参考运行时间,选取运行时间最短的作业投入运行
2.特点:容易实现但效率不高。SJF算法忽视了作业的等待时间,一旦有一个来的早但是很大的作业那么将长时间得不到调度,容易造成饥饿。
响应比高者优先调度算法:
1.响应比定义:作业的响应时间(等待时间+运行时间)与运行时间的比值。响应比=(等待时间+运行时间)/运行时间,可转化成响应比=1+等待时间/运行时间
2.算法:操作系统会计算每个作业的响应比,选择响应比最高的作业先投入运行
3.特点:
1)如果作业的等待时间相同,则运行时间越短的作业,其响应比越高,因此越容易被调度。因而有利于短作业。
2)如果作业的运行时间相同,则等待时间越长的作业,其响应比越高,因此越容易被调度。因而有利于等候长的作业。
3)对于运行时间长的作业,其优先级可以随等待时间的增加而提高,当其等待足够久的时候,就能够获得CPU。
优先数调度算法:
1.算法:根据线程优先数,把CPU分配给优先级最大的线程。优先数=静态优先数+动态优先数。
2.静态优先数:线程创建时就确定了,在整个运行期间不再改变
3.动态优先数:动态优先数在线程运行期间可以改变
4.静态优先数的确定:操作系统会基于线程所需资源的多少、运行时间的长短、类型(IO/CPU型任务、前台/后台线程、内核/用户线程)来确定静态优先数
5.动态优先数的确定:当线程占用CPU超过一定时常后,其动态优先数就会降低;当进行I/O操作后,就会发生阻塞,此时动态优先数就会升高;当线程等待超过一定时常时,动态优先数也会升高
循环轮转调度法:
1.概念:把所有就绪线程按先进先出的原则排成队列,新进来的线程添加到队列的末尾。线程以时间片q为单位轮流使用CPU。在CPU上运行一个时间片的线程被操作系统换下,排到队列的末尾,等候下一轮运行。队列与CPU的逻辑关系类似于环形:
2.优点:保证了公平性,让每个就绪线程都有平等机会获得CPU;保证了交互性,每个线程等待(N-1)*q的时间后就可以重新获得CPU。
3.时间片的设置:对于时间片q来说,如果q太大就会导致交互差,甚至退化成FCFS算法;如果q太小,线程之间频繁切换,操作系统的开销就会增大
4.改进:为了让循环轮转的调度算法更加灵活,每个线程的时间片都是可变的,除此之外,还可以组织多个就绪队列,每个就绪队列的管理策略都不同,这与先前讲过的阻塞队列是一个道理。
由此我们可以退回到操作系统的发展,分时系统的出现是计算机的一次"大革命",直到现在,分时系统依然具有优越性。
思考题:
1.静态优先数的三个因素是如何影响静态优先数的?
线程所需的资源越多,其静态优先数就越大,因为它要尽可能先运行完,尽早释放较多资源给后面的线程使用;运行的时间越长,其静态优先数越大,运行时间长的线程先运行完可以避免总是被运行时间短的线程"插队";偏I/O、处于内核态运行的线程,其静态优先数就越大,因为必须及时处理I/O,否则I/O中的数据可能丢失,而在内核态运行线程往往都是比较重要的任务。
2.动态优先数的三个因素是如何影响静态优先数的?
当在CPU上运行超过一定时间后,其动态优先数就会减小,因为要保证线程调度的公平,不能总是让一个线程占用CPU;当发起I/O操作到I/O操作结束时是一段相对较长的时间,其动态优先数应当被增大,因为长时间等待会发生饥饿问题。
6.3Linux线程调度
在这里需要注意一下,Linux实际上并没有线程这个概念,而是叫做轻量级进程。但我们处于用户视角,我们直接把轻量级进程当成线程来看待。读者需要注意,下面的内容有可能会出现线程,也可能会出现进程,但在调度的角度来看,它们都是一样的。
Linux线程类型:
1.普通线程:采用动态优先级来调度,调度程序(调度模块)周期性地修改优先级(目的是避免饥饿)
2.实时进程:采用静态优先级来调度,有用户预先指定优先级,在其整个运行生命周期中不会改变
Linux线程的优先级:
1.静态优先级:线程创建时由用户指定(用户不指定也可以是默认值)
2.动态优先级:
1)在线程运行期间可以按照调度策略来动态改变
2)非实时线程(普通线程)采用动态优先级,其优先级由操作系统的调度模块计算
3)只要线程占用CPU,优先级就会随着时间流逝而不断减小
4)task_struct(进程控制块,因为Linux只有轻量级进程,所以可以把它看成线程控制块)中的counter成员表示动态优先级
调度策略:
1.PCB的policy成员:在Linux的task_struct(PCB)中有一policy成员指明了进程(线程)调度策略(Linux没有线程,只有轻量级进程):
2.实时进程(线程)的调度策略:
1)SCHED_FIFO(先进先出):与FCFS调度算法一样,当前实时进程(线程)一直占用CPU直至退出或阻塞或被抢占,再就绪时会被操作系统添加到相同优先级的就绪队列末尾(Linux没有就绪态,但是有就绪队列)。
2)SCHED_RR(时间片轮转):与其他实时进程(线程)以循环轮转调度法的方式共同使用CPU,确保了同优先级的多个进程(线程)能够共享CPU
3.普通进程(线程)的调度策略:使用SCHED_OTHER动态优先级的策略来调度普通进程(线程),其task_struct中的counter成员表示进程(线程)的动态优先级
4.调度策略的改变:系统调用sched_setscheduler()能够让用户改变调度策略,并且实时进程的子孙进程也是实时进程。
调度的依据:
主要依赖task_struct中有关调度策略的属性:
1)policy:这个成员记录了调度的策略,用来区分实时进程(线程)和普通进程(线程)
2)priority:所有的进程(线程)的静态优先级
3)rt_priority:实时进程(线程)特有的优先级,rt_priority=priority+1000
4)counter:进程(线程)能够连续运行的时间(实际上动态优先级就可以理解为时间计数器,在CPU上的时间越长其值就越小,最后到0便让出CPU)
动态优先级counter:
1.counter值的含义:进程(线程)能够连续运行的时间,单位时间为时钟滴答tick。假设时钟中断周期tick为10ms,那么当counter=60时,那么该进程(线程)能够连续运行600ms。较高优先级的进程(线程)的counter值一般都比较大。虽然说counter值表示进程(线程)能够连续运行的时间,但在Linux中,把counter看作动态优先级。
2.counter的初值与priority有关:普通进程(线程)创建时counter的处置为priority的值
3.counter的改变:时钟中断tick时,当前进程(线程)的counter值减1,直到0时放弃CPU
4.子进程创建时counter:新建子进程的counter从父进程counter中分一半过来。也就是说,假设父进程的counter值为60,则其创建的子进程counter值为30,其本身的counter修改为30。这样做的目的是防止用户无限制地创建子孙进程而长期占用CPU资源
调度时机:
1.用户态进程(线程)只能通过中断来发生切换(只能被动发生切换):
1)例如发生时钟中断、I/O中断、系统调用等不可预知的中断,然后CPU陷入内核处理这些中断处理程序,在处理的过程当中调用schedule()接口实现进程(线程)切换
2)调用schedule()接口时CPU不能处于内核态,因为这样就相当于赋予用户态进程(线程)很高的权限,对操作系统不安全。在CPU处理中断处理程序要调用schedule()接口之前必须先发生一次态的转换,由内核态返回到用户态,在用户态中调用schedule()接口,并且需要根据need_resched标记来检测是否需要需要调用schedule()接口
2.内核线程可以直接调用schedule()接口实现进程(线程)切换,这是一种内核主动调度的情形
3.进程(线程)的调度实质就是发生一次进程(线程)切换,切换的过程与中断很像,但是存在一定的差别:发生中断时,操作系统需要保护CPU当前运行进程(线程)的上下文,然后装入中断处理程序的入口地址;而发生进程(线程)切换时,操作系统不仅需要保护CPU当前运行的进程(线程)的上下文,还要恢复被调度而来的进程(线程)的上下文。