【OS】RVOS——一个简单的基于riscv架构的操作系统
RVOS
文章目录
一、硬件概念与串口调试
HART:相当于一个CPU


UART串口调试:GND接GND,收发交错相接

波特率:每秒传输的二进制位数(单位bps:bit per second)
RISCV波特率一般是15200
1、开发板的输入晶振频率是固定的(根据硬件决定了),但是可以在UART里通过设置分频系数使得输出的波特率(UART RATE)不同
2、分频系数通过几个固定的寄存器设置完成,寄存器内的值通过查询波特率产生编程表得知,然后设置对应寄存器的值即可完成输出波特率的自定义


二、内存管理
内存的页表存放形式
内存管理需要实现的功能:
1、初始化内存的页

数组的方式
void page_init()
{
ptr_t _heap_start_aligned = _align_page(HEAP_START);//把 HEAP_START 向上对齐到 4KB 边界
uint32_t num_reserved_pages = LENGTH_RAM / (PAGE_SIZE * PAGE_SIZE);
// 每个物理页(4KB)需要一个1字节描述符。那么,对于整个物理内存,需要的描述符总大小为:LENGTH_RAM / PAGE_SIZE 字节。
// 换算过来需要 (LENGTH_RAM / PAGE_SIZE) / PAGE_SIZE 个物理页来存放这些描述符
// 计算num_reserved_pages的目的是确保有足够物理页容纳所有描述符,同时使用户内存起始地址(_alloc_start) 严格对齐4096
_num_pages = (HEAP_SIZE - (_heap_start_aligned - HEAP_START))/ PAGE_SIZE - num_reserved_pages;
//从 heap 总大小里减掉对齐浪费掉的头部空间;再减去用于存放页描述符的页面数量;剩下的就是可以分配给用户的内存页面数量
// 堆页面数=(堆总大小-页面对齐前浪费的空间)/页面大小-描述符页面数量
printf("HEAP_START = %p(aligned to %p), HEAP_SIZE = 0x%lx,\n"
"num of reserved pages = %d, num of pages to be allocated for heap = %d\n",
HEAP_START, _heap_start_aligned, HEAP_SIZE,
num_reserved_pages, _num_pages);
// 初始化页描述符数组,依次将flag置0
struct Page *page = (struct Page *)HEAP_START;
for (int i = 0; i < _num_pages; i++) {
_clear(page);
page++;
}
_alloc_start = _heap_start_aligned + num_reserved_pages * PAGE_SIZE;//计算用户内存起始地址
_alloc_end = _alloc_start + (PAGE_SIZE * _num_pages);
}
2、内存的分配与释放
分配
线性扫描方式,对页描述符进行操作
void *page_alloc(int npages)
{
int found = 0;
struct Page *page_i = (struct Page *)HEAP_START;
for (int i = 0; i <= (_num_pages - npages); i++) {
if (_is_free(page_i)) {
found = 1;
struct Page *page_j = page_i + 1;
for (int j = i + 1; j < (i + npages); j++) {
if (!_is_free(page_j)) {//没有连续的j个页面就将found置0
found = 0;
break;
}
page_j++;//指针移动到下一个页面描述符
}
//如果找到一段连续空闲页,把这几页都标记成 PAGE_TAKEN,并把最后一页标记成 PAGE_LAST
if (found) {
struct Page *page_k = page_i;
for (int k = i; k < (i + npages); k++) {
_set_flag(page_k, PAGE_TAKEN);
page_k++;
}
page_k--;
_set_flag(page_k, PAGE_LAST);
return (void *)(_alloc_start + i * PAGE_SIZE);
}
}
page_i++;
}
return NULL;
}
释放
void page_free(void *p)
{
if (!p || (ptr_t)p >= _alloc_end) {
return;
}
/* get the first page descriptor of this memory block */
struct Page *page = (struct Page *)HEAP_START;
page += ((ptr_t)p - _alloc_start)/ PAGE_SIZE;
//p是分配的内存块的起始地址,减去用户内存起始地址,再除以页面大小,就得到了这个内存块对应的第一个页面描述符在页描述符数组中的索引
while (!_is_free(page)) {
if (_is_last(page)) {//如果当前页面的flag=LAST,说明这是最后一个页,清除这个页面后退出循环
_clear(page);
break;
} else {
_clear(page);
page++;;
}
}
}
3、物理内存的保护

4、虚拟内存

三、异常与中断
1、Trap框架
Trap包括中断和异常,是一切异常控制流的总称呼
Trap有关寄存器

详解:
mtvec:

两种模式:
- Direc:直接到对应的异常处理程序(switch case)
- Vectored:中断处理表,通过表内指针跳转异常处理程序入口


mepc:存储异常处理完成后返回的PC值,异常返回正常

mstatus:掌管着中断使能、特权模式切换和处理器状态监控等关键功能
mcause:中断异常的类型,其中最高bit Interrupt用来指示当前是中断还是异常,位bit用来记录异常code。
mtval:辅助描述中断异常


Trap框架实现:

1、Trap入口:保存Trap发生时的现场,跳转Trap处理函数
trap_vector:
# save context(registers).
csrrw t6, mscratch, t6 # swap t6 and mscratch
reg_save t6
# Save the actual t6 register, which we swapped into
# mscratch
mv t5, t6 # t5 points to the context of current task
csrr t6, mscratch # read t6 back from mscratch
STORE t6, 30*SIZE_REG(t5) # save t6 with t5 as base
# Restore the context pointer into mscratch
csrw mscratch, t5
# call the C trap handler in trap.c
csrr a0, mepc
csrr a1, mcause
call trap_handler//trap_handler(mepc, mcause);
# trap_handler will return the return address via a0.用 trap_handler() 返回的新地址更新 mepc
csrw mepc, a0
# restore context(registers).把Trap时保存的寄存器恢复回来
csrr t6, mscratch
reg_restore t6
# return to whatever we were doing before trap.返回 trap 前的位置
mret
其中RISC-V 函数调用约定:
a0 = 第 1 个参数=mepc(Trap结束后返回地址)
a1 = 第 2 个参数=mcause(Trap类型)
所以call trap_handler相当与trap_handler(mepc, mcause);
同理,回来的时候函数返回值也会放入a0
2、中断或异常处理函数(后续会写处理方法,这里只是有个框架判断了一下是中断还是异常)
reg_t trap_handler(reg_t epc, reg_t cause)
{
reg_t return_pc = epc;
reg_t cause_code = cause & MCAUSE_MASK_ECODE;
//MCAUSE_MASK_ECODE在riscv.h中定义=0x7FFFFFFFFFFFFFFF,就是把最高位去掉,只保留原因编号。
//MCAUSE_MASK_INTERRUPT在riscv.h中定义=0x8000000000000000,就是把最高位保留,其他位清零,只保留最高位。这里用于判断是异常还是中断
if (cause & MCAUSE_MASK_INTERRUPT) {
/* Asynchronous trap - interrupt */
switch (cause_code) {
case 3:
uart_puts("software interruption!\n");//软件中断
break;
case 7:
uart_puts("timer interruption!\n");//定时器中断
break;
case 11:
uart_puts("external interruption!\n");//外部中断
break;
default:
printf("Unknown async exception! Code = %ld\n", cause_code);
break;
}
} else {
/* Synchronous trap - exception */
printf("Sync exceptions! Code = %ld\n", cause_code);//异常,输出异常的类型码
panic("OOPS! What can I do!");//panic死循环
//return_pc += 4;
}
return return_pc;
}
2、中断
2.1中断的分类:
本地中断(软件中断、定时器中断)、全局中断

2.2全局中断
也称外部中断,是由 (UART, GPIO, 以太网等)外设产生的中断,由PLIC平台级中断控制器管理

PLIC:在Soc上的一个控制外设中断的中断控制器,其中外设为中断源

数字通路

PLIC相关寄存器,其中
全局寄存器:
PLIC_PRIORITY(id)
PLIC_PENDING(id)
每个 hart / context 一份的寄存器:
PLIC_MENABLE(hart, id)
PLIC_MTHRESHOLD(hart)
PLIC_MCLAIM(hart)
PLIC_MCOMPLETE(hart)
首先定义PLIC元器件基地址
#define PLIC_BASE 0x0c000000L

Priority优先级:每个中断源(外设)都有一个 32 位优先级寄存器

//定义优先级存放的地址
#define PLIC_PRIORITY(id) (PLIC_BASE + (id) * 4)
//设置优先级,其中UART0_IRQ也是宏,表示在这个外设中断模拟中这个外设的中断源编号
*(uint32_t*)PLIC_PRIORITY(UART0_IRQ) = 1;
Pending:中断发生等待处理指示寄存器

#define PLIC_PENDING(id) (PLIC_BASE + 0x1000 + ((id) / 32) * 4)
Enable:某个hart的中断使能寄存器,决定某个hart是否允许接收外部中断源(每个hart都有这个寄存器)

#define PLIC_MENABLE(hart, id) (PLIC_BASE + 0x2000 + (hart) * 0x80 + ((id) / 32) * 4)
Threshold:中断优先级阈值寄存器,只有当中断源 priority > hart threshold时中断才会被送给 hart进行处理

claim与complete:使用同一个地址,因此
#define PLIC_MCLAIM(hart) (PLIC_BASE + 0x200004 + (hart) * 0x1000)
#define PLIC_MCOMPLETE(hart) (PLIC_BASE + 0x200004 + (hart) * 0x1000)
- claim:使用hart读取该地址的操作被视为认领中断claim,此时消除该中断对应pending位
- complete:使用hart写该地址的操作被视为中断处理完成complete

外部中断处理实现
1、封装寄存器函数
初始化 PLIC
void plic_init(void)
{
int hart = r_tp();
*(uint32_t*)PLIC_PRIORITY(UART0_IRQ) = 1;//设置 UART0 的中断优先级为 1
*(uint32_t*)PLIC_MENABLE(hart, UART0_IRQ)= (1 << (UART0_IRQ % 32));//打开当前 hart 对 UART0 外设的接收开关(PLIC 层面)
*(uint32_t*)PLIC_MTHRESHOLD(hart) = 0;//设置当前 hart 的中断优先级阈值为 0
w_mie(r_mie() | MIE_MEIE);//打开 CPU 里的外部中断开关,允许 CPU 接收外部中断信号
w_mstatus(r_mstatus() | MSTATUS_MIE);//打开 CPU 的全局中断总开关
}
从 PLIC 领取一个待处理中断
int plic_claim(void)
{
int hart = r_tp();//取当前 hart
int irq = *(uint32_t*)PLIC_MCLAIM(hart);//读当前 hart 的 claim 寄存器,此时PLIC会返回当前 hart 的最高优先级的挂起中断 ID,如果没有挂起中断则返回 0
return irq;//返回值是要处理外设中断编号(外部中断源编号)
}
告诉 PLIC 中断处理完成
void plic_complete(int irq)
{
int hart = r_tp();
*(uint32_t*)PLIC_MCOMPLETE(hart) = irq;//向 complete 寄存器写入刚才处理的 IRQ 编号,表示IRQ 编号的中断已经处理完成
}
2、调用封装函数完成中断处理
reg_t trap_handler(reg_t epc, reg_t cause)
{
reg_t return_pc = epc;
reg_t cause_code = cause & MCAUSE_MASK_ECODE;
if (cause & MCAUSE_MASK_INTERRUPT) {
/* Asynchronous trap - interrupt */
switch (cause_code) {
case 3:
uart_puts("software interruption!\n");
break;
case 7:
uart_puts("timer interruption!\n");
break;
case 11:
uart_puts("external interruption!\n");
external_interrupt_handler();
break;
default:
printf("Unknown async exception! Code = %ld\n", cause_code);
break;
}
} else {
/* Synchronous trap - exception */
printf("Sync exceptions! Code = %ld\n", cause_code);
panic("OOPS! What can I do!");
//return_pc += 4;
}
return return_pc;
}
void external_interrupt_handler()
{
int irq = plic_claim();
if (irq == UART0_IRQ){//这里类似switch case,根据中断源做出对应处理
uart_isr();
} else if (irq) {//来了一个外部中断,但当前 OS 没写对应处理函数,只是简单处理
printf("unexpected interrupt irq = %d\n", irq);
}
if (irq) {
plic_complete(irq);
}
}
2.3软件中断与定时器中断
由CLINT对本地中断进行管理

CLINT设备的相关寄存器
基地址
#define CLINT_BASE 0x2000000L

MTIME:全局时间计数器,64 位,它会随着硬件时钟一直增加,整个 CLINT 共享一个全局时间,多个 CPU 核读到的是同一个时间源。
#define CLINT_MTIME (CLINT_BASE + 0xBFF8) // cycles since boot.

MSIP(Machine Software Interrupt Pending):每个 hart一个,32位,用于软件(上层应用)触发或查询一个hart的软件中断
#define CLINT_MSIP(hartid) (CLINT_BASE + 4 * (hartid))
想触发一个hart的软件中断,往这个hart的MSIP寄存器写入1就行了,该hart会触发machine software interrupt
*(uint32_t *)CLINT_MSIP(hart_id) = 1;
//同理,清除一个hart的软件中断,将该hart的MSIP寄存器清0即可
*(uint32_t *)CLINT_MSIP(hart_id) = 0;
MTIMECMP:定时器比较寄存器,每个 hart 一个,64 位,当mtime >= 该hart的mtimecmp寄存器里的值,就会触发一个定时器中断


定时器中断实现
1、封装寄存器函数
设置定时器,本质是设置MTIMECMP寄存器的值
void timer_load(int interval)
{
/* each CPU has a separate source of timer interrupts. */
int id = r_mhartid();
*(uint64_t*)CLINT_MTIMECMP(id) = *(uint64_t*)CLINT_MTIME + interval;
}
初始化定时器
void timer_init()
{
timer_load(TIMER_INTERVAL);//初始化MTIMECMP
//使能定时器中断
w_mie(r_mie() | MIE_MTIE);
//打开 CPU 的全局中断总开关
w_mstatus(r_mstatus() | MSTATUS_MIE);
}
其中#define CLINT_TIMEBASE_FREQ 10000000代表一秒
2、调用封装函数完成定时器中断处理
笔者有话说:其实我觉得这里主要只是看一下定时器框架还有了解熟悉CLINT硬件,定时器触发后的处理程序代码可以写成别的,这里我觉得是为了教学演示明显,将每次触发定时器中断后仅仅将MTIMECMP向后推了一秒,然后不断循环的周期触发定时器中断
reg_t trap_handler(reg_t epc, reg_t cause)
{
reg_t return_pc = epc;
reg_t cause_code = cause & MCAUSE_MASK_ECODE;
if (cause & MCAUSE_MASK_INTERRUPT) {
/* Asynchronous trap - interrupt */
switch (cause_code) {
case 3:
uart_puts("software interruption!\n");
break;
case 7:
uart_puts("timer interruption!\n");
timer_handler();
break;
case 11:
uart_puts("external interruption!\n");
external_interrupt_handler();
break;
default:
printf("Unknown async exception! Code = %ld\n", cause_code);
break;
}
} else {
/* Synchronous trap - exception */
printf("Sync exceptions! Code = %ld\n", cause_code);
panic("OOPS! What can I do!");
//return_pc += 4;
}
return return_pc;
}
void timer_handler()
{
_tick++;
printf("tick: %d\n", _tick);
timer_load(TIMER_INTERVAL);
}
2.4 扩展:软件定时器
来源与定义
硬件定时器由外部晶振提供,并采用外部中断的方式通知CPU,精度高但是定时器个数受到硬件芯片的设计限制
软件定时器是操作系统基于硬件定时器提供的功能,采用软件的方式实现,可以提供数目更多的定时器,缺点是精度较低,必须是Tick的整数倍
软件定时器的分类

对外(应用层)暴露的接口与软件定时器结构体
结构体:软件定时器的处理函数,传入参数与设置定时器的tick数
/* software timer */
struct timer {
void (*func)(void *arg);
void *arg;
uint32_t timeout_tick;
};
extern struct timer *timer_create(void (*handler)(void *arg), void *arg, uint32_t timeout);
extern void timer_delete(struct timer *timer);
软件定时器数组
#define MAX_TIMER 10
static struct timer timer_list[MAX_TIMER];
软件定时器创建
先找数组为空,再将回调指针的一些参数赋给软件定时器,最后timeout_tick = _tick + timeout。
struct timer *timer_create(void (*handler)(void *arg), void *arg, uint32_t timeout)
{
/* TBD: params should be checked more, but now we just simplify this */
if (NULL == handler || 0 == timeout) {
return NULL;
}
/* use lock to protect the shared timer_list between multiple tasks */
spin_lock();
struct timer *t = &(timer_list[0]);
for (int i = 0; i < MAX_TIMER; i++) {
if (NULL == t->func) {
break;
}
t++;
}
if (NULL != t->func) {
spin_unlock();
return NULL;
}
t->func = handler;
t->arg = arg;
t->timeout_tick = _tick + timeout;
spin_unlock();
return t;
}
定时器删除函数
对应数组置空,对应结构体参数置空
void timer_delete(struct timer *timer)
{
spin_lock();
struct timer *t = &(timer_list[0]);
for (int i = 0; i < MAX_TIMER; i++) {
if (t == timer) {
t->func = NULL;
t->arg = NULL;
break;
}
t++;
}
spin_unlock();
}
定时器触发
当全局tick数>=创建定时器时设置的timeout数时,就通过指针调用软件定时器的处理函数,然后清掉该定时器

定时器优化
希望软件定时器效率高(问题:数组遍历效率低,提前开好空间使内存使用被固定化)
方式:
1、用链表替换数组(解决内存固化)
2、定时器按照超时时间排序(解决数组每隔几个就被置空)
3、算法(降低定时器搜索时的开销)


四、多任务系统
1、名词解析
任务:指令执行流

多任务:多个指令执行流(每个任务拥有自己的32个寄存器信息)

任务上下文:寄存器信息

预处理器宏 :预处理器宏是在编译之前,由预处理器进行处理的。它本质上是文本替换工具
- 处理时机:在实际编译开始前,由预处理器处理。
- 语法:以
#define指令定义。 - 核心机制:进行简单的文本替换,不涉及类型检查或作用域。
- 作用域:从定义处到文件结束,或直到被
#undef取消
#define LOAD lw
#define STORE sw
#define SIZE_REG 4
汇编宏:汇编宏是在汇编过程中,由汇编器处理的。它代表一段可重用的汇编代码序列
- 处理时机:在汇编阶段,由汇编器展开。
- 语法:定义方式因汇编器而异。如MASM用
MACRO/ENDM,NASM用%macro/%endmacro。 - 核心机制:代码生成。每次调用都会在当前位置“复制粘贴”一份宏定义的代码副本。
- 作用域:通常局限于定义它的汇编文件或模块内。
.macro reg_save base
STORE ra, 0*SIZE_REG(\base)
STORE sp, 1*SIZE_REG(\base)
STORE t0, 4*SIZE_REG(\base)
STORE t1, 5*SIZE_REG(\base)
STORE t2, 6*SIZE_REG(\base)
STORE s0, 7*SIZE_REG(\base)
STORE s1, 8*SIZE_REG(\base)
STORE a0, 9*SIZE_REG(\base)
STORE a1, 10*SIZE_REG(\base)
STORE a2, 11*SIZE_REG(\base)
STORE a3, 12*SIZE_REG(\base)
STORE a4, 13*SIZE_REG(\base)
STORE a5, 14*SIZE_REG(\base)
STORE a6, 15*SIZE_REG(\base)
STORE a7, 16*SIZE_REG(\base)
STORE s2, 17*SIZE_REG(\base)
STORE s3, 18*SIZE_REG(\base)
STORE s4, 19*SIZE_REG(\base)
STORE s5, 20*SIZE_REG(\base)
STORE s6, 21*SIZE_REG(\base)
STORE s7, 22*SIZE_REG(\base)
STORE s8, 23*SIZE_REG(\base)
STORE s9, 24*SIZE_REG(\base)
STORE s10, 25*SIZE_REG(\base)
STORE s11, 26*SIZE_REG(\base)
STORE t3, 27*SIZE_REG(\base)
STORE t4, 28*SIZE_REG(\base)
STORE t5, 29*SIZE_REG(\base)
# we don't save t6 here, due to we have used
# it as base, we have to save t6 in an extra step
# outside of reg_save
.endm
2、协作式多任务
协作式多任务:每个任务调用方法主动把 CPU 让出来
保存上一个任务的上下文—>切换上下文—>加载下一个任务的上下文到CPU

//初始化任务
int task_create(void (*start_routin)(void))
{
if (_top < MAX_TASKS) {
ctx_tasks[_top].sp = (reg_t) &task_stack[_top][STACK_SIZE];//该任务(指令流)的栈顶指针
ctx_tasks[_top].ra = (reg_t) start_routin;//设置该任务的返回地址为任务入口地址
_top++;
return 0;
} else {
return -1;
}
}
运行:将CPU里面目前的上下文放入上一个任务的上下文寄存器中,将下一个任务的上下文寄存器的信息放入CPU,最妙的一点是这个ra的设计,可以去看视频


3、抢占式多任务
利用定时器中断实现抢占式多任务(CFS),当一个任务运行时就开启定时器,定时器到了触发中断,然后对中断类型进行判断,如果是定时器中断就跳转进行上下文切换,由此循环


4、任务同步与锁
临界区:

锁的分类:不可睡眠锁、可睡眠锁

自旋锁:
自旋体现在不断的for判断上

两任务获取锁:

新的问题:不够原子化


解决方式:改为原子锁,将读取旧值 和 写入新值 必须捆在一起,期间总线不允许其他核心介入

死锁:


五、系统调用
裸机代码从_start开始运行,3左移11位标志着程序跑在Machine模式下,1左移7位标志着中断是打开的

那么如何将任务放在用户模式下呢,去掉3左移11位即可

系统模式的切换的方法

ECALL

系统调用流程:
1、最左func():open(),read()
2、open(),read()内部有ecall触发异常
3、内核态中对异常进行处理函数
4、函数处理完成逐层返回
注:用户程序(1)→ 系统调用入口(2)→ VFS通用处理层(3)→ 驱动代码(4)


注:return PC+4是因为异常产生时epc寄存器存放的是ecall指令的地址,如果不+4将回到ecall然后进行循环
系统调用传参
将一些需要传输的参数放在人为设定的寄存器中

步骤:
1、应用程序调用系统调用函数
2、将系统调用函数号传入a7寄存器,然后ecall进入内核态
3、进入trap入口函数,先保存寄存器上下文(a0、a1、a2…),进入trap处理函数
4、在trap处理函数根据不同的系统调用号调用不同的函数,返回结果放入a0

系统调用向外接口的提供:C库((如 glibc)是系统调用最主要的“标准封装层”和“对外接口”)

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)