鴻蒙內核源碼分析(Shell編輯篇) | 兩個任務,三個階段 | 百篇部落格分析OpenHarmony源碼 | v71.01
子曰:「我非生而知之者,好古,敏以求之者也。」 《論語》:述而篇
百篇部落格系列篇.本篇為:
v71.xx 鴻蒙內核源碼分析(Shell編輯篇) | 兩個任務,三個階段 | 51 .c .h .o
進程管理相關篇為:
- v02.xx 鴻蒙內核源碼分析(進程管理篇) | 誰在管理內核資源 | 51 .c .h .o
- v24.xx 鴻蒙內核源碼分析(進程概念篇) | 進程在管理哪些資源 | 51 .c .h .o
- v45.xx 鴻蒙內核源碼分析(Fork篇) | 一次調用,兩次返回 | 51 .c .h .o
- v46.xx 鴻蒙內核源碼分析(特殊進程篇) | 老鼠生兒會打洞 | 51 .c .h .o
- v47.xx 鴻蒙內核源碼分析(進程回收篇) | 臨終前如何向老祖宗託孤 | 51 .c .h .o
- v48.xx 鴻蒙內核源碼分析(訊號生產篇) | 年過半百,依然活力十足 | 51 .c .h .o
- v49.xx 鴻蒙內核源碼分析(訊號消費篇) | 誰讓CPU連續四次換棧運行 | 51 .c .h .o
- v71.xx 鴻蒙內核源碼分析(Shell編輯篇) | 兩個任務,三個階段 | 51 .c .h .o
- v72.xx 鴻蒙內核源碼分析(Shell解析篇) | 應用窺伺內核的窗口 | 51 .c .h .o
系列篇從內核視角用一句話概括shell
的底層實現為:兩個任務,三個階段。其本質是獨立進程,因而划到進程管理模組。每次創建shell
進程都會再創建兩個任務。
- 客戶端任務(ShellEntry): 負責接受來自終端(控制台)敲入的一個個字元,字元按
VT
規範組裝成一句句的命令。 - 服務端任務(ShellTask): 對命令進行解析並執行,將結果輸出到控制台。
而按命令生命周期可分三個階段.
- 編輯: 鴻蒙在這個部分實現了一個簡單的編輯器功能,處理控制台輸入的每個字元,主要包括了對控制字元 例如
<ESC>
,\t
,\b
,\n
,\r
,四個方向鍵0x41
~0x44
的處理。 - 解析: 對編輯後的字元串進行解析,解析出命令項和參數項,找到對應的命令項執行函數。
- 執行: 命令可通過靜態和動態兩種方式註冊到內核,解析出具體命令後在註冊表中找到對應函數回調。將結果輸出到控制台。
編輯部分由客戶端任務完成,後兩個部分由服務端任務完成,命令全局註冊由內核完成。
- 本篇主要說 客戶端任務 和 編輯過程
- 服務端任務 和 解析/執行過程 已在(Shell解析篇)中說明,請自行翻看.
什麼是 Shell
從用戶視角看,shell
是用戶窺視和操作內核的一個窗口,內核並非鐵板一塊,對應用層開了兩個窗口,一個是系統調用,一個就是shell
,由內核提供實現函數,由用戶提供參數執行。區別是 shell
是由獨立的任務去完成,可通過將shell
命令序列化編寫成獨立的,簡單的shell
程式,所以shell
也是一門腳本語言,系統調用是依附於應用程式的任務去完成,能做的有限。通過shell
窗口能看到 cpu
的運行情況,記憶體的消耗情況,網路的鏈接狀態等等。
鴻蒙 Shell 程式碼在哪
與shell
對應的概念是kernel
,在鴻蒙內核,這兩部分程式碼是分開放的,shell
程式碼在 查看 shell 程式碼 ,目錄結構如下.
├─include
│ dmesg.h
│ dmesg_pri.h
│ shcmd.h
│ shcmdparse.h
│ shell.h
│ shell_lk.h
│ shell_pri.h
│ shmsg.h
│ show.h
│
└─src
├─base
│ shcmd.c
│ shcmdparse.c
│ shell_lk.c
│ shmsg.c
│ show.c
│
└─cmds
date_shellcmd.c
dmesg.c
hwi_shellcmd.c
shell_shellcmd.c
watch_shellcmd.c
Shell 控制塊
跟進程,任務一樣,每個概念的背後需要一個主結構體來的支撐,shell
的主結構體就是ShellCB
,掌握它就可以將shell
拿捏的死死的,搞不懂這個結構體就讀不懂shell
的內核實現.所以在上面花再多功夫也不為過.
typedef struct {
UINT32 consoleID; //控制台ID
UINT32 shellTaskHandle; //shell服務端任務ID
UINT32 shellEntryHandle; //shell客戶端任務ID
VOID *cmdKeyLink; //待處理的shell命令鏈表
VOID *cmdHistoryKeyLink;//已處理的歷史記錄鏈表,去重,10個
VOID *cmdMaskKeyLink; //主要用於方向鍵上下遍歷歷史命令
UINT32 shellBufOffset; //buf偏移量
UINT32 shellKeyType; //按鍵類型
EVENT_CB_S shellEvent; //事件類型觸發
pthread_mutex_t keyMutex; //按鍵互斥量
pthread_mutex_t historyMutex; //歷史記錄互斥量
CHAR shellBuf[SHOW_MAX_LEN]; //shell命令buf,接受鍵盤的輸入,需要對輸入字元解析.
CHAR shellWorkingDirectory[PATH_MAX];//shell的工作目錄
} ShellCB;
//一個shell命令的結構體,命令有長有短,鴻蒙採用了可變數組的方式實現
typedef struct {
UINT32 count; //字元數量
LOS_DL_LIST list; //雙向鏈表
CHAR cmdString[0]; //字元串,可變數組的一種實現方式.
} CmdKeyLink;
enum {
STAT_NOMAL_KEY, //普通的按鍵
STAT_ESC_KEY, //<ESC>鍵在VT控制規範中時控制的起始鍵
STAT_MULTI_KEY //組合鍵
};
解讀
- 鴻蒙支援兩種方式在控制台輸入
Shell
命令,關於控制台請自行翻看控制台篇.- 在串口工具中直接輸入
Shell
命令CONSOLE_SERIAL
。 - 在
telnet
工具中輸入Shell
命令CONSOLE_TELNET
。
- 在串口工具中直接輸入
shellTaskHandle
和shellEntryHandle
編輯/處理shell
命令的兩個任務ID,本篇重點說後一個.cmdKeyLink
,cmdHistoryKeyLink
,cmdMaskKeyLink
是三個類型為CmdKeyLink
的結構體,本質是雙向鏈表,對應編輯shell
命令過程中的三個功能.cmdKeyLink
待執行的命令鏈表cmdHistoryKeyLink
存儲命令歷史記錄的,即:history
命令顯示的內容cmdMaskKeyLink
記錄按上下方向鍵輸出的內容,這個有點難理解,自行在shell
中按上下方向鍵自行體驗
shellBufOffset
和shellBuf
是成對出現的,其中存放的就是用戶敲入處理後的字元.keyMutex
和historyMutex
為操作鏈表所需的互斥鎖,內核用的最多的就是這類鎖.shellEvent
用於任務之間的通訊,比如.SHELL_CMD_PARSE_EVENT
:編輯完成了通知解析任務開始執行CONSOLE_SHELL_KEY_EVENT
:收到來自控制台的CTRL + C
訊號產生的事件.
shellKeyType
按鍵的類型,分三種 普通,鍵,組合鍵 shellWorkingDirectory
工作區就不用說了,從哪個目錄進入shell
的
創建 Shell
//shell進程的入口函數
int main(int argc, char **argv)
{
//...
g_shellCB = shellCB;//全局變數,說明鴻蒙同時只支援一個shell進程
return OsShellCreateTask(shellCB);//初始化兩個任務
}
//創建shell任務
STATIC UINT32 OsShellCreateTask(ShellCB *shellCB)
{
UINT32 ret = ShellTaskInit(shellCB);//執行shell命令的任務初始化
if (ret != LOS_OK) {
return ret;
}
return ShellEntryInit(shellCB);//通過控制台接收shell命令的任務初始化
}
//進入shell客戶端任務初始化,這個任務負責編輯命令,處理命令產生的過程,例如如何處理方向鍵,退格鍵,回車鍵等
LITE_OS_SEC_TEXT_MINOR UINT32 ShellEntryInit(ShellCB *shellCB)
{
UINT32 ret;
CHAR *name = NULL;
TSK_INIT_PARAM_S initParam = {0};
if (shellCB->consoleID == CONSOLE_SERIAL) {
name = SERIAL_ENTRY_TASK_NAME;
} else if (shellCB->consoleID == CONSOLE_TELNET) {
name = TELNET_ENTRY_TASK_NAME;
} else {
return LOS_NOK;
}
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)ShellEntry;//任務入口函數
initParam.usTaskPrio = 9; /* 9:shell task priority */
initParam.auwArgs[0] = (UINTPTR)shellCB;
initParam.uwStackSize = 0x1000;
initParam.pcName = name;
initParam.uwResved = LOS_TASK_STATUS_DETACHED;
ret = LOS_TaskCreate(&shellCB->shellEntryHandle, &initParam);//創建任務
#ifdef LOSCFG_PLATFORM_CONSOLE
(VOID)ConsoleTaskReg((INT32)shellCB->consoleID, shellCB->shellEntryHandle);//將任務註冊到控制台
#endif
return ret;
}
解讀
main
為shell
進程的主任務,每個進程都會創建一個默認的執行緒(任務),這個任務的入口函數就是大家熟知的main
函數,不清楚的自行翻看任務管理各篇有詳細的說明.- 由
main
任務再創建兩個任務,即本篇開頭說的兩個任務,本篇重點說其中的一個ShellEntry
,任務優先順序為9
,算是較高優先順序. - 指定內核棧大小為
0x1000 = 4K
,因任務只負責編輯處理控制台輸入的字元,命令的執行在其他任務,所以4K的內核空間足夠使用. ShellEntry
為入口函數,這個函數的實現為本篇的重點
ShellEntry | 編輯過程
LITE_OS_SEC_TEXT_MINOR UINT32 ShellEntry(UINTPTR param)
{
CHAR ch;
INT32 n = 0;
ShellCB *shellCB = (ShellCB *)param;
CONSOLE_CB *consoleCB = OsGetConsoleByID((INT32)shellCB->consoleID);//獲取控制台
if (consoleCB == NULL) {
PRINT_ERR("Shell task init error!\n");
return 1;
}
(VOID)memset_s(shellCB->shellBuf, SHOW_MAX_LEN, 0, SHOW_MAX_LEN);//重置shell命令buf
while (1) {
#ifdef LOSCFG_PLATFORM_CONSOLE
if (!IsConsoleOccupied(consoleCB)) {//控制台是否被佔用
#endif
/* is console ready for shell ? */
n = read(consoleCB->fd, &ch, 1);//從控制台讀取一個字元內容,字元一個個處理
if (n == 1) {//如果能讀到一個字元
ShellCmdLineParse(ch, (pf_OUTPUT)dprintf, shellCB);
}
if (is_nonblock(consoleCB)) {//在非阻塞模式下暫停 50ms
LOS_Msleep(50); /* 50: 50MS for sleep */
}
#ifdef LOSCFG_PLATFORM_CONSOLE
}
#endif
}
}
//對命令行內容解析
LITE_OS_SEC_TEXT_MINOR VOID ShellCmdLineParse(CHAR c, pf_OUTPUT outputFunc, ShellCB *shellCB)
{
const CHAR ch = c;
INT32 ret;
//不是回車鍵和字元串結束,且偏移量為0
if ((shellCB->shellBufOffset == 0) && (ch != '\n') && (ch != '\0')) {
(VOID)memset_s(shellCB->shellBuf, SHOW_MAX_LEN, 0, SHOW_MAX_LEN);//重置buf
}
//遇到回車或換行
if ((ch == '\r') || (ch == '\n')) {
if (shellCB->shellBufOffset < (SHOW_MAX_LEN - 1)) {
shellCB->shellBuf[shellCB->shellBufOffset] = '\0';//字元串結束
}
shellCB->shellBufOffset = 0;
(VOID)pthread_mutex_lock(&shellCB->keyMutex);
OsShellCmdPush(shellCB->shellBuf, shellCB->cmdKeyLink);//解析回車或換行
(VOID)pthread_mutex_unlock(&shellCB->keyMutex);
ShellNotify(shellCB);//通知任務解析shell命令
return;
} else if ((ch == '\b') || (ch == 0x7F)) { /* backspace or delete(0x7F) */ //遇到刪除鍵
if ((shellCB->shellBufOffset > 0) && (shellCB->shellBufOffset < (SHOW_MAX_LEN - 1))) {
shellCB->shellBuf[shellCB->shellBufOffset - 1] = '\0';//填充`\0`
shellCB->shellBufOffset--;//buf減少
outputFunc("\b \b");//回調入參函數
}
return;
} else if (ch == 0x09) { /* 0x09: tab *///遇到tab鍵
if ((shellCB->shellBufOffset > 0) && (shellCB->shellBufOffset < (SHOW_MAX_LEN - 1))) {
ret = OsTabCompletion(shellCB->shellBuf, &shellCB->shellBufOffset);//解析tab鍵
if (ret > 1) {
outputFunc("OHOS # %s", shellCB->shellBuf);//回調入參函數
}
}
return;
}
/* parse the up/down/right/left key */
ret = ShellCmdLineCheckUDRL(ch, shellCB);//解析上下左右鍵
if (ret == LOS_OK) {
return;
}
if ((ch != '\n') && (ch != '\0')) {//普通的字元的處理
if (shellCB->shellBufOffset < (SHOW_MAX_LEN - 1)) {//buf範圍
shellCB->shellBuf[shellCB->shellBufOffset] = ch;//直接加入
} else {
shellCB->shellBuf[SHOW_MAX_LEN - 1] = '\0';//加入字元串結束符
}
shellCB->shellBufOffset++;//偏移量增加
outputFunc("%c", ch);//向終端輸出字元
}
shellCB->shellKeyType = STAT_NOMAL_KEY;//普通字元
}
解讀
ShellEntry
內部是個死循環,不斷的讀取控制台輸入的每個字元,注意是按字元處理.- 處理四個方向,換行回車,
tab
,backspace
,delete
,esc
等控制鍵,相當於重新認識了下Ascii
表.可以把shell
終端理解為一個簡單的編輯器.- 按回車鍵 表示完成前面的輸入,進入解析執行階段.
- 按方向鍵 要顯示上/下一個命令的內容,一直按就一直顯示上上/下下命令.
- 按
tab
鍵 是要補齊命令的內容,目前鴻蒙支援如下命令: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
例如:當在控制台按下
ch
和tab
鍵後會輸出以下三個chgrp chmod chown
內容,這些功能對使用者而已看似再平常不過,但都需要內核一一實現.
shellBuf
存儲編輯結果,當按下回車鍵時,將結果保存並交付給下一個階段使用.
鴻蒙內核源碼分析.總目錄
v08.xx 鴻蒙內核源碼分析(總目錄) | 百萬漢字註解 百篇部落格分析 | 51 .c .h .o
關注不迷路.程式碼即人生
QQ群:790015635 | 入群密碼: 666
原創不易,歡迎轉載,但請註明出處.