国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 系統 > Linux > 正文

在Linux-0.11中實現基于內核棧切換的進程切換

2024-06-28 13:19:58
字體:
來源:轉載
供稿:網友
在linux-0.11中實現基于內核棧切換的進程切換
    • 原有的基于TSS的任務切換的不足
    • 進程切換的六段論
      • 1 中斷進入內核
      • 2 找到當前進程的PCB和新進程的PCB
      • 3 完成PCB的切換
      • 4 根據PCB完成內核棧的切換
      • 5 切換運行資源LDT
      • 6 利用IRET指令完成用戶棧的切換

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時還未進入內核,所以壓入內核棧的這五個寄存器的值是用戶態時的值,其中EIPint 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_call3     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 1f10     切換PCB11     TSS中的內核棧指針的重寫12     切換內核棧13     切換LDT14     movl $0x17,%ecx15     mov %cx,%fs16     cmpl %eax,last_task_used_math    //和后面的cuts配合來處理協處理器,由于和主題關系不大,此處不做論述17     jne 1f18     clts19 1:  popl %eax20     popl %ebx21     popl %ecx22     popl %ebp23     ret
理解上述代碼的核心,是理解棧幀結構和函數調用時控制轉移權方式。

大多數CPU上的程序實現使用棧來支持函數調用操作。棧被用來傳遞函數參數、存儲返回地址、臨時保存寄存器原有值以備恢復以及用來存儲局部數據。單個函數調用操作所使用的棧部分被稱為棧幀結構,其通常結構如下: 棧幀 棧幀結構的兩端由兩個指針來指定。寄存器ebp通常用作幀指針,而esp則用作棧指針。在函數執行過程中,棧指針esp會隨著數據的入棧和出棧而移動,因此函數中對大部分數據的訪問都基于幀指針ebp進行。 對于函數A調用函數B的情況,傳遞給B的參數包含在A的棧幀中。當A調用B時,函數A的返回地址(調用返回后繼續執行的指令地址)被壓入棧中,棧中該位置也明確指明了A棧幀的結束處。而B的棧幀則從隨后的棧部分開始,即圖中保存幀指針(ebp)的地方開始。再隨后則用來存放任何保存的寄存器值以及函數的臨時值。

所以執行完指令pushl %eax后,內核棧的樣子如下: 執行到switch_to的樣子 switch_to中指令movl 8(%ebp),%ebx即取出參數2_LDT(next)放入寄存器ebx中,而12(%ebp)則是指參數1penxt。

  • 完成PCB的切換
1 movl %ebx,%eax2 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,%ecx2 addl $4096,%ebx3 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 = 1210 signal  = 1611 sigaction = 20      # MUST be 16 (=len of sigaction)12 blocked = (37*16)

switch_to中的實現代碼如下:
1 movl %esp,KERNEL_STACK(%eax)2 movl 8(%ebp),%ebx3 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),%ecx2 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,current10     je 1f11     movl %ebx,%eax12     xchgl %eax,current13     movl tss,%ecx14     addl $4096,%ebx15     movl %ebx,ESP0(%ecx)16     movl %esp,KERNEL_STACK(%eax)17     movl 8(%ebp),%ebx18     movl KERNEL_STACK(%ebx),%esp19     movl 12(%ebp),%ecx  20     lldt %cx21     movl $0x17,%ecx22     mov %cx,%fs23     cmpl %eax,last_task_used_math24     jne 1f25     clts26 1:27     popl %eax28     popl %ebx29     popl %ecx30     popl %ebp31     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 %ds10     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_to2 .globl first_return_from_kernel

對應.c文件中的外部變量聲明:

1 extern long switch_to;2 extern long first_return_from_kernel;


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 绥江县| 连州市| 循化| 桦南县| 新巴尔虎左旗| 乌审旗| 英德市| 南城县| 高邑县| 桑植县| 红桥区| 通化市| 泸溪县| 江达县| 图木舒克市| 洛阳市| 旬阳县| 丰台区| 杂多县| 北川| 宝坻区| 淅川县| 房山区| 静安区| 疏勒县| 铜陵市| 安阳市| 吉安市| 湟中县| 大城县| 象山县| 天水市| 克拉玛依市| 日土县| 江华| 上蔡县| 枣阳市| 封丘县| 河曲县| 长治县| 乐平市|