MIT总结篇

前言

        本项目是在xv6基础上改写JOS内核,实现部分内核功能,在这个过程中,对操作系统的相关知识有更深刻的了解。

Lab1

        了解PC启动(bootloader和kernel)过程,并在kernel中完成了最初的虚拟内存映射、栈设置和硬件相关的一系列初始化。
  BIOS(内存地址为 0xffff0 处的指令)是PC启动后运行的第一段程序,BIOS的操作就是在控制,初始化,检测各种底层的设备,比如时钟,GDTR寄存器。以及设置中断向量表。但是它最重要的功能是把操作系统从磁盘中导入内存。所以BIOS在运行的最后会去检测可以从当前系统的哪个设备中找到操作系统,通常来说是我们的磁盘。也有可能是U盘等等。当BIOS确定了,操作系统位于磁盘中,那么它会把这个磁盘的第一个扇区(传统的硬盘启动中软盘和硬盘都被分为大小为512字节的区域,都叫做扇区,扇区是磁盘操作的最小粒度,每一次读取或写入都必须是一个或多个扇区),通常把它叫做启动区(boot sector)先加载到内存地址0x7c00~0x7dff这个区域内。这个启动区中包括一个非常重要的程序–boot loader,它会负责完成整个操作系统从磁盘导入内存的工作,因为采用的是传统的磁盘启动机制boot loader程序的大小必须小于512字节;整个boot loader是由一个汇编文件(1)boot/boot.S,以及一个C语言文件(2)boot/main.c,而主要完成的功能是(1)一些极其重要的配置工作(设置16位模式,关中断,用lgdt将GDT表的首地址放入GDTR中,开启A20将CR0的bit0(PE)赋值为1,将处理器从实模式转换为32bit的保护模式,因为只有在这种模式下软件可以访问超过1MB空间的内容;(2)从磁盘中读取内核(elf文件),将内核加载到物理内存的0x100000处(先读内核第一页,判断是否为elf,其保存程序中所有段信息,后读所有段,指向代码段运行内核文件,控制权从boot loader转交给操作系统的内核)。

        boot loader结束、kernel载入完成刚开始时,载入简易页表到cr3映射前4MB的物理内存(0xf0000000 ~ 0xf0400000)的页表目录和页表进行静态初始化 这个简易页表干啥的,然后设置 CR0_PG 标志,开启分页,对内存的引用由虚拟地址通过虚拟内存硬件转换为物理地址对内核堆栈进行初始化设置,定义了全局变量 bootstack 作为堆栈32KB,在函数调用时,通过栈保护和恢复现场。
       

1.实模式与保护模式
        实模式出现在8088CPU时期,CPU只有20位地址线(2^20=2^10*2^10=1024KB=1MB),8个16位通用寄存器和4个16位段寄存器。16位寄存器无法计算20位主存地址,故访问物理地址由段地址和段内偏移地址两部分组成(实模式都是直接对物理地址进行操作),物理地址 = 段基址 << 4 + 段内偏移(在位址加法器上执行,不用担心左移溢出)。计算完成后通过地址总线通过内存完成寻址,找到对应指令并读取,由数据总线(16位) 传输回到输入输出电路 - 指令缓冲区 - 执行控制器从而由CPU去执行指令,进而IP = IP + 所读指令的长度,指向下一条指令。重复执行以上过程。但即使超出1MB也是允许的。因为实模式使用了地址回绕(wrap-around) ,将超过1MB的部分自动回绕到0地址,并继续从0地址开始映射。也就是把地址对1MB求模。
        段寄存器因为内存分段而设置。四个段寄存器分别是:CS代码段寄存器(CS:IP 指向当前要执行的指令的地址),DS数据段寄存器,SS堆栈段寄存器(SS:SP 指向栈顶元素),ES附加段寄存器。段内偏移量由通用寄存器提供。

        随CPU发展,地址线变为32根(4GB),寄存器32位,引入保护模式,实现更大空间访问。保护模式下地址转换为:逻辑地址 -> [段式内存管理单元] -> 线性地址(虚拟地址) -> [页式内存管理单元]-> 物理地址在boot loader中 并没有开启分页管理机构,所以计算出的线性地址就是真实需要访问的物理地址。
       
系统会给程序自动分配程序段、代码段等,这些段以及偏移组成了逻辑地址,而逻辑地址通过GDTR/LDTR , 首先根据Flags字段判断能否访问此段内容(为了对进程间的地址进行保护),如果能访问,则把Base字段(段基址)的内容取出,直接与offset相加得到 线性(虚拟)地址。
        GDT是全局可见,即在内存上运行的所有程序都能看见这张表,所以操作系统的内核信息也都存在里面。整个系统中GDT只有一张,可存放内存任意位置,CPU通过寄存器GDTR存放GDT的入口地址。LDT是本地可见,即是每个在内存中运行的程序都有这一张表,里面指明了该程序的段信息。

2.x86系列CPU的主要寄存器
e开头->32位

3.有专门的函数将扇区读入端口
4.啥时候GDT/LDT
5.栈相关寄存器
        esp:栈指针寄存器,指向系统栈最上面一个栈帧的栈顶。
        ebp:基址指针寄存器,指向系统栈最上面一个栈帧的底部
        eip:存储当前执行指令的下一条指令在内存中的偏移地址
6.C函数调用过程     
进入函数之前:将需要的参数压栈、eip压栈(下一条指令在内存中的位置)
进入调用函数(函数内代码执行之前):ebp压栈、当前esp赋值给ebp

Lab2

在JOS操作系统中实现分页内存管理,其包括:
        物理页面管理:用CMOS检测可用的物理内存,为页表目录分配内存(页面总数=物理内存大小>>12(除以4K)),完成页表目录的初始化(分配一页内存设为0),完成页面数据结构和空闲页面链表的初始化。页面数据结构是将物理内存以页为单位记录到一个数组中。其中每一项包括指向下一空闲页面的指针和页面的引用计数。在物理页面管理过程中,将找出物理内存中不可使用的页面将其引用计数记为1,其他页放入空闲页面链表中(页面分配和释放)。
   映射时往哪映射
        
在代码中使用的地址都应该是虚拟地址,它们会被MMU自动转换成物理地址。可通过KADDR(pa)将物理地址转化为虚拟地址,然后对虚拟地址进行操作。在实际应用中经常会有多个不同的虚拟地址页被同时映射到同一物理页的情况。页面数据结构中有字段保存物理页的引用计数。当引用计数为0,代表页面没有被使用,这个物理页才能被释放。

        虚拟内存管理主要是对页表进行管理,包括插入和删除线性地址到物理地址的映射关系,以及创建页表等操作(之前只是创建了页表相关的数据结构,整理出可用的空闲页表,但并未填写页表目录和创建出真正的二级页表)。通过这个虚拟内存管理是想实现物理地址与虚拟地址的映射,主要是通过段页式管理。通过分段机制,根据段选择子和偏移量将虚拟地址转换为线性地址。开启分页后,MMU部件会把这个地址分成 3 部分,分别是页目录索引(Directory)、页表索引(Table)和页内偏移(Offset),根据这三个要素得到对应的物理地址。
        对于一个特定的虚拟地址(线性地址),其映射到哪个物理页面是需要人为指定的,它对应与页表目录的哪个项是确定的(都是根据虚拟地址本身计算得出),该项指向哪个二级页表是不确定的(初始通过page_alloc分配一个可用的页面,将页面基地址填入页表目录)。通过页目录表索引页目录项,查找页目录项中的二级页表是否存在,不存在就看create标志位是否为true如果为true就创建一个新的页表,不为true就返回NULL,最后返回页表项的虚拟地址。

初始化内核地址空间
        
JOS将32位虚拟地址空间分为内核地址空间用户地址空间。操作系统内核一般占据在高地址的部分约256MB。使用页表权限位阻止用户进程访问用户地址空间以外的空间,避免其读取或改写内核数据。完成物理内存前256M的映射,与此同时填写了页表目录和二级页表,并赋予页面相应权限。设置cr3kern_pgdir的物理地址,并设置cr0的标志。

Lab3

        Lab3主要是创建用户环境、初始化用户环境的虚拟内存机制、实现用户代码的加载,然后实现了x86中的中断与异常机制,对IDT表进行初始化(有中断发生从IDT表去查询对应的trap然后通过dispatch去往不同的中断处理函数),通过IDT的dispatch来实现页错误,断点异常和系统调用几种功能,最后完善了内存的保护机制

        首先分配环境数组结构来记录每一个环境的信息(环境的寄存器变量、下一个未使用环境、当先环境id,创建此环境的id,环境的页表目录的虚拟地址)、当前运行环境、空闲环境链表。由于现在还没有文件系统,因此JOS将一些用户程序的静态二进制文件作为ELF文件嵌入在内核中,以便被载入和执行。在读取和运行这些二进制文件前,要完成能够设置这些代码的运行用户环境的功能,故主要初始化用户环境为能够运行做准备。包括:为新环境分配页表目录建立映射、解析ELF文件加载内容到新环境的用户地址空间、运行环境。

        在发生中断或异常时,是要求当前执行的代码无法选择进入内核的位置或方式。只能在严格受控的情况下进入内核态,有两种机制提供保护:中断向量表IDT(中断或异常入口)和任务状态段(仅把它用作定义从用户态切换到内核态时内核堆栈的位置)。为处理内部中断异常,设置宏为每一种中断设置中断处理程序的入口,这些入口地址初始化为IDT表,在触发中断之前还要通过tss找到ss,esp,保存当前信息,然后进入中断程序

        为异常编写入口点,将状态压栈,加载ds、es,初始化IDT(包括用户权限),实现系处理页面错误14、断点异常3具体任务。

        系统调用:用户进程发起系统调用,处理器进入内核态保存当前用户进程的上下文状态,内核执行相应代码实现系统调用,然后返回继续执行用户进程的代码。JOS系统中,我们使用int指令int $0x30作为系统调用(在IDT中初始化中断号和入口函数),触发一个处理器的中断。

        中断具体实现流程: 1、先将所有的中断处理函数的起始地址放到中断向量表 IDT 中,2、当中断发生时,不管是外部中断还是内部中断,处理器捕捉到该中断然后进入核心态,再根据中断
向量去查询中断向量表,找到对应的表项。3、保存被中断的程序的上下文到内核堆栈中调用这个表项中指明的中断处理函数。4、执行中断处理函数。5、执行完毕之后,恢复被中断得进程的上下文,返回用户态,继续运行这个进程。
        
通过几个系统中实现了的宏来进入中断或异常对应的处理程序。
        在中断触发前:
(1) 从任务状态段 tss 找到内核栈的地址,临时保存旧栈的 ss,esp,修改当前 ss,esp 指向内核栈(2) 向内核栈压入旧 ss,旧 esp, elag, cs,eip,若中断有错误码,自动压入错误码 (3) cs,eip 指向中断处理程序入口,准备执行中断处理程序

        上下文的切换:将用户进程在寄存器中的数据保存到内存中,并恢复内核的寄存器数据。内核中会维护一个进程数组(最多容纳64个进程),存储每个进程的状态信息,proc结构体定义在proc.h,这也是xv6对PCB的实现。用户程序的寄存器数据将被暂时保存到proc->trapframe结构中。
        到这步就实现了基本的异常处理,发生中断时进入内核态根据寄存器指示执行中断处理函数,执行完后退出,回到之前的位置,普通的异常时没有中断分发的,直接通过宏来实现了。
        系统调用部分:
和之前的中断类似,将对应的系统调用号置于寄存器 a7 中,并执行 ecall 指令进行系统调用,其中函数参数存在 a0~a5 这 6 个寄存器中。ecall 指令将触发软中断,cpu会暂停对用户程序的执行,转而执行内核的中断处理逻辑,进行上下文的切换(上部分蓝字)进入内核态后执行程序 (见上上部分蓝字)。
        函数执行的结果则存储于a0中。完成调用后同样需要进程切换,先保存内核寄存器信息,再将暂存的用户进程数据恢复到寄存器,重新回到用户空间,cpu从中断处继续执行,从寄存器a0中拿到函数返回值。

系统调用是啥

0~31中部分是系统预留中断不用处理,不处理干啥。

是在运行环境的时候才分配页表目录吗

虚拟内存我想建多大就多大吗

为什么要将内核UTOP以上的虚拟地址到物理地址的已完成映射拷贝到用户页表目录中,

可以同时运行多少个环境

1.        异常中断都是受保护的控制转移,它们将处理器模式从用户态切换到内核态,不给用户模式干扰到其他环境或内核功能的机会。中断一般是指由处理器外部的异步事件引发的受保护的处理器控制权转移,例如外部I/O设备发出的活动信号;异常则是由当前执行的代码同步地引起的控制权转移,例如除零异常或非法存储器访问。

        中断向量表:x86允许内核有256种不同的中断或异常入口,每个入口的值由整数0~255表示,称为中断向量。一个中断向量的值由引发中断的源决定,不同的设备、错误条件以及应用对内核的请求将引发不通的中断。CPU利用中断向量作为中断描述符表IDT的索引查找中断处理程序的入口地址(中断门),IDT表被设置在内核空间。从这个表中的相应条目中存储:
        需要加载到寄存器eip中的值:它指出内核中处理该中断的代码的入口地址
        需要加载到寄存器cs中的值:它指出运行中断处理程序的运行特权级(即将当前进程切换到内核态)

        任务状态段TSS:当中断或异常发生切换到内核态运行中断处理程序之前,处理器需要一个地方保存当前处理器的状态,例如寄存器EIP和CS的值以便在中断处理程序结束后能恢复到中断发生的地方,继续执行原来的代码。这个区域也需要受到保护,避免被用户态的程序访问以破坏内核。因此当处理器在状态切换时也要将它的堆栈切换到内核态中以保存处理器的状态。TSS用来指出内核堆栈所在的段选择子和地址。处理器向内核栈中顺序压入SS, ESP, EFLAGS, CS, EIP和error code(可选),然后它从中断向量加载CS和EIP的值,并将ESP和SS的值设置为内核栈
任务状态段维护用户程序处理器状态,存储在内核栈中?避免被用户态的程序访问以破坏内核?为什么要切换到内核堆栈,因为要在内核运行程序?

2.        x86处理器内部同步地产生异常的中断号都在0~31,大于31的中断号都用作软件中断或硬件中断,软件中断由int指令生成,硬件中断由外部设备在需要时异步地生成。     

3.        系统调用等可能会为内存保护带来问题:系统调用大都传递指向用户读写缓冲区的指针给内核,所以指向的数据可能是内核有权访问但用户无权访问的,因此内核必须小心读写指针指向的数据,防止将重要信息泄露给用户;内核中发生页面错误更严重,会造成整个系统停止运行,等待分辨出这个异常是在引用用户数据时造成。所以对针对上述问题完善,增加检查内核是否发生页面错误、用户是否有权限读写相应内存的代码。

4.        在JOS系统中,环境并没有在内核中拥有各自独立的栈。因为任意时刻只能有一个环境处于活跃状态,因此JOS内核只需要一个内核栈。

Lab4

       Lab4中:开启多处理器。现代处理器一般都是多核的,这样每个CPU能同时运行不同进程,实现并行。需要用锁解决多CPU的竞争。实现进程调度算法。实现写时拷贝fork(进程创建)。实现进程间通信

        使JOS支持对称多处理模型SMP,它是一种所有CPU对系统资源平等访问的多处理器模型,进而使JOS支持多CPU。在SMP中所有CPU功能相同,但是分为两种类型(由BIOS决定):(1)引导处理器(BSP):负责初始化系统和引导操作系统 (2)应用处理器(APs):仅在操作系统启动和运行后,BSP才能激活APs。在SMP中,每个CPU都有一个局部可编程中断控制器(LAPIC),负责传递中断。CPU通过内存映射IO(MMIO)访问它对应的APIC,这样就能通过访问内存达到访问设备寄存器的目的(在MMIO中,物理内存硬连线到部分I/O设备的寄存器去)。
        通过这个局部可编程中断控制器,实现BSP向APs发送处理器间中断启动APs,实现内置定时器触发时钟中断,以支持抢占式多任务处理。

        怎样实现支持多CPU:实现的SMP。对于SMP中每个CPU的LAPIC,通过MIMO去访问,建立LAPIC的虚拟内存映射。

        启动AP:启动前,引导处理器BPS收集多处理器系统的信息,例如CPU的总数、它们的APIC ID、LAPIC的MMIO地址(那还映射干啥)等(从BIOS中获取)。先使找到AP的入口代码,初始化信息(各类寄存器、加载GDT表、设置cr0进入实模式、载入页表修改cr3cr0进入保护模式、初始化堆栈),设置页表目录,建立内存映射IO,初始化当前CPU环境(GDT)TSS,设置CPU状态,进入自旋。
        直到AP将对应CPU状态改变,启动下一个AP。

        解决多个CPU同时运行内核代码时的竞争:要有一些同步机制,才能确保内核只会在其中一个处理器上运行。设置一个大内核锁。它是一个全局锁,在用户环境进入内核态时获得锁,回到用户态时释放锁。在这种模式下,多个用户态的环境能在不同CPU上同时运行,但同一时间只有一个环境可以进入内核态,此时其他企图进入内核态的环境都将等待。

LAPIC是一个硬件存在的东西吗
用户有一片内存空间,不同用户将不同CPU的LAPIC地址映射到相同虚拟地址空间处?
启动AP的过程是要对每个AP环境进行初始化吗

        轮询调度:使其能够以循环使用的方式在多个不同的环境之间切换。循环查找环境数组,实现新的系统调用使得用户环境可以主动唤起内核将CPU资源移交给另一个环境。选择ENV_RUNNABLE的进程进行调度并将其状态改为ENV_RUNNING,然后调用env_run函数,执行上下文切换。改变cr3的值改变为用户空间的页表目录。再设置新环境的重要寄存器(eip, cs, eflags(标志寄存器), esp, ss赋值到相应寄存器),然后进入用户环境执行代码。如果在数组内没有找到状态为ENV_RUNNABLE的环境可以切换,将继续执行curenv。

        允许用户环境创建和启动其他用户进程:创建系统调用在用户空间实现类似fork()函数的功能。将一次fork拆分为了进程创建,进程状态修改,为进程分配地址,映射内存几个部分来进行。首先为子进程在addr虚拟地址处分配一个页面,然后将该物理页面映射到父进程的临时交换区UTEMP,这样父进程对UTEMP的写入等于对子进程addr地址处的写入,最后将父进程addr处的数据拷贝到子进程addr处,最后删除UTEMP的映射。这里要这样写的原因是父进程地址addr和子进程地址addr虽然在地址的值上是相同的,但对应的物理页面是不通的,映射关系也是不同的。
        目前是fork()后父进程页面中的所有数据复制到子进程之后会立刻执行exec(),它将子进程的内存完全替换为新的程序,这时候子进程仅在调用exec()前用一下这部分内存,全复制浪费内存。因此,在后来版本的Unix利用虚拟内存硬件允许父子进程共享映射到各自地址空间的内存,直到某个进程实际修改了内存,这种技术被成为写时拷贝。为此,fork()中内核将复制父进程的地址空间映射到子进程而不是复制页面内容与此同时将共享的页面标记为只读。当父子进程任何一方企图向共享页面写入数据时将会发生页面错误,此时内核会意识到这个页面是一个“虚拟的”或“写时复制的”副本,然后给触发异常的进程分配一个私有的可写页面并复制原页面的数据。这样,在实际有数据写入之前并不会发生页面的复制,降低了fork()+exec()调用的代价。

        写时拷贝可能会出现页面错误,解决方法:通过系统调用向JOS内核注册一个页面错误处理程序入口,并向Env数据结构增加用于记录这个用户环境自定义的页面错误处理程序。

        用户模式下发生页面错误时:用户环境运行在JOS分配给用户的正常栈上,出现错误时JOS内核将栈从正常用户栈切换到用户异常栈以运行用户级页面错误处理程序。1、每个支持自定义用户页面错误处理程序的用户进程都要为它自己的异常栈分配一页内存;2、用户态发生页面错误,走正常的中断处理程序,陷入内核态切换到内核栈;3、根据中断号发现是页面错误,对错误进行处理;4、如果没有用户自定义页面异常处理程序,销毁环境 5、如果有,转向用户态处理异常(如果tf_esp在异常栈地址范围内,说明用户的页面处理程序发生异常);6、恢复用户环境执行页面错误处理程序;7、完成了用户定义的处理函数后就要考虑从用户错误栈直接返回用户运行栈的函数。

        最终实现的fork()流程:父进程创建子进程,将映射关系复制到子进程地址空间,权限为只读并添加PTE_COW标识。父进程为子进程注册页面错误处理程序入口。父进程设置子进程的状态为runnable,子进程运行。

        父子进程之一企图写copy-on-write的页面时,上述2、3、5、6,检查是写页面时(错误码为FEC_WR)发生的错误且页面被标记为PTE_COW,分配一个新的页面并将发生页面错误的页面的数据复制到新页面,赋予其读写权限,然后修改映射,使新页面取代旧页面。

        抢占式:这个时候我们的进程还是只能顺序去执行,如果一个进程获得了CPU资源后一直死循环不主动让出CPU控制权,那整个系统都将死锁住,所以为了允许内核抢占正在运行的环境,必须要扩展JOS内核来支持时钟的外部硬件中断。将16中IQRs映射到IDT表中(IRQ偏移为32),修改eflags寄存器的FL_IF控制为启用外部中断。已完成时钟和中断控制器产生中断,在中断发生时切换到其它进程。

进程间通信:
        在JOS中,可以允许传递两种信息,一是一个32位整数,另外一个就是传递页的映射。最后将这两个功能实现在了同一个系统调用。

        发送和接收消息:
        进程调用系统调用接收消息,切换当前接受状态,放弃CPU,挂起用户进程直到它收到一个消息。在它等待一个消息的时间里,任何一个其他进程都能向它发送一个消息。收到消息后,从当前环境结构体中获取收到的消息值。
        进程调用系统调用发送消息,以接受者的进程id和消息值作为参数,向指定进程发送消息。如果接受者正等待接收消息,则发送者交付这个消息并返回0;否则发送者返回-E_IPC_NOT_RECV表明目标进程不想接收消息。
        如果在调用时接受或提供的是一个有效地址,则表明要传递的是页面。如果发送者发送一个页面,当发送成功后,发送者保留其原有的页面映射,接收者将自己地址空间中的dstva映射到发送者srcva所指向的物理页(原dstva处页面被覆盖),该物理页被两者共享。

1、外部中断通过eflags寄存器的FL_IF位控制,该位为1时启用外部中断,为0时禁用。在我们的简化后,该位仅在保存和恢复eflags寄存器,即仅在进入和离开内核时被修改

2、在内存映射中,操作系统会将磁盘文件或设备的数据缓存在内存中,并将这些数据在进程的地址空间中分配一段连续的虚拟地址,使得进程可以像访问内存一样访问这些数据。

实验代码:https://github.com/zhayujie/xv6-riscv-fall19

原文链接:https://zhayujie.com/mit6828-lab-util.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值