2017-06-27


上篇文章简要介绍了Linux进程调度,以及结合源代码窥探了下CFS的调度实例。但是没有深入内部区分析调度下面的操作,比如就绪队列的维护以及进程时间的更新等。本节就这些问题做深入讨论。

回想进程调度,在thread_info中有一个重调度位,标识当前进程是否需要被调度,如果该位被设置表明当前进程需要被调度,在那么就调用调度器,执行下一个进程。但是该位是如何被设置的呢?换句话说,什么时候会设置该值,主要有以下几个地方

  • 1、时钟中断
  • 2、主动礼让
  • 3、唤醒进程,检查优先级
  • 4、更改进程的调度策略
  • 5、fork进程

今天我们重点说说时钟中断的情况,即进程的周期调度器。周期调度器正式通过时钟中断触发的。时钟中断的内容请参考前面关于时间管理部分,这里仅仅是从时钟中断引入周期调度器。为了简便,我们以普通的周期时钟为例,这种情况下中断处理程序为timer_interrupt,每次时钟中断都是调用update_process_times来更新进程的运行时间,之后会调用一个函数scheduler_tick,这个就是扮演的周期调度器的角色。之前讲进程调度的时候也有提到,进程调度依赖于具体的调度类,目前普通进程一般采用CFS做为调度算法,所以这里我们以CFS调度类为例做介绍。

CFS调度类的周期调度函数为task_tick_fair,看下代码

 static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &curr->se; for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
entity_tick(cfs_rq, se, queued);
} if (sched_feat_numa(NUMA))
task_tick_numa(rq, curr); update_rq_runnable_avg(rq, );
}

这里操作的是调度实体sched_entity,这样就把具体的调度对象抽象化,借助于调度实体可以调度进程、也可以调度进程组。所以这里是for_each……我们按照调度单个进程来讲,里面调用了entity_tick,该函数主要实现两个功能1、通过update_curr更新当前进程的运行时间,注意这里是主要更新vruntime和调度实体的时间字段,然后调用check_preempt_tick检查抢占。

1、vruntime的更新

update_curr()->__update_curr,在此之前我们还是回顾下调度实体中的几个关于时间的字段

struct sched_entity {
  …… /*每次周期调度器执行,当前时间和exec_start之差被计算,然后更新exec_start到当前时间,差值被加到sum_exec_runtime*/
    u64            exec_start;
/*记录进程在CPU 上运行的总时间,在update_curr更新*/
u64 sum_exec_runtime;
/*进程的虚拟时间*/
u64 vruntime;
/*当进程从CPU 挪下,当前的sum_exec_runtime移动到prev_sum_exec_runtime 在进程抢占的时候会用到,但是并不会清除sum_exec_runtime,该值仍然是持续增长的 */
u64 prev_sum_exec_runtime;
  ……
}

现在看下函数代码:

static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_of(cfs_rq)->clock_task;
unsigned long delta_exec; if (unlikely(!curr))
return; /*
* Get the amount of time the current task was running
* since the last time we changed load (this cannot
* overflow on 32 bits):
*/
/*上次更新到现在的时间差*/
delta_exec = (unsigned long)(now - curr->exec_start);
if (!delta_exec)
return; __update_curr(cfs_rq, curr, delta_exec);
/*更新exec_start*/
curr->exec_start = now; if (entity_is_task(curr)) {
struct task_struct *curtask = task_of(curr); trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
cpuacct_charge(curtask, delta_exec);
account_group_exec_runtime(curtask, delta_exec);
} account_cfs_rq_runtime(cfs_rq, delta_exec);
}

可以看到这里操作的实际上是当前队列的正在运行的调度实体,如果该实体为空,则返回。然后获取上次更新到现在的时间差,如果没有更新那么就没必要接下来的更新。否则调用__update_curr,该函数更新了vruntime;接下来更新exec_start到当前时间。如果当前实体是进程的话,还有接下来的统计操作,最后更新队列总的运行时间。

static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
unsigned long delta_exec)
{
unsigned long delta_exec_weighted; schedstat_set(curr->statistics.exec_max,
max((u64)delta_exec, curr->statistics.exec_max));
/*增加进程的总的运行时间*/
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq, exec_clock, delta_exec);
delta_exec_weighted = calc_delta_fair(delta_exec, curr);
/*随着进程的运行,vruntime会增加,在红黑树中的位置越靠右*/
curr->vruntime += delta_exec_weighted;
update_min_vruntime(cfs_rq);
}

这里增加了进程实际运行的总时间,然后调用calc_delta_fair计算vruntime,vruntime大致是delta*NICE_0_LOAD/curr->load.weight,可以看到vruntime是跟实际运行时间成正比,跟自身权重成反比的,权重越大在运行相同时间的情况下其vruntime增长的越慢,得到调度的机会就越多。得到的结果会加到当前进程的vruntime上,故随着进程的运行,其vruntime会一直增加。潜在的优先级也就越来越低。最后调用update_min_vruntime对队列的min_vrntime进行更新,更新条件是当前进程的vruntime 和下一个要被调度的进程的vruntime做对比,取小的 值。如果该值大于cfs_rq的vruntime,则更新,否则不更新。

2、check_preempt_tick检查抢占

static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
unsigned long ideal_runtime, delta_exec;
struct sched_entity *se;
s64 delta; ideal_runtime = sched_slice(cfs_rq, curr);
/*进程的执行时间*/
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
/*如果执行时间超过分配时间,则设置冲调度位*/
if (delta_exec > ideal_runtime) {
resched_task(rq_of(cfs_rq)->curr);
/*
* The current task ran long enough, ensure it doesn't get
* re-elected due to buddy favours.
*/
clear_buddies(cfs_rq, curr);
return;
} /*
* Ensure that a task that missed wakeup preemption by a
* narrow margin doesn't have to wait for a full slice.
* This also mitigates buddy induced latencies under load.
*/
if (delta_exec < sysctl_sched_min_granularity)
return; se = __pick_first_entity(cfs_rq);
delta = curr->vruntime - se->vruntime; if (delta < )
return; if (delta > ideal_runtime)
resched_task(rq_of(cfs_rq)->curr);
}

该函数主要判断当前进程是不是运行的时间已经足够长了,是的话就设置重调度位(仅仅是设置重调度位,在调度器检查调度的时候才会真正执行调度)。分为一下三种情况

  • 当前进程运行时间大于ideal_runtime,就设置重调度位。
  • 当前进程运行时间小于sysctl_sched_min_granularity,就返回继续运行。
  • 当前进程的vruntime大于下一个即将调度进程的vruntime,且差值大于ideal_runtime,设置重调度位。

说到这里需要介绍下几个变量,内核中有个ideal_runtime的概念,保证每个进程运行都至少运行一个固定的时间,默认情况该值为sysctl_sched_latency,同时又一个就绪进程的数目sched_nr_latency,当当前可运行进程数目大于该值时,运行时间sysctl_sched_latency也要相应扩展。同时内核中还有规定一个进程运行的最短时间sysctl_sched_min_granularity,当进程数目超过sched_nr_latency,就是根据sysctl_sched_min_granularity得到的ideal_runtime,ideal_runtime=sysctl_sched_min_granularity*nr_running

总结:根据以上的介绍可以发现,vruntime所影响的仅仅是得到调度的机会,一个优先级高的进程比优先级低的进程更可能得到调度,但是随着优先级高的进程运行时间的增加,其vruntime会逐渐增大,而随着优先级低的进程总有机会得到调度。就进程每次运行的时间而言,高优先级和低优先级的进程每次分到的时间是一样的。CFS的公平体现在减少了高优先级和低优先级进程在调度时带来的待遇的不平等,让低优先级的进程始终有机会得到调度。

参考资料:

linux3.10.1内核源码

深入linux内核架构

linux中的周期调度器的更多相关文章

  1. 关于windows中的任务管理调度器

    windows中的任务管理调度器 任务管理调度器大概就是给windows设置一个任务,同时还可以设置这个任务的执行时间,执行次数等. 这个任务管理调度器是公司培训同事在讲studio中的job可以在s ...

  2. [UE4]Child Widget中的事件调度器

    在Child Widget中新建事件调度器,就会自动在使用该Child Widget的父级界面的事件列表中自动自动出现.功能十分强大.

  3. 【操作系统作业-lab4】 linux 多线程编程和调度器

    linux多线程编程 参考:https://blog.csdn.net/weibo1230123/article/details/81410241 https://blog.csdn.net/skyr ...

  4. 在linux中使用包管理器安装node.js

    网上文章中,在linux下安装node.js都是使用源码编译,其实node的github上已经提供了各个系统下使用各自的包管理器(package manager)安装node.js的方法. 1. 在U ...

  5. Linux中rpm包管理器

    包全名: 1.操作的包是没有安装的软件包时,使用全名,而且要注意路径 2.例如:jdk-8u131-linux-x64.rpm包名: 1.操作的是已经安装好的软件包,使用包名,是搜索/var/lib/ ...

  6. Linux调度器 - deadline调度器

    一.概述 实时系统是这样的一种计算系统:当事件发生后,它必须在确定的时间范围内做出响应.在实时系统中,产生正确的结果不仅依赖于系统正确的逻辑动作,而且依赖于逻辑动作的时序.换句话说,当系统收到某个请求 ...

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

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

  8. Linux核心调度器之周期性调度器scheduler_tick--Linux进程的管理与调度(十八)

    我们前面提到linux有两种方法激活调度器:核心调度器和 周期调度器 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要 因 ...

  9. 第一次作业:基于Linux 4.5的进程模型与调度器分析

    1.操作系统是怎么组织进程的? 1.1什么是线程,什么是进程: 刚接触时可能经常会将这两个东西搞混.简单一点的说,进程是一个大工程,线程则是这个大工程中每个小地方需要做的东西(在linux下看作&qu ...

随机推荐

  1. linux 短信收发

    #include <termios.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h> ...

  2. JackSon解析json字符串

    JackSon解析json字符串 原文:http://blog.csdn.net/java_huashan/article/details/9353903 概述 jackson解析json例子 准备工 ...

  3. Vim快捷键操作命令大全

        Vim是一个超牛的编辑器,命令功能十分强大 .而且这些命令大都可以进行组合 ,比如,9yy命令表示复制9行内容,9表示要复制的行数,同样100dd表示删除100行,当数字和命令合作的时候,就比 ...

  4. 【BZOJ】1670: [Usaco2006 Oct]Building the Moat护城河的挖掘(凸包)

    http://www.lydsy.com/JudgeOnline/problem.php?id=1670 裸打了凸包.. #include <cstdio> #include <cs ...

  5. 《linux系统及其编程》实验课记录(四)

    实验4:组织目录和文件 实验目标: 熟悉几个基本的操作系统文件和目录的命令的功能.语法和用法, 整理出一个更有条理的主目录,每个文件都位于恰当的子目录. 实验背景: 你的主目录中已经积压了一些文件,你 ...

  6. LoadRunner中winsocket协议学习

    首先让我们先看一下loadrunner- winsock 函数 一览表:        lrs_accept_connection 接受侦听套接字连接 lrs_close_socket 关闭打开的套接 ...

  7. PowerShell----Automatic_Variables(预定义变量)

    以下这些变量是由powershell创建和维护的.ls Variable: 可以获取到所有默认的变量, 每个版本的Powershell可能有差异 $$包含会话所收到的最后一行中的最后一个令牌. $? ...

  8. Spring security UserDetailsService autowired注入失败错误

    最近使用spring mvc + spring security 实现登录权限控制的时候,一直不能成功登录,检查过后是dao一直无法注入为null CustomUserDetailConfig.jav ...

  9. Math函数

    floor --将一个小数向下舍入为整数 float floor ( float $value ) 注意:floor返回的虽然是取整的数字 但是类型仍然是float类型. 实例: echo floor ...

  10. AWS系列-EC2实例添加磁盘

    注意:添加的磁盘,必须和挂载的实例是在同一可用区. 1.1 如下图,打开EC2控制台,打开卷,点击创建卷 1.2 选择磁盘配置 磁盘类型:如下图 磁盘大小:如图,最小500G,最大16T 可用区:注意 ...