RT-thread
一种实时多线程的操作系统,任务通过线程实现,其线程调度器相当于RTOS中的任务调度器
相较于Linux系统,其体积小、功耗低、启动快,实时性高
与freeRTOS的对比


标准版本,Nano版本(简化版,代码简单移植简单),Smart版本(面向带MMU,中高端应用芯片)

两种系统的任务(线程)运行逻辑有所不同,他们的链表形式不同,rtt中先创建的线程先执行,freertos中后创建的任务先运行。
线程管理
分为系统线程和用户线程(在rt中有空闲线程和主线程)。线程调度是抢占式的,从就绪线程列表中查找最高优先级线程。线程切换时,保存上下文。
线程控制块: 是管理线程的一个数据结构,会存放线程的一些信息,例如优先级、名称、状态、链表结构、等待事件集合等。
线程栈
线程五种状态

优先级(0最高)
时间片(仅对优先级相同的就绪态线程有效)
线程必须要有让出CPU使用权的动作,如调用延时函数或者主动挂起。因为如果一个高优先级的线程陷入死循环,那么比他低优先级的线程都不能够得到执行。
动态线程与静态线程,静态对象会占用RAM空间,不依赖于内存堆管理器,内存分配时间确定。动态对象则依赖于内存堆 管理器,运行时申请RAM空间,当对象被删除后,占用的RAM空间被释放。如果需要更高的灵活性和适应性,动态线程更为合适;而如果需要固定的资源和更高的稳定性,静态线程可能是更好的选择。
注:rt-thread中创建对象,一般都分为动态创建与静态创建。动态创建时,系统会自动分配空间,我们用指针接收地址就好,而使用静态创建,也就是初始化的时候,要自己创建结构体,把结构体地址传给函数。
动:系统自动从动态内存堆上分配栈空间与线程句柄
动态创建与静态初始化各自的优缺点以及适用场景
动态创建:
程序运行时才创建,避免了预先分配内存造成的资源浪费,但是会引入额外的性能开销,由于涉及动态内存分配,可能导致分配失败和延迟。
静态初始化:
通常是在程序启动时就已经初始化,避免了运行时的内存开销,更加高效。但是灵活性不足,适用于对于实时性要求高的场景,以及已知在设计阶段所需的数量。


静:由用户分配栈空间与线程句柄
rt_err_t rt_thread_init(struct rt_thread* threadconst char* name, void (*entry)(void* parameter), void* parameter, void* stack_start, rt_uint32_t stack_size, rt_uint8_t priority, rt_uint32_t tick);
静态线程的线程控制块指针以及线程栈(全局缓冲区)需要用户自己提供。
启动线程

删除线程
rt_err_t rt_thread_delete(rt_thread_t thread);删除动态线程
rt_err_t rt_thread_detach (rt_thread_t thread);删除静态线程
要注意能运行完毕的线程, RT-Thread在线程运行完毕后,自动删除线程
其他函数
rt_thread_t rt_thread_self(void)
在程序的运行过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过这个的函数接口获得当前执行的线程句柄
rt_err_t rt_thread_yield(void);
调用该函数后,当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优 先级队列链表的尾部,然后激活调度器进行线程上下文切换
rt_err_t rt_thread_sleep(rt_tick_t tick);
rt_err_t rt_thread_delay(rt_tick_t tick);
rt_err_t rt_thread_mdelay(rt_int32_t ms);
调用它们可以使当前线程挂起一段指定的时间,当这个时间过后,线程 会被唤醒并再次进入就绪状态。
rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);
当需要对线程进行一些其他控制时,例如动态更改线程的优先级,可以调用这个函数

rt_err_t rt_thread_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));
空闲钩子函数是空闲线程的钩子函数,如果设置了空闲钩子函数,就可以在系统执行空闲线程时,自动执行空闲钩子函数来做一些其他事情,比如系统指示灯。因为空闲线程永远处于就绪,所以设置的钩子函数必须保证任何时刻都不会挂起。
void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to) );
线程切换钩子函数,在线程切换时会被调用,可以用来查看线程切换时的信息,比如从哪个线程切换到哪个线程。
时钟管理
操作系统需要提供一个时钟节拍,以供系统处理所有和时间有关的事件,如线程的延时、线程的时间片轮转调度以及定时器超时等。时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳。
在rt-thread中,时钟节拍由配置为中断触发模式的硬件定时器产生,当中断到来时,将调用一次:void rt_tick_increase(void),通知操作系统已经过去一个系统时钟。全局变量rt_tick在每经过一个时钟节拍时,值就会加1。
调用rt_tick_get会返回当前rt_tick 的值,即可以获取到当前的时钟节拍值。此接口可用于记录系统的运行时间长短,或者测量某任务运行的 时间。
rt_tick_t rt_tick_get(void)
定时器
两种定时模式:单次定时与循环定时。根据超时函数执行时所处的上下文环境,分为硬件定时器(中断环境)与软件定时器(软件环境),当SOFT_TIMER 模式被启动后,系统会在初始化时创建一个timer线程,这个线程的主要职责是管理和调度所有的软定时器,然后SOFT_TIMER模式的定时器超时函数在都会在 timer 线程的上下文环境中执行。 在创建定时器的create函数的flag标志位中进行设置,当指定的flag为RT_TIMER_FLAG_HARD_TIMER 时,如果定时器超时,定时器的回调函数将在时钟中断的服务例程上下文中被调用;当指定的flag为 RT_TIMER_FLAG_SOFT_TIMER 时,如果定时器超时,定时器的回调函数将在系统时钟timer线程的上 下文中被调用。
定时器工作机制

定时器控制块

定时器创建
动态

静态

控制定时器
![]()

线程同步
同步是指按预定的先后次序进行运行,线程同步是指多个线程通过特定的机制(如互斥量,事件对象, 临界区)来控制线程之间的执行顺序,也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间将是无序的
线程的同步方式有很多种,其核心思想都是:在访问临界区的时候只允许一个(或一类)线程运行。
实现同步的方式:信号量、互斥量、事件集
信号量
线程可以获取或释放它,从而达到同步或 互斥的目的。

线程通过获取信号量来获得信号量资源实例,当信号量值大于零时,线程将获得信号量,并且相应的 信号量值会减1,获取信号量使用下面的函数接口:
rt_err_t rt_sem_take (rt_sem_t sem, rt_int32_t time);
在调用这个函数时,如果信号量的值等于零,那么说明当前信号量资源实例不可用,申请该信号量的线 程将根据time参数的情况选择直接返回、或挂起等待一段时间、或永久等待
释放信号量可以唤醒挂起在该信号量上的线程。释放信号量使用下面的函数接口:
rt_err_t rt_sem_release(rt_sem_t sem);
互斥量(类似于二值信号量,区别是二值信号量可以由不同的线程释放,而互斥量只能由同一线程释放) 锁
信号量会有优先级反转问题,而互斥量有优先级继承机制,解决了优先级反转问题。通过在线程A尝试获取共享资源而被挂起的期间内,将线程C的优先级提升到线程A的优先级别,从而 解决优先级翻转引起的问题。这样能够防止C(间接地防止A)被B抢占。

事件集
它的特点是可以实现一对多,多对多的同步。即一个 线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续 的处理;同样,事件也可以是多个线程同步多个事件。这种多个事件的集合可以用一个32位无符号整型变 量来表示,变量的每一位代表一个事件,线程通过“逻辑与”或“逻辑或”将一个或多个事件关联起来,形成事件组合。

发送事件
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);
使用该函数接口时,通过参数set指定的事件标志来设定event事件集对象的事件标志值,然后遍历 等待在event事件集对象上的等待线程链表,判断是否有线程的事件激活要求与当前event对象事件标志 值匹配,如果有,则唤醒该线程。
接收事件
rt_err_t rt_event_recv(rt_event_t event, rt_uint32_t set, rt_uint8_t option, rt_int32_t timeout, rt_uint32_t* recved);
当用户调用这个接口时,系统首先根据set参数和接收选项option来判断它要接收的事件是否发生, 如果已经发生,则根据参数option上是否设置有RT_EVENT_FLAG_CLEAR来决定是否重置事件的相应 标志位,然后返回(其中recved参数返回接收到的事件);如果没有发生,则把等待的set和option参数 填入线程本身的结构中,然后把线程挂起在此事件上,直到其等待的事件满足条件或等待时间超过指定的 超时时间。如果超时时间设置为零,则表示当线程要接受的事件没有满足其要求时就不等待,而直接返回 RT_ETIMEOUT。
set是事件掩码,option是事件选项,可以给的值有,RT_EVENT_FLAG_OR, RT_EVENT_FLAG_AND,RT_EVENT_FLAG_CLEAR。
recved是指向接收到的事件掩码的指针
线程通信
消息队列
消息队列和邮箱的明显不同是消息的长度并不限定在4个字节以内
消息队列能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。 其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的 消息到达时,挂起的线程将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。

消息队列工作机制
发送者每次将消息发送到队尾,接收者每次从队首取消息。当有多个消息发送到消息队列时,通常将先进入消息队列的消息先传给线程, 也就是说,线程先得到的是最先进入消息队列的消息。但是当发送紧急消息时,从空闲消息链表上取下来的消息块不是挂到消息队列的队尾,而是挂到队首,这样,接收者就能够优先接收到紧急消息。
邮箱
RT-Thread 操作系统的邮箱用于线程间通信,特点是开销比较低,效率较高。邮箱中的每一封邮件只能容纳固定的4字节内容,线程或中断服务例程把一封4字节长度的邮件发送 到邮箱中,而一个或多个线程可以从邮箱中接收这些邮件并进行处理。

当一个线程向邮箱发送邮件时,如果邮箱没满,将把邮件复制到邮箱中。如果邮箱已经满了,发送线 程可以设置超时时间,选择等待挂起或直接返回-RT_EFULL。如果发送线程选择挂起等待,那么当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送。
当一个线程从邮箱中接收邮件时,如果邮箱是空的,接收线程可以选择是否等待挂起直到收到新的邮 件而唤醒,或可以设置超时时间。当达到设置的超时时间,邮箱依然未收到邮件时,这个选择超时等待的 线程将被唤醒并返回-RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的4个字节邮件到接收缓存中。
信号
信号(又称软中断)用于通知目标线程发生了特定事件,其处理类似于中断,用于线程异常通知,应急处理。线程之间可以互相 通过调用rt_thread_kill() 发送软中断信号。
安装
一个线程想要对某个信号进行处理,首先要在这个贤臣中安装信号,使用
rt_sighandler_t rt_signal_install(int signo, rt_sighandler_t[] handler);
其中rt_sighandler_t 是定义信号处理函数的函数指针类型,sigo是信号编号
阻塞
信号阻塞,也可以理解为屏蔽信号。如果该信号被阻塞,则该信号将不会递达给安装此信号的线程
void rt_signal_mask(int signo);
解除阻塞
线程中可以安装好几个信号,使用此函数可以对其中一些信号给予“关注”,那么发送这些信号都会引 发该线程的软中断。调用rt_signal_unmask()可以用来解除信号阻塞:
void rt_signal_unmask(int signo);
发送
调用rt_thread_kill()可以用来向 任何线程发送信号
int rt_thread_kill(rt_thread_t tid, int sig);
tid是线程句柄
内存管理
rt中对内存的管理,大体可以分为内存堆管理和内存池管理。内存堆管理用于管理一段连续的内存空间,内存堆可以在当前资源满足的情况下,根据用户的需求分配任意大小的内存块。而当用户不需要再使 用这些内存块时,又可以释放回堆中供其他应用分配使用。RT-Thread系统为了满足不同的需求,提供了 不同的内存管理算法,分别是小内存管理算法、slab管理算法和memheap管理算法。
小内存管理算法
一种简单的内存分配算法,初始化时有一块大内存,当需要分配内存块时,从这块大内存上分割出相匹配的内存快,并把剩余的空闲内存块归还。每个内存块都包含一个管理用的表头。算法例子说明如下图

如下图所示的内存分配情况,空闲链表指针lfree初始指向32字节的内存块。当用户线程要再分配一 个64字节的内存块时,但此lfree指针指向的内存块只有32字节并不能满足要求,内存管理器会继续寻 找下一内存块,当找到再下一块内存块,128字节时,它满足分配的要求。因为这个内存块比较大,分配 器将把此内存块进行拆分,余下的内存块(52字节)继续留在lfree链表中,分配之后如下图所示。

另外,在每次分配内存块前,都会留出12字节数据头用于magic、used信息及链表节点使用。
slab管理算法
slab分配器会根据对象的大小分成多个区(zone),也可以看成每类对象有一个内存池,如下图所示,没看懂,过。
memheap管理算法
memheap 工作机制如下图所示,首先将多块内存加入memheap_item链表进行粘合。当分配内存 块时,会先从默认内存堆去分配内存,当分配不到时会查找memheap_item链表,尝试从其他的内存堆 上分配内存块。应用程序不用关心当前分配的内存块位于哪个内存堆上,就像是在操作一个内存堆。

在使用内存堆时,必须要在系统初始化的时候进行堆的初始化
void rt_system_heap_init(void* begin_addr, void* end_addr);
参数为堆内存起始地址与堆内存结束地址
在使用memheap堆内存时,必须要在系统初始化的时候进行堆内存的初始化,可以通过下面的函数 接口完成:
rt_err_t rt_memheap_init(struct rt_memheap *memheap, const char *name, void *start_addr, rt_uint32_t size)
如果有多个不连续的memheap可以多次调用该函数将其初始化并加入memheap_item链表。
内存堆的管理

从内存堆上分配用户指定大小的内存块
void *rt_malloc(rt_size_t nbytes);
应用程序使用完从内存分配器中申请的内存后,必须及时释放,否则会造成内存泄漏,释放内存块的 函数接口如下:
void rt_free (void *ptr);
从内存堆中分配连续内存地址的多个内存块,可以通过下面的函数接口完成:
void *rt_calloc(rt_size_t count, rt_size_t size);
内存池
内存堆管理器可以分配任意大小的内存块,非常灵活和方便。但其也存在明显的缺点:一是分配效率 不高,在每次分配时,都要空闲内存块查找;二是容易产生内存碎片。为了提高内存分配的效率,并且避免内存碎片,RT-Thread提供了另外一种内存管理方法:内存池(MemoryPool)
内存池在创建时先向系统申请一大块内存,然后分成同样大小的多个小内存块,小内存块直接通过链 表连接起来(此链表也称为空闲链表)。每次分配的时候,从空闲链表中取出链头上第一个内存块,提供给 申请者。

内存池管理

IO设备模型
RT-Thread 提供了一套简单的I/O设备模型框架,如下图所示,它位于硬件和应用程序之间,共分成 三层,从上到下分别是I/O设备管理层、设备驱动框架层、设备驱动层。

设备驱动框架层是对同类硬件设备驱动的抽象,将不同厂家的同类硬件设备驱动中相同的部分抽取出 来,将不同部分留出接口,由驱动程序实现。
对于操作逻辑简单的设备,可以不经过设备驱动框架层,直接将设备注册到I/O设备管理器中,使用序列 图如下图所示

对于另一些设备,如看门狗等,则会将创建的设备实例先注册到对应的设备驱动框架中,再由设备驱动框架向I/O设备管理器进行注册

驱动层负责创建设备实例,并注册到I/O设备管理器中,可以通过静态申明的方式创建设备实例,也 可以用下面的接口进行动态创建:
rt_device_t rt_device_create(int type, int attach_size);
type是设备类型,attach_size是用户数据大小,返回设备句柄
设备被创建后,需要实现它访问硬件的操作方法。
struct rt_device_ops{ /* common device interface */ rt_err_t (*init) (rt_device_t dev); rt_err_t (*open) (rt_device_t dev, rt_uint16_t oflag); rt_err_t (*close) (rt_device_t dev); rt_size_t (*read) (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size ); rt_size_t (*write) (rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size); rt_err_t (*control)(rt_device_t dev, int cmd, void *args); };
当一个动态创建的设备不再需要使用时可以通过如下函数来销毁:
void rt_device_destroy(rt_device_t device);
设备被创建后,需要注册到I/O设备管理器中,应用程序才能够访问,注册设备的函数如下所示:
rt_err_t rt_device_register(rt_device_t dev, const char* name, rt_uint8_t flags);
flags参数支持下列参数(可以采用或的方式支持多种参数):
#define RT_DEVICE_FLAG_RDONLY 0x001 /*只读*/ #define RT_DEVICE_FLAG_WRONLY 0x002 /*只写*/ #define RT_DEVICE_FLAG_RDWR 0x003 /*读写*/ #define RT_DEVICE_FLAG_REMOVABLE 0x004 /*可移除*/ #define RT_DEVICE_FLAG_STANDALONE 0x008 /*独立*/ #define RT_DEVICE_FLAG_SUSPENDED 0x020 /*挂起*/ #define RT_DEVICE_FLAG_STREAM 0x040 /*流模式*/ #define RT_DEVICE_FLAG_INT_RX 0x100 /*中断接收*/ #define RT_DEVICE_FLAG_DMA_RX 0x200 /* DMA接收*/ #define RT_DEVICE_FLAG_INT_TX 0x400 /*中断发送*/ #define RT_DEVICE_FLAG_DMA_TX 0x800 /* DMA发送*/
当设备注销后的,设备将从设备管理器中移除,也就不能再通过设备查找搜索到该设备。注销设备不 会释放设备控制块占用的内存。注销设备的函数如下所示
rt_err_t rt_device_unregister(rt_device_t dev);
IO设备管理接口
应用程序根据设备名称获取设备句柄,进而可以操作设备。查找设备函数如下所示:
rt_device_t rt_device_find(const char* name);
获得设备句柄后,应用程序可使用如下函数对设备进行初始化操作
rt_err_t rt_device_init(rt_device_t dev);
通过设备句柄,应用程序可以打开和关闭设备,打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。通过如下函数打开设备:
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags);
应用程序打开设备完成读写等操作后,如果不需要再对设备进行操作则可以关闭设备,通过如下函数 完成
rt_err_t rt_device_close(rt_device_t dev);
通过命令控制字,应用程序也可以对设备进行控制,通过如下函数完成:
rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);
应用程序从设备中读取数据可以通过如下函数完成
rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos,void* buffer, rt_size_t size)
向设备中写入数据,可以通过如下函数完成:
rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos,const void* buffer, rt_size_t size);
当硬件设备收到数据时,可以通过如下函数调用另一个函数来设置数据接收指示,通知上层应用线程有数据到达。

浙公网安备 33010602011771号