Linux内核学习笔记(7)--完全公平调度(CFS)
一、完全公平调度算法
完全公平调度 CFS 的出发点基于一个简单的理念:进程调度的效果应该如同系统具备一个理想中的完美多任务处理器。在这种系统中,每个进程能够获得 1/n 的处理器时间(n 为可运行进程数)。同时,我们可以调度给它们无限小的时间周期,所以,在任何可测量周期内,我们给予 n 个进程中每个进程同样多的运行时间。
但是,上述模型并不现实,因为我们无法再一个处理器上真的同时运行多个进程。而且,如果每个进程运行无限小的时间周期也并不高效(调度时的进程抢占会带来一定的代价)。因此,虽然我们希望所有的进程能只运行一个非常短的周期(这样会带来较好的交互性),但是 CFS 充分考虑了这样将带来的额外消耗,实现中首先要确保系统性能不受损失。 CFS 的做法是允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程,而不再采用分配时间片的方式。 CFS 在所有可运行进程总数基础上计算出一个进程应该运行多久,而不是依靠 nice 值来计算时间片。 nice 值在 CFS 中被作为进程获得的处理器运行比的权重,越高的 nice 值将获得更低的处理器使用比。
接下来,我们将在上述思想下,讨论 CFS 是如何实现的。
二、时间记账
所有的调度器都必须对进程运行时间做记账。多数 Unix 系统通过给进程分配时间片的方式来进行时间记账,每当系统时钟发生变化时,时间片都会被减少一个节拍周期。当一个进程的时间片被减到 0 时,它就会被另一个时间片尚未减到 0 的可运行进程抢占。
我们已经知道,CFS不再有时间片的概念。尽管如此,我们依然需要对每个进程运行的时间记账,以保证每个进程只在公平分配给它的处理器时间内运行。CFS 使用调度器实体结构(sched_entity)来追踪进程运行记账,其定义在 include/linux/sched.h 中:
/*
* CFS stats for a schedulable entity (task, task-group etc)
*
* Current field usage histogram:
*
* 4 se->block_start
* 4 se->run_node
* 4 se->sleep_start
* 6 se->load.weight
*/
struct sched_entity {
struct load_weight load; // 权重,与优先级有关
struct rb_node run_node; // 红黑树的结点
struct list_head group_node; // 所在进程组
unsigned int on_rq; // 标记是否处于红黑树运行队列中 u64 exec_start; // 进程开始执行的时间
u64 sum_exec_runtime; // 进程运行总时间
u64 vruntime; // 虚拟运行时间
u64 prev_sum_exec_runtime; // 上个调度周期中进程运行的总时间 u64 last_wakeup;
u64 avg_overlap; u64 nr_migrations; u64 start_runtime;
u64 avg_wakeup;
... ... #ifdef CONFIG_FAIR_GROUP_SCHED
struct sched_entity *parent;
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq;
/* rq "owned" by this entity/group: */
struct cfs_rq *my_q;
#endif
};
调度器实体结构作为一个名为 se 的成员变量,嵌入在进程描述符 task_struct 内。实际上,在 task_struct 结构中,还定义了一个调度器实体结构 sched_rt_entity,这是实时调度的调度器实体。如下:
struct task_struct{
......
struct sched_entity se;
struct sched_rt_entity rt;
......
}
回到 sched_entity 结构。注意到该结构里面有一个元素 vruntime(虚拟运行时间),调度器实体结构就是用这个来对进程运行时间进行记账的。使用 vruntime 进行时间记账的步骤如下:
1)调用 update_curr() 函数,计算当前进程的执行时间,并将结果存放在变量 delta_exec 中;
2)将 delta_exec 作为参数传递给 _update_curr() 函数,该函数将根据当前可运行的进程总数对运行时间进行加权运算;
3)将权值与当前运行进程的 vruntime 相加得到新的 vruntime 值。
update_curr() 函数是由系统定时器周期性调用的,无论进程是处于可运行状态,还是被堵塞处于不可运行状态。根据这种方式,vruntime 可以准确地测量给定进程的运行时间,而且可知道谁应该是下一个被运行的进程。
三、进程选择
当 CFS 需要选择下一个运行进程时,它会挑一个具有最小 vruntime 的进程。CFS 算法的核心就是选择具有最小 vruntime 的任务。CFS 使用红黑树来组织可运行进程队列,并利用其迅速找到最小 vruntime 值(红黑树最左叶子结点)的进程。
CFS 通过调用 _pick_next_entity() 函数来寻找红黑树最左叶子结点,即最小 vruntime 值,如下:
static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
struct rb_node *left = cfs_rq->rb_leftmost; // rb_leftmost 字段中缓存着红黑树最左叶子结点的值 if (!left)
return NULL; return rb_entry(left, struct sched_entity, run_node);
}
_pick_next_entity() 函数本身并不会遍历整个红黑树来寻找最左叶子结点,最左叶子结点的值已经缓存到了 rb_leftmost 字段中,直接调用就行,这样可以节省时间。当没有最左叶子结点(也即没有任何结点)的时候,函数返回值为 NULL,表明没有可运行的进程,此时 CFS 调度器将选择 idle 任务运行。
四、调度器入口
进程调度的主要入口点是函数 schedule(),其定义在 kernel/sched.c 中。它正式内核其他部分用于调度进程调度器的入口:选择哪个进程可以运行,何时将其投入运行。 schedule() 通常都需要和一个具体的调度类相关联,也就是说,它会找到一个最高优先级的调度类(这个调度类需要有自己的可运行队列),然后向这个调度类询问下一个该运行的进程是哪个。
系统通过 schedule() 函数进入进程调度器,然后schedule() 函数调用 pick_next_task() 函数来选择下一个将要执行的进程:
/*
* Pick up the highest-prio task:
*/
static inline struct task_struct *
pick_next_task(struct rq *rq)
{
const struct sched_class *class;
struct task_struct *p; /*
* Optimization: we know that if all tasks are in
* the fair class we can call that function directly:
*/
if (likely(rq->nr_running == rq->cfs.nr_running)) {
p = fair_sched_class.pick_next_task(rq);
if (likely(p))
return p;
} class = sched_class_highest;
for ( ; ; ) {
p = class->pick_next_task(rq); // 每个调度类都实现了 pick_next_task()函数,它会返回指向下一个可运行进程的指针,若没有,则返回 NULL
// 从第一个返回非 NULL 值的类中选择下一个可运行进程
if (p)
return p;
/*
* Will never be NULL as the idle class always
* returns a non-NULL p:
*/
class = class->next;
}
}
picke_next_task() 函数会以优先级为顺序,从高到低,依次检查每一个调度类,并且从最高优先级的调度类中,选择最高优先级的进程。
五、睡眠和唤醒
休眠(被阻塞)的进程处于一个特殊的不可执行状态。如果没有这种状态的话,调度程序就可能选出一个本不愿意被执行的进程,因此,休眠状态是很重要的。进程休眠有多种原因,但肯定都是为了等待一些事件。进程有两种休眠相关的状态:TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE。其中,TASK_INTERRUPTIBLE 在接收到一个信号时,可以被提前唤醒并响应该信号,而后者则会忽略该信号。
当进程把自己标记成休眠状态时,从可执行红黑树中移除,放入等待队列,然后调用 schedule() 选择和执行一个其他进程;唤醒过程刚好相反,进程被设置为执行状态,然后再从等待队列中移到可执行红黑树中。
5.1、等待队列
休眠通过等待队列进行处理。等待队列是由等待某些事件发生的进程组成的简单链表。内核用 wake_queue_head_t 来代表等待队列。等待队列可以通过 DECLARE_WAITQUEUE() 静态创建,也可以由 init_waitqueue_head() 动态创建。进程把自己放入等待队列中并设置为不可执行状态。当与等待队列相关的事件发生时,队列上的进程会被唤醒。进程通过以下几个步骤将自己加入到一个等待队列中:
1)调用宏 DEFINE_WAIT() 创建一个等待队列的项;
2)调用 add_wait_queue() 把自己加入到队列中。该队列会在进程等待的条件满足时唤醒它,通过在事件发生时对等待队列执行 wake_up() 操作;
3)调用 prepare_to_wait() 方法将进程的状态变更为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 。如果有必要的话,会将进程加入到等待队列;
4)若进程被设置为 TASK_INTERRUPTIBLE ,则信号唤醒进程。这就是所谓的伪唤醒(唤醒并不是因为事件的发生),因此检查并处理信号;
5)当进程被唤醒时,它会再次检查条件是否为真。如果为真,则退出循环;否则,再次调用 schedule() 并重复这一步;
6)当条件满足后,进程将自己设置为 TASK_RUNNING 并调用 finish_wait() 方法把自己移除等待队列。
5.2 唤醒
唤醒操作通过函数 wake_up() 进行,它会唤醒指定的等待队列上的所有进程。它调用函数 try_to_wake_up() ,该函数负责将进程设置为 TASK_RUNNING 状态,然后调用 enqueue_task() 将此进程放入红黑树中,如果被唤醒的进程优先级比当前正在执行的进程的优先级高,还要设置 need_resched 标志。另外,关于休眠,存在虚假的唤醒。有时候进程被唤醒并不是因为他所等待的条件达成了,因此需要用一个循环处理来保证等待的条件真正达成(上文中的第5步)。
参考书籍:Linux内核设计与实现(第三版)
Linux内核学习笔记(7)--完全公平调度(CFS)的更多相关文章
- Linux内核学习笔记-2.进程管理
原创文章,转载请注明:Linux内核学习笔记-2.进程管理) By Lucio.Yang 部分内容来自:Linux Kernel Development(Third Edition),Robert L ...
- Linux内核学习笔记二——进程
Linux内核学习笔记二——进程 一 进程与线程 进程就是处于执行期的程序,包含了独立地址空间,多个执行线程等资源. 线程是进程中活动的对象,每个线程都拥有独立的程序计数器.进程栈和一组进程寄存器 ...
- Linux内核学习笔记-1.简介和入门
原创文章,转载请注明:Linux内核学习笔记-1.简介和入门 By Lucio.Yang 部分内容来自:Linux Kernel Development(Third Edition),Robert L ...
- 20135316王剑桥Linux内核学习笔记
王剑桥Linux内核学习笔记 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 计算机是如何工作的 个人理 ...
- Linux内核学习笔记之seq_file接口创建可读写proc文件
转自:http://blog.csdn.net/mumufan05/article/details/45803219 学习笔记与个人理解,如有错误,欢迎指正. 温馨提示:建议跟着注释中的编号顺序阅读代 ...
- Linux内核学习笔记(5)-- 进程调度概述
进程调度程序是多任务操作系统的基础,它是确保进程能有效工作的一个内核子系统,负责决定哪个进程投入运行.何时运行以及运行多长时间.只有通过进程调度程序的合理调度,系统资源才能够最大限度地发挥作用,多进程 ...
- Linux内核学习笔记
1.vanbreaker的专栏 2.LinuxKernel Exploration 3.DroidPhone的专栏 4.Linux内核研究以及学习文档和ARM学习以及研究的开放文档 [力荐] 5. ...
- 20135316王剑桥Linux内核学习笔记第四周
20135316王剑桥 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC 1000029000 1.内核态:在高执行级别,代码可 ...
- Linux内核学习笔记(6)-- 进程优先级详解(prio、static_prio、normal_prio、rt_priority)
Linux 中采用了两种不同的优先级范围,一种是 nice 值,一种是实时优先级.在上一篇粗略的说了一下 nice 值和实时优先级,仍有不少疑问,本文来详细说明一下进程优先级.linux 内核版本为 ...
随机推荐
- Java 常用IO流操作详解
1.基本概念 IO:Java对数据的操作是通过流的方式,IO流用来处理设备之间的数据传输,上传文件和下载文件,Java用于操作流的对象都在IO包中. 2.IO流的分类 图示:(主要IO流) 3.字节流 ...
- C++的六个函数
一.构造函数 在C++中,构造函数是六个函数中的第一个,当一个对象被创建时,在它的整个周期中,是一个由生到死的 过程,即构造函数创建对象,析构函数析构对象.在对象被创建时,调用构造函数创建一个对象,这 ...
- Linux基础练习题(五)
1.创建一个10G分区,并格式为ext4文件系统: (1) 要求其block大小为2048, 预留空间百分比为2, 卷标为MYDATA, 默认挂载属性包含acl: [root@bj-1-142 ~]# ...
- mysql 8 windows 版本zip方式安装步骤
mysql 8 windows 版本zip方式安装步骤(下载地址:https://dev.mysql.com/downloads/mysql/)1,解压ZIP文件到指定目录下:如D:\mysql-8. ...
- PHP-----PHP程序设计基础教程----第二章PHP基本语法
博文结构: 2.1 PHP语法风格 2.1.1 PHP标记 PHP有四种风格的标记,具体如表2-1所示: 表2-1 PHP开始和结束标记 标记类型 开始标记 结束标记 示例 说明 标准标记 <? ...
- WOT干货大放送:大数据架构发展趋势及探索实践分享
WOT大数据处理技术分会场,PingCAP CTO黄东旭.易观智库CTO郭炜.Mob开发者服务平台技术副总监林荣波.宜信技术研发中心高级架构师王东及商助科技(99Click)顾问总监郑泉五位讲师, ...
- 第六节 Go数据结构之集合
一.什么是集合 集合就是不同对象的无序聚集.那么链表和集合有什么关系呢?我们来变个魔术.如下图是各种颜色组成的链表: 下面我们一步步把链表变成集合. 第一步砍去链接 第二步去掉重复 第三步放到一个框里 ...
- 05-基础widgets
05-基础widgets 介绍 Widget的功能是"描述一个UI元素的配置数据",Widget并不是表示最终绘制在设备屏幕上的显示元素,而只是显示元素的一个配置数据.Flutte ...
- 20155215 2006-2007-2 《Java程序设计》第2周学习总结
20155215 2006-2007-2 <Java程序设计>第2周学习总结 教材学习内容总结 第三章主要讲述了JAVA程序编写中的一些基本语法.其实看了第三章之后我就感觉到,C语言不愧是 ...
- 20155323 2016-2017-2 《Java程序设计》第一周学习总结
20155323 2016-2017-2 <Java程序设计>第一周学习总结 1.浏览教材,根据自己的理解每章提出一个问题 第一章:既然JDK本身附有一个JRE,那么JRE和JDK的区别在 ...