本篇关键词:、、、

下载 >> 离线文档.鸿蒙内核源码分析(百篇博客分析.挖透鸿蒙内核).pdf

任务管理相关篇为:

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

Shell | 控制台 | 串口模型

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

模型说明

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

代码实现

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

结构体 | 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
    CirBufSendCB *cirBufSendCB;	///< 循环缓冲发送控制块
    UINT8 fifo[CONSOLE_FIFO_SIZE]; ///< termios 规范模式(ICANON mode )下使用 size:1K
    UINT32 fifoOut;	///< 对fifo的标记,输出位置
    UINT32 fifoIn;	///< 对fifo的标记,输入位置
    UINT32 currentLen;	///< 当前fifo位置
    struct termios consoleTermios; ///< 线路规程
} CONSOLE_CB;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

解析

  • 创建控制台的过程是给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;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • 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;
          } 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);//创建shell任务
      #ifdef LOSCFG_PLATFORM_CONSOLE
          (VOID)ConsoleTaskReg((INT32)shellCB->consoleID, shellCB->shellEntryHandle);//将shell捆绑到控制台
      #endif
          return ret;
      }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    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);
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • 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
      };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 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;
    
    1
    2
    3
    4
    5
    6
    7

发送数据给终端的任务 | 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

任务的入口函数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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

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

传统的控制台和终端

控制台(console)和终端(terminal)有什么区别? 看张古老的图

这个不陌生吧,实现中虽很少看到,可电影里可没少出现。

据说是NASA航天飞机控制台,满满的科技感。

这就是控制台。早期控制台其实是给系统管理人员使用的。因为机器很大,价格很贵,不可能让每个人都拥有一个真正物理上属于自己的计算机,但是只让一个人用那其他人怎么办? 效率太低,就出现了多用户多任务计算机,让一台计算机多个人同时登录使用的情况, 给每个人面前放个简单设备(只有键盘和屏幕)连接到主机上,如图所示

这个就叫终端 ,注意别看那么大,长得很像一体机,但其实它只是一台显示器。这是给普通用户使用,权限也有限,核心功能权限还是在操作控制台的系统管理员手上。

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

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

现在的控制台和终端

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

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

百文说内核 | 抓住主脉络

  • 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
  • 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
  • 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。

按功能模块:

基础知识 进程管理 任务管理 内存管理
双向链表
内核概念
源码结构
地址空间
计时单位
优雅的宏
钩子框架
位图管理
POSIX
main函数
调度故事
进程控制块
进程空间
线性区
红黑树
进程管理
Fork进程
进程回收
Shell编辑
Shell解析
任务控制块
并发并行
就绪队列
调度机制
任务管理
用栈方式
软件定时器
控制台
远程登录
协议栈
内存规则
物理内存
内存概念
虚实映射
页表管理
静态分配
TLFS算法
内存池管理
原子操作
圆整对齐
通讯机制 文件系统 硬件架构 内核汇编
通讯总览
自旋锁
互斥锁
快锁使用
快锁实现
读写锁
信号量
事件机制
信号生产
信号消费
消息队列
消息封装
消息映射
共享内存
文件概念
文件故事
索引节点
VFS
文件句柄
根文件系统
挂载机制
管道文件
文件映射
写时拷贝
芯片模式
ARM架构
指令集
协处理器
工作模式
寄存器
多核管理
中断概念
中断管理
编码方式
汇编基础
汇编传参
链接脚本
内核启动
进程切换
任务切换
中断切换
异常接管
缺页中断
编译运行 调测工具
编译过程
编译构建
GN语法
忍者无敌
ELF格式
ELF解析
静态链接
重定位
动态链接
进程映像
应用启动
系统调用
VDSO
模块监控
日志跟踪
系统安全
测试用例

百万注源码 | 处处扣细节

  • 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

  • < gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码。

关注不迷路 | 代码即人生

  • 互联网从业十五年,计算机硕士,技术副总裁
  • 关注我,持续更新四十年,即聊技术也聊人生
  • 交有趣靠谱的人;做难而正确的事
  • 不做作,不炒作,只唯真
  • 不唯上,不唯书,只唯实

>> 捐助名单

据说喜欢 点赞 + 分享 的,后来都成了大神。😃