函数与任务

        如果要讲解内核任务一定是优先级最高的,可以说没有任务就没有RTOS这个魔法存在。

        那要如何入手这个看似简单其实又复杂的概念呢?

函数

        现在我们先看一段我们比较熟悉的C语言

int calculate(int x, int y){
    int val;
    val = x + y;
    return val;
}

int main(void){
    int result = 0;
    int a = 3,b = 4;
    result  = calculate(a, b);
    printf("val is %d\\n", result);
    return 0;
}

        这是一个简单的c语言函数调用,目的是打印计算后的值,执行流程就是main中调用calculate函数,跳转到calculate函数中,执行完成后打印值

        在执行到result = calculate(a, b)这条指令后,CPU在看不到的地方执行了2个关键动作

函数返回

    当调用calculate后,会跳转到函数体内部进行运算,结束后会精确的回到printf位置进行打印。

    而CPU不是随便跳转的,在跳转进入calculate之前,会把 "下一条指令的地址" 保存下来。然后calculate结束后,就会取出这个保存下来的值,进行跳转,也就是返回到调用calculate的下一条指令的位置。

     这里我们要先引出第一个核心的概念这是必须要知道的概念。

       "栈"

     我们现在只需要理解这是为了防止CPU "遗忘" 而存在的一块内存,功能也是为了记忆而存在。

现场保存与恢复

    我们需要先理解现场是什么:

        CPU运算的时候,会去内存中读取指令和数据,读取数据的时候进行运算时,CPU与内存并不是直接连接的,而是经过了一个类似于中转站的东西,CPU要和内存通信,靠的都是这个 "中转站"。

        这里我们要引出第二个核心的概念

                寄存器组" CPU与内存沟通的中间人 "

        在ARM架构下为 "加载——存储架构" ,也就是说当CPU要处理内存中的数据时,必须要先把数据加载到寄存器中,然后CPU计算完成,写入寄存器中,在从寄存器中写入内存。

        而函数的返回地址也是现场的一部分。

        而所谓的现场呢?其实就是寄存器组里面的值,我们把它称之为 "现场"

        保存现场也就是保存数据,这个过程称为 “压栈”,就是将寄存器数据保存进入栈中;

        恢复现场也就是恢复数据,这个过程称为 “出栈”,就是将保存在栈的数据恢复到寄存器中

        现在我们知道了一些核心概念,那关于上面main中简单的流程下隐藏了多少步骤 (在windows下gcc编译出汇编文件进行推导)。

  1. 在栈上创建result、a、b局部变量
  2. 调用calculate函数,保存下一条指令地址进入栈中(保存现场)
  3. calculate中在栈上创建sum局部变量
  4. 拷贝main在栈中开辟的a、b值进入寄存器组进行和运算,并且把和保存进入寄存器组中
  5. 计算后的值赋值给sum
  6. 取出保存在栈中的返回地址,并且进行跳转
  7. 将值赋给main栈中的变量result
  8. printf打印

在这里我们先简化,现场的数据就是函数返回的地址。(之后会解析关于"现场"的知识)

这是一个简单的栈变化流程图

CPU、寄存器、栈的关系图

注意:2张图省略了很多细节,只是为了更好帮我们理解处理流程,想要全面了解一篇可写不完。

        现在我们了解了在函数跳转背后,CPU需要干很多额外的活来保障代码逻辑的正常运行;我总结一下:

        为了维持本身运行逻辑不被跳转破坏,CPU在进行函数跳转时,会把现场的数据保存下来,也就是把数据保存进入栈中

        为了返回到原来的位置,CPU在函数跳转时,会把返回的位置也保存下来,也就是把返回地址保存进入栈中。

那现在我提出一个问题,如果我需要计算2的20次方和3的10次方,然后我需要同时这2个结果同时输出,这时候CPU要怎么实现呢?

拆分思想

        我们来思考如何用已经有的知识解决上面提出的问题。

一个很简单的方法:

        写2个函数,一个计算2的20次方;一个计算3的10次方,然后一个个输出,可是没法实现同时输出,这肯定是一个先算完,然后才算下一个。

        那如何解决呢?

        我们知道我们现在只有一个脑子,一次只能做一件事情,所以我要么计算2的20次方,要么计算3的10次方,那要如何解决 “同时” 这个问题呢?

        我们拆分一下2的20次方和3的10次方的过程

2的20次方就是20个2相乘,也就是要进行20次乘法运算

3的10次方就是10个3相乘,也就是要进行10次乘法运算

        那可不可以这样:

        我每2次2的乘法运算之后来一次3的乘法运算,在10组这样的运算后,刚好就计算出2个最后结果。

        假设CPU单独计算2的20次方需要20ms,计算3的10次方需要10ms,那通过拆分的方式也就是花2ms计算2的乘法后,接着花1ms计算3的乘法,那最后花费30ms同时计算出结果,和先计算2的20次方然后计算3的10次方结果一样,当时却是 “同时” 完成。虽然不是真正意义上的 “同时”,却是看起来的 “同时”。

        当我们有这样的想法的时候,已经学会了并发的思维来思考问题了,而这就是任务的基本处理方式,所谓多任务处理,就是把复杂的任务拆分成一个个非常小的部分,一部分一部分实现,最后看起来就是 “同时” 实现。

        那我们是怎么实现这样的效果呢?

        我们划分为2个函数,一个函数计算2的乘法、另一个函数计算3的乘法。

        我们把2次2的乘法作为一个单元组,2次运算后需要切换进行一次3的乘法,那么从头开始

void func1(void){
 int num1 = 1;
 for(int i1 = 0; i1 < 10; i1++){
  num1 = num1 * 2 * 2;//2次乘法运算
  //切换到fun2
 }
}

void func2(void){
        int num2 = 1;
        for(int i2 = 0; i2 < 10; i2++){
                num2 = num2 * 3;//1次乘法运算
                //切换到fun1
        }
}

        我们先别管它是如何在main中运行的,我们就只看它的实现细节。

        我们来看func1和func2的内容:

i用于计算参数

 num用于保存计算后的值

这是每个函数中用于保存的数据,那我们来看实现逻辑

i1 = 0, num1 = 4 //fun1
//切换
i2 = 0,num2 = 3 //fun2
//切换
i1 = 1,num1 = 16 //fun1
//切换
i2 = 1,num2 = 9 //fun2
......
i1 = 9, num1 = end //fun1
//切换
i2 = 9,num = end //fun2

上面就是我们可以看到的逻辑过程,那中间CPU要做些什么?

回忆之前的函数跳转的2个隐藏步骤

保存返回地址

保存现场和恢复现场

        对于返回地址:func1和func2都是一样的道理,返回地址我们可以很自然的知道,func1和2跳转后都需要返回继续执行,所以func1需要保存可以返回到自身的地址,fun2同理

        对于保护现场恢复现场:func1需要保存和恢复i1和num1的值;func2需要保护和恢复i2和num2的值,并且保存和恢复都需要靠栈完成

        这样运行10次以后我们就同时得到了我们想要的2个值。

        现在我们已经实现了2个运算同时实现,那我们就回到我们的主题 “任务”,func1和func2其实就可以算作简单的任务

        如果我们理解了2个运算如何同时实现的逻辑,那我们就已经理解了任务实现的基石就是:

" 栈 "

        我们总结一个结论:

        “有了栈这个保护现场的工具,我们可以随意离开原本的处理的事情,转而去处理其他事情,因为原本的现场都被保存下来了,只要我恢复现场以后就可以继续原先的事情。”

        那我们对于多任务或者多线程的底层理解已经足够了,只要我们拥有栈,就可以实现多任务切换。

未完待续..........

(现在还是简单的推导过程,还没到难写的地方呢5555~)

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐