终端,控制台和外壳

一些概念

treminal , tty , console 是一开始都是硬件的概念。

一台电脑只有一个 console ,一般有电源开关等硬件操作的, 一台电脑可以有很多个 terminal 。 terminal 是负责 shell 的输入和输出。 console 是一个特殊的 terminal ,就是一个多了电源开关等硬件操作的 terminal 。 tty 是电传打印机。电传打印机是一种把键盘作为输入,纸带作为输出的硬件,是一种 terminal 。 一开始 terminal 就是指 tty 。 后来出现了使用显示器输出的 terminal 。使用显示器输出的 terminal 被称为 video terminal 简称 vt 。

旧时代的大型电脑为了能让多个用户可以同时使用,会提供多个物理终端。

软件意义上的终端出现,是为了让个人电脑的用户可以直接使用他的个人电脑来与大型计算机联系,而不必使用专门的物理终端。 现在的终端会被称为 emulator treminal 或 virtual terminal 。 因为现在已经没有物理意义上的终端了,都是由软件实现。 现在的 treminal , tty , console 都是指一种可以用来显示 shell 的软件, shell 可以是本地的也可以是远程的。

shell 是软件的概念。 shell 负责接收外部输入,调用各种程序或系统命令,然后输出结果。简单但不严谨的解释,负责人机交互的可以称为 shell ,负责显示 shell 的可以称为 terminal 。 shell 通常是指命令行解释器,但图形界面一样可以有 GUI shll ,例如 Windows 的 explorer.exe 。 shell 通常会被翻译成 外壳 或 壳层。 shell 的概念其实是相对于操作系统内核 (kernel) 而言的。

shell 还可以分为 interactive 和 non-interactive 直接输入的命令运行在 interactive shell 上, shell 脚本代码就运行在 non-interactive shell 中。

词汇表

linux 的 tty 子系统

在 linux 或其它 unix like 系统中, tty 就是指终端,不论是软件意义上的或硬件意义上的。

传说,第一个 Unix 终端是一个名字为 ASR33 的电传打字机,而电传打字机的英文单词为 Teletype 或Teletypewritter ,缩写为 tty 。之后终端设备都被称为 tty 设备。 又因为早期的 tty 都是通过串口和主机连接,所以串口设备也是用 tty 来表示的。

在不看源码的前提下,其实很难彻底理解 tty 子系统

物理终端

物理终端 <---> UART 驱动 <---> LDISC <---> tty 驱动 <---> shell

tty 子系统其实就是指内核中的 LDISC 和 tty 驱动。 这两部分在后续的 虚拟终端 和 伪终端 中始终并没有改动。

UART 是一个串口的通讯协议。 UART 是一个 串行 全双工 异步 的通讯协议。 UART 驱动的主要作用就是把字符串转换为 UART 数据包或把 UART 数据包转换成字符串。

LDISC 是 Line discipline 线路规程;行控制;行规程;线路规则;行规则。这个词汇似乎没有统一的中文翻译,所以还是用英文缩写 LDISC 来表示。 LDISC 的主要作用是解释终端中的各种控制字符或特殊字符, 例如 backspace , ctrl+c 这些。 LDISC 也可以不解释直接传输 raw 字符串。 可以在命令行里用 stty -a 查看 tty 的设置,会输出需要转义的字符。

事实上,只要驱动能正确地和 LDISC 通讯就可以了,只是早期的 物理终端 都使用 UART ,后期有物理终端使用其它的接口,例如 ttyUSB , ttySAC 这样的。 后续出现的 虚拟终端 和 伪终端 都是替换了 UART 驱动 , LDISC 和 tty 驱动 并没有大的改动。

虚拟终端

显示器 和 键盘 <---> 显示器驱动 和 键盘驱动  <---> emulator treminal <---> LDISC <---> tty 驱动 <---> shell

虚拟终端 和 物理终端 没有太多的区别,主要是 UART 驱动 被替换成 emulator treminal 。 这里的 emulator treminal 是由内核实现的。

这种由内核实现的终端模拟器并不灵活。 因为,既然是终端模拟器,模拟的就是一个硬件上的终端。 在内核态的终端模拟器如果要添加对新出现的终端的支持,就只能修改内核源码或添加额外的内核模块。 (二十世纪八九十年代可没有 eBPF )

新的终端类型还在出现,只是现在的新终端更多是指新的终端显示规范(例如支持 utf-8 字符集,支持更大的色域这类),而不是指物理意义上的新终端设备。

可以使用 toe -a 来查看支持的终端类型。 可以使用 infocmp 比较两种终端类型的差异,例如这样 infocmp xterm xterm-vt52

虚拟终端 一定是运行在本地的。

虚拟终端 有时又会被成为虚拟控制台(virtual console), 又或者被称为控制台(console), 又或者被称为系统控制台(system console)。

在虚拟机或物理机启动的 linux 系统,系统启动完后,出现的就是 虚拟终端 的界面。 对于绝大多数 linux 发行版而言,系统启动后会默认启动 6 个虚拟终端。 可以使用 Ctrl+Alt+Fx 来切换虚拟终端。 例如 Ctrl+Alt+F2 能切换到 tty2 , Ctrl+Alt+F2 能切换到 tty3 。 通常 tty7 是图形桌面环境,同样可以使用 Ctrl+Alt+Fx 来切换 (前提是系统有正确地安装图形桌面环境)

打开这个文件 /etc/systemd/logind.conf , 修改这个值 NAutoVTs , 就能修改系统启动时的虚拟控制台数量。

现在的控制台其实就是指一个拥有更多权限的终端。 只有物理终端或虚拟终端才有可能成为控制台。 控制台其实是指当前活跃的虚拟终端。 控制台能接收来自内核的日志信息和告警信息。

伪终端

emulator treminal <---> pty master <---> LDISC <---> pty slave <---> shell

这里的 pty master 和 pty slave 是由 pty 驱动实现的。 pty master 和 pty slave 有时也被称为 pty pair。 但这里的 emulator treminal 是运行在用户态的。

伪终端大概有三类使用场景

  1. 图形界面里的终端模拟软件,例如 xterm 和 gnome-terminal
  2. 远程 shell ,例如 通过 telnet 或 ssh 登录服务器
  3. 终端复用软件,例如 screen 和 tmux

伪终端大概又分成两种

在伪终端中的 pty slave 就是一个普通的 tty 驱动,只是换了一个名称而已。

图形界面 和 tty 子系统有密切的联系。 进程管理 和 tty 子系统有密切的关系。 这两个是比较大的问题,基本可以再写一篇文章来描述。

使用 who 命令找到当前系统登录的用户以及其所在的终端

/dev 目录下的 tty 文件

tty 驱动

pty master

特殊的

在驱动程序中,可以通过 register_console() 函数注册将自身注册成为console 。 如果有多个设备都将自己注册为 console 的时候, 那么默认的 console 就是最后一个注册的设备。 但如果启动参数中指定了【 console=设备名 】的话, 那这个参数中的设备才是默认的 console 。 不同的发行版修改启动参数的方式似乎是不一样的。

查看 tty0 是指向哪个 tty 的

cat /sys/devices/virtual/tty/tty0/active

查看活跃的 console

cat /sys/devices/virtual/tty/console/active

查看内核参数

sysctl -a

telnet 和 ssh 远程连接的大致过程

参考

各种 unix like 的外壳

全称 简称 备注
thompson shell sh 第一个 unix like shell 。1971年至1975年随 Unix 第一版至第六版发布
borune shell sh 1978年随Version 7 Unix首次发布
borune again shell bash 在1987年由布莱恩·福克斯(Brian Fox)为了GNU计划而编写,是当前最常用的 shell
almquist shell ash 派生于 borune shell ,最初作为 bsd 的 shell ,目前已不再被广泛使用
debian almquist shell dash 派生于 almquist shell ,是 debian 的 shell
c shell csh 语法类似于C语言,c shell 是第一个实现了 job controller 的 shell , c shell 目前已不再被广泛使用
tenex c shell tcsh csh 的增强版, FreeBSD 中的默认 shell
korn shell ksh AIX 中的默认 shell ,兼容 borune shell ,同时加入了一些 c shell 的特性
zsh zsh zsh 对 borune shell 做出了大量改进,同时加入了 bash , ksh 及 tcsh 的某些功能。 zsh 现在是 mac 的默认 shell
friendly interactive shell fish fish 的语法既不派生于 borune shell 也不派生于 c shell ,故被分类为一种“外来” shell 。

各种 shell 的发展脉络

+------------------------------------------------------------------------------------------------------------------+
|                                                                                                                  |
|                                                                                                                  |
|                                                                                                                  |
|                          Thompson shell                                                                          |
|                 +----------+     +--------------------------+                                                    |
|                 |                                           |                                                    |
|                 |                                           |                                                    |
|                 v                                           v                                                    |
|                c shell                                  Bourne shell                                             |
|                  +                                           +                                                   |
|                  |                                           |                                                   |
|            +-----+-----------------+                         | +------------------>                              |
|            |                       |                         |                    |                              |
|            v                       v                         v                    v                              |
|     tenex c shell             korn shell           Bourne-Again shell       almquist shell                       |
|         +                          +                        +                     +                              |
|         |                          |                        |                     |                              |
|         +--------------------------v--------+---------------+                     |                              |
|                                             |                                     v                              |
|                                             |                              debian almquist shell                 |
|                                             v                                                                    |
|                                            zsh                                                       fish        |
|                                                                                                                  |
+------------------------------------------------------------------------------------------------------------------+

sh 通常是指遵循 POSIX 标准的 shell 。 bash 有 3 种方式使其遵循 POSIX 标准 https://www.gnu.org/software/bash/manual/html_node/Bash-POSIX-Mode.html

通常情况下 shell 脚本以这句开头 #!/bin/sh ,就是表示这份脚本遵循 POSIX 标准。 如果想脚本足够的通用,最好不要用 bash 的语法。

除了 POSIX 还, shell 还有两份标准, IEEE 1003.1 和 ISO/IEC 9945 。

现在绝大部分 unix like 系统中, /bin/sh 和 /usr/bin/sh 一般都是链接文件,指向真正的默认 shell 。 现在绝大部分 shell 都兼容 POSIX 标准,但同时又会有一些自己的拓展。

新版本的 powershell 也能运行在 linux 上。

查看系统可用的 Shell

cat /etc/shells

查看当前 shell

echo $SHELL

bash

在简体中文互联网中,关于 bash 的启动过程有非常多的文章。 笔者看得有一点混乱,于是还是看了 bash 的文档再结合自己的实践,重新总结一次。

bash 的文档

bash 的运行模式

从文档中看 bash 有四种运行模式(忽略 sh 的兼容)

interactive non-interactive
login login interactive login non-interactive
no-login no-login interactive no-login non-interactive

直接启动的 bash 会以 no-login interactive shell 运行。

bash
# 这时会进入一个新的 shell
# 这个旧的 shell 就是新的 shell 的父进程

加上 --login 或 -l 参数就能以 login interactive shell 运行。

bash --login
bash -l

如果加上了脚本路径或命令,那么默认会以 no-login non-interactive shell 运行。

bash -c "echo hello"
bash ./test.sh

如果 shell 脚本里没有声明 #! 或声明时没有带参数,那么也是以 no-login non-interactive 运行的。 但如果声明相应的参数,那么也可以以对应的模式运行。

#!/bin/bash # 没有声明或不带参数 以 no-login non-interactive 运行
#!/bin/bash -i # 以 no-login interactive 运行
#!/bin/bash -l # 以 login non-interactive 运行
#!/bin/bash -li # 以 login interactive 运行

login 或 no-login 似乎都没有什么权限的限制。 在 no-login shell 中可以启动 login shell , 在 login shell 中也可以启动 no-login shell 。

可以用这样的命令来判断 shell 是不是 login shell

shopt login_shell
# login shell 会输出 login_shell     on
# no-login shell 会输出 login_shell     off

可以用这样的命令来判断 shell 是不是 interactive shell

echo $-
echo $PS1
# echo $- 如果输出的结果中包含 i ,则是 interactive shell ,否则就是 non-interactive shell
# echo $PS1 如果输出的结果不为空,则是 interactive shell ,否则就是 non-interactive shell

可以用这样的命令来观察 bash 的运行模式和各个运行模式下加载的文件,注意修改 --login 和 -i 参数

strace -f -e open,execve bash --login -i -c "shopt login_shell;echo \$-" 2>&1 | grep -E "(profile|bashrc|login_shell|^h)"

bash 的启动脚本

从文档中看 bash 在启动时会执行两种脚本

除了 startup file 和 initialization file 之外,还有一个 ~/.bash_logout 脚本,用于 login shell 退出时执行的。

startup file 和 initialization file 通常会用于设置一些环境变量或命令的别名或一些初始化用的脚本。 命令别名的例子 alias rm='rm -i' 。

在文档中还提及到了 ~/.bash_history 和 ~/.inputrc 。 一个是记录命令行的历史记录, 一个是用于 readline 的初始化。 如果 ~/.inputrc 不存在,则会尝试读取 /etc/.inputrc 。

文档里提及到的文件基本就这几个了

这几个文件在文档里没有提及,但却经常出现在各类文章中

这几个文件本质上都是通过 startup file 或 initialization file 执行的。 在约定俗成的规则中, /etc/.bashrc 由 ~/.bashrc 调用, /etc/profile.d/*.sh 和 /etc/profile.d/sh.local 由 /etc/profile 调用。 具体的调用过程看一下 startup file 和 initialization file 就知道了。 不同的发行版执行的过程可能会不一样,所以最保险的方式还是自己看一下 startup file 和 initialization file 。 在互联网的一些文章里,会把 /etc/profile.d/*.sh 的脚本描述成开机启动的脚本或终端启动时的脚本或用户登录时的脚本, 从效果上看似乎也是正确的。 还有一个需要注意的地方是, startup file 和 initialization file 其实是可以相互调用的, 例如 在 ~/.bash_profile 里调用 ~/.bashrc , 在 /etc/bashrc 里调用 /etc/profile.d/*.sh 。

PS . 其它 shell 的 initialization file 也是以 rc 结尾的

需要特别注意的是 sh 的启动过程和 bash 是不一样的。笔者平时工作时用不到 sh ,就不探究这个了。

Windows 的外壳和脚本

Windows 的外壳和终端

一开始的 dos 和 9x 的系统里, COMMAND.COM 既是也是 shell 和 终端。 这是一个单一的程序。 shell 和 终端 混合在一起实现的。 并没有象 unix like 的系统里分开实现。

和 COMMAND.COM 一样 , cmd.exe 和 早期的 PowerShell 既是也是 shell 和 终端。 笔者留意到,在中文互联网里,关于 cmd 到底是 shell 还是终端,有非常多的讨论。

cmd.exe 一直没更新。 PowerShell 从 PowerShell core 开始,就是一个纯粹的 shell ,里面没有包含终端的实现代码。 旧版的 PowerShell 一直存在并没有被替换掉。 所以,现在的 windows 里其实是有三种 shell 的, cmd.exe , powershell.exe , pwsh.exe 。

windows terminal 是微软在 windows10 之后推出的终端, 是一个纯粹的终端,没有包含 shell ,可以用来显示各种 shell 。 笔者认为现代化的 Windows 开发应该使用 windows terminal 和 PowerShell (7.0及之后的版本)。 bat , JScript , VBScript , Windows PowerShell 这些都是过时的技术了。

这一些系列的文章详细地描述了 Windows 外壳和终端的发展过程

脚本语言

当前的 windows 中一共有四种预装的脚本语言

bat

bat 在 dos 时代就已经存在,是最古老的脚本语言。

在 dos 或 9x 的系统中, bat 的执行主体是 COMMAND.COM 。

在 nt 系统中, bat 的执行主体是 cmd.exe 。

bat 的脚本文件有两种后缀名 bat 和 cmd 。 在 nt 系统中无论哪种后缀名都是用 cmd.exe 执行的。 在 dos 或 9x 的系统中, bat 的后缀会通过 COMMAND.COM 执行,但 cmd 的后缀会无法执行。

在当前的 windows 系统中,两种后缀名是完全没有区别的。

自从 PowerShell 出现后,微软就不更新 bat 了,一直鼓励用户改用 PowerShell 。

JScript 和 VBScript

JScript 和 VBScript 是在 1996 年的 Professional Developers Conference (PDC) 发布。 一开始是应用在 IE3 上的脚本语言,对标的是网景的 JavaScript 。 JScript 能兼容当时的 JavaScript , Jscript 出现的初衷是为了兼容已经存在的使用 JavaScript 的网站。

JScript 和 VBScript 需要运行在宿主内, Windows 提供了几种宿主环境

自从 PowerShell 出现后,微软就不建议用户使用 JScript 和 VBScript 作为系统脚本, 一直鼓励用户改用 PowerShell 。

PowerShell

PowerShell 在 2006 年首次出现在 vista 中,也可以安装在 xp sp2 和 xp sp3 中。 PowerShell 是微软为了替换 bat 和 JScript 和 VBScript 而推出的。

PowerShell 1.0 - 5.1 是基于 .NET Framework 的,所以只能运行在 windows 上。

PowerShell 6.0 - 6.1 是基于 .NET Core 的,所以可以跨平台。这时的 PowerShell 还改名成 PowerShell Core ,能和 PowerShell 同时安装在一个系统里。

PowerShell 7.0 之后, PowerShell 再一次改名,基于 .NET Core 3.1 (后来 .NET Core 直接改名成 .NET) 的 PowerShell 就叫做 PowerShell , PowerShell 5.1 及之前的版本叫做 Windows PowerShell 。

真不愧是微软改名部

PowerShell 7.0 之后,除了改名, 还有把可执行文件的文件名重命名为 pwsh 。 第一个默认参数修改为 -File ,可以象这样执行脚本 pwsh script.ps1 。 可以加上 -i 进入交互模式。 这几项的修改,使得 PowerShell 7.0 的行为上更接近 unix like 里的 shell 。 从 PowerShell 7.0 开始, PowerShell 就可以作为 unix like 系统里的默认 shell 。

从微软的文档来看, PowerShell 会按照微软的软件生命周期策略更新下去。 而 Windows PowerShell 则会象 cmd.exe 一样,会保留在系统里但不会有更新。

git for windows 中 的 bash

git for windows 中 的 bash 主要分成三部分

多数情况下

git for windows 安装完成后,加入到环境变量 path 的是这个目录

git for windows 的安装根目录/cmd

在命令行里直接运行 bash 其实是运行 wsl2 里的 bash

为了能直接在命令行里使用 git for windows 中 的 bash ,可以尝试这样做, 在这个目录 git for windows 的安装根目录/cmd 下新建一个名为 gitbash.bat 的文件,并写入以下内容, 最后就可以在命令行里用 gitbash 命令来运行 git for windows 中 的 bash

@echo off
%~dp0..\bin\bash.exe -l %*

可以用这样的命令来查看帮助 git help git-bash 会在默认浏览器打开一个帮助页面

git-bash.exe 也是只运行 mintty.exe 。 mintty.exe 可以手工加上命令行参数,达到和 git-bash.exe 一样的效果。 git-bash 的参数其实可以直接用在 mintty.exe 里

参考

wsl 中的 bash

各种文件后缀

后缀 备注
.vbs VBScript
.js JScript
.vbe VBScript Encoded , 已编码的 VBScript 脚本文件
.jse JScript Encoded , 已编码的 JScript 脚本文件
.ws Windows Scripting
.wsc Windows Scripting Component
.sct Windows Scripting Component
.wsf Windows Scripting File
.wsh Windows Scripting Host
.hta HTML Application
.cmd Command Prompt 命令提示符
.bat batch 批处理文件
.ps1 Windows PowerShell 脚本
.pac Proxy Auto-Configuration
.asp Active Server Page
.aspx Active Server Page Extended
.srf server response file
.htm html
.xht xhtml
.shtm 包含服务器端指令的 HTML 文件
.shtml 包含服务器端指令的 HTML 文件
.stm 包含服务器端指令的 HTML 文件
.chm Compiled HTML Help 编译的HTML帮助文件
.cpl Control PaneL extension 控制面板扩展程序
.msc MicoSoft management Console 微软管理控制台
.exe executable 可执行程序

在 dos 里,文件的后缀名最多是 3 个字母, 在 9x 和 nt 的系统里延续了这个设定(其实早就没有长度限制了) 。 所以会出现一些和 unix like 不一样的缩写,例如

windows 中的 telnet

参考

windows console 的文档 https://docs.microsoft.com/zh-cn/windows/console/

描述 windows 控制台 的发展历程 https://docs.microsoft.com/zh-cn/windows/console/ecosystem-roadmap#major-historical-milestones

windows terminal 的文档 https://docs.microsoft.com/zh-CN/windows/terminal/

Microsoft Windows 脚本技术 https://www.jb51.net/shouce/script56/

JScript 和 VBScript 的文档 https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/scripting-articles/

JScript 和 VBScript 的起源 https://webdevelopmenthistory.com/1996-microsoft-activates-the-internet-with-activex-jscript/

微软的文章 这里描述了 cmd 和 bat 的区别 https://learn.microsoft.com/en-us/previous-versions//cc723564(v=technet.10)?redirectedfrom=MSDN