简约而不简单。
分类: 嵌入式
2016-03-18 21:45:06
在分析消息队列之前,必须要对消息的数据结构做一个彻底的分析。
消息队列对象和其他内核对象一样,它的结构定义很简单:
下面看一下消息队列的结构体,记住这个结构体名字叫OS_Q:
struct os_q { /* Message Queue */
OS_OBJ_TYPE Type; /* Should be set to OS_OBJ_TYPE_Q */
CPU_CHAR *NamePtr; /* Pointer to Message Queue Name (NUL terminated ASCII) */
OS_PEND_LIST PendList; /* List of tasks waiting on message queue */
OS_MSG_Q MsgQ; /* List of messages */
};
typedef struct os_q OS_Q;
在应用程序中创建消息队列的时候,就是要定义这样的一个结构体变量:
举例创建过程如下:
OS_Q taskq;
void main()
{
OS_ERR err;
OSQCreate ((OS_Q *)&p_q,
(CPU_CHAR *)"my task Q",
(OS_MSG_QTY) 10,
(OS_ERR *)&err );
}
这样一个消息队列就创建好了。这里要注意:
OS_Q taskq;这句话应该是全局变量,因为通常都要在其他函数中访问。
有时不注意,很容易依照OSQCreate 的参数创建成这样的队列变量:
OS_Q * taskq;注意这样创建的只是个指针,并没有实体,这样的定义在OSQCreate中参数传入时不会出错,但一运行就会进入hard fault,因为指针跑飞了。
结构体OS_Q的基本信息就是这么多。应当注意的是最后两个成员:
OS_PEND_LIST PendList;
OS_MSG_Q MsgQ;
能看出来,这两个成员又是结构体,一个是OS_PEND_LIST类型,一个是OS_MSG_Q类型。
这两个数据结构是消息队列的核心,只有掌握它们,才能真正了解消息队列的来龙去脉。
首先看一下OS_PEND_LIST,顾名思义,就是所创建的消息队列taskq下面的等待列表。等待者是谁?就是那些调用了
OSQPend(&taskq...)的阻塞任务。那这两者之间又是怎么样联系在一起的呢?先告诉你,比较复杂,并不是直接连接在一起的,而是又
调用了一个中间结构叫OS_PEND_DATA 。下面先看一下OS_PEND_LIST结构:
struct os_pend_list {
OS_PEND_DATA *HeadPtr;
OS_PEND_DATA *TailPtr;
OS_OBJ_QTY NbrEntries;
};
可见,这个结构又是比较“简单”的:两个指向OS_PEND_DATA的指针,一个指向头,一个指向尾,还有就是计数NbrEntries,记录的是有多
少个OS_PEND_DATA。这样的设计是很明显的,典型的一个链表。正是这个链表,将一连串的OS_PEND_DATA链接起来,挂在每个消息队列下
边,而每个OS_PEND_DATA里记录的正是等待该消息队列的任务TCB。同时,在该任务TCB中也有指针反向记录着对应的
OS_PEND_DATA。下面就仔细看一下OS_PEND_DATA结构,这个分支就到头了,再没有其他结构了:
struct os_pend_data {
OS_PEND_DATA *PrevPtr; /*指向链表中的上一个OS_PEND_DATA */
OS_PEND_DATA *NextPtr; /*指向链表中的下一个OS_PEND_DATA */
OS_TCB *TCBPtr; /*指向等待该队列的任务TCB*/
OS_PEND_OBJ *PendObjPtr; /*反向指着调用它的内核对象(是队列或者信号量)*/
/*以下仅供MultiPend时使用*/
OS_PEND_OBJ *RdyObjPtr;
OS_MSG_SIZE RdyMsgSize;
CPU_TS RdyTS;
};
除了仅供MultiPend时使用的成员,前四个成员很正常,作用一目了然,双向链表,直接指向了等待的任务TCB,不多分析了。另外多说一
句,OS_PEND_DATA是在任务调用OSQPend时自动定义的一个变量,这与MultiPend调用略有不同,在MultiPend中等待多内核
对象时,OS_PEND_DATA是手动分配的。两种方式中OS_PEND_DATA占用的都是任务自已的堆栈,要注意防止栈溢出。
这样等待该消息队列的“任务挂起表”数据结构就分析完了,主线如下:
OS_Q->OS_PEND_LIST<->OS_PEND_DATA <-> 任务TCB
正是这样的一套数据结构,实现了队列Q和等待它的TCB之间的连接。
题外话,OS_PEND_DATA个人认为它的出现纯粹是uC/OSIII为了实现MultiPend统一入口的作用(因为MultiPend要求任务也可以同时等待信号量),不然直接把 TCB挂在OS_PEND_LIST下面,本是一件多么清爽的事情。
下面再看消息队列OS_Q成员中的另一大结构分支:OS_MSG_Q,它的作用是以队列的形式管理消息。这也正是消息队列名称的由来。既有任务等待列表,又有消息存储列表,这样才构成了完整的消息队列结构。
OS_MSG_Q的结构定义如下:
struct os_msg_q { /* OS_MSG_Q */
OS_MSG *InPtr; /* 将要存储进来的消息 */
OS_MSG *OutPtr; /* 下一个要被推送的消息 */
OS_MSG_QTY NbrEntriesSize; /*允许存储的消息数量*/
OS_MSG_QTY NbrEntries; /* 当前有多少条消息 */
OS_MSG_QTY NbrEntriesMax; /* 最多时达到过多少条消息 */
};
可以认为,OS_MSG_Q就是消息队列OS_Q的管家,掌管着消息队列中的全部消息的你来我往。这个管家有权利指派下一个消息被存储在哪里,以及哪个消
息将要被推送出去,场景就像排队买火车票时那个售票员,它有权利让你插队。同时OS_MSG_Q会完全按照主人OS_Q中定义的消息最多数量进行消息队列
管理,这又像排队买火车票时那个售票员会突然对你大喊“我要下班了,你们后面的都不要排队了”一样。
可见,对于消息而言,OS_MSG_Q是掌握其命运的,OS_MSG_Q结构里的OS_MSG结构就是代表的这些消息。OS_MSG结构作为消息就需要有实体变量的,这些实体变量是在uCOSIII初始化时被定义,并且被永久的定义在那里,默认值为50个,在ucosiii/source文件夹的os_app_cfg.h文件里:
#define OS_CFG_MSG_POOL_SIZE 50u
在初始化的50个OS_MSG变量,由OS_MSG_POOL OSMsgPool来管理,它也是个管家,专门管理“没过门的丫头”,过了门的小姐才交由各自OS_Q的OS_MSG_Q来管理了,用完后OS_MSG_Q会把她们再踢回给OS_MSG_POOL。
那消息的模样究竟如何?下面就看一下消息的结构OS_MSG:
struct os_msg { /* MESSAGE CONTROL BLOCK */
OS_MSG *NextPtr; /* 指向下一条消息 */
void *MsgPtr; /* 消息真身 */
OS_MSG_SIZE MsgSize; /* 消息真身的长度 */
CPU_TS MsgTS; /* 时间截 */
};
确切地说,OS_MSG真的只是消息的结构,它是消息的载体,不是真身。仔细观察OS_MSG成员,就能发现它里面这个“void
*MsgPtr和MsgSize”
这两个才是消息真身,它通常是指向一个全局变量的数组或者其他什么变量,消息正是通过这个指针来进行传递的。如果说OS_MSG是一封书信,那void
*MsgPtr和MsgSize才是信的内容,这个内容只是“说”了一些坐标点,而坐标所指向的变量本身才是真正要传递的“小秘密”,可能是某处宝藏吧,
也说不定。
至此消息存储的数据结构也看完了,大概流程如下:
OS_Q->OS_MSG_Q ->OS_MSG -> void *MsgPtr和MsgSize->宝藏
结合之前那条任务挂起表的主线,就形成了以下这条主线:
宝藏<-OS_Q<->任务TCB (注意TCB也反向指着OS_Q)
以上数据结构要牢记。接下来,才可以打开消息队列传递的大门。
对消息队列的基本操作是void OSQPost(OS_Q *p_q...)和void *OSQPend (OS_Q *p_q...)
注意OSQPend 函数为了节省一个传入参数,使用函数返回值作为获得的消息指针。
先看一下OSQPost函数,它的作用是完成“宝藏<-OS_Q”的环节,把消息挂接到对应的消息队列OS_Q上,函数的基本内容如下:
void OSQPost (OS_Q *p_q, /*要post到的消息队列*/
void *p_void, /*指向要传递的消息数据的指针*/
OS_MSG_SIZE msg_size, /*消息数据的长度,与指针配合使用*/
OS_OPT opt, /*选项:用于控制传递到队列头或尾;
是否推送给全部等待的TCB;
是否进行调度*/
OS_ERR *p_err) /*错误指针*/
{
CPU_TS ts;
...(大段的参数检查代码,此处略。)
ts = OS_TS_GET(); /* Get timestamp */
#if OS_CFG_ISR_POST_DEFERRED_EN > 0u /*如果使用中断中延迟推送方案,调用 OS_IntQPost函数进行post*/
if (OSIntNestingCtr > (OS_NESTING_CTR)0) { /*这里判断是否在中断中,延迟推送方案是为中断量身定制的,用于防止关中断时间太长,在其他地方不需要使用*/
OS_IntQPost((OS_OBJ_TYPE)OS_OBJ_TYPE_Q, /* Post to ISR queue */
(void *)p_q,
(void *)p_void,
(OS_MSG_SIZE)msg_size,
(OS_FLAGS )0,
(OS_OPT )opt,
(CPU_TS )ts,
(OS_ERR *)p_err);
return;
}
#endif
/*如果没在中断中,或者没有定义延迟中断推送,就直接调用OS_QPost函数进行推送*/
OS_QPost(p_q,
p_void,
msg_size,
opt,
ts,
p_err);
}
延迟推送中,OS_IntQPost()函数的接收者是中断延迟处理任务OS_IntQTask(),(这两个函数都定义在ucosiii\source
的os_int.c文件中)该任务处理中再调用OS_QPost()函数,结果就是OS_QPost()调用点由中断中转移到中断处理任务中,节省了关中
断时间。延迟推送和中断机制不是这里讨论的重点,所以直接进入OS_QPost()函数:
void OS_QPost (OS_Q *p_q,
void *p_void,
OS_MSG_SIZE msg_size,
OS_OPT opt,
CPU_TS ts,
OS_ERR *p_err) /*入口参数与OS_QPost一样*/
{
OS_OBJ_QTY cnt;
OS_OPT post_type;
OS_PEND_LIST *p_pend_list;
OS_PEND_DATA *p_pend_data;
OS_PEND_DATA *p_pend_data_next;
OS_TCB *p_tcb;
CPU_SR_ALLOC();
OS_CRITICAL_ENTER();
p_pend_list = &p_q->PendList; /*这里是找出该队列下的OS_PEND_LIST列表*/
if (p_pend_list->NbrEntries == (OS_OBJ_QTY)0) {
/* 如果列表里显示等待的任务TCB数目为0,也就是没有任务pend该队列,就没必要查
找相关的任务进行推送,直接把消息保存下来就好了*/
部分代码略。
/*那么就调用OS_MsgQPut,将消息存储到队列中的消息链表中*/
OS_MsgQPut(&p_q->MsgQ,
p_void,
msg_size,
post_type,
ts,
p_err);
OS_CRITICAL_EXIT();
return;
}
/* 如果列表里显示等待的任务TCB数目不为0,也就是有任务正在pend该队列,就必须把消息推送给它,就会执行以下代码*/
cnt = 要推送的数量;代码略;
p_pend_data = p_pend_list->HeadPtr; /*从p_pend_list里找出p_pend_data链表*/
while (cnt > 0u) {
p_tcb = p_pend_data->TCBPtr; /*从p_pend_data里找出等待的任务TCB*/
p_pend_data_next = p_pend_data->NextPtr;
OS_Post((OS_PEND_OBJ *)((void *)p_q), /*推送到等待的任务TCB*/
p_tcb,
p_void,
msg_size,
ts);
p_pend_data = p_pend_data_next;
cnt--; /*按要推送的数量cnt循环,直到退出*/
}
OS_CRITICAL_EXIT_NO_SCHED();
if ((opt & OS_OPT_POST_NO_SCHED) == (OS_OPT)0) {
OSSched(); /* 进行任务调度 */
}
*p_err = OS_ERR_NONE;
}
可见,OS_QPost 函数中又包含了两层调用:如果没有任务等待该消息队列,就调用OS_MsgQPut函数;如果有任务在等待,就调用OS_Post把消息推送给正在等待的任务。
简单介绍下这两个函数,它们是最后一级了,内容基本都是查找排序算法,没有太多的架构知识可讲了:
OS_MsgQPut函数(定义在ucosiii\source的OS_msg.c中)负责从OS_MSG_POOL中取出一个空闲的OS_MSG,将消息写入到它内部,然后将该OS_MSG挂到对应的消息队列下面。
OS_Post函数(定义在ucosiii\source的OS_core.c中)是直接向任务推送消息的函数,它先判断任务是单队列QPend还是MultiPend:
如果是单队列QPend,就把消息内容指针直接写到TCB里面MsgPtr和MsgSize中:
p_tcb->MsgPtr = p_void; /* Deposit message in OS_TCB of task waiting */
p_tcb->MsgSize = msg_size;
注意这两个成员变量,是定义在任务TCB结构体中的两个成员,是伴随TCB一生的,可以随时取用。
如果是MultiPend,则调用OS_Post1函数,把消息内容指针写到OS_PEND_DATA中专供MultiPend使用的几个字段中,这个在前面介绍OS_PEND_DATA时有介绍,可以回头去再看一下。写入代码如下:
p_pend_data->RdyObjPtr = p_obj;
p_pend_data->RdyMsgPtr = p_void;
p_pend_data->RdyMsgSize = msg_size;
p_pend_data->RdyTS = ts;
接下来由MultiPend的任务自己判断就绪的是队列还是信号量,然后提取出相应的消息内容指针,这个是任务处理中自己的家事,由写应用的程序员到时操心,这里就不再关心了。
消息推送过程到此结束。
这里还要再增加一些内容,就是uC/OSIII里有任务消息队列,这个消息队列与OS_Q的区别就是:不需要定义OS_Q,因为它是在任务TCB定义时,被直接定义在任务TCB里面了!伴随任务一生。uC/OSIII真的很舍得,看一下它的定义:
struct os_tcb {
......
#if OS_CFG_TASK_Q_EN > 0u
OS_MSG_Q MsgQ;
......
}
可见,在任务TCB中定义的是OS_MSG_Q,而不是OS_Q,为什么呢?前面说过OS_Q中包含两个重要的主线,这里再把它们列写如下:
OS_Q->OS_PEND_LIST<->OS_PEND_DATA <-> 任务TCB
OS_Q->OS_MSG_Q ->OS_MSG -> void *MsgPtr和MsgSize->宝藏
可见,任务TCB与宝藏相连的纽带就是OS_Q,那既然任务TCB自己都可以包含消息队列了,还要OS_Q干啥,是不是。前面又说过,OS_MSG_Q就是消息队列OS_Q的管家,所以任务TCB中直接定义OS_MSG_Q,找到宝藏就得了呗。
任务队列推送函数叫OSTaskQPost(),里面调用的是OS_TaskQPost(),该函数是被定义在ucosiii\source文件夹下的
os_task.c中的,它与普通OS_QPost()函数是同样的过程,里面也是调用OS_MsgQPut()进行无任务等待时的推送,调用
OS_Post()进行本任务等待时的推送。唯一不同的是,它的输入参数中不是*OS_Q类型,而*TCB,省去了通过队列再查找TCB的过程,所以它的
推送是非常快的,是直接推送。这也是uC/OSIII建议使用的消息队列推送方式。
个人认为,uC/OSIII不惜浪费TCB空间打造任务信号量,任务队列,目的就是要减少使用普通信号量和普通队列,因为进程间通信通常都是点对点的,这
将大幅度提高效率。而普通信号量和普通队列存在的唯一目的,就是多任务Post和MultiPend这两种特殊情况,而uC/OSIII又指出,这两种特
殊情况都是可能会长时间关中断的,建议少用。
消息队列推送机制基本就这些了,还剩下点边边角角的不值得再继续深入。
下面就是另一个重要方向,消息等待。uC/OSIII中的消息等待又分为三部分:普通消息队列等待函数void *OSQPend();任务消息队列等待函数void *OSTaskQPend();多对象等待函数OS_OBJ_QTY OSMultiPend()。
这里重点看第一个,任务调用void *OSQPend()后即进入等待消息状态。
OSQPend()函数是一个比较长的函数(通常接收器都比发送器要复杂一点),但简单讲,它可以分为两大部分:
一、准备进入任务挂起状态,将TCB写入到对应的要等待的消息队列下面的任务挂起表中;
然后执行调试,当前任务阻塞,其他任务执行;
二、收到消息后,从pend状态返回来,继续执行,把收到的消息指针取出来。
注意这两大部分的执行通常都是时间上分开的,但在空间上却是在一起的,就是代码被写在同一个函数里,这也正是Pend()函数的特点。下面分开介绍:
状态一,准备进入任务挂起状态,将TCB写入到对应的要等待的消息队列下面的任务挂起表中
。在这个过程中,Pend()函数做了几下几方面工作:先检查要pend的消息队列中是否已经有之前被post过来的消息存储在里面,如果有,就省事了,
直接返回,不pend;另外,如果在输入参数中指定了不pend,或者是在中断中执行的,都不能pend,必须立即返回;如果没有之前的消息被存储,也没
有在中断中,也指定了要pend,则准备进入阻塞等待状态,将挂起表等数据结构都准备好,将TCB写入其中。
二、收到消息后,从pend状态返回来,继续执行,如果是正常post过来的消息,就把收到的消息指针取出来,这是正常返回的情况。也有可能是等待超时,
或者是消息队列被删除了,或者是pend被人为的abort了,这些异常情况都要进行判断拦截,然后返回空指针,并返回一个错误。
OSQPend()函数的处理过程就是这样的,具体函数内容如下:
void *OSQPend (OS_Q *p_q,
OS_TICK timeout,
OS_OPT opt,
OS_MSG_SIZE *p_msg_size,
CPU_TS *p_ts,
OS_ERR *p_err)
{
OS_PEND_DATA pend_data;
void *p_void;
CPU_SR_ALLOC();
/*参数检查代码略*/
CPU_CRITICAL_ENTER();
p_void = OS_MsgQGet(&p_q->MsgQ, /* 判断队列里是否有已经被推送过的消息*/
p_msg_size,
p_ts,
p_err);
if (*p_err == OS_ERR_NONE) {
CPU_CRITICAL_EXIT();
return (p_void); /* 如果队列里有消息存在,直接返回,不pend */
}
if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0) {
/*如果是在中断中,不能pend,必须立即返回*/
CPU_CRITICAL_EXIT();
*p_err = OS_ERR_SCHED_LOCKED;
return ((void *)0);
}
OS_CRITICAL_ENTER_CPU_EXIT();
/*锁定调度器*/
OS_Pend(&pend_data, /* 准备进入阻塞等待状态,将挂起表等数据结构都准备好*/
(OS_PEND_OBJ *)((void *)p_q),
OS_TASK_PEND_ON_Q,
timeout);
OS_CRITICAL_EXIT_NO_SCHED();
/*退出调度锁定,并且不调度*/
OSSched(); /*进入调度点,切换到其他任务,到此,本任务处于暂停状态,在等待到消息
到达之前,不会再执行以下代码 */
/* 以下为从别的任务切换回来继续执行的代码,可能为pend获得,也可能为超时、删除了,
pend获得的内容是被保留在本任务TCB的MsgPtr和MsgSize中 */
CPU_CRITICAL_ENTER();
switch (OSTCBCurPtr->PendStatus) {
case OS_STATUS_PEND_OK: /* 是正常推送过来的消息 */
/*从本任务的TCB中MsgPtr和MsgSize中取出消息*/
p_void = OSTCBCurPtr->MsgPtr;
*p_msg_size = OSTCBCurPtr->MsgSize;
if (p_ts != (CPU_TS *)0) {
*p_ts = OSTCBCurPtr->TS;
}
*p_err = OS_ERR_NONE;
break;
case OS_STATUS_PEND_ABORT: /* 如果是消息队列被abort的,返回空 */
p_void = (void *)0;
*p_msg_size = (OS_MSG_SIZE)0;
if (p_ts != (CPU_TS *)0) {
*p_ts = OSTCBCurPtr->TS;
}
*p_err = OS_ERR_PEND_ABORT;
/* 报错为OS_ERR_PEND_ABORT */
break;
case OS_STATUS_PEND_TIMEOUT: /* 如果是等待超时,返回空*/
p_void = (void *)0;
*p_msg_size = (OS_MSG_SIZE)0;
if (p_ts != (CPU_TS *)0) {
*p_ts = (CPU_TS )0;
}
*p_err = OS_ERR_TIMEOUT;
/* 报错为OS_ERR_TIMEOUT */
break;
case OS_STATUS_PEND_DEL: /* 如果是消息队列被删除的,返回空 */
p_void = (void *)0;
*p_msg_size = (OS_MSG_SIZE)0;
if (p_ts != (CPU_TS *)0) {
*p_ts = OSTCBCurPtr->TS;
}
*p_err = OS_ERR_OBJ_DEL;
/* 报错为OS_ERR_OBJ_DEL */
break;
default:
p_void = (void *)0;
*p_msg_size = (OS_MSG_SIZE)0;
*p_err = OS_ERR_STATUS_INVALID;
break;
}
CPU_CRITICAL_EXIT();
return (p_void);
}
至于任务消息队列等待函数void
*OSTaskQPend()与此过程基本相同,也是分两部分,而且内部调用的函数也都一样,只是在传递参数的时候省去了将TCB写入对应OS_Q的任务
挂起表中的过程,也不对OS_PEND_DATA中被等待的消息队列赋值,因为消息被推送后,会直接被推送到任务TCB自己的存储空间中,不需要这些数据
结构做查找。对任务消息队列等待函数不再做过多介绍。
多对象等待函数OS_OBJ_QTY
OSMultiPend()中处理过程与此也是基本相同,而最大的区别是内部调用的函数不太一样,它在状态一阶段是用OS_MultiPendWait进
行参数配置,然后进入OSSched()调度点,切换到其他任务;收到消息后,进行返回状态错误判断,就直接返回,并不提取消息内容,因为
MultiPend里面等待的对象太多了,而且数目也不固定,它的消息内容提取工作交给应用程序员自己去完成。想等待多对象,uC/OSIII只能送你到
这一程了,接下来的路还是要自己走了。