LiteOS核心原始碼分析:訊息佇列Queue

語言: CN / TW / HK

摘要:本文通過分析LiteOS佇列模組的原始碼,掌握佇列使用上的差異。

本文分享自華為雲社群《LiteOS核心原始碼分析系列十 訊息佇列Queue》,原文作者:zhushy 。

佇列(Queue)是一種常用於任務間通訊的資料結構。任務能夠從佇列裡面讀取訊息,當佇列中的訊息為空時,掛起讀取任務;當佇列中有新訊息時,掛起的讀取任務被喚醒並處理新訊息。任務也能夠往佇列裡寫入訊息,當佇列已經寫滿訊息時,掛起寫入任務;當佇列中有空閒訊息節點時,掛起的寫入任務被喚醒並寫入訊息。如果將讀佇列和寫佇列的超時時間設定為0,則不會掛起任務,介面會直接返回,這就是非阻塞模式。訊息佇列提供了非同步處理機制,允許將一個訊息放入佇列,但不立即處理。同時佇列還有緩衝訊息的作用。

本文通過分析LiteOS佇列模組的原始碼,掌握佇列使用上的差異。LiteOS佇列模組的原始碼,均可以在LiteOS開源站點https://gitee.com/LiteOS/LiteOS 獲取。佇列原始碼、開發文件,示例程式程式碼如下:

  • LiteOS核心佇列原始碼

包括佇列的私有標頭檔案kernel\base\include\los_queue_pri.h、標頭檔案kernel\include\los_queue.h、C原始碼檔案kernel\base\los_queue.c。

  • 開發指南文件–佇列

線上文件https://gitee.com/LiteOS/LiteOS/blob/master/doc/LiteOS_Kernel_Developer_Guide.md#%E9%98%9F%E5%88%97

接下來,我們看下佇列的結構體,佇列初始化,佇列常用操作的原始碼。

1、佇列結構體定義和常用巨集定義

1.1 佇列結構體定義

在檔案kernel\base\include\los_queue_pri.h定義的佇列控制塊結構體為LosQueueCB,結構體原始碼如下。佇列狀態.queueState取值OS_QUEUE_UNUSED、OS_QUEUE_INUSED,佇列記憶體型別.queueMemType取值OS_QUEUE_ALLOC_STATIC、OS_QUEUE_ALLOC_DYNAMIC,分別表示計使用者分配佇列記憶體空間或系統自動分配。

typedef struct {
    UINT8 *queueHandle; /**< 佇列記憶體空間的指標 */
    UINT8 queueState; /**< 佇列的使用狀態 */
    UINT8 queueMemType; /**< 佇列記憶體型別,使用者分配佇列記憶體空間或系統自動分配 */
    UINT16 queueLen; /**< 佇列長度,訊息數量 */
    UINT16 queueSize; /**< 訊息節點大小 */
    UINT32 queueId; /**< 佇列編號 */
    UINT16 queueHead; /**< 訊息頭節點位置 */
    UINT16 queueTail; /**< 訊息尾節點位置 */
    UINT16 readWriteableCnt[OS_QUEUE_N_RW]; /**< 2維陣列,可讀、可寫的訊息數量, 0:可讀, 1:可寫 */
    LOS_DL_LIST readWriteList[OS_QUEUE_N_RW]; /**< 2維連結串列陣列,阻塞讀、寫任務的雙向連結串列, 0:讀連結串列, 1:寫連結串列 */
    LOS_DL_LIST memList; /**< 記憶體節點雙向連結串列,相容CMSIS時使用 */
} LosQueueCB;

1.2 佇列常用巨集定義

系統支援建立多少佇列是根據開發板情況使用巨集LOSCFG_BASE_IPC_QUEUE_LIMIT定義的,每一個佇列queueId是UINT32型別的。佇列queueId由2部分組成:count和index,分別處於高16位和低16位。建立佇列,使用後刪除時,佇列回收到佇列池時,佇列queueId的高16位即count值會加1,這樣可以用來表示該佇列被建立刪除的次數。index取值為[0,LOSCFG_BASE_IPC_QUEUE_LIMIT),表示佇列池中各個佇列的編號。

⑴處的巨集用來分割count和index的位數,⑵處的巨集在佇列被刪除時用來更新佇列編號queueId,可以看出高16位為count和低16位為index。⑶處獲取佇列queueId的低16位。⑷根據佇列queueId獲取對應的佇列被建立刪除的次數count。⑸處從佇列池中獲取指定佇列queueId對應的佇列控制塊。

⑴    #define QUEUE_SPLIT_BIT        16

⑵    #define SET_QUEUE_ID(count, index)    (((count) << QUEUE_SPLIT_BIT) | (index))

⑶    #define GET_QUEUE_INDEX(queueId)        ((queueId) & ((1U << QUEUE_SPLIT_BIT) - 1))

⑷    #define GET_QUEUE_COUNT(queueId)        ((queueId) >> QUEUE_SPLIT_BIT)

⑸    #define GET_QUEUE_HANDLE(queueId)       (((LosQueueCB *)g_allQueue) + GET_QUEUE_INDEX(queueId))

另外,佇列中還提供了比較重要的佇列讀取訊息操作相關的列舉和巨集。佇列的讀取訊息操作型別和隊首還是隊尾寫入,讀取還是寫入有關係,所以操作型別使用2位元的數字來表示,高1位表示隊首還是隊尾,低1位表示讀取還是寫入。定義如下:

typedef enum {
    OS_QUEUE_READ = 0,
    OS_QUEUE_WRITE = 1,
    OS_QUEUE_N_RW = 2
} QueueReadWrite;

typedef enum {
    OS_QUEUE_HEAD = 0,
    OS_QUEUE_TAIL = 1
} QueueHeadTail;

#define OS_QUEUE_OPERATE_TYPE(ReadOrWrite, HeadOrTail) (((UINT32)(HeadOrTail) << 1) | (ReadOrWrite))
#define OS_QUEUE_READ_WRITE_GET(type) ((type) & 0x01U)
#define OS_QUEUE_READ_HEAD     (OS_QUEUE_READ | (OS_QUEUE_HEAD << 1))
#define OS_QUEUE_READ_TAIL     (OS_QUEUE_READ | (OS_QUEUE_TAIL << 1))
#define OS_QUEUE_WRITE_HEAD    (OS_QUEUE_WRITE | (OS_QUEUE_HEAD << 1))
#define OS_QUEUE_WRITE_TAIL    (OS_QUEUE_WRITE | (OS_QUEUE_TAIL << 1))
#define OS_QUEUE_OPERATE_GET(type) ((type) & 0x03U)
#define OS_QUEUE_IS_READ(type) (OS_QUEUE_READ_WRITE_GET(type) == OS_QUEUE_READ)
#define OS_QUEUE_IS_WRITE(type) (OS_QUEUE_READ_WRITE_GET(type) == OS_QUEUE_WRITE)

2、佇列初始化

佇列在核心中預設開啟,使用者可以通過巨集LOSCFG_BASE_IPC_QUEUE進行關閉。開啟佇列的情況下,在系統啟動時,在kernel\init\los_init.c中呼叫OsQueueInit()進行佇列模組初始化。下面,我們分析下佇列初始化的程式碼。

⑴為佇列申請記憶體,如果申請失敗,則返回錯誤。⑵初始化雙向迴圈連結串列g_freeQueueList,維護未使用的佇列。⑶迴圈每一個佇列進行初始化,為每一個佇列節點指定索引queueId,並把佇列節點插入未使用佇列雙向連結串列g_freeQueueList。程式碼上可以看出,掛在未使用佇列雙向連結串列上的節點是每個佇列控制塊的寫阻塞任務連結串列點.readWriteList[OS_QUEUE_WRITE]。⑷如果開啟了佇列調測開關,則呼叫函式OsQueueDbgInitHook()進行初始化。

LITE_OS_SEC_TEXT_INIT UINT32 OsQueueInit(VOID)
{
    LosQueueCB *queueNode = NULL;
    UINT32 index;
    UINT32 size;

    size = LOSCFG_BASE_IPC_QUEUE_LIMIT * sizeof(LosQueueCB);
⑴  g_allQueue = (LosQueueCB *)LOS_MemAlloc(m_aucSysMem0, size);
    if (g_allQueue == NULL) {
        return LOS_ERRNO_QUEUE_NO_MEMORY;
    }
    (VOID)memset_s(g_allQueue, size, 0, size);
⑵  LOS_ListInit(&g_freeQueueList);
⑶  for (index = 0; index < LOSCFG_BASE_IPC_QUEUE_LIMIT; index++) {
        queueNode = ((LosQueueCB *)g_allQueue) + index;
        queueNode->queueId = index;
        LOS_ListTailInsert(&g_freeQueueList, &queueNode->readWriteList[OS_QUEUE_WRITE]);
    }

⑷  if (OsQueueDbgInitHook() != LOS_OK) {
        return LOS_ERRNO_QUEUE_NO_MEMORY;
    }
    return LOS_OK;
}

3、佇列常用操作

3.1 佇列建立

建立佇列的函式有2個:LOS_QueueCreateStatic()和LOS_QueueCreate(),二者的區別是存放佇列訊息的記憶體空間是使用者建立還是系統自動建立。前者需要開啟巨集LOSCFG_QUEUE_STATIC_ALLOCATION時才可用,使用使用者分配佇列記憶體空間,後者在建立佇列時動態建立佇列記憶體空間。我們看看2個函式的傳參:queueName是佇列名稱,實際上並沒有使用。len是佇列訊息的長度,queueId是佇列編號,flags保留未使用maxMsgSize是佇列中每條訊息的最大大小。對於靜態建立佇列,還有2個引數來表示使用者建立的佇列記憶體空間的地址指標queueMem和記憶體大小memSize。

我們分析下建立佇列的程式碼。先看靜態建立,呼叫⑴處的函式OsQueueCreateParameterCheck()對引數進行校驗,⑵對傳入的記憶體空間進行驗證,確保足夠大放得下佇列的訊息,其中maxMsgSize + sizeof(UINT32)表示訊息最大大小,另外再加4個位元組,在訊息的最後4個位元組用來儲存訊息的實際長度。然後呼叫⑶處函式OsQueueCreateInternal()完成佇列建立。再看下動態建立佇列的程式碼,⑷處對佇列動態申請記憶體,然後呼叫函式OsQueueCreateInternal()完成佇列建立。2個函式呼叫同樣的內部建立佇列函式OsQueueCreateInternal(),下文繼續分析。

#ifdef LOSCFG_QUEUE_STATIC_ALLOCATION
LITE_OS_SEC_TEXT_INIT UINT32 LOS_QueueCreateStatic(CHAR *queueName,
                                                   UINT16 len,
                                                   UINT32 *queueId,
                                                   UINT32 flags,
                                                   UINT16 maxMsgSize,
                                                   VOID *queueMem,
                                                   UINT16 memSize)
{
    UINT32 ret;
    UINT16 msgSize;
    (VOID)queueName;
    (VOID)flags;

⑴  ret = OsQueueCreateParameterCheck(len, queueId, maxMsgSize);
    if (ret != LOS_OK) {
        return ret;
    }

    if (queueMem == NULL) {
        return LOS_ERRNO_QUEUE_CREAT_PTR_NULL;
    }

⑵  msgSize = maxMsgSize + sizeof(UINT32);
    if (memSize < ((UINT32)msgSize * len)) {
        return LOS_ERRNO_QUEUE_CREATE_NO_MEMORY;
    }

⑶  return OsQueueCreateInternal(len, queueId, msgSize, queueMem, OS_QUEUE_ALLOC_STATIC);
}
#endif

LITE_OS_SEC_TEXT_INIT UINT32 LOS_QueueCreate(CHAR *queueName, UINT16 len, UINT32 *queueId,
                                             UINT32 flags, UINT16 maxMsgSize)
{
    UINT32 ret;
    UINT8 *queueMem = NULL;
    UINT16 msgSize;
    (VOID)queueName;
    (VOID)flags;

    ret = OsQueueCreateParameterCheck(len, queueId, maxMsgSize);
    if (ret != LOS_OK) {
        return ret;
    }

    msgSize = maxMsgSize + sizeof(UINT32);
⑷  queueMem = (UINT8 *)LOS_MemAlloc(m_aucSysMem1, (UINT32)len * msgSize);
    if (queueMem == NULL) {
        return LOS_ERRNO_QUEUE_CREATE_NO_MEMORY;
    }

    ret = OsQueueCreateInternal(len, queueId, msgSize, queueMem, OS_QUEUE_ALLOC_DYNAMIC);
    if (ret != LOS_OK) {
        (VOID)LOS_MemFree(m_aucSysMem1, queueMem);
        return ret;
    }

    return LOS_OK;
}

我們看看建立佇列的內部函式OsQueueCreateInternal()。⑴判斷g_freeQueueList是否為空,如果沒有可以使用的佇列,呼叫函式OsQueueCheckHook()做些調測相關的檢測,這個函式需要開啟調測開關,後續系列專門分析。⑵處如果g_freeQueueList不為空,則獲取第一個可用的佇列節點,接著從雙向連結串列g_freeQueueList中刪除,然後呼叫巨集GET_QUEUE_LIST獲取LosQueueCB *queueCB,初始化建立的佇列資訊,包含佇列的長度.queueLen、訊息大小.queueSize,佇列記憶體空間.queueHandle,訊息狀態.queueState,記憶體型別.queueMemType,可讀的數量.readWriteableCnt[OS_QUEUE_READ]為0,可寫的數量readWriteableCnt[OS_QUEUE_WRITE]為佇列訊息長度len,佇列頭位置.queueHead和尾位置.queueTail為0。

⑶初始化雙向連結串列.readWriteList[OS_QUEUE_READ],阻塞在這個佇列上的讀訊息任務會掛在這個連結串列上。初始化雙向連結串列.readWriteList[OS_QUEUE_WRITE],阻塞在這個佇列上的寫訊息任務會掛在這個連結串列上。初始化雙向連結串列.memList,這個開啟相容CMSIS巨集LOSCFG_COMPAT_CMSIS時,才會使用到。⑷賦值給輸出引數*queueId,後續程式使用這個佇列編號對佇列進行其他操作。

LITE_OS_SEC_TEXT_INIT STATIC UINT32 OsQueueCreateInternal(UINT16 len, UINT32 *queueId, UINT16 msgSize,
                                                          UINT8 *queue, UINT8 queueMemType)
{
    LosQueueCB *queueCB = NULL;
    LOS_DL_LIST *unusedQueue = NULL;
    UINT32 intSave;

    SCHEDULER_LOCK(intSave);
⑴  if (LOS_ListEmpty(&g_freeQueueList)) {
        SCHEDULER_UNLOCK(intSave);
        OsQueueCheckHook();
        return LOS_ERRNO_QUEUE_CB_UNAVAILABLE;
    }

⑵  unusedQueue = LOS_DL_LIST_FIRST(&g_freeQueueList);
    LOS_ListDelete(unusedQueue);
    queueCB = GET_QUEUE_LIST(unusedQueue);
    queueCB->queueLen = len;
    queueCB->queueSize = msgSize;
    queueCB->queueHandle = queue;
    queueCB->queueState = OS_QUEUE_INUSED;
    queueCB->queueMemType = queueMemType;
    queueCB->readWriteableCnt[OS_QUEUE_READ] = 0;
    queueCB->readWriteableCnt[OS_QUEUE_WRITE] = len;
    queueCB->queueHead = 0;
    queueCB->queueTail = 0;
⑶  LOS_ListInit(&queueCB->readWriteList[OS_QUEUE_READ]);
    LOS_ListInit(&queueCB->readWriteList[OS_QUEUE_WRITE]);
    LOS_ListInit(&queueCB->memList);

    OsQueueDbgUpdateHook(queueCB->queueId, OsCurrTaskGet()->taskEntry);
    SCHEDULER_UNLOCK(intSave);

⑷  *queueId = queueCB->queueId;

    LOS_TRACE(QUEUE_CREATE, *queueId, len, msgSize - sizeof(UINT32), (UINTPTR)queue, queueMemType);
    return LOS_OK;
}

3.2 佇列刪除

我們可以使用函式LOS_QueueDelete(UINT32 queueId)來刪除佇列,下面通過分析原始碼看看如何刪除佇列的。

⑴處判斷佇列queueId是否超過LOSCFG_BASE_IPC_queue_LIMIT,如果超過則返回錯誤碼。如果佇列編號沒有問題,獲取佇列控制塊LosQueueCB *queueCB。⑵處判斷要刪除的佇列queueId是否匹配,或者要刪除的佇列處於未使用狀態,則跳轉到錯誤標籤QUEUE_END`進行處理。⑶如果佇列的阻塞讀、阻塞寫任務列表不為空,或記憶體節點連結串列不為空,則不允許刪除,跳轉到錯誤標籤進行處理。⑷處檢驗佇列的可讀、可寫數量是否出錯。

⑸處獲取佇列的記憶體空間,如果是動態建立的記憶體,接下來會需要呼叫⑺處函式LOS_MemFree()釋放。⑹處把.queueState設定為未使用OS_QUEUE_UNUSED,設定佇列編號.queueId,並把佇列節點插入未使用佇列雙向連結串列g_freeQueueList。

LITE_OS_SEC_TEXT_INIT UINT32 LOS_QueueDelete(UINT32 queueId)
{
    LosQueueCB *queueCB = NULL;
    UINT8 *queue = NULL;
    UINT32 intSave;
    UINT32 ret = LOS_OK;

⑴  if (GET_QUEUE_INDEX(queueId) >= LOSCFG_BASE_IPC_QUEUE_LIMIT) {
        return LOS_ERRNO_QUEUE_NOT_FOUND;
    }

    queueCB = (LosQueueCB *)GET_QUEUE_HANDLE(queueId);

    LOS_TRACE(QUEUE_DELETE, queueId, queueCB->queueState, queueCB->readWriteableCnt[OS_QUEUE_READ]);

    SCHEDULER_LOCK(intSave);
⑵  if ((queueCB->queueId != queueId) || (queueCB->queueState == OS_QUEUE_UNUSED)) {
        ret = LOS_ERRNO_QUEUE_NOT_CREATE;
        goto QUEUE_END;
    }

⑶  if (!LOS_ListEmpty(&queueCB->readWriteList[OS_QUEUE_READ])) {
        ret = LOS_ERRNO_QUEUE_IN_TSKUSE;
        goto QUEUE_END;
    }

    if (!LOS_ListEmpty(&queueCB->readWriteList[OS_QUEUE_WRITE])) {
        ret = LOS_ERRNO_QUEUE_IN_TSKUSE;
        goto QUEUE_END;
    }

    if (!LOS_ListEmpty(&queueCB->memList)) {
        ret = LOS_ERRNO_QUEUE_IN_TSKUSE;
        goto QUEUE_END;
    }

⑷  if ((queueCB->readWriteableCnt[OS_QUEUE_WRITE] + queueCB->readWriteableCnt[OS_QUEUE_READ]) !=
        queueCB->queueLen) {
        ret = LOS_ERRNO_QUEUE_IN_TSKWRITE;
        goto QUEUE_END;
    }

⑸  queue = queueCB->queueHandle;
⑹  queueCB->queueHandle = NULL;
    queueCB->queueState = OS_QUEUE_UNUSED;
    queueCB->queueId = SET_QUEUE_ID(GET_QUEUE_COUNT(queueCB->queueId) + 1, GET_QUEUE_INDEX(queueCB->queueId));
    OsQueueDbgUpdateHook(queueCB->queueId, NULL);

    LOS_ListTailInsert(&g_freeQueueList, &queueCB->readWriteList[OS_QUEUE_WRITE]);
    SCHEDULER_UNLOCK(intSave);
    if (queueCB->queueMemType == OS_QUEUE_ALLOC_DYNAMIC) {
⑺      ret = LOS_MemFree(m_aucSysMem1, (VOID *)queue);
    }
    return ret;

QUEUE_END:
    SCHEDULER_UNLOCK(intSave);
    return ret;
}

下面就來看看佇列的讀寫,有2點需要注意:

(1)隊首、隊尾的讀寫

只支援隊首讀取,否則就不算隊列了。除了正常的隊尾寫訊息外,還提供插隊機制,支援從隊首寫入。

(2)佇列訊息資料內容

往佇列中寫入的訊息的型別有2中,即支援按地址寫入和按值寫入(帶拷貝)。按哪種型別寫入,就需要配對的按相應的型別去讀取。

佇列讀取介面的類別,歸納如下:

3.3 佇列讀取

我們知道有2個佇列讀取方法,按引用讀取的函式LOS_QueueRead()把訊息地址值bufferAddr作為數值,進一步呼叫按值讀取的函式LOS_QueueReadCopy()。

LITE_OS_SEC_TEXT UINT32 LOS_QueueRead(UINT32 queueId, VOID *bufferAddr, UINT32 bufferSize, UINT32 timeout)
{
    return LOS_QueueReadCopy(queueId, bufferAddr, &bufferSize, timeout);
}

再來看看函式LOS_QueueReadCopy()。⑴處校驗傳入引數,佇列編號不能超出限制,傳入的指標不能為空。如果timeout不為零會阻塞時,不能在中斷中讀取佇列。⑵處操作型別表示隊首讀取,然後呼叫函式OsQueueOperate()進一步操作佇列。

LITE_OS_SEC_TEXT UINT32 LOS_QueueReadCopy(UINT32 queueId,
                                          VOID *bufferAddr,
                                          UINT32 *bufferSize,
                                          UINT32 timeout)
{
    UINT32 ret;
    UINT32 operateType;

⑴  ret = OsQueueReadParameterCheck(queueId, bufferAddr, bufferSize, timeout);
    if (ret != LOS_OK) {
        return ret;
    }

⑵  operateType = OS_QUEUE_OPERATE_TYPE(OS_QUEUE_READ, OS_QUEUE_HEAD);
    return OsQueueOperate(queueId, operateType, bufferAddr, bufferSize, timeout);
}

我們進一步分析下函式OsQueueOperate(),這是是比較通用的封裝,讀取,寫入都會呼叫這個函式,我們以讀取佇列為例分析這個函式。⑴處獲取佇列的操作型別,為讀取操作。⑵處先呼叫函式OsQueueOperateParamCheck()進行引數校驗,校驗佇列是使用中的佇列,並對讀寫訊息大小進行校驗。⑶處如果可讀數量為0,無法讀取時,如果是零等待則返回錯誤碼。如果當前鎖任務排程,跳出函式執行。否則,執行⑷把當前任務放入佇列的讀取訊息阻塞佇列,然後觸發任務排程,後續的程式碼暫時不再執行。如果可讀的數量不為0,可以繼續讀取時,執行⑹處程式碼把可讀數量減1,然後繼續執行⑺處程式碼讀取佇列。

等讀取佇列阻塞超時,或者佇列可以讀取後,繼續執行⑸處的程式碼。如果是發生超時,佇列還不能讀取,更改任務狀態,跳出函式執行。如果佇列可以讀取了,繼續執行⑺處程式碼讀取佇列。⑻處在成功讀取佇列後,如果有任務阻塞在寫入佇列,則獲取阻塞連結串列中的第一個任務resumedTask,然後呼叫喚醒函式OsTaskWake()把待恢復的任務放入就緒佇列,觸發一次任務排程。如果無阻塞任務,則把可寫入的數量加1。

UINT32 OsQueueOperate(UINT32 queueId, UINT32 operateType, VOID *bufferAddr, UINT32 *bufferSize, UINT32 timeout)
{
    LosQueueCB *queueCB = (LosQueueCB *)GET_QUEUE_HANDLE(queueId);
    LosTaskCB *resumedTask = NULL;
    UINT32 ret;
⑴  UINT32 readWrite = OS_QUEUE_READ_WRITE_GET(operateType);
    UINT32 intSave;

    LOS_TRACE(QUEUE_RW, queueId, queueCB->queueSize, *bufferSize, operateType,
        queueCB->readWriteableCnt[OS_QUEUE_READ], queueCB->readWriteableCnt[OS_QUEUE_WRITE], timeout);

    SCHEDULER_LOCK(intSave);
⑵  ret = OsQueueOperateParamCheck(queueCB, queueId, operateType, bufferSize);
    if (ret != LOS_OK) {
        goto QUEUE_END;
    }

⑶  if (queueCB->readWriteableCnt[readWrite] == 0) {
        if (timeout == LOS_NO_WAIT) {
            ret = OS_QUEUE_IS_READ(operateType) ? LOS_ERRNO_QUEUE_ISEMPTY : LOS_ERRNO_QUEUE_ISFULL;
            goto QUEUE_END;
        }

        if (!OsPreemptableInSched()) {
            ret = LOS_ERRNO_QUEUE_PEND_IN_LOCK;
            goto QUEUE_END;
        }

⑷      OsTaskWait(&queueCB->readWriteList[readWrite], OS_TASK_STATUS_PEND, timeout);

        OsSchedResched();
        SCHEDULER_UNLOCK(intSave);
        SCHEDULER_LOCK(intSave);

⑸      if (OsCurrTaskGet()->taskStatus & OS_TASK_STATUS_TIMEOUT) {
            OsCurrTaskGet()->taskStatus &= ~OS_TASK_STATUS_TIMEOUT;
            ret = LOS_ERRNO_QUEUE_TIMEOUT;
            goto QUEUE_END;
        }
    } else {
⑹       queueCB->readWriteableCnt[readWrite]--;
    }

⑺   OsQueueBufferOperate(queueCB, operateType, bufferAddr, bufferSize);

⑻  if (!LOS_ListEmpty(&queueCB->readWriteList[!readWrite])) {
        resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&queueCB->readWriteList[!readWrite]));
        OsTaskWake(resumedTask, OS_TASK_STATUS_PEND);
        SCHEDULER_UNLOCK(intSave);
        LOS_MpSchedule(OS_MP_CPU_ALL);
        LOS_Schedule();
        return LOS_OK;
    } else {
⑼      queueCB->readWriteableCnt[!readWrite]++;
    }

QUEUE_END:
    SCHEDULER_UNLOCK(intSave);
    return ret;
}

我們再繼續看下函式OsQueueBufferOperate()是具體如何讀取佇列的。⑴處switch-case語句獲取操作位置。對於⑵頭部讀取的情況,先獲取讀取位置queuePosition。然後,如果當前頭節點位置.queueHead加1等於佇列訊息長度,頭節點位置.queueHead設定為0,否則加1。對於⑶頭部寫入的情況,如果當前頭節點位置.queueHead加1等於佇列訊息長度,頭節點位置.queueHead設定為佇列訊息長度減1,否則頭節點位置.queueHead減1即可。然後,獲取要寫入的位置queuePosition。對於⑷尾部寫入的情況,先獲取寫入位置queuePosition。然後,如果當前尾節點位置.queueTail加1等於佇列訊息長度,尾節點位置.queueTail設定為0,否則加1。

⑸處基於獲取的佇列讀取位置獲取佇列訊息節點queueNode。我們看下⑹處如何讀訊息,每個訊息節點的後4個位元組儲存的是訊息的長度,首先獲取訊息的長度msgDataSize,然後執行⑺處程式碼把訊息內容讀取到bufferAddr。再看看⑻處如何寫入佇列訊息,首先把訊息內容寫入到queueNode,然後執行⑼再把訊息長度的內容寫入到queueNode + queueCB->queueSize - sizeof(UINT32),就是每個訊息節點的後4位元組。

STATIC VOID OsQueueBufferOperate(LosQueueCB *queueCB, UINT32 operateType, VOID *bufferAddr, UINT32 *bufferSize)
{
    UINT8 *queueNode = NULL;
    UINT32 msgDataSize;
    UINT16 queuePosition;

    /* get the queue position */
⑴  switch (OS_QUEUE_OPERATE_GET(operateType)) {
        case OS_QUEUE_READ_HEAD:
            queuePosition = queueCB->queueHead;
⑵          ((queueCB->queueHead + 1) == queueCB->queueLen) ? (queueCB->queueHead = 0) : (queueCB->queueHead++);
            break;
        case OS_QUEUE_WRITE_HEAD:
⑶          (queueCB->queueHead == 0) ? (queueCB->queueHead = queueCB->queueLen - 1) : (--queueCB->queueHead);
            queuePosition = queueCB->queueHead;
            break;
        case OS_QUEUE_WRITE_TAIL:
⑷          queuePosition = queueCB->queueTail;
            ((queueCB->queueTail + 1) == queueCB->queueLen) ? (queueCB->queueTail = 0) : (queueCB->queueTail++);
            break;
        default:  /* read tail, reserved. */
            PRINT_ERR("invalid queue operate type!\n");
            return;
    }

⑸  queueNode = &(queueCB->queueHandle[(queuePosition * (queueCB->queueSize))]);

    if (OS_QUEUE_IS_READ(operateType)) {
⑹      if (memcpy_s(&msgDataSize, sizeof(UINT32), queueNode + queueCB->queueSize - sizeof(UINT32),
                     sizeof(UINT32)) != EOK) {
            PRINT_ERR("get msgdatasize failed\n");
            return;
        }
⑺      if (memcpy_s(bufferAddr, *bufferSize, queueNode, msgDataSize) != EOK) {
            PRINT_ERR("copy message to buffer failed\n");
            return;
        }

        *bufferSize = msgDataSize;
    } else {
⑻      if (memcpy_s(queueNode, queueCB->queueSize, bufferAddr, *bufferSize) != EOK) {
            PRINT_ERR("store message failed\n");
            return;
        }
⑼      if (memcpy_s(queueNode + queueCB->queueSize - sizeof(UINT32), sizeof(UINT32), bufferSize,
                     sizeof(UINT32)) != EOK) {
            PRINT_ERR("store message size failed\n");
            return;
        }
    }
}

3.4 佇列寫入

我們知道,有4個佇列寫入方法,2個隊尾寫入,2個隊首寫入,分別包含按引用地址寫入訊息和按數值寫入訊息。LOS_QueueWrite()會呼叫LOS_QueueWriteCopy(),LOS_QueueWriteHead()會呼叫LOS_QueueWriteHeadCopy(),然後指定不同的操作型別後,會進一步呼叫前文已經分析過的函式OsQueueOperate()。

小結

本文帶領大家一起剖析了LiteOS佇列模組的原始碼,包含佇列的結構體、佇列池初始化、佇列建立刪除、讀寫訊息等。感謝閱讀,如有任何問題、建議,都可以留言給我們: https://gitee.com/LiteOS/LiteOS/issues 。為了更容易找到LiteOS程式碼倉,建議訪問 https://gitee.com/LiteOS/LiteOS ,關注Watch、點贊Star、並Fork到自己賬戶下,謝謝。

點選關注,第一時間瞭解華為雲新鮮技術~