扫盲 Linux&UNIX 命令行——从“电传打字机”聊到“shell 脚本编程”

本文目标读者

虽然本文的标题号称是【扫盲】,但俺相信:即使是一些 POSIX 系统的命令行【老手】,对本文中介绍的某些概念,可能也会有【欠缺】。
  因此,这篇教程既适合于命令行的新手,也值得某些【老手】看一看。

由于本文介绍的是 POSIX 系统中【通用的】概念与知识。因此,包括 Linux、BSD 家族、macOS 等各种系统的用户,应该都能从中受益。
  (注:POSIX 是某种操作系统的标准/规范。各种 Linux 发行版以及所有的 UNIX 变种,包括 macOS,都属于“POSIX 系统”)

如果你是这方面的【菜鸟】,并且想要掌握这个领域。【不要】企图只看一遍就完全理解本文的内容(可能需要看好几遍)。俺的建议是:要一边看,一边拿命令行的环境【实践】一下。

一切都从【电传打字机】开始说起

(说完了“引子”与“目标读者”,开始切入正题)
  可能有些读者会纳闷——“聊命令行的基本概念”,为啥要扯到“电传打字机”?是不是扯得太远了?
  俺来解释一下:
  IT 行业的很多基本概念都来自于【历史遗迹】。有时候你觉得某些东西很奇怪(并纳闷“为啥会设计成这样”);而当你搞清楚历史的演变过程之后,自然就明白其中的原因。

在那遥远的【电报时代】

在计算机诞生之前(二战前),【电报】属于高科技的玩意儿——它能够瞬间把信息传送到另一个城市(甚至传送到大洋彼岸)。
  当年的电报线路,是以【字符】为单位发送信息。在线路两端使用【电传打字机】,就可以自动地把对方发过来的字符打印出来。


(上世纪40年代的电传打字机——用于电报网)

“回车/换行”的来历

稍微懂点 IT 的同学,应该都听说过“回车/换行”,洋文分别称之为“carriage return”&“line feed”。在编程领域,这两个字符简称为 \r\n
  为啥会有这么两个玩意儿捏?
  因为在电传打字机时代,当打印完一行之后,需要用一个控制命令把“打印头”复位(移到打印纸的左边),然后再用另一个控制命令把“打印头”往下移动一行。很自然地,这俩动作就对应了两个控制字符(CR & LF),也就是所谓的“回车 & 换行”。

其它控制字符

如果你去留意一下 ASCII 字符表的开头部分,前面那32个字符都是控制字符,很多都源于遥远的【电报时代】。
  在本文后续的介绍中,还会再聊到这些“控制字符”。

终端(terminal/TTY)

历史演变

“终端”一词,洋文称之为“terminal”。有时候又被称作 TTY,而 TTY 这个简写就来自刚才介绍的【电传打字机】(teletype printer)。
  因为早期的大型机,其“终端”就是【电传打字机】。那时候的终端,也称作【硬件终端】。

为啥会有“终端”这个概念捏?你依然需要了解历史的变迁。
  最早期的计算机(大型机)是【单任务】滴——也就是说,每次只能干一件事情。
  到了60年代,出现了一个【革命性】的飞跃——发明了【多任务】系统,当时叫做“time-sharing”(分时系统)。有了“分时系统”,就可以让多个人同时使用一台大型机。而为了让多个人同时操作这台大型机,就引入了【终端】的概念。每一台大型机安装多个终端,每个操作员都在各自的终端上进行操作,互不干扰。

(跑题)“约翰·麦卡锡”其人

聊到这里,稍微跑题一下:
  最早的“分时系统”由 IT 超级大牛“约翰·麦卡锡”(John McCarthy)设计。此人不仅仅是“分时系统它爹”,还是“Lisp 语言它爹”,另外还参与设计了编程语言“ALGOL 60”。而这个“ALGOL 60”编程语言虽然知道的人不多,但该语言深刻影响了后续的 Ada、BCPL、C、Pascal…
  为了让你体会这只大牛到底有多牛。俺引用另一个牛人保罗·格雷汉姆(《黑客与画家》作者)的观点——他认为在所有编程语言中, Lisp 与 C 是两座无法超越的高峰。而“约翰·麦卡锡”亲自发明了 Lisp 语言,然后又深刻地影响了 C 语言。
  另外,麦卡锡这只大牛还参与创立了“MIT 人工智能实验室”与“斯坦福人工智能实验室”。前者涌现出一大批早期的黑客,其中包括大名鼎鼎的 Richard Stallman(此人开创了:自由软件运动、GNU 社区、GCC、GDB、GNU Emacs …)。


(超级大牛约翰·麦卡锡)

【远程】终端

跑题结束,言归正传。
  “终端”的好处不光是“多任务”,而且还可以让用户在【远程】进行操作。这种情况下,“终端”通过 modem(调制解调器)与“主机”相连。这种玩法很类似于——互联网普及初期的拨号上网。示意图如下:


(通过 modem 实现的【远程】终端)

最早的“终端”,本质上就是“电传打字机”——以“打字机”作为输入;以“打印纸”作为输出。这类终端,比较经典的是如下这款:


(Teletype Model 33 ASR)

到了上世纪70年初,终于有了带【屏幕】的远程终端。DEC 公司的 VT05 是第一款基于 CRT 显示器的远程终端。


(VT05 终端)

内部结构示意图

下面这张是大型机时代,“终端”与“进程”通讯的示意图。
  图中的 UART 是洋文“Universal Asynchronous Receiver and Transmitter”的缩写(相关维基百科链接在“这里”)。LDISC 是洋文“line discipline”的简写(相关维基百科链接在“这里”)。
  通俗地说,UART 用来处理物理线路的字符传输(比如:“错误校验”、“流控”、等);LDISC 用来撮合底层的“硬件驱动”与上层的“系统调用”,并完成某些“控制字符”的处理与翻译。


(TTY 示意图1:使用【硬件终端】的大型机内部结构图)

如今的含义

如今,“终端”一词的含义已经扩大了——用来指:基于【文本】的输入输出机制。
  在本文后续的章节中, terminal 与 TTY 这两个术语基本上是同义词。

终端的3种【缓冲模式】——字符模式、行模式、屏模式

字符模式(character mode)

又要说回到【电传打字机】。
  在本文开头,已经聊过这个玩意儿,并且提到——它是基于【字符】传输滴。也就是说,操作员每次在“电传打字机”上按键,对应的字符会立即通过线路发送给对方。这就是最传统的【字符模式】
  通俗地说,“字符模式”也就是【无缓冲】的模式。

行模式(line mode)

不客气地说,“字符模式”是非常傻逼滴!因为如果你不小心按错键,这个错误也会立即发送出去。
  比如说,你在输入一串很长的命令,结果输到半当中,敲错一个按键,整个命令就废了——要重新再输入一遍。
  所以,当早期的程序员对“字符模式”实在忍无可忍之后,终于发明了【行模式】。
  【行模式】也叫做“行缓冲”。也就是说,终端会把你当前输入的这行先缓冲在本地。只有当你最终按了【回车键】,才会把这一整行发送出去。如果你不小心敲错了一个字符,可以赶紧用“退格键”删掉重输这个字符。
  因此,这种模式称之为【行缓冲】。

顺便说一下:
  早期的标准键盘,【没有】方向键(“上下左右”这4个键)。不信的话,可以去看本文前面贴的那张“Teletype Model 33 ASR”的照片。
  因为无论是“字符模式”还是“行模式”,都没这个需求。

屏模式(screen mode/block mode)

“行模式”进一步的发展就是【屏模式】。这个玩意儿也叫“全屏缓冲”,顾名思义,终端会缓冲当前屏幕的内容。
  在这种模式下,用户可以利用方向键,操纵光标(cursor)在屏幕上四处游走。
  开发这种类型的软件,比较复杂——程序员至少需要做如下工作:
\1. 保存整个屏幕的状态
\2. 根据键盘输入,操纵光标(cursor)移动
\3. 控制屏幕的哪些区域是光标可达,哪些是不可达;
\4. 对于光标可达的部分,控制哪些是“可编辑”,哪些是“只读”;
\5. 根据“光标移动”以及某些“特定的按键”(比如“翻页键”),重新绘制屏幕

  后来,为了简化”屏模式“的编程,专门搞了一个叫做 curses 的编程库。如今的“ncurses 库”就是从 curses 衍生出来滴(前面加了一个 n 表示 new)。


(“重编译 Linux 内核”的配置界面,基于 ncurses 实现)

前面说了——早期的键盘【没】方向键。有了这个【屏模式】之后,键盘上才开始增加了“方向键”(所以“方向键”位于键盘的扩展区)

小结

上述这三种模式,第1种基本淘汰(仅限于极少数场景);第3种用得也不多。与本文关系比较密切的,其实是【第2种】——行模式。
  为了加深你的印象,用 cat 命令来举例(注:这个命令其实与“猫”【无关】,而是 concatenate 的简写)
  大部分情况下,都是用它来显示某个文件的内容,比如说:cat 文件名 。但如果你运行 cat【没】加任何参数,那么它就会尝试读取你在终端的输入,然后把读到的文本再原样输出到终端。


(动画:演示“行模式”的效果)

在上述动画中,你的输入并【没有】直接传递给 cat 进程。要一直等到你按下【回车键】,cat 进程才收到你的输入,并立即打印了输出。

终端的【回显】

“回显”是啥?

在刚才那个 gif 动画中,当俺逐个输入 test 的每个字母,这些字母也会逐个显示在屏幕上。这种做法叫做【回显】。

◇“回显”的打开与关闭(启用/禁用)

虽然“回显”很人性化,但某些特殊的场合是【不想】“回显”滴,比如当你输入密码/口令的时候。
  因此,终端提供了某种机制,使得程序能够控制“回显”的启用/禁用。
  对于大多数终端,可以用【Ctrl + S】禁用“回显”,然后用【Ctrl + Q】启用“回显”。
  如果你在禁用“回显”的情况下输入一些文本,当你重新启用“回显”的瞬间,这些文本会一起出现在屏幕上。

顺便说一下:
  由于【Ctrl + S】在 Windows 上是很常见的组合键。某些菜鸟刚开始玩 Linux 命令行的时候,会习惯性地按这个组合键,结果就禁用了回显。这时候,任何键盘输入都没有反应。菜鸟就以为终端死掉了。

历史演变

对于 Windows 用户来说,【Ctrl + S】实在太常用了,很容易误按。肯定有大量的用户吐槽过 POSIX 终端的这个快捷键。
  那么,为啥要用这两个快捷键来控制“回显”捏?俺又要第 N 次说到【电传打字机】了。
  由于这玩意儿的输出是【打印纸】,其速率比较【慢】。一旦“对方发送字符的速率”高于“自己这边的打印速率”,就需要向对方发一个控制信号,让对方暂停发送;等到自己这边打印完了,再发送另一个控制字符,通知对方继续。
  (注:上述这种玩法,通信领域行话称之为“流量控制/流控”)
  当年用来表示“暂停发送”的控制字符,对应的就是【Ctrl + S】;用来“恢复发送”的控制字符,也正是【Ctrl + Q】。

(早期的)系统控制台/物理控制台(system console)

(前面说了)在【没】发明“分时系统”之前,当时的计算机只能执行【单任务】。因此,那时候的大型机只有【一个】操作界面,称之为【控制台】。
  话说那时的“控制台”,真的是一个台子(参见下图)。


(上世纪50年代,IBM 公司 704 大型机的控制台)

后来发明了“分时系统”。如刚才所说——“分时系统”使得大型机可以具备多个终端。在这种情况下,你可以把“控制台”通俗地理解为“本地终端”,而【不】是“控制台”的那些终端,称之为“远程终端”。
  在那个年代,计算机属于【非常非常稀缺】的资源。于是拥有大型机的公司,就可以【出租计算资源】,获得一笔相当可观的收入。他们把大型机的某个“远程终端”租给外来人员使用,然后根据“时间/空间”收取费用。由于资源的稀缺性,当年的 CPU 是按【秒】计费,而内存是按【KB】计费。
  由于“远程终端”可能会被【外人】使用,因此对“远程终端”的【权限】要进行一些限制。如果要进行一些高级别的操作(比如“关闭整个系统”),就只能限制在【控制台】(本地终端)进行。有些公司为了安全起见,还会把“控制台”单独锁在某个“secured room”里面。


(上世纪60年代,DEC 公司 PDP-7 小型机的控制台)

(如今的)虚拟控制台(virtual console)

到了 PC 时代,传统意义上的【控制台】已经看不到了。但 console 这个术语保留了下来。

从“物理 console”到“虚拟 console”

早期大型机的 console 是【独占】硬件滴——“键盘/显示器”固定用于某个 console 滴。
  【现代】的 POSIX 系统,衍生出“virtual console”的概念——可以让几个不同的 console【共用】一套硬件(键盘/显示器)。“virtual”一词就是这么来滴。
  再重复唠叨一下:不论是早期的“物理控制台”还是后来的“虚拟控制台”,都属于广义上的“终端”。

举例:Linux 的 virtual console

假设你的 Linux 系统没安装图形界面(或者默认不启用图形界面),当系统启动完成之后,你会在屏幕上看到一个文本模式的登录提示。这个界面就是 virtual console 的界面。
  在默认情况下,Linux 内置了【6个】virtual console 用于命令行操作,然后把第7个 virtual console 预留给图形系统。你可以使用 Alt + FnCtrl + Alt + Fn 在这几个 console 之间切换(注:上述所说的 Fn 指的是 F1、F2… 之类的功能键)。

虚拟控制台的【内部结构】


(TTY 示意图2:【虚拟控制台】的内部结构图)

终端模拟器(terminal emulator)

请注意上面那张示意图,图中出现了一个【终端模拟器】,这就是本章节要说的东东。
  如果你对比前面的【TTY 示意图1】与【TTY 示意图2】的变化,会发现——“UART & UART 驱动”没了,然后多了这个【终端模拟器】。
  多出来的这个玩意儿相当于加了一个【抽象层】,模拟出早期硬件终端的效果,因此就【无需改动】系统内核中的其它部分,比如:LDISC(line discipline)
  请注意,这个场景下的“终端模拟器”位于操作系统【内核】。换句话说,它属于【内核态】的模拟器。正是因为它处于这个地位,所以能够在“驱动”&“LDISC”之间进行协调。

伪终端(PTY/pseudotty/pseudoterminal)

从“文本模式”到“图形模式”

前面讲的那些,都是【文本模式】(文本界面)。
  话说到了上世纪80年代,随着【图形界面】的兴起,就出现某种需求——想在图形界面下使用“【文本】终端”。于是就出现了“伪终端”的概念。
  通俗地说,“伪终端”就是用某个图形界面的软件来模拟传统的“文本终端”的各种行为。前面说了,TTY 这个缩写相当于“终端”的同义词;因此“pseudotty” 就衍生出 PTY 这个缩写。

从“【内核态】终端模拟器”到“【用户态】终端模拟器”

在上一个章节中,emulator 运行在系统内核中,因此是“内核态模拟器”;
  等到后来搞“伪终端”的时候,就直接把这个玩意儿从【内核态】转到【用户态】——让它直接运行在【桌面环境】。如此一来,用户就可以直接在桌面环境中使用“终端模拟器”。
  当“终端模拟器”变为【用户态】,它就【无法】直接与“键盘驱动 or 显卡驱动”打交道。在这种情况下,由“GUI 系统”(比如:X11)负责与这些驱动打交道,然后再把用户的输入输出转交给“终端模拟器”。

下面这张示意图是 xterm。别看它长得丑,它的出现也算是“里程碑”了。


(xterm——“图形化终端模拟器”的祖师爷)

内部结构示意图

很多人把“emulator”与“PTY”混为一谈。实际上两者处于【不同】层次。
  在操作系统内部(内核),PTY 分为两部分实现,分别叫做“PTY master” & “PTY slave”。master 负责与“terminal emulator”打交道;而用户通过 emulator 里面的 shell 启动的其它进程,则与 slave 打交道。
  在这个环节中,“PTY slave”又进一步缩写为“PTS”。如果你用 ps 命令查看系统中的所有进程,经常会看到 PTS 之类的字样,指的就是这个玩意儿。对普通用户而言,看到的是“终端模拟器”的界面,至于 PTY 内部的 master & slave,通常是感觉不到滴。

为了让大伙儿更加直观,再放一张 PTY 的结构示意图。


(TTY 示意图3:【伪终端】的内部结构图)

shell——命令行解释器

费了好多口水,咱们终于聊到 shell 了。
  顺便吐槽一下:
  扫盲命令行的教程,很少会像俺这样,从最基本的概念说起。其导致的后果就是——很多人(甚至包括很多 Linux 程序员)都搞不清“shell、terminal、console、TTY、PTY、PTS”这些概念到底有啥区别。
  在《如何【系统性学习】——从“媒介形态”聊到“DIKW 模型”》一文中,俺特别强调了【基本概念/基础知识】的重要性。这也就是俺为啥前面要费这么多口水的原因。

shell VS terminal

前面所说的“终端”(terminal),本质上是:基于【文本】的输入输出机制。它并【不】理解具体的命令及其语法。
  于是就需要引入 shell 这个玩意儿——shell 负责解释你输入的命令,并根据你输入的命令,执行某些动作(包括:启动其它进程)。

常见 shell 举例

常见的 shell 包括如下这些(为避免排名纠纷,按字母序列出):

bash
csh
fish
ksh
zsh

在维基百科的“这个页面”,列出了各种各样的 shell 及其功能特性的对照表。
  如今影响力最大的 shell 是 bash(没有之一)。其名称源自“Bourne-again shell”,是 GNU 社区对 Bourne shell 的重写,使之符合自由软件(GPL 协议)。
  本文后续章节对 shell 的举例,如果没有做特殊说明,均指 bash 这个 shell。

shell 的基本功能

显示【命令行提示符】

当你打开一个 shell,会看闪烁的光标左侧显示一个东东,那个玩意儿就是【命令行提示符】(参见下图)


(截图中的“命令行提示符”包含了:用户名、当前路径、$分隔符)

很多 shell 的“命令行提示符”都会包含【当前路径】。当你用 cd 命令切换目录,提示符也会随之改变。这有助于你搞清楚当前在哪个目录下,可以有效避免误操作
  下面这张图演示了——“命令行提示符”随着当前目录的变化而变化。

大部分 shell 都可以让你自定义这个【命令行提示符】,使之显示更多的信息量。
  比如说,可以让它显示:当前的时间、主机名、上一个命令的退出码…
  (注:如果你需要开多个【远程】终端,去操作多个【不同】的系统,“主机名”就蛮有用)

解析用户输入的【命令行】

假设你想看一下 /home 这个目录下有哪些子目录,可以在 shell 中运行了如下命令:

ls /home

当你输入这串命令并敲回车键,shell 会拿到这一行,然后它会分析出,空格前面的 ls 是一个外部命令,空格后面的 /home 是该命令的参数。
  然后 shell 会启动这个外部命令对应的进程,并把上述参数作为该进程的启动参数。

◇内部命令 VS 外部命令

(刚才提到了【外部命令】这个词汇,顺便解释一下)
  通俗地说,“内部命令”就是内置在 shell 中的命令;而“外部命令”则对应了某个具体的【可执行文件】。
  当你在 shell 中执行“外部命令”,shell 会启动对应的可执行文件,从而创建出一个“子进程”;而如果是“内部命令”,就【不】产生子进程。
  那么,如何判断某个命令是否为“外部命令”捏?
  比较简单的方法是——用如下方式来帮你查找。如果某个命令能找到对应的可执行文件,就是“外部命令”;反之则是“内部命令”。

whereis 命令名称

翻译【通配符】

玩过命令行的同学,应该都知道:“星号”(*)与“问号”(?)可以作为通配符,用来模糊匹配文件名。
  当你在 shell 中执行的命令包含了上述两个通配符,实际上是 shell 先把”通配符“翻译成具体的文件名,然后再传给相应命令。

翻译某些【特殊符号】

比如说:在 POSIX 系统中,通常用 ~ 来表示当前用户的【主目录】(home 目录)。
  如果你在 shell 中用到了 ~ 这个符号,shell 会先把该符号翻译成“home 目录的【全路径】”,然后再传给相应命令。

翻译【别名】

很多 POSIX 的 shell 都支持用 alias 命令设置别名(把一个较长的命令串,用一个较短的别名来表示)。
  设置了别名之后,当你在 shell 中使用“别名”,由 shell 帮你翻译成原先的命令串。

举例:
  在《扫盲 netcat(网猫)的 N 种用法——从“网络诊断”到“系统入侵”》一文中,俺使用如下命令创建了 nc-tor 这个别名。

alias nc-tor='nc -X 5 -x 127.0.0.1:9050'

设置完之后,当你在 shell 中执行了这个 nc-tor 命令,shell 会把它自动翻译成 nc -X 5 -x 127.0.0.1:9050

历史命令

大部分 shell 都会记录历史命令。你可以使用某些设定的快捷键(通常是【向上】的方向键),重新运行之前执行过的命令。

自动补全

很多 shell 都具备自动补全的功能。
  该功能不仅指“命令”本身的自动补全,还包括对“命令的参数”进行自动补全。

操作“环境变量”

关于这部分,在下面的“环境变量”章节单独聊。

“管道”与“重定向”

关于这部分,在下面的“管道”章节单独聊。

“进程控制”与“作业控制”

关于这部分,在下面的“进程控制”与“作业控制”章节单独聊。

进程的启动与退出

进程的【启动】及其【父子关系】

一般来说,每个“进程”都是由另一个进程启动滴。如果“进程A”创建了“进程B”,则 A 是【父进程】,B 是【子进程】(这个“父子关系”很好理解——因为完全符合直觉)
  有些同学会问,那最早的【第一个】进程是谁启动滴?
  一般来说,第一个进程由【操作系统内核】(kernel)亲自操刀运行起来;而 kernel 又是由“引导扇区”中的“boot loader”加载。

进程树

在 POSIX 系统(Linux & UNIX),所有的进程构成一个【单根树】的层次关系。进程之间的“父子关系”,体现在“进程树”就是树上的【父子节点】。
  你可以使用如下命令,查看当前系统的“进程树”。

pstree


(“进程树”的效果图。注:为了避免暴露俺的系统信息,特意【不】用自己系统的截图)

初始进程

一般情况下,POSIX 系统的“进程树”的【根节点】就是系统开机之后【第一个】创建的进程,并且其进程编号(PID)通常是 1。这个进程称之为“初始进程”。
  (注:上述这句话并【不够】严密——因为某些 UNIX 衍生系统的“进程树”,位于根节点的进程【不是】“初始化进程”。这种情况与本文的主题没太大关系,俺不打算展开讨论)
  对于“大部分 UNIX 衍生系统”以及“2010年之前的 Linux 发行版”,系统中的“初始进程”名叫 init
  如今越来越多的 Linux 发行版采用 systemd 来完成系统引导之后的初始化工作。在这些发行版中,“初始进程”名叫 systemd

你可以用如下命令显示“进程树”中每个节点的“进程编号”(PID),然后就能看到编号为 1 的“初始进程”。

pstree -p

进程的三种死法

关于进程如何死亡,大致有如下三种情况:

自然死亡
  如果某个进程把它该干的事情都干完了,自然就会退出。
  这种是最常见的情况,也是最优雅的死法。俺习惯称作【自然死亡】。

自杀
  如果某个进程的工作干到半当中,突然收到某个通知,让它立即退出。
  这时候,进程会赶紧处理一些善后工作,然后自行了断——这就是【自杀】。

它杀
  比“自杀”更粗暴的方式称之为【它杀】。也就是让“操作系统内核”直接把进程干掉。
  在这种情况下,进程【不会】收到任何通知,因此也【不】可能进行任何善后事宜。

(注:上述三种死法纯属比喻,以加深大伙儿的印象;不必太较真。十年前俺刚开博客,写过几篇帖子谈“C++ 对象之死”,也用过类似比喻)
  关于“自杀&它杀”的方式,会涉及到【信号】。在下一个章节,俺会单独讨论【进程控制】,并会详细介绍“信号”的机制。

“孤儿进程”及其“领养”

如果某个进程死了(退出了),而它的子进程还【没】死,那么这些子进程就被形象地称之为“孤儿”,然后会被上述提到的【初始进程】“领养”——“初始进程”作为“孤儿进程”的父进程。
  对应到“进程树”——“孤儿进程”会被重新调整到“进程树根节点”的【直接下级】。

“进程控制”与“信号”

用【Ctrl + C】杀进程

为了演示这个效果,你可以执行如下命令:

ping 127.0.0.1

如果是 Windows 系统里的 ping 命令,它只会进行4次“乒操作”,然后就自己退出了;
  但对于 POSIX 系统里面的 ping 命令,它会永远运行下去(直到被杀掉)。
  当 ping 在运行的时候,只要你按下 Ctrl + C 这个组合键,就可以立即终止这个 ping 进程。

“Ctrl + C”背后的原理——【信号】(signal)

当你按下了 Ctrl + C 这个组合键,当前正在执行的进程会收到一个叫做【SIGINT】的信号。
  如果进程内部定义了针对该信号的处理函数,那么就会去执行这个函数,完成该函数定义的一些动作。一般而言,该函数会进行一些善后工作,然后进程退出。
  如果进程【没有】定义相应的处理函数,则会执行一个【默认动作】。对于 SIGINT 这个信号而言,默认动作就是“进程退出”。
  上述这2种情况,都属于前面所说的自杀。这2种属于【常规情况】。

下面再来说【特殊情况】——有时候 Ctrl + C【无法】让进程退出。为啥会这样捏?
  假如说,编写某个进程的程序员,定义了该信号的处理函数,但在这个函数内部,并【没有】执行“进程退出”这个动作。那么当该进程收到 SIGINT 信号之后,自然就【不会】退出。这种情况称之为——信号被该进程【屏蔽】了

【谁】发出“Ctrl + C”对应的信号?

很多人(包括很多玩命令行的老手)都有一个【误解】——他们误以为是 shell 发送了 SIGINT 信号给当前进程。其实不然!
  在上述 ping 的例子中,当 ping 进程在持续运行之时,你的键盘输入是关联到 ping 进程的“标准输入”(stdin)。在这种情况下,shell 根本【无法】获取你的按键信息。
  实际上,是【终端】获取了你的 Ctrl + C 组合键信息,并发送了 SIGINT 信号。因为【终端】处于更底层,它负责承载你所有的输入输出。因此,它当然可以截获用户的某个特殊的组合键(比如:Ctrl + C),并执行某些特定的动作。
  聊到这里,大伙儿会发现——
如果没有正确理解“终端”与“shell”这两者的关系,就会犯很多错误(造成很多误解)。

有的读者可能会问:“终端”如何知道【当前进程】是哪一个?(能想到这点,通常是比较爱思考滴)
  俺来解答一下:
  当 shell 启动了某个进程,它当然可以拿到这个进程的编号(pid),于是 shell 会调用某个系统 API(比如 tcsetpgrp)把“进程编号”与 shell 所属的“终端”关联起来。
  当“终端”需要发送 SIGINT 信号时,再调用另一个系统 API(比如 tcgetpgrp),就可以知道当前进程的编号。

对比杀进程的几个信号:SIGINT、SIGTERM、SIGQUIT、SIGKILL

SIGINT
  在大部分 POSIX 系统的各种终端上,Ctrl + C 组合键触发的就是这个信号。
  通常情况下,进程收到这个信号后,做完相关的善后工作,就自行了断(自杀)。

SIGTERM
  这个信号基本类似于 SIGINT。
  它是 killkillall 这两个命令【默认】使用的信号。
  也就是说,当你用这俩命令杀进程,并且【没有】指定信号类型,那么 killkillall 用的就是这个 SIGTERM 信号。

SIGQUIT
  这个信号类似于前两个(SIGINT & SIGINT),差别在于——进程在退出前会执行“core dump”操作。
  一般而言,只有程序员才会去关心“core dump”这个玩意儿,所以这里就不细聊了。

SIGKILL
  在杀进程的几个信号中,这个信号是是最牛逼的(也是最粗暴的)。
  前面三个信号都是【可屏蔽】滴,而这个信号是【不可屏蔽】滴。
  当某个进程收到了【SIGKILL】信号,该进程自己【完全没有】处理信号的机会,而是由操作系统内核直接把这个进程干掉。
  此种行为可以形象地称之为“它杀”。
  当你用下列这些命令杀进程,本质上就是在发送这个信号进行【它杀】。【SIGKILL】这个信号的编号是 9,下列这些命令中的 -9 参数就是这么来滴。

kill -9 进程号
kill -KILL 进程号killall -9 进程名称
killall -KILL 进程名称
killall -SIGKILL 进程名称

为了方便对照上述这4种,俺放一个表格如下:

信号名称编号能否屏蔽默认动作俗称SIGINT2YES进程自己退出自杀SIGTERM15YES进程自己退出自杀SIGQUIT3YES执行 core dump
进程自己退出自杀SIGKILL9NO进程被内核干掉它杀

【它杀】的危险性与副作用

请注意:**【它杀】是一种比较危险的做法,可能导致一些【副作用】。**只有当你用其它各种方式都无法干掉某个进程,才考虑用这招。
  有读者在评论区问到了“它杀的副作用”,俺简单解释一下:
  一方面,当操作系统用这种方式杀掉某个进程,虽然可以把很多内存相关的资源释放掉,但【内存之外】的资源,内核就管不了啦;另一方面,由于进程遭遇“它杀”,无法完成某些善后工作。
  基于上述两点,就【有可能】会产生副作用。另外,“副作用的严重程度”取决于不同类型的软件。无法一概而论。

举例1:
  某个进程正在保存文件。这时候遭遇“它杀”可能会导致文件损坏。
  (注:虽然某些操作系统能做到“写操作的原子性”,但数据存储可能会涉及多个写操作。当进程在作【多个】关键性写操作时,遭遇它杀。可能导致数据文件【逻辑上】的损坏)

举例2:
  还有更复杂的情况,比如涉及跨主机的网络通讯。某个进程可能向【远程】的某个网络服务分配了某个远程的资源,当进程“自然死亡 or 自杀”,它会在“善后工作”释放这个资源;而如果死于内核的“它杀”,这个远程的资源就【没】释放。

kill VS killall

这两个的差别在于——前者用“进程号”,后者用”进程名“(也就是可执行文件名)。
  对于新手而言,
如果用 kill 命令,你需要先用 ps 命令打印出当前进程清单,然后找到你要杀的进程的编号;而如果要用 killall 命令,就比较省事(比较傻瓜化)。但万一碰到有多个【同名】进程在运行,而你只想干掉其中一个,那么就得老老实实用 kill 了。

进程退出码

任何一个进程退出的时候,都对应某个【整数类型】的“退出码”。
  按照 POSIX 系统(UNIX & Linux)的传统惯例——
当“退出码”为【零】,表示“成功 or 正常状态”;
当“退出码”【非零】,表示“失败 or 异常状态”。

暂停进程

刚才聊“杀进程”的时候提到了“自杀 VS 它杀”。前者比较“温柔”;而后者比较“粗暴”。
  对于暂停进程,也有“温柔 & 野蛮”两种玩法。而且也是用 kill 命令发信号。

【温柔】式暂停(SIGTSTP)

kill -TSTP 进程编号

这个【SIGTSTP】信号类似前面提及的【SIGINT】——
\1. 两者默认都绑定到组合键(【SIGINT】默认绑定到组合键【Ctrl + C】;【SIGTSTP】默认绑定到组合键【Ctrl + Z】)
\2. 这两个快捷键都是由【终端】截获,并发出相应的信号(具体原理参见本章节的某个小节)
\3. 两者都是【可】屏蔽的信号。也就是说,如果某个进程屏蔽了【SIGTSTP】信号,你就【无法】用该方式暂停它。这时候你就得改用【粗暴】的方式(如下)。

【粗暴】式暂停(SIGSTOP)

kill -STOP 进程编号

这个【SIGSTOP】信号与前面提及的【SIGKILL】有某种相同之处——这两个信号都属于【不可屏蔽】的信号。也就是说,收到【SIGSTOP】信号的进程【无法】抗拒被暂停(suspend)的命运。

与“杀进程”的风格类似——当你想要暂停某进程,应该先尝试“温柔”的方法,搞不定再用“粗暴”的方法(套用咱们天朝的老话叫“先礼后兵”)。

恢复进程

当你想要重新恢复(resume)被暂停的进程,就用如下命令(该命令发送信号【SIGCONT】)

kill -CONT 进程编号

引申阅读

除了前面几个小节提到的信号,POSIX 系统还支持其它一些信号,具体参见维基百科的“这个页面”。

作业控制(job)

聊完了“进程控制”,再来聊“作业控制”。
  (注:这里所说的“作业”是从洋文 job 翻译过来滴)

啥是“作业”?

“作业”是 shell 相关的术语,用来表示【进程组】的概念(每个作业就是一组进程)。
  比如说,当你用“管道符”把若干命令串起来执行,这几个命令对应的进程就被视作【一组】。
  (注:“管道符”的用法,后面某个章节会介绍)

同步执行(前台执行) VS 异步执行(后台执行)

大部分情况下,你在 shell 中执行的命令都是“同步执行”(或者叫“前台执行”)。对于这种方式,只有当命令运行完毕,你才会重新看到 shell 的“命令行提示符”。
  如果你以“异步执行”的方式启动某个外部命令,在这个命令还没有执行完的时候,你就可以重新看到“命令行提示符”。

请注意:
  对于【短】寿命的外部命令(耗时很短的外部命令),“同步/异步”两种方式其实【没】啥区别。比如 ls 命令通常很快就执行完毕,你就感觉不到上述两种方式的差异。
  只有当你执行了某个【长】寿命的外部命令(其执行时间至少达到若干秒),上述这两种方式才会体现出差别。

到目前为止,本文之前聊的命令执行方式,都属于“同步执行”;如果想用【异步】,需要在整个命令的最末尾追加一个半角的 & 符号。

【同步】方式举例
  下列命令以【同步】的方式启动火狐浏览器,只有当你关闭了火狐,才会重新看到 shell 的命令行提示符。

firefox

【异步】方式举例
  下列命令以【异步】的方式启动火狐浏览器。你刚敲完回车,就会重新看到 shell 的“命令行提示符”(此时火狐依然在运行)

firefox &

以“同步”方式启动的进程,称作“【前台】进程”;反之,以“异步”方式启动的进程,称作“【后台】进程”。

“前台”切换到“后台”

假设当前的 shell 正在执行某个长寿命的【前台】进程,你可以按【Ctrl + Z】,就可以让该进程变为【后台】进程——此时你立即可以看到“命令提示符”。
  只要你不是太健忘,应该记得前一个章节有提到过【Ctrl + Z】这个组合键——它用来实现”【温柔】式暂停“,其原理是:向目标进程发送【SIGTSTP】信号。

“后台”切换到“前台”

假设当前 shell 正在执行某个后台进程。由于该进程在【后台】执行,此时有“命令提示符”,然后你在 shell 中执行 fg 命令,就可以把该后台进程切换到【前台】。

某些爱思考的同学会问了——如果同时启动了【多个】“后台进程”,fg 命令会切换哪一个捏?
  在这种情况下,fg 命令切换的是【最后启动】的那个。

如果你有 N 个“后台进程”,你想把其中的某个切换为“前台进程”,这时候就需要用到 jobs 命令。该命令与乔布斯同名

扫盲 Linux&UNIX 命令行——从“电传打字机”聊到“shell 脚本编程”相关推荐

  1. Linux下命令行压缩照片或图片的脚本 (ImageMagick使用心得,convert,import,display实例)

    from: Linux下命令行压缩照片或图片的脚本 (ImageMagick使用心得,convert,import,display实例) 在认识ImageMagick之前,我使用的图像浏览软件是Kui ...

  2. Linux九阴真经之无影剑残卷9(Shell脚本编程进阶)

    Shell脚本编程进阶 条件选择if语句选择执行: 注意:if语句可嵌套 单分支 if 判断条件;then 条件为真的分支代码 fi 双分支 if 判断条件; then 条件为真的分支代码 e ...

  3. Linux下命令行压缩照片或图片的脚本 (ImageMagick使用心得,convert,import,display实例)...

    在认识ImageMagick之前,我使用的图像浏览软件是KuickShow,截图软件是KSnapShot,这两款软件都是KDE附带的软件,用起来也是蛮方便的.在一次偶然的机会中,我遇到了ImageMa ...

  4. android执行命令行取得结果,Android调用shell脚本并取得输出

    Android调用shell脚本并获得输出 前段时间做的HLS流媒体服务器可以正常工作了,但是它的启动需要在PC机命令行中进行或者在Android下载个Terminal IDE软件,在Android上 ...

  5. 《Linux命令行与Shell脚本编程大全第2版.布卢姆》pdf

    下载地址:网盘下载 内容简介  · · · · · · 本书是一本关于Linux 命令行与shell 脚本编程的全面教程.全书分为四部分:第一部分介绍Linuxshell 命令行:第二部分介绍shel ...

  6. Linux命令行与shell脚本编程大全:第2版

    <Linux命令行与shell脚本编程大全:第2版> 基本信息 作者: (美)布卢姆(Blum,R.) 布雷斯纳汉(Bresnahan.C.) [作译者介绍] 译者: 武海峰 丛书名: 图 ...

  7. 《linux命令行与shell脚本编程大全》读书笔记第一、二章

    第一章 Linux主要Linux内核.GNU工具.图像化桌面环境.应用软件四部分构成 1.Linux内核 内核主要负责以下四种功能: (1)系统内存管理:维护内存页面表,在物理内存和磁盘之间进行页面的 ...

  8. linux 快速 命令快捷键,快速操作Linux终端命令行的快捷键列表

    快速操作Linux终端命令行的快捷键列表 在shell命令终端中,Ctrl+n相当于方向向下的方向键,Ctrl+p相当于方向向上的方向键. 在命令终端中通过它们或者方向键可以实现对历史命令的快速查找. ...

  9. linux SHELL脚本编程

    shell脚本编程 shell定义:简单来讲就是命令解析器,将用户输入的指令转换为相应的机器能够运行的程序. shell种类:bourne shell (sh):korn shell(ksh) bou ...

最新文章

  1. Java ArrayDeque工作原理及实现
  2. SVM的发展和研究热点
  3. ubuntu 下review board一遍安装成功
  4. 傅里叶变换音频可视化_HTML5如何实现音频可视化频谱跳动
  5. 前端学习(2388):封装请求模块
  6. [Cocoa]深入浅出Cocoa之Core Data(2)- 手动编写代码
  7. druid seata 配置_架构设计 | 基于Seata中间件,微服务模式下事务管理
  8. 从未在一起更让人遗憾_二十不惑强行悲伤结尾,我们本可以在一起,才最让人遗憾...
  9. Redis安装+启动报错
  10. 山东大学软件工程硕士天津保送班
  11. 个人学习java的真实经历!
  12. 《阿里巴巴 Java开发手册》读后感
  13. 笔记-15 网络编程入门 UDP通信程序 TCP通信程序 练习
  14. 商品品牌信息的增删改查操作步骤_畅购商城(三):商品管理
  15. 2020美团笔试题目:送餐小区数量
  16. 京北机房 怀来云交换数据中心主机托管
  17. 【附源码】Python计算机毕业设计社区志愿者管理系统
  18. 电脑突然断电蓝屏导致Git错误的一种解决办法
  19. CCF201403-5 任务调度
  20. matplotlib从绘图到排版

热门文章

  1. PE 文件型病毒编写实验(一)
  2. C语言小练习:输入一个字符串,内有数字和非数int字符,如a123x456,将其中连续的数作为一个整数,依次存放到整形数组a中。
  3. 一文详解java线程池 详解Java线程池的七个参数 详解池化技术 java如何选择核心线程数 详解Java线程池的拒绝策略
  4. 硬盘故障诊断软件mhdd使用详解
  5. 数字化转型—‘前言’
  6. 以前有个搜刮音乐_在终端上使用PHP搜刮Google建议
  7. 中南大学c语言课程设计机票预订,求个C语言课程设计的教务管理系统。。。
  8. Java基础练习项目——英雄小游戏
  9. 【即构 Web SDK 开发团队】FLV音画同步 — 你所不知道的 SEI
  10. 零基础小白如何提高学Python的效率?