Windows 命令行:深入 Windows 控制台

歡迎來閱讀第三篇Windows 命令行系列文章。在這篇,我們開始深入Windows 控制檯和命令行,它是什麼,你可以用它可以做什麼……和它不能做什麼!

系列文章:

  1. 命令行產生的背景
  2. Windows 命令行的發展
  3. 深入Windows命令行(本篇)

在開始開發Windows NT操作系統的那時候,大概是1989年,那時候還沒有GUI(圖形化用戶界面),也沒有桌面操作系統,只有最原始的全屏的命令行界面,類似於MS-DOS的可視化界面越來越重要!Windows GUI 開始開發的時候是在開發團隊需要開發一個基於控制檯的應用的背景下誕生的!Windows 控制檯是第一個Windows NT的GUI應用,並且可以保證兼容運行繼續使用已有的Windows應用。

Windows 控制檯最初的代碼到現在(2018年)已經有30年的歷史……古老的東西,事實上,今天還有很多開發者在使用它!

控制檯程序能做什麼?

就像之前的文章說的,終端的工作其實很簡單:

  • 處理用戶輸入:
  • 可以支持的輸入設備包括鍵盤、鼠標、觸摸板、筆等等。
  • 轉換輸入的數據到中間字符的或者ANSI/VT編碼格式
  • 發送字符數據到已連接的應用程序或設備
  • 處理應用程序輸出:
  • 允許從已連接的應用程序輸出文本
  • 更新屏幕上面的顯示,基於應用程序接受顯示(比如顯示文本,移動光標,設置字體顏色等)
  • 系統協調處理:
  • 運行處理作業請求
  • 管理設備和資源
  • 支持調整窗口尺寸、最大化窗口、最小化窗口等
  • 中斷請求或當信道關閉或結束處理

但是,Windows 控制檯能做的事情有些不同:

深入Windows控制檯內部

Windows控制檯是一種傳統的Win32可執行文件,雖然它最初是用“C”編寫的,但隨著團隊現代化和模塊化控制檯的代碼庫,大部分代碼都已正在遷移到現代C++了。

對於那些關心此類事物的人:許多人都在詢問Windows是用C還是C++編寫的。答案是 - 儘管NT是基於對象的設計 - 像大多數操作系統一樣,Windows幾乎完全用C語言編寫!為什麼? C++在內存佔用和代碼執行開銷方面引入了開銷。即使在今天,使用C++編寫的代碼的其所隱藏的開銷也會令人大吃一驚,但早在1990年代後期,此時內存價格約為60$/MB(是的......每個MEGABYTE為60美元!)時,vtable等隱藏機制的內存開銷非常高。此外,虛方法間接調用和對象解引用的開銷可能導致當時的C++代碼存在非常顯著的性能和規模損耗。雖然你仍然需要當心,現代C++在現代計算機上的性能開銷並不是一個值得關注的問題,同時考慮到其安全性、可讀性和可維護性方面的優勢,這通常是一種可接受的折衷...這就是為什麼我們將Console的代碼穩步升級到現代C++這樣做的原因!

那麼,Windows 控制檯內部是什麼樣?

在 Windows 7 之前,Windows 控制檯實例託管於核心的客戶-服務器運行子系統(Client Server Runtime Subsystem,CSRSS)!然而,在 Windows 7 中,考慮到安全性和可靠性因素,控制檯從CSRSS 中剝離出來,組件了一個包含如下二進制文件的新家庭:

  • conhost.exe - 用戶模式的 Windows 控制檯 UX 和命令行管道
  • condrv.sys - 一個提供基礎通信結構的核心驅動,連接 conhost 和命令行 Shell/工具/應用之間的通信

控制檯當前的內部結構總體結構圖就像這樣:

Windows 命令行:深入 Windows 控制檯

控制檯的核心組件包含如下內容(自下而上):

  • ConDrv.sys - 核心模式驅動
  • 請求執行 API 調用控制檯實例的數據呈現
  • 從控制檯發送到命令行應用的文本
  • 為控制檯及其連接的命令行應用提供高性能通信通道
  • 在控制檯及附著於其上的命令行應用這間反覆傳遞 IO 控制 (IOCTL) 消息
  • 管理控制檯 IOCTL 消息
  • ConHost.exe - Win32 圖形界面(GUI)應用:
  • 管理控制檯容器在屏幕上的佈局、大小、位置等。
  • 顯示並處理設置界面等。
  • 調用 Windows 消息隊列,處理 Windows 消息並將用戶輸入轉換為鍵盤和鼠標事件,並將之存儲於輸入緩衝區。
  • API Server
    : 轉換 API 調用時從命令行應用收到的 IOCTL 消息,並將文本記錄從控制檯發給命令行應用。
  • API: 實現 Win32 控制檯 API,以及所有要求控制檯執行的操作背後的邏輯。
  • Input Buffer: 保存由用戶輸入產生的鍵盤和鼠標事件記錄
  • VT Parser: 如果啟動,則從文本中解析 VT 序列,根據找到的信息產生等效的 API 調 I用
  • Output Buffer: 保存控制檯呈現的文本。本質上是一個二維的 CHAR_INFO 結構數組,其每個元素都包含了字符數據及其屬性(緩存區之下的更多信息)
  • Other: 未包含在上層呈現,包含從註冊表或快捷文件中存儲/檢索基礎設置值。
  • ConHost Core - 控制檯的內部控制和管道
  • Console UX App Services - 控制檯的 UX 和 UI 層

Windows控制檯API

從上述的控制檯架構圖中可以看出,與NIX終端不同的是,控制檯發送/接收API調用和/或數據序列化為IO控制(IOCTL)消息,而不是序列化後的文本! 甚至從(主要是Linux)命令行應用程序接收的文本中所嵌入的ANSI/VT序列也被提取、解析並轉換為API調用!

這種差異揭示了*NIX和Windows之間關鍵的基本哲學差異:在*NIX中,“一切都是文件”,然而在Windows中,“一切都是對象”!

兩種方法都有利有弊,我們將概括之,但避免在這裡進行長篇大論。請記住,哲學中的這一關鍵差異是Windows和* NIX之間諸多差異的基礎!

在 *NIX系統中,一切都是文件

在60年代末和70年代初Unix被第一次實現的時候,其中一個核心原則就是任何東西都可以被抽象成文件流,一個關鍵目標是簡化對設備和外設的訪問處理:如果所有的設備都在系統中以文件系統的形式存在,那麼現存的代碼就可以不做修改地直接訪問這些設備。

這個原則影響深遠:你可以通過偽文件系統或虛擬文件系統來瀏覽和查詢大量的基於*NIX的系統和機器配置,它們僅僅是”表現得“像是“文件”或“文件夾”,實際可能是機器配置或硬件。

例如,在Linux中,你可以通過訪問 /proc/cpuinfo 虛擬文件節點來查看CPU的一些信息:

Windows 命令行:深入 Windows 控制檯

這個模型是如此簡單和一致,但它也存在一些額外開銷:從這些偽文件中提取或查詢特殊的文本信息並從執行命令中返回,經常需要一些工具的輔助,比如:sed,awk,perl,python等。這些工具經常被用來寫腳本和命令來解析文本內容、查找特殊模式、區域和值。這些腳本可以變得非常複雜,難以維護和碎片化。如果文本的結構、佈局或格式發生變更,那麼許多腳本也需要隨之更新。

在Windows中,任何事物都是對象

當Windows NT被設計和構建時,“對象”被視為軟件設計的未來:“面向對象”的語言比洞穴裡的兔子更快出現 - Simula和Smalltalk已經建立起來,而C ++正變得越來越流行。其他面向對象的語言,如Python,Eiffel,Objective-C,ObjectPascal / Delphi,Java,C#等許多其他語言都在快速發展緊隨其後。

不可避免的是,它成型於面向對象大好時期(大約1989年)中,Windows NT的設計理念是“一切都是對象”。事實上,NT內核最重要的部分之一是“對象管理器”!

Windows NT公開了一組豐富的Win32 API,可以調用這些API來從操作系統獲取和/或操作對象。開發人員使用Win32 API來收集和呈現* NIX偽文件和工具提供的類似信息,但是通過對象和結構。並且因為解析器,編譯器和分析器理解對象的結構,所以通常可以更早地捕獲許多編碼錯誤,從而幫助驗證程序員的意圖在語法和邏輯上是否正確。隨著時間的推移,這也可以減少系統破損,波動和“攪動”。

所以,回到我們關於Windows控制檯的中心討論:NT團隊決定構建一個“控制檯”,它在幾個關鍵領域區別於傳統的* NIX終端:

  1. 控制檯API:Windows Console可以通過豐富的Console API進行操作和控制,而不是依賴程序員生成“難以驗證”的ANSI / VT序列的能力。
  2. 公共服務:為避免每個命令行shell一次又一次地重新實現相同的服務(例如命令歷史記錄,命令別名),控制檯本身提供了一些這些服務,可通過Console API訪問

Windows控制檯的問題

雖然Console的API已經證明在Windows命令行工具和服務領域非常流行,但以API為中心的模型對命令行方案提出了一些挑戰:

只有Windows實現了Console API

許多Windows命令行工具和應用程序廣泛使用Console API。

問題呢?這些API僅適用於Windows。

因此,結合其他差異化因素(例如過程生命週期差異等),Windows命令行應用程序並不總是易於移植到* NIX,反之亦然。

因此,Windows生態系統開發了自己的,通常類似但通常不同的命令行工具和應用程序。這意味著用戶在使用Windows時必須學習一組命令行應用程序和工具,shell,腳本語言等,而在使用* NIX時則需要學習另一組。

這個問題沒有簡單的快速解決方案:Windows控制檯和命令行不能簡單地丟棄並被bash和iTerm2取代 - 有數以億計的應用程序和腳本依賴於Windows控制檯和Cmd / PowerShell shells。

像Cygwin這樣的第三方工具可以很好地將許多核心GNU工具和兼容性庫移植到Windows,但是它們無法運行未移植的,未經修改的Linux二進制文件。這非常重要,因為許多Ruby,Python,Node包和模塊依賴於或包裝Linux二進制文件,或者依賴於* NIX運轉狀態。

這些原因促使微軟通過在 Windows的子系統Linux(WSL)上本地運行真正的,未經修改的Linux二進制文件和工具來擴展Windows的兼容性。使用WSL的用戶現在可以在同一臺機器上並行下載和安裝一個或多個Linux發行版,並使用apt / zypper / npm / gem / etc.安裝和運行絕大多數Linux命令行工具以及他們喜歡的Windows應用程序和工具。

但是,還有一些控制檯提供的東西尚未被非Microsoft終端採用:具體來說,Windows控制檯提供命令歷史記錄和命令別名服務,從而無需每個命令行shell(特別是)重新實現相同的功能。

把 Windows 命令行遠程化是困難的

正如我們在 Command-Line Backgrounder 一文中所討論的那樣,終端最初與它們所連接的計算機是分開的。快進到今天,這種設計仍然存在:大多數現代終端和命令行應用程序/shell 等等是由進程或機器邊界分隔的。

在基於 *NIX 的平臺上,終端和命令行應用程序的分離並通過簡單的字符進行通信的概念導致 *NIX 命令行易於從遠程計算機/設備訪問和操作:只要終端和命令行應用程序可以通過某種類型的有序串行通信基礎架構(TTY/PTY 等)傳輸字符流,遠程操作 *NIX 機器的命令行是非常簡單的。

但是在 Windows 上,許多命令行應用程序依賴於調用 Console API,並假設它們與控制檯本身在同一臺機器上運行。這使得遠程操作 Windows 命令行 shell/工具等變得很困難:在遠程計算機上運行的命令行應用程序如何調用在用戶本地計算機的控制檯上的 API 呢?更糟糕的是,如果遠程命令行應用程序通過 Mac 或 Linux 機器上的終端訪問,它如何調用 Console API 呢?!

很抱歉開個玩笑,但我們將在以後的文章中更詳細地闡釋這個主題!

啟動控制檯或者不!

通常,在基於 *NIX 的系統上,當用戶想要啟動一個命令行工具時,他們首先會啟動一個終端。然後終端啟動一個默認的 shell ,或者可以配置為啟動一個特定的應用程序/工具。終端和命令行應用程序通過偽終端(PTY)交換字符流進行通信,直到一個或兩個字符終止。

然而,在 Windows 系統上,事情就不一樣了:Windows 用戶永遠不會啟動控制檯(conhost.exe)——然而他們會啟動像是 Cmd.exe,PowerShell.exe,wsl.exe 等等這樣的命令行 shell 和應用程序。Windows 系統將新啟動的應用程序連接到當前控制檯(如果是從命令行啟動的話),或者連接到新創建的控制檯實例。

# 現在要說的?

是的,在 Windows 系統中,用戶啟動命令行應用程序,而不是控制檯本身。

如果用戶從現有的命令行 shell 啟動命令行應用程序,Windows 通常會將新啟動的 .exe(可執行文件) 附加到當前控制檯。否則,Windows 會將一個新的控制檯實例與新推出的應用程序綁定在一起。

小白說:很多人說“命令行程序在控制檯運行”。這不是真的,而且導致很多關於控制檯和命令行應用程序如何工作的困惑!命令行應用程序和它們的控制檯都在各自獨立的 Win32 進程中運行。請通過指出“命令行工具/應用程序連接到控制檯運行”(或類似的)來幫助糾正這種誤解。謝謝!

聽起來不錯,對吧?嗯…不;這裡有一些問題:

1.控制檯和命令行應用程序通過經由驅動程序的 IOCTL 消息進行通信,而不是通過文本流進行通信

2.windows 要求 ConHost.exe 必須是連接到命令行應用程序的控制檯程序

3.Windows 控制了控制檯和命令行應用程序通信之間通信“管道”的創建

這些都是明顯的限制:如果你想為 Windows 創建一個替代控制檯的應用程序,該怎麼辦?你將如何發送鍵盤、鼠標、筆等等外設的信息?如果你無法訪問連接你新控制檯和命令行應用程序的通信“管道”,用戶將怎麼對命令行應用程序進行操作?

遺憾的是,這些情況並不好:有一些很棒的用於 Windows 的第三方控制檯(和服務器應用程序)(例如 ConEmu/Cmder, Console2/ConsoleZ, Hyper, Visual Studio Code, OpenSSH 等),他們必須通過離奇的跳轉才能像正常的控制檯一樣運行!

舉例來說,第三方控制檯必須在屏幕外啟動一個命令行應用程序,例如(-32000,-32000)。然後,他們必須向屏幕外控制檯發送擊鍵信息,然後收集屏幕外控制檯的文本內容並在自己的 UI 上重新繪製它們!

我知道,這很瘋狂,對吧? !這證明了這些應用程序創造者們的獨創性和決心,這些程序甚至還在有效的運行!

這顯然是我們急於補救的一種情況。請繼續關注這部分內容的更多信息——在這方面有一些好消息!

Windows 控制檯 & VT

如上所述,Windows 控制檯提供了大量 API。使用控制檯 API,命令行應用程序和工具可寫入文本,更改文本顏色,移動光標等。並且,由於控制檯 API 的存在,Windows 控制檯幾乎不需要支持 ANSI/VT 序列,這些序列在其他平臺上提供非常類似的功能。

實際上,在 Windows 10 之前,Windows 控制檯僅實現了對 ANSI/VT 序列的最低限度支持:

Windows 命令行:深入 Windows 控制檯

從2014年開始,微軟組建了一個新的 Windows 控制檯團隊,使得這一切都發生了變化。控制檯團隊的最高優先級事項之一是實現對 ANSI/VT 序列的全面支持,以便渲染在 Windows 子系統之Linux(WSL)和遠程 *NIX 機器上運行的 *NIX 應用程序的輸出。您可以在本系列的上一篇文章中閱讀更多關於這個故事的內容。

控制檯團隊迅速為 Windows 10 的控制檯添加了對 ANSI/VT 序列的全面支持,使用戶能夠使用和享用大量 Windows 和 Linux 命令行工具和應用程序。

該團隊繼續改進和完善每個操作系統發佈版本上的控制檯對 VT 的支持,並對您在我們的 GitHub 問題跟蹤器上提交的任何問題表示感謝。

處理Unicode

一個快速的Unicode回顧:

Unicode或ISO/IEC 10646是一個國際標準,定義了地球上幾乎每個書寫系統中所使用的每個字符/字形,以及當今使用的許多非腳本符號和字符大小的圖像(例如表情符號)。目前(2018年7月),Unicode 11定義了137439個字符,包含146個現代和歷史文字系統!

Unicode還定義了幾種字符編碼,包括UTF-8, UTF-16, 和UTF-32:

  • UTF-8: 前127個編碼點使用1字節(主要為了維持與ASCII的兼容性),其他字符可選附加長度1-4字節
  • UTF-16/UCS-2: 每個字符兩個字節。UCS-2 (被Windows內部使用)z支持對前65536編碼點(統稱為基本多語言平面-BMP)。UTF-16通過17個額外的字符平面擴展了UCS-2。
  • UTF-32: 每個字符4字節

由於UTF-8的高效的存儲要求以及在HTML頁面中的廣泛使用,它是目前最流行的編碼。

UTF-16/UCS-2都是常見的,儘管在已存儲文檔(例如網頁、代碼等)中其使用比例正在降低。UTF-32是很少使用的,因為它的效率低且存儲需要相當大的空間。

很好,所以我們有有效並且高效的方式來表示和存儲Unicode字符了!

所以?

哎呀,Windows控制檯及其API是在創建Unicode之前創建的!

Windows控制檯將文本(隨後在屏幕上繪製)存儲為每個單元需要2個字節的UCS-2字符。

命令行應用程序使用控制檯API將文本寫入到控制檯中。處理文本的控制檯API有兩種形式 - 帶有A後綴處理的單字節/字符串的函數,帶有W後綴處理雙字節(wchar)/字符串的函數:

例如,WriteConsoleOutputCharacter()函數編譯為ASCII項目的WriteConsoleOutputCharacterA(),或Unicode項目的WriteConsoleOutputCharacterW()。如果需要指定處理方式,代碼中可以直接調用... A或...W後綴的函數。

注意:每個W API至少支持UCS-2,因為這是在進行A/W拆分時就存在的事情,我們認為這樣做會很棒。但許多W API已更新為在同一渠道上也支持UTF-16

。並非所有W API都可以支持UTF-16,但所有W API至少可以支持UCS-2。

此外,控制檯不支持一些較新的Unicode功能,包括零寬度連接符(ZWJ),該符號被用於連接阿拉伯語和印度語中的其他單獨字符,並將表情符號字符組合成一個可視字形!

那麼如果你想在控制檯上輸出一個ninjacat表情符號或複雜的多字節中文/阿拉伯字符會怎樣呢? 糟糕的是,你做不到

Console API不僅不支持長度超過2字節/字形的Unicode字符(NinjaCat表情符號需要8個字節!),但Console內部的UCS-2緩衝區不能存儲該數據的額外字節,更糟糕的是 ,Console當前的基於GDI的渲染器甚至無法繪製字形,即使緩衝區可以存儲它!

可嘆! 這就是遺留代碼的樂趣。

但是,我也會希望你們到此打住 - 我們將在本系列的新一篇文章中回到這個主題。 敬請關注!

所以,我們在哪裡?

再一次,親愛的讀者,如果你讀過以上的所有內容,謝謝你,也祝賀你 —— 你現在比你的大多數朋友都更瞭解 Windows 控制檯,甚至可能比你想知道的還要多!祝你幸運!

在這篇文章中,我們涵蓋了很多內容:

Windows控制檯的主要構建模塊:

  • API Server —— 通過 IOCTL 消息向驅動程序發送或從驅動程序接收序列化的 API 調用和文本數據。
  • API——控制檯的功能函數。
  • Buffers —— 輸入緩衝用於存儲用戶輸入,輸出緩衝用於存儲輸出和顯示文本。
  • 輸入緩衝存儲用戶輸入,輸出緩衝存儲輸出和顯示文本。
  • VT Parser —— 將嵌入文本流的 ANSI/VT 序列轉換為 API 調用
  • Console UX —— 控制檯的用戶界面狀態、設置和功能
  • Other —— Misc 生命週期、安全性等。
  • Condrv.sys —— 控制檯通信驅動程序
  • ConHost.exe —— 控制檯用戶體驗、內部構件和管道:

控制檯做什麼?

  • 向連接的命令行應用程序發送用戶輸入
  • 接收並顯示連接的命令行應用程序輸出

控制檯與 *NIX 終端有什麼不同

  • *NIX:“一切都是文件/文本流”
  • Windows:“一切都是對象,可以通過 API 進行訪問”

控制檯存在的問題

  • 大部分都在 Windows 10 中得到了修復
  • 只有 ConHost.exe 可以附加到命令行應用程序
  • 第三方終端被迫創建屏幕外控制檯,並向它發送按鍵和屏幕信息,或從中接收按鍵和屏幕信息
  • 遠程操作 Windows 命令行應用程序和工具存在困難
  • 來自 Windows 的端口命令行 APP 的工作變得更多
  • 控制檯和命令行應用程序通過序列化 API 調用請求和文本組成的 IOCTL 消息進行通信
  • 只有 Windows 命令行應用程序能調用控制檯 API
  • 應用程序調用 Windows API 與控制檯交互
  • 對 IOCTL 的依賴打破了“字符交換”原則的終端設計
  • 使從非 Windows 機器操作遠程 Windows 命令行工具變得困難
  • 啟動 Windows 命令行應用程序是“不常用的”
  • Windows一直不識別ANSI/VT序列
  • 控制檯對 Unicode 的支持有限,目前正在努力處理存儲和展現現代 UTF-8 和需要零寬度連接符的字符

在本系列的後續文章中,我們將深入探討控制檯,並討論如何處理這些問題……和更多其他內容!

像往常一樣,請繼續關注我們。

本文由oschina作者參與翻譯,如有侵權,請聯繫刪除。


分享到:


相關文章: