news 2026/11/5 0:24:38

一生一芯学习:多道程序 yield-os.c

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一生一芯学习:多道程序 yield-os.c

随着处理器主频的越来越高,每次读写一次磁盘要耗费很多个时钟周期来等待磁盘操作的完成,与其傻傻等待,在这等待的过程中我们可以做更多有意义的事情,如当第一个程序需要等待输入输出的时候,切换到第二个程序来运行,第二个程序也等待输入输出的时候就可以切换到第三个程序,以此类推。

这就是多道程序的思想,要实现一个多道程序操作系统, 我们只需要实现以下两点就可以了:

在内存中可以同时存在多个进程

在满足某些条件的情况下, 可以让执行流在这些进程之间切换

什么是进程? 进程 = 程序 + 执行

进程是执行中的程序,除了可执行代码外还包含进程的活动信息和数据,比如用来存放函数变量、局部变量、返回值的用户栈,存放进程相关数据的数据段,内核中进程间切换的内核栈,动态分配的堆。

上下文切换

在yield-os.c中构建了两个执行流,不断交替输出A和B,基本原理就是进程A运行的时候触发了系统调用,通过自陷指令陷入到内核中,根据__am_asm_trap(),A的上下文结构(Context)将会被保存在A的栈上。系统调用完后通过__am_asm_trap()恢复A的上下文,如果此时不恢复A的上下文,而是恢复B的上下文,那么执行完__am_asm_trap()

来看下yield-os.c执行流是如何进行进程切换的。首先贴出它的代码。

这个PCB是union类型的,而不是struct类型的,原因如下:定义数据的时候把PCB的stack栈空间和cp 记录上下文指针的元数据存放在同一块内存上。即pcb.stack占满整个PCB内存,然后PCB.CP放在内存的栈底。这样在上下文恢复时用 cp 指向的地址就能直接恢复栈上保存的 Context。

#define STACK_SIZE (4096 * 8)

typedef union {

uint8_t stack[STACK_SIZE];

struct { Context *cp; }; //(context pointer)来记录上下文结构的位置

} PCB;

int main() {

cte_init(schedule);

pcb[0].cp = kcontext((Area) { pcb[0].stack, &pcb[0] + 1 }, f, (void *)1L);

pcb[1].cp = kcontext((Area) { pcb[1].stack, &pcb[1] + 1 }, f, (void *)2L);

yield();

panic("Should not reach here!");

}

第一件事先初始化一下CTE

cte_init的作用是定义了待会跳转去异常处理的地址传给mtvec,然后注册回调函数shedule`

bool cte_init(Context*(*handler)(Event, Context*)) {

// initialize exception entry

asm volatile("csrw mtvec, %0" : : "r"(__am_asm_trap)); //把amasmtrap的地址传给mtvec

user_handler = handler;

return true;

}

这个

static Context *schedule(Event ev, Context *prev) {

current->cp = prev;

current = (current == &pcb[0] ? &pcb[1] : &pcb[0]);

return current->cp;

}

然后把执行完cte_init(schedule)之后到了

pcb[0].cp = kcontext((Area) { pcb[0].stack, &pcb[0] + 1 }, f, (void *)1L);

pcb[1].cp = kcontext((Area) { pcb[1].stack, &pcb[1] + 1 }, f, (void *)2L);

先来看下kcontext()的代码。第一个参数{ pcb[0].stack, &pcb[0] + 1 }就是栈空间,随后将函数名当成指针,函数f 会自动“退化”为指向该函数的指针。于是此时entry就是f了。如果指针后面赋值为mepc=(uintptr_t)entry,那么就会自动执行函数f,带上参数1。

下一行同理

Context *kcontext(Area kstack, void (*entry)(void *), void *arg) {

Context *cp = (Context *)(kstack.end - sizeof(Context));

cp->mepc = (uintptr_t)entry;

cp->mstatus = 0x1800;

cp->gpr[10] = (uintptr_t)arg; //a0传参

return cp;

}

随后陷入yield()

void yield() {

#ifdef __riscv_e

asm volatile("li a5, -1; ecall");

#else

asm volatile("li a7, -1; ecall");

#endif

}

于是进行ecall指令

INSTPAT("0000000 00000 00000 000 00000 11100 11", ecall , I, s->dnpc = isa_raise_intr(11,s->pc);etrace());

然后调用isa_raise_intr(11,s->pc)函数。

word_t isa_raise_intr(word_t NO, vaddr_t epc) {

/* TODO: Trigger an interrupt/exception with ``NO''. 待办事项:使用“NO”触发中断/异常。

* Then return the address of the interrupt/exception vector. 然后返回中断/异常向量的地址

*/

cpu.mstatus = 0x00001800;

cpu.mepc = epc;

cpu.mcause = NO;

return cpu.mtvec;

}

此时PC会跳转到之前定义的mtvec中,也就是cte_init中的__am_asm_trap函数。

__am_asm_trap:

addi sp, sp, -CONTEXT_SIZE

MAP(REGS, PUSH)

csrr t0, mcause

csrr t1, mstatus

csrr t2, mepc

STORE t0, OFFSET_CAUSE(sp)

STORE t1, OFFSET_STATUS(sp)

STORE t2, OFFSET_EPC(sp)

# set mstatus.MPRV to pass difftest

li a0, (1 << 17)

or t1, t1, a0

csrw mstatus, t1

mv a0, sp

call __am_irq_handle

mv sp, a0

LOAD t1, OFFSET_STATUS(sp)

LOAD t2, OFFSET_EPC(sp)

csrw mstatus, t1

csrw mepc, t2

MAP(REGS, POP)

addi sp, sp, CONTEXT_SIZE

mret

这个函数作用之前讲过了,将上下文保存在栈上,然后调用handler之后还原现场,但此时我们把a0作为参数给sp,那就能做到线程切换,具体来看代码。会跳转到__am_irq_handle这个函数,看看他的源码。

Context* __am_irq_handle(Context *c) {

if (user_handler) {

Event ev = {0};

switch (c->mcause) {

case 11:

ev.event=EVENT_YIELD;

if(c->GPR1!=-1)

ev.event = EVENT_SYSCALL;

c->mepc += 4;

break;

default: ev.event = EVENT_ERROR; break;

}

//printf("mcause = %s\n",c->mcause);

c = user_handler(ev, c); //调用之前注册的handler

assert(c != NULL);

}

return c;

}

目前识别出是yield之后然后调用之前注册的回调函数。也就是shedule

static Context *schedule(Event ev, Context *prev) {

current->cp = prev;

current = (current == &pcb[0] ? &pcb[1] : &pcb[0]);

return current->cp;

}

可以看到cte_init()在trace中是这么传递参数的。

image

意思就是根据riscv地abi切换a0的值,也就是切换线程,随后

mv sp, a0

LOAD t1, OFFSET_STATUS(sp)

LOAD t2, OFFSET_EPC(sp)

csrw mstatus, t1

csrw mepc, t2

MAP(REGS, POP)

addi sp, sp, CONTEXT_SIZE

mret

恢复现场,切换为B线程,也就是所有寄存器,什么通用寄存器堆,mepc,mcause, mstatus, mepc都一模一样。

然后调用mret,pc变成cpu.mepc,于是跳到刚刚kcontext定义的entry中,也就是f函数里面,然后判断参数是多少进行对应的输出之后又陷入到yield,一直循环。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 5:50:55

1811种语言+全合规架构:Apertus-8B如何重塑企业AI应用规则

1811种语言全合规架构&#xff1a;Apertus-8B如何重塑企业AI应用规则 【免费下载链接】Apertus-8B-Instruct-2509-unsloth-bnb-4bit 项目地址: https://ai.gitcode.com/hf_mirrors/unsloth/Apertus-8B-Instruct-2509-unsloth-bnb-4bit 导语 瑞士国家AI研究所推出的Ape…

作者头像 李华
网站建设 2026/11/4 21:30:34

90亿参数挑战千亿级模型:GLM-4.1V-9B-Base开启中小企业AI普及时代

90亿参数挑战千亿级模型&#xff1a;GLM-4.1V-9B-Base开启中小企业AI普及时代 【免费下载链接】GLM-4.1V-9B-Base 项目地址: https://ai.gitcode.com/zai-org/GLM-4.1V-9B-Base 导语 清华智谱AI最新开源的GLM-4.1V-9B-Base多模态模型&#xff0c;以仅90亿的参数量在18…

作者头像 李华
网站建设 2026/11/4 8:56:45

数学:用“生活案例”讲透应用题,学生不再怕公式

“老师&#xff0c;这道题的公式我记不住&#xff01;”“明明套了公式&#xff0c;怎么还是算错了&#xff1f;”批改数学作业时&#xff0c;这样的问题是不是总能戳中你的痛点&#xff1f;很多学生学数学就像“背口诀”&#xff0c;公式背得滚瓜烂熟&#xff0c;一到应用题就…

作者头像 李华
网站建设 2026/11/4 17:44:19

慢一点没关系:用“等待式教学”守护学生的思维萌芽

“谁能回答这个问题&#xff1f;”话音刚落&#xff0c;你是不是就下意识地看向那几个常举手的“小活跃”&#xff1f;如果三秒内没人回应&#xff0c;是不是就忍不住开始提示&#xff0c;甚至直接公布答案&#xff1f;身为教师的我们&#xff0c;总习惯在课堂上扮演“引路人”…

作者头像 李华
网站建设 2026/11/2 22:40:13

cmath复数运算:5分钟掌握数学计算神器

cmath复数运算&#xff1a;5分钟掌握数学计算神器 【免费下载链接】cmath CMATH: Can your language model pass Chinese elementary school math test? 项目地址: https://gitcode.com/gh_mirrors/cm/cmath 还在为复杂的数学计算头疼吗&#xff1f;今天要介绍的cmath库…

作者头像 李华
网站建设 2026/11/4 14:55:39

毕业一年后对于大学计科课程的回顾和感受

我们学校年年都有课程评价&#xff0c;在毕业的时候还有对课程体系设计的评价和想法。可是那个时候我是真的云里雾里啊能说出来什么。最近我在学习RTOS&#xff0c;突然想起操作系统这门课程&#xff0c;真是瞬间心头堵的慌——因为我当年没有完全征服它。大三的课程真是又实际…

作者头像 李华