雀恰营销
专注中国网络营销推广

Linux进程组调度机制分析

Linux进程组调度机制分析

又遇到一个神奇的进程调度问题。系统重启过程中发现系统挂了,30s后重启。系统重置的真正原因是硬件看门狗重新启动了系统,而不是原来的正常重启。过程。硬件狗记录的复位时间将狗没有前馈的时间​​推30s分析串口记录日志,当时的日志打印一句:“sched: RT throttlingactivated”。

从linux-3.0.101-0.7.17版本的内核代码可以看出,sched_rt_runtime_exceeded打印了这句话。在内核进程组调度的过程中,实时进程调度受限于rt_rq->rt_throttled。下面详细说说linux中的进程组调度机制。

文章目录

进程组调度机制

调度是cgroup中的一个概念,指的是把N个进程看成一个整体,参与系统中的调度进程,具体体现在例子中:任务A有8个进程或线程,任务B有2个进程或线程,如果还有其他进程或线程,需要控制任务A的CPU使用率不高于40%,任务B的CPU使用率不高于40%,其他任务的使用率控制在不少于 20%。阈值设置,cgroup A设置为200,cgroup B设置为200,其他任务默认设置为100Linux进程组调度机制分析,这样就实现了CPU控制的功能。

在内核中,进程组是由task_group来管理的,其中很多都涉及到cgroup控制机制,开发单位在写,这里指的是重点描述组调度的部分,详细见后面的注释。

struct task_group {
struct cgroup_subsys_state css;
//下面是普通进程调度使用
#ifdef CONFIG_FAIR_GROUP_SCHED
/* schedulable entities of this group on each cpu *///普通进程调度单元,之所以用调度单元,因为被调度的可能是一个进程,也可能是一组进程
struct sched_entity **se;
/* runqueue "owned" by this group on each cpu *///公平调度队列
struct cfs_rq **cfs_rq;
//下面就是如上示例的控制阀值
unsigned long shares;
atomic_t load_weight;
#endif
#ifdef CONFIG_RT_GROUP_SCHED
//实时进程调度单元
struct sched_rt_entity **rt_se;
//实时进程调度队列
struct rt_rq **rt_rq;
//实时进程占用CPU时间的带宽(或者说比例)
struct rt_bandwidth rt_bandwidth;
#endif
struct rcu_head rcu;
struct list_head list;
//task_group呈树状结构组织,有父节点,兄弟链表,孩子链表,内核里面的根节点是root_task_group
struct task_group *parent;
struct list_head siblings;
struct list_head children;
#ifdef CONFIG_SCHED_AUTOGROUP
struct autogroup *autogroup;
#endif
struct cfs_bandwidth cfs_bandwidth;
};

调度单元有两种,即普通调度单元和实时进程调度单元。

struct sched_entity {
struct load_weightload;/* for load-balancing */struct rb_noderun_node;
struct list_headgroup_node;
unsigned inton_rq;
u64exec_start;
u64sum_exec_runtime;
u64vruntime;
u64prev_sum_exec_runtime;
u64nr_migrations;
#ifdef CONFIG_SCHEDSTATS
struct sched_statistics statistics;
#endif
#ifdef CONFIG_FAIR_GROUP_SCHED
//当前调度单元归属于某个父调度单元
struct sched_entity*parent;
/* rq on which this entity is (to be) queued: *///当前调度单元归属的父调度单元的调度队列,即当前调度单元插入的队列
struct cfs_rq*cfs_rq;
/* rq "owned" by this entity/group: *///当前调度单元的调度队列,即管理子调度单元的队列,如果调度单元是task_group,my_q才会有值
//如果当前调度单元是task,那么my_q自然为NULL
struct cfs_rq*my_q;
#endif
void *suse_kabi_padding;
};
struct sched_rt_entity {
struct list_head run_list;
unsigned long timeout;
unsigned int time_slice;
int nr_cpus_allowed;
struct sched_rt_entity *back;
#ifdef CONFIG_RT_GROUP_SCHED
//实时进程的管理和普通进程类似,下面三项意义参考普通进程
struct sched_rt_entity*parent;
/* rq on which this entity is (to be) queued: */struct rt_rq*rt_rq;
/* rq "owned" by this entity/group: */struct rt_rq*my_q;
#endif
};

再来看看调度队列,因为实时调度和普通调度队列有类似的选项。以实时队列为例:

struct rt_rq {
struct rt_prio_array active;
unsigned long rt_nr_running;
#if defined CONFIG_SMP || defined CONFIG_RT_GROUP_SCHED
struct {
int curr; /* highest queued rt task prio */#ifdef CONFIG_SMP
int next; /* next highest */#endif
} highest_prio;
#endif
#ifdef CONFIG_SMP
unsigned long rt_nr_migratory;
unsigned long rt_nr_total;
int overloaded;
struct plist_head pushable_tasks;
#endif
//当前队列的实时调度是否受限
int rt_throttled;
//当前队列的累计运行时间
u64 rt_time;
//当前队列的最大运行时间
u64 rt_runtime;
/* Nests inside the rq lock: */raw_spinlock_t rt_runtime_lock;
#ifdef CONFIG_RT_GROUP_SCHED
unsigned long rt_nr_boosted;
//当前实时调度队列归属调度队列
struct rq *rq;
struct list_head leaf_rt_rq_list;
//当前实时调度队列归属的调度单元
struct task_group *tg;
#endif
};

通过以上三种结构的分析,可以得到下图(点击放大):

任务组

从图中可以看出,调度单元和调度队列组合成一个树节点调度,是另外一个单独的树形结构,但是需要注意的是,调度单元只有在有TASK_RUNNING进程的情况下才会放置调度单元。在调度队列中。

还有一点是,在没有组调度之前,每个CPU上只有一个调度队列。那时可以理解为所有进程都在一个调度组中。现在,每个调度组在每个 CPU 上都有一个调度队列。在调度过程中,系统最初选择了一个进程来运行,但现在它选择了一个调度单元来运行。调度发生时,调度过程从root_task_group开始,寻找调度策略确定的调度单元。当调度单元为task_group时,进入task_group。运行队列选择合适的调度单元,最终找到合适的任务调度单元。整个过程就是树的遍历。具有TASK_RUNNING进程的task_group是树的节点,

组进程调度策略

组进程调度的目的和之前一样,即完成实时进程调度和普通进程调度,即rt和cfs调度

CFS 组调度策略:

文章前面例子中提到的任务分配CPU是指cfs调度。对于CFS调度调度单元与普通调度过程并没有太大区别。调度单元有自己的调度优先级,不受调度过程的影响。每个 task_group 都有一个共享。共享的不是我们说的进程优先级,而是调度权重。这是cfs调度管理的概念,但最终体现在cfs中的调度优先级。默认情况下,共享值相同。对于所有没有权重的值,按照旧的cfs管理分配CPU。综上所述,cfs 组调度策略并没有改变。具体来说说cgroup的CPU控制机制。

RT组调度策略:

实时进程的优先级是固定的,调度器总是选择优先级最高的进程运行。在组调度中,调度单元的优先级是组中优先级最高的调度单元的优先级值,即调度单元的优先级受子调度单元的影响。如果一个进程进入调度单元,那么它的所有父调度单元的调度队列都会重新入队。其实我们看到的结果是调度器总是选择优先级最高的实时进程调度,那么组调度是如何影响实时进程控制机制的呢?

在前面的rt_rq实时进程运行队列中,提到了rt_time和rt_runtime,一个是累计运行时间,一个是最大运行时间。当累计运行时间超过最大运行时间时调度,rt_throttled设置为1,见sched_rt_runtime_exceeded函数。

Linux进程组调度机制分析

if (rt_rq->rt_time > runtime) {
rt_rq->rt_throttled = 1;
if (rt_rq_throttled(rt_rq)) {
sched_rt_rq_dequeue(rt_rq);
return 1;
}
}

设置为1表示实时队列受限,比如__enqueue_rt_entity函数Linux进程组调度机制分析,不能入队。

static inline int rt_rq_throttled(struct rt_rq *rt_rq)
{
return rt_rq->rt_throttled && !rt_rq->rt_nr_boosted;
}
static void __enqueue_rt_entity(struct sched_rt_entity *rt_se, bool head)
{
/*
 * Don't enqueue the group if its throttled, or when empty.
 * The latter is a consequence of the former when a child group
 * get throttled and the current group doesn't have any other
 * active members.
 */if (group_rq && (rt_rq_throttled(group_rq) || !group_rq->rt_nr_running))
return;
.....
}

其实还有一个隐藏的时间概念,即sched_rt_period_us,意思是在sched_rt_period_us内,实时进程可以占用CPU rt_runtime时间。如果实时进程没有在每个时间段内调度,则在 do_sched_rt_period_timer 定时器函数中将 rt_time 减一。循环,然后比较rt_runtime,恢复rt_throttled。

//overrun来自对周期时间定时器误差的校正
rt_rq->rt_time -= min(rt_rq->rt_time, overrun*runtime);
if (rt_rq->rt_throttled && rt_rq->rt_time rt_throttled = 0;
enqueue = 1;

cgroup控制实时进程的占用率是通过rt_runtime实现的,对于root_task_group,即所有进程都在一个cgroup下,是通过/proc/sys/kernel/sched_rt_period_us和/proc/sys/kernel/sched_rt_runtime_us接口设置,默认值为1s和0.95s。貌似实时进程只能占用95%的CPU,那么实时进程是怎么占用100%的CPU导致进程挂掉的呢?

原来是实时进程所在的CPU超时了。实时进程的rt_runtime可以借用其他CPU,剩余的rt_runtime-rt_time值可以借用其他CPU。这样rt_time最多可以等于rt_runtime,使得实际单核CPU达到100.%。这样做的目的是避免由于缺乏 CPU 时间而将实时进程迁移到其他内核的成本。没有核的普通进程也可以不经过调度就迁移到其他CPU上。当然,核心绑定流程仍然是一个杯子。

static int do_balance_runtime(struct rt_rq *rt_rq)
{
struct rt_bandwidth *rt_b = sched_rt_bandwidth(rt_rq);
struct root_domain *rd = cpu_rq(smp_processor_id())->rd;
int i, weight, more = 0;
u64 rt_period;
weight = cpumask_weight(rd->span);
raw_spin_lock(&rt_b->rt_runtime_lock);
rt_period = ktime_to_ns(rt_b->rt_period);
for_each_cpu(i, rd->span) {
struct rt_rq *iter = sched_rt_period_rt_rq(rt_b, i);
s64 diff;
if (iter == rt_rq)
continue;
raw_spin_lock(&iter->rt_runtime_lock);
/*
 * Either all rqs have inf runtime and there's nothing to steal
 * or __disable_runtime() below sets a specific rq to inf to
 * indicate its been disabled and disalow stealing.
 */if (iter->rt_runtime == RUNTIME_INF)
goto next;
/*
 * From runqueues with spare time, take 1/n part of their
 * spare time, but no more than our period.
 */diff = iter->rt_runtime - iter->rt_time;
if (diff > 0) {
diff = div_u64((u64)diff, weight);
if (rt_rq->rt_runtime + diff > rt_period)
diff = rt_period - rt_rq->rt_runtime;
iter->rt_runtime -= diff;
rt_rq->rt_runtime += diff;
more = 1;
if (rt_rq->rt_runtime == rt_period) {
raw_spin_unlock(&iter->rt_runtime_lock);
break;
}
}
next:
raw_spin_unlock(&iter->rt_runtime_lock);
}
raw_spin_unlock(&rt_b->rt_runtime_lock);
return more;
}

先写到这里,待续。

参考:

Linux进程组调度机制分析来自OenHan

链接是:

赞(0) 打赏
未经允许不得转载:雀恰营销 » Linux进程组调度机制分析
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!

 

文章对你有帮助就赞助我一下吧

支付宝扫一扫打赏

微信扫一扫打赏