xv6源码阅读——进程切换
說明
- 閱讀的代碼是 xv6-riscv 版本的
概述
從一個用戶進程(舊進程)切換到另一個用戶進程(新進程)所涉及的步驟:
- 通過中斷機制,從trap機制進入內核線程
- 調用swtch從該進程進行上下文切換,切換到到調度進程
- 通過調度進程切換到新進程的內核線程
- 在通過trap機制返回到新進程用戶線程
上面過程就簡單闡述,是怎樣進行進程切換的
當然,里面會有很多細節,后面會詳細介紹
從用戶線程到內核線程
這一部分在上一章是有過介紹的,通過中斷機制,通過uservec保存寄存器,然后進入usertrap()
usertrap
void usertrap(void) {int which_dev = 0;if((r_sstatus() & SSTATUS_SPP) != 0)panic("usertrap: not from user mode");//省略 ··········// give up the CPU if this is a timer interrupt.if(which_dev == 2)yield();usertrapret(); }其他部分就不過多贅述了(上一篇博客講的很詳細),通過判斷中斷類型,這是一個定時器中斷,就調用yield()函數
從舊進程進入調度進程
yield
// Give up the CPU for one scheduling round. void yield(void) {struct proc *p = myproc();acquire(&p->lock);p->state = RUNNABLE;sched();release(&p->lock); }該函數干的事情很簡單,先獲取該進程的鎖,將進程狀態設置為RUNNABLE
然后調用sched()函數進行后續操作
題:為什么再設置進程狀態前,要先獲取該進程的鎖呢
因為一旦將進程狀態設置為RUNNABLE,調度器就會認為該進程處于等待狀態的可能會讓他接下來運行,但實際上,該進程還是處于運行狀態的
如果這是一個多核CPU的,就可能會發生一個進程被兩個CPU同時運行,程序可能會立馬崩潰。
所以我們要保證改變進程狀態,保存寄存器,載入另一個進程的寄存器這三步是原子的。
sched
void sched(void) {int intena;struct proc *p = myproc();if(!holding(&p->lock))panic("sched p->lock");if(mycpu()->noff != 1)panic("sched locks");if(p->state == RUNNING)panic("sched running");if(intr_get())panic("sched interruptible");intena = mycpu()->intena;swtch(&p->context, &mycpu()->context);mycpu()->intena = intena; }- 該函數操作也很簡單,進行一系列判斷(防御性編程)
- 將mycpu()->intena保存下來,因為切換進程后,他會被改變
- 調用swtch切換上下文,該函數完成后就會進行入了調度進程
- 恢復mycpu()->intena(要等到該進程再次拿到CPU)
swtch
這是一段匯編代碼,主要干的工作是將該進程的寄存器保存下來,然后載入調度進程的寄存器
.globl swtch swtch:# 保存寄存器sd ra, 0(a0)# 省略```````````````````sd s11, 104(a0)# 恢復所要切換進程的寄存器ld ra, 0(a1)# 省略```````````````````ld s11, 104(a1)ret問題:為什么RISC-V中有32個寄存器,但是swtch函數中只保存并恢復了14個寄存器?
因為swtch函數是從C代碼調用的,所以我們知道Caller Saved Register會被C編譯器保存在當前的棧上。
該函數完成后,就進入了調度進程
調度
scheduler
void scheduler(void) {struct proc *p;struct cpu *c = mycpu();c->proc = 0;for(;;){//通過確保設備可以中斷來避免死鎖。intr_on();for(p = proc; p < &proc[NPROC]; p++) {acquire(&p->lock);if(p->state == RUNNABLE) {// 切換到所選的進(釋放它的鎖,然后在跳回我們之前重新獲取它)p->state = RUNNING;c->proc = p;swtch(&c->context, &p->context);// 進程目前已完成運行。c->proc = 0;}release(&p->lock);}} }-
進程切換過來,是切換到swtch(&c->context, &p->context)這一行的,后面他會先釋放鎖
(避免該進程永遠無法被調度,該鎖是在yield()函數中獲取的) -
scheduler在進程表上循環查找可運行的進程,該進程具有p->state == RUNNABLE。一旦找到一個進程,它將設置CPU當前進程變量c->proc,將該進程標記為RUNINING,然后調用swtch開始運行它
-
scheduler在一開始要獲取進程鎖,將RUNNABLE進程轉換為RUNNING,在內核線程完全運行之前(在swtch之后,例如在yield中)絕不能釋放鎖。
調用swtch后就會進入另一個進程了
從調度進程進入新進程
該過程也是在完成swtch后完成切換的
他會從sched函數返回
他會返回到swtch處,然后恢復 mycpu()->intena
從內核線程返回用戶線程
這后面就是一步步返回,通過trap機制返回用戶層,在上一篇博客講的很詳細,不過多贅述
XV6線程第一次調用swtch函數
allocproc
static struct proc* allocproc(void) {struct proc *p;//、、、、、、、、、、省略memset(&p->context, 0, sizeof(p->context));p->context.ra = (uint64)forkret;p->context.sp = p->kstack + PGSIZE;return p; }在創建進程時,會設置ra和sp
ra很重要,因為這是進程的第一個switch調用會返回的位置。同時因為進程需要有自己的棧,所以ra和sp都被設置了。這里設置的forkret函數就是進程的第一次調用swtch函數會切換到的“另一個”線程位置。
當調度線程將CPU交給該進程時,該進程會從forkret()函數處返回,執行該函數,
// A fork child's very first scheduling by scheduler() // will swtch to forkret. void forkret(void) {static int first = 1;// Still holding p->lock from scheduler.release(&myproc()->lock);if (first) {// File system initialization must be run in the context of a// regular process (e.g., because it calls sleep), and thus cannot// be run from main().first = 0;fsinit(ROOTDEV);}usertrapret(); }從代碼中看,它的工作其實就是釋放調度器之前獲取的鎖。函數最后的usertrapret函數其實也是一個假的函數,它會使得程序表現的看起來像是從trap中返回,但是對應的trapframe其實也是假的,這樣才能跳到用戶的第一個指令中。
總結
以上是生活随笔為你收集整理的xv6源码阅读——进程切换的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为杯数学建模2020获奖名单_我校在2
- 下一篇: linux如何查看所有的用户(user)