HIT oslab之实验5 基于内核栈切换的进程切换(含函数调用内存模型的讲解)

一、实验内容

把Linux-0.11进程切换的方式改为基于内核栈切换,而不是基于TSS切换(因为太耗时)。

要实现基于内核栈的任务切换,主要完成如下三件工作

  • 重写 switch_to;
  • 将重写的 switch_to 和 schedule() 函数接在一起;
  • 修改现在的 fork()。

二、修改schedule函数

1.switch_to()函数的2个参数
①第1个参数:指向目标进程 PCB 的指针(即pnext);
②第2个参数:目标进程的LDT;

进程切换必然要涉及到 LDT 的切换

2.根据上述的“1.”,在kernel/sched.c完成对schedule函数的修改
在这里插入图片描述

注意,pnext必须赋初值

在这里插入图片描述
在这里插入图片描述

实验指导书写的是LDT,但实际应该是_LDT,其宏定义如下所述:
在这里插入图片描述

三、实现 switch_to

1.在linux/sched.h中注释掉原有的switch_to
在这里插入图片描述
2.在kernel/sched.c中增加对switch_to函数的声明:
在这里插入图片描述

注意:要在kernel/system_call.s中添加下图红框中的内容
在这里插入图片描述

3.在kernel/system_call.s中,用汇编代码来完成函数switch_to 的编写
(1)栈帧讲解,参考资料1参考资料2

1)示例

int add_a_and_b(int a, int b) {
   return a + b;
}

int main() {
   return add_a_and_b(2, 3);
}

2)内存模型:stack(栈顶是低地址)
①执行函数时,其在stack中占有的一片区域称为栈帧
②栈帧的构成

  • 先保存调用函数栈帧的栈底(存在寄存器ebp中)及更新被调用函数(main函数)栈帧的栈底
    在这里插入图片描述
  • 参数入栈
    在这里插入图片描述
  • 返回地址入栈
    在这里插入图片描述

执行add_a_and_b函数之前,其下一条语句的地址(返回地址)入栈在这里插入图片描述

如图所示:
在这里插入图片描述

所画的便是main的栈帧。

③验证
在这里插入图片描述
在这里插入图片描述

红框中的即返回地址,正好在参数2的下面。

(2)switch_to的源码
1)TSS 中内核栈指针的重写所需的准备工作
①在kernel/sched.c中增添tss全局变量:
在这里插入图片描述
②在kernel/system_call.s中增添ESP0的定义
在这里插入图片描述

long占4个字节
在这里插入图片描述

③Linux 0.11 进程的内核栈和该进程的 PCB 在同一页内存上(一块 4KB 大小的内存),其中 PCB 位于这页内存的低地址,栈位于这页内存的高地址;

④所有进程共用1个tss,一开始当然是指向0号进程的TSS,一旦发生进程切换,就将TSS中的esp0指向目标进程的内核栈基址。

2)切换内核栈栈顶指针所需的准备工作
①在linux/shced.h的task_struct(PCB)中增加指向内核栈栈顶指针的域
在这里插入图片描述
②修改kernal/system_call.s中与kernelstack有关的硬编码
在这里插入图片描述
③因为修改了PCB结构定义,所以也要修改产生 0 号进程的 PCB 初始化(在linux/sched.h中修改)
在这里插入图片描述
3)PC切换所需的理论知识:内核态线程切换的五段论
在这里插入图片描述

通俗理解:
①线程1通过中断请求内核提供服务,即在内核态执行某个系统调用函数A。
②系统调用函数A执行时由于等待某个事件,所以被中断处理调用schedule函数阻塞了,并发生线程切换。
③schedule函数最后调用switch_to函数完成线程切换。
④switch_to的最后一句指令ret就返回到目标进程(此时,它已经是当前进程了;也即图中的线程2)的schedule函数的末尾,遇到},继续 ret回到调用schedule()地方,即回到了中断处理中,就到了中断返回的地址
⑤再调用iret就到了线程2的用户态程序去执行

4)关于fs的一些知识
①当系统调用发生时,int 0x80中断处理函数会把fs设成用户数据段选择符(0x17)
②LDT切换完成就意味着切换了分配给进程的用户态内存地址空间,所以前一个 fs 指向的是上一个进程的用户态内存,而现在需要执行下一个进程的用户态内存,所以就需要重取fs。

5)switch_to的源码及注释(在kernel/system_call.s中)

.align 2 #对齐伪指令,必须从一个能被2整除的地址开始为下面的内存变量分配空间
switch_to:
#这一部分的解释,看上述内存模型: stack
	pushl %ebp
	movl %esp,%ebp
	pushl %ecx
	pushl %ebx
	pushl %eax
	movl 8(%ebp),%ebx # 调用switch_to的第一个参数,即pnext——目标进程的PCB
	cmpl %ebx, current #和 current 做一个比较, current指向当前进程的PCB
	je 1f   #相等就啥也不用做,跳转到“1:”直接恢复保存的寄存器即可
#不相等,开始进程切换
#先切换PCB
	movl %ebx,%eax #eax指向目标进程的PCB
	xchgl %eax,current #current指向目标进程的PCB, eax指向当前进程的PCB

#TSS中的内核指针的重写
	movl tss,%ecx #ecx指向当前进程的TSS
	addl $4096,%ebx #ebx指向的是目标进程的PCB,+4096后指向目标进程的内核栈
	movl %ebx,ESP0(%ecx) #TSS的esp0指向目标进程的内核栈

#切换内核栈栈顶指针(切换当前的内核栈为目标内核栈);当然要先保存被切换进程的esp
	movl %esp, KERNEL_STACK(%eax) #把当前进程的esp保存到其PCB中
	movl 8(%ebp),%ebx #再一次把目标进程的PCB存储到ebx中
	movl KERNEL_STACK(%ebx),%esp #把目标进程PCB中的内核栈基址存储到esp中

#切换LDT
	movl 12(%ebp),%ecx #把_LDT(next)存储到ecx中
	lldt %cx #修改LDTR寄存器后,目标进程在执行用户态程序时使用的映射表就是自己的LDT表,实现了地址空间的分离。

#通过ret完成PC的切换

#切换LDT之后,更新fs,指向目标进程的用户态内存
	movl $0x17,%ecx  #用户空间数据段选择符为0x17
	mov %cx,%fs 

#和后面的clts配合来处理协处理器,由于和主题关系不大,此处不做论述
	cmpl %eax, last_task_used_math
	jne 1f
	clts
1:
	popl %eax
	popl %ebx
	popl %ecx
	popl %ebp
ret

在这里插入图片描述

四、修改fork

准确说是修改kernel/fork.c中的copy_process函数

1.子进程内核栈概览
在这里插入图片描述
(1)switch_to完成内核栈指针切换后,esp的指向应该如上图所示
在这里插入图片描述
(2)从下往上填写子进程的内核栈
①按照下图的popl顺序可知,从下到上依次填入eax, ebx, ecx, ebp;
ret指令将此时esp指向的内容赋值给eip,即跳转到first_return_from_kernel去执行。
在这里插入图片描述
②first_return_from_kernel是一段汇编程序(在kernel/system_call.s中),按照①的逻辑填写子进程的内核栈
在这里插入图片描述

为了在kernel/fork.ccopy_process中使用这段汇编程序,还需要该文件中添加:
在这里插入图片描述
注意:要在kernel/system_call.s中添加下图红框中的内容
在这里插入图片描述
2.copy_process函数的修改情况
①填写子进程的内核栈
在这里插入图片描述

②把TSS相关的所有语句注释掉
在这里插入图片描述
3.copy_process的源码

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;

	long *krnstack;

	p = (struct task_struct *) get_free_page();
	if (!p)
		return -EAGAIN;
	
	//子进程内核栈的位置,准确说是内核栈基址上面
	krnstack = (long *)(PAGE_SIZE + (long)p);

	//设置子进程内核栈
	*(--krnstack) = ss & 0xffff;  //--krnstack这才是内核栈基址!
	*(--krnstack) = esp; //先递减指向内核栈一个空位置
	*(--krnstack) = eflags;
	*(--krnstack) = cs & 0xffff;
	*(--krnstack) = eip;

	//存入first_return_from_kernel要弹栈的寄存器们
	*(--krnstack) = ds & 0xffff;
	*(--krnstack) = es & 0xffff;
	*(--krnstack) = fs & 0xffff; 
	*(--krnstack) = gs & 0xffff;
	*(--krnstack) = esi;
	*(--krnstack) = edi;		
	*(--krnstack) = edx;
			
	*(--krnstack) = (long)first_return_from_kernel; //返回地址,first_return_from_kernel是汇编标号
			
	//swtich_to中,当完成切换内核栈后,在ret之前,要popl4个寄存器,所以此刻要先初始化这4个寄存器
	*(--krnstack) = ebp;
	*(--krnstack) = ecx;
	*(--krnstack) = ebx;
	*(--krnstack) = 0;     //fork返回2个值,子进程返回0,存到eax中。

	task[nr] = p;
	*p = *current;	/* NOTE! this doesn't copy the supervisor stack */
	
	p->kernelstack = krnstack; 
	
	p->state = TASK_UNINTERRUPTIBLE;
	p->pid = last_pid;
	p->father = current->pid;
	p->counter = p->priority;
	p->signal = 0;
	p->alarm = 0;
	p->leader = 0;		/* process leadership doesn't inherit */
	p->utime = p->stime = 0;
	p->cutime = p->cstime = 0;
	p->start_time = jiffies;  //由于要记录新建时的滴答数,那么找到jiffies(滴答数),就找到了新建进程的代码点。
	fprintk(3, "%d\tN\t%ld\n", p->pid, jiffies);
	/*修改TSS中的内容被全部注释掉了
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;
	p->tss.ss0 = 0x10;
	p->tss.eip = eip;
	p->tss.eflags = eflags;
	p->tss.eax = 0;
	p->tss.ecx = ecx;
	p->tss.edx = edx;
	p->tss.ebx = ebx;
	p->tss.esp = esp;
	p->tss.ebp = ebp;
	p->tss.esi = esi;
	p->tss.edi = edi;
	p->tss.es = es & 0xffff;
	p->tss.cs = cs & 0xffff;
	p->tss.ss = ss & 0xffff;
	p->tss.ds = ds & 0xffff;
	p->tss.fs = fs & 0xffff;
	p->tss.gs = gs & 0xffff;
	p->tss.ldt = _LDT(nr);
	p->tss.trace_bitmap = 0x80000000;
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
	*/
	if (copy_mem(nr,p)) {
		task[nr] = NULL;
		free_page((long) p);
		return -EAGAIN;
	}
	for (i=0; i<NR_OPEN;i++)
		if ((f=p->filp[i]))
			f->f_count++;
	if (current->pwd)
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
	set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->state = TASK_RUNNING;	/* do this last, just in case */
	fprintk(3, "%d\tJ\t%ld\n", p->pid, jiffies);
	return last_pid; //父进程中的返回值
}

五、验证

0.make all报错
在这里插入图片描述

解决办法:
在这里插入图片描述

1.运行实验4中的process.c。
在这里插入图片描述

至此,顺利实现基于内核栈切换的进程切换,撒花!

2.问题:为什么process.log的内容为空???

六、参考资料

1.HIT oslab之实验5

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值