MIT 6.828 JOS/XV6 LAB4-partA

这一部分要实现的是对多核处理器的支持,然后实现系统调用只喜欢用户应用创建新的应用,而且还要实现round-robin调度算法

Multiprocessor support

jos中对CPU进行了抽象

要描述一个CPU, 需要知道id,运行状态,当前正在运行的进程

所有的cpu数目放在cpus数组中

接下来则是对有多个cpu的处理器的抽象,这里使用了三个结构体,总之是比较乱,目前还不能完全看懂

多核处理器的初始化都在mp_init函数中完成,首先是调用mpconfig函数,主要功能是寻找一个MP 配置条目,然后对所有的CPU进行配置,找到启动的处理器

接下来就是要完成lapic(local advanced programmable interrupt controller)

apic主要是为与其连接的处理器传送中断信号

而CPU控制与其相关联的apic需要通过读写其中的寄存器,而读写寄存器则是通过映射IO(memory mapped IO)的方法来实现的

有一些物理内存是与apic的寄存器硬件相连的,这样可以通过读写内存完成对寄存器的读写

JOS中在ULIM之上,预留了4MB的内存空间来映射APIC的寄存器

所以在使用APIC之前首先要完成映射。首先需要调用mmio_map_region函数来实现

mmio_map_region函数的实现

这部分的实现还是比较简单的,注意这里的静态变量base,是记录当前还未分配空间的其实地址的

从MMIOBASE开始,分配出size大小的空间出来,调用boot_map_region即可

Exercise 2

在操作系统启动之后,需要启动其他的应用cpu(application processor, ap)

都在boot_aps中完成

这里是将启动代码放到了MPENTRY_PADDR处,而代码的来源则是在kern/mpentry.S中,这里的代码功能与boot.S中的非常类似,主要就是开启分页,转到内核栈上去,当然这个时候实际上内核栈还没建好呢

在执行完mpentry.S中的代码之后,将会跳转到mp_main函数中去

而这里需要提前做的,就是将MPENTRY_PADDR处的物理页表标识为已用,这样不会讲这一页放在空闲链表中分配出去

只需要在page_init中添加一个判断就可以了

question

这里的mpentry.S的代码是链接到KERNBASE之上的,也就是说,其中的符号的地址都是在KERNBASE之上的,但是实际上现在是将这些代码移动到了物理地址0X7000处,而且当前的AP处于实模式下,只支持1MB的物理地址寻址,所以这个时候需要计算相对于0X7000的地址,才能跳转到正确的位置上去

Exercise 3

因为从一个核变成了多个核,所以现在需要注意区分哪些是一个核独有的,哪些是共享的

每个核独有的变量应该有:

  1. 内核栈,因为不同的核可能同时进入到内核中执行,因此需要有不同的内核栈
  2. TSS描述符
  3. 每个核的当前执行的任务
  4. 每个核的寄存器

首先需要为每个核都分配一个内核栈,修改mem_init_mp代码

每个内核栈的大小是KSTKSIZE,而内核栈之间的间距是KSTKGAP,起到保护作用

Exercise 4

需要为每个核的TSS进行初始化任务

Exercise 5

在完成上述的工作之后,4个CPU都启动,但是除了BSP之外,剩下的三个AP都是在空转

因为这个时候还没有处理竞争,所以如果三个AP进入了内核代码的话,很可能会出现错误,所以首先需要解决这个问题

一般在单处理器操作系统中会采用大内核锁,就是当一个CPU需要进入内核的时候,必须获取整个内核的锁

这样的话,所有的CPU可以并行的跑用户程序,但是内核程序最多只能有一个在跑

这个当然是很粗糙的设计,但确实简单而且安全的

更好的设计是给进程表的每个条目以及其他可能出现竞争的变量单独的加锁,这样可以在内核上实现更高的并行度,但是会大福大增加设计的复杂性,比如说XV6就使用了不止一个锁

在kern/spinlock.*中实现了lock_kernel和unlock_kernel()函数,单反是进入到内核临界区之前都需要加锁,离开内核临界区之后需要尽快释放锁

  1. 在启动的时候,BSP启动其余的CPU之前,BSP需要取得内核锁
  2. Mp_main中,也就是CPU被启动之后执行的第一个函数,这里应该是调用调度函数,选择一个进程来执行的,但是在执行调度函数之前,必须获取锁
  3. trap函数也要修改,因为可以访问临界区的CPU只能有一个,所以从用户态陷入到内核态的话,要加锁,因为可能多个CPU同时陷入内核态
  4. Env_run函数,也就是启动进程的函数,之前在试验3中实现的,在这个函数执行结束之后,就将跳回到用户态,此时离开内核,也就是需要将内核锁释放

启动其他CPU之前加锁

在调用调度函数,也就是进入内核临界区的时候加锁

如果是从用户态进入到内核态的话,需要获得锁

在env_pop_tf执行结束之后,就回到用户态了,所以一定要在此之前释放

lock_kernel的调用关系图

unlock_kernel的调用关系图

Question 2

既然有了大内核锁,为什么还需要为四个CPU分配不同的内核栈

因为不同的内核栈上可能保存有不同的信息,在一个CPU从内核退出来之后,有可能在内核栈中留下了一些将来还有用的数据,所以一定要有单独的栈

Exercise 6

实现round-robin调度算法

主要是在sched_yield函数内实现,从该核上一次运行的进程开始,在进程描述符表中寻找下一个可以运行的进程,如果没找到而且上一个进程依然是可以运行的,那么就可以继续运行上一个进程

同时将这个算法实现为了一个系统调用,进程可以主动放弃CPU

上面这张图可以清楚的看到什么时候会调用调度器

  1. 初始化的时候,BSP选择一个进程来运行
  2. AP启动结束,选择一个进程运行
  3. 进程运行结束之后,选择下一个可运行的进程
  4. 进程主动调用系统调用,放弃CPU
  5. 产生时钟中断,当前进程CPU时间片结束
  6. 陷入内核之后发现当前进程是僵尸进程,杀掉

接下来是两个问题

Question 3

这里的问题问的是,在lcr3运行之后,这个CPU对应的页表就立刻被换掉了,但是这个时候的参数e,也就是现在的curenv,为什么还是能正确的解引用?就是下面这一段

这个问题比较简单,因为当前是运行在系统内核中的,而每个进程的页表中都是存在内核映射的,之前也说过,每个进程页表中虚地址高于UTOP之上的地方,只有UVPT不一样,其余的都是一样的,只不过在用户态下是看不到的

所以虽然这个时候的页表换成了下一个要运行的进程的页表,但是curenv的地址没变,映射也没变,还是依然有效的

Question 4

当然要保存,这个还用说。。。

是在trap中保存的

所以说,每次进入到内核态的时候,当前的运行状态都是在一进入的时候就保存了的

如果没有发生调度,那么之前trapframe中的信息还是会恢复回去,如果发生了调度,恢复的就是被调度运行的进程的上下文了

System calls for environment creation

就是实现最笨的fork

不过给定的函数是将功能细分的,而实现的比较笨的fork放在了user/dumbfork.c文件中

这个函数所做的事情就是将当前进程的寄存器,所有的页的内容全部都复制过来,唯一不同的地方就是返回值不同,而实现返回值不同就是将存放返回值的eax寄存器的值修改一下就可以了

一个个函数的看

sys_exofork函数

这里调用env_alloc函数,而这个函数所做的,就是做一系列的准备工作,生成存放页表的页,初始化页表内容等

然后将父进程的上下文内容全部拷贝过来,除了返回值eax

但是此时只能将这个进程设置为不可运行,因为还没有将页表映射复制过来

sys_env_set_status函数

就是将状态设置为可执行或者不可执行而已

sys_page_alloc函数

主要功能就是申请一页物理内存,然后把它映射到虚拟地址va上去

sys_page_map函数

主要功能就是将进程id为srcenvid的进程的srcva处的物理页的内容,映射到进程id为dstenvid的进程的dstva处

sys_page_unmap函数

就是解出映射

但是看上面几个函数实际上看不出啥来,还是看看dumbfork怎么实现的吧

所以这里dumbfork做的事情就是首先获取新生成的子进程的进程id

然后将父进程的进程空间中的每一页的内容都复制过去,对于处在地址addr处的一页,具体做法就是首先为子进程申请一个物理页,然后将这个物理页映射到子进程虚拟地址addr处

然后同时将这个物理页页映射到进程Id为0的进程的虚拟地址PTSIZE处,然后利用memmove,将父进程addr处的内容移动到进程id为0de进程的虚地址PTSIZE处,此时对应的物理页也就有了相同的内容

之所以这样做,是因为如果要复制内容的话,要么在内核中复制,但是也需要将两页映射到内核空间中来,完成复制之后再解映射

而这里用了一种还算是巧妙的方法,因为当前进程的PTSIZE之上是不用的,所以都映射到那去,复制内容的完成也在那

当然实际系统肯定不这么干,都是用copy on write技术,这个partB中再搞

时间: 2024-10-09 11:17:51

MIT 6.828 JOS/XV6 LAB4-partA的相关文章

MIT 6.828 JOS/XV6 LAB4-partC

这一部分要实现抢占式调度和进程间通信 前面的调度是进程资源放弃CPU,但是实际中没有进程会这样做的,而为了不让某一进程耗尽CPU资源,需要抢占式调度,也就需要硬件定时 但是外部硬件定时在Bootloader的时候就关闭了,至今都没有开启 而JOS采取的策略是,在内核中的时候,外部中断是始终关闭的,而在用户态的时候,需要开启中断 所以首先需要求改IDT,但是我在之前把256个都弄好了 所以现在只需要在进入用户态的时候,保证外部中断是使能的就可以了,为了做到这一点,可以在每个进程初始化的时候就将外部

MIT 6.828 JOS/XV6 LAB4-partB

这里要实现的就是UNIX标准系统调用中的fork,核心当然是copy on write技术 至于为什么采用copy on write,是因为子进程在被创建之后很可能立刻执行exec()了,之前做的一系列的拷贝是无用功 所以说,当创建一个新的子进程的时候,只需要拷贝父进程的内存映射(页表)就可以了,而且将父进程所有的内存映射页都标记为只读的,这样,当子进程或者父进程尝试去读的时候是安全的,而当尝试去写的时候,就会出发page fault,而在page fault处理例程中,单独将被写入的页(比如说

MIT 6.828 JOS学习笔记2. Lab 1 Part 1.2: The kernel

Lab 1 Part 1: PC bootstrap 我们继续~ PC机的物理地址空间 这一节我们将深入的探究到底PC是如何启动的.首先我们看一下通常一个PC的物理地址空间是如何布局的:                           这张图仅仅展示了内存空间的一部分. 第一代PC处理器是16位字长的Intel 8088处理器,这类处理器只能访问1MB的地址空间,即0x00000000~0x000FFFFF.但是这1MB也不是用户都能利用到的,只有低640KB(0x00000000~0x00

MIT 6.828 JOS学习笔记17. Lab 3.1 Part A User Environments

Introduction 在这个实验中,我们将实现操作系统的一些基本功能,来实现用户环境下的进程的正常运行.你将会加强JOS内核的功能,为它增添一些重要的数据结构,用来记录用户进程环境的一些信息:创建一个单一的用户环境,并且加载一个程序运行它.你也可以让JOS内核能够完成用户环境所作出的任何系统调用,以及处理用户环境产生的各种异常. Part A: User Environments and Exception Handling 新包含的文件inc/env.h里面包含了JOS内核的有关用户环境(

MIT 6.828 JOS学习笔记7. Lab 1 Part 2.2: The Boot Loader

Lab 1 Part 2 The Boot Loader Loading the Kernel 我们现在可以进一步的讨论一下boot loader中的C语言的部分,即boot/main.c.但是在我们分析之前,我们应该先回顾一些关于C语言的基础知识. Exercise 4: 阅读关于C语言的指针部分的知识.最好的参考书自然是"The C Programming Language". 阅读5.1到5.5节.然后下载pointers.c的代码,并且编译运行它,确保你理解在屏幕上打印出来的所

MIT 6.828 JOS学习笔记16. Lab 2.2

Part 3 Kernel Address Space JOS把32位线性地址虚拟空间划分成两个部分.其中用户环境(进程运行环境)通常占据低地址的那部分,叫用户地址空间.而操作系统内核总是占据高地址的部分,叫内核地址空间.这两个部分的分界线是定义在memlayout.h文件中的一个宏 ULIM.JOS为内核保留了接近256MB的虚拟地址空间.这就可以理解了,为什么在实验1中要给操作系统设计一个高地址的地址空间.如果不这样做,用户环境的地址空间就不够了. Permission and Fault

MIT 6.828 JOS学习笔记5. Exercise 1.3

Exercise 1.3 设置一个断点在地址0x7c00处,这是boot sector被加载的位置.然后让程序继续运行直到这个断点.跟踪/boot/boot.S文件的每一条指令,同时使用boot.S文件和系统为你反汇编出来的文件obj/boot/boot.asm.你也可以使用GDB的x/i指令来获取去任意一个机器指令的反汇编指令,把源文件boot.S文件和boot.asm文件以及在GDB反汇编出来的指令进行比较. 追踪到bootmain函数中,而且还要具体追踪到readsect()子函数里面.找

MIT 6.828 JOS学习笔记4. Lab 1 Part 2.1: The Boot Loader

Part 2: The Boot Loader 对于PC来说,软盘,硬盘都可以被划分为一个个大小为512字节的区域,叫做扇区.一个扇区是一次磁盘操作的最小粒度.每一次读取或者写入操作都必须是一个或多个扇区.如果一个磁盘是可以被用来启动操作系统的,就把这个磁盘的第一个扇区叫做启动扇区.这一部分介绍的boot loader程序就位于这个启动扇区之中.当BIOS找到一个可以启动的软盘或硬盘后,它就会把这512字节的启动扇区加载到内存地址0x7c00~0x7dff这个区域内. 对于6.828,我们将采用

MIT 6.828 JOS学习笔记10. Lab 1 Part 3: The kernel

Lab 1 Part 3: The kernel 现在我们将开始具体讨论一下JOS内核了.就像boot loader一样,内核开始的时候也是一些汇编语句,用于设置一些东西,来保证C语言的程序能够正确的执行. 使用虚拟内存 在运行boot loader时,boot loader中的链接地址(虚拟地址)和加载地址(物理地址)是一样的.但是当进入到内核程序后,这两种地址就不再相同了. 操作系统内核程序在虚拟地址空间通常会被链接到一个非常高的虚拟地址空间处,比如0xf0100000,目的就是能够让处理器