资料来源
Gitee 链接
B站视频链接
RISC-V 开放架构设计之道
RVOS
1 操作系统的定义
- OS(Operating System)是一组系统软件程序
- 主管并控制计算机操作、运用和运行硬件、软件资源
- 提供公共服务来组织用户交互
- 有广义和狭义之分
- 狭义:内核
- 广义:发行包=内核 + 一组软件
分类 特点 应用场景 RISC-V ISA对其支持 裸机系统(Bare Metal) 非常小,没有明显的分层设计,没有通用性。通常为单任务+中断处理 微型控制器,简单外设,简单实时任务 简单的 Machine 模式支持 实时操作系统(Real-Time Operating Systems) 中等规模,支持多任务,具备一定的通用性,和通用性相比更强调实时性 比较复杂的多任务和实时场景,丰富的外设 Machine + User;或许需要支持物理内存保护(Physical Memory Protection,PMP)。 高级操作系统(Rich Operating Systems) 大型规模,强调用户体验或者复杂通用性 智能手持设备,PC工作站,云计算服器… Machine + Supervisor + User,需要支持虚拟内存机制
2 开源 RTOS
3 RVOS 系统引导
系统引导过程介绍:
- QEMU-virt 地址映射源码
- riscv-boot.c源码
- DRAM:Kernel(0x8000 0000 ~ 0x8800 0000)
- ROM:Bootloader(0x0000 0000 ~ 0x0000 F000)
引导程序要做那些事情?
- 如何判断当前hart是不是第一个hart?
- CSRRW(Atomic Read/Write CSR)
- CSRRS(Atomic Read and Set Bits in CSR)
- CSR 读取 Hart ID register(mhartid)
- 包含了运行当前指令的 hart 的 ID
- 多个hart的ID必须是唯一的,且必须有一个hart的ID值为 0(第一个hart的ID)
- Wait for Interrupt instruction(WFI)是RISC-V架构定义的一条休眠指令。当处理器执行到WFI指令后,将会停止执行当前的指令流,将进入一种空闲状态。这种空闲状态可以被称为"休眠"状态,直到处理器接收到中断
- 如何初始化栈?
- 如何跳转到 C 语言的执行环境?
UART(Universal Asynchronous Receiver and Transmitter)
- 串行:相对于并行,串行是按位来进行传递,即一位一位的发送和接收
- 波特率(baud rate),每秒传输的二进制位数,单位为 bps(bit per second)
- 异步:相对于同步,异步数据传输的过程中,不需要时钟线,直接发送数据,但需要约定通讯协议格式
- 全双工:相对于单工和半双工,全双工指可以同时进行收发两方向的数据传递
- 虚拟UART:NS16550-UART0(0x1000 0000 ~ 0x1000 0100)
- 由于之前对UART比较了解,故此处未进行展开记录
4 内存管理
对内存进一步的管理,实现动态的分配和释放
内存管理分类:
- 自动管理内存-栈(stack)
- 静态内存-全局变量/静态变量
- 动态管理内存-堆(heap)
内存映射表(Memory Map)
Linker Script 链接脚本
- GNU Id使用Linker Script来描述和控制链接过程
- Linker Script是简单的纯文本文件,采用特定的脚本描述语言编写
- 每一个Linker Script中包含有多条命令(Command)
- 注释采用"/“和”/"括起来
- gcc -T os.ld …
MOMORY用于描述目标机器上内存区域的位置、大小和相关
SECYIONS告诉链接器如何将input sections 映射到output sections,以及如何将output sections放置在内存中
- .=0x10000; // 从地址0x10000开始存放后面的节
- .text:{*(.text)} // 将所有.text放置在这里
section-command除了可以是对out section的描述外还可以是符号赋值命令等其他形式
- PROVIDE(symbol = expression)
- PROVIDE(_text_start=.)
- 可以在Linker Script 中定义符号(Symbols)
- 每个符号包括一个名字(name)和一个对应的地址值(address)
- 在代码中可以访问这些符号,等同于访问一个地址
通过符号获得各个output section在内存中的地址范围
从Linker Script 到 Code
实现Page级别的内存分配和释放
- *void page_alloc(int npages)
- *void page_free(void p)
- 数据结构设计
- 链表方式
- 数组方式:内存管理信息+用户内存
- Page描述符数据结构设计
5 上下文切换和协作式多任务
任务 Task
多任务(Multitask)系统的分类
- 协作式多任务(Cooperative Multitasking):协作式环境下,下一个任务被调度的前提是当前任务主动放弃处理器
- 抢占式多任务(Preemptive Multitasking):抢占式环境下,操作系统完全决定任务调度方案,操作系统可以剥夺当前任务对处理器的使用,将处理器提供给其他任务
任务上下文(Context)- 关键函数(switch_to):将CPU ra(返回地址)寄存器的值写入内存 and 将内存对应ra的值写入CPU
- 基本和RT-Thread线程调度相同
6 Trap 和 Exception
控制流(Control Flow) 和 Trap
- 控制流(Control Flow):正常执行过程
- branch,jump
- 异常控制流(Exception Control Flow,简称ECP):中断函数或异常处理
- exception
- interrupt
- RISC-V 把 ECP 统称为 Trap
RISC-V Trap处理器中涉及的寄存器
寄存器 用途说明 mtvec(Machine Trap-Vector Base-Address) 它保存发生异常时处理器需要跳转到的地址 mepc(Machine Exception Program Counter) 当 trap 发生时,hart 会将发生 trap 所对应的指令的地址值(pc)保存在 mepc 中 mcause(Machine Cause) 当 trap 发生时,hart 会设置该寄存器通知我们 trap 发生的原因 mtval(Machine Trap Value) 它保存了 exception 发生时的附加信息:譬如访问地址出错时的地址信息、或者执行非法指令时的指令本身,对于其他异常,它的值为 0。 mstatus(Machine Status) 用于跟踪和控制 hart 的当前操作状态(特别地,包括关闭和打开全局中断) mscratch(Machine Scratch) Machine 模式下专用寄存器,我们可以自己定义其用法,譬如用该寄存器保存当前在 hart 上运行的 task 的上下文(context)的地址 mie(Machine Interrupt Enable) 用于进一步控制(打开和关闭)software interrupt/timer interrupt/external interrupt mip(Machine Interrupt Pending) 它列出目前已发生等待处理的中断
mtvec(Machine Trap-Vector Base-Address)
- BASE:trap入口函数的基地址,必须保证四字节对齐
- MODE:进一步用于控制入口函数的地址配置方式:
- Direct:所有的exception和interrupt发生后PC都跳转到BASE指定的地址处
- Vectored:exception处理方式同Direct;但interrupt的入口地址以数组方式排列
mepc(Machine Exception Program Counter)
- 当trap发生时,pc会被替换为mtvec设定的地址,同时hart会设置mepc为当前指令或者下一条指令的地址,当我们需要退出trap时可以调用特殊的mret指令,该指令会将mepc中的值恢复到pc中(实现返回的效果)
- 在处理trap的程序中,我们可以修改mepc的值达到改变mret返回地址的目的
mcause(Machine Cause)
- 当trap发生时,hart会设置该寄存器通知我们trap发生的原因
- 最高位Interrupt为 1 时标识了当前trap为Interrupt,否则为exception
- 剩余的Exception Code 用于标识具体的interrupt或者exception的种类
mtval(Machine Trap Value)
- 当trap发生时,除了通过mcause可以获取exception的种类code值外,hart还提供了mtval来提供exception的其他信息来辅助我们执行更进一步的操作
- 具体的辅助信息由特定的硬件实现定义,RISC-V规范没有定义具体的值。但规范定义了一些行为,譬如访问地址出错时的地址信息、或者执行非法指令时的指令本身等
mstatus(Machine Status)
- xIE(x=M/S/U):分别用于打开(1)或者关闭(0) M/S/U 模式下的全局中断。当trap发生时,hart会自动将xIE设置为0
- xPIE(x=M/S/U):当trap发生时用于保存trap发生之前的xIE值
- xPP(x=M/S/U):当trap发生时用于保存trap发生之前的权限级别值,注意没有UPP,MPP[1:0]有3中情况:M-M、U-M、S-M
- 其他标志位涉及内存访问权限、虚拟内存控制等
RISC-V Trap处理流程
- Trap 初始化:设置函数指针
- Trap 的 Top Half:自动执行
- 把mstatus的MIE值复制到MPIE中,清除mstatus中的MIE标志位,效果是中断被禁止
- 设置mepc,同时PC被设置位mtvec(需要注意的是,对于exception,mepc指向导致异常的指令;对于interrupt,它指向被中断的指令的下一条指令)
- 根据trap的种类设置mcause,并根据需要为mtval设置附加信息
- 将trap发生之前的权限模式保存在mstatus的MPP域中,再把hart权限模式更改为M(也就是说无论在任何Level下触发trap。hart首先切换到Machine模式)
- Trap 的 Bottom Half
- trap handler:软件需要做的事情
- 保存(save)当前控制流上下文信息(利用mscratch)
- 调用C语言的trap handler
- 从trap handler函数返回,mepc的值有可能需要调整
- 恢复(restore)上下文的信息
- 执行MRET指令返回到trap之前的状态
- 从 Trap 返回
- 退出trap:编程调用MRET指令
- 针对不同权限级别下如何退出trap有各自的返回指令xRET(x=M/S/U)
- 以在M模式下执行mret指令为例,会执行如下操作:
- 当前Hart的权限级别 = mstatus.MPP;mstatus.MPP = U(如果hart不支持U则为M)
- mstatus.MIE = mstatus.MPIE;mstatus.MIPE = 1
- pc = mepc
7 外部设备中断
RISC-V中断(Interrupt)的分类
- 本地(Local)中断
- software interrupt
- timer interrupt
- 全局(Global)中断
- externel interrupt
RISC-V中断编程中涉及的寄存器
- mie(Machine Interrupt Enable):打开(1)或者关闭(0) M/S/U模式下对应的External/Timer/Software中断
- mip(Machine Interrupt Pending):获取当前M/S/U模式下对应External/Timer/Software中断是否发生
RISC-V中断处理流程
- 把mstatus的MIE值复制到MPIE中,清除mstatus中的MIE标志位,效果是中断被禁止
- 当前的PC的下一条指令地址被复制到mepc中,同时PC被设置为mtvec。注意如果设置mtvec.MODE = vetcored,PC = mtvec.BASE + 4*exception-code
- 根据interrupt的种类设置mcause,并根据需要为mtval设置附加信息
- 将trap发生之前的权限模式保存在mstatus的MPP域中,再把hart权限模式更改为M
PLIC介绍
- 外部中断(external interrupt)
- Platform-Level Interrupt Controller
- Interrupt Source - PLIC - Hart
- Interrupt Source ID范围:1~53(0x35),0 预留不用
- PLIC 编程接口-寄存器
- RISC-V 规范规定,PLIC 的寄存器编址采用内存映射(memory map)方式。每个寄存器的宽度为 32-bit
- 具体寄存器编址采用 base + offset 的格式,且 base 由各个特定 platform 自己定义。针对 QEMU-virt,其 PLIC 的设计参考了FU540-C000,base 为 0x0c000000
- Priority:设置某一路中断源优先级,内存地址映射:BASE+(Interrupt-id)*4
- 每一个PLIC中断源对应一个寄存器,用于配置该中断源的优先级
- QEMU-virt 支持7个优先级。0表示对该中断源禁用中断。其余优先级,1最低,7最高
- 如果有两个中断源优先级相同,则根据中断源的ID值进一步区分优先级,ID值越小优先级越高
- Pending:用于指示某一路中断源是否发生,内存映射地址:BASE+0x1000+((interrupt-id)/32)
- 每个PLIC包含2个32位的Pending寄存器,每一个bit对应一个中断源,如果为1表示该中断源上发生了中断(进入Pending状态),有待hart处理,否则表示该中断源上当前无中断发生
- Pending寄存器中断的Pending状态可以通过claim方式清除
- 第一个Pending寄存器的第0位对应不存在的0号中断源,其值永远为0
- Enable:针对某个hart开启或关闭某一路中断源,内存映射地址:BASE+0x2000+(hart)*0x80
- 每个hart有2个Enable寄存器(Enable1和Enable2)用于针对该Hart启动或关闭某路中断源
- 每个中断源对应Enable寄存器的一个bit,其中Enable1负责控制131号中断源;Enable2负责控制3253号中断源。将对应的bit位设置为1表示使能该中断源,否则表示关闭该中断源
- Threshold:针对某个hart设置中断源优先级的阈值,内存映射地址:BASE+0x200000+(hart)*0x1000
- 每一个Hart有一个Threshold寄存器用于设置中断优先级的阈值
- 所有小于或者等于(<=)该阈值的中断源即使发生了也会被PLIC丢弃,特别的,当阈值为0时允许所有中断源上发生的中断;当阈值为7时丢弃所有中断源上发生的中断
- Claim/Complete:BASE+0x200004+(hart)*0x1000
- Claim和Complete是同一个寄存器,每个Hart一个
- 对该寄存器执行读操作称之为Claim,即获取当前发生的最高优先级的中断源ID。Claim成功后会清除对应的Pending位
- 对该寄存器执行写操作称之为Complete。所谓Complete指的是通知PLIC对该路中断的处理已经结束
采用中断方式从UART实现输入
8 硬件定时器
CLINT编程接口-寄存器
- RISC-V规范规定,CLINT的寄存器编址采用内存映射(memory map)方式
- 具体寄存器编址采用base+offset的格式,且base由各个特定platform自己定义。针对QEMU-virt,其CLINT的设计参考了SFIVE,base为0x20000000
- mtime:real-time计数器(counter),BASE+0xbff8
- 系统全局唯一,在RV32和RV64上都是64-bit,系统必须保证该计数器的值始终按照一个固定的频率递增
- 上电复位时,硬件负责将mtime的值恢复为0
- mtimecmp:timer compare register,内存映射地址:BASE+0x4000+(hart)*8
- 每个hart一个mtimecmp寄存器,64-bit
- 上电复位时,系统不负责设置mtimecmp的初值
- 当mtime >= mtimecmp时,CLINT会产生一个timer中断。如果要使能该中断需要保证全局中断打开并且mie.MTIE标志位置1
- 当timer中断发生时,hart会设置mip.MTIP,程序可以在mtimecmp中写入新的值清除mip.MTIP
硬件定时器的应用:时间管理
- 生活离不开对时间的管理;操作系统的运行也是一样
- 系统节拍(tick)
- 操作系统中最小的时间单位
- Tick的单位(周期)由硬件定时器的周期决定(通常为1~100ms)
- Tick周期越小,系统的精度越高,但开销越大
- 系统时钟
- 操作系统维护的一个整型计数值,记录着系统和启动直到当前发生的Tick总数
- 可用于维护系统的墙上时间,所以也称为系统时钟
9 抢占式多任务
CLINT编程接口-寄存器(software interrupt部分)
- MSIP:最低位和CSR mip.MSIP对应,内存映射地址:BASE+4*(hart)
- 每个hart都拥有一个MSIP寄存器
- RISC-V规范规定,Machine模式下的mip.MSIP对应到一个memory-mapped的控制寄存器。为此QEMU-virt提供MSIP,该MSIP寄存器为32-bit,高31位不可用,最低位映射到mip.MSIP
- 具体寄存器编程采用base+offset的格式,且base由各个特定platform自己定义。针对QEMU-virt,其CLINT的设计参考了SFIVE,base为0x20000000
- 对MISP写入1时触发software interrupt,写入0表示对该中断进行应答
10 任务同步与锁
- 并发指多个控制流同时执行
- 多处理器多任务
- 单处理器多任务
- 单处理器任务+中断
- 同步是为了保证在并发执行的环境中各个控制流可以有效执行而采用的一种编程技术
- 临界区:在并发的程序执行环境中,所谓临界区(Critical Section)指的是一个会访问共享资源(例如:一个共享设备或者一块共享存储内存)的指令片段,而且当这样的多个指令片段同时访问某个共享资源时可能会引发问题
- 在并发环境下为了有效控制临界区的执行(同步),我们要做的是当有一个控制流进入临界区时,其他相关控制流必须等待
- 锁是一种最常见的用来实现同步的技术
- 不可睡眠的锁:自旋锁(Spin Lock),读取锁状态和上锁的操作必须是原子性的
- 可睡眠的锁
- 死锁(Deadlock)问题
- 什么是死锁以及死锁为何会发生?
- 当控制流执行路径中会涉及多个锁,并且这些控制流执行路径获取(aquire)锁的顺序不同时就可能会发生死锁问题
- 如何解决死锁?
- 调整获取(aquire)锁的顺序,譬如保持一致
- 尽可能防止任务在持有一把锁的同时申请其他的锁
- 尽可能少用锁,尽可能少并发
- 自旋锁的使用:
- 自旋锁可以防止多个任务同时进入临界区(critical region)
- 在自旋锁保护的临界区中不能执行长时间的操作
- 在自旋锁保护的临界区中不能主动放弃CPU
- 其他同步技术
同步技术 描述 自旋锁(Spin Lock) 如果一个任务试图获的一个已经被持有的自旋锁,这个任务就会进入忙循环(busy loops,即自旋)并等待,直到该锁可用,否者该任务就可以立刻获得这个锁并继续执行。自旋锁可以防止多个任务同时进入临界区(critical region) 信号量(Semaphore) 信号量是一种睡眠锁,当任务请求的信号量无法获得时,就会让任务进入等待队列并且让任务睡眠。当信号量可以获得时,等待队列中的一个任务就会被唤醒并获得信号量 互斥锁(Mutex) 互斥锁可以看作是对互斥量(count为1)的改进,是一种特殊的信号处理机制 完成变量(Completion Variable) 一个任务执行某些工作时,另一个任务就在完全变量上等待,当前者完成工作,就会利用完全变量来唤醒所有在这个完全变量上等待的任务
11 软件定时器
定时器的分类:
- 硬件定时器:芯片本身提供的定时器,一般由外部晶振提供,提供寄存器设置超时时间,并采用外部中断方式通知CPU。优点是精度高,但定时器个数受硬件芯片的设计限制
- 软件定时器:操作系统中基于硬件定时器提供的功能,采用软件方式实现。扩展了硬件定时器的限制,可以提供数目更多(几乎不受限制)的定时器;缺点是精度较低,必须是Tick的整数倍
- 按照定时器设计方式分:
- 单次触发定时器:创建后只会触发一次定时器通知事件,触发后该定时器自动停止(销毁)
- 周期触发定时器:创建后按照设定的周期无限循环触发定时器通知事件,直到用户手动停止
- 按照定时器超时后自动执行处理函数的上下文环境分:
- 超时函数运行在中断上下文环境中,要求执行函数的执行时间尽可能短,不可以执行等待其他事件等可能导致中断控制路径挂起的操作。优点是响应比较迅速,实时性较高
- 超时函数运行在任务上下文环境中,即创建一个任务来执行这个函数,函数中可以等待或者挂起,但实时性较差
- 软件定时器的优化
- 定时器按照超时时间排序
- 链表方式实现对定时器的管理
- 跳表(Skip List)算法
12 系统调用
系统模式:用户态和内核态
- 系统模式的切换
- ECALL命令用于主动触发异常
- 根据调用ECALL的权限级别产生不同的exception code
- 异常产生时epc寄存器的值存放的时ECALL指令本身的地址
- 系统调用的传参
- 系统调用作为操作系统的对外接口,由操作系统的实现负责定义。参考Linux的系统调用,RVOS定义系统调用的传参规则如下:
- 系统调用号放在a7中
- 系统调用参数使用a0~a5
- 返回值使用a0