欢迎来阅读第三篇Windows 命令行系列文章。在这篇,我们开始深入Windows 控制台和命令行,它是什么,你可以用它可以做什么……和它不能做什么!
深入Windows命令行(本篇)
在开始开发Windows NT操作系统的那时候,大概是1989年,那时候还没有GUI(图形化用户界面),也没有桌面操作系统,只有最原始的全屏的命令行界面,类似于MS-DOS的可视化界面越来越重要!Windows GUI 开始开发的时候是在开发团队需要开发一个基于控制台的应用的背景下诞生的!Windows 控制台是第一个Windows NT的GUI应用,并且可以保证兼容运行继续使用已有的Windows应用。
Windows 控制台最初的代码到现在(2018年)已经有30年的历史……古老的东西,事实上,今天还有很多开发者在使用它!
Windows控制台是一种传统的Win32可执行文件,虽然它最初是用“C”编写的,但随着团队现代化和模块化控制台的代码库,大部分代码都已正在迁移到现代C++了。
对于那些关心此类事物的人:许多人都在询问Windows是用C还是C++编写的。答案是 - 尽管NT是基于对象的设计 - 像大多数操作系统一样,Windows几乎完全用C语言编写!为什么? C++在内存占用和代码执行开销方面引入了开销。即使在今天,使用C++编写的代码的其所隐藏的开销也会令人大吃一惊,但早在1990年代后期,此时内存价格约为60$/MB(是的......每个MEGABYTE为60美元!)时,vtable等隐藏机制的内存开销非常高。此外,虚方法间接调用和对象解引用的开销可能导致当时的C++代码存在非常显着的性能和规模损耗。虽然你仍然需要当心,现代C++在现代计算机上的性能开销并不是一个值得关注的问题,同时考虑到其安全性、可读性和可维护性方面的优势,这通常是一种可接受的折衷...这就是为什么我们将Console的代码稳步升级到现代C++这样做的原因!
在 Windows 7 之前,Windows 控制台实例托管于核心的客户-服务器运行子系统(Client Server Runtime Subsystem,CSRSS)!然而,在 Windows 7 中,考虑到安全性和可靠性因素,控制台从CSRSS 中剥离出来,组件了一个包含如下二进制文件的新家庭:
conhost.exe - 用户模式的 Windows 控制台 UX 和命令行管道
condrv.sys - 一个提供基础通信结构的核心驱动,连接 conhost 和命令行 Shell/工具/应用之间的通信
控制台当前的内部结构总体结构图就像这样:
控制台的核心组件包含如下内容(自下而上):
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 层
从上述的控制台架构图中可以看出,与NIX终端不同的是,控制台发送/接收API调用和/或数据序列化为IO控制(IOCTL)消息,而不是序列化后的文本! 甚至从(主要是Linux)命令行应用程序接收的文本中所嵌入的ANSI/VT序列也被提取、解析并转换为API调用!
这种差异揭示了*NIX和Windows之间关键的基本哲学差异:在*NIX中,“一切都是文件”,然而在Windows中,“一切都是对象”!
两种方法都有利有弊,我们将概括之,但避免在这里进行长篇大论。请记住,哲学中的这一关键差异是Windows和* NIX之间诸多差异的基础!
在60年代末和70年代初Unix被第一次实现的时候,其中一个核心原则就是任何东西都可以被抽象成文件流,一个关键目标是简化对设备和外设的访问处理:如果所有的设备都在系统中以文件系统的形式存在,那么现存的代码就可以不做修改地直接访问这些设备。
这个原则影响深远:你可以通过伪文件系统或虚拟文件系统来浏览和查询大量的基于*NIX的系统和机器配置,它们仅仅是”表现得“像是“文件”或“文件夹”,实际可能是机器配置或硬件。
例如,在Linux中,你可以通过访问 /proc/cpuinfo 虚拟文件节点来查看CPU的一些信息:
这个模型是如此简单和一致,但它也存在一些额外开销:从这些伪文件中提取或查询特殊的文本信息并从执行命令中返回,经常需要一些工具的辅助,比如: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终端:
控制台API:Windows Console可以通过丰富的Console API进行操作和控制,而不是依赖程序员生成“难以验证”的ANSI / VT序列的能力。
公共服务:为避免每个命令行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(特别是)重新实现相同的功能。
正如我们在 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 进程中运行。请通过指出“命令行工具/应用程序连接到控制台运行”(或类似的)来帮助纠正这种误解。谢谢!
评论删除后,数据将无法恢复
评论(22)
引用来自“梦里江南烟雨后”的评论
谁说c++就一定要用虚函数呢?同类和模板、自由函数就不能做程序了么?引入开销是啥意思