参考官方文档:
https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/basic/basic

1 目录

RT-Thread 简介
1.内核
2.RT-Thread 启动流程
3.程序内存分布
4.内核对象模型
5.线程管理
6.时钟管理
7.线程间同步
8.线程间通信
9.内存管理
10.中断管理
问题与思考


2 嵌入式操作系统分类(实时和分时)

2.1 实时操作系统

分为硬实时和软实时,区别在于实时的保证程度
硬实时:规定操作在有限时间内完成,不允许超出时限的错误。超时错误会带来损害甚至导致系统失效
软实时:可以容忍偶然的超时错误,超时造成的后果不严重

例如,有的应用要求系统在 95%的情况下都会确保在规定的时间内完成某个动作,而不一定要求 100%。在许多情况下,这样的“软性”正确率已经可以达到用户期望的水平。

实时操作系统一般基于优先级抢占式机制,可以确保高优先级的任务在任何情况下都能够优先执行,从而保证实时性 如 RT-Thread,VxWorks,FreeRTOS

2.2 非实时操作系统(分时)

分时操作系统基于时间片轮转调度算法,旨在为所有用户(任务)公平的提供时间片,所有任务按照既定的时间片轮流执行,无法响应突发的事件

3 RT-Thread 简介

‌‌‌  
‌‌‌  具体包括以下部分:

‌‌‌  + 内核层:RT-Thread 内核,是 RT-Thread 的核心部分,包括了内核系统中对象的实现,例如多线程及其调度、信号量、邮箱、消息队列、内存管理、定时器等;libcpu/BSP(芯片移植相关文件
/ 板级支持包)与硬件密切相关,由外设驱动和 CPU 移植构成。
‌‌‌  + 组件与服务层:组件是基于 RT-Thread 内核之上的上层软件,例如虚拟文件系统、FinSH 命令行界面、
‌‌‌  + 网络框架、设备框架等。采用模块化设计,做到组件内部高内聚,组件之间低耦合。

4 内核

‌‌‌  - 内核的组成部分、系统如何启动、内存分布情况以及内核配置方法

‌‌‌   RT-Thread 内核架构图

‌‌‌  + 内核库是为了保证内核能够独立运行的一套小型的类似 C 库的函数实现子集。
‌‌‌  + 实时内核的实现包括:对象管理、线程管理及调度器、线程间通信管理、时钟管理及内存管理等等,内核最小的资源占用情况是 3 KB ROM,1.2 KB RAM。

‌‌‌  线程调度:系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。

‌‌‌  时钟管理:时钟节拍是 RT-Thread 操作系统中最小的时钟单位。通常使用定时器定时回调函数(即超时函数),完成定时服务。

‌‌‌  线程间同步:RT-Thread 采用信号量、互斥量与事件集实现线程间同步。

‌‌‌  线程间通信:支持邮箱和消息队列等通信机制。邮件的长度固定为 4 字节大小;消息队列能够接收不固定长度的消息,并把消息缓存在自己的内存空间中。邮箱效率较消息队列更为高效。
‌‌‌  内存管理:支持静态内存池管理及动态内存堆管理。
‌‌‌  I/O 设备管理:将 PIN、I 2 C、SPI、USB、UART 等作为外设设备,统一通过设备注册完成。当设备事件触发时,由驱动程序通知给上层的应用程序。

5 RT-Thread 启动流程

1705282006720.png

‌‌‌  具体启动流程见 [[RTT启动过程]]
‌‌‌  rtthread_startup () 函数是 RT-Thread 规定的统一启动入口。

1
‌‌‌  MDK 的扩展功能 $Sub$$ 和 $Super$$
1
2
3
‌‌‌  startup_stm32f103xe.s 开始-> components.c
‌‌‌  根据宏跳转到对应函数,如MDK: int $Sub$$main(void) ->int rtthread_startup(void)
‌‌‌  rtthread_startup:关闭中断 ->板级初始化 -> 打印版本信息-> 定时器初始化->调度器初始化 -> 信号初始化->创建初始化线程(main线程) -> 定时器线程初始化 -> 空闲线程初始化->启动调度器。

‌‌‌  启动调度器之前,系统所创建的线程在执行 rt_thread_startup () 后并不会立马运行,它们会处于就绪状态等待系统调度

6 程序内存分布

‌‌‌  MCU 包含的存储空间有:片内 Flash 与片内 RAM。Program Size 包含以下几个部分
‌‌‌  1)Code:代码段,存放程序的代码部分;
‌‌‌  2)RO-data:只读数据段,存放程序中定义的常量;
‌‌‌  3)RW-data:读写数据段,存放初始化为非 0 值的全局变量;
‌‌‌  4)ZI-data:0 数据段,存放未初始化的全局变量及初始化为 0 的变量
‌‌‌  . map 的文件,说明了各个函数占用的尺寸和地址
‌‌‌  1)RO Size 包含了 Code 及 RO-data,表示程序占用 Flash 空间的大小;
‌‌‌  2)RW Size 包含了 RW-data 及 ZI-data,表示运行时占用的 RAM 的大小;
‌‌‌  3)ROM Size 包含了 Code、RO-data 以及 RW-data,表示烧写程序所占用的 Flash 空间的大小;

6.1 烧写过程

‌‌‌  在这里插入图片描述
‌‌‌  STM 32 在上电启动之后默认从 Flash 启动,启动之后会将 RW 段中的 RW-data(初始化的全局变量)搬运到 RAM 中,但不会搬运 RO 段,即 CPU 的执行代码从 Flash 中读取,另外根据编译器给出的 ZI 地址和大小分配出 ZI 段,并将这块 RAM 区域清零。动态内存堆为未使用的 RAM 空间,应用程序申请和释放的内存块都来自该空间。
‌‌‌  一些全局变量则是存放于 RW 段和 ZI 段中,RW 段存放的是具有初始值的全局变量(而常量形式的全局变量则放置在 RO 段中,是只读属性的),ZI 段存放的系统未初始化的全局变量

总结:程序烧录时,代码和部分数据都被烧写到 Flash 中。当 MCU 运行时,全局变量部分被移动到 RAM 中。其他的全局变量直接在 RAM 中生成,剩余的 RAM 空间用作动态内存堆,详细存储说明见[[ARM 存储机制]]

6.2 RT-Thread 自动初始化机制

‌‌‌  自动初始化机制是指初始化函数不需要被显式调用,只需要在函数定义处通过宏定义的方式进行声明,就会在系统启动过程中被执行
‌‌‌  在哪里被调用的呢?

1
2
3
4
5
6
‌‌‌  1	INIT_BOARD_EXPORT(fn)	非常早期的初始化,此时**调度器还未启动**
‌‌‌  2 INIT_PREV_EXPORT(fn) 主要是用于纯软件的初始化、**没有太多依赖的函数**
‌‌‌  3 INIT_DEVICE_EXPORT(fn) 外设驱动初始化相关,比如网卡设备
‌‌‌  4 INIT_COMPONENT_EXPORT(fn) 组件初始化,比如文件系统或者 LWIP
‌‌‌  5 INIT_ENV_EXPORT(fn) 系统环境初始化,比如挂载文件系统
‌‌‌  6 INIT_APP_EXPORT(fn) 应用初始化,比如 GUI 应用

自动初始化机制详见[[自动初始化]]

7 内核对象模型

‌‌‌  RT-Thread 内核采用面向对象的设计思想进行设计,系统级的基础设施都是一种内核对象,例如线程,信号量,互斥量,定时器等。内核对象分为两类:静态内核对象和动态内核对象,静态内核对象通常放在 RW 段和 ZI 段中,在系统启动后在程序中初始化动态内核对象则是从内存堆中创建的,而后手工做初始化。如:静态线程对象 thread 1 ,线程控制块 thread 1 与栈空间 thread 1_stack 都是编译时决定的。thread 2 是一个动态线程对象运行中用到的空间都是动态分配的
‌‌‌  静态对象会占用 RAM 空间,不依赖于内存堆管理器,内存分配时间确定。动态对象则依赖于内存堆管理器,运行时申请 RAM 空间,当对象被删除后,占用的 RAM 空间被释放。

7.1 内核对象管理架构

‌‌‌  RT-Thread 采用内核对象管理系统来访问 / 管理所有内核对象。不依赖于具体的内存分配方式。
‌‌‌  RT-Thread 内核对象包括:线程,信号量,互斥量,事件,邮箱,消息队列和定时器,内存池,设备驱动等。对象容器中包含了每类内核对象的信息,包括对象类型,大小等。对象容器给每类内核对象分配了一个链表,所有的内核对象都被链接到该链表上在这里插入图片描述
‌‌‌  RT-Thread 中各类内核对象的派生和继承关系。对于每一种具体内核对象和对象控制块,除了基本结构外,还有自己的扩展属性(私有属性)。每一种具体对象是抽象对象的派生,继承了基本对象的属性并在此基础上扩展了与自己相关的属性。在这里插入图片描述对象管理模块中,定义了通用的数据结构,用来保存各种对象的共同属性,各种具体对象只需要在此基础上加上自己的某些特别的属性,就可以清楚的表示自己的特征。由对象控制块 rt_object 派生出来的有:线程对象、内存池对象、定时器对象、设备对象和 IPC 对象(IPC:进程间通信。此处作用是进行线程间同步与通信);由 IPC 对象派生出信号量、互斥量、事件、邮箱与消息队列、信号等对象。

7.2 对象控制块

1
2
3
4
5
6
7
8
9
10
11
12
‌‌‌  struct rt_object//内核对象控制块的数据结构:
‌‌‌  {
/* 内核对象名称 */
char name[RT_NAME_MAX];
/* 内核对象类型 */
rt_uint8_t type;
/* 内核对象的参数 */
rt_uint8_t flag;
/* 内核对象管理链表 */
rt_list_t list;
‌‌‌  };
‌‌‌  type ->>>>> enum rt_object_class_type

‌‌‌  如果是静态对象,那么对象类型的最高位将是 1,否则就是动态对象,系统最多能够容纳的对象类别数目是 127 个。

7.3 内核对象管理方式

1
2
3
4
5
6
7
8
9
‌‌‌  struct rt_object_information
‌‌‌  {
/* 对象类型 */
enum rt_object_class_type type;
/* 对象链表 */
rt_list_t object_list;
/* 对象大小 */
rt_size_t object_size;
‌‌‌  };

‌‌‌  一类对象由一个 rt_object_information 结构体来管理,每一个这类对象的具体实例都通过链表的形式挂接在 object_list 上。而这一类对象的内存块尺寸由 object_size 标识出来, 每一类对象的具体实例,他们占有的内存块大小都是相同的

7.3.1 初始化(静态)对象

1
2
3
4
5
6
‌‌‌  //在使用一个未初始化的静态对象前必须先对其进行初始化
‌‌‌  void rt_object_init(struct rt_object* object ,enum rt_object_class_type type ,const char* name)
//object 需要初始化的对象指针,它必须指向具体的对象内存块,而不能是空指针或野指针
//type 对象的类型,必须是 rt_object_class_type 枚举类型中列出的除 RT_Object_Class_Static 以外的类型(对于静态对象,或使用 rt_object_init 接口进行初始化的对象,系统会把它标识成 RT_Object_Class_Static 类型)
//name 对象的名字。每个对象可以设置一个名字,这个名字的最大长度由 RT_NAME_MAX 指定,并且系统不关心它是否是由’\0’做为终结符
‌‌‌   //当调用这个函数进行对象初始化时,系统会把这个对象放置到对象容器中进行管理,即初始化对象的一些参数,然后把这个对象节点插入到对象容器的对象链表中

7.3.2 脱离(静态)对象

‌‌‌  调用该接口,可使得一个静态内核对象从内核对象容器中脱离出来,即从内核对象容器链表上删除相应的对象节点。对象脱离后,对象占用的内存并不会被释放。

1
‌‌‌  void rt_object_detach(rt_object_t object);

7.3.3 分配(动态)对象

‌‌‌  动态的对象则可以在需要时申请,不需要时释放出内存空间给其他应用使用。

1
2
3
‌‌‌  rt_object_t rt_object_allocate(enum  rt_object_class_type type ,const  char*  name)//对象的名字
//rt_object_class_type 只能是 rt_object_class_type 中除 RT_Object_Class_Static 以外的类型。并且经过这个接口分配出来的对象类型是动态的
‌‌‌  //分配成功:对象句柄 失败:RT_NULL

7.3.4 删除(动态)对象

1
‌‌‌  void rt_object_delete(rt_object_t object);//object	对象的句柄

7.3.5 辨别 (静态/动态)对象

1
‌‌‌  rt_err_t rt_object_is_systemobject(rt_object_t object);

7.4 RT-Thread 内核配置示例

‌‌‌  通过修改工程目录下的 rtconfig. h 文件来进行,在实际应用中,系统配置文件 rtconfig. h 是由配置工具自动生成的,无需手动更改。

7.4.1 . RT-Thread 内核部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
‌‌‌  /* 表示内核对象的名称的最大长度,若代码中对象名称的最大长度大于宏定义的长度,多余的部分将被截掉。*/
#define RT_NAME_MAX 8
‌‌‌  /* 字节对齐时设定对齐的字节个数。常使用 ALIGN(RT_ALIGN_SIZE) 进行字节对齐。*/
#define RT_ALIGN_SIZE 4
‌‌‌  /* 定义系统线程优先级数;通常用 RT_THREAD_PRIORITY_MAX 定义空闲线程的优先级 */
#define RT_THREAD_PRIORITY_MAX 32
‌‌‌  /* 定义时钟节拍,为 100 时表示 100 个 tick 每秒,一个 tick 就为 10ms */
#define RT_TICK_PER_SECOND 100
‌‌‌  /* 检查栈是否溢出,未定义则关闭 */
#define RT_USING_OVERFLOW_CHECK
‌‌‌  /* 定义该宏开启 debug 模式,未定义则关闭 */
#define RT_DEBUG
‌‌‌  /* 开启 debug 模式时:该宏定义为 0 时表示关闭打印组件初始化信息,定义为 1 时表示启用 */
#define RT_DEBUG_INIT 0
‌‌‌  /* 开启 debug 模式时:该宏定义为 0 时表示关闭打印线程切换信息,定义为 1 时表示启用 */
#define RT_DEBUG_THREAD 0
‌‌‌  /* 定义该宏表示开启钩子函数的使用,未定义则关闭 */
#define RT_USING_HOOK
‌‌‌  /* 定义了空闲线程的栈大小 */
#define IDLE_THREAD_STACK_SIZE 256

7.4.2 . 线程间同步与通信部分

1
2
3
4
5
6
7
8
9
10
11
12
‌‌‌  /* 定义该宏可开启信号量的使用,未定义则关闭 */
#define RT_USING_SEMAPHORE
‌‌‌  /* 定义该宏可开启互斥量的使用,未定义则关闭 */
#define RT_USING_MUTEX
‌‌‌  /* 定义该宏可开启事件集的使用,未定义则关闭 */
#define RT_USING_EVENT
‌‌‌  /* 定义该宏可开启邮箱的使用,未定义则关闭 */
#define RT_USING_MAILBOX
‌‌‌  /* 定义该宏可开启消息队列的使用,未定义则关闭 */
#define RT_USING_MESSAGEQUEUE
‌‌‌  /* 定义该宏可开启信号的使用,未定义则关闭 */
#define RT_USING_SIGNALS

7.4.3 . 内存管理部分

1
2
3
4
5
6
7
8
9
10
‌‌‌  /* 开启静态内存池的使用 */
#define RT_USING_MEMPOOL
‌‌‌  /* 定义该宏可开启两个或以上内存堆拼接的使用,未定义则关闭 */
#define RT_USING_MEMHEAP
‌‌‌  /* 开启小内存管理算法 */
#define RT_USING_SMALL_MEM
‌‌‌  /* 关闭 SLAB 内存管理算法 */
‌‌‌  /* #define RT_USING_SLAB */
‌‌‌  /* 开启堆的使用 */
#define RT_USING_HEAP

7.4.4 . 内核设备对象

1
2
3
4
5
6
7
8
‌‌‌  /* 表示开启了系统设备的使用 */
#define RT_USING_DEVICE
‌‌‌  /* 定义该宏可开启系统控制台设备的使用,未定义则关闭 */
#define RT_USING_CONSOLE
‌‌‌  /* 定义控制台设备的缓冲区大小 */
#define RT_CONSOLEBUF_SIZE 128
‌‌‌  /* 控制台设备的名称 */
#define RT_CONSOLE_DEVICE_NAME "uart1"

7.4.5 . 自动初始化方式

1
2
3
4
5
6
‌‌‌  /* 定义该宏开启自动初始化机制,未定义则关闭 */
#define RT_USING_COMPONENTS_INIT
‌‌‌  /* 定义该宏开启设置应用入口为 main 函数 */
#define RT_USING_USER_MAIN
‌‌‌  /* 定义 main 线程的栈大小 */
#define RT_MAIN_THREAD_STACK_SIZE 2048

7.4.6 . FinSH

‌‌‌  MSH: 指令描述功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
‌‌‌  /* 定义该宏可开启系统 FinSH 调试工具的使用,未定义则关闭 */
#define RT_USING_FINSH
‌‌‌  /* 开启系统 FinSH 时:将该线程名称定义为 tshell */
#define FINSH_THREAD_NAME "tshell"
‌‌‌  /* 开启系统 FinSH 时:使用历史命令 */
#define FINSH_USING_HISTORY
‌‌‌  /* 开启系统 FinSH 时:对历史命令行数的定义 */
#define FINSH_HISTORY_LINES 5
‌‌‌  /* 开启系统 FinSH 时:定义该宏开启使用 Tab 键,未定义则关闭 */
#define FINSH_USING_SYMTAB
‌‌‌  /* 开启系统 FinSH 时:定义该线程的优先级 */
#define FINSH_THREAD_PRIORITY 20
‌‌‌  /* 开启系统 FinSH 时:定义该线程的栈大小 */
#define FINSH_THREAD_STACK_SIZE 4096
‌‌‌  /* 开启系统 FinSH 时:定义命令字符长度 */
#define FINSH_CMD_SIZE 80
‌‌‌  /* 开启系统 FinSH 时:定义该宏开启 MSH 功能 */
#define FINSH_USING_MSH
‌‌‌  /* 开启系统 FinSH 时:开启 MSH 功能时,定义该宏默认使用 MSH 功能 */
#define FINSH_USING_MSH_DEFAULT
‌‌‌  /* 开启系统 FinSH 时:定义该宏,仅使用 MSH 功能 */
#define FINSH_USING_MSH_ONLY

7.4.7 . 关于 MCU

1
2
3
4
5
6
‌‌‌  /* 定义该工程使用的 MCU 为 STM32F103ZE;系统通过对芯片类型的定义,来定义芯片的管脚 */
#define STM32F103ZE
‌‌‌  /* 定义时钟源频率 */
#define RT_HSE_VALUE 8000000
‌‌‌  /* 定义该宏开启 UART1 的使用 */
#define RT_USING_UART1

7.4.8 -8. 常见宏定义说明

1
2
3
4
5
6
7
8
9
10
11
12
‌‌‌  //static 关键字的作用是令函数只能在当前的文件中使用;inline 表示内联,用 static 修饰后在调用函数时会建议编译器进行内联展开。
#define rt_inline static __inline
‌‌‌  //作用是向编译器说明这段代码有用,即使函数中没有调用也要保留编译。例如 RT-Thread 自动初始化功能使用了自定义的段,使用 RT_USED 会将自定义的代码段保留。
#define RT_USED __attribute__((used))
‌‌‌  //表示函数或变量可能不使用,这个属性可以避免编译器产生警告信息。
#define RT_UNUSED __attribute__((unused))
‌‌‌  //常用于定义函数,编译器在链接函数时会优先链接没有该关键字前缀的函数,如果找不到则再链接由 weak 修饰的函数。
#define RT_WEAK __weak
‌‌‌  //作用是在给某对象分配地址空间时,将其存放的地址按照 n 字节对齐,这里 n 可取 2 的幂次方。
#define ALIGN(n) __attribute__((aligned(n)))
‌‌‌  //将 size 提升为 align 定义的整数的倍数,例如,RT_ALIGN(13,4) 将返回 16。
#define RT_ALIGN(size, align) (((size) + (align) - 1) & ~((align) - 1))

8 线程管理

^3b547f

‌‌‌  例如让嵌入式系统执行这样的任务,系统通过传感器采集数据,并通过显示屏将数据显示出来,在多线程实时系统中,可以将这个任务分解成两个子任务,如下图所示,一个子任务不间断地读取传感器数据,并将数据写到共享内存中,另外一个子任务周期性的从共享内存中读取数据,并将传感器数据输出到显示屏上。
‌‌‌  在这里插入图片描述
‌‌‌  线程是实现任务的载体,它是 RT-Thread 中最基本的调度单位,它描述了一个任务执行的运行环境,也描述了这个任务所处的优先等级.
‌‌‌  线程运行时,它会认为自己是以独占 CPU 的方式在运行,线程执行时的运行环境称为上下文,具体来说就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等。

8.1 线程管理的功能特点

‌‌‌  线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程用户线程,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程
‌‌‌  每个线程都有重要的属性,如线程控制块、线程栈、入口函数等。
‌‌‌  在这里插入图片描述
‌‌‌  RT-Thread 的线程调度器是抢占式的, 当一个运行着的线程使一个比它优先级高的线程满足运行条件,高优先级的线程立刻得到了 CPU 的使用权。
‌‌‌  如果是中断服务程序使一个高优先级的线程满足运行条件,中断完成时,被中断的线程挂起,优先级高的线程开始运行。
‌‌‌  当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该线程的上下文信息恢复。

8.2 线程的工作机制

8.2.1 . 线程控制块

‌‌‌  线程控制块由结构体 struct rt_thread 表示,线程控制块是操作系统用于管理线程的一个数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
‌‌‌  /* 线程控制块 */
‌‌‌  struct rt_thread
‌‌‌  {
/* rt 对象 */
char name[RT_NAME_MAX];
/* 线程名称 */
rt_uint8_t type;
/* 对象类型 */
rt_uint8_t flags;
/* 标志位 */
rt_list_t list;
/* 对象列表 */
rt_list_t tlist;
/* 线程列表 */
/* 栈指针与入口指针 */
void *sp;
/* 栈指针 */
void *entry;
/* 入口函数指针 */
void *parameter;
/* 参数 */
void *stack_addr;
/* 栈地址指针 */
rt_uint32_t stack_size;
/* 栈大小 */
/* 线程错误代码 */
rt_err_t error;
/* 线程状态 */
rt_uint8_t stat;
/* 优先级 */
rt_uint8_t current_priority;
/* 当前优先级 */
rt_uint8_t init_priority;
/* 初始优先级 */
rt_uint32_t number_mask;
//......
rt_ubase_t init_tick;
/* 线程初始化计数值 */
rt_ubase_t remaining_tick;
/* 线程剩余计数值 */
struct rt_timer thread_timer;
/* 内置线程定时器 */
void (*cleanup)(struct rt_thread *tid);
/* 线程退出清除函数 */
rt_uint32_t user_data;
/* 用户数据 */
‌‌‌  };
‌‌‌  /*其中 init_priority 是线程创建时指定的线程优先级,在线程运行过程当中是不会被改变的(除非用户执行线程控制函数进行手动调整线程优先级)。cleanup 会在线程退出时,被空闲线程回调一次以执行用户设置的清理现场等工作。最后的一个成员 user_data 可由用户挂接一些数据信息到线程控制块中,以提供类似线程私有数据的实现。*/

8.2.2 . 线程重要属性

‌‌‌  线程栈
‌‌‌  RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中
‌‌‌  还用来存放函数中的局部变量,函数中局部变量初始时从寄存器中分配(ARM 架构),当这个函数再调用另一个函数时,这些局部变量将放入栈中。
‌‌‌  对于线程第一次运行,可以以手工的方式构造这个上下文来设置一些初始的环境:入口函数(PC 寄存器)、入口参数(R0 寄存器)、返回位置(LR 寄存器)、当前机器运行状态(CPSR 寄存器)。
‌‌‌  ARM Cortex-M 架构
‌‌‌  线程栈大小设定:对于资源相对较大的 MCU,可以适当设计较大的线程栈;也可以在初始时设置较大的栈,例如指定大小为 1 K 或 2 K 字节,然后在 FinSH 中用 list_thread 命令查看线程运行的过程中线程所使用的栈的大小,通过此命令,能够看到从线程启动运行时,到当前时刻点,线程使用的最大栈深度,而后加上适当的余量形成最终的线程栈大小,最后对栈空间大小加以修改。
‌‌‌  线程状态

1
2
3
4
5
‌‌‌  **1.初始状态**	当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_INIT
‌‌‌  **2.就绪状态** 在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操作系统会马上寻找最高优先级的就绪态线程运行。此状态在 RT-Thread 中的宏定义为 RT_THREAD_READY
‌‌‌  **3.运行状态** 线程当前正在运行。在单核系统中,只有 rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在 RT-Thread 中的宏定义为 RT_THREAD_RUNNING
‌‌‌  **4.挂起状态** 也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_SUSPEND
‌‌‌  **5.关闭状态** 当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_CLOSE

‌‌‌  RT-Thread 线程的优先级是表示线程被调度的优先程度。数值越小的优先级越高
‌‌‌  时间片:
‌‌‌  系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick)
‌‌‌  在这里插入图片描述
‌‌‌  线程的入口函数:
‌‌‌  entry 是线程的入口函数,它是线程实现预期功能的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
‌‌‌  般有以下两种代码形式:
‌‌‌  -无限循环模式:
‌‌‌  void thread_entry(void* paramenter)
‌‌‌  {
while (1)
{
/* 等待事件的发生 */
‌‌‌  线程中不能陷入死循环操作,必须要有让出 CPU 使用权的动作,如循环中调用延时函数或者主动挂起。用户设计这种无限循环的线程的目的,就是为了让这个线程一直被系统循环调度运行,永不删除。
/* 对事件进行服务、进行处理 */
}
‌‌‌  }
‌‌‌  -顺序执行或有限次循环模式:
‌‌‌  在执行完毕后,线程将被系统自动删除。
‌‌‌  static void thread_entry(void* parameter)
‌‌‌  {/* 处理事务 #1 *//* 处理事务 #2 */
‌‌‌  }

‌‌‌  线程错误码:

1
2
3
4
5
6
7
8
9
10
11
#define RT_EOK           0 /* 无错误     */
#define RT_ERROR 1 /* 普通错误 */
#define RT_ETIMEOUT 2 /* 超时错误 */
#define RT_EFULL 3 /* 资源已满 */
#define RT_EEMPTY 4 /* 无资源 */
#define RT_ENOMEM 5 /* 无内存 */
#define RT_ENOSYS 6 /* 系统不支持 */
#define RT_EBUSY 7 /* 系统忙 */
#define RT_EIO 8 /* IO 错误 */
#define RT_EINTR 9 /* 中断系统调用 */
#define RT_EINVAL 10 /* 非法参数 */

8.2.3 . 线程状态切换

‌‌‌  转换关系
‌‌‌  线程通过调用函数 rt_thread_create/init () 进入到初始状态(RT_THREAD_INIT);初始状态的线程通过调用函数 rt_thread_startup () 进入到就绪状态(RT_THREAD_READY);就绪状态的线程被调度器调度后进入运行状态(RT_THREAD_RUNNING);当处于运行状态的线程调用 rt_thread_delay (),rt_sem_take (),rt_mutex_take (),rt_mb_recv () 等函数或者获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND);处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,如果调用 rt_thread_delete/detach () 函数,将更改为关闭状态(RT_THREAD_CLOSE);而运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit () 函数,将状态更改为关闭状态。
‌‌‌  RT-Thread 中,实际上线程并不存在运行状态,就绪状态和运行状态是等同的。

8.2.4 .系统线程 (空闲线程、主线程)

‌‌‌  系统线程是指由系统创建的线程,用户线程是由用户程序调用线程管理接口创建的线程,在 RT-Thread 内核中的系统线程有空闲线程和主线程。
‌‌‌  空闲线程
‌‌‌  系统创建的最低优先级的线程,线程状态永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。
‌‌‌  rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。
‌‌‌  空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。
‌‌‌  主线程
‌‌‌  在系统启动时,系统会创建 main 线程,它的入口函数为 main_thread_entry ()
‌‌‌  在这里插入图片描述

8.3 线程的管理方式

‌‌‌  线程的相关操作,包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程。动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。
‌‌‌  在这里插入图片描述

8.3.1 . 创建和删除线程

‌‌‌  动态线程:仅在使能了系统动态堆时才有效(即 RT_USING_HEAP 宏定义已经定义了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
‌‌‌  //创建一个动态线程
‌‌‌  rt_thread_t rt_thread_create(const char* name,void (*entry)(void* parameter),void* parameter,rt_uint32_t stack_size,rt_uint8_t priority,rt_uint32_t tick);
‌‌‌  /*
‌‌‌  name 线程的名称;线程名称的最大长度由 rtconfig.h 中的宏 RT_NAME_MAX 指定,多余部分会被自动截掉
‌‌‌  entry 线程入口函数
‌‌‌  parameter 线程入口函数参数
‌‌‌  stack_size 线程栈大小,单位是字节
‌‌‌  priority 线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0~255,数值越小优先级越高,0 代表最高优先级
‌‌‌  tick 线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
‌‌‌  返回 ——
‌‌‌  thread 线程创建成功,返回线程句柄
‌‌‌  RT_NULL 线程创建失败*/
‌‌‌  //当不需要使用,或者运行出错时,我们可以使用下面的函数接口来从系统中把线程完全删除掉:
‌‌‌  rt_err_t rt_thread_delete(rt_thread_t thread);
‌‌‌  /*
‌‌‌  线程占用的堆栈空间也会被释放,收回的空间将重新用于其他的内存分配。实际上,用 rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为 RT_THREAD_CLOSE 状态,然后放入到 rt_thread_defunct 队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作。
‌‌‌  thread 要删除的线程句柄
‌‌‌  返回 ——
‌‌‌  RT_EOK 删除线程成功
‌‌‌  -RT_ERROR 删除线程失败
‌‌‌  */

‌‌‌  静态线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
‌‌‌  //初始化静态线程对象
‌‌‌  rt_err_t rt_thread_init(struct rt_thread* thread,const char* name,void (*entry)(void* parameter), void* parameter,void* stack_start, rt_uint32_t stack_size,rt_uint8_t priority, rt_uint32_t tick);
‌‌‌  /*线程栈由用户提供。静态线程是指线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。需要注意的是,用户提供的栈首地址需做系统对齐
‌‌‌  thread 线程句柄。线程句柄由用户提供出来,并指向对应的线程控制块内存地址
‌‌‌  name 线程的名称;线程名称的最大长度由 rtconfig.h 中定义的 RT_NAME_MAX 宏指定,多余部分会被自动截掉
‌‌‌  entry 线程入口函数
‌‌‌  parameter 线程入口函数参数
‌‌‌  stack_start 线程栈起始地址
‌‌‌  stack_size 线程栈大小,单位是字节。在大多数系统中需要做栈空间地址对齐(例如 ARM 体系结构中需要向 4 字节地址对齐)
‌‌‌  priority 线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0 ~ 255,数值越小优先级越高,0 代表最高优先级
‌‌‌  tick 线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
‌‌‌  返回 ——
‌‌‌  RT_EOK 线程创建成功
‌‌‌  -RT_ERROR 线程创建失败*/
‌‌‌  //线程脱离函数
‌‌‌  rt_err_t rt_thread_detach (rt_thread_t thread);
‌‌‌  //和 rt_thread_delete() 函数相对应的,线程本身不应调用这个接口脱离线程本身。

8.3.2 启动线程

1
2
3
4
5
6
7
‌‌‌  rt_err_t rt_thread_startup(rt_thread_t thread);
‌‌‌  //创建(初始化)的线程状态处于初始状态,当调用这个函数时,将把线程的状态更改为就绪状态
‌‌‌  /*
‌‌‌  thread 线程句柄
‌‌‌  返回 ——
‌‌‌  RT_EOK 线程启动成功
‌‌‌  -RT_ERROR 线程启动失败*/

8.3.3 . 获得当前线程

1
2
3
4
5
6
‌‌‌  //相同的一段代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄
‌‌‌  rt_thread_t rt_thread_self(void);
‌‌‌  /*
‌‌‌  返回 描述
‌‌‌  thread 当前运行的线程句柄
‌‌‌  RT_NULL 失败,调度器还未启动*/

8.3.4 . 使线程让出处理器资源

1
2
3
4
5
‌‌‌  rt_err_t rt_thread_yield(void);
‌‌‌  //当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优先级队列链表的尾部,然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。
‌‌‌  /*rt_thread_yield() 函数和 rt_schedule() 函数比较相像
‌‌‌  执行 rt_thread_yield() 函数后,当前线程被换出,相同优先级的下一个就绪线程将被执行。
‌‌‌  而执行 rt_schedule() 函数后,当前线程并不一定被换出,即使被换出,也不会被放到就绪线程链表的尾部,而是在系统中选取就绪的优先级最高的线程执行(如果系统中没有比当前线程优先级更高的线程存在,那么执行完 rt_schedule() 函数后,系统将继续执行当前线程)。*/

8.3.5 . 使线程睡眠

1
2
3
4
5
6
7
8
9
10
‌‌‌  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);
‌‌‌  //三个函数接口的作用相同,可以使当前线程挂起一段指定的时间,当这个时间过后,线程会被唤醒并再次进入就绪状态
‌‌‌  /*
‌‌‌  tick/ms 线程睡眠的时间:
‌‌‌  sleep/delay 的传入参数 tick 以 1 个 OS Tick 为单位 ;
‌‌‌  mdelay 的传入参数 ms 以 1ms 为单位;
‌‌‌  返回 ——
‌‌‌  RT_EOK 操作成功*/

8.3.6 . 挂起和恢复线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
‌‌‌  /*当线程调用 rt_thread_delay() 时,线程将主动挂起;
‌‌‌  当调用 rt_sem_take(),rt_mb_recv() 等函数时,资源不可使用也将导致线程挂起。
‌‌‌  处于挂起状态的线程,如果其等待的资源超时(超过其设定的等待时间),那么该线程将不再等待这些资源,并返回到就绪状态;或者,当其他线程释放掉该线程所等待的资源时,该线程也会返回到就绪状态。*/
‌‌‌  rt_err_t rt_thread_suspend (rt_thread_t thread);
‌‌‌  /*
‌‌‌  thread 线程句柄
‌‌‌  返回 ——
‌‌‌  RT_EOK 线程挂起成功
‌‌‌  -RT_ERROR 线程挂起失败,因为该线程的状态并不是就绪状态*/
‌‌‌  只能使用本函数来挂起线程自己,不可以在线程A中尝试挂起线程B,
‌‌‌  而且在挂起线程自己后,需要立刻调用 rt_schedule() 函数进行手动的线程上下文切换。
‌‌‌  不推荐使用该接口。该接口可视为内核内部接口。
‌‌‌  //恢复线程就是让挂起的线程重新进入就绪状态
‌‌‌  rt_err_t rt_thread_resume (rt_thread_t thread);
‌‌‌  /*
‌‌‌  thread 线程句柄
‌‌‌  返回 ——
‌‌‌  RT_EOK 线程恢复成功
‌‌‌  -RT_ERROR 线程恢复失败,因为该个线程的状态并不是 RT_THREAD_SUSPEND 状态*/

8.3.7 . 控制线程

1
2
3
4
5
6
7
8
9
‌‌‌  //控制线程: 当需要对线程进行一些其他控制时
‌‌‌  rt_err_t rt_thread_control (rt_thread_t thread, rt_uint 8_t cmd, void* arg);
‌‌‌  /*
‌‌‌  thread 线程句柄
‌‌‌  cmd 指示控制命令
‌‌‌  arg 控制参数
‌‌‌  返回 ——
‌‌‌  RT_EOK 控制执行正确
‌‌‌  -RT_ERROR 失败*/
1
2
3
4
‌‌‌  指示控制命令 cmd 当前支持的命令包括:
‌‌‌  •RT_THREAD_CTRL_CHANGE_PRIORITY:动态更改线程的优先级;
‌‌‌  •RT_THREAD_CTRL_STARTUP:开始运行一个线程,等同于 rt_thread_startup () 函数调用;
‌‌‌  •RT_THREAD_CTRL_CLOSE:关闭一个线程,等同于 rt_thread_delete () 或 rt_thread_detach () 函数调用。

8.3.8 -8. 设置和删除空闲钩子

‌‌‌  空闲钩子函数是空闲线程的钩子函数,如果设置了空闲钩子函数,就可以在系统执行空闲线程时,自动执行空闲钩子函数来做一些其他事情,比如系统指示灯。

1
2
3
4
5
6
7
8
9
‌‌‌  rt_err_t rt_thread_idle_sethook (void (*hook)(void));
‌‌‌  rt_err_t rt_thread_idle_delhook (void (*hook)(void));
‌‌‌  hook 设置的钩子函数
‌‌‌  返回 ——
‌‌‌  RT_EOK 设置/删除 成功
‌‌‌  -RT_EFULL 设置失败
‌‌‌  -RT_ENOSYS 删除失败
‌‌‌  /*
‌‌‌  空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态,例如 rt_thread_delay (),rt_sem_take () 等可能会导致线程挂起的函数都不能使用。并且,由于 malloc、free 等内存相关的函数内部使用了信号量作为临界区保护,因此在钩子函数内部也不允许调用此类函数!*/

8.3.9 -9. 设置调度器钩子

‌‌‌  在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:

1
2
3
4
‌‌‌  void rt_scheduler_sethook (void (*hook)(struct rt_thread* from, struct rt_thread* to));
‌‌‌  //钩子函数 hook () 的声明如下:
‌‌‌  void hook (struct rt_thread* from, struct rt_thread* to);
‌‌‌  *请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个钩子函数中,基本上不允许调用系统 API,更不应该导致当前运行的上下文挂起)。

8.4 线程应用示例

‌‌‌  Keil 模拟器环境下的应用示例:
‌‌‌  创建一个动态线程初始化一个静态线程,一个线程在运行完毕后自动被系统删除,另一个线程一直打印计数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <rtthread.h>
#define THREAD_PRIORITY 25
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5
‌‌‌  static rt_thread_t tid 1 = RT_NULL;
‌‌‌  /* 线程 1 的入口函数 */
‌‌‌  static void thread 1_entry (void *parameter)
‌‌‌  {
rt_uint 32_t count = 0;
while (1)
{
/* 线程 1 采用低优先级运行,一直打印计数值 */
rt_kprintf ("thread 1 count: %d\n", count ++);
rt_thread_mdelay (500);
}
‌‌‌}
‌‌‌  ALIGN (RT_ALIGN_SIZE)
‌‌‌  static char thread 2_stack[1024];
‌‌‌  static struct rt_thread thread 2;
‌‌‌  /* 线程 2 入口 */
‌‌‌  static void thread 2_entry (void *param)
‌‌‌  {
rt_uint 32_t count = 0;
/* 线程 2 拥有较高的优先级,以抢占线程 1 而获得执行 */
for (count = 0; count < 10 ; count++)
{
/* 线程 2 打印计数值 */
rt_kprintf ("thread 2 count: %d\n", count);
}
rt_kprintf ("thread 2 exit\n");
/* 线程 2 运行结束后也将自动被系统脱离 */
‌‌‌  }
‌‌‌  /* 线程示例 */
‌‌‌  int thread_sample (void)
‌‌‌  {
/* 创建线程 1,名称是 thread 1,入口是 thread 1_entry*/
tid 1 = rt_thread_create ("thread 1", thread 1_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
/* 如果获得线程控制块,启动这个线程 */
if (tid 1 != RT_NULL) rt_thread_startup (tid 1);
/* 初始化线程 2,名称是 thread 2,入口是 thread 2_entry */
rt_thread_init (&thread 2,"thread 2", thread 2_entry, RT_NULL,&thread 2_stack[0], sizeof (thread 2_stack), THREAD_PRIORITY - 1, THREAD_TIMESLICE); rt_thread_startup (&thread 2); return 0;
‌‌‌  }
‌‌‌  /* 导出到 msh 命令列表中 */
‌‌‌  MSH_CMD_EXPORT (thread_sample, thread sample);

‌‌‌  关于删除线程:大多数线程是循环执行的,无需删除;而能运行完毕的线程,RT-Thread 在线程运行完毕后,自动删除线程,在 rt_thread_exit () 里完成删除动作。用户只需要了解该接口的作用,不推荐使用该接口(可以由其他线程调用此接口或在定时器超时函数中调用此接口删除一个线程,但是这种使用非常少)。
‌‌‌  线程调度器钩子示例
‌‌‌  在线程进行调度切换时,会执行调度,我们可以设置一个调度器钩子,这样可以在线程切换时,做一些额外的事情,这个例子是在调度器钩子函数中打印线程间的切换信息,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <rtthread.h>
#define THREAD_STACK_SIZE 1024
#define THREAD_PRIORITY 20
#define THREAD_TIMESLICE 10
‌‌‌  /* 针对每个线程的计数器 */
‌‌‌  volatile rt_uint 32_t count[2];
‌‌‌  /* 线程 1、2 共用一个入口,但入口参数不同 */
‌‌‌  static void thread_entry (void* parameter)
‌‌‌  {rt_uint 32_t value; value = (rt_uint 32_t) parameter; while (1){rt_kprintf ("thread %d is running\n", value); rt_thread_mdelay (1000); // 延时一段时间}
‌‌‌  }
‌‌‌  static rt_thread_t tid 1 = RT_NULL;
‌‌‌  static rt_thread_t tid 2 = RT_NULL;
‌‌‌  static void hook_of_scheduler (struct rt_thread* from, struct rt_thread* to)
‌‌‌  {rt_kprintf ("from: %s --> to: %s \n", from->name , to->name);
‌‌‌  }
‌‌‌  int scheduler_hook (void)
‌‌‌  {/* 设置调度器钩子 */rt_scheduler_sethook (hook_of_scheduler);/* 创建线程 1 */tid 1 = rt_thread_create ("thread 1", thread_entry, (void*) 1, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (tid 1 != RT_NULL) rt_thread_startup (tid 1);/* 创建线程 2 */tid 2 = rt_thread_create ("thread 2", thread_entry, (void*) 2, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE - 5); if (tid 2 != RT_NULL) rt_thread_startup (tid 2); return 0;
‌‌‌  }
‌‌‌  /* 导出到 msh 命令列表中 */
‌‌‌  MSH_CMD_EXPORT (scheduler_hook, scheduler_hook sample);

‌‌‌  由仿真的结果可以看出,对线程进行切换时,设置的调度器钩子函数是在正常工作的,一直在打印线程切换的信息,包含切换到空闲线程。

9 时钟管理

9.1 时钟节拍

‌‌‌  任何操作系统都需要提供一个时钟节拍,以供系统处理所有和时间有关的事件,如线程的延时、线程的时间片轮转调度以及定时器超时等。时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳,中断之间的时间间隔取决于不同的应用,一般是 1 ms–100 ms,时钟节拍率越快,系统的实时响应越快,但是系统的额外开销就越大,从系统启动开始计数的时钟节拍数称为系统时间。
‌‌‌  RT-Thread 中,时钟节拍的长度可以根据 RT_TICK_PER_SECOND 的定义来调整,等于 1/RT_TICK_PER_SECOND 秒。

9.1.1 . 时钟节拍的实现方式

‌‌‌  时钟节拍由配置为中断触发模式的硬件定时器产生,当中断到来时,将调用一次:void rt_tick_increase (void),通知操作系统已经过去一个系统时钟;不同硬件定时器中断实现都不同

1
2
3
4
5
6
7
8
9
‌‌‌  //以 STM 32 定时器作为示例
‌‌‌  void SysTick_Handler (void)
‌‌‌  {
/* 进入中断 */
rt_interrupt_enter ();
……rt_tick_increase ();
/* 退出中断 */
rt_interrupt_leave ();
‌‌‌  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
‌‌‌  void rt_tick_increase (void)
‌‌‌  {
struct rt_thread *thread;
/* 全局变量 rt_tick 自加 */
++ rt_tick;
/* 检查时间片 */
thread = rt_thread_self ();
-- thread->remaining_tick;
if (thread->remaining_tick == 0)
{
/* 重新赋初值 */
thread->remaining_tick = thread->init_tick;
/* 线程挂起 */
rt_thread_yield ();
}
/* 检查定时器 */
rt_timer_check ();
‌‌‌  }
‌‌‌  //全局变量 rt_tick 在每经过一个时钟节拍时,值就会加 1,rt_tick 的值表示了系统从启动开始总共经过的时钟节拍数,即系统时间。
‌‌‌  //此外,每经过一个时钟节拍时,都会检查当前线程的时间片是否用完,以及是否有定时器超时。

‌‌‌  中断中的 rt_timer_check () 用于检查系统硬件定时器链表,如果有定时器超时,将调用相应的超时函数。且所有定时器在定时超时后都会从定时器链表中被移除,而周期性定时器会在它再次启动时被加入定时器链表。

9.1.2 . 获取时钟节拍

‌‌‌  由于全局变量 rt_tick 在每经过一个时钟节拍时,值就会加 1,通过调用 rt_tick_get 会返回当前 rt_tick 的值,即可以获取到当前的时钟节拍值。此接口可用于记录系统的运行时间长短,或者测量某任务运行的时间。

1
‌‌‌  rt_tick_t rt_tick_get (void);

9.2 定时器管理

‌‌‌  定时器有硬件定时器和软件定时器之分:
‌‌‌  1)硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。
‌‌‌  2)软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受数目限制的定时器服务。
‌‌‌  RT-Thread 操作系统提供软件实现的定时器,以时钟节拍(OS Tick)的时间长度为单位,即定时数值必须是 OS Tick 的整数倍,例如一个 OS Tick 是 10 ms,那么上层软件定时器只能是 10 ms,20 ms,100 ms 等,而不能定时为 15 ms。RT-Thread 的定时器也基于系统的节拍,提供了基于节拍整数倍的定时能力。

9.2.1 . RT-Thread 定时器介绍

‌‌‌  RT-Thread 的定时器提供两类定时器机制:第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动的停止
‌‌‌  根据超时函数执行时所处的上下文环境,RT-Thread 的定时器可以分为 HARD_TIMER 模式与 SOFT_TIMER 模式
‌‌‌  在这里插入图片描述

9.2.2 . HARD_TIMER 模式 (定时器默认的方式)

‌‌‌  超时函数在中断上下文环境中执行,可以在初始化 / 创建定时器时使用参数 RT_TIMER_FLAG_HARD_TIMER 来指定。
‌‌‌  与中断服务例程的要求相同:执行时间应该尽量短,执行时不应导致当前上下文挂起、等待。例如在中断上下文中执行的超时函数它不应该试图去申请动态内存、释放动态内存等。
‌‌‌  不能够执行非常长的时间,否则会导致其他中断的响应时间加长或抢占了其他线程执行的时间。

9.2.3 . SOFT_TIMER 模式 (可配置,创建 timer 线程)

‌‌‌  通过宏定义 RT_USING_TIMER_SOFT 来决定是否启用该模式。该模式被启用后,系统会在初始化时创建一个 timer 线程,然后 SOFT_TIMER 模式的定时器超时函数在都会在 timer 线程的上下文环境中执行。可以在初始化 / 创建定时器时使用参数 RT_TIMER_FLAG_SOFT_TIMER 来指定设置 SOFT_TIMER 模式。

9.2.4 . 定时器工作机制

‌‌‌  在 RT-Thread 定时器模块中维护着两个重要的全局变量
‌‌‌  (1)当前系统经过的 tick 时间 rt_tick(当硬件定时器中断来临时,它将加 1);
‌‌‌  (2)定时器链表 rt_timer_list。系统新创建并激活的定时器都会按照以超时时间排序的方式插入到 rt_timer_list 链表中。

9.2.5 . 定时器控制块

‌‌‌  定时器控制块由结构体 struct rt_timer 定义并形成定时器内核对象,再链接到内核对象容器中进行管理。它是操作系统用于管理定时器的一个数据结构,会存储定时器的一些信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
‌‌‌  struct rt_timer
‌‌‌  {
struct rt_object parent;
rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL];
/* 定时器链表节点 */
void (*timeout_func)(void *parameter);
/* 定时器超时调用的函数 */
void *parameter;
/* 超时函数的参数 */
rt_tick_t init_tick;
/* 定时器初始超时节拍数 */
rt_tick_t timeout_tick;
/* 定时器实际超时时的节拍数 */
‌‌‌  };
‌‌‌  typedef struct rt_timer *rt_timer_t;
‌‌‌  //list 成员则用于把一个激活的(已经启动的)定时器链接到 rt_timer_list 链表中。

9.2.6 . 定时器跳表 (Skip List) 算法

‌‌‌  rt_timer_list 链表是一个有序链表,RT-Thread 中使用了跳表算法来加快搜索链表元素的速度。
‌‌‌  跳表是一种基于并联链表的数据结构,实现简单,插入、删除、查找的时间复杂度均为 O (log n)。跳表是链表的一种,但它在链表的基础上增加了 “跳跃” 功能

9.2.7 .定时器的管理方式 (接口函数)

^b6b8c7

1
2
3
4
‌‌‌  //系统启动时需要初始化定时器管理系统
‌‌‌  void rt_system_timer_init(void);
‌‌‌  //如果需要使用 SOFT_TIMER,则系统初始化时,应该调用:
‌‌‌  void rt_system_timer_thread_init(void);

‌‌‌  定时器控制块中含有定时器相关的重要参数,在定时器各种状态间起到纽带的作用。定时器的相关操作如下图所示:
‌‌‌  在这里插入图片描述
‌‌‌  创建和删除定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
‌‌‌  //当动态创建一个定时器时: 调用该函数接口后,内核首先从动态内存堆中分配一个定时器控制块,然后对该控制块进行基本的初始化。
‌‌‌  rt_timer_t rt_timer_create (const char* name, void (*timeout)(void* parameter), void* parameter, rt_tick_t time, rt_uint 8_t flag);
‌‌‌  name 定时器的名称
‌‌‌  void (timeout) (void parameter) 定时器超时函数指针(当定时器超时时,系统会调用这个函数)
‌‌‌  parameter 定时器超时函数的入口参数(当定时器超时时,调用超时回调函数会把这个参数做为入口参数传递给超时函数)
‌‌‌  time 定时器的超时时间,单位是时钟节拍
‌‌‌  flag 定时器创建时的参数,支持的值包括单次定时、周期定时、硬件定时器、软件定时器等(可以用 “或” 关系取多个值)
‌‌‌  返回 ——
‌‌‌  RT_NULL 创建失败(通常会由于系统内存不够用而返回 RT_NULL)
‌‌‌  定时器的句柄 定时器创建成功
‌‌‌  "include/rtdef. h"定时器相关的宏
#define RT_TIMER_FLAG_ONE_SHOT 0 x 0 /* 单次定时 */
#define RT_TIMER_FLAG_PERIODIC 0 x 2 /* 周期定时 */
#define RT_TIMER_FLAG_HARD_TIMER 0 x 0 /* 硬件定时器 */
#define RT_TIMER_FLAG_SOFT_TIMER 0 x 4 /* 软件定时器 */
‌‌‌  rt_err_t rt_timer_delete (rt_timer_t timer);
‌‌‌  timer 定时器句柄,指向要删除的定时器
‌‌‌  返回 ——
‌‌‌  RT_EOK 删除成功(如果参数 timer 句柄是一个 RT_NULL,将会导致一个 ASSERT 断言)

‌‌‌  初始化和脱离定时器

1
2
3
4
5
6
7
8
9
10
‌‌‌  //选择静态创建定时器时,可利用 rt_timer_init 接口来初始化该定时器
‌‌‌  void rt_timer_init (rt_timer_t timer, const char* name, void (*timeout)(void* parameter), void* parameter, rt_tick_t time, rt_uint 8_t flag);
‌‌‌  timer 定时器句柄,指向要初始化的定时器控制块
‌‌‌  name 定时器的名称
‌‌‌  void (timeout) (void parameter) 定时器超时函数指针(当定时器超时时,系统会调用这个函数)
‌‌‌  parameter 定时器超时函数的入口参数(当定时器超时时,调用超时回调函数会把这个参数做为入口参数传递给超时函数)
‌‌‌  time 定时器的超时时间,单位是时钟节拍
‌‌‌  flag 定时器创建时的参数
‌‌‌  //当一个静态定时器不需要再使用时
‌‌‌  rt_err_t rt_timer_detach (rt_timer_t timer);

‌‌‌  启动和停止定时器

1
2
3
4
5
6
7
8
‌‌‌  //当定时器被创建或者初始化以后,并不会被立即启动,必须在调用启动定时器函数接口后,才开始工作
‌‌‌  rt_err_t rt_timer_start (rt_timer_t timer);
‌‌‌  调用定时器启动函数接口后,定时器的状态将更改为激活状态(RT_TIMER_FLAG_ACTIVATED),
‌‌‌  并按照超时顺序插入到 rt_timer_list 队列链表中
‌‌‌  //启动定时器以后,若想使它停止,可以使用下面的函数接口
‌‌‌  rt_err_t rt_timer_stop (rt_timer_t timer);
‌‌‌  调用定时器停止函数接口后,定时器状态将更改为停止状态,并从 rt_timer_list 链表中脱离出来不参与定时器超时检查。
‌‌‌  当一个(周期性)定时器超时时,也可以调用这个函数接口停止这个(周期性)定时器本身

‌‌‌  控制定时器

1
2
3
4
5
6
7
‌‌‌  rt_err_t rt_timer_control (rt_timer_t timer, rt_uint 8_t cmd, void* arg);
‌‌‌  //cmd 用于控制定时器的命令,当前支持四个命令,分别是设置定时时间,查看定时时间,设置单次触发,设置周期触发
‌‌‌  //arg 与 cmd 相对应的控制命令参数 比如,cmd 为设定超时时间时,就可以将超时时间参数通过 arg 进行设定 cmd 支持的命令:
#define RT_TIMER_CTRL_SET_TIME 0 x 0 /* 设置定时器超时时间 */
#define RT_TIMER_CTRL_GET_TIME 0 x 1 /* 获得定时器超时时间 */
#define RT_TIMER_CTRL_SET_ONESHOT 0 x 2 /* 设置定时器为单次定时器 */
#define RT_TIMER_CTRL_SET_PERIODIC 0 x 3 /* 设置定时器为周期型定时器 */

9.3 定时器应用示例

‌‌‌  创建两个动态定时器,一个是单次定时,一个是周期性定时并让周期定时器运行一段时间后停止运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <rtthread.h>
‌‌‌  /* 定时器的控制块 */
‌‌‌  static rt_timer_t timer 1;
‌‌‌  static rt_timer_t timer 2;
‌‌‌  static int cnt = 0;
‌‌‌  /* 定时器 1 超时函数 */
‌‌‌  static void timeout 1 (void *parameter)
‌‌‌  {rt_kprintf ("periodic timer is timeout %d\n", cnt);/* 运行第 10 次,停止周期定时器 */if (cnt++>= 9){rt_timer_stop (timer 1); rt_kprintf ("periodic timer was stopped! \n");}
‌‌‌  }
‌‌‌  /* 定时器 2 超时函数 */
‌‌‌  static void timeout 2 (void *parameter)
‌‌‌  {rt_kprintf ("one shot timer is timeout\n");
‌‌‌  }
‌‌‌  int timer_sample (void)
‌‌‌  {/* 创建定时器 1 周期定时器 */timer 1 = rt_timer_create ("timer 1", timeout 1, RT_NULL, 10, RT_TIMER_FLAG_PERIODIC);/* 启动定时器 1 */if (timer 1 != RT_NULL) rt_timer_start (timer 1);/* 创建定时器 2 单次定时器 */timer 2 = rt_timer_create ("timer 2", timeout 2, RT_NULL, 30, RT_TIMER_FLAG_ONE_SHOT);/* 启动定时器 2 */if (timer 2 != RT_NULL) rt_timer_start (timer 2); return 0;
‌‌‌  }
‌‌‌  /* 导出到 msh 命令列表中 */
‌‌‌  MSH_CMD_EXPORT (timer_sample, timer sample);

‌‌‌  初始化定时器的例子与创建定时器的例子类似,这个程序会初始化 2 个静态定时器,一个是单次定时,一个是周期性的定时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <rtthread.h>
‌‌‌  /* 定时器的控制块 */
‌‌‌  static struct rt_timer timer 1;
‌‌‌  static struct rt_timer timer 2;
‌‌‌  static int cnt = 0;
‌‌‌  /* 定时器 1 超时函数 */
‌‌‌  static void timeout 1 (void* parameter)
‌‌‌  {rt_kprintf ("periodic timer is timeout\n");/* 运行 10 次 */if (cnt++>= 9){rt_timer_stop (&timer 1);}
‌‌‌  }
‌‌‌  /* 定时器 2 超时函数 */
‌‌‌  static void timeout 2 (void* parameter)
‌‌‌  {rt_kprintf ("one shot timer is timeout\n");
‌‌‌  }
‌‌‌  int timer_static_sample (void)
‌‌‌  {/* 初始化定时器 */rt_timer_init (&timer 1, "timer 1", /* 定时器名字是 timer 1 */timeout 1, /* 超时时回调的处理函数 */RT_NULL, /* 超时函数的入口参数 */10, /* 定时长度,以 OS Tick 为单位,即 10 个 OS Tick */RT_TIMER_FLAG_PERIODIC); /* 周期性定时器 */rt_timer_init (&timer 2, "timer 2", /* 定时器名字是 timer 2 */timeout 2, /* 超时时回调的处理函数 */RT_NULL, /* 超时函数的入口参数 */30, /* 定时长度为 30 个 OS Tick */RT_TIMER_FLAG_ONE_SHOT); /* 单次定时器 *//* 启动定时器 */rt_timer_start (&timer 1); rt_timer_start (&timer 2); return 0;
‌‌‌  }
‌‌‌  /* 导出到 msh 命令列表中 */
‌‌‌  MSH_CMD_EXPORT (timer_static_sample, timer_static sample);

9.4 高精度延时

‌‌‌  定时器的最小精度是由系统时钟节拍所决定的, 当需要实现更短时间长度的系统定时时,例如 OS Tick 是 10 ms,而程序需要实现 1 ms 的定时或延时,这种时候操作系统定时器将不能够满足要求,只能通过读取系统某个硬件定时器的计数器或直接使用硬件定时器的方式。
‌‌‌  在 Cortex-M 系列中,SysTick 已经被 RT-Thread 用于作为 OS Tick 使用,它被配置成 1/RT_TICK_PER_SECOND 秒后触发一次中断的方式,中断处理函数使用 Cortex-M 3 默认的 SysTick_Handler 名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <board.h>
‌‌‌  void rt_hw_us_delay (rt_uint 32_t us)
‌‌‌  {
rt_uint 32_t ticks;
rt_uint 32_t told, tnow, tcnt = 0;
rt_uint 32_t reload = SysTick->LOAD;/* 获得延时经过的 tick 数 */
ticks = us * reload / (1000000 / RT_TICK_PER_SECOND);/* 获得当前时间 */
told = SysTick->VAL; while (1){/* 循环获得当前时间,直到达到指定的时间后退出循环 */
tnow = SysTick->VAL; if (tnow != told){if (tnow < told){tcnt += told - tnow;
}
else{
tcnt += reload - tnow + told;}told = tnow;if (tcnt >= ticks){break;}}}
‌‌‌  }
‌‌‌  入口参数 us 指示出需要延时的微秒数目,这个函数只能支持低于 1 OS Tick 的延时

10 线程间同步

‌‌‌  例如一项工作中的两个线程:一个线程从传感器中接收数据并且将数据写到共享内存中,同时另一个线程周期性的从共享内存中读取数据并发送去显示,下图描述了两个线程间的数据传递:
‌‌‌  在这里插入图片描述

‌‌‌  + 如果对共享内存的访问不是排他性的,那么各个线程间可能同时访问它,这将引起数据一致性的问题。例如,在显示线程试图显示数据之前,接收线程还未完成数据的写入,那么显示将包含不同时间采样的数据,造成显示数据的错乱。
‌‌‌  + 将传感器数据写入到共享内存块的接收线程 #1 和将传感器数据从共享内存块中读出的线程 #2 都会访问同一块内存 。为了防止出现数据的差错,两个线程访问的动作必须是互斥进行的,应该是在一个线程对共享内存块操作完成后,才允许另一个线程去操作,这样,接收线程 #1 与显示线程 #2 才能正常配合,使此项工作正确地执行。
‌‌‌  + 同步是指按预定的先后次序进行运行,线程同步是指多个线程通过特定的机制(如互斥量,事件对象,临界区)来控制线程之间的执行顺序,也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间将是无序的。
‌‌‌  + 多个线程操作 /访问同一块区域(代码),这块代码就称为临界区,上述例子中的共享内存块就是临界区。线程互斥是指对于临界区资源访问的排它性。当多个线程都要使用临界区资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。

核心思想都是:在访问临界区的时候只允许一个 (或一类) 线程运行。

‌‌‌  进入 / 退出临界区的方式有很多种:

1
2
3
4
调用 rt_hw_interrupt_disable () 进入临界区,
调用 rt_hw_interrupt_enable () 退出临界区;详见《中断管理》的全局中断开关内容。
调用 rt_enter_critical () 进入临界区,
调用 rt_exit_critical () 退出临界区。

‌‌‌  使用信号量、互斥量、事件集这些对象进行线程间的同步。

10.1 信号量

信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。

‌‌‌  如下图所示,每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为 5,则表示共有 5 个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。
‌‌‌  在这里插入图片描述

10.1.1 . 信号量控制块

信号量控制块是操作系统用于管理信号量的一个数据结构,由结构体 struct rt_semaphore 表示。另外一种 C 表达方式 rt_sem_t,表示的是信号量的句柄,在 C 语言中的实现是指向信号量控制块的指针。

1
2
3
4
5
6
7
8
9
‌‌‌  struct rt_semaphore
‌‌‌  {
struct rt_ipc_object parent; /* 继承自 ipc_object 类 */
rt_uint 16_t value; /* 信号量的值 */
‌‌‌  };
‌‌‌  /* rt_sem_t 是指向 semaphore 结构体的指针类型 */
‌‌‌  typedef struct rt_semaphore* rt_sem_t;
‌‌‌  rt_semaphore 对象从 rt_ipc_object 中派生,由 IPC 容器所管理,
‌‌‌  信号量的最大值是 65535

10.1.2 . 信号量的管理方式

‌‌‌  信号量相关接口如下图所示,对一个信号量的操作包含:创建 / 初始化信号量、获取信号量、释放信号量、删除 / 脱离信号量。
‌‌‌  在这里插入图片描述
‌‌‌  创建和删除信号量

1
2
3
4
5
6
7
‌‌‌  //当创建一个信号量时,内核首先创建一个信号量控制块,
‌‌‌  //然后对该控制块进行基本的初始化工作,创建信号量使用下面的函数接口:rt_sem_t rt_sem_create (const char *name, rt_uint 32_t value, rt_uint 8_t flag);
‌‌‌  name 信号量名称
‌‌‌  value 信号量初始值,表示信号量可用资源数
‌‌‌  flag 信号量标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
‌‌‌  /*
‌‌‌  调用这个函数时,系统将先从对象管理器中分配一个 semaphore 对象,并初始化这个对象,然后初始化父类 IPC 对象以及与 semaphore 相关的部分。在创建信号量指定的参数中,信号量标志参数决定了当信号量不可用时,多个线程等待的排队方式。当选择 RT_IPC_FLAG_FIFO(先进先出)方式时,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量;当选择 RT_IPC_FLAG_PRIO(优先级等待)方式时,等待线程队列将按照优先级进行排队,优先级高的等待线程将先获得等待的信号量。*/

RT_IPC_FLAG_FIFO 属于非实时调度方式,除非应用程序非常在意先来后到,并且你清楚地明白所有涉及到该信号量的线程都将会变为非实时线程,方可使用 RT_IPC_FLAG_FIFO,否则建议采用 RT_IPC_FLAG_PRIO,即确保线程的实时性。

1
2
3
‌‌‌  系统不再使用信号量时,可通过删除信号量以释放系统资源,适用于动态创建的信号量。
‌‌‌  rt_err_t rt_sem_delete (rt_sem_t sem);
‌‌‌  如果删除该信号量时,有线程正在等待该信号量,那么删除操作会先唤醒等待在该信号量上的线程(等待线程的返回值是 - RT_ERROR),然后再释放信号量的内存资源。

‌‌‌  初始化和脱离信号量

1
2
3
4
5
6
‌‌‌  对于静态信号量对象,它的内存空间在编译时期就被编译器分配出来,放在读写数据段或未初始化数据段上,此时使用信号量就不再需要使用 rt_sem_create 接口来创建它,而只需在使用前对它进行初始化即可。
‌‌‌  rt_err_t rt_sem_init (rt_sem_t sem, const char *name, rt_uint 32_t value, rt_uint 8_t flag)
‌‌‌  当调用这个函数时,系统将对这个 semaphore 对象进行初始化,然后初始化 IPC 对象以及与 semaphore 相关的部分。信号量标志可用上面创建信号量函数里提到的标志,value为信号量初始值,表示信号量可用资源数
‌‌‌  //脱离信号量就是让信号量对象从内核对象管理器中脱离,适用于静态初始化的信号量。
‌‌‌  rt_err_t rt_sem_detach (rt_sem_t sem);
‌‌‌  使用该函数后,内核先唤醒所有挂在该信号量等待队列上的线程,然后将该信号量从内核对象管理器中脱离。原来挂起在信号量上的等待线程将获得 - RT_ERROR 的返回值。

‌‌‌  获取信号量

1
2
3
4
5
6
7
8
9
10
11
12
‌‌‌  //线程通过获取信号量来获得信号量资源实例,当信号量值大于零时,
‌‌‌  //线程将获得信号量,并且相应的信号量值会减 1
‌‌‌  rt_err_t rt_sem_take (rt_sem_t sem, rt_int 32_t time);
‌‌‌  调用这个函数时,如果信号量的值等于零,那么说明当前信号量资源实例不可用,申请该信号量的线程将根据 time 参数的情况选择直接返回、或挂起等待一段时间、或永久等待,
‌‌‌  直到其他线程或中断释放该信号量。如果在参数 time 指定的时间内依然得不到信号量,线程将超时返回,返回值是 - RT_ETIMEOUT。
‌‌‌  /*
‌‌‌  sem 信号量对象的句柄
‌‌‌  time 指定的等待时间,单位是操作系统时钟节拍(OS Tick)
‌‌‌  返回 ——
‌‌‌  RT_EOK 成功获得信号量
‌‌‌  -RT_ETIMEOUT 超时依然未获得信号量
‌‌‌  -RT_ERROR 其他错误*/

‌‌‌  无等待获取信号量
‌‌‌  当用户不想在申请的信号量上挂起线程进行等待时,可以使用无等待方式获取信号量

1
2
‌‌‌  rt_err_t rt_sem_trytake (rt_sem_t sem);
‌‌‌  信号量资源实例不可用的时候,它不会等待在该信号量上,而是直接返回 - RT_ETIMEOUT

‌‌‌  释放信号量

1
2
‌‌‌  rt_err_t rt_sem_release (rt_sem_t sem);
‌‌‌  当信号量的值等于零时,并且有线程等待这个信号量时,释放信号量将唤醒等待在该信号量线程队列中的第一个线程,由它获取信号量;否则将把信号量的值加 1

10.1.3 . 信号量应用示例

‌‌‌  该例程创建了一个动态信号量,初始化两个线程,一个线程发送信号量,一个线程接收到信号量后,执行相应的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <rtthread.h>

#define THREAD_STACK_SIZE 512
#define THREAD_PRIORITY 20

// 定义一个全局信号量
static rt_sem_t sem;

// 共享资源
static int shared_resource = 0;

// 线程1的入口函数
static void thread1_entry(void *parameter)
{
while (1)
{
// 等待获取信号量
rt_sem_take(sem, RT_WAITING_FOREVER);

// 访问共享资源
shared_resource++;
rt_kprintf("Thread 1: Shared resource value: %d\n", shared_resource);

// 释放信号量
rt_sem_release(sem);

// 延时一段时间
rt_thread_mdelay(1000);
}
}

// 线程2的入口函数
static void thread2_entry(void *parameter)
{
while (1)
{
// 等待获取信号量
rt_sem_take(sem, RT_WAITING_FOREVER);

// 访问共享资源
shared_resource--;
rt_kprintf("Thread 2: Shared resource value: %d\n", shared_resource);

// 释放信号量
rt_sem_release(sem);

// 延时一段时间
rt_thread_mdelay(1500);
}
}

int main(void)
{
// 创建信号量,初始值为1
sem = rt_sem_create("my_semaphore", 1, RT_IPC_FLAG_FIFO);

// 创建线程1
rt_thread_t thread1 = rt_thread_create("thread1", thread1_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, 10, 0);
if (thread1 != RT_NULL)
rt_thread_startup(thread1);

// 创建线程2
rt_thread_t thread2 = rt_thread_create("thread2", thread2_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, 10, 0);
if (thread2 != RT_NULL)
rt_thread_startup(thread2);

// 启动 RT-Thread
rt_thread_startup(rt_thread_self());

return 0;
}

‌‌‌  信号量的另一个应用例程如下所示,本例程将使用 2 个线程、3 个信号量实现生产者与消费者的例子。
‌‌‌  3 个信号量分别为:①lock:信号量锁的作用,因为 2 个线程都会对同一个数组 array 进行操作,所以该数组是一个共享资源,锁用来保护这个共享资源。②empty:空位个数,初始化为 5 个空位。③full:满位个数,初始化为 0 个满位。
‌‌‌  2 个线程分别为:①生产者线程:获取到空位后,产生一个数字,循环放入数组中,然后释放一个满位。②消费者线程:获取到满位后,读取数组内容并相加,然后释放一个空位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <rtthread.h>  

#define THREAD_PRIORITY 6
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5
#define MAXSEM 5

/* 用于放置生产的整数数组 */
rt_uint32_t array[MAXSEM];
/* 指向生产者、消费者在 array 数组中的读写位置 */
static rt_uint32_t set = 0, get = 0;

/* 指向线程控制块的指针(但这里实际上并不需要,因为已经有了 thread_t 类型的变量) */
// static rt_thread_t producer_tid = RT_NULL;
// static rt_thread_t consumer_tid = RT_NULL;

/* 信号量定义 */
struct rt_semaphore sem_lock;
struct rt_semaphore sem_empty;
struct rt_semaphore sem_full;

/* 生产者线程入口 */
void producer_thread_entry(void *parameter)
{
int cnt = 0;
/* 运行 10 次 */
while (cnt < 10)
{
/* 获取一个空位 */
rt_sem_take(&sem_empty, RT_WAITING_FOREVER);
/* 修改 array 内容,上锁 */
rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
array[set % MAXSEM] = cnt + 1;
rt_kprintf("the producer generates a number: %d\n", array[set % MAXSEM]);
set++;
rt_sem_release(&sem_lock);
/* 发布一个满位 */
rt_sem_release(&sem_full);
cnt++;
/* 暂停一段时间 */
rt_thread_mdelay(20);
}
rt_kprintf("the producer exit!\n");
}

// 注意:这里缺少了消费者线程和信号量的初始化代码
// 你需要在某个地方(如 main 函数或某个初始化函数中)添加以下代码来初始化信号量
// rt_sem_init(&sem_lock, "lock", 1, RT_IPC_FLAG_FIFO);
// rt_sem_init(&sem_empty, "empty", MAXSEM, RT_IPC_FLAG_FIFO);
// rt_sem_init(&sem_full, "full", 0, RT_IPC_FLAG_FIFO);

// 并且还需要创建生产者和消费者线程
// rt_thread_init(&producer_tid, "producer", producer_thread_entry, RT_NULL,
// THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
// rt_thread_startup(&producer_tid);
// ...(类似地创建消费者线程)


/* 消费者线程入口 */
void consumer_thread_entry(void *parameter)
{
rt_uint32_t sum = 0;
while (1)
{
/* 获取一个满位 */
rt_sem_take(&sem_full, RT_WAITING_FOREVER);
/* 临界区,上锁进行操作 */
rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
sum += array[get % MAXSEM];
rt_kprintf("the consumer[%d] get a number: %d\n", (get % MAXSEM), array[get % MAXSEM]);
get++;
rt_sem_release(&sem_lock);
/* 释放一个空位 */
rt_sem_release(&sem_empty);
/* 生产者生产到 10 个数目,停止,消费者线程相应停止 */
if (get == 10)
break;
/* 暂停一小会时间 */
rt_thread_mdelay(50);
}
rt_kprintf("the consumer sum is: %d\n", sum);
rt_kprintf("the consumer exit!\n");
}

/* 生产者消费者模型初始化与线程创建 */
int producer_consumer(void)
{
set = 0;
get = 0;
/* 初始化 3 个信号量 */
rt_sem_init(&sem_lock, "lock", 1, RT_IPC_FLAG_PRIO);
rt_sem_init(&sem_empty, "empty", MAXSEM, RT_IPC_FLAG_PRIO);
rt_sem_init(&sem_full, "full", 0, RT_IPC_FLAG_PRIO);

/* 创建生产者线程 */
producer_tid = rt_thread_create("producer", producer_thread_entry, RT_NULL,
THREAD_STACK_SIZE, THREAD_PRIORITY - 1, THREAD_TIMESLICE);
if (producer_tid != RT_NULL)
{
rt_thread_startup(producer_tid);
}
else
{
rt_kprintf("create thread producer failed\n");
return -1; // 明确返回一个错误码
}

/* 创建消费者线程 */
consumer_tid = rt_thread_create("consumer", consumer_thread_entry, RT_NULL,
THREAD_STACK_SIZE, THREAD_PRIORITY + 1, THREAD_TIMESLICE);
if (consumer_tid != RT_NULL)
{
rt_thread_startup(consumer_tid);
}
else
{
rt_kprintf("create thread consumer failed\n");
return -1; // 明确返回一个错误码
}

return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(producer_consumer, producer_consumer sample);

本例程可以理解为生产者生产产品放入仓库,消费者从仓库中取走产品。

(1)生产者线程:

1)获取 1 个空位(放产品 number),此时空位减 1;

2)上锁保护;本次的产生的 number 值为 cnt+1,把值循环存入数组 array 中;再开锁;

3)释放 1 个满位(给仓库中放置一个产品,仓库就多一个满位),满位加 1;

(2)消费者线程:

1)获取 1 个满位(取产品 number),此时满位减 1;

2)上锁保护;将本次生产者生产的 number 值从 array 中读出来,并与上次的 number 值相加;再开锁;

3)释放 1 个空位(从仓库上取走一个产品,仓库就多一个空位),空位加 1。

生产者依次产生 10 个 number,消费者依次取走,并将 10 个 number 的值求和。信号量锁 lock 保护 array
临界区资源:保证了消费者每次取 number 值的排他性,实现了线程间同步。

10.1.4 信号量的使用场合

^7829c8

‌‌‌  可以运用在多种场合中。形成锁、同步、资源计数等关系,也能方便的用于线程与线程、中断与线程间的同步中。

线程同步
线程同步是信号量最简单的一类应用。例如,使用信号量进行两个线程之间的同步,信号量的值初始化成 0,表示具备 0 个信号量资源实例;而尝试获得该信号量的线程,将直接在这个信号量上进行等待。
当持有信号量的线程完成它处理的工作时,释放这个信号量,可以把等待在这个信号量上的线程唤醒,让它执行下一部分工作。这类场合也可以看成把信号量用于工作完成标志:持有信号量的线程完成它自己的工作,然后通知等待该信号量的线程继续下一部分工作

锁(该功能仅做了解)
在计算机操作系统发展历史上,人们早期使用二值信号量来保护临界区,但是在 1990 年,研究人员发现了使用信号量保护临界区 (共享资源)会导致无界优先级反转的问题,因此提出了互斥量的概念。如今,我们已经不使用二值信号量来保护临界区,互斥量取而代之。
在这里插入图片描述

中断与线程的同步
例如一个中断触发,中断服务例程需要通知线程进行相应的数据处理。这个时候可以设置信号量的初始值是 0,线程在试图持有这个信号量时,由于信号量的初始值是 0,线程直接在这个信号量上挂起直到信号量被释放。当中断触发时,先进行与硬件相关的动作,例如从硬件的 I/O 口中读取相应的数据,并确认中断以清除中断源,而后释放一个信号量来唤醒相应的线程以做后续的数据处理。例如 FinSH 线程的处理方式
在这里插入图片描述
信号量的值初始为 0,当 FinSH 线程试图取得信号量时,因为信号量值是 0,所以它会被挂起。当 console 设备有数据输入时,产生中断,从而进入中断服务例程。在中断服务例程中,它会读取 console 设备的数据,并把读得的数据放入 UART buffer 中进行缓冲,而后释放信号量,释放信号量的操作将唤醒 shell 线程。在中断服务例程运行完毕后,如果系统中没有比 shell 线程优先级更高的就绪线程存在时,shell 线程将持有信号量并运行,从 UART buffer 缓冲区中获取输入的数据。

‌‌‌  中断与线程间的互斥不能采用信号量(锁)的方式,而应采用开关中断的方式。

资源计数
信号量也可以认为是一个递增或递减的计数器,需要注意的是信号量的值非负。例如:初始化一个信号量的值为 5,则这个信号量可最大连续减少 5 次,直到计数器减为 0。资源计数适合于线程间工作处理速度不匹配的场合,这个时候信号量可以做为前一线程工作完成个数的计数,而当调度到后一线程时,它也可以以一种连续的方式一次处理多个事件。
一般资源计数类型多是混合方式的线程间同步,因为对于单个的资源处理依然存在线程的多重访问,这就需要对一个单独的资源进行访问、处理,并进行锁方式的互斥操作。

10.2 互斥量

‌‌‌  又叫相互排斥的信号量,是一种特殊的二值信号量。
‌‌‌  和信号量不同的是:拥有互斥量的线程拥有互斥量的所有权,互斥量支持递归访问且能防止线程优先级翻转;并且互斥量只能由持有线程释放,而信号量则可以由任何线程释放。
‌‌‌  互斥量的状态只有两种,开锁或闭锁(两种状态值)。当有线程持有它时,互斥量处于闭锁状态,由这个线程获得它的所有权。相反,当这个线程释放它时,将对互斥量进行开锁,失去它的所有权。当一个线程持有互斥量时,其他线程将不能够对它进行开锁或持有它,持有该互斥量的线程也能够再次获得这个锁而不被挂起
‌‌‌  与一般的二值信号量有很大的不同:在信号量中,因为已经不存在实例,线程递归持有会发生主动挂起(最终形成死锁)
‌‌‌  在这里插入图片描述
‌‌‌  使用信号量会导致的另一个潜在问题是线程优先级翻转问题。所谓优先级翻转,即当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞,实时性难以得到保证。

有优先级为 A、B 和 C 的三个线程,优先级 A> B > C。线程 A,B 处于挂起状态,等待某一事件触发,线程 C 正在运行,此时线程 C 开始使用某一共享资源 M。在使用过程中,线程 A 等待的事件到来,线程 A 转为就绪态,因为它比线程 C 优先级高,所以立即执行。但是当线程 A 要使用共享资源 M 时,由于其正在被线程 C 使用,因此线程 A 被挂起切换到线程 C 运行。如果此时线程 B 等待的事件到来,则线程 B 转为就绪态。由于线程 B 的优先级比线程 C 高,且线程 B 没有用到共享资源 M ,因此线程 B 开始运行,直到其运行完毕,线程 C 才开始运行。只有当线程 C 释放共享资源 M 后,线程 A 才得以执行。在这种情况下,优先级发生了翻转:线程 B 先于线程 A 运行。这样便不能保证高优先级线程的响应时间。
线程优先级翻转
互斥量可以解决优先级翻转问题,实现的是优先级继承协议 (Sha, 1990)。优先级继承是通过在线程 A 尝试获取共享资源而被挂起的期间内,将线程 C 的优先级提升到线程 A 的优先级别,从而解决优先级翻转引起的问题。这样能够防止 C(间接地防止 A)被 B 抢占
在这里插入图片描述

‌‌‌  在获得互斥量后,请尽快释放互斥量,并且在持有互斥量的过程中,不得再行更改持有互斥量线程的优先级,否则可能人为引入无界优先级反转的问题。

10.2.1 . 互斥量控制块

‌‌‌  操作系统用于管理互斥量的一个数据结构,由结构体 struct rt_mutex 表示。rt_mutex_t,表示的是互斥量的句柄

1
2
3
4
5
6
7
8
9
10
11
12
struct rt_ipc_object; // 假设这个结构体已经在其他地方定义  

struct rt_mutex {
struct rt_ipc_object parent; /* 继承自 ipc_object 类 */
rt_uint16_t value; /* 互斥量的值(通常是1表示可用,0表示被占用) */
rt_uint8_t original_priority; /* 持有线程的原始优先级(用于优先级继承算法) */
rt_uint8_t hold; /* 持有线程的持有次数(用于递归锁) */
struct rt_thread *owner; /* 当前拥有互斥量的线程 */
};

/* rt_mutex_t 为指向互斥量结构体的指针类型 */
typedef struct rt_mutex *rt_mutex_t;

10.2.2 . 互斥量的管理方式

‌‌‌  包含:创建 / 初始化互斥量、获取互斥量、释放互斥量、删除 / 脱离互斥量。
‌‌‌  在这里插入图片描述

1
2
3
4
5
6
‌‌‌  //创建和删除互斥量
‌‌‌  rt_mutex_t rt_mutex_create (const char* name, rt_uint 8_t flag);
‌‌‌  //调用这个函数时,系统将先从对象管理器中分配一个 mutex 对象,并初始化这个对象,然后初始化父类 IPC 对象以及与 mutex 相关的部分。
‌‌‌  互斥量的 flag 标志已经作废,无论用户选择 RT_IPC_FLAG_PRIO 还是 RT_IPC_FLAG_FIFO,内核均按照 RT_IPC_FLAG_PRIO 处理。
‌‌‌  //当不再使用互斥量时,通过删除互斥量以释放系统资源,适用于动态创建的互斥量。
‌‌‌  rt_err_t rt_mutex_delete (rt_mutex_t mutex);
1
2
3
4
5
‌‌‌  //初始化和脱离互斥量
‌‌‌  rt_err_t rt_mutex_init (rt_mutex_t mutex, const char* name, rt_uint 8_t flag);
‌‌‌  使用该函数接口时,需指定互斥量对象的句柄(即指向互斥量控制块的指针),互斥量名称以及互斥量标志。
‌‌‌  rt_err_t rt_mutex_detach (rt_mutex_t mutex);
‌‌‌  内核先唤醒所有挂在该互斥量上的线程(线程的返回值是 -RT_ERROR),然后系统将该互斥量从内核对象管理器中脱离。
1
2
3
4
‌‌‌  //获取互斥量
‌‌‌  rt_err_t rt_mutex_take (rt_mutex_t mutex, rt_int 32_t time);
‌‌‌  如果互斥量没有被其他线程控制,那么申请该互斥量的线程将成功获得该互斥量。如果互斥量已经被当前线程线程控制,则该互斥量的持有计数加 1,当前线程也不会挂起等待。
‌‌‌  如果互斥量已经被其他线程占有,则当前线程在该互斥量上挂起等待,直到其他线程释放它或者等待时间超过指定的超时时间。
1
2
3
‌‌‌  //无等待获取互斥量
‌‌‌  rt_err_t rt_mutex_trytake (rt_mutex_t mutex);
‌‌‌  当线程申请的互斥量资源实例不可用的时候,它不会等待在该互斥量上,而是直接返回 - RT_ETIMEOUT。
1
2
‌‌‌  //释放互斥量
‌‌‌  rt_err_t rt_mutex_release (rt_mutex_t mutex);

10.2.3 . 互斥量应用示例

‌‌‌  互斥量不能在中断服务例程中使用
‌‌‌  互斥锁是一种保护共享资源的方法。当一个线程拥有互斥锁的时候,可以保护共享资源不被其他线程破坏。下面用一个例子来说明,有两个线程:线程 1 和线程 2,线程 1 对 2 个 number 分别进行加 1 操作;线程 2 也对 2 个 number 分别进行加 1 操作,使用互斥量保证线程改变 2 个 number 值的操作不被打断。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <rtthread.h>  

#define THREAD_PRIORITY 8
#define THREAD_TIMESLICE 5

/* 指向互斥量的指针 */
static rt_mutex_t dynamic_mutex = RT_NULL;

/* 注意:变量命名中不应包含空格或特殊字符 */
static rt_uint8_t number1, number2; /* 初始化为0是自动的 */

/* 线程栈和线程结构体需要正确的命名和初始化 */
ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;

static void rt_thread_entry1(void *parameter)
{
while (1)
{
/* 线程1获取到互斥量后,先后对number1、number2进行加1操作,然后释放互斥量 */
rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);
number1++;
rt_thread_mdelay(10);
number2++;
rt_mutex_release(dynamic_mutex);
}
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;

static void rt_thread_entry2(void *parameter)
{
while (1)
{
/* 线程2获取到互斥量后,检查number1、number2的值是否相同 */
rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);
if (number1 != number2)
{
rt_kprintf("not protected. number1 = %d, number2 = %d\n", number1, number2);
}
else
{
rt_kprintf("mutex protected, number1 = number2 is %d\n", number1);
}
number1++;
number2++;
rt_mutex_release(dynamic_mutex);
if (number1 >= 50)
{
return;
}
}
}

/* 互斥量示例的初始化 */
int mutex_sample(void)
{
/* 创建一个动态互斥量 */
dynamic_mutex = rt_mutex_create("dmutex", RT_IPC_FLAG_PRIO);
if (dynamic_mutex == RT_NULL)
{
rt_kprintf("create dynamic mutex failed.\n");
return -RT_ERROR; /* 返回错误码 */
}

/* 初始化并启动线程1 */
rt_thread_init(&thread1, "thread1", rt_thread_entry1, RT_NULL,
&thread1_stack[0], sizeof(thread1_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread1);

/* 初始化并启动线程2 */
rt_thread_init(&thread2, "thread2", rt_thread_entry2, RT_NULL,
&thread2_stack[0], sizeof(thread2_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread2);

return 0;
}

/* 导出到MSH命令列表中 */
MSH_CMD_EXPORT(mutex_sample, mutex sample);

10.2.4 . 互斥量的使用场合

‌‌‌  它是信号量的一种,并且它是以锁的形式存在。在初始化的时候,互斥量永远都处于开锁的状态,而被线程持有的时候则立刻转为闭锁的状态。互斥量更适合于:
‌‌‌  (1)线程多次持有互斥量的情况下。这样可以避免同一线程多次递归持有而造成死锁的问题。
‌‌‌  (2)可能会由于多线程同步而造成优先级翻转的情况。

10.3 事件集

‌‌‌  线程间同步的机制之一,一个事件集可以包含多个事件,利用事件集可以完成一对多,多对多的线程间同步。
‌‌‌  一个线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件。这种多个事件的集合可以用一个 32 位无符号整型变量来表示,变量的每一位代表一个事件,线程通过 “逻辑与” 或“逻辑或”将一个或多个事件关联起来,形成事件组合。事件的 “逻辑或” 也称为是独立型同步,指的是线程与任何事件之一发生同步;事件 “逻辑与” 也称为是关联型同步,指的是线程与若干事件都发生同步。

RT-Thread 定义的事件集有以下特点:
1)事件只与线程相关,事件间相互独立:每个线程可拥有 32 个事件标志,采用一个 32 bit 无符号整型数进行记录,每一个 bit 代表一个事件;
2)事件仅用于同步,不提供数据传输功能;
3)事件无排队性,即多次向线程发送同一事件 (如果线程还未来得及读走),其效果等同于只发送一次。
在 RT-Thread 中,每个线程都拥有一个事件信息标记,它有三个属性,分别是 RT_EVENT_FLAG_AND (逻辑与), RT_EVENT_FLAG_OR (逻辑或)以及 RT_EVENT_FLAG_CLEAR (清除标记)。当线程等待事件同步时,可以通过 32 个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。
在这里插入图片描述
如上图所示,线程 #1 的事件标志中第 1 位和第 30 位被置位,如果事件信息标记位设为逻辑与,则表示线程 #1 只有在事件 1 和事件 30 都发生以后才会被触发唤醒,如果事件信息标记位设为逻辑或,则事件 1 或事件 30 中的任意一个发生都会触发唤醒线程 #1 。如果信息标记同时设置了清除标记位,则当线程 #1 唤醒后将主动把事件 1 和事件 30 清为零,否则事件标志将依然存在(即置 1)。

10.3.1 . 事件集控制块

‌‌‌  由结构体 struct rt_event 表示。另外一种 C 表达方式 rt_event_t,表示的是事件集的句柄,在 C 语言中的实现是事件集控制块的指针. 由 IPC 容器所管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 假设这是IPC对象的定义  
struct rt_ipc_object
{
// IPC对象可能包含的一些字段,例如状态、等待队列等
// 这里只是示例,具体实现取决于你的系统
int status;
// ... 其他字段
};

// 事件结构体定义
struct rt_event
{
struct rt_ipc_object parent; /* 继承自 ipc_object 类,实际上是包含关系 */
/* 事件集合,每一 bit 表示 1 个事件,bit 位的值可以标记某事件是否发生 */
rt_uint32_t set;
};

// rt_event_t 是指向事件结构体的指针类型
typedef struct rt_event* rt_event_t;

10.3.2 . 事件集的管理方式

‌‌‌  在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
‌‌‌  //创建一个事件集时,内核首先创建一个事件集控制块
‌‌‌  rt_event_t rt_event_create (const char* name, rt_uint 8_t flag);
‌‌‌  flag 事件集的标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
‌‌‌  RT_IPC_FLAG_FIFO 属于非实时调度方式,除非应用程序非常在意先来后到,并且你清楚地明白所有涉及到该事件集的线程都将会变为非实时线程,
‌‌‌  方可使用 RT_IPC_FLAG_FIFO,否则建议采用 RT_IPC_FLAG_PRIO,即确保线程的实时性。
‌‌‌  //通过删除事件集对象控制块来释放系统资源
‌‌‌  rt_err_t rt_event_delete (rt_event_t event);
‌‌‌  调用 rt_event_delete 函数删除一个事件集对象时,应该确保该事件集不再被使用。在删除前会唤醒所有挂起在该事件集上的线程(线程的返回值是 - RT_ERROR)
‌‌‌  //初始化事件集
‌‌‌  rt_err_t rt_event_init (rt_event_t event, const char* name, rt_uint 8_t flag);
‌‌‌  调用该接口时,需指定静态事件集对象的句柄(即指向事件集控制块的指针),然后系统会初始化事件集对象,并加入到系统对象容器中进行管理。
‌‌‌  //脱离事件集
‌‌‌  rt_err_t rt_event_detach (rt_event_t event);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
‌‌‌/**  
* 发送事件
*
* 通过参数 set 指定的事件标志来设定 event 事件集对象的事件标志值,
* 然后遍历等待在 event 事件集对象上的等待线程链表,
* 判断是否有线程的事件激活要求与当前 event 对象事件标志值匹配,
* 如果有,则唤醒该线程。
*
* @param event 指向事件集对象的指针
* @param set 指定要设置的事件标志位
* @return 返回执行结果,rt_err_t 类型
*/
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);

/**
* 接收事件
*
* 一个事件集对象可同时等待接收 32 个事件,内核可以通过指定选择参数 "逻辑与" 或 "逻辑或" 来选择如何激活线程。
* 系统首先根据 set 参数和接收选项 option 来判断它要接收的事件是否发生,
* 如果已经发生,则根据参数 option 上是否设置有 RT_EVENT_FLAG_CLEAR 来决定是否重置事件的相应标志位,
* 然后返回(其中 recved 参数返回接收到的事件);
* 如果没有发生,则把等待的 set 和 option 参数填入线程本身的结构中,
* 然后把线程挂起在此事件上,直到其等待的事件满足条件或等待时间超过指定的超时时间。
*
* @param event 指向事件集对象的指针
* @param set 指定要等待接收的事件标志位
* @param option 接收选项,可以是 RT_EVENT_FLAG_OR 或 RT_EVENT_FLAG_AND,以及可选的 RT_EVENT_FLAG_CLEAR
* @param timeout 等待的超时时间,单位通常是系统时钟节拍
* @param recved 如果非NULL,用于返回实际接收到的事件标志位
* @return 返回执行结果,rt_err_t 类型
*/
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);

// option 的值可取:
/* 选择 逻辑与 或 逻辑或 的方式接收事件 */
#define RT_EVENT_FLAG_OR (1 << 0)
#define RT_EVENT_FLAG_AND (1 << 1)
/* 选择清除重置事件标志位 */
#define RT_EVENT_FLAG_CLEAR (1 << 2)

10.3.3 . 事件集应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <rtthread.h>  

#define THREAD_PRIORITY 9
#define THREAD_TIMESLICE 5
#define EVENT_FLAG_3 (1 << 3)
#define EVENT_FLAG_5 (1 << 5)

/* 事件控制块 */
static struct rt_event event;

/* 线程 1 栈空间 */
ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];

/* 线程 1 控制块 */
static struct rt_thread thread1;

/* 线程 1 入口函数 */
static void thread1_recv_event(void *param)
{
rt_uint32_t e;
/* 第一次接收事件,事件 3 或事件 5 任意一个可以触发线程 1,接收完后清除事件标志 */
if (rt_event_recv(&event, (EVENT_FLAG_3 | EVENT_FLAG_5), RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &e) == RT_EOK)
{
rt_kprintf("thread 1: OR recv event 0x%x\n", e);
}
rt_kprintf("thread 1: delay 1 s to prepare the second event\n");
rt_thread_mdelay(1000);
/* 第二次接收事件,事件 3 和事件 5 均发生时才可以触发线程 1,接收完后清除事件标志 */
if (rt_event_recv(&event, (EVENT_FLAG_3 | EVENT_FLAG_5), RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &e) == RT_EOK)
{
rt_kprintf("thread 1: AND recv event 0x%x\n", e);
}
rt_kprintf("thread 1 leave.\n");
}

/* 线程 2 栈空间 */
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];

/* 线程 2 控制块 */
static struct rt_thread thread2;

/* 线程 2 入口 */
static void thread2_send_event(void *param)
{
rt_kprintf("thread 2: send event 3\n");
rt_event_send(&event, EVENT_FLAG_3);
rt_thread_mdelay(200);
rt_kprintf("thread 2: send event 5\n");
rt_event_send(&event, EVENT_FLAG_5);
rt_thread_mdelay(200);
rt_kprintf("thread 2: send event 3\n");
rt_event_send(&event, EVENT_FLAG_3);
rt_kprintf("thread 2 leave.\n");
}

/* 事件和线程初始化及启动函数 */
int event_sample(void)
{
rt_err_t result;
/* 初始化事件对象 */
result = rt_event_init(&event, "event", RT_IPC_FLAG_PRIO);
if (result != RT_EOK)
{
rt_kprintf("init event failed.\n");
return -RT_ERROR;
}
/* 初始化并启动线程 1 */
rt_thread_init(&thread1, "thread1", thread1_recv_event, RT_NULL,
&thread1_stack[0], sizeof(thread1_stack),
THREAD_PRIORITY - 1, THREAD_TIMESLICE);
rt_thread_startup(&thread1);
/* 初始化并启动线程 2 */
rt_thread_init(&thread2, "thread2", thread2_send_event, RT_NULL,
&thread2_stack[0], sizeof(thread2_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread2);
return 0;
}

/* 在RT-Thread的main函数中调用event_sample()来启动示例 */

10.3.4 . 事件集的使用场合

可使用于多种场合,它能够在一定程度上替代信号量,用于线程间同步。一个线程或中断服务例程发送一个事件给事件集对象,而后等待的线程被唤醒并对相应的事件进行处理。但是它与信号量不同的是,事件的发送操作在事件未清除前,是不可累计的,而信号量的释放动作是累计的。事件的另一个特性是,接收线程可等待多种事件,即多个事件对应一个线程或多个线程。同时按照线程等待的参数,可选择是 “逻辑或” 触发还是 “逻辑与” 触发。这个特性也是信号量等所不具备的

11 线程间通信

‌‌‌  在裸机编程中,经常会使用全局变量进行功能间的通信,如某些功能可能由于一些操作而改变全局变量的值,另一个功能对此全局变量进行读取,根据读取到的全局变量值执行相应的动作,达到通信协作的目的。RT-Thread 中则提供了更多的工具帮助在不同的线程中间传递信息

11.1 邮箱

‌‌‌  实时操作系统中一种典型的线程间通信方法。

举一个简单的例子,有两个线程,线程 1 检测按键状态并发送,线程 2 读取按键状态并根据按键的状态相应地改变 LED 的亮灭。这里就可以使用邮箱的方式进行通信,线程 1 将按键的状态作为邮件发送到邮箱,线程 2 在邮箱中读取邮件获得按键状态并对 LED 执行亮灭操作。
线程 1 也可以扩展为多个线程。例如,共有三个线程,线程 1 检测并发送按键状态,线程 2 检测并发送 ADC 采样信息,线程 3 则根据接收的信息类型不同,执行不同的操作。

11.1.1 -0. 邮箱的工作机制

‌‌‌  用于线程间通信,特点是开销比较低,效率较高。邮箱中的每一封邮件只能容纳固定的 4 字节内容(针对 32 位处理系统,指针的大小即为 4 个字节,所以一封邮件恰好能够容纳一个指针)

典型的邮箱也称作交换消息,如下图所示,线程或中断服务例程把一封 4 字节长度的邮件发送到邮箱中,而一个或多个线程可以从邮箱中接收这些邮件并进行处理。
在这里插入图片描述
非阻塞方式的邮件发送过程能够安全的应用于中断服务中,是线程、中断服务、定时器向线程发送消息的有效手段。通常来说,邮件收取过程可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。当邮箱中不存在邮件且超时时间不为 0 时,邮件收取过程将变成阻塞方式。在这类情况下,只能由线程进行邮件的收取。

‌‌‌  当一个线程向邮箱发送邮件时,如果邮箱没满,将把邮件复制到邮箱中。如果邮箱已经满了,发送线程可以设置超时时间,选择等待挂起或直接返回 - RT_EFULL。如果发送线程选择挂起等待,那么当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送。
‌‌‌  当一个线程从邮箱中接收邮件时,如果邮箱是空的,接收线程可以选择是否等待挂起直到收到新的邮件而唤醒,或可以设置超时时间。当达到设置的超时时间,邮箱依然未收到邮件时,这个选择超时等待的线程将被唤醒并返回 - RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的 4 个字节邮件到接收缓存中。

11.1.2 . 邮箱控制块

‌‌‌  管理邮箱的一个数据结构,由结构体 struct rt_mailbox 表示。另外一种 C 表达方式 rt_mailbox_t,表示的是邮箱的句柄

1
2
3
4
5
6
7
8
9
10
11
12
struct rt_mailbox  
{
struct rt_ipc_object parent; /* 继承自IPC对象的父结构体 */
rt_uint32_t* msg_pool; /* 邮箱缓冲区的开始地址 */
rt_uint16_t size; /* 邮箱缓冲区的大小(以字节为单位)*/
rt_uint16_t entry; /* 邮箱中当前存储的邮件数目 */
rt_uint16_t in_offset; /* 写入(发送)时的偏移量 */
rt_uint16_t out_offset; /* 读取(接收)时的偏移量 */
rt_list_t suspend_sender_thread;/* 发送时因邮箱满而被挂起的线程队列 */
};

typedef struct rt_mailbox* rt_mailbox_t; /* 邮箱类型指针的定义 */

11.1.3 . 邮箱的管理方式

‌‌‌  在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* 动态创建邮箱对象  
* @param name 邮箱的名称
* @param msg_num 邮箱可以容纳的消息数量(假设每个消息占用固定大小的内存)
* @param msg_size 每个消息的大小(以字节为单位)
* @param flag 邮箱标志,RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
* @return 指向创建的邮箱对象的指针,失败时返回RT_NULL
*/
rt_mailbox_t* rt_mb_create(const char* name, rt_uint16_t msg_num, rt_uint16_t msg_size, rt_uint8_t flag);

/* 删除邮箱
* @param mb 指向要删除的邮箱对象的指针
* @return RT_EOK 表示成功,其他值表示错误
*/
rt_err_t rt_mb_delete(rt_mailbox_t* mb);

/* 初始化邮箱(内存由调用者提供)
* @param mb 指向邮箱对象的指针
* @param name 邮箱的名称
* @param msgpool 指向消息池内存的指针
* @param msg_num 邮箱可以容纳的消息数量
* @param msg_size 每个消息的大小(以字节为单位)
* @param flag 邮箱标志
* @return RT_EOK 表示成功,其他值表示错误
*/
rt_err_t rt_mb_init(rt_mailbox_t* mb, const char* name, void* msgpool, rt_uint16_t msg_num, rt_uint16_t msg_size, rt_uint8_t flag);

/* 脱离邮箱(通常不是RT-Thread标准API的一部分,可能是自定义的)
* @param mb 指向邮箱对象的指针
* @return RT_EOK 表示成功,其他值表示错误
*/
rt_err_t rt_mb_detach(rt_mailbox_t* mb);

/* 注意:上面的函数声明是假设性的,并且可能需要根据RT-Thread的实际实现进行调整。
* 在RT-Thread中,通常邮箱的创建和初始化是通过rt_mb_init()和相关的内存分配函数来完成的,
* 而删除则是通过rt_mb_detach()或类似的函数(但RT-Thread标准API中可能没有直接名为rt_mb_detach的函数,
* 这可能是个自定义函数或误解)。
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* 发送邮件到邮箱  
* @param mb 指向邮箱对象的指针
* @param value 发送的邮件值,理论上应该是32位的数据,但根据实现,它可能被解释为整型值或指针
* @return RT_EOK 表示成功,-RT_EFULL 表示邮箱已满,-RT_ERROR 表示其他错误
*/
rt_err_t rt_mb_send(rt_mailbox_t mb, rt_uint32_t value);

/* 等待方式发送邮件到邮箱
* @param mb 指向邮箱对象的指针
* @param value 发送的邮件值
* @param timeout 等待的超时时间(以系统时钟节拍为单位),RT_WAITING_FOREVER 表示无限等待
* @return RT_EOK 表示成功,-RT_EFULL 表示邮箱已满(在超时之前),-RT_ETIMEOUT 表示超时,-RT_ERROR 表示其他错误
*/
rt_err_t rt_mb_send_wait(rt_mailbox_t mb, rt_uint32_t value, rt_int32_t timeout);

/* 发送紧急邮件到邮箱
* @param mb 指向邮箱对象的指针
* @param value 发送的紧急邮件值,通常被解释为整型值,但具体取决于实现
* @return RT_EOK 表示成功,-RT_EFULL 表示邮箱已满(但即使邮箱满,紧急邮件也可能被特殊处理),-RT_ERROR 表示其他错误
* 注意:紧急邮件通常会被插入到邮箱队列的头部,但具体行为取决于RT-Thread的实现或邮箱的配置。
*/
rt_err_t rt_mb_urgent(rt_mailbox_t mb, rt_ubase_t value); // 注意:这里使用了rt_ubase_t,它可能是rt_uint32_t的别名或类似类型

/* 从邮箱接收邮件
* @param mb 指向邮箱对象的指针
* @param value 指向用于存储接收到的邮件值的变量的指针
* @param timeout 等待的超时时间(以系统时钟节拍为单位),RT_WAITING_FOREVER 表示无限等待
* @return RT_EOK 表示成功接收到邮件,-RT_EEMPTY 表示邮箱为空(在超时之前),-RT_ETIMEOUT 表示超时,-RT_ERROR 表示其他错误
*/
rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_uint32_t* value, rt_int32_t timeout);

11.1.4 . 邮箱使用示例

‌‌‌  初始化 2 个静态线程,一个静态的邮箱对象,其中一个线程往邮箱中发送邮件,一个线程往邮箱中收取邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <rtthread.h>  

#define THREAD_PRIORITY 25
#define THREAD_TIMESLICE 5

static rt_mailbox_t mb;
static rt_uint8_t mb_pool[128]; // 假设邮箱使用的内存池大小
static char mb_str1[] = "Hello from mb_str1";
static char mb_str2[] = "Hello from mb_str2";
static char mb_str3[] = "Thread 2 finished";

static void thread1_entry(void *parameter)
{
char *str;
while (1)
{
rt_kprintf("thread 1: try to recv a mail\n");
if (rt_mb_recv(&mb, (rt_uint32_t *)&str, RT_WAITING_FOREVER) == RT_EOK)
{
rt_kprintf("thread 1: get a mail from mailbox, the content: %s\n", str);
if (str == mb_str3)
break;
rt_thread_mdelay(100);
}
}
rt_mb_detach(&mb);
}

ALIGN(RT_ALIGN_SIZE)
static rt_uint8_t thread2_stack[1024];
static struct rt_thread thread2;

static void thread2_entry(void *parameter)
{
rt_uint8_t count;
count = 0;
while (count < 10)
{
count++;
if (count & 0x1)
{
rt_mb_send(&mb, (rt_uint32_t)&mb_str1);
}
else
{
rt_mb_send(&mb, (rt_uint32_t)&mb_str2);
}
rt_thread_mdelay(200);
}
rt_mb_send(&mb, (rt_uint32_t)&mb_str3);
}

int mailbox_sample(void)
{
rt_err_t result;

/* 初始化一个 mailbox */
result = rt_mb_init(&mb, "mbt", /* 名称是 mbt */
&mb_pool[0], /* 邮箱用到的内存池是 mb_pool */
sizeof(mb_pool) / sizeof(rt_uint32_t), /* 假设每封邮件占用一个rt_uint32_t的空间 */
RT_IPC_FLAG_FIFO); /* 采用 FIFO 方式进行线程等待 */
if (result != RT_EOK)
{
rt_kprintf("init mailbox failed.\n");
return -1;
}

rt_thread_init(&thread2, "thread 2", thread2_entry, RT_NULL,
&thread2_stack[0], sizeof(thread2_stack), THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread2);

rt_thread_init(&thread1, "thread 1", thread1_entry, RT_NULL,
RT_NULL, 0, THREAD_PRIORITY, THREAD_TIMESLICE); // 假设thread1使用默认栈
rt_thread_startup(&thread1);

return 0;
}

11.1.5 . 邮箱的使用场合

‌‌‌  简单的线程间消息传递方式,特点是开销比较低,效率较高。
‌‌‌  能够一次传递一个 4 字节大小的邮件,并且邮箱具备一定的存储功能,能够缓存一定数量的邮件数; 当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中,即邮箱也可以传递指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct msg
‌‌{
rt_uint 8_t *data_ptr;
rt_uint 32_t data_size;
‌‌};
‌‌//其中包含了指向数据的指针 data_ptr 和数据块长度的变量 data_size。当一个线程需要把这个消息发送给另外一个线程时,可以采用如下的操作
‌‌struct msg* msg_ptr;
‌‌msg_ptr = (struct msg*) rt_malloc (sizeof (struct msg));
‌‌msg_ptr->data_ptr = ...; /* 指向相应的数据块地址 */
‌‌msg_ptr->data_size = len; /* 数据块的长度 */
‌‌/* 发送这个消息指针给 mb 邮箱 */
‌‌rt_mb_send (mb, (rt_uint 32_t) msg_ptr);
‌‌//接收线程中,因为收取过来的是指针,而 msg_ptr 是一个新分配出来的内存块,所以在接收线程处理完毕后,需要释放相应的内存块
‌‌struct msg* msg_ptr;
‌‌if (rt_mb_recv (mb, (rt_uint 32_t*)&msg_ptr) == RT_EOK)
‌‌{
/* 在接收线程处理完毕后,需要释放相应的内存块 */
rt_free (msg_ptr);
‌‌}

11.2 消息队列(邮箱的扩展)

‌‌‌  另一种常用的线程间通讯方式,是邮箱的扩展。可以应用在多种场合:线程间的消息交换、使用串口接收不定长数据等。

11.2.1 消息队列的工作机制

‌‌‌  能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。是一种异步的通信方式

‌‌‌  在这里插入图片描述

消息队列对象由多个元素组成,当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。同时每个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息;消息队列中的第一个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息队列控制块中的 msg_queue_head 和 msg_queue_tail;有些消息框可能是空的,它们通过 msg_queue_free 形成一个空闲消息框链表。所有消息队列中的消息框总数即是消息队列的长度,这个长度可在消息队列创建时指定。

11.2.2 消息队列控制块

‌‌‌  管理消息队列的一个数据结构,由结构体 struct rt_messagequeue 表示。另外一种 C 表达方式 rt_mq_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct rt_messagequeue  
{
struct rt_ipc_object parent; /* 继承自IPC对象的基类 */
void* msg_pool; /* 指向存放消息的缓冲区的指针 */
rt_uint16_t msg_size; /* 每个消息的长度 */
rt_uint16_t max_msgs; /* 最大能够容纳的消息数 */
rt_uint16_t entry; /* 队列中已有的消息数 */
void* msg_queue_head; /* 消息链表头 */
void* msg_queue_tail; /* 消息链表尾 */
void* msg_queue_free; /* 空闲消息链表 */
rt_list_t suspend_sender_thread; /* 发送线程的挂起等待队列 */
};

typedef struct rt_messagequeue* rt_mq_t; /* 定义一个指向该结构体的指针类型 */

11.2.3 消息队列的管理方式

‌‌‌  在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 创建消息队列  
// 创建一个新的消息队列,分配必要的资源并初始化它
rt_mq_t rt_mq_create(const char* name, rt_size_t msg_size, rt_size_t max_msgs, rt_uint8_t flag);
// 描述:
// name: 消息队列的名称,用于调试和查找。
// msg_size: 每个消息的大小(不包括消息头)。
// max_msgs: 消息队列能够容纳的最大消息数。
// flag: 创建消息队列时的标志,例如RT_IPC_FLAG_FIFO(先进先出)或RT_IPC_FLAG_PRIO(优先级)。
// 返回值:成功时返回消息队列的句柄,失败时返回RT_NULL。

// 删除消息队列
// 销毁一个消息队列,释放其占用的资源
rt_err_t rt_mq_delete(rt_mq_t mq);
// 描述:
// mq: 指向要删除的消息队列的句柄。
// 返回值:操作成功时返回RT_EOK,失败时返回错误码。

// 初始化消息队列对象
// 初始化一个已经分配但尚未初始化的消息队列对象
rt_err_t rt_mq_init(rt_mq_t mq, const char* name, void* msgpool, rt_size_t msg_size, rt_size_t pool_size, rt_uint8_t flag);
// 描述:
// mq: 指向要初始化的消息队列对象的句柄。
// name: 消息队列的名称。
// msgpool: 指向预先分配的内存池,用于存放消息。
// msg_size: 每个消息的大小(不包括消息头)。
// pool_size: 内存池中消息的总大小(包括消息头和消息内容),根据max_msgs和msg_size计算得出。
// flag: 初始化时的标志,与rt_mq_create中的flag相同。
// 返回值:操作成功时返回RT_EOK,失败时返回错误码。

// 注意:这里的pool_size应该是根据max_msgs和每个消息的总大小(包括消息头)计算得出的,但在rt_mq_init的原型中并未直接包含max_msgs参数。
// 在实际使用中,用户需要确保msgpool足够大,能够容纳max_msgs个消息,并且每个消息都包含必要的消息头。

// 脱离消息队列
// 这个函数名在RT-Thread的标准API中可能不是直接存在的,但假设其功能是释放消息队列对象占用的内核资源,同时保留其用户空间部分(如果有的话)
// 注意:这通常不是标准RT-Thread API的一部分,可能是一个自定义函数或特定环境下的实现。
rt_err_t rt_mq_detach(rt_mq_t mq);
// 描述(假设):
// mq: 指向要脱离的消息队列的句柄。
// 该函数可能将消息队列从内核的IPC对象管理器中移除,但保留用户空间中的消息池等资源供后续使用。
// 返回值:操作成功时返回RT_EOK,失败时返回错误码。

// 注意:在RT-Thread中,通常没有名为rt_mq_detach的标准函数。如果需要类似的功能,可能需要自己管理消息队列的生命周期和资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 发送消息  
// 将消息内容发送到指定的消息队列中
rt_err_t rt_mq_send(rt_mq_t mq, void* buffer, rt_size_t size);
// 描述:
// mq: 指向目标消息队列的句柄。
// buffer: 指向要发送的消息内容的指针。
// size: 要发送的消息内容的大小(字节数)。注意,这个大小不能超过消息队列中每个消息的大小。
// 返回值:成功时返回RT_EOK,如果消息队列已满(即空闲消息链表上没有可用的空闲消息块),则返回错误码。

// 等待方式发送消息 (阻塞)
// 将消息内容发送到指定的消息队列中,如果队列满,则等待直到有空间可用或超时
rt_err_t rt_mq_send_wait(rt_mq_t mq, const void* buffer, rt_size_t size, rt_int32_t timeout);
// 描述:
// mq: 指向目标消息队列的句柄。
// buffer: 指向要发送的消息内容的指针(const修饰表示不修改原内容)。
// size: 要发送的消息内容的大小(字节数)。
// timeout: 等待超时时间(毫秒)。如果设置为RT_WAITING_FOREVER,则无限期等待。
// 返回值:成功时返回RT_EOK,如果等待超时则返回错误码。

// 发送紧急消息
// 将紧急消息内容发送到指定的消息队列中,并置于队列的头部以便优先处理
rt_err_t rt_mq_urgent(rt_mq_t mq, void* buffer, rt_size_t size);
// 描述:
// mq: 指向目标消息队列的句柄。
// buffer: 指向要发送的紧急消息内容的指针。
// size: 要发送的紧急消息内容的大小(字节数)。
// 返回值:成功时返回RT_EOK,如果消息队列已满则返回错误码。

// 接收消息
// 从指定的消息队列中接收消息
rt_err_t rt_mq_recv(rt_mq_t mq, void* buffer, rt_size_t size, rt_int32_t timeout);
// 描述:
// mq: 指向目标消息队列的句柄。
// buffer: 指向用于存放接收到的消息内容的缓冲区的指针。
// size: 缓冲区的大小(字节数)。这个大小应该足够大,以存放消息队列中最大的消息。
// timeout: 等待超时时间(毫秒)。如果设置为RT_WAITING_FOREVER,则无限期等待。
// 返回值:成功时返回RT_EOK,并将接收到的消息内容复制到buffer中。如果等待超时则返回错误码。

// 注意:
// 1. 在调用rt_mq_send和rt_mq_urgent时,如果size大于消息队列中每个消息的大小,则操作会失败。
// 2. 在调用rt_mq_recv时,buffer的大小应该能够存放消息队列中可能接收到的最大消息,否则可能会导致数据截断。
// 3. 对于rt_mq_send_wait和rt_mq_recv中的timeout参数,如果设置为0,则表示非阻塞模式,即立即返回结果,不等待。

11.2.4 消息队列应用示例

‌‌‌  初始化了 2 个静态线程,一个线程会从消息队列中收取消息;另一个线程会定时给消息队列发送普通消息和紧急消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <rtthread.h>  

/* 消息队列中用到的放置消息的内存池 */
static rt_uint8_t msg_pool[2048];
ALIGN(RT_ALIGN_SIZE)

/* 线程堆栈和线程控制块 */
static char thread1_stack[1024];
static struct rt_thread thread1;
static char thread2_stack[1024];
static struct rt_thread thread2;

/* 消息队列控制块 */
static struct rt_messagequeue mq;

/* 线程 1 入口函数 */
static void thread1_entry(void *parameter)
{
char buf = 0;
rt_uint8_t cnt = 0;
while (1)
{
/* 从消息队列中接收消息 */
if (rt_mq_recv(&mq, &buf, sizeof(buf), RT_WAITING_FOREVER) == RT_EOK)
{
rt_kprintf("thread 1: recv msg from msg queue, the content: %c\n", buf);
if (cnt == 19)
{
break;
}
}
/* 延时 50 ms */
cnt++;
rt_thread_mdelay(50);
}
rt_kprintf("thread 1: quit\n");
// 注意:RT-Thread中没有rt_mq_detach函数,如果需要释放资源,请使用rt_mq_delete(&mq);
// 但这里通常不在线程退出时删除消息队列,除非确实不再需要
}

/* 线程 2 入口函数 */
static void thread2_entry(void *parameter)
{
int result;
char buf = 'A';
rt_uint8_t cnt = 0;
while (1)
{
if (cnt == 8)
{
/* 发送紧急消息到消息队列中 */
result = rt_mq_urgent(&mq, &buf, 1);
if (result != RT_EOK)
{
rt_kprintf("rt_mq_urgent ERR\n");
}
else
{
rt_kprintf("thread 2: send urgent message - %c\n", buf);
}
}
else if (cnt >= 20) /* 发送 20 次消息之后退出 */
{
rt_kprintf("message queue stop send, thread 2 quit\n");
break;
}
else
{
/* 发送消息到消息队列中 */
result = rt_mq_send(&mq, &buf, 1);
if (result != RT_EOK)
{
rt_kprintf("rt_mq_send ERR\n");
}
rt_kprintf("thread 2: send message - %c\n", buf);
buf++; /* 递增buf的值,以便发送不同的字符 */
}
cnt++;
/* 延时 5 ms */
rt_thread_mdelay(5);
}
}

/* 消息队列示例的初始化 */
int msgq_sample(void)
{
rt_err_t result;

/* 初始化消息队列 */
/* 注意:这里修改了每个消息的大小和内存池大小参数,以符合您的需求 */
/* 每个消息大小为1,但内存池大小应足够大以存储多个消息 */
result = rt_mq_init(&mq, "mqt", &msg_pool[0], /* 内存池指向 msg_pool */
1, /* 每个消息的大小是 1 字节 */
sizeof(msg_pool) / 1, /* 内存池可以存储的消息数量 */
RT_IPC_FLAG_PRIO); /* 如果有多个线程等待,按优先级分配消息 */
if (result != RT_EOK)
{
rt_kprintf("init message queue failed.\n");
return -1; /* 使用-1或其他错误码表示失败 */
}

/* 初始化并启动线程 1 */
rt_thread_init(&thread1, "thread1", thread1_entry, RT_NULL,
&thread1_stack[0], sizeof(thread1_stack), 25, 5);
rt_thread_startup(&thread1);

/* 初始化并启动线程 2 */
rt_thread_init(&thread2, "thread2", thread2_entry, RT_NULL,
&thread2_stack[0], sizeof(thread2_stack), 25, 5);
rt_thread_startup(&thread2);

return 0; /* 表示成功 */
}

11.2.5 消息队列的使用场合

‌‌‌  应用于发送不定长消息的场合,包括线程与线程间的消息交换,以及中断服务例程中给线程发送消息(中断服务例程不能接收消息)
‌‌‌  发送消息
‌‌‌  消息队列和邮箱的明显不同是消息的长度并不限定在 4 个字节以内;另外,消息队列也包括了一个发送紧急消息的函数接口。但是当创建的是一个所有消息的最大长度是 4 字节的消息队列时,消息队列对象将蜕化成邮箱

1
2
3
4
5
‌‌‌  struct msg
‌‌‌  {
rt_uint 8_t *data_ptr; /* 数据块首地址 */
rt_uint 32_t data_size; /* 数据块大小 */
‌‌‌  };

‌‌‌  和邮箱例子相同的消息结构定义,假设依然需要发送这样一个消息给接收线程。在邮箱例子中,这个结构只能够发送指向这个结构的指针(在函数指针被发送过去后,接收线程能够正确的访问指向这个地址的内容,通常这块数据需要留给接收线程来释放)。而使用消息队列的方式则大不相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <rtthread.h>  

/* 定义消息结构体 */
struct msg {
void* data_ptr; /* 指向数据的指针 */
rt_size_t data_size; /* 数据的长度 */
};

/* 假设mq已经在某处被正确初始化了 */
extern rt_mq_t mq;

/* 发送消息到消息队列 */
void send_op(void* data, rt_size_t length)
{
struct msg msg_to_send;
msg_to_send.data_ptr = data; /* 指向相应的数据块地址 */
msg_to_send.data_size = length; /* 数据块的长度 */
/* 发送这个消息到mq消息队列 */
if (rt_mq_send(mq, &msg_to_send, sizeof(struct msg)) != RT_EOK) {
/* 发送失败处理 */
rt_kprintf("send message failed.\n");
}
}

/* 消息处理函数 */
void message_handler(void)
{
struct msg msg_received;
/* 从消息队列中接收消息到msg_received中 */
if (rt_mq_recv(mq, &msg_received, sizeof(struct msg), RT_WAITING_FOREVER) == RT_EOK) {
/* 成功接收到消息,进行相应的数据处理 */
// 注意:这里只是接收了消息结构体的副本,并没有接收数据本身
// 如果需要处理数据,需要通过msg_received.data_ptr和msg_received.data_size来访问
rt_kprintf("Received message with size: %d\n", msg_received.data_size);
// ... 处理数据(如果需要的话)
}
}

// 注意:在实际使用中,您可能需要在某个线程或中断服务例程中调用send_op函数
// 并在另一个线程中调用message_handler函数来处理接收到的消息

11.2.6 同步消息

‌‌‌  一般的系统设计中会经常遇到要发送同步消息的问题,这个时候就可以根据当时状态的不同选择相应的实现:两个线程间可以采用 [消息队列 + 信号量或邮箱] 的形式实现。发送线程通过消息发送的形式发送相应的消息给消息队列,发送完毕后希望获得接收线程的收到确认,工作示意图如下图所示:
‌‌‌  在这里插入图片描述

1
2
3
4
5
6
7
8
9
‌‌‌  根据消息确认的不同,可以把消息结构体定义成:
‌‌‌  struct msg
‌‌‌  {/* 消息结构其他成员 */struct rt_mailbox ack;
‌‌‌  };
‌‌‌  /* 或者 */
‌‌‌  struct msg
‌‌‌  {/* 消息结构其他成员 */struct rt_semaphore ack;
‌‌‌  };
‌‌‌  第一种类型的消息使用了邮箱来作为确认标志,而第二种类型的消息采用了信号量来作为确认标志。邮箱作为确认标志,代表着接收线程能够通知一些状态值给发送线程;而信号量作为确认标志只能够单一的通知发送线程,消息已经确认接收。

11.3 信号(软中断信号)

‌‌‌  软件层次上是对中断机制的一种模拟,在原理上,一个线程收到一个信号与处理器收到一个中断请求可以说是类似的。

11.3.1 信号的工作机制

信号在 RT-Thread 中用作异步通信,POSIX 标准定义了 sigset_t 类型来定义一个信号集,然而 sigset_t 类型在不同的系统可能有不同的定义方式,在 RT-Thread 中,将 sigset_t 定义成了 unsigned long 型,并命名为 rt_sigset_t,应用程序能够使用的信号为 SIGUSR 1(10)和 SIGUSR 2(12)

‌‌‌  信号本质是软中断,用来通知线程发生了异步事件,用做线程之间的异常通知、应急处理。一个线程不必通过任何操作来等待信号的到达,事实上,线程也不知道信号到底什么时候到达,线程之间可以互相通过调用 rt_thread_kill () 发送软中断信号。

收到信号的线程对各种信号有不同的处理方法,处理方法可以分为三类:
第一种是类似中断的处理程序,对于需要处理的信号,线程可以指定处理函数,由该函数来处理。
第二种方法是,忽略某个信号,对该信号不做任何处理,就像未发生过一样。
第三种方法是,对该信号的处理保留系统的默认值。

如下图所示,假设线程 1 需要对信号进行处理,首先线程 1
安装一个信号并解除阻塞,并在安装的同时设定了对信号的异常处理方式;然后其他线程可以给线程 1 发送信号,触发线程 1 对该信号的处理。
在这里插入图片描述
当信号被传递给线程 1 时,如果它正处于挂起状态,那会把状态改为就绪状态去处理对应的信号 (即唤醒)。如果它正处于运行状态,那么会在它当前的线程栈基础上建立新栈帧空间去处理对应的信号,需要注意的是使用的线程栈大小也会相应增加

11.3.2 信号的管理方式

‌‌‌  在这里插入图片描述

1
2
3
4
5
6
7
8
9
‌‌‌  //安装信号
‌‌‌  如果线程要处理某一信号,那么就要在线程中安装该信号。安装信号主要用来确定信号值及线程针对该信号值的动作之间的映射关系,
‌‌‌  即线程将要处理哪个信号,该信号被传递给线程时,将执行何种操作。
‌‌‌  rt_sighandler_t rt_signal_install (int signo, rt_sighandler_t[] handler);
‌‌‌  signo 信号值(只有 SIGUSR 1 和 SIGUSR 2 是开放给用户使用的,下同)
‌‌‌  handler 设置对信号值的处理方式, 决定了该信号的不同的处理方法。处理方法可以分为三种:
‌‌‌  1)类似中断的处理方式,参数指向当信号发生时用户自定义的处理函数,由该函数来处理。
‌‌‌  2)参数设为 SIG_IGN,忽略某个信号,对该信号不做任何处理,就像未发生过一样。
‌‌‌  3)参数设为 SIG_DFL,系统会调用默认的处理函数_signal_default_handler ()。
1
2
3
4
5
6
7
8
9
10
11
12
13
‌‌‌  //阻塞信号
‌‌‌  也可以理解为屏蔽信号。如果该信号被阻塞,则该信号将不会递达给安装此信号的线程,也不会引发软中断处理。
‌‌‌  void rt_signal_mask (int signo);
‌‌‌  //解除信号阻塞
‌‌‌  线程中可以安装好几个信号,使用此函数可以对其中一些信号给予 “关注”,那么发送这些信号都会引发该线程的软中断。
‌‌‌  void rt_signal_unmask (int signo);
‌‌‌  //发送信号
‌‌‌  需要进行异常处理时,可以给设定了处理异常的线程发送信号,调用 rt_thread_kill () 可以用来向任何线程发送信号
‌‌‌  int rt_thread_kill (rt_thread_t tid, int sig);
‌‌‌  //等待信号
‌‌‌  等待 set 信号的到来,如果没有等到这个信号,则将线程挂起,直到等到这个信号或者等待时间超过指定的超时时间 timeout。
‌‌‌  如果等到了该信号,则将指向该信号体的指针存入 si
‌‌‌  int rt_signal_wait (const rt_sigset_t *set, rt_siginfo_t[] *si, rt_int 32_t timeout);

11.3.3 信号应用示例

‌‌‌  创建了 1 个线程,在安装信号时,信号处理方式设为自定义处理,定义的信号的处理函数为 thread 1_signal_handler ()。待此线程运行起来安装好信号之后,给此线程发送信号。此线程将接收到信号,并打印信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <rtthread.h>  

#define THREAD_PRIORITY 25
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5

static rt_thread_t tid1 = RT_NULL; // 修正变量名中的空格

/* 线程 1 的信号处理函数 */
void thread1_signal_handler(int sig)
{
rt_kprintf("thread 1 received signal %d\n", sig);
}

/* 线程 1 的入口函数 */
static void thread1_entry(void *parameter)
{
int cnt = 0;
/* 假设存在 rt_signal_install 和 rt_signal_unmask 函数 */
// 注意:RT-Thread 标准 API 中并不包含这些函数,这里仅为示例
// 假设它们用于安装和解除信号阻塞
rt_signal_install(SIGUSR1, thread1_signal_handler); // 注意 SIGUSR1 宏,通常大写
rt_signal_unmask(SIGUSR1);

/* 运行 10 次 */
while (cnt < 10)
{
/* 线程 1 采用低优先级运行,一直打印计数值 */
rt_kprintf("thread 1 count : %d\n", cnt);
cnt++;
rt_thread_mdelay(100);
}
}

/* 信号示例的初始化 */
int signal_sample(void)
{
/* 创建线程 1 */
tid1 = rt_thread_create("thread 1", thread1_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
if (tid1 != RT_NULL)
rt_thread_startup(tid1);

rt_thread_mdelay(300);

/* 注意:RT-Thread 中没有 rt_thread_kill 发送信号的函数,这里仅为示例 */
// 假设有一个函数可以发送信号给线程
// rt_thread_send_signal(tid1, SIGUSR1); // 假设的函数

return 0;
}

12 内存管理

‌‌‌  RAM(随机存储器)
‌‌‌  ROM(只读存储器)

12.1 内存管理的功能特点

‌‌‌  实时系统中对时间的要求非常严格,内存管理往往要比通用操作系统要求苛刻得多:

1)分配内存的时间必须是确定的。一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面。而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。
2)随着内存不断被分配和释放,整个内存区域会产生越来越多的碎片(因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去),系统中还有足够的空闲内存,但因为它们地址并非连续,不能组成一块连续的完整内存块,会使得程序不能申请到大的内存。对于通用系统而言,这种不恰当的内存分配算法可以通过重新启动系统来解决 (每个月或者数个月进行一次),但是对于那些需要常年不间断地工作于野外的嵌入式系统来说,就变得让人无法接受了。
3)嵌入式系统的资源环境也是不尽相同,有些系统的资源比较紧张,只有数十 KB 的内存可供分配,而有些系统则存在数 MB 的内存,如何为这些不同的系统,选择适合它们的高效率的内存分配算法,就将变得复杂化

‌‌‌  RT-Thread 操作系统在内存管理上可分为两类:内存堆管理内存池管理
‌‌‌  内存堆管理又根据具体内存设备划分为三种情况:小内存管理算法,slab 大内存管理算法,memheap 多内存管理算法

12.2 内存堆管理

‌‌‌  RT-Thread 将 “ZI 段结尾处” 到内存尾部的空间用作内存堆
‌‌‌  在这里插入图片描述
‌‌‌  内存堆可以在当前资源满足的情况下,根据用户的需求分配任意大小的内存块。而当用户不需要再使用这些内存块时,又可以释放回堆中供其他应用分配使用。

小内存管理算法主要针对系统资源比较少,一般用于小于 2 MB 内存空间的系统;
slab 内存管理算法则主要是在系统资源比较丰富时,提供了一种近似多内存池管理算法的快速算法。
memheap 方法适用于系统存在多个内存堆的情况,它可以将多个内存 “粘贴” 在一起,形成一个大的内存堆
系统运行时只能选择其中之一或者完全不使用内存堆管理器,他们提供给应用程序的 API 接口完全相同。

‌‌‌  因为内存堆管理器要满足多线程情况下的安全分配,会考虑多线程间的互斥问题,所以请不要在中断服务例程中分配或释放动态内存块。因为它可能会引起当前上下文被挂起等待。

12.2.1 小内存管理算法

^a97576

‌‌‌  初始时,它是一块大的内存。当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来
‌‌‌  在这里插入图片描述

每个内存块(不管是已分配的内存块还是空闲的内存块)都包含一个数据头,其中包括:
1)magic:变数(或称为幻数),它会被初始化成 0x1ea0(即英文单词 heap),用于标记这个内存块是一个内存管理用的内存数据块;变数不仅仅用于标识这个数据块是一个内存管理用的内存数据块,实质也是一个内存保护字:如果这个区域被改写,那么也就意味着这块内存块被非法改写(正常情况下只有内存管理器才会去碰这块内存)。
2)used:指示出当前内存块是否已经分配。

内存管理的表现主要体现在内存的分配与释放上,小型内存管理算法可以用以下例子体现出来。
如下图所示的内存分配情况,空闲链表指针 lfree 初始指向 32 字节的内存块。当用户线程要再分配一个 64 字节的内存块时,但此 lfree 指针指向的内存块只有 32 字节并不能满足要求,内存管理器会继续寻找下一内存块,当找到再下一块内存块,128 字节时,它满足分配的要求。因为这个内存块比较大,分配器将把此内存块进行拆分,余下的内存块(52 字节)继续留在 lfree 链表中,如下图分配 64 字节后的链表结构所示。
在这里插入图片描述 在这里插入图片描述
每次分配内存块前,都会留出 12 字节数据头用于 magic、used 信息及链表节点使用。返回给应用的地址实际上是这块内存块 12 字节以后的地址,前面的 12 字节数据头是用户永远不应该碰的部分。(注:12 字节数据头长度会与系统对齐差异而有所不同)释放时则是相反的过程,但分配器会查看前后相邻的内存块是否空闲,如果空闲则合并成一个大的空闲内存块。

12.2.2 slab 管理算法(大内存)

‌‌‌  slab 分配器会根据对象的大小分成多个区(zone),也可以看成每类对象有一个内存池,如下图所示:

‌‌‌  在这里插入图片描述

一个 zone 的大小在 32 K 到 128 K 字节之间,分配器会在堆初始化时根据堆的大小自动调整。系统中的 zone 最多包括 72 种对象,一次最大能够分配 16 K 的内存空间,如果超出了 16 K 那么直接从页分配器中分配。每个 zone 上分配的内存块大小是固定的,能够分配相同大小内存块的 zone 会链接在一个链表中,而 72 种对象的 zone 链表则放在一个数组(zone_array[])中统一管理。 下面是内存分配器主要的两种操作:
(1)内存分配: 假设分配一个 32 字节的内存,slab 内存分配器会先按照 32 字节的值,从 zone array 链表表头数组中找到相应的 zone 链表。如果这个链表是空的,则向页分配器分配一个新的 zone,然后从 zone 中返回第一个空闲内存块。如果链表非空,则这个 zone 链表中的第一个 zone 节点必然有空闲块存在(否则它就不应该放在这个链表中),那么就取相应的空闲块。如果分配完成后,zone 中所有空闲内存块都使用完毕,那么分配器需要把这个 zone 节点从链表中删除。
(2)内存释放: 分配器需要找到内存块所在的 zone 节点,然后把内存块链接到 zone 的空闲内存块链表中。如果此时 zone 的空闲链表指示出 zone 的所有内存块都已经释放,即 zone 是完全空闲的,那么当 zone 链表中全空闲 zone 达到一定数目后,系统就会把这个全空闲的 zone 释放到页面分配器中去。

12.2.3 memheap 管理算法

‌‌‌  适用于系统含有多个地址可不连续的内存堆。使用 memheap 内存管理可以简化系统存在多个内存堆时的使用:当系统中存在多个内存堆的时候,用户只需要在系统初始化时将多个所需的 memheap 初始化,并开启 memheap 功能就可以很方便地把多个 memheap(地址可不连续)粘合起来用于系统的 heap 分配
‌‌‌  在开启 memheap 之后原来的 heap 功能将被关闭,两者只可以通过打开或关闭 RT_USING_MEMHEAP_AS_HEAP 来选择其一

memheap 工作机制如下图所示,首先将多块内存加入 memheap_item 链表进行粘合。当分配内存块时,会先从默认内存堆去分配内存,当分配不到时会查找 memheap_item 链表,尝试从其他的内存堆上分配内存块。应用程序不用关心当前分配的内存块位于哪个内存堆上,就像是在操作一个内存堆。
在这里插入图片描述

12.2.4 内存堆配置和初始化

1
2
3
4
5
6
‌‌‌  //使用内存堆时,必须要在系统初始化的时候进行堆的初始化,可以通过下面的函数接口完成:
‌‌‌  void rt_system_heap_init (void* begin_addr, void* end_addr);
‌‌‌  这个函数会把参数 begin_addr,end_addr 区域的内存空间作为内存堆来使用。
‌‌‌  //在使用 memheap 堆内存时,必须要在系统初始化的时候进行堆内存的初始化
‌‌‌  rt_err_t rt_memheap_init (struct rt_memheap *memheap, const char *name, void *start_addr, rt_uint 32_t size)
‌‌‌  如果有多个不连续的 memheap 可以多次调用该函数将其初始化并加入 memheap_item 链表。

12.2.5 内存堆的管理方式

‌‌‌  在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
‌‌‌  //从内存堆上分配用户指定大小的内存块
‌‌‌  void *rt_malloc (rt_size_t nbytes);
‌‌‌  返回分配的内存块地址
‌‌‌  //应用程序使用完从内存分配器中申请的内存后,必须及时释放,否则会造成内存泄漏
‌‌‌  void rt_free (void *ptr);//rt_free 函数会把待释放的内存还回给堆管理器中
‌‌‌  //重分配内存块
‌‌‌  void *rt_realloc (void *rmem, rt_size_t newsize);
‌‌‌  在已分配内存块的基础上重新分配内存块的大小(增加或缩小)
‌‌‌  在进行重新分配内存块时,原来的内存块数据保持不变(缩小的情况下,后面的数据被自动截断)。
‌‌‌  //分配多内存块
‌‌‌  从内存堆中分配连续内存地址的多个内存块
‌‌‌  void *rt_calloc (rt_size_t count, rt_size_t size);
‌‌‌  count 内存块数量
‌‌‌  size 内存块容量
‌‌‌  //设置内存钩子函数
‌‌‌  在分配内存块过程中,用户可设置一个钩子函数
‌‌‌  void rt_malloc_sethook (void (*hook)(void *ptr, rt_size_t size));
‌‌‌  hook 钩子函数指针
‌‌‌  设置的钩子函数会在内存分配完成后进行回调。回调时,会把分配到的内存块地址和大小做为入口参数传递进去。
‌‌‌  //在释放内存时,用户可设置一个钩子函数
‌‌‌  void rt_free_sethook (void (*hook)(void *ptr));
‌‌‌  设置的钩子函数会在调用内存释放完成前进行回调。回调时,释放的内存块地址会做为入口参数传递进去

12.2.6 内存堆管理应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <rtthread.h>  

#define THREAD_PRIORITY 25
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5

/* 线程入口函数 */
void thread_entry(void *parameter)
{
int i;
char *ptr = RT_NULL; /* 内存块的指针 */
for (i = 0; ; i++) {
/* 每次分配 (1 << i) 大小字节数的内存空间 */
ptr = rt_malloc(1 << i);
/* 如果分配成功 */
if (ptr != RT_NULL) {
rt_kprintf("get memory :%d byte\n", (1 << i));
/* 释放内存块 */
rt_free(ptr);
rt_kprintf("free memory :%d byte\n", (1 << i));
ptr = RT_NULL;
} else {
rt_kprintf("try to get %d byte memory failed!\n", (1 << i));
return;
}
}
}

/* 动态内存示例函数 */
int dynmem_sample(void)
{
rt_thread_t tid = RT_NULL;
/* 创建线程 */
tid = rt_thread_create("thread 1", thread_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
if (tid != RT_NULL)
rt_thread_startup(tid);
return 0;
}

12.3 内存池

‌‌‌  内存堆管理器可以分配任意大小的内存块,非常灵活和方便。但其也存在明显的缺点:一是分配效率不高,在每次分配时,都要空闲内存块查找;二是容易产生内存碎片。
‌‌‌  RT-Thread 提供了另外一种内存管理方法:内存池(Memory Pool)。
‌‌‌  内存池是一种内存分配方式,用于分配大量大小相同的小内存块,它可以极大地加快内存分配与释放的速度,且能尽量避免内存碎片化,RT-Thread 的内存池支持线程挂起功能,非常适合需要通过内存资源进行同步的场景

例如播放音乐时,播放器线程会对音乐文件进行解码,然后发送到声卡驱动,从而驱动硬件播放音乐。
在这里插入图片描述

如上图所示,当播放器线程需要解码数据时,就会向内存池请求内存块,如果内存块已经用完,线程将被挂起,否则它将获得内存块以放置解码的数据;
而后播放器线程把包含解码数据的内存块写入到声卡抽象设备中 (线程会立刻返回,继续解码出更多的数据);
当声卡设备写入完成后,将调用播放器线程设置的回调函数,释放写入的内存块,如果在此之前,播放器线程因为把内存池里的内存块都用完而被挂起的话,那么这时它将被将唤醒,并继续进行解码。

12.3.1 内存池控制块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 定义内存池控制块的结构体 */  
struct rt_mempool
{
struct rt_object parent; /* 继承自rt_object的父结构体 */
void *start_address; /* 内存池数据区域开始地址 */
rt_size_t size; /* 内存池数据区域大小 */
rt_size_t block_size; /* 内存块大小 */
rt_uint8_t *block_list; /* 内存块列表 */
rt_size_t block_total_count; /* 内存池数据区域中能够容纳的最大内存块数 */
rt_size_t block_free_count; /* 内存池中空闲的内存块数 */
rt_list_t suspend_thread; /* 因为内存块不可用而挂起的线程列表 */
rt_size_t suspend_thread_count; /* 因为内存块不可用而挂起的线程数 */
};

/* 定义内存块句柄的类型 */
typedef struct rt_mempool* rt_mp_t;

12.3.2 内存块分配机制

‌‌‌  内存池在创建时先向系统申请一大块内存,然后分成同样大小的多个小内存块,小内存块直接通过链表连接起来(此链表也称为空闲链表)。每次分配的时候,从空闲链表中取出链头上第一个内存块,提供给申请者。
‌‌‌  在这里插入图片描述
‌‌‌  物理内存中允许存在多个大小不同的内存池,每一个内存池又由多个空闲内存块组成,内核用它们来进行内存管理。当一个内存池对象被创建时,内存池对象就被分配给了一个内存池控制块。
‌‌‌  内存池一旦初始化完成,内部的内存块大小将不能再做调整。
‌‌‌  每一个内存池对象由上述结构组成,其中 suspend_thread 形成了一个申请线程等待列表,即当内存池中无可用内存块,并且申请线程允许等待时,申请线程将挂起在 suspend_thread 链表上。

12.3.3 内存池的管理方式

‌‌‌  在这里插入图片描述

1
2
3
4
5
6
7
‌‌‌  //创建内存池操作将会创建一个内存池对象并从堆上分配一个内存池。
‌‌‌  创建内存池后,线程便可以从内存池中执行申请、释放等操作。
‌‌‌  rt_mp_t rt_mp_create (const char* name, rt_size_t block_count, rt_size_t block_size);
‌‌‌  //删除内存池将删除内存池对象并释放申请的内存
‌‌‌  rt_err_t rt_mp_delete (rt_mp_t mp);
‌‌‌  删除内存池时,会首先唤醒等待在该内存池对象上的所有线程(返回 -RT_ERROR),
‌‌‌  然后再释放已从内存堆上分配的内存池数据存放区域,然后删除内存池对象。
1
2
3
4
5
6
‌‌‌  //初始化内存池用于静态内存管理模式,内存池控制块来源于用户在系统中申请的静态对象。
‌‌‌  与创建内存池不同的是,此处内存池对象所使用的内存空间是由用户指定的一个缓冲区空间,用户把缓冲区的指针传递给内存池控制块
‌‌‌  rt_err_t rt_mp_init (rt_mp_t mp, const char* name, void *start, rt_size_t size, rt_size_t block_size);
‌‌‌  内存池块个数 = size / (block_size + 4 链表指针大小),计算结果取整数
‌‌‌  //脱离内存池将把内存池对象从内核对象管理器中脱离
‌‌‌  rt_err_t rt_mp_detach (rt_mp_t mp);
1
2
3
4
5
‌‌‌  分配和释放内存块
‌‌‌  //从指定的内存池中分配一个内存块
‌‌‌  void *rt_mp_alloc (rt_mp_t mp, rt_int 32_t time);
‌‌‌  //释放内存块使用如下接口
‌‌‌  void rt_mp_free (void *block);

12.3.4 内存池应用示例

‌‌‌  静态内内存池的应用例程: 创建一个静态的内存池对象,2 个动态线程。一个线程会试图从内存池中获得内存块,另一个线程释放内存块内存块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <rtthread.h>  

#define THREAD_PRIORITY 25
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5

/* 注意:变量名不能包含空格和特殊字符,如tid 1应改为tid1 */
static rt_uint8_t *ptr[50];
static rt_uint8_t mempool[4096];
static struct rt_mempool mp;

/* 线程 1 入口 */
static void thread1_mp_alloc(void *parameter)
{
int i;
for (i = 0; i < 50; i++)
{
if (ptr[i] == RT_NULL)
{
/* 试图申请内存块 50 次,当申请不到内存块时,线程 1 挂起,转至线程 2 运行(如果线程 2 优先级更高或可运行)*/
ptr[i] = rt_mp_alloc(&mp, RT_WAITING_FOREVER);
if (ptr[i] != RT_NULL)
rt_kprintf("allocate No.%d\n", i);
}
}
}

/* 线程 2 入口 */
static void thread2_mp_release(void *parameter)
{
int i;
rt_kprintf("thread 2 try to release block\n");
for (i = 0; i < 50; i++)
{
/* 释放所有分配成功的内存块 */
if (ptr[i] != RT_NULL)
{
rt_kprintf("release block %d\n", i);
rt_mp_free(ptr[i]);
ptr[i] = RT_NULL;
}
}
}

int mempool_sample(void)
{
int i;
for (i = 0; i < 50; i++)
ptr[i] = RT_NULL;

/* 初始化内存池对象,假设每个内存块大小为80字节,不考虑额外开销 */
rt_mp_init(&mp, "mp_1", &mempool[0], sizeof(mempool), 80);

/* 创建线程 1:申请内存池 */
rt_thread_t tid1 = rt_thread_create("thread_1", thread1_mp_alloc, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
if (tid1 != RT_NULL)
rt_thread_startup(tid1);

/* 创建线程 2:释放内存池,注意调整优先级以确保线程2在适当时候运行 */
rt_thread_t tid2 = rt_thread_create("thread_2", thread2_mp_release, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY + 1, THREAD_TIMESLICE);
if (tid2 != RT_NULL)
rt_thread_startup(tid2);

return 0;
}

13 中断管理

中断

当 CPU 正在处理内部数据时,外界发生了紧急情况,要求 CPU 暂停当前的工作转去处理这个异步事件。处理完毕后,再回到原来被中断的地址,继续原来的工作
实现这一功能的系统称为 中断系统,申请 CPU 中断的请求源称为 中断源
中断示意图
中断处理与 CPU 架构密切相关

13.1 ARM Cortex-M 的 CPU 架构

13.1.1 寄存器简介

‌‌‌  Cortex-M 系列 CPU 的寄存器组里有 R0~R15 共 16 个通用寄存器组和若干特殊功能寄存器
‌‌‌  在这里插入图片描述

通用寄存器组里的 R13 作为堆栈指针寄存器 (Stack Pointer,SP);
R14 作为连接寄存器 (Link Register,LR),用于在调用子程序时,存储返回地址;
R15 作为程序计数器 (Program Counter,PC),其中堆栈指针寄存器可以是主堆栈指针(MSP),也可以是进程堆栈指针(PSP)。

1
2
3
4
5
6
‌‌‌  特殊功能寄存器包括程序状态字寄存器组(PSRs)、中断屏蔽寄存器组(PRIMASK, FAULTMASK, BASEPRI)
‌‌‌  、控制寄存器(CONTROL),可以通过 MSR/MRS 指令来访问特殊功能寄存器
‌‌‌  MRS R 0, CONTROL ; 读取 CONTROL 到 R 0 中
‌‌‌  MSR CONTROL, R 0 ; 写入 R 0 到 CONTROL 寄存器中程序状态字寄存器里保存算术与逻辑标志,例如负数标志,零结果标志,溢出标志等等。
‌‌‌  中断屏蔽寄存器组控制 Cortex-M 的中断除能。控制寄存器用来定义特权级别和当前使用哪个堆栈指针。如果是具有浮点单元的 Cortex-M 4 或者 Cortex-M 7,控制寄存器也用来指示浮点单元当前是否在使用,
‌‌‌  浮点单元包含了 32 个浮点通用寄存器 S 0~S 31 和特殊 FPSCR 寄存器(Floating point status and control register)。

13.1.2 操作模式和特权级别

‌‌‌  在这里插入图片描述
‌‌‌  Cortex-M 引入了操作模式和特权级别的概念,分别为线程模式和处理模式,如果进入异常或中断处理则进入处理模式,其他情况则为线程模式。
‌‌‌  Cortex-M 有两个运行级别,分别为特权级和用户级,线程模式可以工作在特权级或者用户级,而处理模式总工作在特权级,可通过 CONTROL 特殊寄存器控制。

13.1.3 嵌套向量中断控制器

‌‌‌  NVIC(嵌套向量中断控制器),支持中断嵌套功能。当一个中断触发并且系统进行响应时,处理器硬件会将当前运行位置的上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器。
‌‌‌  在这里插入图片描述
‌‌‌  当系统正在服务一个中断时,如果有一个更高优先级的中断触发,那么处理器同样会打断当前运行的中断服务程序,然后把这个中断服务程序上下文的 PSR、PC、LR、R12、R3-R0 寄存器自动保存到中断栈中。

13.1.4 PendSV 系统调用

‌‌‌  PendSV 也称为可悬起的系统调用,它是一种异常,可以像普通的中断一样被挂起,它是专门用来辅助操作系统进行上下文切换的。PendSV 异常会被初始化为最低优先级的异常。每次需要进行上下文切换的时候,会手动触发 PendSV 异常,在 PendSV 异常处理函数中进行上下文切换。

13.2 RT-Thread 中断工作机制

13.2.1 中断向量表

‌‌‌  中断向量表是所有中断处理程序的入口,如下图所示是 Cortex-M 系列的中断处理过程:把一个函数(用户中断服务程序)同一个虚拟中断向量表中的中断向量联系在一起。当中断向量对应中断发生的时候,被连接的用户中断服务程序就会被调用执行
‌‌‌  在这里插入图片描述
‌‌‌  在 Cortex-M 内核上,所有中断都采用中断向量表的方式进行处理,即当一个中断触发时,处理器将直接判定是哪个中断源,然后直接跳转到相应的固定位置进行处理,每个中断服务程序必须排列在一起放在统一的地址上。每一条中断服务程序的地址按顺序分配到 Flash 代码区上(从 0x0800 0000 开始)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
__Vectors  
DCD __initial_sp ; 栈顶指针
DCD Reset_Handler ; 复位处理函数
DCD NMI_Handler ; NMI(非屏蔽中断)处理函数
DCD HardFault_Handler ; 硬故障处理函数
DCD MemManage_Handler ; 内存管理故障处理函数
DCD BusFault_Handler ; 总线故障处理函数
DCD UsageFault_Handler ; 使用故障处理函数
DCD 0 ; 保留(SVCall未使用)
DCD 0 ; 保留
DCD 0 ; 保留
DCD 0 ; 保留
DCD SVC_Handler ; SVC(系统服务调用)处理函数
DCD DebugMon_Handler ; 调试监视器处理函数
DCD 0 ; 保留
DCD PendSV_Handler ; 可挂起的系统服务调用处理函数
DCD SysTick_Handler ; SysTick定时器处理函数
; ... 其他中断处理函数 ...

; NMI_Handler 弱定义
NMI_Handler
PROC
EXPORT NMI_Handler [WEAK]
B . ; 无限循环或跳转到某处
ENDP

; HardFault_Handler 弱定义
HardFault_Handler
PROC
EXPORT HardFault_Handler [WEAK]
B . ; 无限循环或跳转到某处(通常是调试代码)
ENDP

; ... 其他中断处理函数的弱定义 ...

[WEAK] 标识,它是符号弱化标识,在 [WEAK] 前面的符号 (如 NMI_Handler、HardFault_Handler)将被执行弱化处理,如果整个代码在链接时遇到了名称相同的符号(例如与 NMI_Handler 相同名称的函数),那么代码将使用未被弱化定义的符号(与 NMI_Handler 相同名称的函数),而与弱化符号相关的代码将被自动丢弃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**  
* SysTick 中断处理函数
* 该函数在系统定时器中断时被调用,用于处理时间相关的任务
*/
void SysTick_Handler(void)
{
/* 进入中断处理 */
rt_interrupt_enter();

/* 增加系统时钟节拍 */
rt_tick_increase();

/* 离开中断处理 */
rt_interrupt_leave();
}

13.2.2 中断处理过程

‌‌‌  RT-Thread 中断管理中,将中断处理程序分为中断前导程序、用户中断服务程序、中断后续程序三部分
‌‌‌  在这里插入图片描述
‌‌‌  中断前导程序
‌‌‌  1)保存 CPU 中断现场,这部分跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。
‌‌‌  对于 Cortex-M 来说,该工作由硬件自动完成。当一个中断触发并且系统进行响应时,处理器硬件会将当前运行部分的上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器。
‌‌‌  2)通知内核进入中断状态,调用 rt_interrupt_enter () 函数,作用是把全局变量 rt_interrupt_nest 加 1,用它来记录中断嵌套的层数,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**  
* 进入中断处理函数
* 禁用中断,增加中断嵌套计数,然后恢复之前的中断状态
*/
void rt_interrupt_enter(void)
{
rt_base_t level; // 用于保存当前的中断状态

// 禁用中断,并保存当前的中断状态
level = rt_hw_interrupt_disable();

// 增加中断嵌套计数
rt_interrupt_nest++;

// 恢复之前的中断状态
rt_hw_interrupt_enable(level);
}

‌‌‌  用户中断服务程序
‌‌‌  分为两种情况,第一种情况是不进行线程切换,这种情况下用户中断服务程序和中断后续程序运行完毕后退出中断模式,返回被中断的线程。
‌‌‌  另一种情况是,在中断处理过程中需要进行线程切换,这种情况会调用 rt_hw_context_switch_interrupt () 函数进行上下文切换,该函数跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。
‌‌‌  Cortex-M 架构中,rt_hw_context_switch_interrupt () 的函数实现流程如下图所示
‌‌‌  在这里插入图片描述
‌‌‌  中断后续程序
‌‌‌  1 通知内核离开中断状态,通过调用 rt_interrupt_leave () 函数,将全局变量 rt_interrupt_nest 减 1
‌‌‌  2 恢复中断前的 CPU 上下文,如果在中断处理过程中未进行线程切换,那么恢复 from 线程的 CPU 上下文,如果在中断中进行了线程切换,那么恢复 to 线程的 CPU 上下文。
‌‌‌  在这里插入图片描述

13.2.3 中断嵌套

‌‌‌  在允许中断嵌套的情况下,在执行中断服务程序的过程中,如果出现高优先级的中断,当前中断服务程序的执行将被打断,以执行高优先级中断的中断服务程序,当高优先级中断的处理完成后,被打断的中断服务程序才又得到继续执行,如果需要进行线程调度,线程的上下文切换将在所有中断处理程序都运行结束时才发生
‌‌‌  在这里插入图片描述

13.2.4 中断栈

‌‌‌  在中断处理过程中,在系统响应中断前,软件代码(或处理器)需要把当前线程的上下文保存下来(通常保存在当前线程的线程栈中),再调用中断服务程序进行中断响应、处理。在进行中断处理时(实质是调用用户的中断服务程序函数),中断处理函数中很可能会有自己的局部变量,这些都需要相应的栈空间来保存,所以中断响应依然需要一个栈空间来做为上下文,运行中断处理函数。中断栈可以保存在打断线程的栈中,当从中断中退出时,返回相应的线程继续执行。
‌‌‌  中断栈也可以与线程栈完全分离开来,即每次进入中断时,在保存完打断线程上下文后,切换到新的中断栈中独立运行。在中断退出时,再做相应的上下文恢复。使用独立中断栈相对来说更容易实现,并且对于线程栈使用情况也比较容易了解和掌握(否则必须要为中断栈预留空间,如果系统支持中断嵌套,还需要考虑应该为嵌套中断预留多大的空间)。
‌‌‌  RT-Thread 采用的方式是提供独立的中断栈,即中断发生时,中断的前期处理程序会将用户的栈指针更换到系统事先留出的中断栈空间中,等中断退出时再恢复用户的栈指针。这样中断就不会占用线程的栈空间,从而提高了内存空间的利用率,且随着线程的增加,这种减少内存占用的效果也越明显。

13.2.5 中断的底半处理

‌‌‌  RT-Thread 不对中断服务程序所需要的处理时间做任何假设、限制,但如同其他实时操作系统或非实时操作系统一样,用户需要保证所有的中断服务程序在尽可能短的时间内完成(中断服务程序在系统中相当于拥有最高的优先级,会抢占所有线程优先执行)。这样在发生中断嵌套,或屏蔽了相应中断源的过程中,不会耽误嵌套的其他中断处理过程,或自身中断源的下一次中断信号。
‌‌‌  当一个中断发生时,中断服务程序需要取得相应的硬件状态或者数据。如果中断服务程序接下来要对状态或者数据进行简单处理,比如 CPU 时钟中断,中断服务程序只需对一个系统时钟变量进行加一操作,然后就结束中断服务程序。这类中断需要的运行时间往往都比较短。但对于另外一些中断,中断服务程序在取得硬件状态或数据以后,还需要进行一系列更耗时的处理过程,通常需要将该中断分割为两部分,即上半部分(Top Half)和底半部分(Bottom Half)。在上半部分中,取得硬件状态和数据后,打开被屏蔽的中断,给相关线程发送一条通知(可以是 RT-Thread 所提供的信号量、事件、邮箱或消息队列等方式),然后结束中断服务程序;而接下来,相关的线程在接收到通知后,接着对状态或数据进行进一步的处理,这一过程称之为底半处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <rtthread.h>  

/* 假设的全局变量 */
rt_sem_t nw_bh_sem = RT_NULL;
uint8_t* packet_buffer = RT_NULL; /* 假设的接收缓冲区指针,实际使用中需要适当初始化 */

/* Bottom Half 处理函数 */
void nw_packet_parser(uint8_t* buffer) {
// 实际的报文解析逻辑
}

void nw_packet_process(uint8_t* buffer) {
// 实际的报文处理逻辑
}

/* 数据读取、分析的线程 */
void demo_nw_thread(void *param) {
/* 首先对设备进行必要的初始化工作 */
device_init_setting();
/* 创建一个 semaphore 来响应 Bottom Half 的事件 */
nw_bh_sem = rt_sem_create("bh_sem", 0, RT_IPC_FLAG_PRIO);
if (nw_bh_sem == RT_NULL) {
/* 处理创建信号量失败的情况 */
return;
}

while (1) {
/* 等待在 nw_bh_sem 上,直到接收到信号 */
rt_sem_take(nw_bh_sem, RT_WAITING_FOREVER);
/* 接收到 semaphore 信号后,开始 Bottom Half 处理过程 */
nw_packet_parser(packet_buffer);
nw_packet_process(packet_buffer);
/* 假设处理完成后需要重置或更新packet_buffer */
// packet_buffer = ...;
}
}

/* 中断服务例程(ISR) */
void demo_nw_isr(int vector, void *param) {
/* 当 network 设备接收到数据后,陷入中断异常 */
/* 开始 Top Half 部分的处理 */
nw_device_status_read(); /* 读取硬件设备的状态以判断发生了何种中断 */
/* ... 其他一些数据操作等,例如更新packet_buffer */
// 假设在这里已经更新了packet_buffer指向了新接收到的数据包

/* 释放 nw_bh_sem,发送信号给 demo_nw_thread,准备开始 Bottom Half */
rt_sem_release(nw_bh_sem);
/* 然后退出中断的 Top Half 部分,结束 ISR */
}

int main(void) {
rt_thread_t thread;
/* 创建处理线程 */
thread = rt_thread_create("nwt", demo_nw_thread, RT_NULL, 1024, 20, 5);
if (thread != RT_NULL) {
rt_thread_startup(thread);
}

/* 假设这里还有其他初始化代码和主循环等 */
return 0;
}

‌‌‌  中断服务程序通过对一个信号量对象的等待和释放,来完成中断 Bottom Half 的起始和终结。由于将中断处理划分为 Top 和 Bottom 两个部分后,使得中断处理过程变为异步过程。这部分系统开销需要用户在使用 RT-Thread 时,必须认真考虑中断服务的处理时间是否大于给 Bottom Half 发送通知并处理的时间

13.2.6 RT-Thread 中断管理接口

‌‌‌  为了把操作系统和系统底层的异常、中断硬件隔离开来,RT-Thread 把中断和异常封装为一组抽象接口
‌‌‌  在这里插入图片描述
‌‌‌
中断服务程序挂接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
‌‌‌/**  
* 挂载一个新的中断服务程序
*
* @param vector 中断号,指定了要挂载中断服务程序的中断源。
* @param handler 新的中断服务程序,当中断发生时,系统将自动调用此函数。
* @param param 参数,此参数将被传递给中断服务程序,允许在中断服务程序中访问特定的上下文或数据。
* @param name 中断的名称,主要用于调试和日志记录,帮助开发者识别中断源。
*
* @return 返回之前挂载的中断服务程序(如果有的话),如果没有则返回RT_NULL。
* 注意:这个返回值可以用来恢复之前的中断服务程序。
*
* 注意:
* 1. 这个API并不保证在所有RTOS的移植分支中都存在,特别是像Cortex-M 0/M 3/M 4这样的常见架构的移植分支中可能没有这个API。
* 2. 在调用此API之前,请确保目标中断源是有效的,并且没有被其他中断服务程序占用(除非你的目的是替换它)。
* 3. 在中断服务程序中,应尽量减少执行时间,避免执行复杂的或耗时的操作,以免阻塞其他中断或系统任务。
* 4. 参数param提供了一种机制,允许中断服务程序访问特定的上下文或数据,但请确保在使用前已正确初始化。
* 5. 中断名称(name)对于调试和日志记录非常有用,应尽可能描述性地命名中断。
*/
rt_isr_handler_t rt_hw_interrupt_install(int vector, rt_isr_handler_t handler, void *param, char *name);

‌‌‌  中断服务程序是一种需要特别注意的运行环境,它运行在非线程的执行环境下(一般为芯片的一种特殊运行模式(特权模式)),在这个运行环境中不能使用挂起当前线程的操作,因为当前线程并不存在

‌‌‌  中断源管理
‌‌‌  通常在 ISR 准备处理某个中断信号之前,我们需要先屏蔽该中断源,在 ISR 处理完状态或数据以后,及时的打开之前被屏蔽的中断源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 假设有一个函数用于配置NVIC寄存器来屏蔽特定中断  
// 注意:这不是RT-Thread的API,而是基于Cortex-M的硬件操作
void rt_hw_interrupt_mask(int irqn) {
// 获取NVIC中断使能寄存器组的地址(这里假设是NVIC_ISERx,但通常是NVIC_ICERx用于禁用中断)
// 注意:irqn需要转换为对应的寄存器索引和位位置
// 这里只是示意,实际实现会根据irqn和NVIC的寄存器布局有所不同
// NVIC_ICERx = (1 << (irqn & 0x1F)); // 假设irqn的低5位对应中断号,使用ICERx来禁用中断
// 但由于API名称是mask,我们实际上应该查找如何正确禁用中断的寄存器

// 注意:下面的代码是示意性的,并非直接可用
// 在实际实现中,需要查阅具体的硬件参考手册来确定正确的寄存器操作
uint32_t irq_bit = 1UL << (irqn & NVIC_IRQ_NUMBER_MASK);
NVIC_DisableIRQ(irqn); // 假设有一个这样的函数封装了NVIC_ICERx的操作
// 或者,如果直接操作寄存器:
// *((volatile uint32_t *)((NVIC_ICER0_BASE + ((irqn >> 5) * 4)) + NVIC_ICER_ICERx_OFFSET)) = irq_bit;
}

// 同样地,对于使能中断的函数
void rt_hw_interrupt_umask(int irqn) {
// 类似地,但这次是启用中断
// NVIC_ISERx = (1 << (irqn & 0x19)); // 假设irqn的低5位对应中断号,使用ISERx来启用中断
// 但实际上,我们应该使用标准的NVIC函数
NVIC_EnableIRQ(irqn); // 假设有一个这样的函数封装了NVIC_ISERx的操作
// 或者,如果直接操作寄存器:
// *((volatile uint32_t *)((NVIC_ISER0_BASE + ((irqn >> 5) * 4)) + NVIC_ISER_ISERx_OFFSET)) = irq_bit;
}

// 注意:上面的NVIC_ISER_ISERx_OFFSET和NVIC_ICER_ICERx_OFFSET是示意性的,
// 实际中并不存在这样的宏定义。你需要根据NVIC的寄存器布局来计算偏移量。
// 而且,NVIC_ISERx和NVIC_ICERx等寄存器的地址(NVIC_ISER0_BASE等)也需要根据具体的硬件手册来确定。

‌‌‌  全局中断开关
‌‌‌  也称为中断锁,是禁止多线程访问临界区最简单的一种方式,即通过关闭中断的方式,来保证当前线程不会被其他事件打断

1
2
3
4
5
6
7
8
9
10
// 当需要关闭整个系统的中断时,可调用下面的函数接口。  
// 此函数会返回调用前的中断状态(使能或禁用),以便后续可以恢复。
rt_base_t rt_hw_interrupt_disable(void);


// 恢复中断也称开中断。rt_hw_interrupt_enable()这个函数用于“使能”中断,它恢复了调用rt_hw_interrupt_disable()函数前的中断状态。
// 如果调用rt_hw_interrupt_disable()函数前是关中断状态,并且没有其他地方修改了中断状态,
// 那么调用此函数后,系统应该保持关中断状态(但这通常不是预期的行为,因为函数名暗示了恢复之前的状态)。
// 然而,实际上,这个函数应该根据传入的level参数来恢复中断状态。
void rt_hw_interrupt_enable(rt_base_t level);

‌‌‌  使用中断锁来操作临界区的方法可以应用于任何场合,且其他几类同步方式都是依赖于中断锁而实现的,可以说中断锁是最强大的和最高效的同步方法。只是使用中断锁最主要的问题在于,在中断关闭期间系统将不再响应任何中断,也就不能响应外部的事件。所以中断锁对系统的实时性影响非常巨大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* 中断锁 */
// 关闭中断
rt_base_t level = rt_hw_interrupt_disable();

// 执行互斥操作
a = a + value;

// 恢复中断
rt_hw_interrupt_enable(level);


/* 信号量 */
// 假设sem_lock已经是一个初始化好的信号量

// 获得信号量锁
rt_err_t result = rt_sem_take(sem_lock, RT_WAITING_FOREVER);
if (result == RT_EOK) {
// 执行互斥操作
a = a + value;

// 释放信号量锁
rt_sem_release(sem_lock);
} else {
// 处理获取信号量失败的情况
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <rthw.h> // 假设这是RT-Thread的头文件,实际路径可能不同  

void global_interrupt_demo(void)
{
rt_base_t level0; // 声明并用于存储第一次调用rt_hw_interrupt_disable()之前的中断状态
rt_base_t level1; // 声明并用于存储第二次调用rt_hw_interrupt_disable()之前的中断状态

/* 第一次关闭全局中断,并保存关闭之前的中断状态 */
level0 = rt_hw_interrupt_disable();

/* 第二次关闭全局中断(此时已经关闭),但再次调用以模拟嵌套情况,并保存状态 */
level1 = rt_hw_interrupt_disable();

/* 执行一些操作,此时全局中断是关闭的 */
do_something();

/* 恢复全局中断到第二次关闭之前的状态(实际上,由于已经是关闭状态,所以这里不会有变化) */
rt_hw_interrupt_enable(level1);

/* 执行更多操作,此时全局中断状态取决于level1(但在这个例子中,它仍然是关闭的) */
// do_something_else();

/* 恢复全局中断到第一次关闭之前的状态 */
rt_hw_interrupt_enable(level0);

/* 从这里开始,全局中断状态将恢复到调用global_interrupt_demo()之前的状态 */
}

void do_something(void)
{
/* 这里是执行一些需要关闭中断保护的代码 */
}

‌‌‌  中断通知
‌‌‌  当整个系统被中断打断,进入中断处理函数时,需要通知内核当前已经进入到中断状态。针对这种情况,可通过以下接口:

1
2
‌‌‌  void rt_interrupt_enter (void);
‌‌‌  void rt_interrupt_leave (void);

这两个接口分别用在中断前导程序和中断后续程序中,均会对 rt_interrupt_nest(中断嵌套深度)的值进行修改:
每当进入中断时,可以调用 rt_interrupt_enter () 函数,用于通知内核,当前已经进入了中断状态,并增加中断嵌套深度(执行 rt_interrupt_nest++);
每当退出中断时,可以调用 rt_interrupt_leave () 函数,用于通知内核,当前已经离开了中断状态,并减少中断嵌套深度(执行 rt_interrupt_nest –)。注意不要在应用程序中调用这两个接口函数。

‌‌‌  使用 rt_interrupt_enter/leave () 的作用是,在中断服务程序中,如果调用了内核相关的函数(如释放信号量等操作),则可以通过判断当前中断状态,让内核及时调整相应的行为。例如:在中断中释放了一个信号量,唤醒了某线程,但通过判断发现当前系统处于中断上下文环境中,那么在进行线程切换时应该采取中断中线程切换的策略,而不是立即进行切换。
‌‌‌‌‌‌  如果中断服务程序不会调用内核相关的函数(释放信号量等操作),这个时候,也可以不调用 rt_interrupt_enter/leave () 函数

1
2
3
4
5
6
7
8
9
10
11
12
/**  
* 获取当前的中断嵌套深度。
*
* 在内核需要知道当前是否已经进入到中断状态或当前的中断嵌套深度时,
* 可以调用此接口。
*
* @return 返回值表示当前的中断嵌套深度:
* - 0: 当前系统不处于中断上下文环境中
* - 1: 当前系统处于中断上下文环境中(但没有嵌套中断)
* - 大于1: 当前存在中断嵌套,具体数值表示嵌套层次
*/
rt_uint8_t rt_interrupt_get_nest(void);

13.3 中断与轮询

‌‌‌  当驱动外设工作时,其编程模式到底采用中断模式触发还是轮询模式触发往往是驱动开发人员首先要考虑的问题,并且这个问题在实时操作系统与分时操作系统中差异还非常大。因为轮询模式本身采用顺序执行的方式:查询到相应的事件然后进行对应的处理。所以轮询模式从实现上来说,相对简单清晰。例如往串口中写入数据,仅当串口控制器写完一个数据时,程序代码才写入下一个数据(否则这个数据丢弃掉)。

1
2
3
4
5
6
7
8
9
10
11
‌‌/* 轮询模式向串口写入数据 */  
while (size) {
/* 判断 UART 外设中数据是否发送完毕 */
while (!(uart->uart_device->SR & USART_FLAG_TXE));

/* 当所有数据发送完毕后,才发送下一个数据 */
uart->uart_device->DR = (*ptr & 0x1FF); // 修正了十六进制数的写法

++ptr; // 移动到下一个要发送的数据
--size; // 减少剩余要发送的数据量
}

‌‌‌  在实时系统中轮询模式可能会出现非常大问题,因为在实时操作系统中,当一个程序持续地执行时(轮询时),它所在的线程会一直运行,比它优先级低的线程都不会得到运行。而分时系统中,这点恰恰相反,几乎没有优先级之分,可以在一个时间片运行这个程序,然后在另外一段时间片上运行另外一段程序。
‌‌‌  通常情况下,实时系统中更多采用的是中断模式来驱动外设。当数据达到时,由中断唤醒相关的处理线程,再继续进行后续的动作。

例如一些携带 FIFO(包含一定数据量的先进先出队列)的串口外设,其写入过程可以是这样的(核心是异步处理,写 FIFO 快速动作完成后线程挂起,CPU 执行其他线程,UART 发送慢速动作由控制器外设完成,将二者异步,可使 CPU 不再等待低速设备)如下图所示:

在这里插入图片描述

线程先向串口的 FIFO 中写入数据,当 FIFO 满时,线程主动挂起。串口控制器持续地从 FIFO 中取出数据并以配置的波特率(例如 115200 bps)发送出去。当 FIFO 中所有数据都发送完成时,将向处理器触发一个中断;当中断服务程序得到执行时,可以唤醒这个线程。这里举例的是 FIFO 类型的设备,在现实中也有 DMA 类型的设备,原理类似。

‌‌‌  对于低速设备来说,运用这种模式非常好,因为在串口外设把 FIFO 中的数据发送出去前,处理器可以运行其他的线程,这样就提高了系统的整体运行效率(甚至对于分时系统来说,这样的设计也是非常必要)。
‌‌‌  但是对于一些高速设备,例如传输速度达到 10 Mbps 的时候,假设一次发送的数据量是 32 字节,我们可以计算出发送这样一段数据量需要的时间是:(32 * 8) * 1/10 Mbps = 25 us。当数据需要持续传输时,系统将在 25 us 后触发一个中断以唤醒上层线程继续下次传递。假设系统的线程切换时间是 8 us(通常实时操作系统的线程上下文切换时间只有几个 us),那么当整个系统运行时,对于数据带宽利用率将只有 25/(25+8) =75.8%。但是采用轮询模式,数据带宽的利用率则可能达到 100%。这个也是大家普遍认为实时系统中数据吞吐量不足的缘故,系统开销消耗在了线程切换上(有些实时系统甚至会如本章前面说的,采用底半处理,分级的中断处理方式,相当于又拉长中断到发送线程的时间开销,效率会更进一步下降)。
‌‌‌  当一个实时系统想要提升数据吞吐量时,可以考虑的几种方式:
‌‌‌  1)增加每次数据量发送的长度,每次尽量让外设尽量多地发送数据(多量少次);
‌‌‌  2)必要情况下更改中断模式为轮询模式。同时为了解决轮询方式一直抢占处理机,其他低优先级线程得不到运行的情况,可以把轮询线程的优先级适当降低。

13.4 全局中断开关使用示例

‌‌‌  在多线程访问同一个变量时,使用开关全局中断对该变量进行保护,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <rthw.h>  
#include <rtthread.h>

#define THREAD_PRIORITY 20
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5

/* 同时访问的全局变量 */
static rt_uint32_t cnt = 0; // 初始化全局变量

/* 线程入口函数 */
void thread_entry(void *parameter)
{
rt_uint32_t no = (rt_uint32_t)parameter;
rt_uint32_t level;

while (1)
{
/* 关闭全局中断 */
level = rt_hw_interrupt_disable();
cnt += no;
/* 恢复全局中断 */
rt_hw_interrupt_enable(level);

rt_kprintf("protect thread[%d]'s counter is %d\n", no, cnt);
rt_thread_mdelay(no * 10);
}
}

/* 用户应用程序入口 */
int interrupt_sample(void)
{
rt_thread_t thread;

/* 创建 t1 线程 */
thread = rt_thread_create("thread 1", thread_entry, (void *)10, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
if (thread != RT_NULL)
rt_thread_startup(thread);

/* 创建 t2 线程 */
thread = rt_thread_create("thread 2", thread_entry, (void *)20, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
if (thread != RT_NULL)
rt_thread_startup(thread);

return 0;
}

‌‌‌  注:由于关闭全局中断会导致整个系统不能响应中断,所以在使用关闭全局中断做为互斥访问临界区的手段时,必须需要保证关闭全局中断的时间非常短,例如运行数条机器指令的时间。

#嵌入式
#操作系统
#rtthread