v72.01 鴻蒙內核源碼分析(Shell解析) | 應用窺伺內核的窗口 | 百篇博客分析OpenHarmony源碼

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

在這裡插入圖片描述

百篇博客系列篇.本篇為:

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 ) | 每天死磕一點點,原創不易,歡迎轉載,請註明出處。若能支持點贊則更佳,感謝每一份支持。