实验报告
1. 实验内容
修改进程切换方式,由TSS模式,切换到栈模式。
2. 实验步骤
-
修改schd.c 中的switch_to 函数;
原先此函数中使用ljmp到tr寄存器指定的描述符中获取tss结构体的的地址,并map的寄存器中(包括CS:IP,SS:SP,AX,CX,DX…);
现在使用栈模式即需使在代码中手动将esp由旧的内核栈地址切换为新栈地址,然后pop出相应寄存器,直到完成ret指令,从新栈中获取新进程的eip,cpu才执行新进程。另外定义了一个tss的全局结构体变量,用以在cpu执行int 80 时寻找到内核栈,因为cpu总是根据当前tr获取tss描述符,再找到内存中tss位置获得栈,因为switch时,需要重置tss结构体中内核栈地址。 具体步骤为- Push BP,CX,BX,AX寄存器
- 切换PCB
- TSS中的内核栈指针的重写
- 切换内核栈
- 切换LDT
- POP AX,BX,CX,BP寄存器
- Ret
-
修改Fork函数
- 增加Kernal Stack 指针
- 去除task中tss结构体的赋值
- 拷贝将父进程中的用户态寄存器信息[SS,ESP,EFLAGS,CS,IP]
- Push first_return_from_kernel 标号地址
- push AX,BX,CX,BP寄存器
-
修改task 0 初始值,增加结构体中对应的stack的地址
-
增加tss0全局结构体变量
3. 代码修改
switch_to
stack_switch_to: pushl %ebp movl %esp,%ebp pushl %ecx pushl %ebx pushl %eax pushl %es pushl %gs pushl %esi pushl %edi pushl %edx movl 8(%ebp),%ebx cmpl %ebx,current je 1f movl %ebx,%eax #exchange pcb xchgl %eax,current movl tss,%ecx addl $4096,%ebx movl %ebx,ESP0(%ecx)# rewrite the tss0->sp movl %esp,KERNEL_STACK(%eax) # eax pointer to previous stack movl 8(%ebp),%ebx movl KERNEL_STACK(%ebx),%esp # #exahcge stack movl 12(%ebp),%ecx # load ldt addr lldt %cx #load ldt to ldtr movl $0x17,%ecx mov %cx,%fs cmpl %eax,last_task_used_math jne 1f clts 1:popl %edx popl %edi popl %esi popl %gs popl %es popl %eax popl %ebx popl %ecx popl %ebp ret
fork.c
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, long ebx,long ecx,long edx, long fs,long es,long ds, long eip,long cs,long eflags,long esp,long ss) { struct task_struct *p; int i; struct file *f; long* krnstack; p = (struct task_struct *) get_free_page(); if (!p) return -EAGAIN; fprintk(3, "%ld\t%c\t%ld\n", last_pid, 'N', jiffies); task[nr] = p; *p = *current; /* NOTE! this doesn't copy the supervisor stack */ p->state = TASK_UNINTERRUPTIBLE; p->pid = last_pid; p->father = current->pid; p->counter = p->priority; p->signal = 0; p->alarm = 0; p->leader = 0; /* process leadership doesn't inherit */ p->utime = p->stime = 0; p->cutime = p->cstime = 0; p->start_time = jiffies; if (last_task_used_math == current) __asm__("clts ; fnsave %0"::"m" (p->tss.i387)); if (copy_mem(nr,p)) { task[nr] = NULL; free_page((long) p); return -EAGAIN; } // rearrange stack for usermode krnstack = (long*)(PAGE_SIZE + (long)p);//stack pionter *(--krnstack) = ss & 0xffff; *(--krnstack) = esp; *(--krnstack) = eflags; *(--krnstack) = cs & 0xffff; *(--krnstack) = eip; // stack for schedule *(--krnstack) = ds & 0xffff; *(--krnstack) = es & 0xffff; *(--krnstack) = fs & 0xffff; *(--krnstack) = gs & 0xffff; *(--krnstack) = esi; *(--krnstack) = edi; *(--krnstack) = edx; *(--krnstack) = first_return_from_kernel; *(--krnstack) = ebp; *(--krnstack) = ecx; *(--krnstack) = ebx; *(--krnstack) = 0; //0 for res *(--krnstack) = es; *(--krnstack) = gs; *(--krnstack) = esi; *(--krnstack) = edi; *(--krnstack) = edx; p->kernelstack = krnstack; for (i=0; i<NR_OPEN;i++) if ((f=p->filp[i])) f->f_count++; if (current->pwd) current->pwd->i_count++; if (current->root) current->root->i_count++; if (current->executable) current->executable->i_count++; set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss)); set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt)); p->state = TASK_RUNNING; /* do this last, just in case */ fprintk(3, "%ld\t%c\t%ld\n", p->pid, 'J', jiffies); return last_pid; }
first_return_from_kernel
first_return_from_kernel: popl %edx popl %edi popl %esi pop %gs pop %fs pop %es pop %ds iret
sched.h
INIT_TASK \ /* state etc */ { 0,15,15,PAGE_SIZE+(long)&init_task, \ /* signals */ 0,{{},},0, \ /* ec,brk... */ 0,0,0,0,0,0, \ /* pid etc.. */ 0,-1,0,0,0, \ /* uid etc */ 0,0,0,0,0,0, \ /* alarm */ 0,0,0,0,0,0, \ /* math */ 0, \ /* fs info */ -1,0022,NULL,NULL,NULL,0, \ /* filp */ {NULL,}, \ { \ {0,0}, \ /* ldt */ {0x9f,0xc0fa00}, \ {0x9f,0xc0f200}, \ }, \ /*tss*/ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\ 0,0,0,0,0,0,0,0, \ 0,0,0x17,0x17,0x17,0x17,0x17,0x17, \ _LDT(0),0x80000000, \ {} \ }, \ }
4. 实验问题
问题 1
针对下面的代码片段:
movl tss,%ecx
addl $4096,%ebx
movl %ebx,ESP0(%ecx)
回答问题:
- (1)为什么要加 4096;
- (2)为什么没有设置 tss 中的 ss0。
(1)这里ebx指向的是下一个进程的PCB。加上4096后,即为一个进程分配4K的空间,栈顶即为内核堆栈的指针,栈底为进程的PCB。
(2)因为进程的切换不靠tss进行,但CPU的机制造成对每个进程仍然会有TR寄存器、tss的设置等内容,所以可以让所有进程都共用tss0的空间。所以不需要设置tss0。
问题 2
针对代码片段:
*(--krnstack) = ebp;
*(--krnstack) = ecx;
*(--krnstack) = ebx;
*(--krnstack) = 0;
回答问题:
- (1)子进程第一次执行时,eax=?为什么要等于这个数?哪里的工作让 eax 等于这样一个数?
- (2)这段代码中的 ebx 和 ecx 来自哪里,是什么含义,为什么要通过这些代码将其写到子进程的内核栈中?
- (3)这段代码中的 ebp 来自哪里,是什么含义,为什么要做这样的设置?可以不设置吗?为什么?
(1)这里eax等于0,即返回值。这样设置就可以用if(!fork())来判断是不是子进程。最后一行完成了这件事。
(2)ebx、ecx都来自copy_process函数,存放的是父进程的ebx和ecx。通过将父进程的参数压入栈中,可以保证子进程回到用户态运行时具有和父进程相同的环境。
(3)ebp也来自copy_process函数,存放的是父进程的用户栈指针。即在fork刚刚执行完copy_process的时候,它的用户栈是父进程的用户栈,而非自己的用户栈。当子进程进行其他操作时,造成需要的栈将要与父进程不同了,才会创建自己的用户栈。这么做的好处时当一些子进程什么都不做,系统不用分配额外的空间。当然也可以一创建子进程就为它分配一个新的栈,esp指向新的栈顶。
问题 3
为什么要在切换完 LDT 之后要重新设置 fs=0x17?而且为什么重设操作要出现在切换完 LDT 之后,出现在 LDT 之前又会怎么样?
3.切换LDT时,会切换进程使用的用户栈。此时,会利用fs查找它指向的描述符表,并且取出隐藏部分:段基址与段限长放在cs。如果查完表不重新赋值,下次查表的时候就还会查这个表,不能起到切换LDT的作用。放在切完LDT是因为,switch_to之后下一个进程可能会查其他表,不在切完LDT的时候赋值就会影响其他表的查找。
5.实验结果
6.实验收获
通过本次实验,我基本上了解了如何将TSS切换部分去掉,取而代之的是使用基于堆栈的切换程序,可以在不使用ljmp指令在实现任务切换时大概需要200多个时钟周期。而通过堆栈的切换还可以使用指令流水的并行优化技术,同时还可以使得CPU的设计变得简单。采用堆栈的切换可以更快。
感觉在实验楼上做实验还是要举一反三,感觉提供的样例操作还是存在一些问题,在询问同学和自行百度以后,解决了部分问题,最终完成了实验,得到了实验结果。