4月30日看到大家都纷纷发了自己的读书感受我有些着急了,匆匆忙忙的发了初始读《RT-Thread内核实现与应用开发实战指南》的初始感受,当我发了一篇后,我就静下心来读别人的读书笔记了,我发现了自己和他人的几点差距,我写的东西,没有标注出重点,让读者不易读,很多东西前后的逻辑不够,而感受颇多。我做过的工程有CAN通讯,LWIP的移植,SPWM驱动步进电机,AMIS30543驱动42步进电机,powerstep01驱动86步进电机。所有的这些工程,从编译的hex文件来看,要数LWIP的工程最大,仍使用前后台可以实现。我至今不知道什么时候必须用操作系统的方式进行编程,不过我通过学习RT-Thread内核实现与应用开发实战指南认识到嵌入式操作系统的一大优势是实时性,但是相对于裸机系统的规范,要求,概念就多太多了。因为我不理解多线程系统,我又翻看了书中多线程的概念:相比前后台系统,多线程系统的事件响应也是在中断中完成的,但是事件的处理是在线程中完成的。在多线程系统中,线程跟中断一样,也具有优先级,优先级高的线程会被优先执行。当一个紧急的事件在中断被标记之后,如果事件对应的线程的优先级足够高,就会立马得到响应。相比于前后台系统,多线程的实时性又被提高了。在多线程系统中,根据程序的功能,我们把这个程序主体分割成一个个独立的,无限循环且不能返回的小程序,这个小程序我们称之为线程。每个线程都是独立的,互不干扰的,且具备自身的优先级,它由操作系统调度管理。对于书中的一句话:“加入操作系统,我们的编程反而变得简单了。整个系统随之带来的额外开销就是操作系统占据的那一丁点的FLASH和RAM。”我不理解,对于我来说嵌入式操作系统的很多定义,概念运行原理,我一头雾水,这句话既没有给我增加信心,还让我对自己提出了质疑。
在书中的第6章——线程的定义与线程切换的实现,这一章的重要性让我难以用词汇描述,可以说当我们建造高楼大厦的时候,这章的内容就像高楼大厦的地基,地基打不好,高楼大厦容易倒塌。如果用我们从小到大的学习生涯来比喻,这一章的内容就像我们学习的拼音语言一样重要。那么大家肯定疑问:“什么是线程呢?”在书中这样描述:在多线程系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为线程。这句话描述了线程的特点,但是我却疑问线程到底有怎么样的优势。带着这样的疑问我只能继续去了解线程的实现。在定义线程栈这一节的内容讲了:“在线程和裸机系统的区别是:在裸机系统中,如果有全局变量,有子函数调用,有中断发生。那么系统在运行的时候,全局变量放在哪里,子函数调用时,局部变量放在哪里,中断发生时,函数返回地址放在哪里,我们不用管。而写一个RTOS,这些种种环境参数,我们必须弄清楚他们是如何存储的。在裸机系统中,他们统统放在一个叫栈的地方,栈是单片机RAM中里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里指定,最后由C库函数main进行初始化。但是,在多线程系统中,每个线程都是独立的,互不干扰的,所以要为每个线程分配独立的空间,这个栈空间通常是一个预先定义好的数组,也可以是动态分配的一段空间,但它们都存在RAM中。”这里再次强调了线程栈的优点:独立性。这一点,我在理解的时候一定要牢记。线程是一种特殊的函数,这个函数的特点是独立的,函数主体无线循环且不能返回。而在主函数里面实现线程的转换就需要定义线程的控制块。在定义线程控制块这一节,书中讲到:“在裸机系统中,程序的主体是CPU按照顺序执行的。而在多线程系统中,线程的执行是系统调度的。系统为了顺利的调度线程,为每个线程都额外定义了一个线程控制块,这个线程控制块就相当于线程的身份证,里面存有线程的所有信息,比如线程的栈指针,线程的名称,线程的形参等。有了这个线程控制块就相当于线程之后,以后系统对线程的全部操作都可以通过这个线程控制块来实现。定义一个线程控制块需要一个新的数据类型,该数据类型在rtdef.h这个头文件中声明,使用它可以为每个线程定义一个线程控制块实体。”除了定义线程控制块,线程的栈,线程的实体函数最终需要联系起来才能由系统进行统一调度。这个联系的过程我们叫做线程的初始化函数,在这个函数的函数体中一个非常重要的概念我并不懂得应用,这个概念是链表,书中这样讲到:“在初始化线程链表节点,往后我们把线程插入到各种链表中,就是通过这个节点来实现的,它就好像是线程控制块里面的一个钩子,可以把线程控制块挂在各种链表中,在初始化之前我们需要在线程控制块中添加一个线程链表节点。”这里我对链表的理解还是一知半解。希望有人能帮我解释一下!
在这里补充一下百度对链表的解释:“链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。”对于单个链表的初始化就是将节点里面的next和prev这两个节点指针指向节点本身。在双向链表表头后面插入一个节点第一步是将以前前面prev指针指向新节点。第二步将新节点的next指针指向1节点的next指针指向的节点,第三步将1的next指针指向n节点,n的prev指针指向1节点。像双向链表的表头后面插入一个节点,从双向链表删除一个节点和以上说的操作类似。初始化线程函数的第二步就是将线程入口保存到线程控制块的entry成员中。以及其他的成员都进行相应的保存。最后一步是初始化线程栈,并返回线程栈顶指针。当线程第一次运行的时候,加载到CPU寄存器的环境参数我们要预先初始化好。从栈顶开始,初始化的顺序固定,首先是异常发生时自动保存的8个寄存器,即xPSR、R15、R14、R12、R3、R2、R1和R0。其中xPSR寄存器的位24必须是1,R15的PC指针必须存的是线程的入口地址,R0必须是线程形参,剩下的R14、R12、R3、R2和R1我们初始化为0。剩下的是8个需要手动加载到CPU寄存器的参数,即R4~R11,默认初始化为0xdeadbeaf。初始化线程以后,书中继续讲解了如何实现就绪列表。线程创建好以后,我们需要把线程添加到就绪列表里面,表示线程已经就绪,系统随时可以调度,进一步书中讲解了如何实现调度器。 调度器是操作系统的核心,其主要功能就是实现线程的切换,即从就绪列表里面找到优先级最高的线程,然后去执行该线程。调度器在使用之前必须先初始化。首先定义一个局部变量,用C语言关键词register修饰,防止被编译器优化。初始化线程就绪列表,初始化完后,整个就绪列表为空。然后初始化当前线程控制块指针为空。rt_current_thread是定义的一个全局指针,用于指向当前正在运行的线程的线程控制块。一般我们把调度器初始化放在硬件初始化之后,线程创建之前。然后我们再启动调度器。 如何启动调度器呢?启动调度器函数名是void rt_system_scheduler_start()函数,调度器在启动的时候会从就绪列表中取出优先级最高的线程的线程控制块,然后切换到改线程。但是目前我们的线程还不支持优先级,那么就手动指定第一个运行的线程为就绪列表下标为0的这条链表里面挂着的线程。rt_list_entry()是一个已知一个结构体里面的成员的地址,反推出结构体的首地址的宏。rt_hw_context_switch_to()函数实现第一次切换到新的线程。这是一个汇编语言实现的函数。在此函数中PendSV_Handles()函数是真正实现线程上下文切换的地方。
我们知道了调度器的实现,下一步就是应用调度器实现系统调度。系统调度就是在就绪表中寻找优先级最高的就绪线程,然后去执行改线程。但是目前我们还不支持优先级,仅实现两个线程轮流切换,系统调度函数rt_schedule()实现。rt_hw_contex_switch()函数用于产生上下文切换。
最后,就是整个的main()函数了,main函数首先对硬件初始化,然后初始化线程1,接着将线程1插入到就绪表中,然后初始化线程2,将线程2插入到就绪表中,启动系统调度器,在线程中实现线程的切换。
载或用于商业用途需征得作者同意并注明出处