作業系統原理:斷點切換原理及實現
本文參考書:作業系統真像還原、作業系統原型xv6分析與實驗、其中圖主要來自linux核心完全註釋
本文針對斷點切換迷茫的問題。
詳解核心態-使用者態的棧變化, 瞭解使用者態-核心態的實現原理和程式碼分析
為幫助大家理解,我將模擬斷點切換時的棧變化過程。
首先要知道幾個基礎概念
①呼叫約定:
C語言是用cdecl 約定,
函式引數從右到左入棧,
引數在棧中傳遞,EAX、ECX、EDX 暫存器由呼叫者儲存,
其餘暫存器由被呼叫者儲存,
函式返回值儲存在EAX中。
呼叫者清理棧空間
②核心態上下文context結構 context {
uint edi;
uint esi;
uint ebx ;
unit ebp ;
uint eip ; }
這個結構的作用在於程序在核心態中執行系統呼叫時可能出現的程序切換操作呼叫swtch函式 “後”的 被呼叫者儲存,也就是說swtch是被呼叫者。
還有核心態執行排程器函式 scheduler,每個CPU都有自己獨立的context,用於執行 swtch 函式 “後”的 被呼叫者儲存,可以理解為CPU執行流的上下文。
當核心載入到記憶體,所有都相關資料等初始化完畢後每個CPU只是無限迴圈 scheduler 排程函式中的for(;;)死迴圈,直到排程0號程序,才切換到使用者態。(有些作業系統實現不是無限迴圈,都可以總體上都一樣細節有差別)
③ 當用戶態切換入核心態出現特權級變換時,會在tss中找到當前程序的核心棧地址並進行切換,在當前程序的核心棧中,會壓入 cs :ip、flags、ss:sp 等(這是硬體幫忙完成的)。
④ 中斷入口,執行任何中斷時,優先執行的一段程式碼
ntr%1entry: ; 每個中斷處理程式都要壓入中斷向量號,所以一箇中斷型別一箇中斷處理程式,自己知道自己的中斷向量號是多少%2 ; 中斷若有錯誤碼會壓在eip後面 ; 以下是儲存上下文環境 push ds push es push fs push gs pushad ; PUSHAD指令壓入32位暫存器,其入棧順序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
執行組合語言後將棧變成類似上圖的形式,每個作業系統都是類似的處理方式但是也稍有不同,本文不需要扣這種細節,只需要知道為了儲存使用者態上下文 需要壓入所有暫存器。 從ss 一直到 esp 這段棧空間也可以稱為 使用者態斷點,用trapframe結構體描述。
一、
我們現在模擬第一個場景,核心已經載入入記憶體CPU開始執行scheduler 排程函式,此時程序連結串列中有A 、B 2個程序(實際的作業系統應該是排程init程序)。
上圖為程序剛剛建立好,但是還沒進行排程時,由核心建立的程序核心棧。如果是init程序,那是核心一條條寫進去的,否則是fork系統呼叫所建立的。
為什麼新建立的程序要變成這樣?
這樣是模擬由使用者態切換入核心態時的棧情況。
這裡要注意,如果是正常的使用者態切換入核心態,一般是要執行系統呼叫, 上圖的中斷退出函式地址和context結構之間會有很多呼叫幀,並且中斷退出函式地址不會出現在棧中,而且被中斷入口函式替換,中斷入口函式是第一個呼叫幀,他在下幾條指令會執行中斷退出函式。
我們所要關注的就是如何由核心態切換到使用者態,就是其中所加入的中斷退出函式。
當核心scheduler函式,選中上圖的程序pcb,會執行swtch
1 scheduler(void) 2 { 3 struct proc *p; 4 5 for(;;){ 6 // Enable interrupts on this processor. 7 sti(); 8 9 // Loop over process table looking for process to run. 10 acquire(&ptable.lock); 11 for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ 12 if(p->state != RUNNABLE) 13 continue; 14 15 // Switch to chosen process. It is the process's job 16 // to release ptable.lock and then reacquire it 17 // before jumping back to us. 18 proc = p; 19 switchuvm(p); 20 p->state = RUNNING; 21 swtch(&cpu->scheduler, p->context); 22 switchkvm(); 23 24 // Process is done running for now. 25 // It should have changed its p->state before coming back. 26 proc = 0; 27 } 28 release(&ptable.lock); 29 30 }
swtch 的引數1 old_context ,引數2 new_context
1 .globl swtch 2 swtch: 3 movl 4(%esp), %eax //獲得引數old_context 4 movl 8(%esp), %edx //獲得引數new_context 5 6 # Save old callee-save registers 7 pushl %ebp 8 pushl %ebx 9 pushl %esi 10 pushl %edi 11 12 # Switch stacks 13 movl %esp, (%eax) 14 movl %edx, %esp 15 16 # Load new callee-save registers 17 popl %edi 18 popl %esi 19 popl %ebx 20 popl %ebp 21 ret
問題:為什麼要排程程序僅僅需要context?
答:因為context是放在棧頂的,通過棧頂就可以得到出核心棧的位置。
老的系統或者一些簡單的開源系統,pcb和核心棧是放在同一個頁中,可以通過遮蔽有效位計算核心棧位置,現代linux系統pcb是有單獨分配器分配,不過也有類似的結構和操作計算偏移。
程式碼的 7-10行對於本次swtch呼叫,是壓入context到核心的核心棧,為什麼沒有壓入ip?
在核心排程器scheduler → swtch 時,ip作為返回地址已經壓入棧中了。 此時ip指向scheduler的第22行程式碼。
程式碼13-14 行 重點是這裡,此時的棧頂是new_context的地址,那彈出了 17-20後,ret 返回的ip 地址是上圖的中斷退出函式。由此完成了核心態到使用者態的切換。
中斷退出函式的作用如下圖
而此時的核心的核心棧
我們現在模擬一個場景,2個程序 A , B,其中A在執行,B就緒。
叮叮叮,第一個時鐘中斷來啦 。
1 static void intr_timer_handler(void) { 2 struct task_struct* proc = running_thread(); //獲取程序或執行緒pcb 3 4 ASSERT(proc->stack_magic == 0x19870916); // 檢查棧是否溢位 5 6 proc->elapsed_ticks++; // 記錄此執行緒佔用的cpu時間嘀 7 ticks++; //從核心第一次處理時間中斷後開始至今的滴噠數,核心態和使用者態總共的嘀噠數 8 9 if (proc->ticks == 0) { // 若程序時間片用完就開始排程新的程序上cpu 10 swtch(proc->context,cpu->context) 11 } else { // 將當前程序的時間片-1 12 proc->ticks--; 13 } 14 }
從整體上看呼叫過程,如下圖,中斷入口拿中會拿到中斷號,然後執行相應的中斷也就是時鐘中斷的中斷號。通過swtch 將當前棧頂切換回核心的核心棧,在swtch中執行ret時,下一條程式會執行 scheduler的第22行程式碼。然後重新排程下一個程序。
此時的程序A的核心棧如下圖
當然如果呼叫其他中斷函式,那中斷入扣幀到context結構中間會替換為其他棧幀。