操作系统实验报告1 下载本文

内容发布更新时间 : 2024/6/3 11:15:48星期一 下面是文章的全部内容请认真阅读。

实验四:基于内核栈切换的进程切换

一、实验目的

?深入理解进程和进程切换的概念;

?综合应用进程、CPU 管理、PCB、LDT、内核栈、内核态等知识解决实际问题; ?开始建立系统认识。

二、实验内容

现在的 Linux 0.11 采用 TSS和一条指令就能完成任务切换,虽然简单, 但这指令的执行时间却很长,在实现任务切换时大概需要 200 多个时钟周期。而通过堆栈 实现任务切换可能要更快,而且采用堆栈的切换还可以使用指令流水的并行优化技术,同时 又使得 CPU 的设计变得简单。所以无论是 Linux 还是 Windows,进程/线程的切换都没有使 用 Intel 提供的这种 TSS 切换手段,而都是通过堆栈实现的。

本次实践项目就是将 Linux 0.11 中采用的 TSS 切换部分去掉,取而代之的是基于堆栈的切 换程序。具体的说,就是将 Linux 0.11 中的 switch_to 实现去掉,写成一段基于堆栈切换的 代码。

本次实验包括如下内容: ?编写汇编程序 switch_to: ?完成主体框架;

?在主体框架下依次完成 PCB 切换、内核栈切换、LDT 切换等;

?修改 fork(),由于是基于内核栈的切换,所以进程需要创建出能完成内核栈切换的样子。

?修改 PCB,即 task_struct 结构,增加相应的内容域,同时处理由于修改了 task_struct 所造成的影响。

?用修改后的 Linux 0.11 仍然可以启动、可以正常使用。

四、实验过程

4.1、关于切换的分析

不管使用何种方式进行进程切换(此次实验不涉及线程),总之要实现调度进程的寄存器的保存和切换,也就是说只要有办法保存被调度出 cpu 的进程的寄存器状态及数据,再把调度的进程的寄存器状态及数据放入到 cpu 的相应寄存器中即可完成进程的切换。由于切换都是在内核态下完成的所以两个进程之间的 tss 结构中只有几个信息是不同的,其中 esp 和trace_bitmap 是必须切换的,但在 0.11 的系统中,所有进程的 bitmap 均一样,所以也可以不用切换。

调度进程的切换方式修改之前,我们考虑一个问题,进程0不是通过调度运行的,那进 程0的上下文是如何建立的?因为在进程0运行时系统中并没有其他进程,所以进程0的建立模板一定可以为进程栈切换方式有帮助。所以先来分析一下进程0的产生。进程0是在move_to_user_mode 宏之后直接进入的。在这之前一些准备工做主要是 task_struct 结构的填充。

#define move_to_user_mode() \\ __asm__ (\ \ \ \ \ \ \ \ \ \ \ \ :::\

这个宏模拟了一个中断返回,把但却没有设置 cs、ss,其原因应该是进程0代码和数据 均在内核,所以不需要设置,这样就可以知道,在使用栈切换时我们也只要模仿这种方式,

手动造一个中断返回即可。但实际上还要麻烦一点,因为进程0不通过 schedule 进行第一 次运行,但我们fork出来的新进程却要经过schedule,也就是要经过switch_to来进行调度, 所以在新 fork 的进程栈中不仅要模拟中断返回,还要为 schedule 的返回准备数据。基于此,新 fork 的进程的内核栈应该更类似于一个被调度出去的一个进程。所以我们只要把 fork 的进程的内核栈的状态手工打造成类似的样子就可以了。根据实验指导的内容,此次修改将 switch_to 宏变成函数,所以需要压栈,故相比上图要做一定的调整,因为 c 函数的调用参数是通过栈来传递的,其汇编后的代码一般如下: pushl ebp movl esp,ebp sub $x,esp

此时栈内会放置函数所用的参数,新进程并没有经过 schedule 的前一部分之直跳进来 的,所以 fork 中的进程栈中也必须要有这些参数数据。此外由于新进程也没有经过 int,所以 iret 返回时所需要的数据也要在栈内准备好。 4.2 基于栈的切换代码 本次要修改的文件如下: (1) schedule.h

这个文件中主要修改如下几处。 第一,task_struct 结构的相关部分,由于之前使用 tss 切换,而此次修改要使用栈切换,tss 结构弃之不用(只保留一个进程 0 的),所以 esp 指 针必须要保存到 task_struct 中,因此要在 task_struct 结构中添加一个新的成员 long

kernelstack; //内核栈指针,实际上此次实验的最后结果中我添加了两个,另一个是 long eip; //保存切换时使用的 EIP 指针,具体原因后面会说明。同时进程 0 的 task_struct 也要做相 应修改。老师在视频中也讲到成员添加的位置要小心,因为有些成员在系统中被硬编码了, 所以不能放在结构的最前面,也最好不要放最后面,因为那样,在汇编程序中计算新添加成 员偏移位置时就变得困难了。

此外,为了能在函数中使用汇编中的函数,这里还要声明如下几个函数。 /*添加用来 PCB 切换的函数声明*/

extern void switch_to(struct task_struct * pnext,int next); //这里的参数类型是不重要的,记得 C 语言 //的老师说过编译器只并不对此处的类型进行严格检查。 extern void

first_return_from_kernel(void); extern void first_switch_from(void); //这里也是在实验指导之外添加的

(2) schedule.c

要修改的位置就是 schedule 函数,因为要将 switch_to 宏修改为一个汇编函数,所以无 法在里面使用宏在查找 pcb 指针以及 ldt 指针,所以这两个数据均要使用参数来传递,故在

schedule 中也添加了一个新成员用来保存当前要调度进 cpu 的进程的 pcb 指针。如下: struct task_struct * pnext=NULL; //保存 PCB 指针, … while (1) { c = -1; next = 0; /*为 pnext 赋初值,让其总有值可用。*/ pnext=task[next]; //最初我并没有加这句,导致如果系统没有进程可以调度时传递进去的是一 个空值,系统宕机,所以加上这句,这样就可以在 next=0 时不会有空指针传递。 /**/ i = NR_TASKS; p = &task[NR_TASKS]; while (--i) { if (!*--p) continue; if ((*p)->state == TASK_RUNNING && (*p)->counter > c) c = (*p)->counter, next = i,pnext=*p; //保存要调度到的 pcb 指针 } if (c) break; for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority; } /*调度进程到运行态*/ if(task[next]->pid !=

current->pid) { //判断当前正在运行的进程状态是否为 TASK_RUNNING, //如果是,则表明当前的进程是时间片到期被抢走的,这时当前进程的状态还应是 TASK_RUNNING, //如果不是,则说明当前进程是主动让出 CPU,则 状态应为其他 Wait 状态。

if(current->state == TASK_RUNNING) { //记录当前进程的状态为 J,在此处,当前进程由运行态转变为就绪态。 fprintk(3,\ } fprintk(3,\switch_to(pnext,_LDT(next)); (3) fork.c

主要修改的是 copy_process 函数,见下面:

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) { /*melon - 添加用来取得内核栈指针*/ long * krnstack; /*melon added End*/ struct task_struct *p; int i; struct file *f; p = (struct task_struct *) get_free_page(); if (!p) return -EAGAIN; /*melon -取得当前子进程的内核栈指针*/ krnstack=(long)(PAGE_SIZE+(long)p); //实际上进程每次进入内核,栈顶都指向这里。 /*mel on added End*/ 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; //初始化内核栈内容,由于系统不再使用 tss 进行切换,所以内核栈内容要自已安排好 //下面部分就是进入内核后 int 之前入栈内容,即用户态下的 cpu 现场 *(--krnstack) = ss & 0xffff; //保存用户栈段寄存器,这些参数均来自于此次的函数调用, //即父进程压栈内容,看下面关于 tss 的设置此处和那里一样。 *(--krnstack) = esp; //保存用户栈顶指针 *(--krnstack) = eflags; //保存标识寄存器 *(--krnstack) = cs & 0xffff; //保存用户代码段寄存器 *(--krnstack) = eip; //保存 eip 指针数据,iret 时会出栈使用 ,这里也是子进程运行时的语句地 址。即 if(!fork()==0) 那里的地址,由父进程传递 //下面是 iret 时要使用的栈内容,由于调度发生前被中断的进程总是在内核的 int 中, //所以这里也要模拟中断返回现场,这里为什么不能直接将中断返回时使用的 //return_from_systemcall 地址加进来呢?如果完全模仿可不可以呢? //有空我会测试一下。 //根据老师的视频讲义和实验指导,这里保存了段寄存器数据。 //由 switch_to 返回后 first_return_fromkernel 时运行,模拟 system_call 的返回 *(--krnstack) = ds & 0xffff;

*(--krnstack) = es & 0xffff; *(--krnstack) = fs & 0xffff; *(--krnstack) = gs & 0xffff; *(--krnstack) = esi; *(--krnstack) = edi; *(--krnstack) = edx; //*(--krnstack) = ecx; //这三句是我根据 int 返回栈内容加上去的,后来 发现不加也可以 //但如果完全模拟 return_from_systemcall 的话,这里应该要加上。 //*(--krnstack) = ebx; //*(--krnstack) = 0; //此处应是返回的子进程 pid//eax; //其意义等同于 p->tss.eax=0;因为 ts s 不再被使用, //所以返回值在这里被写入栈内,在 switch_to 返回前被弹出给 eax; //switch_to 的 ret 语 句将会用以下地址做为弹出进址进行运行 *(--krnstack) = (long)first_return_from_kernel;

//*(--krnstack) = &first_return_from_kernel; //讨论区中有同学说应该这样写,结果同上 //这是在 switch_to 一起定义的一段用来返回用户态的汇编标 号,也就是 //以下是 switch_to 函数返回时要使用的出栈数据 //也就是说如果子进程得到机会运行,一定 也是先 //到

switch_to 的结束部分去运行,因为 PCB 是在那里被切换的,栈也是在那里被切换的, //所 以下面的数据一定要事先压到一个要运行的进程中才可以平衡。 *(--krnstack) = ebp; *(--krnstack) = eflags; //新添加 *(--krnstack) = ecx; *(--krnstack) = ebx; *(--krnstack) = 0; //这里的 eax=0 是 switch_to 返 回时弹出的,而且在后面没有被修改过。 //此处之所以是 0,是因为子进程要返回 0。而返回数据要放在 eax 中, //由于 switch_to 之后 eax 并没有被修改,所以这个值一直被保留。 //所以在上面的栈中可以不用再压入 eax 等数据。 //将内核栈的栈顶保存到内核指针处 p->kernelstack=krnstack; //保存当前栈顶

//p->eip=(long)first_switch_from; //上面这句是第一次被调度时使用的地址 ,这里是后期

经过测试后发现系统修改 //后会发生 不定期死机,经分析后认为是 ip 不正确导致的,但分析是否正确不得 //而知,只是经过这样修改后问题 解决,不知其他同学是否遇到这个问题。 /*melon added End*/ (4) system_call.s

这个文件中主要是添加新的 switch_to 函数。见下面:

.align 2 switch_to: pushl ?p movl %esp,?p #上面两条用来调整 C 函数栈态 pushfl #将当前的内核 eflags 入栈!!!! pushl ìx pushl ?x pushl êx movl 8(?p),?x #此时 ebx 中保存的是第一个参数 switch_to(pnext,LDT(next))

cmpl ?x,current #此处判断传进来的 PCB 是否为当前运行的 PCB je 1f #如果相等,则直接退出 #切换 PCB

movl ?x,êx #ebx 中保存的是传递进来的要切换的 pcb xchgl êx,current #交换 eax 和 current,交换完毕后 eax 中保存的是被切出去的 PCB #TSS 中 内核栈指针重写 movl tss,ìx #将全局的 tss 指针保存在 ecx 中 addl $4096,?x #取得 tss 保存的内核栈指针保存到 ebx 中 movl ?x,ESP0(ìx) #将内核栈指针保存到全局的 tss 的内核栈指针处 esp0=4 #切换内核栈 movl %esp,KERNEL_STACK(êx) #将切出去的 PCB 中的内核栈指针存回去 movl $1f,KERNEL_EIP(êx) #将 1 处地址保存在切出去的 PCB 的 EIP 中!!!! movl 8(?p),?x #重取 ebx 值, movl

KERNEL_STACK(?x),%esp #将切进来的内核栈指针保存到寄存器中 #下面两句是后来 添加的,实验指导上并没有这样做。 pushl KERNEL_EIP(?x) #将保存在切换的 PCB 中的 EIP 放入栈中!!!! jmp switch_csip #跳到 switch_csip 处执行!!!! # 原切换 LDT 代码换到下面 # 原切换 LDT 的代码在下面 1: popl êx popl ?x

popl ìx popfl #将切换时保存的 eflags 出栈!!!! popl ?p ret #该语句用来出栈 ip switch_csip: #用来进行内核段切换和 cs:ip 跳转 #切换 LDT movl 12(?p),ìx #取出第二个参数,_LDT(next) lldt %cx #切换 LDT #切换 cs 后要重新切换 fs,所以要将 fs 切换到用户态内存 movl $0x17,ìx #此时 ECX 中存放的是 LDT mov %cx,%fs cmpl êx,last_task_used_math jne 1f clts 1: ret #此处用来弹出 pushl next->eip!!!!!!!! #第一次被调度时运行的切换代码 first_switch_from: popl êx popl ?x popl ìx popfl #将切换时保存的 eflags 出栈!!!! popl ?p ret #该语句用来出栈 ip #此处是从内核站中返回时使用的代码,用来做中断返回

first_return_from_kernel: #popl êx #popl ?x #popl ìx popl íx popl íi popl %esi popl %gs popl %fs popl %es popl %ds iret 此外还要添加几个常量值:

/*melon 添加用来取内核栈指针的位置宏*/

KERNEL_STACK =16 //内核栈指针在 task_struct 结构中的位移,指导上这里是 12 ESP0 =4 //tss 中内核栈指针位移

KERNEL_EIP =20 //新添加的 task_sturct 结构中的 eip 指针位移 /*melon added End*/

这样就修改完毕。编译运行测试

四、实验结果