Linux 系统命令及 Shell 脚本实践指南
王军
415 个笔记

◆ 点评

认为好看
此前一些似懂非懂的概念,经常使用却总感觉不得法的工具和系统特性,终于在这本书中系统性地找到了答案。

◆ 第 1 章 Linux 简介

1969 年 8 月左右,他的妻儿出门探亲了一个月,就在这一个月的时间里,Thompson 编写了一个操作系统,并成功地将“星际旅行”移植到了 DPD-7 上,而这个操作系统就是 UNIX 的原型。

Linux 的内核设计分成进程管理、内存管理、进程间通信、虚拟文件系统、网络 5 部分

Linux 采取了很多安全技术措施,包括读写权限控制、带保护的子系统、审计跟踪、核心授权等

多任务是现代化计算机的主要特点,指的是计算机能同时运行多个程序,且程序之间彼此独立,Linux 内核负责调度每个进程,使之平等地访问处理器

Grub 是一个系统引导工具,通过它可以加载内核,从而引导系统启动。

/boot 分区用于放置 Linux 启动所用到的文件,如 kernel 和 initrd 文件。

◆ 第 2 章 Linux 用户管理

Linux 系统中的用户分为 3 类,即普通用户、根用户、系统用户。

通常普通用户的 UID 大于 500,因为在添加普通用户时,系统默认用户 ID 从 500 开始编号。

根用户也就是 root 用户,它的 ID 是 0,也被称为超级用户,root 账户拥有对系统的完全控制权:可以修改、删除任何文件,运行任何命令。

root 用户也是系统里面最具危险性的用户,root 用户甚至可以在系统正常运行时删除所有文件系统,造成无法挽回的灾难

系统用户是指系统运行时必须有的用户,但并不是指真实的使用者。

在 RedHat 或 CentOS 下,系统用户的 ID 范围是 1~499

系统用来记录用户名、密码最重要的两个文件就是/etc/passwd 和/etc/shadow。

[插图]

[插图]

系统在添加用户时,需要预先为这个用户创建一些默认的“配置文件”,而默认配置的就是/etc/skel 目录下的几个隐藏文件。可以说,/etc/skel 实际上是创建用户时的“模板”。

创建用户后,该用户实际上还没有登录系统的权限,因为在不设置密码的情况下,在/etc/shadow 中该用户记录中以冒号分隔的第二列将显示为两个感叹号“!!”,这说明不允许该用户登录系统。

su 是切换用户的意思。在不加参数的情况下,su 命令默认表示切换到 root 用户

su 命令后面还可以加上一个“-”参数,就是键盘上的中横线。加上这个参数后,切换成 root 用户时,不但身份变成了 root,而且还能应用 root 的用户环境。

所谓“用户环境”就是/etc/passwd 中定义的用户家目录、使用的 Shell,以及关于这个用户的个性化设置等。

使用 su 命令虽然很方便,但还是有很明显的缺陷,就是切换成其他用户的前提是需要知道对方的密码

但是考虑到这个配置文件的重要性,Linux 提供了专门编辑这个文件的方式,就是使用命令 visudo 来编辑这个文件,它的好处是可以在编辑后保存退出时自动检查语法设置,以防止不小心配置错误而造成无法使用 sudo 命令。

加入的“john ALL=(ALL) ALL”这一行代表的意思是,john 这个用户(第一列)可以从任何地方登录后(第二列的 ALL)执行任何人(第三列的 ALL)的任何命令(第四列的 ALL)。还可以定义某一个组的 sudo 权限,比如“%john ALL=(ALL) ALL”可以让所有属于 john 用户组的用户从任何地方登录后执行任何人的任何命令。

只需要知道自己的密码就可以使用 sudo 执行任何命令,这样方便多了。但是每次都需要输入一遍密码也是比较麻烦的事情,想要实现不需要输入密码就可以执行命令,可以在最后一个 ALL 前添加“NOPASSWD:”,

严格来说,sudo 并不是真的切换了用户,而是使用其他用户的身份和权限执行了命令。

如果任务是周期性执行的,其命令为 cron;如果只是在某一个特定的时间执行一次,其命令为 at。

可以使用 atq 命令查看当前使用 at 命令调度的任务列表,第一列是任务编号;也可以使用 atrm 删除已经进入任务队列的任务

默认情况下,所有用户都可以使用 at 命令来调度自己的任务,如果由于特殊的原因需要禁止某些用户使用这个功能,可以将该用户的用户名添加至/etc/at.deny 中。

用户可通过 crontab 来设置自己的计划任务,并使用-e 参数来编辑任务

在下面的示例中,前面 5 个可以用来定义时间,第一个表示分钟,可以使用的值是 1~59,每分钟可以使用/1 表示;第二个表示小时,可以使用的值是 0~23;第三个表示日期,可以使用的值是 1~31;第四个表示月份,可以使用的值是 1~12;第五个表示星期几,可以使用的值是 0~6,0 代表星期日;最后是执行的命令。

          • command​​

与 at 类似,每个用户都可以设置自己的 crontab,如果由于特殊的原因需要禁止某些用户使用这个功能,可以将该用户的用户名添加至/etc/cron.deny 中。除了 root 之外,普通用户只可以设置、查看、删除自己的计划任务,root 可以使用-u 参数查看指定用户的任务。

事实上,系统也有自己的例行任务,而其配置文件是/etc/crontab

◆ 第 3 章 Linux 文件管理

根据 FHS 的定义,每个目录应该放置的文件如表 3-1 所示。
表 3-1 FHS 定义的目录结构

一个点(.)代表的是当前目录,两个点(..)代表的是当前目录的上层目录

查看文件:cat
该命令是 concatenate 的简写,用户查看文件内容,后面跟上要查看的文件名即可

默认情况下,head 将显示该文件前 10 行的内容

文件格式转换:dos2unix
该命令是 DOS to UNIX 的简写,也许你从字面上可以大概猜到它的作用,就是可以把 DOS 格式的文本文件转变成 UNIX 下的文本文件

进入目录:cd
该命令是 change directory 的简写,方便用户切换到不同的目录

删除目录:rmdir 和 rm
该命令是 remove directory 的简写,用来删除目录。但是需要注意的是,它只能删除空目录,如果目录不为空(存在文件或者子目录),那么该命令将拒绝删除指定的目录

由于 root 用户在 Linux 系统中的权限非常高,甚至可以用 rm-rf /命令来删除全部的系统文件(这样做的后果是灾难性的),所以使用-rf 参数删除目录一定要慎之又慎!

在 Linux 下目录也是一种文件,所以如果 touch 一个目录,这个目录的创建时间也会被更新

第一列是文件类别和权限,这列由 10 个字符组成,第一个字符表明该文件的类型

目录文件的连接数是该目录中包含其他目录的总个数 +2

[插图]

Linux 下的文件还有一些隐藏属性,必须使用 lsattr 来显示

如果要设置文件的隐藏属性,需要使用 chattr 命令

这里介绍几个常用的隐藏属性,第一种是 a 属性。拥有这种属性的文件只能在尾部增加数据而不能被删除

还有一种比较常用的属性是 i 属性。设置了这种属性的文件将无法写入、改名、删除,即便是 root 用户也不行。这种属性常用于设置在系统或者关键服务中的配置文件,这对提升系统安全性有较大的帮助

[插图]

如果需要修改的不是一个文件而是一个目录,以及该目录下所有的文件、子目录、子目录下所有的文件和目录(即递归设置该目录下所有的文件和目录的权限),则需要使用-R 参数,

默认情况下,使用什么用户登录系统,那么该用户新创建的文件和目录的拥有者就是这个用户

改变文件的拥有组:chgrp
该命令用来更改文件的拥有组

文件特殊属性:SUID/SGID/Sticky

这就是奥秘所在—该命令是设置了 SUID 权限的,这意味着普通用户可以使用 root 的身份来执行这个命令

但是必须注意的是,SUID 权限只能用于二进制文件

SGID 就很容易了:如果某个二进制文件的用户组权限被设置了 s 权限,则该文件的用户组中所有的用户将都能以该文件的用户身份去运行这个命令

Sticky 权限只能用于设置在目录上,设置了这种权限的目录,任何用户都可以在该目录中创建或修改文件,但是只有该文件的创建者和 root 可以删除自己的文件。

可以给出一个结论:对于 root 用户,文件的默认权限是 644,目录的默认权限是 755;对于普通用户,文件的默认权限是 664,目录的默认权限是 775。

在 Linux 下,定义目录创建的默认权限的值是“umask 遮罩 777 后的权限”,定义文件创建的默认权限是“umask 遮罩 666 后的权限”。
系统在/etc/profile 文件中,通过第 51 行至 55 行的一段代码设置了不同用户的遮罩值。

[插图]

与 find 不同,locate 命令依赖于一个数据库文件,Linux 系统默认每天会检索一下系统中的所有文件,然后将检索到的文件记录到数据库中。

在执行这个命令之前一般需要执行 updatedb 命令(这不是必须的,因为系统每天会自动检索并更新数据库信息,但是有时候会因为文件发生了变化而系统还没有再次更新而无法找到实际上确实存在的文件。所以有时需要主动运行该命令,以创建最新的文件列表数据库)

which 用于从系统的 PATH 变量所定义的目录中查找可执行文件的绝对路径

使用 whereis 也能查到其路径,但是和 which 不同的是,它不但能找出其二进制文件,还能找出相关的 man 文件

gzip/gunzip 是用来压缩和解压缩单个文件的工具

tar 不但可以打包文件,还可以将整个目录中的全部文件整合成一个包,整合包的同时还能使用 gzip 的功能进行压缩

一般来说,整合后的包习惯使用.tar 作为其后缀名,使用 gzip 压缩后的文件则使用.gz 作为其后缀名。因为 tar 有同时整合和压缩的功能,所以可使用.tar.gz 作为后缀名,或者简写为.tgz

这里-z 的含义是使用 gzip 压缩,-c 是创建压缩文件(create),-v 是显示当前被压缩的文件,-f 是指使用文件名,也就是这里的 boot.tgz 文件。

上面的命令会直接将 boot.tgz 在当前目录中解压成 boot 目录,-z 是解压的意思。如需要指定压缩后的目录存放的位置,需要再使用-C 参数

使用 bzip2 压缩文件时,默认会产生以.bz2 扩展名结尾的文件,这里使用-z 参数进行压缩,使用-d 参数进行解压缩。

该命令一般是不单独使用的,需要和 find 命令一同使用。当由 find 按照条件找出需要备份的文件列表后,可通过管道的方式传递给 cpio 进行备份,生成/tmp/conf.cpio 文件,然后再将生成的/tmp/conf.cpio 文件中包含的文件列表完全还原回去。

◆ 第 4 章 Linux 文件系统

文件系统是操作系统用于明确磁盘或分区上相关文件的方法和数据结构,通俗的说法就是在磁盘上组织文件的方法。

大部分 Linux 系统都具有类似的通用结构,包括超级块(superblock)、i 节点(inode)、数据块(data block)、目录块(directory block)等

超级块包括文件系统的总体信息,是文件系统的核心,所以在磁盘中会有多个超级块,以防止由于磁盘出现坏块导致全部文件系统无法使用

i 节点存储所有与文件有关的元数据,也就是文件所有者、权限等属性数据以及指向的数据块,但是不包括文件名和文件内容

数据块是真实存放文件数据的部分,一个数据块默认情况下是 4KB

目录块包括文件名和文件在目录中的位置,并包括文件的 i 节点信息

Linux 最早引入的文件系统类型是 minix,由于其存在一定的局限性,比如说文件名最长仅支持 14 个字符,文件最大为 64MB 等因素,后来被 ext2(The Second Extended File System)文件系统所取代

但是 ext2 文件系统的弱点也是很明显的:它不支持日志功能。这很容易造成在一些情况下丢失数据,这个天然的弱点让 ext2 文件系统无法用于关键应用中,目前已经很少有企业使用 ext2 文件系统了。

那么为什么需要日志文件系统呢?因为日志文件系统使用了“两阶段提交”的方式来维护待处理的事务

比方说在写入数据之前,文件系统会先在日志中写入相关记录信息,然后再开始真实地写数据,写完数据后则会将之前写入日志中的内容删除

如果遇到问题需要检查文件系统或对 ext3 文件系统进行修复时,只需要检查日志即可

为了更好地使用磁盘空间,提高系统空间的可扩展性,此时就需要使用逻辑卷。

逻辑卷就是使用逻辑卷组管理(Logic Volume Manager)创建出来的设备,也是 Linux 操作系统可以认识的设备

LVM 是介于硬盘祼设备和文件系统的中间层

首先创建一个或多个物理卷,物理卷按照相同(或不同)的组名称聚集形成一个(或多个)物理卷组,而逻辑卷就是从某个物理卷组中抽象出来的一块磁盘空间。

在对逻辑卷创建文件系统的时候,其全路径是/dev/卷组名/逻辑卷名

硬链接(hard link)又称实际链接,是指通过索引节点来进行链接

在 Linux 文件系统中,所有的文件都会有一个编号,称为 inode,多个文件名指向同一索引节点是被允许的,这种链接就是硬链接

软链接(soft link)又称符号链接(symbolic link),是一个包含了另一个文件路径名的文件,可以指向任意文件或目录,也可以跨不同的文件系统

◆ 第 5 章 字符处理

在 Linux 中也存在着管道,它是一个固定大小的缓冲区,该缓冲区的大小为 1 页,即 4K 字节。

由于 grep 区分大小写,所以虽然第二行中含有大写的 NAME,但是也不会匹配到。如果希望忽略大小写,可以加上-i 参数。

如果想打印出文件中不包含 name 的行,可以使用 grep 的反选参数-v。

如果文件(或标准输出)中有多行完全相同的内容,我们很自然希望能删除重复的行,同时还可以统计出完全相同的行出现的总次数,uniq 命令就能帮助解决这个问题

uniq [-ic]
#-i 忽略大小写 
#-c 计算重复行数​​

需要说明的是,uniq 一般都需要和 sort 命令一起使用,也就是先将文件使用 sort 进行排序(这样重复的内容就能显示在连续的几行中),然后再使用 uniq 删除掉重复的内容(uniq 的作用就在于删除连续的完全一致的行)。

顾名思义,cut 就是截取的意思,它能处理的对象是“一行”文本,可从中选取出用户所需要的部分。

cut-f 指定的列-d'分隔符'​​

cut-c 指定列的字符​​

tr 命令比较简单,其主要作用在于文本转换或删除

paste 的作用在于将文件按照行进行合并,中间使用 tab 隔开

在 Linux 下使用 split 命令来实现文件的分割,支持按照行数分割和按照大小分割这两种模式

◆ 第 6 章 网络管理

nameserver 关键字后面紧跟着一个 DNS 主机的 IP 地址,可以设置 2~3 个 nameserver,但是主机在查询域名时会首先查询第一个 DNS,当该 DNS 不可用时才会查询第二个 DNS,以此类推

search 关键字后紧跟的是一个域名。每个主机严格来说都应该有一个 FQDN(全限定域名),所以往往域名就很长,如果这里写成 search google.com,那么 www 就代表 www.google. com 了,这个关键字后可以跟多个域名。

domain 关键字和 search 类似,不同的是 domain 后面只能跟一个域名。

host 命令是用来查询 DNS 记录的,如果使用域名作为 host 的参数,命令返回该域名的 IP

在 IP 包结构中有一个定义数据包生命周期的 TTL(Time To Live)字段,该字段用于表明 IP 数据包的生命值,当 IP 数据包在网络上传输时,每经过一个路由器该值就减 1,当该值减为 0 时此包就会被路由器丢弃。这种设计可用于避免出现一些由于某种原因始终无法到达目的地的包不断地在互联网上传递(可以形象地称之为“幽灵包”),减少无谓的网络资源耗用。

不过路由器也不是“无声无息”地将 TTL 值为 0 的 IP 包丢弃的,它会同时给发送该 IP 数据包的主机发送一个 ICMP“超时”消息,主机在接收到这个 ICMP 包后就同时能得到该路由的 IP 地址。

根据上面两个特点,人们写了一个检测数据包是如何经由路由器的工具—traceroute,我们可以想象一下该工具的工作原理:它先构造出一个 TTL 值为 1 的数据包发送给目的主机,这个数据包在经由第一个路由器时,路由器先将 TTL 值减 1 变为 0,然后将该 IP 包丢弃,同时给发送一个 ICMP 消息,这样就得到了经过的第一台路由器的 IP 地址;然后再构造出一个 TTL 值为 2 的数据包,以此类推,就能得到该 IP 包经历的整条链路的路由器 IP。

第一步是要确认网卡本身是否能正常工作?利用 ping 工具可以确认这点。输入 ping 127.0.0.1,然后看是否能正常 ping 通?这里的 127.0.0.1 被称为主机的回环接口,是 TCP/IP 协议栈正常工作的前提。

第二步是要确认网卡是否出现了物理或驱动故障,使用 ping 本机 IP 地址的方式,如果能 ping 通则说明本地设备和驱动都正常。

第三步要确认是否能 ping 通同网段的其他主机。这一步主要是确认二层网络设备(比如交换机或者 HUB)工作是否正常

第四步要确认是否能 ping 通网关 IP。如果数据包能正常到达网关,则说明主机和本地网络都工作正常。

第五步确认是否能 ping 通公网上的 IP,如果可以则说明本地的路由设置正确,否则就要确认路由设备是否做了正确的 nat 或路由设置。

第六步确认是否能 ping 通公网上的某个域名,如果能 ping 通则说明 DNS 部分设置正确。

◆ 第 7 章 进程管理

进程包括动态执行的程序和数据两部分

所有的进程都可能存在 3 种状态:运行态、就绪态、阻塞态

进程之间又存在互斥和同步的关系

现代计算机使用信号量机制来实现进程间的互斥和同步,它的基本原理是:两个或者多个进程可以通过简单的信号进行合作,一个进程可以被迫在某一位置停止,直到它接收到一个特定的信号。

进程是动态的,而程序是静态的,进程是程序以及数据在计算机上的一次执行,没有静态的程序也就没有动态的执行。

第三行是 CPU 信息,us 代表用户空间占用的 CPU 百分比,sy 代表内核空间占用的 CPU 百分比,ni 代表改变过优先级的进程占用的 CPU 百分比,id 代表空闲 CPU 百分比

wa 代表 I/O 等待百分比,hi 代表硬中断占用的 CPU 百分比,si 代表软中断占用的 CPU 百分比。

如果要显示更多的字段,可以在 top 显示界面中按字母键 f。

另外,默认情况下 top 显示的进程是按照 CPU 使用率来进行排序的,如果要另选排序规则怎么办呢?可以按大写字母 O 键进入排序选择页

比如按字母 P 键表示按照 CPU 的使用率排序,按字母 M 键表示按照 Memory 的使用率排序,按字母 N 键表示以 PID 排序,按字母 T 键表示按照 CPU 使用时间排序,按字母 K 键则表示 kill 进程,按字母 R 键表示可以 renice 一个进程等。

命令 kill 后可以跟的信号代码一共有 64 种,使用 kill-l 就可以看到具体有哪些

但是常用的一般只有 3 个,即 HUP(1)、KILL(9)、TERM(15),分别代表重启、强行杀掉、正常结束。

使用-9 参数强行停止该进程了,其效果是立即杀死进程,而且该信号无法被阻塞或忽略。但是这个命令也有其天然的危险,就是进程可能会直接被系统终止,而没有清理之前申请的内存,这会造成一定程度的“内存泄露”,因此一般情况下不建议使用。

而-15 这个参数就比较温和了,它会使进程正常退出,它也是 Linux 默认的程序中断信号(也就是在不加参数的情况下默认使用的信号)。

lsof(list open files)是一个列出当前系统中所有打开文件的工具。

在系统中,被打开的文件可以是普通文件、目录、网络文件系统中的文件、字符设备、管道、socket 等

输出的字段有 COMMAND、PID、USER、FD、TYPE、DEVICE、SIZE、NODE、NAME9 列,

COMMAND:进程的名称。
▪ PID:进程标识符。
▪ USER:进程所有者。
▪ FD:文件描述符,应用程序通过文件描述符识别该文件。
▪ TYPE:文件类型,如 DIR、REG 等。
▪ DEVICE:磁盘的名称。
▪ SIZE:文件大小。
▪ NODE:索引节点。
▪ NAME:打开文件的全路径名称。

尝试将其 umount 卸载,系统提示 device is busy,无法卸载。这时候使用 lsof 命令确认了一下,确实有进程在占用这个目录

使用 lsof 还可以查找使用了某个端口的进程

lsof-i:22

假设文件/var/log/messages 不小心被删除了,首先来确认一下当前是否有进程正在使用这个文件,如果有则可以继续,如果没有就无法使用该方法继续了。本例中看到有个 PID 为 2449 的进程正在使用该文件,那么接下来只要找到对应/proc 目录下的文件就可以了

在学习 top 时,我们看到其输出中有 NI 字段,标记了对应进程的优先级,该字段的取值范围是-20~19,数值越低代表优先级越高,也就能更多地被操作系统调度运行,如果一个进程在启动时并没有设定 nice 优先级,则默认使用 0

普通用户也可以给自己的进程设定 nice 优先级,但是范围只限于 0~19。

实际上,Linux 使用了“动态优先级”的调度算法来确定每一个进程的优先级,一个进程的最终优先级=优先级 +nice 优先级。

nice 命令仅限于在启动一个进程的时候同时赋予其 nice 优先级

对于已经启动的进程,可以用 renice 命令进行修改

除了使用 renice 外,还可以使用 top 提供的功能来修改

◆ 第 8 章 Linux 下的软件安装

在 Linux 系统中,一般在/usr/local/src/目录里下载源码包(这不是硬规定,而是一个良好的习惯

一般来说建议自行编译安装的软件放置的目录为/usr/local/

源码编译的前提是系统中安装了 gcc 工具,对于注重安全生产环境的用户而言是不应该安装这个工具的

RPM 是 RedHat Package Manager 的简写,顾名思义是红帽软件包管理器的意思

Linux 中一切皆文件,所以说白了,RMP 其实是一种集成了文件管理和软件版本控制的工具

RPM 分为两类,第一类是二进制安装包(也就是预编译包)

如果将编译好的软件复制到相同软件环境(内核版本一致、软硬件运行环境一致)的服务器中,只要软件在原编译机中能运行,那么在新主机中也同样可以运行

第二类是 RPM 源码包,当希望自定义编译参数,自行制作二进制安装包的时候使用

使用 RPM 包管理的方式是通过 rpm 命令

默认情况下 RedHat 会因为未注册 RHN 而无法使用 yum

作为一个 Linux 工程师,不能把技术活做成体力活。

如果读者在网上搜索自建网络 yum 源的相关文档,可能会发现其中十有八九都有使用 createrepo 工具重新创建 repodata 这一步骤

RPM 包有两种,一种是二进制安装包,还有一种是源码包,这种包的后缀名一般以.src.rpm 结束(有时简称为 srpm),标识着这是一个“包含源码的 RPM 包”。

◆ 第 9 章 vi 和 vim 编辑器

vi 编辑器是 Visual Interface 的简称,是 Linux 系统中最基本的文本编辑器

◆ 第 10 章 正则表达式

sed 其实是以行为单位的文本处理工具,而 awk 则是基于列的文本处理工具

它的工作方式是按行读取文本并视为一条记录,每条记录以字段分割成若干字段,然后输出各字段的值。

◆ 第 11 章 Shell 编程概述

当你使用 ssh 客户端工具远程连接到系统,或坐在一台服务器前输入密码登录到系统中时,面前呈现的跳动的光标就是一个 Shell

在计算机语言中,Shell 是指一种命令行解释器,是为用户和操作系统之间通信提供的一种接口

图 11-1 显示了 Shell 在操作系统中的位置。

cat /etc/Shells

为了对用户屏蔽这些复杂的技术细节,同时也是为了保护内核不会因用户直接操作而受到损害,有必要在内核之上创建一个层,该层就是一个“壳”,也就是 Shell 名称的由来。

Bash Shell 有两种工作模式,分别是互动模式和脚本模式

个 Shell 脚本永远是以“#!”开头的,这是一个脚本开始的标记,它是在告诉系统执行这个文件需要使用某个解释器,后面的/bin/bash 就是指明了解释器的具体位置。

为了更清晰地看到脚本运行的过程,还可以借助-x 参数来观察脚本的运行情况

为了更精细地调试运行 Shell,我们可以借助第三方工具 bashdb。这是一个类似于 GDB 的脚本调试软件,小巧而强大,具有设置断点、单步执行、观察变量等功能

所谓 Shell 内建命令,就是由 Bash 自身提供的命令,而不是文件系统中的某个可执行文件

通常来说,内建命令会比外部命令执行得更快,执行外部命令时不但会触发磁盘 I/O,还需要 fork 出一个单独的进程来执行,执行完成后再退出。而执行内建命令相当于调用当前 Shell 进程的一个函数。

不要试图用脑子记住所有的命令,这不可能也不需要。判断一个命令是不是内建命令只需要借助于命令 type 即可

点号用于执行某个脚本,甚至脚本没有可执行权限也可以运行

alias 可用于创建命令的别名,若直接输入该命令且不带任何参数,则列出当前用户使用了别名的命令。

任务前后台切换:bg、fg、jobs
该命令用于将任务放置后台运行,一般会与 Ctrl+z、fg、&符号联合使用

declare、typeset
这两个命令都是用来声明变量的,作用完全相同。

Shell 又被称为弱类型编程语言

若使用 declare 命令,可以用-i 参数声明整型变量,

使用-r 声明变量为只读

使用-a 声明变量

使用-F、-f 显示脚本中定义的函数和函数体,

echo 用于打印字符

该命令会打印出引号中的内容,并在最后默认加上换行符

使用-n 参数可以不打印换行符。

默认情况下,echo 命令会隐藏-e 参数(禁止解释打印反斜杠转义的字符)。比如“\n”代表新的一行,如果尝试使用 echo 输出新的一行,在不加参数的情况下只会将“\n”当作普通的字符,若要打印转义字符,则需要通过使用-e 参数来允许。

从一个循环(for、while、until 或者 select)中退出。break 后可以跟一个数字 n,代表跳出 n 层循环,n 必须大于 1,如果 n 比当前循环层数还要大,则跳出所有循环

continue
停止当前循环,并执行外层循环(for、while、until 或者 select)的下一次循环。continue 后可以跟上一个数字 n,代表跳至外部第 n 层循环

将所跟的参数作为 Shell 的输入,并执行产生的命令:eval

内建命令 exec 并不启动新的 Shell,而是用要被执行的命令替换当前的 Shell 进程,并且将老进程的环境清理掉,而且 exec 命令后的其他命令将不再执行

假设在一个 Shell 里面执行了 exec echo ''Hello''命令,在正常地输出一个“Hello”后 Shell 会退出,因为这个 Shell 进程已被替换为仅仅执行 echo 命令的一个进程,执行结束自然也就退出了

exec 典型的用法是与 find 联合使用,用 find 找出符合匹配的文件,然后交给 exec 处理,如下所示:

在 Shell 脚本中使用 exit 代表退出当前脚本

该命令可以接受的参数是一个状态值 n,代表退出的状态

如果不指定,默认状态值是 0

使用 $?可以取出之前命令的退出状态值

要说明的是,即便子 Shell 确实读取到了父 Shell 中变量 var 的值,也只是值的传递,如果在子 Shell 中尝试改变 var 的值,改变的只是 var 在子 Shell 中的值,父 Shell 中的该值并不会因此受到影响,你可以认为父 Shell 和子 Shell 都各自拥有一个叫 var 的变量,它们恰巧名字相同而已。

发送信号给指定 PID 或进程:kill

Linux 操作系统包括 3 种不同类型的进程

第一种是交互进程,这是由一个 Shell 启动的进程,既可以在前台运行,也可以在后台运行

第二种是批处理进程,与终端没有联系,是一个进程序列

第三种是监控进程,也称系统守护进程,它们往往在系统启动时启动,并保持在后台运行

let 是 Shell 内建的整数运算命令

pwd 命令会打印当前工作目录的绝对路径名

如果使用-P 选项,打印出的路径名中不会包含符号连接

如果使用了-L 选项,打印出的路径中可以包含符号连接

声明局部变量:local

该命令用于在脚本中声明局部变量,典型的用法是用于函数体内,其作用域也在声明该变量的函数体中。如果试图在函数外使用 local 声明变量,则会提示错误。

从标准输入读取一行到变量:read

实际上 read 可以使用-p 参数代替

如果不指定变量,read 命令会将读取到的值放入环境变量 REPLY 中

另外要记住,read 是按行读取的,用回车符区分一行,你可以输入任意文字,它们都会保存在变量 REPLY 中。

定义函数返回值:return

典型的用于函数中,常见用法是 return n,其中 n 是一个指定的数字,使函数以指定值退出

如果没有指定 n 值,则返回状态是函数体中执行的最后一个命令的退出状态。

向左移动位置参数:shift

假设一个脚本在运行时可以接受参数,那么从左到右第一个参数被记作 $1,第二个参数为 $2,以此类推,第 n 个参数为 $N

所有参数记作$@或$*,参数的总个数记作 $#,而脚本本身记作 $0。

shift 命令可以对脚本的参数做“偏移”操作

使用 ulimit 可以控制进程对可用资源的访问

默认情况下 Linux 系统的各个资源都做了软硬限制,其中硬限制的作用是控制软限制(换言之,软限制不能高于硬限制)

使用 ulimit-a 可以查看当前系统的软限制(使用命令 ulimit-a-H 可查看系统的硬限制)

使用 ulimit 直接调整参数,只会在当前运行时生效,一旦系统重启,所有调整过的参数就会变回系统默认值。所以建议将所有的改动放在 ulimit 的系统配置文件中。

测试表达式:test

该命令用于测试表达式 EXPRESSION 的值,根据测试结果返回 0(测试失败)或 1(测试成功)。

◆ 第 12 章 Bash Shell 的安装

Linux 是学习 Bash Shell 的天然环境,但是借助工具,在 Windows 下同样可以运行 bash

最著名的工具是 Cygwin,它是模拟类 UNIX 环境的软件,最初由 Cygnus Solution 公司开发,目的在于通过重新编译将 Linux 系统上的软件移植到 Windows 上。

◆ 第 13 章 Shell 编程基础

可以使用 local 内建命令来“显式”的声明局部变量,但仅限于函数内使用

每个 Shell 都有自己的变量空间,彼此互不影响

环境变量通常又称“全局变量”,以区别于局部变量

为了让子 Shell 继承当前 Shell 的变量,则可以使用 export 内建命令将其导出为环境变量

bash 中默认包含有几十个预设的环境变量,这里挑选常见的一些予以介绍

变量:BASH
说明:Bash Shell 的全路径。

变量:BASH_VERSION
说明:Bash Shell 的版本。

变量:CDPATH
说明:用于快速进入某个目录。

变量:EUID
说明:记录当前用户的 UID

变量:FUNCNAME
说明:在用户函数体内部,记录当前函数体的函数名。

变量:HISTCMD
说明:记录下一条命令在 history 命令中的编号

变量:HISTFILE
说明:记录 history 命令记录文件的位置

变量:HISTFILESIZE
说明:设置 HISTFILE 文件记录命令的行数。

变量:HISTSIZE

Shell 采用了“命令缓冲区”来记录所有已运行过的命令,在缓冲区满或退出 Shell 时才将缓冲区的记录写到 HISTFILE 文件中。缓冲区的大小使用 HISTSIZE 定义。

变量:HOSTNAME
说明:展示主机名。

变量:HOSTTYPE
说明:展示主机的架构,是 i386、i686,还是 x86_64 等。

变量:MACHTYPE
说明:主机类型的 GNU 标识,这种标识有统一的结构。一般来说是“主机架构-公司-系统-gnu”

变量:LANG
说明:设置当前系统的语言环境,其实就是 language 的意思。

变量:PWD
说明:记录当前目录。

变量:OLDPWD
说明:记录之前目录,这个值是什么由之前所在的那个目录决定。

变量:PATH

代表命令的搜索路径

变量:PS1
说明:命令提示符,默认值是[\u@\h \W]$,其中\u 是用户名、\h 是主机名、\W 是当前工作目录的 basename、$是用户 UID 的替换字符:如果 UID 是 0 则替换成“#”,否则替换成“$”,所以此处具体显示出来就是“[root@localhost ~]#”。该变量可以有很多种组合,可以根据自己的喜好进行定制。

[插图]

[插图]

Shell 中的变量必须以字母或者下划线开头,后面可以跟数字、字母和下划线,变量长度没有限制。

定义变量:变量名=变量值 

注意点一:变量名和变量值之间用等号紧紧相连,之间没有任何空格 

注意点二:当变量中有空格时必须用引号括起,否则会出现错误 

变量的取值也很简单,只需要在变量名前加上$符号既可,严谨一点的写法是${}

由于 Shell 具有“弱变量”的特性,因此即便在没有预先声明变量的时候也是可以引用的,而且没有任何报错或者提醒,这可能会造成脚本中引用不正确的变量,从而导致脚本异常,但是却很难找出原因。

设置变量必须先声明再使用 
[root@localhost ~]# shopt-s-o nounset

取消变量指的是将变量从内存中释放,使用的命令是 unset,后面跟变量名

函数也是可以被取消的,所以 unset 后面还可以跟上函数名以取消函数

Shell 中还有一些预先定义的特殊只读变量,它们的值只有在脚本运行时才能确定

首先是“位置参数”,位置参数的命名简单直接,比如,脚本本身为 $0,第一个参数为 $1,第二个参数为 $2,第三个为 $3,以此类推。当位置参数的个数大于 9 时,需要用${}括起来标识,比如说第10个位置参数应该记为${10

$#表示脚本参数的个数总和,$@或 $*表示脚本的所有参数。

脚本或命令返回值:$?

Shell 中的数组对元素个数没有限制,但只支持一维数组,这一点和很多语言不同。

数组的定义方法如下:用 declare 命令定义数组 Array

最简单的操作就是数组取值,其格式为:${数组名[索引]}。

${Array[@]}得到的是以空格隔开的元素值,而${Array[*]}的输出是一整个字符串。

利用“@”或“*”字符,可以将数组扩展成列表,然后使用“#”来获取数组元素的个数,

可以截取某个元素的一部分,对象可以是整个数组或某个元素。

连接数组:将若干个数组进行拼接操作。

替换元素:将数组内某个元素的值替换成其他值。

取消数组或元素:和取消一般变量一样,取消一个数组的方式也使用 unset 命令。

只读变量又称常量,是通过 readonly 内建命令创建的变量。

变量的作用域又叫“命名空间”,表示变量(identifier,标识符)的上下文。

在 Linux 系统中,不同进程 ID 的 Shell 默认为一个不同的命名空间

Shell 中有两类字符,一类是普通字符,在 Shell 中除了本身的字面意思外没有其他特殊意义,即普通纯文本(literal);另一类即元字符(meta),是 Shell 的保留字符,在 Shell 中有着特殊意义

Shell 中的转义符是反斜线“\”,使用转义符的目的是使转义符后面的字符单纯地作为字符出现,而不解释其特殊的含义

[插图]

引用是指将字符串用某种符号括起来,以防止特殊字符被解析为其他意思

比如说上一小节中的转义符就是一种引用

Shell 中一共有 4 种引用符,分别是双引号、单引号、反引号(在键盘上和波浪号位于同一个键)和转义符。

其中双引号又叫“部分引用”或“弱引用”,可以引用除 $ 符、反引号、转义符之外的所有字符

单引号又叫“全引用”或“强引用”,可以引用所有字符

反引号则会将反引号括起的内容解释为系统命令

部分引用是指用双引号括起来的引用。在这种引用方式中,$ 符、反引号(`)、转义符(\)这 3 种特殊字符依然会被解析为特殊意义

全引用是指用单引号括起来的引用。单引号中的任何字符都只当作是普通字符(除了单引号本身,也就是说单引号中间无法再包含单引号,即便用转义符转义单引号也不行)

命令替换是指将命令的标准输出作为值赋给某个变量

Shell 中有两种方式可以完成命令替换,一种是反引号(`),一种是 $()

要注意的是,$()仅在 Bash Shell 中有效,而反引号可在多种 UNIX Shell 中使用。

Shell 中的运算符主要有比较运算符(用于整数比较)、字符串运算符(用于字符串测试)、文件操作运算符(用于文件测试)、逻辑运算符、算术运算符、位运算符、自增自减运算符等

算术运算符指的是加、减、乘、除、余、幂等常见的算术运算,以及加等、减等、乘等、除等、余等复合算术运算

要特别注意的是,Shell 只支持整数计算

常见的位运算有左移运算、右移运算、按位与、按位或、按位非、按位异或等运算。

按位与运算(&),是将两个整数写成二进制的形式,然后同位置相比较,只有当对应的二进制值都为 1 时,结果才为 1。

按位或运算(|),是将两个整数写成二进制的形式,然后同位置相比较,只要对应的位置有 1,结果就为 1。

按位异或运算(^),是将两个整数写成二进制的形式,然后同位置相比较,只要对应的位置同为 0 或同为 1,结果就为 0,否则为 1。

按位非(~)的计算方式比较麻烦,这里有个快捷的计算公式:“~a”的值为“-(a+1)”。

自增自减运算主要包括前置自增、前置自减、后置自增、后置自减等

$[]和$(())类似,可用于简单的算术运算

expr 命令也可用于整数运算

和其他算术运算方式不同,expr 要求操作数和操作符之间使用空格隔开(否则只会打印出字符串),所以特殊的操作符要使用转义符转义(比如*)。
expr 支持的算术运算符有加、减、乘、除、余等,

declare 是 Shell 的内建命令,通过它们也能进行整数运算

它和显式使用 declare 定义变量的差别是很大的。

1 里的变量 I 未经正式定义便赋值“1+1”,对 Shell 来说,此时的“1+1”只是一个字符串,和“abc”无异,所以打印出来也只是字符串。而例 2 中,使用 declare 显式地定义了整数变量 J(-i 参数指定变量为“整数”),此时再赋值“1+1”,Shell 会将后面的字符串解析成算术运算,所以打印出的值是算术表达式的计算值。

算术扩展是 Shell 提供的整数变量的运算机制,是 Shell 的内建命令之一

基本语法如下:
​​$((算术表达式))​​

其中的算术表达式由变量和运算符组成,常见的用法是显示输出和变量赋值

Linux 下的 bc 正是这样一款专门用于高精度计算的工具

与其说 bc 是一个命令或者工具,不如说它是一门语言,bc 的 man 文件对它的描述是:“一款高精度计算语言(An arbitrary precision calculator language)”。

设置显示的小数位数 
scale=3

默认情况下 bc 并不显示小数部分,必须设置要显示的小数位数

希望一次性处理多个计算,只需要创建一个文件,并按行写好需要计算的表达式就可以了

如果想让脚本变得更灵活,也可以使用 read 命令动态地给变量赋值。

Shell 中除了普通字符外,还有很多具有特殊含义和功能的字符,在使用它们时要特别注意其含义和作用。

通配符用于模式匹配,常见的通配符有*、?和用[]括起来的字符序列

Shell 使用#作为注释符。

如果出现#后连着!,也就是“#!”不会被理解成注释,因此,其后跟着的部分必须是某个解释器的路径,而且“#!”必须出现在整个脚本的第一行。

.通配符扩展
用于匹配多个排列组合的可能。

比如坐标,横坐标是 x1、x2、x3,纵坐标是 y1、y2、y3,那么所有可能的坐标就是{x1,x2,x3}{y1,y2,y3}。

还可以用于匹配不同的文件,文件名的特征是只有其中一部分不同。比如 file_A、file_B,就可以用 file_{A,B}来匹配

控制字符即 Ctrl+KEY 组合键一起使用,用于修改终端或文本显示。但是控制字符在脚本中不能使用,也就是说控制字符是交互式使用的

[插图]

引号
反引号用于命令替换,和 $()的作用相同,表示返回当前命令的执行结果并赋值给变量。

位置参数的含义如下。
$0:脚本名本身。
$1、$2……${10}:脚本的第一个参数、第二个参数……第十个参数。
$#:变量总数。
$*、$@:显示所有参数。
$?:前一个命令的退出的返回值。
$!:最后一个后台进程的 ID 号。

◆ 第 14 章 测试和判断

测试的第一种使用方式是直接使用 test 命令,该命令的格式如下:
​​test expression​​

其中 expression 是一个表达式,可以是算术比较、字符串比较、文本和文件属性比较等

第二种测试方式是使用“[”启动一个测试,再写 expression,再以“]”结束测试。需要注意的是,左边的括号“[”后有个空格,右括号“]”前面也有个空格,如果任意一边少了空格都会造成 Shell 报错

增加代码的可读性,推荐使用第二种方式,而且这种方式更容易与 if、case、while 这些条件判断的关键字连用

​​#文件测试方法一 
test file_operator FILE
#文件测试方法二 
[ file_operator FILE ]​​

[插图]

Shell 中的字符串比较主要有等于、不等于、大于、小于、是否为空等测试

[插图]

整数测试是一种简单的算术运算,作用在于比较两个整数的大小关系,测试成立则返回 0,否则返回非 0 值

[插图]

逻辑测试主要有逻辑非、逻辑与、逻辑或 3 种

[插图]

[插图]

if expression; then
 command
fi​​
如果 expression 测试返回真,则执行 command

if expression; then
 command
else
 command
fi​​

if expression1; then
 command1
else
 if expression2; then
 command2
 else
 command3
 fi
fi​​

case VAR in
var1) command1 ;;
var2) command2 ;;
var3) command3 ;;
...
*) command ;;
esac​​

◆ 第 15 章 循环

在现实生活中,如果要某人不断地重复做某一件事情,那么他很快就会感觉到厌倦,并慢慢失去兴趣,随后效率也会渐渐降低,并越来越容易出错。而计算机在这方面就显得非常有“天赋”:它天生就适合做重复的事情,并乐此不疲。

Shell 中的循环主要有 for、while、until、select 几种。

​​for VARIABLE in (list)
do
 command
done​​

比如说上例中 1 到 5 可以用{1..5}表示

还可以使用 seq 命令结合命令替换的方式生成列表

其实列表 for 循环中 in 后面的内容可以是任意命令的标准输出

[root@localhost ~]# cat for_list05.sh
#!/bin/bash
for VAR in $(ls)
do
 ls-l $VAR
done​​

不带列表的 for 循环的结构如下所示:
​​for VARIABLE
do
 command
done​​

使用不带列表的 for 循环时,需要在运行脚本时通过参数的方式给 for 循环传递变量值

该语法虽然可以工作,但是可读性较差,所以不建议使用。可利用特殊变量 $@改写上述结构

Shell 支持类 C 的 for 循环

和 for 循环一样,while 循环也是一种运行前测试语句,相比 for 循环来说,其语法更为简单

while expression
do
 command
done​​

按行读取文件是 while 一个非常经典的用法,常用于处理格式化数据。

方法一 
while ((1))
do
 command
done​​
​​#方法二 
while true
do
 command
done​​
​​方法三 
while :
do
 command
done​​

我们可以利用 while 的无限循环实时的监测系统进程,以保证系统中的关键应用一直处于运行状态。

until 循环也是运行前测试,但是 until 采用的是测试假值的方式,当测试结果为假时才继续执行循环体,直到测试为真时才停止循环

until expression
do
 command
done​​

和 while 的无限循环相反,until 的无限循环的条件是判断假成立时退出

​​#方法一 
until ((0))
do
 command
done
#方法二 
until false
do
 command
done​​

select 是一种菜单扩展循环方式,其语法和带列表的 for 循环非常类似,基本结构如下:

select MENU in (list)
do
 command
done​​

当程序运行到 select 语句时,会自动将列表中的所有元素生成为可用 1、2、3 等数选择的列表,并等待用户输入。用户输入并回车后,select 可判断输入并执行后续命令。如果用户在等待输入的光标后直接按回车键,select 将不会退出而是再次生成列表等待输入。

除了确实必要的情况下,不建议使用多层嵌套(三层以上的嵌套)。

break 用于终止当前整个循环体。

continue 语句用于结束当前循环转而进入下一次循环

◆ 第 16 章 函数

函数是 Shell 脚本中自定义的一系列执行命令,一般来说函数应该设置有返回值(正确返回 0,错误返回非 0。

使用函数最大的好处是可避免出现大量重复代码,同时增强了脚本的可读性

函数的返回值又叫函数的退出状态,实际上是一种通信方式。

除了在脚本运行时给脚本传入位置参数外,还可以使用内置命令 set 命令给脚本指定位置参数的值(又叫重置)。一旦使用 set 设置了传入参数的值,脚本将忽略运行时传入的位置参数(实际上是被 set 命令重置了位置参数的值)

对某些很常用的功能,必须考虑将其独立出来,集中存放在一些独立的文件中,这些文件就称为“函数库”。

为了和一般函数区分开来,在实践中建议库函数使用下划线开头。

由于 Shell 是一门面向过程的脚本型语言,而且用户主要是 Linux 系统管理人员,所以并没有非常活跃的社区

很多 Linux 发行版中都有/etc/init.d 目录,这是系统中放置所有开机启动脚本的目录,这些开机脚本在脚本开始运行时都会加载/etc/init.d/functions 或/etc/rc.d/init.d/functions 函数库(实际上这两个函数库的内容是完全一样的)

实际上 functions 函数库中定义了 27 个函数,表 16-1 中列举了常见的 17 个。

[插图]

具有“递归”功能的函数则被称为“递归函数”。递归函数的典型特征为:在函数体中继续调用函数自身。

递归函数一定要有结束递归的条件,当满足该条件时,递归就会终止

典型的递归函数的结构如下所示:
​​function recursion() {
 recursion
 conditionThatEndTheRecursion #停止递归的条件 
}​​

数学中有个经典的需要使用递归算法计算的公式是:阶乘。

递归另一个典型的例子是“汉诺塔”游戏

嵌套函数天生的结构就注定了其晦涩的可读性,在不少大公司内部的开发规范中也明确规定了不允许使用递归,所以在实际工作中要尽量避免使用递归。不过实际上你更可能根本没有使用递归的机会—从笔者多年从事 Linux 系统管理的经验来看,基本上不存在必须使用递归才能解决问题的场景。

◆ 第 17 章 重定向

计算机最基础的功能是可以提供输入输出操作

常见的输入设备有键盘、鼠标、扫描仪等,对于 Linux 系统来说,通常以键盘为默认输入设备,又称标准输入设备;计算机常见的输出设备有显示器、蜂鸣器、打印机等,而 Linux 系统则以显示器为默认的输出设备,又称标准输出设备

所谓“重定向”,就是将原本应该从标准输入设备(键盘)输入的数据,改由其他文件或设备输入;或将原本应该输出到标准输出设备(显示器)的内容,改而输出到其他文件或设备上。

文件标识符是重定向中很重要的一个概念,Linux 使用 0 到 9 的整数指明了与特定进程相关的数据流

系统在启动一个进程的同时会为该进程打开三个文件:标准输入(stdin)、标准输出(stdout)、标准错误输出(stderr),分别用文件标识符 0、1、2 来标识

如果要为进程打开其他的输入输出,则需要从整数 3 开始标识

默认情况下,标准输入为键盘,标准输出和错误输出为显示器。

简单来说,I/O 重定向可以将任何文件、命令、脚本、程序或脚本的输出重定向到另外一个文件、命令、程序或脚本。

[插图]

如果指定的重定向文件存在且内容不为空,重定向并不会清空原文件内容,而是将命令的输出新增到原文件的尾部。

标识输出重定向的作用是将一个标识的输出重定向到另一个标识的输入。

这时可以利用系统中的一个特殊设备/dev/null,将所有错误输出重定向到该设备中—系统会将任何输入到该设备的内容全部删除(就像一个宇宙黑洞)

标准输入重定向可以将原本应由从标准输入设备中读取的内容转由文件内容输入,也就是将文件内容写入标准输入中。

简单地说管道就是将一个命令的输出作为另一个命令的输入,借此方式可通过多个简单命令的共同协作来完成较为复杂的工作

exec 还可以用于 I/O 重定向

[插图]

实际上,文件标识符类似于很多编程语言中的“句柄”。在 Linux 中,文件标识符也是一种“设备”,这个标识符会出现在/dev/fd 目录中

进入这个目录,可以看到 3 是一个到该文件的软链接

主动打开的文件标识符需要主动关闭,否则除了系统重启,该文件标识符会一直被占用

由于在脚本中无法使用组合键,因此要终止输入就需要用到 Here Document

◆ 第 18 章 脚本范例

从字面上就能大概猜出 expect 的用途,即“期待”系统的输出,且对应地发送输入作为响应。

netcat 被誉为网络工具中的“瑞士军刀”,体积虽小但是功能强大。netcat 最简单的功能是用于端口扫描

系统在启动时将根据当前的运行级(runlevel X)确定运行在/etc/rc.d/rcX.d 目录下的脚本(都是到/etc/init.d 目录中的文件软链接)。

和一般的 shell 脚本不同,init 脚本需要满足一定的格式,最基本的要求是,脚本必须接收至少两个参数:start 和 stop,分别用于启动和停止服务。

LVM 的快照(snapshot)功能可以很好地解决这个问题。在对一个 LV 创建 snapshot 时,仅会复制其中的元数据,而不会有任何真实数据的复制,所以创建过程几乎是实时的

当原 LV 有写操作时,数据会写到快照中而不是原 LV 中(写时复制机制,Copy On Write, CoW),从而保证了原 LV 中数据的一致性。为了确保数据的一致(这里特指 MySQL 数据),在对其做快照之前也需要对数据库进行锁定操作,做完快照后再解除锁。

由于快照的过程极为迅速,所以短时间的数据库锁定并不会对前台应用造成影响。

最后修改:2023 年 02 月 10 日
如果觉得我的文章对你有用,请随意赞赏