linux内核—进程调度(核心)

Posted 为了维护世界和平_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux内核—进程调度(核心)相关的知识,希望对你有一定的参考价值。

目录

核心函数__schedule()

处理过程

1、选择下一个进程

2、切换线程

1)切换进程的虚拟地址空间

2)切换寄存器

3)执行清理工作


核心函数__schedule()

主要的调度程序

进入次函数的主要方法是:

1、显示阻塞:互斥、信号量、等待队列等;

2、在中断和用户空间返回时检查TIF_NED_RESCHED标志。参考arch/arm64/entry_64.s

        为了在任务中抢占,调度器在计时器中设置标志中断处理程序schedule_tick

3、唤醒不会真正导致进入schedule,他们添加任务到任务队列而已。

        如果添加到运行队列的新任务抢占了当前任务,则唤醒设置TIF_NED_RESCHED,schedule获取在最近的情况下调用。

        如果内核是可抢占的(CONFIG_PREEMPTION=y) 

                在syscall或异常中断上下文中,位于下一个最外层preempt_enable。

                一个IRQ上下文中,从中断处理程序返回到可抢占上下文。

        如果内核是非抢占的

                则在下一个

                cond_sched()调用,

                explict_schedule()调用       

                从syscall或异常返回到用户空间

                从中断处理程序返回到用户空间 

必须在禁用抢占的情况下使用


static void __sched notrace __schedule(bool preempt)

preempt表示是否抢占调度,true抢占,强制剥夺当前进程对处理器的使用权,false表示主动调度,把当前进程让出处理器。

处理过程

1)调用pick_next_task以选择下一个进程。

2)调用context_switch以切换进程

1、选择下一个进程

      

/*
 * Pick up the highest-prio task:
 */
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)

	const struct sched_class *class;
	struct task_struct *p;

    //优化,如果所有进程属于限期或公平调度类,可以直接使用公平调度类的pick_next_task方法

	if (likely((prev->sched_class == &idle_sched_class ||
		    prev->sched_class == &fair_sched_class) &&
		   rq->nr_running == rq->cfs.h_nr_running)) 

		p = pick_next_task_fair(rq, prev, rf);
		if (unlikely(p == RETRY_TASK))
			goto restart;

		/* Assumes fair_sched_class->next == idle_sched_class */
		if (!p) 
			put_prev_task(rq, prev);
			p = pick_next_task_idle(rq);
		

		return p;
	

restart:
	put_prev_task(rq, prev);
	for_each_class(class) 
		p = class->pick_next_task(rq);
		if (p)
			return p;
	

如果当前进程属于空闲调度类或公平调度类,并且所有可运行的进程属于公平调度类,那么直接调用公平调度类的pick_next_task方法。如果公平调度类没有选择下一个进程,那么从空闲调度类选择下一个进程。

一般情况,从优先级最高的调度类开始,调用调度类的pick_next_task方法来选择下一个进程,如果选中了下一个进程,就调度这个进程。否则从优先级低的调度类选择下一个进程。

优先级从高到低依次是 停机、限期、实时、公平和空闲

1)停机调度类:pick_next_task_stop,如果运行队列中的成员stop指向某个线程,那么这个进程在运行队列中,那么返回成员stop指向的进程,否则反馈空指针。

2)限期调度类:pick_next_task_dl,从限期运行队列中选择截止期限最小的进程。(就是红黑树最左边的进程)。限期进程不支持任务组。

    

3)实时调度类:pick_next_task_rt,

        如果实时运行队列没有加入运行队列,返回空指针。

        从根任务组在当前上的实时运行队列开始,选择优先级最高的调度实体

        如果选择的调度实体是任务组,继续从这个任务组在当前处理器上的实时运行队列中选择优先级最高的调度实体。重复这个步骤,知道选中的调度实体为进程为止。

4)公平调度类:pick_next_task_fair

        从根任务组在当前处理器上的公平队列中,选择虚拟运行时间最小的调度实体,就是红黑树中最左边的调度实体。

        如果选中的调度实体是任务组,继续从这个任务组在当前处理器上的公平运行队列中选择虚拟运行时间最小的调度实体。重复这个步骤,直到找到调度实体为进程为止。

5)空闲调度类:pick_next_task_fair

        返回运行队列的成员idle指向的空闲线程。

2、切换线程

        context_switch 工作主要如下

1)switch_mm_irq_off 负责切换进程的用户虚拟地址空间。

2)switch_to 负责切换处理器的寄存器

static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
	       struct task_struct *next, struct rq_flags *rf)

	prepare_task_switch(rq, prev, next);//执行进程切换的准备工作,每种处理器加过必须定义的函数prepare_arch_switch。

	arch_start_context_switch(prev);//开启上下文切换

	//内核线程,内核线程没有用户虚拟地址空间,需要借用上一个进程的用户虚拟地址空间,把借来的用户虚拟地址空间保存在成员active_mm中,
	//内核线程在借用的虚拟地址空间上执行
	if (!next->mm)                                 // to kernel
		//通知处理器架构不需要切换用户虚拟地址空间,这种加速进程切换计数称为惰性TLB
		enter_lazy_tlb(prev->active_mm, next);

		next->active_mm = prev->active_mm;//
		if (prev->mm)                           // from user
			mmgrab(prev->active_mm);
		else
			prev->active_mm = NULL;
	 else                                         // to user

		membarrier_switch_mm(rq, prev->active_mm, next->mm);

		//切换进程的虚拟地址空间
		switch_mm_irqs_off(prev->active_mm, next->mm, next);//switch_mm

		if (!prev->mm)                         // from kernel 如果上一个进程是内核线程
			/* will mmdrop() in finish_task_switch(). */
			rq->prev_mm = prev->active_mm;
			prev->active_mm = NULL;//设置空指针,断开它和借用用户虚拟地址空间的联系,把借用的用户虚拟地址空间保存在运行队列pre_mm中
		
	
	rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
	prepare_lock_switch(rq, next, rf);

	/* Here we just switch the register state and the stack. */
	switch_to(prev, next, prev);//切换寄存器状态和栈
	barrier();//内存屏障

	return finish_task_switch(prev);//负责在进程切换后执行清理工作

执行进程切换的准备工作,每种处理器加过必须定义的函数prepare_arch_switch。

开启上下文切换

切换类型以及所执行的过程。

	/*
	 * kernel -> kernel   lazy + transfer active
	 *   user -> kernel   lazy + mmgrab() active
	 *
	 * kernel ->   user   switch + mmdrop() active
	 *   user ->   user   switch
	 */

1)切换进程的虚拟地址空间

switch_mm_irqs_off->switch_mm (架构相关)

static inline void
switch_mm(struct mm_struct *prev, struct mm_struct *next,
	  struct task_struct *tsk)

	if (prev != next)
		__switch_mm(next);

	update_saved_ttbr0(tsk, next);

 如果prev不等于next,进程空间不同,执行__switch_mm切换用户虚拟地址空间

static inline void __switch_mm(struct mm_struct *next)

	unsigned int cpu = smp_processor_id();
	if (next == &init_mm) 
		cpu_set_reserved_ttbr0();
		return;
	

	check_and_switch_context(next, cpu);
  • 如果切换到内核的内存描述符init_mm,把寄存器TTBR0_EL1设置为保留的地址空间描述符0和保留的零页empty_zero_page的物理地址。
  • check_and_switch_context为进程分配地址空间标识符 

2)切换寄存器

__notrace_funcgraph struct task_struct *__switch_to(struct task_struct *prev,
				struct task_struct *next)

	struct task_struct *last;

	fpsimd_thread_switch(next);//切换浮点寄存器
	tls_thread_switch(next);//切换本地存储相关的寄存器
	hw_breakpoint_thread_switch(next);//切换调试寄存器
	contextidr_thread_switch(next);//把上下文标识符寄存器CONTEXTDIR_EL1 设置为下一个进程的进程号
	entry_task_switch(next);//使用当前处理器的每处理器变量__entry_task记录下一个进程的进程描述符地址。
	uao_thread_switch(next);//根据下一个进程可访问的虚拟地址空间上限,恢复用户访问覆盖(UAO)状态
	ptrauth_thread_switch(next);
	ssbs_thread_switch(next);

	dsb(ish);//数据同步屏障,确保前面的缓存维护操作和页表缓存为何操作执行完

	/* the actual thread switch */
	last = cpu_switch_to(prev, next);//切换通用寄存器

	return last;

cpu_switch_to 通用寄存器的切换

arch/arm64/kernel/entry.S

ENTRY(cpu_switch_to)
	mov	x10, #THREAD_CPU_CONTEXT
	add	x8, x0, x10
	mov	x9, sp
	stp	x19, x20, [x8], #16		// store callee-saved registers
	stp	x21, x22, [x8], #16
	stp	x23, x24, [x8], #16
	stp	x25, x26, [x8], #16
	stp	x27, x28, [x8], #16
	stp	x29, x9, [x8], #16
	str	lr, [x8]
	add	x8, x1, x10
	ldp	x19, x20, [x8], #16		// restore callee-saved registers
	ldp	x21, x22, [x8], #16
	ldp	x23, x24, [x8], #16
	ldp	x25, x26, [x8], #16
	ldp	x27, x28, [x8], #16
	ldp	x29, x9, [x8], #16
	ldr	lr, [x8]
	mov	sp, x9
	msr	sp_el0, x1
	ret
ENDPROC(cpu_switch_to)
NOKPROBE(cpu_switch_to)

由被调用函数负责保存寄存器x19~x28.

寄存器x29 ,帧指针(Frame Pointer) 寄存器

栈指针(Stack Pointer ,SP)寄存器

寄存器x30,链接寄存器(Link Register,LR) ,存放函数返回地址

用户栈指针寄存器SP_EL0,内核使用它存放当前进程的进程描述符的第一个成员thread_info的地址。 

cpu_switch_to 两个参数,x0存放上一个进程的进程描述符的地址,x1存放下一个进程的进程描述符地址。

寄存器x10,存放进程描述符的成员thread.cpu_context的偏移。

寄存器x8,存放上一个进程的进程描述符的成员thread.cpu_context地址

寄存器x9保存栈指针

返回值时寄存器x0的值,上一个进程的进程描述符的地址。

3)执行清理工作

finish_task_switch

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;
	long prev_state;

	if (WARN_ONCE(preempt_count() != 2*PREEMPT_DISABLE_OFFSET,
		      "corrupted preempt_count: %s/%d/0x%x\\n",
		      current->comm, current->pid, preempt_count()))
		preempt_count_set(FORK_PREEMPT_COUNT);

	//如果prev是内核线程,那么rq->prev_mm存放它借用的内存描述符,置为空指针。
	rq->prev_mm = NULL;

	prev_state = prev->state;
	vtime_task_switch(prev);//计算进程prev的时间统计
	perf_event_task_sched_in(prev, current);
	finish_task(prev);
	finish_lock_switch(rq);//把prev->on_cpu设置为0,表示进程prev没有在处理器上运行;然后释放运行队列的锁,开启硬中断
	finish_arch_post_lock_switch();
	kcov_finish_switch(current);

	fire_sched_in_preempt_notifiers(current);

	if (mm) 
		membarrier_mm_sync_core_before_usermode(mm);
		mmdrop(mm);
	
	//主动退出或者被终止
	if (unlikely(prev_state == TASK_DEAD)) 
		if (prev->sched_class->task_dead)
			prev->sched_class->task_dead(prev);

		kprobe_flush_task(prev);

		/* Task is done with its stack. */
		//释放进程的内核栈
		put_task_stack(prev);
		//释放进程描述符
		put_task_struct_rcu_user(prev);
	

	tick_nohz_task_switch();
	return rq;

  参考

https://course.0voice.com/v1/course/intro?courseId=2&agentId=0

以上是关于linux内核—进程调度(核心)的主要内容,如果未能解决你的问题,请参考以下文章

Linux学习笔记:001Linux内核分析

Linux进程核心调度器之主调度器schedule--Linux进程的管理与调度(十九)

Linux 内核实时调度类 ⑦ ( 实时调度类核心函数源码分析 | dequeue_task_rt 函数 | 从执行队列中移除进程 )

Linux 内核进程优先级与调度策略 ③ ( 设置获取线程优先级的核心函数 | 修改线程调度策略函数 )

linux中进程 kacpid, kblockd是啥

Linux系统的进程调度