xv6学习笔记——第二章 操作系统架构

引言

  • 操作系统满足的条件:多路复用、隔离、交互
  • Xv6运行在多核RISC-V微处理器上
    • RISC-V是一个64位的中央处理器
    • 基于“LP64”的C语言编写
    • long(L)和指针(P)变量是64位,但int是32位

抽象系统资源

  • Unix应用程序只通过文件系统的openreadwriteclose系统调用与存储交互,而不是直接读写磁盘
  • Unix进程使用exec来构建它们的内存映像,而不是直接与物理内存交互

用户态,核心态,以及系统调用

用户态=用户模式=目态
核心态=管理模式=管态
  • CPU为强隔离提供硬件支持
  • RISC-V有三种CPU可以执行指令的模式:机器模式(Machine Mode)、用户模式(User Mode)和管理模式(Supervisor Mode)

  • 机器模式:
    • 执行的指令具有完全特权
    • CPU在机器模式下启动
    • 主要用于配置计算机
    • 执行部分代码后,进入管理模式

  • 管理模式:
    • CPU被允许执行特权指令,在内核空间中运行
      • 启用和禁用中断、读取和写入保存页表地址的寄存器
    • 内核:在此模式下(或内核空间)中运行的软件

  • 用户模式:
    • 程序无法执行特权指令,而是要切换到管理模式
      • 所以想要调用内核函数(像read)的应用程序,必须过度到内核
      • ecall指令:将CPU从用户模式切换到管理模式,并在内核指定的入口点进入内核—>入口点绝对是内核决定的
      • 内核对应用程序请求的操作有决定权
    • 应用程序执行用户模式的指令,在用户空间中运行

用户空间与内核空间

  • 用户空间运行的程序运行在user mode,内核空间的程序运行在kernel mode

  • 操作系统位于内核空间

  • 在这里插入图片描述


内核组织

  • 宏内核(monolithic kernel):整个操作系统都驻留在内核中,所有系统调用的实现都以管理模式运行
    • 优点:整个操作系统以完全的硬件特权运行;操作系统的不同部分更容易合作
    • 缺点:不同部分之间的接口通常很复杂,管理模式中的错误经常会导致内核失败
    • 解决办法:减少在管理模式下运行的操作系统代码量,并**在用户模式下执行大部分操作系统 **—> 微内核(microkernel)

在这里插入图片描述

  • 文件系统作为用户级进程运行

  • 服务器:作为进程运行操作系统服务

    客户/服务器(Client/Server)模式:

    单机微内核操作系统都采用客户/服务器模式,将操作系统中最基本的部分放入内核中,而把操作系统的绝大部分功能都放在微内核外面的一组服务器(进程)中实现

  • 为了允许应用程序与文件服务器交互,内核提供了允许从一个用户态进程向另一个用户态进程发送消息的进程间通信机制。例如,如果像shell这样的应用程序想要读取或写入文件,它会向文件服务器发送消息并等待响应。

  • xv6属于宏内核

    • 因此,xv6内核接口对应于操作系统接口,内核实现了完整的操作系统

xv6架构

  • XV6的源代码位于**kernel /**子目录中

  • 源代码按照模块化的概念划分为多个文件

  • 模块间的接口都被定义在了def.hkernel/defs.h

  • 文件描述
    bio.c文件系统的磁盘块缓存
    console.c连接到用户的键盘和屏幕
    entry.S首次启动指令
    exec.cexec()系统调用
    file.c文件描述符支持
    fs.c文件系统
    kalloc.c物理页面分配器
    kernelvec.S处理来自内核的陷入指令以及计时器中断
    log.c文件系统日志记录以及崩溃修复
    main.c在启动过程中控制其他模块初始化
    pipe.c管道
    plic.cRISC-V中断控制器
    printf.c格式化输出到控制台
    proc.c进程和调度
    sleeplock.cLocks that yield the CPU
    spinlock.cLocks that don’t yield the CPU.
    start.c早期机器模式启动代码
    string.c字符串和字节数组库
    swtch.c线程切换
    syscall.cDispatch system calls to handling function.
    sysfile.c文件相关的系统调用
    sysproc.c进程相关的系统调用
    trampoline.S用于在用户和内核之间切换的汇编代码
    trap.c对陷入指令和中断进行处理并返回的C代码
    uart.c串口控制台设备驱动程序
    virtio_disk.c磁盘设备驱动程序
    vm.c管理页表和地址空间

注:syscall.c:操作系统内核中用于处理系统调用的核心部分,它负责从用户态切换到内核态,根据系统调用号来执行相应的系统调用函数,并将结果返回给用户程序。这是操作系统的关键功能之一,用于实现用户程序与硬件之间的交互

注:proc.c:多进程操作系统内核的进程管理机制,允许多个进程在共享计算资源的情况下并发执行,用于创建、管理和调度进程,以及提供了一些与进程相关的功能,如进程创建、进程退出、等待子进程、唤醒进程等。以实现多任务和多用户的支持

  1. 进程初始化 (procinitproc_pagetable 函数):在系统启动时初始化进程表和进程页表,为每个进程分配内核栈和页表procinit 函数初始化进程表和内核栈,而 proc_pagetable 函数为进程创建用户页表,并映射 trampoline 代码和 trapframe。
  2. 进程创建 (allocprocfork 函数):allocproc 函数用于分配一个新的进程控制块struct proc)以及相关资源。fork 函数复制父进程的内存空间和上下文。
  3. 进程退出 (exit 函数):exit 函数用于终止当前进程的执行,关闭该进程打开的文件,释放进程占用的资源,并将进程状态设置为 ZOMBIE
  4. 进程等待 (wait 函数):如果没有子进程退出,父进程将会被阻塞,直到有子进程退出。
  5. 进程调度 (scheduler 函数):scheduler 函数是一个无限循环,负责选择要运行的进程,并在不同进程之间切换。它会扫描进程表,找到处于可运行状态的进程,并进行上下文切换,将 CPU 分配给选定的进程。
  6. 进程睡眠和唤醒 (sleepwakeup 函数)

进程概述

  • Xv6中的隔离单位是一个进程

  • 进程抽象防止一个进程破坏或监视另一个进程的内存、CPU、文件描述符等。它还防止一个进程破坏内核本身

  • 内核用来实现进程的机制包括:用户/管理模式标志、地址空间和线程的时间切片

  • Xv6使用页表(由硬件实现)为每个进程提供自己的地址空间

    RISC-V页表将虚拟地址(RISC-V指令操纵的地址)转换(或“映射”)为物理地址(CPU芯片发送到主存储器的地址)

在这里插入图片描述

操作系统可能会使用不同的虚拟地址空间部分来隔离栈和堆,或者出于安全性考虑,操作系统可能会将栈映射到较低的虚拟地址,以减少栈溢出对其他部分的潜在影响

(xv6和我们的x86可能就是不太一样)

  • Xv6为每个进程维护一个单独的页表,定义了该进程的地址空间

  • 硬件在页表中查找虚拟地址时只使用低39位;xv6只使用这39位中的38位。因此,最大地址是2^38-1=0x3fffffffff,即MAXVA(定义在kernel/riscv.h:348)

  • xv6为trampoline(用于在用户和内核之间切换)和trapframe(用于映射进程切换到内核)分别保留了一个页面


  • xv6内核为每个进程维护许多状态片段,并将它们聚集到一个proc(kernel/proc.h:86)结构体中

    • 一个进程最重要的内核状态片段是它的页表、内核栈区和运行状态
    • 使用符号p->xxx来引用proc结构体的元素
      • p->pagetable:指向该进程页表的指针
      • p->kstack:内核栈区
      • p->state:表明进程是已分配、就绪态、运行态、等待I/O中(阻塞态)还是退出

  • 每个进程都有一个执行线程(或简称线程)来执行进程的指令

    • 在进程之间切换:内核挂起当前运行的线程,并恢复另一个进程的线程
    • 线程的大部分状态存储在线程的栈区上
    • 线程可以在内核中“阻塞”等待I/O,并在I/O完成后恢复到中断的位置

  • 每个进程有两个栈区:一个用户栈区和一个内核栈区(p->kstack

    • 用户栈:进程执行用户指令时
    • 内核栈:进程进入内核(由于系统调用或中断)时,内核代码在上面执行

一个进程可以通过执行RISC-V的ecall指令进行系统调用,该指令提升硬件特权级别,并将程序计数器(PC)更改为内核定义的入口点,入口点的代码切换到内核栈,执行实现系统调用的内核指令,当系统调用完成时,内核切换回用户栈,并通过调用sret指令返回用户空间,该指令降低了硬件特权级别,并在系统调用指令刚结束时恢复执行用户指令


启动xv6和第一个进程

  1. RISC-V计算机上电时,它会初始化自己运行一个存储在只读内存中的引导加载程序

  2. 引导加载程序将xv6内核加载到内存中

    此时页式硬件被禁用

    因此虚拟地址将直接映射到物理地址

    加载到物理地址为0x80000000的内存中

    因为地址范围0x0 ~ 0x80000000包含I/O设备,所以从0x80000000开始

  3. 机器模式下,中央处理器从_entry (kernel/entry.S:6)开始运行xv6

  4. _entry的指令设置了一个栈区,这样xv6就可以运行C代码

    Xv6在*start. c (kernel/start.c:11)文件中为初始栈stack0***声明了空间

    栈是向下扩展的,所以_entry的代码将栈顶地址stack0+4096加载到栈顶指针寄存器sp

    此时内核有了栈区,_entry便调用C代码start(kernel/start.c:21)

  5. 函数start在机器模式下执行部分操作,然后切换到管理模式

    其中一个操作是:对时钟芯片进行编程以产生计时器中断

    指令mret:进入管理模式

    但这里是用到其他的方式

  6. main(kernel/main.c:11)初始化几个设备和子系统后,便通过调用userinit (kernel/proc.c:212)创建第一个进程

    第一个进程执行一个小型程序:initcode. S (**user/initcode.S:**1)

    它通过调用exec系统调用重新进入内核

    exec用一个新程序(本例中为 /init)替换当前进程的内存和寄存器

  7. 当内核完成exec,该小型程序就返回/init进程中的用户空间

    有时候,init(*user/init.c*:15)将创建一个新的控制台设备文件,然后以文件描述符0、1和2打开它

    然后它在控制台上启动一个shell

  8. 系统启动完毕


内核的编译与运行

  1. Makefile(XV6目录下的文件)会读取一个C文件,例如proc.c

  2. 之后调用gcc编译器,生成一个文件叫做proc.s,这是RISC-V 汇编语言文件

  3. 后再走到汇编解释器,生成proc.o,这是汇编语言的二进制格式。

Makefile会为所有内核的文件做相同的操作,并得到.o文件

  1. 最后,系统加载器(Loader)会收集所有的.o文件,将它们链接在一起,并生成内核文件。

    生成的内核文件就是我们将会在QEMU中运行的文件。

    同时,为了你们的方便,Makefile还会创建kernel.asm,这里包含了内核的完整汇编语言,你们可以通过查看它来定位究竟是哪个指令导致了Bug

在这里插入图片描述

  • 这样,XV6系统就在QEMU中启动了

从C源代码到可执行文件的四个过程:预处理、编译、汇编、链接

来自:here
在这里插入图片描述


系统调用的过程

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值