進程調(diào)度相關(guān)內(nèi)核結(jié)構(gòu)
我們知道,進程運行需要各種各樣的系統(tǒng)資源,如內(nèi)存、文件、打印機和最寶貴的 CPU 等,所以說,調(diào)度的實質(zhì)就是資源的分配。系統(tǒng)通過不同的調(diào)度算法(Scheduling Algorithm)來實現(xiàn)這種資源的分配。通常來說,選擇什么樣的調(diào)度算法取決于資源分配的策略(Scheduling Policy)。
有關(guān)調(diào)度相關(guān)的結(jié)構(gòu)保存在 task_struct 中,如下:
struct task_struct {
/*
task_struct 采用了如下3個成員表示進程的優(yōu)先級,prio、normal_prio表示動態(tài)優(yōu)先級
static_prio 為靜態(tài)優(yōu)先級,靜態(tài)優(yōu)先級是進程啟動時分配的優(yōu)先級,它可以使用nice和sched_setscheduler系統(tǒng)調(diào)用修改,
否則在進程運行期間會一直保持恒定
*/
int prio, static_prio, normal_prio;
/*
sched_clas表示該進程所屬的調(diào)度器類
2.6.24版本中有3中調(diào)度類:
實時調(diào)度器 : rt_sched_class
完全公平調(diào)度器:fair_sched_class
空閑調(diào)度器: idle_sched_class
*/
const struct sched_class *sched_clas;
//調(diào)度實體
struct sched_entity se;
/*
policy 保存了對該進程應(yīng)用的調(diào)度策略。
進程的調(diào)度策略有6種
SCHED_NORMAL SCHED_FIFO SCHED_RR SCHED_BATCH SCHED_IDLE
普通進程調(diào)度策略: SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE,這些都是通過完全公平調(diào)度器來處理的
實時進程調(diào)度策略: SCHED_RR、SCHED_FIFO 這些都是通過實時調(diào)度器來處理的
*/
unsigned int policy;
/*
除了內(nèi)核線程(Kernel Thread),每個進程都擁有自己的地址空間(也叫虛擬空間),用mm_struct 來描述。active_mm是為內(nèi)核線程而引入的。因為內(nèi)核線程沒有自己的地址空間,
為了讓內(nèi)核線程與普通進程具有統(tǒng)一的上下文切換方式,當(dāng)內(nèi)核線程進行上下文切換時,讓切換進來的線程的active_mm
指向剛被調(diào)度出去的進程的active_mm(如果進程的mm 域不為空,則其active_mm 域與mm 域相同)
*/
struct mm_struct *mm, *active_mm;
//表示實時進程的優(yōu)先級,最低的優(yōu)先級為0,最高為99,值越大,優(yōu)先級越高
unsigned int rt_priority;
...
};
每個進程都擁有自己的地址空間(也叫虛擬空間),用 mm_struct 來描述。
active_mm 是為內(nèi)核線程而引入的,因為內(nèi)核線程沒有自己的地址空間,為了讓內(nèi)核線程與普通進程具有統(tǒng)一的上下文切換方式,當(dāng)內(nèi)核線程進行上下文切換時,讓切換進來的線程的 active_mm 指向剛被調(diào)度出去的進程的 active_mm(如果進程的mm 域不為空,則其 active_mm 域與 mm 域相同)。

在 linux 2.6 中 sched_class 表示該進程所屬的調(diào)度器類有3種:
- 實時調(diào)度器 (RT,rt_sched_class) : 為每個優(yōu)先級維護一個隊列;
- 完全公平調(diào)度器 (CFS,fair_sched_class) : 采用完全公平調(diào)度算法,引入虛擬運行時間的概念。
- 空閑調(diào)度器(idle_sched_class) : 為每個 CPU 都會有一個 idle 線程,當(dāng)沒有其他進程可以調(diào)度時,調(diào)度運行idle線程。
進程的調(diào)度策略有5種,用戶可以調(diào)用調(diào)度器里不同的調(diào)度策略:
- SCHED_FIFO :采用了先入先出,不使用時間片的調(diào)度算法。若處于可執(zhí)行狀態(tài),就會一直執(zhí)行,沒有更高優(yōu)先級的情況下,進程只能等待其主動讓出 cpu;
- SCHED_RR :時間片輪轉(zhuǎn),進程用完時間片后加入優(yōu)先級對應(yīng)的運行隊列的尾部,把 CPU 讓給同一優(yōu)先級的其他進程;
- SCHED_NORMAL : 使 task 選擇 CFS 調(diào)度器來調(diào)度運行;
- SCHED_BATCH:批量處理,使 task 選擇 CFS 調(diào)度器來調(diào)度運行;
- SCHED_IDLE: 使 task 選擇 CFS 調(diào)度器來調(diào)度運行

在每個 CPU 中都有一個自身的運行隊列 rq,每個活動進程只出現(xiàn)在一個運行隊列中,在多個 CPU 上同時運行一個進程是不可能的。
運行隊列是使用如下結(jié)構(gòu)實現(xiàn)的:
struct rq {
//運行隊列中進程的數(shù)目
unsigned long nr_running;
//進程切換總數(shù)
u64 nr_switches;
//用于完全公平調(diào)度器的就緒隊列
struct cfs_rq cfs;
//用于實時調(diào)度器的就緒隊列
struct rt_rq rt;
//記錄本cpu尚處于TASK_UNINTERRUPTIBLE狀態(tài)的進程數(shù),和負載信息有關(guān)
unsigned long nr_uninterruptible;
//curr指向當(dāng)前運行的進程實例,idle 指向空閑進程的實例
struct task_struct *curr, *idle;
//上一次調(diào)度時的mm結(jié)構(gòu)
struct mm_struct *prev_mm;
...
};
每個運行隊列中有2個調(diào)度隊列:CFS 調(diào)度隊列 和 RT 調(diào)度隊列。
tast 作為調(diào)度實體加入到 CPU 中的調(diào)度隊列中。

系統(tǒng)中所有的運行隊列都在 runqueues 數(shù)組中,該數(shù)組的每個元素分別對應(yīng)于系統(tǒng)中的一個 CPU。在單處理器系統(tǒng)中,由于只需要一個就緒隊列,因此數(shù)組只有一個元素。
static DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
內(nèi)核也定義了一下便利的宏,其含義很明顯。
#define cpu_rq(cpu) (&per_cpu(runqueues, (cpu)))
#define this_rq() (&__get_cpu_var(runqueues))
#define task_rq(p) cpu_rq(task_cpu(p))
#define cpu_curr(cpu) (cpu_rq(cpu)->curr)
進程調(diào)度與切換
在分析調(diào)度流程之前,我們先來看在什么情況下要執(zhí)行調(diào)度程序,我們把這種情況叫做調(diào)度時機。
Linux 調(diào)度時機主要有。
- 進程狀態(tài)轉(zhuǎn)換的時刻:進程終止、進程睡眠;
- 當(dāng)前進程的時間片用完時(current->counter=0);
- 設(shè)備驅(qū)動程序;
- 進程從中斷、異常及系統(tǒng)調(diào)用返回到用戶態(tài)時。
時機1,進程要調(diào)用 sleep() 或 exit() 等函數(shù)進行狀態(tài)轉(zhuǎn)換,這些函數(shù)會主動調(diào)用調(diào)度程序進行進程調(diào)度。
時機2,由于進程的時間片是由時鐘中斷來更新的,因此,這種情況和時機4 是一樣的。
時機3,當(dāng)設(shè)備驅(qū)動程序執(zhí)行長而重復(fù)的任務(wù)時,直接調(diào)用調(diào)度程序。在每次反復(fù)循環(huán)中,驅(qū)動程序都檢查 need_resched 的值,如果必要,則調(diào)用調(diào)度程序 schedule() 主動放棄 CPU。
時機4 , 如前所述, 不管是從中斷、異常還是系統(tǒng)調(diào)用返回, 最終都調(diào)用 ret_from_sys_call(),由這個函數(shù)進行調(diào)度標(biāo)志的檢測,如果必要,則調(diào)用調(diào)用調(diào)度程序。那么,為什么從系統(tǒng)調(diào)用返回時要調(diào)用調(diào)度程序呢?這當(dāng)然是從效率考慮。從系統(tǒng)調(diào)用返回意味著要離開內(nèi)核態(tài)而返回到用戶態(tài),而狀態(tài)的轉(zhuǎn)換要花費一定的時間,因此,在返回到用戶態(tài)前,系統(tǒng)把在內(nèi)核態(tài)該處理的事全部做完。

Linux 的調(diào)度程序是一個叫 Schedule() 的函數(shù),這個函數(shù)來決定是否要進行進程的切換,如果要切換的話,切換到哪個進程等。
Schedule 的實現(xiàn)
asmlinkage void __sched schedule(void)
{
/*prev 表示調(diào)度之前的進程, next 表示調(diào)度之后的進程 */
struct task_struct *prev, *next;
long *switch_count;
struct rq *rq;
int cpu;
need_resched:
preempt_disable(); //關(guān)閉內(nèi)核搶占
cpu = smp_processor_id(); //獲取所在的cpu
rq = cpu_rq(cpu); //獲取cpu對應(yīng)的運行隊列
rcu_qsctr_inc(cpu);
prev = rq->curr; /*讓prev 成為當(dāng)前進程 */
switch_count = &prev->nivcsw;
/釋放全局內(nèi)核鎖,并開this_cpu 的中斷/
release_kernel_lock(prev);
need_resched_nonpreemptible:
__update_rq_clock(rq); //更新運行隊列的時鐘值
...
if (unlikely(!rq->nr_running))
idle_balance(cpu, rq);
// 對應(yīng)到CFS,則為 put_prev_task_fair
prev->sched_class->put_prev_task(rq, prev); //通知調(diào)度器類當(dāng)前運行進程要被另一個進程取代
/pick_next_task以優(yōu)先級從高到底依次檢查每個調(diào)度類,從最高優(yōu)先級的調(diào)度類中選擇最高優(yōu)先級的進程作為
下一個應(yīng)執(zhí)行進程(若其余都睡眠,則只有當(dāng)前進程可運行,就跳過下面了)/
next = pick_next_task(rq, prev); //選擇需要進行切換的task
//進程prev和進程next切換前更新各自的sched_info
sched_info_switch(prev, next);
if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
++switch_count;
//完成進程切換(上下文切換)
context_switch(rq, prev, next); / unlocks the rq */
} else
spin_unlock_irq(&rq->lock);
if (unlikely(reacquire_kernel_lock(current) < 0)) {
cpu = smp_processor_id();
rq = cpu_rq(cpu);
goto need_resched_nonpreemptible;
}
preempt_enable_no_resched();
if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
goto need_resched;
}
從代碼分析來看,Schedule 主要完成了2個功能:
- pick_next_task 以優(yōu)先級從高到底依次檢查每個調(diào)度類,從最高優(yōu)先級的調(diào)度類中選擇最高優(yōu)先級的進程作為下一個應(yīng)執(zhí)行進程。
- 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; //獲取要執(zhí)行進程的mm字段
oldmm = prev->active_mm; //被切換出去進程的 active_mm 字段
//mm為空,說明是一個內(nèi)核線程
if (unlikely(!mm)) {
//內(nèi)核線程共享上一個運行進程的mm
next->active_mm = oldmm; //借用切換出去進程的 mm_struct
//增加引用計數(shù)
atomic_inc(&oldmm->mm_count);
//惰性TLB,因為內(nèi)核線程沒有虛擬地址空間的用戶空間部分,告訴底層體系結(jié)構(gòu)無須切換
enter_lazy_tlb(oldmm, next);
} else
//進程切換包括進程的執(zhí)行環(huán)境切換和運行空間的切換。運行空間的切換是有switch_mm完成的。若是用戶進程,則切換運行空間
switch_mm(oldmm, mm, next);
//若上一個運行進程是內(nèi)核線程
if (unlikely(!prev->mm)) {
prev->active_mm = NULL; //斷開內(nèi)核線程與之前借用的地址空間聯(lián)系
//更新運行隊列的prev_mm成員,為之后歸還借用的mm_struct做準(zhǔn)備
rq->prev_mm = oldmm;
}
/* Here we just switch the register state and the stack. */
//切換進程的執(zhí)行環(huán)境
switch_to(prev, next, prev);
barrier();
//進程切換之后的處理工作
finish_task_switch(this_rq(), prev);
}
進程上下文切換包括進程的地址空間的切換和執(zhí)行環(huán)境的切換。
- switch_mm 完成了進程的地址空間的切換:如果新進程有自己的用戶空間,也就是說,如果 next->mm 與 next->active_mm 相同,那么,switch_mm() 函數(shù)就把該進程從內(nèi)核空間切換到用戶空間,也就是加載next 的頁目錄。如果新進程無用戶空間(next->mm 為空),也就是說,如果它是一個內(nèi)核線程,那它就要在內(nèi)核空間運行,因此,需要借用前一個進程(prev)的地址空間,因為所有進程的內(nèi)核空間都是共享的,因此,這種借用是有效的。
- switch_to 完成了執(zhí)行環(huán)境的切換,該宏實現(xiàn)了進程之間的真正切換。
//進程切換包括進程的執(zhí)行環(huán)境切換和運行空間的切換。運行空間的切換是有switch_mm完成的
static inline void switch_mm(struct mm_struct *prev,
struct mm_struct *next,
struct task_struct tsk)
{
//得到當(dāng)前進程運行的cpu
int cpu = smp_processor_id();
//若要切換的prev != next,執(zhí)行切換過程
if (likely(prev != next)) {
/ stop flush ipis for the previous mm */
//清除prev的cpu_vm_mask,標(biāo)志prev已經(jīng)棄用了當(dāng)前cpu
cpu_clear(cpu, prev->cpu_vm_mask);
#ifdef CONFIG_SMP
//在smp系統(tǒng)中,更新cpu_tlbstate
per_cpu(cpu_tlbstate, cpu).state = TLBSTATE_OK;
per_cpu(cpu_tlbstate, cpu).active_mm = next;
#endif
//設(shè)置cpu_vm_mask,表示next占用的當(dāng)前的cpu
cpu_set(cpu, next->cpu_vm_mask);
/* Re-load page tables */
//加載CR3
load_cr3(next->pgd);
/*
? load the LDT, if the LDT is different:
*/
//若ldt不相同,還要加載ldt
if (unlikely(prev->context.ldt != next->context.ldt))
load_LDT_nolock(&next->context);
}
#ifdef CONFIG_SMP
else {
per_cpu(cpu_tlbstate, cpu).state = TLBSTATE_OK;
//prev == next 那當(dāng)前cpu中的active_mm就是prev,也即是next
BUG_ON(per_cpu(cpu_tlbstate, cpu).active_mm != next);
/*
在smp系統(tǒng)中,雖然mm時一樣的,但需要加載CR3
執(zhí)行cpu_test_and_set來判斷next是否正運行在此cpu上,這里是判斷在切換next是否運行
在當(dāng)前的cpu中,
假設(shè)cpu為1,一個進程在1上執(zhí)行時候,被調(diào)度出來,再次調(diào)度的時候,
又發(fā)生在cpu1上
*/
if (!cpu_test_and_set(cpu, next->cpu_vm_mask)) {
/* We were in lazy tlb mode and leave_mm disabled
? tlb flush IPI delivery. We must reload %cr3.
*/
load_cr3(next->pgd);
load_LDT_nolock(&next->context);
}
}
#endif
}
對于 switch_mm 處理,關(guān)鍵的一步就是它將新進程頁面目錄的起始物理地址裝入到寄存器 CR3 中。CR3 寄存器總是指向當(dāng)前進程的頁面目錄。
#define switch_to(prev,next,last) do { \
unsigned long esi,edi; \
/*分別保存了eflags、ebp、esp,flags和 ebp他們是被保存在prev進程(其實就是要被切換出去的進程)的內(nèi)核堆棧中的*/
asm volatile("pushfl\n\t" /* Save flags */ \
"pushl %%ebp\n\t" \
//%0為 prev->thread.esp, %1 為 prev->thread.eip
"movl %%esp,%0\n\t" /* save ESP */ \
//%5為 next->thread.esp,%6 為 next->thread.eip
"movl %5,%%esp\n\t" /* restore ESP */ \
/*將標(biāo)號為1所在的地址,也即是"popl %%ebp\n\t" 指令所在的地址保存在prev->thread.eip中,作為prev進程
下一次被調(diào)度運行而切入時的返回地址,因此可以知道,每個進程調(diào)離時都要執(zhí)行"movl $1f,%1\n\t",所以
這就決定了每個進程在受到調(diào)度恢復(fù)運行時都會從標(biāo)號1處也即是"popl %%ebp\n\t"開始執(zhí)行。
但是有一個例外,那就是新創(chuàng)建的進程,新創(chuàng)建的進程并沒有在上一次調(diào)離時執(zhí)行上面的指令,所以一來要將其
task_struct中的thread.eip事先設(shè)置好,二來所設(shè)置的返回地址也未必是這里的標(biāo)號1所在的地址,這取決于系統(tǒng)空間
堆棧的設(shè)置。事實上可以下fork()中可以看到,這個地址在copy_thread()中設(shè)置為ret_from_fork,其代碼在entry.S中,
也就是對于新創(chuàng)建的進程,在調(diào)用schedule_tail()后直接轉(zhuǎn)到了ret_from_sys_call, 也即是返回到了用戶空間去了
*/
"movl $1f,%1\n\t" /* save EIP */ \
/*將next->thread.eip壓入棧中,這里的next->thread.eip正是next進程上一次被調(diào)離在"movl $1f,%1\n\t"保存的,也即是
指向標(biāo)號為1的地方,也即是"popl %%ebp\n\t"指令所在的地址*/
"pushl %6\n\t" /* restore EIP */ \
/*需要注意的是 __switch_to 是經(jīng)過regparm(3)來修飾的,這個是gcc的一個擴展語法。
即從eax,ebx,ecx寄存器取函數(shù)的參數(shù)。這樣,__switch_to函數(shù)的參數(shù)就是從寄存器中取的。
并是不向普通函數(shù)那樣從堆棧中取的。在__switch_to之前,將next->thread.eip壓棧了,這樣從函數(shù)返回后
,它的下一條運行指令就是 next->thread.eip了。
對于新創(chuàng)建的進程。我們設(shè)置了p->thread.eip = ret_from_fork.這樣子進程被切換進來之后,就會通過
ret_from_fork返回到用戶空間了。
對于已經(jīng)運行的進程,我們這里可以看到,在進程被切換出去的時候,prev->thread.eip被設(shè)置了標(biāo)號1的地址,
即是從標(biāo)號1的地址開始運行的。
標(biāo)號1的操作:
恢復(fù)ebp(popl %%ebp)
恢復(fù)flags(popf1)
這樣就恢復(fù)了進程的執(zhí)行環(huán)境。
從代碼可以看到,在進程切換時,只保留了flags esp和ebp寄存器,顯示的用到了eax和edx,
那其他寄存器怎么保存的呢?
實際上過程切換只是發(fā)生在內(nèi)核態(tài),對于內(nèi)核態(tài)的寄存器來說,它的段寄存器都是一樣的,所以不需要保存
*/
/*通過jmp指令轉(zhuǎn)入到函數(shù)__switch_to中,由于上一行"pushl %6\n\t"把next->thread.eip,也即是標(biāo)號為1的地址壓到棧中,所以
跳入__switch_to執(zhí)行完后,執(zhí)行標(biāo)記為1的地方,也即是"popl %%ebp\n\t" 指令*/
"jmp __switch_to\n" \
"1:\t" \
"popl %%ebp\n\t" \
"popfl" \
:"=m" (prev->thread.esp),"=m" (prev->thread.eip), \
"=a" (last),"=S" (esi),"=D" (edi) \
:"m" (next->thread.esp),"m" (next->thread.eip), \
"2" (prev), "d" (next)); \
} while (0)
switch_to 把寄存器中的值比如esp等存放到進程thread結(jié)構(gòu)中,保存現(xiàn)場一邊后續(xù)恢復(fù),同時調(diào)用 __switch_to 完成了堆棧的切換。
在進程的 task_struct 結(jié)構(gòu)中有個重要的成分 thread,它本身是一個數(shù)據(jù)結(jié)構(gòu) thread_struct, 里面記錄著進程在切換時的(系統(tǒng)空間)堆棧指針,取指令地址(也就是“返回地址”)等關(guān)鍵性的信息。
/*
__switch_to 處理的主要邏輯是TSS,其核心就是load_esp0將TSS中的內(nèi)核空間(0級)堆棧指針換成next->esp0. 這是因為
cpu在穿越中斷門或者陷阱門時要根據(jù)新的運行級別從TSS中取得進程在系統(tǒng)空間的堆棧指針,其次,段寄存器fs和gs的內(nèi)容也做了
相應(yīng)的切換。同時cpu中為debug而設(shè)計的一些寄存器以及說明進程I/O 操作權(quán)限的位圖。
*/
struct task_struct fastcall * __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
struct thread_struct *prev = &prev_p->thread,
*next = &next_p->thread;
int cpu = smp_processor_id();
struct tss_struct *tss = &per_cpu(init_tss, cpu);
...
/* 將TSS 中的內(nèi)核級(0 級)堆棧指針換成next->esp0,這就是next 進程在內(nèi)核棧的指針*/
load_esp0(tss, next);
//為prev保存gs
savesegment(gs, prev->gs);
//從next的tls_array緩存中加載線程的Thread-Local Storage描述符
load_TLS(next, cpu);
/*若當(dāng)前特權(quán)級別是0且prev->iopl != next->iopl,則恢復(fù)IOPL設(shè)置set_iopl_mask
*/
if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
set_iopl_mask(next->iopl);
/*根據(jù)thread_info的TIF標(biāo)志_TIF_WORK_CTXSW_PREV和 _TIF_WORK_CTXSW_NEXT 判斷是否需要處理
debug寄存器和IO位圖*/
if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV ||
task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT))
__switch_to_xtra(prev_p, next_p, tss);
//設(shè)置cpu的lazy模式
arch_leave_lazy_cpu_mode();
//若fpu_counter > 5 則恢復(fù)next_p 的FPU寄存器
if (next_p->fpu_counter > 5)
math_state_restore();
//若需要,恢復(fù)gs寄存器
if (prev->gs | next->gs)
loadsegment(gs, next->gs);
x86_write_percpu(current_task, next_p);
return prev_p;
}
關(guān)于__switch_to 的工作就是處理 TSS (任務(wù)狀態(tài)段)。
TSS 全稱task state segment,是指在操作系統(tǒng)進程管理的過程中,任務(wù)(進程)切換時的任務(wù)現(xiàn)場信息。
linux 為每一個 CPU 提供一個 TSS 段,并且在 TR 寄存器中保存該段。
linux 中之所以為每一個 CPU 提供一個 TSS 段,而不是為每個進程提供一個TSS 段,主要原因是 TR 寄存器永遠指向它,在任務(wù)切換的適合不必切換 TR 寄存器,從而減小開銷。
在從用戶態(tài)切換到內(nèi)核態(tài)時,可以通過獲取 TSS 段中的 esp0 來獲取當(dāng)前進程的內(nèi)核棧 棧頂指針,從而可以保存用戶態(tài)的 cs,esp,eip 等上下文。
TSS 在任務(wù)切換過程中起著重要作用,通過它實現(xiàn)任務(wù)的掛起和恢復(fù)。所謂任務(wù)切換是指,掛起當(dāng)前正在執(zhí)行的任務(wù),恢復(fù)或啟動另一任務(wù)的執(zhí)行。
在任務(wù)切換過程中,首先,處理器中各寄存器的當(dāng)前值被自動保存到 TR(任務(wù)寄存器)所指定的任務(wù)的 TSS 中;然后,下一任務(wù)的 TSS 被裝入 TR;最后,從 TR 所指定的 TSS 中取出各寄存器的值送到處理器的各寄存器中。由此可見,通過在 TSS 中保存任務(wù)現(xiàn)場各寄存器狀態(tài)的完整映象,實現(xiàn)任務(wù)的切換。
因此,__switch_to 核心內(nèi)容就是將 TSS 中的內(nèi)核空間(0級)堆棧指針換成 next->esp0。這是因為 CPU 在穿越中斷門或者陷阱門時要根據(jù)新的運行級別從TSS中取得進程在系統(tǒng)空間的堆棧指針。
thread_struct.esp0 指向進程的系統(tǒng)空間堆棧的頂端。當(dāng)一個進程被調(diào)度運行時,內(nèi)核會將這個變量寫入 TSS 的 esp0 字段,表示這個進程進入0級運行時其堆棧的位置。換句話說,進程的 thread_struct 結(jié)構(gòu)中的 esp0 保存著其系統(tǒng)空間堆棧指針。當(dāng)進程穿過中斷門、陷阱門或者調(diào)用門進入系統(tǒng)空間時,處理器會從這里恢復(fù)期系統(tǒng)空間棧。

由于棧中變量的訪問依賴的是段、頁、和 esp、ebp 等這些寄存器,所以當(dāng)段、頁、寄存器切換完以后,棧中的變量就可以被訪問了。
因此 switch_to 完成了進程堆棧的切換,由于被切進的進程各個寄存器的信息已完成切換,因此 next 進程得以執(zhí)行指令運行。
由于 A 進程在調(diào)用 switch_to 完成了與 B 進程堆棧的切換,也即是寄存器中的值都是 B 的,所以 A 進程在 switch_to 執(zhí)行完后,A停止運行,B開始運行,當(dāng)過一段時間又把 A 進程切進去后,A 開始從 switch_to 后面的代碼開始執(zhí)行。
schedule 的調(diào)用流程如下:
