“操作系统实验”实验报告

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

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

实验日期:2021.5.24

班级:软嵌192

姓名:贾梦娇

学号:1930110798

一、实验目的

  • 深入理解进程和进程切换的概念;
  • 综合应用进程、CPU 管理、PCB、LDT、内核栈、内核态等知识解决实际问题;
  • 开始建立系统认识。

二、实验环境

cd /home/shiyanlou/oslab/
tar -zxvf hit-oslab-linux-20110823.tar.gz \-C /home/shiyanlou/
copy

三、实验内容

  • 编写汇编程序 switch_to:
  • 完成主体框架;
  • 在主体框架下依次完成 PCB 切换、内核栈切换、LDT 切换等;
  • 修改 fork(),由于是基于内核栈的切换,所以进程需要创建出能完成内核栈切换的样子。
  • 修改 PCB,即 task_struct 结构,增加相应的内容域,同时处理由于修改了 task_struct 所造成的影响。
  • 用修改后的 Linux 0.11 仍然可以启动、可以正常使用。
  • (选做)分析实验 3 的日志体会修改前后系统运行的差别。

四、实验过程及数据记录

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 切换手段,而都是通过堆栈实现的。

最新评论
暂无评论~