开源鸿蒙内核源码分析系列 | Shell编辑 | 两个任务 三个阶段(转载)

开源鸿蒙内核源码分析系列 | Shell编辑 | 两个任务 三个阶段(转载)

原文来自鸿蒙研究站:http://weharmonyos.com/blog/05.html

鸿蒙研究站网址:http://weharmonyos.com 

系列篇从内核视角用一句话概括shell的底层实现为:两个任务,三个阶段。其本质是独立进程,因而划到进程管理模块。每次创建shell进程都会再创建两个任务:

  • 客户端任务(ShellEntry):负责接受来自终端(控制台)敲入的一个个字符,字符按VT规范组装成一句句的命令。
  • 服务端任务(ShellTask):对命令进行解析并执行,将结果输出到控制台。

而按命令生命周期可分三个阶段。

  • 编辑:鸿蒙在这个部分实现了一个简单的编辑器功能,处理控制台输入的每个字符,主要包括了对控制字符 例如 <ESC>,\t,\b,\n,\r,四个方向键0x41 ~ 0x44 的处理。
  • 解析:对编辑后的字符串进行解析,解析出命令项和参数项,找到对应的命令项执行函数。
  • 执行:命令可通过静态和动态两种方式注册到内核,解析出具体命令后在注册表中找到对应函数回调。将结果输出到控制台。

编辑部分由客户端任务完成,后两个部分由服务端任务完成,命令全局注册由内核完成。

本篇主要说 客户端任务和编辑过程。

服务端任务和解析/执行过程将在下篇《开源鸿蒙内核源码分析系列 | Shell解析 | 应用窥伺内核的窗口》中说明,请大家稍后阅读。

什么是 Shell?

The Birth of Venus by Sandro Botticelli(c. 1484–1486)

从用户视角看,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 按键的类型,分三种:普通键、<ESC>键、组合键。
  • 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存储编辑结果,当按下回车键时,将结果保存并交付给下一个阶段使用。

    百文说内核 | 抓住主脉络

    子曰:“诗三百,一言以蔽之,曰‘思无邪’。”——《论语》:为政篇。

    百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在开源鸿蒙内核源码加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。
    百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

    与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。百篇博客系列思维导图结构如下:

    根据上图的思维导图,我们未来将要和大家一一分享以上大部分关键技术点的博客文章。

    百万汉字注解.精读内核源码

    如果大家觉得看文章不过瘾,想直接撸代码的话,可以去下面四大码仓围观同步注释内核源码:

    gitee仓

    https://gitee.com/weharmony/kernel_liteos_a_note

    github仓 :

    https://github.com/kuangyufei/kernel_liteos_a_note

    codechina仓

    https://codechina.csdn.net/kuangyufei/kernel_liteos_a_note

    coding仓

    https://weharmony.coding.net/public/harmony/kernel_liteos_a_note/git/files

    写在最后

    我们最近正带着大家玩嗨OpenHarmony。如果你有用OpenHarmony开发的好玩的东东,或者有对OpenHarmony的深度技术剖析,想通过我们平台让更多的小伙伴知道和分享的,欢迎投稿,让我们一起嗨起来!有点子,有想法,有Demo,立刻联系我们:

    合作邮箱:zzliang@atomsource.org