哈工大操作系统(李治军)实验课,实验五:基于内核栈完成进程切换

基于ubuntu22.04进行实验

一 实验内容

将 Linux 0.11 中基于任务状态段TSS的进程切换,修改为基于内核栈的切换。
—— 实验步骤 :
————1. 去除Linux 0.11下的switch_to
————2. 重写switch_to
————3. 将重写的switch_to和schedul函数连接在一起
————4. 修改fork()

二 修改schedule

2.1 确认switch_to()的参数

由于当前进程的PCB,也就是task_struct用一个全局变量current指向,所以需要告诉新的switch_to()函数一个指向目标进程的task_struct指针,所以我们将pnext指针作为第一个参数传给switch_to();

同时,我们需要把每个进程的LDT在GDT表上的索引,传给switch_to(),所以我们将_LDT(next)作为第二个参数。_LDT是一个宏,它可以得到进程n的LDT。

在这里插入图片描述

_TSS()是获取进程n的TSS位置。每个进程都对应一个TSS和LDT,且每个描述符长度都为8个字节,所以在GDT表中,LDT就在TSS下面。进程0的TSS段描述符出现在GDT表的第四个位置上

2.2 修改schedule()

//这里需要给pnext赋初值,不然系统无法运行

struct task_struct* pnext = &(init_task.task);

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

.......

switch_to(pnext, _LDT(next));

三 switch_to

3.1基于TSS切换的switch_to

基于TSS切换的switch_to。(在oslab/linux-0.11/include/linux/sched.h)。我们将他注释掉

大概作用:使用_TSS(n)得到目标进程n的TSS段选择子,然后通过dx寄存器放到64位结构体tmp中后32位长整数的前16位;然后ljmp指令就可以找到进程n的TSS段描述符,并修改TR寄存器,完成进程切换。

#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" \
	"je 1f\n\t" \
	"movw %%dx,%1\n\t" \
	"xchgl %%ecx,current\n\t" \
	"ljmp *%0\n\t" \
	"cmpl %%ecx,last_task_used_math\n\t" \
	"jne 1f\n\t" \
	"clts\n" \
	"1:" \
	::"m" (*&__tmp.a),"m" (*&__tmp.b), \
	"d" (_TSS(n)),"c" ((long) task[n])); \
}

3.2 基于内核栈切换的switch_to

我们在(/oslab/linux-0.11/kernel/system_call.s)中,重新编写switch_to
switch_to:
    pushl %ebp
    movl %esp,%ebp
    pushl %ecx
    pushl %ebx
    pushl %eax
    movl 8(%ebp),%ebx
    cmpl %ebx,current
    je 1f
#切换PCB
#重写TSS指针
#切换内核栈
#切换LDT
    cmpl %eax,last_task_used_math 
    jne 1f
    clts

1: 
	popl %eax
    popl %ebx
    popl %ecx
    popl %ebp
	ret

1. 切换PCB

movl %ebx,%eax
xchgl %eax,current

ebx寄存器中存放的是switch_to()的第一个参数pnext,也就是要切换的进程的PCB指针。这段代码将要切换的PCB指针赋值给current。完成了PCB的切换

2.重写TSS指针

由于中断处理时,需要通过TSS中存放的esp0去寻找内核栈;我们这里需要保持Intel的中断处理机制,所以
要对TSS做处理:
	让所有进程都共用一个TSS,这里使用0号进程的TSS,定义一个全局指针变量tss来指向0号进程的TSS,
	这个tss在中断发生时,帮助寻找当前进程的内核栈位置。

在sched.c中定义

struct tss_struct* tss = &(init_task.task.tss);

定义宏 ESP0 在system_call.s中,ESP0 = 4是因为 tss_struct的偏移量4位置处,就是esp0(内核栈指针)

ESP0 = 4

继续在switch_to中写

movl tss,%ecx
addl $4096,%ebx
movl %ebx,ESP0(%ecx)

PCB和内核栈同在一页内存上(4kb),内核栈在高地址处,PCB在低地址处;这里EBX存放着完成切换的进程的PCB指针,加上4096后,它就指向进程内核栈的栈底之前一个byte。将这个指针赋值给tss中的esp0,中断就可以找到当前进程的内核栈

3 切换内核栈

内核栈的切换,将ESP寄存器的值保存在当前的PCB中,再将目标PCB中取出目标内核栈栈顶位置放入物理
寄存器ESP。但Linux 0.11 的task_struct结构体定义中没有保存内核栈指针,所以需要在这个结构体内
加入kernelstack,再使用宏KERNEL_STACK给出这个域对应的位置

在task_struct中添加kernelstack域 (/oslab/linux-0.11/include/linux/sched.h),并且修改0号进程的PCB初始化
在这里插入图片描述
在sched.h中修改第四项为PAGE_SIZE+(long)&init_task

#define INIT_TASK \
/* state etc */ { 0,15,15,PAGE_SIZE+(long)&init_task,\
/* signals */   0,{{},},0, \
......
}

在system_call.s中添加宏:KERNEL_STACK,并且修改硬编码

KERNEL_STACK = 12
......
state = 0
counter = 4
priority = 8
kernelstack = 12
signal = 16
sigaction = 20
blocked = (37*16)

继续在system_call.s的switch_to中添加

movl %esp,KERNEL_STACK(%eax)
movl 8(%ebp),%ebx
movl KERNEL_STACK(%ebx),%esp

在切换PCB时,使用了xchgl指令,这个指令交换了两个操作数的值,所以此时EAX寄存器中是完成切换前的当前进程的PCB。将ESP寄存器指针的值存放到当前进程的PCB中,再将目标进程PCB的kernelstack存放到ESP寄存器。完成内核栈的切换

4 切换LDT

切换LDT后,目标进程执行时使用的内存映射表就是自己的LDT表,实现地址分离
movl 12(%ebp),%ecx
lldt %cx
movl $0x17,%ecx
mov %cx,%fs

通过EBP寄存器,取出第二个参数_LDT(next)到ECX寄存器,这个数值是目标进程LDT在GTD表中的索引,再通过lldt指令修改ldtr寄存器,完成LDT的切换。因为fs段寄存器机制,会在隐藏部分存储切换前进程的基址和段限长来快速寻址,所以需要重置fs寄存器为0x17

3.3 修改后的switch_to

switch_to:
    pushl %ebp
    movl %esp,%ebp
    pushl %ecx
    pushl %ebx
    pushl %eax
    movl 8(%ebp),%ebx
    cmpl %ebx,current
    je 1f
#切换PCB
    movl %ebx,%eax
	xchgl %eax,current
#重写TSS指针
    movl tss,%ecx
    addl $4096,%ebx
    movl %ebx,ESP0(%ecx)
#切换内核栈
    movl %esp,KERNEL_STACK(%eax)
    movl 8(%ebp),%ebx
    movl KERNEL_STACK(%ebx),%esp
#切换LDT
	movl 12(%ebp), %ecx
    lldt %cx
    movl $0x17,%ecx
	mov %cx,%fs

    cmpl %eax,last_task_used_math 
    jne 1f
    clts

1: 
	popl %eax
    popl %ebx
    popl %ecx
    popl %ebp
	ret

四 修改fork.c

对fork.c的修改,主要是对子进程内核栈的初始化

4.1 确认内核栈位置

在fork.c的copy_process()中 p = (struct task_struct*) get_free_page()申请一页内存(4kb)作为子进程的PCB,p+PAGE_SIZE就是子进程内核栈的位置

long* krnstack;
krnstack = (long*)((long)p+PAGE_SIZE);

4.2 初始化krnstack

*(--krnstack) = ss & 0xffff;
*(--krnstack) = esp;
*(--krnstack) = eflags;
*(--krnstack) = cs & 0xffff;
*(--krnstack) = eip

根据switch_to 完成内核栈切换后开始执行的代码
在这里插入图片描述
ret指令从内核栈中弹出32位数作为EIP跳转执行,这里内核栈以及切换完成,所以现在要进行中断返回——first_return_from_kernel
在这里插入图片描述
iret指令设置cs:eip
根据这些代码,继续初始化内核栈

    *(--krnstack) = ds & 0xffff;
    *(--krnstack) = es & 0xffff;
    *(--krnstack) = fs & 0xffff;
    *(--krnstack) = gs & 0xffff;
    *(--krnstack) = esi;
    *(--krnstack) = edi;
    *(--krnstack) = edx;
    *(--krnstack) = (long)first_return_from_kernel;
    *(--krnstack) = ebp;
    *(--krnstack) = ecx;
    *(--krnstack) = ebx;
    *(--krnstack) = 0; //将EAX设置为0

因为fork()会把EAX寄存器内的值作为函数的返回值,所以这里设置为0

将task_struct中的内核栈地址kernelstack修改为初始化完成后内核栈的地址

p->kernelstack = krnstack;

4.3 添加first_return_from_kernel

在system_call.s中添加first_return_from_kernel
.align 2
first_return_from_kernel:
    popl %edx
    popl %edi
    popl %esi
    pop %gs
    pop %fs
    pop %es
    pop %ds
    iret

五 剩余操作

因为switch_to和first_return_from都在system_call.s中实现,要想在sched.c和fork.c中使用,需要将这两个标号声明为全局,并且在c文件中声明他们为外部变量。

system_call.s中的全局声明

.globl switch_to
.globl first_return from kernel

在fork.c中声明

extern long first_return_from_kernel(void);

在sched.c中声明

extern long switch_to(struct task_struct* p,unsigned long add);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值