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->runtime task->se->sum_exec _runtime 每個進程對應一個可調度實體,在 task_struct 的結構體中,該實體就是成員變量 se。
task->weight task->se.load.weight  
task->vruntime task->se.vruntime  
∑( task->weight) cfs_rq->load.weight 在抽象模型中,我們計算 ideal_runtime 的時候 需要求所有進程的權重值的和,在實現的時候, 沒有求和的過程,而是把該值記錄在就緒隊列 的 load.weight 中。向就緒隊列中添加新進程時,就加上新進程的 權重值,進程被移出就緒隊列時則減去被移除 的進程的權重值。
  cfs_rq->min_vruntime 該值用來解決之前在抽象模型中遺留的問題, 所以在抽象模型中沒有與之對應的值。
task->ideal_runtime sched_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 <[email protected]>
	 */
	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該進程。因爲調度器總是從隊列頭開始取最優進程進行調度。

特定時刻主調度器 則負責任務的切換,根據週期性調度器算出來的最優調度進程,切換到該進程上面繼續執行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章