第4章 进程调度
- 调度:调度是一个平衡的过程。一方面,它要保证各个运行的进程能够最大限度的使用CP;另一方面,保证各个进程能公平的使用CPU。
- 调度功能:决定哪个进程运行以及进程运行多长时间。
- 调度实现原理:与进程的优先级有关
- Linux上调度实现的方法:O(1)的调度算法
- 调度相关的系统调用
4.1多任务
多任务系统可以划分为两类:非抢占式多任务( cooperative multitasking )和抢占式多任务(preemptive multitasking)。像所 有Unix 的变体和许多其他现代操作系统一样, Linux 提供了抢占式的多任务模式。在此模式下,由调度程序来决定什么时候停止一个进程的运行, 以便其他进程能够得到执行机会.这个强制的挂起动作就叫做抢占( preemption ).进程在被抢占之前能够运行的时间是预先设置好的,而且有一 个专门的名字,叫进程的时间片( timeslice )。时间片实际上就是分配给每个可运行进程的处理器时间段。有效管理时间片能使调度程序从系统 全局的角度做出调度决定,这样做还可以避免个别进程独占系统资源.当今众多现代操作系统对程序运行都采用了动态时间片计算的方式,并且引入 了可配置的计算策略.不过我们将看到, Linux 姐一无二的“公平”调度程度本身并没有采取时间片来达到公平调度。
4.2Linux的进程调度
0(1)调度程序一一它是因为其算泌的行为而得名的θ 。它解决了先前版本Linux 调度程序的许多不足,引入了许多强大的新特性和性能特征。
这里主要要感谢静态时间片算法和针对每一处理器的运行队列,它们帮助我们摆脱了先前调度程序设计上的限制。
0(1)调度器虽然在拥有数以十计(不是数以百计〉的多处理器的环境下尚能表现出近乎完美的性能和可扩展性,但是时间证明该调度算提对于
调度那些响应时间敏感的程序却有一些先天不足。这些程序我们称其为交互进程一一它无疑包括了所有需要用户交互的程序。
4.3策略
策略决定调度程序在何时让什么进程运行
- 1/0 消耗型和处理器消耗型的进程
- 进程可以被分为uo 消耗型和处理器消耗型。前者指进程的大部分时间用来提交1/0 请求或
是等待ν0 请求。处理器艳费型进程把时间大多用在执行代码上。除非被抢占,否则它们通常都一直
不停地运行,因为它们没有太多的1/0 需求。 - 调度策略通常要在两个矛盾的目标中间寻找平衡:进程响应迅速(响应时间短〉和最大系统
利用率(高吞吐量)。
- 进程可以被分为uo 消耗型和处理器消耗型。前者指进程的大部分时间用来提交1/0 请求或
- 进程优先级
- nice 值,色的范围是从-20 到+19,默认值为0 :越大的nice 值意味着更低的优先级
- 实时优先级,其值是可配置的,默认情况下它的变化范围是从0 到99 (包括0和99 )。与nice 值意义相反,越高的实时优先级数值意味着进程优先级越高.
4.4Linux调度算法
- 调度器类
- Linux 调度器是以模块方式提供的,这样做的目的是允许不同类型的进程可以有针对性地选
择调度算哉。
这种模块化结构被称为调度器类( scheduler classes ),它允许多种不同的可动态添加的调度
算能并存,调度属于自己范畴的进程。每个调度器都有-个优先级,基础的调度器代码定义在
kemel/scbed.c 文件中,它会按照优先级顺序遍历调度类,拥有一个可执行进程的最高优先级的调
度器类胜出,去选择下面要执行的那一个程序。
完全公平调度( CFS )是一个针对普通进程的调度类,在Linux 中称为SCHED_NORMAL
〈在POSIX 中称为SCHED_OTHER) , CFS 算捧实现定义在文件kemel/scbed_ fair.c 。
4.5Linux调度的实现
- 时间记账
- CFS 不再有时间片的概念,但是它也必须维护每个进程运行的时间记账,因为它需要确保
每个进程只在公平分配给它的处理器时间内运行。CFS 使用调度器实体结构(定义在文件<linux/
sched.h>的struct_sched _entity 中)来追踪进程运行记账 - CFS 使用vruntime 变量来记录一个程序到底运行了多长时间以及它还应该再运行多久。
定义在kemeVsched_fair.c 文件中的update_curr()函数实现了该记账功能。 - update_ currO 计算了当前进程的执行时间,并且将其存放在变量delta_exec 中。然后它又将
运行时间传递给了一up也te_curr(),由后者再根据当前可运行进程总数对运行时间进行加权计
算。最终将上述的权重值与当前运行进程的vruntime 相加。
- CFS 不再有时间片的概念,但是它也必须维护每个进程运行的时间记账,因为它需要确保
- 进程选择
- CFS 的进程选择算法可简单总结为“运行rbtr回树中最左边叶子节点所代表的那个
进程”。实现这一过程的函数是_pick_next_ entityQ,它定义在文件kemel/sched_剑”中。 - 如何将进程加入rbtree 中,enqueue_entity()函数实现了这一目的
- CFS 的进程选择算法可简单总结为“运行rbtr回树中最左边叶子节点所代表的那个
- 调度器入口
- 进程调度的主要入口点是函数schedule(),它定义在文件kemel/sched .c 中。它正是内核其他
部分用子调用进程调度器的入口。它会调用pick_next_task() (也定义在文件kernel/sched.c 中)。pick_
next_task()会以优先级为序,从高到低,依次检查每一个调度类,并且从最高优先级的调度’类
中,选择最高优先级的进程。
- 进程调度的主要入口点是函数schedule(),它定义在文件kemel/sched .c 中。它正是内核其他
- 睡眠和唤醒
- 休眠〈被阻塞)的进程处于一个特殊的不可执行状态。
- 休眠通过等待队列进行处理。等待队列是由等待某些事件发生的进程组成的简单链表。内
核用wake_queue_ head _t 来代表等待队列。等待队列可以通过DECL成立WAITQUEUE()静态创
建,也可以由init_waitqueue _head()动态创建。 - 进程通过执行下面几个步骤将自己加入到一个等待队列中:
- 1 )调用宏DEF刷E_WAIT()创建一个等待队列的项。
2 )调用add_wai!_ queue()把自己加入到队列中。该队列会在进程等待的条件摘足时唤醒它。
当然我们必须在其他地方撰写相关代码,在事件发生时,对等待队列执行wake_u以)操作。
3 )调用prepare_to_ wait()方站将进程的状态变更为TASK_剧TERRUPTIBLE 或TASK_
UNINTERRUPTIBLE。而且该函数如果有必要的话会将进程加回到等待队列,这是在接下来的
循环遍历中所需要的。
4 )如果状态被设置为TASK_INTERRUPTIBLE ,贝I]信号唤醒进程。这就是所谓的伪唤醒
〈唤醒不是因为事件的发生),因此检查并处理倍号。
5 )当进程被唤醒的时候,它会再次检查条件是否为真。如果是,它就退出循环:如果不
是,它再次调用schedule()井一直重复这步操作。
6)当条件满足后,进程将自己设置为TASK_RUNNING 并调用finish_wait()方挫把自己移
出等待队列。
如果在进程开始休眠之前条件就已经达成了,那么循环会退出,进程不会存在错误地进入休
眠的倾向。需要注意的是,内核代码在循环体内常常需要完成一些其他的任务,比如,它可能在
调用schedule()之前需要释放掉锁,而在这以后再重新获取它们,或者响应其他事件。
函数inotify_r,臼d(),位于文件fs/notify/inotify/inotify_ user.c 中,负责从通知文件描述符中读
取信息。
- 1 )调用宏DEF刷E_WAIT()创建一个等待队列的项。
- 唤醒操作通过函数wake_upO 进行,它会唤醒指定的等待队列上的所有进程。它调用函
数衍y_to_wake_upO,该函数负责将进程设置为TASK_RUNNING 状态,调用enqueue_task()将
此进程放入红黑树中。
4.6抢占和上下文切换
上下文切换,也就是从一个可执行进程切换到另一个可执行进程,由定义在kernel/ sched.c 中
的context_switch()函数负责处理.每当一个新的进程被选出来准备投入运行的时候, schedule()
就会调用该函数。它完成了两项基本的工作:
·调用声明在<asm/mmu_ context.h>中的switch_mm(), 该函数负责把虚拟内存从上一个
进程映射切换到新进程中。
·调用声明在<asm/system.h> 中的switch_to(),
该函数负责从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复检信息和寄存器信息,还有
其他任何与体系结构相关的状态信息,都必须以每个进程为对象进行管理和保存。_add_w血’_queue衍巴任务
加到等待队列中.然后调用schedule()
- 用户抢占
- 内核即将返回用户空间的时候,如果need_resched 标志被设置,会导致scheduleO 被调用,
此时就会发生用户抢占。在内核返回用户空间的时候,它知道自己是安全的,因为既然它可以继
续去执行当前进程,那么它当然可以再去选择一个新的进程去执行。所以,内核无论是在中断处
理程序还是在系统调用后返回,都会检查need_resched 标志。如果它被设置了,那么,内核会选
择一个其他〈更合适的)进程投入运行。从中断处理程序或系统调用返回的返回路径都是跟体系
结构相关的,在en町.s (此文件不仅包含内核入口部分的程序,内核退出部分的相关代码也在其
巾)文件中通过汇编语言来实现。
简而言之,用户抢占在以下情况时产生
.从系统调返回用户空间时.
·从中断处理程序返回用户空间时。
- 内核即将返回用户空间的时候,如果need_resched 标志被设置,会导致scheduleO 被调用,
- 内核抢占
- 内核抢占会发生在2
·中断处理程序正在执行,且返回内核空间之前.
·内核代码再一次具有可抢占性的时候。
·如果内核中的任务显式地调用schedule() o
·如果内核中的任务阻塞(这同样也会导敖调用schedule() )。
- 内核抢占会发生在2
4.7实时调度策略
- Linux 提供了两种实时调度策略: SCI租D_FIFO 和SCHED_RR。
- SCHED_FIFO 实现了一种简单的、先入先出的调度算怯:它不使用时间片.处于可运行
状态的SCHED_FIFO 级的进程会比任何SCHED_NORMAL 级的进程都先得到调度。一且一个
SCI盟D_FIFO 级进程处于可执行状态,就会一直执行,直到它自己受阻塞或显式地释放处理器
为止;它不基于时间片,可以一直执行下去.只有更高优先级的SCHED_FIFO 或者SCHED_RR
任务才能抢占SCHED_FIFO 任务。如果有两个或者更多的同优先级的SCHED_FIFO 级进程,它
们会轮流执行,但是依然只有在它们愿意让出处理器时才会退出.只要有SCHED_FIFO 级进程
在执行,其他级别较低的进程就只能等待它变为不可运行态后才有机会执行。 - SC阻止RR 级的进程在耗尽事先分配给它的
时间后就不能再继续执行了.也就是说, SC阻止RR 是带有时闹片的SCHED_FIFO-这是一
种实时轮流调度算挂. 当SCHED_RR 任务艳尽它的时间片时,在同一优先级的其他实时进程被
轮流调度。时间片只用来重新调度同一优先级的进程。对于SCHED_FIFO 进程,高优先级总是
立即抢占低优先级,但低优先级进程决不能抢占SCHED_RR 任务,即使它的时间片艳尽.
4.8调度相关的系统调用
- 与调度策略和优先级相关的系统调用
- sched _ setschedulerO 和sched__getscheduler()分别用于设置和获取进程的调度策略和实时优先
级。与其他的系统调用相似,它们的实现也是囱许多参数检查、初始化和清理构成的。其实最重
要的工作在于读取或改写进程tast_struct 的policy 和rt_priority 的值。
sched _ setparam()和sched__getparam()分别用于设置和获取进程的实时优先级。这两个系统
调用族取封装在sched_param 特殊结构体的rt_priority 中。sched__get_priority_max 0 和sched_
get_priority _min()分别用于返回给定调度策略的最大和最小优先级。实时调度策略的最大优先级
是MAX_ USER_RT_PRIO 减1,最小优先级等于1.
对于一个普通的进程, nice()函数可以将给定进程的静态优先级增加一个给定的量。只有超
级用户才能在调用它时使用负值,从而提高进程的优先级。nice()函数会调用内核的set_ user_
nice()函数, 这个函数会设置进程的task_struct 的static_prio 和prio 值。
- sched _ setschedulerO 和sched__getscheduler()分别用于设置和获取进程的调度策略和实时优先
- 与处理器绑定有关的系统调用
- Linux 调度程序提供强制的处理器绑定(processor affinity?机制。也就是说,虱然它尽力通
过一种软的〈或者说自然的)亲和性试图使进程尽量在同一个处理器上运行,但它也允许用户
强制指定“这个进程无论如何都必须在这些处理器上运行”。这种强制的亲和性保存在进程task_
struct 的cpus_allowed 这个位掩码标志中。该掩码标志的每一位对应一个系统可用的处理器。默
认情况下,所有的位都被设置,进程可以在系统中所有可用的处理器上执行。用户可以通过
sched _ setaffinity()设置不同的一个或几个位组合的位掩码,而调用scbed_ getaffinity()则返回当
前的叩us_allowed 位掩码。
- Linux 调度程序提供强制的处理器绑定(processor affinity?机制。也就是说,虱然它尽力通
- 放弃处理器时间
- Linux 通过sched_yieldO 系统调用,提供了一种让进程显式地将处理器时间让给其他等待执
行进程的机制。它是通过将进程从活动队列中( 因为进程正在执行,所以色肯定位于此队列当
中〉移到过期队列中实现的。由此产生的效果不仅抢占了该进程并将其放入优先级队列的最后
面,还将其放入过期队列中一一这样能确保在一段时间内它都不会再被执行了。由于实时进程
不会过期,所以属于例外。它们只被移动到其优先级队列的最后面〈不会放到过期队列中〉。在
Linux 的早期版本中, sched_yield()的语义有所不同, 进程只会被放置到优先级队列的末尾,放
弃的时间往往不会太长。现在,应用程序甚至内核代码在调用sched_yield()前,应该仔细考虑是
否真的希望放弃处理器时间.
内核代码为了方便,可以直接调用yield(),先要确定给定进程确实处于可执行状态,然后再
调用sched__yield() o 用户空间的应用程序直接使用sched__yield()系统调用就可以了。
- Linux 通过sched_yieldO 系统调用,提供了一种让进程显式地将处理器时间让给其他等待执
时间: 2024-12-18 20:45:39