Linux 实时调度器:带宽限制

文章目录

  • 1. 前言
  • 2. 概念
  • 3. 实时进程 的 带宽限制
    • 3.1 实时进程 带宽限制 初始化
    • 3.2 启动 实时进程 带宽 监测定时器
    • 3.3 累加 实时进程 消耗的带宽
    • 3.4 查看 实时进程 带宽消耗情况
    • 3.5 小结

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 概念

Linux 实时调度器(RT scheduler)带宽限制,是指限制系统中实时进程占用的 CPU 时间的配额、比例。和实时进程打过交道的读者,应该有见过如下内核日志:

[ 7957.249361] sched: RT throttling activated

这条日志表示系统中的实时进程消耗的 CPU 时间,已经超过了设置的配额。每个 CPU 分配给实时进程的默认时间配额为 95%

/*
 * 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 实时进程 带宽限制 初始化

初始化的过程,主要是设置:

1. 实时进程带宽检查周期(sysctl_sched_rt_period)
2. 每周期实时进程允许占用的 CPU 时间上限(sysctl_sched_rt_runtime)
3. 实时进程带宽周期监测定时器
start_kernel()
	sched_init()
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 启动 实时进程 带宽 监测定时器

在实时进程插入到运行队列时,启动实时进程带宽周期监测定时器。过程中,代码会检查是否已经激活定时器,如果已经激活,则不会重复启动定时器。

/* 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 时间。细节如下:

/* 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 中启动的实时进程带宽监测定时器到期,查看一下实时进程带宽消耗情况,并做相应处理。细节如下:

/* 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),则会爆出内核日志:

sched: RT throttling activated

从前面的分析得知,这个日志在多 CPU 场景下,只代表某个 CPU 的实时进程消耗超过了阈值,并非所有。笔者认为,在这个日志里面,加上 CPU 信息可能会更好。另外,出现这个日志,只代表当前的情形,一段时间后,随着系统的运行,实时进程的CPU 消耗可能会恢复到带宽控制阈值范围内。

最后,Linux 内核提供了修改实时进程带宽监测周期 sysctl_sched_rt_period,以及周期内 CPU 消耗带宽阈值 sysctl_sched_rt_runtime 的用户接口:

/proc/sys/kernel/sched_rt_period_us
/proc/sys/kernel/sched_rt_runtime_us

接口代码实现如下:

/* 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

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) 实时调度器。

你可能感兴趣的:(#,进程调度,Linux,实时调度器,带宽限制)