实验名称:基于内核栈切换的进程切换
实验日期:2021.5.24
班级:软嵌192
姓名:贾梦娇
学号:1930110798
一、实验目的
二、实验环境
cd /home/shiyanlou/oslab/
tar -zxvf hit-oslab-linux-20110823.tar.gz \-C /home/shiyanlou/
copy
三、实验内容
四、实验过程及数据记录
1、 TSS 切换
在现在的 Linux 0.11 中,真正完成进程切换是依靠任务状态段(Task State Segment,简称 TSS)的切换来完成的。
2、 本次实验的内容
将 Linux 0.11 中的 switch_to 实现去掉,写成一段基于堆栈切换的代码。
(1)重写 switch_to;
(2)将重写的 switch_to 和 schedule() 函数接在一起;
(3)修改现在的 fork()。
3、 schedule 与 switch_to
将 schedule() 函数(在 kernal/sched.c 中)中的代码:
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
//......
switch_to(next);
copy
改为:
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i, pnext = *p;
//.......
switch_to(pnext, LDT(next));
copy
4、 实现 switch_to
删除头文件sched.h中的长跳转指令:"ljmp *%0\n\t" \ ,在system_call.s中添加系统调用函数switch_to():
.align 2
switch_to:
pushl %ebp
movl %esp,%ebp
pushl %ecx
pushl %ebc
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
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
copy
5、 修改 fork
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;
p->tss.back_link = 0;
p->tss.esp0 = PAGE_SIZE + (long) p;
p->tss.ss0 = 0x10;
*(--krnstack) = ss & 0xffff;
*(--krnstack) = esp;
*(--krnstack) = eflags;
*(--krnstack) = cs & 0xffff;
*(--krnstack) = eip;
*(--krnstack) = (long) first_return_kernel;//处理switch_to返回的位置
*(--krnstack) = ebp;
*(--krnstack) = ecx;
*(--krnstack) = ebx;
*(--krnstack) = 0;
//把switch_to中要的东西存进去
p->kernelstack = krnstack;
copy
写first_return_kernel在system_call.s中:
首先需要将first_return_kernel设置在全局可见:
.globl switch_to,first_return_kernel
然后需要在fork.c中添加该函数的声明:
extern void first_return_from_kernel(void);
最后就是将具体的函数实现放在system_call.s头文件里面:
first_return_kernel:
popl %edx
popl %edi
popl %esi
pop %gs
pop %fs
pop %es
pop %ds
iret
copy
、实验结果分析
问题1
movl tss,%ecx addl $4096,%ebx movl %ebx,ESP0(%ecx)
copy
(1)为什么要加 4096?
因为页表是4KB
(2)为什么没有设置ss0?
SS0、SS1和SS2分别是0、1和2特权级的栈段选择子。这里用不着特权级为0的内核段。
问题2
*(--krnstack) = ebp;
*(--krnstack) = ecx;
*(--krnstack) = ebx;
*(--krnstack) = 0;
copy
(1)子进程第一次执行时,eax=?为什么要等于这个数?哪里的工作让 eax 等于这样一个数?
这个eax,根据课程里面讲的内容是p_id,所以子进程eax=0;当使用copy_process()创建子进程的时候赋值的。
(2)这段代码中的 ebx 和 ecx 来自哪里,是什么含义,为什么要通过这些代码将其写到子进程的内核栈中?
父子的内核栈在初始化的时候完全一致,用户栈指向一个地方。通过copy_process()拷贝了参数。
(3)这段代码中的 ebp 来自哪里,是什么含义,为什么要做这样的设置?可以不设置吗?为什么?
ebp是用户栈地址,不设置就不能运行了。
问题3
为什么要在切换完 LDT 之后要重新设置 fs=0x17?而且为什么重设操作要出现在切换完 LDT 之后,出现在 LDT 之前又会怎么样?
cpu的段寄存器都存在两类值,一类是显式设置段描述符,另一类是隐式设置的段属性及段限长等值,这些值必须经由movl、lldt、lgdt等操作进行设置,而在设置了ldt后,要将fs显示设置一次才能保证段属性等值正确。
六、实验心得
虽然用一条指令就能完成任务切换,但这指令的执行时间却很长,这条 ljmp 指令在实现任务切换时大概需要 200 多个时钟周期。而通过堆栈实现任务切换可能要更快,而且采用堆栈的切换还可以使用指令流水的并行优化技术,同时又使得 CPU 的设计变得简单。所以无论是 Linux 还是 Windows,进程/线程的切换都没有使用 Intel 提供的这种 TSS 切换手段,而都是通过堆栈实现的。
学习时间 380分钟
操作时间 36分钟
按键次数 287次
实验次数 17次
报告字数 4032字
是否完成 完成