子曰:“我非生而知之者,好古,敏以求之者也。” 《論語》:述而篇

百篇部落格系列篇.本篇為:

v71.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
  • shellTaskHandleshellEntryHandle編輯/處理shell命令的兩個任務ID,本篇重點說後一個.
  • cmdKeyLink,cmdHistoryKeyLink,cmdMaskKeyLink是三個型別為CmdKeyLink的結構體,本質是雙向連結串列,對應編輯shell命令過程中的三個功能.
    • cmdKeyLink 待執行的命令連結串列
    • cmdHistoryKeyLink 儲存命令歷史記錄的,即: history命令顯示的內容
    • cmdMaskKeyLink 記錄按上下方向鍵輸出的內容,這個有點難理解,自行在shell中按上下方向鍵自行體驗
  • shellBufOffsetshellBuf是成對出現的,其中存放的就是使用者敲入處理後的字元.
  • keyMutexhistoryMutex為操作連結串列所需的互斥鎖,核心用的最多的就是這類鎖.
  • 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;
}

解讀

  • mainshell程序的主任務,每個程序都會建立一個預設的執行緒(任務),這個任務的入口函式就是大家熟知的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

      例如:當在控制檯按下 chtab鍵後會輸出以下三個

      chgrp         chmod         chown

      內容,這些功能對使用者而已看似再平常不過,但都需要核心一一實現.

  • shellBuf儲存編輯結果,當按下回車鍵時,將結果儲存並交付給下一個階段使用.

鴻蒙核心原始碼分析.總目錄

v08.xx 鴻蒙核心原始碼分析(總目錄) | 百萬漢字註解 百篇部落格分析 | 51.c.h .o

關注不迷路.程式碼即人生

QQ群:790015635 | 入群密碼: 666

原創不易,歡迎轉載,但請註明出處.