Linux程序切換以及核心執行緒的返回值

2021-06-20 08:20:05 字數 4410 閱讀 1268

inux中的程序是個最基本的概念,程序從執行佇列到開始執行有兩個開始的地方,乙個就是switch_to巨集中的標號1:"1:/t",另 乙個就是ret_form_fork,只要不是新建立的程序,幾乎都是從上面的那個標號1開始的,而switch_to巨集則是除了核心本身,所有的程序要 想執行都要經過的地方,這樣看來,雖然linux的程序體系以及程序排程非常複雜,但是總體看來就是乙個沙漏狀,而switch_to巨集就是沙漏中間那個 最細的地方,想從一端到另一端,必然要經過那個地方,在非新建立的程序的情況下,所有程序都是從標號1開始,讓我們先看一下這是怎麼回事:

#define switch_to(prev,next,last) do while (0)

linux 之所以實現上述的單點切換就是為了降低複雜度,其實很多作業系統核心都是這麼做的,這裡的單點並不是指switch_to這個單點,而是儲存/恢復eip 這個暫存器從而保證所有切換回來的程序都從乙個地方開始,但是有點美中不足的就是linux並沒有將所有的程序從就緒到開始執行都從標號1開始,看看 do_fork的實現就知道,其實新建立的程序是不這麼做的,新建立的程序的eip是ret_from_fork而不是標號1,這個原因是什麼?新建立進 程的時候要手工指定乙個開始的位址,畢竟它要開始就要有個起點,那麼起點在**好呢(千萬不要和regs.eip相混淆,那個是正常執行時的eip,屬於 程序的,建立程序是乙個系統呼叫,系統呼叫的話就是請求系統核心幫忙做事,然而做事之前要儲存自己的當前狀態,regs.eip就是屬於這個狀態的,而這 裡的起點是作業系統核心管理程序用的,和程序或者核心執行緒沒有關係的)?最好是模擬該程序和別的已有程序一樣是重新開始執行的,這樣比較統一又便於管理, 然後將這個開始位址也指為標號1,但是這時標號1在**,是標號1在嵌入式彙編巨集中導致標號1的位址不好取到嗎,如果真的因為這的話,完全可以將標號1分 離出來放到乙個地方,然後不管是已經有的程序還是新建立的程序都從這個固定的分離出來標號1的位址處取指令不就可以了嗎?核心的設計者不可能還沒有我聰 明,那樣的話會浪費取指令的時間和空間的,來個間接引用肯定沒有嵌入式彙編標號直接,而且還有乙個原因,用ret_from_fork完全可以做到和既有 程序的標號1一樣的好,我們看看程序切換函式的設計,既有程序的切換都是在schedule裡面進入switch_to從而找到標號1的,而在 switch_to之後就剩下乙個finish_task_switch和判斷重新排程標誌了,我們看看ret_from_fork:

entry(ret_from_fork)

pushl %eax      //注意剛從switch_to呼叫的__switch_to中ret回來,正好ret到了ret_from_fork(注意switch_to中jmp 指令前的push),而那個函式返回的就是prev,將其放到了eax中,故這裡schedule_tail的引數就是prev,也就是切換出去的程序。

call schedule_tail

get_thread_info(%ebp)

popl %eax

jmp syscall_exit

上 面看到ret_from_fork呼叫的schedule_tail引數是切換出去的程序,而後者馬上呼叫finish_task_switch,這樣就 和schedule中的switch_to之後的邏輯對上了,而且引數也沒有什麼問題,那麼finish_task_switch之後的邏輯呢,比如判斷 重新排程標誌怎麼辦?那就看看ret_from_fork中的syscall_exit吧,那裡面做了判斷,如果需要排程,那就會進入正常的 schedule流程,十分正確。其實就是這個finish_task_switch善後惹的禍,不過它的設計也是乙個很巧妙的看點,它主要判斷原先的進 程是否還有存在的必要,如果已經dead了,那麼就是在這裡徹底釋放其task_struct的,因此必須儲存prev的值,因為prev是 schedule的區域性變數在prev的核心棧中,在切換到新的核心棧後(schedule函式用到了兩個核心棧),prev失效,因此才要儲存的。在 do_exit中,即使exit的程序已經沒有引用了其task_struct也不能釋放,因為linux中沒有專門的排程管理器可以發現這一點然後自動 切換到別的程序,最終必須靠退出的程序自己schedule掉才行,而它自己呼叫schedule的時候它就是current,current最終成了 prev,整個切換過程都需要退出程序的task_struct結構,只有在switch_to到新程序了,才可以將再也沒有用的退出程序的 task_struct釋放掉。可見程序退出也設計的很好,linux中沒有專門的排程管理執行緒雖然咋一看很不美觀,但是它畢竟不是微核心結構,大核心的 優點就是高效,直接讓需要切換的程序自己呼叫切換**另外別的程序就緒後告訴正執行的程序有切換需要然後著手排程,這種方式肯定最高效,如果設定了排程管 理執行緒,需要排程時還要通知這個管理器,很多切換很低效,但是卻很美觀。這一點上,linux中的排程是和諧自發的搶占式協作,而帶有排程管理器的核心對 於排程則是強行的管制。

asmlinkage void schedule_tail(task_t *prev)

到 此為止,linux的程序切換還是在核心程序管理的**下,還沒有開始和使用者程序相關的行為,也就是說regs儲存的暫存器還沒有起作用,只有核心判斷內 核的事情已經完了,沒有什麼遺漏了才會開始程序本身的工作,也就是restore_all的邏輯了。新建立的程序用鬆散的**和緊湊的schedule邏 輯達成了一致,然後只要這個新程序開始了,那麼它就會進入那個巨大的linux程序切換沙漏從而進入了正常的單點切換流程。

最後我們看一下核心執行緒的返回值,也就是kernel_thread的返回值問題。其實可以這麼理解:根本就不應該這麼設計核心執行緒。 kernel_thread的實現可以看出,核心主要借用了使用者程序的建立方式建立了核心執行緒。在使用者空間,程序建立是copy-on-write的,但 是核心執行緒卻沒有這一說,而且unix的程序建立就是複製父程序的位址空間而沒有任何子程序的特殊策略,特殊行為策略需要以後的exec或者別的方式設 置,而且子程序如何執行需要在父程序的程式源**中判斷fork函式的返回值後加以指定。之所以使用者程序建立函式fork的返回值那麼重要就是因為父子共 享乙個位址空間然後copy-on-write,通過fork的返回值區別父子程序。而核心執行緒雖然也是do_fork實現,但是它一開始就指定了子程序 的執行函式也就是子程序的行為策略,如此一來do_fork的返回值就顯得不是那麼重要了,其實在核心中建立核心執行緒時,根本就不返回0,也沒有什麼返回 0就是子程序之說,實際上即使在使用者空間fork函式呼叫時,返回的0也不是核心的do_fork返回的,do_fork只會返回新程序的pid,而 fork的0返回值是核心在ret_from_fork之後進入使用者空間前restore_all的時候pop到eax中的,然後庫實現的fork將 eax作為返回值,實際上,fork的子程序在進入使用者空間前從來不經過do_fork這條路,可以看看它的thread的eip是 ret_from_fork,也就是只要開始執行子程序,就在switch_to中會執行ret_from_fork,而從ret_from_fork順 著看,一直就到了restore_all從而返回使用者空間。對於核心執行緒,根本就沒有子程序返回這一說,子程序也就是新建立的核心執行緒直接執行,完事了就 直接退出,這樣的原因就是在建立子程序時就已經給了它執行策略,如此一來就不需要返回原點來靠返回值分辨父子程序,但是核心執行緒確實是按照使用者程序的那一 套機制建立的啊,子程序也在copy_process複製父程序,這沒有什麼不同啊,如此怎麼能不返回原點呢?其實linux使用了乙個騙術,它在建立內 核執行緒時偽造了乙個父程序的現場:

int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)

__asm__(".section .text/n"

".align 4/n"

"kernel_thread_helper:/n/t"  //這個標號函式管理了核心子程序

"movl %edx,%eax/n/t"     //這裡事實上沖掉了在copy_thread中被置為0的eax,如此看出eax根本就沒有像使用者程序建立那樣保持為0

"pushl %edx/n/t"   //edx裡面是核心執行緒函式的引數

"call *%ebx/n/t"   //ebx裡面就是核心執行緒函式指標

"pushl %eax/n/t"   //核心執行緒函式的返回值

"call do_exit/n"   //以核心函式返回值作為引數呼叫do_exit

".previous");

tlb懶惰模式可以參考我的《tlb重新整理的懶惰模式》一文,大致意思就是,在單cpu下,重新整理tlb是乙個主動的過程,因此沒有什麼要說的,主動的過程往往行為很確定,但是smp下就複雜多了,可以簡單看一下:

static inline task_t * context_switch(runqueue_t *rq, task_t *prev, task_t *next)

else

switch_mm(oldmm, mm, next);    //切換

if (unlikely(!prev->mm))

...

} static inline void enter_lazy_tlb(struct mm_struct *mm, struct task_struct *tsk)

執行緒切換與程序切換以及開銷

為了更好的了解上下文切換,需要我們了解虛擬記憶體的概念。虛擬記憶體是作業系統為每個程序提供的一種抽象,每個程序都有屬於自己的 私有的 位址連續的虛擬記憶體,當然我們知道最終程序的資料及 必然要放到物理記憶體上,那麼必須有某種機制能記住虛擬位址空間中的某個資料被放到了哪個物理記憶體位址上,這就是所謂的...

linux核心 5 核心程序排程與程序切換

一 程序排程 程序被建立到了鍊錶中,如何再進行進一步的呼叫和排程?程序排程 void schedule void 程序排程函式 switch to next 程序切換函式 一 void schedule void 程序排程函式 1 看一下 呼叫了schedule函式,在system call中尋找也...

linux的核心程序 執行緒

linux啟動後,核心自動執行如下執行緒 程序 核心其實是不區分程序和執行緒的。idle程序 pid 0。系統的啟動程序。啟動後負責程序的排程。init程序 pid 1。由idle程序建立,首先負責系統的啟動。負責啟動所有其他程序。啟動後變成守護程序,用於監視其他所有程序和使用者程序的啟動。所有使用...