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