操作系统实验之基于内核栈切换的进程切换
来源:互联网 发布:赛尔网络大赛 编辑:程序博客网 时间: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);
- 操作系统实验之基于内核栈切换的进程切换
- 操作系统之进程切换
- 操作系统原理与实践5--内核栈切换的进程切换
- 在Linux-0.11中实现基于内核栈切换的进程切换
- linux内核分析学习笔记:操作系统的进程切换
- java开发操作系统内核:实现进程的优先级切换
- linux内核之进程切换
- 【操作系统】进程的状态切换
- 基于I386的Linux2.4.18内核的进程切换分析
- 操作系统之进程与线程3——内核级线程及切换(未完成)
- 计算机操作系统进程间的切换
- Linux内核进程切换
- Linux内核进程切换
- Linux内核进程切换
- Linux内核分析:实验八--Linux进程调度与切换
- Linux内核的进程切换(上)
- 计算机操作系统进程切换详解
- 操作系统进程切换简易分析
- cocos2dx-3.4 编译apk包 文件名、目录名或卷标语法不正确 解决方案
- Django安装mysqlclient
- Ajax了解(一)
- C 回文字符串
- WPF学习笔记(2)——x名称空间详解 上
- 操作系统实验之基于内核栈切换的进程切换
- shell在centOS6.5中安装redis
- 插鼠标出现 usb设备无法识别 解决办法
- java io总结
- Tensorflow分布式并行策略
- PHP防止sql注入
- Android JNI学习之动态注册native函数
- Redis配置文件redis.conf 详解
- 初探堆(未完成)