深入解读Linux进程调度Schedule

长文慎入~~

调度系统是现代操作系统非常核心的基础子系统之一,尤其在多任务并行操作系统(Multitasking OS)上,系统可能运行于单核或者多核CPU上,进程可能处于运行状态或者在内存中可运行等待状态。如何实现多任务同时使用资源并且提供给用户及时的响应实现实时交互以及提供高流量并发等对现代操作系统的设计实现带来了巨大挑战,而Linux调度子系统的设计同样需要实现这些看似矛盾的要求,适应不同的使用场景。

我们看到Linux是一个复杂的现在操作系统,各个子系统之间相互合作才能完成高效的任务。本文从围绕调度子系统,介绍了调度子系统核心的概念,并且将其与Linux各个相关组件的关系进行探讨,尤其是与调度子系统息息相关的中断(softirq和irq)子系统以及定时器Timer,深入而全面地展示了调度相关的各个概念以及相互联系。

由于笔者最近在调试PowerPC相关的芯片,因此相关的介绍会以此为例提取相关的内核源代码进行解读展示。涉及的代码为Linux-4.4稳定发布版本,读者可以查看源码进行对照。

1. 相关概念

要理解调度子系统,首先需要总体介绍调度的流程,对系统有一个高屋建瓴的认识之后,再在整体流程中对各个节点分别深入分析,从而掌握丰富而饱满的细节。

在系统启动早期,会注册硬件中断,时钟中断是硬件中断中非常重要的一种,调度过程中需要不断地刷新进程的状态以及设置调度标志已决定是否抢占进程的执行进行调度。时钟中断就是周期性地完成此项工作。这里又引出另外一个现代OS的调度设计思想即抢占(preempt),而与其对应的概念则为非抢占或者合作(cooperate),后面会给出两者的详细区别。时钟中断属于硬件中断,Linux系统不支持中断嵌套,所以在中断发生时又会禁止本地中断(local_irq_disable),而为了尽快相应其他可能的硬件事件,必须要尽快完成处理并开启中断,因此引出了中断下半部,也就是softirq的概念。同样在调度过程中有很多定时器(Timer/Hrtimer)会被启动来完成相应的工作。在调度发生时,针对进程的资源需求类型又有不同的调度策略,因此出现了不同的调度类,以实现不同的调度算法完成不同场景下的需求。因此本文从中断和软中断,定时器和高精度定时器,抢占和非抢占,实时和普通进程调度,锁合并发等角度进行深入分析,并将相关的概念联系起来,以完成对Linux内核调度子系统的解读。

1.1 Preemptive

Preemptive Multitasking系统上,调度器决定运行中的进程何时中止运行换出而新的进程开始执行,该过程称为抢占Preemption,而抢占前的进程运行时间一般为提前设定的时间片(Timeslice),时间片的设定与进程优先级有关,根据实际的调度类方法决定,调度类后面会具体介绍。在定时器中断处理过程中对进程的运行时间vruntime进行刷新,如果已经超过了进程可运行的时间片,则设置当前进程current的thread_info flag的调度标志TIF_NEED_RESCHED,在下一个调度入口会调用need_resched函数判断该标志,如果被设置则会进入调度过程,换出当前进程并选择新进程开始执行。关于调度入口,下面章节会进行详细介绍。

1.2 Cooperative

非抢占的Cooperative Multitasking系统最大的特点就是进程只有在主动决定放弃CPU的时候才开始调度其他进程执行,称为yielding,调度器无法控制全局的进程运行状态和时间,这其中最大的缺点就是挂起的进程可能会导致整个系统停止运行,无法调度。进程在因为需要等待特定的信号活着事件发生时会放弃CPU而进入睡眠,通过主动调用schedule进入调度。

1.3 Nice

系统普通进程一般会设定一个数值Nice来决定其优先级,在用户空间可以通过nice系统调用设置进程的nice值。Nice取值范围在-20 ~ +19,进程时间片大小一般根据nice值进行调整,nice值越高则进程时间片一般会分配越小,通过ps -el可以查看。nice可以理解为对别的进程nice一些。

1.4 Real-time priority

进程实时优先级,与nice为两个不同维度的考量,取值范围为0 ~ 99,值越大则其优先级越高,一般实时进程real-time process的优先级高于普通进程normal process。ps -eo state,uid,pid,ppid,rtprio,time,comm可以查看具体信息,其中-代表进程非实时,数值代表实时优先级。

2. 调度器的类型

根据任务的资源需求类型可以将其分为IO-bounced和Processor-bounced进程,其中IO-bounced可以较为广义的理解,比如网络设备以及键盘鼠标等,实时性要求较高,但是CPU占用可能并不密集。Processor-bounced进程对CPU的使用较为密集,比如加密解密过程,图像处理等。针对任务类型区分调度,可以实现较好的体验,提高实时性的交互,同时可以预留大量的CPU资源给计算密集型的进程。所以在调度设计中采用了复杂的算法保证及时响应以及大吞吐量。

有五种调度类:

  • fair_sched_class,现在较高版本的Linux上也就是CFS(Completely Fair Scheduler),Linux上面主要的调度方式,由CONFIG_FAIR_GROUP_SCHED宏控制
  • rt_sched_class,由CONFIG_RT_GROUP_SCHED宏控制,实时调度类型。
  • dl_sched_class,deadline调度类,实时调度中较高级别的调度类型,一般之后在系统紧急情况下会调用;
  • stop_sched_class,最高优先级的调度类型,属于实时调度类型的一种,在系统终止时会在其上创建进程进入调度。
  • idle_sched_class,优先级最低,在系统空闲时才会进入该调度类型调度,一般系统中只有一个idle,那就是初始化进程init_task,在初始化完成后它将自己设置为idle进程,并不做更多工作。

3. 调度子系统的初始化

start_kernel函数调用sched_init进入调度的初始化。首先分配alloc_size大小的内存,初始化root_task_group,root_task_group为系统默认的task group,系统启动阶段每个进程都属于该task group需要注意root_task_group中的成员是针对perCPU的。初始化完成之后将init_task标记为idle进程。具体看下面函数中的注释。

void __init sched_init(void)
{
	int i, j;
	unsigned long alloc_size = 0, ptr;

	/* calculate the size to be allocated for root_task_group items.
	 * some items in the struct task_group are per-cpu fields, so use 
	 * no_cpu_ids here.
	 */
#ifdef CONFIG_FAIR_GROUP_SCHED
	alloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif
#ifdef CONFIG_RT_GROUP_SCHED
	alloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif
	if (alloc_size) {
		/* allocate mem here. */
		ptr = (unsigned long)kzalloc(alloc_size, GFP_NOWAIT);

#ifdef CONFIG_FAIR_GROUP_SCHED
		root_task_group.se = (struct sched_entity **)ptr;
		ptr += nr_cpu_ids * sizeof(void **);

		root_task_group.cfs_rq = (struct cfs_rq **)ptr;
		ptr += nr_cpu_ids * sizeof(void **);

#endif /* CONFIG_FAIR_GROUP_SCHED */
#ifdef CONFIG_RT_GROUP_SCHED
		root_task_group.rt_se = (struct sched_rt_entity **)ptr;
		ptr += nr_cpu_ids * sizeof(void **);

		root_task_group.rt_rq = (struct rt_rq **)ptr;
		ptr += nr_cpu_ids * sizeof(void **);

#endif /* CONFIG_RT_GROUP_SCHED */
	}
#ifdef CONFIG_CPUMASK_OFFSTACK
	/* Use dynamic allocation for cpumask_var_t, instead of putting them on the stack. 
	 * This is a bit more expensive, but avoids stack overflow. 
	 * Allocate load_balance_mask for every cpu below.
	 */
	for_each_possible_cpu(i) {
		per_cpu(load_balance_mask, i) = (cpumask_var_t)kzalloc_node(
			cpumask_size(), GFP_KERNEL, cpu_to_node(i));
	}
#endif /* CONFIG_CPUMASK_OFFSTACK */

	/* init the real-time task group cpu time percentage. 
	 * the hrtimer of def_rt_bandwidth is initialized here.
	 */
	init_rt_bandwidth(&def_rt_bandwidth,
			global_rt_period(), global_rt_runtime());
	/* init the deadline task group cpu time percentage. */
	init_dl_bandwidth(&def_dl_bandwidth,
			global_rt_period(), global_rt_runtime());

#ifdef CONFIG_SMP
	/* 初始化默认调度域,调度域包含一个或者多个CPU,负载均衡是在调度域之内执行,相互之间进行隔离 */
	init_defrootdomain();
#endif

#ifdef CONFIG_RT_GROUP_SCHED
	init_rt_bandwidth(&root_task_group.rt_bandwidth,
			global_rt_period(), global_rt_runtime());
#endif /* CONFIG_RT_GROUP_SCHED */

#ifdef CONFIG_CGROUP_SCHED
	/* 将分配并初始化好的邋root_task_group加入到錿ask_groups全局链表 */
	list_add(&root_task_group.list, &task_groups);
	INIT_LIST_HEAD(&root_task_group.children);
	INIT_LIST_HEAD(&root_task_group.siblings);
	/* 初始化自动分组 */
	autogroup_init(&init_task);

#endif /* CONFIG_CGROUP_SCHED */

	/* 遍历每个cpu的运行队列,对其进行初始化 */
	for_each_possible_cpu(i) {
		struct rq *rq;

		rq = cpu_rq(i);
		raw_spin_lock_init(&rq->lock);
		/* CPU运行队列的所有调度实体(sched_entity)的数目 */
		rq->nr_running = 0;
		/* CPU负载 */
		rq->calc_load_active = 0;
		/* 负载更新时间 */
		rq->calc_load_update = jiffies + LOAD_FREQ;
		/* 分别初始化运行队列的cfs rt和dl队列 */
		init_cfs_rq(&rq->cfs);
		init_rt_rq(&rq->rt);
		init_dl_rq(&rq->dl);
#ifdef CONFIG_FAIR_GROUP_SCHED
		/* root的CPU总的配额 */
		root_task_group.shares = ROOT_TASK_GROUP_LOAD;
		INIT_LIST_HEAD(&rq->leaf_cfs_rq_list);
		/*
		 * How much cpu bandwidth does root_task_group get?
		 *
		 * In case of task-groups formed thr' the cgr
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值