1. CFS进程入队和出队

完全公平调度器CFS中有两个函数可用来增删队列的成员:enqueue_task_fairdequeue_task_fair分别用来向CFS就绪队列中添加或者删除进程

2 enqueue_task_fair入队操作

2.1 enque_task_fair函数

向就绪队列中放置新进程的工作由函数enqueue_task_fair函数完成, 该函数定义在kernel/sched/fair.c, line 5442, 其函数原型如下

该函数将task_struct *p所指向的进程插入到rq所在的就绪队列中, 除了指向所述的就绪队列rq和task_struct的指针外, 该函数还有另外一个参数wakeup. 这使得可以指定入队的进程是否最近才被唤醒并转换为运行状态(此时需指定wakeup = 1), 还是此前就是可运行的(那么wakeup = 0).

static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)

enqueue_task_fair的执行流程如下

  • 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做.
  • 否则, 具体的工作委托给enqueue_entity完成, 其中内核会借机用update_curr更新统计量

在enqueue_entity内部如果需要会调用__enqueue_entity将进程插入到CFS红黑树中合适的结点

2.2 enque_task_fair完全函数

/*
* The enqueue_task method is called before nr_running is
* increased. Here we update the fair scheduling stats and
* then put the task into the rbtree:
*/
static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &p->se; for_each_sched_entity(se) {
if (se->on_rq)
break;
cfs_rq = cfs_rq_of(se);
enqueue_entity(cfs_rq, se, flags); /*
* end evaluation on encountering a throttled cfs_rq
*
* note: in the case of encountering a throttled cfs_rq we will
* post the final h_nr_running increment below.
*/
if (cfs_rq_throttled(cfs_rq))
break;
cfs_rq->h_nr_running++; flags = ENQUEUE_WAKEUP;
} for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
cfs_rq->h_nr_running++; if (cfs_rq_throttled(cfs_rq))
break; update_load_avg(se, 1);
update_cfs_shares(cfs_rq);
} if (!se)
add_nr_running(rq, 1); hrtick_update(rq);
}

2.3 for_each_sched_entity

首先内核查找到待天机进程p所在的调度实体信息, 然后通过for_each_sched_entity循环所有调度实体,

//  enqueue_task_fair函数
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &p->se; for_each_sched_entity(se)
{
/* ...... */
}
}

linux对组调度的支持可以通过CONFIG_FAIR_GROUP_SCHED来启用, 在启用和不启用的条件下, 内核对很多函数的实现也会因条件而异, 这点对for_each_sched_entity函数尤为明显, 参见启用CONFIG_FAIR_GROUP_SCHED不启用CONFIG_FAIR_GROUP_SCHED

  • 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做.
  • 否则, 具体的工作委托给enqueue_entity完成, 其中内核会借机用update_curr更新统计量.
//  enqueue_task_fair函数
{
/* 如果当前进程已经在就绪队列上 */
if (se->on_rq)
break; /* 获取到当前进程所在的cfs_rq就绪队列 */
cfs_rq = cfs_rq_of(se);
/* 内核委托enqueue_entity完成真正的插入工作 */
enqueue_entity(cfs_rq, se, flags);
}

2.4 enqueue_entity插入进程

enqueue_entity完成了进程真正的入队操作, 其具体流程如下所示

  • 更新一些统计统计量, update_curr, update_cfs_shares等
  • 如果进程此前是在睡眠状态, 则调用place_entity中首先会调整进程的虚拟运行时间
  • 最后如果进程最近在运行, 其虚拟运行时间仍然有效, 那么则直接用__enqueue_entity加入到红黑树

首先如果进程最近正在运行, 其虚拟时间时间仍然有效, 那么(除非它当前在执行中)它可以直接用__enqueue_entity插入到红黑树, 该函数徐娅萍处理一些红黑树的机制, 这可以依靠内核的标准实现, 参见__enqueue_entity函数, kernel/sched/fair.c, line483

static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
/*
* Update the normalized vruntime before updating min_vruntime
* through calling update_curr().
*
* 如果当前进程之前已经是可运行状态不是被唤醒的那么其虚拟运行时间要增加
*/
if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
se->vruntime += cfs_rq->min_vruntime; /*
* Update run-time statistics of the 'current'.
* 更新进程的统计量信息
*/
update_curr(cfs_rq);
enqueue_entity_load_avg(cfs_rq, se);
account_entity_enqueue(cfs_rq, se);
update_cfs_shares(cfs_rq); /* 如果当前进行之前在睡眠刚被唤醒 */
if (flags & ENQUEUE_WAKEUP)
{
/* 调整进程的虚拟运行时间 */
place_entity(cfs_rq, se, 0);
if (schedstat_enabled())
enqueue_sleeper(cfs_rq, se);
} check_schedstat_required();
if (schedstat_enabled()) {
update_stats_enqueue(cfs_rq, se);
check_spread(cfs_rq, se);
} /* 将进程插入到红黑树中 */
if (se != cfs_rq->curr)
__enqueue_entity(cfs_rq, se);
se->on_rq = 1; if (cfs_rq->nr_running == 1) {
list_add_leaf_cfs_rq(cfs_rq);
check_enqueue_throttle(cfs_rq);
}
}

2.5 place_entity处理睡眠进程

如果进程此前在睡眠, 那么则调用place_entity处理其虚拟运行时间

设想一下子如果休眠进程的vruntime保持不变, 而其他运行进程的 vruntime一直在推进, 那么等到休眠进程终于唤醒的时候, 它的vruntime比别人小很多, 会使它获得长时间抢占CPU的优势, 其他进程就要饿死了. 这显然是另一种形式的不公平,因此CFS是这样做的:在休眠进程被唤醒时重新设置vruntime值,以min_vruntime值为基础,给予一定的补偿,但不能补偿太多. 这个重新设置其虚拟运行时间的工作就是就是通过place_entity来完成的, 另外新进程创建完成后, 也是通过place_entity完成其虚拟运行时间vruntime的设置的. place_entity通过其第三个参数initial来标识新进程创建和休眠进程苏醒两种不同情形的.

place_entity函数定义在kernel/sched/fair.c, line 3135中首先会调整进程的虚拟运行时间

//  http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3134
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.
*
* 如果是新进程第一次要入队, 那么就要初始化它的vruntime
* 一般就把cfsq的vruntime给它就可以
* 但是如果当前运行的所有进程被承诺了一个运行周期
* 那么则将新进程的vruntime后推一个他自己的slice
* 实际上新进程入队时要重新计算运行队列的总权值
* 总权值显然是增加了,但是所有进程总的运行时期并不一定随之增加
* 则每个进程的承诺时间相当于减小了,就是减慢了进程们的虚拟时钟步伐。
*/
/* initial标识了该进程是新进程 */
if (initial && sched_feat(START_DEBIT))
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:
*/
/* 若设了GENTLE_FAIR_SLEEPERS */
if (sched_feat(GENTLE_FAIR_SLEEPERS))
thresh >>= 1; /* 补偿减为调度周期的一半 */ vruntime -= thresh;
} /* ensure we never gain time by being placed backwards.
* 如果是唤醒已经存在的进程,则单调附值
*/
se->vruntime = max_vruntime(se->vruntime, vruntime);
}

我们可以看到enqueue_task_fair调用place_entity传递的initial参数为0

place_entity(cfs_rq, se, 0);

所以会执行if (!initial)后的语句。因为进程睡眠后,vruntime就不会增加了,当它醒来后不知道过了多长时间,可能vruntime已经比 min_vruntime小了很多,如果只是简单的将其插入到就绪队列中,它将拼命追赶min_vruntime,因为它总是在红黑树的最左面。如果这 样,它将会占用大量的CPU时间,导致红黑树右边的进程被饿死。但是我们又必须及时响应醒来的进程,因为它们可能有一些工作需要立刻处理,所以系统采取了 一种折衷的办法,将当前cfs_rq->min_vruntime时间减去sysctl_sched_latency赋给vruntime,这时它 会被插入到就绪队列的最左边。这样刚唤醒的进程在当前执行进程时间耗尽时就会被调度上处理器执行。当然如果进程没有睡眠那么多时间,我们只需保留原来的时 间vruntime = max_vruntime(se->vruntime, vruntime)。这有什么好处的,我觉得它可以将所有唤醒的进程排个队,睡眠越久的越快得到响应。

对于新进程创建时initial为1,所以它会执行vruntime += sched_vslice(cfs_rq, se);这句,而这里的vruntime就是当前CFS就绪队列的min_vruntime,新加进程应该在最近很快被调度,这样减少系统的响应时间,我们已经知道当前进程的vruntime越小,它在红黑树中就会越靠左,就会被很快调度到处理器上执行。但是,Linux内核需要根据新加入的进程的权重决策一下应该何时调度该进程,而不能任意进程都来抢占当前队列中靠左的进程,因为必须保证就绪队列中的所有进程尽量得到他们应得的时间响应, sched_vslice函数就将其负荷权重转换为等价的虚拟时间, 其定义在kernel/sched/fair.c, line 626

函数就是根据initial的值来区分两种情况, 一般来说只有在新进程被加到系统中时,才会首次设置该参数, 但是这里的情况并非如此:

由于内核已经承诺在当前的延迟周期内使所有活动进程都至少运行一次, 队列的min_vruntime用作基准虚拟时间, 通过减去sysctl_sched_latency, 则可以确保新唤醒新唤醒的进程只有在当前延迟周期结束后才能运行.

但是如果进程在睡眠的过程中累积了比较大的不公平值(即se->vruntime值比较大), 则内核必须考虑这一点. 如果se->vruntime比先前的差值更大, 则将其作为进程的vruntime, 这会导致高进程在红黑树中处于靠左的位置, 而具有较小vruntime值得进程可以更早调度执行.

2.6 __enqueue_entity完成红黑树的插入

如果进程最近在运行, 其虚拟时间是有效的, 那么它可以直接通过__enqueue_entity加入到红黑树

//  enqueue_entity函数解析
/* 将进程插入到红黑树中 */
if (se != cfs_rq->curr)
__enqueue_entity(cfs_rq, se);
se->on_rq = 1;

__enqueue_entity函数定义在kernel/sched/fair.c, line 486中, 其实就是一个机械性地红黑树插入操作

/*
* Enqueue an entity into the rb-tree:
*/
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;
struct rb_node *parent = NULL;
struct sched_entity *entry;
int leftmost = 1; /*
* Find the right place in the rbtree:
* 从红黑树中找到se所应该在的位置
* 同时leftmost标识其位置是不是最左结点
* 如果在查找结点的过程中向右走了, 则置leftmost为0
* 否则说明一直再相左走, 最终将走到最左节点, 此时leftmost恒为1
*/
while (*link) {
parent = *link;
entry = rb_entry(parent, struct sched_entity, run_node);
/*
* We dont care about collisions. Nodes with
* the same key stay together.
* 以se->vruntime值为键值进行红黑树结点的比较
*/
if (entity_before(se, entry)) {
link = &parent->rb_left;
} else {
link = &parent->rb_right;
leftmost = 0;
}
}
/*
* Maintain a cache of leftmost tree entries (it is frequently
* used):
* 如果leftmost为1, 说明se是红黑树当前的最左结点, 即vruntime最小
* 那么把这个节点保存在cfs就绪队列的rb_leftmost域中
*/
if (leftmost)
cfs_rq->rb_leftmost = &se->run_node; /* 将新进程的节点加入到红黑树中 */
rb_link_node(&se->run_node, parent, link);
/* 为新插入的结点进行着色 */
rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);
}

3 dequeue_task_fair出队操作

dequeue_task_fair函数在完成睡眠等情况下调度, 将任务从就绪队列中移除

其执行的过程正好跟enqueue_task_fair的思路相同, 只是操作刚好相反

dequeue_task_fair的执行流程如下

  • 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做.

  • 否则, 具体的工作委托给dequeue_entity完成, 其中内核会借机用update_curr更新统计量

    在enqueue_entity内部如果需要会调用__dequeue_entity将进程插入到CFS红黑树中合适的结点

dequeue_task_fair定义在/kernel/sched/fair.c, line 4155, 其大致框架流程如下

/*
* The dequeue_task method is called before nr_running is
* decreased. We remove the task from the rbtree and
* update the fair scheduling stats:
*/
static void dequeue_task_fair(struct rq *rq, struct task_struct *p, int flags)
; struct cfs_rq *cfs_rq;
struct sched_entity *se = &p->se;
int task_sleep = flags & DEQUEUE_SLEEP; // 设置
flags |= DEQUEUE_SLEEP; for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
cfs_rq->h_nr_running--; if (cfs_rq_throttled(cfs_rq))
break; update_load_avg(se, 1);
update_cfs_shares(cfs_rq);
} if (!se)
sub_nr_running(rq, 1); hrtick_update(rq);
}

3.2 dequeue_entity将调度实体出队

static void
dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
/*
* Update run-time statistics of the 'current'.
*/
update_curr(cfs_rq);
dequeue_entity_load_avg(cfs_rq, se); if (schedstat_enabled())
update_stats_dequeue(cfs_rq, se, flags); clear_buddies(cfs_rq, se); if (se != cfs_rq->curr)
__dequeue_entity(cfs_rq, se);
se->on_rq = 0;
account_entity_dequeue(cfs_rq, se); /*
* 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; /* return excess runtime on last dequeue */
return_cfs_rq_runtime(cfs_rq); update_min_vruntime(cfs_rq);
update_cfs_shares(cfs_rq);
}

3.3 __dequeue_entity完成真正的出队操作

static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
if (cfs_rq->rb_leftmost == &se->run_node) {
struct rb_node *next_node; next_node = rb_next(&se->run_node);
cfs_rq->rb_leftmost = next_node;
} rb_erase(&se->run_node, &cfs_rq->tasks_timeline);
}

Linux CFS调度器之队列操作--Linux进程的管理与调度(二十七)的更多相关文章

  1. Linux CFS调度器之负荷权重load_weight--Linux进程的管理与调度(二十五)

    1. 负荷权重 1.1 负荷权重结构struct load_weight 负荷权重用struct load_weight数据结构来表示, 保存着进程权重值weight.其定义在/include/lin ...

  2. Linux进程调度器的设计--Linux进程的管理与调度(十七)

    1 前景回顾 1.1 进程调度 内存中保存了对每个进程的唯一描述, 并通过若干结构与其他进程连接起来. 调度器面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分为 ...

  3. Linux唤醒抢占----Linux进程的管理与调度(二十三)

    1. 唤醒抢占 当在try_to_wake_up/wake_up_process和wake_up_new_task中唤醒进程时, 内核使用全局check_preempt_curr看看是否进程可以抢占当 ...

  4. Linux进程上下文切换过程context_switch详解--Linux进程的管理与调度(二十一)

    1 前景回顾 1.1 Linux的调度器组成 2个调度器 可以用两种方法来激活调度 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测 ...

  5. Linux进程退出详解(do_exit)--Linux进程的管理与调度(十四)

    Linux进程的退出 linux下进程退出的方式 正常退出 从main函数返回return 调用exit 调用_exit 异常退出 调用abort 由信号终止 _exit, exit和_Exit的区别 ...

  6. Linux CFS调度器之唤醒抢占--Linux进程的管理与调度(三十)

    我们也讲解了CFS的很多进程操作 table th:nth-of-type(1){ width: 20%; } table th:nth-of-type(2){ width: 20% ; } 信息 函 ...

  7. Linux CFS调度器之pick_next_task_fair选择下一个被调度的进程--Linux进程的管理与调度(二十八)

    1. CFS如何选择最合适的进程 每个调度器类sched_class都必须提供一个pick_next_task函数用以在就绪队列中选择一个最优的进程来等待调度, 而我们的CFS调度器类中, 选择下一个 ...

  8. Linux CFS调度器之虚拟时钟vruntime与调度延迟--Linux进程的管理与调度(二十六)

    1 虚拟运行时间(今日内容提醒) 1.1 虚拟运行时间的引入 CFS为了实现公平,必须惩罚当前正在运行的进程,以使那些正在等待的进程下次被调度. 具体实现时,CFS通过每个进程的虚拟运行时间(vrun ...

  9. Linux CFS调度器之task_tick_fair处理周期性调度器--Linux进程的管理与调度(二十九)

    1. CFS如何处理周期性调度器 周期性调度器的工作由scheduler_tick函数完成(定义在kernel/sched/core.c, line 2910), 在scheduler_tick中周期 ...

随机推荐

  1. Linux常用命令之网络和关机重启命令

    目录 1.网络命令 一.给指定用户发送信息:write 二.给所有用户发送广播信息:wall 三.测试网络连通性:ping 四.查看和设置网卡信息:ifconfig 五.查看发送电子邮件:mail 六 ...

  2. es6入门2--对象解构赋值

    解构赋值:ES6允许按照一定规则从数组或对象中提取值,并对变量进行赋值.说直白点,等号两边的结构相同,右边的值会赋给左边的变量. 一.数组的解构赋值: 1.基本用法 let [a, b, c] = [ ...

  3. 翻译:window function(已提交到MariaDB官方手册)

    本文为mariadb官方手册:window functions的译文. 原文:https://mariadb.com/kb/en/window-functions-overview/ 我提交到Mari ...

  4. μC/OS-II 中的任务管理

    1. 任务的状态及其转换 睡眠状态: 任务在没有被配备任务控制块或被剥夺了任务控制块时的状态叫做任务的睡眠状态. 等待状态: 正在运行的任务,需要等待一段时间或需要等待一个事件发生再运行时,该任务就会 ...

  5. 关于"Could not find acceptable representation"错误

    在项目中调用一个第三方服务,第三方服务是用Spring Boot写成的.结果调用时返回"Could not find acceptable representation"错误. 经 ...

  6. .NET 单元测试的利剑——模拟框架Moq(简述篇)

    .NET 单元测试的利剑--模拟框架Moq 前言 这篇文章是翻译文,因为通过自己参与的项目,越发觉得单元测试的重要性,特别是当跟业务数据打交道的时候的,Moq就如雪中送炭,所以想学习这个框架,就从这篇 ...

  7. Vue项目build打包部署到Tomcat后,刷新报404错误解决方案

    问题描述: 一.更新依赖,并打包项目 cd /root/.jenkins/workspace/v-test;npm installcd /root/.jenkins/workspace/v-test; ...

  8. 6.方法_EJ

    第38条: 检查参数的有效性 对于这一条,最常见的莫过于检查参数是否为null. 有时出现调用方未检查传入的参数是否为空,同时被调用方也没有检查参数是否为空,结果这就导致两边都没检查以至于出现null ...

  9. 51单片机:IO口扩展芯片用法(74HC165,74HC595)

    IO口扩展芯片,主要是解决单片机IO口太少. 74HC165:数据从并转串 74HC595:数据从串转并 两种芯片,都是通过时序电路,加上移位功能,进行数据传输 74HC165:数据从并转串.以下实例 ...

  10. 用函数式编程对JavaScript进行断舍离

    译者按: 当从业20的JavaScript老司机学会函数式编程时,他扔掉了90%的特性,也不用面向对象了,最后发现了真爱啊!!! 原文: How I rediscovered my love for ...