导航

【转】【进程管理】Linux进程调度:调度时机

Posted on 2021-07-18 17:05  yibuyibu01  阅读(385)  评论(0编辑  收藏  举报
转自:https://zhuanlan.zhihu.com/p/163728119

概述:

进程切换分为自愿(voluntary)和强制(involuntary)两种。通常自愿切换是指任务由于等待某种资源,将state改为非RUNNING状态后,调用schedule()主动让出CPU;而强制切换(即抢占)则是任务状态仍为RUNNING却失去CPU使用权,情况有任务时间片用完、有更高优先级的任务、任务中调用cond_resched()或yield让出CPU。

本文主要介绍抢占式进程切换(involuntary context switch),抢占又根据抢占发生的时机划分为用户抢占和内核抢占。

注意:1.文章基于arm64架构的linux5.0内核代码;2. 默认开启内核抢占CONFIG_PREEMPT

关于调度器:

内核中存在两个调度器,即主调度器及周期性调度器,通称为通用调度器。主调度器函数为__schedule();周期性调度器函数为scheduler_tick()。其中主调度器在内核代码中需要被主动调用,周期性调度器则伴随着系统的tick中断每秒HZ次的发生。

抢占前的检查:

  • 是否安全?

抢占发生的前提是要确保此次抢占是安全的。什么算安全?即current任务没有持有自旋锁,否则可能会发生死锁。于是在引入内核抢占机制(CONFIG_PREEMPT)的时同时引入了preempt_count,用来保证抢占的安全性,获取锁前会去inc抢占计数,而抢占发生前会去检查preempt_count是否为0。

  • 是否需要抢占?

关于是否需要去抢占,会去判断thread_info的成员flags是否设置了TIF_NEED_RESCHED标志位。(tif_need_resched()即用来判断此flag是否置位)

*后面会有具体的抢占例子分析。

进程切换的统计:

每个进程的强制切换和自愿切换的次数都会被记录在/proc/pid/status中:

/*通过procfs查看*/
grep ctxt /proc/26995/status
   voluntary_ctxt_switches: 79
   nonvoluntary_ctxt_switches: 4

/*使用pidstat命令查看*/
pidstat -w   

自愿切换和强制切换的统计值在实践中有什么意义呢?

答:大致而言,如果一个进程的自愿切换占多数,意味着它对CPU资源的需求不高;如果一个进程的强制切换占多数,表明它对CPU的依赖较强,意味着对它来说CPU资源可能是个瓶颈(这里需要排除进程频繁调用sched_yield()导致强制切换的情况)

内核抢占的几个时机:

  • 中断返回内核空间:

发生在内核态的中断el1_irq退出前,即irq handler之后,kernel_exit恢复现场之间。会去检查current进程的preempt_count和need_resched以判断是否需要进行一次抢占。

/*文:arch/arm64/kernel/entry.S
  函数调用:
  	el1_irq
          irq_handler
          el1_preempt
            preempt_schedule_irq
              __schedule(true) 
*/
el1_irq:
        kernel_entry 1
        enable_da_f
        irq_handler
#ifdef CONFIG_PREEMPT
        ldr     x24, [tsk, #TSK_TI_PREEMPT]     /* 抢占前的检查:preempt_count和need_resched.
        					   注意:设置need_resched的地方,同时也会设置TIF_NEED_RESCHED,所以这里检查need_resched即可。
                                                   */
        cbnz    x24, 1f                         /* 判断是否要跳转*/
        bl      el1_preempt			/* 跳转el1_preempt中尝试抢占*/
1:
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_on
#endif
        kernel_exit 1
ENDPROC(el1_irq)


#ifdef CONFIG_PREEMPT   
el1_preempt:
    mov x24, lr
1:  bl  preempt_schedule_irq        		/* 中断抢占的核心函数。注意:执行到此中断仍是关闭的,所以跳转回来时中断也要是关闭的*/
    ldr x0, [tsk, #TSK_TI_FLAGS]    
    tbnz    x0, #TIF_NEED_RESCHED, 1b   
    ret x24
#endif
/*文件:kernel/sched/core.c */



asmlinkage __visible void __sched preempt_schedule_irq(void)    // 封装了__schedule函数,且它会保证返回时local中断仍关闭。
{
	enum ctx_state prev_sta;

	/* Catch callers which need to be fixed */
	BUG_ON(preempt_count() || !irqs_disabled());      //检测异常:若抢占计数不为零,或者中断没有关闭 则dump_stack并产生oops。
	prev_state = exception_enter();             	  //保存当前cpu的异常状态下的上下文到prev_state

	do {
		preempt_disable();                        //关抢占,__schedule的过程不允许被抢占  
		local_irq_enable();                       //使能中断
		__schedule(true);                         //调度器核心函数。true表示此次切换为抢占。
		local_irq_disable();                      //关闭中断
		sched_preempt_enable_no_resched();        //开抢占
	} while (need_resched());                         //如果调度出去后的进程操作了被中断进程的thread_info.flags,使它仍为TIF_NEED_SCHED,那就继续进行调度。

	exception_exit(prev_state);                       //恢复抢占前的保存在prev_state中的异常上下文。
}

  • 内核中调用cond_resched()

内核代码中显式调用cond_resched()触发抢占,它的核心函数是preempt_schedule_common

/*cond_resched()在kernel代码中有着较广泛的使用*/

int __sched _cond_resched(void)     
{
    if (should_resched(0)) {        		//判断preempt_count是否为0。为0则代表开启抢占,need_resched也已被设置。
        preempt_schedule_common();    		//__schedule函数的封装函数。
        return 1;
    }
    rcu_all_qs();
    return 0;
}

static void __sched notrace preempt_schedule_common(void)
{
	do {
		preempt_disable_notrace();
		preempt_latency_start(1);
		__schedule(true);                    //调用__sechdule,且传参true代表这里是抢占
		preempt_latency_stop(1);
		preempt_enable_no_resched_notrace();
	} while (need_resched());		     //如果调度出去后的进程操作了被当前进程的thread_info.flags,使它仍为TIF_NEED_SCHED,那就继续进行调度
}
  • 内核中使能preempt时

内核代码中调用preempt_enable()开启preempt时也会检查执行抢占。

/*函数调用:
 	preempt_enable()
          __preempt_schedule()
            preempt_schedule()
              preempt_schedule_common()
*/
当前任务开启抢占时,也会检查并执行一次抢占。类似cond_resched()最终调用preempt_schedule_common()函数。

注意:非抢占的内核在spin_unlock()中不检查调度,而cond_resched在两种内核中都检查调度 (具体可查看代码实现)

用户抢占:

  • 中断返回用户空间:

当中断发生在进程的用户态,中断返回用户空间时也会检查是否需要执行抢占。

/*文件:arch/arm64/kernel/entry.S
  函数调用:
  	ret_to_user
          work_pending
            do_notify_resume
              schedule() 
*/
work_pending:
	mov	x0, sp				 // 'regs'
	bl	do_notify_resume            	 //跳转到do_notify_resume函数。
#ifdef CONFIG_TRACE_IRQFLAGS
	bl	trace_hardirqs_on		 // enabled while in userspace
#endif
	ldr	x1, [tsk, #TSK_TI_FLAGS]	 // re-check for single-step
	b	finish_ret_to_user
    
ret_to_user:
	disable_daif                             //D A I F分别为PSTAT中的四个异常屏蔽标志位,此处屏蔽这4中异常。(irq这里理论上没有开,为什么还关?)
	ldr	x1, [tsk, #TSK_TI_FLAGS]         //获取thread_info中的flags变量的值   
	and	x2, x1, #_TIF_WORK_MASK          //_TIF_WORK_MASK是一些列flags的集合,其中包括NEED_RESCHED|SIGPENDING|NOTIFY_RESUME等值
                                          
	cbnz	x2, work_pending		 //判断并跳转。此处判断若有_TIF_WORK_MASK的中任何flag都去执行work_pending
finish_ret_to_user:
	enable_step_tsk x1, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
	bl	stackleak_erase
#endif
	kernel_exit 0
ENDPROC(ret_to_user)


asmlinkage void do_notify_resume(struct pt_regs *regs,                   /* 处理thread_info中pending的flag事件。
				 unsigned long thread_flags)              进入此函数,中断必须是关闭的。这里大概就是ret_to_user中再次调disable_daif的原因。
                 							  */
{
	do {
		/* Check valid user FS if needed */
		addr_limit_user_check();
		if (thread_flags & _TIF_NEED_RESCHED) {                 //上来就判断_TIF_NEED_RESCHED,若置位则直接调schedule
			/* Unmask Debug and SError for the next task */ //取消被屏蔽的其他异常,只关闭IRQ异常!
			local_daif_restore(DAIF_PROCCTX_NOIRQ);
			schedule();					//调度的核心函数,进行抢占。
		} else {
......
	} while (thread_flags & _TIF_WORK_MASK);
}
  • 系统调用返回用户空间

系统调用返回用户空间和中断返回用户空间都是通过ret_to_user函数,所以判断执行抢占的部分是相同的。

/*文件:arch/arm64/kernel/entry.S
  函数调用:
  	ret_to_user
  	  work_pending
            do_notify_resume
              schedule() 
*/
arm64中syscall通过同步异常el0_sync->el0_svc->el0_svc_handler处理完成后,ret_to_user返回用户空间。
参考中断返回用户空间。

主动调度:

当进程阻塞时,例如mutex,sem,waitqueue获取不到资源,在或者进程进行磁盘读写。这种情况下进程会将自己的状态从TASK_RUNNING修改为TASK_INTERRUPTIBLE或是TASK_UNINTERRUPTIBLE,然后调用schedule()主动让出CPU并等待唤醒。

/*以磁盘IO中的一处使用举例:
  文件:fs/direct-io.c
*/
static struct bio *dio_await_one(struct dio *dio)
{
....
        while (dio->refcount > 1 && dio->bio_list == NULL) {
                __set_current_state(TASK_UNINTERRUPTIBLE);	//先将当前进程的state设置为TASK_UNINTERRUPTIBLE不可唤醒状态。
                dio->waiter = current;
                spin_unlock_irqrestore(&dio->bio_lock, flags);
                if (!(dio->iocb->ki_flags & IOCB_HIPRI) ||
                    !blk_poll(dio->bio_disk->queue, dio->bio_cookie, true))
                        io_schedule();				//调用io_schedule让出cpu,并等待io完成后被唤醒。
                /* wake up sets us TASK_RUNNING */
.....
}

关于周期性调度器的简要说明:

前文我们说到,执行抢占时会检查TIF_NEED_RESCHED标志,以判断是否需要执行抢占。那TIF_NEED_RESCHED在什么地方被设置呢?其一是在进程被wakeup的时候,再一地方就是在周期性调度器(scheduler_tick)中。
时钟tick以每秒HZ次的频率发生,在时钟tick的中断处理函数里,会去调用周期性调度器scheduler_tick()检查并设置TIF_NEED_RESCHED和need_resched。而这本身就在一个中断里,当中断返回时又回去检查执行抢占。

/*函数调用:
    .....
      tick_sched_timer()
        tick_sched_handle()
          update_process_times()
            scheduler_tick()
              task_tick_fair()
                entity_tick()
                  resched_curr()
                    set_tsk_need_resched(curr);  //设置TIF_NEED_RESCHED
                    set_preempt_need_resched();  //设置need_resched,即PREEMPT_NEED_RESCHED
*/

scheduler_tick(){
    sched_clock_tick();   			//更新系统的时钟ticket
    update_rq_lock(rq);   			//更新当前cpu就需队列中的时钟计数,struct rq中包含clock和clock_task两个runqueue的时间
    curr->sched_class->task_tick(rq,curr,0);	//调用对应调度类中的task_tick方法(每个调度类都有实现自己的task_tick)
    						//重点关注cfs中实现的task_tick_fair->entity_tick;分析如下面代码
    						//entity_tick中会检查是否需要抢占调度
    cpu_load_update_active(rq);
    
    trigger_load_balance(rq);                   //若是smp架构的话会去调负载均衡
}   

void resched_curr(struct rq *rq)
{
.....
        if (cpu == smp_processor_id()) {
                set_tsk_need_resched(curr);   //设置TIF_NEED_RESCHED
                set_preempt_need_resched();   //设置need_resched,即PREEMPT_NEED_RESCHED
                return;
        }
......
}

转自:https://zhuanlan.zhihu.com/p/163728119