了解程序排程時機跟蹤分析程序排程與程序切換的過程
本章的基礎知識總結
- 一般來說,程序排程分為三種類型:中斷處理過程(包括時鐘中斷、I/O 中斷、系統調用和異常)中,直接調用schedule,或者傳回使用者态時根據 need_resched 标記調用 schedule;核心線程可以直接調用 schedule 進行程序切換,也可以在中斷處理過程中進行排程,也就是說核心線程作為一類的特殊的程序可以主動排程,也可以被動排程;使用者态程序無法實作主動排程,僅能通過陷入核心态後的某個時機點進行排程,即在中斷處理過程中進行排程。
- 為了控制程序的執行,核心必須有能力挂起正在 CPU 上執行的程序,并恢複以前挂起的某個程序的執行的過程,叫做程序切換、任務切換、上下文切換。挂起正在 CPU 上執行的程序,與中斷時儲存現場是有差別的,中斷前後是在同一個程序上下文中,隻是由使用者态轉向核心态執行。也即是說中斷是在同一個程序中執行的,程序上下文是在不同的程序中執行的。
- 程序上下文資訊:使用者位址空間:包括程式代碼,資料,使用者堆棧等;控制資訊:程序描述符,核心堆棧等;硬體上下文(注意中斷也要儲存硬體上下文隻是儲存的方法不同);schedule 函數選擇一個新的程序來運作,并調用 context_switch 宏進行上下文的切換,這個宏又調用 switch_to 宏來進行關鍵上下文切換;switch_to 宏中定義了 prev 和 next 兩個參數:prev 指向目前程序,next 指向被排程的程序。
實驗流程
1.用gdb來進行調試,并設定相應的斷點
2.schedule()函數斷點截圖,程序排程的主體
3.context_switch函數的斷點截圖,用于實作程序的切換
4.pick_next_task函數斷點截圖,使用某種排程政策選擇下一個程序來切換
代碼分析
static void __sched __schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
need_resched:
preempt_disable();
cpu = smp_processor_id();
rq = cpu_rq(cpu);
rcu_note_context_switch(cpu);
prev = rq->curr;
schedule_debug(prev);
if (sched_feat(HRTICK))
hrtick_clear(rq);
/*
* Make sure that signal_pending_state()->signal_pending() below
* can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
* done by the caller to avoid the race with signal_wake_up().
*/
smp_mb__before_spinlock();
raw_spin_lock_irq(&rq->lock);
switch_count = &prev->nivcsw;
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
if (unlikely(signal_pending_state(prev->state, prev))) {
prev->state = TASK_RUNNING;
} else {
deactivate_task(rq, prev, DEQUEUE_SLEEP);
prev->on_rq = 0;
/*
* If a worker went to sleep, notify and ask workqueue
* whether it wants to wake up a task to maintain
* concurrency.
*/
if (prev->flags & PF_WQ_WORKER) {
struct task_struct *to_wakeup;
to_wakeup = wq_worker_sleeping(prev, cpu);
if (to_wakeup)
try_to_wake_up_local(to_wakeup);
}
}
switch_count = &prev->nvcsw;
}
if (task_on_rq_queued(prev) || rq->skip_clock_update < 0)
update_rq_clock(rq);
next = pick_next_task(rq, prev);
clear_tsk_need_resched(prev);
clear_preempt_need_resched();
rq->skip_clock_update = 0;
if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
++*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 函數主要做了這麼幾件事:針對搶占的處理;檢查prev的狀态,并且重設state的狀态;next = pick_next_task(rq, prev); //程序排程;更新就緒隊列的時鐘;context_switch(rq, prev, next); //程序上下文切換
stwitch_to的代碼
asm volatile("pushfl\n\t" /* save flags */ \
"pushl %%ebp\n\t" /* save EBP */ \
"movl %%esp,%[prev_sp]\n\t" /* save ESP */ \
"movl %[next_sp],%%esp\n\t" /* restore ESP */ \
"movl $1f,%[prev_ip]\n\t" /* save EIP */ \
"pushl %[next_ip]\n\t" /* restore EIP */ \
"jmp __switch_to\n" /* regparm call */ \
"1:\t" \
"popl %%ebp\n\t" /* restore EBP */ \
"popfl\n" /* restore flags */ \
\
/* output parameters */ \
: [prev_sp] "=m" (prev->thread.sp), \
/* =m 表示把變量放入記憶體,即把 [prev_sp] 存儲的變量放入記憶體,最後再寫入prev->thread.sp */\
[prev_ip] "=m" (prev->thread.ip), \
"=a" (last), \
/*=a 表示把變量 last 放入 ax, eax = last */ \
\
/* clobbered output registers: */ \
"=b" (ebx), "=c" (ecx), "=d" (edx), \
/* b 表示放入ebx, c 表示放入 ecx,d 表示放入 edx, S表示放入 si, D 表示放入 edi */\
"=S" (esi), "=D" (edi) \
\
/* input parameters: */ \
: [next_sp] "m" (next->thread.sp), \
/* next->thread.sp 放入記憶體中的 [next_sp] */\
[next_ip] "m" (next->thread.ip), \
\
/* regparm parameters for __switch_to (): */ \
[prev] "a" (prev), \
/*eax = prev edx = next*/\
[next] "d" (next) \
\
: /* reloaded segment registers */ \
"memory");
switch_to從A程序切換到B程序的步驟如下:
1.複制兩個變量到寄存器: [prev]"a" (prev) [next]"d" (next)。這也就是eax <== prev_A或eax<%p(%ebp_A) edx < next_A 或edx<==%n(%ebp_A)
2.儲存程序A的ebp和eflags。注意,因為現在esp還在A的堆棧中,是以它們是被儲存到A程序的核心堆棧中。
3.儲存目前esp到A程序核心描述符中: 這也就是prev_A->thread.sp<== esp_A 在調用switch_to時,prev是指向A程序自己的程序描述符的。
4.從next(程序B)的描述符中取出之前從B切換出去時儲存的esp_B 注意,在A程序中的next是指向B的程序描述符的。從這個時候開始,CPU目前執行的程序已經是B程序了,因為esp已經指向B的核心堆棧。但是,現在的ebp仍然指向A程序的核心堆棧,是以所有局部變量仍然是A中的局部變量,比如next實質上是%n(%ebp_A),也就是next_A,即指向B的程序描述符。
5.把标号為1的指令位址儲存到A程序描述符的ip域:當A程序下次從switch_to回來時,會從這條指令開始執行。具體方法要看後面被切換回來的B的下一條指令。
6.将傳回位址儲存到堆棧,然後調用switch_to()函數,switch_to()函數完成硬體上下文切換 注意,如果之前B也被switch_to出去過,那麼[next_ip]裡存的就是下面這個1f的标号,但如果程序B剛剛被建立,之前沒有被switch_to出去過,那麼[next_ip]裡存的将是ret_ftom_fork(參看copy_thread()函數)。
當這裡switch_to()傳回時,将傳回值prev_A又寫入了%eax,這就使得在switch_to宏裡面eax寄存器始終儲存的是prev_A的内容,或者,更準确的說,是指向A程序描述符的“指針”。
7.從switch_to()傳回後繼續從1:标号後面開始執行,修改ebp到B的核心堆棧,恢複B的eflags。
8.将eax寫入last,以在B的堆棧中儲存正确的prev資訊。是以,這裡面的last實質上就是prev,是以在switch_to宏執行完之後,prev_B就是正确的A的程序描述符了。這裡,last的作用相當于把程序A堆棧中的A程序描述符位址複制到了程序B的堆棧中。
9.至此,switch_to已經執行完成,A停止運作,而開始執行B。此後,可能在某一次排程中,程序A得到排程,就會出現switch_to(C,A)這樣的調用,這時,A再次得到排程,得到排程後,A程序從context_switch()中switch_to後面的代碼開始執行,這時候,它看到的prev_A将指向C的程序描述符。
本章總結
一般情形:
正在運作的使用者态程序 A 切換到運作使用者态程序 B 的過程:
1、正在運作的使用者态程序 A;
2、中斷——save cs:eip/esp/eflags(current) to kernel stack,and load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack);
3、SAVE_ALL //儲存現場;
4、中斷處理或中斷傳回前調用 schedule,其中,switch_to 做了關鍵的程序上下文切換;
5、标号1之後開始運作使用者态程序 B;
6、restore_all //恢複現場;
7、iret——pop cs:eip/ss:esp/eflags from kernel stack;
8、繼續運作使用者态程序 B;
特殊情況:
1、通過中斷處理過程中的排程,使用者态程序與核心程序之間互相切換,與一般情形類似;
2、核心程序程主動調用 schedule 函數,隻有程序上下文的切換,沒有中斷上下文切換;
3、建立子程序的系統調用在子程序中的執行起點及傳回使用者态,如:fork;
4、加載一個新的可執行程式後傳回到使用者态的情況,如:execve;