
百篇部落格分析.本篇為: (控制台篇) | 一個讓很多人模糊的概念
檔案系統相關篇為:
- v62.02 鴻蒙核心源碼分析(檔案概念) | 為什麼說一切皆是檔案
- v63.04 鴻蒙核心源碼分析(檔案系統) | 用圖書管理說檔案系統
- v64.06 鴻蒙核心源碼分析(索引節點) | 誰是檔案系統最重要的概念
- v65.05 鴻蒙核心源碼分析(挂載目錄) | 為何檔案系統需要挂載
- v66.07 鴻蒙核心源碼分析(根檔案系統) | 誰先挂到/誰就是根總
- v67.03 鴻蒙核心源碼分析(字元裝置) | 絕大多數裝置都是這類
- v68.02 鴻蒙核心源碼分析(VFS) | 檔案系統是個大家庭
- v69.04 鴻蒙核心源碼分析(檔案句柄) | 你為什麼叫句柄
- v70.05 鴻蒙核心源碼分析(管道檔案) | 如何降低資料流動成本
- v74.01 鴻蒙核心源碼分析(控制台) | 一個讓很多人模糊的概念
本篇嘗試講明白控制台實作以及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
和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
(行規程)的規範模式,輸入資料基于行進行處理。在使用者輸入一個行結束符(回車符、EOF等)之前,系統調用read()讀不到使用者輸入的任何字元。除了EOF之外的行結束符(回車符等),與普通字元一樣會被read()讀到緩沖區termios
中。在規範模式中,可以進行行編輯,而且一次調用read()最多隻能讀取一行資料。如果read()請求讀取的資料位元組少于目前行可讀取的位元組,則read()隻讀取被請求的位元組數,剩下的位元組下次再讀。詳細内容見系列篇之 行規程篇fifo
-
是專用于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 ) | 每天死磕一點點,原創不易,歡迎轉載,請注明出處。若能支援點贊則更佳,感謝每一份支援。