原文,观看体验更佳:https://www.yuxuaan.one/2024/10/04/OSLearn/
OS是建立在上层软件和下层硬件之间的重要桥梁
os随笔
RISC-V 寄存器编号和别名
寄存器 | 别名 | 全称 | 说明 |
---|---|---|---|
$x_0$ | $zero$ | 零寄存器 | 写无效 |
$x_1$ | $ra$ | 链接寄存器 | 函数返回地址 |
$x_2$ | $sp$ | 栈指针寄存器 | 指向下一个将要被存储的栈顶位置 |
$x_3$ | $gp$ | 全局指针寄存器 | 全局变量指针(基地址) |
$x_4$ | $tp$ | 线程指针寄存器 | 线程指针(基地址) |
$x_5$~$x_7$ | $t_0$~$t_2$ | 临时寄存器 | |
$x_8$ | $s_0$/$fp$ | 帧指针寄存器 | 临时寄存器/指向当前函数调用的栈帧的基地址 |
$x_9$ | $s_1$ | 用于函数调用, 被调用函数需要保存的数据 | |
$x_{10}$~$x_{17}$ | $a_0$~$a_7$ | 用于函数调用, 传递参数和返回值 | |
$x_{18}$~$x_{27}$ | $s_2$~$s_{11}$ | 用于函数调用, 被调用函数需要保存的数据 | |
$x_{28}$~$x_{31}$ | $t_3$~$t_6$ | 临时寄存器 |
CSR 寄存器 | 全称(略去首字母) | 说明 |
---|---|---|
mstatus | 存储全局的机器状态信息 | |
mepc | 存储发生异常时的程序计数器值 | |
mcause | 存储导致异常的原因 | |
mtval | Trap Value | 存储与异常相关的附加信息 |
mscratch | Scratch | 用于异常处理程序的临时存储 |
mie | 控制各类中断的使能 | |
mip | 指示各类中断的挂起状态 | |
satp | Address Translation and Protection | 存储页表基址和模式信息 |
sstatus | 存储全局的监督者模式状态信息 | |
sepc | Exception PC | 存储发生异常时的程序计数器值 |
scause | 存储导致陷阱的原因 | |
stval | 存储与陷阱相关的附加信息 | |
stvec | 存储异常处理程序的入口地址 | |
sscratch | 用于监督者模式异常处理程序的临时存储 | |
sie | 控制监督者模式下各类中断的使能 | |
sip | 指示监督者模式下各类中断的挂起状态 |
操作系统基本知识
系统软件
- 定义: 为计算机系统提供基本功能, 并在计算机系统范围内使用的软件, 其作用可涉及到整个计算机系统。
- 包括: 操作系统内核、驱动程序、工具软件、用户界面、软件库等
- 操作系统内核是负责控制计算机的硬件资源并为用户和应用程序提供服务, 操作系统也算是一种系统软件
执行环境
- 操作系统的定义可以被简化为: 应用程序的软件执行环境
操作系统
- 真定义: 操作系统是一种系统软件, 主要功能是向下管理CPU、内存和各种外设等硬件资源, 并形成软件执行环境来向上管理和服务应用软件;操作系统对计算机硬件重要组成的抽象和虚拟化, 有助于应用程序开发
- 主要组成: 内核、系统工具和软件库、用户接口
- API: 应用程序接口, 是操作系统提供给应用程序的接口, 是不同二进制代码片段的纽带
- ABI: 定义了二进制机器代码级别的规则, 例如寄存器怎么用, 是用来约束链接器 (Linker) 和汇编器 (Assembler) 的;操作系统主要通过基于 ABI 的系统调用接口来给应用程序提供上述服务
控制流上下文
- 定义: 确保下一时刻能继续 正确 执行控制流指令的物理资源内容
- 异常控制流
- 中断: 外设引起的I/O操作, 与CPU无关
- 异常: CPU执行过程中发现的
- 陷入(
trap
): 操作系统需要执行系统调用服务
地址空间
- 定义: 是对物理内存的虚拟化和抽象, 也称虚存 (Virtual Memory)
文件
- 文件 (File) 主要用于对持久存储的抽象, 并进一步扩展到为外设的抽象
进程
- 定义: 一个正在运行的程序实例, 它自己认为拥有整个地址空间, 占有整个CPU;精确定义: 一个进程是一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程
- 含义: 在操作系统管理下的程序的一次执行过程
任务
- 任务 (Task): 应用程序的一次执行过程(也是一段控制流), 应用程序在一个时间片段内的执行过程称为任务片
- 任务切换: 两个不同应用在内核中
trap
流的切换 - 任务上下文: 在任务切换的
trap
流中所需要保存的上下文
任务和进程的异同
- 相同: 从一般用户和应用程序的角度看, 任务和进程都表示运行的程序;从操作系统角度看, 任务和进程都表示一个程序的执行过程
- 不同: 进程可以在运行的过程中, 创建子进程、用新的程序内容覆盖已有的程序内容
Qemu 启动流程
- Qemu CPU 的起始 PC 地址为
0x1000
, 在 Qemu 中内嵌了几条指令, 用于初始化 CPU, 然后跳转到0x80000000
处执行 - 物理内存的起始物理地址为
0x80000000
, 这一阶段需要进行各个设备的初始化, 所以在这里交由 Bootloader 来完成, 完成后跳转到 Kernel 的入口地址 - 在 RustSBI 的 Bootloader 中, 会将 Kernel 加载到
0x80200000
处, 所以我们需要把内核镜像加载到这个地址
gdb调试常用命令
b
: 设置断点r
: 运行程序c
: 继续运行n
: 单步执行s
: 单步执行, 如果遇到函数调用则进入函数p
: 打印变量的值bt
: 查看函数调用栈q
: 退出gdbp/d $x1
: 查看x1
寄存器的值x/10i $pc
: 查看当前指令附近的10条指令
内存布局
- 数据部分
- 未初始化数据段(
.bss
), 存放未初始化的全局数据, 通常由程序的加载者代为进行零初始化, 即将这块区域逐字节清零 - 初始化数据段(
.rodata
、.data
), 前者存放只读的全局数据, 通常是一些常量;而后者存放可修改的全局数据 - 堆: 存放程序动态分配的内存, 例如
malloc
函数 - 栈: 存放函数的局部变量、函数参数、返回地址等, 栈是向下增长的
- 未初始化数据段(
- 代码部分
- 代码段(
.text
): 存放程序的机器指令, 通常是只读的
- 代码段(
编译流程
- 编译: 高级语言编译器将源代码编译成汇编代码
- 汇编: 汇编器将汇编代码转换成机器码
- 链接: 链接器将机器码和库文件链接成可执行文件, 将各段放置在内存的合适位置
函数调用
- 函数调用上下文 (Function Call Context): 由于函数调用, 在控制流转移前后需要保持不变的寄存器集合
- CALL 函数时, 上下文将保存在内存里;从函数 Return 时, 上下文将恢复到寄存器中
- 调用者和被调用者合作保存寄存器
- 调用者保存不希望在函数调用中发生变化的寄存器, 在函数调用返回后调用子函数恢复($a_0 ~ a_7$、$t_0 ~ t_6$)
- 被调用者在函数起始保存执行过程中会发生变化的寄存器, 在函数退出之前恢复($s_0 ~ s_{11}$、 $r_a$、 $s_p$、 $f_p$)
特权等级
ecall
具有用户态到内核态的执行环境切换能力的函数调用指令;sret
: 具有内核态到用户态的执行环境切换能力的函数返回指令。监控管理: 当上层软件执行的时候出现了一些异常或特殊情况, 导致需要用到执行环境中提供的功能, 因此需要暂停上层软件的执行, 转而运行执行环境的代码
断点/执行环境调用: 陷入类指令或trap类指令
低特权级软件的要求超出了其能力范围, 就必须寻求高特权级软件的帮助, 否则就是一种异常行为了
低特权级的软件的某条指令发生了某种错误(除零、无效地址访问、无效指令、执行高特权级指令等), 触发异常, 交由高特权级软件(如os)处理, 由它判断恢复/杀死
ch2 邓氏鱼操作系统
功能 | 进度 | 备注 |
---|---|---|
批处理系统 | ✔️ | 用来自动安排程序的执行 |
系统调用服务 | ✔️ | |
多道程序 | ✖️ | |
分时多任务 | ✖️ | |
动态分配内存 | ✖️ |
它对于用户程序调用系统函数的实现
1
2
3//! 存储trap上下文 恢复trap上下文
//! 用户程序 -> ecall -> __alltraps -> trap_handler(UserEnvCall) -> __restore(含sret) -> 用户程序
//! U -> S -> S -> S -> S -> U它对于执行应用程序的实现
- 构造启动应用程序所需
trap
上下文, 具体包含: 全部为0的寄存器、sp寄存器设定为用户栈指针、特权态为用户态、return_addr设定为应用起始地址 - 压入内核栈, 并返回栈指针
- 调用
_restore
进U态执行用户程序
- 构造启动应用程序所需
ch3 始初龙操作系统、腔骨龙操作系统
功能 | 进度 | 备注 |
---|---|---|
批处理系统 | ✔️ | 处理器只能一次运行一个程序, 运行完毕才能运行下一个 |
系统调用服务 | ✔️ | |
多道程序 | ✔️ | 处理器可以交错执行多个程序, 程序可以主动或被动放弃执行 |
分时多任务 | ✔️ | 操作系统管理每个应用程序, 以时间片为单位来分时占用处理器 |
动态分配内存 | ✖️ |
在内存中同一时间可以驻留多个应用, 而且所有的应用都是在系统启动的时候分别加载到内存的不同区域中
同一时间最多只有一个应用在执行(目前是单核), 但是应用可以发出
yield
请求主动放弃占用处理器它对于执行应用程序(任务)的实现
1
2
3//! 存储trap上下文 恢复trap上下文
//! 用户程序 -> ecall(sys_yield) -> __alltraps -> trap_handler(UserEnvCall) -> __switch(cur,next) -> __restore(含sret) -> 用户程序
//! U -> S -> S -> S -> S -> S -> U- 构造启动应用程序所需
trap
上下文, 压入所有程序的内核栈;并且构造所有应用程序的初始任务上下文, 设置ra = __restore
, 若是执行第一个任务, 则构造一个空任务上下文与之__switch
; 若非第一个任务, 则__switch(current_task_cx, next_task_cx)
switch
结束后, 返回到_restore
进U态执行用户程序
- 构造启动应用程序所需
分时多任务: 把用户态下应用程序主动发出的
yield
请求改成时钟中断即可, 两者的本质__switch
是一样的类图如下:
ch4 头甲龙操作系统
功能 | 进度 | 备注 |
---|---|---|
批处理系统 | ✔️ | 处理器只能一次运行一个程序, 运行完毕才能运行下一个 |
系统调用服务 | ✔️ | |
多道程序 | ✔️ | 处理器可以交错执行多个程序, 程序可以主动或被动放弃执行 |
分时多任务 | ✔️ | 操作系统管理每个应用程序, 以时间片为单位来分时占用处理器 |
动态分配内存 | ✔️ |
RAII (Resource Acquisition Is Initialization) 思想: 资源获取即初始化, 资源释放即析构
riscv中的页表项(PageTableEntry)
多级页表(
trie
树)的节点规定, 显然当x=w=r=0, v=1
时, 为非叶子结点从
va
到pa
的转换过程- 首先把
va
看成(VPN0, VPN1, VPN2, Offset)
- 根据
VPN0
在一级页表找到二级页表的页号 - 根据
VPN1
在二级页表找到三级页表的页号 - 根据
VPN2
在三级页表找到物理页号 - 根据
Offset
找到物理地址
- 首先把
分页机制启用后, 在修改
satp
寄存器切换地址空间时, 从处理器视角看, 修改satp
寄存器的指令后后面的指令是相邻的, 但是实际它们的地址空间不一样, 所以我们需要保证在切换地址空间处的指令是平滑的, 这样才能保证地址空间的切换不会影响指令的连续执行——跳板页面,trap.S
段切换地址空间前后位于同一个物理页帧分页机制启用后,
trap
上下文放到应用地址次高页面原因:- 首先,
trap
上下文的保存是需要保存所有通用寄存器的, 那么进trap以后你不能覆盖这些通用寄存器的值 - 如果选择将
trap
上下文保存到内核地址空间的内核栈里面, 你需要:- 覆盖
satp
寄存器, 使得其根节点指向内核地址空间的页表根节点 - 从内核地址空间的内核栈中读出
trap
上下文, 存到应用内核栈中, 所以你需要得知内核栈栈顶地址, 覆盖sp
寄存器
- 覆盖
- 那么上述操作覆盖了两个寄存器, 而RISC-V中只提供了一个
sscratch
寄存器可以临时存储保存, 所以trap
上下文不能放在内核地址空间的内核栈里 - 所以我们选择全程在应用地址空间中进行
trap
上下文的保存
- 首先,
类图如下:
ch5 伤齿龙操作系统
- 将应用编号替换为进程标识符
- 进程的生成机制: 内核中手动生成的进程只有初始进程
initproc
, 其余进程均为初始进程fork
而来- 系统调用
fork
会创建一个新的进程, 新进程的内存空间和父进程完全一致 - 系统调用
exec
使得一个进程能够加载一个新应用的ELF
可执行文件中的代码和数据替换原有的应用地址空间中的内容
- 系统调用
exec
系统调用时,trap_handler
中的原先的trap
上下文失效了, 我们需要在syscall
分发函数返回之后需要重新获取trap
上下文- 父进程 fork 的返回值为子进程的 PID , 而子进程的返回值为 0
ch6 暴王龙操作系统
- 文件系统内部的操作都是假定在已持有 efs 锁的情况下才被调用的, 因此它们不应尝试获取锁;相反提供给用户的操作, 全程均需持有
EasyFileSystem
的互斥锁 - 在之前需要将所有的应用都链接到内核中, 随后在应用管理器中通过应用名进行索引来找到应用的
ELF
数据。在实现了文件系统之后, 可以将这些应用打包到easy-fs
镜像中放到磁盘中, 当需要执行应用的时候只需从文件系统中取出ELF
执行文件格式的应用并加载到内存中执行即可 diskinode
实际管理着文件/目录的元数据,它指向数据块,目录项和文件内容是实际存在数据块中的inode
封装了diskinode
,提供了更方便的读写功能
ch7 白垩纪“迅猛龙”操作系统
一切皆文件: 对I/O设备的抽象, 只需要完成
File
trait- 键盘: 只读的文件
- 屏幕: 只写的文件
- 串口: 是获得字符输入和展示程序的字符输出结果的一种字符通信设备, 可抽象为一种可读写性质的文件
应用程序基于”一切皆文件“的访问
- open: 先打开文件, 获得文件描述符, 开启读写权限
- read/write: 读写文件
- close: 关闭文件,关闭读写权限
管道: 有读段和写端; 自身是那个带有一定大小缓冲区的字节队列
I/O 重定向: 内核获取Usershell的命令行参数;
sys_exec
将命令行参数压入用户栈; 用户库从用户栈还原命令行参数; 重定向(后续fork的子进程, 关闭标准输入输出, 利用sys_dup
分配新fd)信号: 类似硬件的中断信号, 提供进程间异步的通知
sys_kill
当前进程发送信号(信号有不同类型)给另一个进程sys_sigaction
可以为当前进程的某个信号设置一个处理函数(传函数地址), 并且可以保存原来的处理函数, 处理函数中含有信号掩码sys_sigprocmask
可以为当前进程设置信号掩码, 屏蔽某些信号sys_sigreturn
信号处理例程退出时使用
信号的来源
kill
系统调用- 内核给进程发的信号, 常见的例子是进程执行的时候出错, 比如段错误
SIGSEGV
和非法指令异常SIGILL
ch8 达科塔盗龙操作系统
进程是线程的资源容器,线程成为了程序的基本执行实体
- 进程间相互独立(即资源隔离),同一进程的各线程间共享进程的资源(即资源共享)
- 每个线程有其自己的执行上下文(线程ID、程序计数器、寄存器集合和执行栈),而进程的执行上下文包括其管理的所有线程的执行上下文和地址空间(故同一进程下的线程间上下文切换比进程间上下文切换要快)
- 线程是一个可调度/分派/执行的实体(线程有就绪、阻塞和运行三种基本执行状态),进程不是可调度/分派/执行的的实体,而是线程的资源容器
- 每个线程也需要有一个独立的跳板页
TRAMPOLINE
来完成用户态切换到内核态的地址空间平滑转换的事务
线程有关系统调用
sys_thread_create
: 当前进程创建一个新的线程- 线程正常运行所需环境:
- 用户态栈
- 内核态栈
- 跳板页
- 上下文:用于线程切换
- 线程正常运行所需环境:
sys_waittid
: 进程/主线程要负责通过waittid
来等待它创建出来的线程(不是主线程)结束并回收它们在内核中的资源 (如线程的内核栈、线程控制块等);一个线程在通过exit
系统调用退出时,内核会回收线程占用的部分资源,即用户态用到的资源,比如用户态的栈,用于系统调用和异常处理的跳板页等
信号量是操作系统中的一种同步原语,用于在多个线程或进程之间共享资源时进行互斥访问。它通常是一个整数值,用于计数指定数量的资源可用。
网络
- 创建套接字:使用系统调用创建一个套接字。
- 绑定套接字:将套接字绑定到一个特定的IP地址和端口号。
- 监听连接:对于服务器端,监听来自客户端的连接请求。
- 接受连接:接受客户端的连接请求。
- 发送和接收数据:通过套接字发送和接收数据。
- 关闭套接字:完成通信后关闭套接字。
其他
- batch一般指批处理, 而job一般指作业
- fd 通常是文件描述符(File Descriptor)的缩写