天天看點

v74.01 鴻蒙核心源碼分析(控制台篇) | 一個讓很多人模糊的概念 | 百篇部落格分析OpenHarmony源碼

v74.01 鴻蒙核心源碼分析(控制台篇) | 一個讓很多人模糊的概念 | 百篇部落格分析OpenHarmony源碼

百篇部落格分析.本篇為: (控制台篇) | 一個讓很多人模糊的概念

檔案系統相關篇為:

  • v62.02 鴻蒙核心源碼分析(檔案概念) | 為什麼說一切皆是檔案
  • v63.04 鴻蒙核心源碼分析(檔案系統) | 用圖書管理說檔案系統
  • v64.06 鴻蒙核心源碼分析(索引節點) | 誰是檔案系統最重要的概念
  • v65.05 鴻蒙核心源碼分析(挂載目錄) | 為何檔案系統需要挂載
  • v66.07 鴻蒙核心源碼分析(根檔案系統) | 誰先挂到/誰就是根總
  • v67.03 鴻蒙核心源碼分析(字元裝置) | 絕大多數裝置都是這類
  • v68.02 鴻蒙核心源碼分析(VFS) | 檔案系統是個大家庭
  • v69.04 鴻蒙核心源碼分析(檔案句柄) | 你為什麼叫句柄
  • v70.05 鴻蒙核心源碼分析(管道檔案) | 如何降低資料流動成本
  • v74.01 鴻蒙核心源碼分析(控制台) | 一個讓很多人模糊的概念

本篇嘗試講明白控制台實作以及Shell如何依賴控制台工作.涉及源碼部分隻列出關鍵代碼.

詳細代碼前往 >> 中文注解鴻蒙核心源碼 檢視

Shell | 控制台模型

下圖為看完鴻蒙核心Shell和控制台源碼後整理的模型圖

v74.01 鴻蒙核心源碼分析(控制台篇) | 一個讓很多人模糊的概念 | 百篇部落格分析OpenHarmony源碼

模型說明

  • 模型涉及四個任務, 兩個在使用者空間,兩個在核心空間.使用者空間的在系列篇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

    ,而

    shell

    console

    之間正是通過這種方式勾搭在一塊的.具體在建立

    ShellEntry

    任務時将自己與控制台進行捆綁.看源碼實作
    ///進入shell用戶端任務初始化,這個任務負責編輯指令,處理指令産生的過程,例如如何處理方向鍵,倒退鍵,Enter鍵等
      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;
      }
               

    ConsoleTaskReg

    shellCB

    consoleCB

    捆綁在一塊,二者可以互相查找.

    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航天飛機控制台,滿滿的科技感.

這就是控制台.早期控制台其實是給系統管理人員使用的.因為機器很大,價格很貴,不可能讓每個人都擁有一個真正實體上屬于自己的計算機,但是隻讓一個人用那其他人怎麼辦? 效率太低,就出現了多使用者多任務計算機,讓一台計算機多個人同時登入使用的情況, 給每個人面前放個簡單裝置(隻有鍵盤和螢幕)連接配接到主機上,如圖所示

v74.01 鴻蒙核心源碼分析(控制台篇) | 一個讓很多人模糊的概念 | 百篇部落格分析OpenHarmony源碼

這個就叫終端 ,注意别看那麼大,長得很像一體機,但其實它隻是一台顯示器.這是給普通使用者使用,權限也有限,核心功能權限還是在操作控制台的系統管理者手上.

綜上所述,用圖表列出二者早期差異

差別 終端(terminal) 控制台(console)
裝置屬性 外挂的附加裝置 自帶的基本裝置
數量 多個 一個
主機信任度
輸出内容 主機處理的資訊 主機核心/自身資訊
操作員 普通使用者 管理者

現在的控制台和終端

由于時代的發展計算機的硬體越來越便宜,現在都是一個人獨占一台計算機(個人電腦),已經不再需要傳統意義上的硬體終端。現在終端和控制台都由硬體概念,逐漸演化成了軟體的概念。終端和控制台的界限也慢慢模糊了,複雜了,甚至控制台也變成了終端, 現在要怎麼了解它們,推薦一篇文章,請自行前往搜看.

<< 徹底了解Linux的各種終端類型以及概念 >>

v74.01 鴻蒙核心源碼分析(控制台篇) | 一個讓很多人模糊的概念 | 百篇部落格分析OpenHarmony源碼

本篇内容與圖中右上角的

/dev/console

那部分相關. 從鴻蒙核心視角來看,控制台和終端還是有很大差别的.

百篇部落格分析.深挖核心地基

  • 給鴻蒙核心源碼加注釋過程中,整理出以下文章。内容立足源碼,常以生活場景打比方盡可能多的将核心知識點置入某種場景,具有畫面感,容易了解記憶。說别人能聽得懂的話很重要! 百篇部落格絕不是百度教條式的在說一堆诘屈聱牙的概念,那沒什麼意思。更希望讓核心變得栩栩如生,倍感親切.确實有難度,自不量力,但已經出發,回頭已是不可能的了。 😛
  • 與代碼有bug需不斷debug一樣,文章和注解内容會存在不少錯漏之處,請多包涵,但會反複修正,持續更新,v**.xx 代表文章序号和修改的次數,精雕細琢,言簡意赅,力求打造精品内容。

按功能子產品:

前因後果 基礎工具 加載運作 程序管理

總目錄

排程故事

記憶體主奴

源碼注釋

源碼結構

靜态站點

注釋文檔

雙向連結清單

位圖管理

用棧方式

定時器

原子操作

時間管理

ELF格式

ELF解析

靜态連結

重定位

程序映像

程序概念

Fork

特殊程序

程序回收

信号生産

信号消費

Shell編輯

Shell解析

編譯建構 程序通訊 記憶體管理 任務管理

編譯環境

編譯過程

環境腳本

建構工具

gn應用

忍者ninja

自旋鎖

互斥鎖

信号量

事件控制

消息隊列

記憶體配置設定

記憶體彙編

記憶體映射

記憶體規則

實體記憶體

時鐘任務

任務排程

排程隊列

排程機制

線程概念

并發并行

CPU

系統調用

任務切換

檔案系統 硬體架構

檔案概念

索引節點

挂載目錄

根檔案系統

字元裝置

VFS

檔案句柄

管道檔案

控制台

彙編基礎

彙編傳參

工作模式

寄存器

異常接管

彙編彙總

中斷切換

中斷概念

中斷管理

百萬漢字注解.精讀核心源碼

四大碼倉中文注解 . 定期同步官方代碼

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

上一篇: Smart Link概述

繼續閱讀