RT-Thread4的FinSH服務簡析

1、基本原理

shell服務是一個人性化系統應該具備的功能,使用者透過執行相應的命令來執行對應的程式,實現命令的工具,shell在linux,uboot下非常常見,但是在RTOS中,整合shell服務的系統比較少,RT-thread集成了shell服務(嚴格來說是元件),類似的Treadx,freertos,uCos並沒有整合shell,必須使用者自己實現,實現一個簡單的shell並不困難,簡單的shell大體框架如下:

RT-Thread4的FinSH服務簡析

命令解析程式,負責解析出命令和命令引數兩個部分,查詢註冊的命令(查表思路),並執行其回撥函式,將命令引數傳遞給回撥函式,完成命令。

在linux下命令的執行相對會複雜一些,首先要有一個shell終端,終端可以是實際的物理終端比如串列埠,也可以是偽終端,比如ssh登陸的終端,終端輸入的命令傳遞到shell處理程序,解析出命令和引數,回顯命令等。shell透過命令在/bin,/sbin,/usr/bin,/usr/sbin目錄下根據命令名稱查詢相應的可執行程式,並啟動改程序,程序main函式的兩個引數argc和argv接收使用者傳遞的引數,完成相應的命令操作。

2、宏定義註冊

RT-Thread整合的shell服務是比較簡單的,這個和uboot比較類似,我們知道在uboot下要增加一個命令那麼需要透過一個名稱註冊的宏來增加命令,在RT-Thread下也是一樣的:

FINSH_FUNCTION_EXPORT(list_mem, list memory usage information)

FinSH元件定義在相應的元件目錄下:

rt-thread\components\finsh

註冊命令的宏定義介面定義在:

finsh_api。h

在這個檔案中,有比較多的宏定義,但是我們沒有必要去深究,這些宏定義和編譯器息息相關的,其實原理很簡單,就是利用編譯器的屬性,建立一個命令表,存放在ROM內,找命令時就在這個表內查詢,利用編譯器建立表,然後查表的過程。下面我們來簡單的分析一下這個宏定義:

/** * @ingroup finsh * * This macro exports a system function to finsh shell。 * * @param name the name of function。 * @param desc the description of function, which will show in help。 */#define FINSH_FUNCTION_EXPORT(name, desc) \ FINSH_FUNCTION_EXPORT_CMD(name, name, desc)//編譯器比較多,我們選擇一個TI的編譯器來看看#define FINSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \ __TI_FINSH_EXPORT_FUNCTION(__fsym_##cmd); \ const char __fsym_##cmd##_name[] = #cmd; \ const char __fsym_##cmd##_desc[] = #desc; \ const struct finsh_syscall __fsym_##cmd = \ { \ __fsym_##cmd##_name, \ __fsym_##cmd##_desc, \ (syscall_func)&name \ };//連線到DATA區的宏定義,我們知道f會被編譯器連結到符號為FsymTab的DATA區中#define __TI_FINSH_EXPORT_FUNCTION(f) PRAGMA(DATA_SECTION(f,“FSymTab”))//我們來具體分析一下宏定義//__fsym_##cmd會被連結到“FSymTab”中,形成表的一個記錄,在編譯器中##常用於將字元連結起來//我們知道這個cmd是使用者註冊時定義的,比如定義為ls吧,那麼記錄的名稱被連結為“__fsym_ls”//下面這個宏定義很簡單,就是初始化一個struct finsh_syscall結構,然後新增到“FSymTab”__TI_FINSH_EXPORT_FUNCTION(__fsym_##cmd); \ //定義連結 const char __fsym_##cmd##_name[] = #cmd; \ //定義name成員 const char __fsym_##cmd##_desc[] = #desc; \ //定義desc成員 const struct finsh_syscall __fsym_##cmd = \ //定義finsh_syscall結構 { \ __fsym_##cmd##_name, \ //初始化name __fsym_##cmd##_desc, \ //初始化描述部分 (syscall_func)&name \ //初始化回撥 };//回撥函式其實就是宏定義中的name,所以在註冊時,使用回撥函式直接註冊,這個函式的名稱//就是命令名稱,比如我要註冊一個ls命令,相應的我要定義一個ls函式。#define FINSH_FUNCTION_EXPORT_CMD(name, cmd, desc)

我們再來看看“FSymTab”表的成員struct finsh_syscall結構:

//命令表的成員比較簡單,三個成員//名稱:這個是必須,我們必須透過名稱查詢命令//描述:這個是一個可選項,描述可以簡單的描述一下這個命令的作用//回撥函式:這個是必須的,回撥函式會執行命令的具體內容/* system call table */struct finsh_syscall{ const char* name; /* the name of system call */#if defined(FINSH_USING_DESCRIPTION) && defined(FINSH_USING_SYMTAB) const char* desc; /* description of system call */#endif syscall_func func; /* the function address of system call */};//回撥函式定義為引數為void返回值為long。typedef long (*syscall_func)(void);//表的起始地址記錄,便於後續查表,定義為外部的,這個後面我們會講到extern struct finsh_syscall *_syscall_table_begin, *_syscall_table_end;

3、命令的執行

之前我們講了命令的註冊是透過宏定義將命令註冊到表中,熟悉uboot的同學,對這個一定理解的比較透徹,在linux中也很常見,既然命令已經有了,那麼當我們從終端輸入命令後,到執行命令又會經過一個怎樣的過程呢,我們來簡單的分析一下,finsh shell的管理定義在檔案:

shell。c

初始化函式finsh_system_init:

//初始化函式注意完成三個部分//第1,及時記錄下表的地址資訊//第2,啟動一個管理執行緒//第3,建立一個訊號量,啟動執行緒/* * @ingroup finsh * * This function will initialize finsh shell */int finsh_system_init(void){ rt_err_t result = RT_EOK; rt_thread_t tid; //第1個部分,就是記錄地址資訊,這個和編譯器息息相關。#ifdef FINSH_USING_SYMTAB#if defined(__CC_ARM) || defined(__CLANG_ARM) /* ARM C Compiler */ extern const int FSymTab$$Base; extern const int FSymTab$$Limit; extern const int VSymTab$$Base; extern const int VSymTab$$Limit; finsh_system_function_init(&FSymTab$$Base, &FSymTab$$Limit);#ifndef FINSH_USING_MSH_ONLY finsh_system_var_init(&VSymTab$$Base, &VSymTab$$Limit);#endif#elif defined (__ICCARM__) || defined(__ICCRX__) /* for IAR Compiler */ finsh_system_function_init(__section_begin(“FSymTab”), __section_end(“FSymTab”)); finsh_system_var_init(__section_begin(“VSymTab”), __section_end(“VSymTab”));#elif defined (__GNUC__) || defined(__TI_COMPILER_VERSION__) /* GNU GCC Compiler and TI CCS */ extern const int __fsymtab_start; extern const int __fsymtab_end; extern const int __vsymtab_start; extern const int __vsymtab_end; finsh_system_function_init(&__fsymtab_start, &__fsymtab_end); finsh_system_var_init(&__vsymtab_start, &__vsymtab_end);#elif defined(__ADSPBLACKFIN__) /* for VisualDSP++ Compiler */ finsh_system_function_init(&__fsymtab_start, &__fsymtab_end); finsh_system_var_init(&__vsymtab_start, &__vsymtab_end);#elif defined(_MSC_VER) unsigned int *ptr_begin, *ptr_end; if(shell) { rt_kprintf(“finsh shell already init。\n”); return RT_EOK; } ptr_begin = (unsigned int *)&__fsym_begin; ptr_begin += (sizeof(struct finsh_syscall) / sizeof(unsigned int)); while (*ptr_begin == 0) ptr_begin ++; ptr_end = (unsigned int *) &__fsym_end; ptr_end ——; while (*ptr_end == 0) ptr_end ——; finsh_system_function_init(ptr_begin, ptr_end);#endif#endif //第2個部分,建立一個管理執行緒,靜態記憶體和動態記憶體的選擇。#ifdef RT_USING_HEAP /* create or set shell structure */ shell = (struct finsh_shell *)rt_calloc(1, sizeof(struct finsh_shell)); if (shell == RT_NULL) { rt_kprintf(“no memory for shell\n”); return -1; } tid = rt_thread_create(FINSH_THREAD_NAME, finsh_thread_entry, RT_NULL, FINSH_THREAD_STACK_SIZE, FINSH_THREAD_PRIORITY, 10);#else shell = &_shell; tid = &finsh_thread; result = rt_thread_init(&finsh_thread, FINSH_THREAD_NAME, finsh_thread_entry, RT_NULL, &finsh_thread_stack[0], sizeof(finsh_thread_stack), FINSH_THREAD_PRIORITY, 10);#endif /* RT_USING_HEAP */ //第3部分,建立訊號量和啟動管理執行緒,訊號量將用於命令的觸發 rt_sem_init(&(shell->rx_sem), “shrx”, 0, 0); finsh_set_prompt_mode(1); if (tid != NULL && result == RT_EOK) rt_thread_startup(tid); return 0;}INIT_APP_EXPORT(finsh_system_init);

上面講到初始化時主要分為三個部分,前面的兩個部分比較重要,最重要的是執行緒部分,也就是說使用者所有的命令將在該執行緒內進行處理,相比於linux下每個命令都有啟動一個程序處理,這個要相對簡單一些,同時shell服務有個重要的管理結構struct finsh_shell:

struct finsh_shell{ //訊號量,用於喚醒執行緒 struct rt_semaphore rx_sem; enum input_stat stat; //回顯 rt_uint8_t echo_mode:1; rt_uint8_t prompt_mode: 1;#ifdef FINSH_USING_HISTORY rt_uint16_t current_history; rt_uint16_t history_count; // 歷史命令記錄,這裡類似於linux下按向上鍵就可以檢視歷史命令的功能 char cmd_history[FINSH_HISTORY_LINES][FINSH_CMD_SIZE];#endif //引數表相關#ifndef FINSH_USING_MSH_ONLY struct finsh_parser parser;#endif //儲存命令和回顯的位置資訊 char line[FINSH_CMD_SIZE]; rt_uint16_t line_position; rt_uint16_t line_curpos; //終端裝置#if !defined(RT_USING_POSIX) && defined(RT_USING_DEVICE) rt_device_t device;#endif //終端許可權,密碼#ifdef FINSH_USING_AUTH char password[FINSH_PASSWORD_MAX];#endif};

第1個部分,地址資訊的記錄:

//地址資訊分為兩個部分//第1部分,命令表的地址資訊//第2部分,引數表的地址資訊/* finsh symtab */#ifdef FINSH_USING_SYMTABstruct finsh_syscall *_syscall_table_begin = NULL;struct finsh_syscall *_syscall_table_end = NULL;struct finsh_sysvar *_sysvar_table_begin = NULL;struct finsh_sysvar *_sysvar_table_end = NULL;#endif//我們回頭看,回撥函式並沒有傳遞引數,那麼引數從哪裡來?所有還要有一個引數表。typedef long (*syscall_func)(void);//命令表的地址資訊初始化finsh_system_function_init(&FSymTab$$Base, &FSymTab$$Limit);//引數表的地址資訊初始化,有沒有引數表還要取決於這個宏定義#ifndef FINSH_USING_MSH_ONLY finsh_system_var_init(&VSymTab$$Base, &VSymTab$$Limit);#endif//初始地址資訊,就是將全域性變數賦值的過程,這個沒什麼好講的了。void finsh_system_function_init(const void *begin, const void *end){ _syscall_table_begin = (struct finsh_syscall *) begin; _syscall_table_end = (struct finsh_syscall *) end;}void finsh_system_var_init(const void *begin, const void *end){ _sysvar_table_begin = (struct finsh_sysvar *) begin; _sysvar_table_end = (struct finsh_sysvar *) end;}

第2部分,也是最重要的部分,執行緒處理部分:

//執行緒部分,我們來看使用靜態記憶體建立的部分,動態建立也差不多,有兩個地方要注意//第1,執行緒的優先順序FINSH_THREAD_PRIORITY,預設是20//第2,執行緒的堆疊大小,FINSH_THREAD_STACK_SIZE,預設是2048//所有我們使用命令時,要注意臨時變數的數量和呼叫深度shell = &_shell;tid = &finsh_thread;result = rt_thread_init(&finsh_thread, FINSH_THREAD_NAME, finsh_thread_entry, RT_NULL, &finsh_thread_stack[0], sizeof(finsh_thread_stack), FINSH_THREAD_PRIORITY, 10);//執行緒的入口finsh_thread_entry函式//執行緒處理有很多程式碼,大體也分為3個部分//第1,獲取終端裝置,即輸入的裝置//第2,啟用認證時,密碼登陸//第3,命令回顯,解析,執行//裝置在終端裝置rt_device_t console = rt_console_get_device();if (console){ finsh_set_device(console->parent。name);}//等待密碼完成,取決於FINSH_USING_AUTHfinsh_wait_auth();//執行緒的while迴圈裡面主要處理命令相關的while (1){ //從終端裝置獲取一個字元 ch = finsh_getchar(); //上下左右鍵的處理,回顯等,這裡我就不分析 /* * handle control key * up key : 0x1b 0x5b 0x41 * down key: 0x1b 0x5b 0x42 * right key:0x1b 0x5b 0x43 * left key: 0x1b 0x5b 0x44 */ //enter鍵代表命令結束,命令結束後的處理是最關鍵的 /* handle end of line, break */ if (ch == ‘\r’ || ch == ‘\n’) { //記錄命令為歷史記錄#ifdef FINSH_USING_HISTORY shell_push_history(shell);#endif#ifdef FINSH_USING_MSH if (msh_is_used() == RT_TRUE) { if (shell->echo_mode) rt_kprintf(“\n”); //命令的執行函式 msh_exec(shell->line, shell->line_position); } else#endif}

透過分析命令處理的執行緒finsh_thread_entry,我們知道處理處理的主要函式如下:

int msh_exec(char *cmd, rt_size_t length) ——if (_msh_exec_cmd(cmd, length, &cmd_ret) == 0) //命令執行函式static int _msh_exec_cmd(char *cmd, rt_size_t length, int *retp){ //。。。省略 //透過命令名稱查找回調函式 cmd_func = msh_get_cmd(cmd, cmd0_size); if (cmd_func == RT_NULL) return -RT_ERROR; //講命令列解析到argv /* split arguments */ memset(argv, 0x00, sizeof(argv)); argc = msh_split(cmd, length, argv); if (argc == 0) return -RT_ERROR; /* exec this command */ //執行回撥函式,引數argc, argv *retp = cmd_func(argc, argv);}//根據命令查找回調函式,就是查表,沒什麼好講的static cmd_function_t msh_get_cmd(char *cmd, int size){ struct finsh_syscall *index; cmd_function_t cmd_func = RT_NULL; //初始化的時候已經記錄了表的起始地址資訊 //_syscall_table_begin和_syscall_table_end //記錄的增加的偏移FINSH_NEXT_SYSCALL(index)計算 //偏移就是一個struct finsh_syscall的大小 for (index = _syscall_table_begin; index < _syscall_table_end; FINSH_NEXT_SYSCALL(index)) { if (strncmp(index->name, “__cmd_”, 6) != 0) continue; //匹配命令的名稱,匹配成功返回相應的回撥函式 if (strncmp(&index->name[6], cmd, size) == 0 && index->name[6 + size] == ‘\0’) { cmd_func = (cmd_function_t)index->func; break; } } //返回回撥函式 return cmd_func;}//命令列的解析函式,就是以空格劃分的引數儲存的argv陣列中//FINSH_ARG_MAX預設是8,所以最多8個引數//正常返回值是引數的個數即argcstatic int msh_split(char *cmd, rt_size_t length, char *argv[FINSH_ARG_MAX]) //回撥函式,回撥函式定義和之前講的表的回撥函式定義typedef long (*syscall_func)(void);//有點不一樣,但是這個是一個指標,所以並沒有使用原先的定義typedef int (*cmd_function_t)(int argc, char **argv);cmd_function_t cmd_func;*retp = cmd_func(argc, argv);

3、小結

總體思路就是名命令記錄在表內,其實表的形式不限,可以是編譯器連結的,也可以是資料的,也可以是連結串列等。其實在RT-Thread中沒有定義FINSH_USING_SYMTAB,那麼表就是一個數組,有了表,那麼輸入命令時,就要將命令解析出來,然後執行回撥函式,回撥函式是命令執行的最終步驟,也是關鍵步驟。上文中沒有講到_sysvar_table_begin引數變數,歷史命令儲存,命令的回顯,密碼認證等。有興趣的同學可以自己研究一下。