一, 试验内容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