v74.01 鸿蒙内核源码分析(控制台篇) | 一个让很多人模糊的概念 | 百篇博客分析OpenHarmony源码

原创
2021/12/11 20:08
阅读数 7.8K
AI总结

百篇博客分析.本篇为: (控制台篇) | 一个让很多人模糊的概念

文件系统相关篇为:

本篇尝试讲明白控制台实现以及Shell如何依赖控制台工作.涉及源码部分只列出关键代码. 详细代码前往 >> 中文注解鸿蒙内核源码 查看

Shell | 控制台模型

下图为看完鸿蒙内核Shell和控制台源码后整理的模型图

模型说明

  • 模型涉及四个任务, 两个在用户空间,两个在内核空间.用户空间的在系列篇Shell部分中已有详细说明,请前往查看.
  • SystemInit任务是在内核OsMain中创建的系统初始化任务,其中初始化了根文件系统,串口,控制台等内核模块
  • 在控制台模块中创建SendToSer任务,这是一个负责将控制台结果输出到终端的任务.
  • 结构体CONSOLE_CB,CirBufSendCB承载了控制台的实现过程.

代码实现

每个模块都有一个核心结构体,控制台则是

结构体 | CONSOLE_CB

/**
 * @brief 控制台控制块(描述符)
 */
typedef struct {
    UINT32 consoleID;	///< 控制台ID    例如 : 1 | 串口 , 2 | 远程登录
    UINT32 consoleType;	///< 控制台类型
    UINT32 consoleSem;	///< 控制台信号量
    UINT32 consoleMask;	///< 控制台掩码
    struct Vnode *devVnode;	///< 索引节点
    CHAR *name;	///< 名称 例如: /dev/console1 
    INT32 fd;	///< 系统文件句柄, 由内核分配
    UINT32 refCount;	///< 引用次数,用于判断控制台是否被占用
    UINT32 shellEntryId; ///<  负责接受来自终端信息的 "ShellEntry"任务,这个值在运行过程中可能会被换掉,它始终指向当前正在运行的shell客户端
    INT32 pgrpId;	///< 进程组ID
    BOOL isNonBlock; ///< 是否无锁方式		
#ifdef LOSCFG_SHELL
    VOID *shellHandle;	///< shell句柄,本质是 shell控制块 ShellCB
#endif
    UINT32 sendTaskID;	///< 创建任务通过事件接收数据, 见于OsConsoleBufInit
    /*--以下为 一家子 start---------*/
    CirBufSendCB *cirBufSendCB;	///< 循环缓冲发送控制块
    UINT8 fifo[CONSOLE_FIFO_SIZE]; ///< 控制台缓冲区大小 1K
    UINT32 fifoOut;	///< 对fifo的标记,输出位置
    UINT32 fifoIn;	///< 对fifo的标记,输入位置
    UINT32 currentLen;	///< 当前fifo位置
    /*---以上为 一家子 end------- https://man7.org/linux/man-pages/man3/tcflow.3.html */
    struct termios consoleTermios; ///< 行规程
} CONSOLE_CB;

解析

  • 创建控制台的过程是给CONSOLE_CB赋值的过程,如下

    STATIC CONSOLE_CB *OsConsoleCreate(UINT32 consoleID, const CHAR *deviceName)
    {
      INT32 ret;
      CONSOLE_CB *consoleCB = OsConsoleCBInit(consoleID);//初始化控制台
      ret = (INT32)OsConsoleBufInit(consoleCB);//控制台buf初始化,创建 ConsoleSendTask 任务
      ret = (INT32)LOS_SemCreate(1, &consoleCB->consoleSem);//创建控制台信号量
      ret = OsConsoleDevInit(consoleCB, deviceName);//控制台设备初始化,注意这步要在 OsConsoleFileInit 的前面.
      ret = OsConsoleFileInit(consoleCB);	//为 /dev/console(n|1:2)分配fd(3)
      OsConsoleTermiosInit(consoleCB, deviceName);//控制台行规程初始化
      return consoleCB;
    }
    
  • Shell是用户空间进程, 负责解析和执行用户输入的命令. 但前提是得先拿到用户的输入数据. 不管数据是从串口进来,还是远程登录进来,必须得先经过内核, 而控制台的作用就是帮你拿到数据再交给shell处理, shell再将要显示的处理结果通过控制台返回给终端用户, 那数据怎么传给shell呢? 很显然用户进程只能通过系统调用 read(fd,...)来读取内核数据, 因为应用程序的视角是只认fd.通用的办法是通过文件路径来打开文件来获取fd.

  • 还有一种办法是内核先打开文件,获取fd后,用户任务通过捆绑的方式获取fd,而shellconsole之间正是通过这种方式勾搭在一块的.具体在创建ShellEntry任务时将自己与控制台进行捆绑.看源码实现

    ///进入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;
          } 
          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);//创建shell任务
      #ifdef LOSCFG_PLATFORM_CONSOLE
          (VOID)ConsoleTaskReg((INT32)shellCB->consoleID, shellCB->shellEntryHandle);//将shell捆绑到控制台
      #endif
          return ret;
      }
    

    ConsoleTaskRegshellCBconsoleCB捆绑在一块,二者可以相互查找.ShellEntry任务个人更愿意称之为shell的客户端任务,用死循环不断一个字符一个字符的读取用户的输入,为何要单字符 读取可翻看系列篇的Shell编辑篇,简单的说是因为要处理控制字符(如:删除,回车==)

    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);//获取绑定的控制台,目的是从控制台读数据
        (VOID)memset_s(shellCB->shellBuf, SHOW_MAX_LEN, 0, SHOW_MAX_LEN);//重置shell命令buf
        while (1) {
            n = read(consoleCB->fd, &ch, 1);//系统调用,从控制台读取一个字符内容,字符一个个处理
            if (n == 1) {//如果能读到一个字符
                ShellCmdLineParse(ch, (pf_OUTPUT)dprintf, shellCB);
            }
        }
    }
    
  • read函数的consoleCB->fd是个虚拟字符设备文件 如:/dev/console1,对文件的操作由g_consoleDevOps实现.read最终会调用ConsoleRead,再往下会调用到UART_Read

      /*! console device driver function structure | 控制台设备驱动程序,统一的vfs接口的实现 */
      STATIC const struct file_operations_vfs g_consoleDevOps = {
          .open = ConsoleOpen,   /* open */
          .close = ConsoleClose, /* close */
          .read = ConsoleRead,   /* read */
          .write = ConsoleWrite, /* write */
          .seek = NULL,
          .ioctl = ConsoleIoctl,
          .mmap = NULL,
      #ifndef CONFIG_DISABLE_POLL
          .poll = ConsolePoll,
      #endif
      };
    
  • fifo用于termios(行规程)的规范模式,输入数据基于行进行处理。在用户输入一个行结束符(回车符、EOF等)之前,系统调用read()读不到用户输入的任何字符。除了EOF之外的行结束符(回车符等),与普通字符一样会被read()读到缓冲区fifo中。在规范模式中,可以进行行编辑,而且一次调用read()最多只能读取一行数据。如果read()请求读取的数据字节少于当前行可读取的字节,则read()只读取被请求的字节数,剩下的字节下次再读。详细内容见系列篇之 行规程篇

  • CirBufSendCB是专用于SendToSer任务的结构体,任务之间通过事件相互驱动,控制台通知SendToSer将数据发送给终端

    /**
     * @brief 发送环形buf控制块,通过事件发送
     */
    typedef struct {
        CirBuf cirBufCB;        /* Circular buffer CB | 循环缓冲控制块 */
        EVENT_CB_S sendEvent;   /* Inform telnet send task | 例如: 给SendToSer任务发送事件*/
    } CirBufSendCB;
    

发送数据给终端的任务 | ConsoleSendTask

ConsoleSendTask只干一件事,将数据发送给串口或远程登录,任务优先级与shell同级,为9,它由系统初始化任务SystemInit创建, 具体可翻看系列篇之内核启动篇

/// 控制台缓存初始化,创建一个 发送任务
STATIC UINT32 OsConsoleBufInit(CONSOLE_CB *consoleCB)
{
    UINT32 ret;
    TSK_INIT_PARAM_S initParam = {0};
    consoleCB->cirBufSendCB = ConsoleCirBufCreate();//创建控制台
    if (consoleCB->cirBufSendCB == NULL) {
        return LOS_NOK;
    }
    initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)ConsoleSendTask;//控制台发送任务入口函数
    initParam.usTaskPrio   = SHELL_TASK_PRIORITY;	//优先级9
    initParam.auwArgs[0]   = (UINTPTR)consoleCB;	//入口函数的参数
    initParam.uwStackSize  = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;	//16K
    if (consoleCB->consoleID == CONSOLE_SERIAL) {//控制台的两种方式
        initParam.pcName   = "SendToSer";	//任务名称(发送数据到串口) 
    } else {
        initParam.pcName   = "SendToTelnet";//任务名称(发送数据到远程登录)
    }
    initParam.uwResved     = LOS_TASK_STATUS_DETACHED; //使用任务分离模式
    ret = LOS_TaskCreate(&consoleCB->sendTaskID, &initParam);//创建task 并加入就绪队列,申请立即调度
    if (ret != LOS_OK) { //创建失败处理
        ConsoleCirBufDelete(consoleCB->cirBufSendCB);//释放循环buf
        consoleCB->cirBufSendCB = NULL;//置NULL
        return LOS_NOK;
    }//永久等待读取 CONSOLE_SEND_TASK_RUNNING 事件,CONSOLE_SEND_TASK_RUNNING 由 ConsoleSendTask 发出.
    (VOID)LOS_EventRead(&consoleCB->cirBufSendCB->sendEvent, CONSOLE_SEND_TASK_RUNNING,
                        LOS_WAITMODE_OR | LOS_WAITMODE_CLR, LOS_WAIT_FOREVER);
	// ... 读取到 CONSOLE_SEND_TASK_RUNNING 事件才会往下执行  
    return LOS_OK;
}

任务的入口函数ConsoleSendTask实现也很简单,此处全部贴出来,死循环等待事件的发送.说到死循环多说两句,不要被while (1)吓倒,认为内核会卡死在这里玩不下去,那是应用程序员看待死循环的视角,其实在内核当等待的事件没有到来的时,这个任务并不会往下执行,而是处于挂起状态,当事件到来时才会切换回来继续往下走,那如何知道事件到来了呢? 可翻看系列篇之事件控制篇

STATIC UINT32 ConsoleSendTask(UINTPTR param)
{
    CONSOLE_CB *consoleCB = (CONSOLE_CB *)param;
    CirBufSendCB *cirBufSendCB = consoleCB->cirBufSendCB;
    CirBuf *cirBufCB = &cirBufSendCB->cirBufCB;
    UINT32 ret, size;
    UINT32 intSave;
    CHAR *buf = NULL;
    (VOID)LOS_EventWrite(&cirBufSendCB->sendEvent, CONSOLE_SEND_TASK_RUNNING);//发送一个控制台任务正在运行的事件
    while (1) {//读取 CONSOLE_CIRBUF_EVENT | CONSOLE_SEND_TASK_EXIT 这两个事件
        ret = LOS_EventRead(&cirBufSendCB->sendEvent, CONSOLE_CIRBUF_EVENT | CONSOLE_SEND_TASK_EXIT,
                            LOS_WAITMODE_OR | LOS_WAITMODE_CLR, LOS_WAIT_FOREVER);//读取循环buf或任务退出的事件
        if (ret == CONSOLE_CIRBUF_EVENT) {//控制台循环buf事件发生
            size =  LOS_CirBufUsedSize(cirBufCB);//循环buf使用大小
            if (size == 0) {
                continue;
            }
            buf = (CHAR *)LOS_MemAlloc(m_aucSysMem1, size + 1);//分配接收cirbuf的内存
            if (buf == NULL) {
                continue;
            }
            (VOID)memset_s(buf, size + 1, 0, size + 1);//清0
            LOS_CirBufLock(cirBufCB, &intSave);
            (VOID)LOS_CirBufRead(cirBufCB, buf, size);//读取循环cirBufCB至  buf
            LOS_CirBufUnlock(cirBufCB, intSave);

            (VOID)WriteToTerminal(consoleCB, buf, size);//将buf数据写到控制台终端设备
            (VOID)LOS_MemFree(m_aucSysMem1, buf);//清除buf
        } else if (ret == CONSOLE_SEND_TASK_EXIT) {//收到任务退出的事件, 由 OsConsoleBufDeinit 发出事件.
            break;//退出循环
        }
    }
    ConsoleCirBufDelete(cirBufSendCB);//删除循环buf,归还内存
    return LOS_OK;
}

上面提到了控制台和终端,是经常容易搞混的又变得越来越模糊两个概念,简单说明下.

传统的控制台和终端

控制台(console)和终端(terminal)有什么区别? 看张古老的图 这个不陌生吧,实现中虽很少看到,可电影里可没少出现.

据说是NASA航天飞机控制台,满满的科技感. 这就是控制台.早期控制台其实是给系统管理人员使用的.因为机器很大,价格很贵,不可能让每个人都拥有一个真正物理上属于自己的计算机,但是只让一个人用那其他人怎么办? 效率太低,就出现了多用户多任务计算机,让一台计算机多个人同时登录使用的情况, 给每个人面前放个简单设备(只有键盘和屏幕)连接到主机上,如图所示 这个就叫终端 ,注意别看那么大,长得很像一体机,但其实它只是一台显示器.这是给普通用户使用,权限也有限,核心功能权限还是在操作控制台的系统管理员手上.

综上所述,用图表列出二者早期差异

区别 终端(terminal) 控制台(console)
设备属性 外挂的附加设备 自带的基本设备
数量 多个 一个
主机信任度
输出内容 主机处理的信息 主机核心/自身信息
操作员 普通用户 管理员

现在的控制台和终端

由于时代的发展计算机的硬件越来越便宜,现在都是一个人独占一台计算机(个人电脑),已经不再需要传统意义上的硬件终端。现在终端和控制台都由硬件概念,逐渐演化成了软件的概念。终端和控制台的界限也慢慢模糊了,复杂了,甚至控制台也变成了终端, 现在要怎么理解它们,推荐一篇文章,请自行前往搜看. << 彻底理解Linux的各种终端类型以及概念 >>

本篇内容与图中右上角的/dev/console那部分相关. 从鸿蒙内核视角来看,控制台和终端还是有很大差别的.

百篇博客分析.深挖内核地基

  • 给鸿蒙内核源码加注释过程中,整理出以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。 😛
  • 与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。

按功能模块:

前因后果 基础工具 加载运行 进程管理
总目录 调度故事 内存主奴 源码注释 源码结构 静态站点 注释文档 双向链表 位图管理 用栈方式 定时器 原子操作 时间管理 ELF格式 ELF解析 静态链接 重定位 进程映像 进程管理 进程概念 Fork 特殊进程 进程回收 信号生产 信号消费 Shell编辑 Shell解析
编译构建 进程通讯 内存管理 任务管理
编译环境 编译过程 环境脚本 构建工具 gn应用 忍者ninja 自旋锁 互斥锁 进程通讯 信号量 事件控制 消息队列 内存分配 内存管理 内存汇编 内存映射 内存规则 物理内存 时钟任务 任务调度 任务管理 调度队列 调度机制 线程概念 并发并行 CPU 系统调用 任务切换
文件系统 硬件架构
文件概念 文件系统 索引节点 挂载目录 根文件系统 字符设备 VFS 文件句柄 管道文件 控制台 汇编基础 汇编传参 工作模式 寄存器 异常接管 汇编汇总 中断切换 中断概念 中断管理

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

四大码仓中文注解 . 定期同步官方代码

鸿蒙研究站( weharmonyos ) | 每天死磕一点点,原创不易,欢迎转载,请注明出处。若能支持点赞则更佳,感谢每一份支持。

展开阅读全文
加载中
点击加入讨论🔥(1) 发布并加入讨论🔥
1 评论
1 收藏
1
分享
AI总结
返回顶部
顶部