linux调度器源码分析 (四)- 调度原理

在前面的概述里说到过,调度分为周期性调度和特定时刻调度。

1 周期性调度

系统启动调度器初始化时会初始化一个调度定时器,定时器每隔一定时间产生一个中断,即一个滴答,在中断会对当前运行进程运行时间进行更新,如果进程需要被调度,在调度定时器中断中会设置一个调度标志位,而不会真正的调度时机将会推迟到特定时刻调度,总的来说周期性调度的任务并不完成进程的真正切换工作,只是检查当前进程是否需要调度并设置相应调度位。当时钟发生中断时,首先会调用的是tick_handle_periodic()函数,在此函数中又主要执行tick_periodic()函数进行操作。我们先看一下tick_handle_periodic()函数:

void tick_handle_periodic(struct clock_event_device *dev)
{
    /* 获取当前CPU */
    int cpu = smp_processor_id();
    /* 获取下次时钟中断执行时间 */
    ktime_t next = dev->next_event;

    tick_periodic(cpu);
    
    /* 如果是周期触发模式,直接返回 */
    if (dev->mode != CLOCK_EVT_MODE_ONESHOT)
        return;

    /* 为了防止当该函数被调用时,clock_event_device中的计时实际上已经经过了不止一个tick周期,这时候,tick_periodic可能被多次调用,使得jiffies和时间可以被正确地更新。 */
    for (;;) {
        /*
         * Setup the next period for devices, which do not have
         * periodic mode:
         */
        /* 计算下一次触发时间 */
        next = ktime_add(next, tick_period);

        /* 设置下一次触发时间,返回0表示成功 */
        if (!clockevents_program_event(dev, next, false))
            return;
        /*
         * Have to be careful here. If we're in oneshot mode,
         * before we call tick_periodic() in a loop, we need
         * to be sure we're using a real hardware clocksource.
         * Otherwise we could get trapped in an infinite(无限的)
         * loop, as the tick_periodic() increments jiffies,
         * which then will increment time, possibly causing
         * the loop to trigger again and again.
         */
        if (timekeeping_valid_for_hres())
            tick_periodic(cpu);
    }
}

此函数主要工作是执行tick_periodic()函数,而在tick_periodic()函数中,程序主要执行路线为tick_periodic()->update_process_times()->scheduler_tick()。最后的scheduler_tick()函数则是跟调度相关的主要函数。

void scheduler_tick(void)
{
    /* 获取当前CPU的ID */
    int cpu = smp_processor_id();
    /* 获取当前CPU的rq队列 */
    struct rq *rq = cpu_rq(cpu);
    /* 获取当前CPU的当前运行程序,实际上就是current */
    struct task_struct *curr = rq->curr;
    /* 更新CPU调度统计中的本次调度时间 */
    sched_clock_tick();

    raw_spin_lock(&rq->lock);
    /* 更新该CPU的rq运行时间 ,更新时钟(rq->clock)以及rq->clock_task  */
    update_rq_clock(rq);
    curr->sched_class->task_tick(rq, curr, 0);
    /* 更新CPU的负载 */
    update_cpu_load_active(rq);
    raw_spin_unlock(&rq->lock);

    perf_event_task_tick();

#ifdef CONFIG_SMP
    rq->idle_balance = idle_cpu(cpu);
    trigger_load_balance(rq);
#endif
    /* rq->last_sched_tick = jiffies; */
    rq_last_tick_reset(rq);
}

可以看到,关键函数是task_tick,实时进程和普通进程的调度类分别提供不同的函数。

1.1 普通进程cfs调度原理

对于普通进程,采用的调度算法是cfs,其对应的task_tick函数是task_tick_fair。在介绍该函数之前,先分析一下cfs调度。

1.1.1 抽象模型

在不考虑睡眠,抢占等细节的情况下,大概可以如下描述cfs模型:

a) 每个进程有一个权重值(weight),值越大,表示该进程越优先。

b) 每个进程还对应一个 vruntime(虚拟时间)值,它是根据进程实际运行的时间 runtime 计算出来的。vruntime 值不能反映进程执行的真实时间,只是用来 作为系统判断接下来应该将 CPU 使用权交给哪个进程的依据——调度器总 是选择 vruntime 值最小的进程执行。

c) vruntime 行走的速度和进程的 weight 成反比。

d) 为了保证每个进程在某段时间(period)内每个进程至少能执行一次,操作 系统引入了 ideal_runtime 的概念,规定每个进程每次获得 CPU 使用权时, 执行时间不能超过它对应的 ideal_runtime 值。达到该值就会激活调度器, 让调度器再选择一个 vruntime 值最小的进程执行。

e) 每个进程的 ideal_runtime 长度与它的 weight 成正比。如果有 N 个进程那 么:

                                     

抽象模型与代码中的数据结构对应起来有如下关系:

抽象模型真实模型说明
task->runtimetask->se->sum_exec _runtime每个进程对应一个可调度实体,在 task_struct 的结构体中,该实体就是成员变量 se。
task->weighttask->se.load.weight 
task->vruntimetask->se.vruntime 
∑( task->weight)cfs_rq->load.weight在抽象模型中,我们计算 ideal_runtime 的时候 需要求所有进程的权重值的和,在实现的时候, 没有求和的过程,而是把该值记录在就绪队列 的 load.weight 中。向就绪队列中添加新进程时,就加上新进程的 权重值,进程被移出就绪队列时则减去被移除 的进程的权重值。
 cfs_rq->min_vruntime该值用来解决之前在抽象模型中遗留的问题, 所以在抽象模型中没有与之对应的值。
task->ideal_runtimesched_slice( )函数每个进程的 ideal_runtime 并没有用变量保存 起来,而是在需要用到时用函数 shed_slice( ) 计算得到。 公式跟抽象模型中公式一样.
period__sched_period()函数period 也没有用变量来保存,也是在需要用到 时由函数计算得到: 在默认情况下 period 的值是 20ms,当可运 行进程数目超过 5 个时,period 就等于: nr_running*4ms(nr_running 是可运行进程的 数目。 上面提到的“20ms”由 sysctl_sched_latency 指定;“5”个由 sched_nr_latency 指定;“4ms” 是由 sysctl_sched_min_granularity 指定的。 这样设定有它的目的,不与深究,以免迷失在 细节里。

 

1.1.2 源码分析

上面说过,对于cfs调度,task_tick函数是task_tick_fair,看一下其源码来进一步了解其调度过程。

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; //取出当前进程的se结构

	for_each_sched_entity(se) {
		cfs_rq = cfs_rq_of(se); //从当前se结构开始,向上父节点遍历,更新调度实体的运行时间
		entity_tick(cfs_rq, se, queued); //具体的调度实现在这里
	}

	if (sched_feat_numa(NUMA))
		task_tick_numa(rq, curr);

	update_rq_runnable_avg(rq, 1);
}

for_each_sched_entity循环中如果当前调度实体在某个组调度中,则需要向上遍历,更新调度组实体的信息,只分析最简单的情况,当前进程不再组调度中,则先调用entity_tick更新当前进程的调度信息。

static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
	/*
	 * Update run-time statistics of the 'current'.
	 */
	update_curr(cfs_rq); //更新相关统计信息

	/*
	 * Ensure that runnable average is periodically updated.
	 */
	update_entity_load_avg(curr, 1);
	update_cfs_rq_blocked_load(cfs_rq, 1);

#ifdef CONFIG_SCHED_HRTICK
	/*
	 * queued ticks are scheduled to match the slice, so don't bother
	 * validating it and just reschedule.
	 */
	if (queued) {
		resched_task(rq_of(cfs_rq)->curr);
		return;
	}
	/*
	 * don't let the period tick interfere with the hrtick preemption
	 */
	if (!sched_feat(DOUBLE_TICK) &&
			hrtimer_active(&rq_of(cfs_rq)->hrtick_timer))
		return;
#endif

	if (cfs_rq->nr_running > 1)
		check_preempt_tick(cfs_rq, curr); //判断是否需要把调度flag置位
}

上面比较重要的两个函数是update_curr和check_preempt_tick

entity_tick

       ------------>update_curr

static void update_curr(struct cfs_rq *cfs_rq)
{
	struct sched_entity *curr = cfs_rq->curr;
	u64 now = rq_of(cfs_rq)->clock_task;
	unsigned long delta_exec;

	if (unlikely(!curr))
		return;

	/*
	 * Get the amount of time the current task was running
	 * since the last time we changed load (this cannot
	 * overflow on 32 bits):
	 */
	delta_exec = (unsigned long)(now - curr->exec_start);
	if (!delta_exec)
		return;

	__update_curr(cfs_rq, curr, delta_exec);
	curr->exec_start = now;

	if (entity_is_task(curr)) {
		struct task_struct *curtask = task_of(curr);

		trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
		cpuacct_charge(curtask, delta_exec);
		account_group_exec_runtime(curtask, delta_exec);
	}

	account_cfs_rq_runtime(cfs_rq, delta_exec);
}

delta_exec = (unsigned long)(now - curr->exec_start); 是计算周期性调度器上次执行时到周期性这次执行之间,进程实际执行的 CPU 时间(如果周期性调度器每 1ms 执行一次,delta_exec 就表示没 1ms 内进 程消耗的 CPU 时间,这个在前面讲了),它是一个实际运行时间。 update_curr()函数内只负责计算 delta_exec 以及更新 exec_start。更新 其他相关数据的任务交给了__update_curr()函数。

entity_tick

       ------------>update_curr

            ---------------->__update_curr

static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
	      unsigned long delta_exec)
{
	unsigned long delta_exec_weighted;

	schedstat_set(curr->statistics.exec_max,
		      max((u64)delta_exec, curr->statistics.exec_max));

	curr->sum_exec_runtime += delta_exec; //.更新当前进程的实际运行时间(抽象模型中runtime)。
	schedstat_add(cfs_rq, exec_clock, delta_exec);
	delta_exec_weighted = calc_delta_fair(delta_exec, curr);

	curr->vruntime += delta_exec_weighted; //更新当前进程的虚拟时间 vruntime
	update_min_vruntime(cfs_rq);//更新cfs_rq->min_vruntim
}

calc_delta_fair 函数是根据实际运行时间增量算出虚拟运行时间增量。由于计算过程要规避除法,所以实际实现过程很复杂,这边只把其计算原理放上:

delta_exec_weighted=delta_exec*NICE_0_LOAD/curr->load.weight

NICE_0_LOAD为1024,可以看出vruntime 的增长速度与权重值成 反比,而如果当前进程的权重等于1024,则实际运行时间增量等于虚拟运行时间增量。

更新 cfs_rq->min_vruntime。在当前进程和下一个将要被调度的进程中选 择 vruntime 较小的值(因为下一个要执行的进程的 vruntime 是就绪队列中 vruntime 值最小的,那么在它和当前进程中选择 vruntime 更小的意味着选 出的是可运行进程中 vruntime 最 小 的 值 ) 。 然 后 用 该 值 和 cfs_rq->min_vruntime 比较,如果比 min_vruntime 大,则更新 cfs_rq 为它 (保证了 min_vruntime 值单调增加)。

再来看一下check_preempt_tick

entity_tick

       ------------>check_preempt_tick

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

	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 (delta_exec < sysctl_sched_min_granularity)
		return;

	se = __pick_first_entity(cfs_rq);
	delta = curr->vruntime - se->vruntime;

	if (delta < 0)
		return;

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

check_preempt_tick(),做了两件事情:

(1)检查进程本次被获得 CPU 使用权执行的时间是否超 过 了 它 对 应 的 ideal_runtime 值,如果超过了,则将当前进程的 TIF_NEED_RESCHED 标志位置位。

(2)接着调用__pick_first_entity取出cfs调度队列里面虚拟运行时间最小的那个调度实体,计算当进程的虚拟运行时间和最小虚拟运行时间进程的差值,如果该差值大于ideal_runtime,则也需要将当前进程的 TIF_NEED_RESCHED 标志位置位。

总结一下调用过程,大概如下所示:

              

1.2 实时进程调度

实时进程的调度要比cfs的调度简单的多,实时进程分为两种,FIFO(先进先出)和 RR(时间片轮转)。

FIFO 策略很简单:得到CPU使用权的进程可以执行任意长时间,直到它主动放弃CPU。

RR 策略呢,则是给每个进程分配一个时间片,当前进程的时间片消耗完毕则切 换至下一个进程。

接下来的代码,判断调度策略是不是 RR,如果不是 RR 则无事可做。 如果是 RR,则将其时间片减一,如果时间片不为零,该进程可以继续执行,那 么什么都不需要做。如果时间片为 0,则重新给它分配时间片(长度由 DEF_TIMESLICE 指定),如果可运行进程大于一个,就调用 requeue_task_rt() 将当前进程放到实时就绪队列的末尾,并将 TIF_NEED_RESCHED 标志位置为, 提示系统需要进行进程切换。

static void task_tick_rt(struct rq *rq, struct task_struct *p, int queued)
{
	struct sched_rt_entity *rt_se = &p->rt;

	update_curr_rt(rq);  //更新统计信息,并判断当前进程执行时间是否超过系统最大值

	watchdog(rq, p);

	/*
	 * RR tasks need a special form of timeslice management.
	 * FIFO tasks have no timeslices.
	 */
	if (p->policy != SCHED_RR)  //如果是FIFO策略,直接退出
		return;

	if (--p->rt.time_slice)  //RR调度策略进程减少当前时间片,如果时间片不为0,直接退出
		return;

	p->rt.time_slice = sched_rr_timeslice;//时间片为0,则初始化当前时间片。

	/*
	 * Requeue to the end of queue if we (and all of our ancestors) are the
	 * only element on the queue
	 */
	for_each_sched_rt_entity(rt_se) {
		if (rt_se->run_list.prev != rt_se->run_list.next) {
			requeue_task_rt(rq, p, 0); //重新queue当前task,同一优先级下,实时进程总是会选择先入队列的那个进程执行,所以requeue一次,当前进程进跑到队列后面去了
			set_tsk_need_resched(p);  //设置当前进程调度位
			return;
		}
	}
}

task_tick_rt 

      ------------->update_curr_rt

一般来讲,系统会通过proc定义一个sysctl_sched_rt_runtime,在每个tick中断处理中都会通过update_curr_rt->sched_rt_runtime_exceeded判断实时进程最近一次被调度后的运行时间,是否超过系统定义的实时进程运行时间的阈值(这个阈值通常为0.95s,即1s内实时进程运行时间不得超过0.95s).如果超过阈值则会设置重新调度的标志(通过resched_task接口),系统调度时选择下一进程,不会再选择实时进程.

static void update_curr_rt(struct rq *rq)
{
 struct task_struct *curr = rq->curr;
 struct sched_rt_entity *rt_se = &curr->rt;
 struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
 u64 delta_exec;
 if (curr->sched_class != &rt_sched_class)
  return;
/*
进程的执行时间,如果被调度走,则间隔可能不是一个时钟中断
进程在一个tick中断中运行的时间=rq上的运行时间-本进程开始执行的时间
*/
 delta_exec = rq->clock_task - curr->se.exec_start;
 if (unlikely((s64)delta_exec <= 0))
  return;
 schedstat_set(curr->se.statistics.exec_max,
        max(curr->se.statistics.exec_max, delta_exec));
/*
更新当前进程总运行时间
*/
 curr->se.sum_exec_runtime += delta_exec;
 account_group_exec_runtime(curr, delta_exec);
/*
下次运行的起始时间
*/
 curr->se.exec_start = rq->clock_task;
 cpuacct_charge(curr, delta_exec);
 sched_rt_avg_update(rq, delta_exec);
/*开启sysctl_sched_rt_runtime表示实时进程运行时间是0.95s*/
 if (!rt_bandwidth_enabled())
  return;
 for_each_sched_rt_entity(rt_se) {
  rt_rq = rt_rq_of_se(rt_se);
  if (sched_rt_runtime(rt_rq) != RUNTIME_INF) {
   raw_spin_lock(&rt_rq->rt_runtime_lock);
   rt_rq->rt_time += delta_exec;
    /* 
     判断实时进程最近一次被调度后的运行时间,是否超过系统定义的实时进程运行时间的阈值,
     这个阈值通常为0.95s,即1s内实时进程运行时间不得超过0.95s 
    */
   if (sched_rt_runtime_exceeded(rt_rq))
    /* 超过实时进程运行时间的阈值,需要设置重新调度标识 */
    resched_task(curr);
   raw_spin_unlock(&rt_rq->rt_runtime_lock);
  }
 }
}

实时进程运行队列更新队列时间时(即update_curr_rt),会通过sched_rt_runtime_exceeded接口判断实时进程运行队列上的运行时间在实时进程的一个周期时间内(即sysctl_sched_rt_period),是否超过系统设定的实时进程实际运行时间(即sysctl_sched_rt_runtime),如果超过了,则设置rt_rq->rt_throttled = 1;

rt_throttled标志的作用是:schedule过程中,系统调度器会选择下一个运行的进程,当pick_next_task函数在实时进程运行队列上选择时(即通过_pick_next_task_rt接口选择);会通过rt_rq_throttled(rt_rq)函数判断rt_throttled的值,如果为1,则直接返回NULL,就是说尽管实时进程在实时进程的active优先级队列上,但是也不能被选择,而是要从cfs运行队列上选择普通进程。

task_tick_rt 

      ------------->update_curr_rt

              ---------------->sched_rt_runtime_exceeded

static int sched_rt_runtime_exceeded(struct rt_rq *rt_rq)
{
	u64 runtime = sched_rt_runtime(rt_rq);      /* runtime为额定时间rt_rq->rt_runtime */
 
	if (rt_rq->rt_throttled)                      /* 如果受到调度限制直接返回 */
		return rt_rq_throttled(rt_rq);       
 
	if (runtime >= sched_rt_period(rt_rq))        /* 如果rt_rq的额定时间大于周期说明不会发生超时,返回0表示不超额 */
		return 0;
 
	balance_runtime(rt_rq);                    /* 对rt_rq的额定时间进行"balance" */
	runtime = sched_rt_runtime(rt_rq);          /* balance后rt_rq的额定时间可能会改变,所以需要重新获取rt_rq->rt_runtime */
	if (runtime == RUNTIME_INF)                 /* 额定时间"无限",也返回0表示没有超额 */
		return 0;
 
	if (rt_rq->rt_time > runtime) {            /* 如果 rt_rq上的运行时间大于了额定时间 */
		struct rt_bandwidth *rt_b = sched_rt_bandwidth(rt_rq);
 
		/*
		 * Don't actually throttle groups that have no runtime assigned
		 * but accrue some time due to boosting.
		 */
		if (likely(rt_b->rt_runtime)) {    /* 一般情况下带宽额定时间rt_b->rt_runtime都不为0 */
			rt_rq->rt_throttled = 1;    /* 在rt_rq的运行时间超过额定时间的情况下设置调度限制rt_throttled */
			printk_deferred_once("sched: RT throttling activated\n");
		} else {
			/*
			 * In case we did anyway, make it go away,
			 * replenishment is a joke, since it will replenish us
			 * with exactly 0 ns.
			 */
			rt_rq->rt_time = 0;
		}
 
		if (rt_rq_throttled(rt_rq)) {     /* 如果rt_rq运行时间超额且设置了调度限制标志 */
			sched_rt_rq_dequeue(rt_rq);  /* 先将rt_rq对应的实体从队列删除,再放到队尾;
							  * 注意:函数想将rt_rq这个组的rt_se及其所有的祖先rt_se出队,
								   然后再从祖先rt_se开始再依次放到队尾;
								   任何一个rt_rq的调度受限时,对应的rt_se在__enqueue_rt_entity(rt_se)
								   是不能入队的,所以这里的rt_rq对应组的调度实体不会入队的;
								   入不了队也就意味着无法得到调度。 */
			return 1;
		}
	}
 
	return 0;
}

那么rt_throttled = 1这个调度限制何时解除呢,在高精度定时器的回调函数里。每当调用__enqueue_rt_entity()函数将一个rt_se调度实体入队时,都会检查rt_se所在组的rt_bandwidth上的高精度时钟是否激活,如果没有激活则将其激活。激活后定时器开始飞速运转,直到我们设置的定时器到期;而定时器到期意味着什么呢?意味着时钟到期处理函数rt_b->rt_period_timer.function的调用执行,而这个函数在带宽初始化时设置为sched_rt_period_timer(),所以时钟到期后实际回调的是sched_rt_period_timer()。

sched_rt_period_timer

      ------------------>do_sched_rt_period_timer

static int do_sched_rt_period_timer(struct rt_bandwidth *rt_b, int overrun)
{
	int i, idle = 1, throttled = 0;
	const struct cpumask *span;
 
	span = sched_rt_period_mask();
#ifdef CONFIG_RT_GROUP_SCHED
	/*
	 * FIXME: isolated CPUs should really leave the root task group,
	 * whether they are isolcpus or were isolated via cpusets, lest
	 * the timer run on a CPU which does not service all runqueues,
	 * potentially leaving other CPUs indefinitely throttled.  If
	 * isolation is really required, the user will turn the throttle
	 * off to kill the perturbations it causes anyway.  Meanwhile,
	 * this maintains functionality for boot and/or troubleshooting.
	 */
	if (rt_b == &root_task_group.rt_bandwidth)
		span = cpu_online_mask;
#endif
	for_each_cpu(i, span) {		/* 分析此带宽所在的task_group组上各个cpu的运行队列rt_rq */
		int enqueue = 0;
		struct rt_rq *rt_rq = sched_rt_period_rt_rq(rt_b, i);
		struct rq *rq = rq_of_rt_rq(rt_rq);
 
		raw_spin_lock(&rq->lock);
		if (rt_rq->rt_time) {	/* rt_rq运行时间不为0:rt_rq的运行时间只有在rt_bandwidth高精度时钟
						 * 到期后才得以重新统计 */
			u64 runtime;
 
			raw_spin_lock(&rt_rq->rt_runtime_lock);
			if (rt_rq->rt_throttled)
				balance_runtime(rt_rq);		/* 如果rt_rq调度受限进行"balcance",以尝试从其他cpu的rt_rq偷时间
									 * 这是第二次出现。
									*/
			runtime = rt_rq->rt_runtime;
			rt_rq->rt_time -= min(rt_rq->rt_time, overrun*runtime);	/* 抹去周期运行时间;
												 * @overrun:超过时钟周期数;@runtime:一个周期内运行队列的额定运行时间;
												 * 没有到一个周期,则将运行时间清0;否则	
												 * 运行时间设置为过期超出的额定时间;
												 */
			if (rt_rq->rt_throttled && rt_rq->rt_time < runtime) {		/* 如果剩余的运行时间小于一个周期额定时间 
				rt_rq->rt_throttled = 0;					 * 则清除调度限制标志,并将入队标志设置为1 */
				enqueue = 1;
 
				/*
				 * When we're idle and a woken (rt) task is
				 * throttled check_preempt_curr() will set
				 * skip_update and the time between the wakeup
				 * and this unthrottle will get accounted as
				 * 'runtime'.
				 */
				if (rt_rq->rt_nr_running && rq->curr == rq->idle)
					rq_clock_skip_update(rq, false);
			}
			if (rt_rq->rt_time || rt_rq->rt_nr_running)
				idle = 0;
			raw_spin_unlock(&rt_rq->rt_runtime_lock);
		} else if (rt_rq->rt_nr_running) {		/* 如果此周期rt_rq没有运行时间,但是rt_rq还有就绪的任务,
			idle = 0;				 * 且rt_rq没有调度限制则入队标志置1 */
			if (!rt_rq_throttled(rt_rq))
				enqueue = 1;
		}
		if (rt_rq->rt_throttled)
			throttled = 1;
 
		if (enqueue)
			sched_rt_rq_enqueue(rt_rq);	/* 在3.2中可以看到rt_rq带宽超时后sched_rt_rq_dequeue()出队后无法再入队,直到这里解除了调度限制 */
		raw_spin_unlock(&rq->lock);
	}
 
	if (!throttled && (!rt_bandwidth_enabled() || rt_b->rt_runtime == RUNTIME_INF))
		return 1;
 
	return idle;			/* idle返回0表示有cpu上无可运行调度实体 */
}

从上面do_sched_rt_period_timer(rt_b, overrun)函数也可以看到队列的带宽限制的解除条件:在时钟到期后重新计算rt_rq的运行时间(也就是剩余的运行时间),如果更新后的运行时间小于一个周期的额定时间,则会解除rt_rq的调度限制rt_rq->rt_throttled = 0。

大概概括一下整个调用流程:

                    

整个周期性调度完了,再看一下特定时间的主动调度。

2 特定时间主动调度

之前文章已经说过,特定时刻显示或隐示的调度schedule()函数,真正负责进程上下文切换的地方大概有如下几个:

(1)在内核程序中显式调用schedule()函数,放弃当前cpu。当然也可能不是这个函数名,但所调用函数都是对__schedule()的封装
(2)从中断上下文或者系统调用返回用户空间时
(3)在内核中运行时,从中断上下文返回内核线程上下文时(这是2.4版本以后新加的支持内核抢占功能)
(4)当内核代码再一次具有可抢占性的时候,如解锁(spin_unlock_bh)及使能软中断(local_bh_enable)等, 此时当kernel code从不可抢占状态变为可抢占状态时(preemptible again)。也就是preempt_count从正整数变为0时。这也是隐式的调用schedule()函数。

不管是哪种情况,都显式或者隐式的调用了schedule()函数,所以我们把schedule函数的分析放到最后面,依次先分析,调用该schedule的时机。

所以情况一先不用分析,这是当前进程主动放弃cpu时调度的,比如说sleep_on函数。

先看情况二:

2.1 中断上下文或者系统调用返回用户空间时刻

2.1.1 中断上下文返回用户空间

当进程运行在用户空间时,中断来临陷入内核空间进行执行,当中断执行完以后,返回用户空间时会判断当前进程是否被设置了调度位,如果设置了,则需要进行调度。

中断处理过程可以参考这篇文章:

https://blog.csdn.net/oqqYuJi12345678/article/details/99654760

中断发生在user mode下的退出过程,代码如下:

get_thread_info tsk------tsk是r9,指向当前的thread info数据结构
mov    why, #0--------why是r8
b    ret_to_user_from_irq----中断返回

进程的thread_info和svc状态下的stack是放在一块内存里面的,这边是2K,而thread_info从低地址开始放,所以只需要把stack 按2K 对其,即可得到thread_info,并把其放在r9寄存器中

#define _TIF_WORK_MASK   (_TIF_NEED_RESCHED | _TIF_SIGPENDING | _TIF_NOTIFY_RESUME)
ENTRY(ret_to_user_from_irq) 
    ldr    r1, [tsk, #TI_FLAGS] 
    tst    r1, #_TIF_WORK_MASK---------------A 
    bne    work_pending 
no_work_pending: 
    asm_trace_hardirqs_on ------和irq flag trace相关,暂且略过
 
    /* perform architecture specific actions before user return */ 
    arch_ret_to_user r1, lr----有些硬件平台需要在中断返回用户空间做一些特别处理 
    ct_user_enter save = 0 ----和trace context相关,暂且略过
 
    restore_user_regs fast = 0, offset = 0------------B 
ENDPROC(ret_to_user_from_irq) 

当 thread_info的TI_FLAGS中,_TIF_NEED_RESCHED 被置位,则会去调用work_pending:

work_pending:
	mov	r0, sp				@ 'regs'
	mov	r2, why				@ 'syscall'
	bl	do_work_pending
	cmp	r0, #0
	beq	no_work_pending
	movlt	scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)
	ldmia	sp, {r0 - r6}			@ have to reload r0 - r6
	b	local_restart			@ ... and off we go

可以看到,真正调用的是do_work_pending:

asmlinkage int
do_work_pending(struct pt_regs *regs, unsigned int thread_flags, int syscall)
{
	do {
		if (likely(thread_flags & _TIF_NEED_RESCHED)) {//设置了调度标志位,则调用schedule
			schedule();
		} else {
			if (unlikely(!user_mode(regs)))
				return 0;
			local_irq_enable();
			if (thread_flags & _TIF_SIGPENDING) {
				int restart = do_signal(regs, syscall);
				if (unlikely(restart)) {
					/*
					 * Restart without handlers.
					 * Deal with it without leaving
					 * the kernel space.
					 */
					return restart;
				}
				syscall = 0;
			} else {
				clear_thread_flag(TIF_NOTIFY_RESUME);
				tracehook_notify_resume(regs);
			}
		}
		local_irq_disable();
		thread_flags = current_thread_info()->flags;
	} while (thread_flags & _TIF_WORK_MASK);
	return 0;
}

可以看到在do_work_pending里面,如果设置了_TIF_NEED_RESCHED,则调用schedule进程进程切换。

2.1.2 系统调用返回用户空间

系统调用处理 完以后,从ret_fast_syscall返回:

ret_fast_syscall:
 UNWIND(.fnstart	)
 UNWIND(.cantunwind	)
	disable_irq			//关闭中断	@ disable interrupts
	ldr	r1, [tsk, #TI_FLAGS]  //从thread info中获取#TI_FLAGS,
	tst	r1, #_TIF_WORK_MASK  //返回user之前,_TIF_WORK_MASK 中的位如果有值,则需要去处理相关任务
	bne	fast_work_pending   //先不返回,去处理pending的相关事务,比如系统调度
	asm_trace_hardirqs_on
 
	/* perform architecture specific actions before user return */
	arch_ret_to_user r1, lr    //这两个命令不是重点
	ct_user_enter
 
	restore_user_regs fast = 1, offset = S_OFF //恢复用户寄存器,并返回用户态
 UNWIND(.fnend		)
 
/*
 * Ok, we need to do extra processing, enter the slow path.
 */
fast_work_pending:   //penging相关的处理,暂时不分析
	str	r0, [sp, #S_R0+S_OFF]!		@ returned r0
work_pending:
	mov	r0, sp				@ 'regs'
	mov	r2, why				@ 'syscall'
	bl	do_work_pending
	cmp	r0, #0
	beq	no_work_pending
	movlt	scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)
	ldmia	sp, {r0 - r6}			@ have to reload r0 - r6
	b	local_restart			@ ... and off we go

可以看到上面代码和中断返回用户空间一样,会去判断thread_info的TI_FLAGS位,如果需要调度,那么就会去执行do_work_pending

2.2 从中断上下文返回进程内核上下文

当开启了CONFIG_PREEMPT抢占的情况下

    .align    5 
__irq_svc: 
    svc_entry----保存发生中断那一刻的现场保存在内核栈上 
    irq_handler ----具体的中断处理,同user mode的处理。
 
#ifdef CONFIG_PREEMPT
    get_thread_info tsk 
    ldr    r8, [tsk, #TI_PREEMPT]        @ get preempt count 
    ldr    r0, [tsk, #TI_FLAGS]        @ get flags 
    teq    r8, #0                @ if preempt count != 0 
    movne    r0, #0                @ force flags to 0 
    tst    r0, #_TIF_NEED_RESCHED 
    blne    svc_preempt 
#endif
 
    svc_exit r5, irq = 1            @ return from exception

可以看到系统调用以后,在内核空间中执行,然后又发生了中断,当irq_handler中断执行完以后,返回进程内核上下文空间继续执行之前,会判断进程的TI_PREEMPT位是否开启,即当前时刻是否可以抢占,如果开启了抢占,并且当前进程又设置了_TIF_NEED_RESCHED,则进入抢占处理

#ifdef CONFIG_PREEMPT
svc_preempt:
	mov	r8, lr    
1:	bl	preempt_schedule_irq		@ irq en/disable is done inside
	ldr	r0, [tsk, #TI_FLAGS]		@ get new tasks TI_FLAGS
	tst	r0, #_TIF_NEED_RESCHED
	moveq	pc, r8				@ go again
	b	1b
#endif

真正去处理抢占的函数是preempt_schedule_irq

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

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

	prev_state = exception_enter();

	do {
		add_preempt_count(PREEMPT_ACTIVE); //preempt_count +1,禁止抢占
		local_irq_enable();
		__schedule();
		local_irq_disable();
		sub_preempt_count(PREEMPT_ACTIVE); //preempt_count -1,开启抢占

		/*
		 * Check again in case we missed a preemption opportunity
		 * between schedule and now.
		 */
		barrier();
	} while (need_resched());

	exception_exit(prev_state);
}

可以看到preempt_schedule_irq函数里会一直检查进程的_TIF_NEED_RESCHED位,调用__schedule去做进程切换

2.3 进程调度函数__schedule

static void __sched __schedule(void)
{
/* prev保存换出进程(也就是当前进程),next保存换进进程 */
	struct task_struct *prev, *next;
	unsigned long *switch_count;
	struct rq *rq;
	int cpu;

need_resched:
	preempt_disable(); /* 禁止抢占 */
	cpu = smp_processor_id();/* 获取当前CPU ID */
	rq = cpu_rq(cpu);/* 获取当前CPU运行队列 */
	rcu_note_context_switch(cpu);
	prev = rq->curr;

	schedule_debug(prev);

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

	raw_spin_lock_irq(&rq->lock);

	switch_count = &prev->nivcsw;/* 当前进程非自愿切换次数 */
/*
       * 当内核抢占时会置位thread_info的preempt_count的PREEMPT_ACTIVE位,调用schedule()之后会清除,PREEMPT_ACTIVE置位表明是从内核抢占进入到此的
       * preempt_count()是判断thread_info的preempt_count整体是否为0
       * prev->state大于0表明不是TASK_RUNNING状态
       *
       */
/*
schedule过程中,如果是自愿调度走,
进程的调度实体也需要从active队列上删除    
如果是抢占的则不从运行队列优先级队列active上删除
*/
	if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
/* 当前进程不为TASK_RUNNING状态并且不是通过内核态抢占进入调度 */
		if (unlikely(signal_pending_state(prev->state, prev))) {
 /* 有信号需要处理,置为TASK_RUNNING */
			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, cpu);
				if (to_wakeup)
					try_to_wake_up_local(to_wakeup);
			}
		}
		switch_count = &prev->nvcsw;
	}

	pre_schedule(rq, prev);

	if (unlikely(!rq->nr_running))
		idle_balance(cpu, rq);
/*更新统计信息,将当前进程重新放回红黑树*/
	put_prev_task(rq, prev);
/* 获取下一个调度实体,这里的next的值会是一个进程,而不是一个调度组,在pick_next_task会递归选出一个进程 */
	next = pick_next_task(rq);
/* 清除当前进程的thread_info结构中的flags的TIF_NEED_RESCHED和PREEMPT_NEED_RESCHED标志位,这两个位表明其可以被调度调出(因为这里已经调出了,所以这两个位就没必要了) */
	clear_tsk_need_resched(prev);
	rq->skip_clock_update = 0;

	if (likely(prev != next)) {  //当前进程和选出的最优执行进程不同,则需要切换
		rq->nr_switches++;/* 该CPU进程切换次数加1 */
		rq->curr = next;/* 该CPU当前执行进程为新进程 */
		++*switch_count;
/* 这里进行了进程上下文的切换 */
		context_switch(rq, prev, next); /* unlocks the rq */
		/*
		 * The context switch have flipped the stack from under us
		 * and restored the local variables which were saved when
		 * this task called schedule() in the past. prev == current
		 * is still correct, but it can be moved to another cpu/rq.
		 */
		cpu = smp_processor_id();
		rq = cpu_rq(cpu);
	} else
		raw_spin_unlock_irq(&rq->lock);
/* 上下文切换后的处理 */
	post_schedule(rq);
/* 重新打开抢占使能但不立即执行重新调度 */
	sched_preempt_enable_no_resched();
	if (need_resched())
		goto need_resched;
}

选取下一个进程的任务在__schedule()中交给了pick_next_task()函数,而进程切换则交给了context_switch()函数。我们先看看pick_next_task()函数是如何选取下一个进程的:

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:
	 */
//如果进程都在cfs队列里面运行,则直接调用cfs调度类的pick_next_task
	if (likely(rq->nr_running == rq->cfs.h_nr_running)) {
		p = fair_sched_class.pick_next_task(rq);
		if (likely(p))
			return p;
	}
//否则,则根据调度类的优先级,去一次检查每个队列里面符合条件的调度实体
	for_each_class(class) {
		p = class->pick_next_task(rq);
		if (p)
			return p;
	}

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

2.3.1 不同调度类的pick_next_task

2.3.1.1 cfs调度类的pick_next_task

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);
	if (hrtick_enabled(rq))
		hrtick_start_fair(rq, p);

	return p;
}
    static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)  
    {  
        //__pick_next_entity就是直接选择红黑树缓存的最左结点,也就是vruntime最小的结点  
        struct sched_entity *se = __pick_next_entity(cfs_rq);  
        if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, se) < 1)  
            return cfs_rq->next;  
        if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, se) < 1)  
            return cfs_rq->last;  
        return se;  //如果选出的进程vruntime值比next和last指向的进程的vruntime值小到粒度之外, 则返回新选出的进程
    }  

    static void  
    set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)  
    {  
        if (se->on_rq) {  

            update_stats_wait_end(cfs_rq, se);  
            //就是把结点从红黑树上取下来. 前面说过, 当前运行进程不在红黑树上  
            __dequeue_entity(cfs_rq, se);  //把新选出的进程移出红黑树
        }  
        update_stats_curr_start(cfs_rq, se);  
        cfs_rq->curr = se;  //设置为当前进程

        se->prev_sum_exec_runtime = se->sum_exec_runtime;  //记录本次调度之前总的已运行时间
    }  

pick_next_entity一般是宣城红黑树里面vruntime最下的那个节点,把其调度实体返回,其他情况暂时不讨论。 

需要注意的是,当前运行进程不会处于运行队列中,所以需要把选中的进程从cfs调度队列中取出,在set_next_entity中调用__dequeue_entity把当前进程从红黑树队列中删除。

2.3.1.2 实时进程的pick_next_task

static struct task_struct *pick_next_task_rt(struct rq *rq)
{
	struct task_struct *p = _pick_next_task_rt(rq);

	/* The running task is never eligible for pushing */
	if (p)
		dequeue_pushable_task(rq, p);

#ifdef CONFIG_SMP
	/*
	 * We detect this state here so that we can avoid taking the RQ
	 * lock again later if there is no need to push
	 */
	rq->post_schedule = has_pushable_tasks(rq);
#endif

	return p;
}

pick_next_task_rt

    ------------>_pick_next_task_rt

static struct task_struct *_pick_next_task_rt(struct rq *rq)
{
	struct sched_rt_entity *rt_se;
	struct task_struct *p;
	struct rt_rq *rt_rq;

	rt_rq = &rq->rt;

	if (!rt_rq->rt_nr_running)
		return NULL;
//这边就是前面说的,当实时进程在一个调度周期内运行的时间太长,就会设置rt_throttled,不再从实时队列里面选择任务执行
	if (rt_rq_throttled(rt_rq))
		return NULL;

	do {
		rt_se = pick_next_rt_entity(rq, rt_rq);//从队列里选择调度优先级最高的进程
		BUG_ON(!rt_se);
		rt_rq = group_rt_rq(rt_se);
	} while (rt_rq);

	p = rt_task_of(rt_se);
	p->se.exec_start = rq->clock_task; //设置选中的该进程起始执行时间

	return p;
}

pick_next_task_rt

    ------------>_pick_next_task_rt

          ------------->pick_next_rt_entity

static struct sched_rt_entity *pick_next_rt_entity(struct rq *rq,
						   struct rt_rq *rt_rq)
{
	struct rt_prio_array *array = &rt_rq->active;
	struct sched_rt_entity *next = NULL;
	struct list_head *queue;
	int idx;
从位图中选择第一个不为0的位的index(优先级最高的那个)
	idx = sched_find_first_bit(array->bitmap);
	BUG_ON(idx >= MAX_RT_PRIO);

	queue = array->queue + idx; //通过idx作为hash表的键值,找到对应的链表
	next = list_entry(queue->next, struct sched_rt_entity, run_list);//从链表中取出第一个

	return next;
}

可以看到pick_next_rt_entity从hash表中找出优先级最高的那个队列,然后取出队列头,就是我们需要调度的优先级最高的进程。

总结一下,上面的工作主要用流程如下:

               

2.3.2 put_prev_task

2.3.2.1 put_prev_task_fair

cfs调度类为put_prev_task_fair

static void put_prev_task_fair(struct rq *rq, struct task_struct *prev)
{
	struct sched_entity *se = &prev->se;
	struct cfs_rq *cfs_rq;

	for_each_sched_entity(se) {
		cfs_rq = cfs_rq_of(se);
		put_prev_entity(cfs_rq, se);
	}
}
static void put_prev_entity(struct cfs_rq *cfs_rq, struct sched_entity *prev)
{
	/*
	 * If still on the runqueue then deactivate_task()
	 * was not called and update_curr() has to be done:
	 */
	if (prev->on_rq)
		update_curr(cfs_rq);

	/* throttle cfs_rqs exceeding runtime */
	check_cfs_rq_runtime(cfs_rq);

	check_spread(cfs_rq, prev);
	if (prev->on_rq) {
		update_stats_wait_start(cfs_rq, prev);
		/* Put 'current' back into the tree. */
		__enqueue_entity(cfs_rq, prev);
		/* in !on_rq case, update occurred at dequeue */
		update_entity_load_avg(prev, 1);
	}
	cfs_rq->curr = NULL;
}

在cfs算法中,当前run的进程,其本身不在cfs红黑树队列中,因为马上要被替换掉了,所以调用__enqueue_entity把他重新放入红黑树队列中。 

                       

2.3.2.2 put_prev_task_rt

实时进程为put_prev_task_rt

put_prev_task_rt

     --------------->update_curr_rt

update_curr_rt函数在上面已经分析过。

                           

2.3.3 进程切换函数context_switch

static inline void
context_switch(struct rq *rq, struct task_struct *prev,
	       struct task_struct *next)
{
	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);
---------------------------------------------------------------(1)
	if (!mm) {/* 如果新进程的内存描述符为空,说明新进程为内核线程 */
		next->active_mm = oldmm;//借用当前进程的mm,b并增加引用计数
		atomic_inc(&oldmm->mm_count);
		enter_lazy_tlb(oldmm, next);
	} else
----------------------------------------------------------------(2)
		switch_mm(oldmm, mm, next); //如果不是内核线程,则有自己的mm,切换进程空间
------------------------------------------------------------------(3)
	if (!prev->mm) {
		prev->active_mm = NULL;/* 如果被切换出去的进程是内核线程 */
		rq->prev_mm = oldmm; /* 记录借用的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:
	 */
#ifndef __ARCH_WANT_UNLOCKED_CTXSW
	spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
#endif

	context_tracking_task_switch(prev, next);
	/* Here we just switch the register state and the stack. */
/* 切换进程上下文*/
--------------------------------------------------------------------(4)
	switch_to(prev, next, prev);

	barrier();
	/*
	 * this_rq must be evaluated again because prev may have moved
	 * CPUs since it called schedule(), thus the 'rq' on its stack
	 * frame will be invalid.
	 */
/*进行进程切换的最后工作*/
----------------------------------------------------------------(5)
	finish_task_switch(this_rq(), prev);
}

下面对context_switch函数做详细分析

(1)我们知道,内核线程是不具有mm结构的,他只有内核空间,不具有用户空间,由于所有进程的内核页表都是相同的,所以我们可以借用上一个进程的mm,使用其内核空间即可

(2)如果是用户进程,那么用户空间的页表肯定是不一样的,所以需要用switch_mm来切换页表:

switch_mm

    ------------->check_and_switch_context

            --------------->cpu_switch_mm(mm->pgd, mm);

mm->pgd为页表目录基地址

#define cpu_switch_mm(pgd,mm) cpu_do_switch_mm(virt_to_phys(pgd),mm)

cpu_do_switch_mm对应于我是用的架构的汇编函数为:

ENTRY(cpu_arm920_switch_mm)
#ifdef CONFIG_MMU
	mov	ip, #0
#ifdef CONFIG_CPU_DCACHE_WRITETHROUGH
	mcr	p15, 0, ip, c7, c6, 0		@ invalidate D cache
#else
@ && 'Clean & Invalidate whole DCache'
@ && Re-written to use Index Ops.
@ && Uses registers r1, r3 and ip

	mov	r1, #(CACHE_DSEGMENTS - 1) << 5	@ 8 segments
1:	orr	r3, r1, #(CACHE_DENTRIES - 1) << 26 @ 64 entries
2:	mcr	p15, 0, r3, c7, c14, 2		@ clean & invalidate D index
	subs	r3, r3, #1 << 26
	bcs	2b				@ entries 63 to 0
	subs	r1, r1, #1 << 5
	bcs	1b				@ segments 7 to 0
#endif
	mcr	p15, 0, ip, c7, c5, 0		@ invalidate I cache
	mcr	p15, 0, ip, c7, c10, 4		@ drain WB
	mcr	p15, 0, r0, c2, c0, 0		@ load page table pointer//切换页表
	mcr	p15, 0, ip, c8, c7, 0		@ invalidate I & D TLBs
#endif
	mov	pc, lr

我们只需要关注这一句代码即可mcr    p15, 0, r0, c2, c0, 0        @ load page table pointer

r0传入的参数是新进程的页表基地址,把该基地址写入cp15协处理其的c2寄存器即可,c2寄存器是页表基址寄存器。即完成了页表的切换,由于当前运行在内核空间,进程内核空间的页表都是一样的,所以这个切换是很安全的。

(3)在第一步中,内核线程引用了其他进程的mm,并增加了引用计数,那么什么时候要减少引用计数,释放该mm呢。

这里会有点绕口,假设prev A是个用户进程,是要切换出去的那个进程,next B是个内核进程,是个要切换进去的进程,经历第一步,next B借用了 A的mm,并且prev->mm不为0,什么都不做,A进程切换成B进程以后,在B进程切换成C进程之前,prevB 的prev->mm为0,所以把之前借用的active_mm记录下来,执行进程切换,切换到next C中,在C进程的finish_task_switch中发现rq->prev_mm不为0,则减少该mm的应用计数。所以整个过程是在A中借用,C中归还。

(4)切换进程上下文,即寄存器和堆栈。

switch_to

    ------------->__switch_to

ENTRY(__switch_to)
 UNWIND(.fnstart	)
 UNWIND(.cantunwind	)
	add	ip, r1, #TI_CPU_SAVE //获取上一个进程thread_info的偏移TI_CPU_SAVE的地址
	ldr	r3, [r2, #TI_TP_VALUE] /* r2 指向的是要切换进程的 thread_info
                                          * 在 arch\arm\kernel\asm-offsets.c 里 
                                          * DEFINE(TI_TP_VALUE,     offsetof(struct thread_info, tp_value));
                                          * r3 得到要切换进程的 tp_value.
                                          */


 ARM(	stmia	ip!, {r4 - sl, fp, sp, lr} )	@ Store most regs on stack   /* 将当前进程寄存器的值保存到当前进程 thread info的 cpu_context 里
                                                 * 其中 lr 保存到了 cpu_context_save 里的 pc 里。
                                                 * ia 表示 increase after, 由于使用了 !, 从而 ip也会一直更新,
                                                 * 最后指向了 cpu_context 里的 extra
                                                 */
 THUMB(	stmia	ip!, {r4 - sl, fp}	   )	@ Store most regs on stack
 THUMB(	str	sp, [ip], #4		   )
 THUMB(	str	lr, [ip], #4		   )
#ifdef CONFIG_CPU_USE_DOMAINS
	ldr	r6, [r2, #TI_CPU_DOMAIN]
#endif
	set_tls	r3, r4, r5  //TLS即Thread Local Storage,可以高效的访问TLS里面存储的信息而不用一次次的调用系统调用,暂不分析
#if defined(CONFIG_CC_STACKPROTECTOR) && !defined(CONFIG_SMP)
	ldr	r7, [r2, #TI_TASK]/* DEFINE(TI_TASK,        offsetof(struct thread_info, task)); 
                              * 通过thread info 得到要切入进程的 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  /* r4 指向的要切入进程的 thread_info 中的 struct cpu_context_save    cpu_context; */
	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           /* 此时 r0 表示 previous task_struct ,即要切出的进程的 task */
 ARM(	ldmia	r4, {r4 - sl, fp, sp, pc}  )	@ Load all regs saved previously  /* 将要切入进程的 cpu_context 的值加载到寄存器, 
                                                 * 其中加载到 pc,实现跳转执行
                                                 * 如果该切入的进程是之前切出的的,则加载到 pc 的值,为
                                                 * context_switch 中 switch_to 的下一条指令,即 barrier();
                                                 */
 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)

可以看到__switch_to做的主要工作就是保存当前进程的寄存器到thread_info的cpu_context中,并把下一个进程的cpu_context中保存的寄存器的信息,恢复到寄存器中,完成进程的切换。从__switch_to返回,我们已经在下一个进程的空间了。

(5)finish_task_switch

static void finish_task_switch(struct rq *rq, struct task_struct *prev)
	__releases(rq->lock)
{
	struct mm_struct *mm = rq->prev_mm;
	long prev_state;

	rq->prev_mm = NULL;

	/*
	 * A task struct has one reference for the use as "current".
	 * If a task dies, then it sets TASK_DEAD in tsk->state and calls
	 * schedule one last time. The schedule call will never return, and
	 * the scheduled task must drop that reference.
	 * The test for TASK_DEAD must occur while the runqueue locks are
	 * still held, otherwise prev could be scheduled on another cpu, die
	 * there before we look at prev->state, and then the reference would
	 * be dropped twice.
	 *		Manfred Spraul <manfred@colorfullife.com>
	 */
	prev_state = prev->state;
	vtime_task_switch(prev);
	finish_arch_switch(prev);
	perf_event_task_sched_in(prev, current);
	finish_lock_switch(rq, prev);
	finish_arch_post_lock_switch();

	fire_sched_in_preempt_notifiers(current);
	if (mm)  //释放上一个进程借用的mm
		mmdrop(mm);
	if (unlikely(prev_state == TASK_DEAD)) {
		/*
		 * Remove function-return probe instances associated with this
		 * task and put them back on the free list.
		 */
		kprobe_flush_task(prev);
		put_task_struct(prev);
	}

	tick_nohz_task_switch(current);
}

最后总结一下:

周期性调度器完成的任务就是不管是cfs调度还是实时进程调度,选择下一个可以调度的最优任务,如果需要调度,则把当前进程置上调度位cfs调度则把该任务放到红黑树的最左边,而实时进程则调整队列顺序,requeue该进程。因为调度器总是从队列头开始取最优进程进行调度。

特定时刻主调度器 则负责任务的切换,根据周期性调度器算出来的最优调度进程,切换到该进程上面继续执行。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux内核进程调度是一个非常复杂的系统,主要由可调度进程队列、进程调度策略、调度负责。 在Linux中,进程调度是一个单独的内核线程,其主要工作是在可调度进程队列中选择下一个要运行的进程,并将CPU分配给该进程。进程调度根据进程的优先级和调度策略来选择下一个要运行的进程。 Linux中进程调度的核心代码位于sched目录下,主要包括以下文件: 1. sched.h:定义了调度的数据结构和函数原型。 2. sched.c:实现了进程调度的主要功能,包括进程的加入、删除、更新等操作。 3. rt.c:实时调度策略相关代码。 4. fair.c:CFS调度策略相关代码。 5. idle.c:空闲进程相关代码。 6. deadline.c:DEADLINE调度策略相关代码。 下面我们以CFS调度策略为例,简单介绍一下进程调度的实现过程: CFS调度策略是一种完全公平的调度策略,它通过动态优先级来保证进程的公平性。在CFS调度策略中,每个进程都有一个虚拟运行时间(virtual runtime),该时间是进程已经运行的时间和优先级的函数。 CFS调度策略的核心代码位于fair.c文件中,主要包括以下函数: 1. enqueue_task_fair():将一个进程添加到可调度进程队列中。 2. dequeue_task_fair():将一个进程从可调度进程队列中删除。 3. update_curr_fair():更新当前进程的虚拟运行时间。 4. pick_next_task_fair():选择下一个要运行的进程。 以上函数的实现过程中,都涉及到了对进程调度的数据结构的操作,如可调度进程队列、进程控制块等。具体实现过程需要结合代码进行分析。 总体来说,Linux内核进程调度的实现非常复杂,需要涉及到很多的数据结构和算法,同时还需要考虑到性能、公平性等因素。因此,对于想要深入了解Linux内核的人来说,进程调度是必须要掌握的一个重要部分。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值