目前网上有许多讲解RT-Thread 的IPC(信号量、互斥量、事件、邮箱、队列)相关文档,但仅停留在API的使用,鲜有从源码角度讲解其实现原理。野火出版的《RT-Thread内核实现与应用开发实战指南》不仅讲解了线程调度等实现原理,还讲解了IPC的实现原理,本文仅仅是作为学习笔记来简短叙述下IPC的实现原理,想深入学习的话可以参考野火的这本书,也可以直接阅读源码。下文涉及的代码做了适当删减,例如入参检查、钩子函数等。
1 线程内置定时器
2 IPC父类对象
2.1 IPC父类数据结构
2.2 IPC父类方法
3 信号量
3.1 信号量数据结构
3.2 初始化信号量
3.3 请求信号量
3.4 释放信号量
4 互斥量
4.1 互斥量数据结构
4.2 初始化互斥量
4.3 请求互斥量
4.4 释放互斥量
5 事件
5.1 事件数据结构
5.2 初始化事件
5.3 发送事件
5.4 接收事件
6 邮箱
6.1 邮箱数据结构
6.2 初始化邮箱
6.3 发送邮件
6.4 接收邮件
7 消息队列
7.1 消息队列数据结构
7.2 初始化消息队列
7.3 发送消息队列
7.4 接收消息队列
为什么要从线程内置定时器讲起?因为RT-Thread的IPC是依靠线程内置定时器实现的,这里简单叙述下线程内置定时器,想要具体深入这部分的知识,可以参考《RT-Thread内核实现与应用开发实战指南》的第11章——定时器的实现。在RT-Thread(以下简称RTT)中线程定义中,有一个struct rt_timer thread_timer成员,即线程内置定时器,如下面的程序所示。
struct rt_thread
{
char name[RT_NAME_MAX]; /* 线程的名字 */
rt_uint8_t type; /* 类型 */
rt_uint8_t flags; /* 标志位 */
rt_list_t list; /* 对象链表节点 */
rt_list_t tlist; /* 线程链表节点*/
void *sp; /* 线程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;
#if defined(RT_USING_EVENT)
rt_uint32_t event_set; /* 线程等待的事件 */
rt_uint8_t event_info; /* 线程事件参数 */
#endif
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; /* 用户私有数据指针 */
};
在RTT中,使用了双向链表来管理定时器,在启动一个定时器时,RTT会根据该定时器的超时时间来将该节点插入合适的位置,即根据超时时间按升序排序。
RTT的定时器有硬件定时器(系统定时器)和软件定时器之分,这里的硬件定时器一般就是指systick,在systick中断里会轮询硬件定时器链表,如果链表的第一个节点没有超时,那么后面的节点必定没有超时,如果定时器超时了,就会调用定时器的超时回调函数(在中断里直接调用回调,因此回调函数应尽量简短)。软件定时器的实现机制和硬件定时器大致相似,也是根据超时时间做升序排序处理,不同的是软件定时器是创建了一个系统定时器线程来处理软件定时器的超时,但是系统定时器线程所实际上要依靠硬件定时器来及时唤醒系统定时器线程。
言归正传,线程内置定时器用于实现线程挂起等待功能,包含线程休眠、等待信号量等。比如当调用rt_thread_delay时,RTT会先当前线程挂起,然后设置线程内置定时器的超时时间为delay的时间,并开启该定时器。
rt_err_t rt_thread_delay(rt_tick_t tick)
{
return rt_thread_sleep(tick);
}
rt_err_t rt_thread_sleep(rt_tick_t tick)
{
register rt_base_t temp;
struct rt_thread *thread;
/* 获取当前线程 */
thread = rt_thread_self();
/* 关闭中断*/
temp = rt_hw_interrupt_disable();
/* 挂起线程 */
rt_thread_suspend(thread);
/* 设置线程内置定时器的超时时间,并启动 */
rt_timer_control(&(thread->thread_timer), RT_TIMER_CTRL_SET_TIME, &tick);
rt_timer_start(&(thread->thread_timer));
/* 开启中断*/
rt_hw_interrupt_enable(temp);
rt_schedule();
/* 执行到这里时,线程的延时已经结束,超时回调会将thread->error置为-RT_ETIMEOUT,
* rt_thread_sleep导致的超时不应该算错误状态,所以这里手动改成RT_EOK
*/
if (thread->error == -RT_ETIMEOUT)
thread->error = RT_EOK;
return RT_EOK;
}
当systick中断里轮询到该定时器超时时便会调用定时器超时回调。我们已经可以大致猜到这个超时回调会做什么了吧?应该就是该线程从挂起队列中移除并放入就绪队列。rt_thread_timeout函数就是所有线程(空闲线程除外,因为本身不允许阻塞)内置定时器的超时回调!所以说,线程内置定时器用于实现线程阻塞等待功能,包含线程休眠、等待信号量等,换句话来说就是线程在休眠前给自己定了一个何时醒来的闹钟。扯了这么多,实际上是想说IPC的实现和rt_thread_sleep是相似的,例如rt_sem_take如果有超时,当获取不到sem时会导致线程挂起并开启内置定时器,只不过当对应的sem被释放时,等待的线程会被提早唤醒并关闭线程内置定时器。
void rt_thread_timeout(void *parameter)
{
struct rt_thread *thread;
thread = (struct rt_thread *)parameter;
/* 设置线程状态为超时 */
thread->error = -RT_ETIMEOUT;
/* 从挂起队列中移除自身节点 */
rt_list_remove(&(thread->tlist));
/* 插入到就绪队列 */
rt_schedule_insert_thread(thread);
/* 启动调度 */
rt_schedule();
}
RTT的整个实现都采用了面向对象的思想,包括IPC的实现。RTT实现了一个IPC类,IPC类是从rt_object类继承而来的,而信号量、互斥量等子类又是继承了IPC类,这种面向对象的编程思想很大程度上避免了重复造轮子,提升了代码的重用性。
子类继承父类时,会继承父类的(非私有)变量和方法,本章节叙述的是IPC父类的变量和方法。
下面是rt_ipc_object的数据结构,可以看到,rt_ipc_object继承自rt_object。rt_object包括名字、类型、标志位、节点这些基本参数;suspend_thread则是因该IPC而挂起的线程链表节点,实际上是首节点。线程有一个成员tlist(可以看第一章节的struct rt_thread定义),当线程因该IPC而挂起时,线程的tlist节点将挂在suspend_thread链表上。简单来说,suspend_thread就是用于记录有哪些线程因该IPC而阻塞。
struct rt_object
{
char name[RT_NAME_MAX]; /* 对象的名字 */
rt_uint8_t type; /* 对象的类型*/
rt_uint8_t flag; /* 标志位,用于记录一些设置*/
rt_list_t list; /* 对象链表节点 */
};
struct rt_ipc_object
{
struct rt_object parent; /* 从rt_object继承的父类 */
rt_list_t suspend_thread; /* 因此而挂起的线程链表节点 */
};
接下来我们来看一下IPC父类的方法,由于比较简单,所以下面将实现的原理作为注释和代码放在一起,不再额外叙述了。
/* 初始化一个rt_ipc_object父类 */
rt_inline rt_err_t rt_ipc_object_init(struct rt_ipc_object *ipc)
{
/* 初始化suspend_thread链表,即head和tail都指向自己 */
rt_list_init(&(ipc->suspend_thread));
/* 这里不初始化rt_ipc_object 的父类,而是交给rt_ipc_object的子类来实现 */
return RT_EOK;
}
/* 将线程挂起在某个链表节点上 */
rt_inline rt_err_t rt_ipc_list_suspend(rt_list_t *list,
struct rt_thread *thread,
rt_uint8_t flag)
{
/* 挂起线程 */
rt_thread_suspend(thread);
/*
* RTT的IPC有2中方式:FIFO模式和PRIO模式
* FIFO模式即先入先出模式,最先请求的线程将优先获得IPC
* PRIO模式即优先级模式,正在等待的优先级最高的线程将获得IPC
*/
switch (flag)
{
case RT_IPC_FLAG_FIFO:
/* 插入到list链表首节点的前面,即插入到list链表的末尾 */
rt_list_insert_before(list, &(thread->tlist));
break;
case RT_IPC_FLAG_PRIO:
{
struct rt_list_node *n;
struct rt_thread *sthread;
/* 根据优先级来插入到合适的位置 */
for (n = list->next; n != list; n = n->next)
{
sthread = rt_list_entry(n, struct rt_thread, tlist);
/* 根据优先级来查找,优先级越高越靠前*/
if (thread->current_priority < sthread->current_priority)
{
rt_list_insert_before(&(sthread->tlist), &(thread->tlist));
break;
}
}
/* 如果找不到合适的位置,就退化成FIFO模式,比如所有线程的优先级相同的情况 */
if (n == list)
rt_list_insert_before(list, &(thread->tlist));
}
break;
}
return RT_EOK;
}
/* 恢复IPC挂起链表的第一个线程 */
rt_inline rt_err_t rt_ipc_list_resume(rt_list_t *list)
{
struct rt_thread *thread;
/*
* 获取list链表的第一个挂起线程指针
* rt_list_entry实际上就是rt_container_of宏,这个宏在linux和RTT里大量使用
* 其作用是根据结构体成员的某一地址来找到结构体的首地址
*/
thread = rt_list_entry(list->next, struct rt_thread, tlist);
/*
* 恢复该线程,rt_thread_resume内部会将thread的tlist节点从对应的挂起链表
* 中移除,并关闭线程内置定时器
*/
rt_thread_resume(thread);
return RT_EOK;
}
/* 恢复IPC挂起链表的所有线程 */
rt_inline rt_err_t rt_ipc_list_resume_all(rt_list_t *list)
{
struct rt_thread *thread;
register rt_ubase_t temp;
/* 遍历链表唤醒因该IPC而挂起的所有线程 */
while (!rt_list_isempty(list))
{
/* 关闭中断 */
temp = rt_hw_interrupt_disable();
/* 获取链表的下一个节点*/
thread = rt_list_entry(list->next, struct rt_thread, tlist);
/* 设置线程错误状态为错误 */
thread->error = -RT_ERROR;
/* 恢复该线程,rt_thread_resume内部会将thread的tlist节点从对应的挂起链表中移除 */
rt_thread_resume(thread);
/* 开启中断 */
rt_hw_interrupt_enable(temp);
}
return RT_EOK;
}
有了IPC父类的最基本的挂起和恢复功能,就很容易衍生出各种IPC子类了。
信号量常用作线程同步、资源计数等场景,信号量的实现是所有的IPC中最简单的,从下面信号量的数据结构就可以看出,信号量是IPC的子类,仅仅在IPC的基础上增加了一个value变量,value即信号量的值,当value只能为0或1时,这种信号量就称为二值信号量。
struct rt_semaphore
{
struct rt_ipc_object parent; /* 继承自IPC父类 */
rt_uint16_t value; /* 信号量的值 */
};
RTT提供的API从申请和释放方式来说,可以分为两类:静态和动态。静态API即函数名包含init、detach的API,其所需的内存由用户提供;动态即函数名包含create、delete,其所需内存由动态内存来管理。这两类API的实现除了内存管理方式不同,在实现上几乎是一样的,因此下面仅选取静态方式作讲解。
信号量在初始化时可以设置初值,一般在使用时,这个初值就代表的资源的多少。下面的函数是信号量的初始化与卸载。
/* 初始化信号量 */
rt_err_t rt_sem_init(rt_sem_t sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag)
{
/* 初始化“爷爷”类,即object类 */
rt_object_init(&(sem->parent.parent), RT_Object_Class_Semaphore, name);
/* 初始化IPC父类 */
rt_ipc_object_init(&(sem->parent));
/* 设置信号量的初始值 */
sem->value = value;
/* 设置object类的属性,这里一般选择FIFO模式或者PRIO模式 */
sem->parent.parent.flag = flag;
return RT_EOK;
}
/* 卸载信号量 */
rt_err_t rt_sem_detach(rt_sem_t sem)
{
/* 唤醒因该信号量而挂起的所有线程 */
rt_ipc_list_resume_all(&(sem->parent.suspend_thread));
/* 将对象节点从对象链表中移除 */
rt_object_detach(&(sem->parent.parent));
return RT_EOK;
}
请求信号量函数rt_sem_take函数如下所示,其原理比较简单,如果信号量可用,即value大于0,那么表示成功获取到信号量,同时value减1;如果信号量不可用,即value等于0,那么需要判断入参time,time为0表示不等待立即返回,time不为0则挂起线程并启动线程内置定时器,如果看懂了第1章和第2章的内容,这里是一眼就能看出来的。
需要注意的是,RT_DEBUG_IN_THREAD_CONTEXT这个宏定义会检查当前是否处于线程环境,这就说明不建议在中断服务函数里请求信号量,一旦信号量不可用,而且超时时间不为0,就会发生错误。一般使用的情景都是在中断服务函数里释放信号量来同步线程。
/* 获取信号量 */
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time)
{
register rt_base_t temp;
struct rt_thread *thread;
temp = rt_hw_interrupt_disable();
/* 如果信号量不为0,信号量减1并立即返回 */
if (sem->value > 0)
{
sem->value --;
rt_hw_interrupt_enable(temp);
}
/* 如果信号量已经为0,那么就要看是否设置了超时 */
else
{
/* 如果超时时间为0,那么立即返回超时 */
if (time == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_ETIMEOUT;
}
/* 超时时间不为0,需要挂起线程进行等待 */
else
{
/* 检查当前是否处于RTOS运行状态,即检查是否已经初始化过RTT */
RT_DEBUG_IN_THREAD_CONTEXT;
/* 获取当前线程*/
thread = rt_thread_self();
/* 复位线程错误标志*/
thread->error = RT_EOK;
/* 将线程挂起,并记录到信号量的挂起链表上 */
rt_ipc_list_suspend(&(sem->parent.suspend_thread),
thread,
sem->parent.parent.flag);
/* 判断超时时间是否为永久(-1),若非永久则启动线程内置定时器 */
if (time > 0)
{
/* 设置线程内置定时器的超时时间为信号量的等待时间,并启动 */
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&time);
rt_timer_start(&(thread->thread_timer));
}
rt_hw_interrupt_enable(temp);
/* 发起线程调度 */
rt_schedule();
if (thread->error != RT_EOK)
{
/* 如果执行到这里,说明一直没有等到信号量,超时了 */
return thread->error;
}
}
}
return RT_EOK;
}
/* 尝试获取信号量,非阻塞 */
rt_err_t rt_sem_trytake(rt_sem_t sem)
{
/* 尝试获取信号量:超时时间为0,当获取不到时也会立即返回 */
return rt_sem_take(sem, 0);
}
释放信号量函数非常简短,如果释放时suspend_thread链表非空,说明有线程因请求该信号量被挂起,那么就唤醒suspend_thread链表的第一个节点的线程;反之,只需要简单地让信号量的value值加1即可。
/* 释放信号量 */
rt_err_t rt_sem_release(rt_sem_t sem)
{
register rt_base_t temp;
register rt_bool_t need_schedule;
need_schedule = RT_FALSE;
temp = rt_hw_interrupt_disable();
/* 释放信号量时如果有线程因此信号量而挂起,那么需要立即发起线程调度,否则将信号量加1 */
if (!rt_list_isempty(&sem->parent.suspend_thread))
{
/* 恢复信号量挂起链表的第一个节点对应的线程 */
rt_ipc_list_resume(&(sem->parent.suspend_thread));
need_schedule = RT_TRUE;
}
else
sem->value ++; /* 信号量加1 */
rt_hw_interrupt_enable(temp);
/* 如果需要调度,那么发起线程调度*/
if (need_schedule == RT_TRUE)
rt_schedule();
return RT_EOK;
}
使用信号量作为公共资源保护时,会存在线程优先级反转问题,互斥量这类IPC就是为了解决这个问题而诞生的,RTT的互斥量采用了临时提升占有互斥量线程优先级的方法,即优先级继承,在后面的代码分析中会有所体现。
互斥量的数据结构如下所示,value的作用和信号量一样,但其实value的值只会是0或1。original_priority用于记录占有该信号量的原始优先级,因为当互斥量被一个低优先级的线程占有后,当有高优先级线程因请求该互斥量而挂起时,会将这个低优先级的线程的优先级提升到和高优先级线程一致,当互斥量被原先的低优先级线程释放后,要将该线程的优先级还原到原先的低优先级。hold成员要和owner成员一起来看,owner记录了当前占有该信号量的线程,从这里就可以看出互斥量和线程的一种所属关系,即owner只能被单一的某个线程占有,而不能被同时占有。hold变量就是用来记录所属线程占有该互斥量的次数,简单来说,当hold为0时,owner指向空,value为1;当hold不为0时,owner必定指向某个线程,value为0。
struct rt_mutex
{
struct rt_ipc_object parent; /* 继承自IPC父类 */
rt_uint16_t value; /* 互斥量的值 */
rt_uint8_t original_priority; /* 占有该互斥量的原始优先级 */
rt_uint8_t hold; /* 线程占有该信号量的次数*/
struct rt_thread *owner; /* 占有该互斥量的线程 */
};
rt_mutex_init函数用于初始化一个互斥量,可以和rt_sem_init函数对比一下,可以发现互斥量在初始化时,其value值是固定为1,而信号量则由初始化函数入参设置。互斥量初始化时,还没有归属线程,因此owner指向空,hold为0。
rt_err_t rt_mutex_init(rt_mutex_t mutex, const char *name, rt_uint8_t flag)
{
/* 初始化“爷爷”类,即object类 */
rt_object_init(&(mutex->parent.parent), RT_Object_Class_Mutex, name);
/* 初始化IPC父类 */
rt_ipc_object_init(&(mutex->parent));
mutex->value = 1;
mutex->owner = RT_NULL;
mutex->original_priority = 0xFF;
mutex->hold = 0;
/* 设置object类的属性,这里一般选择FIFO模式或者PRIO模式 */
mutex->parent.parent.flag = flag;
return RT_EOK;
}
互斥量的请求相较于信号量的请求要稍复杂些,代码如下,从rt_mutex_take函数源码中我们可以分析出以下几点:
1.不允许在中断里请求互斥量,即使设置的超时时间为0。这是因为互斥量和线程具有归属关系,当成功请求到互斥量时,互斥量需要重新记录当前被哪个线程所占有,如果中断成功请求到了互斥量,那么还怎么记录呢?因为中断根本不是线程啊!
2.请求互斥量会导致hold值加1,hold具体在何时使用要看互斥量释放函数rt_mutex_release,下文会提到。
3.互斥量的值在非0时,互斥量必定被某个线程占有,结合分析rt_mutex_release时将会得出互斥量的值只能是0或1的结论。
4.若请求互斥量的线程的优先级比已占有互斥量的线程的优先级高,且前者因此被挂起,那么后者的优先级将被提升到和前者一样高。
/* 请求互斥量 */
rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time)
{
register rt_base_t temp;
struct rt_thread *thread;
/* this function must not be used in interrupt even if time = 0 */
/* 上面的一段英文是官方的注释,很重要! */
RT_DEBUG_IN_THREAD_CONTEXT; /* 确保当前处于线程中而不是中断服务函数里 */
/* 获取当前线程 */
thread = rt_thread_self();
temp = rt_hw_interrupt_disable();
thread->error = RT_EOK;
if (mutex->owner == thread) /* 说明是占有该互斥量的线程请求的,hold值加1 */
{
mutex->hold ++;
}
else /* 是其他线程请求的 */
{
__again:
/* 如果value大于0,说明互斥量还没有被请求过,也就是还没有被任何线程占有 */
if (mutex->value > 0)
{
/* 成功请求到互斥量,value = 0 */
mutex->value --;
/* 记录被哪个线程占有,并记录这个线程的当前优先级 */
mutex->owner = thread;
mutex->original_priority = thread->current_priority;
mutex->hold ++;
}
else /* 到此处说明互斥量已经被其他线程占有,不可用 */
{
if (time == 0) /* 超时时间为0,立即返回超时 */
{
thread->error = -RT_ETIMEOUT;
rt_hw_interrupt_enable(temp);
return -RT_ETIMEOUT;
}
else /* 设置了超时时间,需要挂起当前线程 */
{
/* 如果当前请求的线程的优先级比互斥量占有线程的优先级高,那么提升后者的优先级*/
if (thread->current_priority < mutex->owner->current_priority)
{
/* 将互斥量占有线程的优先级调到和当前请求的线程的优先级一样高 */
rt_thread_control(mutex->owner,
RT_THREAD_CTRL_CHANGE_PRIORITY,
&thread->current_priority);
}
/* 将当前线程挂起在该互斥量的suspend_thread链表上 */
rt_ipc_list_suspend(&(mutex->parent.suspend_thread),
thread,
mutex->parent.parent.flag);
/* 判断超时时间是否为永久(-1),若非永久则启动线程内置定时器 */
if (time > 0)
{
/* 设置线程内置定时器的超时时间并启动 */
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&time);
rt_timer_start(&(thread->thread_timer));
}
rt_hw_interrupt_enable(temp);
/* 发起线程调度,当前线程至此被挂起 */
rt_schedule();
/* 因某种原因线程被唤醒,判断线程错误状态 */
if (thread->error != RT_EOK)
{
/* 被信号打断,回到__again的位置重新判断请求状态 */
if (thread->error == -RT_EINTR)
goto __again;
return thread->error;
}
else
{
/* 运行到这里说明成功获取到互斥量了 */
/* 这里关闭中断是为了和最下面的rt_hw_interrupt_enable成对调用*/
temp = rt_hw_interrupt_disable();
}
}
}
}
rt_hw_interrupt_enable(temp);
return RT_EOK;
}
互斥量的释放相较于信号量的释放也要稍复杂些,代码如下,从rt_mutex_release函数源码中我们可以分析出以下几点:
1.不允许在中断里释放互斥量,因为互斥量只能由互斥量的占有线程释放。
2.释放互斥量会导致hold值减1,当hold值减到0时,才允许互斥量被另一个线程占有或不被任何线程占有。
3.互斥量的值只能0或1,即互斥量是一种特殊的二值信号量。
4.互斥量被成功释放需要易主时,如果当前线程(互斥量的占有线程)优先级曾经被提升过,优先级会被改回来。
/* 释放互斥量 */
rt_err_t rt_mutex_release(rt_mutex_t mutex)
{
register rt_base_t temp;
struct rt_thread *thread;
rt_bool_t need_schedule;
need_schedule = RT_FALSE;
/* only thread could release mutex because we need test the ownership */
RT_DEBUG_IN_THREAD_CONTEXT;
thread = rt_thread_self();
temp = rt_hw_interrupt_disable();
/* 只有互斥量的占有线程能释放该互斥量,否则返回错误 */
if (thread != mutex->owner)
{
thread->error = -RT_ERROR;
rt_hw_interrupt_enable(temp);
return -RT_ERROR;
}
/* hold减1 */
mutex->hold --;
/* hold为0时,互斥量才能易主或无主 */
if (mutex->hold == 0)
{
/* 如果互斥量的占有线程优先级被修改过,在此处修改回来 */
if (mutex->original_priority != mutex->owner->current_priority)
{
rt_thread_control(mutex->owner,
RT_THREAD_CTRL_CHANGE_PRIORITY,
&(mutex->original_priority));
}
/* 如果当前有线程因请求该互斥量而挂起,恢复suspend_thread链表第一个节点线程 */
if (!rt_list_isempty(&mutex->parent.suspend_thread))
{
thread = rt_list_entry(mutex->parent.suspend_thread.next,
struct rt_thread,
tlist);
/* 互斥量易主 */
mutex->owner = thread;
mutex->original_priority = thread->current_priority;
mutex->hold ++;
/* 唤醒线程 */
rt_ipc_list_resume(&(mutex->parent.suspend_thread));
/* 因为有线程需要被唤醒,所以需要调度一次 */
need_schedule = RT_TRUE;
}
/* 没有线程在请求该互斥量,互斥量改为无主状态,即初始状态 */
else
{
/* value加1,实际上就是等于1 */
mutex->value ++;
mutex->owner = RT_NULL;
mutex->original_priority = 0xff;
}
}
rt_hw_interrupt_enable(temp);
/* 如果需要调度,那么立即调度 */
if (need_schedule == RT_TRUE)
rt_schedule();
return RT_EOK;
}
在裸机中,我们常用标志位的方式来做异步通知,例如在中断服务函数里置标志,在主循环中轮询标志做事件的处理。在RTT中,也有这样的机制,并且融合了IPC功能,已经给我们封装的很完善了。
事件的数据结构如下所示,从数据结构上来看,事件和信号量比较接近。事件的set值用于记录事件,而信号量的value值用于记录资源的多少。如果使用二值信号量来做线程的同步,此时用单个事件也可以实现,即事件和信号量存在部分交集,具体用事件还是信号量取决于具体应用。
set是rt_uint32_t类型的,也就意味着RTT的线程最多可以同时等待32个事件。此外,还需要线程的定义,RTT的线程定义中有2个事件相关的变量event_set和event_info(可以从第1章看到),event_set即线程在等待的事件集,event_info用于记录事件的参数,例如以何种方式等待事件、接收到事件是否需要自动清空事件。
RTT的线程等待事件具有两种等待方式:“与”、“或”。当线程请求多个事件时,如果以“与”的方式请求,只有这些事件全都发生时,线程才会被唤醒;如果以“或”的方式请求,那么只要等待的任意一个事件发生时,线程就会被唤醒。RTT的事件实现原理:通过event_info中的运算方式(“与或”)比对事件的set值和线程的event_set值来判断线程是否获得了所期望的事件。
struct rt_event
{
struct rt_ipc_object parent; /* 继承自IPC父类 */
rt_uint32_t set; /* 事件集,每个bit位记录一个事件*/
};
typedef struct rt_event *rt_event_t;
初始化事件没啥好说的,和信号量的初始化是一样的套路。
/* 初始化事件 */
rt_err_t rt_event_init(rt_event_t event, const char *name, rt_uint8_t flag)
{
/* 初始化“爷爷”类,即object类 */
rt_object_init(&(event->parent.parent), RT_Object_Class_Event, name);
/* 设置object类的属性,这里一般选择FIFO模式或者PRIO模式 */
event->parent.parent.flag = flag;
/* 初始化IPC父类 */
rt_ipc_object_init(&(event->parent));
/* 初始化时没有事件,事件集清零 */
event->set = 0;
return RT_EOK;
}
发送事件时,事件会被记录到event的set中,然后遍历其IPC挂起链表,如果set与线程的event_set值相匹配,那么就会唤醒该线程。
通过以下源码的分析,可以得出以下几点需要注意的点:
1.不允许发送空事件,即set的值不能为0;但允许发送多个事件,即RTT没有要求set的值仅有1个bit位置1。
2.当线程以“或”的方式等待事件时,若成功等到了事件,线程记录的event_set会被替换成最近一次事件的值。
3.当线程设置了RT_EVENT_FLAG_CLEAR且成功等到了事件时,会将event的对应的set值清除。这里就需要特别注意了!当有多个线程都在等待同一事件时,第一个线程成功等到事件时就已经把set对应的位清零了,后面的线程会等不到这个事件。
/* 发送事件 */
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set)
{
struct rt_list_node *n;
struct rt_thread *thread;
register rt_ubase_t level;
register rt_base_t status; /* 标记线程是否请求到了期望的事件集 */
rt_bool_t need_schedule; /* 标记是否需要发起线程调度 */
/* 不允许发送空事件 */
if (set == 0)
return -RT_ERROR;
need_schedule = RT_FALSE;
level = rt_hw_interrupt_disable();
/* 记录当前发生的事件 */
event->set |= set;
/* 以下遍历因请求该事件集而挂起的线程链表 */
if (!rt_list_isempty(&event->parent.suspend_thread))
{
/* search thread list to resume thread */
n = event->parent.suspend_thread.next;
while (n != &(event->parent.suspend_thread))
{
/* 获取线程句柄 */
thread = rt_list_entry(n, struct rt_thread, tlist);
status = -RT_ERROR;
/* 如果线程以“与”的方式请求事件集,进一步判断是否收到了全部所期望的事件集 */
if (thread->event_info & RT_EVENT_FLAG_AND)
{
if ((thread->event_set & event->set) == thread->event_set)
{
/* received an AND event */
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;
/* received an OR event */
status = RT_EOK;
}
}
/* 获取链表下一节点 */
n = n->next;
/* 线程成功请求到了期望的事件集 */
if (status == RT_EOK)
{
/*
* 如果线程设置了接收事件成功后自动清空事件集,
* 那么清空线程所期望的事件集,否则下次请求时就会立即请求到
*/
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_schedule();
return RT_EOK;
}
在线程请求事件时,即调用rt_event_recv时,首先会立即检查一下当前的事件集是否已经满足线程的期望,如果已经满足,那么相当于已经成功等到了期望的事件,否则就需要等待。如果等待时间为0,那么立即返回超时;如果等待时间不为0,那么挂起线程等待期望的事件集发生。需要注意的是,当用RT_EVENT_FLAG_CLEAR方式等待事件且成功时,RTT会帮我们清空event->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)
{
struct rt_thread *thread;
register rt_ubase_t level;
register rt_base_t status;
/* 只能在线程环境中等待事件 */
RT_DEBUG_IN_THREAD_CONTEXT;
/* 不能设置等待空事件 */
if (set == 0)
return -RT_ERROR;
status = -RT_ERROR;
thread = rt_thread_self();
thread->error = RT_EOK;
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
{
/* 只能用“与”或者“或”这两种方式之一来请求事件 */
RT_ASSERT(0);
}
/ 已经成功请求到事件 */
if (status == RT_EOK)
{
/* 将接收到的事件通过recved指针传回 */
if (recved)
*recved = (event->set & set);
/* 以“或”的方式请求事件,成功后需要清空对应的事件位,否则相当于等待的事件一直发生 */
if (option & RT_EVENT_FLAG_CLEAR)
event->set &= ~set;
}
/* 还没有等到的期望的事件集发生,判断是否设置了超时等待 */
else if (timeout == 0)
{
/* 不等待,立即返回超时 */
thread->error = -RT_ETIMEOUT;
}
/* 还没有等到的期望的事件集发生,且设置了超时,那么挂起线程进行超时等待 */
else
{
/* 设置当前线程所期望的事件集和操作*/
thread->event_set = set;
thread->event_info = option;
/* 将线程挂起在事件的suspend_thread链表上 */
rt_ipc_list_suspend(&(event->parent.suspend_thread),
thread,
event->parent.parent.flag);
/* 判断超时时间是否为永久(-1),若非永久则启动线程内置定时器 */
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 (thread->error != RT_EOK)
{
/* return error */
return thread->error;
}
/* 成功等到了事件集发生了 */
level = rt_hw_interrupt_disable();
/* 将接收到的事件通过recved指针传回 */
if (recved)
*recved = thread->event_set;
}
rt_hw_interrupt_enable(level);
return thread->error;
}
RTT的邮箱可用于线程于线程之间、中断与线程之间的异步消息通信。
邮箱的数据结构如下所示,其中msg_pool是邮箱的内存地址,size是邮箱的总大小,注意这里的size不是指msg_pool内存的大小,而是值邮箱的容量,即能容纳多少封邮件,由于RTT的一封邮件的大小是4字节,因此所需的msg_pool的大小为4*size,这在邮箱初始化时要特别注意。entry用于记录当前邮箱中有多少封邮件,判断entry是否为0即可得知邮箱中是否有邮件,判断entry和size是否相等即可得知邮箱是否已经被塞满。简单地讲,邮箱就是一个rt_ubase_t类型的数组,数组的项数为size,当往邮箱投递一封邮件时,in_offset就会加1;当从邮箱读取一封邮件时,out_offset就会加1。也就是说in_offset和out_offset用于记录msg_pool数组当前的读写位置。当发送一封邮件时,如果邮箱已经被塞满且设置了超时时间,那么发送该邮件的线程将会被挂起在suspend_sender_thread链表上,直到邮箱空出了位置。
邮箱和信号量、互斥量、事件的其中一点区别需要特别注意:线程阻塞地发送或接收邮件时,即使中途被其他方式唤醒(线程内置定时器导致的超时除外),还会继续阻塞地请求发送或接收。这点在下面的分析中会体现出来。
因为邮箱只能存4字节的内容,所以邮箱适合传输一些精简的消息,当然,邮箱刚好可以传递一个指针,这样就可以传输大量的内容,但是指针指向的内容可能随时会被更改,所以这种情况下最好另外加保护锁。
struct rt_mailbox
{
struct rt_ipc_object parent; /* 继承自IPC父类 */
rt_ubase_t *msg_pool; /* 邮箱内存地址 */
rt_uint16_t size; /* 邮箱的总容量*/
rt_uint16_t entry; /* 当前邮箱中邮件的数量 */
rt_uint16_t in_offset; /* 记录邮箱写入偏移*/
rt_uint16_t out_offset; /* 记录邮箱读出偏移*/
rt_list_t suspend_sender_thread; /*因发送邮邮件而挂起的线程链表 */
};
typedef struct rt_mailbox *rt_mailbox_t;
/* 初始化邮箱 */
rt_err_t rt_mb_init(rt_mailbox_t mb,
const char *name,
void *msgpool,
rt_size_t size,
rt_uint8_t flag)
{
/* 初始化“爷爷”类,即object类 */
rt_object_init(&(mb->parent.parent), RT_Object_Class_MailBox, name);
/* 设置object类的属性,这里一般选择FIFO模式或者PRIO模式 */
mb->parent.parent.flag = flag;
/* 初始化IPC父类 */
rt_ipc_object_init(&(mb->parent));
/* 初始化邮箱参数 */
mb->msg_pool = msgpool;
mb->size = size;
mb->entry = 0;
mb->in_offset = 0;
mb->out_offset = 0;
/* 初始化邮件发送线程挂起链表 */
rt_list_init(&(mb->suspend_sender_thread));
return RT_EOK;
}
从rt_mb_init函数不容易看出传入的msgpool需要多大,可以参考一下rt_mb_create函数,在rt_mb_create中有如下操作,因此可以知道msg_pool所需的大小。
mb->size = size;
mb->msg_pool = RT_KERNEL_MALLOC(mb->size * sizeof(rt_ubase_t));
上文提到了当邮箱已经被塞满时,如果此时再往该邮箱塞邮件,那么发送邮件的线程可能被挂起,因此RTT中的发送邮件的函数名为rt_mb_send_wait。而rt_mb_send函数则是rt_mb_send_wait超时时间为0特殊情况,即当邮箱已经被塞满时,立即返回-RT_EFULL。
rt_mb_send_wait函数如下,从这个发送函数,我们可以得出以下几点结论:
1.不能在中断服务函数里以阻塞方式发送邮件。
2.当邮箱已满时,如果线程选择阻塞,那么会被挂起到邮箱的suspend_sender_thread链表上,在发送成功前,即使被其他方式(线程内置定时器除外)提前唤醒,唤醒后也依然会尝试等待邮件的发送。
rt_err_t rt_mb_send_wait(rt_mailbox_t mb,
rt_ubase_t value,
rt_int32_t timeout)
{
struct rt_thread *thread;
register rt_ubase_t temp;
rt_uint32_t tick_delta; /* 用于记录过去了多长时间 */
/* 初始化时间差 */
tick_delta = 0;
thread = rt_thread_self();
temp = rt_hw_interrupt_disable();
/* 如果邮箱已经被塞满,且不等待,那么立即返回-RT_EFULL */
if (mb->entry == mb->size && timeout == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
/* 邮箱已经塞满的情况 */
while (mb->entry == mb->size)
{
thread->error = RT_EOK;
/* 如果超时时间为0,立即返回-RT_EFULL */
if (timeout == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
/* 运行到这里,说明timeout不等于0,需要挂起当前线程 */
/* 检查当前是否处于线程环境中 */
RT_DEBUG_IN_THREAD_CONTEXT;
/* 将当前线程挂起在邮箱的suspend_sender_thread链表上 */
rt_ipc_list_suspend(&(mb->suspend_sender_thread),
thread,
mb->parent.parent.flag);
/* 判断超时时间是否为永久(-1),若非永久则启动线程内置定时器 */
if (timeout > 0)
{
/* 记录一下挂起的时刻 */
tick_delta = rt_tick_get();
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&timeout);
rt_timer_start(&(thread->thread_timer));
}
rt_hw_interrupt_enable(temp);
/* 线程调度 */
rt_schedule();
if (thread->error != RT_EOK)
{
/* 运行到这里,说明等待超时了 */
return thread->error;
}
temp = rt_hw_interrupt_disable();
/* 运行到这里,说明还没有等到邮箱有空位,但是线程因为其他原因被提前唤醒了 */
/* 如果超时时间不为永久,那么重新计算一下剩余超时等待时间并继续等待 */
if (timeout > 0)
{
tick_delta = rt_tick_get() - tick_delta; /* 计算上一次挂起到现在过去了多久 */
timeout -= tick_delta; /* 重新计算超时时长 */
if (timeout < 0)
timeout = 0;
}
}
/* 运行到这里,说明邮箱有空位了,发送邮件 */
mb->msg_pool[mb->in_offset] = value; /* 记录邮件 */
++ mb->in_offset; /* 写入偏移加1 */
if (mb->in_offset >= mb->size) /* 写到末尾了,回到初始位置 */
mb->in_offset = 0;
mb->entry ++; /* 邮箱中的邮件数量加1 */
/* 判断是否有线程因为请求邮箱而挂起,若有则恢复挂起链表上的第一个线程 */
if (!rt_list_isempty(&mb->parent.suspend_thread))
{
rt_ipc_list_resume(&(mb->parent.suspend_thread));
rt_hw_interrupt_enable(temp);
rt_schedule();
return RT_EOK;
}
rt_hw_interrupt_enable(temp);
return RT_EOK;
}
rt_mb_recv和rt_mb_send_wait非常“对称”,看懂了rt_mb_send_wait后,rt_mb_recv也就一目了然了。需要注意的是以下几点:
1.不能在中断服务函数里以阻塞方式接收邮件。
2.当邮箱为空时,如果线程选择阻塞,那么会被挂起到邮箱的suspend_thread链表上,在接收成功前,即使被其他方式(线程内置定时器除外)提前唤醒,唤醒后也依然会尝试等待邮件的接收。
/* 接收邮件 */
rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_ubase_t *value, rt_int32_t timeout)
{
struct rt_thread *thread;
register rt_ubase_t temp;
rt_uint32_t tick_delta; /* 用于记录过去了多长时间 */
/* 初始化时间差 */
tick_delta = 0;
thread = rt_thread_self();
temp = rt_hw_interrupt_disable();
/* 如果邮箱是空的,且不等待,那么立即返回-RT_ETIMEOUT*/
if (mb->entry == 0 && timeout == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_ETIMEOUT;
}
/* 邮箱是空的情况 */
while (mb->entry == 0)
{
thread->error = RT_EOK;
/* 如果超时时间为0,立即返回-RT_ETIMEOUT*/
if (timeout == 0)
{
rt_hw_interrupt_enable(temp);
thread->error = -RT_ETIMEOUT;
return -RT_ETIMEOUT;
}
/* 运行到这里,说明timeout不等于0,需要挂起当前线程 */
/* 检查当前是否处于线程环境中 */
RT_DEBUG_IN_THREAD_CONTEXT;
/* 将当前线程挂起在邮箱的suspend_thread链表上 */
rt_ipc_list_suspend(&(mb->parent.suspend_thread),
thread,
mb->parent.parent.flag);
/* 判断超时时间是否为永久(-1),若非永久则启动线程内置定时器 */
if (timeout > 0)
{
/* 记录一下挂起的时刻 */
tick_delta = rt_tick_get();
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&timeout);
rt_timer_start(&(thread->thread_timer));
}
rt_hw_interrupt_enable(temp);
/* 线程调度 */
rt_schedule();
if (thread->error != RT_EOK)
{
/* 运行到这里,说明等待超时了 */
return thread->error;
}
/* disable interrupt */
temp = rt_hw_interrupt_disable();
/* 运行到这里,说明邮箱还是空的,但是线程因为其他原因被提前唤醒了 */
/* 如果超时时间不为永久,那么重新计算一下剩余超时等待时间并继续等待 */
if (timeout > 0)
{
tick_delta = rt_tick_get() - tick_delta; /* 计算上一次挂起到现在过去了多久 */
timeout -= tick_delta; /* 重新计算超时时长 */
if (timeout < 0)
timeout = 0;
}
}
/* 运行到这里,说明邮箱非空了 */
*value = mb->msg_pool[mb->out_offset]; /* 读取邮件 */
++ mb->out_offset; /* 读取偏移加1 */
if (mb->out_offset >= mb->size) /* 读到末尾了,回到初始位置 */
mb->out_offset = 0;
mb->entry --; /* 邮箱中的邮件数量减1 */
/* 判断是否有线程因为请求发送邮件而挂起,若有则恢复挂起链表上的第一个线程 */
if (!rt_list_isempty(&(mb->suspend_sender_thread)))
{
rt_ipc_list_resume(&(mb->suspend_sender_thread));
rt_hw_interrupt_enable(temp);
rt_schedule();
return RT_EOK;
}
rt_hw_interrupt_enable(temp);
return RT_EOK;
}
RTT的消息队列和邮箱有些相似,只是邮箱只能传递4字节的消息,而消息队列则可以以拷贝的方式传递用户指定长度的数据。实际上,RTT的消息队列使用了动态内存池机制,lwip中也有这样的动态内存池。
消息队列的数据结构稍显复杂,实际上只需关注两点:消息队列的大小计算、消息队列的内置链表,在初始化消息队列时将会对这两部分内容进行分析。
struct rt_messagequeue
{
struct rt_ipc_object parent; /* 继承自IPC父类 */
void *msg_pool; /* 消息队列所需的内存首地址 */
rt_uint16_t msg_size; /* 单个消息块的数据容量 */
rt_uint16_t max_msgs; /* 消息队列的容量,即消息块的个数 */
rt_uint16_t entry; /* 已经使用的消息块个数 */
void *msg_queue_head; /* 指向待读取的消息队列第一个的消息块 */
void *msg_queue_tail; /* 指向待读取的消息队列最后一个消息块 */
void *msg_queue_free; /* 空闲消息链表头 */
};
typedef struct rt_messagequeue *rt_mq_t;
struct rt_mq_message
{
struct rt_mq_message *next;
};
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)
{
struct rt_mq_message *head;
register rt_base_t temp;
/* 初始化“爷爷”类,即object类 */
rt_object_init(&(mq->parent.parent), RT_Object_Class_MessageQueue, name);
/* 设置object类的属性,这里一般选择FIFO模式或者PRIO模式 */
mq->parent.parent.flag = flag;
/* 初始化IPC父类 */
rt_ipc_object_init(&(mq->parent));
/* 设置消息队列内存地址 */
mq->msg_pool = msgpool;
/* 计算获取正确的参数,msg_size需要向上4字节对齐,然后计算消息队列的实际可用内存块数量 */
mq->msg_size = RT_ALIGN(msg_size, RT_ALIGN_SIZE);
mq->max_msgs = pool_size / (mq->msg_size + sizeof(struct rt_mq_message));
mq->msg_queue_head = RT_NULL;
mq->msg_queue_tail = RT_NULL;
/* 将所有空闲消息块通过单向链表串联起来,msg_queue_free指向这个链表的第一个节点 */
mq->msg_queue_free = RT_NULL;
for (temp = 0; temp < mq->max_msgs; temp ++)
{
head = (struct rt_mq_message *)((rt_uint8_t *)mq->msg_pool +
temp * (mq->msg_size + sizeof(struct rt_mq_message)));
head->next = mq->msg_queue_free;
mq->msg_queue_free = head;
}
/* 已使用的消息块数量标记为0 */
mq->entry = 0;
return RT_EOK;
}
以上是初始化消息队列的函数rt_mq_init,我们需要重点关注消息队列的总需内存大小、消息块的实际个数等计算。我们可以参考以下所示rt_mq_create函数来获得答案。在rt_mq_create中,msg_size需要以RT_ALIGN_SIZE(一般是4字节)向上对齐,msg_pool所需的总内存为(对齐后的msg_size + rt_mq_message结构体大小(4字节) * 消息块个数),那么反过来就不难理解rt_mq_init函数中相关的计算了。
举个例子,当入参msg_size = 9,pool_size = 100时,首先要对msg_size向上4字节对齐,即msg_size = 12,此时max_msgs = 100/ (12 + 4) = 6。这种情况下浪费的字节数为100 % 16 = 4字节,所以在使用前了解内部实现还是很有必要的。
/* rt_mq_create函数中相关计算 */
mq->msg_size = RT_ALIGN(msg_size, RT_ALIGN_SIZE);
mq->max_msgs = max_msgs;
mq->msg_pool = RT_KERNEL_MALLOC((mq->msg_size + sizeof(struct rt_mq_message)) * mq->max_msgs);
另外,初始化时还初始化了3个指针,重点看msg_queue_free,msg_queue_free用于构建空闲消息块链表,初始化时将所有空闲块通过单向链表全部串联在一起,串联后的结构如下图所示。当需要发送消息队列时,从msg_queue_free链表上直接取下一个节点,如果节点非空就说明获取空闲消息块成功了(是不是很像内存申请?),填充好数据后再挂接到msg_queue_tail链表等待被读取。当消息队列中的消息块被读取后,就会将该内存块放回msg_queue_free链表(是不是很像内存释放?)。
正如上文所说,发送消息时,需要从msg_queue_free链表获取空闲消息块,填充好用户数据后再放到msg_queue_tail所指向的节点后面。那么我们就可以猜测,接收消息队列应该是取出msg_queue_head指向的内存块,然后将这个内存块再放回到msg_queue_free链表。在RTT还有一个发送消息队列的函数叫rt_mq_urgent,顾名思义,这个API肯定是用来发送紧急信息的,通过这个接口发送的消息会被挂接到msg_queue_head指向的消息块前面,这样就会优先被处理。
注意以下两点:
1.可以在中断里发送消息。
2.发送消息和发送邮件不一样,如果消息队列已经满了,不会引起发送线程阻塞,而是立即返回,这点和信号量一致。
/* 发送消息块 */
rt_err_t rt_mq_send(rt_mq_t mq, void *buffer, rt_size_t size)
{
register rt_ubase_t temp;
struct rt_mq_message *msg;
/* 检查要发送的消息大小是否超了 */
if (size > mq->msg_size)
return -RT_ERROR;
temp = rt_hw_interrupt_disable();
/* 从msg_queue_free获取一个空闲消息块 */
msg = (struct rt_mq_message *)mq->msg_queue_free;
/* 消息块为空,说明没有空闲消息块了,也就是说消息队列已经被塞满了 */
if (msg == RT_NULL)
{
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
/* 成功获取到消息块了,将这个消息块从msg_queue_free链表中删除 */
mq->msg_queue_free = msg->next;
rt_hw_interrupt_enable(temp);
/* 将获取到的消息块的next指针指向空 */
msg->next = RT_NULL;
/* 复制用户数据,+1是因为前面的4字节是一个next指针,需要跳过 */
rt_memcpy(msg + 1, buffer, size);
temp = rt_hw_interrupt_disable();
/*
* 将消息块放到msg_queue_tail链表的最后,
* 初始化时msg_queue_tail为空,所以需要判断非空
*/
if (mq->msg_queue_tail != RT_NULL)
{
((struct rt_mq_message *)mq->msg_queue_tail)->next = msg;
}
/* 将msg_queue_tail指向当前消息块,即指向末端 */
mq->msg_queue_tail = msg;
/* 如果msg_queue_head是空的,那么msg_queue_head也指向当前消息块 */
if (mq->msg_queue_head == RT_NULL)
mq->msg_queue_head = msg;
/* 消息队列中的待处理消息块数量加1 */
mq->entry ++;
/* 如果有线程因为请求该消息队列而挂起,那么恢复suspend_thread链表的第一个节点线程*/
if (!rt_list_isempty(&mq->parent.suspend_thread))
{
rt_ipc_list_resume(&(mq->parent.suspend_thread));
rt_hw_interrupt_enable(temp);
rt_schedule();
return RT_EOK;
}
rt_hw_interrupt_enable(temp);
return RT_EOK;
}
貌似没啥好说的了,只需要注意以下两点:
1.不允许在中断里请求消息队列。
2.接收消息队列和接收邮箱一样,会有个while循环等待,在接收成功前,即使被其他方式(线程内置定时器除外)提前唤醒,唤醒后也依然会尝试等待消息块的接收。
/* 接收消息块 */
rt_err_t rt_mq_recv(rt_mq_t mq,
void *buffer,
rt_size_t size,
rt_int32_t timeout)
{
struct rt_thread *thread;
register rt_ubase_t temp;
struct rt_mq_message *msg;
rt_uint32_t tick_delta; /* 用于记录过去了多长时间 */
/* 初始化时间差 */
tick_delta = 0;
thread = rt_thread_self();
temp = rt_hw_interrupt_disable();
/* 消息队列中没有消息块,且不等待,那么立即返回-RT_ETIMEOUT */
if (mq->entry == 0 && timeout == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_ETIMEOUT;
}
/* 消息队列是空的情况 */
while (mq->entry == 0)
{
/* 检查当前是否在线程环境中 */
RT_DEBUG_IN_THREAD_CONTEXT;
thread->error = RT_EOK;
/* 不等待,立即返回-RT_ETIMEOUT */
if (timeout == 0)
{
rt_hw_interrupt_enable(temp);
thread->error = -RT_ETIMEOUT;
return -RT_ETIMEOUT;
}
/* 将线程挂起在suspend_thread链表上 */
rt_ipc_list_suspend(&(mq->parent.suspend_thread),
thread,
mq->parent.parent.flag);
/* 判断超时时间是否为永久(-1),若非永久则启动线程内置定时器 */
if (timeout > 0)
{
/* 记录一下挂起的时刻 */
tick_delta = rt_tick_get();
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&timeout);
rt_timer_start(&(thread->thread_timer));
}
rt_hw_interrupt_enable(temp);
rt_schedule();
/* recv message */
if (thread->error != RT_EOK)
{
/* return error */
return thread->error;
}
/* 运行到这里,说明还没有请求到消息块,但是线程因为其他原因被提前唤醒了 */
temp = rt_hw_interrupt_disable();
/* 如果超时时间不为永久,那么重新计算一下剩余超时等待时间并继续等待 */
if (timeout > 0)
{
tick_delta = rt_tick_get() - tick_delta; /* 计算上一次挂起到现在过去了多久 */
timeout -= tick_delta; /* 重新计算超时时长 */
if (timeout < 0)
timeout = 0;
}
}
/* 成功请求到了一个消息块 */
msg = (struct rt_mq_message *)mq->msg_queue_head;
/* 移动msg_queue_head到下一个消息块 */
mq->msg_queue_head = msg->next;
/* 如果msg_queue_tail就是当前的消息块,那么说明当前消息队列里仅有这一个消息块 */
if (mq->msg_queue_tail == msg)
mq->msg_queue_tail = RT_NULL;
/* 消息队列中消息块个数减1 */
mq->entry --;
rt_hw_interrupt_enable(temp);
/* 将消息块中的数据拷贝出来,最多拷贝mq->msg_size个字节 */
rt_memcpy(buffer, msg + 1, size > mq->msg_size ? mq->msg_size : size);
temp = rt_hw_interrupt_disable();
/* 将消息块放回msg_queue_free链表 */
msg->next = (struct rt_mq_message *)mq->msg_queue_free;
mq->msg_queue_free = msg;
rt_hw_interrupt_enable(temp);
return RT_EOK;
}