Syscall
CH2 Operating system organization
os的任务是:
- 多任务(multiplexing): 一次支持几个活动(在进程间time-share资源). 即使进程比CPU多, os必须确保每个进程有机会运行.
- 隔离(isolation): 在进程间实现隔离. 如果一个进程有bug, 不会影响到另一个与这个进程无关的进程.
- 协同(interaction): 完全隔离太过了, 进程间需要内部协同.
CPU周围由RAM, ROM(保存着启动时代码), 串行连接到用户的键盘/显示屏和一个用来存储的磁盘.
User mode, supervisor mode, and system calls
隔离需要:
- 在应用程序和os间有界限(应用程序的错误不能影响os的运行, os反过来要处理错误).
- 应用程序之间不能获取各自内存.
CPU可以在三中模式下运行指令:
- machine mode: 在这个模式下的指令由最高的特权. CPU开始于这个模式, machine mode用作配置计算机, 在machine mode下运行几行指令后, CPU进入supervisor mode.
- supervisor mode: CPU被允许运行特权指令(打开关闭中断, 读写寄存器等).
- user mode: 只能运行user mode指令, 如果在user mode 运行特权指令的话, CPU不会运行该指令, 而是切换到supervisor mode, 在supervisor mode下运行指令来终止程序.
应用程序调用kernel函数步骤:
- 应用程序调用read系统调用.
- CPU中有一个特殊指令, 将CPU从user mode切换到supervisor mode, 并进入由内核制定的entry point(ecall指令).
- CPU切换到supervisor mode, 内核检查系统调用参数, 决定应用是否被允许执行.
Process overview
xv6中隔离的最小单元是进程. 进程之间内存, CPU, 文件描述符都不共享. 进程和内核之间也不共享. 内核通过user/supervisor模式标志, 地址空间和线程的time-slicing.
xv6使用页表, RISC-V页表将虚拟地址(RISC-V指令操作的地址)翻译成物理地址(CPU发送给主存).
每个进程都有独立的页表
RISC-V的指针是64 bits, 硬件使用低39 bits来寻找虚拟地址, xv6使用39中的38 bits, 所以最大地址是(2^38 - 1 = 0x3fffffffff, MAXVA).
每个进程保存着许多状态.
每个进程有一个线程, 来运行进程的指令. 线程可以被挂起恢复. 内核通过挂起当前运行进程的线程, 恢复其他进程的线程来切换进程. 线程的状态(局部变量, 函数调用的返回地址)存储在线程栈. 进程有两个栈, user栈和kernel栈. 当进程在运行指令时, 只有user栈被使用, kernel栈为空. 当进程进入内核, user栈仍然保存数据.
ecall指令: 提高特权等级, 并将pc变为内核定义的entry point.
p->state表明进程的状态.
p->pagetable保存进程页表.
Code: starting xv6 and the first process
内核启动并运行第一个进程:
-
RISC-V硬件启动, 初始化并运行一个存储在只读内存中的boot loader. boot loader将xv6内核加载进内存. loader把内核加载进0x80000000的物理地址(0x0: 0x80000000包含I/O设备).
-
在machine mode下, 从_entry运行xv6, 这是MMU还不能用(虚拟地址直接映射到物理地址). _entry的指令设置栈, 这样xv6可以运行C代码. _entry代码把stack0+4096加载进sp, 栈是向下生长的. 这时, 内核有一个栈stack0, 这样 _entry可以调用start.
-
start执行了一些只能在machine mode下的配置. start先开启时钟来产生时钟中断.
之后会在寄存器mstatus中设置之前的模式为supervisor mode
通过将main的地址写入mepc来将返回地址设置为main.
这时还不能开启虚拟地址翻译, 所以把0写入satp中最后调用mret从machine mode 返回到supervisor mode
-
main会初始化几个设备
调用userinit来创建第一个进程
第一个程序会运行一个小程序initcode, 通过调用exec重新进入内核. initcode会用一个新的程序/init来替换当前程序.
init创建一个新的控制台设备文件并开启shell.
Lec3
编译运行内核
xv6代码由三个部分组成:
-
kernel: kernel文件夹中包含所有的内核文件, 这些文件会被编译成kernel的二进制文件, 这个二进制文件会被运行在kernel mode下
-
user: 运行在user mode下的程序.
-
mkfs: 创建一个空的文件镜像, 将这个镜像存放在磁盘上, 这样就可以直接使用一个空的文件系统.
编译过程:
- makefile会读取一个c文件(proc.c), 调用gcc编译器, 生成proc.s的文件, 之后到汇编解释器, 生成proc.o. 所有文件都会有相同操作.
- 系统加载器(loader)会收集所有.o文件, 将它们链接在一起, 生成内核文件.
几个参数:
- -kernel: 内核文件
- -m: RISC-V虚拟机将会使用的内存数量
- -smp: 虚拟机可以使用的CPU核数
- -drive: 虚拟机使用的磁盘驱动, 这里是fs.img
xv6启动过程
-
设置断点在_entry
继续运行到_entry, 发现第一条指令读取了控制系统寄存器mhartid, 并将结果加载到a1寄存器 -
xv6从entry.s启动, 这时没有内存分页, 没有隔离性, 并运行在machine mode下, xv6要跳到supervisor mode下, 给main函数设置一个断点, 这是main已经运行在supervisor mode下了.
-
先是调用consoleinit, 设置好console, 一旦console设置好, 就可以向console打印输出.
-
之后还会初始化一系列
- kinit: 设置好页表分配器(page allocator)
- kvminit: 设置好虚拟内存
- kvminithart: 打开页表
- processinit: 设置好初始进程
- trapinit/ trapinithart: 设置好user/kernel mode转换代码
- plicinit/plicinithart: 设置好中断控制器PLIC(platform level interrupt controller)
- binit: 分配buffer cache
- iinit: 初始化inode缓存
- fileinit: 初始化文件系统
- virtio_disk_init: 初始化磁盘
- userinit: 以上所有设置完成, os系统开始运行, 会通过userinit 运行第一个进程.
-
userinit启动了第一个用户程序(总是需要一个用户程序运行).
userinit需要一个小程序initcode来初始化第一个用户进程.
initcode对应汇编代码如下首先将init地址加载到a0, argv中的地址加载到a1, exec系统调用对应的数字加载到a7, 最后调用ECALL.
-
在syscall上设置一个断点, 继续运行程序, userint会创建进程, 返回到用户空间, 执行3条指令, 再回到内核空间.
num = p->trapframe->a7会读取系统调用对应的整数p->trapframe->a0 = syscall [num] ()实际调用系统调用, 由于传入的是init程序, initcode通过exec调用init程序, init程序会为用户空间设置好一些东西, 调用fork, 并在fork的子程序执行shell.
-
这是shell正常运行