操作系统实验之基于内核栈切换的进程切换

终于利用课余的时间完成了这个实验,收获很多,代码链接:http://git.shiyanlou.com/xubing/shiyanlou_cs115/src/process_stack/teacher_process_stack

一 复习Git基础知识

1.首先复习下git的操作,实验平台使用的是实验楼,没有开通会员,所以每次需要把代码提交到自己建立的分支上面进行保存,下次需要进行修改的时候,在进行下载
1)初始化git
git init
2) 配置基本信息
git config –global user.name xxxx
git config –global user.emai xxxx
3) 查看远程的分支
git branch -d
4) 选择分支,进行代码同步到本地
git checkout –track origin/分支名
这个会在本地建立选择分支,同时切换到这个分分支
5) 查看分支
git status
6) 选择分支
git checkout 分支名
7) 提交分支代码
git push origin 分支名
git add 文件夹名
git commit -m “注释”
8)删除分支
  git brance -d 分支名

二 实验指导书

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上,就完成了执行现场的切换,如下图所示。
这里写图片描述
Intel架构不仅提供了TSS来实现任务切换,而且只要一条指令就能完成这样的切换,即图中的ljmp指令。具体的工作过程是:
(1)首先用TR中存取的段选择符在GDT表中找到当前TSS的内存位置,由于TSS是一个段,所以需要用段表中的一个描述符来表示这个段,和在系统启动时论述的内核代码段是一样的,那个段用GDT中的某个表项来描述,还记得是哪项吗?是8对应的第1项。此处的TSS也是用GDT中的某个表项描述,而TR寄存器是用来表示这个段用GDT表中的哪一项来描述,所以TR和CS、DS等寄存器的功能是完全类似的。
(2)找到了当前的TSS段(就是一段内存区域)以后,将CPU中的寄存器映像存放到这段内存区域中,即拍了一个快照。
(3)存放了当前进程的执行现场以后,接下来要找到目标进程的现场,并将其扣在CPU上,找目标TSS段的方法也是一样的,因为找段都要从一个描述符表中找,描述TSS的描述符放在GDT表中,所以找目标TSS段也要靠GDT表,当然只要给出目标TSS段对应的描述符在GDT表中存放的位置——段选择子就可以了,仔细想想系统启动时那条著名的jmpi 0, 8指令,这个段选择子就放在ljmp的参数中,实际上就jmpi 0, 8中的8。
(4)一旦将目标TSS中的全部寄存器映像扣在CPU上,就相当于切换到了目标进程的执行现场了,因为那里有目标进程停下时的CS:EIP,所以此时就开始从目标进程停下时的那个CS:EIP处开始执行,现在目标进程就变成了当前进程,所以TR需要修改为目标TSS段在GDT表中的段描述符所在的位置,因为TR总是指向当前TSS段的段描述符所在的位置。
上面给出的这些工作都是一句长跳转指令“ljmp 段选择子:段内偏移”,在段选择子指向的段描述符是TSS段时CPU解释执行的结果,所以基于TSS进行进程/线程切换的switch_to实际上就是一句ljmp指令:

#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])); \
}

GDT表的结构如下图所示,所以第一个TSS表项,即0号进程的TSS表项在第4个位置上,4<<3,即48,相当于TSS在GDT表中开始的位置(以字节为单位),TSS(n)找到的是进程n的TSS位置,所以还要再加上n<<4,即n16,因为每个进程对应有1个TSS和1个LDT,每个描述符的长度都是8个字节,所以是乘以16,其中LDT的作用就是上面论述的那个映射表,关于这个表的详细论述要等到内存管理一章。TSS(n)=n16+48,得到就是进程n(切换到的目标进程)的TSS选择子,将这个值放到dx寄存器中,并且又放置到结构体tmp中32位长整数b的前16位,现在64位tmp中的内容是前32位为空,这个32位数字是段内偏移,就是jmpi 0, 8中的0;接下来的16位是n16+48,这个数字是段选择子,就是jmpi 0, 8中的8,再接下来的16位也为空。所以swith_to的核心实际上就是“ljmp 空, n16+48”,现在和前面给出的基于TSS的进程切换联系在一起了。
这里写图片描述
2、基于内核栈的切换
不管使用何种方式进行进程切换(此次实验不涉及线程),总之要实现调度进程的寄存器的保存和切换,也就是说只要有办法保存被调度出cpu的进程的寄存器状态及数据,再把调度的进程的寄存器状态及数据放入到cpu的相应寄存器中即可完成进程的切换。由于切换都是在内核态下完成的所以两个进程之间的tss结构中只有几个信息是不同的,其中esp和trace_bitmap是必须切换的,但在0.11的系统中,所有进程的bitmap均一样,所以也可以不用切换。

调度进程的切换方式修改之前,我们考虑一个问题,进程0不是通过调度运行的,那进程0的上下文是如何建立的?因为在进程0运行时系统中并没有其他进程,所以进程0的建立模板一定可以为进程栈切换方式有帮助。所以先来分析一下进程0的产生。进程0是在move_to_user_mode宏之后直接进入的。在这之前一些准备工做主要是task_struct结构的填充。

#define move_to_user_mode() \ 
__asm__ ("movl %%esp,%%eax\n\t" \ 
    "pushl $0x17\n\t" \ 
    "pushl %%eax\n\t" \ 
    "pushfl\n\t" \ 
    "pushl $0x0f\n\t" \ 
    "pushl $1f\n\t" \ 
    "iret\n" \ 
    "1:\tmovl $0x17,%%eax\n\t" \ 
    "movw %%ax,%%ds\n\t" \ 
    "movw %%ax,%%es\n\t" \ 
    "movw %%ax,%%fs\n\t" \ 
    "movw %%ax,%%gs" \ 
    :::"ax")

这里0x17表示用户数据库,0x10表示内核数据段,就是改变现在段寄存的值,改变选择子
①需要对PCB数据结构进行补充,增加记录当前任务一个指针指向栈空间,如下所示,同时需要注意的是:在system_call.s中有对task的硬编码,需要进行修改,如下所示:

struct task_struct {
/* these are hardcoded - don't touch */
    long state; /* -1 unrunnable, 0 runnable, >0 stopped */
    long counter;
    long priority;
    /* 增加利用堆栈进行任务切换的,需要记录当前任务的栈起始位,
    注意的是:linux0.11中栈和task在同一页中,位于这页的高地址中/
    unsigned long kernelstack;
    long signal;
    struct sigaction sigaction[32];
    long blocked;   /* bitmap of masked signals */
/* various fields */
    int exit_code;
    unsigned long start_code,end_code,end_data,brk,start_stack;
    long pid,father,pgrp,session,leader;
    unsigned short uid,euid,suid;
    unsigned short gid,egid,sgid;
    long alarm;
    long utime,stime,cutime,cstime,start_time;
    unsigned short used_math;
/* file system info */
    int tty;        /* -1 if no tty, so it must be signed */
    unsigned short umask;
    struct m_inode * pwd;
    struct m_inode * root;
    struct m_inode * executable;
    unsigned long close_on_exec;
    struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
    struct desc_struct ldt[3];
/* tss for this task */
    struct tss_struct tss;
};

修改system_call.s如下:

KERNEL_STACK    = 12 //kernelstack的偏移
state   = 0     
counter = 4
priority = 8
signal  = 16    //修改
sigaction = 20  //修改    
blocked = (37*16)

②修改task结构,同时需要对进程0的修改,如下:

#define INIT_TASK \
/* state etc */ { 0,15,15, /*新增*/PAGE_SIZE+(long)&init_task,\
/* signals */   0,{{},},0, \
/* ec,brk... */ 0,0,0,0,0,0, \
/* pid etc.. */ 0,-1,0,0,0, \
/* uid etc */   0,0,0,0,0,0, \
/* alarm */ 0,0,0,0,0,0, \
/* math */  0, \
/* fs info */   -1,0022,NULL,NULL,NULL,0, \
/* filp */  {NULL,}, \
    { \
        {0,0}, \
/* ldt */   {0x9f,0xc0fa00}, \
        {0x9f,0xc0f200}, \
    }, \
/*tss*/ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
     0,0,0,0,0,0,0,0, \
     0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
     _LDT(0),0x80000000, \
        {} \
    }, \
}

③现在就需要对栈空间进行初始化,保证两套栈可以正常转化,如下如所示,初始化为中断栈空间数据一样,修改fork.c如下:
这里写图片描述

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) 
{ 
    /*melon - 添加用来取得内核栈指针*/ 
    long * krnstack; 
    /*melon added End*/ 
    struct task_struct *p; 
    int i; 
    struct file *f; 

    p = (struct task_struct *) get_free_page(); 
    if (!p) 
        return -EAGAIN; 

    /*melon  -取得当前子进程的内核栈指针*/ 
    krnstack=(long)(PAGE_SIZE+(long)p); //实际上进程每次进入内核,栈顶都指向这里。
    /*melon added End*/ 
    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; 
    //初始化内核栈内容,由于系统不再使用tss进行切换,所以内核栈内容要自已安排好 
    //下面部分就是进入内核后int之前入栈内容,即用户态下的cpu现场
    *(--krnstack) = ss & 0xffff; //保存用户栈段寄存器,这些参数均来自于此次的函数调用, 
                      //即父进程压栈内容,看下面关于tss的设置此处和那里一样。 
    *(--krnstack) = esp; //保存用户栈顶指针 
    *(--krnstack) = eflags; //保存标识寄存器 
    *(--krnstack) = cs & 0xffff; //保存用户代码段寄存器 
    *(--krnstack) = eip; //保存eip指针数据,iret时会出栈使用 ,这里也是子进程运行时的语句地址。即if(!fork()==0) 那里的地址,由父进程传递
    //下面是iret时要使用的栈内容,由于调度发生前被中断的进程总是在内核的int中,
    //所以这里也要模拟中断返回现场,这里为什么不能直接将中断返回时使用的
    //return_from_systemcall地址加进来呢?如果完全模仿可不可以呢?
    //有空我会测试一下。
    //根据老师的视频讲义和实验指导,这里保存了段寄存器数据。
    //由switch_to返回后first_return_fromkernel时运行,模拟system_call的返回 
    *(--krnstack) = ds & 0xffff; 
    *(--krnstack) = es & 0xffff; 
    *(--krnstack) = fs & 0xffff; 
    *(--krnstack) = gs & 0xffff; 
    *(--krnstack) = esi; 
    *(--krnstack) = edi; 
    *(--krnstack) = edx; 
    *(--krnstack) = ecx; //这三句是我根据int返回栈内容加上去的,后来发现不加也可以
                //但如果完全模拟return_from_systemcall的话,这里应该要加上。
    //*(--krnstack) = ebx; 
    //*(--krnstack) = 0; //此处应是返回的子进程pid//eax; 
   //其意义等同于p->tss.eax=0;因为tss不再被使用, 
   //所以返回值在这里被写入栈内,在switch_to返回前被弹出给eax; 

    //switch_to的ret语句将会用以下地址做为弹出进址进行运行 
    *(--krnstack) = (long)first_return_from_kernel; 
    //*(--krnstack) = &first_return_from_kernel; //讨论区中有同学说应该这样写,结果同上
    //这是在switch_to一起定义的一段用来返回用户态的汇编标号,也就是 
    //以下是switch_to函数返回时要使用的出栈数据
    //也就是说如果子进程得到机会运行,一定也是先 
    //到switch_to的结束部分去运行,因为PCB是在那里被切换的,栈也是在那里被切换的, 
    //所以下面的数据一定要事先压到一个要运行的进程中才可以平衡。 
    *(--krnstack) = ebp; 
    *(--krnstack) = eflags; //新添加 
    *(--krnstack) = ecx; 
    *(--krnstack) = ebx; 
    *(--krnstack) = 0; //这里的eax=0是switch_to返回时弹出的,而且在后面没有被修改过。 
            //此处之所以是0,是因为子进程要返回0。而返回数据要放在eax中,
            //由于switch_to之后eax并没有被修改,所以这个值一直被保留。
            //所以在上面的栈中可以不用再压入eax等数据。
            //将内核栈的栈顶保存到内核指针处 
    p->kernelstack=krnstack; //保存当前栈顶 
    //p->eip=(long)first_switch_from;
        //上面这句是第一次被调度时使用的地址 ,这里是后期经过测试后发现系统修改
        //后会发生不定期死机,经分析后认为是ip不正确导致的,但分析是否正确不得
        //而知,只是经过这样修改后问题解决,不知其他同学是否遇到这个问题。
    /*melon added End*/

④样子已经做好了,现在就需要修改schedule.c

struct task_struct * pnext=&(init_task.task); 
//保存需要切换的PCB,
//注意这里初始化必须为进程0的地址,因为:但没有进程可以切换的时候,就会调用0号进程,进行pause()操作
…
while (1) { 
        c = -1; 
        next = 0; 
        /*为pnext赋初值,让其总有值可用。*/ 
        pnext=task[next]; //最初我并没有加这句,导致如果系统没有进程可以调度时传递进去的是一个空值,系统宕机,所以加上这句,这样就可以在next=0时不会有空指针传递。
        /**/
i = NR_TASKS; 
        p = &task[NR_TASKS]; 
        while (--i) { 
            if (!*--p) 
                continue; 
            if ((*p)->state == TASK_RUNNING && (*p)->counter > c) 
                c = (*p)->counter, next = i,pnext=*p; //保存要调度到的pcb指针
        } 
        if (c) break; 
        for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) 
            if (*p) 
                (*p)->counter = ((*p)->counter >> 1) + 
                        (*p)->priority; 
    } 
    /*调度进程到运行态*/ 
    if(task[next]->pid != current->pid) 
    { 
        //判断当前正在运行的进程状态是否为TASK_RUNNING, 
        //如果是,则表明当前的进程是时间片到期被抢走的,这时当前进程的状态还应是TASK_RUNNING, 
        //如果不是,则说明当前进程是主动让出CPU,则状态应为其他Wait状态。 
        if(current->state == TASK_RUNNING) 
        { 
            //记录当前进程的状态为J,在此处,当前进程由运行态转变为就绪态。 
            fprintk(3,"%ld\t%c\t%ld\n",current->pid,'J',jiffies); 
        } 
        fprintk(3,"%ld\t%c\t%ld\n",pnext->pid,'R',jiffies); 
    } 
    /**/ 

    //switch_tss(next); //由于此次实验难度还是挺高的,所以一般不会一次成功,所以我没有将switch_to宏删除,而只是将其改了一个名字,这样,如果下面的切换出问题,就切换回来测试是否是其他代码出问题了。如果换回来正常,则说明问题就出现在下面的切换上。这样可以减少盲目地修改。
    switch_to(pnext,_LDT(next));

⑤最后编写我们的重点添加到system_call.s文件中,switch_to方法,这个需要对进程进行精确控制,需要利用汇编语言进行编写,如下所示:

.align 2 
switch_to: 
    pushl %ebp 
    movl %esp,%ebp        #上面两条用来调整C函数栈态 
    pushfl            #将当前的内核eflags入栈!!!! 
    pushl %ecx 
    pushl %ebx 
    pushl %eax 
    movl 8(%ebp),%ebx    #此时ebx中保存的是第一个参数switch_to(pnext,LDT(next)) 
    cmpl %ebx,current    #此处判断传进来的PCB是否为当前运行的PCB 
    je 1f            #如果相等,则直接退出 
    #切换PCB 
    movl %ebx,%eax        #ebx中保存的是传递进来的要切换的pcb 
    xchgl %eax,current    #交换eax和current,交换完毕后eax中保存的是被切出去的PCB 
    #TSS中内核栈指针重写 
    movl tss,%ecx        #将全局的tss指针保存在ecx中 
    addl $4096,%ebx        #取得tss保存的内核栈指针保存到ebx中 
    movl %ebx,ESP0(%ecx)    #将内核栈指针保存到全局的tss的内核栈指针处esp0=4 
    #切换内核栈 
    movl %esp,KERNEL_STACK(%eax)    #将切出去的PCB中的内核栈指针存回去 
    movl $1f,KERNEL_EIP(%eax)    #将1处地址保存在切出去的PCB的EIP中!!!! 
    movl 8(%ebp),%ebx    #重取ebx值, 
    movl KERNEL_STACK(%ebx),%esp    #将切进来的内核栈指针保存到寄存器中 
#下面两句是后来添加的,实验指导上并没有这样做。
    pushl KERNEL_EIP(%ebx)        #将保存在切换的PCB中的EIP放入栈中!!!! 
    jmp  switch_csip        #跳到switch_csip处执行!!!! 
#    原切换LDT代码换到下面
#    原切换LDT的代码在下面 
1:    popl %eax 
    popl %ebx 
    popl %ecx 
    popl %ebp 
#该语句用来出栈调用进行内核态到用户态进行转化处,
# first_return_from_kernel,这个是在fork.c中添加的
    ret            
.align 2
first_return_from_kernel:
    popl %edx
    popl %edi
    popl %esi
    pop  %gs
    pop  %fs
    pop  %es
    pop  %ds
    iret

整体的栈空间内存图如下所示:
这里写图片描述

在调用的过程中需要注意的是:
1)汇编语言中定义的方法可以被其他调用需要:
.globl first_return_from_kernel
2) tss需要在sched.c中定义:
struct tss_struct *tss= &(init_task.task.tss);
3)fork.c中,schdule()方法中向栈中添加函数,需要声明这个函数:
extern void first_return_from_kernel(void);

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
<h3>回答1:</h3><br/>要切换Linux内核版本,可以按照以下步骤进行操作: 1. 下载所需的内核版本,可以从官方网站或其他可靠的源获取。 2. 解压下载的内核文件,进入解压后的目录。 3. 运行make menuconfig命令,进入内核配置界面,根据需要进行配置。 4. 运行make命令编译内核,这可能需要一些时间。 5. 运行make modules_install命令安装内核模块。 6. 运行make install命令安装新内核。 7. 修改/boot/grub/grub.conf文件,将默认启动的内核版本修改为新安装的版本。 8. 重启计算机,选择新安装的内核版本启动。 注意:在进行内核版本切换时,需要谨慎操作,避免出现意外情况。建议在备份数据后再进行操作。 <h3>回答2:</h3><br/>在Linux操作系统中,内核是核心部件之一,它是操作系统的底层构件,直接与硬件进行交互。Linux内核版本的切换主要分为以下几种方式: 1.升级内核 升级内核一般是在原有的Linux操作系统的基础上升级版本。升级内核需要手动下载内核源代码,并对其进行编译和安装,常见的编译工具有GCC等。这种方式需要对Linux操作系统有一定的了解,并且需要具备较强的技术能力和工具技巧。 2.降级内核 降级内核是将当前的Linux内核版本降低到较低的版本,主要是为了解决升级后发生的问题,同时保持系统的稳定性。降级内核需要先安装低版本内核,并对内核进行编译和安装等操作,同时需要注意依赖库和配置文件的变更。 3.多内核并存 多内核并存是指在同一台机器上同时安装多个内核版本,以便用户可以在需要时选择不同的内核运行。多内核环境是Linux操作系统的保障之一,可以提供多样的应用场景和解决不同的技术需求。 4.切换启动项 切换启动项也叫切换grub选项,用户可以通过编辑grub配置文件来切换内核版本。grub是Linux的一个引导程序,负责在开机时加载内核,用户可以通过selector菜单选择不同的内核版本启动。在grub配置文件中,用户可以指定默认启动的内核版本,以及将不需要的内核版本删除。 总结:内核版本的切换可以通过多种方式进行,选择不同方式可以根据使用场景和个人技术水平做出决策。在切换内核版本时,需要注意配置文件和依赖库的变更,以免影响系统的稳定性和性能。 <h3>回答3:</h3><br/>Linux内核版本切换是在Linux系统中更新内核版本或者回滚到之前的旧版本的过程。通常情况下,内核的版本更新是为了改进系统的性能、安全性和稳定性。在进行内核版本切换前,需要先备份当前的内核版本,以便在出现问题时可以恢复到之前的状态。 Linux内核版本切换的方法有多种,以下是其中的三种常见方法: 1.手动编译安装新版本内核 首先需要从Linux官网下载需要更新到的内核版本的源代码,然后进行编译安装。这个过程需要自行处理依赖关系、解决编译错误等问题。编译安装新版本内核后,需要修改GRUB配置文件以便启动新内核。 2.利用包管理器更新内核 在有些Linux系统中,可以使用包管理器(如yum或apt-get)直接安装新版本内核。通过命令行或软件中心等方式更新内核,方便快捷。但是要注意,更新后需要重新引导系统,否则新内核将无法启动。 3.使用系统备份工具回滚到旧版本内核 当新版本内核出现问题无法正常使用时,可以使用系统备份工具(如Timeshift或Clonezilla等)回滚到之前的旧版本内核。这种方法比较安全,但是需要提前进行备份,并且不适用于需要更新内核以修复漏洞的情况。 需要注意的是,在更新或回滚内核版本时,需要根据不同的Linux发行版和版本号选择正确的更新方法和适配的内核。同时,更新后需要重新进行相应的配置和测试,以确保系统能够正常使用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xubing716

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值