基于内核栈切换的进程切换
一, 试验内容
修改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