日期 | 內(nèi)核版本 | 架構(gòu) | 作者 | GitHub | CSDN |
---|---|---|---|---|---|
2016-06-14 | Linux-4.6 | X86 & arm | gatieme | LinuxDeviceDrivers | Linux進(jìn)程管理與調(diào)度 |
前面我們了解了linux進(jìn)程調(diào)度器的設(shè)計(jì)思路和注意框架
周期調(diào)度器scheduler_tick通過linux定時(shí)器周期性的被激活, 進(jìn)行程序調(diào)度
進(jìn)程主動(dòng)放棄CPU或者發(fā)生阻塞時(shí), 則會(huì)調(diào)用主調(diào)度器schedule進(jìn)行程序調(diào)度
在分析的過程中, 我們提到了內(nèi)核搶占和用戶搶占的概念, 但是并沒有詳細(xì)講, 因此我們在這里詳細(xì)分析一下子
CPU搶占分兩種情況, 用戶搶占, 內(nèi)核搶占
其中內(nèi)核搶占是在Linux2.5.4版本發(fā)布時(shí)加入, 同SMP(Symmetrical Multi-Processing, 對稱多處理器), 作為內(nèi)核的可選配置。
2個(gè)調(diào)度器
可以用兩種方法來激活調(diào)度
一種是直接的, 比如進(jìn)程打算睡眠或出于其他原因放棄CPU
另一種是通過周期性的機(jī)制, 以固定的頻率運(yùn)行, 不時(shí)的檢測是否有必要
因此當(dāng)前l(fā)inux的調(diào)度程序由兩個(gè)調(diào)度器組成:主調(diào)度器,周期性調(diào)度器(兩者又統(tǒng)稱為通用調(diào)度器(generic scheduler)或核心調(diào)度器(core scheduler))
并且每個(gè)調(diào)度器包括兩個(gè)內(nèi)容:調(diào)度框架(其實(shí)質(zhì)就是兩個(gè)函數(shù)框架)及調(diào)度器類
6種調(diào)度策略
linux內(nèi)核目前實(shí)現(xiàn)了6中調(diào)度策略(即調(diào)度算法), 用于對不同類型的進(jìn)程進(jìn)行調(diào)度, 或者支持某些特殊的功能
SCHED_NORMAL和SCHED_BATCH調(diào)度普通的非實(shí)時(shí)進(jìn)程
SCHED_FIFO和SCHED_RR和SCHED_DEADLINE則采用不同的調(diào)度策略調(diào)度實(shí)時(shí)進(jìn)程
SCHED_IDLE則在系統(tǒng)空閑時(shí)調(diào)用idle進(jìn)程.
5個(gè)調(diào)度器類
而依據(jù)其調(diào)度策略的不同實(shí)現(xiàn)了5個(gè)調(diào)度器類, 一個(gè)調(diào)度器類可以用一種種或者多種調(diào)度策略調(diào)度某一類進(jìn)程, 也可以用于特殊情況或者調(diào)度特殊功能的進(jìn)程.
其所屬進(jìn)程的優(yōu)先級(jí)順序?yàn)?/p>
stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class
3個(gè)調(diào)度實(shí)體
調(diào)度器不限于調(diào)度進(jìn)程, 還可以調(diào)度更大的實(shí)體, 比如實(shí)現(xiàn)組調(diào)度.
這種一般性要求調(diào)度器不直接操作進(jìn)程, 而是處理可調(diào)度實(shí)體, 因此需要一個(gè)通用的數(shù)據(jù)結(jié)構(gòu)描述這個(gè)調(diào)度實(shí)體,即seched_entity結(jié)構(gòu), 其實(shí)際上就代表了一個(gè)調(diào)度對象,可以為一個(gè)進(jìn)程,也可以為一個(gè)進(jìn)程組.
linux中針對當(dāng)前可調(diào)度的實(shí)時(shí)和非實(shí)時(shí)進(jìn)程, 定義了類型為seched_entity的3個(gè)調(diào)度實(shí)體
sched_dl_entity 采用EDF算法調(diào)度的實(shí)時(shí)調(diào)度實(shí)體
sched_rt_entity 采用Roound-Robin或者FIFO算法調(diào)度的實(shí)時(shí)調(diào)度實(shí)體
sched_entity 采用CFS算法調(diào)度的普通非實(shí)時(shí)進(jìn)程的調(diào)度實(shí)體
周期性調(diào)度器通過調(diào)用各個(gè)調(diào)度器類的task_tick函數(shù)完成周期性調(diào)度工作
如果當(dāng)前進(jìn)程是完全公平隊(duì)列中的進(jìn)程, 則首先根據(jù)當(dāng)前就緒隊(duì)列中的進(jìn)程數(shù)算出一個(gè)延遲時(shí)間間隔,大概每個(gè)進(jìn)程分配2ms時(shí)間,然后按照該進(jìn)程在隊(duì)列中的總權(quán)重中占得比例,算出它該執(zhí)行的時(shí)間X,如果該進(jìn)程執(zhí)行物理時(shí)間超過了X,則激發(fā)延遲調(diào)度;如果沒有超過X,但是紅黑樹就緒隊(duì)列中下一個(gè)進(jìn)程優(yōu)先級(jí)更高,即curr->vruntime-leftmost->vruntime > X,也將延遲調(diào)度
如果當(dāng)前進(jìn)程是實(shí)時(shí)調(diào)度類中的進(jìn)程:則如果該進(jìn)程是SCHED_RR,則遞減時(shí)間片[為HZ/10],到期,插入到隊(duì)列尾部,并激發(fā)延遲調(diào)度,如果是SCHED_FIFO,則什么也不做,直到該進(jìn)程執(zhí)行完成
延遲調(diào)度**的真正調(diào)度過程在:schedule中實(shí)現(xiàn),會(huì)按照調(diào)度類順序和優(yōu)先級(jí)挑選出一個(gè)最高優(yōu)先級(jí)的進(jìn)程執(zhí)行
而對于主調(diào)度器則直接關(guān)閉內(nèi)核搶占后, 通過調(diào)用schedule來完成進(jìn)程的調(diào)度
可見不管是周期性調(diào)度器還是主調(diào)度器, 內(nèi)核中的許多地方, 如果要將CPU分配給與當(dāng)前活動(dòng)進(jìn)程不同的另外一個(gè)進(jìn)程(即搶占),都會(huì)直接或者調(diào)用調(diào)度函數(shù), 包括schedule或者其子函數(shù)__schedule, 其中schedule在關(guān)閉內(nèi)核搶占后調(diào)用__schedule完成了搶占.
而__schedule則執(zhí)行了如下操作
__schedule如何完成內(nèi)核搶占
完成一些必要的檢查, 并設(shè)置進(jìn)程狀態(tài), 處理進(jìn)程所在的就緒隊(duì)列
調(diào)度全局的pick_next_task選擇搶占的進(jìn)程
如果當(dāng)前cpu上所有的進(jìn)程都是cfs調(diào)度的普通非實(shí)時(shí)進(jìn)程, 則直接用cfs調(diào)度, 如果無程序可調(diào)度則調(diào)度idle進(jìn)程
否則從優(yōu)先級(jí)最高的調(diào)度器類sched_class_highest(目前是stop_sched_class)開始依次遍歷所有調(diào)度器類的pick_next_task函數(shù), 選擇最優(yōu)的那個(gè)進(jìn)程執(zhí)行
context_switch完成進(jìn)程上下文切換
即進(jìn)程的搶占或者切換工作是由context_switch完成的
那么我們今天就詳細(xì)講解一下context_switch完成進(jìn)程上下文切換的原理
操作系統(tǒng)管理很多進(jìn)程的執(zhí)行. 有些進(jìn)程是來自各種程序、系統(tǒng)和應(yīng)用程序的單獨(dú)進(jìn)程,而某些進(jìn)程來自被分解為很多進(jìn)程的應(yīng)用或程序。當(dāng)一個(gè)進(jìn)程從內(nèi)核中移出,另一個(gè)進(jìn)程成為活動(dòng)的, 這些進(jìn)程之間便發(fā)生了上下文切換. 操作系統(tǒng)必須記錄重啟進(jìn)程和啟動(dòng)新進(jìn)程使之活動(dòng)所需要的所有信息. 這些信息被稱作上下文, 它描述了進(jìn)程的現(xiàn)有狀態(tài), 進(jìn)程上下文是可執(zhí)行程序代碼是進(jìn)程的重要組成部分, 實(shí)際上是進(jìn)程執(zhí)行活動(dòng)全過程的靜態(tài)描述, 可以看作是用戶進(jìn)程傳遞給內(nèi)核的這些參數(shù)以及內(nèi)核要保存的那一整套的變量和寄存器值和當(dāng)時(shí)的環(huán)境等
進(jìn)程的上下文信息包括, 指向可執(zhí)行文件的指針, 棧, 內(nèi)存(數(shù)據(jù)段和堆), 進(jìn)程狀態(tài), 優(yōu)先級(jí), 程序I/O的狀態(tài), 授予權(quán)限, 調(diào)度信息, 審計(jì)信息, 有關(guān)資源的信息(文件描述符和讀/寫指針), 關(guān)事件和信號(hào)的信息, 寄存器組(棧指針, 指令計(jì)數(shù)器)等等, 諸如此類.
處理器總處于以下三種狀態(tài)之一
1. 內(nèi)核態(tài),運(yùn)行于進(jìn)程上下文,內(nèi)核代表進(jìn)程運(yùn)行于內(nèi)核空間;
2. 內(nèi)核態(tài),運(yùn)行于中斷上下文,內(nèi)核代表硬件運(yùn)行于內(nèi)核空間;
3. 用戶態(tài),運(yùn)行于用戶空間。
用戶空間的應(yīng)用程序,通過系統(tǒng)調(diào)用,進(jìn)入內(nèi)核空間。這個(gè)時(shí)候用戶空間的進(jìn)程要傳遞 很多變量、參數(shù)的值給內(nèi)核,內(nèi)核態(tài)運(yùn)行的時(shí)候也要保存用戶進(jìn)程的一些寄存器值、變量等。所謂的”進(jìn)程上下文”
硬件通過觸發(fā)信號(hào),導(dǎo)致內(nèi)核調(diào)用中斷處理程序,進(jìn)入內(nèi)核空間。這個(gè)過程中,硬件的 一些變量和參數(shù)也要傳遞給內(nèi)核,內(nèi)核通過這些參數(shù)進(jìn)行中斷處理。所謂的”中斷上下文”,其實(shí)也可以看作就是硬件傳遞過來的這些參數(shù)和內(nèi)核需要保存的一些其他環(huán)境(主要是當(dāng)前被打斷執(zhí)行的進(jìn)程環(huán)境)。
LINUX完全注釋中的一段話
當(dāng)一個(gè)進(jìn)程在執(zhí)行時(shí),CPU的所有寄存器中的值、進(jìn)程的狀態(tài)以及堆棧中的內(nèi)容被稱 為該進(jìn)程的上下文。當(dāng)內(nèi)核需要切換到另一個(gè)進(jìn)程時(shí),它需要保存當(dāng)前進(jìn)程的 所有狀態(tài),即保存當(dāng)前進(jìn)程的上下文,以便在再次執(zhí)行該進(jìn)程時(shí),能夠必得到切換時(shí)的狀態(tài)執(zhí)行下去。在LINUX中,當(dāng)前進(jìn)程上下文均保存在進(jìn)程的任務(wù)數(shù)據(jù)結(jié) 構(gòu)中。在發(fā)生中斷時(shí),內(nèi)核就在被中斷進(jìn)程的上下文中,在內(nèi)核態(tài)下執(zhí)行中斷服務(wù)例程。但同時(shí)會(huì)保留所有需要用到的資源,以便中繼服務(wù)結(jié)束時(shí)能恢復(fù)被中斷進(jìn)程 的執(zhí)行.
進(jìn)程被搶占CPU時(shí)候, 操作系統(tǒng)保存其上下文信息, 同時(shí)將新的活動(dòng)進(jìn)程的上下文信息加載進(jìn)來, 這個(gè)過程其實(shí)就是上下文切換, 而當(dāng)一個(gè)被搶占的進(jìn)程再次成為活動(dòng)的, 它可以恢復(fù)自己的上下文繼續(xù)從被搶占的位置開始執(zhí)行. 參見維基百科-[context](https://en.wikipedia.org/wiki/Context_(computing), context switch
上下文切換(有時(shí)也稱做進(jìn)程切換或任務(wù)切換)是指CPU從一個(gè)進(jìn)程或線程切換到另一個(gè)進(jìn)程或線程
稍微詳細(xì)描述一下,上下文切換可以認(rèn)為是內(nèi)核(操作系統(tǒng)的核心)在 CPU 上對于進(jìn)程(包括線程)進(jìn)行以下的活動(dòng):
掛起一個(gè)進(jìn)程,將這個(gè)進(jìn)程在 CPU 中的狀態(tài)(上下文)存儲(chǔ)于內(nèi)存中的某處,
在內(nèi)存中檢索下一個(gè)進(jìn)程的上下文并將其在 CPU 的寄存器中恢復(fù)
跳轉(zhuǎn)到程序計(jì)數(shù)器所指向的位置(即跳轉(zhuǎn)到進(jìn)程被中斷時(shí)的代碼行),以恢復(fù)該進(jìn)程
因此上下文是指某一時(shí)間點(diǎn)CPU寄存器和程序計(jì)數(shù)器的內(nèi)容, 廣義上還包括內(nèi)存中進(jìn)程的虛擬地址映射信息.
上下文切換只能發(fā)生在內(nèi)核態(tài)中, 上下文切換通常是計(jì)算密集型的。也就是說,它需要相當(dāng)可觀的處理器時(shí)間,在每秒幾十上百次的切換中,每次切換都需要納秒量級(jí)的時(shí)間。所以,上下文切換對系統(tǒng)來說意味著消耗大量的 CPU 時(shí)間,事實(shí)上,可能是操作系統(tǒng)中時(shí)間消耗最大的操作。
Linux相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點(diǎn),其中有一項(xiàng)就是,其上下文切換和模式切換的時(shí)間消耗非常少.
linux中進(jìn)程調(diào)度時(shí), 內(nèi)核在選擇新進(jìn)程之后進(jìn)行搶占時(shí), 通過context_switch完成進(jìn)程上下文切換.
注意 進(jìn)程調(diào)度與搶占的區(qū)別
進(jìn)程調(diào)度不一定發(fā)生搶占, 但是搶占時(shí)卻一定發(fā)生了調(diào)度
在進(jìn)程發(fā)生調(diào)度時(shí), 只有當(dāng)前內(nèi)核發(fā)生當(dāng)前進(jìn)程因?yàn)橹鲃?dòng)或者被動(dòng)需要放棄CPU時(shí), 內(nèi)核才會(huì)選擇一個(gè)與當(dāng)前活動(dòng)進(jìn)程不同的進(jìn)程來搶占CPU
context_switch其實(shí)是一個(gè)分配器, 他會(huì)調(diào)用所需的特定體系結(jié)構(gòu)的方法
調(diào)用switch_mm(), 把虛擬內(nèi)存從一個(gè)進(jìn)程映射切換到新進(jìn)程中
switch_mm更換通過task_struct->mm描述的內(nèi)存管理上下文, 該工作的細(xì)節(jié)取決于處理器, 主要包括加載頁表, 刷出地址轉(zhuǎn)換后備緩沖器(部分或者全部), 向內(nèi)存管理單元(MMU)提供新的信息
調(diào)用switch_to(),從上一個(gè)進(jìn)程的處理器狀態(tài)切換到新進(jìn)程的處理器狀態(tài)。這包括保存、恢復(fù)棧信息和寄存器信息
switch_to切換處理器寄存器的呢內(nèi)容和內(nèi)核棧(虛擬地址空間的用戶部分已經(jīng)通過switch_mm變更, 其中也包括了用戶狀態(tài)下的棧, 因此switch_to不需要變更用戶棧, 只需變更內(nèi)核棧), 此段代碼嚴(yán)重依賴于體系結(jié)構(gòu), 且代碼通常都是用匯編語言編寫.
context_switch函數(shù)建立next進(jìn)程的地址空間。進(jìn)程描述符的active_mm字段指向進(jìn)程所使用的內(nèi)存描述符,而mm字段指向進(jìn)程所擁有的內(nèi)存描述符。對于一般的進(jìn)程,這兩個(gè)字段有相同的地址,但是,內(nèi)核線程沒有它自己的地址空間而且它的 mm字段總是被設(shè)置為 NULL
context_switch( )函數(shù)保證:如果next是一個(gè)內(nèi)核線程, 它使用prev所使用的地址空間
由于不同架構(gòu)下地址映射的機(jī)制有所區(qū)別, 而寄存器等信息弊病也是依賴于架構(gòu)的, 因此switch_mm和switch_to兩個(gè)函數(shù)均是體系結(jié)構(gòu)相關(guān)的
context_switch定義在kernel/sched/core.c#L2711, 如下所示
/* * context_switch - switch to the new MM and the new thread's register state. */static __always_inline struct rq *context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next){ struct mm_struct *mm, *oldmm; /* 完成進(jìn)程切換的準(zhǔn)備工作 */ 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); /* 如果next是內(nèi)核線程,則線程使用prev所使用的地址空間 * schedule( )函數(shù)把該線程設(shè)置為懶惰TLB模式 * 內(nèi)核線程并不擁有自己的頁表集(task_struct->mm = NULL) * 它使用一個(gè)普通進(jìn)程的頁表集 * 不過,沒有必要使一個(gè)用戶態(tài)線性地址對應(yīng)的TLB表項(xiàng)無效 * 因?yàn)閮?nèi)核線程不訪問用戶態(tài)地址空間。 */ if (!mm) /* 內(nèi)核線程無虛擬地址空間, mm = NULL*/ { /* 內(nèi)核線程的active_mm為上一個(gè)進(jìn)程的mm * 注意此時(shí)如果prev也是內(nèi)核線程, * 則oldmm為NULL, 即next->active_mm也為NULL */ next->active_mm = oldmm; /* 增加mm的引用計(jì)數(shù) */ atomic_inc(&oldmm->mm_count); /* 通知底層體系結(jié)構(gòu)不需要切換虛擬地址空間的用戶部分 * 這種加速上下文切換的技術(shù)稱為惰性TBL */ enter_lazy_tlb(oldmm, next); } else /* 不是內(nèi)核線程, 則需要切切換虛擬地址空間 */ switch_mm(oldmm, mm, next); /* 如果prev是內(nèi)核線程或正在退出的進(jìn)程 * 就重新設(shè)置prev->active_mm * 然后把指向prev內(nèi)存描述符的指針保存到運(yùn)行隊(duì)列的prev_mm字段中 */ if (!prev->mm) { /* 將prev的active_mm賦值和為空 */ prev->active_mm = NULL; /* 更新運(yùn)行隊(duì)列的prev_mm成員 */ rq->prev_mm = 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: */ lockdep_unpin_lock(&rq->lock); spin_release(&rq->lock.dep_map, 1, _THIS_IP_); /* Here we just switch the register state and the stack. * 切換進(jìn)程的執(zhí)行環(huán)境, 包括堆棧和寄存器 * 同時(shí)返回上一個(gè)執(zhí)行的程序 * 相當(dāng)于prev = witch_to(prev, next) */ switch_to(prev, next, prev); /* switch_to之后的代碼只有在 * 當(dāng)前進(jìn)程再次被選擇運(yùn)行(恢復(fù)執(zhí)行)時(shí)才會(huì)運(yùn)行 * 而此時(shí)當(dāng)前進(jìn)程恢復(fù)執(zhí)行時(shí)的上一個(gè)進(jìn)程可能跟參數(shù)傳入時(shí)的prev不同 * 甚至可能是系統(tǒng)中任意一個(gè)隨機(jī)的進(jìn)程 * 因此switch_to通過第三個(gè)參數(shù)將此進(jìn)程返回 */ /* 路障同步, 一般用編譯器指令實(shí)現(xiàn) * 確保了switch_to和finish_task_switch的執(zhí)行順序 * 不會(huì)因?yàn)槿魏慰赡艿膬?yōu)化而改變 */ barrier(); /* 進(jìn)程切換之后的處理工作 */ return finish_task_switch(prev);}````<div class="se-preview-section-delimiter"></div>##3.2 prepare_arch_switch切換前的準(zhǔn)備工作-------在進(jìn)程切換之前, 首先執(zhí)行調(diào)用每個(gè)體系結(jié)構(gòu)都必須定義的prepare_task_switch掛鉤, 這使得內(nèi)核執(zhí)行特定于體系結(jié)構(gòu)的代碼, 為切換做事先準(zhǔn)備. 大多數(shù)支持的體系結(jié)構(gòu)都不需要該選項(xiàng)<div class="se-preview-section-delimiter"></div>```cstruct mm_struct *mm, *oldmm;prepare_task_switch(rq, prev, next); /* 完成進(jìn)程切換的準(zhǔn)備工作 */
prepare_task_switch函數(shù)定義在kernel/sched/core.c, line 2558, 如下所示
/** * prepare_task_switch - prepare to switch tasks * @rq: the runqueue preparing to switch * @prev: the current task that is being switched out * @next: the task we are going to switch to. * * This is called with the rq lock held and interrupts off. It must * be paired with a subsequent finish_task_switch after the context * switch. * * prepare_task_switch sets up locking and calls architecture specific * hooks. */static inline voidprepare_task_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next){ sched_info_switch(rq, prev, next); perf_event_task_sched_out(prev, next); fire_sched_out_preempt_notifiers(prev, next); prepare_lock_switch(rq, next); prepare_arch_switch(next);}````<div class="se-preview-section-delimiter"></div>##3.3 next是內(nèi)核線程時(shí)的處理-------由于用戶空間進(jìn)程的寄存器內(nèi)容在進(jìn)入核心態(tài)時(shí)保存在內(nèi)核棧中, 在上下文切換期間無需顯式操作. 而因?yàn)槊總€(gè)進(jìn)程首先都是從核心態(tài)開始執(zhí)行(在調(diào)度期間控制權(quán)傳遞給新進(jìn)程), 在返回用戶空間時(shí), 會(huì)使用內(nèi)核棧上保存的值自動(dòng)恢復(fù)寄存器數(shù)據(jù).另外需要注意, 內(nèi)核線程沒有自身的用戶空間上下文, 其task_struct->mm為NULL, 參見[Linux內(nèi)核線程kernel thread詳解--Linux進(jìn)程的管理與調(diào)度(十)](http://blog.csdn.net/gatieme/article/details/51589205#t3), 從當(dāng)前進(jìn)程"借來"的地址空間記錄在active_mm中<div class="se-preview-section-delimiter"></div>```c/* 如果next是內(nèi)核線程,則線程使用prev所使用的地址空間 * schedule( )函數(shù)把該線程設(shè)置為懶惰TLB模式 * 內(nèi)核線程并不擁有自己的頁表集(task_struct->mm = NULL) * 它使用一個(gè)普通進(jìn)程的頁表集 * 不過,沒有必要使一個(gè)用戶態(tài)線性地址對應(yīng)的TLB表項(xiàng)無效 * 因?yàn)閮?nèi)核線程不訪問用戶態(tài)地址空間。 */if (!mm) /* 內(nèi)核線程無虛擬地址空間, mm = NULL*/{ /* 內(nèi)核線程的active_mm為上一個(gè)進(jìn)程的mm * 注意此時(shí)如果prev也是內(nèi)核線程, * 則oldmm為NULL, 即next->active_mm也為NULL */ next->active_mm = oldmm; /* 增加mm的引用計(jì)數(shù) */ atomic_inc(&oldmm->mm_count); /* 通知底層體系結(jié)構(gòu)不需要切換虛擬地址空間的用戶部分 * 這種加速上下文切換的技術(shù)稱為惰性TBL */ enter_lazy_tlb(oldmm, next);}else /* 不是內(nèi)核線程, 則需要切切換虛擬地址空間 */ switch_mm(oldmm, mm, next);````qizhongenter_lazy_tlb通知底層體系結(jié)構(gòu)不需要切換虛擬地址空間的用戶空間部分, 這種加速上下文切換的技術(shù)稱之為惰性TLB<div class="se-preview-section-delimiter"></div>##3.4 switch_mm切換進(jìn)程虛擬地址空間-------<div class="se-preview-section-delimiter"></div>###3.4.1 switch_mm函數(shù)-------switch_mm主要完成了進(jìn)程prev到next虛擬地址空間的映射, 由于內(nèi)核虛擬地址空間是不許呀切換的, 因此切換的主要是用戶態(tài)的虛擬地址空間這個(gè)是一個(gè)體系結(jié)構(gòu)相關(guān)的函數(shù), 其實(shí)現(xiàn)在對應(yīng)體系結(jié)構(gòu)下的[arch/對應(yīng)體系結(jié)構(gòu)/include/asm/mmu_context.h](http://lxr.free-electrons.com/ident?v=4.6;i=switch_mm)文件中, 我們下面列出了幾個(gè)常見體系結(jié)構(gòu)的實(shí)現(xiàn)| 體系結(jié)構(gòu) | switch_mm實(shí)現(xiàn) || ------- |:-------:|| x86 | [arch/x86/include/asm/mmu_context.h, line 118](http://lxr.free-electrons.com/source/arch/x86/include/asm/mmu_context.h?v=4.6#L118) || arm | [arch/arm/include/asm/mmu_context.h, line 126](http://lxr.free-electrons.com/source/arch/arm/include/asm/mmu_context.h?v=4.6#L126) || arm64 | [arch/arm64/include/asm/mmu_context.h, line 183](http://lxr.free-electrons.com/source/arch/arm64/include/asm/mmu_context.h?v=4.6#L183)其主要工作就是切換了進(jìn)程的CR3<div class="se-preview-section-delimiter"></div>###3.4.2 CPU-CR0~CR4寄存器-------控制寄存器(CR0~CR3)用于控制和確定處理器的操作模式以及當(dāng)前執(zhí)行任務(wù)的特性| 控制寄存器 | 描述 || ------- |:-------:|| CR0 | 含有控制處理器操作模式和狀態(tài)的系統(tǒng)控制標(biāo)志 || CR1 | 保留不用, 未定義的控制寄存器,供將來的處理器使用 || CR3 | 含有頁目錄表物理內(nèi)存基地址,因此該寄存器也被稱為頁目錄基地址寄存器PDBR(Page-Directory Base address Register), 保存頁目錄表的物理地址,頁目錄表總是放在以4K字節(jié)為單位的存儲(chǔ)器邊界上,因此,它的地址的低12位總為0,不起作用,即使寫上內(nèi)容,也不會(huì)被理會(huì) || CR4 | 在Pentium系列(包括486的后期版本)處理器中才實(shí)現(xiàn),它處理的事務(wù)包括諸如何時(shí)啟用虛擬8086模式等 |<div class="se-preview-section-delimiter"></div>### 3.4.3 保護(hù)模式下的GDT、LDT和IDT-------保護(hù)模式下三個(gè)重要的系統(tǒng)表——GDT、LDT和IDT這三個(gè)表是在內(nèi)存中由操作系統(tǒng)或系統(tǒng)程序員所建,并不是固化在哪里,所以從理論上是可以被讀寫的。這三個(gè)表都是描述符表. 描述符表是由若干個(gè)描述符組成, 每個(gè)描述符占用8個(gè)字節(jié)的內(nèi)存空間, 每個(gè)描述符表內(nèi)最多可以有8129個(gè)描述符. 描述符是描述一個(gè)段的大小,地址及各種狀態(tài)的。描述符表有三種,分別為**全局描述符表GDT**、**局部描述符表LDT**和**中斷描述符表IDT** | 描述符表 | 描述 || ------- |:-------:|| 全局描述符表GDT | 全局描述符表在系統(tǒng)中只能有一個(gè),且可以被每一個(gè)任務(wù)所共享.任何描述符都可以放在GDT中,但中斷門和陷阱門放在GDT中是不會(huì)起作用的. 能被多個(gè)任務(wù)共享的內(nèi)存區(qū)就是通過GDT完成的 || 局部描述符表LDT | 局部描述符表在系統(tǒng)中可以有多個(gè),通常情況下是與任務(wù)的數(shù)量保持對等,但任務(wù)可以沒有局部描述符表.<br><br>任務(wù)間不相干的部分也是通過LDT實(shí)現(xiàn)的.這里涉及到地址映射的問題.<br><br>和GDT一樣,中斷門和陷阱門放在LDT中是不會(huì)起作用的. || 中斷描述符表IDT | 和GDT一樣,中斷描述符表在系統(tǒng)最多只能有一個(gè),中斷描述符表內(nèi)可以存放256個(gè)描述符,分別對應(yīng)256個(gè)中斷.因?yàn)槊總€(gè)描述符占用8個(gè)字節(jié),所以IDT的長度可達(dá)2K.<br><br>中斷描述符表中可以有任務(wù)門、中斷門、陷阱門三個(gè)門描述符,其它的描述符在中斷描述符表中無意義 |**段選擇子**在保護(hù)模式下,段寄存器的內(nèi)容已不是段值,而稱其為選擇子.該選擇子指示描述符在上面這三個(gè)表中的位置,所以說選擇子即是索引值。當(dāng)我們把段選擇子裝入寄存器時(shí)不僅使該寄存器值,同時(shí)CPU將該選擇子所對應(yīng)的GDT或LDT中的描述符裝入了不可見部分。這樣只要我們不進(jìn)行代碼切換(不重新裝入新的選擇子)CPU就不會(huì)對不可見部分存儲(chǔ)的描述符進(jìn)行更新,可以直接進(jìn)行訪問,加快了訪問速度。一旦寄存器被重新賦值,不可見部分也將被重新賦值。**關(guān)于選擇子的值是否連續(xù)**關(guān)于選擇子的值,我認(rèn)為不一定要連續(xù)。但是每個(gè)描述符的起始地址相對于第一個(gè)描述符(即空描述符)的首地址的偏移必須是8的倍數(shù),即二進(jìn)制最后三位為0。這樣通過全局描述符表寄存器GDTR找到全局描述符表的首地址后,使用段選擇子的高13位索引到正確的描述符表項(xiàng)(段選擇子的高13位左移3位加上GDTR的值即為段選擇子指定的段描述符的邏輯首地址)也就是說在兩個(gè)段選擇符之間可以填充能被8整除個(gè)字節(jié)值。當(dāng)然,如果有選擇子指向了這些填充的字節(jié),一般會(huì)出錯(cuò),除非你有意填充一些恰當(dāng)?shù)臄?shù)值,呵呵。**關(guān)于為什么LDT要放在GDT中 -LDT中的描述符和GDT中的描述符**除了選擇子的bit3一個(gè)為0一個(gè)為1用于區(qū)分該描述符是在GDT中還是在LDT中外,描述符本身的結(jié)構(gòu)完全一樣。開始我考慮既然是這樣,為什么要將LDT放在GDT中而不是像GDT那樣找一個(gè)GDTR寄存器呢?后來終于明白了原因——很簡單,GDT表只有一個(gè),是固定的;而LDT表每個(gè)任務(wù)就可以有一個(gè),因此有多個(gè),并且由于任務(wù)的個(gè)數(shù)在不斷變化其數(shù)量也在不斷變化。如果只有一個(gè)LDTR寄存器顯然不能滿足多個(gè)LDT的要求。因此INTEL的做法是把它放在放在GDT中。<div class="se-preview-section-delimiter"></div>##3.5 prev是內(nèi)核線程時(shí)的處理-------如果前一個(gè)進(jìn)程prev四內(nèi)核線程(即prev->mm為NULL), 則其active_mm指針必須重置為NULL, 已斷開其于之前借用的地址空間的聯(lián)系, 而當(dāng)prev重新被調(diào)度的時(shí)候, 此時(shí)它成為next會(huì)在前面[next是內(nèi)核線程時(shí)的處理](未填寫網(wǎng)址)處重新用`next->active_mm = oldmm;`賦值, 這個(gè)我們剛講過<div class="se-preview-section-delimiter"></div>```c/* 如果prev是內(nèi)核線程或正在退出的進(jìn)程 * 就重新設(shè)置prev->active_mm * 然后把指向prev內(nèi)存描述符的指針保存到運(yùn)行隊(duì)列的prev_mm字段中 */if (!prev->mm){ /* 將prev的active_mm賦值和為空 */ prev->active_mm = NULL; /* 更新運(yùn)行隊(duì)列的prev_mm成員 */ rq->prev_mm = oldmm;}
下面我們提取了x86架構(gòu)下的switch_mm函數(shù), 其定義在arch/x86/include/asm/mmu_context.h, line 118
// http://lxr.free-electrons.com/source/arch/x86/include/asm/mmu_context.h?v=4.6#L118static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk){ unsigned cpu = smp_processor_id(); /* 確保prev和next不是同一進(jìn)程 */ if (likely(prev != next)) {#ifdef CONFIG_SMP /* 刷新cpu地址轉(zhuǎn)換后備緩沖器TLB */ this_cpu_write(cpu_tlbstate.state, TLBSTATE_OK); this_cpu_write(cpu_tlbstate.active_mm, next);#endif /* 設(shè)置當(dāng)前進(jìn)程的mm->cpu_vm_mask表示其占用cpu */ cpumask_set_cpu(cpu, mm_cpumask(next)); /* * Re-load page tables. * * This logic has an ordering constraint: * * CPU 0: Write to a PTE for 'next' * CPU 0: load bit 1 in mm_cpumask. if nonzero, send IPI. * CPU 1: set bit 1 in next's mm_cpumask * CPU 1: load from the PTE that CPU 0 writes (implicit) * * We need to prevent an outcome in which CPU 1 observes * the new PTE value and CPU 0 observes bit 1 clear in * mm_cpumask. (If that occurs, then the IPI will never * be sent, and CPU 0's TLB will contain a stale entry.) * * The bad outcome can occur if either CPU's load is * reordered before that CPU's store, so both CPUs must * execute full barriers to prevent this from happening. * * Thus, switch_mm needs a full barrier between the * store to mm_cpumask and any operation that could load * from next->pgd. TLB fills are special and can happen * due to instruction fetches or for no reason at all, * and neither LOCK nor MFENCE orders them. * Fortunately, load_cr3() is serializing and gives the * ordering guarantee we need. * * 將新進(jìn)程的pgd頁目錄表填寫到cpu的cr3寄存器中 */ load_cr3(next->pgd); trace_tlb_flush(TLB_FLUSH_ON_TASK_SWITCH, TLB_FLUSH_ALL); /* Stop flush ipis for the previous mm * 除prev的cpu_vm_mask,表示prev放棄使用cpu */ cpumask_clear_cpu(cpu, mm_cpumask(prev)); /* Load per-mm CR4 state */ load_mm_cr4(next);#ifdef CONFIG_MODIFY_LDT_SYSCALL /* * Load the LDT, if the LDT is different. * * It's possible that prev->context.ldt doesn't match * the LDT register. This can happen if leave_mm(prev) * was called and then modify_ldt changed * prev->context.ldt but suppressed an IPI to this CPU. * In this case, prev->context.ldt != NULL, because we * never set context.ldt to NULL while the mm still * exists. That means that next->context.ldt != * prev->context.ldt, because mms never share an LDT. * * */ if (unlikely(prev->context.ldt != next->context.ldt)) load_mm_ldt(next);#endif }#ifdef CONFIG_SMP else { this_cpu_write(cpu_tlbstate.state, TLBSTATE_OK); BUG_ON(this_cpu_read(cpu_tlbstate.active_mm) != next); if (!cpumask_test_cpu(cpu, mm_cpumask(next))) { /* * On established mms, the mm_cpumask is only changed * from irq context, from ptep_clear_flush() while in * lazy tlb mode, and here. Irqs are blocked during * schedule, protecting us from simultaneous changes. */ cpumask_set_cpu(cpu, mm_cpumask(next)); /* * We were in lazy tlb mode and leave_mm disabled * tlb flush IPI delivery. We must reload CR3 * to make sure to use no freed page tables. * * As above, load_cr3() is serializing and orders TLB * fills with respect to the mm_cpumask write. */ load_cr3(next->pgd); trace_tlb_flush(TLB_FLUSH_ON_TASK_SWITCH, TLB_FLUSH_ALL); load_mm_cr4(next); load_mm_ldt(next); } }#endif}
最后用switch_to完成了進(jìn)程的切換, 該函數(shù)切換了寄存器狀態(tài)和棧, 新進(jìn)程在該調(diào)用后開始執(zhí)行, 而switch_to之后的代碼只有在當(dāng)前進(jìn)程下一次被選擇運(yùn)行時(shí)才會(huì)執(zhí)行
執(zhí)行環(huán)境的切換是在switch_to()中完成的, switch_to完成最終的進(jìn)程切換,它保存原進(jìn)程的所有寄存器信息,恢復(fù)新進(jìn)程的所有寄存器信息,并執(zhí)行新的進(jìn)程
該函數(shù)往往通過宏來實(shí)現(xiàn), 其原型聲明如下
/* * Saving eflags is important. It switches not only IOPL between tasks, * it also protects other tasks from NT leaking through sysenter etc. */#define switch_to(prev, next, last)
體系結(jié)構(gòu) | switch_to實(shí)現(xiàn) |
---|---|
x86 | arch/x86/include/asm/switch_to.h中兩種實(shí)現(xiàn) 定義CONFIG_X86_32宏 未定義CONFIG_X86_32宏 |
arm | arch/arm/include/asm/switch_to.h, line 25 |
通用 | include/asm-generic/switch_to.h, line 25 |
內(nèi)核在switch_to中執(zhí)行如下操作
進(jìn)程切換, 即esp的切換, 由于從esp可以找到進(jìn)程的描述符
硬件上下文切換, 設(shè)置ip寄存器的值, 并jmp到__switch_to函數(shù)
堆棧的切換, 即ebp的切換, ebp是棧底指針, 它確定了當(dāng)前用戶空間屬于哪個(gè)進(jìn)程
__switch_to函數(shù)
體系結(jié)構(gòu) | __switch_to實(shí)現(xiàn) |
---|---|
x86 | arch/x86/kernel/process_32.c, line 242 |
x86_64 | arch/x86/kernel/process_64.c, line 277 |
arm64 | arch/arm64/kernel/process.c, line 329 |
調(diào)度過程可能選擇了一個(gè)新的進(jìn)程, 而清理工作則是針對此前的活動(dòng)進(jìn)程, 請注意, 這不是發(fā)起上下文切換的那個(gè)進(jìn)程, 而是系統(tǒng)中隨機(jī)的某個(gè)其他進(jìn)程, 內(nèi)核必須想辦法使得進(jìn)程能夠與context_switch例程通信, 這就可以通過switch_to宏實(shí)現(xiàn). 因此switch_to函數(shù)通過3個(gè)參數(shù)提供2個(gè)變量.
在新進(jìn)程被選中時(shí), 底層的進(jìn)程切換冽程必須將此前執(zhí)行的進(jìn)程提供給context_switch, 由于控制流會(huì)回到陔函數(shù)的中間, 這無法用普通的函數(shù)返回值來做到, 因此提供了3個(gè)參數(shù)的宏
我們考慮這個(gè)樣一個(gè)例子, 假定多個(gè)進(jìn)程A, B, C…在系統(tǒng)上運(yùn)行, 在某個(gè)時(shí)間點(diǎn), 內(nèi)核決定從進(jìn)程A切換到進(jìn)程B, 此時(shí)prev = A, next = B, 即執(zhí)行了switch_to(A, B), 而后當(dāng)被搶占的進(jìn)程A再次被選擇執(zhí)行的時(shí)候, 系統(tǒng)可能進(jìn)行了多次進(jìn)程切換/搶占(至少會(huì)經(jīng)歷一次即再次從B到A),假設(shè)A再次被選擇執(zhí)行時(shí)時(shí)當(dāng)前活動(dòng)進(jìn)程是C, 即此時(shí)prev = C. next = A.
在每個(gè)switch_to被調(diào)用的時(shí)候, prev和next指針位于各個(gè)進(jìn)程的內(nèi)核棧中, prev指向了當(dāng)前運(yùn)行的進(jìn)程, 而next指向了將要運(yùn)行的下一個(gè)進(jìn)程, 那么為了執(zhí)行從prev到next的切換, switcth_to使用前兩個(gè)參數(shù)prev和next就夠了.
在進(jìn)程A被選中再次執(zhí)行的時(shí)候, 會(huì)出現(xiàn)一個(gè)問題, 此時(shí)控制權(quán)即將回到A, switch_to函數(shù)返回, 內(nèi)核開始執(zhí)行switch_to之后的點(diǎn), 此時(shí)內(nèi)核棧準(zhǔn)確的恢復(fù)到切換之前的狀態(tài), 即進(jìn)程A上次被切換出去時(shí)的狀態(tài), prev = A, next = B. 此時(shí), 內(nèi)核無法知道實(shí)際上在進(jìn)程A之前運(yùn)行的是進(jìn)程C.
因此, 在新進(jìn)程被選中執(zhí)行時(shí), 內(nèi)核恢復(fù)到進(jìn)程被切換出去的點(diǎn)繼續(xù)執(zhí)行, 此時(shí)內(nèi)核只知道誰之前將新進(jìn)程搶占了, 但是卻不知道新進(jìn)程再次執(zhí)行是搶占了誰, 因此底層的進(jìn)程切換機(jī)制必須將此前執(zhí)行的進(jìn)程(即新進(jìn)程搶占的那個(gè)進(jìn)程)提供給context_switch. 由于控制流會(huì)回到函數(shù)的該中間, 因此無法通過普通函數(shù)的返回值來完成. 因此使用了一個(gè)3個(gè)參數(shù), 但是邏輯效果是相同的, 仿佛是switch_to是帶有兩個(gè)參數(shù)的函數(shù), 而且返回了一個(gè)指向此前運(yùn)行的進(jìn)程的指針.
switch_to(prev, next, last);
即
prev = last = switch_to(prev, next);
其中返回的prev值并不是做參數(shù)的prev值, 而是prev被再次調(diào)度的時(shí)候搶占掉的那個(gè)進(jìn)程last.
在上個(gè)例子中, 進(jìn)程A提供給switch_to的參數(shù)是prev = A, next = B, 然后控制權(quán)從A交給了B, 但是恢復(fù)執(zhí)行的時(shí)候是通過prev = C, next = A完成了再次調(diào)度, 而后內(nèi)核恢復(fù)了進(jìn)程A被切換之前的內(nèi)核棧信息, 即prev = A, next = B. 內(nèi)核為了通知調(diào)度機(jī)制A搶占了C的處理器, 就通過last參數(shù)傳遞回來, prev = last = C.
內(nèi)核實(shí)現(xiàn)該行為特性的方式依賴于底層的體系結(jié)構(gòu), 但內(nèi)核顯然可以通過考慮兩個(gè)進(jìn)程的內(nèi)核棧來重建所需要的信息
switch_mm()進(jìn)行用戶空間的切換, 更確切地說, 是切換地址轉(zhuǎn)換表(pgd), 由于pgd包括內(nèi)核虛擬地址空間和用戶虛擬地址空間地址映射, linux內(nèi)核把進(jìn)程的整個(gè)虛擬地址空間分成兩個(gè)部分, 一部分是內(nèi)核虛擬地址空間, 另外一部分是內(nèi)核虛擬地址空間, 各個(gè)進(jìn)程的虛擬地址空間各不相同, 但是卻共用了同樣的內(nèi)核地址空間, 這樣在進(jìn)程切換的時(shí)候, 就只需要切換虛擬地址空間的用戶空間部分.
每個(gè)進(jìn)程都有其自身的頁目錄表pgd
進(jìn)程本身尚未切換, 而存儲(chǔ)管理機(jī)制的頁目錄指針cr3卻已經(jīng)切換了,這樣不會(huì)造成問題嗎?不會(huì)的,因?yàn)檫@個(gè)時(shí)候CPU在系統(tǒng)空間運(yùn)行,而所有進(jìn)程的頁目錄表中與系統(tǒng)空間對應(yīng)的目錄項(xiàng)都指向相同的頁表,所以,不管切換到哪一個(gè)進(jìn)程的頁目錄表都一樣,受影響的只是用戶空間,系統(tǒng)空間的映射則永遠(yuǎn)不變
我們下面來分析一下子, x86_32位下的switch_to函數(shù), 其定義在arch/x86/include/asm/switch_to.h, line 27
先對flags寄存器和ebp壓入舊進(jìn)程內(nèi)核棧,并將確定舊進(jìn)程恢復(fù)執(zhí)行的下一跳地址,并將舊進(jìn)程ip,esp保存到task_struct->thread_info中,這樣舊進(jìn)程保存完畢;然后用新進(jìn)程的thread_info->esp恢復(fù)新進(jìn)程的內(nèi)核堆棧,用thread->info的ip恢復(fù)新進(jìn)程地址執(zhí)行。
關(guān)鍵點(diǎn):內(nèi)核寄存器[eflags、ebp保存到內(nèi)核棧;內(nèi)核棧esp地址、ip地址保存到thread_info中,task_struct在生命期中始終是全局的,所以肯定能根據(jù)該結(jié)構(gòu)恢復(fù)出其所有執(zhí)行場景來]
/* * Saving eflags is important. It switches not only IOPL between tasks, * it also protects other tasks from NT leaking through sysenter etc. */#define switch_to(prev, next, last) do { /* \ * Context-switching clobbers all registers, so we clobber \ * them explicitly, via unused output variables. \ * (EAX and EBP is not listed because EBP is saved/restored \ * explicitly for wchan access and EAX is the return value of \ * __switch_to()) \ */ unsigned long ebx, ecx, edx, esi, edi; asm volatile("pushfl\n\t" /* save flags 保存就的ebp、和flags寄存器到舊進(jìn)程的內(nèi)核棧中*/ "pushl %%ebp\n\t" /* save EBP */ "movl %%esp,%[prev_sp]\n\t" /* save ESP 將舊進(jìn)程esp保存到thread_info結(jié)構(gòu)中 */ "movl %[next_sp],%%esp\n\t" /* restore ESP 用新進(jìn)程esp填寫esp寄存器,此時(shí)內(nèi)核棧已切換 */ "movl $1f,%[prev_ip]\n\t" /* save EIP 將該進(jìn)程恢復(fù)執(zhí)行時(shí)的下條地址保存到舊進(jìn)程的thread中*/ "pushl %[next_ip]\n\t" /* restore EIP 將新進(jìn)程的ip值壓入到新進(jìn)程的內(nèi)核棧中 */ __switch_canary "jmp __switch_to\n" /* regparm call */ "1:\t" "popl %%ebp\n\t" /* restore EBP 該進(jìn)程執(zhí)行,恢復(fù)ebp寄存器*/ "popfl\n" /* restore flags 恢復(fù)flags寄存器*/ /* output parameters */ : [prev_sp] "=m" (prev->thread.sp), [prev_ip] "=m" (prev->thread.ip), "=a" (last), /* clobbered output registers: */ "=b" (ebx), "=c" (ecx), "=d" (edx), "=S" (esi), "=D" (edi) __switch_canary_oparam /* input parameters: */ : [next_sp] "m" (next->thread.sp), [next_ip] "m" (next->thread.ip), /* regparm parameters for __switch_to(): */ [prev] "a" (prev), [next] "d" (next) __switch_canary_iparam : /* reloaded segment registers */ "memory"); } while (0)
witch_to完成了進(jìn)程的切換, 新進(jìn)程在該調(diào)用后開始執(zhí)行, 而switch_to之后的代碼只有在當(dāng)前進(jìn)程下一次被選擇運(yùn)行時(shí)才會(huì)執(zhí)行.
/* switch_to之后的代碼只有在 * 當(dāng)前進(jìn)程再次被選擇運(yùn)行(恢復(fù)執(zhí)行)時(shí)才會(huì)運(yùn)行 * 而此時(shí)當(dāng)前進(jìn)程恢復(fù)執(zhí)行時(shí)的上一個(gè)進(jìn)程可能跟參數(shù)傳入時(shí)的prev不同 * 甚至可能是系統(tǒng)中任意一個(gè)隨機(jī)的進(jìn)程 * 因此switch_to通過第三個(gè)參數(shù)將此進(jìn)程返回 *//* 路障同步, 一般用編譯器指令實(shí)現(xiàn) * 確保了switch_to和finish_task_switch的執(zhí)行順序 * 不會(huì)因?yàn)槿魏慰赡艿膬?yōu)化而改變 */barrier();/* 進(jìn)程切換之后的處理工作 */return finish_task_switch(prev);
而為了程序編譯后指令的執(zhí)行順序不會(huì)因?yàn)榫幾g器的優(yōu)化而改變, 因此內(nèi)核提供了路障同步barrier來保證程序的執(zhí)行順序.
barrier往往通過編譯器指令來實(shí)現(xiàn), 內(nèi)核中多處都實(shí)現(xiàn)了barrier, 形式如下
// http://lxr.free-electrons.com/source/include/linux/compiler-gcc.h?v=4.6#L15/* Copied from linux/compiler-gcc.h since we can't include it directly * 采用內(nèi)斂匯編實(shí)現(xiàn) * __asm__用于指示編譯器在此插入?yún)R編語句 * __volatile__用于告訴編譯器,嚴(yán)禁將此處的匯編語句與其它的語句重組合優(yōu)化。 * 即:原原本本按原來的樣子處理這這里的匯編。 * memory強(qiáng)制gcc編譯器假設(shè)RAM所有內(nèi)存單元均被匯編指令修改,這樣cpu中的registers和cache中已緩存的內(nèi)存單元中的數(shù)據(jù)將作廢。cpu將不得不在需要的時(shí)候重新讀取內(nèi)存中的數(shù)據(jù)。這就阻止了cpu又將registers,cache中的數(shù)據(jù)用于去優(yōu)化指令,而避免去訪問內(nèi)存。 * "":::表示這是個(gè)空指令。barrier()不用在此插入一條串行化匯編指令。在后文將討論什么叫串行化指令。 */#define barrier() __asm__ __volatile__("": : :"memory")
關(guān)于內(nèi)存屏障的詳細(xì)信息, 可以參見 Linux內(nèi)核同步機(jī)制之(三):memory barrier
finish_task_switch完成一些清理工作, 使得能夠正確的釋放鎖, 但我們不會(huì)詳細(xì)討論這些. 他會(huì)向各個(gè)體系結(jié)構(gòu)提供了另一個(gè)掛鉤上下切換過程的可能性, 當(dāng)然這只在少數(shù)計(jì)算機(jī)上需要.
前面我們諒解switch_to函數(shù)的3個(gè)參數(shù)時(shí), 講到
注:A進(jìn)程切換到B, A被切換, 而當(dāng)A再次被選擇執(zhí)行, C再次切換到A,此時(shí)A執(zhí)行,但是系統(tǒng)為了告知調(diào)度器A再次執(zhí)行前的進(jìn)程是C, 通過switch_to的last參數(shù)返回的prev指向C,在A調(diào)度時(shí)候需要把調(diào)用A的進(jìn)程的信息清除掉
由于從C切換到A時(shí)候, A內(nèi)核棧中保存的實(shí)際上是A切換出時(shí)的狀態(tài)信息, 即prev=A, next=B,但是在A執(zhí)行時(shí), 其位于context_switch上下文中, 該函數(shù)的last參數(shù)返回的prev應(yīng)該是切換到A的進(jìn)程C, A負(fù)責(zé)對C進(jìn)程信息進(jìn)行切換后處理,比如,如果切換到A后,A發(fā)現(xiàn)C進(jìn)程已經(jīng)處于TASK_DEAD狀態(tài),則將釋放C進(jìn)程的TASK_STRUCT結(jié)構(gòu)
函數(shù)定義在kernel/sched/core.c, line 2715中, 如下所示
/** * finish_task_switch - clean up after a task-switch * @prev: the thread we just switched away from. * * finish_task_switch must be called after the context switch, paired * with a prepare_task_switch call before the context switch. * finish_task_switch will reconcile locking set up by prepare_task_switch, * and do any other architecture-specific cleanup actions. * * Note that we may have delayed dropping an mm in context_switch(). If * so, we finish that here outside of the runqueue lock. (Doing it * with the lock held can cause deadlocks; see schedule() for * details.) * * 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 we need to recalculate this_rq * because prev may have moved to another CPU. */static struct rq *finish_task_switch(struct task_struct *prev) __releases(rq->lock){ struct rq *rq = this_rq(); struct mm_struct *mm = rq->prev_mm; long prev_state; /* * The previous task will have left us with a preempt_count of 2 * because it left us after: * * schedule() * preempt_disable(); // 1 * __schedule() * raw_spin_lock_irq(&rq->lock) // 2 * * Also, see FORK_PREEMPT_COUNT. */ if (WARN_ONCE(preempt_count() != 2*PREEMPT_DISABLE_OFFSET, "corrupted preempt_count: %s/%d/0x%x\n", current->comm, current->pid, preempt_count())) preempt_count_set(FORK_PREEMPT_COUNT); 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. * * We must observe prev->state before clearing prev->on_cpu (in * finish_lock_switch), otherwise a concurrent wakeup can get prev * running on another CPU and we could rave with its RUNNING -> DEAD * transition, resulting in a double drop. */ prev_state = prev->state; vtime_task_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) mmdrop(mm); if (unlikely(prev_state == TASK_DEAD)) /* 如果上一個(gè)進(jìn)程已經(jīng)終止,釋放其task_struct 結(jié)構(gòu) */ { if (prev->sched_class->task_dead) prev->sched_class->task_dead(prev); /* * 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(); return rq;}
聯(lián)系客服