哈工大OS实验五——基于内核栈切换的进程切换

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

tss:

linux0.11中主要是基于TSS方式的切换,通过switch_to函数来实现,在shcdule()函数中,给switch_to函数传递一个next参数,这个参数为下一个进程在进程队列中的索引值

        while (1) {
                c = -1;
                next = 0;l
                i = NR_TASKS;
                p = &task[NR_TASKS];
                while (--i) {
                        if (!*--p)
                                continue;
                        if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
                                c = (*p)->counter, next = i;
                }
                if (c) break;
                for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
                        if (*p)
                                (*p)->counter = ((*p)->counter >> 1) +
                                                (*p)->priority;
        }
        switch_to(next);
}

这里的p是一个二级指针,也就是一个队列的开头,while循环主要是进行队列中的counter最大值的查找,也就是将counter为最大的进程进行执行,当找到counter了就退出,当没有找到就进行其他的判断标准,所以next的产生就是在这里进行的。

#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" \
	"je 1f\n\t" \
	"movw %%dx,%1\n\t" \
	"xchgl %%ecx,current\n\t" \
	"ljmp *%0\n\t" \
	"cmpl %%ecx,last_task_used_math\n\t" \
	"jne 1f\n\t" \
	"clts\n" \
	"1:" \
	::"m" (*&__tmp.a),"m" (*&__tmp.b), \
	"d" (_TSS(n)),"c" ((long) task[n])); \
}

这是在sched.h中对switch_to的宏替换,主要功能就是当传入的参数对应的进程和current不是同一个就进行tss段和ldt段的切换

栈切换:

我们需要将tss方式转化为栈切换方式,需要下一个进程的pcb表项,也就是*p的值,同时因为传输的不是next了,但是switch_to中还需要LDT的切换,所以我们需要进行next的LDT的计算和传输 * p给switch_to函数。

        struct task_struct *pnext;
        while (1) {
                c = -1;
                next = 0;
                i = NR_TASKS;
                p = &task[NR_TASKS];
                while (--i) {
                        if (!*--p)
                                continue;
                        if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
                                c = (*p)->counter, next = i,pnext=*p;
                }
                if (c) break;
                for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
                        if (*p)
                                (*p)->counter = ((*p)->counter >> 1) +
                                                (*p)->priority;
        }
        switch_to(pnext,5 * 8 + next * 16);
}

pnext指向的就是下一个进程pcb的地址,switch_to的第二个参数就是下一个进程的ldt的位置
在这里插入图片描述
上图为一个进程的tss和ldt位置对应关系
在tss模式下,使用了宏定义的方式在sched.h中定义了TSS(int next),用来获得next的tss的位置。我们现在需要注释sched.h中的switch_to,在sched.c中重写写一个switch_to函数
实验给出了switch_to函数的大概模板
变量说明:

  • struct tss_struct *tss = &(init_task.task.tss)
  • kernel_stack,在pcb中增加定义的一个long变量,存放的是内核栈的位置,在sched.h中,因为增加了一个变量,所以当我们定义init_task时,我们知道代码使用的时一个宏定义来赋值,所以我们在INIT_TASK这个宏定义中添加kernel_stack的值
#define INIT_TASK \
/* state etc */	{ 0,15,15, /*添加的kernel_stack值 也就是init_tack的pcb页最后一个字节*/PAGE_SIZE+(long)&init_task\
/* signals */	0,{{},},0, \
/* ec,brk... */	0,0,0,0,0,0, \
/* pid etc.. */	0,-1,0,0,0, \
/* uid etc */	0,0,0,0,0,0, \
/* alarm */	0,0,0,0,0,0, \
/* math */	0, \
/* fs info */	-1,0022,NULL,NULL,NULL,0, \
/* filp */	{NULL,}, \
switch_to:
    pushl %ebp
    movl %esp,%ebp
    pushl %ecx
    pushl %ebx
    pushl %eax
    movl 8(%ebp),%ebx
    cmpl %ebx,current
    je 1f					//取出一个参数,放入ebx中,和current比较,当相同就结束switch_to函数
! 切换PCB
    mov %ebx,%eax		   //ebx为next的地址
    xchgl %eax,current	   //交换将current写入到eax,eax写入到current
! TSS中的内核栈指针的重写
    movl tss,%ecx			
	addl $4096,%ebx
	movl %ebx,ESP0(%ecx) 
	//tss+4得到的就是esp0的地址,将ebx+4096表示的是pcb+4096表示一个页的页尾。ESP0被宏定义为4
	//next的tss变为0长度
	//这样的作用,每次开始运行一个进程之前,将内核栈长度置为0,
	//当下一个这个进程进行了系统调用int之后,就直接在内核栈为0的位置开始第一轮的压栈ss,esp,eflag,cs,ip,
	//但是这对于内核栈中的数据并不影响,因为真正的内核栈地址在kernel_stack中,我们在后面只需要将esp换为kernel_stack就可以了。
	//可以知道的是,0号进程的tss属于当前正在运行的进程,只有当这个进程被调度离开,
	//这个结构再交给下一个进程,而这个tss中的esp0的值也就是当前进程被中断时,栈开始的位置,当压完栈后,
	//将tss交给下一个线程,然后使用kernel_stack来存储当前线程的栈顶位置。
! 切换内核栈
    movl %esp,KERNEL_STACK(%eax)
	! 再取一下 ebx,因为前面修改过 ebx 的值
	//当前栈顶存放到current的kernel_stack中
	//KERNEL_STACK为宏定义的kernel_stack在pcb中的位置。
	movl 8(%ebp),%ebx
	//next的开始地址
	movl KERNEL_STACK(%ebx),%esp
	//esp换为next的esp
! 切换LDT
    movl $0x17,%ecx
    mov %cx,%fs
! 和后面的 clts 配合来处理协处理器,由于和主题关系不大,此处不做论述
    cmpl %eax,last_task_used_math
    jne 1f
    clts

1:  popl %eax
    popl %ebx
    popl %ecx
    popl %ebp
ret

上述代码中主要实现的就是

  • 切换PCB
  • TSS中的内核栈指针的重写
  • 切换内核栈
  • 切换LDT
    这四个工作

tss为一个定义的全局变量,struct tss_struct *tss = &(init_task.task.tss),指向的是init_task.task.tss的地址,当计算机开机,第一次执行到这里,创建了第一个进程,tss就执行了这个进程的task.tss的位置,之后便不再变化了,tss一直指向这个位置,用于当current进程被中断了,就将值保存
全局变量tss指向的就是当前进程的tss信息,虽然我们不使用tss方式来切换进程了,但是我们还是需要tss的这种机制来保存当前进程的信息,因为intel的机制,int指令会去tr中寻找tss的esp0来作为这个进程的内核栈的栈顶,从这个地方开始存放数据,我们在这个将current的tss的esp0位置置为这个pcb的最后一个字节,也就是栈顶为最后一个字节,那么当这个current下一次被中断,tss中存放的就仍然是栈顶的位置。

栈结构:
高地址--------------------------------------------------------------------------------->低地址
SS,ESP,EFLAGS,CS,EIP,DS,ES,FS,EDX,ECX,EBX,EAX,ret_from_sys_call
高地址--------------------------------------------------------------------------------->低地址
SS-EIP的值就是int指令将值push到栈中的,DS-EBX就是system_call函数开始时push进去的,EAX是执行完sys_call_table之后得到的返回值
当程序执行到这里,我们进行判断,当当前运行进程状态为阻塞或者时钟不足,就调用reschedule函数,push一个ret_from_sys_call,这是一个地址,执行这个函数就会退出system_call函数,这个地址的被pop的位置在当我们执行了schdule(),schdule的"}"会作为ret指令,将eip置为ret_from_sys_call在这里插入代码片

ret_from_sys_call:
	movl current,%eax		# task[0] cannot have signals
	cmpl task,%eax
	je 3f
	cmpw $0x0f,CS(%esp)		# was old code segment supervisor ?
	jne 3f
	cmpw $0x17,OLDSS(%esp)		# was stack segment = 0x17 ?
	jne 3f
	movl signal(%eax),%ebx
	movl blocked(%eax),%ecx
	notl %ecx
	andl %ebx,%ecx
	bsfl %ecx,%ecx
	je 3f
	btrl %ecx,%ebx
	movl %ebx,signal(%eax)
	incl %ecx
	pushl %ecx
	call do_signal
	popl %eax
3:	popl %eax
	popl %ebx
	popl %ecx
	popl %edx
	pop %fs
	pop %es
	pop %ds
	iret

我们可以看到ret_from_system_call中的后面部分,进行push和iret,但执行完就正好达到了栈平衡

fork():

当我们进行创建进程的时候,执行fork函数,执行的主要函数是copy_process():

long *krnstack;
p = (struct task_struct *) get_free_page();	//申请一个页
krnstack = (long)(PAGE_SIZE +(long)p);		//内核栈位置
 *(--krnstack) = ss & 0xffff;
 *(--krnstack) = esp;
 *(--krnstack) = eflags;
 *(--krnstack) = cs & 0xffff;
 *(--krnstack) = eip;
 *(--krnstack) = ds & 0xffff;
 *(--krnstack) = es & 0xffff;
 *(--krnstack) = fs & 0xffff;
 *(--krnstack) = gs & 0xffff;
 *(--krnstack) = esi;
 *(--krnstack) = edi;
 *(--krnstack) = edx;
 *(--krnstack) = (long)first_return_from_kernel;
 *(--krnstack) = ebp;
 *(--krnstack) = ecx;
 *(--krnstack) = ebx;
 *(--krnstack) = 0;
 p->kernelstack = krnstack;
 ......

我们需要在这个函数中为新增的进程完成内核栈的初始化,也就是按照父进程的内核栈,仿造一个新的内核栈。之后加入到就绪队列中。当下一次轮到这个进程执行,在switch_to函数中弹出0,ebx,ecx,ebp后schdule的"}"弹到first_return_from_kernel执行。
然后使用一个fisrst_return_from_kernel

popl %edx
popl %edi
popl %esi
pop %gs
pop %fs
pop %es
pop %ds
iret

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值