Linux内核对比学习系列(3)——进程调度

本文深入对比了Linux 0.12与2.6版本在进程调度上的差异。0.12版本采用简单的调度算法,遍历任务结构数组,选择counter最大的进程;而2.6版本引入了就绪队列、调度器类和调度实体等概念,支持可扩展的调度策略,通过调度器类选择进程,并通过时钟中断和标志位TIF_NEED_RESCHED实现灵活的调度时机。此外,介绍了2.6版本如何通过时钟中断设置重调度标志和中断返回时的调度流程。
摘要由CSDN通过智能技术生成

前言

linux0.12与linux2.6在进程调度的实现上有很大的不同,在此进行记录

Linux 0.12

该版本对于进程调度算法的实现十分简单,具体实现看schedule()即可,该函数为调度入口
kernel/sched.c

void schedule(void)
{
	int i,next,c;
	struct task_struct ** p;
    // ----------------省略检查相关代码------------------

/* this is the scheduler proper: */

	while (1) {
		c = -1;
		next = 0;
		i = NR_TASKS;
		p = &task[NR_TASKS];
		while (--i) {
			if (!*--p)
				continue;
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
				c = (*p)->counter, next = i;
		}
		if (c) break;
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;
	}
	switch_to(next);
}

由上可知晓该版本调度全过程,无非遍历 task_struct 指针数组(task),判断是否有进程的 counter(允许运行时间)大于0,若有则找出 counter 最大的,否则对所有进程根据优先级(priority)重新分配 counter ,再遍历找出 counter 最大的进行切换(switch_to)

说到调度,除了如何执行调度还需要了解何时调度。根据之前文章,我们知道时钟中断对应的中断执行程序为timer_interrupt,其触发do_timer 进行当前时间片的检查,对于该版本而言,do_timer()先判断当前进程counter是否耗尽,才会执行schedule()。这意味着,该版本的进程是不可抢占的。(倘若分配的时间片很多,则其他进程将久久得不到相应)

void do_timer(long cpl)
{
	//-----省略无关代码---------
	if (next_timer) {
		next_timer->jiffies--;
		while (next_timer && next_timer->jiffies <= 0) {
			void (*fn)(void);
			
			fn = next_timer->fn;
			next_timer->fn = NULL;
			next_timer = next_timer->next;
			(fn)();
		}
	}
	if (current_DOR & 0xf0)
		do_floppy_timer();
	if ((--current->counter)>0) return;
	current->counter=0;
	if (!cpl) return;
	schedule();
}

关于0.12版本的进程调度核心内容只有这么多,对于何种调度算法最优在此不进行讨论,只是比较两个版本的实现有何区别

Linux 2.6

在该版本中,为了实现进程调度,相较于0.12版本设计了很多新的概念。笔者进行学习的过程中,参考了 https://www.cnblogs.com/ck1020/p/6089970.html。而《Linux内核设计与实现》中对于该知识点的梳理十分跳跃,对于初学者难以把握精髓。为此,先从原理进行梳理,在进入代码与0.12版本进行对比。

如何调度

相关概念

关于进程调度的一些结构体:

  1. 就绪队列 rq
  2. 调度器 sched_class
  3. 调度实体 sched_entity

调度器结构

与0.12不同,该版本内核并不是直接操作进程的task_struct,在获取到最终需要执行的task_struct之前,还涉及就绪队列,调度器等相关逻辑。首先,如下图所示,主调度器(可以理解为schedule())会先选择调度器类,再由调度器类选择下一个要执行的进程。每个进程只会属于一个调度器类,而调度器类之间会有优先级,主调度器调用时会参考优先级从高到底选择调度器。由于每个cpu上执行的进程不同,为了在多个cpu环境下执行任务,设计就绪队列rq,每个CPU关联一个rq,该rq上记录可以被当前cpu执行的调度器类及其进程。
在这里插入图片描述
因此,schedule()主调度器要做的事情是:

  1. 获取当前就绪队列rq
  2. 找到rq上关联的调度器类
  3. 根据调度器类优先级,按顺序从调度器类中选择进程
  4. 至于每个调度器类会执行怎样的进程选择策略交由调度器类自己定义
  5. 切换任务

如此一来,内核开发人员可以自定义调度器类,使得内核的调度算法变得可扩展。这与dubbo设计loadbanlance接口是类似的。代码如下(省略了非重点代码)
kernel/sched.c

asmlinkage void __sched schedule(void)
{
	struct task_struct *prev, *next;
	unsigned long *switch_count;
	struct rq *rq;
	int cpu;

	// ---------省略无关代码--------
	rq = cpu_rq(cpu); // 1. 获取当前就绪队列rq
	pre_schedule(rq, prev);

	if (unlikely(!rq->nr_running))
		idle_balance(cpu, rq);

	put_prev_task(rq, prev);
	next = pick_next_task(rq); // 2. 找到rq上关联的调度器类。3. 根据调度器类优先级,按顺序从调度器类中选择进程

	if (likely(prev != next)) {
		sched_info_switch(prev, next);
		perf_event_task_sched_out(prev, next);

		rq->nr_switches++;
		rq->curr = next;
		++*switch_count;

		context_switch(rq, prev, next); // 4. 切换任务
		/*
		 * the context switch might have flipped the stack from under
		 * us, hence refresh the local variables.
		 */
		cpu = smp_processor_id();
		rq = cpu_rq(cpu);
	} else
		raw_spin_unlock_irq(&rq->lock);

	// ---------省略无关代码--------	
}

进一步地跟踪pick_next_task()。根据变量及函数名我们大致能够猜测该函数完成了几件事

  1. 判断就绪队列rq上就绪态的进程数nr_running是否等于调度器cfs的就绪态进程数,如果相等,说明进程都在调度器cfs中,直接执行调度器cfs的pick方法选择进程
  2. 否则,从优先级最高的调度器类sched_class_highest开始遍历,调用其pick方法,如果当前调度器找不到进程,则去后一个调度器里找
    kernel/sched.c
static inline struct task_struct *
pick_next_task(struct rq *rq)
{
	const struct sched_class *class;
	struct task_struct *p;

	/*
	 * Optimization: we know that if all tasks are in
	 * the fair class we can call that function directly:
	 */
	if (likely(rq->nr_running == rq->cfs.nr_running)) {
		p = fair_sched_class.pick_next_task(rq);
		if (likely(p))
			return p;
	}

	class = sched_class_highest;
	for ( ; ; ) {
		p = class->pick_next_task(rq);
		if (p)
			return p;
		/*
		 * Will never be NULL as the idle class always
		 * returns a non-NULL p:
		 */
		class = class->next;
	}
}

具体地,以cfs调度器类为例,看看其pick方法如何找到一个待执行的进程。根据该调度器类的定义,我们知道知道其pick_next_task方法对应的是pick_next_task_fair

kernel\sched_fair.c

static const struct sched_class fair_sched_class = {
	.next			= &idle_sched_class,
	.enqueue_task		= enqueue_task_fair,
	.dequeue_task		= dequeue_task_fair,
	.yield_task		= yield_task_fair,

	.check_preempt_curr	= check_preempt_wakeup,

	.pick_next_task		= pick_next_task_fair,
	.put_prev_task		= put_prev_task_fair,
	.task_tick		= task_tick_fair,
	.task_fork		= task_fork_fair,
};

可以看到,该方法先是通过rq获取cfs_rq,再调用该类的pick_next_entity方法从cfs_rq上获得一个sched_entity,其中sched_entity对应一个进程或者一个进程组,是一个调度单位。由此我们也知道了,实际上,所有的进程或者进程组都被封装为一个调度实体sched_entity并关联着rq。而每个调度器类只是提供了在rq上找出目标调度实体的方法而已,它并不关联和存储任何调度实体内容

kernel\sched_fair.c

static struct task_struct *pick_next_task_fair(struct rq *rq)
{
	struct task_struct *p;
	struct cfs_rq *cfs_rq = &rq->cfs;
	struct sched_entity *se;

	if (!cfs_rq->nr_running)
		return NULL;

	do {
		se = pick_next_entity(cfs_rq);
		set_next_entity(cfs_rq, se);
		cfs_rq = group_cfs_rq(se);
	} while (cfs_rq);

	p = task_of(se);
	hrtick_start_fair(rq, p);

	return p;
}

在这里,由于sched_entity是task_struct的属性,因此通过task_of(se)调用container_of可以由sched_entity找到task的位置。container_of是内核十分常见的函数了,也是C语言魅力的体现。

更进一步地,pick_next_entity()就是具体地CFS调度算法了,这部分内容可以回到《Linux内核设计与实现》中找到答案。在梳理清楚调度的整个流程后,我们进一步地去学习每个CFS调度算法才能更得心应手。这也是本篇文章的目的

何时调度

前半部分我们了解了schedule()执行一次进程调度的大致流程。那么该版本何时会触发schedule()进行调度呢?即类似0.12版本的时钟中断,timer_interrupt和do_timer的功能由哪些函数来实现?

本节内容参考了几个博文

直接说结论:

  1. 主动调度:直接显式调用schedule()进行调度
  2. 被动调度(延时调度):系统调用返回或中断返回时,会检查进程的thread_info中的flag是否标记为TIF_NEED_RESCHED,若被设置,则意味着需要被调度,执行schedule()主调度器。而TIF_NEED_RESCHED可以由用户定义程序进行设置。

该版本的调度时机判断相较于0.12版本复杂了不少,需要通过标志位flag进行判断。在0.12版本中,进程被动调度一定要在当前进程时间片耗尽才能触发,这样导致其他进程无法抢占,实时交互需求无法满足。在2.6版本中,是否需要调度,只与当前进程标志位flag有关,因此可以自定义flag的设置算法,满足具体需求,来实现合理的调度策略。

设置重调度标志

最简单地,我们希望每次时钟中断都判断一下当前进程是否满足被调度条件,若满足,则把flag设置为TIF_NEED_RESCHED。下面,我们跟踪一下具体流程,可以看到update_process_times的注释提到其与timer interrupt关联,具体地再往前跟踪就涉及时钟管理内容,因此本文打算直接从该函数开始往后跟踪

kernel\timer.c

/*
 * Called from the timer interrupt handler to charge one tick to the current
 * process.  user_tick is 1 if the tick is user time, 0 for system.
 */
void update_process_times(int user_tick)
{
	struct task_struct *p = current;
	int cpu = smp_processor_id();

	/* Note: this timer irq context must be accounted for as well. */
	account_process_tick(p, user_tick);
	run_local_timers();
	rcu_check_callbacks(cpu, user_tick);
	printk_tick();
	perf_event_do_pending();
	scheduler_tick();
	run_posix_cpu_timers(p);
}

进一步地,该函数调用了scheduler_tick()。该函数调用当前进程所在调度器的task_tick方法

kernel\sched.c

void scheduler_tick(void)
{
	int cpu = smp_processor_id();
	struct rq *rq = cpu_rq(cpu);
	struct task_struct *curr = rq->curr;

	sched_clock_tick();

	raw_spin_lock(&rq->lock);
	update_rq_clock(rq);
	update_cpu_load(rq);
	curr->sched_class->task_tick(rq, curr, 0);
	raw_spin_unlock(&rq->lock);

	perf_event_task_tick(curr);

#ifdef CONFIG_SMP
	rq->idle_at_tick = idle_cpu(cpu);
	trigger_load_balance(rq, cpu);
#endif
}

我们以CFS公平调度器类为例,其task_ticktask_tick_fair(由上述fair_sched_class定义可知)

kernel\sched_fair.c

static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &curr->se;

	for_each_sched_entity(se) {
		cfs_rq = cfs_rq_of(se);
		entity_tick(cfs_rq, se, queued);
	}
}

该函数会进一步执行 entity_tick-->check_preempt_tick。在该函数中,设置了一些与CFS有关的条件,若满足则通过resched_task设置重调度标志位

kernel\sched_fair.c

//kernel\sched.c
static void resched_task(struct task_struct *p)
{
	assert_raw_spin_locked(&task_rq(p)->lock);
	set_tsk_need_resched(p);
}

static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
	unsigned long ideal_runtime, delta_exec;

	ideal_runtime = sched_slice(cfs_rq, curr);
	delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
	if (delta_exec > ideal_runtime) {
		resched_task(rq_of(cfs_rq)->curr);
		/*
		 * The current task ran long enough, ensure it doesn't get
		 * re-elected due to buddy favours.
		 */
		clear_buddies(cfs_rq, curr);
		return;
	}

	/*
	 * Ensure that a task that missed wakeup preemption by a
	 * narrow margin doesn't have to wait for a full slice.
	 * This also mitigates buddy induced latencies under load.
	 */
	if (!sched_feat(WAKEUP_PREEMPT))
		return;

	if (delta_exec < sysctl_sched_min_granularity)
		return;

	if (cfs_rq->nr_running > 1) {
		struct sched_entity *se = __pick_next_entity(cfs_rq);
		s64 delta = curr->vruntime - se->vruntime;

		if (delta > ideal_runtime)
			resched_task(rq_of(cfs_rq)->curr);
	}
}

至此,我们大概清楚了通过时钟中断设置进程重调度标志的流程

  1. 时钟中断执行程序调用update_process_times
  2. update_process_times通过scheduler_tick调用当前进程所在调度器类的task_tick方法
  3. 在task_tick方法中,定义与调度有关的条件,判断是否满足条件来进行flag的设置

中断返回执行调度

另一方面,我们还要验证一下如中断返回时是否会根据flag标志执行schedule()
我们知道中断返回会调用ret_from_intr,其进一步调用resume_kernel

arch\x86\kernel\entry_32.S

ret_from_intr:
	GET_THREAD_INFO(%ebp)
check_userspace:
	movl PT_EFLAGS(%esp), %eax	# mix EFLAGS and CS
	movb PT_CS(%esp), %al
	andl $(X86_EFLAGS_VM | SEGMENT_RPL_MASK), %eax
	cmpl $USER_RPL, %eax
	jb resume_kernel		# not returning to v8086 or userspace

ENTRY(resume_kernel)
	DISABLE_INTERRUPTS(CLBR_ANY)
	cmpl $0,TI_preempt_count(%ebp)	# non-zero preempt_count ?
	jnz restore_all
need_resched:
	movl TI_flags(%ebp), %ecx	# need_resched set ?
	testb $_TIF_NEED_RESCHED, %cl
	jz restore_all
	testl $X86_EFLAGS_IF,PT_EFLAGS(%esp)	# interrupts off (exception path) ?
	jz restore_all
	call preempt_schedule_irq
	jmp need_resched
END(resume_kernel)

resume_kernel则调用preempt_schedule_irq,可以看到,该函数会根据need_resched()执行schedule()。至此,验证完毕

kernel\sched.c

asmlinkage void __sched preempt_schedule_irq(void)
{
	struct thread_info *ti = current_thread_info();

	/* Catch callers which need to be fixed */
	BUG_ON(ti->preempt_count || !irqs_disabled());

	do {
		add_preempt_count(PREEMPT_ACTIVE);
		local_irq_enable();
		schedule();
		local_irq_disable();
		sub_preempt_count(PREEMPT_ACTIVE);

		/*
		 * Check again in case we missed a preemption opportunity
		 * between schedule and now.
		 */
		barrier();
	} while (need_resched());
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值