原有的Linux 0.11採用基於TSS和一條指令,雖然簡單,但這指令的執行時間卻很長,在實現任務切換時大概須要200多個時鐘週期。而經過堆棧實現任務切換可能要快,並且採用堆棧的切換還可使用指令流水的並行化優化技術,同時又使得CPU的設計變得簡單。因此不管是Linux仍是Windows,進程/線程的切換都沒有使用Intel 提供的這種TSS切換手段,而都是經過堆棧實現的。編程
基於內核棧實現進程切換的基本思路:當進程由用戶態進入內核時,會引發堆棧切換,用戶態的信息會壓入到內核棧中,包括此時用戶態執行的指令序列EIP。因爲某種緣由,該進程變爲阻塞態,讓出CPU,從新引發調度時,操做系統會找到新的進程的PCB,並完成該進程與新進程PCB的切換。若是咱們將內核棧和PCB關聯起來,讓操做系統在進行PCB切換時,也完成內核棧的切換,那麼當中斷返回時,執行IRET
指令時,彈出的就是新進程的EIP,從而跳轉到新進程的用戶態指令序列執行,也就完成了進程的切換。這個切換的核心是構建出內核棧的樣子,要在適當的地方壓入適當的返回地址,並根據內核棧的樣子,編寫相應的彙編代碼,精細地完成內核棧的入棧和出棧操做,在適當的地方彈出適當的返回地址,以保證能順利完成進程的切換。同時完成內核棧和PCB的關聯,在PCB切換時,完成內核棧的切換。markdown
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: 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 |
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) |
} |
這些工做都將有改寫後的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)的基本框架以下:spa
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。線程
1 movl %ebx,%eax 2 xchgl %eax,current
struct tss_struct *tss=&(init_task.task.tss)
這樣一個全局變量,即0號進程的tss,全部進程都共用這個tss,任務切換時再也不發生變化。 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中的那些硬編碼就能夠了。設計
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 }
1 movl 12(%ebp),%ecx 2 lldt %cx
一旦修改完成,下一個進程在執行用戶態程序時使用的映射表就是本身的LDT表了,地址分離實現了。
ret
會彈出switch_to()後面的指令}
做爲返回返回地址繼續執行,從而執行}
從schedule()函數返回,將彈出ret_from_sys_call
做爲返回地址執行ret_from_sys_call,在ret_from_sys_call中進行一些處理,最後執行iret
指令,進行中斷返回,將彈出原來用戶態進程被中斷地方的指令做爲返回地址,繼續從被中斷處執行。 注意此處須要和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;