CS162 23Fall总结

打印 上一主题 下一主题

主题 992|帖子 992|积分 2976

  PintOS是一个x86架构的教育用操作体系,它支持多线程,加载并运行用户程序,以及文件体系。骨架代码只提供了很简单的实现,本Lab需要丰富并加强这些模块的功能。本实验提供Bochs2和QEMU3模拟器模拟x86 CPU和相应外设来运行并调试PintOS.
  PintOS源码结构:
    threads/: PintOS的底子内核相干源码,包罗bootloader,内核入口, 底子中断处 理,内存分配,CPU调度等。
    userprog/: 用户程序支持相干源码,包罗页表管理,体系调用处理惩罚,缺页处理惩罚, 各种traps,以及程序加载器。
    filesys/: 文件体系相干源码,可以使用初始的接口进行文件操作,后续会进行扩 展以及支持树形目录等。
    devices/: I/O装备接口相干源码,包罗键盘,磁盘,时钟等等。
    lib/: 部分C尺度库相干源码,有kernel子目录和user子目录,分别被链接进内 核程序和用户程序。
  PintOS启动过程:
    1. BIOS将Pintos引导加载程序(threads/loader.S)从磁盘的第一个扇区读取到地址0x7c00的内存中。
    2. 引导加载程序将内核代码从磁盘读取到地址0x20000的内存中,然后跳转到内核入口点(threads/start.s)。
    3. 内核入口点的代码切换到32位掩护模式1,然后调用main(threads/init.c)。我们称运行init.c的线程为OS的主线程。为了使其能运行用户程序,需要给它分配一个pcb来管理子历程,即使它不是一个历程。
    4. OS主线程通过初始化线程调度器、内存子体系、中断向量、硬件装备和文件体系来启动Pintos,并根据命令行输入(argv)来加载用户程序或运行测试,运行完毕后shutdown.
  PintOS内存模型:
    假造地址一共4GB,其中0-PHYS_BASE(3GB)是用户空间,PHYS_BASE-4GB是内核空间。
    内核空间是所有历程共享的,并且直接映射到物理地址,pa = va – PHYS_BASE. 即假造地址PHYS_BASE的物理地址是0.
    用户空间每个历程不同,通过二级页表映射,在历程切换时由pagedir_activate()通过修改CR3寄存器(页目录基址寄存器)进行切换。
    内核中分配内存以页为单位,通过内存池管理连续空闲物理帧,并且分为用户池和内核池,前者分给用户空间,后者分给内核空间。
    CS162没有实现完备的假造内存,page_fault非常处理惩罚未实现。
Proj0: Introduction
  程序启动时有一个段错误,是某一行代码的问题,调试一下找出来改了就好了,该项目只是让我们熟悉一下PintOS的运行和调试。
Proj1: User Programs
  实现用户程序支持和浮点数操作。该proj涉及tcb,pcb,syscall,FPU管理等,此外还有task-state segment (tss) ,主要用于中断时栈的切换(用户态到内核态需要切换栈帧,而内核态到内核态不用,且CPU是先根据tss切换到内核栈再保存断点的)。使用tss而不用tcb保存栈信息的原因是tss是x86硬件支持的,而tcb不是。
  Task1. Argument Passing
  实现用户程序加载时的命令行参数传递,即argc和argv[]. 历程创建过程起首是 由当前历程执行process execute调用thread_create创建一个对应的内核线程并由该 线程调用start_process,然后在start_process中进行pcb、中断栈帧等的初始化,在 load可执行程序,在加载乐成后通过移动中断栈帧中的esp并进行相应赋值将参数存 到栈中,末了通过中断返回指令将控制权转交给用户程序即可。
以下是参数在栈中的放置方式,留意x86由于call的时候esp必须与16字节对 齐,因此末了将argc传入后需要进行对齐。

  Task2. Process Control Syscalls
    实现历程控制相干体系调用。要留意体系调用和函数调用一样通过栈传递参数, 体系调用处理惩罚时要先检查参数是否合法,如是否空指针,是否访问了内核空间,是否 访问了为映射空间。体系调用的第一个参数是体系调用范例,根据该范例调用对应的 处理惩罚函数,并将返回值保存在中断栈帧的eax中。
    exit()
      历程退出体系调用。开释历程的各类资源,如页表,打开文件,子历程链表 等,此外假如父历程还在,需要signal对应的信号量,以便父历程进行wait,并 设置退出状态供父历程获取。
    exec()
      历程执行体系调用。功能相称于UNIX中的fork()+exec(),即创建新历程并执 行新的可执行程序。为了支持并发加载,设置一个全局的load_list,将待加载的 历程放入链表,并使用信号量对每个节点进行同步。当父历程创建了线程去加载 子历程时,父历程必须阻塞等待看是否加载乐成。子历程加载完毕后唤醒父进 程,然后等待父历程将自己加入到其子历程链表中,才可以开始运行。
    wait()
      历程等待体系调用。等待子历程退出并获取其退出状态。通过子历程链表中 的信号量进行wait,然后获取退出状态,再将其从链表中移除即可。要留意每个 历程只能wait他的子历程,且子历程不存在继承关系,即不能wait孙子历程,否 则直接返回-1. 此外假如某个历程意外退出(kill),也要直接返回-1,因此在还要修 改userprog/exception.c中的kill()函数,将退出状态设为-1.
  Task3. File Operation Syscalls
    实现文件操作体系调用。骨架代码已经实现了根本的文件体系,因此这里直接调 用相应接口就好。留意这些接口不是线程安全的,因此要进行同步,这里直接加一把 全局文件操作锁。
    create(), remove()
      创建一个文件和删除一个文件,调用相应文件体系接口即可。要留意这里的 删除文件指的是将其从目录中删除,若该文件已被某历程打开,该历程还可继承 对文件进行读写,并且由于其已从目录中删除,其他历程无法再打开该文件。当 所有对应的文件描述符都关闭时该文件才真正消失。这一规则与UNIX一致。
    open()
      打开一个文件并返回一个文件描述符。文件描述符(fd)是一个非负整数,是下 图fd Table中元素的下标,每个元素都是一个struct file* 指针,指向一个struct file结构体,该结构体除了存储读写权限,偏移量外,还有成员struct Inode* 指 针指向对应的struct Inode,Inode对应唯一的文件,存储着该文件在磁盘中的位 置等信息。每次open一个文件,不管是不是同一历程,都会构造新的struct file,并返回新的fd. 并且在PintOS中子历程不会继承父历程的文件描述符表。要 留意文件描述符0 (STDIN_FILENO)和1 (STDOUT_FILENO)分别表示尺度输入和标 准输出,不能被open()返回。

    read(), write()
      对fd对应的文件进行读写操作。若fd == STDIN_FILENO则从键盘读入,调 用devices/input.c中的input_getc();若fd == STDOUT_FILENO则输出到控制 台,调用lib/kernel/console.c中的putbuf().
    seek(), tell()
      设置或获取当前fd对应file的读写位置。要留意seek假如超出EOF需要对 文件进行扩展,这将在proj 3中实现。
    close()
      关闭fd对应的文件。调用file_close()开释struct file,file_close会调用 inode_close减少引用次数,当体系中该文件inode的引用次数减为0时才真正关 闭一个文件。
  Task4. Floating Point Operations
    实现支持浮点数操作。x86中浮点数没有专用寄存器来存储,而是使用栈来存 储。由于CPU中只有一个floating-point-unit (FPU),线程要共享它必须存储相干状态 FPU save state,共108字节,在线程切换或中断时进行状态切换,在线程切换栈帧和 中断栈帧中添加FPU save state并在switch.S和intr-subs.S中添加相应指令即可,这 和寄存器值等其他上下文类似。此外FPU save state还需要在线程/历程创建时进行初 始化,需要修改start process()中初始化中断栈帧的代码,这是寄存器切换不需要的。
Proj2: Threads
  实现线程优先级调度和用户历程的多线程支持。PintOS每个线程在内核中都有一页内存用于存储它的TCB (struct thread)和内核栈,如下图所示。线程切换时会调用thread.c中的schedule(),根据调度算法获取下一个线程的tcb,并调用switch.S中的switch_thread()来进行上下文切换,这是一个汇编函数,在切换完成后当火线程也切换了,也就是说每次线程重新获得CPU都是从switch_thread()中返回,之后再调用thread.c中的thread_switch_tail()进行状态设置、页表切换等收尾操作。
  另外PintOS是分时体系,在计时器中断处理惩罚函数中会查看时间片是否用完,用完了会在中断结束时让出CPU (intr_yield_on_return).

  Task1. Efficient Alarm Clock
    实现更高效的timer_sleep()函数。原来的实现是忙等(busy wait)直到时间到,可以 通过thread_block()将其阻塞并将其加入到sleep_list中,在每次时钟处理惩罚函数中检查 是否到时间,到了将其thread_unblock()即可。
  Task2. Strict Priority Scheduler
    实现线程的优先级调度。除了schedule()外还需要修改3个同步原语(互斥锁,信 号量,条件变量)的相干调度代码。
    另外,为了避免优先级逆转(priority inversion)征象,即高优先级的线程A由于同 步机制等待低优先级的线程B,而B由于中优先级的线程C得不到CPU,我们需要实 现优先级捐赠(priority donation)机制,即将B的优先级暂时提高到和A相同。只需实 现lock的优先级捐赠。要考虑嵌套捐赠,即A等C,C等B的情况(将C和B都提高 到A). 我的实现方法是在tcb中添加一个waiting_lock成员,表示正在等待的锁,锁中 有holder成员,表示持有锁的线程,如许就可以形成一个等待链表,每次lock()时遍 历等待链表对链表中的线程进行donation.

    而unlock()时捐赠情况可能会变,如H(high)和M(mid)分别在等L(low)的lock1和 lock2,则L的优先级是high,当lock1开释时,H不再捐赠L,而变成M在捐赠L, 即L的优先级要变为mid. 这可以通过在tcb中添加一个lock_list链表,表示所有持有 的锁,当开释一把锁时遍历其他锁获得最高的优先级来捐赠。
    实现优先级调度的数据结构选择有多种方法:
      (1)链表:这是最简单的方法,插入O(1),查找O(n),修改O(n).
      (2):用堆来维护就绪队列,插入O(logn),查找O(1),但缺点是不方便修 改优先级,需要遍历堆来找到目标修改后再上滤或下滤,复杂度O(n).
      (3)位图+多级队列:这是我想到的相对好实现的最好的方法,每个队列对应 一个优先级,插入是O(1);假如没有位图则查找需要遍历所有队列O(n),而使 用位图表示某一队列是否为空,查找优化为O(1);修改在最坏情况下仍是O(n).
      (4)红黑树:复杂度稳定但难实现,插入,查找,修改都是O(logn).
  Task3. User threads
    实现部分pthread线程库和线程同步syscall以支持用户多线程。PintOS是一对一 的线程模型,也就是说每个用户线程对应一个内核线程。在实现用户多线程后还需要 修改之前User Program的部分代码,主要是exit(),分以下情况:1.主线程调用 pthread_exit(), 等待其他线程结束然后以状态0退出;2.任何线程调用exit(n),整个进 程应该以状态n退出;3.任何线程意外停止,整个历程应该以状态-1退出。上述中的 “整个历程退出”指该历程其他线程也必须立刻停止。当同时发生时三种情况的优先级 是3>2>1.
    pthread_create()
      创建一个用户线程。与process_execute()类似,创建一个新的内核线程让其 运行start_pthread()进行初始化,创建栈,设置参数,并设置中断栈帧中的eip, esp等以在返回时运行用户线程,此外还要将该线程加入到pcb的thread_list 中。留意多用户线程栈结构如下。

    pthread_exit()
      停止当前用户线程,假如是主线程,则join所有其他线程然退却出。其他线 程:先signal join()等待的信号量,然后调用thread_exit()将其从调度队列中去掉 并开释其内核栈即可。主线程:为了防止其他线程join主线程,提前signal对应 信号量,然后再join其他所有线程,末了调用process_exit().
    pthread_join()
      等待指定线程结束。只能join同一历程的线程,且每个线程只能被join一 次。设置信号量进行同步即可。
    lock_init(), lock_acquire(), lock_release()
      互斥锁体系调用。在tcb中用一个链表管理该历程所有互斥锁然后使用其接 口进行相应操作即可。
    sema_init(), sema_down(), sema_up()
      信号量体系调用。在tcb中用一个链表管理该历程所有信号量然后使用其接 口进行相应操作即可。
Proj3: File Systems
  丰富扩展文件体系的功能,如实现内存缓冲区,支持可扩展文件,树形目录结构等。
  Task1. Buffer cache
    实现文件读写的内存缓冲区,更换算法自行选择,我选择的是enhanced-clock算 法,即考虑used位和dirty位,扫描缓冲区,按00,01,10,11的优先次序进行淘 汰。要留意保证对缓冲区访问的线程安全,可以对每个block都加一把读写锁。此外 假如只在淘汰或关机时进行写回会使体系在崩溃时更脆弱,因此可以定期写回,可通 过时钟处理惩罚函数实现。
  Task2. Extensible files
    实现可扩展文件,使得文件巨细可变。这里使用的是类似UNIX-FFS的方法,前 12个block使用直接索引,往后128个block使用一级索引,再往后128 * 128个 block使用二级索引,每个block 512B,也就是说最大可支持8MB的文件巨细。主要 修改inode_create()改变分配block的方式,留意要一块一块地分配,由于索引不需要 连续块,还要修改inode_write_at()以支持动态文件扩展。

  Task3. Subdirectories
    实现树形目录结构和目录操作syscall并且支持相对路径,还需要修改p1实现的 部分文件操作syscall
    树形目录结构通过路径解析并递归读取目录文件即可实现,路径解析时开头假如 是 ’/’ 表示绝对路径,否则是相对路径,相对路径则直接从cwd开始解析,且要留意. 和..这两个特别的文件名,表示本目录和上级目录。留意创建和删除文件时还要在其目 录文件中增加或删除目录项,若某目录是某历程的cwd或已被某历程打开,则不允许 删除(另一种做法是允许删除但不允许再打开该目录下的文件)。
    体系调用部分,目录文件也是个特别的文件,和普通文件一样使用文件描述符来 操作,因此在fd Table中还需要区分其指向的是目录文件还是普通文件,我在pcb中 增加了一个位图来描述,此外在操作时也要判断范例区分接口的使用。
    chdir()
      更改当前历程的工作目录。更改pcb中的cwd指向的struct dir即可。
    mkdir()
      创建新的目录。留意在父目录中添加目录项。
    readdir()
      读取目录中下一个目录项的文件名。struct dir中有pos成员来记录当前读取 的目录项的位置。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

吴旭华

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表