操作系统-2024春季-清华大学计算机系

主讲教师:向勇 陈渝 李国良 任炬
助教饶淙元、田凯夫、闭浩扬、王拓为、苏明贤、黄旺、郝子胥
上课时间星期一 上午第二大节 09:50-12:15 (1-16周)
上课地点六教6A209(任)六教6A211(李)六教6A213(陈)
课程大纲课程&实验大纲
课程幻灯片Github仓库 Gitee备份仓库 课程幻灯片在线阅读
课程答疑QA仓库 助教的实验讲解视频
课后习题rCore Tutorial Book v3各章“练习”和“练习参考答案”
rCore实验指导书rCore-Tutorial-Guide文档源码 rCore-Tutorial-Guide文档在线阅读
lab1(ch3) OS框架API在线文档 lab2(ch4) OS框架API在线文档
rCore代码框架Github仓库 git.tsinghua仓库
rCore用户态测例Github仓库 git.tsinghua仓库
uCore实验指导书Github仓库 uCore-Tutorial-Guide文档在线阅读
uCore代码框架Github仓库 git.tsinghua仓库
uCore用户态测例Github仓库 git.tsinghua仓库
课程参考书OS:Operating Systems: Three Easy Pieces
Computer System:深入了解计算机系统
RISC-V CPU:RISC-V Reader中文版
OS实践rCore Tutorial Book v3
Github仓库 Gitee备份仓库
各章OS的API 在线文档 可显示内核动态执行信息的内核分支


关于本mdbook

forked from LearningOS/os-lectures

本 mdbook 整合了课堂幻灯片的主要内容,对幻灯片中的一些概念进行了简单补充。 更多内容请参考 rCore Tutorial Book 和 rCore-Tutorial-Guide。

感谢清华大学计算机系提供的宝贵课程资源。

补充大多来自 ChatGPT。ChatGPT 可能会出错。考虑检查重要信息。以清华教程文档为准。

清华大学计算机系操作系统课程

向勇 陈渝 李国良 任炬

2023 春季

课程幻灯片列表 -- 课程总体简介 -- 大实验简介


第一讲 操作系统概述


第二讲 实践与实验介绍


第三讲 基于特权级的隔离与批处理


第四讲 多道程序与分时多任务


第五讲 地址空间-物理内存管理


第六讲 地址空间-虚拟存储管理


第七讲 进程管理与单处理器调度


第八讲 多处理器调度


第九讲 文件系统


第十讲 线程与协程


第十一讲 进程间通信


第十二讲 同步互斥


第十三讲 设备管理

教学日历(教学大纲)(48学时)

大纲修定历史:

  • v1
  • v2:2020年10月21日提交给系里的版本;
  • v3: 21 Oct 2020
  • v4: cooperate with 2021-spring labs
  • v5: 2021年秋季学期结束时更新教学内容部分
  • v6: 2022 spring
  • v7: 2022 autumn
  • v8: 2023 spring
  • v9: 2023 autumn

课程组织与基本目的

操作系统是计算机系统中负责管理各种软硬件资源的核心系统软件,掌握操作系统的基本原理及其设计实现技术是研究型大学计算机专业本科毕业生的基本要求。本课程是计算机专业核心课,从系统程序员的视角进行内容组织,强调实践教学以及对计算机软硬件系统的深入理解,教学操作系统以基于C语言的uCore Tutorial kernel、基于Rust语言的rCore Tutorial kernel和RISC-V处理器为实验环境,讲授操作系统的核心概念、基本原理、关键技术和设计实现方法,并介绍当前操作系统领域的研究热点、相关论文和开源项目,帮助学生了解和掌握大型复杂系统软件的分析方法和核心设计思路,并为学生充分利用操作系统功能进行应用软件研究和开发打下扎实的基础。

教学理念(原则/目标)

让同学重视实践和原理的结合,围绕OS的视角,结合编译/组原等,深入地理解计算机软硬件系统。

建议与要求(2023春)

  • 完善 第六讲:增加第四节 实践:虚存管理优化机制
  • 完善 第七讲和第八讲:增加内容:实践:调度算法的设计实现,COW
  • 完善 第九讲:增加内容:按需加载文件(Demand Paging)
  • 完善 第十三讲:改进第一/三节, 第二节选讲
  • 第十讲 线程与协程 <--> 第十一讲 进程间通信
  • 表明哪些是必讲内容,其它的就是选讲内容(即考试不涉及)
  • 提供rCore Tutorial Book v3中的一般问题的答案
  • 改进实验代码注释和文档
  • 改进基本实验:第4章(lab2),直接提供sbrk系统调用的实现,这样简化了mmap/munmap的实现
  • 增加OS实验陷阱系列的文档/视频
  • 在实验中添加基于rCore Tutorial v3 ch9的应用程序编写题目
  • 添加实验中的可选题目

评分

不参加大实验:

  • 期中: 15%, 期末: 50%,实验必做题目:35%
  • 加分项:做实验中可选题目(只选做一道题即可,多做不算):加0~7分,总分能不超过100

参加大实验:

  • 实验必做题目(5周完成):35%, 大实验:65% (代替期中/期末考试)

教学内容

下面的每一讲大约是三个课时左右

1. 操作系统概述

知识点:操作系统的定义、操作系统结构、Linux的基本使用、Linux C应用编程、rCore Tutorial应用编程

基本内容

  1. 课程概述&教学安排
  2. 什么是操作系统 定义/分类/抽象/特征
  3. 操作系统历史演化
  4. 操作系统结构
  5. 实践:UNIX/Linux/rCore类应用

核心学习成效

  1. 掌握操作系统的定义:能从软件角度、资源管理角度定义操作系统及其所属范围
  2. 掌握操作系统的抽象:理解进程、文件、地址空间的抽象对象以及与硬件关系
  3. 掌握操作系统的特征:理解并发/并行、共享、虚拟化、异步的含义
  4. 掌握操作系统的历史:理解单用户系统、批处理系统、多道程序系统、分时系统、个人计算机、SMP系统、VMM系统、分布式系统中的操作系统特征
  5. 掌握操作系统的架构:理解简单单体/分层单体/复杂单体/微/外/VMM kernel的特点
  6. 掌握 计算机硬件与操作系统的关系:理解硬件与操作系统之间的interface和各自的主要工作
  7. 掌握 应用与操作系统的关系:理解应用与操作系统之间的interface和各自的主要工作

能力检测方法

  1. 能分析硬件的哪些功能能够给操作系统提供哪些支持
  2. 能对一个计算机系统中的软硬件的特征、区别、关系、运行状态进行清晰判断
  3. 对操作系统有一个完整的总体分析
  4. 能编写/运行基本的UNIX类程序
  5. 能在Linux环境下进行操作和编程
  6. 能够完成 rt-v3::ch0的相关练习

2. 实践与实验介绍

知识点:计算机系统加电启动过程;编译与OS的关系;实验环境

基本内容

  1. 实践与实验简要分析(站在应用/OS的角度理解rt-v3)
    1. 库级支持
    2. 批处理支持
    3. 多道程序与分时多任务
    4. 支持地址空间
    5. 支持文件系统
    6. 支持进程间通信
    7. 并发支持
    8. 管理I/O设备
  2. Compiler与OS
    1. 通用的编译
  3. 硬件启动与软件启动
  4. 实践:裸机程序 -- LibOS
    1. 基本环境搭建
    2. RV基本组成
    3. QEMU模拟器
    4. QEMU启动
    5. 程序内存布局
    6. 编译流程

核心学习成效

  1. 理解操作系统实验设计的基本思路
  2. 理解操作系统提供的实验要达到的基本目标
  3. 理解函数调用的设计与执行细节
  4. 理解编译器与操作系统的共识
  5. 理解硬件与操作系统的共识
  6. 理解计算机加电后硬件和软件的启动过程
  7. 理解程序的执行过程
  8. 了解RV的SBI
  9. 了解当前的OS各部分的联系与交互

能力检测方法

  1. 能够搭建实验环境
  2. 能够Debug OS实例
  3. 能编写裸机程序
  4. 灵活应用目前掌握的操作系统知识解决裸机编程相关问题(功能、性能、安全性、便捷性等)
  5. 能够描述/分析 rt-v3::ch1的LibOS设计思路和实现的成效
  6. 能够完成 rt-v3::ch1的相关练习

3. 基于特权级的隔离与批处理

知识点:计算机的特权级/系统调用/异常、RV的特权级/系统调用/异常、批处理

基本内容

  1. 从 OS 角度看计算机系统
  2. 从 OS 角度看RISC-V
  3. 实践:批处理操作系统
    1. 应用程序设计
    2. 加载应用程序
    3. 特权级切换

核心学习成效

  1. 理解特权级
  2. 理解软件如何面向特权级进行编程
  3. 理解系统调用的概念、设计与实现、执行过程
  4. 理解批处理操作系统的设计与实现
  5. 理解程序/批处理操作系统的执行过程
  6. 了解当前的OS各部分的联系与交互

能力检测方法

  1. 描述基于RV硬件/和软件协调实现系统调用
  2. 分析RV中的异常与系统调用之间的关系
  3. 分析并编程举例如果不遵守特权级约束会产生的后果:异常
  4. 灵活应用目前掌握的操作系统知识解决批处理/特权级相关问题(功能、性能、安全性、便捷性等)
  5. 能够描述/分析 rt-v3::ch2的Batch OS设计思路和实现的成效
  6. 能够完成 rt-v3::ch2的相关练习

4. 多道程序与分时多任务

知识点:中断、上下文、任务/中断上下文、任务/中断上下文切换、任务/中断上下文切换的时机、任务生命周期、任务执行过程

基本内容

  1. 相关背景与基本概念
    1. 多道程序概念
    2. 分时多任务概念
    3. 协作式调度
    4. 抢占式调度
    5. 中断处理
  2. 实践:多道程序与分时多任务操作系统
    1. 上下文切换
    2. 协作式调度
    3. 中断机制
    4. 抢占式调度

核心学习成效

  1. 从开发者/应用/操作系统的角度来理解:什么是任务(进程的初级阶段)
  2. 理解任务生命周期的状态转换关系和时机
  3. 理解任务切换的概念与任务切换的设计实现之间的关系和差异
  4. 理解中断和异常的区别和关系
  5. 了解当前的OS各部分的联系与交互

能力检测方法

  1. 灵活应用目前掌握的操作系统知识解决多道程序/分时多任务/任务切换相关问题(功能、性能、安全性、便捷性等)
  2. 能够描述/分析 rt-v3::ch3的Multiprog OS和TimeSharing OS设计思路和实现的成效
  3. 能够完成 rt-v3::ch3的相关练习
  4. 能够完成实验一

5. 地址空间-物理内存管理

知识点:内存管理、连续物理内存分配、非连续物理内存分配

基本内容

  1. 地址空间
  2. 计算机体系结构和内存层次
  3. 内存分配
    1. 静态内存分配
    2. 动态内存分配
    3. 连续内存分配
    4. 非连续内存分配
      1. 段式存储管理
      2. 页式存储管理
      3. 段页式存储管理
  4. 实践:建立地址空间的操作系统
    1. 管理sv39多级页表
    2. 内核与应用的地址空间
    3. 基于地址空间的分时多任务
  5. 页表自映射(option)

核心学习成效

  1. 理解地址空间的概念
  2. 理解静态内存分配与动态内存分配的区别与关系
  3. 理解连续内存分配与不连续内存分配的区别与关系
  4. 理解段式存储管理的基本设计管理
  5. 理解页式存储管理的基本设计、多级页表、反置页表
  6. 基于RV,理解软硬件协同实现页式存储
  7. 了解当前的OS各部分的联系与交互

能力检测方法

  1. 能描述虚拟内存、地址空间在概念和设计实现上的联系和差异
  2. 能从不同角度(应用、内核、CPU)理解地址空间
  3. 掌握不同的内存分配策略,并灵活应用
  4. 能灵活掌握基于某种硬件页机制,设计页表,并掌握虚拟地址到物理地址的转换过程
  5. 能够描述/分析 rt-v3::ch4的Address OS设计思路和实现的成效
  6. 能够完成 rt-v3::ch4的页表/物理内存管理相关练习
  7. 能够完成实验二

6. 地址空间-虚拟存储管理

知识点:局部性原理、虚拟存储基本概念、Page Fault异常、局部页面置换算法、全局页面置换算法、Belady异常

基本内容

  1. 虚拟存储的基本概念
    1. 局部性原理
    2. 覆盖和交换
    3. 虚拟页式存储
    4. RISC-V 缺页异常
  2. 页面置换算法的概念
  3. 局部页面置换算法
    1. 最优算法、先进先出算法和最近最久未使用算法
    2. 时钟置换算法和最不常用算法
    3. Belady现象和局部置换算法比较
  4. 全局页面置换算法
    1. 工作集置换算法
    2. 缺页率置换算法
  5. 实践:基于虚存的优化
    1. 再理解Page Fault异常
    2. 惰性页面分配(Lazy page allocation)
    3. 相同页面共享(Same page sharing)
    4. 页面置换算法在OS中的具体实现

核心学习成效

  1. 理解虚拟存储的基本概念和目标
  2. 掌握如何结合Page Fault异常来设计实现虚拟存储
  3. 掌握各种局部页面置换算法和全局页面置换算法
  4. 理解Belady异常,以及Belady异常与页面置换算法的关系

能力检测方法

  1. 掌握局部性原理
  2. 采用覆盖技术管理应用在有限内存上的内存配置
  3. 采用交互技术管理应用换入换出
  4. 理解虚拟存储在RV上如何实现,以及虚拟存储部分涉及到的OS基本运行过程
  5. 能够使用局部页面置换算法和全局页面置换算法来分析缺页次数等
  6. 掌握Page Fault异常出现后的软硬件协同处理过程
  7. 了解基于虚存优化的各种机制
  8. 灵活应用目前掌握的操作系统知识解决虚拟存储相关问题(功能、性能、安全性、便捷性等)
  9. 能够完成 rt-v3::ch4的Page Fault/虚拟内存管理相关练习

7. 进程管理与单处理器调度

知识点:进程概念、进程运行状态、进程的管理、基本调度策略/算法

基本内容

  1. 进程管理
  2. 单处理机调度
    1. 处理机调度概念
    2. 调度准则
    3. 调度算法
  3. 实时调度
    1. 实时任务
    2. 实时调度算法
    3. 优先级反置问题与解决方法
  4. 实践:进程管理
    1. 应用示例
    2. 进程的数据结构
    3. 进程管理机制的实现
    4. 介绍Copy-on-Write技术

核心学习成效

  1. 理解进程管理的核心系统调用 fork, exec, exit, waitpid
  2. 理解进程管理的核心数据结构和大致处理过程
  3. 理解处理器调度各种调度算法
  4. 理解实时调度算法
  5. 理解优先级反转问题和相应的解决方法
  6. 了解当前的OS各部分的联系与交互
  7. 掌握Copy-on-Write技术

能力检测方法

  1. 掌握进程管理的核心系统调用 fork, exec, waitpid, wait
  2. 能运用调度算法来分配处理器时间
  3. 灵活应用目前掌握的操作系统知识解决进程管理相关问题(功能、性能、安全性、便捷性等)
  4. 能够描述/分析 rt-v3::ch5的Process OS设计思路和实现的成效
  5. 能够完成rt-v3::ch5的进程管理相关练习
  6. 能够完成实验三

8. 多处理器调度

知识点:多处理器调度策略/算法

基本内容

  1. 对称多处理与多核架构
  2. 多处理器调度策略
  3. 多处理器调度机制
  4. O(1) 调度(option)
  5. CFS调度(option)
  6. BFS调度算法(option)
  7. 实践:单处理器调度的实现
    1. 应用示例
    2. 调度相关数据结构
    3. 调度机制的具体实现

核心学习成效

  1. 理解对称多处理与多核架构
  2. 理解操作系统如何管理对称多处理与多核架构
  3. 掌握多处理器调度与其中的负载迁移技术
  4. 能够完成rt-v3::ch5的进程管理相关练习
  5. 能够完成实验三

能力检测方法

  1. 理解多核架构对软件的影响(好的方面,不好的方面)
  2. 能运用多核调度算法来分配处理器时间
  3. 了解RV硬件的多核启动过程和软件初始化过程

9. 文件系统

知识点:文件系统基本概念、文件组织、文件系统设计与实现

基本内容

  1. 文件系统概述
  2. 文件系统接口
  3. 文件系统实现(涉及 VFS,按需加载(Demand loading)等)
  4. 实验:文件系统
    1. easyfs简介
    2. 块设备接口
    3. 块缓冲层
    4. 磁盘布局与管理
    5. 索引节点
    6. 在内核中接入easy-fs
  5. 实例:进程文件系统 procfs(option)

核心学习成效

  1. 从不同的角度(应用、操作系统)理解文件系统
  2. 文件系统的核心系统调用
  3. 典型文件系统的设计实现
  4. 了解文件系统与OS其它部分的联系与交互
  5. 理解按需加载(Demand loading)的虚存与文件系统结合的优化技术

能力检测方法

  1. 掌握文件系统的基本概念
  2. 掌握文件系统的内部机制和组成
  3. 能设计简单的文件系统
  4. 能够描述应用发出文件系统相关系统调用后OS的处理过程
  5. 灵活应用目前掌握的操作系统知识解决文件系统相关问题(功能、性能、安全性、便捷性等)
  6. 能够描述/分析 rt-v3::ch6的Filesystem OS设计思路和实现的成效
  7. 能够完成rt-v3::ch6的相关练习
  8. 能够完成实验四

10. 线程与协程

知识点:线程、协程

基本内容

  1. 线程与协程
    1. 线程
      1. 用户线程、内核线程
    2. 协程
    3. 协程、线程与进程的关系
  2. 实践:线程实现

核心学习成效

  1. 理解进程、线程,协程的概念
  2. 理解进程、线程,协程的区别与联系

能力检测方法

  1. 能够在用户态和内核态设计并实现线程管理
  2. 灵活应用目前掌握的操作系统知识解决线程管理相关问题(功能、性能、安全性、便捷性等)
  3. 能够完成rt-v3::ch8的相关练习

11. 进程间通信

知识点: 信号、管道、消息队列、共享内存、I/O重定向

基本内容

  1. 进程通信概念
  2. 信号
  3. 管道
  4. 消息队列
  5. 共享内存
  6. 实验:进程间通信与I/O重定向
    1. 标准I/O
    2. 管道
    3. 命令行参数与I/O重定向
  7. D-Bus机制(option)
  8. Binder机制(option)

核心学习成效

  1. 理解进程--进程,进程--内核的通信机制和特点
  2. 理解不同IPC机制的特征和大致实现
  3. 掌握I/O重定向
  4. 了解IPC与OS其它部分的联系与交互

能力检测方法

  1. 能描述不同IPC机制的大致实现
  2. 能设计管道机制
  3. 能描述I/O重定向的设计实现思路和运行过程
  4. 灵活应用目前掌握的操作系统知识解决IPC相关问题(功能、性能、安全性、便捷性等)
  5. 能够描述/分析 rt-v3::ch7的IPC OS设计思路和实现的成效
  6. 能够完成rt-v3::ch7的相关练习

12. 同步互斥

知识点:基于软件的同步互斥方法、基于硬件的同步互斥方法、信号量、管程、基于信号量/管程的经典同步问题、死锁、死锁的解决方法、优先级反置与解决方法

基本内容

  1. 同步互斥基本概念
    1. 现实生活中的同步互斥问题
    2. 临界区
    3. 禁用硬件中断
    4. 基于软件的解决方法
  2. 更高级的抽象方法
    1. 信号量
    2. 管程(option)、条件变量
  3. 同步互斥问题
    1. 哲学家就餐问题(信号量/管程)
    2. 读者-写者问题(信号量/管程)
  4. 死锁
    1. 死锁概念
      1. 死锁的必要条件
    2. 死锁处理方法
    3. 银行家算法
    4. 死锁检测
  5. 实践:并发与同步互斥
    1. 锁机制实现
    2. 信号量机制
    3. 条件变量机制
  6. 并发错误检测(option)
  7. 同步互斥-RCU(option)

核心学习成效

  1. 理解同步互斥的基本概念
  2. 掌握各种同步互斥的操作手段
  3. 解决同步互斥问题
  4. 理解死锁概念
  5. 掌握各种死锁处理方法
  6. 理解各种死锁处理方法的局限性
  7. 了解当前的OS各部分的联系与交互

能力检测方法

  1. 能够采用不同的同步互斥手段(基于软件的方法、信号量、条件变量、禁用中断等)灵活解决各种同步互斥问题
  2. 能应用银行家算法
  3. 掌握如何进行死锁检测
  4. 灵活应用目前掌握的操作系统知识解决同步互斥/死锁相关问题(功能、性能、安全性、便捷性等)
  5. 能够描述/分析 rt-v3::ch8的SyncMutex OS设计思路和实现的成效
  6. 能够完成rt-v3::ch8的相关练习
  7. 能够完成实验五

13. I/O设备管理

知识点:I/O抽象、I/O特征、DMA/Interrupt/轮询机制、同步I/O、异步I/O

基本内容

  1. 设备概述
  2. 设备抽象
  3. 设备执行模型
  4. 实验:I/O设备管理
    1. 外设平台(option)
    2. 串口驱动(option)
    3. virtio设备驱动(option)

核心学习成效

  1. 理解设备的分类和特点
  2. 理解设备的各种抽象形式
  3. 理解设备执行模型(同步,异步....)
  4. 掌握设备驱动的设计与实现
  5. 掌握OS与外设的交互过程
  6. 了解当前的OS各部分的联系与交互

能力检测方法

  1. 能描述OS与外设的交互过程
  2. 能编写应用程序来与设备进行交互
  3. 能编写简单的设备驱动程序(如uart等)
  4. 灵活应用目前掌握的操作系统知识解决设备相关问题(功能、性能、安全性、便捷性等)
  5. 能够完成rt-v3::ch9的相关练习

14. 扩展主题(可选项:可靠文件系统,多核优化,高级语言支持,请外校专家讲)

知识点: 基于日志的文件系统设计

基本内容

OPTION1:可靠文件系统主题:

  1. 系统可靠性概述
  2. 基于日志的文件系统
  3. 实际文件习题
    1. FAT文件系统(option)
    2. EXT4文件系统(option)
    3. NTFS(option)
    4. Zettabyte File System (ZFS)(option)
    5. 数据库文件系统(option)

核心学习成效

能力检测方法

教学实验

实验一:操作系统的基本支持

覆盖:1/2/3章

知识点:计算机/OS启动、特权级切换、系统调用、应用程序/库/内核的关系、特权级相关异常、任务切换

用户态

  • 显示目录文件名
    • 实现一个Linux应用程序ls,能显示当前目录中的文件。
  • 显示调用栈
    • 实现一个linux应用程序,能打印出调用栈信息
  • 显示目录信息
  • 睡眠
    • 实现一个rcore应用程序,用sleep系统调用睡眠一段时间(in rcore tutorial v3 Branch 2022spring)

内核态

  • 显示调用栈
    • 实现裸机程序,能打印调用栈
  • 实现新系统调用get_taskinfo
    • 增加get_taskinfo系统调用,能显示task的id和task name
  • 统计系统调用
    • 实现内核扩展,能够统计应用执行过程中系统调用编号和次数
  • 统计时间
    • 实现内核扩展,能够统计应用执行的完成时间
  • 分析异常
    • 统计执行异常的程序的异常情况说明(主要是各种特权级涉及的异常)
    • 能够打印异常程序的调用栈

实验二:地址空间

覆盖:4章

知识点:地址空间、应用与内核之间在不同地址空间的数据交互/控制交互、内存/地址相关异常(如缺页异常)

提供系统调用sbrk 的实现

用户态

  • linux应用访问内存相关系统调用
    • 使用sbrk,mmap,munmap,mprotect系统调用

内核态

  • 改进系统调用sys_get_time
    • 引入虚存机制后,原来内核的 sys_get_time 函数实现就无效了。请你重写这个函数,恢复其正常功能。
  • 实现新系统调用mmap, munmap
    • 请实现 mmap 和 munmap 系统调用

可选题目

  • 惰性页面分配(Lazy page allocation)
  • 局部页面置换算法:改进的Clock页面置换算法
  • 全局页面置换算法:工作集置换策略
  • 全局页面置换算法:缺页率置换策略

实验三:进程管理与调度

覆盖:5章

知识点:进程管理的关联性和必要性、调度算法

用户态

  • 实现一个与进程管理相关的linux应用程序
    • 能用nice,fork,exec,spawn等系统调用

内核态

  • 实现新系统调用 spawn
  • 实现一个调度算法-如:stride

可选题目

  • 相同页面共享(Same page sharing)fork时的Copy on Write
  • 调度算法:可提升/降低优先级的多级反馈队列
  • 多核支持与多核调度(支持进程迁移和多核模式执行应用程序,但在内核中没有抢占和多核支持)

实验四:文件系统与进程间通信

覆盖:6,7章

知识点:文件系统的实现、进程间通信机制

用户态

  • 实现一个应用程序能体现文件/进程间通信的特点

内核态

  • 扩展文件系统功能
    • 扩大单个文件的大小
      • 支持三重间接inode
    • 扩展mmap系统调用,支持对文件的映射
      • 这样对内存读写,其实就是对文件读写
    • 支持crash一致性(hard)
      • 扩展log fs
  • 实现一个文件相关系统调用相关
    • 实现硬链接相关系统调用
    • 实现创建目录的系统调用
  • 实现一个新的重定向能力
    • 实现共享内存

可选题目

  • 按需加载机制
  • log-easyfs:实现基于日志的文件系统

实验五:同步互斥

覆盖:8章

知识点:线程,同步互斥的机制,解决同步互斥问题,死锁,优先级反转问题。

用户态

  • 实现一个Linux线程应用程序能体现线程属性和线程间同步互斥的特点
    • 使用了pthread_* 系统调用

内核态

  • 扩展内核功能:内核线程
    • 在内核中支持内核线程
  • 在内核线程中支持同步互斥机制
    • 实现内核线程用的mutex, semaphore, cond-var
  • 扩展内核功能:多核支持
    • 扩展rcore-tutorial-v3运行在多核系统中
  • 用银行家算法解决死锁问题
    • 设计某种资源申请导致死锁的例子
    • 设计银行家算法来避免死锁问题
  • 解决优先级反转问题
    • 实现RM实时调度算法
    • 设计优先级反转的实例
    • 实现优先级天花板和优先级继承方法

可选题目

  • 内核的协程支持
  • 基于多核的OS内核线程支持,内核支持抢占,支持多核方式下的同步互斥
  • 提升多核的OS内核性能,实现内核中的并行性能优化(fs中的缓冲区管理并行化, 物理内存分配的并行化)
  • 更通用的内核+应用的死锁检查
  • 支持简易网络协议栈和virtio_net device或e1000网卡驱动(需自学rCore Tutorial Book第九章)

扩展实验 (即大实验,课程设计)

前提条件:5周完成实验1~5并通过助教和老师的评审后,可选择完成扩展实验来代替期中和期末考试。

内容和要求:与全国大学生操作系统比赛要求基本一致:跑Linux应用,模块化OS设计(用OS比赛的测例)。

第十三讲 课前准备建议

(初步建议) 阅读

  1. 《Operating Systems:Three Easy Pieces》第36、37章
  2. 阅读 Lec13 的slides 草稿
  3. 阅读 rCore Tutorial Book v3第9章
  4. 编译运行rCore Tutorial v3的ch9分支的代码,了解串口和块设备外设驱动的执行情况

第十二讲 课前准备建议

(初步建议) 阅读

  1. 深入理解计算机系统(ver.2 +)12.5-7节;《Operating Systems:Three Easy Pieces》第27章的27.3, 27.4节,第28章,第30章,第31章,第32章
  2. 阅读 Lec12 的slides 草稿
  3. 阅读 rCore Tutorial Book v3第8章的“锁机制”、“信号量机制”、“条件变量机制”、”并发中的问题“
  4. 编译运行rCore Tutorial v3的ch8分支的代码,了解多线程同步互斥的执行情况

第十一讲 课前准备建议

(初步建议) 阅读

  1. 深入理解计算机系统(ver.2 +)12.3-4节;《Operating Systems:Three Easy Pieces》第26章, 第27章的27.1, 27.2, 27.5节
  2. 阅读 Lec11 的slides 草稿
  3. 阅读 rCore Tutorial Book v3第8章的“引言”、“用户态的线程管理”、“内核态的线程管理”
  4. 编译运行rCore Tutorial v3的ch8分支的代码,了解用户态管理的线程和内核态管理的线程的执行情况

第十讲 课前准备建议

(初步建议) 阅读

  1. 深入理解计算机系统(ver.2 +)10.1-3节,10.7-8节;《Operating Systems:Three Easy Pieces》5.4节
  2. 阅读 Lec10 的slides 草稿
  3. 阅读 rCore Tutorial Book v3第7章
  4. 编译运行rCore Tutorial v3的ch7分支的代码,了解内核中pipe和signal的执行情况

第九讲 课前准备建议

(初步建议) 阅读

  1. 《Operating Systems:Three Easy Pieces》第39、40、42章
  2. 阅读 Lec9 的slides 草稿
  3. 阅读 rCore Tutorial Book v3第6章
  4. 编译运行rCore Tutorial v3的ch9-log分支的代码,了解内核中文件系统动态管理情况

第八讲 课前准备建议

(初步建议) 阅读

  1. 《Operating Systems:Three Easy Pieces》第10章 多处理器调度(高级)
  2. 阅读 Lec8 的slides 草稿

lec7 (4月2日)课前准备建议

(初步建议)

阅读

  1. 《Operating Systems:Three Easy Pieces》第4~9章
  2. 《Computer Systems A Programmer’s Perspective》的第 八 章:“异常控制流” 中与进程相关的8.2/8.4/8.7节
  3. 阅读 Lec7 的slides 草稿
  4. 阅读 rCore Tutorial Book v3第5章
  5. 编译运行rCore Tutorial v3的ch9-log分支的代码,了解内核中进程动态管理情况

lec6 (3月28日)课前准备建议

(初步建议)

阅读

  1. 《Operating Systems:Three Easy Pieces》第21~22章
  2. 《Computer Systems A Programmer’s Perspective》的第六章“存储器层次结构”、第七章“链接”、第九章"Virtual Memory"
  3. 阅读 Lec6 的slides 草稿
  4. 阅读 rCore Tutorial Book 第4章

lec5 (3月21日)课前准备建议

(初步建议)

阅读

  1. 《Operating Systems:Three Easy Pieces》第13~20章
  2. 《Computer Systems A Programmer’s Perspective》的第六章“存储器层次结构”、第七章“链接”、第九章"Virtual Memory"
  3. 《RISC-V-Reader》第十章 10.6节 基于页面的虚拟内存
  4. 阅读 Lec5 的slides 草稿
  5. 阅读 rCore Tutorial Book 第4章
  6. 编译运行并分析 https://github.com/LearningOS/rCore-Tutorial-v3 中 Branch ch4

lec4 (3月14日)课前准备建议

(初步建议)

  1. 分析并运行 在用户态打印”hello world”的小例子 和 在用户态执行特权指令的小例子

  2. 阅读 Lec3 中没讲完的部分slids

  3. 阅读 Lec4 的slides 草稿

  4. 阅读 《深入理解计算系统》”即CSAPP“的第五章“优化程序性能”

  5. 阅读 https://rcore-os.github.io/rCore-Tutorial-Book-v3/ 第三章

  6. 编译运行并分析 https://github.com/LearningOS/rCore-Tutorial-v3 中 Branch ch3-coop 的multiprog-os 和 ch3 的 timesharing-os

lec3 (3月7日)课前准备建议

(初步建议)

  1. 配置好OS实验环境

  2. 阅读放在课程文件栏--参考资料 RISC-V-Reader-Chinese-v2p1-中文 书的第十章 RV32/64 特权架构

  3. 阅读 《深入理解计算系统》”即CSAPP“的第四章“处理器体系结构”、第八章“异常控制流”

(进一步建议)

  1. 阅读 https://rcore-os.github.io/rCore-Tutorial-Book-v3/ 第二章

  2. 编译运行并分析 https://github.com/LearningOS/rCore-Tutorial-v3 中 Branch ch2 的 batch os

  3. 阅读 https://learningos.github.io/os-lectures/#3 (第三次课的slides 草稿)

lec2 (2月28日)课前准备建议

(初步建议)

  1. 配置好OS实验环境

  2. 阅读放在课程文件栏--参考资料 RISC-V-Reader-Chinese-v2p1-中文 书的第十章 RV32/64 特权架构

  3. 阅读 《深入理解计算系统》”即CSAPP“的第二章“信息的表示和处理”、第三章“程序的机器级表示”、第七章“链接”

(进一步建议)

  1. 阅读 https://rcore-os.github.io/rCore-Tutorial-Book-v3/ 第一章

  2. 编译运行并分析 https://github.com/LearningOS/rCore-Tutorial-v3 中 Branch ch1 的libos

  3. 阅读 https://learningos.github.io/os-lectures/#2 (第二次课的slides 草稿)

lec1 (2月21日)课前准备建议

(初步建议)

  1. 配置好OS实验环境

  2. 阅读 《深入理解计算系统》”即CSAPP“的第一章“计算机系统漫游”

(进一步建议)

  1. 阅读 https://rcore-os.github.io/rCore-Tutorial-Book-v3/ 第零章

  2. 编译运行并分析 https://github.com/LearningOS/rCore-Tutorial-v3 中的应用程序

  3. 阅读 https://learningos.github.io/os-lectures/#1 (第一次课的slides 草稿)

第一讲 操作系统概述

第一节 课程概述 & 教学安排

向勇 陈渝 李国良 任炬

2024年春季

课程幻灯片列表


课程信息

主讲教师

  • 向勇 陈渝 李国良 任炬

助教

  • 饶淙元、田凯夫、闭浩扬、王拓为、苏明贤、黄旺、郝子胥

上课信息

上课时间地点

  • 时间
    • 星期一 第2大节 上午09:50-12:15 (1-16周)
  • 地点
    • 六教6A209(任)六教6A211(李)六教6A213(陈)

预备知识

程序设计语言(汇编、C 或 Rust)

  • :(: 不是开发应用程序
  • :): 而是开发系统程序

数据结构

  • :): 理解基本数据结构即可

预备知识

计算机组成原理

  • :): :): :): 刘总/康总/陆总的 RISC-V 原理
  • :): Patterson 的 RISC-V 原理

编译原理

  • :): 没学过影响不大
  • :): 但还是要了解高级语言 <–>RISC-V 汇编语言

课程参考

课程实践:rCore Tutorial Book v3


实验指导

uCore Tutorial


实验指导


bg right:57% 100%

OS 原理与设计思想

  • 操作系统结构
  • 中断及系统调用
  • 内存管理
  • 进程管理
  • 处理机调度
  • 同步互斥
  • 文件系统
  • I/O 子系统

作业与实验

  • 平时作业

    • 课后练习:包括问答题和编程题目,要求在deadline前提交
  • 基础实验

    • (必做):面向 RISC-V CPU 用 Rust/C 设计实现操作系统的功能
    • (选做):难度和工作量比必做题目大的其它题目
  • 课程设计(大实验)

    • 用组件来组合操作系统内核
    • 参加全国大学生OS比赛

基础实验

  • 实验一:操作系统的基本支持

  • 实验二:地址空间

  • 实验三:进程管理与调度

  • 实验四:文件系统与进程间通信

  • 实验五:同步互斥

注意:每次实验的必做题和选做题的提交截止时间相同。


课程设计(大实验)

  • 用组件来组合操作系统内核

  • 操作系统功能和扩展

    • 内核模块的完善和改进
    • 内核可加载模块、微内核、在内核中引入异步编程
    • RISC-V、x86-64、MIPS、ARM支持
    • RaspBerry PI、U740、D1等
    • GUI、驱动、文件系统、网络
  • 往届同学的课程设计成果


成绩评定

选择1:(总分不超过100)

  • 实验一~五必做题目: 40% ;期中考试 20% ; 期末考试 40%
    • 可选做其他实验(如果觉得实验一-实验五不能得到40分),但实验总分不超过40分
  • 加分:
    • 平时作业:每做一次平时作业加1分,最高加5分,且课程总评成绩总分不超过100分

选择2

  • 四周内完成实验一~五(2023秋季实验):40%
  • 课程设计(即大实验):60%

注:选择大实验的同学如果后续退出课程设计,需参加考试。

第一讲 操作系统概述

第二节 什么是操作系统

向勇 陈渝 李国良 任炬

2024年春季


操作系统定义

没有公认的精确定义

操作系统是管理硬件资源、控制程序运行、改善人机界面和为应用软件提供支持的一种系统软件。[计算机百科全书]

bg right 100%

承上启下的操作系统


操作系统是一个资源管理程序

  • 应用与硬件之间的中间层
  • 管理各种软硬件资源
  • 访问软硬件资源的服务
  • 解决访问冲突, 确保公平使用

bg right 100%


操作系统是一个控制程序

  • 一个系统软件
  • 执行程序, 给程序提供服务
  • 控制程序执行过程, 防止错误
  • 方便用户使用计算机系统

bg right 100%


操作系统中的软件分类

  • Shell – 命令行接口
  • GUI – 图形用户接口
  • Kernel – 操作系统的内部

bg right 100%


uCore/rCore 教学操作系统内核

w:800


操作系统内核的抽象

w:800


操作系统内核的抽象

w:700


操作系统内核的特征

  • 并发:计算机系统中同时存在多个运行程序
  • 共享:程序间“同时”访问互斥共享各种资源
  • 虚拟:每个程序” 独占” 一台完整的计算机
  • 异步:服务的完成时间不确定,也可能失败

你对操作系统内核的理解

用户/应用对操作系统的需求?


你对操作系统内核的理解

用户/应用对操作系统的需求?

  • 高效 -- 易用 ?
  • 强大的操作系统服务 -- 简单的接口 ?
  • 灵活性 -- 安全性 ?

为什么要学习这门课程

  • 可以了解计算机机壳后面的软硬件运行原理
  • 可以学习软硬件基础架构和原理
  • 深入了解程序运行的机理
  • 可以发现和修复难对付的bug

【总结笔记】

本节一系列内容提供了对操作系统的全面介绍,从定义、作用、内部组件到其在现代计算中的重要性。让我们逐一扩展和解释。

操作系统的定义

操作系统(OS)没有一个公认的精确定义,但普遍被认为是一种系统软件,负责管理计算机的硬件资源,控制程序的运行,改善人机界面,为应用软件提供必要的支持。这个定义描绘了操作系统作为计算机系统中不可或缺的组成部分,是硬件和用户之间沟通的桥梁。

操作系统作为资源管理器

操作系统作为应用软件与硬件之间的中间层,负责管理各种资源,如处理器(CPU)、内存、存储设备和输入/输出设备。它提供了一个访问这些资源的标准化服务层,解决资源访问冲突,确保系统的公平和高效使用。

操作系统作为控制程序

操作系统控制计算机硬件的执行程序,并向运行的程序提供服务。它通过管理程序的执行过程来防止错误和系统崩溃,同时提供用户友好的接口,使人们能够方便地使用计算机系统。

操作系统的软件分类

  • Shell:命令行接口,允许用户通过文本命令与计算机进行交互。
  • GUI(Graphical User Interface):图形用户接口,提供图形化的交互方式,使得用户可以通过图标、窗口和菜单与系统交互。
  • Kernel:操作系统的核心部分,直接与硬件交互,负责资源管理、程序执行控制等核心功能。

教学操作系统内核:uCore/rCore

uCore和rCore是为教学目的设计的操作系统内核,它们展示了操作系统内核的基本架构和功能。这些教学内核帮助学生理解操作系统的基本原理,如进程管理、内存管理和设备I/O。

操作系统内核的抽象和特征

操作系统内核提供了一系列的抽象概念,如进程、线程、虚拟内存等,以简化编程和资源管理。它的主要特征包括并发(多个程序同时运行)、共享(程序间共享资源)、虚拟(为每个程序提供独占的资源感觉)和异步(操作的完成时间不确定)。

用户/应用对操作系统的需求

用户和应用程序对操作系统的需求通常集中在效率、易用性、强大的服务与简单接口、灵活性与安全性之间的平衡。操作系统设计旨在满足这些多样化的需求,提供既强大又易用的计算环境。

学习操作系统的重要性

通过学习操作系统,我们不仅可以了解计算机的软硬件运行原理,还可以深入理解软硬件架构和原理、程序运行的机制,以及如何发现和修复复杂的bug。这门课程为理解计算机科学的深层原理提供了基础,对于想要深入探索计算机科学和技术的人来说是不可或缺的。

第一讲 操作系统概述

第三节 操作系统历史演化

向勇 陈渝 李国良 任炬

2024年春季


单用户系统

单用户系统 (1945-1955)

  • 手动连线/纸带传输进行程序输入
  • 机器成本远大于人力成本
  • 操作系统 = 装载器 + 程序库
  • 问题:昂贵组件的低利用率

bg right 100%


批处理系统

批处理系统 (1955-1965)

  • 磁带/磁盘传输进行程序输入
  • 机器成本大于人力成本
  • 操作系统 = 装载器 + 程序控制器 + 输出处理器
  • 问题:相比以前利用率提高

bg right 90%


批处理系统

批处理系统 (1955-1965)

  • 磁带/磁盘传输进行程序输入
  • 机器成本大于人力成本
  • 操作系统 = 装载器 + 程序控制器 + 输出处理器
  • 问题:相比以前利用率提高

bg right 100%


多道程序系统

多道程序系统 (1955-1980)

  • 多个程序驻留内存
  • 多个程序轮流使用 CPU
  • 操作系统 = 装载器 + 程序调度 + 内存管理 + 输出管理
  • 演变:相比以前利用率提高

bg right 100%


分时系统

分时系统 (1970- 至今)

  • 多个程序驻留内存中
  • 多个程序分时使用 CPU
  • 操作系统 = 装载器 + 程序调度 + 内存管理 + 中断处理 +...
  • 演变:相比以前利用率提高、与外界交互延时缩短

bg right 100%


Multics OS


Multics OS


开放的UNIX(Ken Thompson 与 Dennis Ritchie)

bg 50%


Linux家族

bg 70%


个人电脑

个人电脑 (1981- )

  • 单用户
  • 计算机成本下降使CPU利用率不再是最重要的关注点
  • 重点是用户界面和多媒体功能
  • 操作系统 = 装载器 + 程序调度 + 内存管理 + 中断处理 +...
  • 演变:走向大众,老的服务和功能不存在,越来越多的安全问题

bg right:35% 100%


MacOS家族

bg 55%


MacOS家族

bg 55%


Windows家族

bg 70%


分布式系统

分布式系统 (1990- )

  • 分布式多用户
  • 分布式系统利用率是关注点
  • 重点是网络/存储/计算的效率
  • 操作系统 = 分布式(装载器 + 程序/OS 调度 + 内存管理)
  • 演变:走向大众,走向网络,新的挑战 (不可靠/不确定)

bg right:40% 100%


Android操作系统

  • 跨平台:支持Java应用程序
  • 运行时(runtime):Android虚拟机
  • 应用框架:简化应用程序开发

bg right 80%


AIoT操作系统

AIoT 系统 (2000- )

  • 分布式多设备
  • 分布式系统利用率/可用性是关注点
  • 重点是网络/存储/计算的效率
  • 操作系统 = 分布式(程序/OS 调度 + 内存管理 + 安全/更新)
  • 演变:走向设备,走向网络,新的挑战 (不可靠/大数据)

bg right:40% 100%


【总结笔记】

本节展示了操作系统从早期的单用户系统到现代AIoT操作系统的发展历程。我们将通过不同的时代,一步步解释并扩展这些内容。

单用户系统 (1945-1955)

在计算机技术的早期,计算机由单个用户直接操作,通过手动连线或纸带传输输入程序。这个时期的操作系统非常原始,主要由装载器和程序库组成。这种方式的主要问题是昂贵的计算机组件利用率低下,因为每次只能运行一个任务。

批处理系统 (1955-1965)

为了提高计算机的利用率,批处理系统被引入。这种系统允许将多个作业集合起来,然后一次性处理,使用磁带或磁盘进行程序输入。操作系统在这时期开始具备程序控制器和输出处理器,能够自动地装载、执行和管理作业队列。

多道程序系统 (1955-1980)

多道程序系统允许多个程序同时驻留在内存中,CPU在这些程序间切换,以提高计算机资源的利用率。这种方式引入了程序调度、内存管理和输出管理,是操作系统功能大幅度增加的一个时期。

分时系统 (1970-至今)

分时系统的出现是为了进一步提高计算机资源的利用率,并减少用户与系统交互的延时。在这种系统中,多个用户通过终端与同一台计算机交互,计算机分时处理各个用户的请求。操作系统加入了中断处理功能,使得交互更为实时。

Multics OS

Multics是一种早期的分时操作系统,对后来的操作系统,特别是UNIX有着重要影响。它引入了很多现代操作系统的概念,如分层结构、环形保护等。

UNIX系统

由Ken Thompson与Dennis Ritchie开发的UNIX系统,以其强大的网络功能、多用户支持和强大的命令行接口著称。UNIX系统的设计哲学和架构对后来的许多操作系统产生了深远影响,包括Linux家族和MacOS家族。

Linux家族

Linux是一种免费和开源的UNIX-like操作系统,由Linus Torvalds在1991年首次发布。Linux家族包括众多的发行版,适用于个人电脑、服务器到嵌入式设备。

个人电脑 (1981- )

随着计算机成本的下降,个人电脑成为普及。操作系统开始重视用户界面和多媒体功能,如Windows和MacOS家族,它们提供了图形用户界面和丰富的应用程序支持。

分布式系统 (1990- )

分布式系统由多台计算机组成,它们通过网络连接,共同完成任务。这种系统的操作系统重点在于提高网络、存储和计算效率,如分布式文件系统和计算集群。

Android操作系统

Android是一种基于Linux内核的移动操作系统,它支持跨平台的Java应用程序。它的应用框架为开发者提供了强大的工具和API,简化了移动应用的开发。

AIoT操作系统

随着物联网和人工智能技术的发展,AIoT操作系统应运而生。这些系统连接多种设备,重点在于提高网络、存储和计算的效率,同时关注系统的可用性和安全性。

总结

操作系统的发展历程展示了技术进步如何推动计算模式的演变,从早期的单任务处理到今天的云计算和物联网。每个阶段的转变都是为了提高效率、增加用户交互的响应性和提供新的功能。随着技术的不断发展,未来的操作系统将继续适应新的计算需求和挑战。

第一讲 操作系统概述

第四节 操作系统结构

向勇 陈渝 李国良 任炬

2024年春季


简单结构

MS-DOS:应用和OS混在一起 (1981–1994)

  • 没有拆分为模块
  • 主要用汇编编写
  • 没有安全保护

bg right 100%


单体分层结构

将单体操作系统 (Monolithic OS) 划分为多层 (levels)

  • 每层建立在低层之上
  • 最底层 (layer 0), 是硬件驱动
  • 最高层 (layer N) 是用户界面
  • 每一层仅使用更低一层的功能和服务

bg right 100%


微内核结构(Micro Kernel)

  • 尽可能把内核功能移到用户空间
  • 用户模块间的通信使用消息传递
  • 好处: 灵活/安全...
  • 缺点: 性能
  • LPC: 本地过程调用(Local Procedure Call)
  • HAL: 硬件抽象层(Hardware Abstraction Layer)

bg right:45% 100%


外核结构(Exokernel)

  • 让内核分配物理资源给多个应用程序, 并让每个程序决定如何处理这些资源
  • 程序能链接到操作系统库 (libOS) 实现了操作系统抽象
  • 保护与控制分离
  • Distributed Shared Memory(DSM)

bg right:45% 100%


虚拟机结构

虚拟机管理器将单独的机器接口转换成很多的虚拟机, 每个虚拟机都是一个原始计算机系统的有效副本, 并能完成所有的处理器指令

bg right:45% 100%


虚拟机结构

bg 70%


应用程序运行与 OS 抽象 + 架构的关系

bg 90%


【总结笔记】

本节讲述了操作系统结构的不同类型和它们各自的特点、优势与局限。通过展示从早期的简单结构到现代复杂架构的演进,我们可以理解操作系统设计的多样性和其对软硬件兼容性、性能、安全性及灵活性的影响。

简单结构

MS-DOS是最早期的操作系统之一,以其简单的结构为特点。在MS-DOS中,应用程序和操作系统代码混合在一起,几乎没有模块化的概念。这种结构简化了操作系统的设计,但因缺乏安全和模块化,使得系统维护和更新变得困难。

单体分层结构

单体分层结构通过将操作系统分为多个层次来解决简单结构的一些问题。每一层提供一组功能,仅依赖于其下一层的服务。这种结构增加了操作系统的可维护性和模块化,但可能会引入一些性能开销,因为每个操作都可能需要通过多个层次才能完成。

微内核结构

微内核结构旨在将尽可能多的操作系统功能从内核移至用户空间,通过消息传递机制在用户模块间通信。这种设计提高了系统的灵活性和安全性,因为它减少了内核的复杂性和出错的可能性。然而,频繁的上下文切换和消息传递可能导致性能损失。

外核结构

外核架构尝试解决微内核可能带来的性能问题,通过允许应用程序直接控制物理资源,同时保持对这些资源的基本保护。这种方法提高了灵活性和性能,因为应用可以更直接地管理其所需的资源,但这也增加了编程的复杂性和出错的风险。

虚拟机结构

虚拟机结构使用虚拟机监控器(VMM)将单一物理机的资源虚拟化为多个独立的虚拟机,每个虚拟机都能运行一个完整的操作系统实例。这种结构提供了高度的隔离性和灵活性,允许在同一物理硬件上运行不同操作系统和配置,但可能会引入一定的性能开销。

应用程序运行与OS抽象+架构的关系

不同的操作系统架构提供了不同级别的抽象和服务,这直接影响了应用程序的运行方式和性能。操作系统的抽象层为应用程序提供了一个统一的接口来访问底层硬件资源,如CPU、内存和I/O设备,同时也定义了应用程序与系统资源交互的规则和约束。

总结

随着计算需求的不断演变,操作系统架构也在不断地发展以适应这些变化。从简单的MS-DOS到现代的微内核和虚拟机架构,每种设计都旨在平衡性能、安全性、灵活性和可维护性。了解这些不同的架构及其背后的设计原则,对于理解现代计算环境以及如何最有效地利用操作系统提供的服务至关重要。

第一讲 操作系统概述

第五节 实践:试试UNIX/Linux

向勇 陈渝 李国良 任炬

2024年春季


UNIX/Linux?

  • Linux
    • Ubuntu、Fedora、SuSE、openEuler
    • 麒麟 统信
  • Windows with WSL (Windows Subsystem of Linux)
  • MacOS with UNIX shell

bg right 90%


UNIX/Linux?

  • 开放源码,有很好的文档,设计简洁,使用广泛
  • 如果你了解Linux的内部情况,学习ucore/rcore会有帮助。

bg right 100%


Try UNIX/Linux

  • shell

    • bash 基本的shell环境
    • fish 强调交互性和可用性
    • zsh 带有自动补全、支持插件
    • starship 轻量、迅速、可无限定制
  • program

    • ls, rm,gcc,gdb, vim ...

bg right:40% 100%


UNIX/Linux提供哪些服务?

  • 进程(正在运行的程序)
  • 内存分配
  • 文件内容、文件名、目录
  • 访问控制(安全)
  • 许多其他的:用户、IPC、网络、时间

bg right 100%


UNIX/Linux提供的应用/内核接口?

  • APP -> C lib -> Syscall -> Kernel

  • 用C语言,来自类UNIX OS

          fd = open("out", 1);
          write(fd, "hello\n", 6);
    
  • 看起来像函数调用,其实是系统调用

  • 核心的系统调用数量并不多

bg right:50% 100%


UNIX/Linux提供的应用/内核接口?

系统调用名含义
int fork()创建一个进程,返回子进程的PID。
int exit(int status)终止当前进程;报告状态给执行wait()系统调用的父进程。没有返回。
int wait(int *status)等待子进程退出;退出状态为 *status ;返回子进程的PID。
int kill (int pid)终止进程号为PID的进程。返回0表示成功,或-1表示错误。
int getpid()返回当前进程的PID。

UNIX/Linux提供的应用/内核接口?

系统调用名含义
int sleep(int n)暂停n个时钟周期。
int exec(char *file,char *argv[])用参数加载文件并执行;仅当出错时返回。
char *sbrk(int n)将进程内存增加n个字节。返回新内存的开始地址。
int open(char *file,int flags)打开文件;标志flag表示文件操作的读/写属性;返回一个fd(文件描述符)。
int write(int fd,char *buf,int n)从buf向文件描述符fd写入n个字节;返回n。

UNIX/Linux提供的应用/内核接口?

系统调用名含义
int read(int fd,char *buf,int n)将n个字节读入buf;返回读取的数字;如果文件结束,则为0。
int close(int fd)释放打开的描述符为fd的文件。
int dup(int fd)返回一个新的文件描述符,引用与文件描述符相同的文件。
int pipe(int p[])创建一个管道,将读/写文件描述符放在p[0]和p[1]中。
int chdir(char *dir)更改当前目录。

UNIX/Linux提供的应用/内核接口?

系统调用名含义
int mkdir(char *dir)创建一个新目录。
int mknod(char *file, int, int)创建一个设备文件。
int fstat(int fd,struct stat *st)将文件fd的元信息放入*st
int stat(char *file,struct stat *st)将文件 *file 的元信息放入 *st
int link(char *file1,char *file2)为文件file1创建另一个名称(file2)
int unlink(char *file)删除文件。

UNIX/Linux应用

分析一些非常简单的小程序

进程相关

fork.c exec.c forkexec.c

文件系统相关

list.c open.c echo.c copy.c

进程间通信相关

pipe1.c pipe2.c redirect.c

bg right:55% 100%


UNIX/Linux应用 - open

  • 例如:open.c,创建一个文件

    $ open $ cat output.txt

  • open() 创建一个文件,返回一个文件描述符(File Descriptor,简称FD,或-1表示错误)。

  • FD是正整数,代表一个打开的文件

  • 进一步细节可以参考UNIX/Linux手册,例如执行 "man 2 open"

  • man的第一个参数:1 表示查shell命令;2 表示查系统调用


UNIX/Linux应用 - copy

例如:copy.c,将输入文件内容复制到输出文件中 从输入文件中读取字节内容,将其写入输出文件中

    copy

read()和write()是系统调用 read()/write()第一个参数是"文件描述符"(fd) 传递给内核,告诉它要读/写哪个 "打开的文件"


UNIX/Linux应用 - copy

  • 一个文件描述符对应一个打开的文件

  • 一个进程可以打开许多文件,有许多描述符

  • 缺省情况:

    • 文件描述符0是 "标准输入", 通常是键盘
    • 文件描述符1是 "标准输出",通常是显示器
  • read()第二个参数是指向要读取的缓冲区的指针,缓冲区的大小由第三个参数指定

  • 文件访问模式:open -> read/write -> close


UNIX/Linux应用 - fork

fork()系统调用创建一个进程的副本(子进程)

  • 复制:指令、数据、寄存器、文件描述符、当前目录
  • 形成"父 "和 "子 "进程

bg right:50% 100%


UNIX/Linux应用 - fork

  • 区别:fork()在父进程中返回一个pid,在子进程中返回0。

  • pid(进程ID)是一个整数,内核给每个进程一个不同的pid

  • 因此,fork.c中父子进程的fork()返回值pid不同

  • 父子进程的执行差别就体现在对fork()返回值pid的判别上

    • 0代表子进程,否则代表父进程

bg right:40% 100%


UNIX/Linux应用 - exec

  • 怎样才能在这个进程中运行一个新程序呢?

  • 例如:exec.c,用一个可执行文件代替调用进程。

exec(filename, argument-array) argument-array保存命令行参数;exec传递给main()

exec()用新执行文件取代当前进程

  • 丢弃已有指令和数据内存空间
  • 从文件中加载新执行程序的指令和数据

UNIX/Linux应用 - forkexec

例如:forkexec.c,fork()一个新进程,exec()一个程序。

常见的UNIX APP执行模式

  • fork():创建子进程
  • exec():子进程中执行新程序
  • wait():父进程等待子进程完成
  • exit():进程退出

bg right:45% 100%


UNIX/Linux应用 - redirect

例子:redirect.c,重定向一个命令的输出

  • 缺省情况下,文件描述符为1的文件是屏幕输出
  • open()总是选择值最小的未使用的文件描述符
  • 通过 close(1) + open(...)操作,设定"output.txt"的文件描述符为1
  • exec(...)系统调用保留了文件描述符,执行echo命令后,它的屏幕输出将被重定向到"output.txt"
    redirect
    cat output.txt

分析UNIX/Linux类应用 - pipe2

  • 例子pipe2.c,进程间通信。
  • shell如何使用管道机制 "|"
    ls | grep x

pipe()系统调用创建了两个fd

  • 从fd[1]写入,从fd[0]中读取

bg right:50% 100%


UNIX/Linux应用

一些值得思考的问题:

  • 为什么是这些I/O和进程的抽象?为什么不是其他的东西?
  • 为什么要提供一个文件系统,而不让程序以自己的方式使用磁盘?
  • 为什么read/write文件用FD而不是文件名?
  • 为什么文件是字节流,而不是磁盘块或格式化记录?
  • 为什么不把fork()和exec()结合起来?

UNIX的设计很好用,但也存在其他的设计


分析UNIX/Linux类应用

小结

  • 介绍了UNIX的I/O、文件系统和进程的抽象
  • 这些接口很简洁,只有整数和I/O缓冲区
  • 这些抽象结合得很好,例如,I/O重定向

【总结笔记】

本节介绍了UNIX/Linux系统的基础,提供一个初步的操作实践指南。通过本节内容,学生将了解UNIX/Linux系统的基本概念、如何在不同环境中尝试使用UNIX/Linux,以及UNIX/Linux系统提供的核心服务和应用程序接口。

UNIX/Linux基本介绍

UNIX和Linux代表了一系列基于开放源代码的操作系统,它们以其稳定性、灵活性和强大的网络功能著称。Linux拥有多个发行版,如Ubuntu、Fedora、SuSE、openEuler、麒麟和统信,每个都有其特点和目标用户。此外,Windows用户可以通过Windows Subsystem for Linux (WSL) 在Windows环境中运行Linux,而MacOS用户则可以通过内置的UNIX shell来体验UNIX环境。

尝试UNIX/Linux

Shell环境

  • bash: UNIX和Linux系统中最基本的shell。
  • fish: 以用户交互性和易用性为特点的现代shell。
  • zsh: 提供自动补全功能和丰富的插件支持的强大shell。
  • starship: 一个轻量、快速且高度可定制的shell提示符。

常用程序

  • 基础命令行工具如ls(列出目录内容)、rm(删除文件或目录)、gcc(GNU编译器集合)、gdb(GNU调试器)、vim(文本编辑器)等,是日常操作中不可或缺的工具。

UNIX/Linux提供的服务

UNIX/Linux系统提供的服务覆盖了进程管理、内存分配、文件系统操作、访问控制以及网络和时间管理等方面。这些服务是通过系统调用(Syscall)来实现的,系统调用为用户空间的应用程序提供了访问操作系统内核功能的方式。

应用/内核接口

UNIX/Linux系统的应用程序通过C库(C lib)调用系统调用接口(Syscall)与内核(Kernel)进行交云。例如,使用open系统调用打开文件,write系统调用写入内容。这些看起来像普通函数调用的接口实际上触发了内核级的操作,使得应用程序能够执行诸如创建进程、读写文件等操作。

UNIX/Linux应用示例

通过分析和运行一些基本的UNIX/Linux应用程序,如fork.cexec.copen.ccopy.c等,学生可以深入理解UNIX/Linux系统调用的使用方式和程序之间的交云方式。这些示例程序涵盖了进程管理、文件操作和进程间通信等多个重要概念。

第二讲 实践与实验介绍

第一节 实践与实验简要分析

向勇 陈渝 李国良 任炬

2024年春季

课程幻灯片列表


提纲

1. 原理、实践与实验介绍

  1. 循续渐进的操作系统实验
  2. 实验安排

满足应用逐渐增加的需求

  • LibOS
  • 批处理OS
  • 多道程序与分时多任务OS

逐步体现操作系统的概念抽象

  • 地址空间抽象的OS
  • 进程抽象的OS
  • 文件抽象的OS

逐步体现操作系统的关键能力

  • 可进程间通信的OS
  • 可并发的OS
  • 管理I/O设备的OS

提纲

  1. 原理、实践与实验介绍

2. 循续渐进的操作系统实验

  1. 实验安排

LibOS

  • 远古操作系统雏形
  • 现代简单嵌入式操作系统

相关知识点

  • 以库的形式提供给应用程序的OS
  • 函数调用: 编译器与操作系统的配合
  • 硬件启动和软件启动
  • 编写/调试裸机程序 裸机程序(Bare Metal Program):与操作系统无关的OS类型的程序

bg right:40% 100%


批处理OS

  • 内存只驻留单道程序
  • 支持系统调用

相关知识点

  • 特权级/特权操作
  • RISC-V特权级/特权操作
  • 系统调用/异常
  • 加载&执行&切换应用程序
  • 特权级切换

bg right:50% 100%


多道程序OS

  • 支持多个程序同时驻留内存
  • 支持多个程序依次执行

相关知识点

  • 内存空间划分与管理
  • 协作式调度

bg right:51% 100%


分时多任务OS

  • 支持多个程序轮流执行

相关知识点

  • 中断处理
  • 上下文切换
  • 抢占式调度

bg right:57% 100%


OS的地址空间抽象

  • 支持程序间内存空间隔离

相关知识点

  • 地址空间抽象
  • 静态内存分配
  • 动态内存分配
  • 页式存储管理

bg right:54% 100%


OS的地址空间抽象

  • 超越物理内存的虚拟存储

相关知识点

  • 局部性原理
  • 缺页异常
  • 虚拟页式存储
  • 置换算法 bg right:50% 100%

OS的进程抽象

  • 支持动态创建程序执行

相关知识点

  • 进程抽象
  • 进程管理
  • 调度机制

bg right:57% 100%


OS的进程抽象

  • 支持多处理器并行

相关知识点

  • 多处理器/多核架构
  • 多处理器调度
  • 实际OS调度

bg right:62% 100%


OS的文件抽象

  • 处理数据的便捷持久存储

相关知识点

  • 文件抽象
  • 文件组织结构
  • 文件系统设计与实现

bg right:54% 100%


可进程间通信的OS

  • 进程间可显式/隐式交互信息

相关知识点

  • 信号、管道
  • 消息队列、共享内存
  • I/O重定向

bg right:50% 100%


可并发的OS

  • 提升CPU的利用率

相关知识点

  • 线程:用户/内核线程
  • 协程
  • 进程、线程和协程的关系

bg right:54% 100%


可并发的OS

  • 合理共享资源
  • 同步与互斥

相关知识点

  • 同步互斥的机制
  • 同步互斥解决并发问题
  • 死锁等问题 bg right:57% 100%

管理I/O设备的OS

  • 支持各种外设

相关知识点

  • 设备抽象
  • 设备执行模型
  • 同步/异步 I/O
  • I/O设备管理 bg right:50% 100%

提纲

  1. 原理、实践与实验介绍
  2. 循续渐进的操作系统实验

3. 实验安排


实验一:操作系统的基本支持

覆盖内容

  • LibOS、 批处理OS、 多道程序与分时多任务OS

知识点:特权级和切换

  • 计算机/OS启动
  • 特权级切换、系统调用、特权级相关异常、任务切换
  • 应用程序/库/内核的关系

实验二:地址空间

覆盖内容

  • 地址空间抽象的OS

知识点:页表

  • 地址空间
  • 应用与内核之间在不同地址空间的数据交互/控制交互
  • 内存/地址相关异常(如缺页异常)

实验三:进程管理与调度

覆盖内容

  • 进程抽象的OS

知识点:进程控制块PCB

  • 进程管理
  • 调度算法

实验四:文件系统与进程间通信

覆盖内容

  • 文件抽象的OS、可进程间通信的OS

知识点:文件

  • 文件系统实现
  • 进程间通信机制

实验五:同步互斥

覆盖内容

  • 可并发的OS

知识点

  • 线程
  • 同步互斥的机制、解决同步互斥问题、死锁问题
  • 优先级反转问题

扩展实验(即大实验,课程设计)

4周内完成基础实验1~5后,与老师协商:选择完成扩展实验来代替考试。

早完成基础实验,可早开展扩展实验

  • 扩展组件化操作操作系统的crates/modules/framworks
    • 实现新feature(多核、新外设、新处理器、新功能)的支持
    • 参加全国大学生OS比赛

第二讲 实践与实验介绍

第二节 Compiler与OS

向勇 陈渝 李国良 任炬

2024年春季


提纲

1. 硬件环境

  1. 应用程序执行环境
  2. 操作系统执行环境

开发的硬件环境

bg right:67% 85%


目标硬件环境

bg right:70% 85%


提纲

  1. 硬件环境

2. 应用程序执行环境

  1. 操作系统执行环境

编译器工作

  • 源码-->汇编码 bg right:55% 100%

Assembler(汇编器)工作

  • 汇编码 --> 机器码 bg right:55% 100%

linker(链接器)工作

  • 多个机器码目标文件 --> 单个机器码执行文件 bg right:55% 100%

OS工作

  • 加载/执行/管理机器码执行文件 bg right:50% 100%

提纲

  1. 硬件环境
  2. 应用程序执行环境

3. 操作系统执行环境


编译器/汇编器/链接器工作

  • 源码 ---> 汇编码 ---> 机器码 --->执行程序
  • Bootloader加载OS执行

bg right:50% 100%


可执行文件格式

三元组

  • CPU 架构/厂商/操作系统
rustc --print target-list | grep riscv
riscv32gc-unknown-linux-gnu
...
riscv64gc-unknown-linux-gnu
riscv64imac-unknown-none-elf
  • ELF: Executable and Linkable Format

bg right:50% 100%


链接和执行

bg 70%


函数库

  • 标准库:依赖操作系统
    • Rust: std 标准库
    • C:glibc, musl libc
  • 核心库:与操作系统无关
    • Rust: core 核心库
    • C: Linux/BSD kernel libc bg right:50% 100%

裸机程序

与操作系统无关的OS类型的程序(Bare Metal program, 裸机程序)

// os/src/main.rs
#![no_std]
#![no_main]

mod lang_items;

// os/src/lang_items.rs
use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

ELF文件格式

文件格式

file target/riscv64gc-unknown-none-elf/debug/os
target/riscv64gc-unknown-none-elf/debug/os: ELF 64-bit LSB executable, UCB RISC-V, ......

ELF文件格式 Executable and Linkable Format


ELF文件格式

bg 55%


文件头信息

文件头信息

rust-readobj -h target/riscv64gc-unknown-none-elf/debug/os
   File: target/riscv64gc-unknown-none-elf/debug/os
   Format: elf64-littleriscv
   Arch: riscv64
   AddressSize: 64bit
   ......
   Type: Executable (0x2)
   Machine: EM_RISCV (0xF3)
   Version: 1
   Entry: 0x0
   ......
   }

导出汇编程序

反汇编导出汇编程序

rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
   target/riscv64gc-unknown-none-elf/debug/os:       file format elf64-littleriscv

代码中移除了 main 函数并将项目设置为 #![no_main]

  • 没有一个传统意义上的入口点(即程序首条被执行的指令所在的位置)
  • Rust 编译器会生成一个空程序
  • 这是一个面向操作系统开发的程序

App/OS内存布局

  • .text: 代码段
  • .rodata:已初始化数据段,只读的全局数据(常数或者是常量字符串)
  • .data:可修改的全局数据
  • .bss:未初始化数据段(Block Started by Symbol)
  • 堆 (heap)向高地址增长
  • 栈 (stack)向低地址增长 bg right:50% 120%

【总结笔记】

本节内容围绕编译器、操作系统(OS)以及它们在软件开发和执行过程中的作用。课程内容涵盖硬件环境、应用程序执行环境、以及操作系统执行环境三个主要部分。

应用程序执行环境

  • 编译器:将源代码转换为汇编语言。
  • 汇编器(Assembler):将汇编语言转换为机器语言(机器码)。
  • 链接器(Linker):将多个机器码目标文件链接成一个单独的可执行机器码文件。

操作系统执行环境

  • OS的角色:负责加载、执行、和管理应用程序的机器码执行文件。
  • Bootloader:负责加载操作系统。

可执行文件格式

  • Executable and Linkable Format (ELF):广泛使用的可执行文件格式,包含重要的元信息,如CPU架构、厂商和操作系统等。
  • ELF文件格式:是Linux和许多Unix-like系统中广泛采用的标准文件格式,用于可执行文件、目标代码、共享库和核心转储。

函数库

  • 标准库:依赖操作系统的库,如Rust的std库和C的glibc。
  • 核心库:与操作系统无关的库,如Rust的core库。

裸机程序

  • 与操作系统无关,直接运行在硬件上的程序。通过特定的编程模式(如Rust语言的#![no_std]#![no_main]属性),可以编写不依赖于标准库的裸机程序。
  • 裸机编程:在没有操作系统支撑的环境下进行编程,常见于嵌入式系统和系统启动代码的开发中。

ELF文件格式深入

  • 文件头信息:提供了文件的基本信息,如架构、地址大小、文件类型等。
  • 导出汇编程序:通过反汇编工具查看和导出程序的汇编代码。

App/OS内存布局

  • 内存分区:包括代码段、数据段(已初始化和未初始化)、堆和栈。
  • 内存管理:堆向高地址增长,而栈向低地址增长。
  • 内存布局:对于任何复杂的应用程序或操作系统来说,合理的内存布局是性能和安全的关键。

第二讲 实践与实验介绍

第三节 硬件启动与软件启动

向勇 陈渝 李国良 任炬

2024年春季


提纲

1. RISC-V开发板

  1. QEMU启动参数和流程
  2. x86启动流程

K210开发板

  • 基于RISC-V 64 多核处理器 w:450

哪吒D1开发板

  • 基于RISC-V 64 单核处理器 w:600

HiFive Unmatched 开发板(U740)

  • 基于RISC-V 64 多核处理器 w:1200

提纲

  1. RISC-V开发板

2. QEMU启动参数和流程

  1. x86启动流程

QEMU模拟器

使用软件 qemu-system-riscv64 来模拟一台 64 位 RISC-V 架构的计算机,它包含:

  • 一个 CPU(可调整为多核)
  • 一块物理内存
  • 若干 I/O 外设

QEMU启动参数

qemu-system-riscv64 \
    -machine virt \
    -nographic \
    -bios ../bootloader/rustsbi-qemu.bin \
    -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
  • machine virt 表示将模拟的 64 位 RISC-V 计算机设置为名为 virt 的虚拟计算机
  • 物理内存的默认大小为 128MiB

QEMU启动参数

qemu-system-riscv64 \
    -machine virt \
    -nographic \
    -bios ../bootloader/rustsbi-qemu.bin \
    -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
  • nographic 表示模拟器不需要提供图形界面,而只需要对外输出字符流

QEMU启动参数

qemu-system-riscv64 \
    -machine virt \
    -nographic \
    -bios ../bootloader/rustsbi-qemu.bin \
    -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
  • bios 可以设置 QEMU 模拟器开机时用来初始化的引导加载程序(bootloader)
  • 这里使用预编译好的 rustsbi-qemu.bin

QEMU启动参数

qemu-system-riscv64 \
    -machine virt \
    -nographic \
    -bios ../bootloader/rustsbi-qemu.bin \
    -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
  • device的loader 参数可以在 QEMU 模拟器开机之前将一个宿主机上的文件载入到 QEMU 的物理内存的指定位置中
  • file 和 addr 参数分别可以设置待载入文件的路径以及将文件载入到的 QEMU 物理内存上的物理地址

QEMU启动流程

qemu-system-riscv64 \
    -machine virt \
    -nographic \
    -bios ../bootloader/rustsbi-qemu.bin \
    -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
  • 通常计算机加电之后的启动流程可以分成若干个阶段,每个阶段均由一层软件负责
  • 每一层软件在完成它承担的初始化工作,然后跳转到下一层软件的入口地址,将计算机的控制权移交给了下一层软件。

QEMU启动流程

qemu-system-riscv64 \
    -machine virt \
    -nographic \
    -bios ../bootloader/rustsbi-qemu.bin \
    -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000

QEMU 模拟的启动流程则可以分为三个阶段:

  1. 由固化在 QEMU模拟的计算机内存中的一小段汇编程序初始化并跳转执行bootloader;
  2. 由 bootloader 负责,初始化并加载OS,跳转OS执行;
  3. 由内核执行初始化工作。

提纲

  1. RISC-V开发板
  2. QEMU启动参数和流程

3. x86启动流程


真实计算机(x86)的启动流程

基于x86的PC的启动固件的引导流程,从IBM PC机诞生第一天起,本质上就没有改变过。

  1. Rom Stage:直接在ROM上运行BIOS代码;
  2. Ram Stage:在RAM上运行代码,检测并初始化芯片组、主板等;
  3. Bootloader Stage:在存储设备上找到Bootloader,加载执行Bootloader;
  4. OS Stage:Bootloader初始化外设,在存储设备上找到OS,加载执行OS。

【总结笔记】

本节内容主要围绕RISC-V开发板、QEMU启动参数和流程,以及x86启动流程三个主要部分进行介绍。

RISC-V开发板

本部分介绍了三种基于RISC-V架构的开发板:

  • K210开发板:基于RISC-V 64位多核处理器。
  • 哪吒D1开发板:基于RISC-V 64位单核处理器。
  • HiFive Unmatched 开发板(U740):基于RISC-V 64位多核处理器。

这些开发板提供了实践RISC-V硬件开发的基础,展示了RISC-V架构的多样性和应用潜力。

QEMU启动参数和流程

QEMU是一种常用的系统模拟器,可以模拟多种处理器架构,包括RISC-V。通过QEMU模拟器,可以模拟出一台具有CPU、物理内存和I/O外设的64位RISC-V计算机环境。

主要启动参数:

  • -machine virt:模拟一个名为virt的虚拟计算机,使用64位RISC-V架构。
  • -nographic:QEMU不提供图形界面,仅输出字符流。
  • -bios:指定QEMU模拟器启动时使用的引导加载程序(bootloader),如使用预编译的rustsbi-qemu.bin
  • -device loader:在QEMU开机前将宿主机上的文件(如操作系统镜像)载入到QEMU的物理内存的指定位置。

启动流程:

QEMU的启动流程模拟了真实计算机的启动过程,包括以下阶段:

  1. 初始化阶段:QEMU模拟计算机通过内存中的一小段汇编程序初始化并跳转执行bootloader。
  2. Bootloader阶段:bootloader负责初始化硬件环境,并加载操作系统到内存中,然后跳转到操作系统的入口点执行。
  3. 操作系统阶段:操作系统内核执行初始化工作,然后进入正常的运行状态。

x86启动流程

x86架构计算机的启动流程自IBM PC机诞生之日起本质上未发生大的变化,分为以下阶段:

  1. ROM Stage:在ROM上直接运行BIOS代码,负责硬件的最初级初始化。
  2. RAM Stage:代码转移到RAM中执行,进行芯片组、主板等硬件的检测与初始化。
  3. Bootloader Stage:BIOS在存储设备上寻找bootloader,加载并执行之,为操作系统的加载做准备。
  4. OS Stage:Bootloader初始化外设,并在存储设备上找到操作系统,加载并将控制权交给操作系统。

通过这一系列阶段的介绍,我们可以看到无论是在传统的x86架构还是新兴的RISC-V架构上,计算机的启动过程都涉及到硬件初始化、bootloader的角色、以及操作系统的加载,这些过程对于理解计算机系统的工作原理至关重要。


【关于QEMU】

QEMU是一个流行的开源机器模拟与虚拟化软件,它支持多种硬件架构,包括x86、ARM、RISC-V等。通过QEMU,用户可以在不同的硬件平台上模拟运行操作系统和程序,这在开发、测试以及教学中非常有用。下面我们来详细解释QEMU启动参数,特别是那些用于模拟RISC-V架构的参数。

主要启动参数解释

-machine virt

  • -machine virt参数指定QEMU模拟一个名为virt的虚拟机,这个虚拟机采用的是64位RISC-V架构。virt是一个为虚拟化而设计的简化版硬件平台,它提供了基本的硬件组件,如CPU、内存、以及一些输入输出设备,但没有过多复杂的外设,使得它非常适合用于教学、测试和开发目的。

-nographic

  • -nographic参数告诉QEMU不提供图形界面,所有的输出和输入都通过终端进行。这对于运行没有图形界面或者不需要图形输出的操作系统和程序非常有用,如服务器操作系统、嵌入式系统或是在自动化测试脚本中使用QEMU时。

-bios

  • -bios参数用于指定QEMU在模拟机器启动时使用的引导加载程序(bootloader)。在RISC-V架构中,这个bootloader负责一些基本的硬件初始化工作,并且负责加载并跳转到操作系统的入口。例如,使用-bios ../bootloader/rustsbi-qemu.bin指令,QEMU会加载一个预编译的名为rustsbi-qemu.bin的RISC-V启动引导程序。这个启动引导程序通常包含了为操作系统提供启动所需环境的最小必要代码。

-device loader

  • -device loader参数用于在QEMU启动之前将一个文件从宿主机载入到QEMU模拟的物理内存中的指定位置。这对于加载操作系统镜像到模拟器的内存中非常关键。例如,-device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000告诉QEMU在物理地址0x80200000处加载os.bin文件。这允许操作系统镜像被直接放置到模拟内存中的正确位置,从而可以被bootloader正确地找到并执行。

这些参数合在一起,允许用户配置QEMU模拟出一个相对简单但足够用于运行操作系统的RISC-V虚拟机。通过这种方式,开发者可以在没有物理RISC-V硬件的情况下,开发和测试RISC-V操作系统和应用程序。

第二讲 实践与实验介绍

第四节 实践:裸机程序 -- LibOS

向勇 陈渝 李国良 任炬

2024年春季


提纲

1. 实验目标和思路

  1. 实验要求
  2. 实践步骤
  3. 代码结构
  4. 内存布局
  5. 基于 GDB 验证启动流程
  6. 函数调用
  7. LibOS初始化
  8. SBI调用

LibOS的实验目标

裸机程序(Bare Metal Program ):与操作系统无关的OS类型的程序

  • 建立应用程序的执行环境
    • 让应用与硬件隔离
    • 简化应用访问硬件的难度和复杂性
  • 执行环境(Execution Environment):负责给在其上执行的软件提供相应的功能与资源的多层次软硬件系统

bg right:35% 100%


LibOS历史背景

1949-1951 年,英国 J. Lyons and Co. 公司(连锁餐厅和食品制造集团公司)开创性地引入并使用剑桥大学的 EDSAC 计算机,联合设计实现了 LEO I ‘Lyons Electronic Office’ 软硬件系统

bg right:50% 70%


LibOS历史背景 -- 子程序

  • 参与 EDSAC 项目的 David Wheeler 发明了子程序的概念 – Wheeler Jump
  • 基于便捷有效的子程序概念和子程序调用机制,软件开发人员在EDSAC和后续的LEO计算机上开发了大量的系统子程序库,形成了最早的操作系统原型。

bg right:50% 70%


LibOS总体思路

  • 编译:通过设置编译器支持编译裸机程序
  • 构造:建立裸机程序的栈和SBI(Supervisor Binary Interface)服务请求接口
  • 运行:OS的起始地址和执行环境初始化

bg right:50% 100%


提纲

  1. 实验目标和思路

2. 实验要求

  1. 实践步骤
  2. 代码结构
  3. 内存布局
  4. 基于 GDB 验证启动流程
  5. 函数调用
  6. LibOS初始化
  7. SBI调用

理解LibOS的执行过程

  • 会编写/编译/运行裸机程序
  • 懂基于裸机程序的函数调用
  • 能看懂汇编代码伪代码
  • 能看懂内嵌汇编代码
  • 初步理解SBI调用 bg right:50% 90%

掌握基本概念

  • 会写三叶虫操作系统了!
    • ABI是啥? Application Binary Interface
    • SBI是啥? Supervisor Binary Interface bg right:50% 90%

注:三叶虫Trilobita是寒武纪最有代表性的远古动物


分析执行细节

  • 在机器级层面理解函数
    • 寄存器(registers)
    • 函数调用/返回(call/return)
    • 函数进入/离开(enter/exit)
    • 函数序言/收尾(prologue/epilogue)

bg right:50% 90%


OS不总是软件的最底层

  • 天外有天
  • Why?

bg right:50% 90%


提纲

  1. 实验目标和思路
  2. 实验要求

3. 实践步骤

  1. 代码结构
  2. 内存布局
  3. 基于 GDB 验证启动流程
  4. 函数调用
  5. LibOS初始化
  6. SBI调用

实践步骤

  • 建好开发与实验环境
  • 移出标准库依赖
  • 支持函数调用
  • 基于SBI服务完成输出与关机

理解运行程序的内存空间和栈

bg right:50% 100%


操作步骤

git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
cd rCore-Tutorial-v3
git checkout ch1

cd os
make run

执行结果

[RustSBI output]
Hello, world!
.text [0x80200000, 0x80202000)
.rodata [0x80202000, 0x80203000)
.data [0x80203000, 0x80203000)
boot_stack [0x80203000, 0x80213000)
.bss [0x80213000, 0x80213000)
Panicked at src/main.rs:46 Shutdown machine!

除了显示 Hello, world! 之外还有一些额外的信息,最后关机。


提纲

  1. 实验目标和思路
  2. 实验要求
  3. 实践步骤

4. 代码结构

  1. 内存布局
  2. 基于 GDB 验证启动流程
  3. 函数调用
  4. LibOS初始化
  5. SBI调用

bg right:55% 100%


LibOS代码结构

./os/src
Rust        4 Files   119 Lines
Assembly    1 Files    11 Lines

├── bootloader(内核依赖的运行在 M 特权级的 SBI 实现,本项目中我们使用 RustSBI)
│   ├── rustsbi-k210.bin(可运行在 k210 真实硬件平台上的预编译二进制版本)
│   └── rustsbi-qemu.bin(可运行在 qemu 虚拟机上的预编译二进制版本)

LibOS代码结构

├── os(我们的内核实现放在 os 目录下)
│   ├── Cargo.toml(内核实现的一些配置文件)
│   ├── Makefile
│   └── src(所有内核的源代码放在 os/src 目录下)
│       ├── console.rs(将打印字符的 SBI 接口进一步封装实现更加强大的格式化输出)
│       ├── entry.asm(设置内核执行环境的的一段汇编代码)
│       ├── lang_items.rs(需要我们提供给 Rust 编译器的一些语义项,目前包含内核 panic 时的处理逻辑)
│       ├── linker-qemu.ld(控制内核内存布局的链接脚本以使内核运行在 qemu 虚拟机上)
│       ├── main.rs(内核主函数)
│       └── sbi.rs(调用底层 SBI 实现提供的 SBI 接口)

提纲

  1. 实验目标和思路
  2. 实验要求
  3. 实践步骤
  4. 代码结构

5. 内存布局

  1. 基于 GDB 验证启动流程
  2. 函数调用
  3. LibOS初始化
  4. SBI调用

App/OS内存布局

bg w:1000


bss段

  • bss段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域
  • bss是英文Block Started by Symbol的简称
  • bss段属于静态内存分配 bg right:55% 130%

data段

  • 数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域
  • 数据段属于静态内存分配 bg right:50% 130%

text段

  • 代码段(code segment/text segment)是指存放执行代码的内存区域
  • 这部分区域的大小确定,通常属于只读
  • 在代码段中,也有可能包含一些只读的常数变量 bg right:50% 130%

堆(heap)

  • 堆是用于动态分配的内存段,可动态扩张或缩减
  • 程序调用malloc等函数新分配的内存被动态添加到堆上
  • 调用free等函数释放的内存从堆中被剔除 bg right:50% 130%

栈(stack)

  • 栈又称堆栈,是用户存放程序临时创建的局部变量
  • 函数被调用时,其参数和函数的返回值也会放到栈中
  • 由于栈的先进后出特点,所以栈特别方便用来保存/恢复当前执行状态

bg right:50% 130%


栈(stack)

可以把堆栈看成一个寄存和交换临时数据的内存区

OS编程与应用编程的一个显著区别是,OS编程需要理解栈上的物理内存结构机器级内容(相关寄存器和指令)

bg right:50% 130%


链接时的内存布局定制

# os/src/linker-qemu.ld
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80200000;

SECTIONS
{
    . = BASE_ADDRESS;
    skernel = .;

    stext = .;
    .text : {
      *(.text.entry)

bg right:50% 130%


链接时的内存布局定制

    .bss : {
        *(.bss.stack)
        sbss = .;
        *(.bss .bss.*)
        *(.sbss .sbss.*)

BSS:Block Started by Symbol SBSS:small bss,近数据,即使用短指针(near)寻址的数据

bg right:50% 130%


生成内核二进制镜像

bg w:1020


生成内核二进制镜像

rust-objcopy --strip-all \
target/riscv64gc-unknown-none-elf/release/os \
-O binary target/riscv64gc-unknown-none-elf/release/os.bin

提纲

  1. 实验目标和思路
  2. 实验要求
  3. 实践步骤
  4. 代码结构
  5. 内存布局

6. 基于 GDB 验证启动流程

  1. 函数调用
  2. LibOS初始化
  3. SBI调用

基于 GDB 验证启动流程

qemu-system-riscv64 \
    -machine virt \
    -nographic \
    -bios ../bootloader/rustsbi-qemu.bin \
    -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000 \
    -s -S
riscv64-unknown-elf-gdb \
    -ex 'file target/riscv64gc-unknown-none-elf/release/os' \
    -ex 'set arch riscv:rv64' \
    -ex 'target remote localhost:1234'
[GDB output]
0x0000000000001000 in ?? ()

提纲

  1. 实验目标和思路
  2. 实验要求
  3. 实践步骤
  4. 代码结构
  5. 内存布局
  6. 基于 GDB 验证启动流程

7. 函数调用

  1. LibOS初始化
  2. SBI调用

编译原理对函数调用的支持

bg right:60% 90%


call/return伪指令

伪指令基本指令含义
retjalr x0, x1, 0函数返回
call offsetauipc x6, offset[31:12]; jalr x1, x6, offset[11:0]函数调用

auipc(add upper immediate to pc)被用来构建 PC 相对的地址,使用的是 U 型立即数。 auipc 以低 12 位补 0,高 20 位是 U 型立即数的方式形成 32 位偏移量,然后和 PC 相加,最后把结果保存在寄存器 x1。


函数调用跳转指令

w:1000

伪指令 ret 翻译为 jalr x0, 0(x1),含义为跳转到寄存器 ra(即x1)保存的地址。 快速入门RISC-V汇编的文档


call/return伪指令

伪指令基本指令含义
retjalr x0, x1, 0函数返回
call offsetauipc x6, offset[31:12]; jalr x1, x6, offset[11:0]函数调用

函数调用核心机制:

  • 在函数调用时,通过 call 伪指令保存返回地址并实现跳转;
  • 在函数返回时,通过 ret 伪指令回到跳转之前的下一条指令继续执行

函数调用约定

函数调用约定 (Calling Convention) 约定在某个指令集架构上,某种编程语言的函数调用如何实现。它包括了以下内容:

  • 函数的输入参数和返回值如何传递;
  • 函数调用上下文中调用者/被调用者保存寄存器的划分;
  • 其他的在函数调用流程中对于寄存器的使用方法。

RISC-V函数调用约定:调用参数和返回值传递

w:1200

  • RISC-V32:如果返回值64bit,则用a0~a1来放置
  • RISC-V64:如果返回值64bit,则用a0来放置

RISC-V函数调用约定:栈帧

w:1000


RISC-V函数调用约定:栈帧

栈帧(Stack Frames)

return address *
previous fp
saved registers
local variables
…
return address fp register
previous fp (pointed to *)
saved registers
local variables
… sp register

bg right:50% 180%


RISC-V函数调用约定:栈帧

  • 堆栈帧可能有不同的大小和内容,但总体结构是类似的
  • 每个堆栈帧始于这个函数的返回值前一个函数的fp值
  • sp 寄存器总是指向当前堆栈框架的底部
  • fp 寄存器总是指向当前堆栈框架的顶部

bg right:50% 180%


RISC-V函数调用约定:ret指令

  • 当 ret 指令执行,下面的伪代码实现调整堆栈指针和PC:
pc = return address
sp = sp + ENTRY_SIZE
fp = previous fp

bg right:50% 180%


RISC-V函数调用约定:函数结构

函数结构组成:prologue,body partepilogue

  • Prologue序言的目的是为了保存程序的执行状态(保存返回地址寄存器和堆栈寄存器FP)
  • Epilogue尾声的目的是在执行函数体之后恢复到之前的执行状态(跳转到之前存储的返回地址以及恢复之前保存FP寄存器)。

RISC-V函数调用约定:函数结构

函数结构组成:prologue,body partepilogue

.global sum_then_double
sum_then_double:
	addi sp, sp, -16		# prologue
	sd ra, 0(sp)			
	
	call sum_to                     # body part 
	li t0, 2
	mul a0, a0, t0
	
	ld ra, 0(sp)			# epilogue
	addi sp, sp, 16
	ret

RISC-V函数调用约定:函数结构

函数结构组成:prologue,body partepilogue

.global sum_then_double
sum_then_double:		
	
	call sum_to                     # body part 
	li t0, 2
	mul a0, a0, t0
	
	ret

Q:上述代码的执行与前一页的代码执行相比有何不同?


提纲

  1. 实验目标和思路
  2. 实验要求
  3. 实践步骤
  4. 代码结构
  5. 内存布局
  6. 基于 GDB 验证启动流程
  7. 函数调用

8. LibOS初始化

  1. SBI调用

分配并使用启动栈

分配并使用启动栈 快速入门RISC-V汇编的文档

# os/src/entry.asm
    .section .text.entry
    .globl _start
_start:
    la sp, boot_stack_top
    call rust_main

    .section .bss.stack
    .globl boot_stack
boot_stack:
    .space 4096 * 16
    .globl boot_stack_top
boot_stack_top:

分配并使用启动栈

# os/src/linker-qemu.ld
.bss : {
    *(.bss.stack)
    sbss = .;
    *(.bss .bss.*)
    *(.sbss .sbss.*)
}
ebss = .;

在链接脚本 linker.ld 中 .bss.stack 段最终会被汇集到 .bss 段中 .bss 段一般放置需要被初始化为零的数据


控制权转交:ASM --> Rust/C

将控制权转交给 Rust 代码,该入口点在 main.rs 中的rust_main函数

#![allow(unused)]
fn main() {
// os/src/main.rs  
pub fn rust_main() -> ! {
    loop {}
}
}
  • fn 关键字:函数; pub 关键字:对外可见,公共的
  • loop 关键字:循环

清空bss段

清空bss段(未初始化数据段)

pub fn rust_main() -> ! {
    clear_bss(); //调用清空bss的函数clear_bss()
}
fn clear_bss() {
    extern "C" {
        fn sbss(); //bss段的起始地址
        fn ebss(); //bss段的结束地址
    }
    //对[sbss..ebss]这段内存空间清零
    (sbss as usize..ebss as usize).for_each(|a| {
        unsafe { (a as *mut u8).write_volatile(0) }
    });
}

提纲

  1. 实验目标和思路
  2. 实验要求
  3. 实践步骤
  4. 代码结构
  5. 内存布局
  6. 基于 GDB 验证启动流程
  7. 函数调用
  8. LibOS初始化

9. SBI调用


SBI服务接口

在屏幕上打印 Hello world!

  • SBI服务接口
    • Supervisor Binary Interface
    • 更底层的软件给操作系统提供的服务
  • RustSBI
    • 实现基本的SBI服务
    • 遵循SBI调用约定 bg right:55% 90%

SBI服务编号

#![allow(unused)]
fn main() {
// os/src/sbi.rs
const SBI_SET_TIMER: usize = 0;
const SBI_CONSOLE_PUTCHAR: usize = 1;
const SBI_CONSOLE_GETCHAR: usize = 2;
const SBI_CLEAR_IPI: usize = 3;
const SBI_SEND_IPI: usize = 4;
const SBI_REMOTE_FENCE_I: usize = 5;
const SBI_REMOTE_SFENCE_VMA: usize = 6;
const SBI_REMOTE_SFENCE_VMA_ASID: usize = 7;
const SBI_SHUTDOWN: usize = 8;
}
  • usize 机器字大小的无符号整型

汇编级SBI调用

#![allow(unused)]
fn main() {
// os/src/sbi.rs
#[inline(always)] //总是把函数展开
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
    let mut ret; //可修改的变量ret
    unsafe {
        asm!(//内嵌汇编
            "ecall", //切换到更高特权级的机器指令
            inlateout("x10") arg0 => ret, //SBI参数0&返回值
            in("x11") arg1,  //SBI参数1
            in("x12") arg2,  //SBI参数2
            in("x17") which, //SBI编号
        );
    }
    ret //返回ret值
}
}

SBI调用:输出字符

在屏幕上输出一个字符

#![allow(unused)]
fn main() {
// os/src/sbi.rs
pub fn console_putchar(c: usize) {
    sbi_call(SBI_CONSOLE_PUTCHAR, c, 0, 0);
}
}

实现格式化输出

  • 编写基于 console_putchar 的 println! 宏

SBI调用:关机

#![allow(unused)]
fn main() {
// os/src/sbi.rs
pub fn shutdown() -> ! {
    sbi_call(SBI_SHUTDOWN, 0, 0, 0);
    panic!("It should shutdown!");
}
}
  • panic!println!是一个宏(类似C的宏),!是宏的标志

优雅地处理错误panic

#![allow(unused)]
fn main() {
#[panic_handler]
fn panic(info: &PanicInfo) -> ! { //PnaicInfo是结构类型
    if let Some(location) = info.location() { //出错位置存在否?
        println!(
            "Panicked at {}:{} {}",
            location.file(), //出错的文件名
            location.line(), //出错的文件中的行数
            info.message().unwrap() //出错信息
        );
    } else {
        println!("Panicked: {}", info.message().unwrap());
    }
    shutdown() //关机
}
}

LibOS完整功能

优雅地处理错误panic

#![allow(unused)]
fn main() {
pub fn rust_main() -> ! {
    clear_bss();
    println!("Hello, world!");
    panic!("Shutdown machine!");
}
}

运行结果

[RustSBI output]
Hello, world!
Panicked at src/main.rs:26 Shutdown machine!

小结

  • 构造各种OS的实践中需要掌握的知识点(原理&实现)
  • 理解Compiler/OS/Machine的相互关联关系
  • 知道从机器启动到应用程序打印出字符串的过程
  • 能写Trilobita OS

【总结笔记】

本节深入探讨裸机程序LibOS的实践内容,它是一种与操作系统无关的程序,专门设计来建立一个应用程序的执行环境。这样的环境能有效隔离应用和硬件,简化应用程序访问硬件的复杂性。这一过程不仅涉及到编译器的配置,还包括栈和SBI(Supervisor Binary Interface)服务请求接口的建立,以及操作系统的启动地址和执行环境的初始化。

在探讨LibOS的实践过程中,理解内存布局是关键一环。内存布局包含了多个关键部分,每个部分在程序运行时扮演着特定的角色。

实验目标和思路

LibOS实验的核心目标是建立一个裸机程序,这意味着创建一个与操作系统无关但能在硬件上直接运行的程序。这样的程序可以直接与硬件交互,无需操作系统的干预,因此是理解硬件和软件交互的极佳案例。

LibOS的历史背景

LibOS的概念不是全新的。它可以追溯到1949-1951年间,当时英国的J. Lyons and Co.公司与剑桥大学合作,使用EDSAC计算机开发了LEO I,这是世界上第一台用于商业的计算机系统。在这个过程中,David Wheeler发明了“子程序”的概念,即所谓的Wheeler Jump,这是一种早期的软件复用方法,为操作系统的发展奠定了基础。

实验要求

为了完成LibOS实验,参与者需要具备以下能力:

  • 编写、编译和运行裸机程序;
  • 理解和实现基于裸机程序的函数调用;
  • 能够阅读并理解汇编代码及内嵌汇编代码;
  • 对SBI调用有初步的理解。

实践步骤

实践步骤包括准备开发环境、移除标准库依赖、实现函数调用支持,以及利用SBI服务完成基本的输入输出和关机操作。这个过程不仅要求理论知识,还需要实际的编程技能,特别是在Rust语言和汇编语言方面。

代码结构和内存布局

LibOS的代码结构包含Rust和汇编两部分,主要涵盖了内核主函数、SBI接口的封装、栈的配置和启动入口等。而内存布局则定义了.text、.rodata、.data、.bss段以及堆和栈的安排,这对于理解程序如何在硬件上运行至关重要。

GDB验证启动流程和函数调用

使用GDB(GNU Debugger)验证LibOS的启动流程是确认一切按预期运行的重要步骤。此外,深入理解函数调用的机器级实现—包括寄存器使用、调用约定和栈帧管理—是理解程序执行的关键。

LibOS初始化和SBI调用

LibOS的初始化过程包括分配启动栈、清除bss段,以及将控制权从汇编代码转移给Rust代码。SBI调用则提供了与硬件通信的基础服务,如在控制台上打印字符和系统关机。

内存布局

关键内存段

  1. bss段:用于存储未初始化的全局变量。由于这些变量在程序开始执行前未被明确初始化,它们在物理内存中的值为零。这一部分属于静态内存分配。
  2. data段:存放已初始化的全局变量。不同于bss段,data段中的变量在程序开始执行前已被初始化,因此在物理内存中有具体的值。
  3. text段:也称为代码段,用于存放程序的执行代码。这部分内存通常设为只读,以防止程序运行时修改其自身的指令。
  4. 堆(heap):用于动态内存分配。程序运行时,可以根据需要增加或减少堆空间的大小。
  5. 栈(stack):用于存储局部变量、函数参数和返回地址等。栈具有先进后出(LIFO)的特点,非常适合于管理函数调用过程中的数据。

内存布局定制

在链接阶段,通过特定的链接脚本可以定制程序的内存布局,比如指定各个内存段的起始地址和大小。这一步骤对于嵌入式系统和操作系统开发尤为重要,因为它直接关系到程序能否正确地与硬件交互。

基于GDB验证启动流程

为了确保LibOS按照预期执行,使用GDB进行调试是必不可少的步骤。通过GDB,开发者可以单步执行程序,检查寄存器和内存的状态,从而验证程序的启动流程是否正确。这一过程有助于快速定位和修复可能存在的问题。

函数调用和LibOS初始化

在LibOS的实现中,对函数调用的处理是基本且核心的部分。这涉及到对栈的操作、寄存器的保存和恢复,以及调用约定的遵循等。LibOS的初始化则包括设置启动栈、清空bss段和转交控制权给主函数等步骤。这些都是确保程序能够正常运行的基础环节。

SBI调用

LibOS通过SBI调用与硬件交互。SBI(Supervisor Binary Interface)提供了一套服务接口,使得操作系统可以请求底层的硬件服务,如输出字符到控制台、设置定时器和关机等。这些操作对于一个操作系统来说至关重要,因为它们构成了系统对硬件操作的基础。

总的来说,通过LibOS的开发和实践,参与者不仅能够深入理解操作系统的内部工作原理,还能学习到如何在裸机上编程,以及如何通过底层接口与硬件进行交互。这些知识和技能对于深入掌握计算机科学的基本原理和提高系统编程能力都是极为宝贵的。

第三讲 基于特权级的隔离与批处理

第一节 从OS角度看计算机系统

向勇 陈渝 李国良 任炬

2024年春季


提纲

1. OS与硬件的关系

  1. OS与应用程序的关系
  2. 隔离机制

计算机系统

bg right:55% 100%

计算机系统(computer architecture)是一种抽象层次设计,用于实现可有效使用现有制造技术信息处理应用。 -- cs-152 berkeley


计算机系统抽象层次

硬件 支持 OS 支持 应用

bg right:52% 95%

  • 操作系统位于硬件(HW)和应用(APP)之间
  • 只有理解OS与HW/APP的关系,才能更好掌握OS

指令集:软硬件接口

硬件OS边界 :指令集+寄存器 w:1150


OS是对硬件的虚拟与抽象

bg 55%


RISC-V处理器架构

w:800


u/rCore的框架结构 w:900


提纲

  1. OS与硬件的关系

2. OS与应用程序的关系

  1. 隔离机制

OS对应用程序执行的支持

  • 提供服务

  • 系统调用

  • 地址空间布局


OS为应用程序提供服务

  • 通过系统调用来提供服务
  • 系统调用:OS/APP的接口(边界之一) w:1100

系统调用如何实现?

  • 调用ssize_t read(int fd, void *buf, size_t count);会发生什么?

  • 可以在应用程序中直接调用内核的函数吗?

  • 可以在内核中使用应用程序普通的函数调用吗?


引入系统调用的目的是增强安全性和可靠性

  • 函数调用的特征
    • 好处:执行很快;
    • 好处:灵活-易于传递和返回复杂数据类型;
    • 好处:程序员熟悉的机制,...
    • 坏处:应用程序不可靠,可能有恶意,有崩溃的风险

进程的地址空间

进程的地址空间(memory layout)是界定了OS/APP的边界w:850


OS内核与应用进程的地址空间划分

进程的地址空间(memory layout)是界定了OS/APP的边界w:600

内核页表隔离(KPTI, kernel page-table isolation)

w:600


提纲

  1. OS与硬件的关系
  2. OS与应用程序的关系

3. 隔离机制

  • 为什么隔离
  • 隔离要解决的问题
  • 隔离的方法

隔离要解决的问题

  • 防止程序 X 破坏或监视程序 Y

    • 读/写内存,使用 100%的 CPU,更改文件描述符
  • 防止进程干扰操作系统

  • 防止恶意程序、病毒、木马和 bug

    • 错误的过程可能会试图欺骗硬件或内核

什么是隔离?

  • 隔离的定义

    • 操作系统中的应用程序不会影响(或破坏)其他应用或操作系统的正常执行或信息泄露
  • 隔离的本质

    • 在需要交换信息或共享资源的情况下才出现
  • 隔离并不意味着不要共享


隔离边界

隔离需要建立边界(boundary)

  • 边界决定了各自的势力范围
    • 跨界即有风险的共享资源
  • 强制隔离
    • 避免有问题的单元对整个系统的安全影响
  • 隔离的单位
    • 通常是运行的程序

隔离方法

  • 隔离的方法分类
    • 基于软件的隔离
    • 基于硬件的隔离
    • 基于网络的隔离

OS隔离APP的分类

  • 控制的隔离:特权级机制
    • 用户态 vs 内核态
  • 数据的隔离:地址空间
    • 用户地址空间 vs 内核地址空间
  • 时间的隔离:中断处理
    • 随时打断正在执行的用户态App
  • 对破坏隔离的处理:异常处理
    • OS在内核态及时处理用户态App的异常行为

数据隔离:虚拟内存

  • 虚拟内存
    • 读写内存的安全性问题
    • 进程间的安全问题
    • 内存空间利用率的问题
    • 内存读写的效率问题
  • 地址空间 address spaces
    • 一个程序仅寻址其自己的内存
    • 若无许可,则每个程序无法访问不属于自己的内存

虚拟内存的工作原理

w:800


控制隔离:特权模式

  • CPU 硬件中的特权模式
    • 防止应用程序访问设备和敏感的 CPU 寄存器
      • 地址空间配置寄存器
      • 关机相关的指令或寄存器
      • ......

特权模式

  • CPU 硬件支持不同的特权模式
    • Kernel Mode(内核态) vs User Mode(用户态)
    • 内核态可以执行用户态无法执行的特权操作
      • 访问外设
      • 配置地址空间(虚拟内存)
      • 读/写特殊系统级寄存器
  • OS内核运行在内核态
  • 应用程序运行在用户态
  • 每个微处理器都有类似的用户/内核模式标志

时间隔离:中断/异常机制

  • CPU 硬件支持中断/异常的处理 -异常: 及时响应和处理应用的异常行为 -中断: 打断一直占用 CPU 的应用程序

  • 中断是异步发生,是来自处理器外部的 I/O 设备的信号的结果。

    • 异步是指,硬件中断不是由任何一条专门的 CPU 指令造成。

中断处理例程

  • 中断处理例程(interrupt handle):硬件中断/异常的处理程序
    1. I/O 设备通过向处理器芯片的一个引脚发信号,并将异常号放到系统总线上,以触发中断;
    2. 在当前指令执行完后,处理器从系统总线读取异常号,保存现场,切换到内核态
    3. 调用中断处理例程,当中断处理程序完成后,它将控制返回给下一条本来要执行的指令。

时钟中断

  • Timer 可以稳定定时地产生中断
    • 防止应用程序死占着 CPU 不放
    • 让OS内核能周期性地进行资源管理

中断处理例程

  • 触发中断

  • 保存现场,切换到内核态运行

  • 返回,恢复中断前下一条指令 bg right:62% 100%


异常处理例程

  • 根据异常编号去查询处理程序

  • 保存现场

  • 异常处理:杀死产生异常的程序;或者 重新执行异常指令

  • 恢复现场 bg right:52% 100%


系统调用处理例程

  • 查找系统调用程序

  • 用户态切换到内核态

  • 栈切换,上下文保存

  • 执行内核态

  • 返回用户态

bg right:52% 100%


中断 vs 异常 vs 系统调用

中断异常系统调用
发起者外设、定时器应用程序应用程序
响应方式异步同步同步、异步
触发机制被动触发内部异常、故障自愿请求
处理机制持续,用户透明杀死或重新执行等待和持续

进程切换 vs 函数切换

w:500 w:600


小结

  • 了解计算机硬件与操作系统的关系:接口/边界
  • 了解操作系统与应用程序的关系:接口/边界
  • 了解操作系统如何隔离与限制应用程序

【总结笔记】

从OS角度看计算机系统

OS与硬件的关系

操作系统(OS)作为计算机系统中的一个关键组成部分,扮演着硬件和应用程序之间的中介角色。OS的主要职责包括管理硬件资源和提供应用程序执行的环境。从计算机系统的架构来看,操作系统位于硬件和应用程序之间的抽象层次,通过对硬件的虚拟化和抽象,使得应用程序能够在抽象的、简化的环境中运行,而无需直接与复杂的硬件交互。

OS与应用程序的关系

操作系统为应用程序提供了一个执行的环境,其中包括执行所需的各种服务,如文件操作、进程管理、内存分配等。这些服务通常通过系统调用的形式提供给应用程序,形成了OS与应用程序之间的另一个重要接口。系统调用是应用程序向操作系统请求服务的一种方式,它是一种受控的入口点,使得操作系统能够在提供服务的同时,保持对系统资源的控制和管理,从而确保系统的安全性和稳定性。

隔离机制

操作系统需要在各个应用程序之间以及应用程序与操作系统之间建立有效的隔离,以确保一个应用程序的行为不会影响到其他应用程序的正常运行,同时也保护操作系统免受恶意软件的攻击。隔离机制包括对控制、数据、时间的隔离,以及通过异常处理来对破坏隔离的行为进行处理。这些隔离方法可以基于软件、硬件或网络实现,具体方式取决于隔离的需求和系统设计。

隔离要解决的问题

隔离机制旨在解决的核心问题是如何在允许必要的信息交换和资源共享的同时,防止程序间或程序与操作系统间的不当影响。这包括防止程序读写其他程序的内存、占用过多的CPU资源、更改系统配置等潜在的恶意行为。有效的隔离策略能够增强系统的安全性、稳定性和可靠性。

隔离方法

操作系统实现隔离的方法多种多样,包括:

  • 控制隔离:通过特权级机制实现,区分用户态和内核态,限制应用程序对特权操作的访问。
  • 数据隔离:通过虚拟内存机制实现,为每个进程提供独立的地址空间,防止进程间的数据访问冲突。
  • 时间隔离:通过中断和异常处理机制实现,确保操作系统能够及时响应外设事件和应用程序异常,以及通过时钟中断实现对CPU资源的公平分配。

隔离机制的设计和实现是操作系统安全性和稳定性的关键。通过在需要的时候实现应用程序和操作系统间的严格分离,操作系统能够为所有应用程序提供一个安全、稳定且高效的运行环境。


计算机系统抽象层次
1. Physics(物理): 这是基础层,涉及物理学原理,如量子力学和电磁学,它们是构造计算设备物理组件的基础。
2. Devices(设备): 在物理层之上是设备层,这里的设备指的是实际构成计算机的物理组件,例如晶体管、二极管和电容器等。
3. Circuits(电路): 设备层的上一层是电路层,这里设计将电子设备(例如晶体管)连接在一起形成电路,以实现特定的功能,如逻辑门电路。
4. Gates/Register-Transfer Level (RTL): 在电路层之上是门级/寄存器传输级别,这里涉及逻辑门(如与门、或门和非门)的设计,以及如何将这些门组合成更复杂的电路来执行寄存器之间的数据传输和操作。
5. Microarchitecture(微架构): 微架构层是在逻辑门和电路的基础上构建具体的处理器架构。它定义了处理器内部的数据路径、流水线结构、缓存和执行单元等。
6. Instruction Set Architecture (ISA): 指令集架构是硬件(微架构)和软件之间的接口。它定义了处理器可以识别和执行的指令集合,包括数据类型、寄存器、指令格式、寻址模式等。
7. Operating System/Virtual Machines(操作系统/虚拟机): 操作系统是管理计算机硬件资源和提供用户与应用程序接口的软件层。虚拟机为运行在物理硬件之上的软件提供了一个虚拟的平台。
8. Programming Language(编程语言): 编程语言层提供了开发者用来编写软件的高级语言,这些语言通常更接近人类语言和抽象概念,使得软件开发更加高效。
9. Algorithm(算法): 算法层指的是解决特定问题和执行任务的方法和流程。算法是在编程语言中实现的,但在概念上独立于具体的实现。
10. Application(应用程序): 最顶层是应用程序层,它指的是最终用户直接使用的软件,例如文本编辑器、游戏或者企业软件等。这些应用程序使用底层所有层级提供的功能来执行用户需要的任务。
总的来说,这些层次从物理硬件到用户界面提供了一种分层的方式来理解和构建复杂的计算系统。每个层级抽象了底层的复杂性,提供了建立在其上的下一个层级所需的接口和功能。

第三讲 基于特权级的隔离与批处理

第二节 从OS角度看RISC-V

向勇 陈渝 李国良 任炬

2024年春季


提纲

1. 主流CPU比较

  1. RISC-V系统模式
  2. RISC-V系统编程:用户态编程
  3. RISC-V系统编程:M-Mode编程
  4. RISC-V系统编程:内核编程

本节主要目标

  • 了解 RISC-V 特权级和硬件隔离方式
  • 了解 RISC-V 的 M-Mode 和 S-Mode 的基本特征
  • 了解OS在 M-Mode 和 S-Mode 下如何访问和控制计算机系统
  • 了解不同软件如何在 M-Mode<–>S-Mode<–>U-Mode 之间进行切换

主流CPU比较

w:1150


主流CPU比较

  • 由于兼容性和历史原因,导致x86和ARM的设计实现复杂
  • RISC-V简洁/灵活/可扩展

w:1150


提纲

  1. 主流CPU比较

2. RISC-V系统模式

  1. RISC-V系统编程:用户态编程
  2. RISC-V系统编程:M-Mode编程
  3. RISC-V系统编程:内核编程

2.1 概述

2.2 特权级 2.3 CSR寄存器


RISC-V 系统模式

w:800

  • ABI/SBI/HBI:Application/Supervisor/Hypervisor Bianry Interface
  • AEE/SEE/HEE:Application/Superv/Hyperv Execution Environment
  • HAL:Hardware Abstraction Layer
  • Hypervisor,虚拟机监视器(virtual machine monitor,VMM)
  • RISC-V 系统模式 即 与系统编程相关的RISC-V模式

RISC-V相关术语

  • 应用执行环境(Application Execution Environment, AEE)
  • 应用程序二进制接口(Application Binary Interface, ABI)
  • 管理员二进制接口(Supervisor Binary Interface, SBI)
  • 管理员执行环境(Supervisor Execution Environment, SEE)
  • Hypervisor:虚拟机监视器
  • Hypervisor二进制接口(Hypervisor Binary interface,HBI)
  • Hypervisor执行环境(Hypervisor Execution Environment, HEE)

RISC-V 系统模式:单应用场景

w:900

  • 不同软件层有清晰的特权级硬件隔离支持
  • 左侧的单个应用程序被编码在ABI上运行
  • ABI是用户级ISA(Instruction Set Architecture)和AEE交互的接口
  • ABI对应用程序隐藏了AEE的细节,使得AEE具有更大的灵活性

RISC-V 系统模式:操作系统场景

w:900

  • 中间加了一个传统的操作系统,可支持多个应用程序的多道运行
  • 每个应用程序通过ABI和OS进行通信
  • RISC-V操作系统通过SBI和SEE进行通信
  • SBI是OS内核与SEE交互的接口,支持OS的ISA

RISC-V 系统模式:虚拟机场景

w:900

  • 右侧是虚拟机场景,可支持多个操作系统

RISC-V 系统模式:应用场景

w:900

  • M Mode:小型设备(蓝牙耳机等)
  • U+M Mode:嵌入式设备(电视遥控器、刷卡机等)
  • U+S+M Mode:手机
  • U+S+H+M Mode:数据中心服务器

RISC-V 系统模式:硬件线程

w:900

  • 特权级是为不同的软件栈部件提供的一种保护机制
  • 硬件线程(hart,即CPU core)是运行在某个特权级上(CSR配置)
  • 当处理器执行当前特权模式不允许的操作时将产生一个异常,这些异常通常会产生自陷(trap)导致下层执行环境接管控制权

提纲

  1. 主流CPU比较
  2. RISC-V系统模式
  3. RISC-V系统编程:用户态编程
  4. RISC-V系统编程:M-Mode编程
  5. RISC-V系统编程:内核编程

2.1 概述

2.2 特权级

2.3 CSR寄存器


RISC-V 系统模式:多个特权级

w:900

  • 现代处理器一般具有多个特权级的模式(Mode)
  • U:User | S: Supervisor | H: Hypervisor | M: Machine

为何有这4种模式? 它们的区别和联系是啥?


RISC-V 系统模式:执行环境

执行环境编码含义跨越特权级
APP00User/Applicationecall
OS01Supervisorecall sret
VMM10Hypervisor---
BIOS11Machineecall mret
  • M, S, U 组合在一起的硬件系统适合运行类似UNIX的操作系统

RISC-V 系统模式:特权级的灵活组合

w:900

  • 随着应用的需求变化,需要灵活可组合的硬件构造
  • 所以就出现了上述4种模式,且模式间可以组合的灵活硬件设计

RISC-V 系统模式:用户态

w:900

  • U-Mode (User Mode,用户模式、用户态)
    • 非特权级模式(Unprivileged Mode):基本计算
    • 应用程序运行的用户态CPU执行模式
    • 不能执行特权指令,不能直接影响其他应用程序执行

RISC-V 系统模式:内核态

w:800

  • S-Mode(Supervisor Mode, Kernel Mode,内核态,内核模式)
    • 在内核态的操作系统具有足够强大的硬件控制能力
    • 特权级模式(Privileged Mode):限制APP的执行与内存访问
    • 操作系统运行的内核态CPU执行模式
    • 能执行内核态特权指令,能直接影响应用程序执行

RISC-V 系统模式:H-Mode

w:900

  • H-Mode(Hypervisor Mode, Virtual Machine Mode,虚拟机监控器)
    • 特权级模式:限制OS访问的内存空间
    • 虚拟机监控器运行的Hypervisor Mode CPU执行模式,能执行H-Mode特权指令,能直接影响OS执行

RISC-V 系统模式:M-Mode

w:900

  • M-Mode(Machine Mode, Physical Machine Mode)
    • 特权级模式:控制物理内存,直接关机
    • Bootloader/BIOS运行的Machine Mode CPU执行模式
    • 能执行M-Mode特权指令,能直接影响上述其他软件的执行

提纲

  1. 主流CPU比较
  2. RISC-V系统模式
  3. RISC-V系统编程:用户态编程
  4. RISC-V系统编程:M-Mode编程
  5. RISC-V系统编程:内核编程

2.1 概述 2.2 特权级

2.3 CSR寄存器


RISC-V CSR寄存器分类

  • 通用寄存器 x0-x31
    • 一般指令访问
    • 非特权指令可以使用的速度最快的存储单元
  • 控制状态寄存器(CSR:Control and Status Registers)
    • 通过控制状态寄存器指令访问,可以有4096个CSR
    • 运行在用户态的应用程序不能访问大部分的CSR寄存器
    • 运行在内核态的操作系统通过访问CSR寄存器控制计算机

通过CSR寄存器实现的隔离

OS通过硬件隔离手段(三防)来保障计算机的安全可靠

  • 设置 CSR(控制状态寄存器) 实现隔离
    • 权力:防止应用访问系统管控相关寄存器
      • 地址空间配置寄存器:mstatus/sstatus CSR(中断及状态)
    • 时间:防止应用长期使用 100%的 CPU
      • 中断配置寄存器:sstatus/stvec CSR(中断跳转地址)
    • 数据:防止应用破坏窃取数据
      • 地址空间相关寄存器:sstatus/stvec/satp CSR (分页系统)

CSR寄存器功能

  • 信息类:主要用于获取当前芯片id和cpu核id等信息。
  • Trap设置:用于设置中断和异常相关寄存器。
  • Trap处理:用于处理中断和异常相关寄存器。
  • 内存保护:有效保护内存资源

Risc-V中异常和中断统称Trap


提纲

  1. 主流CPU比较
  2. RISC-V系统模式

3. RISC-V系统编程:用户态编程

  1. RISC-V系统编程:M-Mode编程
  2. RISC-V系统编程:内核编程

系统编程简述

  • 系统编程需要了解处理器的特权级架构,熟悉各个特权级能够访问的寄存器资源、内存资源和外设资源
  • 编写内核级代码,构造操作系统,支持应用程序执行
    • 内存管理 进程调度
    • 异常处理 中断处理
    • 系统调用 外设控制
  • 系统编程通常没有广泛用户编程库和方便的动态调试手段的支持
  • 本课程的系统编程主要集中在 RISC-V 的 S-Mode 和 U-Mode,涉及部分对M-Mode的理解

RISC-V U-Mode编程:使用系统调用

  • U-Mode 下的应用程序不能够直接使用计算机的物理资源
  • 环境调用异常:在执行 ecall 的时候发生,相当于系统调用
  • 操作系统可以直接访问物理资源
  • 如果应用程序需要使用硬件资源怎么办?
    • 在屏幕上打印”hello world”
    • 从文件中读入数据
  • 通过系统调用从操作系统中获得服务

U-Mode编程:第一个例子”hello world”

在用户态打印”hello world”的小例子 大致执行流

w:1000


第一个例子的启动执行

在用户态打印”hello world”的小例子 启动执行流

w:1000


第二个例子:在用户态执行特权指令

在用户态执行特权指令的小例子 启动与执行流程

w:1000


特权操作

  • 特权操作:特权指令和CSR读写操作

  • 指令非常少:

    • mret 机器模式返回
    • sret 监管者模式返回
    • wfi 等待中断 (wait for interupt)
    • sfence.vma 虚拟地址屏障(barrier)指令,用于虚拟内存无效和同步
  • 很多其他的系统管理功能通过读写控制状态寄存器来实现

注:fence.i是i-cache屏障(barrier)指令,非特权指令,属于 “Zifencei”扩展规范,用于i-cache和d-cache一致性


提纲

  1. 主流CPU比较
  2. RISC-V系统模式
  3. RISC-V系统编程:用户态编程

4. RISC-V系统编程:M-Mode编程

  1. RISC-V系统编程:内核编程

4.1 中断机制和异常机制

4.2 中断/异常的硬件响应 4.3 中断/异常处理的控制权移交


M-Mode编程

  • M-Mode是 RISC-V 中 hart(hardware thread)的最高权限模式
  • M-Mode下,hart 对计算机系统的底层功能有完全的使用权
  • M-Mode最重要的特性是拦截和处理中断/异常
    • 同步的异常:执行期间产生,访问无效的寄存器地址,或执行无效操作码的指令
    • 异步的中断:指令流异步的外部事件,中断,如时钟中断
  • RISC-V 要求实现精确异常:保证异常之前的所有指令都完整执行,后续指令都没有开始执行

    mstatus CSR寄存器

    • mstatus(Machine Status)保存全局中断以及其他的状态
      • SIE控制S-Mode下全局中断,MIE控制M-Mode下全局中断。
      • SPIE、MPIE记录发生中断之前MIE和SIE的值。
      • SPP表示变化之前的特权级别是S-Mode还是U-Mode
      • MPP表示变化之前是S-Mode还是U-Mode还是M-Mode PP:Previous Privilege

    w:1000


    mcause CSR寄存器

    当发生异常时,mcause CSR中被写入一个指示导致异常的事件的代码,如果事件由中断引起,则置上Interrupt位,Exception Code字段包含指示最后一个异常的编码。

    w:1150


    M-Mode时钟中断Timer

    • 中断是异步发生的
      • 来自处理器外部的 I/O 设备的信号
    • Timer 可以稳定定时地产生中断
      • 防止应用程序死占着 CPU 不放, 让 OS Kernel 能得到执行权...
      • 高特权模式下的软件获得 CPU 控制权
      • 高特权模式下的软件可授权低特权模式软件处理中断

    RISC-V处理器FU540模块图

    w:650


    提纲

    1. 主流CPU比较
    2. RISC-V系统模式
    3. RISC-V系统编程:用户态编程
    4. RISC-V系统编程:M-Mode编程
    5. RISC-V系统编程:内核编程

    4.1 中断机制和异常机制

    4.2 中断/异常的硬件响应

    4.3 中断/异常处理的控制权移交


    M-Mode中断的硬件响应过程

    • 异常/中断指令的PC被保存在mepc中,PC设置为mtvec。
      • 对于同步异常,mepc指向导致异常的指令;
      • 对于中断,指向中断处理后应该恢复执行的位置。
    • 根据异常/中断来源设置 mcause,并将 mtval 设置为出错的地址或者其它适用于特定异常的信息字
    • 把mstatus[MIE位]置零以禁用中断,并保留先前MIE值到MPIE中
      • SIE控制S模式下全局中断,MIE控制M模式下全局中断;
      • SPIE记录的是SIE中断之前的值,MPIE记录的是MIE中断之前的值
    • 保留发生异常之前的权限模式到mstatus 的 MPP 域中,然后更改权限模式为M。(MPP表示变化之前的特权级别是S、M or U模式)

    M-Mode中断分类

    通过 mcause 寄存器的不同位(mie)来获取中断的类型。

    • 软件中断:通过向内存映射寄存器写入数据来触发,一个 hart 中断另外一个hart(处理器间中断)
    • 时钟中断:hart 的时间计数器寄存器 mtime 大于时间比较寄存器 mtimecmp
    • 外部中断:由中断控制器触发,大部分情况下的外设都会连到这个中断控制器

    RISC-V 的中断/异常

    通过 mcause 寄存器的不同位来获取中断源的信息。 第一列1代表中断,第2列代表中断ID,第3列中断含义 w:1000


    M-Mode RISC-V异常机制

    通过 mcause 寄存器的不同位来获取导致异常的信息。 第一列0代表异常,第2列代表异常ID,第3列异常含义 w:900


    M-Mode中断/异常的硬件响应

    • 中断/异常的指令的 PC 被保存在 mepc 中, PC 被设置为 mtvec。
      • 对于异常,mepc指向导致异常的指令
      • 对于中断,mepc指向中断处理后应该恢复执行的位置
    • 根据中断/异常来源设置 mcause,并将 mtval 设置为出错的地址或者其它适用于特定异常的信息字。

    M-Mode中断/异常的硬件响应

    • 把控制状态寄存器 mstatus[MIE位]置零以禁用中断,并保留先前的 MIE 值到 MPIE 中。
      • SIE控制S-Mode下全局中断,MIE控制M-Mode下全局中断;
      • SPIE记录的是SIE中断之前的值,MPIE记录的是MIE中断之前的值)
    • 发生异常之前的权限模式保留在 mstatus 的 MPP 域中,再把权限模式更改为M
      • MPP表示变化之前的特权级别是S、M or U-Mode
    • 跳转到mtvec CSR设置的地址继续执行

    提纲

    1. 主流CPU比较
    2. RISC-V系统模式
    3. RISC-V系统编程:用户态编程
    4. RISC-V系统编程:M-Mode编程
    5. RISC-V系统编程:内核编程

    4.1 中断机制和异常机制 4.2 中断/异常的硬件响应

    4.3 中断/异常处理的控制权移交


    M-Mode中断/异常处理的控制权移交

    • 默认情况下,所有的中断/异常都使得控制权移交到 M-Mode的中断/异常处理例程
    • M-Mode的中断/异常处理例程可以将中断/异常重新导向 S-Mode,但是这些额外的操作会减慢中断/异常的处理速度
    • RISC-V 提供一种中断/异常委托机制,通过该机制可以选择性地将中断/异常交给 S-Mode处理,而完全绕过 M-Mode

    M-Mode中断/异常处理的控制权移交

    • mideleg/medeleg (Machine Interrupt/Exception Delegation)CSR 控制将哪些中断/异常委托给 S-Mode处理
    • mideleg/medeleg 中的每个位对应一个中断/异常
      • 如 mideleg[5] 对应于 S-Mode的时钟中断,如果把它置位,S-Mode的时钟中断将会移交 S-Mode的中断/异常处理程序,而不是 M-Mode的中断/异常处理程序
      • 委托给 S-Mode的任何中断都可以被 S-Mode的软件屏蔽。sie(Supervisor Interrupt Enable) 和 sip(Supervisor Interrupt Pending)CSR 是 S-Mode的控制状态寄存器

    中断委托寄存器mideleg

    • mideleg (Machine Interrupt Delegation)控制将哪些中断委托给 S 模式处理
    • mideleg 中的每个为对应一个中断/异常
      • mideleg[1]用于控制是否将核间中断交给s模式处理
      • mideleg[5]用于控制是否将定时中断交给s模式处理
      • mideleg[9]用于控制是否将外部中断交给s模式处理

    异常委托寄存器medeleg

    • medeleg (Machine Exception Delegation)控制将哪些异常委托给 S 模式处理
    • medeleg 中的每个为对应一个中断/异常
      • medeleg[1]用于控制是否将指令获取错误异常交给s模式处理
      • medeleg[12]用于控制是否将指令页异常交给s模式处理
      • medeleg[9]用于控制是否将数据页异常交给s模式处理

    中断/异常处理的控制权移交

    • 发生中断/异常时,处理器控制权通常不会移交给权限更低的模式
      • 例如 medeleg[15] 会把 store page fault 委托给 S-Mode
      • M-Mode下发生的异常总是在 M-Mode下处理
      • S-Mode下发生的异常总是在 M-Mode,或者在 S-Mode下处理
      • 上述两种模式发生的异常不会由 U-Mode处理

    Why?


    思考题

    • 如何通过断点异常来实现调试器的断点调试功能?
    • 如何实现单步跟踪?

    提纲

    1. 主流CPU比较
    2. RISC-V系统模式
    3. RISC-V系统编程:用户态编程
    4. RISC-V系统编程:M-Mode编程

    5. RISC-V系统编程:内核编程

    5.1 中断/异常机制

    5.2 中断/异常的处理 5.3 虚存机制


    S-Mode的中断控制和状态寄存器

    • stvec(SupervisorTrapVector)保存发生中断/异常时要跳转到的地址
    • sepc(Supervisor Exception PC)指向发生中断/异常时的指令
    • scause(Supervisor Exception Cause)指示发生中断/异常的种类
    • sie(Supervisor Interrupt Enable)中断使能寄存器
    • sip(Supervisor Interrupt Pending)中断请求寄存器
    • stval(Supervisor Trap Value)保存陷入(trap)附加信息
    • sscratch(Supervisor Scratch)不同mode交换数据中转站
    • sstatus(Supervisor Status)保存全局中断以及其他的状态

    sstatus寄存器

    • sstatus的SIE 和 SPIE 位分别保存了当前的和中断/异常发生之前的中断使能状态

    w:1100


    S-Mode中断/异常机制

    sie & sip 寄存器是用于保存待处理中断中断使能情况的CSR

    • sie (supervisor interrupt-enabled register)
    • sip(supervisor interrupt pending)

    w:1150


    scause寄存器

    当发生异常时,CSR中被写入一个指示导致中断/异常的事件编号,记录在Exception Code字段中;如果事件由中断引起,则置Interrupt位。 scause 寄存器

    w:1150


    mtvec & stvec 寄存器

    中断/异常向量(trap-vector)基地址寄存器stvec CSR用于配置trap_handler地址

    • 包括向量基址(BASE)和向量模式(MODE):BASE 域中的值按 4 字节对齐
      • MODE = 0 表示一个trap_handler处理所有的中断/异常
      • MODE = 1 表示每个中断/异常有一个对应的trap_handler

    mtvec & stvec 寄存器 w:1000


    提纲

    1. 主流CPU比较
    2. RISC-V系统模式
    3. RISC-V系统编程:用户态编程
    4. RISC-V系统编程:M-Mode编程
    5. RISC-V系统编程:内核编程

    5.1 中断/异常机制

    5.2 中断/异常的处理

    5.3 虚存机制


    S-Mode中断/异常的硬件响应

    硬件执行内容

    hart 接受了中断/异常,并需要委派给 S-Mode,那么硬件会原子性的经历下面的状态转换

    1. 发生中断/异常的指令PC被存入 sepc, 且 PC 被设置为 stvec
    2. scause 设置中断/异常类型,stval被设置为出错的地址/异常相关信息
    3. 把 sstatus中的 SIE 位置零,屏蔽中断SIE位之前的值被保存在 SPIE 位中

    S-Mode中断/异常的硬件响应

    1. 发生例外前的特权模式被保存在 sstatus 的 SPP(previous privilege) 域,然后设置当前特权模式为S-Mode
    2. 跳转到stvec CSR设置的地址继续执行

    S-Mode中断/异常的软件处理

    • 初始化
      • 编写中断/异常的处理例程(如trap_handler)
      • 设置trap_handler地址给stvec
    • 软件执行
      1. 处理器跳转到trap_handler
      2. trap_handler处理中断/异常/系统调用等
      3. 返回到之前的指令和之前的特权级继续执行

    提纲

    1. 主流CPU比较
    2. RISC-V系统模式
    3. RISC-V系统编程:用户态编程
    4. RISC-V系统编程:M-Mode编程
    5. RISC-V系统编程:内核编程

    5.1 中断/异常机制 5.2 中断/异常的处理

    5.3 虚存机制


    S-Mode虚拟内存系统

    • 虚拟地址将内存划分为固定大小的页来进行地址转换内容保护
    • satp(Supervisor Address Translation and Protection,监管者地址转换和保护)S模式控制状态寄存器控制分页。satp 有三个域:
      • MODE 域可以开启分页并选择页表级数
      • ASID(Address Space Identifier,地址空间标识符)域是可选的,避免了切换进程时将TLB刷新的问题,降低上下文切换的开销
      • PPN 字段保存了根页表的物理页号 w:900

    S-Mode虚存机制

    • 通过stap CSR建立页表基址
    • 建立OS和APP的页表
    • 处理内存访问异常

    bg right:55% 85%


    S-Mode虚存的地址转换

    S、U-Mode中虚拟地址会以从根部遍历页表的方式转换为物理地址:

    • satp.PPN 给出了一级页表基址, VA [31:22] 给出了一级页号,CPU会读取位于地址(satp. PPN × 4096 + VA[31: 22] × 4)页表项。
    • PTE 包含二级页表基址,VA[21:12]给出了二级页号,CPU读取位于地址(PTE. PPN × 4096 + VA[21: 12] × 4)叶节点页表项。
    • 叶节点页表项的PPN字段和页内偏移(原始虚址的最低 12 个有效位)组成了最终结果:物理地址(LeafPTE.PPN×4096+VA[11: 0])

    S-Mode虚存的地址转换

    w:650


    小结

    • 了解 RISC-V 特权级和硬件隔离方式
    • 了解 RISC-V 的 M-Mode 和 S-Mode 的基本特征
    • 了解OS在 M-Mode 和 S-Mode 下如何访问控制计算机系统
    • 了解不同软件如何在 M-Mode<–>S-Mode<–>U-Mode 之间进行切换

    【总结笔记】

    主流CPU架构比较

    • x86和ARM:由于兼容性和历史原因,这两种架构的设计实现较为复杂。它们的发展历史悠久,以向后兼容性为重,因此积累了大量的指令和特性,这增加了它们的实现复杂度。
    • RISC-V:与x86和ARM相比,RISC-V架构以其简洁、灵活和可扩展性著称。RISC-V是一个较新的开源指令集架构(ISA),设计时就考虑到了易于学习和使用,特别适合教育和研究,同时也因为其开放性和模块化设计,在工业应用中越来越受欢迎。

    RISC-V系统模式

    RISC-V定义了多个系统模式来支持从简单的嵌入式系统到复杂的操作系统和虚拟化环境。这些模式分别是:

    • U-Mode (User/Application Mode):最不具特权的模式,用于运行应用程序代码。
    • S-Mode (Supervisor Mode):运行操作系统的内核代码,具有一定级别的特权,可以进行一些硬件资源管理。
    • H-Mode (Hypervisor Mode):用于运行虚拟机监控器(VMM),比S-Mode拥有更高的特权。
    • M-Mode (Machine Mode):最高的特权级别,通常用于运行Bootloader或系统的最底层硬件抽象层。

    每个模式都对应于不同的执行环境和特权级别,以及一系列特定的二进制接口(ABI/SBI/HBI)和执行环境(AEE/SEE/HEE),为不同的应用场景提供支持。

    特权级别和应用场景

    • 单应用场景:只运行一个应用程序,直接在U-Mode下执行。
    • 操作系统场景:引入S-Mode,允许多个应用程序通过操作系统的调度和管理并发运行。
    • 虚拟机场景:通过引入H-Mode,支持在同一硬件上运行多个操作系统实例。
    • 应用场景举例:不同的模式组合可以支持从小型设备到数据中心服务器的广泛应用。

    RISC-V CSR寄存器分类

    通用寄存器 (x0-x31)

    • 这些是日常编程中最常用的寄存器,用于一般的指令访问。
    • 非特权指令可以使用,是最快的存储单元。

    控制状态寄存器 (CSR)

    • 专为特权指令访问设计,理论上可以有4096个。
    • 用户态应用程序受限,无法访问大部分CSR寄存器。
    • 操作系统(内核态)通过CSR寄存器控制计算机,实现了对硬件细节的精确控制和管理。

    通过CSR寄存器实现的隔离

    操作系统通过设置CSR寄存器实现对资源的隔离,保护系统的安全可靠:

    • 权力隔离:限制应用访问关键系统寄存器,如地址空间配置寄存器。
    • 时间隔离:防止应用长时间占用CPU,如中断配置寄存器。
    • 数据隔离:防止应用程序破坏或窃取数据,如地址空间相关寄存器。

    CSR寄存器功能

    CSR寄存器包括信息类(如芯片ID、CPU核ID等信息),Trap设置(设置中断和异常相关寄存器),Trap处理(处理中断和异常相关寄存器),内存保护(保护内存资源)等多种功能,强化了RISC-V的安全性和灵活性。

    系统编程

    系统编程涉及深入理解处理器的特权级架构,熟悉各特权级能够访问的寄存器、内存和外设资源。这包括编写内核级代码,实现内存管理、进程调度、异常处理、中断处理、系统调用和外设控制等核心功能。

    RISC-V中的U-Mode编程

    用户态编程(U-Mode)依赖于系统调用(通过执行ecall指令触发)从操作系统获取服务,如屏幕打印或文件读取。

    RISC-V中的M-Mode编程

    机器模式(M-Mode)是RISC-V中最高权限模式,主要负责拦截和处理中断/异常。M-Mode下的程序拥有对系统底层功能的完全控制权,这包括同步异常处理和异步中断处理。

    M-Mode中断的硬件响应

    当发生中断/异常时,处理器执行以下步骤:

    1. 保存当前PCmepc,然后设置PC到mtvec指定的中断处理例程入口地址。
    2. 设置mcause以指示中断/异常的类型,并设置mtval为出错地址或其他信息。
    3. 禁用中断,通过将mstatus中的MIE位置零,并保留先前的MIE值到MPIE中。
    4. 保留发生异常之前的权限模式mstatus的MPP域中,并更改权限模式为M。

    中断/异常处理的控制权移交

    M-Mode中断/异常处理的控制权移交

    RISC-V提供的中断/异常委托机制允许选择性地将特定的中断和异常处理委托给S-Mode,绕过M-Mode,从而加速处理速度。midelegmedeleg寄存器用于控制这一委托过程,每个位代表一种中断或异常,设置相应的位即可将处理委托给S-Mode。

    为何需要控制权移交

    控制权的移交机制使得操作系统(运行在S-Mode)能够更加灵活地管理中断和异常,提高系统的响应速度和效率。此外,这也是一种安全策略,允许操作系统处理大部分中断和异常,而仅在必要时才使用更高权限的M-Mode。

    RISC-V系统编程:内核编程

    S-Mode中断控制和状态寄存器

    S-Mode具有一套自己的中断控制和状态寄存器,如stvecsepcscausesiesipstvalsscratchsstatus,它们与M-Mode的类似寄存器相对应,但用于管理S-Mode下的中断和异常。

    S-Mode中断/异常的硬件响应

    当S-Mode接收到一个中断或异常时,处理器自动执行一系列操作,包括保存当前程序计数器(PC)到sepc,将中断或异常类型记录到scause,并跳转到stvec指定的处理程序地址。

    S-Mode中断/异常的软件处理

    软件方面,需要初始化中断和异常处理程序,并设置stvec寄存器指向这些程序。在中断或异常发生时,处理器跳转到这些程序执行相应的处理逻辑,然后返回到原来的执行流程。

    S-Mode虚存机制

    S-Mode支持基于页表的虚拟内存系统,通过satp寄存器控制,实现虚拟地址到物理地址的映射和转换。这一机制为操作系统提供了强大的内存管理能力,包括进程间的地址空间隔离和内存保护。


    lzz

    以上的知识点感觉比较虚幻全是各种定义 有没有什么加深理解的介绍

    ChatGPT

    理解RISC-V的中断、异常处理和虚存机制确实需要把握很多概念和细节。让我们尝试通过一些具体的场景和比喻来加深理解。

    中断/异常处理

    想象你在一个大型公司工作,每个员工(程序)都在各自的办公室(内存空间)里忙碌。突然,火警(中断)响起,大家需要按照既定的紧急疏散程序(中断服务例程)迅速撤离。这个疏散计划就像是mtvecstvec寄存器,它告诉大家在紧急情况下应该去哪里集合。

    • M-Mode中断处理:想象公司的保安部门(M-Mode)负责处理所有紧急情况。他们有权利决定是自己处理还是指派给各楼层的安全代表(S-Mode)。
    • S-Mode中断处理:楼层的安全代表只负责自己楼层的紧急情况。如果有更大的紧急情况,他们会联系保安部门。

    中断/异常的控制权移交

    当保安部门决定让楼层的安全代表处理某些类型的紧急情况时,这就像是通过midelegmedeleg寄存器将中断和异常处理的控制权委托给S-Mode。

    虚拟内存系统

    虚拟内存系统可以比作一家大型图书馆,每本书(数据/指令)都有一个特定的位置(物理地址)。但是,图书馆为了方便访问和管理,使用了一个索引系统(页表),让读者(程序)通过索引号(虚拟地址)就能找到书籍的实际位置。satp寄存器就是这个索引系统的入口,告诉你如何通过虚拟地址找到数据的物理地址。

    • 地址转换:当你查找一本书时,你只需知道它的索引号。图书馆的管理系统(MMU)会自动通过索引号查找到书的实际位置,这个过程就像虚拟地址被转换为物理地址。

    实际应用

    • 开发调试:使用断点和单步执行来调试程序,就像是在图书馆里找书时偶尔需要停下来检查你是否走在正确的路径上。
    • 操作系统设计:操作系统的设计者就像图书馆的管理员,需要规划索引系统,决定哪些书籍(数据/程序)应该放在哪里,以及如何高效地管理这些资源。

    通过这些场景和比喻,希望能帮你更直观地理解RISC-V的这些概念。在实践中,通过编写和运行一些简单的RISC-V程序,尤其是那些涉及中断处理和内存管理的程序,将进一步加深你对这些概念的理解。


    lzz

    解释总结以上使用的寄存器的名字、缩写的英文和功能职责,展示它们一定的工作流程

    ChatGPT

    让我们详细总结一下之前讨论的RISC-V中的重要寄存器及其在中断/异常处理和虚拟内存系统中的职责。

    中断/异常处理寄存器

    1. PC (Program Counter): 程序计数器,保存着处理器当前执行指令的地址。
    2. mepc (Machine Exception Program Counter): 机器异常程序计数器,保存发生异常时指令的地址。在M-Mode中用于记录中断或异常发生前的PC值。
    3. sepc (Supervisor Exception Program Counter): 监管者异常程序计数器,与mepc相似,但用于S-Mode。记录发生异常或中断时的PC值。
    4. mtvec (Machine Trap-Vector Base-Address Register): 机器陷阱向量基址寄存器,存储M-Mode中断处理程序的入口地址。
    5. stvec (Supervisor Trap-Vector Base-Address Register): 监管者陷阱向量基址寄存器,存储S-Mode中断处理程序的入口地址。
    6. mcause (Machine Cause Register): 机器原因寄存器,记录最近一次异常或中断的原因。
    7. scause (Supervisor Cause Register): 监管者原因寄存器,与mcause类似,但用于S-Mode。
    8. mie (Machine Interrupt Enable): 机器中断使能寄存器,M-Mode中断使能寄存器,用于控制是否允许中断发生。
    9. sie (Supervisor Interrupt Enable): 监管者中断使能寄存器,S-Mode中断使能寄存器,与mie相似,但用于S-Mode。
    10. mideleg (Machine Interrupt Delegation Register): 机器中断委托寄存器,控制哪些中断可以被委托给S-Mode处理的M-Mode寄存器。
    11. medeleg (Machine Exception Delegation Register): 机器异常委托寄存器,控制哪些异常可以被委托给S-Mode处理的M-Mode寄存器。

    虚拟内存系统寄存器

    1. satp (Supervisor Address Translation and Protection): S-Mode下控制页表的寄存器,包括启用分页机制、页表的物理地址和地址空间标识符。

    工作流程示例

    以下是一个简化的中断处理流程:

    1. 中断发生:假设CPU正在执行用户程序,此时发生了一个时钟中断。
    2. 保存现场:
      • CPU自动将当前的PC值保存到mepcsepc(取决于当前的模式和委托设置)。
      • mcause/scause被设置为描述中断原因的值。
    3. 中断处理:
      • CPU读取mtvec/stvec的值,跳转到中断处理程序的入口地址。
      • 中断处理程序执行,例如,可能更新系统时钟,调度另一个进程等。
    4. 恢复现场并返回:
      • 处理完成后,中断处理程序通过指令mret/sret指示CPU从mepc/sepc读取之前保存的PC值,恢复到中断发生前的执行点。

    在虚拟内存系统中的地址转换过程:

    1. 程序访问虚拟地址:用户程序试图访问一个虚拟地址。
    2. 地址转换:
      • CPU使用satp寄存器中的信息找到根页表。
      • 根据虚拟地址中的页号和偏移量,通过一系列页表项遍历找到最终的物理地址。
    3. 访问物理内存:CPU根据转换后的物理地址访问实际的内存数据或指令。

    通过这些寄存器和工作流程,RISC-V能够有效地处理中断和异常,同时提供灵活的内存管理机制。


    寄存器缩写英文全称中文全称功能描述
    PCProgram Counter程序计数器保存着处理器当前执行指令的地址。
    mepcMachine Exception Program Counter机器异常程序计数器保存发生异常时指令的地址,在M-Mode中用于记录中断或异常发生前的PC值。
    sepcSupervisor Exception Program Counter监管者异常程序计数器与mepc相似,但用于S-Mode,记录发生异常或中断时的PC值。
    mtvecMachine Trap-Vector Base-Address Register机器陷阱向量基址寄存器存储M-Mode中断处理程序的入口地址。
    stvecSupervisor Trap-Vector Base-Address Register监管者陷阱向量基址寄存器存储S-Mode中断处理程序的入口地址。
    mcauseMachine Cause Register机器原因寄存器记录最近一次异常或中断的原因。
    scauseSupervisor Cause Register监管者原因寄存器与mcause类似,但用于S-Mode。
    mieMachine Interrupt Enable机器中断使能寄存器M-Mode中断使能寄存器,用于控制是否允许中断发生。
    sieSupervisor Interrupt Enable监管者中断使能寄存器S-Mode中断使能寄存器,与mie相似,但用于S-Mode。
    midelegMachine Interrupt Delegation Register机器中断委托寄存器控制哪些中断可以被委托给S-Mode处理的M-Mode寄存器。
    medelegMachine Exception Delegation Register机器异常委托寄存器控制哪些异常可以被委托给S-Mode处理的M-Mode寄存器。

    RISC-V中断处理流程

    1. 中断触发:首先,中断信号由硬件(如定时器、I/O设备)发出。
    2. 中断识别:处理器检测到中断信号,并根据当前的中断使能状态(通过miesie寄存器控制)和优先级决定是否接受中断。
    3. 保存当前状态
      • 保存PC:处理器自动将当前的程序计数器(PC)保存到mepc(如果在M-Mode)或sepc(如果中断被委托给S-Mode)寄存器中。
      • 设置mcause/scausemcause(在M-Mode)或scause(在S-Mode)寄存器被设置为描述中断原因的值。
    4. 中断处理程序入口
      • 跳转执行:处理器读取mtvec(在M-Mode)或stvec(在S-Mode)寄存器的值,并跳转到相应的中断处理程序入口地址执行。
    5. 执行中断处理程序:执行中断服务例程(ISR),完成必要的处理任务,如读取数据、更新状态、清除中断标志等。
    6. 恢复现场并返回
      • 通过执行mret(在M-Mode)或sret(在S-Mode)指令,处理器从mepc/sepc寄存器恢复之前保存的PC值,返回到中断发生前的执行点继续执行。

    RISC-V的启动流程中,不同的模式(U-Mode、S-Mode、M-Mode)以及它们之间的交互反映了一个层级结构和控制流程。这个流程确保了系统在启动和运行时的安全和稳定性。

    从下到上(启动和初始化流程)

    1. 启动(Boot-ROM)
      • 系统电源开启或重置后,首先执行的是ROM中的启动代码,它初始化硬件,并跳转到更高级别的程序或者引导加载器。
    2. 机器模式(M-Mode)
      • 接下来,控制权传递到机器模式,这是最低级别的权限模式。在这里,通常执行的是固件代码,如RustSBI(Supervisor Binary Interface的一个实现),负责初始化系统的基础设施,包括设置中断和异常处理程序。
    3. 监管模式(S-Mode)
      • 一旦机器模式的初始化完成,通过mret指令将控制权交给监管模式。监管模式负责启动操作系统,设置虚拟内存、进程和用户模式的隔离。
    4. 用户模式(U-Mode)
      • 操作系统创建用户空间环境,并通过sret指令跳转到用户模式运行用户程序。用户模式程序可以通过系统调用与操作系统交互,请求服务。

    从上到下(系统调用和响应流程)

    1. 用户模式(U-Mode)
      • 当用户程序需要操作系统服务时(比如写文件、网络通信等),它会通过系统调用(syscall)请求操作系统介入。
    2. 监管模式(S-Mode)
      • 系统调用触发中断,将控制权从用户模式转交给监管模式。操作系统在监管模式中响应系统调用,执行必要的服务。
    3. 机器模式(M-Mode)
      • 若需要执行更低级别的操作,如控制台输入输出,操作系统可能会通过SBI调用(sbicall),请求机器模式下的SBI固件提供支持。

    从下到上(执行SBI调用并从M-Mode返回到U-Mode)

    1. M-Mode执行SBI调用:
      • 在机器模式下,RustSBI等固件执行一个特权级操作,如sbicall进行系统级别的服务请求。
    2. M-Mode完成操作:
      • SBI服务完成后,例如完成了控制台字符输出或硬件设备的控制。
    3. 返回到S-Mode:
      • 使用mret指令,RustSBI将控制权从M-Mode返回到S-Mode。此时,处理器状态恢复到S-Mode执行sbicall之前的状态。
    4. S-Mode处理结果:
      • 操作系统在S-Mode处理sbicall的结果,可能是更新操作系统状态、记录日志或者准备返回到用户模式。
    5. 准备返回到U-Mode:
      • 操作系统完成所有必要的处理后,准备将控制权返回给用户程序。
    6. 返回到U-Mode:
      • 操作系统使用sret指令,将控制权返回到用户模式,继续用户程序的执行。
    7. U-Mode继续执行:
      • 用户程序继续执行,可能是处理SBI调用的结果,或者执行下一步操作。
    8. 用户程序运行结束:
      • 用户程序完成其任务后,可能是自然结束,或者因为需要再次服务而触发另一次系统调用。

    在整个流程中,mretsret指令用于返回到之前的权限模式,它们分别用在机器模式和监管模式的退出操作。这样的层级结构和控制流程允许RISC-V系统在保证安全性的同时,实现复杂的操作系统功能,包括用户与硬件之间的交互。

    从上到下(关机流程)

    1. 用户模式(U-Mode):
      • 用户程序决定关机,发起系统调用(syscall)通知操作系统进行关机。
    2. 监管模式(S-Mode):
      • 操作系统接收到来自用户程序的关机系统调用,并开始执行关机准备工作,如关闭文件、通知其他进程等。
    3. 机器模式(M-Mode):
      • 操作系统执行完所有准备工作后,通过SBI调用(sbicall)请求M-Mode的固件执行关机操作。
    4. 执行SBI关机调用:
      • 固件(如RustSBI)接收到关机请求,执行必要的硬件关闭程序,包括停止时钟、关闭I/O设备等。
    5. 电源关闭(Power Down):
      • 完成所有硬件关闭程序后,固件执行最终的电源关闭操作。

    w:1000

    用户程序执行“Hello World”

    1. 执行用户程序:
      • 用户程序(在U-Mode中)开始执行,当需要打印输出时,会执行如uprint这样的打印函数。
    2. 系统调用:
      • 执行uprint函数时,用户程序会进行系统调用(通过ecall指令),请求操作系统提供打印服务。
    3. S-Mode响应系统调用:
      • 系统调用将控制权从U-Mode转交给S-Mode,操作系统的sys_write函数响应系统调用,处理打印请求。
    4. 调用SBI服务:
      • 如果打印需要低级访问,如向控制台输出字符,操作系统可能会调用SBI服务(通过SBI调用)。
    5. M-Mode SBI执行打印:
      • SBI的sbi.console_putchar函数在M-Mode中被调用,将字符输出到控制台。

    返回监管模式(S-Mode)

    1. SBI to OS:
      • 打印操作完成后,SBI固件通过mret指令返回到监管模式,准备返回控制权给操作系统。
    2. OS处理完成:
      • 操作系统接收来自SBI的返回,并完成任何后续的清理或状态更新操作。

    返回用户模式(U-Mode)

    1. 返回到系统调用点:
      • 操作系统通过sret指令将控制权返回到发起系统调用的用户程序的位置。
    2. 用户程序继续执行:
      • 用户程序继续执行打印“Hello World”后的下一条指令。
    3. 程序执行完毕:
      • “Hello World”打印请求执行完毕后,用户程序可能会继续执行其他任务或者终止。

    系统返回到正常状态

    1. 任务调度(可选):

      • 如果有多任务调度,操作系统可能在此时进行上下文切换,调度另一个进程或线程运行。
    2. 系统空闲或等待:

      • 如果用户程序是最后一个任务或系统处于空闲状态,操作系统可能会进入等待状态,等待下一个中断或系统调用。

    w:1000

    在RISC-V架构中,用户态(U-Mode)是不允许直接执行特权指令的。如果用户程序尝试执行特权指令,这将触发一个异常,进而由更高权限模式的异常处理程序来响应。以下是在用户态执行特权指令时的流程:

    1. 用户模式(U-Mode)执行特权指令:
      • 用户程序试图执行特权指令(例如:uprint内部尝试直接执行硬件操作)。
    2. 异常被触发:
      • 由于用户程序无权执行特权指令,处理器将触发异常。
    3. 转到监管模式(S-Mode):
      • 异常使处理器从U-Mode跳转到S-Mode,控制权转交给操作系统的异常处理程序(trap handler)。
    4. 操作系统处理异常:
      • 操作系统的异常处理程序确定异常原因(通过scause寄存器)并决定如何处理(比如记录错误、终止程序等)。
    5. 可选的SBI调用:
      • 如果需要,操作系统可以选择调用SBI接口(例如:sbi_exit),请求M-Mode下的SBI来执行某些操作(比如关闭硬件设备或重启)。
    6. 恢复或终止:
      • 根据异常的性质和严重性,操作系统可能会决定继续执行应用程序、重新启动应用程序或完全终止应用程序。
    7. 返回用户模式(U-Mode):
      • 如果操作系统决定让应用程序继续运行,它将通过执行sret指令返回到用户模式。
    8. 处理完成:
      • 用户程序根据操作系统的处理结果决定下一步操作,可能是继续执行、尝试其他操作或正常退出。

    在这个流程中,异常处理程序为系统的稳定性提供了关键保障,确保即使在不当操作尝试时,系统也能保持响应性和可靠性。这也显示了RISC-V架构对权限控制的严格性,防止了非授权的特权操作。


    RISC-V架构中的中断处理和权限模式为系统设计提供了一系列的优点,这些优点在上述的“Hello World”程序执行流程和关机流程中表现得尤为明显:

    1. 层级安全性:通过层级化的权限模式(U-Mode, S-Mode, M-Mode),RISC-V确保了每个层级只能执行它被授权的操作。这防止了低权限代码对系统的潜在破坏,提高了系统的整体安全性。
    2. 细粒度控制:通过各种模式特定的寄存器和指令(如mepc, sepc, mcause, scause, mret, sret),系统可以精确控制执行流程和资源管理,从而在需要时提供灵活性。
    3. 异常处理:当用户态程序尝试执行非法操作时(如特权指令),RISC-V能够优雅地捕获并处理异常,而不是让整个系统崩溃。
    4. 中断效率:通过委托机制(midelegmedeleg),RISC-V允许中断直接被S-Mode处理,而无需每次都经过M-Mode,这减少了中断响应时间,提高了效率。
    5. 模块化设计:RISC-V的设计鼓励模块化,这意味着不同的组件(比如SBI)可以独立于处理器核心进行开发和优化。
    6. 简化的上下文切换:在中断处理和系统调用过程中,通过寄存器保存和恢复现场,RISC-V简化了上下文切换,减少了切换开销。
    7. 可预测的行为:每个级别的行为都是可预测的,这对于系统的可靠性和可维护性至关重要。在关机流程中,这一点尤其显著,因为系统需要按照严格的顺序逐步关闭资源。
    8. 支持虚拟化:通过satp寄存器和虚存机制的支持,RISC-V允许操作系统实现强大的内存管理策略,为虚拟化和容器化技术提供了基础。
    9. 底层支持:底层的SBI提供了一个通用的接口,让上层操作系统能够与硬件进行互动,而不需要关心硬件的具体实现细节,促进了软硬件之间的解耦。

    总的来说,通过这些设计,RISC-V架构提供了一个强大、安全和高效的平台,适用于从简单的嵌入式系统到复杂的操作系统和应用程序。它的设计允许系统开发者在确保核心功能安全性的同时,为最终用户提供灵活、快速响应的体验。

    第三讲 基于特权级的隔离与批处理

    第三节 实践:批处理操作系统

    向勇 陈渝 李国良 任炬

    2024年春季


    提纲

    1. 实验目标

    1. 实践步骤
    2. 软件架构
    3. 相关硬件
    4. 应用程序设计
    5. 内核程序设计

    批处理操作系统的结构

    w:750


    批处理OS目标

    • 让APP与OS隔离
    • 自动加载并运行多个程序
      • 批处理(batch)

    LibOS目标

    • 让应用与硬件隔离
    • 简化应用访问硬件的难度和复杂性

    bg right:54% 95%


    实验要求

    • 理解运行其他软件的软件
    • 理解特权级和特权级切换
    • 理解系统调用
    • 会写邓式鱼批处理操作系统

    邓式鱼:Dunkleosteus 在泥盆纪有坚固盾甲的海洋霸主
    bg right:52% 90%


    总体思路

    • 编译:应用程序和内核独立编译,合并为一个镜像
    • 构造:系统调用服务请求接口,应用管理与初始化
    • 运行:OS一个一个地执行应用
    • 运行:应用发出系统调用请求,OS完成系统调用
    • 运行:应用与OS基于硬件特权级机制进行特权级切换

    历史背景

    • GM-NAA I/O System(1956)
      • 启发:汽车生产线
    • MULTICS OS(1969,MIT/GE/AT&T)
      • GE 645 具有 8 级硬件支持的保护环 GM-NAA: General Motors and North American Aviation; GE: General Electric bg right:38% 95%

    Ref: What was the first operating system to be called an "operating system"?


    提纲

    1. 实验目标

    2. 实践步骤

    1. 软件架构
    2. 相关硬件
    3. 应用程序设计
    4. 内核程序设计

    步骤

    • 构造包含OS和多个APP的单一执行镜像
    • 通过批处理支持多个APP的自动加载和运行
    • 利用硬件特权级机制实现对操作系统自身的保护
    • 支持跨特权级的系统调用
    • 实现特权级的切换

    bg right:54% 100%


    编译步骤

    git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
    cd rCore-Tutorial-v3
    git checkout ch2
    
    cd os
    make run
    

    参考运行结果

    ...
    [kernel] num_app = 5
    [kernel] app_0 [0x8020a038, 0x8020af90)
    ...
    [kernel] Loading app_0
    Hello, world!
    [kernel] Application exited with code 0
    [kernel] Loading app_1
    ...
    [kernel] Panicked at src/batch.rs:58 All applications completed!
    

    提纲

    1. 实验目标
    2. 实践步骤

    3. 软件架构

    1. 相关硬件
    2. 应用程序设计
    3. 内核程序设计

    构建应用

    把多个应用合在一起与OS形成一个二进制镜像

    ├── os
    │   ├── build.rs(新增:生成 link_app.S 将应用作为一个数据段链接到内核)
    │   ├── Cargo.toml
    │   ├── Makefile(修改:构建内核之前先构建应用)
    │   └── src
    │       ├── link_app.S(构建产物,由 os/build.rs 输出)
    

    改进OS

    加载和执行程序、特权级上下文切换

    ├── os
    │   └── src
    │       ├── batch.rs(新增:实现了一个简单的批处理系统)
    │       ├── main.rs(修改:主函数中需要初始化 Trap 处理并加载和执行应用)
    
    │       └── trap(新增:Trap 相关子模块 trap)
    │           ├── context.rs(包含 Trap 上下文 TrapContext)
    │           ├── mod.rs(包含 Trap 处理入口 trap_handler)
    │           └── trap.S(包含 Trap 上下文保存与恢复的汇编代码)
    

    系统调用

    ├── os
    │   └── src
    │       ├── syscall(新增:系统调用子模块 syscall)
    │       │   ├── fs.rs(包含文件 I/O 相关的 syscall)
    │       │   ├── mod.rs(提供 syscall 方法根据 syscall ID 进行分发处理)
    │       │   └── process.rs(包含任务处理相关的 syscall)
    

    添加应用

    批处理OS会按照文件名开头的数字顺序依次加载并运行它们

    └── user(新增:应用测例保存在 user 目录下)
       └── src
          ├── bin(基于用户库 user_lib 开发的应用,每个应用放在一个源文件中)
          │   ├── 00hello_world.rs # 显示字符串的应用
          │   ├── 01store_fault.rs # 非法写操作的应用
          │   ├── 02power.rs       # 计算与I/O频繁交替的应用 
          │   ├── 03priv_inst.rs   # 执行特权指令的应用
          │   └── 04priv_csr.rs    # 执行CSR操作指令的应用
    

    应用库和编译应用支持

    应用库和编译应用支持

    └── user(新增:应用测例保存在 user 目录下)
       └── src
          ├── console.rs              # 支持println!的相关函数与宏
          ├── lang_items.rs           # 实现panic_handler函数
          ├── lib.rs(用户库 user_lib)  # 应用调用函数的底层支撑库 
          ├── linker.ld               # 应用的链接脚本
          └── syscall.rs(包含 syscall 方法生成实际用于系统调用的汇编指令,
                         各个具体的 syscall 都是通过 syscall 来实现的)
    

    提纲

    1. 实验目标
    2. 实践步骤
    3. 软件架构

    4. 相关硬件

    1. 应用程序设计
    2. 内核程序设计

    RISC-V陷入(trap)类指令

    • ecall: 随着 CPU 当前特权级而触发不同的陷入异常
    • ebreak:触发断点陷入异常

    RISC-V特权指令

    • sret: 随着 CPU 当前特权级而触发不同的陷入异常

    bg right:51% 120%


    RISC-V异常向量

    InterruptException CodeDescription
    00Instruction address misaligned
    01Instruction access fault
    02Illegal instruction
    03Breakpoint
    04Load address misaligned
    05Load access fault
    06Store/AMO address misaligned
    07Store/AMO access fault
    08Environment call from U-mode
    09Environment call from S-mode
    011Environment call from M-mode
    012Instruction page fault
    013Load page fault
    015Store/AMO page fault
    • AMO: atomic memory operation

    提纲

    1. 实验目标
    2. 实践步骤
    3. 软件架构
    4. 相关硬件

    5. 应用程序设计

    1. 内核程序设计

    5.1 项目结构

    5.2 内存布局 5.3 系统调用


    应用与底层支撑库分离

    └── user(应用程序和底层支撑库)
       └── src
          ├── bin(该目录放置基于用户库 user_lib 开发的应用)   
          ├── lib.rs(用户库 user_lib)  # 库函数的底层支撑库 
          ├── ......                  # 支撑库相关文件
          └── linker.ld               # 应用的链接脚本
    

    引入外部库

    #![allow(unused)]
    fn main() {
      #[macro_use]
      extern crate user_lib;
    }

    设计支撑库

    在 lib.rs 中我们定义了用户库的入口点 _start :

    #![allow(unused)]
    fn main() {
    #[no_mangle]
    #[link_section = ".text.entry"]
    pub extern "C" fn _start() -> ! {
        clear_bss();
        exit(main());
        panic!("unreachable after sys_exit!");
    }
    }

    提纲

    1. 实验目标
    2. 实践步骤
    3. 软件架构
    4. 相关硬件
    5. 应用程序设计
    6. 内核程序设计

    5.1 项目结构

    5.2 内存布局

    5.3 系统调用


    应用程序的内存布局

    bg 77%


    设计支撑库

    user/src/linker.ld

    • 程序的起始物理地址调整为 0x80400000 ,应用程序都会被加载到这个物理地址上运行;
    • 将 _start 所在的 .text.entry 放在整个程序的开头,也就是说批处理系统只要在加载之后跳转到 0x80400000 就已经进入了 用户库的入口点,并会在初始化之后跳转到应用程序主逻辑;
    • 提供了最终生成可执行文件的 .bss 段的起始和终止地址,方便 clear_bss 函数使用。

    其余的部分与之前相同


    提纲

    1. 实验目标
    2. 实践步骤
    3. 软件架构
    4. 相关硬件
    5. 应用程序设计
    6. 内核程序设计

    5.1 项目结构 5.2 内存布局

    5.3 系统调用


    应用程序的系统调用执行流

    w:1050


    应用程序的系统调用执行流

    • 在子模块 syscall 中,应用程序通过 ecall 调用批处理系统提供的接口
    • ecall 指令会触发 名为 Environment call from U-mode 的异常
    • Trap 进入 S 模式执行批处理系统针对这个异常特别提供的服务代码
    • a0~a6 保存系统调用的参数, a0 保存返回值, a7 用来传递 syscall ID

    bg right:42% 100%


    系统调用支撑库

    #![allow(unused)]
    fn main() {
    /// 功能:将内存中缓冲区中的数据写入文件。
    /// 参数:`fd` 表示待写入文件的文件描述符;
    ///      `buf` 表示内存中缓冲区的起始地址;
    ///      `len` 表示内存中缓冲区的长度。
    /// 返回值:返回成功写入的长度。
    /// syscall ID:64
    fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize;
    
    /// 功能:退出应用程序并将返回值告知批处理系统。
    /// 参数:`xstate` 表示应用程序的返回值。
    /// 返回值:该系统调用不应该返回。
    /// syscall ID:93
    fn sys_exit(xstate: usize) -> !;
    }

    系统调用参数传递

    #![allow(unused)]
    fn main() {
    fn syscall(id: usize, args: [usize; 3]) -> isize {
        let mut ret: isize;
        unsafe {
            asm!(
                "ecall",
                inlateout("x10") args[0] => ret, //第一个参数&返回值
                in("x11") args[1],               //第二个参数
                in("x12") args[2],               //第三个参数
                in("x17") id                     //syscall编号
            );
        }
        ret //返回值
    }
    }

    参考文档:Rust by Example - Inline assembly


    系统调用封装

    #![allow(unused)]
    fn main() {
    const SYSCALL_WRITE: usize = 64; 
    const SYSCALL_EXIT: usize = 93;
    //对系统调用的封装
    pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
        syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
    }
    pub fn sys_exit(xstate: i32) -> isize {
        syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
    }
    }

    系统调用封装

    #![allow(unused)]
    fn main() {
    pub fn write(fd: usize, buf: &[u8]) -> isize { sys_write(fd, buf) }
    
    const STDOUT: usize = 1;
    
    impl Write for Stdout {
        fn write_str(&mut self, s: &str) -> fmt::Result {
            write(STDOUT, s.as_bytes());
            Ok(())
        }
    }
    }

    提纲

    1. 实验目标
    2. 实践步骤
    3. 软件架构
    4. 相关硬件
    5. 应用程序设计

    6. 内核程序设计

    6.1 应用管理和加载

    6.2 特权级切换 6.3 Trap上下文 6.4 Trap处理流程 6.5 执行应用程序


    将应用程序映像链接到内核

    # os/src/link_app.S 由脚本 os/build.rs 生成
        .section .data
        .global _num_app
    _num_app:
        .quad 5
        .quad app_0_start
        ...
        .quad app_4_end
        .section .data
        .global app_0_start
        .global app_0_end
    app_0_start:
        .incbin "../user/target/riscv64gc-unknown-none-elf/release/00hello_world.bin"
    app_0_end:
    

    应用程序管理数据结构

    #![allow(unused)]
    fn main() {
    // os/src/batch.rs
    
    struct AppManager {
        num_app: usize,
        current_app: usize,
        app_start: [usize; MAX_APP_NUM + 1],
    }
    }

    找到应用程序二进制码

    • 找到 link_app.S 中提供的符号 _num_app
    #![allow(unused)]
    fn main() {
    lazy_static! {
        static ref APP_MANAGER: UPSafeCell<AppManager> = unsafe { UPSafeCell::new({
            extern "C" { fn _num_app(); }
            let num_app_ptr = _num_app as usize as *const usize;
            ...
            app_start[..=num_app].copy_from_slice(app_start_raw);
            AppManager {
                num_app,
                current_app: 0,
                app_start,
            }        
    }

    加载应用程序二进制码

    #![allow(unused)]
    fn main() {
    unsafe fn load_app(&self, app_id: usize) {
        // clear icache
        asm!("fence.i");
        // clear app area
        ...
        let app_src = core::slice::from_raw_parts(
            self.app_start[app_id] as *const u8,
            self.app_start[app_id + 1] - self.app_start[app_id] );
        let app_dst = core::slice::from_raw_parts_mut(
            APP_BASE_ADDRESS as *mut u8,
            app_src.len() );
        app_dst.copy_from_slice(app_src);
    }
    }

    加载应用程序二进制码

    • fence.i :用来清理 i-cache

    注:fence.i是i-cache屏障(barrier)指令,非特权指令,属于 “Zifencei”扩展规范

    WHY?


    加载应用程序二进制码

    • fence.i :用来清理 i-cache

    • CPU 对物理内存所做的缓存又分成d-cache和i-cache

    • OS将修改会被 CPU 取指的内存区域,这会使得 i-cache 中含有与内存中不一致的内容

    • OS在这里必须使用 fence.i 指令手动清空 i-cache ,让里面所有的内容全部失效,才能够保证CPU访问内存数据和代码的正确性


    提纲

    1. 实验目标
    2. 实践步骤
    3. 软件架构
    4. 相关硬件
    5. 应用程序设计
    6. 内核程序设计

    6.1 应用管理和加载

    6.2 特权级切换

    6.3 Trap上下文 6.4 Trap处理流程 6.5 执行应用程序


    特权级切换相关CSR

    CSR 名该 CSR 与 Trap 相关的功能
    sstatusSPP 等字段给出 Trap 发生之前 CPU 的特权级(S/U)等
    sepc记录 Trap 发生之前执行的最后一条指令的地址
    scause描述 Trap 的原因
    stval给出 Trap 附加信息
    stvec控制 Trap 处理代码的入口地址

    特权级切换后的硬件逻辑

    1. sstatus 的 SPP 字段会被修改为 CPU 当前的特权级(U/S);
    2. sepc 会被修改为产生 Trap 的指令地址;
    3. scause/stval 分别会被修改成这次 Trap 的原因以及相关的附加信息;
    4. CPU 将当前特权级设为 S,跳到 stvec 所设置的 Trap 处理入口地址。

    特权级切换与用户栈和内核栈

    Why 使用两个不同的栈?

    安全 安全 安全

    #![allow(unused)]
    fn main() {
    const USER_STACK_SIZE: usize = 4096 * 2;
    const KERNEL_STACK_SIZE: usize = 4096 * 2;
    
    static KERNEL_STACK: KernelStack = KernelStack { data: [0; KERNEL_STACK_SIZE] };
    static USER_STACK: UserStack = UserStack { data: [0; USER_STACK_SIZE] };
    }

    特权级切换中的换栈

    #![allow(unused)]
    fn main() {
    impl UserStack {
        fn get_sp(&self) -> usize {
            self.data.as_ptr() as usize + USER_STACK_SIZE
        }
    }
    RegSP = USER_STACK.get_sp();
    RegSP = KERNEL_STACK.get_sp();
    }

    提纲

    1. 实验目标
    2. 实践步骤
    3. 软件架构
    4. 相关硬件
    5. 应用程序设计
    6. 内核程序设计

    6.1 应用管理和加载 6.2 特权级切换

    6.3 Trap上下文

    6.4 Trap处理流程 6.5 执行应用程序


    Trap上下文数据结构

    #![allow(unused)]
    fn main() {
    #[repr(C)]
    pub struct TrapContext {
        pub x: [usize; 32],
        pub sstatus: Sstatus,
        pub sepc: usize,
    }
    }
    • 对于通用寄存器而言,应用程序/内核控制流运行在不同的特权级
    • 进入 Trap 的时候,硬件会立即覆盖掉 scause/stval/sstatus/sepc

    特权级切换后的Trap入口点

    #![allow(unused)]
    fn main() {
    pub fn init() {
        extern "C" { fn __alltraps(); }
        unsafe {
            stvec::write(__alltraps as usize, TrapMode::Direct);
        }
    }
    }

    系统调用过程中的Trap上下文处理

    1. 应用程序通过 ecall 进入到内核状态时,操作系统保存被打断的应用程序的 Trap 上下文;
    2. 操作系统根据Trap相关的CSR寄存器内容,完成系统调用服务的分发与处理;
    3. 操作系统完成系统调用服务后,需要恢复被打断的应用程序的Trap 上下文,并通 sret指令让应用程序继续执行。 bg right:40% 100%

    用户栈到内核栈的切换

    sscratch CSR 重要的中转寄存器

    在特权级切换的时候,我们需要将 Trap 上下文保存在内核栈上,因此需要一个寄存器暂存内核栈地址,并以它作为基地址指针来依次保存 Trap 上下文的内容。

    但是所有的通用寄存器都不能够用作基地址指针,因为它们都需要被保存,如果覆盖掉它们,就会影响后续应用控制流的执行。


    用户栈到内核栈的切换

    sscratch CSR 重要的中转寄存器

    • 暂时保存内核栈的地址
    • 作为一个中转站让 sp (目前指向的用户栈的地址)的值可以暂时保存在 sscratch
    • 仅需一条 csrrw sp, sscratch, sp // 交换对 sp 和 sscratch 两个寄存器内容
    • 完成用户栈-->内核栈的切换

    保存Trap上下文中的通用寄存器

    保存通用寄存器的宏

    # os/src/trap/trap.S
    .macro SAVE_GP n
        sd x\n, \n*8(sp)
    .endm
    

    提纲

    1. 实验目标
    2. 实践步骤
    3. 软件架构
    4. 相关硬件
    5. 应用程序设计
    6. 内核程序设计

    6.1 应用管理和加载 6.2 特权级切换 6.3 Trap上下文

    6.4 Trap处理流程

    6.5 执行应用程序


    Trap处理流程

    Trap 处理的总体流程如下:

    1. 首先通过 __alltraps 将 Trap 上下文保存在内核栈上;
    2. 然后跳转到 trap_handler 函数完成 Trap 分发及处理。
    __alltraps:
        csrrw sp, sscratch, sp
        # now sp->kernel stack, sscratch->user stack
    
        # allocate a TrapContext on kernel stack
        addi sp, sp, -34*8    
    

    保存Trap上下文

    保存通用寄存器

        # save general-purpose registers
        sd x1, 1*8(sp)
        # skip sp(x2), we will save it later
        sd x3, 3*8(sp)
        # skip tp(x4), application does not use it
        # save x5~x31
        .set n, 5
        .rept 27
            SAVE_GP %n
            .set n, n+1
        .endr
    

    保存Trap上下文

    保存 sstatus 和 sepc

        # we can use t0/t1/t2 freely, because they were saved on kernel stack
        csrr t0, sstatus
        csrr t1, sepc
        sd t0, 32*8(sp)
        sd t1, 33*8(sp)
    

    保存Trap上下文

    保存 user SP

        # read user stack from sscratch and save it on the kernel stack
        csrr t2, sscratch
        sd t2, 2*8(sp)
    
    #![allow(unused)]
    fn main() {
    pub struct TrapContext {
        pub x: [usize; 32],
        pub sstatus: Sstatus,
        pub sepc: usize,
    }
    }

    调用trap_handler

        # set input argument of trap_handler(cx: &mut TrapContext)
        mv a0, sp
        call trap_handler
    

    让寄存器 a0 指向内核栈的栈指针也就是我们刚刚保存的 Trap 上下文的地址,这是由于我们接下来要调用 trap_handler 进行 Trap 处理,它的第一个参数 cx 由调用规范要从 a0 中获取。


    恢复Trap上下文

    1. 大部分是保存寄存器的反向操作;
    2. 最后一步是 sret指令 //从内核态返回到用户态

    注:后面讲解“执行程序”时会比较详细的讲解"恢复Trap上下文"


    trap_handler处理syscall

    #![allow(unused)]
    fn main() {
    #[no_mangle]
    pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
        let scause = scause::read();
        let stval = stval::read();
        match scause.cause() {
            Trap::Exception(Exception::UserEnvCall) => {
                cx.sepc += 4;
                cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
            }
        ...
        }
        cx
    }
    }

    trap_handler处理syscall

    #![allow(unused)]
    fn main() {
    pub fn sys_exit(xstate: i32) -> ! {
        println!("[kernel] Application exited with code {}", xstate);
        run_next_app()
    }
    }

    提纲

    1. 实验目标
    2. 实践步骤
    3. 软件架构
    4. 相关硬件
    5. 应用程序设计
    6. 内核程序设计

    6.1 应用管理和加载 6.2 特权级切换 6.3 Trap上下文 6.4 Trap处理流程

    6.5 执行应用程序


    应用程序的执行时机

    • 当批处理操作系统初始化完成
    • 某个应用程序运行结束或出错

    让应用程序执行

    从内核态切换到用户态

    • 准备好应用的上下文Trap上下文
    • 恢复应用的相关寄存器
    • 特别是应用用户栈指针和执行地址
    • 返回用户态让应用执行

    bg right:35% 100%


    返回用户态让应用执行

    • 从内核态切换到用户态
      • sret指令的硬件逻辑
        • 恢复响应中断/异常
        • CPU Mode从S-Mode 回到U-Mode
        • pc <-- spec CSR
        • 继续运行

    切换到下一个应用程序

    调用 run_next_app 函数切换到下一个应用程序:

    • 构造应用程序开始执行所需的 Trap 上下文;
    • 通过 __restore 函数,从刚构造的 Trap 上下文中,恢复应用程序执行的部分寄存器;
    • 设置 sepc CSR的内容为应用程序入口点 0x80400000
    • 切换 scratchsp 寄存器,设置 sp 指向应用程序用户栈;
    • 执行 sret 从 S 特权级切换到 U 特权级。

    构造Trap上下文

    #![allow(unused)]
    fn main() {
    impl TrapContext {
        pub fn set_sp(&mut self, sp: usize) { self.x[2] = sp; }
        pub fn app_init_context(entry: usize, sp: usize) -> Self {
            let mut sstatus = sstatus::read();
            sstatus.set_spp(SPP::User);
            let mut cx = Self {
                x: [0; 32],
                sstatus,
                sepc: entry,
            };
            cx.set_sp(sp);
            cx
    }

    运行下一程序

    #![allow(unused)]
    fn main() {
    ub fn run_next_app() -> ! {
        ...
        unsafe {
            app_manager.load_app(current_app);
        }
        ...
        unsafe {
            __restore(KERNEL_STACK.push_context(
                TrapContext::app_init_context(APP_BASE_ADDRESS, USER_STACK.get_sp())
            ) as *const _ as usize);
        }
        panic!("Unreachable in batch::run_current_app!");
    }
    }

    运行下一程序

    __restore:
        # case1: start running app by __restore
        # case2: back to U after handling trap
        mv sp, a0
        # now sp->kernel stack(after allocated), sscratch->user stack
        # restore sstatus/sepc
        ld t0, 32*8(sp)
        ld t1, 33*8(sp)
        ld t2, 2*8(sp)
        csrw sstatus, t0
        csrw sepc, t1
        csrw sscratch, t2
    

    运行下一程序

    # restore general-purpuse registers except sp/tp
        ld x1, 1*8(sp)
        ld x3, 3*8(sp)
        .set n, 5
        .rept 27
            LOAD_GP %n
            .set n, n+1
        .endr
        # release TrapContext on kernel stack
        addi sp, sp, 34*8
        # now sp->kernel stack, sscratch->user stack
        csrrw sp, sscratch, sp
        sret
    

    提问

    sscratch 是何时被设置为内核栈顶的?


    小结

    • OS与硬件的关系
    • OS与应用程序的关系
    • 隔离(isolation)机制
    • 批处理创建并执行程序
    • 特权级切换
    • 系统调用
    • 能写邓氏鱼OS

    bg right:57% 95%


    【总结笔记】

    本节关于基于特权级隔离的批处理操作系统的内容,我们可以将总结分为几个部分:

    实验目标与总体设计

    • 实验目标:理解和掌握基于RISC-V特权级机制的操作系统的隔离机制,包括系统调用的支持、特权级切换,以及批处理系统的基本概念。
    • 系统设计:设计一个简单的批处理操作系统,能够自动加载并执行多个程序,同时通过特权级机制保护操作系统不被应用程序影响。

    批处理操作系统的结构与实践步骤

    • 操作系统结构:一个包含了多个应用程序的单一执行镜像,通过批处理支持多个应用程序的自动加载和运行。
    • 实践步骤:包括编译、构造、运行和调试多个应用程序,以及内核程序。

    软件架构与硬件依赖

    • 软件架构:OS 和应用程序的独立编译与链接,以及系统调用的支持。
    • 硬件特性:包括RISC-V的陷阱(trap)指令和异常向量,这些是支持特权级切换和系统调用的基础。

    应用程序与内核程序设计

    • 应用程序设计:基于用户库的应用程序开发,确保了应用程序能够在用户态下安全运行。
    • 内核程序设计:包括应用管理和加载、特权级切换、陷阱上下文保存和恢复、以及应用程序的执行和系统调用处理。

    历史背景与启发

    • 历史背景:批处理操作系统的概念源于早期的计算机操作系统,如GM-NAA I/O System,以及后来的多道程序设计和操作系统设计,如MULTICS OS。

    通过本节,我们可以深入理解批处理操作系统的工作原理,包括它是如何利用RISC-V特权级机制实现隔离,以及如何自动加载并运行多个程序,这为理解更复杂的现代操作系统提供了重要的基础。同时,这也说明了现代操作系统设计中对硬件特性的依赖,以及硬件特性如何影响操作系统的功能和性能。

    第四讲 多道程序与分时多任务

    第一节 进程和进程模型

    向勇 陈渝 李国良 任炬

    2024年春季


    提纲

    1. 多道程序与协作式调度

    1. 分时多任务与抢占式调度
    2. 进程的概念
    3. 进程模型

    历史

    操作系统的被广泛使用是从大型机向小型机(minicomputer)过渡的时期开始的。

    • OS/360是大型机(System/360)时代的多道批处理操作系统
    • 数字设备公司(DEC)的PDP系列小型计算机
    • 一个工作单位内的一群人可能拥有自己的计算机
    • 多道程序(multiprogramming)变得很普遍

    多道程序(Multiprogramming)

    • 在内存中存在多个可执行程序
    • 各个可执行程序共享处理器

    作业(Job)

    • 应用的一次执行过程

    历史上出现过的术语:Job、Multiprogramming

    • Job和Multiprogramming是IBM用于多道程序设计的概念。

    协作式调度(Cooperative scheduling)

    • 可执行程序主动放弃处理器使用
    • 操作系统不会打断正在执行的程序
    • 操作系统选择下一个执行程序使用处理器

    提纲

    1. 多道程序与协作式调度

    2. 分时多任务与抢占式调度

    1. 进程的概念
    2. 进程模型

    历史

    小型机(minicomputer)的普及和广泛使用推动了分时多任务的需求,形成了支持多用户的分时操作系统。

    • DEC公司的PDP、VAX小型机逐渐侵蚀大型机市场
    • DEC公司的VMX操作系统
    • MIT的CTSS操作系统
    • AT&T的UNIX操作系统

    从用户的视角看分时多任务

    分时多任务(Time sharing multitask):从用户的视角看

    • 在内存中存在多个可执行程序
    • 各个可执行程序分时共享处理器
    • 操作系统按时间片来给各个可执行程序分配CPU使用时间
    • 进程(Process) :应用的一次执行过程

    从OS的视角看分时多任务(Time sharing multitask)

    • 进程(Process) :一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。也称为任务(Task)
    • 从一个应用程序对应的进程切换到另外一个应用程序对应的进程,称为进程切换

    作业(Job)、任务(Task)和进程(Process)

    历史上出现过的术语:Job、Task、Process

    • Task、Process是Multics和UNIX等用于分时多任务提出的概念
    • 进程是一个应用程序的一次执行过程。在操作系统语境下,任务和进程是同义词
    • 作业(目标)是围绕一个共同目标由一组相互关联的程序执行过程(进程、任务)形成的一个整体

    参考:Difference between Job, Task and Process


    抢占式调度(Preemptive scheduling)

    • 进程被动地放弃处理器使用
    • 进程按时间片轮流使用处理器,是一种“暂停-继续”组合的执行过程
    • 基于时钟硬件中断机制,操作系统可随时打断正在执行的程序
    • 操作系统选择下一个执行程序使用处理器

    提纲

    1. 多道程序与协作式调度
    2. 分时多任务与抢占式调度

    3. 进程的概念

    1. 进程模型

    进程切换

    w:1250


    进程的特点

    • 动态性
      • 开始执行-->暂停-->继续-->结束执行的过程
    • 并发性
      • 一段时间内多个进程在执行
    • 有限度的独立性
      • 进程之间不用感知对方的存在

    目前还没具备更强大的特点

    • 隔离更彻底、任务间协同工作、任务创建任务......

    进程与程序的组成

    bg w:1100


    进程与程序的组成

    bg w:700


    进程(任务)与程序的对应关系

    • 进程是操作系统处于执行状态程序的抽象

      • 程序 = 文件 (静态的可执行文件)
      • 进程 = 执行中的程序 = 程序 + 执行状态
    • 同一个程序的多次执行过程对应为不同进程

      • 如命令“ls”的多次执行对应多个进程
    • 进程执行需要的资源

      • 内存:保存代码和数据
      • CPU:执行指令

    进程与程序的区别

    • 进程是动态的,程序是静态的
      • 程序是有序代码的集合
      • 进程是程序的执行
    • 进程是暂时的,程序是永久的
      • 进程是一个状态变化的过程
      • 程序可长久保存
    • 进程与程序的组成不同
      • 进程的组成包括程序、数据和进程控制块

    进程状态

    进程包含了运行程序的所有状态信息

    • 进程执行的控制流
      • 代码内容与代码的执行位置(代码段)
    • 进程访问的数据
      • 被进程读写的内存(堆、栈、数据段)
      • 被进程读写的寄存器
        • 通用寄存器

    进程状态

    进程包含了运行程序的所有状态信息

    • 操作系统管理进程的相关数据(任务的上下文
      • 进程切换所需的通用寄存器
      • 进程切换所需的状态寄存器(PC等)
      • 其他信息:进程的栈地址等
      • 其他资源:......

    进程控制块(PCB, Process Control Block)

    操作系统管理进程的核心数据结构,也称为任务控制块(TCB, Task Control Block)

    • 操作系统管理控制进程运行所用的信息集合
    • 操作系统用PCB来描述进程的基本情况以及运行变化的过程
    • PCB是进程存在的唯一标志
    • 每个进程都在操作系统中有一个对应的PCB

    操作系统管理的进程控制块

    bg w:900


    提纲

    1. 多道程序与协作式调度
    2. 分时多任务与抢占式调度
    3. 进程的概念

    4. 进程模型


    进程状态:创建和就绪

    • 创建 --> 就绪
      • 何时创建?
      • 如何创建? bg right:50% 60%

    进程状态:运行

    • 创建-->就绪-->执行
      • 内核选择一个就绪的任务
      • 如何执行?

    bg right 70%


    进程状态:等待

    • 创建-->就绪-->执行-->等待
      • 任务进入等待的原因?
        • 自身
        • 外界

    bg right 70%


    进程状态变迁:唤醒

    • 创建-->就绪-->执行--> 等待 -->唤醒
      • 唤醒任务的原因?
        • 自身:自然醒?
        • 外界:被叫醒?

    bg right 70%


    进程状态变迁:抢占

    • 创建-->就绪-->执行--> 抢占
      • 任务被抢占的原因?

    bg right 70%


    进程状态:退出

    • 创建--> 就绪 --> 执行 --> ...... -->结束
      • 任务退出的原因?
        • 自愿?
        • 被迫?

    bg right 70%


    三状态进程模型

    w:700


    进程状态变迁与系统调用

    • 创建--> 就绪 --> 执行 --> ...... -->结束
    • 抢占 等待 唤醒

    涉及哪些系统调用?

    • exit
    • sleep
    • ...... bg right 70%

    进程状态变迁与进程切换

    • 创建--> 就绪 --> 执行 --> ...... -->结束
    • 抢占 等待 唤醒

    在任务的生命周期中,何时会进行任务切换?

    bg right 70%


    进程切换

    task-switch

    第四讲 多道程序与分时多任务

    第二节 实践:多道程序与分时多任务操作系统

    向勇 陈渝 李国良 任炬

    2024年春季


    提纲

    1. 实验目标和步骤

    1. 多道批处理操作系统设计
    2. 应用程序设计
    3. LibOS:支持应用程序加载
    4. BatchOS:支持多道程序协作调度
    5. MultiprogOS:分时多任务OS

    1.1 实验目标

    1.2 实践步骤


    实验目标

    bg right:53% 90%

    • MultiprogOS目标
      • 进一步提高系统中多个应用的总体性能和效率
    • BatchOS目标
      • 让APP与OS隔离,提高系统的安全性和效率
    • LibOS目标
      • 让应用与硬件隔离,简化应用访问硬件的难度和复杂性

    实验要求

    • 理解
      • 协作式调度和抢占式调度
      • 任务和任务切换
    • 会写
      • 多道程序操作系统
      • 分时多任务操作系统

    bg right:51% 70%


    多道程序系统的结构

    bg 55%


    总体思路

    • 编译:应用程序和内核独立编译,合并为一个镜像
    • 编译:应用程序需要各自的起始地址
    • 构造:系统调用服务请求接口,进程的管理与初始化
    • 构造:进程控制块,进程上下文/状态管理
    • 运行:特权级切换,进程与OS相互切换
    • 运行:进程通过系统调用/中断实现主动/被动切换

    历史背景

    • 1961年英国 Leo III 计算机
    • 支持在计算机内存中加载多个不同的程序,并从第一个开始依次运行

    bg right 100%


    提纲

    1. 实验目标和步骤
    2. 多道批处理操作系统设计
    3. 应用程序设计
    4. LibOS:支持应用程序加载
    5. BatchOS:支持多道程序协作调度
    6. MultiprogOS:分时多任务OS

    1.1 实验目标

    1.2 实践步骤


    实践步骤(基于BatchOS)

    • 修改APP的链接脚本(定制起始地址)
    • 加载&执行应用
    • 切换任务

    bg right 100%


    三个应用程序交替执行

    git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
    cd rCore-Tutorial-v3
    git checkout ch3-coop
    

    包含三个应用程序,大家谦让着交替执行

    user/src/bin/
    ├── 00write_a.rs # 5次显示 AAAAAAAAAA 字符串
    ├── 01write_b.rs # 2次显示 BBBBBBBBBB 字符串
    └── 02write_c.rs # 3次显示 CCCCCCCCCC 字符串
    

    运行结果

    [RustSBI output]
    [kernel] Hello, world!
    AAAAAAAAAA [1/5]
    BBBBBBBBBB [1/2]
    ....
    CCCCCCCCCC [2/3]
    AAAAAAAAAA [3/5]
    Test write_b OK!
    [kernel] Application exited with code 0
    CCCCCCCCCC [3/3]
    ...
    [kernel] Application exited with code 0
    [kernel] Panicked at src/task/mod.rs:106 All applications completed!
    

    提纲

    1. 实验目标和步骤

    2. 多道批处理操作系统设计

    1. 应用程序设计
    2. LibOS:支持应用程序加载
    3. BatchOS:支持多道程序协作调度
    4. MultiprogOS:分时多任务OS

    软件架构

    bg 70%


    代码结构:应用程序

    构建应用

    └── user
        ├── build.py(新增:使用 build.py 构建应用使得它们占用的物理地址区间不相交)
        ├── Makefile(修改:使用 build.py 构建应用)
        └── src (各种应用程序)    
    

    代码结构:完善任务管理功能

    改进OS:Loader模块加载和执行程序

    ├── os
    │   └── src
    │       ├── batch.rs(移除:功能分别拆分到 loader 和 task 两个子模块)
    │       ├── config.rs(新增:保存内核的一些配置)
    │       ├── loader.rs(新增:将应用加载到内存并进行管理)
    │       ├── main.rs(修改:主函数进行了修改)
    │       ├── syscall(修改:新增若干 syscall)
    

    代码结构:进程切换

    改进OS:TaskManager模块管理/切换程序的执行

    ├── os
    │   └── src
    │       ├── task(新增:task 子模块,主要负责任务管理)
    │       │   ├── context.rs(引入 Task 上下文 TaskContext)
    │       │   ├── mod.rs(全局任务管理器和提供给其他模块的接口)
    │       │   ├── switch.rs(将任务切换的汇编代码解释为 Rust 接口 __switch)
    │       │   ├── switch.S(任务切换的汇编代码)
    │       │   └── task.rs(任务控制块 TaskControlBlock 和任务状态 TaskStatus 的定义)
    

    提纲

    1. 实验目标和步骤
    2. 多道批处理操作系统设计

    3. 应用程序设计

    1. LibOS:支持应用程序加载
    2. BatchOS:支持多道程序协作调度
    3. MultiprogOS:分时多任务OS

    应用程序项目结构

    没有更新 应用名称有数字编号

    user/src/bin/
    ├── 00write_a.rs # 5次显示 AAAAAAAAAA 字符串
    ├── 01write_b.rs # 2次显示 BBBBBBBBBB 字符串
    └── 02write_c.rs # 3次显示 CCCCCCCCCC 字符串
    

    应用程序的内存布局

    • 由于每个应用被加载到的位置都不同,也就导致它们的链接脚本 linker.ld 中的 BASE_ADDRESS 都是不同的。
    • 写一个脚本定制工具 build.py ,为每个应用定制了各自的链接脚本
      • 应用起始地址 = 基址 + 数字编号 * 0x20000

    yield系统调用

    //00write_a.rs
    fn main() -> i32 {
        for i in 0..HEIGHT {
            for _ in 0..WIDTH {
                print!("A");
            }
            println!(" [{}/{}]", i + 1, HEIGHT);
            yield_(); //放弃处理器 
        }
        println!("Test write_a OK!");
        0
    }

    yield系统调用

    • 应用之间是相互不知道
    • 应用需要主动让出处理器
    • 需要通过新的系统调用实现
      • const SYSCALL_YIELD: usize = 124;

    yield系统调用

    const SYSCALL_YIELD: usize = 124;
    pub fn sys_yield() -> isize {
        syscall(SYSCALL_YIELD, [0, 0, 0])
    }
    pub fn yield_() -> isize {
        sys_yield()
    }
    

    提纲

    1. 实验目标和步骤
    2. 多道批处理操作系统设计
    3. 应用程序设计

    4. LibOS:支持应用程序加载

    1. BatchOS:支持多道程序协作调度
    2. MultiprogOS:分时多任务OS

    LibOS:支持应用程序加载

    LibOS支持在内存中驻留多个应用,形成多道程序操作系统;

    bg right:57% 100%


    多道程序加载

    • 应用的加载方式有不同
    • 所有的应用在内核初始化的时候就一并被加载到内存中
    • 为了避免覆盖,它们自然需要被加载到不同的物理地址

    多道程序加载

    fn get_base_i(app_id: usize) -> usize {
        APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
    }
    
    let base_i = get_base_i(i);
    // load app from data section to memory
    let src = (app_start[i]..app_start[i + 1]);
    let dst = (base_i.. base_i+src.len());
    dst.copy_from_slice(src);
    

    执行程序

    • 执行时机

      • 当多道程序的初始化放置工作完成
      • 某个应用程序运行结束或出错的时
    • 执行方式

      • 调用 run_next_app 函数切换到第一个/下一个应用程序

    切换下一个程序

    • 内核态到用户态
    • 用户态到内核态

    切换下一个程序

    • 跳转到编号i的应用程序编号i的入口点 entry(i)
    • 将使用的栈切换到用户栈stack(i)

    bg right:55% 90%


    执行程序

    现在完成了支持把应用都放到内存中的LibOS。 bg right:57% 95%


    提纲

    1. 实验目标和步骤
    2. 多道批处理操作系统设计
    3. 应用程序设计
    4. LibOS:支持应用程序加载

    5. BatchOS:支持多道程序协作调度

    1. MultiprogOS:分时多任务OS

    5.1 任务切换

    5.2 Trap控制流切换 5.3 协作式调度


    支持多道程序协作式调度

    协作式多道程序:应用程序主动放弃 CPU 并切换到另一个应用继续执行,从而提高系统整体执行效率;

    bg right:54% 90%


    任务切换

    bg 70%


    进程

    • 进程(Process) :一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。也称为任务(Task)bg right 100%

    时间片(time slice)

    • 应用执行过程中的一个时间片段称为时间片(time slice)

    任务片(task slice)

    • 应用执行过程中的一个时间片段上的执行片段或空闲片段,称为 “ 计算任务片 ” 或“ 空闲任务片 ”,统称任务片(task slice) bg right 100%

    任务运行状态

    • 在一个时间片内的应用执行情况
      • running
      • ready
    #![allow(unused)]
    fn main() {
    pub enum TaskStatus {
        UnInit,
        Ready,
        Running,
        Exited,
    }
    }

    bg right:65% 100%


    任务切换

    • 从一个应用的执行过程切换到另外一个应用的执行过程
      • 暂停一个应用的执行过程(当前任务)
      • 继续另一应用的执行过程(下一任务) bg right:65% 100%

    任务上下文(Task Context)

    • 应用运行在某一时刻的执行状态(上下文)
      • 应用要暂停时,执行状态(上下文)可以被保存
      • 应用要继续时,执行状态(上下文)可以被恢复
    #![allow(unused)]
    fn main() {
    1// os/src/task/context.rs
    2 pub struct TaskContext {
    3    ra: usize,      //函数返回地址
    4    sp: usize,      //task内核栈指针
    5    s: [usize; 12], //属于Callee函数保存的寄存器集s0~s11
    6}
    }

    任务上下文和trap上下文数据结构

    #![allow(unused)]
    fn main() {
    1// os/src/task/context.rs
    2 pub struct TaskContext {
    3    ra: usize,
    4    sp: usize,
    5    s: [usize; 12],
    6}
    }
    #![allow(unused)]
    fn main() {
    // os/src/trap/context.rs
    pub struct TrapContext {
        pub x: [usize; 32],
        pub sstatus: Sstatus,
        pub sepc: usize,
    }
    }

    bg right:60% 100%


    不同类型上下文

    • 函数调用上下文
    • Trap上下文
    • 任务(Task)上下文

    bg right:60% 90%


    任务(Task)上下文 vs 系统调用(Trap)上下文

    任务切换是来自两个不同应用在内核中的 Trap 控制流之间的切换

    • 任务切换不涉及特权级切换;Trap切换涉及特权级切换;
    • 任务切换只保存编译器约定的callee 函数应该保存的部分寄存器;而Trap切换需要保存所有通用寄存器;
    • 任务切换和Trap切换都是对应用是透明
    • Trap切换需要硬件参与,任务切换完全由软件完成;

    控制流

    • 程序的控制流 (Flow of Control or Control Flow) --编译原理
      • 以一个程序的指令、语句或基本块为单位的执行序列
    • 处理器的控制流 --计算机组成原理
      • 处理器中程序计数器的控制转移序列

    普通控制流:从应用程序员的角度来看控制流

    • 普通控制流 (CCF,Common Control Flow) 是指程序中的常规控制流程,比如顺序执行、条件判断、循环等基本结构。是程序员编写的程序的执行序列,这些序列是程序员预设好的
      • 普通控制流是可预测的。
      • 普通控制流是程序正常运行所遵循的流。

    异常控制流:从操作系统程序员的角度来看控制流

    • 应用程序在执行过程中,如果发出系统调用请求,或出现外设中断、CPU 异常等情况,会出现前一条指令还在应用程序的代码段中,后一条指令就跑到操作系统的代码段中去了。
    • 这是一种控制流的“突变”,即控制流脱离了其所在的执行环境,并产生执行环境的切换
    • 这种“突变”的控制流称为 异常控制流 (ECF, Exceptional Control Flow) 。
      • 在RISC-V场景中,异常控制流 == Trap控制流

    控制流上下文(执行环境的状态)

    从硬件的角度来看普通控制流或异常控制流的执行过程

    • 从控制流起始的某条指令执行开始,指令可访问的所有物理资源的内容,包括自带的所有通用寄存器、特权级相关特殊寄存器、以及指令访问的内存等,会随着指令的执行而逐渐发生变化。
    • 把控制流在执行完某指令时的物理资源内容,即确保下一时刻能继续正确执行控制流指令的物理/虚拟资源内容称为控制流上下文 (Context) ,也可称为控制流所在执行环境的状态。

    对于当前实践的OS,没有虚拟资源,而物理资源内容就是通用寄存器/CSR寄存器


    控制流上下文(执行环境的状态)

    • 函数调用上下文
      • 函数调用(执行函数切换)过程中的控制流上下文
    • 中断/异常/陷入上下文
      • 操作系统中处理中断/异常/陷入的切换代码时的控制流的上下文
    • 任务(进程)上下文
      • 操作系统中任务(进程)执行相关切换代码时的控制流的上下文

    提纲

    1. 实验目标和步骤
    2. 多道批处理操作系统设计
    3. 应用程序设计
    4. LibOS:支持应用程序加载
    5. BatchOS:支持多道程序协作调度
    6. MultiprogOS:分时多任务OS

    5.1 任务切换

    5.2 Trap控制流切换

    5.3 协作式调度


    OS面临的挑战:任务切换

    在分属不同任务的两个Trap控制流之间进行hacker级操作,即进行Trap上下文切换,从而实现任务切换。

    • Trap上下文在哪?
    • 任务上下文在哪?
    • 如何切换任务?
    • 任务切换应该发生在哪?
    • 任务切换后还能切换回吗?

    bg right 95%


    Trap控制流切换:暂停运行

    • 一个特殊的函数__switch()
    • 调用 __switch() 之后直到它返回前的这段时间,原 Trap 控制流 A 会先被暂停并被切换出去, CPU 转而运行另一个应用在内核中的 Trap 控制流 B 。 bg right 95%

    Trap控制流切换:恢复运行

    • 一个特殊的函数 __switch()
    • 然后在某个合适的时机,原 Trap 控制流 A 才会从某一条 Trap 控制流 C (很有可能不是它之前切换到的 B )切换回来继续执行并最终返回。

    从实现的角度讲, __switch() 函数和一个普通的函数之间的核心差别仅仅是它会换栈bg right 95%


    Trap控制流切换函数__switch()

    w:800


    Trap控制流切换过程:切换前的状态

    阶段[1]:在 Trap 控制流 A 调用__switch()之前,A 的内核栈上只有 Trap 上下文和 Trap 处理函数的调用栈信息,而 B 是之前被切换出去的; bg right:55% 100%


    Trap控制流切换过程:保存A任务上下文

    阶段 [2]:A 在 A 任务上下文空间在里面保存 CPU 当前的寄存器快照bg right:55% 100%


    Trap控制流切换过程:恢复B任务上下文

    阶段 [3]:读取 next_task_cx_ptr 指向的 B 任务上下文,恢复 ra 寄存器、s0~s11 寄存器以及 sp 寄存器。

    • 这一步做完后, __switch() 才能做到一个函数跨两条控制流执行,即 通过换栈也就实现了控制流的切换 。 bg right:55% 100%

    Trap控制流切换过程:执行B任务代码

    阶段 [4]:当 CPU 执行 ret 汇编伪指令完成 __switch() 函数返回后,任务 B 可以从调用 __switch() 的位置继续向下执行。

    • __switch()通过恢复 sp 寄存器换到了任务 B 的内核栈上,实现了控制流的切换,从而做到一个函数跨两条控制流执行。 bg right:53% 100%

    __switch()的接口

     1 // os/src/task/switch.rs
     2 
     3 global_asm!(include_str!("switch.S"));
     4 
     5 use super::TaskContext;
     6 
     7 extern "C" {
     8     pub fn __switch(
     9         current_task_cx_ptr: *mut TaskContext,
    10         next_task_cx_ptr: *const TaskContext
    11     );
    12 }
    

    __switch()的实现

    12 __switch:
    13    # 阶段 [1]
    14    # __switch(
    15    #     current_task_cx_ptr: *mut TaskContext,
    16    #     next_task_cx_ptr: *const TaskContext
    17    # )
    18    # 阶段 [2]
    19    # save kernel stack of current task
    20    sd sp, 8(a0)
    21    # save ra & s0~s11 of current execution
    22    sd ra, 0(a0)
    23    .set n, 0
    24    .rept 12
    25        SAVE_SN %n
    26        .set n, n + 1
    27    .endr
    
    

    __switch()的实现

    28    # 阶段 [3]
    29    # restore ra & s0~s11 of next execution
    30    ld ra, 0(a1)
    31    .set n, 0
    32    .rept 12
    33        LOAD_SN %n
    34        .set n, n + 1
    35    .endr
    36    # restore kernel stack of next task
    37    ld sp, 8(a1)
    38    # 阶段 [4]
    39    ret
    

    提纲

    1. 实验目标和步骤
    2. 多道批处理操作系统设计
    3. 应用程序设计
    4. LibOS:支持应用程序加载
    5. BatchOS:支持多道程序协作调度
    6. MultiprogOS:分时多任务OS

    5.1 任务切换 5.2 Trap控制流切换

    5.3 协作式调度


    任务控制块

    操作系统管理控制进程运行所用的信息集合

    pub struct TaskControlBlock {
        pub task_status: TaskStatus,
        pub task_cx: TaskContext,
    }
    
    • 任务管理模块
    struct TaskManagerInner {
        tasks: [TaskControlBlock; MAX_APP_NUM],
        current_task: usize,
    }
    

    bg right:50% 100%


    协作式调度

    • sys_yieldsys_exit系统调用
    #![allow(unused)]
    fn main() {
    pub fn sys_yield() -> isize {
        suspend_current_and_run_next();
        0
    }
    pub fn sys_exit(exit_code: i32) -> ! {
        println!("[kernel] Application exited with code {}", exit_code);
        exit_current_and_run_next();
        panic!("Unreachable in sys_exit!");
    }
    }

    协作式调度

    • sys_yieldsys_exit系统调用
    #![allow(unused)]
    fn main() {
    // os/src/task/mod.rs
    
    pub fn suspend_current_and_run_next() {
        mark_current_suspended();
        run_next_task();
    }
    
    pub fn exit_current_and_run_next() {
        mark_current_exited();
        run_next_task();
    }
    }

    协作式调度

    • sys_yieldsys_exit系统调用
     fn run_next_task(&self) {
        ......
        unsafe {
            __switch(
                current_task_cx_ptr, //当前任务上下文
                next_task_cx_ptr,    //下个任务上下文
            );
        }
    

    第一次进入用户态

    Q:如何实现?

    如果能搞定,我们就实现了支持多道程序协作调度的BatchOS


    提纲

    1. 实验目标和步骤
    2. 多道批处理操作系统设计
    3. 应用程序设计
    4. LibOS:支持应用程序加载
    5. BatchOS:支持多道程序协作调度

    6. MultiprogOS:分时多任务OS


    MultiprogOS:分时多任务OS

    BatchOS可抢占应用的执行,从而可以公平和高效地分时执行多个应用,提高系统的整体效率。

    bg right:52% 100%


    MultiprogOS的基本思路

    • 设置时钟中断
    • 在收到时钟中断后统计任务的使用时间片
    • 在时间片用完后,切换任务 bg right:58% 100%

    时钟中断与计时器

    • 设置时钟中断
    #![allow(unused)]
    fn main() {
    // os/src/sbi.rs
    pub fn set_timer(timer: usize) {
         sbi_call(SBI_SET_TIMER, timer, 0, 0);
     }
    // os/src/timer.rs
    pub fn set_next_trigger() {
        set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
    }
    pub fn rust_main() -> ! {
        trap::enable_timer_interrupt();
        timer::set_next_trigger();
    }    
    }

    抢占式调度

    #![allow(unused)]
    fn main() {
    // os/src/trap/mod.rs trap_handler函数
    ......
    match scause.cause() {
        Trap::Interrupt(Interrupt::SupervisorTimer) => {
            set_next_trigger();
            suspend_current_and_run_next();
        }
    }
    }

    这样我们就实现了分时多任务的腔骨龙操作系统


    小结

    • 多道程序&分时共享多任务
    • 协作式调度&抢占式调度
    • 任务与任务切换
    • 中断机制

    课程实验一

    • 实验任务:增加一个系统调用sys_task_info()
    • 实验提交要求
      • 在自己的已创建实验仓库中提交完整的代码和文档;
      • 在荷塘雨课中提交实验一报告链接和commit ID;
      • 实验截止时间:布置实验任务后的第13天(10月22日24点);

    第五讲 物理内存管理

    第一节 地址空间

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 计算机的存储层次

    1. 地址和地址空间
    2. 虚拟存储的作用

    物理地址和逻辑地址

    • 物理地址(PA, Physical Address) :用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
    • 逻辑地址(LA, Logical Address) :CPU执行机器指令时,用来指定一个操作数或者是一条指令的地址。也是用户编程时使用的地址。
    • 线性地址(linear address)或也叫虚拟地址(virtual address) :跟逻辑地址类似,它也是一个不真实的地址。
      • 逻辑地址指CPU在段式内存管理转换前的地址;
      • 线性地址指CPU在页式内存管理转换前的地址。

    逻辑地址与物理地址的关系

    • 逻辑地址到物理地址的转换
      • 逻辑地址 -> 线性地址(虚拟地址) -> 物理地址
    • 在没有段式内存管理的情况下,逻辑地址与虚拟地址相同
    • 在没有页式内存管理的情况下,逻辑地址、虚拟地址和物理地址都相同

    计算机的存储层次结构

    w:800


    计算机的存储多层结构

    w:950


    操作系统对内存资源的抽象

    w:950


    内存管理

    • 操作系统中的内存管理方式
      • 重定位(relocation)
      • 分段(segmentation)
      • 分页(paging)
      • 虚拟存储(virtual memory/storage)
    • 操作系统的内存管理高度依赖硬件
      • 与计算机存储架构紧耦合
      • MMU (内存管理单元): 处理CPU存储访问请求的硬件

    提纲

    1. 计算机的存储层次

    2. 地址和地址空间

    1. 虚拟存储的作用

    地址空间的定义

    • 物理地址空间:物理内存的地址空间
      • 起始地址$0$,直到 $MAX_{phy}$
    • 虚拟地址空间:虚拟内存的地址空间
      • 起始地址$0$,直到 $MAX_{virt}$
    • 逻辑地址空间:程序执行的地址空间
      • 起始地址$0$, 直到 $MAX_{prog}$

    三种地址空间的视角不同


    逻辑地址生成

    w:950


    地址生成时机

    • 编译时
      • 假设起始地址已知
      • 如果起始地址改变,必须重新编译
    • 加载时
      • 如编译时起始地址未知,编译器需生成可重定位的代码 (relocatable code)
      • 加载时,位置可不固定,生成绝对(虚拟)地址
    • 执行时
      • 执行时代码不可修改
      • 地址转换(映射)硬件支持

    地址生成过程

    • CPU
      • ALU:需要逻辑地址的内存内容
      • MMU:进行逻辑地址和物理地址的转换
      • CPU控制逻辑:给总线发送物理地址请求
    • 内存
      • 发送物理地址的内容给CPU
      • 接收CPU数据到物理地址
    • 操作系统
      • 建立逻辑地址LA和物理地址PA的映射

    地址检查

    bg w:950


    提纲

    1. 计算机的存储层次
    2. 地址和地址空间

    3. 虚拟存储的作用


    外存的缓存

    虚拟内存可作为外存的缓存

    • 常用数据放在物理内存中
    • 不常用数据放在外存
    • 运行的程序直接用虚存地址,不用关注具体放在物理内存还是外存

    bg right:49% 95%


    简化应用编译和加载运行

    每个运行程序具有独立的地址空间,而不管代码和数据在物理内存的实际存放,从而简化:

    • 编译的执行程序链接
    • 操作系统的执行程序加载
    • 共享:动态链接库、共享内存
    • 内存分配:物理不连续,虚拟连续 bg right:43% 100%

    保护数据

    虚拟内存可保护数据

    • 独立的地址空间使得区分不同进程各自内存变得容易
    • 地址转换机制可以进行可读/可写/可执行的检查
    • 地址转换机制可以进行特权级检查 bg right:49% 100%

    第五讲 物理内存管理

    第二节 内存分配

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 内存分配

    1. 连续内存分配
    2. 非连续内存分配

    内存分配方式

    运行应用所占内存按存储数据特征划分成多个段(Segment)

    • 内存分配方式
      • 静态内存分配
      • 动态内存分配
        • 连续内存分配
        • 非连续内存分配
    • 内存管理的目的
      • 让应用方便/灵活/高效地使用有限内存

    bg right:35% 150%


    动态内存分配接口

    w:850


    静态内存分配

    静态内存分配是指编译时的内存分配

    • 包括全局、静态变量和代码
    • 位于全局/静态数据段、常量数据段、代码段

    bg right:55% 140%


    动态内存分配

    动态内存分配是指运行时的内存分配

    • 栈(stack)
      • 局部变量
    • 堆(heap)
      • malloc()函数分配内存
      • free()函数释放内存

    bg right:53% 140%


    使用动态内存分配的原因

    无法事先确定程序运行所需要的内存大小。

    • 经常直到程序实际运行时,才知道某些数据结构的大小
    • 在大型软件代码中硬编码数据大小会是一种噩梦

    bg right:53% 140%


    动态内存分配方式的分类

    • 显式分配(explicit allocation)
      • 要求应用显式地释放任何已分配的块,例如malloc,free
    • 隐式分配(implicit allocation)
      • 编译器/运行时库自动释放未使用的已分配的块
      • 隐式分配器称为垃圾收集器(Garbage Collector),比如Java

    bg right:49% 140%


    堆和栈的内存分配

    分配方式:动态内存分配

    • 栈由编译器管理:隐式分配
    • 堆的分配和释放由程序员管理:显式分配

    分配大小

    • 栈是由高地址向低地址生长的数据结构,是一块连续的内存,能从栈中获得的内存较小,编译期间确定大小;
    • 堆是由低地址向高地址生长的数据结构,是一个不连续的储存空间,内存获取比较灵活,也较大

    动态内存分配函数malloc()

    • malloc()函数: void * malloc (size_ t size);

      • 申请一块size大小的连续堆内存
      • 函数返回值是一个指针,指向刚分配的内存首地址
      • 如果申请内存失败, 返回一个空指针,即返回值为NULL
    • 动态内存的分配和释放必须成对使用

      • 如果malloc()free()多,会造成内存泄漏
      • 如果malloc()free()少,会造成二次删除,破坏内存,导致程序崩溃

    动态内存回收函数free()

    • free()函数:void free (void *ptr)
      • 释放指针变量在堆区上的内存空间
      • 不能释放栈上的内存空间
      • free()要与malloc()成对使用

    提纲

    1. 内存分配

    2. 连续内存分配

    1. 非连续内存分配

    2.1 动态分区分配

    2.2 伙伴系统(Buddy System)


    连续内存分配

    给应用分配一块不小于指定大小连续的内存区域

    • 内存碎片:不能被利用的空闲内存
      • 外碎片:分配单元间未被使用内存
      • 内碎片:分配单元内部未被使用内存

    bg right:44% 95%


    动态分区分配

    当程序被加载执行时或运行中数据存储时,分配一个进程指定大小可变的分区(内存块)

    • 分区的地址是连续的
    • 用户库/操作系统需要维护的数据结构
      • 已分配分区:已分配给应用的分区
      • 空闲分区(Empty-blocks)

    bg right:35% 100%


    动态分区分配的设计要解决的问题

    • 空闲块组织:如何记录和组织空闲块?
    • 放置位置:如何选择合适的空闲块来分配?
    • 分割:如何处理没分配完的空闲块中的剩余部分?
    • 合并:如何处理一个刚刚被释放的块?

    bg right:42% 100%


    动态分区分配策略

    • 最先匹配(First-fit)
    • 最佳匹配(Best-fit)
    • 最差匹配(Worst-fit)

    bg right:42% 100%


    最先匹配(First Fit)分配策略

    • 优点:简单,在高地址空间有大块的空闲分区
    • 缺点:外碎片,分配大块时较慢
    • 示例:分配400字节, 使用第1个空闲块

    bg right 30% 80%


    最佳匹配(Best Fit)分配策略

    • 分配n字节分区时, 查找并使用不小于n的最小空闲分区

    • 释放分区时,检查是否可与临近的空闲分区合并

    • 示例:分配400字节, 使用第3个空闲块(最小) bg right:50% 80%


    最佳匹配(Best Fit)分配策略

    • 优点:多数申请分配的尺寸较小时,效果很好
    • 缺点:外碎片,释放分区较慢,容易产生很多无用的小碎片

    bg right:50% 80%


    最差匹配(Worst Fit)分配策略

    • 分配n字节时,使用尺寸大于n的最大空闲分区

    • 释放分区时,检查是否可与临近的空闲分区合并

    • 示例:分配400字节,使用第2个空闲块(最大) bg right:48% 80%


    最差匹配(Worst Fit)分配策略

    • 优点:中等大小的分配较多时,效果最好
    • 缺点:外碎片释放分区较慢,容易破坏大的空闲分区

    bg right 80%


    提纲

    1. 内存分配
    2. 连续内存分配
    3. 非连续内存分配

    2.1 动态分区分配

    2.2 伙伴系统(Buddy System)


    伙伴系统(Buddy System)的需求背景

    • 观察&分析
      • 基本的分配策略很简单和通用,但性能差,外碎片多
      • 内核和应用的内存需求特征
        • 内核经常以$2^U$个4KB大小来分配和释放连续地址的内存块
        • 需要能快速地分配和释放,且不会产生外碎片
    • 需要新的连续内存分配策略

    伙伴系统的工作原理

    bg w:950


    分区大小

    • 可分配分区的大小 $2^U$
    • 待分配分区的大小为$2^{(U-1)} < s ≤ 2^U$
      • 把整块分配给应用;
    • 待分配分区的大小为$s ≤2^{(i-1)}$
      • 将大小为$2^i$ 的当前空闲分区划分成两个大小为$2^{(i-1)}$空闲分区
      • 重复划分过程,直到$2^{(i-1)} < s ≤ 2^i$,把一个空闲分区分配出去

    分配过程

    • 数据结构
      • 空闲块按大小和起始地址组织成二维数组
      • 初始状态:只有一个大小为$2^U$的空闲块
    • 分配过程
      • 由小到大在空闲块中找最小可用块
      • 如空闲块过大,对可用空闲块进行二等分,直到得到合适可用空闲块

    释放过程

    • 释放过程
      • 把块放入空闲块数组
      • 合并满足条件的空闲块
    • 合并条件
      • 大小相同$2^i$
      • 地址相邻
      • 低地址空闲块起始地址为$2^{(i+1)}$的位数

    伙伴系统工作过程示例

    w:1100


    伙伴系统工作过程示例

    w:1100


    伙伴系统工作过程示例

    w:1100


    伙伴系统工作过程示例

    w:1100


    伙伴系统工作过程示例

    w:1100


    伙伴系统的参考实现


    课后练习

    用C或Rust语言实现如下的改进伙伴系统。(已有参考实现

    修改伙伴系统分配操作:

    • 由小到大在空闲块中找最小可用块
    • 如空闲块大于申请块的二倍,对可用空闲块进行二等分,直到得到合适可用空闲块
    • 如空闲块大于申请块的4/3,对可用空闲块进行四等分,直到得到合适可用空闲块

    分析“改进伙伴系统”的存储利用率和分配释放开销。


    提纲

    1. 内存分配
    2. 连续内存分配

    3. 非连续内存分配

    3.1 非连续内存分配的概念

    3.2 页式存储管理 3.3 内存分配示例


    非连续内存分配的需求背景:碎片问题

    • 内核通过页表能够把多个地址不连续的物理页转换为地址连续的多个虚拟页
    • 提供给应用程序和内核自身使用地址连续的虚拟内存块,这样可以比较轻松地解决内存分配的碎片问题

    bg right:50% 100%


    非连续内存分配的需求背景:较大的内存空间

    • 创建运行的程序时需要分配让其正常运行所需的比较大的内存空间
    • 程序运行时会需要动态申请和释放比较大的内存空间
      • 通常用户库发出请求
      • 减少系统调用次数
      • 一次申请$2^U$MB大小(如:64MB)的内存

    bg right:43% 100%


    非连续内存分配的设计目标

    提高内存利用效率和管理灵活性

    • 允许一个程序使用非连续的物理地址空间
    • 允许共享代码与数据
    • 支持动态加载和动态链接

    bg right:50% 100%


    非连续分配需要解决的问题

    • 虚拟地址到物理地址的地址转换
      • 软件实现 (灵活,开销大)
      • 硬件实现 (够用,开销小)
    • 非连续分配的硬件辅助机制
      • 如何选择非连续分配中内存块大小
        • 段式存储管理(segmentation)
        • 页式存储管理(paging)

    bg right:40% 100%


    段式存储管理

    程序运行的段地址空间由多个段组成

    • 主代码段、子模块代码段、公共库代码段、栈段、堆数据(heap)...

    w:700


    段表

    • 位于内存中
    • 由内核管理
    • 与任务/进程对应 bg right:71% 100%

    提纲

    1. 内存分配
    2. 连续内存分配
    3. 非连续内存分配

    3.1 非连续内存分配的概念

    3.2 页式存储管理

    3.3 内存分配示例


    物理页面和逻辑页面

    • 物理页面(页帧、帧, Frame, Page Frame)
      • 把物理地址空间划分为大小相同(2^n)的基本分配单位
    • 逻辑页面(页面、页, Page)
      • 把逻辑地址空间也划分为相同大小的基本分配单位
      • 物理页面和逻辑页面的基本单位大小是相同的
    • 逻辑页面到物理页面的对应
      • 逻辑地址到物理地址的地址转换
      • 硬件机制:页表/MMU/TLB

    页表

    • 位于内存中
    • 由内核管理
    • 与任务/进程对应 bg right:71% 100%

    页式存储管理面临的性能挑战

    • 内存访问性能
      • 访问一个内存单元需要2次内存访问
        • 第一次访问:获取页表项
        • 第二次访问:访问数据
    • 页表大小
      • 页表可能非常大

    提高页式存储管理性能的方法

    • 缓存(Caching)
    • 间接(Indirection)访问 w:1000

    多级页表

    bg w:1000


    多级页表的地址转换

    w:1000


    反置页表

    基于Hash映射值查找对应页表项中的物理页号

    • 任务/进程id与页号的Hash值可能有冲突
    • 页表项中包括保护位、修改位、访问位和存在位等标识

    反置页表的地址转换

    w:1100


    反置页表的hash冲突

    w:1100


    反置页表的hash冲突

    w:1100


    反置页表的hash冲突

    w:1000


    段页式存储管理

    • 段式存储在内存保护方面有优势,页式存储在内存利用和优化转移到后备存储方面有优势。
    • 段式存储、页式存储能否结合?

    段页式存储管理

    w:1000


    提纲

    1. 内存分配
    2. 连续内存分配
    3. 非连续内存分配

    3.1 非连续内存分配的概念 3.2 页式存储管理

    3.3 内存分配示例


    一个app调用malloc的例子

    #include <stdlib.h>
    int main(){
    int *ptr;
    ptr = malloc(15 * sizeof(*ptr)); /* a block of 15 integers */
        if (ptr != NULL) {
          *(ptr + 5) = 480; /* assign 480 to sixth integer */
          printf("Value of the 6th integer is %d",*(ptr + 5));
        }
    }
    

    app的地址空间

    bg right:68% 120%


    加载程序运行

    第一步:OS加载程序运行 w:1000


    malloc函数调用

    第二步:程序发出malloc函数调用,且Lib库有空闲空间 w:700


    内核分配内存空间

    第二步:程序发出malloc函数调用,且Lib库无空闲空间 w:700

    第五讲 物理内存管理

    第三节 实践:建立地址空间的OS

    Address Space OS(ASOS)

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 实验目标和步骤

    1. 系统架构
    2. 用户视角的地址空间
    3. 内核管理地址空间
    4. 实现ASOS

    1.1 实验目标

    1.2 实践步骤


    以往目标

    提高性能、简化开发、加强安全

    • multiprog & time-sharing OS目标
      • 让APP有效共享CPU,提高系统总体性能和效率
    • BatchOS目标
      • 让APP与OS隔离,加强系统安全,提高执行效率
    • LibOS目标
      • 让APP与硬件隔离,简化应用访问硬件的难度和复杂性

    实验目标

    提高性能、简化开发、加强安全、

    • 简化编程,APP不用考虑其运行时的起始执行地址
      • 与编译器达成共识,给每个APP设定一个固定起始执行地址
    • 隔离APP访问的内存地址空间
      • 给APP的内存地址空间划界,不能越界访问OS和其他APP

    实验要求

    • 理解地址空间
    • 掌握页机制
    • 会处理页访问异常
    • 会写支持页机制的操作系统

    bg right 80%


    总体思路

    bg 65%


    总体思路

    • 编译:应用程序和内核独立编译,合并为一个镜像
    • 编译:不同应用程序可采用统一的起始地址
    • 构造:系统调用服务,任务的管理与初始化
    • 构造:建立基于页表机制的虚存空间
    • 运行:特权级切换,任务与OS相互切换
    • 运行:切换地址空间,跨地址空间访问数据

    历史背景

    • 在 1940 年出现了两级存储系统
      • 主存:磁芯;辅助:磁鼓
    • 提出虚拟内存Virtual memory技术概念
      • 德国的柏林工业大学博士生 Fritz-Rudolf Güntsch
    • 1959 年的Atlas Supervisor 操作系统
      • 英国曼彻斯特大学的 Tom Kilburn 教授团队展示了 Atlas 计算机和 Atlas Supervisor 操作系统
      • 创造出分页(paging)技术和虚拟内存技术(virtual memory,当时称为 one-level storage system)

    提纲

    1. 实验目标和步骤
    2. 系统架构
    3. 用户视角的地址空间
    4. 内核管理地址空间
    5. 实现ASOS

    1.1 实验目标

    1.2 实践步骤


    实践步骤

    • 修改APP的链接脚本(定制起始地址)
    • 加载&执行应用
    • 切换任务和任务的地址空间

    编译步骤

    git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
    cd rCore-Tutorial-v3
    git checkout ch4
    cd os
    make run
    

    输出结果

    Into Test load_fault, we will insert an invalid load operation...
    Kernel should kill this application!
    [kernel] PageFault in application, bad addr = 0x0, bad instruction = 0x1009c, kernel killed it.
    
    store_fault APP running...
    
    Into Test store_fault, we will insert an invalid store operation...
    Kernel should kill this application!
    [kernel] PageFault in application, bad addr = 0x0, bad instruction = 0x1009c, kernel killed it.
    power_3 [130000/300000]
    

    测试应用

    其中包含两个应用程序04load_fault, 05store_fault

    // usr/src/bin/04load_fault.rs
    ......
        unsafe {
            let _i=read_volatile(null_mut::<u8>());
        }
    
    // usr/src/bin/05store_fault.rs
    ......
        unsafe {
           null_mut::<u8>().write_volatile(1);
        }
    

    提纲

    1. 实验目标和步骤

    2. 系统架构

    1. 用户视角的地址空间
    2. 内核管理地址空间
    3. 实现ASOS

    2.1 代码结构

    2.2 RISC-V SV39页机制


    软件架构

    • 简化应用
    • 建立Paging
    • 内核页表
    • 应用页表
    • 信息传递
    • 跳板机制
    • 扩展TCB
    • 扩展异常

    bg right:65% 100%


    构建应用

    └── user
        ├── build.py(移除:给应用设定唯一起始地址的脚本)
        └── src(用户态库和应用程序)
            ├── bin(各个应用程序)
            ├── ...
            └── linker.ld(修改:将所有应用放在各自地址空间中固定的位置)
    

    地址空间

    ├── os
        └── src
             ├── config.rs(修改:新增一些内存管理的相关配置)
             ├── linker-k210.ld(修改:将跳板页引入内存布局)
             ├── linker-qemu.ld(修改:将跳板页引入内存布局)
             ├── loader.rs(修改:仅保留获取应用数量和数据的功能)
             ├── main.rs(修改)
    

    mm子模块

    ├── os
        └── src
             ├── mm(新增:内存管理的 mm 子模块)
                 ├──address.rs(物理/虚拟 地址/页号的 Rust 抽象)
                 ├──frame_allocator.rs(物理页帧分配器)
                 ├──heap_allocator.rs(内核动态内存分配器)
                 ├──memory_set.rs(引入地址空间 MemorySet 及逻辑段 MemoryArea 等)
                 ├──mod.rs(定义了 mm 模块初始化方法 init)
                 └──page_table.rs(多级页表抽象 PageTable 以及其他内容)
    

    改进OS

    ├── os
        └── src
             ├── syscall
                 ├──fs.rs(修改:基于地址空间的 sys_write 实现)
             ├── task
                 ├──context.rs(修改:构造一个跳转到不同位置的初始任务上下文)
                 ├──mod.rs(修改)
                 └──task.rs(修改)
             └── trap
                 ├── context.rs(修改:在 Trap 上下文中加入了更多内容)
                 ├── mod.rs(修改:基于地址空间修改了 Trap 机制)
                 └── trap.S(修改:基于地址空间修改了 Trap 上下文保存与恢复汇编代码) 
    

    提纲

    1. 实验目标和步骤
    2. 系统架构
    3. 用户视角的地址空间
    4. 内核管理地址空间
    5. 实现ASOS

    2.1 代码结构

    2.2 RISC-V SV39页机制


    RISC-V 基于SATP的虚拟内存系统

    • 虚拟地址将内存划分为固定大小的页来进行地址转换和内容保护。
    • 页表基址寄存器satp:内核态控制状态寄存器控制了分页。satp 有三个域:
      • MODE 域:开启分页并选择页表级数
      • ASID(Address Space Identifier,地址空间标识符)域:可选的,用来降低上下文切换的开销
      • PPN 字段:保存了根页表的物理地址 w:800

    页表基址寄存器satp

    • satp CSR:(S-Mode) Supervisor Address Translation and Protection,监管者地址转换和保护
    • 控制硬件分页机制

    bg right:55% 95%


    初始化&使能页机制

    • M模式的RustSBI在第一次进入S-Mode之前会把0写入satp,以禁用分页
    • 然后S-Mode的OS在初始化页表后会再次写satp
      • 使能页表:MODE=8
      • 设定页表起始物理地址页号 PPN

    bg right:55% 90%


    页表项属性

    • V:有效位
    • R,W,X:读/写/执行位
    • U:U-Mode能否访问
    • G:是否对所有地址有效
    • A:Access,访问位
    • D:Dirty,修改位
    • RSW:保留位
    • PPN:物理页号

    bg right:55% 100%


    提纲

    1. 实验目标和步骤
    2. 系统架构

    3. 用户视角的地址空间

    1. 内核管理地址空间
    2. 实现ASOS

    3.1 ASOS地址空间

    3.2 跳板页 3.3 应用的地址空间


    应用程序的地址空间

    • 地址空间:一系列有关联不一定连续的逻辑段
    • 由若干逻辑段组成的虚拟/物理内存空间与一个运行的程序(目前把一个运行的程序称为任务进程)绑定
    • 进程对代码和数据的直接访问范围限制在它关联的地址空间之内 bg right:50% 100%

    应用地址空间和内核地址空间

    • 应用地址空间
      • 编译器给应用生成的地址空间,内核通过页表来约束应用地址空间,应用不能访问它之外的地址空间
    • 内核地址空间
      • 编译器给内核生成的地址空间,内核通过页表调整应用/内核地址空间,并管理整个物理内存 bg right:47% 100%

    提纲

    1. 实验目标和步骤
    2. 系统架构
    3. 用户视角的地址空间
    4. 内核管理地址空间
    5. 实现ASOS

    3.1 ASOS地址空间

    3.2 跳板页

    3.3 应用的地址空间


    跳板页

    • 应用与内核的跳板Trampoline页的虚拟地址是相同的,且映射到同一物理页
    • 放置的是trap.S中的执行代码

    bg right:65% 100%


    基于跳板页的平滑过渡

    • 特权级过渡:产生异常/中断时,CPU会跳到跳板页的_all_traps入口
    • 地址空间过渡:在切换页表后,可平滑地继续执行内核代码

    bg right:55% 100%


    陷入(Trap)上下文页

    • 跳板页的_all_traps汇编函数会保存相关寄存器到陷入上下文
    • 跳板页的_restore汇编函数会从陷入上下文中恢复相关寄存器

    bg right:55% 100%


    回顾:没有页机制的OS

    陷入上下文保存在内核栈顶,sscratch保存应用的内核栈

    • 只通过sscratch寄存器中转用户/内核的栈指针
    • 当一个应用 Trap 到内核时,sscratch 已指向该应用的内核栈栈顶,用一条指令即可从用户栈切换到内核栈,然后直接将 Trap 上下文压入内核栈栈顶。

    对比使能页机制的OS

    如何只通过sscratch寄存器中转栈指针页表基址

    • 能用之前的方法吗?
    • 方案1:通过sscratch寄存器中转用户/内核的栈指针
    • 方案2:通过sscratch寄存器中转用户栈指针/页表基址

    bg right:30% 100%


    方案1:通过sscratch寄存器中转用户/内核的栈指针

    • 通过sscratch寄存器中转用户/内核的栈指针
      • 当前sp指针指向的是内核地址空间
      • 而此时页表还是用的用户态页表
    • 导致在内核态产生异常,系统崩溃

    bg right:30% 100%


    方案2:通过sscratch寄存器中转用户栈指针/页表基址

    • 通过sscratch寄存器中转用户栈指针/页表基址
    • 当前用的是内核态页表,访问内核地址空间
    • 接下来需要取得应用的内核栈指针来把用户态当前的通用寄存器保存到陷入上下文中
    • 获取内核栈指针需要修改(破坏)通用寄存器才能完成,无法正确保存

    bg right:25% 100%


    方案3:sscratch - 应用的陷入上下文地址

    • 通过sscratch进行应用的用户态栈指针<->陷入上下文地址切换;
    • 保存用户态寄存器到陷入上下文;
    • 读出陷入上下文中的页表基址/应用的内核栈指针/trap_handler地址;
    • 切换页表,跳转trap_handler

    bg right:42% 100%


    提纲

    1. 实验目标和步骤
    2. 系统架构
    3. 用户视角的地址空间
    4. 内核管理地址空间
    5. 实现ASOS

    3.1 ASOS地址空间 3.2 跳板页

    3.3 应用的地址空间


    应用程序设计

    • 应用程序
      • 内存布局有调整
    • 没有更新
      • 项目结构
      • 应用代码
      • 函数调用
      • 系统调用

    bg right:60% 100%


    应用程序的内存布局

    • 由于每个应用被加载到的位置都相同,所以它们共用一个链接脚本 linker.ld
      • BASE_ADDRESS = 0x10000

    bg right:60% 100%


    提纲

    1. 实验目标和步骤
    2. 系统架构
    3. 用户视角的地址空间

    4. 内核管理地址空间

    1. 实现ASOS

    4.1 管理物理内存

    4.2 建立内核/应用页表 4.3 管理地址空间


    从内核角度看地址空间

    • 理解地址空间
    • 理解陷入上下文页

    bg right:55% 100%


    从内核角度看地址空间

    • 内核理解地址空间
      • 建立&感知虚拟/物理地址
      • 在内核/应用虚拟地址空间之间穿越
    • 应用的页表
      • 代表了内核管理下的现实情况下的应用地址空间
      • 让CPU"能看"到的应用地址空间

    bg right:43% 100%


    页表机制

    • 管理物理内存
    • 建立内核/应用页表
    • 使能页机制

    bg right:62% 100%


    物理内存

    • 物理内存(RAM 设定位8MB)
      • 物理内存起始地址::0x80000000
      • 可用物理内存起始地址: os/src/linker.ldekernel地址
      • 物理内存结束地址:0x80800000
    • 物理内存中有啥?

    物理内存

    • 物理内存(RAM 设定位8MB),包括:
      • 应用/内核的数据/代码/栈/堆
      • 空闲的空间
    • 特别是各种管理类数据
      • 任务控制块
        • MemorySet
          • 应用/内核的多级页表等
        • 应用核心栈
        • 应用的TrapContext页 ....

    管理物理内存

    • 物理内存上已经有一部分用于放置内核的代码和数据
    • 需要将剩下的空闲内存以单个物理页帧为单位管理起来
      • 当需要存应用数据或扩展应用的多级页表时分配空闲的物理页帧
      • 在应用出错或退出的时候回收应用占的所有物理页帧

    管理物理内存

    • 采用连续内存的动态分配策略
    • 分配/回收物理页帧的接口
      • 提供alloc()dealloc()函数接口

    bg right:49% 100%


    提纲

    1. 实验目标和步骤
    2. 系统架构
    3. 用户视角的地址空间
    4. 内核管理地址空间
    5. 实现ASOS

    4.1 管理物理内存

    4.2 建立内核/应用页表

    4.3 管理地址空间


    SV39多级页表

    • SV39 多级页表是以页大小的节点为单位进行管理。每个节点恰好存储在一个物理页帧中,它的位置可以用一个物理页号来表示
    • satp CSR bg right:45% 100%

    建立内核/应用页表

    • 页表起始物理地址
    • 页表内容:虚地址<->物理地址映射
      • 恒等映射 Identical Mapping
      • 随机映射 Framed Mapping

    VPN: Virtual Page Number PPN: Physical Page Number satp: 包含页表起始处PPN的CSR bg right:40% 100%


    建立和拆除虚实地址映射关系

    • 在多级页表中找到一个虚拟地址对应的页表项
    • 通过修改页表项的内容即可完成键值对的插入和删除,从而实现映射关系的建立和拆除。 bg right:55% 100%

    使能页机制

    • 设置satp= root_ppn

    核心数据结构的包含关系

    TCB-->MemorySet-->PageTable-->root_ppn
    任务控制块  --------------->任务的页表基址
    

    bg right:50% 100% bg right 100%


    提纲

    1. 实验目标和步骤
    2. 系统架构
    3. 用户视角的地址空间
    4. 内核管理地址空间
    5. 实现ASOS

    4.1 管理物理内存 4.2 建立内核/应用页表

    4.3 管理地址空间


    应用地址空间

    w:950


    逻辑段

    • 逻辑段:内核/应用会用到的一段连续地址的虚拟内存
    • 内核/应用运行的虚拟地址空间:由多个逻辑段组成

    理想: 丰满 v.s. 现实: 骨感

    • 应用的页表
      • 现实情况下的应用地址空间
    • 应用的逻辑段
      • 理想情况下的应用地址空间

    bg right:48% 100%


    逻辑段的数据结构MapArea

    • 逻辑段:一段连续地址的虚拟内存
    #![allow(unused)]
    fn main() {
    // os/src/mm/memory_set.rs
    
    pub struct MapArea {
        vpn_range: VPNRange, //一段虚拟页号的连续区间
        data_frames: BTreeMap<VirtPageNum, FrameTracker>,//VPN<-->PPN映射关系
        map_type: MapType,  //映射类型
        map_perm: MapPermission, //可读/可写/可执行属性
    }
    }

    data_frames 是一个保存了该逻辑段内的每个虚拟页面和它被映射到的物理页面 FrameTracker 的一个键值对容器


    地址空间的数据结构 MemorySet

    • 地址空间:一系列有关联的不一定连续的逻辑段
    #![allow(unused)]
    fn main() {
    // os/src/mm/memory_set.rs
    
    pub struct MemorySet {
        page_table: PageTable, //页表
        areas: Vec<MapArea>, //一系列有关联的不一定连续的逻辑段
    }
    }
    • 地址空间的数据结构字段
      • 多级页表: 基于数据结构PageTable的变量page_table
      • 逻辑段集合: 基于数据结构MapArea 的向量 areas

    内核管理任务地址空间的时机

    • 操作系统管理的地址空间 MemorySet=PageTable+MapAreas
      • 创建任务:创建任务的 MemorySet
      • 清除任务:回收任务的 MemorySet所占内存
      • 调整应用的内存空间大小: 修改任务的MemorySet
      • 用户态切换到内核态:切换任务的MemorySet为内核的MemorySet
      • 内核态切换到用户态:切换内核的MemorySet为任务的MemorySet

    新建任务地址空间MemorySet的过程

    • 创建页表
    • 创建逻辑段向量

    bg right:64% 100%


    在地址空间插入/删除一个逻辑段

    • 更新页表中的相应页表项
    • 更新逻辑段对应的物理页帧内容 bg right:59% 100%

    提纲

    1. 实验目标和步骤
    2. 系统架构
    3. 用户视角的地址空间
    4. 内核管理地址空间

    5. 实现ASOS

    5.1 启动分页模式

    5.2 实现跳板机制 5.3 加载和执行应用程序 5.4 改进 Trap 处理的实现 5.5 改进 sys_write 的实现


    对分时共享多任务操作系统的扩展

    1. 创建内核页表,使能分页机制,建立内核的虚拟地址空间;
    2. 扩展Trap上下文,在保存与恢复Trap上下文的过程中切换页表(即切换虚拟地址空间);
    3. 建立用于内核地址空间与应用地址空间相互切换所需的跳板空间
    4. 扩展任务控制块包括虚拟内存相关信息,并在加载执行创建基于某应用的任务时,建立应用的虚拟地址空间;
    5. 改进Trap处理过程和sys_write等系统调用的实现以支持分离的应用地址空间和内核地址空间。

    启动分页模式

    1. 创建内核地址空间
    2. 内存管理子系统的初始化

    bg right:63% 100%


    创建内核地址空间的全局实例

    • 内核地址空间KERNEL_SPACE
    #![allow(unused)]
    fn main() {
    pub static ref KERNEL_SPACE: MemorySet = MemorySet::new_kernel()
    }

    内存管理子系统的初始化

    1. 空闲物理内存按照堆(heap)进行动态连续内存管理初始化
    2. 基于堆实现物理页帧分配管理初始化
    3. 设置satp,启动分页机制,激活内核地址空间KERNEL_SPACE
    #![allow(unused)]
    fn main() {
    // os/src/mm/mod.rs
    pub fn init() {
        heap_allocator::init_heap();
        frame_allocator::init_frame_allocator();
        KERNEL_SPACE.exclusive_access().activate();
    }
    }

    提纲

    1. 实验目标和步骤
    2. 系统架构
    3. 用户视角的地址空间
    4. 内核管理地址空间
    5. 实现ASOS

    5.1 启动分页模式

    5.2 实现跳板机制

    5.3 加载和执行应用程序 5.4 改进 Trap 处理的实现 5.5 改进 sys_write 的实现


    实现跳板机制的动机

    在启动分页机制后,让处于不同地址空间的应用和内核能够进行正常的特权级切换操作和数据交互

    bg right:56% 100%


    跳板机制的思路

    • 内核和应用的虚拟地址空间中最高的虚拟页面是一个跳板(trampoline)页
    • 特权级切换后,要迅速完成地址空间切换,内核栈切换,并平滑地继续执行内核代码

    bg right:54% 100%


    跳板机制的思路

    • 应用地址空间的次高虚拟页面被设置为存放应用的 Trap 上下文

    bg right:54% 100%


    跳板机制的思路

    • Q:为何不直接把 Trap 上下文仍放到应用的内核栈中呢?

    bg right:54% 100%


    跳板机制的思路

    • Q:为何不直接把Trap上下文仍放到应用的内核栈中呢?
      • 访问内核栈中的Trap上下文地址,需要先切换页表
      • 页表信息放在 Trap上下文中,形成了相互依赖。

    bg right:54% 100%


    建立跳板页面

    将 trap.S 中的整段汇编代码放置在 .text.trampoline 段,并在调整内存布局的时候将它对齐到代码段的一个页面中

    # os/src/linker.ld
        stext = .;
            .text : {
            *(.text.entry)
            . = ALIGN(4K);
            strampoline = .;
            *(.text.trampoline);
            . = ALIGN(4K);
            *(.text .text.*)
        }
    

    扩展Trap 上下文数据结构

    #![allow(unused)]
    fn main() {
     // os/src/trap/context.rs
     pub struct TrapContext {
         pub x: [usize; 32],
         pub sstatus: Sstatus,
         pub sepc: usize,
         pub kernel_satp: usize, //内核页表的起始物理地址
         pub kernel_sp: usize,   //当前应用内核栈栈顶的虚拟地址
         pub trap_handler: usize,//内核中 trap handler 入口点的虚拟地址
    }
    }

    切换Traps上下文

    • 保存Trap上下文
      • 用户栈指针切换到用户地址空间中的TrapContext
      • 在TrapContext保存通用寄存器、sstatus、sepc
      • 在TrapContext读出kernel_satp、kernel_sp、trap_handler
      • 切换内核地址空间,切换到内核栈
      • 跳转到trap_handler继续执行
    • 恢复Trap上下文
      • 上述过程的逆过程

    跳转到trap_handler继续执行

    Q:为何用jr t1 而不是 call trap_handler完成跳转?


    保存Trap上下文

    Q:为何用jr t1 而不是 call trap_handler完成跳转?

    • 在内存布局中,这条 .text.trampoline 段中的跳转指令和 trap_handler 都在代码段之内,汇编器(Assembler)和链接器(Linker)会根据 linker-qemu.ld 的地址布局描述,设定跳转指令的地址,并计算二者地址偏移量,让跳转指令的实际效果为当前 pc 自增这个偏移量。
    • 这条跳转指令在被执行的时候,它的虚拟地址被操作系统内核设置在地址空间中的最高页面之内,所以加上这个偏移量并不能正确的得到 trap_handler 的入口地址。

    提纲

    1. 实验目标和步骤
    2. 系统架构
    3. 用户视角的地址空间
    4. 内核管理地址空间
    5. 实现ASOS

    5.1 启动分页模式 5.2 实现跳板机制

    5.3 加载和执行应用程序

    5.4 改进 Trap 处理的实现 5.5 改进 sys_write 的实现


    加载和执行应用程序

    1. 扩展任务控制块
    2. 更新任务管理

    bg right:61% 100%


    扩展任务控制块TCB

    • 应用的地址空间 memory_set
    • Trap 上下文所在物理页帧的物理页号trap_cx_ppn
    • 应用数据大小base_size
    // os/src/task/task.rs
    pub struct TaskControlBlock {
        pub task_cx: TaskContext,
        pub task_status: TaskStatus,
        pub memory_set: MemorySet,
        pub trap_cx_ppn: PhysPageNum,
        pub base_size: usize,
    }
    

    更新任务管理

    • 创建任务控制块TCB
      1. 根据应用的ELF执行文件内容形成应用的虚拟地址空间
      2. 建立应用转换到内核态后用的内核栈
      3. 在内核地址空间建立应用的TCB
      4. 在用户地址空间构造出一个Trap上下文TrapContext

    提纲

    1. 实验目标和步骤
    2. 系统架构
    3. 用户视角的地址空间
    4. 内核管理地址空间
    5. 实现ASOS

    5.1 启动分页模式 5.2 实现跳板机制 5.3 加载和执行应用程序

    5.4 改进 Trap 处理的实现

    5.5 改进 sys_write 的实现


    改进 Trap 处理的实现

    由于应用的 Trap 上下文不在内核地址空间,因此调用 current_trap_cx 来获取当前应用的 Trap 上下文的可变引用而不是像之前那样作为参数传入 trap_handler 。至于 Trap 处理的过程则没有发生什么变化。

    为了简单起见,弱化了 S态 –> S态的 Trap 处理过程:直接 panic 。 注:ch9会支持 S态 –> S态的 Trap 处理过程


    改进 Trap 处理的实现

    #![allow(unused)]
    fn main() {
    let restore_va = __restore as usize - __alltraps as usize + TRAMPOLINE;
    unsafe {
      asm!(
         "fence.i",
         "jr {restore_va}",
      )
    }
    }

    提纲

    1. 实验目标和步骤
    2. 系统架构
    3. 用户视角的地址空间
    4. 内核管理地址空间
    5. 实现ASOS

    5.1 启动分页模式 5.2 实现跳板机制 5.3 加载和执行应用程序 5.4 改进 Trap 处理的实现

    5.5 改进 sys_write 的实现


    改进 sys_write 的实现

    • 由于内核和应用地址空间的隔离, sys_write 不再能够直接访问位于应用空间中的数据
    • 需要手动查页表才能知道那些数据被放置在哪些物理页帧上并进行访问。

    访问应用空间数据的辅助函数

    页表模块 page_table 提供了将应用地址空间中一个缓冲区转化为在内核空间中能够直接访问的形式的辅助函数:

    #![allow(unused)]
    fn main() {
    // os/src/mm/page_table.rs
    pub fn translated_byte_buffer(
    }
    1. 查找应用的页表,根据应用虚地址找到物理地址
    2. 查找内核的页表,根据物理地址找到内核虚地址
    3. 基于内核虚地址完成对应用数据的读写

    小结

    • 地址空间
    • 连续内存分配
    • 段机制
    • 页表机制
    • 页访问异常
    • 能写头甲龙OS bg right:59% 100%

    第六讲 虚拟存储管理

    第一节 虚拟存储概念

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 虚拟存储技术的需求

    1. 覆盖技术
    2. 交换技术
    3. 虚拟存储的基本概念
    4. 缺页异常

    虚拟存储技术的需求背景

    程序规模的增长速度远大于存储器容量的增长速度 w:900 理想中的存储器: 容量更大、速度更快、价格更便宜的非易失性存储器


    虚拟存储的基本思路

    挑战:计算机系统时常出现内存不够用 思路:内存不够,外存来补

    • 函数覆盖(overlay)
      • 应用程序以函数/模块为单位手动换入换出内存
    • 程序交换(swapping)
      • 操作系统以程序为单位自动换入换出内存
    • 虚拟存储(virtual storage)
      • 操作系统以为单位自动换入换出内存

    虚拟存储=内存+外存


    地址空间

    地址空间是操作系统对虚拟存储的抽象w:900


    提纲

    1. 虚拟存储技术的需求

    2. 覆盖技术

    1. 交换技术
    2. 虚拟存储的基本概念
    3. 缺页异常

    覆盖技术

    • 目标
      • 程序员手动控制在较小的可用内存中运行较大的程序
    • 基本思路
      • 不同时间段内执行的函数或模块共享一块有限的空间

    bg right:48% 75%


    覆盖的基本原理

    覆盖是指把一个程序划分为一系列功能相对独立的程序段,让执行时不要求同时装入内存的程序段组成一组(称为覆盖段),共享主存的同一个区域。

    • 必要部分(常用)的代码和数据常驻内存
    • 可选部分(不常用)放在其他程序模块中,只在需要时装入内存
    • 不存在调用关系的模块可相互覆盖,共用同一块内存区域

    覆盖技术示例

    w:900


    覆盖技术的不足

    • 增加编程困难
      • 需程序员划分功能模块,并确定模块间的覆盖关系
      • 增加了编程的复杂度;
    • 增加执行时间
      • 从外存装入覆盖模块
      • 时间换空间

    Turbo Pascal的Overlay系统单元支持程序员控制的覆盖技术


    提纲

    1. 虚拟存储技术的需求
    2. 覆盖技术

    3. 交换技术

    1. 虚拟存储的基本概念
    2. 缺页异常

    交换技术

    • 基本思路
      • 操作系统以程序为单位自动换入换出内存
    • 方法
      • 换出(swap out):把一个执行程序的整个地址空间内容保存到外存
      • 换入(swap in):将外存中某执行程序的地址空间内容读入到内存

    bg right:40% 100%


    交换技术面临的问题

    • 交换时机:何时需要发生交换?
      • 只当内存空间不够或有不够的可能时换出
    • 程序换入时的重定位:换出后再换入时要放在原处吗?
      • 不一定在原处,需要某种机制保证程序正确寻址&执行

    交换技术面临的问题

    • 交换区(空间)大小:经验值。下面是Linux专家给出的建议

    w:1150


    覆盖与交换的比较

    • 程序覆盖
      • 发生在某时间段不在一个控制流上的模块/函数间
      • 以模块/函数为单位
      • 程序员须给出模块/函数间的逻辑覆盖结构
    • 交换
      • 发生在运行的程序间
      • 以运行的程序为单位
      • 不需要模块间的逻辑覆盖结构

    运行的程序:任务进程


    提纲

    1. 虚拟存储技术的需求
    2. 覆盖技术
    3. 交换技术

    4. 虚拟存储的基本概念

    1. 缺页异常

    虚拟存储的定义

    • 定义
      • 虚拟存储 = 内存 + 外存
    • 思路
      • 操作系统将不常用的部分内存暂存到外存,将要处理器访问的数据从外存装入内存
    • 前提
      • 程序具有局部性

    bg right:54% 95%


    局部性原理

    局部性(locality):程序在执行过程中的一个较短时期,所执行的指令地址和指令的操作数地址,分别局限于一定区域

    • 时间局部性:一条指令的一次执行和下次执行,一个数据的一次访问和下次访问都集中在一个较短时期
    • 空间局部性:当前指令和邻近时间的几条指令,当前访问的数据和邻近时间访问的几个数据都集中在一个较小区域
    • 分支局部性:一条跳转指令的两次执行,很可能跳到相同的内存位置

    局部性的意义:如果大部分程序运行具有局部性特征,则虚拟存储技术是能够实现的,而且可取得满意的效果


    虚拟存储的思路与规则

    • 思路:将不常用的部分内存块暂存到外存
    • 规则:
      • 装载程序时:只将当前指令执行需要的部分页面或段装入内存
      • 指令执行中需要的指令或数据不在内存(称为缺页或缺段)时:处理器通知操作系统将相应的页面或段调入内存
      • 操作系统将内存中暂时不用的页面或段保存到外存
    • 实现方式:
      • 虚拟页式存储
      • 虚拟段式存储

    虚拟存储的基本特征

    • 不连续性
      • 物理内存分配非连续
      • 虚拟地址空间使用非连续
    • 大用户空间
      • 提供给用户的虚拟内存可大于实际的物理内存
    • 部分交换
      • 虚拟存储只对部分虚拟地址空间进行调入和调出

    虚拟存储的底层支撑

    • 硬件(MMU/TLB/PageTable)
      • 页式或段式存储中的硬件地址转换机制、硬件异常
    • 软件(OS)
      • 内存中建立页表或段表
      • 管理内存和外存间页面或段的换入和换出

    虚拟页式存储管理

    在页式存储管理的基础上,增加请求调页和页面置换

    • 基本思路
      • 当用户程序要装载到内存时,只装入部分页面,就启动程序运行
      • 用户程序在运行中发现有需要的代码或数据不在内存时,则向系统发出缺页异常请求
      • 操作系统在处理缺页异常时,将外存中相应的页面调入内存,使得用户程序能继续运行
      • 当内存快用完时,操作系统把部分页从内存调出到外存

    虚拟页式存储管理

    在页式存储管理的基础上,增加请求调页和页面置换

    • 请求调页:也称按需分页,在处理器需要访问某数据时,才把数据从外存调入内存
    • 页面置换:把不常用页换出,要使用的页换入
    • 缺页异常处理:软硬件协同支持

    bg right:54% 100%


    提纲

    1. 虚拟存储技术的需求
    2. 覆盖技术
    3. 交换技术
    4. 虚拟存储的基本概念

    5. 缺页异常


    缺页异常的处理流程

    1. CPU读内存单元,在TLB中根据其虚拟地址匹配物理地址,未命中,读页表
    2. 由于页表项的存在位为0,CPU产生缺页异常
    3. OS查找到保存在外存中对应的应用的页面内容;

    bg left:51% 100%


    缺页异常的处理流程

    4-1. 如有空闲物理页帧,把外存中的页面内容换入到某空闲物理页帧中; 4-2. 如无空闲物理页帧,通过置换算法释放/换出某物理页帧到外存,再把外存中的页面内容换入到某空闲物理页帧中;

    bg left:51% 100%


    缺页异常的处理流程

    1. 修改页表项,建立虚拟页到物理页帧的映射,存在位置1;
    2. OS返回到应用程序,让处理器重新执行产生缺页异常的读内存单元指令。

    bg left:51% 100% 在何处保存未被映射的页?如何找到这个页?


    在何处保存未被映射的页?

    • 交换空间(磁盘/文件形态)
      • 采用特殊格式存储未被映射的页面
    • 磁盘上的文件(代码或数据)

    bg right:50% 100%


    虚拟存储的外存交换空间

    在何处保存放在外存中的页的地址?

    • 交换空间
      • 磁盘分区:一般是扇区地址
      • 在存在位为0的页表项中保存外存的页地址

    bg right:54% 100%


    虚拟存储的外存磁盘文件

    在何处保存放在外存中的页的地址?

    • 磁盘上的文件(代码或数据)
      • 地址空间中的逻辑段表示中有对应的文件位置
        • 如:MemorySet::MapArea
      • 代码段:可执行二进制文件
      • 动态加载的共享库程序段:动态调用的库文件

    bg right:48% 100%


    虚拟存储的性能

    有效存储访问时间(EAT, Effective memory Access Time)

    • EAT = 内存访问时间 $*$ (1-p) + 缺页异常处理时间
    • 缺页异常处理时间 = 磁盘访问时间*p(1+q)
      • p: 缺页率;
      • q: 写回概率
    • 例子
      • 内存访问时间: 10 ns ;磁盘访问时间: 5 ms
      • EAT = 10(1–p) + 5,000,000p(1+q)

    小结

    1. 虚拟存储技术的需求
    2. 覆盖技术
    3. 交换技术
    4. 虚拟存储的基本概念
    5. 缺页异常

    第六讲 虚拟存储管理

    第二节 局部页面置换算法

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 页面置换算法的基本概念

    1. 最优页面置换算法 (OPT, optimal)
    2. 先进先出页面置换算法 (FIFO)
    3. 最近最久未使用页面置换算法 (LRU, Least Recently Used)
    4. 时钟页面置换算法 (Clock)
    5. 改进的时钟页面置换算法
    6. 最不常用页面置换算法 (LFU, Least Frequently Used)
    7. Belady现象

    页面置换算法的功能与设计目标

    • 功能

      • 出现缺页异常需调入新页面而内存已满时,置换算法选择被置换的物理页面
    • 设计目标

      • 尽可能减少页面缺页次数换入/换出次数
      • 把未来不再访问或短期内不访问的页面调出

    bg right:50% 100%


    页面置换的时机

    • 空闲内存数量上限和下限
    • 到达下限,开始回收内存
    • 到达上限,暂停回收内存

    bg right:50% 100%


    页面锁定(frame locking)/常驻内存

    必须常驻内存的逻辑页面

    • 操作系统的关键部分
    • 要求响应速度的代码和数据
    • 页表中的锁定标志位(lock bit)

    bg right:53% 100%


    页面置换算法的评价方法

    • 评价方法
      • 记录进程访问内存页面的轨迹,模拟置换行为,记录缺页次数
      • 的缺页, 更的性能
    • 示例: 虚拟地址访问用(页号, 位移)表示
    (3,0),  (1,9),  (4,1),  (2,1),  (5,3),  (2,0),  (1,9),  (2,4),  (3,1),  (4,8)
    
    • 对应的页面轨迹
    3, 1, 4, 2, 5, 2, 1, 2, 3, 4 用数字表示 
    c, a, d, b, e, b, a, b, c, d 用字符表示
    

    页面置换算法的分类

    • 局部页面置换算法

      • 置换页面的选择范围仅限于当前进程占用的物理页面内
      • 最优算法、先进先出算法、最近最久未使用算法
      • 时钟算法、最不常用算法
    • 全局页面置换算法

      • 置换页面的选择范围是所有可换出的物理页面
      • 工作集算法、缺页率算法

    提纲

    1. 页面置换算法的基本概念

    2. 最优页面置换算法 (OPT, optimal)

    1. 先进先出页面置换算法 (FIFO)
    2. 最近最久未使用页面置换算法 (LRU, Least Recently Used)
    3. 时钟页面置换算法 (Clock)
    4. 改进的时钟页面置换算法
    5. 最不常用页面置换算法 (LFU, Least Frequently Used)
    6. Belady现象

    最优页面置换算法的工作原理

    • 基本思路
      • 置换在未来最长时间不访问的页面
    • 算法实现
      • 缺页时,计算内存中每个逻辑页面的下一次访问时间
      • 选择未来最长时间不访问的页面

    最优页面置换算法特征

    • 缺页最少,是理想情况
    • 实际系统中无法实现
    • 无法预知每个页面在下次访问前的等待时间
      • 在模拟器上运行某个程序,并记录每一次的页面访问情况
      • 第二遍运行时使用最优算法

    最优页面置换算法示例

    w:1000


    最优页面置换算法示例

    w:1000


    最优页面置换算法示例

    w:1000


    最优页面置换算法示例

    w:1000


    提纲

    1. 页面置换算法的基本概念
    2. 最优页面置换算法 (OPT, optimal)

    3. 先进先出页面置换算法 (FIFO)

    1. 最近最久未使用页面置换算法 (LRU, Least Recently Used)
    2. 时钟页面置换算法 (Clock)
    3. 改进的时钟页面置换算法
    4. 最不常用页面置换算法 (LFU, Least Frequently Used)
    5. Belady现象

    先进先出算法的工作原理

    • 基本思路
      • 选择在内存驻留时间最长的页面进行置换
    • 算法实现
      • 维护一个记录所有位于内存中的逻辑页面链表
      • 链表元素按驻留内存的时间排序,链首最长,链尾最短
      • 出现缺页时,选择链首页面进行置换,新页面加到链尾

    先进先出算法特征

    • 实现简单
    • 性能较差,调出的页面可能是经常访问的
    • 分配物理页面数增加时,缺页并不一定减少(Belady现象)
    • 很少单独使用

    先进先出算法示例

    w:900


    先进先出算法示例

    w:900


    先进先出算法示例

    w:900


    先进先出算法示例

    w:900


    先进先出算法示例

    w:900


    先进先出算法示例

    w:900


    提纲

    1. 页面置换算法的基本概念
    2. 最优页面置换算法 (OPT, optimal)
    3. 先进先出页面置换算法 (FIFO)

    4. 最近最久未使用算法 (LRU, Least Recently Used)

    1. 时钟页面置换算法 (Clock)
    2. 改进的时钟页面置换算法
    3. 最不常用页面置换算法 (LFU, Least Frequently Used)
    4. Belady现象

    最近最久未使用算法的工作原理

    • 基本思路

      • 选择最长时间没有被引用的页面进行置换
      • 如某页面长时间未被访问,则它在将来还可能会长时间不会访问
    • 算法实现

      • 缺页时,计算内存中每个逻辑页面的上一次访问时间
      • 选择上一次使用到当前时间最长的页面进行置换
    • 算法特征

      • 最优置换算法的一种近似

    最近最久未使用算法示例

    w:900


    最近最久未使用算法示例

    w:900


    最近最久未使用算法示例

    w:900


    最近最久未使用算法示例

    w:900


    最近最久未使用算法示例

    w:900


    最近最久未使用算法示例

    w:900


    最近最久未使用算法示例

    w:900


    LRU的页面链表实现

    • 页面链表
      • 系统维护一个按最近一次访问时间排序的页面链表
        • 链表首节点是最近刚刚使用过的页面
        • 链表尾节点是最久未使用的页面
      • 访问内存时,找到相应页面,并把它移到链表之首
      • 缺页时,置换链表尾节点的页面
    • 特征
      • 开销大

    LRU的活动页面栈实现

    • 活动页面
      • 访问页面时,将此页号压入栈顶,并栈内相同的页号抽出
      • 缺页时,置换栈底的页面
    • 特征
      • 开销大

    LRU的活动页面栈实现示例

    w:900


    LRU的活动页面栈实现示例

    w:900


    LRU的活动页面栈实现示例

    w:900


    LRU的活动页面栈实现示例

    w:900


    LRU的活动页面栈实现示例

    w:900


    LRU的活动页面栈实现示例

    w:900


    LRU的活动页面栈实现示例

    w:900


    LRU的活动页面栈实现示例

    w:900


    LRU的活动页面栈实现示例

    w:900


    提纲

    1. 页面置换算法的基本概念
    2. 最优页面置换算法 (OPT, optimal)
    3. 先进先出页面置换算法 (FIFO)
    4. 最近最久未使用页面置换算法 (LRU, Least Recently Used)

    5. 时钟页面置换算法 (Clock)

    1. 改进的时钟页面置换算法
    2. 最不常用页面置换算法 (LFU, Least Frequently Used)
    3. Belady现象

    时钟置换算法的工作原理

    • 基本思路
      • 仅对页面的访问情况进行大致统计
    • 数据结构
      • 在页表项中增加访问位,描述页面在过去一段时间的内访问情况
      • 各页面组织成环形链表
      • 指针指向最先调入的页面

    bg right:51% 100%


    时钟置换算法的工作原理

    • 算法实现
      • 访问页面时,在页表项记录页面访问情况
      • 缺页时,从指针处开始顺序查找未被访问的页面进行置换

    bg right:54% 100%


    时钟置换算法的具体实现过程

    • 页面装入内存时,访问位初始化为0
    • 访问页面(读/写)时,访问位置1
    • 缺页时,从指针当前位置顺序检查
      • 访问位为0,则置换该页
      • 访问位为1,则访问位置0,并指针移动到下一个页面,直到找到可置换的页面
    • 算法特征
      • 时钟算法是LRU和FIFO的折中

    bg right:34% 100%


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    时钟置换算法示例

    w:900


    提纲

    1. 页面置换算法的基本概念
    2. 最优页面置换算法 (OPT, optimal)
    3. 先进先出页面置换算法 (FIFO)
    4. 最近最久未使用页面置换算法 (LRU, Least Recently Used)
    5. 时钟页面置换算法 (Clock)

    6. 改进的时钟页面置换算法

    1. 最不常用页面置换算法 (LFU, Least Frequently Used)
    2. Belady现象

    改进的时钟置换算法的工作原理

    • 基本思路
      • 减少修改页的缺页处理开销
    • 数据结构
      • 在页面中增加修改位,描述页面在过去一段时间的内写访问情况
    • 算法实现
      • 访问页面时,在页表项记录页面访问情况
      • 修改页面时,在页表项记录页面修改情况
      • 缺页时,修改页面标志位,以跳过有修改的页面

    改进的时钟置换算法的工作原理

    w:1150


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    改进的时钟置换算法示例

    w:1000


    提纲

    1. 页面置换算法的基本概念
    2. 最优页面置换算法 (OPT, optimal)
    3. 先进先出页面置换算法 (FIFO)
    4. 最近最久未使用页面置换算法 (LRU, Least Recently Used)
    5. 时钟页面置换算法 (Clock)
    6. 改进的时钟页面置换算法

    7. 最不常用置换算法 (LFU, Least Frequently Used)

    1. Belady现象

    最不常用算法的工作原理

    • 基本思路

      • 缺页时,置换访问次数最少的页面
    • 算法实现

      • 每个页面设置一个访问计数
      • 访问页面时,访问计数加1
      • 缺页时,置换计数最小的页面

    最不常用算法特征

    • 特征

      • 算法开销大
      • 开始时频繁使用,但以后不使用的页面很难置换
        • 解决方法:计数定期右移
    • LRU关注多久未访问,时间越短越好

    • LFU关注访问次数,次数越多越好


    LFU示例

    4个物理页帧,最初的访问次数a->8 b->5 c->6 d->2 w:1100


    LFU示例

    4个物理页帧,最初的访问次数a->8 b->5 c->6 d->2 w:1100


    LFU示例

    4个物理页帧,最初的访问次数a->8 b->5 c->6 d->2 w:1100


    LFU示例

    4个物理页帧,最初的访问次数a->8 b->5 c->6 d->2 w:1100


    LFU示例

    4个物理页帧,最初的访问次数a->8 b->5 c->6 d->2 w:1100


    LFU示例

    4个物理页帧,最初的访问次数a->8 b->5 c->6 d->2 w:1100


    LFU示例

    4个物理页帧,最初的访问次数a->8 b->5 c->6 d->2 w:1100


    提纲

    1. 页面置换算法的基本概念
    2. 最优页面置换算法 (OPT, optimal)
    3. 先进先出页面置换算法 (FIFO)
    4. 最近最久未使用页面置换算法 (LRU, Least Recently Used)
    5. 时钟页面置换算法 (Clock)
    6. 改进的时钟页面置换算法
    7. 最不常用页面置换算法 (LFU, Least Frequently Used)

    8. Belady现象


    Belady现象

    • 现象
      • 采用FIFO等算法时,可能出现分配的物理页面数增加缺页次数反而升高的异常现象
    • 原因
      • FIFO算法的置换特征与进程访问内存的动态特征矛盾
      • 被它置换出去的页面并不一定是进程近期不会访问的
    • 思考
      • 哪些置换算法没有Belady现象?

    FIFO算法的Belady现象

    访问顺序 : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 物理页面数: 3 ; 缺页次数: 9 w:1100


    FIFO算法的Belady现象

    访问顺序 : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 物理页面数: 3 ; 缺页次数: 9 w:1100


    FIFO算法的Belady现象

    访问顺序 : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 物理页面数: 3 ; 缺页次数: 9 w:1100


    FIFO算法的Belady现象

    访问顺序 : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 物理页面数: 3 ; 缺页次数: 9 w:1100


    FIFO算法的Belady现象

    访问顺序 : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 物理页面数: 3 ; 缺页次数: 9 w:1100


    FIFO算法的Belady现象

    访问顺序 : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 物理页面数: 3 ; 缺页次数: 9 w:1100


    FIFO算法的Belady现象

    访问顺序 : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 物理页面数: 3 ; 缺页次数: 9 w:1100


    FIFO算法的Belady现象

    访问顺序 : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 物理页面数: 4 ; 缺页次数: 10 w:900


    FIFO算法的Belady现象

    访问顺序 : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 物理页面数: 4 ; 缺页次数: 10 w:900


    FIFO算法的Belady现象

    访问顺序 : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 物理页面数: 4 ; 缺页次数: 10 w:900


    FIFO算法的Belady现象

    访问顺序 : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 物理页面数: 4 ; 缺页次数: 10 w:900


    FIFO算法的Belady现象

    访问顺序 : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 物理页面数: 4 ; 缺页次数: 10 w:900


    FIFO算法存在Belady现象

    w:600 w:600


    LRU算法不存在Belady现象

    w:1100

    时钟/改进的时钟页面置换是否有Belady现象? 为什么LRU页面置换算法没有Belady现象?


    LRU、FIFO和Clock的比较

    • LRU算法和FIFO本质上都是先进先出的思路
    • LRU依据页面的最近访问时间排序
    • LRU需要动态地调整顺序
    • FIFO依据页面进入内存的时间排序
    • FIFO的页面进入时间是固定不变的

    LRU、FIFO和Clock的比较

    • LRU可退化成FIFO
      • 如页面进入内存后没有被访问,最近访问时间与进入内存的时间相同
      • 例如:给进程分配3个物理页面,逻辑页面的访问顺序为1、2、3、4、5、6、1、2、3…

    LRU、FIFO和Clock的比较

    • LRU算法性能较好,但系统开销较大
    • FIFO算法系统开销较小,会发生Belady现象
    • Clock算法是它们的折衷
      • 页面访问时,不动态调整页面在链表中的顺序,仅做标记
      • 缺页时,再把它移动到链表末尾
      • 对于未被访问的页面,Clock和LRU算法的表现一样好
      • 对于被访问过的页面,Clock算法不能记录准确访问顺序,而LRU算法可以

    小结

    1. 页面置换算法的基本概念
    2. 最优页面置换算法 (OPT, optimal)
    3. 先进先出页面置换算法 (FIFO)
    4. 最近最久未使用页面置换算法 (LRU, Least Recently Used)
    5. 时钟页面置换算法 (Clock)
    6. 改进的时钟页面置换算法
    7. 最不常用页面置换算法 (LFU, Least Frequently Used)
    8. Belady现象

    第六讲 虚拟存储管理

    第三节 全局页面置换算法

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 全局页面置换算法的定义

    1. 工作集页面置换算法
    2. 缺页率页面置换算法

    局部置换算法没有考虑进程访存差异

    FIFO 页面置换算法: 假设初始顺序 a->b->c 物理页面数: 3 缺页次数: 9 w:1100


    局部置换算法没有考虑进程访存差异

    FIFO 页面置换算法: 假设初始顺序 a->b->c 物理页面数: 4 缺页次数: 1 w:1100


    全局置换算法的工作原理

    • 思路
      • 为进程分配可变数目的物理页面
    • 全局置换算法要解决的问题
      • 进程在不同阶段的内存需求是变化的
      • 分配给进程的内存也需要在不同阶段有所变化
      • 全局置换算法需要确定分配给进程的物理页面数

    CPU利用率与程序运行数量

    bg right:40% 100%

    • CPU利用率与程序运行数量存在相互促进和制约的关系
      • 运行程序少时,提高程序运行数,可提高CPU利用率
      • 程序运行数量大导致内存访问增加,并会降低访存的局部性
      • 局部性下降会导致缺页率上升和CPU利用率下降

    提纲

    1. 全局页面置换算法的定义

    2. 工作集页面置换算法

    1. 缺页率页面置换算法

    工作集

    一个进程当前正在使用的逻辑页面集合,可表示为二元函数W(t, $\Delta$)

    • 当前执行时刻$t$
    • 工作集窗口(working-set window)$\Delta$:一个定长的页面访问时间窗口
    • 工作集窗口$\Delta$大小$\tau$
      • 时间段长度,用当前时刻 $t$ 前的内存访问次数来表示
    • 工作集W(t, $\Delta$)
      • 在当前时刻 $t$ 前的$\Delta$时间窗口中的所有访问页面所组成的集合
    • 工作集大小 | W(t, $\Delta$) |:页面数目

    进程的工作集示例

    页面访问顺序: W(t, $\Delta$) ={1,2,5,6,7} , 工作集窗口大小 $\tau=10, 当前时刻 t=t_1$ w:1100


    进程的工作集示例

    页面访问顺序: W(t, $\Delta$) ={1,2,3,4,5,6,7} , 工作集窗口大小 $\tau=10, 当前时刻 t=t_1$

    w:1100


    进程的工作集示例

    页面访问顺序: W(t, $\Delta$) ={3,4}, 工作集窗口大小 $\tau=10$,当前时刻 $t=t_2$

    w:1100


    工作集的变化

    w:600

    • 进程开始执行后,随着访问新页面逐步建立较稳定的工作集
    • 当内存访问的局部性区域位置大致稳定时,工作集大小也大致稳定
    • 局部性区域位置改变时,工作集快速扩张和收缩过渡到下一个稳定值

    常驻集

    在当前时刻,进程实际驻留内存中的页面集合

    • 工作集与常驻集的关系
      • 工作集是进程在运行过程中的固有性质
      • 常驻集取决于系统分配给进程的物理页面数目和页面置换算法
    • 缺页率与常驻集的关系
      • 常驻集 $\supseteq$ 工作集时,缺页较少
      • 工作集发生剧烈变动(过渡)时,缺页较多
      • 进程常驻集大小达到一定数目后,缺页率也不会明显下降

    工作集页面置换算法

    • 思路

      • 换出不在工作集中的页面
    • 工作集窗口大小$\tau$

      • 当前时刻前$\tau$次内存访问的页面集合构成工作集
    • 实现方法

      • 访存链表:维护窗口内的访存页面链表
      • 访存时,换出不在工作集的页面,更新访存链表
      • 缺页时,换入页面,更新访存链表

    工作集置换算法示例

    $\tau=4$

    w:1100


    工作集置换算法示例

    $\tau=4$

    w:1100


    工作集置换算法示例

    $\tau=4$

    w:1100


    工作集置换算法示例

    $\tau=4$

    w:1100


    工作集置换算法示例

    $\tau=4$

    w:1100


    工作集置换算法示例

    $\tau=4$

    w:1100


    工作集置换算法示例

    $\tau=4$

    w:1100


    工作集置换算法示例

    $\tau=4$

    w:1100


    工作集置换算法示例

    $\tau=4$

    w:1100


    提纲

    1. 全局页面置换算法的定义
    2. 工作集页面置换算法

    3. 缺页率页面置换算法


    缺页率(Page-Fault-Frequency, Page Fault Rate)

    缺页次数 / 内存访问次数 或 缺页平均时间间隔的倒数

    • 影响缺页率的因素
      • 页面置换算法
      • 分配给进程的物理页面数目
      • 页面大小
      • 程序的编写方法

    缺页率置换算法

    bg right:60% 100%

    通过调节常驻集大小,使每个进程的缺页率保持在一个合理的范围内

    • 若进程缺页率过高,则增加常驻集以分配更多的物理页面
    • 若进程缺页率过低,则减少常驻集以减少它的物理页面数

    缺页率页面置换算法

    • 访存时,设置引用位标志
    • 缺页时,计算从上次缺页时间$t_{last}$ 到现在$t_{current}$ 的时间间隔
      • 如果 $t_{current} – t_{last}>T$(容忍的缺页窗口),则置换所有在$[t_{last} , t_{current} ]$时间内没有被引用的页
      • 如果$t_{current} – t_{last} \le T$,则增加缺失页到常驻集中

    缺页率置换算法示例

    假定窗口大小为 2 w:1100


    缺页率置换算法示例

    假定窗口大小为 2 w:1100


    缺页率置换算法示例

    假定窗口大小为 2 w:1100


    缺页率置换算法示例

    假定窗口大小为 2 w:1100


    缺页率置换算法示例

    假定窗口大小为 2 w:1100


    缺页率置换算法示例

    假定窗口大小为 2 w:1100


    缺页率置换算法示例

    假定窗口大小为 2 w:1100


    缺页率置换算法示例

    假定窗口大小为 2 w:1100


    缺页率置换算法示例

    假定窗口大小为 2 w:1100


    缺页率置换算法示例

    假定窗口大小为 2 w:1100


    抖动问题(thrashing)

    • 抖动

      • 进程物理页面太少,不能包含工作集
      • 造成大量缺页,频繁置换
      • 进程运行速度变慢
    • 产生抖动的原因

      • 随着驻留内存的进程数目增加,分配给每个进程的物理页面数不断减小,缺页率不断上升
    • 操作系统需在并发水平和缺页率之间达到一个平衡

      • 选择一个适当的进程数目和进程需要的物理页面数

    课程实验二

    • 第四章:地址空间 -> chapter4练习 ->
    • 实验任务
      • 重写获取系统时间和进程控制块信息的内核函数
      • 实现申请和取消虚存映射的系统调用
    • 实验提交要求
      • 2023年11月01日;

    第五讲虚拟存储管理小结

    • 第一节 虚拟存储概念
      • 需求、覆盖、交换、虚拟存储的概念、缺页异常
    • 第二节 局部页面置换算法
      • 页面置换算法的概念、OPT、FIFO、LRU、Clock、改进的时钟页面置换算法、LFU、Belady现象
    • 第三节 全局页面置换算法
      • 全局页面置换算法、工作集置换算法、缺页率置换算法

    第七讲 进程管理与单处理器调度

    第一节 进程管理

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 进程的基本概念

    1. 进程管理
    2. 关于Fork()的思考
    • 进程管理的需求背景
    • 进程的概念
    • 进程与任务

    进程管理的需求背景

    • 背景

      • 硬件越来越强大
        • 更快的CPU with MMU/TLB/Cache
        • 更大的内存和外存
        • 更丰富的外设
      • 开发者希望能够在计算机上有更多的动态交互和控制能力
      • 使用者需要更方便的计算机交互能力
    • 目标

      • 提高开发效率和执行效率

    进程管理的需求背景

    • 操作系统需要与用户交互的接口/界面
      • 命令行界面(CLI)
        • 用户通过键盘直接输入命令
        • Shell
      • 图形界面(GUI)
        • 用户通过鼠标/窗口等输入命令

    bg right:53% 90%


    进程管理的需求背景

    • 用户需要动态管理和控制应用执行
    • 在应用的执行过程中,用户通过界面主动给操作系统发出请求,来创建并执行新的应用程序,暂停或停止应用程序的执行等。

    bg right:53% 90%


    引入进程(Process)概念的目的

    • 清晰地刻画操作系统系统中程序运行的动态内在规律
    • 有效管理和调度多个程序的执行和对资源的使用

    bg right:47% 90%


    进程提供给应用程序的抽象

    • 从应用角度看,进程提供给应用程序的关键抽象
      • 独立的逻辑控制流:好像自己的程序独占地使用处理器
      • 私有的地址空间:好像自己的程序独占地使用内存系统

    bg right:47% 90%


    从实现角度看进程

    从实现角度看,进程是操作系统建立程序运行中的过程管理相关的数据结构,以及对数据结构的动态操作过程

    bg right:55% 90%


    从资源角度看进程

    • 从资源角度看,进程是程序执行中占用资源的集合
      • 共享资源 v.s. 独占资源
      • 处理器、时间
      • 内存、地址空间
      • 文件、I/O、...

    bg right:52% 90%


    什么是进程?

    • 简单定义
      • 一个程序的执行过程
      • 一个执行中程序的实例
    • 详细定义:一个具有一定独立功能的程序在某数据集合上的一次执行和资源使用的动态过程
      • 执行程序逻辑并读写数据
      • 创建并执行新进程
      • 使用共享资源:文件等

    bg right:46% 90%


    任务和进程

    资源占用执行过程两个方面分析

    不同点:

    • 任务 是这里提到的进程的初级阶段,不具有如下功能:
      • 进程可以在运行的过程中,创建子进程 、 用新的程序内容覆盖已有的程序内容
      • 进程成为程序执行过程中动态申请/使用/释放各种资源的载体

    进程的动态功能可让程序的运行更加灵活。


    进程是计算机科学中的重要概念

    进程是计算机科学中最深刻、最成功的概念之一(from CSAPP) bg right:65% 90%


    提纲

    1. 进程的基本概念

    2. 进程管理

    1. 关于Fork()的思考

    2.1 进程管理系统调用

    2.2 进程控制块PCB 2.3 进程创建和程序加载 2.4 进程等待与退出


    进程管理系统调用产生背景

    • 如何让应用方便地动态执行其他应用
      • process_id = execute(app_name)?
    • 如何让应用了解其启动的其他应用是否结束?
      • 被启动的其他应用 exit(status)?
      • 发起的主应用 wait(process_id)?

    于是各种OS(UNIX/Windows...)都设计出了类似上面的进程管理类的各种系统调用


    进程管理系统调用

    系统调用名含义
    int fork()创建一个进程,返回子进程的PID。
    int exec(char *file)加载文件并执行;仅当出错时返回。
    int exit(int status)终止自身;报告status给执行waitpid()系统调用的父进程。
    int waitpid(int pid, int *status)等待pid子进程退出,得到其 *status退出状态。
    int getpid()获得当前进程的PID。

    进程管理应用示例:getpid()

    // usr/src/bin/hello_world.rs
    pub fn main() -> i32 {
        // 显示自己的PID
        println!("pid {}: Hello world from user mode program!", getpid()); 
        0  // 返回的退出码
    }

    进程管理应用示例:fork()exec()

    // usr/src/bin/forkexec.rs
    pub fn main() -> i32 {
        println!("pid {}: parent start forking ...", getpid());
        let pid = fork();  // 创建子进程
        if pid == 0 {
            // 子进程
            println!("pid {}: forked child start execing hello_world app ... ", getpid());
            exec("hello_world");  // 执行hello_world程序
            100
        } else {
            // 父进程
            let mut exit_code: i32 = 0;
            println!("pid {}: ready waiting child ...", getpid());
            assert_eq!(pid, wait(&mut exit_code)); //确认等待的子进程PID
            assert_eq!(exit_code, 0);  //确认退出码是0
            println!("pid {}: got child info:: pid {}, exit code: {}", getpid() , pid, exit_code);
            0
        }
    }

    进程管理应用示例:fork()exec()

    执行结果

    Rust user shell
    >> forkexec
    pid 2: parent start forking ...
    pid 2: ready waiting child ...
    pid 3: forked child start execing hello_world app ...
    pid 3: Hello world from user mode program!
    pid 2:  got child info:: pid 3, exit code: 0
    Shell: Process 2 exited with code 0
    >> QEMU: Terminated
    
    

    提纲

    1. 进程的基本概念
    2. 进程管理
    3. 关于Fork()的思考

    2.1 进程管理系统调用

    2.2 进程控制块PCB

    2.3 进程创建和程序加载 2.4 进程等待与退出


    进程控制块PCB

    bg right:70% 95%


    shell执行用户输入命令

    w:1200


    shell执行中的进程控制块

    w:550 bg right:50% 100%


    进程切换

    • 进程切换过程
      • 暂停当前运行进程,从运行状态变成其他状态
      • 调度另一个进程从就绪状态变成运行状态
    • 进程切换的要求
      • 切换前,保存进程上下文
      • 切换后,恢复进程上下文

    bg right:51% 100%


    进程生命周期

    • 进程切换
      • 暂停当前运行进程,从运行状态变成其他状态
      • 调度另一个进程从就绪状态变成运行状态
    • 进程生命周期的信息
      • 寄存器 (PC, SP, …)
      • CPU状态
      • 内存地址空间 bg right:51% 100%

    提纲

    1. 进程的基本概念
    2. 进程管理
    3. 关于Fork()的思考

    2.1 进程管理系统调用 2.2 进程控制块PCB

    2.3 进程创建和程序加载

    2.4 进程等待与退出


    Windows进程创建API: CreateProcess(filename)

    • 创建时关闭所有在子进程里的文件描述符
      • CreateProcess(filename, CLOSE_FD)
    • 创建时改变子进程的环境
      • CreateProcess(filename, CLOSE_FD, new_envp)

    进程创建/加载

    • Unix进程创建/加载系统调用: fork/exec

      • fork()把一个进程复制成两个进程
        • parent (old PID), child (new PID)
      • exec()用新程序来重写当前进程
        • PID没有改变

      bg right:55% 90%


    用fork和exec创建进程的示例

    int pid = fork();		// 创建子进程
    if(pid == 0) {			// 子进程在这里继续
         // Do anything (unmap memory, close net connections…)
         exec(“program”, argc, argv0, argv1, …);
    }
    
    • fork() 创建一个继承的子进程
      • 复制父进程的所有变量和内存
      • 复制父进程的所有CPU寄存器(有一个寄存器例外)

    用fork和exec创建进程的示例

    int pid = fork();		// 创建子进程
    if(pid == 0) {			// 子进程在这里继续
         // Do anything (unmap memory, close net connections…)
         exec(“program”, argc, argv0, argv1, …);
    }
    
    • fork()的返回值
      • 子进程的fork()返回0
      • 父进程的fork()返回子进程标识符
      • fork() 返回值可方便后续使用,子进程可使用getpid()获取PID

    进程创建fork()的执行过程

    • 对于子进程而言,fork()是对父进程地址空间的一次复制过程 w:1000

    程序加载并执行的示例

    • 系统调用exec( )加载新程序取代当前运行进程 (代码是否有问题???)
    main()
    …
    int pid = fork();			// 创建子进程
    if (pid == 0) {			        // 子进程在这里继续
        exec_status = exec(“calc”, argc, argv0, argv1, …);
        printf(“Why would I execute?”);     // 这行代码能执行到吗???
    }  else {				// 父进程在这里继续
        printf(“Whose your daddy?”);
        …
        child_status = wait(pid);
    }
    
    

    程序加载并执行的示例

    • 系统调用exec( )加载新程序取代当前运行进程
    main()
    …
    int pid = fork();			// 创建子进程
    if (pid == 0) {			        // 子进程在这里继续
        exec_status = exec(“calc”, argc, argv0, argv1, …);
        printf(“Why would I execute?”);
    }  else {				// 父进程在这里继续
        printf(“Whose your daddy?”);
        …
        child_status = wait(pid);
    }
    if (pid < 0) { /* error occurred */
    

    程序加载并执行的过程

    在shell中调用fork()后加载计算器

    w:900


    程序加载并执行的过程

    在shell中调用fork()后加载计算器

    w:900


    程序加载并执行的过程

    在shell中调用fork()后加载计算器

    w:900


    程序加载并执行的过程

    在shell中调用fork()后加载计算器

    w:800


    程序加载并执行的过程

    在shell中调用fork()后加载计算器

    w:800


    程序加载并执行的过程

    在shell中调用fork()后加载计算器

    w:800


    进程管理应用示例:fork()

    int  main() {
         pid_t  pid;
          int  i;
          for  (i=0;  i<LOOP;  i++){
               /* fork  another  process  */
               pid = fork();
               if  (pid < 0) { /*error  occurred  */
                    fprintf(stderr, “Fork Failed”);
                    exit(-1);
               }
               else if (pid == 0) { /* child process */
                fprintf(stdout, “i=%d, pid=%d, parent  pid=%d\n”,I, getpid() ,getppid());
               }   
          }
          wait(NULL);
          exit(0);
    } 
    

    进程管理应用示例:fork()

    w:1000


    提纲

    1. 进程的基本概念
    2. 进程管理
    3. 关于Fork()的思考

    2.1 进程管理系统调用 2.2 进程控制块PCB 2.3 进程创建和程序加载

    2.4 进程等待与退出


    父进程等待子进程

    • wait()系统调用用于父进程等待子进程的结束
      • 子进程结束时通过exit()向父进程返回一个值
      • 父进程通过wait()接受并处理返回值
    • wait()系统调用的功能
      • 有子进程存活时,父进程进入等待状态,等待子进程的返回结果
      • 当某子进程调用exit()时,唤醒父进程,将exit()返回值作为父进程中wait的返回值

    僵尸进程与孤儿进程

    • 僵尸进程:已经执行sys_exit系统调用,但还没有被父进程通过sys_wait系统调用回收其进程控制块的子进程。
      • 等待僵尸子进程时,wait()立即返回其中一个值
    • 孤儿进程:其父进程先退出的子进程。
      • 孤儿进程由root进程负责等待并回收

    进程退出exit()

    • 进程结束执行时调用exit(),完成进程资源回收
      • exit()系统调用的功能
      • 将调用参数作为进程的“结果”
      • 关闭所有打开的文件等占用资源
      • 释放内存
      • 释放大部分进程相关的内核数据结构
      • 保留结果的值,检查是否父进程存活
        • 如没有存活,设置父进程为Root进程
      • 进入僵尸(zombie/defunct)状态,等待父进程回收

    进程管理的其他相关系统调用

    • 优先级控制
      • nice()指定进程的初始优先级
      • Unix系统中进程优先级会随执行时间而衰减
    • 进程调试
      • ptrace()允许一个进程控制另一个进程的执行
      • 设置断点和查看寄存器等
    • 定时
      • sleep()可以让进程在定时器的等待队列中等待指定时间

    进程管理与进程状态的关系

    进程管理相关的系统调用可能会影响进程的状态 w:600


    进程管理与进程状态的关系

    w:700


    提纲

    1. 进程的基本概念
    2. 进程管理

    3. 关于Fork()的思考

    • Fork()的开销?
    • 重新思考fork

    Fork()的开销?

    • fork()的实现开销
      • 对子进程分配内存
      • 复制父进程的内存和CPU寄存器到子进程里
      • 开销昂贵!!

    bg right:50% 100%


    Fork()的开销?

    • 在99%的情况里,我们在调用fork()之后调用exec()
      • 在fork()操作中内存复制是没有作用的 --why?
      • 子进程将可能关闭打开的文件和网络连接? --why?

    bg right:50% 100%


    Fork()的开销?

    • vfork()创建进程时,不再创建一个同样的内存映像
      • 轻量级fork()
      • 子进程应该几乎立即调用exec()
      • 现在使用 Copy on Write (COW) 技术 bg right:50% 100%

    重新思考fork

    Andrew Baumann,etc., A fork() in the road,HotOS 2019

    w:800


    重新思考fork

    The fork system call is one of Unix's great ideas.
         -- https://cs61.seas.harvard.edu/site/2018/WeensyOS/
    
    • It’s simple: no parameters!
    • It’s elegant: fork is orthogonal to exec
    • It eased concurrency

    重新思考fork

    但是!

    • Fork is no longer simple
      • Fork encourages memory overcommit
      • Fork is incompatible with a single address space
      • Fork is incompatible with heterogeneous hardware
      • Fork infects an entire system

    重新思考fork

    但是! w:1100


    重新思考fork

    w:900


    重新思考fork

    w:1100


    重新思考fork

    For implementation expedience [Ritchie, 1979]

    • fork was 27 lines of PDP-7 assembly
      • One process resident at a time
      • Copy parent’s memory out to swap
      • Continue running child
    • exec didn’t exist – it was part of the shell
      • Would have been more work to combine them

    重新思考fork

    结论

    • Fork is not an inspired design, but an accident of history
    • Only Unix implemented it this way
    • We may be stuck with fork for a long time to come
    • But, let’s not pretend that it’s still a good idea today!

    Please, stop teaching students that fork is good design

    • Begin with spawn
    • Teach fork, but include historical context

    小结

    1. 进程的基本概念
    2. 进程管理
    3. 关于Fork()的思考
    • Fork()的开销?
    • 重新思考fork

    第七讲 进程管理与单处理器调度

    第二节 单处理器调度

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1.1 处理机调度的时机和策略

    1.2 比较调度算法的准则


    CPU资源的时分复用

    • 进程切换:CPU资源的当前占用者切换

      • 保存当前进程在PCB中的执行上下文(CPU状态)
      • 恢复下一个进程的执行上下文
    • 处理机调度

      • 从就绪队列中挑选下一个占用CPU运行的进程
      • 从多个可用CPU中挑选就绪进程可使用的CPU资源
    • 调度器:挑选就绪进程的内核函数

    • 调度策略

      • 依据什么原则挑选进程?

    调度时机

    • 内核执行调度的条件
      • 进程从运行状态切换到等待/就绪状态
      • 进程被终结了
    • 非抢占系统
      • 当前进程主动放弃CPU时
    • 可抢占系统
      • 中断请求被服务例程响应完成时

    调度策略

    确定如何从就绪队列中选择下一个执行进程

    • 要解决的问题
      • 通过什么样的准则来选择?
      • 挑选就绪队列中的哪一个进程?
    • 调度算法
      • 在内核调度中实现的调度策略
    • 比较调度算法的准则
      • 哪一个策略/算法较好?

    处理机资源的使用模式

    • 进程在CPU计算和I/O操作间交替
      • 每次调度决定在下一个CPU计算时将哪个工作交给CPU
      • 在时间片机制下,进程可能在结束当前CPU计算前被迫放弃CPU

    w:650


    提纲

    1. 处理机调度概念
    2. 调度算法

    1.1 处理机调度的时机和策略

    1.2 比较调度算法的准则


    比较调度算法的准则

    • CPU使用率 : CPU处于忙状态的时间百分比
    • 吞吐量:单位时间内完成的进程数量
    • 周转时间:进程从初始化到结束(包括等待)的总时间
    • 就绪等待时间:就绪进程在就绪队列中的总时间
    • 响应时间:从提交请求到产生响应所花费的总时间
    • 公平:进程占用相同的资源,如CPU时间等

    比较调度算法的吞吐量与延迟准则

    • 调度算法的要求:希望“更快”的服务
    • 什么是更快?
      • 传输文件时的高带宽,调度算法的高吞吐量
      • 玩游戏时的低延迟,调度算法的低响应延迟
      • 这两个因素相互影响
    • 与水管的类比
      • 低延迟:喝水的时候想要一打开水龙头水就流出来
      • 高带宽:给游泳池充水时希望从水龙头里同时流出大量的水,并且不介意是否存在延迟

    比较调度算法的响应时间准则

    • 减少响应时间
      • 及时处理用户的输入请求,尽快将输出反馈给用户
    • 减少平均响应时间的波动
      • 在交互系统中,可预测性比高差异低平均更重要
    • 低延迟调度改善了用户的交互体验
      • 如果移动鼠标时,屏幕中的光标没动,用户可能会重启电脑
    • 响应时间是操作系统的计算延迟

    比较调度算法的吞吐量准则

    • 增加吞吐量
      • 减少开销(操作系统开销,上下文切换)
      • 系统资源的高效利用(CPU,I/O设备)
    • 减少就绪等待时间
      • 减少每个就绪进程的等待时间
    • 操作系统需要保证吞吐量不受用户交互的影响
      • 即使存在许多交互任务
    • 吞吐量是操作系统的计算带宽

    比较调度算法的公平准则

    一个用户比其他用户运行更多的进程时,公平吗?怎么办?

    • 公平的定义
      • 保证每个进程占用相同的CPU时间
      • 保证每个进程的就绪等待时间相同
    • 公平通常会增加平均响应时间

    提纲

    1. 处理机调度概念

    2. 调度算法

    2.1 FCFS、SJF、SRT和HRRN

    2.2 时间片轮转算法RR 2.3 MQ、MLFQ和FSS


    先来先服务调度算法FCFS

    FCFS: First Come, First Served

    • 依据进程进入就绪状态的先后顺序排列
    • 进程进入等待或结束状态时,就绪队列中的下一个进程占用CPU
    • 指标
      • FCFS算法的周转时间

    先来先服务调度算法示例

    • 示例:3个进程,计算时间分别为12,3,3

    w:800


    先来先服务调度算法的特征

    • 优点:简单
    • 缺点
      • 平均等待时间波动较大
      • 短作业/任务/进程可能排在长进程后面
      • I/O资源和CPU资源的利用率较低
        • CPU密集型进程会导致I/O设备闲置时, I/O密集型进程也等待

    短作业优先调度算法SJF

    Short Job First

    • 选择就绪队列中执行时间最短作业/进程占用CPU进入运行状态
    • 就绪队列按预期的执行时间来排序

    w:800


    短作业优先调度算法的特征

    具有最优平均周转时间

    修改作业/进程执行顺序可能减少平均等待时间吗? w:800


    短作业优先调度算法的特征

    • 可能导致饥饿

      • 连续的短作业/进程流会使长作业/进程无法获得CPU资源
    • 需要预知未来

      • 如何预估下一个CPU计算的持续时间?
      • 简单的解决办法:询问用户
        • 用户欺骗就杀死相应进程
        • 用户不知道怎么办?

    短作业优先算法的执行时间预估

    • 用历史的执行时间来预估未来的执行时间

    $\tau_{n+1} = \alpha t_n+(1-\alpha) \tau_n,其中 0\le \alpha \le 1$

    • $t_n$ -- 第n次的CPU计算时间
    • $\tau_{n+1}$ -- 第n+1次的CPU计算时间预估

    $\tau_{n+1} = \alpha t_n+(1-\alpha) \alpha t_{n-1} + (1-\alpha) (1-\alpha) \alpha t_{n-2} + ...$


    短作业优先算法的执行时间预估

    • 执行时间预估 w:800

    最短剩余时间算法SRT

    Shortest Remaining Time, SRT

    • SRT支持抢占调度机制,即有新的进程就绪,且新进程的服务时间小于当前进程的剩余时间,则转到新的进程执行。

    最高响应比优先算法HRRN

    Highest Response Ratio Next,HRRN

    • 高响应比优先调度算法主要用于作业调度
    • 该算法是对FCFS调度算法和SJF调度算法的一种综合平衡,同时考虑每个作业的就绪等待时间和估计的运行时间
    • 在每次进行作业调度时,先计算后备作业队列中每个作业的响应比,从中选出响应比最高的作业投入运行。

    最高响应比优先算法HRRN

    • 选择就绪队列中响应比R值最高的进程

      • $R=(w+s)/s$
      • w: 就绪等待时间(waiting time)
      • s: 执行时间(service time)
    • 在短作业优先算法的基础上改进

    • 关注进程的等待时间

    • 防止无限期推迟


    提纲

    1. 处理机调度概念
    2. 调度算法

    2.1 FCFS、SJF、SRT和HRRN

    2.2 时间片轮转算法RR

    2.3 MQ、MLFQ和FSS


    时间片轮转算法RR

    RR, Round-Robin

    • 时间片
      • 分配处理机资源的基本时间单元
    • 算法思路
      • 时间片结束时,按FCFS算法切换到下一个就绪进程
      • 每隔(n – 1)个时间片进程执行一个时间片q w:700

    时间片轮转算法示例

    w:700


    时间片轮转算法的时间片长度参数

    • RR算法开销: 额外的上下文切换
    • 时间片太大
      • 等待时间过长,极限情况退化成FCFS
    • 时间片太小
      • 反应迅速,但产生大量上下文切换
      • 大量上下文切换开销影响到系统吞吐量
    • 时间片长度选择目标
      • 选择一个合适的时间片长度
      • 经验规则:维持上下文切换开销处于1%以内

    比较FCFS和RR

    w:800


    提纲

    1. 处理机调度概念
    2. 调度算法

    2.1 FCFS、SJF、SRT和HRRN 2.2 时间片轮转算法RR

    2.3 MQ、MLFQ和FSS


    多级队列调度算法MQ

    MQ, MultiQueue

    • 就绪队列被划分成多个独立的子队列

      • 如:前台进程(交互)子队列、后台进程(批处理)子队列
      • 同一优先级的进程属于某个队列,且不能跨越队列
    • 每个队列拥有自己的调度策略

      • 如:前台进程–RR、后台进程–时间片大的RR/FCFS
    • 规则1:如果A的优先级 > B的优先级,运行A(不运行B)。

    • 规则2:如果A的优先级 = B的优先级,轮转运行A和B。


    多级队列调度算法MQ

    • 队列间的调度
      • 固定优先级
        • 先处理前台(交互)进程,然后处理后台进程
        • 可能导致饥饿
      • 时间片轮转
        • 每个队列都得到一个确定的能够调度其进程的CPU总时间
        • 如:80%CPU时间用于前台进程,20%CPU时间用于后台进程

    多级反馈队列调度算法MLFQ

    MLFQ, Multi-Level Feedback Queue

    • 1962年,MIT教授Corbato首次提出多级反馈队列,应用于兼容时分共享系统(CTSS-Compatible Time-Sharing System)
    • 解决两方面的问题
      • 如何在不知道工作要运行多久的情况下,优化周转时间
      • 如何降低响应时间,给交互用户很好的交互体验

    多级反馈队列调度算法MLFQ

    • 关键问题:没有完备的知识如何调度?
      • 对进程工作长度未知情况下,如何构建能同时减少响应时间和周转时间的调度程序?
    • 启发:从历史中学习 
      • 用历史经验预测未来
    • 继承Multi Queue的调度规则
      • 如果A的优先级 > B的优先级,运行A(不运行B)
      • 如果A的优先级 = B的优先级,轮转/FIFO运行A和B

    多级反馈队列调度算法MLFQ

    基本调度规则

    • 工作进入系统时,放在最高优先级(最上层队列)
    • 如进程在当前的时间片没有完成,则降到下一个优先级
    • 如果工作在其时间片以内主动释放CPU,则优先级不变
    • 时间片大小随优先级级别增加而增加 w:500

    三个优先级队列的MLFQ调度例子

    • CPU密集型进程首先进入最高优先级队列;
    • 执行1ms时间片后,调度器将进程的优先级减1,进入次高优先级队列;
    • 执行2ms时间片后,进入系统的最低优先级队列,一直留在那里,按4ms时间片执行。

    多级反馈队列调度算法MLFQ

    • MLFQ算法的特征

      • CPU密集型进程的优先级下降很快
      • I/O密集型进程停留在高优先级
    • 潜在问题

      • CPU密集型进程会饥饿
      • 恶意进程会想办法留在高优先级 bg right:40% 100%

    多级反馈队列调度算法MLFQ

    基本调度规则

    • 如果A的优先级 > B的优先级,运行A(不运行B)
    • 如果A的优先级 = B的优先级,轮转/FIFO运行A和B
    • 工作进入系统时,放在最高优先级(最上层队列)
    • 一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次CPU),就降低其优先级(移入低一级队列)
    • 经过一段时间S,就将系统中所有工作重新加入最高优先级队列

    公平共享调度算法FSS

    FSS, Fair Share Scheduling

    • 控制用户对系统资源的访问
      • 不同用户拥有多个进程
      • 按用户优先级分配资源
      • 保证不重要的用户无法垄断资源
      • 未使用的资源按比例分配

    bg right:40% 100%


    调度算法的特征

    • 先来先服务算法
      • 平均等待时间较差
    • 短作业优先算法
      • 平均周转时间最小
      • 需要精确预测计算时间
      • 不允许抢占;可能导致饥饿
    • 最短剩余时间算法
      • 对短作业优先算法的改进,允许抢占
      • 可能导致饥饿

    调度算法的特征

    • 最高响应比优先算法
      • 基于短作业优先调度,不可抢占
      • 同时考虑每个作业的等待时间和估计的运行时间
    • 时间片轮转算法
      • 公平,但是平均等待时间较差
    • 多级反馈队列算法
      • 多种算法的集成
    • 公平共享调度算法
      • 公平是第一要素

    小结

    1. 处理机调度概念
      • 处理机调度的时机和策略
      • 比较调度算法的准则
    2. 调度算法
      • FCFS、SJF、SRT、HRRN
      • RR
      • MQ、MLFQ、FSS

    第七讲 进程管理与单处理器调度

    第三节 实时调度

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 实时操作系统

    1. 实时调度
    2. 优先级反置
    • 实时操作系统的定义
    • 实时任务

    实时操作系统的定义

    • 实时操作系统的定义
      • 正确性依赖于其时间和功能两方面的操作系统
    • 实时操作系统的性能指标
      • 时间约束的及时性(deadlines)
      • 速度和平均性能相对不重要
    • 实时操作系统的特性
      • 时间约束的可预测性

    实时操作系统分类

    • 强/硬实时操作系统
      • 要求在指定的时间内必须完成重要的任务
    • 弱/软实时操作系统
      • 重要进程有高优先级,要求尽量但非必须完成

    实时任务

    • 任务(工作单元)
      • 一次计算,一次文件读取,一次信息传递等等
    • 任务属性
      • 完成任务所需要的资源
      • 定时参数

    w:1100


    周期实时任务

    • 周期实时任务:一系列相似的任务
      • 任务有规律地重复
      • 周期p = 任务请求时间间隔 (0 <p)
      • 执行时间e = 最大执行时间(0 < e < p)
      • 使用率U = e/p w:900
    • 可调度:if $\sum_{p_i} \frac{e_i}{p_i}\leq 1$; otherwise 不可调度

    软时限和硬时限

    • 硬时限(Hard deadline)
      • 错过任务时限会导致灾难性或非常严重的后果
      • 必须验证,在最坏情况下能够满足时限
    • 软时限(Soft deadline)
      • 通常能满足任务时限
      • 如有时不能满足,则降低要求
      • 尽力保证满足任务时限

    可调度性

    • 可调度表示一个实时操作系统能够满足任务时限要求
      • 需要确定实时任务的执行顺序
      • 静态优先级调度:任务执行过程中不会改变任务的优先级
      • 动态优先级调度:任务执行过程中改变任务的优先级 w:900

    提纲

    1. 实时操作系统

    2. 实时调度

    1. 优先级反置
    • 速率单调调度算法
    • 最早截止时间优先算法
    • 最低松弛度优先算法

    实时调度

    • 静态优先级调度:速率单调调度算法(RM, Rate Monotonic)

      • 通过周期安排优先级
      • 周期越短优先级越高
      • 执行周期最短的任务
    • 动态优先级调度:最早截止时间优先算法 (EDF, Earliest Deadline First)

      • 截止时间越早优先级越高
      • 执行离截止时间最近的任务

    如果任务间有共享资源占用的情况,高优先级任务可能会被延误执行!


    速率单调调度算法(RM, Rate Monotonic)

    • 根据任务周期来确定任务优先级(周期越短优先级越高,抢占式)
      • 进程P1:e=20 p=50
      • 进程P2:e=35 p=100

    w:900


    速率单调调度算法(RM, Rate Monotonic)

    • 根据任务周期来确定任务优先级(周期越短优先级越高,抢占式)
      • 进程P1:e=25 p=50
      • 进程P2:e=35 p=80

    w:900


    最早截止时间优先算法 (EDF, Earliest Deadline First)

    • 固定优先级的问题:有的任务可能错过期限
      • 进程P1:e=10 p=20
      • 进程P2:e=25 p=50

    w:1000


    最早截止时间优先算法 (EDF, Earliest Deadline First)

    • 固定优先级的问题:有的任务可能错过期限
      • 进程P1:e=10 p=20
      • 进程P2:e=25 p=50

    w:1000


    最早截止时间优先算法 (EDF, Earliest Deadline First)

    • 固定优先级的问题:有的任务可能错过期限
      • 进程P1:e=10 p=20
      • 进程P2:e=25 p=50

    w:1000


    最早截止时间优先算法 (EDF, Earliest Deadline First)

    • 任务的优先级根据任务的截止时间动态分配。截止时间越短,优先级越高。
      • 进程P1:e=10 p=20
      • 进程P2:e=25 p=50

    w:1000


    最低松弛度优先算法(LLF)

    • 根据任务紧急或者松弛程度,来确定任务优先级
      • 任务紧急度越高,优先级越高
      • 松弛度=必须完成时间-本身还需要运行时间-当前时间
      • 进程P1:e=10 p=20
      • 进程P2:e=25 p=50 w:1100

    提纲

    1. 实时操作系统
    2. 实时调度

    3. 优先级反置

    • 优先级继承
    • 优先级天花板协议

    优先级反置(Priority Inversion)

    高优先级进程长时间等待低优先级进程所占用资源的现象

    • 基于优先级的可抢占调度算法存在优先级反置问题 优先级:T1>T2>T3 w:800

    优先级反置(Priority Inversion)

    高优先级进程长时间等待低优先级进程所占用资源的现象

    • 基于优先级的可抢占调度算法存在优先级反置问题 优先级:T1>T2>T3 w:800

    优先级继承(Priority Inheritance)

    • 占用资源的低优先级进程继承申请资源的高优先级进程的优先级
    • 只在想占有资源的高优先级进程被阻塞时,才提高占有资源的低优先级进程的优先级。
      • 注:临界区:互斥访问共享资源的代码片段 w:750

    优先级继承(Priority Inheritance)

    • 占用资源的低优先级进程继承申请资源的高优先级进程的优先级
    • 只在想占有资源的高优先级进程被阻塞时,才提高占有资源的低优先级进程的优先级。注:临界区:互斥访问共享资源的代码片段 w:600

    优先级继承(Priority Inheritance)

    • 占用资源的低优先级进程继申请资源的高优先级进程的优先级
    • 只在想占有资源的高优先级进程被阻塞时,才提高占有资源的低优先级进程的优先级。注:临界区:互斥访问共享资源的代码片段 w:600

    优先级继承(Priority Inheritance)

    • 占用资源的低优先级进程继承申请资源的高优先级进程的优先级
    • 只在想占有资源的高优先级进程被阻塞时,才提高占有资源的低优先级进程的优先级。注:临界区:互斥访问共享资源的代码片段 w:700

    优先级继承(Priority Inheritance)

    • 占用资源的低优先级进程继承申请资源的高优先级进程的优先级
    • 只在想占有资源的高优先级进程被阻塞时,才提高占有资源的低优先级进程的优先级。注:临界区:互斥访问共享资源的代码片段

    w:650


    优先级继承(Priority Inheritance)

    • 占用资源的低优先级进程继承申请资源的高优先级进程的优先级
    • 只在想占有资源的高优先级进程被阻塞时,才提高占有资源的低优先级进程的优先级。注:临界区:互斥访问共享资源的代码片段

    w:650


    优先级继承(Priority Inheritance)

    • 占用资源的低优先级进程继承申请资源的高优先级进程的优先级
    • 只在想占有资源的高优先级进程被阻塞时,才提高占有资源的低优先级进程的优先级。注:临界区:互斥访问共享资源的代码片段

    w:650


    优先级天花板协议(priority ceiling protocol)

    • 占用资源进程的优先级与所有可能申请该资源的进程的最高优先级相同
      • 不管是否发生等待,都提升占用资源进程的优先级
      • 优先级高于系统中所有被锁定的资源的优先级上限,任务执行临界区时就不会被阻塞

    小结

    1. 实时操作系统
      • 实时操作系统的定义、实时任务
    2. 实时调度
      • 速率单调调度算法、最早截止时间优先算法、最低松弛度优先算法
    3. 优先级反置
      • 优先级继承、优先级天花板协议

    第七讲 进程管理与单处理器调度

    第四节 实践:支持进程的操作系统

    Process OS(POS)

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 实验目标和步骤

    1. 代码结构
    2. 应用程序设计
    3. 内核程序设计
    • 实验目标
    • 实践步骤

    以往目标

    提高性能、简化开发、加强安全

    • Address Space OS
      • APP不用考虑运行时的起始执行地址,隔离APP访问的地址空间
    • multiprog & time-sharing OS
      • 让APP有效共享CPU,提高系统总体性能和效率
    • BatchOS: 让APP与OS隔离,加强系统安全,提高执行效率
    • LibOS: 让APP与HW隔离,简化应用访问硬件的难度和复杂性

    实验目标

    增强进程管理和资源管理、提高性能、简化开发、加强安全

    • 整合之前的特权级、地址空间、任务,形成进程
    • 进程成为资源的拥有者
    • 扩展进程动态特征,能够在应用层面发出如下系统调用请求:
      • 动态创建子进程
      • 动态构造新进程
      • 子进程退出/父进程等待子进程退出

    实验要求

    • 理解进程概念
    • 理解进程的动态管理机制的设计与实现
    • 初步认识进程调度
    • 掌握shell应用的编写与使用
    • 会写支持进程的操作系统

    bg right 80%


    总体思路

    bg right:76% 90%


    总体思路

    bg right:76% 85%


    总体思路

    • 编译:应用程序和内核独立编译,合并为一个镜像
    • 编译:不同应用程序可采用统一的起始地址
    • 构造:系统调用服务,进程的管理与初始化
    • 构造:建立基于页表机制的虚存空间
    • 运行:特权级切换,进程与OS相互切换
    • 运行:切换地址空间,跨地址空间访问数据

    历史背景

    • 1965:描述未来的 MULTICS 操作系统
      • MIT 的 Fernando J. Corbató 教授牵头
      • 参与单位:MIT, GE(通用电气公司), AT&T Bell Labs
      • 提出了进程的动态管理思想,启发和造就了UNIX
    • 1971:Thompson shell
      • 由Ken Thompson写的第一个UNIX Shell
      • 按照极简主义设计,语法非常简单,是一个简单的命令行解释器
      • 它的许多特征影响了以后的操作系统命令行界面的发展

    实践步骤

    git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
    cd rCore-Tutorial-v3
    git checkout ch5
    cd os
    make run
    

    实践步骤

    [RustSBI output]
    ...
    yield
    **************/
    Rust user shell
    >>
    

    操作系统启动shell后,用户可以在shell中通过敲入应用名字来执行应用。


    软件架构

    • 管理进程
      • 创建
      • 回收
      • fork
      • exec

    bg right:74% 90%


    提纲

    1. 实验目标和步骤

    2. 代码结构

    1. 应用程序设计
    2. 内核程序设计

    改进OS

    ├── os
        ├── build.rs(修改:基于应用名的应用构建器)
        └── src
             ├── loader.rs(修改:基于应用名的应用加载器)
             ├── main.rs(修改)
             ├── mm(修改:为了支持本章的系统调用对此模块做若干增强)
    

    改进OS

    ├── os
        └── src
             ├── syscall
                 ├── fs.rs(修改:新增 sys_read)
                 ├── mod.rs(修改:新的系统调用的分发处理)
                 └── process.rs(修改:新增 sys_getpid/fork/exec/waitpid)
             ├── task
                 ├── manager.rs(新增:任务管理器,为上一章任务管理器功能的一部分)
                 ├── mod.rs(修改:调整原来的接口实现以支持进程)
                 ├── pid.rs(新增:进程标识符和内核栈的 Rust 抽象)
                 ├── processor.rs(新增:处理器管理结构 ``Processor`` ,为上一章任务管理器功能的一部分)
                 └── task.rs(修改:支持进程管理机制的任务控制块)
             └── trap
                  ├── mod.rs(修改:对于系统调用的实现进行修改以支持进程系统调用)
    

    提纲

    1. 实验目标和步骤
    2. 代码结构

    3. 应用程序设计

    1. 内核程序设计

    理解进程

    • 应用角度

      • 进程 是正在执行的应用
    • OS角度

      • 进程 是应用在其地址空间上的一次执行过程
        • 进程拥有资源,操作系统根据进程的执行状态管理其资源 bg right:50% 100%

    进程管理系统调用

    /// 功能:当前进程 fork 出来一个子进程。
    /// 返回值:对于子进程返回 0,对于当前进程则返回子进程的 PID
    /// syscall ID:220
    pub fn sys_fork() -> isize;
    
    /// 功能:将当前进程的地址空间清空并加载一个特定的可执行文件,返回用户态后开始它的执行。
    /// 参数:path 给出了要加载的可执行文件的名字;
    /// 返回值:如果出错的话(如找不到名字相符的可执行文件)则返回 -1,否则不应该返回。
    /// syscall ID:221
    pub fn sys_exec(path: &str) -> isize;
    

    进程管理系统调用

    /// 功能:当前进程等待一个子进程变为僵尸进程,回收其全部资源并收集其返回值。
    /// 参数:pid 表示要等待的子进程的进程 ID,如果为 -1 的话表示等待任意一个子进程;
    /// exit_code 表示保存子进程返回值的地址,如果这个地址为 0 的话表示不必保存。
    /// 返回值:如果要等待的子进程不存在则返回 -1;否则如果要等待的子进程均未结束则返回 -2;
    /// 否则返回结束的子进程的进程 ID。
    /// syscall ID:260
    pub fn sys_waitpid(pid: isize, exit_code: *mut i32) -> isize;
    

    应用shell的执行流程

    1. 通过sys_read获取字符串(即文件名)
    2. 通过sys_fork创建子进程
    3. 在子进程中通过sys_exec创建新应用的进程
    4. 在父进程中通过sys_waitpid等待子进程结束
    5. 跳转到第一步循环执行

    提纲

    1. 实验目标和步骤
    2. 代码结构
    3. 应用程序设计

    4. 内核程序设计

    4.1 应用的链接与加载支持

    4.2 核心数据结构 4.3 进程管理机制实现


    应用的链接与加载支持

    在编译操作系统的过程中,会生成如下的 link_app.S 文件

     3 _num_app:
     4     .quad 15            #应用程序个数
     7 ......
     9 _app_names:             #app0的名字
    10     .string "exit"          
    12 ......
    17 app_0_start:            #app0的开始位置
    18     .incbin "../user/target/riscv64gc-unknown-none-elf/release/exit"
    19 app_0_end:              #app0的结束位置
    

    基于应用名的应用加载

    在加载器 loader.rs 中,分析 link_app.S 中的内容,并用一个全局可见的 只读 向量 APP_NAMES 来按照顺序将所有应用的名字保存在内存中,为通过 exec 系统调用创建新进程做好了前期准备。


    提纲

    1. 实验目标和步骤
    2. 代码结构
    3. 应用程序设计
    4. 内核程序设计

    4.1 应用的链接与加载支持

    4.2 核心数据结构

    4.3 进程管理机制实现


    核心数据结构间的关系

    bg right:70% 100%


    进程控制块TCB

    进程抽象的对应实现是进程控制块 -- TCB TaskControlBlock

    #![allow(unused)]
    fn main() {
    pub struct TaskControlBlock {
        // immutable
        pub pid: PidHandle,                      // 进程id
        pub kernel_stack: KernelStack,           // 进程内核栈
        // mutable
        inner: UPSafeCell<TaskControlBlockInner>,//进程内部管理信息
    }
    }

    进程控制块TCB

    进程抽象的对应实现是进程控制块 -- TCB TaskControlBlock

    #![allow(unused)]
    fn main() {
    pub struct TaskControlBlockInner {
        pub trap_cx_ppn: PhysPageNum,               // 陷入上下文页的物理页号
        pub base_size: usize,                       // 进程的用户栈顶
        pub task_cx: TaskContext,                   // 进程上下文
        pub task_status: TaskStatus,                // 进程执行状态  
        pub memory_set: MemorySet,                  // 进程地址空间
        pub parent: Option<Weak<TaskControlBlock>>, // 父进程控制块
        pub children: Vec<Arc<TaskControlBlock>>,   // 子进程任务控制块组
        pub exit_code: i32,                         // 退出码
    }
    }

    进程管理器TaskManager

    • 任务管理器自身仅负责管理所有就绪的进程
    #![allow(unused)]
    fn main() {
    pub struct TaskManager {
        ready_queue: VecDeque<Arc<TaskControlBlock>>,  // 就绪态任务控制块的链表
    }
    }

    处理器管理结构

    处理器管理结构 Processor 描述CPU 执行状态

    #![allow(unused)]
    fn main() {
    pub struct Processor {
        current: Option<Arc<TaskControlBlock>>, // 在当前处理器上正在执行的任务
        idle_task_cx: TaskContext,              // 空闲任务
    }
    }
    • 负责从任务管理器 TaskManager 中分出去的维护 CPU 状态的职责
    • 维护在一个处理器上正在执行的任务,可以查看它的信息或是对它进行替换
    • Processor 有一个 idle 控制流,功能是尝试从任务管理器中选出一个任务来在当前 CPU 核上执行,有自己的CPU启动内核栈上

    提纲

    1. 实验目标和步骤
    2. 代码结构
    3. 应用程序设计
    4. 内核程序设计

    4.1 应用的链接与加载支持 4.2 核心数据结构

    4.3 进程管理机制实现


    进程管理机制实现概述

    1. 创建初始进程:创建第一个用户态进程 initproc
    2. 进程生成机制:介绍进程相关的系统调用 sys_fork/sys_exec
    3. 进程调度机制:进程主动/被动切换
    4. 进程资源回收机制:调用 sys_exit 退出或进程终止后保存其退出码
    5. 进程资源回收机制:父进程通过 sys_waitpid 收集该进程的信息并回收其资源
    6. 字符输入机制:通过sys_read 系统调用获得字符输入

    创建初始进程

    #![allow(unused)]
    fn main() {
    lazy_static! {
        pub static ref INITPROC: Arc<TaskControlBlock> = Arc::new(
            TaskControlBlock::new(get_app_data_by_name("initproc").unwrap()));
    }
    pub fn add_initproc() {
        add_task(INITPROC.clone());
    }
    }
    • TaskControlBlock::new 会解析initproc的ELF执行文件格式,并建立应用的地址空间、内核栈等,形成一个就绪的进程控制块
    • add_task会把进程控制块加入就绪队列中

    创建新进程fork()

    复制父进程内容并构造新的进程控制块

    #![allow(unused)]
    fn main() {
    pub fn fork(self: &Arc<TaskControlBlock>) -> Arc<TaskControlBlock> {...}
    }
    • 建立新页表,复制父进程地址空间的内容
    • 创建新的陷入上下文
    • 创建新的应用内核栈
    • 创建任务上下文
    • 建立父子关系
    • 设置0fork返回码

    加载新应用exec()

    用新应用的 ELF 可执行文件中的代码和数据替换原有的应用地址空间中的内容

    #![allow(unused)]
    fn main() {
    pub fn exec(&self, elf_data: &[u8]) {...}
    }
    • 回收已有应用地址空间,基于ELF 文件的全新的地址空间直接替换已有应用地址空间
    • 修改进程控制块的 Trap 上下文,将解析得到的应用入口点、用户栈位置以及一些内核的信息进行初始化

    进程调度机制

    暂停当前任务并切换到下一个任务

    • 时机
      • sys_yield系统调用时
      • 进程的时间片用完时
    • 操作
      • 执行suspend_current_and_run_next 函数
        • 取出当前正在执行的任务,修改其状态,放入就绪队列队尾
        • 接着调用 schedule 函数来触发调度并切换任务

    进程资源回收机制

    进程退出exit_current_and_run_next

    • 当前进程控制块从PROCESSOR中取出,修改其为僵尸进程
    • 退出码 exit_code 写入进程控制块中
    • 把所有子进程挂到initproc的子进程集合中
    • 释放应用地址空间
    • 接着调用 schedule 函数来触发调度并切换任务

    进程资源回收机制

    等待子进程退出sys_waitpid

    • 不存在进程 ID 为 pid(pid==-1 或 > 0)的子进程时,返回 -1
    • 存在进程 ID 为 pid 的僵尸子进程时,正常回收子进程,返回子进程pid,更新退出码参数为 exit_code
    • 子进程还没退出时,返回 -2,用户库看到是 -2 后,就进一步调用 sys_yield 系统调用,让父进程进入等待状态
    • 返回前,释放子进程的进程控制块

    字符输入机制

    #![allow(unused)]
    fn main() {
    pub fn sys_read(fd: usize, buf: *const u8, len: usize) -> isize {
       c=sbi::console_getchar(); ...}
      
    }
    • 目前仅支持每次只能读入一个字符
    • 调用 sbi 子模块提供的从键盘获取输入的接口 console_getchar

    支持进程的操作系统POS

    • 进程概念与进程实现的关系
    • 进程管理机制
    • 基本调度机制
    • 能写伤齿龙OS bg right 70%

    小结

    提纲

    1. 实验目标和步骤
    2. 代码结构
    3. 应用程序设计
    4. 内核程序设计
    • 应用的链接与加载支持
    • 核心数据结构
    • 进程管理机制实现

    bg right:57% 100%

    第八讲 多处理器调度

    第一节 对称多处理与多核架构

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 多处理机器

    1. Cache一致性(Cache Coherence)

    单核处理器

    w:800


    超线程(Hyperthread, Simultaneous multithreading)处理器

    • 将CPU内部暂时闲置处理资源充分调动起来
      • 寄存器、程序计数器独立
      • 算术计算单元等硬件共用
      • 不适合计算密集型任务
      • 适合IO密集型任务

    bg right 80%


    多核(multi-core)处理器

    w:1150


    众核(many-core)处理器

    w:1150


    提纲


    对称多处理器(SMP)与非一致内存访问系统(NUMA)

    w:1000


    Cache 一致性 (Cache Coherence)

    w:800


    Cache 一致性问题

    w:900

    第八讲 多处理器调度

    第二节 多处理器调度概述

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 单队列多处理器调度SQMS

    1. 多队列多处理器调度MQMS

    单队列多处理器调度

    Single Queue Multiprocessor Scheduling, SQMS

    • 复用单处理器调度下的基本架构
    • 所有需要调度的进程放入一个队列w:800

    单队列多处理器调度的特征

    • 缺乏可扩展性 (scalability)
    • 缓存亲和性 (cache affinity) 弱 w:800

    多处理器调度的亲和度与负载均衡

    尽可能让进程在同一个 CPU 上运行。保持一些进程的亲和度的同时,可能需要牺牲其他进程的亲和度来实现负载均衡。 bg right:63% 90%


    提纲

    1. 单队列多处理器调度SQMS

    2. 多队列多处理器调度MQMS


    多队列多处理器调度

    Multi-Queue MultiprocessorScheduling, MQMS

    • 基本调度框架包含多个调度队列,每个队列可用不同的调度规则。
    • 依照一些启发性规则,在进程进入系统时,将其放入某个调度队列。
    • 每个 CPU 调度相互独立,避免单队列方式的数据共享及同步问题。 w:800 w:800

    多队列多处理器调度的特征

    • 具有可扩展性:队列的数量会随着CPU 的增加而增加,因此锁和缓存争用的开销不是大问题。
    • 具有良好的缓存亲和度:所有进程都保持在固定的 CPU 上,因而可以很好地利用缓存数据。 w:800 w:800

    多队列多处理器调度的负载不均

    • 假定4个进程,2个CPU;队列都执行轮转调度策略;进程C执行完毕
    • A 获得了 B 和 D 两倍的 CPU 时间

    w:1000 w:1000


    如何解决MQMS的负载不均?

    • 假定 4 个进程,2 个 CPU;每个队列都执行轮转调度策略;A 和 C 都执行完毕,系统中只有 B 和 D
      • CPU1 很忙
      • CPU0 空闲

    w:1000 w:1000


    进程迁移 (migration)

    • 通过进程的跨 CPU 迁移,可实现负载均衡。
      • 情况:CPU0 空闲,CPU1 有一些进程。
      • 迁移:将 B 或 D 迁移到 CPU0

    w:1000 w:1000


    MQMS如何确定进程迁移时机?

    • 情况:A 独自留在 CPU 0 上,B 和 D 在 CPU 1 上交替运行
    • 迁移:不断地迁移和切换一个或多个进程

    w:1000 w:1000


    MQMS的工作窃取 (work stealing)

    • 进程量较少的 (源) 队列不定期地“偷看”其他 (目标) 队列是不是比自己的进程多
    • 如果目标队列比源队列 (显著地) 更满,就从目标队列“窃取”一个或多个进程,实现负载均衡。

    w:1000 w:1000


    工作窃取的队列检查间隔

    • 如果频繁地检查其他队列,就会带来较高的开销,可扩展性不好
    • 如果检查间隔太长,又可能会带来严重的负载不均 w:1000 w:1000

    第八讲 多处理器调度

    第三节 Linux O(1) 调度

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. SMP 和 早期Linux 内核

    1. Linux O(n)调度器
    2. Linux O(1) 调度器

    Linux调度器的演进

    • O(n) 调度器:内核版本 2.4-2.6
    • O(1) 调度器:内核版本 2.6.0-2.6.22
    • CFS 调度器:内核版本 2.6.23-至今

    w:650


    调度器需要考虑的关键问题

    • 采用何种数据结构来组织进程
    • 如何根据进程优先级来确定进程运行时间
    • 如何判断进程类型(I/O密集、CPU密集型;实时、非实时)
    • 如何确定进程的动态优先级:影响因素
      • 静态优先级、nice值
      • I/O密集型和CPU密集型产生的优先级奖惩
    • 如何适配多处理器情况

    SMP 和 早期Linux 内核

    • Linux 1.2
      • 环形队列 + Round Robin调度策略
    • Linux 2.0
      • SMP 支持由一个“大锁”组成,“大锁”对内核访问串行化
      • 在用户态支持并行,Linux 内核本身并不能利用多处理器加速
    • Linux 2.2
      • 引入调度类(real-time, non-real-time)

    提纲

    1. SMP 和 早期Linux 内核

    2. Linux O(n)调度器

    1. Linux O(1) 调度器

    Linux 2.4 内核:Linux $O(n)$调度器

    w:700


    Linux $O(n)$调度器

    • 使用多处理器可以加快内核的处理速度,调度器是复杂度为 $O(n)$
      • $O(n)$ 这个名字,来源于算法复杂度的大$O$表示法
      • 字母$n$在这里代表操作系统中的活跃进程数量
      • $O(n)$ 表示这个调度器的时间复杂度和活跃进程的数量成正比

    bg right:43% 90%


    Linux $O(n)$ 调度算法的思路

    • 把时间分成大量的微小时间片(Epoch)
    • 每个时间片开始时
      • 计算进程的动态优先级
      • 将进程优先级映射成缺省时间片
      • 然后选择优先级最高的进程来执行
    • 进程被调度器切换执行后,可不被打扰地用尽这个时间片
    • 如进程没有用尽时间片,则剩余时间增加到进程的下一个时间片中

    $O(n)$ 调度算法的复杂度

    O(n)调度算法的复杂度为$O(n)$

    • 每次使用时间片前都要检查所有就绪进程的优先级
    • 检查时间和进程中进程数目$n$成正比

    Linux O(n)调度器数据结构

    • 只用一个 global runqueue放置就绪任务
    • 各个 core 需要竞争同一个 runqueue 里面的任务

    bg right:52% 90%


    Linux $O(n)$调度器的缺点

    • $O(n)$的执行开销
      • 当有大量进程在运行时,这个调度器的性能将会被大大降低
    • 多处理器竞争访问同一个 runqueue 里面的任务
      • $O(n)$调度器没有很好的可扩展性(scalability)

    bg right:40% 100%


    提纲

    1. SMP 和 早期Linux 内核
    2. Linux O(n)调度器

    3. Linux O(1) 调度器


    Linux O(1) 调度器

    Linux 2.6 版本的调度器是由 Ingo Molnar 设计并实现的。

    • 为唤醒、上下文切换和定时器中断开销建立一个完全 O(1) 的调度器 bg right:52% 90%

    Linux O(1) 调度器的思路

    • 实现了per-cpu-runqueue,每个CPU都有一个就绪进程任务队列
    • 采用全局优先级
      • 实时进程0-99
      • 普通进程100-139

    bg right:45% 90%


    Linux O(1) 调度器的思路

    • 活跃数组active:放置就绪进程
    • 过期数组expire:放置过期进程 bg right:62% 90%

    Linux O(1) 调度器的思路

    • 每个优先级对应一个链表
    • 引入bitmap数组来记录140个链表中的活跃进程情况 bg right:65% 90%

    常用数据结构访问的时间复杂度

    • 满足 O(1) 的数据结构?
    • 常用数据结构的四种基本操作和时间复杂度
      • access:随机访问
        • array: 平均情况和最坏情况均能达到 O(1)
        • linked list 是 O(N)
        • tree 一般是 O(log N) bg right:53% 100%

    常用数据结构的搜索操作

    • search:搜索
      • hash table 时间复杂度是 O(1),但它最坏情况下是 O(N)
      • 大部分 tree(b-tree / red-black tree)平均和最坏情况都是 O(log N) bg right:53% 100%

    常用数据结构的插入和删除操作

    • insert/deletion:插入和删除
      • hash table 时间复杂度是 O(1),但它最坏情况下是 O(N)
      • linked list,stack,queue 在平均和最坏情况下都是 O(1)

    bg right:43% 100%


    Linux O(1) 调度器的时间复杂度

    • 进程有 140 种优先级,可用长度为 140 的数组去记录优先级。
      • access 是 $O(1)$
    • 位图bitarray为每种优先级分配一个 bit
      • 如果这个优先级队列下面有进程,那么就对相应的 bit 染色,置为 1,否则置为 0。
      • 问题简化为寻找位图中最高位是 1 的 bit(left-most bit),可用一条CPU 指令实现。

    bg right:37% 100%


    Linux O(1) 调度器的时间复杂度

    • 每个优先级下面用一个FIFO queue 管理这个优先级下的进程。
      • 新来的插到队尾,先进先出,insert/deletion 都是 $O(1)$

    bg right:43% 100%


    Linux $O(1)$活跃数组和过期数组

    活跃数组(Active Priority Array, APA)过期数组(Expired Priority Array, EPA)

    • 在 active bitarray 中寻找 left-most bit 的位置 x;
    • 在 APA 中找到对应队列 APA[x];
    • 从 队列APA[x] 中取出一个进程;

    bg right:40% 100%


    Linux $O(1)$活跃数组和过期数组

    • 对于当前执行完的进程,重新计算其优先级,然后 放入到 EPA 相应的队列EPA[priority];
    • 如果进程优先级在 expired bitarray 里对应的 bit 为 0,将其置 1;
    • 如果 active bitarray 全为零,将 active bitarray 和 expired bitarray 交换;

    bg right:40% 100%


    Linux O(1) 调度器的多核/SMP支持

    • 按一定时间间隔,分析各CPU负载
      • 在每个时钟中断后进行计算CPU负载
      • 由负载轻的 CPU pulling 进程而不是 pushing进程 bg right:52% 100%

    小结

    1. SMP 和 早期Linux 内核
    2. Linux O(n)调度器
    3. Linux O(1) 调度器

    参考文献

    • http://www.wowotech.net/process_management/scheduler-history.html
    • https://courses.engr.illinois.edu/cs423/sp2018/slides/13-linux-schedulers.pdf
    • https://www.cnblogs.com/vamei/p/9364382.html
    • https://cloud.tencent.com/developer/article/1077507?from=article.detail.1603917
    • https://www.eet-china.com/mp/a111242.html
    • https://loda.hala01.com/2017/06/linux-kernel.html
    • https://jishuin.proginn.com/p/763bfbd2df25

    第八讲 多处理器调度

    第四节 Linux CFS 调度

    完全公平调度(CFS, Completely Fair Scheduler)

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. CFS的原理

    1. CFS 的实现

    CFS的背景

    • O(1)和O(n)都将CPU资源划分为时间片
      • 采用固定额度分配机制,每个调度周期的进程可用时间片是确定的
      • 调度周期结束被重新分配
    • O(1)调度器本质上是MLFQ(multi-level feedback queue)算法思想
      • 不足:O(1)调度器对进程交互性的响应不及时
    • 需求
      • 根据进程的运行状况判断它属于IO密集型还是CPU密集型,再做优先级奖励和惩罚
      • 这种推测本身存在误差,场景越复杂判断难度越大

    CFS的背景

    匈牙利人Ingo Molnar所提出和实现CFS调度算法

    • 他也是O(1)调度算法的提出者

    bg right:40% 100%


    CFS 的思路

    • 摒弃固定时间片分配,采用动态时间片分配
    • 每次调度中进程可占用的时间与进程总数、总CPU时间、进程权重等均有关系,每个调度周期的值都可能会不一样
    • 每个进程都有一个nice值, 表示其静态优先级

    bg right 90%


    CFS 的思路

    • 把 CPU 视为资源,并记录下每一个进程对该资源使用的情况
      • 在调度时,调度器总是选择消耗资源最少的进程来运行(公平分配
    • 由于一些进程的工作会比其他进程更重要,这种绝对的公平有时也是一种不公平
      • 按照权重来分配 CPU 资源 bg right:47% 90%

    CFS 的进程运行时间动态分配

    • 根据各个进程的优先级权重分配运行时间
      • 进程权重越大, 分到的运行时间越多 分配给进程的运行时间 = 调度周期 * 进程权重 / 所有进程权重总和
    • 调度周期
      • 将所有处于 TASK_RUNNING 态进程都调度一遍的时间

    CFS 的相对公平性

    • 系统中两个进程 A,B,权重分别为 1, 2,调度周期设为 30ms,
    • A 的 CPU 时间为:30ms * (1/(1+2)) = 10ms
    • B 的 CPU 时间为:30ms * (2/(1+2)) = 20ms
    • 在这 30ms 中 A 将运行 10ms,B 将运行 20ms

    它们的运行时间并不一样。 公平怎么体现呢?


    CFS 的虚拟时间vruntime

    • virtual runtime(vruntime):记录着进程已经运行的时间
      • vruntime是根据进程的权重将运行时间放大或者缩小一个比例。 vruntime = 实际运行时间 * 1024 / 进程权重
      • 1024是nice为0的进程的权重,代码中是NICE_0_LOAD
      • 所有进程都以nice为0的进程的权重1024作为基准,计算自己的vruntime增加速度

    CFS 的虚拟时间vruntime

    以上面A和B两个进程为例,B的权重是A的2倍,那么B的vruntime增加速度只有A的一半。

    vruntime = (调度周期 * 进程权重 / 所有进程总权重) * 1024 / 进程权重
             = 调度周期 * 1024 / 所有进程总权重
    

    虽然进程的权重不同,但是它们的 vruntime增长速度应该是一样的 ,与权重无关。


    CFS 的虚拟时间计算

    所有进程的vruntime增长速度宏观上看应该是同时推进的,就可以用这个vruntime来选择运行的进程。

    • 进程的vruntime值较小说明它以前占用cpu的时间较短,受到了“不公平”对待,因此下一个运行进程就选择它。
    • 这样既能公平选择进程,又能保证高优先级进程获得较多的运行时间。

    CFS 的虚拟时间计算示例

    CFS让每个调度实体(进程或进程组)的vruntime互相追赶,而每个调度实体的vruntime增加速度不同,权重越大的增加的越慢,这样就能获得更多的cpu执行时间。

     A每6个时间片执行1个时间片,B每3个时间片执行1个时间片,,C每2个时间片执行1个时间片
     vruntime:
     A:   0  6  6  6  6  6  6  12 12 12 12 12 12
     B:   0  0  3  3  3  6  6  6  9  9  9  12 12
     C:   0  0  0  2  4  4  6  6  6  8  10 10 12
     调度:   A  B  C  C  B  C  A  B  C  C   B  C
    

    提纲

    1. CFS的原理

    2. CFS 的实现


    红黑树:CFS中进程vruntime数据结构

    • Linux 采用了红黑树记录下每一个进程的 vruntime
      • 在多核系统中,每个核一棵红黑树
      • 调度时,从红黑树中选取vruntime最小的进程出来运行 bg right:53% 90%

    CFS 的进程权重

    • 权重由 nice 值确定,权重跟进程 nice 值一一对应
      • nice值越大,权重越低
    • 通过全局数组 prio_to_weight 来转换 bg right:48% 90%

    CFS中新创建进程的 vruntime如何设置?

    • 如果新进程的 vruntime 初值为 0 的话,比老进程的值小很多,那么它在相当长的时间内都会保持抢占 CPU 的优势,老进程就要饿死了,这显然是不公平的。 bg right:46%  90%

    CFS中新创建进程的 vruntime设置

    • 每个 CPU 的运行队列 cfs_rq 都维护一个min_vruntime 字段
      • 记录该运行队列中所有进程的 vruntime 最小值
    • 新进程的初始vruntime 值设置为它所在运行队列的min_vruntime
      • 与老进程保持在合理的差距范围内 bg right:41% 90%

    CFS中休眠进程的 vruntime 一直保持不变吗?

    如果休眠进程的 vruntime 保持不变,而其他运行进程的 vruntime 一直在推进,那么等到休眠进程终于唤醒的时候,它的 vruntime 比别人小很多,会使它获得长时间抢占 CPU 的优势,其他进程就要饿死了。 bg right:47% 100%


    CFS中休眠进程的vruntime

    • 在休眠进程被唤醒时重新设置 vruntime 值,以 min_vruntime 值为基础,给予一定的补偿,但不能补偿太多。 bg right:45% 90%

    CFS中休眠进程在唤醒时会立刻抢占 CPU 吗?

    • 休眠进程在醒来的时候有能力抢占 CPU 是大概率事件,这也是 CFS 调度算法的本意,即保证交互式进程的响应速度,交互式进程等待用户输入会频繁休眠。 bg right:55% 100%

    CFS中休眠进程在唤醒时会立刻抢占 CPU 吗?

    • 主动休眠的进程同样也会在唤醒时获得补偿,这类进程往往并不要求快速响应,它们同样也会在每次唤醒并抢占,这有可能会导致其它更重要的应用进程被抢占,有损整体性能。
    • sched_features 的 WAKEUP_PREEMPT 位表示禁用唤醒抢占特性,刚唤醒的进程不立即抢占运行中的进程,而是要等到运行进程用完时间片之后

    bg right:35% 100%


    CFS中的进程在 CPU 间迁移时 vruntime 会不会变?

    • 在多 CPU 的系统上,不同的 CPU 的负载不一样,有的 CPU 更忙一些,而每个 CPU 都有自己的运行队列,每个队列中的进程的vruntime 也走得有快有慢,每个CPU运行队列的 min_vruntime 值,都会有不同 bg right:48% 90%

    CFS中的进程迁移

    • 当进程从一个 CPU 的运行队列中出来时,它的 vruntime 要减去队列的 min_vruntime 值;
    • 当进程加入另一个 CPU 的运行队列时,它的vruntime 要加上该队列的 min_vruntime 值。

    bg right:45% 90%


    CFS的vruntime 溢出问题

    • vruntime 的类型 usigned long
    • 进程的虚拟时间是一个递增的正值,因此它不会是负数,但是它有它的上限,就是unsigned long 所能表示的最大值
    • 如果溢出了,那么它就会从 0 开始回滚,如果这样的话,结果会怎样?

    bg right:48% 100%


    CFS 的vruntime 溢出示例

    unsigned char a = 251;
    unsigned char b = 254;
    b += 5;
    //b溢出,导致a > b,应该b = a + 8
    //怎么做到真正的结果呢?改为以下:
    unsigned char a = 251;
    unsigned char b = 254;
    b += 5;
    signed char c = a - 250, 
    signed char d = b - 250;
    //到此判断 c 和 d 的大小
    

    bg right:54% 90%


    Linux调度器的模块化

    w:850


    参考文献

    • https://www.eet-china.com/mp/a111242.html
    • https://www.jianshu.com/p/1da5cfd5cee4
    • https://developer.ibm.com/tutorials/l-completely-fair-scheduler/
    • http://www.wowotech.net/process_management/scheduler-history.html

    课程实验三 进程及进程管理

    • 实验目标
      • spawn 系统调用
      • stride 调度算法
    • 实验任务描述
    • 实验提交要求
      • 布置实验任务后的第13天(11月12日24点);

    第八讲 多处理器调度

    第五节 Linux/FreeBSD BFS 调度

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. BFS调度器

    1. BFS 与 CFS 的性能对比 (2012)

    Ref: Analysis of the BFS Scheduler in FreeBSD


    BFS 的思路

    BFS全称:Brain Fuck Scheduler,脑残调度器

    • BFS 调度算法是一种时间片轮转算法的变种。
    • 在多处理机时使用单就绪队列(双向链表)
      • 增加了队列互斥访问的开销
      • 减少了负载均衡算法开销

    BFS 的进程优先级

    • 进程有 103 个优先级
      • 100 个静态的实时优先级;
      • 3 个普通优先级
        • SCHED_ISO (isochronous) : 交互式任务
        • SCHED_NORMAL : 普通任务
        • SCHED_IDLEPRIO :低优先级任务

    BFS 的就绪队列

    • 就绪队列
      • 所有 CPU 共享一个双向链表结构的单就绪队列
      • 所有进程按优先级排队;
      • 相同优先级的每个进程有一个时间片长度和虚拟截止时间;

    BFS 的时间片

    • 时间片大小:由算法参数指定,可在 1ms 到 1000ms 间选择,缺省设置为 6ms;
    • 虚拟截止时间(Virtual Deadline):关于就绪队列中进程等待 CPU 最长时间的排序,并不是真实的截止时间;
      • 进程时间片用完时,重新计算虚拟截止时间;
      • 事件等待结束时,虚拟截止时间保持不变,以抢先相同优先级的就绪进程;
      • 为了让进程在上次运行的 CPU 上运行(亲和性),不同 CPU 对进程的虚拟截止时间加一个权重;

    BFS 的虚拟截止时间计算

    • 依据当前时间、进程优先级和时间片设置计算;
    offset = niffies + (prioratio ∗ rr_interval)
    prioratio increases by 10% for every nice level
    
    • niffies是当前时间;prio_ratios[priority]是一个常量数组,不同的priority对应不同的prio_ratios[priority];rr_interval是timeslice,是CPU分配给每个任务的时间片,是一个常数

    • 虚拟截止时间计算结果:https://wikimili.com/en/Brain_Fuck_Scheduler


    BFS 的调度思路

    使用O(1)调度器中的位图概念,所有进程被安排到103个queue中,各个进程不是按照优先级而是按照优先级区间被排列到各自所在的区间,每一个区间拥有一个queue。

    bg right 100%


    BFS 的调度思路

    按照O(1)调度器的方式首先查找位图中不为0的那个queue,然后在该queue中执行O(n)查找,查找到virtual deadline最小的那个进程投入执行。

    bg right 100%


    BFS 的就绪队列插入

    • 时间片用完:重新设置虚拟截止时间后,插入就绪队列;
    • 等待事件出现:虚拟截止时间保持不变,抢先低优先级进程或插入就绪队列;

    提纲

    1. BFS调度器

    2. BFS 与 CFS 的性能对比 (2012)

    Ref: Analysis of the BFS Scheduler in FreeBSD


    BFS 与 CFS 的性能对比 (2012)

    测试用例集

    • Linux kernel v3.6.2.2 的 GCC 编译
    • Linux kernel v3.6.2 内核源代码树的 lrzip 压缩
    • 从 720p 到 360p 的 MPEG2 视频 ffmpeg 压缩

    bg right:53% 90%


    BFS 与 CFS 的性能对比: 压缩测试

    w:1000


    BFS 与 CFS 的性能对比: 测试编译

    w:1000


    BFS 与 CFS 的性能对比: 视频编码测试

    w:1000


    参考文献

    • http://repo-ck.com/bench/cpu_schedulers_compared.pdf
    • https://zhuanlan.zhihu.com/p/351876567
    • https://blog.csdn.net/dog250/article/details/7459533
    • https://www.cnblogs.com/dragonsuc/p/7144265.html

    第九讲 文件系统

    第一节 文件和文件系统

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 文件

    1. 文件系统和文件组织

    1.1 文件的概念

    1.2 文件操作 1.3 文件描述符


    什么是文件系统?

    • 文件系统是存储设备上组织文件的方法数据结构

    bg right:60% 90%


    什么是文件系统?

    • 文件系统是存储设备上组织文件的方法数据结构
    • 文件系统是操作系统中负责文件命名、存储和检索的子系统

    bg right:60% 90%


    什么是文件?

    • 文件是具有符号名,由字节序列构成的数据项集合

      • 文件是文件系统的基本数据单位
      • 文件名是文件的标识符号
    • 文件头:文件系统元数据中的文件信息

      • 文件属性:名称、类型、位置、创建时间、…
      • 文件存储位置和顺序

    一切都是文件

    • UNIX类操作系统的一个设计哲学:一切都是文件
      • 普通文件,目录文件
      • 字符设备文件(如键盘,鼠标...)
      • 块设备文件(如硬盘,光驱...)
      • 网络文件(socket ...)等等
    • 所有一切均抽象成文件,提供了统一的接口,方便应用程序调用

    文件视图

    • 用户的文件视图
      • 持久的数据结构
      • 系统调用接口:字节序列的集合(UNIX)
    • 操作系统的文件视
      • 数据块的集合
      • 数据块是逻辑存储单元,而扇区是物理存储单元

    文件中数据的内部结构

    • 与应用相关
      • 无结构:文本文件
      • 简单结构:CSV、JSON等格式化文件
      • 复杂结构:Word文件、ELF可执行文件

    提纲

    1. 文件
    2. 文件系统和文件组织

    1.1 文件的概念

    1.2 文件操作

    1.3 文件描述符


    文件的基本操作

    • 进程读文件
      • 获取文件所在的数据块
      • 返回数据块内对应部分
    • 进程写文件
      • 获取数据块
      • 修改数据块中对应部分
      • 写回数据块

    bg right:50% 100%


    文件的基本操作单位

    • 文件系统中的基本操作单位是数据块
      • 例如, getc()和putc()即使每次只访问1字节的数据,也需要缓存目标数据4096字节

    文件的访问模式

    • 顺序访问: 按字节依次读取
      • 把一维数据映射到文件中
    • 随机访问: 从任意位置读写
      • 把一个复杂结构(矩阵)映射到文件中
    • 索引访问: 依据数据特征索引
      • 数据库访问是一种基于索引的访问

    文件访问控制

    • 多用户操作系统中的文件共享是很必要的
    • 访问控制
      • 用户对文件的访问权限
      • 读、写、执行、删除
    • 文件访问控制列表(ACL-Access Control List)
      • <文件实体, 权限>

    文件访问控制

    • UNIX模式
      • <用户|组|所有人, 读|写|可执行>
      • 用户标识ID
      • 组标识ID

    多进程如何同时访问共享文件?

    • 文件是一类共享资源
      • 需要互斥访问
      • 采用类似的同步互斥技术(后续)
        • 读写锁

    文件的共享访问

    UNIX 文件系统(UFS)语义

    • 对打开文件的写入内容,立即对其他打开同一文件的其他用户可见
    • 共享文件指针允许多用户同时读取和写入文件

    提纲

    1. 文件
    2. 文件系统和文件组织

    1.1 文件的概念 1.2 文件操作

    1.3 文件描述符


    应用程序如何访问文件?

    • 应用访问文件数据前必须先“打开”文件,获得文件描述符
    • 再进一步通过文件描述符(File Descriptor,fd)读写文件
    fd = open(name, flag);
    read(fd, …);
    close(fd);
    

    文件描述符

    当应用程序请求内核打开/新建一个文件时,内核返回一个文件描述符用于对应这个打开/新建的文件

    • 形式上,文件描述符是一个非负整数
    • 实际上,文件描述符是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表

    bg right:54% 90%


    打开文件表

    • 内核跟踪进程打开的所有文件
      • 操作系统为每个进程维护一个打开的文件描述符表
      • 一个系统级的打开文件表
      • i-node表指中向具体的文件内容 bg right:48% 90%

    打开文件表

    • 内核在打开文件表中维护的打开文件状态和信息
      • 文件指针
        • 最近一次读写位置
        • 每个进程分别维护自己的打开文件指针

    bg right:48% 90%


    打开文件表

    • 内核在打开文件表中维护的打开文件状态和信息
      • 文件打开计数
        • 当前打开文件的次数
        • 最后一个进程关闭文件时,将其从打开文件表中移除

    bg right:52% 90%


    打开文件表

    • 操作系统在打开文件表中维护的打开文件状态和信息
      • 文件的磁盘位置
        • 缓存数据访问信息
      • 访问权限
        • 每个进程的文件访问模式信息 bg right:50% 90%

    提纲


    文件系统类型

    • 磁盘文件系统:FAT, NTFS, ext2/3, ISO9660,等
    • 网络/分布式文件系统:NFS, SMB, AFS, GFS
    • 特殊文件系统:procfs, sysfs

    虚拟文件系统(VFS)

    w:650


    文件系统的功能

    • 文件系统是操作系统中管理持久性数据的子系统,提供数据文件命名、存储和检索功能。
      • 组织、检索、读写访问数据
      • 大多数计算机系统都有文件系统

    文件系统功能

    • 分配文件磁盘空间
      • 管理文件块(位置和顺序)
      • 管理空闲空间(位置)
      • 分配算法 (策略)

    文件系统功能

    • 管理文件集合
      • 组织:组织文件的控制结构和数据结构
      • 命名:给文件取名字
      • 定位:通过名字找到文件文件及其内容

    文件系统功能

    • 数据可靠和安全
      • 安全:多层次保护数据安全
      • 可靠
        • 持久保存文件
        • 避免系统崩溃、数据丢失等

    文件系统组织形式

    分层文件系统

    • 文件以目录的方式组织起来
    • 目录是一类特殊的文件
    • 目录的内容是文件索引表<文件名, 指向文件的指针>

    bg right 100%


    提纲

    1. 文件
    2. 文件系统和文件组织

    2.1 文件系统的功能

    2.2 目录


    目录操作

    应用程序通过系统调用对目录进行操作

    • 搜索文件 创建文件
    • 删除文件 列目录
    • 重命名文件 重命名文件

    bg right 90%


    目录实现

    • 文件名的线性列表,包涵了指向数据块的指针
      • 编程简单、执行耗时
    • 哈希表 – 哈希数据结构的线性表
      • 减少目录搜索时间
      • 可能有冲突 - 两个文件名的哈希值相同

    遍历目录路径

    举例: 解析./fs/inode.rs

    • 读取当前目录文件.的数据内
    • 找到fs项,读取目录文件fs的数据内容
    • 找到inode.rs项,读取一般文件inode.rs的数据内容

    bg right:48% 90%


    文件别名

    多个文件名关联同一个文件

    • 硬链接(hard link)
      • 多个文件项指向一个文件
    • 软链接(soft link, symbolic link)
      • 以存储文件名称的方式来指向其他文件

    inode:管理文件数据的结构

    bg right:51% 100%


    如何避免目录中没有形成循环?

    • 只允许对文件的链接,不允许对子目录的链接
    • 增加链接时,用循环检测算法确定是否合理
    • 限制遍历文件目录的路径数量

    w:800


    文件系统挂载

    • 文件系统需要先挂载才能被访问 w:800

    小结

    1. 文件
    • 文件的概念
    • 文件操作
    • 文件描述符
    1. 文件系统和文件组织
    • 文件系统的功能
    • 目录

    第九讲 文件系统

    第二节 文件系统的设计与实现

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 概述

    1. 文件系统的基本数据结构
    2. 文件缓存
    3. 文件分配
    4. 文件访问过程示例

    文件系统在内核中的位置

    w:850


    文件系统的分层结构

    w:950


    文件系统在计算机系统中的分层结构

    w:700


    文件系统的用户视图与内核视图

    w:1000


    文件系统的用户视图与内核视图

    w:680


    虚拟文件系统

    (Virtual File System,VFS)

    • 定义了一组所有文件系统都支持的数据结构和标准接口。
    • 磁盘的文件系统:直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS。
    • 内存的文件系统:内存辅助数据结构 - 例如目录项。 bg right 50% 80%

    虚拟文件系统的功能

    • 目的:对所有不同文件系统的抽象
    • 功能
      • 提供相同的文件和文件系统接口
      • 管理所有文件和文件系统关联的数据结构
      • 高效查询例程, 遍历文件系统
      • 与特定文件系统模块的交互

    虚拟文件系统统一不同文件系统的访问接口

    w:750


    提纲

    1. 概述

    2. 文件系统的基本数据结构

    1. 文件缓存
    2. 文件分配
    3. 文件访问过程示例

    文件系统的存储视图

    • 文件卷控制块 (superblock)
    • 文件控制块( inode/vnode)
    • 目录项 (dir_entry)
    • 数据块(data blockbg right:51% 100%

    文件系统的组织视图

    bg 90% bg 90%


    文件系统基本数据结构

    w:700


    文件卷控制块 (superblock)

    每个文件系统一个文件卷控制块

    • 文件系统详细信息
    • 块大小、空余块数量等
    • block 与inode 的总量,未使用与已使用的数量
    • filesystem的挂载时间、最近一次写入时间、最近一次检验磁盘(fsck) 时间 bg right:49% 100%

    文件控制块inode

    每个文件有一个文件控制块inode (inode/vnode)

    • 大小、数据块位置(指向一个或多个datablock)
    • 访问模式(read/write/excute)
    • 拥有者与群组(owner/group)
    • 时间信息:建立或状态改变的时间、最近读取时间/修改的时间
    • 文件名存放在目录的datablock

    bg right:46% 90%


    文件控制块inode

    每个文件有一个文件控制块inode (inode/vnode)

    • 文件的字节数。
    • 文件拥有者的 User ID。
    • 文件的 Group ID。
    • 链接数,即有多少文件名指向这个 inode。
    • 文件数据 block 的位置(直接、间接)

    bitmap块

    bitmap块( bitmap inode/dnode)

    • inode使用或者未使用标志位
    • dnode使用或者未使用标志位
    • 用于空间分配

    数据块dnode( data node)

    • 目录和文件的数据块
      • 放置目录和文件内容
      • 格式化时确定data block的固定大小
      • 每个block都有编号,以方便inode记录
      • inode一般为128B
      • data block一般为4KB

    目录的数据块

    w:850


    目录项 (dir_entry)

    • 一个目录(文件夹)包含多个目录项
      • 每个目录项一个(目录和文件)
      • 将目录项数据结构及树型布局编码成树型数据结构
      • 指向文件控制块、父目录、子目录等
    • OS会缓存一个读过目录项来提升效率 bg right 100%

    bg right 100%


    提纲

    1. 概述
    2. 文件系统的基本数据结构

    3. 文件缓存

    1. 文件分配
    2. 文件访问过程示例

    多种磁盘缓存位置

    w:1200


    数据块缓存

    • 数据块按需读入内存
      • 提供read()操作
      • 预读: 预先读取后面的数据块
    • 数据块使用后被缓存
      • 假设数据将会再次用到
      • 写操作可能被缓存和延迟写入

    bg right:45% 90%


    虚拟页式存储与页缓存

    • 页缓存: 统一缓存数据块和内存页
    • 在虚拟地址空间中虚拟页面可映射到本地外存文件中

    w:700


    虚拟页式存储与页缓存

    • 文件数据块的页缓存
      • 在虚拟内存中文件数据块被映射成页
      • 文件的读/写操作被转换成对内存的访问
      • 可能导致缺页和/或设置为脏页
    • 问题: 页置换算法需要协调虚拟存储和页缓存间的页面数

    文件描述符

    • 每个被打开的文件都有一个文件描述符
    • 作为index,指向对应文件状态信息

    w:750


    打开文件表

    • 每个进程1个进程打开文件表
    • 一个系统打开文件表

    w:750


    文件锁

    一些文件系统提供文件锁,用于协调多进程的文件访问

    • 强制 – 根据锁保持情况和访问需求确定是否拒绝访问
    • 劝告 – 进程可以查找锁的状态来决定怎么做

    提纲

    1. 概述
    2. 文件系统的基本数据结构
    3. 文件缓存

    4. 文件分配

    1. 文件访问过程示例

    文件大小

    • 大多数文件都很小
      • 需要支持小文件
      • 数据块空间不能太大
    • 一些文件非常大
      • 能支持大文件
      • 可高效读写 bg right:55% 90%

    文件分配

    分配文件数据块

    • 分配方式
      • 连续分配
      • 链式分配
      • 索引分配
    • 评价指标
      • 存储效率:外部碎片等
      • 读写性能:访问速度

    bg right:54% 90%


    连续分配

    文件头指定起始块和长度

    w:900

    • 分配策略: 最先匹配, 最佳匹配, ...
    • 优点:
      • 高效的顺序和随机读访问
    • 缺点
      • 频繁分配会带来碎片;增加文件内容开销大

    链式分配

    数据块以链表方式存储

    w:800

    • 优点: 创建、增大、缩小很容易;几乎没有碎片
    • 缺点:
      • 随机访问效率低;可靠性差;
      • 破坏一个链,后面的数据块就丢了

    链式分配

    • 显式连接
    • 隐式连接 bg right:35% 80%

    bg right:70% 100%


    索引分配

    • 文件头包含了索引数据块指针
    • 索引数据块中的索引是文件数据块的指针 w:800
    • 优点
      • 创建、增大、缩小很容易;几乎没有碎片;支持直接访问
    • 缺点
      • 当文件很小时,存储索引的开销相对大

    如何处理大文件?


    索引分配

    • 链式索引块 (IB+IB+…) w:800
    • 多级索引块(IB*IB *…) w:800

    索引分配

    w:1000


    多级索引分配

    w:800


    多级索引分配

    • 文件头包含13个指针
      • 10 个指针指向数据块
      • 第11个指针指向索引块
      • 第12个指针指向二级索引块
      • 第13个指针指向三级索引块

    大文件在访问数据块时需要大量查询

    bg right:43% 100%


    文件分配方式比较

    w:1150


    空闲空间管理

    跟踪记录文件卷中未分配的数据块: 数据结构?

    • 位图:用位图代表空闲数据块列表
      • 11111111001110101011101111...
      • $D_i = 0$ 表明数据块$i$是空闲, 否则,表示已分配
      • 160GB磁盘 --> 40M数据块 --> 5MB位图
      • 假定空闲空间在磁盘中均匀分布,
        • 找到“0”之前要扫描n/r
          • n = 磁盘上数据块的总数 ; r = 空闲块的数目

    空闲空间管理

    • 链表 w:800
    • 索引 w:900

    提纲

    1. 概述
    2. 文件系统的基本数据结构
    3. 文件缓存
    4. 文件分配

    5. 文件访问过程示例


    文件系统组织示例

    w:850


    文件读操作过程

    w:650


    文件写操作过程

    w:800


    文件系统分区

    • 多数磁盘划分为一个或多个分区,每个分区有一个独立的文件系统。 w:600

    bg right:40% 80%


    小结

    1. 概述
    2. 文件系统的基本数据结构
    3. 文件缓存
    4. 文件分配
    5. 文件访问过程示例

    第九讲 文件系统

    第三节 支持崩溃一致性的文件系统

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 崩溃一致性问题

    1. 文件系统检查程序 fsck
    2. 日志文件系统

    1.1 崩溃一致性

    1.2 崩溃场景


    文件系统的持久数据更新挑战

    如何在出现断电(power loss)或系统崩溃(system crash)的情况下,更新持久数据结构。

    崩溃可能导致磁盘文件系统映像中的文件系统数据结构出现不一致性。如,有空间泄露、将垃圾数据返回给用户等。


    崩溃一致性问题

    崩溃一致性问题(crash-consistency problem)也称一致性更新问题(consistent-update problem)

    • 特定操作需要更新磁盘上的两个结构A和B。
    • 磁盘一次只为一个请求提供服务,因此其中一个请求将首先到达磁盘(A或B)。
    • 如果在一次写入完成后系统崩溃或断电,则磁盘上的结构将处于不一致(inconsistent)的状态。

    崩溃一致性的需求

    • 目标
      • 将文件系统从一个一致状态(在文件被追加之前),原子地(atomically)变迁到另一个一致状态(在inode、位图和新数据块被写入磁盘之后)。
    • 困难
      • 磁盘一次只提交一次写入,更新之间可能会发生崩溃或断电。

    文件更新过程示例

    一个应用以某种方式更新磁盘结构:将单个数据块附加到原有文件。

    • 通过打开文件,调用lseek()将文件偏移量移动到文件末尾,然后在关闭文件之前,向文件发出单个4KB写入来完成追加。

    文件系统数据结构

    • inode位图(inode bitmap,只有8位,每个inode一个)
    • 数据位图(data bitmap,也是8位,每个数据块一个)
    • inode(总共8个,编号为0到7,分布在4个块上)
    • 数据块(总共8个,编号为0~7)。

    w:1100


    文件更新中的磁盘操作

    一个应用以某种方式更新磁盘结构:将单个数据块附加到原有文件

    • 必须对磁盘执行3次单独写入
      • inode(I[v2])、位图(B[v2])和数据块(Db)
    • 发出write()系统调用时,这些写操作通常不会立即发生。
      • 脏的inode、位图和新数据先在内存(页面缓存page cache,或缓冲区缓存buffer cache)中存在一段时间。
    • 当文件系统最终决定将它们写入磁盘时(比如说5s或30s),文件系统将向磁盘发出必要的写入请求

    提纲

    1. 崩溃一致性问题
    2. 文件系统检查程序 fsck
    3. 日志文件系统

    1.1 崩溃一致性

    1.2 崩溃场景


    文件操作中的崩溃

    在文件操作过程中可能会发生崩溃,从而干扰磁盘的这些更新。

    • 如果写入操作中的一个或两个完成后发生崩溃,而不是全部 3个,则文件系统可能处于有趣(不一致)的状态。

    w:1100


    崩溃场景一

    只将数据块(Db)写入磁盘

    • 数据在磁盘上,但没有指向它的inode,也没有表示块已分配的位图
    • 好像写入从未发生过一样

    w:1100


    崩溃场景二

    只有更新的inode(I[v2])写入了磁盘

    • inode指向磁盘块5,其中Db即将写入,但Db尚未写入
    • 从磁盘读取垃圾数据(磁盘块5的旧内容)。

    w:1100


    崩溃场景三

    只有更新后的位图(B [v2])写入了磁盘

    • 位图指示已分配块5,但没有指向它的inode
    • 这种写入将导致空间泄露(space leak),文件系统永远不会使用块5

    w:1100


    崩溃场景四

    inode(I[v2])和位图(B[v2])写入了磁盘,但没有写入数据(Db)

    • inode有一个指向块5的指针,位图指示5正在使用,因此从文件系统的元数据的角度来看,一切看起来很正常
    • 磁盘块5中又是垃圾

    w:1100


    崩溃场景五

    写入了inode(I[v2])和数据块(Db),但没有写入位图(B[v2])

    • inode指向了磁盘上的正确数据
    • 在inode和位图(B1)的旧版本之间存在不一致

    w:1100


    崩溃场景六

    写入了位图(B[v2])和数据块(Db),但没有写入inode(I[v2])

    • inode和数据位图之间再次存在不一致
    • 不知道它属于哪个文件,因为没有inode指向该块

    w:1100


    提纲

    1. 崩溃一致性问题

    2. 文件系统检查程序 fsck

    1. 日志文件系统

    崩溃解决方案

    • 文件系统检查程序 fsck
    • 基于预写日志(write ahead log)的文件系统

    文件系统检查程序 fsck

    早期的文件系统采用了一种简单的方法来处理崩溃一致性。

    • 让不一致的事情发生,然后再修复它们(重启时)
    • 目标:确保文件系统元数据内部一致

    超级块检查

    检查超级块是否合理,主要是进行健全性检查

    • 确保文件系统大小大于分配的块数
    • 找到超级块的内容不合理(冲突),系统(或管理员)可以决定使用超级块的备用副本

    注:可靠性高的文件系统,会有多处放置超级块备份的磁盘扇区。

    w:650


    位图与inode间的一致性检查

    扫描inode、间接块、双重间接块等,以了解当前在文件系统中分配的块,生成正确版本的分配位图

    • 如果位图和inode之间存在任何不一致,则通过信任inode内的信息来解决它
    • 对所有inode执行相同类型的检查,确保所有看起来像在用的inode,都在inode位图中有标记

    inode状态检查

    检查每个inode是否存在损坏或其他问题

    • 每个分配的inode具有有效的类型字段(即常规文件、目录、符号链接等)
    • 如果inode字段存在问题,不易修复,则inode被认为是可疑的,并被fsck清除,inode位图相应地更新。

    链接计数检查

    inode链接计数表示包含此特定文件的引用(即链接)的不同目录的数量。

    • 从根目录开始扫描整个目录树,并为文件系统中的每个文件和目录构建自己的链接计数
    • 如果新计算的计数与inode中找到的计数不匹配,则通常是修复inode中的计数
    • 如果发现已分配的inode但没有目录引用它,则会将其移动到lost + found目录。

    重复指针检查

    两个不同的inode引用同一个块的情况

    • 如果一个inode明显错误,可能会被清除或复制指向的块,从而为每个inode提供其自己的文件数据。
    • inode有很多错误可能性,比如其inode内的元数据不一致
      • inode有文件的长度记录,但其实际指向的数据块大小小于其文件长度。

    坏块检查

    在扫描所有指针列表时,检查坏块指针。如果指针显然指向超出其有效范围的某个指针,则该指针被认为是“坏的”。

    • 地址指向大于分区大小的块
    • 从inode或间接块中删除(清除)该指针

    目录检查

    fsck不了解用户文件的内容,但目录包含由文件系统本身创建的特定格式的信息。对每个目录的内容执行额外的完整性检查。

    • 确保“.”和“..”是前面的条目,目录条目中引用的每个inode都已分配
    • 确保整个层次结构中没有目录的引用超过一次。

    文件系统检查程序 fsck 的不足

    • 对于非常大的磁盘卷,扫描整个磁盘,以查找所有已分配的块并读取整个目录树,可能需要几分钟或几小时。
    • 可能丢数据!

    提纲

    1. 崩溃一致性问题
    2. 文件系统检查程序 fsck

    3. 日志文件系统

    3.1 日志

    3.2 数据日志(data journaling) 3.3 日志文件系统的性能优化


    日志(或预写日志)

    预写日志(write-ahead logging)

    • 从数据库管理系统的世界中借鉴的想法
    • 在文件系统中,出于历史原因,通常将预写日志称为日志(journaling)
    • 第一个实现它的文件系统是Cedar
    • 许多现代文件系统都使用这个想法,包括Linux ext3和ext4、reiserfs、IBM的JFS、SGI的XFS和Windows NTFS。

    预写日志的思路

    • 更新磁盘时,在覆写结构之前,首先写下一点小注记(在磁盘上的其他地方,在一个众所周知的位置),描述你将要做的事情
    • 写下这个注记就是“预写”部分,把它写入一个结构,并组织成“日志”

    预写日志的崩溃恢复

    • 通过将注释写入磁盘,可以保证在更新(覆写)正在更新的结构期间发生崩溃时,能够返回并查看你所做的注记,然后重试
    • 在崩溃后准确知道要修复的内容(以及如何修复它),而不必扫描整个磁盘
    • 日志功能通过在更新期间增加了一些工作量,大大减少了恢复期间所需的工作量

    提纲

    1. 崩溃一致性问题
    2. 文件系统检查程序 fsck
    3. 日志文件系统

    3.1 日志

    3.2 数据日志(data journaling)

    3.3 日志文件系统的性能优化


    数据日志(data journaling)

    w:900

    • TxB: transaction 开始
    • TxE: transaction 结束
    • logical logging: 中间 3 块数据

    数据日志(data journaling)

    w:900

    • 数据日志写到磁盘上(写日志)
    • 更新磁盘,覆盖相关结构(写真实数据) (checkpoint)
      • I[V2] B[v2] Db

    写入日志期间发生崩溃

    磁盘内部可以(1)写入TxB、I[v2]、B[v2]和TxE,然后(2)才写入Db。

    • 如果磁盘在(1)和(2)之间断电,那么磁盘上会变成:

    w:1000


    数据日志的两步事务写入

    为避免该问题,文件系统分两步发出事务写入。

    • 将除TxE块之外的所有块写入日志,同时发出这些写入操作
    • 当这些写入完成时,日志将看起来像这样(假设又是文件追加的工作负载): w:1000

    数据日志的两步事务写入

    当这些写入完成时,文件系统会发出TxE块的写入,从而使日志处于最终的安全状态: w:900


    数据日志的更新流程

    当前更新文件系统的协议如下,3个阶段中的每一个都标上了名称。

    1. 日志写入 Journal write:
      • 将事务的内容(包括TxB、元数据和数据)写入日志,等待这些写入完成。
    2. 日志提交 Journal Commit:
      • 将事务提交块(包括TxE)写入日志,等待写完成,事务被认为已提交(committed)。
    3. 加检查点 Checkpoint
      • 将更新内容(元数据和数据)写入其最终的磁盘位置

    数据日志的崩溃恢复

    • 崩溃发生在Journal Commit完成前:文件系统可以丢掉之前写入的log。由于磁盘具体位置的bitmap,inodes,data blocks都没变,所以可以确保文件系统一致性。

    • 崩溃发生在Journal Commit后,Checkpoint之前:文件系统在启动时候,可以扫描所有已经commited的log,然后针对每一个log记录操作进行replay,即recovery的过程中执行Checkpoint,将log的信息回写到磁盘对应的位置。这种操作也成为redo logging。

    • 崩溃发生在Checkpoint完成后:那无所谓,都已经成功回写到磁盘了,文件系统的bitmap、inodes、data blocks也能确保一致性。


    数据日志的崩溃恢复

    在此更新序列期间的任何时间都可能发生崩溃。

    • 如果崩溃发生在将事务安全地写入日志之前
    • 如果崩溃是在事务提交到日志之后,但在检查点完成之前发生

    太多写,慢!

    数据+元数据日志 -> 元数据日志


    提纲

    1. 崩溃一致性问题
    2. 文件系统检查程序 fsck
    3. 日志文件系统

    3.1 日志 3.2 数据日志(data journaling)

    3.3 日志文件系统的性能优化


    日志超级块 journal superblock

    • 单独区域存储
    • 批处理日志更新
    • 循环日志回收与复用

    w:900 w:900


    日志超级块的更新过程

    • Journal write:将TxB以及对应的文件操作写入到事务中
    • Journal commit:写入TxE,并等待完成。完成后,这个事务是committed。
    • Checkpoint:将事务中的数据,分别各自回写到各自的磁盘位置中。
    • Free: 一段时间后,通过更新日记帐,超级块将交易记录标记为空闲

    元数据日志 Metadata Journaling

    什么时候应该将数据块 Db 写入磁盘?

    • 数据写入的顺序对于仅元数据的日志记录很重要
    • 如果在事务(包含 I [v2] 和 B [v2])完成后将 Db 写入磁盘,这样有问题吗?

    元数据日志的更新过程

    • Data write:写入数据到磁盘的对应位置
    • Journal metadata write:将TxB以及对应的文件metadata操作写入到事务中
    • Journal commit:写入TxE,并等待完成。完成后,这个事务是committed。
    • Checkpoint metadata:将事务中的metadata的操作相关数据,分别各自回写到各自的磁盘位置中。
    • Free:释放journal区域的log记录

    通过强制首先写入数据,文件系统可保证指针永远不会指向垃圾数据。


    Data Journaling时间线 v.s. Metadata Journaling时间线

    bg 90%

    bg 90%


    不同日志模式

    • Journal Mode: 操作的metadata和file data都会写入到日志中然后提交,这是最慢的。
    • Ordered Mode: 只有metadata操作会写入到日志中,但是确保数据在日志提交前写入到磁盘中,速度较快
    • Writeback Mode: 只有metadata操作会写入到日志中,且不确保数据在日志提交前写入,速度最快

    小结

    1. 崩溃一致性问题
    • 崩溃一致性
    • 崩溃场景
    1. 文件系统检查程序 fsck
    2. 日志文件系统
    • 日志
    • 数据日志(data journaling)
    • 日志文件系统的性能优化

    第九讲 文件系统

    第四节 支持文件的操作系统

    Filesystem OS(FOS)

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 实验目标和步骤

    1. 代码结构
    2. 应用程序设计
    3. 内核程序设计

    1.1 实验目标

    1.2 文件系统接口和数据结构 1.3 实践步骤


    以往实验目标

    提高性能、简化开发、加强安全

    • Process OS: 增强进程管理和资源管理
    • Address Space OS: APP不用考虑其运行时的起始执行地址,隔离APP访问的内存地址空间
    • multiprog & time-sharing OS: 让APP有效共享CPU,提高系统总体性能和效率
    • BatchOS: 让APP与OS隔离,加强系统安全,提高执行效率
    • LibOS: 让APP与HW隔离,简化应用访问硬件的难度和复杂性

    实验目标

    支持数据持久保存

    • 文件形式保存持久数据,并能进行文件数据读写
    • 进程成为文件资源的使用者
    • 能够在应用层面发出如下系统调用请求:
      • open/read/write/close

    bg right:51% 90%


    Filesystem OS (FOS)

    bg right:74% 95%


    历史:UNIX文件系统

    • 1965:描述未来的 MULTICS 操作系统
      • 指明方向的舵手
        • 文件数据看成是一个无格式的字节流
        • 第一次引入了层次文件系统的概念
      • 启发和造就了UNIX文件系统
        • 一切皆文件

    实验要求

    • 理解文件系统/文件概念
    • 理解文件系统的设计与实现
    • 理解应用<->库<->...<->设备驱动的整个文件访问过程
    • 会写支持文件系统的OS

    bg right 80%


    需要考虑的问题

    • 硬盘上的文件系统如何组织?硬盘布局?
    • 如何管理空闲磁盘块?
    • 如何表示一个文件/目录?
      • 文件是啥?目录是啥?
    • 如何表示文件/目录数据内容?
    • 如何访问一个文件?

    bg right:48% 95%


     实验中的文件类型

    • 当前
      • Regular file 常规文件
      • Directory 目录文件
    • 未来
      • Link file 链接文件
      • Device 设备文件
      • Pipe 管道文件

    bg right:56% 95%


    总体思路

    bg 95%


    提纲

    1. 实验目标和步骤
    2. 代码结构
    3. 应用程序设计
    4. 内核程序设计

    1.1 实验目标

    1.2 文件系统接口和数据结构

    1.3 实践步骤


    文件访问流程

    bg 90%


    文件系统访问接口

    w:950


    文件系统访问接口

    w:950


    文件系统访问接口

    w:1100


    文件系统的数据结构

    bg right:90% w:850


    文件系统的数据结构

    bg right:90% 95%


    提纲

    1. 实验目标和步骤
    2. 代码结构
    3. 应用程序设计
    4. 内核程序设计

    1.1 实验目标 1.2 文件系统接口和数据结构

    1.3 实践步骤


    实验步骤

    • 编译:内核独立编译,单独的内核镜像
    • 编译:应用程序编译后,组织形成文件系统镜像
    • 构造:进程的管理与初始化,建立基于页表机制的虚存空间
    • 构造:构建文件系统
    • 运行:特权级切换,进程与OS相互切换
    • 运行:切换地址空间,跨地址空间访问数据
    • 运行:从文件系统加载应用,形成进程
    • 运行:数据访问:内存--磁盘,基于文件的读写

    实践步骤

    git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
    cd rCore-Tutorial-v3
    git checkout ch6
    cd os
    make run
    

    实践步骤

    [RustSBI output]
    ...
    filetest_simple
    fantastic_text
    **************/
    Rust user shell
    >>
    

    操作系统启动shell后,用户可以在shell中通过敲入应用名字来执行应用。

    从用户界面上,没看出文件系统的影子


    实践步骤

    在这里我们运行一下本章的测例 filetest_simple :

    >> filetest_simple
    file_test passed!
    Shell: Process 2 exited with code 0
    >>
    

    它会将 Hello, world! 输出到另一个文件 filea,并读取里面的内容确认输出正确。


    实践步骤

    我们也可以通过命令行工具 cat_filea 来更直观的查看 filea 中的内容:

    >> cat_filea
    Hello, world!
    Shell: Process 2 exited with code 0
    >>
    

    提纲

    1. 实验目标和步骤

    2. 代码结构

    1. 应用程序设计
    2. 内核程序设计

    bg right:66% 95%


    软件架构

    • 文件操作
      • open
      • read
      • write
      • close

    bg right:74% 90%


    代码结构

    添加easy-fs

    ├── easy-fs(新增:从内核中独立出来的一个简单的文件系统 EasyFileSystem 的实现)
    │   ├── Cargo.toml
    │   └── src
    │       ├── bitmap.rs(位图抽象)
    │       ├── block_cache.rs(块缓存层,将块设备中的部分块缓存在内存中)
    │       ├── block_dev.rs(声明块设备抽象接口 BlockDevice,需要库的使用者提供其实现)
    │       ├── efs.rs(实现整个 EasyFileSystem 的磁盘布局)
    │       ├── layout.rs(一些保存在磁盘上的数据结构的内存布局)
    │       ├── lib.rs(定义的必要信息)
    │       └── vfs.rs(提供虚拟文件系统的核心抽象,即索引节点 Inode)
    ├── easy-fs-fuse(新增:将当前 OS 上的应用可执行文件按照 easy-fs 的格式进行打包)
    │   ├── Cargo.toml
    │   └── src
    │       └── main.rs
    

    代码结构

    改进OS

    ├── os
    │   ├── build.rs
    │   ├── Cargo.toml(修改:新增 Qemu 和 K210 两个平台的块设备驱动依赖 crate)
    │   ├── Makefile(修改:新增文件系统的构建流程)
    │   └── src
    │       ├── config.rs(修改:新增访问块设备所需的一些 MMIO 配置)
    │       ├── console.rs
    │       ├── drivers(修改:新增 Qemu 和 K210 两个平台的块设备驱动)
    │       │   ├── block
    │       │   │   ├── mod.rs(将不同平台上的块设备全局实例化为 BLOCK_DEVICE 提供给其他模块使用)
    │       │   │   ├── sdcard.rs(K210 平台上的 microSD 块设备, Qemu不会用)
    │       │   │   └── virtio_blk.rs(Qemu 平台的 virtio-blk 块设备)
    │       │   └── mod.rs
    

    代码结构

    改进OS

    ├── os
    │       ├── fs(修改:在文件系统中新增常规文件的支持)
    │       │   ├── inode.rs(新增:将 easy-fs 提供的 Inode 抽象封装为内核看到的 OSInode
    │       │            并实现 fs 子模块的 File Trait)
    │       ├── loader.rs(移除:应用加载器 loader 子模块,本章开始从文件系统中加载应用)
    │       ├── mm
    │       │   ├── memory_set.rs(修改:在创建地址空间的时候插入 MMIO 虚拟页面)
    │       ├── syscall
    │       │   ├── fs.rs(修改:新增 sys_open)
    │       │   └── process.rs(修改:sys_exec 改为从文件系统中加载 ELF,并支持命令行参数)
    │       ├── task
    │       │   ├── mod.rs(修改初始进程 INITPROC 的初始化)
    

    存储块设备

    bg right:90% 85%


    提纲

    1. 实验目标和步骤
    2. 代码结构

    3. 应用程序设计

    1. 内核程序设计

    3.1 文件和目录

    3.2 文件访问系统调用


    理解文件

    • 对持久存储(persistent storage)的虚拟化和抽象
      • Tape,Disk,SSD...
      • 用户用它们保存真正关心的数据

    bg right:53% 95%


    从应用角度理解文件

    • 文件 是一个特殊的线性字节数组,每个字节都可以读取或写入。
    • 每个文件都有一个给用户可理解的字符串名字
    • 每个文件都有一个应用程序员可理解的某种低级名称-文件描述符 (file descriptor)
    • 显示文件的线性字节内容
     $ hexedit os/src/main.rs
    

    bg right:43% 95%


    从内核角度理解文件

    • 文件 是存储设备上的数据,需要通过文件系统进行管理
    • 管理文件的结构称为inode,inode描述了文件的各种属性和数据位置
    • 显示文件的线性字节内容
     $ cd os ; stat src/main.rs
    

    bg right:50% 95%


    从应用角度理解目录

    • 目录 是一个特殊的文件,它的内容包含一个位于该目录下的文件名列表
    • 显示目录内容
      $ cd os ; ls -la
    

    bg right:54% 95%


    从内核角度理解目录

    • 目录 是一个特殊的文件,它的内容包含一个(用户可读文件名字,inode)对的数组
    • DirEntry数组
    #![allow(unused)]
    fn main() {
    pub struct DirEntry {
        name: [u8; NAME_LENGTH_LIMIT + 1],
        inode_number: u32,
    }
    }

    bg right:50% 100%


    提纲

    1. 实验目标和步骤
    2. 代码结构
    3. 应用程序设计
    4. 内核程序设计

    3.1 文件和目录

    3.2 文件访问系统调用


    Filesystem OS (FOS)

    bg right:74% 95%


    open()系统调用

    /// 功能:打开一个常规文件,并返回可以访问它的文件描述符。
    /// 参数:path 描述要打开的文件的文件名
    /// (简单起见,文件系统不需要支持目录,所有的文件都放在根目录 / 下),
    /// flags 描述打开文件的标志,具体含义下面给出。
    /// 返回值:如果出现了错误则返回 -1,否则返回打开常规文件的文件描述符。
    /// 可能的错误原因是:文件不存在。
    /// syscall ID:56
    fn sys_open(path: &str, flags: u32) -> isize
    

    close()系统调用

    /// 功能:当前进程关闭一个文件。
    /// 参数:fd 表示要关闭的文件的文件描述符。
    /// 返回值:如果成功关闭则返回 0 ,否则返回 -1 。
    /// 可能的出错原因:传入的文件描述符并不对应一个打开的文件。
    
    /// syscall ID:57
    fn sys_close(fd: usize) -> isize
    

    read()系统调用

    /// 功能:当前进程读取文件。
    /// 参数:fd 表示要读取文件的文件描述符。
    /// 返回值:如果成功读入buf,则返回 读取的字节数,否则返回 -1 。
    /// 可能的出错原因:传入的文件描述符并不对应一个打开的文件。
    
    /// syscall ID:63
    sys_read(fd: usize, buf: *const u8, len: usize) -> isize
    

    write()系统调用

    /// 功能:当前进程写入一个文件。
    /// 参数:fd 表示要写入文件的文件描述符。
    /// 返回值:如果成功把buf写入,则返回写入字节数 ,否则返回 -1 。
    /// 可能的出错原因:传入的文件描述符并不对应一个打开的文件。
    
    /// syscall ID:64
    fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize
    

    应用程序示例

    // user/src/bin/filetest_simple.rs
    pub fn main() -> i32 {
        let test_str = "Hello, world!";
        let filea = "filea\0";
        // 创建文件filea,返回文件描述符fd(有符号整数)
        let fd = open(filea, OpenFlags::CREATE | OpenFlags::WRONLY);
        write(fd, test_str.as_bytes());               // 把test_str写入文件中
        close(fd);                                    // 关闭文件
        let fd = open(filea, OpenFlags::RDONLY);      // 只读方式打开文件
        let mut buffer = [0u8; 100];                  // 100字节的数组缓冲区
        let read_len = read(fd, &mut buffer) as usize;// 读取文件内容到buffer中
        close(fd);                                    // 关闭文件
    }
    

    提纲

    1. 实验目标和步骤
    2. 代码结构
    3. 应用程序设计

    4. 内核程序设计

    4.1 核心数据结构

    4.2 文件管理机制


    Filesystem OS (FOS)

    bg right:74% 95%


    核心数据结构

    • 进程管理文件
      • 目录、文件
      • inode
      • 文件描述符
      • 文件描述符表

    bg right:65% 95%


    核心数据结构

    • 文件位于根目录ROOT_INODE
    • 目录的内容是DirEntry组成的数组
    • 文件/目录用inode表示
    #![allow(unused)]
    fn main() {
    pub struct DirEntry {
        name: [u8; NAME_LENGTH_LIMIT + 1],
        inode_number: u32,
    }
    ... 
    let fd = open(filea, OpenFlags::RDONLY);
    }

    bg right:42% 95%


    核心数据结构

    • 打开的文件在进程中fd_table
    • fd_tableOSInode组成的数组
    #![allow(unused)]
    fn main() {
    pub struct TaskControlBlockInner {
        pub fd_table: ... //文件描述符表
    
    pub struct OSInode {//进程管理的inode
        readable: bool,  writable: bool,
        inner: UPSafeCell<OSInodeInner>,
    }
    
    pub struct OSInodeInner {
        offset: usize, //文件读写的偏移位置
        inode: Arc<Inode>,//存储设备inode
    }
    }

    bg right:42% 95%


    核心数据结构

    • 超级块
    • inode/data位图
    • disk_inode
    • disk_data
    • blk_cache

    bg right:66% 95%


    超级块

    超级块(SuperBlock)描述文件系统全局信息

    #![allow(unused)]
    fn main() {
    pub struct SuperBlock {
        magic: u32,
        pub total_blocks: u32,
        pub inode_bitmap_blocks: u32,
        pub inode_area_blocks: u32,
        pub data_bitmap_blocks: u32,
        pub data_area_blocks: u32,
    }
    }

    bg right:60% 95%


    位图(bitmap)

    位图(bitmap)描述文件系统全局信息

    • 在 easy-fs 布局中存在两类位图
      • 索引节点位图
      • 数据块位图
    #![allow(unused)]
    fn main() {
    pub struct Bitmap {
        start_block_id: usize,
        blocks: usize,
    }
    }

    bg right:64% 95%


    磁盘索引节点(DiskInode)

    • read_atwrite_at把文件偏移量和buf长度转换为一系列的数据块编号,并进行通过get_block_cache数据块的读写。
    • get_block_id 方法体现了 DiskInode 最重要的数据块索引功能,它可以从索引中查到它自身用于保存文件内容的第 block_id 个数据块的块编号,这样后续才能对这个数据块进行访问

    bg right:42% 95%


    磁盘索引节点(DiskInode)

    磁盘索引节点(DiskInode)描述文件信息和数据

    #![allow(unused)]
    fn main() {
    pub struct DiskInode {
        pub size: u32,
        pub direct: [u32; INODE_DIRECT_COUNT],
        pub indirect1: u32,
        pub indirect2: u32,
        type_: DiskInodeType,
    }
    }

    bg right:55% 95%


    数据块与目录项

    #![allow(unused)]
    fn main() {
    type DataBlock = [u8; BLOCK_SZ];
    
    pub struct DirEntry {
        name: [u8; NAME_LENGTH_LIMIT + 1],
        inode_number: u32,
    }
    }

    bg right:55% 95%


    块缓存BlockCache

    #![allow(unused)]
    fn main() {
    pub const BLOCK_SZ: usize = 512;
    
    pub struct BlockCache {
        cache: [u8; BLOCK_SZ], //512 字节数组
        block_id: usize, //对应的块编号
        //底层块设备的引用,可通过它进行块读写
        block_device: Arc<dyn BlockDevice>, 
        modified: bool, //它有没有被修改过
    }
    }

    get_block_cache :取一个编号为 block_id 的块缓存 bg right:55% 95%


    提纲

    1. 实验目标和步骤
    2. 代码结构
    3. 应用程序设计
    4. 内核程序设计

    4.1 核心数据结构

    4.2 文件管理机制


    文件管理机制概述

    1. 文件系统初始化
    2. 打开与关闭文件
    3. 基于文件加载应用
    4. 读写文件

    bg right:60% 95%


    文件系统初始化

    1. 打开块设备 BLOCK_DEVICE ;
    2. 从块设备 BLOCK_DEVICE 上打开文件系统;
    3. 从文件系统中获取根目录的 inode 。
    #![allow(unused)]
    fn main() {
    lazy_static! {
        pub static ref BLOCK_DEVICE = Arc::new(BlockDeviceImpl::new());
    ......
    lazy_static! {
        pub static ref ROOT_INODE: Arc<Inode> = {
            let efs = EasyFileSystem::open(BLOCK_DEVICE.clone());
            Arc::new(EasyFileSystem::root_inode(&efs))
    }

    打开(创建)文件

    #![allow(unused)]
    fn main() {
    pub fn sys_open(path: *const u8, flags: u32) -> isize {
        //调用open_file函数获得一个OSInode结构的inode
        if let Some(inode) = open_file(path.as_str(), 
                               OpenFlags::from_bits(flags).unwrap()) {
            let mut inner = task.inner_exclusive_access();
            let fd = inner.alloc_fd();  //得到一个空闲的fd_table项的idx,即fd
            inner.fd_table[fd] = Some(inode); //把inode填入fd_table[fd]中
            fd as isize  //返回fd 
        ...
    }

    如果失败,会返回 -1


    打开(创建)文件

    #![allow(unused)]
    fn main() {
    fn open_file(name: &str, flags: OpenFlags) -> Option<Arc<OSInode>> {
      ......
     ROOT_INODE.create(name) //在根目录中创建一个DireEntry<name,inode>
                    .map(|inode| {//创建进程中fd_table[OSInode]
                        Arc::new(OSInode::new( 
                            readable,
                            writable,
                            inode,  ))
                    })
    }

    在根目录ROOT_INODE中创建一个文件,返回OSInode


    打开(查找)文件

    #![allow(unused)]
    fn main() {
    fn open_file(name: &str, flags: OpenFlags) -> Option<Arc<OSInode>> {
      ......
     ROOT_INODE.find(name) //在根目录中查找DireEntry<name,inode>
                .map(|inode| { //创建进程中fd_table[OSInode]
                    Arc::new(OSInode::new(
                        readable,
                        writable,
                        inode ))
                })
    }

    在根目录ROOT_INODE中找到一个文件,返回OSInode


    关闭文件

    #![allow(unused)]
    fn main() {
    pub fn sys_close(fd: usize) -> isize {
        let task = current_task().unwrap();
        let mut inner = task.inner_exclusive_access();
        ......
        inner.fd_table[fd].take();
        0
    }
    }

    sys_close :将进程控制块中的文件描述符表对应的一项改为 None 代表它已经空闲即可,同时这也会导致文件的引用计数减一,当引用计数减少到 0 之后文件所占用的资源就会被自动回收。


    基于文件加载应用

    基于文件加载应用(ELF可执行文件格式)

    #![allow(unused)]
    fn main() {
        pub fn sys_exec(path: *const u8) -> isize {
            if let Some(app_inode) = open_file(path.as_str(), ...) {
                let all_data = app_inode.read_all();
                let task = current_task().unwrap();
                task.exec(all_data.as_slice()); 0
        } else { -1 }
    }

    当获取应用的 ELF 文件数据时,首先调用 open_file 函数,以只读方式打开应用文件并获取它对应的 OSInode 。接下来可以通过 OSInode::read_all 将该文件的数据全部读到一个向量 all_data


    读写文件

    • 基于文件抽象接口和文件描述符表
    • 可以按照无结构的字节流在处理基本的文件读写
    #![allow(unused)]
    fn main() {
      pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
            if let Some(file) = &inner.fd_table[fd] {
                file.write(
                    UserBuffer::new(translated_byte_buffer(token, buf, len))
                ) as isize
    }

    操作系统都是通过文件描述符在当前进程的文件描述符表中找到某个文件,无需关心文件具体的类型。


    读写文件

    • 基于文件抽象接口和文件描述符表
    • 可以按照无结构的字节流在处理基本的文件读写
    #![allow(unused)]
    fn main() {
      pub fn sys_read(fd: usize, buf: *const u8, len: usize) -> isize {
            if let Some(file) = &inner.fd_table[fd] {
                file.read(
                    UserBuffer::new(translated_byte_buffer(token, buf, len))
                ) as isize
    }

    操作系统都是通过文件描述符在当前进程的文件描述符表中找到某个文件,无需关心文件具体的类型。


    小结

    • 文件/目录的概念与实现
    • 文件系统设计与实现
    • 进程与文件系统
    • 能写霸王龙OS bg right 50%

    第十讲 进程间通信

    Inter Process Communication, IPC

    第一节 进程间通信(IPC)概述

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 进程间通信概述

    1. 管道(pipe)
    2. 消息队列(Message Queue)
    3. 共享内存(shared memory)
    4. 信号(Signal)

    进程间通信的需求

    • 挑战:单个程序的功能有限
    • IPC的目标:多进程协作完成复杂应用需求
      • 功能模块化
      • 程序之间相对隔离
      • 多个程序合作可完成复杂任务

    进程间通信的定义:进程间通过数据交换(共享或传递)进行交互的行为 bg right:43% 95%


    进程间的交互关系

    • 独立进程:与其它进程无交互
    • 协作进程:两个或多个进程之间有交互
      • 发送者 接收者 / 客户端 服务端
    ❯ cat README.md | grep rcore
    $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
    ...
    * [x] expand the fs image size generated by `rcore-fs-fuse` to 128MiB
    
    • grep依赖cat
      • grepcat产生的输出作为其输入,来匹配字符串

    进程通信方式

    • 直接通信:两个进程间不需要通过内核的中转,就可以相互传递信息
    • 间接通信:两个进程间通过系统调用和内核的中转,来相互传递消息

    bg right:54% 95%


    IPC机制

    进程间能共享或传递数据就算是进程间通信。

    IPC机制含义通信方式
    信号 (Signal)异步发送信号给进程处理间接通信
    管道 (Pipe)单方向传输字节流间接通信
    消息队列 (Message Queue)通过队列中转收/发消息间接通信
    套接字 (Socket)多/单机进程间网络通信间接通信
    共享内存 (Shared Memory)多个进程共享一块物理内存直接通信
    文件 (File)多个进程可访问同一文件间接通信

    UNIX的典型IPC机制

    进程间能共享或传递数据就是进程间通信。

    bg right:69% 67%


    消息传递的基本接口

    • 发送(send)消息
    • 接收(recv)消息
    • 远程过程调用(RPC)
    • 回复(reply)消息

    Remote Procedure Call, RPC = send + recv

    bg right:54% 95%


    阻塞或非阻塞通信

    • 阻塞通信:
      • 阻塞发送、阻塞接收
    • 非阻塞通信
      • 非阻塞发送、非阻塞接收

    bg right:51% 65%


    IPC的缓冲方式

    • 无限容量:发送方不需要等待
    • 有限容量:通信链路缓冲队列满时,发送方必须等待
    • 0容量: 发送方必须等待接收方

    bg right:47% 95%


    提纲

    1. 进程间通信概述

    2. 管道(pipe)

    1. 消息队列(Message Queue)
    2. 共享内存(shared memory)
    3. 信号(Signal)

    管道(pipe)

    管道是一种进程间通信机制, 也称为匿名管道(anonymous pipe)

    • 有读写端的一定大小的字节队列
    • 读端只能用来从管道中读取
    • 写端只能用来将数据写入管道
    • 读/写端通过不同文件描述符表示

    w:1000


    创建管道

    int pipe(int pipefd[2])

    • 管道可表示为两个文件描述符加一段内核空间中的内存
    • 创建管道时,返回两个文件描述符
      • 读管道
      • 写管道

    w:1000


    管道(pipe)的应用场景

    • 支持有关系的进程间通信
      • 父子进程、兄弟进程等
    • 父进程创建管道(两个文件描述符)
      • 子进程会继承文件描述符,执行读写管道

    w:1100


    管道(pipe)的应用场景

    • 通常管道两端的进程会各自关闭管道的一个文件描述符,如
      • 父进程关闭读描述符,只能向管道写数据
      • 子进程关闭写描述符,只能从管道读数据

    bg right:54% 95%


    管道实现机制

    bg 75%


    管道示例

    $ gcc -o ex1 ex1.c
    $ ./ex1
    parent
    write: the 0 message. 
    write: the 1 message. 
    ...
    children
    read: the 0 message. 
    read: the 1 message. 
    ...
    

    建议:同学们可在课后在自己的开发环境中实践一下

    bg right:54% 70%


    Shell中的管道

    只需使用一根竖线 "|" 连接两个命令即

    rCore-Tutorial-v3 on ch7
    ❯ cat README.md | grep rcore
    $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
    ...
    * [x] expand the fs image size generated by `rcore-fs-fuse` to 128MiB
    
    • 对于编写灵活的命令行脚本非常方便
    • 不支持任意两个进程间的通信

    命名管道(named pipe)

    在shell中可用mkfifo命令创建命名管道,也称为FIFO。

    匿名管道与命名管道都属于单向通信机制。两者的不同是:

    • 命名管道可以支持任意两个进程间的通信
    • 匿名管道只支持父子进程和兄弟进程间的通信

    命名管道是阻塞式的单向通信管道

    • 任意一方都可以读、写
    • 只有读、写端同时打开了命名管道时,数据才会写入并被读取

    命名管道

    shell A

    $ mkfifo name.fifo
    $ echo README > name.fifo  #文件类型为p,写命名管道阻塞
    

    shell B

    $ cat name.fifo
    

    但:字节流形态,不支持任意两个进程间的双向通信 命名管道示例


    提纲

    1. 进程间通信概述
    2. 管道(pipe)

    3. 消息队列(Message Queue)

    1. 共享内存(shared memory)
    2. 信号(Signal)

    消息队列(Message Queue)

    消息队列是由操作系统维护的以结构数据为基本单位的间接通信机制

    • 每个消息(Message)是一个字节序列,有自己的类型标识
    • 相同类型标识的消息组成按先进先出顺序组成一个消息队列

    w:1200


    消息队列实现机制

    w:1000


    消息队列实现机制

    w:900


    消息队列的系统调用

    • 消息队列的系统调用
      • msgget ( key, flags) //获取消息队列标识
      • msgsnd ( QID, buf, size, flags ) //发送消息
      • msgrcv ( QID, buf, size, type, flags ) //接收消息
      • msgctl( … ) // 消息队列控制

    消息的结构

    struct msgbuf {
    	long mtype;         /* 消息的类型 */
    	char mtext[1];      /* 消息正文 */
    };
    

    创建消息队列

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    
    int msgget(key_t key, int msgflg);
    

    参数:

    • key: 某个消息队列的名字
    • msgflg:由九个权限标志构成,用法和创建文件时使用的mode模式标志是一样的,IPC_CREAT or IPC_EXCL等

    创建消息队列

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    
    int msgget(key_t key, int msgflg);
    

    返回值:

    • 成功:msgget将返回一个非负整数,即该消息队列的标识码;
    • 失败:则返回“-1”

    创建消息队列

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    
    int msgget(key_t key, int msgflg);
    

    那么如何获取key值?

    • 通过宏定义key值
    • 通过ftok函数生成key值

    发送消息

    int  msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
    

    参数:

    • msgid: 由msgget函数返回的消息队列标识码
    • msg_ptr:是指向待发送数据的指针
    • msg_sz:是msg_ptr指向的数据长度
    • msgflg:控制着当前消息队列满或到达系统上限时的行为 如:IPC_NOWAIT 表示队列满不等待,返回EAGAIN错误

    发送消息

    int  msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
    
    • 成功返回0
    • 失败则返回-1

    接收消息

    int  msgrcv(int msgid, void *msg_ptr, size_t msgsz,long int msgtype, int msgflg);
    
    • msgid: 由msgget函数返回的消息队列标识码
    • msg_ptr:是指向准备接收的消息的指针
    • msgsz:是msg_ptr指向的消息长度
    • msgtype:它可以实现接收优先级的简单形式
      • msgtype=0返回队列第一条信息
      • msgtype>0返回队列第一条类型等于msgtype的消息 
      • msgtype<0返回队列第一条类型小于等于msgtype绝对值的消息

    接收消息

    int  msgrcv(int msgid, void *msg_ptr, size_t msgsz,long int msgtype, int msgflg);
    
    • msgflg:控制着队列中没有相应类型的消息可供接收时的行为
      • IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误
      • MSG_NOERROR,消息大小超过msgsz时被截断

    返回值:

    • 成功:返回实际放到接收缓冲区里去的字符个数
    • 失败:则返回-1

    消息队列示例程序

    $ gcc ex1.c 
    $ ./a.out
    Parent: input message type:
    1
    Parent: input message to be sent:
    test
    Parent: input message type:
    Child: read msg:test
    0
    

    建议:同学们可在课后在自己的开发环境中实践一下


    提纲

    1. 进程间通信概述
    2. 管道(pipe)
    3. 消息队列(Message Queue)

    4. 共享内存(shared memory)

    1. 信号(Signal)

    共享内存(shared memory, shmem)

    共享内存是把同一个物理内存区域同时映射到多个进程的内存地址空间的通信机制

    • 每个进程的内存地址空间需明确设置共享内存段
    • 优点:快速、方便地共享数据
    • 不足:需要同步机制协调数据访问

    w:550


    共享内存的系统调用

    • shmget( key, size, flags) //创建共享段
    • shmat( shmid, *shmaddr, flags) //把共享段映射到进程地址空间
    • shmdt( *shmaddr)//取消共享段到进程地址空间的映射
    • shmctl( …) //共享段控制

    注:需要信号量等同步机制协调共享内存的访问冲突


    共享内存实现机制

    w:900


    创建共享内存

    #include <sys/ipc.h>
    #include <sys/shm.h>
    int shmget(key_t key, size_t size,int shmflg);
    
    • key:进程间通信键值,ftok() 的返回值。
    • size:该共享存储段的长度(字节)。
    • shmflg:标识函数的行为及共享内存的权限,其取值如下:
      • IPC_CREAT:如果不存在就创建
      • IPC_EXCL: 如果已经存在则返回失败
    • 返回值:成功:共享内存标识符; 失败:-1。

    共享内存映射

    #include <sys/types.h>
    #include <sys/shm.h>
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    

    将一个共享内存段映射到调用进程的数据段中。即:让进程和共享内存建立一种联系,让进程某个指针指向此共享内存。

    返回值:

    • 成功:共享内存段映射地址( 相当于这个指针就指向此共享内存 )
    • 失败:-1

    共享内存映射

    void *shmat(int shmid, const void *shmaddr, int shmflg);
    
    • shmid:共享内存标识符,shmget() 的返回值。
    • shmaddr:共享内存映射地址,若为 NULL 则由系统自动指定
    • shmflg:共享内存段的访问权限和映射条件,取值如下:
      • 0:共享内存具有可读可写权限。
      • SHM_RDONLY:只读。
      • SHM_RND:(shmaddr 非空时才有效)

    共享内存示例程序

    $ gcc writer.c -o w
    $ gcc reader.c -o r
    $ ./w
     Writer: copy data to shared-memory
    

    共享内存示例

    $ ./r
    ------------ 共享内存段 --------------
    键        shmid      拥有者  权限     字节     连接数  状态
    0xdf20482b 1          chyyuu     666        512        0
    
    data = [ How are you, mike: from Writer ]
    deleted shared-memory
    
    ------------ 共享内存段 --------------
    键        shmid      拥有者  权限     字节     连接数  状态
    

    建议:同学们可在课后在自己的开发环境中实践一下


    提纲

    1. 进程间通信概述
    2. 管道(pipe)
    3. 消息队列(Message Queue)
    4. 共享内存(shared memory)

    5. 信号(Signal)


    信号(Signal)

    • 信号是中断正在运行的进程的异步消息或事件
    • 信号机制是一种进程间异步通知机制

    问题:

    • Ctrl+C为什么可以结束进程?
    • kill命令是怎么结束进程的?

    bg right:57% 95%


    信号发送和响应过程

    w:1100


    信号命名

    • 信号是一个整数编号,这些整数编号都定义了对应的宏名,宏名都是以SIG开头,比如SIGABRT, SIGKILL, SIGSTOP, SIGCONT w:850

    信号发送

    • 进程通过内核发出信号
      • shell通过kill命令向某个进程发送一个信号将其终止
    • 内核直接发出信号
      • 某进程从管道读取数据,但是管道的读权限被关闭了,内核会给进程发送一个SIGPIPE信号,提示读管道出错

    bg right:50% 100%


    信号发送

    • 外设通过内核发出
      • 比如按下Ctrl+C按键时,内核收到包含Ctrl+C按键的外设中断,会向正在运行的进程发送SIGINT信号,将其异常终止 bg right:50% 100%

    信号接收进程的处理方式

    • 忽略:信号没有发生过
    • 捕获:进程会调用相应的处理函数进行处理
    • 默认:如果不忽略也不捕获,此时进程会使用内核默认的处理方式来处理信号
      • 内核默认的信号处理:在大多情况下就是杀死进程或者直接忽略信号 bg right:45% 100%

    Linux信号

    Linux有哪些信号? -- 62个

    w:1150


    Linux信号

    为什么这么多信号?

    • 每个信号代表着某种事件,一般情况下,当进程收到某个信号时,就表示该信号所代表的事件发生了。
    • 对1~34中的常用信号,要求是理解,而不是记忆。当忘记了信号名字时,kill -l查看即可。

    Linux常用信号

    • SIGKILL
    • SIGINT
    • SIGSEGV bg right:68% 95%

    信号实现机制

    w:900


    信号实现机制

    w:950


    信号实现机制

    • 注册用户态信号处理函数sig_handler;
    • 内核在返回用户态前,发现有信号要处理;
    • 内核在用户栈压入sig_handler函数栈信息;
      • 模拟用户代码调用sig_handler函数
    • 内核在陷入上下文中修改用户态返回地址;
    • 内核返回用户态,直接跳到sig_handler;
    • sig_handler函数返回到旧代码位置继续执行 bg right:31% 95%

    信号实现机制

    bg right:70% 60%


    信号应用编程

    bg right:85% 100%


    小结

    • 管道的机制和实现原理
    • 消息队列的机制和实现原理
    • 共享内存的机制和实现原理
    • 信号的机制和实现原理
    • 上述机制与进程控制与管理的关系

    第十讲 进程间通信

    Inter Process Communication, IPC

    第二节 支持IPC的OS

    IPC OS (IOS)

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 实验安排

    1. 代码结构
    2. 管道的设计实现
    3. 信号的设计实现

    1.1 实验目标

    1.2 总体思路 1.3 历史背景 1.4 实践步骤


    以往实验目标

    提高性能、简化开发、加强安全、支持数据持久保存

    • Filesystem OS:支持数据持久保存
    • Process OS: 增强进程管理和资源管理
    • Address Space OS: 隔离APP访问的内存地址空间
    • multiprog & time-sharing OS: 让APP共享CPU资源
    • BatchOS: 让APP与OS隔离,加强系统安全,提高执行效率
    • LibOS: 让APP与HW隔离,简化应用访问硬件的难度和复杂性

    实验目标

    支持应用的灵活性,支持进程间交互

    bg right:74% 95%


    实验目标

    支持应用的灵活性,支持进程间交互

    • 扩展文件抽象:Pipe,Stdout, Stdin
    • 以文件形式进行进程间数据交换
    • 以文件形式进行串口输入输出
    • 信号实现进程间异步通知机制
    • 系统调用数量:11个 --> 17个
      • 管道:2 个、用于传数据
      • 信号:4 个、用于发通知

    bg right:40% 95%


    实验要求

    • 理解文件抽象
    • 理解IPC机制的设计与实现
      • pipe
      • signal
    • 会写支持IPC的OS

    bg right:63% 80%


    提纲

    1. 实验安排
    2. 代码结构
    3. 管道的设计实现
    4. 信号的设计实现

    1.1 实验目标

    1.2 总体思路

    1.3 历史背景 1.4 实践步骤


    管道实现需要考虑的问题

    • 管道是啥?
    • 如何访问管道?
    • 如何管理管道?

    w:1100


    理解管道

    管道是内核中的一块内存

    • 顺序写入/读出字节流

    管道可抽象为文件

    • 进程中包含管道文件描述符
      • 管道的FileTrait的接口
      • read/write
    • 应用创建管道的系统调用
      • sys_pipe

    bg right:50% 100%


    管道示例程序 (用户态)

    ...// usr/src/bin/pipetest.rs
    static STR: &str = "Hello, world!"  //字符串全局变量
    pub fn main() -> i32 {
        let mut pipe_fd = [0usize; 2]; //包含两个元素的fd数组
        pipe(&mut pipe_fd); // create pipe
        if fork() == 0 { // child process, read from parent
            close(pipe_fd[1]); // close write_end
            let mut buffer = [0u8; 32]; //包含32个字节的字节数组
            let len_read = read(pipe_fd[0], &mut buffer) as usize; //读pipe
        } else { // parent process, write to child
            close(pipe_fd[0]); // close read end
            write(pipe_fd[1], STR.as_bytes()); //写pipe
            let mut child_exit_code: i32 = 0;
            wait(&mut child_exit_code); //父进程等子进程结束
        }
    ...

    管道与进程的关系

    • pipe是进程控制块的资源之一

    bg right:71% 95%


    信号实现需要考虑的问题

    • 信号是啥?
    • 如何使用信号?
    • 如何管理信号?

    bg right:66% 90%


    理解信号

    signal是内核通知应用的软件中断

    准备阶段

    • 设定signal的整数编号值
    • 建立应对某signal编号值的例程signal_handler

    执行阶段

    • 向某进程发出signal,打断进程的当前执行,转到signal_handler执行

    bg right:40% 100%


    信号示例程序(用户态)

    ...// usr/src/bin/sig_simple.rs
    fn func() { //signal_handler
        println!("user_sig_test succsess");
        sigreturn(); //回到信号处理前的位置继续执行
    }
    pub fn main() -> i32 {
        let mut new = SignalAction::default();  //新信号配置
        let old = SignalAction::default();      //老信号配置
        new.handler = func as usize;            //设置新的信号处理例程
        if sigaction(SIGUSR1, &new, &old) < 0 { //setup signal_handler
            panic!("Sigaction failed!");
        }
        if kill(getpid() as usize, SIGUSR1) <0{ //send SIGUSR1 to itself
          ...
        }
    ...

    信号与进程的关系

    • signal是进程控制块的资源之一

    bg right:70% 90%


    提纲

    1. 实验安排
    2. 代码结构
    3. 管道的设计实现
    4. 信号的设计实现

    1.1 实验目标 1.2 总体思路

    1.3 历史背景

    1.4 实践步骤


    管道:Unix 中最引人注目的发明

    • 管道的概念来自贝尔实验室的Douglas McIlroy,他在1964年写的一份内部文件中,提出了把多个程序“像花园水管一样”串连并拧在一起的想法,这样数据就可以在不同程序中流动。
    • 大约在1972年下半年,Ken Thompson在听了Douglas McIlroy关于管道的唠叨后,灵机一动,迅速把管道机制实现在UNIX中。

    bg right:35% 90%


    信号:Unix 中容易出错的软件中断

    信号从Unix的第一个版本就已存在,只是与我们今天所知道的有点不同,需要通过不同的系统调用来捕获不同类型的信号。在版本4之后,改进为通过一个系统调用来捕获所有信号。

    bg right:35% 90%


    提纲

    1. 实验安排
    2. 代码结构
    3. 管道的设计实现
    4. 信号的设计实现

    1.1 实验目标 1.2 总体思路 1.3 历史背景

    1.4 实践步骤


    实践步骤

    git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
    cd rCore-Tutorial-v3
    git checkout ch7
    cd os
    make run
    

    参考输出

    [RustSBI output]
    ...
    filetest_simple
    fantastic_text
    **************/
    Rust user shell
    >>
    

    操作系统启动shell后,用户可以在shell中通过敲入应用名字来执行应用。


    测例 pipetest

    在这里我们运行一下本章的测例 pipetest :

    >> pipetest
    Read OK, child process exited!
    pipetest passed!
    >>
    

    此应用的父子进程通过pipe完成字符串"Hello, world!"的传递。


    测例 sig_simple

    在这里我们运行一下本章的测例 sig_simple :

    >> sig_simple 
    signal_simple: sigaction
    signal_simple: kill
    user_sig_test succsess
    signal_simple: Done
    >>
    

    此应用建立了针对SIGUSR1信号的信号处理例程func,然后再通过kill给自己发信号SIGUSR1,最终func会被调用。


    提纲

    1. 实验安排

    2. 代码结构

    1. 管道的设计实现
    2. 信号的设计实现

    bg right:66% 95%


    用户代码结构

    └── user
        └── src
            ├── bin
            │   ├── pipe_large_test.rs(新增:大数据量管道传输)
            │   ├── pipetest.rs(新增:父子进程管道传输)
            │   ├── run_pipe_test.rs(新增:管道测试)
            │   ├── sig_tests.rs(新增:多方位测试信号机制)
            │   ├── sig_simple.rs(新增:给自己发信号)
            │   ├── sig_simple2.rs(新增:父进程给子进程发信号)
            ├── lib.rs(新增两个系统调用:sys_close/sys_pipe/sys_sigaction/sys_kill...)
            └── syscall.rs(新增两个系统调用:sys_close/sys_pipe/sys_sigaction/sys_kill...)
    

    内核代码结构

    ├── fs(新增:文件系统子模块 fs)
    │   ├── mod.rs(包含已经打开且可以被进程读写的文件的抽象 File Trait)
    │   ├── pipe.rs(实现了 File Trait 的第一个分支——可用来进程间通信的管道)
    │   └── stdio.rs(实现了 File Trait 的第二个分支——标准输入/输出)
    ├── mm
    │   └── page_table.rs(新增:应用地址空间的缓冲区抽象 UserBuffer 及其迭代器实现)
    ├── syscall
    │   ├── fs.rs(修改:调整 sys_read/write 的实现,新增 sys_close/pipe)
    │   ├── mod.rs(修改:调整 syscall 分发)
    ├── task
    │   ├── action.rs(信号处理SignalAction的定义与缺省行为)
    │   ├── mod.rs(信号处理相关函数)
    │   ├── signal.rs(信号处理的信号值定义等)
    │   └── task.rs(修改:在任务控制块中加入信号相关内容)
    └── trap
    ├── mod.rs(进入/退出内核时的信号处理)
    

    提纲

    1. 实验安排
    2. 代码结构

    3. 管道的设计实现

    1. 信号的设计实现

    bg right:59% 95%


    管道的设计实现

    基于文件抽象,支持I/O重定向

    1. [K] 实现基于文件的标准输入/输出
    2. [K] 实现基于文件的实现管道
    3. [U] 支持命令行参数
    4. [U] 支持 “|" 符号

    bg right:60% 100%


    标准输入/输出文件

    1. 实现基于文件的标准输入/输出
    • FD:0 -- Stdin ; 1/2 -- Stdout
    • 实现File 接口
      • read -> call(SBI_CONSOLE_GETCHAR)
      • write -> call(SBI_CONSOLE_PUTCHAR)

    bg right:40% 100%


    标准输入/输出文件初始化

    1. 创建TCB时初始化fd_table
    #![allow(unused)]
    fn main() {
    TaskControlBlock::fork(...)->... {
      ...
      let task_control_block = Self {
          ...
              fd_table: vec![
                  // 0 -> stdin
                  Some(Arc::new(Stdin)),
                  // 1 -> stdout
                  Some(Arc::new(Stdout)),
                  // 2 -> stderr
                  Some(Arc::new(Stdout)),
              ],
    ...
    }

    bg right:35% 100%


    fork实现中的标准文件创建

    1. fork时复制fd_table
    #![allow(unused)]
    fn main() {
    TaskControlBlock::new(elf_data: &[u8]) -> Self{
      ...
        // copy fd table
        let mut new_fd_table = Vec::new();
        for fd in parent_inner.fd_table.iter() {
            if let Some(file) = fd {
                new_fd_table.push(Some(file.clone()));
            } else {
                new_fd_table.push(None);
            }
        }
    }

    bg right:35% 100%


    管道文件

    1. 管道的系统调用
    #![allow(unused)]
    fn main() {
    /// 功能:为当前进程打开一个管道。
    /// 参数:pipe 表示应用地址空间中
    /// 的一个长度为 2 的 usize 数组的
    /// 起始地址,内核需要按顺序将管道读端
    /// 和写端的文件描述符写入到数组中。
    /// 返回值:如果出现了错误则返回 -1,
    /// 否则返回 0 。
    /// 可能的错误原因是:传入的地址不合法。
    /// syscall ID:59
    pub fn sys_pipe(pipe: *mut usize) -> isize;
    }

    bg right:35% 100%


    管道文件

    1. 创建管道中的Buffer
    #![allow(unused)]
    fn main() {
    pub struct PipeRingBuffer {
        arr: [u8; RING_BUFFER_SIZE],
        head: usize,
        tail: usize,
        status: RingBufferStatus,
        write_end: Option<Weak<Pipe>>,
    }
    
    make_pipe() -> (Arc<Pipe>, Arc<Pipe>) {
        let buffer = PipeRingBuffer::new();
        let read_end = Pipe::read_end_with_buffer();
        let write_end = Pipe::write_end_with_buffer();
        ...
        (read_end, write_end)  
    }

    bg right:35% 100%


    管道文件

    1. 实现基于文件的输入/输出
    • 实现File 接口
    #![allow(unused)]
    fn main() {
        fn read(&self, buf: UserBuffer) -> usize {
           *byte_ref = ring_buffer.read_byte();
        }
        fn write(&self, buf: UserBuffer) -> usize {  
          ring_buffer.write_byte( *byte_ref );
        }
    }

    bg right:35% 100%


    exec系统调用的命令行参数

    • sys_exec 的系统调用接口需要发生变化
    #![allow(unused)]
    fn main() {
    // 增加了args参数
    pub fn sys_exec(path: &str, args: &[*const u8]) -> isize;
    }
    • shell程序的命令行参数分割
    #![allow(unused)]
    fn main() {
    // 从一行字符串中获取参数
    let args: Vec<_> = line.as_str().split(' ').collect();
    // 用应用名和参数地址来执行sys_exec系统调用
    exec(args_copy[0].as_str(), args_addr.as_slice())
    }

    bg right:35% 100%


    exec系统调用的命令行参数

    • 将获取到的参数字符串压入到用户栈上
    #![allow(unused)]
    fn main() {
    impl TaskControlBlock {
     pub fn exec(&self, elf_data: &[u8], args: Vec<String>) {
       ...
       // push arguments on user stack
     }
    }
    • Trap 上下文中的 a0/a1 寄存器,让 a0 表示命令行参数的个数,而 a1 则表示图中 argv_base 即蓝色区域的起始地址。

    bg right:30% 100%


    exec系统调用的命令行参数

    #![allow(unused)]
    fn main() {
    pub extern "C" fn _start(argc: usize, argv: usize) -> ! {
       //获取应用的命令行个数 argc, 获取应用的命令行参数到v中
       //执行应用的main函数
       exit(main(argc, v.as_slice()));
    } 
    }

    bg right:30% 100%


    重定向

    • 复制文件描述符系统调用
    #![allow(unused)]
    fn main() {
    /// 功能:将进程中一个已经打开的文件复制
    /// 一份并分配到一个新的文件描述符中。
    /// 参数:fd 表示进程中一个已经打开的文件的文件描述符。
    /// 返回值:如果出现了错误则返回 -1,否则能够访问已打
    /// 开文件的新文件描述符。
    /// 可能的错误原因是:传入的 fd 并不对应一个合法的已打
    /// 开文件。
    /// syscall ID:24
    pub fn sys_dup(fd: usize) -> isize;
    }

    重定向

    • 复制文件描述符系统调用
    #![allow(unused)]
    fn main() {
    pub fn sys_dup(fd: usize) -> isize {
      ...
      let new_fd = inner.alloc_fd();
      inner.fd_table[new_fd] = inner.fd_table[fd];
      newfd
    } 
    }

    shell重定向 "$ A | B"

    #![allow(unused)]
    fn main() {
    // user/src/bin/user_shell.rs
    {
      let pid = fork();
        if pid == 0 {  
            let input_fd = open(input, ...); //输入重定向 -- B 子进程
            close(0);                        //关闭文件描述符0
            dup(input_fd); //文件描述符0与文件描述符input_fd指向同一文件
            close(input_fd); //关闭文件描述符input_fd
            //或者
            let output_fd = open(output, ...);//输出重定向 -- A子进程
            close(1);                         //关闭文件描述符1
            dup(output_fd);//文件描述符1与文件描述符output_fd指向同一文件
            close(output_fd);//关闭文件描述符output_fd
        //I/O重定向后执行新程序
         exec(args_copy[0].as_str(), args_addr.as_slice()); 
        }...
    }

    提纲

    1. 实验安排
    2. 代码结构
    3. 管道的设计实现

    4. 信号的设计实现

    • signal的系统调用
    • signal核心数据结构
    • 建立signal_handler
    • 支持kill系统调用

    bg right:59% 95%


    与信号处理相关的系统调用

    • sigaction: 设置信号处理例程
    • sigprocmask: 设置要阻止的信号
    • kill: 将某信号发送给某进程
    • sigreturn: 清除堆栈帧,从信号处理例程返回

    bg right:60% 100%


    与信号处理相关的系统调用

    #![allow(unused)]
    fn main() {
    // 设置信号处理例程
    // signum:指定信号
    // action:新的信号处理配置
    // old_action:老的的信号处理配置
    sys_sigaction(signum: i32, 
       action: *const SignalAction,
       old_action: *const SignalAction) 
       -> isize
    
    pub struct SignalAction {
        // 信号处理例程的地址
        pub handler: usize, 
        // 信号掩码
        pub mask: SignalFlags
    }   
    }

    bg right:50% 100%


    与信号处理相关的系统调用

    #![allow(unused)]
    fn main() {
    // 设置要阻止的信号
    // mask:信号掩码
    sys_sigprocmask(mask: u32) -> isize 
    }
    #![allow(unused)]
    fn main() {
    // 清除堆栈帧,从信号处理例程返回
     sys_sigreturn() -> isize
    }
    #![allow(unused)]
    fn main() {
    // 将某信号发送给某进程
    // pid:进程pid
    // signal:信号的整数码
    sys_kill(pid: usize, signal: i32) -> isize
    }

    bg right:50% 100%


    信号的核心数据结构

    进程控制块中的信号核心数据结构

    #![allow(unused)]
    fn main() {
    pub struct TaskControlBlockInner {
        ...
        pub signals: SignalFlags,     // 要响应的信号
        pub signal_mask: SignalFlags, // 要屏蔽的信号
        pub handling_sig: isize,      // 正在处理的信号
        pub signal_actions: SignalActions,       // 信号处理例程表
        pub killed: bool,             // 任务是否已经被杀死了
        pub frozen: bool,             // 任务是否已经被暂停了
        pub trap_ctx_backup: Option<TrapContext> //被打断的trap上下文
    }
    }

    建立signal_handler

    #![allow(unused)]
    fn main() {
    fn sys_sigaction(signum: i32, action: *const SignalAction, 
                              old_action: *mut SignalAction) -> isize {
      //保存老的signal_handler地址到old_action中
      let old_kernel_action = inner.signal_actions.table[signum as usize];
      *translated_refmut(token, old_action) = old_kernel_action;
     //设置新的signal_handler地址到TCB的signal_actions中
      let ref_action = translated_ref(token, action);
      inner.signal_actions.table[signum as usize] = *ref_action;
    }

    对于需要修改的信号编号signum:

    1. 保存老的signal_handler地址到old_action
    2. 设置action为新的signal_handler地址

    通过kill发出信号

    #![allow(unused)]
    fn main() {
    fn sys_kill(pid: usize, signum: i32) -> isize {
          let Some(task) = pid2task(pid);
          // insert the signal if legal
          let mut task_ref = task.inner_exclusive_access();
          task_ref.signals.insert(flag);
         ...
    }

    对进程号为pid的进程发送值为signum的信号:

    1. 根据pid找到TCB
    2. 在TCB中的signals插入signum信号值

    通过kill发出和处理信号的过程

    pid进程进入内核后,直到从内核返回用户态前的执行过程:

    执行APP --> __alltraps 
             --> trap_handler 
                --> handle_signals 
                    --> check_pending_signals 
                        --> call_kernel_signal_handler
                        --> call_user_signal_handler
                           -->  // backup trap Context
                                // modify trap Context
                                trap_ctx.sepc = handler; //设置回到中断处理例程的入口
                                trap_ctx.x[10] = sig;   //把信号值放到Reg[10]
                --> trap_return //找到并跳转到位于跳板页的`__restore`汇编函数
           -->  __restore //恢复被修改过的trap Context,执行sret
    执行APP的signal_handler函数
    

    APP恢复正常执行

    当进程号为pid的进程执行完signal_handler函数主体后,会发出sys_sigreturn系统调用:

    #![allow(unused)]
    fn main() {
    fn sys_sigreturn() -> isize {
      ...
      // 恢复之前备份的trap上下文
      let trap_ctx = inner.get_trap_cx();
      *trap_ctx = inner.trap_ctx_backup.unwrap();
      ...
    执行APP --> __alltraps 
           --> trap_handler 
                --> 处理 sys_sigreturn系统调用
                --> trap_return //找到并跳转到位于跳板页的`__restore`汇编函数
        -->  __restore //恢复被修改过的trap Context,执行sret
    执行APP被打断的地方
    }

    屏蔽信号

    #![allow(unused)]
    fn main() {
    fn sys_sigprocmask(mask: u32) -> isize {
        ...
        inner.signal_mask = flag;
        old_mask.bits() as isize
        ...
    }

    把要屏蔽的信号直接记录到TCB的signal_mask数据中


    小结

    • 管道的概念与实现
    • 信号的概念与实现
    • 能写迅猛龙操作系统

    bg right 80%


    课程实验四 文件系统与进程间通信

    • 实验目标
      • 硬链接
    • 实验任务描述
    • 实验提交要求
      • 2023年11月26日;

    第十一讲 线程与协程

    第一节 线程

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 为何需要线程?

    1. 线程的概念
    2. 使用线程
    3. 线程的设计实现

    进程存在的不足

    • 进程之间地址空间隔离
    • 通过IPC共享/交换数据不方便
    • 管理进程开销大
    • 创建/删除/切换
    • 并行/并发处理困难 bg right:62% 100%

    进程存在的不足

    • 进程之间地址空间隔离
    • 通过IPC共享/交换数据不方便
    • 管理进程开销大
    • 创建/删除/切换
    • 并行/并发处理困难 bg right:62% 100%

    为何需要线程?

    • 在应用中可能同时发生多种活动,且某些活动会被阻塞
    • 程序分解成可并行运行的多个顺序控制流
      • 可提高执行效率
      • 程序设计模型也会变得更简单 bg right:62% 95%

    为何需要线程?

    永远存在的用户需求 -- 性能

    • 并行实体(多个顺序控制流)共享同一个地址空间和所有可用数据
    • 访问数据和共享资源方便
    • 切换控制流轻量
    • 管理不同控制流便捷

    bg right:51% 100%


    线程 vs 进程

    • 进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位;
    • 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
    • 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;
    • 线程能减少并发执行的时间和空间开销;

    线程 vs 进程

    • 一个进程中可以同时存在多个线程;
    • 各个线程之间可以并发执行;
    • 各个线程之间可以共享地址空间和文件等资源;
    • 当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃(这里是针对 C/C++ 语言,Java语言中的线程崩溃不会造成进程崩溃)。

    提纲

    1. 为何需要线程?

    2. 线程的概念

    1. 使用线程
    2. 线程的设计实现

    2.1 线程的定义

    2.2 线程管理


    线程的定义

    线程是进程的一部分,描述指令流执行状态。它是进程中的指令执行流的基本单元,是CPU调度的基本单位

    bg right:60% 90%


    进程和线程的角色

    • 进程的资源分配角色

      • 进程由一组相关资源构成,包括地址空间(代码段、数据段)、打开的文件等各种资源
    • 线程的处理机调度角色

      • 线程描述在进程资源环境中的指令流执行状态

    bg right:57% 95%


    不同操作系统对线程的支持

    w:950


    进程和线程的关系

    进程 = 线程 + 共享资源

    • 一个进程中可存在多个线程
    • 线程共享进程的地址空间
    • 线程共享进程的资源
    • 线程崩溃会导致进程崩溃

    线程是一个调度实体 Scheduling Entry User-SE v.s. Kernel-SE

    bg right:50% 100%


    线程与进程的比较

    • 进程是资源分配单位,线程是CPU调度单位
    • 进程拥有一个完整的资源平台,而线程只独享指令流执行的必要资源,如寄存器和栈
    • 线程具有就绪、等待和运行三种基本状态和状态间的转换关系
    • 线程能减少并发执行的时间和空间开销
      • 线程的创建/终止/切换时间比进程短
      • 同一进程的各线程间共享内存和文件资源,可不通过内核进行直接通信

    提纲

    1. 为何需要线程?
    2. 线程的概念
    3. 使用线程
    4. 线程的设计实现

    2.1 线程的定义

    2.2 线程管理


    用户态管理的线程与内核态管理的线程

    bg 95% bg 85%


    线程控制块(TCB, Thread Control Block)

    typedef struct
    {
           int                       detachstate;   // 线程的分离状态
           int                       schedpolicy;   // 线程调度策略 FIFO、RR等
           structsched_param         schedparam;    // 线程的调度参数 优先级
           int                       inheritsched;  // 线程的继承性
           int                       scope;         // 线程的作用域进程级、系统级
           size_t                    guardsize;     // 线程栈末尾的警戒缓冲区大小
           int                       stackaddr_set; // 线程的栈设置
           void*                     stackaddr;     // 线程栈的位置,起始地址
           size_t                    stacksize;     // 线程栈的大小
    } pthread_attr_t;
    

    创建线程API

    创建线程:成功返回零,否则返回非零值

    #include <pthread.h>
    int pthread_create(      pthread_t *        thread,
                   const pthread_attr_t *       attr,
                         void *                 (*start_routine)(void*),
                         void *                 arg);
    
    • thread指向pthread_t结构类型的指针
    • attr用于指定该线程可能具有的任何属性
    • start_routine是线程开始运行的函数指针
    • arg是要传递给线程开始执行的函数的参数

    等待线程API

    等待线程:一直阻塞调用它的线程,直至目标线程执行结束

    #include <pthread.h>
    int pthread_join(pthread_t thread, void **retval);
    
    • thread指向pthread_t结构类型的指针
    • retval是指向返回值的指针

    提纲

    1. 为何需要线程?
    2. 线程的概念

    3. 使用线程

    1. 线程的设计实现

    线程示例

    1    void *mythread(void *arg) {
    2        printf("%s\n", (char *) arg);
    3        return NULL;
    4    }
    5    int main(int argc, char *argv[]) {
    6       pthread_t p1, p2;
    7       int rc;
    8       printf("main: begin\n");
    9       rc = pthread_create(&p1, NULL, mythread, "A"); assert(rc == 0);
    10      rc = pthread_create(&p2, NULL, mythread, "B"); assert(rc == 0);
    11      // join waits for the threads to finish
    12      rc = pthread_join(p1, NULL); assert(rc == 0);
    13      rc = pthread_join(p2, NULL); assert(rc == 0);
    14      printf("main: end\n");
    15      return 0;
    16   }
    

    线程示例输出

    一个程序,它创建两个线程,每个线程都做了一些独立的工作,在这例子中,打印“A”或“B”。

    ❯ ./t0
    main: begin
    A
    B
    main: end
    

    提纲

    1. 为何需要线程?
    2. 线程的概念
    3. 使用线程

    4. 线程的设计实现

    4.1 用户态管理且用户态运行的线程

    4.2 内核态管理且用户态运行的线程 4.3 内核态管理且内核态运行的线程 4.4 混合管理且运行的线程


    线程的几种实现方式

    • 用户态管理且用户态运行的线程(内核不可见的用户线程)
      • Thread managed&running in User-Mode
    • 内核态管理且用户态运行的线程(内核可见的用户线程)
      • Thread managed in Kernel-Mode&running in User-Mode
    • 内核态管理且内核态运行的线程(内核线程)
      • Thread managed&running in Kernel-Mode
    • 混合管理且运行的线程(轻量级进程,混合线程)
      • Thread managed&running in Mixed-Mode

    用户态管理且用户态运行的线程

    • 在用户态实现线程的管理与运行,操作系统感知不到这类线程的存在
      • POSIX Pthreads,Mach C-threads,Solaris threads
      • 别名:用户态线程(User-level Thread)、绿色线程(Green Thread)、有栈协程(Stackful Coroutine)、纤程(Fiber)

    bg right:45% 100%


    用户态管理且用户态运行的线程

    • 由一组用户级的线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等

    bg right:45% 100%


    用户态管理线程的优点

    • 线程的调度不需要内核直接参与,控制简单。
    • 可以在不支持线程的操作系统中实现。
    • 创建和销毁线程、线程切换等线程管理的代价比内核线程少得多。
    • 允许每个进程定制自己的调度算法,线程管理比较灵活。
    • 线程能够利用的表空间和堆栈空间比内核级线程多。
    • 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。

    用户态管理线程的不足

    • 一个线程发起系统调用而阻塞时,则整个进程进入等待
    • 不支持基于线程的处理机抢占
    • 只能按进程分配CPU时间
    • 多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用

    bg right:48% 100%


    提纲

    1. 为何需要线程?
    2. 线程的概念
    3. 使用线程
    4. 线程的设计实现

    4.1 用户态管理且用户态运行的线程

    4.2 内核态管理且用户态运行的线程

    4.3 内核态管理且内核态运行的线程 4.4 混合管理且运行的线程


    内核态管理且用户态运行的线程

    • 由内核通过系统调用实现的线程机制,由内核完成线程的创建、终止和管理
    • 由内核维护线程控制块TCB, 在内核实现
    • 线程执行系统调用而被阻塞不影响其他线程

    bg right:45% 100%


    内核态管理且用户态运行的线程

    • 一个进程中可以包括多个线程
      • Windows内核的设计
      • rCore/uCore内核的设计
    • 一个进程中只包括一个线程
      • Linux内核的设计

    bg right:45% 100%


    内核态管理且用户态运行线程的不足

    • 在一般情况下,线程切换开销与进程切换开销相差不大,大于用户态管理且用户态允许的线程切换开销
    • 与传统的进程管理机制会产生一些矛盾,一些系统调用的实现功能/语义上会不协调
      • fork()、signal() ...

    bg right:50% 100%


    多线程fork()引起的问题

    多线程应用程序中,建议谨慎使用 fork()

    • 竞态条件:线程正在修改共享变量或资源时调用了fork(),会导致子进程继承这个状态和资源的不一致性
    • 死锁:线程正在持有某个锁或资源时调用fork(),可能导致子进程无法获得该锁而导致死锁
    • 内存占用:多线程fork()会复制整个进程的地址空间,包括所有线程所拥有的栈、寄存器和锁等资源
    • 性能下降:多线程fork()的开销较大,可能会影响应用程序的性能

    提纲

    1. 为何需要线程?
    2. 线程的概念
    3. 使用线程
    4. 线程的设计实现

    4.1 用户态管理且用户态运行的线程 4.2 内核态管理且用户态运行的线程

    4.3 内核态管理且内核态运行的线程

    4.4 混合管理且运行的线程


    内核态管理且内核态运行的线程(简称:内核线程)

    • 由内核实现线程机制,由内核完成线程的创建、终止和管理
    • 由内核维护TCB, 在内核实现
    • 线程在内核中执行
      • 如:Linux的内核线程

    bg right:47% 95%


    内核态管理且内核态运行的线程

    • 一个内核线程可分时/并行处理一件内核任务
    • 内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程

    bg right:45% 100%


    内核态管理且内核态运行的线程的作用

    • 执行周期性的任务
      • 把Buffer-Cache定期写回到存储设备上
      • 在可用物理内存页很少情况下执行虚存交换操作
      • 实现文件系统的事务日志
      • ......

    bg right:45% 100%


    提纲

    1. 为何需要线程?
    2. 线程的概念
    3. 使用线程
    4. 线程的设计实现

    4.1 用户态管理且用户态运行的线程 4.2 内核态管理且用户态运行的线程 4.3 内核态管理且内核态运行的线程

    4.4 混合管理且运行的线程


    轻量级进程:双态管理的线程

    轻量级进程(Light-Weight Process,LWP)是内核支持的用户线程

    • 一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的,也就是 LWP 都是由一个内核线程支持。
    • 在 LWP 之上也可使用用户线程。

    轻权进程与用户线程的对应关系

    • 1 : 1,即一个 LWP 对应 一个用户线程:Linux, JVM
      • 用户态管理取消,内核管理线程
    • N : 1,即一个 LWP 对应多个用户线程:与OS无关的Green Thread
      • 内核态仅管理包含多个线程的进程,用户态的线程运行时管理线程
    • M : N,即多个 LWP 对应多个用户线程:Solaris OS, Go runtime
      • 用户态线程运行时和内核协同进行管理

    轻权进程实例

    M : N线程模型

    • Solaris 操作系统+C线程运行时库
    • Go语言+Go运行时库+OS

    bg right:50% 100%


    轻权进程管理

    • 编程人员决定内核线程与用户级线程的对应关系
    • 用户级线程由用户线程管理库管理
    • 内核只识别内核级线程/进程,并对其进行调度
    • 内核与用户态线程管理库交互
    • 具有最大灵活度和实现复杂性

    bg right:45% 100%


    线程的上下文切换

    线程是调度的基本单位,而进程则是资源拥有的基本单位。

    • 不同进程中的线程切换:进程上下文切换
    • 相同进程中的线程切换:虚拟内存等进程资源保持不动,只需要切换线程的私有数据、寄存器等不共享的数据

    小结

    1. 为何需要线程?
    2. 线程的概念
    3. 使用线程
    4. 线程的设计实现

    第十一讲 线程与协程

    第二节 协程(Coroutine)

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 协程的概念

    1. 协程的实现
    2. 协程示例
    3. 协程与操作系统内核

    bg right:60% 70%


    线程存在的不足

    • 大规模并发I/O操作场景
      • 大量线程占内存总量大
      • 管理线程程开销大
        • 创建/删除/切换
      • 访问共享数据易错 bg right:51% 100%

    协程(coroutine)的提出

    协程由Melvin Conway在1963年提出并实现(Ref)

    • 作者对协程的描述是“行为与主程序相似的子例程(subroutine)”
    • 协程采用同步编程方式支持大规模并发I/O异步操作

    Donald Knuth :子例程是协程的特例 bg right:42% 100%


    协程的定义

    • Wiki的定义:协程是一种程序组件,是由子例程(过程、函数、例程、方法、子程序)的概念泛化而来的,子例程只有一个入口点且只返回一次,协程允许多个入口点,可在指定位置挂起和恢复执行。

    协程的核心思想:控制流的主动让出与恢复

    bg right:35% 100%


    协程(异步函数)与函数(同步函数)

    • 相比普通函数,协程的函数体可以挂起并在任意时刻恢复执行
      • 无栈协程是普通函数的泛化
      • 本课程中的协程限指无栈协程(Stackless Coroutine)

    w:900


    协程与用户线程的比较

    • 协程的内存占用比线程小
      • 线程数量越多,协程的性能优势越明显
    • 不需要多线程的锁机制,不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态,所以执行效率比多线程高很多。 w:750

    协程示例(python)

    def func()://普通函数
       print("a")
       print("b")
       print("c")
    
    def func()://协程函数
      print("a")
      yield
      print("b")
      yield
      print("c")
    

    bg right:55% 80%


    提纲

    1. 协程的概念

    2. 协程的实现

    1. 协程示例
    2. 协程与操作系统内核

    bg right:60% 70%


    协程的实现方式

    2004年Lua的作者Ana Lucia de Moura和Roberto Ierusalimschy发表论文“Revisiting Coroutines”,提出依照三个因素来对协程进行分类:

    • 控制传递(Control-transfer)机制
    • 栈式(Stackful)构造
    • 编程语言中第一类(First-class)对象

    基于控制传递的协程

    控制传递机制:对称(Symmetric) v.s. 非对称(Asymmetric)协程

    • 对称协程:
      • 只提供一种传递操作,用于在协程间直接传递控制
      • 对称协程都是等价的,控制权直接在对称协程之间进行传递
      • 对称协程在挂起时主动指明另外一个对称协程来接收控制权
    • 非对称协程(半对称(Semi-symmetric)协程):
      • 提供调用和挂起两种操作,非对称协程挂起时将控制返回给调用者
      • 调用者或上层管理者根据某调度策略调用其他非对称协程

    对称协程的控制传递

    w:900


    非对称协程的控制传递

    w:850


    对称协程

    对称协程是指所有协程都是对等的,每个协程可以主动挂起自己,并让出处理器给其他协程执行。对称协程不需要操作系统内核的支持,可以在用户空间中实现,具有更快的上下文切换速度和更小的内存开销。

    • 优点:简单易用,没有复杂的调度逻辑。
    • 缺点:如果某个协程死循环或阻塞,会导致整个进程挂起。

    非对称协程

    非对称协程是指协程和线程一起使用,协程作为线程的子任务来执行。只有线程可以主动挂起自己,而协程则由线程控制其执行状态。

    • 优点:
      • 支持并发执行,可以通过多线程实现更高的并发性。
      • 协程之间不会相互阻塞,可处理一些长时间任务。
    • 缺点:
      • 实现较为复杂,需要操作系统内核的支持。
      • 需要通过锁等机制来保证协程之间的同步和互斥。

    有栈(stackful)协程和无栈(stackless)协程

    • 无栈协程:指可挂起/恢复的函数
      • 无独立的上下文空间(栈),数据保存在堆上
      • 开销: 函数调用的开销
    • 有栈协程:用户态管理并运行的线程
      • 有独立的上下文空间(栈)
      • 开销:用户态切换线程的开销

    基于第一类语言对象的协程

    第一类(First-class)语言对象:First-class对象 v.s. second-class对象 (是否可以作为参数传递)

    • First-class对象 : 协程被在语言中作为first-class对象
      • 可作为参数被传递,由函数创建并返回,并存储在一个数据结构中供后续操作
      • 提供了良好的编程表达力,方便开发者对协程进行操作
    • 受限协程
      • 特定用途而实现的协程,协程对象限制在指定的代码结构中

    第一类(First-class)语言对象

    • 可被赋值给一个变量
    • 可嵌入到数据结构中
    • 可作为参数传递给函数
    • 可作为值被函数返回

    第一类(First-class)语言对象

    • First-class 对象优势:
      • 可作为函数参数传递,使得代码更加灵活
      • 可作为函数返回值返回,方便编写高阶函数
      • 可被赋值给变量或存储在数据结构中,方便编写复杂的数据结构
    • First-class 对象劣势:
      • 可能会增加程序的开销和复杂度。
      • 可能存在安全性问题,例如对象被篡改等。
      • 可能会导致内存泄漏和性能问题。

    第二类(Second-class)语言对象

    • Second-class 对象优势:
      • 可以通过类型系统来保证程序的正确性
      • 可以减少程序的复杂度和开销
      • 可以提高程序的运行效率和性能
    • Second-class 对象劣势:
      • 缺乏灵活性,不能像 First-class 对象一样灵活使用
      • 不太适合处理复杂的数据结构和算法
      • 不支持函数式编程和面向对象编程的高级特性(例如不支持多态)

    Rust语言中的协程Future

    A future is a representation of some operation which will complete in the future.

    bg right:54% 95%


    Rust语言中的协程Future

    Rust 的 Future 实现了 Async Trait,它包含了三个方法:

    • poll: 用于检查 Future 是否完成。
    • map: 用于将 Future 的结果转换为另一个类型。
    • and_then: 用于将 Future 的结果传递给下一个 Future。

    使用 Future 时,可以通过链式调用的方式对多个异步任务进行串联。


    Rust语言中的协程Future

    use futures::future::Future;
    
    fn main() {
        let future1 = async { 1 + 2 };
        let future2 = async { 3 + 4 };
    
        let result = future1
            .and_then(|x| future2.map(move |y| x + y))
            .await;
    
        println!("Result: {}", result);
    }

    基于有限状态机的Rust协程实现

    #![allow(unused)]
    fn main() {
    async fn example(min_len: usize) -> String {
        let content = async_read_file("foo.txt").await;
        if content.len() < min_len {
            content + &async_read_file("bar.txt").await
        } else {
            content
        }
    }
    }

    bg right width:600px


    基于轮询的 Future的异步执行过程

    width:750px


    协程的优点

    • 协程创建成本小,降低了内存消耗
    • 协程自己的调度器,减少了 CPU 上下文切换的开销,提高了 CPU 缓存命中率
    • 减少同步加锁,整体上提高了性能
    • 可按照同步思维写异步代码
      • 用同步的逻辑,写由协程调度的回调

    协程 vs 线程 vs 进程

    • 切换
      • 进程:页表,堆,栈,寄存器
      • 线程:栈,寄存器
      • 协程:寄存器,不换栈

    w:560w:580


    协程 vs 线程 vs 进程

    协程适合IO密集型场景

    w:1200


    提纲

    1. 协程的概念
    2. 协程的实现

    3. 协程示例

    1. 协程与操作系统内核

    bg right:60% 70%


    支持协程的编程语言

    • 无栈协程:Rust、C++20、C、Python、Java、Javascript等
    • 有栈协程(即线程):Go、Java2022、Python、Lua

    w:1100


    GO协程(goroutine)

    ... //https://gobyexample-cn.github.io/goroutines
    func f(from string) {
        for i := 0; i < 3; i++ {
            fmt.Println(from, ":", i)
        }
    }
    func main() {
        f("direct")
        go f("goroutine")
        go func(msg string) {
            fmt.Println(msg)
        }("going")
        time.Sleep(time.Second)
        fmt.Println("done")
    }
    

    bg right:30% 100%


    python协程

    URL = 'https://httpbin.org/uuid'
    async def fetch(session, url):
        async with session.get(url) as response:
            json_response = await response.json()
            print(json_response['uuid'])
    async def main():
        async with aiohttp.ClientSession() as session:
            tasks = [fetch(session, URL) for _ in range(100)]
            await asyncio.gather(*tasks)
    def func():
        asyncio.run(main())
    
    // https://github.com/nikhilkumarsingh/async-http-requests-tut/blob/master/test_asyncio.py
    b6e20fef-5ad7-49d9-b8ae-84b08e0f2d35
    69d42300-386e-4c49-ad77-747cae9b2316
    1.5898115579998375
    

    Rust协程

    use futures::executor::block_on;
    
    async fn hello_world() {
        println!("hello, world!");
    }
    
    fn main() {
        let future = hello_world(); // Nothing is printed
        block_on(future); // `future` is run and "hello, world!" is printed
    }
    https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html 
    

    进程/线程/协程性能比较

    单进程:28秒

    import requests
    from timer import timer
    URL = 'https://httpbin.org/uuid'
    def fetch(session, url):
        with session.get(url) as response:
            print(response.json()['uuid'])
    @timer(1, 1)
    def main():
        with requests.Session() as session:
            for _ in range(100):
                fetch(session, URL)
    
    

    进程/线程/协程性能比较

    多进程:7秒

    from multiprocessing.pool import Pool
    import requests
    from timer import timer
    URL = 'https://httpbin.org/uuid'
    def fetch(session, url):
        with session.get(url) as response:
            print(response.json()['uuid'])
    @timer(1, 1)
    def main():
        with Pool() as pool:
            with requests.Session() as session:
                pool.starmap(fetch, [(session, URL) for _ in range(100)])
    

    进程/线程/协程性能比较

    线程:4秒

    from concurrent.futures import ThreadPoolExecutor
    import requests
    from timer import timer
    URL = 'https://httpbin.org/uuid'
    def fetch(session, url):
        with session.get(url) as response:
            print(response.json()['uuid'])
    @timer(1, 1)
    def main():
        with ThreadPoolExecutor(max_workers=100) as executor:
            with requests.Session() as session:
                executor.map(fetch, [session] * 100, [URL] * 100)
                executor.shutdown(wait=True)
    

    进程/线程/协程性能比较

    协程:2秒

    ...
    URL = 'https://httpbin.org/uuid'
    async def fetch(session, url):
        async with session.get(url) as response:
            json_response = await response.json()
            print(json_response['uuid'])
    async def main():
        async with aiohttp.ClientSession() as session:
            tasks = [fetch(session, URL) for _ in range(100)]
            await asyncio.gather(*tasks)
    @timer(1, 1)
    def func():
        asyncio.run(main())
    

    Rust线程与协程的示例

    Multi-threaded concurrent webserver

    fn main() {
        let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); // bind listener
        let pool = ThreadPool::new(100); // same number as max concurrent requests
    
        let mut count = 0; // count used to introduce delays
    
        // listen to all incoming request streams
        for stream in listener.incoming() {
            let stream = stream.unwrap();
            count = count + 1;
            pool.execute(move || {
                handle_connection(stream, count); // spawning each connection in a new thread
            });
        }
    }

    Rust线程与协程的示例

    Asynchronous concurrent webserver

    #[async_std::main]
    async fn main() {
        let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap(); // bind listener
        let mut count = 0; // count used to introduce delays
    
        loop {
            count = count + 1;
            // Listen for an incoming connection.
            let (stream, _) = listener.accept().await.unwrap();
            // spawn a new task to handle the connection
            task::spawn(handle_connection(stream, count));
        }
    }

    Rust线程与协程的示例

    fn main() { //Asynchronous multi-threaded concurrent webserver
        let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); // bind listener
    
        let mut pool_builder = ThreadPoolBuilder::new();
        pool_builder.pool_size(100);
        let pool = pool_builder.create().expect("couldn't create threadpool");
        let mut count = 0; // count used to introduce delays
    
        // Listen for an incoming connection.
        for stream in listener.incoming() {
            let stream = stream.unwrap();
            count = count + 1;
            let count_n = Box::new(count);
    
            // spawning each connection in a new thread asynchronously
            pool.spawn_ok(async {
                handle_connection(stream, count_n).await;
            });
        }
    }

    线程/协程性能比较

    width:950px


    提纲

    1. 协程的概念
    2. 协程的实现
    3. 协程示例

    4. 协程与操作系统内核

    bg right:52% 95%


    共享调度器:一种支持优先级的协程调度框架(赵方亮、廖东海)

    • 将协程作为操作系统和应用程序的最小任务单元
    • 引入协程的优先级属性,基于优先级位图,操作系统和应用程序实现协程调度

    Architecture of SharedScheduler

    bg right:60% 95% arch

    1. 操作系统与用户程序各自的 Executor 维护协程
    2. SharedScheduler 通过 vDSO 共享给用户进程
    3. 通过 Global Bitmap 进行操作系统与用户进程之间协调调度

    Coroutine Control Block

    #![allow(unused)]
    fn main() {
    pub struct Coroutine{
     /// Immutable fields
     pub cid: CoroutineId,
     pub kind: CoroutineKind,
     /// Mutable fields
     pub priority: usize,
     pub future: Pin<Box<dyn Future<Output=()> + 'static + Send + Sync>>, 
     pub waker: Arc<Waker>,
    }
    }
    1. future、waker 字段由 Rust 协程特性决定

    2. cid 字段用于标识协程

    3. kind 字段标识协程任务类型,根据类型进行不同处理

    4. priority 字段表示优先级,实现优先级调度的关键


    Coroutine state transition model

    bg right:55% 90% cstate

    根据 CPU 和 stack 占用的情况划分为三类

    • 状态转换
      1. 就绪 <==> 运行
      2. 运行 <==> 运行挂起
      3. 运行 <==> 阻塞
      4. 阻塞 ==> 就绪

    Asynchronous system call

    #![allow(unused)]
    fn main() {
    read!(fd, buffer, cid); // Async call
    read!(fd, buffer); // Sync call
    }
    • 用户态系统调用接口,通过参数区分
    • 内核协程与异步 I/O 机制结合,内核协程完成读取、复制数据操作 width:900px async_syscall

    Throughput and message latency

    width:750px throughput

    1. kcuc: 内核协程 + 用户协程
    2. kcut:内核协程 + 用户线程
    3. ktut:内核线程 + 用户线程
    4. ktuc:内核线程 + 用户协程

    Throughput and message latency

    width:750px latency

    1. SharedScheduler 同步互斥开销,不适用于低并发或低响应要求的场景
    2. 协程切换开销小
    3. SharedScheduler 适用于高并发场景

    Throughput and message latency of different priority connections

    width:700px prio-throughput


    Throughput and message latency of different priority connections

    width:550px prio-latency 结论:在资源有限的条件下,高优先级协程能够得到保证


    小结

    1. 协程的概念
    2. 协程的实现
    3. 协程示例
    4. 协程与操作系统内核

    bg right:40% 100%


    参考文献

    • https://www.youtube.com/watch?v=R4Oz8JUuM4s
    • https://github.com/nikhilkumarsingh/async-http-requests-tut
    • http://www.dabeaz.com/coroutines/
    • https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html
    • https://github.com/nikhilkumarsingh/async-http-requests-tut/blob/master/test_asyncio.py
    • https://gobyexample-cn.github.io/goroutines
    • https://zijiaw.github.io/posts/a7-rsfuture/

    第十一讲 线程与协程

    第三节 支持线程/协程的OS(TCOS)

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 实验目标

    1. 用户态管理的用户线程
    2. 内核态管理的用户线程

    参考:


    实验目标

    bg right:83% 95%


    以往目标

    提高性能、简化开发、加强安全、支持数据持久保存、支持应用的灵活性,支持进程间交互

    • IPC OS:进程间交互
    • Filesystem OS:支持数据持久保存
    • Process OS: 增强进程管理和资源管理
    • Address Space OS: 隔离APP访问的内存地址空间
    • multiprog & time-sharing OS: 让APP共享CPU资源
    • BatchOS: 让APP与OS隔离,加强系统安全,提高执行效率
    • LibOS: 让APP与HW隔离,简化应用访问硬件的难度和复杂性

    进化目标

    提高并发执行效率,支持线程和协程

    • 在进程内实现多个控制流(线程/协程)的执行
    • 在用户态或内核态管理多个控制流(线程/协程)

    同学的进化目标

    • 理解基于任务(Task)的进程/线程/协程抽象
    • 理解进程/线程/协程的实现与运行机制
    • 会写支持线程/协程的OS

    bg right 80%


    提纲

    1. 实验目标

    2. 用户态管理的用户线程

    1. 内核态管理的用户线程

    2.1 实践步骤

    2.2 用户态管理的线程结构 2.3 用户态管理的线程控制接口和实现

    参考:


    如何管理协程/线程/进程?

    • 任务上下文
    • 用户态管理
    • 内核态管理

    bg right:70% 95%


    用户态管理

    bg right:80% 95%


    用户态管理线程的任务控制块

    • 与 Lec4中的任务控制块类似
    • 由用户态的Runtime管理
    struct Task {
        id: usize,
        stack: Vec<u8>,
        ctx: TaskContext,
        state: State,
    }
    

    bg right:50% 95%


    实践步骤

    git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
    cd rCore-Tutorial-v3
    git checkout ch8
    

    包含一个应用程序

    user/src/bin/
    ├──  stackful_coroutine.rs
    

    实践步骤

    执行这个应用程序

    Rust user shell
    >> stackful_coroutine
    stackful_coroutine begin...
    TASK  0(Runtime) STARTING
    TASK  1 STARTING
    task: 1 counter: 0
    TASK 2 STARTING
    task: 2 counter: 0
    TASK 3 STARTING
    task: 3 counter: 0
    TASK 4 STARTING
    task: 4 counter: 0
    ...
    

    提纲

    1. 实验目标
    2. 用户态管理的用户线程
    3. 内核态管理的用户线程

    2.1 实践步骤

    2.2 用户态管理的线程结构

    2.3 用户态管理的线程控制接口和实现

    参考:


    简单的用户态管理多线程应用

    简单的用户态管理多线程应用 stackful_coroutine.rs

    pub fn main()  {
        let mut runtime = Runtime::new(); //创建线程管理子系统
        runtime.init();  // 初始化线程管理子系统
        runtime.spawn(|| {  //创建一个用户态线程
            println!("TASK  1 STARTING");
            let id = 1;
            for i in 0..4 {
                println!("task: {} counter: {}", id, i);
                yield_task();  //主动让出处理器
            }
            println!("TASK 1 FINISHED");
        }); //... 继续创建第2~4个用户态线程
        runtime.run(); //调度执行各个线程
    }

    用户态管理的线程结构与执行状态

    #![allow(unused)]
    fn main() {
    struct Task { //线程控制块
        id: usize,
        stack: Vec<u8>,
        ctx: TaskContext,
        state: State,
    }
    }
    #![allow(unused)]
    fn main() {
    pub struct TaskContext { //线程上下文
        x1: u64,  //ra: return addres 
        x2: u64,  //sp
        ...,  //s[0..11] 寄存器
        nx1: u64, //new return addres
    }
    }

    用户态管理的线程结构与执行状态

    #![allow(unused)]
    fn main() {
    struct Task { //线程控制块
        id: usize,
        stack: Vec<u8>,
        ctx: TaskContext,
        state: State,
    }
    }
    #![allow(unused)]
    fn main() {
    enum State { //线程状态
        Available,
        Running,
        Ready,
    }
    }

    提纲

    1. 实验目标
    2. 用户态管理的用户线程
    3. 内核态管理的用户线程

    2.1 实践步骤 2.2 用户态管理的线程结构

    2.3 用户态管理的线程控制接口和实现

    参考:


    用户态线程管理运行时初始化

    Runtime::new() 主要有三个步骤:

    1. 设置主线程:初始化应用主线程控制块(TID为 0 ),并设置其状态为 Running;
    2. 设置调度队列:初始化线程控制块向量(线程调度队列),加入应用主线程控制块和空闲线程控制块,为后续的线程运行做好准备;
    3. 设置当前运行线程id:设置Runtime 结构变量中的 current 值为0, 表示当前正在运行的线程是应用主线程。

    用户态线程管理运行时初始化

    Runtime::init() 把Rutime结构变量的地址赋值给全局可变变量RUNTIME,以便在后续执行中会根据RUNTIME找到对应的Runtime结构变量。

    在应用的 main() 函数中,首先会依次调用上述两个函数(new和init),完成线程管理运行时的初始化过程。这样正在运行的TID为 0 的主线程就可代表线程运行时进行后续创建线程等一系列工作。


    用户态管理的线程创建

    #![allow(unused)]
    fn main() {
        pub fn spawn(&mut self, f: fn()) { // f函数是线程入口
            let available = self  
                .tasks.iter_mut()  //遍历队列中的任务
                .find(|t| t.state == State::Available) //查找可用的任务
                .expect("no available task.");
            let size = available.stack.len();
            unsafe {
                let s_ptr = available.stack.as_mut_ptr().offset(size as isize);
                let s_ptr = (s_ptr as usize & !7) as *mut u8; // 栈按8字节对齐
                available.ctx.x1 = guard as u64;  //ctx.x1  is old return address
                available.ctx.nx1 = f as u64;     //ctx.nx2 is new return address
                available.ctx.x2 = s_ptr.offset(-32) as u64; //cxt.x2 is sp
            }
            available.state = State::Ready; //设置任务为就绪态
        }
    }
    }

    用户态管理的线程创建

    • 在线程向量中查找一个状态为 Available 的空闲线程控制块
    • 初始化该空闲线程的线程控制块的线程上下文
      • x1寄存器:老的返回地址 -- guard函数地址
      • nx1寄存器:新的返回地址 -- 输入参数 f 函数地址
      • x2 寄存器:新的栈地址 -- available.stack+size

    #![allow(unused)]
    fn main() {
    fn guard() {
        unsafe {
            let rt_ptr = RUNTIME as *mut Runtime;
            (*rt_ptr).t_return();
        };
    }
    fn t_return(&mut self) {
        if self.current != 0 {
            self.tasks[self.current].state = State::Available;
            self.t_yield();
        }
    }
    }

    guard函数意味着传入的f函数(线程的主体)已经返回,线程已完成运行任务,进而取消引用我们的运行时并调用t_return()。


    用户态管理的线程切换

    当应用要切换线程时,会调用 yield_task 函数,通过 runtime.t_yield 函数来完成具体的切换过程。runtime.t_yield() 函数主要完成的功能:

    • 在线程向量中查找一个状态为 Ready 的线程控制块
    • 把当前运行的线程的状态改为Ready,把新就绪线程的状态改为Running,把 runtime 的 current 设置为新就绪线程控制块的id
    • 调用函数 switch ,完成两个线程的栈和上下文的切换;

    用户态管理的线程切换

    #![allow(unused)]
    fn main() {
    fn t_yield(&mut self) -> bool {
            ...
        self.tasks[pos].state = State::Running;
        let old_pos = self.current;
        self.current = pos;
    
        unsafe {
            switch(&mut self.tasks[old_pos].ctx, &self.tasks[pos].ctx);
        }
        ...
    }

    switch 主要完成的工作

    • 完成当前指令指针(PC)的切换;
    • 完成栈指针的切换;
    • 完成通用寄存器集合的切换;

    switch 主要完成的工作

    unsafe fn switch(old: *mut TaskContext, new: *const TaskContext)  {
        // a0: _old, a1: _new
        asm!("
            sd x1, 0x00(a0)
            ...
            sd x1, 0x70(a0)
            ld x1, 0x00(a1)
            ...
            ld t0, 0x70(a1)
            jr t0
        ...
    

    用户态管理的线程执行&调度

    #![allow(unused)]
    fn main() {
        pub fn run(&mut self){
            while self.t_yield() {} 
           println!("All tasks finished!");
        }
    }

    提纲

    1. 实验目标
    2. 用户态管理的用户线程

    3. 内核态管理的用户线程

    3.1 实践步骤

    3.2 内核态管理的线程控制接口 3.3 线程管理与进程管理 3.4 内核态管理的线程的实现

    参考:


    总体思路

    bg right:83% 95%


    如何管理协程/线程/进程?

    • 任务上下文
    • 用户态管理
    • 内核态管理

    bg right:69% 95%


    总体思路

    bg right:83% 95%


    内核态管理的用户线程的线程控制块

    • 与 Lec7中的任务控制块类似
    • 重构:进程中有多个代表线程的任务控制块
    pub struct ProcessControlBlockInner {
        pub tasks: Vec<Option<Arc<TaskControlBlock>>>,
        ...
    }  
    

    bg right:40% 100%


    实践步骤

    git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
    cd rCore-Tutorial-v3
    git checkout ch8
    

    包含几个与内核态管理的用户线程相关的应用程序

    user/src/bin/
    ├──  threads.rs
    ├──  threads_arg.rs
    

    执行threads_arg应用程序

    Rust user shell
    >> threads_arg
    aaa...bbb...ccc...aaa...bbb...ccc...
    thread#1 exited with code 1
    thread#2 exited with code 2
    ccc...thread#3 exited with code 3
    main thread exited.
    ...
    

    提纲

    1. 实验目标
    2. 用户态管理的用户线程
    3. 内核态管理的用户线程

    3.1 实践步骤

    3.2 内核态管理的线程控制接口

    3.3 线程管理与进程管理 3.4 内核态管理的线程的实现

    参考:


    简单的内核态管理多线程应用

    简单的内核态管理多线程应用 threads_arg.rs

    fn thread_print(arg: *const Argument) -> ! { //线程的函数主体
        ...
        exit(arg.rc)
    }
    pub fn main() -> i32 {
        let mut v = Vec::new();
        for arg in args.iter() {
            v.push(thread_create( thread_print, arg ));  //创建线程
        ...
        for tid in v.iter() {
            let exit_code = waittid(*tid as usize); //等待线程结束
        ...
    }

    创建线程系统调用

    进程运行过程中,可创建多个属于这个进程的线程,每个线程有自己的线程标识符(TID,Thread Identifier)。

    系统调用 thread_create 的原型:

    #![allow(unused)]
    fn main() {
    /// 功能:当前进程创建一个新的线程
    /// 参数:entry 表示线程的入口函数地址
    /// 参数:arg:表示线程的一个参数
    pub fn sys_thread_create(entry: usize, arg: usize) -> isize
    }
    • 创建线程不需要建立新的地址空间
    • 属于同一进程中的线程之间没有父子关系

    线程退出系统调用

    线程执行完代表它的功能后,会通过 exit 系统调用退出。进程/主线程调用 waittid 来回收其资源,来彻底销毁整个线程。

    系统调用 waittid 的原型:

    #![allow(unused)]
    fn main() {
    /// 参数:tid表示线程id
    /// 返回值:如果线程不存在,返回-1;如果线程还没退出,返回-2;其他情况下,返回结束线程的退出码
    pub fn sys_waittid(tid: usize) -> i32
    }
    • 进程/主线程通过 waittid 来等待它创建出来的线程(不是主线程)结束并回收它们在内核中的资源
    • 如果进程/主线程先调用了 exit 系统调用来退出,那么整个进程(包括所属的所有线程)都会退出

    提纲

    1. 实验目标
    2. 用户态管理的用户线程
    3. 内核态管理的用户线程

    3.1 实践步骤 3.2 内核态管理的线程控制接口

    3.3 线程管理与进程管理

    3.4 内核态管理的线程的实现

    参考:


    线程管理与进程管理的关系

    引入了线程机制后,进程相关的重要系统调用:fork 、 exec 、 waitpid 接口上没有变化,但完成的功能上需要有一定的扩展

    • 把以前进程中与处理器执行相关的部分拆分到线程中
    • fork 创建进程意味着要单独建立一个主线程来使用处理器,并为以后创建新的线程建立相应的线程控制块向量
    • exec 和 waitpid 改动较少,还是按照与之前进程的处理方式来进行

    进程相关的这三个系统调用还是保持了已有的进程操作的语义,并没有由于引入了线程,而带来大的变化


    fork与多个线程

    问题:“被fork的子进程是否要复制父进程的多个线程?”

    • 选择A:要复制多个线程;
    • 选择B:不复制,只复制当前执行fork的这个线程;
    • 选择C:不支持多线程进程执行fork这种情况

    目前的rcore tutorial ,选择了C,简化了应用的使用场景,即在使用fork和create_thread(以及基于线程的信号量,条件变量等)是不会同时出现的。如果有fork,假定是这个应用是单线程的进程,所以只拷贝了这个单线程的结构。这种简化设计虽然是一种鸵鸟做法,但也避免了一些允许fork和create_thread共存而导致的比较复杂的情况:...


    fork与多个线程

    场景:在fork前,有三个线程Main thread, thread X, thread Y, 且Thread X拿到一个lock,在临界区中执行;Thread Y正在写一个文件。Main thread执行fork.

    • 选择A:会出现子进程的Thread Y和 父进程的Thread Y都在写一个文件的情况。
    • 选择B,则子进程中只有Main Thread,当它想得到Thread X的那个lock时,这个lock是得不到的(因为Thread X 在子进程中不存在,没法释放锁),会陷入到持续忙等中。

    提纲

    1. 实验目标
    2. 用户态管理的用户线程
    3. 内核态管理的用户线程

    3.1 实践步骤 3.2 内核态管理的线程控制接口 3.3 线程管理与进程管理

    3.4 内核态管理的线程的实现

    参考:


    线程管理数据结构

    改进现有进程管理的一些数据结构包含的内容及接口,把进程中与处理器相关的部分分拆出来,形成线程相关的部分。

    • 任务控制块 TaskControlBlock :表示线程的核心数据结构
    • 任务管理器 TaskManager :管理线程集合的核心数据结构
    • 处理器管理结构 Processor :用于线程调度,维护线程的处理器状态

    线程控制块

    #![allow(unused)]
    fn main() {
    pub struct TaskControlBlock {
        pub process: Weak<ProcessControlBlock>, //线程所属的进程控制块
        pub kstack: KernelStack,//任务(线程)的内核栈
        inner: UPSafeCell<TaskControlBlockInner>,
    }
    pub struct TaskControlBlockInner {
        pub res: Option<TaskUserRes>,  //任务(线程)用户态资源
        pub trap_cx_ppn: PhysPageNum,//trap上下文地址
        pub task_cx: TaskContext,//任务(线程)上下文
        pub task_status: TaskStatus,//任务(线程)状态
        pub exit_code: Option<i32>,//任务(线程)退出码
    }
    }

    进程控制块

    #![allow(unused)]
    fn main() {
    pub struct ProcessControlBlock {
        pub pid: PidHandle,
        inner: UPSafeCell<ProcessControlBlockInner>,
    }
    pub struct ProcessControlBlockInner {
        pub tasks: Vec<Option<Arc<TaskControlBlock>>>,
        pub task_res_allocator: RecycleAllocator,
        ...
    }
    }

    RecycleAllocator是PidAllocator的升级版,即一个相对通用的资源分配器,可用于分配进程标识符(PID)和线程的内核栈(KernelStack)。


    线程创建sys_thread_create

    当一个进程执行中发出系统调用 sys_thread_create 后,操作系统就需要在当前进程的基础上创建一个线程,即在线程控制块中初始化各个成员变量,建立好进程和线程的关系等,关键要素包括:

    • 线程的用户态栈:确保在用户态的线程能正常执行函数调用
    • 线程的内核态栈:确保线程陷入内核后能正常执行函数调用
    • 线程的跳板页:确保线程能正确的进行用户态<–>内核态切换
    • 线程上下文:即线程用到的寄存器信息,用于线程切换

    线程创建sys_thread_create

    #![allow(unused)]
    fn main() {
    pub fn sys_thread_create(entry: usize, arg: usize) -> isize {
        // create a new thread
        let new_task = Arc::new(TaskControlBlock::new(...
        // add new task to scheduler
        add_task(Arc::clone(&new_task));
        // add new thread to current process
        let tasks = &mut process_inner.tasks;
        tasks[new_task_tid] = Some(Arc::clone(&new_task));
        *new_task_trap_cx = TrapContext::app_init_context( //建立trap/task上下文
            entry,
            new_task_res.ustack_top(),
            kernel_token(),
        ... 
    }

    线程退出sys_exit

    • 当一个非主线程的其他线程发出 sys_exit 系统调用时,内核会调用 exit_current_and_run_next 函数退出当前线程并切换到下一个线程,但不会导致其所属进程的退出。
    • 当主线程 即进程发出这个系统调用,当内核收到这个系统调用后,会回收整个进程(这包括了其管理的所有线程)资源,并退出。

    线程退出sys_exit

    #![allow(unused)]
    fn main() {
    pub fn sys_exit(exit_code: i32) -> ! {
        exit_current_and_run_next(exit_code); ...
    pub fn exit_current_and_run_next(exit_code: i32) { 
        let task = take_current_task().unwrap();
        let mut task_inner = task.inner_exclusive_access();   
        drop(task_inner); //释放线程资源
        drop(task);  //释放线程控制块
         if tid == 0 {
            // 释放当前进程的所有线程资源
            // 释放当前进程的资源
    ...
    }

    等待线程结束sys_waittid

    • 如果找到 tid 对应的线程,则尝试收集该线程的退出码 exit_tid ,否则返回错误(退出线程不存在)。
    • 如果退出码存在(意味该线程确实退出了),则清空进程中对应此线程的线程控制块(至此,线程所占资源算是全部清空了),否则返回错误(线程还没退出)。

    等待线程结束sys_waittid

    #![allow(unused)]
    fn main() {
    pub fn sys_waittid(tid: usize) -> i32 {
        ...
        if let Some(waited_task) = waited_task {
            if let Some(waited_exit_code) = waited_task.....exit_code {
                exit_code = Some(waited_exit_code);
            }
        } else {
            return -1; // waited thread does not exist
        }
        if let Some(exit_code) = exit_code {
            process_inner.tasks[tid] = None; //dealloc the exited thread
            exit_code
        } else {
            -2 // waited thread has not exited
        }
    }

    线程执行中的特权级切换和调度切换

    • 线程执行中的特权级切换与第四讲中介绍的任务切换的设计与实现是一致的
    • 线程执行中的调度切换过程与第七讲中介绍的进程调度机制是一致的

    小结

    • 用户态管理的用户线程
    • 内核态管理的用户线程
    • 能写达科塔盗龙OS

    bg right 70%

    第十二讲 同步与互斥

    第一节 概述

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 背景

    1. 现实生活中的同步互斥
    2. 临界区
    3. 同步互斥的方法

    背景

    • 独立进/线程

      • 不和其他进/线程共享资源或状态
      • 确定性 => 输入状态决定结果
      • 可重现 => 能够重现起始条件
      • 调度顺序不重要
    • 进/线程如果有资源共享

      • 存在不确定性
      • 存在不可重现
      • 可能出现难以重现的错误

    bg right:40% 90%


    背景

    • 有资源共享的进/线程执行fork时的可能错误

    w:900


    原子操作(Atomic Operation)

    • 原子操作是指一次不存在任何中断或失败的操作
      • 要么操作成功完成
      • 或者操作没有执行
      • 不会出现部分执行的状态

    操作系统需要利用同步机制在并发执行的同时,保证一些操作是原子操作 bg right:50% 100%


    提纲

    1. 背景

    2. 现实生活中的同步互斥

    1. 临界区
    2. 同步互斥的方法

    现实生活中的同步互斥

    例如: 家庭采购协调 (利用现实生活问题帮助理解操作系统同步问题)

    • 注意,计算机与人的差异 w:900

    现实生活中的同步互斥

    • 如何保证家庭采购协调的成功和高效
      • 需要采购时,有人去买面包
      • 最多只有一个人去买面包
    • 可能的解决方法
      • 在冰箱上设置一个锁和钥匙( lock&key)
      • 去买面包之前锁住冰箱并且拿走钥匙
    • 加锁导致的新问题
      • 冰箱中还有其他食品时,别人无法取到 bg right:30% 95%

    方案一

    • 使用便签来避免购买太多面包
      • 购买之前留下一张便签
      • 买完后移除该便签
      • 别人看到便签时,就不去购买面包
     if (nobread) {
        if (noNote) {
            leave Note;
            buy bread;
            remove Note;
        }
    }
    

    方案一的分析

    • 偶尔会购买太多面包 - 重复

      • 检查面包和便签后帖便签前,有其他人检查面包和便签
    • 解决方案只是间歇性地失败

      • 问题难以调试
      • 必须考虑调度器所做事情

    bg right:50% 100%


    方案二

    • 先留便签,后查面包和便签
    leave Note;
    if (nobread) {  
      if (noNote) { 
           buy bread;
        }
    }
    remove note;
    
    • 会发生什么?
      • 不会有人买面包

    bg right:50% 100%


    方案三

    • 为便签增加标记,以区别不同人的便签
      • 现在可在检查之前留便签
    // 进程A
    leave note_2;
    if (no note_1) {
       if (no bread) { 
         buy bread; 
       } 
    } 
    remove note_2;	
    

    bg right:50% 100%


    方案三

    • 为便签增加标记,以区别不同人的便签
      • 现在可在检查之前留便签
    // 进程B
    leave note_1;
    if (no note_2) {
       if (no bread) { 
         buy bread; 
       } 
    } 
    remove note_1;
    

    bg right:50% 100%


    方案三

    • 为便签增加标记,以区别不同人的便签
      • 现在可在检查之前留便签
    • 会发生什么?
      • 可能导致没有人去买面包
      • 每个人都认为另外一个去买面包

    bg right:50% 100%


    方案四

    两个人采用不同的处理流程

    w:1000


    方案四

    两个人采用不同的处理流程

    • 现在有效吗?
      • 它有效,但太复杂
    • A和B的代码不同
      • 如果线程更多,怎么办?
    • 当A等待时,不能做其他事
      • 忙等待(busy-waiting)

    bg right:50% 100%


    方案五

    • 利用两个原子操作实现一个锁(lock)
      • Lock.Acquire()
        • 在锁被释放前一直等待,然后获得锁
        • 如果两个线程都在等待同一个锁,并且同时发现锁被释放了,那么只有一个能够获得锁
      • Lock.Release()
        • 解锁并唤醒任何等待中的线程

    bg right:32% 100%


    提纲

    1. 背景
    2. 现实生活中的同步互斥

    3. 临界区

    1. 同步互斥的方法

    临界区(Critical Section)

    entry section
       critical section
    exit section
       remainder section
    
    • 进入区(entry section)
      • 检查可否进入临界区的一段代码
      • 如可进入,设置相应"正在访问临界区"标志
    • 临界区(critical section)
      • 线程中访问临界资源的一段需要互斥执行的代码

    临界区(Critical Section)

    entry section
       critical section
    exit section
       remainder section
    
    • 退出区(exit section)
      • 清除“正在访问临界区”标志
    • 剩余区(remainder section)
      • 代码中的其余部分

    临界区访问规则

    entry section
       critical section
    exit section
       remainder section
    
    1. 空闲则入:没有线程在临界区时,任何线程可进入
    2. 忙则等待:有线程在临界区时,其他线程均不能进入临界区
    3. 有限等待:等待进入临界区的线程不能无限期等待
    4. 让权等待(可选):不能进入临界区的线程,应释放CPU(如转换到阻塞状态)

    提纲

    1. 背景
    2. 现实生活中的同步互斥
    3. 临界区

    4. 同步互斥的方法

    4.1 禁用硬件中断

    4.2 基于软件的解决方法 4.3 更高级的抽象方法


    禁用硬件中断

    • 没有中断,没有上下文切换,因此没有并发
      • 硬件将中断处理延迟到中断被启用之后

      • 现代计算机体系结构都提供指令来实现禁用中断

         local_irq_save(unsigned long flags); 
             critical section
         local_irq_restore(unsigned long flags); 
        
    • 进入临界区:禁止所有中断,并保存标志
    • 离开临界区:使能所有中断,并恢复标志

    禁用硬件中断

    • 缺点
      • 禁用中断后,线程无法被停止
        • 整个系统都会为此停下来
        • 可能导致其他线程处于饥饿状态
      • 临界区可能很长
        • 无法确定响应中断所需的时间(可能存在硬件影响)
      • 不适合多核
    • 要小心使用

    提纲

    1. 背景
    2. 现实生活中的同步互斥
    3. 临界区
    4. 同步互斥的方法

    4.1 禁用硬件中断

    4.2 基于软件的解决方法

    4.3 更高级的抽象方法


    基于软件的解决方法

    w:900


    尝试一

    bg right:50% 100%

    • 满足“忙则等待”,但是有时不满足“空闲则入”
      • Ti不在临界区,Tj想要继续运行,但是必须等待Ti进入过临界区后
      • turn = 0;
        • T0 不需要访问临界区
        • T1 需要访问,但没有轮到,只能一直等待

    尝试二

    bg right:50% 100%

    • 互相依赖(线程忙等)
    • 不满足“忙则等待”
      • flag[i]=flag[j]=0
    // 线程 Tj
    do {
       while (flag[i] == 1) ;
       flag[j] = 1;
       critical section
       flag[j] = 0;
       remainder section
    } while(1)
    

    尝试三

    bg right:50% 100%

    • 满足“忙则等待”,但是不满足“空闲则入”
      • flag[i]=flag[j]=1
    // 线程 Tj
    do {
       flag[j] = 1;
       while (flag[i] == 1) ;
       critical section
       flag[j] = 0;
       remainder section
    } while(1)
    

    Peterson算法

    bg right:50% 100%

    • 满足线程Ti和Tj之间互斥的经典的基于软件的解决方法(1981年)
    • 孔融让梨

    Peterson算法

    // 共享变量
    let mut flag = [false; N]; // 标识进程是否请求进入临界区
    let mut turn = 0; // 记录应该让哪个进程进入临界区
    // 进程P0
    while (true) {
        flag[0] = true;
        turn = 1;
        while (flag[1] == true && turn == 1) ;
        // 进入临界区执行任务
        // 退出临界区
        flag[0] = false;
    }
    // 进程P1
    while (true) {
        flag[1] = true;
        turn = 0;
        while (flag[0] == true && turn == 0) ;
        // 进入临界区执行任务
        // 退出临界区
        flag[1] = false;
    }
    

    Peterson算法

    bg right:50% 100%

    //进程Pi
    flag[i] = True;
    turn = j;
    while(flag[j] && turn == j);
    critical section;
    flag[i] = False;
    remainder section;
    //进程Pj
    flag[j] = True;
    turn = i;
    while(flag[i] && turn == i);
    critical section;
    flag[j] = False;
    remainder section;
    

    Dekkers算法

    bg right:35% 100%

    do{
      flag[0] = true;// 首先P0举手示意我要访问
      while(flag[1]) {// 看看P1是否也举手了
         if(turn==1){// 如果P1也举手了,那么就看看到底轮到谁
             flag[0]=false;// 如果确实轮到P1,那么P0先把手放下(让P1先)
             while(turn==1);// 只要还是P1的时间,P0就不举手,一直等
             flag[0]=true;// 等到P1用完了(轮到P0了),P0再举手
         }
      }
      critical section;// 访问临界区
      turn = 1;// P0访问完了,把轮次交给P1,让P1可以访问
      flag[0]=false;// P0放下手
      remainder section;
    } while(true);
    

    Dekkers算法

    w:400 vs w:400


    N线程

    Eisenberg和McGuire

    • 一个共享的turn变量,若干线程排成一个环
    • 每个环有个flag标志,想要进入临界区填写flag标志
    • 有多个想进入临界区,从前往后走,执行完一个线程,turn改为下一个线程的值。 bg right:50% 100%

    N线程

    INITIALIZATION:
    
    enum states flags[n -1]; //{IDLE, WAITING, ACTIVE}
    int turn;
    for (index=0; index<n; index++) {
       flags[index] = IDLE;
    }
    

    N线程

    ENTRY PROTOCOL (for Process i ):
    repeat {//从turn到i是否存在请求进程:若存在,则不断循环,直至不存在这样的进程,将当前进程标记为ACTIVE
       flags[i] = WAITING;//表明自己需要资源
       index = turn;//轮到谁了
       while (index != i) {//从turn到i轮流找不idle的线程
          if (flag[index] != IDLE) index = turn;//turn到i有非idle的阻塞
          else index = (index+1) mod n; //否则轮到i,并跳出
       }
       flags[i] = ACTIVE;//Pi active; 其他线程有可能active
       //对所有ACTIVE的进程做进一步的判断,判断除了当前进程以外,是否还存在其他ACTIVE的进程
       index = 0;//看看是否还有其他active的
       while ((index < n) && ((index == i) || (flags[index] != ACTIVE))) {
          index = index+1;
       }//如果后面没有active了,并且轮到Pi或者turn idle, 就轮到i;否则继续循环
    } until ((index >= n) && ((turn == i) || (flags[turn] == IDLE)));
    turn = i;//获得turn并处理
    

    N线程

    EXIT PROTOCOL (for Process i ):
    
    index = turn+1 mod n;//找到一个不idle的
    while (flags[index] == IDLE) {
       index = index+1 mod n;
    }
    turn = index;//找到不idle的设置为turn;或者设置为自己
    flag[i] = IDLE;//结束,自己变idle
    

    提纲

    1. 背景
    2. 现实生活中的同步互斥
    3. 临界区
    4. 同步互斥的方法

    4.1 禁用硬件中断 4.2 基于软件的解决方法

    4.3 更高级的抽象方法


    方法3:更高级的抽象方法

    • 基于软件的解决方法

      • 复杂,需要忙等待
    • 更高级的抽象方法

      • 硬件提供了一些同步原语
        • 中断禁用,原子操作指令等
      • 操作系统提供更高级的编程抽象来简化线程同步
        • 例如:锁、信号量
        • 用硬件原语来构建

    锁(lock)

    • 锁是一个抽象的数据结构
      • 一个二进制变量(锁定/解锁)
      • 使用锁来控制临界区访问
      • Lock::Acquire()
        • 锁被释放前一直等待,后得到锁
      • Lock::Release()
        • 释放锁,唤醒任何等待的线程 bg right:40% 100%

    锁(lock)

    现代CPU提供一些特殊的原子操作指令

    • 原子操作指令
      • 测试和置位(Test-and-Set )指令
        • 从内存单元中读取值
        • 测试该值是否为1(然后返回真或假)
        • 内存单元值设置为1
          • 输入0,改成1,返回0;
          • 输入1,保持1,返回1;

    bg right:35% 100%


    锁(lock)

    现代CPU都提供一些特殊的原子操作指令

    do {
      while(TestAndSet(&lock)) ;
      critical section; 
      lock = false;
      remainder section;
    } while (true)
    

    bg right:35% 100%


    锁(lock)

    现代CPU都提供一些特殊的原子操作指令

    do {
      while(TestAndSet(&lock)) ;
      critical section; 
      lock = false;
      remainder section;
    } while (true)
    
    lock(): while(TestAndSet(&lock));
    critical section; 
    unlock(): lock=false;
    

    bg right:35% 100%


    锁(lock)

    • 原子操作:交换指令CaS(Compare and Swap)
    bool compare_and_swap(int *value, int old, int new) {
       if(*value==old) {
          *value = new; 
          return true; }
       return false;
    }
    
    int lock = 0;                           // 初始时锁空闲 
    while(!compare_and_swap(&lock,0,1));    // lock 加锁
    critical section; 
    lock=0;                                 // unlock 解锁
    remainder section;
    

    锁(lock)

    • 原子操作:交换指令CaS(Compare and Swap)
    • ABA 问题:
      • value= 100;
      • Thread1: value - 50; //成功 value=50
      • Thread2: value - 50; //阻塞
      • Thread3: value + 50; //成功 value=50
      • Thread2: 重试成功
    • 解决思路:加上版本号(时间戳)
      • (100,1); (50,2); (100,3)

    锁(lock)

    使用TaS指令实现自旋锁(spinlock)

    • 线程在等待的时候消耗CPU时间 w:800

    锁(lock)

    忙等锁 v.s. 等待锁 w:900


    锁(lock)

    • 优点
      • 适用于单处理器或者共享主存的多处理器中任意数量的线程同步
      • 简单并且容易证明
      • 支持多临界区
    • 缺点
      • 忙等待消耗处理器时间
      • 可能导致饥饿
        • 线程离开临界区时有多个等待线程的情况
      • 可能死锁:线程间相互等待,无法继续执行

    小结

    • 常用的三种同步实现方法
      • 禁用中断(仅限于单处理器)
      • 软件方法(复杂)
      • 锁是一种高级的同步抽象方法
        • 硬件原子操作指令(单处理器或多处理器均可)
        • 互斥可以使用锁来实现

    第十二讲 同步与互斥

    第二节 信号量

    向勇 陈渝 李国良 任炬

    2023年秋季


    信号量(semaphore)

    • 信号量是操作系统提供的一种协调共享资源访问的方法
    • Dijkstra在20世纪60年代提出
    • 早期的操作系统的主要同步机制 w:700

    信号量(semaphore)

    • 信号量是一种抽象数据类型,由一个整型 (sem)变量和两个原子操作组成
      • P():Prolaag 荷兰语:尝试减少
        • $sem = sem - 1$
        • 如sem<0, 进入等待, 否则继续
      • V():Verhoog 荷兰语:增加
        • $sem = sem + 1$
        • 如$sem \le 0$, 唤醒一个等待的 bg right:40% 100%

    信号量(semaphore)

    • 信号量是被保护的整数变量
      • 初始化完成后,只能通过P()和V()操作修改
      • 操作系统保证,PV操作是原子操作
    • P() 可能阻塞,V()不会阻塞
    • 通常假定信号量是“公平的”
      • 线程不会被无限期阻塞在P()操作
      • 假定信号量等待按先进先出排队

    自旋锁能否实现先进先出?

    bg right:30% 100%


    信号量在概念上的实现

    w:1200


    信号量的分类和使用

    • 信号量的分类
      • 二进制信号量:资源数目为0或1
      • 计数信号量:资源数目为任何非负值
    • 信号量的使用
      • 互斥访问
      • 条件同步

    bg right:35% 95%


    互斥访问举例

    • 每个临界区设置一个初值为1的信号量
    • 成对使用P()操作和V()操作
      • P()操作保证互斥访问资源
      • V()操作在使用后释放资源
      • PV操作次序不能错误、重复或遗漏

    bg right:50% 100%


    条件同步举例

    • 每个条件同步设置一个信号量,其初值为0

    bg right:65% 100%


    生产者-消费者问题

    • 有界缓冲区的生产者-消费者问题描述
      • 一个或多个生产者在生成数据后放在一个缓冲区里
      • 单个消费者从缓冲区取出数据处理
      • 任何时刻只能有一个生产者或消费者可访问缓冲区 w:700

    生产者-消费者问题

    • 问题分析
      • 任何时刻只能有一个线程操作缓冲区(互斥访问)
      • 缓冲区空时,消费者必须等待生产者(条件同步)
      • 缓冲区满时,生产者必须等待消费者(条件同步)
    • 用信号量描述每个约束
      • 二进制信号量mutex
      • 计数信号量fullBuffers
      • 计数信号量emptyBuffers

    生产者-消费者问题

    w:900px P、V操作的顺序有影响吗?


    生产者-消费者问题

    • 读/开发代码比较困难
    • 容易出错
      • 使用已被占用的信号量
      • 忘记释放信号量
      • 不能够避免死锁问题
      • 对程序员要求较高 bg right:50% 100%

    第十二讲 同步与互斥

    第三节 管程与条件变量

    管程(Monitor)、条件变量(Condition Variable)

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 管程与条件变量

    1. 管程实现方式
    2. 条件变量的实现
    3. 生产者-消费者问题的管程实现

    为什么引入管程?

    传统PV和锁机制的可读性差

    • 程序可读性差:要了解对于一组共享变量及信号量的操作是否正确,则必须通读整个系统或者并发程序
    • 程序不利于修改和维护:程序局部性很差,所以任一组变量或一段代码的修改都可能影响全局
    • 正确性难以保证:操作系统或并发程序通常很大,很难保证一个复杂的系统没有逻辑错误。
    • 容易发生死锁:如果不使用好P、V操作时,逻辑上发生错误,很有可能会导致死锁。

    管程

    • 管程是一种用于多线程互斥访问共享资源的程序结构
    • 采用面向对象方法,简化了线程间的同步控制
    • 任一时刻最多只有一个线程执行管程代码
    • 正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复 w:600

    管程

    • 模块化,一个管程是一个基本程序单位,可以单独编译

    • 抽象数据类型,管程是一种特殊的数据类型,其中不仅有数据,而且有对数据进行操作的代码

    • 信息隐蔽,管程是半透明的,管程中的过程(函数)实现了某些功能,在其外部则是不可见的

    bg right:45% 100%


    管程的外部特征

    管程中的共享变量在管程外部是不可见的,外部只能通过调用管程中所说明的外部过程 (函数)来间接地访问管程中的共享变量

    • 互斥:任一时刻管程中只能有一个活跃进程,通过锁竞争进入管程
    • 等待:进入管程的线程因资源被占用而进入等待状态
      • 每个条件变量表示一种等待原因,对应一个等待队列
      • 入口队列管理未进入管程的线程/进程
    • 唤醒:管程中等待的线程可以在其他线程释放资源时被唤醒
    • 管程操作:进入enter, 离开leave, 等待wait, 唤醒signal

    条件变量

    • 条件变量是多线程编程中的一种同步机制,用于线程间通信和协调
      • 它是一个结构体,包含了一个等待队列和一些基本操作函数
      • 通过条件变量实现线程的阻塞、唤醒和通讯等功能
    • 条件变量与互斥锁(mutex)配合,可实现线程间的同步和互斥
      • 共享资源被占用时,线程可通过条件变量挂起自己
      • 其他线程释放该资源后,继续执行

    条件变量的主要操作

    • 初始化:通过pthread_cond_init()函数初始化一个条件变量
    • 销毁:通过pthread_cond_destroy()函数销毁一个条件变量
    • 等待:通过pthread_cond_wait()函数在条件变量上等待,线程会自动解锁互斥锁并进入等待状态,直到被唤醒
    • 唤醒:通过pthread_cond_signal()pthread_cond_broadcast()函数唤醒一个或多个等待在条件变量上的线程

    条件变量的使用步骤

    • 创建条件变量和互斥锁,并初始化它们
    • 线程在持有互斥锁时,可修改或访问共享资源
    • 当共享资源被其他线程占用时,当前线程阻塞自己,并让出互斥锁,等待条件变量唤醒
    • 线程释放共享资源后,可通过条件变量唤醒等待在条件变量上的线程,让它们重新尝试获取共享资源并执行相应的操作

    管程的组成

    一个由过程(函数)、变量及数据结构等组成的一个集合

    • 一个锁:控制管程代码的互斥访问
    • 0或者多个条件变量: 管理共享数据的并发访问,每个条件变量有个等待(紧急)队列
    • 入口等待队列
    • 紧急等待队列
    • 条件变量队列

    bg right:40% 100%


    管程的队列

    • 入口等待队列:管程入口处等待队列
    • 条件等待队列:某个条件变量的等待队列(为资源占用而等待)
    • 紧急等待队列:唤醒使用的紧急队列
      • 当T1线程执行唤醒操作而唤醒T2,如果T1把访问权限交给T2,T1被挂起;T1放入紧急等待队列
      • 紧急等待队列优先级高于条件变量等待队列

    bg right:40% 100%


    管程操作

    • T.enter过程:线程T在进入管程之前要获得互斥访问权(lock)
    • T.leave过程:当线程T离开管程时,如果紧急队列不为空,唤醒紧急队列中的线程,并将T所持锁赋予唤醒的线程;如果紧急队列为空,释放lock,唤醒入口等待队列某个线程
    • T.wait(c):1)阻塞线程T自己,将T自己挂到条件变量c的等待队列;
      • 2)释放所持锁; 3)唤醒入口等待队列的一个或者多个线程;
    • T.signal(c):1)把条件变量c的等待队列某个线程唤醒;
      • 2)把线程T所持lock给被唤醒的线程;
      • 3)把线程T自己挂在紧急等待队列

    管程操作

    w:1000


    提纲

    1. 管程与条件变量

    2. 管程实现方式

    1. 条件变量的实现
    2. 生产者-消费者问题的管程实现

    管程实现方式

    如果线程T1因条件A未满足处于阻塞状态,那么当线程T2让条件A满足并执行signal操作唤醒T1后,不允许线程T1和T2同时处于管程中,那么如何确定哪个执行/哪个等待?

    • 管程中条件变量的释放处理方式:
      • Hoare管程:T1执行/T2等待,直至T1离开管程,然后T2继续执行
      • MESA/Hansen管程:T2执行/T1等待,直至T2离开管程,然后T1可能继续执行

    管程中条件变量的释放处理方式

    • 线程 T2 的signal,使线程 T1 等待的条件满足时
      • Hoare:T2 通知完 T1后,T2 阻塞,T1 马上执行;等 T1 执行完,再唤醒 T2 执行
      • Hansen: T2 通知完 T1 后,T2 还会接着执行,T2 执行结束后(规定:最后操作是signal),然后 T1 再执行(将锁直接给T1)
      • MESA:T2 通知完 T1 后,T2 还会接着执行,T1 并不会立即执行,而是重新竞争访问权限

    管程中条件变量的释放处理方式

    w:1200


    w:1150


    管程中条件变量的释放处理方式

    唤醒一个线程的两种选择:直接赋予锁 vs 重新公平竞争锁

    w:1200


    Hoare管程

    1. T1 进入管程monitor
    2. T1 等待资源 (进入等待队列wait queue)
    3. T2 进入管程monitor
    4. T2 资源可用 ,通知T1恢复执行,
       并把自己转移到紧急等待队列
    5. T1 重新进入管程monitor并执行
    6. T1 离开monitor
    7. T2 重新进入管程monitor并执行
    8. T2 离开管程monitor
    9. 其他在entry queue中的线程通过竞争
       进入管程monitor
    

    bg right:35% 100%


    Mesa管程

    - 1. T1 进入管程monitor
    - 2. T1 等待资源 
        (进入wait queue,并释放monitor)
    - 3. T2 进入monitor
    - 4. T2 资源可用,通知T1
        (T1被转移到entey queue,重新平等竞争)
    - 5. T2 继续执行
    - 6. T2 离开monitor
    - 7. T1 获得执行机会,从entry queue
         出队列,恢复执行
    - 8. T1 离开monitor
    - 9. 其他在entry queue中的线程通过竞争
         进入monitor
    

    bg right:35% 100%


    Hansen管程

    - 1. T1 进入管程monitor
    - 2. T1 等待资源c
    - 3. T2 进入monitor
    - 4. T2 离开Monitor,并给通知等待
         资源c的线程,资源可用
    - 5. T1 重新进入 monitor
    - 6. T1 离开monitor
    - 7. 其他线程从entry queue中通过竞争
         进入monitor
    

    bg right:35% 100%


    提纲

    1. 管程与条件变量
    2. 管程实现方式

    3. 条件变量的实现

    1. 生产者-消费者问题的管程实现

    条件变量的实现

    w:1000


    条件变量的实现

    w:1000


    条件变量的实现

    w:1000


    条件变量的实现

    w:1000


    条件变量的实现

    w:1000


    条件变量的实现

    w:1000


    条件变量的实现

    w:1000


    条件变量的实现

    w:1000


    条件变量的实现

    w:1000


    提纲

    1. 管程与条件变量
    2. 管程实现方式
    3. 条件变量的实现

    4. 生产者-消费者问题的管程实现


    生产者-消费者问题的管程实现

    w:1000


    生产者-消费者问题的管程实现

    w:1000


    生产者-消费者问题的管程实现

    w:1000


    生产者-消费者问题的管程实现

    w:1000

    第十二讲 同步与互斥

    第四节 同步互斥实例问题

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 哲学家就餐问题

    1. 读者-写者问题

    哲学家就餐问题

    • 5个哲学家围绕一张圆桌而坐
    • 桌子上放着5支叉子
    • 每两个哲学家之间放一支叉子
    • 哲学家的动作包括思考和进餐
    • 进餐时需同时用左右两边的叉子
    • 思考时将两支叉子放回原处

    如何保证哲学家们的动作有序进行? 如:不出现有人永远拿不到叉子

    bg right:40% 100%


    方案1

    w:900

    不正确,可能导致死锁


    方案2

    w:900


    方案2

    w:900


    方案2

    w:900


    方案2

    w:900


    方案2

    w:800 互斥访问正确,但每次只允许一人进餐


    方案3

    w:800


    方案3

    w:800


    方案3

    w:800


    方案3

    w:800


    方案3

    w:800


    方案3

    w:750 没有死锁,可有多人同时就餐


    方案4

    w:900


    方案4

    AND型信号量集是指同时需要多个资源且每种占用一个资源时的信号量操作。

    当一段代码需要同时获取两个或多个临界资源时,就可能出现由于各线程分别获得部分临界资源并等待其余的临界资源的局面。各线程都会“各不相让”,从而出现死锁。

    解决这个问题的一个基本思路是:在一个原语中申请整段代码需要的多个临界资源,要么全部分配给它,要么一个都不分配给它。这就是AND型信号量集的基本思想。


    方案4

    AND型信号量集

    P(S1, S2, …, Sn)
    {
        While(TRUE)
        {
            if (S1 >=1 and … and Sn>=1 ){
                for( i=1 ;i<=n; i++) Si--;
            break;
            }
            else{
                 Place the thread in the waiting queue associated  with the first Si 
                 found with Si < 1   
            }
        }
    }
    

    方案4

    AND型信号量集

    V(S1, S2, …, Sn){ 
        for (i=1; i<=n; i++) {
                Si++ ;
                Remove all the thread waiting in the queue associated with Si into 
                the ready queue                
         }
    }
    

    方案5

    w:1000


    方案5

    w:1000


    方案5

    w:1000


    方案5

    方案5不仅没有死锁,而且对于任意位哲学家的情况都能获得最大的并行度。算法中使用一个数组state跟踪每一个哲学家是在进餐、思考还是饥饿状态(正在试图拿叉子)。一个哲学家只有在两个邻居都没有进餐时才允许进入到进餐状态。

    每个线程将函数philosopher作为主代码运行,而其他函数take_forksput_forkstest只是普通的函数,而非单独的线程。

    bg right:40% 100%


    提纲

    1. 哲学家就餐问题

    2. 读者-写者问题

    2.1 读者-写者问题描述

    2.2 读者-写者问题的信号量实现 2.3 读者-写者问题的管程实现


    读者-写者问题

    • 共享数据的两类使用者
      • 读者:只读不修改数据
      • 写者:读取和修改数据
    • 对共享数据的读写
      • 多个:“读-读”-- 允许
      • 单个:“读-写”-- 互斥
      • 单个:“写-写”-- 互斥

    bg right:50% 100%


    读者-写者问题

    • 读者优先策略
      • 只要有读者正在读状态,后来的读者都能直接进入
      • 如读者持续不断进入,则写者就处于饥饿
    • 写者优先策略
      • 只要有写者就绪,写者应尽快执行写操作
      • 如写者持续不断就绪,则读者就处于饥饿

    bg right:40% 100%


    提纲

    1. 哲学家就餐问题
    2. 读者-写者问题

    2.1 读者-写者问题描述

    2.2 读者-写者问题的信号量实现

    2.3 读者-写者问题的管程实现


    读者-写者问题的信号量实现方案

    用信号量描述每个约束

    • 信号量WriteMutex:控制读写操作的互斥,初始化为1
    • 读者计数Rcount :正在进行读操作的读者数目,初始化为0
    • 信号量CountMutex:控制对读者计数的互斥修改,初始化为1 bg right:40% 100%

    读者-写者问题的信号量实现

    w:800


    读者-写者问题的信号量实现

    w:800


    读者-写者问题的信号量实现

    w:800


    读者-写者问题的信号量实现

    w:800


    读者-写者问题的信号量实现

    w:800


    读者-写者问题的信号量实现

    w:700 此实现中,读者优先


    提纲

    1. 哲学家就餐问题
    2. 读者-写者问题

    2.1 读者-写者问题描述 2.2 读者-写者问题的信号量实现

    2.3 读者-写者问题的管程实现


    管程的状态变量

    w:900


    管程的状态变量

    w:900


    读者实现

    w:900


    读者实现

    w:900


    读者实现

    w:900


    读者实现

    w:900


    读者实现

    w:900


    读者实现

    w:900


    读者实现

    w:900


    读者实现

    w:900


    写者实现

    w:900


    写者实现

    w:900


    写者实现

    w:900


    写者实现

    w:900


    写者实现

    w:900


    写者实现

    w:900

    第十二讲 同步与互斥

    第五节 死锁

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 死锁问题

    1. 死锁处理办法

    死锁问题

    w:700

    • 桥梁只能单向通行
    • 桥的每个部分可视为一个资源
    • 可能出现死锁
      • 对向行驶车辆在桥上相遇
      • 解决方法:一个方向的车辆倒退(资源抢占和回退)

    死锁问题

    w:700

    • 桥梁只能单向通行
    • 桥的每个部分可视为一个资源
    • 可能发生饥饿
      • 由于一个方向的持续车流,另一个方向的车辆无法通过桥梁

    死锁问题

    由于竞争资源或者通信关系,两个或更多线程在执行中出现,永远相互等待只能由其他进程引发的事件

    Thread 1:    Thread 2:
    lock(L1);    lock(L2);
    lock(L2);    lock(L1);
    

    bg right:40% 80%


    死锁问题 -- 资源

    • 资源类型$R_1, R_2, . . .,R_m$
      • CPU执行时间、内存空间、I/O设备等
    • 每类资源$R_i$有$W_i$个实例
    • 线/进程访问资源的流程
      • 请求:申请空闲资源
      • 使用:占用资源
      • 释放:资源状态由占用变成空闲 bg right:40% 100%

    死锁问题 -- 资源

    资源分类

    • 可重用资源(Reusable Resource)
      • 任何时刻只能有一个线/进程使用资源
      • 资源被释放后,其他线/进程可重用
      • 可重用资源示例
        • 硬件:处理器、内存、设备等
        • 软件:文件、数据库和信号量等
      • 可能出现死锁:每个进程占用一部分资源并请求其它资源

    bg right:35% 100%


    死锁问题 -- 资源

    资源分类

    • 可消耗资源(Consumable resource)
      • 资源可被销毁
      • 可消耗资源示例
        • 在I/O缓冲区的中断、信号、消息等
      • 可能出现死锁:进程间相互等待接收对方的消息 bg right:35% 100%

    死锁问题 -- 资源分配图

    描述资源和进程间的分配和占用关系的有向图

    • 顶点:系统中的进程
      • $P = { P_1, P_2, …, P_n }$
    • 顶点:系统中的资源
      • $R = {R_1, R_2, …, P_m}$
    • 边:资源请求
      • 进程$P_i$请求资源$R_j: P_i \rightarrow R_j$
    • 边:资源分配
      • 资源$R_j$已分配给进程$P_i:R_j \rightarrow P_i$ bg right:30% 100%

    死锁问题 -- 资源分配图

    w:1000 是否有死锁?


    死锁问题 -- 必要条件

    • 互斥
      • 任何时刻只能有一个进/线程使用一个资源实例
    • 持有并等待
      • 进/线程保持至少一个资源,并正在等待获取其他进程持有的资源
    • 非抢占
      • 资源只能在进程使用后自愿释放
    • 循环等待
      • 存在等待进程集合${P_0,P_1,...,P_N}$
      • 进程间形成相互等待资源的环

    提纲

    1. 死锁问题

    2. 死锁处理办法

    2.1 死锁预防

    2.2 死锁避免 2.3 死锁检测 2.4 死锁恢复


    死锁处理办法

    • 死锁预防(Deadlock Prevention)
      • 确保系统永远不会进入死锁状态
    • 死锁避免(Deadlock Avoidance)
      • 在使用前进行判断,只允许不会出现死锁的进程请求资源
    • 死锁检测和恢复(Deadlock Detection & Recovery)
      • 在检测到运行系统进入死锁状态后,进行恢复
    • 由应用进程处理死锁
      • 通常操作系统忽略死锁
        • 大多数操作系统(包括UNIX)的做法

    死锁预防

    预防采用某种策略限制并发进程对资源的请求,或破坏死锁必要条件。

    • 破坏“互斥”
      • 把互斥的共享资源封装成可同时访问,例如用SPOOLing技术将打印机改造为共享设备;
      • 缺点:但是很多时候都无法破坏互斥条件。
    • 破坏“持有并等待“
      • 只在能够同时获得所有需要资源时,才执行分配操作
      • 缺点:资源利用率低

    死锁预防

    预防采用某种策略限制并发进程对资源的请求,或破坏死锁必要条件。

    • 破坏“非抢占”
      • 如进程请求不能立即分配的资源,则释放已占有资源
      • 申请的资源被其他进程占用时,由OS协助剥夺
      • 缺点:反复地申请和释放资源会增加系统开销,降低系统吞吐量。
    • 破坏“循环等待“
      • 对资源排序,要求进程按顺序请求资源
      • 缺点:必须按规定次序申请资源,用户编程麻烦
      • 缺点:难以支持资源变化(例如新资源)

    提纲

    1. 死锁问题
    2. 死锁处理办法

    2.1 死锁预防

    2.2 死锁避免

    2.3 死锁检测 2.4 死锁恢复


    死锁避免

    利用额外的先验信息,在分配资源时判断是否会出现死锁,只在不会死锁时分配资源

    • 要求进程声明需要资源的最大数目
    • 限定提供与分配的资源数量,确保满足进程的最大需求
    • 动态检查的资源分配状态,确保不会出现环形等待

    死锁避免

    资源分配中,系统处于安全状态

    • 针对所有已占用进程,存在安全执行序列$<P_1,P_2,...,P_N>$
    • $P_i$要求的资源 $\le$ 当前可用资源 $+$ 所有$P_j$ 持有资源,其中$j<i$
    • 如$P_i$的资源请求不能立即分配,则$P_i$等待所有$P_j (j<i)$完成
    • $P_i$完成后,$P_{i+1}$可得到所需资源,执行完并释放所分配的资源
    • 最终整个序列的所有Pi都能获得所需资源

    死锁避免

    安全状态与死锁的关系

    • 系统处于安全状态,一定没有死锁
    • 系统处于不安全状态,可能出现死锁
      • 避免死锁就是确保系统不会进入不安全状态

    bg right:40% 100%


    死锁避免

    银行家算法(Banker's Algorithm)-- 概述

    • 银行家算法是一个避免死锁产生的算法。以银行借贷分配策略为基础,判断并保证系统处于安全状态
      • 客户在第一次申请贷款时,声明所需最大资金量,在满足所有贷款要求并完成项目时,及时归还
      • 在客户贷款数量不超过银行拥有的最大值时,银行家尽量满足客户需要

    银行家 $\leftrightarrow$操作系统;资金 $\leftrightarrow$资源;客户 $\leftrightarrow$线/进程


    死锁避免

    银行家算法(Banker's Algorithm)-- 算法思路

    • 1、对于一个线程T的请求,判断请求的资源是否超过最大可用资源
      • 如果超过,不分配,T阻塞等待
      • 如果不超过,继续2
    • 2、如果分配给该请求资源,判断是否安全
      • 安全则分配给T资源;否则不分配,T阻塞等待
    • 3、如何判断是否安全?
      • 判断是否每个线程都可以安全完成
        • 如果每个都可以完成则安全;否则不安全

    死锁避免

    银行家算法(Banker's Algorithm)-- 数据结构 w:800


    死锁避免

    银行家算法(Banker's Algorithm)-- 判断安全状态的例程 w:1000


    死锁避免

    银行家算法(Banker's Algorithm)-- 完整算法 w:700


    死锁避免

    银行家算法(Banker's Algorithm)-- 示例1 w:1000


    死锁避免

    银行家算法(Banker's Algorithm)-- 示例1 w:1000


    死锁避免

    银行家算法(Banker's Algorithm)-- 示例1 w:1000


    死锁避免

    银行家算法(Banker's Algorithm)-- 示例1 w:1000


    死锁避免

    银行家算法(Banker's Algorithm)-- 示例2 w:1000


    死锁避免

    银行家算法(Banker's Algorithm)-- 示例2 w:1000


    提纲

    1. 死锁问题
    2. 死锁处理办法

    2.1 死锁预防 2.2 死锁避免

    2.3 死锁检测

    2.4 死锁恢复


    死锁检测

    • 允许系统进入死锁状态
    • 维护系统的资源分配图
    • 定期调用死锁检测算法来搜索图中是否存在死锁
    • 出现死锁时,用死锁恢复机制进行恢复 bg right:30% 100%

    死锁检测

    死锁检测算法:数据结构

    • Available:长度为m的向量:每种类型可用资源的数量
    • Allocation:一个n×m矩阵:当前分配给各个进程每种类型资源的数量
      • 进程$P_i$ 拥有资源$R_j$的$Allocation[i, j]$个实例

    死锁检测

    死锁检测算法:完整算法 w:1000


    死锁检测

    死锁检测算法:-- 示例1 w:800


    死锁检测

    死锁检测算法:-- 示例1 w:800


    死锁检测

    死锁检测算法:-- 示例1 w:800


    死锁检测

    死锁检测算法:-- 示例1 w:800


    死锁检测

    死锁检测算法:-- 示例1 w:800


    死锁检测

    死锁检测算法:-- 示例1 w:800 序列$<T_0, T_2, T_1, T_3, T_4>$ 对于所有的i,都可满足Finish[i] = true


    死锁检测

    死锁检测算法:-- 示例2 w:700 可通过回收线程$T_0$占用的资源,但资源不足以完成其他线程请求 线程$T_1, T_2, T_3, T_4$形成死锁


    死锁检测

    使用死锁检测算法

    • 死锁检测的时间和周期选择依据
      • 死锁多久可能会发生
      • 多少进/线程需要被回滚
    • 资源图可能有多个循环
      • 难于分辨“造成”死锁的关键进/线程

    检测到死锁后,应该如何处理?


    提纲

    1. 死锁问题
    2. 死锁处理办法

    2.1 死锁预防 2.2 死锁避免 2.3 死锁检测

    2.4 死锁恢复


    进程终止

    • 终止所有的死锁进程
    • 一次只终止一个进程直到死锁消除
    • 终止进程的顺序的参考因素:
      • 进程的优先级
      • 进程已运行时间以及还需运行时间
      • 进程已占用资源
      • 进程完成需要的资源
      • 终止进程数目
      • 进程是交互还是批处理

    资源抢占

    • 选择被抢占进程
      • 参考因素:最小成本目标
    • 进程回退
      • 返回到一些安全状态, 重启进程到安全状态
    • 可能出现饥饿
      • 同一进程可能一直被选作被抢占者

    第十二讲 同步与互斥

    第六节 支持同步互斥的OS(SMOS)

    向勇 陈渝 李国良 任炬

    2023年秋季


    提纲

    1. 实验安排

    1. 全局变量累加应用
    2. 互斥锁
    3. 信号量
    4. 管程与条件变量

    实践:SMOS

    • 进化目标
    • 总体思路
    • 历史背景
    • 实践步骤
    • 程序设计

    bg right:65% 100%


    以往目标

    提高性能、简化开发、加强安全、支持数据持久保存、支持应用的灵活性,支持进程间交互,支持线程和协程

    • TCOS:支持线程和协程 ; IPC OS:进程间交互
    • Filesystem OS:支持数据持久保存
    • Process OS: 增强进程管理和资源管理
    • Address Space OS: 隔离APP访问的内存地址空间
    • multiprog & time-sharing OS: 让APP共享CPU资源
    • BatchOS: 让APP与OS隔离,加强系统安全,提高执行效率
    • LibOS: 让APP与HW隔离,简化应用访问硬件的难度和复杂性

    进化目标

    在多线程中支持对共享资源的同步互斥访问

    • 互斥锁机制
    • 信号量机制
    • 管程与条件变量机制

    同学的进化目标

    • 理解同步互斥的各种机制
    • 理解用同步互斥机制解决同步互斥问题
    • 会写支持线程间同步互斥的OS

    bg right 80%


    实践:SMOS

    • 进化目标
    • 总体思路
      • 同步互斥
    • 历史背景
    • 实践步骤
    • 程序设计

    bg right:70% 100%


    实践:SMOS

    • 进化目标
    • 总体思路
      • 同步互斥
    • 历史背景
    • 实践步骤
    • 程序设计

    bg right:70% 100%


    历史背景

    • 1963年前后,当时的数学家 Edsger Dijkstra和他的团队正在为Electrologica X8计算机开发一个操作系统(THE多道程序系统)的过程中,提出了信号(Semaphore)是一种变量或抽象数据类型,用于控制多个线程对共同资源的访问。
    • Brinch Hansen(1973)和Hoare(1974)结合操作系统和Concurrent Pascal编程语言,提出了一种高级同步原语,称为管程(monitor)。一个管程是一个由过程(procedures,Pascal语言的术语,即函数)、共享变量等组成的集合。线程可调用管程中的过程。

    实践步骤

    git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
    cd rCore-Tutorial-v3
    git checkout ch8
    

    包含了多个同步互斥相关的多线程应用程序

    user/src/bin/
    ├──  mpsc_sem.rs          # 基于信号量的生产者消费者问题
    ├──  phil_din_mutex.rs    # 基于互斥锁的哲学家问题
    ├──  race_adder_*.rs      # 各种方式的全局变量累加问题
    ├──  sync_sem.rs          # 基于信号量的同步操作
    ├──  test_condvar.rs      # 基于条件变量的同步操作
    

    实践步骤

    内核代码的主要改进部分

    os/src/
    ├── sync
    │   ├── condvar.rs        //条件变量
    │   ├── mod.rs 
    │   ├── mutex.rs          //互斥锁
    │   ├── semaphore.rs      //信号量
    │   └── up.rs
    ├── syscall
    │   ├── sync.rs //增加了互斥锁、信号量和条件变量相关系统调用
    ├── task
    │   ├── process.rs //进程控制块增加了互斥锁、信号量和条件变量
    ├── timer.rs     // 添加与定时相关的TimerCondVar类条件变量
    

    实践步骤

    比如执行哲学家问题的应用程序,展示了5个哲学家用5把叉子进行思考/进餐/休息的过程。

    Rust user shell
    >> phil_din_mutex
    time cost = 7271
    '-' -> THINKING; 'x' -> EATING; ' ' -> WAITING
    #0: -------                 xxxxxxxx----------       xxxx-----  xxxxxx--xxx
    #1: ---xxxxxx--      xxxxxxx----------    x---xxxxxx
    #2: -----          xx---------xx----xxxxxx------------        xxxx
    #3: -----xxxxxxxxxx------xxxxx--------    xxxxxx--   xxxxxxxxx
    #4: ------         x------          xxxxxx--    xxxxx------   xx
    #0: -------                 xxxxxxxx----------       xxxx-----  xxxxxx--xxx
    >>
    

    提纲

    1. 实验安排

    2. 全局变量累加应用

    1. 互斥锁
    2. 信号量
    3. 管程与条件变量

    全局变量累加问题的多线程应用 race_adder.rs

    A           //全局变量 
    A=A+1       //多个线程对A进行累加 
    

    多个线程执行上述代码,真的会出现Race Condition(竞争条件)吗?

    • 并发、无序的线程在使用有限、独占、不可抢占的资源而产生矛盾称为竞争(Race)
    • 多个线程无序竞争不能被同时访问的资源而出现执行出错的问题,称为竞争条件(Race Condition)

    全局变量累加问题的多线程应用 race_adder.rs

    pub fn main() -> i32 {
        let start = get_time();
        let mut v = Vec::new();
        for _ in 0..THREAD_COUNT {
            v.push(thread_create(f as usize, 0) as usize);  // f函数是线程主体
        }
        let mut time_cost = Vec::new();
        for tid in v.iter() {
            time_cost.push(waittid(*tid));
        }
        println!("time cost is {}ms", get_time() - start);
        assert_eq!(unsafe { A }, PER_THREAD * THREAD_COUNT); //比较累计值A
        0
    }

    实践步骤

    全局变量累加问题的多线程应用 race_adder.rs

    #![allow(unused)]
    fn main() {
    unsafe fn f() -> ! {
        let mut t = 2usize;
        for _ in 0..PER_THREAD {    
            let a = &mut A as *mut usize;    // “缓慢执行”A=A+1 
            let cur = a.read_volatile();     // “缓慢执行”A=A+1
            for _ in 0..500 {  t = t * t % 10007; } // 增加切换概率
            a.write_volatile(cur + 1);      // “缓慢执行”A=A+1
        }
        exit(t as i32)
    }
    }

    实践步骤

    全局变量累加问题的多线程应用 race_adder.rs

    >> race_adder
    time cost is 31ms
    Panicked at src/bin/race_adder.rs:40, assertion failed: `(left == right)`
      left: `15788`,
     right: `16000`
    [kernel] Aborted, SIGABRT=6
    

    每次都会执行都会出现Race Condition(竞争条件)!


    实践步骤

    基于原子操作的全局变量累加问题的多线程应用 race_adder_atomic.rs

    #![allow(unused)]
    fn main() {
    unsafe fn f() -> ! {
        for _ in 0..PER_THREAD {
            while OCCUPIED
                .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
                .is_err()  {  yield_(); }           // 基于CAS操作的近似spin lock操作
            let a = &mut A as *mut usize;           // “缓慢执行”A=A+1 
            let cur = a.read_volatile();            // “缓慢执行”A=A+1 
            for _ in 0..500 { t = t * t % 10007; }  // 增加切换概率
            a.write_volatile(cur + 1);              // “缓慢执行”A=A+1 
            OCCUPIED.store(false, Ordering::Relaxed);  // unlock操作
        }
        ...
    }

    实践步骤

    基于原子操作的全局变量累加问题的多线程应用 race_adder_atomic.rs

    >> race_adder_atomic
    time cost is 29ms
    >> race_adder_loop
    

    可以看到,执行速度快,且正确。


    实践步骤

    基于互斥锁的多线程应用 race_adder_mutex_[spin|block]

    #![allow(unused)]
    fn main() {
    unsafe fn f() -> ! {
        let mut t = 2usize;
        for _ in 0..PER_THREAD {
            mutex_lock(0); //lock(id)
            let a = &mut A as *mut usize;   // “缓慢执行”A=A+1 
            let cur = a.read_volatile();    // “缓慢执行”A=A+1 
            for _ in 0..500 {  t = t * t % 10007; } // 增加切换概率
            a.write_volatile(cur + 1);      // “缓慢执行”A=A+1 
            mutex_unlock(0); //unlock(id)
        }
        exit(t as i32)
    }
    }

    实践步骤

    基于互斥锁的全局变量累加问题的多线程应用 race_adder_mutex_spin

    >> race_adder_mutex_spin  
    time cost is 249ms
    # 执行系统调用,且进行在就绪队列上的取出/插入/等待操作
    

    基于互斥锁的全局变量累加问题的多线程应用 race_adder_mutex_block

    >> race_adder_mutex_blocking
    time cost is 919ms  
    # 执行系统调用,且进行在就绪队列+等待队列上的取出/插入/等待操作
    

    提纲

    1. 实验安排
    2. 全局变量累加应用

    3. 互斥锁

    1. 信号量
    2. 管程与条件变量

    程序设计

    spin mutex和 block mutex 的核心数据结构(全局变量): UPSafeCell

    #![allow(unused)]
    fn main() {
    pub struct UPSafeCell<T> { //允许在单核上安全**使用可变全局变量**
        inner: RefCell<T>,  //提供内部可变性和运行时借用检查
    }
    unsafe impl<T> Sync for UPSafeCell<T> {} //声明支持全局变量安全地在线程间共享
    impl<T> UPSafeCell<T> {
        pub unsafe fn new(value: T) -> Self {
            Self { inner: RefCell::new(value) }
        }
        pub fn exclusive_access(&self) -> RefMut<'_, T> {
            self.inner.borrow_mut()  //得到它包裹的数据的独占访问权
        }
    }
    }

    程序设计

    spin mutex和 block mutex 的核心数据结构

    #![allow(unused)]
    fn main() {
    pub struct MutexSpin {
        locked: UPSafeCell<bool>,  //locked是被UPSafeCell包裹的布尔全局变量
    }
    pub struct MutexBlocking {
        inner: UPSafeCell<MutexBlockingInner>,
    }
    pub struct MutexBlockingInner {
        locked: bool,
        wait_queue: VecDeque<Arc<TaskControlBlock>>, //等待获取锁的线程等待队列
    }
    }

    程序设计

    spin mutex的相关函数

    #![allow(unused)]
    fn main() {
    pub trait Mutex: Sync + Send { //Send表示跨线程 move,Sync表示跨线程share data
        fn lock(&self);
        fn unlock(&self);
    }
    
        fn unlock(&self) {
            let mut locked = self.locked.exclusive_access(); //独占访问locked
            *locked = false; //
        }
    }

    程序设计

    spin mutex的相关函数

    #![allow(unused)]
    fn main() {
    impl Mutex for MutexSpin {
        fn lock(&self) {
            loop {
                let mut locked = self.locked.exclusive_access(); //独占访问locked
                if *locked {
                    drop(locked);
                    suspend_current_and_run_next(); //把当前线程放到就绪队列末尾
                    continue;
                } else {
                    *locked = true; //得到锁了,可以继续进入临界区执行
                    return;
            ...
    }

    程序设计

    block mutex的相关函数

    #![allow(unused)]
    fn main() {
    impl Mutex for MutexBlocking {
        fn lock(&self) {
            let mut mutex_inner = self.inner.exclusive_access(); //独占访问mutex_inner
            if mutex_inner.locked {
                //把当前线程挂到此lock相关的等待队列中
                mutex_inner.wait_queue.push_back(current_task().unwrap());
                drop(mutex_inner);
                //把当前线程从就绪队列中取出,设置为阻塞态,切换到另一就绪线程执行
                block_current_and_run_next();
            } else {
                mutex_inner.locked = true; //得到锁了,可以继续进入临界区执行
            }
        }
    }

    程序设计

    block mutex的相关函数

    #![allow(unused)]
    fn main() {
        fn unlock(&self) {
            let mut mutex_inner = self.inner.exclusive_access();
            assert!(mutex_inner.locked);
            //从等待队列中取出线程,并放入到就绪队列中
            if let Some(waking_task) = mutex_inner.wait_queue.pop_front() {
                add_task(waking_task); 
            } else {
                mutex_inner.locked = false; //释放锁
            }
        }
    }

    提纲

    1. 实验安排
    2. 全局变量累加应用
    3. 互斥锁

    4. 信号量

    1. 管程与条件变量

    实践步骤

    基于信号量的多线程应用 sync_sem

    pub fn main() -> i32 {
        // create semaphores
        assert_eq!(semaphore_create(0) as usize, SEM_SYNC);
        // create threads
        let threads = vec![
            thread_create(first as usize, 0),
            thread_create(second as usize, 0),
        ];
        // wait for all threads to complete
        for thread in threads.iter() {
            waittid(*thread as usize);
        }
    ...

    实践步骤

    基于信号量的多线程应用 sync_sem

    #![allow(unused)]
    fn main() {
    unsafe fn first() -> ! {
        sleep(10);
        println!("First work and wakeup Second");
        semaphore_up(SEM_SYNC);
        exit(0)
    }
    unsafe fn second() -> ! {
        println!("Second want to continue,but need to wait first");
        semaphore_down(SEM_SYNC);
        println!("Second can work now");
        exit(0)
    }
    }

    实践步骤

    基于信号量的多线程应用 sync_sem

    >> sync_sem
    Second want to continue,but need to wait first
    First work and wakeup Second
    Second can work now
    sync_sem passed!
    
    • 信号量初值设为0
    • semaphore_down() :线程会挂起/阻塞(suspend/block)
    • semaphore_up():会唤醒挂起的线程

    程序设计

    semaphore的核心数据结构

    #![allow(unused)]
    fn main() {
    pub struct Semaphore {
        pub inner: UPSafeCell<SemaphoreInner>, //UPSafeCell包裹的内部可变结构
    }
    
    pub struct SemaphoreInner {
        pub count: isize, //信号量的计数值
        pub wait_queue: VecDeque<Arc<TaskControlBlock>>, //信号量的等待队列
    }
    }

    程序设计

    semaphore的相关函数

    #![allow(unused)]
    fn main() {
        pub fn down(&self) {
            let mut inner = self.inner.exclusive_access();
            inner.count -= 1; //信号量的计数值减一
            if inner.count < 0 {
                inner.wait_queue.push_back(current_task().unwrap()); //放入等待队列
                drop(inner);
                //把当前线程从就绪队列中取出,设置为阻塞态,切换到另一就绪线程执行
                block_current_and_run_next();
            }
        }
    }

    程序设计

    semaphore的相关函数

    #![allow(unused)]
    fn main() {
        pub fn up(&self) {
            let mut inner = self.inner.exclusive_access();
            inner.count += 1;//信号量的计数值加一
            if inner.count <= 0 {
                //从等待队列中取出线程,并放入到就绪队列中
                if let Some(task) = inner.wait_queue.pop_front() {
                    add_task(task);
                }
            }
        }
    }

    提纲

    1. 实验安排
    2. 全局变量累加应用
    3. 互斥锁
    4. 信号量

    5. 管程与条件变量


    实践步骤

    基于互斥锁和条件变量的多线程应用 test_condvar

    pub fn main() -> i32 {
        // create condvar & mutex
        assert_eq!(condvar_create() as usize, CONDVAR_ID);
        assert_eq!(mutex_blocking_create() as usize, MUTEX_ID);
        // create threads
        let threads = vec![ thread_create(first as usize, 0),
                            thread_create(second as usize, 0),];
        // wait for all threads to complete
        for thread in threads.iter() {
            waittid(*thread as usize);
        }
        ...

    实践步骤

    基于互斥锁和条件变量的多线程应用 test_condvar

    #![allow(unused)]
    fn main() {
    unsafe fn second() -> ! {
        println!("Second want to continue,but need to wait A=1");
        mutex_lock(MUTEX_ID);
        while A == 0 {
            println!("Second: A is {}", A);
            condvar_wait(CONDVAR_ID, MUTEX_ID);
        }
        mutex_unlock(MUTEX_ID);
        println!("A is {}, Second can work now", A);
        exit(0)
    }
    }

    实践步骤

    基于互斥锁和条件变量的多线程应用 test_condvar

    #![allow(unused)]
    fn main() {
    unsafe fn first() -> ! {
        sleep(10);
        println!("First work, Change A --> 1 and wakeup Second");
        mutex_lock(MUTEX_ID);
        A = 1;
        condvar_signal(CONDVAR_ID);
        mutex_unlock(MUTEX_ID);
        exit(0)
    }
    }

    实践步骤

    基于互斥锁和条件变量的多线程应用 test_condvar

    >> test_condvar
    Second: A is 0
    First work, Change A --> 1 and wakeup Second
    A is 1, Second can work now
    
    • second先执行,但由于A==0,使得等在条件变量上
    • first后执行,但会先于second,并通过条件变量唤醒second

    程序设计

    condvar的核心数据结构

    #![allow(unused)]
    fn main() {
    pub struct Condvar {
        pub inner: UPSafeCell<CondvarInner>, //UPSafeCell包裹的内部可变结构
    }
    
    pub struct CondvarInner {
        pub wait_queue: VecDeque<Arc<TaskControlBlock>>,//等待队列
    }
    }

    程序设计

    condvar的相关函数

    #![allow(unused)]
    fn main() {
        pub fn wait(&self, mutex: Arc<dyn Mutex>) {
            mutex.unlock(); //释放锁
            let mut inner = self.inner.exclusive_access();
            inner.wait_queue.push_back(current_task().unwrap()); //放入等待队列
            drop(inner);
            //把当前线程从就绪队列中取出,设置为阻塞态,切换到另一就绪线程执行
            block_current_and_run_next();
            mutex.lock();
        }
    }

    程序设计

    condvar的相关函数

    #![allow(unused)]
    fn main() {
        pub fn signal(&self) {
            let mut inner = self.inner.exclusive_access();
            //从等待队列中取出线程,并放入到就绪队列中
            if let Some(task) = inner.wait_queue.pop_front() {
                add_task(task);
            }
        }
    }

    程序设计

    sleep的设计实现

    #![allow(unused)]
    fn main() {
    pub fn sys_sleep(ms: usize) -> isize {
        let expire_ms = get_time_ms() + ms;
        let task = current_task().unwrap();
        add_timer(expire_ms, task);
        block_current_and_run_next();
        0
    }
    }

    小结

    • 学习掌握面向多线程应用的同步互斥机制
      • 互斥锁
      • 信号量
      • 条件变量
      • 原子操作
    • 能写慈母龙OS

    bg right 70%

    第十三讲 设备管理

    第一节 设备接口

    向勇 陈渝 李国良 任炬 任炬

    2023年春季


    内容

    • 要解决的问题
    • 内核 I/O 结构
    • 常见设备接口类型
    • 设备访问特征
    • 设备传输方式
    • I/O执行模型

    I/O子系统 -- 要解决的问题

    • 为何设备的差异性那么大?
    • 为何要管理设备?
    • 如何统一对设备的访问接口?
    • 为何要对设备建立抽象?
    • 如何感知设备的状态并管理设备?
    • 如何提高 CPU 与设备的访问性能?
    • 如果保证 I/O 操作的可靠性?

    I/O子系统 -- 内核 I/O 结构

    w:900


    I/O子系统 -- 常见设备接口类型

    • 设备的发展历史
      • 简单设备:CPU 可通过 I/O 接口直接控制 I/O 设备
      • 多设备:CPU 与 I/O 设备之间增加了一层 I/O 控制器和总线 BUS
      • 支持中断的设备:提高 CPU 利用率
      • 高吞吐量设备:支持 DMA
      • 各种其他设备:GPU、声卡、智能网卡、RDMA
      • 连接方式:直连、(设备/中断)控制器、总线、分布式

    I/O子系统 -- 常见设备接口类型

    • 常见设备:字符设备 块设备 网络设备

    w:500 包含上述外设的嵌入式开发板


    I/O子系统 -- 常见设备接口类型

    字符设备:如GPIO, 键盘/鼠标, 串口等

    w:700 GPIO LED light


    I/O子系统 -- 常见设备接口类型

    字符设备:如GPIO, 键盘/鼠标, 串口等

    w:900 键盘


    I/O子系统 -- 常见设备接口类型

    字符设备:如GPIO, 键盘/鼠标, 串口等 w:900 UART 串口通信


    I/O子系统 -- 常见设备接口类型

    块设备:如: 磁盘驱动器、磁带驱动器、光驱等 w:600 磁盘


    I/O子系统 -- 常见设备接口类型

    网络设备:如ethernet、wifi、bluetooth 等 w:500 网卡


    I/O子系统 -- 设备访问特征

    字符设备

    • 以字节为单位顺序访问
    • I/O 命令:get()、put() 等
    • 通常使用文件访问接口和语义

    w:600


    I/O子系统 -- 设备访问特征

    块设备

    • 均匀的数据块访问
    • I/O 命令:原始 I/O 或文件系统接口、内存映射文件访问
    • 通常使用文件访问接口和语义

    w:300


    I/O子系统 -- 设备访问特征

    网络设备

    • 格式化报文交换
    • I/O 命令:send/receive 网络报文,通过网络接口支持多种网络协议
    • 通常使用 socket 访问接口和语义

    w:300


    I/O子系统 -- 设备传输方式

    • 程序控制 I/O(PIO, Programmed I/O)
    • Interrupt based I/O
    • 直接内存访问 (DMA)

    bg right:50% 100%


    I/O子系统 -- 设备传输方式

    程序控制 I/O(PIO, Programmed I/O)

    • Port-mapped 的 PIO(PMIO):通过 CPU 的 in/out 指令
    • Memory-mapped 的 PIO(MMIO):通过 load/store 传输所有数据
    • 硬件简单,编程容易
    • 消耗的 CPU 时间和数据量成正比
    • 适用于简单的、小型的设备 I/O
    • I/O 设备通知 CPU:PIO 方式的轮询

    I/O子系统 -- 设备传输方式

    中断传输方式

    • I/O 设备想通知 CPU ,便会发出中断请求信号
    • 可中断的设备和中断类型逐步增加
    • 除了需要设置 CPU,还需设置中断控制器
    • 编程比较麻烦
    • CPU 利用率高
    • 适用于比较复杂的 I/O 设备
    • I/O 设备通知 CPU:中断方式的提醒

    I/O子系统 -- 设备传输方式

    中断传输方式

    w:800


    I/O子系统 -- 设备传输方式

    DMA 传输方式

    • 设备控制器可直接访问系统总线
    • 控制器直接与内存互相传输数据
    • 除了需要设置 CPU,还需设置中断控制器
    • 编程比较麻烦,需要 CPU 参与设置
    • 设备传输数据不影响 CPU
    • 适用于高吞吐量 I/O 设备

    I/O子系统 -- CPU 与设备的连接

    w:800


    I/O子系统 -- 读取磁盘数据的例子

    w:900


    I/O子系统 -- I/O 请求生存周期

    w:900


    I/O子系统 -- I/O执行模型 -- I/O 接口的交互协议

    基于轮询的抽象设备接口:状态 命令 数据 w:900


    I/O子系统 -- I/O执行模型 -- I/O 接口的交互协议

    基于中断的抽象设备接口:状态 命令 数据 中断 w:1000


    I/O子系统 -- I/O执行模型 -- 设备抽象

    基于文件的 I/O 设备抽象

    • 访问接口:open/close/read/write
    • 特别的系统调用:ioctl :input/output control
    • ioctl 系统调用很灵活,但太灵活了,请求码的定义无规律可循
    • 文件的接口太面向用户应用,不足覆盖到OS对设备进行管理的过程

    I/O子系统 -- I/O执行模型 -- 设备抽象

    基于流的 I/O 设备抽象

    • 流是用户进程和设备或伪设备之间的全双工连接
    • 特别的系统调用:ioctl :input/output control
    • ioctl 系统调用很灵活,但太灵活了,请求码的定义无规律可循
    • Dennis M. Ritchie 写出了“A Stream Input-Output System”,1984

    w:900


    I/O子系统 -- I/O执行模型 -- 设备抽象

    基于virtio的 I/O 设备抽象

    • Rusty Russell 在 2008 年提出通用 I/O 设备抽象–virtio 规范
    • 虚拟机提供 virtio 设备的实现,virtio 设备有着统一的 virtio 接口
    • OS 只要能够实现这些通用的接口,就可管理和控制各种 virtio 设备

    w:600


    I/O子系统 -- I/O执行模型 -- 分类

    当一个用户进程发出一个 read I/O 系统调用时,主要经历两个阶段:

    1. 等待数据准备好;
    2. 把数据从内核拷贝到用户进程中
    • 进程执行状态:阻塞/非阻塞:进程执行系统调用后会被阻塞/非阻塞
    • 消息通信机制:
      • 同步:用户进程与操作系统之间的操作是经过双方协调的,步调一致的
      • 异步:用户进程与操作系统之间并不需要协调,都可以随意进行各自的操作

    I/O子系统 -- I/O执行模型 -- 分类

    • blocking I/O
    • nonblocking I/O
    • I/O multiplexing
    • signal driven I/O
    • asynchronous I/O

    I/O子系统 -- I/O执行模型 -- 阻塞 I/O

    w:1000


    I/O子系统 -- I/O执行模型 -- 阻塞 I/O

    基于阻塞 I/O(blocking I/O)模型的文件读系统调用–read 的执行过程是:

    1. 用户进程发出 read 系统调用;
    2. 内核发现所需数据没在 I/O 缓冲区中,需要向磁盘驱动程序发出 I/O 操作,并让用户进程处于阻塞状态;
    3. 磁盘驱动程序把数据从磁盘传到 I/O 缓冲区后,通知内核(一般通过中断机制),内核会把数据从 I/O 缓冲区拷贝到用户进程的 buffer 中,并唤醒用户进程(即用户进程处于就绪态);
    4. 内核从内核态返回到用户态进程,此时 read 系统调用完成。

    I/O子系统 -- I/O执行模型 -- 非阻塞 I/O

    w:1000


    I/O子系统 -- I/O执行模型 -- 非阻塞 I/O

    基于非阻塞 IO(non-blocking I/O)模型的文件读系统调用–read 的执行过程:

    1. 用户进程发出 read 系统调用;
    2. 内核发现所需数据没在 I/O 缓冲区中,需要向磁盘驱动程序发出 I/O 操作,并不会让用户进程处于阻塞状态,而是立刻返回一个 error;
    3. 用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作(这一步操作可以重复多次);

    I/O子系统 -- I/O执行模型 -- 非阻塞 I/O

    1. 磁盘驱动程序把数据从磁盘传到 I/O 缓冲区后,通知内核(一般通过中断机制),内核在收到通知且再次收到了用户进程的 system call 后,会马上把数据 从 I/O 缓冲区拷贝到用户进程的 buffer 中;
    2. 内核从内核态返回到用户态的用户态进程,此时 read 系统调用完成。

    所以,在非阻塞式 I/O 的特点是用户进程不会被内核阻塞,而是需要不断的主动询问内核所需数据准备好了没有。


    I/O子系统 -- I/O执行模型 -- 多路复用 I/O

    w:1000


    I/O子系统 -- I/O执行模型 -- 多路复用 I/O

    多路复用 I/O(I/O multiplexing)的文件读系统调用–read 的执行过程:

    1. 对应的 I/O 系统调用是 select 和 epoll 等
    2. 通过 select 或 epoll 系统调用来不断的轮询用户进程关注的所有文件句柄或socket,当某个文件句柄或 socket 有数据到达了,select 或 epoll 系统调用就会返回到用户进程,用户进程再调用 read 系统调用,让内核将数据从内核的I/O 缓冲区拷贝到用户进程的 buffer 中。

    I/O子系统 -- I/O执行模型 -- 信号驱动 I/O

    w:800


    I/O子系统 -- I/O执行模型 -- 信号驱动 I/O

    1. 当进程发出一个 read 系统调用时,会向内核注册一个信号处理函数,然后系统调用返回,进程不会被阻塞,而是继续执行。
    2. 当内核中的 IO 数据就绪时,会发送一个信号给进程,进程便在信号处理函数中调用 IO 读取数据。

    此模型的特点是,采用了回调机制,这样开发和调试应用的难度加大。


    I/O子系统 -- I/O执行模型 -- 异步 I/O

    w:800


    I/O子系统 -- I/O执行模型 -- 异步 I/O

    1. 用户进程发起 read 异步系统调用之后,立刻就可以开始去做其它的事。
    2. 从内核的角度看,当它收到一个 read 异步系统调用之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞情况。
    3. kernel 会等待数据准备完成,然后将数据拷贝到用户内存。
    4. 当这一切都完成之后,kernel 会通知用户进程,告诉它 read 操作完成了。

    I/O子系统 -- I/O执行模型 -- 比较

    w:1000


    I/O子系统 -- I/O执行模型 -- 比较

    1. 阻塞 I/O:在用户进程发出 I/O 系统调用后,进程会等待该 IO 操作完成,而使得进程的其他操作无法执行。
    2. 非阻塞 I/O:在用户进程发出 I/O 系统调用后,如果数据没准备好,该 I/O 操作会立即返回,之后进程可以进行其他操作;如果数据准备好了,用户进程会通过系统调用完成数据拷贝并接着进行数据处理。
    3. 多路复用 I/O:将多个非阻塞 I/O 请求的轮询操作合并到一个 select 或 epoll系统调用中进行。
    4. 信号驱动 I/O:利用信号机制完成从内核到应用进程的事件通知。
    5. 异步 I/O:不会导致请求进程阻塞。

    第十三讲 设备管理

    第二节 磁盘子系统


    磁盘 -- 概述

    磁盘工作机制和性能参数 w:700


    磁盘 -- 概述

    磁盘 I/O 传输时间 w:1000


    磁盘 -- 概述

    磁盘 I/O 传输时间 w:1000


    磁盘 -- 概述

    磁盘 I/O 传输时间 w:1000


    磁盘 -- 概述

    磁盘 I/O 传输时间 w:1000


    磁盘 --磁盘调度算法

    通过优化磁盘访问请求顺序来提高磁盘访问性能

    • 寻道时间是磁盘访问最耗时的部分
    • 同时会有多个在同一磁盘上的 I/O 请求
    • 随机处理磁盘访问请求的性能表现很差

    磁盘 --磁盘调度算法 -- FIFO

    w:900


    磁盘 --磁盘调度算法 -- FIFO

    • 先进先出 (FIFO) 算法
    • 按顺序处理请求
    • 公平对待所有进程
    • 在有很多进程的情况下,接近随机调度的性能

    磁盘 --磁盘调度算法 -- 最短服务时间优先 (SSTF)

    • 选择从磁臂当前位置需要移动最少的 I/O 请求
    • 总是选择最短寻道时间 w:750

    磁盘 --磁盘调度算法 -- 扫描算法 (SCAN)

    w:750


    磁盘 --磁盘调度算法 -- 扫描算法 (SCAN)

    • 磁臂在一个方向上移动,访问所有未完成的请求
    • 直到磁臂到达该方向上最后的磁道,调换方向
    • 也称为电梯算法 (elevator algorithm)

    磁盘 --磁盘调度算法 -- 循环扫描算法 (C-SCAN)

    • 限制了仅在一个方向上扫描
    • 当最后一个磁道也被访问过了后,磁臂返回到磁盘的另外一端再次进行C-LOOK 算法
    • 磁臂先到达该方向上最后一个请求处,然后立即反转,而不是先到最后点路径上的所有请求

    磁盘 --磁盘调度算法 -- 循环扫描算法 (N-step-SCAN)

    • 磁头粘着 (Arm Stickiness) 现象
      • SSTF、SCAN 及 CSCAN 等算法中,可能出现磁头停留在某处不动的情况
    • N 步扫描算法
      • 将磁盘请求队列分成长度为 N 的子队列
      • 按 FIFO 算法依次处理所有子队列
      • 扫描算法处理每个队列

    磁盘 --磁盘调度算法 -- 双队列扫描算法 (FSCAN)

    FSCAN 算法

    • 把磁盘 I/O 请求分成两个队列
    • 交替使用扫描算法处理一个队列
    • 新生成的磁盘 I/O 请求放入另一队列中
    • 所有的新请求都将被推迟到下一次扫描时处理

    FSCAN 算法是 N 步扫描算法的简化 FSCAN 只将磁盘请求队列分成两个子队列

    第十三讲 设备管理

    第三节 支持device的OS(DOS)


    内容

    • 进化目标
    • 历史背景
    • 相关硬件
    • 总体思路
    • 实践步骤
    • 软件架构
    • 程序设计

    bg right:65% 100%


    进化目标 vs 以往目标

    • SMOS:在多线程中支持对共享资源的同步互斥访
    • TCOS:支持线程和协程
    • IPC OS:进程间交互
    • Filesystem OS:支持数据持久保存
    • Process OS: 增强进程管理和资源管理
    • Address Space OS: 隔离APP访问的内存地址空间
    • multiprog & time-sharing OS: 让APP共享CPU资源
    • BatchOS: 让APP与OS隔离,加强系统安全,提高执行效率
    • LibOS: 让APP与HW隔离,简化应用访问硬件的难度和复杂性

    进化目标 vs 以往目标

    DOS需要支持对多种外设的高效访问

    • 在内核中响应外设中断
    • 在内核中保证对全局变量的互斥访问
    • 基于中断机制的串口驱动
    • 基于中断机制的Virtio-Block驱动
    • 其它外设驱动

    bg right:40% 100%


    实践:DOS

    同学的进化目标

    • 了解设备与CPU的交互关系
    • 理解如何在内核中响应中断
    • 理解驱动的基本管理过程
    • 理解驱动的基本设计思路
    • 理解驱动与内核其它部分的交互
    • 会写支持多种外设的OS bg right:45% 80%

    实践:DOS

    • 进化目标
    • 历史背景
    • 相关硬件
    • 总体思路
    • 实践步骤
    • 软件架构
    • 程序设计

    bg right:65% 100%


    历史背景

    UNIX诞生是从磁盘驱动程序开始的

    贝尔实验室的Ken Tompson先在一台闲置的PDP-7计算机的磁盘驱动器写了一个包含磁盘调度算法的磁盘驱动程序,希望提高磁盘I/O读写速度。为了测试磁盘访问性能,他花了三周时间写了一个操作系统,这就是Unix的诞生。

    bg right:30% 80%


    历史背景

    写磁盘驱动程序包括如下一些操作:

    1. 数据结构:包括设备信息、状态、操作标识等
    2. 初始化:即配置设备,分配I/O所需内存,完成设备初始化
    3. 中断响应:如果设备产生中断,响应中断并完成I/O操作后续工作
    4. 设备操作:根据内核模块(如文件系统)的要求(如读/写磁盘数据),给I/O设备发出命令
    5. 内部交互:与操作系统上层模块或应用进行交互,完成上层模块或应用的要求(如接受文件系统下达的I/O请求,上传读出的磁盘数据)

    实践:DOS

    • 进化目标
    • 历史背景
    • 相关硬件
    • 总体思路
    • 实践步骤
    • 软件架构
    • 程序设计

    相关硬件

    • PLIC(Platform-Level Interrupt Controller)
      • 处理各种外设中断
    • CLINT(Core Local Interruptor)
      • Software Intr
      • Timer Intr bg right:45% 100%

    相关硬件 -- 系统中的外设

    $ qemu-system-riscv64 -machine virt -machine dumpdtb=riscv64-virt.dtb -bios default
       qemu-system-riscv64: info: dtb dumped to riscv64-virt.dtb. Exiting.
    $ dtc -I dtb -O dts -o riscv64-virt.dts riscv64-virt.dtb
    $ less riscv64-virt.dts
    

    PLIC设备

                    plic@c000000 {
                            phandle = <0x03>;
                            riscv,ndev = <0x35>;
                            reg = <0x00 0xc000000 0x00 0x600000>;
                            interrupts-extended = <0x02 0x0b 0x02 0x09>;
                            interrupt-controller;
                            ...
                    };
    

    相关硬件 -- 系统中的外设

    virtio-blk磁盘块设备

                    virtio_mmio@10008000 {
                            interrupts = <0x08>;
                            interrupt-parent = <0x03>;
                            reg = <0x00 0x10008000 0x00 0x1000>;
                            compatible = "virtio,mmio";
                    };
    

    相关硬件 -- 系统中的外设

    UART串口设备

                    uart@10000000 {
                            interrupts = <0x0a>;
                            interrupt-parent = <0x03>;
                            clock-frequency = <0x384000>;
                            reg = <0x00 0x10000000 0x00 0x100>;
                            compatible = "ns16550a";
                    };
    

    相关硬件 -- 系统中的外设

    virtio-input 键盘设备

                    virtio_mmio@10005000 {
                            interrupts = <0x05>;
                            interrupt-parent = <0x03>;
                            reg = <0x00 0x10005000 0x00 0x1000>;
                            compatible = "virtio,mmio";
                    };
    

    相关硬件 -- 系统中的外设

    virtio-input 鼠标设备

                    virtio_mmio@10006000 {
                            interrupts = <0x06>;
                            interrupt-parent = <0x03>;
                            reg = <0x00 0x10006000 0x00 0x1000>;
                            compatible = "virtio,mmio";
                    };
    

    相关硬件 -- 系统中的外设

    virtio-gpu 显示设备

                    virtio_mmio@10007000 {
                            interrupts = <0x07>;
                            interrupt-parent = <0x03>;
                            reg = <0x00 0x10007000 0x00 0x1000>;
                            compatible = "virtio,mmio";
                    };
    

    相关硬件 -- PLIC

    PLIC中断源 PLIC支持多个中断源,每个中断源可以是不同触发类型,电平触发或者边沿触发、PLIC为每个中断源分配

    bg right:65% 100%


    相关硬件 -- PLIC

    PLIC中断处理流程

    bg right:55% 100%


    相关硬件 -- PLIC

    • 闸口(Gateway)和IP寄存器(中断源的等待标志寄存器)
    • 编号(ID)
    • 优先级(priority)
    • 使能(Enable)

    bg right:45% 100%


    相关硬件 -- PLIC

    PLIC中断源

    • 闸口(Gateway)将不同类型的外部中断传换成统一的内部中断请求
    • 闸口保证只发送一个中断请求,中断请求经过闸口发送后,硬件自动将对应的IP寄存器置高
    • 闸口发送一个中断请求后则启动屏蔽,如果此中断没有被处理完成,则后续的中断将会被闸口屏蔽

    相关硬件 -- PLIC

    PLIC中断源

    • PLIC为每个中断源分配编号(ID)。ID编号0被预留,作为表示“不存在的中断”,因此有效的中断ID从1开始
    • 每个中断源的优先级寄存器应该是存储器地址映射的可读可写寄存器,从而使得软件可以对其编程配置不同的优先级
    • PLIC支持多个优先级,优先级的数字越大,表示优先级越高
    • 优先级0意味着“不可能中断”,相当于中断源屏蔽

    相关硬件 -- PLIC

    PLIC中断源 每个中断目标的中断源均分配了一个中断使能(IE)寄存器,IE寄存器是可读写寄存器,从而使得软件对其编程

    • 如果IE寄存器被配置为0,则意味着此中断源对应中断目标被屏蔽
    • 如果IE寄存器被配置为1,则意味着此中断源对应中断目标被打开

    实践:DOS

    • 进化目标
    • 历史背景
    • 相关硬件
    • 总体思路
      • 外设中断
    • 实践步骤
    • 软件架构
    • 程序设计 bg right:65% 100%

    实践:DOS -- 总体思路

    • 为何支持外设中断
      • 提高系统的整体执行效率
    • 为何在内核态响应外设中断
      • 提高OS对外设IO请求的响应速度
    • 潜在的问题
      • 内核态能响应中断后,不能保证对全局变量的互斥访问
      • 原因:中断会打断当前执行,并切换到另一控制流访问全局变量
    • 解决方案
      • 在访问全局变量起始前屏蔽中断,结束后使能中断

    实践:DOS

    • 进化目标
    • 历史背景
    • 相关硬件
    • 实践步骤
    • 软件架构
    • 程序设计 bg right:65% 100%

    实践:DOS -- 实践步骤

    git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
    cd rCore-Tutorial-v3
    git checkout ch9
    

    应用程序没有改变,但在串口输入输出、块设备读写的IO操作上是基于中断方式实现的。


    实践:DOS

    • 进化目标
    • 历史背景
    • 相关硬件
    • 实践步骤
    • 软件架构
    • 程序设计 bg right:65% 100%

    实践:DOS -- 软件架构

    内核的主要修改 ( os/src

    ├── boards
    │   └── qemu.rs  // UART、VIRTIO、PLIC的MMIO地址
    ├── console.rs  //基于UART的STDIO
    ├── drivers
    │   ├── block
    │   │   └── virtio_blk.rs //基于中断/DMA方式的VIRTIO-BLK驱动
    │   ├── chardev
    │   │   └── ns16550a.rs //基于中断方式的串口驱动
    │   └── plic.rs //PLIC驱动
    ├── main.rs  //外设中断相关初始化
    └── trap
        ├── mod.rs //支持处理外设中断
        └── trap.S //支持内核态响应外设中断
    

    实践:DOS

    • 进化目标
    • 历史背景
    • 相关硬件
    • 实践步骤
    • 软件架构
    • 程序设计 bg right:65% 100%

    程序设计

    设备直接相关(提供)

    1. 外设初始化操作
    2. 外设中断处理操作
    3. 外设I/O读写(或配置)操作

    OS交互相关(需求)

    1. 内存分配/映射服务
    2. 中断/调度/同步互斥/文件系统等服务

    系统设备管理

    • 了解各个设备的基本信息
      • 控制寄存器地址范围
      • 设备中断号
    • 对PLIC进行配置
      • 使能中断
      • 设置中断优先级

    系统设备管理

    • 了解各个设备的基本信息(控制寄存器基址,中断号)
    const VIRT_PLIC: usize = 0xC00_0000;   // PLIC 
    const VIRT_UART: usize = 0x1000_0000;  // UART
    const VIRTIO0: usize = 0x10008000;     // VIRTIO_BLOCK
    const VIRTIO5: usize = 0x10005000;     // VIRTIO_KEYBOARD
    const VIRTIO6: usize = 0x10006000;     // VIRTIO_MOUSE
    const VIRTIO7: usize = 0x10007000;     // VIRTIO_GPU
    // 在总中断处理例程中对不同外设的中断进行响应
    match intr_src_id {
       5 => KEYBOARD_DEVICE.handle_irq(),
       6 => MOUSE_DEVICE.handle_irq(),
       8 => BLOCK_DEVICE.handle_irq(),
       10 => UART.handle_irq(),
    

    系统设备管理

    系统设备管理初始化

    • 配置PLIC:
      • 设置接收中断优先级的下限
      • 使能S-Mode下的响应外设中断号:5/6/8/10
      • 设置外设中断号的优先级
    • 配置CPU
      • 设置 sie CSR寄存器,使能响应外部中断

    os/src/drivers/plic.rsos/src/boards/qemu.rs::devices_init()


    UART设备驱动

    UART设备驱动的核心数据结构

    pub struct NS16550a<const BASE_ADDR: usize> {
        inner: UPIntrFreeCell<NS16550aInner>,
        condvar: Condvar, //用于挂起/唤醒读字符的经常
    }
    struct NS16550aInner {
        ns16550a: NS16550aRaw,
        read_buffer: VecDeque<u8>, //用于缓存读取的字符
    }
    pub struct NS16550aRaw {
        base_addr: usize, //控制寄存器基址
    }
    

    UART设备驱动

    1. UART初始化操作
    2. UART中断处理操作
    3. UART I/O读写(或配置)操作

    字符类设备需要实现的接口

    pub trait CharDevice {
        fn init(&self);
        fn read(&self) -> u8;
        fn write(&self, ch: u8);
        fn handle_irq(&self);
    }
    

    UART设备驱动

    1. UART初始化操作
    impl<const BASE_ADDR: usize> CharDevice for NS16550a<BASE_ADDR> {
        fn init(&self) {
            let mut inner = self.inner.exclusive_access(); //独占访问
            inner.ns16550a.init(); //调用ns16550a的UART初始化函数
            drop(inner);
        }
    

    UART设备驱动

    1. UART中断处理操作
    fn handle_irq(&self) {
       let mut count = 0;
       self.inner.exclusive_session(|inner| {
          //调用ns16550a中读字符函数
          while let Some(ch) = inner.ns16550a.read() { 
                count += 1;
                inner.read_buffer.push_back(ch);
       ...
       if count > 0 {
          // 唤醒等待读取字符的进程
          self.condvar.signal();
       ...
    

    UART设备驱动

    1. UART I/O读写(或配置)操作
    fn read(&self) -> u8 {
       loop {
          let mut inner = self.inner.exclusive_access();
          if let Some(ch) = inner.read_buffer.pop_front() {
                return ch;
          } else {
                let task_cx_ptr = self.condvar.wait_no_sched();
                drop(inner);
                schedule(task_cx_ptr);
        ...
    

    UART设备驱动

    1. UART I/O读写(或配置)操作
        fn write(&self, ch: u8) {
            let mut inner = self.inner.exclusive_access();
            inner.ns16550a.write(ch);
        }
    

    virtio_blk块设备驱动

    virtio_blk设备驱动的核心数据结构

    pub struct VirtIOBlock {
        virtio_blk: UPIntrFreeCell<VirtIOBlk<'static, VirtioHal>>,
        condvars: BTreeMap<u16, Condvar>, //<虚拟队列号,条件变量>映射
    }
    

    virtio_blk块设备驱动

    1. virtio_blk初始化操作
    2. virtio_blk中断处理操作
    3. virtio_blk I/O读写(或配置)操作

    存储类设备要实现的接口

    pub trait BlockDevice: Send + Sync + Any {
        fn read_block(&self, block_id: usize, buf: &mut [u8]);
        fn write_block(&self, block_id: usize, buf: &[u8]);
        fn handle_irq(&self);
    }
    

    virtio_blk块设备驱动

    1. virtio_blk初始化操作
    pub fn new() -> Self {
       let virtio_blk = unsafe {
          UPIntrFreeCell::new(
                // 初始化vritio_drivers中的VirtIOBlk块设备 
                VirtIOBlk::<VirtioHal>::new(&mut *(VIRTIO0 as *mut VirtIOHeader)).unwrap(),)
       let mut condvars = BTreeMap::new();
       let channels = virtio_blk.exclusive_access().virt_queue_size();
       // 建立虚拟队列号与条件变量的映射
       for i in 0..channels {  
          let condvar = Condvar::new();
          condvars.insert(i, condvar);
       }
       ...
    

    virtio_blk块设备驱动

    1. virtio_blk中断处理操作
    fn handle_irq(&self) {
       self.virtio_blk.exclusive_session(|blk| {
          //获得块访问完成的虚拟队列号
          while let Ok(token) = blk.pop_used() {
                // 根据队列号对应的信号量,唤醒等待块访问结束的挂起进程
                self.condvars.get(&token).unwrap().signal();
          }
       ...
    

    virtio_blk块设备驱动

    1. virtio_blk I/O读写(或配置)操作
    fn read_block(&self, block_id: usize, buf: &mut [u8]) {
       ...
          let mut resp = BlkResp::default();// 生成一个块访问命令
          let task_cx_ptr = self.virtio_blk.exclusive_session(|blk| {
                // 调用virtio_drivers库中VirtIOBlk的read_block_nb函数,发出读块命令
                let token = unsafe { blk.read_block_nb(block_id, buf, &mut resp).unwrap() };
                // 通过条件变量挂起当前进程,等待块访问结束
                self.condvars.get(&token).unwrap().wait_no_sched()
          });
          // 唤醒等待块访问结束的进程
          schedule(task_cx_ptr);
       ...
    }
    

    virtio_blk块设备驱动

    1. virtio_blk I/O读写(或配置)操作
    fn write_block(&self, block_id: usize, buf: &[u8]) {
       ...
          let mut resp = BlkResp::default(); // 生成一个块访问命令
          let task_cx_ptr = self.virtio_blk.exclusive_session(|blk| {
                // 调用virtio_drivers库中VirtIOBlk的read_block_nb函数,发出写块命令
                let token = unsafe { blk.write_block_nb(block_id, buf, &mut resp).unwrap() };
                // 通过条件变量挂起当前进程,等待块访问结束
                self.condvars.get(&token).unwrap().wait_no_sched()
          });
          // 唤醒等待块访问结束的进程
          schedule(task_cx_ptr);
       ...
    

    操作系统课程建设的分析与实践探索

    陈渝 向勇 李国良 任炬

    2022年8月2日

    人才培养与核心课程建设研讨会


    报告内容

    • 背景介绍
      • 历史
      • 当前基本情况
    • 课程建设
      • 目标+策略
      • 基本思路
      • 具体实施

    背景介绍

    清华大学相关院系的操作系统课程

    w:800


    背景介绍 -- 历史

    铁打的课程,流水的老师

    时间任课老师教材实验
    1997前史美林 张尧学 伍尚广计算机操作系统/
    1998向勇无固定教材Nachos MIPS
    2008~2017向勇 陈渝无固定教材uCore x86
    2018~至今向勇 陈渝 李国良 任炬无固定教材uCore/rCore RISC-V

    背景介绍 -- 历史

    铁打的课程,流水的老师

    • 任课老师:李国良(2021)、任炬(2023)
    • 相关课程:
      • 大四本科:操作系统专题训练(2008年开始)
      • 研究生:高级操作系统(2009开始)
      • MOOC:学堂在线(2015年开始)

    背景介绍 -- 当前基本情况

    基本情况

    • 特征:计算机专业课 必修
    • 时间:春季/秋季(48学时)
    • 学分:3学分
    • 教师:向勇 陈渝 李国良 任炬 任炬
    • 学生:大三(部分大一/二/四)
    • 有实验内容(不算课时)
    • 无指定教材/有参考教材

    课程目标

    • 掌握OS的基本原理和设计思路
    • 掌握OS机制的实现技术
    • 理解计算机软硬件系统

    先修课

    • 程序设计+编程语言
    • 数据结构+算法
    • 计算机组成原理+编译原理

    背景介绍 -- 当前基本情况

    学生反馈

    • 太难,量太大,不会编程
    • 觉得时间投入太大
    • 部分同学为了学分而学
    • 趣味性不足,缺少及时反馈
    • 碰到问题难以及时获得帮助
    • 就业并不看好

    bg right 60%


    背景介绍 -- 当前基本情况

    参考教材

    • Operating Systems: Three Easy Pieces, Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau
    • Operating system concepts,Abraham Silberschatz, Peter Baer Galvin,Greg Gagne
    • Operating Systems: Internals and Design Principles,William Stallings,
    • rCore-Tutorial-Book, 清华OS课程教学组

    背景介绍 -- 当前基本情况 -- 教学大纲

    特点:粗看:几十年基本不变

    • 操作系统结构
    • 中断及系统调用
    • 内存管理
    • 进程管理
    • 处理机调度
    • 同步互斥
    • 文件系统

    bg right 100%


    背景介绍 -- 当前基本情况 -- 教学内容

    特点:基本抽象(骨)大致不变,内容与实验(肉)一直在变化

    -- 基本抽象:进程、地址空间、文件 -- 方法技术:调度、页表、文件系统、同步互斥、指令集、配套实验设计...

    bg right 100%


    背景介绍 -- 当前基本情况 -- 教学内容

    特点:基本(骨)不变,内容(肉)在变化 -- 骨架子大,没太多肉

    • 1995年前,缺少对实际操作系统的分析
      • 学生感觉像说教类课,把背诵记忆作为主要学习手段
      • 几乎没有实践:只能说

    背景介绍 -- 当前基本情况 -- 教学内容

    特点:基本(骨)不变,内容(肉)在变化 -- 骨头撑不起肉

    • 2000~2007年,增加了对实际操作系统(Windows、Linux、Solaris)的分析
      • 学生感觉是雾里看花,只可远观,不过细品
      • 远看很漂亮,近看太复杂
      • 有一些实践:侧重用户态编程或模拟抽象环节下的编程
      • Nachos实验展示的OS与实际操作系统有间距
      • 前期准备:C、Java

    背景介绍 -- 当前基本情况 -- 教学内容

    特点:基本(骨)不变,内容(肉)在变化 -- 肉太厚

    • 2008年,MIT教授Frans Kaashoek来清华访问,并引入xv6教学操作系统到本科实验;后简化实验,形成uCore教学操作系统。
    • 2008~2015,弱化了对实际操作系统的深入分析,增加了对教学操作系统的分析与实践
      • 能力强的学生可以比较深入地理解一个实际的小操作系统
      • 如果认真完成课程,投入的时间估计是上课时间的2倍以上
      • 前期准备:x86硬件架构、x86汇编、C语言

    背景介绍 -- 当前基本情况 -- 教学内容

    特点:基本(骨)不变,内容(肉)在变化 -- 合适的肌肉与脂肪

    • 2015年,探索代替x86的指令集: MIPS、ARM、OR1200、RISC-V
    • 2017年,选择RISC-V指令集,并逐步调整uCore实验和教学内容
    • 2018年,增加Rust编程语言,并逐步添加rCore实验和教学内容
    • 2021年,设计递增式的教学操作系统集,设计配套支撑的教学内容
    • 2022年,加强从app-->syscall-->kernel的全系统分析

    课程建设的目标

    基本观点:迭代实践与迭代认识是学好OS的基础

    • 硬件简单
    • 软件简洁
    • 循序渐进
    • 理技结合
    • 应用驱动
    • 生物进化

    bg right:60% 100%


    课程建设的策略

    让学生通过OS实践来深入地理解OS原理

    • 支持应用 -- OS功能扩展全来自于应用的需求
    • 硬件简单 -- RISC-V 64 支持OS的最小硬件子集
    • 软件简洁 -- 选用Rust语言的抽象能力和高安全设计
    • 循序渐进 -- 随着知识点扩展来扩展OS实践
    • 理技结合 -- 原理的知识点与OS实践内容对应
    • 生物进化 -- 实践逐步形成的多个OS类似生命进化中形成的各种生物

    课程建设的基本思路

    • 理解式学习:编写应用,并通过分析应用与OS的执行过程,掌握OS原理
      • 几行~几十行的应用程序,理解应用需求
      • 十几个系统调用,理解接口
      • OS的系统调用实现功能的流程图,理解OS的设计思路
      • 动态调试应用程序-->系统调用-->操作系统内核的全系统执行过程,从而理解操作系统的全局

    课程建设的基本思路

    • 构造式学习:深入OS内部实现,参考/基于这些OS,扩展某些相对完整的OS功能
      • 几行~几十行的应用程序,理解应用需求
      • 十几个系统调用,理解接口
      • OS的系统调用实现功能的流程图,理解OS的设计思路
      • 动态调试应用程序-->系统调用-->操作系统内核的全系统执行过程,从而理解操作系统的全局
      • 参考已有实现,扩展/实现操作系统的新功能/性能优化,从而具备操作系统的设计实现能力,并掌握操作系统

    课程建设的具体实施 -- OS设计

    • 原理与实践结合
      • 提供十几个由简单到相对复杂功能进化的OS实例
      • 提供OS的详细设计实现文档、多种测试用例、自动测试环境
      • 课程上讲的OS原理和概念在实践或实验中基本上有对应
      • 分析原理和实践有共同点和差异点
      • 通过原理和实践来深化对操作系统全局与细节的理解

    课程建设的具体实施 -- OS设计

    设计实现满足应用APP逐步递增需求的逐步进化的OS 操作系统 | 功能 | 系统调用个数 ---------|----------|---------- LibOS | 让APP与HW隔离,简化应用访问硬件的难度和复杂性 | 0 BatchOS | 让APP与OS隔离,加强系统安全,提高执行效率 | 2 multiprogOS | 让APP共享CPU资源 | 3


    课程建设的具体实施 -- OS设计

    设计实现满足应用APP逐步递增需求的逐步进化的OS 操作系统 | 功能 | 系统调用个数 ---------|----------|---------- TimeSharing OS | 让APP共享CPU资源 | 4 Address Space OS | 隔离APP访问的内存地址空间,加强APP间的安全 | 4 Process OS | 支持APP动态创建新进程,增强进程管理和资源管理能力 |9


    课程建设的具体实施

    设计实现满足应用APP逐步递增需求的逐步进化的OS 操作系统 | 功能 | 系统调用个数 ---------|----------|---------- Filesystem OS | 支持APP对数据的持久保存 | 11 IPC OS | 支持多个APP进程间数据交互与事件通知 | 17 TreadOS | 支持线程APP,简化切换与数据共享 | 18 SyncMutex OS | 在多线程APP中支持对共享资源的同步互斥访问 | 27


    课程建设的具体实施 -- OS设计 -- 系统调用

    30个系统调用

    • 进程相关: 13个
    • 文件相关:5个
    • 地址空间相关:2个
    • 同步互斥相关:9个

    课程建设的具体实施 -- 课程资源建设

    课程实践内容 -- rCore Tutorial Book v3

    课程实验内容 -- rCore Tutorial Guide 2022 Spring

    课程参考文档 --教材/课件


    课程建设的具体实施 -- 在线学习建设

    • 当前还存在一些需要改进的地方
      • 学习内容的逐步递进
        • 进一步细化知识粒度和衔接
      • 学习效果的及时反馈
        • 在线IDE
        • 在线评测
      • 学习问题的及时解答
        • 在线答疑
        • 鼓励机制

    bg right:25% 100%


    课程建设的具体实施 -- 课程设计

    • 课程内容
      • 48学时,16次课(13讲)
    • 实践内容
      • 16~32学时,5次实验
    • 扩展讲解和训练
      • 最新技术进展
      • 实验代替考试
      • 操作系统比赛

    bg right:50% 100%


    谢谢!

    开源操作系统训练营

    https://github.com/LearningOS/rust-based-os-comp2022

    一年三期:春季学期、秋季学期、暑假


    备份材料

    • 十三讲的课程内容设计+知识点设计
    • 十一个教学OS的实践设计
    • 五个实验设计

    第一讲 操作系统概述

    知识点

    • 操作系统定义、抽象、特征
    • 操作系统历史演化
    • 操作系统的架构
    • 硬件与操作系统的关系与接口
    • 应用与操作系统的关系与接口
    • 实践:Linux/rCore/uCore APP

    第二讲 实践与实验介绍

    知识点

    • 函数调用、栈帧与参数
    • 编译器/硬件与OS的共识
    • 加电后硬件/软件启动过程
    • 程序执行过程、Linux应用编程
    • RISC-V的SBI
    • 开发环境与执行环境、裸机编程
    • 实践:LibOS操作系统

    第三讲 基于特权级的隔离与批处理

    知识点

    • 特权级与特权级切换
    • 系统调用、外设中断、软件异常
    • 系统调用设计与实现、执行过程
    • 实践:批处理操作系统

    第四讲 多道程序与分时多任务

    知识点

    • 上下文、中断、任务、任务/中断上下文、任务/中断上下文切换、任务/中断上下文切换的时机
    • 任务生命周期、任务执行过程
    • 协作式调度、抢占式调度
    • 实践:多道程序操作系统
    • 实践:分时多任务操作系统

    第五讲 地址空间-物理内存管理

    知识点

    • 地址空间、内存管理、连续物理内存分配、非连续物理内存分配
    • 物理内存的管理
    • 多级页表的设计与实现
    • 访存异常及其软硬件协同处理过程
    • 实践:基于地址空间的分时多任务操作系统

    第六讲 地址空间-虚拟存储管理

    知识点

    • 局部性原理、虚拟存储基本概念、Page Fault异常、局部页面置换算法、全局页面置换算法、Belady异常
    • 按需分页、Copy On Write、基于页的内存换入换出机制
    • 实践:支持虚存的分时多任务操作系统

    第七讲 进程管理与单处理器调度

    知识点

    • 进程概念、进程运行状态、进程的管理、基本调度策略/算法
    • 实时任务、实时调度算法、优先级反置问题与解决方法
    • 进程控制块和fork, exec, waitpid, exit系统调用的设计与执行
    • 实践:支持进程的操作系统

    第八讲 多处理器调度

    知识点

    • 多核/SMP/NUMA架构的特征
    • 多处理器调度算法
    • 负载迁移技术

    第九讲 文件系统

    知识点

    • 文件系统基本概念:文件/目录/文件描述符/目录项, 软/硬链接
    • 文件/文件系统设计与实现
    • open/close/read/write系统调用的设计与执行
    • 链式/索引文件结构设计、空闲磁盘块空间管理、缓冲区管理
    • 实践:支持文件的FS操作系统

    第十讲 进程间通信

    知识点

    • 无名/有名管道、消息队列、共享内存、信号的应用编程与设计实现
    • 支持管道与信号的IPC操作系统

    第十一讲 线程与协程

    知识点

    • 线程、协程的起因与特征
    • 用户态管理的线程设计与实现
    • 内核态管理的线程设计与实现
    • 实践:支持线程的操作系统

    第十二讲 同步互斥

    知识点

    • 软件实现的互斥、基于中断的互斥、基于原子指令的互斥
    • 忙等方式与休眠方式的同步互斥
    • 信号量的设计与实现
    • 管程与条件变量的设计与实现
    • 死锁必要条件、死锁安全、银行家算法、死锁检测算法等
    • 实践:支持同步互斥的操作系统

    第十三讲 设备管理

    知识点

    • I/O设备分类、I/O传输方式、I/O设备抽象
    • I/O执行模型
    • 串口驱动、块设备驱动
    • 内核态响应中断
    • 实践:支持外设中断的操作系统

    实践 1: UNIX/Linux APP

    • "系统调用"

    • 例子,用C语言,来自UNIX(例如Linux、macOS、FreeBSD)

          fd = open("out", 1);
          write(fd, "hello\n", 6);
          pid = fork()
      
    • 能理解和编写包含操作系统进程/文件等相关的简单命令行Linux程序


    实践 2: 裸机程序:LibOS

    • 软硬件启动,栈的建立、函数调用,SBI调用 w:600
    • 理解RISC-V的特权模式,理解SBI访问,编写裸机程序

    实践 3: Batch OS

    • 特权级: U-Mode, S-Mode
    • 特权级切换
    • 陷入上下文
    • 编译多应用+OS的镜像
    • 加载并执行应用

    bg right 100%


    实践 4-1: MultiProg OS

    • 任务的概念
    • 任务的设计实现
    • 协作/抢占式调度
    • 任务上下文
    • 陷入上下文
    • 切换任务
    • 切换特权级 bg right:60% 100%

    实践 4-2: TimeSharing OS

    • 中断
    • 中断响应
    • 协作/抢占式调度
    • 陷入上下文
    • 任务上下文
    • 切换任务
    • 切换特权级 bg right:60% 100%

    实践 5: AddrSpace OS

    App/OS内存布局

    • .text: 数据段
    • .data:可修改的全局数据。
    • 未初始化数据段 .bss
    • 堆 (heap)向高地址增长
    • 栈 (stack)向低地址增长 bg right 120%

    实践 5: AddrSpace OS

    • 地址空间
    • 物理地址
    • 页表
    • 陷入上下文
    • 任务上下文
    • 中断响应

    bg right:60% 100%


    实践 5: AddrSpace OS

    • 应用地址空间
    • 内核地址空间
    • 切换任务
    • 切换特权级
    • 切换页表

    bg right:65% 100%


    实践 6:Process OS

    • Process
      • Trap
      • Task
      • Address Space
      • state
      • relations
      • exit code bg right:65% 100%

    实践 6:Process OS

    • fork
    • exec
    • exit
    • wait

    bg right:70% 100%


    实践 6:Process OS

    • PCB bg right:70% 100%

    实践 7: Filesystem OS

    • 文件系统层次结构
    • 块设备驱动
    • 块缓冲区
    • EasyFS
    • Virtual FS
    • 进程的文件描述符表
    • 文件相关系统调用 bg right:60% 100%

    实践 7: Filesystem OS

    • 文件系统在操作系统中的位置 bg right:70% 100%

    实践 7: Filesystem OS

    • 进程的文件描述符表
    • 文件相关系统调用 bg right:70% 100%

    实践 8: IPC OS

    支持进程间通信和异步消息机制

    • 管道(PIPE)
    • 信号(Signal) bg right:70% 100%

    实践 8: IPC OS

    对进程控制块的扩展

    • 管道也是一种文件
    • 支持I/O重定向 bg right:60% 100%

    实践 9: Thread OS

    • 用户态管理的用户态运行的线程
    • 内核态管理的用户态运行的线程 bg right:65% 100%

    实践 9: Thread OS

    • 协程结构
    • 线程结构
    • 进程结构

    bg right:60% 100%


    实践 10: SyncMutex OS

    对进程控制块扩展,支持线程同步互斥访问共享变量

    • Mutex
    • Semphore
    • Condvar bg right:70% 100%

    实践 10: SyncMutex OS

    对进程控制块扩展,支持线程同步互斥访问共享变量

    • Mutex
    • Semphore
    • Condvar

    bg right:70% 100%


    实践 11: Device OS

    支持块设备/串口等外设

    • 内核态中断响应
    • DMA
    • 轮询
    • 设备<-->内存间数据传输
    • 同步互斥保护

    bg right:60% 100%


    实验 1 :获取任务信息

    • 预先学习完成 实践 1--4

    内容

    我们的系统已经能够支持多个任务分时轮流运行,我们希望引入一个新的系统调用 sys_task_info 以获取当前任务的信息


    实验 2 :完成mmap和munmap系统调用功能

    • 预先学习完成 实践 1--5

    内容

    • 引入虚存机制后,原来内核的 sys_get_time 和 sys_task_info 函数实现就无效了。请你重写这个函数,恢复其正常功能。
    • mmap 在 Linux 中主要用于在内存中映射文件, 本次实验简化它的功能,仅用于申请内存。请实现 mmap 和 munmap 系统调用

    实验 3 :完成spawn系统调用功能

    • 预先学习完成 实践 1--6

    内容

    实现一个完全 DIY 的系统调用 spawn,用以创建一个新进程。


    实验 4 :实现文件的硬链接功能

    • 预先学习完成 实践 1--7

    内容

    硬链接要求两个不同的目录项指向同一个文件,在我们的文件系统中也就是两个不同名称目录项指向同一个磁盘块。要求实现三个系统调用 sys_linkat、sys_unlinkat、sys_stat 。


    实验 5 :实现文件的硬链接功能

    • 预先学习完成 实践 1--10

    内容

    完成对基于信号量/条件变量的同步互斥多线程程序的死锁检测

    从操作系统的角度看

    编译原理和计算机组成原理

    陈渝

    清华大学

    2023年5月13日

    第七届全国高校计算机类专业系统能力培养高峰论坛


    报告内容

    - 背景和问题

    - 编译/操统/计组的特点与联系

    - 关于编译/操统/计组的综合实践


    背景与问题

    自我介绍

    • 清华大学计算机系软件所 陈渝
      • 研究方向:操作系统
      • 教学内容:
        • “计算机科学基础” 大一第一学期
        • “编译原理” 大三第一学期
        • “操作系统” 大三第二学期
        • “高级操作系统” 研究生课

    bg right:40% 100%


    背景与问题

    清华SYS课程系列

    • 计算机系统概论 大二第一学期
    • 计算机组成原理 大三第一学期
    • “编译原理” 大三第一学期
    • “操作系统” 大三第二学期
    • “计算机系统结构” 大三第二学期
    • “计算机网络原理” 大三第一学期

    bg right:45% 100%


    背景与问题

    • WHY "从操作系统角度看编译原理/计算机组成原理"
    • SYS Courses --> SOS Courses
      • 学生觉得学习这三门课程很难/量大,缺乏兴趣
      • 老师觉得讲授这三门课程很难/量大,缺乏动力
      • 三门课程的内容相对独立,缺少联系
      • 与实际应用的联系不够紧密,缺少牵引性

    bg right:20% 100%


    背景与问题

    • WHY "从操作系统的角度看编译原理和计算机组成原理"
      • 我们培养学生的目标
        • 系统分析能力:提问题
        • 系统抽象能力:建模型
        • 系统综合能力:系统观
        • 系统设计能力:提思路
        • 系统实现能力:写代码
      • 这些能力更多是通过学生的自身实践来获得的,上课是实践的辅助
      • 而老师的主要精力可能花在讲授知识上了,导致目标与现状不符

    背景与问题

    简洁明了的上古计算机系统

    • Dennis Ritchie开发的CC编译器源码行数:< 1万行
    • Ken Thompson开发的UNIX内核源码行数:< 1万行
    • Intel 4004 晶体管数量:2300个
      • Marcian Hoff提出单芯片设计思路,Federico Faggin完成设计实现

    编译器<-->操作系统<-->计算机组成之间的整体交互简单清晰 学生能够通过实践对计算机系统有全面的理解 通过Compiler/OS/CPU的实践让学生足以产生计算机操作的连贯画面


    背景与问题

    日趋复杂的现代计算机系统

    • GCC-11 编译器源码行数:1667万行
    • Linux-6.0 内核源码行数:3306万行
    • AMD桌面处理器5950X 晶体管数量:395亿

    复杂性的必然结果是专业化,清晰度消失了 计算机系统的本质隐藏在各种晦涩的接口和实现中 学生也陷入了知识的汪洋大海中,难以把握全局


    背景与问题

    学生学习系统类课程的困难

    • 实际实现与抽象概念的巨大差距
      • 学生难以理解老师和课本中的概念
    • 代码阅读量不够
      • 学生难以理解程序的实现细节

    背景与问题

    学生学习系统类课程的困难

    • 复杂的实验和编程任务
      • 课程通常需要与计算机底层打交道
      • 学生可能缺乏与计算机底层相关的实践经验
    • 缺乏系统性和整体性
      • 学生难以理解系统类课程的知识体系
      • 学生难以把这些课程综合起来理解和灵活运用

    背景与问题

    解决上述问题的已有方法

    • 新类型教材与实验
    • 《计算机系统基础 第2版》,袁春风、余子濠
    • 《计算机系统- 系统架构与操作系统的高度集成》(Computer Systems: An Integrated Approach to Architecture and Operating System), Umakishore Ramachandran等
    • 《计算机系统设计原理》(Principal of Computer System Design), Jerome H. Saltzer等

    背景与问题

    解决上述问题的已有方法

    • 新类型教材与实验
      • 《深入理解计算机系统》(Computer Systems: A Programmer's Perspective,CSAPP ), Randal E. Bryant等, 2003
        • 全面覆盖计算机系统原理
        • 注重底层原理的实践和应用
        • 一些内容过于深入

    背景与问题

    解决上述问题的已有方法

    • 新类型教材与实验
    • 《从NAND到Tetris:用硬件和软件构建现代计算机》(The Elements of Computing Systems: Building a Modern Computer from First Principles), Noam Nisan等,2005
      • 从基础的布尔逻辑、组合逻辑和时序逻辑开始,逐步引导读者构建出一个简单的计算机
      • 涵盖了计算机系统的各个层面,包括硬件、指令集架构、汇编语言、编译器、操作系统和应用程序等

    报告内容

    - 背景和问题

    - 编译/操统/计组的特点与联系

    - 关于编译/操统/计组的综合实践


    编译/操统/计组的特点与联系

    - 共性在哪里?

    - 关系在哪里?

    - 需要学生掌握的核心点是啥?


    编译/操统/计组的特点与联系

    bg 90%


    编译/操统/计组的特点与联系

    bg 90%


    编译/操统/计组的特点与联系

    bg 90%


    编译/操统/计组的特点与联系

    bg 90%


    编译/操统/计组的特点与联系

    共性的关键点

    归纳与抽象(做减法)

    • 编译原理:词法与语法(去除复杂语法分析/代码优化/中间表示等)
    • 操作系统:进程(去除地址空间/文件/同步互斥/性能优化等)
    • 计算机组成原理:机器指令/语言(去除流水线等优化实现) 简化已有课程的知识点,只留下基本综合知识点

    编译/操统/计组的特点与联系

    共性的关键点(留本质)

    语言 Language

    • 编译原理:高级语言--中间语言--汇编语言
    • 操作系统:高级语言+汇编语言+系统调用
    • 计算机组成原理:机器语言(汇编语言)及其硬件实现

    编译/操统/计组的特点与联系

    关系的关键点(留本质)

    接口 Interface

    • 编译原理--计算机组成原理:数据与程序的机器表示
    • 编译原理--操作系统:机器视角下的程序结构/控制流/变量访问
    • 计算机组成原理--操作系统:汇编语言/特权级/中断/虚拟地址

    编译/操统/计组的特点与联系

    需要学生掌握的核心点:高级语言编写的程序如何编译&加载&运行

    • 基于语言接口,用全局的思路来理解编译/操统/计组
    • 用一个工程项目(甚至一个程序)来实践编译/操统/计组
    • 讲课内容:简化的编译/操统/计组的核心关联知识
      • 编译:高级语言 --> 汇编语言 --> 机器语言
      • 计组:机器语言 --> 特权级指令:数据处理/CPU状态
      • 操统:高级语言 --> 特权级指令:控制CPU/处理中断

    报告内容

    - 背景和问题

    - 编译/操统/计组的特点与联系

    - 关于编译/操统/计组的综合实践


    关于编译/操统/计组的综合实践

    bg 90%


    关于编译/操统/计组的综合实践

    邢其正 本科:北京科技大学,研究生:北京大学,将去阿里工作

    • 2021年作为出题导师参加全国大学生计算机系统能力大赛
      • https://github.com/maxXSoft
      • proj5-fuxi-full-system-with-cpu-compiler-os
      • 处理器: Fuxi (伏羲), 32-bit pipelined RISC-V CPU
      • 编译器: Yu (羽), a simple system programming language
      • 操作系统: GeeOS (寂), a lightweight, UNIX like operating system written in YuLang,running on Fuxi

    关于编译/操统/计组的综合实践

    • 初步构想
      • 学生能够得到的收获
        • 三者的抽象能力
        • 三者的综合能力
        • 三者的实践能力
      • 不涉及
        • 性能优化,/操作系统/编译器/计算机组成的非相关复杂细节

    关于编译/操统/计组的综合实践

    • 初步构想

    bg 90%


    关于编译/操统/计组的综合实践

    • 初步构想
      • 一个简化RISC-V的处理器设计:定义机器语言
        • 初级:纯软件实现的硬件指令功能模拟
        • 高级:课硬件实现的时钟周期级模拟仿真
        • 基本计算/控制指令
        • 特权级/中断支持
        • (option)虚拟地址支持

    关于编译/操统/计组的综合实践

    • 初步构想
      • 一个简化C语言的编译器设计:定义高级语言
        • 初级:C语言的子集,只有基本整型数的运算和基本控制逻辑,编译为RISC-V汇编语言
        • 高级:C语言的子集,可支持指针和函数,可编译为RISC-V机器语言

    关于编译/操统/计组的综合实践

    • 初步构想
      • 一个面向简化RISC-V并基于简化C语言的OS设计:定义上下接口
        • 初级:支持一个计算和"Helloworld"输出功能的应用加载与执行
        • 高级:支持两个应用交替加载与执行

    关于编译/操统/计组的综合实践

    • 初步构想
      • 简化已有三门课程
      • 形成教程和实验指导书
      • 能够在线进行实验

    bg right:50%  100%


    谢谢!

    计算机系统能力大赛操作系统比赛 https://os.educg.net

    开源操作系统训练营 https://github.com/LearningOS/rust-based-os-comp2023

    通过系统综合实践

    贯通计组编译操统

    陈渝

    清华大学计算机系

    2023第17届全国高校操作系统课程教学研讨会


    报告内容

    - 背景和问题

    - 编译/操统/计组的特点与联系

    - 关于编译/操统/计组的综合实践

    • 清华操作系统课件:learningos.github.io/os-lectures/
    • 清华编译原理实验:decaf-lang.github.io/minidecaf-tutorial/
    • 奥地利萨尔茨堡大学三课融合实践:https://github.com/cksystemsteaching/selfie

    背景与问题

    • 清华大学计算机系软件所 陈渝
      • 研究方向:操作系统
      • 教学内容:
        • “计算机科学基础” 大一第一学期
        • “编译原理” 大三第一学期
        • “操作系统” 大三第二学期
        • “高级操作系统” 研究生课

    bg right:40% 100%


    背景与问题

    清华SYS课程系列

    • 计算机系统概论 大二第一学期
    • 计算机组成原理 大三第一学期
    • “编译原理” 大三第一学期
    • “操作系统” 大三第二学期
    • “计算机系统结构” 大三第二学期
    • “计算机网络原理” 大三第一学期

    bg right:45% 100%


    背景与问题

    • WHY "从操作系统角度看编译原理/计算机组成原理"
    • SYS Courses --> SOS Courses
      • 学生觉得学习这三门课程很难/量大,缺乏兴趣
      • 老师觉得讲授这三门课程很难/量大,缺乏动力
      • 三门课程的内容相对独立,缺少联系
      • 与实际应用的联系不够紧密,缺少牵引性

    bg right:20% 100%


    背景与问题

    • WHY "从操作系统的角度看编译原理和计算机组成原理"
      • 我们培养学生的目标
        • 系统分析能力:提问题
        • 系统抽象能力:建模型
        • 系统综合能力:系统观
        • 系统设计能力:理思路
        • 系统实现能力:写代码
      • 这些能力更多是通过学生的自身实践来获得的,上课是实践的辅助
      • 而老师的主要精力可能花在讲授知识上了,导致目标与现状不符

    背景与问题

    简洁明了的上古计算机系统

    • Dennis Ritchie开发的CC编译器源码行数:< 1万行
    • Ken Thompson开发的UNIX内核源码行数:< 1万行
    • Intel 4004 晶体管数量:2300个
      • Marcian Hoff提出单芯片思路,Federico Faggin完成设计实现

    编译器<-->操作系统<-->计算机组成之间的整体交互简单清晰 学生能够通过实践对计算机系统有全面的理解 通过Compiler/OS/CPU的实践让学生足以产生计算机操作的连贯画面


    背景与问题

    日趋复杂的现代计算机系统

    • GCC-11 编译器源码行数:1667万行 增加了一千倍
    • Linux-6.0 内核源码行数:3306万行 增加了三千倍
    • AMD桌面处理器5950X 晶体管数量:395亿 增加了一千七百万倍

    复杂性的必然结果是专业化,清晰度消失了 计算机系统的本质隐藏在各种晦涩的接口和实现中 学生也陷入了知识的汪洋大海中,难以把握全局


    背景与问题

    学生学习系统类课程的困难

    • 实际实现与抽象概念的巨大差距
      • 学生难以理解老师和课本中的概念
    • 代码阅读量不够
      • 学生难以理解程序的实现细节

    背景与问题

    学生学习系统类课程的困难

    • 复杂的实验和编程任务
      • 课程通常需要与计算机底层打交道
      • 学生可能缺乏与计算机底层相关的实践经验
    • 缺乏系统性和整体性
      • 学生难以理解系统类课程的知识体系
      • 学生难以把这些课程综合起来理解和灵活运用

    背景与问题

    解决上述问题的已有方法

    • 新类型教材与实验
    • 《计算机系统基础 第2版》,袁春风、余子濠
    • 《计算机系统- 系统架构与操作系统的高度集成》(Computer Systems: An Integrated Approach to Architecture and Operating System), Umakishore Ramachandran等
    • 《计算机系统设计原理》(Principal of Computer System Design), Jerome H. Saltzer等

    背景与问题

    解决上述问题的已有方法

    • 新类型教材与实验
      • 《深入理解计算机系统》(Computer Systems: A Programmer's Perspective,CSAPP ), Randal E. Bryant等, 2003
        • 全面覆盖计算机系统原理
        • 注重底层原理的实践和应用
        • 一些内容过于深入

    背景与问题

    解决上述问题的已有方法

    • 新类型教材与实验
    • 《从NAND到Tetris:用硬件和软件构建现代计算机》(The Elements of Computing Systems: Building a Modern Computer from First Principles), Noam Nisan等,2005
      • 从基础的布尔逻辑、组合逻辑和时序逻辑开始,逐步引导读者构建出一个简单的计算机
      • 涵盖了计算机系统的各个层面,包括硬件、指令集架构、汇编语言、编译器、操作系统和应用程序等

    报告内容

    - 背景和问题

    - 编译/操统/计组的特点与联系

    - 关于编译/操统/计组的综合实践


    编译/操统/计组的特点与联系

    - 共性在哪里?

    - 关系在哪里?

    - 需要老师/学生掌握的核心点是啥?


    编译/操统/计组的特点与联系

    bg 90%


    编译/操统/计组的特点与联系

    bg 90%


    编译/操统/计组的特点与联系

    bg 90%


    编译/操统/计组的特点与联系

    bg 90%


    编译/操统/计组的特点与联系

    共性的关键点

    方法:归纳与抽象(做减法)--形成--> 关键点

    • 编译原理:词法与语法(去除复杂语法分析/代码优化/中间表示等)
    • 操作系统:进程(去除地址空间/文件/同步互斥/性能优化等)
    • 计算机组成原理:机器指令/语言(去除流水线等优化实现) 简化已有课程的知识点,只留下基本关键点

    编译/操统/计组的特点与联系

    共性的关键点(留本质)

    语言 Language --体现--> 功能

    • 编译原理:高级语言--中间语言--汇编语言
    • 操作系统:高级语言+汇编语言+系统调用
    • 计算机组成原理:机器语言(汇编语言)及其硬件实现

    编译/操统/计组的特点与联系

    关系的关键点(留本质)

    接口 Interface --体现--> 关系

    • 编译原理<-->计算机组成原理:数据与程序的机器表示
    • 编译原理<-->操作系统:机器视角下的程序结构/控制流/变量访问
    • 计算机组成原理<-->操作系统:汇编语言/特权级/中断/虚拟地址

    编译/操统/计组的特点与联系

    需要老师/学生掌握的核心点:高级语言编写的程序如何编译--加载--运行

    • 基于语言接口,用全局的思路来理解编译/操统/计组
    • 用一个工程项目(甚至一个程序)来实践编译/操统/计组
    • 讲课内容:简化的编译/操统/计组的核心关联知识
      • 编译:高级语言 --> 汇编语言 --> 机器语言
      • 计组:机器语言 --> 特权级指令:数据处理/CPU状态
      • 操统:高级语言 --> 特权级指令:控制CPU/处理中断

    报告内容

    - 背景和问题

    - 编译/操统/计组的特点与联系

    - 关于编译/操统/计组的综合实践


    关于编译/操统/计组的综合实践

    bg 90%


    关于编译/操统/计组的综合实践

    邢其正 本科:北京科技大学,研究生:北京大学,将去阿里工作

    • 2021年作为出题导师参加全国大学生计算机系统能力大赛
      • https://github.com/maxXSoft
      • proj5-fuxi-full-system-with-cpu-compiler-os
      • 处理器: Fuxi (伏羲), 32-bit pipelined RISC-V CPU
      • 编译器: Yu (羽), a simple system programming language
      • 操作系统: GeeOS (寂), a lightweight, UNIX like operating system written in YuLang,running on Fuxi

    关于编译/操统/计组的综合实践

    • 初步构想
      • 老师/学生能够得到的收获
        • 三者的抽象能力
        • 三者的综合能力
        • 三者的实践能力
      • 不涉及
        • 性能优化,/操作系统/编译器/计算机组成的非相关复杂细节

    关于编译/操统/计组的综合实践

    • 初步构想
      • 综合性辅助工具:简化三门课程,形成辅助工具
      • 可视化展示:能够动态展示软硬件协同的运行细节
      • 课外实验指导:形成课外实验指导书,可扩展工具

    bg right:50%  100%


    关于编译/操统/计组的综合实践

    • 初步构想

    bg 90%


    关于编译/操统/计组的综合实践

    • 初步构想
      • 一个简化RISC-V的处理器设计:定义机器语言
        • 初级:纯软件实现的硬件指令功能模拟
        • 基本计算/控制指令
        • 特权级/中断支持
        • (option)虚拟地址支持
        • (option)高级:课硬件实现的时钟周期级模拟仿真

    关于编译/操统/计组的综合实践

    • 初步构想
      • 一个简化的C语言子集的编译器设计:定义高级语言,编译为RISC-V汇编语言
        • 初级:只有基本整型数的运算和基本控制逻辑
        • 中级:可支持函数和结构型数据
        • (option)高级:可支持指针、数组

    关于编译/操统/计组的综合实践

    • 初步构想
      • 一个面向简化RISC-V并基于简化C语言的OS设计:定义上下接口
        • 初级:支持一个计算和"Helloworld"输出功能的应用加载与执行
        • 中级:支持两个应用交替加载与执行
        • 高级:支持虚拟内存
        • 高级:支持文件系统

    谢谢!

    全国大学生计算机系统能力大赛操作系统比赛 https://os.educg.net

    开源操作系统训练营 https://github.com/LearningOS/rust-based-os-comp2023

    2024春季OS课大实验(课程设计)相关信息

    参加课程设计的同学,需要每周(一般是周六上午)交流一次,主要交流内容是同学和老师/助教在本周的进展情况介绍,问题,解决方案、计划和建议等方面讨论,帮助各位同学顺利完成基本实验和大实验。

    自学操作系统基础知识(前四周完成)

    在第四周末检查5个实验的完成情况和对操作系统的掌握情况

    自学Rust

    自学操作系统原理

    自学OS基本实验

    • OS实验指导(代码/指导/测试/测例等):https://github.com/learningos/ 中的 u/rCore-Tutorial-*-2024S

    开展大实验(后十二周完成)

    一人一队,鼓励各队之间多合作,形成共赢。每周交流一次,在交流前,需要完成周进展纪要文档。在第十六周末检查大实验的最终完成情况,并鼓励参加全国大学生操作系统比赛(可以是暑期课程内容,以最终系里公布情况为准) 对大实验最终完成情况的检查内容包括:每周进展纪要,代码仓库的更新情况,代码展示,总结报告,总结slide等

    大实验题目列表

    • 支持Linux App(如healer, syzkaller, ConfFuzz, DDS等,不用都支持)的组件化宏内核框架和kernel直接相关内核模块改进和优化
    • 支持Linux App(如healer, syzkaller, ConfFuzz, DDS等,不用都支持)的基于异步协程的组件化宏内核框架和kernel直接相关内核模块改进和优化
    • 兼容redox/seL4/zircon等的组件化微内核框架和kernel直接相关内核模块改进和优化
    • 支持ARM or x64的组件化hypervisor框架和hypervisor直接相关内核模块改进和优化
    • 支持Rust STD库的组件化unikernel框架和kernel直接相关内核模块改进和优化
    • 扩展Verus安全验证的lab1-5的rCore-Tutorial内核
    • 扩展Prusti安全验证的lab1-5的rCore-Tutorial微内核
    • 扩展Verus安全验证的Rust-based seL4组件化微内核
    • 扩展Prusti安全验证的Rust-based seL4组件化微内核
    • 扩展Verus安全验证的Rust-based hypervisor的信息不干扰(参考asplos24-dai.pdf)
    • 扩展Prusti安全验证的Rust-based hypervisor的信息不干扰(参考asplos24-dai.pdf)
    • 基于OS无关内核组件设计,实现对AI加速单元:如Intel NPU、瑞芯微RK3588 NPU,k230 NPU等)的内核驱动支持
    • 基于OS无关内核组件设计,实现并扩展 x64/arm/rv64/loongarch的HAL(硬件抽象层)
    • 基于OS无关内核组件设计,实现并扩展对starfive星光二代开发板的多种外设驱动支持
    • 基于OS无关内核组件设计,实现并扩展对树莓派4开发板的多种外设驱动支持
    • 基于OS无关内核组件设计,实现并扩展对通用PC计算机的多种外设驱动支持
    • 基于OS无关内核组件设计,实现并扩展对virtio spec的多种virtio外设驱动支持
    • 基于OS相关/无关内核组件设计,实现并协程/线程/进程/扩展调度(linux支持的各种调度算法等)/内存分配(minmalloc等)/虚存管理/文件系统(ext4等)/网络协议栈/同步互斥/中断处理/驱动 的内核组件设计/优化/完善
    • 扩展带Verus(或Prusti/Kani等)安全验证属性的OSkernel相关/无关的内核组件(如上所示的各种内核组件)
    • 支持用户态中断的新型操作系统内核设计(在研的学术科研项目)
    • 使用最新的LLM/AI技术进行设计OS kernel等(也可以是设计CPU/Compiler等探索)相关的研究
      • 采用智能翻译等完成rCore Tutorial <--> uCoreTutorial,即实现两种OS的相互翻译
      • 采用MoE/Multi-Agent Collaboration或最新的LLM API/talk等自动化完成一个教学型的OS Kernel/Compiler

    注:上述内容绝大部分都有前期工作可以借鉴,也有同学/工程师在做,在下面的参考资源中大部分有提及,并可在前期的基础上进一步改进完善,可进一步联系助教和老师了解相关题目的细节。同学们也可提出自己的题目,并与老师交流,得到老师同意后可开展。

    可作为大实验项目的参考和可选项

    2023年春季OS课大实验完成项目(代码,文档,slide等)和往年OS相关开源毕设等

    • https://shimo.im/docs/QTPRT8h8jyGQCqkJ/read

    上面有2023春季学期操作系统课程大实验的参加者和做的题目。 每位同学做的是某个操作系统的某个组成部分或一个比较完整的系统软件,如操作系统/hypervisor等。题目来于自科研项目,开源项目或自己的想法(需要与老师交流并得到老师的认可)等。

    开源实验小项目(也可作为本科或研究生毕设课题)

    • https://github.com/orgs/rcore-os/discussions/categories/ideas

    ArceOS相关

    • https://github.com/rcore-os/arceos 源代码仓库
    • https://oslearning365.github.io/arceos-tutorial-book/ 还未完成,仅供参考
    • http://record-1257797626.cos.ap-beijing.myqcloud.com/cicvedu/100476113/1708953075907.mp4 该视频在第27分钟开始讲解了arceos的基本概念和编译运行等。

    参考资源

    系统工具

    面向Rust软件的安全验证工具

    面向操作系统内核的Fuzzing工具

    中间件

    • 基于Rust的DDS(用于机器人/工业控制/自动驾驶领域的中间件)

    kernel&hypervisor

    这是部分可供参考的OS&Hypervisor

    参考:与OS无关的kernel components

    这是部分与OS无关的kernel components,有一些不一定特别完善,供参考

    参考:基于LLM/AI的软硬件开发

    其他一些与还未归类的OS无关的kernel components,OS等所在网址

    1. 多种语言协同构造的 OS

    已有工作:

    1. TypeKernel:三位六字班同学在2019年OS+编译专题训练课上的联合实验。用Haskell语言构造了一个用于编写OS的DSL,将其编译为C语言子集(C----),并实现了一个基于UEFI的Demo。

    参考资料:

    1. Typekernel给后来者的话

    2. 为 rcore 实现更多 Linux 系统调用

    主要目标:

    • 在 rcore 上扩展 Linux 系统调用支持,以支持更多应用。可以考虑实现或者完善:信号机制、TTY、用户和群组权限管理、procfs和devfs。

    已有工作:

    1. 在 OS2019 大实验中,王润基和陈嘉杰合作在 rCore 中实现了 Linux ABI,支持运行原生的 Linux 程序(基于 musl libc,不支持 glibc)。目前 rCore 中已经实现了60多个 Linux Syscall(+20多个空实现),支持运行:Busybox, GCC, Nginx, Redis, Rustc 等实际应用。
    2. Biscuit 是 MIT 用 Golang 编写的类 Unix OS,相关论文发表在 OSDI2018 上。他们实现了58 个 Syscall(不完全兼容Linux),支持运行 Nginx 和 Redis(经过修改,基于他们自己的 libc)。性能测试与 Linux 对比,只慢了 5%-10%。我们尝试在 rCore 上重复这一工作,但最终由于真机调试过于困难而作罢。

    参考资料:

    1. rCore 对标 Biscuit OS 进行真实应用/网卡的性能测试
    2. rCore 毕业论文
    3. 在 rCore 中实现 epoll
    4. 在 rCore 中实现 SystemV 信号量和共享内存
    5. 在 rCore 中实现 Framebuffer 运行 mgba

    3. 用 RUST 重写 zircon(zCore)

    主要目标:

    • 基于 rCore 现有基础,用 Rust 重新实现 Zircon 微内核。
    • 根据文档描述,实现相应的内核对象和系统调用,目标是能运行到 shell。
    • 在新项目中尝试新技术、积累经验,用来重构和改进 rCore。

    已有工作:

    1. 潘庆霖在 OSTrain2019 大实验中对 Fuchsia 进行了完整的调研和分析。
    2. 随后王润基在寒假期间尝试搭起了 zCore 项目框架,目前已实现了 8 个 syscall 和诸多内核对象,还剩 50 多个。
    3. zCore 尝试的新技术包括:抽象出 HAL 以实现纯用户态开发和测试,应用 async 实现内核任务管理……

    参考资料:

    1. Fuchsia 调研项目 wiki分析文档
    2. zCore 项目仓库
    3. Fuchsia 源码及官方文档
    4. Fuchsia 中文社区

    PS:

    • 此项目是某位助教哥哥的毕设课题,和另一位助教哥哥本学期的个人娱乐项目:)))

    4. 用 RUST 实现 KVM 功能

    主要目标:

    • 实现一个简单的 VMM(虚拟机监控程序)
    • 对外提供 Linux KVM 或 Zircon Hypervisor 接口
    • 尝试利用 x86 VT-x、RISCV H 扩展或 ARM 相关指令集
    • 能够同时运行多个 rCore

    参考资料:

    1. JailHouse:Linux-based Hypervisor
    2. RVirt:MIT 用 Rust 写的 RISCV Hypervisor
    3. Linux KVM, Apple Hypervisor Framework

    5. RISCV 用户态中断

    主要目标:

    • 利用 RISCV 用户态中断机制,改进 OS 以提升 IPC 及 IO 性能。

    参考资料:

    1. RISCV 特权级手册

    6. 重构 rcore 内核组件形成 OS-Kit

    主要目标:

    • 将 rCore 拆分成独立可复用的 crates,可组合成多种特定 OS

    已有工作:

    • 将庞大的 OS 拆分成一个个独立的模块,一直是 rCore 开发的指导思想和终极目标。不过现实中工程总是十分 dirty 的,需要不断地在优雅设计和实际产出中作出妥协。

    参考资料:

    7. 为 rcore 适配树莓派4(ARM64)

    主要目标:

    • 让 rCore 支持树莓派4物理硬件
    • 完善与改进已有的树莓派3驱动(SD卡、音频、GPU),或添加新驱动支持(USB、网卡、蓝牙)

    参考资料:

    1. rCore 的 ARM64 和树莓派3移植
    2. 在 rCore 中支持树莓派3声卡
    3. 在 rCore 中支持树莓派3 SD卡
    4. 在 rCore 中移植 VideoCore IV 驱动

    8. 为 rcore 适配基于 rv64 的物理硬件(如 K210、FPGA+Rocket Chip等)

    9. 形式化验证的OS

    10. 基于 rcore 的网络路由器

    主要目标:

    • 完善多网卡支持,实现系统路由表和内置的网络功能,或者给用户态提供一个类似PCAP的接口,然后编写一个软路由
    • (可选)在计网联合实验的成果上继续
    • (可选)优化 10G 网卡驱动(ixgbe)的稳定性和性能,并且在 PC 机上运行

    参考资料:

    1. FPGA 上运行 RISC-V rCore 构建路由器
    2. 计网联合实验

    11. 改进rust语言的async on no-std

    12. 内核态的rust std实现

    13. 基于Labeled RISCV的OS kernel支持

    参考资料:

    1. 为 rCore 适配 PARD / Labeled RISCV

    14. 在 rcore 中支持 container

    1. 自由选择 OS 相关的论文、改进结果

    参考资料

    1. 为 rcore 添加 USB 驱动支持

    主要目标:

    • 在 rcore 中实现 USB 协议栈
    • 支持简单的 USB 设备,如 USB 键盘/鼠标、U 盘读写
    • 能够在至少一种真实硬件上演示(树莓派、PC)

    参考资料:

    1. https://github.com/cfgbd/rustos/tree/master/usb
    2. 在 rCore 中移植 USB 驱动的尝试

    大实验可选项目


    ArceOS: 组件化操作系统的初步探索

    贾越凯 陈渝

    清华大学 计算机系

    2023/3/15


    背景

    • 为什么要做组件化的 OS?
      • 可形成不同特征/形态/架构的操作系统内核
        • 易于定制
          • 性能
          • 安全
        • 易于开发
        • 易于复用

    ArceOS 简介

    • https://github.com/rcore-os/arceos
    • Ar-Qs, Ar-key-O-S
    • 内核组件 + 组件化的OS框架 = 不同形态的OS kernel
      • 提供了一套组件化的操作系统框架
      • 提供各种内核组件的实现
        • 各种内核组件可在没有OS kernel的情况下独立运行
          • 如filesystem, network stack等内核组件可以在裸机或用户态以库的形式运行
          • 各种设备驱动等内核组件可以在裸机上运行

    ArceOS 简介

    • https://github.com/rcore-os/arceos
    • Ar-Qs, Ar-key-O-S
    • 支持形成unikernel
      • 只运行一个用户程序
      • 用户程序与内核链接为同一镜像
      • 不区分地址空间与特权级
      • 安全性由底层 hypervisor 保证

    ArceOS 简介

    • https://github.com/rcore-os/arceos
    • Ar-Qs, Ar-key-O-S
    • 支持形成normal kernel
      • 在用户态运行多个用户程序
      • 用户程序与内核分开
      • 区分地址空间与特权级
      • 安全性由kernel保证

    ArceOS 简介

    • https://github.com/rcore-os/arceos
    • Ar-Qs, Ar-key-O-S
    • 支持形成micro kernel
      • 在用户态运行多个用户程序和OS服务
      • 用户程序和OS服务与内核分开
      • 区分地址空间与特权级
      • 安全性由内核保证

    ArceOS 简介

    • https://github.com/rcore-os/arceos
    • Ar-Qs, Ar-key-O-S
    • 支持形成hypervisor
      • 在hyervisor态运行hyerpvisor
      • 用户程序和OS内核运行在VM中,与hyerpvisor分开
      • 安全性由hypervisor保证

    ArceOS 整体架构


    ArceOS 模块

    crates

    • 与 OS 的设计无关的公共模块
      • linked_list
      • page_table
      • allocator
      • scheduler
      • drivers
      • ...

    modules

    • 与 OS 的设计紧耦合的模块
      • axtask
      • axnet
    • 对 crates 的选取与包装:
      • axalloc:
      • axdriver

    应用程序模块依赖

    • 必选模块
      • axruntime:启动、初始化、模块总体管控
      • axhal:硬件抽象层
      • axlog:打印日志
    • 可选模块
      • axalloc:动态内存分配
      • axtask:多任务 (线程)
      • axdriver:设备驱动 (如网络、磁盘)
      • axnet:网络

    应用程序配置变更

    • 示例:调度算法变更

      # apps/multitask/Cago.toml
      [dependencies]
      libax = {
          path = "../../ulib/libax",
          default-features = false,
          features = ["paging", "multitask", "sched_fifo"]
      }
      
    • 其他可能的配置变更:

      • 内存分配器
      • 网络协议栈
      • ...

    ArceOS 应用程序清单

    应用启用的功能额外依赖的模块镜像大小
    helloworld28K
    memtestalloc, pagingaxalloc44K
    multitaskpaging, multitask, sched_fifoaxalloc, axtask40K
    httpclientpaging, netaxalloc, axdriver, axnet88K
    echoserverpaging, net, multitaskaxalloc, axdriver, axnet, axtask96K

    单元测试

    • 可以只运行要测试的模块,无需运行完整的 OS
    • 在本机用户态执行,方便调试
    • 示例:axtask 单元测试


    ArceOS 相关大实验选题

    • crates 层:
      • 算法:调度、内存分配、...
        ★★☆☆☆
      • 驱动:物理网卡、SD 卡、virtio GPU, ...
        ★★★☆☆
      • 文件系统:FAT、ext4、...
        ★★★☆☆
      • 网络:支持 lwip 协议栈
        ★★★★☆
      • virtio:virtio-fs、virtio-console、...
        ★★★★☆
    • modules 层:
      • 移植:x86_64、树莓派、...
        ★★☆☆☆
      • 异步 I/O:Rust async
        ★★★☆☆

    ArceOS 相关大实验选题

    • ulib 层:
      • 兼容 Linux 系统调用接口
        ★★★☆☆
      • 支持已有复杂应用:NGINX、Redis、...
        ★★★★☆
      • 支持 Rust std
        ★★★★☆
    • 框架完善:
      • 增加更多单元测试、文档
        ★★☆☆☆
      • 支持多核、增加同步互斥功能
        ★★★☆☆
      • 支持内核中断、支持内核抢占
        ★★★★☆
      • 支持unikernel/MicroKernel/MonolithicKernel架构的OS
        ★★★★☆
    • 其他:
      • 支持游戏应用、webassembly...
        ★★★☆☆

    相关参考


    Thanks!

    2020-12-20 更新

    • 说明了子进程机制;
    • 在基础系统调用中,补充了 open/close/pipe 的相关说明;
    • 将 kill 移动到拓展系统调用;

    实现细节约定

    • 以下 int/i32 均指有符号 32 位整数。(然而出于实现方便可以进行一些调整,比如将所有的返回值都调整为 64 位有符号整数 long long/i64)

    子进程机制

    当一个进程通过 exit 系统调用或者由于出错退出之后,它应当被标记为处于僵尸(Zombie)状态,此时可以尽可能多的回收资源,但不是全部。需要等待它的父进程在 waitpid 系统调用中 wait 到这个子进程的时候,该子进程的资源(包括进程控制块 PCB)才被全部回收。

    一个进程 fork 出来的进程会成为它的子进程,一般来说它需要负责通过 waitpid 系统调用来回收子进程的资源。然而,如果一个进程在子进程还在运行的时候退出,它的所有子进程需要被转移到进程 initproc 下面。

    在内核初始化之后,需要加载唯一的用户进程 initproc,参考实现如下:

    #[no_mangle]
    fn main() -> i32 {
        if fork() == 0 {
            exec("user_shell\0");
        } else {
            loop {
                let mut exit_code: i32 = 0;
                let pid = wait(&mut exit_code);
                if pid == -1 {
                    yield_();
                    continue;
                }
                println!(
                    "[initproc] Released a zombie process, pid={}, exit_code={}",
                    pid,
                    exit_code,
                );
            }
        }
        0
    }

    可以看到它 fork 了一个子进程并运行用户终端 user_shell。它自身则是不断循环 wait,来回收那些被转移到它下面的子进程的资源。

    基础系统调用接口

    open

    syscall ID:56

    功能:打开一个文件,并返回可以访问它的文件描述符。

    C 接口:int open(char* path, unsigned int flags);

    Rust 接口:fn open(path: *const u8, flags: u32);

    参数:fd描述要打开的文件的文件名(简单起见,文件系统不需要支持目录,所有的文件都放在根目录 / 下),flags描述打开文件的标志,具体含义见备注。

    返回值:如果出现了错误则返回 -1,否则返回可以访问给定文件的文件描述符。

    可能的错误:文件不存在。

    备注:在分配文件描述符的时候,总是会选取所有可能的文件描述符中编号最小的一个。参考 xv6,打开文件的 flags 支持以下几种:

    • 如果 flags 为 0,则表示以只读模式RDONLY打开;
    • 如果 flags 第 0 位被设置(0x001),表示以只写模式WRONLY打开;
    • 如果 flags 第 1 位被设置(0x002),表示既可读又可写RDWR
    • 如果 flags 第 9 位被设置(0x200),表示允许创建文件CREATE,在找不到该文件的时候应创建文件;如果该文件已经存在则应该将该文件的大小归零;
    • 如果 flags 第 10 位被设置(0x400),则在打开文件的时候应该将该文件的大小归零,也即TRUNC

    close

    syscall ID:57

    功能:关闭一个文件。

    C 接口:int close(int fd);

    Rust 接口:fn close(fd: i32) -> i32;

    参数:fd描述一个文件,可能是索引节点/管道/串口。

    返回值:如果出现了错误则返回 -1,否则返回 0。

    可能的错误:传入的文件描述符 fd 并未被打开。

    pipe

    syscall ID:59

    功能:建立一个管道,用于当前进程及其子进程之间的通信。

    C 接口:int pipe(int pipefd[2]);

    Rust 接口:fn pipe(pipefd: &mut [i32]) -> i32;

    参数:pipefd描述一个大小为 2 的 fd 数组,前一项为管道输入端的 fd,后一项为管道输出端的 fd。

    返回值:如果出现了错误则返回 -1,否则返回 0。

    可能的错误:传入的地址 pipefd 不合法;

    备注:在分配文件描述符的时候,总是会选取所有可能的文件描述符中编号最小的一个。

    read

    syscall ID:63

    功能:从文件中读取一段内容到内存中的缓冲区。

    C 接口:int read(int fd, char *buf, int len);

    Rust 接口: fn read(fd: i32, buf: *mut u8, len: i32) -> i32;

    参数:fd描述当前进程需要访问的文件,buf表示保存文件中读到的数据的缓冲区的地址,len表示最大的读取字节数。

    返回值:如果出现了错误则返回 -1,否则返回实际读到的字节数。

    可能的错误:传入的fd不合法;

    备注:该 syscall 的实现可能是阻塞的。

    write

    syscall ID:64

    功能:从内存中的缓冲区写入一段内容到文件。

    C 接口:int write(int fd, char *buf, int len);

    Rust 接口:fn write(fd: i32, buf: *mut u8, len: i32) -> i32;

    参数:fd描述当前进程需要访问的文件,buf表示保存即将写入文件的数据的缓冲区的地址,len表示最大的写入字节数。

    返回值:如果出现了错误则返回 -1,否则返回实际写入的字节数。

    可能的错误:传入的fd不合法;

    备注:该 syscall 的实现可能是阻塞的。

    exit

    syscall ID:93

    功能:退出当前进程。

    C 接口:int exit(int status);

    Rust 接口:fn exit(status: i32) -> i32;

    参数:status描述当前进程的返回值,并应当由其父进程捕获到。

    返回值:正常情况下应不会返回。请在调用 exit 之后加入 panic 语句来确保这一点。

    可能的错误:触发了调用 exit 之后的 panic;

    sleep

    syscall ID:101

    功能:将当前进程休眠一段时间。

    C 接口:int sleep(int n);

    Rust 接口:fn sleep(n: i32) -> i32;

    参数:n描述将当前进程休眠多少个时间单位,采用哪种时间单位待定,可选项:时间片个数/毫秒数/ CPU 时钟周期数,请帮忙确定。暂定为毫秒数。

    返回值:总是返回 0。

    可能的错误:无。

    备注:该 syscall 的实现可能是阻塞的。

    yield

    syscall ID:124

    功能:主动交出当前进程的 CPU 使用权,从而使得 CPU 可以执行其他进程。

    C 接口:int yield();

    Rust 接口:fn yield() -> i32;

    参数:无参数。

    返回值:总是返回 0。

    可能的错误:无。

    gettime

    syscall ID:169

    功能:获取当前时间。

    C 接口:int gettime(unsigned long long* time);

    Rust 接口:fn gettime(time: *mut u64) -> i32;

    参数:将当前时间保存在地址time处。时间的单位待定,可选为毫秒数/ CPU 时钟周期数,请帮忙确定。暂定为毫秒数。

    返回值:总是返回 0。

    可能的错误:无。

    getpid

    syscall ID:172

    功能:获取当前进程的进程 ID。

    C 接口:int getpid();

    Rust 接口:fn getpid() -> i32;

    参数:无参数。

    返回值:返回当前进程的进程 ID。

    可能的错误:无。

    fork

    syscall ID:220

    功能:生成一个子进程,其地址空间与当前进程(也称父进程)完全相同,且和父进程一样,回到用户态之后都是从系统调用的下一条指令开始执行。

    C 接口:int fork();

    Rust 接口:fn fork() -> i32;

    参数:无参数。

    返回值:如果出现了错误则返回 -1;否则对于父进程,返回子进程的进程 ID;对于子进程则返回 0。

    可能的错误:生成子进程的过程中会分配新的物理页框,如果物理页框不足则应该撤销所有操作,放弃生成子进程并返回错误;

    exec

    syscall ID:221

    功能:替换当前进程的地址空间为一个程序,且返回用户态之后从该程序的入口点开始执行。还需要初始化当前进程的运行栈支持带有参数。

    C 接口:int exec(char *file, char *argv[]);

    Rust 接口:fn exec(file: *const u8, argv: *const *const u8);

    参数:file表示将要替换到的程序的文件名,argv表示新程序的执行参数。

    返回值:如果出现了错误则返回 -1;否则不应该返回。

    可能的错误:file不存在;需要分配物理页框而物理页框不足,此时应该撤销所有操作并返回错误。

    waitpid

    syscall ID:260

    功能:当前进程等待一个子进程结束,并获取其返回值。

    C 接口:int waitpid(int pid, int *status);

    Rust 接口: fn waitpid(pid: i32, status: *mut i32) -> i32;

    参数:pid表示要等待结束的子进程的进程 ID,如果为 0 的话表示等待任意一个子进程结束;status表示保存子进程返回值的地址,如果这个地址为 0 的话表示不必保存。

    返回值:如果出现了错误则返回 -1;否则返回结束的子进程的进程 ID。

    可能的错误:如果在调用时找不到符合要求的子进程(当前进程没有子进程或者 pid!= 0 而当前进程没有 pid 相符的子进程)则返回错误;传入的地址 status 不为 0 但是不合法;

    备注:该 syscall 的实现可能是阻塞的。

    拓展系统调用接口

    kill

    syscall ID:129

    功能:杀死一个进程。

    C 接口:int kill(int pid);

    Rust 接口:fn kill(pid: i32) -> i32;

    参数:pid表示要杀死的进程的进程 ID。

    返回值:如果杀死当前进程的话则不返回。如果出现了错误则返回 -1,否则返回 0。

    可能的错误:尝试杀死初始用户进程 initproc;不存在对应 pid 的进程;

    备注:在 kill 一个目前正处于阻塞状态的进程的时候情况比较复杂,随着其处于的阻塞状态的不同,需要讨论更加明确的语义,请帮忙一同确定。

    参考内容

    xv6 实验指导书

    之前总结的用户程序支持文档

    备注1被加粗的 syscall 可能导致阻塞
    备注2未来考虑引入的 syscall:dup/chdir/mkdir/mknod/fstat/kill/mmap
    sys_write第二章出现第六章需要支持管道的写入,轮询实现第八章至少对串口、管道改成阻塞实现
    sys_exit第二章出现,与应用程序交互第五章重新实现,与进程交互
    sys_yield第三章出现,与应用程序交互第五章重新实现,与进程交互
    sys_get_time(not in xv6)第三章出现
    sys_sbrk第四章出现,用于实现用户库的堆内存管理
    sys_fork第五章出现
    sys_wait第五章出现,非阻塞,需要在用户库轮询第八章改成阻塞实现
    sys_exec第五章出现
    sys_getpid第五章出现
    sys_sleep第五章出现,用户看来阻塞,内核实现通过轮询,不满足条件时任务切换第八章改成阻塞实现
    sys_read第五章出现,作用是用户终端从串口读取命令,用户看来阻塞,内核实现通过轮询,不满足条件时任务切换第六章需要支持管道的读取,轮询实现第八章至少对串口、管道改成阻塞实现
    sys_pipe第六章出现
    sys_close第六章出现,作用是关闭管道
    sys_dup第六章出现,作用是替换进程的标准输入/输出为管道输入/输出
    sys_open第七章出现
    sys_lseek第七章出现(可选)
    sys_link第七章出现(可选)
    sys_unlink第七章出现(可选)
    sys_stat第七章出现(可选)

    回顾

    • 给应用提供"好"的服务
      • 提高性能、简化开发、加强安全
      • 应用的执行环境持续进化
      • 操作系统功能持续增强

    回顾

    • lec1: UNIX/Linux APP

      • "系统调用"

      • 例子,用C语言,来自UNIX(例如Linux、macOS、FreeBSD)。

            fd = open("out", 1);
            write(fd, "hello\n", 6);
            pid = fork()
        

    回顾

    • lec2: 裸机程序:LibOS
      • 软硬件启动,函数调用,SBI调用 w:600

    回顾

    • lec3: 写 Batch OS
      • 特权级: U-Mode, S-Mode
      • 特权级切换
      • 陷入上下文
      • 编译多应用+OS的镜像
      • 加载并执行应用 bg right 100%

    回顾

    • lec4-1: MultiProg OS
      • 任务的概念
      • 任务的设计实现
      • 协作/抢占式调度
      • 任务上下文
      • 陷入上下文
      • 切换任务
      • 切换特权级 bg right:60% 100%

    回顾

    • lec4-2: TimeSharing OS
      • 中断
      • 中断响应
      • 协作/抢占式调度
      • 陷入上下文
      • 任务上下文
      • 切换任务
      • 切换特权级 bg right:60% 100%

    回顾 App/OS内存布局

    • .text: 数据段
    • 已初始化数据段.rodata:只读的全局数据(常数或者是常量字符串)、.data:可修改的全局数据。
    • 未初始化数据段 .bss
    • 堆 (heap)向高地址增长
    • 栈 (stack)向低地址增长 bg right 120%

    回顾

    • lec5: AddrSpace OS
    • 地址空间
    • 物理地址
    • 页表
    • 陷入上下文
    • 任务上下文
    • 中断响应

    bg right:60% 100%


    回顾 App/OS内存布局

    • 应用地址空间
    • 内核地址空间
    • 切换任务
    • 切换特权级
    • 切换页表

    bg right:65% 100%


    回顾

    • lec7: Process OS
      • Process
        • Trap
        • Task
        • Address Space
        • state
        • relations
        • exit code bg right:65% 100%

    回顾

    • lec7: Process OS
      • fork
      • exec
      • exit
      • wait

    bg right:70% 100%


    回顾

    • lec7: Process OS
      • PCB bg right:70% 100%

    回顾

    • lec9: Filesystem OS

    bg right:70% 100%


    回顾

    • lec9: Filesystem OS

    bg right:70% 100%


    回顾

    • lec9: Filesystem OS

    bg right:70% 100%


    回顾

    • lec10: IPC OS

    bg right:70% 100%


    回顾

    • lec10: IPC OS bg right:70% 100%

    操作系统知识点

    每个知识领域应有本领域的范围说明,包括领域知识的组成(知识单元)以及预期的能力培养目标,能力点的检测方法(实验设计的依据)

    操作系统知识领域包含以下内容:

    1. 操作系统概述:操作系统定义、抽象性与特征、操作系统历史、操作系统的系统结构、计算机硬件与操作系统的关系
    2. 中断:中断、异常、系统调用
    3. 内存管理 :连续物理内存分配、非连续物理内存分配
    4. 虚拟存储:局部性原理、虚拟存储基本概念、缺页异常、局部页面置换算法、全局页面置换算法
    5. 进程与线程:概念、运行状态、执行过程、控制管理、*协程
    6. 调度:单处理器调度,多处理器调度
    7. 同步互斥:基于软件的同步互斥方法、基于硬件的同步互斥方法、信号量、管程、经典同步问题
    8. 死锁:死锁概念、死锁处理方法、并发错误检测
    9. 进程间通信:信号、管道、消息队列、共享内存、*RPC、*APC、*异步机制
    10. 文件系统:文件系统基本概念、文件组织、文件系统实例
    11. 输入输出管理:输入输出特征、输入输出调度、输入输出子系统实例
    12. *操作系统进展:OS内部安全、操作系统与编程语言、操作系统与系统结构(系统虚拟化)、异步编程

    1. 操作系统概述

    核心学习成效

    1. 掌握操作系统的定义:能从软件角度、资源管理角度定义操作系统及其所属范围
    2. 掌握操作系统的抽象:理解进程、文件、地址空间的抽象对象以及与硬件关系
    3. 掌握操作系统的特征:理解并发/并行、共享、虚拟化、异步的含义
    4. 掌握操作系统的历史:理解单用户系统、批处理系统、多道程序系统、分时系统、个人计算机、SMP系统、VMM系统、分布式系统中的操作系统特征
    5. 掌握操作系统的架构:理解简单单体/分层单体/复杂单体/微/外/VMM kernel的特点
    6. 掌握 计算机硬件与操作系统的关系:理解硬件与操作系统之间的interface和各自的主要工作

    能力检测方法

    1. 能分析硬件的哪些功能能够给操作系统提供哪些支持
    2. 能对一个计算机系统中的软硬件的特征、区别、关系、运行状态进行清晰判断
    3. 对操作系统有一个完整的总体分析

    2. 中断

    核心学习成效

    1. 中断/异常/系统调用:从硬件产生方式和处理的角度和软件处理的角度理解中断/异常/系统调用的产生缘由,产生方式与处理过程
    2. 三者关系、区别与作用:理解 中断、异常和系统调用的关系,区别和作用
    3. 对于RISC-V或x86等硬件,理解中断、异常和系统调用的设置,处理的细节

    能力检测方法

    1. 理解硬件的中断、异常和系统调用的设置过程,且能完成针对具体硬件的操作系统的相关实验
    2. 以时钟中断、除零异常、基本系统调用为例,理解RISC-V或x86等硬件的中断、异常和系统调用的处理过程,且能完成针对具体硬件的操作系统相关实验
    3. 理解特权级:用户态和内核态的区别,以及硬件和操作系统在转换特权级的注意事项和具体实现

    3. 内存管理

    核心学习成效

    1. 地址空间:理解从源程序-->编译-->链接-->加载-->动态链接-->执行过程中地址的不同定义与变化
    2. 连续内存分配算法:first-fit, best-fit,worst-fit,buddy, *slab及其一些变种算法
    3. 内碎片和外碎片
    4. 连续内存分配方法:段地址,段式管理,页地址,页式管理,反置页表管理,段页式管理
    5. TLB与页式管理的关系

    能力检测方法

    1. 能够理解和运用 first-fit, best-fit,worst-fit,buddy算法进行内存分配与释放
    2. 能够基于段式、页式、段页式进行虚实地址转换和虚存-->物理内存的访存操作
    3. 理解在段式、页式、段页式内存访问过程中,软硬件的分工
    4. 理解硬件的段/页式内存的处理过程,且能完成针对具体硬件的段页式操作系统相关实验

    4. 虚拟存储

    核心学习成效

    1. 理解虚拟存储的前提条件:局部性,掌握虚拟存储相关的覆盖/交换技术,掌握实现虚拟存储
    2. swap的基本原理和在实现机制
    3. 局部页置换算法:理解OPT、FIFO、LRU、LFU、CLOCK、改进的CLOCK等页置换算法和特征,理解belady异常,无belady异常的属于stack算法类的页置换算法
    4. 全局页置换算法:理解工作集、常驻集、缺页率的概念,内存抖动问题,负载控制方法,理解工作集置换算法、缺页率置换算法

    能力检测方法

    1. 能够理解和运用 fOPT、FIFO、LRU、LFU、CLOCK、改进的CLOCK、工作集置换算法、缺页率置换算法等页置换算法进行内存替换
    2. 能判断某个置换算法是否属于stack算法类
    3. 理解硬件的段/页式内存的处理过程,且能完成针对具体硬件的虚拟存储操作系统相关实验

    5. 进程与线程

    核心学习成效

    1. 理解进程/线程的概念,进程与线程的关系与区别,进程/线程与程序的关系和差异,进程/线程控制块的组成
    2. 理解进程/线程的生命周期和状态模型
    3. 理解管理进程的过程:创建 fork、执行 exec、等待 wait、停止 exit
    4. 理解进程管理与内存管理的关系

    注:可根据课程时长:讲解协程,理解不同的CPU复用方式;讲解2019 HotOS论文“A fork() in the road”,理解fork的利弊;讲解meltdown漏洞,从另外的角度理解arch/页表/进程/信息之间的关系

    能力检测方法

    1. 能描述进程生命周期的状态转换关系和时机
    2. 能理解和掌握进程管理的4个重要过程
    3. 理解硬件的内核线程/用户进程的管理过程,且能完成针对具体硬件的线程/进程操作系统相关实验

    6. 调度

    核心学习成效

    1. 理解调度策略、准则和评价目标
    2. 掌握调度算法:先来先服务算法、短进程优先算法(抢占/不抢占)、最高响应比优先算法、最高响应比优先算法、多级反馈队列算法
    3. 理解实时任务的特点,RM/EDF实时调度算法,优先级反转问题和解决方法:优先级继承,优先级天花板
    4. *了解多处理器调度相关的调度策略 O(1),CFS,BFS(基于Linux)

    能力检测方法

    1. 理解不同调度算法的差异,能够在给定的条件下(时间片,算法名称,进程执行时间等),进行具体调度算法的推演
    2. 理解并求解优先级反转问题
    3. 理解硬件的内核线程/用户进程的调度执行过程,且能完成针对具体硬件的调度框架/调度算法操作系统相关实验

    7. 同步互斥

    核心学习成效

    1. 理解原子操作、临界区的含义,临界区的访问规则
    2. 掌握临界区的基本实现方法:禁用中断、纯软件方法:Peterson算法、Dekkers算法、Eisenberg&McGuire算法;以及它们的局限性
    3. 掌握临界区的硬件原子指令支持下的实现方法:理解T&S,EXCHG等硬件原子指令,以及基于此指令的临界区方法
    4. 掌握信号量:特征,具体实现,易错情况
    5. 掌握管程:特征,具体实现,条件变量的特征与实现,易错情况,不同类型的管程:Hansen管程,Hoare管程,Mesa管程
    6. 掌握用信号量或管程来解决各种同步互斥问题:哲学家问题、理发师问题、不同优先策略的读者写者问题等

    能力检测方法

    1. 能指出存在bug的临界区方法的问题所在和后果
    2. 能用信号量或管程来解决各种同步互斥问题(伪代码形式)
    3. 理解硬件的硬件原子指令,且能完成针对具体硬件的信号量、管程的实现和基于这两种机制的同步互斥问题求解的操作系统相关实验

    8. 死锁

    核心学习成效

    1. 掌握死锁的抽象表示方法
    2. 掌握死锁的必要条件
    3. 掌握死锁的的处理方法:死锁预防(Deadlock Prevention)、死锁避免(Deadlock Avoidance)、死锁检测和恢复(Deadlock Detection & Recovery)
    4. 掌握银行家算法,理解出现死锁与死锁安全的关系与区别

    能力检测方法

    1. 能用伪代码或形式描述银行家算法的设计实现
    2. 理解硬件,且能实现针对具体硬件的死锁处理方法的操作系统相关实验

    9. 进程间通信

    核心学习成效

    1. 掌握IPC的基本原理
    2. 掌握PIPE机制和使用方法
    3. 掌握signal机制和使用方法
    4. 掌握message机制和使用方法
    5. 掌握shmem机制和使用方法

    (注:可根据课程时长:讲解RPC、APC、异步编程)

    能力检测方法

    1. 能用伪代码或文字形式描述PIPE/signal/message/shmem的实现机制
    2. 理解硬件,且能实现针对具体硬件的各种IPC机制的操作系统相关实验。
    3. *了解binder,dbus等IPC机制

    10. 文件系统

    核心学习成效

    1. 理解文件系统概念:文件系统、目录、文件的定义,属性与基本功能;文件描述符,文件控制块的特征与功能
    2. 理解目录与文件访问操作:文件目录操作:搜索文件,创建文件,删除文件,重命名文件,遍历路径,修改文件。
    3. 理解文件别名的两种实现方式:软链接、硬链接
    4. 理解虚拟文件系统的作用
    5. 理解文件系统的基本安全管理措施
    6. 理解日志文件系统的基本原理
    7. 理解基于fat/inode机制的文件系统的设计与实现

    能力检测方法

    1. 能用伪代码或文字形式描述文件系统的组成和基本功能
    2. 能判断文件系统相关的系统调用在内核中的具体行为
    3. 理解硬件和磁盘等外设,且能实现针对具体硬件的文件系统机制的操作系统相关实验。

    11. 输入输出管理

    核心学习成效

    1. 理解外设驱动的基本功能
    2. 理解不同外设的特点和相应的处理模式:轮询、中断、DMA等
    3. 理解一个具体外设的中断响应过程
    4. 理解各种磁盘调度算法

    能力检测方法

    1. 能用伪代码或文字形式描述磁盘调度算法,并能在给定条件下,进行算法的推演。
    2. 理解硬件和各种等外设,且能实现针对具体外设的设备驱动的操作系统相关实验。

    12. *操作系统进展

    核心学习成效

    1. 了解操作系统与计算机系统结构的关系和影响:系统虚拟化VMM的方法、CPU虚拟化机制、MEM虚拟化机制、I/O虚拟化机制
    2. 了解操作系统与编程语言的相关关系和影响:非C语言编写操作系统的特点、GO语言与操作系统、RUST语言与操作系统
    3. 了解异步编程的框架和实现方法:在用户态和内核中应用、设计和实现基于事件的异步编程方法

    能力检测方法

    1. 能够阅读、分析测试具体的VMM, golang based OS, RUST-based OS,内核级异步编程基础架构