RT-Thread

RT-Thread 目录

[TOC]

内核简介

自动初始化机制

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

内核对象管理架构

派生和继承

RT-Thread 的内核对象容器及链表

03kernel_object2

内核rtconfig.h配置

(1)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
/* 表示内核对象的名称的最大长度,若代码中对象名称的最大长度大于宏定义的长度,
* 多余的部分将被截掉。*/
#define RT_NAME_MAX 8

/* 字节对齐时设定对齐的字节个数。常使用 ALIGN(RT_ALIGN_SIZE) 进行字节对齐。*/
#define RT_ALIGN_SIZE 4

/* 定义系统线程优先级数;通常用 RT_THREAD_PRIORITY_MAX-1 定义空闲线程的优先级 */
#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

(2)线程间同步与通信部分,该部分会使用到的对象有信号量、互斥量、事件、邮箱、消息队列、信号等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 定义该宏可开启信号量的使用,未定义则关闭 */
#define RT_USING_SEMAPHORE

/* 定义该宏可开启互斥量的使用,未定义则关闭 */
#define RT_USING_MUTEX

/* 定义该宏可开启事件集的使用,未定义则关闭 */
#define RT_USING_EVENT

/* 定义该宏可开启邮箱的使用,未定义则关闭 */
#define RT_USING_MAILBOX

/* 定义该宏可开启消息队列的使用,未定义则关闭 */
#define RT_USING_MESSAGEQUEUE

/* 定义该宏可开启信号的使用,未定义则关闭 */
#define RT_USING_SIGNALS

(3)内存管理部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 开启静态内存池的使用 */
#define RT_USING_MEMPOOL

/* 定义该宏可开启两个或以上内存堆拼接的使用,未定义则关闭 */
#define RT_USING_MEMHEAP

/* 开启小内存管理算法 */
#define RT_USING_SMALL_MEM

/* 关闭 SLAB 内存管理算法 */
/* #define RT_USING_SLAB */

/* 开启堆的使用 */
#define RT_USING_HEAP

(4)内核设备对象

1
2
3
4
5
6
7
8
9
/* 表示开启了系统设备的使用 */
#define RT_USING_DEVICE

/* 定义该宏可开启系统控制台设备的使用,未定义则关闭 */
#define RT_USING_CONSOLE
/* 定义控制台设备的缓冲区大小 */
#define RT_CONSOLEBUF_SIZE 128
/* 控制台设备的名称 */
#define RT_CONSOLE_DEVICE_NAME "uart1"

(5)自动初始化方式

1
2
3
4
5
6
7
/* 定义该宏开启自动初始化机制,未定义则关闭 */
#define RT_USING_COMPONENTS_INIT

/* 定义该宏开启设置应用入口为 main 函数 */
#define RT_USING_USER_MAIN
/* 定义 main 线程的栈大小 */
#define RT_MAIN_THREAD_STACK_SIZE 2048

(6)FinSH

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
/* 定义该宏可开启系统 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)关于 MCU

1
2
3
4
5
6
7
8
/* 定义该工程使用的 MCU 为 STM32F103ZE;系统通过对芯片类型的定义,来定义芯片的管脚 */
#define STM32F103ZE

/* 定义时钟源频率 */
#define RT_HSE_VALUE 8000000

/* 定义该宏开启 UART1 的使用 */
#define RT_USING_UART1

常见宏定义说明

1)rt_inline,定义如下,static 关键字的作用是令函数只能在当前的文件中使用;inline 表示内联,用 static 修饰后在调用函数时会建议编译器进行内联展开。

1
#define rt_inline                   static __inline

2)RT_USED,定义如下,该宏的作用是向编译器说明这段代码有用,即使函数中没有调用也要保留编译。例如 RT-Thread 自动初始化功能使用了自定义的段,使用 RT_USED 会将自定义的代码段保留。

1
#define RT_USED                     __attribute__((used))

3)RT_UNUSED,定义如下,表示函数或变量可能不使用,这个属性可以避免编译器产生警告信息。

1
#define RT_UNUSED                   __attribute__((unused))

4)RT_WEAK,定义如下,常用于定义函数,编译器在链接函数时会优先链接没有该关键字前缀的函数,如果找不到则再链接由 weak 修饰的函数。

1
#define RT_WEAK                     __weak

5)ALIGN(n),定义如下,作用是在给某对象分配地址空间时,将其存放的地址按照 n 字节对齐,这里 n 可取 2 的幂次方。字节对齐的作用不仅是便于 CPU 快速访问,同时合理的利用字节对齐可以有效地节省存储空间。

1
#define ALIGN(n)                    __attribute__((aligned(n)))

6)RT_ALIGN(size,align),定义如下,作用是将 size 提升为 align 定义的整数的倍数,例如,RT_ALIGN(13,4) 将返回 16。

1
#define RT_ALIGN(size, align)      (((size) + (align) - 1) & ~((align) - 1))

线程管理

线程状态

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

线程状态的切换

线程状态转换图

线程的管理API

线程相关操作

创建删除-create

系统会从动态堆内存中分配一个线程句柄以及按照参数中指定的栈大小从动态堆内存中分配相应的空间。分配出来的栈空间是按照 rtconfig.h 中配置的 RT_ALIGN_SIZE 方式对齐

1
2
3
4
5
6
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线程创建失败
1
rt_err_t rt_thread_delete(rt_thread_t thread);
参数描述
thread要删除的线程句柄
返回——
RT_EOK删除线程成功
-RT_ERROR删除线程失败

初始化和脱离-init

1
2
3
4
5
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);

静态线程的线程句柄(或者说线程控制块指针)、线程栈由用户提供。静态线程是指线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。需要注意的是,用户提供的栈首地址需做系统对齐(例如 ARM 上需要做 4 字节对齐)。

参数描述
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线程创建失败
1
rt_err_t rt_thread_detach (rt_thread_t thread);
参数描述
thread线程句柄,它应该是由 rt_thread_init 进行初始化的线程句柄。
返回——
RT_EOK线程脱离成功
-RT_ERROR线程脱离失败

线程启动-startup

1
rt_err_t rt_thread_startup(rt_thread_t thread);
参数描述
thread线程句柄
返回——
RT_EOK线程启动成功
-RT_ERROR线程起动失败

获取当前线程-self

在程序的运行过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄:

1
rt_thread_t rt_thread_self(void);
返回描述
thread当前运行的线程句柄
RT_NULL失败,调度器还未启动

使线程让出处理器资源-yield

(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。

1
rt_err_t rt_thread_yield(void);

rt_thread_yield() 和 rt_schedule() 比较相像,有相同优先级的其他就绪态线程存在时其行为有所差异

使线程睡眠(延时)-sleep

1
2
3
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操作成功

挂起和恢复线程-suspend

通常不应该使用这个函数来挂起线程本身,如果确实需要采用 rt_thread_suspend() 函数挂起当前任务,需要在调用 rt_thread_suspend() 函数后立刻调用 rt_schedule() 函数进行手动的线程上下文切换。用户只需要了解该接口的作用,不推荐使用该接口。

1
rt_err_t rt_thread_suspend (rt_thread_t thread);
参数描述
thread线程句柄
返回——
RT_EOK线程挂起成功
-RT_ERROR线程挂起失败,因为该线程的状态并不是就绪状态

恢复线程就是让挂起的线程重新进入就绪状态,并将线程放入系统的就绪队列中;

1
rt_err_t rt_thread_resume (rt_thread_t thread);
参数描述
thread线程句柄
返回——
RT_EOK线程恢复成功
-RT_ERROR线程恢复失败,因为该个线程的状态并不是 RT_THREAD_SUSPEND 状态

控制线程-control

1
rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);

线程控制接口 rt_thread_control() 的参数和返回值见下表:

函数参数描述
thread线程句柄
cmd指示控制命令
arg控制参数
返回——
RT_EOK控制执行正确
-RT_ERROR失败

指示控制命令 cmd 当前支持的命令包括:

RT_THREAD_CTRL_CHANGE_PRIORITY:动态更改线程的优先级;

RT_THREAD_CTRL_STARTUP:开始运行一个线程,等同于 rt_thread_startup() 函数调用;

RT_THREAD_CTRL_CLOSE:关闭一个线程,等同于 rt_thread_delete() 函数调用。

设置和删除空闲钩子-idle

空闲钩子函数是空闲线程的钩子函数,如果设置了空闲钩子函数,就可以在系统执行空闲线程时,自动执行空闲钩子函数来做一些其他事情,比如系统指示灯。钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态。

1
2
rt_err_t rt_thread_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));

设置空闲钩子函数 rt_thread_idle_sethook()

函数参数描述
hook设置的钩子函数
返回——
RT_EOK设置成功
-RT_EFULL设置失败

删除空闲钩子函数 rt_thread_idle_delhook()

函数参数描述
hook删除的钩子函数
返回——
RT_EOK删除成功
-RT_ENOSYS删除失败

设置调度器钩子-scheduler

有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个钩子函数中,基本上不允许调用系统 API,更不应该导致当前运行的上下文挂起)。

1
void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to));
函数参数描述
hook表示用户定义的钩子函数指针

钩子函数 hook() 的声明如下:

1
void hook(struct rt_thread* from, struct rt_thread* to);
函数参数描述
from表示系统所要切换出的线程控制块指针
to表示系统所要切换到的线程控制块指针

API应用示例

线程 2 计数到一定值会执行完毕,线程 2 被系统自动删除,计数停止。线程 1 一直打印计数。

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
#include <rtthread.h>

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

static rt_thread_t tid1 = RT_NULL;

/* 线程 1 的入口函数 */
static void thread1_entry(void *parameter)
{
rt_uint32_t count = 0;

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

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程 2 入口 */
static void thread2_entry(void *param)
{
rt_uint32_t count = 0;

/* 线程 2 拥有较高的优先级,以抢占线程 1 而获得执行 */
for (count = 0; count < 10 ; count++)
{
/* 线程 2 打印计数值 */
rt_kprintf("thread2 count: %d\n", count);
}
rt_kprintf("thread2 exit\n");
/* 线程 2 运行结束后也将自动被系统脱离 */
}

/* 线程示例 */
int thread_sample(void)
{
/* 创建线程 1,名称是 thread1,入口是 thread1_entry*/
tid1 = rt_thread_create("thread1",
thread1_entry, RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY, THREAD_TIMESLICE);

/* 如果获得线程控制块,启动这个线程 */
if (tid1 != RT_NULL)
rt_thread_startup(tid1);

/* 初始化线程 2,名称是 thread2,入口是 thread2_entry */
rt_thread_init(&thread2,
"thread2",
thread2_entry,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack),
THREAD_PRIORITY - 1, THREAD_TIMESLICE);
rt_thread_startup(&thread2);

return 0;
}

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

仿真运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
\ | /
- RT - Thread Operating System
/ | \ 3.1.0 build Aug 24 2018
2006 - 2018 Copyright by rt-thread team
msh >thread_sample
msh >thread2 count: 0
thread2 count: 1
thread2 count: 2
thread2 count: 3
thread2 count: 4
thread2 count: 5
thread2 count: 6
thread2 count: 7
thread2 count: 8
thread2 count: 9
thread2 exit
thread1 count: 0
thread1 count: 1
thread1 count: 2
thread1 count: 3

笔记

  • 若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。

时钟管理

获取时钟节拍API

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

1
rt_tick_t rt_tick_get(void);
返回描述
rt_tick当前时钟节拍值

软件定时器管理

RT-Thread 操作系统提供软件实现的定时器,以时钟节拍(OS Tick)的时间长度为单位,即定时数值必须是 OS Tick 的整数倍。它构建在硬件定时器基础之上,使系统能够提供不受数目限制的定时器服务。

RT-Thread 的定时器提供两类定时器机制:

  • 第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。
  • 第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动的停止,否则将永远持续执行下去。

另外,根据超时函数执行时所处的上下文环境,RT-Thread 的定时器可以分为 HARD_TIMER 模式与 SOFT_TIMER 模式,如下图。使用RT_TIMER_FLAG_HARD_TIMER 和 **RT_TIMER_FLAG_SOFT_TIMER ** 来决定使用的模式

定时器上下文环境

系统新创建并激活的定时器都会按照以超时时间排序的方式插入到 rt_timer_list 链表中。

定时器链表示意图

rt_tick(当前系统tick) 从 20 增长到 70,与 Timer1 的 timeout 值相等,这时会触发与 Timer1 定时器相关联的超时函数,同时将 Timer1 从 rt_timer_list 链表上删除。

如果系统当前定时器状态在 10 个 tick 以后(rt_tick=30)有一个任务新创建了一个 tick 值为 300 的 Timer4 定时器,由于 Timer4 定时器的 timeout=rt_tick+300=330, 因此它将被插入到 Timer2 和 Timer3 定时器中间如下图,所示:

定时器链表插入示意图

定时器API

定时器相关操作

在系统启动时需要初始化定时器管理系统。可以通过下面的函数接口完成:

1
void rt_system_timer_init(void);

如果需要使用 SOFT_TIMER,则系统初始化时,应该调用下面这个函数接口:

1
void rt_system_timer_thread_init(void);

创建和删除-create

1
2
3
4
5
rt_timer_t rt_timer_create(const char* name,
void (*timeout)(void* parameter),
void* parameter,
rt_tick_t time,
rt_uint8_t flag);
参数描述
name定时器的名称
void (timeout) (void parameter)定时器超时函数指针(当定时器超时时,系统会调用这个函数)
parameter定时器超时函数的入口参数(当定时器超时时,调用超时回调函数会把这个参数做为入口参数传递给超时函数)
time定时器的超时时间,单位是时钟节拍
flag定时器创建时的参数,支持的值包括单次定时、周期定时、硬件定时器、软件定时器等(可以用 “或” 关系取多个值)
返回——
RT_NULL创建失败(通常会由于系统内存不够用而返回 RT_NULL)
定时器的句柄定时器创建成功

如下2 组值可以以 “或” 逻辑的方式赋给 flag。

1
2
3
4
5
#define RT_TIMER_FLAG_ONE_SHOT      0x0     /* 单次定时     */
#define RT_TIMER_FLAG_PERIODIC 0x2 /* 周期定时 */

#define RT_TIMER_FLAG_HARD_TIMER 0x0 /* 时钟中断的服务例程上下文中被调用 */
#define RT_TIMER_FLAG_SOFT_TIMER 0x4 /* timer 线程的上下文中被调用 */
1
rt_err_t rt_timer_delete(rt_timer_t timer);
参数描述
timer定时器句柄,指向要删除的定时器
返回——
RT_EOK删除成功(如果参数 timer 句柄是一个 RT_NULL,将会导致一个 ASSERT 断言)

初始化和脱离定时器-init

1
2
3
4
5
void rt_timer_init(rt_timer_t timer,
const char* name,
void (*timeout)(void* parameter),
void* parameter,
rt_tick_t time, rt_uint8_t flag);
参数描述
timer定时器句柄,指向要初始化的定时器控制块
name定时器的名称
void (timeout) (void parameter)定时器超时函数指针(当定时器超时时,系统会调用这个函数)
parameter定时器超时函数的入口参数(当定时器超时时,调用超时回调函数会把这个参数做为入口参数传递给超时函数)
time定时器的超时时间,单位是时钟节拍
flag定时器创建时的参数,支持的值包括单次定时、周期定时、硬件定时器、软件定时器(可以用 “或” 关系取多个值),详见上面 创建和删除
1
rt_err_t rt_timer_detach(rt_timer_t timer);
参数描述
timer定时器句柄,指向要脱离的定时器控制块
返回——
RT_EOK脱离成功

启动和停止定时器-start

1
rt_err_t rt_timer_start(rt_timer_t timer);

调用接口后,定时器的状态将更改为激活状态(RT_TIMER_FLAG_ACTIVATED),并按照超时顺序插入到 rt_timer_list 队列链表中。

参数描述
timer定时器句柄,指向要启动的定时器控制块
返回——
RT_EOK启动成功
1
rt_err_t rt_timer_stop(rt_timer_t timer);

当一个(周期性)定时器超时时,可以调用这个函数接口停止这个(周期性)定时器本身。

参数描述
timer定时器句柄,指向要停止的定时器控制块
返回——
RT_EOK成功停止定时器
- RT_ERRORtimer 已经处于停止状态

控制定时器-control

1
rt_err_t rt_timer_control(rt_timer_t timer, rt_uint8_t cmd, void* arg);
参数描述
timer定时器句柄,指向要停止的定时器控制块
cmd用于控制定时器的命令,当前支持四个命令,分别是设置定时时间,查看定时时间,设置单次触发,设置周期触发
arg与 cmd 相对应的控制命令参数 比如,cmd 为设定超时时间时,就可以将超时时间参数通过 arg 进行设定
返回——
RT_EOK成功

函数参数 cmd 支持的命令:

1
2
3
4
#define RT_TIMER_CTRL_SET_TIME      0x0     /* 设置定时器超时时间       */
#define RT_TIMER_CTRL_GET_TIME 0x1 /* 获得定时器超时时间 */
#define RT_TIMER_CTRL_SET_ONESHOT 0x2 /* 设置定时器为单次定时器 */
#define RT_TIMER_CTRL_SET_PERIODIC 0x3 /* 设置定时器为周期型定时器 */

API应用示例

这是一个创建定时器的例子,这个例程会创建两个动态定时器,一个是单次定时,一个是周期性定时并让周期定时器运行一段时间后停止运行,如下所示:周期性定时器 1 的超时函数,每 10 个 OS Tick 运行 1 次,共运行 10 次(10 次后调用 rt_timer_stop 使定时器 1 停止运行);单次定时器 2 的超时函数在第 30 个 OS Tick 时运行一次。

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
#include <rtthread.h>

/* 定时器的控制块 */
static rt_timer_t timer1;
static rt_timer_t timer2;
static int cnt = 0;

/* 定时器 1 超时函数 */
static void timeout1(void *parameter)
{
rt_kprintf("periodic timer is timeout %d\n", cnt);

/* 运行第 10 次,停止周期定时器 */
if (cnt++>= 9)
{
rt_timer_stop(timer1);
rt_kprintf("periodic timer was stopped! \n");
}
}

/* 定时器 2 超时函数 */
static void timeout2(void *parameter)
{
rt_kprintf("one shot timer is timeout\n");
}

int timer_sample(void)
{
/* 创建定时器 1 周期定时器 */
timer1 = rt_timer_create("timer1", timeout1,
RT_NULL, 10,
RT_TIMER_FLAG_PERIODIC);

/* 启动定时器 1 */
if (timer1 != RT_NULL) rt_timer_start(timer1);

/* 创建定时器 2 单次定时器 */
timer2 = rt_timer_create("timer2", timeout2,
RT_NULL, 30,
RT_TIMER_FLAG_ONE_SHOT);

/* 启动定时器 2 */
if (timer2 != RT_NULL) rt_timer_start(timer2);
return 0;
}

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

仿真运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 \ | /
- RT - Thread Operating System
/ | \ 3.1.0 build Aug 24 2018
2006 - 2018 Copyright by rt-thread team
msh >timer_sample
msh >periodic timer is timeout 0
periodic timer is timeout 1
one shot timer is timeout
periodic timer is timeout 2
periodic timer is timeout 3
periodic timer is timeout 4
periodic timer is timeout 5
periodic timer is timeout 6
periodic timer is timeout 7
periodic timer is timeout 8
periodic timer is timeout 9
periodic timer was stopped!

初始化定时器的例子与创建定时器的例子类似,这个程序会初始化 2 个静态定时器,一个是单次定时,一个是周期性的定时,如下代码所示:周期性定时器 1 的超时函数,每 10 个 OS Tick 运行 1 次,共运行 10 次(10 次后调用 rt_timer_stop 使定时器 1 停止运行);单次定时器 2 的超时函数在第 30 个 OS Tick 时运行一次。

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
#include <rtthread.h>

/* 定时器的控制块 */
static struct rt_timer timer1;
static struct rt_timer timer2;
static int cnt = 0;

/* 定时器 1 超时函数 */
static void timeout1(void* parameter)
{
rt_kprintf("periodic timer is timeout\n");
/* 运行 10 次 */
if (cnt++>= 9)
{
rt_timer_stop(&timer1);
}
}

/* 定时器 2 超时函数 */
static void timeout2(void* parameter)
{
rt_kprintf("one shot timer is timeout\n");
}

int timer_static_sample(void)
{
/* 初始化定时器 */
rt_timer_init(&timer1, "timer1", /* 定时器名字是 timer1 */
timeout1, /* 超时时回调的处理函数 */
RT_NULL, /* 超时函数的入口参数 */
10, /* 定时长度,以 OS Tick 为单位,即 10 个 OS Tick */
RT_TIMER_FLAG_PERIODIC); /* 周期性定时器 */
rt_timer_init(&timer2, "timer2", /* 定时器名字是 timer2 */
timeout2, /* 超时时回调的处理函数 */
RT_NULL, /* 超时函数的入口参数 */
30, /* 定时长度为 30 个 OS Tick */
RT_TIMER_FLAG_ONE_SHOT); /* 单次定时器 */

/* 启动定时器 */
rt_timer_start(&timer1);
rt_timer_start(&timer2);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(timer_static_sample, timer_static sample);

仿真运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
\ | /
- RT - Thread Operating System
/ | \ 3.1.0 build Aug 24 2018
2006 - 2018 Copyright by rt-thread team
msh >timer_static_sample
msh >periodic timer is timeout
periodic timer is timeout
one shot timer is timeout
periodic timer is timeout
periodic timer is timeout
periodic timer is timeout
periodic timer is timeout
periodic timer is timeout
periodic timer is timeout
periodic timer is timeout
periodic timer is timeout

低于一个Tick的延时

入口参数 us 指示出需要延时的微秒数目,这个函数只能支持低于 1 OS Tick 的延时,否则 SysTick 会出现溢出而不能够获得指定的延时时间。

1
2
3
4
5
6
7
8
9
10
11
#include <board.h>
void rt_hw_us_delay(rt_uint32_t us)
{
rt_uint32_t delta;
/* 获得延时经过的 tick 数 */
us = us * (SysTick->LOAD/(1000000/RT_TICK_PER_SECOND));
/* 获得当前时间 */
delta = SysTick->VAL;
/* 循环获得当前时间,直到达到指定的时间后退出循环 */
while (delta - SysTick->VAL< us);
}

笔记

  • 时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳,中断之间的时间间隔取决于不同的应用,一般是 1ms–100ms,时钟节拍率越快,系统的额外开销就越大,从系统启动开始计数的时钟节拍数称为系统时间。

线程间同步

线程的同步方式有很多种,其核心思想都是:在访问临界区的时候只允许一个 (或一类) 线程运行。进入 / 退出临界区的方式有很多种:

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

2)调用 rt_enter_critical() 进入临界区,调用 rt_exit_critical() 退出临界区。

信号量Semaphore

信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为 5,则表示共有 5 个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。

可以形成同步资源计数等关系,也能方便的用于线程与线程中断与线程间的同步中。

信号量工作示意图

Semaphore API

信号量相关接口

创建和删除信号量-create

1
2
3
rt_sem_t rt_sem_create(const char *name,
rt_uint32_t value,
rt_uint8_t flag);

当选择 RT_IPC_FLAG_FIFO(先进先出)方式时,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量;

当选择 RT_IPC_FLAG_PRIO(优先级等待)方式时,等待线程队列将按照优先级进行排队,优先级高的等待线程将先获得等待的信号量。

参数描述
name信号量名称
value信号量初始值
flag信号量标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_NULL创建失败
信号量的控制块指针创建成功
1
rt_err_t rt_sem_delete(rt_sem_t sem);

如果删除该信号量时,有线程正在等待该信号量,那么删除操作会先唤醒等待在该信号量上的线程(等待线程的返回值是 - RT_ERROR),然后再释放信号量的内存资源。

参数描述
semrt_sem_create() 创建的信号量对象
返回——
RT_EOK删除成功

初始化和脱离信号量-init

1
2
3
4
rt_err_t rt_sem_init(rt_sem_t       sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag)

flag见上↑

参数描述
sem信号量对象的句柄
name信号量名称
value信号量初始值
flag信号量标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_EOK初始化成功
1
rt_err_t rt_sem_detach(rt_sem_t sem);

使用该函数后,内核先唤醒所有挂在该信号量等待队列上的线程,然后将该信号量从内核对象管理器中脱离。原来挂起在信号量上的等待线程将获得 - RT_ERROR 的返回值。

参数描述
sem信号量对象的句柄
返回——
RT_EOK脱离成功

获取信号量-take

1
rt_err_t rt_sem_take (rt_sem_t sem, rt_int32_t time);
参数描述
sem信号量对象的句柄
time指定的等待时间,单位是操作系统时钟节拍(OS Tick),或者RT_WAITING_FOREVER永远等待
返回——
RT_EOK成功获得信号量
-RT_ETIMEOUT超时依然未获得信号量
-RT_ERROR其他错误

无等待获取信号量-trytake

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

1
rt_err_t rt_sem_trytake(rt_sem_t sem);

这个函数与 rt_sem_take(sem, 0) 的作用相同,即当线程申请的信号量资源实例不可用的时候,它不会等待在该信号量上,而是直接返回 - RT_ETIMEOUT。

参数描述
sem信号量对象的句柄
返回——
RT_EOK成功获得信号量
-RT_ETIMEOUT获取失败

释放信号量-release

1
rt_err_t rt_sem_release(rt_sem_t sem);

例如当信号量的值等于零时,并且有线程等待这个信号量时,释放信号量将唤醒等待在该信号量线程队列中的第一个线程,由它获取信号量;否则将把信号量的值加 1。

参数描述
sem信号量对象的句柄
返回——
RT_EOK成功释放信号量

信号量应用示例

动态信号量的使用

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
#include <rtthread.h>

#define THREAD_PRIORITY 25
#define THREAD_TIMESLICE 5

/* 指向信号量的指针 */
static rt_sem_t dynamic_sem = RT_NULL;

ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
static void rt_thread1_entry(void *parameter)
{
static rt_uint8_t count = 0;

while(1)
{
if(count <= 100)
{
count++;
}
else
return;

/* count 每计数 10 次,就释放一次信号量 */
if(0 == (count % 10))
{
rt_kprintf("t1 release a dynamic semaphore.\n");
rt_sem_release(dynamic_sem);
}
}
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
static void rt_thread2_entry(void *parameter)
{
static rt_err_t result;
static rt_uint8_t number = 0;
while(1)
{
/* 永久方式等待信号量,获取到信号量,则执行 number 自加的操作 */
result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
if (result != RT_EOK)
{
rt_kprintf("t2 take a dynamic semaphore, failed.\n");
rt_sem_delete(dynamic_sem);
return;
}
else
{
number++;
rt_kprintf("t2 take a dynamic semaphore. number = %d\n" ,number);
}
}
}

/* 信号量示例的初始化 */
int semaphore_sample(void)
{
/* 创建一个动态信号量,初始值是 0 */
dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_FIFO);
if (dynamic_sem == RT_NULL)
{
rt_kprintf("create dynamic semaphore failed.\n");
return -1;
}
else
{
rt_kprintf("create done. dynamic semaphore value = 0.\n");
}

rt_thread_init(&thread1,
"thread1",
rt_thread1_entry,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread1);

rt_thread_init(&thread2,
"thread2",
rt_thread2_entry,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack),
THREAD_PRIORITY-1, THREAD_TIMESLICE);
rt_thread_startup(&thread2);

return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(semaphore_sample, semaphore sample);
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 - Thread Operating System
/ | \ 3.1.0 build Aug 27 2018
2006 - 2018 Copyright by rt-thread team
msh >semaphore_sample
create done. dynamic semaphore value = 0.
msh >t1 release a dynamic semaphore.
t2 take a dynamic semaphore. number = 1
t1 release a dynamic semaphore.
t2 take a dynamic semaphore. number = 2
t1 release a dynamic semaphore.
t2 take a dynamic semaphore. number = 3
t1 release a dynamic semaphore.
t2 take a dynamic semaphore. number = 4
t1 release a dynamic semaphore.
t2 take a dynamic semaphore. number = 5
t1 release a dynamic semaphore.
t2 take a dynamic semaphore. number = 6
t1 release a dynamic semaphore.
t2 take a dynamic semaphore. number = 7
t1 release a dynamic semaphore.
t2 take a dynamic semaphore. number = 8
t1 release a dynamic semaphore.
t2 take a dynamic semaphore. number = 9
t1 release a dynamic semaphore.
t2 take a dynamic semaphore. number = 10

信号量锁的作用,生产者消费者例程

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
126
127
128
129
#include <rtthread.h>

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

/* 定义最大 5 个元素能够被产生 */
#define MAXSEM 5

/* 用于放置生产的整数数组 */
rt_uint32_t array[MAXSEM];

/* 指向生产者、消费者在 array 数组中的读写位置 */
static rt_uint32_t set, get;

/* 指向线程控制块的指针 */
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, 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");
}

/* 消费者线程入口 */
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_FIFO);
rt_sem_init(&sem_empty, "empty", MAXSEM, RT_IPC_FLAG_FIFO);
rt_sem_init(&sem_full, "full", 0, RT_IPC_FLAG_FIFO);

/* 创建生产者线程 */
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");
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");
return -1;
}

return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(producer_consumer, producer_consumer sample);
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
\ | /
- RT - Thread Operating System
/ | \ 3.1.0 build Aug 27 2018
2006 - 2018 Copyright by rt-thread team
msh >producer_consumer
the producer generates a number: 1
the consumer[0] get a number: 1
msh >the producer generates a number: 2
the producer generates a number: 3
the consumer[1] get a number: 2
the producer generates a number: 4
the producer generates a number: 5
the producer generates a number: 6
the consumer[2] get a number: 3
the producer generates a number: 7
the producer generates a number: 8
the consumer[3] get a number: 4
the producer generates a number: 9
the consumer[4] get a number: 5
the producer generates a number: 10
the producer exit!
the consumer[0] get a number: 6
the consumer[1] get a number: 7
the consumer[2] get a number: 8
the consumer[3] get a number: 9
the consumer[4] get a number: 10
the consumer sum is: 55
the consumer exit!

互斥量 Mutex

互斥量又叫相互排斥的信号量,是一种特殊的二值信号量。

互斥量和信号量不同的是:拥有互斥量的线程拥有互斥量的所有权,互斥量支持递归访问且能防止线程优先级翻转;并且互斥量只能由持有线程释放,而信号量则可以由任何线程释放。

互斥量工作示意图

线程优先级翻转如下。

优先级反转 (M 为信号量)

优先级继承 (M 为互斥量)

互斥量API

互斥量相关接口

创建和删除互斥量-create

1
rt_mutex_t rt_mutex_create (const char* name, rt_uint8_t flag);

互斥量的 flag 标志设置为 RT_IPC_FLAG_PRIO,表示在多个线程等待资源时,将由优先级高的线程优先获得资源。flag 设置为 RT_IPC_FLAG_FIFO,表示在多个线程等待资源时,将按照先来先得的顺序获得资源。

参数描述
name互斥量的名称
flag互斥量标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
互斥量句柄创建成功
RT_NULL创建失败
1
rt_err_t rt_mutex_delete (rt_mutex_t mutex);

当删除一个互斥量时,所有等待此互斥量的线程都将被唤醒,等待线程获得的返回值是 - RT_ERROR。然后系统将该互斥量从内核对象管理器链表中删除并释放互斥量占用的内存空间。

参数描述
mutex互斥量对象的句柄
返回——
RT_EOK删除成功

初始化和脱离互斥量-init

1
rt_err_t rt_mutex_init (rt_mutex_t mutex, const char* name, rt_uint8_t flag);

flag见上

参数描述
mutex互斥量对象的句柄,它由用户提供,并指向互斥量对象的内存块
name互斥量的名称
flag互斥量标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_EOK初始化成功
1
rt_err_t rt_mutex_detach (rt_mutex_t mutex);

使用该函数接口后,内核先唤醒所有挂在该互斥量上的线程(线程的返回值是 -RT_ERROR),然后系统将该互斥量从内核对象管理器中脱离。

参数描述
mutex互斥量对象的句柄
返回——
RT_EOK成功

获取互斥量-take

线程获取了互斥量,那么线程就有了对该互斥量的所有权,即某一个时刻一个互斥量只能被一个线程持有。

1
rt_err_t rt_mutex_take (rt_mutex_t mutex, rt_int32_t time);
  • 如果互斥量没有被其他线程控制,那么申请该互斥量的线程将成功获得该互斥量。

  • 如果互斥量已经被当前线程线程控制,则该互斥量的持有计数加 1,当前线程也不会挂起等待。

  • 如果互斥量已经被其他线程占有,则当前线程在该互斥量上挂起等待,直到其他线程释放它或者等待时间超过指定的超时时间。

参数描述
mutex互斥量对象的句柄
time指定等待的时间
返回——
RT_EOK成功获得互斥量
-RT_ETIMEOUT超时
-RT_ERROR获取失败

释放互斥量-release

当线程完成互斥资源的访问后,应尽快释放它占据的互斥量,使得其他线程能及时获取该互斥量。

1
rt_err_t rt_mutex_release(rt_mutex_t mutex);
  • 只有已经拥有互斥量控制权的线程才能释放它,每释放一次该互斥量,它的持有计数就减 1。
  • 当该互斥量的持有计数为零时(即持有线程已经释放所有的持有操作),它变为可用,等待在该信号量上的线程将被唤醒。
参数描述
mutex互斥量对象的句柄
返回——
RT_EOK成功

互斥量应用示例

这是一个互斥量的应用例程,互斥锁是一种保护共享资源的方法。当一个线程拥有互斥锁的时候,可以保护共享资源不被其他线程破坏。下面用一个例子来说明,有两个线程:线程 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 的值是否相同,相同则表示 mutex 起到了锁的作用 */
rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);
if(number1 != number2)
{
rt_kprintf("not protect.number1 = %d, mumber2 = %d \n",number1 ,number2);
}
else
{
rt_kprintf("mutex protect ,number1 = mumber2 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_FIFO);
if (dynamic_mutex == RT_NULL)
{
rt_kprintf("create dynamic mutex failed.\n");
return -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);

rt_thread_init(&thread2,
"thread2",
rt_thread_entry2,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack),
THREAD_PRIORITY-1, THREAD_TIMESLICE);
rt_thread_startup(&thread2);
return 0;
}

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

线程 1 与线程 2 中均使用互斥量保护对 2 个 number 的操作(倘若将线程 1 中的获取、释放互斥量语句注释掉,线程 1 将对 number 不再做保护),仿真运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
\ | /
- RT - Thread Operating System
/ | \ 3.1.0 build Aug 24 2018
2006 - 2018 Copyright by rt-thread team
msh >mutex_sample
msh >mutex protect ,number1 = mumber2 is 1
mutex protect ,number1 = mumber2 is 2
mutex protect ,number1 = mumber2 is 3
mutex protect ,number1 = mumber2 is 4

mutex protect ,number1 = mumber2 is 48
mutex protect ,number1 = mumber2 is 49

线程使用互斥量保护对两个 number 的操作,使 number 值保持一致。

互斥量的另一个例子见下面的代码,这个例子将创建 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
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
#include <rtthread.h>

/* 指向线程控制块的指针 */
static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;
static rt_thread_t tid3 = RT_NULL;
static rt_mutex_t mutex = RT_NULL;


#define THREAD_PRIORITY 10
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5

/* 线程 1 入口 */
static void thread1_entry(void *parameter)
{
/* 先让低优先级线程运行 */
rt_thread_mdelay(100);

/* 此时 thread3 持有 mutex,并且 thread2 等待持有 mutex */

/* 检查 thread2 与 thread3 的优先级情况 */
if (tid2->current_priority != tid3->current_priority)
{
/* 优先级不相同,测试失败 */
rt_kprintf("the priority of thread2 is: %d\n", tid2->current_priority);
rt_kprintf("the priority of thread3 is: %d\n", tid3->current_priority);
rt_kprintf("test failed.\n");
return;
}
else
{
rt_kprintf("the priority of thread2 is: %d\n", tid2->current_priority);
rt_kprintf("the priority of thread3 is: %d\n", tid3->current_priority);
rt_kprintf("test OK.\n");
}
}

/* 线程 2 入口 */
static void thread2_entry(void *parameter)
{
rt_err_t result;

rt_kprintf("the priority of thread2 is: %d\n", tid2->current_priority);

/* 先让低优先级线程运行 */
rt_thread_mdelay(50);

/*
* 试图持有互斥锁,此时 thread3 持有,应把 thread3 的优先级提升
* 到 thread2 相同的优先级
*/
result = rt_mutex_take(mutex, RT_WAITING_FOREVER);

if (result == RT_EOK)
{
/* 释放互斥锁 */
rt_mutex_release(mutex);
}
}

/* 线程 3 入口 */
static void thread3_entry(void *parameter)
{
rt_tick_t tick;
rt_err_t result;

rt_kprintf("the priority of thread3 is: %d\n", tid3->current_priority);

result = rt_mutex_take(mutex, RT_WAITING_FOREVER);
if (result != RT_EOK)
{
rt_kprintf("thread3 take a mutex, failed.\n");
}

/* 做一个长时间的循环,500ms */
tick = rt_tick_get();
while (rt_tick_get() - tick < (RT_TICK_PER_SECOND / 2)) ;

rt_mutex_release(mutex);
}

int pri_inversion(void)
{
/* 创建互斥锁 */
mutex = rt_mutex_create("mutex", RT_IPC_FLAG_FIFO);
if (mutex == RT_NULL)
{
rt_kprintf("create dynamic mutex failed.\n");
return -1;
}

/* 创建线程 1 */
tid1 = rt_thread_create("thread1",
thread1_entry,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY - 1, THREAD_TIMESLICE);
if (tid1 != RT_NULL)
rt_thread_startup(tid1);

/* 创建线程 2 */
tid2 = rt_thread_create("thread2",
thread2_entry,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY, THREAD_TIMESLICE);
if (tid2 != RT_NULL)
rt_thread_startup(tid2);

/* 创建线程 3 */
tid3 = rt_thread_create("thread3",
thread3_entry,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY + 1, THREAD_TIMESLICE);
if (tid3 != RT_NULL)
rt_thread_startup(tid3);

return 0;
}

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

仿真运行结果如下:

1
2
3
4
5
6
7
8
9
10
\ | /
- RT - Thread Operating System
/ | \ 3.1.0 build Aug 27 2018
2006 - 2018 Copyright by rt-thread team
msh >pri_inversion
the priority of thread2 is: 10
the priority of thread3 is: 11
the priority of thread2 is: 10
the priority of thread3 is: 10
test OK.

事件集Event

一个事件集可以包含多个事件,利用事件集可以完成一对多多对多多对一的线程间同步。

这种多个事件的集合可以用一个 32 位无符号整型变量来表示,变量的每一位代表一个事件,线程通过 “逻辑与” 或“逻辑或”将一个或多个事件关联起来,形成事件组合。

  • 事件的 “逻辑或” 也称为是独立型同步,指的是线程与任何事件之一发生同步;
  • 事件 “逻辑与” 也称为是关联型同步,指的是线程与若干事件都发生同步。
  • 事件只与线程相关,事件间相互独立:每个线程可拥有 32 个事件标志,采用一个 32 bit 无符号整型数进行记录,每一个 bit 代表一个事件;
  • 事件仅用于同步,不提供数据传输功能;
  • 事件无排队性,即多次向线程发送同一事件 (如果线程还未来得及读走),其效果等同于只发送一次。

事件集工作示意图

线程 #1 的事件标志中第 1 位和第 30 位被置位,如果事件信息标记位设为逻辑与,则表示线程 #1 只有在事件 1 和事件 30 都发生以后才会被触发唤醒,如果事件信息标记位设为逻辑或,则事件 1 或事件 30 中的任意一个发生都会触发唤醒线程 #1。如果信息标记同时设置了清除标记位,则当线程 #1 唤醒后将主动把事件 1 和事件 30 清为零,否则事件标志将依然存在(即置 1)。

事件集API

事件相关接口

创建和删除事件集-create

1
rt_event_t rt_event_create(const char* name, rt_uint8_t flag);
参数描述
name事件集的名称
flag事件集的标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_NULL创建失败
事件对象的句柄创建成功
1
rt_err_t rt_event_delete(rt_event_t event);

在删除前会唤醒所有挂起在该事件集上的线程(线程的返回值是 - RT_ERROR),然后释放事件集对象占用的内存块。

参数描述
event事件集对象的句柄
返回——
RT_EOK成功

初始化和脱离事件集-init

1
rt_err_t rt_event_init(rt_event_t event, const char* name, rt_uint8_t flag);
参数描述
event事件集对象的句柄
name事件集的名称
flag事件集的标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_EOK成功
1
rt_err_t rt_event_detach(rt_event_t event);

用户调用这个函数时,系统首先唤醒所有挂在该事件集等待队列上的线程(线程的返回值是 - RT_ERROR),然后将该事件集从内核对象管理器中脱离。

参数描述
event事件集对象的句柄
返回——
RT_EOK成功

发送事件-send

1
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);

使用该函数接口时,通过参数 set 指定的事件标志来设定 event 事件集对象的事件标志值,然后遍历等待在 event 事件集对象上的等待线程链表,判断是否有线程的事件激活要求与当前 event 对象事件标志值匹配,如果有,则唤醒该线程。

参数描述
event事件集对象的句柄
set发送的一个或多个事件的标志值,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_EOK成功

接收事件-recv

内核使用 32 位的无符号整数来标识事件集,它的每一位代表一个事件,因此一个事件集对象可同时等待接收 32 个事件.

1
2
3
4
5
rt_err_t rt_event_recv(rt_event_t event,
rt_uint32_t set,
rt_uint8_t option,
rt_int32_t timeout,
rt_uint32_t* recved);

当用户调用这个接口时,系统首先根据 set 参数和接收选项 option 来判断它要接收的事件是否发生,如果已经发生,则根据参数 option 上是否设置有 RT_EVENT_FLAG_CLEAR 来决定是否重置事件的相应标志位,然后返回(其中 recved 参数返回接收到的事件);如果没有发生,则把等待的 set 和 option 参数填入线程本身的结构中,然后把线程挂起在此事件上,直到其等待的事件满足条件或等待时间超过指定的超时时间。如果超时时间设置为零,则表示当线程要接受的事件没有满足其要求时就不等待,而直接返回 - RT_ETIMEOUT。

参数描述
event事件集对象的句柄
set接收线程感兴趣的事件
option接收选项 可取值见下
timeout指定超时时间
recved指向接收到的事件
返回——
RT_EOK成功
-RT_ETIMEOUT超时
-RT_ERROR错误

option 的值可取:

1
2
3
4
5
6
/* 选择 逻辑与 或 逻辑或 的方式接收事件 */
RT_EVENT_FLAG_OR
RT_EVENT_FLAG_AND

/* 选择清除重置事件标志位 */
RT_EVENT_FLAG_CLEAR

事件集应用示例

这是事件集的应用例程,例子中初始化了一个事件集,两个线程。一个线程等待自己关心的事件发生,另外一个线程发送事件,如代码清单 6-5 例所示:

事件集的使用例程

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
#include <rtthread.h>

#define THREAD_PRIORITY 9
#define THREAD_TIMESLICE 5

#define EVENT_FLAG3 (1 << 3)
#define EVENT_FLAG5 (1 << 5)

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

ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
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_FLAG3 | EVENT_FLAG5),
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &e) == RT_EOK)
{
rt_kprintf("thread1: OR recv event 0x%x\n", e);
}

rt_kprintf("thread1: delay 1s to prepare the second event\n");
rt_thread_mdelay(1000);

/* 第二次接收事件,事件 3 和事件 5 均发生时才可以触发线程 1,接收完后清除事件标志 */
if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),
RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &e) == RT_EOK)
{
rt_kprintf("thread1: AND recv event 0x%x\n", e);
}
rt_kprintf("thread1 leave.\n");
}


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

/* 线程 2 入口 */
static void thread2_send_event(void *param)
{
rt_kprintf("thread2: send event3\n");
rt_event_send(&event, EVENT_FLAG3);
rt_thread_mdelay(200);

rt_kprintf("thread2: send event5\n");
rt_event_send(&event, EVENT_FLAG5);
rt_thread_mdelay(200);

rt_kprintf("thread2: send event3\n");
rt_event_send(&event, EVENT_FLAG3);
rt_kprintf("thread2 leave.\n");
}

int event_sample(void)
{
rt_err_t result;

/* 初始化事件对象 */
result = rt_event_init(&event, "event", RT_IPC_FLAG_FIFO);
if (result != RT_EOK)
{
rt_kprintf("init event failed.\n");
return -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);

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

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

仿真运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
 \ | /
- RT - Thread Operating System
/ | \ 3.1.0 build Aug 24 2018
2006 - 2018 Copyright by rt-thread team
msh >event_sample
thread2: send event3
thread1: OR recv event 0x8
thread1: delay 1s to prepare the second event
msh >thread2: send event5
thread2: send event3
thread2 leave.
thread1: AND recv event 0x28
thread1 leave.

笔记

Semaphore:值、数值

Mutex:二值性、只能持有进程释放

Event:32Bit、进行’’或’’和’’与’’操作

  • 在获得互斥量后,请尽快释放互斥量,并且在持有互斥量的过程中,不得再行更改持有互斥量线程的优先级。
  • 互斥量不能在中断服务例程中使用。

线程间通信

邮箱Mailbox

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

如下图所示,线程或中断服务例程把一封 4 字节长度的邮件发送到邮箱中,而一个或多个线程可以从邮箱中接收这些邮件并进行处理。

邮箱工作示意图

邮箱API

邮箱相关接口

创建和删除邮箱-create

1
rt_mailbox_t rt_mb_create (const char* name, rt_size_t size, rt_uint8_t flag);
参数描述
name邮箱名称
size邮箱容量
flag邮箱标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_NULL创建失败
邮箱对象的句柄创建成功
1
rt_err_t rt_mb_delete (rt_mailbox_t mb);

删除邮箱时,如果有线程被挂起在该邮箱对象上,内核先唤醒挂起在该邮箱上的所有线程(线程返回值是 - RT_ERROR),然后再释放邮箱使用的内存,最后删除邮箱对象。

参数描述
mb邮箱对象的句柄
返回——
RT_EOK成功

初始化和脱离邮箱-init

1
2
3
4
5
rt_err_t rt_mb_init(rt_mailbox_t mb,
const char* name,
void* msgpool,
rt_size_t size,
rt_uint8_t flag)
参数描述
mb邮箱对象的句柄
name邮箱名称
msgpool缓冲区指针
size邮箱容量
flag邮箱标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_EOK成功

即如果 msgpool 指向的缓冲区的字节数是 N,那么邮箱容量应该是 N/4。

1
rt_err_t rt_mb_detach(rt_mailbox_t mb);

使用该函数接口后,内核先唤醒所有挂在该邮箱上的线程(线程获得返回值是 - RT_ERROR),然后将该邮箱对象从内核对象管理器中脱离。

参数描述
mb邮箱对象的句柄
返回——
RT_EOK成功

发送邮件-send

1
rt_err_t rt_mb_send (rt_mailbox_t mb, rt_uint32_t value);

发送的邮件可以是 32 位任意格式的数据,一个整型值或者一个指向缓冲区的指针。当邮箱中的邮件已经满时,发送邮件的线程或者中断程序会收到 -RT_EFULL 的返回值。

参数描述
mb邮箱对象的句柄
value邮件内容
返回——
RT_EOK发送成功
-RT_EFULL邮箱已经满了

等待方式发送邮件-wait send

1
2
3
rt_err_t rt_mb_send_wait (rt_mailbox_t mb,
rt_uint32_t value,
rt_int32_t timeout);

rt_mb_send_wait() 与 rt_mb_send() 的区别在于有等待时间,如果邮箱已经满了,那么发送线程将根据设定的 timeout 参数等待邮箱中因为收取邮件而空出空间。如果设置的超时时间到达依然没有空出空间,这时发送线程将被唤醒并返回错误码。

参数描述
mb邮箱对象的句柄
value邮件内容
timeout超时时间
返回——
RT_EOK发送成功
-RT_ETIMEOUT超时
-RT_ERROR失败,返回错误

接收邮件-recv

1
rt_err_t rt_mb_recv (rt_mailbox_t mb, rt_uint32_t* value, rt_int32_t timeout);
参数描述
mb邮箱对象的句柄
value邮件内容
timeout超时时间
返回——
RT_EOK发送成功
-RT_ETIMEOUT超时
-RT_ERROR失败,返回错误

邮箱应用示例

这是一个邮箱的应用例程,初始化 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
#include <rtthread.h>

#define THREAD_PRIORITY 10
#define THREAD_TIMESLICE 5

/* 邮箱控制块 */
static struct rt_mailbox mb;
/* 用于放邮件的内存池 */
static char mb_pool[128];

static char mb_str1[] = "I'm a mail!";
static char mb_str2[] = "this is another mail!";
static char mb_str3[] = "over";

ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;

/* 线程 1 入口 */
static void thread1_entry(void *parameter)
{
char *str;

while (1)
{
rt_kprintf("thread1: try to recv a mail\n");

/* 从邮箱中收取邮件 */
if (rt_mb_recv(&mb, (rt_uint32_t *)&str, RT_WAITING_FOREVER) == RT_EOK)
{
rt_kprintf("thread1: get a mail from mailbox, the content:%s\n", str);
if (str == mb_str3)
break;

/* 延时 100ms */
rt_thread_mdelay(100);
}
}
/* 执行邮箱对象脱离 */
rt_mb_detach(&mb);
}

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

/* 线程 2 入口 */
static void thread2_entry(void *parameter)
{
rt_uint8_t count;

count = 0;
while (count < 10)
{
count ++;
if (count & 0x1)
{
/* 发送 mb_str1 地址到邮箱中 */
rt_mb_send(&mb, (rt_uint32_t)&mb_str1);
}
else
{
/* 发送 mb_str2 地址到邮箱中 */
rt_mb_send(&mb, (rt_uint32_t)&mb_str2);
}

/* 延时 200ms */
rt_thread_mdelay(200);
}

/* 发送邮件告诉线程 1,线程 2 已经运行结束 */
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) / 4, /* 邮箱中的邮件数目,因为一封邮件占 4 字节 */
RT_IPC_FLAG_FIFO); /* 采用 FIFO 方式进行线程等待 */
if (result != RT_EOK)
{
rt_kprintf("init mailbox failed.\n");
return -1;
}

rt_thread_init(&thread1,
"thread1",
thread1_entry,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread1);

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

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

仿真运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 \ | /
- RT - Thread Operating System
/ | \ 3.1.0 build Aug 27 2018
2006 - 2018 Copyright by rt-thread team
msh >mailbox_sample
thread1: try to recv a mail
thread1: get a mail from mailbox, the content:I'm a mail!
msh >thread1: try to recv a mail
thread1: get a mail from mailbox, the content:this is another mail!

thread1: try to recv a mail
thread1: get a mail from mailbox, the content:this is another mail!
thread1: try to recv a mail
thread1: get a mail from mailbox, the content:over

消息队列Messagequeue

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

线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程也可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则 (FIFO)。

  • 消息框含有消息头(用于链表连接)

消息队列工作示意图

消息队列和邮箱的明显不同是消息的长度并不限定在 4 个字节以内;另外,消息队列也包括了一个发送紧急消息的函数接口。但是当创建的是一个所有消息的最大长度是 4 字节的消息队列时,消息队列对象将蜕化成邮箱。

消息队列API

消息队列相关接口

创建和删除消息队列-create

1
2
rt_mq_t rt_mq_create(const char* name, rt_size_t msg_size,
rt_size_t max_msgs, rt_uint8_t flag);

消息队列内存的大小 =[消息大小+消息头(用于链表连接)的大小]X消息队列最大个数

参数描述
name消息队列的名称
msg_size消息队列中一条消息的最大长度,单位字节
max_msgs消息队列的最大个数
flag消息队列采用的等待方式,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_EOK发送成功
消息队列对象的句柄成功
RT_NULL失败
1
rt_err_t rt_mq_delete(rt_mq_t mq);

删除消息队列时,如果有线程被挂起在该消息队列等待队列上,则内核先唤醒挂起在该消息等待队列上的所有线程(线程返回值是 - RT_ERROR),然后再释放消息队列使用的内存,最后删除消息队列对象。

参数描述
mq消息队列对象的句柄
返回——
RT_EOK成功

初始化和脱离消息队列-init

1
2
3
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存放消息的缓冲区大小
flag消息队列采用的等待方式,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回——
RT_EOK成功
1
rt_err_t rt_mq_detach(rt_mq_t mq);

使用该函数接口后,内核先唤醒所有挂在该消息等待队列对象上的线程(线程返回值是 -RT_ERROR),然后将该消息队列对象从内核对象管理器中脱离。

参数描述
mq消息队列对象的句柄
返回——
RT_EOK成功

发送消息-send

线程或者中断服务程序都可以给消息队列发送消息。当发送消息时,消息队列对象先从空闲消息链表上取下一个空闲消息块,把线程或者中断服务程序发送的消息内容复制到消息块上,然后把该消息块挂到消息队列的尾部。当且仅当空闲消息链表上有可用的空闲消息块时,发送者才能成功发送消息;当空闲消息链表上无可用消息块,说明消息队列已满,此时,发送消息的的线程或者中断程序会收到一个错误码(-RT_EFULL)。

1
rt_err_t rt_mq_send (rt_mq_t mq, void* buffer, rt_size_t size);

发送消息时,发送者需指定发送的消息队列的对象句柄(即指向消息队列控制块的指针),并且指定发送的消息内容以及消息大小。如下图所示,在发送一个普通消息之后,空闲消息链表上的队首消息被转移到了消息队列尾。

参数描述
mq消息队列对象的句柄
buffer消息内容
size消息大小
返回——
RT_EOK成功
-RT_EFULL消息队列已满
-RT_ERROR失败,表示发送的消息长度大于消息队列中消息的最大长度

等待方式发送消息-wait

1
2
3
4
rt_err_t rt_mq_send_wait(rt_mq_t     mq,
const void *buffer,
rt_size_t size,
rt_int32_t timeout);

rt_mq_send_wait() 与 rt_mq_send() 的区别在于有等待时间,如果消息队列已经满了,那么发送线程将根据设定的 timeout 参数进行等待。如果设置的超时时间到达依然没有空出空间,这时发送线程将被唤醒并返回错误码。

参数描述
mq消息队列对象的句柄
buffer消息内容
size消息大小
timeout超时时间
返回——
RT_EOK成功
-RT_EFULL消息队列已满
-RT_ERROR失败,表示发送的消息长度大于消息队列中消息的最大长度

发送紧急消息-urgent

发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,从空闲消息链表上取下来的消息块不是挂到消息队列的队尾,而是挂到队首,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。发送紧急消息的函数接口如下:

1
rt_err_t rt_mq_urgent(rt_mq_t mq, void* buffer, rt_size_t size);
参数描述
mq消息队列对象的句柄
buffer消息内容
size消息大小
返回——
RT_EOK成功
-RT_EFULL消息队列已满
-RT_ERROR失败

接收消息-recv

1
2
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_EOK成功收到
-RT_ETIMEOUT超时
-RT_ERROR失败,返回错误

消息队列应用示例

这是一个消息队列的应用例程,例程中初始化了 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
#include <rtthread.h>

/* 消息队列控制块 */
static struct rt_messagequeue mq;
/* 消息队列中用到的放置消息的内存池 */
static rt_uint8_t msg_pool[2048];

ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
/* 线程 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("thread1: recv msg from msg queue, the content:%c\n", buf);
if (cnt == 19)
{
break;
}
}
/* 延时 50ms */
cnt++;
rt_thread_mdelay(50);
}
rt_kprintf("thread1: detach mq \n");
rt_mq_detach(&mq);
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程 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("thread2: send urgent message - %c\n", buf);
}
}
else if (cnt>= 20)/* 发送 20 次消息之后退出 */
{
rt_kprintf("message queue stop send, thread2 quit\n");
break;
}
else
{
/* 发送消息到消息队列中 */
result = rt_mq_send(&mq, &buf, 1);
if (result != RT_EOK)
{
rt_kprintf("rt_mq_send ERR\n");
}

rt_kprintf("thread2: send message - %c\n", buf);
}
buf++;
cnt++;
/* 延时 5ms */
rt_thread_mdelay(5);
}
}

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

/* 初始化消息队列 */
result = rt_mq_init(&mq,
"mqt",
&msg_pool[0], /* 内存池指向 msg_pool */
1, /* 每个消息的大小是 1 字节 */
sizeof(msg_pool), /* 内存池的大小是 msg_pool 的大小 */
RT_IPC_FLAG_FIFO); /* 如果有多个线程等待,按照先来先得到的方法分配消息 */

if (result != RT_EOK)
{
rt_kprintf("init message queue failed.\n");
return -1;
}

rt_thread_init(&thread1,
"thread1",
thread1_entry,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack), 25, 5);
rt_thread_startup(&thread1);

rt_thread_init(&thread2,
"thread2",
thread2_entry,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack), 25, 5);
rt_thread_startup(&thread2);

return 0;
}

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

仿真运行结果如下:

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
\ | /
- RT - Thread Operating System
/ | \ 3.1.0 build Aug 24 2018
2006 - 2018 Copyright by rt-thread team
msh > msgq_sample
msh >thread2: send message - A
thread1: recv msg from msg queue, the content:A
thread2: send message - B
thread2: send message - C
thread2: send message - D
thread2: send message - E
thread1: recv msg from msg queue, the content:B
thread2: send message - F
thread2: send message - G
thread2: send message - H
thread2: send urgent message - I
thread2: send message - J
thread1: recv msg from msg queue, the content:I
thread2: send message - K
thread2: send message - L
thread2: send message - M
thread2: send message - N
thread2: send message - O
thread1: recv msg from msg queue, the content:C
thread2: send message - P
thread2: send message - Q
thread2: send message - R
thread2: send message - S
thread2: send message - T
thread1: recv msg from msg queue, the content:D
message queue stop send, thread2 quit
thread1: recv msg from msg queue, the content:E
thread1: recv msg from msg queue, the content:F
thread1: recv msg from msg queue, the content:G

thread1: recv msg from msg queue, the content:T
thread1: detach mq

信号Signal

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

应用程序(用户)能够使用的信号为 SIGUSR1(10)和 SIGUSR2(12)。

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

收到信号的线程对各种信号有不同的处理方法,在信号安装时设定 handler 参数,处理方法可以分为三类:

  • 第一种是类似中断的处理程序,对于需要处理的信号,线程可以指定处理函数,由该函数来处理。

  • 第二种方法是,参数设为 SIG_IGN,忽略某个信号,对该信号不做任何处理,就像未发生过一样。

  • 第三种方法是,参数设为 SIG_DFL,系统会调用默认的处理函数_signal_default_handler()。

如下图所示,假设线程 1 需要对信号进行处理,首先线程 1 安装一个信号并解除阻塞,并在安装的同时设定了对信号的异常处理方式;然后其他线程可以给线程 1 发送信号,触发线程 1 对该信号的处理。

当信号被传递给线程 1 时,如果它正处于挂起状态,那会把状态改为就绪状态去处理对应的信号。如果它正处于运行状态,那么会在它当前的线程栈基础上建立新栈帧空间去处理对应的信号,需要注意的是使用的线程栈大小也会相应增加。

信号工作机制

信号API

信号相关接口

安装信号-install

如果线程要处理某一信号,那么就要在线程中安装该信号。安装信号主要用来确定信号值及线程针对该信号值的动作之间的映射关系,即线程将要处理哪个信号,该信号被传递给线程时,将执行何种操作。

1
rt_sighandler_t rt_signal_install(int signo, rt_sighandler_t[] handler);

其中 rt_sighandler_t 是定义信号处理函数的函数指针类型。

参数描述
signo信号值(只有 SIGUSR1 和 SIGUSR2 是开放给用户使用的,下同)
handler设置对信号值的处理方式 (函数指针)
返回——
SIG_ERR错误的信号
安装信号前的 handler 值成功

屏蔽信号-mask

信号阻塞,也可以理解为屏蔽信号。如果该信号被阻塞,则该信号将不会递达给安装此信号的线程,也不会引发软中断处理。

1
void rt_signal_mask(int signo);
参数描述
signo信号值

解除信号屏蔽-unmask

1
void rt_signal_unmask(int signo);
参数描述
signo信号值

发送信号-kill

1
int rt_thread_kill(rt_thread_t tid, int sig);
参数描述
tid接收信号的线程
sig信号值
返回——
RT_EOK发送成功
-RT_EINVAL参数错误

等待信号-wait

等待 set 信号的到来,如果没有等到这个信号,则将线程挂起,直到等到这个信号或者等待时间超过指定的超时时间 timeout。如果等到了该信号,则将指向该信号体的指针存入 si,如下是等待信号的函数。

1
2
int rt_signal_wait(const rt_sigset_t *set,
rt_siginfo_t[] *si, rt_int32_t timeout);
参数描述
set指定等待的信号
si指向存储等到信号信息的指针
timeout指定的等待时间
返回——
RT_EOK等到信号
-RT_ETIMEOUT超时
-RT_EINVAL参数错误

信号应用示例

这是一个信号的应用例程,如下代码所示。此例程创建了 1 个线程,在安装信号时,信号处理方式设为自定义处理,定义的信号的处理函数为 thread1_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
51
52
53
54
55
56
#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("thread1 received signal %d\n", sig);
}

/* 线程 1 的入口函数 */
static void thread1_entry(void *parameter)
{
int cnt = 0;

/* 安装信号 */
rt_signal_install(SIGUSR1, thread1_signal_handler);
rt_signal_unmask(SIGUSR1);

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

cnt++;
rt_thread_mdelay(100);
}
}

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

if (tid1 != RT_NULL)
rt_thread_startup(tid1);

rt_thread_mdelay(300);

/* 发送信号 SIGUSR1 给线程 1 */
rt_thread_kill(tid1, SIGUSR1);

return 0;
}

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

仿真运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 \ | /
- RT - Thread Operating System
/ | \ 3.1.0 build Aug 24 2018
2006 - 2018 Copyright by rt-thread team
msh >signal_sample
thread1 count : 0
thread1 count : 1
thread1 count : 2
msh >thread1 received signal 10
thread1 count : 3
thread1 count : 4
thread1 count : 5
thread1 count : 6
thread1 count : 7
thread1 count : 8
thread1 count : 9

笔记

Mailbox:开销较小,4字节内容,

Message Queue:长度不固定、需要进行内存分配、

Signal:软件中断

内存管理

RT-Thread 操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。总体上可分为两类:内存堆管理与内存池管理,而内存堆管理又根据具体内存设备划分为三种情况:

  • 第一种是针对小内存块的分配管理(小内存管理算法);

  • 第二种是针对大内存块的分配管理(slab 管理算法);

  • 第三种是针对多内存堆的分配情况(memheap 管理算法)

内存堆Heap

RT-Thread 将 “ZI 段结尾处” 到内存尾部的空间用作内存堆。

RT-Thread 内存分布

内存堆可以在当前资源满足的情况下,根据用户的需求分配任意大小的内存块。而当用户不需要再使用这些内存块时,又可以释放回堆中供其他应用分配使用。RT-Thread 系统为了满足不同的需求,提供了不同的内存管理算法,分别是小内存管理算法Slab 管理算法和 memheap 管理算法

小内存管理算法主要针对系统资源比较少,一般用于小于 2MB 内存空间的系统;

slab 内存管理算法则主要是在系统资源比较丰富时,提供了一种近似多内存池管理算法的快速算法。

memheap 管理算法。memheap 方法适用于系统存在多个内存堆的情况,它可以将多个内存 “粘贴” 在一起,形成一个大的内存堆,用户使用起来会非常方便。

小内存管理算法

小内存管理工作机制图

每个内存块(不管是已分配的内存块还是空闲的内存块)都包含一个数据头,其中包括:

1)magic:变数(或称为幻数),它会被初始化成 0x1ea0(即英文单词 heap),用于标记这个内存块是一个内存管理用的内存数据块;变数不仅仅用于标识这个数据块是一个内存管理用的内存数据块,实质也是一个内存保护字:如果这个区域被改写,那么也就意味着这块内存块被非法改写(正常情况下只有内存管理器才会去碰这块内存)。

2)used:指示出当前内存块是否已经分配。

内存管理的表现主要体现在内存的分配与释放上,小型内存管理算法可以用以下例子体现出来。

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

小内存管理算法链表结构示意图 2

另外,在每次分配内存块前,都会留出 12 字节数据头用于 magic、used 信息及链表节点使用。返回给应用的地址实际上是这块内存块 12 字节以后的地址,前面的 12 字节数据头是用户永远不应该碰的部分(注:12 字节数据头长度会与系统对齐差异而有所不同)。

释放时则是相反的过程,但分配器会查看前后相邻的内存块是否空闲,如果空闲则合并成一个大的空闲内存块。

Slab管理算法

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

slab 内存分配结构图

一个 zone 的大小在 32K 到 128K 字节之间,分配器会在堆初始化时根据堆的大小自动调整。系统中的 zone 最多包括 72 种对象,一次最大能够分配 16K 的内存空间,如果超出了 16K 那么直接从页分配器中分配。每个 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 释放到页面分配器中去。

Memheap管理算法

memheap 功能就可以很方便地把多个 memheap(地址可不连续)粘合起来用于系统的 heap 分配。

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

memheap 处理多内存堆

内存池Pool

创建/初始化rt_mp_create()/rt_mp_init()
分配内存块rt_mp_alloc()
释放内存块rt_mp_free()
删除/脱离rt_mp_delete()/rt_mp_detach()

创建/删除-create

1
2
3
rt_mp_t rt_mp_create(const char* name,
rt_size_t block_count,
rt_size_t block_size);
参数描述
name内存池名
block_count内存块数量
block_size内存块容量
返回——
内存池的句柄创建内存池对象成功
RT_NULL创建失败
1
rt_err_t rt_mp_delete(rt_mp_t mp);
参数描述
mprt_mp_create 返回的内存池对象句柄
返回——
RT_EOK删除成功

初始化/剥离-init

内存池块个数 = size / (block_size + 4 链表指针大小),计算结果取整数。

例如:内存池数据区总大小 size 设为 4096 字节,内存块大小 block_size 设为 80 字节;则申请的内存块个数为 4096/ (80+4)= 48 个。

1
2
3
4
rt_err_t rt_mp_init(rt_mp_t mp,
const char* name,
void *start, rt_size_t size,
rt_size_t block size);
参数描述
mp内存池对象
name内存池名
start内存池的起始位置
size内存池数据区域大小
block_size内存块容量
返回——
RT_EOK初始化成功
- RT_ERROR失败
1
rt_err_t rt_mp_detach(rt_mp_t mp);
参数描述
mp内存池对象
返回——
RT_EOK成功

分配-alloc

1
void *rt_mp_alloc (rt_mp_t mp, rt_int32_t time);
参数描述
mp内存池对象
time超时时间
返回——
分配的内存块地址成功
RT_NULL失败

释放-free

1
void rt_mp_free (void *block);
参数描述
block内存块指针

笔记

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

RT-Thread的中断管理

中断工作机制

中断处理过程

RT-Thread 中断管理中,将中断处理程序分为中断前导程序、用户中断服务程序、中断后续程序三部分
中断处理程序的 3 部分

中断前导

  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
    void rt_interrupt_enter(void)
    {
    rt_base_t level;

    level = rt_hw_interrupt_disable();
    rt_interrupt_nest ++;
    rt_hw_interrupt_enable(level);
    }

中断处理

  1. 中断完后不进行线程切换,这种情况下用户中断服务程序和中断后续程序运行完毕后退出中断模式,返回被中断的线程。

  2. 中断完后进行线程切换,这种情况会调用 rt_hw_context_switch_interrupt() 函数进行上下文切换,该函数跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。

    rt_hw_context_switch_interrupt() 函数会触发PendSV异常,PendSV 异常被触发后,不会立即进行 PendSV 异常中断处理程序,因为此时还在中断处理中,只有当中断后续程序运行完毕,真正退出中断处理后,才进入 PendSV 异常中断处理程序。

中断后续

  1. 通知内核离开中断状态,通过调用 rt_interrupt_leave() 函数,将全局变量 rt_interrupt_nest 减 1,代码如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    void rt_interrupt_leave(void)
    {
    rt_base_t level;

    level = rt_hw_interrupt_disable();
    rt_interrupt_nest --;
    rt_hw_interrupt_enable(level);
    }
  2. 恢复中断前的 CPU 上下文,中断过程发生了线程切换没发生线程切换的CPU上下文是不一样的。

中断嵌套

如果需要进行线程调度,线程的上下文切换将在所有中断处理程序都运行结束时才发生,如下图所示。

中断中的线程切换

中断栈

  1. 中断栈可以保存在打断线程的栈中,当从中断中退出时,返回相应的线程继续执行。
  2. 中断栈也可以与线程栈完全分离开来,即每次进入中断时,在保存完打断线程上下文后,切换到新的中断栈中独立运行。在中断退出时,再做相应的上下文恢复。

使用独立中断栈相对来说更容易实现,并且对于线程栈使用情况也比较容易了解和掌握(否则必须要为中断栈预留空间,如果系统支持中断嵌套,还需要考虑应该为嵌套中断预留多大的空间)。

中断的底半处理

发生中断后,中断一般读取硬件状态或者数据,然后发送一个通知(信号量、事件、邮箱、消息队列等),接下来的相关线程收到通知对着数据进行进一步处理,这个处理的过程就叫底半处理

当一个中断发生时,中断服务程序需要取得相应的硬件状态或者数据。如果中断服务程序接下来要对状态或者数据进行简单处理,比如 CPU 时钟中断,中断服务程序只需对一个系统时钟变量进行加一操作,然后就结束中断服务程序。这类中断需要的运行时间往往都比较短。但对于另外一些中断,中断服务程序在取得硬件状态或数据以后,还需要进行一系列更耗时的处理过程,通常需要将该中断分割为两部分,即上半部分(Top Half)和底半部分(Bottom Half)。在上半部分中,取得硬件状态和数据后,打开被屏蔽的中断,给相关线程发送一条通知(可以是 RT-Thread 所提供的信号量、事件、邮箱或消息队列等方式),然后结束中断服务程序;而接下来,相关的线程在接收到通知后,接着对状态或数据进行进一步的处理,这一过程称之为底半处理

中断管理API

中断相关接口

请注意!!!装载中断服务例程、中断源管理——的API 并不会出现在每一个移植分支中,例如通常 Cortex-M0/M3/M4 的移植分支中就没有这些 API。

装载中断服务例程

系统把用户的中断服务程序 (handler) 和指定的中断号关联起来,可调用如下的接口挂载一个新的中断服务程序:

1
2
3
4
rt_isr_handler_t rt_hw_interrupt_install(int vector,
rt_isr_handler_t handler,
void *param,
char *name);
参数描述
vectorvector 是挂载的中断号
handler新挂载的中断服务程序
paramparam 会作为参数传递给中断服务程序
name中断的名称
返回——
return挂载这个中断服务程序之前挂载的中断服务程序的句柄

中断源管理

  1. 屏蔽中断源

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

屏蔽中断源可以保证在接下来的处理过程中硬件状态或者数据不会受到干扰,可调用下面这个函数接口:

1
void rt_hw_interrupt_mask(int vector);
参数描述
vector要屏蔽的中断号
  1. 打开被屏蔽的中断源
1
void rt_hw_interrupt_umask(int vector);
参数描述
vector要打开屏蔽的中断号

全局中断开关

全局中断开关也称为中断锁,是禁止多线程访问临界区最简单的一种方式,即通过关闭中断的方式,来保证当前线程不会被其他事件打断(因为整个系统已经不再响应那些可以触发线程重新调度的外部事件),也就是当前线程不会被抢占,除非这个线程主动放弃了处理器控制权。

  1. 失能全局中断
1
rt_base_t rt_hw_interrupt_disable(void);
返回描述
中断状态rt_hw_interrupt_disable 函数运行前的中断状态
2. 使能全局中断
1
void rt_hw_interrupt_enable(rt_base_t level);
参数描述
level前一次 rt_hw_interrupt_disable 返回的中断状态

中断通知

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

这两个接口分别用在中断前导程序和中断后续程序中,均会对 rt_interrupt_nest(中断嵌套深度)的值进行修改。

使用 rt_interrupt_enter/leave() 的作用是,在中断中释放了一个信号量,唤醒了某线程,但通过判断发现当前系统处于中断上下文环境中,那么在进行线程切换时应该采取中断中线程切换的策略(等中断结束再切换),而不是立即进行切换(正常情况下立即进行切换)。

(不建议)但如果中断服务程序不会调用内核相关的函数(释放信号量等操作),这个时候,也可以不调用 rt_interrupt_enter/leave() 函数。

在上层应用中,在内核需要知道当前已经进入到中断状态或当前嵌套的中断深度时,可调用 rt_interrupt_get_nest() 接口,它会返回 rt_interrupt_nest。如下:

1
rt_uint8_t rt_interrupt_get_nest(void);
返回描述
0当前系统不处于中断上下文环境中
1当前系统处于中断上下文环境中
大于 1当前中断嵌套层次

中断与轮询

当驱动外设工作时,其编程模式到底采用中断模式触发还是轮询模式触发往往是驱动开发人员首先要考虑的问题,并且这个问题在实时操作系统与分时操作系统中差异还非常大。

在实时系统中轮询模式可能会出现非常大问题,因为在实时操作系统中,当一个程序持续地执行时(轮询时),它所在的线程会一直运行,比它优先级低的线程都不会得到运行。而分时系统中,这点恰恰相反,几乎没有优先级之分,可以在一个时间片运行这个程序,然后在另外一段时间片上运行另外一段程序。

所以通常情况下,实时系统中更多采用的是中断模式来驱动外设。当数据达到时,由中断唤醒相关的处理线程,再继续进行后续的动作。例如一些携带 FIFO(包含一定数据量的先进先出队列)的串口外设,其写入过程可以是这样的,如下图所示:

中断模式驱动外设

对于低速设备来说,运用这种模式非常好,而对于高速设备,数据量又小的情况下,线程的切换时间(几个us)会很明显的影响数据吞吐量和带宽利用率。

发送数据量越小,发送速度越快,对于数据吞吐量的影响也将越大。归根结底,取决于系统中产生中断的频度如何。当一个实时系统想要提升数据吞吐量时,可以考虑的几种方式:

1)增加每次数据量发送的长度,每次尽量让外设尽量多地发送数据;

2)必要情况下更改中断模式为轮询模式。同时为了解决轮询方式一直抢占处理机,其他低优先级线程得不到运行的情况,可以把轮询线程的优先级适当降低。

内核移植

参考官方文档

CPU架构移植

BSP移植

线程切换

cortex-m3中上下文切换统一是使用PendSV来实现的。


RT-Thread
https://www.oikiou.top/2021/42e53028/
作者
Oikiou
发布于
2021年7月21日
许可协议