一, 試驗內容linux
修改fork(), switch(), PCB結構等把linux 0.11的基於tss切換的進程切換改爲基於內核棧的進程切換編程
二, 實驗步驟數組
1, 重寫switch_to()函數函數
目前Linux 0.11中工做的schedule()函數是首先找到下一個進程的數組位置next,而這個next就是GDT中的n,因此這個next是用來找到切換後目標TSS段的段描述符的,一旦得到了這個next值,直接調用上面剖析的那個宏展開switch_to(next);就能完成如圖TSS切換所示的切換了。如今,咱們不用TSS進行切換,而是採用切換內核棧的方式來完成進程切換,因此在新的switch_to中將用到當前進程的PCB、目標進程的PCB、當前進程的內核棧、目標進程的內核棧等信息。因爲Linux 0.11進程的內核棧和該進程的PCB在同一頁內存上(一塊4KB大小的內存),其中PCB位於這頁內存的低地址,棧位於這頁內存的高地址;另外,因爲當前進程的PCB是用一個全局變量current指向的,因此只要告訴新switch_to()函數一個指向目標進程PCB的指針就能夠了。同時還要將next也傳遞進去,雖然TSS(next)再也不須要了,可是LDT(next)仍然是須要的,也就是說,如今每一個進程不用有本身的TSS了,由於已經不採用TSS進程切換了,可是每一個進程須要有本身的LDT,地址分離地址仍是必需要有的,而進程切換必然要涉及到LDT的切換。this
綜上所述,須要將目前的schedule()函數作稍許修改,即將下面的代碼:編碼
1 void schedule(void) 2 { 3 int i,next,c; 4 struct task_struct ** p; 5 6 /* check alarm, wake up any interruptible tasks that have got a signal */ 7 8 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) 9 if (*p) { 10 if ((*p)->alarm && (*p)->alarm < jiffies) { 11 (*p)->signal |= (1<<(SIGALRM-1)); 12 (*p)->alarm = 0; 13 } 14 if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && 15 (*p)->state==TASK_INTERRUPTIBLE) 16 (*p)->state=TASK_RUNNING; 17 } 18 19 /* this is the scheduler proper: */ 20 21 while (1) { 22 c = -1; 23 next = 0; 24 i = NR_TASKS; 25 p = &task[NR_TASKS]; 26 while (--i) { 27 if (!*--p) 28 continue; 29 if ((*p)->state == TASK_RUNNING && (*p)->counter > c) 30 c = (*p)->counter, next = i; 31 } 32 if (c) break; 33 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) 34 if (*p) 35 (*p)->counter = ((*p)->counter >> 1) + 36 (*p)->priority; 37 } 38 switch_to(next); 39 }
修改成:spa
1 void schedule(void) 2 { 3 int i,next,c; 4 struct task_struct ** p; 5 struct task_struct *pnext = NULL; // 添加的代碼 6 7 /* check alarm, wake up any interruptible tasks that have got a signal */ 8 9 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) 10 if (*p) { 11 if ((*p)->alarm && (*p)->alarm < jiffies) { 12 (*p)->signal |= (1<<(SIGALRM-1)); 13 (*p)->alarm = 0; 14 } 15 if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && 16 (*p)->state==TASK_INTERRUPTIBLE) 17 (*p)->state=TASK_RUNNING; 18 } 19 20 /* this is the scheduler proper: */ 21 22 while (1) { 23 c = -1; 24 next = 0; 25 pnext = task[next]; // 添加的代碼. 若是系統沒有進程能夠調度時傳遞進去的是一個空值,系統宕機,因此加上這句,這樣就能夠在next=0時不會有空指針傳遞 26 i = NR_TASKS; 27 p = &task[NR_TASKS]; 28 while (--i) { 29 if (!*--p) 30 continue; 31 if ((*p)->state == TASK_RUNNING && (*p)->counter > c) 32 c = (*p)->counter, next = i, pnext = *p; 33 } 34 if (c) break; 35 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) 36 if (*p) 37 (*p)->counter = ((*p)->counter >> 1) + 38 (*p)->priority; 39 } 40 41 switch_to(pnext, _LDT(next)); // 修改的代碼 42 }
2, 實現switch_to()函數指針
原來的switch_to()函數在include/linux/kernel.h中經過宏來實現的. 先把原來的switch_to()刪去. 因爲要對內核棧進行精細的操做,因此須要用匯編代碼來完成函數switch_to的編寫,這個函數依次主要完成以下功能:因爲是C語言調用匯編,因此須要首先在彙編中處理棧幀,即處理ebp寄存器;接下來要取出表示下一個進程PCB的參數,並和current作一個比較,若是等於current,則什麼也不用作;若是不等於current,就開始進程切換,依次完成PCB的切換、TSS中的內核棧指針的重寫、內核棧的切換、LDT的切換PC指針(即CS:EIP)的切換, 修改fs寄存器等code
(1) PCB的切換: switch_to()函數的第一個實參就是指向要切換的進程的PCB的, 而當前進程的PCB的指針被保存在了全局變量current中, 因此只要把這兩個指針的值交換一下就好了.blog
(2) TSS中的內核棧指針的重寫: 前面已經詳細論述過,在中斷的時候,要找到內核棧位置,並將用戶態下的SS:ESP,CS:EIP以及EFLAGS這五個寄存器壓到內核棧中,這是溝通用戶棧(用戶態)和內核棧(內核態)的關鍵橋樑,而找到內核棧位置就依靠TR指向的當前TSS。如今雖然不使用TSS進行任務切換了,可是Intel的這套中斷處理機制還要保持,因此仍然須要有一個當前TSS. 因此還須要定義一個全局變量struct tss_struct *tss = &(init_task.task.tss);, 全部進程都共用這個tss,任務切換時再也不發生變化.
(3) 內核棧的切換: 因爲如今的Linux 0.11的PCB定義中沒有保存內核棧指針這個域(kernelstack),因此須要在include/linux/sched.h中的task_struct結構定義中加上這個域.另外在一些彙編程序中有些關於操做這個結構的一些彙編硬編碼,因此一旦增長了kernelstack,這些硬編碼須要跟着修改, 因此應該講這個域放到合適的位置以免最少的修改. 經過分析, 放到第4個位置比較好, 這樣就只須要修改system_call.s中的signal, sigaction和blocked這三個值便可, 這三個值就分別表示task_struct結構體中對應值的偏移量. 因爲將PCB結構體的定義改變了,因此在產生0號進程的PCB初始化時也要跟着一塊兒變化,因此須要將原來的#define INIT_TASK { 0,15,15, 0,{{},},0,...修改成#define INIT_TASK { 0,15,15,PAGE_SIZE+(long)&init_task, 0,{{},},0,...,即在PCB的第四項中增長關於內核棧棧指針的初始化。
(4) LDT的切換: 這個很簡單, 只須要將棧中的ltd值經過lldt指令切換一下就能夠了.
(5) PC指針(即CS:EIP)的切換: 這個不須要添加指令, 最後加上一個ret指令便可一步一步地從棧中切換
(6) 修改fs寄存器: 因爲fs寄存器能夠在內核態訪問用戶態的內存, 因此須要修改fs寄存器. 實際上段寄存器包含兩個部分:顯式部分和隱式部分,好比jmpi 0, 8 雖然這條指令是讓cs=8,但在執行這條指令時,會在段表(GDT)中找到8對應的那個描述符表項,取出基地址和段限長,除了完成和eip的累加算出PC之外,還會將取出的基地址和段限長放在cs的隱藏部分。爲何要這樣作?下次執行jmp 100時,因爲cs沒有改過,仍然是8,因此能夠再也不去查GDT表,而是直接用其隱藏部分中的基地址0和100累加直接獲得PC,增長了執行指令的效率. 而fs也和cs同樣, 是一個選擇子,即fs是一個指向描述符表項的指針,這個描述符纔是指向實際的用戶態內存的指針,因此上一個進程和下一個進程的fs實際上都是0x17,真正找到不一樣的用戶態內存是由於兩個進程查的LDT表不同,因此這樣重置一下fs=0x17, 使fs的隱藏部分的值表示的是下一個進程的棧的位置
因此最後修改以後的switch_to函數爲:
1 ...... 2 /* 修改後的三個常量值 */ 3 signal = 16 4 sigaction = 20 5 blocked = (37*16) 6 ...... 7 8 /* 讓其它C程序能夠和switch_to函數鏈接 */ 9 .globl switch_to 10 11 switch_to: 12 pushl %ebp 13 movl %esp,%ebp 14 pushl %ecx 15 pushl %ebx 16 pushl %eax 17 movl 8(%ebp),%ebx # ebx指向要切換的進程 */ 18 cmpl %ebx,current # 若是當前進程和要切換的進程是同一個進程 */ 19 je 1f 20 21 # 切換PCB 22 movl %ebx,%eax 23 xchgl %eax,current 24 25 # TSS中的內核棧指針的重寫 26 movl tss,%ecx 27 addl $4096,%ebx # now ebx is the top of stack 28 movl %ebx,ESP0(%ecx) # let esp0 of tss is the top of stack 29 30 # 切換內核棧 31 movl %esp,KERNEL_STACK(%eax) 32 movl 8(%ebp),%ebx # 再取一下ebx,由於前面修改過ebx的值 33 movl KERNEL_STACK(%ebx),%esp 34 35 # 切換LDT 36 movl 12(%ebp),%ecx # 取出對應LDT(next)的那個參數 37 lldt %cx # 修改LDTR寄存器 38 39 movl $0x17,%ecx 40 mov %cx,%fs 41 cmpl %eax,last_task_used_math # 和後面的clts配合來處理協處理器,因爲和主題關係不大,此處不作論述 42 jne 1f 43 clts 44 1: popl %eax 45 popl %ebx 46 popl %ecx 47 popl %ebp 48 ret
3, 修改fork()
不難想象,對fork()的修改就是對子進程的內核棧的初始化,在fork()的核心實現copy_process中,p = (struct task_struct) get_free_page();用來完成申請一頁內存做爲子進程的PCB,而p指針加上頁面大小就是子進程的內核棧位置. 因此須要再定義一個指針變量krnstack, 並將其初始化爲內核棧頂指針, 而後再根據傳遞進來的參數把前一個進程的PCB中各類信息都保存到當前棧中.
最後還要考慮到如何從內核態返回到用戶態. 最後返回的時候確定是經過switch_to()函數的ret指令返回的, 可是因爲copy_process()作了不少的棧的操做, cs和ip的值並非在棧頂, 因此還須要一個first_return_from_kernel()函數來作進一步的返回操做. 具體代碼以下所示:
1 int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, 2 long ebx,long ecx,long edx, 3 long fs,long es,long ds, 4 long eip,long cs,long eflags,long esp,long ss) 5 { 6 long * krnstack; // 添加的代碼 7 8 struct task_struct *p; 9 int i; 10 struct file *f; 11 p = (struct task_struct *) get_free_page(); 12 13 krnstack = (long)(PAGE_SIZE + (long)p); // 添加的代碼 14 15 if (!p) 16 return -EAGAIN; 17 task[nr] = p; 18 *p = *current; /* NOTE! this doesn't copy the supervisor stack */ 19 p->state = TASK_UNINTERRUPTIBLE; 20 p->pid = last_pid; 21 p->father = current->pid; 22 p->counter = p->priority; 23 24 /* 添加的代碼 */ 25 *(--krnstack) = ss & 0xffff; 26 *(--krnstack) = esp; 27 *(--krnstack) = eflags; 28 *(--krnstack) = cs & 0xffff; 29 *(--krnstack) = eip; 30 31 *(--krnstack) = ds & 0xffff; 32 *(--krnstack) = es & 0xffff; 33 *(--krnstack) = fs & 0xffff; 34 *(--krnstack) = gs & 0xffff; 35 *(--krnstack) = esi; 36 *(--krnstack) = edi; 37 *(--krnstack) = edx; 38 39 *(--krnstack) = first_return_from_kernel; // 這個就是作進一步返回操做的那個函數的地址 40 41 *(--krnstack) = ebp; 42 *(--krnstack) = ecx; 43 *(--krnstack) = ebx; 44 *(--krnstack) = 0; 45 46 p->kernelstack=krnstack; //保存當前棧頂 47 /* 添加結束 */ 48 49 p->signal = 0; 50 p->alarm = 0; 51 p->leader = 0; /* process leadership doesn't inherit */ 52 p->utime = p->stime = 0; 53 p->cutime = p->cstime = 0; 54 p->start_time = jiffies; 55 p->tss.back_link = 0; 56 p->tss.esp0 = PAGE_SIZE + (long) p; 57 p->tss.ss0 = 0x10; 58 p->tss.eip = eip; 59 p->tss.eflags = eflags; 60 p->tss.eax = 0; 61 p->tss.ecx = ecx; 62 p->tss.edx = edx; 63 p->tss.ebx = ebx; 64 p->tss.esp = esp; 65 p->tss.ebp = ebp; 66 p->tss.esi = esi; 67 p->tss.edi = edi; 68 p->tss.es = es & 0xffff; 69 p->tss.cs = cs & 0xffff; 70 p->tss.ss = ss & 0xffff; 71 p->tss.ds = ds & 0xffff; 72 p->tss.fs = fs & 0xffff; 73 p->tss.gs = gs & 0xffff; 74 p->tss.ldt = _LDT(nr); 75 p->tss.trace_bitmap = 0x80000000; 76 if (last_task_used_math == current) 77 __asm__("clts ; fnsave %0"::"m" (p->tss.i387)); 78 if (copy_mem(nr,p)) { 79 task[nr] = NULL; 80 free_page((long) p); 81 return -EAGAIN; 82 } 83 for (i=0; i<NR_OPEN;i++) 84 if ((f=p->filp[i])) 85 f->f_count++; 86 if (current->pwd) 87 current->pwd->i_count++; 88 if (current->root) 89 current->root->i_count++; 90 if (current->executable) 91 current->executable->i_count++; 92 set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss)); 93 set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt)); 94 p->state = TASK_RUNNING; /* do this last, just in case */ 95 return last_pid; 96 }
first_return_from_kernel()函數能夠在system_call.s中添加:
first_return_from_kernel: popl %edx popl %edi popl %esi popl %gs popl %fs popl %es popl %ds iret