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);

当硬件设备收到数据时,可以通过如下函数调用另一个函数来设置数据接收指示,通知上层应用线程有数据到达。

 

posted @ 2025-03-25 09:56  要是天天吃鱼就好了  阅读(330)  评论(0)    收藏  举报