v75.01 鴻蒙內核源碼分析(遠程登錄篇) | 內核如何接待遠方的客人 | 百篇部落格分析OpenHarmony源碼
- 2021 年 12 月 31 日
- 筆記
- OpenHarmony, 鴻蒙, 鴻蒙內核源碼, 鴻蒙內核源碼分析
子曰:「不學禮,無以立 ; 不學詩,無以言 」 《論語》:季氏篇
百篇部落格分析.本篇為: (遠程登錄篇) | 內核如何接待遠方的客人
設備驅動相關篇為:
- v67.03 鴻蒙內核源碼分析(字元設備) | 絕大多數設備都是這類
- v74.01 鴻蒙內核源碼分析(控制台) | 一個讓很多人模糊的概念
- v75.01 鴻蒙內核源碼分析(遠程登錄) | 內核如何接待遠方的客人
什麼是遠程登錄?
-
每個人都有上門做客的經歷,抖音也一直在教我們做人,做客不要空手去,總得帶點東西,而對中國人你就不能送鍾,不能送梨,最好也別送鞋,因他們與 終 離 邪 諧音,犯忌諱. 這是人情世故,叫禮儀,是中華文明圈的共識,是相互交流信任的基礎.
-
那互聯網圈有沒有這種共識呢? 當然有,互聯網世界的人情世故就是協議, 種種協議映射到人類社會來說就是種種禮儀,協議有
TCP
,HTTP
,SSH
,Telnet
等等,就如同禮儀分商業禮儀,外交禮儀,校園禮儀,家庭禮儀等等. 孔聖人不也說 不學禮,無以立 應該就是這個道理, 登門拜訪的禮儀可類比遠程登錄協議Telnet
. 來了就跟自己家一樣, 我家的東西就是你家的,隨便用,甭客氣.
Telnet
協議的具體內容可以查看以下文檔.
協議 | 時間 | 英文版 | 中文版 | 標題 |
---|---|---|---|---|
Telnet | 1983 | rfc854 | rfc854 | TELNET PROTOCOL SPECIFICATION(遠程登錄協議規範) |
Telnet | 1983 | rfc855 | rfc855 | TELNET OPTION SPECIFICATIONS(遠程登錄選項規範) |
Telnet
協議細節不是本篇討論的重點,後續會有專門的 Lwip協議棧 系列部落格說清楚.本篇要說清楚的是內核如何接待遠方的客人.
Shell | 控制台 | 遠程登錄模型
對遠程登錄來有客戶端和服務端的說法,跟別人來你家你是主人和你去別人家你是客人一樣,身份不同,職責不同,主人要做的事明顯要更多,本篇只說鴻蒙對telnet
服務端的實現,說清楚它是如何接待外面來的客人.至於圖中提到的客戶端任務是指主人為每個客人專門提供了一個對接人的意思.下圖為看完三部分源碼後整理的模型圖
模型解釋
- 通過本地的
shell
命令telnet on
啟動遠程登錄模組,由此創建Telnet的服務任務TelnetServer
TelnetServer
任務,創建socket
監聽23
埠,接受來自遠程終端的telnet xx.xx.xx.xx 23
請求- 收到請求後創建一個
TelnetClientLoop
用於接待客戶的任務,對接詳細的客戶需求. - 在接待客戶期間創建一個遠程登錄類型的控制台,來處理和轉發遠程客戶的請求給
shell
進程最終執行遠程命令. shell
處理完成後通過專門的任務SendToSer
回寫遠程終端,控制台部分詳細看 系列篇的(控制台篇)
鴻蒙是如何實現的?
1. 啟動 Telnet
//SHELLCMD_ENTRY(telnet_shellcmd, CMD_TYPE_EX, "telnet", 1, (CmdCallBackFunc)TelnetCmd);/// 以靜態方式註冊shell 命令
/// 本命令用於啟動或關閉telnet server服務
INT32 TelnetCmd(UINT32 argc, const CHAR **argv)
{
if (strcmp(argv[0], "on") == 0) { // 輸入 telnet on
/* telnet on: try to start telnet server task */
TelnetdTaskInit(); //啟動遠程登錄 服務端任務
return 0;
}
if (strcmp(argv[0], "off") == 0) {// 輸入 telnet off
/* telnet off: try to stop clients, then stop server task */
TelnetdTaskDeinit();//關閉所有的客戶端,並關閉服務端任務
return 0;
}
return 0;
}
2. 創建Telnet服務端任務
STATIC VOID TelnetdTaskInit(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S initParam = {0};
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)TelnetdMain; // telnet任務入口函數
initParam.uwStackSize = TELNET_TASK_STACK_SIZE; // 8K
initParam.pcName = "TelnetServer"; //任務名稱
initParam.usTaskPrio = TELNET_TASK_PRIORITY; //優先順序 9,和 shell 的優先順序一樣
initParam.uwResved = LOS_TASK_STATUS_DETACHED; //獨立模式
if (atomic_read(&g_telnetTaskId) != 0) {//只支援一個 telnet 服務任務
PRINT_ERR("telnet server is already running!\n");
return;
}
ret = LOS_TaskCreate((UINT32 *)&g_telnetTaskId, &initParam);//創建遠程登錄服務端任務並發起調度
}
3. Telnet服務端任務入口函數
//遠程登錄操作命令
STATIC const struct file_operations_vfs g_telnetOps = {
TelnetOpen,
TelnetClose,
TelnetRead,
TelnetWrite,
NULL,
TelnetIoctl,
NULL,
#ifndef CONFIG_DISABLE_POLL
TelnetPoll,
#endif
NULL,
};
STATIC INT32 TelnetdMain(VOID)
{
sock = TelnetdInit(TELNETD_PORT);//1.初始化創建 socket ,socket的本質就是打開了一個虛擬文件
TelnetLock();
ret = TelnetedRegister();//2.註冊驅動程式 /dev/telnet ,g_telnetOps g_telnetDev
TelnetUnlock();
TelnetdAcceptLoop(sock);//3.等待連接,處理遠程終端過來的命令 例如#task 命令
return 0;
}
4. 循環等待遠程終端的連接請求
STATIC VOID TelnetdAcceptLoop(INT32 listenFd)
{
while (g_telnetListenFd >= 0) {//必須啟動監聽
TelnetUnlock();
(VOID)memset_s(&inTelnetAddr, sizeof(inTelnetAddr), 0, sizeof(inTelnetAddr));
clientFd = accept(listenFd, (struct sockaddr *)&inTelnetAddr, (socklen_t *)&len);//接收數據
if (TelnetdAcceptClient(clientFd, &inTelnetAddr) == 0) {//
/*
* Sleep sometime before next loop: mostly we already have one connection here,
* and the next connection will be declined. So don't waste our cpu.
| 在下一個循環來臨之前休息片刻,因為鴻蒙只支援一個遠程登錄,此時已經有一個鏈接,
在TelnetdAcceptClient中創建執行緒不會立即調度, 休息下任務會掛起,重新調度
*/
LOS_Msleep(TELNET_ACCEPT_INTERVAL);//以休息的方式發起調度. 直接申請調度也未嘗不可吧 @note_thinking
} else {
return;
}
TelnetLock();
}
TelnetUnlock();
}
5. 遠方的客人到來,安排專人接待
鴻蒙目前只支援接待一位遠方的客人,g_telnetClientFd
是個全局變數,創建專門任務接待客人.
STATIC INT32 TelnetdAcceptClient(INT32 clientFd, const struct sockaddr_in *inTelnetAddr)
{
g_telnetClientFd = clientFd;
//創建一個執行緒處理客戶端的請求
if (pthread_create(&tmp, &useAttr, TelnetClientLoop, (VOID *)(UINTPTR)clientFd) != 0) {
PRINT_ERR("Failed to create client handle task\n");
g_telnetClientFd = -1;
goto ERROUT_UNLOCK;
}
}
6. 接待員做好接待工作
因接待工作很重要,這邊把所有程式碼貼出來,並加上了大量的注釋,目的只有一個,讓咱客人爽.
STATIC VOID *TelnetClientLoop(VOID *arg)
{
struct pollfd pollFd;
INT32 ret;
INT32 nRead;
UINT32 len;
UINT8 buf[TELNET_CLIENT_READ_BUF_SIZE];
UINT8 *cmdBuf = NULL;
INT32 clientFd = (INT32)(UINTPTR)arg;
(VOID)prctl(PR_SET_NAME, "TelnetClientLoop", 0, 0, 0);
TelnetLock();
if (TelnetClientPrepare(clientFd) != 0) {//做好準備工作
TelnetUnlock();
(VOID)close(clientFd);
return NULL;
}
TelnetUnlock();
while (1) {//死循環接受遠程輸入的數據
pollFd.fd = clientFd;
pollFd.events = POLLIN | POLLRDHUP;//監聽讀數據和掛起事件
pollFd.revents = 0;
/*
POLLIN 普通或優先順序帶數據可讀
POLLRDNORM 普通數據可讀
POLLRDBAND 優先順序帶數據可讀
POLLPRI 高優先順序數據可讀
POLLOUT 普通數據可寫
POLLWRNORM 普通數據可寫
POLLWRBAND 優先順序帶數據可寫
POLLERR 發生錯誤
POLLHUP 發生掛起
POLLNVAL 描述字不是一個打開的文件
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然後查詢每個fd對應的設備狀態,
如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷完所有fd後沒有發現就緒設備,則掛起當前進程,
直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。
這個過程經歷了多次無謂的遍歷。
poll還有一個特點是「水平觸發」,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。
poll與select的不同,通過一個pollfd數組向內核傳遞需要關注的事件,故沒有描述符個數的限制,
pollfd中的events欄位和revents分別用於標示關注的事件和發生的事件,故pollfd數組只需要被初始化一次
poll的實現機制與select類似,其對應內核中的sys_poll,只不過poll向內核傳遞pollfd數組,
然後對pollfd中的每個描述符進行poll,相比處理fdset來說,poll效率更高。poll返回後,
需要對pollfd中的每個元素檢查其revents值,來得指事件是否發生。
優點
1)poll() 不要求開發者計算最大文件描述符加一的大小。
2)poll() 在應付大數目的文件描述符的時候速度更快,相比於select。
3)它沒有最大連接數的限制,原因是它是基於鏈表來存儲的。
缺點
1)大量的fd的數組被整體複製於用戶態和內核地址空間之間,而不管這樣的複製是不是有意義。
2)與select一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符
*/
ret = poll(&pollFd, 1, TELNET_CLIENT_POLL_TIMEOUT);//等2秒鐘返回
if (ret < 0) {//失敗時,poll()返回-1
break;
/* ret < 0 各值
EBADF 一個或多個結構體中指定的文件描述符無效。
EFAULTfds 指針指向的地址超出進程的地址空間。
EINTR 請求的事件之前產生一個訊號,調用可以重新發起。
EINVALnfds 參數超出PLIMIT_NOFILE值。
ENOMEM 可用記憶體不足,無法完成請求
*/
}
if (ret == 0) {//如果在超時前沒有任何事件發生,poll()返回0
continue;
}
/* connection reset, maybe keepalive failed or reset by peer | 連接重置,可能keepalive失敗或被peer重置*/
if ((UINT16)pollFd.revents & (POLLERR | POLLHUP | POLLRDHUP)) {
break;
}
if ((UINT16)pollFd.revents & POLLIN) {//數據事件
nRead = read(clientFd, buf, sizeof(buf));//讀遠程終端過來的數據
if (nRead <= 0) {
/* telnet client shutdown */
break;
}
cmdBuf = ReadFilter(buf, (UINT32)nRead, &len);//對數據過濾
if (len > 0) {
(VOID)TelnetTx((CHAR *)cmdBuf, len);//對數據加工處理
}
}
}
TelnetLock();
TelnetClientClose();
(VOID)close(clientFd);
clientFd = -1;
g_telnetClientFd = -1;
TelnetUnlock();
return NULL;
}
最後結語
理解遠程登錄的實現建議結合 shell編輯篇
,shell執行篇
,控制台篇
三篇來理解,實際上它們是上中下三層.
百文說內核 | 抓住主脈絡
- 百文相當於摸出內核的肌肉和器官系統,讓人開始豐滿有立體感,因是直接從注釋源碼起步,在加註釋過程中,每每有心得處就整理,慢慢形成了以下文章。內容立足源碼,常以生活場景打比方儘可能多的將內核知識點置入某種場景,具有畫面感,容易理解記憶。說別人能聽得懂的話很重要! 百篇部落格絕不是百度教條式的在說一堆詰屈聱牙的概念,那沒什麼意思。更希望讓內核變得栩栩如生,倍感親切。
- 與程式碼需不斷
debug
一樣,文章內容會存在不少錯漏之處,請多包涵,但會反覆修正,持續更新,v**.xx
代表文章序號和修改的次數,精雕細琢,言簡意賅,力求打造精品內容。 - 百文在 < 鴻蒙研究站 | 開源中國 | 部落格園 | 51cto | csdn | 知乎 | 掘金 > 站點發布。
按功能模組:
百萬注源碼 | 處處扣細節
- 百萬漢字註解內核目的是要看清楚其毛細血管,細胞結構,等於在拿放大鏡看內核。內核並不神秘,帶著問題去源碼中找答案是很容易上癮的,你會發現很多文章對一些問題的解讀是錯誤的,或者說不深刻難以自圓其說,你會慢慢形成自己新的解讀,而新的解讀又會碰到新的問題,如此層層遞進,滾滾向前,拿著放大鏡根本不願意放手。
- < gitee | github | coding | codechina > 四大碼倉推送 | 同步官方源碼
原創不易,歡迎轉載,也請註明出處。若能點贊 | 分享則更佳,感謝支援,一點微光,足以照亮前方。