Linux0.11實現核心棧任務切換 在Linux-0.11中實現基於核心棧切換的程序切換
轉載:https://www.cnblogs.com/Wangzhike/p/4608405.html
在Linux-0.11中實現基於核心棧切換的程序切換
1. 原有的基於TSS的任務切換的不足
原有的Linux 0.11採用基於TSS和一條指令,雖然簡單,但這指令的執行時間卻很長,在實現任務切換時大概需要200多個時鐘週期。而通過堆疊實現任務切換可能要快,而且採用堆疊的切換還可以使用指令流水的並行化優化技術,同時又使得CPU的設計變得簡單。所以無論是Linux還是Windows,程序/執行緒的切換都沒有使用Intel 提供的這種TSS切換手段,而都是通過堆疊實現的。
2. 程序切換的六段論
基於核心棧實現程序切換的基本思路:當程序由使用者態進入核心時,會引起堆疊切換,使用者態的資訊會壓入到核心棧中,包括此時使用者態執行的指令序列EIP。由於某種原因,該程序變為阻塞態,讓出CPU,重新引起排程時,作業系統會找到新的程序的PCB,並完成該程序與新程序PCB的切換。如果我們將核心棧和PCB關聯起來,讓作業系統在進行PCB切換時,也完成核心棧的切換,那麼當中斷返回時,執行IRET
指令時,彈出的就是新程序的EIP,從而跳轉到新程序的使用者態指令序列執行,也就完成了程序的切換。這個切換的核心是構建出核心棧的樣子,要在適當的地方壓入適當的返回地址,並根據核心棧的樣子,編寫相應的彙編程式碼,精細地完成核心棧的入棧和出棧操作,在適當的地方彈出適當的返回地址,以保證能順利完成程序的切換。同時完成核心棧和PCB的關聯,在PCB切換時,完成核心棧的切換。
2.1 中斷進入核心
-
為什麼要進入核心中去?
大家都知道,作業系統負責程序的排程與切換,所以程序的切換一定是在核心中發生的。要實現程序切換,首先就要進入核心。而使用者程式都是執行在使用者態的,在Linux中,應用程式訪問核心唯一的方法就是系統呼叫,應用程式通過作業系統提供的若干系統呼叫函式訪問核心,而該程序在核心中執行時,可能因為要訪問磁碟檔案或者由於時間片耗完而變為阻塞態,從而引起排程,讓出CPU的使用權。 -
從使用者態進入核心態,要發生堆疊的切換
系統呼叫的核心是指令int 0x80
這個系統呼叫中斷。一個程序在執行時,會有函式間的呼叫和變數的儲存,而這些都是依靠堆疊完成的。程序在使用者態執行時有使用者棧,在核心態執行時有核心棧,所以當執行系統呼叫中斷int 0x80
-
從使用者態進入核心發生了什麼?
當執行int 0x80
這條語句時由使用者態進入核心態時,CPU會自動按照SS、ESP、EFLAGS、CS、EIP的順序,將這幾個暫存器的值壓入到核心棧中,由於執行int 0x80
時還未進入核心,所以壓入核心棧的這五個暫存器的值是使用者態時的值,其中EIP為int 0x80
的下一條語句"=a" (__res)
,這條語句的含義是將eax所代表的暫存器的值放入到_res變數中。所以當應用程式在核心中返回時,會繼續執行 “=a” (__res) 這條語句。這個過程完成了程序切換中的第一步,通過在核心棧中壓入使用者棧的ss、esp建立了使用者棧和核心棧的聯絡,形象點說,即在使用者棧和核心棧之間拉了一條線,形成了一套棧。 -
核心棧的具體樣子
父程序核心棧的樣子
執行int 0x80
將SS、ESP、EFLAGS、CS、EIP入棧。
在system_call中將DS、ES、FS、EDX、ECX、EBX入棧。
system_call:
cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
在system_call中執行完相應的系統呼叫sys_call_xx後,又將函式的返回值eax壓棧。若引起排程,則跳轉執行reschedule。否則則執行ret_from_sys_call。
1 reschedule:
2 pushl $ret_from_sys_call
3 jmp schedule
在執行schedule前將ret_from_sys_call壓棧,因為schedule是c函式,所以在c函式末尾的}
,相當於ret
指令,將會彈出ret_from_sys_call作為返回地址,跳轉到ret_from_sys_call執行。
總之,在系統呼叫結束後,將要中斷返回前,核心棧的樣子如下:
核心棧 |
---|
SS |
ESP |
EFLAGS |
CS |
EIP |
DS |
ES |
FS |
EDX |
ECX |
EBX |
EAX |
ret_from_sys_call |
2.2 找到當前程序的PCB和新程序的PCB
- 當前程序的PCB
當前程序的PCB是用一個全域性變數current指向的(在sched.c中定義) ,所以current即指向當前程序的PCB - 新程序的PCB
為了得到新程序的PCB,我們需要對schedule()函式做如下修改:
void schedule(void)
{
int i,next,c;
struct task_struct *pnext = &(init_task.task);
struct task_struct ** p; /* add */
......
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter,next = i,pnext=*p;
} /* edit */
if (c) break;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
switch_to(pnext,_LDT(next)); /* edit */
}
這樣,pnext就指向下個程序的PCB。
在schedule()函式中,當呼叫函式switch_to(pent, _LDT(next))時,會依次將返回地址}、引數2 _LDT(next)、引數1 pnext壓棧。當執行switch_to的返回指令ret
時,就回彈出schedule()函式的}執行schedule()函式的返回指令}
。關於執行switch_to時核心棧的樣子,在後面改寫switch_to函式時十分重要。
此處將跳入到switch_to中執行時,核心棧的樣子如下:
核心棧 |
---|
SS |
ESP |
EFLAGA |
CS |
EIP |
DS |
ES |
FS |
EDX |
ECX |
EBX |
EAX |
ret_from_sys_call |
pnext |
_LDT(next) |
} |
2.3 完成PCB的切換
2.4 根據PCB完成核心棧的切換
2.5 切換執行資源LDT
這些工作都將有改寫後的switch_to完成。
將Linux 0.11中原有的switch_to實現去掉,寫成一段基於堆疊切換的程式碼。由於要對核心棧進行精細的操作,所以需要用匯編程式碼來實現switch_to的編寫,既然要用匯編來實現switch_to,那麼將switch_to的實現放在system_call.s中是最合適的。這個函式依次主要完成如下功能:由於是c語言調用匯編,所以需要首先在彙編中處理棧幀,即處理ebp暫存器;接下來要取出表示下一個程序PCB的引數,並和current做一個比較,如果等於current,則什麼也不用做;如果不等於current,就開始程序切換,依次完成PCB的切換、TSS中的核心棧指標的重寫、核心棧的切換、LDT的切換以及PC指標(即CS:EIP)的切換。
switch_to(system_call.s)的基本框架如下:
1 switch_to:
2 pushl %ebp
3 movl %esp,%ebp
4 pushl %ecx
5 pushl %ebx
6 pushl %eax
7 movl 8(%ebp),%ebx
8 cmpl %ebx,current
9 je 1f
10 切換PCB
11 TSS中的核心棧指標的重寫
12 切換核心棧
13 切換LDT
14 movl $0x17,%ecx
15 mov %cx,%fs
16 cmpl %eax,last_task_used_math //和後面的cuts配合來處理協處理器,由於和主題關係不大,此處不做論述
17 jne 1f
18 clts
19 1: popl %eax
20 popl %ebx
21 popl %ecx
22 popl %ebp
23 ret
理解上述程式碼的核心,是理解棧幀結構和函式呼叫時控制轉移權方式。
大多數CPU上的程式實現使用棧來支援函式呼叫操作。棧被用來傳遞函式引數、儲存返回地址、臨時儲存暫存器原有值以備恢復以及用來儲存區域性資料。單個函式呼叫操作所使用的棧部分被稱為棧幀結構,其通常結構如下:
棧幀結構的兩端由兩個指標來指定。暫存器ebp通常用作幀指標,而esp則用作棧指標。在函式執行過程中,棧指標esp會隨著資料的入棧和出棧而移動,因此函式中對大部分資料的訪問都基於幀指標ebp進行。
對於函式A呼叫函式B的情況,傳遞給B的引數包含在A的棧幀中。當A呼叫B時,函式A的返回地址(呼叫返回後繼續執行的指令地址)被壓入棧中,棧中該位置也明確指明瞭A棧幀的結束處。而B的棧幀則從隨後的棧部分開始,即圖中儲存幀指標(ebp)的地方開始。再隨後則用來存放任何儲存的暫存器值以及函式的臨時值。
所以執行完指令pushl %eax
後,核心棧的樣子如下:
switch_to中指令movl 8(%ebp),%ebx
即取出引數2_LDT(next)放入暫存器ebx中,而12(%ebp)則是指引數1penxt。
- 完成PCB的切換
1 movl %ebx,%eax
2 xchgl %eax,current
- TSS中的核心棧指標的重寫
如前所述,當從使用者態進入核心態時,CPU會自動依靠TR暫存器找到當前程序的TSS,然後根據裡面ss0和esp0的值找到核心棧的位置,完成使用者棧到核心棧的切換。所以仍需要有一個當前TSS,我們需要在schedule.c中定義struct tss_struct *tss=&(init_task.task.tss)
這樣一個全域性變數,即0號程序的tss,所有程序都共用這個tss,任務切換時不再發生變化。
雖然所有程序共用一個tss,但不同程序的核心棧是不同的,所以在每次程序切換時,需要更新tss中esp0的值,讓它指向新的程序的核心棧,並且要指向新的程序的核心棧的棧底,即要保證此時的核心棧是個空棧,幀指標和棧指標都指向核心棧的棧底。
這是因為新程序每次中斷進入核心時,其核心棧應該是一個空棧。為此我們還需要定義:ESP0 = 4
,這是TSS中核心棧指標esp0的偏移值,以便可以找到esp0。具體實現程式碼如下:
1 movl tss,%ecx
2 addl $4096,%ebx
3 movl %ebx,ESP0(%ecx)
- 核心棧的切換
Linux 0.11的PCB定義中沒有儲存核心棧指標這個域(kernelstack),所以需要加上,而巨集KERNEL_STACK就是你加的那個位置的偏移值,當然將kernelstack域加在task_struct中的哪個位置都可以,但是在某些彙編檔案中(主要是在system_call.s中)有些關於操作這個結構一些彙編硬編碼,所以一旦增加了kernelstack,這些硬編碼需要跟著修改,由於第一個位置,即long state出現的彙編硬編碼很多,所以kernelstack千萬不要放置在task_struct中的第一個位置,當放在其他位置時,修改system_call.s中的那些硬編碼就可以了。
在schedule.h中將struct task_struct修改如下:
1 struct task_struct {
2 long state;
3 long counter;
4 long priority;
5 long kernelstack;
6 ......
7 }
同時在system_call.s中定義`KERNEL_STACK = 12` 並且修改彙編硬編碼,修改程式碼如下:
1 ESP0 = 4
2 KERNEL_STACK = 12
3
4 ......
5
6 state = 0 # these are offsets into the task-struct.
7 counter = 4
8 priority = 8
9 kernelstack = 12
10 signal = 16
11 sigaction = 20 # MUST be 16 (=len of sigaction)
12 blocked = (37*16)
switch_to中的實現程式碼如下:
1 movl %esp,KERNEL_STACK(%eax)
2 movl 8(%ebp),%ebx
3 movl KERNEL_STACK(%ebx),%esp
由於這裡將PCB結構體的定義改變了,所以在產生0號程序的PCB初始化時也要跟著一起變化,需要在schedule.h中做如下修改:
1 #define INIT_TASK \
2 /* state etc */ { 0,15,15,PAGE_SIZE+(long)&init_task,\
3 /* signals */ 0,{{},},0, \
4 ......
5 }
- LDT的切換
switch_to中實現程式碼如下:
1 movl 12(%ebp),%ecx
2 lldt %cx
一旦修改完成,下一個程序在執行使用者態程式時使用的對映表就是自己的LDT表了,地址分離實現了。
2.6 利用IRET指令完成使用者棧的切換
- PC的切換
對於被切換出去的程序,當它再次被排程執行時,根據被切換出去的程序的核心棧的樣子,switch_to的最後一句指令ret
會彈出switch_to()後面的指令}
作為返回返回地址繼續執行,從而執行}
從schedule()函式返回,將彈出ret_from_sys_call
作為返回地址執行ret_from_sys_call,在ret_from_sys_call中進行一些處理,最後執行iret
指令,進行中斷返回,將彈出原來使用者態程序被中斷地方的指令作為返回地址,繼續從被中斷處執行。
對於得到CPU的新的程序,我們要修改fork.c中的copy_process()函式,將新的程序的核心棧填寫成能進行PC切換的樣子。根據實驗提示,我們可以得到新程序的核心棧的樣子,如圖所示:
注意此處需要和switch_to接在一起考慮,應該從“切換核心棧”完事的那個地方開始,現在到子程序的核心棧開始工作了,接下來做的四次彈棧以及ret處理使用的都是子程序核心棧中的東西。
注意執行ret指令時,這條指令要從核心棧中彈出一個32位數作為EIP跳去執行,所以需要弄出一個個函式地址(仍然是一段彙編程式,所以這個地址是這段彙編程式開始處的標號)並將其初始化到棧中。既然這裡也是一段彙編程式,那麼放在system_call.s中是最合適的。我們弄的一個名為first_return_from_kernel的彙編標號,將這個地址初始化到子程序的核心棧中,現在執行ret以後就會跳轉到first_return_from_kernel去執行了。
system_call.s中switch_to的完整程式碼如下:
1 .align 2
2 switch_to:
3 pushl %ebp
4 movl %esp,%ebp
5 pushl %ecx
6 pushl %ebx
7 pushl %eax
8 movl 8(%ebp),%ebx
9 cmpl %ebx,current
10 je 1f
11 movl %ebx,%eax
12 xchgl %eax,current
13 movl tss,%ecx
14 addl $4096,%ebx
15 movl %ebx,ESP0(%ecx)
16 movl %esp,KERNEL_STACK(%eax)
17 movl 8(%ebp),%ebx
18 movl KERNEL_STACK(%ebx),%esp
19 movl 12(%ebp),%ecx
20 lldt %cx
21 movl $0x17,%ecx
22 mov %cx,%fs
23 cmpl %eax,last_task_used_math
24 jne 1f
25 clts
26 1:
27 popl %eax
28 popl %ebx
29 popl %ecx
30 popl %ebp
31 ret
system_call.s中first_return_from_kernel程式碼如下:
1 .align 2
2 first_return_from_kernel:
3 popl %edx
4 popl %edi
5 popl %esi
6 pop %gs
7 pop %fs
8 pop %es
9 pop %ds
10 iret
fork.c中copy_process()的具體修改如下:
1 ......
2 p = (struct task_struct *) get_free_page();
3 ......
4 p->pid = last_pid;
5 p->father = current->pid;
6 p->counter = p->priority;
7
8 long *krnstack;
9 krnstack = (long)(PAGE_SIZE +(long)p);
10 *(--krnstack) = ss & 0xffff;
11 *(--krnstack) = esp;
12 *(--krnstack) = eflags;
13 *(--krnstack) = cs & 0xffff;
14 *(--krnstack) = eip;
15 *(--krnstack) = ds & 0xffff;
16 *(--krnstack) = es & 0xffff;
17 *(--krnstack) = fs & 0xffff;
18 *(--krnstack) = gs & 0xffff;
19 *(--krnstack) = esi;
20 *(--krnstack) = edi;
21 *(--krnstack) = edx;
22 *(--krnstack) = (long)first_return_from_kernel;
23 *(--krnstack) = ebp;
24 *(--krnstack) = ecx;
25 *(--krnstack) = ebx;
26 *(--krnstack) = 0;
27 p->kernelstack = krnstack;
28 ......
29 }
最後,注意由於switch_to()和first_return_from_kernel都是在system_call.s中實現的,要想在schedule.c和fork.c中呼叫它們,就必須在system_call.s中將這兩個標號宣告為全域性的,同時在引用到它們的.c檔案中宣告它們是一個外部變數。
具體程式碼如下:
system_call.s中的全域性宣告
1 .globl switch_to
2 .globl first_return_from_kernel
對應.c檔案中的外部變數宣告:
1 extern long switch_to;
2 extern long first_return_from_kernel;
1. 原有的基於TSS的任務切換的不足
原有的Linux 0.11採用基於TSS和一條指令,雖然簡單,但這指令的執行時間卻很長,在實現任務切換時大概需要200多個時鐘週期。而通過堆疊實現任務切換可能要快,而且採用堆疊的切換還可以使用指令流水的並行化優化技術,同時又使得CPU的設計變得簡單。所以無論是Linux還是Windows,程序/執行緒的切換都沒有使用Intel 提供的這種TSS切換手段,而都是通過堆疊實現的。
2. 程序切換的六段論
基於核心棧實現程序切換的基本思路:當程序由使用者態進入核心時,會引起堆疊切換,使用者態的資訊會壓入到核心棧中,包括此時使用者態執行的指令序列EIP。由於某種原因,該程序變為阻塞態,讓出CPU,重新引起排程時,作業系統會找到新的程序的PCB,並完成該程序與新程序PCB的切換。如果我們將核心棧和PCB關聯起來,讓作業系統在進行PCB切換時,也完成核心棧的切換,那麼當中斷返回時,執行IRET
指令時,彈出的就是新程序的EIP,從而跳轉到新程序的使用者態指令序列執行,也就完成了程序的切換。這個切換的核心是構建出核心棧的樣子,要在適當的地方壓入適當的返回地址,並根據核心棧的樣子,編寫相應的彙編程式碼,精細地完成核心棧的入棧和出棧操作,在適當的地方彈出適當的返回地址,以保證能順利完成程序的切換。同時完成核心棧和PCB的關聯,在PCB切換時,完成核心棧的切換。
2.1 中斷進入核心
-
為什麼要進入核心中去?
大家都知道,作業系統負責程序的排程與切換,所以程序的切換一定是在核心中發生的。要實現程序切換,首先就要進入核心。而使用者程式都是執行在使用者態的,在Linux中,應用程式訪問核心唯一的方法就是系統呼叫,應用程式通過作業系統提供的若干系統呼叫函式訪問核心,而該程序在核心中執行時,可能因為要訪問磁碟檔案或者由於時間片耗完而變為阻塞態,從而引起排程,讓出CPU的使用權。 -
從使用者態進入核心態,要發生堆疊的切換
系統呼叫的核心是指令int 0x80
這個系統呼叫中斷。一個程序在執行時,會有函式間的呼叫和變數的儲存,而這些都是依靠堆疊完成的。程序在使用者態執行時有使用者棧,在核心態執行時有核心棧,所以當執行系統呼叫中斷int 0x80
從使用者態進入核心態時,一定會發生棧的切換。而這裡就不得不提到TSS的一個重要作用了。程序核心棧線上性地址空間中的地址是由該任務的TSS段中的ss0和esp0兩個欄位指定的,依靠TR暫存器就可以找到當前程序的TSS。也就是說,當從使用者態進入核心態時,CPU會自動依靠TR暫存器找到當前程序的TSS,然後根據裡面ss0和esp0的值找到核心棧的位置,完成使用者棧到核心棧的切換。TSS是溝通使用者棧和核心棧的關鍵橋樑,這一點在改寫成基於核心棧切換的程序切換中相當重要! -
從使用者態進入核心發生了什麼?
當執行int 0x80
這條語句時由使用者態進入核心態時,CPU會自動按照SS、ESP、EFLAGS、CS、EIP的順序,將這幾個暫存器的值壓入到核心棧中,由於執行int 0x80
時還未進入核心,所以壓入核心棧的這五個暫存器的值是使用者態時的值,其中EIP為int 0x80
的下一條語句"=a" (__res)
,這條語句的含義是將eax所代表的暫存器的值放入到_res變數中。所以當應用程式在核心中返回時,會繼續執行 “=a” (__res) 這條語句。這個過程完成了程序切換中的第一步,通過在核心棧中壓入使用者棧的ss、esp建立了使用者棧和核心棧的聯絡,形象點說,即在使用者棧和核心棧之間拉了一條線,形成了一套棧。 -
核心棧的具體樣子
父程序核心棧的樣子
執行int 0x80
將SS、ESP、EFLAGS、CS、EIP入棧。
在system_call中將DS、ES、FS、EDX、ECX、EBX入棧。
system_call:
cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
在system_call中執行完相應的系統呼叫sys_call_xx後,又將函式的返回值eax壓棧。若引起排程,則跳轉執行reschedule。否則則執行ret_from_sys_call。
1 reschedule:
2 pushl $ret_from_sys_call
3 jmp schedule
在執行schedule前將ret_from_sys_call壓棧,因為schedule是c函式,所以在c函式末尾的}
,相當於ret
指令,將會彈出ret_from_sys_call作為返回地址,跳轉到ret_from_sys_call執行。
總之,在系統呼叫結束後,將要中斷返回前,核心棧的樣子如下:
核心棧 |
---|
SS |
ESP |
EFLAGS |
CS |
EIP |
DS |
ES |
FS |
EDX |
ECX |
EBX |
EAX |
ret_from_sys_call |
2.2 找到當前程序的PCB和新程序的PCB
- 當前程序的PCB
當前程序的PCB是用一個全域性變數current指向的(在sched.c中定義) ,所以current即指向當前程序的PCB - 新程序的PCB
為了得到新程序的PCB,我們需要對schedule()函式做如下修改:
void schedule(void)
{
int i,next,c;
struct task_struct *pnext = &(init_task.task);
struct task_struct ** p; /* add */
......
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter,next = i,pnext=*p;
} /* edit */
if (c) break;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
switch_to(pnext,_LDT(next)); /* edit */
}
這樣,pnext就指向下個程序的PCB。
在schedule()函式中,當呼叫函式switch_to(pent, _LDT(next))時,會依次將返回地址}、引數2 _LDT(next)、引數1 pnext壓棧。當執行switch_to的返回指令ret
時,就回彈出schedule()函式的}執行schedule()函式的返回指令}
。關於執行switch_to時核心棧的樣子,在後面改寫switch_to函式時十分重要。
此處將跳入到switch_to中執行時,核心棧的樣子如下:
核心棧 |
---|
SS |
ESP |
EFLAGA |
CS |
EIP |
DS |
ES |
FS |
EDX |
ECX |
EBX |
EAX |
ret_from_sys_call |
pnext |
_LDT(next) |
} |
2.3 完成PCB的切換
2.4 根據PCB完成核心棧的切換
2.5 切換執行資源LDT
這些工作都將有改寫後的switch_to完成。
將Linux 0.11中原有的switch_to實現去掉,寫成一段基於堆疊切換的程式碼。由於要對核心棧進行精細的操作,所以需要用匯編程式碼來實現switch_to的編寫,既然要用匯編來實現switch_to,那麼將switch_to的實現放在system_call.s中是最合適的。這個函式依次主要完成如下功能:由於是c語言調用匯編,所以需要首先在彙編中處理棧幀,即處理ebp暫存器;接下來要取出表示下一個程序PCB的引數,並和current做一個比較,如果等於current,則什麼也不用做;如果不等於current,就開始程序切換,依次完成PCB的切換、TSS中的核心棧指標的重寫、核心棧的切換、LDT的切換以及PC指標(即CS:EIP)的切換。
switch_to(system_call.s)的基本框架如下:
1 switch_to:
2 pushl %ebp
3 movl %esp,%ebp
4 pushl %ecx
5 pushl %ebx
6 pushl %eax
7 movl 8(%ebp),%ebx
8 cmpl %ebx,current
9 je 1f
10 切換PCB
11 TSS中的核心棧指標的重寫
12 切換核心棧
13 切換LDT
14 movl $0x17,%ecx
15 mov %cx,%fs
16 cmpl %eax,last_task_used_math //和後面的cuts配合來處理協處理器,由於和主題關係不大,此處不做論述
17 jne 1f
18 clts
19 1: popl %eax
20 popl %ebx
21 popl %ecx
22 popl %ebp
23 ret
理解上述程式碼的核心,是理解棧幀結構和函式呼叫時控制轉移權方式。
大多數CPU上的程式實現使用棧來支援函式呼叫操作。棧被用來傳遞函式引數、儲存返回地址、臨時儲存暫存器原有值以備恢復以及用來儲存區域性資料。單個函式呼叫操作所使用的棧部分被稱為棧幀結構,其通常結構如下:
棧幀結構的兩端由兩個指標來指定。暫存器ebp通常用作幀指標,而esp則用作棧指標。在函式執行過程中,棧指標esp會隨著資料的入棧和出棧而移動,因此函式中對大部分資料的訪問都基於幀指標ebp進行。
對於函式A呼叫函式B的情況,傳遞給B的引數包含在A的棧幀中。當A呼叫B時,函式A的返回地址(呼叫返回後繼續執行的指令地址)被壓入棧中,棧中該位置也明確指明瞭A棧幀的結束處。而B的棧幀則從隨後的棧部分開始,即圖中儲存幀指標(ebp)的地方開始。再隨後則用來存放任何儲存的暫存器值以及函式的臨時值。
所以執行完指令pushl %eax
後,核心棧的樣子如下:
switch_to中指令movl 8(%ebp),%ebx
即取出引數2_LDT(next)放入暫存器ebx中,而12(%ebp)則是指引數1penxt。
- 完成PCB的切換
1 movl %ebx,%eax
2 xchgl %eax,current
- TSS中的核心棧指標的重寫
如前所述,當從使用者態進入核心態時,CPU會自動依靠TR暫存器找到當前程序的TSS,然後根據裡面ss0和esp0的值找到核心棧的位置,完成使用者棧到核心棧的切換。所以仍需要有一個當前TSS,我們需要在schedule.c中定義struct tss_struct *tss=&(init_task.task.tss)
這樣一個全域性變數,即0號程序的tss,所有程序都共用這個tss,任務切換時不再發生變化。
雖然所有程序共用一個tss,但不同程序的核心棧是不同的,所以在每次程序切換時,需要更新tss中esp0的值,讓它指向新的程序的核心棧,並且要指向新的程序的核心棧的棧底,即要保證此時的核心棧是個空棧,幀指標和棧指標都指向核心棧的棧底。
這是因為新程序每次中斷進入核心時,其核心棧應該是一個空棧。為此我們還需要定義:ESP0 = 4
,這是TSS中核心棧指標esp0的偏移值,以便可以找到esp0。具體實現程式碼如下:
1 movl tss,%ecx
2 addl $4096,%ebx
3 movl %ebx,ESP0(%ecx)
- 核心棧的切換
Linux 0.11的PCB定義中沒有儲存核心棧指標這個域(kernelstack),所以需要加上,而巨集KERNEL_STACK就是你加的那個位置的偏移值,當然將kernelstack域加在task_struct中的哪個位置都可以,但是在某些彙編檔案中(主要是在system_call.s中)有些關於操作這個結構一些彙編硬編碼,所以一旦增加了kernelstack,這些硬編碼需要跟著修改,由於第一個位置,即long state出現的彙編硬編碼很多,所以kernelstack千萬不要放置在task_struct中的第一個位置,當放在其他位置時,修改system_call.s中的那些硬編碼就可以了。
在schedule.h中將struct task_struct修改如下:
1 struct task_struct {
2 long state;
3 long counter;
4 long priority;
5 long kernelstack;
6 ......
7 }
同時在system_call.s中定義`KERNEL_STACK = 12` 並且修改彙編硬編碼,修改程式碼如下:
1 ESP0 = 4
2 KERNEL_STACK = 12
3
4 ......
5
6 state = 0 # these are offsets into the task-struct.
7 counter = 4
8 priority = 8
9 kernelstack = 12
10 signal = 16
11 sigaction = 20 # MUST be 16 (=len of sigaction)
12 blocked = (37*16)
switch_to中的實現程式碼如下:
1 movl %esp,KERNEL_STACK(%eax)
2 movl 8(%ebp),%ebx
3 movl KERNEL_STACK(%ebx),%esp
由於這裡將PCB結構體的定義改變了,所以在產生0號程序的PCB初始化時也要跟著一起變化,需要在schedule.h中做如下修改:
1 #define INIT_TASK \
2 /* state etc */ { 0,15,15,PAGE_SIZE+(long)&init_task,\
3 /* signals */ 0,{{},},0, \
4 ......
5 }
- LDT的切換
switch_to中實現程式碼如下:
1 movl 12(%ebp),%ecx
2 lldt %cx
一旦修改完成,下一個程序在執行使用者態程式時使用的對映表就是自己的LDT表了,地址分離實現了。
2.6 利用IRET指令完成使用者棧的切換
- PC的切換
對於被切換出去的程序,當它再次被排程執行時,根據被切換出去的程序的核心棧的樣子,switch_to的最後一句指令ret
會彈出switch_to()後面的指令}
作為返回返回地址繼續執行,從而執行}
從schedule()函式返回,將彈出ret_from_sys_call
作為返回地址執行ret_from_sys_call,在ret_from_sys_call中進行一些處理,最後執行iret
指令,進行中斷返回,將彈出原來使用者態程序被中斷地方的指令作為返回地址,繼續從被中斷處執行。
對於得到CPU的新的程序,我們要修改fork.c中的copy_process()函式,將新的程序的核心棧填寫成能進行PC切換的樣子。根據實驗提示,我們可以得到新程序的核心棧的樣子,如圖所示:
注意此處需要和switch_to接在一起考慮,應該從“切換核心棧”完事的那個地方開始,現在到子程序的核心棧開始工作了,接下來做的四次彈棧以及ret處理使用的都是子程序核心棧中的東西。
注意執行ret指令時,這條指令要從核心棧中彈出一個32位數作為EIP跳去執行,所以需要弄出一個個函式地址(仍然是一段彙編程式,所以這個地址是這段彙編程式開始處的標號)並將其初始化到棧中。既然這裡也是一段彙編程式,那麼放在system_call.s中是最合適的。我們弄的一個名為first_return_from_kernel的彙編標號,將這個地址初始化到子程序的核心棧中,現在執行ret以後就會跳轉到first_return_from_kernel去執行了。
system_call.s中switch_to的完整程式碼如下:
1 .align 2
2 switch_to:
3 pushl %ebp
4 movl %esp,%ebp
5 pushl %ecx
6 pushl %ebx
7 pushl %eax
8 movl 8(%ebp),%ebx
9 cmpl %ebx,current
10 je 1f
11 movl %ebx,%eax
12 xchgl %eax,current
13 movl tss,%ecx
14 addl $4096,%ebx
15 movl %ebx,ESP0(%ecx)
16 movl %esp,KERNEL_STACK(%eax)
17 movl 8(%ebp),%ebx
18 movl KERNEL_STACK(%ebx),%esp
19 movl 12(%ebp),%ecx
20 lldt %cx
21 movl $0x17,%ecx
22 mov %cx,%fs
23 cmpl %eax,last_task_used_math
24 jne 1f
25 clts
26 1:
27 popl %eax
28 popl %ebx
29 popl %ecx
30 popl %ebp
31 ret
system_call.s中first_return_from_kernel程式碼如下:
1 .align 2
2 first_return_from_kernel:
3 popl %edx
4 popl %edi
5 popl %esi
6 pop %gs
7 pop %fs
8 pop %es
9 pop %ds
10 iret
fork.c中copy_process()的具體修改如下:
1 ......
2 p = (struct task_struct *) get_free_page();
3 ......
4 p->pid = last_pid;
5 p->father = current->pid;
6 p->counter = p->priority;
7
8 long *krnstack;
9 krnstack = (long)(PAGE_SIZE +(long)p);
10 *(--krnstack) = ss & 0xffff;
11 *(--krnstack) = esp;
12 *(--krnstack) = eflags;
13 *(--krnstack) = cs & 0xffff;
14 *(--krnstack) = eip;
15 *(--krnstack) = ds & 0xffff;
16 *(--krnstack) = es & 0xffff;
17 *(--krnstack) = fs & 0xffff;
18 *(--krnstack) = gs & 0xffff;
19 *(--krnstack) = esi;
20 *(--krnstack) = edi;
21 *(--krnstack) = edx;
22 *(--krnstack) = (long)first_return_from_kernel;
23 *(--krnstack) = ebp;
24 *(--krnstack) = ecx;
25 *(--krnstack) = ebx;
26 *(--krnstack) = 0;
27 p->kernelstack = krnstack;
28 ......
29 }
最後,注意由於switch_to()和first_return_from_kernel都是在system_call.s中實現的,要想在schedule.c和fork.c中呼叫它們,就必須在system_call.s中將這兩個標號宣告為全域性的,同時在引用到它們的.c檔案中宣告它們是一個外部變數。
具體程式碼如下:
system_call.s中的全域性宣告
1 .globl switch_to
2 .globl first_return_from_kernel
對應.c檔案中的外部變數宣告:
1 extern long switch_to;
2 extern long first_return_from_kernel;