基於內核棧切換的進程切換

一, 試驗內容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
相關文章
相關標籤/搜索