文章目录
- [1. 前言](#1. 前言)
- [2. 概念](#2. 概念)
- [3. 实时进程 的 带宽限制](#3. 实时进程 的 带宽限制)
-
- [3.1 实时进程 带宽限制 初始化](#3.1 实时进程 带宽限制 初始化)
- [3.2 启动 实时进程 带宽 监测定时器](#3.2 启动 实时进程 带宽 监测定时器)
- [3.3 累加 实时进程 消耗的带宽](#3.3 累加 实时进程 消耗的带宽)
- [3.4 查看 实时进程 带宽消耗情况](#3.4 查看 实时进程 带宽消耗情况)
- [3.5 小结](#3.5 小结)
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 概念
Linux 实时调度器(RT scheduler)
的带宽限制
,是指限制系统中实时进程占用的 CPU 时间的配额、比例
。和实时进程打过交道的读者,应该有见过如下内核日志:
bash
[ 7957.249361] sched: RT throttling activated
这条日志表示系统中的实时进程消耗的 CPU 时间,已经超过了设置的配额。每个 CPU
分配给实时进程的默认时间配额为 95%
:
c
/*
* period over which we measure -rt task CPU usage in us.
* default: 1s
*/
unsigned int sysctl_sched_rt_period = 1000000;
......
/*
* part of the period that we allow rt tasks to run in us.
* default: 0.95s
*/
int sysctl_sched_rt_runtime = 950000;
实时进程的 CPU 时间配额,是以 sysctl_sched_rt_period
为一个周期
进行统计的;每个统计周期内,分配给实时进程的时间配额
为 sysctl_sched_rt_runtime
。默认配置下实时进程 CPU 时间配额占比为 950000 / 1000000 = 95%
。
3. 实时进程 的 带宽限制
实时进程的带宽限制
,就是限制实时进程 CPU 时间占比
。
3.1 实时进程 带宽限制 初始化
初始化的过程,主要是设置:
bash
1. 实时进程带宽检查周期(sysctl_sched_rt_period)
2. 每周期实时进程允许占用的 CPU 时间上限(sysctl_sched_rt_runtime)
3. 实时进程带宽周期监测定时器
c
start_kernel()
sched_init()
c
void __init sched_init(void)
{
...
init_rt_bandwidth(&def_rt_bandwidth, global_rt_period(), global_rt_runtime());
...
for_each_possible_cpu(i) {
struct rq *rq;
rq = cpu_rq(i); /* 返回 CPU @i 的运行队列 */
...
/* 初始化 RT 运行队列 每周期 实时进程 运行时间配额 */
rq->rt.rt_runtime = def_rt_bandwidth.rt_runtime;
...
}
...
}
static inline u64 global_rt_period(void)
{
return (u64)sysctl_sched_rt_period * NSEC_PER_USEC;
}
static inline u64 global_rt_runtime(void)
{
if (sysctl_sched_rt_runtime < 0)
return RUNTIME_INF;
return (u64)sysctl_sched_rt_runtime * NSEC_PER_USEC;
}
void init_rt_bandwidth(struct rt_bandwidth *rt_b, u64 period, u64 runtime)
{
rt_b->rt_period = ns_to_ktime(period);
rt_b->rt_runtime = runtime;
raw_spin_lock_init(&rt_b->rt_runtime_lock);
/* 初始化 RT 运行队列消耗 CPU 时间、每周期 检查更新 定时器 */
hrtimer_init(&rt_b->rt_period_timer,
CLOCK_MONOTONIC, HRTIMER_MODE_REL);
rt_b->rt_period_timer.function = sched_rt_period_timer;
}
3.2 启动 实时进程 带宽 监测定时器
在实时进程插入到运行队列时,启动实时进程带宽周期监测定时器。过程中,代码会检查是否已经激活定时器,如果已经激活,则不会重复启动定时器。
c
/* kernel/sched/rt.c */
enqueue_task_rt()
enqueue_rt_entity(rt_se, flags)
__enqueue_rt_entity(rt_se, flags)
inc_rt_tasks(rt_se, rt_rq)
inc_rt_group(rt_se, rt_rq)
start_rt_bandwidth(&def_rt_bandwidth)
static void start_rt_bandwidth(struct rt_bandwidth *rt_b)
{
...
raw_spin_lock(&rt_b->rt_runtime_lock);
if (!rt_b->rt_period_active) { /* 判定周期监测定时器是否已经激活 */
rt_b->rt_period_active = 1; /* 设置激活标记,防止周期监测定时器到期前重复激活 */
/* 启动实时进程带宽周期监测定时器 */
hrtimer_forward_now(&rt_b->rt_period_timer, ns_to_ktime(0));
hrtimer_start_expires(&rt_b->rt_period_timer, HRTIMER_MODE_ABS_PINNED);
}
raw_spin_unlock(&rt_b->rt_runtime_lock);
}
3.3 累加 实时进程 消耗的带宽
累加实时进程消耗的带宽
,是指统计实时进程消耗的 CPU 时间
。细节如下:
c
/* kernel/sched/rt.c */
static void update_curr_rt(struct rq *rq)
{
struct task_struct *curr = rq->curr;
struct sched_rt_entity *rt_se = &curr->rt;
u64 delta_exec;
...
delta_exec = rq_clock_task(rq) - curr->se.exec_start;
...
curr->se.exec_start = rq_clock_task(rq)
...
if (!rt_bandwidth_enabled())
return; /* 未开启实时进程带宽控制,直接返回 */
for_each_sched_rt_entity(rt_se) {
struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
if (sched_rt_runtime(rt_rq) != RUNTIME_INF) {
raw_spin_lock(&rt_rq->rt_runtime_lock);
rt_rq->rt_time += delta_exec; /* 统计实时进程消耗的 CPU 时间(带宽)到运行队列 */
if (sched_rt_runtime_exceeded(rt_rq)) /* 检查实时进程带宽是否超出设定值 */
resched_curr(rq);
raw_spin_unlock(&rt_rq->rt_runtime_lock);
}
}
}
static int sched_rt_runtime_exceeded(struct rt_rq *rt_rq)
{
/* 一个周期内,实时进程允许消耗 CPU 总时间的上限值: @rt_rq->rt_runtime */
u64 runtime = sched_rt_runtime(rt_rq);
...
/*
* 实时进程运行队列是每 CPU 的,实时进程消耗的 CPU 时间
* 是分别统计到每个运行队列的。
* 如果当前 CPU 运行队列上实时进程消耗总时间已经超过设定
* 值 @rt_rq->rt_runtime ,balance_runtime() 尝试从别的 CPU
* 借取一部分时间 - 如果别的 CPU 有空闲的分配给实时进程的
* 时间的话。
*
* 如果向别的 CPU 借取了时间的话,这时候 @rt_rq->rt_runtime
* 会大于设定的阈值(但会限定在一个周期时间内),即 @rt_rq->rt_runtime
* 发生了变化,所以需要通过 sched_rt_runtime() 重新读取。
*/
balance_runtime(rt_rq);
runtime = sched_rt_runtime(rt_rq);
...
if (rt_rq->rt_time > runtime) { /* 当前 CPU 上发生了实时进程超过限定带宽的情况 */
struct rt_bandwidth *rt_b = sched_rt_bandwidth(rt_rq);
if (likely(rt_b->rt_runtime)) { /* 设定了实时进程带宽限制 */
rt_rq->rt_throttled = 1; /* 标记当前 CPU 对实时进程限流 */
printk_deferred_once("sched: RT throttling activated\n");
} else {
...
}
/* CPU 上实时进程限流处理: 将实时进程移出运行队列 */
if (rt_rq_throttled(rt_rq)) {
sched_rt_rq_dequeue(rt_rq);
return 1; /* 返回 1,表示发生了限流 */
}
}
return 0; /* 返回 0,表示没有限流 */
}
3.4 查看 实时进程 带宽消耗情况
每当 3.2
中启动的实时进程带宽监测定时器到期,查看一下实时进程带宽消耗情况,并做相应处理。细节如下:
c
/* kernel/sched/rt.c */
static enum hrtimer_restart sched_rt_period_timer(struct hrtimer *timer)
{
struct rt_bandwidth *rt_b =
container_of(timer, struct rt_bandwidth, rt_period_timer);
int idle = 0;
int overrun;
raw_spin_lock(&rt_b->rt_runtime_lock);
for (;;) {
/*
* 重启定时器。
* 超时时间: @rt_b->rt_period,即 监测周期
*/
overrun = hrtimer_forward_now(timer, rt_b->rt_period);
if (!overrun)
break;
raw_spin_unlock(&rt_b->rt_runtime_lock);
idle = do_sched_rt_period_timer(rt_b, overrun);
raw_spin_lock(&rt_b->rt_runtime_lock);
}
...
raw_spin_unlock(&rt_b->rt_runtime_lock);
...
}
static int do_sched_rt_period_timer(struct rt_bandwidth *rt_b, int overrun)
{
int i, idle = 1, throttled = 0;
const struct cpumask *span;
span = sched_rt_period_mask();
...
for_each_cpu(i, span) {
int enqueue = 0;
struct rt_rq *rt_rq = sched_rt_period_rt_rq(rt_b, i);
struct rq *rq = rq_of_rt_rq(rt_rq);
int skip;
...
raw_spin_lock(&rq->lock);
update_rq_clock(rq);
if (rt_rq->rt_time) {
u64 runtime;
raw_spin_lock(&rt_rq->rt_runtime_lock);
if (rt_rq->rt_throttled) /* 如果 运行队列当前处于 带宽限制 状态,*/
balance_runtime(rt_rq); /* 如果别的 CPU 有多的 实时进程的运行时间,从别的 CPU 借一些 */
runtime = rt_rq->rt_runtime;
/*
* 周期性定时器到期后,将 运行队列消耗总时间 减掉 周期时间:
* 运行队列总时间 按 每周期 进行检查,看是否超过了允许的比例。
*/
rt_rq->rt_time -= min(rt_rq->rt_time, overrun*runtime);
/* 如果 运行队列 处于 带宽限制 状态,且 运行队列 的 实时进程运行时间 还有余额,*/
if (rt_rq->rt_throttled && rt_rq->rt_time < runtime) {
/* 解除 运行队列 的 带宽限制 状态 */
rt_rq->rt_throttled = 0;
enqueue = 1;
...
}
raw_spin_unlock(&rt_rq->rt_runtime_lock);
} else if (rt_rq->rt_nr_running) {
idle = 0;
if (!rt_rq_throttled(rt_rq))
enqueue = 1;
}
...
if (enqueue) /* 解除带宽限制,重新将进程插入运行队列 */
sched_rt_rq_enqueue(rt_rq);
raw_spin_unlock(&rq->lock);
}
...
return idle;
}
3.5 小结
一方面,实时调度器通过累加实时进程的消耗的 CPU 总时间;另一方面,实时调度器启动一个监测定时器,周期性地查看实时进程消耗的 CPU 时间,如果发现当前监测周期(sysctl_sched_rt_period
)内,实时进程消耗的 CPU 时间超过设定的阈值(sysctl_sched_rt_runtime
),则会爆出内核日志:
bash
sched: RT throttling activated
从前面的分析得知,这个日志在多 CPU 场景下,只代表某个 CPU 的实时进程消耗超过了阈值,并非所有。笔者认为,在这个日志里面,加上 CPU 信息可能会更好。另外,出现这个日志,只代表当前的情形,一段时间后,随着系统的运行,实时进程的CPU 消耗可能会恢复到带宽控制阈值范围内。
最后,Linux 内核提供了修改实时进程带宽监测周期 sysctl_sched_rt_period
,以及周期内 CPU 消耗带宽阈值 sysctl_sched_rt_runtime
的用户接口:
bash
/proc/sys/kernel/sched_rt_period_us
/proc/sys/kernel/sched_rt_runtime_us
接口代码实现如下:
c
/* kernel/sysctl.c */
static struct ctl_table kern_table[] = {
...
{
.procname = "sched_rt_period_us",
.data = &sysctl_sched_rt_period,
.maxlen = sizeof(unsigned int),
.mode = 0644,
.proc_handler = sched_rt_handler,
},
{
.procname = "sched_rt_runtime_us",
.data = &sysctl_sched_rt_runtime,
.maxlen = sizeof(int),
.mode = 0644,
.proc_handler = sched_rt_handler,
},
...
};
通过同一接口 sched_rt_handler()
修改 /proc/sys/kernel/sched_rt_period_us
和 /proc/sys/kernel/sched_rt_runtime_us
:
c
int sched_rt_handler(struct ctl_table *table, int write,
void __user *buffer, size_t *lenp,
loff_t *ppos)
{
int old_period, old_runtime;
static DEFINE_MUTEX(mutex);
int ret;
mutex_lock(&mutex);
old_period = sysctl_sched_rt_period;
old_runtime = sysctl_sched_rt_runtime;
/*
* write == 0: 读 sysctl_sched_rt_period 或 sysctl_sched_rt_runtime
* write == 1: 写 sysctl_sched_rt_period 或 sysctl_sched_rt_runtime
*/
ret = proc_dointvec(table, write, buffer, lenp, ppos);
/* 改写 sysctl_sched_rt_period 或 sysctl_sched_rt_runtime */
if (!ret && write) { /* 写操作 */
...
/* 将新写入的值应用到运行队列 */
ret = sched_rt_global_constraints();
if (ret)
goto undo;
sched_rt_do_global(); /* 更新 RT 调度器的带宽控制参数 def_rt_bandwidth */
sched_dl_do_global(); /* 更新 DL 调度器的带宽控制参数 def_dl_bandwidth */
}
if (0) {
undo:
sysctl_sched_rt_period = old_period;
sysctl_sched_rt_runtime = old_runtime;
}
mutex_unlock(&mutex);
return ret;
}
从上面的代码分析可以看到,对 /proc/sys/kernel/sched_rt_period_us
和 /proc/sys/kernel/sched_rt_runtime_us
的修改,不仅影响 RT(Real-Time)
实时调度器,也影响 DL(DeadLine)
实时调度器。