進(jìn)程調(diào)度相關(guān)內(nèi)核結(jié)構(gòu)
我們知道,進(jìn)程運(yùn)行需要各種各樣的系統(tǒng)資源,如內(nèi)存、文件、打印機(jī)和最寶貴的 CPU 等,所以說(shuō),調(diào)度的實(shí)質(zhì)就是資源的分配。系統(tǒng)通過(guò)不同的調(diào)度算法(Scheduling Algorithm)來(lái)實(shí)現(xiàn)這種資源的分配。通常來(lái)說(shuō),選擇什么樣的調(diào)度算法取決于資源分配的策略(Scheduling Policy)。
有關(guān)調(diào)度相關(guān)的結(jié)構(gòu)保存在 task_struct 中,如下:
struct task_struct {
/*
task_struct 采用了如下3個(gè)成員表示進(jìn)程的優(yōu)先級(jí),prio、normal_prio表示動(dòng)態(tài)優(yōu)先級(jí)
static_prio 為靜態(tài)優(yōu)先級(jí),靜態(tài)優(yōu)先級(jí)是進(jìn)程啟動(dòng)時(shí)分配的優(yōu)先級(jí),它可以使用nice和sched_setscheduler系統(tǒng)調(diào)用修改,
否則在進(jìn)程運(yùn)行期間會(huì)一直保持恒定
*/
int prio, static_prio, normal_prio;
/*
sched_clas表示該進(jìn)程所屬的調(diào)度器類
2.6.24版本中有3中調(diào)度類:
實(shí)時(shí)調(diào)度器 : rt_sched_class
完全公平調(diào)度器:fair_sched_class
空閑調(diào)度器: idle_sched_class
*/
const struct sched_class *sched_clas;
//調(diào)度實(shí)體
struct sched_entity se;
/*
policy 保存了對(duì)該進(jìn)程應(yīng)用的調(diào)度策略。
進(jìn)程的調(diào)度策略有6種
SCHED_NORMAL SCHED_FIFO SCHED_RR SCHED_BATCH SCHED_IDLE
普通進(jìn)程調(diào)度策略: SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE,這些都是通過(guò)完全公平調(diào)度器來(lái)處理的
實(shí)時(shí)進(jìn)程調(diào)度策略: SCHED_RR、SCHED_FIFO 這些都是通過(guò)實(shí)時(shí)調(diào)度器來(lái)處理的
*/
unsigned int policy;
/*
除了內(nèi)核線程(Kernel Thread),每個(gè)進(jìn)程都擁有自己的地址空間(也叫虛擬空間),用mm_struct 來(lái)描述。active_mm是為內(nèi)核線程而引入的。因?yàn)閮?nèi)核線程沒(méi)有自己的地址空間,
為了讓內(nèi)核線程與普通進(jìn)程具有統(tǒng)一的上下文切換方式,當(dāng)內(nèi)核線程進(jìn)行上下文切換時(shí),讓切換進(jìn)來(lái)的線程的active_mm
指向剛被調(diào)度出去的進(jìn)程的active_mm(如果進(jìn)程的mm 域不為空,則其active_mm 域與mm 域相同)
*/
struct mm_struct *mm, *active_mm;
//表示實(shí)時(shí)進(jìn)程的優(yōu)先級(jí),最低的優(yōu)先級(jí)為0,最高為99,值越大,優(yōu)先級(jí)越高
unsigned int rt_priority;
...
};
每個(gè)進(jìn)程都擁有自己的地址空間(也叫虛擬空間),用 mm_struct 來(lái)描述。
active_mm 是為內(nèi)核線程而引入的,因?yàn)閮?nèi)核線程沒(méi)有自己的地址空間,為了讓內(nèi)核線程與普通進(jìn)程具有統(tǒng)一的上下文切換方式,當(dāng)內(nèi)核線程進(jìn)行上下文切換時(shí),讓切換進(jìn)來(lái)的線程的 active_mm 指向剛被調(diào)度出去的進(jìn)程的 active_mm(如果進(jìn)程的mm 域不為空,則其 active_mm 域與 mm 域相同)。
在 linux 2.6 中 sched_class 表示該進(jìn)程所屬的調(diào)度器類有3種:
- 實(shí)時(shí)調(diào)度器 (RT,rt_sched_class) : 為每個(gè)優(yōu)先級(jí)維護(hù)一個(gè)隊(duì)列;
- 完全公平調(diào)度器 (CFS,fair_sched_class) : 采用完全公平調(diào)度算法,引入虛擬運(yùn)行時(shí)間的概念。
- 空閑調(diào)度器(idle_sched_class) : 為每個(gè) CPU 都會(huì)有一個(gè) idle 線程,當(dāng)沒(méi)有其他進(jìn)程可以調(diào)度時(shí),調(diào)度運(yùn)行idle線程。
進(jìn)程的調(diào)度策略有5種,用戶可以調(diào)用調(diào)度器里不同的調(diào)度策略:
- SCHED_FIFO :采用了先入先出,不使用時(shí)間片的調(diào)度算法。若處于可執(zhí)行狀態(tài),就會(huì)一直執(zhí)行,沒(méi)有更高優(yōu)先級(jí)的情況下,進(jìn)程只能等待其主動(dòng)讓出 cpu;
- SCHED_RR :時(shí)間片輪轉(zhuǎn),進(jìn)程用完時(shí)間片后加入優(yōu)先級(jí)對(duì)應(yīng)的運(yùn)行隊(duì)列的尾部,把 CPU 讓給同一優(yōu)先級(jí)的其他進(jìn)程;
- SCHED_NORMAL : 使 task 選擇 CFS 調(diào)度器來(lái)調(diào)度運(yùn)行;
- SCHED_BATCH:批量處理,使 task 選擇 CFS 調(diào)度器來(lái)調(diào)度運(yùn)行;
- SCHED_IDLE: 使 task 選擇 CFS 調(diào)度器來(lái)調(diào)度運(yùn)行
在每個(gè) CPU 中都有一個(gè)自身的運(yùn)行隊(duì)列 rq,每個(gè)活動(dòng)進(jìn)程只出現(xiàn)在一個(gè)運(yùn)行隊(duì)列中,在多個(gè) CPU 上同時(shí)運(yùn)行一個(gè)進(jìn)程是不可能的。
運(yùn)行隊(duì)列是使用如下結(jié)構(gòu)實(shí)現(xiàn)的:
struct rq {
//運(yùn)行隊(duì)列中進(jìn)程的數(shù)目
unsigned long nr_running;
//進(jìn)程切換總數(shù)
u64 nr_switches;
//用于完全公平調(diào)度器的就緒隊(duì)列
struct cfs_rq cfs;
//用于實(shí)時(shí)調(diào)度器的就緒隊(duì)列
struct rt_rq rt;
//記錄本cpu尚處于TASK_UNINTERRUPTIBLE狀態(tài)的進(jìn)程數(shù),和負(fù)載信息有關(guān)
unsigned long nr_uninterruptible;
//curr指向當(dāng)前運(yùn)行的進(jìn)程實(shí)例,idle 指向空閑進(jìn)程的實(shí)例
struct task_struct *curr, *idle;
//上一次調(diào)度時(shí)的mm結(jié)構(gòu)
struct mm_struct *prev_mm;
...
};
每個(gè)運(yùn)行隊(duì)列中有2個(gè)調(diào)度隊(duì)列:CFS 調(diào)度隊(duì)列 和 RT 調(diào)度隊(duì)列。
tast 作為調(diào)度實(shí)體加入到 CPU 中的調(diào)度隊(duì)列中。
系統(tǒng)中所有的運(yùn)行隊(duì)列都在 runqueues 數(shù)組中,該數(shù)組的每個(gè)元素分別對(duì)應(yīng)于系統(tǒng)中的一個(gè) CPU。在單處理器系統(tǒng)中,由于只需要一個(gè)就緒隊(duì)列,因此數(shù)組只有一個(gè)元素。
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)
進(jìn)程調(diào)度與切換
在分析調(diào)度流程之前,我們先來(lái)看在什么情況下要執(zhí)行調(diào)度程序,我們把這種情況叫做調(diào)度時(shí)機(jī)。
Linux 調(diào)度時(shí)機(jī)主要有。
- 進(jìn)程狀態(tài)轉(zhuǎn)換的時(shí)刻:進(jìn)程終止、進(jìn)程睡眠;
- 當(dāng)前進(jìn)程的時(shí)間片用完時(shí)(current->counter=0);
- 設(shè)備驅(qū)動(dòng)程序;
- 進(jìn)程從中斷、異常及系統(tǒng)調(diào)用返回到用戶態(tài)時(shí)。
時(shí)機(jī)1,進(jìn)程要調(diào)用 sleep() 或 exit() 等函數(shù)進(jìn)行狀態(tài)轉(zhuǎn)換,這些函數(shù)會(huì)主動(dòng)調(diào)用調(diào)度程序進(jìn)行進(jìn)程調(diào)度。
時(shí)機(jī)2,由于進(jìn)程的時(shí)間片是由時(shí)鐘中斷來(lái)更新的,因此,這種情況和時(shí)機(jī)4 是一樣的。
時(shí)機(jī)3,當(dāng)設(shè)備驅(qū)動(dòng)程序執(zhí)行長(zhǎng)而重復(fù)的任務(wù)時(shí),直接調(diào)用調(diào)度程序。在每次反復(fù)循環(huán)中,驅(qū)動(dòng)程序都檢查 need_resched 的值,如果必要,則調(diào)用調(diào)度程序 schedule() 主動(dòng)放棄 CPU。
時(shí)機(jī)4 , 如前所述, 不管是從中斷、異常還是系統(tǒng)調(diào)用返回, 最終都調(diào)用 ret_from_sys_call(),由這個(gè)函數(shù)進(jìn)行調(diào)度標(biāo)志的檢測(cè),如果必要,則調(diào)用調(diào)用調(diào)度程序。那么,為什么從系統(tǒng)調(diào)用返回時(shí)要調(diào)用調(diào)度程序呢?這當(dāng)然是從效率考慮。從系統(tǒng)調(diào)用返回意味著要離開內(nèi)核態(tài)而返回到用戶態(tài),而狀態(tài)的轉(zhuǎn)換要花費(fèi)一定的時(shí)間,因此,在返回到用戶態(tài)前,系統(tǒng)把在內(nèi)核態(tài)該處理的事全部做完。
Linux 的調(diào)度程序是一個(gè)叫 Schedule() 的函數(shù),這個(gè)函數(shù)來(lái)決定是否要進(jìn)行進(jìn)程的切換,如果要切換的話,切換到哪個(gè)進(jìn)程等。
Schedule 的實(shí)現(xiàn)
asmlinkage void __sched schedule(void)
{
/*prev 表示調(diào)度之前的進(jìn)程, next 表示調(diào)度之后的進(jìn)程 */
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對(duì)應(yīng)的運(yùn)行隊(duì)列
rcu_qsctr_inc(cpu);
prev = rq->curr; /*讓prev 成為當(dāng)前進(jìn)程 */
switch_count = &prev->nivcsw;
/釋放全局內(nèi)核鎖,并開this_cpu 的中斷/
release_kernel_lock(prev);
need_resched_nonpreemptible:
__update_rq_clock(rq); //更新運(yùn)行隊(duì)列的時(shí)鐘值
...
if (unlikely(!rq->nr_running))
idle_balance(cpu, rq);
// 對(duì)應(yīng)到CFS,則為 put_prev_task_fair
prev->sched_class->put_prev_task(rq, prev); //通知調(diào)度器類當(dāng)前運(yùn)行進(jìn)程要被另一個(gè)進(jìn)程取代
/pick_next_task以優(yōu)先級(jí)從高到底依次檢查每個(gè)調(diào)度類,從最高優(yōu)先級(jí)的調(diào)度類中選擇最高優(yōu)先級(jí)的進(jìn)程作為
下一個(gè)應(yīng)執(zhí)行進(jìn)程(若其余都睡眠,則只有當(dāng)前進(jìn)程可運(yùn)行,就跳過(guò)下面了)/
next = pick_next_task(rq, prev); //選擇需要進(jìn)行切換的task
//進(jìn)程prev和進(jìn)程next切換前更新各自的sched_info
sched_info_switch(prev, next);
if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
++switch_count;
//完成進(jìn)程切換(上下文切換)
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;
}
從代碼分析來(lái)看,Schedule 主要完成了2個(gè)功能:
- pick_next_task 以優(yōu)先級(jí)從高到底依次檢查每個(gè)調(diào)度類,從最高優(yōu)先級(jí)的調(diào)度類中選擇最高優(yōu)先級(jí)的進(jìn)程作為下一個(gè)應(yīng)執(zhí)行進(jìn)程。
- context_switch 完成進(jìn)程的上下文切換。
進(jìn)程上下文切換
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í)行進(jìn)程的mm字段
oldmm = prev->active_mm; //被切換出去進(jìn)程的 active_mm 字段
//mm為空,說(shuō)明是一個(gè)內(nèi)核線程
if (unlikely(!mm)) {
//內(nèi)核線程共享上一個(gè)運(yùn)行進(jìn)程的mm
next->active_mm = oldmm; //借用切換出去進(jìn)程的 mm_struct
//增加引用計(jì)數(shù)
atomic_inc(&oldmm->mm_count);
//惰性TLB,因?yàn)閮?nèi)核線程沒(méi)有虛擬地址空間的用戶空間部分,告訴底層體系結(jié)構(gòu)無(wú)須切換
enter_lazy_tlb(oldmm, next);
} else
//進(jìn)程切換包括進(jìn)程的執(zhí)行環(huán)境切換和運(yùn)行空間的切換。運(yùn)行空間的切換是有switch_mm完成的。若是用戶進(jìn)程,則切換運(yùn)行空間
switch_mm(oldmm, mm, next);
//若上一個(gè)運(yùn)行進(jìn)程是內(nèi)核線程
if (unlikely(!prev->mm)) {
prev->active_mm = NULL; //斷開內(nèi)核線程與之前借用的地址空間聯(lián)系
//更新運(yùn)行隊(duì)列的prev_mm成員,為之后歸還借用的mm_struct做準(zhǔn)備
rq->prev_mm = oldmm;
}
/* Here we just switch the register state and the stack. */
//切換進(jìn)程的執(zhí)行環(huán)境
switch_to(prev, next, prev);
barrier();
//進(jìn)程切換之后的處理工作
finish_task_switch(this_rq(), prev);
}
進(jìn)程上下文切換包括進(jìn)程的地址空間的切換和執(zhí)行環(huán)境的切換。
- switch_mm 完成了進(jìn)程的地址空間的切換:如果新進(jìn)程有自己的用戶空間,也就是說(shuō),如果 next->mm 與 next->active_mm 相同,那么,switch_mm() 函數(shù)就把該進(jìn)程從內(nèi)核空間切換到用戶空間,也就是加載next 的頁(yè)目錄。如果新進(jìn)程無(wú)用戶空間(next->mm 為空),也就是說(shuō),如果它是一個(gè)內(nèi)核線程,那它就要在內(nèi)核空間運(yùn)行,因此,需要借用前一個(gè)進(jìn)程(prev)的地址空間,因?yàn)樗羞M(jìn)程的內(nèi)核空間都是共享的,因此,這種借用是有效的。
- switch_to 完成了執(zhí)行環(huán)境的切換,該宏實(shí)現(xiàn)了進(jìn)程之間的真正切換。
//進(jìn)程切換包括進(jìn)程的執(zhí)行環(huán)境切換和運(yùn)行空間的切換。運(yùn)行空間的切換是有switch_mm完成的
static inline void switch_mm(struct mm_struct *prev,
struct mm_struct *next,
struct task_struct tsk)
{
//得到當(dāng)前進(jìn)程運(yùn)行的cpu
int cpu = smp_processor_id();
//若要切換的prev != next,執(zhí)行切換過(guò)程
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時(shí)一樣的,但需要加載CR3
執(zhí)行cpu_test_and_set來(lái)判斷next是否正運(yùn)行在此cpu上,這里是判斷在切換next是否運(yùn)行
在當(dāng)前的cpu中,
假設(shè)cpu為1,一個(gè)進(jìn)程在1上執(zhí)行時(shí)候,被調(diào)度出來(lái),再次調(diào)度的時(shí)候,
又發(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
}
對(duì)于 switch_mm 處理,關(guān)鍵的一步就是它將新進(jìn)程頁(yè)面目錄的起始物理地址裝入到寄存器 CR3 中。CR3 寄存器總是指向當(dāng)前進(jìn)程的頁(yè)面目錄。
#define switch_to(prev,next,last) do {
unsigned long esi,edi;
/*分別保存了eflags、ebp、esp,flags和 ebp他們是被保存在prev進(jìn)程(其實(shí)就是要被切換出去的進(jìn)程)的內(nèi)核堆棧中的*/
asm volatile("pushflnt" /* Save flags */
"pushl %%ebpnt"
//%0為 prev->thread.esp, %1 為 prev->thread.eip
"movl %%esp,%0nt" /* save ESP */
//%5為 next->thread.esp,%6 為 next->thread.eip
"movl %5,%%espnt" /* restore ESP */
/*將標(biāo)號(hào)為1所在的地址,也即是"popl %%ebpnt" 指令所在的地址保存在prev->thread.eip中,作為prev進(jìn)程
下一次被調(diào)度運(yùn)行而切入時(shí)的返回地址,因此可以知道,每個(gè)進(jìn)程調(diào)離時(shí)都要執(zhí)行"movl $1f,%1nt",所以
這就決定了每個(gè)進(jìn)程在受到調(diào)度恢復(fù)運(yùn)行時(shí)都會(huì)從標(biāo)號(hào)1處也即是"popl %%ebpnt"開始執(zhí)行。
但是有一個(gè)例外,那就是新創(chuàng)建的進(jìn)程,新創(chuàng)建的進(jìn)程并沒(méi)有在上一次調(diào)離時(shí)執(zhí)行上面的指令,所以一來(lái)要將其
task_struct中的thread.eip事先設(shè)置好,二來(lái)所設(shè)置的返回地址也未必是這里的標(biāo)號(hào)1所在的地址,這取決于系統(tǒng)空間
堆棧的設(shè)置。事實(shí)上可以下fork()中可以看到,這個(gè)地址在copy_thread()中設(shè)置為ret_from_fork,其代碼在entry.S中,
也就是對(duì)于新創(chuàng)建的進(jìn)程,在調(diào)用schedule_tail()后直接轉(zhuǎn)到了ret_from_sys_call, 也即是返回到了用戶空間去了
*/
"movl $1f,%1nt" /* save EIP */
/*將next->thread.eip壓入棧中,這里的next->thread.eip正是next進(jìn)程上一次被調(diào)離在"movl $1f,%1nt"保存的,也即是
指向標(biāo)號(hào)為1的地方,也即是"popl %%ebpnt"指令所在的地址*/
"pushl %6nt" /* restore EIP */
/*需要注意的是 __switch_to 是經(jīng)過(guò)regparm(3)來(lái)修飾的,這個(gè)是gcc的一個(gè)擴(kuò)展語(yǔ)法。
即從eax,ebx,ecx寄存器取函數(shù)的參數(shù)。這樣,__switch_to函數(shù)的參數(shù)就是從寄存器中取的。
并是不向普通函數(shù)那樣從堆棧中取的。在__switch_to之前,將next->thread.eip壓棧了,這樣從函數(shù)返回后
,它的下一條運(yùn)行指令就是 next->thread.eip了。
對(duì)于新創(chuàng)建的進(jìn)程。我們?cè)O(shè)置了p->thread.eip = ret_from_fork.這樣子進(jìn)程被切換進(jìn)來(lái)之后,就會(huì)通過(guò)
ret_from_fork返回到用戶空間了。
對(duì)于已經(jīng)運(yùn)行的進(jìn)程,我們這里可以看到,在進(jìn)程被切換出去的時(shí)候,prev->thread.eip被設(shè)置了標(biāo)號(hào)1的地址,
即是從標(biāo)號(hào)1的地址開始運(yùn)行的。
標(biāo)號(hào)1的操作:
恢復(fù)ebp(popl %%ebp)
恢復(fù)flags(popf1)
這樣就恢復(fù)了進(jìn)程的執(zhí)行環(huán)境。
從代碼可以看到,在進(jìn)程切換時(shí),只保留了flags esp和ebp寄存器,顯示的用到了eax和edx,
那其他寄存器怎么保存的呢?
實(shí)際上過(guò)程切換只是發(fā)生在內(nèi)核態(tài),對(duì)于內(nèi)核態(tài)的寄存器來(lái)說(shuō),它的段寄存器都是一樣的,所以不需要保存
*/
/*通過(guò)jmp指令轉(zhuǎn)入到函數(shù)__switch_to中,由于上一行"pushl %6nt"把next->thread.eip,也即是標(biāo)號(hào)為1的地址壓到棧中,所以
跳入__switch_to執(zhí)行完后,執(zhí)行標(biāo)記為1的地方,也即是"popl %%ebpnt" 指令*/
"jmp __switch_ton"
"1:t"
"popl %%ebpnt"
"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等存放到進(jìn)程thread結(jié)構(gòu)中,保存現(xiàn)場(chǎng)一邊后續(xù)恢復(fù),同時(shí)調(diào)用 __switch_to 完成了堆棧的切換。
在進(jìn)程的 task_struct 結(jié)構(gòu)中有個(gè)重要的成分 thread,它本身是一個(gè)數(shù)據(jù)結(jié)構(gòu) thread_struct, 里面記錄著進(jìn)程在切換時(shí)的(系統(tǒng)空間)堆棧指針,取指令地址(也就是“返回地址”)等關(guān)鍵性的信息。
/*
__switch_to 處理的主要邏輯是TSS,其核心就是load_esp0將TSS中的內(nèi)核空間(0級(jí))堆棧指針換成next->esp0. 這是因?yàn)?cpu在穿越中斷門或者陷阱門時(shí)要根據(jù)新的運(yùn)行級(jí)別從TSS中取得進(jìn)程在系統(tǒng)空間的堆棧指針,其次,段寄存器fs和gs的內(nèi)容也做了
相應(yīng)的切換。同時(shí)cpu中為debug而設(shè)計(jì)的一些寄存器以及說(shuō)明進(jìn)程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)核級(jí)(0 級(jí))堆棧指針換成next->esp0,這就是next 進(jìn)程在內(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)級(jí)別是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)進(jìn)程管理的過(guò)程中,任務(wù)(進(jìn)程)切換時(shí)的任務(wù)現(xiàn)場(chǎng)信息。
linux 為每一個(gè) CPU 提供一個(gè) TSS 段,并且在 TR 寄存器中保存該段。
linux 中之所以為每一個(gè) CPU 提供一個(gè) TSS 段,而不是為每個(gè)進(jìn)程提供一個(gè)TSS 段,主要原因是 TR 寄存器永遠(yuǎn)指向它,在任務(wù)切換的適合不必切換 TR 寄存器,從而減小開銷。
在從用戶態(tài)切換到內(nèi)核態(tài)時(shí),可以通過(guò)獲取 TSS 段中的 esp0 來(lái)獲取當(dāng)前進(jìn)程的內(nèi)核棧 棧頂指針,從而可以保存用戶態(tài)的 cs,esp,eip 等上下文。
TSS 在任務(wù)切換過(guò)程中起著重要作用,通過(guò)它實(shí)現(xiàn)任務(wù)的掛起和恢復(fù)。所謂任務(wù)切換是指,掛起當(dāng)前正在執(zhí)行的任務(wù),恢復(fù)或啟動(dòng)另一任務(wù)的執(zhí)行。
在任務(wù)切換過(guò)程中,首先,處理器中各寄存器的當(dāng)前值被自動(dòng)保存到 TR(任務(wù)寄存器)所指定的任務(wù)的 TSS 中;然后,下一任務(wù)的 TSS 被裝入 TR;最后,從 TR 所指定的 TSS 中取出各寄存器的值送到處理器的各寄存器中。由此可見,通過(guò)在 TSS 中保存任務(wù)現(xiàn)場(chǎng)各寄存器狀態(tài)的完整映象,實(shí)現(xiàn)任務(wù)的切換。
因此,__switch_to 核心內(nèi)容就是將 TSS 中的內(nèi)核空間(0級(jí))堆棧指針換成 next->esp0。這是因?yàn)?CPU 在穿越中斷門或者陷阱門時(shí)要根據(jù)新的運(yùn)行級(jí)別從TSS中取得進(jìn)程在系統(tǒng)空間的堆棧指針。
thread_struct.esp0 指向進(jìn)程的系統(tǒng)空間堆棧的頂端。當(dāng)一個(gè)進(jìn)程被調(diào)度運(yùn)行時(shí),內(nèi)核會(huì)將這個(gè)變量寫入 TSS 的 esp0 字段,表示這個(gè)進(jìn)程進(jìn)入0級(jí)運(yùn)行時(shí)其堆棧的位置。換句話說(shuō),進(jìn)程的 thread_struct 結(jié)構(gòu)中的 esp0 保存著其系統(tǒng)空間堆棧指針。當(dāng)進(jìn)程穿過(guò)中斷門、陷阱門或者調(diào)用門進(jìn)入系統(tǒng)空間時(shí),處理器會(huì)從這里恢復(fù)期系統(tǒng)空間棧。
由于棧中變量的訪問(wèn)依賴的是段、頁(yè)、和 esp、ebp 等這些寄存器,所以當(dāng)段、頁(yè)、寄存器切換完以后,棧中的變量就可以被訪問(wèn)了。
因此 switch_to 完成了進(jìn)程堆棧的切換,由于被切進(jìn)的進(jìn)程各個(gè)寄存器的信息已完成切換,因此 next 進(jìn)程得以執(zhí)行指令運(yùn)行。
由于 A 進(jìn)程在調(diào)用 switch_to 完成了與 B 進(jìn)程堆棧的切換,也即是寄存器中的值都是 B 的,所以 A 進(jìn)程在 switch_to 執(zhí)行完后,A停止運(yùn)行,B開始運(yùn)行,當(dāng)過(guò)一段時(shí)間又把 A 進(jìn)程切進(jìn)去后,A 開始從 switch_to 后面的代碼開始執(zhí)行。
schedule 的調(diào)用流程如下: