Windows 命令行:深入 Windows 控制台
在这篇,我们开始深入Windows 控制台和命令行,它是什么,你可以用它可以做什么……和它不能做什么!在开始开发Windows NT操作系统的那时候,大概是1989年,那时候还没有GUI(图形化用户界面),也没有桌面操作系统,只有最原始的全屏的命令行界面,类似于MS-DOS的可视化界面越来越重要!
Windows GUI 开始开发的时候是在开发团队需要开发一个基于控制台的应用的背景下诞生的!Windows 控制台是第一个Windows NT的GUI应用,并且可以保证兼容运行继续使用已有的Windows应用。
Windows 控制台最初的代码到现在(2018年)已经有30年的历史……古老的东西,事实上,今天还有很多开发者在使用它!
控制台程序能做什么?
就像之前的文章说的,终端的工作其实很简单:
处理用户输入:
可以支持的输入设备包括键盘、鼠标、触摸板、笔等等。
转换输入的数据到中间字符的或者ANSI/VT编码格式
发送字符数据到已连接的应用程序或设备
处理应用程序输出:
允许从已连接的应用程序输出文本
更新屏幕上面的显示,基于应用程序接受显示(比如显示文本,移动光标,设置字体颜色等)
系统协调处理:
运行处理作业请求
管理设备和资源
支持调整窗口尺寸、最大化窗口、最小化窗口等
中断请求或当信道关闭或结束处理
但是,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/工具/应用之间的通信
控制台当前的内部结构总体结构图就像这样:
[!--empirenews.page--]
控制台的核心组件包含如下内容(自下而上):
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的一些信息:
[!--empirenews.page--]
这个模型是如此简单和一致,但它也存在一些额外开销:从这些伪文件中提取或查询特殊的文本信息并从执行命令中返回,经常需要一些工具的辅助,比如: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 控制了控制台和命令行应用程序通信之间通信“管道”的创建
[!--empirenews.page--]
这些都是明显的限制:如果你想为 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 序列的最低限度支持:
从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。
[!--empirenews.page--]
此外,控制台不支持一些较新的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 和需要零宽度连接符的字符
在本系列的后续文章中,我们将深入探讨控制台,并讨论如何处理这些问题……和更多其他内容!
像往常一样,请继续关注我们。