深入理解Linux内核之主调度器
進程上下文切換
上下文切換動作,來保存上一個進程的“上下文”,恢復下一個進程的“上下文”,主要包括進程地址空間切換和處理器狀態切換。
注:這里的上下文實際上是指進程運行時最小寄存器的集合。
如果切換的next進程不是同一個進程,才進行切換:
__schedulei??f?(likely(prev?!=?next))?{??????...context_switch??//進程上下文切換}進程地址空間切換
進程地址空間切換就是切換虛擬地址空間,使得切換之后,當前進程訪問的是屬于自己的虛擬地址空間(包括用戶地址空間和內核地址空間),本質上是切換頁表基地址寄存器。
進程地址空間切換讓進程產生獨占系統內存的錯覺,因為切換完地址空間后,當前進程可以訪問屬于它的海量的虛擬地址空間(內核地址空間各個進程共享,用戶地址空間各個進程私有),而實際上物理地址空間只有一份。
下面給出源代碼分析:
context_switch ->/*|*?kernel?->?kernel???lazy?+?transfer?active|*???user?->?kernel???lazy?+?mmgrab()?active|*|*?kernel?->???user???switch?+?mmdrop()?active|*???user?->???user???switch|*/if?(!next->mm)?{????????????????????????????????//?to?kernelenter_lazy_tlb(prev->active_mm,?next);next->active_mm?=?prev->active_mm;if?(prev->mm)???????????????????????????//?from?usermmgrab(prev->active_mm);elseprev->active_mm?=?NULL;}?else?{????????????????????????????????????????//?to?user...switch_mm_irqs_off(prev->active_mm,?next->mm,?next);if?(!prev->mm)?{????????????????????????//?from?kernel/*?will?mmdrop()?in?finish_task_switch().?*/rq->prev_mm?=?prev->active_mm;prev->active_mm?=?NULL;}????????????}????????????????????以上代碼是判斷是否next進程是內核線程,如果是則不需要進行地址空間切換(實際上指的是用戶地址空間),因為內核線程總是運行在內核態訪問的是內核地址空間,而內核地址空間是所有的進程共享的。在arm64架構中,內核地址空間是通過ttbr1_el1來訪問,而它的主內核頁表在內核初始化的時候已經填充好了,也就是我們常說的swapper_pg_dir頁表,后面所有對內核地址空間的訪問,無論是內核線程也好還是用戶任務,統統通過swapper_pg_dir頁表來訪問,而在內核初始化期間swapper_pg_dir頁表地址已經加載到ttbr1_el1中。
需要說明一點的是:這里會做“借用” prev->active_mm的處理,借用的目的是為了避免切換屬于同一個進程的地址空間。舉例說明:Ua ?-> ?Ka ?-> ?Ua ??,Ua表示用戶進程, ?Ka表示內核線程,當進行這樣的切換的時候,Ka 借用Ua地址空間,Ua ?-> ?Ka不需要做地址空間切換,而Ka ?-> ?Ua按理來說需要做地址空間切換,但是由于切換的還是Ua 地址空間,所以也不需要真正的切換(判斷了Ka->active_mm == Ua->active_mm ),當然還包括切換的是同一個進程的多個線程的情況,這留給大家思考。
下面來看下真正的地址空間切換:
?switch_mm_irqs_off(prev->active_mm,?next->mm,?next);->switch_mm??//arch/arm64/include/asm/mmu_context.h->?if?(prev?!=?next)?__switch_mm(next);->check_and_switch_context(next)->?...??//asid處理->?cpu_switch_mm(mm->pgd,?mm)->cpu_do_switch_mm(virt_to_phys(pgd),mm)->?unsigned?long?ttbr1?=?read_sysreg(ttbr1_el1);??unsigned?long?asid?=?ASID(mm);?????????????????unsigned?long?ttbr0?=?phys_to_ttbr(pgd_phys);??...write_sysreg(ttbr1,?ttbr1_el1);???//設置asid到ttbr1_el1isb();????????????????????????????write_sysreg(ttbr0,?ttbr0_el1);???//設置mm->pgd?到ttbr0_el1上面代碼是做真正的地址空間切換,實際的切換很簡單,并沒有那么復雜和玄乎,僅僅設置頁表基地址寄存器即可,當然這里還涉及到了為了防止頻繁無效tlb的ASID的設置。
主要做的工作就是設置next進程的ASID到ttbr1_el1, 設置mm->pgd 到ttbr0_el1,僅此而已!
需要注意的是:1.寫到ttbr0_el1的值是進程pgd頁表的物理地址。2.雖然做了這樣的切換,但是這個時候并不能訪問到next的用戶地址空間,因為還處在主調度器上下文中,屬于內核態,訪問的是內核空間。
而一旦返回了用戶態,next進程就能正常訪問自己地址空間內容:
訪問一個用戶空間的虛擬地址va,首先通過va和記錄在ttbr1_el1的asid查詢tlb,如果找到相應表項則獲得pa進行訪問。
如果tlb中沒有找到,通過ttbr0_el1來遍歷自己的多級頁表,找到相應表項則獲得pa進行訪問。
如果發生中斷異常等訪問內核地址空間,直接通過ttbr1_el1即可完成訪問。
訪問沒有建立頁表映射的合法va,發生缺頁異常來建立映射關系,填寫屬于進程自己的各級頁表,然后訪問。
訪問無法地址,發生缺頁殺死進程等等。
處理器狀態切換
來切換下一個進程的執行流,上一個進程執行狀態保存,讓下一個進程恢復執行狀態。
處理器狀態切換而后者讓進程產生獨占系統cpu的錯覺,使得系統中各個任務能夠并發(多個任務在多個cpu上運行)或分時復用(多個任務在一個cpu上運行)cpu資源。
下面給出代碼:
context_switch ->(last)?=?__switch_to((prev),?(next))->?fpsimd_thread_switch(next)?//浮點寄存器切換...last?=?cpu_switch_to(prev,?next);?處理器狀態切換會做浮點寄存器等切換,最終調用cpu_switch_to做真正切換。
cpu_switch_to??//arch/arm64/kernel/entry.S SYM_FUNC_START(cpu_switch_to)mov?????x10,?#THREAD_CPU_CONTEXTadd?????x8,?x0,?x10mov?????x9,?spstp?????x19,?x20,?[x8],?#16?????????????//?store?callee-saved?registersstp?????x21,?x22,?[x8],?#16stp?????x23,?x24,?[x8],?#16stp?????x25,?x26,?[x8],?#16stp?????x27,?x28,?[x8],?#16stp?????x29,?x9,?[x8],?#16str?????lr,?[x8]add?????x8,?x1,?x10ldp?????x19,?x20,?[x8],?#16?????????????//?restore?callee-saved?registersldp?????x21,?x22,?[x8],?#16ldp?????x23,?x24,?[x8],?#16ldp?????x25,?x26,?[x8],?#16ldp?????x27,?x28,?[x8],?#16ldp?????x29,?x9,?[x8],?#16ldr?????lr,?[x8]mov?????sp,?x9msr?????sp_el0,?x1ptrauth_keys_install_kernel?x1,?x8,?x9,?x10scs_save?x0,?x8scs_load?x1,?x8ret SYM_FUNC_END(cpu_switch_to)這里傳遞過來的是x0為prev進程的進程描述符(struct task_struct)地址, x1為next的進程描述符地址。會就將prev進程的 x19-x28,fp,sp,lr保存到prev進程的tsk.thread.cpu_context中,next進程的這些寄存器值從next進程的tsk.thread.cpu_context中恢復到相應寄存器。這里還做了sp_el0設置為next進程描述符的操作,為了通過current宏找到當前的任務。
需要注意的是:
mov ? ? sp, x9 ?做了切換進程內核棧的操作。
ldr ? ? lr, [x8] 設置了鏈接寄存器,然后ret的時候會將lr恢復到pc從而真正完成了執行流的切換。
精美圖示
這里給出了進程切換的圖示(以arm64處理器為例),這里從prev進程切換到next進程。
進程再次被調度
當進程重新被調度的時候,從原來的調度現場恢復執行。
關于lr地址的設置
1)如果切換的next進程是剛fork的進程,它并沒有真正的這些調度上下文的存在,那么lr是什么呢?這是在fork的時候設置的:
do_fork...copy_thread?//arch/arm64/kernel/process.c->memset(&p->thread.cpu_context,?0,?sizeof(struct?cpu_context));p->thread.cpu_context.pc?=?(unsigned?long)ret_from_fork;p->thread.cpu_context.sp?=?(unsigned?long)childregs;設置為了ret_from_fork的地址,當然這里也設置了sp等調度上下文(這里將進程切換保存的寄存器稱之為調度上下文)。
SYM_CODE_START(ret_from_fork)bl??????schedule_tailcbz?????x19,?1f?????????????????????????//?not?a?kernel?threadmov?????x0,?x20blr?????x19 1:??????get_current_task?tskb???????ret_to_user SYM_CODE_END(ret_from_fork)剛fork的進程,從cpu_switch_to的ret指令執行后返回,lr加載到pc。
于是執行到ret_from_fork:這里首先調用schedule_tail對前一個進程做清理工作,然后判斷是否為內核線程如果是執行內核線程的執行函數,如果是用戶任務通過ret_to_user返回到用戶態。
2)如果是之前已經被切換過的進程,lr為cpu_switch_to調用的下一條指令地址(這里實際上是__schedule函數中調用barrier()的指令地址)。
關于__switch_to的參數和返回值
?switch_to(prev,?next,?prev) ->??((last)?=?__switch_to((prev),?(next)))這里做處理器狀態切換時,傳遞了兩個參數,返回了一個參數:
prev和next很好理解就是 就是前一個進程(當前進程)和下一個進程的 task_struct結構指針,那么last是什么呢?
一句話:返回的last是當前重新被調度的進程的上一個進程的 task_struct結構指針。
如:A ->B ->千山萬水->D -> A 上面的切換過程:A切換到B 然后經歷千山萬水再從D -> A,這個時候A重新被調度時,last即為D的 task_struct結構指針。
獲得當前重新被調度進程的前一個進程是為了回收前一個進程資源,見后面分析。
關于finish_task_switch
進程被重新調度時無論是否為剛fork出的進程都會走到finish_task_switch這個函數,下面我們來看它做了什么事情:
主要工作為:檢查回收前一個進程資源,為當前進程恢復執行做一些準備工作。
finish_task_switch ->finish_lock_switch->raw_spin_unlock_irq???//使能本地中斷 ->if?(mm)?mmdrop(mm)??//有借有還??借用的mm現在歸還->if?(unlikely(prev_state?==?TASK_DEAD))?{????????//前一個進程是死亡狀態put_task_stack(prev);????//如果內核棧在task_struct中???釋放內核棧??????????????????????????????????????put_task_struct_rcu_user(prev);??//釋放前一個進程的task_struct占用內存}????????????????????????????????????????可以看到進程被重新調度時首先需要做的主要是:
重新使能本地中斷 ,進程被重新調度時,本地cpu中斷是被重新打開的!!!
如果有借用mm的情況,現在歸還 如果前一個是內核線程,在進程地址空間切換時“借用了”某個進程的mm_struct,現在切換到了下一個進程,理應歸還,歸還做的是遞減借用的mm_struct的引用計數,引用計數為0就會釋放mm_struct占用的內存。
對于上一個死亡的進程現在回收最后的資源, 注意這里是遞減引用計數,當引用計數為0時才會真正釋放。
總結
主調度器可以說Linux內核進程管理中的核心組件,進程管理的其他部分如搶占、喚醒、睡眠等都是圍繞它來運作。在原子上下文不能發生調度,說的就是調用主調度器,但是可以設置搶占標志以至于在最近的搶占點發生調度,如中斷中喚醒高優先級進程的場景。主調度器所做的工作就是讓出cpu,內核很多場景可以直接或間接調用它,而大體上可以分為兩種情況:即為主動調度和搶占式調度。主調度器做了兩件事情:選擇下一個進程和進程進程上下文切換。選擇下一個進程解決選擇合適高優先級進程的問題。進程進程上下文切換又分為地址空間切換和處理器狀態切換,前者讓進程產生獨自占用系統內存的錯覺,而后者讓進程產生獨自占用系統cpu的錯覺,讓系統各個進程有條不紊的共享內存和cpu等資源。
- END -
看完一鍵三連在看,轉發,點贊
是對文章最大的贊賞,極客重生感謝你
推薦閱讀
硬核致敬Linux !30歲生日快樂!
后端技術趨勢指南|如何選擇自己的技術方向
Linux調度系統全景指南(上篇)
總結
以上是生活随笔為你收集整理的深入理解Linux内核之主调度器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 成为明星程序员的独特秘密|极客原创
- 下一篇: 赠送 12 本 《C++ 服务器开发精髓