1. 说明
1> linux内核关于task调度这块是比较复杂的,流程也比较长,要从源码一一讲清楚很容易看晕,因此需要简化,抓住主要的一个点,抛开无关的部分才能讲清楚核心思想
2> 本篇文章主要是讲清楚在cfs公平调度算法中,CGroup如何限制cpu使用的主要过程,所以与此无关的代码一律略过
3> 本篇源码来自CentOS7.6的3.10.0-957.el7内核
4> 本篇内容以《极简cfs公平调度算法》为基础,里面讲过的内容这里就不重复了
5> 为了极简,这里略去了CGroup嵌套的情况
 
2. CGroup控制cpu配置
CGroup控制cpu网上教程很多,这里就不重点讲了,简单举个创建名为test的CGroup的基本流程
1> 创建一个/sys/fs/cgroup/cpu/test目录
2> 创建文件cpu.cfs_period_us并写入100000,创建cpu.cfs_quota_us并写入10000
表示每隔100ms(cfs_period_us)给test group分配一次cpu配额10ms(cfs_quota_us),在100ms的周期内,group中的进程最多使用10ms的cpu时长,这样就能限制这个group最多使用单核10ms/100ms = 10%的cpu
3> 最后创建文件cgroup.procs,写入要限制cpu的pid即生效
 
3. CGroup控制cpu基本思想
1> 《极简cfs公平调度算法》中我们讲过cfs调度是以se为调度实例的,而不是task,因为group se也是一种调度实例,所以将调度实例抽象为se,统一以se进行调度
2> CGroup会设置一个cfs_period_us的时长的定时器,定时给group分配cfs_quota_us指定的cpu配额
3> 每次group下的task执行完一个时间片后,就会从group的cpu quota减去该task使用的cpu时长
4> 当group的cpu quota用完后,就会将整个group se throttle,即将其从公平调度运行队列中移出,然后等待定时器触发下个周期重新分配cpu quota后,重启将group se移入到cpu rq上,从而达到控制cpu的效果。
 
一句话说明CGroup的控制cpu基本思想:
进程执行完一个时间片后,从cpu quota中减去其执行时间,当quota使用完后,就将其从rq中移除,这样在一个period内就不会再调度了。
 
4. 极简CGroup控制cpu相关数据结构
4.1 名词解释
 
说明
task group
进程组,为了支持CGroup控制cpu,引入了组调度的概念,task group即包含所有要控制cpu的task集合以及配置信息。
group task
本文的专有名词,是指一个进程组下的task,这些task受一个CGroup控制
cfs_bandwidth
task_group的重要成员,包含了所要控制cpu的period,quota,定时器等信息
throttle
当group se在一个设定的时间周期内,消耗完了指定的cpu配额,则将其从cpu运行队列中移出,并不再调度。
注意:处于throttled状态的task仍是Ready状态的,只是不在rq上。
unthrottle
将throttle状态的group se,重新加入到cpu运行队列中调度。
 
4.2 cfs调度相关数据结构
struct cfs_rq
{
struct rb_root tasks_timeline; // 以vruntime为key,se为value的红黑树根节点,schedule时,cfs调度算法每次从这里挑选vruntime最小的se投入运行
struct rb_node* rb_leftmost; // 最左的叶子节点,即vruntime最小的se,直接取这个节点以加快速度
sched_entity* curr; // cfs_rq中当前正在运行的se
struct rq* rq; /* cpu runqueue to which this cfs_rq is attached */
struct task_group* tg; /* group that "owns" this runqueue */
int throttled; // 表示该cfs_rq所属的group se是否被throttled
s64 runtime_remaining; // cfs_rq从全局时间池申请的时间片剩余时间,当剩余时间小于等于0的时候,就需要重新申请时间片
}; struct sched_entity
{
unsigned int on_rq; // se是否在rq上,不在的话即使task是Ready状态也不会投入运行的
u64 vruntime; // cpu运行时长,cfs调度算法总是选择该值最小的se投入运行
/* rq on which this entity is (to be) queued: */
struct cfs_rq* cfs_rq; // se所在的cfs_rq,如果是普通task se,等于rq的cfs_rq,如果是group中的task,则等于group的cfs_rq
/* rq "owned" by this entity/group: */
struct cfs_rq* my_q; // my_q == NULL表示是一个普通task se,否则表示是一个group se,my_q指向group的cfs_rq
}; struct task
{
struct sched_entity se;
}; struct rq
{
struct cfs_rq cfs; // 所有要调度的se都挂在cfs rq中
struct task_struct* curr; // 当前cpu上运行的task
};

本文中的sched_entity定义比《极简cfs公平调度算法》中的要复杂些,各种cfs_rq容易搞混,这里讲一下cfs公平调度挑选group task调度流程(只用到了my_q这个cfs_rq),以梳理清楚其关系

1> 当se.my_q为NULL时,表示一个task se,否则是group se
 
2> 选择当group task3的流程
 
3> 选择当group task的代码
task_struct *pick_next_task_fair(struct rq *rq)
{
struct cfs_rq *cfs_rq = &rq->cfs; // 开始的cfs_rq为rq的cfs
do {
se = pick_next_entity(cfs_rq); // 《极简cfs公平调度算法》中讲过这个函数,其就是取cfs_rq->rb_leftmost,即最小vruntime的se
cfs_rq = group_cfs_rq(se); // 取se.my_q,如果是普通的task se,cfs_rq = NULL,这里就会退出循环,如果是group se,cfs_rq = group_se.my_q,然后在group se的cfs_rq中继续寻找vruntime最小的se
} while (cfs_rq); return task_of(se);
} cfs_rq *group_cfs_rq(struct sched_entity *grp)
{
return grp->my_q;
}
4.3 CGroup控制cpu的数据结构
struct cfs_bandwidth
{
ktime_t period; // cpu.cfs_period_us的值
u64 quota; // cpu.cfs_quota_us的值
u64 runtime; // 当前周期内剩余的quota时间
int timer_active; // period_timer是否激活
struct hrtimer period_timer; // 定时分配cpu quota的定时器,定时器触发时会更新runtime
}; struct task_group
{
struct sched_entity** se; /* schedulable entities of this group on each cpu */
struct cfs_rq** cfs_rq; /* runqueue "owned" by this group on each cpu */
struct cfs_bandwidth cfs_bandwidth; // 管理记录CGroup控制cpu的信息
};

1> task_group.se是一个数组,每个cpu都有一个其对应的group se

 
2>task_group.cfs_rq也是一个数组,每个cpu都有一个其对应的cfs_rq,每个cpu上的group se.my_q指向该cpu上对应的group cfs_rq,group下的task.se.cfs_rq也指向该group cfs_rq
 
3> cfs_bandwidth是CGroup管理控制cpu的关键数据结构,具体用途见定义
 
5. 极简流程图
从throttle到unthrottle:
 
6. 极简code
6.1 检测group se cpu quota的使用
1>《极简cfs公平调度算法》中我们讲过,task调度的发动机时钟中断触发后,经过层层调用,会到update_curr()这里,update_curr()不仅++了当前se的vruntime,还调用 account_cfs_rq_runtime()统计并检测group se是否使用完了cpu quota
void update_curr(struct cfs_rq* cfs_rq)
{
struct sched_entity* curr = cfs_rq->curr;
curr->vruntime += delta_exec; // 增加se的运行时间
account_cfs_rq_runtime(cfs_rq, delta_exec);
}
 
2> account_cfs_rq_runtime()--了cfs_rq->runtime_remaining,如果runtime_remaining不足就调用assign_cfs_rq_runtime()从task group中分配,当分配不到(即表示当前周期的cpu quota用完了)就设置resched标记
void account_cfs_rq_runtime(struct cfs_rq* cfs_rq, u64 delta_exec)
{
cfs_rq->runtime_remaining -= delta_exec;
if (cfs_rq->runtime_remaining > 0)
return;
// 如果runtime_remaining不够了,则要向task group分配cpu quota,分配失败则设置task的thread flag为TIF_NEED_RESCHED,表示需要重新调度
if (!assign_cfs_rq_runtime(cfs_rq) && likely(cfs_rq->curr))
resched_curr(cfs_rq->rq);
}
3> assign_cfs_rq_runtime()就是从task_group.cfs_bandwidth.runtime减去要分配的时间片,如果其为0就分配失败
/* returns 0 on failure to allocate runtime */
int assign_cfs_rq_runtime(struct cfs_rq* cfs_rq)
{
struct cfs_bandwidth* cfs_b = cfs_rq->tg->cfs_bandwidth;; // 如果有限制cpu,则减去最小分配时间,如果cfs_b->runtime为0,那就没有时间可分配了,本函数就会返回0,表示分配失败
amount = min(cfs_b->runtime, min_amount);
cfs_b->runtime -= amount;
cfs_rq->runtime_remaining += amount;
return cfs_rq->runtime_remaining > 0;
}
6.2 throttle
6.1中我们看到cpu quota被使用完了,标记了resched,要进行重新调度了,但并没有看到throttle。这是因为上面的代码还在中断处理函数中,是不能进行实际调度的,所以只设置resched标记,真正throttle干活的还是在schedule()中(还记得《极简cfs公平调度算法》中讲的task运行时间片到了后,进行task切换,也是这样干的吗?)
 
1> 每次中断返回返回或系统调用返回时(见ret_from_intr),都会判定TIF_NEED_RESCHED标记,如有则会调用schedule()重新调度,《极简cfs公平调度算法》中未暂开讲put_prev_task_fair(),而throttle就是在这里干的
void schedule()
{
prev = rq->curr;
put_prev_task_fair(rq, prev);
// 选择下一个task并切换运行
next = pick_next_task(rq);
context_switch(rq, prev, next);
}
 
2> put_prev_task_fair() → put_prev_entity() → check_cfs_rq_runtime()
void put_prev_task_fair(struct rq* rq, struct task_struct* prev)
{
struct sched_entity* se = &prev->se;
put_prev_entity(se->cfs_rq, se);
} void put_prev_entity(struct cfs_rq* cfs_rq, struct sched_entity* prev)
{
check_cfs_rq_runtime(cfs_rq);
}
3> check_cfs_rq_runtime()这里判定runtime_remaining不足时,就要调用throttle_cfs_rq()进行throttle
void check_cfs_rq_runtime(struct cfs_rq* cfs_rq)
{
if (cfs_rq->runtime_remaining > 0)
return;
throttle_cfs_rq(cfs_rq);
}
4> throttle_cfs_rq()将group se从rq.cfs_rq中移除,这样整个group下的task就不再会被调度了
void throttle_cfs_rq(struct cfs_rq* cfs_rq)
{
struct sched_entity* se = cfs_rq->tg->se[cpu_of(rq_of(cfs_rq))]; // 取对应cpu rq上的group se
dequeue_entity(se->cfs_rq, se, DEQUEUE_SLEEP); //从cpu rq中删除group se
cfs_rq->throttled = 1; // 标记group cfs_rq被throttled
}
6.3 cpu quota重新分配
6.2中group se被从rq移除后,不再会被调度,这时经过一个period周期,定时器激活后,就会再次加入到rq中重新调度
 
1> cfs_bandwidth的定期器初始化回调函数为sched_cfs_period_timer()
viod init_cfs_bandwidth(struct cfs_bandwidth* cfs_b)
{
hrtimer_init(&cfs_b->period_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
cfs_b->period_timer.function = sched_cfs_period_timer;
}
2> 定时器到期后回调sched_cfs_period_timer(),其只是简单调用实际干活的do_sched_cfs_period_timer()
enum hrtimer_restart sched_cfs_period_timer(struct hrtimer* timer)
{
idle = do_sched_cfs_period_timer(cfs_b, overrun);
return idle ? HRTIMER_NORESTART : HRTIMER_RESTART;
}
3> do_sched_cfs_period_timer()调用__refill_cfs_bandwidth_runtime()重新分配task_group的runtime,然后调用distribute_cfs_runtime()进行unthrottle
int do_sched_cfs_period_timer(struct cfs_bandwidth* cfs_b, int overrun)
{
__refill_cfs_bandwidth_runtime(cfs_b);
distribute_cfs_runtime(cfs_b, runtime, runtime_expires);
}
4> __refill_cfs_bandwidth_runtime()就是将task_group.cfs_bandwidth.runtime重置为设置的cpu quota
void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth* cfs_b)
{
cfs_b->runtime = cfs_b->quota;
}
5> distribute_cfs_runtime()调用unthrottle_cfs_rq()将所有se加回到rq上去,这样group下的task就能重新调度了
u64 distribute_cfs_runtime(struct cfs_bandwidth* cfs_b, u64 remaining, u64  expires)
{
struct cfs_rq* cfs_rq;
list_for_each_entry_rcu(cfs_rq, &cfs_b->throttled_cfs_rq, throttled_list)
{
unthrottle_cfs_rq(cfs_rq);
}
} void unthrottle_cfs_rq(struct cfs_rq* cfs_rq)
{
se = cfs_rq->tg->se[cpu_of(rq_of(cfs_rq))];
enqueue_entity(cfs_rq, se, ENQUEUE_WAKEUP); // 将se加回rq.cfs_rq的红黑树上
}
 

极简组调度-CGroup如何限制cpu的更多相关文章

  1. Linux进程组调度机制分析【转】

    转自:http://oenhan.com/task-group-sched 又碰到一个神奇的进程调度问题,在系统重启过程中,发现系统挂住了,过了30s后才重新复位,真正系统复位的原因是硬件看门狗重启的 ...

  2. 极简的js点击组图切换效果

    程序员进行前端开发时,时常要用到点击切换组图的动画效果,网上确实有很多此类插件,但是都很麻烦,乌糟糟无数代码,有那个看的时间,自己都能把功能写完了.在这里我提供一段极简的js点击组图切换效果代码,包含 ...

  3. docker cgroup技术之cpu和cpuset

    在centos7的/sys/fs/cgroup下面可以看到与cpu相关的有cpu,cpuacct和cpuset 3个subsystem.cpu用于对cpu使用率的划分:cpuset用于设置cpu的亲和 ...

  4. cgroup 分析之CPU和内存部分

    https://ggaaooppeenngg.github.io/zh-CN/2017/05/07/cgroups-%E5%88%86%E6%9E%90%E4%B9%8B%E5%86%85%E5%AD ...

  5. 【原创】(四)Linux进程调度-组调度及带宽控制

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

  6. NodeJS 极简教程 <1> NodeJS 特点 & 使用场景

    NodeJS 极简教程 <1> NodeJS 特点 & 使用场景 田浩 因为看开了所以才去较劲儿.   1. NodeJS是什么 1.1 Node.js is a JavaScri ...

  7. WebFetch 是无依赖极简网页爬取组件

    WebFetch 是无依赖极简网页爬取组件,能在移动设备上运行的微型爬虫. WebFetch 要达到的目标: 没有第三方依赖jar包 减少内存使用 提高CPU利用率 加快网络爬取速度 简洁明了的api ...

  8. 极极极极极简的的增删查改(CRUD)解决方案

    去年这个时候写过一篇全自动数据表格的文章http://www.cnblogs.com/liuyh/p/5747331.html.文章对自己写的一个js组件做了个概述,很多人把它当作了一款功能相似的纯前 ...

  9. 极简Python DeBug工具——PySnooper

    DeBug Python 代码的方式有很多种?比如: (1)设置断点 (2)print函数 (3)... 本文要介绍的是一个新开源的项目PySnooper ,只要给有疑问的代码加上装饰器,各种信息一目 ...

  10. 基于Linux-3.9.4的mykernel实验环境的极简内核分析

    382 + 原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/ 一.实验环境 win10 -> VMware -> Ubuntu1 ...

随机推荐

  1. matlab 将某文件夹的内容复制到另一文件下

    %% 清空close all;clear;clc; %% 选择文件路径(复制某文件夹下部分文件夹到其他路径)folder = uigetdir('C:\Desktop','请选择文件夹'); %% i ...

  2. List,Set,Map存取元素各有什么特点 hashMap、hashTable的区别 Arraylist和linkedList的区别

    1.List,Set,Map存取元素各有什么特点? 1.存放 (1)List存放元素是有序,可重复 (2)Set存放元素无序,不可重复 (3)Map元素键值对形式存放,键无序不可重复,值可重复 2.取 ...

  3. spring管理配置文件实现注入

    创建配置文件 写入以下内容: 创建配置文件的bean: <bean id="configProperties" class="org.springframework ...

  4. 容器数据库(CDB)和传统的非容器数据库的区别

    传统的非容器数据库在系统元数据和实例资源没有进行任何共享.容器数据库把 Oracle 提供的元数据.后台进程以及内存结构进行共享,把每个部门数据库的存储结构以 PDB 的形式独立出来,从而实现了系统资 ...

  5. 《JavaScript高级程序设计》Chapter03 JavaScript语言基础

    目录 Syntax Variable var let const Data Type Undefined Null Boolean Number String Symbol Object Operat ...

  6. resnet模型下载

    resnet模型下载: model_urls = { 'resnet18': 'https://s3.amazonaws.com/pytorch/models/resnet18-5c106cde.pt ...

  7. printf( )和scanf( )

    printf()的转换说明 转换说明 输出 %a,%A 浮点数,十六进制数和p记数法 %c 单个字符 %d.%i 有符号的十进制整数 %e,%E 浮点数,e记数法 %f 浮点数,十进制计数法 %g/% ...

  8. Spring Framework学习总结

    一.Spring 概述 Spring 有两个核心部分: IoC 和 AOP. Spring 是一种基于 Bean 的编程技术,它深刻地改变着 Java 开发世界.Spring 使用简单.基本的 Jav ...

  9. win10edge浏览器个人账户退出登录后再次登录自动登录问题

    edge浏览器退出登录后,再次点击登录以同步数据会自动登录,可查看书签等个人数据 解决方法: 先在浏览器里面退出账户. 1.设置--电子邮件和账户--管理 2.登录后--安全--安全仪表板--高级安全 ...

  10. How to Apply WebLogic Server (WLS) Patches Using Smart Update

    本文目的: 描述weblogic10.3.6及之前的版本,如何通过Smart Update打补丁 先决条件: You should download and apply the enhanced Sm ...