v72.01 鴻蒙核心原始碼分析(Shell解析篇) | 應用窺伺核心的視窗 | 百篇部落格分析OpenHarmony原始碼

語言: CN / TW / HK

子曰:“苟正其身矣,於從政乎何有?不能正其身,如正人何?” 《論語》:子路篇

在這裡插入圖片描述

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

v72.xx 鴻蒙核心原始碼分析(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拿到所有通過靜態方式註冊的命令.

pipefinal-1

其中有網路的,程序的,任務的,記憶體的 等等,此處列出幾個常用的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 代表文章序號和修改的次數,精雕細琢,言簡意賅,力求打造精品內容。

按功能模組:

前因後果 基礎工具 載入執行 程序管理
總目錄 排程故事 記憶體主奴 原始碼註釋 原始碼結構 靜態站點 雙向連結串列 點陣圖管理 用棧方式 定時器 原子操作 時間管理 ELF格式 ELF解析 靜態連結 重定位 程序映像 程序管理 程序概念 Fork 特殊程序 程序回收 訊號生產 訊號消費 Shell編輯 Shell解析
編譯構建 程序通訊 記憶體管理 任務管理
編譯環境 編譯過程 環境指令碼 構建工具 gn應用 忍者ninja 自旋鎖 互斥鎖 程序通訊 訊號量 事件控制 訊息佇列 記憶體分配 記憶體管理 記憶體彙編 記憶體對映 記憶體規則 實體記憶體 時鐘任務 任務排程 任務管理 排程佇列 排程機制 執行緒概念 併發並行 CPU 系統呼叫 任務切換
檔案系統 硬體架構
檔案概念 檔案系統 索引節點 掛載目錄 根檔案系統 字元裝置 VFS 檔案控制代碼 管道檔案 彙編基礎 彙編傳參 工作模式 暫存器 異常接管 彙編彙總 中斷切換 中斷概念 中斷管理

百萬漢字註解.精讀核心原始碼

四大碼倉中文註解 . 定期同步官方程式碼

WeHarmony/kernel_liteos_a_note

鴻蒙研究站( weharmonyos ) | 每天死磕一點點,原創不易,歡迎轉載,請註明出處。若能支援點贊則更佳,感謝每一份支援。

「其他文章」