基于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);