面试中问到 RT-thread嵌入式操作系统相关的问题
RT-thread操作系统调度器的实现细节
- RT-Thread中提供的线程调度器是基于优先级的全抢占式调度:
- 在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。
- 系统总共支持256个优先级(0 ~ 255,数值越小的优先级越高,0为最高优先级,255分配给空闲线程使用,一般用户不使用。
- 在一些资源比较紧张的系统中,可以根据实际情况选择只支持8个或32个优先级的系统配置)。
- 在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。
- 在RT-Thread调度器的实现中,包含了一个共256个优先级队列的数组(如果系统最大支持32个优先级,那么这里将是一个包含了32个优先级队列的数组),每个数组元素中放置相同优先级链表的表头。这些相同优先级的列表形成一个双向环形链表,最低优先级线程链表一般只包含一个idle线程。
- 当所有就绪线程都链接在它们对应的优先级队列中时,抉择过程就将演变为在优先级数组中寻找具有最高优先级线程的非空链表
- RT-Thread内核中采用了基于位图(bitmap)的优先级算法(时间复杂度O(1),即与就绪线程的多少无关),通过位图的定位快速的获得优先级最高的线程
- 每次调度的时间是恒定的:无论当前的系统中存在多少个线程,多少个优先级,rt-thread的调度函数总是可以在一个恒定的时间内选择出最高优先级的那个线程来执行
- 对不同优先级的线程,RT-Thread采用可抢占的方式:即高优先级的线程会“立刻”抢占低优先级的线程
- 相同优先级的线程采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级就绪线程存在的情况下才有效
- 什么是位图?
- 当位图变量取0-255之间的任意一个数字时,它的最低为1的BIT位置都是预知的。我们可以预先将这位图变量的所有取值对应的最低为1的BIT位置(最高优先级)计算出来,并存成一张表格,而只需要查表即可,这个执行时间自然是恒定的.
- 所有取值对应的最低为1的BIT位置
- 若当前最大优先级为32,即优先级位图变量可以使用u32型,也就是等价于4个字节,我们可以对这4个字节从字节0开始依次查表,如果字节0中非0,则最高优先级一定存在于字节0中,我们对字节0查表rt_lowest_bitmap,即可以得到最高优先级
- 如果字节0为0,字节1非0,我们对字节1查表得到的是字节1中为1的最低bit位,然后加上8,就是系统的最高优先级
- 256优先级使用二级位图, 256个bit由32个字节存储,每一个字节的8个bit代表着位图变量中的8个优先级,如果某个字节非0,则表示其中必有非0的bit位。
- 32 * 8 = 256
- 256位一级位图,代表32个字节,分别对应256个线程优先级。比如第一个字节的bit0表示优先级0,bit7表示优先级7。第二个字节bit0表示优先级8,bit7表示优先级15
- 调度算法首先要找出所有线程优先级最高的那个线程优先级
- 优先级别除以8的商取整数即对应位图中的字节
- 优先级别除以8的余数就是对应位图字节中的bit位
RT-thread操作系统的通信机制
- rt-thread操作系统的IPC(Inter-Process Communication,进程间同步与通信)包含有中断锁、调度器锁、信号量、互斥锁、事件、邮箱、消息队列;
- 中断锁、调度器锁、信号量、互斥锁主要用于同步, 事件;
- 邮箱、消息队列主要用于线程间通信
- 关闭中断也叫中断锁,是禁止多任务访问临界区最简单的一种方式,即使是在分时操作系统中也是如此
- 由于关闭中断会导致整个系统不能响应外部中断,所以在使用关闭中断做为互斥访问临界区的手段时,首先必须需要保证关闭中断的时间非常短,例如数条机器指令
- 为了保证一行代码(例如赋值)的互斥运行,最快速的方法是使用中断锁而不是信号量或互斥量
- 对调度器上锁,系统依然能响应外部中断,中断服务例程依然能进行相应的响应
- rt_enter_critical/rt_exit_critical可以多次嵌套调用,但每调用一次rt_enter_critical就必须相对应地调用一次rt_exit_critical退出操作,嵌套的最大深度是65535
- 因为使用了调度器锁后,系统将不再具备优先级的关系,直到它脱离了调度器锁的状态
- 线程可以获取或释放它,从而达到同步或互斥的目的
- 使用信号量会导致的另一个潜在问题是线程优先级翻转
- 所谓优先级翻转问题即当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞,实时性难以得到保证
- 线程优先级反转:
- 有优先级为A、B和C的三个线程,优先级A> B > C。线程A,B处于挂起状态,等待某一事件触发,线程C正在运行,此时线程C开始使用某一共享资源M。在使用过程中,线程A等待的事件到来,线程A转为就绪态,因为它比线程C优先级高,所以立即执行。但是当线程A要使用共享资源M时,由于其正在被线程C使用,因此线程A被挂起切换到线程C运行。如果此时线程B等待的事件到来,则线程B转为就绪态。由于线程B的优先级比线程C高,因此线程B开始运行,直到其运行完毕,线程C才开始运行。
- 将线程C的优先级提升到线程A的优先级别,从而解决优先级翻转引起的问题
- 线程B先于线程A运行
- RT-Thread操作系统中实现的是优先级继承算法, 在使用资源时使用的是优先级继承, 优先级与当前等待线程中最高优先级, 释放了资源则回到之前的优先级
- 提高某个占有某种资源的低优先级线程的优先级,使之与所有等待该资源的线程中优先级最高的那个线程的优先级相等,然后执行,而当这个低优先级线程释放该资源时,优先级重新回到初始设定
- 一般资源计数类型多是混合方式的线程间同步,因为对于单个的资源处理依然存在线程的多重访问,这就需要对一个单独的资源进行访问、处理,并进行锁方式的互斥操作
- 两个线程用来进行任务间的执行控制转移,信号量的值初始化成具备0个信号量资源实例(信号量的值初始化为0),而等待线程先直接在这个信号量上进行等待
- 信号量在作为锁来使用时,通常应将信号量资源实例初始化成1(信号量的值初始化为1),代表系统默认有一个资源可用
- 因为信号量的值始终在1和0之间变动,所以这类锁也叫做二值信号量因为信号量的值始终在1和0之间变动,所以这类锁也叫做二值信号量
- 中断与线程间的互斥不能采用信号量(锁)的方式,而应采用中断锁
- 另外需要切记的是互斥量不能在中断服务例程中使用。信号量则可用于中断与线程同步
- 事件主要用于线程间的同步, 与信号量不同,它的特点是可以实现一对多,多对多的同步; 一个线程可等待多个事件的触发
- 这种多个事件的集合可以用一个32位无符号整型变量来表示,变量的每一位代表一个事件,线程通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件集。事件的“逻辑或”也称为是独立型同步,指的是线程与任何事件之一发生同步;事件“逻辑与”也称为是关联型同步,指的是线程与若干事件都发生同步。
- 每个线程拥有32个事件标志,采用一个32 bit无符号整型数进行记录,每一个bit代表一个事件。若干个事件构成一个事件集;
- 事件仅用于同步,不提供数据传输功能;
- 事件无排队性
- 事件的发送操作在事件未清除前,是不可累计的,而信号量的释放动作是累计的。 事件另外一个特性是,接收线程可等待多种事件,即多个事件对应一个线程或多个线程。
- 这个特性也是信号量等所不具备的,信号量只能识别单一的释放动作,而不能同时等待多种类型的释放
- RT-thread线程间通信
- 邮箱
- 邮箱服务是实时操作系统中一种典型的任务间通信方法,特点是开销比较低,效率较高
- 每一封邮件只能容纳固定的4字节内容(每一封邮件恰好容纳一个指针)
- 典型的邮箱也称作交换消息,线程或中断服务例程把一封4字节长度的邮件发送到邮箱中。而一个或多个线程可以从邮箱中接收这些邮件进行处理。
- 邮箱通信机制有点类似于传统意义上的管道,用于线程间通讯
- 非阻塞方式的邮件发送过程能够安全的应用于中断服务中,是线程,中断服务,定时器向线程发送消息的有效手段
- 通常来说,邮件收取过程可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间
- 当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中
- 最重要的是,在32位系统上4字节的内容恰好适合放置一个指针,所以邮箱也适合那种仅传递指针的情况
- 消息队列
- 消息队列是另一种常用的线程间通讯方式,它能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中
- 其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的消息到达时,挂起的线程将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。
- 当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等
- 消息队列可以应用于发送不定长消息的场合,包括线程与线程间的消息交换,以及中断服务例程中发送给线程的消息(中断服务例程不可能接收消息)
- 消息队列和邮箱的明显不同是消息的长度并不限定在4个字节以内,另外消息队列也包括了一个发送紧急消息的函数接口
- 发送同步消息的问题,这个时候就可以根据当时的状态选择相应的实现:两个线程间可以采用[消息队列+信号量或邮箱]的形式实现
- 邮箱做为确认标志,代表着接收线程能够通知一些状态值给发送线程;而信号量作为确认标志只能够单一的通知发送线程,消息已经确认接收
- 邮箱
STM32上电启动过程
-
stm32上电启动过程详解
- 微控制器(单片机)上电后, 是如何寻找和执行main函数的?
- main函数的入口地址在微控制器的内部存储空间中不再是绝对不变的
- 启动文件, 英文名字叫做
bootloader
- 每一种微控制器(处理器)都必须有启动文件,启动文件的作用便是负责执行微控制器从“复位”到“开始执行main函数”中间这段时间(称为启动过程)所必须进行的工作;这段时间需要做那些工作呢?
- ARM7/ARM9内核的控制器在复位后,CPU会从存储空间的绝对地址0x000000取出第一条指令执行复位中断服务程序的方式启动,即固定了复位后的起始地址为0x000000(PC = 0x000000)同时中断向量表的位置并不是固定的;
- Cortex-M3内核则正好相反,有3种情况:
- 通过boot引脚设置可以将中断向量表定位于SRAM区,即起始地址为0x2000000,同时复位后PC指针位于0x2000000处;
- 通过boot引脚设置可以将中断向量表定位于FLASH区,即起始地址为0x8000000,同时复位后PC指针位于0x8000000处;
- 通过boot引脚设置可以将中断向量表定位于内置Bootloader区;
- Cortex-M3内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在Cortex-M3内核复位后,会自动从起始地址的下一个32位空间取出复位中断入口向量,跳转执行复位中断服务程序;
- 上电后第一个要执行的程序是复位中断服务程序
- 对比ARM7/ARM9内核,Cortex-M3内核则是固定了中断向量表的位置而起始地址是可变化的;
- 是stm32的大致的启动过程:
- 首先对栈和堆的大小进行定义,并在代码区的起始处建立中断向量表,其第一个表项是栈顶地址,第二个表项是复位中断服务入口地址。
- 然后在复位中断服务程序中跳转C/C++标准实时库的__main函数,完成用户堆栈等的初始化后,跳转.c文件中的main函数开始执行C程序。
- 假设STM32被设置为从内部FLASH启动(这也是最常见的一种情况),中断向量表起始地位为0x8000000,则栈顶地址存放于0x8000000处,而复位中断服务入口地址存放于0x8000004处。 (上电启动最重要的两个因素, 栈顶地址位置存放位置, 复位中断服务程序入口地址, 复位后是调用复位中断程序, 中断服务入口地址一般紧跟在栈顶地址后面, 32维微处理器, 所以地址是加4; 在复位中断服务程序中会跳转到__main函数执行, 即main函数入口地址执行)
- 当STM32遇到复位信号后,则从0x80000004处取出复位中断服务入口地址,继而执行复位中断服务程序,然后跳转__main函数,最后进入mian函数,来到C的世界
-
知乎上的讲解
- 初始化堆栈指针
- 初始化 PC 指针
- 初始化中断向量表
- 配置系统时钟
- 调用 C 库函数_main
- 启动文件中宏定义的讲解
-
比较详细的讲解
- 当从主闪存(Main Flash memory)启动时,启动空间别名区映射到Flash。
- 当从系统内存(System memory)启动时,启动空间别名区映射到System memory。
- 当从内部SRAM(Embedded SRAM)启动时,启动空间别名区映射到SRAM
- 默认是这样的映射关系,但上述的映射关系启动后可以由软件修改(通过修改SYSCFG启动器的MEM_MODE位域)
- Flash、System memory和SRAM分别可以从别名区和原始地址处访问
- Flash:访问地址为0x00000000或0x08000000
- System memory:访问地址为0x00000000或0x1FF00000
- SRAM:启动时地址为0x00000000或0x20000000(STM32Fxx的参考手册上说,启动后只能在0x20000000开始访问,即启动后这个映射消失,需要重定位中断向量表,这是特例)
- System memory内置了ST提供的boot loader,可以通过该boot loader下载程序到Flash中
- 用户程序实际只能存储在Flash中,且能在Flash和SRAM中执行(因为cortex-m3核采用哈佛结构,代码可直接在Flash运行,冯•诺依曼结构则必须将代码拷贝至RAM运行)
- Flash就像是电脑的硬盘,用于存储代码;
- SRAM就像是电脑的内存,里面的数据都是动态的,掉电就丢失的,用于建立堆栈等
- System memory就像是电脑的ROM,里面的程序有芯片厂商写好,用户不可改写
- 对于用户代码来说,只需要考虑从Flash启动和从SRAM启动两种情况
- Cortex-m3核的中断向量表是不变的(中断向量表每一项为4个字节,中断向量表的第一项:栈顶,中断向量表的第二项:复位向量……,中断向量表每项内容可以看官方的启动文件,或者查看相关的手册),只需要用户设定表头的地址
- 默认情况下,从Flash启动,中断向量表从Flash的起始地址(0x08000000)开始存放。同时映射到0x00000000处。向量表偏移寄存器(VTOR)的值为0x00000000(实际映射到0x08000000)
- 若从SRAM启动,中断向量表还是存放在Flash中(Flash才能固化存储,SRAM只能加电才有效),只不过拷贝到SRAM的首地址0x20000000处。此时向量表偏移寄存器(VTOR)的值也是0x00000000(实际映射到0x20000000)
- 而启动过程结束后,这个特殊的映射不复存在了
- 需要修改向量表偏移寄存器(VTOR)的值为0x20000000以后的值(其中TBLOFF位域要是0x200的倍数,这个是字对齐的要求
- 无论用哪种模式启动,复位时栈顶指针总能在0x00000000(或0x08000000)处找到,而复位向量总能在0x00000004(或0x08000004)处找到
- 复位时,CPU从0x00000000处获取栈顶指针MSP(默认使用主堆栈),从0x00000004处获取程序计数器PC(复位向量)
- 调用SystemInit函数。这个函数里面开启了外部晶振,设置了PLL,除能了所有中断,设置了时钟为32MHz,并且重定位中断向量表在0x08000000处(这句在Flash启动时可以不需要,因为能从0x00000000映射到0x08000000)
- 上电复位,CPU从0x00000000处获取栈顶指针MSP(默认使用主堆栈),从0x00000004处获取程序计数器PC(复位向量)。
- MSP指针必然指向SRAM区的,因为堆栈必须建立在该区。
- 根据PC的值找到复位中断处理函数Reset_Handler
- 在复位中断服务程序中,调用SystemInit函数。
- 在复位中断服务程序中,调用__main函数,初始化用户堆栈
- 调用main函数,进入C语言环境
- 当选择从主Flash启动模式后,芯片一上电,Flash的0x0800 0000地址被映射到0地址处,不影响CM3内核的读取; 所以这时的CM3既可以在0地址处访问中断向量表,也可以在0x0800 0000地址处访问中断向量表,而代码还是在0x0800 0000地址处存储的
- 这个中断向量表是可以在程序中再次被映射的, 控制它的就是CM3已经规定的NVIC寄存器SCB->VTOR
- 可以看出中断向量重映射是一个选择性编译,通常宏定义VECT_TAB_SRAM都没有被定义,所以这里执行结束后,SCB->VTOR就是FLASH_BASE了,值为0x0800 0000。
- 以后CM3再取中断向量里,就会根据SCB->VTOR的设置,从这里取向量执行了
- 上电后根据boot引脚来决定PC位置,比如boot设置为flash启动,则启动后PC跳到0x08000000
- stm32的ISP和IAP区别和联系:
- ISP(In-System Programming)在系统可编程,指电路板上的空白器件可以编程写入最终用户代码, 而不需要从电路板上取下器件,已经编程的器件也可以用ISP方式擦除或再编程
- IAP(In-Application Programming) 指MCU可以在系统中获取新代码并对自己重新编程,即可用程序来改变程序
- IAP的实现相对要复杂一些,在实现IAP功能时, 单片机内部一定要有两块存储区,一般一块被称为BOOT区,另外一块被称为存储区
- 单片机上电运行在BOOT区,如果有外部改写程序的条件满足,则对存储区的程序进行改写操作
-
stm32f40X的启动分析
- boot0/boot1是选择上电时, 0地址和04地址的映射位置(分别对应栈指针SP的初始值和程序计数指针PC的初始值)
- 内部FLASH启动方式:
- 当芯片上电后采样到BOOT0引脚为低电平时,0x00000000和0x00000004地址被映射到内部FLASH的首地址0x08000000和0x08000004
- 意思我上电一开始读0x00000000地址上的值, 程序实际读到的值是地址0x08000000上的值, 因为地址被映射了呀
- 同理, 上电本应该读取0x00000004地址作为中断向量首地址, 但它被映射到了0x08000000地址上, 所以读取的是0x08000000地址上的值
- 内部SRAM启动方式:
- 芯片上电后采样到BOOT0和BOOT1引脚均为高电平时,0x00000000和0x00000004地址被映射到内部SRAM的首地址0x20000000和0x20000004,内核从SRAM空间获取内容进行自举, 自举就是设置运行环境并执行主体程序
- 系统存储器启动方式
- 当芯片上电后采样到BOOT0引脚为高电平,BOOT1为低电平时,内核将从系统存储器的0x1FFF0000及0x1FFF0004获取MSP及PC值进行自举
- 系统存储器是一段特殊的空间,用户不能访问,ST公司在芯片出厂前就在系统存储器中固化了一段代码
- 因而使用系统存储器启动方式时,内核会执行该代码,该代码运行时,会为ISP提供支持(In System Program),如检测USART1/3、CAN2及USB通讯接口传输过来的信息,并根据这些信息更新自己内部FLASH的内容,达到升级产品应用程序的目的,因此这种启动方式也称为ISP启动方式。
- 启动代码可是要求运行速度非常快, 所以利用的是汇编编写