天天看點

uC/OSIII的消息隊列處理機制

在uC/OSIII中沒有郵箱這個概念,而是統一合并到了消息隊列MSG_Q。因為消息隊列可以看作是很多郵箱的集合,郵箱隻是包含單個消息的消息隊列。

在分析消息隊列之前,必須要對消息的資料結構做一個徹底的分析。

消息隊列對象和其他核心對象一樣,它的結構定義很簡單:

下面看一下消息隊列的結構體,記住這個結構體名字叫OS_Q:

struct os_q {

    OS_OBJ_TYPE Type;

    CPU_CHAR *NamePtr;

    OS_PEND_LIST PendList;

    OS_MSG_Q MsgQ;

};

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 *NextPtr; 

    OS_TCB *TCBPtr;    

    OS_PEND_OBJ *PendObjPtr;

    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 *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 {

    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,     

                          void *p_void,  

                          OS_MSG_SIZE msg_size,  

                          OS_OPT opt,   

                          OS_ERR *p_err)

{

    CPU_TS ts;

    ...(大段的參數檢查代碼,此處略。)

    ts = OS_TS_GET();

#if OS_CFG_ISR_POST_DEFERRED_EN > 0u

    if (OSIntNestingCtr > (OS_NESTING_CTR)0) {  

        OS_IntQPost((OS_OBJ_TYPE)OS_OBJ_TYPE_Q,

                    (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(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_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;  

    if (p_pend_list->NbrEntries == (OS_OBJ_QTY)0) {

          部分代碼略。

        OS_MsgQPut(&p_q->MsgQ,

                   p_void,

                   msg_size,

                   post_type,

                   ts,

                   p_err);

        OS_CRITICAL_EXIT(); 

        return;

    }

   cnt = 要推送的數量;代碼略;

    p_pend_data = p_pend_list->HeadPtr; 

    while (cnt > 0u) {

        p_tcb = p_pend_data->TCBPtr;  

        p_pend_data_next = p_pend_data->NextPtr;

        OS_Post((OS_PEND_OBJ *)((void *)p_q), 

                p_tcb,

                p_void,

                msg_size,

                ts);

        p_pend_data = p_pend_data_next;  

        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;

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);

    }

      if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0) {

            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();

    CPU_CRITICAL_ENTER();

    switch (OSTCBCurPtr->PendStatus) {

        case OS_STATUS_PEND_OK:

             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:

             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;

             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;  

             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;

             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隻能送你到這一程了,接下來的路還是要自己走了。