Linux 实时调度器:带宽限制

文章目录

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

相关推荐
眠修9 分钟前
Kuberrnetes 服务发布
linux·运维·服务器
即将头秃的程序媛3 小时前
centos 7.9安装tomcat,并实现开机自启
linux·运维·centos
fangeqin3 小时前
ubuntu源码安装python3.13遇到Could not build the ssl module!解决方法
linux·python·ubuntu·openssl
爱奥尼欧4 小时前
【Linux 系统】基础IO——Linux中对文件的理解
linux·服务器·microsoft
超喜欢下雨天5 小时前
服务器安装 ros2时遇到底层库依赖冲突的问题
linux·运维·服务器·ros2
tan77º6 小时前
【Linux网络编程】网络基础
linux·服务器·网络
笑衬人心。6 小时前
Ubuntu 22.04 + MySQL 8 无密码登录问题与 root 密码重置指南
linux·mysql·ubuntu
chanalbert8 小时前
CentOS系统新手指导手册
linux·运维·centos
星宸追风8 小时前
Ubuntu更换Home目录所在硬盘的过程
linux·运维·ubuntu
热爱生活的猴子9 小时前
Poetry 在 Linux 和 Windows 系统中的安装步骤
linux·运维·windows