Linux0.11操作系统(哈工大李治军老师)实验楼实验5-基于内核栈切换的进程切换

Linux0.11操作系统(哈工大李治军老师)实验楼实验5-基于内核栈切换的进程切换

任务
在linux0.11中实现基于内核栈切换的进程切换

简介
现在的 Linux 0.11 采用 TSS(后面会有详细论述)和一条指令就能完成任务切换,虽然简单,但这指令的执行时间却很长,在实现任务切换时大概需要 200 多个时钟周期。

而通过堆栈实现任务切换可能要更快,而且采用堆栈的切换还可以使用指令流水的并行优化技术,同时又使得 CPU 的设计变得简单。所以无论是 Linux 还是 Windows,进程/线程的切换都没有使用 Intel 提供的这种 TSS 切换手段,而都是通过堆栈实现的。

本次实践项目就是将 Linux 0.11 中采用的 TSS 切换部分去掉,取而代之的是基于堆栈的切换程序。具体的说,就是将 Linux 0.11 中的 switch_to 实现去掉,写成一段基于堆栈切换的代码。

1.基于tss切换的进程切换
在现在的 Linux 0.11 中,真正完成进程切换是依靠任务状态段(Task State Segment,简称 TSS)的切换来完成的。

具体的说,在设计“Intel 架构”(即 x86 系统结构)时,每个任务(进程或线程)都对应一个独立的 TSS,TSS 就是内存中的一个结构体,里面包含了几乎所有的 CPU 寄存器的映像。有一个任务寄存器(Task Register,简称 TR)指向当前进程对应的 TSS 结构体,所谓的 TSS 切换就将 CPU 中几乎所有的寄存器都复制到 TR 指向的那个 TSS 结构体中保存起来,同时找到一个目标 TSS,即要切换到的下一个进程对应的 TSS,将其中存放的寄存器映像“扣在” CPU 上,就完成了执行现场的切换,如下图所示。
在这里插入图片描述
通过kernel/sched.c中

void schedule(void)
{
	...
	switch_to(next);
}

来完成进程的切换,其中switch_to()是宏定义:

#define switch_to(n) {
    struct{long a,b;} tmp;
    __asm__(
        "movw %%dx,%1"
        "ljmp %0" ::"m"(*&tmp.a), "m"(*&tmp.b), "d"(TSS(n)
    )
 }

#define FIRST_TSS_ENTRY 4

#define TSS(n) (((unsigned long) n) << 4) + (FIRST_TSS_ENTRY << 3))

TSS(n)得到进程n的TSS选择子:
GDT表中每个进程对应有1个TSS的描述符和1个LDT的描述符,每个描述符长度是8个字节,所以n*16,即n<<4,又因为第0个进程在GDT表中第四个位置开始,所以FIRST_TSS_ENTRY定义为4,进程0的起始地址为FIRST_TSS_ENTRY << 3,第n个进程的TSS描述符的选择子为((unsigned long) n) << 4) + (FIRST_TSS_ENTRY << 3)

GDT表的结构如图所示:
在这里插入图片描述
将TSS(n)装在到dx寄存器中,将dx的内容放置到结构体tmp中32位长整数b的前16位,现在 64 位 tmp 中的内容是前 32 位为空,这个 32 位数字是段内偏移,接下来的 16 位是 n * 16 + 4 * 8,这个数字是段选择子,再接下来的 16 位也为空。
所以swith_to 的核心实际上就是 ljmp 空, n*16+4*8

这时进程就完成了切换。

2.编写switch_to

现在我们采用切换内核栈的方式来完成进程的切换,所以在新的switch_to中会用到当前进程的pcb、目标进程的pcb、当前进程的内核栈、目标进程的内核栈等信息。在linux中一个进程的内核栈与该进程的pcb在同一页内存上,其中 PCB 位于这页内存的低地址,栈位于这页内存的高地址,其布局大致为
|---------------------------------------4096个字节----------------------------------------------------|
|-------task_struct----------|----------------------剩余未用------------|---------------内核栈----|

现在切换进程不在需要TSS(next)了,但是还是需要LDT(next),因为每个进程需要有自己的LDT。
将目前的schedule()函数改写:

schedule()
{
	struct task_struct *pnext = &(INIT_TASK.task);
	
	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汇编代码:

为了将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代码

这个函数依次主要完成如下功能:由于是 C 语言调用汇编,所以需要首先在汇编中处理栈帧,即处理 ebp 寄存器;接下来要取出表示下一个进程 PCB 的参数,并和 current 做一个比较,如果等于 current,则什么也不用做;如果不等于 current,就开始进程切换,依次完成 PCB 的切换、TSS 中的内核栈指针的重写、内核栈的切换、LDT 的切换以及 PC 指针(即 CS:EIP)的切换。

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

当switch_to执行完前5条指令后,内核栈的状态如图所示:

| ss |
| sp |
| EFLAGS |
| PC |
| cs |
| ds |
| es |
| fs |
| edx |
| ecx |
| ebx |
| eax |
| ret_from_sys_call |
| LDT(next) |
| pnext |
| } |
| ebp | <— ebp
| ecx |
| ebx |
| eax | <— esp

此时ebp指向如图所示的位置,这能更加方便地对栈中地数据进行访问。

	movl 8(%ebp),%ebx
    cmpl %ebx,current
    je 1f

这三条指令:将pnext的地址放入ebx寄存器中,与当前进程current比较,如果相等,就跳到1f的位置,否则就继续执行接下去的指令。

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

此时ebx是下一个进程的地址,将ebx赋值给eax,交换eax和current的内容,此时current指向下一个进程,eax指向当前进程。

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

虽然不使用TSS进行任务切换,但是Intel的中断处理机制还要保持,所以要有一个当前TSS,在sched.c中定义struct tss_struct *tss = &(init_task.task.tss)全局变量,即进程0的tss,在任务切换时,不在变化tss。

tss的地址放入ecx中,此时ebx寄存器存放的是下一个进程,将ebx加上4096,也就是一个页,正好是下一个进程的最后的地址,因为一个进程的大小正好是一个页(4096字节)。然后将下一个进程的内核栈放入tssESP0偏移,ESP0需要手动在system_call.s中添加,ESP0=4

接下来进行内核栈的切换。

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

eax指向当前进程,KERNEL_STACK(%eax)表示当前进程的内核栈,但是目前pcb中还没有保存内核栈的域,所以要在include/linux/sched.h中定义:

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

因为这里改变了PCB结构体的定义,所以对进程0的定义也要跟着一起变化:

/* 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)

	mov 8(%ebp),%ebx
	mov KERNEL_STACK(%ebx),%esp

表示将下一个进程的地址移到ebx寄存器中,将下一个进程的内核栈指针赋值给esp,此时esp就指向了下一个进程的内核栈栈顶。

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

接下来就是切换LDT了,将下一个进程的LDT选择子移到ecx中,将下一个进程的LDT的选择子加载到LDTR中。

加载完LDT表以后,将fs寄存器置为0x17

为什么要在切换完 LDT 之后要重新设置 fs=0x17?而且为什么重设操作要出现在切换完 LDT 之后,出现在 LDT 之前又会怎么样?

因为通过fs访问进程的用户态内存,LDT切换完成后,fs还指向上一个进程的用户态内存,所以需要重置fs,使其指向当前进程的ldt[1],ldt[1] 是进程的代码段描述符。

关于LDT的具体解析:LDT

此时完成了一系列操作,使操作系统完成了PCB、TSS指针以及内核栈的切换,现在esp指针已经指向下一个进程的内核栈,但是下一个进程的内核栈还什么也没有。原因就是在fork进程时,还没有对进程的内核栈进行fork。

修改kernel/fork.c

    /* ...... */
    p = (struct task_struct *) get_free_page();
    /* ...... */
    p->pid = last_pid;
    p->father = current->pid;
    p->counter = p->priority;

    long *krnstack;
    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;
    /* ...... */
    }

system_call.s中添加标号为first_return_from_kernel的汇编代码:

/* kernel/system_call.s */
.align 2
first_return_from_kernel:
    popl %edx
    popl %edi
    popl %esi
    pop %gs
    pop %fs
    pop %es
    pop %ds
    iret

最后,注意由于switch_to()first_return_from_kernel都是在(kernel/system_call.s)中实现的,要想在(kernel/sched.c)和(kernel/fork.c)中调用它们,就必须在system_call.s中将这两个标号声明为全局的,同时在引用到它们的.c文件中声明它们是一个外部变量。

system_call.s中的全局声明

.globl switch_to
.globl first_return_from_kernel

对应.c文件中的外部变量声明:

extern long switch_to(struct task_struct *p, unsigned long address);//sched.c
extern long first_return_from_kernel(void);	//fork.c

具体解析:进程切换

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值