一、什么是RTOS
在裸机上写程序,例如51,通常分为两部分:前台系统(中断,中断嵌套)和后台系统(while)
RTOS,实时操作系统,实时性,核心内容在于:实时内核
RTOS操作系统:FreeRTOS,UCOS,RTX,RT-Thread,DJYOS等
二、UCOSII
1.UCOS2中,使用的是抢占实时调度算法,调度原则是在每一个systick(系统时间,每隔一段时间进去一次中断切换任务)的hanlder中,都要去判断下64个任务中所有处于就绪态的任务里,谁的优先级高,就执行谁。
2.ucosii的就绪表设计:64个任务,8个组每个组8个任务,用一个字节的每个位表示这8个组
rdyGrp这个变量拆成8个位来使用,这个变量的值反映哪一个组有就绪任务,然后去查对应的rdytbl
3.任务切换核心:保存上下文(私有栈),恢复要去执行的上下文,然后跳转任务中执行。
4.任务状态:运行态,就绪态,休眠态,挂起态
5.解决临界区有3种方法,中断关闭和使能,中断状态保存到栈,中断状态保存在本地的局部变量
6.函数运行需要栈(存放局部变量)支持,在单片机中整个程序一个栈,OS中因为有任务(进程线程概念)所以需要很多个独立的栈(私有栈)。
7.运行起来后最少有2个任务(也就是进程的概念),状态任务(30)统计各种状态参数(CPU使用率,内存使用率),空任务(31)什么都不干。
互斥锁:1个资源互斥访问,资源保护,任务同步,多个任务等待和唤醒
信号量:至少2个多资源的互斥访问
优先级翻转:ABC任务(a>b>c),c拿到资源,这时候a要资源运行(拿不到所以不能运行),这个时候b事件抢占了资源运行了(ac)。a优先级是最高的但是b先执行了。
如何解决优先级翻转:优先级天花板(拿到资源后优先级提升到最高),优先级继承(谁拿资源就设置为谁的优先级)
旗标:flag实质是16u变量,每一bit代表一个含义,任务通过判断flag状态,从而决定任务运行轨迹(主要用途是任务之间同步)
队列:缓冲,速率匹配,循环使用(接收数据太多,处理跟不上)(可以用数组也可以链表实现,栈因为简单数组来实现就是)
三、RT-Thread
1.在进入main函数之前会先去$Sub$$前缀的main,在原有函数的名字前加上$Sub$$前缀可以将原有函数劫持下来,并通过加上$Super$$前缀再调用原始函数。
3.1、自动初始化机制
通过2个函数实现初始化rt_components_board_init() 与 rt_components_init()
都是使用的else后面的实现,通过把函数指针挨个放入到区间段,然后for遍历挨个实现函数的初始化
用typedef 定义了一个函数指针类型init_fn_t
typedef int (*init_fn_t)(void);
void rt_components_board_init(void)
{
if RT_DEBUG_INIT
int result;
const struct rt_init_desc *desc;
for (desc = &__rt_init_desc_rti_board_start; desc < &__rt_init_desc_rti_board_end; desc ++)
{
rt_kprintf("initialize %s", desc->fn_name);
result = desc->fn();
rt_kprintf(":%d done\n", result);
}
else
const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
{
(*fn_ptr)();
}
endif
}
/**
- RT-Thread Components Initialization
*/
void rt_components_init(void)
{
if RT_DEBUG_INIT
int result;
const struct rt_init_desc *desc;
rt_kprintf("do components intialization.\n");
for (desc = &__rt_init_desc_rti_board_end; desc < &__rt_init_desc_rti_end; desc ++)
{
rt_kprintf("initialize %s", desc->fn_name);
result = desc->fn();
rt_kprintf(":%d done\n", result);
}
else
const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++)
{
(*fn_ptr)();
}
endif
}
如何把函数指针放入到指定位置的,通过宏实现
查看宏代码,fn是函数名,指针
define INIT_EXPORT(fn, level)
/ board init routines will be called in board_init() function /
define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "1")
/ pre/device/component/env/app init routines will be called in init_thread /
/ components pre-initialization (pure software initilization) /
define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "2")
/ device initialization /
define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "3")
/ components initialization (dfs, lwip, ...) /
define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "4")
/ environment initialization (mount disk, ...) /
define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "5")
/ appliation initialization (rtgui application etc ...) /
define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
继续展开INIT_EXPORT,init_fn_t函数指针,其##为连接符,整体就是把fn函数的地址赋值给__rt_init_fn这个函数指针。(例如fn函数名为rti_start,函数指针就是__rt_init_rti_start)
define INIT_EXPORT(fn, level) \
RT_USED const init_fn_t __rt_init_##fn SECTION(".rti_fn." level) = fn
define SECTION(x) __attribute__((section(x)))
展开SECTION
_attribute_((section(x))) :将作用的函数或数据放入指定名为x(level)的输入段中。(在不同的编译器中实现的方式也有所不同。)
总结:作用就是将函数 fn 的地址赋给一个 __rt_init_fn 的指针,然后放入相应 level 的数据段中。所有函数使用自动初始化宏导出后,这些数据段中就会存储指向各个初始化函数的指针。
3.2、线程管理
线程功能特点
RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行。线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该线程的上下文信息恢复。
系统线程有2个,主线程(main)和空闲线程
线程状态
初始状态,就绪状态,运行状态,挂起状态,关闭状态
优先级和时间片
优先级:0为最高优先级,最低优先级默认分配给空闲线程(回收被删除线程的资源,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作)
时间片:时间片起到约束线程单次运行时长的作用
动态线程与静态线程
动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。
3.3、线程间同步
线程的同步方式
线程的同步方式有很多种,信号量(semaphore)、互斥量(mutex)、和事件集(event)等。其核心思想都是:在访问临界区的时候只允许一个 (或一类) 线程运行。
信号量
线程可以获取或释放它,从而达到同步或互斥的目的。每个信号量对象都有一个信号量值和一个线程等待队列。获取信号量,值就-1,释放就+1.
可以运用在多种场合中。形成锁、同步、资源计数等关系(生产者消费者)
锁,单一的锁常应用于多个线程间对同一共享资源(即临界区)的访问
中断与线程间的互斥不能采用信号量(锁)的方式,而应采用开关中断的方式(中断释放信号量,线程处理数据)。
资源计数适合于线程间工作处理速度不匹配的场合(如生产者消费者)
互斥量
互斥量和信号量不同的是:拥有互斥量的线程拥有互斥量的所有权,互斥量支持递归访问且能防止线程优先级翻转;并且互斥量只能由持有线程释放,而信号量则可以由任何线程释放。
互斥量解决优先级翻转,实现的是优先级继承协议
互斥量的使用比较单一
(1)线程多次持有互斥量的情况下。这样可以避免同一线程多次递归持有而造成死锁的问题。
(2)可能会由于多线程同步而造成优先级翻转的情况。
事件集
一个事件集可以包含多个事件,利用事件集可以完成一对多,多对多的线程间同步。
一个线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续的处理;
线程通过 “逻辑与” 或“逻辑或”将一个或多个事件关联起来,形成事件组合。
1)事件只与线程相关,事件间相互独立:每个线程可拥有 32 个事件标志,采用一个 32 bit 无符号整型数进行记录,每一个 bit 代表一个事件;
2)事件仅用于同步,不提供数据传输功能;
3)事件无排队性,即多次向线程发送同一事件 (如果线程还未来得及读走),其效果等同于只发送一次。
与信号量不同的是,事件的发送操作在事件未清除前,是不可累计的,而信号量的释放动作是累计的。信号量只能识别单一的释放动作,而不能同时等待多种类型的释放。
3.4、线程间通信
邮箱
邮箱用于线程间通信,特点是开销比较低,效率较高。邮箱中的每一封邮件只能容纳固定的 4 字节内容(针对 32 位处理系统,指针的大小即为 4 个字节,所以一封邮件恰好能够容纳一个指针)即邮箱也可以传递指针
消息队列
消息队列是一种异步的通信方式。先进先出原则 (FIFO)。
消息队列可以应用于发送不定长消息的场合,包括线程与线程间的消息交换
信号
信号本质是软中断,用来通知线程发生了异步事件,用做线程之间的异常通知、应急处理。
3.5、内存管理
实时系统苛刻:内存分配的时间必须确定,随着运行时间推移内存变的碎片化,内存大小
针对上层应用和系统资源不同分为两类:内存堆管理与内存池管理
而内存堆管理又根据具体内存设备划分为三种情况:
第一种是针对小内存块的分配管理(小内存管理算法);
第二种是针对大内存块的分配管理(slab 管理算法);
第三种是针对多内存堆的分配情况(memheap 管理算法)
内存堆管理
内存堆管理器可以分配任意大小的内存块,非常灵活和方便。但其也存在明显的缺点:一是分配效率不高,在每次分配时,都要空闲内存块查找;二是容易产生内存碎片。
小内存管理算法,slab 管理算法,memheap 管理算法
内存池
避免内存碎片的策略是使用 内存池 + 内存堆 混用的方法。
用于分配大量大小相同的小内存块,它可以极大地加快内存分配与释放的速度,且能尽量避免内存碎片化。支持线程挂起功能:当无空闲内存时申请线程会被挂起(内存资源进行同步的场景,如播放器)