本节将更新哈工大《操作系统》课程第五个 Lab 实验 基于内核栈切换的进程切换。按照实验书要求,介绍了非常详细的实验操作流程,并提供了超级无敌详细的代码注释。
Linux0.11 采用 TSS 和一条指令完成任务切换,虽然简单但执行时间长。
堆栈实现任务切换更快,且可以使用指令流水的并行优化技术
实验目的:
- 深入理解进程和进程切换的概念;
- 综合应用进程、CPU 管理、PCB、LDT、内核栈、内核态等知识解决实际问题;
- 开始建立系统认识。
实验任务:
本次实践项目就是将 Linux 0.11 中采用的 TSS 切换部分去掉,取而代之的是基于堆栈的切换程序。具体的说,就是将 Linux 0.11 中的 switch_to 实现去掉,写成一段基于堆栈切换的代码。
1、编写汇编程序 switch_to:
2、完成主体框架;
3、在主体框架下依次完成 PCB 切换、内核栈切换、LDT 切换等;
4、修改 fork(),由于是基于内核栈的切换,所以进程需要创建出能完成内核栈切换的样子。
5、修改 PCB,即 task_struct 结构,增加相应的内容域,同时处理由于修改了 task_struct 所造成的影响。
6、用修改后的 Linux 0.11 仍然可以启动、可以正常使用。
文件名 | 介绍 |
---|---|
hit-操作系统实验指导书.pdf | 哈工大OS实验指导书 |
Linux内核完全注释(修正版v3.0).pdf | 赵博士对Linux v0.11 OS进行了详细全面的注释和说明 |
file1615.pdf | BIOS 涉及的中断数据手册 |
hit-oslab-linux-20110823.tar.gz | hit-oslab 实验环境 |
gcc-3.4-ubuntu.tar.gz | Linux v0.11 所使用的编译器 |
Bochs 汇编级调试指令 | bochs 基本调试指令大全 |
最全ASCII码对照表0-255 | 屏幕输出字符对照的 ASCII 码 |
x86_64 常用寄存器大全 | x86_64 常用寄存器大全 |
一、编写 switch_to()
在 kernel/sched.c
文件中修改 switch_to() 函数传入参数。
- 目标进程PCB指针
- 下一个进程对应的局部描述符表
struct tss_struct *tss = &(init_task.task.tss)
void schedule(void)
{
int i,next,c;
struct task_struct ** p;
// 声明 pnext
struct task_struct *pnext = &(init_task.task); // 定义目标进程PCB指针
// ....
while (1) {
c = -1;
next = 0;
// 给 pnext 赋值
pnext = task[next]
// ....
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i, pnext = *p;
// ....
}
switch_to(pnext, _LDT(next));
}
在 kernel/system_call.s
中添加 switch_to 实现(代码中给出了非常详细的注释)
KERNEL_STACK = 12
ESP0 = 4
switch_to:
! c语言调用汇编,处理栈帧ebp
pushl %ebp ! 保存帧指针
movl %esp,%ebp ! 将栈指针赋给帧指针,即在栈顶创建新函数栈帧
! 保存了几个常用的寄存器值
pushl %ecx
pushl %ebx
pushl %eax
! ebp+4:返回地址;ebp+8:第一个参数;ebp+12:第二个参数
movl 8(%ebp),%ebx ! 将下一个进程PCB指针存入ebx
cmpl %ebx,current ! 判断下一个进程是否为当前进程
je 1f ! 跳转去弹出保存的寄存器值并返回,f-向前
! 完成 PCB 切换(修改current)
movl %ebx,%eax
xchgl %eax,current ! 交换eax和current的值
! eax-指向当前进程,ebx、current-指向下一个进程
! 重写 TSS 中的内核栈指针(修改TSS的第四个字段)
! 寻找当前进程的内核栈,为了从用户栈切换到内核栈
movl tss,%ecx
! 一页内存大小为4KB,PCB位于内存低地址,栈位于内存的高地址
addl $4096,%ebx ! 得到新进程的栈指针
movl %ebx,ESP0(%ecx) ! 将下一进程内核栈指针存入 TSS 的 esp0
! 内核栈的切换(即修改esp)
! eax-指向当前进程
movl %esp,KERNEL_STACK(%eax) ! 将当前栈指针保存到当前进程的内核栈指针字段中,确保下次切换回来可以恢复
movl 8(%ebp),%ebx ! 将下一个进程PCB指针存入ebx
movl KERNEL_STACK(%ebx),%esp ! 将栈指针设置为下一进程的内核栈指针
! LDT 的切换(修改LDTR)
movl 12(%ebp), %ecx ! 取出第二个参数,即LDT(next)
lldt %cx ! 修改 LDTR 寄存器,实现用户态程序 LDT 映射表切换
! 段寄存器fs:访问进程的用户态内存
! 现在的 fs 指向上一个进程的用户态内存,而现在LDT切换完成,用户态内存已经改变,所以需要重取fs
movl $0x17,%ecx
mov %cx,%fs
! nonsense
cmpl %eax,last_task_used_math
jne 1f
clts
! 恢复寄存器值并返回
1: popl %eax
popl %ebx
popl %ecx
popl %ebp
ret
! 提示:汇编指令后的l表示操作32位数据(b-8, w-16, l-32, q-64)
需要修改的地方:
- inux0.11 进程控制块(
pcb
)中是没有保存内核栈信息的寄存器,所以需要在include/linux/sched.h
文件中PCB定义task_struct
中添加内核栈指针:
// PCB 定义
struct task_struct {
long state;
long counter;
long priority;
long kernelstack;
...
//......
- 由于PCB定义改变,所以0号进程的PCB初始化也要新增添加的内核栈指针初始化。在
include/linux/sched.h
文件中INIT_TASK
定义上添加内核栈指针初始化(即在第四项添加)
#define INIT_TASK \
/* state etc */ { 0,15,15,PAGE_SIZE+(long)&init_task, \
/* signals */ 0,{{},},0, \
提示:Linux 0.11 进程的内核栈和该进程的 PCB 在同一页内存上(一块 4KB 大小的内存),其中 PCB 位于这页内存的低地址,栈位于这页内存的高地址
- 在
kernel/sched.h
文件中声明一下使用 switch_to()
// 使用switch_to还需要声明一下
extern void switch_to(struct task_struct *pnext, unsigned long ldt);
二、修改 fork.c
通过压栈的方式,把进程的用户栈和内核栈通过内核栈中的
SS:ESP
,CS:IP
关联在一起
修改 kernel/fork.c
文件中的 copy_process()
,如下所示:
extern void first_return_kernel(void);
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;
/* add lab4 */
long *krnstack;
// 1. 申请内存作为子进程的PCB
p = (struct task_struct *) get_free_page();
// 2. 关联子进程的用户栈和内核栈,就是将父进程内核栈前五个内容拷贝
krnstack = (long *) (PAGE_SIZE + (long) p); // 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;
// 3. 设置ret地址代码。父线程执行switch_to到了最后一步,此时PCB的esp已经切换到了子线程,基地址是对的,只需设置相对地址,无需jmp
*(--krnstack) = (long) first_return_kernel;
// 4. 继续压栈,保护子线程现场
*(--krnstack) = ebp;
*(--krnstack) = ecx;
*(--krnstack) = ebx;
// 这里的 0 最有意思。
*(--krnstack) = 0; // 子进程内核栈中,fork后eax返回0
if (!p)
return -EAGAIN;
task[nr] = p;
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
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;
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 */
// 5. 设置结构体指针,把switch_to中要的东西存进去
p->kernelstack = krnstack; // 第四段(将PCB与内核栈关联),上面只是得到内核栈位置并赋值,这边才实现关联
return last_pid;
}
需补充的内容:
- 在
kernel/system_call.s
中添加first_return_kernel
实现
! system_call.s
! 汇编语言中定义的方法可以被其他调用需要
.globl switch_to
.globl first_return_kernel
! 硬编码改变 these are offsets into the task-struct
! 修改了几个变量
state = 0 # these are offsets into the task-struct.
counter = 4
priority = 8
# 在 task_struct(PCB) 添加了 kernelstack,所以要修改
signal = 16
sigaction = 20
blocked = (33*16+4)
.align 2
first_return_kernel:
popl %edx
popl %edi
popl %esi
pop %gs
pop %fs
pop %es
pop %ds
iret
- 注释掉
include/linux/sched.h
文件中的switch_to
实现
/*#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])); \
}*/
最后,编译并运行,若可以成功启动,则实验成功。
cd oslab_Lab4/linux-0.11
make all
../run
总结:
本次实验归根结底还是完成了李老师所述的五段论:
(fork:int 0x80
->system_call
->sys_call_table
->sys_fork
->find_empty_process
->copy_process
->system_call(rescjedule)
->schedule
->switch_to
)
- 用户栈->内核栈:调用
fork
时会执行0x80
中断,进而进入内核态。进入前,CPU会将SS、ESP、EFLAGS、CS、EIP压入内核栈,进而父进程内核栈和用户栈取得联系。(system_call
将DS、ES、FS…压入栈) - 内核栈->PCB:在PCB的结构体中添加了内核栈指针。
- PCB完成切换:通过
Schedule
中的switch_to
传入下个进程 - PCB->内核栈:
copy_process
中获取内核栈位置,并在PCB中内核栈指针赋值 - 内核栈->用户栈:为内核栈指针进行赋值填入用户态SS:ESP,并设置包含
iret
的返回函数