RT-Thread之知识点
资料来源RT-Thread官网
RT-thread简介
Real Time-Thread
嵌入式实时多线程操作系统
一个处理器核心在某一时刻只能运行一个任务,由于每次对一个任务的执行时间很短、任务与任务之间通过任务调度器进行非常快速地切换(调度器根据优先级决定此刻该执行的任务)
面向对象的设计
3KB Flash、1.2KB RAM 内存资源的 NANO 版本
RT-Thread 体积小,成本低,功耗低、启动快速
RT-Thread 与其他很多 RTOS 如 FreeRTOS、uC/OS 的主要区别之一是,它不仅仅是一个实时内核,还具备丰富的中间层组件
做到组件内部高内聚,组件之间低耦合
提示:C 库:也叫 C 运行库(C Runtime Library),它提供了类似 “strcpy”、“memcpy” 等函数,有些也会包括 “printf”、“scanf” 函数的实现。RT-Thread Kernel Service Library 仅提供内核用到的一小部分 C 库函数实现,为了避免与标准 C 库重名,在这些函数前都会添加上 rt_前缀。
RT-Thread内核简介
启动
而 rtthread_startup() 函数是 RT-Thread 规定的统一启动入口
在/src/components.c 文件
int rtthread_startup(void)
{
rt_hw_interrupt_disable();
/* 板级初始化:需在该函数内部进行系统堆的初始化 */
rt_hw_board_init();
/* 打印 RT-Thread 版本信息 */
rt_show_version();
/* 定时器初始化 */
rt_system_timer_init();
/* 调度器初始化 */
rt_system_scheduler_init();
#ifdef RT_USING_SIGNALS
/* 信号初始化 */
rt_system_signal_init();
#endif
/* 由此创建一个用户 main 线程 */
rt_application_init();
/* 定时器线程初始化 */
rt_system_timer_thread_init();
/* 空闲线程初始化 */
rt_thread_idle_init();
/* 启动调度器 */
rt_system_scheduler_start();
/* 不会执行至此 */
return 0;
}
自动初始化机制
是指初始化函数不需要被显式调用,只需要在函数定义处通过宏定义的方式进行申明,就会在系统启动过程中被执行
int rt_hw_usart_init(void) /* 串口初始化函数 */
{
... ...
/* 注册串口 1 设备 */
rt_hw_serial_register(&serial1, "uart1",
RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,
uart);
return 0;
}
INIT_BOARD_EXPORT(rt_hw_usart_init); /* 使用组件自动初始化机制 */
rt_components_board_init() 函数执行的比较早,主要初始化相关硬件环境,执行这个函数时将会遍历通过 INIT_BOARD_EXPORT(fn) 申明的初始化函数表,并调用各个函数
/* Board underlying hardware initialization */
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
void rt_components_board_init(void)
{
#if RT_DEBUG_INIT
int result;
const struct rt_init_desc *desc;
for (desc = &__rt_init_desc_rti_board_start; desc < &__rt_init_desc_rti_board_end; desc ++)
{
rt_kprintf("initialize %s", desc->fn_name);
result = desc->fn();
rt_kprintf(":%d done\n", result);
}
#else
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
{
(*fn_ptr)();
}
#endif
}
rt_components_init() 函数会在操作系统运行起来之后创建的 main 线程里被调用执行,这个时候硬件环境和操作系统已经初始化完成,可以执行应用相关代码。rt_components_init() 函数会遍历通过剩下的其他几个宏申明的初始化函数表。
内核对象
内核对象分为两类:静态内核对象和动态内核对象
静态内核对象通常放在 RW(非0) 段和 ZI(为0) 段中
在系统启动后在程序中初始化
动态内核对象则是从内存堆中创建的,而后手工做初始化
静态对象会占用 RAM 空间,不依赖于内存堆管理器,内存分配时间确定
动态对象则依赖于内存堆管理器,运行时申请 RAM 空间,当对象被删除后,占用的 RAM 空间被释放。这两种方式各有利弊,可以根据实际环境需求选择具体使用方式
对象容器给每类内核对象分配了一个链表,所有的内核对象都被链接到该链表上
对象容器中包含了每类内核对象的信息,包括对象类型,大小等
内核对象控制块的数据结构:
struct rt_object
{
/* 内核对象名称 */
char name[RT_NAME_MAX];
/* 内核对象类型 */
rt_uint8_t type;
/* 内核对象的参数 */
rt_uint8_t flag;
/* 内核对象管理链表 */
rt_list_t list;
};
enum rt_object_class_type
{
RT_Object_Class_Thread = 0, /* 对象为线程类型 */
#ifdef RT_USING_SEMAPHORE
RT_Object_Class_Semaphore, /* 对象为信号量类型 */
#endif
#ifdef RT_USING_MUTEX
RT_Object_Class_Mutex, /* 对象为互斥量类型 */
#endif
#ifdef RT_USING_EVENT
RT_Object_Class_Event, /* 对象为事件类型 */
#endif
#ifdef RT_USING_MAILBOX
RT_Object_Class_MailBox, /* 对象为邮箱类型 */
#endif
#ifdef RT_USING_MESSAGEQUEUE
RT_Object_Class_MessageQueue, /* 对象为消息队列类型 */
#endif
#ifdef RT_USING_MEMPOOL
RT_Object_Class_MemPool, /* 对象为内存池类型 */
#endif
#ifdef RT_USING_DEVICE
RT_Object_Class_Device, /* 对象为设备类型 */
#endif
RT_Object_Class_Timer, /* 对象为定时器类型 */
#ifdef RT_USING_MODULE
RT_Object_Class_Module, /* 对象为模块 */
#endif
RT_Object_Class_Unknown, /* 对象类型未知 */
RT_Object_Class_Static = 0x80 /* 对象为静态对象 */
};
内核对象容器的数据结构:
struct rt_object_information
{
/* 对象类型 /
enum rt_object_class_type type;
/ 对象链表 /
rt_list_t object_list;
/ 对象大小 */
rt_size_t object_size;
};
配置文件
rtconfig.h
/* 表示内核对象的名称的最大长度,若代码中对象名称的最大长度大于宏定义的长度,
* 多余的部分将被截掉。*/
#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 1000
/* 检查栈是否溢出,未定义则关闭 */
#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
/* 定义该宏可开启信号量的使用,未定义则关闭 */
#define RT_USING_SEMAPHORE
/* 定义该宏可开启互斥量的使用,未定义则关闭 */
#define RT_USING_MUTEX
/* 定义该宏可开启事件集的使用,未定义则关闭 */
#define RT_USING_EVENT
/* 定义该宏可开启邮箱的使用,未定义则关闭 */
#define RT_USING_MAILBOX
/* 定义该宏可开启消息队列的使用,未定义则关闭 */
#define RT_USING_MESSAGEQUEUE
/* 定义该宏可开启信号的使用,未定义则关闭 */
#define RT_USING_SIGNALS
/* 开启静态内存池的使用 */
#define RT_USING_MEMPOOL
/* 定义该宏可开启两个或以上内存堆拼接的使用,未定义则关闭 */
#define RT_USING_MEMHEAP
/* 开启小内存管理算法 */
#define RT_USING_SMALL_MEM
/* 关闭 SLAB 内存管理算法 */
/* #define RT_USING_SLAB */
/* 开启堆的使用 */
#define RT_USING_HEAP
/* 表示开启了系统设备的使用 */
#define RT_USING_DEVICE
/* 定义该宏可开启系统控制台设备的使用,未定义则关闭 */
#define RT_USING_CONSOLE
/* 定义控制台设备的缓冲区大小 */
#define RT_CONSOLEBUF_SIZE 128
/* 控制台设备的名称 */
#define RT_CONSOLE_DEVICE_NAME "uart1"
/* 定义该宏开启自动初始化机制,未定义则关闭 */
#define RT_USING_COMPONENTS_INIT
/* 定义该宏开启设置应用入口为 main 函数 */
#define RT_USING_USER_MAIN
/* 定义 main 线程的栈大小 */
#define RT_MAIN_THREAD_STACK_SIZE 2048
/* 定义该宏可开启系统 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
/* 定义该工程使用的 MCU 为 STM32F103ZE;系统通过对芯片类型的定义,来定义芯片的管脚 */
#define STM32F103ZE
/* 定义时钟源频率 */
#define RT_HSE_VALUE 8000000
/* 定义该宏开启 UART1 的使用 */
#define RT_USING_UART1
注:在实际应用中,系统配置文件 rtconfig.h 是由配置工具自动生成的,无需手动更改。
RT_USED,定义如下,该宏的作用是向编译器说明这段代码有用,即使函数中没有调用也要保留编译。例如 RT-Thread 自动初始化功能使用了自定义的段,使用 RT_USED 会将自定义的代码段保留。
#define RT_USED __attribute__((used))
RT_WEAK,定义如下,常用于定义函数,编译器在链接函数时会优先链接没有该关键字前缀的函数,如果找不到则再链接由 weak 修饰的函数
#define RT_WEAK __weak
线程管理
线程是 RT-Thread 操作系统中最小的调度单位
当线程运行时,它会认为自己是以独占 CPU 的方式在运行
线程执行时的运行环境称为上下文
具体来说就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等。
-
RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到 CPU 的使用权
-
当一个运行着的线程使一个比它优先级高的线程满足运行条件,当前线程的 CPU 使用权就被剥夺了,或者说被让出了,高优先级的线程立刻得到了 CPU 的使用权。
-
如果是中断服务程序使一个高优先级的线程满足运行条件,中断完成时,被中断的线程挂起,优先级高的线程开始运行。
当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该线程的上下文信息恢复。 -
RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复
/* 线程控制块 */
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; /* 用户数据 */
};
RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复
线程栈还用来存放函数中的局部变量:函数中的局部变量从线程栈空间中申请;函数中局部变量初始时从寄存器中分配(ARM 架构),当这个函数再调用另一个函数时,这些局部变量将放入栈中。
线程的状态
- 初始状态
当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状态在 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
优先级
RT-Thread 线程的优先级是表示线程被调度的优先程度。每个线程都具有优先级,线程越重要,赋予的优先级就应越高,线程被调度的可能才会越大
- RT-Thread 最大支持 256 个线程优先级 (0~255),数值越小的优先级越高,0 为最高优先级
- 在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置;
对于 ARM Cortex-M 系列,普遍采用 32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用
在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。
时间片
- 每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效
系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用
其单位是一个系统节拍(OS Tick)
必须注意的一点就是:线程中不能陷入死循环操作,必须要有让出 CPU 使用权的动作,如循环中调用延时函数或者主动挂起。用户设计这种无限循环的线程的目的,就是为了让这个线程一直被系统循环调度运行,永不删除。
错误代码
#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 /* 非法参数 */
- 线程执行完毕,系统自动删除
- 线程通过调用函数 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() 函数,将状态更改为关闭状态。
空闲线程
空闲线程(idle)是系统创建的最低优先级的线程,线程状态永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起
若某线程运行完毕,系统将自动删除线程:
自动执行 rt_thread_exit() 函数
先将该线程从系统就绪队列中删除
再将该线程的状态更改为关闭状态不再参与系统调度
然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中
最后空闲线程会回收被删除线程的资源。
在空闲线程运行时会调用该钩子函数,适合处理功耗管理、看门狗喂狗等工作
空闲线程必须有得到执行的机会,即其他线程不允许一直while(1)死卡
必须调用具有阻塞性质的函数;否则例如线程删除、回收等操作将无法得到正确执行。
主线程
在系统启动时,系统会创建 main 线程,它的入口函数为 main_thread_entry(),
用户的应用入口函数 main() 就是从这里真正开始的
(调用
S
u
p
e
r
Super
Super$main 转到 main() 函数执行)
这个是main的线程
/* the system main thread */
void main_thread_entry(void *parameter)
{
extern int main(void);
//调用 $Super$$main 转到 main() 函数执行
extern int $Super$$main(void);
#ifdef RT_USING_COMPONENTS_INIT
/* RT-Thread components initialization */
rt_components_init();
#endif
#ifdef RT_USING_SMP
rt_hw_secondary_cpu_up();
#endif
/* invoke system main function */
#if defined(__CC_ARM) || defined(__CLANG_ARM)
$Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
main();
#endif
}
void rt_application_init(void)
{
rt_thread_t tid;
#ifdef RT_USING_HEAP
tid = rt_thread_create("main", main_thread_entry, RT_NULL,
RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20);
RT_ASSERT(tid != RT_NULL);
#else
rt_err_t result;
tid = &main_thread;
result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
main_stack, sizeof(main_stack), RT_MAIN_THREAD_PRIORITY, 20);
RT_ASSERT(result == RT_EOK);
/* if not define RT_USING_HEAP, using to eliminate the warning */
(void)result;
#endif
rt_thread_startup(tid);
}
线程
动态线程与静态线程的区别是
动态线程是系统自动从动态内存堆 上分配栈空间 与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),
静态线程是由用户分配栈空间与线程句柄
线程创建
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);
}
}
/* 创建线程 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);
rt_err_t rt_thread_delete(rt_thread_t thread);
用 rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为 RT_THREAD_CLOSE 状态
然后放入到 rt_thread_defunct 队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作
注:rt_thread_create() 和 rt_thread_delete() 函数仅在使能了系统动态堆时才有效(即 RT_USING_HEAP 宏定义已经定义了)
//启动线程
rt_err_t rt_thread_startup(rt_thread_t thread);
//获得当前运行线程
rt_thread_t rt_thread_self(void);
//线程主动切换
rt_err_t rt_thread_yield(void); //yield让步
调用该函数后,当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优先级队列链表的尾部
然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)
rt_thread_yield() 函数和 rt_schedule() 函数比较相像,
执行 rt_thread_yield() 函数后,当前线程被换出,相同优先级的下一个就绪线程将被执行
而执行 rt_schedule() 函数后,当前线程并不一定被换出,即使被换出,也不会被放到就绪线程链表的尾部,而是在系统中选取就绪的优先级最高的线程执行(如果系统中没有比当前线程优先级更高的线程存在,那么执行完 rt_schedule() 函数后,系统将继续执行当前线程)
//线程睡眠
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);
sleep/delay 的传入参数 tick 以 1 个 OS Tick 为单位
mdelay 的传入参数 ms 以 1ms 为单位;
当线程调用 rt_thread_delay() 时,线程将主动挂起
//挂起
rt_err_t rt_thread_suspend (rt_thread_t thread);
注:一个线程尝试挂起另一个线程是一个非常危险的行为,因此RT-Thread对此函数有严格的使用限制:该函数只能使用来挂起当前线程(即自己挂起自己),不可以在线程A中尝试挂起线程B。而且在挂起线程自己后,需要立刻调用 rt_schedule() 函数进行手动的线程上下文切换。这是因为A线程在尝试挂起B线程时,A线程并不清楚B线程正在运行什么程序,一旦B线程正在使用例如互斥量、信号量等影响、阻塞其他线程(如C线程)的内核对象,如果此时其他线程也在等待这个内核对象,那么A线程尝试挂起B线程的操作将会引发其他线程(如C线程)的饥饿,严重危及系统的实时性。
//恢复
rt_err_t rt_thread_resume (rt_thread_t thread);
//控制线程
rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);
//设置 / 删除空闲钩子
rt_err_t rt_thread_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));
注:空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态,例如 rt_thread_delay(),rt_sem_take() 等可能会导致线程挂起的函数都不能使用。并且,由于 malloc、free 等内存相关的函数内部使用了信号量作为临界区保护,因此在钩子函数内部也不允许调用此类函数!
在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。
有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用
//系统调度钩子函数
void rt_scheduler_sethook(void (hook)(struct rt_thread from, struct rt_thread* to));
void hook(struct rt_thread* from, struct rt_thread* to);
注:请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个钩子函数中,基本上不允许调用系统
API,更不应该导致当前运行的上下文挂起)。
- 在线程进行调度切换时,会执行调度,我们可以设置一个调度器钩子,这样可以在线程切换时,做一些额外的事情,这个例子是在调度器钩子函数中打印线程间的切换信息
注:关于删除线程:大多数线程是循环执行的,无需删除;而能运行完毕的线程,RT-Thread 在线程运行完毕后,自动删除线程,在
rt_thread_exit()
里完成删除动作。用户只需要了解该接口的作用,不推荐使用该接口(可以由其他线程调用此接口或在定时器超时函数中调用此接口删除一个线程,但是这种使用非常少)
时钟管理
操作系统中最小的时间单位是时钟节拍 (OS Tick)
- 时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳
- RT-Thread 中,时钟节拍的长度可以根据 RT_TICK_PER_SECOND 的定义来调整,等于 1/RT_TICK_PER_SECOND 秒。
- 时钟节拍由配置为中断触发模式的硬件定时器产生,当中断到来时,将调用一次
滴答定时器
void SysTick_Handler(void)
{
/* 进入中断 */
rt_interrupt_enter();
……
rt_tick_increase();
/* 退出中断 */
rt_interrupt_leave();
}
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()
用于检查系统硬件定时器链表,如果有定时器超时,将调用相应的超时函数。且所有定时器在定时超时后都会从定时器链表中被移除,而周期性定时器会在它再次启动时被加入定时器链表
//获取时钟节拍
rt_tick_t rt_tick_get(void);
- 硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟
- 硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式
- 软件定时器是构建在硬件定时器基础之上,使系统能够提供不受数目限制的定时器服务
- 操作系统提供软件实现的定时器,以时钟节拍(OS Tick)的时间长度为单位,即定时数值必须是 OS Tick 的整数倍
定时器创建
- 单次/周期触发模式
- RT-Thread 定时器默认的方式是 HARD_TIMER 模式,即定时器超时后,超时函数是在系统时钟中断的上下文环境中运行的。在中断上下文中的执行方式决定了定时器的超时函数不应该调用任何会让当前上下文挂起的系统函数;也不能够执行非常长的时间,否则会导致其他中断的响应时间加长或抢占了其他线程执行的时间
-
在中断上下文环境中执行时,对于超时函数的要求与中断服务例程的要求相同:执行时间应该尽量短,执行时不应导致当前上下文挂起、等待。例如在中断上下文中执行的超时函数它不应该试图去申请动态内存、释放动态内存等。
-
SOFT_TIMER 模式可配置,通过宏定义 RT_USING_TIMER_SOFT 来决定是否启用该模式。该模式被启用后,系统会在初始化时创建一个 timer 线程,然后 SOFT_TIMER 模式的定时器超时函数在都会在 timer 线程的上下文环境中执行
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;
链表跳表
跳表是一种基于并联链表的数据结构,实现简单,插入、删除、查找的时间复杂度均为 O(log n)。跳表是链表的一种,但它在链表的基础上增加了 “跳跃” 功能,正是这个功能,使得在查找元素时,跳表能够提供 O(log n)的时间复杂度
- *对定时器的操作包含:创建 / 初始化定时器、启动定时器、运行定时器、删除 / 脱离定时器,所有定时器在定时超时后都会从定时器链表中被移除,而周期性定时器会在它再次启动时被加入定时器链表
#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 / 软件定时器 */
上面 2 组值可以以 “或” 逻辑的方式赋给 flag。当指定的 flag 为 RT_TIMER_FLAG_HARD_TIMER 时,如果定时器超时,定时器的回调函数将在时钟中断的服务例程上下文中被调用;当指定的 flag 为 RT_TIMER_FLAG_SOFT_TIMER 时,如果定时器超时,定时器的回调函数将在系统时钟 timer 线程的上下文中被调用
RT-Thread 定时器的最小精度是由系统时钟节拍所决定的(1 OS Tick = 1/RT_TICK_PER_SECOND 秒,RT_TICK_PER_SECOND 值在 rtconfig.h 文件中定义)
1秒跳动多少次,跳一次多长时间
定时器设定的时间必须是 OS Tick 的整数倍
在 Cortex-M 系列中,SysTick 已经被 RT-Thread 用于作为 OS Tick 使用,它被配置成 1/RT_TICK_PER_SECOND 秒后触发一次中断的方式
断处理函数使用 Cortex-M3 默认的 SysTick_Handler 名字
在 Cortex-M3 的 CMSIS
规范中规定了 SystemCoreClock 代表芯片的主频
所以基于 SysTick 以及 SystemCoreClock,我们能够使用 SysTick 获得一个精确的延时函数
rt_timer_t timer1;
/* 定时器 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");
}
}
/* 创建定时器 1 周期定时器 */
timer1 = rt_timer_create("timer1", timeout1,
RT_NULL, 10,
RT_TIMER_FLAG_PERIODIC);
/* 启动定时器 1 */
if (timer1 != RT_NULL)
rt_timer_start(timer1);
多线程操作
示例:两个线程同时工作
线程1周期产生数据写入共享内存
线程2周期从共享内存获取数据
如共享内存不排他性
如线程2获取时,线程1未写入完成,则获取的数据包含不同时间的数据
- 同步是指按预定的先后次序进行运行,线程同步是指多个线程通过特定的机制(如互斥量,事件对象,临界区)来控制线程之间的执行顺序,
- 也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间将是无序的
临界区
多个线程操作 / 访问同一块区域(代码),这块代码就称为临界区
线程互斥是指对于临界区资源访问的排它性。当多个线程都要使用临界区资源时,任何时刻最多只允许一个线程去使用
-
调用 rt_hw_interrupt_disable() 进入临界区,
-
调用 rt_hw_interrupt_enable() 退出临界区;
-
详见《中断管理》的全局中断开关内容。
-
调用 rt_enter_critical() 进入临界区,
-
调用 rt_exit_critical() 退出临界区。
信号量
-
示例
-
停车位空,进入车辆
-
停车位满,车辆等候
-
停车位空出,车辆补充
-
管理员->信号量
-
空车位数->信号量的值
-
停车位->公共资源(临界区)
-
车辆->线程
-
车辆通过管理员获得停车位
-
线程通过获取信号量访问公共资源
-
每个信号量对象都有一个信号量值和一个线程等待队列,
-
信号量的值对应了信号量对象的实例数目、资源数目,
-
假如信号量值为 5,则表示共有 5 个信号量实例(资源)可以被使用,
-
当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例
struct rt_semaphore
{
struct rt_ipc_object parent; /* 继承自 ipc_object 类 */
rt_uint16_t value; /* 信号量的值 */
};
/* rt_sem_t 是指向 semaphore 结构体的指针类型 */
typedef struct rt_semaphore* rt_sem_t;
当创建一个信号量时,内核首先创建一个信号量控制块,然后对该控制块进行基本的初始化工作,创建信号量使用下面的函数接口:
rt_sem_t rt_sem_create(const char *name,
rt_uint32_t value,
rt_uint8_t flag);
当调用这个函数时,系统将先从对象管理器中分配一个 semaphore 对象,并初始化这个对象,
然后初始化父类 IPC 对象以及与 semaphore 相关的部分
在创建信号量指定的参数中,信号量标志参数决定了当信号量不可用时,多个线程等待的排队方式。
当选择 RT_IPC_FLAG_FIFO(先进先出)方式时,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量;
当选择 RT_IPC_FLAG_PRIO(优先级等待)方式时,等待线程队列将按照优先级进行排队,优先级高的等待线程将先获得等待的信号量
注:RT_IPC_FLAG_FIFO
属于非实时调度方式,除非应用程序非常在意先来后到,并且你清楚地明白所有涉及到该信号量的线程都将会变为非实时线程,方可使用
RT_IPC_FLAG_FIFO,否则建议采用 RT_IPC_FLAG_PRIO,即确保线程的实时性
//创建
// rt_sem_t rt_sem_create(const char *name,
rt_uint32_t value,
rt_uint8_t flag);
//删除
//rt_err_t rt_sem_delete(rt_sem_t sem);
调用这个函数时,系统将删除这个信号量。
如果删除该信号量时,有线程正在等待该信号量,
那么删除操作会先唤醒等待在该信号量上的线程(等待线程的返回值是 - RT_ERROR),
然后再释放信号量的内存资源
//获取信号量
rt_err_t rt_sem_take (rt_sem_t sem, rt_int32_t time);
线程通过获取信号量来获得信号量资源实例,
当信号量值大于零时,线程将获得信号量,并且相应的信号量值会减 1
在调用这个函数时,如果信号量的值等于零,那么说明当前信号量资源实例不可用,
申请该信号量的线程将根据 time 参数的情况选择直接返回、或挂起等待一段时间、或永久等待,
直到其他线程或中断释放该信号量
//无等待获取信号量
rt_err_t rt_sem_trytake(rt_sem_t sem);
//释放信号量
rt_err_t rt_sem_release(rt_sem_t sem);
释放信号量可以唤醒挂起在该信号量上的线程
例如当信号量的值等于零时,并且有线程等待这个信号量时,
释放信号量将唤醒等待在该信号量线程队列中的第一个线程,
由它获取信号量;否则将把信号量的值加 1。
//线程1负责每隔10个计数值释放一个信号量出来
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);
}
}
}
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);
}
}
}
/* 指向信号量的指针 */
static rt_sem_t dynamic_sem = RT_NULL;
/* 创建一个动态信号量,初始值是 0 */
dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_PRIO);
if (dynamic_sem == RT_NULL)
{
rt_kprintf("create dynamic semaphore failed.\n");
return -1;
}
else
{
rt_kprintf("create done. dynamic semaphore value = 0.\n");
}
生产者消费者示例
。
线程1:获取到一个位置后,空减一个,给这个位置赋值,然后发布出来占满了一位,满加一个
线程2:获取到满一个位后,满减一个,然后读出这个数,空多出一个,空加一位
运行图示
可以看到生产者和消费者的执行操作不是同时进行
可以灵活运用
形成锁,同步,资源计数
线程与线程之间同步
线程同步是信号量最简单的一类应用
信号量初始化成为0
表示具有0个信号量资源
而第二个获取信号量的线程将在这个信号量上面等待
当第一个持有信号量的线程完成工作,释放信号量
可以把等待在这个信号量上的线程唤醒
(把信号量当成完成标志,线程1通知信号量开始工作)
锁(二值信号量,会导致优先级翻转)
信号量当作锁来使用
初始化设置成为1,只有1个资源可用
因为信号量的值始终在1和0之间变动
所以这类锁叫做二值信号量
访问资源的时候线程1首先获取持有这个锁semaphore=0
其他访问共享资源的线程由于获取不到将被挂起
当线程1处理完毕释放信号量解开锁
而挂起的第一个等待的线程被唤醒获得使用权
信号量资源计数
信号量值非负
可以用作线程间工作不匹配的场合
中断与线程的同步
一个中断触发后
中断服务程序会通知线程进行相应的处理
信号量初始值设置为0
线程直接在这个信号量挂起
当中断触发后
在中断服务函数释放一个信号量
对应的线程被唤醒
Note
注:中断与线程间的互斥不能采用信号量(锁)的方式,而应采用开关中断的方式。
中断中不能使用互斥锁
互斥量
互斥量是相互排斥的信号量
是一种特殊的二值信号量
类似厕所坑位只允许一个人进入
互斥量和信号量不同
拥有互斥量的线程拥有互斥量的所有权
互斥量支持递归访问还能防止优先级翻转
并且互斥量只能由持有的线程释放
而信号量只能由任何线程释放
互斥量有两种状态
开锁或闭锁状态
优先级翻转
当一个高优先级线程试图通过信号量机制访问共享资源时
如果该信号量正在被低优先级任务持有
这个低优先级线程运行过程中可能被其他中等的优先级线程抢占
因此造成高优先级的任务被许多低优先级的任务阻塞
举例说明
线程A,B,C
优先级A>B>C
当C访问共享资源时
线程A在等待获取
此时B可以直接运行
此时可以看到BC线程都在运行
但是A的优先级最高
被低优先级任务阻塞
互斥量解决这个问题
优先级继承
A等待获取共享资源时
将C的优先级提升到A的级别
从而解决优先级翻转的问题
注:在获得互斥量后,请尽快释放互斥量,并且在持有互斥量的过程中,不得再行更改持有互斥量线程的优先级,否则可能人为引入无界优先级反转的问题。
struct rt_mutex
{
struct rt_ipc_object parent; /* 继承自 ipc_object 类 */
rt_uint16_t value; /* 互斥量的值 */
rt_uint8_t original_priority; /* 持有线程的原始优先级 */
rt_uint8_t hold; /* 持有线程的持有次数 */
struct rt_thread *owner; /* 当前拥有互斥量的线程 */
};
/* rt_mutext_t 为指向互斥量结构体的指针类型 */
typedef struct rt_mutex* rt_mutex_t;
rt_mutex 对象从 rt_ipc_object 中派生,由 IPC 容器所管理
//创建互斥量
rt_mutex_t rt_mutex_create (const char* name, rt_uint8_t flag);
//删除互斥量
rt_err_t rt_mutex_delete (rt_mutex_t mutex);
//获取互斥量
rt_err_t rt_mutex_take (rt_mutex_t mutex, rt_int32_t time);
//无等待获取互斥量
rt_err_t rt_mutex_trytake(rt_mutex_t mutex);
//释放互斥量
rt_err_t rt_mutex_release(rt_mutex_t mutex);
/* 指向互斥量的指针 */
static rt_mutex_t dynamic_mutex = RT_NULL;
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);//释放信号量
}
}
/* 创建一个动态互斥量 */
dynamic_mutex = rt_mutex_create("dmutex", RT_IPC_FLAG_PRIO);
if (dynamic_mutex == RT_NULL)
{
rt_kprintf("create dynamic mutex failed.\n");
return -1;
}
互斥量不能在中断服务例程中使用
事件集
利用事件集可以完成一对多,多对多的线程间的同步
任意一个事件唤醒线程,或者几个事件都成立唤醒线程
多个事件的集合可以用一个32位的无符号整型变量来表示
变量每一位代表一个事件
线程通过逻辑与,逻辑或关联起来
RT-Thread 定义的事件集有以下特点:
1)事件只与线程相关,事件间相互独立:每个线程可拥有 32 个事件标志,采用一个 32 bit 无符号整型数进行记录,每一个 bit 代表一个事件
2)事件仅用于同步,不提供数据传输功能;
3)事件无排队性,即多次向线程发送同一事件 (如果线程还未来得及读走),其效果等同于只发送一次。
在 RT-Thread 中,每个线程都拥有一个事件信息标记,它有三个属性,分别是 RT_EVENT_FLAG_AND(逻辑与),RT_EVENT_FLAG_OR(逻辑或)以及 RT_EVENT_FLAG_CLEAR(清除标记)。
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;
//创建
rt_event_t rt_event_create(const char* name, rt_uint8_t flag);
注: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_send(rt_event_t event, rt_uint32_t set);
//接收事件
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);
内核使用 32 位的无符号整数来标识事件集,它的每一位代表一个事件,
因此一个事件集对象可同时等待接收 32 个事件,
(option 接收选项
/* 选择 逻辑与 或 逻辑或 的方式接收事件 /
RT_EVENT_FLAG_OR
RT_EVENT_FLAG_AND
/ 选择清除重置事件标志位 */
RT_EVENT_FLAG_CLEAR
)
/* 事件控制块 */
static struct rt_event event;
/* 初始化事件对象 */
result = rt_event_init(&event, "event", RT_IPC_FLAG_PRIO);
if (result != RT_EOK)
{
rt_kprintf("init event failed.\n");
return -1;
}
/* 线程
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");
}
/* 线程 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");
}
事件集可使用于多种场合,
它能够在一定程度上替代信号量,用于线程间同步。
一个线程或中断服务例程发送一个事件给事件集对象,
事件的发送操作在事件未清除前,是不可累计的,
而信号量的释放动作是累计的。
线程间同步
信号量
二值信号量
互斥量
事件集
线程间通讯
邮箱
消息队列
信号
邮箱
RT-Thread 操作系统的邮箱用于线程间通信,特点是开销比较低,效率较高
邮箱中的每一封邮件只能容纳固定的 4 字节内容(针对 32 位处理系统,指针的大小即为 4 个字节,
所以一封邮件恰好能够容纳一个指针)
非阻塞方式的邮件发送过程能够安全的应用于中断服务中,
是线程、中断服务、定时器向线程发送消息的有效手段
通常来说,邮件收取过程可能是阻塞的
这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。
当邮箱中不存在邮件且超时时间不为 0 时,邮件收取过程将变成阻塞方式。
在这类情况下,只能由线程进行邮件的收取。
当一个线程向邮箱发送邮件时,如果邮箱没满,将把邮件复制到邮箱中。如果邮箱已经满了,发送线程可以设置超时时间,选择等待挂起或直接返回 - RT_EFULL。如果发送线程选择挂起等待,那么当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送
当一个线程从邮箱中接收邮件时,如果邮箱是空的,接收线程可以选择是否等待挂起直到收到新的邮件而唤醒,或可以设置超时时间。当达到设置的超时时间,邮箱依然未收到邮件时,这个选择超时等待的线程将被唤醒并返回 - RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的 4 个字节邮件到接收缓存中
struct rt_mailbox
{
struct rt_ipc_object parent;
rt_uint32_t* msg_pool; /* 邮箱缓冲区的开始地址 */
rt_uint16_t size; /* 邮箱缓冲区的大小 */
rt_uint16_t entry; /* 邮箱中邮件的数目 */
rt_uint16_t in_offset, out_offset; /* 邮箱缓冲的进出指针 */
rt_list_t suspend_sender_thread; /* 发送线程的挂起等待队列 */
};
typedef struct rt_mailbox* rt_mailbox_t;
//创建
rt_mailbox_t rt_mb_create (const char* name, rt_size_t size, rt_uint8_t flag);
给邮箱动态分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4 字节)与邮箱容量的乘积
//删除
rt_err_t rt_mb_delete (rt_mailbox_t mb);
//发送邮件
rt_err_t rt_mb_send (rt_mailbox_t mb, rt_uint32_t value);
发送的邮件可以是 32 位任意格式的数据,
一个整型值或者一个指向缓冲区的指针。
当邮箱中的邮件已经满时,
发送邮件的线程或者中断程序会收到 -RT_EFULL 的返回值
//发送邮件并等待
rt_err_t rt_mb_send_wait (rt_mailbox_t mb,
rt_uint32_t value,
rt_int32_t timeout);
//发生紧急邮件
rt_err_t rt_mb_urgent (rt_mailbox_t mb, rt_ubase_t value);
发送紧急邮件的过程与发送邮件几乎一样,唯一的不同是,
当发送紧急邮件时,邮件被直接插队放入了邮件队首,
这样,接收者就能够优先接收到紧急邮件,从而及时进行处理
//接收邮件
rt_err_t rt_mb_recv (rt_mailbox_t mb, rt_uint32_t* value, rt_int32_t timeout);
/* 邮箱控制块 */
static struct rt_mailbox_t mb;
mb= rt_mb_creat("mb", 5, RT_IPC_FLAG_FIFO);
if(mb != RT_NULL)
rt_kprintf("creat mb ok");
//永久等待接收邮件
{
/* 从邮箱中收取邮件 */
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);
}
}
}
//发送邮件
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);
}
邮箱是一种简单的线程间消息传递方式,特点是开销比较低,效率较高。
在 RT-Thread 操作系统的实现中能够一次传递一个 4 字节大小的邮件,
并且邮箱具备一定的存储功能,
能够缓存一定数量的邮件数
消息队列
RT-Thread 操作系统的消息队列对象由多个元素组成,当消息队列被创建时,
它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等
struct rt_messagequeue
{
struct rt_ipc_object parent;
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;
//创建
rt_mq_t rt_mq_create(const char* name, rt_size_t msg_size,(消息队列中一条消息的最大长度,单位字节)
rt_size_t max_msgs,(消息队列的最大个数)
rt_uint8_t flag);
//删除
rt_err_t rt_mq_delete(rt_mq_t mq);
//发送消息
rt_err_t rt_mq_send (rt_mq_t mq, void* buffer, rt_size_t size);
把线程或者中断服务程序发送的消息内容复制到消息块上,然后把该消息块挂到消息队列的尾部
//发送消息等待
rt_err_t rt_mq_send_wait(rt_mq_t mq,
const void buffer,
rt_size_t size,
rt_int32_t timeout);
//发送紧急消息
rt_err_t rt_mq_urgent(rt_mq_t mq, void buffer, rt_size_t size);
//接收消息
rt_ssize_t rt_mq_recv (rt_mq_t mq, void* buffer,
rt_size_t size, rt_int32_t timeout);
rt_mq_t mq = NULL;
mq = rt_mq_create("mq", 10, 5, RT_IPC_FLAG_FIFO);
if( mq == RT_NULL)
rt_kprintf("rt_mq_create err");
/* 发送消息到消息队列中 */
result = rt_mq_send(&mq, &buf, 1);
if (result != RT_EOK)
{
rt_kprintf("rt_mq_send ERR\n");
}
/* 从消息队列中接收消息 */
if (rt_mq_recv(&mq, &buf, sizeof(buf), RT_WAITING_FOREVER) > 0)
{
rt_kprintf("thread1: recv msg from msg queue, the content:%c\n", buf);
}
消息队列可以应用于发送不定长消息的场合,
包括线程与线程间的消息交换,以及中断服务例程中给线程发送消息(中断服务例程不能接收消息)。
消息队列和邮箱的明显不同是消息的长度并不限定在 4 个字节以内;
信号
信号本质是软中断,用来通知线程发生了异步事件,用做线程之间的异常通知、应急处理
内存管理
计算机系统中,存储空间分为内部存储空间和外部存储空间
内部存储空间访问速度比较快,能够按照变量地址随机访问
RAM
可以理解为电脑的内存
外部存储空间,掉电不丢失数据,
ROM
理解为硬盘
计算机中,变量中间数据一般存放在RAM
只有实际使用的时候才从RAM调入CPU进行运算
- 分配内存的时间必须是确定的
- 小内存管理算法(小于2M)
- SLAB管理算法
- MEMHEAP管理算法
因为内存堆管理器要满足多线程情况下的安全分配,会考虑多线程间的互斥问题,所以请不要在中断服务例程中分配或释放动态内存块。因为它可能会引起当前上下文被挂起等待。
中断管理
当CPU正在处理内部数据时,外界发生了紧急情况,
要求CPU暂停当前工作转处理这个异步事件
处理完毕后,再回到原来被中断的地方,
继续原来的工作
这个的过程称为中断
实现这一功能的系统系统称为中断系统
申请CPU中断的请求源称为中断源
中断是一种异常
通用寄存器组里的
R13 作为堆栈指针寄存器 (Stack Pointer,SP);
R14 作为连接寄存器 (Link Register,LR),用于在调用子程序时,存储返回地址;
R15 作为程序计数器 (Program Counter,PC),
其中堆栈指针寄存器可以是主堆栈指针(MSP),也可以是进程堆栈指针(PSP)
Cortex-M 中断控制器名为 NVIC(嵌套向量中断控制器)
支持中断嵌套功能
当一个中断触发并且系统进行响应时
处理器硬件会将当前运行位置的上下文寄存器自动压入中断栈中
这部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器
当系统正在服务一个中断时,
如果有一个更高优先级的中断触发,
那么处理器同样会打断当前运行的中断服务程序,
然后把这个中断服务程序上下文的 PSR、PC、LR、R12、R3-R0 寄存器自动保存到中断栈中。
中断向量表
中断向量表是所有中断处理程序的入口
Cortex-M 系列的中断处理过程:
把一个函数(用户中断服务程序)同一个虚拟中断向量表中的中断向量联系在一起。
当中断向量对应中断发生的时候,
被挂接的用户中断服务程序就会被调用执行
在 Cortex-M 内核上,
所有中断都采用中断向量表的方式进行处理,
即当一个中断触发时,处理器将直接判定是哪个中断源,
然后直接跳转到相应的固定位置进行处理,
每个中断服务程序必须排列在一起放在统一的地址上
(这个地址必须要设置到 NVIC 的中断向量偏移寄存器中)
中断向量表一般由一个数组定义或在起始代码中给出,默认采用起始代码给出
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset 处理函数
DCD NMI_Handler ; NMI 处理函数
DCD HardFault_Handler ; Hard Fault 处理函数
DCD MemManage_Handler ; MPU Fault 处理函数
DCD BusFault_Handler ; Bus Fault 处理函数
DCD UsageFault_Handler ; Usage Fault 处理函数
DCD 0 ; 保留
DCD 0 ; 保留
DCD 0 ; 保留
DCD 0 ; 保留
DCD SVC_Handler ; SVCall 处理函数
DCD DebugMon_Handler ; Debug Monitor 处理函数
DCD 0 ; 保留
DCD PendSV_Handler ; PendSV 处理函数
DCD SysTick_Handler ; SysTick 处理函数
… …
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
HardFault_Handler PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
… …
注意代码后面的 [WEAK] 标识,它是符号弱化标识,
在 [WEAK] 前面的符号(如 NMI_Handler、HardFault_Handler)将被执行弱化处理,
如果整个代码在链接时遇到了名称相同的符号(例如与 NMI_Handler 相同名称的函数),
那么代码将使用未被弱化定义的符号(与 NMI_Handler 相同名称的函数),
而与弱化符号相关的代码将被自动丢弃
RT-Thread 中断管理中,
将中断处理程序分为
中断前导程序、
用户中断服务程序、
中断后续程序三部分
中断前导程序
1)保存 CPU 中断现场,这部分跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。
对于 Cortex-M 来说,该工作由硬件自动完成。当一个中断触发并且系统进行响应时,处理器硬件会将当前运行部分的上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器。
2)通知内核进入中断状态,调用 rt_interrupt_enter() 函数,
作用是把全局变量 rt_interrupt_nest 加 1,用它来记录中断嵌套的层数,代码如下所示。
void rt_interrupt_enter(void)
{
rt_base_t level;
level = rt_hw_interrupt_disable();
rt_interrupt_nest ++;
rt_hw_interrupt_enable(level);
}
用户中断服务程序
用户中断服务程序(ISR)
分为两种情况
第一种情况是不进行线程切换,这种情况下用户中断服务程序和中断后续程序运行完毕后退出中断模式,
返回被中断的线程
另一种情况是,在中断处理过程中需要进行线程切换,
这种情况会调用 rt_hw_context_switch_interrupt() 函数进行上下文切换,
该函数跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。
中断嵌套
在允许中断嵌套的情况下,
在执行中断服务程序的过程中,
如果出现高优先级的中断,
当前中断服务程序的执行将被打断,以执行高优先级中断的中断服务程序,
当高优先级中断的处理完成后,被打断的中断服务程序才又得到继续执行
如果需要进行线程调度,线程的上下文切换将在所有中断处理程序都运行结束时才发生
中断栈
(一)
在中断处理过程中,在系统响应中断前,
软件代码(或处理器)需要把当前线程的上下文保存下来(通常保存在当前线程的线程栈中),
再调用中断服务程序进行中断响应、处理。在进行中断处理时(实质是调用用户的中断服务程序函数),
中断处理函数中很可能会有自己的局部变量,这些都需要相应的栈空间来保存,
所以中断响应依然需要一个栈空间来做为上下文,运行中断处理函数。
中断栈可以保存在打断线程的栈中,当从中断中退出时,返回相应的线程继续执行
(二)
中断栈也可以与线程栈完全分离开来,
即每次进入中断时,在保存完打断线程上下文后,切换到新的中断栈中独立运行
在中断退出时,再做相应的上下文恢复
使用独立中断栈相对来说更容易实现,并且对于线程栈使用情况也比较容易了解和掌握
否则必须要为中断栈预留空间,如果系统支持中断嵌套,还需要考虑应该为嵌套中断预留多大的空间)
RT-Thread 采用的方式是提供独立的中断栈,
即中断发生时,中断的前期处理程序会将用户的栈指针更换到系统事先留出的中断栈空间中,
等中断退出时再恢复用户的栈指针。
这样中断就不会占用线程的栈空间,从而提高了内存空间的利用率,且随着线程的增加,
这种减少内存占用的效果也越明显
在 Cortex-M 处理器内核里有两个堆栈指针,
一个是主堆栈指针(MSP),是默认的堆栈指针,在运行第一个线程之前和在中断和异常服务程序里使用;
另一个是线程堆栈指针(PSP),在线程里使用。
中断服务程序在系统中相当于拥有最高的优先级,会抢占所有线程优先执行
中断服务程序在取得硬件状态或数据以后还需要进行一系列更耗时的处理过程,
通常需要将该中断分割为两部分,
即上半部分(Top Half)和底半部分(Bottom Half)
在上半部分中,取得硬件状态和数据后,打开被屏蔽的中断,给相关线程发送一条通知
(可以是 RT-Thread 所提供的信号量、事件、邮箱或消息队列等方式),
然后结束中断服务程序;
相关的线程在接收到通知后,接着对状态或数据进行进一步的处理,
这一过程称之为底半处理
中断管理接口
全局中断开关
全局中断开关也称为中断锁,
是禁止多线程访问临界区最简单的一种方式,
即通过关闭中断的方式,来保证当前线程不会被其他事件打断
void rt_interrupt_enter(void)
{
rt_base_t level;
//关闭整个系统的中断
level = rt_hw_interrupt_disable();
rt_interrupt_nest ++;
//恢复整个系统的中断
rt_hw_interrupt_enable(level);
}
使用中断锁来操作临界区的方法可以应用于任何场合,
且其他几类同步方式都是依赖于中断锁而实现的,
可以说中断锁是最强大的和最高效的同步方法。
只是使用中断锁最主要的问题在于,
在中断关闭期间系统将不再响应任何中断,
也就不能响应外部的事件。
所以中断锁对系统的实时性影响非常巨大
/* 关闭中断 */
level = rt_hw_interrupt_disable();
a = a + value;
/* 恢复中断 */
rt_hw_interrupt_enable(level);
/* 获得信号量锁 */
rt_sem_take(sem_lock, RT_WAITING_FOREVER);
a = a + value;
/* 释放信号量锁 */
rt_sem_release(sem_lock);
中断通知
当整个系统被中断打断,进入中断处理函数时,需要通知内核当前已经进入到中断状态
void rt_interrupt_enter(void);
void rt_interrupt_leave(void);
大家普遍认为实时系统中数据吞吐量不足的缘故
系统开销消耗在了线程切换上
线程:应用25微秒,线程切换8微秒,利用率=25/(25+8)=75.8%
轮询:100%
由于关闭全局中断会导致整个系统不能响应中断,
所以在使用关闭全局中断做为互斥访问临界区的手段时,
必须需要保证关闭全局中断的时间非常短,
例如运行数条机器指令的时间。