RT-Thread 線程通信(IPC)源碼解析

目前網上有許多講解RT-Thread 的IPC(信號量、互斥量、事件、郵箱、隊列)相關文檔,但僅停留在API的使用,鮮有從源碼角度講解其實現原理。野火出版的《RT-Thread內核實現與應用開發實戰指南》不僅講解了線程調度等實現原理,還講解了IPC的實現原理,本文僅僅是作爲學習筆記來簡短敘述下IPC的實現原理,想深入學習的話可以參考野火的這本書,也可以直接閱讀源碼。下文涉及的代碼做了適當刪減,例如入參檢查、鉤子函數等。

目錄

1 線程內置定時器

2 IPC父類對象

2.1 IPC父類數據結構

2.2 IPC父類方法

3 信號量

3.1 信號量數據結構

3.2 初始化信號量

3.3 請求信號量

3.4 釋放信號量

4 互斥量

4.1 互斥量數據結構

4.2 初始化互斥量

4.3 請求互斥量

4.4 釋放互斥量

5 事件

5.1 事件數據結構

5.2 初始化事件

5.3 發送事件

5.4 接收事件

6 郵箱

6.1 郵箱數據結構

6.2 初始化郵箱

6.3 發送郵件

6.4 接收郵件

7 消息隊列

7.1 消息隊列數據結構

7.2 初始化消息隊列

7.3 發送消息隊列

7.4 接收消息隊列


1 線程內置定時器

爲什麼要從線程內置定時器講起?因爲RT-Thread的IPC是依靠線程內置定時器實現的,這裏簡單敘述下線程內置定時器,想要具體深入這部分的知識,可以參考《RT-Thread內核實現與應用開發實戰指南》的第11章——定時器的實現。在RT-Thread(以下簡稱RTT)中線程定義中,有一個struct rt_timer thread_timer成員,即線程內置定時器,如下面的程序所示。

struct rt_thread
{
    char        name[RT_NAME_MAX];                      /* 線程的名字 */
    rt_uint8_t  type;                                   /* 類型 */
    rt_uint8_t  flags;                                  /* 標誌位 */

    rt_list_t   list;                                   /* 對象鏈表節點 */
    rt_list_t   tlist;                                  /* 線程鏈表節點*/

    void       *sp;                                     /* 線程sp指針 */
    void       *entry;                                  /* 線程入口地址 */
    void       *parameter;                              /* 線程入口參數 */
    void       *stack_addr;                             /* 線程棧地址 */
    rt_uint32_t stack_size;                             /* 線程棧大小 */

    rt_err_t    error;                                  /* 線程錯誤狀態 */

    rt_uint8_t  stat;                                   /* 線程狀態*/

    rt_uint8_t  current_priority;                       /* 線程當前優先級*/
    rt_uint8_t  init_priority;                          /* 線程初始優先級 */
    rt_uint32_t number_mask;

#if defined(RT_USING_EVENT)
    rt_uint32_t event_set;                              /* 線程等待的事件 */
    rt_uint8_t  event_info;                             /* 線程事件參數 */
#endif

    rt_ubase_t  init_tick;                              /* 初始時間片 */
    rt_ubase_t  remaining_tick;                         /* 剩餘時間片*/

    struct rt_timer thread_timer;                       /* 線程內置定時器 */

    void (*cleanup)(struct rt_thread *tid);             /* 線程退出回調 */

    rt_uint32_t user_data;                              /* 用戶私有數據指針 */
};

在RTT中,使用了雙向鏈表來管理定時器,在啓動一個定時器時,RTT會根據該定時器的超時時間來將該節點插入合適的位置,即根據超時時間按升序排序。

RTT的定時器有硬件定時器(系統定時器)和軟件定時器之分,這裏的硬件定時器一般就是指systick,在systick中斷裏會輪詢硬件定時器鏈表,如果鏈表的第一個節點沒有超時,那麼後面的節點必定沒有超時,如果定時器超時了,就會調用定時器的超時回調函數(在中斷裏直接調用回調,因此回調函數應儘量簡短)。軟件定時器的實現機制和硬件定時器大致相似,也是根據超時時間做升序排序處理,不同的是軟件定時器是創建了一個系統定時器線程來處理軟件定時器的超時,但是系統定時器線程所實際上要依靠硬件定時器來及時喚醒系統定時器線程。

言歸正傳,線程內置定時器用於實現線程掛起等待功能,包含線程休眠、等待信號量等。比如當調用rt_thread_delay時,RTT會先當前線程掛起,然後設置線程內置定時器的超時時間爲delay的時間,並開啓該定時器。

rt_err_t rt_thread_delay(rt_tick_t tick)
{
    return rt_thread_sleep(tick);
}

rt_err_t rt_thread_sleep(rt_tick_t tick)
{
    register rt_base_t temp;
    struct rt_thread *thread;

    /* 獲取當前線程 */
    thread = rt_thread_self();

    /* 關閉中斷*/
    temp = rt_hw_interrupt_disable();

    /* 掛起線程 */
    rt_thread_suspend(thread);

    /* 設置線程內置定時器的超時時間,並啓動 */
    rt_timer_control(&(thread->thread_timer), RT_TIMER_CTRL_SET_TIME, &tick);
    rt_timer_start(&(thread->thread_timer));

    /* 開啓中斷*/
    rt_hw_interrupt_enable(temp);

    rt_schedule();

    /* 執行到這裏時,線程的延時已經結束,超時回調會將thread->error置爲-RT_ETIMEOUT,
     * rt_thread_sleep導致的超時不應該算錯誤狀態,所以這裏手動改成RT_EOK
     */
    if (thread->error == -RT_ETIMEOUT)
        thread->error = RT_EOK;

    return RT_EOK;
}

當systick中斷裏輪詢到該定時器超時時便會調用定時器超時回調。我們已經可以大致猜到這個超時回調會做什麼了吧?應該就是該線程從掛起隊列中移除並放入就緒隊列。rt_thread_timeout函數就是所有線程內置定時器的超時回調,記住,是所有!所以說,線程內置定時器用於實現線程阻塞等待功能,包含線程休眠、等待信號量等,換句話來說就是線程在休眠前給自己定了一個何時醒來的鬧鐘。扯了這麼多,實際上是想說IPC的實現和rt_thread_sleep是相似的,例如rt_sem_take如果有超時,當獲取不到sem時會導致線程掛起並開啓內置定時器,只不過當對應的sem被釋放時,等待的線程會被提早喚醒並關閉線程內置定時器。

void rt_thread_timeout(void *parameter)
{
    struct rt_thread *thread;

    thread = (struct rt_thread *)parameter;

    /* 設置線程狀態爲超時 */
    thread->error = -RT_ETIMEOUT;

    /* 從掛起隊列中移除自身節點 */
    rt_list_remove(&(thread->tlist));

    /* 插入到就緒隊列 */
    rt_schedule_insert_thread(thread);

    /* 啓動調度 */
    rt_schedule();
}

2 IPC父類對象

RTT的整個實現都採用了面向對象的思想,包括IPC的實現。RTT實現了一個IPC類,IPC類是從rt_object類繼承而來的,而信號量、互斥量等子類又是繼承了IPC類,這種面向對象的編程思想很大程度上避免了重複造輪子,提升了代碼的重用性。

子類繼承父類時,會繼承父類的(非私有)變量和方法,本章節敘述的是IPC父類的變量和方法。

2.1 IPC父類數據結構

下面是rt_ipc_object的數據結構,可以看到,rt_ipc_object繼承自rt_object。rt_object包括名字、類型、標誌位、節點這些基本參數;suspend_thread則是因該IPC而掛起的線程鏈表節點,實際上是首節點。線程有一個成員tlist(可以看第一章節的struct rt_thread定義),當線程因該IPC而掛起時,線程的tlist節點將掛在suspend_thread鏈表上。簡單來說,suspend_thread就是用於記錄有哪些線程因該IPC而阻塞

struct rt_object
{
    char       name[RT_NAME_MAX];     /* 對象的名字 */
    rt_uint8_t type;                  /* 對象的類型*/
    rt_uint8_t flag;                  /* 標誌位,用於記錄一些設置*/
    rt_list_t  list;                  /* 對象鏈表節點 */
};

struct rt_ipc_object
{
    struct rt_object parent;          /* 從rt_object繼承的父類 */

    rt_list_t        suspend_thread;  /* 因此而掛起的線程鏈表節點 */
};

2.2 IPC父類方法

接下來我們來看一下IPC父類的方法,由於比較簡單,所以下面將實現的原理作爲註釋和代碼放在一起,不再額外敘述了。

/* 初始化一個rt_ipc_object父類 */
rt_inline rt_err_t rt_ipc_object_init(struct rt_ipc_object *ipc)
{
    /* 初始化suspend_thread鏈表,即head和tail都指向自己 */
    rt_list_init(&(ipc->suspend_thread));

    /* 這裏不初始化rt_ipc_object 的父類,而是交給rt_ipc_object的子類來實現 */

    return RT_EOK;
}

/* 將線程掛起在某個鏈表節點上 */
rt_inline rt_err_t rt_ipc_list_suspend(rt_list_t        *list,
                                       struct rt_thread *thread,
                                       rt_uint8_t        flag)
{
    /* 掛起線程 */
    rt_thread_suspend(thread);

    /*
     * RTT的IPC有2中方式:FIFO模式和PRIO模式
     * FIFO模式即先入先出模式,最先請求的線程將優先獲得IPC
     * PRIO模式即優先級模式,正在等待的優先級最高的線程將獲得IPC
     */
    switch (flag)
    {
    case RT_IPC_FLAG_FIFO:
        /* 插入到list鏈表首節點的前面,即插入到list鏈表的末尾 */
        rt_list_insert_before(list, &(thread->tlist));  
        break;

    case RT_IPC_FLAG_PRIO:
        {
            struct rt_list_node *n;
            struct rt_thread *sthread;

            /* 根據優先級來插入到合適的位置 */
            for (n = list->next; n != list; n = n->next)
            {
                sthread = rt_list_entry(n, struct rt_thread, tlist);

                /* 根據優先級來查找,優先級越高越靠前*/
                if (thread->current_priority < sthread->current_priority)
                {
                    rt_list_insert_before(&(sthread->tlist), &(thread->tlist));
                    break;
                }
            }

            /* 如果找不到合適的位置,就退化成FIFO模式,比如所有線程的優先級相同的情況 */
            if (n == list)
                rt_list_insert_before(list, &(thread->tlist));
        }
        break;
    }

    return RT_EOK;
}

/* 恢復IPC掛起鏈表的第一個線程 */
rt_inline rt_err_t rt_ipc_list_resume(rt_list_t *list)
{
    struct rt_thread *thread;

    /* 
     * 獲取list鏈表的第一個掛起線程指針
     * rt_list_entry實際上就是rt_container_of宏,這個宏在linux和RTT裏大量使用
     * 其作用是根據結構體成員的某一地址來找到結構體的首地址
     */
    thread = rt_list_entry(list->next, struct rt_thread, tlist);

    /* 
     * 恢復該線程,rt_thread_resume內部會將thread的tlist節點從對應的掛起鏈表
     * 中移除,並關閉線程內置定時器 
     */
    rt_thread_resume(thread);

    return RT_EOK;
}

/* 恢復IPC掛起鏈表的所有線程 */
rt_inline rt_err_t rt_ipc_list_resume_all(rt_list_t *list)
{
    struct rt_thread *thread;
    register rt_ubase_t temp;

    /* 遍歷鏈表喚醒因該IPC而掛起的所有線程 */
    while (!rt_list_isempty(list))
    {
        /* 關閉中斷 */
        temp = rt_hw_interrupt_disable();

        /* 獲取鏈表的下一個節點*/
        thread = rt_list_entry(list->next, struct rt_thread, tlist);

        /* 設置線程錯誤狀態爲錯誤 */
        thread->error = -RT_ERROR;

        /* 恢復該線程,rt_thread_resume內部會將thread的tlist節點從對應的掛起鏈表中移除 */
        rt_thread_resume(thread);

        /* 開啓中斷 */
        rt_hw_interrupt_enable(temp);
    }

    return RT_EOK;
}

 有了IPC父類的最基本的掛起和恢復功能,就很容易衍生出各種IPC子類了。

3 信號量

3.1 信號量數據結構

信號量常用作線程同步、資源計數等場景,信號量的實現是所有的IPC中最簡單的,從下面信號量的數據結構就可以看出,信號量是IPC的子類,僅僅在IPC的基礎上增加了一個value變量,value即信號量的值,當value只能爲0或1時,這種信號量就稱爲二值信號量。

struct rt_semaphore
{
    struct rt_ipc_object parent;           /* 繼承自IPC父類 */
    rt_uint16_t          value;            /* 信號量的值 */
};

3.2 初始化信號量

RTT提供的API從申請和釋放方式來說,可以分爲兩類:靜態和動態。靜態API即函數名包含init、detach的API,其所需的內存由用戶提供;動態即函數名包含create、delete,其所需內存由動態內存來管理。這兩類API的實現除了內存管理方式不同,在實現上幾乎是一樣的,因此下面僅選取靜態方式作講解。

信號量在初始化時可以設置初值,一般在使用時,這個初值就代表的資源的多少。下面的函數是信號量的初始化與卸載。

/* 初始化信號量 */
rt_err_t rt_sem_init(rt_sem_t    sem,
                     const char *name,
                     rt_uint32_t value,
                     rt_uint8_t  flag)
{
    /* 初始化“爺爺”類,即object類 */
    rt_object_init(&(sem->parent.parent), RT_Object_Class_Semaphore, name);

    /* 初始化IPC父類 */
    rt_ipc_object_init(&(sem->parent));

    /* 設置信號量的初始值 */
    sem->value = value;

    /* 設置object類的屬性,這裏一般選擇FIFO模式或者PRIO模式 */
    sem->parent.parent.flag = flag;

    return RT_EOK;
}

/* 卸載信號量 */
rt_err_t rt_sem_detach(rt_sem_t sem)
{
    /* 喚醒因該信號量而掛起的所有線程 */
    rt_ipc_list_resume_all(&(sem->parent.suspend_thread));

    /* 將對象節點從對象鏈表中移除 */
    rt_object_detach(&(sem->parent.parent));

    return RT_EOK;
}

3.3 請求信號量

請求信號量函數rt_sem_take函數如下所示,其原理比較簡單,如果信號量可用,即value大於0,那麼表示成功獲取到信號量,同時value減1;如果信號量不可用,即value等於0,那麼需要判斷入參time,time爲0表示不等待立即返回,time不爲0則掛起線程並啓動線程內置定時器,如果看懂了第1章和第2章的內容,這裏是一眼就能看出來的。

需要注意的是,RT_DEBUG_IN_THREAD_CONTEXT這個宏定義會檢查當前是否處於線程環境,這就說明不建議在中斷服務函數裏請求信號量,一旦信號量不可用,而且超時時間不爲0,就會發生錯誤。一般使用的情景都是在中斷服務函數裏釋放信號量來同步線程。

/* 獲取信號量 */
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time)
{
    register rt_base_t temp;
    struct rt_thread *thread;

    temp = rt_hw_interrupt_disable();

    /* 如果信號量不爲0,信號量減1並立即返回 */
    if (sem->value > 0)
    {
        sem->value --;
        rt_hw_interrupt_enable(temp);
    }
    /* 如果信號量已經爲0,那麼就要看是否設置了超時 */
    else
    {
        /* 如果超時時間爲0,那麼立即返回超時 */
        if (time == 0)    
        {    
            rt_hw_interrupt_enable(temp);

            return -RT_ETIMEOUT;
        }
        /* 超時時間不爲0,需要掛起線程進行等待 */
        else
        {
            /* 檢查當前是否處於RTOS運行狀態,即檢查是否已經初始化過RTT */
            RT_DEBUG_IN_THREAD_CONTEXT;

            /* 獲取當前線程*/
            thread = rt_thread_self();

            /* 復位線程錯誤標誌*/
            thread->error = RT_EOK;

            /* 將線程掛起,並記錄到信號量的掛起鏈表上 */
            rt_ipc_list_suspend(&(sem->parent.suspend_thread),
                                thread,
                                sem->parent.parent.flag);

            /* 判斷超時時間是否爲永久(-1),若非永久則啓動線程內置定時器 */
            if (time > 0)
            {
                /* 設置線程內置定時器的超時時間爲信號量的等待時間,並啓動 */
                rt_timer_control(&(thread->thread_timer),
                                 RT_TIMER_CTRL_SET_TIME,
                                 &time);
                rt_timer_start(&(thread->thread_timer));
            }

            rt_hw_interrupt_enable(temp);

            /* 發起線程調度 */
            rt_schedule();

            if (thread->error != RT_EOK)
            {
                /* 如果執行到這裏,說明一直沒有等到信號量,超時了 */
                return thread->error;
            }
        }
    }

    return RT_EOK;
}

/* 嘗試獲取信號量,非阻塞 */
rt_err_t rt_sem_trytake(rt_sem_t sem)
{
    /* 嘗試獲取信號量:超時時間爲0,當獲取不到時也會立即返回 */
    return rt_sem_take(sem, 0);
}

3.4 釋放信號量

釋放信號量函數非常簡短,如果釋放時suspend_thread鏈表非空,說明有線程因請求該信號量被掛起,那麼就喚醒suspend_thread鏈表的第一個節點的線程;反之,只需要簡單地讓信號量的value值加1即可。

/* 釋放信號量 */
rt_err_t rt_sem_release(rt_sem_t sem)
{
    register rt_base_t temp;
    register rt_bool_t need_schedule;

    need_schedule = RT_FALSE;

    temp = rt_hw_interrupt_disable();

    /* 釋放信號量時如果有線程因此信號量而掛起,那麼需要立即發起線程調度,否則將信號量加1 */
    if (!rt_list_isempty(&sem->parent.suspend_thread))
    {
        /* 恢復信號量掛起鏈表的第一個節點對應的線程 */
        rt_ipc_list_resume(&(sem->parent.suspend_thread));
        need_schedule = RT_TRUE;
    }
    else
        sem->value ++; /* 信號量加1 */

    rt_hw_interrupt_enable(temp);

    /* 如果需要調度,那麼發起線程調度*/
    if (need_schedule == RT_TRUE)
        rt_schedule();

    return RT_EOK;
}

4 互斥量

使用信號量作爲公共資源保護時,會存在線程優先級反轉問題,互斥量這類IPC就是爲了解決這個問題而誕生的,RTT的互斥量採用了臨時提升佔有互斥量線程優先級的方法,即優先級繼承,在後面的代碼分析中會有所體現。

4.1 互斥量數據結構

互斥量的數據結構如下所示,value的作用和信號量一樣,但其實value的值只會是0或1。original_priority用於記錄佔有該信號量的原始優先級,因爲當互斥量被一個低優先級的線程佔有後,當有高優先級線程因請求該互斥量而掛起時,會將這個低優先級的線程的優先級提升到和高優先級線程一致,當互斥量被原先的低優先級線程釋放後,要將該線程的優先級還原到原先的低優先級。hold成員要和owner成員一起來看,owner記錄了當前佔有該信號量的線程,從這裏就可以看出互斥量和線程的一種所屬關係,即owner只能被單一的某個線程佔有,而不能被同時佔有。hold變量就是用來記錄所屬線程佔有該互斥量的次數,簡單來說,當hold爲0時,owner指向空,value爲1;當hold不爲0時,owner必定指向某個線程,value爲0。

struct rt_mutex
{
    struct rt_ipc_object parent;               /* 繼承自IPC父類 */

    rt_uint16_t          value;                /* 互斥量的值 */

    rt_uint8_t           original_priority;    /* 佔有該互斥量的原始優先級 */
    rt_uint8_t           hold;                 /* 線程佔有該信號量的次數*/
    struct rt_thread    *owner;                /* 佔有該互斥量的線程 */
};

4.2 初始化互斥量

rt_mutex_init函數用於初始化一個互斥量,可以和rt_sem_init函數對比一下,可以發現互斥量在初始化時,其value值是固定爲1,而信號量則由初始化函數入參設置。互斥量初始化時,還沒有歸屬線程,因此owner指向空,hold爲0。

rt_err_t rt_mutex_init(rt_mutex_t mutex, const char *name, rt_uint8_t flag)
{
    /* 初始化“爺爺”類,即object類 */
    rt_object_init(&(mutex->parent.parent), RT_Object_Class_Mutex, name);

    /* 初始化IPC父類 */
    rt_ipc_object_init(&(mutex->parent));

    mutex->value = 1;                   
    mutex->owner = RT_NULL;             
    mutex->original_priority = 0xFF;
    mutex->hold  = 0;

    /* 設置object類的屬性,這裏一般選擇FIFO模式或者PRIO模式 */
    mutex->parent.parent.flag = flag;

    return RT_EOK;
}

4.3 請求互斥量

互斥量的請求相較於信號量的請求要稍複雜些,代碼如下,從rt_mutex_take函數源碼中我們可以分析出以下幾點:

1.不允許在中斷裏請求互斥量,即使設置的超時時間爲0。這是因爲互斥量和線程具有歸屬關係,當成功請求到互斥量時,互斥量需要重新記錄當前被哪個線程所佔有,如果中斷成功請求到了互斥量,那麼還怎麼記錄呢?因爲中斷根本不是線程啊!

2.請求互斥量會導致hold值加1,hold具體在何時使用要看互斥量釋放函數rt_mutex_release,下文會提到。

3.互斥量的值在非0時,互斥量必定被某個線程佔有,結合分析rt_mutex_release時將會得出互斥量的值只能是0或1的結論。

4.若請求互斥量的線程的優先級比已佔有互斥量的線程的優先級高,且前者因此被掛起,那麼後者的優先級將被提升到和前者一樣高。

/* 請求互斥量 */
rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time)
{
    register rt_base_t temp;
    struct rt_thread *thread;

    /* this function must not be used in interrupt even if time = 0 */
    /* 上面的一段英文是官方的註釋,很重要! */
    RT_DEBUG_IN_THREAD_CONTEXT;   /* 確保當前處於線程中而不是中斷服務函數裏 */

    /* 獲取當前線程 */
    thread = rt_thread_self();

    temp = rt_hw_interrupt_disable();

    thread->error = RT_EOK;

    if (mutex->owner == thread)     /* 說明是佔有該互斥量的線程請求的,hold值加1 */
    {
        mutex->hold ++;
    }
    else                            /* 是其他線程請求的 */
    {
__again:
        /* 如果value大於0,說明互斥量還沒有被請求過,也就是還沒有被任何線程佔有 */
        if (mutex->value > 0)
        {
            /* 成功請求到互斥量,value = 0 */
            mutex->value --;

            /* 記錄被哪個線程佔有,並記錄這個線程的當前優先級 */
            mutex->owner             = thread;
            mutex->original_priority = thread->current_priority;
            mutex->hold ++;
        }
        else    /* 到此處說明互斥量已經被其他線程佔有,不可用 */
        {
            if (time == 0)         /* 超時時間爲0,立即返回超時 */
            {
                thread->error = -RT_ETIMEOUT;

                rt_hw_interrupt_enable(temp);

                return -RT_ETIMEOUT;
            }
            else                   /* 設置了超時時間,需要掛起當前線程 */ 
            {
                /* 如果當前請求的線程的優先級比互斥量佔有線程的優先級高,那麼提升後者的優先級*/
                if (thread->current_priority < mutex->owner->current_priority)
                {
                    /* 將互斥量佔有線程的優先級調到和當前請求的線程的優先級一樣高 */
                    rt_thread_control(mutex->owner,
                                      RT_THREAD_CTRL_CHANGE_PRIORITY,
                                      &thread->current_priority);
                }

                /* 將當前線程掛起在該互斥量的suspend_thread鏈表上 */
                rt_ipc_list_suspend(&(mutex->parent.suspend_thread),
                                    thread,
                                    mutex->parent.parent.flag);

                /* 判斷超時時間是否爲永久(-1),若非永久則啓動線程內置定時器 */
                if (time > 0)
                {
                    /* 設置線程內置定時器的超時時間並啓動 */
                    rt_timer_control(&(thread->thread_timer),
                                     RT_TIMER_CTRL_SET_TIME,
                                     &time);
                    rt_timer_start(&(thread->thread_timer));
                }

                rt_hw_interrupt_enable(temp);

                /* 發起線程調度,當前線程至此被掛起 */
                rt_schedule();    

                /* 因某種原因線程被喚醒,判斷線程錯誤狀態 */
                if (thread->error != RT_EOK)
                {
                    /* 被信號打斷,回到__again的位置重新判斷請求狀態 */
                    if (thread->error == -RT_EINTR) 
                        goto __again;

                    return thread->error;
                }
                else
                {
                    /* 運行到這裏說明成功獲取到互斥量了 */
                    /* 這裏關閉中斷是爲了和最下面的rt_hw_interrupt_enable成對調用*/
                    temp = rt_hw_interrupt_disable();
                }
            }
        }
    }
    rt_hw_interrupt_enable(temp);

    return RT_EOK;
}

4.4 釋放互斥量

互斥量的釋放相較於信號量的釋放也要稍複雜些,代碼如下,從rt_mutex_release函數源碼中我們可以分析出以下幾點:

1.不允許在中斷裏釋放互斥量,因爲互斥量只能由互斥量的佔有線程釋放

2.釋放互斥量會導致hold值減1,當hold值減到0時,才允許互斥量被另一個線程佔有或不被任何線程佔有。

3.互斥量的值只能0或1,即互斥量是一種特殊的二值信號量。

4.互斥量被成功釋放需要易主時,如果當前線程(互斥量的佔有線程)優先級曾經被提升過,優先級會被改回來。

/* 釋放互斥量 */
rt_err_t rt_mutex_release(rt_mutex_t mutex)
{
    register rt_base_t temp;
    struct rt_thread *thread;
    rt_bool_t need_schedule;

    need_schedule = RT_FALSE;

    /* only thread could release mutex because we need test the ownership */
    RT_DEBUG_IN_THREAD_CONTEXT;

    thread = rt_thread_self();

    temp = rt_hw_interrupt_disable();

    /* 只有互斥量的佔有線程能釋放該互斥量,否則返回錯誤 */
    if (thread != mutex->owner)
    {
        thread->error = -RT_ERROR;

        rt_hw_interrupt_enable(temp);

        return -RT_ERROR;
    }

    /* hold減1 */
    mutex->hold --;

    /* hold爲0時,互斥量才能易主或無主 */
    if (mutex->hold == 0)    
    {
        /* 如果互斥量的佔有線程優先級被修改過,在此處修改回來 */
        if (mutex->original_priority != mutex->owner->current_priority)
        {
            rt_thread_control(mutex->owner,
                              RT_THREAD_CTRL_CHANGE_PRIORITY,
                              &(mutex->original_priority));
        }

        /* 如果當前有線程因請求該互斥量而掛起,恢復suspend_thread鏈表第一個節點線程 */
        if (!rt_list_isempty(&mutex->parent.suspend_thread))
        {
            thread = rt_list_entry(mutex->parent.suspend_thread.next,
                                   struct rt_thread,
                                   tlist);

            /* 互斥量易主 */
            mutex->owner             = thread;
            mutex->original_priority = thread->current_priority;
            mutex->hold ++;

            /* 喚醒線程 */
            rt_ipc_list_resume(&(mutex->parent.suspend_thread));

            /* 因爲有線程需要被喚醒,所以需要調度一次 */
            need_schedule = RT_TRUE;
        }
        /* 沒有線程在請求該互斥量,互斥量改爲無主狀態,即初始狀態 */  
        else
        {
            /* value加1,實際上就是等於1 */
            mutex->value ++;

            mutex->owner             = RT_NULL;
            mutex->original_priority = 0xff;
        }
    }

    rt_hw_interrupt_enable(temp);

    /* 如果需要調度,那麼立即調度 */
    if (need_schedule == RT_TRUE)
        rt_schedule();

    return RT_EOK;
}

5 事件

在裸機中,我們常用標誌位的方式來做異步通知,例如在中斷服務函數裏置標誌,在主循環中輪詢標誌做事件的處理。在RTT中,也有這樣的機制,並且融合了IPC功能,已經給我們封裝的很完善了。

5.1 事件數據結構

事件的數據結構如下所示,從數據結構上來看,事件和信號量比較接近。事件的set值用於記錄事件,而信號量的value值用於記錄資源的多少。如果使用二值信號量來做線程的同步,此時用單個事件也可以實現,即事件和信號量存在部分交集,具體用事件還是信號量取決於具體應用。

set是rt_uint32_t類型的,也就意味着RTT的線程最多可以同時等待32個事件。此外,還需要線程的定義,RTT的線程定義中有2個事件相關的變量event_set和event_info(可以從第1章看到),event_set即線程在等待的事件集,event_info用於記錄事件的參數,例如以何種方式等待事件、接收到事件是否需要自動清空事件。

RTT的線程等待事件具有兩種等待方式:“與”、“或”。當線程請求多個事件時,如果以“與”的方式請求,只有這些事件全都發生時,線程纔會被喚醒;如果以“或”的方式請求,那麼只要等待的任意一個事件發生時,線程就會被喚醒。RTT的事件實現原理:通過event_info中的運算方式(“與或”)比對事件的set值和線程的event_set值來判斷線程是否獲得了所期望的事件。

struct rt_event
{
    struct rt_ipc_object parent;          /* 繼承自IPC父類 */
    rt_uint32_t          set;             /* 事件集,每個bit位記錄一個事件*/
};
typedef struct rt_event *rt_event_t;

5.2 初始化事件

初始化事件沒啥好說的,和信號量的初始化是一樣的套路。

/* 初始化事件 */
rt_err_t rt_event_init(rt_event_t event, const char *name, rt_uint8_t flag)
{
    /* 初始化“爺爺”類,即object類 */
    rt_object_init(&(event->parent.parent), RT_Object_Class_Event, name);

    /* 設置object類的屬性,這裏一般選擇FIFO模式或者PRIO模式 */
    event->parent.parent.flag = flag;

    /* 初始化IPC父類 */
    rt_ipc_object_init(&(event->parent));

    /* 初始化時沒有事件,事件集清零 */
    event->set = 0;

    return RT_EOK;
}

5.3 發送事件

發送事件時,事件會被記錄到event的set中,然後遍歷其IPC掛起鏈表,如果set與線程的event_set值相匹配,那麼就會喚醒該線程。

通過以下源碼的分析,可以得出以下幾點需要注意的點:

1.不允許發送空事件,即set的值不能爲0;但允許發送多個事件,即RTT沒有要求set的值僅有1個bit位置1。

2.當線程以“或”的方式等待事件時,若成功等到了事件,線程記錄的event_set會被替換成最近一次事件的值。

3.當線程設置了RT_EVENT_FLAG_CLEAR且成功等到了事件時,會將event的對應的set值清除。這裏就需要特別注意了!當有多個線程都在等待同一事件時,第一個線程成功等到事件時就已經把set對應的位清零了,後面的線程會等不到這個事件。

/* 發送事件 */
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set)
{
    struct rt_list_node *n;
    struct rt_thread *thread;
    register rt_ubase_t level;    
    register rt_base_t status;    /* 標記線程是否請求到了期望的事件集 */
    rt_bool_t need_schedule;      /* 標記是否需要發起線程調度 */

    /* 不允許發送空事件 */
    if (set == 0)
        return -RT_ERROR;

    need_schedule = RT_FALSE;

    level = rt_hw_interrupt_disable();

    /* 記錄當前發生的事件 */
    event->set |= set;
    
    /* 以下遍歷因請求該事件集而掛起的線程鏈表 */
    if (!rt_list_isempty(&event->parent.suspend_thread))
    {
        /* search thread list to resume thread */
        n = event->parent.suspend_thread.next;
        while (n != &(event->parent.suspend_thread))
        {
            /* 獲取線程句柄 */
            thread = rt_list_entry(n, struct rt_thread, tlist);

            status = -RT_ERROR;
            /* 如果線程以“與”的方式請求事件集,進一步判斷是否收到了全部所期望的事件集 */
            if (thread->event_info & RT_EVENT_FLAG_AND)
            {
                if ((thread->event_set & event->set) == thread->event_set)
                {
                    /* received an AND event */
                    status = RT_EOK;
                }
            }
            /* 如果線程以“或”的方式請求事件集,進一步判斷是否收到了所期望的其中任一事件 */
            else if (thread->event_info & RT_EVENT_FLAG_OR)
            {
                if (thread->event_set & event->set)
                {
                    /* 此處要讓線程記錄最近一次接收到的事件 */
                    thread->event_set = thread->event_set & event->set;

                    /* received an OR event */
                    status = RT_EOK;
                }
            }

            /* 獲取鏈表下一節點 */
            n = n->next;

            /* 線程成功請求到了期望的事件集 */
            if (status == RT_EOK)
            {
                /*
                 * 如果線程設置了接收事件成功後自動清空事件集,
                 * 那麼清空線程所期望的事件集,否則下次請求時就會立即請求到
                 */
                if (thread->event_info & RT_EVENT_FLAG_CLEAR)
                    event->set &= ~thread->event_set;

                /* 恢復線程*/
                rt_thread_resume(thread);

                /* 標記需要調度 */
                need_schedule = RT_TRUE;
            }
        }
    }

    rt_hw_interrupt_enable(level);

    if (need_schedule == RT_TRUE)
        rt_schedule();

    return RT_EOK;
}

5.4 接收事件

在線程請求事件時,即調用rt_event_recv時,首先會立即檢查一下當前的事件集是否已經滿足線程的期望,如果已經滿足,那麼相當於已經成功等到了期望的事件,否則就需要等待。如果等待時間爲0,那麼立即返回超時;如果等待時間不爲0,那麼掛起線程等待期望的事件集發生。需要注意的是,當用“或”的方式等待事件且成功時,RTT會幫我們清空event->set對應的位,這個很好理解,類比裸機,我們一般在中斷服務函數裏置標記,在主循環輪詢到標記置位後,先清零標誌位再執行對應的處理。

/* 請求事件 */
rt_err_t rt_event_recv(rt_event_t   event,
                       rt_uint32_t  set,
                       rt_uint8_t   option,
                       rt_int32_t   timeout,
                       rt_uint32_t *recved)
{
    struct rt_thread *thread;
    register rt_ubase_t level;
    register rt_base_t status;

    /* 只能在線程環境中等待事件 */
    RT_DEBUG_IN_THREAD_CONTEXT;

    /* 不能設置等待空事件 */
    if (set == 0)
        return -RT_ERROR;

    status = -RT_ERROR;

    thread = rt_thread_self();

    thread->error = RT_EOK;

    level = rt_hw_interrupt_disable();

    /* 檢查等待的方式,是否是“與”的方式 */
    if (option & RT_EVENT_FLAG_AND)
    {
        /* 檢查當前是否已經發生了所有期望的事件,若已經發生,說明成功等到了事件 */
        if ((event->set & set) == set)
            status = RT_EOK;
    }
     /* 檢查等待的方式,是否是“或”的方式 */
    else if (option & RT_EVENT_FLAG_OR)
    {
        /* 檢查當前是否已經發生了期望的事件之一,若已經發生,說明成功等到了事件 */
        if (event->set & set)
            status = RT_EOK;
    }
    else
    {
        /* 只能用“與”或者“或”這兩種方式之一來請求事件 */
        RT_ASSERT(0);
    }

    / 已經成功請求到事件 */
    if (status == RT_EOK)
    {
        /* 將接收到的事件通過recved指針傳回 */
        if (recved)
            *recved = (event->set & set);

        /* 以“或”的方式請求事件,成功後需要清空對應的事件位,否則相當於等待的事件一直髮生 */
        if (option & RT_EVENT_FLAG_CLEAR)
            event->set &= ~set;
    }
    /* 還沒有等到的期望的事件集發生,判斷是否設置了超時等待 */
    else if (timeout == 0)
    {
        /* 不等待,立即返回超時 */
        thread->error = -RT_ETIMEOUT;
    }
    /* 還沒有等到的期望的事件集發生,且設置了超時,那麼掛起線程進行超時等待 */
    else
    {
        /* 設置當前線程所期望的事件集和操作*/
        thread->event_set  = set;
        thread->event_info = option;

        /* 將線程掛起在事件的suspend_thread鏈表上 */
        rt_ipc_list_suspend(&(event->parent.suspend_thread),
                            thread,
                            event->parent.parent.flag);

        /* 判斷超時時間是否爲永久(-1),若非永久則啓動線程內置定時器 */
        if (timeout > 0)
        {
            rt_timer_control(&(thread->thread_timer),
                             RT_TIMER_CTRL_SET_TIME,
                             &timeout);
            rt_timer_start(&(thread->thread_timer));
        }

        rt_hw_interrupt_enable(level);

        /* 發起線程調度*/
        rt_schedule();

        /* 執行到這裏,要麼超時了,要麼成功等到了事件集發生了 */

         /* 超時了 */
        if (thread->error != RT_EOK)   
        {
            /* return error */
            return thread->error;
        }

        
        /* 成功等到了事件集發生了 */
        level = rt_hw_interrupt_disable();

        /* 將接收到的事件通過recved指針傳回 */
        if (recved)
            *recved = thread->event_set;
    }

    rt_hw_interrupt_enable(level);

    return thread->error;
}

6 郵箱

RTT的郵箱可用於線程於線程之間、中斷與線程之間的異步消息通信。

6.1 郵箱數據結構

郵箱的數據結構如下所示,其中msg_pool是郵箱的內存地址,size是郵箱的總大小,注意這裏的size不是指msg_pool內存的大小,而是值郵箱的容量,即能容納多少封郵件,由於RTT的一封郵件的大小是4字節,因此所需的msg_pool的大小爲4*size,這在郵箱初始化時要特別注意。entry用於記錄當前郵箱中有多少封郵件,判斷entry是否爲0即可得知郵箱中是否有郵件,判斷entry和size是否相等即可得知郵箱是否已經被塞滿。簡單地講,郵箱就是一個rt_ubase_t類型的數組,數組的項數爲size,當往郵箱投遞一封郵件時,in_offset就會加1;當從郵箱讀取一封郵件時,out_offset就會加1。也就是說in_offset和out_offset用於記錄msg_pool數組當前的讀寫位置。當發送一封郵件時,如果郵箱已經被塞滿且設置了超時時間,那麼發送該郵件的線程將會被掛起在suspend_sender_thread鏈表上,直到郵箱空出了位置。

郵箱和信號量、互斥量、事件的其中一點區別需要特別注意:線程阻塞地發送或接收郵件時,即使中途被其他方式喚醒(線程內置定時器導致的超時除外),還會繼續阻塞地請求發送或接收。這點在下面的分析中會體現出來。

因爲郵箱只能存4字節的內容,所以郵箱適合傳輸一些精簡的消息,當然,郵箱剛好可以傳遞一個指針,這樣就可以傳輸大量的內容,但是指針指向的內容可能隨時會被更改,所以這種情況下最好另外加保護鎖。

struct rt_mailbox
{
    struct rt_ipc_object parent;                   /* 繼承自IPC父類 */

    rt_ubase_t          *msg_pool;                 /* 郵箱內存地址 */
    rt_uint16_t          size;                     /* 郵箱的總容量*/
    rt_uint16_t          entry;                    /* 當前郵箱中郵件的數量 */
    rt_uint16_t          in_offset;                /* 記錄郵箱寫入偏移*/
    rt_uint16_t          out_offset;               /* 記錄郵箱讀出偏移*/
    rt_list_t            suspend_sender_thread;    /*因發送郵郵件而掛起的線程鏈表 */
};
typedef struct rt_mailbox *rt_mailbox_t;

6.2 初始化郵箱

/* 初始化郵箱 */
rt_err_t rt_mb_init(rt_mailbox_t mb,
                    const char  *name,
                    void        *msgpool,
                    rt_size_t    size,
                    rt_uint8_t   flag)
{
    /* 初始化“爺爺”類,即object類 */
    rt_object_init(&(mb->parent.parent), RT_Object_Class_MailBox, name);

    /* 設置object類的屬性,這裏一般選擇FIFO模式或者PRIO模式 */
    mb->parent.parent.flag = flag;

    /* 初始化IPC父類 */
    rt_ipc_object_init(&(mb->parent));

    /* 初始化郵箱參數 */
    mb->msg_pool   = msgpool;
    mb->size       = size;
    mb->entry      = 0;
    mb->in_offset  = 0;
    mb->out_offset = 0;

    /* 初始化郵件發送線程掛起鏈表 */
    rt_list_init(&(mb->suspend_sender_thread));

    return RT_EOK;
}

 從rt_mb_init函數不容易看出傳入的msgpool需要多大,可以參考一下rt_mb_create函數,在rt_mb_create中有如下操作,因此可以知道msg_pool所需的大小。

mb->size     = size;
mb->msg_pool = RT_KERNEL_MALLOC(mb->size * sizeof(rt_ubase_t));

6.3 發送郵件

上文提到了當郵箱已經被塞滿時,如果此時再往該郵箱塞郵件,那麼發送郵件的線程可能被掛起,因此RTT中的發送郵件的函數名爲rt_mb_send_wait。而rt_mb_send函數則是rt_mb_send_wait超時時間爲0特殊情況,即當郵箱已經被塞滿時,立即返回-RT_EFULL。

rt_mb_send_wait函數如下,從這個發送函數,我們可以得出以下幾點結論:

1.不能在中斷服務函數裏以阻塞方式發送郵件。

2.當郵箱已滿時,如果線程選擇阻塞,那麼會被掛起到郵箱的suspend_sender_thread鏈表上,在發送成功前,即使被其他方式(線程內置定時器除外)提前喚醒,喚醒後也依然會嘗試等待郵件的發送。

rt_err_t rt_mb_send_wait(rt_mailbox_t mb,
                         rt_ubase_t   value,
                         rt_int32_t   timeout)
{
    struct rt_thread *thread;
    register rt_ubase_t temp;
    rt_uint32_t tick_delta;    /* 用於記錄過去了多長時間 */

    /* 初始化時間差 */
    tick_delta = 0;

    thread = rt_thread_self();

    temp = rt_hw_interrupt_disable();

    /* 如果郵箱已經被塞滿,且不等待,那麼立即返回-RT_EFULL */
    if (mb->entry == mb->size && timeout == 0)
    {
        rt_hw_interrupt_enable(temp);

        return -RT_EFULL;
    }

    /* 郵箱已經塞滿的情況 */
    while (mb->entry == mb->size)
    {
        thread->error = RT_EOK;

        /* 如果超時時間爲0,立即返回-RT_EFULL */
        if (timeout == 0)
        {
            rt_hw_interrupt_enable(temp);

            return -RT_EFULL;
        }
        
        /* 運行到這裏,說明timeout不等於0,需要掛起當前線程 */

        /* 檢查當前是否處於線程環境中 */
        RT_DEBUG_IN_THREAD_CONTEXT;

        /* 將當前線程掛起在郵箱的suspend_sender_thread鏈表上 */
        rt_ipc_list_suspend(&(mb->suspend_sender_thread),
                            thread,
                            mb->parent.parent.flag);

        /* 判斷超時時間是否爲永久(-1),若非永久則啓動線程內置定時器 */
        if (timeout > 0)
        {
            /* 記錄一下掛起的時刻 */
            tick_delta = rt_tick_get();

            rt_timer_control(&(thread->thread_timer),
                             RT_TIMER_CTRL_SET_TIME,
                             &timeout);
            rt_timer_start(&(thread->thread_timer));
        }

        rt_hw_interrupt_enable(temp);

        /* 線程調度 */
        rt_schedule();


        if (thread->error != RT_EOK)
        {
            /* 運行到這裏,說明等待超時了 */
            return thread->error;
        }

        temp = rt_hw_interrupt_disable();

        /* 運行到這裏,說明還沒有等到郵箱有空位,但是線程因爲其他原因被提前喚醒了 */

        /* 如果超時時間不爲永久,那麼重新計算一下剩餘超時等待時間並繼續等待 */
        if (timeout > 0)
        {
            tick_delta = rt_tick_get() - tick_delta;    /* 計算上一次掛起到現在過去了多久 */
            timeout -= tick_delta;                      /* 重新計算超時時長 */
            if (timeout < 0)        
                timeout = 0;
        }
    }

    /* 運行到這裏,說明郵箱有空位了,發送郵件 */

    mb->msg_pool[mb->in_offset] = value;    /* 記錄郵件 */
    
    ++ mb->in_offset;                       /* 寫入偏移加1 */
    if (mb->in_offset >= mb->size)          /* 寫到末尾了,回到初始位置 */
        mb->in_offset = 0;
   
    mb->entry ++;                           /* 郵箱中的郵件數量加1 */

    /* 判斷是否有線程因爲請求郵箱而掛起,若有則恢復掛起鏈表上的第一個線程 */
    if (!rt_list_isempty(&mb->parent.suspend_thread))
    {
        rt_ipc_list_resume(&(mb->parent.suspend_thread));
        rt_hw_interrupt_enable(temp);
        rt_schedule();

        return RT_EOK;
    }

    rt_hw_interrupt_enable(temp);

    return RT_EOK;
}

6.4 接收郵件

rt_mb_recv和rt_mb_send_wait非常“對稱”,看懂了rt_mb_send_wait後,rt_mb_recv也就一目瞭然了。需要注意的是以下幾點:

1.不能在中斷服務函數裏以阻塞方式接收郵件。

2.當郵箱爲空時,如果線程選擇阻塞,那麼會被掛起到郵箱的suspend_thread鏈表上,在接收成功前,即使被其他方式(線程內置定時器除外)提前喚醒,喚醒後也依然會嘗試等待郵件的接收。

/* 接收郵件 */
rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_ubase_t *value, rt_int32_t timeout)
{
    struct rt_thread *thread;
    register rt_ubase_t temp;
    rt_uint32_t tick_delta;        /* 用於記錄過去了多長時間 */

    /* 初始化時間差 */
    tick_delta = 0;

    thread = rt_thread_self();

    temp = rt_hw_interrupt_disable();

    /* 如果郵箱是空的,且不等待,那麼立即返回-RT_ETIMEOUT*/
    if (mb->entry == 0 && timeout == 0)
    {
        rt_hw_interrupt_enable(temp);

        return -RT_ETIMEOUT;
    }

    /* 郵箱是空的情況 */
    while (mb->entry == 0)
    {
        thread->error = RT_EOK;

        /* 如果超時時間爲0,立即返回-RT_ETIMEOUT*/
        if (timeout == 0)
        {
            rt_hw_interrupt_enable(temp);

            thread->error = -RT_ETIMEOUT;

            return -RT_ETIMEOUT;
        }
        /* 運行到這裏,說明timeout不等於0,需要掛起當前線程 */

        /* 檢查當前是否處於線程環境中 */
        RT_DEBUG_IN_THREAD_CONTEXT;

         /* 將當前線程掛起在郵箱的suspend_thread鏈表上 */
        rt_ipc_list_suspend(&(mb->parent.suspend_thread),
                            thread,
                            mb->parent.parent.flag);

        /* 判斷超時時間是否爲永久(-1),若非永久則啓動線程內置定時器 */
        if (timeout > 0)
        {
            /* 記錄一下掛起的時刻 */
            tick_delta = rt_tick_get();

            rt_timer_control(&(thread->thread_timer),
                             RT_TIMER_CTRL_SET_TIME,
                             &timeout);
            rt_timer_start(&(thread->thread_timer));
        }

        rt_hw_interrupt_enable(temp);

        /* 線程調度 */
        rt_schedule();

        if (thread->error != RT_EOK)
        {
            /* 運行到這裏,說明等待超時了 */
            return thread->error;
        }

        /* disable interrupt */
        temp = rt_hw_interrupt_disable();

        /* 運行到這裏,說明郵箱還是空的,但是線程因爲其他原因被提前喚醒了 */

        /* 如果超時時間不爲永久,那麼重新計算一下剩餘超時等待時間並繼續等待 */
        if (timeout > 0)
        {
            tick_delta = rt_tick_get() - tick_delta;   /* 計算上一次掛起到現在過去了多久 */
            timeout -= tick_delta;                     /* 重新計算超時時長 */
            if (timeout < 0)
                timeout = 0;
        }
    }

    /* 運行到這裏,說明郵箱非空了 */

    *value = mb->msg_pool[mb->out_offset];    /* 讀取郵件 */

    ++ mb->out_offset;                        /* 讀取偏移加1 */
    if (mb->out_offset >= mb->size)           /* 讀到末尾了,回到初始位置 */
        mb->out_offset = 0;
    mb->entry --;                             /* 郵箱中的郵件數量減1 */

    /* 判斷是否有線程因爲請求發送郵件而掛起,若有則恢復掛起鏈表上的第一個線程 */
    if (!rt_list_isempty(&(mb->suspend_sender_thread)))
    {
        rt_ipc_list_resume(&(mb->suspend_sender_thread));
        rt_hw_interrupt_enable(temp);
        rt_schedule();

        return RT_EOK;
    }

    rt_hw_interrupt_enable(temp);

    return RT_EOK;
}

7 消息隊列

RTT的消息隊列和郵箱有些相似,只是郵箱只能傳遞4字節的消息,而消息隊列則可以以拷貝的方式傳遞用戶指定長度的數據。實際上,RTT的消息隊列使用了動態內存池機制,lwip中也有這樣的動態內存池。

7.1 消息隊列數據結構

消息隊列的數據結構稍顯複雜,實際上只需關注兩點:消息隊列的大小計算、消息隊列的內置鏈表,在初始化消息隊列時將會對這兩部分內容進行分析。

struct rt_messagequeue
{
    struct rt_ipc_object parent;      /* 繼承自IPC父類 */

    void *msg_pool;                   /* 消息隊列所需的內存首地址 */

    rt_uint16_t msg_size;             /* 單個消息塊的數據容量 */
    rt_uint16_t max_msgs;             /* 消息隊列的容量,即消息塊的個數 */

    rt_uint16_t entry;                /* 已經使用的消息塊個數 */
    void *msg_queue_head;             /* 指向待讀取的消息隊列第一個的消息塊 */
    void *msg_queue_tail;             /* 指向待讀取的消息隊列最後一個消息塊 */
    void *msg_queue_free;             /* 空閒消息鏈表頭 */
};
typedef struct rt_messagequeue *rt_mq_t;

struct rt_mq_message
{
    struct rt_mq_message *next;
};

7.2 初始化消息隊列

rt_err_t rt_mq_init(rt_mq_t     mq,
                    const char *name,
                    void       *msgpool,
                    rt_size_t   msg_size,
                    rt_size_t   pool_size,
                    rt_uint8_t  flag)
{
    struct rt_mq_message *head;
    register rt_base_t temp;

    /* 初始化“爺爺”類,即object類 */
    rt_object_init(&(mq->parent.parent), RT_Object_Class_MessageQueue, name);

    /* 設置object類的屬性,這裏一般選擇FIFO模式或者PRIO模式 */
    mq->parent.parent.flag = flag;

    /* 初始化IPC父類 */
    rt_ipc_object_init(&(mq->parent));

    /* 設置消息隊列內存地址 */
    mq->msg_pool = msgpool;

    /* 計算獲取正確的參數,msg_size需要向上4字節對齊,然後計算消息隊列的實際可用內存塊數量 */
    mq->msg_size = RT_ALIGN(msg_size, RT_ALIGN_SIZE);
    mq->max_msgs = pool_size / (mq->msg_size + sizeof(struct rt_mq_message));

    mq->msg_queue_head = RT_NULL;
    mq->msg_queue_tail = RT_NULL;

    /* 將所有空閒消息塊通過單向鏈表串聯起來,msg_queue_free指向這個鏈表的第一個節點 */
    mq->msg_queue_free = RT_NULL;
    for (temp = 0; temp < mq->max_msgs; temp ++)
    {
        head = (struct rt_mq_message *)((rt_uint8_t *)mq->msg_pool +
                                        temp * (mq->msg_size + sizeof(struct rt_mq_message)));
        head->next = mq->msg_queue_free;
        mq->msg_queue_free = head;
    }

    /* 已使用的消息塊數量標記爲0 */
    mq->entry = 0;

    return RT_EOK;
}

以上是初始化消息隊列的函數rt_mq_init,我們需要重點關注消息隊列的總需內存大小、消息塊的實際個數等計算。我們可以參考以下所示rt_mq_create函數來獲得答案。在rt_mq_create中,msg_size需要以RT_ALIGN_SIZE(一般是4字節)向上對齊,msg_pool所需的總內存爲(對齊後的msg_size + rt_mq_message結構體大小(4字節) * 消息塊個數),那麼反過來就不難理解rt_mq_init函數中相關的計算了。

舉個例子,當入參msg_size = 9,pool_size = 100時,首先要對msg_size向上4字節對齊,即msg_size = 12,此時max_msgs = 100/ (12 + 6) = 5。這種情況下浪費的字節數爲100 % 18 = 10字節,所以在使用前瞭解內部實現還是很有必要的。

/* rt_mq_create函數中相關計算 */
mq->msg_size = RT_ALIGN(msg_size, RT_ALIGN_SIZE);
mq->max_msgs = max_msgs;
mq->msg_pool = RT_KERNEL_MALLOC((mq->msg_size + sizeof(struct rt_mq_message)) * mq->max_msgs);

 另外,初始化時還初始化了3個指針,重點看msg_queue_free,msg_queue_free用於構建空閒消息塊鏈表,初始化時將所有空閒塊通過單向鏈表全部串聯在一起,串聯後的結構如下圖所示。當需要發送消息隊列時,從msg_queue_free鏈表上直接取下一個節點,如果節點非空就說明獲取空閒消息塊成功了(是不是很像內存申請?),填充好數據後再掛接到msg_queue_tail鏈表等待被讀取。當消息隊列中的消息塊被讀取後,就會將該內存塊放回msg_queue_free鏈表(是不是很像內存釋放?)。

初始化後的空閒消息塊鏈表

7.3 發送消息隊列

正如上文所說,發送消息時,需要從msg_queue_free鏈表獲取空閒消息塊,填充好用戶數據後再放到msg_queue_tail所指向的節點後面。那麼我們就可以猜測,接收消息隊列應該是取出msg_queue_head指向的內存塊,然後將這個內存塊再放回到msg_queue_free鏈表。在RTT還有一個發送消息隊列的函數叫rt_mq_urgent,顧名思義,這個API肯定是用來發送緊急信息的,通過這個接口發送的消息會被掛接到msg_queue_head指向的消息塊前面,這樣就會優先被處理。

注意以下兩點:

1.可以在中斷裏發送消息。

2.發送消息和發送郵件不一樣,如果消息隊列已經滿了,不會引起發送線程阻塞,而是立即返回,這點和信號量一致。

/* 發送消息塊 */
rt_err_t rt_mq_send(rt_mq_t mq, void *buffer, rt_size_t size)
{
    register rt_ubase_t temp;
    struct rt_mq_message *msg;

    /* 檢查要發送的消息大小是否超了 */
    if (size > mq->msg_size)
        return -RT_ERROR;

    temp = rt_hw_interrupt_disable();

    /* 從msg_queue_free獲取一個空閒消息塊 */
    msg = (struct rt_mq_message *)mq->msg_queue_free;
    /* 消息塊爲空,說明沒有空閒消息塊了,也就是說消息隊列已經被塞滿了 */
    if (msg == RT_NULL)
    {
        rt_hw_interrupt_enable(temp);

        return -RT_EFULL;
    }
    /* 成功獲取到消息塊了,將這個消息塊從msg_queue_free鏈表中刪除 */
    mq->msg_queue_free = msg->next;

    rt_hw_interrupt_enable(temp);

    /* 將獲取到的消息塊的next指針指向空 */
    msg->next = RT_NULL;
    /* 複製用戶數據,+1是因爲前面的4字節是一個next指針,需要跳過 */
    rt_memcpy(msg + 1, buffer, size);


    temp = rt_hw_interrupt_disable();

    /* 
     * 將消息塊放到msg_queue_tail鏈表的最後,
     * 初始化時msg_queue_tail爲空,所以需要判斷非空 
     */
    if (mq->msg_queue_tail != RT_NULL)  
    {
        ((struct rt_mq_message *)mq->msg_queue_tail)->next = msg;
    }

    /* 將msg_queue_tail指向當前消息塊,即指向末端 */
    mq->msg_queue_tail = msg;

    /* 如果msg_queue_head是空的,那麼msg_queue_head也指向當前消息塊 */
    if (mq->msg_queue_head == RT_NULL)
        mq->msg_queue_head = msg;

    /* 消息隊列中的待處理消息塊數量加1 */
    mq->entry ++;

    /* 如果有線程因爲請求該消息隊列而掛起,那麼恢復suspend_thread鏈表的第一個節點線程*/
    if (!rt_list_isempty(&mq->parent.suspend_thread))
    {
        rt_ipc_list_resume(&(mq->parent.suspend_thread));
        rt_hw_interrupt_enable(temp);
        rt_schedule();

        return RT_EOK;
    }

    rt_hw_interrupt_enable(temp);

    return RT_EOK;
}

7.4 接收消息隊列

貌似沒啥好說的了,只需要注意以下兩點:

1.不允許在中斷裏請求消息隊列。

2.接收消息隊列和接收郵箱一樣,會有個while循環等待,在接收成功前,即使被其他方式(線程內置定時器除外)提前喚醒,喚醒後也依然會嘗試等待消息塊的接收。

/* 接收消息塊 */
rt_err_t rt_mq_recv(rt_mq_t    mq,
                    void      *buffer,
                    rt_size_t  size,
                    rt_int32_t timeout)
{
    struct rt_thread *thread;
    register rt_ubase_t temp;
    struct rt_mq_message *msg;
    rt_uint32_t tick_delta;        /* 用於記錄過去了多長時間 */

    /* 初始化時間差 */
    tick_delta = 0;

    thread = rt_thread_self();

    temp = rt_hw_interrupt_disable();

    /* 消息隊列中沒有消息塊,且不等待,那麼立即返回-RT_ETIMEOUT */
    if (mq->entry == 0 && timeout == 0)
    {
        rt_hw_interrupt_enable(temp);

        return -RT_ETIMEOUT;
    }

    /* 消息隊列是空的情況 */
    while (mq->entry == 0)
    {
        /* 檢查當前是否在線程環境中 */
        RT_DEBUG_IN_THREAD_CONTEXT;

        thread->error = RT_EOK;

        /* 不等待,立即返回-RT_ETIMEOUT */
        if (timeout == 0)
        {
            rt_hw_interrupt_enable(temp);
            thread->error = -RT_ETIMEOUT;

            return -RT_ETIMEOUT;
        }

        /* 將線程掛起在suspend_thread鏈表上 */
        rt_ipc_list_suspend(&(mq->parent.suspend_thread),
                            thread,
                            mq->parent.parent.flag);

        /* 判斷超時時間是否爲永久(-1),若非永久則啓動線程內置定時器 */
        if (timeout > 0)
        {
             /* 記錄一下掛起的時刻 */
            tick_delta = rt_tick_get();

            rt_timer_control(&(thread->thread_timer),
                             RT_TIMER_CTRL_SET_TIME,
                             &timeout);
            rt_timer_start(&(thread->thread_timer));
        }

        rt_hw_interrupt_enable(temp);

        rt_schedule();

        /* recv message */
        if (thread->error != RT_EOK)
        {
            /* return error */
            return thread->error;
        }

        /* 運行到這裏,說明還沒有請求到消息塊,但是線程因爲其他原因被提前喚醒了 */

        temp = rt_hw_interrupt_disable();

        /* 如果超時時間不爲永久,那麼重新計算一下剩餘超時等待時間並繼續等待 */
        if (timeout > 0)
        {
            tick_delta = rt_tick_get() - tick_delta;  /* 計算上一次掛起到現在過去了多久 */
            timeout -= tick_delta;                    /* 重新計算超時時長 */
            if (timeout < 0)
                timeout = 0;
        }
    }

    /* 成功請求到了一個消息塊 */
    msg = (struct rt_mq_message *)mq->msg_queue_head;

    /* 移動msg_queue_head到下一個消息塊 */
    mq->msg_queue_head = msg->next;

    /* 如果msg_queue_tail就是當前的消息塊,那麼說明當前消息隊列裏僅有這一個消息塊 */
    if (mq->msg_queue_tail == msg)
        mq->msg_queue_tail = RT_NULL;

    /* 消息隊列中消息塊個數減1 */
    mq->entry --;

    rt_hw_interrupt_enable(temp);

    /* 將消息塊中的數據拷貝出來,最多拷貝mq->msg_size個字節 */
    rt_memcpy(buffer, msg + 1, size > mq->msg_size ? mq->msg_size : size);

    temp = rt_hw_interrupt_disable();

    /* 將消息塊放回msg_queue_free鏈表 */
    msg->next = (struct rt_mq_message *)mq->msg_queue_free;
    mq->msg_queue_free = msg;

    rt_hw_interrupt_enable(temp);

    return RT_EOK;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章