实时调度类及SMP

Posted 打工人打工魂打工人上人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实时调度类及SMP相关的知识,希望对你有一定的参考价值。

实时调度类源码分析

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

以上是关于实时调度类及SMP的主要内容,如果未能解决你的问题,请参考以下文章

NUMA 和 SMP 上的 Linux 调度程序

一文搞懂Linux进程调度原理

(转帖)linux内核SMP负载均衡浅析

[QNX 自适应分区用户指南]9 同时使用线程调度器和多核

OSX AudioUnit SMP

linux 进程调度2