栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

嵌入式工程师实现一个简单的操作系统(一)

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

嵌入式工程师实现一个简单的操作系统(一)

操作系统中的任务与任务切换

1. 为什么要有任务?
    我们常常见到操作系统,也常常要学习操作系统,从一个小白的角度看,任何事情都有其对立的东西,或者说事务都是一步步发展的,既然有操作系统这个东西,那么也必然有无操作系统的软件。

    如果大家都有学习把玩过单片机,比如51单片机或者是STM32单片机,那么应该是都有无操作系统时代这样的经历。无操作系统时代,可以看作是有一个任务的操作系统。无操作系统时,软件的流程是怎么样子的?

无操作系统,裸机环境

while(1)
{
    taskA();

    taskB();

    taskC();

    taskD();
}

带操作系统环境

taskA();

taskB();

taskC();

taskD();

所以我们需要一个操作系统,来提高我们处理器的效率,把处理器的每一刻都去做有意义的事情。

相当于是间接的提高了处理器的性能。

综上所述,有操作系统与无操作系统,最大的区别就是操作系统下可以宏观上并行的执行多个任务,并可以在任务之间切换,选择合适的任务在处理器执行。


所以我们来实现的一个操作系统,首先就是实现以下功能

  • 可以创建任务
  • 可以在不同的任务之间进行切换

有了上面的功能,就可以说是实现了从无操作系统到操作系统的转变

操作系统就是一个上帝视角,每一个任务都是一个无线循环的函数功能

void taskA()
{
    initA();
    while(1)
    {
        aaa();
    }
}

void taskB()
{
    initB();
    while(1)
    {
        bbb();
    }
}

2. 任务切换怎么实现

2.1 任务切换过程

    不知道大家对函数,处理器运行相关的知识有多少的了解。任务切换有点类似与函数调用的过程,但是也会有很多不同
void taskA()
{
    initA();
    while(1)
    {
        aaa();
        bbb();
        ccc();
        ddd();
    }
}

void taskB()
{
    initB();
    while(1)
    {
        xxx();
        yyy();
        zzz();
    }
}

假设上面有2个无限循环执行的任务,可能的执行过程是这样的

aaa -> xxx -> bbb -> ccc -> yyy -> zzz -> xxx -> ddd -> aaa -> yyy

如上的执行过程,处理器在2个循环中来回执行,一定是发生了任务切换的过程

处理器执行一个函数的时候,有几个重要的要素:

  • 堆栈
  • PC 程序指针
  • LR 程序返回指针
  • 其他的通用寄存器

每一个函数有输入自己的堆栈内容,咋进入函数时候,开辟出堆栈空间,堆栈存放以下内容:

  • 函数的参数
  • 函数内定义的局部变量

因为每一个函数的参数和局部变量都是该函数私有的,并且不能被随意修改,所以在进行任务切换时候,要考虑一下堆栈

接着就是 PC 程序指针,处理器执行那一段代码程序,就是有PC指针决定,处理器总是从PC指针存放的地址去取指令执行。

所以让处理器从一个任务切换到另一个任务去执行,可能会与PC指针有关系了

2.2 任务切换需要作什么

我们想想一下如何从一个任务切换到另一个任务?

1、保护堆栈、保存堆栈

    一般的处理器都会包含一个SP堆栈寄存器,该寄存器存放了当前堆栈的地址。我们现在要进行任务切换,一段时间过后还要从其他任务切换回来。所以我们需要在切换之前吧堆栈的指针保存,在切换过程中吧目标任务的堆栈指针恢复

2、跳转到目标任务

    我们从当前任务切换到目标任务,需要在切换之前把当前运行的地址也保存下来,同理目标任务也已经保存了运行地址,所以切换目标任务就是把目标任务的保存的运行地址恢复,并且是恢复到PC指针,让处理器执行目标任务

3、保存通用寄存器

    通用寄存器在函数执行过程中存放了一些有用的信息,在任务切换之前要保存起来,在切换到目标任务的时候要恢复回来

4、上面的信息保存在哪里

    为每一个任务定义并创建一个结构体,用于保存上述重要的信息

2.3 准备切换任务的材料

上面介绍了任务切换需要什么,既然知道需要什么,那我们就开始着手准备


  1. 定义一个保存任务的信息的结构体,包含上面需要的几个部分:
  • 堆栈指针
  • PC指针
  • 通用寄存器存放
struct task{    void *sp;    void *pc;    uint64_t regs[32];}
  1. 创建一个任务

创建任务,就需要填充任务的结构体,定义一个任务初始化函数,提供以下参数

  • 任务结构体
  • 堆栈地址
  • 任务入口地址
int task_init(struct task *t, void *sp_addr, void *pc_addr)
{
    t->sp = sp_addr;
    t->pc = pc_addr;

    return 0;
}

上述函数完成了一个任务的初始化

现在,我们准备好了任务需要的材料,就可以开始进行切换了

2.4 进行切换任务实现

具体的任务切换又可以分成几个种类:

  • 从系统启动到第一个任务开始运行,当前没有任务,切换到第一个任务

    task_switch_to(struct task *task_to);
    
  • 当前正在一个任务中,切换到另一个任务

    task_switch_from_to(struct task *task_from, struct task *task_to)
    

    我们应该去思考如何填充实现上面的函数,当然了,这一部分是与具体的处理器体系结构相关联最大的部分

可以去了解下常用的RTOS在各个架构下是如何实现上述的功能,尤其是AARCH64架构,因为我目前的操作系统就是基于aarch64处理器架构的

关于上面2个任务切换函数的实现,已经上传到github的项目代码中,可以浏览查看

//根据C语言和汇编语言的调用过程,第一个参数存放在x0寄存器中
//我们函数传递的是任务结构体指针
task_switch_to:
    ldr x1, [x0]    //任务的pc地址是第一个参数,读取到x1寄存器
    mov x30, x1     //把pc地址放入到x30寄存器,x30寄存器是lr寄存器,稍后切换完成以后返回时候就会返回到任务中执行

    add x0, x0, 8   //sp参数在结构体中偏移是八字节
    ldr x1, [x0]    //取出并设置sp地址
    mov sp, x1

    ret             //函数返回,跳转到x30寄存器存放地址去执行

如果对上面的步骤不是很清楚,可以使用vscode配合gdb进行调试,单步执行代码跟踪处理器执行的过程,尝试一下是否可以正确完成任务切换

目前已经在github的项目中已经实现了上面的代码,可以下载并尝试调试和执行

项目地址:https://github.com/jhbdream/armv8_os.git

欢迎关注 star

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/312051.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号