本文转载自从几个问题开始理解CFS调度器

导语

CFS(完全公平调度器)是Linux内核2.6.23版本开始采用的进程调度器,它的基本原理是这样的:设定一个调度周期(sched_latency_ns),目标是让每个进程在这个周期内至少有机会运行一次,换一种说法就是每个进程等待CPU的时间最长不超过这个调度周期;然后根据进程的数量,大家平分这个调度周期内的CPU使用权,由于进程的优先级即nice值不同,分割调度周期的时候要加权;每个进程的累计运行时间保存在自己的vruntime字段里,哪个进程的vruntime最小就获得本轮运行的权利。

如果你觉没有从cfs中看到什么,那么最简单我告诉你,就是提高了响应速度,为何呢?在shell下执行vmstat,观察cs字段,也就是1秒内进程切换的次数,然后尽量满载系统,观察cs和谁有关,它的值和进程的数量,进程的优先级有关,当进程的优先级很大的时候,cs的值就会减少,反之cs值会增加,我们看看这是为什么,O(1)调度器中,时间片的分配是绝对的分配,也就是说排除进程饥饿和内核抢占,每个进程运行的时间片是绝对的,是按照它的nice值计算出来的,如果说有变动,那也只是由于优先级动态调整导致的变化,那时的时间片只和进程的优先级有关,优先级变化幅度不大,时间片调整的也不多,因此我们不考虑优先级调整,一旦系统负载增加,调度完整个系统的所有进程的时间就会很长,活动进程数量越多,这一段时间就会越长,O(1)调度器中的时间片是固定的,那么cfs中的呢?

cfs中的时间片是动态分配的,是按照比例分配的而不是按照优先级固定分配的,其精髓就是系统拥有一个可配置的系统调度周期,在该周期内运行完所有的进程,如果系统负载高了,那么每一个进程在该周期内被分配的时间片都会减少,将这些进程减少的部分累积正好就是新进程的时间片,其实完全可以实现一个更简单的cfs版本,按照固定的顺序运行进程,就是将红黑树退化成一个先入先出的队列,每个进程都排入进程,然后运行完按比例分配给它的动态时间片之后排入队列最后,然后继续下一个进程,如此反复,但是这样实现的话很不灵活,很难实现内核的实时抢占和新进程的抢占以及进程唤醒后的补偿,于是红黑树完美的解决了这一个问题,于是就出现了虚拟时钟的概念,每一个进程都有一个虚拟时钟,按照不同的速率在每一个物理时钟节拍内向前推进,越高权值的进程的推进速率越慢,这样它就可以运行更多的物理时间。cfs调度器选择当前最慢的虚拟时钟进行推进,做到了公平。

cfs调度器中分配的动态时间片和HZ没有关系,它只和进程的权值以及当前红黑树进程的总权值还有调度周期有关,它本质上是一个相对的概念而不像以前是一个绝对的概念,相对的概念就是比绝对的概念要灵活,比如用相对目录的程序就比用绝对目录的程序拥有更好的移植性。cfs调度器将时间片的概念进行了相对了,抽象出了虚拟时钟的概念,如此一来1秒钟内的进程切换次数就不再和进程优先值有关了,而是和调度周期和进程数量有关,理论上就是(进程数量)*(1秒/调度周期)次,当然加上抢占和新进程创建就不是这么理想了,经过试验,和理论数据差别不大,在同一负载下,提高或者降低多个进程的优先级在O(1)调度器下会引起vmstat中cs的变化,但是在cfs中vmstat中的cs值却不会受到影响。

那么问题就来了:

新进程的vruntime的初值是不是0啊?

假如新进程的vruntime初值为0的话,比老进程的值小很多,那么它在相当长的时间内都会保持抢占CPU的优势,老进程就要饿死了,这显然是不公平的。所以CFS是这样做的:每个CPU的运行队列cfs_rq都维护一个min_vruntime字段,记录该运行队列中所有进程的vruntime最小值,新进程的初始vruntime值就以它所在运行队列的min_vruntime为基础来设置,与老进程保持在合理的差距范围内。参见后面的源代码。

新进程的vruntime初值的设置与两个参数有关:

sched_child_runs_first:规定fork之后让子进程先于父进程运行;

sched_featuresSTART_DEBIT位:规定新进程的第一次运行要有延迟。

注: sched_features是控制调度器特性的开关,每个bit表示调度器的一个特性。在sched_features.h文件中记录了全部的特性。START_DEBIT是其中之一,如果打开这个特性,表示给新进程的vruntime初始值要设置得比默认值更大一些,这样会推迟它的运行时间,以防进程通过不停的fork来获得cpu时间片。

如果参数 sched_child_runs_first打开,意味着创建子进程后,保证子进程会在父进程之前运行。

子进程在创建时,vruntime初值首先被设置为min_vruntime;然后,如果sched_features中设置了START_DEBIT位,vruntime会在min_vruntime的基础上再增大一些。设置完子进程的vruntime之后,检查sched_child_runs_first参数,如果为1的话,就比较父进程和子进程的vruntime,若是父进程的vruntime更小,就对换父、子进程的vruntime,这样就保证了子进程会在父进程之前运行。

休眠进程的vruntime一直保持不变吗?

如果休眠进程的 vruntime 保持不变,而其他运行进程的 vruntime 一直在推进,那么等到休眠进程终于唤醒的时候,它的vruntime比别人小很多,会使它获得长时间抢占CPU的优势,其他进程就要饿死了。这显然是另一种形式的不公平。CFS是这样做的:在休眠进程被唤醒时重新设置vruntime值,以min_vruntime值为基础,给予一定的补偿,但不能补偿太多。

static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
u64 vruntime = cfs_rq->min_vruntime; /*
* The 'current' period is already promised to the current tasks,
* however the extra weight of the new task will slow them down a
* little, place the new task so that it fits in the slot that
* stays open at the end.
*/
if (initial && sched_feat(START_DEBIT)) /* initial表示新进程 */
vruntime += sched_vslice(cfs_rq, se); /* sleeps up to a single latency don't count. */
if (!initial) { /* 休眠进程 */
unsigned long thresh = sysctl_sched_latency; /* 一个调度周期 */ /*
* Halve their sleep time's effect, to allow
* for a gentler effect of sleepers:
*/
if (sched_feat(GENTLE_FAIR_SLEEPERS)) /* 若设了GENTLE_FAIR_SLEEPERS */
thresh >>= 1; /* 补偿减为调度周期的一半 */ vruntime -= thresh;
} /* ensure we never gain time by being placed backwards. */
vruntime = max_vruntime(se->vruntime, vruntime); se->vruntime = vruntime;
}

休眠进程在唤醒时会立刻抢占CPU吗?

这是由CFS的唤醒抢占 特性决定的,即sched_features的*WAKEUP_PREEMPT`位。

由于休眠进程在唤醒时会获得vruntime的补偿,所以它在醒来的时候有能力抢占CPU是大概率事件,这也是CFS调度算法的本意,即保证交互式进程的响应速度,因为交互式进程等待用户输入会频繁休眠。除了交互式进程以外,主动休眠的进程同样也会在唤醒时获得补偿,例如通过调用sleep()、nanosleep()的方式,定时醒来完成特定任务,这类进程往往并不要求快速响应,但是CFS不会把它们与交互式进程区分开来,它们同样也会在每次唤醒时获得vruntime补偿,这有可能会导致其它更重要的应用进程被抢占,有损整体性能。

我曾经处理过一个案例,服务器上有两类应用进程:

A进程定时循环检查有没有新任务,如果有的话就简单预处理后通知B进程,然后调用nanosleep()主动休眠,醒来后再重复下一个循环;

B进程负责数据运算,是CPU消耗型的;

B进程的运行时间很长,而A进程每次运行时间都很短,但睡眠/唤醒却十分频繁,每次唤醒就会抢占B,导致B的运行频繁被打断,大量的进程切换带来很大的开销,整体性能下降很厉害。

那有什么办法吗?有,最后我们通过禁止CFS唤醒抢占 特性解决了问题:

# echo NO_WAKEUP_PREEMPT > /sys/kernel/debug/sched_features

禁用唤醒抢占 特性之后,刚唤醒的进程不会立即抢占运行中的进程,而是要等到运行进程用完时间片之后。在以上案例中,经过这样的调整之后B进程被抢占的频率大大降低了,整体性能得到了改善。

如果禁止唤醒抢占特性对你的系统来说太过激进的话,你还可以选择调大以下参数:

sched_wakeup_granularity_ns

这个参数限定了一个唤醒进程要抢占当前进程之前必须满足的条件:只有当该唤醒进程的vruntime比当前进程的vruntime小、并且两者差距(vdiff)大于sched_wakeup_granularity_ns的情况下,才可以抢占,否则不可以。这个参数越大,发生唤醒抢占就越不容易。

进程占用的CPU时间片可以无穷小吗?

假设有两个进程,它们的vruntime初值都是一样的,第一个进程只要一运行,它的vruntime马上就比第二个进程更大了,那么它的CPU会立即被第二个进程抢占吗?答案是这样的:为了避免过于短暂的进程切换造成太大的消耗,CFS设定了进程占用CPU的最小时间值,sched_min_granularity_ns,正在CPU上运行的进程如果不足这个时间是不可以被调离CPU的。

sched_min_granularity_ns发挥作用的另一个场景是,本文开门见山就讲过,CFS把调度周期sched_latency按照进程的数量平分,给每个进程平均分配CPU时间片(当然要按照nice值加权,为简化起见不再强调),但是如果进程数量太多的话,就会造成CPU时间片太小,如果小于sched_min_granularity_ns的话就以sched_min_granularity_ns为准;而调度周期也随之不再遵守sched_latency_ns,而是以 (sched_min_granularity_ns * 进程数量) 的乘积为准。

进程从一个CPU迁移到另一个CPU上的时候vruntime会不会变?

在多CPU的系统上,不同的CPU的负载不一样,有的CPU更忙一些,而每个CPU都有自己的运行队列,每个队列中的进程的vruntime也走得有快有慢,比如我们对比每个运行队列的min_vruntime值,都会有不同:

# grep min_vruntime /proc/sched_debug
.min_vruntime : 12403175.972743
.min_vruntime : 14422108.528121

如果一个进程从min_vruntime更小的CPU (A) 上迁移到min_vruntime更大的CPU (B) 上,可能就会占便宜了,因为CPU (B) 的运行队列中进程的vruntime普遍比较大,迁移过来的进程就会获得更多的CPU时间片。这显然不太公平。

CFS是这样做的:

当进程从一个CPU的运行队列中出来 (dequeue_entity) 的时候,它的vruntime要减去队列的min_vruntime值;

而当进程加入另一个CPU的运行队列 ( enqueue_entiry) 时,它的vruntime要加上该队列的min_vruntime值。

这样,进程从一个CPU迁移到另一个CPU之后,vruntime保持相对公平。

static void
dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
...
/*
* Normalize the entity after updating the min_vruntime because the
* update can refer to the ->curr item and we need to reflect this
* movement in our normalized position.
*/
if (!(flags & DEQUEUE_SLEEP))
se->vruntime -= cfs_rq->min_vruntime;
...
} static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
/*
* Update the normalized vruntime before updating min_vruntime
* through callig update_curr().
*/
if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
se->vruntime += cfs_rq->min_vruntime;
...
}

从几个问题开始理解CFS调度器的更多相关文章

  1. Linux进程管理 (2)CFS调度器

    关键词: 目录: Linux进程管理 (1)进程的诞生 Linux进程管理 (2)CFS调度器 Linux进程管理 (3)SMP负载均衡 Linux进程管理 (4)HMP调度器 Linux进程管理 ( ...

  2. CFS调度器

    一.前言 随着内核版本的演进,其源代码的膨胀速度也在递增,这让Linux的学习曲线变得越来越陡峭了.这对初识内核的同学而言当然不是什么好事情,满腔热情很容易被当头浇灭.我有一个循序渐进的方法,那就是先 ...

  3. Linux内核——进程管理之CFS调度器(基于版本4.x)

    <奔跑吧linux内核>3.2笔记,不足之处还望大家批评指正 建议阅读博文https://www.cnblogs.com/openix/p/3262217.html理解linux cfs调 ...

  4. 【原创】(五)Linux进程调度-CFS调度器

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

  5. CFS调度器(1)-基本原理

    首先需要思考的问题是:什么是调度器(scheduler)?调度器的作用是什么?调度器是一个操作系统的核心部分.可以比作是CPU时间的管理员.调度器主要负责选择某些就绪的进程来执行.不同的调度器根据不同 ...

  6. CFS 调度器

    CFS调度器的原理明白了但是有个地方,搜遍了整个网络也没找到一个合理的解释: if (delta > ideal_runtime) resched_task(rq_of(cfs_rq)-> ...

  7. linux cfs调度器_模型实现

    调度器真实模型的主要成员变量及与抽象模型的对应关系 I.cfs_rq结构体    a) struct sched_entity *curr        指向当前正在执行的可调度实体.调度器的调度单位 ...

  8. linux cfs调度器

    在抽象模型中vruntime决定了进程被调度的先后顺序,在真实模型中决定被调度的先后顺序的参数是由函数entity_key决定的.   static inline s64 entity_key(str ...

  9. linux cfs调度器_理论模型

    参考资料:<调度器笔记>Kevin.Liu <Linux kernel development> <深入Linux内核架构> version: 2.6.32.9 下 ...

随机推荐

  1. Prometheus+Grafana通过kafka_exporter监控kafka

    Prometheus+Grafana通过kafka_exporter监控kafka 一.暴露 kafka-metric 方式 二.jmx_exporter方式 2.1 下载jmx_prometheus ...

  2. eclipse 断点调试方法

    1 Debug视图 1.1 线程堆栈视图 线程堆栈视图表示当前线程的堆栈,从中可以看出在运行哪些代码,并且整个调用过程,以及代码行号.分别介绍一下这几个按钮的含义.从左至右分别为: 1.表示当前实现继 ...

  3. 2. Linux常用系统工作命令

    1.echo:在终端输出字符串或变量提取后的值.echo [字符串 | $变量] 举例:[root@Centos~]# echo $SHELL /bin/bash 2.date:显示及设置系统的时间或 ...

  4. 机器学习算法之Kmeans算法(K均值算法)

    Kmeans算法(K均值算法) KMeans算法是典型的基于距离的聚类算法,采用距离作为相似性的评价指标,即认为两个对象的距离越近,其相似度就越大.该算法认为簇是由距离靠近的对象组成的,因此把得到紧凑 ...

  5. CF-1445 C - Division 数论,质因数,唯一分解定理

    题意 给定一个 \(p (p\le 10^{18})\), 一个 \(q(q \le 10^9)\), 要找到一个最大的 \(x\) 满足: \(p \%x = 0\) \(q \% x \neq 0 ...

  6. Codeforces Round #579 (Div. 3) E. Boxers (贪心)

    题意:给你一组数,每个数都可以进行一次加一减一,问最后最多能有多少不同的数. 题解:我们可以用桶存每个数的次数,然后枚举\([1,150001]\)来求对答案的贡献,然后贪心,这里我们不用担心其他乱七 ...

  7. 活动精彩实录 | 阿里云刘军民(米诺):Cassandra中文社区年度回顾

    点击这里观看完整视频 大家好,我是刘军民,我是阿里云数据库的产品经理,目前负责云数据库的产品规划以及相关工作.曾在2019年和多位小伙伴一起发起了中文社区,我希望有更多的小伙伴能加入到社区建设中,这样 ...

  8. ArcGIS处理栅格数据(三)

    六.制作镶嵌数据集(栅格数据集优点:a.浏览速度快:b.入库速度快:c.可指定区域显示) 1.右键目录中的数据库,新建"镶嵌数据集". 2.添加栅格数据. 3.定义金字塔. 4.构 ...

  9. Http和Https之为什么Https更安全

    [除夕了,加油干.希望自己新的一年万事顺意,祝大家身体健康,心想事成!] 我们都知道 HTTPS 安全,可是为什么安全呢? 看小电影还是浏览正常网站,一定要检查是不是 HTTPS 的,因为Https相 ...

  10. sizeof和strlen在string类中的使用

    字符串的sizeof和strlen 考虑下面的问题: char a[] = "abcdef"; char b[20] = "abcdef"; string s ...