基于内核栈切换的进程切换

一, 试验内容

  修改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的切换。

  综上所述,需要将目前的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 }

  修改为:

 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寄存器等

  (1) PCB的切换: switch_to()函数的第一个实参就是指向要切换的进程的PCB的, 而当前进程的PCB的指针被保存在了全局变量current中, 所以只要把这两个指针的值交换一下就行了.

  (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

 

posted on 2015-08-25 19:26  艾翔飞  阅读(1621)  评论(0编辑  收藏  举报

导航