這篇文章內容採自李雲大牛編寫的著作《專業嵌入式軟件開發 全面走向高質高效編程》一書中的第14章內容。我看後根據自己在實際項目中遇到的情況感覺這一章內容對於程序設計及開發尤其重要,並且對於軟件後期的正規維護都有很重要的參考價值,所以特地摘抄記錄下來以供大家參考。
該書籍的電子版已經上傳CSDN資源,大家可以下載閱讀。
https://download.csdn.net/my
模塊管理,保障系統有序運行
採用模塊化的方法設計軟件,早已成爲行業的共識。模塊化除了反映在方法上,還在實現上得下足功夫,既凸顯模塊的概念又實現整個系統的有序運行。
管理參照系
當一個系統比較複雜,所包含的模塊數量比較多時,不可避免地會產生模塊間負責的依賴關係,且有可能出現“牽一髮而動全身”這種情形。無論採用怎麼樣的方法管理模塊,都應當先將系統中所有模塊之間的依賴關係通過某種方式表達出來,就比如同下圖的形式表示:
從上圖中的各個模塊之間的依賴關係可以看出,當系統進行初始化時堆管理模塊應當最先被初始化,接着是緊跟其後的定時器管理模塊和內存池管理模塊,上圖中所有模塊的初始化順序是從上到下的,且左右順序並不重要。
爲了更好表達模塊間的依賴關係,我們需要引入分層的概念。一個系統中的模塊可以分爲三大層,分別是平臺層、框架層和應用層。對於上圖中模塊分類採用分層的方式進行分割,可以得到下圖這樣的分類。
對系統進行初始化的順序顯然應該是平臺層最先,應用層最後。而終止化應採用完全相反的順序,即先對應用層進行終止化,最後對平臺層進行終止化。這遵循了順序分配、逆序釋放的原則。
當在系統中增加一個模塊時,首先需要確定將其劃入哪個層次。一個模塊是否屬於應用層相對容易判斷,只要看這個模塊實現的功能是否是產品所私有的。如果是,那說明該模塊應屬於應用層,否則就屬於平臺層或框架層。有時一些模塊的歸屬並不明顯,那麼在這種情形下,最好一開始不要太計較,而是先給它定一個大概層次,等到項目開展下去對之認識加深後在調整也不遲。模塊管理如果只有層的概念則粒度太大,因爲在一層中也可能存在多個模塊且模塊之間也存在依賴關係。比如,上圖中的平臺層中就存在定時器管理模塊依賴於堆管理模塊。由此看來,在同一層中還需要進行劃分,爲此引入分級概念。對上圖進行分割,每一層中自上而下分級。如下圖:
有了分層和分級的概念後,就爲模塊的初始化和終止化順序提供了參照系。當新加入一個模塊時,需要通過先分層後分級的方式以確定它應位於系統初始化過程中的哪一個點,這又間接地確立了終止化順序點。在這個參照系中,模塊的初始化順序只與依賴關係中的上下位置有關,而與左右順序無關。如果左右之間的模塊存在某種情形下的依賴關係,那就將它們劃分爲不同級進從而轉化爲上下關係。
設計思路
模塊管理最注意是做到能集中控制所有模塊的初始化和終止化順序,而要實現這個設計目的,方式之一是讓每一個模塊通過註冊回調函數的形式實現控制反轉。下圖說明了模塊管理模塊的外部行爲。
如圖中所示,每個模塊都需要調用 module_register() 函數向模塊管理模塊進行註冊,註冊時需要指明模塊所屬層級和模塊的回調函數。當系統啓動時, system_up() 函數被調用,這時會直接觸發模塊管理模塊根據層與級的順序調用各註冊模塊的回調函數實現模塊的初始化。反之,當系統終止時,system_down() 函數被調用,它將觸發各模塊終止化操作,終止化操作的順序與初始化過程是完全相反的。
從追求簡單的原則考慮,每一個註冊模塊只需註冊一個回調函數。回調函數被設計成包含一個參數以指示每一次被調用時的目的。這裏我們定義了“初始化(Initializing)”、“已啓動(up)”、“已停止(down)”、“終止化(destroying)” 四種系統狀態,每個函數的回調函數以它們作爲參數值。如下圖:
注意:這裏定義4個狀態而非2個狀態是考慮到——“初始化”和“已啓動”是針對系統啓動時的,“已停止”和“終止化”是針對系統終止時的,也就是說,爲每一個模塊分別提供兩次初始化和終止化的機會。通過這樣的定義有助於更好管理模塊之間的依賴關係。比如,各個模塊可以在“初始化”狀態只依賴底層模塊的初始化工作,而在“已啓動”狀態可以做依賴其他高層模塊的初始化工作。
程序實現
到目前爲止,我們已經塑造了模型,接下來看一看具體實現。
引入模塊標識
要實現模塊管理需要爲模塊定義數據結構。首先,需要引入模塊標識值以方便識別和管理各個模塊。標識值從0開始,採用這種方式有助於使用數組這種簡單的數據結構實現模塊管理,使得模塊標識值能夠作爲數組下標。
typedef enm{
MODULE_MODULE, //模塊管理模塊
MODULE_INTERRUPT, //中斷管理
MODULE_DEVICE, //設備管理
MODULE_CLOCK, //時鐘管理
MODULE_CONSOLE, //設備串口
MODULE_CTRLC, //針對Ctrl+C的響應
MOUDLE_FLASH, //flash設備
MODULE_TIMER, //時間管理
MODULE_TASK, //task管理
MODULE_SYNC, //對task緩存目標管理
MODULE_SEMAPHORE, //信號管理
MODULE_MUTEX, //鎖管理
MODULE_QUEUE, //消息隊列管理
MODULE_HEAP, //堆管理
MODULE_MPOOL, //對內存池管理
MODULE_TESTAPP, //測試應用模塊
MODULE_COUNT, //記錄當前模塊數據個數
MODULE_LAST = (MODULE_COUNT - 1) //數組end標識
}modeule_t;
其中,MODULE_COUNT和MODULE_LAST並不對應相應的模塊,MODULE_COUNT用於表示整個系統一共有多少個模塊,而MODULE_LAST表示系統中最後一個模塊的標識值。當需要增加新模塊標識值時,在枚舉體中的MODULE_COUNT之前直接添加即可,這樣不會影響到MODULE_COUNT和MODULE_LAST所表達的意思。
模塊標識除了被運用於模塊管理以外,還被運用於錯誤碼管理中,以標識一個錯誤發生在哪一個模塊。
實現層與級的表達
儘管在模塊管理參照系中,層與級是完全不同的概念,但從實現的角度來看,這兩個概念的目的卻都是爲了表達模塊初始化和終止化時的先後順序。因此,在程序實現中完全可以對它們採用統一的方法加以表達,但通過名稱加以區分。如下的init_level_t數據類型以表達層與級的概念。
typedef enum{
LEVEL_FIRST,
CPU_LEVEL = LEVEL_FIRST,
PERIPHERALS_LEVEL,
DRIVER_LEVEL,
OS_LEVEL,
//平臺層
FLATFORM_LEVEL0,
FLATFORM_LEVEL1,
FLATFORM_LEVEL2,
FLATFORM_LEVEL3,
FLATFORM_LEVEL4,
FLATFORM_LEVEL5,
FLATFORM_LEVEL6,
FLATFORM_LEVEL7,
//框架層
FRAMEWORK_LEVEL0,
FRAMEWORK_LEVEL1,
FRAMEWORK_LEVEL2,
FRAMEWORK_LEVEL3,
FRAMEWORK_LEVEL4,
FRAMEWORK_LEVEL5,
FRAMEWORK_LEVEL6,
FRAMEWORK_LEVEL7,
//應用層
APPLICATION_LEVEL0,
APPLICATION_LEVEL1,
APPLICATION_LEVEL2,
APPLICATION_LEVEL3,
APPLICATION_LEVEL4,
APPLICATION_LEVEL5,
APPLICATION_LEVEL6,
APPLICATION_LEVEL7,
//level個數和level_last必須在最後,類似模塊枚舉類型的含義
LEVEL_COUNT,
LEVEL_LAST = (LEVEL_COUNT -1 )
}init_level_t;
從上面的枚舉內容可以看出,一共包含平臺、框架和應用三大層,且每一層又定義了八個級,LEVEL0到LEVEL7。除了這三大層八大級外還二外定義了處理器級(CPU_LEVEL)、外設級(PERIPHERALS_LEVEL)、驅動級(DRIVER_LEVEL)和操作系統級(OS_LEVEL)。從某種意義上來說,這四個級可以被歸類爲平臺層,將它們獨立出來完全是爲了使概念更清晰。
系統狀態和回調函數原型定義
從前面定義了表示系統四個狀態的system_state_t數據類型和模塊回調函數原型 module_callback_t。正如前面提到的,模塊回調函數是以系統狀態爲參數。如下:
typedef enum{
STATE_INITIALIZING,
STATE_UP,
STATE_DOWN,
STATE_DESTROYING
}system_state_t;
typedef error_t (*module_callback_t)(system_state_t _state);
模塊註冊
下列代碼說明了用於管理模塊所需的數據結構和全局變量。
typedef struct{
dll_node_t node_;
const char *p_name_;
module_callback_t callback_;
bool is_registered_;
}module_init_t;
static dll_t g_levels[LEVEL_COUNT];
static module_init_t g_modules[MODULE_CONT];
每一個模塊需要使用一個module_init_t 結構實例來記錄它的註冊信息。module_init_t結構各成員變量的作用是:
- node_:當用戶進行模塊註冊時,會通過這個鏈表節點將相同初始化級別的模塊串聯在一起。
- p_name_:用於記錄模塊名。這裏存在一個假設,即所傳入的模塊名的內存並不是採用malloc()函數分配的。而是採用字符串字面值的形式傳入的。這一假設使得可以省去內存拷貝而簡化實現。
- callback_:用於保存模塊的回調函數。系統狀態的變遷都將導致各模塊的回調函數被依次調用。
- is_registered_:這個變量用於放置模塊的重複註冊。當一個模塊被註冊後,這個變量的值將變成true。
其中數組g_levels爲每一個初始化級別都定義了一個鏈表。當模塊被註冊到某一級別時,模塊的註冊信(即module_init_t實例)將掛接到對應的鏈表上。(同級模塊以鏈表方式進行管理)。具體如下:
typedef struct dll_node{
struct dll_node *prev;
struct dll_node *next;
}dll_node_t, *dll_node_handle_t;
typedef struct {
dll_node_t *head_;
dll_node_t *tail_;
usize_t count_;
}dll_t, *dll_handle_t;
module_register() 函數用來實現模塊註冊,其實現如下所示。該函數各參數的含義是:參數_name指明被註冊模塊的名稱是什麼;參數 _module 指示所需註冊的模塊標識值;參數 _level標明這一模塊將屬於哪一個初始化級別;參數 _callback 用於指示模塊的回調函數。
error_t module_register(const char _name[], module_t _module, init_level_t _level, module_callback_t _callback)
{
module_init_t *p_module;
if(_module > MODULE_LAST)
{
return ERRCR_T(ERROR_MODULE_REG_INVMODULE);
}
if(_level > LEVEL_LAST)
{
return ERROR_T(ERROR_MODULE_REG_INVLEVEL);
}
if(null == _callback)
{
return ERROR_T(ERROR_MODULE_REG_INVCB);
}
p_module = &g_modules[_module];
if(p_module->is_registered_)
{
return ERROR_T(ERROR_MODULE_REGISTERED);
}
p_module->p_name_ = _name;
p_module->callback_ = _callback;
p_module->is_registered_ = true;
dll_push_tail(&g_levels[_level], &p_module->node_);
return 0;
}
注意:這個註冊函數並沒有採用上鎖的方式以防止出現競爭問題,原因是模塊註冊行爲通常發生在系統的最開始階段,而此時並不存在多任務問題。別忘了,任務的創建是在註冊模塊的回調函數中完成的。
從模塊註冊函數的實現來看,它只是告知了被註冊模塊處於什麼級別,而真正的初始化或終止化操作動作並沒用發送。這兩個行爲是通過調用 system_up() 和 system_down() 函數觸發的。
系統啓動
system_up() 函數的調用標誌這系統開始啓動。具體實現如下:
static system_state_t g_state;
static bool init_for_each(dll_t *_p_dll, dll_node_t *_p_node, void *_p_arg)
{
module_init_t *p_module = (module_init_t *)_p_node;
error_t result = p_module->callback_(STATE_INITALIZING);
UNUSED(_p_dll);
UNUSED(_p_arg);
if(0 != result)
{
console_print("Error: can't initialize module %s (%s)", p_module->p_name, errstr(result));
return false;
}
return true;
}
static bool up_for_each(dll_t *_p_dll, dll_node_t *_p_node, void *_p_arg)
{
module_init_t *p_module = (module_init_t *)_p_node;
error_t result = p_module->callback_(STATE_UP);
UNUSED(_p_dll);
UNUSED(_p_arg);
if(0 != result)
{
console_print("Error: can't start up module %s (%s)", p_module->p_name_, errstr(result));
return false;
}
return true;
}
error_t system_up()
{
init_level_t level;
g_state = STATE_INITALIZING;
for(level = LEVEL_FIRST; level <= LEVEL_LAST; ++level)
{
if(0 != dll_traverse(&g_levels[level], init_for_each, (void *)&level))
{
return ERROR_T(ERROR_MODULE_INIT_FAILURE);
}
}
g_state = STATE_UP;
for(level = LEVEL_FIRST; level <= LEVEL_LAST; ++level)
{
if(0 != dll_traverse(&g_levels[level], up_for_each, (void *)&level))
{
return ERROR_T(ERROR_MODULE_UP_FAILURE);
}
}
return 0;
}
system_up() 函數將從 LEVEL_FIRST 級開始,依次按序調用g_levels數組中各鏈表內所記錄模塊的回調函數,對於鏈表中各節點的遍歷是通過調用 dll_traverse() 函數實現的。
值得一提的是,在system_up() 函數內通過使用LEVEL_FIRST 和 LEVEL_LAST 兩個枚舉值,使得 init_level_t 數據類型內的級別即使被更改也不必跟着更改,這提高了程序的可維護性。
爲了方便閱讀,下面給出了鏈表遍歷函數 dll_traverse() 函數的實現。
dll_node_t *dll_traverse(dll_t *_p_dll, traverse_callback_t _cb, void *_p_arg)
{
register dll_node_t *p_node = _p_dll->head_;
if(null == cb)
{
return 0;
}
while((0 != p_node) && ((*_cb)(_p_dll, p_node, _p_arg)))
{
p_node = p_node->next;
}
return p_node;
}
系統關閉
系統關閉函數 system_down() 的實現與 system_up() 很相似。
static bool down_for_each(dll_t *_p_dll, dll_node_t *_p_node, void *_p_arg)
{
module_init_t *p_module = (module_init_t *)_p_node;
error_t result = p_module->callback_(STATE_DOWN);
UNUSED(_p_dll);
UNUSED(_p_arg);
if(0 != result)
{
console_print("Error: can't shut down module %s (%s)", p_module->p_name, errstr(result));
//!!! don't return false;
}
return true;
}
static bool destroy_for_each(dll_t *_p_dll, dll_node_t *_p_node, void *_p_arg)
{
module_init_t *p_module = (module_init_t *)_p_node;
error_t result = p_module->callback_(STATE_UP);
UNUSED(_p_dll);
UNUSED(_p_arg);
if(0 != result)
{
console_print("Error: can't start up module %s (%s)", p_module->p_name_, errstr(result));
//!!! don't return false;
}
return true;
}
void system_down()
{
init_level_t level;
g_state = STATE_DOWN;
for(level = LEVEL_LAST; level > LEVEL_FIRST; --level)
{
(void)dll_traverse_reversely(&g_levels[level], down_for_each, null);
}
g_state = STATE_DESTROYING;
for(level = LEVEL_LAST; level > LEVEL_FIRST; --level)
{
(void)dll_traverse_reversely(&g_levels[level], destroy_for_each, null);
}
dll_traverse_reversely(&g_levels[LEVEL_FIRST], destroy_for_each, null);
}
system_down() 函數的實現與 system_up() 函數有幾個不同點。
- 鏈表節點回調函數 destroy_for_each() 和 down_for_each() 在發現錯誤時並不返回錯誤,而只是輸出一行錯誤信息。
- system_down() 函數調用各個模塊的註冊回調函數的順序與 system_up() 函數是逆序的。
dll_node_t *dll_traverse_reversely(dll_t *_p_dll, traverse_callback_t _cb, void *_p_arg)
{
register dll_node_t *p_node = _p_dll->tail_;
if(null == cb)
{
return 0;
}
while((0 != p_node) && ((*_cb)(_p_dll, p_node, _p_arg)))
{
p_node = p_node->prev_;
}
return p_node;
}
對於 system_down() 函數的調用可以考慮使用人爲或信號觸發。比如通過命令行、以太網想設備發送一個消息等等。在各模塊實現其終止行爲時,要考慮在進行真正的終止動作之前做必要檢查,以確保它所管理的資源都被回收了。如果發現仍有資源沒用被回收,可以通過日誌等形式提示以幫助發現潛在的資源泄漏問題。
module示例程序
以下是用於測試模塊管理功能的小程序。
#include <stdio.h>
#include <stdarg.h>
#include "module.h"
error_t module_timer(system_state_t _state)
{
if(STATE_INITIALIZING == _state)
{
printf("info: timer module is initializing\n");
}
else if(STATE_UP == _state)
{
printf("info: timer module is up\n");
}
else if(STATE_DOWN == _state)
{
printf("info: timer module is down\n");
}
else if(STATE_DESTROYING == _state)
{
printf("info: timer module is destroying\n");
}
return 0;
}
error_t module_memory(system_state_t _state)
{
if(STATE_INITALIZING == _state)
{
printf("info: memory module is initializing\n");
}
else if(STATE_UP == _state)
{
printf("info: memory module is up\n");
}
else if(STATE_DOWN == _state)
{
printf("info: memory module is down\n");
}
else if(STATE_DESTROYING == _state)
{
printf("info: memory module is destroying\n");
}
return 0;
}
void module_registration_entry()
{
(void) module_register("Timer", MODULE_TIMER, OS_LEVEL, module_timer);
(void) module_register("Memory", MODULE_HEAP, OS_LEVEL, module_memory);
}
int main()
{
module_registration_entry();
printf("\nSystem is going to be up\n");
if(0 != system_up())
{
printf("Error: system cannot be up\n");
return -1;
}
printf("\nSystem is going to be down\n");
system_down();
return 0;
}
小結
通過引入分層和分級的概念,爲模塊的初始化和終止化提供了一個運行先後順序的參照系,每一個模塊都在這個參照系中佔有一席之地。
模塊管理的目的就是爲了做到系統中各個模塊有序啓停,並在系統中強化模塊的概念。在設計模塊時,需要考慮在初始化過程中獲取資源(包括任務創建),以及在終止化過程中釋放資源。