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

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

时间:2024-10-17 21:47:04浏览次数:3  
标签:task struct long 哈工大 切换 Lab5 PCB tss 内核

本节将更新哈工大《操作系统》课程第五个 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.pdfBIOS 涉及的中断数据手册
hit-oslab-linux-20110823.tar.gzhit-oslab 实验环境
gcc-3.4-ubuntu.tar.gzLinux 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

在这里插入图片描述

总结

本次实验归根结底还是完成了李老师所述的五段论:
forkint 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的返回函数

在这里插入图片描述

标签:task,struct,long,哈工大,切换,Lab5,PCB,tss,内核
From: https://blog.csdn.net/weixin_53159274/article/details/143029437

相关文章

  • 不用再来回切换不同账号啦!这个多微管理神器快码住!
    无论是为了工作、社交还是其他原因,来回切换不同的微信号实在是太麻烦了,不仅浪费时间,还容易遗漏重要信息。为了帮助大家告别这种困扰,今天要分享一个多微管理神器——个微管理系统,让你从此轻松管理多个微信号!1、多账号同时登录,从此不再繁琐你可以在系统上同时登录多个微信号,同......
  • QT实现滑动页面切换
    1.界面实现效果以下是具体的项目需要用到的效果展示。2.简介原理:使用Qt的QPropertyAnimation动画类,这里简单来说就是切换两个界面。这个widget里面可以放很多个待切换的界面,每次切换的时候将当前界面和切换后的界面显示,其他界面都隐藏,然后当前界面移动到主界面之外,下一......
  • FineReport 动态列切换统计维度
    目标:自己选择统计维度进行数据的汇总统计数据集参数实现动态列1、单维度切换1.1、数据库查询SELECT${统计维度}"统计维度",count(1)"人员总数"FROM人员花名册groupby${统计维度}1.2、内容配置$统计维度下拉框设置ARRAY("年龄段","学历","岗位职......
  • keepalived跨网段主备切换
    1.主节点(192.168.1.10/24)配置,/etc/keepalived/keepalived.confvrrp_scriptchk_nginx{  script/opt/test.sh  interval2  weight-20}vrrp_instanceVI_1{stateMASTERinterfaceeth0virtual_router_id51priority100nopreempt......
  • 宝塔面板切换PHP版本
    可以通过修改当前站点指定的PHP运行版本,合适安装多个PHP版本的用户。支持自定义PHP的连接配置信息来运行,也支持修改该站点为纯静态站点,不通过PHP服务运行。PHP7不支持mysql扩展,默认安装mysqli以及mysql-pdo。Session隔离是指在一个多用户的系统中,为每个用户提供独立的会话环境,使......
  • 从远程桌面连接切换回主屏幕的快捷键
     转自:https://www.cnblogs.com/yozima/p/18137940因为工作经常要用到远程主机,所以经常要切换来切换去,碍于比较难多申请一个屏幕专门用作远程桌面,所以想通过快捷键的方式进行快速切换,但是远程桌面全屏下面本机的快捷键和远程桌面的快捷键没办法很好的兼容,以下是探索的几种实现方......
  • 用sdkman管理多个jdk切换
    前言最近项目前后端进行升级,需要在jdk8和jdk17两个版本切换。最简单的是通过手动切换,但切换过程太繁琐,修改环境变量,达到切换目的。于是尝试其它解决方案,最终确实使用sdkman工具。  sdkman 是一款面向Java开发者的命令行工具,旨在简化操作系统上SDKs的管理。支持跨平台使用,提供......
  • 进程的调度和切换
    目录1.进程的调度和切换的基本概念2.其他概念3.进程切换的时候,CPU和进程的操作3.1CPU基本概念4.Linux2.6内核进程调度队列1.进程的调度和切换的基本概念进程在CPU上运行的时候,CPU并不会一直运行一个进程,直到进程结束。现代操作系统的进程之间都是根据时间片进行轮转......
  • 电脑快速切换IP地址命令是什么?详解与实践
    有时,出于安全考虑或测试需要,我们可能需要快速切换电脑的IP地址。虽然这一过程在初学者看来可能略显复杂,但通过简单的命令和步骤,即使是普通用户也能轻松实现。本文将详细介绍在Windows系统中快速切换IP地址的几种方法,特别是通过命令提示符来执行的操作。一、IP地址与网络环境......
  • Win10 小技巧:切换大小写自动提示音
    在Win10里,我们可以给CapsLock键、NumLock键设置提示音,及时了解输入法状态。‍如何设置按下「Win+I」打开设置,然后搜索「切换」,然后点击「打开粘滞键、切换键、或筛选键时显示消息」:​‍‍然后开启:​‍‍这样按CapsLock键时会播放声音。此外声音是不同:切换......