首页 > 其他 > 详细

uC/OSIII的消息队列处理机制

时间:2014-03-16 20:29:50      阅读:550      评论:0      收藏:0      [点我收藏+]

在uC/OSIII中没有邮箱这个概念,而是统一合并到了消息队列MSG_Q。因为消息队列可以看作是很多邮箱的集合,邮箱只是包含单个消息的消息队列。
在分析消息队列之前,必须要对消息的数据结构做一个彻底的分析。
消息队列对象和其他内核对象一样,它的结构定义很简单:
下面看一下消息队列的结构体,记住这个结构体名字叫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只能送你到这一程了,接下来的路还是要自己走了。

 

uC/OSIII的消息队列处理机制,布布扣,bubuko.com

uC/OSIII的消息队列处理机制

原文:http://blog.csdn.net/mmhh3000/article/details/21331757

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!