https://blog.csdn.net/Swartz2015/article/details/61615603
xv6進程切換-swtch函數
進程切換中由於需要保存當前進程的寄存器狀態信息,又要將新進程記錄的寄存器狀態信息加載到寄存器,因此涉及到許多棧的操作,堆棧間的來回切換,容易讓人眼花繚亂,難以理解。本文試圖分析以下xv6中的進程切換過程。
當前進程通過調用yield函數,進行進程切換。yield函數調用sched函數,sched函數啟動swtch函數完成進程切換。整個流程是這樣的:
yield->sched->swtch
- 1
在sched函數中,可以看到,當前進程總是先切換到當前cpu的scheduler切換器:
//void sched(void)
swtch(&proc->context, cpu->scheduler);
- 1
- 2
切換器是一個死循環,該循環不斷在進程表中掃描,選擇一個RUNNABLE的進程調度,即從scheduler切換器轉換到新選擇的進程:
//scheduler切換器 void scheduler(void) { struct proc *p; for(;;){ // Enable interrupts on this processor. sti(); // Loop over process table looking for process to run. acquire(&ptable.lock); for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ if(p->state != RUNNABLE) continue; // Switch to chosen process. It is the process's job // to release ptable.lock and then reacquire it // before jumping back to us. proc = p; switchuvm(p); p->state = RUNNING; swtch(&cpu->scheduler, proc->context); switchkvm(); // Process is done running for now. // It should have changed its p->state before coming back. proc = 0; } release(&ptable.lock); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
顯然,swtch函數是重點。swtch函數的原型是:
void swtch(struct context **old, struct context *new);
- 1
它的工作主要包括:1. 保存當前(old)進程的上下文。 2. 加載新進程(new)的上下文到機器寄存器中。
contex中其實就是幾個寄存器變量,用來保存這些寄存器的值。
swtch的函數代碼如下:
# Context switch # # void swtch(struct context **old, struct context *new); # # Save current register context in old # and then load register context from new. .globl swtch swtch: movl 4(%esp), %eax movl 8(%esp), %edx # Save old callee-save registers pushl %ebp pushl %ebx pushl %esi pushl %edi # Switch stacks movl %esp, (%eax) movl %edx, %esp # Load new callee-save registers popl %edi popl %esi popl %ebx popl %ebp ret
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
下面分析以下swtch函數的執行過程。
swtch函數的執行過程
當任意進程調用swtch函數時,該進程的堆棧是下面這個樣子的:
其中棧中每個存儲區域都是4個字節。這里需要提一下,在執行指令call時,當前指令的下一條指令的地址將會push到棧中,也就是上面看到的eip,這樣就可以保證在函數返回時,可以回到函數調用的下一條指令的地方。然后swtch開始執行,
movl 4(%esp), %eax movl 8(%esp), %edx
- 1
- 2
所以寄存器eax和edx都會指向如下圖所示的位置:
接下來會依次將有關上下文信息的寄存器push到棧中
pushl %ebp pushl %ebx pushl %esi pushl %edi
- 1
- 2
- 3
- 4
push完之后,就可以堆棧布局就變成下圖所示:
對比contex和當前的棧布局,可以直觀看到,此時在棧上存儲的剛好是一個contex。當前的棧頂esp剛好指向當前進程的contex中的第一個元素的地址,所以這里可以說contex中包括了棧信息和寄存器信息,不僅可以用它來進行棧切換,還可以通過它恢復寄存器。因此可以想到,下次重新調度這個進程時,只需要直接將棧上的信息彈到相應寄存器中就可以了。而這個棧頂指針,我們可以直接用proc->contex指針記錄就可以了。即
proc->contex = esp;
- 1
我們繼續看swtch函數:
# Switch stacks movl %esp, (%eax) movl %edx, %esp
- 1
- 2
- 3
根據注釋也可以看到,這兩行主要是用於棧的切換。前面我們知道,eax中存儲的是指向old_proc->contex的指針,old_proc->contex是指向當前進程的contex的指針,所以movl %esp, (%eax),相當於:
old_proc->contex = esp;
- 1
也就是讓當前進程的contex指針指向棧頂。根據前面的示意圖我們可以理解原理。
到此為止,舊進程的contex保存工作已經完成了。
下面的工作自然是怎么把新進程的contex彈出到對應的寄存器中。其實保存過程和彈出恢復過程是對稱的。
從保存過程中,我們知道新進程的contex信息總是可以通過new_proc->contex獲得,因為new_proc->contex指向了new_proc內核棧的棧頂,而棧頂依次保存着上下文寄存器信息,所以將new_proc->contex賦值到esp就完成了堆棧切換,再依次pop就可以恢復上下文寄存器信息。但是我們怎么獲得new_proc的contex呢?回到前面,我們看到,就進程在調用swtch時,就把new_proc的contex放在了堆棧中,並且根據前面edx=new_proc->contex。所以堆棧切換就是把
esp= edx;
- 1
然后
popl %edi popl %esi popl %ebx popl %ebp
- 1
- 2
- 3
- 4
就可以依次將new_contex中保存的寄存器值依次彈回到對應寄存器中了。
最后的
ret
- 1
指令將eip彈回。所以新進程會從它上次調用swtch函數的下一條指令開始執行。
