实时调度类源码分析
Linux 实时进程与普通进程的根本不同之处,系统中有一个实时进程且可运行,调度器总是会选择它,除非另有一个优先级更高的实时进程。
SCHED_FIFO:没有时间片,在调度器被选择之后,可以运行任意长时间;
SCHED_RR:有时间片,其值在进程运行时会减少。
实时调度实体sched_rt_entity数据结构及操作
进程的插入、选择、删除三种基本操作。
//实体
struct sched_rt_entity {
struct list_head run_list;
unsigned long timeout; // watchdog计数器 主要用于判断当前进程时间是否超过RLIMIT_RTIME
unsigned long watchdog_stamp;
unsigned int time_slice; // 针对RR调度策略的调度时隙
struct sched_rt_entity *back; // dequeue_rt_stack() 中作为临时变量
#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
};
//类
const struct sched_class rt_sched_class = {
.next = &fair_sched_class,
.enqueue_task = enqueue_task_rt, //将一个task放入到就绪队列头部或者尾部
.dequeue_task = dequeue_task_rt, // 将一个task从就绪队列末尾删除
.yield_task = yield_task_rt, //主动放弃执行
.check_preempt_curr = check_preempt_curr_rt,
.pick_next_task = pick_next_task_rt, // 核心调度器 选择就绪队列的某个任务将被调度
.put_prev_task = put_prev_task_rt, // 当一个任务将要被调度的时候执行
#ifdef CONFIG_SMP
.select_task_rq = select_task_rq_rt, //核心调度器给任务选定CPU 将任务分发到不同的CPU上执行
.set_cpus_allowed = set_cpus_allowed_common,
.rq_online = rq_online_rt,
.rq_offline = rq_offline_rt,
.task_woken = task_woken_rt,
.switched_from = switched_from_rt,
#endif
.set_curr_task = set_curr_task_rt, // 当任务修改其调度类或修改其它任务组时,将调用这个函数
.task_tick = task_tick_rt, // 当时钟中断触发时将被调用,主要更新新进程运行统计信息及是否需要调度
.get_rr_interval = get_rr_interval_rt,
.prio_changed = prio_changed_rt,
.switched_to = switched_to_rt,
.update_curr = update_curr_rt,
};
//进程插入操作
/*
* Adding/removing a task to/from a priority array:
*/
// 更新调度信息,将调度实体插入到对应优先级队列末尾
static void
enqueue_task_rt(struct rq *rq, struct task_struct *p, int flags)
{
struct sched_rt_entity *rt_se = &p->rt;
if (flags & ENQUEUE_WAKEUP)
rt_se->timeout = 0;
// 实际工作
// 将当前实时调度实体添加到对应优先级链表上面,添加到头部还是尾部取决于flags是否包含ENQUEUE_HEAD来判断
enqueue_rt_entity(rt_se, flags & ENQUEUE_HEAD);
if (!task_current(rq, p) && p->nr_cpus_allowed > 1)
enqueue_pushable_task(rq, p); //添加到hash表中
}
// 进程选择操作
// 实时调度会选择最高优先级的实时进程来运行。
static struct task_struct *_pick_next_task_rt(struct rq *rq)
{
struct sched_rt_entity *rt_se;
struct task_struct *p;
struct rt_rq *rt_rq = &rq->rt;
do { //遍历组调度中的每一个进程
rt_se = pick_next_rt_entity(rq, rt_rq);
BUG_ON(!rt_se);
rt_rq = group_rt_rq(rt_se);
} while (rt_rq);
p = rt_task_of(rt_se);
// 更新执行域
p->se.exec_start = rq_clock_task(rq);
return p;
}
static struct sched_rt_entity *pick_next_rt_entity(struct rq *rq,
struct rt_rq *rt_rq)
{
struct rt_prio_array *array = &rt_rq->active;
struct sched_rt_entity *next = NULL;
struct list_head *queue;
int idx;
// 找到一个可用实体
idx = sched_find_first_bit(array->bitmap);
BUG_ON(idx >= MAX_RT_PRIO);
// 从链表组中找到对应的链表
queue = array->queue + idx;
next = list_entry(queue->next, struct sched_rt_entity, run_list);
// 返回找到运行实体
return next;
}
// 进程删除操作
// 从优先级队列中删除实时进程,并更新调度信息,然后把这个进程添加到队尾。
static void dequeue_task_rt(struct rq *rq, struct task_struct *p, int flags)
{
struct sched_rt_entity *rt_se = &p->rt;
// 更新调度数据信息
update_curr_rt(rq);
// 实际工作,将rt_se从运行队列中删除,然后添加到队尾
dequeue_rt_entity(rt_se);
// 从hash表中删除
dequeue_pushable_task(rq, p);
}
/*
* Update the current task's runtime statistics. Skip current tasks that
* are not in our scheduling class.
*/
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;
// 判断是否有实时调度进程
if (curr->sched_class != &rt_sched_class)
return;
// 执行时间
delta_exec = rq_clock_task(rq) - curr->se.exec_start;
if (unlikely((s64)delta_exec <= 0))
return;
schedstat_set(curr->se.statistics.exec_max,
max(curr->se.statistics.exec_max, delta_exec));
// 更新当前进程总执行时间
curr->se.sum_exec_runtime += delta_exec;
account_group_exec_runtime(curr, delta_exec);
// 更新执行的开始时间
curr->se.exec_start = rq_clock_task(rq);
cpuacct_charge(curr, delta_exec);
sched_rt_avg_update(rq, delta_exec);
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;
if (sched_rt_runtime_exceeded(rt_rq))
resched_curr(rq);
raw_spin_unlock(&rt_rq->rt_runtime_lock);
}
}
}
static void dequeue_rt_entity(struct sched_rt_entity *rt_se)
{
struct rq *rq = rq_of_rt_se(rt_se);
dequeue_rt_stack(rt_se); // 从运行队列中删除
for_each_sched_rt_entity(rt_se) {
struct rt_rq *rt_rq = group_rt_rq(rt_se);
if (rt_rq && rt_rq->rt_nr_running)
__enqueue_rt_entity(rt_se, false);
}
enqueue_top_rt_rq(&rq->rt);
}
对称多处理器SMP
多处理器系统的工作方式分为非对称多处理(asym-metrical mulit-processing)和对称多处理(symmetrical mulit-processing,SMP)两种。
在对称多处理器系统中,所有处理器的地位都是相同的,所有的资源,特别是存储器、中断及I/O空间,都具有相同的可访问性,消除结构上的障碍。
多处理器系统上,内核必须考虑几个额外的问题,以确保良好的调度。
- CPU负荷必须尽可能公平地在所有的处理器上共享。
- 进程与系统中某些处理器的亲合性(affinity)必须是可设置的。
- 内核必须能够将进程从一个CPU迁移到另一个。
linux SMP调度就是将进程安排/迁移到合适的CPU中去,保持各CPU负载均衡的过程。
SMP优点
- 增加吞吐时的一种划算方法;
- 由于操作系统由所有处理器共享,它们提供了一个单独的系统映像(容易管理);
- 对一个单独的问题应用多处理器(并行编程);
- 负载均衡由操作系统实现;
- 单处理器(UP)编程模型可用于一个SMP中;
- 对于共享数据来说,可伸缩;
- 所有数据可由所有处理器寻址,并且由硬件监视逻辑保持连续性;
- 由于通信经由全局共享内存执行,在处理器之间通信不必使用消息传送库;
SMP局限性
- 由于告诉缓存相关性、锁定机制、共享对象和其它问题,可伸缩性受限制;
- 需要新技术来利用多处理器,例如:线程编程和设备驱动程序编程等。
CPU域初始化
Linux内核中有一个数据结构struct sched_domain_topology_level用来描述CPU的层次关系。
内核对CPU的管理是通过bitmap来管理,并且定义possible、present、online、active这4种状态。
struct sched_domain_topology_level {
sched_domain_mask_f mask; //函数指针 用于指定某个SDTL层级的cpumask位置
sched_domain_flags_f sd_flags; //函数指针 用于指定某个SDTL层级的标志位
int flags;
int numa_level;
struct sd_data data;
#ifdef CONFIG_SCHED_DEBUG
char *name;
#endif
};
// 表示系统中有多少个可以运行的CPU核心
const struct cpumask *const cpu_possible_mask = to_cpumask(cpu_possible_bits);
EXPORT_SYMBOL(cpu_possible_mask);
// 表示系统中有多少个正处于运行状态的CPU核心
static DECLARE_BITMAP(cpu_online_bits, CONFIG_NR_CPUS) __read_mostly;
const struct cpumask *const cpu_online_mask = to_cpumask(cpu_online_bits);
EXPORT_SYMBOL(cpu_online_mask);
// 表示系统中有多少个具备online条件的CPU核心,它们不一定都处于online状态,有的CPU核心可能被热插拔。
static DECLARE_BITMAP(cpu_present_bits, CONFIG_NR_CPUS) __read_mostly;
const struct cpumask *const cpu_present_mask = to_cpumask(cpu_present_bits);
EXPORT_SYMBOL(cpu_present_mask);
//表示系统中有多少个活跃的CPU核心
static DECLARE_BITMAP(cpu_active_bits, CONFIG_NR_CPUS) __read_mostly;
const struct cpumask *const cpu_active_mask = to_cpumask(cpu_active_bits);
EXPORT_SYMBOL(cpu_active_mask);
//以上4个变量都是bitmap类型变量。
SMP负载均衡
SMP负载均衡机制从注册软中断开始,每次系统处理调度tick时会检查当前是否需要处 理SMP负载均衡。
负载均衡时机
- 周期性调用进程调度程序scheduler_tick()->trigger_load_balance()中,通过软中断触发负载均衡。
- 某个CPU上无可运行进程,__schedule()准备调度idle进程前,会尝试从其它CPU上拉一批进程过来。
两路4核8核心CPU,CPU调度域逻辑关系
分层角度分析
所有CPU一共分成撒个层次:SMT、MC、NUMA,每层都包含所有CPU,但是划分粒度不同。根据Cache和内存的相关性划分调度域,调度域内的CPU又划分一次调度组。越往下层调度域越小,越往上层调度域越大。进程负载均衡会尽可以在底层调度域内部解决,这样Cache利用率最优。
周期性负载均衡:CPU对应的运行队列数据结构记录下一次周期性负载均衡时间,当超过这个时间点后,将触发SCHED_SOFIRQ软中断来进行负载均衡。
用到SMP负载均衡模型的时机
内核运行中,还有部分情况需要用掉SMP负载均衡模型来确定最佳运行CPU:
- 进程A唤醒进程B时,try_to_wake_up()中会考虑进程B将在哪个CPU上运行;
- 进程调用execve()系统调用时;
- fork出子进程,子进程第一次被调度运行。
Linux运行时调优:
Linux引入重要sysctls来在运行时对调度程序进行调优(单位ns)
sched_child_runs_first: child在fork之后进行调度,为默认设备。如果设置为0,则先调度parent。
sched_min_granularity_ns:针对CPU密集型任务执行最低级别抢占粒度。
sched_latency_ns:针对CPU密集型任务进行目标抢占延迟。
sched_stat_granularity_ns:收集调度程序统计信息的粒度。
总结
本文主要介绍了实时调度类源码分析,包括实时调度数据结构及其相关操作(插入、选择、删除等);SMP的优缺点,负载均衡机制,CPU分层角度分析,linux运行时调优等相关参数介绍等。
技术参考
https://ke.qq.com/webcourse/3294666/103425320#taid=11144559668118986&vid=5285890815288776379