目录
杂谈
笔者在学习os的过程中,接触过linux这样的操作系统,也接触过minirtos,freertos,rt-thread这样的微型实时操作系统,操作系统的原理让人倍感好奇,在学习过一段时间的rtos后,笔者尝试实现了freertos的内核。因为有大量关于freertos内核的资料,笔者用了一个月的时间完成了内核的实现,随着笔者对操作系统的认知进一步深刻,再回头来看FreeRTOS又有了不一样的感受。对FreeRTOS的温故而知新,让笔者有了记录更深刻的理解的冲动,所以写下了这个系列的博客,希望大家能沿着笔者的思路,一起理解实时操作系统的本质。
系列一:微型操作系统内核源码详解系列一:rtos内核源码概论篇(以freertos为例)-CSDN博客
系列二:微型操作系统内核源码详解系列二:数据结构和对象篇(以freertos为例)-CSDN博客
系列三:微型操作系统内核源码详解系列三(0):空间存储及内存管理篇(前置篇)-CSDN博客
微型操作系统内核源码详解系列三(1):任务及切换篇(任务函数定义)-CSDN博客
微型操作系统内核源码详解系列三(2):任务及切换篇(任务函数定义)-CSDN博客
微型操作系统内核源码详解系列三(3):任务及切换篇(任务函数定义)-CSDN博客
微型操作系统内核源码详解系列三(4):arm架构篇-CSDN博客
微型操作系统内核源码详解系列三(5):进程与线程-CSDN博客
系列四:
微型操作系统内核源码详解系列四(1):操作系统调度算法(linux0.11版本内核)-CSDN博客
微型操作系统内核源码详解系列四(2):操作系统调度算法(rt-thread内核)-CSDN博客
微型操作系统内核源码详解系列四(3):操作系统调度算法(FreeRTOS内核篇上)-CSDN博客
微型操作系统内核源码详解系列四(4):操作系统调度算法(FreeRTOS内核篇下)-CSDN博客
系列五:
微型操作系统内核源码详解系列五(1):arm cortex m3架构-CSDN博客
微型操作系统内核源码详解系列五(2):cm3下栈的初始化-CSDN博客
微型操作系统内核源码详解系列五(3):cm3下调度的开启-CSDN博客
微型操作系统内核源码详解系列五(四):cm3下svc启动任务-CSDN博客
微型操作系统内核源码详解系列五(五):cm3下Pendsv切换任务上篇-CSDN博客
微型操作系统内核源码详解系列五(六):Pendsv切换任务下篇-CSDN博客
出发点
其实大家难免会问:为什么要学习操作系统内核代码呢?为什么要重复造轮子呢?把freertos应用弄熟练已经完全够了啊!
笔者也思考过这个问题。但是笔者认为:FreeRTOS作为一个微型操作系统,其实它远远没有想象中那么稳定,当遇到涉及底层的问题时,只会使用API就已经完全不够了,了解底层还是有必要的。
虽然是这么说,但是,也不必太过底层了啊?能把API运用熟练,已经能够解决百分之90的问题了,如果懂得内核各种函数的作用,已经具备解决几乎所有问题的能力了。那照这样看,笔者更应该像其他的作者一样,从宏观角度出发,从API到阐述各个函数的联系。
但是,笔者认为写下这篇博客的出发点是对底层的理解与思考以及个人的总结。
因此,笔者将这个系列的博客定位为学习博客,而非工具博客,笔者会从实时操作系统的层面渗透到基础知识层面,笔者将会在博客中整理大量基础内容,希望大家能在这繁忙的生活中抽出时间来跟随笔者一起探寻实时操作系统的奥秘。
由于笔者接触rtos的时间不多,学习和做项目也并不是以单片机为主,在去年实现过内核后就没怎么接触过rtos了,可能文章有大量的口误和漏洞,但还是想把自己对于rtos内核的理解分享给大家。如果你认为这些内容对你有所帮助,那自然是笔者所希望的。如果你认为笔者的博客非常糟糕,欢迎接受批评,笔者会不断修改博客,直到文章思路清晰,逻辑正确。
本章作为开篇,笔者并不会像很多博客一样,扯一些rtos内部的细节或者应用及优点。笔者会与大家探讨rtos的本质,引导大家思考rtos的原理。这篇博客我会覆盖后面博客所有内容概要,是笔者带领读者理解rtos的大蓝图,希望大家能够耐心看完。
基础知识篇
在这篇博客中,读者并不需要具备多少基础,但是笔者个人认为了解以下内容中即可:C语言,数据结构,算法分析,操作系统知识,arm架构汇编。不了解也没关系,本篇博客的定位就是从无到有,如果你具备了一定的基础,那么我希望你通过本系列博客延伸到操作系统层面;如果你不具备任何基础,那么我希望本系列博客将会成为你理解底层基础的钥匙。
为什么要用rtos?
可能有小伙伴感到困惑,rtos到底是什么?为什么要用rtos?看到实时操作系统这几个字,一般来说从操作系统层面理解是正确的,也就是硬件,操作系统,应用程序出发。但是笔者决定先从内核层面出发,再讲解rtos的本质,逐步延伸到操作系统层面。
先让我们理解实时操作系统的作用,我们用rtos的原因就是,我们希望任务能快速响应。这也就是为什么被称之为实时的原因。
现在问题又来了,为什么rtos能快速响应?首先让我们看看裸机,我们知道,如果发生中断,mcu会放下所有的事情去处理中断,那么之前的任务怎么办?这不就不能及时处理任务了吗?
为了解决这个问题,我们试想,如果多个任务能同时执行不就行了吗?这也就是为什么要用rtos的原因:为了更及时的处理任务。
如何做到同时?
我们把同时运行的任务称之为多线程,我们知道mcu在同一时间只能做一件事情,执行一个上下文的代码,在裸机中我们使用while(1)的形式来进行轮询,while(1)中是任务,通常是执行完一个任务代表的函数再去执行下一个,一个任务代表执行的最小单元,那么我们可不可以把任务进行分割呢?试想,如果mcu不断切换任务,那不就能做到看起来是在同时做两件事情吗?也就是说,rtos的本质就是不断切换任务达到模拟同时的效果,它把裸机的任务分割为更小的单元,从更微观的层面分配任务执行的时间。
如何切换任务?
我们将用新的形式表示要执行的函数,也就是任务。我们将通过链表储存任务,将任务添加到TCB(任务控制块)中,而原先要执行的函数的代码,我们会把地址添加到栈(手动分配用来保存信息的空间)中(一个函数的名称就是这个函数的指针,括号是“函数调用运算符”)。大概流程就是:添加任务到链表->运行任务->保存上一个任务入栈再切换->运行->保存上一个任务入栈再切换->运行.........
多说无益,请看下图:
那么我们该如何切换任务呢?在这里,我们不得不采用操作尺度更小的语言:汇编。通过操作寄存器,先保存寄存器的值,再切换任务,再重复这个过程。所以我们要估计任务所需要的栈的空间,这是为了保存任务而存在的。了解以上内容后,现在让我们看看这个:
xReturn=xTaskCreate(
(TaskFunction_t ) AppTaskCreate,
(const char* ) "AppTaskCreate",
(uint16_t )AppTaskCreate_size,
(void* ) pvParameters,
(UBaseType_t ) START_TASK_PRIO,
(TaskHandle_t* )&AppTaskCreate_Handle
);
根据上文所说,读者大概可以推断:AppTaskCreate是创建任务的函数的地址,xTaskCreate函数的代码会保存这个地址,方便随时执行这个任务,"AppTaskCreate"是任务的名字,是为了方便我们调试时辨别任务的,AppTaskCreate_size就是栈了,它需要我们自己估计任务的大小并分配,它的单位是字,因为stm32是32位的,所以实际的字节大小为你输入的数字×4;这个栈会保存任务被切换之前的所有情况,方便还原。pvParameters一般是NULL,那可能大家会好奇,既然是NULL那有什么用呢?其实这就是一个用来传递参数的指针,根据笔者的看法,它是一个独立在任务中的指针,可能会被用来调试等操作。START_TASK_PRIO是优先级, AppTaskCreate_Handle是用于传出任务的句柄。这个句柄将在API 调用中对该创建出来的任务进行引用,如改变任务优先级,删除任务等。pxCreatedTask 一般被设为NULL。
由于本篇只是概论,笔者就不深入讨论了。只要理解rtos的本质是不断切换任务达到模拟同时运行的效果就行了。
既然是切换任务,那么换成谁呢?笔者在上文已经说了,rtos其实就是让任务的时间分配得更加合理了,我们可以显而易见的得出:设置优先级,优先级越高,占用时间越长。
我们把任务分割为了更小的尺度,是否意味着我们可以利用更碎片化的时间?就比如你上班还时不时摸一下鱼。为此,我们引入空闲任务和阻塞延时。
不断切换任务可能会导致某些操作发生混乱,为了解决切换任务带来的麻烦,我们引入临界区。
临界区
临界区,就是一段在执行时不能被中断的代码段,可以类比原子操作那些不可被切割代码,但是不可被切割的部分可以由我们自己选择。试想,如果存在全局变量,在前一个任务中保存了它的值,但是后一个任务进行了更改,那么岂不是会导致事故的发生?简单介绍一下,除了外部中断,系统调度也是产生PendSV中断,也就是说,如果我们关闭所有中断,那岂不是可以保证这段代码安全执行?然后执行完后再进行开启,系统正常运行。
延时篇
我们任务中经常要用到延时,既然我们定义了切换任务的一系列函数,那么可不可以在延时的时候切换任务去做其他的事情呢?答案是肯定的,这也就是我们经常使用的vTaskDelay(),在这个函数中,会触发PendSV中断,进行上下文切换,换成其他优先级高的任务,从而合理利用延时的时间。当然,太小的时间也不行,所以这个函数是以系统时钟节拍周期作为单位的,也就是configTICK_RATE_HZ这个频率所代表的节拍时间周期,从这里,我们就引出systick,即系统滴答计时器,随后一篇的博客我会详细介绍它。当CPU没有任务可做时,我们就会让它执行空闲任务,在FreeRTOS中,空闲任务一般是系统内存的清理工作,当然,我们也可以让单片机进入休眠或者低功耗状态。
优先级与任务调度
很显然,我们在每次调度任务时,总会选择优先级最高的就绪任务执行,保证优先级高的任务得到最及时的响应,笔者将会讲解如何使用查表法等方法找到最高优先级。
FreeRTOS支持的任务调度方法有抢占式、协作式、时间片轮转。抢占式就是:优先级高的任务可以抢占优先级低的任务的资源和时间片,哪怕优先级低的任务正在运行。协作式:任务运行一段时间后,自己放弃CPU运行权,交给其他任务。时间片轮转:让相同优先级的几个任务轮流运行,每个任务运行一个时间片,任务在时间片运行完之后,操作系统自动切换到下一个任务运行,在任务运行的时间片中,也可以提前让出CPU运行权,把它交给下一个任务运行。
时间片就是:同一个优先级下有多个任务,每个任务拥有共同的CPU时间,我们把这个CPU时间称之为时间片。
时钟与时间片(待补充)
FreeRTOS使用的是systick作为时钟源,也就是滴答计时器,在这里,笔者将会重点讲解时钟是如何贯穿整个rtos的。不同于RTT和uc/OS,FreeRTOS的时间片不能指定为多少个时钟节拍,只能是一个ticks。笔者将会讨论同一优先级下如何通过链表和已有的优先级算法实现时间片。
以上几篇大概就是FreeRTOS内核的基本概述了。
容器与对象(预留)
FreeRTOS中并不像RTT那样有对象池这些管理消息队列这些应用,笔者之所以要留下这一篇,不仅是考虑到RTOS的扩展性和移植性,更是为了梳理FreeRTOS各个组成部分所起到的作用。笔者将会从面向对象的思想出发,将各个部分之间的联系梳理清楚,笔者将会着重围绕以下几个对象展开:时钟,中断,任务,优先级,时间片,调度器;并准备参考RTT,构造容器,方便管理对象。
以上就是大蓝图了,笔者将会在接下来的几篇博客中教大家如何手敲一个RTOS。当然,笔者是个鸟科动物,如果你发现文章被鸽了,记得踢我一脚。