鴻蒙內核源碼分析(Shell編輯篇) | 兩個任務,三個階段 | 百篇部落格分析OpenHarmony源碼 | v71.01

子曰:「我非生而知之者,好古,敏以求之者也。」 《論語》:述而篇
在這裡插入圖片描述

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

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

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