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

来源:互联网 发布:赛尔网络大赛 编辑:程序博客网 时间:2024/04/29 23:27

终于利用课余的时间完成了这个实验,收获很多,代码链接: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 = 4priority = 8signal  = 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 2first_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);

1 0
原创粉丝点击