admin管理员组

文章数量:1530516

  近期准备分析一下Linux进程调度相关内容,计划从以下几个方面来分析。

  • 1)调度点,分析主动切换和被动切换点。
  • 2)结合armv7架构分析硬件架构的进程上下文切换
  • 3)进程是否需要被调度标记点分析
  • 4)分析各个调度器的具体行为逻辑(如果时间不够就只分析公平调度)

Linux进程调度——schedule函数分析

  • Linux进程调度——schedule函数分析
    • 1. 说明
    • 2. 进程调度时机
    • 3. schedule源码实现
      • 4. switch_mm
      • 5. switch_to

1. 说明

Linux内核版本:4.9.335
架构:armv7

2. 进程调度时机

  __schedule是调度器的核心函数,作用是让调度器选择和切换到一个合适的进程运行。调度的时机可以分为以下几种。
1)阻塞操作:互斥量(mutex)、信号量(semaphore)、等待队列(waitqueue)等。
2)在中断返回用户空间前和系统调用返回用户空间时,会去检查TIF_NEED_RESCHED标志位以判断是否需要调度。
3)将要被唤醒的进程不会马上被调度,而是会被添加到CFS就绪队列中,并且设置TIF_NEED_RESCHED标志。那么被唤醒的进程什么时候被调度?这要根据内核是否具有可抢占功能(CONFIG_PREEMPT=y)分为两种情况。

  如果内核可抢占,则:

  • 如果唤醒动作发生在系统调用或者异常处理上下文中,在下一次调用preempt_enable时会检查是否需要抢占调度。
  • 如果唤醒动作发生在硬中断处理上下文中,硬件中断处理返回前夕会检查是否需要抢占当前进程。

  如果内核不可抢占(抢占内核其实也支持以下几项,多了抢占功能),则:

  • 当前进程调用cond_resched()时会检查是否需要调度。
  • 主动调度调用schedule()。
  • 系统调用或者异常处理返回用户空间时。
  • 中断处理完成返回用户空间时。

3. schedule源码实现

schedule函数定义在kernel/sched/core.c,源码如下:

asmlinkage __visible void __sched schedule(void)
{
	struct task_struct *tsk = current;

	sched_submit_work(tsk);
	do {
		preempt_disable();			(1)
		__schedule(false);			(2)
		sched_preempt_enable_no_resched();	(3)
	} while (need_resched());			(4)
}
EXPORT_SYMBOL(schedule);

1)关闭内核抢占,这里就是禁止内核调度。根据是否开启内核抢占CONFIG_PREEMPT分为以下两种情况

  • 非抢占内核:因为当前代码是在内核态执行的,所以就算代码切换过程中发生了中断,中断处理结束返回内核空间时,也不会发生抢占动作,所以这时候的preempt_disable函数里面只写了个内存屏障防编译器优化。
  • 抢占内核:增加了当前进程thread_info->preempt_count的抢占计数,即在调用__schedule过程中允许发生中断,但是中断返回时禁止切换进程,必须回到当前上下文继续执行进程切换操作。
    2)进程切换主函数。
    3)非抢占内核sched_preempt_enable_no_resched()函数是空操作,抢占内核中就是对preempt_count进行减1操作。
    4)进程重新被唤醒时,有可能被设置抢占标识,需要判断当前进程是否被设置了TIF_NEEW_RESCHED标志(thread_info->flags中设置该bit)。如果设置了,则需要继续把自己调度出去。

ps:这里有一个问题,很显然抢占内核preempt_disable()和sched_preempt_enable_no_resched()需要成对出现。但是进程在调用__schedule过程中就已经切换到别的进程了。所以这里是否存在调度被关掉的问题?其实不然,判断是否关闭内核抢占使用的是当前进程的thread_info数据结构中的变量,随着当前进程被调度出去,切换到新的进程上下文时,新的抢占计数就是针对当前进程而言了。

下面展开看__schedule函数内部实现。

static void __sched notrace __schedule(bool preempt)
{
	struct task_struct *prev, *next;
	unsigned long *switch_count;
	struct pin_cookie cookie;
	struct rq *rq;
	int cpu;

	cpu = smp_processor_id();
	rq = cpu_rq(cpu);							(1)
	prev = rq->curr;

	schedule_debug(prev);

	if (sched_feat(HRTICK))
		hrtick_clear(rq);

	local_irq_disable();							(2)
	rcu_note_context_switch();

	/*
	 * Make sure that signal_pending_state()->signal_pending() below
	 * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
	 * done by the caller to avoid the race with signal_wake_up().
	 */
	smp_mb__before_spinlock();
	raw_spin_lock(&rq->lock);
	cookie = lockdep_pin_lock(&rq->lock);

	rq->clock_skip_update <<= 1; /* promote REQ to ACT */

	switch_count = &prev->nivcsw;
	if (!preempt && prev->state) {						(3)
		if (unlikely(signal_pending_state(prev->state, prev))) {
			prev->state = TASK_RUNNING;
		} else {
			deactivate_task(rq, prev, DEQUEUE_SLEEP);
			prev->on_rq = 0;

			/*
			 * If a worker went to sleep, notify and ask workqueue
			 * whether it wants to wake up a task to maintain
			 * concurrency.
			 */
			if (prev->flags & PF_WQ_WORKER) {
				struct task_struct *to_wakeup;

				to_wakeup = wq_worker_sleeping(prev);
				if (to_wakeup)
					try_to_wake_up_local(to_wakeup, cookie);
			}
		}
		switch_count = &prev->nvcsw;
	}

	if (task_on_rq_queued(prev))
		update_rq_clock(rq);

	next = pick_next_task(rq, prev, cookie);				(4)
	clear_tsk_need_resched(prev);
	clear_preempt_need_resched();
	rq->clock_skip_update = 0;

	if (likely(prev != next)) {						(5)
		rq->nr_switches++;
		rq->curr = next;
		++*switch_count;

		trace_sched_switch(preempt, prev, next);
		rq = context_switch(rq, prev, next, cookie); /* unlocks the rq */(6)
	} else {
		lockdep_unpin_lock(&rq->lock, cookie);
		raw_spin_unlock_irq(&rq->lock);
	}

	balance_callback(rq);
}

1)获取当前cpu rq数据结构(runqueue data structure)。

2)关闭当前cpu本地中断,重新打开的地方有两种情况。

  • line73,当prev==next时,不需要进程切换,解锁rq->lock并重新打开中断
  • line139,进行进程切换时,next进程被唤醒后,从switch_to返回时调用context_switch->finish_task_switch->finish_lock_switch解锁rq->lock并重新打开中断。

3)line33-54代码主要作用是,将状态非TASK_RUNNING的进程剔除自己所在的调度器的就绪队列。这个if判断条件分几种情况来讨论。

  • 非抢占情况:即schedule调用__schedule情况,这时候preempt等于0,仅需要判断第二个条件prev->state。已知TASK_RUNNING宏定义为0,所以判断条件的意思是,所有状态不是就绪态的进程都需要进入if代码执行。

  • 抢占情况:即preempt_schedule_irq等接口调用__schedule时,preempt参数传递的是true条件。举个例子,比如

    DEFINE_WAIT(wait);
    while (1) {
    	...
    	set_current_state(TASK_UNINTREEUPTIBEL);
    	if (condition)
    		break;
    	schedule();
    }
    set_current_state(TASK_RUNNING);
    

    进程W在循环判断条件是否满足,满足则退出循环。在执行完第3行的设置进程状态后,发生了一个硬件中断,中断处理完成后,判断TIF_NEED_RESCHED标志位,此时如果被标记了,则会调用preempt_schedule_irq函数进行抢占调度。这时候如果条件本来就是满足的,本来应该用来唤醒W进程的另一个进程P因为条件满足,认为进程W并不会进行睡眠,所以不进行唤醒操作。这时候如果不对__schedule进入条件(即是否抢占切换)进行判断,则会出现被抢占的进程被剔除就绪队列,再也没办法被唤醒。

继续看33-54行代码,35、36行代码判断是否有需要处理的信号,如果有,当前进程也不应该被踢出就绪队列。
37、38代码调用deactivate_task来将当前进程踢出当前调度器的就绪队列。

void deactivate_task(struct rq *rq, struct task_struct *p, int flags)
{
	if (task_contributes_to_load(p))
		rq->nr_uninterruptible++;

	dequeue_task(rq, p, flags);
}

static inline void dequeue_task(struct rq *rq, struct task_struct *p, int flags)
{
	update_rq_clock(rq);
	if (!(flags & DEQUEUE_SAVE))
		sched_info_dequeued(rq, p);
	p->sched_class->dequeue_task(rq, p, flags);
}

最终调用了调度类(class)的dequeue_task方法。具体调度类相关接口这里不展开说明。

4)字面意思,挑选出下一个要执行的进程。

/*
 * Pick up the highest-prio task:
 */
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct pin_cookie cookie)
{
	const struct sched_class *class = &fair_sched_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(prev->sched_class == class &&
		   rq->nr_running == rq->cfs.h_nr_running)) {				(a)
		p = fair_sched_class.pick_next_task(rq, prev, cookie);
		if (unlikely(p == RETRY_TASK))
			goto again;

		/* assumes fair_sched_class->next == idle_sched_class */
		if (unlikely(!p))							(b)
			p = idle_sched_class.pick_next_task(rq, prev, cookie);

		return p;
	}

again:
	for_each_class(class) {								(c)
		p = class->pick_next_task(rq, prev, cookie);
		if (p) {
			if (unlikely(p == RETRY_TASK))
				goto again;
			return p;
		}
	}

	BUG(); /* the idle class will always have a runnable task */
}

a)如果当前进程的调度类是公平调度,且runqueue中的运行态进程数量与cfs调度器中的运行态进程数量一致,意味着当前cpu上没有其他调度类的进程,直接调用公平调度的pick_next_task回调挑选出下一个要执行的进程即可。
b)如果从cfs调度器中挑选不出下一个要被执行的进程,意味着当前没有需要运行的进程。那么从idle调度器中选出下一个进程。(其实idle调度器只有一个idle进程,这里其实就是挑选idle进程作为下一个进程)
c)遍历所有调度器,挑选出下一个要执行的进程。优先级顺序如下:

stop_sched_class
dl_sched_class
rt_sched_class
fair_sched_class
idle_sched_class

下面继续看__schedule函数
5)如果next进程不等于prev进程,则进行上下文切换。else情况下,如果是同一个进程,那么也就没必要进行上下文切换了。

6)context_switch源码如下:

static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
	       struct task_struct *next, struct pin_cookie cookie)
{
	struct mm_struct *mm, *oldmm;

	prepare_task_switch(rq, prev, next);

	mm = next->mm;
	oldmm = prev->active_mm;
	/*
	 * For paravirt, this is coupled with an exit in switch_to to
	 * combine the page table reload and the switch backend into
	 * one hypercall.
	 */
	arch_start_context_switch(prev);

	if (!mm) {
		next->active_mm = oldmm;
		atomic_inc(&oldmm->mm_count);
		enter_lazy_tlb(oldmm, next);
	} else
		switch_mm_irqs_off(oldmm, mm, next);

	if (!prev->mm) {
		prev->active_mm = NULL;
		rq->prev_mm = oldmm;
	}
	/*
	 * Since the runqueue lock will be released by the next
	 * task (which is an invalid locking op but in the case
	 * of the scheduler it's an obvious special-case), so we
	 * do an early lockdep release here:
	 */
	lockdep_unpin_lock(&rq->lock, cookie);
	spin_release(&rq->lock.dep_map, 1, _THIS_IP_);

	/* Here we just switch the register state and the stack. */
	switch_to(prev, next, prev);
	barrier();

	return finish_task_switch(prev);
}

  line7中prepare_task_switch()->prepare_lock_switch()函数设置next进程的task_struct结构中的on_cpu成员为1,表示next进程马上进入执行状态。on_cpu成员会在mutex和semaphore的自旋等待机制中用到。

  line9~10行,变量mm指向next进程的地址空间描述符struct mm_struct,变量oldmm指向prev进程的正在使用的地址空间描述符(prev->active_mm)。对于普通进程来说,task_struct中的mmactive_mm都指向进程的地址空间描述符mm_struct。但是对于内核线程来说是没有独立的地址空间的(mm=NULL),但是因为进程调度需要用到,所以需要借用一个进程的地址空间,因此有了active_mm成员。

  line18~21,如果next进程的mm为空,表明这是一个内核线程,需要借用prev进程的active_mm。因为prev也可能是个内核线程,所以一直往前借就完事了。增加oldmm->mm_count引用计数。保证债主不会释放mm。递减引用计数在line42 finish_task_switch函数中。

  line23,对于普通进程,需要调用switch_mm_irqs_off来进行地址空间切换。稍后会详细分析。

  line25~28,对于prev也是一个内核线程情况,prev进程马上就要被换出,所以设置prev->active_mm为NULL,另外就绪队列rq数据结构的成员prev_mm记录了prev->acitive_mm的值,该值稍后会在finish_task_switch中用到。

  line39,switch_to函数切换进程,从prev进程上下文切换到next进程上下文中运行。

  finish_task_switch函数中会递减20行中增加的mm_count的引用计数。

static struct rq *finish_task_switch(struct task_struct *prev)
	__releases(rq->lock)
{
	struct rq *rq = this_rq();
	struct mm_struct *mm = rq->prev_mm;
	...
	rq->prev_mm = NULL;
	...
	if (mm)
		mmdrop(mm);
	...
}

  这里有个逻辑,finish_task_switch其实是由next进程来进行处理的。prev进程在switch_to时就已经被调度出去了,被唤醒时才能执行finish_task_switch,这个时间点是难以把控的。
  但是反过来讲,next进程被唤醒的时候,从switch_to返回时,执行的也是自己上下文中的finish_task_switch函数,这时候把prev增加的mm_count引用计数递减。相当于替prev收拾残局。

4. switch_mm

  通常情况下,内核switch_mm_irqs_off定义为switch_mm。内核注释翻译一下意思是“如果架构在调用switch_mm时关心中断状态,可以重写switch_mm_irqs_offarmv7没有这个限制,直接实现switch_mm

armv7switch_mm定义在arch/arm/include/asm/mmu_context.h中。

static inline void
switch_mm(struct mm_struct *prev, struct mm_struct *next,
	  struct task_struct *tsk)
{
#ifdef CONFIG_MMU
	unsigned int cpu = smp_processor_id();

	/*
	 * __sync_icache_dcache doesn't broadcast the I-cache invalidation,
	 * so check for possible thread migration and invalidate the I-cache
	 * if we're new to this CPU.
	 */
	if (cache_ops_need_broadcast() &&
	    !cpumask_empty(mm_cpumask(next)) &&
	    !cpumask_test_cpu(cpu, mm_cpumask(next)))
		__flush_icache_all();

	if (!cpumask_test_and_set_cpu(cpu, mm_cpumask(next)) || prev != next) {
		check_and_switch_context(next, tsk);
		if (cache_is_vivt())
			cpumask_clear_cpu(cpu, mm_cpumask(prev));
	}
#endif
}

line18,先把当前CPU设置到下一个进程的cpumask位图中,然后调用check_and_switch_context()函数来完成ARM体系架构相关的硬件设置,如flush TLB等,TLB机制非常影响系统性能,所以比较复杂,这里就不展开介绍了。

5. switch_to

  switch_to最终调用架构各自实现的__switch_to函数

#define switch_to(prev,next,last)					\
do {									\
	__complete_pending_tlbi();					\
	last = __switch_to(prev,task_thread_info(prev), task_thread_info(next));	\
} while (0)

  此时传递给__switch_tor0prevtask_stcuct指针,r1prevthread_info指针,r2nextthread_info指针(thread_info架构相关!!!)

armv7__switch_to定义在entry-armv.S中,源码如下:

ENTRY(__switch_to)
 UNWIND(.fnstart	)
 UNWIND(.cantunwind	)
	add	ip, r1, #TI_CPU_SAVE
 ARM(	stmia	ip!, {r4 - sl, fp, sp, lr} )	@ Store most regs on stack
 THUMB(	stmia	ip!, {r4 - sl, fp}	   )	@ Store most regs on stack
 THUMB(	str	sp, [ip], #4		   )
 THUMB(	str	lr, [ip], #4		   )
	ldr	r4, [r2, #TI_TP_VALUE]
	ldr	r5, [r2, #TI_TP_VALUE + 4]
#ifdef CONFIG_CPU_USE_DOMAINS
	mrc	p15, 0, r6, c3, c0, 0		@ Get domain register
	str	r6, [r1, #TI_CPU_DOMAIN]	@ Save old domain register
	ldr	r6, [r2, #TI_CPU_DOMAIN]
#endif
	switch_tls r1, r4, r5, r3, r7
#if defined(CONFIG_CC_STACKPROTECTOR) && !defined(CONFIG_SMP)
	ldr	r7, [r2, #TI_TASK]
	ldr	r8, =__stack_chk_guard
	ldr	r7, [r7, #TSK_STACK_CANARY]
#endif
#ifdef CONFIG_CPU_USE_DOMAINS
	mcr	p15, 0, r6, c3, c0, 0		@ Set domain register
#endif
	mov	r5, r0
	add	r4, r2, #TI_CPU_SAVE
	ldr	r0, =thread_notify_head
	mov	r1, #THREAD_NOTIFY_SWITCH
	bl	atomic_notifier_call_chain
#if defined(CONFIG_CC_STACKPROTECTOR) && !defined(CONFIG_SMP)
	str	r7, [r8]
#endif
 THUMB(	mov	ip, r4			   )
	mov	r0, r5
 ARM(	ldmia	r4, {r4 - sl, fp, sp, pc}  )	@ Load all regs saved previously
 THUMB(	ldmia	ip!, {r4 - sl, fp}	   )	@ Load all regs saved previously
 THUMB(	ldr	sp, [ip], #4		   )
 THUMB(	ldr	pc, [ip]		   )
 UNWIND(.fnend		)
ENDPROC(__switch_to)ENTRY(__switch_to)
 UNWIND(.fnstart	)
 UNWIND(.cantunwind	)
	add	ip, r1, #TI_CPU_SAVE
 ARM(	stmia	ip!, {r4 - sl, fp, sp, lr} )	@ Store most regs on stack

	switch_tls r1, r4, r5, r3, r7

	mov	r5, r0
	add	r4, r2, #TI_CPU_SAVE
	ldr	r0, =thread_notify_head
	mov	r1, #THREAD_NOTIFY_SWITCH
	bl	atomic_notifier_call_chain

	mov	r0, r5
 ARM(	ldmia	r4, {r4 - sl, fp, sp, pc}  )	@ Load all regs saved previously
 UNWIND(.fnend		)
ENDPROC(__switch_to)

  这里把prev进程相关寄存器上下文保存到该进程的thread_info->cpu_context结构体中,然后再把next进程的thread_info->cpu_context结构体中的值设置到CPU的各个寄存器中,最后的PC寄存器出栈时,就完成了进程的切换。

  此时,CPU就已经在运行next进程了。

本文标签: 函数进程Linuxschedule