Linux(1)
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了Linux(1),小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含15589字,纯文字阅读大概需要23分钟。
内容图文
★终端(terminal/TTY)
最早期的计算机(大型机)是【单任务】滴——也就是说,每次只能干一件事情。
到了60年代,出现了一个【革命性】的飞跃——发明了【多任务】系统,当时叫做“time-sharing”(分时系统)。
有了“分时系统”,就可以让多个人同时使用一台大型机。而为了让多个人同时操作这台大型机,就引入了【终端】的概念。
每一台大型机安装多个终端,每个操作员都在各自的终端上进行操作,互不干扰。
“终端”的好处不光是“多任务”,而且还可以让用户在【远程】进行操作。
这种情况下,“终端”通过 modem(调制解调器)与“主机”相连。
这种玩法很类似于——互联网普及初期的拨号上网。示意图如下:
最早的“终端”,本质上就是“电传打字机”——以“打字机”作为输入;以“打印纸”作为输出。
◇内部结构示意图
下面这张是大型机时代,“终端”与“进程”通讯的示意图。
图中的 UART
是洋文“Universal Asynchronous Receiver and Transmitter”的缩写(相关维基百科链接在“这里”)。
LDISC 是洋文“line discipline”的简写(相关维基百科链接在“这里”)。
通俗地说,UART 用来处理物理线路的字符传输(比如:“错误校验”、“流控”、等);
LDISC 用来撮合底层的“硬件驱动”与上层的“系统调用”,并完成某些“控制字符”的处理与翻译。
The terminal subsystem consists of three layers:
the upper layer to provide the character device interface,
the lower hardware driver to communicate with the hardware or pseudo terminal,
and the middle line discipline to implement behavior common to terminal devices.
(TTY 示意图1:使用【硬件终端】的大型机内部结构图)
◇如今的含义
如今,“终端”一词的含义已经扩大了——用来指:基于【文本】的输入输出机制。
★终端的3种【缓冲模式】——字符模式、行模式、屏模式
◇字符模式(character mode)
【电传打字机】是基于【字符】传输滴。也就是说,操作员每次在“电传打字机”上按键,对应的字符会立即通过线路发送给对方。
这就是最传统的【字符模式】通俗地说,“字符模式”也就是【无缓冲】的模式。
◇行模式(line mode)
不客气地说,“字符模式”是非常傻逼滴!因为如果你不小心按错键,这个错误也会立即发送出去。
比如说,你在输入一串很长的命令,结果输到半当中,敲错一个按键,整个命令就废了——要重新再输入一遍。
所以,当早期的程序员对“字符模式”实在忍无可忍之后,终于发明了【行模式】。
【行模式】也叫做“行缓冲”。也就是说,终端会把你当前输入的这行先缓冲在本地。
只有当你最终按了【回车键】,才会把这一整行发送出去。
如果你不小心敲错了一个字符,可以赶紧用“退格键”删掉重输这个字符。
因此,这种模式称之为【行缓冲】。
◇屏模式(screen mode/block mode)
“行模式”进一步的发展就是【屏模式】。这个玩意儿也叫“全屏缓冲”,顾名思义,终端会缓冲当前屏幕的内容。
在这种模式下,用户可以利用方向键,操纵光标(cursor)在屏幕上四处游走。
早期的键盘【没】方向键。有了这个【屏模式】之后,键盘上才开始增加了“方向键”(所以“方向键”位于键盘的扩展区)
上述这三种模式,第1种基本淘汰(仅限于极少数场景);第3种用得也不多。与本文关系比较密切的,其实是【第2种】——行模式。
为了加深你的印象,用 cat
命令来举例(注:这个命令其实与“猫”【无关】,而是 concatenate 的简写)
大部分情况下,都是用它来显示某个文件的内容,比如说:cat 文件名
。
但如果你运行 cat
【没】加任何参数,那么它就会尝试读取你在终端的输入,然后把读到的文本再原样输出到终端。
在上述动画中,你的输入并【没有】直接传递给 cat
进程。要一直等到你按下【回车键】,cat
进程才收到你的输入,并立即打印了输出。
★终端的【回显】
◇“回显”是啥?
在刚才那个 gif 动画中,当俺逐个输入 test
的每个字母,这些字母也会逐个显示在屏幕上。这种做法叫做【回显】。
★(早期的)系统控制台/物理控制台(system console)
在【没】发明“分时系统”之前,当时的计算机只能执行【单任务】。因此,那时候的大型机只有【一个】操作界面,称之为【控制台】。那时的“控制台”,真的是一个台子。
后来发明了“分时系统”。“分时系统”使得大型机可以具备多个终端。
在这种情况下,你可以把“控制台”通俗地理解为“本地终端”,而【不】是“控制台”的那些终端,称之为“远程终端”。
由于“远程终端”可能会被【外人】使用,因此对“远程终端”的【权限】要进行一些限制。
如果要进行一些高级别的操作(比如“关闭整个系统”),就只能限制在【控制台】(本地终端)进行。
(这里要注意终端和控制台的区别,控制台是本地终端。从终端讲到控制台,范围在缩小,但还是在终端这个大范围说事情)
★(如今的)虚拟控制台(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 + Fn
或 Ct
rl + 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)负责与这些驱动打交道,然后再把用户的输入输出转交给“终端模拟器”。
◇内部结构示意图
很多人把“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 VS terminal
前面所说的“终端”(terminal),本质上是:基于【文本】的输入输出机制。它并【不】理解具体的命令及其语法。
于是就需要引入 shell 这个玩意儿——shell 负责解释你输入的命令,并根据你输入的命令,执行某些动作(包括:启动其它进程)。
◇常见 shell 举例
bash
csh
fish
ksh
zsh
在维基百科的“这个页面”,列出了各种各样的 shell 及其功能特性的对照表。
如今影响力最大的 shell 是 bash(没有之一)。其名称源自“Bourne-again shell”,是 GNU 社区对 Bourne shell 的重写,使之符合自由软件(GPL 协议)。
本文后续章节对 shell 的举例,如果没有做特殊说明,均指 bash 这个 shell。
(git那里用的就是这个东西)
★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到底是什么关系?上面不是说终端模拟器中包含shell?]
有的读者可能会问:“终端”如何知道【当前进程】是哪一个?(能想到这点,通常是比较爱思考滴)
当 shell 启动了某个进程,它当然可以拿到这个进程的编号(pid),于是 shell 会调用某个系统 API(比如 tcsetpgrp
)把“进程编号”与 shell 所属的“终端”关联起来。
当“终端”需要发送 SIGINT 信号时,再调用另一个系统 API(比如 tcgetpgrp
),就可以知道当前进程的编号。
◇对比杀进程的几个信号:SIGINT、SIGTERM、SIGQUIT、SIGKILL
SIGINT
在大部分 POSIX 系统的各种终端上,Ctrl + C
组合键触发的就是这个信号。
通常情况下,进程收到这个信号后,做完相关的善后工作,就自行了断(自杀)。
SIGTERM
这个信号基本类似于 SIGINT。
它是 kill
& killall
这两个命令【默认】使用的信号。
也就是说,当你用这俩命令杀进程,并且【没有】指定信号类型,那么 kill
或 killall
用的就是这个 SIGTERM 信号。
SIGQUIT
这个信号类似于前两个(SIGINT & SIGINT),差别在于——进程在退出前会执行“core dump”操作。
一般而言,只有程序员才会去关心“core dump”这个玩意儿,所以这里就不细聊了。
SIGKILL
在杀进程的几个信号中,这个信号是是最牛逼的(也是最粗暴的)。
前面三个信号都是【可屏蔽】滴,而这个信号是【不可屏蔽】滴。
当某个进程收到了【SIGKILL】信号,该进程自己【完全没有】处理信号的机会,而是由操作系统内核直接把这个进程干掉。
此种行为可以形象地称之为“它杀”。
当你用下列这些命令杀进程,本质上就是在发送这个信号进行【它杀】。【SIGKILL】这个信号的编号是 9
,下列这些命令中的 -9
参数就是这么来滴。
kill -9 进程号 kill -KILL 进程号 killall -9 进程名称 killall -KILL 进程名称 killall -SIGKILL 进程名称
内容总结
以上是互联网集市为您收集整理的Linux(1)全部内容,希望文章能够帮你解决Linux(1)所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。