第四章 进程调度
进程调度程序可看做在可运行进程之间分配有限的处理器时间资源的内核子系统。
多任务
多任务操作系统就是能同时并发地交互执行多个进程的操作系统。
多任务可以划分为两类:
非抢占多任务
抢占式多任务
多任务模式下由调度程序来决定什么时候停止一个进程的运行,以便其他进程能够得到执行机会的这个强制挂起动作就叫做抢占。
时间片实际上就是分配给每个可运行进程的处理时间段。
在非抢占式多任务模式下,除非进程自己主动停止运行,否则它会一直执行。
进程主动挂起自己的过程叫做让步。
进程被分为
I/O消耗型 大部分时间用来提交I/O请求或是等待I/O请求或是等待I/O请求
处理器消耗型 把时间大部分花在执行代码上——降低调度频率
调度策略通常要在两个矛盾的目标中寻找平衡:进程响应迅速(响应时间短)和最大系统利用利率(高吞吐量)
进程优先级
调度程序总是选择时间片为用尽而且优先级最高的进程运行。
nice值:范围-20~+19,默认值为0;越大的nice值意味着更低的优先级——意味着它对系统中其他进程更“优待”。
低nice值(高优先级)的进程可以获得更多的处理器时间。
第二种范围是实时优先级,其值是可以配置的,默认情况下它的变化范围是从0到99(包括0和99)越高的实时优先级数值意味着进程优先级越高。任何实时进程优先级都高于进程。
查看你系统中的进程列表:
ps-eo state,uid,pid,ppid,rtprio,time,comm
CFS调度器抢占时机取决于新的可运行程序消耗了多少处理器使用比。如果消耗的使用比比当前进程小,则新进程立刻投入运行,抢占当前进程。
调度器算法
Linux调度器是以模块方式提供的,目的:允许不同类型的进程可以有针对性地选择调度算法。
这种模块化结构允许不同的可动态添加的调度算法并存,调度属于自己范畴的进程。
完全公平制度(CFS)是一个针对普通进程的调度类,在LInux中称为SCHED_NORMAL,CFS算法实现定义在kernel/sched_fair.c.
优先级以nice单位形式出输给用户空间带来的问题:
-
将nice值映射到时间片,就必然需要将nice单位值对应到处理器的绝对时间。将导致进程切换无法最优化进行。
-
把进程的nice值减小到1所带来的效果极大地取决于其nice的初始值。
-
如果执行nice值到时间片的映射,需要分配一个绝对时间片,且这个绝对时间片必须能在内核的测试范围之内。引发问题:
1)最小时间片必然是定时器节拍的整数倍
2)系统定时器限制了两个时间片的差异
3)时间片还会随着定时器节拍改变 -
有关于基于优先级的调度器为了优化交互任务而唤醒相关进程的问题为了进程能够更快的投入运行,而去对新要唤醒的进程提升优先级,几遍它们的时间片已经涌进来。一些特定情况也有可能发生,因为它同时也给某些特殊的睡眠/唤醒用例一个玩弄调度器的后门,使得给定进程打破公平原则,获得更多处理器时间,损害系统中其他进程的利益。
CFS采用对时间片分配方式进行根本性地重新设计:
完全摒弃时间片而是分配给进程一个处理器使用比重
CFS的做法:
允许每一个进程运行一段时间,循环轮转,选择运行最少的进程作为下一个运行进程
nice值在CFS中被作为进程获得的处理器运行比较权重:越高的nice值(越低的优先级)进程获得更多的处理器使用权重。
越小的调度周期将带来越好的交互性,但必须承受更高的切换代价和更差的系统总吞吐能力。
CFS为此引入每个进程获得的时间片底线,这个底线称为最小粒度
默认情况下这个值是1ms,这样可运行进程数量趋于无穷,每个最少也能获得1ms的运行时间,确保切换消耗被限制在一定范围内。
linux调度的实现
CFS是如何得以实现的。(代码在kernel/sched_fair.c中)
时间记账
进程选择
调度器入口
睡眠和唤醒
时间记账
所有的调度器都必须对进程运行时间做记账。多数Unix系统,分配一个时间片给每一个进程。那么当每次系统时钟节拍发生时,时间片都会被减少一个节拍周期。
- 调度器实体结构(定义在<Linux/sched.h>的struct_sched_entity中)
用来追踪进程记账,调度器实体结构作为一名se的成员变量,嵌入在进程描述符struct_task_struct内。
- 虚拟实时
vruntime变量来记录一个程序到底运行了多长时间以及它应该再运行多久。
kernel/sched_fair.c中update_curr()函数实现了该记账功能。
CFS调度算法的核心:
选择具有最小vruntime任务。
使用红黑树来组织可运行进程队列,并利用其迅速找到最小vruntime的进程。
1.挑选下一个任务
CFS进程算法可以总结为“运行rbtree树中最左边叶子节点所代表的那个进程”
由_pick_next_entity(),定义在kernel/sched_fair.c
2.向树中加入进程
以enqueue_entity()函数实现此目的。
平衡二叉树的基本原则:如果键值小于当前节点的键值,则需要转向树的左分支:相反如果大于当前节点的键值,则转向左分支;相反如果大于当前节点的键值,则转向右分支。如果插入进程不会是最新的最左节点,因此可以设置为leftmost为0.
3.从树中删除进程
调度器入口
进程调度的主要入口点是schedule(),它定义在文件kernel/sched.c中。
睡眠和唤醒
休眠的一个常见原因就是文件I/O——如进程对个文件执行了read()操作,而这需要从磁盘里读取,还有,进程在获取键盘输入的时候也需要等待,无论哪种情况,内核的操作都相同:
进程把自己标记成休跳态,从可执照课树黑树中移出,放入等待队列,然后调用schedule()选择和执行―个其他进程,唤醒的过程刚好相反:进程被设置为可执行状态,然后再从等待队列中移到可晰红黑树中。
1.等待队列
内核用wake_ queue_ head_ t来代表等待队列。
等待队列可以通过DECLARE_ WAITQUEUE()静态创建,也可以由init_ waitqueue_head()动态创建。
具体步骤:
函数inotify_ read(),位于fs/notify/inotify/inotify_user.c中,负责从通知文件描述符中读取信息
2.唤醒
通过wake _ up函数进行,唤醒指定的等待队列上的所有进程。调用try_ o_ wake_up()
抢占和切换
上下文切换:从一个可执行进程切换到另一个可执行进程,有context_ switch负责。完成以下两项基本工作:
- 调用声明在<asm/mmu_context.h>中的switch_mm(),该函数负责把虚拟内存从上一个进程映射切换到新进程中
- 调用声明在<asm/system.h>中的switch_to(),该函数负责从上一个进程的处理器状态新的进程的处理器状态
用户抢占在以下情况时产生:
- 从系统调返回用户空间时。
- 从中断处理程序返回用户空间时。
内核抢占
只要重新调度室安全的,内核就可以在任何时间抢占正在执行的任务。即只要没有持有锁(非抢占区域的标志),内核就可以抢占进程
内核抢占发生的情况:
中断处理程序正在执行,且返回内核空间之前
内核代码再一次具有可抢占性的时候
如果内核中的任务显示的调用schedule
如果内核中的任务阻塞
实时调度策略
两种实时调度策略:
SCHED_FIFO——实现了一种简单的先入先出的调度算法:不使用时间片
SCHED_RR——进程在消耗事先分配给他的时间后就不能再执行了
相关调用
sched_setscheduler()和 sched_getscheduler()分别用于设置和获取进程的调度策略和实时优先级。
与其他的系统调用相似,它们的实现也是由许多参数检查、初始化和清理构成的。
其实最重要的工作在于读取或改写进程task_struct的policy和rt_priority的值。
sched_setscheduler()和 sched_getscheduler()分别用于设置和获取进程的实时优先级。
这两个系统调用获取封装在sched_param特殊结构体的rt_priority中。实时调度策略的的最大优先级是MAX_ USERRT_PRIO减1。最小优先级等于1。
Linux调度程序提供强制的处理器绑定机制。这种强制的亲和性保存进程task_struct的cpus_ allowed这个位掩码标志中。
用户可以通过sched_ setaffinity()设置不同一个或几个位组合的位掩码,而调用sched_ gettaffinity则返回当前的cpus_ allowed位掩码。
内核提供强制处理器绑定的方法很简单:
当处理器第一次创建时,它继承了其父进程的相关掩码。
当处理器绑定关系改变时,内核会采用“移植线程”把任务推到合法处理器上
加载平衡器只把任务拉到允许的处理器上。
因此进程只运行在指定处理器上,对处理器的指定是由该进程表述符的cpus_allowed域设置的。
Linux通过sched_yield()系统调用,提供了一种让进程显式地将处理器时间让给其他等待执行进程的机制,它是通过将进程从活动队列中(因为进程正在执行,所以它肯定位于此队列当中)移到过期队列中实现的。
内核代码为了方便,可以直接调用sched_yield(),先要确定给定进程确实处于可执行状态,然后再调用sched_yield(),用户空间的应用程序直接使用sched_yield()系统调用就可以 。