哈工大-操作系统-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上述这段代码前,内核栈的具体情况如下图所示:
如果你现在还不能理解这张图,请查看我的上一篇文章:操作系统学习笔记(四)——用户级线程和核心级线程
现在,在脑海中留着对内核栈的印象,一条条的解析switch_to
的汇编代码:
pushl %ebp
将栈帧指针ebp压入内核栈中,内核栈变为:
movl %esp,%ebp
将esp栈指针传递给ebp。原来ebp指针指向哪处地方我们是不在乎的,不过这句代码执行完成后,ebp指针就指向刚刚压入的ebp位置。
pushl %ecx
pushl %ebx
pushl %eax
执行完这三句压栈代码后,内核栈的变化如图所示:
movl 8(%ebp),%ebx
linux 0.11的内核栈的地址顺序从上往下看,是由高到低的。也就是说,这句代码是将ebp指针+8指向的数据传递给了ebx寄存器,也就是将pnext(下一个进程的PCB)放在ebx寄存器中:
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)
(不知道大家是否还记得我上一篇文章:操作系统学习笔记(四)——用户级线程和核心级线程中,对内核栈切换写的伪代码)
#切换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中实现基于内核栈切换的进程切换
标签:movl,哈工大,ebp,切换,PCB,HitOSlab,ebx,内核 来源: https://blog.csdn.net/qq_42518941/article/details/119182097