基于内核栈切换的进程切换
实验目的:将linux-0.11中采用的TSS切换部分去掉,取而代之的是基于堆栈的切换程序,写成一段基于堆栈切换的代码
要实现基于内核栈的任务切换,主要完成如下三件工作
- 重写switch_to
- 将重写的switch_to和schedule()函数接在一起
- 修改原本的fork()
第一步,修改schedule()函数
schedule函数在kernel/sched.c文件中
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i, pnext = *p;
//.......
switch_to(pnext, _LDT(next)); //修改switch_to,实验中的LDT缺少了一个下划线,这里要给它补上,不然会报错
//同时在schedule中要添加一个全局变量
struct task_struct *pnext = &(init_task.task);
在sched.c文件中,要定义一个全局变量tss,虽然我们放弃使用tss切换进程,但是在中断的时候,要找到内核栈的位置,并将用户态的SS:ESP以及EFLAGS这五个寄存器压到内核栈中,这是沟通用户栈和内核栈的桥梁,而找到用户栈位置就依靠TR指向的当前的TSS,所有进程都公用这个tss,即0号进程的tss。
struct tss_struct *tss = &(init_task.task.tss);
第二步,实现switch_to
实现switch_to主要在system_call.s文件中完成
重写TSS中的内核栈指针
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)
在include/linux/sched.h中
添加long kernelstack作为内核栈指针,kernelstack最好不要放在第一个,因为有关操作这个结构的一些硬编码,一旦增加了kernelstack,这些硬编码也要跟着一起改,而第一个位置的硬编码特别多,所以不能放在第一个位置。
struct task_struct {
long state;
long counter;
long priority;
long kernelstack;
...
}
修改了struct task_struct的内容,我们修改kernel/system_call.s中的硬编码。
#define INIT_TASK \
/* state etc */ { 0,15,15,PAGE_SIZE+(long)&init_task, \
/* signals */ 0,{{},},0, \
在编写switch_to,我们先将原先的switch_to注释掉,
/*#define switch_to()
{\
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])); \
}*/
接下来就是switch_to的编写了,这里先给出完整的代码,然后再逐个进行分析。
.align 2
first_return_from_kernel:
popl %edx
popl %edi
popl %esi
pop %gs
pop %fs
pop %es
pop %ds
iret
switch_to:
pushl %ebp
movl %esp, %ebp
pushl %ecx
pushl %ebx
pushl %eax
movl 8(%ebp), %ebx
cmpl %ebx, current
je 1f
mov %ebx, %eax
xchgl %eax, current
movl tss, %ecx
addl $4096, %ebx
movl %ebx, ESP0(%ecx)
movl %esp, KERNEL_STACK(%eax)
movl 8(%ebp), %ebx
movl KERNEL_STACK(%ebx), %esp
movl 12(%ebp) , %ecx
lldt %cx
movl $0x17, %ecx
mov %cx, %fs
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
切换PCB
movl %ebx, %eax
xchgl %eax, current
movl的用法:
movl %eax, %ebx #把32位的EAX寄存器值传送给32为的EBX寄存器值
xchal的用法:交换eax和current的neir
因此经历这两条指令后,eax指向当前进程,ebx和current指向下一个进程
TSS中的内核栈指针的重写
movl tss,%ecx
addl $4096,%ebx
movl %ebx,ESP0(%ecx)
定义 ESP0 = 4 是因为 TSS 中内核栈指针 esp0 就放在偏移为 4 的地方,看一看 tss 的结构体定义就明白了,而addl $4096, %ebx
,ebx加上4096就是4k的大小,因为栈的指针是在esp上面4k的位置的,所以加上4096
切换内核栈
movl %esp,KERNEL_STACK(%eax)
! 再取一下 ebx,因为前面修改过 ebx 的值
movl 8(%ebp),%ebx
movl KERNEL_STACK(%ebx),%esp
切换内核栈就是将寄存器esp的值放在PCB中,再从下一个进程的PCB中取出保存的内核栈栈顶放在当前的esp中
切换LDT
movl 12(%ebp), %ecx
ldt %cx
movl $0x17, %ecx
mov %cx, %fs
指令movl 12(%ebp),%ecx
负责取出对应LDT(next)
的那个参数,指令lldt %cx
负责修改LDTR
寄存器,一旦完成了修改,下一个进程在执行用户态程序时使用的映射表就是自己的LDT
表了,地址空间实现了分离
切换LDT之后
movl $0x17,%ecx
mov %cx,%fs
这两句代码的含义是重新取一下段寄存器 fs 的值,这两句话必须要加、也必须要出现在切换完 LDT 之后,这是因为在实践项目 2 中曾经看到过 fs 的作用——通过 fs 访问进程的用户态内存,LDT 切换完成就意味着切换了分配给进程的用户态内存地址空间,所以前一个 fs 指向的是上一个进程的用户态内存,而现在需要执行下一个进程的用户态内存,所以就需要用这两条指令来重取 fs
关于first_return_from_kernel
这一段代码就是中断返回的关键代码,它将用户态的参数从栈中pop出来
修改fork
因此system_call中的sys_fork里面实际上调用的是fork.c中的copy_process(),因此,我们要在fork.c文件中修改**copy_process()**函数
首先在函数中定义一个变量krnstack
long * krnstack;
接下来,将p->tss这类初始化代码给注释掉,因为这是为TSS进程切换服务的,我们这里不需要这些
然后,在copy_process()函数中实现对内核栈的初始化
krnstack = (long *)(PAGE_SIZE + (long) p); //内核栈指针位置
*(--krnstack) = ss & 0xffff; //用户态的位置
*(--krnstack) = esp; //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_kernel;
*(--krnstack) = ebp;
*(--krnstack) = ecx;
*(--krnstack) = ebx;
*(--krnstack) = 0;
p->kernelstack = krnstack;
上面的这些代码就是实现了对内核栈的初始化。
最后一步
因为switch_to和first_return_from_kernel被其他文件使用,因此我们需要在system_call.s中将其设为全局可见
.globl switch_to, first_return_from_kernel
然后在sched.c文件中
extern long switch_to(struct task_struct *p, unsigned long address);
在fork.c文件中
extern void_return_kernel(void);
实验结果
中将其设为全局可见
.globl switch_to, first_return_from_kernel
然后在sched.c文件中
extern long switch_to(struct task_struct *p, unsigned long address);
在fork.c文件中
extern void_return_kernel(void);
实验结果
部分参考了这位博主
https://blog.csdn.net/a634238158/article/details/100118927