企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 十一、 来源:[JOS学习笔记(十一)](http://blog.csdn.net/roger__wong/article/details/9632311) 不知不觉已经写了11篇日志了,本篇博客将完成LAB 4的PART A的剩余部分,包括内核锁、进程(环境)的简单调度算法,以及fork系统调用。 ## 一、内核锁 ### 1、锁实现 考虑到当多个CPU同时陷入内核的场景,若对于关键数据结构不加锁必然就会导致重入错误(如cprintf不加锁会在屏幕上输出奇怪的结果),因此使用锁来保证内核函数内部的逻辑正确性是很有必要的。 内核锁相关代码都在spinlock.c和spinlock.h中,关键代码如下: ![](https://box.kancloud.cn/2015-12-24_567b6e7bc8104.jpg) 以及x86.h里面的xchg函数: ![](https://box.kancloud.cn/2015-12-24_567b6e7bdc2f3.jpg) 可以看到,xchg函数首先使用lock指令使xchgl变为原子操作,然后尝试将xchgl两个操作数互换,并把原先第一个操作数的结果放入result中返回。 若此时锁是空闲的,则xchg返回0,spin_lock函数执行完成,否则继续执行pause指令,然后接着执行xchg函数直到其返回值为1. 关于lock引用一段汇编手册的资料: 总线加锁前缀“lock”,它是为了在多处理器环境中,保证在当前指令执行期间禁止一切中断。这个前缀仅仅对ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG,DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD,XCHG指令有效,如果将Lock前缀用在其它指令之前,将会引起异常。 关于pause: 提升spin-wait-loop的性能,当执行spin-wait循环的时候,笨死和小强处理器会因为在退出循环的时候检测到memory order violation而导致严重的性能损失,pause指令就相当于提示处理器哥目前处于spin-wait中。在绝大多数情况下,处理器根据这个提示来避免violation,藉此大幅提高性能,由于这个原因,我们建议在spin-wait中加上一个pause指令。(出自于intel 汇编手册) ### 2、实验相关: 实验要求在以下4个地方加锁: (1)i386_init中,启动多个ap之前。 (2)mp_main中,开始把任务调度到cpu上之前。 (3)trap中,若从用户态陷入内核则加锁。 (4)env_run中,从内核态返回用户态需要释放锁。 加锁后,将原有的并行执行过程在关键位置变为串行执行过程,整个启动过程大概如下: i386_init-->BSP获得锁-->boot_ap-->(BSP建立为每个cpu建立idle任务、建立用户任务,mp_main)--->BSP的sched_yield-->其中的env_run释放锁-->AP1获得锁-->执行sched_yield-->释放锁-->AP2获得锁-->执行sched_yield-->释放锁..... 其中括号表示并行执行 具体代码如下: (1)i386_init ![](https://box.kancloud.cn/2015-12-24_567b6e7be95be.jpg) (2)mp_main ![](https://box.kancloud.cn/2015-12-24_567b6e7c0264e.jpg) (3)trap ![](https://box.kancloud.cn/2015-12-24_567b6e7c15658.jpg) (4)env_run ![](https://box.kancloud.cn/2015-12-24_567b6e7c26344.jpg) ## 二、任务调度 ### 1、原理 在JOS中,任务调度在内核中是函数sched_yield,同时在用户态有相应的系统调用sys_yield也可以调用内核中的这个函数。 i386_init启动时,在boot_ap函数调用后,为每个CPU创建一个idle任务,相应代码在user/idle.c,通过代码可以看到,这些任务使用一个死循环,不断调用sys_yield尝试切换任务。 所以在这种机制下我们需要实现sched_yield函数,具有以下几个要求: (1)找到状态为runnable的任务,并切换执行 (2)如果找到一个running状态的任务,且此任务执行的CPU为当前CPU,也可将此任务切换执行 (3)若没有runnable任务,则执行idle任务。 (4)从当前CPU执行的任务处开始遍历链表(为了保证公平性) ### 2、代码 sched_yield的任务调度代码如下: ![](https://box.kancloud.cn/2015-12-24_567b6e7c413d3.jpg) 首先找到CPU当前任务的下标,然后从下标的下一个开始遍历数组。博主这段代码写的比较笨,主要是为了方便调试所用。 同时还要增加系统调用的部分代码,把进入内核态后的系统调用号和具体的系统调用对应起来,较为容易故不再详细论述。 ## 三、fork ### 1、原理 fork作为一个系统调用,其功能是根据父进程创建出一个一模一样的子进程,若返回的是0则说明是子进程,否则是父进程,同时返回值为子进程的系统调用号。 JOS实现fork过程采用的是用户态“类库”的形式封装了一系列系统调用,包括创建新进程、设置新进程状态、虚拟地址映射等等,这部分已经在user/dumbfork.c中封装好了,实验所要求的是实现相关的系统调用,包括: + sys_exofork:若为父进程返回子进程号,子进程则返回0。 + sys_env_set_status:设置子进程格式为runnable或者not_runnable + sys_page_alloc:分配一个物理页并对应到某个虚拟地址 + sys_page_map:拷贝父进程的某个PTE,以此来建立子进程的虚拟内存映射 + sys_page_unmap:解除某个虚拟地址的映射(在PART B中使用) ### 2、代码 sys_exofork: ![](https://box.kancloud.cn/2015-12-24_567b6e7c556f7.jpg) 可以看出,该函数首先复制各寄存器的状态(env_tf),然后系统调用本身返回子进程的id,因为调用此系统调用的进程为父进程。同时将子进程的eax寄存器设置为0,因为系统调用的结果存放在eax寄存器中,这样子进程返回后得到的系统调用返回结果就为0。 sys_env_set_status ![](https://box.kancloud.cn/2015-12-24_567b6e7c64407.jpg) 首先判断状态会否合法,然后根据进程ID进行查找,最后设置状态并返回。 sys_page_alloc: ![](https://box.kancloud.cn/2015-12-24_567b6e7c725c3.jpg) 按实验要求判断各种条件。 sys_page_map: ![](https://box.kancloud.cn/2015-12-24_567b6e7c85897.jpg) 主要是一些判断+LAB 2中的函数的封装,没什么可说的。 sys_page_unmap: ![](https://box.kancloud.cn/2015-12-24_567b6e7ca08c5.jpg) 到此PART A基本结束了,运行结果如下: ![](https://box.kancloud.cn/2015-12-24_567b6e7cb427d.jpg) PS:写起来怎么感觉这个实验好简单啊,做起来怎么感觉难到爆啊。。。。