LOADING

加载过慢请开启缓存 浏览器默认开启

OS学习随笔

OS是建立在上层软件和下层硬件之间的重要桥梁

os随笔

源自rCore-Tutorial-Book

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 指示监督者模式下各类中断的挂起状态

操作系统基本知识

系统软件

  1. 定义: 为计算机系统提供基本功能, 并在计算机系统范围内使用的软件, 其作用可涉及到整个计算机系统。
  2. 包括: 操作系统内核、驱动程序、工具软件、用户界面、软件库等
  3. 操作系统内核是负责控制计算机的硬件资源并为用户和应用程序提供服务, 操作系统也算是一种系统软件

执行环境

alt text

  1. 操作系统的定义可以被简化为: 应用程序的软件执行环境

操作系统

  1. 真定义: 操作系统是一种系统软件, 主要功能是向下管理CPU、内存和各种外设等硬件资源, 并形成软件执行环境来向上管理和服务应用软件;操作系统对计算机硬件重要组成的抽象和虚拟化, 有助于应用程序开发
  2. 主要组成: 内核、系统工具和软件库、用户接口
  3. API: 应用程序接口, 是操作系统提供给应用程序的接口, 是不同二进制代码片段的纽带
  4. ABI: 定义了二进制机器代码级别的规则, 例如寄存器怎么用, 是用来约束链接器 (Linker) 和汇编器 (Assembler) 的;操作系统主要通过基于 ABI 的系统调用接口来给应用程序提供上述服务

控制流上下文

  1. 定义: 确保下一时刻能继续 正确 执行控制流指令的物理资源内容
  2. 异常控制流
    1. 中断: 外设引起的I/O操作, 与CPU无关
    2. 异常: CPU执行过程中发现的
    3. 陷入( trap ): 操作系统需要执行系统调用服务

地址空间

  1. 定义: 是对物理内存的虚拟化和抽象, 也称虚存 (Virtual Memory)

文件

  1. 文件 (File) 主要用于对持久存储的抽象, 并进一步扩展到为外设的抽象

进程

  1. 定义: 一个正在运行的程序实例, 它自己认为拥有整个地址空间, 占有整个CPU;精确定义: 一个进程是一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程
  2. 含义: 在操作系统管理下的程序的一次执行过程

任务

  1. 任务 (Task): 应用程序的一次执行过程(也是一段控制流), 应用程序在一个时间片段内的执行过程称为任务片
  2. 任务切换: 两个不同应用在内核中 trap 流的切换
  3. 任务上下文: 在任务切换的 trap 流中所需要保存的上下文

任务和进程的异同

  1. 相同: 从一般用户和应用程序的角度看, 任务和进程都表示运行的程序;从操作系统角度看, 任务和进程都表示一个程序的执行过程
  2. 不同: 进程可以在运行的过程中, 创建子进程、用新的程序内容覆盖已有的程序内容

Qemu 启动流程

  1. Qemu CPU 的起始 PC 地址为0x1000, 在 Qemu 中内嵌了几条指令, 用于初始化 CPU, 然后跳转到0x80000000处执行
  2. 物理内存的起始物理地址为0x80000000, 这一阶段需要进行各个设备的初始化, 所以在这里交由 Bootloader 来完成, 完成后跳转到 Kernel 的入口地址
  3. 在 RustSBI 的 Bootloader 中, 会将 Kernel 加载到0x80200000处, 所以我们需要把内核镜像加载到这个地址

gdb调试常用命令

  1. b: 设置断点
  2. r: 运行程序
  3. c: 继续运行
  4. n: 单步执行
  5. s: 单步执行, 如果遇到函数调用则进入函数
  6. p: 打印变量的值
  7. bt: 查看函数调用栈
  8. q: 退出gdb
  9. p/d $x1: 查看x1寄存器的值
  10. x/10i $pc: 查看当前指令附近的10条指令

内存布局

内存布局

  1. 数据部分
    • 未初始化数据段(.bss), 存放未初始化的全局数据, 通常由程序的加载者代为进行零初始化, 即将这块区域逐字节清零
    • 初始化数据段(.rodata.data), 前者存放只读的全局数据, 通常是一些常量;而后者存放可修改的全局数据
    • 堆: 存放程序动态分配的内存, 例如malloc函数
    • 栈: 存放函数的局部变量、函数参数、返回地址等, 栈是向下增长的
  2. 代码部分
    • 代码段(.text): 存放程序的机器指令, 通常是只读的

编译流程

  1. 编译: 高级语言编译器将源代码编译成汇编代码
  2. 汇编: 汇编器将汇编代码转换成机器码
  3. 链接: 链接器将机器码和库文件链接成可执行文件, 将各段放置在内存的合适位置

函数调用

函数调用

  1. 函数调用上下文 (Function Call Context): 由于函数调用, 在控制流转移前后需要保持不变的寄存器集合
  2. CALL 函数时, 上下文将保存在内存里;从函数 Return 时, 上下文将恢复到寄存器中
  3. 调用者和被调用者合作保存寄存器
    • 调用者保存不希望在函数调用中发生变化的寄存器, 在函数调用返回后调用子函数恢复($a_0 ~ a_7$、$t_0 ~ t_6$)
    • 被调用者在函数起始保存执行过程中会发生变化的寄存器, 在函数退出之前恢复($s_0 ~ s_{11}$、 $r_a$、 $s_p$、 $f_p$)
    • fp和sp

特权等级

执行环境栈

  1. ecall 具有用户态到内核态的执行环境切换能力的函数调用指令;

    ecall

  2. sret : 具有内核态到用户态的执行环境切换能力的函数返回指令。

  3. 监控管理: 当上层软件执行的时候出现了一些异常或特殊情况, 导致需要用到执行环境中提供的功能, 因此需要暂停上层软件的执行, 转而运行执行环境的代码

  4. 断点/执行环境调用: 陷入类指令或trap类指令

  5. 低特权级软件的要求超出了其能力范围, 就必须寻求高特权级软件的帮助, 否则就是一种异常行为了

  6. 低特权级的软件的某条指令发生了某种错误(除零、无效地址访问、无效指令、执行高特权级指令等), 触发异常, 交由高特权级软件(如os)处理, 由它判断恢复/杀死

ch2 邓氏鱼操作系统

功能 进度 备注
批处理系统 ✔️ 用来自动安排程序的执行
系统调用服务 ✔️
多道程序 ✖️
分时多任务 ✖️
动态分配内存 ✖️
  1. 它对于用户程序调用系统函数的实现

    //!                     存储trap上下文                                恢复trap上下文
    //! 用户程序 -> ecall -> __alltraps -> trap_handler(UserEnvCall) -> __restore(含sret) -> 用户程序
    //!    U     ->   S  ->      S      ->             S             ->         S         ->    U
    
  2. 它对于执行应用程序的实现

    1. 构造启动应用程序所需 trap 上下文, 具体包含: 全部为0的寄存器、sp寄存器设定为用户栈指针、特权态为用户态、return_addr设定为应用起始地址
    2. 压入内核栈, 并返回栈指针
    3. 调用 _restore 进U态执行用户程序

ch3 始初龙操作系统、腔骨龙操作系统

功能 进度 备注
批处理系统 ✔️ 处理器只能一次运行一个程序, 运行完毕才能运行下一个
系统调用服务 ✔️
多道程序 ✔️ 处理器可以交错执行多个程序, 程序可以主动或被动放弃执行
分时多任务 ✔️ 操作系统管理每个应用程序, 以时间片为单位来分时占用处理器
动态分配内存 ✖️
  1. 在内存中同一时间可以驻留多个应用, 而且所有的应用都是在系统启动的时候分别加载到内存的不同区域中

  2. 同一时间最多只有一个应用在执行(目前是单核), 但是应用可以发出 yield 请求主动放弃占用处理器

  3. 它对于执行应用程序(任务)的实现

    //!                                存储trap上下文                                                      恢复trap上下文
    //! 用户程序 -> ecall(sys_yield) -> __alltraps -> trap_handler(UserEnvCall) -> __switch(cur,next) -> __restore(含sret) -> 用户程序
    //!    U     ->   S              ->      S     ->             S             ->         S          ->         S         ->    U
    
    1. 构造启动应用程序所需 trap 上下文, 压入所有程序的内核栈;并且构造所有应用程序的初始任务上下文, 设置 ra = __restore , 若是执行第一个任务, 则构造一个空任务上下文与之 __switch ; 若非第一个任务, 则 __switch(current_task_cx, next_task_cx)
    2. switch 结束后, 返回到 _restore 进U态执行用户程序
  4. 分时多任务: 把用户态下应用程序主动发出的 yield 请求改成时钟中断即可, 两者的本质 __switch 是一样的

  5. 类图如下:

class

ch4 头甲龙操作系统

功能 进度 备注
批处理系统 ✔️ 处理器只能一次运行一个程序, 运行完毕才能运行下一个
系统调用服务 ✔️
多道程序 ✔️ 处理器可以交错执行多个程序, 程序可以主动或被动放弃执行
分时多任务 ✔️ 操作系统管理每个应用程序, 以时间片为单位来分时占用处理器
动态分配内存 ✔️
  1. RAII (Resource Acquisition Is Initialization) 思想: 资源获取即初始化, 资源释放即析构

  2. riscv中的页表项(PageTableEntry)

    PageTableEntry

  3. 多级页表(trie 树)的节点规定, 显然当 x=w=r=0, v=1 时, 为非叶子结点

    PTE_rwx_fields

  4. vapa 的转换过程

    1. 首先把 va 看成 (VPN0, VPN1, VPN2, Offset)
    2. 根据 VPN0 在一级页表找到二级页表的页号
    3. 根据 VPN1 在二级页表找到三级页表的页号
    4. 根据 VPN2 在三级页表找到物理页号
    5. 根据 Offset 找到物理地址
  5. 分页机制启用后, 在修改 satp 寄存器切换地址空间时, 从处理器视角看, 修改 satp 寄存器的指令后后面的指令是相邻的, 但是实际它们的地址空间不一样, 所以我们需要保证在切换地址空间处的指令是平滑的, 这样才能保证地址空间的切换不会影响指令的连续执行——跳板页面, trap.S 段切换地址空间前后位于同一个物理页帧

  6. 分页机制启用后, trap 上下文放到应用地址次高页面原因:

    1. 首先, trap 上下文的保存是需要保存所有通用寄存器的, 那么进trap以后你不能覆盖这些通用寄存器的值
    2. 如果选择将 trap 上下文保存到内核地址空间的内核栈里面, 你需要:
      1. 覆盖 satp 寄存器, 使得其根节点指向内核地址空间的页表根节点
      2. 从内核地址空间的内核栈中读出 trap 上下文, 存到应用内核栈中, 所以你需要得知内核栈栈顶地址, 覆盖 sp 寄存器
    3. 那么上述操作覆盖了两个寄存器, 而RISC-V中只提供了一个 sscratch 寄存器可以临时存储保存, 所以 trap 上下文不能放在内核地址空间的内核栈里
    4. 所以我们选择全程在应用地址空间中进行 trap 上下文的保存
  7. 类图如下:

class

ch5 伤齿龙操作系统

  1. 将应用编号替换为进程标识符
  2. 进程的生成机制: 内核中手动生成的进程只有初始进程 initproc, 其余进程均为初始进程 fork 而来
    1. 系统调用 fork 会创建一个新的进程, 新进程的内存空间和父进程完全一致
    2. 系统调用 exec 使得一个进程能够加载一个新应用的 ELF 可执行文件中的代码和数据替换原有的应用地址空间中的内容
  3. exec 系统调用时, trap_handler 中的原先的 trap 上下文失效了, 我们需要在 syscall 分发函数返回之后需要重新获取 trap 上下文
  4. 父进程 fork 的返回值为子进程的 PID , 而子进程的返回值为 0

ch6 暴王龙操作系统

  1. 文件系统内部的操作都是假定在已持有 efs 锁的情况下才被调用的, 因此它们不应尝试获取锁;相反提供给用户的操作, 全程均需持有 EasyFileSystem 的互斥锁
  2. 在之前需要将所有的应用都链接到内核中, 随后在应用管理器中通过应用名进行索引来找到应用的 ELF 数据。在实现了文件系统之后, 可以将这些应用打包到 easy-fs 镜像中放到磁盘中, 当需要执行应用的时候只需从文件系统中取出 ELF 执行文件格式的应用并加载到内存中执行即可
  3. diskinode 实际管理着文件/目录的元数据,它指向数据块,目录项和文件内容是实际存在数据块中的
  4. inode 封装了 diskinode,提供了更方便的读写功能

ch7 白垩纪“迅猛龙”操作系统

  1. 一切皆文件: 对I/O设备的抽象, 只需要完成 File trait

    • 键盘: 只读的文件
    • 屏幕: 只写的文件
    • 串口: 是获得字符输入和展示程序的字符输出结果的一种字符通信设备, 可抽象为一种可读写性质的文件
  2. 应用程序基于”一切皆文件“的访问

    1. open: 先打开文件, 获得文件描述符, 开启读写权限
    2. read/write: 读写文件
    3. close: 关闭文件,关闭读写权限
  3. 管道: 有读段和写端; 自身是那个带有一定大小缓冲区的字节队列

  4. I/O 重定向: 内核获取Usershell的命令行参数; sys_exec 将命令行参数压入用户栈; 用户库从用户栈还原命令行参数; 重定向(后续fork的子进程, 关闭标准输入输出, 利用 sys_dup 分配新fd)

  5. 信号: 类似硬件的中断信号, 提供进程间异步的通知

    • sys_kill 当前进程发送信号(信号有不同类型)给另一个进程
    • sys_sigaction 可以为当前进程的某个信号设置一个处理函数(传函数地址), 并且可以保存原来的处理函数, 处理函数中含有信号掩码
    • sys_sigprocmask 可以为当前进程设置信号掩码, 屏蔽某些信号
    • sys_sigreturn 信号处理例程退出时使用
  6. 信号的来源

    • kill 系统调用
    • 内核给进程发的信号, 常见的例子是进程执行的时候出错, 比如段错误 SIGSEGV 和非法指令异常 SIGILL

ch8 达科塔盗龙操作系统

  1. 进程是线程的资源容器,线程成为了程序的基本执行实体

    • 进程间相互独立(即资源隔离),同一进程的各线程间共享进程的资源(即资源共享)
    • 每个线程有其自己的执行上下文(线程ID、程序计数器、寄存器集合和执行栈),而进程的执行上下文包括其管理的所有线程的执行上下文和地址空间(故同一进程下的线程间上下文切换比进程间上下文切换要快)
    • 线程是一个可调度/分派/执行的实体(线程有就绪、阻塞和运行三种基本执行状态),进程不是可调度/分派/执行的的实体,而是线程的资源容器
    • 每个线程也需要有一个独立的跳板页 TRAMPOLINE 来完成用户态切换到内核态的地址空间平滑转换的事务
  2. 线程有关系统调用

    • sys_thread_create : 当前进程创建一个新的线程
      • 线程正常运行所需环境:
        • 用户态栈
        • 内核态栈
        • 跳板页
        • 上下文:用于线程切换
    • sys_waittid: 进程/主线程要负责通过 waittid 来等待它创建出来的线程(不是主线程)结束并回收它们在内核中的资源 (如线程的内核栈、线程控制块等);一个线程在通过 exit 系统调用退出时,内核会回收线程占用的部分资源,即用户态用到的资源,比如用户态的栈,用于系统调用和异常处理的跳板页等
  3. 信号量是操作系统中的一种同步原语,用于在多个线程或进程之间共享资源时进行互斥访问。它通常是一个整数值,用于计数指定数量的资源可用。

网络

OSI模型

OSI

套接字

  1. 定义:套接字的本质是操作系统提供的一种抽象接口,它将网络通信的复杂性封装成简单的API,使应用程序能够方便地实现网络功能(应用层到传输层或其他协议层的访问接口)。套接字既是通信端点(例如TCP套接字,它是TCP端点的同义词)的抽象,也是操作系统资源和协议栈的接口。可以看做是一种特殊的文件

TCP连接的建立

从用户的角度(应用层)看,建立TCP连接就像打电话。

  1. socket: 申请一个电话机(套接字描述符号)。
  2. bind: 给电话机绑定自己的电话号码(IP地址和端口号)。
  3. listen: 开机,允许接受别人打电话打进来(监听端口)。
  4. accept: 有人打电话进来,接听电话(建立连接)。
  5. connect: 拨号,打电话给别人(发起连接)。

TCP状态机

TCP状态机

TIME-WAIT 状态

  1. 用于可靠实现TCP全双工连接的中止。比如ACK发送端发送ACK失败了,接收端重发FIN,此时ACK发送端需要该状态重新接受。
  2. 允许老的重复分组在网络中消失:

基本假设:每个数据报存在跳限(数据包在网络传输过程中能够经过的最大路由器数量),假设具有最大跳限的分组(网络层数据传输的基本单位)在网络中存在的时间不会超过MSL秒;路由协议寻找新路径的时间不超过MSL。

考虑网路中可能出现分组”迷路”的情况(可能是某个路由器坏了,路由协议需要时间寻找新路径),导致该分组在网络中滞留。通过TIME-WAIT状态,在最坏情况下,发送方向和接受方向的分组都在网络中滞留,等待2MSL时间后,可以保证网络中的所有分组都已经消失,可以安全地关闭连接。

套接字地址结构体

套接字地址结构体

套接字编程的值-结果参数

长度参数以可变引用形式传递。

  1. 长度参数传入时:表示该结构体大小(size)。
  2. 长度参数传出时:表示该结构体的实际大小,告诉进程该结构真正存储的信息的大小。

套接字编程最一般模式

套接字编程最一般模式

smoltcp 模块

  • Socket<'a>: 常见的 Socket 类型,有 TCP 和 UDP 等等
  • SocketHandle: socket 的句柄,socket 的唯一标识符
  • SocketStorage: 存储单个 socket 的容器, 可为空
  • SocketBuffer: 用于存储收发数据的缓冲区
  • SocketSet: socket 的集合管理器
  • Interface: 网络接口的主要抽象

I/O 设备

本质

对操作系统来讲就是一组寄存器

分类

  • 字符设备
  • 块设备

I/O设备的抽象接口

  • read
  • write
  • ioctl

何为驱动程序

把系统调用翻译成对寄存器的操作

文件系统

本质:对物理设备的虚拟化,文件(虚拟磁盘) = 一个可以读写的动态字节序列,并且可以改变大小(vec)

目录树的拼接:运行任意目录挂载目录树

符号链接:可以让文件的树,变成图,甚至通过设定特定的符号链接,创造出一个有意思的迷宫

其他

  1. batch一般指批处理, 而job一般指作业
  2. fd 通常是文件描述符(File Descriptor)的缩写