哈工大-操作系统-HitOSlab-李治军-实验4-基于内核栈切换的进程切换

实验4-基于内核栈切换的进程切换

实验内容请查看蓝桥云课实验说明

一、实验内容

1.schedule 与 switch_to

目前 Linux 0.11 中工作的 schedule() 函数是首先找到下一个进程的数组位置 next,而这个 next 就是 GDT 中的 n,所以这个 next 是用来找到切换后目标 TSS 段的段描述符的,一旦获得了这个 next 值,直接调用上面剖析的那个宏展开 switch_to(next);就能完成 TSS 切换所示的切换了。

现在,我们不用 TSS 进行切换,而是采用切换内核栈的方式来完成进程切换,所以在新的 switch_to 中将用到当前进程的 PCB、目标进程的 PCB、当前进程的内核栈、目标进程的内核栈等信息。

因此需要将目前的 schedule() 函数(在 kernal/sched.c 中)做稍许修改,即将下面的代码:

if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
    c = (*p)->counter, next = i; i = NR_TASKS ,NR_TASK应该是定义在其他头文件的一个宏?

//......

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

schedule()函数中,当调用函数switch_to(pnext, LDT(next))时,会依次将参数2 LDT(next)、参数1 pnext、返回地址 }压栈。

当执行switch_to的返回指令ret时,就回弹出schedule()函数的}执行schedule()函数的返回指令}

2.编写switch_to汇编代码

为了将linux-0.11基于TSS切换内核线程的方式修改成基于PCB的方式,需要将原来放在 (/oslab/linux-0.11/include/linux/sched.h) 的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
    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

理解上述代码的核心,是理解栈帧结构和函数调用时控制转移权方式。

大多数CPU上的程序实现使用栈来支持函数调用操作。栈被用来传递函数参数、存储返回地址、临时保存寄存器原有值以备恢复以及用来存储局部数据。单个函数调用操作所使用的栈部分被称为栈帧结构。
栈帧结构的两端由两个指针来指定。寄存器ebp通常用作帧指针,而esp则用作栈指针。在函数执行过程中,栈指针esp会随着数据的入栈和出栈而移动,因此函数中对大部分数据的访问都基于帧指针ebp进行。

在执行switch_to上述这段代码前,内核栈的具体情况如下图所示:

哈工大-操作系统-HitOSlab-李治军-实验4-基于内核栈切换的进程切换
如果你现在还不能理解这张图,请查看我的上一篇文章:操作系统学习笔记(四)——用户级线程和核心级线程

现在,在脑海中留着对内核栈的印象,一条条的解析switch_to的汇编代码:

	pushl %ebp

将栈帧指针ebp压入内核栈中,内核栈变为:
哈工大-操作系统-HitOSlab-李治军-实验4-基于内核栈切换的进程切换

	movl %esp,%ebp

将esp栈指针传递给ebp。原来ebp指针指向哪处地方我们是不在乎的,不过这句代码执行完成后,ebp指针就指向刚刚压入的ebp位置。

哈工大-操作系统-HitOSlab-李治军-实验4-基于内核栈切换的进程切换

	pushl %ecx
    pushl %ebx
    pushl %eax

执行完这三句压栈代码后,内核栈的变化如图所示:
哈工大-操作系统-HitOSlab-李治军-实验4-基于内核栈切换的进程切换

	movl 8(%ebp),%ebx

linux 0.11的内核栈的地址顺序从上往下看,是由高到低的。也就是说,这句代码是将ebp指针+8指向的数据传递给了ebx寄存器,也就是将pnext(下一个进程的PCB)放在ebx寄存器中:
哈工大-操作系统-HitOSlab-李治军-实验4-基于内核栈切换的进程切换

	cmpl %ebx,current
    je 1f

ebx寄存器中保存的是下一个进程的PCB,current是当前进程的PCB。如果两个进程相同,跳转到1f位置处,后面的代码不用执行了,什么都不会发生。

#切换PCB
    movl %ebx,%eax
	xchgl %eax,current

把ebx的数据置给eax,交换eax和current中的内容。这两句代码执行完后,ebx和current都指向下一个进程的PCB,eax指向当前进程的PCB

#重写TSS指针
    movl tss,%ecx
    addl $4096,%ebx
    movl %ebx,ESP0(%ecx)

虽然不使用 TSS 进行任务切换了,但是 Intel 的这态中断处理机制还要保持,所以仍然需要有一个当前 TSS。

在schedule.c中定义struct tss_struct *tss=&(init_task.task.tss)这样一个全局变量,即0号进程的tss,所有进程都共用这个tss,任务切换时不再发生变化。

ebx本来是指向?一个进程的PCB,执行完指令addl $4096,%ebx后,ebx指向下一个进程的PCB。为什么偏移量是4096?4096 = 4KB。在linux0.11中,一个进程的内核栈和该进程的PCB段是放在一块大小为4KB的内存段中的,其中该内存段的高地址开始是内核栈,低地址开始是PCB段。

ESP0常量需要我们手动添加在system_call.s中,其中ESP0 = 4。看一看 tss 的结构体定义就明白为什么是4了。

#切换内核栈
    movl %esp,KERNEL_STACK(%eax) 
    movl 8(%ebp),%ebx
    movl KERNEL_STACK(%ebx),%esp

Linux 0.11的PCB定义中没有保存内核栈指针这个域(kernelstack),所以需要我们额外添加。在(/oslab/linux0.11/include/linux/sched.h)中找到结构体task_struct的定义,对其进行如下修改:

/* linux/sched.h */
struct task_struct {
long state;
long counter;
long priority;
long kernelstack;
/* ...... */
}

由于这里将PCB结构体的定义改变了,所以在产生0号进程的PCB初始化时也要跟着一起变化,需要在(sched.h)中做如下修改:

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

同时在(system_call.s)中定义KERNEL_STACK = 12 并且修改汇编硬编码,修改代码如下:

/* kernel/system_call.s */
ESP0        = 4
KERNEL_STACK    = 12
/* ...... */
state   = 0     # these are offsets into the task-struct.
counter = 4
priority = 8
kernelstack = 12
signal  = 16
sigaction = 20      # MUST be 16 (=len of sigaction)
blocked = (37*16)

不知道大家是否还记得我上一篇文章:操作系统学习笔记(四)——用户级线程和核心级线程中,对内核栈切换写的伪代码
哈工大-操作系统-HitOSlab-李治军-实验4-基于内核栈切换的进程切换

#切换LDT
	movl 12(%ebp), %ecx
    lldt %cx
    movl $0x17,%ecx
	mov %cx,%fs

一旦修改完成,下一个进程在执行用户态程序时使用的内存映射表就是自己的LDT表了,地址分离实现了。

参考文献:
1.哈工大实验“官方”github仓库
2.蓝桥云课-操作系统原理与实践
3.GDT,LDT,GDTR,LDTR 详解,包你理解透彻
4.在Linux-0.11中实现基于内核栈切换的进程切换

上一篇:c内嵌汇编


下一篇:AT&T汇编