1、对RTOS/RT-Thread优先级反转的理解
参考链接 1: https://blog.csdn.net/m0_74712453/article/details/134001652
参考链接 2:https://blog.csdn.net/weixin_45590051/article/details/118330634
优先级反转是实时操作系统最常见的问题,解决办法是互斥量使用优先级继承方法。
1、优先级翻转
1.1 简单定义
- 简单言之:高优先级线程等待资源的过程中,被其他低优先级线程抢占了 CPU,从而造成高优先级线程被其他低优先级线程阻塞,实时性难以得到保证。
1.2 详细定义
- 详细言之:最高优先级线程 A 和最低优先级线程 C 通过信号机制共享资源。目前最低优先级线程 C 占有资源,锁定了信号量,最高优先级线程 A 陷入阻塞。若在最低优先级线程 C 释放信号量前,中等优先级线程 B 抢占了线程 C 的 CPU,此时就出现线程 B 优先级比线程 A 低,却导致线程 A 阻塞。
2、解决办法:优先级继承
2.1 定义
若互斥信号量正在被低优先级 C 的线程持有时,若此时高优先级线程 A 尝试获取这个互斥信号量,则高优先级线程 A 阻塞之时,会将低优先级 C 的优先级提高至与自己相同的优先级。
2.2 获取互斥量函数
RT-Thread 中,获取互斥量函数 rt_mutex_take
内部已经实现了优先级继承
// 高优先级线程 tid2 试图拥有低优先级线程 tid3 的互斥锁,
// RT-Thread 会将 tid3 优先级提高到 tid2 同等优先级
result = rt_mutex_take(mutex, RT_WAITING_FOREVER);
2.3 验证优先级继承 Demo
#include <rtthread.h>
/* tid1 优先级最高,tid2 优先级中等,tid1 优先级最低 */
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
static void thread1_entry(void *parameter)
{
rt_kprintf("thread1_entry: the priority of thread1 is: %d\n", tid1->current_priority);
// 延迟让低优先级线程运行
rt_thread_mdelay(100);
// 此时 tid3 持有 mutex,tid2 等待持有 mutex
// 若此时 tid2 与 tid3 优先级不等,则说明测试失败
if (tid2->current_priority != tid3->current_priority)
{
rt_kprintf("thread1_entry: the priority of thread2 is: %d\n", tid2->current_priority);
rt_kprintf("thread1_entry: the priority of thread3 is: %d\n", tid3->current_priority);
rt_kprintf("thread1_entry: test failed.\n");
return;
}
else
{
rt_kprintf("thread1_entry: the priority of thread2 is: %d\n", tid2->current_priority);
rt_kprintf("thread1_entry: the priority of thread3 is: %d\n", tid3->current_priority);
rt_kprintf("thread1_entry: test OK.\n");
}
}
/* 线程 2 入口 */
static void thread2_entry(void *parameter)
{
rt_err_t result;
rt_kprintf("thread2_entry: the priority of thread2 is: %d\n", tid2->current_priority);
/* 延迟让低优先级线程运行 */
rt_thread_mdelay(50);
// 高优先级线程 tid2 试图拥有低优先级线程 tid3 的互斥锁,
// RT-Thread 会将 tid3 优先级提高到 tid2 同等优先级
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("thread3_entry: 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_entry: thread3 take a mutex, failed.\n");
}
// 做长时间的循环,以便 tid2 可以试图抢夺互斥锁 mutex
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;
}
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);
tid2 = rt_thread_create( "thread2", thread2_entry, RT_NULL,
THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
if (tid2 != RT_NULL)
rt_thread_startup(tid2);
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;
}
2、操作系统为什么会使应用程序所需的内存暴增?
使用操作系统所增加的内存需求,主要来源于:
-
1)运行操作系统本身所需的内存
-
2)创建线程块、消息控制块等操作系统组件需要的内存;
裸机使用标志位或全局变量来为不同模块之间传递信息,操作系统模式下,会涉及同步、互斥等,需要使用信号量、消息队列等操作系统组件。
- 扩展:裸机跟操作系统传递消息方式的区别?(最核心的是操作系统可以有多线程,线程栈由堆分配)
3、RT-Thread 内核
实时内核的实现包括:对象管理、线程管理及调度、时钟管理、线程间通信管理、及内存管理等。
3.1 线程调度
- 1)线程是 RTT 最小的调度单位,RTT 的线程调度算法是基于优先级的全抢占式多线程调度算法。
- 2)除了中断处理函数、临界区代码和禁止中断的代码时不可抢占之外,其余都可抢占,包括线程调度器本身。
- 3)支持 256 个线程优先级,0 优先级最高,255 优先级最低,最低优先级留给空闲线程使用。
3.2 时钟管理
- RTT 的时钟管理以时钟节拍为基础,基于时钟节拍处理所有和时间相关的事件,如线程延迟、线程时间片轮转调度及定时器超时等。
- 通常使用定时器定时回调函数完成定时服务。
- 时钟节拍的实现和获取(见3.2.1)
3.2.1 RTT 时钟节拍的实现和获取
参考链接:https://blog.csdn.net/qq_52251819/article/details/134982751
- 时钟节拍由配置为中断模式的系统滴答定时器产生,在系统滴答定时器初始化函数(
rt_hw_systick_init(void)
)里会使用到RT_TICK_PER_SECOND
这个宏。时钟节拍的值等于1/RT_TICK_PER_SECOND
(位于 rtconfig.h),如若RT_TICK_PER_SECOND - 1000
,则滴答定时器中断会 1ms 进行一次。
- 滴答定时器每次中断会进入滴答定时器中断函数(
void SysTick_Handler(void)
),每次进入中断,全局变量滴答数rt_tick
变量就会自增。因此系统是根据全局变量滴答数来获取时间的。
4 线程同步
Q:RTT 如何实现线程同步?
A:通过信号量、互斥量与事件集实现线程间同步。
- 1)线程通过对信号量、互斥量的获取与释放进行同步。互斥量通过优先级继承的方式解决了实时操作系统常见的优先级反转问题。
- 2)线程通过对事件的发送与接收进行同步。事件集支持多事件的“或触发”和“与触发”,适合于线程等待多个事件的情况。
4.1 事件集详解与应用
事件用于实现线程之间的同步,一个事件发生即使一个同步。事件集可以实现一对多(一个线程等待多个事件触发)、多对多(多个线程等待多个事件触发)的同步。
其中,一个线程与多个事件的关系可以设为:
- 1)特定事件触发唤醒线程;
- 2)其中任意一个事件触发唤醒线程;
- 3)几个事件都触发后才唤醒线程;
4.1.1 事件集工作机制
事件集可以用 32 bit 无符号整型变量表示,每个 bit 代表一个事件,线程通过逻辑与或者逻辑或将一个或多个事件关联起来,形成事件组合。
- 逻辑或:即独立性同步,线程与任何事件之一发生同步;
- 逻辑与:即关联型同步,线程与若干事件都发生同步;
- 逻辑或和逻辑与只能二选一,不可一起使用。
4.1.2 事件集与信号量的区别
- 对于事件集,线程与事件集的同步关系可以是一对多,也可以是多对多。即一个线程等待多个事件触发,或多个线程等待多个事件触发;对于信号量,线程与信号量的同步关系只能是一对一,即一个信号量只能同步一个线程。
- 事件的发送动作在事件未清除之前,是不可累加的(类似边缘触发模式,只触发一次);而信号量的释放动作可以累加(类似水平触发模式)
4.1.3 事件集本质是做标志,为什么不用全局变量?
- 在操作系统中,涉及多线程同时对全局变量进行访问,需要通过同步与互斥的方式保护全局变量(临界资源);
- 使用全局变量,线程对事件的获取是主动轮询方式,浪费 CPU;
- 事件集具有等待超时机制,而全局变量只能由用户自己实现;
4.1.4 事件集控制块
事件集控制块含有 IPC 成员变量,IPC 成员变量含有线程挂起等待链表。
struct rt_object
{
const char *name;
rt_uint8_t type; /**< type of kernel object */
rt_uint8_t flag; /**< flag of kernel object */
rt_list_t list;
}
struct rt_ipc_object
{
struct rt_object parent;
// 线程挂起等待链表
rt_list_t suspend_thread;
}
struct rt_event
{
struct rt_ipc_object parent;
rt_uint32_t set;
}
typedef struct rt_event* rt_event_t;
4.1.5 事件集函数接口
4.1.5.1 创建/删除
- 创建事件集:核心操作是1)设置阻塞唤醒模式;2)初始化一个链表用于因记录访问此事件而被阻塞挂起的线程;3)清空事件集。
- 参数 name:事件集名称;
- flag:事件集的标志。阻塞线程按优先级等待
RT_IPC_FLAG_PRIO
还是RT_IPC_FLAG_FIFO
,与信号量使用方法一致。(备注:既然有阻塞线程链表,必定要有对阻塞链表排序的方法)rt_event_t rt_event_create(const char *name, rt_uint8_t flag) { ... // 设置阻塞唤醒模式 event->parent.parent.flag = flag; // 初始化一个链表用于因记录访问此事件而被阻塞挂起的线程 _ipc_object_init(&(event->parent)); /* 事件集合清零 */ event->set = 0; return event; }
- 删除事件集:核心操作是1)唤醒所有等待此事件集的线程;2)将事件集从内核对象管理器链表中删除
rt_err_t rt_event_delete(rt_event_t event) { ... // 唤醒所有等待此事件集的线程 rt_susp_list_resume_all(&(event->parent.suspend_thread), RT_ERROR); // 将事件集从内核对象管理器链表中删除 rt_object_delete(&(event->parent.parent)); return RT_EOK; }
4.1.5.2 发送事件
- 定义:事件发送
rt_event_send()
是指一个线程(任务)向另一个线程(任务)发送事件信号,通知该线程(任务)某事件已经满足条件。如传感器采集事件完成了数据采集工作,就可发送事件通知数据处理线程进行数据处理。 - 线程 A 调用事件发送函数
rt_event_send()
依赖事件对象参数rt_event_t event
和事件集标志参数rt_uint32_t set
。 此函数遍历因等待事件而阻塞挂起的线程,若线程的事件信息与当前事件对象的事件标志相匹配,此线程将被恢复。- 备注:
rt_thread_t
包含1)用于链接的链表头rt_list_t
变量;2)记录线程感兴趣的事件集rt_uint32_t event_set
变量;3)记录事件模式的变量rt_uint8_t event_info
。
- 备注:
/*
@param set:要发送的事件标志
*/
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set)
{
struct rt_list_node *n;
...
/* 关中断 */
level = rt_hw_interrupt_disable();
/* 设置事件 */
event->set |= set;
// 若线程因等待某个事件而阻塞,会被添加进该事件的挂起线程链表
if (!rt_list_isempty(&event->parent.suspend_thread))
{
n = event->parent.suspend_thread.next;
while (n != &(event->parent.suspend_thread))
{
// 根据已知成员 rt_list_t 的地址,算出结构体 rt_thread_t 的首地址
thread = rt_list_entry(n, struct rt_thread, list);
status = -RT_ERROR;
if (thread->event_info & RT_EVENT_FLAG_AND)
{
if ((thread->event_set & event->set) == thread->event_set)
{
status = RT_EOK;
}
}
else if (thread->event_info & RT_EVENT_FLAG_OR)
{
if (thread->event_set & event->set)
{
/* 保存收到的事件集*/
thread->event_set = thread->event_set & event->set;
status = RT_EOK;
}
}
else
{...}
n = n->next;
// 若条件满足,恢复线程
if (status == RT_EOK)
{
// 若在接收中设置了RT_EVENT_FLAG_CLEAR,线程唤醒时会将事件标志位清除, 防止一直响应事件
if (thread->event_info & RT_EVENT_FLAG_CLEAR)
event->set &= ~thread->event_set;
// 恢复阻塞的线程
rt_thread_resume(thread);
need_schedule = RT_TRUE;
}
}
}
/* 关中断 */
rt_hw_interrupt_enable(level);
// 发起一次线程调度
if (need_schedule == RT_TRUE)
rt_shedule();
return RT_EOK;
}
4.1.5.3 接收事件
- 定义:事件接收 是指一个线程等待某个事件发生,一旦发生,此线程被唤醒并得到相应的信息。
- 调用接收事件函数
rt_event_recv()
,线程从事件对象 event 接收事件。- 若事件对象 event 满足线程所感兴趣的事件,则根据接收选项 option 是否设置
RT_EVENT_FLAG_CLEAR
来决定是否清除事件相应标志位,并返回; - 若不满足,则把线程感兴趣的事件 set 和接收选项 option 写到线程控制块,然后把线程挂在此事件对象的阻塞队列上,直到事件发生或等待事件超时。
- 若事件对象 event 满足线程所感兴趣的事件,则根据接收选项 option 是否设置
/*
@param event 指向要接收的事件集对象指针
@param set 线程感兴趣的事件标志
@param option 取值为:RT_EVENT_FLAG_OR、RT_EVENT_FLAG_AND、RT_EVENT_FLAG_CLEAR
@param recved 指向返回的已接收事件的指针
*/
rt_err_t rt_event_recv(rt_event_t event, rt_uint32_t set, rt_uint8_t option,
rt_uint32_t timeout, rt_uint32_t *recved)
{
...
// 获取当前接收事件的线程
thread = rt_thread_self();
/* 关中断 */
level = rt_hw_interrupt_disable();
if (option & RT_EVENT_FLAG_AND)
{
if ((event->set & set) == set)
status = RT_EOK;
}
else if (option & RT_EVENT_FLAG_OR)
{
if (event->set & set)
status = RT_EOK;
}
else
{...}
// 若事件集达到触发要求
if (status == RT_EOK)
{
// 记录接收的事件
if (recved)
*recved = (event->set & set);
// 设置线程事件信息
thread->event_set = (event->set & set);
thread->event_info = option;
if (option & RT_EVENT_FLAG_CLEAR)
event->set &= ~set;
}
// 若当前线程不等待
else if (timeout == 0)
{
...
}
// 若当前线程等待
else
{
/* 设置线程感兴趣的事件信息 */
thread->event_set = set;
thread->event_info = option;
_ipc_list_suspend(&(event->parent.suspend_thread), thred, event->parent.parent.flag);
// 若有等待超时,则启动线程计时器
if (timeout > 0)
{
// 重置线程超时时间并启动定时器
rt_timer_control(&(thread->thread_timer), RT_TIMER_CTRL_SET_TIME, &timeout);
rt_timer_start(&(thread->thread_timer));
}
/* 开中断 */
rt_hw_interrupt_enable(level);
/* 发起一次线程调度 */
rt_schedule();
...
// 返回接收到的事件
if (recved)
*recved = thread->event_set;
}
...
}
4 RT-Thread 学习记录之线程
https://blog.csdn.net/m0_46430715/article/details/126319773?spm=1001.2014.3001.5502
RT-Thread 大致是采用定时器、中断和链表来执行线程调度。
RT-Thread 线程的 5 种状态:创建态、就绪态、运行态、挂起态、终止态。线程调度就是使得线程在这 5 种状态中切换。
4.1 线程控制块结构体
线程控制块结构体:包含 1)线程名称;2)用于链接线程之间的链表;3)记录线程栈的相关变量(如栈底地址、栈顶指针、栈大小);4)记录线程入口的相关变量(如入口地址、参数等);5)用于抢占的线程优先级;6)用于时间片调度的相关变量(如初始滴答数、剩余滴答数、定时器)、7)用于同步的记录线程感兴趣的事件变量
struct rt_thread
{
char name[RT_NAME_MAX]; // 线程名称
rt_list_t list; // 对象链表
/* 阻塞线程之间的链表(?) */
rt_list_t tlist; // 线程链表,
/* 记录线程栈的相关变量 */
void *sp; // 栈指针
void *stack_addr; // 栈地址指针
rt_uint32_t stack_size; // 栈大小
/* 记录线程入口的相关变量 */
void *entry; // 入口地址
void *parameter; // 参数
rt_uint8_t stat; // 线程状态
/* 记录线程优先级的变量 */
rt_uint8_t current_priority; // 当前优先级
rt_uint8_t init_priority; // 初始优先级
/* 记录线程时间片相关的变量 */
rt_ubase_t init_tick; // 初始滴答数
rt_ubase_t remaining_tick; // 剩余滴答数
struct rt_timer thread_timer; // 内置线程定时器
/* 记录线程感兴趣的事件变量 */
rt_uint32_t event_set;
rt_uint8_t event_info;
}
4.1 静态初始化线程过程
初始化线程,首先会调用内核对象初始函数 rt_object_init()
将线程对象初始化注册到系统管理器,然后用内置静态初始化线程函数 _rt_thread_init()
将线程属性记录到线程控制块中。
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)
{
/* 将线程对象注册到系统管理器 */
rt_object_init((rt_object_t)thread, RT_Object_Class_Thread, name);
return _rt_thread_init(thread,name, entry, parameter,stack_start,
stack_size,priority, tick);
}
内置静态初始化线程函数_rt_thread_init()` 将线程属性记录到线程控制块,包括:
1)初始化线程链表(链表只有自身,头尾指针指向自己);
2)指定线程入口函数和参数;
3)初始化线程栈;
4)初始化线程时间片;
5)初始化线程内置定时器;
6)绑定钩子函数
4.2 线程恢复
线程恢复函数rt_thread_resume()
核心操作是将线程从阻塞线程链表移除,并插入就绪链表,设置链表状态为就绪态。
rt_err_t rt_thread_resume(rt_thread_t thread)
{
...
/* 将该线程从链表移除 */
rt_list_remove(&(thread->tlist));
/* 将该线程插入就绪链表,线程状态将被设置为就绪并从挂起链表中删除。 */
rt_schedule_insert_thread(thread);
...
return RT_EOK;
}
4.3 线程启动(核心)
线程启动函数 rt_thread_up()
主要动作是改变线程状态字,先从初始态切换为挂起态,然后调用线程恢复函数 rt_resume()
将挂起态切换为就绪态。最后做一次调度 rt_schedule()
。
rt_err_t rt_thread_startup(rt_thread_t thread)
{
...
/* 改变线程状态为挂起状态 */
thread->stat = RT_THREAD_SUSPEND;
/* 线程恢复,将线程切换到就绪状态 */
rt_thread_resume(thread);
/* 获得当前执行的线程句柄 */
if (rt_thread_self() != RT_NULL)
{
/* 做一次调度 */
rt_schedule();
}
return RT_EOK
}
- 【总结】线程启动流程如下:1)创建线程/初始化线程;2)调用线程启动函数
rt_thread_startup()
;3)将线程状态从初始态切换为挂起态;4)调用线程恢复函数rt_thread_resume()
将线程从线程挂起链表移除,并插入线程就绪链表,设置线程的状态为就绪态;5)线程启动成功
5 RT-Thread 学习记录之线程调度
参考链接1:https://blog.csdn.net/m0_46430715/article/details/126558101?spm=1001.2014.3001.5502
参考链接2:https://blog.csdn.net/weixin_44746581/article/details/109055194
RT-Thrread 采用基于优先级的抢占式线程调度,它通过维护一个队列数组,即优先级表rt_thread_priority_table[RT_THREAD_PRIORITY_MAX]
来实现线程优先级管理系统。如下图所示,每个优先级队列采用双向环形链表链接。
5.1 调度器初始化
调度器初始化rt_system_scheduler_init()
核心动作就是初始化优先级表里的所有双向队列。
void rt_system_scheduler_init(void)
{
...
/* 所有优先级对应的队列数组初始化 */
for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++)
{
rt_list_init(&rt_thread_priority_table[offset]);
}
/* 初始化就绪链表 */
rt_memset(rt_thread_ready_table, 0, sizeof(rt_thread_ready_table));
}
5.1 调度器启动(系统初始化调度器时启动)
调度器启动函数rt_system_scheduler_start()
发生在系统初始化线程调度器之时。调度器启动函数rt_system_scheduler_start()
即在就绪线程链表里,按照优先级策略,挑选一个线程启动。
具体步骤如下:1)查找出新的最高优先级线程;2)切换该线程;3)从就绪链表中移除此线程,并修改线程的状态;4)通过上下文切换函数,切换到目标线程。
void rt_system_scheduler_start(void)
{
...
// 查找出新的最高优先级线程
to_thread = _get_highest_priority_thread(&highest_ready_priority);
// 切换该线程
rt_current_thread = to_thread;
// 从就绪链表中移除此线程,并修改线程的状态
rt_schedule_remove_thread(to_thread);
to_thread->stat = RT_THREAD_RUNNING;
// 切换到目标线程
rt_hw_context_swith_to((rt_ubase_t)&to_thread->sp);
}
5.2 调度器调度(系统运行时调度)
调度器调度一次 rt_schedule()
,即是选择一个最高优先级的就绪线程,并立即切换到该进程。
6 RT-Thread 学习记录之 IPC (线程间同步)
https://blog.csdn.net/m0_46430715/article/details/126690452?spm=1001.2014.3001.5502
7 RT-Thread 启动过程
参考链接:https://blog.csdn.net/weixin_44746581/article/details/108659183?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171370552216800222820810%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=171370552216800222820810&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_ecpm_v1~rank_v31_ecpm-6-108659183-null-null.nonecase&utm_term=RT-Thread&spm=1018.2226.3001.4450
8、RT-Thread 定时器机制
标签:RT,rt,优先级,Thread,thread,线程,专栏,event From: https://www.cnblogs.com/MasterBean/p/18149535