v72.01 鴻蒙內核源碼分析(Shell解析) | 應用窺伺內核的窗口 | 百篇博客分析OpenHarmony源碼
子曰:「苟正其身矣,於從政乎何有?不能正其身,如正人何?」 《論語》:子路篇
百篇博客系列篇.本篇為:
v72.xx 鴻蒙內核源碼分析(Shell解析篇) | 應用窺視內核的窗口
進程管理相關篇為:
- v02.06 鴻蒙內核源碼分析(進程管理) | 誰在管理內核資源
- v24.03 鴻蒙內核源碼分析(進程概念) | 如何更好的理解進程
- v45.05 鴻蒙內核源碼分析(Fork) | 一次調用 兩次返回
- v46.05 鴻蒙內核源碼分析(特殊進程) | 老鼠生兒會打洞
- v47.02 鴻蒙內核源碼分析(進程回收) | 臨終託孤的短命娃
- v48.05 鴻蒙內核源碼分析(信號生產) | 年過半百 活力十足
- v49.03 鴻蒙內核源碼分析(信號消費) | 誰讓CPU連續四次換棧運行
- v71.03 鴻蒙內核源碼分析(Shell編輯) | 兩個任務 三個階段
- v72.01 鴻蒙內核源碼分析(Shell解析) | 應用窺伺內核的窗口
系列篇從內核視角用一句話概括shell
的底層實現為:兩個任務,三個階段。其本質是獨立進程,因而划到進程管理模塊。每次創建shell
進程都會再創建兩個任務。
- 客戶端任務(ShellEntry): 負責接受來自終端(控制台)敲入的一個個字符,字符按
VT
規範組裝成一句句的命令。 - 服務端任務(ShellTask): 對命令進行解析並執行,將結果輸出到控制台。
而按命令生命周期可分三個階段. - 編輯: 鴻蒙在這個部分實現了一個簡單的編輯器功能,處理控制台輸入的每個字符,主要包括了對控制字符 例如
<ESC>
,\t
,\b
,\n
,\r
,四個方向鍵0x41
~0x44
的處理。 - 解析: 對編輯後的字符串進行解析,解析出命令項和參數項,找到對應的命令項執行函數。
- 執行: 命令可通過靜態和動態兩種方式註冊到內核,解析出具體命令後在註冊表中找到對應函數回調。將結果輸出到控制台。
編輯部分由客戶端任務完成,後兩個部分由服務端任務完成,命令全局註冊由內核完成。
- 本篇主要說 服務端任務 和 解析/執行過程.
- 客戶端任務 和 編輯過程 已在(Shell編輯篇)中說明,請自行翻看.
總體過程
- 第一步: 將支持的
shell
命令註冊進全局鏈表,支持靜態和動態兩種方式,內容包括命令項,參數信息和回調函數. - 第二步: 由獨立任務解析出用戶輸入的命令行,拆分出命令項和參數內容
- 第三步: 通過命令項在全局鏈表中遍歷找到已註冊的回調函數,並執行.
結構體
鴻蒙對命令的註冊用了三個結構體,個人感覺前兩個可以合成一個,降低代碼閱讀難度.
STATIC CmdModInfo g_cmdInfo;//shell 命令模塊信息,上面掛了所有的命令項(ls,cd ,cp ==)
typedef struct {//命令項
CmdType cmdType; //命令類型
//CMD_TYPE_EX:不支持標準命令參數輸入,會把用戶填寫的命令關鍵字屏蔽掉,例如:輸入ls /ramfs,傳入給註冊函數的參數只有/ramfs,而ls命令關鍵字並不會被傳入。
//CMD_TYPE_STD:支持的標準命令參數輸入,所有輸入的字符都會通過命令解析後被傳入。
const CHAR *cmdKey; //命令關鍵字,例如:ls 函數在Shell中訪問的名稱。
UINT32 paraNum; //調用的執行函數的入參最大個數,暫不支持。
CmdCallBackFunc cmdHook;//命令執行函數地址,即命令實際執行函數。
} CmdItem;
typedef struct { //命令節點
LOS_DL_LIST list; //雙向鏈表
CmdItem *cmd; //命令項
} CmdItemNode;
/* global info for shell module */
typedef struct {//shell 模塊的全局信息
CmdItemNode cmdList; //命令項節點
UINT32 listNum;//節點數量
UINT32 initMagicFlag;//初始魔法標籤 0xABABABAB
LosMux muxLock; //操作鏈表互斥鎖
CmdVerifyTransID transIdHook;//暫不知何意
} CmdModInfo;
解讀
CmdItem
為註冊的內容載體結構體,cmdHook
為回調函數,是命令的真正執行體.- 通過雙向鏈表
CmdItemNode.list
將所有命令穿起來 CmdModInfo
記錄命令數量和操作的互斥鎖,shell
的魔法數字為0xABABABAB
第一步 | Shell 註冊
-
靜態宏方式註冊,鏈接時處理
靜態註冊命令方式一般用在系統常用命令註冊,鴻蒙已支持以下命令.arp cat cd chgrp chmod chown cp cpup date dhclient dmesg dns format free help hwi ifconfig ipdebug kill log ls lsfd memcheck mkdir mount netstat oom partinfo partition ping ping6 pwd reset rm rmdir sem statfs su swtmr sync systeminfo task telnet test tftp touch umount uname watch writeproc
例如註冊
ls
命令SHELLCMD_ENTRY(ls_shellcmd, CMD_TYPE_EX, "ls", XARGS, (CMD_CBK_FUNC)osShellCmdLs)
需在鏈接選項中添加鏈接該新增命令項參數,具體在
liteos_tables_ldflags.mk
文件的LITEOS_TABLES_LDFLAGS
項下添加-uls_shellcmd
。至於SHELLCMD_ENTRY
是如何實現的在鏈接階段的註冊,請自行翻看(內聯彙編篇),有詳細說明實現細節. -
動態命令方式,運行時處理
動態註冊命令方式一般用在用戶命令註冊,具體實現代碼如下:osCmdReg(CMD_TYPE_EX, "ls", XARGS, (CMD_CBK_FUNC)osShellCmdLs) { // .... //5.正式創建命令,掛入鏈表 return OsCmdItemCreate(cmdType, cmdKey, paraNum, cmdProc);//不存在就註冊命令 } //創建一個命令項,例如 chmod STATIC UINT32 OsCmdItemCreate(CmdType cmdType, const CHAR *cmdKey, UINT32 paraNum, CmdCallBackFunc cmdProc) { CmdItem *cmdItem = NULL; CmdItemNode *cmdItemNode = NULL; //1.構造命令節點過程 cmdItem = (CmdItem *)LOS_MemAlloc(m_aucSysMem0, sizeof(CmdItem)); if (cmdItem == NULL) { return OS_ERRNO_SHELL_CMDREG_MEMALLOC_ERROR; } (VOID)memset_s(cmdItem, sizeof(CmdItem), '\0', sizeof(CmdItem)); cmdItemNode = (CmdItemNode *)LOS_MemAlloc(m_aucSysMem0, sizeof(CmdItemNode)); if (cmdItemNode == NULL) { (VOID)LOS_MemFree(m_aucSysMem0, cmdItem); return OS_ERRNO_SHELL_CMDREG_MEMALLOC_ERROR; } (VOID)memset_s(cmdItemNode, sizeof(CmdItemNode), '\0', sizeof(CmdItemNode)); cmdItemNode->cmd = cmdItem; //命令項 cmdItemNode->cmd->cmdHook = cmdProc;//回調函數 osShellCmdLs cmdItemNode->cmd->paraNum = paraNum;//`777`,'/home' cmdItemNode->cmd->cmdType = cmdType;//關鍵字類型 cmdItemNode->cmd->cmdKey = cmdKey; //`chmod` //2.完成構造後掛入全局鏈表 (VOID)LOS_MuxLock(&g_cmdInfo.muxLock, LOS_WAIT_FOREVER); OsCmdAscendingInsert(cmdItemNode);//按升序方式插入 g_cmdInfo.listNum++;//命令總數增加 (VOID)LOS_MuxUnlock(&g_cmdInfo.muxLock); return LOS_OK; }
第二步 解析 | ShellTask
//shell 服務端任務初始化,這個任務負責解析和執行命令
LITE_OS_SEC_TEXT_MINOR UINT32 ShellTaskInit(ShellCB *shellCB)
{
CHAR *name = NULL;
TSK_INIT_PARAM_S initParam = {0};
//輸入Shell命令的兩種方式
if (shellCB->consoleID == CONSOLE_SERIAL) { //通過串口工具
name = SERIAL_SHELL_TASK_NAME;
} else if (shellCB->consoleID == CONSOLE_TELNET) {//通過遠程工具
name = TELNET_SHELL_TASK_NAME;
} else {
return LOS_NOK;
}
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)ShellTask;//任務入口函數,主要是解析shell命令
initParam.usTaskPrio = 9; /* 9:shell task priority */
initParam.auwArgs[0] = (UINTPTR)shellCB;
initParam.uwStackSize = 0x3000;
initParam.pcName = name;
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
(VOID)LOS_EventInit(&shellCB->shellEvent);//初始化事件,以事件方式通知任務解析命令
return LOS_TaskCreate(&shellCB->shellTaskHandle, &initParam);//創建任務
}
LITE_OS_SEC_TEXT_MINOR UINT32 ShellTask(UINTPTR param1,
UINTPTR param2,
UINTPTR param3,
UINTPTR param4)
{
UINT32 ret;
ShellCB *shellCB = (ShellCB *)param1;
(VOID)param2;
(VOID)param3;
(VOID)param4;
while (1) {
PRINTK("\nOHOS # ");//讀取shell 輸入事件 例如: cat weharmony.net 命令
ret = LOS_EventRead(&shellCB->shellEvent,
0xFFF, LOS_WAITMODE_OR | LOS_WAITMODE_CLR, LOS_WAIT_FOREVER);
if (ret == SHELL_CMD_PARSE_EVENT) {//獲得解析命令事件
ShellCmdProcess(shellCB);//處理命令
} else if (ret == CONSOLE_SHELL_KEY_EVENT) {//退出shell事件
break;
}
}
OsShellKeyDeInit((CmdKeyLink *)shellCB->cmdKeyLink);//
OsShellKeyDeInit((CmdKeyLink *)shellCB->cmdHistoryKeyLink);
(VOID)LOS_EventDestroy(&shellCB->shellEvent);//註銷事件
(VOID)LOS_MemFree((VOID *)m_aucSysMem0, shellCB);//釋放shell控制塊
return 0;
}
解讀
-
任務優先級和 客戶端任務 一樣同為
9
-
指定內核棧大小為
0x3000 = 12K
,因任務負責命令的解析和執行,所以需要更大的內核空間. -
任務的入口函數
ShellTask
,一個死循環在以LOS_WAIT_FOREVER
方式死等事件發生.SHELL_CMD_PARSE_EVENT
通知開始解析事件,該事件由 客戶端任務ShellEntry
檢測到回車鍵時發出.
STATIC VOID ShellNotify(ShellCB *shellCB) { (VOID)LOS_EventWrite(&shellCB->shellEvent, SHELL_CMD_PARSE_EVENT); }
CONSOLE_SHELL_KEY_EVENT
收到exit
命令時將發出該事件,退出shell
回收資源
鴻蒙內核是如何管理和使用事件的請自行翻看(事件控制篇)
-
層層跟進
ShellCmdProcess
,解析出命令項和參數內容,最終跑到OsCmdExec
中遍歷 已註冊的命令表,找出命令對應的函數完成回調.LITE_OS_SEC_TEXT_MINOR UINT32 OsCmdExec(CmdParsed *cmdParsed, CHAR *cmdStr) { UINT32 ret; CmdCallBackFunc cmdHook = NULL; CmdItemNode *curCmdItem = NULL; UINT32 i; const CHAR *cmdKey = NULL; if ((cmdParsed == NULL) || (cmdStr == NULL) || (strlen(cmdStr) == 0)) { return (UINT32)OS_ERROR; } ret = OsCmdParse(cmdStr, cmdParsed);//解析出命令關鍵字,參數 if (ret != LOS_OK) { goto OUT; } //遍曆命令註冊全局鏈表 LOS_DL_LIST_FOR_EACH_ENTRY(curCmdItem, &(g_cmdInfo.cmdList.list), CmdItemNode, list) { cmdKey = curCmdItem->cmd->cmdKey; if ((cmdParsed->cmdType == curCmdItem->cmd->cmdType) && (strlen(cmdKey) == strlen(cmdParsed->cmdKeyword)) && (strncmp(cmdKey, (CHAR *)(cmdParsed->cmdKeyword), strlen(cmdKey)) == 0)) {//找到命令的回調函數 例如: ls <-> osShellCmdLs cmdHook = curCmdItem->cmd->cmdHook; break; } } ret = OS_ERROR; if (cmdHook != NULL) {//執行命令,即回調函數 ret = (cmdHook)(cmdParsed->paramCnt, (const CHAR **)cmdParsed->paramArray); } OUT: for (i = 0; i < cmdParsed->paramCnt; i++) {//無效的命令要釋放掉保存參數的內存 if (cmdParsed->paramArray[i] != NULL) { (VOID)LOS_MemFree(m_aucSysMem0, cmdParsed->paramArray[i]); cmdParsed->paramArray[i] = NULL; } } return (UINT32)ret; }
第三步 | 執行
想知道有哪些系統shell
命令,可以搜索關鍵詞SHELLCMD_ENTRY
拿到所有通過靜態方式註冊的命令.
其中有網絡的,進程的,任務的,內存的 等等,此處列出幾個常用的shell
命令的實現.
ls 命令
SHELLCMD_ENTRY(ls_shellcmd, CMD_TYPE_EX, "ls", XARGS, (CmdCallBackFunc)osShellCmdLs);
/*******************************************************
命令功能
ls命令用來顯示當前目錄的內容。
命令格式
ls [path]
path為空時,顯示當前目錄的內容。
path為無效文件名時,顯示失敗,提示:
ls error: No such directory。
path為有效目錄路徑時,會顯示對應目錄下的內容。
使用指南
ls命令顯示當前目錄的內容。
ls可以顯示文件的大小。
proc下ls無法統計文件大小,顯示為0。
*******************************************************/
int osShellCmdLs(int argc, const char **argv)
{
char *fullpath = NULL;
const char *filename = NULL;
int ret;
char *shell_working_directory = OsShellGetWorkingDirtectory();//獲取當前工作目錄
if (shell_working_directory == NULL)
{
return -1;
}
ERROR_OUT_IF(argc > 1, PRINTK("ls or ls [DIRECTORY]\n"), return -1);
if (argc == 0)//木有參數時 -> #ls
{
ls(shell_working_directory);//執行ls 當前工作目錄
return 0;
}
filename = argv[0];//有參數時 -> #ls ../harmony or #ls /no such file or directory
ret = vfs_normalize_path(shell_working_directory, filename, &fullpath);//獲取全路徑,注意這裡帶出來fullpath,而fullpath已經在內核空間
ERROR_OUT_IF(ret < 0, set_err(-ret, "ls error"), return -1);
ls(fullpath);//執行 ls 全路徑
free(fullpath);//釋放全路徑,為啥要釋放,因為fullpath已經由內核空間分配
return 0;
}
task 命令
SHELLCMD_ENTRY(task_shellcmd, CMD_TYPE_EX, "task", 1, (CmdCallBackFunc)OsShellCmdDumpTask);
LITE_OS_SEC_TEXT_MINOR UINT32 OsShellCmdDumpTask(INT32 argc, const CHAR **argv)
{
UINT32 flag = 0;
#ifdef LOSCFG_KERNEL_VM
flag |= OS_PROCESS_MEM_INFO;
#endif
if (argc >= 2) { /* 2: The task shell name restricts the parameters */
goto TASK_HELP;
}
if (argc == 1) {
if (strcmp("-a", argv[0]) == 0) {
flag |= OS_PROCESS_INFO_ALL;
} else if (strcmp("-i", argv[0]) == 0) {
if (!OsShellShowTickRespo()) {
return LOS_OK;
}
goto TASK_HELP;
} else if (strcmp("-t", argv[0]) == 0) {
if (!OsShellShowSchedParam()) {
return LOS_OK;
}
goto TASK_HELP;
} else {
goto TASK_HELP;
}
}
return OsShellCmdTskInfoGet(OS_ALL_TASK_MASK, NULL, flag);
TASK_HELP:
PRINTK("Unknown option: %s\n", argv[0]);
PRINTK("usage: task or task -a\n");
return LOS_NOK;
}
cat 命令
SHELLCMD_ENTRY(cat_shellcmd, CMD_TYPE_EX, "cat", XARGS, (CmdCallBackFunc)osShellCmdCat);
/*****************************************************************
cat用於顯示文本文件的內容。cat [pathname]
cat weharmony.txt
*****************************************************************/
int osShellCmdCat(int argc, const char **argv)
{
char *fullpath = NULL;
int ret;
unsigned int ca_task;
struct Vnode *vnode = NULL;
TSK_INIT_PARAM_S init_param;
char *shell_working_directory = OsShellGetWorkingDirtectory();//顯示當前目錄 pwd
if (shell_working_directory == NULL)
{
return -1;
}
ERROR_OUT_IF(argc != 1, PRINTK("cat [FILE]\n"), return -1);
ret = vfs_normalize_path(shell_working_directory, argv[0], &fullpath);//由相對路徑獲取絕對路徑
ERROR_OUT_IF(ret < 0, set_err(-ret, "cat error"), return -1);
VnodeHold();
ret = VnodeLookup(fullpath, &vnode, O_RDONLY);
if (ret != LOS_OK)
{
set_errno(-ret);
perror("cat error");
VnodeDrop();
free(fullpath);
return -1;
}
if (vnode->type != VNODE_TYPE_REG)
{
set_errno(EINVAL);
perror("cat error");
VnodeDrop();
free(fullpath);
return -1;
}
VnodeDrop();
(void)memset_s(&init_param, sizeof(init_param), 0, sizeof(TSK_INIT_PARAM_S));
init_param.pfnTaskEntry = (TSK_ENTRY_FUNC)osShellCmdDoCatShow;
init_param.usTaskPrio = CAT_TASK_PRIORITY; //優先級10
init_param.auwArgs[0] = (UINTPTR)fullpath; //入口參數
init_param.uwStackSize = CAT_TASK_STACK_SIZE;//內核棧大小
init_param.pcName = "shellcmd_cat"; //任務名稱
init_param.uwResved = LOS_TASK_STATUS_DETACHED | OS_TASK_FLAG_SPECIFIES_PROCESS;
init_param.processID = 2; /* 2: kProcess */ //內核任務
ret = (int)LOS_TaskCreate(&ca_task, &init_param);//創建任務顯示cat內容
if (ret != LOS_OK)
{
free(fullpath);
}
return ret;
}
你能看明白這些命令的底層實現嗎? 如果看明白了,可能會不由得發出 原來如此 的感嘆!
百篇博客分析.深挖內核地基
- 給鴻蒙內核源碼加註釋過程中,整理出以下文章。內容立足源碼,常以生活場景打比方儘可能多的將內核知識點置入某種場景,具有畫面感,容易理解記憶。說別人能聽得懂的話很重要! 百篇博客絕不是百度教條式的在說一堆詰屈聱牙的概念,那沒什麼意思。更希望讓內核變得栩栩如生,倍感親切.確實有難度,自不量力,但已經出發,回頭已是不可能的了。 😛
- 與代碼有bug需不斷debug一樣,文章和註解內容會存在不少錯漏之處,請多包涵,但會反覆修正,持續更新,v**.xx 代表文章序號和修改的次數,精雕細琢,言簡意賅,力求打造精品內容。
按功能模塊:
百萬漢字註解.精讀內核源碼
四大碼倉中文註解 . 定期同步官方代碼
鴻蒙研究站( weharmonyos ) | 每天死磕一點點,原創不易,歡迎轉載,請註明出處。若能支持點贊則更佳,感謝每一份支持。