【哈工大_操作系统实验】Lab5 基于内核栈切换的进程切换

October 01, 2024

Github代码仓库链接

本节将更新哈工大《操作系统》课程第五个 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)

需要修改的地方:

  1. inux0.11 进程控制块(pcb)中是没有保存内核栈信息的寄存器,所以需要在 include/linux/sched.h 文件中PCB定义 task_struct 中添加内核栈指针
// PCB 定义
struct task_struct {
    long state;
    long counter;
    long priority;
    long kernelstack;
    ...
//......
  1. 由于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 位于这页内存的低地址,栈位于这页内存的高地址

  1. kernel/sched.h 文件中声明一下使用 switch_to()
// 使用switch_to还需要声明一下
extern void switch_to(struct task_struct *pnext, unsigned long ldt); 

二、修改 fork.c

通过压栈的方式,把进程的用户栈内核栈通过内核栈中的 SS:ESPCS: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;
}

需补充的内容:

  1. 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
  1. 注释掉 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

  1. 用户栈->内核栈:调用fork时会执行0x80中断,进而进入内核态。进入前,CPU会将SS、ESP、EFLAGS、CS、EIP压入内核栈,进而父进程内核栈和用户栈取得联系。(system_call将DS、ES、FS....压入栈)
  2. 内核栈->PCB:在PCB的结构体中添加了内核栈指针。
  3. PCB完成切换:通过 Schedule 中的 switch_to 传入下个进程
  4. PCB->内核栈:copy_process 中获取内核栈位置,并在PCB中内核栈指针赋值
  5. 内核栈->用户栈:为内核栈指针进行赋值填入用户态SS:ESP,并设置包含iret的返回函数

在这里插入图片描述


Profile picture

Written by JokerDebug who works at Southeast University, Nanjing, China You can follow me on Github