实现基于内核栈切换的进程切换-linux011

实验题目:

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

实验目的和要求:
构建出内核栈,要在适当的地址压入适当的返回地址,并根据内核栈的样子,编写相应的汇编代码,精细地完成内核栈的入栈和出栈操作,在适当的地方弹出正确的返回地址,以保证能顺利完成进程的切换。同时,还要完成内核栈和 PCB 的关联,在 PCB 切换时,完成内核栈的切换。

实验过程:
本次实践项目就是将Linux 0.11中采用的TSS切换部分去掉,取而代之的是基于堆栈的切换程序。具体的说,就是将Linux 0.11中的switch_to实现去掉,写成一段基于堆栈切换的代码。
本次实验包括如下内容:
编写汇编程序switch_to:
完成主体框架;
在主体框架下依次完成PCB切换、内核栈切换、LDT切换等;
修改fork(),由于是基于内核栈的切换,所以进程需要创建出能完
成内核栈切换的样子。
修改PCB,即task_struct结构,增加相应的内容域,同时处理由于修改了task_struct所造成的影响。

1、找到当前进程的PCB和新进程的PCB

当前进程的PCB是用一个全局变量current指向的(在sched.c中定义) ,所以current即指向当前进程的PCB
为了得到新进程的PCB,我们需要对schedule()函数做如下修改:

if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;

switch_to(next);
修改为:
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i, pnext = *p;

switch_to(pnext, LDT(next));
这样,pnext就指向下个进程的PCB

2.修改switch_to

将 Linux 0.11 中原有的 switch_to 实现去掉,写成一段基于栈切换的代码。由于要对
内核进行精细的操作,所以需要用汇编代码来实现 switch_to 的编写,既然要用汇编实
现 switch_to,那么将 switch_to 的实现放在 system_call.s 中是最合适的。这个函数
依次主要完成如下功能:由于是 C 语言调用汇编,所以需要首先在汇编中处理栈帧,即
处理 ebp 寄存器;接下来要取出表示下一个进程 PCB 的参数,并和 current 做一个比较,
如果等于 current,则什么也不用做;如果不等于 current,就开始进程切换,依次完
成 PCB 的切换、TSS 中的内核栈指针的重写、内核栈的切换、LDT 的切换以及 PC 指针(即
CS:EIP)的切换
switch_to:
pushl %ebp
movl %esp,%ebp
pushl %ecx
pushl %ebx
pushl %eax

(1)判断要切换的进程和当前进程是否是同一个进程

movl 8(%ebp),%ebx	/*%ebp+8就是从右往左数起第二个参数,也就是*pnext*/
cmpl %ebx,current	/* 如果当前进程和要切换的进程是同一个进程,就不切换了 */
je 1f
/*先得到目标进程的pcb,然后进行判断
如果目标进程的pcb(存放在ebp寄存器中) 等于   当前进程的pcb => 不需要进行切换,直接退出函数调用
如果目标进程的pcb(存放在ebp寄存器中) 不等于 当前进程的pcb => 需要进行切换,直接跳到下面去执行*/

(2)切换PCB

movl %ebx,%eax
xchgl %eax,current
/*ebx是下一个进程的PCB首地址,current是当前进程PCB首地址*/

(3)TSS中的内核栈指针的重写

movl tss,%ecx		/*%ecx里面存的是tss段的首地址,在后面我们会知道,tss段的首地址就是进程0的tss的首地址,
根据这个tss段里面的内核栈指针找到内核栈,所以在切换时就要更新这个内核栈指针。也就是说,
任何正在运行的进程内核栈都被进程0的tss段里的某个指针指向,我们把该指针叫做内核栈指针。*/
addl $4096,%ebx           /* 未加4KB前,ebx指向下一个进程的PCB首地址,加4096后,相当于为该进程开辟了一个“进程页”,ebx此时指向进程页的最高地址*/
movl %ebx,ESP0(%ecx)        /* 将内核栈底指针放进tss段的偏移为ESP0(=4)的地方,作为寻找当前进程的内核栈的依据*/
/* 由上面一段代码可以知道们的“进程页”是这样的,PCB由低地址向上扩展,栈由上向下扩展。
也可以这样理解,一个进程页就是PCB,我们把内核栈放在最高地址,其它的task_struct从最低地址开始扩展*/

(4)切换内核栈

# KERNEL_STACK代表kernel_stack在PCB表的偏移量,意思是说kernel_stack位于PCB表的第KERNEL_STACK个字节处,注意:PCB表就是task_struct
movl %esp,KERNEL_STACK(%eax)	/* eax就是上个进程的PCB首地址,这句话是将当前的esp压入旧PCB的kernel_stack。所以该句就是保存旧进程内核栈的操作。*/
movl 8(%ebp),%ebx		/*%ebp+8就是从左往右数起第一个参数,也就是ebx=*pnext ,pnext就是下一个进程的PCB首地址。至于为什么是8,请查看附录(I)*/
movl KERNEL_STACK(%ebx),%esp	/*将下一个进程的内核栈指针加载到esp*/

(5)切换LDT

movl 12(%ebp),%ecx         /* %ebp+12就是从左往右数起第二个参数,对应_LDT(next) */
lldt %cx                /*用新任务的LDT修改LDTR寄存器*/
下一个进程在执行用户态程序时使用的映射表就是自己的 LDT 表了,地址空间实现了分离

(6)重置一下用户态内存空间指针的选择符fs

movl $0x17,%ecx
mov %cx,%fs
通过 fs 访问进程的用户态内存,LDT 切换完成就意味着切换了分配给进程的用户态内存地址空间,
所以前一个 fs 指向的是上一个进程的用户态内存,而现在需要执行下一个进程的用户态内存,
所以就需要用这两条指令来重取 fs。

3.修改fork
对于得到CPU的新的进程,我们就是要把进程的用户栈、用户程序和其内核栈通过压在内核栈中的 SS:ESP,CS:EIP 关联在一起。
另外,由于 fork() 这个叉子的含义就是要让父子进程共用同一个代码、数据和堆栈,所以修改 fork() 的核心工作就是要形成如下图所示的子进程内核栈结构。
对 fork() 的修改就是对子进程的内核栈的初始化,在 fork() 的核心实现 *copy_process 中,p = (struct task_struct *) get_free_page();用来完成申请一页内存作为子进程的 PCB,而 p 指针加上页面大小就是子进程的内核栈位置,所以语句 krnstack = (long *) (PAGE_SIZE + (long) p); 就可以找到子进程的内核栈位置,*接下来就是初始化 krnstack (内核栈)中的内容了。
*(–krnstack) = ss & 0xffff;
*(–krnstack) = esp;
*(–krnstack) = eflags;
*(–krnstack) = cs & 0xffff;
*(–krnstack) = eip;
这五条语句就完成了上图所示的那个重要的关联,因为其中 ss,esp 等内容都是 copy_proces() 函数的参数,这些参数来自调用 copy_proces() 的进程的用户栈中,就是父进程的用户栈中。

对 krnstack 进行初始化:
*(–krnstack) = ebp;
*(–krnstack) = ecx;
*(–krnstack) = ebx;
// 这里的 0 最有意思,代表返回值是0,与父进程区分
*(–krnstack) = 0;

四次弹栈以及 ret 处理使用1: popl %eax
popl %ebx
popl %ecx
popl %ebp
Ret

switch_to()中的 ret 指令,这条指令要从内核栈中弹出一个 32 位数作为 EIP 跳去执行,所以需要弄一个函数地址(仍然是一段汇编程序,所以这个地址是这段汇编程序开始处的标号)并将其初始化到栈中。搞一个名为 first_return_from_kernel 的汇编标号,然后可以用语句 *(–krnstack) = (long) first_return_from_kernel; 将这个地址初始化到子进程的内核栈中,执行 ret 以后就会跳转到 first_return_from_kernel 去执行了。

4.编写first_return_from_kernel
PCB 切换完成、内核栈切换完成、LDT 切换完成之后需要完成用户栈和用户代码的切换,依靠的核心指令就是 iret,当然在切换之前应该恢复一下执行现场,主要就是eax,ebx,ecx,edx,esi,edi,gs,fs,es,ds 等寄存器的恢复。

first_return_from_kernel 的核心代码:

popl %edx
popl %edi
popl %esi
pop %gs
pop %fs
pop %es
pop %ds
Iret

最后,注意由于switch_to()和first_return_from_kernel都是在system_call.s中实现的,要想在schedule.c和fork.c中调用它们,就必须在system_call.s中将这两个标号声明为全局的,同时在引用到它们的.c文件中声明它们是一个外部变量。
system_call.s中的全局声明

.globl switch_to
.globl first_return_from_kernel

对应.c文件中的外部变量声明:

extern long switch_to;
extern long first_return_from_kernel;

猜你喜欢

转载自blog.csdn.net/perfectzxiny/article/details/122100920