在Java中,主要有三种IO模型,分别是同步阻塞IO (BIO-Blocking I/O)、同步非阻塞IO (NIO-non-blocking I/O)和异步非阻塞IO (AIO-Asynchronous I/O)。BIO 为同步阻塞,一个连接一个线程,资源要求高,适用于连接少且固定的架构。NIO 是同步非阻塞,有 ChannelBufferSelector 三大核心,通过多路复用技术,一个线程可处理多个操作,适用于连接多且短的架构。AIO 是异步非阻塞,采用 Proactor 模式,适用于连接多且长的架构。

Java BIO基本介绍

  1. Java BIO就是传统的Java IO编程,其相关的类和接口在java.io包下。
  2. BIOBlocking I/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。
  3. BIO方式适用于数据数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解。

Java BIO 工作机制

BIO编程简单流程

  1. 服务器启动一个ServerSocket
  2. 客户端启动一个Socket对服务器进行通信,默认情况下,服务器端需要对每一个客户端建立一个线程与之通信。
  3. 客户端发出请求后,先咨询服务器是否有线程相应,如果没有则会等待,或者被拒绝。
  4. 如果有响应,客户端线程会等待请求结束后,再继续执行。

Java BIO应用实例

实例说明

  1. 使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯。
  2. 要求使用线程池机制改善,可以连接多个客户端。
  3. 服务端可以接受客户端发送的数据。

代码实现

下面是服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.wotzc.bio;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOServer {

public static void main(String[] args) throws IOException {
// 创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
// 创建ServerSocket并且监听6666端口
ServerSocket serverSocket = new ServerSocket(6666);
while (true) {
// 监听---一直等待客户端连接
Socket socket = serverSocket.accept();
// 连接来了之后,启用一个线程去执行里面的方法
executorService.execute(() -> {
try {
// 获取客户端发送过来的输入流
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
int read = inputStream.read(bytes);
// 读取发送过来的信息并打印
if (read != -1) {
System.out.println(new String(bytes, 0, read));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 断开通讯
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
}

下面是客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.wotzc.bio;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class BIOClient {

public static void main(String[] args) throws IOException {
// 创建一个socket,并绑定本地ip及6666端口
Socket socket = new Socket("127.0.0.1", 6666);
// 获取output流
OutputStream outputStream = socket.getOutputStream();
// 向输出流中写入
outputStream.write("Hello World!".getBytes());
outputStream.flush();
// 关闭连接
socket.close();
}

}

通过以上代码能够看到:在服务端的控制台中能有信息打印出来,且能得出结论:如果客户端一直没有请求发送,则服务端一直在等待;如果发送过来的数据是空的话,就会引起线程的消耗。

上述服务端可以通过线程池机制改善(实现多个客户连接服务器)。

实例说明:

  1. 使用 BIO 模型编写一个服务器端,监听 6666 端口,当有客户端连接时,就启动一个线程与之通讯。
  2. 要求使用线程池机制改善,可以连接多个客户端。
  3. 服务器端可以接收客户端发送的数据(telnet 方式即可)。
  4. 代码演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.wotzc.bio;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOServer {

public static void main(String[] args) throws Exception {
//线程池机制
//思路
//1. 创建一个线程池
//2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true) {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
//监听,等待客户端连接
System.out.println("等待连接....");
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
//就创建一个线程,与之通讯(单独写一个方法)
newCachedThreadPool.execute(new Runnable() {
public void run() {//我们重写
//可以和客户端通讯
handler(socket);
}
});
}
}

//编写一个handler方法,和客户端通讯
public static void handler(Socket socket) {
try {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
//通过socket获取输入流
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while (true) {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
System.out.println("read....");
int read = inputStream.read(bytes);
if (read != -1) {
System.out.println(new String(bytes, 0, read));//输出客户端发送的数据
} else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("关闭和client的连接");
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

}

out:

1
2
3
4
5
6
7
8
9
10
11
12
13
服务器启动了
线程信息id = 1名字 = main
等待连接....
连接到一个客户端
线程信息id = 1名字 = main
等待连接....
线程信息id = 12名字 = pool-1-thread-1
线程信息id = 12名字 = pool-1-thread-1
read....
Hello World!
线程信息id = 12名字 = pool-1-thread-1
read....
关闭和client的连接

Java BIO问题分析

  1. 每个请求都需要创建独立的线程,与对应的客户端进行数据,业务处理,然后再数据
  2. 当并发数较大时,需要创建大量的线程来处理连接,系统资源占用较大。
  3. 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在操作上,造成线程资源浪费。

一次在项目开发过程中需要用到加密相关的功能,然后公司就给了一个jar包,我们上传私服就通过pom引入在项目中。并基于这个新依赖开发相关的功能,我们在本地IDEA编译测试都没问题,然后我们打包上传到虚拟机上运行时,调用加密方法的时候会报错,并且错误信息给不了我们任何提示,然后在本地测试却是正常的。

Linux虚拟机上我们服务的运行方式如下:

  1. 我们通过本地的IDEA把项目打成zip包上传到虚拟机上
  2. 我们通过运行启动脚本来启动我们的服务
    • 启动脚步先把zip文件解压到指定的lib文件夹
    • 然后用nohup命令启动Java服务

我们通过启动脚本启动我们的服务时,服务起不起来,而且关键的错误信息没有打印。于是我们先调整了日志文件,让关键信息能够打印,调整日志文件后我们看到了关键的错误信息:NoSuchMethodError

通过这个错误信息,就很容易想到应该是jar包冲突了,但是很奇怪的就是我们在本地测试没问题,但是部署到虚拟机上就报错,应该是类的加载顺序不一致导致的。

复习一下类加载机制

我们写的 Java 应用代码,一般是通过 App ClassLoader 应用加载器进行加载,它不会自己先去加载它,而是通过 Extension ClassLoader 扩展类加载器进行加载(其中扩展类加载器又会去找 Bootstrap ClassLoader 启动类加载器进行加载),只有父加载器无法加载情况下,才会让下级加载器进行加载。

当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。

ClassLoader

Java 使用的是双亲委派加载机制,通过查看 ClassLoader 类,可以对此有所了解。

类被成功加载后,将被放入到内存中,内存中存放 Class 实例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先,检查 class 是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
// 如果没有被加载
long t0 = System.nanoTime();
try {
if (parent != null) {
// 寻找 parent 加载器
c = parent.loadClass(name, false);
} else {
// 如果父加载器不存在,则委托给启动类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
// 如果仍然无法加载,才会尝试自身加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

类加载顺序

从代码中了解到,如果某个名字的类被加载后,类加载器是不会再重新加载,所以我们的问题根本原因可以是出现在:

我们本地通过IDEA启动时,加载的那个class文件正好是我们需要的使用的,但是当我们在虚拟机上启动的时候,首先加载的那个类是其他jar里面的同名类,并且全类名一样,并非我们需要的。

通过查阅文章:

跟JAR文件的文件名有关。按照字母的顺序加载JAR文件。有了这个类以后,后面的类则不会加载了。

jvm 加载包名和类名相同的类时,先加载classpath中jar路径放在前面的,包名类名都相同,那jvm没法区分了,如果使用ide一般情况下是会提示发生冲突而报错,若不报错,只有第一个包被引入(在classpath路径下排在前面的包),第二个包会在classloader加载类时判断重复而忽略。

查看加载顺序

jvm 启动脚本中,添加 -verbose 参数或者 -XX:+TraceClassLoading

按理说加载顺序按照字母顺序加载,预发环境还是能够跟本地开发一样,加载到我们需要的类。实际上,加载器加载到的是另一个类,导致应用无法启动。

通过查找资料

问题就是jar的加载顺序问题,而这个顺序实际上是由文件系统决定的,linux内部是用inode来指示文件的。

这种储存文件元信息的区域就叫做inode,中文译名为”索引节点”。每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。

Unix/linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。


相关知识点

maven包加载顺序和jvm类加载顺序

一、mvaven jar包加载顺序

Maven对版本不同的相同依赖包生效优先级:

  1. 不同依赖层级深度的遵从【最短路径优先】原则。
  2. 具有相同依赖层级深度的遵从pom中【最先声明优先】原则。

二、JVM类加载顺序

相同的类指:类的全限定名一样
问题:怎么优先加载自己写的类

  1. 解析:

    class文件所在位置, 直接在classpath下,在jar包中:

  • a.相同的类, 一个在工程src路径下, 一个在jar包中: 此时优先加载工程src文件。
  • b.相同的类, 都在jar包中:此时按照jar的装载顺序,它加载的顺序完全取决于操作系统!

jar包冲突的解决办法:

  1. 如果引起冲突类的jar包可以进行排除,在引入的pom文件中进行exclude
  2. 如果引起冲突的jar包排除了会引起项目启动报错,即两个jar包必须共存
    • 如果冲突的class文件数量不多,则可以将存在冲突并且我们需要的class文件,从jar中类copysrc目录下
    • 如果冲突的class文件数量很多,上述方式不现实则可以考虑通过maven shade plugin插件改包名

现象:

我们在对一些接口进行压测时,会偶发性的出现502 Bad Gateway 的报错,大约是2亿笔交易中会有2-3笔出现502 Bad Gateway 的报错。

HTTP 数据流向如下:

1
[前置服务] --- [Ingress] --- [网关] --- [聚合服务] --- [基础服务]

我们最开始怀疑这个错误是ingress爆出来的,怀疑是ingress导致的,然后我们就把ingress去掉,继续压测试试,此时的HTTP数据流如下:

1
[前置服务] --- [网关] --- [聚合服务] --- [基础服务]

此时,502 Bad Gateway 的报错虽然没有出现,但是会偶发性的出现NoHttpResponseException的报错,大搞也是2亿笔出现3-4笔的NoHttpResponseException的报错。然后我们就怀疑502 Bad Gateway 报错和NoHttpResponseException报错本质上是一样的。

NoHttpResponseException错误详情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
org.apache.http.NoHttpResponseException: target failed to respond
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:141)
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56)
at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259)
at org.apache.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:163)
at org.apache.http.impl.conn.CPoolProxy.receiveResponseHeader(CPoolProxy.java:157)
at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:273)
at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:125)
at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:272)
at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186)
at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89)
at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)

分析:

看一下报错的源代码,相关注释提示服务端主动关闭了连接导致的:

通过WireShark抓包,我们可以看到的确是服务端已经发送了FIN,而客户端依旧使用了这个链接进行访问,所以就收到了RST,造成整个连接失败。

遇到问题,首先打开Debug日遇到问题,首先打开Debug日志,我们可以看到这是因为尝试读数据是没读到,所以链接失败了。

  • 查阅TCP资料,我们可以知道一般读到end of stream都是因为服务端主动关闭连接造成的,此时客户端应该主动关闭连接,而不是继续复用。
  • 通过网络抓包,我们可以看到的确是服务端已经发送了FIN,而客户端依旧使用了这个链接进行访问,所以就收到了RST,造成整个连接失败

那么为什么会继续复用已经关闭的链接呢?

我们继续从代码出发,通过日志,我们可以看出每次链接前都有次向连接池请求连接的过程

进入连接池管理代码,我们可以看到这边有两次检测是否要关闭:

首先检测有没有配置validateAfterInactivity(默认为2000ms),如果超过了指定的时间间隔就进行validate

  • validate就是尝试读一次(超时1ms)来判断有没有失效
  • 如果读到**-1**(end of stream)或者发生IO异常了就认为失效了
  • 如果超时了就认为还是正常(因为httpclient阻塞读)

所以出现链接异常会有2种情况

  1. keepalive时间太短,因此httpClient以为还能用就没有检测直接使用了,从日志可以看出,根本没有validate过程,在TCP的连接上表现为FIN后直接发起请求
  2. 刚好服务端释放请求时发起访问。如之前例子所示,在服务端还没发送FIN是进行了validate,但是刚好在正式发送请求前服务端发送了FIN,因此造成了RST。或者是如下正好在服务端关闭时发送了请求

就要提一下Keepalive机制。KeepaliveHTTP的连接复用机制,在HTTP1.0时代,每个请求经过三次握手后,只会传输一次HTTP请求和响应报文后,就进入四次挥手关闭连接了。而TCP建立连接和关闭连接的代价是比较大的,导致HTTP1.0的通道利用率较低,时延较高。针对这个问题,退出了Keepalive机制,一个TCP连接建立后,可以在上面发送多个HTTP报文,只有这个TCP连接的空闲时间达到超时时间,才会被关闭。HTTP1.1默认开启Keepalive。这里的关闭行为可能发生在客户端和服务端,比如客户端的Keepalive超时时间更短,则客户端就会先关闭连接,如果服务端配置的Keepalive超时时间更短,则服务端就会先关闭连接。

乍看起来无论那一边关闭连接都没什么问题,但是还是有细节需要注意。比如服务端关闭连接,发送FIN包,在这个FIN包发送但是还未到达客户端期间,客户端如果继续复用这个TCP连接,发送HTTP请求报文的话,服务端会因为在四次挥手期间不接收报文而发送RST报文给客户端,客户端收到RST报文就会提示异常。

解决办法:

我们调整前置、Ingress以及网关等服务的keep-alive-timeout的时间,确保前置小于IngressIngress小于网关、网关小于聚合。即客户端设置的keep-alive-timeout的时间小于服务端的keep-alive-timeout的时间。

Java 应用程序通常在复杂且动态的环境中运行,因此监控其性能并有效诊断问题至关重要。幸运的是,Java 开发工具包 (JDK) 附带了一套强大的工具来实现此目的。在本指南中,我们将探索四个基本工具:jpsjstatjcmdjmapjstack。我们将讨论它们的功能、示例用法以及如何有效地解释它们的输出。

jps

jps(Java Process Status) 工具列出本地计算机上的 Java 虚拟机 (JVM) 进程。它提供进程 ID (PID) 和正在执行的主类或 JAR 文件等信息。

Usage:

1
jps [options]

Sample Usage:

1
2
$ jps -l
12345 com.example.MainApp

理解输出:

  • 第一列表示 PID
  • 第二列显示主类的完全限定类名或 JAR 文件名

jstat

jstat (JVM Statistics Monitoring Tool) 是一个命令行工具,提供有关 JVM 内部统计信息,例如垃圾收集、类加载、编译器活动等。

Usage:

1
jstat [options] <vmid> [<interval> [<count>]]

Sample Usage:

1
$ jstat -gcutil 12345 1000 10

-gcutil 提供垃圾收集统计数据,每秒统计1次,共统计两次

1
2
3
4
5
6
7
8
9
10
11
12
13
(base) mac7@TZC-Mac ~ % jstat -options
-class
-compiler
-gc
-gccapacity
-gccause
-gcmetacapacity
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcutil
-printcompilation

option 选项详解

  • -class 用于查看类加载情况的统计
  • -compiler 用于查看HotSpot中即时编译器编译情况的统计
  • -gc 用于查看JVM中堆的垃圾收集情况的统计
  • -gccapacity 用于查看新生代、老生代及持久代的存储容量情况
  • -gccause 显示[垃圾回收]的相关信息(同-gcutil),同时显示最后一次仅当前正在发生的垃圾收集的原因
  • -gcmetacapacity 用于查看新生代垃圾收集的情况
  • -gcnew 用于查看新生代存储容量的情况
  • -gcnewcapacity 用于查看老生代及持久代垃圾收集的情况
  • -gcold 用于查看老生代及持久代垃圾收集的情况
  • -gcoldcapacity 用于查看老生代的容量
  • -gcutil 显示垃圾收集信息
  • -printcompilation 输出JIT编译的方法信息

interval 参数详解

  • 指定输出统计数据的周期,单位毫秒

count 参数详解

  • 一共输出多少次数据

Usage:

-class命令,每秒统一次classloader信息,一共输出 2 次

1
2
3
4
[root@localhost ~]$ jstat -class 21074 1000 2
Loaded Bytes Unloaded Bytes Time
3198 6281.9 0 0.0 0.99
3198 6281.9 0 0.0 0.99

装载了3198个类,大小6281.9个字节,卸载了0个类,大小0个字节,装载和卸载耗费时间总时间0.99秒

-compiler命令,显示JVM实时编译的数量

1
2
3
[root@localhost ~]$ jstat -compiler 21074
Compiled Failed Invalid Time FailedType FailedMethod
3920 2 0 10.55 1 com/mysql/jdbc/AbandonedConnectionCleanupThread run

编译任务执行了3920个,失败了2个,失效了0个,编译耗费10.55秒,最后一个编译失败任务的类型为1 ,最后一个编译失败任务所在的方法

-gc命令,显示GC堆相关信息

1
2
3
[root@localhost ~]$ jstat -gc 21074
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
12800.0 12288.0 288.0 0.0 3120640.0 360814.7 1048576.0 17050.0 20608.0 20235.5 2432.0 2280.5 26 0.129 0 0.000 0.129

字段解释:

  • S0C 年轻代中第一个S区容量(千字节)

  • S1C 年轻代中第二个S区容量(千字节)

  • S0U 年轻代中第一个S区已使用的空间(千字节)

  • S1U 年轻代中第二个S区已使用的空间(千字节)

  • EC 年轻代中E区容量(千字节)

  • EU 年轻代中E区使用空间(千字节)

  • OC old代的容量(千字节)

  • OU old代已使用空间(千字节)

  • MC metaspace元空间容量(千字节)

  • MU metaspace元空间已使用容量(千字节)

  • CCSC:当前压缩类空间的容量 (千字节)

  • CCSU:当前压缩类空间目前已使用空间 (千字节)

  • YGC 应用程序启动到采样时年轻代中GC次数

  • YGCT 应用程序启动到采样时年轻代中GC所用时间

  • FGC 应用程序启动到采样老年代GC次数(秒)

  • FGCT 应用程序启动到采样老年代GC所用时间(秒)

  • GCT 应用程序从启动到采样GC所用总时间(秒)

-gcmetacapacity命令,查看metaspace中对象的信息

1
2
3
[root@localhost ~]$ jstat -gcmetacapacity 21074
MCMN MCMX MC CCSMN CCSMX CCSC YGC FGC FGCT GCT
0.0 1067008.0 20608.0 0.0 1048576.0 2432.0 31 0 0.000 0.134

字段解释:

  • MCMN 最小元数据容量
  • MCMX 最大元数据容量
  • MC 当前元数据空间大小
  • CCSMN 最小压缩类空间大小
  • CCSMX 最大压缩类空间大小
  • CCSC 当前压缩类空间大小
  • YGC 从应用程序启动到采样YGC次数
  • FGC 从应用程序启动到采样FULL GC次数
  • FGCT 从应用程序到采样FULL GC所用时间
  • GCT 从应用程序到采样GC总时间

-gcnew命令,年轻代对象信息

1
2
3
[root@localhost ~]$ jstat -gcnew 21074
S0C S1C S0U S1U TT MTT DSS EC EU YGC YGCT
8704.0 9728.0 0.0 288.0 1 15 8704.0 3125760.0 3067072.3 31 0.134

字段解释:

  • S0C 年轻代中第一个S区容量

  • S1C 年轻代中第二个S区容量

  • S0U 年轻代中第一个S区已使用容量

  • S1U 年轻代中第二个S区已使用容量

  • TT 持有次数限制

  • MTT 最大持有次数限制

  • DSS 期望的S区大小

  • EC 年轻代中E区大小

  • EU 年轻代中E区已使用大小

  • YGC 从应用程序启动到采样YGC次数

  • YGCT 从应用程序启动到采样YGC所用时间

-gcnewcapacity命令,年轻代对象信息以及占用量

1
2
3
[root@localhost ~]$ jstat -gcnewcapacity 21074
NGCMN NGCMX NGC S0CMX S0C S1CMX S1C ECMX EC YGC FGC
3145728.0 3145728.0 3145728.0 1048576.0 8704.0 1048576.0 8192.0 3144704.0 3128832.0 32 0

字段解释:

  • NGCMN 年轻代初始化大小

  • NGCMX 年轻代最大容量

  • NGC 年轻代中当前容量

  • S0CMX S0最大的容量

  • S0C 当前S0大小

  • S1CM S1最大容量

  • S1C 当前S1大小

  • ECMX E区最大容量

  • EC E区当前大小

  • YGC 从应用程序启动到采样YGC次数

  • FGC 从应用程序启动到采样FGC次数

-gcold命令,老年代对象信息

1
2
3
[root@localhost ~]$ jstat -gcold 21074
MC MU CCSC CCSU OC OU YGC FGC FGCT GCT
20608.0 20235.5 2432.0 2280.5 1048576.0 17234.0 32 0 0.000 0.135

字段解释:

  • MC metaspace 元空间最大容量

  • MU metaspace 元空间当前大小

  • CCSC 压缩类空间大小

  • CCSU 压缩类空间当前使用大小

  • OC 老年代容量

  • OU 老年代已使用大小

  • YGC 从应用程序启动到采样YGC次数

  • FGC 从应用程序启动到采样FGC次数

  • FGCT 从应用程序启动到采样FGC耗费总时间

  • GCT 从应用程序启动到采样GC耗费总时间

-gcoldcapacity命令,老年代对象占用情况

1
2
3
[root@localhost ~]$ jstat -gcoldcapacity 21074
OGCMN OGCMX OGC OC YGC FGC FGCT GCT
1048576.0 1048576.0 1048576.0 1048576.0 32 0 0.000 0.135

字段解释:

  • OGCMN 老年代初始化大小
  • OGCMX 老年代最大容量
  • OGC 老年代当前使用大小
  • OC 老年代容量

-gcutil命令,统计GC情况

1
2
3
[root@localhost ~]$ jstat -gcutil 21074
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
2.21 0.00 54.97 1.64 98.19 93.77 32 0.135 0 0.000 0.135

字段解释:

  • S0 S0区占用比
  • S1 S1区占用比
  • E 伊甸园区占用比
  • O 老年代占用比
  • M 元空间占用比
  • CCS 压缩类占用比
  • YGC YGC发生的次数
  • YGCT 所有YGC的总共耗时
  • FGC FGC发生的次数
  • FGCT 所有FGC的共耗时
  • GCT 所有GC的总耗时

-gccause命令,显示GC导致的原因

1
2
3
[root@localhost ~]$ jstat -gccause 21074
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT LGCC GCC
2.21 0.00 68.95 1.64 98.19 93.77 32 0.135 0 0.000 0.135 Allocation Failure No GC

大部分字段同-gcutil命令

字段解释:

  • LGCC 上一次GC原因
  • GCC 当前GC原因

-printcompilation命令,VM执行的信息

1
2
3
[root@localhost ~]$ jstat -printcompilation 21074
Compiled Size Type Method
3964 311 1 com/alibaba/druid/sql/visitor/SchemaStatVisitor visit

字段解释:

  • compiled 编译任务的数目
  • size 方法生成的字节码大小
  • type 编译类型
  • method 类名和方法名

jcmd

jcmd - 向正在运行的 Java 虚拟机 (JVM) 发送诊断命令请求

jcmd [pid | main-class ] command … | PerfCounter.print | -f filename

jcmd [-l ]

jcmd -h

查看帮助

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ jcmd -h
Usage: jcmd <pid | main class> <command ...|PerfCounter.print|-f file>
or: jcmd -l
or: jcmd -h

command must be a valid jcmd command for the selected jvm.
Use the command "help" to see which commands are available.
If the pid is 0, commands will be sent to all Java processes.
The main class argument will be used to match (either partially
or fully) the class used to start Java.
If no options are given, lists Java processes (same as -p).

PerfCounter.print display the counters exposed by this process
-f read and execute commands from the file
-l list JVM processes on the local machine
-h this help

查看正在运行的 Java 进程ID、名称和 main 函数参数

1
2
3
4
$ jcmd
7200 sun.tools.jcmd.JCmd
10614 com.install4j.runtime.launcher.MacLauncher
88348 org.gradle.launcher.daemon.bootstrap.GradleDaemon 2.14

注意,7200 进程是jcmd本身。执行完jcmd后,该进程已经结束了。

查看某个进程支持的命令

jcmd 后加上进程 ID,然后加上 help

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
$ jcmd 10614 help
10614:
The following commands are available:
JFR.configure
JFR.stop
JFR.start
JFR.dump
JFR.check
VM.log
VM.native_memory
VM.check_commercial_features
VM.unlock_commercial_features
ManagementAgent.status
ManagementAgent.stop
ManagementAgent.start_local
ManagementAgent.start
Compiler.directives_clear
Compiler.directives_remove
Compiler.directives_add
Compiler.directives_print
VM.print_touched_methods
Compiler.codecache
Compiler.codelist
Compiler.queue
VM.classloader_stats
Thread.print
JVMTI.data_dump
JVMTI.agent_load
VM.stringtable
VM.symboltable
VM.class_hierarchy
GC.class_stats
GC.class_histogram
GC.heap_dump
GC.finalizer_info
GC.heap_info
GC.run_finalization
GC.run
VM.info
VM.uptime
VM.dynlibs
VM.set_flag
VM.flags
VM.system_properties
VM.command_line
VM.version
help

For more information about a specific command use 'help <command>'.

jcmd 后也可以跟上进程名:

1
$ jcmd MacLauncher help

输出结果和 jcmd 10614 help 相同。

查看某个进程的 JVM 版本

1
2
3
4
$ jcmd 10614 VM.version
10614:
Java HotSpot(TM) 64-Bit Server VM version 9.0.1+11
JDK 9.0.1

查看 JVM 进程信息

1
$ jcmd 10614 VM.info

建议进程进行垃圾回收

1
$ jcmd 10614 GC.run

获取类的统计信息

1
$ jcmd 10614 GC.class_histogram | more

可以看到类名、对象数量、占用空间等。

获取启动参数

1
$ jcmd 10614 VM.flags

获取进程到现在运行了多长时间

1
$ jcmd 10614 VM.uptime

查看线程信息

1
$ jcmd 10614 Thread.print

获取性能相关数据

1
$ jcmd 10614 PerfCounter.print

导出堆快照到当前目录

1
$ jcmd 10614 GC.heap_dump $PWD/heap.dump

堆快照可以使用 VisualVM 等工具打开分析。


jmap

jmap (Memory Map for Java) 为给定的 Java 进程生成与内存相关的信息,包括堆dump和内存使用情况统计信息。

Usage:

1
jmap [option] <pid>

查看一下jmap命令的帮助信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[root@localhost ~]$ jmap -help
Usage:
jmap [option] <pid>
(to connect to running process)
jmap [option] <executable <core>
(to connect to a core file)
jmap [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server)

where <option> is one of:
<none> to print same info as Solaris pmap
-heap to print java heap summary
-histo[:live] to print histogram of java object heap; if the "live"
suboption is specified, only count live objects
-clstats to print class loader statistics
-finalizerinfo to print information on objects awaiting finalization
-dump:<dump-options> to dump java heap in hprof binary format
dump-options:
live dump only live objects; if not specified,
all objects in the heap are dumped.
format=b binary format
file=<file> dump heap to <file>
Example: jmap -dump:live,format=b,file=heap.bin <pid>
-F force. Use with -dump:<dump-options> <pid> or -histo
to force a heap dump or histogram when <pid> does not
respond. The "live" suboption is not supported
in this mode.
-h | -help to print this help message
-J<flag> to pass <flag> directly to the runtime system

参数:

  • option: 选项参数
  • pid: 需要打印配置信息的进程ID
  • executable: 产生核心dump的Java可执行文件
  • core: 需要打印配置信息的核心文件
  • server-id 可选的唯一id,如果相同的远程主机上运行了多台调试服务器,用此选项参数标识服务器
  • remote server IP or hostname 远程调试服务器的IP地址或主机名

这些参数里面一般使用option和pid即可

  • no option: 查看进程的内存映像信息,类似Solaris pmap命令
  • heap: 显示Java堆详细信息
  • histo[:live]: 显示堆中对象的统计信息
  • clstats:打印类加载器信息
  • finalizerinfo: 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象
  • dump::生成堆转储快照
  • F: 当-dump没有响应时,使用-dump或者-histo参数。在这个模式下,live子参数无效
  • help:打印帮助信息
  • J:指定传递给运行jmapJVM的参数

示例

  1. jmap -heap pid命令,打印堆内存详细信息

打印一个堆的摘要信息,包括使用的GC算法、堆配置信息和各内存区域内存使用信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
[root@localhost ~]$ jmap -heap 21074
Attaching to process ID 21074, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.131-b11

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration: ##堆配置情况,也就是JVM参数配置的结果
MinHeapFreeRatio = 0 ##最小堆使用比例
MaxHeapFreeRatio = 100 ##最大堆可用比例
MaxHeapSize = 4294967296 (4096.0MB) ##最大堆空间大小
NewSize = 3221225472 (3072.0MB) ##新生代分配大小
MaxNewSize = 3221225472 (3072.0MB) ##最大可新生代分配大小
OldSize = 1073741824 (1024.0MB) ##老年代大小
NewRatio = 2 ##新生代比例
SurvivorRatio = 8 ##新生代与suvivor的比例
MetaspaceSize = 21807104 (20.796875MB) ##元空间大小
CompressedClassSpaceSize = 1073741824 (1024.0MB) ##压缩类空间大小
MaxMetaspaceSize = 17592186044415 MB ##最大元空间大小
G1HeapRegionSize = 0 (0.0MB) ##G1的region大小

Heap Usage: ##堆使用情况
PS Young Generation ##新生代(伊甸Eden区 + 幸存者survior(from + to)区)
Eden Space: ##伊甸区
capacity = 3206021120 (3057.5MB) ##伊甸区容量
used = 1334298032 (1272.4857635498047MB) ##已经使用大小
free = 1871723088 (1785.0142364501953MB) ##剩余容量
41.61850412264283% used ##使用比例
From Space: ##survior1区
capacity = 7340032 (7.0MB) ##survior1区容量
used = 229376 (0.21875MB) ##surviror1区已使用情况
free = 7110656 (6.78125MB) ##surviror1区剩余容量
3.125% used ##survior1区使用比例
To Space: ##survior2 区
capacity = 6815744 (6.5MB) ##survior2区容量
used = 0 (0.0MB) ##survior2区已使用情况
free = 6815744 (6.5MB) ##survior2区剩余容量
0.0% used ##survior2区使用比例
PS Old Generation ##老年代使用情况
capacity = 1073741824 (1024.0MB) ##老年代容量
used = 17754144 (16.931671142578125MB) ##老年代已使用容量
free = 1055987680 (1007.0683288574219MB) ##老年代剩余容量
1.653483510017395% used ##老年代使用比例

7111 interned Strings occupying 559016 bytes. ##系统中使用的字符串总大小

  1. jmap -histo:live pid命令,显示堆中对象的统计信息

其中包括每个Java类、对象数量、内存大小(单位:字节)、完全限定的类名。打印的虚拟机内部的类名称将会带有一个*前缀。如果指定了live子选项,则只计算活动的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@localhost ~]$ jmap -histo:live 21074
#编号 #实例个数 #实例总大小 #全限定类名
num #instances #bytes class name
----------------------------------------------
1: 262144 6291456 org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor$Log4jEventWrapper
2: 16536 2830928 [C
3: 2622 1197536 [Ljava.lang.Object;
4: 1547 1122888 [I
5: 896 675144 [B
6: 16462 395088 java.lang.String
7: 3483 393304 java.lang.Class
8: 20149 322384 java.lang.Integer
9: 8820 282240 io.mycat.statistic.HeartbeatRecorder$Record
10: 8954 214896 java.util.concurrent.ConcurrentLinkedQueue$Node
11: 2007 211264 [Ljava.util.HashMap$Node;
12: 5444 174208 java.util.HashMap$Node
13: 4183 133856 java.util.concurrent.ConcurrentHashMap$Node
14: 1493 119440 io.mycat.route.RouteResultsetNode
15: 1959 94032 java.util.HashMap
16: 2 80560 [Ljava.lang.Integer;
17: 93 67352 [Ljava.util.concurrent.ConcurrentHashMap$Node;
18: 3713 59408 java.lang.Object
......

类名解释如下

  • B byte
  • C char
  • D double
  • F float
  • I int
  • J long
  • Z boolean
  • [ 数组,如[I表示int[]
  • [L+类名 其他对象
  1. jmap -dump:format=b,file=heapdump.hprof pid命令,dump当前内存快照

hprof二进制格式转储Java堆到指定filename的文件中。live子选项是可选的。如果指定了live子选项,堆中只有活动的对象会被转储。想要浏览heap dump,你可以使用jhat(Java堆分析工具)或者MAT等工具读取生成的文件。


jstack

jstack(Java Virtual Machine Stack Trace)是JDK提供的一个可以生成Java虚拟机当前时刻的线程快照信息的命令行工具。线程快照一般被称为threaddump或者javacore文件,是当前Java虚拟机中每个线程正在执行的Java线程、虚拟机内部线程和可选的本地方法堆栈帧的集合。对于每个方法栈帧,将会显示完整的类名、方法名、字节码索引(bytecode indexBCI)和行号。生成的线程快照可以用于定位线程出现长时间停顿的原因,比如:线程间死锁、死循环、请求外部资源被长时间挂起等等。

jstack prints Java stack traces of Java threads for a given Java process or core file or a remote debug server.

  • jstack命令用于生成虚拟机当前时刻的线程快照。
  • 线程快照是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因, 如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。
  • 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
  • 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stacknative stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。
  • 另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stacknative stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。

命令语法:

1
jstack [options] pid

命令参数说明:

  • optionjstack命令的可选参数。如果没有指定这个参数,jstack命令会显示Java虚拟机当前时刻的线程快照信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
D:\Desktop>jstack 9348
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.281-b09 mixed mode):

"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000018da5881000 nid=0x34c8 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"mythread2" #12 prio=5 os_prio=0 tid=0x0000018dc2547000 nid=0x2580 waiting on condition [0x000000468cdff000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076b8dbf28> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(Unknown Source)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(Unknown Source)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(Unknown Source)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Unknown Source)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(Unknown Source)
at java.util.concurrent.locks.ReentrantLock.lock(Unknown Source)
at DeathLock.lambda$deathLock$1(DeathLock.java:24)
at DeathLock$$Lambda$2/1044036744.run(Unknown Source)
at java.lang.Thread.run(Unknown Source)

查看帮助信息

可以通过执行jstack 或者 jstack -h来查看帮助信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ jstack
Usage:
jstack [-l] <pid>
(to connect to running process)
jstack -F [-m] [-l] <pid>
(to connect to a hung process)
jstack [-m] [-l] <executable> <core>
(to connect to a core file)
jstack [-m] [-l] [server_id@]<remote server IP or hostname>
(to connect to a remote debug server)

Options:
-F to force a thread dump. Use when jstack <pid> does not respond (process is hung)
-m to print both java and native frames (mixed mode)
-l long listing. Prints additional information about locks
-h or -help to print this help message

option参数说明如下:

1
2
3
4
-F 当’jstack [-l] pid’没有相应的时候强制打印栈信息
-l 长列表. 打印关于锁的附加信息,例如属于java.util.concurrent的ownable synchronizers列表.
-m 打印java和native c/c++框架的所有栈信息.
-h | -help 打印帮助信息

pid 需要被打印配置信息的java进程id,可以用jps查询

线程状态简介

jstack用于生成线程快照的,我们分析线程的情况,需要复习一下线程状态

Java语言定义了6种线程池状态:

  • New:创建后尚未启动的线程处于这种状态,不会出现在Dump中。
  • RUNNABLE:包括RunningReady。线程开启start()方法,会进入该状态,在虚拟机内执行的。
  • Waiting:无限的等待另一个线程的特定操作。
  • Timed Waiting:有时限的等待另一个线程的特定操作。
  • 阻塞(Blocked):在程序等待进入同步区域的时候,线程将进入这种状态,在等待监视器锁。
  • 结束(Terminated):已终止线程的线程状态,线程已经结束执行。

Dump文件的线程状态一般其实就以下3种:

  • RUNNABLE,线程处于执行中
  • BLOCKED,线程被阻塞
  • WAITING,线程正在等待

Dump 文件分析关注重点

  • runnable,线程处于执行中
  • deadlock,死锁(重点关注)
  • blocked,线程被阻塞 (重点关注)
  • Parked,停止
  • locked,对象加锁
  • waiting,线程正在等待
  • waiting to lock 等待上锁
  • Object.wait(),对象等待中
  • waiting for monitor entry 等待获取监视器(重点关注)
  • Waiting on condition,等待资源(重点关注),最常见的情况是线程在等待网络的读写

正则表达式与文件格式化处理

正则表达式(Regular Expression )是通过一些特殊字符的排列,用来搜索或替换、删除一列或多列文字字符串

本章需要多练习:因为目前很多套件都使用正则表达式来达成过来、分析的目的,为了未来主机管理的便利性,使用者至少要能看懂正则表达式的含义

开始之前:什么是正则表达式

  • 什么是正则表达式:

    这个就不解释了,某些指令支持,比如 grep 'mail' /lib/systemd/system/* 搜索该目录下的所有文件中包含 mail 的文件,但是 cp ls 等命令不支持正则表达式,只能使用 bash 自己本身的通配符

  • 正则表达式对于系统管理员的用途

    对于一般使用者来说,使用不多,对于系统管理员来说,是必学的知识,如 错误信息登录文件(第十八章中)的内容记录了系统产生的所有信息,包含是否被入侵的记录数据,可以通过正则表达式将这些登录信息进行处理,仅取出有问题的信息进行分析

  • 正则表达式的广泛用途

    由于正则表达式强大的字符串处理能力,一堆软件都支持

  • 正则表达式与 shellLinux 中的角色定位

    这样说吧,小学的 九九乘法表 有多重要,shell 与 正则表达式就有多重要

  • 扩展的正则表达式

    正则表达式的字符串表示方式依照不同的严谨程度分为:基础正则表达式、扩展正则表达式。

基础正则表达式

既然正则表达式是处理字符串的一种表示方式,那么对字符排序有影响的语系数据就会对正则表达式的结果有影响。此外也需要有支持工具程序来辅助才行。

因此这里先介绍一个最简单的字符串摘取工具程序 grep。前面讲解了 grep 的相关参数与参数,本章着重讲解进阶的 grep 选项说明,介绍完 grep 的功能后,就进入正则表达式的特殊字符处理能力

语系对正则表达式的影响

为什么语系数据会影响正则表达式的输出结果?在第 0 章计算器概论的文字编码系统里面谈到,文件其实记录的仅有 01,我们看到的字符与数值都是通过编码表转换来的。

由于不同语系的编码数据不同,就会造成数据处理结果的差异了,举例说明,假设两种语系输出结果为:

  • LANG=C:0 1 2 3 … A B C D ..Z a b c d .. z
  • LANG=zh_TW :0 1 2 3 … a A b c C D .. z Z

两种语系明显不一样,如果想获取大写字符使用 [A-Z]时,会发现 C 可以获取到正确的大写字符(因为是连续的),zh_TW 连同小写也会 b-z 也会获取到,因为就编码的顺序来看,big5 语系可以获取到 A b B c C .. z Z 这一堆字符。

所以使用正则表达式时,需要留意当前的语系,否则可能发现与别人不同的截取结果

由于一般我们再联系正则表达式时,使用的是兼容于 POSIX 的标准,因此就使用 C 这个语系,因此下面的练习都是使用 LANG=C来练习的。为了避免这样编码所造成的英文与数字截取问题,因此特殊符号需要了解下

  • [:alnum:]:代表英文大小写字符及数字,即 0-9A-Za-z
  • [:alpha:]:代表任何英文大小写字符,A-Za-z
  • [:blank:]:代表空格与 tab
  • [:cntrl:]:代表键盘上面的控制按键,包括 CRLFTABDel
  • [:digit:]:代表数字,0-9
  • [:graph:]:除了空格符(空格键与 tab 键)外其他的所有按键
  • [:lower:]:代表些小字符,a-z
  • [:print:]:代表任何可以被打印出来的字符
  • [:punct:]:代表标点符号(punctuation symbol
  • [:upper:]:代表大写字符,A-Z
  • [:space:]:任何会产生空白的字符,包括空格、tabCR
  • [:xdigit:]:代表 16 进制的数值类型,包括 0-9A-Fa-f 的数字与字符

尤其是 [:alnum:][:alpha:][:upper:][:lower:][:digit:]一定要知道代表什么意思,因为他们要比 a-zA-Z 的用途要确定。

grep 的一些进阶选项

在第十章 BASH 中的 grep 谈论过一些基础用法,下面列出较进阶的 grep 选项与参数

1
2
3
4
5
grep [-A] [-B] [--color='auto'] '关键词' filename

-A:后面可以加数字,为 after 的意思,除了列出该行外,后续的 n 行也列出来
-B:后面可以加数字,为 befer 的意思,处理列出该行外,前面的 n 行也列出来
--colort=auto:可将正确的哪个截取数据列出颜色

实践与练习

1
2
3
4
5
6
7
8
9
10
11
# 范例 1:用 dmesg 列出核心信息,再以 grep 找出含有 qx1 那一行
dmesg | grep 'qx1'
# 笔者不知道自己使用的显卡是什么,而且使用的是虚拟机,而作者使用的显卡是 qx1,所以查看显卡信息

# 范例 2:用 --color=auto 显示查找到的关键词高亮,并显示行号
dmesg | grep -n --color=auto ‘qx1’

# 范例 3:在关键词所在行的前两行与后三行也一起显示出来
dmest | grep -n -A2 -B3 --color=auto 'qx1'


grep 是一个很常见也很常用的指令,最重要的功能就是进行字符串的比对,然后将符合用户需求的字符串打印出来。需要注意的是:grep 是已整行为单位来进行数据截取的

基础正则表达式练习

要了解正则表达式最简单的方法就是由实际练习去感受,所以在汇总特殊符号前,先以下面这个文件的内容来进行正则表达式的练习,练习前提为:

  • 语系已经使用 export LANG=C;export LC_ALL=C
  • grep 已经使用 alias 设置为 grep --color=auto

本机默认为 LANG=en_US.UTF-8;LC_ALL=

文件为 regular——express.txt ,该文件内容是在 windows 系统下编辑的,所以包含 dos 的换行符;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"Open Source" is a good mechanism to develop programs.
apple is my favorite food.
Football game is not use feet only.
this dress doesn't fit me.
However, this dress is about $ 3183 dollars.
GNU is free air not free beer.
Her hair is very beauty.
I can't finish the test.
Oh! The soup taste good.
motorcycle is cheap than car.
This window is clear.
the symbol '*' is represented as start.
Oh! My god!
The gd software is a library for drafting programs.
You are the best is mean you are the no. 1.
The world <Happy> is the same with "glad".
I like dog.
google is the best tools for search keyword.
goooooogle yes!
go! go! Let's go.
# I am VBird


范例 1:搜索特定字符

从文件中取得 the 这个特定字符串,最简单的方式如下

1
2
3
4
5
6
[mrcode@study tmp]$ grep -n 'the' regular_express.txt
8:I can't finish the test.
12:the symbol '*' is represented as start.
15:You are the best is mean you are the no. 1.
16:The world <Happy> is the same with "glad".
18:google is the best tools for search keyword.

反向选择,可以看到输出结果少了上面的 8、12、15、16、18

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[mrcode@study tmp]$ grep -vn 'the' regular_express.txt
1:"Open Source" is a good mechanism to develop programs.
2:apple is my favorite food.
3:Football game is not use feet only.
4:this dress doesn't fit me.
5:However, this dress is about $ 3183 dollars.
6:GNU is free air not free beer.
7:Her hair is very beauty.
9:Oh! The soup taste good.
10:motorcycle is cheap than car.
11:This window is clear.
13:Oh! My god!
14:The gd software is a library for drafting programs.
17:I like dog.
19:goooooogle yes!
20:go! go! Let's go.
21:# I am VBird
22:

忽略大小写 ,多出来几行

1
2
3
4
5
6
7
8
[mrcode@study tmp]$ grep -in 'the' regular_express.txt
8:I can't finish the test.
9:Oh! The soup taste good.
12:the symbol '*' is represented as start.
14:The gd software is a library for drafting programs.
15:You are the best is mean you are the no. 1.
16:The world <Happy> is the same with "glad".
18:google is the best tools for search keyword.

范例 2:利用中括号[]来搜索集合字符

如果要搜索 test 或 taste 这两个单词时,可以发现他们其实有共同的 t?st 存在

1
2
3
[mrcode@study tmp]$ grep -n 't[ae]st' regular_express.txt
8:I can't finish the test.
9:Oh! The soup taste good.

中括号中,无论几个字符都表示任意一个字符。如果想要搜索到所有 oo 字符时

1
2
3
4
5
6
7
[mrcode@study tmp]$ grep -n 'oo' regular_express.txt
1:"Open Source" is a good mechanism to develop programs.
2:apple is my favorite food.
3:Football game is not use feet only.
9:Oh! The soup taste good.
18:google is the best tools for search keyword.
19:goooooogle yes!

如果不想要 oo 前面的 g 呢?

1
2
3
4
5
[mrcode@study tmp]$ grep -n '[^g]oo' regular_express.txt
2:apple is my favorite food.
3:Football game is not use feet only.
18:google is the best tools for search keyword.
19:goooooogle yes!

会发现可能会有一部分是正确的,一部分是错误的,比如 19 行少了,但是 googlegoooooogle 还是出来了,是怎么回事?第 18 行,出现了 tools 所以也符合 [^g]oo,而 19 行,中间有那么多的 oo,也符合

继续,不想要 oo 前面是小写字符的

1
2
3
4
5
6
7
8
9
# 由于小写字符的 ASCII 编码顺序是连续的,所以可以简化为,否则就需要把 a-z 都写出来
[mrcode@study tmp]$ grep -n '[^a-z]oo' regular_express.txt
3:Football game is not use feet only.

# 取得有数字那一行
[mrcode@study tmp]$ grep -n '[0-9]' regular_express.txt
5:However, this dress is about $ 3183 dollars.
15:You are the best is mean you are the no. 1.

由于考虑到语系对于编码顺序的影响,因此除了连续编码使用减号 -,还可以使用如下的方法来取得前面两个测试的结果

1
2
3
4
5
6
[mrcode@study tmp]$ grep -n '[^[:lower:]]oo' regular_express.txt
3:Football game is not use feet only.

[mrcode@study tmp]$ grep -n '[[:digit:]]' regular_express.txt
5:However, this dress is about $ 3183 dollars.
15:You are the best is mean you are the no. 1.

范例 3:行首与行尾字符 ^、$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 只要行首是 the 的
[mrcode@study tmp]$ grep -n '^the' regular_express.txt
12:the symbol '*' is represented as start.

# 想要行首是小写字符开头的
[mrcode@study tmp]$ grep -n '^[a-z]' regular_express.txt
2:apple is my favorite food.
4:this dress doesn't fit me.
10:motorcycle is cheap than car.
12:the symbol '*' is represented as start.
18:google is the best tools for search keyword.
19:goooooogle yes!
20:go! go! Let's go.
# 下面的等效
# [mrcode@study tmp]$ grep -n '^[[:lower:]]' regular_express.txt

# 不要英文字母开头的
# ^ 在中括号内表示反选,在外表示定位首航
[mrcode@study tmp]$ grep -n '^[^a-zA-Z]' regular_express.txt
1:"Open Source" is a good mechanism to develop programs.
21:# I am VBird

行尾练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 找出行尾为 . 符号的数据
# 使用 \ 对 小数点转义
[mrcode@study tmp]$ grep -n '\.$' regular_express.txt
1:"Open Source" is a good mechanism to develop programs.
2:apple is my favorite food.
3:Football game is not use feet only.
4:this dress doesn't fit me.
5:However, this dress is about $ 3183 dollars.
6:GNU is free air not free beer.
7:Her hair is very beauty.
8:I can't finish the test.
9:Oh! The soup taste good.
10:motorcycle is cheap than car.
11:This window is clear.
12:the symbol '*' is represented as start.
14:The gd software is a library for drafting programs.
15:You are the best is mean you are the no. 1.
16:The world <Happy> is the same with "glad".
17:I like dog.
18:google is the best tools for search keyword.
20:go! go! Let's go.

这里需要说一句,原本的文件 5-9 行默认是 .^M$ 结尾的,也就是 \r\n,由于没有网络,无法下载文件,所以复制粘贴丢失了这些换行符,和书上结果不一样。

也就是说上面的示例 5-9 不应该出来的,使用命令查看特殊字符应该如下

1
2
3
4
5
6
7
[mrcode@study tmp]$ cat -An regular_express.txt | head -n 10 | tail -n 6
5 However, this dress is about $ 3183 dollars.^M$
6 GNU is free air not free beer.^M$
7 Her hair is very beauty.^M$
8 I can't finish the test.^M$
9 Oh! The soup taste good.^M$
10 motorcycle is cheap than car.$ # 但实际上 ^M 被丢失了

找出空白行

1
2
3
[mrcode@study tmp]$ grep -n '^$' regular_express.txt 
22:
# 只有行首和行尾的表示法,中间没有任何字符,所以是 ^$

假设你已经知道 shell script 或则是配置文件中,空白行与开头为 # 的那一行是批注,想要将这些数据忽略掉,该怎么做?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[mrcode@study tmp]$ cat -n /etc/rsyslog.conf 
# 在 centOS 7 中可以看到有 91 行,有大量的空行与批注信息

# 第一种写法:-v '^$' 是反选,也就是排除空行的,-v ‘^#’ 排除开头是 # 号的
# 但是这里的行号与源文件对不上了,后面的行号是针对前面排除空行后的行号
[mrcode@study tmp]$ grep -v '^$' /etc/rsyslog.conf | grep -vn '^#'
6:$ModLoad imuxsock # provides support for local system logging (e.g. via logger command)
7:$ModLoad imjournal # provides access to the systemd journal
18:$WorkDirectory /var/lib/rsyslog
20:$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat
25:$IncludeConfig /etc/rsyslog.d/*.conf
28:$OmitLocalLogging on
30:$IMJournalStateFile imjournal.state
37:*.info;mail.none;authpriv.none;cron.none /var/log/messages
39:authpriv.* /var/log/secure
41:mail.* -/var/log/maillog
43:cron.* /var/log/cron
45:*.emerg :omusrmsg:*
47:uucp,news.crit /var/log/spooler
49:local7.* /var/log/boot.log

# 第二种实现:直接匹配行首非 # 开头的
# 因为使用了中括号表示需要有一个字符存在,所以空行的不会被匹配
[mrcode@study tmp]$ grep -n '^[^#]' /etc/rsyslog.conf
9:$ModLoad imuxsock # provides support for local system logging (e.g. via logger command)
10:$ModLoad imjournal # provides access to the systemd journal
26:$WorkDirectory /var/lib/rsyslog
29:$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat
36:$IncludeConfig /etc/rsyslog.d/*.conf
40:$OmitLocalLogging on
43:$IMJournalStateFile imjournal.state
54:*.info;mail.none;authpriv.none;cron.none /var/log/messages
57:authpriv.* /var/log/secure
60:mail.* -/var/log/maillog
64:cron.* /var/log/cron
67:*.emerg :omusrmsg:*
70:uucp,news.crit /var/log/spooler
73:local7.* /var/log/boot.log

这里要注意的是批注可以出现在任意处,所以匹配行首的是最安全的做法

范例 4:任意一个字符 . 与重复字符 *

在第十章 bash 中,通配符 *表示任意(0 或 多个)字符,但是正则表达式中并不是这样,他们含义如下:

  • .:一定有一个任意字符
  • *:重复前一个字符,0 到任意次,为组合形态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 找出 g??d 的字符串,也就是 g 开头 d 结尾的 4 字符的字符串
[mrcode@study tmp]$ grep -n 'g..d' regular_express.txt
1:"Open Source" is a good mechanism to develop programs.
9:Oh! The soup taste good.
16:The world <Happy> is the same with "glad".

# 找出 oo、ooo、ooo 等数据,至少含有 2 个 o
# 注意,这里不能写 oo* 因为,*是作用于第二个 o 的,表示 0 到任意个
# 也就是说如果是 oo* 有可能匹配到一个 o
[mrcode@study tmp]$ grep -n 'ooo*' regular_express.txt
1:"Open Source" is a good mechanism to develop programs.
2:apple is my favorite food.
3:Football game is not use feet only.
9:Oh! The soup taste good.
18:google is the best tools for search keyword.
19:goooooogle yes!

# 找出 开头与结尾都是 g ,并且中间至少含有一个 o 的数据
# 也就是 gog、goog 之类的数据
[mrcode@study tmp]$ grep -n 'goo*g' regular_express.txt
18:google is the best tools for search keyword.
19:goooooogle yes!

# 找出 开头与结尾都是 g,中间有无字符均可
[mrcode@study tmp]$ grep -n 'g*g' regular_express.txt
1:"Open Source" is a good mechanism to develop programs.
3:Football game is not use feet only.
9:Oh! The soup taste good.
13:Oh! My god!
14:The gd software is a library for drafting programs.
16:The world <Happy> is the same with "glad".
17:I like dog.
18:google is the best tools for search keyword.
19:goooooogle yes!
20:go! go! Let's go.
# 使用 g*g 发现第一行的数据就不匹配,这个还是需要再终端看,因为可以开启高亮,方便查看哈
# 原因是 * 作用于 g,g* 代表空字符或一个以上的 g,因此应该匹配 g、gg、ggg 等

# 正确的应该这样实现
[mrcode@study tmp]$ grep -n 'g.*g' regular_express.txt
1:"Open Source" is a good mechanism to develop programs.
14:The gd software is a library for drafting programs.
18:google is the best tools for search keyword.
19:goooooogle yes!
20:go! go! Let's go.

# 找出包含任意数字的数据
# 同上,[0-9]* 只作用于一个中括号
[mrcode@study tmp]$ grep -n '[0-9][0-9]*' regular_express.txt
5:However, this dress is about $ 3183 dollars.
15:You are the best is mean you are the no. 1.
# 直接使用 grep -n '[0-9]' regular_express.txt 也可以得到相同结果哈

范例 5:限定连续 正则字符范围 {}

找出 2 个到 5o 的连续字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 华括弧在 shell 中是特殊符号,需要转义
[mrcode@study tmp]$ grep -n 'o\{2\}' regular_express.txt
1:"Open Source" is a good mechanism to develop programs.
2:apple is my favorite food.
3:Football game is not use feet only.
9:Oh! The soup taste good.
18:google is the best tools for search keyword.
19:goooooogle yes!
# 上述结果是至少是 2 个 oo 的出来了

# 单词开头结尾都是 g,中间 o,至少 2 个,最多 5 个
[mrcode@study tmp]$ grep -n 'go\{2,5\}g' regular_express.txt
18:google is the best tools for search keyword.

# 承上,只是中间的 o 至少 2 个
[mrcode@study tmp]$ grep -n 'go\{2,\}g' regular_express.txt
18:google is the best tools for search keyword.
19:goooooogle yes!

基础正则表示法字符汇总

  • ^word:搜索的关键词 word 在行首

    范例:搜索行首为 # 的,并列出行号 grep -n '^#' file

  • word$:搜索的关键词 word 在行尾

    范例:搜索以 !结尾的,grep -n '!$' file

  • .:一定有一个任意字符

    范例:搜索字符串可以是 eveeaeeeee egrep -n 'e.e' file

  • \:转义字符

    范例:搜索含有单引号数据。grep -n '\’' file

  • *:重复另个到无穷多个前一个字符

    范例:找出含有 esessesss 等字符串;grep -n 'es*' file

  • [list]:里面列出想要截取的字符合集

    范例:找出含有 g1gd 的数据;grep -n 'g[1d]' file

  • [n1-n2]:字符合集范围

    范例:找出含有任意大写字母的数据;grep -n '[A-Z]' file

  • [^list]:不要包含该集合中的字符或该范围的字符

    范例:找出 ooaoog 但是不包含 oot 的数据; grep -n 'oo[^t]'

  • \{n,m\}:连续 nm 个前一个字符

  • \{n\}:连续 n 个前一个字符

  • \{n,\}:至少 n 个以上的前一个字符;咋效果上感觉和 \{n\} 是一样的

最后再强调,通配符和正则表达式不一样,比如在 ls 命令中找出以 a 开头的文件

  • 通配符:ls -l a*
  • 正则表达式:ls | grep -n '^a' 或则 ls | grep -n '^a.*'
1
2
3
4
5
# 范例:以 ls -l 配合 grep 找出 /etc/ 下文件类型为链接文件属性的文件名
# 符号链接文件的特点是权限前面一位是 l,根据 ls 的输出,只要找到行首为 l 的即可
[mrcode@study tmp]$ ls -l /etc | grep '^l'
lrwxrwxrwx. 1 root root 56 Oct 4 18:22 favicon.png -> /usr/share/icons/hicolor/16x16/apps/fedora-logo-icon.png
lrwxrwxrwx. 1 root root 22 Oct 4 18:23 grub2.cfg -> ../boot/grub2/grub.cfg

sed 工具

了解了一些正则基础使用后,可以来玩一玩 sedawk ;作者就利用他们两个实现了一个小工具:logfile.sh 分析登录文件(第十八章会讲解)。里面绝大部分关键词的提取、统计等都是通过他们来完成的

sed:本身是一个管线命令,可以分析 standard input 的数据,还可以将数据进行替换、新增、截取特定行等功能

1
2
sed [-nefr] [动作]

选项与参数:

  • n:使用安静(silent)模式

    在一般 sed 的用法中,所有来自 STDIN 的数据一般都会列出到屏幕上,加上 -n 之后,只有经过 sed 特殊处理的那一行(或则动作)才会被打印出来

  • e:直接在指令模式上进行 sed 的动作编辑

  • f:直接将 sed 的动作写在一个文件内,- f filename 则可以执行 filename 内的 sed 动作

  • rsed 的动作支持是延伸类型正则表达式的语法(预设是基础正则表达式语法)

  • i:直接修改读取的文件内容,而不是由屏幕输出

动作说明:[n1[,n2]]function

n1,n2:不见得会存在,一般代表「选择进行动作的行数」,比如:如果我的动作是需要再 1020 行之间进行的,则「10,20[动作行为]」

function 有如下:

  • a:新增,a 后面可以接字符串,这些字符串会在新的一行出现(当前的下一行)
  • c:替换,c 后面可以接字符串,这些字符串替换 n1,n2 之间的行
  • d:删除,后面不接任何字符串
  • i:插入,i 的后面可以接字符串,而这些字符串会在新的一行出现(当前的上一行)
  • p:打印,将某个选择的数据打印。通常 p 会参与 sed -n 一起运作
  • s:替换,可以直接进行替换工作。通常这个 s 的动作可以搭配正则表达式,例如:1,20s/old/new/g

以行为单位的新增/删除功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 范例1:将 /etc/passwd 的内容列出并且打印行号,同时将第 2~5 行删除
[mrcode@study ~]$ nl /etc/passwd | sed '2,5d' # 注意写法和结果
1 root:x:0:0:root:/root:/bin/bash
6 sync:x:5:0:sync:/sbin:/bin/sync
7 shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
# 另外这里,应该带上 sed -e '2,5d' 才标准,不过不带也可以,但是需要使用单引号括起来
# 实测不用单引号也可以实现

# 范例2:只删除第二行
[mrcode@study ~]$ nl /etc/passwd | sed '2d'

# 范例3:删除第三行到最后一行
[mrcode@study ~]$ nl /etc/passwd | sed '3,$d'
1 root:x:0:0:root:/root:/bin/bash
2 bin:x:1:1:bin:/bin:/sbin/nologin

# 范例4:在第二行后(也就是加载第三行)加上「drink tea?」字样
[mrcode@study ~]$ nl /etc/passwd | sed '2a drink tea?'
1 root:x:0:0:root:/root:/bin/bash
2 bin:x:1:1:bin:/bin:/sbin/nologin
drink tea?
3 daemon:x:2:2:daemon:/sbin:/sbin/nologin

# 范例5:在第二行后面加入两行字
# 注意:不要一开始就写好所有的单引号,因为需要使用 \ + 回车触发换行
[mrcode@study ~]$ nl /etc/passwd | sed '2a drink tea \
> drink beer?'
1 root:x:0:0:root:/root:/bin/bash
2 bin:x:1:1:bin:/bin:/sbin/nologin
drink tea
drink beer?

以行为单位的取代显示功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 范例1:将第 2-5 行的内容替换为 no 2-5 nuber
[mrcode@study ~]$ nl /etc/passwd | sed '2,5c no 2-5 number'
1 root:x:0:0:root:/root:/bin/bash
no 2-5 number

# 范例2:取出第 11-20 行
# 通过之前的知识来达成需要这样写
[mrcode@study ~]$ nl /etc/passwd | head -n 20 | tail -n 10
11 games:x:12:100:games:/usr/games:/sbin/nologin
12 ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
13 nobody:x:99:99:Nobody:/:/sbin/nologin
14 systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
15 dbus:x:81:81:System message bus:/:/sbin/nologin
16 polkitd:x:999:998:User for polkitd:/:/sbin/nologin
17 colord:x:998:997:User for colord:/var/lib/colord:/sbin/nologin
18 libstoragemgmt:x:997:995:daemon account for libstoragemgmt:/var/run/lsm:/sbin/nologin
19 rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
20 saslauth:x:996:76:Saslauthd user:/run/saslauthd:/sbin/nologin
# 注意需要使用 -n 只输出 sed 处理过的数据
[mrcode@study ~]$ nl /etc/passwd | sed -n '11,20p'

部分数据的搜索并替换功能

除了整行的处理模式外,还可以用行为单位进行部分数据的搜索并替换的功能,基本上 sed 的搜索与替换与 vi 类似

1
2
sed 's/要被替换的字符串/新的字符串/g'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 范例1:先观察原始信息,利用 /sbin/ifconfig 查询 IP
[mrcode@study ~]$ /sbin/ifconfig
enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.0.128 netmask 255.255.255.0 broadcast 192.168.0.255
inet6 fe80::deb9:3a1b:fd0f:f6c2 prefixlen 64 scopeid 0x20<link>
ether 08:00:27:a0:49:8f txqueuelen 1000 (Ethernet)
RX packets 2436261 bytes 219827411 (209.6 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 2011081 bytes 319310584 (304.5 MiB)
# 还未讲解到 IP,这里先关注第二行的 IP

# 利用关键词配合 grep 截取出关键的一行数据
[mrcode@study ~]$ /sbin/ifconfig enp0s3 | grep 'inet '
inet 192.168.0.128 netmask 255.255.255.0 broadcast 192.168.0.255

# 将 ip 前面的信息删除,也就是 inet
[mrcode@study ~]$ /sbin/ifconfig enp0s3 | grep 'inet ' | sed 's/inet //g'
192.168.0.128 netmask 255.255.255.0 broadcast 192.168.0.255
# 需要使用通配符,不然会留下前面的空白符号:任意字符开头另个或多个
[mrcode@study ~]$ /sbin/ifconfig enp0s3 | grep 'inet ' | sed 's/^.*inet //g'
192.168.0.128 netmask 255.255.255.0 broadcast 192.168.0.255

# 再删除后续部分,只剩下 192.168.0.128
# 注意这里需要使用:空格任意个,来匹配前面多个空格
[mrcode@study ~]$ /sbin/ifconfig enp0s3 | grep 'inet ' | sed 's/^.*inet //g' | sed 's/ *netmask.*$//g'
192.168.0.128

上面例子建议一步一步的来做,下面继续研究 sed 与正则表示法配合练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 范例2:只要 MAN 存在的那几行数据,但是含有 # 在内的批注和空白行不要
# 步骤1:先使用 grep 将关键词 MAN 所在行取出来
[mrcode@study ~]$ cat /etc/man_db.conf | grep 'MAN'
# MANDATORY_MANPATH manpath_element
# MANPATH_MAP path_element manpath_element
# MANDB_MAP global_manpath [relative_catpath]
# every automatically generated MANPATH includes these fields
#MANDATORY_MANPATH /usr/src/pvm3/man
MANDATORY_MANPATH /usr/man
MANDATORY_MANPATH /usr/share/man
...省略...
# 步骤2:删除掉批注数据行
[mrcode@study ~]$ cat /etc/man_db.conf | grep 'MAN' | sed 's/^#.*$//g'





MANDATORY_MANPATH /usr/man
MANDATORY_MANPATH /usr/share/man
MANDATORY_MANPATH /usr/local/share/man
# 步骤3:删除空白行
# 注意这里使用了动作里面的 d 命令,前面是正则匹配?
[mrcode@study ~]$ cat /etc/man_db.conf | grep 'MAN' | sed 's/^#.*$//g' | sed '/^$/d'
MANDATORY_MANPATH /usr/man
MANDATORY_MANPATH /usr/share/man
MANDATORY_MANPATH /usr/local/share/man

直接修改文件内容(危险动作)

1
2
3
4
5
6
7
8
9
10
# 范例1:利用 sed 将 /tmp/regular_express.txt 内每一行结尾若为 . 则换成 !
# 下面还是使用了动作 s 替换,后面的是转义 . 和 !
# 这样可以直接修改文件内容
[mrcode@study tmp]$ sed -i 's/\./\!/g' regular_express.txt

# 范例2:利用 sed 直接在 /tmp/regular_express.txt 最后一行加入 # This is a test
# $ 表示最后一行
[mrcode@study tmp]$ sed -i '$a # This is a test ' regular_express.txt
# 想要删除最后一行就简单了
[mrcode@study tmp]$ sed -i '$d' regular_express.txt

延伸正则表示法

一般来说,只要了解了基础正则表示法大概就已经相当足够了,所谓技多不压身;还可以了解使用范围更广的延伸正则表示法。举个例子:前面讲解到要去除空白行与行首为 # 的行,使用的是

1
grep -v '^$' regular_express.txt | grep -v '^#'

需要使用到管线命令来搜寻两次,使用延伸的正则表示法则如下

1
egrep -v '^$|^#' regular_express.txt

此外,grep 预设仅支持基础的正则表示法,可以使用 -E 参数开启,不过建议用别名 egrep

下面是延伸正则表示法的符号(RE 字符)说明:

+:重复「一个或一个以上」的前一个 RE 字符

范例:搜索 (god)(good)(goood)...等字符串。 可以使用

1
2
3
4
[mrcode@study tmp]$ egrep -n 'go+d' regular_express.txt 
1:"Open Source" is a good mechanism to develop programs!
9:Oh! The soup taste good!
13:Oh! My god!

?:「0 个或 1 个」的前一个 RE 字符

范例:搜索 gdgod

1
2
3
[mrcode@study tmp]$ egrep -n 'go?d' regular_express.txt 
13:Oh! My god!
14:The gd software is a library for drafting programs!

|:用或(or)的方式找出数个字符串

范例:搜索 gdgood

1
2
3
4
[mrcode@study tmp]$ egrep -n 'gd|good' regular_express.txt 
1:"Open Source" is a good mechanism to develop programs!
9:Oh! The soup taste good!
14:The gd software is a library for drafting programs!

():找出「群组」字符串

范例:搜索 gladgood

1
2
3
4
5
6
# 当然,这里使用上面完整的或来匹配两个固定单词也是可以的
[mrcode@study tmp]$ egrep -n 'g(la)|(oo)d' regular_express.txt
1:"Open Source" is a good mechanism to develop programs!
2:apple is my favorite food!
9:Oh! The soup taste good!
16:The world <Happy> is the same with "glad"!

()+:多个重复群组的判别

范例:将「AxyzxyzxyzxyzC」用 echo 叫出,然后再使用如下的方法搜索

1
2
3
4
5
6
[mrcode@study tmp]$ echo 'AxyzxyzxyzxyzC' | egrep 'A(xyz)'
Axyz xyzxyzxyzC # 在命令行中是有红色高亮的,这个只能高亮到 Axyz
[mrcode@study tmp]$ echo 'AxyzxyzxyzxyzC' | egrep 'A(xyz)+'
Axyzxyzxyzxyz C # C 不会高亮
[mrcode@study tmp]$ echo 'AxyzxyzxyzxyzC' | egrep 'A(xyz)+C'
AxyzxyzxyzxyzC # 完全匹配

TIP

要特别注意:grep -n '[!>]' xx.txt 的含义并不是除了 > 字符之外的字符,因为 ! 不是一个特殊符号

想要表示非,需要这样写 grep -n '[^a-z]' xx.txt

文件的格式化与相关处理

不需要通过 vim 去编辑,而是通过数据流重导向配置 printf 功能以及 awk 指令,可以对文字信息进行排版显示

格式化打印:printf

比如将考试分数输出,姓名与科目及分数之间,稍微做个比较漂亮的版面,比如输出下面这样的表格

1
2
3
4
Name		Chinese		Enlish		Math		Average
DmTsai 80 60 92 77.33
VBird 75 55 80 70.00
Ken 60 90 70 73.33

上表数据主要分成 5 个字段,每个字段之间可以使用 tab 或空格进行分割。将上表存储到 printf.txt 文件中,后续会使用到这个文件进行练习。

由于每个字段的长度并不一样,所以要达到上表效果,就需要打印格式管理员 printf 来帮忙了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
printf '打印格式' 实际类容
选项与参数:
关于格式方面的几个特殊样式:
\a 警告剩余输出
\b 退格键(backspace)
\f 清楚屏幕(form feed)
\n 输出新的一行
\r Enter 按键,换行
\t 水平的 tab 按键
\v 垂直的 tab 按键
\xNN NN 为两位数的数字,可以转换数字称为字符
关于 C 程序语言内,常见的变量格式:
%ns n 数字,s 表示 string,也就是多少个字符
%ni n 数字,i 表示 integer,多少整数数字
%N.nf n 与 N 都是数字,f 表示 floating(浮点),如果有小数,比如共 10 个位数,小数点 2 位数,则写成 %10.2f

下面进行练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 范例 1:将上面存储的 printf.txt 内容仅列出姓名与成绩,并且用 tab 分割
# 文件存储时,字段之间全部用 tab 隔开的,复制进去就变成下面展示这样了
[mrcode@study tmp]$ cat printf.txt
Name Chinese Enlish Math Average
DmTsai 80 60 92 77.33
VBird 75 55 80 70.00
Ken 60 90 70 73.33
# 由于 printf 不是管线命令,需要通过 cat 先提取出来内容
# %s 表示不固定长度的字符串,后面跟了一个空格,并使用横向制表符 \t 来格式化
[mrcode@study tmp]$ printf '%s \t %s \t %s \t %s \t %s \t \n ' $(cat printf.txt)
Name Chinese Enlish Math Average
DmTsai 80 60 92 77.33
VBird 75 55 80 70.00
Ken 60 90 70 73.33

可以看到上述的效果虽然好多了,但是还是没有对齐。可能是由于 Chinese 比其他的长度要长,导致对不齐,那么下面来固定长度

1
2
3
4
5
6
7
8
9
10
# 范例 2:将上述第二行以后,分别以字符串、整数、小数点来显示
# grep -v Name 排除包含 Name 字符的行
[mrcode@study tmp]$ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt | grep -v Name)
DmTsai 80 60 92 77.33
VBird 75 55 80 70.00
Ken 60 90 70 73.33
# 由于这里是格式化数字,所以第一行无法使用这里的表达式,如果使用将得到数字 0 的展示
# 展示效果好了很多
%10s:这一个字段永远显示 10 个字符宽度,不足的用空格补位
%8.2f:表示 00000.00

printf 除了可以格式化处理之外,还可以根据 ASCII 的数字与图形对应来显示数据,如下

1
2
3
4
5
# 范例 3: 列出 16 进制 45 代表的字符是什么
[mrcode@study tmp]$ printf '\x45\n'
E
# 可以将数值转换为字符,如果你会写 script 的话
# 可以测试下,20~80 之间的数值表示的字符是什么

printf 使用相当广泛,包括后面提到的 awk 以及在 c 程序语言中使用的屏幕输出,都是利用 printf

printf 使用场景就是格式化输出,如果你要写自己的软件,把信息漂亮的输出到屏幕的话,可是很有用的

awk:好用的数据处理工具

  • sed:常常用于一整行的处理
  • awk:倾向于将一行分成数个字段来处理

因此,awk 适合处理小型的数据处理。

1
2
3
4
awk '条件类型1{动作1} 条件类型2{动作2} ...' filename

awk 后可以跟文件,也可以接受前个指令的 standard output
awk 主要处理每一行的字段内的数据,他默认的分隔符为「空格键」或「tab 键」
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 范例:使用 last 将登录者数据取出来
[mrcode@study tmp]$ last -n 5 # 取出前 5 行
mrcode pts/1 192.168.0.105 Wed Jan 15 22:20 still logged in
mrcode pts/0 192.168.0.105 Wed Jan 15 22:20 still logged in
reboot system boot 3.10.0-1062.el7. Wed Jan 15 22:19 - 23:05 (00:45)
mrcode pts/1 192.168.0.105 Mon Jan 13 22:51 - 23:13 (00:22)
mrcode pts/0 192.168.0.105 Mon Jan 13 22:51 - 23:13 (00:22)

# 若要取出账户与登录 IP ,且账户与 IP 之间以 tab 隔开,可以这样写
[mrcode@study tmp]$ last -n 5 | awk '{print $1 "\t" $3}'
mrcode 192.168.0.105
mrcode 192.168.0.105
reboot boot
mrcode 192.168.0.105
mrcode 192.168.0.105

wtmp Fri
# 由于每一行数据都需要处理,所以不需要有条件类型
# 通过 print 功能将数据列出来
# 第 3 行数据被误判了,第二个字段中包含了空格
# 那么 $1 开始的变量表示哪一个字段,要注意的是:$0 表示整行数据

对于上面示例,awk 的处理流程是:

  1. 读入第一行,并将第一行的内容填入 $0、$1... 变量中
  2. 依据 条件类型 的限制,判断是否需要进行后面的 动作
  3. 做完所有的动作与条件类型
  4. 若还有后续的「行」数据,则重复上面 1~3 步骤,直到所有数据都处理完为止

awk 是「以行为一次处理的单位」而「以字段为最小的处理单位」,那么 awk 中还提供了以下变量信息

变量名称 含义
NF 每一行($0)拥有的字段总数
NR 目前 awk 所处理的是「第几行」数据
FS 目前的分割字符,默认是空格

继续上面 last -n 5 的例子来做说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 想要列出每一行的账户:就是 $1
# 列出目前处理的行数:NR 变量
# 该行有多少字段:NF 变量
# 注意:在 awk 的格式内使用 print 打印时,非变量部分需要用双引号引用起来,因为 awk 动作是以单引号的
[mrcode@study ~]$ last -n 5 | awk '{print $1 "\t lines:" NR "\t columns:" NF}'
mrcode lines:1 columns:10
mrcode lines:2 columns:10
reboot lines:3 columns:11
mrcode lines:4 columns:10
mrcode lines:5 columns:10
lines:6 columns:0
wtmp lines:7 columns:7
# 注意 NF 等变量不需要有 $ 并且需要大写

awk 的逻辑运算字符

既然有「条件」,那么就有逻辑运算符号

运算单元 代表意义
> 大于
< 小于
>= 大于或等于
>= 小于或等于
== 等于
!= 不等于

范例:在 /etc/passwd 中是以冒号「:」来分割字段的,第一个字段为账户,第三字段则是 UID

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查阅 第三栏小于 10 以下的数据,并且仅列出账户与第三栏
# FS 是字段分隔符
[mrcode@study ~]$ cat /etc/passwd | awk '{FS=":"} $3 < 10 {print $1 "\t" $3}'
root:x:0:0:root:/root:/bin/bash
bin 1
daemon 2
adm 3
lp 4
sync 5
shutdown 6
halt 7
mail 8

第一行,没有生效是为啥呢?在 awk 中,在上述定义中,FS 仅能在第二行开始,

1
2
3
4
5
6
# 需要使用关键字 BEGIN,对应的还有 END
[mrcode@study ~]$ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t" $3}'
root 0
bin 1
daemon 2

使用 awk 的计算功能,比如有如下的数据 pay.txt

1
2
3
4
5
Name		1st		2nd		3th
Mrcode 2300 2400 2500
DMTsai 2100 2000 2300
Mrcode2 4300 4200 4100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 计算每个人的总额,而且还要格式化输出
- 第一行是说明,不需要计算,所以需要使用条件 NR=1 时再处理
- 第二行才开始计算,NR >=2 才处理

[mrcode@study tmp]$ cat pay.txt |
> awk 'NR==1 {printf "%10s %10s %10s %10s %10s\n",$1,$2,$3,$4,"Total" }
> NR>=2 {total = $2 + $3 + $4 ; printf "%10s %10d %10d %10d %10.2f\n",$1,$2,$3,$4,total}'
Name 1st 2nd 3th Total
Mrcode 2300 2400 2500 7200.00
DMTsai 2100 2000 2300 6400.00
Mrcode2 4300 4200 4100 12600.00

为了方便复制,这里粘贴上完整的一行命令:cat pay.txt | awk 'NR==1 {printf "%10s %10s %10s %10s %10s\n",$1,$2,$3,$4,"Total" } NR>=2 {total = $2 + $3 + $4 ; printf "%10s %10d %10d %10d %10.2f\n",$1,$2,$3,$4,total}'

# 现在来分解上面指令
# 1. 在 awk 中,非变量需要使用双引号引用起来
# 2. 使用 printf 时,需要加上 \n 才能换行
# 下面的含义是,当是第一行的时候,执行打印个格式化,前面是格式化表达式
# 后面用逗号分割,给出对应内容,这里给出了 1~4 个字段,并新增了一个 total 字段
[mrcode@study tmp]$ cat pay.txt | awk 'NR==1 {printf "%10s %10s %10s %10s %10s\n",$1,$2,$3,$4,"total"}'
Name 1st 2nd 3th total

# 对于计算的讲解
# 1. 在{} 动作中可以设置变量,进行运算;这里设置了一个 total 变量,并把 1~3 个字段相加
# 2. 由于这里有多个指令,所以需要使用冒号 「;」 进行分割
# 3. 使用 printf 常规打印,第 5 个字段引用了动作内设置的变量 total,记住 awk 中引用变量不需要使用 % 符号
[mrcode@study tmp]$ cat pay.txt | awk 'NR>=2 {total=$1+$2+$3 ; printf "%10s %10d %10d %10d %10.2f\n",$1,$2,$3,$4,total}'
Mrcode 2300 2400 2500 4700.00
DMTsai 2100 2000 2300 4100.00
Mrcode2 4300 4200 4100 8500.00

# 那么上面两条是针对各自条件进行处理的,相当于 if 语句;多个条件动作之间使用空格分割;链接起来就完成了

利用 awk 可以帮助我们处理很多日常工作了,在 awk 的输出格式中,常常会以 printf 来辅助。另外在 {} 动作内,也支持 if(条件) 语句。那么上面的指令可以使用 if 来做,如下

1
2
3
cat pay.txt | awk '{if(NR==1) printf "%10s %10s %10s %10s %10s\n",$1,$2,$3,$4,"Total" } NR>=2 {total = $2 + $3 + $4 ; printf "%10s %10d %10d %10d %10.2f\n",$1,$2,$3,$4,total}'


笔者没有感觉这个 if 有多方便啊?

另外,awk 还可以进行循环计算,不过这个属于比较进阶的单独课程了

文件比对工具

通常会在同一个软件包的不同版本之间,比较配置文件与原始文件的差异的时候,就会用到文件对比。

很多时候所谓的对比,通常是用在 ASCII 纯文本的比对。常见的指令有 diff,还可以使用 cmp 来对比非纯文本。同时也可以使用 diff 建立分析文档,以处理补丁 patch 功能的文件

diff

diff 用在比对两个文件之间的差异,以行为单位来比对的。一般是用在 ASCII 纯文本文件的比对上。

比如:将 /etc/passwd 删除第 4 行,第 6 行则替换为「no six line」,新文件放置到 /tmp/test 里,该如何做?

1
2
3
4
5
6
7
8
# 创建测试目录
[mrcode@study tmp]$ mkdir -p /tmp/testpw
[mrcode@study tmp]$ cd /tmp/testpw/
[mrcode@study testpw]$ cp /etc/passwd passwd.old
# sed -e 直接在命令行模式上修改;d 是删除,c是替换;前面 sed 中有讲到过的
# 这里把修改后的内容存到了 passwd.new 文件中
# sed 中有超过两个以上的动作时需要加 -e
[mrcode@study testpw]$ cat /etc/passwd | sed -e '4d' -e '6c no six line' > passwd.new
1
2
3
4
5
6
7
8
9
10
11
diff [-bBi] from-file to-file
选项与参数:

from-file:文件名,原始对比文件
to-file:文件名,目的比较文件
注意:两个文件,都可以使用 - 表示,- 代表 standard input

-b:忽略一行当中,仅有多个空白的差异;例如:“about me“ 与 “about me” 视为相同
-B:忽略空白行的差异
-i:忽略大小写的不同

1
2
3
4
5
6
7
8
9
10
# 范例 1:比对 passwd.old passwd.new 文件
[mrcode@study testpw]$ diff passwd.old passwd.new
4d3 # 左边第 4 行被删除(d)掉了,基准是右边第 3 行
< adm:x:3:4:adm:/var/adm:/sbin/nologin # 列出了左边被删除的那一行内容
6c5 # 左边第 6 行,被替换(c)成右边文件的第 5 行
< sync:x:5:0:sync:/sbin:/bin/sync # 左边文件第 6 行内容
---
> no six line # 右边文件第 5 行内容
# 注意这里的,左边第 4 行被删除意思是:左边文件是完整的,右边是修改之后的,右边与左边对比,原来的第 4 行被删除了

如果用 diff 去对比两个完全不相干的文件,是对比不出来什么的;另外 diff 还可以对比整个目录下的差异

1
2
3
4
# 范例:了解一下不同的开机执行等级(runlevel)内容有啥不同?假设你已经知道执行等级 0 与 5的启动脚本分别放置到 /etc/rc0.d 及 /etc/rc5.d 则可以对比下
[mrcode@study testpw]$ diff /etc/rc0.d/ /etc/rc5.d/
只在 /etc/rc0.d/ 存在:K90network
只在 /etc/rc5.d/ 存在:S10network

cmp

cmp 主要也是对比两个文件,主要利用字节单位去对比

1
2
3
4
cmp [-l] file1 file2
-i:将所有的不同点的字节处都列出来。因为 cmp 预设仅会输出第一个发现的不同点


1
2
3
# 范例 1:用 cmp 比较 passwd.old 与 passwd.new
[mrcode@study testpw]$ cmp passwd.old passwd.new
passwd.old passwd.new 不同:第 106 字节,第 4 行

patch

patchdiff 可配合使用,diff 比较出不同,而 patch 则可以将「旧文件升级为新的文件」。

  1. 先比较新旧版本的差异
  2. 将差异制作成补丁文件
  3. 再由补丁文件更新旧文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 范例 1:以 /tmp/testpw 内的 passwd.old 与 passwd.new 制作补丁文件
[mrcode@study testpw]$ diff -Naur passwd.old passwd.new > passwd.patch
[mrcode@study testpw]$ cat passwd.patch
--- passwd.old 2020-01-17 15:58:55.405462402 +0800 # 新旧文件的信息
+++ passwd.new 2020-01-17 16:01:03.115462402 +0800
@@ -1,9 +1,8 @@ # 新旧文件要修改数据的界定范围,旧文件在 1-0 行,新文件在 1-8 行
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
-adm:x:3:4:adm:/var/adm:/sbin/nologin # 左侧文件删除
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
-sync:x:5:0:sync:/sbin:/bin/sync # 左侧文件删除
+no six line # 右侧新加入
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin

# 这里怎么理解? 可以理解为 old 文件是基准文件
# 根据这里的基准文件,看到 - 就剪掉,看到 + 就增加;执行完成后,则会得到 new 这个文件;
# 并且补丁中限制了行数。

passwd.old 同步为 passwd.new 相同的内容,

1
2
3
4
5
6
7
8
9
10
11
12
# 由于系统未预装 patch 软件,需要将之前的 iso 镜像文件挂载
# 在虚拟机上找到顺序为 0 的控制器位置,选择 iso 文件,设备就能被 linux 找到了
[root@study ~]# mount /dev/sr0 /mnt/
mount: /dev/sr0 写保护,将以只读方式挂载
[root@study ~]# rpm -ivh /mnt/Packages/patch-2.*
警告:/mnt/Packages/patch-2.7.1-11.el7.x86_64.rpm: 头V3 RSA/SHA256 Signature, 密钥 ID f4a80eb5: NOKEY
准备中... ################################# [100%]
正在升级/安装...
1:patch-2.7.1-11.el7 ################################# [100%]
[root@study ~]# umount /mnt/
[root@study ~]# exit
# 透过上述方式安装所需软件

语法

1
2
3
4
5
6
patch -pN < patch_file  # 更新
patch -R -pN < patch_file # 还原

选项与参数:
-p:后面可以接 取消几层目录 的意思
-R:代表还原,将新的文件还原成原来的旧文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 范例 2:将刚刚制作出来的 patch file 用来更新旧版本数据
[mrcode@study testpw]$ patch -p0 < passwd.patch
patching file passwd.old
[mrcode@study testpw]$ ll passwd.*
-rw-rw-r--. 1 mrcode mrcode 2266 1月 17 16:01 passwd.new
-rw-r--r--. 1 mrcode mrcode 2266 1月 17 16:50 passwd.old # 文件大小和new文件一样了
-rw-rw-r--. 1 mrcode mrcode 480 1月 17 16:38 passwd.patch

# 范例 3:恢复旧文件内容
[mrcode@study testpw]$ patch -R -p0 < passwd.patch
patching file passwd.old
[mrcode@study testpw]$ ll passwd.*
-rw-rw-r--. 1 mrcode mrcode 2266 1月 17 16:01 passwd.new
-rw-r--r--. 1 mrcode mrcode 2323 1月 17 16:52 passwd.old

这里为什么会使用 -p0 ?因为两个文件在同一个目录下,因此不需要减去目录。如果是整体目录比对(diff 旧目录 新目录)时,就要依据建立 patch 文件所在目录来进行目录删减

更详细的 patch 用法在后续的第二十章「原始码编译」

文件打印准备:pr

在图形界面中的文字处理软件,打印时可以选择每一页的标头和页码,在文字界面下,可以使用 pr 来实现,由于 pr 参数实在太多了,这里使用最简单的方式来处理

1
2
3
4
5
6
7
8
9
10
11
12
# 打印 /etc/man_db.conf
[mrcode@study testpw]$ pr /etc/man_db.conf


2018-10-31 04:26 /etc/man_db.conf 第 1 页


#
#
# This file is used by the man-db package to configure the man and cat paths.
# It is also used to provide a manpath for those without one by examining
# their PATH environment variable. For details see the manpath(5) man page.

最上面的一行就是 pr 处理之后的效果。依次是:文件时间、文件名、页码

学习 Shell Scripts

基本上 shell script 类似早期的批处理文件,将一些指令汇总起来一次执行,但是 shell script 拥有更强大的功能,可以进行类似程序的编写,并且不需要经过编译就可以执行。

我们可以通过 shell script 来简化我们日常的管理工作,而且整个 Linux 环境中,一些服务(services)的启动都是透过 shell script 的。

所以 shell scripts 是很重要的课程

什么是 Shell Scripts

Shell Scripts :程序化脚本;

1
2
3
- shell :在前面第十章中讲过的 BASH,是一个文字接口让我们与系统沟通的一个工具接口。
- script:脚本

那么就是针对 shell 写的脚本

shell script 可以简单的看成是批处理文件,也可以称为一种程序语言,该语言是利用 shell 与相关工具指令,所以不需要编译即可执行,且有不错的 debug 工具,所以,它可以帮助系统管理员快速的管理好主机

为什么要学习 shell scripts?

简单说:想要玩清楚 Linux 的来龙去脉,shell script 是必须的知识,因为:

  • 自动化管理的重要依据

    管理一部主机每天要进行的任务就有:

    • 查询登录文件
    • 追踪流量
    • 监控用户使用主机状态
    • 主机各项硬件设备状态
    • 主机软件更新查询

    等等,这里白不包括有其他使用者突然的要求了。这些工作进行又可以分为:

    1. 自行手动处理
    2. 写个简单的程序来帮你每日「自动处理分析」
  • 追踪与管理系统的重要工作

    CentOS 6.x 以前的版本中,系统的服务(services)启动的接口是在 /etc/init.d 目录下,所有文件都是 scripts;另外,包括开机(booting)过程也是利用 shell script 来帮忙搜索系统的相关设置数据,再代入各个服务的设置参数。

    比如:想要重新启动系统注册表,可以使用 /etc/init.d/rsyslogd restart rsyslogd 文件就是 script

    另外,比如 Mysql 数据库服务启动时,有可能就在 script 中主动以「空密码」尝试登陆 Mysql,为了安全性,那么你就可以修改这个 script 文件。

    虽然 /etc/init.d/* 这个脚本目前的启动方式(systemV)已经被新一代的 systemd 所代替了(从 CentOS 7 开始),但是很多的个别服务在管理他们的服务启动方面,还是使用 shell script 的机制

  • 简单入侵检测功能

    当系统有异常状态时,大多会讲这些记录在「系统注册表」中(系统记录器),那么就可以在固定的几分钟内主动的去分析注册表文件,若察觉有问题,就立刻通知管理员,或者是立刻加强防火墙的规则,如此一来,主机就能过达到自我保护的聪明学习功能了。

    比如:可以通过 shell script 分析「当该封包尝试几次还是联机失败之后,就抵挡住该 IP」之类的动作

  • 连续指令单一化

    简单说,script 最简单的功能就是,将一批指令写入 script 中,达到执行一个文件就能下达一批指令的目的。

    比如:防火墙连续规则(iptables)、开机加载程序的项目(/etc/rc.d/rc.local) 等等

  • 简易的数据处理

    前面几章讲解的如 awk 等指令就可以用来处理简单的数据。配合各种指令来达到处理数据的目的

  • 跨平台支持与学习历程较短

    几乎所有的 Unix Like 上都可以运行 shell script,连 MS Windows 系列也有相关的 script 仿真器可以用

虽然 shell script 号称是程序,实际上,shell script 处理数据的速度上还是不够快,因为用的是外部的指令与 bash shell 的一些默认工具,所以常常去调用外部的函数库,因此指令周期上面比不上传统的程序语言

所以,shell script 用在系统管理上是很好的一项工具,但是用在处理大量数值运算上,就不行了,速度较慢,使用 CPU 资源较多,造成主机资源的分配不良。我们通常利用 shell script 来处理服务器的侦测就比较合适

第一支 script 的编写与执行

shell script 是纯文本文件,可以在里面一次性执行多个指令,或者是利用一些运算与逻辑判断来帮助我们达成某些功能。所以需要具备 bash 指令下达相关知识(第四章中开始下达指令中讲过),除此之外,还有以下知识需要了解:

  1. 指令的执行是从上而下、从左而右的分析与执行
  2. 指令的下达:指令、选项与参数间的多个空白都会被忽略掉
  3. 空白行也将被忽略,并且「tab」按键锁推开的空白行同样视为空格
  4. 如果读取到一个 enter 符号(CR),就尝试开始执行改行(或该串)命令
  5. 如果一行内容太多,则可以使用「\[Enter]」来延伸至下一行
  6. #可作为批注。任何加在 #后面的文字将被视为批注文字而被忽略

假设现在存在一个 script/home/mrcode/shell.sh,有如下的方式执行这个文件

  • 直接指令下达:shell.sh 文件必须有可读与执行权限(rx)
    • 绝对路径:使用 /home/mrcode/shell.sh执行
    • 相对路径:假设工作目录在 /home/mnrcode,就使用 ./shell.sh执行
    • 变量「PATH」功能:将 shell.sh 放在 PATH 指定目录内,例如 ~/bin/
  • bash 程序来执行:bash shell.shsh shell.sh 执行

至于那个相对路径 ./shell.sh 为什么需要这样,是因为 路径与指令搜索顺序 的关系;

sh shell.sh 为啥可以执行?

1
2
3
4
5
[mrcode@study ~]$ type -a sh
sh is /usr/bin/sh
[mrcode@study ~]$ ll /usr/bin/sh
lrwxrwxrwx. 1 root root 4 Jan 17 14:32 /usr/bin/sh -> bash

可以看到 shbash 的链接文件,同时还可以使用参数 -n 和 -x 来检查与追踪 shell.sh 的语法是否正确

Hello World

先来一个 Hello World 脚本,再来逐步说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[mrcode@study ~]$ pwd
/home/mrcode
[mrcode@study ~]$ mkdir bin; cd bin
[mrcode@study bin]$ vi hello.sh

#!/bin/bash
# Program:
# This program shows "Hello World" in your screen.
# HIstory:
# 2020/02/19 mrcode first release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
echo -e "Hello World! \a \n"
exit 0

在本章中,请将所有编写的 script 放置到你家目录的 ~/bin 目录内,未来比较好管理,针对如上脚本的写法,分段说明:

  1. #!/bin/bash:宣告这个 script 使用的 shell 名称

    因为我们使用的是 bash,所以必须以 #!/bin/bash 来声明该文件内的语法使用 bash 语法。

    当这个程序被执行时,能够加载 bash 的相关环境配置文件(一般来说是 non-login shell~/.bashrc),并且执行 bash 来使指令能够执行。很多情况下导致无法执行可能就是因为这一行的原因,系统无法判断该 sh 文件使用什么 shell 来执行

  2. 程序内容说明

    整个文件中,除了第一行的 #! 是用来声明 shell 之外,其他的 #都是批注信息。一般来说,建议一定要养成说明该 script 的:

    1. 内容与功能
    2. 版本信息
    3. 作者与联络方式
    4. 建档日期
    5. 历史记录

    等等,这将有助于未来程序的改写与 debug

  3. 主要环境变量的声明

    建议务必将一些重要的环境变量设置好,PATHLANG(输出相关信息时) 是当中最重要的,如此一来就可以直接下达外部指令,而不用写绝对路径,比较方便

  4. 主要程序部分

    在本例中,就是 echo 那一行

  5. 执行结果告知(定义回传值)

    指令回传值 中讲解到,可以使用 $? 来观察,那么可以利用 exit 这个指令来让程序中断,并且回传一个数值给系统。

    本例中使用的是 exit 0,表示离开 script 并且回传一个 0 给系统,所以执行完这个 shell.sh 之后,下达 echo $? 则可以得到 0 的值。

    利用这个 exit nn 是数字)的功能,还可以自定义错误信息,让这支程序变得更加智能

执行与观察结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 观察权限,目前没有 x 执行权限
[mrcode@study bin]$ ll
total 4
-rw-rw-r--. 1 mrcode mrcode 239 Jan 19 11:25 hello.sh
# 尝试执行,报错无权限
[mrcode@study bin]$ ./hello.sh
-bash: ./hello.sh: Permission denied
# 添加执行权限
[mrcode@study bin]$ chmod u+x hello.sh
[mrcode@study bin]$ ll
total 4
-rwxrw-r--. 1 mrcode mrcode 239 Jan 19 11:25 hello.sh
# 执行脚本
[mrcode@study bin]$ ./hello.sh
Hello World!

# 查看回传值
[mrcode@study bin]$ echo $?
0

编写 shell script 的良好习惯建立

一个良好习惯的养成至关重要,往往最开始时最容易忽视这部分的,觉得程序只要写出来就可以了,但是随着时间的拉长,不断的维护和修改。后续维护就可能出现问题

比如:作者管理很多计算机,由于太懒,经常同一个程序在不同的主机上进行修改,最最后也不知道哪一个程序是最新的,其中做了什么修改,又为什么做那样的修改。

所以,在写程序时,需要仔细的将程序的设计过程记录下来,而且还会记录一些历史记录,这样会导致维护成本降低

另外,在一些环节设置上面,毕竟每个人的环境都不相同,为了取得较佳的执行环境,一般都会预先定义一些一定会被用到的环境变量,例如上面的 PATH。因此养成良好的 script 编写习惯,建议在每个 script 文件头记录如下信息:

  • 功能描述
  • 版本信息
  • 作者与联系方式
  • 版权信息
  • 历史记录(History
  • script 内较为特殊的指令,使用「绝对路径」方式来下达
  • script 运作时需要的环境变量预先声明与设置

除了这些信息之外,在关键和难理解的代码部分添加批注信息。另外推荐代码编排格式使用 「巢状方式」,使用 tab 来缩进。编写 script 的工具是 vim 而不是 vi,因为 vim 有额外的语法校验机制

简单的 Shell Script 练习

本章 范例中,实现的方式很多,建议先自行编写,再参考例子,才能加深概念

简单范例

本小节范例在很多脚本程序中都会用到,而且简单

对谈式脚本:变量类容由用户决定

在很多场景中,需要用户输入一些内容,让程序可以顺利运行。比如,安装软件时,让用户选择安装目录;

BASH 中的变量读取指令 read ,那么以 read 指令的用途实现:

  1. 用户输入 first name
  2. 用户输入 last name
  3. 最后在屏幕上显示:You full name is:xxx
1
2
3
4
5
6
[mrcode@study bin]$ vim read.sh
#!/bin/bash
read -p 'first name: ' firstName
read -p 'last name: ' lastName
echo "You full name: ${firstName}${lastName}"
exit 0
1
2
3
4
5
6
7
8
# 增加执行权限
[mrcode@study bin]$ chmod a+x read.sh
# 执行
[mrcode@study bin]$ ./read.sh
first name: zhu
last name: mrcode
You full name: zhumrcode

下面是书上的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
vim showname.sh
#!/bin/bash
# Program:
# 用户输入姓名,程序显示出输入的姓名
# History:
# 2020/01/19 mrcode first release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

read -p "Please input you first name: " firstname # 提示使用者输入
read -p "Please input you last name: " lastname # 提示使用者输入
# -e 开启反斜杠转移的特殊字符显示,比如下面的 \n 换行显示
echo -e “\n Your full name is: ${firstname}${lastname}# 结果由屏幕输出
1
2
3
4
5
6
# 执行结果
[mrcode@study bin]$ ./showname.sh
Please input you first name: zhu
Please input you last name: mrcode

Your full name is: zhumrcode

笔者小结:可以看到上面这个脚本,增加了一个良好的习惯,就是脚本说明等信息

随日期变化:利用 date 进行文件的建立

考虑一个场景,每天备份 MySql 的数据文件,备份文件名以当天日期命名,如 backup.2020-01-19.data.

重点是 2020-01-19 是怎么来的?范例需求如下:

  1. 用户输入一个文件名前缀
  2. 创建出以日期为名的三个空文件(通过 touch 指令),生成 前天、昨天、今天 日期,及格式为:filename_2020-01-19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
vim create_3_filename.sh
#!/bin/bash
# Program:
# 用户输入文件名前缀,生成前天、昨天、今天的三个空文件
# History:
# 2020/01/19 mrcode first release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

echo -e "将使用 ‘touch’ 命令创建 3 个文件"
read -p "请输入文件名:" fileuser

# 容错,使用变量功能判定与赋值默认值
filename=${fileuser:-"filename"}

# date 命令的使用
date1=$(date --date='2 days ago' +%Y-%m-%d) # 两天前的日期,并格式化显示
date2=$(date --date='1 days ago' +%Y-%m-%d)
date3=$(date +%Y-%m-%d)

file1="${filename}_${date1}"
file2="${filename}_${date2}"
file3="${filename}_${date3}"

# 在这里其实可以直接拼接文件名
touch "${file1}"
touch "${file2}"
touch "${file3}"

这里使用了变量的赋值相关功能,详参考:变量功能

运行测试

1
2
3
4
5
6
7
8
9
10
11
[mrcode@study bin]$ ./create_3_filename.sh 
将使用 ‘touch’ 命令创建 3 个文件
请输入文件名:mrcode
[mrcode@study bin]$ ll
总用量 16
-rwxrwxr-x. 1 mrcode mrcode 677 1月 19 14:15 create_3_filename.sh
-rwxrwxr-x. 1 mrcode mrcode 239 1月 19 11:25 hello.sh
-rw-rw-r--. 1 mrcode mrcode 0 1月 19 14:15 mrcode_2020-01-17
-rw-rw-r--. 1 mrcode mrcode 0 1月 19 14:15 mrcode_2020-01-18
-rw-rw-r--. 1 mrcode mrcode 0 1月 19 14:15 mrcode_2020-01-19
# 一次正常输入文件名,一次直接按 enter 按键完成输入,查看是否达到默认赋值等功能

数值运算:简单的加减乘除

在变量功能课程中讲解到,需要使用 declare 来定义变量为正数才能进行计算,此外,也可以利用 $((计算表达式)) 来进行数值运算,可惜的是,bashe shell 预设仅支持整数数据。

范例需求:

  1. 要求用户输入两个变量
  2. 将两个变量相乘后输出到屏幕

下面是笔者自己写的

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# Program:
# 用户输入 2 个整数;输出相乘后的结果
# History:
# 2020/01/19 mrcode first release
read -p '请输入第一个整数:' intUser1
read -p '请输入第二个整数:' intUser2
declare -i int1=${intUser1}
declare -i int2=${intUser2}

echo -e "\n ${int1} x ${int2} = $((int1*int2))"

测试输出

1
2
3
4
5
[mrcode@study bin]$ ./multiplying.sh 
请输入第一个整数:2
请输入第二个整数:3

2 x 3 = 6

其实用下面这样的方式来定义和输出

1
2
3
4
5
6
read -p '请输入第一个整数:' intUser1
read -p '请输入第二个整数:' intUser2
total=$((${intUser1}*${intUser2})) # 使用 $((运算内容)) 方式计算
# declare -i total=${intUser1}*${intUser2} # 还可以使用此种方式
echo -e "\n ${intUser1} x ${intUser2} = ${total}"

建议用 var = $((计算内容)) 方式来计算,此种方式简单。比如

1
2
3
4
5
6
7
8
# 取余数
[mrcode@study bin]$ echo $((3 % 2))
1

# 对于小数,可以使用 bc 指令来协助
[mrcode@study bin]$ echo $((3 / 2)); echo "3/2" | bc -l
1
1.50000000000000000000

数值运算:通过 bc 计算 pi

bc 提供了一个计算 pi 的公式: pi=$(echo "scale=10; 4*a(1)" | bc -l),此计算公式可以通过 man bc | grep 'pi' 定位到相关文档。这里的 scale 是计算 pi 的精度,越高则利用到的 cpu 资源越多,计算时间越长。

好了,了解到怎么启用 pi 计算,这里要求用户输入 scale 进行计算 pi 值,并输出显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vim cal_pi.sh
#!/bin/bash
# Program:
# 用户输入 scale 的值,程序计算出 scale 精度的 pi 值,并显示
# History:
# 2020/01/19 mrcode first release
# PATH 常规赋值
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

read -p '请输入 scale 的值(10~10000)?' checking
num=${checking:-"10"}

echo -e '\n开始计算 pi 的值'
time echo "scale=${num}; 4*a(1)" | bc -l

测试输出

1
2
3
4
5
6
7
8
9
[mrcode@study bin]$ ./cal_pi.sh 
请输入 scale 的值(10~10000)?20

开始计算 pi 的值
3.14159265358979323844

real 0m0.002s
user 0m0.000s
sys 0m0.001s

script 的执行方式差异(source、sh script、./script)

不同的方式执行执行会造成不一样的结果,尤其影响 bash 的环境很大。

利用直接执行的方式来执行 script:在子程序中执行

直接指令下达 或者是利用 bashsh)来运行脚本时,都会使用一个新的 bash 环境来执行脚本的指令。也就是说这种方式执行是在子程序的 bash 内执行的。在第十章 BASH 内谈到 export 自定义变量转成环境变量 的功能时,重点在于:当子程序完成后,子程序内的各项变量或动作将会结束儿不会传回到父程序中。

1
2
3
4
5
6
7
# 运行上面范例的姓名打印
[mrcode@study bin]$ ./showname.sh
Please input you first name: m
Please input you last name: q

Your full name is: mq # echo -e "\n Your full name is: ${firstname}${lastname}" 打印出来了信息
[mrcode@study bin]$ echo ${fristname}${lastname} # 但是在父程序中却没有信息

利用 source 来执行脚本:在父程序中执行

同样的测试代码,使用 source 就不一样了

1
2
3
4
5
6
7
[mrcode@study bin]$ source showname.sh 
Please input you first name: m
Please input you last name: q

Your full name is: mq
[mrcode@study bin]$ echo ${firstname}${lastname}
mq # 在父程序中还能获取到

善用判断

在 上一章中,提到过 $? 这个变量所代表的含义,以及通过 && 和 || 来判定前一个指令执行回传值对于后一个指令是否要进行的依据。

在上一章中,判定一个目录是否存在,使用了 ll 目录 && 执行指令 的方式来判定 xx 目录是否存在,从而决定后续指令是否执行,但是有更简单的方式进行条件判断,就是通过 test 指令

使用 test 指令的测试功能

test 指令主要用于检测文件或相关属性时的指令和比较值,比如检查 /mrcode 是否存在时

1
2
# -e 是检测文件是否存在的选项
[root@cloud-08 script]# test -e /mrcode

上面命令没有任何输出值

1
2
3
[root@cloud-08 script]# test -e /mrcode && echo "exist" || "not exist"
exist
# 通过与 && 或 || 可以知道是存在还是不存在了

要善用 man 查看该指令的信息,下面是整理翻译出来的其他选项

关于某个文件名的 文件类型 判断。如 test -e filename 标识是否存在

测试的标志 含义
-e 文件是否存在;常用
-f 该文件是否存在且为文件(file)?常用
-d 该文件是否存在且为目录(directory)?常用
-b 该文件是否存在且为一个 block device 装置?
-c 该文件是否存在且为一个 character device 装置?
-S 该文件是否存在且为一个 Socket 文件?
-p 该文件是否存在且为一个 FIFOpipe)文件?
-L 该文件是否存在且为一个连接文件?

关于文件的 权限 判定。如 test -r filename 标识是否可读?(但 root 权限常有例外)

测试的标志 含义
-r 该文件是否存在且具有可读权限?
-w 该文件是否存在且具有可写权限?
-x 该文件是否存在且具有可执行权限?
-u 该文件是否存在且具有 SUID 属性?
-g 该文件是否存在且具有 SGID 属性?
-k 该文件是否存在且具有 Sticky bit 属性?
-s 该文件是否存在且为「非空白文件」?

两个文件之间的比较。如 test file1 -nt file2

测试的标志 含义
-nt newer than)判断 file1 是否比 file2
-ot older than)判断 file1 是否比 file2
-ef 判断 file1file2 是否是同一文件,可用在判断 hard link 的判定上。主要意义在判定两个文件是否均指向同一个 inode

两个整数之间的判定。test nl -eq n2

测试的标志 含义
-eq 两数值相等(equal
-ne 不相等(not equal
-gt 大于(greater than
-lt 小于(less than
-ge 大于等于(greater than or equal
-le 小于等于(less than or equal

判定字符串的数据

测试的标志 含义
test -z string 判定字符串是否为 0?若为空串,则为 true
test -n string 判定字符串是否不为 0?若为空串,则为 false;注意:-n 可省略
test str1 == str2 是否相等,相等则为 true
test str1 != str2 是否不相等,相等则为 false

多重条件判断。比如 test -r filename -a -x filename

测试的标志 含义
-a and)两状况同时成立;如:test -r filename -a -x filename,则 file 同时具有 rx 权限时才为 true
-o or)任意一个成立。如:test -r filename -o -x filename,则 file 具有 rx 权限时就为 true
! 反向状态。

总结完这么多的判定,就可以来写几个简单的例子。让用户输入一个文件名,我们判断:

  1. 该文件是否存在,若不存在则给予一个「Filename does not exist」 提示,并中断程序
  2. 若该文件存在,则判断是文件还是目录:文件输出「Filename is regular file」,目录输出 「Filename is directory
  3. 判断执行者的身份对这个文件或目录所拥有的权限,并输出权限数据

下面是笔者写的思路,代码组织方面有点糟糕。还有指令使用不太熟悉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# Program
#
# History
# 2020/01/19 mrcode first relese
read -p "请输入一个文件名:" filename
# 判断是否输入了字符串
test -z ${filename} && echo "请输入一个有效的文件名!" && exit -1

# 判断该文件是否存在: 不存在输出提示信息并退出
# 特别是这里的多条指令的执行,使用 || 会很难处理,只能转成 true
test ! -e ${filename} && echo "${filename} does not exist" && exit -1

# 提示是文件还是目录
test -f ${filename} && echo "${filename} is regular file" || echo "${filename} is directory"

# 判断执行者的身份对这个文件拥有的权限,并输出
test -r ${filename} && echo "${filename} 可读"
test -w ${filename} && echo "${filename} 可写"
test -x ${filename} && echo "${filename} 可执行"

测试输出如下

1
2
3
4
5
6
7
8
[mrcode@study bin]$ ./file_perm.sh 
请输入一个文件名:ss
ss does not exist
[mrcode@study bin]$ ./file_perm.sh
请输入一个文件名:/etc
/etc is directory
/etc 可读
/etc 可执行

书上代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
vim file_perm.sh
#!/bin/bash
# Program
# User input a filename,program will check the flowing:
# 1.) exist?
# 2.) file/directory?
# 3.) file permissions
# History
# 2020/01/19 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

echo -e "Please input a filename,I will check the filename's type and permission. \n\n"
read -p "Input a filename :" filename
# 判断是否输入了字符串
test -z ${filename} && echo "You MUST input a filename. " && exit 0

# 判断该文件是否存在: 不存在输出提示信息并退出
test ! -e ${filename} && echo "The filename ${filename} does not exist" && exit 0

# 开始判断文件类型与属性
test -f ${filename} && filetype="regulare file"
test -d ${filename} && filetype="directory"
test -r ${filename} && perm="readable"
test -w ${filename} && perm="${perm} writable"
test -x ${filename} && perm="${perm} executable"

# 信息输出
echo "The filename: ${filename} is a ${filetype}"
echo "And the permissions for you are : ${perm}"

测试输出如下

1
2
3
4
5
6
7
8
9
10
11
12
13
[mrcode@study bin]$ ./file_perm.sh 
Please input a filename,I will check the filename's type and permission.


Input a filename :ss
The filename ss does not exist
[mrcode@study bin]$ ./file_perm.sh
Please input a filename,I will check the filename's type and permission.


Input a filename :/etc
The filename: /etc is a directory
And the permissions for you are : readable executable

自己写的脚本组织来看,除了不熟悉指令用法之外,对于程序结构的抽象不够好,对比书上的,发觉这个代码组织的不错

另外,该脚本检查权限的指令是针对运行该脚本的用户所反馈的,所以当使用 root 的时候,常常会发现与 ls -l 观察到的结果并不相同

利用判断符号 []

除了 test 外,还可以使用中括号 [] 来判定

1
2
3
# 判断 ${HOME} 这个变量是否为空
[mrcode@study bin]$ [ -z "${HOME}" ]; echo $?
1

使用该种方式需要特别注意,因为中括号在很多地方都代表特殊符号,在 bash 的语法中作为 shell 判断时,必须要注意 中括号的两端需要有空格符来分隔

  • 在中括号内的每个组件都需要有空格来分隔
  • 在中括号内的变量,最好都以双引号括起来
  • 在中括号内的常量,都好都以单或双引号括起来

看一个例子,设置一个 name 变量,再用中括号方式判断

1
2
3
4
5
6
7
[mrcode@study bin]$ name="Mrcode Tset"
[mrcode@study bin]$ [ ${name} == "Mrcode" ]
-bash: [: 参数太多
# 是因为,如果 ${name} 没有使用双引号括起来就会变成 [ Mrcode Test == "Mrcode" ]
# 中括号内的变量是以空格来分隔的,那么这里就出现了 Mrcode Test “Mrcode” 三个比较对象了
# 那么使用 [ “${name}” == "Mrcode" ] 就变成了 [ “Mrcode Test” == "Mrcode" ]

除了以上注意之外,中括号使用方式与 test 几乎一模一样,只是中括号比较常用在 条件判断 if…then..fi 的情况中。

实践范例需求如下:

  1. 当执行一个程序的时候,要求用户选择 YN
  2. 如果用户输入 Yy 时,就显示「Ok,continue
  3. 如果用户输入 N 或 n 时,就显示「Oh,interrupt!
  4. 如果不是以上规定字符,则显示「I don’t know what your choice is

利用中括号、&&|| 来达成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vi ans_yn.sh
#!/bin/bash
# Program:
# This program shows the user's choice
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

read -p "请输入 Y/N:" yn
[ "${yn}" == "Y" -o "${yn}" == "y" ] && echo "Ok,continue" && exit 0
[ "${yn}" == "N" -o "${yn}" == "n" ] && echo "Oh,interrupt!" && exit 0
echo "I don't know what your choice is" && exit 0

输出测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 这里报错时因为 [ "${yn}" == "Y" || "${yn}" == "y" ]  中使用了 || 来达成条件判定
[mrcode@study bin]$ ./ans_yn.sh
请输入 Y/N:n
./ans_yn.sh: 第 10 行:[: 缺少 `]'
./ans_yn.sh:行10: n: 未找到命令
./ans_yn.sh: 第 11 行:[: 缺少 `]'
./ans_yn.sh:行11: n: 未找到命令
I don't know what your choice is

# [ "${yn}" == "Y" -o "${yn}" == "y" ] 使用了 test 中的参数, -o 只要任意一个成立都算 true
# 程序正常
[mrcode@study bin]$ vim ans_yn.sh
[mrcode@study bin]$ ./ans_yn.sh
请输入 Y/N:n
Oh,interrupt!
[mrcode@study bin]$ ./ans_yn.sh
请输入 Y/N:y
Ok,continue
[mrcode@study bin]$ ./ans_yn.sh
请输入 Y/N:
I don't know what your choice is

shell script 的默认变量 $0,$1...

指令可以带有选项与参数,如 ls -la 可以查看包含隐藏文件的所有属性。那么 script 也可以携带参数。

1
2
3
4
5
6
7
# 重新启动系统的网络
[mrcode@study bin]$ file /etc/init.d/network
/etc/init.d/network: Bourne-Again shell script, ASCII text executable
# 利用 file 指令查询该文件,显示是个可执行的 shell script 文件

# 这里携带 restart 参数,如果替换成 stop 参数就是关闭该服务了
[mrcode@study bin]$ /etc/init.d/network restart

read 是使用过程中需要手动输入,而参数是可以跟随在执行命令后的,这样就比较方便

script 针对参数已经设置好一些变量名称了,对应如下

1
2
/path/to/scriptname		opt1	opt2	opt3	opt4
&0 &1 &2 &3 &4

除了这些数字的变量参数外,还有一些较为特殊的变量可以使用

  • $#:代表后接的参数「个数」,以上表为例这里显示「4
  • $@:代表 「"&1" "&2" "&3" "&4"」 的意思,每个变量是独立的(用双引号括起来)
  • $*:代表「"&1c&2c&3c&4"」,其中 c 为分隔符,默认为空格,所以本例中代表「"&1 &2 &3 &4"

$@ 与 $* 基本上还是有所不同,一般使用 $@ 较多。

范例需求:输出如下数据

  • 程序的文件名
  • 共有几个参数
  • 若参数小于 2 ,则告知使用者参数数量太少
  • 全部的参数内容
  • 第一个参数
  • 第二个参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vi prit_info.sh
#!/bin/bash
# Program:
# 输出脚本文件名,与相关参数信息
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
echo $0
echo $#
# 这样写,语法是错误的,要记得这里是使用 test 里面的语法
# 并且,不能用 ${变量} 的方式来写
[ "${$#}" < "2" ] && echo "参数数量太少,比如大于等于 2 个" && exit 0
echo $@
echo $1
echo $2

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[mrcode@study bin]$ ./print_info.sh 
./print_info.sh
0
./print_info.sh:行11: 2: 没有那个文件或目录



[mrcode@study bin]$ ./print_info.sh a b
./print_info.sh
2
./print_info.sh:行11: 2: 没有那个文件或目录
a b
a
b

以下是书上的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vi how_paras.sh
#!/bin/bash
# Program:
# 输出脚本文件名,与相关参数信息
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

echo "The script name is ==> $0"
echo "Total parameter number is ==> $#"
[ "$#" -lt 2 ] && echo "参数数量太少,比如大于等于 2 个" && exit 0
echo "Your whole parameter is ==> '$@'"
echo "The 1st parameter ==> $1"
echo "The 2nd parameter ==> $2"

输出测试

1
2
3
4
5
6
7
8
9
10
11
[mrcode@study bin]$ ./how_paras.sh 
The script name is ==> ./how_paras.sh
Total parameter number is ==> 0
参数数量太少,比如大于等于 2 个

[mrcode@study bin]$ ./how_paras.sh a b
The script name is ==> ./how_paras.sh
Total parameter number is ==> 2
Your whole parameter is ==> 'a b'
The 1st parameter ==> a
The 2nd parameter ==> b

shift:造成参数变量位置偏移

先修改下上面的范例,how_paras.sh 先来看看效果什么是偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vi how_paras.sh
#!/bin/bash
# Program:
# Program shows the effect of shift function
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

echo "Total parameter number is ==> $#"
echo -e "Your whole parameter is ==> '$@' \n"

shift # 进行第一次 一个变量的 shift
echo "Total parameter number is ==> $#"
echo -e "Your whole parameter is ==> '$@' \n"

shift 3 # 进行第二次 三个变量的 shift
echo "Total parameter number is ==> $#"
echo "Your whole parameter is ==> '$@'"

输出如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[mrcode@study bin]$ ./how_paras.sh a b c d e f
Total parameter number is ==> 6 # 位偏移的参数数量,是 6 个
Your whole parameter is ==> 'a b c d e f'

Total parameter number is ==> 5 # 偏移一次后,只剩下 5 个,并且第一个参数 a 不见了
Your whole parameter is ==> 'b c d e f'

Total parameter number is ==> 2 # 第二次偏移掉 3 个后,b c d 不见了
Your whole parameter is ==> 'e f'

# 再来看看如果参数不够偏移会出现什么情况
[mrcode@study bin]$ ./how_paras.sh a b c # 给 3 个参数
Total parameter number is ==> 3
Your whole parameter is ==> 'a b c'

Total parameter number is ==> 2 # 第一次偏移 1 个,只生效 2 个了
Your whole parameter is ==> 'b c'

Total parameter number is ==> 2 # 第二次偏移 3 个,发现没有生效,不够偏移
Your whole parameter is ==> 'b c'


[mrcode@study bin]$ ./how_paras.sh a b c d # 给 4 个参数
Total parameter number is ==> 4
Your whole parameter is ==> 'a b c d'

Total parameter number is ==> 3 # 第一次偏移 1 个,还剩下 3 个
Your whole parameter is ==> 'b c d'

Total parameter number is ==> 0 # 第二次偏移 3 个,剩下 0 个
Your whole parameter is ==> ''

总结如下:

  • shift 可以忽略掉 n 个参数
  • shif 中的 n 必须要有足够的参数才会生效,否则不会偏移

条件判断

在程序中,没有条件判断 if then 方式的话,在执行多条指令的时候,就会很麻烦。

利用 if...then

单层、简单条件判断

1
2
3
if [ 表达式 ]; then
当条件成立时,可以进行的指令工作内容
fi

至于表达式的编码,与上一章的 test 一致,但是有一个特别的是,可以使用 &&|| 来连接多个中括号,在这里他们的含义就是表示 并且 和 或者 的意思

所以在使用中括号的时候, &&|| 与指令状态下的含义不同。比如:

1
2
3
[ "${yn}" == "Y" -o "${yn}" == "y" ]
可以替换为下面的方式
[ "${yn}" == "Y" ] || [ "${yn}" == "y" ]

这样就很方便维护了,一个中括号一个表达式。那么将这个 script 修改为 if...then 的形式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[mrcode@study bin]$ vi ans_yn.sh

#!/bin/bash
# Program:
# This program shows the user's choice
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

read -p "请输入 Y/N:" yn
# [ "${yn}" == "Y" -o "${yn}" == "y" ] && echo "Ok,continue" && exit 0
if [ "${yn}" == "Y" ] || [ "${yn}" == "y" ]; then
echo "Ok,continue"
exit 0
fi

# [ "${yn}" == "N" -o "${yn}" == "n" ] && echo "Oh,interrupt!" && exit 0
if [ "${yn}" == "N" ] || [ "${yn}" == "n" ]; then
echo "Oh,interrupt!"
exit 0
fi
echo "I don't know what your choice is" && exit 0

此方式只是在代码组织上更偏向于笔者所学的 JAVA 语言了,对于变量的判定还可以使用如下的多重判断来达到效果

多重、复杂条件判断

简单说,上述实例对于变量 ${yn} 使用了两次 if,那么可以使用如下方式简化

1
2
3
4
5
if [ 条件表达式 ]; then
做点啥
else
做点啥
fi

更复杂的情况,增加 elseif ,如下

1
2
3
4
5
6
7
if [ 条件表达式 ]; then
做点啥
elif [ 条件表达式 ]; then
做点啥
else
做点啥
fi

改写 ans_yn.sh 脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
# Program:
# This program shows the user's choice
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

read -p "请输入 Y/N:" yn
# [ "${yn}" == "Y" -o "${yn}" == "y" ] && echo "Ok,continue" && exit 0
if [ "${yn}" == "Y" ] || [ "${yn}" == "y" ]; then
echo "Ok,continue"
exit 0
else
echo "Oh,interrupt!"
exit 0
fi
echo "I don't know what your choice is" && exit 0

另一个范例知识,上一节提到参数功能(**$1),让用户在下达指令的时候将参数带进去,让用户输入 **hello 关键词,利用参数的方法可以如下设计:

  1. 判断 $1 是否为 hello ,如果是,则显示「**Hello, how ary you?**」
  2. 如果无参数,则提示使用者必须要使用的参数下达方法
  3. 如果参数不是 hello,则提示使用者仅能使用 hello 为参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
# Program:
# 直接携带参数提示
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

if [ "$1" == "hello" ]; then
echo "Hello, how ary you?"
elif [ -z "$1" ]; then
echo "请携带参数"
else
echo "只能携带参数 hello"
fi

测试输出

1
2
3
4
5
6
[mrcode@study bin]$ ./hello-2.sh 
请携带参数
[mrcode@study bin]$ ./hello-2.sh hello
Hello, how ary you?
[mrcode@study bin]$ ./hello-2.sh hellox
只能携带参数 hello

书上例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# Program:
# Chek $1 is equal to "hello"
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

if [ "$1" == "hello" ]; then
echo "Hello, how ary you?"
elif [ "$1" == "" ]; then
echo "You MUST input parameters, ex> {${0} someword}"
else
echo "The only parameter is 'hello', ex> {${0} hello}"
fi

信息输出如下

1
2
3
4
5
6
[mrcode@study bin]$ ./hello-2.sh 
You MUST input parameters, ex> {./hello-2.sh someword}
[mrcode@study bin]$ ./hello-2.sh hell
The only parameter is 'hello', ex> {./hello-2.sh hello}
[mrcode@study bin]$ ./hello-2.sh hello
Hello, how ary you?

那么深入练习。

在第十章学习了 grep 指令,现在多了解一个 netstat 指令,可以查询到目前主机有开启的网络服务端口(service ports),相关功能会在 服务器架设篇 继续介绍;这里只需要知道 netstat -tuln可以取得目前主机有启动的服务,而且取得的信息类似下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[mrcode@study bin]$ netstat -tuln
Active Internet connections (only servers)
# 封包格式 本地 IP:端口 远程 IP:端口 是否监听
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:6010 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:6011 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN
tcp 0 0 192.168.122.1:53 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp6 0 0 ::1:631 :::* LISTEN
tcp6 0 0 ::1:25 :::* LISTEN
tcp6 0 0 ::1:6010 :::* LISTEN
tcp6 0 0 ::1:6011 :::* LISTEN
tcp6 0 0 :::111 :::* LISTEN
tcp6 0 0 :::22 :::* LISTEN
udp 0 0 0.0.0.0:48829 0.0.0.0:*
udp 0 0 192.168.122.1:53 0.0.0.0:*
udp 0 0 0.0.0.0:67 0.0.0.0:*
udp 0 0 0.0.0.0:111 0.0.0.0:*
udp 0 0 127.0.0.1:323 0.0.0.0:*
udp 0 0 0.0.0.0:672 0.0.0.0:*
udp 0 0 0.0.0.0:5353 0.0.0.0:*
udp6 0 0 :::111 :::*
udp6 0 0 ::1:323 :::*
udp6 0 0 :::672 :::*

重点关注 Local Address 字段(本地主机 IP 与端口对应),代表本机所启动的网络服务,127.0.0.1 则是针对本机开放,若是 0.0.0.0 或 ::: 则代表对整个 Internet 开放。每个端口 port 都有其特定的网络服务,几个常见的 port 与网络服务的关系是:

  • 80www
  • 22ssh
  • 21ftp
  • 25mail
  • 111RPC
  • 631CUPS(打印服务功能)

假设我要检测常见端口 port 21、22、25、80 时,可以通过 netstat 检测主机是否有开启这四个主要的网络服务端口,由于每个服务的关键词都是接在冒号「**:」后面,所以可以截取类似「:80**」来检测。那么程序如下

下面是笔者写的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
vim netstat.sh

#!/bin/bash
# Program:
#
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

echo "现在开始检测当前主机上的服务"
echo -e "www、ftp、mail、www 服务将被检测 \n"
# 将 local Address 字段截取出来
datas=$(netstat -tuln | awk '{print $4}')
testing=$(grep ":80" ${datas})
if [ ! -z "${testing}" ]; then
echo "www"
fi
testing=$(grep ":22" ${datas})
if [ ! -z "${testing}" ]; then
echo "ssh"
fi
testing=$(grep ":21" ${datas})
if [ ! -z "${testing}" ]; then
echo "ftp"
fi
testing=$(grep ":25" ${datas})
if [ ! -z "${testing}" ]; then
echo "mail"
fi

不过很遗憾,grep 后只能跟一个文件路径。那么正确的做法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/bin/bash
# Program:
# Using netstat and grep to detect www⽀~Assh⽀~Aftp and mail services
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

# 1. 佅~H佑~J潟¥彎¥䷾K彝¥襾A佁~Z亾@举H
echo "潎°作¨廾@妾K梾@派K弾S佉~M主彜º䷾J潚~D彜~M佊¡"
echo -e "www⽀~Aftp⽀~Amail⽀~Awww 彜~M佊¡対F被梾@派K \n"

# 2. 达[蠾L佈¤孾Z佒~L信彁¯轾S佇º
# 対F local Address 嬾W段彈ª住~V佇º彝¥﻾L并潔~_彈~P彖~G件
testfile=/dev/shm/netstat_checking.txt
netstat -tuln | awk '{print $4}' > ${testfile}

testing=$(grep ":80" ${testfile})
if [ "${testing}" != "" ]; then
echo "www is running in you system. "
fi

testing=$(grep ":22" ${testfile})
if [ ! -z "${testing}" ]; then
echo "ssh is running in you system. "
fi

testing=$(grep ":21" ${testfile})
if [ ! -z "${testing}" ]; then
echo "ftp is running in you system. "
fi
testing=$(grep ":25" ${testfile})
if [ ! -z "${testing}" ]; then
echo "mail is running in you system. "
fi

输出信息如下

1
2
3
4
5
6
[mrcode@study bin]$ ./netstat.sh 
现在开始检测当前主机上的服务
www、ftp、mail、www 服务将被检测

ssh is running in you system.
mail is running in you system.

条件判断还可以更复杂,比如:在台湾当兵是国民应尽的义务,不过,在当兵的时候总是很想退伍,那么写个脚本程序来实现:让用户输入他的退伍日期,计算出还有多少天才退伍?的功能

那么思路如下:

  1. 用户输入自己的退伍日期
  2. 由现在的日期对比退伍日期
  3. 由两个日期的比较来显示「还需要几天」才能够退伍的字样

温馨提示:日期可以使用 date --date="YYYYMMDD" +%s 来取得指定日期的秒数,再利用秒数相减,再计算到天

笔者从现在开始,就不再贴出自己写的代码了,先自己写,然后对照书上的,最后部分修改成书上的展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
vim cal_retired.sh

#!/bin/bash
# Program:
# You input you demobilization date,I calculate how many days before you demobilize.
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

# 1. 告知用户程序的用途,并且告知应该如何输入日期格式
# 这个程序将尝试计算出,您的退伍日期还有多少天
echo "This program will try to calculate :"
echo "How many days before your demobilization date..."
read -p "Please input your demobilization date (YYYYMMDD ex>20200112):" date2

# 2. 测试判定,输入内容是否正确,使用正则表达式
date_d=$(echo ${date2} | grep '[0-9]\{8\}') # 匹配 8 位数的字符串
if [ -z "${date_d}" ]; then
# 您输入了错误的日期格式
echo "You input the wrong date format..."
exit 1
fi

# 3. 开始计算日期
declare -i date_dem=$(date --date="${date_d}" +%s) # 退伍日期秒数
declare -i date_now=$(date +%s) # 当前日期秒数
declare -i date_total_s=$((${date_dem}-${date_now})) # 剩余秒数
# 需要注意的是:这种嵌套执行的时候,括号一定要嵌套对位置
declare -i date_d=$((${date_total_s}/60/60/24)) # 转换为日
# 中括号里面不能直接使用 < 这种符号
if [ "${date_total_s}" -lt 0 ]; then
# 这里是用 -1 乘,得到是正数,标识已经退伍多少天了
echo "You had been demobilization before: $((-1*${date_d})) ago"
else
# 这里使用 总秒数 - 转换为日的变量(这里只是转换为了天),剩余数据转成小时
# 则计算到 n 天 n 小时
declare -i date_h=$(($((${date_total_s}-${date_d}*60*60*24))/60/60))
echo "You will demobilize after ${date_d} days and ${date_h} hours."
fi


测试输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[mrcode@study bin]$ ./cal_retired.sh 
This program will try to calculate :
How many days before your demobilization date...
Please input your demobilization date (YYYYMMDD ex>20200112):20200120 # 输入当天
You had been demobilization before: 0 ago

[mrcode@study bin]$ ./cal_retired.sh
This program will try to calculate :
How many days before your demobilization date...
Please input your demobilization date (YYYYMMDD ex>20200112):20200119 # 输入前一天
You had been demobilization before: 1 ago

[mrcode@study bin]$ ./cal_retired.sh
This program will try to calculate :
How many days before your demobilization date...
Please input your demobilization date (YYYYMMDD ex>20200112):20200121 # 输入明天
You will demobilize after 0 days and 8 hours.

[mrcode@study bin]$ ./cal_retired.sh
This program will try to calculate :
How many days before your demobilization date...
Please input your demobilization date (YYYYMMDD ex>20200112):2020^H^H3 # 输入错误的格式
You input the wrong date format...

[mrcode@study bin]$ ./cal_retired.sh
This program will try to calculate :
How many days before your demobilization date...
Please input your demobilization date (YYYYMMDD ex>20200112):20300120 # 输入10 年后
You will demobilize after 3652 days and 8 hours.

笔者总结:

  • 本例结合了 grep 查找符合条件的参数,如果完全不符合,则为空白返回了
  • 结合了 declare -i 定义整数变量
  • 使用了 $(($(()))) 嵌套指令执行语法
  • 该范例还是有难度的,难点在于 用正则检查输入参数 和 计算 天 并计算小时

利用 case...esac 判断

作为 JAVA 程序员,这个不用多解释,直接看语法

1
2
3
4
5
6
7
8
9
10
11
case $变量名称 in		# 关键词为 case 还有 变量前的 $ 符号
“变量内容 1”) # 每个变量内容建议用双引号括起来,关键词则为小括号
程序段
;; # 使用两个连续的分号来结尾
“变量内容 2”)
程序段
;;
*) # 最后一个变量内容需要用 * 来代表所有其他值
程序段
;;
esac # 最终的 case 结尾,就是反过来拼写的字符 esac

将上面 ./hello-2.sh 的例子使用该语法修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
./hello-3.sh 

#!/bin/bash
# Program:
# 直接携带参数提示
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

case $1 in
"hello")
echo "Hello, how ary you?"
;;
"")
echo "请携带参数"
;;
*)
echo "只能携带参数 hello"
;;
esac

此种判定方式,针对于判定字符串时会更加的方便,CentOS6.x 以前系统很多服务的启动都是使用使用这种写法写的。虽然 CentOS7 已经使用 systemd ,不过任然有数个服务时放在 /etc/init.d 目录下的、比如有个名为 netconsole 的服务在该目录下

1
2
3
# 重新启动该服务
# 注意该服务需要使用 root 身份才行,一般账户可以执行,但是不会成功
/etc/init.d/netconsole restart

查阅该文件,找到文件末尾为发现以下的内容,这里就判定了输入的参数,使用的就是 case 语法

1
2
3
4
5
6
7
case "$1" in
stop) stop ;;
status) status ;;
start|restart|reload|force-reload) restart ;;
condrestart) condrestart ;;
*) usage ;;
esac

所以对于脚本的编写,可以参考这些已经有的,看看人家是怎么写的

一般来说,使用「case $变量 in」语法,那个变量大致有两种取得方式:

  • 直接下达:利用 script.sh variable 方式直接给 $1 变量,这也是在 /etc/init.d 目录下大多数程序的设计方式
  • 交互式:通过 read 指令让用户输入变量内容

下面来演示下:

  • 用户输入 one、two、three 并显示在屏幕上
  • 如果不是以上变量,那么提示用户只有这三种选择
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vim show123.sh

#!/bin/bash
# Program:
#
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

# 如需要让用户交互性输入,那么可以用这两行代替:case "$1" in
# read -p "请输入您的选择:" choice
# case "${choice}" in

case "$1" in
"one") echo $1 ;;
"two") echo $1 ;;
"three") echo $1 ;;
*) echo "只能输入 one、two、three" ;;
esac

测试输出

1
2
3
4
5
6
7
8
9
10
[mrcode@study bin]$ ./show123.sh 
只能输入 one、two、three
[mrcode@study bin]$ ./show123.sh one
one
[mrcode@study bin]$ ./show123.sh two
two
[mrcode@study bin]$ ./show123.sh three
three
[mrcode@study bin]$ ./show123.sh three111
只能输入 one、two、three

利用 function 功能

函数功能,不用多说,可以被复用,优化程序结构,语法如下

1
2
3
function fname(){
程序段
}

TIP

由于 shell script 执行方式是由上而下,由左而右,因此 function 的代码一定要在程序的最前面

下面将 show123.sh 改写成使用 function 方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vim show123-2.sh
#!/bin/bash
# Program:
#
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

# 如需要让用户交互性输入,那么可以用这两行代替:case "$1" in
# read -p "请输入您的选择:" choice
# case "${choice}" in

function printit(){
echo -n "Your choice is " # -n 可以不断行连续在同一行显示
}

case "$1" in
"one") printit; echo $1 ;;
"two") printit; echo $1 | tr 'a-z' 'A-z' ;; # 转换为大写
"three") printit; echo $1 ;;
*) echo "只能输入 one、two、three" ;;
esac

输出信息

1
2
3
4
5
6
7
[mrcode@study bin]$ ./show123-2.sh one
Your choice is one
[mrcode@study bin]$ vim show123-2.sh
[mrcode@study bin]$ ./show123-2.sh tow
只能输入 one、two、three
[mrcode@study bin]$ ./show123-2.sh two
Your choice is TWO

上述代码,做了一个打印部分重复信息的功能,这个例子比较简单,当在程序中有大量重复,和大量逻辑的时候,就会体现出来了

同样,function 也可以有参数变量,改写成有参数调用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vim show123-3.sh

#!/bin/bash
# Program:
#
# History:
# 2020/01/20 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

function printit(){
echo "Your choice is ${1}" # 在函数域中,的参数变量,与外部的不一致
}

case "$1" in
"one") printit 1 ;;
"two") printit 2 ;;
"three") printit $1 ;;
*) echo "只能输入 one、two、three" ;;
esac

测试如下

1
2
3
4
5
6
7
8
9
[mrcode@study bin]$ ./show123-3.sh one
Your choice is 1
[mrcode@study bin]$ ./show123-3.sh two
Your choice is 2 # 可以看到,这里给定参数 1,那么在里面获取 ${1},的时候就获取到了

[mrcode@study bin]$ ./show123-3.sh three
Your choice is three # 在外部给定的是脚本中的变量 $1, 在内部也能获取到变量的具体内容
[mrcode@study bin]$ ./show123-3.sh threex
只能输入 one、two、three

循环(loop)

循环可以不断执行某个程序段楼,直到用户设定的条件达成为止。

while do done、until do done(不定循环)

当条件成立时,执行循环体

1
2
3
4
while [ condition ]   # 中括号中条件判断
do # 循环开始
程序段落
done # 循环结束

还有一种不定循环的方式,当条件成立时退出循环体

1
2
3
4
until [ condition ]
do
程序段落
done

范例:让使用者输入 yes 或则是 YES 才结束程序的执行,否则就一直告知用户输入字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vim yes_to_stop.sh

#!/bin/bash
# Program:
#
# History:
# 2020/02/12 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

while [ "${yn}" != 'yes' -a "${yn}" != 'YES' ]
# 使用 until 则是如下
# until [ "${yn}" == 'yes' -o "${yn}" == 'YES' ]
do
read -p '请输入 yes 或 YES 退出程序' yn
done

echo "你输入了正确答案"

测试如下

1
2
3
4
5
[mrcode@study bin]$ ./yes_to_stop.sh
请输入 yes 或 YES 退出程序j
请输入 yes 或 YES 退出程序jj
请输入 yes 或 YES 退出程序yes
你输入了正确答案

如果想要计算 1+2+3+..100则如下写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vim cal_1_100.sh

#!/bin/bash
# Program:
# 计算 1+2+3+..100 的结果
# History:
# 2020/02/12 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

total=0 # 计算结果
i=0 # 当前数值

while [ "${i}" != 100 ]
do
i=$(($i+1)) # 每次增加 1
total=$(($total+$i))
done
echo "1+2+3+..100 = ${total}"

for...do...done 固定循环

1
2
3
4
5
for var in con1 con2 con3 ...
do
循环体
done

范例:假设有三种动物,分别是 dogcatelephant 三种,输出三行信息,如 There are dogs... 之类的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vim show_animal.sh

#!/bin/bash
# Program:
#
# History:
# 2020/02/12 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

for animal in dog cat elephant
do
echo "There are ${animal}s..."
done

/etc/passwd 中第一个字段存放了用户名,使用循环打印出每个用户名的 id 信息;可使用 cut 截取第一字段,使用 id指令获取用户名的信息(标识符与特殊参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vim userid.sh

#!/bin/bash
# Program:
#
# History:
# 2020/02/12 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

users=$(cut -d ':' -f1 /etc/passwd) # 获取到所有的用户名
for user in ${users}
do
id ${user}
done

当然还可以使用数字来做循环项,比如需要执行 ping 192.168.1.1~192.168.1.100 也就是从 1 ping100,但是不可能需要我们手动输入 100 个数字吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vim pingip.sh


#!/bin/bash
# Program:
#
# History:
# 2020/02/12 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

network="192.168.0" # 先定义一个网域的前部分
for sitenu in $(seq 1 100) # seq 为 sequence 连续的意思
do
# ping -c 1 -w 1 192.168.0.101 &> /dev/null && echo "1" || echo "0"
# 不显示执行结果,并获取命令是否执行成功
ping -c 1 -w 1 ${network}.${sitenu} &> /dev/null && result=0 || result=1
if [ "${result}" == 0 ]; then
echo "${network}.${sitenu} is up"
else
echo "${network}.${sitenu} is down"
fi
done

测试结果

1
2
3
4
5
[mrcode@study bin]$ ./pingip.sh
192.168.0.1 is up
192.168.0.2 is down
192.168.0.3 is down
..

对于 $(seq 1 100) 来说,还可以使用 bash 的内建机制 {1..100} 来代替,中间两个点表示连续的意思,比如想要输出 a~g 则可以使用 a..g

最后一个范例:

  1. 让用户输入一个目录
  2. 如果目录不存在,则提示并退出程序
  3. 如果目录存在,则获取该目录下第一级文件是否可读、可写、可执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
vim dir_perm.sh

#!/bin/bash
# Program:
#
# History:
# 2020/02/12 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

read -p "请输入一个目录,将会检测该目录是否可读、可写、可执行:" dir
# 判定输入不为空,并且目录存在
if [ "${dir}" == '' -o ! -d "${dir}" ]; then
echo "The ${dir} is NOT exist in your system"
exit 1
fi

# 获取该目录下的文件权限信息
filelist=$(ls ${dir})
for file in ${filelist}
do
perm=""
test -r "${dir}/${file}" && perm="${perm} readable"
test -w "${dir}/${file}" && perm="${perm} writable"
test -x "${dir}/${file}" && perm="${perm} executable"
echo "The file ${dir}/${file}'s permission is ${perm}"
done

使用这种方式,可以很轻易的来处理一些文件的特性

for...do...done 数值处理

1
2
3
4
for (( 初始值; 限制值; 执行步阶))
do
循环体
done
  • 初始值:某个变量在循环中的起始值,可以以 i=1 设置好初始值
  • 限制值:当变量值在这个限制值范围内,则继续循环。例如 i<=100
  • 执行步阶:每执行一次循环时,变量的变化量。例如 i=i+1,如果是自增则可以使用 i++ 来替代

范例:计算从 1 累加到指定数值的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vim cal_1_100-2.sh

#!/bin/bash
# Program:
#
# History:
# 2020/02/12 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

read -p "请输入一个数值,将计算出从 1 累加到该数值的计算结果" nu

total=0

for (( i=1; i<=${nu}; i++))
do
total=$((${total}+${i}))
done

echo "1+..+${nu} = ${total}"

测试输出如下

1
2
3
4
5
6
[mrcode@study bin]$ ./cal_1_100-2.sh
请输入一个数值,将计算出从 1 累加到该数值的计算结果2
1+..+2 = 3
[mrcode@study bin]$ ./cal_1_100-2.sh
请输入一个数值,将计算出从 1 累加到该数值的计算结果100
1+..+100 = 5050

搭配随机数与数组的实验

现在大概已经能够掌握 shell script 了。

现在来做个有趣的小东西,今天中午吃啥?要完成这个脚本,首先需要将全部的店家输入到一组数组中,再通过随机数的处理,获得可能的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
vim what_to_eat.sh

#!/bin/bash
# Program:
#
# History:
# 2020/02/12 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

# 定义你搜集到的店家信息
eat[1]="卖当当汉堡"
eat[2]="肯爷爷炸鸡"
eat[3]="彩虹日式便当"
eat[4]="越油越好吃打呀"
eat[5]="想不出吃什么"
eat[6]="太师傅便当"
eat[7]="池上便当"
eat[8]="怀恋火车便当"
eat[9]="一起吃泡面"
eat[10]="太上皇"
eatnum=10 # 一共有几家可用的店铺

check=$((${RANDOM} * ${eatnum} / 32767 + 1))
echo "your may eat ${eat[${check}]}"

测试输出

1
2
3
4
5
6
7
[mrcode@study bin]$ ./what_to_eat.sh
your may eat 太上皇
[mrcode@study bin]$ ./what_to_eat.sh
your may eat 越油越好吃打呀
[mrcode@study bin]$ ./what_to_eat.sh
your may eat 想不出吃什么
[mrcode@study bin]$ ./what_to_eat.sh

继续深入,一次性输出 3 个选择,并且不能重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
vim what_to_eat-2.sh

#!/bin/bash
# Program:
#
# History:
# 2020/02/12 mrcode first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

# 定义你搜集到的店家信息
eat[1]="卖当当汉堡"
eat[2]="肯爷爷炸鸡"
eat[3]="彩虹日式便当"
eat[4]="越油越好吃打呀"
eat[5]="想不出吃什么"
eat[6]="太师傅便当"
eat[7]="池上便当"
eat[8]="怀恋火车便当"
eat[9]="一起吃泡面"
eat[10]="太上皇"
eatnum=10 # 一共有几家可用的店铺

# 其实就是需要轮询出来 3 个不同的索引结果

eated=0 # 已选中数量

while [ "${eated}" -lt 3 ];
do
check=$((${RANDOM} * ${eatnum} / 32767 + 1))
mycheck=0 # 当为 0 时,表示不重复
# 去重检查
if [ ${eated} -gt 0 ]; then # 当已选中至少一个店铺的时候,才执行
for i in $(seq 1 ${eated})
do
if [ "${eatedcon[$i]}" == $check ]; then
mycheck=1
fi
done
fi
if [ ${mycheck} == 0 ]; then
echo "your may eat ${eat[${check}]}"
eated=$(( ${eated} + 1 ))
eatedcon[${eated}]=${check} # 将已选中结果存储起来
fi
done

Shell Script 的追踪与 debug

scripts 在执行前,最怕出现语法错误问题了,可以通过 bash 相关参数来检测

1
2
3
4
5
6
sh [-nvx] scripts.sh

选项与参数:
-n:不执行 script,仅检查语法问题
-v:执行 script 前,先将 scripts 内容输出到屏幕上
-x:将执行到的 script 内容显示到屏幕上,相当于 debug 了
1
2
3
4
5
# 范例 1:测试 dir_perm.sh 有无语法问题?
sh -n dir_perm.sh
# 如果没有语法问题,则不会显示任何信息
# 笔者实测,貌似语法检测效果并不强大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 范例 2:将 show_animal.sh 的执行过程全部列出来
[mrcode@study bin]$ sh -x show_animal.sh
+ PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/home/mrcode/bin
+ export PATH
+ for animal in dog cat elephant
+ echo 'There are dogs...'
There are dogs...
+ for animal in dog cat elephant
+ echo 'There are cats...'
There are cats...
+ for animal in dog cat elephant
+ echo 'There are elephants...'
There are elephants...

# 下面是原始脚本,方便对比
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

for animal in dog cat elephant
do
echo "There are ${animal}s..."
done
# 可以看到,每次循环执行,都打印出来了关键信息,
# 该功能非常有用,可以当成 debug 来使用

熟悉 sh 的用法,可以使你在管理 Linux 的过程中得心应手,至于在 shell script 的学习方法上,需要多看、多模仿、并加以修改成自己的代码,是最快的学习手段了。网络有上相当多的有用的 scripts,你可以将其拿来进行修改成自己的代码

另外,Linux 系统本来就有很多的服务启动脚本,如果想要知道每个 script 所代表的功能是什么,直接 vim 进入该 script 查阅下,通常就知道了。比如说之前提到的 /etc/init.d/netconsole 是做什么的?直接查看他的前几行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vim /etc/init.d/netconsole 


#!/bin/bash
#
# netconsole This loads the netconsole module with the configured parameters.
#
# chkconfig: - 50 50
# description: Initializes network console logging
# config: /etc/sysconfig/netconsole
#
### BEGIN INIT INFO
# Provides: netconsole
# Required-Start: $network
# Short-Description: Initializes network console logging
# Description: Initializes network console logging of kernel messages.
### END INIT INFO

# Copyright 2002 Red Hat, Inc.
#
# Based in part on a shell script by
# Andreas Dilger <adilger@turbolinux.com> Sep 26, 2001

进程管理与 SELinux 初探

一个程序被加载到内存中运行,那么在内存中的那个数据就称为进程(process)。

所有系统上跑的数据都会以进程的形态存在,进程有哪些状态?不同的状态会如何影响听的运行?进程之间是否可以互相控管?等等,这些是我们所必须要知道的项目

与进程有关的还有 SELinux 加强文件存取安全性的东西

什么是进程(process)

在 Linux 系统中:触发任何一个事件事,系统都会将它定义成一个进程,并且给予这个进程一个 ID,称为 PID,同时依据启发这个进程的用户与相关属性关系,给予这个 PID 一组有效的权限设置

进程与程序 (process & program)

执行一个程序或指令,就可以触发一个事件而取得一个 PID

不同的用户运行程序,程序所拥有的权限也是不同的,概念如下图

系统通过这个 PID 来判断该 process 是否具有权限进行工作的。

比如我们登陆的 bash tty,它是一个程序,登陆之后,系统会根据登陆者的 UID/GID(/etc/paswd)来分配一个 PID,比如执行了一个 touch 的执行,那么由这个进程 衍生出来的其他进程在一般状态下,也会沿用这个进程的相关权限

程序与进程总结:

  • 程序 program:通常为 binary program,实体文件的形态存在
  • 进程 process:程序被触发后,执行者的权限与属性、程序的程序代码与所需数据等都会被加载到内存中,操作系统并给予这个内存单元一个标识符(PID),可以说,进程就是一个正在运行的程序

子进程与父进程

上面提到 衍生出来的进程,我们登陆到 bash,该 bash 是一个程序,并有一个 PID,在这个 bash 上执行指令,触发了相关指令的程序运行,从而得到该程序的 PID,这个 PID 就是一个子进程,原本的 bash 就是一个父进程

下面以一个小练习,来了解什么是子进程/父进程

1
2
3
4
5
6
7
8
9
10
11
12
# 在目前的 bash 环境下,再触发一次 bash,并以 ps -l 指令管擦进程相关的输出信息
# 直接执行 bash 指令,会进入到子进程的环境中
[root@study ~]# bash
[root@study ~]# ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 0 5713 1923 0 80 0 - 32064 do_wai pts/0 00:00:00 su
4 S 0 5862 5713 0 80 0 - 29218 do_wai pts/0 00:00:00 bash
4 S 0 10917 5862 0 80 0 - 3184 do_wai pts/0 00:00:00 bash
0 R 0 11193 10917 0 80 0 - 12407 - pts/0 00:00:00 ps
# 注意 PID 与 PPID,第 1 行的 PID 与第 2 行的 PPID 是一样的
# 第 2 行的 CMD 是 bash,就是从第一行中执行 bash 产生出来的

如果你发现,你杀掉了一个进程,不多久又出现了,这样的情况,如果不是 crontab 的定时任务产生的,那么就是有一个父进程在产生新的子进程

fork and exec:进程呼叫的流程

进程与父进程的关系最为复杂的在于进程相互间的呼叫。

Linux 的进程呼叫通常称为 fork-and-exec 的流程,进程都会借由父进程以复制(fork)的方式产生一个一模一样的子进程,然后被复制出来的子进程再以 exec 的方式来执行时机要进行的程序,最终就成为一个子进程的存在。整个流程类似下图:

系统或网络服务:常驻在内存的进程

常驻就是一直在运行的程序,比如 crond 程序,每分钟都会扫描 /etc/crontab 以及相关的配置文件,将它启动在背景当中一直持续不断的运行,这就是常驻在内存中的进程

这些进程通常都是负责一些系统所提供的功能以服务用户各项任务,因此这些常驻程序称为 服务(daemon)。系统的服务非常多,主要大致分为系统本身所需要的服务,如 crondatdrsyslogd 等。还有一些则负责网络联机服务,例如 Apachenamedpostfixvsftpd 等,这些网络服务程序被执行后,会启动一个可以负责网络监听的端口 port,以提供外部客户端的联机要求

TIP

cronat 是他们主要执行的程序名称,但是后面加了一个 d,成为 crondatd ,是因为这样可以简单的判定为该程序是一个服务 daemon,所以一般 daemon 类型的程序都会加上 d 的文件名,比如服务器篇中会看到的 httpdvsftpd

Linux 的多人多任务环境

了解了进程的知识点后,你可以简单的认为进程的出现,让我们多用户在 Linux 都能拥有自己的环境了。

  • 多人环境

    Linux 最棒的地方就在于它的多人多任务环境。什么是多人多任务?每个账户都有其特殊的权限,除了 root 之外,其他人都必须受一些限制,而每个人进入 Linux 的环境设置都可以自定义,所以每个人登录后取得的 shell PID 不同,是因为不在同一个进程程序中

  • 多任务行为

    CPU 多在各个进程之间进行切换工作,因此当多人同时登录系统时,你的感觉就像整部主机好像就是你一个人的一样

  • 多重登录环境的七个基本终端窗口

    Linux 中,默认提供了 6 个文字界面登录窗口和一个图形界面,可以使用 alt+F{1-7}来切换不同的终端机界面,每个终端机界面的登录者可以是不同用户

    这也是多任务环境下所产生的一个情况,Linux 默认会启动 6 个终端机登录环境的程序,所以才可以切换,在后续第 19 章开机管理流程中会仔细介绍的。

  • 特殊的进程管理行为

    对于宕机情况,在 Linux 上,几乎不会出现,因为他可以在任何时候,将某个困住的进程杀掉,然后在重新执行该进程而不用重新启动。

    比如在 Linux 下以文字界面登录,在屏幕中显示错误信息后就挂了,不能动了。这个时候你可以切换到另外的 6 个终端机接口,以 ps -aux 找出刚刚的错误进程,kiil 掉它,再回到刚刚的终端机界面就好了(笔者疑问?kill 后会自动重启?有点不太理解)

  • bash 环境下的工作管理

    当我们登录 bash 后,取到了一个 PID,那么在这个环境下执行的其他指令,几乎是子进程了,在这个单一的 bash 接口中,可以如下进行多个工作,并且是同时进行

    1
    2
    cp file1 file2 &

    上述指令串中,& 表示把 file1 复制为 file2,并放到背景中执行。也就是说,下达完这一串指令后,马上就可以下达其他的指令串了,当着一个指令执行完成后,系统将会在你的终端接口显示完成的消息

  • 多人多任务的系统资源分配问题考虑

    当人多的时候,由于是共用计算机资源,所以有可能会导致资源不够用的情况发生,这个时候就需要升级机器了


工作管理(job control)

是在 bash 环境下的概念,当我们登录系统取得 bash shell 后,在单一终端机下同时进行多个工作的行为管理。

什么是工作管理?

进行工作管理的行为中,其实每个工作都是目前 bash 的子进程,彼此之间是有相关性的。我们无法以 job control 的方式由 tty1 的环境去管理 tty2 的 bash

为什么会有工作管理?系统有多个 tty 使用,这样切换很麻烦,还有之前讲解的 /etc/security/limits.conf(第 13 章)可以设置同时登录的联机数量,假设只允许一个呢?

假设我们只有一个终端机接口,因此在可以出现提示字符让你操作的环境称为 前景 foreground,其他工作可以放入 背景 background 去暂停或运行。要注意的是:放入背景的工作在运行时,不能与使用者互动。比如 vim 不能再背景里面执行(running)的,因为你没有输入数据它就不会运行。而且放入背景的工作是不可以使用 ctrl+c 来终止的

进行 bashjob control 必须要注意的限制是:

  • 这些工作所触发的进程必须来自于你的 shell 的子进程(只管理自己的 bash
  • 前景 foreground:你可以控制与下达指令的环境
  • 背景:可以自动运行的工作,你无法使用 ctrl + c 终止它,可以使用 bgfg 呼叫该工作
  • 背景中执行的进程不能等待 terminal/shell 的输入(input

job control 的管理

直接将指令丢到背景中 执行&

1
2
3
4
5
6
7
8
9
10
11
12
# 使用 & 将 /etc/ 整个备份为 /tmp/etc/tar.gz 工作丢到背景中执行
# 原因就是,压缩费时,不想一直就在当前界面看着他完成
[root@study ~]# tar -zpcf /tmp/etc.tar.gz /etc &
[1] 19763 # job number 与 PID
[root@study ~]# tar: Removing leading `/' from member names
# PID 与 bash 的控制有关,后续出现的数据信息是 tar 执行的数据流
# 由于没有加上数据流重导向,所以会影响画面,不过不会影响前景的操作

# 那他什么时候完成呢?当你输入几个指令后,发现出现了这一行
# 那么久表示在背景中的工作已经完成了
[1]+ Done tar -zpcf /tmp/etc.tar.gz /etc

[1]+ 表示这个工作已经完成(Done),后面是具体的指令串。如果有有信息出现,那么你的前景会出现干扰,只需要按下 enter 键就会出现提示字符,更下下指令

1
2
[root@study ~]# tar -zpcvf /tmp/etc.tar.gz /etc &

由于输出了信息,stdoutstderr 都会输出到屏幕上,这样就会影响前景终端,所以一般都利用数据流重导向,将输出数据传送至某个文件中,比如

1
2
3
4
[root@study ~]# tar -zpcvf /tmp/etc.tar.gz /etc  > /tmp/log.txt 2>&1 &
[1] 16592
[root@study ~]#

目前 的工作丢到背景中_暂停_:ctrl+z

考虑这个场景,我正在使用 vim,却发现某个文件的路径不记得了,需要到 bash 环境下进程搜索,此时不需要结束 vim,可以把它丢到背景中等待

1
2
3
4
5
6
7
8
9
[root@study ~]# vim ~/.bashrc 
# 在 vim 环境下按 ctrl + z 组合键
[2]+ Stopped vim ~/.bashrc
[root@study ~]# # 这就取得了前景
[root@study ~]# find / -print
# 会大量输出信息,我们把这个工作也丢到背景中执行
[3]+ Stopped find / -print
[root@study ~]#

[2]+ 表示这个是加入到背景中的第二个工作,Stopped 是状态,预设情况下,使用 ctrl+z 丢到背景中的工作都是暂停状态

观察目前的背景工作状态:jobs

1
2
3
4
5
6
7
jobs [-lrs]

选项与参数:
-l:除了列出 job number 与指令之外,同时列出 PID 的号码
-r:仅列出正在背景 run 的工作
-s:仅列出正在背景中暂停 stop 的工作

1
2
3
4
5
# 范例 1:观察目前的 bash 中,所有工作与队友的 PID
[root@study ~]# jobs -l
[2]- 26476 Stopped vim ~/.bashrc
[3]+ 2207 Stopped find / -print

仔细看上面有减号和加号:

  • +:表示最近被放到背景的工作;如果只输入 fg 指令,那么 [3] 会被拿到前景中来处理
  • -:表示最近最后第二个被放置到背景中的工作。如果超过最后第三个以后的工作,就不会有 -、+ 符号了

将背景工作拿到前景来处理:fg

1
2
3
4
fg %jobnumber

$jobnumber: jobnumber 是工作号码(数字),哪个 % 是可有可无的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 范例 1:先以 jobs 管擦工作,再将工作取出
[root@study ~]# jobs -l
[2]- 26476 Stopped vim ~/.bashrc
[3]+ 2207 Stopped find / -print
# 取出 + 号的工作,注意会刷屏,马上按下 ctrl + z ,再次放入到背景中
[root@study ~]# fg
# 直接取出 2 的工作,在放到背景中
[root@study ~]# fg %2
vim ~/.bashrc

[2]+ Stopped vim ~/.bashrc
[root@study ~]# jobs -l
[2]+ 26476 Stopped vim ~/.bashrc
[3]- 2207 Stopped find / -print

# 可以看到, 2 的工作被标记为了 + 号,表示是最近放进去的

让工作在背景下的状态变成运行中:bg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 范例 1: 执行 find / -perm /7000 > /tmp/text.txt,立刻丢到背景去暂停
[root@study ~]# find / -perm /7000 > /tmp/text.txt
find: '/proc/29541/task/29541/fd/5': No such file or directory
find: '/proc/29541/task/29541/fdinfo/5': No such file or directory
find: '/proc/29541/fd/6': No such file or directory
find: '/proc/29541/fdinfo/6': No such file or directory
^Z
[4]+ Stopped find / -perm /7000 > /tmp/text.txt

# 范例 2:让该工作在背景下进行,并且观察他
[root@study ~]# jobs ; bg %4; jobs
[2]- Stopped vim ~/.bashrc
[3] Stopped find / -print
[4]+ Stopped find / -perm /7000 > /tmp/text.txt

[4]+ find / -perm /7000 > /tmp/text.txt &
[2]+ Stopped vim ~/.bashrc
[3] Stopped find / -print
[4]- Running find / -perm /7000 > /tmp/text.txt &
# 第 4 个由 Stopped 变成了 Running 状态

管理背景中的工作:kill

通过 fg 拿到前景来,可以通过 kill 将该工作直接移除

1
2
3
4
5
6
7
8
9
10
11
kill -signal $jobnumber
kill -l

选项与参数:
-l:L 的小写,列出目前 kill 能够使用的信号(signal)有哪些?
signal:给予后续工作什么指示,用 man 7 signal 可知:
-1:重新读取一次参数的配置文件(类似 reload)
-2:代表与由键盘输入 ctrl+c 同样的动作
-9:立刻强制删除一个工作
-15:已正常的进程方式终止一项工作。与 -9 是不一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
范例 1: 找出目前 bash 环境下的背景工作,并将该工作 强制删除
[root@study ~]# jobs
[2]+ Stopped vim ~/.bashrc
[3] Stopped find / -print
[4]- Exit 1 find / -perm /7000 > /tmp/text.txt

[root@study ~]# kill -9 %3; jobs
[2]+ Stopped vim ~/.bashrc
[3] Killed find / -print
# 过几秒再运行 jobs 会发现 killed 的不见了

# 范例 2:找出目前 bash 环境下的而背景工作,并将该工作 正常 终止
[root@study ~]# kill -SIGTERM %2

[2]+ Stopped vim ~/.bashrc
[root@study ~]# jobs
[2]+ Stopped vim ~/.bashrc
# -SIGTERM 与 -15 同效果,可以用哪个 kill -l 来查阅
# 在这个案例中,会发现 vim 的工作无法被结束,无法通过 kill 正常终止

使用 vim 时候,会产生一个 .filename.swp 文件,使用 -15 这个 signal 时,vim 会尝试以正常的步骤来结束掉该 vi 的工作,使用 .filename.swp 会主动的被移除,若是使用 -9,那么 swp 文件不会被移除调用

kill 需要了解 1、9、15 的 signal 的含义,可以用 man 7 signal 查询相关资料,还有一个 killall 也是同样的用法。

kill 后面接的数字,默认是 PID,要管理 bash 的工作控制,需要加上 %数字 的方式

脱机管理问题

注意:前面工作管理中的「背景」是指在终端机模式下可以避免「ctrl+c」中断,可以理解为是这个 bash 的背景,并 不是放到系统的背景 中去。所以,工作管理的背景依旧与终端机有关

如果你是以远程方式连接到 Linux 主机,并且将工作以 & 的方式放到背景中去,在工作未结束时,你脱机了,该工作不会继续进行,而是会被中断掉

那么可以使用前一章学习的 at 指令,因为它是将工作放置到系统背景,还可以使用 nohup 指令来达到效果

1
2
3
nohup [指令与参数]		# 在终端机前景中工作
nohup [指令与参数] # 在终端机背景中工作

TIP

nohup 后面的指令不支持 bash 内置指令!

1
2
3
4
5
6
7
8
9
10
11
12
#1. 编辑一个会随眠  500 秒的程序
[root@study ~]# vim sleep500.sh
#!/bin/bash
/bin/sleep 500s
/bin/echo "I have sleep 500 seconds."

# 2. 丢到背景中执行
[root@study ~]# chmod a+x sleep500.sh
[root@study ~]# nohup ./sleep500.sh &
[3] 14915
[root@study ~]# nohup: ignoring input and appending output to 'nohup.out'

你登出登录后,再次登录系统,使用 pstree (这里没有说是什么)去查询你的进程,会发现它还在执行,还输出了一个信息,nohup 与终端机无关了,因此整个信息的输出就会被导向 ~/nohup.out

进程管理

为什么进程管理这么重要?是因为:

  • 我们在操作系统时的各项工作都是经过某个 PID 来达成的(包括你的 bash 环境),因此,能不能进行某项工作,与该进程的权限有关
  • 如果你的 LInux 是个很忙碌的系统,当整个系统资源要被使用光的时候,你是否能够找出最耗资源的哪个进程,然后删除该进程,让系统恢复正常?
  • 由于某个程序写的不好,导致产生一个有问题的进程在内存中,如何找出它,将它移除呢?
  • 如果有 5、6 项工作在系统中运行,但其中有一项工作才是最重要的,该如何让那一项重要的工作被最优先执行?

以上几点,在系统使用中都是很重要且常见的问题

进程的观察

ps:将某个时间点的进程运行情况截取下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ps aux		# 观察系统所有的进程数据
ps -l # 观察与当前终端机相关的进程
ps -lA # 观察系统所有的进程数据(显示内容项同 ps -l 的项一样,只不过是系统所有进程)
ps axjf # 连同部分进程树状态

选项与参数:
-A:所有的 process 都显示出来,与 -e 具有同样的效果
-a:不与 terminal 有关的所有 process
-u:有效使用者(effective user)相关的 process
x:通常与 a 一起使用,可列出完整信息
输出格式规划:
l:较长、较详细的将该 PID 的信息列出
j:工作的格式(jobs format)
-f:做一个更为完整的输出

ps 指令的 man page 不太好查阅,不同的 Unix 都使用 ps 来查阅进程状态,为了符合不同版本的需求,该 man page 写的非常庞大,因此建议你有两个选择:

  1. 只能查询自己 bash 进程的 ps -l
  2. 可以查询所有系统运行的进程 ps aux

仅查看自己的 bash 相关进程:ps -l

1
2
3
4
5
6
7
8
# 范例 1: 将目前属于您自己这次登录的 PID 与相关信息列出来(只与自己的 bash 有关)
[root@study ~]# ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 0 29260 28796 0 80 0 - 57972 do_wai pts/0 00:00:00 su
4 S 0 29473 29260 0 80 0 - 29090 do_wai pts/0 00:00:00 bash
0 R 0 30444 29473 0 80 0 - 12407 - pts/0 00:00:00 ps
# 前面三项,最初是用了普通账户登录的,使用了 su - 切换到了一个 bash

这里列出的只是与你操作环境 bash 有关的进程,没有延伸到 systemd(后续交代):

  • F:进程旗标(process flags),说明这个进程的总结权限,常见的号码有:
    • 4:表示此进程的权限为 root
    • 1:则表示此子进程仅进行 复制(fork)而没有实际执行(exec)
  • S:进程状态(STAT),主要状态有:
    • RRunning):正在运行中
    • SSleep):该程序目前正在睡眠状态(idle),但可以被唤醒(signal
    • D:不可被唤醒的睡眠状态,通常该程序可能在等待 I/O 的情况
    • T:停止状态(stop),可能是在工作控制(背景暂停)或除错(traced)状态
    • ZZombie):僵尸状态,进程已终止但却无法被移除至内存外
  • UUID/PID/PPID:代表此进程被该 UID 所拥有、进程的 PID 、此进程的父进程 PID
  • C:代表 CPU 使用率,单位为百分比
  • PRI/NIPriority/Nice 的缩写,代表此进程被 CPU 所执行的优先级,数值越小表示该进程越快被 CPU 执行。详细的 PRINI 将在下一小节讲解
  • ADDR/SZ/WCHAN:都与内存有关
    • ADDRkernel function,该进程在内存的哪个部分,如果是 running 的进程,一般会显示 -
    • SZ:该进程用掉多少内存
    • WCHAN 该进程是否运行中,若为 - 表示正在运行中
  • TTY:登陆者的终端机位置,若为远程登录则使用动态终端接口(pts/n
  • TIME:使用掉的 CPU 时间。注意:是此进程实际花费 CPU 运行的时间
  • CMDcommand 的缩写,此进程的触发程序指令

如上列出的信息表示, bash 的程序属于 UID 为 0 的使用者,状态是睡眠(sleep),他睡眠是因为他触发了 ps(状态为 Rrun)的原因,ps 的 PID=30444,优先执行顺序为 80,下达 bash 所取得的终端机接口为 pts/0,运行状态为 do_wai

观察系统所有进程:ps aux

1
2
3
4
5
6
7
8
9
[root@study ~]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.5 128372 6988 ? Ss 21:14 0:01 /usr/lib/systemd/systemd --switched-root --system --deseriali
root 2 0.0 0.0 0 0 ? S 21:14 0:00 [kthreadd]
root 4 0.0 0.0 0 0 ? S< 21:14 0:00 [kworker/0:0H]
...
root 27082 0.0 0.1 51752 1716 pts/2 R+ 21:41 0:00 ps aux


会发现 ps -lps aux 显示的项目也不一样

  • USER:该 process 属于哪个使用者账户
  • PID:进程标识符
  • %CPU:该进程使用掉的 CPU 资源百分比
  • %MEM:占用的虚拟内存(KBytes
  • RSS:占用的固定内存(KBytes
  • TTY:在哪个终端机上面运行?
    • ?:与终端机无关
    • tty1-tty6:本机上登录的
    • pts/0等:是由网络连接进入的进程
  • STAT:目前的状态,与 ps -l 中的状态相同含义
  • START:该进程被触发启动时间(如果太久不会显示具体时间)
  • TIME:该进程实际使用 CPU 运行的时间
  • COMMAND:进程执行的指令

一般来说,ps aux 会按照 PID 的顺序来排序显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 范例 3:以范例 1 的显示内容,显示出所有的进程
[root@study ~]# ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 0 25710 1956 0 80 0 - 57972 do_wai pts/2 00:00:00 su
4 S 0 25917 25710 0 80 0 - 29090 do_wai pts/2 00:00:00 bash
0 R 0 32189 25917 0 80 0 - 12407 - pts/2 00:00:00 ps
[root@study ~]# ps -lA
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 0 1 0 0 80 0 - 32093 ep_pol ? 00:00:01 systemd
1 S 0 2 0 0 80 0 - 0 kthrea ? 00:00:00 kthreadd
1 S 0 4 2 0 60 -20 - 0 worker ? 00:00:00 kworker/0:0H
1 S 0 6 2 0 80 0 - 0 smpboo ? 00:00:00 ksoftirqd/0
1 S 0 7 2 0 -40 - - 0 smpboo ? 00:00:00 migration/0
1 S 0 8 2 0 80 0 - 0 rcu_gp ? 00:00:00 rcu_bh
1 R 0 9 2 0 80 0 - 0 - ? 00:00:01 rcu_sched
# 会发现,与 ps -l 显示类似,不过显示的是系统的所有进程

# 范例 4:列出类似进程树的进程显示
[root@study ~]# ps axjf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 2 0 0 ? -1 S 0 0:00 [kthreadd]
2 4 0 0 ? -1 S< 0 0:00 \_ [kworker/0:0H]
2 6 0 0 ? -1 S 0 0:00 \_ [ksoftirqd/0]
1 1269 1269 1269 ? -1 Ss 0 0:00 /usr/sbin/sshd -D
1269 1922 1922 1922 ? -1 Ss 0 0:01 \_ sshd: mrcode [priv]
1922 1932 1922 1922 ? -1 S 1000 0:09 | \_ sshd: mrcode@pts/0,pts/1
1932 1934 1934 1934 pts/0 1934 Ss+ 1000 0:00 | \_ -bash
1932 1939 1939 1939 ? -1 Ss 1000 0:00 | \_ /usr/libexec/openssh/sftp-server
1932 1941 1941 1941 pts/1 2573 Ss 1000 0:00 | \_ -bash
1941 2573 2573 1941 pts/1 2573 S+ 1000 0:04 | | \_ top
1932 7742 7742 7742 ? -1 Ss 1000 0:00 | \_ bash -c export LANG="en_US.UTF-8";export LANGUAGE="en_US.
7742 7789 7742 7742 ? -1 S 1000 0:00 | \_ sleep 1
1269 1926 1926 1926 ? -1 Ss 0 0:01 \_ sshd: mrcode [priv]
1926 1950 1926 1926 ? -1 S 1000 0:09 \_ sshd: mrcode@pts/2,pts/3
1950 1956 1956 1956 pts/2 7790 Ss 1000 0:00 \_ -bash
1956 25710 25710 1956 pts/2 7790 S 0 0:00 | \_ su -
25710 25917 25917 1956 pts/2 7790 S 0 0:00 | \_ -bash
25917 7790 7790 1956 pts/2 7790 R+ 0 0:00 | \_ ps axjf
1950 2009 2009 2009 ? -1 Ss 1000 0:00 \_ /usr/libexec/openssh/sftp-server
1950 2012 2012 2012 pts/3 2574 Ss 1000 0:00 \_ -bash


看上面 PPID1269 的那一行开始,我这里使用了 ssh 远程链接,用的是 mrcode 账户,登录成功后,获得了一个 bash 环境,后面我使用了 su - 指令切换到了 rootbash 环境,然后执行了刚刚的 ps axjf 指令。这样就比较清楚了。

还可以通过 pstree 指令来显示进程树,不过貌似没有这么详细

1
2
3
4
5
6
7
# 范例 5:找出与 cron 和 rsyslog 这两个服务有关的 PID 号码
[root@study ~]# ps aux | egrep '(cron|rsyslog)'
root 1273 0.0 0.3 215672 3652 ? Ssl 21:15 0:00 /usr/sbin/rsyslogd -n
root 1285 0.0 0.1 126288 1696 ? Ss 21:15 0:00 /usr/sbin/crond -n
root 4838 0.0 0.0 9096 932 pts/2 R+ 21:58 0:00 grep -E --color=auto (cron|rsyslog)
# 对于上面为什么要使用 egrep,在第 11 章,延伸正则表示法中有介绍。

僵尸进程 zombie

僵尸 zombie:该进程以及执行完毕或则是因故应该要终止了,但是该进程的父进程却无法完整的将该进程结束掉,而造成哪个进程一直在内存中。

在进程中它的标识是在 CMD 后面有 <defunct> 标识,例如下面这样

1
2
apache 8683 0.0 0.9 83383 9992 ?Z 14:33 0:00 /usr/sbin/httpd<defunct>

当系统不稳定时,容易造成僵尸进程,可能是因为程序有问题,或则是使用者的操作习惯不良等。

发现有僵尸进程时,应该找出来,分析原因,否则有可能一直产生僵尸进程

事实上,通常僵尸进程都已经无法管控,而直接交给 systemd 程序来负责了,偏偏 systemd 是系统第一个执行的程序,它是所有程序的父程序,无法杀掉该程序(杀掉它,系统就死了),所以,经过一段时间后,系统无法通过核心非经常性的特殊处理来将该进程删除时,那只有重启机器了

top:动态观察进程的变化

ps 可以显示一个时间点的进程状态,而 top 则可以持续的侦测进程运行状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
top [-d 数字] | top [-bnp]

选项与参数:
-d:后面可以接秒数,整个进程画面更新的秒数,预设是 5 秒更新一次
-b:以批次的方式执行 top,还有更多的参数可以使用(莫名其妙啊,啥参数?),通常会搭配数据流重导向来将批次的结果输出为文件
-n:与 -b 搭配,需要进行几次 top 的输出
-p:指定某些 PID 来进行观察

在 top 执行过程中可以使用的按键指令:
?:显示在 top 中可以输入的按键指令
P:以 CPU 的使用资源排序显示
M:以 Memory 的使用资源排序显示
N:以 PID 排序
T:由该进程使用 CPU 时间累积(TIME+)排序
k:给予某个 PID 一个信号(signal)
r:给予某个 PID 重新制定一个 nice 值
q:离开 top 软件的按键
E:切换单位显示,比如从 KB 切换为 G 显示
c:切换 COMMAND 的信息,name/完成指令

top 的功能太多,可用的按键也很多,可以参考 man top 的内部文件说明,上面只是列出常用的选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 范例 1:每两秒更新一次 top,观察整体信息
[root@study ~]# top -d 2
top - 22:20:11 up 1:05, 4 users, load average: 0.52, 0.53, 0.52
Tasks: 186 total, 2 running, 184 sleeping, 0 stopped, 0 zombie
%Cpu(s): 7.7 us, 9.7 sy, 0.0 ni, 82.1 id, 0.0 wa, 0.0 hi, 0.5 si, 0.0 st
KiB Mem : 1190952 total, 428928 free, 402624 used, 359400 buff/cache
KiB Swap: 1048572 total, 1048572 free, 0 used. 632160 avail Mem
# <<< 如果按下 k 或 r 时,有相关的提示在这里出现
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1699 gdm 20 0 2947388 136736 61224 S 0.5 11.5 0:04.00 gnome-shell
1932 mrcode 20 0 161324 3016 1296 S 0.5 0.3 0:17.28 sshd
1950 mrcode 20 0 161324 3028 1296 S 0.5 0.3 0:17.41 sshd
2573 mrcode 20 0 162820 3068 1576 S 0.5 0.3 0:07.43 top
1 root 20 0 128372 6988 4196 S 0.0 0.6 0:01.67 systemd

top 的信息基本上分为两个区域,上面 6 行,和下面的列表

  • 第一行信息:top -

    • 目前开机时间:22:20:11 这个

    • 开机到目前为止所经过的时间:up 1:05 这个

    • 已经登录系统的用户人数:4 users

    • 系统在 1、5、15 分钟的平均工作负载

      在第 15 章谈到过 batch 工作方式负载小于 0.8 就是这里显示的值了。

      表示的是,系统平均要负责运行几个进程,这里是三个值,也就是对应平均 1/5/15 分钟

      越小达标系统越空闲,若高于 1 ,那么你的系统进程执行太频繁了

  • 第二行:tasks

    显示的是目前进程的总量与各个状态(running、sleeping、stopped、zombie)的进程数量

    如果发现有 zombie 进程的话,就需要找下是哪个进程变成了僵尸进程了

  • 第三行:$Cpus

    CPU 整体负载,每个项目可使用 ? 查询。

    需要特别注意的是 wa 项,表示 I/O wait,通常系统变慢,都是 I/O 产生的问题比较大,需要特别注意该项占用的 CPU 资源,如果是多核 CPU,可以按下数字键「1」来切换成不同 CPU

  • 第四行和第五行

    目前的物理内存与虚拟内存(Mem/Swap)的使用情况。要注意的是 swap 的使用量要尽量的少,如果 swap 被大量使用,表示系统的物理内存不足

  • 第六行:当在 top 程序中输入指令时,显示状态的地方

下面的列表部分大部分都见过了,下面再列出含义:

  • PID:进程 ID
  • USER:进程所属使用者
  • PRpriority):进程优先执行顺序,越小越早被执行
  • NInice):与 PR 有关,越小越早被执行
  • %CPUCPU 使用率
  • %MEM:内存使用率
  • TIME+CPU 使用时间的累加

top 预设使用 CPU 使用率 %CPU作为排序的重点,如果想要使用内存使用率排序,可以按下 M 键,要离开按下 q

1
2
3
4
# 范例 2:将 top 的信息进行 2 次,然后将结果输出到 /tmp/top.txt
[root@study ~]# top -b -n 2 > /tmp/top.txt
# 这里的结果就是,写入了执行 2 次的结果信息。是追加写入的

由于只有一屏显示,所以当你要观察的进程没有排序到最前面的时候,还可以单独观察该线程

1
2
3
4
5
6
7
8
9
10
# 范例 3:我们自己的 bash PID 可以由 $$ 变量取得,使用 top 持续观察该 PID
[root@study ~]# top -d 2 -p $$
top - 22:53:55 up 1:39, 2 users, load average: 0.59, 0.28, 0.32
Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
%Cpu(s): 2.1 us, 4.6 sy, 0.0 ni, 92.8 id, 0.0 wa, 0.0 hi, 0.5 si, 0.0 st
KiB Mem : 1190952 total, 435612 free, 392456 used, 362884 buff/cache
KiB Swap: 1048572 total, 1048572 free, 0 used. 642448 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
9051 root 20 0 116472 3172 1780 S 0.0 0.3 0:00.04 bash

就只显示着一个程序给你看了,还可以修改 NI

1
2
3
4
5
6
7
8
# 范例 4:上题的 NI 指是 0,把它修改成 10
# 在上题的 top 画面中按下 r 键出现下面的提示
PID to renice [default pid = 9051] 5501 # 输入要修改的 PID
Renice PID 9051 to value 10 # 输入要修改的 nice 值
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
9051 root 30 10 116472 3172 1780 S 0.0 0.3 0:00.04 bash

# 会发现 NI 值已经修改了

pstree

1
2
3
4
5
6
7
pstree [-AIU] [-up]

选项与参数:
-A:各进程之间的连接以 ASCII 字符来连接
-U:各进程之间的连接以万国码的字符来连接。在某些终端机接口下可能会有错误
-p:并同时列出每个 process 的 PID
-u:并同时列出每个 process 的所属账户名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 范例 1:列出目前系统上所有的进程树的相关性
[root@study ~]# pstree -A
systemd-+-ModemManager---2*[{ModemManager}] # ModenManager 与其子进程
|-NetworkManager---2*[{NetworkManager}]
|-2*[abrt-watch-log]
|-abrtd
|-accounts-daemon---2*[{accounts-daemon}]
....
|-sshd---sshd---sshd-+-bash---su---bash---pstree # 我们指令执行的相依性
| |-bash---top
| |-bash
| `-sftp-server
# 看下这个相依性,差不多就是登陆之后,在 su 切换账户之后,执行的

# 范例 2:同时显示出 PID 与 users
[root@study ~]# pstree -Aup
systemd(1)-+-ModemManager(871)-+-{ModemManager}(881)
| `-{ModemManager}(891)
|-NetworkManager(935)-+-{NetworkManager}(941)
| `-{NetworkManager}(945)
|-abrt-watch-log(856)
|-sshd(1269)---sshd(7771)---sshd(7779,mrcode)-+-bash(3239)---sleep(3263)
| |-bash(7780)---su(8985,root)---bash(9051)---pstree(3264)
| |-bash(7835)---top(8102)
| `-sftp-server(7833)
# 可以看到 sshd 登录的 PID 是 7779 ,用 mrcode 账户登录的。后续用 su 切换到了 root,这个时候新开了一个进程 7780 的 bash

pstree 来找相关性,同时使用 -A 来让连线不断开。默认的 Unicode 连线有可能出现断线,整体画面显示错位的问题

pstree 的输出我们可以知道,所有的进程都是依附在 systemd 程序下面的,systemd 的进程 ID 是 1 号,是 LInux 核心主动运行的第一个程序

之前讲解遇到僵尸进程为啥要重启,因为 systemd 要重启,那么就相当于重启系统了

进程的管理

进程相互管理是通过一个信号(signal)去告知该进程你要它做什么。信号可以通过 man 7 signal 查阅,主要信号代号与名称含义如下:

代号 名称 含义
1 SIGHUP 启动被终止的进程,可让该 PID 重新读取自己的配置文件,类似重新启动
2 SIGINT 相当于用键盘输入 ctrl + c 来终端一个进程的运行
9 SIGKILL 强制终端一个进程的运行,如果该进程进行到一半,那么尚未完成的部分可能会有半成品产生,类似 vim 会有 .filename.swp 保留下来
15 SIGTERM 以正常的结束进程来终止该进程。由于是正常的终止,所以后续的动作会将他完成。不过,如果该进程已经发生问题,就无法使用正常的方法终止时,输入该 signal 也是没有用的
19 SIGSTOP 相当于用键盘输入 ctrl-z 来暂停一个进行的运行

可以使用 killkillall 把信号传递给进程

kill -signal PID

kill 可以将信号传递给某个工作(**%jobnumber) 或某个 **PID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 范例 1:以 ps 找出 rsyslogd 这个进程 PID 后,再使用 kill 传递信号,让它可以重新读取配置文件
[root@study ~]# ps aux | grep 'rsyslogd'
root 1273 0.0 0.3 215672 3728 ? Ssl 21:15 0:00 /usr/sbin/rsyslogd -n
root 18876 0.0 0.0 9096 928 pts/0 RN+ 23:30 0:00 grep --color=auto rsyslogd
[root@study ~]# ps aux | grep 'rsyslogd' | grep -v 'grep'
root 1273 0.0 0.3 215672 3728 ? Ssl 21:15 0:00 /usr/sbin/rsyslogd -n
[root@study ~]# ps aux | grep 'rsyslogd' | grep -v 'grep' | awk '{print $2}'
1273

# 最终的指令是如下的
[root@study ~]# kill -SIGHUP $(ps aux | grep 'rsyslogd' | grep -v 'grep' | awk '{print $2}')
# 是否重启无法看通过看进程来知道,可以看日志
[root@study ~]# tail -5 /var/log/messages
Mar 9 23:20:01 study systemd: Removed slice User Slice of root.
Mar 9 23:30:01 study systemd: Created slice User Slice of root.
Mar 9 23:30:01 study systemd: Started Session 19 of user root.
Mar 9 23:30:01 study systemd: Removed slice User Slice of root.
Mar 9 23:35:20 study rsyslogd: [origin software="rsyslogd" swVersion="8.24.0-38.el7" x-pid="1273" x-info="http://www.rsyslog.com"] rsyslogd was HUPed
# 看上面,rsyslogd was HUPed 的字样,表示有重新启动

还记得可以查询到登录的 bash 的进程吗?也可以使用 kill -9 来删除,就意味着,该登陆者被踢下线了

killall -signal 指令名称

由于 kill 后面必须要加上 PID (或是 job number),所以通常需要配合 pspstree 等指令,还可以使用另外一种方法来达到效果

1
2
3
4
5
6
killall [-iIe] [command name]

选项与参数:
-i:interactive ,交互式的,若需要删除时,会出现提示字符给用户确认
-e:exact,后面接的 command name 要一致,但整个完整的指令不能超过 15 个字符
-I:指令名称(可能含参数)忽略大小写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 范例 1:给予 rsyslogd 指令启动的 PID 一个 SIGHUP 的信号
[root@study ~]# killall -1 rsyslogd
# 这里 -1 是信号

# 范例 2:强制终止所有以 httpd 启动的进程(其实当前没有该进程启动)
[root@study ~]# killall -9 httpd
httpd: no process found

# 范例 3:依次询问每个 bash 程序是否需要被终止
[root@study ~]# killall -i -9 bash
Signal bash(7780) ? (y/N) n
Signal bash(7835) ? (y/N) n
Signal bash(9051) ? (y/N) n
bash: no process found

# 这里都选择了 n,所以提示没有进程被找到,按下 y 就杀掉了

关于程序的执行顺序

CPU 是切换着执行进程,那么谁先执行?这个就要看进程的优先级 priorityCPU 排程(每个进程被 CPU 运行的演算规则)

Priority 与 Nice 值

CPU 一秒钟可以运行多达数 G 的微指令次数,通过核心的 CPU 调度可以让各程序 被 CPU 所切换运行, 因此每个程序在一秒钟内或多或少都会被 CPU 执行部分的指令码。

如果进程不分优先级顺序的话,那么就是排队执行,如果中间有个进程执行时间很长,其他进程就要等待很长时间

如上图,有了优先级之后,高优先级的可用被执行两次,低优先级则执行 1 次,但是上图仅是示意图,并非高优先级的就会执行两次,Linux 给予进程一个优先执行序(priority PRI),PRI 值越低优先级越高,不过该值是由核心动态调整的,用户无法直接调整 PRI

1
2
3
4
5
[root@study ~]# ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 R 0 7183 9051 0 90 10 - 12406 - pts/0 00:00:00 ps
4 S 0 8985 7780 0 80 0 - 57972 do_wai pts/0 00:00:00 su
4 S 0 9051 8985 0 90 10 - 29118 do_wai pts/0 00:00:00 bash

由于 PRI 是动态调整的,用户无法干涉,但是可以通过 Nice 值来达到一定的优先级调整,Nice 就是上述中的 NI 值,一般来说 PRINI 的相关性 PRI(new)=PRI(old)+nice,虽然可以调整 nice 的值,由于 PRI 是动态调整的,所以不包装调整完之后,最终的 PRI 就会变低,优先级变高的

此外,必须要注意,nice 值范围

  • nice 值范围是 -20~19
  • root 可随意调整自己或他人进程的 Nice 值,且范围为 -20~19
  • 一般使用者仅可调整自己进程的 Nice 值,且范围仅为 0~19(避免一般用户抢占系统资源)
  • 一般使用者仅可将 nice 值越调越高;比如 nice5,则未来仅能调整到大于 5

那么调整 nice 值有两种方式:

  • 一开始执行程序就立即给予一个特定的 nice 值:用 nice 指令
  • 调整某个已经存在的 PIDnice 值:用 renice 指令

nice:新执行的指令给予新的 nice 值

1
2
3
4
nice [-n 数字] command

-n:后面接一个数值,数值范围 -20~19

1
2
3
4
5
6
7
8
9
10
11
# 范例 1: 用 root 给一个 nice 值为 -5,用于执行 vim,并观察该进程
[root@study ~]# nice -n -5 vim &
[2] 30185
[root@study ~]# ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 0 8985 7780 0 80 0 - 57972 do_wai pts/0 00:00:00 su
4 S 0 9051 8985 0 90 10 - 29118 do_wai pts/0 00:00:00 bash
4 T 0 30185 9051 0 85 5 - 10791 do_sig pts/0 00:00:00 vim
0 R 0 30652 9051 0 90 10 - 12407 - pts/0 00:00:00 ps
# 原本的 bash PRI 为 90,所以 vim 预设为 90,这里给予 nice -5,所以最终 PRI 变成了 85
# 要注意:不一定正好变成 85,因为会动态调整的

那么通常什么时候需要将 nice 值调大呢?比如:系统的背景工作中,某些比较不重要的进程进行时,比如备份工作,由于备份工作相当耗系统资源,这个时候就可以将备份的指令 nice 值调大一些,可以使系统的资源分配更公平

renice:已存在进程的 nice 重新调整

1
2
renice [number] PID

1
2
3
4
5
6
7
8
9
10
11
12
13
# 范例 1:找出自己的 bash PID ,并将该 PID 的 nice 调整到 -5
[root@study ~]# ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 0 3426 3372 0 80 0 - 58072 do_wai pts/1 00:00:00 su
4 S 0 3443 3426 0 80 0 - 29059 do_wai pts/1 00:00:00 bash
0 R 0 3487 3443 0 80 0 - 12407 - pts/1 00:00:00 ps
[root@study ~]# renice -5 3443
3443 (process ID) old priority 0, new priority -5
[root@study ~]# ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 0 3426 3372 0 80 0 - 58072 do_wai pts/1 00:00:00 su
4 S 0 3443 3426 0 75 -5 - 29059 do_wai pts/1 00:00:00 bash
0 R 0 3493 3443 0 75 -5 - 12407 - pts/1 00:00:00 ps

系统资源的观察

top 可以看到很多系统的资源使用情况,还有其他工具

free:观察内存使用情况

1
2
3
4
5
6
7
8
free [-b|-k|-m|-g|-h] [-t] [-s N -c N]

选项与参数:
-b:单位参数;默认是用 k,其他单位对应 bytes、Mbytes、Kbytes、Gbytes
-t: 输出的最终结果,显示物理内存与 swap 的总量
-s:可以让系统每几秒输出一次,不间断输出;
-c:与 -s 同时处理,让 free 列出几次

1
2
3
4
5
6
# 范例 1:显示目前系统的内存容量
[root@study ~]# free -m
# 总内存 已使用 剩余 可用
total used free shared buff/cache available
Mem: 7631 713 6374 15 542 6671
Swap: 4095 0 4095

shared buff/cache 是缓冲区等使用量,available 是可用容量,当系统忙碌时,可以被释放掉,给系统使用

由于系统会把空闲内存拿来做缓冲区之用,所以你系统没有那么繁忙的时候,也会显示内存被用的多的原因,这个是正常的,需要注意的是 swapswap 最好不要被使用,而且不要使用超过 20% 以上,因为 swap 被使用,那么很有可能是物理内存不够用了

uname:查询系统与核心相关信息

1
2
3
4
5
6
7
8
9
uname [-asrmpi]

选项与参数:
-a:所有系统相关的,都列出来
-s:系统核心名称
-r:核心的版本
-m:本系统的硬件名称,例如 i686 或 x86_64
-p:CPU 的类型,与 -m 类似
-i:硬件的平台(ix86)
1
2
3
4
5
# 范例 1:输出系统的基本信息
[root@study ~]# uname -a
# 核心名称 主机名 核心版本 核心建立日期 与 硬件平台
Linux study.centos.mrcode 3.10.0-1062.el7.x86_64 #1 SMP Wed Aug 7 18:08:02 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

uptime:观察系统启动时间与工作负载

1
2
3
[root@study ~]# uptime 
17:31:46 up 43 min, 2 users, load average: 0.00, 0.01, 0.05
# 当前时间 已开机多久 几个用户登录 平均负载:1、5、15 分钟的平均负载

netstat:追踪网络或插槽文件

该指令常被用在网络的监控方面;netstat 基本上的输出分为两大部分:网络与系统自己的进程相关性部分

1
2
3
4
5
6
7
8
9
netstat -[atunlp]

选项与参数:
-a:将目前系统上所有的联机、监听、Socket 数据都列出来
-t:列出 tcp 网络封包的数据
-u:列出 udp 网络封包的数据
-n:不以进程的服务名称,以端口号来显示
-l:列出目前正在网络监听的(listen)的服务
-p:列出该网络服务的进程 PID
1
2
3
4
5
6
7
8
9
10
11
# 范例 1:列出目前系统上已经建立的网络连接与 unix socket 状态
[root@study ~]# netstat
Active Internet connections (w/o servers) # 与网络相关部分
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 36 study.centos.mrcode:ssh 192.168.4.170:50821 ESTABLISHED
Active UNIX domain sockets (w/o servers) # 与本机的进程自己的相关性(非网络)
Proto RefCnt Flags Type State I-Node Path
unix 2 [ ] DGRAM 12644 /run/systemd/shutdownd
unix 3 [ ] DGRAM 7618 /run/systemd/notify
unix 2 [ ] DGRAM 7620 /run/systemd/cgroups-agent
unix 5 [ ] DGRAM 7634 /run/systemd/journal/socket

网络联机部分:

  • Proto:网络封包协议,主要分为 TCPUDP
  • Recv-Q:非由用户程序连接到此 socket 的复制和总 Bytes
  • Send-Q:非由远程主机传送过来的 acknowledgedBytes
  • Local Address:本地端的 Ip:port
  • Foreign Address:远程主机的 IP:port
  • State:联机状态,主要有建立(ESTABLISED)、监听(LISTEN

上面有一条数据,含义是:192.168.4.170:50821 通过 TCP 封包联机到本机端的 study.centos.mrcode:ssh,状态是 ESTABLISHED;至于更多的知识点这里不深入,在服务器篇讲解

除了网络上的联机之外,Linux 系统上的进程是可以接收不同进程所发来的信息,通过 socket file 可以在两个进程之间通信。比如 X Window 这种需要通过网络连接的软件,新版 distributionsocket 来进行窗口接口的联机沟通。上表中 socket file 的输出字段含义为:

  • Proto:一般是 unix
  • RefCnt:连接到此 socket 的进程数量
  • Flags:联机旗标
  • Typesocket 存取的类型。主要有 STREAM:确认联机、DGRAM:不需确认 两种
  • State:若为 CONNECTED 表示多个进程之间已经联机建立
  • PATH:连接到此 socket 的相关程序路径,或则是相关数据输出的路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 范例 2:找出目前系统上已在监听的网络联机与 PID
[root@study ~]# netstat -tulnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 1380/cupsd
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1579/master
tcp 0 0 127.0.0.1:6010 0.0.0.0:* LISTEN 3765/sshd: mrcode@p
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 1/systemd
tcp 0 0 192.168.122.1:53 0.0.0.0:* LISTEN 1973/dnsmasq
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1379/sshd
tcp6 0 0 ::1:631 :::* LISTEN 1380/cupsd
tcp6 0 0 ::1:25 :::* LISTEN 1579/master
tcp6 0 0 ::1:6010 :::* LISTEN 3765/sshd: mrcode@p
tcp6 0 0 :::111 :::* LISTEN 1/systemd
tcp6 0 0 :::22 :::* LISTEN 1379/sshd
udp 0 0 192.168.122.1:53 0.0.0.0:* 1973/dnsmasq
udp 0 0 0.0.0.0:67 0.0.0.0:* 1973/dnsmasq
udp 0 0 0.0.0.0:111 0.0.0.0:* 1/systemd
udp 0 0 127.0.0.1:323 0.0.0.0:* 938/chronyd
udp 0 0 0.0.0.0:41378 0.0.0.0:* 953/avahi-daemon: r
udp 0 0 0.0.0.0:672 0.0.0.0:* 927/rpcbind
udp 0 0 0.0.0.0:5353 0.0.0.0:* 953/avahi-daemon: r
udp6 0 0 :::111 :::* 1/systemd
udp6 0 0 ::1:323 :::* 938/chronyd
udp6 0 0 :::672 :::* 927/rpcbind
# 最后一个字段是 PID 与进程的指令名称
1
2
3
4
# 范例 3:将上述的 0 0.0.0.0:41378 网络服务关闭
[root@study ~]# kill -9 953
[root@study ~]# killall -9 avahi-daemon

对于非正常的关闭服务方法就用暴力的 kill -9,正常的关闭方式,下个章节讲解

dmesg:分析核心产生的信息

系统在开机的时候,核心会去侦测系统的硬件,那么硬件的检测信息由于开机过程中要么一闪而过,要么没有显示在屏幕上,可以使用 dmesg 来查看

从系统开机起,核心产生的信息都会记录到内存中,通过 dmesg 可以查询到,信息过多时可以通过 more 指令查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 范例 1:输出所有的核心开机时的信息
[root@study ~]# dmesg | more
[ 0.000000] Initializing cgroup subsys cpuset
[ 0.000000] Initializing cgroup subsys cpu
[ 0.000000] Initializing cgroup subsys cpuacct
[ 0.000000] Linux version 3.10.0-1062.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) (gcc version 4.8.5 20150623 (Red Hat
4.8.5-36) (GCC) ) #1 SMP Wed Aug 7 18:08:02 UTC 2019
[ 0.000000] Command line: BOOT_IMAGE=/vmlinuz-3.10.0-1062.el7.x86_64 root=/dev/mapper/centos-root ro crashkernel=auto spect
re_v2=retpoline rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet LANG=zh_CN.UTF-8
[ 0.000000] e820: BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
--More--


# 范例 2:找到硬盘相关信息
[root@study ~]# dmesg | grep -i 'sda'
[ 2.632630] sd 2:0:0:0: [sda] 85491712 512-byte logical blocks: (43.7 GB/40.7 GiB)
[ 2.632651] sd 2:0:0:0: [sda] Write Protect is off
[ 2.632653] sd 2:0:0:0: [sda] Mode Sense: 00 3a 00 00
[ 2.632662] sd 2:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[ 2.643988] sda: sda1 sda2 sda3 sda4 sda5 sda6 sda7 sda8
[ 2.644394] sd 2:0:0:0: [sda] Attached SCSI disk
[ 4.616881] XFS (sda2): Mounting V5 Filesystem
[ 4.636376] XFS (sda2): Ending clean mount

vmstat:侦测系统资源变化

vmstat 可以侦测 CPU、内存、磁盘输入输出状态等信息。比如可以了解一台繁忙的系统到底是哪个环节最耗时间,可以使用 vmstat 分析看看,常见选项与参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
vmstat [-a] [延迟 [总计侦测次数]]		# CPU/内存等信息
vmstat [-fs] # 内存相关
vmstat [-S 单位] # 设置显示数据的单位
vmstat [-d] # 与磁盘有关
vmstat [-p 分区槽] # 与磁盘有关

选项与参数:
-a:使用 inactive/active(是否活跃)取代 buffer/cache 的内存输出信息
-f:开机到目前为止,系统复制(fork)的进程数
-s:将一些事件(开机到目前为止)导致的内存变化情况列表说明
-S:后面可以接单位,例如 k、M 等
-d:列出磁盘的读写总量统计表
-p:后面列出分区槽,可显示该分区槽的读写总量统计表
1
2
3
4
5
6
7
# 范例 1:统计目前主机 CPU 状态,每秒一次,总共 3 次
[root@study ~]# vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 450296 2116 346828 0 0 501 36 181 320 2 3 95 0 0
0 0 0 450156 2116 346860 0 0 0 0 163 223 2 3 95 0 0
0 0 0 450156 2116 346860 0 0 0 0 273 388 3 5 91 0 0

还可以不限制次数,就一直统计字段说明如下:

  • procs:进程

    • r:等待运行中的进程数量
    • b:不可被唤醒的进程数量

    rb 越多表示系统越繁忙。因为系统太忙,导致很多进程无法被执行或一直在等待而无法被唤醒

  • memory:内存

    • swpd:虚拟内存被使用的容量
    • free:未被使用的内容容量
    • buff:用于缓冲存储器
    • cache:用于高速缓存

    这里的含义与 free 指令一致

  • swap:内存交换空间

    • si:由磁盘中将进程取出的量
    • so:由于内存不足而将没用到的进程写入到磁盘的 swap 的容量

    如果 siso 的数值太大,表示内存的数据常常得在磁盘与主存储器之间传来传去,效率很低

  • io:磁盘读写

    • bi:由磁盘读入的区块数量
    • bo:写入到磁盘去的区块数量

    如果这部分数值越高,代表系统的 I/O 非常忙碌

  • system:系统

    • in:每秒被中断的进程次数
    • cs:每秒钟进行的事件切换次数

    这两个值越大,代表系统与接口设备的沟通非常频繁,接口设备包括磁盘、网卡、时钟等

  • CPU

    • us:非核心层的 CPU 使用状态
    • sy:核心层所使用的 CPU 状态
    • id:闲置的状态
    • wa:等待 I/O 所耗费的 CPU 状态
    • st:被虚拟机(virtual machine)所盗用的 CPU 使用状态(2.6.11

练习机上看不到忙碌的数据,如果有一天,你的系统非常忙碌,可以使用该指令来分析是哪里出现了问题

1
2
3
4
5
6
7
8
9
10
# 范例 2:系统上面所有的磁盘读写状态
[root@study ~]# vmstat -d
disk- ------------reads------------ ------------writes----------- -----IO------
total merged sectors ms total merged sectors ms cur sec
sda 7640 1 709893 6377 2486 351 54323 8478 0 5
sdb 116 0 5384 27 0 0 0 0 0 0
sr0 0 0 0 0 0 0 0 0 0 0
dm-0 7072 0 661717 6054 2611 0 45902 10871 0 5
dm-1 88 0 4408 21 0 0 0 0 0 0
dm-2 103 0 10834 58 23 0 4325 56 0 0

至于上面的字段含义,可以通过 man vmstat 查阅

特殊文件与进程

在第 6 章中讲到特殊权限 SUIDSGIDSBIT,那么这些权限对于你的 进程 是如何影响的?进程用到的系统资源,比如硬盘资源,使用 umount 硬盘时,出现提示 「device is busy」的提示是怎么回事?

具有 SUID、SGID 权限的指令执行状态

SUID 的权限与进程的相关性非常大,SUID 的程序是如何被一般用户执行,具有什么特色?

  • SUID 权限仅对二进制程序(binary program)有效
  • 执行者对于该进程需要具有 x 的可执行权限
  • 本权限仅在执行程序的过程中有效(run-time
  • 执行者将具有该程序拥有者(owner)的权限

所以,整个 SUID 的权限会生效是由于具有该权限的程序被触发,一个进程表示一个程序的运行,所以执行者可以具有程序拥有者的权限就是在该程序变成进程的时候

比如执行了 passwd 后你就具有 root 的权限?是因为你再触发 passwd 后,会取得一个新的进程与 PID,该 PID 产生时通过 SUID 来给予该 PID 特殊的权限设置

下面使用 mrcode 登录系统并执行 passwd 后,通过工作控制来理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[mrcode@study ~]$ export LANG=C
[mrcode@study ~]$ passwd
Changing password for user mrcode.
Changing password for mrcode.
(current) UNIX password: # 这里按下 ctrl + z,并按下 enter 键

[1]+ Stopped passwd
[mrcode@study ~]$ pstree -uA
systemd-+-ModemManager---2*[{ModemManager}]
|-sshd-+-sshd---sshd(mrcode)-+-bash---su(root)---bash
| | |-bash---top
| | |-bash---sleep
| | `-sftp-server
| `-sshd---sshd(mrcode)-+-bash-+-passwd(root)
| | `-pstree
| |-bash---top
| |-bash---sleep
| `-sftp-server


从上面的进程来看,在执行 passwd 前是 mrcode 的权限,passwd 则是 root 权限,passwd 是由 bash 衍生出来的,但是权限不一样,这样一来就能理解为什么不同程序所产生的权限不同了,是由于 SUID 程序运行过程中产生的进程的关系。

1
2
3
4
5
6
7
8
9
[mrcode@study ~]$ type passwd
passwd is hashed (/usr/bin/passwd)
[mrcode@study ~]$ ll /usr/bin/passwd
-rwsr-xr-x. 1 root root 27856 Aug 9 2019 /usr/bin/passwd
#可以看到,的确该指令也有 s 权限

# 还可以通过以下指令查找 SUID/SGID 的文件
find / -perm /6000

/proc/* 代表的意义

进程在内存中,内存中的数据都是写入到 /proc/* 目录下的,可以直接查看该目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[mrcode@study ~]$ ll /proc/
total 0
dr-xr-xr-x. 9 root root 0 Mar 15 20:13 1
dr-xr-xr-x. 9 root root 0 Mar 15 20:13 10
dr-xr-xr-x. 9 root root 0 Mar 15 20:13 11
...
dr-xr-xr-x. 2 root root 0 Mar 15 20:54 sysvipc
-r--r--r--. 1 root root 0 Mar 15 20:54 timer_list
-rw-r--r--. 1 root root 0 Mar 15 20:54 timer_stats
dr-xr-xr-x. 4 root root 0 Mar 15 20:54 tty
-r--r--r--. 1 root root 0 Mar 15 20:16 uptime
-r--r--r--. 1 root root 0 Mar 15 20:54 version
-r--------. 1 root root 0 Mar 15 20:54 vmallocinfo
-r--r--r--. 1 root root 0 Mar 15 20:54 vmstat
-r--r--r--. 1 root root 0 Mar 15 20:54 zoneinfo

基本上,目前主机上面的各个进程的 PID 都是以目录的形态存在该目录中。如第 1 行的 PID1,它是开机执行的第一个程序 systemd,该 PID 的所有相关信息都写入在 /proc/1/* 下面

1
2
3
4
5
6
7
8
9
[root@study ~]# ll /proc/1
total 0
dr-xr-xr-x. 2 root root 0 Mar 15 20:48 attr
-rw-r--r--. 1 root root 0 Mar 15 20:53 autogroup
-r--------. 1 root root 0 Mar 15 20:53 auxv
-r--r--r--. 1 root root 0 Mar 15 20:13 cgroup
-r--r--r--. 1 root root 0 Mar 15 20:13 cmdline # 指令串
-r--------. 1 root root 0 Mar 15 20:13 environ # 一些环节变量
lrwxrwxrwx. 1 root root 0 Mar 15 20:13 exe -> /usr/lib/systemd/systemd

里面数据很多,可以查询下 cmdline 的内容

1
2
[root@study ~]# cat /proc/1/cmdline
/usr/lib/systemd/systemd--switched-root--system--deserialize22

上面指令显示了是以什么参数启动的 systemd 指令,这个是针对 PID 有关的内容,下面是针对整个 Linux 系统相关的参数,对应与 /proc 目录下的文件如下

文件名 文件内容
/proc/cmdline 加载 kernel 时所下达的相关指令与参数,查询此文件,可了解指令是如何启动的
/proc/cpuinfo 本机的 CPU 相关信息,包含频率、类型与计算功能等
/proc/devices 系统各个主要装置的主要装置代号,与 mknod 有关
/proc/filesystems 目前系统已经加载的文件系统
/proc/interrupts 目前系统上 IRQ 分配状态
/proc/ioports 目前系统上各个装置所配置的 I/O 地址
/proc/kcore 内存大小,很大?不要读取该文件
/proc/loadavg top 以及 uptime 的三个平均数值就是记录在这里的
/proc/meminfo 使用 free 列出的内存信息,在这里也可以查询到
/proc/modules 目前我们 LInux 已经加载的模块列表,可以看成是驱动程序
/proc/mounts 系统已经挂载的数据,就是用 mount 指令查询出来的数据
/proc/swaps 系统挂载的内存在哪里?使用掉的 partition 记录在这里
/proc/partitions 使用 fsidk -l 会出现目前所有的 partition,在该文件中也有记录
/proc/uptime 使用 uptime 出现的信息
/proc/version 核心的版本,使用 uname -a 显示的信息
/proc/bus/* 一些总线的装置,还有 USB 的装置也记录在这里

这些文件内容建议使用 cat 去查阅看看,不必深入了解,如果未来你要写某些工具软件,那么这个目录下相关文件可能会对你有点帮助

查询已开启文件或已执行进程开启之文件

还有一些与进程相关的指令可以参考与应用

fuser:由文件(或文件系统)找出正在使用该文件的进程

1
2
3
4
5
6
7
8
9
fuser [-umv] [-k [i] [signal]] file/dir

选项与参数:
-u:除了进程的 PID 之外,同时列出该进程的拥有者
-m:后面接的文件名会主动的上提到该文件系统的最顶层,对 umount 不成功很有效
-v:可以列出每个文件与进程还有指令的完整相关性
-k:找出使用该文件/目录的 PID,并试图以 SIGKILL 这个信号给予该 PID
-i:必须与 -k 配合使用,在删除 PID 之前会先询问使用者
-signal:例如 -1 -15 等,若不加的话,预设是 -9:SIGKILL
1
2
3
4
# 范例 1:找出目前所在目录的使用 PID、所属账户、权限
[root@study ~]# fuser -uv .
USER PID ACCESS COMMAND
/root: root 2604 ..c.. (root)bash

有一个进程属于 root,而 ACCESS 项则略为复杂一点:

  • c:此进程在当前的目录下(非次目录)
  • e:可被触发为执行状态
  • f:是一个被开启的文件
  • r:代表顶层目录(root directory
  • F:该文件被开启了,不过在等待回应中
  • m:可能为分享的动态函数库

如果想知道某个文件系统下又多少进程正在占用该文件系统时,可以使用 -m 选项

下面做几个简单测试,包括实体文件系统挂载与 /proc 虚拟文件系统的内容,看看有多少的进程对这些挂载点或其他目录的使用状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 范例 2:找到所有使用到 /proc 这个文件系统的进程
[root@study ~]# fuser -uv /proc/
USER PID ACCESS COMMAND
/proc: root kernel mount (root)/proc
rtkit 834 .rc.. (rtkit)rtkit-daemon

[root@study ~]# fuser -muv /proc/
USER PID ACCESS COMMAND
/proc: root kernel mount (root)/proc
root 1 f.... (root)systemd
root 589 f.... (root)systemd-journal
rtkit 834 .rc.. (rtkit)rtkit-daemon
root 844 f.... (root)udisksd
root 929 f.... (root)NetworkManager
root 1277 F.... (root)libvirtd
root 1638 F.... (root)X
gdm 1693 f.... (gdm)gnome-shell
root 1759 f.... (root)packagekitd
mrcode 2280 f.... (mrcode)top
mrcode 7722 f.... (mrcode)top
# 这就能看到有几个程序在对该目录进行存取


# 范例 3:找到所有使用到 /home 这个文件系统的进程
[root@study ~]# echo $$
2604 # 先确定下自己的 bash 的进程 PID
[root@study ~]# cd /home/
[root@study home]# fuser -muv .
USER PID ACCESS COMMAND
/home: root kernel mount (root)/home
mrcode 1346 ..c.. (mrcode)bash
mrcode 1371 ..c.. (mrcode)bash
mrcode 1378 ..c.. (mrcode)sleep
mrcode 1399 ..c.. (mrcode)sleep
mrcode 1958 ..c.. (mrcode)bash
mrcode 1991 ..c.. (mrcode)sftp-server
mrcode 1992 ..c.. (mrcode)bash
mrcode 2280 ..c.. (mrcode)top
root 2604 ..c.. (root)bash # 看这里,自己的 bash 存在列表中
mrcode 7294 ..c.. (mrcode)bash
mrcode 7358 ..c.. (mrcode)sftp-server
mrcode 7362 ..c.. (mrcode)bash
mrcode 7722 ..c.. (mrcode)top
root 8884 ..c.. (root)passwd

[root@study home]# cd ~
[root@study ~]# umount /home/
umount: /home: target is busy.
(In some cases useful info about processes that use
the device is found by lsof(8) or fuser(1))
# 通过 fuser 知道有好几个进程在该目录下运行,可以通过如下的方式一个一个删除
[root@study ~]# fuser -mki /home/
/home: 7294c 7358c 7362c 7722c 8884c 19238c 19289c 19291c 19601c 25650c 25674c 25685c 25746c
Kill process 7294 ? (y/N)
# 以上指令有一个问题,颇为棘手,就是很容易杀到自己 bash 的进程,那么久直接把直接踢掉了
# 不知道这个这么排除掉是出方便的

上面可以针对整个文件系统,其实也可以针对单一文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 范例 4:找到 /run 下属于 FIFO 类型的文件,并找出存取该文件的进程
[root@study ~]# find /run -type p
/run/dmeventd-client
/run/dmeventd-server
/run/systemd/inhibit/7.ref
/run/systemd/inhibit/6.ref
/run/systemd/inhibit/5.ref
/run/systemd/inhibit/4.ref
/run/systemd/inhibit/2.ref
/run/systemd/inhibit/1.ref
/run/systemd/sessions/13.ref
/run/systemd/sessions/5.ref
/run/systemd/sessions/c1.ref
/run/systemd/initctl/fifo

# 随便找到文件测试
[root@study ~]# fuser -uv /run/systemd/sessions/c1.ref
USER PID ACCESS COMMAND
/run/systemd/sessions/c1.ref:
root 842 f.... (root)systemd-logind
root 1649 F.... (root)gdm-session-wor
# 通常系统的 FIFO 文件都会放置到 /run 下,通过该方式来追踪该文件存取的 process
# 同样也能够看到系统有多忙碌(进程多当然就忙碌)

fuser 的重点是可以让我们了解到某个文件系统或文件目前正在被哪些进程所使用

lsof:列出被进程所开启的文件名

fuser 是通过文件或则装置名去找使用它的进程,而 lsof 则是通过某个进程去找它开启或使用的文件与装置

1
2
3
4
5
6
7
8
9
lsof [-aUu] [+d]

选项与参数:
-a:多想数据需要同时成立才显示出结果时
-U:仅列出 Unix like 系统的 socket 文件类型
-u:后面接 username,列出该使用者相关进程所开启的文件
+d:后面接目录,找出某个目录下已经被开启的文件


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 范例 1:列出目前系统上所有已经被开启的文件与装置
[root@study ~]# lsof
libvirtd 1277 1318 root mem REG 253,0 53848 9645351 /usr/lib64/libavahi-common.so.3.5.3
libvirtd 1277 1318 root mem REG 253,0 155784 8569818 /usr/lib64/libselinux.so.1
libvirtd 1277 1318 root mem REG 253,0 37056 8655202 /usr/lib64/libacl.so.1.1.0
# 文件很多很多,直接刷屏了


# 范例 2:仅列出关于 root 的所有进程开启的 socket 文件
[root@study ~]# lsof -u root -a -U
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd 1 root 12u unix 0xffff8922437ed800 0t0 12513 /run/systemd/private
systemd 1 root 13u unix 0xffff892243dc2c00 0t0 25917 /run/systemd/journal/stdout
systemd 1 root 15u unix 0xffff892243e71800 0t0 25941 /run/systemd/journal/stdout
systemd 1 root 16u unix 0xffff892243e8fc00 0t0 25942 /run/systemd/journal/stdout
systemd 1 root 17u unix 0xffff892243e6ec00 0t0 26002 /run/systemd/journal/stdout
systemd 1 root 18u unix 0xffff892243e6dc00 0t0 26009 /run/systemd/journal/stdout
systemd 1 root 23u unix 0xffff89224359a800 0t0 7620 /run/systemd/notify
# 注意 -a 参数,分别执行 lsof -u root 及 lsof -U 信息都不同
# -a 取他们的交集结果


# 范例 3:列出目前系统上所有被启动的周边装置
[root@study ~]# lsof +d /dev/
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd 1 root 0u CHR 1,3 0t0 5342 /dev/null
systemd 1 root 1u CHR 1,3 0t0 5342 /dev/null
systemd 1 root 2u CHR 1,3 0t0 5342 /dev/null
systemd 1 root 27r CHR 10,235 0t0 7250 /dev/autofs
systemd 1 root 30u unix 0xffff8922436ce000 0t0 7645 /dev/log
kdevtmpfs 13 root cwd DIR 0,5 3340 3 /dev
# 因为都在目录中,所以搜索目录即可

# 范例 4:列出 root 的 bash 程序开启的文件
[root@study ~]# lsof -u root | grep bash
ksmtuned 921 root txt REG 253,0 964600 309027 /usr/bin/bash
bash 20030 root cwd DIR 253,0 4096 25165889 /root
bash 20030 root rtd DIR 253,0 251 64 /
bash 20030 root txt REG 253,0 964600 309027 /usr/bin/bash
bash 20030 root mem REG 253,0 106075056 309022 /usr/lib/locale/locale-archive
bash 20030 root mem REG 253,0 61624 8548289 /usr/lib64/libnss_files-2.17.so
bash 20030 root mem REG 253,0 2156160 8532847 /usr/lib64/libc-2.17.so
bash 20030 root mem REG 253,0 19288 8532853 /usr/lib64/libdl-2.17.so
bash 20030 root mem REG 253,0 174576 8548350 /usr/lib64/libtinfo.so.5.9
bash 20030 root mem REG 253,0 163400 8532840 /usr/lib64/ld-2.17.so
bash 20030 root mem REG 253,0 26254 16946906 /usr/lib64/gconv/gconv-modules.cache
bash 20030 root 0u CHR 136,0 0t0 3 /dev/pts/0
bash 20030 root 1u CHR 136,0 0t0 3 /dev/pts/0
bash 20030 root 2u CHR 136,0 0t0 3 /dev/pts/0
bash 20030 root 255u CHR 136,0 0t0 3 /dev/pts/0

可以通过范例 4 找出某个进程是否有启用哪些信息

pidof:找出某个正在运行的程序的 PID

1
2
3
4
5
6
pidof [-sx] program_name

选项与参数:
-s:仅列出一个 PID 而不列出所有的 PID
-x:同时列出该程序可能的 PPID 那个进程的 PID

1
2
3
4
5
# 范例 1:列出目前系统上 systemd 以及 rsyslogd 这两个程序的 PID
[root@study ~]# pidof systemd rsyslogd
1 1265
# 结果显示的是两个 PID

pidof 指令较简单,可配合 pas aux 与正则表示法,就可以很轻易的找到你想要的进程内容了。如果要找的是 bash,那就 pidof bash ,就列出一堆 PID 号码了

SELinux 初探

CentOS 5.x 之后,SELinux 已经是个非常完备的核心模块了,尤其是 CentOS 提供了很多管理 SELinux 的指令与机制,因此在整理架构上面是单纯且容易操作管理的,所以在没有自行开发网络服务软件以及使用其他第三方协力软件的情况下,也就是全部使用 CentOS 官方提供的软件来使用我们服务器的情况下,建议不要关闭 SELinux

什么是 SELinux

Security Enhanced Linux 的缩写 SELinux,字面意思是安全强化的 LInux。至于强化的是哪个部分?下面来了解下

当初设计的目标:避免资源的误用

SELinux 是由美国国家安全局(NSA)开发的,需求来源于内部员工资源误用导致系统出现问题;

资源误用:将一个 /var/www/html/ 目录权限设置成 777,那么当启动 www 服务器软件,就意味着这个软件触发的进程拥有对该目录写入的权限,只要通过该进程服务器对目录大量写入,就会导致系统硬盘资源被爆破

SELinux 是在进行进程、文件等西部权限设置依据的一个核心模块,由于启动网络服务的也是进程,因此刚好也能够控制网络服务是否能存取系统资源的一道关卡

在讲解 SELinux 之前,先回顾一下之前讲到的:系统文件权限与用户之间的关系

传统的文件权限与账户关系:自主式访问控制 DAC

13 章中讲到:系统账户主要分为系统管理员(root)与一般用户,他们能否使用系统上的文件资源与 rwx 权限设置有关。(各种权限设置对 root 无效)。当某个进程想要对文件进行存取时,系统会根据该进程的拥有者、群组,并比对文件的权限,若通过权限检查,就可以存取该文件

这种存取文件的方式被称为 自主式访问控制 Discretionary Access Controller 简称 DAC,基本上就是依据进程的拥有者与文件资源的 rwx 权限来决定有无存取的能力。DAC 有如下困扰:

  • root 具有最高的权限:只要取得属于 root 的进程,那么就很危险
  • 使用者可以取得进程来变更文件资源的访问权限:如果将某个目录权限不小心设置为 777,由于对任何人的权限会变成 rwx,因此该目录就会被任何人所任意存取

以政策规则规定特定进程读取特定文件:委任式访问控制 MAC

为了避免 DAC 的困扰,SELinux 导入了委任式访问控制 Mandatory Access Control 简称 MAC

MAC 可以针对特定的进程与特定的文件资源来进行权限的控制。即使你是 root,那么在使用不同的进程时,你所能取得的权限并不一定是 root,而需要看当时该进程的设置。如此一来针对控制的「主体」变成了「进程」而不是使用者,但是真个系统进程很多、文件也很多,一项一项控制太麻烦,所以 SELinux 也提供一些预设的政策 Policy ,并在该政策内提供多个规则 rule,让你可以选择是否启用该控制规则

在该种模式下,进程能够活动的空变小了。比如:www 服务器软件达成进程为 httpd 这个程序,默认情况下, httpd 仅能在 /var/www 目录下存取文件,如果 httpd 进程要去其他目录存储数据时,除了规则设置要开放外,目标目录也要设置成 httpd 可读取的模式 type 才行,限制非常多,所以,即使 httpd 这个进程被黑客取得了控制权限,它也无权限浏览其他的目录文件

简单说,针对 Apache 这个 www 网络服务使用 DACMAC 的结果来说,两者的关系可用下图来说明

传统的进程与文件的 rwx 方式,在这中间增加了 SELinux 安全性本文 规则 ,通过了这些规则之后,才和传统的进程与文件的 rwx 方式一致。

笔者理解为是通过拦截器的方式,出台了 SELinux ,前面通过 SElinux 拦截细化权限,符合要求的再去到传统的方式,这样一来就对传统的加强了。

安全性本文 Security Context

CentOS 7.xtarget 政策提供了非常多的规则,只需要如何开启关闭某项规则即可。

安全性本文则非常麻烦,可能需要自行配置它,比如你常常设置文件的 rwx 权限,那么这个安全性本文就类似,可以看成是 SELinux 中的 rwx

安全性本文存在于主体进程中与目标文件资源中,物理位置是放在文件的 inode 中,因此主体进程想要读取目标文件资源时,同样需要读取 inode,这就可以对比安全性本文一级 rwx 等权限是否正确了。

观察安全性本文可使用 ls -Z ,但是前提是需要启动 SELinux 才行,下个小节会介绍如何启动 SELinux,这里先介绍知识点

1
2
3
4
5
6
7
8
[root@study ~]# ls -Z
-rw-r--r--. root root unconfined_u:object_r:admin_home_t:s0 accountadd.sh
-rw-r--r--. root root unconfined_u:object_r:admin_home_t:s0 accountadd.txt
-rwxr--r--+ root root unconfined_u:object_r:admin_home_t:s0 acl_test1
-rw-r--r--. root root unconfined_u:object_r:admin_home_t:s0 addaccount2.sh
-rw-------. root root system_u:object_r:admin_home_t:s0 anaconda-ks.cfg
-rw-r--r--. root root system_u:object_r:admin_home_t:s0 initial-setup-ks.cfg
# 上述字段很长的那一栏就是安全性本文了

安全性本文主要用冒号分割为三个字段,含义如下:

  • identify:身份

    相当于账户方面的身份识别,常见有几下几种类型

    • unconfined_u:不受限的用户

      该文件来自不受限的进程所产生的,一般来说,可以使用可登录账号来取得 bash,预设的 bash 环境是不受 SELinux 管制的,因为 bash 并不是什么特别的网络服务,因此在该 bash 进程所产生的文件,其身份识别大多就是该类型了

    • system_u:系统用户

      基本上,如果是系统会软件本身所提供的文件,大多就是该类型,如果是用户通过 bash 自己建立的文件,大多则是不受限的 unconfined_u 身份,如果是网络服务所产生的文件,或则是系统服务运行过程中所产生的文件,则大部分是 system_u

  • role:角色

    通过该字段,可以知道这个资料是属于进程、文件资源还是代表使用者,一般的角色有:

    • object_r:代表的是文件或目录等文件资源
    • system_r:代表的是进程,不过一般使用者也会被指定为 system_r
  • type:类型,最重要

    在预设的 targeted 政策中, identifyrole 字段基本上是不重要的,而 type 是最重要的,基本上,一个主体进程能不能读取到这个文件资源,与类型字段有关,而类型字段在文件与进程的定义不相同:

    • type:在文件资源(object)上面称为类型(type
    • domain:在主体进程(subject)则称为领域(domain

    domain 需要与 type 搭配,则该进程才能够顺利的读取文件资源

进程与文件 SELinux type 字段的相关性

通过身份识别与角色字段的定义,我们可以大概某个进程所代表的意义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 观察下系统 进程的 SELinux 相关信息
[root@study ~]# ps -eZ
LABEL PID TTY TIME CMD
system_u:system_r:init_t:s0 1 ? 00:00:01 systemd
system_u:system_r:kernel_t:s0 2 ? 00:00:00 kthreadd
system_u:system_r:kernel_t:s0 4 ? 00:00:00 kworker/0:0H
system_u:system_r:kernel_t:s0 5 ? 00:00:00 kworker/u2:0
...
system_u:system_r:sshd_t:s0-s0:c0.c1023 2344 ? 00:00:00 sshd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 2350 ? 00:00:00 sshd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 2353 pts/0 00:00:00 bash
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 2415 pts/0 00:00:00 su
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 2424 pts/0 00:00:00 bash
system_u:system_r:kernel_t:s0 2726 ? 00:00:00 kworker/u2:2
system_u:system_r:kernel_t:s0 2778 ? 00:00:00 kworker/0:1
system_u:system_r:kernel_t:s0 2836 ? 00:00:00 kworker/0:3
system_u:system_r:kernel_t:s0 2877 ? 00:00:00 kworker/0:0
system_u:system_r:ksmtuned_t:s0 2885 ? 00:00:00 sleep
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 2886 pts/0 00:00:00 ps

# 基本上进程主要分为两大类,
# 一种是系统有受限的 system_u:system_r,
# 另一种可能是用户自己的,比较不受限的进程(通常是本机用户自己执行的程序 ) unconfined_u:unconfined_r

# unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 2424 pts/0 00:00:00 bash
# 比如上面这个进程,就是我们自己执行命令所在的 bash

基本上,这些对于资料在 targeted 政策下的对应对下

身份识别 角色 对应在 targeted 的意义
unconfined_u unconfined_r 一般可登陆使用者的进程,比较没有受限的进程。大多数都是用户已经顺利登陆系统(不论是网络还是本机登陆来取得可用的 shell)后,所用来操作系统的进程,如 bash x window 相关富安居等
system_u system_r 由于为系统账户,因此是非交谈式的系统运行进程,大多数的系统进程均是这种类型

如上所述,在预设的 target 政策下,最重要的是 type 字段,主体与目标之间是否具有可读写的权限,与进程的 domain 与文件的 type 有关。这两者的关系可以使用 crond 以及他的配置文件来说明

1
2
3
4
5
6
7
8
9
10
11
# 1. 先看看 crond 这个进程的安全本文内容
[root@study ~]# ps -eZ | grep cron
system_u:system_r:crond_t:s0-s0:c0.c1023 1398 ? 00:00:00 atd
system_u:system_r:crond_t:s0-s0:c0.c1023 1400 ? 00:00:00 crond
# 这个安全本文的类型名称为 crond_t 格式

# 2. 看看 /usr/ssbin/crond 、 /etc/cron.d、/etc/cron.d 文件的安全本文内容
[root@study ~]# ll -Zd /usr/sbin/crond /etc/crontab /etc/cron.d
drwxr-xr-x. root root system_u:object_r:system_cron_spool_t:s0 /etc/cron.d
-rw-r--r--. root root system_u:object_r:system_cron_spool_t:s0 /etc/crontab
-rwxr-xr-x. root root system_u:object_r:crond_exec_t:s0 /usr/sbin/crond

执行 /usr/ssbin/crond 后,该程序编程的进程 domain 类似是 crond_t,它能够读取的配置文件是 system_cron_spool_t 类型。因此无论 /etc/crontab/etc/cron.d 以及 /var/spool/cron 都会是相关的 SELinux 类型(/var/spool/cronuser_cron_spool_t 类型)。下面图示说明

  1. crond 执行后,具有 crond_exec_t 类型
  2. 该文件类型会造成主体进程 Subject 具有 crond 这个领域 domain,政策针对这个领域有许多规则,其中就包括可以读取的目标资源类型
  3. 由于 crond domain 被设置为可以读取 system_cron_spool_t 类型的目标文件 object,因此你的配置文件放到 /etc/cron.d/ 目录下,就能够被 crond 进程读取了
  4. 但是最终能不能读到正确的资料,还需要看传统的 rwx 是否符合 Linux 的权限规范

下面来测试上述说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 1. 假设你因为不熟悉的缘故,因此是在 root 家目录建立一个如下的 cron 设置
[root@study ~]# vim checktime
10 * * * * root sleep 60s

# 2. 发现文件放错目录了,又不想要保留副本,因此使用 mv 移动到正确的目录
[root@study ~]# mv checktime /etc/cron.d/
[root@study ~]# ll /etc/cron.d/checktime
-rw-r--r--. 1 root root 26 Mar 17 13:12 /etc/cron.d/checktime
# 权限是 644,任何进程都可以读取

# 3. 强制重新启动 crond,然后查看登录日志
[root@study ~]# systemctl restart crond
[root@study ~]# tail /var/log/cron
Mar 17 13:01:01 study run-parts(/etc/cron.hourly)[3889]: finished mcelog.cron
Mar 17 13:10:01 study CROND[3972]: (root) CMD (/usr/lib64/sa/sa1 1 1)
Mar 17 13:14:01 study crond[1400]: ((null)) Unauthorized SELinux context=system_u:system_r:system_cronjob_t:s0-s0:c0.c1023 file_context=unconfined_u:object_r:admin_home_t:s0 (/etc/cron.d/checktime)
Mar 17 13:14:01 study crond[1400]: (root) FAILED (loading cron table)
Mar 17 13:15:08 study crond[1400]: (CRON) INFO (Shutting down)
Mar 17 13:15:08 study crond[4073]: (CRON) INFO (RANDOM_DELAY will be scaled with factor 13% if used.)
Mar 17 13:15:08 study crond[4073]: ((null)) Unauthorized SELinux context=system_u:system_r:system_cronjob_t:s0-s0:c0.c1023 file_context=unconfined_u:object_r:admin_home_t:s0 (/etc/cron.d/checktime)
Mar 17 13:15:08 study crond[4073]: (root) FAILED (loading cron table)
Mar 17 13:15:08 study crond[4073]: (CRON) INFO (running with inotify support)
Mar 17 13:15:08 study crond[4073]: (CRON) INFO (@reboot jobs will be run at computer's startup.)

# 上述日志中有 Unauthorized 的信息,表示有错误,因为原本的安全本文与文件的实际安全本文无法搭配的缘故,
# 信息还列出了 SELinux context 与 file_context 的信息,表示的确不匹配

SELinux 三种模式的启动、关闭与观察

并非所有的 Linux distribution 都支持 SELinuxCentOS 7.x 本身就有支持 SELinux,所以你不需要自行编译 SELinux 到你的 Linux 核心中。目前 SELinux 是否启动有三种模式:

  • enforcing:强制模式,表示 SELinux 运行中,且已经正确的开始限制 domain/type
  • permissive:宽容模式,表示 SELinux 运行中,不过仅有警告进行并不会实际限制 domain/type 的存取。这种模式可以用来 debug SELinux 的配置
  • disabled:SELinux 关闭中

三种模式的示意图如下:

注意:并非有所的进程都受 SELinux 的管控,注意是有 受限的进程主体,可以通过 ps -eZ 来观察该进程是否有受限(confined)。下面来观察 crondbash 程序是否有被限制

1
2
3
4
5
[root@study ~]# ps -eZ | grep -E 'cron|bash'
system_u:system_r:crond_t:s0-s0:c0.c1023 1398 ? 00:00:00 atd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 2353 pts/0 00:00:00 bash
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 2424 pts/0 00:00:00 bash
system_u:system_r:crond_t:s0-s0:c0.c1023 4073 ? 00:00:00 crond

因为目前 target 这个政策下,只有第 3 个字段 type 会有影响,因此可以看到 crondcrond_t 类型,是受限的,而 bashunconfined_t 类型,是不受限的,也就是说 bash 不会经过上图的流程,而直接去判定 rwx

可以通过以下方式获取当前的 SELinux 模式

1
2
[root@study ~]# getenforce 
Enforcing

查询当前 SELinux 的政策(Policy

1
2
3
4
5
sestatus [-vb]

选项与参数:
-v:检查 /etc/sestatus.conf 内的文件与进程的安全性本文内容
-b:将目前政策的规则布尔值列出,即某些规则 rule 是否要启动(0/1)
1
2
3
4
5
6
7
8
9
10
11
12
13
# 范例 1:列出目前 SELinux 使用的哪个政策 Policy

[root@study ~]# sestatus
SELinux status: enabled # SELinux 是否启动
SELinuxfs mount: /sys/fs/selinux # SELinux 的相关文件数据挂载点
SELinux root directory: /etc/selinux # SELinux 的根目录所在
Loaded policy name: targeted # 当前的政策
Current mode: enforcing # 当前模式
Mode from config file: enforcing # 目前配置文件内规范的 SELinux 模式
Policy MLS status: enabled # 是否含有 MLS 的模式机制
Policy deny_unknown status: allowed # 是否预设抵挡未知的主体进程
Max kernel policy version: 31

上述信息科知道,SELinux 目前的政策是 targeted ,可通过如下方式修改

1
2
3
4
5
6
7
8
9
10
11
12
[root@study ~]# vim /etc/selinux/config 
# This file controls the state of SELinux on the system.
# SELINUX= can take one of these three values:
# enforcing - SELinux security policy is enforced.
# permissive - SELinux prints warnings instead of enforcing.
# disabled - No SELinux policy is loaded.
SELINUX=enforcing # 可选择为上述 3 个
# SELINUXTYPE= can take one of three values:
# targeted - Targeted processes are protected,
# minimum - Modification of targeted policy. Only selected processes are protected.
# mls - Multi Level Security protection.
SELINUXTYPE=targeted # 可选值为上述 3 个

SElinux 的启动与关闭

由于 SElinux 是整合到核心中去的,因此修改上述配置文件之后,需要重新启动。

注意:如果从 disable 转到启动 SELinux 的模式时,由于系统必须要针对文件写入安全性本文信息,因此开机过程需要耗费不少时间等待重新写入 SELinux 安全性本文(有时也称为SELinux Label),而且在写完之后还需要重新启动一次,启动成功之后,再使用 getenforce 和 sestatus 来观察是否有成功启动到 Enforcing 模式

如果当前已经是 Enforcing 模式,可能由于一些设置问题大道至 SELinux 让某些服务无法正常的运行,此时可将模式修改为宽容模式(permissive),让 SELinux 只发出警告信息

1
2
3
4
5
6
setenforce [0|1]

选项与参数:
0:转成 permissive 宽容模式
1:转成 Enforcing 强制模式
注意:无法在 Disabled 模式下进程模式的切换

某些时候从 Disabled 换成 Enforcing 之后,有部分服务可能无法顺利启动,可能会报错 /lib/xxx 数据没有权限读取的错误信息。这大多数是由于重新写入 Selinux typeRelabel)出错的原因,使用 Permissive 模式就没有该错误。最简单的办法是在 Permissive 模式下使用指令 restorecon -Rv / 重新还原所有 SELinux 的类型。

SELinux 政策内的规则管理

SELinux 各个规则的布尔值查询:getsebool

1
2
3
4
getsebool [-a] [规则名称]

选项与参数:
-a:列出目前系统上所有 SELinux 规则的布尔值为开启或关闭(on/off)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 范例 1:查询所有的布尔值设置
[root@study ~]# getsebool -a
abrt_anon_write --> off
abrt_handle_event --> off
abrt_upload_watch_anon_write --> on
...
cron_can_relabel --> off # 这个与 cron 有关
cron_system_cronjob_use_shares --> off
cron_userdomain_transition --> on
...
httpd_anon_write --> off # 与网页 http 有关
httpd_builtin_scripting --> on
httpd_can_check_spam --> off
# 每一行都是一个规则

SELinux 各个规则规范的主体进程能够读取的文件 SELinux type 查询 seinfo、sesearch

上述指令知道了所有的规则开启情况,可以通过 seinfosesearch 等工具来查看每个规则具体在限制什么。

上述工具并未预装,请拿出安装光盘挂载到 /mnt 目录下,安装

1
2
3
4
5
6
7
8
9
10
11
12
[root@study ~]# blkid 
/dev/sr0: UUID="2019-09-11-18-50-31-00" LABEL="CentOS 7 x86_64" TYPE="iso9660" PTTYPE="dos"
/dev/sda1: UUID="e9d54afb-2afe-42de-87fe-9f55d747fcd9" TYPE="xfs"
/dev/sda2: UUID="CNUXwS-J3Lh-0nDA-TssW-l1vT-90us-MHYnT1" TYPE="LVM2_member"
/dev/mapper/centos_study-root: UUID="d7e09bb4-2f04-4ed4-b377-91a22fe85ce7" TYPE="xfs"
/dev/mapper/centos_study-swap: UUID="684eebc0-3f70-4fc1-9a5d-d683f6a07cd0" TYPE="swap"
[root@study ~]# mount /dev/sr0 /mnt/
mount: /dev/sr0 is write-protected, mounting read-only
[root@study ~]# yum install /mnt/Packages/setools-console-*
...
Complete!
[root@study ~]# umount /mnt/ # 卸载光盘
1
2
3
4
5
6
7
8
seinfo [-Atrub]

选项与参数:
-A:列出 SELinux 的状态、规则布尔值、身份识别、角色、类型等所有信息
-u:列出 SELinux 的所有身份识别 user 种类
-r:列出 SELinux 的所有角色 role 种类
-t:列出 SELinux 的所有类型 type 种类
-b:列出所有规则的种类(布尔值)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 范例 1:列出 SELinux 在此政策下的统计状态
[root@study ~]# seinfo

Statistics for policy file: /sys/fs/selinux/policy
Policy Version & Type: v.31 (binary, mls)

Classes: 130 Permissions: 272
Sensitivities: 1 Categories: 1024
Types: 4792 Attributes: 253
Users: 8 Roles: 14
Booleans: 316 Cond. Expr.: 362
Allow: 107360 Neverallow: 0
Auditallow: 157 Dontaudit: 10020
Type_trans: 18129 Type_change: 74
Type_member: 35 Role allow: 39
Role_trans: 416 Range_trans: 5899
Constraints: 143 Validatetrans: 0
Initial SIDs: 27 Fs_use: 32
Genfscon: 103 Portcon: 614
Netifcon: 0 Nodecon: 0
Permissives: 0 Polcap: 5

# 当前政策是 targeted ? (哪里显示的?),此政策下的 Types 类型有 4792 个
# SELinux 的规则(Booleans)有 316 条

在前面讲到过几个身份识别 user 与 角色 roleseinfo 可以查询到所有的种类,可自行查询

在前面讲到 /etc/cron.d/checktimeSElinux type 类型不太对,我们知道 crond 进程的 typecrond_t,那么查找下 crond_t 能够读取的文件 SELinux type 有哪些

1
2
3
4
5
6
sesearch [-A] [-s 主体类别] [-t 目标类别] [-b 布尔值]

选项与参数:
-A:列出后面数据中,允许「读取或放行」的相关数据
-t:后面还要接 type、例如 -t httpd_t
-b:后面接 SELinux 的规则,例如 -b httpd_enable_ftp_server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 范例 1:找出 crond_t 主体进程能够读取的文件 SELinux type

[root@study ~]# sesearch -A -s crond_t | grep spool
allow crond_t var_spool_t : dir { ioctl read getattr lock search open } ;
allow crond_t system_cron_spool_t : dir { ioctl read getattr lock search open } ;
allow crond_t user_cron_spool_t : lnk_file { read getattr } ;
allow crond_t user_cron_spool_t : file { ioctl read write create getattr setattr lock append unlink link rename open } ;
allow crond_t system_cron_spool_t : file { ioctl read write create getattr setattr lock append unlink link rename open } ;
allow crond_t var_spool_t : file { ioctl read getattr lock open } ;
allow crond_t cron_spool_t : file { ioctl read write create getattr setattr lock append unlink link rename open } ;
allow daemon user_cron_spool_t : file { ioctl read write getattr lock append } ;
allow crond_t cron_spool_t : dir { ioctl read write getattr lock add_name remove_name search open } ;
allow crond_t user_cron_spool_t : dir { ioctl read write getattr lock add_name remove_name search open } ;
allow crond_t user_cron_spool_t : file { ioctl read write create getattr setattr lock append unlink link rename open } ;
allow crond_t system_cron_spool_t : file { ioctl read write create getattr setattr lock append unlink link rename open } ;

# allow 后面是主体进程以及文件的 SELinux type,上面数据是截取出来的
# crond_t 可以读取 system_cron_spool_t 的文件/目录类型等

# 范例 2:找出 crond_t 是否能读取 /etc/cron.d/checktime 这个我们自定义的配置文件?
[root@study ~]# ll -Z /etc/cron.d/checktime
-rw-r--r--. root root unconfined_u:object_r:admin_home_t:s0 /etc/cron.d/checktime
# 两个重点:SELinux type 为 admin_home_t,一个是文件(file)

[root@study ~]# sesearch -A -s crond_t | grep admin_home_t
allow domain admin_home_t : dir { getattr search open } ;
allow crond_t admin_home_t : dir { ioctl read getattr lock search open } ;
allow userdom_filetrans_type admin_home_t : lnk_file { read getattr } ;
allow userdom_filetrans_type admin_home_t : dir { ioctl read write getattr lock add_name remove_name search open } ;
allow domain admin_home_t : lnk_file { read getattr } ;
allow crond_t admin_home_t : lnk_file { read getattr } ;

# 发现有 crond_t admin_home_t 存在,不过这个是总体的信息
# 没有针对某些规则的查询,所以不能确定 checktime 能否被读取,但是基本上就是 SELinux type 出现问题,才无法读取的

现在知道了 /etc/cron.d/checktimeSELinux type 错误导致无法读取的。看来在 getsebool -a 中看到的 httpd_enable_homedirs 是什么?又是规范了哪些主体进程能够读取的 SELinux type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[root@study ~]# semanage boolean -l | grep httpd_enable_homedirs
httpd_enable_homedirs (off , off) Allow httpd to enable homedirs
# httpd_enable_homedirs 的功能是允许 httpd 进程读取用户家目录

# 范例 3:列出该规则中,主体进程能够读取的文件 SELinux type
[root@study ~]# sesearch -A -b httpd_enable_homedirs
Found 77 semantic av rules:
allow httpd_t user_home_type : lnk_file { read getattr } ;
allow httpd_suexec_t user_home_type : lnk_file { read getattr } ;
allow httpd_suexec_t user_home_dir_t : lnk_file { read getattr } ;
allow httpd_t nfs_t : lnk_file { read getattr } ;
allow httpd_sys_script_t nfs_t : file { ioctl read getattr lock open } ;
allow httpd_sys_script_t cifs_t : lnk_file { read getattr } ;
allow httpd_user_script_t user_home_type : lnk_file { read getattr } ;
allow httpd_user_script_t user_home_type : dir { getattr search open } ;
allow httpd_t cifs_t : file { ioctl read getattr lock open } ;
allow httpd_sys_script_t nfs_t : dir { getattr search open } ;
allow httpd_sys_script_t nfs_t : dir { ioctl read getattr lock search open } ;
allow httpd_sys_script_t nfs_t : dir { getattr search open } ;
allow httpd_sys_script_t nfs_t : dir { ioctl read getattr lock search open } ;
allow httpd_t user_home_dir_t : dir { getattr search open } ;
allow httpd_sys_script_t cifs_t : file { ioctl read getattr lock open } ;
allow httpd_sys_script_t user_home_dir_t : dir { getattr search open } ;
allow httpd_sys_script_t user_home_dir_t : lnk_file { read getattr } ;
xxx
# 从上面的数据才可以理解,主要是放行 httpd_t 能否读取用户家目录的文件 (笔者这里是懵逼的没有看出来)
# 所以,如果该规则没有启动,基本上 httpd_t 这种进程就无法读取用户家目录下的文件

修改 SELinux 规则的布尔值 setsebool

查询到某个 SELinux rule ,并且以 seaserch 知道该规则的用途后,可以通过下面的方式来管理

1
2
3
setsebool [-p] [规则名称][0|1]

-P:直接将设置值写入配置文件,该设置数据未来会生效
1
2
3
4
5
6
7
8
# 范例 1:查询 httpd_enable_homedirs 这个规则的状态,并且修改这个规则为不同的布尔值
[root@study ~]# getsebool httpd_enable_homedirs
httpd_enable_homedirs --> off # 关闭状态
[root@study ~]# setsebool -P httpd_enable_homedirs 1 # 开启它
[root@study ~]# getsebool httpd_enable_homedirs
httpd_enable_homedirs --> on


SELinux 安全本文的修改

SELinux 对受限的主体进程没有影响:

  1. 考虑 SELinux 的三种类型
  2. 考虑 SELinux的政策规则是否放行
  3. 比对 SELinux type 关系

上面讲解过可以通过 sesearch 来找到主体进程与文件的 SELinux type 关系,那么怎么修改文件的 SELinux type,能让主体进程读到呢?

使用 chcon 手动修改文件的 SELinux type

1
2
3
4
5
6
7
8
9
10
chcon [-R] [-t type] [-u user] [-r role] 文件
chcon [-R] --reference=范例文件 文件

选项与参数:
-R:连同该目录下的次目录也同时修改
-t:后面接安全性本文的类型字段,例如 httpd_sys_content_t
-u:后面接身份识别,例如 system_u (不重要)
-r:后面接角色,例如 system_r (不重要)
-v:若有变化成功,将变动的结果列出来
--reference=文件:拿某个文件档范例来修改后续接的文件的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 范例 1:查询 /etc/hosts 的 SELinux type,并将该类型套用到 /etc/cron.d/checktime 上
[root@study ~]# ll -Z /etc/hosts
-rw-r--r--. root root system_u:object_r:net_conf_t:s0 /etc/hosts
# net_conf_t 是上面文件中的类型
[root@study ~]# chcon -v -t net_conf_t /etc/cron.d/checktime
changing security context of '/etc/cron.d/checktime'
[root@study ~]# ll -Z /etc/cron.d/checktime
-rw-r--r--. root root unconfined_u:object_r:net_conf_t:s0 /etc/cron.d/checktime

# 范例 2:直接以 /etc/shadow 的 type 套用
[root@study ~]# chcon -v --reference=/etc/shadow /etc/cron.d/checktime
changing security context of '/etc/cron.d/checktime'
[root@study ~]# ll -Z /etc/shadow /etc/cron.d/checktime
-rw-r--r--. root root system_u:object_r:shadow_t:s0 /etc/cron.d/checktime
----------. root root system_u:object_r:shadow_t:s0 /etc/shadow

上面的示例并不能解决 crond 不能读取 /etc/cron.d/checktime 的问题,因为需要改成 /etc/cron.d 下的标准 type 才行。可以使用 restorecon 来让 SELinux 自己默认解决目录下的 type 问题

使用 restorecon 让文件恢复正确的 SELinux type

1
2
3
4
5
restorecon [-Rv] 文件或目录

选项与参数:
-R:连同次目录一起修改
-v:将过程显示到屏幕上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 范例 3:将 /etc/cron.d/ 下的文件都恢复成预设的 SELinux type
[root@study ~]# restorecon -Rv /etc/cron.d/
restorecon reset /etc/cron.d/checktime context system_u:object_r:shadow_t:s0->system_u:object_r:system_cron_spool_t:s0

# 上面将 shadow_t 改成了 system_cron_spool_t 类型

# 范例 4:重新启动 crond 看看有没有正确启动 checktime
[root@study ~]# systemctl restart crond
[root@study ~]# tail /var/log/cron
Mar 17 16:01:01 study CROND[5886]: (root) CMD (run-parts /etc/cron.hourly)
Mar 17 16:01:01 study run-parts(/etc/cron.hourly)[5886]: starting 0anacron
Mar 17 16:01:01 study run-parts(/etc/cron.hourly)[5898]: finished 0anacron
Mar 17 16:01:01 study run-parts(/etc/cron.hourly)[5886]: starting mcelog.cron
Mar 17 16:01:01 study run-parts(/etc/cron.hourly)[5904]: finished mcelog.cron
Mar 17 16:10:01 study CROND[5989]: (root) CMD (/usr/lib64/sa/sa1 1 1)
Mar 17 16:12:48 study crond[4073]: (CRON) INFO (Shutting down)
Mar 17 16:12:48 study crond[6068]: (CRON) INFO (RANDOM_DELAY will be scaled with factor 62% if used.)
Mar 17 16:12:49 study crond[6068]: (CRON) INFO (running with inotify support)
Mar 17 16:12:49 study crond[6068]: (CRON) INFO (@reboot jobs will be run at computer's startup.)
# 没有报错信息

从这里看来 restorecon 很方便,chcon 还是比较麻烦的

semanage 默认目录的安全性本文查询与修改

为什么 restorecon 可以恢复原本的 SELinux type 呢?那一定是有个地方在记录每个文件/目录的 SELinux 默认类型

  1. 如何查询预设的 SELinux type
  2. 如何增加、修改、删除 预设的 SELinux type
1
2
3
4
5
6
7
8
semanage {login|user|port|interface|fcontext|translation} -l
semanage fcontext -{a|d|m} [-frst] file_spec

选项与参数:
fcontext:主要用在安全性本文方面的用途, -l 为查询
-a:增加;可以增加一些目录的默认安全性本文类型设置
-m:修改
-d:删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 范例 1:查询 /etc/   /etc/cron.d/ 的预设 SELinux type
[root@study ~]# semanage fcontext -l | grep -E '^/etc |^/etc/cron'
/etc/cron.daily(/.*)? all files system_u:object_r:bin_t:s0
/etc/cron.weekly(/.*)? all files system_u:object_r:bin_t:s0
/etc/cron.hourly(/.*)? all files system_u:object_r:bin_t:s0
/etc/cron.monthly(/.*)? all files system_u:object_r:bin_t:s0
/etc/cron.minutely/openshift-facts regular file system_u:object_r:openshift_cron_exec_t:s0
/etc/cron\.(daily|monthly)/acct regular file system_u:object_r:acct_exec_t:s0
/etc/cron\.(daily|weekly)/sysklogd regular file system_u:object_r:logrotate_exec_t:s0
/etc/cron\.(daily|monthly)/mailman regular file system_u:object_r:mailman_queue_exec_t:s0
/etc/cron\.(daily|weekly)/man-db.* regular file system_u:object_r:mandb_exec_t:s0
/etc/cron\.(daily|monthly)/radiusd regular file system_u:object_r:radiusd_exec_t:s0
/etc/cron\.(daily|weekly)/ntp-simple regular file system_u:object_r:ntpd_exec_t:s0
/etc/cron\.(daily|weekly)/ntp-server regular file system_u:object_r:ntpd_exec_t:s0
/etc/cron\.((daily)|(weekly)|(monthly))/freeradius regular file system_u:object_r:radiusd_exec_t:s0
/etc/cron\.d(/.*)? all files system_u:object_r:system_cron_spool_t:s0
/etc/cron\.daily/[sm]locate regular file system_u:object_r:locate_exec_t:s0
/etc/cron\.weekly/(c)?fingerd regular file system_u:object_r:fingerd_exec_t:s0
/etc all files system_u:object_r:etc_t:s0
/etc/crontab regular file system_u:object_r:system_cron_spool_t:s0
/etc/cron\.daily/prelink regular file system_u:object_r:prelink_cron_system_exec_t:s0
/etc/cron\.daily/calamaris regular file system_u:object_r:calamaris_exec_t:s0
/etc/cron\.daily/certwatch regular file system_u:object_r:certwatch_exec_t:s0
/etc/cron\.monthly/proftpd regular file system_u:object_r:ftpd_exec_t:s0


/etc/cron\.d(/.*)? all files system_u:object_r:system_cron_spool_t:s0 这一行,这也是为什么直接使用 vim 在 /etc/cron.d 下新建文件时,预设 SELinux type 是正确的。

练习:下面要建立一个 /srv/mycron 目录,默认也是需要变成 system_cron_spool_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 1. 先建立 mycron 目录,再放入配置文件,观察 SELinux type
[root@study ~]# mkdir /srv/mycron
[root@study ~]# cp /etc/cron.d/checktime /srv/mycron/
[root@study ~]# ll -dZ /srv/mycron/ /srv/mycron/checktime
drwxr-xr-x. root root unconfined_u:object_r:var_t:s0 /srv/mycron/
-rw-r--r--. root root unconfined_u:object_r:var_t:s0 /srv/mycron/checktime
# 发现变成了 var_t

# 2. 观察上层 /srv 的 SELinux type
[root@study ~]# semanage fcontext -l | grep '^/srv'
/srv/.* all files system_u:object_r:var_t:s0
/srv/([^/]*/)?www(/.*)? all files system_u:object_r:httpd_sys_content_t:s0
/srv/([^/]*/)?ftp(/.*)? all files system_u:object_r:public_content_t:s0
/srv/([^/]*/)?rsync(/.*)? all files system_u:object_r:public_content_t:s0
/srv/([^/]*/)?www/logs(/.*)? all files system_u:object_r:httpd_log_t:s0
/srv/node(/.*)? all files system_u:object_r:swift_data_t:s0
/srv/gallery2(/.*)? all files system_u:object_r:httpd_sys_content_t:s0
/srv/lib/gitosis(/.*)? all files system_u:object_r:gitosis_var_lib_t:s0
/srv/gallery2/smarty(/.*)? all files system_u:object_r:httpd_sys_rw_content_t:s0
/srv/loopback-device(/.*)? all files system_u:object_r:swift_data_t:s0
/srv all files system_u:object_r:var_t:s0
# 可以看到这里默认就是 var_t 类型的

# 3. 将 mycron 默认值改为 system_cron_spool_t
[root@study ~]# semanage fcontext -a -t system_cron_spool_t "/srv/mycron(/.*)?"
[root@study ~]# semanage fcontext -l | grep '^/srv/mycron'
/srv/mycron(/.*)? all files system_u:object_r:system_cron_spool_t:s0

# 4. 回复 /srv/mycron 以及子目录相关的 SELinux type
[root@study ~]# restorecon -Rv /srv/mycron/
restorecon reset /srv/mycron context unconfined_u:object_r:var_t:s0->unconfined_u:object_r:system_cron_spool_t:s0
restorecon reset /srv/mycron/checktime context unconfined_u:object_r:var_t:s0->unconfined_u:object_r:system_cron_spool_t:s0


通过这个例子来看,restorecon 的确是很方便 ,学会这些基础的工具,对于 SELinux 来说基本上也够用了


一个网络服务案例及登录文件协助

本章在 SELinux 小节中介绍到的各个指令,尤其是 setseboolchconrestorecon 等都是为了当你的某些网络服务无法正常提供相关功能时,才需要进行修改的一些指令动作。

可以通过主动检查的方式来检查是否有 SELinux 产生的错误。而不是等客户端联机失败来反馈

setroubleshoot:错误信息写入 /var/log/messages

几乎所有 SELinux 相关的程序都是以 se 开头,该服务时错误克服,启动后,会将关于 SELinux 的错误信息与克服方法记录到 /var/log/messages/var/log/setroubleshoot/*

需要安装:setroubleshootsetroubleshoot-server。原本 SELinux 信息是两个服务来记录的,分别是 auditdsetroubleshoot。在 CentOS 6.x 起整合成 auditd 了。所以安装好 setroubleshoot-server 后,需要重新启动 auditd 服务,否则 setroubleshoot 功能不会被启动

实际上。CentOS 7.x 对 setroubleshoot 的运行方式是:先由 auditd 去呼叫 audispd 服务,然后 audispd 服务启动 sedispatch 程序, sedispatch 再将原本的 auditd 信息转成 setroubleshoot 的信息,存储下来

1
2
3
4
[root@study ~]# rpm -qa | grep setroubleshoot
setroubleshoot-3.2.30-7.el7.x86_64
setroubleshoot-plugins-3.0.67-4.el7.noarch
setroubleshoot-server-3.2.30-7.el7.x86_64

在预设的情况下 setroubleshoot 被安装了,记得刚安装 setroubleshoot 的话,需要重新启动 auditd 服务的、

目前我们没有任何受限的网络服务主体进程在运行,下面使用一个简单的 FTP 服务器软件示例,来了解上面讲到的许多重点应用

实例说明:通过 vsftpd 这个 FTP 服务器来存取系统上的文件

CentOS 7.x 环境下, FTP 的默认服务器软件主要是 vsftpd

详细的 FTP 协议在服务器篇讲解,这里简单利用 vsftpdFTP 的协议来讲解 SELinux 的问题与错误克服。

下面只接受一些简单的 FTP 知识:客户端需要使用 FTP 账户登录 FTP 服务器,有一个称为「匿名 (anonymous)」的账户可以登录系统,但是这个匿名的账户登录后,只能存取一个特定的目录,而无法脱离该目录

vsftpd 中,一般用户与匿名者的家目录说明如下:

  • 匿名者:如果使用浏览器来联机到 FTP 服务器,那预设就是使用匿名者登录系统。匿名者的家目录默认是在 /var/ftp 中,同时,匿名者在家目录下只能下载数据,不能上传数据到 FTP 服务器,同时匿名者无法离开 FTP 服务器的 /var/ftp 目录
  • 一般 FTP 账户:在预设情况下,所有 UID 大于 1000 的账户,都可以使用 FTP 来登录系统,登录系统后,所有的账户都能够取得自己家目录下的文件数据,预设也可以上传、下载文件的

为了避免与之前章节的用户产生误解情况,创建一个名为 ftptest 的账户,且账户密码为 myftp123

1
2
3
4
[root@study ~]# useradd -s /sbin/nologin ftptest
[root@study ~]# echo "myftp123" | passwd --stdin ftptest
Changing password for user ftptest.
passwd: all authentication tokens updated successfully.

下面来安装 vsftp 服务器软件(还是在光盘中安装,前面挂载那样)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@study ~]# yum install /mnt/Packages/vsftpd-3*                    

[root@study ~]# systemctl start vsftpd # 启动 vsftpd 服务
[root@study ~]# systemctl enable vsftpd # 设置为开机启动
Created symlink from /etc/systemd/system/multi-user.target.wants/vsftpd.service to /usr/lib/systemd/system/vsftpd.service.
[root@study ~]# netstat -tlnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 1374/cupsd
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1578/master
tcp 0 0 127.0.0.1:6010 0.0.0.0:* LISTEN 2350/sshd: mrcode@p
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 1/systemd
tcp 0 0 192.168.122.1:53 0.0.0.0:* LISTEN 1975/dnsmasq
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1378/sshd
tcp6 0 0 ::1:631 :::* LISTEN 1374/cupsd
tcp6 0 0 ::1:25 :::* LISTEN 1578/master
tcp6 0 0 ::1:6010 :::* LISTEN 2350/sshd: mrcode@p
tcp6 0 0 :::111 :::* LISTEN 1/systemd
tcp6 0 0 :::21 :::* LISTEN 6656/vsftpd
tcp6 0 0 :::22 :::* LISTEN 1378/sshd

# 可以看到 6656/vsftpd 这行数据,代表已经启动了

匿名者无法下载的问题

模拟一些 FTP 的常用状态,假设将 /etc/securetty 以及主要的 /etc/sysctl.conf 放置给所有人下载,那么可以能会这样做

1
2
3
4
5
[root@study ~]# cp -a /etc/securetty /etc/sysctl.conf /var/ftp/pub
[root@study ~]# ll /var/ftp/pub/
total 8
-rw-------. 1 root root 221 Oct 31 2018 securetty
-rw-r--r--. 1 root root 449 Aug 9 2019 sysctl.conf

一般来说,默认要给用户下载的 FTP 文件会放在 /var/ftp/pub 目录中。下面使用简单的终端机浏览器 curl 来观察

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 1. 查看 FTP 根目录下有哪些内容
[root@study ~]# curl ftp://localhost
drwxr-xr-x 2 0 0 42 Mar 17 09:03 pub
# 确实看到了 pub 目录

# 2. 查看 pub 目录内的内容
[root@study ~]# curl ftp://localhost/pub
curl: (78) RETR response: 550
# 无法访问,是因为 pub 是一个目录需要后缀 / 结尾
[root@study ~]# curl ftp://localhost/pub/
-rw------- 1 0 0 221 Oct 30 2018 securetty
-rw-r--r-- 1 0 0 449 Aug 08 2019 sysctl.conf

# 3. 查看里面的文件内容
[root@study ~]# curl ftp://localhost/pub/sysctl.conf
# sysctl settings are defined through files in
# /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/.
#
# Vendors settings live in /usr/lib/sysctl.d/.
# To override a whole file, create a new file with the same in
# /etc/sysctl.d/ and put new settings there. To override
# only specific settings, add a file with a lexically later
# name in /etc/sysctl.d/ and put new settings there.
#
# For more information, see sysctl.conf(5) and sysctl.d(5).

# 上面不是错误信息,是哪个文件的内容

# 4. 继续查看下一个文件内容
[root@study ~]# curl ftp://localhost/pub/securetty
curl: (78) RETR response: 550
# 这里看不到了,但是 securetty 的确是一个文件而不是一个目录,基本原因应该是权限问题
# 因为 vsftpd 默认放在 /var/ftp/pub 内的资料,无论什么 SELinux type 几乎都可以被读取才对

# 5. 修正权限后,再观察一次 securetty 文件
[root@study ~]# ll /var/ftp/pub/
total 8
-rw-------. 1 root root 221 Oct 31 2018 securetty
-rw-r--r--. 1 root root 449 Aug 9 2019 sysctl.conf
# 可以看到 securetty 的其他人权限没有。改变成其他人也可以读取
[root@study ~]# chmod a+r /var/ftp/pub/securetty
[root@study ~]# curl ftp://localhost/pub/securetty
console
vc/1
vc/2
vc/3
# 此时已经能看到文件内容了

# 6. 修正 SELinux type 的内容(非必须)
[root@study ~]# restorecon -Rv /var/ftp/
restorecon reset /var/ftp/pub/securetty context system_u:object_r:etc_runtime_t:s0->system_u:object_r:public_content_t:s0
restorecon reset /var/ftp/pub/sysctl.conf context system_u:object_r:system_conf_t:s0->system_u:object_r:public_content_t:s0


上述列子告诉我们,要先从权限角度来检查,如果无法被读取 ,可能是因为没有 r 或则没有 rx 权限,并不一定是 SELinux 引起的。下面看看用一般账户登录

无法从家目录下载文件的问题分析与解决

由于通过一般账户,前面建立的 ftptest 账户登录的话,文字型的 FTP 客户端软件,默认会将用户引导在根目录,而不是家目录,因此,访问的 URL 需要更改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 0. 在 ftptest 家目录下创建一些数据
[root@study ~]# echo ~ftptest/
/home/ftptest/
[root@study ~]# echo "testing" > ~ftptest/test.txt
[root@study ~]# cp -a /etc/hosts /etc/sysctl.conf ~ftptest/
[root@study ~]# ll ~ftptest/
total 12
-rw-r--r--. 1 root root 158 Jun 7 2013 hosts
-rw-r--r--. 1 root root 449 Aug 9 2019 sysctl.conf
-rw-r--r--. 1 root root 8 Mar 17 17:23 test.txt

# 1. 一般账户直接登录 FTP 服务器,同时变换目录到家目录
[root@study ~]# curl ftp://ftptest:myftp123@localhost/~/
curl: (67) Access denied: 530 # 这里报错了
# 注意:书上在增加 ftptest 用户的时候,使用的是 /sbin/nologin,就无法访问 ftp,这里修改下,就可以了
[root@study ~]# usermod -s /bin/bash ftptest
[root@study ~]# curl ftp://ftptest:myftp123@localhost/~/
-rw-r--r-- 1 0 0 158 Jun 07 2013 hosts
-rw-r--r-- 1 0 0 449 Aug 08 2019 sysctl.conf
-rw-r--r-- 1 0 0 8 Mar 17 09:23 test.txt
# 看左边的权限也是没有问题的
# 从这里开始,笔者的实验和书上的结果对不上了,下面只记录书上的操作指令
# 就是因为上面修改用户的 bash 后,虽然可以访问了,但是下面的却可以下载文件,无法达到和书上的效果一样

# 2. 下载上面可以阅读的权限文件
[root@study ~]# curl ftp://ftptest:myftp123@localhost/~/test.txt
curl:(78) RETR response:550
# 无法访下载,是否是 SELinux 造成的?

# 3. 将 SELinux 从 Enforce 转成 Permissive
[root@study ~]# setenforce 0
[root@study ~]# curl ftp://ftptest:myftp123@localhost/~/test.txt
testing
[root@study ~]# setenforce 1 # 确定是 SELinux 权限问题后,改回来
# 需要该规则还是该 type?现在不知道
# 所以先查询下登录日志有没有相关的信息提供给我们处理

[root@study ~]# vim /var/log/messages
Aug 9 02:55:58 station3-39 setroubleshoot:SELinux is preventing /usr/sbin/vsftpd
from lock access on the file /home/ftptest/test.txt. For complete SELinux messages.
run sealert -l 3axxxxxxxx
# 之类的字样,关键词就是 sealert ,执行这条命令
[root@study ~]# sealert -l 3axxxxxxxx
SELinux is preventing /usr/sbin/vsftpd from lock access on the file /home/ftptest/test/txt.
# 下面说有 47.5% 的几率是由于这个原因所发生,并且可以使用 setsebool 去解决的意思
******* Plugin catchall_boolean(47.5 confidence) suggests ********

if you want to allow ftp to home dir
...
Do
setsebool -P ftp_home_dir 1

******* Plugin catchall(6.38confidence) suggests ********
DO
# grep vsftpd /var/log/audit/audit.log | audit2allow -M mypol
# semodule -i mypol.pp

# 下面就重要了,是整个问题发生的主要原因
Additional Information:
Source Context system_u:system_r:ftpd_t:s0-s0:c0.c1023
Target Context unconfined_u:object_r:user_home_t:s0
Target Objects /home/ftptest/test/txt [ file ]

通过上面的测试,知道主要的问题发生在 SElinuxtype 不是 vsftpd_t 所能读取的原因,上面 47.5 的概率问题,ftp_home_dirSELinux rules 的配置

1
2
3
4
5
6
7
8
# 1. 确认下 SELinux 的模式,并且无法访问
[root@study ~]# getenforce
Enforcing
[root@study ~]# curl ftp://ftptest:myftp123@localhost/~/test.txt
curl:(78) RETR response:550
[root@study ~]# setsebool -P ftp_home_dir 1
Boolean ftp_home_dir is not defined
# 可惜笔者这里提示没有被定义,与书上对不上啊

一般账户用户从非正规目录上传/下载文件

提供 /srv/gogogo 目录给 ftptest 用户使用,该如何处理?假设不考虑 SELiunx 的话,就是如下方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 1. 处理好所需要的目录数据
[root@study ~]# mkdir /srv/gogogo
[root@study ~]# chgrp ftptest /srv/gogogo/
# 把用户组改成 ftptest 这个组
[root@study ~]# ll -d /srv/gogogo/
drwxr-xr-x. 2 root ftptest 22 3月 17 22:43 /srv/gogogo/
[root@study ~]# echo "test" > /srv/gogogo/test.txt
[root@study ~]# curl ftp://ftptest:myftp123@localhost//srv/gogogo/test.txt
curl: (78) RETR response: 550
# 访问不了,查看日志
[root@study ~]# grep sealert /var/log/messages | tail
Mar 17 22:46:35 study setroubleshoot: SELinux is preventing /usr/sbin/vsftpd from read access on the file test.txt. For complete SELinux messages run: sealert -l 88f08c09-c510-4518-bbcc-58bcee06ffb0

[root@study ~]# sealert -l 88f08c09-c510-4518-bbcc-58bcee06ffb0
SELinux is preventing /usr/sbin/vsftpd from read access on the file test.txt.

# 虽然这个可信度很高,不过,因为会全部方向 FTP,所以不考虑
***** Plugin catchall_boolean (57.6 confidence) suggests ******************

If you want to allow ftpd to full access
Then you must tell SELinux about this by enabling the 'ftpd_full_access' boolean.

Do
setsebool -P ftpd_full_access 1

# 因为是非正规目录的使用,所以这边加上预设 SELinux type 恐怕能解决
***** Plugin catchall_labels (36.2 confidence) suggests *******************

If you want to allow vsftpd to have read access on the test.txt file
Then you need to change the label on test.txt
Do
# 下面这一条数据
# semanage fcontext -a -t FILE_TYPE 'test.txt'
.... 很多数据
Then execute:
restorecon -v 'test.txt' # 还有这一条数据,都是要参考的解决方案

***** Plugin catchall (7.64 confidence) suggests **************************

If you believe that vsftpd should be allowed read access on the test.txt file by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'vsftpd' --raw | audit2allow -M my-vsftpd
# semodule -i my-vsftpd.pp


Additional Information:
Source Context system_u:system_r:ftpd_t:s0-s0:c0.c1023
Target Context unconfined_u:object_r:var_t:s0
Target Objects test.txt [ file ]
Source vsftpd
Source Path /usr/sbin/vsftpd
Port <Unknown>
Host study.centos.mrcode
Source RPM Packages
Target RPM Packages
Policy RPM selinux-policy-3.13.1-252.el7.noarch
Selinux Enabled True
Policy Type targeted
Enforcing Mode Enforcing
Host Name study.centos.mrcode
Platform Linux study.centos.mrcode 3.10.0-1062.el7.x86_64
#1 SMP Wed Aug 7 18:08:02 UTC 2019 x86_64 x86_64
Alert Count 2
First Seen 2020-03-17 22:46:17 CST
Last Seen 2020-03-17 22:46:32 CST
Local ID 88f08c09-c510-4518-bbcc-58bcee06ffb0

Raw Audit Messages
type=AVC msg=audit(1584456392.386:979): avc: denied { read } for pid=10979 comm="vsftpd" name="test.txt" dev="dm-0" ino=35108539 scontext=system_u:system_r:ftpd_t:s0-s0:c0.c1023 tcontext=unconfined_u:object_r:var_t:s0 tclass=file permissive=0


Hash: vsftpd,ftpd_t,var_t,file,read

# 3. 查看 /var/ftp 的 SELinux type
[root@study ~]# ll -Zd /var/ftp/
drwxr-xr-x. root root system_u:object_r:public_content_t:s0 /var/ftp/
[root@study ~]# ll -Zd /srv/gogogo/
drwxr-xr-x. root ftptest unconfined_u:object_r:var_t:s0 /srv/gogogo/

# 4. 以 sealert 建议的方法来处理好 SELinux type
[root@study ~]# semanage fcontext -a -t public_content_t '/srv/gogogo(/.*)?'
[root@study ~]# restorecon -Rv /srv/gogogo
restorecon reset /srv/gogogo context unconfined_u:object_r:var_t:s0->unconfined_u:object_r:public_content_t:s0
restorecon reset /srv/gogogo/test.txt context unconfined_u:object_r:var_t:s0->unconfined_u:object_r:public_content_t:s0
# 再次访问就可以了
[root@study ~]# curl ftp://ftptest:myftp123@localhost//srv/gogogo/test.txt
test

在这个范例中,修改的是 type,前一个范例中修改的是 rule,不太一样的

无法变更 FTP 联机端口问题分析解决

比如你想要改变 FTP 默认的启动端口 21 改成 555,基本上,既然 SELinux 的主体进程大多是被受限的网络服务,很有可能连端口也限制了,下面尝试修改端口,来查看是怎么解决问题的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# 1. 先处理 vsftpd 的配置文件,加入 port 的端口参数
[root@study ~]# vim /etc/vsftpd/vsftpd.conf
listen_port=555

# 2. 重启服务,并查看日志
[root@study ~]# systemctl restart vsftpd
Job for vsftpd.service failed because the control process exited with error code. See "systemctl status vsftpd.service" and "journalctl -xe" for details.
[root@study ~]# grep sealert /var/log/messages
Mar 17 23:03:23 study setroubleshoot: SELinux is preventing /usr/sbin/vsftpd from name_bind access on the tcp_socket port 555. For complete SELinux messages run: sealert -l e3e3dee0-83eb-4cb8-b894-8be590fee082

[root@study ~]# sealert -l e3e3dee0-83eb-4cb8-b894-8be590fee082
SELinux is preventing /usr/sbin/vsftpd from name_bind access on the tcp_socket port 555.

# 这个 92.2 的概率,基本上就是这个了
***** Plugin bind_ports (92.2 confidence) suggests ************************

If you want to allow /usr/sbin/vsftpd to bind to network port 555
Then you need to modify the port type.
Do
# semanage port -a -t PORT_TYPE -p tcp 555
where PORT_TYPE is one of the following: certmaster_port_t, cluster_port_t, ephemeral_port_t, ftp_data_port_t, ftp_port_t, hadoop_datanode_port_t, hplip_port_t, isns_port_t, port_t, postgrey_port_t, unreserved_port_t.

***** Plugin catchall_boolean (7.83 confidence) suggests ******************

If you want to allow nis to enabled
Then you must tell SELinux about this by enabling the 'nis_enabled' boolean.

Do
setsebool -P nis_enabled 1

***** Plugin catchall (1.41 confidence) suggests **************************

If you believe that vsftpd should be allowed name_bind access on the port 555 tcp_socket by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'vsftpd' --raw | audit2allow -M my-vsftpd
# semodule -i my-vsftpd.pp


Additional Information:
Source Context system_u:system_r:ftpd_t:s0-s0:c0.c1023
Target Context system_u:object_r:hi_reserved_port_t:s0
Target Objects port 555 [ tcp_socket ]
Source vsftpd
Source Path /usr/sbin/vsftpd
Port 555
Host study.centos.mrcode
Source RPM Packages vsftpd-3.0.2-25.el7.x86_64
Target RPM Packages
Policy RPM selinux-policy-3.13.1-252.el7.noarch
Selinux Enabled True
Policy Type targeted
Enforcing Mode Enforcing
Host Name study.centos.mrcode
Platform Linux study.centos.mrcode 3.10.0-1062.el7.x86_64
#1 SMP Wed Aug 7 18:08:02 UTC 2019 x86_64 x86_64
Alert Count 1
First Seen 2020-03-17 23:03:20 CST
Last Seen 2020-03-17 23:03:20 CST
Local ID e3e3dee0-83eb-4cb8-b894-8be590fee082

Raw Audit Messages
type=AVC msg=audit(1584457400.225:1008): avc: denied { name_bind } for pid=11443 comm="vsftpd" src=555 scontext=system_u:system_r:ftpd_t:s0-s0:c0.c1023 tcontext=system_u:object_r:hi_reserved_port_t:s0 tclass=tcp_socket permissive=0


type=SYSCALL msg=audit(1584457400.225:1008): arch=x86_64 syscall=bind success=no exit=EACCES a0=4 a1=55e9e4d4e800 a2=1c a3=3 items=0 ppid=11440 pid=11443 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm=vsftpd exe=/usr/sbin/vsftpd subj=system_u:system_r:ftpd_t:s0-s0:c0.c1023 key=(null)

Hash: vsftpd,ftpd_t,hi_reserved_port_t,tcp_socket,name_bind

# 3. 根据建议解决执行指令, 92% 哪个指令下面 PORT_TYPE 下面又可选的 ftp_port_t
# 但是笔者还是懵逼的,不知道为什么那么多里面就选这个了
[root@study ~]# semanage port -a -t ftp_port_t -p tcp 555
[root@study ~]# systemctl restart vsftpd
[root@study ~]# netstat -tlnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 1374/cupsd
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1578/master
tcp 0 0 127.0.0.1:6010 0.0.0.0:* LISTEN 2350/sshd: mrcode@p
tcp 0 0 127.0.0.1:6011 0.0.0.0:* LISTEN 10579/sshd: root@pt
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 1/systemd
tcp 0 0 192.168.122.1:53 0.0.0.0:* LISTEN 1975/dnsmasq
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1378/sshd
tcp6 0 0 ::1:631 :::* LISTEN 1374/cupsd
tcp6 0 0 ::1:25 :::* LISTEN 1578/master
tcp6 0 0 ::1:6010 :::* LISTEN 2350/sshd: mrcode@p
tcp6 0 0 ::1:6011 :::* LISTEN 10579/sshd: root@pt
tcp6 0 0 :::555 :::* LISTEN 11573/vsftpd
tcp6 0 0 :::111 :::* LISTEN 1/systemd
tcp6 0 0 :::22 :::* LISTEN 1378/sshd
# 可以看到 vsftpd 的端口变成了 555 了

# 4. 实验看看该 port 是否可用
[root@study ~]# curl ftp://localhost:555
drwxr-xr-x 2 0 0 42 Mar 17 09:03 pub

基础信息

其实整个指令下达的方式很简单,你只要记得几个重要的概念就可以了。举例来说,你可以这样下达指令的:

1
2
[dmtsai@study ~]$ command [-options] parameter1 parameter2 
... 指令 选项 参数(1) 参数(2)

上述指令详细说明如下:

  1. 一行指令中第一个输入的部分绝对是“指令(command)”或“可执行文件案(例如批次 脚本,script)”

  2. command为指令的名称,例如变换工作目录的指令为cd等等;

  3. 中刮号[]并不存在于实际的指令中,而加入选项设置时,通常选项前会带-号,例如-h;

    有时候会使用选项的完整全名,则选项前带有 -- 符号,例如 –help;

  4. parameter1 parameter2..为依附在选项后面的参数,或者是command的参数;

  5. 指令,选项,参数等这几个咚咚中间以空格来区分,不论空几格shell都视为一格。所以空格是很重要的特殊字符!;

  6. 按下[Enter]按键后,该指令就立即执行。[Enter]按键代表着一行指令的开始启动。

  7. 指令太长的时候,可以使用反斜线(\)来跳脱[Enter]符号,使指令连续到下一行。注意!反斜线后就立刻接特殊字符,才能跳脱!

  8. 其他:
    i. 在 Linux 系统中,英文大小写字母是不一样的。举例来说, cdCD 并不同。

注意到上面的说明当中,“第一个被输入的数据绝对是指令或者是可执行的文件”! 这个是很 重要的概念喔!还有,按下[Enter]键表示要开始执行此一命令的意思。

基础指令的操作

下面我们立刻来操作几个简单的指令看看!

  • 显示日期与时间的指令: date
  • 显示日历的指令: cal
  • 简单好用的计算机: bc

重要的几个热键[Tab], [ctrl]-c, [ctrl]-d

[Tab]按键

在各种Unix-Like的 Shell当中, 这个[Tab]按键算是Linux的Bash shell最棒的功能之一了!他具有“命令补全”与“文件补齐”的功能喔!

[Ctrl]-c 按键

如果你在Linux下面输入了错误的指令或参数,有的时候这个指令或程序会在系统下面“跑不 停”这个时候怎么办?别担心, 如果你想让当前的程序“停掉”的话,可以输入:[Ctrl]与c按键 (先按着[Ctrl]不放,且再按下c按键,是组合按键), 那就是中断目前程序的按键啦!

[Ctrl]-d 按键

那么[Ctrl]-d是什么呢?就是[Ctrl]与d按键的组合啊!这个组合按键通常代表着: “键盘输入结 束(End Of File, EOF 或 End Of Input)”的意思! 另外,他也可以用来取代exit的输入呢!例 如你想要直接离开命令行,可以直接按下[Ctrl]-d就能够直接离开了(相当于输入exit啊!)。

[shift]+{[PageUP]|[Page Down]}按键

如果你在纯文本的画面中执行某些指令,这个指令的输出讯息相当长啊!所以导致前面的部 份已经不在目前的屏幕画面中, 所以你想要回头去瞧一瞧输出的讯息,那怎办?其实,你可 以使用 [Shift]+[Page Up] 来往前翻页,也能够使用 [Shift]+[Page Down] 来往后翻页! 这两个 组合键也是可以稍微记忆一下,在你要稍微往前翻画面时,相当有帮助!

Linux 的文件权限与目录配置

Linux一般将文件可存取的身份分为三个类 别,分别是 owner/group/others,且三种身份各有 read/write/execute 等权限。

Linux文件属性

嗯!既然要让你了解Linux的文件属性,那么有个重要的也是常用的指令就必须要先跟你说 啰!那一个?就是“ ls ”这一个察看文件的指令啰!在你以dmtsai登陆系统,然后使用 su - 切 换身份成为root后, 下达“ ls -al ”看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 [dmtsai@study ~]$ su - # 先来切换一下身份看看

Password:
Last login: Tue Jun 2 19:32:31 CST 2015 on tty2
[root@study ~]# ls -al
total 48
dr-xr-x---. 5 root root 4096 May 29 16:08 .
dr-xr-xr-x. 17 root root 4096 May 4 17:56 ..
-rw-------. 1 root root 1816 May 4 17:57 anaconda-ks.cfg
-rw-------. 1 root root 927 Jun 2 11:27 .bash_history
-rw-r--r--. 1 root root 18 Dec 29 2013 .bash_logout
-rw-r--r--. 1 root root 176 Dec 29 2013 .bash_profile
-rw-r--r--. 1 root root 176 Dec 29 2013 .bashrc
drwxr-xr-x. 3 root root 17 May 6 00:14 .config
drwx------. 3 root root 24 May 4 17:59 .dbus
-rw-r--r--. 1 root root 1864 May 4 18:01 initial-setup-ks.cfg
[1] [2] [3] [4] [5] [6] [7]
[权限] [链接] [拥有者] [群组] [文件大小][修改日期 [文件名]

文件属性的示意图

第一个字符代表这个文件是“目录、文件或链接文件等等”:

各个文件类型及其字符表示为:

表示字符 文件类型
d 目录
- 文件
l 符号链接等
b 可供储存的接口设备
c 串行端口设备,如键盘、鼠标等

接下来的字符中,以三个为一组,且均为 rwx 的三个参数的组合。其中, r 代表可读(read)、 w 代表可写(write)、 x 代表可执行(execute)。 要注意的是,这三个权限的位置不会改变,如果没有权限,就会出现减号 - 而已。

  • 第一组为“文件拥有者可具备的权限”,以“initial-setup-ks.cfg”那个文件为例, 该文件 的拥有者可以读写,但不可执行;
  • 第二组为“加入此群组之帐号的权限”;
  • 第三组为“非本人且没有加入本群组之其他帐号的权限”。

每个文件的属性由左边第一部分的 10 个字符来确定(如下图)。

从左至右用 0-9 这些数字来表示。

0 位确定文件类型,第 1-3 位确定属主(该文件的所有者)拥有该文件的权限。

第4-6位确定属组(所有者的同组用户)拥有该文件的权限,第7-9位确定其他用户拥有该文件的权限。

其中,第 1、4、7 位表示读权限,如果用 r 字符表示,则有读权限,如果用 - 字符表示,则没有读权限;

2、5、8 位表示写权限,如果用 w 字符表示,则有写权限,如果用 - 字符表示没有写权限;第 3、6、9 位表示可执行权限,如果用 x 字符表示,则有执行权限,如果用 - 字符表示,则没有执行权限。

第五栏为这个文件的容量大小,默认单位为Bytes;

第六栏为这个文件的创建日期或者是最近的修改日期:

例题:如果我的目录为下面的样式,请问testgroup这个群组的成员与其他人 (others)是否可以进入本目录?

1
drwxr-xr--   1 test1    testgroup    5238 Jun 19 10:25 groups/

答:

  • 文件拥有者test1[rwx]可以在本目录中进行任何工作;
  • 而testgroup这个群组[r-x]的帐号,例如test2, test3亦可以进入本目录进行工作,但是不能 在本目录下进行写入的动作;
  • 至于other的权限中[r–]虽然有r ,但是由于没有x的权限,因此others的使用者,并不能进 入此目录!

如何改变文件属性与权限

我们先介绍几个常用于群组、拥有者、各种身份的 权限之修改的指令,如下所示:

  • chgrp :改变文件所属群组
  • chown :改变文件拥有者
  • chmod :改变文件的权限, SUID, SGID, SBIT等等的特性

改变所属群组, chgrp

改变一个文件的群组真是很简单的,直接以chgrp来改变即可,咦!这个指令就是change group的缩写嘛!这样就很好记了吧! ^_^。不过,请记得,要被改变的群组名称必须要 在/etc/group文件内存在才行,否则就会显示错误!

假设你已经是root的身份了,那么在你的主文件夹内有一个名为 initial-setup-ks.cfg 的文件, 如何将该文件的群组改变一下呢?假设你已经知道在/etc/group里面已经存在一个名为users的 群组, 但是testing这个群组名字就不存在/etc/group当中了,此时改变群组成为users与 testing分别会有什么现象发生呢?

1
2
3
4
5
6
7
8
9
10
[root@study ~]# chgrp [-R] dirname/filename ...
选项与参数:
-R : 进行递回(recursive)的持续变更,亦即连同次目录下的所有文件、目录
都更新成为这个群组之意。常常用在变更某一目录内所有的文件之情况。
范例:
[root@study ~]# chgrp users initial-setup-ks.cfg
[root@study ~]# ls -l
-rw-r--r--. 1 root users 1864 May 4 18:01 initial-setup-ks.cfg
[root@study ~]# chgrp testing initial-setup-ks.cfg
chgrp: invalid group: `testing' <== 发生错误讯息啰~找不到这个群组名~

发现了吗?文件的群组被改成users了,但是要改成testing的时候, 就会发生错误~注意喔!

改变文件拥有者, chown

如何改变一个文件的拥有者呢?很简单呀!既然改变群组是change group,那么改变拥有者 就是change owner啰!BINGO!那就是chown这个指令的用途,要注意的是, 使用者必须是已经存在系统中的帐号,也就是在/etc/passwd 这个文件中有纪录的使用者名称才能改变。

chown的用途还蛮多的,他还可以顺便直接修改群组的名称呢!此外,如果要连目录下的所有次目录或文件同时更改文件拥有者的话,直接加上 -R 的选项即可!我们来看看语法与范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@study ~]# chown [-R] 帐号名称 文件或目录
[root@study ~]# chown [-R] 帐号名称:群组名称 文件或目录
选项与参数:
-R : 进行递回(recursive)的持续变更,亦即连同次目录下的所有文件都变更

范例:将 initial-setup-ks.cfg 的拥有者改为bin这个帐号:
[root@study ~]# chown bin initial-setup-ks.cfg
[root@study ~]# ls -l
-rw-r--r--. 1 bin users 1864 May 4 18:01 initial-setup-ks.cfg

范例:将 initial-setup-ks.cfg 的拥有者与群组改回为root:
[root@study ~]# chown root:root initial-setup-ks.cfg
[root@study ~]# ls -l
-rw-r--r--. 1 root root 1864 May 4 18:01 initial-setup-ks.cfg

改变文件权限, chmod

数字类型改变文件权限

我们可以用数字来代表各个权限,各个权限对应的数字为:

1
2
3
r:4
w:2
x:1

每种身份各自的三个权限分数是需要累加的,例如当权限为 rwxrwx--- 时,对应的分数为:

1
2
3
user   = rwx = 4+2+1 = 7
group = rwx = 4+2+1 = 7
others = --- = 0+0+0 = 0

得到的文件权限数字也就为 770, 修改权限时就可以使用这个数字完成:

1
2
3
$ chmod [-R] 770 文件或目录
参数
-R : 进行递回(recursive)的持续变更,亦即连同次目录下的所有文件都会变更

在实际的系统运行中 最常发生的一个问题就是,常常我们以vim编辑一个shell的文字批处理文件后,他的权限 通常是 -rw-rw-r– 也就是664, 如果要将该文件变成可可执行文件,并且不要让其他人修 改此一文件的话, 那么就需要-rwxr-xr-x这样的权限,此时就得要下达:“ chmod 755 test.sh ”的指令啰!

另外,如果有些文件你不希望被其他人看到,那么应该将文件的权限设置为例如:“-rwxr- —-”,那就下达“ chmod 740 filename ”吧!

符号类型改变文件权限

还有一个改变权限的方法呦!从之前的介绍中我们可以发现,基本上就九个权限分别是 (1)user (2)group (3)others三种身份啦!那么我们就可以借由u, g, o来代表三种 身份的权限!此外, a 则代表 all 亦即全部的身份!那么读写的权限就可以写成r, w, x

啰!也就是可以使用下面的方式来看:

1
| chmod | u g o a | +(加入) -(除去) =(设置) | r w x | 文件或目录 |

符号类型改变文件权限需要遵循一定的语法规则,分别需要了解的有 身份表示符, 操作表示符权限表示符.

身份表示符:

表示符 代表的身份
u 文件的拥有者
g 文件的拥有者所在用户组
o 其他人
a 所有用户

操作表示符:

表示符 代表的操作
+ 添加权限
- 去除权限
= 设定权限

权限表示符 就是 r, wx.

来实作一下吧!假如我们要“设置”一个文件的权限成为“-rwxr-xr-x”时,基本上就是:

  • user (u):具有可读、可写、可执行的权限;
  • group 与 others (g/o):具有可读与执行的权限。 所以就是
1
2
[root@study ~]# chmod u=rwx,go=rx .bashrc
# 注意喔!那个 u=rwx,go=rx 是连在一起的,中间并没有任何空白字符!

那么假如是“ -rwxr-xr– ”这样的权限呢?可以使用“ chmod u=rwx,g=rx,o=r filename ”来设 置。此外,如果我不知道原先的文件属性,而我只想要增加.bashrc这个文件的每个人均可写入的权限, 那么我就可以使用:

1
2
3
4
5
[root@study ~]# ls -al .bashrc
-rwxr-xr-x. 1 root root 176 Dec 29 2013 .bashrc
[root@study ~]# chmod a+w .bashrc
[root@study ~]# ls -al .bashrc
-rwxrwxrwx. 1 root root 176 Dec 29 2013 .bashrc

比如说下面的这条指令让拥有者具有所有权限,而为用户组和其他人添加执行权限:

1
chmod u=rwx,go+x .vimrc

需要注意的是: u=rwx,go+x 之间没有空格。

目录与文件之权限意义

权限对文件的重要性

文件是实际含有数据的地方,包括一般文本文件、数据库内容档、二进制可可执行文件 (binary program)等等。 因此,权限对于文件来说,他的意义是这样的:

  • r (read):可读取此一文件的实际内容,如读取文本文件的文字内容等;
  • w (write):可以编辑、新增或者是修改该文件的内容(但不含删除该文件);
  • x (eXecute):该文件具有可以被系统执行的权限。

那个可读(r)代表读取文件内容是还好了解,那么可执行(x)呢?这里你就必须要小心啦! 因为在Windows下面一个文件是否具有执行的能力是借由“ 扩展名 ”来判断的, 例如:.exe, .bat, .com 等等,但是在Linux下面,我们的文件是否能被执行,则是借由是否具有“x”这个权 限来决定的!跟文件名是没有绝对的关系的!

至于最后一个w这个权限呢?当你对一个文件具有w权限时,你可以具有写入/编辑/新增/修改 文件的内容的权限, 但并不具备有删除该文件本身的权限!对于文件的rwx来说, 主要都是 针对“文件的内容”而言,与文件文件名的存在与否没有关系喔!因为文件记录的是实际的数据嘛!

权限对目录的重要性

如果是针对目录时,那个 r, w, x 对目录是什么意 义呢?

  • r (read contents in directory):

    表示具有读取目录结构清单的权限,所以当你具有读取(r)一个目录的权限时,表示你 可以查询该目录下的文件名数据。 所以你就可以利用 ls 这个指令将该目录的内容列表显 示出来!

  • w (modify contents of directory):
    这个可写入的权限对目录来说,是很了不起的! 因为他表示你具有异动该目录结构清单 的权限,也就是下面这些权限:

    • 创建新的文件与目录;
    • 删除已经存在的文件与目录(不论该文件的权限为何!) 将已存在的文件或目录进行更名;
    • 搬移该目录内的文件、目录位置。 总之,目录的w权限就与该目录下面的文件名异 动有关就对了啦!
  • x (access directory):

    咦!目录的执行权限有啥用途啊?目录只是记录文件名而已,总不能拿来执行吧?没 错!目录不可以被执行,目录的x代表的是使用者能否进入该目录成为工作目录的用途! 所谓的工作目录(work directory)就是你目前所在的目录啦!举例来说,当你登陆Linux

同一个权限对于 文件目录 来说,含义是不一样的,这里来了解一下。

权限 文件 目录
r 可以读取文件内容 可以读取目录结构列表
w 可以编辑修改文件内容 可以改动目录结构列表
x 可以被系统执行 用户可以进入目录 (cd)

这里需要注意的一个权限是: 可以改动目录结构列表, 这意味着可以:

  • 建立新的文件与目录
  • 删除已经存在的文件与目录
  • 将已存在的文件或目录进行更名
  • 搬移该目录内的文件、目录位置

所以 w 这个权限还是慎重使用好了。

Linux目录配置

由于 linux distribution 太多,所以有了 FHSFilesystem Hierarchy Standard)标准。

该标准主要目的是:让使用者可以了解到已安装软件通常放置于哪个目录下, FHS 的重点在于每个特定的目录下应该要放上面样子的数据。

FHS 是根据过去的经验一直在持续的改版,依据文件系统使用的频繁与是否允许使用者随意更动, 而将目录定义成为四种交互作用的形态。

- 可分享的(shareable) 不可分享的(unshareable)
不变得(static) /usr(软件放置处) /etc (配置文件)
- /opt(第三方软件) /boot (开机与核心)
可变动的(variable) /var/mail (使用者邮箱) /var/run (程序相关)
- /var/spool/news (新闻组) /var/lock (程序相关)

上表中是一些代表性的目录,而下面放置的数据后面会讲到,这里主要了解什么是那四个类型?

  • 可分享的:可以分享给其他系统挂载使用的目录,所以包括可执行文件与使用者的邮件 等数据, 是能够分享给网络上其他主机挂载用的目录;
  • 不可分享的:自己机器上面运行的设备文件或者是与程序有关的socket文件等, 由于仅 与自身机器有关,所以当然就不适合分享给其他主机了。
  • 不变的:有些数据是不会经常变动的,跟随着distribution而不变动。 例如函数库、文件说明文档、系统管理员所管理的主机服务配置文件等等;
  • 可变动的:经常改变的数据,例如登录文件、一般用户可自行收受的新闻群组等。

事实上 FHS 针对目录树架构仅定义出三层目录下应该放置什么数据:

  • / : root 根目录,与开机系统有关
  • /usr :unix software resource 与软件安装/执行有关
  • /var:variable 与系统运作过程有关

根目录 / 的意义与内容

根目录是整个系统最重要的一个目录,因为不但所有的目录都是由根目录衍生出来的,同时 根目录也与开机/还原/系统修复等动作有关。 由于系统开机时需要特定的开机软件、核心文 件、开机所需程序、函数库等等文件数据,若系统出现错误时,根目录也必须要包含有能够 修复文件系统的程序才行。 因为根目录是这么的重要,所以在FHS的要求方面,他希望根目 录不要放在非常大的分区内, 因为越大的分区你会放入越多的数据,如此一来根目录所在分 区就可能会有较多发生错误的机会。

因此 FHS 标准建议是:根目录所在分区槽应该越小越好,且应用程序所安装的软件最好不要与根目录放在同一个分区槽内, 报纸根目录越小越好。如此不但效能较佳,根目录所在的文件系统也较不容易发生问题

因此 FHS 定义出根目录下应该要有以下目录存在,即使没有实体目录,也希望至少有连接文件存在。

第一部分:FHS 要求必须要存在的目录

  • /bin

    系统有很多放置执行文件的目录,单 /bin 比较特殊。 因为放置的是在单人维护模式下还能够被操作的指令。

    /bin 下的指令可以被 root 与一般账户所使用,主要有 cat、chmod、chown、date、mv、mkdir、cp、bash 等常用命令

  • /boot

    主要放置开机会使用到的文件,包括 linux 核心文件以及开机选单与开机锁需配置文件等。

    Linux kernel 常用额文件名为 vmlinuz ,如果使用 grub2 开机管理程序,则还会存在 /boot/grub2 这个目录

  • /dev

    任何装置与接口设备都是以文件形态存在这个目录当中。只要透过存取这个目录下的某个文件, 就等于存取某个装置,比较重要的文件有 /dev/null、/dev/zero、/dev/tty、/dev/loop、/dev/sd

  • /etc

    系统主要的配置文件几乎都放在这个目录中,例如人员的账户密码文件、各种服务的启动文件等, 一般来说,这个目录下的各文件属性是可以让一般使用者查阅的,但是只有 root 有权利修改。 FHS 建议不要放置可执行文件 (binary) 在这个目录中。

    比较重要的有 /etc/modprobe.d、/etc/passwd、/etc/fstab、/etc/issue 等。

    另外 FHS 还规范几个重要的目录页最好咋 /etc 目录下:

    • /etc/opt/ :必要,放置第三方协力软件 /opt 的相关配置文件
    • /etc/xqq/ :建议,与 x window 有关的各种配置文件,尤其是 xorg.conf 这 x server 的配置文件
    • /etc/sgml :建议,与 SGML 格式有管的各项配置文件
    • /etc/xm :建议,与 XML 格式有关的各项配置文件
  • /lib

    系统的函式库非常的多,而 lib 下放的是在 开机时会用到的函数库,以及在 /bin 和 /sbin 下的指令会呼叫的函数库。

    另外 FHS 还要求 /lib/modules 目录存在,主要放可抽换式的核心先关模块(驱动程序)

  • /media

    放的是可移除的设备,例如 软盘、光盘、 DVD 等都暂时挂载于此。

    常见的有 /media/floppy、/media/cdrom 等

  • /mnt

    如果暂时挂载某些额外的设备,一般建议可以放到这个目录中,在很早的时候该目录用途与 /mnt 相同, 只是有了 /media 后,这个目录就用来暂时挂载用了

  • opt

    放第三方协力软件的目录。比如 KDE 这个桌面管理系统是一个独立的计划,不过他可以安装到 linux 系统中, 因此 KDE 就建议放置到该目录下了。

    如果你想要自行安装额外的软件(非原本 distribution 提供的),那么也建议放这里, 不过,以前的 linux 系统中,还是习惯放在 /usr/local 目录下

  • run

    早期的 FHS 规定系统开机后所产生的各项信息应该放置到 /var/run 目录下, 新版的则规范到 /run 目录下了,由于 /run 可以用来内存仿真,因此效能上会好很多

  • /sbin

    Linux 有非常多的指令是用来设置系统环境的,这些指令只有 root才能够利用来设置系统, 其他用户只能用来「查询」。放在 /sbin 下的为开机过程中所需要的,包括了开机、修复、还原系统所需要的指令。

    至于某些服务器软件程序,一般放置到 /usr/sbin 中。 至于本机自行安装的软件产生的系统执行文件(system binary)则放到 /usr/local/sbin 中了。

    常见的指令包括:fdisk、fsck、ifconfig、mkfs 等

  • /srv

    src 可以视为 「service」的缩写,是一些网络服务启动之后,这些服务所需要取用的数据目录。 常见的服务如 www、ftp 等。例如:www 服务器需要的网页资源就可以放在 /srv/www 里面。

    不过,系统的服务数据如果尚未要提供给英特网上任何人浏览的话,预设还是建议放在 /var/lib 下

  • /tmp

    一般用户或则是正在执行的程序暂时放文件的地方。该目录是任何人都可以存取的,所以需要定期清理一下。 因此 FHS 甚至建议在开机时,应该删除该目录下的文件

  • /usr:属于第二层 FHS 规范,后续介绍

  • /var:属于第二层 FHS 规范,主要放置变动性的数据,后续介绍

第二部分:FHS 建议可以存在的目录

  • /home

    系统默认的用户目录。在你新增一个一般使用者账户时,默认的用户家目录都会规范到这里来。 比较重要的是,家的木有两种代号:

    • ~:代表目前这个用户的家目录
    • ~mrcode:则代表 mrcode 的家目录
  • /lib<qual> 用来存放于 /lib 不同的格式的二进制函数库,例如支持 64 位的 /lib64 函数库

  • /root

    系统管理员 root 的家目录。之所以放这里,是因为如果进入单人维护模式而仅挂载根目录时,该目录就能够拥有 root 的家目录, 所以会希望 root 的家目录与根目录放同一个分区槽中

事实上 FHS 针对目录所定义的标准就仅有上面的规范,不过还有其他的目录一需要了解下, 也是 linux 当中几个非常重要的目录:

  • /lost+found

    这个目录使用标准的 ext2/3/4 文件系统格式才会产生的一个目录,目的是当文件系统发生错误时, 将一些遗失的片段放到这个目录下。

    不过如果使用的是 xfs 文件系统的话,就不会存在这个目录了

  • /proc

    这个目录本身是一个「虚拟文件系统(virtual filesystem),放的数据都在内存当中, 例如系统核心、进程信息(process)、周边装置的状态以及网络状态等。

    因为这个目录下的数据都是内存当中,使用本身不占任何硬盘空间。比较重要的文件:

    1
    2
    3
    4
    5
    6
    7
    /proc
    cpuinfo
    dma
    interrupts
    ioports
    net/*

  • /sys

    与 proc 非常类似,也是一个虚拟的文件系统,主要也是记录核心与系统硬件信息较相关的信息。 包括目前已加载的核心模块与核心侦测到的硬件装置信息等。同样不占用硬盘容量

/usr 的意义与内容

根据 FHS 的基本定义, /usr 里面放置的数据属于可以分享的与不可变动的, 如果你知道如何透过网络进行分区槽的挂载(例如在服务器篇会谈到的 NFS 服务器), 那么 /usr 确实可以分享给局域网内的其他主机来使用

/usr 不是 user 的缩写,而是 Unix Software Resource 的缩写(Unix 操作系统软件资源), FHS 建议所有软件开发者,应该将他们的数据合理的分辨放置到这个目录下的次目录,而不要自行建立该软件自己独立的目录。

因为所有系统默认的软件(distribution 发布者提供的软件)都会放置到 /usr 下, 因此该目录类似 windows 「c:/windows 和 c:/Program files」这两个目录的综合体。

一般来说 /usr 的此目录建议有以下:

第一部分:FHS 要求必须要存在的目录

  • /usr/bin/

    所有一般用户能够使用的指令都放在这里。 CentOS7 新版已经将全部的用户指令放在这里, 而使用连接文件的方式将 /bin 连接到这里。也就是说 /usr/bin 与 /bin 是一样的了。 而且 FHS 要求在此目录下不应该有子目录

  • /usr/lib/

    基本上 与 /lib 功能相同,使用 /lib 就是连接到此目录的

  • /usr/local/

    系统管理员在本机自行安装自己下载的软件(非 distribution 默认提供),建议安装到此目录。 比如,distribution 提供的软件较旧,想安装新的但是又不想移除旧版本的,就可以将新版安装到这里。

    该目录下也是具有 bin、etc、include、lib 的次目录

  • /usr/sbin

    非系统正常运作所需要的系统指令。最长久的就是某些网络服务器软件的指令(daemon)。 不过功能基本与 /sbin 差不多,因此 /sbin 也是连接到此目录的

  • /usr/share/

    主要放置只读架构的数据文件和共享文件。在该目录下的数据几乎是不分硬件架构均可读取的数据, 因为几乎上都是文本文件。常见的还有以下次目录

    • /usr/share/man:联机帮助文件
    • /usr/share/doc:软件杂项的文件说明
    • /usr/share/zoneinfo 与时区有关的时区文件

第二部分:FHS 建议可以存在的目录

  • /usr/games/:与游戏比较相关的数据

  • /usr/include

    c/c++ 等程序语言的档头(header)与包含档(include)放置处,当我们以 tarball 方式 (tar.gz 的方式安装软件)安装某些数据时,会使用到里头的许多包含档

  • /usr/libexe

    某些不被一般使用者惯用的执行档或脚本,例如大部分的 x 窗口下的操作指令

  • /usr/lib<qual>

    /lib<qual> 功能相同,连接过来的

  • /usr/src

    一般源码建议放这里,src 有 source 的意思。 至于核心源码则建议放到 /usr/src/linux 目录下

/var 的意义与内容

如果 /usr 是安装时会占用较大硬盘容量的目录,那么 /var 则是在运行后才会渐渐占用容量的。 主要放置的是针对常态性变动的文件,包括 cache、登录文件(log file)以及某些软件所产生的文件, 包括程序文件(lock file,run file),或则例如 mysql 数据库的文件等, 常见的目录有

第一部分:FHS 要求必须要存在的目录

  • /var/cache:应用程序运行中使用的缓存文件

  • /var/lib

    程序本身执行过程中,需要用到的数据文件存放处。在此目录下各自的软件应该要有各自的目录, 比如:mysql 数据库放到 /var/lib/mysql 而 rpm 的数据库则放到 /var/lib/rpm

  • /var/lock

    某些装置或是文件资源一次只能被一个程序使用,所以这里存放的是加锁的标识, 目前此目录已经挪到 /run/lock 中了

  • /var/mail:个人电子邮件信箱目录,不过也被放置到了 /var/spool/mail 中了,通常两个目录互为连接文件

  • /var/run

    某些程序或则是服务启动后,会将他们的 PID 放置在这个目录下,与 /run 相同,也连接到 /run 下了。 至于 PID 后续讲解

  • /var/spool

    通常放置一些对了数据,这些数据被使用后通常都会被删除。 比如:系统受到新信会放到 /var/spool/mail 中,但使用者手下该信件后该封信原则上就会被删除。 信件如果展示寄不出去,则会放到 /var/spool/mqueue 中。等待被送出后会被删除。

    如果是工作排程数据(crontab)就会被放到 /var/spool/cron 目录中

建议在读完整个基础篇之后,可以挑战 FHS 官网英文文件,会让你对于 linux 操作系统的目录有更深入的了解

针对 FHS 各家 distribution 的异同,与 CentOS 7 的变化

由于 FHS 仅是定义出上层 / 与次层 /var 的目录内容应该放置的文件或目录, 其他的就由开发者自行配置了。

如: CentOS 网络设置数据放在 /etc/sysconfig/network-scripts 下。 但是 SuSE 的则放在 /etc/sysconfig/netwok 目录下,所以名称不一致,但是记住大致的 FHS 标准,差异性其实不大

centOS7 相对于老版做了改进,将许多原本应该要在 / 目录中的数据全部挪到 /usr 里面去,然后进行连接设置。 包括以下这些:

  • /bin -> /usr/bin
  • /sbin -> /usr/sbin
  • /lib -> /usr/lib
  • /lib64 -> /usr/lib64
  • /var/lock -> /run/lock
  • /var/run -> /run

目录树(directory tree)

在Linux下面,所有的文件与目录都是由根目录开始的!那是所有目录与文件的源头~ 然后再一个一个的分支下来,有点像是树枝状啊~因此,我们也称这种目录配置方式为:“目录树(directory tree)” 。主要特性如下:

  • 目录树的起始点为根目录 /
  • 每个目录可以使用本地端的分区(partition)文件系统,也可以使用网络上的文件系统。举例来说,就是可以利用 Network File System(NFS)服务器挂载某些特定的目录
  • 每一个文件在此目录树种的文件名(包含完整路径)都是独一无二的

可以使用命令 ls -l / 来查看根目录下又哪些文件与数据。 下图将较为重要的文件数据列出来,那么目录树架构如下图这样。

学习了这么多,那么现在回去看看安装前 主机规划与磁盘划分,对于当初如何要这样划分, 现在你就明白了。

根据 FHS 的定义,最好能将 /var 独立出来,因为当 /var 死掉时,你的根目录还会活着,还可以进入救援模式。

绝对路径与相对路径

文件名与路径的写法分为:

  • 绝对路径:由根目录开始写起的文件或目录,例如 /home/mrcode/.bashrc
  • 相对路径:开头不是 / 则是相对路径,例如: ./home/mrcode

对于 . 的概念:

  • .:代表当前目录,也可以使用 ./ 来表示
  • ..:代表上一层目录,也可以使用 ../ 来表示

CentOS 的观察,linux 版本查询

除了第一章中谈到的 Linux distribution 的差异性,除了 FHS 之外,还有个 Linux Standard Base(LSB) 的标准是可以依循的。

可以简单的使用 ls 来查看 FHS 规范的目录是否正确的存在你的 Linux 系统中, 那么 支持 LSB 标准的 distribution 在 https://www.linuxbase.org/lsb-cert/productdir.php?by_lsb 中被列出

如果想要知道确切的核心与 LSB 所需求的几种重要的标准的话,就需要例如 unamelsb_release 等指令来查询了。

lsb_release 软件不是默认安装软件了,因此需要先安装。

但是这里,新安装的机器居然不能连接外网,可以与宿主机通网了。那么这里无法安装,只能先记录命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 检查 linux 核心与操作系统的位版本
# 查看核心版本
[mrcode@study ~]$ uname -r
3.10.0-1062.el7.x86_64
# 查看操作系统位版本
[mrcode@study ~]$ uname -m
x86_64

# 如果可以联网的话,可以安装 lsb_release 指令(使用 root 身份)
yum install redhat-lsb
# 安装完成之后,使用指令
lsb_release -a

# 就会显示 LSB Version 等版本信息,如下类似的信息
Distributor ID:CentOS
Description : CentOS linux release 7.0(Core)
Release : 7.0

Linux 文件与目录管理

本章进一步操作与管理文件及目录,包括在不同的目录间变换、建立、删除目录, 建立与删除文件、查找文件、查阅问价内容等,都会在这个章节进行简单介绍。

相对路径与绝对路径

  • 绝对路径:由根目录开头,如 /home/mrcode
  • 相对路径:不是由根目录开头的,如 ./mrcode

目录的相关操作

以下的特殊目录需要着重了掌握

  • .:代表此层目录
  • ..:上一层目录
  • -:前一个工作目录
  • ~:目前用户身份坐在的家目录
  • ~account:表示 account 这个用户的家目录(account 是个账户名称)

需要特别注意的是,在所有目录下都会看到两个目录 . 与 ..,当前目录和上一层目录。

那么 linux 中,根目录有没有上层目录存在?

1
2
3
4
[mrcode@study ~]$ ls -al /
# 可以看到这两个目录的属性一模一样,所以这两个目录其实都是同一个目录
dr-xr-xr-x. 17 root root 224 Oct 4 18:31 .
dr-xr-xr-x. 17 root root 224 Oct 4 18:31 ..

那么下面讲解下几个常见的处理目录的指令:

  • cd:变换目录
  • pwd:显示当前目录
  • mkdir:建立一个新的目录
  • rmdir:删除一个空的目录

cd(change directory)变换目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[dmtsai@study ~]$ su - # 先切换身份成为 root 看看!
[root@study ~]# cd [相对路径或绝对路径]
# 最重要的就是目录的绝对路径与相对路径,还有一些特殊目录的符号啰!
[root@study ~]# cd ~dmtsai
# 代表去到 dmtsai 这个使用者的主文件夹,亦即 /home/dmtsai
[root@study dmtsai]# cd ~
# 表示回到自己的主文件夹,亦即是 /root 这个目录
[root@study ~]# cd
# 没有加上任何路径,也还是代表回到自己主文件夹的意思喔!
[root@study ~]# cd ..
# 表示去到目前的上层目录,亦即是 /root 的上层目录的意思;
[root@study /]# cd -
# 表示回到刚刚的那个目录,也就是 /root 啰~
[root@study ~]# cd /var/spool/mail
# 这个就是绝对路径的写法!直接指定要去的完整路径名称!
[root@study mail]# cd ../postfix
# 这个是相对路径的写法,我们由/var/spool/mail 去到/var/spool/postfix 就这样写!

cd是Change Directory的缩写,这是用来变换工作目录的指令。注意,目录名称与cd指令之间 存在一个空格。 一登陆Linux系统后,每个帐号都会在自己帐号的主文件夹中。那回到上一层 目录可以用“ cd .. ”。 利用相对路径的写法必须要确认你目前的路径才能正确的去到想要去的目录。例如上表当中最后一个例子, 你必须要确认你是在/var/spool/mail当中,并且知道 在/var/spool当中有个mqueue的目录才行啊~ 这样才能使用cd ../postfix 去到正确的目录说, 否则就要直接输入cd /var/spool/postfix 啰~

pwd(print Working Directory) 显示当前所在目录

1
2
3
4
5
6
7
8
[mrcode@study mail]$ pwd
/var/mail
# 带参数 P 是显示真实的路径,而不是连接(link)路径,然而 /var/mail 就是一个连接路径
[mrcode@study mail]$ pwd -P
/var/spool/mail
# 通过命令也能看到,连接到了 spool/mail 目录中
[mrcode@study mail]$ ls -ld /var/mail
lrwxrwxrwx. 1 root root 10 Oct 4 18:21 /var/mail -> spool/mail

pwd是Print Working Directory的缩写,也就是显示目前所在目录的指令。

mkdir 建立新目录

语法如下

1
2
3
4
mkdir [-mp] 目录名称

-m:配置文件案的权限,直接设定,不需要看预设权限(umask)的脸色
-p:将该路径上所有的目录都创建出来(当然不存在的话)

练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 进入临时目录
[mrcode@study ~]$ cd /tmp/
[mrcode@study tmp]$ mkdir test
# 不带参数 -p 不能创建多级目录
[mrcode@study tmp]$ mkdir test1/test2/test3/test4
mkdir: cannot create directory ‘test1/test2/test3/test4’: No such file or directory
[mrcode@study tmp]$ mkdir -p test1/test2/test3/test4

# 创建目录时直接配置该目录的权限
[mrcode@study tmp]$ mkdir -m 711 test2
[mrcode@study tmp]$ ls -ld test*
# 这些是创建目录默认的权限
drwxrwxr-x. 2 mrcode mrcode 6 Oct 11 04:32 test
drwxrwxr-x. 3 mrcode mrcode 19 Oct 11 04:33 test1
# 这个是创建目录时直接配置的权限
drwx--x--x. 2 mrcode mrcode 6 Oct 11 04:35 test2

rmdir 删除空的目录

语法如下

1
2
3
4
rmdir [-p] 目录名称

-p:「上层」的「空目录」也一起删除

练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[mrcode@study tmp]$ ls -ld test*
drwxrwxr-x. 2 mrcode mrcode 6 Oct 11 04:32 test
drwxrwxr-x. 3 mrcode mrcode 19 Oct 11 04:33 test1
drwx--x--x. 2 mrcode mrcode 6 Oct 11 04:35 test2
# 该目录下无数据,可直接删除
[mrcode@study tmp]$ rmdir test
# 该目录下由多个目录,就无法阐述了,会报错
[mrcode@study tmp]$ rmdir test1
rmdir: failed to remove ‘test1’: Directory not empty
# 这里使用 -p 删除最后一个目录,但是当再次查看的时候,test4 的上层空的目录都不在了
[mrcode@study tmp]$ rmdir -p test1/test2/test3/test4/
[mrcode@study tmp]$ ls -ld test*
drwx--x--x. 2 mrcode mrcode 6 Oct 11 04:35 test2

# -p 删除上级空目录是什么意思,下面再来体验下

# 创建了多个目录
[mrcode@study tmp]$ mkdir -p test1/test2/test3/test4
# 然后在 test1 中创建了一个 txt 文件
[mrcode@study tmp]$ touch test1/txt
# 删除的时候,就报错了,无法删除 test1,因为该目录下有 txt 文件
# 但是注意,只是 test1 没有被删除, test2、test3、test4 还是被删除了的
[mrcode@study tmp]$ rmdir -p test1/test2/test3/test4/
rmdir: failed to remove directory ‘test1’: Directory not empty

但是如果想把该目录下所有的东西都删除呢?你可以使用指令 rm -r test1 就能全部删掉了, 相对来说,rmdir 没有这么危险

关于执行文件路径的变量:$PATH

前面讲解 FHS 后,我们知道 ls 指令完整文件名为 /bin/ls(这是绝对路径), 那么为什么我们可以在任何地方执行 /bin/ls 这个指令呢?这是因为换了变量 PATH 的能力

当我们执行一个指令的时候,系统会按照 PATH 的设定去每个 PATH 定义的目录下搜索对应的可执行文件 (比如 ls),如果在 PATH 定义的目录中含有多个文件名为 ls 的可执行文件,那么先搜索到的被执行

1
2
3
4
5
6
7
8
# 打印变量的信息,使用 echo ,「$」表示接一个变量
[mrcode@study tmp]$ echo $PATH
/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/mrcode/.local/bin:/home/mrcode/bin

# 注意,每个账户的 path 值也是不一样的
[root@study ~]# echo $PATH
/usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin

PATH(一定是大写)这个变量的内容是由一堆目录所组成的,每个目录中间用冒号(:)来隔 开, 每个目录是有“顺序”之分的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 先把 ls 移动到 /bin 目录之外去,再运行 ls 看能不能运行?
[root@study ~]# mv /bin/ls ../
[root@study ~]# ls
bash: ls: 未找到命令...
相似命令是: 'lz'
# 现在已经报错找不到命令了,是因为 / 并不再 PATH 变量中

# 但是可以通过路径来运行
[root@study ~]# /l
lib/ lib64/ ls # ls 的确被移动到 根目录下了
# 这里直接通过绝对路径运行指令
[root@study ~]# /ls
anaconda-ks.cfg initial-setup-ks.cfg initial-setup-ks-mrcode.cfg

# 要想不用绝对/相对路径也能使用 ls ,那么将 根目录加入到 PATH 中即可
# 也可以使用 PATH="${PATH}:/" 来配置
[root@study ~]# PATH="$PATH:/"
[root@study ~]# ls
anaconda-ks.cfg initial-setup-ks.cfg initial-setup-ks-mrcode.cfg

# 把 ls 移回原来的目录
[root@study ~]# mv /ls /bin/
# 可能会出现找不到指令了,没有关系,可能是因为指令参数被快取得关系
# 只要 exit 再登入 su - 就可以使用了
# 另外说一句,刚刚在命令行中把根目录添加到 PATH 中,不是永久的,退出后,再登录就失效了
[root@study ~]# ls
-bash: /ls: 没有那个文件或目录

# 假设 /usr/local/bin/ls 与 /bin/ls 两个指令,哪个先被执行?
# 可以使用 echo $PATH 或则 ${PATH} 直接显示某一个变量
[root@study ~]# ${PATH}
# 这里看哪一个目录在最前面,就是哪个目录下的 ls 先执行
-bash: /usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin: 没有那个文件或目录
[root@study ~]# PATH
bash: PATH: 未找到命令...

为什么不建议把 . 当前目录添加到 PATH 路径中?这其实是为了安全起见,不建议添加到 PATH 中, 比如在 /tmp 目录下,因为是大家都可以写的,有人搞破坏,写了一个 ls 的指令,但是里面写的是删除文件的, 这样就会先收到这个恶意的命令

由上面的示例,我们可以知道几件事情:

  • 不同身份使用者预设的 PATH 不同,默认能够随意执行的指令也不同(如 root 与 mrcode)
  • PATH 是可以修改的
  • 使用绝对路径或相对路径直接指定某个指令文件名来执行,会比搜寻 PATH 来的正确
  • 指令应该要放置到正确的目录下,执行才比较方便
  • 当前目录「.」建议不要放到 PATH 中

文件与目录管理

文件与目录的检视: ls

1
2
3
ls [-aAdfFhilnrRSt] 文件名或目录名称
ls [--color={never,auto,always}] 文件名或目录名称
ls [--full-time] 文件名或目录名称

选项与参数:

  • a:全部的文件,连同隐藏文件(开头为 .)一起列出来(常用)

  • A:全部的文件,连同隐藏文件(不包括 . 和 .. 这两个目录)

  • d:仅列出目录本身,而不是列出目录内的文件数据(常用)

  • f:直接列出结果,而不进行排序(ls 默认以文档名排序)

  • F:根据文件、目录等信息,给予附加数据结构

    如:

    • * 代表可执行文件,
    • / 代表目录
    • = 代表 socket 文件
    • | 代表 FIFO 文件
  • h:将文件容量以人类较易读的方式(例如 GB、KB)列出来

  • i:列出 inode 号码,inode 的意义后续讲解

  • l:长数据串输出,包含文件的属性与权限等数据(常用)

  • n:列出 UID 与 GID 而非使用者与群组的名称(UID 与 GID 会在账户管理中讲解)

  • r:将排序结果反向输出,例如原本文件名由小到大,反向则由大到小

  • R:连同子目录内容一起列出来,等于该目录下的所有文件都会显示出来

  • S:按文件容量大小排序

  • t:按时间排序

  • color 颜色配置

    • never:不要依据文件特性给予颜色显示
    • always:显示颜色
    • auto:让系统自行依据设置来判断是否给予颜色
  • full-time:以完整时间模式)包含年月日时分输出

  • time={atime,ctime}:输出 access 时间或改变权限属性时间(ctime),而非内容变更时间

在 linux 中 ls 指令可能是 最常用的,由于文件所记录的信息实在是太多了, 所以默认显示的只有:非隐藏文档、以文件名进行排序、文件名代表的颜色显示

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 将家目录下的所有问价列出来,包含属性与隐藏文件
[mrcode@study ~]$ ls -al ~
total 40
drwx------. 18 mrcode mrcode 4096 Oct 8 23:15 . # 深蓝色
drwxr-xr-x. 4 root root 42 Oct 8 23:01 .. # 深蓝色
-rw-------. 1 mrcode mrcode 2927 Oct 11 05:16 .bash_history
-rw-r--r--. 1 mrcode mrcode 18 Aug 8 20:06 .bash_logout
-rw-r--r--. 1 mrcode mrcode 193 Aug 8 20:06 .bash_profile
-rw-r--r--. 1 mrcode mrcode 231 Aug 8 20:06 .bashrc
drwx------. 16 mrcode mrcode 4096 Oct 4 21:33 .cache # 深蓝色

# 接上题,不显示颜色,但在文件名末尾显示该文件名代表的类型
# 实际的终端中看,颜色就没显示了
[mrcode@study ~]$ ls -alF --color=never ~
total 40
drwx------. 18 mrcode mrcode 4096 Oct 8 23:15 ./
drwxr-xr-x. 4 root root 42 Oct 8 23:01 ../
-rw-------. 1 mrcode mrcode 2927 Oct 11 05:16 .bash_history
-rw-r--r--. 1 mrcode mrcode 18 Aug 8 20:06 .bash_logout
-rw-r--r--. 1 mrcode mrcode 193 Aug 8 20:06 .bash_profile
-rw-r--r--. 1 mrcode mrcode 231 Aug 8 20:06 .bashrc
drwx------. 16 mrcode mrcode 4096 Oct 4 21:33 .cache/

# 显示完整的修改实践(modification time)
[mrcode@study ~]$ ls -al --full-time ~
total 40
drwx------. 18 mrcode mrcode 4096 2019-10-08 23:15:44.109000000 +0800 .
drwxr-xr-x. 4 root root 42 2019-10-08 23:01:04.516000000 +0800 ..
-rw-------. 1 mrcode mrcode 2927 2019-10-11 05:16:27.662000000 +0800 .bash_history
-rw-r--r--. 1 mrcode mrcode 18 2019-08-08 20:06:55.000000000 +0800 .bash_logout
-rw-r--r--. 1 mrcode mrcode 193 2019-08-08 20:06:55.000000000 +0800 .bash_profile
-rw-r--r--. 1 mrcode mrcode 231 2019-08-08 20:06:55.000000000 +0800 .bashrc
drwx------. 16 mrcode mrcode 4096 2019-10-04 21:33:12.075000000 +0800 .cache

可以看到 ls 支持的功能很多,这些都是因为 linux 文件系统记录了很多有用的信息的缘故, 那么这些与权限、属性有关的数据放在 i-node 里面的。后续会深入讲解 i-node 的

另外,由于 ls -l 使用频率很高,为此,很多 distribution 在预设情况中已经将 ll 设定为 ls -l 的意思了。其实,那个功能是 Bash shell 的 alias 功能

复制、删除与移动:cp、rm、mv

  • cpcopy 复制文件,该指令还有其他功能,如建立连接档、比较亮文件的新旧而给予更新,复制整个目录等功能
  • mvmove 移动目录与文件,也可以直接拿来当做更名(rename)
  • rmremove 移除文件

cp 复制文件或目录

1
2
cp [-adfilprsu] 来源文件(source)目标文件(destination)
cp [options] source1 source2 source3 .... directory

选项与参数:

  • a:相当于 -dr –preserve=all 的意思,至于 dr 请参考下列说明;(常用)
  • d:若来源文件为链接文件的属性(link file),则复制链接文件属性而非文件本身
  • f:强制(force)的意思,若目标文件已经存在且无法开启,则移除后再尝试一次
  • i:若目标文件已经存在时,在覆盖时会先询问动作的进行。(常用)
  • l:进行硬式链接(hard link)的链接档的建立,而非复制文件本身
  • p:连同文件的属性(权限、用户、时间)一起复制过去,而非使用默认属性;(备份文件常用)
  • r:递归持续复制,用于目录的复制行为。(常用)
  • s:复制称为符号链接文件(symbolic link)
  • u:destination 与 source 旧才更新 destination,或 destination 不存在的情况下才复制

--preserve=all:除了 -p 的权限相关参数外,还加入 SELinux 的属性,linksxattr 等也复制

最后需要注意的是:如果来源档有两个以上,则最后一个目的文件一定要是目录才行

而且不同身份者执行这个指令会有不同的结果产生,尤其是 -a、**-p** 的选项,对于不同身份来说, 差异则非常的大。

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 使用 root 身份,将家目录下的 .bashrc 复制到 /tmp 下,并更名为 bashrc
[root@study ~]# cp ~/.bashrc /tmp/bashrc
# 加上 -i 属性,由于上面已经复制过一次了,所以 bashrc 文件已经存在
[root@study ~]# cp -i ~/.bashrc /tmp/bashrc
cp:是否覆盖"/tmp/bashrc"# n 不覆盖,y 覆盖

# 变换目录到 /tmp ,并将 /var/log/wtmp 复制到 /tmp 且观察属性
[root@study tmp]# cd /tmp/
[root@study tmp]# cp /var/log/wtmp .
# ls 可以列出多个文档名,这里列出了两个,刚好可以对比他们的属性
[root@study tmp]# ls -l /var/log/wtmp wtmp
-rw-rw-r--. 1 root utmp 44160 10月 13 14:38 /var/log/wtmp
-rw-r--r--. 1 root root 44160 10月 13 15:42 wtmp
# 可以看到上面不加任何选项,被复制之后的某些属性或权限已经被改变了
# 这是个很重要的特性,要注意,文件建立的时间也不一样
# 下面将所有的属性权限都一起复制过来
[root@study tmp]# cp -a /var/log/wtmp wtmp2
[root@study tmp]# ls -l /var/log/wtmp wtmp2
-rw-rw-r--. 1 root utmp 44160 10月 13 14:38 /var/log/wtmp
-rw-rw-r--. 1 root utmp 44160 10月 13 14:38 wtmp2

上面示例中,不加任何选项会使用预设的配置,比如常常会复制别人的数据(当然需要有 read 权限), 总是希望复制到的数据最后是我们自己的,所以上面示例才有由 utmp 变更为 root

由于具有这个特性,因此在进行备份的时候,需要特别注意的特殊权限文件,例如密码文件(/etc/shadow) 以及一些配置文件,就不能直接以 cp 来复制,需要将全部的属性都原样复制过来

1
2
3
4
5
6
7
# 复制 etc 目录下的所有内容
[root@study tmp]# cp /etc/ /tmp/
cp: 略过目录"/etc/" # 提示该目录不能直接复制,要加上 -r
[root@study tmp]# cp -r /etc/ /tmp/
# 再次强调,-r 只是能递归复制,但是文件权限等属性还是会更改
# 因此可以使用 cp -a /etc/ /tmp/ 来复制,尤其是在备份的情况下

创建符号链接与实体链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 将之前复制过来的 bashrc 建立一个链接档
[root@study tmp]# pwd
/tmp
# 先查看该文件的属性
[root@study tmp]# ls -l bashrc
-rw-r--r--. 1 root root 176 10月 13 15:38 bashrc
# 分别建立 符号链接 和硬式链接
[root@study tmp]# cp -s bashrc bashrc_link
[root@study tmp]# cp -l bashrc bashrc_hlink
[root@study tmp]# ls -l bashrc*
# 注意看这里的数值,源文件是 1 这里变成了 2
-rw-r--r--. 2 root root 176 10月 13 15:38 bashrc
-rw-r--r--. 2 root root 176 10月 13 15:38 bashrc_hlink
# 下面这条数据,在终端中,bashrc_link 会显示浅蓝色
lrwxrwxrwx. 1 root root 6 10月 13 15:55 bashrc_link -> bashrc

使用 -l-s 都会建立连接档(link file),那么有什么不同呢?

  • bashrc_link:使用 s 创建出来的,是符号链接(symbolic link),简单说是一个快捷方式,会链接到 bashrc 中去。有一个 -> 的符号
  • bashrc_hlink:使用 l 创建出来的,是实体链接(hard link)

实体链接与源文件的属性与权限一模一样,与尚未链接前的差异是 第二栏 的 link 由 1 变成了 2. 由于实体链接与 i-node 有关,这里先不深入了。

备份常见下的复制

当源文件比目标新的时候才复制

1
2
3
4
5
6
7
8
9
10
11
# 先查看两个文件的时间,可以看到源文件是 2013 年,比目标文件旧
[root@study tmp]# ls -l ~/.bashrc /tmp/bashrc
-rw-r--r--. 1 root root 176 12月 29 2013 /root/.bashrc
-rw-r--r--. 2 root root 176 10月 13 15:38 /tmp/bashrc
# 这里使用 -u 复制后,没有任何提示
[root@study tmp]# cp -u ~/.bashrc /tmp/bashrc
# 再次查看,发现没有复制成功,当前时间是 16:14 了,如果成功,目标文件的时间也会变更
[root@study tmp]# ls -l ~/.bashrc /tmp/bashrc
-rw-r--r--. 1 root root 176 12月 29 2013 /root/.bashrc
-rw-r--r--. 2 root root 176 10月 13 15:38 /tmp/bashrc

连接文档的复制

1
2
3
4
5
6
7
8
9
10
11
12
13
# 该文件是一个符号链接文件
[root@study tmp]# ls -l bashrc_link
lrwxrwxrwx. 1 root root 6 10月 13 15:55 bashrc_link -> bashrc
# 这里使用不加参数复制和加参数复制
[root@study tmp]# cp bashrc_link bashrc_link_1
[root@study tmp]# cp -d bashrc_link bashrc_link_2
[root@study tmp]# ls -l bashrc bashrc_link*
-rw-r--r--. 2 root root 176 10月 13 15:38 bashrc
lrwxrwxrwx. 1 root root 6 10月 13 15:55 bashrc_link -> bashrc
# 可以看到,不加参数复制把源文件复制过来了
-rw-r--r--. 1 root root 176 10月 13 16:16 bashrc_link_1
# 添加 -d 参数,只复制了链接文件本身
lrwxrwxrwx. 1 root root 6 10月 13 16:16 bashrc_link_2 -> bashrc

多个文件同时复制到通一个目录下

1
2
cp ~/.bashrc ~/.bash_history /tmp/

身份不同执行 cp 指令表现不同

1
2
3
4
5
6
# 使用 mrcode 身份, -a 把文件原原本本的复制过来
[mrcode@study ~]$ cp -a /var/log/wtmp /tmp/mrcode_wtmp
[mrcode@study ~]$ ls -l /var/log/wtmp /tmp/mrcode_wtmp
-rw-rw-r--. 1 mrcode mrcode 44160 Oct 13 14:38 /tmp/mrcode_wtmp
-rw-rw-r--. 1 root utmp 44160 Oct 13 14:38 /var/log/wtmp

rm (移除文件或目录)

  • f:force 强制的意思,忽略不存在的文件,不会出现警告信息
  • i:互动模式,在闪出去会询问使用者是否操作
  • r:递归删除

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 互动模式删除
[root@study ~]# cd /tmp/
[root@study tmp]# rm -i bashrc
rm:是否删除普通文件 "bashrc"?y
# 删除的文件名还可以使用通配符, * 表示 0 个或多个
[root@study tmp]# rm -i bashrc*
rm:是否删除普通文件 "bashrc_hlink"?y
rm:是否删除符号链接 "bashrc_link"?y
rm:是否删除普通文件 "bashrc_link_1"?y
rm:是否删除符号链接 "bashrc_link_2"?y

# 删除一个目录, rmdir 无法删除非空目录
[root@study tmp]# rmdir /tmp/etc/
rmdir: 删除 "/tmp/etc/" 失败: 目录非空
# 这里使用 r 参数递归删除
[root@study tmp]# rm -r /tmp/etc/
# 但是出现了交互模式,是因为 root 身份预设加入了参数 -i
rm:是否进入目录"/tmp/etc/"? y
rm:是否删除普通文件 "/tmp/etc/fstab"?y
rm:是否删除普通空文件 "/tmp/etc/crypttab"?y
rm:是否删除符号链接 "/tmp/etc/mtab"?y
rm:是否删除普通文件 "/tmp/etc/resolv.conf"?y
rm:是否进入目录"/tmp/etc/fonts"? ^C

# 在指令前添加反斜杠,可以忽略掉 alias 的指定选项,至于 alias 后续再 bash 章节详讲
\rm -r /tmp/etc/

# 删除一个带有 - 开头的文件
# 先使用 touch 建立一个空文件
touch ./-aaa
# 注意:在 /tmp 下文件太多,常见好的文件就在最前面
ls -l
# 看到文件大小是 0 ,这是一个空文件
-rw-r--r--. 1 root root 0 10月 13 19:05 -aaa-

# 删除刚才创建的,肯定不会成功的,之前讲解过 - 是个特殊字符,表示选项
[root@study tmp]# rm -aaa-
rm:无效选项 -- a
Try 'rm ./-aaa-' to remove the file "-aaa-". # 但是这里给出了建议,添加双引号删除
Try 'rm --help' for more information.

[root@study tmp]# rm "./-aaa-"
rm:是否删除普通空文件 "./-aaa-"?y


由于 root 的是天神,所以大部分 distribution 都默认添加了 -i 的选项,删除前请三思啊。

mv 移动文件与目录或更名

1
2
mv [-fiu]  source destination
mv [options] source1 source2 ... directory
  • f:强制,如果目标文件已经存在,不会询问,直接覆盖
  • i:若目标文件已经存在时,就会询问是否覆盖
  • u:若目标已经存在,且 source 比较新,才会功更新该文件
1
2
3
4
5
6
7
8
9
10
11
[root@study tmp]# cd /tmp/   
[root@study tmp]# cp ~/.bashrc bashrc
# 创建目录
[root@study tmp]# mkdir mvtest
# 将刚刚拷贝的 bashrc 复制到目录中
[root@study tmp]# mv bashrc mvtest/
# 目录更名
# 其实还有一个指令 rename,该指令专职进行多个文档名同时更名,并非针对单一文件更名
# 与 mv 不同,详细请 man rename
[root@study tmp]# mv mvtest/ mvtest2

取得路径的文件名与目录名称,basename、dirname

每个文件的完整文档名包含了前面的目录与最终的文件名,而每个文档名的长度都可达 255 个字符, 那么怎么区分哪个是文件名?哪个是目录名?可以使用斜线「/」来分辨

一般要获取文件名或目录名称,都是些程序的手来判断用,所以这部分指令可以用在后续的 shell scripts 里面。

1
2
3
4
5
6
# /etc/sysconfig/network 比如这个路径
# 可以使用指令分别获取到他的目录与文件名
[root@study tmp]# basename /etc/sysconfig/network
network
[root@study tmp]# dirname /etc/sysconfig/network
/etc/sysconfig

文件内容查阅

查阅一个文件内容是,这里有相当多有趣的指令来了解下, 最常使用的可以说是 catmoreless,那么当查阅一个很大型的文件的时候, 想要在几百兆的文件内容中找到我们想要的数据怎么办?下面的指令能发挥出一些作用

  • cat:由第一行开始显示文件内容
  • tac:从最后一行开始显示,可以看出 taccat 的倒着写
  • nl:显示的时候顺道输出行号
  • more:一页一页的显示文件内容
  • less:与 more 类似,但是比 more 更好的是,他可以往前翻页
  • head 只看头几行

直接检视文件内容

直接查阅一个文件的内容可以使用 cattacnl 这几个指令

cat(concatenate)

1
2
cat [-AbEnTv]

  • A:相当于 -vET 的整合选项,可列出一些特殊字符而不是空白
  • b:列出行号,仅针对非空白行做行号显示,空白行不标行号
  • E:将结尾的断行字符 $ 显示出来
  • n:打印出行号(包含空白行)
  • T:将 tab 按键以 ^I 显示出来
  • v:列出一些看不出来的特殊字符

实践练习

1
2
3
4
5
6
7
8
9
10
[root@study tmp]# cat /etc/issue
\S
Kernel \r on an \m

# 带行号显示,最后还有一行空白行呢。对于大文件要找某个特定的行时,有点用处
[root@study tmp]# cat -n /etc/issue
1 \S
2 Kernel \r on an \m
3

下面练习显示特殊的内容

1
2
3
4
5
6
7
8
9
10
11
[root@study tmp]# cat -A /etc/man_db.conf
#^I^I*MANPATH* ->^I*CATPATH*$
#$
MANDB_MAP^I/usr/man^I^I/var/cache/man/fsstnd$
MANDB_MAP^I/usr/share/man^I^I/var/cache/man$

# 上面只是部分内容,说下差异
# 断行以 $ 显示,可以发现每行后面都有 $ ,这个其实就 window 中的换行把?
# tab 以 ^I 显示
# windows 的断行字符是 ^M$
# 这部分在 vim 软件介绍时会再次说明

tac 反向列示

1
2
3
4
5
6
# 从最后一行开始显示
[root@study tmp]# tac /etc/issue

Kernel \r on an \m
\S

nl 添加行号打印

1
2
nl [-bnw] 文件

  • b:指定行号指定的方式,主要有两种
    • -b a:表示不论是否为空行,也同样列出行号(类似 cat -n)
    • -b t:如果有空行,空行不要列出行号(默认值)
  • n:列出行号表示的方法,主要有三种
    • -b ln:行号在屏幕的最左方显示
    • -b rn:行号在自己字段的最右方显示,且不加 0
    • -b rz:行号在自己字段的最有方显示,且加 0
  • w:行号字段的占用字符数

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 用 nl 列出 /etc/issue 的内容
# 默认不显示空行的行号
[root@study tmp]# nl /etc/issue
1 \S
2 Kernel \r on an \m

# 显示空行行号
[root@study tmp]# nl -b a /etc/issue
1 \S
2 Kernel \r on an \m
3
# 行号自动补 0,前面说的左右,看下面的对比,这个右是指,行号区域的左右
[root@study tmp]# nl -b a -n rz /etc/issue
000001 \S
000002 Kernel \r on an \m
000003
[root@study tmp]# nl -b a -n rn /etc/issue
1 \S
2 Kernel \r on an \m
3
[root@study tmp]# nl -b a -n ln /etc/issue
1 \S
2 Kernel \r on an \m
3

可翻页检视

more 一页一页翻动

1
2
3
4
5
6
7
8
[root@study tmp]# more /etc/man_db.conf
#
#
# This file is used by the man-db package to configure the man and cat paths.
# It is also used to provide a manpath for those without one by examining
# their PATH environment variable. For details see the manpath(5) man page.
#
--More--(14%) # 重点在这一行,你的光标也会在这里等待你的指令

more 程序中,有几个按键可以按:

  • 空格键(space):向下翻一页
  • Enter:向下翻一行
  • /字符串:在显示的内容中,向下搜索「字符串」这个关键词
  • q:立即离开 more
  • bctrl+b:向前翻页,只针对文件有用,对管线(管道 |)无用

less 一页一页翻动

1
2
3
4
5
6
7
# 使用指令后,就会进入到 less 环境
less /etc/man_db.conf

# This file is used by the man-db package to configure the man and cat paths.
# It is also used to provide a manpath for those without one by examining

注意,刚进入的时候,没有光标,可以直接输入 : 或则方向下键,就会在最下面出现 「:光标」这里就可以输入指令了

可以使用的按键和指令有

  • 空格键:向下翻一页
  • pagedown:向下翻一页
  • pageup:向上翻一页
  • /字符串:向下搜索字符串;注意这个斜杠也是需要输入的,不是在 「:」输入,:也和这个是一个功能
  • ?字符串:向上搜索字符串
  • n:重复前一个搜索(与 / 或 ?有关)
  • N:反向的重复前一个搜索
  • g:前进到这个资料的第一行
  • G:前进到这个资料的最后一行去(注意是大写)
  • q:离开 less 这个程序

此外,man page 就是调用 less 来显示说明文件内容的,所以看上去很相似

笔者工作中查看日志中有用得数据的时候,就是这个 less 了,但是只知道 shift+g 可以前进到最后一行去,原来 shift+g 其实就是输入了大写的 G 指令

资料摘取

可以将输出的资料做一个最简单的摘取,如去除文件前面几行(head)或则后面几行(tail), 需要注意的是, headtail 都是以行为单位来进行摘取的

head 取出前面几行

1
2
3
4
head [-n number] 文件

-n:后面接数字,表示摘取几行

1
2
3
4
5
6
7
8
9
10
11
12
# 默认显示前 10 行,可以指定显示 20 行
head -n 20 /etc/man_db.conf

# 注意后面的数值为负数
# 该文件共有 131 行,这里是的意思就是,从尾部 -128 行,剩下的内容显示
# 也就是说,忽略显示后 128 行的数据
[root@study tmp]# head -n -128 /etc/man_db.conf
#
#
# This file is used by the man-db package to configure the man and cat paths.


tail 取出后面几行

1
2
3
4
5
tail [-nf number] 文件

-n :后面接数字,表示显示几行
-f :表示持续侦测后面所接的档名,要等到按下 ctrl+c 才会结束 tail 的侦测

1
2
3
4
5
6
7
8
9
10
11
# 默认显示最后 10 行
tail /etc/man_db.conf
# 显示最后 20 行
tail -n 20 /etc/man_db.conf
# 忽略显示前 100 行的数据,也就是说显示 100 行后的数据
tail -n +100 /etc/man_db.conf


# 这个就是笔者最常用查看某个项目当前滚动日志的方式了
tail -f /var/log/messages

组合使用示例

1
2
3
4
5
6
# 获取 第 11 到 20 行的数据
# 思路是:先取前 20 行数据出来,再从这 20 行里面取后 10 行数据
[root@study tmp]# head -n 20 /etc/man_db.conf | tail -n 10

# 这个 | 就是管线的意思

|:管线/管道符,前面的指令所输出的信息,请透过管线交由后续的指令继续使用。后续会详细讲解

上面的例子,其实我也不知道到底取出来的行数对不对,那么就可以使用管线来组合其他的指令使用

1
2
3
4
5
6
7
# 先使用 cat -n 显示行号,再交给后续的指令
# 我这里是显示 第 18 行到 20 行的内容
[root@study tmp]# cat -n /etc/man_db.conf | head -n 20 | tail -n 3
18 #MANDATORY_MANPATH /usr/src/pvm3/man
19 #
20 MANDATORY_MANPATH /usr/man

非纯文本 od

上面讲解了读取出文本的内容,那么想阅读非文本文件呢?比如查看 /usr/bin/passwd 文档, 使用上面提出来的指令读取就会乱码。

可以使用 od 指令来读取

1
2
od [-t TYPE] 文件

type 选项为:

  • a:利用默认的字符来输出
  • c:使用 ASCII 字符来输出
  • d[size]:十进制(decimal)输出数据,每个整数占用 size bytes
  • f[size]:浮点数(floating)输出数据
  • o[size]:八进制(octal)
  • x[size]:十六进制(hexadecimal)

实践练习

使用 ASCII 展示

1
2
3
4
5
6
7
8
9
10
[root@study ~]# od -t c /usr/bin/passwd
0000000 177 E L F 002 001 001 \0 \0 \0 \0 \0 \0 \0 \0 \0
0000020 003 \0 > \0 001 \0 \0 \0 H 2 \0 \0 \0 \0 \0 \0
0000040 @ \0 \0 \0 \0 \0 \0 \0 220 e \0 \0 \0 \0 \0 \0
0000060 \0 \0 \0 \0 @ \0 8 \0 \t \0 @ \0 035 \0 034 \0
0000100 006 \0 \0 \0 005 \0 \0 \0 @ \0 \0 \0 \0 \0 \0 \0

# 最左边第一栏以 8 进制来表示 bytes 数。
# 比如 00000020 表示是第16 个 bytes (2x8)

使用 8 进制位列出存储值与 ASCII 的对照表

1
2
3
4
5
6
7
8
9
[root@study ~]# od -t oCc /etc/issue
0000000 134 123 012 113 145 162 156 145 154 040 134 162 040 157 156 040
\ S \n K e r n e l \ r o n
0000020 141 156 040 134 155 012 012
a n \ m \n \
0000027

# 上面是八进制表示,下面是对应的 ascii 字符

对照指令对于工程师来说可能更有用处,上面是文件是一个纯文本文件,显示了字符的 ACCIS 对照表, 百度了下, ACCIS 可以与上面的各种进制来对照

比如 password 字符串,需要他的 10 进制对照表

1
2
3
4
5
6
# 可以使用管道符来给 od 处理
[root@study ~]# echo password | od -t dCc
0000000 112 97 115 115 119 111 114 100 10
p a s s w o r d \n
0000011

修改文件时间或新建文件 touch

使用 ls 指令的时候,提到过每个文件 linux 底下都会记录许多的时间参数,其实是有三个主要的变动时间:

  • modification timemtime

    当文档 内容数据 变更时。该时间会被更新。

  • status timectime

    当文件 状态 改变时。比如权限与数学被更改了

  • access timeatime

    当文件 内容被取用 时。比如我们使用 cat 去读取 /etc/man_db.conf ,该时间就会改变

1
2
3
4
5
6
7
8
[root@study ~]# date;ls -l /etc/man_db.conf ;ls -l --time=atime /etc/man_db.conf ;ls -l --time=ctime /etc/man_db.conf
2019年 10月 13日 星期日 21:33:52 CST
-rw-r--r--. 1 root root 5171 10月 31 2018 /etc/man_db.conf # 2018/10/31 建立的 mtime
-rw-r--r--. 1 root root 5171 10月 13 15:36 /etc/man_db.conf # 10月13号 读取过 atime
-rw-r--r--. 1 root root 5171 10月 4 18:22 /etc/man_db.conf # 10月4号 更新过状态 ctime

# 笔者就现在使用了 cat /etc/man_db.conf,也没有发现时间变更,不知道是啥原因

当你看到一个未来时间的文件,这个是有可能的,因为支持多时区,安装系统行为不当,就有可能导致这种情况发生

可以使用 touch 来修订时间

1
touch [-acdmt] 文件
  • a:仅修订 access time
  • c:仅修改文件的时间,若该文件不存在则不建立新文件
  • d:后面可以接欲修订的日期而不用目前的日期,也可以使用 –date=”日期或时间”
  • m:仅修改 mtime
  • t:后面可以接欲修订的时间而不用目前的时间,格式为 YYYYMMDDhhmm

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[mrcode@study ~]$ cd /tmp/
[mrcode@study tmp]$ touch testtouch
[mrcode@study tmp]$ ls -l testtouch
-rw-rw-r--. 1 mrcode mrcode 0 Oct 13 21:45 testtouch

# 注意到这个文件的大小是 0,在预设的状态下,如果 touch 没有接文件
# 则该文件的三个时间(atime、ctime、mtime 都会更新为目前的时间。
# 若该文件不存在,则会主动建立一个新的空文件


# 复制一个文件,假设复制全部的属性,并检查日期
[mrcode@study tmp]$ cp -a ~/.bashrc bashrc
[mrcode@study tmp]$ date; ll bashrc ; ll --time=atime bashrc ; ll --time=ctime bashrc
Sun Oct 13 21:48:24 CST 2019
-rw-r--r--. 1 mrcode mrcode 231 Aug 8 20:06 bashrc # mtime
-rw-r--r--. 1 mrcode mrcode 231 Oct 13 14:38 bashrc # atime
-rw-r--r--. 1 mrcode mrcode 231 Oct 13 21:47 bashrc # ctime
# 属性完全被复制,mtime 与源文件相同,该文件是刚刚建立的, ctime 就是当前时间

# 可以将日期调整为两天前
[mrcode@study tmp]$ touch -d "2 days ago" bashrc
[mrcode@study tmp]$ date; ll bashrc ; ll --time=atime bashrc ; ll --time=ctime bashrc
Sun Oct 13 21:51:31 CST 2019
-rw-r--r--. 1 mrcode mrcode 231 Oct 11 21:51 bashrc # mtime
-rw-r--r--. 1 mrcode mrcode 231 Oct 11 21:51 bashrc # atime
-rw-r--r--. 1 mrcode mrcode 231 Oct 13 21:51 bashrc # ctime
# 可以看到前两个实际变化了,ctime 又变成当前时间了

# 将日期调整为诶指定的时间 2014/06/15 00:00
[mrcode@study tmp]$ touch -t 201406150000 bashrc
[mrcode@study tmp]$ date; ll bashrc ; ll --time=atime bashrc ; ll --time=ctime bashrc
Sun Oct 13 21:54:31 CST 2019
# 由于时间太久远,默认的格式显示不全的,没有显示时分格式
-rw-r--r--. 1 mrcode mrcode 231 Jun 15 2014 bashrc
-rw-r--r--. 1 mrcode mrcode 231 Jun 15 2014 bashrc
-rw-r--r--. 1 mrcode mrcode 231 Oct 13 21:54 bashrc

那么 touc 中最常用的功能是:

  • 建立一个空的文件
  • 将某个文件日期秀固定为目前(mtime 与 atime)
  • 比较重要的是 mtime,关心这个文件内容是什么时候被更新的

文件与目录的默认权限与隐藏权限

前面讲解过文件有若干的属性,读写执行等基本权限(rwx), 是否为目录(d)、文件(-)或则是链接(l)等属性,修改属性也可通过 chgrpchownchmod

除了基本的 rwx 权限外,在传统的 ext2、3、4 文件系统下,还可以设置其他的系统隐藏属性, 可以使用 chattr 来设置,以 lsattr 来查看,最重要的属性就是可以设置不可修改的特性, 让连文件的拥有者都不能进行修改。

在安全机制方面特别的重要,但是在 CentOS7 中利用 xfs 作为预设文件系统, 该文件系统就不支持 chattr 参数了,仅有部分参数还有支持

文件预设权限 umask

umask:指定目前用户在建立文件或目录时候的默认权限

1
2
3
4
5
6
7
8
# 以数值形态显示
[mrcode@study tmp]$ umask
0002 # 与一般权限有关的是后面三个数字

# 还可以以符号来显示
[mrcode@study tmp]$ umask -S
u=rwx,g=rwx,o=rx

在数值形态下有 4 组,第一组是特殊权限用的,先不看,因此预设情况如下:

  • 文件

    没有可执行(x)权限、只有 rw 两个项目,也就是最大为 666 分 -rw-rw-rw-

  • 目录

    由于 x 与是否可以进入此目录有关,因此默认所有权限均开发,即 777 分 drwxrwxrwx

注意:umask 的分数指的是,该默认值需要 减掉 的权限!也就是需要从预设的权限中减掉

使用上面的示例来说明:

1
2
3
4
5
6
7
r、w、x 分别是 4、2、1 分。

002,也就是 others 的权限被拿掉了 2 也就是 w,那么权限如下:

建立文件时:预设 -rw-rw-rw-,减掉 2 变成 -rw-rw-r--
建立目录时:预设 drwxrwxrwx,减掉 2 变成 drwxrwxr-x

不信吗?可以实践看下

umask 的利用与重要性:专题制作

你和你同学在同一个目录下 /home/class 合作一个专题,那么有没有可能你制作的文件, 你的同学无法编辑?

如果 umask 设置为 0022 ,那么相当于 group 默认创建只有 r 属性,除了拥有者, 其他人只能读,不能写。所以需要修改 umask 的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 修改语法是 umask 后接数值
# 由于笔者的 centos 较新,默认已经是 002 的了,这里就更改回 022 来测试
[mrcode@study tmp]$ umask
0002
[mrcode@study tmp]$ umask 022 # 更改为 022
[mrcode@study tmp]$ umask
0022
[mrcode@study tmp]$ touch test3
[mrcode@study tmp]$ mkdir test4
[mrcode@study tmp]$ ll -d test[34] # 使用正则来匹配 test3和4
# 可以看到 文件 group 和 ohters 已经没有了 w
-rw-r--r--. 1 mrcode mrcode 0 Oct 13 22:23 test3
drwxr-xr-x. 2 mrcode mrcode 6 Oct 13 22:23 test4

TIP

umask 对于新建文件与目录的默认权限很重要,这个概念可以用在任何服务器上面, 尤其是未来假设文件服务器(file server),如 SAMBA Server 或则是 FTP server 时, 牵涉到你的使用者是否能够将文件进一步利用的问题

原来在预设的情况下,身份不同默认值也是不同的,rootumask 默认是 022,一般账户是 002。 关于预设设定可以参考 /etc/bashrc 这个文件的内容,不过这里不建议修改该文件, 后续讲解 bash shell 环境参数配置中再详解

文件隐藏属性

除了基本的 9 个权限外,还有隐藏属性,而隐藏属性对系统有很大的帮助,尤其是在安全上面。

chattr 配置文件隐藏属性

强调:在 ext2/3/4 中完全支持,而在 xfs 上部分支持

1
chattr [+-=][ASacdistu] 文件或目录名称
  • +:增加一个特殊参数,其他参数不变
  • -:移除一个特殊参数
  • =:设定为后面接的参数
  • A:若有存取此文件/目录时,它的访问时间 atime 将不会被修改
  • S:对文件的修改变成同步写入磁盘中,一般默认是异步写入(前面章节讲到过 sync)
  • a:该问价只能增加数据,不能删除也不能修改数据,只有 root 才能设置该属性
  • c:自动将此文件压缩,在读取的时候也将会自动解压缩,但是在存储的时候,会先压缩后再存储(对大文件似乎有用)
  • d:当 dump 程序被执行的时候,可使该标记的文件或目录不被 dump 备份
  • i:让文件不能被删除、改名、设置连接、写入或新增数据,完完全全就是只读文件了。只有 root 能设置该属性
  • s:当文件被删除时,将会被完全的移除这个硬盘空间,所以如果误删,就找不回来了
  • u:与 s 相反,删除后,其实数据还在磁盘中,可以用来救援该文件

注意:

  • 属性设置常见的是 ai 的设置,而且很多设置值必须要 root
  • xfs 文件系统仅支持 AadiS 选项

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@study tmp]# cd /tmp/
[root@study tmp]# touch attrtest
# 添加 i 属性
[root@study tmp]# chattr +i attrtest
# 尝试删除,发现不能删除,连 root 也无法删除
[root@study tmp]# rm attrtest
rm: remove regular empty file ‘attrtest’? y
rm: cannot remove ‘attrtest’: Operation not permitted
[root@study tmp]# rm -rf attrtest
rm: cannot remove ‘attrtest’: Operation not permitted

# 移除 -i 属性
[root@study tmp]# chattr -i attrtest
[root@study tmp]# rm attrtest
rm: remove regular empty file ‘attrtest’? y
# 这次再删除就成功了

个人觉得 +i+a 属性最重要:

  • i:无法被更动
  • a:不能修改旧的数据,只能新增

那么 a 属性在后续的登录档(log file)这种登录日志类的场景就很适合了

lsattr 显示文件隐藏属性

1
2
lsattr [-adR] 文件或目录

  • a:将隐藏文件的属性也秀出来
  • d:如果接的是目录,仅列出目录本身的属性而非目录内的文件名
  • R:连同子目录的数据也列出来
1
2
3
4
5
6
7
8
9
10
11
12
# 这里创建一个文件,然后观察他的特殊属性
[root@study tmp]# touch attrtest
[root@study tmp]# ll attrtest
-rw-r--r--. 1 root root 0 Oct 13 22:50 attrtest
[root@study tmp]# lsattr attrtest
# 发现是一片空白
---------------- attrtest
# 添加之后再观察
[root@study tmp]# chattr +aiS attrtest
[root@study tmp]# lsattr attrtest
--S-ia---------- attrtest

文件特殊权限 SUID、SGID、SBIT

除了前面的 9 个权限之外,还有特殊的权限,如下面两个目录

1
2
3
4
5
6
[mrcode@study ~]$ ls -ld /tmp/;ls -l /usr/bin/passwd
# 尾部多了一个 t
drwxrwxrwt. 38 root root 4096 Oct 16 21:37 /tmp/
# 拥有者里面多了一个 s
-rwsr-xr-x. 1 root root 27856 Aug 9 09:39 /usr/bin/passwd

st 这两个的权限与后续的 「系统的账户」及系统的程序(process)较为相关, 关于概念需要再后续两个章节讲完之后,才会了解,这里只需要知道 SUIDSGID 如何设定即可

Set UID

s 标志出现在文件拥有者 x 权限上时,就被称为 Set UID,简称 SUID 特殊权限, 对于文件的特殊功能如下:

  • SUID 权限仅对二进制程序(binary program)有效
  • 执行者对于该程序需要具有 x 的可执行权限
  • 本权限仅在执行该程序的过程中有效(run-time)
  • 执行者将具有该程序拥有者(owner)的权限

比如:linux 中,所有的账户的密码都记录在 /etc/shadow 文件中,既然该文件仅有 root 可以修改,那么我自己的 mrcode 一般账户使用者能否自行修改自己的密码呢?

1
2
3
4
5
[mrcode@study ~]$ passwd
Changing password for user mrcode.
Changing password for mrcode.
(current) UNIX password:

使用如上命令,发现可以修改,那么: shadow 一般账户不能读取,为什么还能修改密码呢?(也就是间接的修改了 shadow 中的数据),这就是 SUID 的功能了。

  • mrcode 对于 /usr/bin/passwd 这个程序来说具有 x 权限的,表示 mrcode 能执行 passwd
  • passwd 的拥有者是 root 账户
  • mrcode 执行 passwd 的过程中,会暂时获得 root 的权限
  • /etc/shadow 就可以被 mrcode 所执行的 passwd 所修改

那么使用 cat 去读取 /etc/shadow 可以吗?通过查看 cat 的权限,会发现 cat 没有包含 SUID 特殊权限,就是为什么不能读取的原因

1
2
3
4
[mrcode@study ~]$ ll /usr/bin/passwd
-rwsr-xr-x. 1 root root 27856 Aug 9 09:39 /usr/bin/passwd
[mrcode@study ~]$ ll /usr/bin/cat
-rwxr-xr-x. 1 root root 54080 Aug 20 14:25 /usr/bin/cat

TIP

SUID 仅可用在 binary program 上,不能用在 shell script 上面, 因为 shell script 只是将很多的 binary 执行档叫进来执行而已。

所以 SUID 的权限部分需要看脚本中执行的指令是否具有 SUID ,而不是脚本自身。 对目录页是无效的

Set GID

s 在群组的 x 时称为 Set GID

1
2
3
[mrcode@study ~]$  ls -l /usr/bin/locate
-rwx--s--x. 1 root slocate 40520 Apr 11 2018 /usr/bin/locate

SGID 可以针对文件或目录来设置,针对文件来说有如下功能含义:

  • SGID 对二进制程序有用
  • 程序执行者对于该程序来说,需要具备 x 的权限
  • 执行者在执行的过程中将会获得该程序群组的支持

例如:**/usr/bin/locate** 这个程序可以搜索 /var/lib/mlocate/mlocate.db 文件内容, 权限如下

1
2
3
4
[root@study ~]# ll /usr/bin/locate /var/lib/mlocate/mlocate.db
-rwx--s--x. 1 root slocate 40520 4月 11 2018 /usr/bin/locate
-rw-r-----. 1 root slocate 3468856 10月 13 15:36 /var/lib/mlocate/mlocate.db

如果使用 mrcode 账户去执行 locate 时,mrcode 将会取得 slocate 群组的支持; (这里有点懵逼,使用 locate -A /var/lib/mlocate/mlocate.db 没有报错,但是没有内容, 但是直接使用 ll /var/lib/mlocate/ 却提示没有权限,只能后续的课程讲了后才知道是什么意思了)

除了 binary program 外,SGID 还能用在目录上,当一个目录设置了 SGID 的权限后,将具有如下的功能:

  • 用户若对于此目录具有 rx 的权限时,该用户能够进入此目录
  • 用户在此目录下的有效群组(effective group)将会变成该目录的群组
  • 用途:若用户在此目录下具有 w 的权限(可以新建文件),则使用者所建立的新文件,该新文件的群组与此目录的群组相同

SGID 对于项目开发来说非常重要,涉及到群组权限的问题。可以参考下后续的「情景模拟的案例」, 能加深一点了解

Sticky Bit

Sticky Bit简称为 SBT ,目前只针对目录有效,对于文件没有效果了

作用是:当用户对于此目录具有 wx 权限,即具有写入的权限时,当用户在该目录下简历文件或目录时, 仅有自己与 root 才有权利删除该文件

例如:mrcode 用户在 A 目录是具有 w 的权限(群组或其他人类型权限),这表示 mrcode 对该目录 内任何人简历的目录或则文件均可进行删除、更名、搬移等动作,但是将 A 目录加上了 SBIT 的权限时,则 mrcode 只能够针对自己建立的文件或目录进行删除、更名、搬移等动作,而无法删除他人的文件

TIP

这部分内容在后续章节「关于程序方面」的只是后,再回过头来看,才能明白讲的是什么

SUID、SGID、SBIT 权限设定

可以使用数值权限更改方法来设置,他们代表的数值是:

  • SUID4
  • SGID2
  • SBIT1

下面演示具体这个数值加载哪里

1
2
3
4
5
6
7
8
[root@study tmp]# cd /tmp/
[root@study tmp]# touch test
# -rwsr-xr-x 拥有者权限 rwx 都有分数为 7,后面的都是5,原本权限为 755
# 那么久在 755 前增加特殊权限数值即可
# 这里添加 SUID 的权限
[root@study tmp]# chmod 4755 test; ls -l test
-rwsr-xr-x. 1 root root 0 10月 16 22:16 test

下面再来演示几个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 添加 SUID + SGID 权限
[root@study tmp]# chmod 6755 test; ls -l test
-rwsr-sr-x. 1 root root 0 10月 16 22:16 test

# 添加 SBIT
[root@study tmp]# chmod 1755 test; ls -l test
-rwxr-xr-t. 1 root root 0 10月 16 22:16 test

# 添加具有空的 SUID SGID 权限
# 这里出现了大写的 SST
[root@study tmp]# chmod 7666 test; ls -l test
-rwSrwSrwT. 1 root root 0 10月 16 22:16 test


上面最后一个例子出现了大写的三个特殊权限 SST,这里是这样的,因为 666 的权限中 不包含 x 权限,所以当特殊权限出现在 x 中的时候(又不拥有 x)则会出现大写的,表示空。 SUID 表示该文件在执行的时候,具有文件拥有者的权限,但是文件拥有者都无法执行了, 哪里来的权限给其他人使用呢?

除了数值,还可以使用符号来处理:

  • SUIDu+s
  • SGIDg+s
  • SBITo+t
1
2
3
4
5
6
7
8
9
# 设置为 -rws--x--x
[root@study tmp]# chmod u=rwxs,go=x test; ls -l test
-rws--x--x. 1 root root 0 10月 16 22:16 test

# 在上面的权限基础上,增加 SGID 与 SBIT
[root@study tmp]# chmod g+s,o+t test; ls -l test
-rws--s--t. 1 root root 0 10月 16 22:16 test


观察文件类型 file

想知道某个文件的基本数据,例如属于 ASCII 或则是 data 文件、binary 、是否用到动态函数库(share library)等信息,可以使用 file 指令来检阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ASCII 文本文件
[root@study tmp]# file ~/.bashrc
/root/.bashrc: ASCII text

# 执行文件的数据就很多了,包括这个文件的 suid 权限、兼容于 intel x86-64 等级的硬件平台
# 使用的是 linux 核心 2.6.32 的动态函数库链接
[root@study tmp]# file /usr/bin/passwd
/usr/bin/passwd: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=471dad50eb96512f90dd9394adbd7513ae60f072, stripped

# data 文件
[root@study tmp]# file /var/lib/mlocate/mlocate.db
/var/lib/mlocate/mlocate.db: data


通过这个指令可以简单的判断文件的格式,包括判断使用 tar 文档是使用的哪一种压缩功能

指令与文件的搜寻

很有用的功能之一,需要搜索某个文件在哪个位置,因为很多软件的配置文件名是不变的, 但是各 distribution 放置的目录则不同。要把位置找出来才能修改配置

脚本文件名的搜索

我们已经知道在终端模式下,连续两次「tab」有指令补全的功能,能展示出想匹配的指令, 那么这些指令在哪里呢?

which 搜索执行文档

1
2
3
4
which [-a] command

-a:将所有 PATH 目录中可以找到的指令均累出,而不止第一个被找到的指令名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 搜索 ifconfig 这个指令完整文件名
[root@study tmp]# which ifconfig
/sbin/ifconfig

# 查看 which 在哪个位置
[root@study tmp]# which which
alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
/bin/alias
/usr/bin/which

# 这里发现了两个 which,其中一个是 alias
# alias 是指令的别名,输入 which 后,就等于属于了等于后面的那一串指令
# 更多的数据在后续的 bash 章节中讲解

# 找出 history 指令的完整文档名
[root@study tmp]# which history
/usr/bin/which: no history in (/usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin)
# 上面在列出的目录中没有找到
# 下面报错没有 --help 这个选项
[root@study tmp]# history --help
-bash: history: --: 无效选项
history: 用法:history [-c] [-d 偏移量] [n] 或 history -anrw [文件名] 或 history -ps 参数 [参数...]


上面 history 为什么找不到?

  • which 根据 PATH 环境变量中的目录来搜索的
  • 只能找出执行文件
  • history 是 bash 内置的指令

history 不在 PATH 内的目录中,是 bash 内置的指令, 但是可以通过 type 指令,后续章节 bash 详解

文件名的搜索

linux 中有许多搜索指令,通常 find 不很常用,因为速度慢,操硬盘(啥意思?), 一般先用 whereis 或则是 locate 来检查,如果找不到,则用 find 来搜索。

  • whereis 只找系统中某些特定目录下的文件,速度快
  • locate 则利用数据库来搜索文件名的,速度块
  • find 搜索全磁盘内的文件系统状态,耗时

whereis 由一些特定的目录中搜索文件名

1
2
whereis [-bmsu] 文件或目录名

  • l:列出 whereis 会去查询的几个主要目录
  • b:只找 binary 格式的文件
  • m:只找在说明文件 manual 路径下的文件
  • s:只找 source 来源文件
  • u:搜索不在上述三个选项中的其他特殊文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 找到 ifconfig 文件名
# 下面发现找到了两个包含 ifconfig 的
[root@study tmp]# whereis ifconfig
ifconfig: /usr/sbin/ifconfig /usr/share/man/man8/ifconfig.8.gz

# 找到与 passwd 有关的说明文件文件名(man page)
[root@study tmp]# whereis passwd
passwd: /usr/bin/passwd /etc/passwd /usr/share/man/man1/passwd.1.gz /usr/share/man/man5/passwd.5.gz

# 这里添加 -m 参数就只找说明文件了
[root@study tmp]# whereis -m passwd
passwd: /usr/share/man/man1/passwd.1.gz /usr/share/man/man5/passwd.5.gz


whereis 主要是针对 /bin/sbin 下的执行文件、**/usr/share/man** 下的 man page 文件、和几个特定的目录,所以速度块很多,由于不是全盘查找,可能找不到你想要的文件,可以使用 whereis -l 来显示具体会找那些目录

locate / updatedb

1
2
locate [-ir] keyword

  • i:忽略大小写的差异
  • c:不输出文件名,仅计算找到的文件数量
  • l:仅输出几行,例如输出五行则是 -l 5
  • S:输出 locate 所使用的数据库文件相关信息,包括该数据库记录的文件/目录数量等
  • r:后面可接正规表示法的显示方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 找出系统中所有与 passwd 先关的文件名,且只列出 5 个
[root@study tmp]# locate -l 5 passwd
/etc/passwd
/etc/passwd-
/etc/pam.d/passwd
/etc/security/opasswd
/usr/bin/gpasswd
# 可以看到找到了 9 前多个相关的
[root@study tmp]# locate -c 5 passwd
9863

# 列出 locate 查询所使用的数据库文件与各数据数量
[root@study tmp]# locate -S
数据库 /var/lib/mlocate/mlocate.db:
11,623 文件夹
153,170 文件
7,466,565 文件名中的字节数
3,468,856 字节用于存储数据库

locate 可以其实就是模糊搜索,只要包含关键词的文件名都会被匹配,他是他有一个限制, 查找的数据是已建立的数据库 /var/lib/mlocate 里面的数据来搜索的。

该数据库建立默认是每天执行一次(每个 distribution 不同,CentOS 7 是每天更新一次数据库), 所以能搜索到的结果是有延迟的

可以手动触发数据库的更新,直接使用 updatedb 指令就可以

  • updatedb

    根据 /etc/updatedb.config 的设置去搜索系统盘内的文件名,并更新到 /var/lib/mlocate 数据库文件内

  • locate:从 /var/lib/mlcate 内的数据库中搜索关键词

find

1
2
find [path] [option] [action]

与时间有关的参数

与时间有关的参数有 -atime、**-ctime-mtime,以 **-mtime 说明:

  • mtime n:在 n 天前的「一天之内」被修改过内容的文件
  • mtime +n:列出在 n 天之前(不含 n 本身)被修改过内容的文件
  • mtime -n:列出在 n 天之内(含 n 天本身)被修改过内容的文件
  • newer filefile 为一个存在的文件,列出比 file 还要新的文件
1
2
3
4
5
6
7
8
9
10
11
12
13
# 将过去系统上 24 小时内有更动过内容(mtime)的文件列出
find / -mtime 0
# 0 表示当前时间,也就是当前时间开始往前 24 小时,也就是 24 小时内被修改过的文件

# 3 天前,24 小时内,如下
find / -mtime 3

# 寻找 /etc 下的文件,如果文件日期比 /etc/passwd 新旧列出
find /etc -newer /etc/passwd

# 列出 4 天内被更动多的文件
find / -mtime -4

mtime 选项的 n 正负数差别表示不同的含义,图示如下

  • +4:表示大于等于 5 天前的
  • -4:表示小于等于 4 天内的
  • 4:表示 4~5 哪一天的文件

与使用者或组名有关的参数

  • uid nn 为数字,是用户的账户 ID(UID),UID 记录在 /etc/passwd 里面与账户名称对于的数字。后续介绍

  • gid nn 为数字,是组名的 ID,记录在 /etc/group 文件中

  • user name:name 为使用者账户名称,如 mrcode

  • group name:name 为组名

  • nouser:寻找文件的拥有者不存在 /etc/passwd 的人

  • nogroup:寻找文件的拥有群组不存在 /etc/group 的文件

    当你自行安装软件时,很可能该软件的属性当中并没有文件拥有者,这个时候就可以使用 nouser 与 nogroup

实践与练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查找 /home 下属于 mrcode 的文件
find /home/ -user mrcode
# 比较有用,如找到一个用户在系统中的所有文件时

# 查找系统中不属于任何人的文件
[root@study ~]# find / -nouser
find: ‘/proc/19655/task/19655/fd/5’: 没有那个文件或目录
find: ‘/proc/19655/task/19655/fdinfo/5’: 没有那个文件或目录
find: ‘/proc/19655/fd/6’: 没有那个文件或目录
find: ‘/proc/19655/fdinfo/6’: 没有那个文件或目录
find: ‘/proc/19657’: 没有那个文件或目录
find: ‘/proc/19668’: 没有那个文件或目录
find: ‘/proc/19669’: 没有那个文件或目录
find: ‘/proc/19670’: 没有那个文件或目录

# 这里没有找出来,但是报错了一些目录不存在,不知道是啥原因
# 透过这个指令,可以轻易的找出那些不太正常的文件,如果找到了,那么有可能是正常的,比如你以源码编译软件时

nousernogroup 的选项,除了你自行由网络上面下载文件时会发生之外,如果你将系统里面某个账户删除了, 但是该账户以及在系统内建立了很多文件,那么就可能发生 nousernogroup 的文件

与文件权限及名称有关的参数

  • name filename:查找文件名为 filename 的文件

  • size [-+]SIZE:查找比 SIZE 还要大(**+)或则小(-**)的文件

    SIZE 支持的单位有:

    • cbyte
    • k1024 byte

    所以要查找 比 50 KB 还要大的文件,指令为 find /home/ -size +50ks

  • type TYPE:查找文件类型为 TYPE 的。主要有

    • f:一般正规文件
    • b,c:装置文件
    • d:目录
    • l:连接
    • ssocket
    • pFIFO
  • perm mode:查找文件权限「刚好等于」mode 的文件,mode 为类似 chmod 的属性。

    例如:**-rwsr-xr-x** 的属性为 4755

  • perm -mode:查找文件权限「必须要全部包括 mode 的权限」的文件

    例如:查找 -rwxr–r– ,即 0744 的文件,使用 -perm -0744

  • perm /mode:查找文件权限「包含任意 mode 的权限」的文件

    例如:**-rwxr-xr-x,即 **-perm /755 时,但一个属性属性为 -rw—— 也会被列出来, 因为他有 -rw 的属性存在

实践与练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 找出文件名为 passwd 的文件
find / -name passwd

# 找出包含了 passwd 关键词的文件
find / -name "*passwd*"

# 找出 /run 目录下,文件类型为 socket 的文件
find /run -type s
# -type 属性也很有用,可以找出那些怪异的文件
# 列入 socket 与 FIFO 文件,可以使用 find /run -type p 或 -type s 来找

# 查找文件中含有 SGID 或 SUID 或 SBIT 的属性
find / -perm /7000
# 7000 就是 ---s--s--t

上面范例中比较有趣的是 -perm 可以找出特殊权限的文件,SUIDSGID 都可以设置在二进制文件上

1
2
3
4
# 找出 /usr/bin /usr/sbin 具有 SUID 或 SGID 的文件
find /usr/bin /usr/sbin -perm /6000
# SUID=4、SGID=2、SBIT=1

额外可以进行的动作

  • exec commandcommand 为其他指令,**-exec** 后面可再接额外的指令来处理搜索到的结果
  • print:将结果打印到屏幕上,这个动作是预设的,不然不会看到结果

实践与练习

1
2
3
4
5
6
7
8
# 将上个范例找到的文件使用 ls -l 列出来
find /usr/bin /usr/sbin -perm /6000 -exec ls -l {} \;
# -exec 后面的 ls -l 就是额外的指令,指令不支持命令别名
# 所以只能使用 ls -l 而不能使用 ll

# 找出系统中,大于 1MB 的文件
find / -size +1M

find 的特殊功能就是可以进行额外的动作(action),图解一个范例

  • {}:表示由 find 找到的内容
  • -exec 开头到 \; 结尾:中间的表示指令额外动作
  • ;:在 bash 环境下又特殊意义的,用斜杠 \ 来跳脱

说使用 find 在寻找数据的时候相当操硬盘是啥意思?耗费硬盘?所以能用 whereis 与 locate 操作的尽量用

删除 n 天前的文件

笔者这里既然学习了 find 知识点,现在拿出之前在工作中经常用到删除 n 天前的指令来分析下, 看能不能看懂

1
2
3
4
5
6
7
find /usr/local/backups -mtime +10 -name "*.*" -exec rm -rf {} \;

-mtime :表示文件修改时间
+10 :表示 10 天前的(不含 10 哪一天)
name:查找文件名,后面使用了通配符,查找所有的文件
-exec rm -rf {} \; 使用执行额外动作,将查找到的文件执行了 rm -rf 删除操作

find 常用命令收集

以下收集一些常用的使用方式

1
2
3
# 搜索文件内容,并显示命中的文件 与 内容所在行
find . -type f -name "*.conf" -print0 | xargs -0 grep -n "8081"

权限与指令间的关系

权限对于使用者账户来说是非常重要的,因为可以限制使用者不能读取、建立、删除、修改文件或目录。

那么什么指令在什么样的权限下才能够运行?

让用户能进入某目录称为「可工作目录」的基本权限

  • 可使用的指令:例如 cd 等变换工作目录的指令
  • 目录所需权限:用户对这个目录至少具有 x 的权限
  • 额外需求:如果想在该目录内利用 ls 查阅文件名,则还需要有目录的 r 权限

用户在某个目录内读取一个文件的基本权限

  • 可使用的指令:例如 cat、more、less 等
  • 目录所需权限:至少具有 x 权限
  • 文件所需权限:至少具有 r 权限

让使用者可以修改一个文件的基本权限(修改文件内容)

  • 可使用的指令:例如 nano 或未来要介绍的 vi 编辑器等
  • 目录所需权限:至少具有 x 权限
  • 文件所需权限:至少具有 r、w 权限

让一个使用者可以建立一个文件的基本权限

  • 目录所需权限:至少具有 w、x 权限,重点是 x 权限

让用户进入某目录并执行该目录下的某个指令之基本权限

  • 目录所需要的权限:至少具有 x 权限
  • 文件所需要的权限:至少具有 x 权限

文件与文件系统的压缩、打包与备份

压缩文件的用途与技术

文件压缩技术一般用于的场景是:当文件容量很大的时候,想要降低一些容量,在网络中传输时间少,当然下载的人就能更快的下载完,还有数据归档使用 cd 或则 dvd 来存储,但是某些单一文件比这些传统的一次性存储媒体还要大、等等的场景。

简单说就是:这些大型文件通过压缩技术之后,可以将他的磁盘使用量降低,达到减低文件容量的效果

文件压缩的原理是什么?

计算机最小的计量单位是 bits,不过目前我们使用的计算机系统中都是使用 bytes 单位来计量的,1 bytes=8bits,计算机存储文件是二进制的,当这个 8 bits 中没有被填满时,就会出现大量的 bit 被 0 填充,实际上他们是没有什么意义的,一些工程师利用一些复杂的计算方式,将这些没有使用到的空间去掉,来达到让文件占用空间变小的目的,这就是 压缩技术

还有一种压缩技术是将重复的数据进行统计记录。比如:你的数据为「1111….」有 100 个 1,压缩技术会记录「100 个 1」,而不是真的写了 100 个 1 出来。这样也能达到减少文件体积的目的

简单说:文件里面有相当多的「空间」存在,并不是完全填满的,而压缩技术就是将这「空间」填满,让整个文件占用的容量下降。但是被压缩过的文件无法被直接使用,需要还原回未压缩前的模样,这就是 解压缩 技术。

压缩后与压缩的文件所占用的磁盘空间大小,就可以被称为是「压缩比」,更多的技术可以查阅 GZIP 文件格式规范

解压缩有什么好处呢?Linux 3.10.81(CentOS 7 用的延伸版本)完整核心大小约 570MB 左右,由于核心主要多是 ASCII code 的纯文本形态文件,这种文件的「多余空间」是比较多的。那么压缩之后的核心仅有 76MB 左右,相差几倍。网络传输时间减少,你的磁盘占用也减少。

Linux 系统常用的压缩指令

在 linux 环境中,压缩文件的扩展名大多是:.tar.tar.gz.tgz.Z.bz2.xz ,为什么会有这么多?

虽然在 linux 下扩展名没有啥作用,但是支持的压缩指令非常多,彼此之间无法互通压缩或解压缩,扩展名用于分别是使用哪种软件来解压缩。常用的扩展名如下:

  • .Zcompress 程序压缩的文件
  • .zipzip 程序压缩的文件
  • .gzgzip 程序压缩的文件
  • .bz2bzip2 程序压缩的文件
  • .xzxz 程序压缩的文件
  • .tartar 程序打包的数据,并没有压缩过
  • .tar.gztar 程序打包的数据,并经过 gzip 的压缩
  • .tar.bz2:同上,经过了 bzip2 压缩
  • .tar.xz:同上,经过了 xz 的压缩

linux 上常见的压缩指令是 gzipbzip2 以及最新的 xz,还有支持 windows 的 zip,至于其他的压缩指令基本上都淘汰了。这些指令通常仅能针对一个文件来压缩与解压缩,如此一来每次压缩与解压缩都要一大堆文件,所以 tar (打包)软件就出现了

tar 可以将很多文件「打包」成为一个文件,将很多文件集结为一个文件,但是没有提供压缩的功能,后来 GNU 计划中,将整个 tar 与压缩的功能结合在一起,提供了更强大的压缩与打包功能。

gzip,zcat/zmore/zless/zgrep

gzip 可以说是应用最广的压缩指令,目前可用解开 compresszipgzip 等软件所压缩的文件,语法如下

1
2
3
gzip [-cdtv#] 文档名
zcat 文档名.gz

选项与参数:

  • c:将压缩的数据输出到屏幕上,可通过数据流重导向来处理
  • d:解压缩的参数
  • t:可以用来检验一个压缩文件的一致性,看看文件有无错误
  • v:可以显示出原文件、压缩文件的压缩比等信息
  • **#**:为数字的意思,代表压缩等级
    • -1:最快,但是压缩比最差
    • -9:最慢,但是压缩比最好
    • -6:默认值

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 找出 /etc 下(不含子目录)容量最大的文件,并将他们复制到 /tmp,然后以 gzip 压缩
# S 排序时按文件 size,r 翻转,倒序;对于参数,笔者这里使用 man ls 查看的参数说明,记不住的时候就可以这样做
# ls -ldSr /etc/* 最大的排在最后,莫非使用了 S 就是降序排列的吗?
[mrcode@study ~]$ ls -ldS /etc/*
-rw-r--r--. 1 root root 670293 Jun 7 2013 /etc/services
-rw-r--r--. 1 root root 104251 Oct 4 18:28 /etc/ld.so.cache

[mrcode@study ~]$ cd /tmp/
[mrcode@study tmp]$ cp /etc/services .
[mrcode@study tmp]$ gzip -v services
services: 79.7% -- replaced with services.gz
[mrcode@study tmp]$ ll /etc/services /tmp/services*
-rw-r--r--. 1 root root 670293 Jun 7 2013 /etc/services
-rw-r--r--. 1 mrcode mrcode 136088 Oct 28 22:39 /tmp/services.gz
# 可以看到压缩比为 79.7,压缩之后变成了 130 多 k

这里需要注意,使用 gzip 进行压缩时,默认状态下原本的文件会被压缩成 .gz 的文件,并且原始文件不存在了(文案已经提示了);另外 gzip 压缩的文件在 windows 中可以被 WinRAR7zip 软件解压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 由于 services 是文件内容,将压缩后的文件内容读出来
zcat services.gz
# 该文件过大,直接读完,我们看不到最前面的内容了
# 可以使用 zmore、zless 去读取

# 将 services.gz 加压缩
# 这里使用 -d 来解压缩,还有一个 gunzip 指令也可以解压缩,但是有点难记住它
[mrcode@study tmp]$ gzip -d services.gz
[mrcode@study tmp]$ ll -l services*
# 同样,默认会将 .gz 的删除,剩下原来的文件名
-rw-r--r--. 1 mrcode mrcode 670293 Oct 28 22:39 services

# 将上面解开的文件,使用最佳压缩比压缩,并保留原文件
# 这个例子就明白 -c 使用数据流重导来处理是啥意思了,压缩输出到指定文件中
# 这里的 > 后续再 bash 章节会详细讲解
[mrcode@study tmp]$ gzip -9 -c services > services.gz
[mrcode@study tmp]$ ll -l services*
-rw-r--r--. 1 mrcode mrcode 670293 Oct 28 22:39 services
-rw-rw-r--. 1 mrcode mrcode 135489 Oct 28 22:50 services.gz

# 还可以在 services.gz 找那个找出 http 关键词在哪里(关键词搜索)
[mrcode@study tmp]$ zgrep -n 'http' services.gz
# 下面会输出好多包含 http 的信息
14:# http://www.iana.org/assignments/port-numbers
89:http 80/tcp www www-http # WorldWideWeb HTTP

在压缩文档中搜索字符的话可以使用 zgrepegrep 等指令

bzip2、bzcat/bzmore/bzgrep

bzip2 可以说是取代了 gzip 并提供更佳的压缩比。使用方式几乎与 gzip 相同

1
2
3
bzip2 [-cdkzv#] 文档名
bzcat 文档名.bz2

选项与参数:

  • c:将压缩的过程产生的数据输出到屏幕上
  • d:解压缩的参数
  • k:保留源文件
  • z:压缩的参数(默认值,可以不加)
  • v:可以显示出源文件/压缩文件的压缩比信息
  • #:与 gzip 一样,-9 最佳、-1 最快

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[mrcode@study tmp]$ bzip2 -v services
services: 5.409:1, 1.479 bits/byte, 81.51% saved, 670293 in, 123932 out.
[mrcode@study tmp]$ ll -l services*
-rw-r--r--. 1 mrcode mrcode 123932 Oct 28 22:39 services.bz2
-rw-rw-r--. 1 mrcode mrcode 135489 Oct 28 22:50 services.gz
# bz2 的压缩率比 gz 的压缩率还要好,看文件大小

# 读取 bz2 文件内容
bzcat services.bz2
# 解压,默认都是会删除掉原文件,包括压缩也是
[mrcode@study tmp]$ bzip2 -d services.bz2
[mrcode@study tmp]$ ls -l services*
-rw-r--r--. 1 mrcode mrcode 670293 Oct 28 22:39 services
-rw-rw-r--. 1 mrcode mrcode 135489 Oct 28 22:50 services.gz

# 这里使用 -k 参数来保留源文件,并使用最优压缩比
# 还可以使用 -c 来输出 bzip2 -9 -c services > services.gz
[mrcode@study tmp]$ bzip2 -9 -k services
[mrcode@study tmp]$ ls -l services*
-rw-r--r--. 1 mrcode mrcode 670293 Oct 28 22:39 services
-rw-r--r--. 1 mrcode mrcode 123932 Oct 28 22:39 services.bz2


使用方式 bzip2 与 gzip 几乎一模一样,不过压缩率好的一般都会更耗时

xz、xzcat/xzmore/xzless/xzgrep

xzbzip2 压缩比更高,用法也与 bzip2gzip 就一模一样

1
2
3
xz [-dtlkc#] 文档名
xcat 文档名.xz

选项与参数:

  • d:解压缩
  • t:测试压缩文件的完整性,看是否有错误
  • l:列出压缩文件的相关信息
  • k:保留原本的文件
  • c:将数据由屏幕上输出
  • #:同样,压缩比数值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 压缩
[mrcode@study tmp]$ xz -v services
services (1/1)
100 % 97.3 KiB / 654.6 KiB = 0.149
[mrcode@study tmp]$ ls -l services*
-rw-r--r--. 1 mrcode mrcode 123932 Oct 28 22:39 services.bz2
-rw-rw-r--. 1 mrcode mrcode 135489 Oct 28 22:50 services.gz
-rw-r--r--. 1 mrcode mrcode 99608 Oct 28 22:39 services.xz
# 看看上面的压缩,果真 xz 压缩比是最好的
# 列出压缩文件信息
[mrcode@study tmp]$ xz -l services.xz
Strms Blocks Compressed Uncompressed Ratio Check Filename
1 1 97.3 KiB 654.6 KiB 0.149 CRC64 services.xz

# 读取压缩文件内容
xzless services.xz 、xzcat services.xz 都可以
# 解压缩
[mrcode@study tmp]$ xz -d services.xz
# 使用 -k 压缩并保留源文件
[mrcode@study tmp]$ xz -k services


# 可以使用 time 指令统计他们的时间
# time [gzip|bzip2|xz] -c services > services.[gz|bz2|xz]
# 上面的语法,下面是实际的指令
[mrcode@study tmp]$ time gzip -c services > services.gz;\
> time bzip2 -c services > services.bz2;\
> time xz -c services > services.xz ;
real 0m0.023s # 看这个汇总时间
user 0m0.021s
sys 0m0.002s

real 0m0.043s
user 0m0.036s
sys 0m0.007s

real 0m0.232s # 看这个汇总时间
user 0m0.227s
sys 0m0.005s

一个 0.023s 一个 0.232s,相差 10 倍

打包指令:tar

前面讲解的 gzipbzip2xz 也能够针对目录进行压缩,但是是将目录内所有文件 分别 压缩的。而在 windows 下可以使用 winRAR 之类的压缩文件,将好多数据包成一个文件的样式。

这种将多个文件或目录包成一个大文件的指令功能,就可以称呼为 打包指令tar 就是这样一个功能的打包指令,同时还可以通过压缩指令将该文件进行压缩。windows 中的 WinRAR 也支持 .tar.gz 的解压缩

tar 的选项与参数非常多,这里只接受几个常用的选项

1
2
3
4
5
6
7
打包与压缩:`tar [-z|-j|-J][cv][-f 待建立的文件名] filename`
观察文件: `tar [-z|-j|-J][tv][-f file.tar]`
解压缩: `tar [-z|-j|-J][xf][-f file.tar] [-C 目录]`

特别注意:`[-z|-j|-J]` 不可同时出现在一串指令中
特殊注意:c、t、x 也不可同时出现在一串指令中

选项与参数

  • c:建立打包文件,可搭配 -v来观察过程中被打包的文件名
  • t:查看打包文件的内容含有哪些文件,重点在查看文件名
  • x:接打包或解压缩的功能,可搭配 -C 在特定目录解开,特别注意 c、t、x 不能同时出现在一起
  • z:通过 gzip 的支持进行压缩、解压缩;此时文件名最好为 *.tar.gz
  • j:通过 bzip2 的支持进行压缩、解压缩;此时文件名最好为 *.tar.bz2
  • J:通过 xz 的支持进行压缩、解压缩;此时文件名最好为 *.tar.xz
  • v:在压缩、解压缩的过程中,将正在处理的文件名显示出来
  • f:后面要立刻接要被处理的文件名,建议 -f 单独写一个选项(不容易忘记)
  • C:在指定目录解压缩
  • p:保留备份数据的原本权限与属性,常用语备份(-c)重要的配置文件
  • P:保留绝对路径,保留 root 跟路径
  • --exclude=FILE:在压缩过程中,排除指定的文件,不打包

最常用的是以下命令:

  • 压 缩:tar -jcv -f filename.tar.bz2 要被压缩的文件或目录
  • 查 询:tar -jtv -f filename.tar.bz2
  • 解压缩:tar -jxv -f filename.tar.bz2 -C 指定目录解开

小提示:上面 -jcvf 可以写一起,但是阅读起来就没有上面这样分开好理解

使用 tar 加入 -z、-j 或 -J 的参数备份 /etc/ 目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 备份 /etc/ 需要 root 权限,否则会出现一堆错误
[mrcode@study ~]$ su -
Password:
Last login: Sun Oct 27 20:38:34 CST 2019 on pts/0

[root@study ~]# time tar -zpcv -f /root/etc.tar.gz /etc
tar: 从成员名中删除开头的“/” # 注意这里的警告
/etc/
/etc/fstab
/etc/crypttab
...
real 0m2.329s # 耗时 2.329 秒
user 0m1.322s
sys 0m0.308s

# -p 重点是保留文件的权限与属性
# 下面去掉了 -v,所以不会显示处理的文件名
[root@study ~]# time tar -jpc -f /root/etc.tar.bz2 /etc
tar: 从成员名中删除开头的“/”

real 0m3.012s
user 0m2.710s
sys 0m0.078s

[root@study ~]# time tar -Jpc -f /root/etc.tar.xz /etc
tar: 从成员名中删除开头的“/”

real 0m14.836s
user 0m13.511s
sys 0m0.224s

[root@study ~]# ll -h /root/etc*
-rw-r--r--. 1 root root 11M 10月 29 00:05 /root/etc.tar.bz2
-rw-r--r--. 1 root root 12M 10月 29 00:01 /root/etc.tar.gz
-rw-r--r--. 1 root root 8.2M 10月 29 00:06 /root/etc.tar.xz

# etc 占用 42M
[root@study ~]# du -sh /etc/
42M /etc/


前面讲解 cp 指令复制的时候也涉及到复制后的文件权限与属性问题,这里的 -p 选项也是这样

查阅 tar 文件的数据内容(可查看文件名)与备份文件名是否有根目录的意义

1
2
3
4
5
6
7
8
9
# -v 把权限属性也列出来了
# 这里查看文件名前面无根路径的
[root@study ~]# tar -jtv -f /root/etc.tar.bz2
drwxr-xr-x root/root 0 2019-10-04 18:38 etc/
-rw-r--r-- root/root 808 2019-10-27 22:43 etc/fstab
-rw------- root/root 0 2019-10-04 18:20 etc/crypttab
lrwxrwxrwx root/root 0 2019-10-04 18:20 etc/mtab -> /proc/self/mounts
-rw-r--r-- root/root 51 2019-10-04 18:20 etc/resolv.conf

为什么需要拿到根目录呢?主要是为了安全,使用 tar 备份的数据可能会需要解压缩回来使用,在 tar 所记录的文件名(上面 -jtv 显示的文件名)就是解压缩后的实际文件名。如果拿到了根目录,则会在当前目录解压。比如现在在 /tmp ,解压后就变成 /tmp/etc/xxx;如果不拿掉根目录,源文件就被覆盖了

1
2
3
4
5
6
7
8
9
10
[root@study ~]# tar -jPc -f /root/etc.and.root.tar.bz2 /etc
[root@study ~]# tar -jtv -f /root/etc.and.root.tar.bz2
[root@study ~]# tar -jtv -f /root/etc.and.root.tar.bz2
tar: 从成员名中删除开头的“/”
drwxr-xr-x root/root 0 2019-10-04 18:38 /etc/
-rw-r--r-- root/root 808 2019-10-27 22:43 /etc/fstab
-rw------- root/root 0 2019-10-04 18:20 /etc/crypttab
lrwxrwxrwx root/root 0 2019-10-04 18:20 /etc/mtab -> /proc/self/mounts
# 对比下,确实是带上了根路径

将备份的数据解压缩,并考虑指定目录压缩(-C 选项的应用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@study ~]# pwd
/root
[root@study ~]# tar -jx -f etc.tar.bz2
[root@study ~]# ll -d etc*
drwxr-xr-x. 143 root root 8192 10月 4 18:38 etc
-rw-r--r--. 1 root root 10520237 10月 29 00:15 etc.and.root.tar.bz2
-rw-r--r--. 1 root root 10518433 10月 29 00:05 etc.tar.bz2
-rw-r--r--. 1 root root 12212046 10月 29 00:01 etc.tar.gz
-rw-r--r--. 1 root root 8580036 10月 29 00:06 etc.tar.xz

# 解压到指定目录
tar -zx -f etc.tar.gz -C /tmp
# 记得删除解压后的文件
rm -rf /tmp/etc/ /root/etc

仅解开单一文件

前面讲解的都是解开该压缩包中的所有文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 利用 -t 查看文件名,接管道查找 shadow
[root@study ~]# tar -jtv -f /root/etc.tar.bz2 | grep 'shadow'
---------- root/root 1271 2019-10-04 18:31 etc/shadow-
---------- root/root 797 2019-10-04 18:31 etc/gshadow
---------- root/root 1266 2019-10-04 18:31 etc/shadow # 假设要提取出这个文件
---------- root/root 791 2019-10-04 18:31 etc/gshadow-

# 后面接需要提取出来的文件路径
[root@study ~]# tar -jxv -f /root/etc.tar.bz2 etc/shadow
etc/shadow
[root@study ~]# ll etc
总用量 4
----------. 1 root root 1266 10月 4 18:31 shadow


打包某目录,但不包含该目录下的某些文件

1
2
3
4
5
[root@study ~]# tar -jc -f /root/system.tar.bz2 --exclude=/root/etc* --exclude=/root/system.tar.bz2 /etc /root
tar: 从成员名中删除开头的“/”
tar: 从硬连接目标中删除开头的“/”


仅备份比某个时刻还要新的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 先找出比 /etc/passwd 还要新的文件
# 前面 touch 中介绍过 --newer 和 --newer-mtime
# newer 包含 mtime 和 ctime,而 --newer-mtime 只包含 mtime
[root@study ~]# find /etc -newer /etc/passwd
/etc
/etc/fstab
/etc/group
/etc/gshadow
...

[root@study ~]# ls --full-time /etc/passwd
-rw-r--r--. 1 root root 2323 2019-10-04 18:31:08.332738182 +0800 /etc/passwd

[root@study ~]# tar -jcv -f /root/etc.newer.the.passwd.tar.bz2 --newer-mtime="2019-10-04" /etc/*
tar: 选项 --newer-mtime: 将日期 ‘2019-10-04’ 当作 2019-10-04 00:00:00
tar: 从成员名中删除开头的“/”
/etc/abrt/
tar: /etc/abrt/abrt-action-save-package-data.conf: 文件未改变;未输出
tar: /etc/abrt/abrt.conf: 文件未改变;未输出
tar: /etc/abrt/gpg_keys.conf: 文件未改变;未输出

# 验证下是否被打进去了,这里搜索都搜不到,确实没有被打进去
[root@study ~]# tar -jtv -f etc.newer.the.passwd.tar.bz2 | grep 'etc/abrt/abrt.conf'
[root@study ~]# tar -jtv -f etc.newer.the.passwd.tar.bz2 | grep 'abrt.conf'

基本名称: tarfile, tarball ?

tar 可以只打包不压缩 tar -c -f file.tar,这种文件称为 tarfile,如果有压缩就称为 tarball

此外 tar 还可以将文件打包到特别的装置中去,例如,tar -c -f /dev/st0/home /root/etc ,把 etc 打包到磁带机去(磁带机是一次性读取、写入装置,因此不能使用 cp 等指令)

特殊应用:利用管线命令与数据流

关于数据流重导向与管线命令在 bash 章节再详细讲解

1
2
3
4
5
6
7
8
[mrcode@study ~]$ cd /tmp/
[mrcode@study tmp]$ tar -cv -f - /etc/ | tar -xv -f -
# 前面是将 /etc/ 打包到 - ,后面是吧 - 解压
# 这里的 - 表示标准的输出 和输出,可以吧 - 想成是内存中的一个缓冲区
# 这里命令像 cp -r /etc /tmp 的效果
# 这里不想用 -r 命令,所以使用 tar 打包到特殊的装置 - 中,然后管线前面输出的作为后面用来解压,没有产生中间文件,完成了复制的功能


例题:系统备份范例

系统上有非常多的目录需要进行备份,也不建议将备份数据放到 /root 目录下,假设目前已经知道重要的目录有:

  • /etc/:配置文件
  • /home/ :用户的家目录
  • /var/spool/mail/:系统中所有的邮件信箱
  • /var/spool/cron/:所有账户的工作排成配置文件
  • /root/:系统管理员的家目录

前面做过的练习,*/home/loop** 不需要备份,**/root** 下的压缩文件也不需要备份,假设需要将备份的数据放到 /backups 中,并且该目录仅有 root 权限进入,此外,每次备份的文件名希望不相同。

1
2
3
4
5
6
7
8
9
10
11
12
# 创建备份目录,并修改权限
[root@study ~]# mkdir /backups
[root@study ~]# chmod 700 /backups/
[root@study ~]# ll -d /backups/
drwx------. 2 root root 6 10月 29 01:33 /backups/

# 这里的 xxx 需要手动写上想要的日期等字符串每次就不一样了,并不是用脚本变量啥的
tar -zcv -f /backups/xxx.tar.gz --exclude="/home/loop*" --exclude="/root/*.gz" --exclude="/root/*.bz2" --exclude="/root/*.xz" /etc/ /home/ /var/spool/mail /var/spool/cron /root

[root@study ~]# ll -h /backups/
总用量 13M
-rw-r--r--. 1 root root 13M 10月 29 01:37 xxx.tar.gz

解压缩后的 SELinux 课题

假如你的系统必须要以备份的数据来回填到原本的系统中,那么需要特别注意复原后的系统 SELinux 问题,尤其是在系统文件上面。比如:**/etc** 下的文件群。SElinux 是比较特别的细部权限设定,具体的会在第 16 章介绍。SELinux 的权限问题,可能让你的系统无法存取某些配置文件内容,导致影响到系统的正常使用权。

有一个例子,通过上面的 tar 备份,然后在另外一部系统上还原回来,但是无法正常的登录系统,在单位维护模式去操作系统,看起来一切都正常,但是这里就是无法登录。大部分原因就是因为 /etc/shadow 密码文件的 SELinux 类型在还原时被更改了,简单的处理方式有如下几个:

  • 通过各种可行的救援方式登录系统,修改 /etc/seliux/config 文件,将 SELinux 改成 permissive 模式,重新启动系统就可以了
  • 在第一次复原系统后,不要立即重新启动,先使用 restorecon -Rv /etc 自动修复下 SELinux 的类型即可
  • 通过各种可行的方式登录系统,建立 /.autorelabel 文件,重新启动后系统会自动修复 SELinux 的类型,并且又会再次重新启动,之后就正常了

vim 程序编辑器

系统管理员的重要工作就是需要修改与设置某些重要软件的配置文件,因此至少得学会一种以上的文字模式下的文本编辑器。所有的 Linux distribution 上都有一套文本编辑器 vi,而且很多软件默认也是使用 vi 作为他们编辑器的接口。此外 vim 是进阶版的 vi,不但可以用不同颜色显示文字内容,还能够进行诸如 shell scriptC program 等程序编辑功能,可以将 vim 视为一种程序编辑器

vi 与 vim

LInux 的世界中,绝大部分的配置文件都是以 ASCII 的纯文本形态存在的,因此利用简单的文字编辑软件就可以修改配置了

linux 的文本模式下的编辑器有:emacspiconanojoevim 等,那么为何就要学 vi 呢?

为何要学 vim

为什么需要学习 vi ?原因如下:

  • 所有 Unix Like 系统都会内置 vi 编辑器,其他的编辑器则不一定会存在
  • 很多各别软件的编辑接口都会主动调用 vi (例如未来会讲解的 crontabvisudoedquota 等指令)
  • vim 具有程序编辑的能,可以主动的以字体颜色辨别语法的正确性,方便程序设计
  • 因为程序简单,编辑速度相当快

可以将 vim 视作是 vi 的进阶版,有语法高亮等功能。比如当使用 vim 编辑一个 shell script 脚本时,vim 会依据文件的扩展名或则是文件内的开头信息,判断该文件的内容而自动调用该程序的语法判断。甚至一些 Linux 基础配置文件内的语法,都能用 vim 来检查,例如第 7 章谈到的 /etc/fstab 文件内容

简单说,vi 是老式的文字处理器,vim 则是程序开发工具(https://www.vim.org/ 官网也是这样介绍的)而不是文字处理软件。因为 vm 里面加入了很多额外的功能,例如支持正规表示法的搜索架构、多文件编辑、区块复制等等。

vi 的使用

基本上 vi 共分为三种模式:一般指令模式、编辑模式、指令列命令模式

  • 一般指令模式(command mode

    vi 打开一个文件就直接进入一般指令模式了(默认模式,也简称一般模式)。

    在该模式中,可以使用「上下左右」按键移动光标,可以使用「删除字符」或「删除整列」来处理文件内容,也可以使用「复制、粘贴」

  • 编辑模式(insert mode

    在一般模式中可以进行删除、复制、粘贴等动作,但是无法编辑文件内容。

    需要按下「iIoOaArR」等任意按键后才会进入编辑模式,通常会在左下方出现 INSERTREPLACE 的字样,可以通过 esc 按键退出编辑模式,回到一般指令模式

  • 指令列命令模式(commadn-line mode

    在一般模式中,输入「**:/?**」任意字符,则光标会移动到最底下的一列。

    在这个模式中,可以提供你搜索、读取、存盘、大量取代字符、离开 vi、显示行号等功能

简单说,可以将这三个模式想象成下面的图标来表示

注意这里互换,编辑模式不能直接换到指令列模式!

按键说明

第一部分:一般指令模式可用的按钮说明

移动光标的方法

按键 说明
h 或 左箭头 光标向左移动一个字符
j 或 下箭头 光标向下移动一个字符
k 或 向上箭头 光标向上移动一个字符
i 或 右箭头 光标向右移动一个字符
特别说明 hjki 在键盘上是排列在一起 的,适合移动光标,移动多个的话可以加上数值再按方向键,比如 30↓ ,向下移动 30 行(注意是一般指令模式下)
ctrl + f 常用;向下移动一页,相当于 Page Down 按键
ctrl + b 常用;向上移动一页
ctrl + d 向下移动半页
ctrl + u 向上移动半页
+ 光标移动到非空格符的下一列
- 光标移动到非空格符的上一列
n<space> n 表示数字,如按下 20 ,再按空格键,光标会向右移动 n 个字符
0 或功能键 Home 常用;移动到这一行的最前面字自字符处
$或功能键 End 常用;移动到这一行的后面字符处
H 光标移动到这个屏幕的最上方那一行的第一个字符
M 光标移动到这个屏幕的中央那一行的第一个字符
L 光标移动到这个屏幕的最下方那一行的第一个字符
G 常用;移动到这个文件的最后一行
nG n 为数字,移动到这个文件的第 n 行。可配合 :set nu 显示行号,再移动到具体的行
gg 常用;移动到这个文件的第一行,相当于 1G 的功能
n<Enter> 常用;n 为数字,光标向下移动 n 行

搜索与取代

按键 说明
/word 常用;向光标之下寻找一个名称为 word 的字符串
?word 向光标之上寻找 word
n n 为键盘的 n 按键。代表「重复前一个搜索动作」比如找到多个搜索结果的时候,可以按 n 来跳到下一个下一个
N 大写的 N 按键,与 n 相反
说明 使用 /word 配合 n 或 N 是非常方便的,可以让你重复的找到一些你搜寻的关键词
:n1,n2s/word1/word2/g 常用;n1 与 n2 为数值。在第 n1 与 n2 列之间查找 word1 这个字符串,并将该字符串替换为 word2;比如::100,200s/mrcode/MRCODE/g 就是在 100 到 200 列之间寻找 mrcode 并替换成大写的
1,$s/word1/word2/g 常用;从第一行到最后一行,将 word1 替换成 word2
1,$s/word1/word2/gc 常用;从第一行到最后一行,将 word1 替换为 word2,在替换前,显示字符让用户确认(confirm)是否需要替换

删除、复制、粘贴

按键 说明
x,X 常用;在一行字当中,x 为向后删除一个字符(相当于 del 按键),X 向前删除一个字符
nx n 为数值,连续向后删除 n 个字符
dd 常用;删除光标所在列(这一行文本)
ndd 常用;删除光标所在的向下 n 行,例如 20dd 则是删除 20 行
d1G 删除光标所在到第一行的所有数据
dG 删除光标所在到最后一行的所有数据
d$ 删除光标所在处,到该行最后一个字符
d0 删除光标所在处,到该行最前面一个字符,这个 0 就是数值 0
yy 常用;复制光标所在处的行
nyy 常用;n 为数值,复制光标所在的向下 n 行
y1G 复制光标所在处到第一行的所有数据
yG 复制光标所在处到最后一行的所有数据
y0 复制光标所在处那个字符到该行第一个字符的数据
y$ 复制光标所在处那个字符到该行最后一个字符的数据
p,P 常用p 为将以复制的数据在光标的下一行粘贴上,P 则为贴在光标的上一行。
J 将光标所在行与下一行的数据结合成同一行
c 重复删除多个数据,例如向下删除 10 行,10cj
u 常用;复原前一个动作
ctrl + r 常用;重做上一个动作
说明 uctrl + r 是很常用的执行,一个是复原,一个是重做一次
. 常用;小数点,重复前一个动作。例如先要重复删除、重复粘贴等,按下小数点就可以了

第二部分:一般指令模式切换到编辑模式可用的按键说明

按键 说明
i,I 常用;进入插入模式(insert mode):i 从当前光标所在处插入,I 从当前所在行的第一个非空格符号处插入
a,A 常用;进入插入模式,a 从当前光标所在的下一个字符开始插入,A 从光标所在行的最后一个字符处开始插入
o,O 常用;进入插入模式,o 从当前光标所在的下一行插入新行,O 从光标所在处的上一行插入新的行
r,R 常用;进入取代模式(Replace mode):r 只会取代光标所在的那个字符一次;R 会一直取代光标所在的文字,直到按下 ESC 为止
说明 上面这些按键中,在 vi 画面左下角会出现 —INSERT——REPLACE— 的字样。
Esc 常用;退出编辑模式,回到一般指令模式中

第三部分:一般指令模式切换到指令模式的可用按钮说明

指令列模式的存储、离开等指令

按键 说明
:w 常用;将编辑的数据写入硬盘文件中
:w! 若文件属性为「只读」时,强制写入该文件。不过,到底能不能写入,还是跟你对该文件的文件权限有关
:q 常用;离开 vi
:q! 不想存储,强制离开
说明 !惊叹号在 vi 中,常常具有「强制」的意思
:wq 常用;存储后离开,后面加 !则表示强制存储后离开
ZZ 若文件没有改动,则不存储离开,若文件已经被改动过,则存储后离开
:w[filename] 将编辑的数据存成另一个文件,类似另存为
:r[rilename] 在编辑的数据中,读取另一个文件的数据。即将 filename 文件内容加到光标所在处后面
:n1,n2 w [filename] 将 n1 到 n2 的内容存储成 filename 这个文件。(n 说的是行数把?)
:! command 暂时离开 vi 到指令模式下执行 comman 的显示结果!例如 「:! ls /home」即可再 vi 中查看 /home 下以 ls输出的文件信息;这个笔者感觉很常用,在编辑中往往会忘记路径啥的,通过这个就可以查看了

vi 环境的变更

按键 说明
:set nu 在每一行最前面显示行号
:set nonu 取消行号

特别注意,在 vi 中 「数字」是很有意义的,数字通常代表重复做几次的意思,也有可能是代表去到第几个什么什么的意思。

比如:要删除 50 行,使用 「50dd」;向下移动 20 行,使用 「20j」或「20↓

会上面这些指令就已经很厉害了,因为常用到的指令也只有不到一半,除了上面列举到常用的之外,其他的都不用死记硬背,用到再查询即可

一个案例练习

http://linux.vbird.org/linux_basic/0310vi/man_db.conf 可以使用这个文件来测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
[mrcode@study ~]$ cd /tmp/
[mrcode@study tmp]$ mkdir vitest; cd vitest;
# 开启文件
[mrcode@study vitest]$ vi man_db.conf
# 按 i 进入插入模式,把上面网址里面的内容复制,然后粘贴到这里
# 按 Esc 键,回到一般指令模式,并输入「:wq」保存并退出
# 由于该虚拟机还没有网络,不知道怎么下载文件,上面间接的完成了下载的功能,下面开始练习

# 设置行号
:set nu
# 温馨提示,上面的文件其实比书上的的多一行,就是头尾的 # 少一行,下面的第 43,59 才能看到 as

# 移动到第 43 行,然后向右移动 59 个字符,找到小括号里面的单词
「43G」跳转到第 43 行, 「59→」 可以看到光标跳转到了第 59 个字符后面,能看到 "(as 开头的文案"

# 移动到第 1 行,并且向下搜索「gzip」字符,请问在第几行?
「gg」或则 「1G」跳转到第一行,「/gzip」向下搜索,回车后,会定位到 93,23

# 将 29 到 41 行之间的「小写 man 字符串」改为「大写 MAN 字符串」,并且一个一个挑选是否需要修改
# 如果在挑选过程中一直按 y,结果会在最后一列出现改变了几个 man ?
「:29,41s/man/MAN/gc」会高亮被选中的 man,并且在最下面出现提示是否要替换。这里一直按 y
最后会提示:13 substitutions on 13 lines ,改变了 13 个;
注意的是:高亮是所有的都高亮,但是替换只是在指定的行之间

# 修改之后,突然反悔了,要全部复原,有哪些方法?
1. 简单的方法可以一直按 u 一个一个的撤销刚刚的替换
2. 使用强制不存储离开 「:q!」,之后再读取一次文件

# 复制 66 到 71 这 6 行的内容,(含有 MANDB_MAP),并且贴到最后一行之后
先跳转到 66 行 「66G」,再向下复制 6 行 「6yy」,此时会在指令列中显示 6 lines yanked
再跳转到最后一行「G」光标会定位到第 132 行,使用「p」粘贴到当前光标所在的下一行上
注意:粘贴多行的话,先会粘贴第一行,然后在指令列显示有 6 行需要粘贴,需要手动按下回车键确认粘贴,一次回车粘贴一行。粘贴 6 行后,光标会定位到第 138 行

# 113 行到 128 行之间的开头为 # 号的批注数据不要了,要如何删除?
先跳转到第 113 行:「113G」
再输入 「16dd」删除,其实这里为什么是 16 而不是 128 - 113 = 15,这里面的包含头不包含尾而来的,要注意这个是否包不包含当前一行,在上面的文档中有些描述可能就不太准确

# 将整个文件另存为 man.test.config;上面删除 16 行之后,只剩下 116 行了,待会对比两个文件
使用指令 「:w man.test.config」
会在指令列提示:"man.test.config" [New] 116L, 4862C written
如果使用 「:wq! man.test.config」 则你没有机会看到上述的提示,另存后就强制退出当前文件了

特别注意:使用 :w man.test.config 指令后,可以直接强制退出当前的文件,因为当前还在 man_db.config 中,强制退出的话,刚才删除的操作等都不会写到 man_db.config 文件中去,而另存里面的文件却保存了刚刚删除等操作后的数据

# 去到第 25 行,并删除 15 个字符,结果出现的第一个单字是什么?
「25G」然后「15x」看到留下的字符串是「_to MANPATH mapping」,下划线是光标所在处

# 在第一行新增一行,该内容输入 「I am a student...」
「gg」,再按「O」大写的 o 会在光标所在处的上一行插入新行

# 存储后离开
:wq

上面的练习部分比如删除字符等,与书上的部分内容对不上,我想可能是因为整个文件内容就对不上的原因

vim 的暂存档、救援恢复与开启时的警告讯息

在你编辑过程中,突然宕机等情况下,在你还诶呦保存的时候,可能就想要是能恢复下刚刚未保存的数据就好了

那么 vim 就提供了这样的功能,是通过暂存档来实现的。在使用 vim 编辑时,会在被编辑的文件目录下,再建立一个名为 .filename.swp 的文件,编辑的数据会被存在该文件中。

来测试这个恢复功能(注:下面的部分指令,现在还未讲解,后续讲解后,再回头来这里练习下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
[mrcode@study vitest]$ cd /tmp/vitest/
[mrcode@study vitest]$ vim man_db.conf
# 使用 vim 进入文件后,然后按 ctrl + z 组合键,会退出来,并提示下面的信息
# 该组合键的作用是吧 vim man_db.conf 丢到背景去执行(后续在 16 章程序管理中会讲解)
[1]+ Stopped vim man_db.conf

# 找到 .swp 的文件
[mrcode@study vitest]$ ls -al
total 48
drwxrwxr-x. 2 mrcode mrcode 96 Oct 29 18:27 .
drwxrwxrwt. 54 root root 4096 Oct 29 18:26 ..
-rw-rw-r--. 1 mrcode mrcode 4862 Oct 29 17:13 man_db.conf
-rw-r--r--. 1 mrcode mrcode 16384 Oct 29 18:27 .man_db.conf.swp

# 仿真断线停止 vim 工作
# 现在可以暂时理解为宕机了
[mrcode@study vitest]$ kill -9 %1

[1]+ Stopped vim man_db.conf
# swp 文件还存在
[mrcode@study vitest]$ ls -al .man_db.conf.swp
-rw-r--r--. 1 mrcode mrcode 16384 Oct 29 18:27 .man_db.conf.swp
[1]+ Killed vim man_db.conf

# 再次进入该文件
[mrcode@study vitest]$ vim man_db.conf
E325: ATTENTION # 错误代码
Found a swap file by the name ".man_db.conf.swp" # 有暂存当的存在,并显示相关信息
owned by: mrcode dated: Tue Oct 29 18:27:34 2019
file name: /tmp/vitest/man_db.conf # 这个暂存当实际属于哪个文件
modified: no
user name: mrcode host name: study.centos.mrcode
process ID: 2259
While opening file "man_db.conf"
dated: Tue Oct 29 17:13:28 2019

# 下面说明可能发生这个错误的两个主要原因与解决方案
(1) Another program may be editing the same file. If this is the case,
be careful not to end up with two different instances of the same
file when making changes. Quit, or continue with caution.
(2) An edit session for this file crashed.
If this is the case, use ":recover" or "vim -r man_db.conf"
to recover the changes (see ":help recovery").
If you did this already, delete the swap file ".man_db.conf.swp"
to avoid this message.

# 下面说明你可以进行的动作
Swap file ".man_db.conf.swp" already exists!
[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort:

上面翻译成中文有如下的主要信息:由于暂存文件的存在,vim 会主动判断你的这个文件可能有些问题,上面列出的两个主要原因与解决方案翻译如下:

  1. 可能有其他人或程序同时在编辑这个文件

    • 找到另外那个程序或人员,请他将该 vim 的工作结束,然后你再继续处理
    • 如果只是想要看该文件的内容并不会有任何修改编辑的行为,那么可以选择开启成为只读文件(O),就是那个 [o] pen Read-Only 选项
  2. 在前一个 vim 环境中,可能因为某些不知名的原因导致 vim 中断(crashed

    这就是常见的不正常结束 vim 产生的后果。解决方案依据不同的情况不同,常见的处理方法为:

    • 如果之前的 vim 处理动作尚未存储,此时应该按下 R (使用 (R)ecover 选项),此时 vim 会载入 .man_db.conf.swp 的内容,让你自己来决定要不要存储!不过需要你离开 vim 后手动删除 .man_db.conf.swp 文件,避免下次打开还出现这样的警告
    • 如果你确定这个暂存文件是没有用的,可以直接按下 D(**(D)elete it**)删除它

下面是出现的 6 个选项的说明:

  • [O]pen Read-Only:以只读方式打开。不能编辑
  • (E)dit anyway:以正常方式打开文件,不会载入暂存文件中的内容。不过很容易出现两个使用者互相改变对方的文件等问题。不推荐(如果是多人编辑的情况下)
  • (R)ecover:加载暂存文件的内容,用在恢复之前未保存的内容,恢复之后记得手动删除暂存文件
  • (D)elete it:确定暂存文件是无用的,删除它
  • (Q)uit:离开 vim,不会进行任何动作
  • (A)bort:忽略这个编辑行为,感觉上与 quit 非常类似。

vim 额外功能

其实,目前大部分的distribution 都以 vim 取代 vi 的功能了,因为 vim 具有颜色显示、支持许多程序语法(syntax)等功能

那么怎么分辨是否当前 vivim 取代了呢?

通过 alias 分辨

1
2
3
4
5
6
7
8
9
10
11
[mrcode@study vitest]$ alias
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l.='ls -d .* --color=auto'
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
alias vi='vim' # 可以看到这里 vi 调用的就是 vim
alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
# 原来上一个章节,笔者使用的不是 vi 而是 vim

通过界面分布

区块选择(Visual Block)

上面提到的简单 vi 操作过程中,几乎提到的都是以行为单位来操作的。那么如果想要搞定一个区块范围呢?如下面这个文件内容

1
2
3
4
5
6
7
8
9
10
192.168.1.1    host1.class.net
192.168.1.2 host2.class.net
192.168.1.3 host3.class.net
192.168.1.4 host4.class.net
192.168.1.5 host5.class.net
192.168.1.6 host6.class.net
192.168.1.7 host7.class.net
192.168.1.8 host8.class.net
192.168.1.9 host9.class.net

假设想要将 host1host2 等复制,并且加到每一行的后面,即每一行的结果变成 192.168.1.1 host1.class.net host2.class.net... 。在传统或现代的窗口型编辑器似乎不容易达到这个需求,在 vim 中可以使用 Visual Block 区块功能。当按下 vV 或则 ctrl+v 时,光标移动过的地方就会开始反白,按键含义如下

按键 含义
v 字符选择,会将光标经过的地方反白选择
V 行选择,会将光标经过的行反白选择
ctrl + v 区块选择,可以用长方形的方式选择
y 将反白的地方复制起来
d 将反白的地方删除
p 将刚刚复制的区块,在光标所在处贴上

实践练习区块怎么使用

多文件编辑

想象这样一个场景:要将刚刚 host 内的 IP 复制到 /etc/hosts 这个文件去,那么该如何编辑?我们知道在 vi 内可以使用 :r filename 来读入某个文件的内容,不过是将整个文件读入,如果只想要部分内容呢?这个时候就可以使用 vim 的多文件编辑功能了。使用 vim 后面同时接好几个文件来同时开启,相关按键有

按键 含义
:n 编辑下一个文件
:N 编辑上一个文件
:files 列出目前这个 vim 开启的所有文件

没有多文件编辑的话,实现将 A 文件内的 10 条消息移动到 B 文件中,通常需要开两个 vim 窗口来复制,但是无法在 A 文件下达 nyy 再跑到 B 文件去 p 的指令。

练习多文件编辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 开启两个文件,host 是我们刚刚编辑的那个
vi host /etc/hosts
# 使用 files 指令查看编辑的文件有哪些
# 在一般指令模式下输入 :files 指令
:files
1 %a "host" line 1
2 "/etc/hosts" line 0
Press ENTER or type command to continue
# 上面列出了两个,并告知按下任意键会回到 vim 的一般指令模式中

1. 回到一般指令模式中,跳转到第一行,输入 4yy 复制 4 行数据
2. 输入 :n 会来到第二个编辑的文件,也就是 /etc/hosts
3. 按下 G 跳转到最后一行,再按 p 贴上 4 行数据
4. 按下多次 u 来取消刚才的操作,也就是恢复 /etc/hosts 中数据到原样
5. 最终按下 :q 离开 vim 编辑器

多窗口功能

在开始逐个小节前,先来想象两个情况:

  1. 当我有一个文件非常的大,查阅到后面的数据时,想要对照前面的数据,是否需要使用 ctrl + fctrl + bpageuppagedown 功能键来前后翻页对照?
  2. 我有两个需要对照看的文件,不想使用前一小节提到的多文件编辑功能

vim 有「分区窗口」的功能,在指令行模式输入 :sp filename即可,filename 存在则开启另一个文件,不存在则出现的是相同的文件内容

使用 vim /etc/man_db.conf,然后输入 「**:sp**」就会出现上下各一个窗口,两个窗口都是同一个文件内容

再次输入 :sp /etc/hosts 则会再分出来一个窗口

可以使用 ctrl + w + ↑ 和 ctrl + w + ↓ 组合键来切换窗口(笔者测试使用 ctrl + w 可以切换 ctrl + w + 箭头触发了宿主机的 ui 切换功能)

多窗口情况下的按键功能

按键 说明
:sp [filename] 开启一个新窗口,不加 filename 则默认打开当前文件,否则打开指定文件
ctrl + w + j/↓ 使用方法:先按下 ctrl 不放,再按下 w 后放开所有的按键,再按下 j 或向下的箭头键,则光标可移动到下方的窗口
ctrl + w + k/上 同上
ctrl + w + q 就是 :q 结束离开。比如:想要结束下方的窗口,先使用 ctrl + w + j 移动到下方窗口,输入 :q 或则按下 ctrl + w + q 离开

vim 的挑字补全功能

我们知道在 bash 环境下可以按下 tab 按钮来达成指令、参数、文件名的补全功能,还有 windows 系统上的各种程序编辑器,如 **notepad++**,都会提供:语法检验和根据扩展名来挑字的功能。

在语法检验方面,vim 已经使用颜色来达成了,建议可以记忆的 vim 补齐功能如下:

组合按键 补齐内容
ctrl + x -> ctrl +n 通过目前正在编辑的这个「文件的内容文件」作为关键词,补齐;
ctrl + x -> ctr + f 以当前目录内的「文件名」作为关键词,予以补齐
ctrl + x -> ctrl + o 以扩展名作为语法补充,以 vim 内置的关键词,予以补齐

用法:先输入关键词如 host 再按 ctrl + x,再按 ctrl + n,如果有可补齐的待选文案,会显示下拉列表给你选择

实践练习:使用 css 美化功能时,突然想到有个北京的东西要处理,但是忘记了背景 CSS 关键语法,就可以用如下的模式来处理

1
2
# 一定要是 .html 否则不会使用正确的语法检验功能
vi htmltest.html

vim 环境设置与记录:~/.vimrc~/.viminfo

有没有发现:如果以 vim 软件来搜寻一个文件内部的某个字符串时,这个字符串会被反白,而下次我们再次以 vim 编辑这个文件时,该搜索的字符串反白的情况还是存在的,甚至于在编辑其他文件时,如果也存在该字符,也会主动反白。另外,当我们重复编辑同一个文件时,当第二次进入该文件时,光标竟然在上次离开的那一行上面

这是因为 vim 会主动将你曾经做过的行为记录在 ~/.viminfo 文件中,方便你下次可以轻松作业

此外,每个 distributionvim 的预设环境都不太相同,例如:某些版本在搜寻关键词时并不会高亮度反白,有些版本则会主动帮你进行缩排的行为。这些其实都可以自定设置的,vim 的环境设置参数有很多,可以在一般模式下输入「**:set all**」来查询,不过可设置的项目太多了,这里仅列出一些平时比较常用的一些简单设置值,供你参考

vim 的环境设置参数

item 含义
:set nu、**:set nonu** 设置与取消行号
:set hlsearch、:set nohlsearch hlsearch 是 high light search (高亮度搜索)。设置是否将搜索到的字符串反白设置。默认为 hlsearch
:set autoindent、**:set noautoindent** 是否自动缩排?当你按下 Enter 编辑新的一行时,光标不会在行首,而是在于上一行第一个非空格符处对齐
:set backup 是否自动存储备份文件,一般是 nobackup 的,如果设置为 backup,那么当你更改任何一个文件时,则源文件会被另存一个文件名为 filename~ 的文件。如:编辑 hosts,设置 :set backup ,那么修改 hosts 时,在同目录下就会产生 hosts~ 的文件
:set ruler 右下角的状态栏说明,是否显示或不显示该状态的显示
:set shwmode 是否要显示 —INSERT– 之类的提示在左下角的状态栏
:set backpace=(012) 一般来说,如果我们按下 i 进入编辑模式后,可以利用退格键(baskpace)来删除任意字符的。但是某些 distribution 则不允许如此。此时,可以通过 backpace 来设置,值为 2 时,可以删除任意值;0 或 1 时,仅可删除刚刚输入的字符,而无法删除原本就已经存在的文字
:set all 显示目前所有的环境参数设置
:syntax on:syntax off 是否依据程序相关语法显示不同颜色
:set bg=dark、**:set bg=light** 可以显示不同颜色色调,预设是 light。如果你常常发现批注的字体深蓝色是在很不容易看,就可以设置为 dark

总之这些常用的设置非常有用处,但是在行模式下设置只是针对当前打开的 vim 有效果;想要修改默认打开就生效的话,可以修改 ~/.vimrc 这个文件来达到(如果此文件不存在,请手工创建)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vi ~/.vimrc
" 该文件的注释是使用双引号表达
set hlsearch "高亮度反白
set backspace=2 "可随时用退格键删除
set autoindent "自动缩进
set ruler "可现实最后一列的状态
set showmode "左下角那一列的状态
set nu "在每一行的最前面显示行号
set bg=dark "显示不同的底色色调
syntax on "进行语法检验,颜色显示


# 保存后,再次打开最明显的就是自动显示行号了,可见是生效了的

vim 常用指令示意图


其他 vim 使用注意事项

vim 功能很强大,但是上手不是那么容易,下面分享一些需要注意地方

中文编码的问题

vim 里面无法显示中文,那么你需要考虑:

  1. Linux 系统默认支持的语系数据,与 /etc/locale.conf 有关
  2. 终端界面(bash)的语系;与 LANGLC_ALL 几个参数有关
  3. 文件原本的编码
  4. 开机终端机的软件,例如在 GNOME 下的窗口

上面最重要的是第 3 和 4 点,只要这两点编码一致,就能不乱码;

可以使用如下的方式来暂时修改 tty 的语系(前面都讲过的)

1
2
LANG=zh_CN.UTF-8
export LC_ALL=zh_CN.UTF-8

DOS 与 Linux 的断行字符

cat 命令 中讲解过 DOS(windows 系统)建立的文件的特殊格式,发现 DOS 为 ^M$,而 linux 是 $,windows 是 CR(^M) 与 LF($) 两个符号组成的,Linux 是 LF ;对于 Linux 的影响很大

在 Linux 指令开始执行的时候,判断依据是 Enter 按键(也就是换行符,回车一下就会出现换行符),由于两个系统的换行符不一致,会导致 shell script 程序文件无法执行

可以使用 dos2unix 指令来一键转换,但是目前为止,虚拟机还没有网络,无法安装,笔者这里只记录用法

1
2
3
4
5
6
dos2unix [-kn] file [newfile]
unix2dos [-kn] file [newfile]

-k:保留该文件原本的 mtime 时间格式(不更新文件上次内容经过修订的时间)
-n:保留原本的旧文件,将转换后的内容输出到新文件,如:dos2unix -n old new

练习

1
2
3
4
5
6
7
8
9
10
11
12
# 将 /etc/man_db_conf 重新复制到 /tmp/vitest 下,并将其修改为 dos 断行
cd /tmp/vitest
cp -a /etc/man_db_conf .
ll man_db.conf
unix2dos -k man_db.conf

# 将上述的 man_db.conf 转成 linux 换行符,并保留旧文件,新文件防御 man_db.conf.linux
dos2unix -k -n man_db.conf man_db.conf.linux
ll man_db.conf*
file man_db.conf*
man_db.conf: ASCII text,with CRLF line terminators # 说明了是 CRLF 换行
man_db.conf.linux: ASCII text

语系编码转换

文件编码转换,可以使用 iconv 指令来做,比如下面这一段文字内容(没有网络下载不了,直接粘贴复制保存把)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
每個系統管理員都應該至少要學會一種文字介面的文書處理器,以方便系統日常的管理行為。
在 Linux 上頭的文書處理軟體非常的多,不過,鳥哥還是建議使用 vi 這個正規的文書處理器。
這是因為 vi 幾乎在任何一個 Unix Like 的機器都存在,學會他,輕鬆很多啊!
而且後來的計畫也有推出 vim 這個 vi 的進階版本,可以用的額外功能更多了!
vi 是未來我們進行 shell script 程式的編寫與伺服器設定的重要工具喔!
而且是非常非常重要的工具,一定要學會才行啊! ^_^

無論如何,要管理好 Linux 系統時,純文字的手工設定仍是需要的!那麼在 Linux 底下有哪些文書編輯器呢?
可多了~例如 vi, emacs, xemacs, joe, e3, xedit, kedit, pico .... 多的很~
各家處理器各有其優缺點,您當然可以選擇任何一個您覺得適用的文書處理器來使用。不過,鳥哥還是比較建議使用 vi
啦!這是因為 vi 是 Unix Like 的機器上面預設都有安裝的軟體,也就是說,您一定可以接觸到這個軟體就是了。
另外,在較新的 distributions 上,您也可以使用較新較先進的 vim 這個文書處理器!
vim 可以看做是 vi 的進階軟體,他可以具有顏色顯示,很方便程式開發人員在進行程式的撰寫呢!

簡單的來說, vi 是老式的文書處理器,不過功能已經很齊全了,但是還是有可以進步的地方。
vim 則可以說是程式開發者的一項很好用的工具,就連 vim 的官方網站 (http://www.vim.org)
自己也說 vim 是一個『程式開發工具』而不是文書處理軟體~^_^。
因為 vim 裡面加入了很多額外的功能,例如支援正規表示法的搜尋架構、多檔案編輯、區塊複製等等。
這對於我們在 Linux 上面進行一些設定檔的修訂工作時,是很棒的一項功能呢!

底下鳥哥會先就簡單的 vi 做個介紹,然後再跟大家報告一下 vim 的額外功能與用法呢!


1
2
3
4
[root@study ~]# cd /tmp/
[root@study tmp]# vi big5.txt
# 把上面的内容保存到该文件中,然后使用这个文件来练习

语法

1
2
3
4
5
6
7
8
iconv --list
iconv -f 原本编码 -t 新编码 filename [-o newfile]

--list:列出 iconv 支持的语系数据
-f:from 来源,原本的编码格式
-t:to,即要转换的编码格式
-o file:如果要保留原本的文件,使用 -o 新文件名,可以建立新编码文件

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看原本文件编码,这里由于刚刚终端机是 utf-8 的,保存后就是 utf-8 了
[root@study tmp]# file big5.txt
big5.txt: UTF-8 Unicode text
# 那么将 UTF8 转成 big5
[root@study tmp]# iconv -f utf8 -t big5 big5.txt -o big5.txt.big5
[root@study tmp]# file big5.*
big5.txt: UTF-8 Unicode text
big5.txt.big5: ISO-8859 text
# 不过笔者测试,由 utf8 转成 big5 会乱码,但是还可以转回来

# 下面的指令可以将上面繁体中文,转为简体中文,并且还是 utf8 格式
# 看指令是吧 utf8 转成 big5,再转成 gb2312,再转成 utf8
[root@study tmp]# iconv -f utf8 -t big5 big5.txt | \
> iconv -f big5 -t gb2312 | iconv -f gb2312 -t utf8 -o vi.gb.utf8

认识与学习 BASH

Linux 环境下,如果你不懂 bash 是什么,那么其他的东西就不用学习了,所以 bash 非常重要

bash 的东西非常多:变量的设置与使用、bash 操作环境的建立、数据流重导向功能、管线命令 等

本章几乎是所有指令模式(commadn line)与未来主机维护与管理的重要基础。

认识 BASH 这个 Shell

在第一章讲到:管理整个计算机硬件的其实是操作系统的核心(kernel),一般使用者只能通过 shell 来与核心沟通。那么有系统有多少 shell 可用呢?以及为什么要使用 bash?本章告诉你答案

硬件、核心与 Shell

什么是 Shell?几乎上都听听到过,因为只要有操作通,那么就离不开 Shell 这个东西。在讨论 Shell之前,先来了解一下计算机的运作状况。举个例子:当你要计算机传输出来「音乐」的时候,你的计算机需要什么东西呢?

  1. 硬件:有「声卡芯片」设备,才能发声
  2. 核心管理:操作系统支持这个芯片组,以及提供芯片的驱动程序
  3. 应用程序:需要使用者(就是你)输入发生声音的指令

这就是基本的一个输出声音所需要的步骤,可以用如下图示来说明:

在第 0 章的操作系统章节曾提到过:操作系统其实是一组软件,控制整个硬件与管理系统的活动检测,如果这组软件能被用户随意的操作,若使用者应用不当,将会使得整个系统崩溃!所以不能随便被一些没有管理能力的终端用户随意使用

但是可以考虑使用程序来指挥核心,在第 0 章所提供的操作系统图示中,可以发现应用程序其实是在最外层,就如同鸡蛋的外壳一样,因此也就被称呼为壳程序(Shell

其实壳程序的功能只是提供用户操作系统的一个接口,因此整个壳程序需要可以呼叫其他软件的功能,如前面提到过的很多指令,包括 manchmodchownvifdiskmkfs 等指令,这些指令都是独立的应用程序,但是可以通过壳程序(指令行模式)来操作这些应用程序

也就是说,只要能够操作应用程序的接口都能够成为壳程序。狭义的壳程序指的是指令方面的软件,包括本章要介绍的 bash 等。广义的壳程序包括图形界面软件。

为何要学习文字接口的 shell?

文字接口的 shell 不好学,但是学了之后好处多多,因此需要克服这个困难

文件接口的 shell:大家都一样

为什么要学习 shellx window 下的 ui 工具点一点就可以达到目的,比如 Webmin 是真的好用,他可以帮我很建议的设置我们的主机,甚至是一些很进阶的设置都可以帮我们搞定

但是这里还是需要再强调下:x windowweb 接口的工具,它虽然功能强大,只是把所有利用到的软件整合到一起的一组程序而已,并非一个完整的套件,所以某些当你升级或则是使用其他套件管理模块(如 tarball 而非 npm 文件等)时,就会造成设置的困扰了。甚至不同的 distribution 所设计的 x window 接口也都不相同,这样也造成学习方面的困扰

而几乎各家 distribution 使用的 bash 都是一样的,如此一来几乎上能够轻轻松松的转换不同的 distribution

远程管理:文字接口就是比较快

Linux 的管理常常需要通过远程联机,而联机时文字接口的传输速度一定比较快,而且不容易出现断线或则是信息外流的问题,因此 shell 真的是得学习的一项工具,而且会让你更深入 Linux

Linux 的任督二脉:shell

所谓技多不压身,书到用时方恨少。此外,如果你真的有心要将你的主机管理好,那么良好的 shell 程序编写是一定需要的!

例如作者的经验来说,管理的主机不到十台,但是如果每台书籍都要花上几十分钟来查询他的登录文件信息以及相关的信息,可能会疯掉,太没有效率。但是通过 shell 提供的数据流重导向以及管线命令,分析登录信息只要花费不到十分钟就可以看完所有的主机的重要信息了

由于学习 shell 的好处真的多多,想要管理好系统的话,shell 就像是打通任督二脉一样,任何武功都能随你应用

系统的合法 shell/etc/shells 功能

由于早年 Unix 年代,发展众多,所以 shell 依据发展者的不同就有许多版本,例如 Bourne SHellsh)、在 Sun 里头预设的 CSHell、商业上常用的 K ShellTCSH 等,每一种 Shell 都各有其特点。而 Linux 使用的这一种版本就称为「Bourne Again SHell(简称 bash),是 Bourne Shell 的增强版,也是基于 GNU 的架构下发展出来的

shell 简单历史

第一个流行的 shell 是由 Steven Bourne 发展出来的,所以称为 Bourne shell(简称 sh)。后来另一个广泛流传的 shell 是由柏克莱大学的 Bill Joy 设计依附于 BSD 版的 Unix 系统中的 shell,该 shell 语法类似 c 语言,所以才得名为 C shell(简称 csh)。 Sun 主要是 BSD 的分支之一,而且 Sun 主机势力庞大,所以 csh 流传广泛

目前 Linux 可以使用的 shells

CentOS 7 为例,有多少我们可以使用的 shells 可以通过检查 /etc/shells 文件,至少有以下几个

  • /bin/sh:已被 /bin/bash 所取代
  • /bin/bashLinux 预设的 shell
  • /bin/tcsh:整合 C Shell 提供更多的功能
  • /bin/csh:已被 /bin/tcsh 取代

虽然各家 shell 的功能都差不多,但是在某些语法下达方面则所有不同,因此建议需要选择一种 shell 来熟悉。Linux 预设就是使用的 bash,因此最初学会它就非常了不起了。

为什么系统上合法的 shell 要写入 /etc/shells 这个文件?因为系统某些服务在运行过程中,回去检查使用者能够使用的 shells

举例来说:某些 FTP 网站回去检查使用者的可用 shell,而如果你不想让这些用户使用 FTP 以外的主机资源时,可能会给予该使用者一些怪怪的 shell,让使用者无法以其他服务登录主机。这个时候,你就可以将那些怪怪的 shell 写到 /etc/shells 中。举例来说, CentOS 7/etc/shells 里头就有个 /sbin/nologin 文件的存在,这个就是我们说的怪怪的 shell

我这个使用者上面时候可以取得 shell 来工作呢?还有预设会取得哪一个 shell 呢?在登录终端的时候,系统就会给一个 shell 进行工作,而这个登录取得的 shell 就记录在 /etc/passwd 文件内

1
2
3
4
5
6
7
8
[mrcode@study ~]$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
...

如上所示,每一行的最后一个数据,就是你登录后可以取得的预设 shell,系统账户 bindaemon 等就是使用哪个怪怪的 /sbin/nologin ,关于使用者这部分的内容,在后续 第十三章的账户管理中讲解

Bash shell 的功能

Linux 预设的 /bin/bashGNU 计划中重要的工具软件之一,目前也是 Linux distribution 的标准 shell,主要兼容于 sh,并且依据一些使用者需求加强的 shell 版本。主要有点有如下几个

命令编修能力(history)

能记录使用过的指令,只要在指令列按「上下键」可以浏览历史记录,默认的指令记忆条数可达 1000 个。

指令记录在你的家目录内的 .bash_history ,该文件记录的是前一次登录以前所执行过的指令,而当前这一次的指令被暂存在内存中,当你成功注销系统后,指令记录才会存入该文件中

这种工作机制的优点:最大好处可以查询曾经做过的举动,如此可以知道你的执行步骤,那么就可以追踪你曾经下达过的指令,以作为除错的重要流程,但是如果被黑客入侵,只要翻阅你曾经执行过的指令,刚好你的指令又与系统有关(比如登录 mysql 的密码在指令列上),那么很容易数据库密码就被泄露了

那么该功能和历史记录数是好是坏?只能是仁者见仁智者见智了

命令与文件补全功能(tab 按键的好处)

bash 中常常使用 tab 补全功能,可以让你效率提升,并且减少输入时数据错误的几率,

  • 命令补全:tab 接在一串指令的第一个字的后面
  • 文件补全:tab 接在一串指令的第二个字以后时
  • 若安装 bash-completion 软件,则在某些指令后面使用 tab 按键时,可以进行「选项**/**参数的补齐」功能

命令别名设置功能(alias)

假如我需要知道这个目录下的所有文件(包含隐藏文件)以及所有的文件属性,那么必须下达 ls -al这样的指令,可以通过 alias 来自定义命令取代上面的命令

1
2
alias lm='ls -al'
# 这里使用 lm 取代了 ls -al

工作控制、前景背景控制(job control、foreground、background)

这部分在 第十六章 Linux 过程控制中详细讲解。使用前、背景可以让工作进行得更为顺利,而工作控制(jobs)用途则更广,可以让我们随时将工作丢到背景中执行,而不怕不小心使用了 ctrl + c 来停掉该程序。此外,可以在单一登录的环境中,达到多任务的目的

程序化脚本(shell scripts)

DOS 年代将一堆指令写在一起的批处理文件,在 Linux 下的 shell scripts 则发挥更强大的功能,可以将你平时管理系统常需要下达的连续指令写成一个文件,该文件并且可以通过对谈交互式的方式来进行主机的侦测工作。也可以借由 shell 提供的环境变量及相关指令来进行设计,以前在 DOS 下需要程序语言才能写的东西,在 Linux 下使用简单的 shell scripts 就可以实现,这部分在 第十二章 讲解

通配符(wildcard)

举例来说:想要知道 /usr/bin 下有多少以 X 开头的文件,使用ls -l /usr/bin/X* 就可以知道,此外还有其他可用的通配符

查询指令是否为 Bash shell 的内置命令:type

可以通过 man bash 查看联机帮助文档,内容很多,让你看几天几夜也无法看完,不过该 bashman page 中,还有其他文件的说明,比如 cd 指令也在该 man page 内。在输入 man cd 时,最上方也出现一堆的指令介绍,这是由于方便 shell 的操作内置了这些指令

可以通过 type 指令来观察某个指令是否是内置指令

1
2
3
4
5
6
7
8
9
10
type [-tpa] name

- 不加任何选项与参数时,type 会显示出 name 是外部指令还是 bash 内置指令
- t:type 会将 name 以下面这些字眼显示出他的意义
file:表示为外部指令
alias:为别名
builtin:bash 内置指令
- p:如果后面接的 name 为外部指令时,才会显示完整文件名
- a:根据 PATH 变量定义的路径中,将含有 name 的指令都列出来,包含 alias

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 查询 ls 这个指令是否为 bash 内置
# 未加任何参数,列出 ls 的最主要使用情况
[mrcode@study ~]$ type ls
ls is aliased to `ls --color=auto'
# 仅列出执行时的依据
[mrcode@study ~]$ type -t ls
alias
[mrcode@study ~]$ type -a ls
ls is aliased to `ls --color=auto' # 最先使用 aliased
ls is /usr/bin/ls # 还找到外部指令在 /bin/ls

# 查看 cd 的情况
[mrcode@study ~]$ type cd
cd is a shell builtin # cd 是 shell 内置指令

指令的下达与快速编辑按钮

前面讲过怎么下达指令,这里仅以反斜杠来说明下指令下达方式

1
2
3
4
# 如果指令串太长的话,如何使用两行来输出
[mrcode@study ~]$ cp /var/spool/mail/root /etc/crontab \
> /etc/fstab /root

使用 \ 来跳脱回车键,前面的 > 是跳脱模式下的标识符,还有组合按键帮助我们快速实现功能

组合键 功能与示范
ctrl + uctrl + k 快速删除:分别是从光标处向前删除指令串,及向后删除指令串
ctrl + a、ctrl + e 快速移动:分别是让光标移动到整个指令串的最前面和最后面

总之,当我们顺利的在终端机 tty 上面登录后,Linux 就会依据 /etc/passwd 文件的设置给我一个 shell(预设是 bash),可以通过 man 来查询指令的使用方式,根据上面下达指令的方式来操作 shell

Shell 的变量功能

变量是 bash 环境中非常重要的一个概念

什么是变量

简单说,某一个特定字符串代表不固定的内容;比如:y = ax+b 等号左边的是变量,右边的是变量的内容,使用简单的变量来取代另一个比较复杂或则是容易变动的数据,这样做的好处就是方便!

变数的可变性与方便性

举例来说,我们每个账户的邮件信箱预设是以 MAIL 这个变量来进行存取的,当不同的账户登录取得的变量内容如下所示

1
2
3
dmtsai 的 MAIL = /var/spool/mail/dmtsai 
root 的 MAIL = /var/spool/mail/root
mrcode 的 MAIL = /var/spool/mail/mrcode

好处是则是邮件处理程序读取 MAIL 变量就能为对应的账户处理了

影响 bash 环境操作的变量

某些特定变量会影响到 bash 的环境,例如前面多次提到的 PATH 变量,它会影响指令是否能找到。

为了区别与自定义变量的不同,环境变量通常以大写字符来表示

脚本程序设计(shell script)的好帮手

写过程序的都知道,变量在程序中的重要性,比如在 shell script 中,前面几行定义变量,后面的大量逻辑处理使用变量,那么修改变量的内容,就能让后续的处理逻辑改变,达到非常方便的效果

变量的取用与设置:echo 、变量设置规则、unset

变量的取用:echo

1
2
3
4
echo $variable
echo $PATH
echo ${PATH} # 作者推荐使用这种方式取用

关于 echo 的功能也较多,自行 man echo,这只是用来显示变量内容

1
2
3
4
5
6
# 在屏幕上显示你的环境变量 HOME 与 MAIL
[mrcode@study ~]$ echo $HOME
/home/mrcode
[mrcode@study ~]$ echo ${MAIL}
/var/spool/mail/mrcode

变量的修改使用等号赋值

1
2
3
4
5
6
7
8
[mrcode@study ~]$ echo ${myname}
# 这里没有任何数据,该变量不存在或未设置
[mrcode@study ~]$ myname=mrcode
[mrcode@study ~]$ echo ${myname}
mrcode

# 在 bash 中,当一个变量名称未被设置时,预设内容就是 空

需要注意的是:每一种 shell 的语法都不相同,在 bashecho 一个不存在的变量不会保存,并显示空,其他的可能就会报错了

变量的设置规则

  • 变量与变量内容以一个「**=**」来连接

    1
    myname=Mrcode
  • 等号两边不能直接接空格符号

    1
    myname = Mrcode		# java 语法格式强迫症不要这样写
  • 变量名称只能是英文字母与数字

  • 变量内容若有空格可以使用双引号或单引号限定,但是以下除外

    • 「**$**」在双引号中可以保留原本的特性

      1
      2
      var="lang is $LANG"
      则使用 echo $var 则得到输出信息为:lang is utf8 等的字样
    • 」在单引号内的特殊字符仅为一般字符

      1
      2
      var='lang is $LANG'
      则输出信息为:lang is $LANG
  • 可用跳脱字符「\」把特殊字符变成一般字符

    1
    2
    # 就是转义符
    myname=mrcode\ Tsai # 这里将空格转义成普通字符了
  • 在一串指令的执行中,还需要使用其他额外的指令所提供的信息时,可以使用反单引号「指令」或「**$(指令)**」

    1
    2
    3
    4
    5
    6
    # 取得核心版本的设置
    [mrcode@study ~]$ echo $version

    [mrcode@study ~]$ version=$(uname -r); echo $version
    3.10.0-1062.el7.x86_64

  • 若该变量为扩增变量内容时,则可使用如下方式累加变量

    1
    2
    3
    PATH="$PATH:/home/bin"
    PATH=${PATH}:/home/bin

  • 若该变量需要再其他子程序执行,则需要以 export 来使变量变成环境变量

    1
    2
    export PATH

  • 通常大写字符为系统默认变量,自行设定变量可以使用小写字符,方便判断(纯粹按个人风格决定)

  • 取消变量使用 unset

    1
    2
    # 如取消 myname 的设置
    unset myname

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 练习 1:设置变量 name,内容为Mrcode
[mrcode@study ~]$ 12name=mrcode # 变量命名语法问题
bash: 12name=mrcode: command not found...
[mrcode@study ~]$ name = Mrcode
bash: name: command not found... # 变量命名语法问题
[mrcode@study ~]$ name=Mrcode # 成功

# 练习 2:接上面,把内容修改为 Mrcode's name,就是内容中含有特殊字符
[mrcode@study ~]$ name=Mrcode's name
> c^C
# 单引号与双引号必须要成对出现,上面只有一个单引号,按下回车键时,还可以继续输入内容
# 不能达到题目要求,记得使用 ctrl + c 结束
[mrcode@study ~]$ name="Mrcode's name"
# 在双引号中,单引号变为一般字符
[mrcode@study ~]$ name=Mrcode\'s\ name
#使用转义符转义特殊字符

# 练习 3: 在 PATH 变量中累加 /home/dmtsai/bin 这个目录
PATH=$PATH:/home/dmtsai/bin
PATH="$PATH":/home/dmtsai/bin
PATH=${PATH}:/home/dmtsai/bin
# 上面三种格式都可以,但是下面的例子就不见得了

# 练习 4:将 name 的内容多出 yes
[mrcode@study ~]$ echo $name
Mrcode's name
[mrcode@study ~]$ name=$nameyes
[mrcode@study ~]$ echo $name

[mrcode@study ~]$
# 如没有双引号或则 {} 则完全变成了一个变量 nameyes
# 正确的如下
name="$name"yes
name=${name}yes

# 练习 5:如何让我刚刚设置的 name=Mrcode 可以用在下个 shell 程序?
[mrcode@study ~]$ name=Mrcode
[mrcode@study ~]$ bash # 进入所谓的子程序
[mrcode@study ~]$ echo $name
# 这里并没有获取到刚刚设置的值
[mrcode@study ~]$ exit # 退出子程序
exit
[mrcode@study ~]$ echo $name
Mrcode
[mrcode@study ~]$ export name # 导出变量
[mrcode@study ~]$ bash
[mrcode@study ~]$ echo $name # 在子程序中找到了
Mrcode
[mrcode@study ~]$ exit
exit

什么是子程序?像上面那样,在当前这个 shell 下,去启用另一个新的 shell,新的哪个 shell 就是子程序了。在一般的状态下,父程序的自定义变量是无法在子程序内使用的。可以通过 export 将变量变成环境变量,就可以在子程序中使用了。

至于子程序相关概念,在第十六章程序管理中讲解。

1
2
3
4
5
6
7
# 练习 6:如何进入到你目前核心的模块目录?
cd /lib/modules/3.10.0-1062.el7.x86_64/kernel/
# 由于每个 linux 能够拥有多个核心版本,且几乎 distribution 的核心版本都不相同
# 所以上面的指令无法通用,这个时候可以使用其他额外指令语法来达成
cd /lib/modules/`uname -r`/kernel
cd /lib/modules/$(uname -r)/kernel

其实上面的指令可以说是做了两次动作:

  1. 先进行反单引号内的动作「uname -r」,并得到核心版本 3.10.0-1062.el7.x86_64
  2. 在上述结果带入原指令,得到 cd /lib/modules/3.10.0-1062.el7.x86_64/kernel/

TIP

为啥推荐${} 方式?方便识别 在复杂的变量引用中,没有分割符来分割非常的不方便识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 练习 7:取消刚刚设置的 name 变量内容
[mrcode@study kernel]$ unset name
[mrcode@study kernel]$ echo $name

[mrcode@study kernel]$

# 练习 8:输出 locate crontab 所找到的相关文件的权限
# locate 可以查找文件,并同时查看他们的文件权限
[mrcode@study kernel]$ locate crontab
/etc/anacrontab
/etc/crontab
/usr/bin/crontab
/usr/share/doc/man-pages-overrides-7.7.3/crontabs
/usr/share/doc/man-pages-overrides-7.7.3/crontabs/COPYING
/usr/share/man/man1/crontab.1.gz
/usr/share/man/man1p/crontab.1p.gz
/usr/share/man/man4/crontabs.4.gz
/usr/share/man/man5/anacrontab.5.gz
/usr/share/man/man5/crontab.5.gz
/usr/share/vim/vim74/syntax/crontab.vim

[mrcode@study kernel]$ ls -ld `locate crontab`
-rw-------. 1 root root 541 Aug 9 07:07 /etc/anacrontab
-rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab
-rwsr-xr-x. 1 root root 57656 Aug 9 07:07 /usr/bin/crontab
# 这个是个目录,上面使用 -d 参数的效果就是,不输出该目录下的明细,只输出目录信息
drwxr-xr-x. 2 root root 21 Oct 4 18:25 /usr/share/doc/man-pages-overrides-7.7.3/crontabs
-rw-r--r--. 1 root root 17738 Aug 9 08:47 /usr/share/doc/man-pages-overrides-7.7.3/crontabs/COPYING
-rw-r--r--. 1 root root 2626 Aug 9 07:07 /usr/share/man/man1/crontab.1.gz
-rw-r--r--. 1 root root 4229 Jun 10 2014 /usr/share/man/man1p/crontab.1p.gz
-rw-r--r--. 1 root root 1121 Jun 10 2014 /usr/share/man/man4/crontabs.4.gz
-rw-r--r--. 1 root root 1658 Aug 9 07:07 /usr/share/man/man5/anacrontab.5.gz
-rw-r--r--. 1 root root 4980 Aug 9 07:07 /usr/share/man/man5/crontab.5.gz
-rw-r--r--. 1 root root 2566 Aug 9 11:17 /usr/share/vim/vim74/syntax/crontab.vim

# 练习 9:如何简化一条命令
# cd /cluster/server/work/taiwan_2015/003 假设这条命令是经常用到的,但是特别长,如何简化?
work="/cluster/server/work/taiwan_2015/003"
cd work
# 使用变量方式,来达成效果
# 该变量可以记录在 bash 的配置文件 「~/.bashrc」中,那么以后可随时使用 cd $work 进入该目录

环境变量的功能

环境变量可以帮我打到很多功能,如:家的目录变换、提示字符的显示、执行文件搜寻的路径等,可以使用 envexport 来查询当前 shell 环境中有多少默认的环境变量

用 env 观察环境变量与常见环境变量说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[mrcode@study kernel]$ env
XDG_SESSION_ID=5
HOSTNAME=study.centos.mrcode # 主机名
SELINUX_ROLE_REQUESTED=
TERM=xterm # 终端机使用的环境是什么类型
SHELL=/bin/bash # 目前这个环境下,使用的 Shell 是哪一个程序?
HISTSIZE=1000 # 历史指令记录数量
SSH_CLIENT=192.168.0.105 53699 22
SELINUX_USE_CURRENT_RANGE=
QTDIR=/usr/lib64/qt-3.3
OLDPWD=/home/mrcode # 上一个工作目录所在
QTINC=/usr/lib64/qt-3.3/include
SSH_TTY=/dev/pts/0
QT_GRAPHICSSYSTEM_CHECKED=1
USER=mrcode # 使用者名称
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;... # 颜色显示
MAIL=/var/spool/mail/mrcode
PATH=/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/mrcode/.local/bin:/home/mrcode/bin:/home/dmtsai/bin
PWD=/lib/modules/3.10.0-1062.el7.x86_64/kernel #该用户目前所在的工作目录,使用 pwd 取出
LANG=en_US.UTF-8 # 语系设置
KDEDIRS=/usr
SELINUX_LEVEL_REQUESTED=
HISTCONTROL=ignoredups
SHLVL=1
HOME=/home/mrcode # 登录用户家的目录
LOGNAME=mrcode # 登录者登录的账户名称
QTLIB=/usr/lib64/qt-3.3/lib
XDG_DATA_DIRS=/home/mrcode/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share:/usr/share
SSH_CONNECTION=192.168.0.105 53699 192.168.0.128 22
LESSOPEN=||/usr/bin/lesspipe.sh %s
XDG_RUNTIME_DIR=/run/user/1000
QT_PLUGIN_PATH=/usr/lib64/kde4/plugins:/usr/lib/kde4/plugins
_=/usr/bin/env # 上一次使用的指令最后一个参数(或指令本身)

envenvironment 环境 的简写,上面列出来所有的环境变量,使用 export 也是一样的内容,只不过 export 还有其他额外的功能,上面这些变量的作用如下

  • HOME

    代表用户的家目录。使用 cd 或 cd ~ 也能回到自己的家,这个就是取用的 HOME 变量

  • SHELL

    目前这个环境使用的 SHELL 是哪个程序,Linux 预设使用 /bin/bash

  • HISTSIZE:历史命令可记录的总数量

  • MAIL:使用 mail 指令收信时,系统会读取的邮件信箱文件(mailbox

  • PATH

    执行文件搜索的路径,目录与目录中间以冒号「:」分割,由于文件搜索是按 PATH 变量内的目录查询的,所以目录的顺序也很重要

  • LANG

    语系信息,很多程序都会用到。比如,启动某些 perl 的程序语言文件,会主动分析语系数据文件,如果发现有他无法解析的编码语系,可能会产生错误

  • RANDOM

    随机树生成器的变量,目前大多数 distribution 都会有随机数生成器,就是 /dev/random 文件。可以通过该随机数文件相关的变量($RANDOM)来获取随机数值。

    BASH 环境下,该变量范围为 0~32767 之前

1
2
3
4
5
6
[mrcode@study kernel]$ echo $RANDOM
9229
# 想要 0 ~ 9 怎么办?
# 使用 declare 指令来让字符串转成计算公式 6593*10/32768 然后就能得到数值了
[mrcode@study kernel]$ declare -i number=$RANDOM*10/32768 ; echo $number
6

用 set 观察所有变量(含环境变量与自定义变量)

bash 不只有环境变量,还有一些与操作接口有关的变量,以及用户自己定义的变量存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 使用 set,除了环境变量之外,还会将其他咋 bash 内的变量都显示出来
# 下面只是其中一部分重要的,内容太多
[mrcode@study kernel]$ set
BASH=/bin/bash # bash 的主程序放置路径
# bash 的版本
BASH_VERSINFO=([0]="4" [1]="2" [2]="46" [3]="2" [4]="release" [5]="x86_64-redhat-linux-gnu")
BASH_VERSION='4.2.46(2)-release'
COLUMNS=126 # 在目前终端机环境下,使用的字段有几个字符长度
HISTFILE=/home/mrcode/.bash_history # 历史命令记录文件
HISTFILESIZE=1000 # 上面那个文件能存储历史命令的数量
HISTFILE=1000 # 在目前环境中,内存中能记录的历史命令最大数量
IFS=$' \t\n' # 预设的分隔符
LINES=20 # 目前的终端机下的最大行数
MACHTYPE=x86_64-redhat-linux-gnu # 安装的机器类型
OSTYPE=linux-gnu # 操作系统的类型
PS1=’[\u@\h \W]\$ ‘ # PS1 就厉害了,是命令提示字符,也就是我们常见的 [root@www ~]#、 [mrcode ~]$ 的设置,可以修改的
PS2=’> ‘ # 使用跳脱符号 \,在第二行开始显示的提示字符
$ # 目前这个 shell 使用的 PID
? # 刚刚执行完指令的回传值
...
其他的请自行查阅


一般来说,无论是否为环境变量,只要跟我们这个 shell 的操作接口有关的变量,通常都会被设置为大写字符。也就是说,基本上,在 Linux 预设的情况中,使用{大写的字母}来设置的变量一般为系统内定需要的变量。上面的变量中有如下几个比较重要

PS1 提示字符的设置

命令提示字符,当我们每次按下 Enter 键去执行某个指令后,最后要再次出现提示字符时,就会主动去读取这个变数值了。相关设置可以通过 man bash 查询 PS1 的相关说明,下面列出一些符号含义:

  • \d:可显示出「星期、月、日」的日期格式,如:「Mon Feb 2
  • \H:完整的主机名。如:本次练习机名称「study.centos.mrcode」.
  • \h:仅取主机名第一个小数点之前的名字,如上面的则取「study
  • \t:显示时间,24 小时格式的 HH:MM:SS
  • \T:显示时间,12 小时格式
  • \A:显示时间,24 小时格式 HH:MM
  • \@:显示时间,12 小时格式 am/pm 格式
  • \u:目前使用者的账户名称,如 mrcode
  • \vBASH 的版本信息,如 4.2.46(1)-release 仅取「4.2」
  • \w:完整的工作目录名称,由根目录写起的目录名称。但家目录会以 ~ 取代
  • \W:利用 basename 函数取得工作目录名称,所以仅会列出最后一个目录名
  • \#:下达的第几个指令
  • \$:提示字符,如果是 root 时,则为 # ,否则就是 $

预设内容为 [\u@\h \W]\$,对照上表来看,[mrcode@study ~]$ 这个为啥会显示成这样了

假设我们需要有类似如下的提示符号时,可以通过以下方式设置

1
2
3
4
5
6
# [mrcode@study /home/mrcode 16:50 #12]
[mrcode@study ~]$ cd /home/
[mrcode@study home]$ PS1='[\u@\h \w \A #\#]\$ '
[mrcode@study /home 02:26 #6]$
# 后面的 #6 信息,更新频率为 1 秒一次,输入一次命令算一次

?关于上个执行指令的回传值

bash 中该变量非常重要,表示「上一个执行的指令所回传的值」,当我们执行某些指令时,这些指令都会回传一个执行后的代码。一般来说,如果成功的执行该指令,则会回传一个 0 值,如果执行过程中发生错误,则会回传「错误代码」。简单说:非 0 则执行有错误

1
2
3
4
5
6
7
8
9
10
11
12
[mrcode@study /home 02:31 #13]$ echo $SHELL
/bin/bash # 执行成功
[mrcode@study /home 02:31 #14]$ echo $?
0 # 显示 0
[mrcode@study /home 02:32 #15]$ 12name=mrcode
bash: 12name=mrcode: command not found... # 执行失败
[mrcode@study /home 02:32 #16]$ echo $?
127 # 显示非 0
[mrcode@study /home 02:32 #17]$ echo $?
0 # 显示 0,? 只取代上一个命令的执行返回代码,不会累积,只能被使用一次


OSTYPE、HOSTTYPE、MACHTYPE主机硬件与核心的等级

在第 0 章中谈到过 CPU 等级,个人主机的 CPU 主要分为 32/64 位,其中 32 位又分为 i386、i586、i686 ,而 64 位则称为 x86_64。由于不同等级的 CPU 指令集不太相同,因此你的软件可能会针对某些 CPU 进行优化,以取得更佳的软件性能。所以软件就有 i386x86_64 之分了。

要留意的是,较高阶的硬件通常会向下兼容旧的软件,但较高阶的软件可能无法在旧机器上面安装

export 自定义变量转成环境变量

evnset 表示环境变量与自定义变量,他们的差异在于「该变量是否会被子程序所继续引用」。

当你登录 Linux 并取得一个 bash 之后,你的 bash 就是一个独立的程序,这个程序的识别使用的是一个称为程序标识符(PID)。接下来你再这个 bash 下下达的任何指令都是由这个 bash 所衍生出来的,那些被下达的指令就被称为子程序了。

如上,在原本的 bash 下执行另一个 bash,结果操作的环境接口会跑到第二个 bash 去(就是子程序),原本的 bashsleep 了。整个指令运作的环境是实线的部分!若要回到原本的 bash 去,只有将第二个 bash 结束掉(exit 或 logout)才行。更多的程序概念后续讲解

因为子程序仅会继承父程序的环境变量,子程序不会继承父程序的自定义变量;这里就会出现在这种父子切换中可能一不小心就会出现找不到变量等的情况发生

可以使用 export 将自定义变量变成环境变量,那么子程序就会继承了。

1
2
export 变量名称
# 如果 export 后面不带任何值,则会显示所有的环境变量

影响显示结果的语系变量 locale

笔者在使用 man 命令等指令时,mrcoderoot 账户一个显示英文,一个显示中文,使用 locale 查询如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[mrcode@study /home 02:59 #20]$ locale
LANG=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=
[mrcode@study /home 02:59 #21]$ su -
Password:
Last login: Tue Oct 29 20:45:07 CST 2019 on pts/0
[root@study ~]# man bash
[root@study ~]# locale
LANG=zh_CN.UTF-8 # 主语言环境
LC_CTYPE="zh_CN.UTF-8" # 字符(文字)编码
LC_NUMERIC="zh_CN.UTF-8" # 数字系统
LC_TIME="zh_CN.UTF-8" # 时间系统
LC_COLLATE="zh_CN.UTF-8" # 字符串的比较与排序
LC_MONETARY="zh_CN.UTF-8" # 货币显示
LC_MESSAGES="zh_CN.UTF-8" # 信息显示内容,如菜单、错误信息等
LC_PAPER="zh_CN.UTF-8"
LC_NAME="zh_CN.UTF-8"
LC_ADDRESS="zh_CN.UTF-8"
LC_TELEPHONE="zh_CN.UTF-8"
LC_MEASUREMENT="zh_CN.UTF-8"
LC_IDENTIFICATION="zh_CN.UTF-8"
LC_ALL= # 整体语系
[root@study ~]#
# locale -a 可以显示 linux 主机内有的语系文件,文件放置在 /usr/lib/locale

发现一个账号是 en_US.UTF-8 一个是 zh_CN.UTF-8 ,以上可单独设置的变量有好多个,但是有 LANGLC_ALL 是全局的,当其他都没有设置的时候,就会以这两个的语系为准

默认的语系配置文件在下面文件中

1
2
3
[root@study ~]# cat /etc/locale.conf 
LANG="zh_CN.UTF-8"

如果只是暂时在 tty 中显示指定的语系,而不是持久化的更改时,直接使用环境变量赋值方式

1
2
3
4
5
[mrcode@study /home 03:09 #22]$ LANG="zh_CN.UTF-8"
# 作者说在 Centos7 中可能需要使用如下的方式才可以
# 从 zh_TW.UTF-8 修改为 en_US.UTF8;
LANG=en_US.UTF8;
export LC_ALL=en_US.UTF8

变量的有效范围

export 指令中就提到了这个概念,如:父子变量不会被继承,需要使用 export 导出为环境变量。

某些书籍中会谈到全局变量(global variable)与局部变量(local variable),在本章:

  • 环境变量 = 全局变量
  • 自定义变量=局部变量

为啥环境变量的数据可以被子程序所引用呢?是因为内存配置的关系,理论上是这样的:

  • 当启动一个 shell,操作系统会分配一块内存给 shell 使用,此内存变量可让子程序取用
  • 若在父程序中利用 export 功能,可以让自定义变量的内容写到上述的区块中(环境变量)
  • 当加载另一个 shell 时,子 shell 可以将父 shell 的环境变量所在的区块导入自己的环境变量区块中

但是需要注意的是:这里的环境变量与「bash 的操作环境」不太一样,如 PS1 并不是环境变量,可以看成是对 bash 程序的配置

变量键盘读取、数组与宣告:read、array、declare

上述的变量都是由指令直接设置的,可以让用户使用键盘输入,如某些程序执行过程中,会等待使用者输入 yes/no 之类的信息。

read

交互式指令,阻塞等待用户输入信息。该指令在 shell script 中经常用到。关于 script 在 第十三章介绍

1
2
3
4
5
6
read [-pt] variable

选项与参数

-p:后面可以接提示符
-t:后面可以接等待的秒数

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
# 范例 1:让用户由键盘输入一个内容,将该内容变成名为 atest 的变量
[mrcode@study ~]$ read atest
this is a test # 光标闪烁,等待你的输入
[mrcode@study ~]$ echo ${atest} # 这里打印刚刚用户输入的信息
this is a test

# 范例 2:提示使用者 30 秒内输入自己的大名,将该输入字符串作为名为 named 的变量内容
[mrcode@study ~]$ read -p "Please keyin your name: " -t 30 named
Please keyin your name: mrcode # -p 的提示信息
[mrcode@study ~]$ echo ${named}
mrcode

# -t 30 ,如果 30 秒之后没有输入,则自动略过

declare 、 typeset

declaretypeset 都是声明变量的类型。如果使用 declare 后面并没有接任何参数,那么 bash 会主动将所有变量名称与内容显示出来,就好像使用 set 一样。语法如下

1
2
3
4
5
6
7
8
9
10
11
declare [-aixr] variable

选项与参数

-a:将后面的 variable 的变量定义为数组 array 类型
-i:定义为整数数字 integer 类型
-x:用法与 export 一样,将后面的 variable 变成环境变量
+x:将环境变量变成普通的自定义变量
-r:将变量设置为 readonly 类型,该变量不可被更改内容,也不能 unset
-p:显示变量的定义和类型

实践与练习

1
2
3
4
5
6
7
8
9
# 范例 1 :让变量 sum 进行 100 + 300 + 50 的加总结果
[mrcode@study ~]$ sum=100+300+50
[mrcode@study ~]$ echo ${sum}
100+300+50 # 发现没有生效,变成了字符串
# 使用 declare 声明后,成功
[mrcode@study ~]$ declare -i sum=100+300+50
[mrcode@study ~]$ echo ${sum}
450

在默认的情况下, bash 对于变量有几个基本的定义:

  • 变量类型默认为字符串
  • bash 环境中的数值运算,预设最多仅能达到整数形态,所以 1/3 结果是 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 范例 2:将 sum 变成环境变量
[mrcode@study ~]$ declare -x sum
[mrcode@study ~]$ export | grep sum # 这里是在 export 指令的信息下搜索 sum
declare -ix sum="450" # 看到使用了 declare -ix 来声明

# 范例 3:将 sum 变成只读属性,不可更动
[mrcode@study ~]$ declare -r sum
[mrcode@study ~]$ sum=123
-bash: sum: readonly variable

# 范例 4:将 sum 变成非环境变量的自定义变量
[mrcode@study ~]$ declare +x sum # 将 - 变成 + 就是去掉环境变量
[mrcode@study ~]$ declare -p sum # -p 显示某个变量的定义和类型
declare -ir sum="450"

declare 功能很有用,在 shell script 中经常使用。如果不小心将变量设置为「只读」,通常需要注销再登录才能复原该变量的类型

array

废话不多说,笔者是个程序员,就不记录那么低级的概念

1
2
# 语法
var[index]=countent

实践与练习

1
2
3
4
5
6
# 范例:设置 var[1] ~ var[3] 的变量
[mrcode@study ~]$ var[1]="small min"
[mrcode@study ~]$ var[2]="big min"
[mrcode@study ~]$ var[3]="nice min"
[mrcode@study ~]$ echo "${var[1]},${var[2]},${var[3]}"
small min,big min,nice min

与文件系统及程序的限制关系:ulimit

想象一个状况:Linux 主机同时登陆了 10 个人,同时开启了 100 个文件,每个文件约 10MBytes,那么需要的内存则是 10*100*10=100000MBytes=10GBytes,耗费太多内存,系统很容易崩溃;为了预防这种情况,bash 可以「限制用户的某些系统资源」,包括可以开启的文件数量、CPU 可以使用的时间、可用内存总量等。

1
2
ulimit [-SHacdfltu] [配额]

选项与参数:

  • Hhard limit,严格的设定,必定不能超过这个设定的数值

  • Ssoft limit,警告的设定,可以超过该设定值,超过则出现警告信息

    在设置上,通常 soft 会比 hard 小。比如:soft=80hard=100,那么你可以使用到 90(因为没有超过 100), 但是介于 80 ~ 100 之间,系统会有警告信息通知你

  • a:后面不接任何选项与参数,可列出所有的限制额度

  • c:限制每个核心文件的最大容量

    当某些程序发生错误时,系统可能会将该程序再内存中的信息写成文件(排除用),这种文件被称为核心文件(core file)。

  • f:此 shell 可以建立的最大文件容量(一般可能设置为 2GB)单位为 Kbytes

  • d:程序可使用最大断裂内存(segment)容量

  • l:可用于锁定(lock)的内存量

  • t:可使用最大 CPU 时(单位为妙)

  • u:单一用户可以使用的最大程序(process)数量

实践与练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 范例 1:列出你目前身份(假设为一般账户)的所有限制数据值

[mrcode@study ~]$ ulimit -a
core file size (blocks, -c) 0 # 只要为 0 则表示没有限制
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited # 可建立的单一文件的大小
pending signals (-i) 4519
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024 # 同时可开启的文件数量
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 4096
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited

# 范例 2:限制用户仅能建立 10MBytes 以下的容量文件
[mrcode@study ~]$ ulimit -f 10240
[mrcode@study ~]$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) 10240 # 已经修改为 10 MBytes
# 尝试创建大于 10MBytes 的文件,报错
[mrcode@study ~]$ dd if=/dev/zero of=123 bs=1M count=11
File size limit exceeded (core dumped)
# 记得删除 123 这个文件,同时需要注销再次登录才能让 10M 的限制消失
# 这里为什么需要删除?错误是报了,但是文件是生成了的,只是文件大小只有限制的 10M 大小

在第七章 Linux 磁盘文件系统中提到过,单一 filesystem 能够支持单一文件大小与 block 的大小有关系。但是文件系统的限制容量都允许太大了,可以使用 ulimit -f 来限制使用者建立的文件不要太大。

TIP

此外,ulimit 除了重新登录账户外,还可以重新设置 ulimit,但是普通用户只能降低,而不能增加文件容量,

若想要管控使用者的 ulimit 限值,可以参考第十三章的 pam 介绍

变量内容的删除、取代与替换(Optional)

除了可以设置修改原本的内容外,还可以对变量进行微调,如删除、取代、替换

变量内容的删除

下面的范例以此进行,比较能理解到这里想表达的意思

1
2
3
4
5
6
7
8
9
10
# 范例 1:让小写的 path 自定义变量设置与 PATH 内容相同
[mrcode@study ~]$ path=${PATH}
[mrcode@study ~]$ echo ${path}
/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/mrcode/.local/bin:/home/mrcode/bin

# 范例 2:假设不喜欢 local/bin,所以将前一个目录删除掉(/usr/local/bin:)
# 这里的语法其实就是:将 符合 /*local/bin:(含) 之前的目录都删掉
[mrcode@study ~]$ echo ${path#/*local/bin:}
/usr/bin:/usr/local/sbin:/usr/sbin:/home/mrcode/.local/bin:/home/mrcode/bin

上面的语法示意解析如下

1
2
3
4
5
6
7
8
${变量#/*local/bin;}   

- ${} :这种华括弧必须在,这种删除模式只有括弧起来才能识别
- 变量:原本的变量名称
- # :代表从变量内容的最前面开始向右删除,且仅删除最短的那个
- /*local/bin:代表要删除的部分,由于 # 代表由前面开始删除,所以这里便由开始的 / 写起
需要注意的是:还可以通过通配符 * 来取代 0 到无穷多个任意字符

1
2
3
4
5
6
7
8
# 范例 3:想要删除前面所有的目录,仅保留最后一个目录
[mrcode@study ~]$ echo ${path#/*:}
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/mrcode/.local/bin:/home/mrcode/bin
# # 表示删除前面最短的那一个,这里通配符了前面的一个路径,也就是那个 /usr/lib64/qt-3.3/bin: 被删除了

[mrcode@study ~]$ echo ${path##/*:}
/home/mrcode/bin
# 多增加了一个 # 字符,则只剩下最后一个路径了

PATH 变量中的内容都是以冒号「**:」隔开的,所以要从头删除掉目录就是介于「/」到「:」之间,但是 **PATH 中不止一个冒号,所以需要以 ### 分表表示

  • #:符合取代文字的「最短的」那一个
  • ##:符合取代文字的「最长的」那一个

# 是由后面往前删除内容,%则是由前往后删除内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 范例 4:假设你的 MAIL 变量是 /var/spool/mail/mrcode,只想要 mrcode 这个字符串,也就是前面的目录都不要了
[mrcode@study ~]$ echo ${MAIL}
/var/spool/mail/mrcode
[mrcode@study ~]$ echo ${MAIL##/*/}
mrcode

# 要删除目录,只要前面的
[mrcode@study ~]$ echo ${MAIL%/*}
/var/spool/mail

# 下面再来看看这两个到底是什么意思,前面的演示,笔者自己觉得没有明白是什么意思
# # 是从头开始匹配,下面演示是什么意思
[mrcode@study ~]$ echo ${MAIL};echo ${MAIL#/*}
/var/spool/mail/mrcode
var/spool/mail/mrcode # 匹配到的是 /,并且只匹配最短的那一个,那么最短的就是最前面的 /
[mrcode@study ~]$ echo ${MAIL};echo ${MAIL##/*}
/var/spool/mail/mrcode
# 被完全删掉了,/* 最常的就是整个字符串
[mrcode@study ~]$ echo ${MAIL};echo ${MAIL##/*/}
/var/spool/mail/mrcode
mrcode # 匹配到的是 /var/spool/mail/ 因为是匹配最常的结果,mrcode 后面没有没有 / 所以不会被匹配
[mrcode@study ~]$ echo ${MAIL};echo ${MAIL##/*spool/}
/var/spool/mail/mrcode
mail/mrcode # 匹配到的是 /var/spool/ ,按最常的匹配,这里唯一能匹配上的就是整个了

# 那么相反的是:% 是从尾部开始匹配
[mrcode@study ~]$ echo ${MAIL};echo ${MAIL%/*}
/var/spool/mail/mrcode
/var/spool/mail # 「%/*」匹配到的是 /mrcode ,并且是最短的这一个
[mrcode@study ~]$ echo ${MAIL};echo ${MAIL%%/*}
/var/spool/mail/mrcode
# 匹配了整个路径
[mrcode@study ~]$ echo ${MAIL};echo ${MAIL%%/*/}
/var/spool/mail/mrcode
/var/spool/mail/mrcode # 注意看这里,`%%/*/` /*/ 没有匹配上,是因为从 mrcode 开始匹配,然而 mrcode 后面没有 / 所以从开始就匹配不上了,所以没有删除成功
[mrcode@study ~]$ echo ${MAIL};echo ${MAIL%%/spool*}
/var/spool/mail/mrcode
/var # 「%%/spool*」 * 代表了 /mail/mrcode 这一串,所以能匹配上


  • #:从字符串头部往后匹配,匹配上则删除这一串,按最短匹配原则
  • ##:同上,按最长匹配原则
  • %:从字符串尾部往前匹配,匹配上则删除这一串,按最短匹配原则
  • %%:同上,按最常匹配原则

变量内容的替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 范例 1:将 path 的变量内容内的 sbin 替换成大写的 SBIN
[mrcode@study ~]$ echo ${path}; echo ${path/sbin/SBIN}
/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/mrcode/.local/bin:/home/mrcode/bin
/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/usr/local/SBIN:/usr/sbin:/home/mrcode/.local/bin:/home/mrcode/bin
[mrcode@study ~]$ echo ${path}; echo ${path/"local/sbin"/SBIN}
/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/mrcode/.local/bin:/home/mrcode/bin
/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/usr/SBIN:/usr/sbin:/home/mrcode/.local/bin:/home/mrcode/bin
# 看下上面的替换前和替换后的对比,这个就很好理解了

# 如果是两条斜线,则替换所有的
[mrcode@study ~]$ echo ${path}; echo ${path//sbin/SBIN}
/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/mrcode/.local/bin:/home/mrcode/bin
/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/usr/local/SBIN:/usr/SBIN:/home/mrcode/.local/bin:/home/mrcode/bin

  • \:替换首次出现的字符串为指定字符串; ${path\关键字\替换成目标字符串}
  • \\:替换所有匹配的字符串为指定字符串

变量的测试与内容替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 范例 1: 测试 username 变量,若不存在则给定默认内容为 root
[mrcode@study ~]$ echo ${username}
# 可以看到没有内容输出
[mrcode@study ~]$ username=${username-root}; # 使用短横线设置默认值
[mrcode@study ~]$ echo ${username}
root
[mrcode@study ~]$ echo ${username-root2}
root # 可以看到,当有值的时候,给定的默认值不会生效

# 范例 2:当内容为空串时,会出现什么?
[mrcode@study ~]$ username=""
[mrcode@study ~]$ username=${username-root}
[mrcode@study ~]$ echo ${username}
# 可以看到,当内容为空串时,变量其实已经定义了,只是内存为空串
[mrcode@study ~]$ username=${username:-root}
[mrcode@study ~]$ echo ${username}
root # 这里使用 : 来识别空串

除了以上的是否为空判定之外,还有其他的功能,总结如下

变量设置方式 str 不存在 str 为空字符串 str 已存在且不为空字符串
var=${str-expr} var=expr var= var=$str
var=${str:-expr} var=expr var=expr var=$str
var=${str+expr} var= var=expr var=expr
var=${str:+expr} var= var= var=expr
var=${str=expr} str=expr; var=expr str 不变;var= str 不变;var=$str
var=${str:=expr} str=expr; var=expr str=expr; var=expr str 不变;var=$str
var=${str?expr} expr 输出至 stderr var= var=$str
var=${str:?expr} expr 输出至 stderr expr 输出至 stderr var=$str

总结:: 冒号都是把空字符串识别为不存在,其他的按功能如下:

  • -:不存在则给默认值,存在则使用原始值
  • +:存在则给默认值,不存在不给值
  • =:不存在则改变变量值,会影响原始变量的值;存在则使用原始值
  • ?:不存在则报错,存在则使用原始值

实践练习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 测试:- ,str 不存在
[mrcode@study ~]$ unset str; var=${str-newvar};
[mrcode@study ~]$ echo "var=${var}, str=${str}"
var=newvar, str= # 因为 str 不存在,所以 var 被赋值了

# 测试:- ,str 存在
[mrcode@study ~]$ str="oldvar"; var=${str-newvar};
[mrcode@study ~]$ echo "var=${var}, str=${str}"
var=oldvar, str=oldvar # 因为 str 存在,所以不赋值

# 测试:=
[mrcode@study ~]$ unset str; var=${str?newvar};
-bash: str: newvar # 不存在,直接报错
[mrcode@study ~]$ str="oldstr"; var=${str?newvar};
[mrcode@study ~]$ echo "var=${var}, str=${str}"
var=oldstr, str=oldstr # 存在则使用存在的值,所以这个变量没有用

这里其实还是稍微有点难以理解,没有 if else 这样看的明白,追求极致代码简洁的情况下可以使用这种方式。

命令别名与历史命令

命令别名设置:alias、unalias

命令别名就是你可以把一长串指令指定一个简短的名称,在键入指令的时候使用简短的名称来达到调用一长串指令的目的。例如:ls -al|more 查看隐藏文档并且翻页查看,觉得这串指令太长了,可以通过下面的指令来设置别名

1
2
3
4
5
6
7
8
[mrcode@study ~]$ alias lm='ls -al | more'
[mrcode@study ~]$ lm
total 68
drwx------. 18 mrcode mrcode 4096 Nov 11 10:20 .
drwxr-xr-x. 4 root root 42 Oct 8 23:01 ..
-rw-------. 1 mrcode mrcode 10279 Nov 11 14:12 .bash_history
-rw-r--r--. 1 mrcode mrcode 18 Aug 8 20:06 .bash_logout
-rw-r--r--. 1 mrcode mrcode 193 Aug 8 20:06 .bash_profile

别名的定义规则与变量定义规则几乎相同,另外可以取代已经存在的变量名

1
alias rm='rm -i'

root 可以移除(rm)任何数据,所以当使用 rm 的时候需要小心,可以使用上面的别名指令覆盖掉原始的 rm 指令,执行的时候就是执行 rm -i 指令了

1
2
3
4
5
6
7
8
9
10
11
[mrcode@study ~]$ alias
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l.='ls -d .* --color=auto'
alias ll='ls -l --color=auto'
alias lm='ls -al | more'
alias ls='ls --color=auto'
alias rm='rm -i'
alias vi='vim'
alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'

在 root 用户下是没有 vi='vim' 的,一般用户会默认添加该别名

想取消别名可以使用 unalias 指令

1
unalias lm

例题:DOS 年代,列出目录与文件用 dir,清除屏幕用 cls,在 linux 如何达到这个效果?

1
2
[mrcode@study ~]$ alias cls='clear'
[mrcode@study ~]$ alias dir='ls -l'

历史命令:history

前面提过 bash 有提供指令历史的服务,可以使用 history 来查询曾经下达过的指令

1
2
3
4
history [n]
history [-c]
history [-raw] histfiles

选项与参数:

  • n:数字,列出最近 n 条命令
  • c:将目前的 shell 中的所有 history 内容全部消除
  • a:将目前新增的 history 指令新增如 histfiles 中,若没有加 histfiles 则预设写入 ~/.bash_history
  • r:将 histfiles 的内容读到目前这个 shellhistory 记忆中
  • w:将目前的 history 记忆内容写入 histfiles

实践与练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 范例 1:列出目前内存内的所有 history 记忆
[mrcode@study ~]$ history
1 cd /root/
2 sudo
3 sudo cd /root/
.... 中间省略
666 alias
667 alias cls='clear'
668 alias dir='ls -l'
669 dir
670 history

# 范例 2:列出们目前最近的 3 条指令
[mrcode@study ~]$ history 3
669 dir
670 history
671 history 3

# 范例 3:like将目前的资料写入 histfile 中
[mrcode@study ~]$ history -w
# 默认情况会将记录记录写入 ~/.bash_history 中
[mrcode@study ~]$ echo ${HISTSIZE}
1000 # 这里不是现实文件中有多少条,而是最大可存储多少条


正常情况下历史命令的读取记录是这样的:

  • 当以 bash 登录 Linux 主机后,系统会主动的由家目录的 ~/.bash_history 读取
  • 假设这次登录后,共下达过 100 次命令,等你注销时,系统就会将 101~1100 总共 1000 条记录更新~/.bash_history 中,因为和能存储最大条数 HISTSIZE 有关系,前面的序号会增加,但是总存储条数只有 HISTSIZE
  • 也可以使用 history -w 强制写入

history 指令不只是提供了查询历史记录而已,还可以利用相关命令来执行指令,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 执行第几条命令,这里 number 是数值序号
[mrcode@study ~]$ !number
# 由最近的指令向前搜索指令开头为 command 的哪个指令,并执行
[mrcode@study ~]$ !command
# 执行上一个指令(相当于按 ↑ 后,再按 Enter)
[mrcode@study ~]$ !!

# 使用范例
[mrcode@study ~]$ history 4
681 man rm
682 alias
683 man history
684 history 4
[mrcode@study ~]$ !681 # 执行第 681 条指令
man rm # 这里会显示具体执行的指令是什么
[mrcode@study ~]$ !! # 执行上一个指令
man rm
[mrcode@study ~]$ !al # 从最新的历史指令开始搜索 al 开头的指令并执行他
alias

虽然好用,但是需要小心安全问题,尤其是 root 的历史记录,这是黑客的最爱。另外使用 history 配合 ! 曾经使用过的指令下达是很有效率的一个指令下达方式

同一账户同时多次登录的 history 写入问题

常常会同时开几个 bash 窗口,这些 bash 的身份都是 root。这样会有 ~/.bash_history的写入问题吗?

自动写入的条件是注销 bash 时,自动写入,那么最后一个被注销的 bash 窗口中的历史记录会存下来,如果记录大于了 1000 的话,后注销的会覆盖前面先注销的(会有同时注销的情况导致错乱的吗?书上没有说)

由于多重登录有这样的问题,很多朋友都习惯单一 bash 登录,再用后续要讲解的 「工作控制 job control 来切换不同的工作」,这样才能将所有曾经下达过的指令记录下来,也方便未来系统管理员进行指令的 debug

无法记录时间

history 有一个问题就是无法记录指令下达时间。按序号记录的,但是没有记录时间。如果有兴趣,其实可以通过 ~/.bash_logout 来进行 history 的记录,并加上 date 来增加时间参数(后续的情景模拟题中会讲到怎么做)

TIP

有一种情况就是,想不要别人翻阅你的历史记录的话,可以使用 history -c;history -w 强迫清除并立即写入文件来清空历史记录


Bash Shell 的操作环境

在我们登陆主机的时候,屏幕上会有一些说明文字,告知我们的 Linux 版本之类的信息,还可以显示一些欢迎等信息。此外,我们习惯的环境变量、命令别名等,是否可以在登录后就主动帮我设置好?

这些设置又分为系统全局配置和个人账户级配置,仅是文件放置位置不同

路径与指令搜寻顺序

前面讲到过使用 alias 可以建立别名,比如创建了一个 ls 的别名,其实 ls 有少的指令,那么到底是哪一个会被选中执行呢?基本上,指令运行顺序可以这样看:

  1. 以相对、绝对路径执行命令,例如 /bin/ls./ls
  2. alias 找到该指令来执行
  3. bash 内置的指令来执行
  4. 通过 $PATH 这个变量的顺序搜索到第一个指令执行

举例来说:

  • /bin/ls:该指令运行后,没有颜色
  • ls:该指令运行后输出的内容有颜色,因为是使用别名 alias ls=‘ls --color=auto’

也可以使用 type -a ls 来查询指令搜寻的顺序

1
2
3
4
5
6
7
# 范例:设置 echo 的命令别名为 echo -n,然后观察 echo 执行的顺序
[mrcode@study ~]$ alias echo='echo -n'
[mrcode@study ~]$ type -a echo
echo is aliased to `echo -n'
echo is a shell builtin
echo is /usr/bin/echo

可以看到上面的顺序与本节总结的执行顺序一致

bash 的进站与欢迎信息:/etc/issue、/etc/motd

进站信息 /etc/issue

tty1~tty6 登录时,会有几行提示字符,这个就是进站画面,该字符串在 /etc/issue 中配置的

1
2
3
4
[mrcode@study ~]$ cat /etc/issue
\S
Kernel \r on an \m

如上的变量引用使用的是反斜杠,变量可以通过 man issue 中查看到 agetty ,再 man agetty 得到如下的信息,代码变量信息如下

  • \d:本地端时间的日期
  • \l:显示第几个终端机接口
  • \m:显示硬件的等级(i386、i486、i586…)
  • \n:显示主机的网络名称
  • \O:显示 domain name
  • \r:操作系统的版本(相当于 uname -r)
  • \t:显示本地端时间的时间
  • \S:操作系统的名称
  • \v:操作系统的版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 练习:如果想在 tty3 的进站画面看到如下显示,该如何设置才能达到效果?

CentOS Linux 7 (Core)(terminal:tty3)
Date:2019-12-01 18:00:00
Kernel 3.10.0-229.e17.x86_64 on an x86_64
Welcome!

使用 root 身份参考上面的变量说明修改 /etc/issue 文件达成效果

vim /etc/issue

\S (terminal: \l)
Date: \d \t
Kernel \r on an \m
Welcome!


怎么登录 tty 和切换 tty 请参考之前的章节,记得,进站画面是切换到 tty 时顶部显示的信息,而不是登录后显示的信息。

该文件中的规则就是使用反斜杠引用上面的变量,其他的你可以随意操作,比如写个字符画等,搞得个性一点

当使用 telnet 登录主机时,是不会显示 /etc/issue 中的配置,而是显示 /etc/issue.net 中的配置

欢迎信息 /etc/motd

想要使用者登录后,取得一些信息,例如使用注意事项信息,就可以修改 /etc/motd 文件

1
2
3
4
5
6
7
8
9
10
11
[root@study ~]# vi /etc/motd 
Hello everyone,
这是欢迎信息中文测试

# 重新登录后会看到如下的信息
Last login: Sun Dec 1 17:37:58 2019 from 192.168.0.105
Hello everyone,
这是欢迎信息中文测试
[mrcode@study ~]$


bash 的环境配置文件

我们一进入 bash 就取得了一堆有用的变量,这是因为系统有一些环境配置文件的存在,让 bash 在启动时直接读取这些配置文件,以规划好 bash 的操作环境。而这些配置文件分为全局系统配置和用户个人偏好配置

login 与 non-login shell

在介绍 bash 的配置文件前,一定要先知道 login shellnon-login shell ,重点就在于有没有登录(login

  • login shell:取得 bash 时需要完整的登录流程,就称为 login shell

    举例来说,你要由 tty1~tty6 登录,需要输入用户的账户与密码,此时取得的 bash 就称为「login shell

  • non-login shell:取得 bash 接口的方法不需要重复登录的举动

    比如:你以 x window 登录 linux 后,再以 X 的图形化接口启动终端机,此时该终端机并没有再次输入账户与密码,那么该 bash 的环境就称为 non-login shell

    再比如:你再原本的 bash 环境下再次下达 bash 这个指令,同样也没有输入账户密码,那第二个 bash(子程序)也是 non-login shell

上面两种情况取得的 bash 配置文件不一致。由于我们需要登录系统,所以先谈谈 login shell 会读取哪些配置文件?一般来说,login shell 其实只会读取这两个配置文件

  1. /etc/profile:系统整体配置,最好不要修改这个文件
  2. ~/.bash_profile~/.bash_login~/.profile:属于使用者个人设置

/etc/profile (login shell 才会读)

该文件相对于现在我们来看,可能还不太能看得懂,里面是利用使用者的标识符(UID)来决定很多重要的变量数据,这也是 每个使用者登录取得 bash 时一定会读取的配置文件 ,也就是系统级全局配置,主要变量如下:

  • PATH:会依据 UID 决定 PATH 变量要不要含有 sbin 的系统指令目录
  • MAIL:依据账户设置好使用者的 mailbox 到 **/var/spool/mail/**账号名
  • USER:根据用户的账户设置该变量类容
  • HOSTANME:依据主机的 hostname 指令决定此变量内容
  • HISTSIZE:历史命令记录数量。CentOS 7.x 设置为 1000
  • umask:包括 root 默认为 022 而一般用户为 002

/etc/profile 可不止会做这些事情,还会呼叫外部的设置数据,在 CentOS 7.x 默认情况下,下面的数据会依序被呼叫进来:

*/etc/profile.d/.sh**

通配符方式,加载该目录内所有的 sh 文件,另外,使用者需要具有 r 的权限,那么该文件就会被 /etc/profile 调用。

CentOS 7.x 中,该目录下的文件规范了 bash 操作窗口的颜色、语系、llls 指令的命令别名、vi 的命令别名、which 的命令别名等。如果你需要帮所有使用者设置一些共享的命令别名时,可以在该目录下自行建立后缀为 .sh 的文件,并将所需要的数据加入即可

/etc/local.conf

该文件是由 /etc/profile.d/lang.sh 呼叫进来的,这也是我们决定 bash 预设使用何种语系的重要配置文件!文件里最重要的就是 LANGLC_ALL 这些变量的设置,前面讨论过

*/usr/share/bash-completion/completions/**

tab 键补全,除了命令补齐、文档名补齐外,还可以进行指令的选项、参数补齐功能。就是从这个目录里面找到对应的指令来处理的。

该目录下的内容是由 /etc/profile.d/bash_completion.sh 文件载入的

~/.bash_profile (login shell 才会读)

bash 在读完了整体环境设置的 /etc/profile ,并借此加载其他配置文件后,接下来则是会读取使用者的个人配置文件。在 login shell 的 bash 环境中,所读取的个人偏好配置文件其实主要有 3 个,依序分别是:

  1. ~/.bash_profile
  2. ~/bash_login
  3. ~/,profile

其实 bashlogin shell 设置只会读取上面三个文件中的一个,而读取的顺序则是依照上面的顺序。

什么意思呢?是当第一个文件不存在时,读取第二个,那么当第一个文件存在时,后面的都不读取了

为什么会有这么多的文件?是因为其他 shell 转换过来的使用者习惯不同,而做的兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 看看 mrcode 的 .bash_profile 的内容
# 具体路径为 /home/mrcode/.bash_profile
[mrcode@study ~]$ cat ~/.bash_profile
# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then # 判断并读取 ~/.bashrc
. ~/.bashrc
fi

# User specific environment and startup programs
# 下面再处理个人化设置
PATH=$PATH:$HOME/.local/bin:$HOME/bin

export PATH

该文件设置了 PATH,并使用 exportPATH 变成环境变量,看配置是通过累加方式将用户家目录下的 ~/bin/ 目录添加进 PATH 了,这就意味着,你可以将可执行文件放到 ~/bin/ 下,执行时,就不需要写全路径了

上面的文件内容中有一段 if…then… 代码,该代码后续再 shell sript 中讲解,这里判断 ~/.bashrc 文件是否存在,存在则加载。

bash 配置文件的读入方式是通过 source 指令来读取的。这个后续讲解,最后来看看整个 login shell 的读取流程

实线的方向是主线流程,虚线的方向则是被加载的配置文件。从上图来看,CentOSlogin shell 环境下,最终被读取的配置文件是 ~/.bashrc 文件,所以可以将自己的偏好设置写入该文件即可。

下面还要讨论 source~/.bashrc

source : 读取环境配置文件的指令

由于 /etc/profile~/.bash_profile 都是在取得 login shell 的时候才会读取的配置文件,所以将自己的偏好设置写入上述文件后,通常都是需要注销后再登录,才会生效。可以使用 source 指令达到立即生效。

1
source 配置文件名
1
2
3
4
5
# 范例:将 家目录的 ~/.bashrc 的设置读入目前的 bash 环境中
[mrcode@study ~]$ source ~/.bashrc
[mrcode@study ~]$ . ~/.bashrc
# 使用 source 或则 小数点的语法 都能将内容读取到当前的 shell 环境中

source 还可以用于不同环境配置文件的场景中,比如,我的工作环境分为 3 个,那么需要分别编写属于 3 个项目的环境变量配置文件,当需要该环境时,直接使用 source 加载进来

~/.bashrcnon-login shell 会读)

在非登录情况下取得 bash 环境配置文件时,仅会读取 ~/.bashrc 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
[mrcode@study ~]$ cat ~/.bashrc 
# .bashrc

# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi

# Uncomment the following line if you don't like systemctl's auto-paging feature:
# export SYSTEMD_PAGER=

# User specific aliases and functions

注意看,不同身份账户不同,这也解释了个人偏好配置文件是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@study ~]# cat ~/.bashrc 
# .bashrc

# User specific aliases and functions
# 使用者的个人设置
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'

# Source global definitions
# 整体环境的设置
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi

CentOS 7.x 中为什么会主动加载 /etc/bashrc 文件呢?是因为 /etc/bashrc 帮我们的 bash 定义出下面的数据:

  1. 依据不同的 UID 规范出 umask 的值
  2. 依据不同的 UID 规范出提示字符(就是 PS1 变量)
  3. 加载 /etc/profile.d/*.sh 的设置

需要注意的是,**/etc/bashrc** 是 CentOS 特有的(Red Hat 系统),其他的 distribution 可能不是该名称。由于 ~/.bashrc会加载 /etc/bashrc/etc/profile.d/*.sh 所以,当你不小心删除了 ~/.bashrc 那么这些都不能读取了,你的 bash 提示字符可能就变成下面这个样子了

1
2
-bash-4.2$

原因是,没有加载 /etc/bashrc 来规范 PS1 d的变量,这种情况也不会影响你的 bash 使用。可以复制 /etc/skel/.bashrc 文件复制到 ~/.bashrc ,恢复回来

其他相关配置文件

事实上还有一些配置文件可能会影响到你的 bash 操作

/etc/man_db.conf

该文件对于系统管理员来说,是一个很重要的文件,它规范了使用 man 时, man page 的路径到哪里去寻找。

如果你是以 tarball 的方式来安装你的数据库,那么你的 man page 可能会放置在 /usr/local/softpackage/man 中,softpackage 是套件的名称,这个时候就需要手动将该路径加到 /etc/man_db.conf 中。否则 man 就会找不到相关的说明文档

~/bash_history

在讲解「历史命令」时提到过该文件,预设情况下,历史命令就记录在该文件中。每次登陆 bash 后,bash 会先读取这个文件,将所有的历史指令读入内存,因此,当我们登陆 bash 后就可以查知上次使用过哪些指令

~/.bash_logout

该文件则记录了:当我注销 bash 后,系统再帮我做完师门动作后才离开的意思。你可以读取下该文件的内容,预设情况下,注销时,bash 只是帮我们清掉屏幕的信息而已。

不过,你也可以将一些备份或则是其他你认为重要的工作写在这个文件中(如:清空暂存盘)

终端机的环境设置:stty、set

前面讲解过可以在 tty1~tty6 这 6 个文字终端机(terminal)环境中登录,登录的时候可以取得一些字符设置的功能。比如

  • 使用退格键(删除键)来删除命令行上的字符
  • ctrl + c 来强制终止一个指令的执行
  • 当时呼入错误时,会有声音跑出来警告

以上功能都是在登录终端机时,自动获取终端机的输入环境设置实现的

事实上,目前我们使用的 Linux distributions 都帮我们制作了最棒的使用者环境了,但是在某些 Unix like 机器中,还是可能需要手动修改配置

1
2
3
4
# setting tty  
stty [-a]
参数 a:将目前所有的 stty 参数列出来

1
2
3
4
5
6
7
8
9
10
11
# 范例 1 :列出所有的按键与按键内容
[root@study ~]# stty -a
speed 38400 baud; rows 19; columns 126; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S;
susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke
# 以上特殊字符 ^ 表示 Ctrl,^C 表示 ctrl + c

下面是几个重要的含义:

  • intr:送出一个 interrupt 中断信号给目前正在 run 的程序
  • quit:送出一个 quit 信号给目前正在 run 的程序
  • erase:向后删除字符
  • kill:删除在目前指令列上的所有文字
  • eofEnd of file 的意思,代表「结束输入」
  • start:在某个程序停止后,重新启动它的 output
  • stop:停止目前屏幕的输出
  • susp:送出一个 terminal stop 的喜好给正在 run 的程序

比如要设置 ctrl + h 来进行字符的删除

1
2
3
stty erase ^h
# 默认可以看到使用 ^? 但是实际测试的时候,改不回去了

1
2
3
4
错误操作问题:在 windows 下 ctrl + s 是保存功能,在 Linux 使用 vim 时,使用 ctrl + s 整个画面死锁,不能动了,是什么原因?

通过 stty -a 可以看到 ctrl + s 是 stop 功能,停止目前屏幕的输出了,恢复输出的话就是 start,ctrl + q

除了 stty 之外,bash 还有自己的一些终端机设置

1
set [-uvCHhmBx]

选项与参数:

  • u:预设不启用。若启用后,当使用未设置变量时,会显示错误信息
  • v:预设不启用。若启用后,在信息被输出前,会先显示信息的原始内容
  • x:预设不启用。若启用后,在指令被执行前,会显示指令内容(前面有 ++ 符号)
  • h:预设启用。与历史命令有关
  • H:预设启用。与历史命令有关
  • m:预设启用。与工作管理有关
  • B:预设启用。与括号[] 的作用有关
  • C:预设不启用。若使用 > 等,则若文件存在时,该文件不会被覆盖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 范例 1: 显示目前所有的 set 设置
[mrcode@study ~]$ echo $-
himBH

# 范例 2:若使用未定义变量时,则显示错误信息
[mrcode@study ~]$ set -u
[mrcode@study ~]$ echo $mrcode
-bash: mrcode: unbound variable
[mrcode@study ~]$ set +u # 关闭该功能使用 + 号
[mrcode@study ~]$ echo $mrcode

[mrcode@study ~]$

# 范例 3:执行前,显示该指令内容
[mrcode@study ~]$ set -x
++ printf '\033]0;%s@%s:%s\007' mrcode study '~'
[mrcode@study ~]$ echo ${HOME}
+ echo /home/mrcode
/home/mrcode
++ printf '\033]0;%s@%s:%s\007' mrcode study '~'

#要输出的指令都会被先打印到屏幕上,前面会多出 + 号

另外,还有其他的按键设置功能,前一小节提到的 /etc/inputrc 这个文件里面设置。还有例如 /etc/DIR_COLORS* 与 /usr/share/terminfo/* 等,也都是与终端机有关的环境配置文件。但是这里不建议修改 tty 的环境,因为 bash 的环境以及设置的很亲和了。

bash 默认的组合键汇总如下

组合按键 功能
ctrl + c 终止目前的命令
ctrl + D 输入结束(EOF),例如邮件结束的时候
ctrl + M Enter
ctrl + S 暂停屏幕的输出
ctrl + Q 恢复屏幕的输出
ctrl + U 在提示字符下,将整列命令删除
ctrl + Z 暂停 目前的命令

通配符与特殊符号

bash 操作环境中,通配符(wildcard)是非常有用的,利用 bash 处理数据就更方便了。下面是一些常用的通配符:

符号 含义
* 代表「0 个到无穷多个」任意字符
? 代表「一定有一个」任意字符
[] 代表「一定由一个在括号内」的字符(非任意字符)。例如[abcd] 则表示一定由一个字符,可能是 a、b、c、d 中的任意一个
[-] 若有减号在括号中时,表示「在编码顺序内的所有字符」。例如[0-9],表示 0~9 之前所有数字
[^] 若括号中的第一个字符为指数符号 ^,表示反向旋转,例如[^abc],表示不包含 a、b、c

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 范例 1:找出 /etc 下一 cron 开头的文件名
[mrcode@study ~]$ ll -d /etc/cron* # -d 仅显示目录
drwxr-xr-x. 2 root root 54 Oct 4 18:25 /etc/cron.d
drwxr-xr-x. 2 root root 57 Oct 4 18:25 /etc/cron.daily

# 范例 2:找出 etc 下刚好是 5 个字母的目录名
[mrcode@study ~]$ ll -d /etc/?????
drwxr-x---. 3 root root 83 Oct 4 18:38 /etc/audit
drwxr-xr-x. 4 root root 71 Oct 4 18:25 /etc/avahi

# 范例 3:找出 etc 下目录名含有数字的目录
[mrcode@study ~]$ ll -d /etc/*[0-9]* # 记得通过 ** 来模糊匹配
drwxr-xr-x. 4 root root 78 Oct 4 18:22 /etc/dbus-1
-rw-r--r--. 1 root root 5725 Aug 6 21:44 /etc/DIR_COLORS.256color

# 范例 4:找出 etc 下,目录名开头不是小写的目录
[mrcode@study ~]$ ll -d /etc/[^a-z]*
ls: cannot access /etc/[^a-z]*: No such file or directory
# 看到没有找到不是小写的目录,换成非大写的,出来结果了
[mrcode@study ~]$ ll -d /etc/[^A-Z]*
drwxr-xr-x. 3 root root 101 Oct 4 18:23 /etc/abrt
-rw-r--r--. 1 root root 16 Oct 4 18:31 /etc/adjtime

# 范例 5:将范例 4 中找到的文件复制到 /tmp/upper 中
[mrcode@study ~]$ mkdir /tmp/upper; cp -a /etc/[^a-z]* /tmp/upper


除了通配符外,bash 环境中的特殊符号还有以下项,这里进行整理:

符号 含义
# 批注、注释符号
\ 跳脱符号、转义符号
` `
; 连续指令下达分隔符:连续性命令的节点。与管线命令不相同
~ 用户的家目录
$ 取用变量前导符
& 工作控制(job control):将指令变成背景下工作
! 逻辑运算意义上的「非」not 的意思
/ 目录符号:路径分割的符号
>>> 数据流重导向:输出导向,分别是「覆盖」和「追加」
<<< 数据流重导想:输入导向(下个章节讲解)
'' 单引号,不具有变量替换功能,**$** 变为纯文本
"" 双引号,具有变量替换功能,**$** 可保留相关功能
`` 两个 「」中间为可以先执行的指令,也可以使用 $()`
() 在中间为 子 shell 的起始与结束
{} 在中间为命令区块的组合

以上是 bash 环境中常见的特殊符号整理,理论上,文件名尽量不要使用上述字符

数据流重导向

数据流重导向(redirect),将数据传导到其他地方去,将某个指令执行后应该要出现在屏幕上的数据,给传输到其他的地方。

例如文件或则是装置(打印机之类的),数据流重导向在 Linux 的文本模式下很重要,尤其是想要将某些数据存储下来时,就更有用了

什么是数据流重导向?

执行一个指令时,这个指令可能会由文件读入资料,经过处理之后,再将数据输出到屏幕上。

  • standard output:标准输出 STDOUT
  • standard error output:标准错误输出 STDERR

standard output 与 standard error output

可以简单理解为:

  • 标准输出:指令执行所回传的正确的信息
  • 标准错误输出:指令执行失败后,所回传的错误信息

比如,我们的系统默认有 /etc/crontab 但无 /etc/mrcode ,此时若下达 cat /etc/crontab /etc/mrcode 指令时,cat 会执行:

  • 标准输出:读取 /etc/crontab 后,将该文件内容显示到屏幕上
  • 标准错误输出:因为无法找到 /etc/mrcode ,因此在屏幕上显示错误信息

可见不管正确或错误信息都输出到屏幕上,那么可以通过数据流重导向将 stdoutstderr 分别传送到其他文件或装置去,就达到了分别输出的目的,语法如下:

  • 标准输入(stdin 简写):代码为 0,使用 <<<
  • 标准输出(stdout):代码为 1,使用 >>>
  • 标准错误输出(stderr):代码为 2,使用 2>2>>

为了理解 stdoutstderr,下面进行练习

1
2
3
4
5
6
# 范例 1:观察你的系统根目录 / 下各目录的文件名、权限与属性,并记录下来
[mrcode@study ~]$ ll / # 会把结果输出到屏幕
[mrcode@study ~]$ ll / > ~/rootfile # 会吧结果输出到指定的 rootfile 文件中
[mrcode@study ~]$ ll ~/rootfile
-rw-rw-r--. 1 mrcode mrcode 1078 Dec 1 22:53 /home/mrcode/rootfile

上面的指令流程:

  1. 该文件若不存在,系统会自动创建文件
  2. 该文件若存在,那么会清空内容,再写入数据

标准输出和标准错误输出,单个符号是覆盖数据,2 个符号的是追加数据;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 范例 2:利用一般身份账户查找 /home 下是否有 .bashrc 的文件存在
# 为了这个练习有效果,我在 abc 目录下用 root 身份创建了 .bashrc 空文件
# 并且,把 abc 目录的权限都改为只有 root 才能读取
[mrcode@study home]$ ll
total 4
drwx------. 2 root root 32 Dec 1 23:01 abc
drwx------. 18 mrcode mrcode 4096 Dec 1 22:53 mrcode
[mrcode@study home]$ find /home/ -name .bashrc
/home/mrcode/.bashrc # 标准输出
find: ‘/home/abc’: Permission denied # 标准错误输出

# 使用数据流重导向指令,发现标准输出不显示在屏幕了
# 但是标准错误输出还显示在屏幕上
[mrcode@study home]$ find /home/ -name .bashrc > list
-bash: list: Permission denied # 这里报错了,是因为 home 目录是普通用户是无法写数据的
# 写在 mrcode 自己的目录下
[mrcode@study home]$ find /home/ -name .bashrc > ./mrcode/list
find: ‘/home/abc’: Permission denied

# 范例 3: 使用标准输出 和 标准错误输出都输出到文件中
[mrcode@study home]$ find /home/ -name .bashrc > ./mrcode/list 2> ./mrcode/listerr

/dev/null 垃圾桶黑洞装置的特殊写法

就是可以将任何信息吃掉的黑洞装置

1
2
3
4
# 范例 4:将错误的数据丢弃,屏幕上显示正确的数据
[mrcode@study ~]$ find /home/ -name .bashrc 2> /dev/null
/home/mrcode/.bashrc

那么能否将正确和错误的数据都写到同一个文件呢?需要特殊的写法才行

1
2
3
4
5
6
7
8
9
10
# 范例 5:将指令的数据全部写入 list 文件中

# 错误的写法,可能会交叉写入该文件,数据错乱
[mrcode@study ~]$ find /home/ -name .bashrc > list 2> list

# 正确的写法
[mrcode@study ~]$ find /home/ -name .bashrc > list 2>&1
# 正确的写法
[mrcode@study ~]$ find /home/ -name .bashrc &> list

standard input :<<<

简单来说:将原本需要由键盘输入的数据,该由文件内容来代替。

1
2
3
4
5
# 范例 6:利用 cat 指令来建立一个文件的简单流程
[mrcode@study ~]$ cat > catfile
testing
cat file test
# 这里使用快捷键 ctrl + d 来离开

上面使用 cat > catfile ,使用了数据流重导向,catfile 文件会被建立,内容是需要键盘输入,也就是上面的两行内容。这里可以使用标准输入来取代键盘的敲击

1
2
3
4
5
6
# 范例 7:用 stdin 代替键盘输入,建立新文件的简单流程
[mrcode@study ~]$ cat > catfile < ~/.bashrc
[mrcode@study ~]$ ll catfile ~/.bashrc
-rw-rw-r--. 1 mrcode mrcode 231 Dec 1 23:28 catfile
-rw-r--r--. 1 mrcode mrcode 231 Aug 8 20:06 /home/mrcode/.bashrc
# 大小一模一样,几乎像是用 cp 来复制一样

<< 表示接受的输入字符。比如:我要用 cat 直接将输入的信息输出到 catfile 中,且当由键盘输入 eof 时,该次输入就结束

1
2
3
4
5
6
7
8
9
10
[mrcode@study ~]$ cat > catfile << 'eof'
> This is a test
> Ok new stop
> eof
[mrcode@study ~]$ cat catfile
This is a test
Ok new stop
# 只有两行数据,不会存在关键词一行
# 这里就有点类似判定结束标准输入的功能

<<可以代替快捷键 ctrl + d,来终止输入,那为什么要使用命令输出重导向呢?

  • 屏幕输出的信息很重要,而且我们需要将它存下来的时候
  • 背景执行的程序,不希望他干扰屏幕正常的输出结果的时候
  • 一些系统的例行命令(例如在 /etc/crontab 中的文件)的执行结果,希望他可以存下来时
  • 一些执行命令可能已知错误信息时,想以2>/dev/null丢弃时
  • 错误信息与正确信息需要分别输出时

当然还有其他的使用场景,最简单的就是网友们经常问到:为何我的 root 都会受到系统 crontab 寄来的错误信息呢?这个是场景的错误,而如果我们已经知道这个错误信息是可以忽略的时,2> errorfile 这个功能就很重要了吧

1
2
3
4
5
6
7
8
9
# 问:假设要将 echo `error message` 以 standard error output 的格式来输出,怎么做?
答:既然有 2>&1 来将 2> 转到 1> 去,
那么就应该有 1>&2,可以这样做

[mrcode@study ~]$ echo 'error message' 1>&2
error message
[mrcode@study ~]$ echo 'error message' 2> /dev/null 1>&2


命令执行的判断依据:;&&||

很多指令想要一次输入去执行,而不想分此执行,基本上有两种方法:

  • 第十二章要介绍的 shell script 脚本执行
  • 通过本章的知识点来完成

cmd;cmd 不考虑指令相关性的连续指令下达

比如子关机的时候希望可以执行两次 sync 同步写入磁盘后,再 shutdown 计算机

1
sync; sync; shutdown -h now

这个是两个指令之前没有关系的执行,前一个执行完成后,就执行后一个;如果是这样的情况:在某个目录下创建文件,如果目录存在,则创建文件,如果不存在则不做任何操作,该指令就无法完成了

$?(指令回传值)与 &&||

前面章节讲到过指令回传值:若前一个指令执行的结果为正确,在 Linux 下会回传一个 $?=0 的值。可以通过判断这个值来是否执行后面的指令

逻辑操作符这里就不过多解释了

  • &&:前一个执行正确,后面才会执行
  • ||:前一个执行正确,后面的不会执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 范例 1:使用 ls 查阅 目录 /tmp/abc 是否存在,若存在则用 touch 建立 /tmp/abc/hehe
[mrcode@study ~]$ ls /tmp/abc && touch /tmp/abc/hehe
ls: cannot access /tmp/abc: No such file or directory
# 只有 ls 报错了,后续的指令没有报错,说明没有被执行

[mrcode@study ~]$ mkdir /tmp/abc
[mrcode@study ~]$ ls /tmp/abc && touch /tmp/abc/hehe
[mrcode@study ~]$ ll /tmp/abc/
total 0
-rw-rw-r--. 1 mrcode mrcode 0 Dec 2 00:22 hehe

# 范例 2:测试 /tmp/abc 是否存在,若不存在则建立该目录,若存在则不做操作
[mrcode@study ~]$ rm -r /tmp/abc/

[mrcode@study ~]$ ls /tmp/abc || mkdir /tmp/abc
ls: cannot access /tmp/abc: No such file or directory
# 报错没有找到目录,后面没有报错,但是查看缺创建了,证明执行了

[mrcode@study ~]$ ll -d /tmp/abc/
drwxrwxr-x. 2 mrcode mrcode 6 Dec 2 00:24 /tmp/abc/

# 而下面这个没有报错,后面也没有报错,说明只执行了前面的指令
[mrcode@study ~]$ ls /tmp/abc || mkdir /tmp/abc


# 范例 3:我不清楚 /tmp/abc 是否存在,但就是要建立 /tmp/abc/hehe 文件
[mrcode@study ~]$ ls /tmp/abc || mkdir /tmp/abc/ && touch /tmp/abc/hehe


范例三,对于的表达式对于 java 或则 js 来说,理解不太一样,如下分析:

  • 第一种情况:**/tmp/abc** 不存在
    1. ls /tmp/abc 回传 $?≠0,结果为 false
    2. 则执行创建操作,由于会成功,故 $?=0,结果为 true
    3. 则执行创建 hehe 文件
  • 第二种情况:**/tmp/abc** 存在
    1. ls /tmp/abc 回传 $?=0,结果为 true
    2. || 遇到 true 后面的不会执行,但是 结果会往后传递
    3. 前一个结果为 true,那么就执行创建

只要注意:linux 指令是从左往右执行的,只有相邻的指令会被特殊符号阻断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
## 例题:以 ls 测试 /tmp/mrcode 是否存在,存在则显示 exist, 不存在则显示 not exist
[mrcode@study ~]$ ls /tmp/mrcode && echo 'exist' || echo 'not exist'
ls: cannot access /tmp/mrcode: No such file or directory
not exist

[mrcode@study ~]$ mkdir /tmp/mrcode
[mrcode@study ~]$ ls /tmp/mrcode && echo 'exist' || echo 'not exist'
exist
# 可以看到的确只有相邻的两个指令会被阻断

# 如果搞不清楚他们的逻辑的话,就会出现下面这种情况
[mrcode@study ~]$ ls /tmp/mrcode || echo 'exist' && echo 'not exist'
ls: cannot access /tmp/mrcode: No such file or directory
exist
not exist
# 两种都出现了,不存在,则执行 exist,后面并且关系,再次执行
[mrcode@study ~]$ mkdir /tmp/mrcode
[mrcode@study ~]$ ls /tmp/mrcode || echo 'exist' && echo 'not exist'
not exist
# 存在,则不执行 exit,true 往后传递,则执行 not exist

只要注意:linux 指令是从左往右执行的,只有相邻的指令会被特殊符号阻断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
## 例题:以 ls 测试 /tmp/mrcode 是否存在,存在则显示 exist, 不存在则显示 not exist
[mrcode@study ~]$ ls /tmp/mrcode && echo 'exist' || echo 'not exist'
ls: cannot access /tmp/mrcode: No such file or directory
not exist

[mrcode@study ~]$ mkdir /tmp/mrcode
[mrcode@study ~]$ ls /tmp/mrcode && echo 'exist' || echo 'not exist'
exist
# 可以看到的确只有相邻的两个指令会被阻断

# 如果搞不清楚他们的逻辑的话,就会出现下面这种情况
[mrcode@study ~]$ ls /tmp/mrcode || echo 'exist' && echo 'not exist'
ls: cannot access /tmp/mrcode: No such file or directory
exist
not exist
# 两种都出现了,不存在,则执行 exist,后面并且关系,再次执行
[mrcode@study ~]$ mkdir /tmp/mrcode
[mrcode@study ~]$ ls /tmp/mrcode || echo 'exist' && echo 'not exist'
not exist
# 存在,则不执行 exit,true 往后传递,则执行 not exist

管线命令(pipe)

bash 命令执行的时候有输出数据,如果这群数据必须经过几道手续之后才能得到我们想要的格式,这就可以使用管线命令(pipe)来完成了

假设我们想知道 /etc/ 下有多少文件,可以使用 ls /etc/ 来查询,不过因为文件太多了,输出占满整个屏幕,导致最开始是什么文件看不到了,这就可以通过管线命令结合 less 指令来达成

1
2
[mrcode@study ~]$ ls -al | less

如此一来, ls -al 指令输出后的内容,能够被 less 读取,并且利用 less 的功能,可以前后翻动相关信息

管线命令仅能处理由前一个指令传来的正确信息(standard output),对于 standard error 没有直接处理的能力,整体管线命令可以使用下图表示

在每个管线后面接的第一个数据必定是「指令」,而且这个指令必须能接受 standard input 的数据才可以,这样的指令则是「管线命令」,例如 lessmoreheadtail 等都是可以接受 standard input 的管线命令。而 lscpmv 等就不是管线命令了,因为他们不不会接受来自 stdin 的数据。管线命令主要有两个比较需要注意的地方:

  • 管线命令仅会处理 standard output ,对于 standard error output 会忽略
  • 管线命令必须要能接受来自前一个指令的数据成为 standard input 继续处理才行

如果硬要 standard error 可以被管线命令所使用可以使用如下方式

那么下面来玩一些管线命令,以下知识点对系统管理费用有用

截取命令 cut、grep

简单说:将一段时间经过分析后,取出我们想要的。或则是经过由分析关键词,取得我们所想要的那一行。一般来说,截取信息通常是针对一行一行来分析的。

cut

1
2
3
cut -d '分割字符' -f fields  # 用于有特定分割字符
cut -c 字符区间 # 用于排列整齐的信息

选项与参数:

  • d:后面接分割字符。与 -f 一起使用
  • f:依据 -d 的分割字符将一段信息分区成数段,用 -f 取出第几段的意思
  • c:以字符(characters)的单位取出固定字符区间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 范例 1:将 PATH 变量取出,我要找出第 5 个路径
[mrcode@study ~]$ echo ${PATH}
/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/mrcode/.local/bin:/home/mrcode/bin
# 数量是从 1 开始,不是从 0 哟
[mrcode@study ~]$ echo ${PATH} | cut -d ':' -f 5
/usr/sbin

# 取出第 5 个和第 6 个
[mrcode@study ~]$ echo ${PATH} | cut -d ':' -f 5,6
/usr/sbin:/home/mrcode/.local/bin


# 范例 2 :将 export 输出的信息,取得第 12 字符以后的所有字符串
[mrcode@study ~]$ export
declare -x HISTCONTROL="ignoredups"
declare -x HISTSIZE="1000"
declare -x HOME="/home/mrcode"
declare -x HOSTNAME="study.centos.mrcode"
...

# 以上数据每个都是排列整齐的,有着 declare -x 前缀
# 那么想要把前缀去掉,就可以这样做
[mrcode@study ~]$ export | cut -c 12-
HISTCONTROL="ignoredups"
HISTSIZE="1000"
HOME="/home/mrcode"
# 使用 12-15 则是截取出这个区间的字符
# 使用 12 则只截取 12 这个字符

# 范例 3 :用 last 将显示的登陆者信息,仅留下用户名
[mrcode@study ~]$ last
# 账户 终端机 登录 IP 日期时间
mrcode pts/1 192.168.0.105 Mon Dec 2 01:25 still logged in
mrcode pts/0 192.168.0.105 Mon Dec 2 01:25 still logged in
mrcode pts/1 192.168.0.105 Mon Dec 2 00:21 - 01:12 (00:51)
# 用空格分隔的数据,那么可以这样做
[mrcode@study ~]$ last | cut -d ' ' -f 1
mrcode
mrcode
mrcode
# 其实 账户和终端机之间的空格有好几个,并不是一个所以使用下面的命令并不能把 终端机一列也提取出来
last | cut -d ' ' -f 1,2

cut 主要的用途:将同一行里面的数据进行分解

常使用在分析一些数据或文字数据的时候,因为有时候会以某些字符当做分区的参数,然后将数据切割,以取得我们所需要的数据,作者常常在分析 log 文件的时候,但是 cut 在处理多空格相连的数据时,就比较麻烦,所以某些常见可能需要使用下一章节要讲解的 awk 来取代

grep

cut 是将一行信息中,取出某部分我们想要的数据,而 grep 则是分析一堆信息,若一行当中有匹配的数据,则将这一行数据拿出来

1
2
grep [-acinv] [--color=auto] '搜索的字符串' filename

选项与参数:

  • a:将 binary 文件以 text 文件的方式搜索数据
  • c:计算找到「搜索字符」的次数
  • i:忽略大小写
  • n:输出行号
  • v:反向选择,显示出没有搜索字符串的那一行数据
  • --color:可以将找到的关键词部分加上颜色显示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 范例 1:将 last 中,有出现 root 的那一行找出来
[mrcode@study ~]$ last | grep 'root'
root tty3 Sun Oct 6 23:16 - crash (22:40)
root tty4 Fri Oct 4 22:48 - 22:48 (00:00)
# 会发现 root 被高亮颜色了,我们时候 type 命令查看,发现被自动加上了 color 参数
[mrcode@study ~]$ type grep
grep is aliased to 'grep --color=auto'

# 范例 2:与 范例 1 相反,不要 root 的数据
[mrcode@study ~]$ last | grep -v 'root'
mrcode pts/1 192.168.0.105 Mon Dec 2 01:25 still logged in
mrcode pts/0 192.168.0.105 Mon Dec 2 01:25 still logged in
mrcode pts/1 192.168.0.105 Mon Dec 2 00:21 - 01:12 (00:51)
reboot system boot 3.10.0-1062.el7. Fri Oct 4 18:47 - 03:43 (08:56)

# 范例 3:在 last 的输出信息中,只要有 root 就取出,并且只取第一栏
# 结合 cut 命令取出第一栏
[mrcode@study ~]$ last | grep 'root' | cut -d ' ' -f 1
root
root

# 范例 4:取出 /etc/man_db.conf 内涵 MANPATH 的那几行
[mrcode@study ~]$ grep 'MANPATH' /etc/man_db.conf
# MANDATORY_MANPATH manpath_element
# MANPATH_MAP path_element manpath_element


grep 支持的语法很多,用在正规表示法里,能够处理的数据太多。但是这里先不了解正规表示法,下一章再来讲解

这里只需要了解下,grep 可以解析一行文字,取得关键词,若改行有存在关键词,就会整行取出来

排序命令:sort、wc、uniq

sort

可以依据不同的数据形态来排序。例如数字与文字的排序不一样,另外,排序的字符与语系的编码有关,因此,如果需要排序时,建议使用 LANG=C 来让语系统一,数据排序比较好一些

1
2
sort [-fbMnrtuk] [file or stdin]

选项与参数:

  • f:忽略大小写的差异
  • b:忽略最前面的空格符
  • M:以月份的名字来排序,例如 JANDEC 等排序方法
  • n:使用纯数字进行排序,默认是以文字形态来排序
  • r:反向排序
  • uuniq,相同的数据中,仅出现一行代表,也就是去重
  • t:分隔符,预设使用 「tab」来分割
  • k:以那个区间(field)来进行排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 范例 1:个人账户都记录在 /etc/passwd 下,将账户进行排序
[mrcode@study ~]$ cat /etc/passwd | sort
abrt:x:173:173::/etc/abrt:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
avahi:x:70:70:Avahi mDNS/DNS-SD Stack:/var/run/avahi-daemon:/sbin/nologin
bin:x:1:1:bin:/bin:/sbin/nologin
chrony:x:993:990::/var/lib/chrony:/sbin/nologin
# 可以看到按字符排序了

# 范例 2:/etc/passwd 内容是以 : 来分割的,想使用第三栏进行排序
[mrcode@study ~]$ cat /etc/passwd | sort -t ':' -k 3
root:x:0:0:root:/root:/bin/bash
mrcode:x:1000:1000:mrcode:/home/mrcode:/bin/bash
qemu:x:107:107:qemu user:/:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
# 第三栏是数字,但是这里并没有按数字大小来排序,因为默认使用文字排序
# 与数值大小进行排序
[mrcode@study ~]$ cat /etc/passwd | sort -t ':' -k 3 -n
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

# 范例 3:利用 last ,将输出的数据仅取账户,并排序
[mrcode@study ~]$ last | cut -d ' ' -f 1 | sort

mrcode
mrcode


uniq

1
2
3
4
uniq [-ic]
- i:忽略大小写
- c:进行计数

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 范例 1: 使用 last 将账户列出,仅取出账户,排序后去重
[mrcode@study ~]$ last | cut -d ' ' -f 1 | sort | uniq

mrcode
reboot
root
wtmp

# 范例 2:以上题,统计每个账户登录的总次数
[mrcode@study ~]$ last | cut -d ' ' -f 1 | sort | uniq -c
1
136 mrcode
19 reboot
2 root
1 wtmp

# 第一行和 wtmp 是 last 的默认字符,可以忽略

wc

wc 可以计算输出的信息。比如:/etc/man_db.conf 这个文件里面有多少字?多少行?

1
2
3
4
5
6
wc [-lwm]

-l:仅列出行
-w:仅列出多少字(英文单字)
-m:多少字符

1
2
3
4
5
6
7
8
9
10
11
12
13
# 范例 1:/etc/man_db.conf 这个文件里面有多少字
[mrcode@study ~]$ cat /etc/man_db.conf | wc
行 字数 字符数
131 723 5171

# 范例 2:last 可以输出登陆者,但是 last 最后两行并非账户内容,那么该如何以一行指令取得登录系统的总人次?
last | grep [a-zA-Z] | grep -v 'wtmp' | grep -v 'reboot' | grep -v 'unknown' | wc -l
138
# grep 正则匹配,排除了非英文字符的账户
# grep -v 反向选择,相当于排除了指定的账户
# 最后使用 wc 统计行数


双向重导向:tee

前一节讲解到 > 会将数据流整个栓送给文件或装置,因此除非去读取该文件或装置,那么如果想要将整个暑假流的处理过程中将某段信息存下来该怎么做?就可以使用 tree

1
2
3
4
Standard input   ------> tee --------> Screen

file
# 流程如上

tee 会同时将数据流分送到文件与屏幕,而输出到屏幕的其实就是 stdout,那么就可以让指令继续处理

1
2
3
tee [-a] file
- a:以累加(append)的方式,将数据加入 file 中

1
2
3
4
5
6
# 将 last 内容输出到 last.list 文件中,并继续处理
[mrcode@study ~]$ last | tee last.list | cut -d " " -f 1

# 将 ls 数据存一分到 ~/homefile 同时屏幕也输出信息
[mrcode@study ~]$ ls -l /home/ | tee ~/hoefile | more

字符转换命令:tr、col、join、paste、expand

vim 程序编辑器中提到过 DOS 换行符与 Unix 不一样,并且可以使用 dos2unixunix2dos 来完成转换。

那么思考下,是否还有其他的字符转换命令,比如:将大写改成小写、将数据中的 tab 转成空格、如何将两篇信息整合成一篇?

tr:正则替换或删除字符

tr 可以用来删除一段信息中的文字,或则是进行文字信息的替换

1
2
3
4
5
tr [-ds] SET1 ...

-d:删除信息当中的 SET1 这个字符串
-s:替换重复的字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 范例 1:将 last 输出的信息中,原有的小写变成大写字符
[mrcode@study ~]$ last | tr '[a-z]' '[A-Z]'
MRCODE PTS/1 192.168.0.105 MON DEC 2 07:00 STILL LOGGED IN

# 范例 2:将 /etc/passwd 输出的信息中,将冒号 : 删除
[mrcode@study ~]$ cat /etc/passwd | tr -d ':'
rootx00root/root/bin/bash

# 范例 3:将 /etc/passwd 转成 dos 换行到 ~/passwd 中,再将 ^M 符号删除
# 由于我这里没有安装 unix2dos 这里无法实际演示
cp /etc/passwd ~/passwd && unix2dos ~/passwd
file /etc/passwd ~/passwd
cat ~/passwd | tr -d '\r' > ~/passwd.linux
# \r 是 dos 的换行符
ll /etc/passwd ~/passwd*
# 就会发现处理之后和源文件一样大小了
# 本例子是:将 unix 转成 dos,/n 转成了 /r/n ,然后使用 tr 命令将 /r 删除了,相当于又还原了

#那么经过上面的分析之后,其实转换程序就是转换了换行符,那么可以利用 tr 手动来完成转换
[mrcode@study ~]$ cp /etc/passwd ~/passwd
[mrcode@study ~]$ file /etc/passwd ~/passwd
/etc/passwd: ASCII text
/home/mrcode/passwd: ASCII text
# 将 unix 换行符 \n 替换成 dos 换行符 \r\n
[mrcode@study ~]$ cat passwd | tr '\n' '\r\n' > passwd.dos
[mrcode@study ~]$ file passwd*
passwd: ASCII text
passwd.dos: ASCII text, with CR line terminators # 可以看到已经变了
# 再将 \r 删掉
[mrcode@study ~]$ cat passwd | tr -d '\r' > passwd.linux
[mrcode@study ~]$ file passwd*
passwd: ASCII text
passwd.dos: ASCII text, with CR line terminators
passwd.linux: ASCII text


该指令也可以写在正规表示法里面,因为他也是由正规表示法的方式来取代数据的,比如上面使用 [] 来设置字符,通常用来取代文件中的怪异符号。

col:将 tab 转换成对等的空格

1
2
3
4
col [-xb]

-x:将 tab 键转换成对等的空格键

1
2
3
4
5
6
7
# 范例 : 利用 cat -A 显示出所有的特殊按键,最后以 col 将 tab 转成空白
[mrcode@study ~]$ cat -A /etc/man_db.conf
MANDATORY_MANPATH^I^I^Imanpath_element$ # ^I 的符号就是 tab

[mrcode@study ~]$ cat /etc/man_db.conf | col -x | cat -A | more
MANDATORY_MANPATH /usr/src/pvm3/man$

虽然 col 有特殊的用途,但是很多时候可以用来简单的将 tab 取代为空格键,并且可以取代会对等宽度的空格

join:合并两个文件中相同行的数据

1
2
join [-ti12] file1 file2

选项与参数:

  • tjoin 默认以空格符分割数据,并且比对「第一个字段」的数据,如果两个文件相同,则将两笔数据连城一行,且第一个字段放在第一个
  • i:忽略大小写
  • 1:数值 1,代表「第一个文件要用哪个字段来分析」
  • 2:数值 2,代表「第二个文件要用哪个字段来分析」
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 范例 1:用 root 身份,将 /etc/passwd 与 /etc/shadow 相关数据整合成一栏
[root@study ~]# head -n 3 /etc/passwd /etc/shadow
==> /etc/passwd <==
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin

==> /etc/shadow <==
root:$6$oTg/fYGfv9/GIl6h$UEcmYlRZacV757rHtXlvmu5xH5TWGfqd3eDOEotB3CAc5mcW5UEoMTSg0pDICd/sYGrEScsHQY9tYZY0FGkKS1::0:99999:7:::
bin:*:17834:0:99999:7:::
daemon:*:17834:0:99999:7:::
# 输出的信息来看,最左边的的账户有相同的账户,且以 : 分割

[root@study ~]# join -t ':' /etc/passwd /etc/shadow | head -n 3
# 看到了吗,作用就是将某个字段的数据合并成一段
root:x:0:0:root:/root:/bin/bash:$6$oTg/fYGfv9/GIl6h$UEcmYlRZacV757rHtXlvmu5xH5TWGfqd3eDOEotB3CAc5mcW5UEoMTSg0pDICd/sYGrEScsHQY9tYZY0FGkKS1::0:99999:7:::
bin:x:1:1:bin:/bin:/sbin/nologin:*:17834:0:99999:7:::
daemon:x:2:2:daemon:/sbin:/sbin/nologin:*:17834:0:99999:7:::

# 范例 2:/etc/passwd 第四个字段是 GID,/etc.group 的第三个字段是 GID ,那么如何将两个文件合并?
[root@study ~]# head -n 3 /etc/passwd /etc/group
==> /etc/passwd <==
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin

==> /etc/group <==
root:x:0:
bin:x:1:
daemon:x:2:

# 下面两种写法一致
join -t ':' -1 4 -2 3 /etc/passwd /etc/group | head -n 3
join -t ':' -1 4 /etc/passwd -2 3 /etc/group | head -n
# 报错了,提示没有排序过,所以在使用时要先对内容排序,这样才能合并两行数据
join: /etc/passwd:6: is not sorted: sync:x:5:0:sync:/sbin:/bin/sync
join: /etc/group:11: is not sorted: wheel:x:10:mrcode
# 看下面被整合的内容
0:root:x:0:root:/root:/bin/bash:root:x:
1:bin:x:1:bin:/bin:/sbin/nologin:bin:x:
2:daemon:x:2:daemon:/sbin:/sbin/nologin:daemon:x:


paste:将两行贴在一起

将两行贴在一起,且中间以 tab 隔开

1
2
3
4
5
paste [-d] file1 file2

-d:后面可以接分割符。默认以 tab 来分割
- :如果 file 部分写成 -,表示来自 standard input

1
2
3
4
5
6
7
8
9
10
11
12
# 范例 1:用 root 身份,将 /etc/passwd 与 /etc/shadow 同一行贴在一起
[root@study ~]# paste /etc/passwd /etc/shadow | head -n 3
root:x:0:0:root:/root:/bin/bash root:$6$oTg/fYGfv9/GIl6h$UEcmYlRZacV757rHtXlvmu5xH5TWGfqd3eDOEotB3CAc5mcW5UEoMTSg0pDICd/sYGrEScsHQY9tYZY0FGkKS1::0:99999:7:::
bin:x:1:1:bin:/bin:/sbin/nologin bin:*:17834:0:99999:7:::
daemon:x:2:2:daemon:/sbin:/sbin/nologin daemon:*:17834:0:99999:7:::

# 范例 2:先将 /etc/group 用 cat 读出,然后与范例 1 贴在一起,且仅取出前三行
# paset 文件部分可以是多个,这里最后一个文件使用了 -,也就是 cat /cat/etc/group
[root@study ~]# cat /etc/group | paste /etc/passwd /etc/shadow - | head -n 3
root:x:0:0:root:/root:/bin/bash root:$6$oTg/fYGfv9/GIl6h$UEcmYlRZacV757rHtXlvmu5xH5TWGfqd3eDOEotB3CAc5mcW5UEoMTSg0pDICd/sYGrEScsHQY9tYZY0FGkKS1::0:99999:7::: root:x:0:
bin:x:1:1:bin:/bin:/sbin/nologin bin:*:17834:0:99999:7::: bin:x:1:
daemon:x:2:2:daemon:/sbin:/sbin/nologin daemon:*:17834:0:99999:7::: daemon:x:2:

expand:将 tab 转成空格

1
2
3
4
expand [-t] file

-t:后面可以接数字。一般来说,一个 tab 可以用 8 个空格取代,这里自定义几个空格取代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 范例 1:将 /etc/man_db.conf 内行首为 MANPATH 的字样取出,仅取前三行
[root@study ~]# grep '^MANPATH' /etc/man_db.conf | head -n 3
MANPATH_MAP /bin /usr/share/man
MANPATH_MAP /usr/bin /usr/share/man
MANPATH_MAP /sbin /usr/share/man
# 行首正则为 ^,下接讲解

# 范例 2:承上,将所有的符号都列出来
[root@study ~]# grep '^MANPATH' /etc/man_db.conf | head -n 3 | cat -A
MANPATH_MAP^I/bin^I^I^I/usr/share/man$
MANPATH_MAP^I/usr/bin^I^I/usr/share/man$
MANPATH_MAP^I/sbin^I^I^I/usr/share/man$
# ^I 是 tab

# 范例 3:承上,将 tab 转成 6 个空格
[root@study ~]# grep '^MANPATH' /etc/man_db.conf | head -n 3 | expand -t 6 | cat -A
MANPATH_MAP /bin /usr/share/man$
MANPATH_MAP /usr/bin /usr/share/man$
MANPATH_MAP /sbin /usr/share/man$
# 可以看到 tab 被替换成空格了

有一个需要特别注意:tab 最大功能就是格式排列整齐,但是换成空格之后,就不一定是排列整齐的了,也可以参考一下 unexpand 这个将空白转成 tab 的指令

1
2
3
4
5
6
[root@study ~]# grep '^MANPATH' /etc/man_db.conf | head -n 3 | expand -t 6 | unexpand -t 6 | cat -A
MANPATH_MAP /bin^I^I^I/usr/share/man$
MANPATH_MAP /usr/bin^I^I/usr/share/man$
MANPATH_MAP /sbin^I^I^I/usr/share/man$
# 可以看到,范例 3 的还可以被 unexpand 给转换回来

分区命令:split

split 可以分割文件,按文件大小或行数来分割

1
2
3
4
5
6
7
split [-bl] file PREFIX

-b:后面可接要分区的大小,可加单位,如 b、k、m 等
-l:以行数进行分区

PREFIX:表示分区文件命名前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 范例 1:/etc/services 有 600 多 k,若想要分成 300k 一个文件
[mrcode@study ~]$ cd /tmp; split -b 300k /etc/services servers
[mrcode@study tmp]$ ll servers*
-rw-rw-r--. 1 mrcode mrcode 307200 Dec 2 09:53 serversaa
-rw-rw-r--. 1 mrcode mrcode 307200 Dec 2 09:53 serversab
-rw-rw-r--. 1 mrcode mrcode 55893 Dec 2 09:53 serversac

# 范例 2:如何将上面三个文件合成一个文件?
[mrcode@study tmp]$ cat serversa* > servicesback
[mrcode@study tmp]$ ll serv*
-rw-rw-r--. 1 mrcode mrcode 307200 Dec 2 09:53 serversaa
-rw-rw-r--. 1 mrcode mrcode 307200 Dec 2 09:53 serversab
-rw-rw-r--. 1 mrcode mrcode 55893 Dec 2 09:53 serversac
-rw-rw-r--. 1 mrcode mrcode 670293 Dec 2 09:54 servicesback

# 范例 3:使用 ls -al / 输出的信息中,每 10 行记录成一个文件
# 这里文件使用了 - ,表示使用标准输入,前面讲过的
[mrcode@study tmp]$ ls -al / | split -l 10 - lsroot
[mrcode@study tmp]$ ll lsroot*
-rw-rw-r--. 1 mrcode mrcode 456 Dec 2 09:57 lsrootaa
-rw-rw-r--. 1 mrcode mrcode 523 Dec 2 09:57 lsrootab
-rw-rw-r--. 1 mrcode mrcode 192 Dec 2 09:57 lsrootac
[mrcode@study tmp]$ wc -l lsroot*
10 lsrootaa
10 lsrootab
4 lsrootac
24 total
# - 一般用在,指令 stdout/stdin 时,但偏偏又没有文件,就用 - 来表示 stdout/stdin

参数代换:xargs

产生某个指令的参数。xargs 可以读入 stdin 的数据,并且以空格符或换行符号作为分辨,将 stdin 的数据分割成为 arguments

1
2
xargs [-0epn] command

  • 0:数值 0,如果输入的 stdin 含有特殊字符,例如 `、\、空格等时,可以将他转义为一个普通字符
  • eEOFend of file)。后面可以接一个字符串,当 xargs 分析到这个字符串时,会停止继续工作;注意:-e'sync' 选项与后面的 eof 字符中间没有空格
  • p:在执行每个指令的 argument 时,都会询问使用者
  • n:后面接次数,每次 command 指令执行时,要使用几个参数

xargs 后面没有接任何指令时,默认是以 echo 来进行输出的

实践练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 范例 1:将 /etc/passwd 内第一栏取出,仅取三行,使用 id 这个指令将每个账户内容秀出来
# id 可以查询用户的 UID/GID 等信息
[mrcode@study tmp]$ id root
uid=0(root) gid=0(root) groups=0(root)

# 通过之前的指令把前三行的第一栏用户名提取出来
[mrcode@study tmp]$ cat /etc/passwd | head -n 3 | cut -d ':' -f 1
root
bin
daemon

# 通过 $(cmd) 可以预先取得参数,但可惜的时候,id 这个指令只能接收一个参数,导致报错了
[mrcode@study tmp]$ id $(cat /etc/passwd | head -n 3 | cut -d ':' -f 1)
id: extra operand ‘bin’
Try 'id --help' for more information.

# 因为 ID 不是管线命令,管线前的输出都没有用,相当于只输出了 id 的内容
[mrcode@study tmp]$ cat /etc/passwd | head -n 3 | cut -d ':' -f 1 | id
uid=1000(mrcode) gid=1000(mrcode) groups=1000(mrcode),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

# xargs 将 3 个账户处理后给 id,一样的会报错
[mrcode@study tmp]$ cat /etc/passwd | head -n 3 | cut -d ':' -f 1 | xargs id
id: extra operand ‘bin’
Try 'id --help' for more information.

# 通过 -n 来指定每次指令命令使用几个参数
# 因为前面输出是三行,在 xargs 中会被当成 3 个参数
[mrcode@study tmp]$ cat /etc/passwd | head -n 3 | cut -d ':' -f 1 | xargs -n 1 id
uid=0(root) gid=0(root) groups=0(root)
uid=1(bin) gid=1(bin) groups=1(bin)
uid=2(daemon) gid=2(daemon) groups=2(daemon)

# 范例 2:同上,但是每次执行 id 时,都要询问使用者是否动作
[mrcode@study tmp]$ cat /etc/passwd | head -n 3 | cut -d ':' -f 1 | xargs -n 1 -p id
id root ?... # 这里没有输入 y 被判定为不执行了
id bin ?...y
uid=1(bin) gid=1(bin) groups=1(bin)
id daemon ?...

# 范例 3:将所有的 /etc/passwd 内的账户都以 id 查询,但差到 sync 就结束指令串
[mrcode@study tmp]$ cat /etc/passwd | cut -d ':' -f 1 | xargs -e'sync' -n 1 id
uid=0(root) gid=0(root) groups=0(root)
uid=1(bin) gid=1(bin) groups=1(bin)
uid=2(daemon) gid=2(daemon) groups=2(daemon)
uid=3(adm) gid=4(adm) groups=4(adm)
uid=4(lp) gid=7(lp) groups=7(lp)


xargs 是一个非常好用的指令,一般使用它的原因是,很多指令其实并不支持管线命令,因此可以通过 xargs 来提供该指令引用 standard input 。如果还不太明白,下面在来看一个例子

1
2
3
4
5
6
7
8
9
10
11
# 范例 4:找出 /usr/sbin 下具有特殊权限的文件名,并使用 ls -l 列出详细属性
# 但是 ls 不是管线命令
[mrcode@study tmp]$ find /usr/bin/ -perm /7000 | ls
# 可以使用 $(cmd) 语法
[mrcode@study tmp]$ ls -l $(find /usr/bin/ -perm /7000)

# 使用 xargs
[mrcode@study tmp]$ find /usr/bin/ -perm /7000 | xargs -n 1 ls -l
-r-xr-sr-x. 1 root tty 15344 Jun 10 2014 /usr/bin/wall
-rwsr-xr-x. 1 root root 32096 Oct 31 2018 /usr/bin/fusermount

关于减号 - 的用途

管线命令在 bash 的连续的处理程序中是相当重要的。另外,在 log file 的分析中也是很重要的一环。

另外,在管线命令中,常常会使用到前一个指令的 stdout 作为这次的 stdin,某些指令需要用到文件名(例如 tar)来进行处理时,该 stdinstdout 可以利用减号 -来替代

1
2
3
4
5
# 将 /home 里的文件打包,但打包的数据不是记录到文件,而是传送到 stdout
# 经过管线后,将 tar -cvf - /home 传送给后面的 tar -xvf - ,
# 这里的 - 就是取用前一个指令的 stdout
mkdir /tmp/homeback
[mrcode@study tmp]$ tar -cvf - /home/ | tar -xvf - -C /tmp/homeback/

git 远程

SVN 使用单一的集中式版本库作为开发人员的通信中心,通过在开发人员的工作副本和中心版本库之间传递变更集来实现协作。这与 Git 的分布式协作模式不同,后者为每个开发人员提供了自己的版本库副本,并拥有自己的本地历史记录和分支结构。用户通常需要共享一系列提交,而不是单个变更集。Git 可让你在不同版本库之间共享整个分支,而不是将工作副本中的变更集提交到中央版本库。

git remote命令是更广泛系统中负责同步变更的一个部分。通过 git remote命令注册的记录会与 git fetchgit pushgit pull 命令结合使用。这些命令都有各自的同步职责,可以在相应的链接中查看。

git remote

使用 git remote 命令可以创建、查看和删除与其他版本库的连接。远程连接更像是书签,而不是其他版本库的直接链接。与其说它们提供了对另一个版本库的实时访问,不如说它们是一个方便的名字,可以用来引用一个不太方便的 URL。

例如,下图显示了从你的版本库到中央版本库和另一个开发者版本库的两个远程连接。与其用完整的 URL 来引用它们,你可以把 origin 和 john 的快捷方式传递给其他 Git 命令。

git remote使用概述

git remote 命令本质上是一个界面,用于管理存储在版本库 ./.git/config 文件中的远程条目列表。以下命令用于查看远程列表的当前状态。

查看 git 远程配置

1
git remote

列出您与其他存储库的远程连接。

1
git remote -v

与上面的命令相同,但包含每个连接的 URL。

创建和修改 git 远程配置

git remote 命令也是修改软件仓库 ./.git/config 文件的一种方便或 “辅助 “方法。下面的命令可以让你管理与其他版本库的连接。以下命令将修改版本库的 ./.git/config 文件。使用文本编辑器直接编辑 ./.git/config 文件也能达到以下命令的效果。

1
git remote add <name> <url>

创建与远程版本库的新连接。添加远程仓库后,你就可以在其他 Git 命令中使用<name>作为<url>的快捷方式。

1
git remote rm <name>

删除与名为 <name> 的远程存储库的连接。

1
git remote rename <old-name> <new-name>

将远程连接从 <old-name> 重命名为 <new-name>

git remote讨论

Git 旨在为每个开发人员提供一个完全独立的开发环境。这意味着信息不会在仓库之间自动来回传递。相反,开发者需要手动将上游提交拉入本地仓库,或手动将本地提交推回中央仓库。git remote 命令实际上只是为这些 “共享 “命令传递 URL 的一种更简便的方式。

当你用 git clone 克隆一个仓库时,它会自动创建一个名为 origin 的远程连接,指向被克隆的仓库。这对于创建中央仓库本地副本的开发者来说非常有用,因为它提供了一种拉取上游变更或发布本地提交的简便方法。这也是大多数基于 Git 的项目将其中央仓库称为 origin 的原因。

Git 支持多种引用远程仓库的方式。访问远程仓库最简单的两种方式是 HTTPSSH 协议。HTTP 是一种允许匿名、只读访问仓库的简单方式。例如

1
http://host/path/to/repo.git

但是,通常无法将提交推送到 HTTP 地址(反正你也不想允许匿名推送)。要进行读写访问,应该使用 SSH

1
ssh://user@host/path/to/repo.git

你需要在主机上有一个有效的 SSH 账户,除此之外,Git 本身就支持通过 SSH 验证访问。现代安全的第三方托管解决方案(如 Bitbucket.com)会为你提供这些 URL。

git remote命令

git remote命令是众多 Git 命令中的一个,这些命令会附加 “子命令”。下面将介绍常用的 git remote子命令。

1
git remote ADD <NAME> <URL>

./.git/config 中为名为<name>的远程仓库URL<url>添加一条记录。

接受 -f 选项,该选项会在远程记录创建后立即 git fetch

接受 --tags 选项,该选项将立即 git 抓取并从远程仓库导入每个标签。

1
git remote RENAME <OLD> <NEW>

更新 ./.git/config,将记录<OLD>重命名为<NEW>。所有远程跟踪分支和远程配置设置都会更新。

1
git remote REMOVE or RM <NAME>

修改 ./.git/config 并删除名为 <NAME> 的远程。所有远程跟踪分支和远程配置设置都会被删除。

1
git remote GET-URL <NAME>

输出远程记录的 URL。

接受 --push 时,查询的是推送 URL 而不是获取 URL。

使用 --all 时,将列出远程记录的所有 URL。

1
git remote SHOW <NAME>

输出远程 <NAME>的高级信息。

1
git remote PRUNE <NAME>

删除远程版本库中不存在的任何本地<NAME> 分支。

接受 --dry-run 选项,该选项会列出要删除的分支,但不会真的删除。

除了origin之外,与队友的仓库建立连接通常也很方便。例如,如果你的同事约翰在 dev.example.com/john.git 上维护了一个可公开访问的仓库,你可以添加如下连接:

1
git remote add john http://dev.example.com/john.git

有了这种访问个人开发者仓库的权限,就可以在中央仓库之外进行协作。这对于开发大型项目的小团队来说非常有用。

查看远程存储库

默认情况下,git remote 命令会列出之前存储的与其他版本库的远程连接。这将产生单行输出,列出远程仓库的 “书签 “名称。

1
2
3
4
$ git remote
origin
upstream
other_users_repo

使用-v选项调用git remote会打印书签仓库名称列表以及相应的仓库 URL。-v选项代表 “verbose“。下面是 git remote 的详细输出示例。

1
2
3
4
5
6
7
git remote -v
origin git@bitbucket.com:origin_user/reponame.git (fetch)
origin git@bitbucket.com:origin_user/reponame.git (push)
upstream https://bitbucket.com/upstream_user/reponame.git (fetch)
upstream https://bitbucket.com/upstream_user/reponame.git (push)
other_users_repo https://bitbucket.com/other_users_repo/reponame (fetch)
other_users_repo https://bitbucket.com/other_users_repo/reponame (push)

添加远程存储库

git remote add 命令将为远程仓库创建一个新的连接记录。添加远程记录后,你就可以把它作为其他 Git 命令的快捷方式。有关可接受的 URL 语法的更多信息,请参阅下面的 “版本库 URL “部分。该命令将在版本库的./.git/config文件中创建一条新记录。配置文件更新示例如下

1
2
3
$ git remote add fake_test https://bitbucket.com/upstream_user/reponame.git; [remote "remote_test"] 
url = https://bitbucket.com/upstream_user/reponame.git
fetch = +refs/heads/*:refs/remotes/remote_test/*

检查远程存储库

show 子命令可附加到 git remote,以提供远程配置的详细输出。该输出将包含与远程相关联的分支列表,以及用于获取和推送的端点。

1
2
3
4
5
6
7
8
9
10
11
git remote show upstream
* remote upstream
Fetch URL: https://bitbucket.com/upstream_user/reponame.git
Push URL: https://bitbucket.com/upstream_user/reponame.git
HEAD branch: main
Remote branches:
main tracked
simd-deprecated tracked
tutorial tracked
Local ref configured for 'git push':
main pushes to main (fast-forwardable)

从 Git 远程fetching和pulling

使用 git remote 命令配置远程记录后,远程名称就可以作为参数传递给其他 Git 命令,以便与远程仓库通信。git fetchgit pull 都可以用来读取远程仓库。这两个命令都有不同的操作,我们将在它们各自的链接中作更深入的解释。

推送到 Git 远程

git push命令用于写入远程仓库。

1
git push <remote-name> <branch-name>

此示例将把<branch-name>的本地状态上传到<remote-name>指定的远程存储库。

重命名和删除Git 远程

1
git remote rename <old-name> <new-name>

git remote rename 命令不言自明。执行该命令后,远程连接将从<old-name>更名为<new-name>。此外,该命令还将修改 ./.git/config 中的内容,重新命名远程记录。

1
git remote rm <name>

git remote rm命令将删除与<name>参数指定的远程仓库的连接。为了演示,让我们 “撤销 “上一个例子中的远程添加。如果我们执行git remote rm remote_test,然后检查./.git/config的内容,就会发现 [remote “remote_test”] 记录已不复存在。


git fetch

git fetch 命令会将提交、文件和引用从远程仓库下载到本地仓库。当你想看看其他人都在做些什么时,你就会使用 fetch 命令。它与svn update类似,能让你看到中心历史的进展,但并不强制你把修改合并到自己的仓库中。Git 将获取的内容与现有的本地内容隔离开来,对本地开发工作完全没有影响。获取的内容必须使用 git checkout 命令明确签出。因此,在将提交内容整合到本地仓库之前,获取内容是一种安全的审查方式。

从远程仓库下载内容时,可以使用git pullgit fetch命令来完成任务。git fetch 是这两个命令的 “安全 “版本。git pull是更激进的选择;它会下载本地活动分支的远程内容,并立即执行 git merge 为新的远程内容创建合并提交。如果您有待处理的修改正在进行中,这将导致冲突,并启动合并冲突解决流程。

git fetch 如何使用远程分支

为了更好地理解 git fetch 的工作原理,我们先来讨论一下 Git 是如何组织和存储提交的。在仓库的 ./.git/objects 目录中,Git 隐藏了所有本地和远程提交。Git 通过使用分支索引将远程和本地分支的提交区分开来。本地分支的 refs 保存在 ./.git/refs/heads/ 目录中。执行 git branch 命令会输出本地分支 refs 的列表。下面是一个 git 分支输出示例,其中包含一些演示分支名称。

1
2
3
4
git branch
main
feature1
debug2

检查 /.git/refs/heads/ 目录的内容也会发现类似的输出。

1
2
3
4
ls ./.git/refs/heads/
main
feature1
debug2

远程分支和本地分支一样,只不过它们映射的是别人版本库中的提交。远程分支的前缀是其所属的远程仓库,这样就不会与本地分支混淆。和本地分支一样,Git 也有远程分支的 refs。远程分支参考位于 ./.git/refs/remotes/ 目录中。下一个示例代码片段展示了在获取一个名为remote-repo的远程仓库后可能看到的分支:

1
2
3
4
5
6
git branch -r
# origin/main
# origin/feature1
# origin/debug2
# remote-repo/main
# remote-repo/other-feature

该输出显示了我们之前检查过的本地分支,但现在显示的是以 origin/ 为前缀的分支。此外,我们现在还能看到以 remote-repo 为前缀的远程分支。你可以像检出本地分支一样检出远程分支,但这将使你处于分离的 HEAD 状态(就像检出旧提交一样)。你可以把它们看作只读分支。要查看远程分支,只需在 git branch 命令中加入 -r 标志即可。

你还可以用常用的 git checkoutgit log 命令来检查远程分支。如果您批准了远程分支包含的改动,就可以用普通的 git 合并将其合并到本地分支中。因此,与 SVN 不同,同步本地仓库和远程仓库实际上只需两步:fetch,然后mergegit pull 命令是实现这一过程的便捷快捷方式。

git fetch 命令和选项

1
git fetch <remote>

从版本库中获取所有分支。这也会从另一个版本库下载所有需要的提交和文件。

1
git fetch <remote> <branch>

与上述命令相同,但只获取指定的分支。

1
git fetch --all

强力移动,可获取所有已注册的远程版本库及其分支:

1
git fetch --dry-run

--dry-run 选项将执行命令的试运行。它会输出获取过程中的操作示例,但不会真的执行这些操作。

git fetch示例

git fetch远程分支

下面的示例将演示如何获取远程分支并将本地工作状态更新为远程内容。在这个示例中,我们假设有一个中心仓库,本地仓库是使用 git clone 命令从该仓库克隆的。我们还假设有一个名为coworkers_repo的远程仓库,其中包含一个 feature_branch,我们将对其进行配置和获取。有了这些假设,让我们继续举例说明。

首先,我们需要使用 git remote 命令配置远程仓库。

1
git remote add coworkers_repo git@bitbucket.org:coworker/coworkers_repo.git

在这里,我们使用 repo URL 创建了一个指向同事 repo 的引用。现在,我们将把该远程名称传递给 git fetch 以下载内容。

1
2
git fetch coworkers_repo coworkers/feature_branch
fetching coworkers/feature_branch

现在我们在本地拥有了coworkers/feature_branch的内容,我们需要将其整合到本地工作副本中。首先,我们使用 git checkout 命令签出新下载的远程分支。

1
2
3
4
5
6
7
8
9
10
11
git checkout coworkers/feature_branch
Note: checking out coworkers/feature_branch'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

git checkout -b <new-branch-name>

该检出操作的输出结果显示,我们处于分离的 HEAD 状态。这是意料之中的,这意味着我们的 HEAD ref 指向的 ref 与本地历史不一致。由于 HEAD 指向的是coworkers/feature_branch ref,我们可以从该 ref 创建一个新的本地分支。分离的 HEAD “输出向我们展示了如何使用 git checkout 命令来做到这一点:

1
git checkout -b local_feature_branch

在这里,我们创建了一个名为 local_feature_branch 的新本地分支。这样,HEAD 的更新就会指向最新的远程内容,我们就可以在此基础上继续开发。

使用 git fetch 同步远程分支

下面的示例介绍了本地版本库与中央版本库主分支同步的典型工作流程。

1
git fetch origin

这将显示已下载的分支:

1
2
3
a1e8fb5..45e66a4 main -> origin/main
a1e8fb5..9e8ab1c develop -> origin/develop
* [new branch] some-feature -> origin/some-feature

这些新远程分支的提交在下图中显示为方形而不是圆形。正如你所看到的,git fetch 可以让你访问另一个仓库的整个分支结构。

要查看上游main分支添加了哪些提交,可以使用 origin/main 作为过滤器运行 git 日志:

1
git log --oneline main..origin/main

要批准更改并将其合并到本地主分支,请使用以下命令:

1
2
git checkout main
git log origin/main

然后我们就可以使用 git merge origin/main

1
git merge origin/main

origin/main 分支和main分支现在指向同一个提交,你就能与上游开发同步了。

Summary

git fetchgit remotegit branchgit checkoutgit reset 配合使用,可将本地仓库更新为远程仓库的状态。git fetch 命令是 git 协作工作流程中的一个重要部分。git fetch 的行为与 git pull 相似,但 git fetch 可被视为更安全的非破坏性版本。


git push

git push命令用于将本地仓库内容上传到远程仓库。push是将提交从本地仓库转移到远程仓库的方式。它与 git fetch 相对应,但 fetch 是将提交导入本地分支,而 push 则是将提交导出到远程分支。使用 git remote 命令可以配置远程分支。推送有可能会覆盖改动,因此在推送时应小心谨慎。下文将讨论这些问题。

git push 用法

1
git push <remote> <branch>

将指定的分支连同所有必要的提交和内部对象推送到远程指定分支。这会在目标仓库创建一个本地分支。为了防止覆盖提交,Git 不会允许你在目标版本库中进行非快速合并的推送。

1
git push <remote> --force

与上述命令相同,但即使结果是非快进合并,也会强制推送。除非你确信知道自己在做什么,否则不要使用 --force 标志。

将所有本地分支推送到指定的远程分支。

1
git push <remote> --tags

推送分支或使用 --all 选项时,标签不会自动推送。--tags标记会将所有本地标记发送到远程版本库。

git push讨论

git push 最常用于发布本地修改并上传到中央仓库。本地版本库修改后,执行推送以与远程团队成员共享修改内容。


上图显示的是当本地主仓库的进度超过中心仓库的主仓库时,您通过运行 git push origin main 发布改动的情况。请注意,git push 与在远程仓库中运行 git merge main 本质上是一样的。

git push 是整个 Git “同步 “过程中使用的众多命令之一。git push 可视为 “上传 “命令,而 git fetchgit pull 可视为 “下载 “命令。通过下载或上传移动变更集后,可在目的地执行 git merge以整合变更。

强制push

为了防止覆盖中央仓库的历史,Git 会在推送请求导致非快速合并时拒绝推送请求。因此,如果远程历史与本地历史有偏差,就需要拉取远程分支并将其合并到本地分支,然后再尝试推送。这类似于 SVN 让你在提交变更集之前通过 svn update 与中心版本库同步。

--force标志会覆盖这一行为,并使远程版本库的分支与本地分支相匹配,同时删除上次推送后可能发生的任何上游变更。唯一需要强制推送的情况是,当你意识到刚刚共享的提交并不完全正确,并用 git commit --amend 或交互式 rebase 进行了修正。不过,在使用 --force 选项之前,您必须绝对确定没有任何队友拉取过这些提交。

例子

默认 git push

下面的示例描述了向中央版本库发布本地贡献的标准方法之一。首先,它通过获取中央版本库的副本并在其上重置你的改动,确保你的本地main分支是最新的。交互式变基也是在分享之前清理提交的好机会。然后,git push 命令会将本地主仓库的所有提交发送到中央仓库。

1
2
3
4
5
git checkout main
git fetch origin main
git rebase -i origin/main
# Squash commits, fix up commit messages etc.
git push origin main

由于我们已经确保了本地main分支是最新的,这应该会导致快进合并,而 git push 也不会抱怨上面讨论过的任何非快进问题。

修改后的强制推送

git commit 命令接受一个 --amend 选项,它将更新之前的提交。修改提交通常是为了更新提交信息或添加新的改动。一旦提交被修改,git 推送就会失败,因为 Git 会把修改后的提交和远程提交视为不同的内容。要推送修改过的提交,必须使用 --force 选项。

1
2
3
4
# make changes to a repo and git add
git commit --amend
# update the existing commit message
git push --force origin main

上面的示例假定是在一个有提交历史的现有版本库中执行的。git commit --amend 用于更新之前的提交。然后使用 --force 选项强制推送修正后的提交。

删除远程分支或标签

有时,出于记账或组织目的,需要清理分支。要完全删除分支,必须在本地和远程删除。

1
2
git branch -D branch_name
git push origin :branch_name

上面的命令将删除名为 branch_name 的远程分支,向 git push 传递一个以冒号为前缀的分支名称将删除该远程分支。


git pull

git pull 命令用于从远程仓库获取和下载内容,并立即更新本地仓库以匹配这些内容。在基于 Git 的协作工作流程中,将远程上游改动合并到本地仓库是一项常见任务。git pull 命令实际上是其他两个命令的组合,即 git fetchgit merge。在操作的第一阶段,git pull 会对 HEAD 指向的本地分支执行 git fetch。下载完成后,git pull 将进入合并工作流程。会创建一个新的合并提交,并更新 HEAD 以指向新的提交。

How it works

git pull 命令首先运行 git fetch,从指定的远程仓库下载内容。然后执行 git merge,将远程内容的引用和头部合并到一个新的本地合并提交中。为了更好地演示拉取和合并过程,让我们看看下面的例子。假设我们有一个包含主分支和远程起源的仓库。

在这种情况下,git pull 会下载本地提交和主提交分歧点上的所有改动。在本例中,分歧点是 E。git pull 会获取分歧的远程提交,即 A-B-C。然后,拉取过程会创建一个新的本地合并提交,其中包含新的远程分歧提交的内容。

在上图中,我们可以看到新的提交 H。该提交是一个新的合并提交,包含了远程 A-B-C 提交的内容,并有一条合并日志信息。这个例子是 git pull 合并策略中的一种。git pull 可以通过 --rebase 选项来使用 rebase 合并策略,而不是merge提交。下一个例子将演示 rebase pull 的工作原理。假设我们在第一个图的起点执行了 git pull --rebase 命令。

从图中我们可以看到,rebase 拉取并没有创建新的 H 提交。相反,rebase 复制了远程提交 A–B–C,并改写了本地提交 E–F–G,使其出现在本地origin/main提交历史记录中。

常用选项

1
git pull <remote>

获取当前分支的指定远程副本,并立即将其合并到本地副本中。这等同于git fetch <remote> 然后 git merge origin/<current-branch>

1
git pull --no-commit <remote>

与默认调用类似,获取远程内容,但不会创建新的合并提交。

1
git pull --rebase <remote>

与上一次的拉取相同,并非使用 git merge 将远程分支与本地分支整合,而是使用 git rebase

1
git pull --verbose

在拉取过程中提供详细输出,显示正在下载的内容和合并细节。

讨论

你可以把 git pull 想象成 Git 版本的 svn update。它是同步本地仓库和上游改动的简便方法。下图解释了拉取过程的每一步。



您一开始以为您的版本库已经同步,但 git fetch 发现自从您上次检查之后,originmain 版本有了新的进展。然后 git merge 立即将远程的 main 整合到本地的 main 中。

git pull是许多负责 “同步 “远程内容的命令之一。git remote命令用于指定同步命令将在哪些远程端点上运行。git push命令用于将内容上传到远程仓库。

git fetch命令可能会与 git pull 命令混淆。它们都用于下载远程内容。git fetch被视为 “安全 “选项,而git pull则被视为不安全选项。或者,git pull会下载远程内容,并立即尝试改变本地状态以匹配该内容。这可能会无意中导致本地仓库处于冲突状态。

Pulling via Rebase

使用 --rebase 选项可以防止不必要的合并提交,从而确保线性历史。相比合并,许多开发者更喜欢重定向,因为这就像在说:”我想把我的改动放在其他人的改动之上”。从这个意义上说,使用带有 --rebase 标志的 git pull 甚至比普通的 git pull 更像 svn update。

事实上,使用 --rebase 拉取是一个很常见的工作流程,以至于有专门的配置选项:

1
git config --global branch.autosetuprebase always

运行该命令后,所有 git pull 命令都将通过 git rebase 而不是 git merge 进行整合。

Git Pull Examples

以下示例演示了如何在常见情况下使用 git pull

1
git pull

执行 git pull 的默认调用相当于 git fetch origin HEADgit merge HEAD,其中 HEAD 是指向当前分支的 ref。

Git pull on remotes

1
2
git checkout new_feature
git pull <remote repo>

本例首先执行签出一个新分支并切换到分支。然后,通过传递执行 git pull。这将隐式地从 <remote repo> 下载完成后,将启动 git merge

Git pull rebase instead of merge

下面的示例演示了如何使用 rebase 与中央版本库的主分支同步:

1
2
git checkout main
git pull --rebase origin

这只是将你的本地修改移到其他人已经贡献的内容之上。


使用分支

Git Branch

本文将深入评述 git 分支命令,并讨论 Git 的整体分支模型。分支是大多数现代版本控制系统都具备的功能。在其他版本控制系统中,分支操作会耗费大量时间和磁盘空间。在 Git 中,分支是日常开发流程的一部分。

Git 分支实际上就是您所做修改的快照指针。当你想添加一个新功能或修复一个错误时,无论大小,你都要创建一个新的分支来封装你的改动。这样一来,不稳定的代码就很难被合并到主代码库中,而且在合并到主分支之前,你还有机会清理未来的历史。

上图展示了一个包含两条独立开发线的版本库,一条用于开发一个小功能,另一条用于开发一个运行时间较长的功能。通过在分支中开发这两条线,不仅可以并行处理这两条线,还能保证主分支不被有问题的代码干扰。

与其他版本控制系统模式相比,Git 分支的实现要轻便得多。Git 并不是把文件从一个目录复制到另一个目录,而是把分支作为提交的引用来存储。从这个意义上说,分支代表了一系列提交的顶端,而不是提交的容器。分支的历史是通过提交关系推断出来的。

在阅读过程中,请记住 Git 分支与 SVN 分支不同。SVN 分支只在偶尔的大规模开发工作中使用,而 Git 分支则是日常工作流程中不可或缺的一部分。下面的内容将详细介绍 Git 分支的内部架构。

How it works

一个分支代表一条独立的开发线路。分支是edit/stage/commit流程的抽象。你可以把它们看作是申请全新工作目录、暂存区域和项目历史的一种方式。新的提交会被记录在当前分支的历史中,从而在项目历史中形成一个分叉。

git 分支命令允许你创建、列出、重命名和删除分支。但它不能在分支间切换,也不能把分叉的历史记录重新组合起来。因此,git branchgit checkoutgit merge 命令紧密结合。

Common options

1
git branch

列出版本库中的所有分支。这与 git branch --list 同义。

1
git branch <branch>

创建一个名为 <branch> 的新分支。这并不签出新分支。

1
git branch -d <branch>

删除指定的分支。这是一个 “安全 “的操作,因为如果分支有未合并的更改,Git 会阻止你删除该分支。

1
git branch -D <branch>

强制删除指定的分支,即使它还有未合并的改动。如果你想永久删除与某一行开发相关的所有提交,可以使用这条命令。

1
git branch -m <branch>

将当前分支重命名为 <branch>

1
git branch -a

列出所有远程分支。

创建分支

要知道,分支只是提交的指针。创建分支时,Git 需要做的只是创建一个新指针,不会以任何其他方式改变版本库。如果你一开始创建的版本库是这样的

然后,使用以下命令创建一个分支:

1
git branch crazy-experiment

版本库历史保持不变。你得到的只是一个指向当前提交的新指针:

注意,这只会创建新分支。要开始向其添加提交,需要用 git checkout 选中它,然后使用标准的 git addgit commit 命令。

创建远程分支

到目前为止,这些例子都演示了本地分支的操作。git branch 命令同样适用于远程分支。要在远程分支上运行,必须先配置远程 repo 并将其添加到本地 repo 配置中。

1
2
3
4
$ git remote add new-remote-repo https://bitbucket.com/user/repo.git
# Add remote repo to local repo config
$ git push <new-remote-repo> crazy-experiment~
# pushes the crazy-experiment branch to new-remote-repo

这条命令会将本地分支 crazy-experiment 的副本推送到远程仓库 <remote>

删除分支

一旦你完成了某个分支的工作,并将其合并到主代码库中,你就可以自由删除该分支,而不会丢失任何历史记录:

1
git branch -d crazy-experiment

但是,如果分支尚未合并,上述命令将输出错误信息:

1
error: The branch 'crazy-experiment' is not fully merged. If you are sure you want to delete it, run 'git branch -D crazy-experiment'.

这样就不会失去对整个开发线的访问权限。如果你真的想删除该分支(例如,它是一个失败的实验),可以使用大写的 -D 标志:

1
git branch -D crazy-experiment

该命令会删除分支,无论其状态如何,且不会发出警告,因此请谨慎使用。

前面的命令将删除分支的本地副本。该分支可能仍然存在于远程版本库中。要删除远程分支,请执行以下命令。

1
git push origin --delete crazy-experiment

1
git push origin :crazy-experiment

这将向远程源代码库推送一个删除信号,触发远程 crazy-experiment 分支的删除。

Summary

本文将讨论 Git 的分支行为和 git 分支命令。git 分支命令的主要功能是创建、列出、重命名和删除分支。为了进一步操作生成的分支,该命令通常与其他命令(如 git checkout)一起使用。有关 git checkout 分支操作的更多信息,如切换分支和合并分支,请参阅 git checkout 页面。

与其他 VCS 相比,Git 的分支操作既便宜又常用。这种灵活性使得 Git 工作流程的定制功能非常强大。


git checkout

本页将介绍 git checkout 命令。它将涵盖使用示例和边缘案例。在 Git 术语中,”checkout”是在目标实体的不同版本之间切换的行为。git checkout 命令针对三个不同的实体:文件、提交和分支。除了 “checkout “的定义,”checking out “也常用来表示执行 git checkout 命令的行为。在 “撤销修改 “主题中,我们看到了如何使用 git 签出查看旧提交。本文的大部分内容将集中在对分支的签出操作上。

签出分支与签出旧提交和文件类似,工作目录会被更新以匹配所选的分支/修订版本;不过,新的改动会被保存到项目历史中,也就是说,这不是一个只读操作。

Checking out branches

使用 git checkout 命令,你可以在 git 分支创建的各个分支之间切换。签出分支会更新工作目录中的文件,使之与该分支中的版本相匹配,并让 Git 记录该分支上的所有新提交。可以把它想象成一种选择开发方向的方式。

为每个新功能建立一个专用分支,是对传统 SVN 工作流程的巨大转变。它让尝试新功能变得非常容易,而不必担心破坏现有功能,也让同时开发许多不相关的功能成为可能。此外,分支还为多个协作工作流程提供了便利。

git checkout 命令有时会与 git clone 混淆。这两个命令的区别在于,clone 的作用是从远程仓库获取代码,而 checkout 的作用则是在本地系统已经存在的代码版本之间切换。

使用已存在的分支

如果您正在使用的软件仓库包含已有的分支,您可以使用 git checkout 在这些分支间切换。要了解有哪些可用分支以及当前分支的名称,请执行 git branch

1
2
3
4
5
$> git branch 
main
another_branch
feature_inprogress_branch
$> git checkout feature_inprogress_branch

上面的例子演示了如何通过执行 git branch 命令查看可用分支列表,并切换到指定分支,在本例中就是 feature_inprogress_branch

新分支

git checkoutgit branch是并行的。git branch命令可以用来创建一个新分支。当你想创建一个新功能时,可以用 git branch new_branch在主分支之外创建一个新分支。创建完成后,就可以使用 git checkout new_branch 切换到该分支。此外,git checkout 命令还接受一个 -b 参数,作为一种方便的方法,它会创建新的分支并立即切换到该分支。通过使用 git checkout,您可以在一个仓库中的多个特性之间进行切换。

1
git checkout -b <new-branch>

上面的例子同时创建和签出了<new-branch>-b选项是一个方便的标志,它告诉 Git 在运行git checkout <new-branch> 之前先运行git branch

1
git checkout -b <new-branch> <existing-branch>

默认情况下,git checkout -b 会以当前 HEAD 为基础创建新分支。git checkout 可以传递一个可选的附加分支参数。在上面的例子中,传递了<existing-branch>,新分支就会基于现有分支,而不是当前的 HEAD

切换分支

切换分支是一个简单明了的操作。执行以下命令将把 HEAD 指向<branchname>的顶端。

1
git checkout <branchname>

Git 会在 reflog 中记录检出操作的历史。你可以执行git reflog查看历史记录。

git checkout远程分支

与团队协作时,通常会使用远程资源库。这些版本库可能是托管和共享的,也可能是其他同事的本地副本。每个远程版本库都包含自己的分支集。要检出一个远程分支,必须先获取该分支的内容。

1
git fetch --all

在现代版本的 Git 中,你可以像签出本地分支一样签出远程分支。

1
git checkout <remotebranch>

旧版本的 Git 需要在远程分支的基础上创建一个新分支。

1
git checkout -b <remotebranch> origin/<remotebranch>

此外,你还可以签出一个新的本地分支,并将其重置为远程分支的最后一次提交。

1
2
git checkout -b <branchname>
git reset --hard origin/<branchname>

分离的 HEAD

既然我们已经了解了 git checkout 在分支上的三种主要用途,那么讨论一下 “分离的 HEAD “状态就很重要了。请记住,HEAD 是 Git 表示当前快照的方式。在内部,git checkout 命令只是更新 HEAD,使其指向指定的分支或提交。当它指向一个分支时,Git 不会抱怨,但当你签出一个提交时,它就会切换到 “detached HEAD”状态。

这是一个警告,告诉你你正在做的一切都与项目的其他开发 “分离 “了。如果你在分离 HEAD 状态下开始开发某个功能,就不会有任何分支允许你返回到该功能。当你不可避免地签出另一个分支(例如,将你的功能合并到其中)时,你将无法引用你的功能:


重点是,你的开发应该始终在分支上进行,而不是在分离的 HEAD 上。这样才能确保你的新提交始终有一个引用。不过,如果你只是在查看旧提交,那么是否处于分离的 HEAD 状态并不重要。

Summary

本页主要介绍 git checkout 命令在更改分支时的用法。总的来说,在分支上使用 git checkout 会改变 HEAD ref 的目标。它可以用来创建分支、切换分支和签出远程分支。git checkout 命令是标准 Git 操作的基本工具。它是 git merge 的对应命令。git checkoutgit merge 命令是实现 Git 工作流程的关键工具。


git merge

合并是 Git 将已分叉的历史重新组合起来的一种方式。通过 git merge 命令,你可以将 git branch创建的独立开发分支整合到一个分支中。

请注意,下面介绍的所有命令都会合并到当前分支。当前分支将被更新以反映合并,但目标分支将完全不受影响。这再次说明,git merge 经常与 git checkoutgit branch -d 结合使用,前者用于选择当前分支,后者用于删除过时的目标分支。

How it works

git merge会将多个提交序列合并成一个统一的历史。在最常见的使用案例中,git merge 被用来合并两个分支。本文档的以下示例将重点介绍这种分支合并模式。在这些情况下,git 合并会使用两个提交指针(通常是分支提示),并在它们之间找到一个共同的基本提交。一旦 Git 找到了共同的基本提交,它就会创建一个新的 “合并提交”,把每个排队合并的提交序列的改动合并在一起。

假设我们有一个基于主分支的新特性分支。现在我们想把这个特性分支合并到主干分支中。

调用该命令会将指定的分支特性合并到当前分支,我们假设是主分支。Git 会自动决定合并算法(下文将讨论)。

与其他提交相比,合并提交有两个父提交。在创建合并提交时,Git 会尝试自动神奇地合并两个不同的历史。如果 Git 遇到两个历史中都有改动的数据,它就无法自动合并它们。这种情况属于版本控制冲突,Git 需要用户干预才能继续。

准备合并

在执行合并之前,有几个准备步骤要做,以确保合并顺利进行。

确认接收分支

执行 git status 以确保 HEAD 指向正确的合并接收分支。如果需要,执行 git checkout 切换到接收分支。在我们的例子中,我们将执行 git checkout main

获取最新的远程提交

确保接收分支和合并分支都有最新的远程变更。执行 git fetch 提取最新的远程提交。获取完成后,执行 git pull 确保主分支有最新更新。

合并

一旦完成了之前讨论过的 “准备合并 “步骤,就可以执行 git merge 开始合并了。

快进合并

当从当前分支顶端到目标分支之间存在一条线性路径时,就会发生快进合并。Git 不需要 “实际 “合并分支,只需移动(即 “快进”)当前分支顶端到目标分支顶端,就能整合历史。这就有效地合并了历史,因为目标分支的所有提交现在都可以通过当前分支获得。例如,将 some-feature 快速合并到 main 分支的过程如下:


不过,如果分支已经分叉,则无法进行快进合并。如果没有通往目标分支的线性路径,Git 只能通过三向合并来合并它们。三向合并使用专门的提交来连接两个历史分支。Git 使用三个提交来生成合并提交:两个分支的顶端和它们的共同祖先。


虽然您可以使用这两种合并策略中的任何一种,但许多开发人员喜欢使用快进合并(通过rebase来实现)来处理小功能或错误修复,而保留三向合并来整合运行时间较长的功能。在后一种情况下,合并提交的结果就是两个分支的象征性连接。

我们的第一个例子演示了快进合并。下面的代码创建了一个新分支,向其添加了两次提交,然后通过快进合并将其整合到主线中。

1
2
3
4
5
6
7
8
9
10
11
12
# Start a new feature
git checkout -b new-feature main
# Edit some files
git add <file>
git commit -m "Start a feature"
# Edit some files
git add <file>
git commit -m "Finish a feature"
# Merge in the new-feature branch
git checkout main
git merge new-feature
git branch -d new-feature

这是短生命周期特性分支的常见工作流程,这些分支更多是作为独立开发使用,而不是作为长期运行特性的组织工具。

另外要注意的是,由于 new-feature 现在可以从主分支访问,因此 Git 不会抱怨 git branch -d 的问题。

如果在快进合并过程中需要合并提交以保存记录,可以使用 --no-ff 选项执行 git merge

1
git merge --no-ff <branch>

该命令将指定的分支合并到当前分支,但始终会生成合并提交(即使是快进合并)。这对记录版本库中发生的所有合并很有用。

3 向合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Start a new feature
git checkout -b new-feature main
# Edit some files
git add <file>
git commit -m "Start a feature"
# Edit some files
git add <file>
git commit -m "Finish a feature"
# Develop the main branch
git checkout main
# Edit some files
git add <file>
git commit -m "Make some super-stable changes to main"
# Merge in the new-feature branch
git merge new-feature
git branch -d new-feature

请注意,Git 不可能执行快进合并,因为没有办法在不回溯的情况下将 main 上移到 new-feature。

对于大多数工作流程来说,new-feature 会是一个更大的特性分支,需要很长时间才能开发完成,这也是新提交会同时出现在 main 上的原因。如果您的特性分支和上面例子中的分支一样小,您最好将其重定向到主分支,然后进行快进合并。这样可以防止多余的合并提交扰乱项目历史。

解决冲突

如果要合并的两个分支都修改了同一个文件的相同部分,Git 就无法判断该使用哪个版本。这种情况下,Git 会在合并提交前停止,让你手动解决冲突。

Git 合并流程的最大优点在于,它使用我们熟悉的编辑/阶段/提交工作流程来解决合并冲突。遇到合并冲突时,运行 git status 命令就会显示哪些文件需要解决。例如,如果两个分支都修改了 hello.py 的同一个部分,就会出现类似下面的情况:

1
2
3
4
On branch main
Unmerged paths:
(use "git add/rm ..." as appropriate to mark resolution)
both modified: hello.py

冲突如何呈现

当 Git 在合并过程中遇到冲突时,它会编辑受影响文件的内容,并在冲突内容的两侧标上可视化标记。这些视觉标记是 <<<<<<<=======>>>>>>>。在合并过程中,搜索项目中的这些标记,有助于找到需要解决冲突的地方。

1
2
3
4
5
6
here is some content not affected by the conflict
<<<<<<< main
this is conflicted text from main
=======
this is conflicted text from feature branch
>>>>>>> feature branch;

一般来说,======= 标记之前的内容是接收分支,之后的部分是合并分支。

一般来说,======= 标记之前的内容是接收分支,之后的部分是合并分支。

一旦确定了有冲突的部分,就可以按照自己的喜好进行修改。准备完成合并时,只需在有冲突的文件上运行 git add,告诉 Git 它们已经解决了。然后,再运行正常的 git commit,生成合并提交。这与提交普通快照的过程完全相同,因此普通开发者也能轻松管理自己的合并。

请注意,合并冲突只会发生在三方合并的情况下。快进合并中不可能出现冲突的变更。

Summary

本文档概述了git merge命令。在使用 Git 时,合并是一个必不可少的过程。我们讨论了合并背后的内部机制,以及快速合并和三方真正合并之间的区别。一些主要收获如下

  1. Git 合并将提交序列合并为一个统一的提交历史。

  2. Git 有两种主要的合并方式: 快进和3向合并方式

  3. Git 可以自动合并提交,除非两个提交序列中的改动有冲突。


Git merge conflicts

版本控制系统主要是管理多个分布式作者(通常是开发人员)之间的贡献。有时,多个开发人员可能会尝试编辑相同的内容。如果开发人员 A 试图编辑开发人员 B 正在编辑的代码,就可能发生冲突。为了减少冲突的发生,开发者会在独立的分支中工作。git merge命令的主要职责就是合并不同的分支,并解决任何冲突的编辑。

理解合并冲突

一般来说,当两个人在一个文件中修改了相同的行,或者一个开发者删除了一个文件,而另一个开发者正在修改该文件时,就会产生冲突。在这种情况下,Git 无法自动判断哪个是正确的。冲突只会影响到进行合并的开发者,团队的其他成员并不知晓。Git 会将文件标记为冲突文件,并停止合并进程。解决冲突是开发人员的责任。

合并冲突的类型

合并可能在两个不同的时间点进入冲突状态。开始合并时和合并过程中。下面将讨论如何解决这些冲突情况。

Git 启动合并失败

当 Git 发现当前项目的工作目录或暂存区域有改动时,合并就会失败。Git 无法启动合并是因为这些待处理的改动可能会被正在合并的提交所覆盖。发生这种情况时,不是因为与其他开发者的提交冲突,而是与本地的待处理变更冲突。本地状态需要使用 git stashgit checkoutgit commitgit reset 来稳定。启动时合并失败会输出以下错误信息:

1
error: Entry '<fileName>' not uptodate. Cannot merge. (Changes in working directory)

Git 在合并过程中失败

合并失败表示当前本地分支与正在合并的分支之间存在冲突。这表示与另一位开发者的代码有冲突。Git 会尽最大努力合并文件,但会把冲突文件中的问题留给你手动解决。中途合并失败会输出以下错误信息:

1
error: Entry '<fileName>' would be overwritten by merge. Cannot merge. (Changes in staging area)

创建合并冲突

为了真正熟悉合并冲突,下一节将模拟一个冲突,以便稍后检查和解决。本示例将使用类似 Unix 的 Git 命令行界面来执行模拟示例。

1
2
3
4
5
6
7
8
9
$ mkdir git-merge-test
$ cd git-merge-test
$ git init .
$ echo "this is some content to mess with" > merge.txt
$ git add merge.txt
$ git commit -am"we are commiting the inital content"
[main (root-commit) d48e74c] we are commiting the inital content
1 file changed, 1 insertion(+)
create mode 100644 merge.txt

此代码示例将执行一系列命令,完成以下工作。

  • 新建一个名为 git-merge-test 的目录,并将其初始化为一个新的 Git 仓库。
  • 新建一个文本文件 merge.txt,并在其中添加一些内容。
  • 将 merge.txt 添加到 repo 并提交。

现在我们有了一个新的软件仓库,其中有一个主分支和一个包含内容的 merge.txt 文件。接下来,我们将创建一个新分支,作为冲突合并使用。

1
2
3
4
5
$ git checkout -b new_branch_to_merge_later
$ echo "totally different content to merge later" > merge.txt
$ git commit -am"edited the content of merge.txt to cause a conflict"
[new_branch_to_merge_later 6282319] edited the content of merge.txt to cause a conflict
1 file changed, 1 insertion(+), 1 deletion(-)

接下来的命令序列可实现以下功能:

  • 创建并签出名为 new_branch_ttoo_merge_later 的新分支
  • 覆盖 merge.txt 中的内容
  • 提交新内容

通过这个新分支:new_branch_to_merge_later,我们创建了一个覆盖 merge.txt 内容的提交。

1
2
3
4
5
6
git checkout main
Switched to branch 'main'
echo "content to append" >> merge.txt
git commit -am"appended content to merge.txt"
[main 24fbe3c] appended content to merge.tx
1 file changed, 1 insertion(+)

这一系列命令会检查主分支,将内容添加到 merge.txt 中,然后提交。这样,我们的示例仓库就有了 2 条新提交。一个在主分支,一个在 new_branch_too_merge_later 分支。现在让我们用 git 合并 new_branch_to_merge_later,看看会发生什么!

1
2
3
4
$ git merge new_branch_to_merge_later
Auto-merging merge.txt
CONFLICT (content): Merge conflict in merge.txt
Automatic merge failed; fix conflicts and then commit the result.

BOOM 💥. 冲突出现了。感谢 Git 让我们了解到这一点!

如何确定合并冲突

正如我们在下面的例子中所体验到的,Git 会产生一些描述性输出,让我们知道发生了冲突。我们可以通过运行 git status 命令进一步了解情况

1
2
3
4
5
6
7
8
9
10
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)

Unmerged paths:
(use "git add <file>..." to mark resolution)

both modified: merge.txt

git status 的输出显示,由于冲突,存在未合并的路径。现在,merge.text 文件处于修改状态。让我们检查一下文件,看看修改了什么。

1
2
3
4
5
6
7
$ cat merge.txt
<<<<<<< HEAD
this is some content to mess with
content to append
=======
totally different content to merge later
>>>>>>> new_branch_to_merge_later

在这里,我们使用 cat 命令输出了 merge.txt 文件的内容。我们可以看到一些奇怪的新内容

  • <<<<<<< HEAD
  • =======
  • >>>>>>> new_branch_to_merge_later

将这些新线视为 “冲突分界线”。======= 行是冲突的 “中心”。中心和 <<<<<<< HEAD 行之间的所有内容都是 HEAD ref 指向的当前分支 main 中存在的内容。或者,中心线和 >>>>>>> new_branch_too_merge_later 之间的所有内容都是合并分支中的内容。

如何使用命令行解决合并冲突

解决合并冲突的最直接方法是编辑冲突文件。用你喜欢的编辑器打开 merge.txt 文件。在我们的例子中,让我们简单地删除所有冲突分隔符。修改后的 merge.txt 内容应如下所示:

1
2
3
this is some content to mess with
content to append
totally different content to merge later

文件编辑完成后,使用 git add merge.txt 将新合并的内容放入阶段。要最终完成合并,执行以下命令创建一个新提交

1
git commit -m "merged and resolved the conflict in merge.txt"

Git 会发现冲突已被解决,并创建一个新的合并提交来最终完成合并。

有助于解决合并冲突的 Git 命令

1
git status

在使用 Git 时,status 命令经常被使用,在合并过程中,它可以帮助识别冲突文件。

1
git log --merge

git log命令中传递 --merge 参数,会生成一个包含合并分支间冲突提交列表的日志。

1
git diff

diff可帮助查找版本库/文件状态之间的差异。这对预测和防止合并冲突非常有用。

当 git 启动合并失败时的工具

1
git checkout

checkout 可用于撤销对文件的更改,或更改分支

1
git reset --mixed

reset可用于撤销对工作目录和暂存区域的更改。

合并过程中出现 git 冲突时的工具

1
git merge --abort

使用 --abort 选项执行 git merge 会退出合并过程,并将分支恢复到合并开始前的状态。

1
git reset

git reset可在合并冲突期间使用,将冲突文件重置为已知的良好状态

总结

合并冲突是一种令人生畏的经历。幸运的是,Git 提供了功能强大的工具来帮助浏览和解决冲突。Git 具备自动合并功能,可以自行处理大部分合并工作。当两个不同的分支对文件的同一行进行了编辑,或者一个分支删除了文件,而另一个分支却编辑了文件时,就会产生冲突。在团队环境中工作时,冲突最有可能发生。

有很多工具可以帮助解决合并冲突。我们在这里讨论的 Git 命令行工具就有很多。有关这些工具的详细信息,请访问 git log、git reset、git status、git checkout 和 git reset 的独立页面。除了 Git 之外,许多第三方工具也提供简化的合并冲突支持功能。


merge strategy

当一项工作完成、经过测试并准备并回开发主线时,你的团队需要做出一些策略选择。你有哪些合并策略选择?在本文中,我们将探讨各种可能性,然后提供一些有关 Atlassian 运行方式的说明。希望最后你能掌握一些工具来决定什么最适合你的团队。

Git merge strategies

合并两个分支时。Git 会获取两个(或更多)提交指针,并试图在它们之间找到一个共同的基础提交。Git 有几种不同的方法来寻找基础提交,这些方法被称为 “合并策略”。一旦 Git 找到了共同的基础提交,它就会创建一个新的 “合并提交”,把指定合并提交的改动合并在一起。从技术上讲,合并提交是一个普通提交,只是恰好有两个父提交。

除非明确指定,否则git merge会自动选择合并策略。git mergegit pull 命令可以通过一个-s(策略)选项。-s选项可以附加所需的合并策略名称。如果没有明确指定,Git 会根据提供的分支选择最合适的合并策略。以下是可用的

Recursive

1
git merge -s recursive branch1 branch2

这是在两个分支上进行的操作。在拉取或合并一个分支时,递归是默认的合并策略。此外,它还能检测并处理涉及重命名的合并,但目前还不能使用检测到的副本。在拉取或合并一个分支时,这是默认的合并策略。

Resolve

1
git merge -s resolve branch1 branch2

这只能使用三向合并算法解决两个head的问题。它能仔细地检测出交叉合并的模糊之处,一般被认为是安全和快速的。

Octopus

1
git merge -s octopus branch1 branch2 branch3 branchN

两个以上分支的默认合并策略。如果合并中出现需要手动解决的冲突,octopus将拒绝合并尝试。它主要用于将类似功能的分支头捆绑在一起。

Ours

1
git merge -s ours branch1 branch2 branchN

我们的策略在多个 N 个分支上运行。输出的合并结果总是当前分支 HEAD 的结果。我们的 “Ours“一词意味着优先选择当前分支而有效忽略所有其他分支的所有更改。它的目的是用于合并相似特性分支的历史记录。

Subtree

1
git merge -s subtree branchA branchB

这是递归策略的扩展。在合并 A 和 B 时,如果 B 是 A 的子树,则首先更新 B 以反映 A 的树结构,同时也更新 A 和 B 共享的共同祖先树。

递归的 git merge策略选项

上文介绍的 “递归 “策略有自己的附加操作选项子集。

1
ours

不要与 “Ours“合并策略混淆。该选项通过偏向’our‘版本来自动解决冲突。如果不冲突,’theirs‘一方的改动会自动合并。

1
theirs

与 “Ours“战略相反,”theirs“方案在解决冲突时更倾向于外部合并分支。

1
patience

此选项花费额外的时间来避免不重要的匹配行的错误合并。当要合并的分支极度分歧时,最好使用此选项。

1
diff-algorithim
1
2
3
4
5
6
ignore-*

ignore-space-change
ignore-all-space
ignore-space-at-eol
ignore-cr-at-eol

一组针对空白字符的选项。任何与所传选项子集匹配的行都将被忽略。

1
renormalize

该选项在解决三向合并的同时,对所有 git 树进行签出和签入。该选项适用于合并不同签入/签出状态的分支。

1
no-normalize

禁用renormalize一化选项。这将覆盖merge.renormalize配置变量。

1
no-renames

该选项将在合并过程中忽略重命名的文件。

1
find-renames=n

这是默认行为。递归合并将遵循文件重命名。该n参数可用于传递重命名相似性的阈值。默认n值为100%.

1
subtree

该选项借鉴了 subtree 策略。该策略对两棵树进行操作,并修改如何使它们在共享祖先上匹配,而该选项则对树的路径元数据进行操作,使它们匹配。

总结

强烈倾向于使用显式合并。原因很简单:显式合并可为被合并的特性提供很好的可追溯性和上下文。在共享特性分支供审核之前进行本地历史清理重置是绝对值得鼓励的,但这根本不会改变政策。它只是对政策的补充。

比较 Git 工作流程

Git 是当今最常用的版本控制系统。Git 工作流程是如何使用 Git 以一致、高效的方式完成工作的秘诀或建议。Git 工作流程鼓励开发人员和 DevOps 团队有效、一致地使用 Git。Git 为用户管理变更提供了很大的灵活性。鉴于 Git 注重灵活性,因此在如何与 Git 互动方面并没有标准化的流程。在与团队合作开发 Git 管理的项目时,确保团队就如何应用变更流程达成一致非常重要。为确保团队意见一致,应制定或选择一个约定俗成的 Git 工作流程。有几种公开的 Git 工作流程可能很适合您的团队。在此,我们将讨论其中一些 Git 工作流程选项。

在工作场所实施 Git 时,可能会因为工作流程繁多而不知从何下手。本页通过调查软件团队最常见的 Git 工作流程,为您提供一个起点。

在阅读过程中,请记住这些工作流程只是指南,而非具体规则。我们希望向你展示各种可能,因此你可以根据自己的需求,混合搭配不同工作流程的各个环节。

什么是成功的 Git 工作流?

在为团队评估工作流程时,最重要的是考虑团队文化。您希望工作流程能提高团队效率,而不是成为限制工作效率的负担。评估 Git 工作流程时需要考虑以下几点

  • 该工作流程是否可根据团队规模进行扩展?
  • 该工作流程是否容易撤销错误?
  • 该工作流程是否会给团队带来新的不必要的认知开销?

集中式工作流程

对于从 SVN 过渡而来的团队来说,集中式工作流是一个很好的 Git 工作流。与 Subversion 类似,集中式工作流使用一个中央仓库作为项目所有变更的单一入口。默认的开发分支不是trunk,而是main,所有变更都提交到该分支。除main外,该工作流程不需要其他分支。

过渡到分布式版本控制系统看似是一项艰巨的任务,但你不必改变现有的工作流程,就能充分利用 Git 的优势。你的团队可以像使用 Subversion 一样开发项目。

不过,与 SVN 相比,使用 Git 支持开发工作流程有一些优势。首先,它为每个开发人员提供了整个项目的本地副本。这种隔离的环境让每个开发人员都能独立工作,不受项目中所有其他改动的影响–他们可以将提交添加到本地存储库,并完全忘记上游开发,直到对他们来说方便为止。

其次,它还能让你使用 Git 强大的分支和合并模型。与 SVN 不同,Git 分支的设计是一种安全可靠的机制,用于整合代码并在不同版本库之间共享变更。集中式工作流与其他工作流类似,都是利用远程服务器端托管的仓库,由开发人员推拉形成。与其他工作流相比,集中式工作流没有定义拉取请求或分叉模式。集中式工作流通常更适合从 SVN 迁移到 Git 的团队和规模较小的团队。

How it works

开发人员从克隆中央版本库开始。在他们自己的项目本地副本中,他们编辑文件并提交更改,就像使用 SVN 一样;不过,这些新提交的内容都存储在本地,与中心版本库完全隔离。这样,开发人员就可以推迟向上游同步,直到达到方便的中断点。

要向正式项目发布变更,开发人员需要将本地主分支 “推送 “到中央版本库。这就相当于 svn commit,只不过它添加的是中心主分支中还没有的所有本地提交。

初始化中央存储库

首先,需要有人在服务器上创建中央资源库。如果是新项目,可以初始化一个空仓库。否则,就需要导入现有的 Git 或 SVN 仓库。

中心仓库应始终是裸仓库(不应有工作目录),创建方法如下:

1
ssh user@host git init --bare /path/to/repo.git

用户名请务必使用有效的 SSH 用户名,主机请使用服务器的域名或 IP 地址,/path/to/repo.git 请使用存储 repo 的位置。请注意,.git 扩展名通常会附加到版本库名称上,以表明这是一个裸版本库。

托管中央存储库

中心仓库通常是通过第三方 Git 托管服务(如 Bitbucket Cloud)创建的。托管服务会为你处理上述初始化裸仓库的过程。托管服务会为中央仓库提供一个地址,以便从本地仓库访问。

克隆中央存储库

接下来,每个开发人员都要创建整个项目的本地副本。这是通过 git clone 命令完成的:

1
git clone ssh://user@host/path/to/repo.git

当你克隆一个仓库时,Git 会自动添加一个名为 origin 的快捷方式,该快捷方式会指向 “父 “仓库,前提是你还想继续与它交互。

更改并提交

仓库克隆到本地后,开发人员就可以使用标准的 Git 提交流程进行修改:编辑、暂存和提交。如果你对暂存区还不熟悉,它是一种准备提交的方法,而不必包含工作目录中的所有改动。这样,即使本地改动很多,也能创建高度集中的提交。

1
2
3
git status # View the state of the repo
git add <some-file> # Stage a file
git commit # Commit a file</some-file>

请记住,由于这些命令创建的是本地提交,所以 John 可以随意重复这个过程,而不必担心中央版本库中的情况。这对于需要分解成更简单、更原子化的大功能来说非常有用。

将新提交推送到中央版本库

一旦本地版本库提交了新的变更。这些变更需要推送给项目中的其他开发人员共享。

1
git push origin main

该命令将把新提交的变更推送到中央版本库。在向中央仓库推送变更时,可能会出现之前推送的其他开发者的更新包含的代码与计划推送的更新冲突的情况。Git 会输出一条信息来说明这种冲突。在这种情况下,首先需要执行 git pull。这种冲突情况将在下一节中详述。

管理冲突

中央仓库代表着正式项目,因此它的提交历史应该被视为神圣不可侵犯的。如果开发者的本地提交与中央仓库有出入,Git 会拒绝推送他们的改动,因为这会覆盖官方提交。

开发人员在发布自己的功能之前,需要获取更新后的中央提交,并将自己的更改重新加载到中央提交之上。这就好比在说:”我想把我的改动添加到其他人已经完成的改动中”。结果就是一个完美的线性历史,就像传统的 SVN 工作流程一样。

如果本地改动与上游提交直接冲突,Git 会暂停重定向过程,给你一个手动解决冲突的机会。Git 的好处在于,它使用相同的 git statusgit add 命令来生成提交和解决合并冲突。这让新开发者可以轻松管理自己的合并。此外,如果他们遇到麻烦,Git 还能让他们很容易地中止整个重置过程,然后再试一次(或者去寻求帮助)。


Example

让我们以一个典型的小型团队如何使用此工作流程进行协作为例。我们将看到两个开发人员,John和Mary,如何在不同的功能上工作,并通过一个集中的资源库共享他们的贡献。

John致力于他的特色功能

在他的本地存储库中,John 可以使用标准 Git 提交流程来开发功能:编辑、暂存和提交。

请记住,由于这些命令创建本地提交,John 可以根据需要多次重复此过程,而不必担心中央存储库中发生的情况。

Mary致力于她的特色功能

与此同时,玛丽正在自己的本地版本库中使用相同的编辑/阶段/提交流程开发自己的功能。和约翰一样,她并不关心中央版本库中发生了什么,她也真的不关心约翰在本地版本库中做了什么,因为所有的本地版本库都是私有的。

John推送他的工作内容

一旦约翰完成了他的功能,他就应该把本地提交发布到中央仓库,这样其他团队成员就可以访问它了。他可以使用 git push 命令这样做:

1
git push origin main

记住,origin 是约翰克隆中央仓库时 Git 创建的与中央仓库的远程连接。main 参数告诉 Git 尽量让 origin 的主分支看起来像他本地的主分支。由于中心仓库在约翰克隆后没有更新过,这不会导致任何冲突,推送也会如期进行。

Mary推送他的工作内容

让我们看看如果 Mary 在 John 成功将其更改发布到中心版本库后尝试推送她的功能会发生什么。她可以使用完全相同的推送命令:

1
git push origin main

但是,由于她的本地历史已经偏离了中央仓库,Git 会拒绝该请求,并给出一条相当冗长的错误信息:

1
2
3
4
5
error: failed to push some refs to '/path/to/repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

这就防止了 Mary 覆盖官方提交。她需要将 John 的更新拉入自己的版本库,与本地变更整合,然后再试一次。

Mary在John提交的基础上进行Rebase

Mary 可以使用 git pull 将上游的改动整合到自己的版本库中。这个命令有点像 svn update–它会把整个上游提交历史拉入 Mary 的本地仓库,并尝试将其与本地提交整合在一起:

1
git pull --rebase origin main

如下所示,--rebase 选项会告诉 Git 将 Mary 的所有提交与中心仓库的改动同步后移到主分支的顶端:

如果忘记了这个选项,pull仍然可以正常工作,但每次有人需要与中央版本库同步时,就会出现一个多余的“merge commit”。在这种工作流程中,最好是rebase而不是生成merge commit。

Mary解决了合并冲突

Rebasing的工作原理是将每个本地提交逐一转移到更新后的主分支。这意味着你可以逐个提交地捕捉合并冲突,而不是在一次大规模的合并提交中解决所有冲突。这样就能让提交尽可能集中,使项目历史更加清晰。反过来,这也更容易找出引入错误的地方,并在必要时回滚更改,将对项目的影响降到最低。

重置的工作原理是将每个本地提交逐一转移到更新后的主分支。这意味着你可以逐个提交地捕捉合并冲突,而不是在一次大规模的合并提交中解决所有冲突。这样就能让提交尽可能集中,使项目历史更加清晰。反过来,这也更容易找出引入错误的地方,并在必要时回滚更改,将对项目的影响降到最低。

如果 Mary 和 John 正在开发不相关的功能,Rebasing过程不太可能产生冲突。但如果发生冲突,Git 会在当前提交处暂停重置,并输出以下信息和相关说明:

1
CONFLICT (content): Merge conflict in <some-file>

Git 的好处在于,任何人都可以解决自己的合并冲突。在我们的例子中,Mary 只需运行 git status 就能看到问题所在。有冲突的文件会出现在未合并路径部分:

1
2
3
4
5
# Unmerged paths:
# (use "git reset HEAD <some-file>..." to unstage)
# (use "git add/rm <some-file>..." as appropriate to mark resolution)
#
# both modified: <some-file>

然后,她会按照自己的喜好编辑文件。一旦她对结果感到满意,就可以按照通常的方式对文件进行暂存,然后让 git rebase 完成剩下的工作:

1
2
git add <some-file>
git rebase --continue

仅此而已。Git 会继续下一次提交,并对产生冲突的其他提交重复上述过程。

如果到了这一步,你发现自己完全不知道发生了什么,也不用惊慌。只要执行以下命令,就能回到起点:

1
git rebase --abort

Mary成功推送他的特色功能

完成与中央版本库的同步后,玛丽就能成功发布她的更改:

1
git push origin main

何去何从?

如你所见,只需使用少量 Git 命令,就能复制传统的 Subversion 开发环境。这对于从 SVN 过渡到 Git 的团队来说是个不错的选择,但它并不能充分利用 Git 的分布式特性。

集中式工作流程非常适合小型团队。上文详述的冲突解决流程会随着团队规模的扩大而形成瓶颈。如果你的团队对集中式工作流程感到满意,但又想简化协作流程,那就绝对值得探索特性分支工作流程的好处。通过为每个功能指定一个独立的分支,可以在将新添加功能整合到正式项目之前发起深入讨论。

其他常见工作流程

集中式工作流本质上是其他 Git 工作流的构件。大多数流行的 Git 工作流程都会有某种集中式仓库,供开发人员推拉使用。下面我们将简要讨论一些其他流行的 Git 工作流程。这些扩展工作流在管理功能开发、热修复和最终发布的分支方面提供了更专业的模式。

功能分支

功能分支是集中式工作流程的逻辑延伸。特性分支工作流背后的核心理念是,所有特性开发都应在专用分支而非主分支中进行。这种封装方式可以让多个开发人员在不影响主代码库的情况下,轻松完成某个特定功能的开发。这也意味着主分支永远不会包含损坏的代码,这对于持续集成环境来说是一个巨大的优势。

Gitflow 工作流程

Gitflow 工作流最早发表于 nvie 的 Vincent Driessen 于 2010 年发表的一篇备受推崇的博文中。Gitflow 工作流定义了一种严格的分支模型,以项目发布为中心进行设计。除了特性分支工作流所需的内容外,该工作流没有添加任何新的概念或命令。相反,它为不同的分支分配了非常具体的角色,并定义了它们之间的交互方式和时间。

分叉工作流程

分叉工作流与本教程中讨论的其他工作流有本质区别。它不使用单一的服务器端仓库作为 “中心 “代码库,而是为每个开发者提供一个服务器端仓库。这意味着每个贡献者拥有的不是一个,而是两个 Git 仓库:一个私有的本地仓库和一个公共的服务器端仓库。

指南

没有放之四海而皆准的 Git 工作流程。如前所述,开发一套能提高团队工作效率的 Git 工作流程非常重要。除了团队文化,工作流程还应该与企业文化相辅相成。分支和标签等 Git 功能应与企业的发布计划相辅相成。如果您的团队正在使用任务跟踪项目管理软件,您可能希望使用与进行中的任务相对应的分支。此外,在决定工作流程时还应考虑以下准则:

短期分支

分支与生产分支分离的时间越长,合并冲突和部署挑战的风险就越高。生命周期较短的分支能促进更简洁的合并和部署。

最大限度减少和简化还原

重要的是要有一个工作流程,帮助主动防止合并后必须进行还原。在将分支合并到主分支之前对其进行测试的工作流程就是一个例子。然而,意外总会发生。尽管如此,拥有一个允许轻松还原且不会扰乱其他团队成员工作流程的工作流程还是很有好处的。

与发布时间计划相匹配

工作流程应与企业的软件开发发布周期相辅相成。如果您计划每天发布多次版本,就需要保持主分支的稳定。如果发布频率较低,则可以考虑使用 Git 标签将分支标记为版本。


Git 功能分支工作流

特性分支工作流程的核心理念是,所有特性开发都应在专用分支而非main中进行。这种封装方式可以让多个开发人员在不影响主代码库的情况下,轻松完成某个特定功能的开发。这也意味着main永远不会包含损坏的代码,这对于持续集成环境来说是一个巨大的优势。

封装功能开发还可以利用拉取请求,这是一种围绕分支发起讨论的方式。拉取请求是围绕分支发起讨论的一种方式,它让其他开发人员有机会在功能集成到正式项目之前对其进行签字确认。或者,如果你被某个功能卡住了,也可以打开拉取请求,征求同事的建议。关键是,拉取请求能让团队成员非常容易地对彼此的工作发表评论。

Git 功能分支工作流是一个可组合的工作流,可被其他高级 Git 工作流利用。我们在 Git 工作流程概览页面讨论了其他 Git 工作流程。Git 特性分支工作流专注于分支模型,也就是说,它是一个管理和创建分支的指导框架。其他工作流程则更侧重于仓库。Git 特性分支工作流可以并入其他工作流。传统上,Gitflow 和 Git f分叉工作流都使用 Git 特性分支工作流作为分支模型。

How it works

功能分支工作流程假定有一个中央存储库,主分支代表正式的项目历史。开发人员每次开始开发新功能时,都会创建一个新的分支,而不是直接在本地主分支上提交。特性分支应有描述性的名称,如 animated-menu-items 或 issue-#1061。这样做的目的是让每个分支都有一个清晰、高度集中的目的。Git 在技术上不区分主分支和特性分支,因此开发者可以对特性分支进行编辑、阶段化和提交修改。

此外,特性分支还可以(也应该)推送到中心仓库。这样,开发人员就可以在不接触任何正式代码的情况下与其他开发人员共享某个特性。由于主分支是唯一的 “特殊 “分支,因此在中心版本库中存储多个特性分支不会造成任何问题。当然,这也是备份每个人的本地提交的便捷方法。下面是一个特性分支的生命周期。

从主分支开始

所有特性分支都是根据项目的最新代码状态创建的。本指南假定这是在主分支中维护和更新的。

1
2
3
git checkout main
git fetch origin
git reset --hard origin/main

创建存储库

这会将 repo 切换到主分支,提取最新提交,并重置 repo 的本地 main 副本以匹配最新版本。

创建新分支

为每个功能或问题创建一个单独的分支。创建分支后,在本地签出它,这样你所做的任何更改都会在该分支上进行。

1
git checkout -b new-feature

这会基于 main 检查出一个名为 new-feature 的分支,而 -b 标志会告诉 Git 如果该分支还不存在,就创建它。

更新、添加、提交和推送更改

在该分支上,按常规方式编辑、暂存和提交修改,根据需要提交尽可能多的改动。像任何时候使用 Git 一样,对功能进行修改和提交。准备就绪后,推送您的提交,更新 Bitbucket 上的功能分支。

1
2
3
git status
git add <some-file>
git commit

向远程推送功能分支

将特性分支推送到中央版本库是个好主意。这可以作为一个方便的备份,在与其他开发人员协作时,可以让他们查看对新分支的提交。

1
git push -u origin new-feature

该命令会将 new-feature 推送到远程中央仓库(origin),而 -u 标志会将其添加为远程跟踪分支。设置好跟踪分支后,就可以调用 git push,无需任何参数,自动将 new-feature 分支推送到中心仓库。要获得对新特性分支的反馈,可在 Bitbucket Cloud 或 Bitbucket Data Center 等版本库管理解决方案中创建一个拉取请求。在那里,你可以添加审核员,并在合并前确保一切顺利。

解决反馈

现在,队友们可以评论并批准推送的提交。在本地解决他们的意见,提交并将建议的修改推送到 Bitbucket。您的更新就会出现在拉取请求中。

合并您的拉取请求

在合并之前,如果其他人对 repo 进行了修改,你可能需要解决合并冲突。当你的拉取请求被批准且无冲突后,你就可以把代码添加到主分支中了。从 Bitbucket 中的拉取请求进行合并。

除了隔离功能开发外,分支还能通过拉取请求讨论变更。一旦有人完成了一项功能,他们不会立即将其合并到主分支中。相反,他们会将功能分支推送到中心服务器,并提交拉取请求,要求将他们添加的内容合并到主分支中。这样,其他开发人员就有机会在修改成为主代码库的一部分之前对其进行审查。

代码审查是拉取请求的一个主要优点,但实际上拉取请求的设计初衷是作为一种谈论代码的通用方式。你可以把拉取请求看作是专门针对某个分支的讨论。这意味着拉取请求也可以在开发过程的更早阶段使用。例如,如果开发人员在某个功能上需要帮助,他们只需提交拉取请求即可。相关人员会自动收到通知,并能在相关提交旁边看到问题。

一旦拉取请求被接受,发布功能的实际操作与集中式工作流程中的操作大致相同。首先,你需要确保本地主目录与上游主目录同步。然后,将特性分支合并到主版本中,并将更新后的主版本推送回中央版本库。

Bitbucket Cloud 或 Bitbucket Server 等产品仓库管理解决方案可为拉取请求提供便利。查看 Bitbucket Server 拉取请求文档,了解示例。

示例

下面是一个使用功能分支工作流程的示例。该场景是一个团队围绕新功能拉取请求进行代码审查。这只是该模型多种用途中的一个例子。

玛丽开始了一项新功能

在开始开发功能之前,Mary 需要一个独立的分支来工作。她可以使用以下命令申请一个新分支:

1
git checkout -b marys-feature main

这样就在 main 分支的基础上签出了一个名为 marys-feature 的分支,而 -b 标志则告诉 Git 如果该分支还不存在,就创建它。在这个分支上,Mary 按常规方式进行编辑、暂存和提交,并根据需要提交尽可能多的改动:

1
2
3
git status
git add <some-file>
git commit

玛丽去吃午饭

玛丽在一上午的时间里为她的功能添加了一些提交。在她离开去吃午饭之前,最好把她的功能分支推送到中央仓库。这可以作为方便的备份,但如果 Mary 与其他开发人员协作,这也会让他们访问她的初始提交。

1
git push -u origin marys-feature

这条命令会将 marys-feature 推送到中心仓库(origin),而 -u 标志会将其添加为远程跟踪分支。设置好跟踪分支后,Mary 可以不加任何参数地调用 git push 来推送她的功能。

玛丽完成她的特色分支

当 Mary 吃完午饭回来时,她完成了她的功能。在将其合并到主仓库之前,她需要提交一个拉取请求,让团队其他成员知道她已经完成了。但首先,她要确保中央仓库中有她的最新提交:

1
git push

然后,她在 Git GUI 中提交拉取请求,要求将 marys-feature 合并到 main 中,团队成员就会自动收到通知。拉取请求的好处在于,它们会在相关提交的旁边显示注释,因此很容易就能提出有关特定变更集的问题。

Bill收到拉取请求

比尔收到pull请求并查看了 marys-feature。他决定在将其整合到正式项目之前做一些修改,于是他和玛丽通过pull请求进行了一些来回交流。

玛丽进行更改

为了进行更改,Mary 采用了与创建第一个功能迭代完全相同的流程。她进行编辑、暂存、提交,并将更新推送到中央版本库。她的所有活动都会显示在pull请求中,而Bill仍然可以在整个过程中发表评论。

如果Bill愿意,他可以将 marys-feature 拉入他的本地版本库,然后自己继续工作。他添加的任何提交也会显示在pull请求中。

玛丽发布特色功能

一旦Bill准备好接受pull请求,就需要有人将该功能合并到稳定项目中(这可以由Bill或Mary完成):

1
2
3
4
git checkout main
git pull
git pull origin marys-feature
git push

这一过程通常会导致合并提交。有些开发人员喜欢这样做,因为这就像是将特性与代码库的其他部分象征性地结合在一起。不过,如果你偏爱线性历史,也可以在执行合并之前将特性重置到 main 的顶端,从而实现快进合并。

有些图形用户界面会自动执行拉取请求接受流程,只需点击 “Accept “按钮即可运行所有这些命令。如果你的 GUI 没有这样做,它至少能在特性分支合并到主分支时自动关闭拉取请求。

与此同时,John也在做着同样的事情。

当Mary和Bill在研究 marys-feature 并在她的拉取请求中进行讨论时,John正在他自己的特性分支中做着完全相同的事情。通过将特性功能隔离到不同的分支中,每个人都可以独立工作,但在必要时与其他开发人员共享变更仍是轻而易举的事。

Summary

在本文档中,我们讨论了 Git 功能分支工作流程。该工作流有助于组织和跟踪专注于业务领域功能集的分支。其他 Git 工作流,如 Git Forking 工作流和 Gitflow 工作流,都是以 repo 为中心的,可以利用 Git 功能分支工作流来管理它们的分支模型。本文档演示了用于实现 Git 功能分支工作流的高级代码示例和虚构示例。与功能分支工作流程建立的一些关键关联是:

  • 专注于分支模式
  • 可被其他面向仓库的工作流程利用
  • 通过拉取请求和合并审查促进与团队成员的协作

在功能分支的审核和合并阶段使用git rebase,可以创建一个有凝聚力的 Git 历史记录。功能分支模型是促进团队协作的绝佳工具。


Gitflow 工作流

Gitflow 是一种传统的 Git 工作流程,最初是一种管理 Git 分支的颠覆性新策略。如今,Gitflow 已不再受欢迎,而基于主干的工作流程已被视为现代持续软件开发和 DevOps 实践的最佳实践。在 CI/CD 中使用 Gitflow 也很有挑战性。本文章将详细介绍 Gitflow 的历史。

什么是 Gitflow?

Gitflow 是另一种 Git 分支模式,包括使用特性分支和多个主分支。它由 nvie 网站的 Vincent Driessen 首次发布并流行开来。与基于主干的开发模式相比,Gitflow 的分支数量多、寿命长、提交量大。在这种模式下,开发人员创建一个特性分支,并推迟将其合并到主干分支,直到特性完成。这些生命周期较长的特性分支需要更多协作才能合并,偏离主干分支的风险也更高。它们还可能带来相互冲突的更新。

Gitflow 可用于有计划发布周期的项目和 DevOps 最佳实践–持续交付。除特性分支工作流程外,该工作流程不添加任何新概念或命令。相反,它为不同的分支分配了非常具体的角色,并定义了它们应该如何以及何时进行交互。除了功能分支,它还使用单个分支来准备、维护和记录发布。当然,您还可以利用特性分支工作流的所有优点:拉取请求、隔离实验和更高效的协作。

How it works

开发和主要分支

这种工作流程使用两个分支来记录项目的历史,而不是一个main分支。main分支存储正式发布的历史,而开发分支则作为功能的集成分支。在main分支中的所有提交都标上版本号也很方便。

第一步是用 develop 分支来补充默认的 main 分支。一个简单的方法是由一名开发人员在本地创建一个空的 develop 分支,然后推送到服务器:

1
2
git branch develop
git push -u origin develop

该分支将包含项目的完整历史,而主分支将包含简略版本。其他开发者现在应该克隆中央仓库,并为 develop 创建一个跟踪分支。

使用 git-flow 扩展库时,在现有仓库上执行 git flow init 将创建 develop 分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ git flow init


Initialized empty Git repository in ~/project/.git/
No branches exist yet. Base branches must be created now.
Branch name for production releases: [main]
Branch name for "next release" development: [develop]


How to name your supporting branch prefixes?
Feature branches? [feature/]
Release branches? [release/]
Hotfix branches? [hotfix/]
Support branches? [support/]
Version tag prefix? []


$ git branch
* develop
main

功能分支

步骤 1. 创建存储库

每个新功能都应该有自己的分支,可以推送到中央仓库进行备份/协作。但是, feature 分支不是从main分支中分离出来,而是将 develop 分支作为其父分支。当特性完成后,它会被合并回develop 分支。功能分支绝不能直接与main分支交互。

请注意, feature 分支与 develop 分支的结合,就是功能分支工作流程。但是,Gitflow 的工作流程并不止于此。

feature 分支通常是根据最新的 develop 分支创建的。

创建功能分支

不使用 git-flow 扩展

1
2
git checkout develop
git checkout -b feature_branch

使用 git-flow 扩展时

1
git flow feature start feature_branch

继续工作,像平常一样使用 Git。

完成功能分支

完成功能分支的开发工作后,下一步就是把 feature_branch 合并到 develop 中。

不使用 git-flow 扩展

1
2
git checkout develop
git merge feature_branch

使用 git-flow 扩展

1
git flow feature finish feature_branch

发布分支

一旦develop分支完成了足够的功能等待发布(或者预定的发布日期即将到来),就从 develop分支中分叉出一个 release 分支。创建该分支会启动下一个发布周期,因此在此之后就不能再添加新功能了,只有错误修复、文档生成和其他面向发布的任务应放在该分支中。一旦准备好发布,release分支就会被合并main 分支并标注版本号。此外,还应将其合并回develop分支,因为develop分支在发布分支后可能已经取得了进展。

使用专用分支来准备发布,可以让一个团队在完善当前发布的同时,让另一个团队继续为下一个发布开发功能。它还能创建定义明确的开发阶段(例如,可以轻松地说:”本周我们要准备 4.0 版”,并在版本库的结构中实际看到它)。

创建release分支是另一种直接的分支操作。与 feature 分支一样,release分支也基于 develop 分支。可以使用以下方法创建新的release分支。

不使用 git-flow 扩展:

1
2
git checkout develop
git checkout -b release/0.1.0

使用 git-flow 扩展时:

1
2
$ git flow release start 0.1.0
Switched to a new branch 'release/0.1.0'

一旦release版本准备就绪,就会将其合并到maindevelop分支中,然后删除release分支。合并回开发分支很重要,因为关键更新可能已添加到发布分支,它们需要能访问新功能。如果你的组织强调代码审查,这里将是拉取请求的理想位置。

要完成分支发布,可使用以下方法:

不使用 git-flow 扩展:

1
2
git checkout main
git merge release/0.1.0

或者使用 git-flow 扩展:

1
git flow release finish '0.1.0'

热修复分支

维护或 “热修复 “分支用于快速修补生产版本。Hotfix 分支与 release 分支和 feature 分支很相似,只是它们基于 main 分支而不是 develop分支。这是唯一应该直接从main分支分叉出来的分支。修复完成后,应立即将其并入main分支和 develop分支(或当前的release 分支),并在main分支上标注更新的版本号。

为错误修复设置专门的开发线,可以让团队在不影响其他工作流程或等待下一个发布周期的情况下解决问题。你可以把维护分支看作是直接与主分支协同工作的临时发布分支。可以使用以下方法创建热修复分支:

不使用 git-flow 扩展:

1
2
git checkout main
git checkout -b hotfix_branch

使用 git-flow 扩展时:

1
$ git flow hotfix start hotfix_branch

与完成release分支类似,hotfix分支会合并到main 分支和develop分支

1
2
3
4
5
git checkout main
git merge hotfix_branch
git checkout develop
git merge hotfix_branch
git branch -D hotfix_branch
1
$ git flow hotfix finish hotfix_branch

Example

演示功能分支流程的完整示例如下。假设我们有一个包含main分支的版本库。

1
2
3
4
5
6
7
8
9
git checkout main
git checkout -b develop
git checkout -b feature_branch
# work happens on feature branch
git checkout develop
git merge feature_branch
git checkout main
git merge develop
git branch -d feature_branch

除功能分支和发布分支流程外,热修复示例如下:

1
2
3
4
5
6
7
git checkout main
git checkout -b hotfix_branch
# work is done commits are added to the hotfix_branch
git checkout develop
git merge hotfix_branch
git checkout main
git merge hotfix_branch

这里我们讨论的是 Gitflow 工作流。Gitflow 是您和您的团队可以使用的多种 Git 工作流程之一。

了解 Gitflow 的一些要点如下:

  • 该工作流非常适合基于发布的软件工作流。
  • Gitflow 为热修复到生产提供了专用通道。

Gitflow 的整体流程是:

  1. main分支创建一个 develop 分支

  2. develop分支创建 release 分支

  3. develop分支创建 Feature 分支

  4. feature 完成后,合并到 develop 分支中

  5. release 分支完成后,合并到 develop 分支和 main分支中

  6. 如果在 main分支中发现问题,就会从 main分支创建 hotfix 分支

  7. 一旦完成热修复,它就会被合并到 develop 分支和 main分支中


Forking工作流

Forking工作流与其他流行的 Git 工作流有本质区别。它不使用单一的服务器端仓库作为 “中央 “代码库,而是让每个开发者都拥有自己的服务器端仓库。这意味着每个贡献者拥有的不是一个,而是两个 Git 仓库:一个私有的本地仓库和一个公共的服务器端仓库。Forking Workflow最常见于公共开源项目。

Forking工作流的主要优势在于,无需每个人都推送到一个中央仓库,就能整合贡献。开发人员推送到自己的服务器端版本库,只有项目维护者才能推送到官方版本库。这样,维护者就可以接受任何开发者的提交,而无需给予他们写入官方代码库的权限。

Forking工作流程通常遵循基于 Gitflow 工作流程的分支模型。这意味着完整的功能分支将被合并到原始项目维护者的仓库中。这样就形成了一个分布式工作流程,为大型有机团队(包括不受信任的第三方)提供了一种灵活的安全协作方式。这也使其成为开源项目的理想工作流程。

How it works

与其他 Git 工作流程一样,Forking Workflow也是从服务器上存储的官方公共仓库开始的。但当新的开发者想开始项目工作时,他们不会直接克隆官方仓库。

相反,他们会分叉官方版本库,在服务器上创建一个副本。这个新副本将作为他们的个人公共仓库–其他开发者不得向其推送,但他们可以从中提取修改(稍后我们将了解这一点的重要性)。创建完服务器端副本后,开发人员会执行git clone将其复制到本地计算机上。就像其他工作流程一样,这也是他们的私人开发环境。

当他们准备好发布本地提交时,就会把提交推送到自己的公共仓库–而不是官方仓库。然后,他们向主版本库提交拉取请求,让项目维护者知道更新已准备好集成。如果贡献的代码有问题,拉取请求还可以作为方便的讨论线程。下面是这一工作流程的分步示例。

  1. 开发者 “forks”一个 “官方”服务器端版本库。这将创建他们自己的服务器端副本
  2. 将新的服务器端副本克隆到他们的本地系统
  3. 将 “官方 “仓库的 Git 远程路径添加到本地克隆中
  4. 创建新的本地特性分支
  5. 开发人员对新分支进行修改
  6. 为更改创建新提交
  7. 分支会被推送到开发者自己的服务器端副本
  8. 开发人员从新分支向 “官方 “版本库打开拉取请求
  9. 拉取请求被批准合并,并被合并到原来的服务器端版本库中

要将功能集成到正式代码库中,维护者会将贡献者的修改拉入本地版本库,检查确保不会破坏项目,将其合并到本地主分支,然后将主分支推送到服务器上的正式版本库。现在,该贡献已成为项目的一部分,其他开发人员应从官方版本库中提取,以同步他们的本地版本库。

重要的是要明白,在Forking工作流程中,”官方 “版本库的概念只是一种约定。事实上,官方版本库之所以如此官方,是因为它是项目维护者的公共版本库。

Forking vs cloning

需要注意的是,”forked”仓库和”forking”都不是特殊的操作。Forked仓库是使用标准的git clone命令创建的。分叉仓库一般是 “服务器端克隆”,通常由第三方 Git 服务(如 Bitbucket)管理和托管。创建分叉仓库没有唯一的 Git 命令。克隆操作本质上是复制一个仓库及其历史。

Forking 工作流程中的分支

所有这些个人公共仓库实际上只是一种与其他开发者共享分支的便捷方式。就像特性分支工作流和 Gitflow 工作流一样,每个人都应该继续使用分支来隔离单个特性。唯一的区别在于如何共享这些分支。在Forking工作流中,它们会被拉入其他开发者的本地版本库,而在特性分支和 Gitflow 工作流中,它们会被推送到官方版本库。

Fork a repository

所有加入 Forking Workflow 项目的新开发者都需要 fork 官方仓库。如前所述,fork只是一个标准的git clone操作。可以通过 SSH 登录服务器,然后运行git clone将其复制到服务器上的另一个位置。流行的 Git 托管服务(如 Bitbucket)提供的版本库fork功能可自动完成这一步骤。

Clone your fork

假设使用 Bitbucket 托管这些版本库,项目中的开发人员就应该拥有自己的 Bitbucket 账户,并克隆他们fork的版本库副本:

1
git clone https://user@bitbucket.org/user/repo.git

Adding a remote

其他 Git 工作流使用一个指向中央仓库的 origin 远程地址,而分叉工作流则需要两个远程地址–一个指向官方仓库,另一个指向开发者的个人服务器端仓库。虽然你可以随心所欲地调用这些远程地址,但常见的惯例是使用 origin 作为分叉仓库的远程地址(运行 git clone 时会自动创建),而使用 upstream 作为官方仓库的远程地址。

1
git remote add upstream https://bitbucket.org/maintainer/repo

您需要使用上述命令自行创建upstream远程仓库。这样你就能轻松地随着官方项目的进展更新本地版本库。请注意,如果你的upstream版本库启用了身份验证(即它不是开源的),你就需要提供一个用户名,就像这样:

1
git remote add upstream https://user@bitbucket.org/maintainer/repo.git

这要求用户在克隆或拉取官方代码库之前提供有效的密码。

Working in a branch: making & pushing changes

在开发人员的分叉存储库的本地副本中,他们可以编辑代码、提交更改并创建分支,就像在其他 Git 工作流程中一样:

1
git checkout -b some-feature # Edit some code git commit -a -m "Add first draft of some feature"

他们的所有更改都将完全私有,直到他们将其推送到公共存储库为止。而且,如果官方项目已经向前推进,他们可以使用 git pull 访问新的提交:

1
git pull upstream main

由于开发人员应该在专用的功能分支中工作,因此这通常会导致快进合并。

Making a pull request

一旦开发人员准备好分享他们的新功能,他们需要做两件事。首先,他们必须将自己的贡献推送到公共仓库,让其他开发者也能访问。他们的远程源应该已经设置好了,所以他们只需做以下几件事

1
git push origin feature-branch

这与其他工作流程的不同之处在于,origin remote指向开发者的个人服务器端版本库,而不是主代码库。

其次,他们需要通知项目维护者,他们想把自己的功能合并到官方代码库中。Bitbucket 提供了一个 “pull request”按钮,点击后会弹出一个表单,要求你指定要将哪个分支合并到正式版本库中。通常情况下,你会希望将你的功能分支整合到上游远程的main分支中。

Summary

简而言之,Forking工作流程常用于公共开源项目。Forking是在项目 repo 的服务器副本上执行的git clone操作。Forking工作流程通常与 Bitbucket 等 Git 托管服务结合使用。Forking工作流程的一个高级示例是

  1. 您想为一个托管在 bitbucket.org/userA/open-project 的开源库作出贡献

  2. 使用 Bitbucket 创建一个 repo 的 fork 到 bitbucket.org/YourName/open-project

  3. 在本地系统上执行 git clone on https://bitbucket.org/YourName/open-project 以获得该 repo 的本地副本

  4. 在本地仓库中创建一个新的 feature 分支

  5. 完成新功能的工作,并执行git commit保存更改

  6. 然后将新 feature 分支推送到远程forked仓库

  7. 使用 Bitbucket,在 bitbucket.org/userA/open-project 上针对原 repo 打开新分支的拉取请求。

Forking工作流能帮助项目维护者向任何开发者开放版本库,而无需手动管理每个贡献者的授权设置。这就为维护者提供了更多 “pull”式的工作流程。Forking工作流最常用于开源项目,也可用于私有业务工作流,以便对合并到发布版本中的内容进行更权威的控制。这对于有部署经理或严格发布周期的团队非常有用。

概述

Git 是一个免费的开源版本控制系统,最初由 Linus Torvalds 于 2005 年创建。与 SVN 和 CVS 等旧的集中式版本控制系统不同,Git 是分布式的:每个开发人员都在本地拥有其代码存储库的完整历史记录。这使得存储库的初始克隆速度变慢,但后续操作(例如提交、责备、比较、合并和日志)速度显着加快。

Git 还对分支、合并和重写存储库历史提供出色的支持,这导致了许多创新且强大的工作流程和工具。Pull 请求是一种流行的工具,它允许团队在 Git 分支上进行协作并有效地审查彼此的代码。Git 是当今世界上使用最广泛的版本控制系统,被认为是软件开发的现代标准。

工作流

你的本地仓库由 git 维护的三棵“树”组成。第一个是你的 工作目录,它持有实际文件;第二个是 暂存区(Index),它像个缓存区域,临时保存你的改动;最后是 HEAD,它指向你最后一次提交的结果。

Git 目录(位于 YOUR-PROJECT-PATH/.git/ 中)是 Git 存储准确跟踪项目所需的所有内容的位置。这些内容包括元数据和一个对象数据库,其中包含项目文件的压缩版本。

工作目录是用户在本地对项目进行更改的地方。工作目录从 Git 目录的对象数据库中提取项目的文件,并将其放置在用户的本地计算机上。

暂存区是一个文件,用于存储下一次 commit 内容的信息。“commit” 的意思是你告诉 Git 保存暂存区的更改。 Git 照原样拍摄文件快照,并将该快照永久存储在 Git 目录中。

在三个部分中,文件可以在任何给定时间处于三种主要状态:提交修改暂存。在工作目录中对文件进行修改,然后,将其移至暂存区进行暂存,最后,commit 提交文件。

以下是 Git 工作的基本概述:

  1. 使用 git 托管工具(如 Bitbucket)创建一个“存储库”(项目)
  2. 将远程存储库复制(或克隆)到本地计算机
  3. 将文件add到本地存储库并commit(保存)更改
  4. 将您的更改push到您的主分支
  5. 使用 git 托管工具更改文件并提交
  6. 将更改pull到本地计算机
  7. 创建“分支”(版本),进行更改,提交更改
  8. 打开“拉取请求”(建议对主分支进行更改)
  9. 将您的分支“合并”到主分支

设置存储库

git init

git init 命令会创建一个新的 Git 仓库。它可以用来将一个现有的、未版本控制的项目转换为 Git 仓库,也可以用来初始化一个新的、空的仓库。大多数其他 Git 命令在初始化仓库之前都不可用,所以这通常是在新项目中运行的第一个命令。

执行 git init 会在当前工作目录下创建一个 .git 子目录,其中包含新仓库所需的所有 Git 元数据。这些元数据包括对象、引用和模板文件的子目录。此外,还会创建一个 HEAD 文件,它是一个指向当前已签出分支或提交的指针,其中包含特定时间内整个代码库的不可更改快照。无论 HEAD 直接(使用哈希值)或通过引用(使用分支)引用哪个提交,它始终都是本地更改所依据的提交。

除了项目根目录中的 .git 目录外,现有项目不会被改动(与 SVN 不同,Git 并不要求每个子目录中都有 .git 子目录)。

默认情况下,git init 会将 Git 配置初始化为 .git 子目录路径。如果你想把子目录放在其他地方,可以修改或自定义子目录路径。你可以将 $GIT_DIR 环境变量设置为自定义路径,这样 git init 就会在那里初始化 Git 配置文件。此外,你还可以通过--separate-git-dir参数来达到同样的效果。

与 SVN 相比,该git init命令是创建新版本控制项目的极其简单的方法。Git 不需要您创建存储库、导入文件和签出工作副本。此外,Git 不需要任何预先存在的服务器或管理员权限。您所要做的就是 cd 进入您的项目子目录并运行git init,您将拥有一个功能齐全的 Git 存储库。

1
git init

将当前目录转换为 Git 存储库。这会向当前目录创建一个.git子目录,并可以开始记录项目的修订。

1
git init <directory>

在指定目录下创建一个空的 Git 仓库。运行此命令将创建一个名为 <directory> 的新子目录,其中只包含 .git 子目录。

如果已经在某个项目目录下运行过 git init,且其中包含 .git 子目录,那么可以放心地在同一项目目录下再次运行 git init。它不会覆盖现有的 .git 配置。

git clone

目的:repo-to-repo协作开发副本

如果一个项目已经在中央仓库已建立,git clone 命令是用户获取开发副本的最常用方法。与 git init 一样,git clone 通常也是一次性操作。开发人员获得工作副本后,所有版本控制操作和协作都将通过本地仓库进行管理。

要知道,Git 的 “工作副本 “与从 SVN 仓库中签出代码所得到的工作副本是完全不同的。与 SVN 不同,Git 不区分工作副本和中央仓库,它们都是完整的 Git 仓库。

这就使得使用 Git 与使用 SVN 进行协作有了本质区别。SVN 依赖于中心仓库和工作副本之间的关系,而 Git 的协作模式则基于仓库与仓库之间的交互。你不需要把工作副本检查到 SVN 的中心仓库,而是从一个仓库推送或拉取提交到另一个仓库。

git clone 主要用于指向现有存储库,并在另一个位置的新目录中克隆或复制该存储库。 原始存储库可以位于本地文件系统或远程计算机可访问的支持协议上。 git clone 命令复制现有的 Git 存储库。 这有点像 SVN checkout,只不过“工作副本”是一个成熟的 Git 存储库——它有自己的历史记录,管理自己的文件,并且是与原始存储库完全隔离的环境。

为了方便起见,克隆会自动创建一个名为“origin”的远程连接,指向原始存储库。 这使得与中央存储库交互变得非常容易。 这种自动连接是通过配置在refs/remotes/origin下的remote.origin.urlremote.origin.fetch 变量来建立对远程分支头的 Git 引用。

下面的示例演示了如何获取存储在可使用 SSH 用户名 john 访问的服务器上的中央存储库的本地副本:git clone example.com

1
2
3
git clone ssh://john@example.com/path/to/my-project.git 
cd my-project
# Start working on the project

第一条命令是在本地计算机的 my-project 文件夹中初始化一个新的 Git 仓库,并将中央仓库的内容填充到其中。您可以 cd 进入项目并开始编辑文件、提交快照以及与其他存储库交互。还要注意的是,克隆仓库中省略了 .git 扩展名。这反映了本地副本的非裸状态。

clone到特定文件夹

1
git clone <repo> <directory>

将位于 <repo> 的版本库克隆到本地计算机上名为 <directory> 的文件夹中。

clone特定标签

1
git clone --branch <tag> <repo>

克隆位于 <repo> 的资源库,并只克隆 <tag>ref

clone特定分支

1
git clone --branch

通过 -branch 参数,你可以指定要克隆的特定分支,而不是远程 HEAD 指向的分支(通常是主分支)。此外,你还可以传递一个 tag 来代替分支,以达到同样的效果。

Git URL 协议

-SSH

Secure Shell (SSH) 是一种普遍存在的经过身份验证的网络协议,大多数服务器上通常默认配置该协议。由于 SSH 是一种经过身份验证的协议,因此您需要在连接之前与托管服务器建立凭据。

- GIT

git 独有的协议。Git 自带一个守护进程,运行端口为 (9418)。该协议与SSH类似,但没有身份验证。

- HTTP

超文本传输协议。Web 协议,最常用于通过 Internet 传输网页 HTML 数据。Git 可以配置为通过HTTP进行通信


git config

git config 命令是一个方便的功能,用于在全局或本地项目级别设置 Git 配置值。这些配置级别与 .gitconfig 文本文件相对应。执行 git config 会修改配置文本文件。

我们将讨论常见的配置设置,如电子邮件、用户名和编辑器。我们还将讨论 Git 别名,它允许你为常用的 Git 操作创建快捷方式。熟悉git config和各种 Git 配置设置,将有助于你创建强大的、个性化的 Git 工作流程。

git config 最基本的用例是用一个配置名来调用它,从而显示该配置名下的设置值。配置名称是以点分隔的字符串。例如:user.email

1
git config user.email

查看配置文件中设置的所有变量,以及它们的值。

1
git config --list

git 配置级别和文件

在进一步讨论git config的用法之前,我们先来了解一下配置级别。git config命令可以接受参数来指定操作的配置级别。以下是可用的配置级别:

  • --local

    默认情况下,如果没有通过配置选项,git config会写入本地级别。本地配置会应用到调用 git config 的上下文仓库。本地配置值保存在一个文件中,可以在 repo 的 .git 目录中找到:.git/config

  • --global

    全局配置是针对特定用户的,这意味着它适用于操作系统用户。全局配置值存储在用户主目录下的一个文件中。在 unix 系统中为 ~ /.gitconfig,在 windows 系统中为 C:\Users\\.gitconfig

  • --system

    系统级配置适用于整个机器。这包括操作系统上的所有用户和所有版本库。系统级配置文件位于系统根目录下的 gitconfig 文件中。在 unix 系统中为 $(prefix)/etc/gitconfig。在 Windows XP 上,该文件位于 C:\Documents and Settings\All Users\Application Data\Git\config 下;在 Windows Vista 及更新版本上,该文件位于 C:\ProgramData\Git\config 下。

因此,配置级别的优先顺序是:localglobalsystem。这意味着在查找配置值时,Git 会从本地层级开始,然后上升到系统层级。

写一个配置项

以我们已经了解的git config为基础,我们来看一个写值的例子:

1
git config --global user.email "your_email@example.com"

​ 此示例将配置项user.email的值写为 your_email@example.com 。它使用了 --global 标志,因此该值是为当前操作系统用户设置的。

git 配置编辑器 - core.editor

许多 Git 命令都会启动一个文本编辑器,提示进一步输入。git config最常见的用例之一就是配置 Git 应该使用哪个编辑器。下面列出了常用的编辑器和与之匹配的 git 配置命令:

编辑器 配置命令
Atom ~ git config --global core.editor "atom --wait"~
emacs ~ git config --global core.editor "emacs"~
nano ~ git config --global core.editor "nano -w"~
vim ~ git config --global core.editor "vim"~
Sublime Text (Mac) ~ git config --global core.editor "subl -n -w"~
Sublime Text (Win, 32-bit install) ~ git config --global core.editor "'c:/program files (x86)/sublime text 3/sublimetext.exe' -w"~
Sublime Text (Win, 64-bit install) ~ git config --global core.editor "'c:/program files/sublime text 3/sublimetext.exe' -w"~
Textmate ~ git config --global core.editor "mate -w"~

配置合并工具

如果出现合并冲突,Git 会启动一个 “合并工具”。默认情况下,Git 使用的是通用 Unix diff 程序的内部实现。Git 内部的 diff 程序是最基本的合并冲突查看器。有许多外部第三方合并冲突解决工具可以替代它。有关各种合并工具和配置的概述,请参阅”tips and tools to resolve conflits with Git“指南。

1
git config --global merge.tool kdiff3

配置彩色输出

Git 支持彩色终端输出,有助于快速读取 Git 输出。你可以自定义 Git 输出,使用个性化的颜色主题。git config 命令用于设置这些颜色值。

color.ui是 Git 颜色的主变量。设置为 false 将禁用所有 Git 彩色终端输出。

1
git config --global color.ui false

默认情况下,color.ui 设置为auto,这将在直接终端输出流中应用颜色。如果输出流被重定向到文件或管道到其他进程,自动设置将省略颜色代码输出。

可以将 color.ui 值设置为always,这样在将输出流重定向到文件或管道时也会应用颜色代码输出。这可能会无意中造成问题,因为接收管道可能并不期待彩色编码输入。

Git 颜色值

除了 color.ui,还有许多其他细粒度的颜色设置。与 color.ui 一样,这些颜色设置都可以设置为 falseautoalways。这些颜色设置还可以设置特定的颜色值。支持的颜色值举例如下

  • normal
  • black
  • red
  • green
  • yellow
  • blue
  • magenta
  • cyan
  • white

颜色也可以指定为十六进制颜色代码(如 #ff0000),或者 ANSI 256 颜色值(如果您的终端支持)。

  1. color.branch

    配置 Git 分支命令的输出颜色

  2. color.branch.<slot>

    • 此值也适用于 Git 分支输出。<slot> 是以下选项之一:
      1. current: 当前分支
      2. local: 本地分支
      3. remote: refs/remotes中的远程分支
      4. upstream: 上游跟踪分支
      5. plain: 任何其他引用
  3. color.diff

    git diffgit loggit show命令 配置输出颜色

  4. color.grep

    git grep 的应用输出颜色。

  5. color.showBranch

    启用或禁用git show branch命令的颜色输出

  6. color.status

Aliases-别名

你可能对操作系统命令行中的别名概念并不陌生;如果不熟悉,别名是一种自定义快捷方式,它定义了哪条命令将扩展为更长或更组合的命令。别名可以节省输入常用命令的时间和精力。Git 提供了自己的别名系统。Git 别名的一个常见用例是缩短提交命令。Git 别名存储在 Git 配置文件中。这意味着你可以使用 git config 命令来配置别名。

1
git config --global alias.ci commit

这个例子为 commit 命令创建了一个 ci 别名。然后你就可以通过执行 git ci 来调用 git commit。别名还可以引用其他别名来创建强大的组合。


git alias

本节将重点讨论 Git 别名。要更好地理解 Git 别名的价值,我们必须先讨论一下什么是别名。别名是快捷方式的同义词。

创建别名是其他流行工具(如 bash shell)的常见模式。别名用于创建可映射到较长命令的较短命令。别名可以减少执行命令所需的击键次数,从而提高工作流程的效率。

git checkout 命令为例。checkout 命令是一个频繁使用的 git 命令,随着时间的推移,按键次数会不断增加。我们可以创建一个别名,将 git co 映射到 git checkout,这样就可以用更短的按键形式:git co 代替,从而节省宝贵的人类指尖力量。

需要注意的是,没有直接的 git alias 命令。别名是通过使用 git config 命令和 Git 配置文件创建的。与其他配置值一样,别名可以在本地或全局范围内创建。

为了更好地理解 Git 别名,让我们举几个例子。

1
2
3
4
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status

前面的代码示例为常用的 git 命令创建了全局存储的快捷方式。创建别名不会修改源代码命令。因此,即使我们现在有了 git co 别名,git checkout 仍然可用。这些别名是使用 --global 标志创建的,这意味着它们将存储在 Git 的全局操作系统级配置文件中。在 Linux 系统中,全局配置文件位于用户主目录下的 /.gitconfig

1
2
3
4
5
[alias]
co = checkout
br = branch
ci = commit
st = status

如何创建 Git 别名?

可以通过两种主要方法创建别名:

  1. 直接编辑 Git 配置文件

    可以手动编辑并保存全局或本地配置文件以创建别名。全局配置文件位于$HOME/.gitconfig文件路径中。本地路径位于活动 git 存储库中/.git/config

    配置文件将遵循如下所示的部分:[alias]

    1
    2
    [alias]
    co = checkout
  2. 使用 git config 创建别名

    如前所述,git config 命令是快速创建别名的便捷工具。git config命令实际上是一个辅助工具,用于写入全局和本地 Git 配置文件。

    1
    git config --global alias.co checkout

保存更改

在 Git 或其他版本控制系统中工作时,”保存 “的概念要比在文字处理程序或其他传统文件编辑程序中的 “保存 “更为细微。传统软件中的 “保存 “与 Git 中的 “提交 “同义。提交相当于 Git 的 “保存”。传统的 “保存 “应被视为一种文件系统操作,用于覆盖现有文件或写入新文件。而 Git 的提交则是对文件和目录集合的操作。

在 Git 和 SVN 中保存更改也是一个不同的过程。SVN 提交或“签入”是远程推送到集中式服务器的操作。这意味着 SVN 提交需要互联网访问才能完全“保存”项目更改。Git 提交可以在本地捕获并建立,然后根据需要使用 git push -u origin main 命令推送到远程服务器。Git 是分布式应用模式,而 SVN 是集中式模式。分布式应用程序通常更健壮,因为它们不像集中式服务器那样存在单点故障。

git addgit statusgit commit 这几条命令结合使用,可以保存 Git 项目当前状态的快照。

Git 有一个额外的保存机制,做 “储藏库”。储藏室是一个短暂的存储区域,用于存储尚未提交的变更。储藏室在工作目录(三棵树中的第一棵)上运行,有很多使用选项。要了解更多信息,请访问 git stash 页面。

Git 仓库可以配置为忽略特定文件或目录。这将阻止 Git 保存对忽略内容的修改。Git 有多种配置方法来管理忽略列表。关于 Git 忽略配置的更多详情,请参阅 git ignore 页面。

git add

git add 命令将工作目录中的改动添加到暂存区域。它告诉 Git,您想在下一次提交中包含对某个文件的更新。不过,git add并不会对版本库产生任何重大影响–直到运行 git commit 才会真正记录更改。

除了这些命令,你还需要使用 git status 来查看工作目录和暂存区域的状态。

How it works

git addgit commit命令构成了 Git 的基本工作流程。无论团队的协作模式如何,每个 Git 用户都需要了解这两个命令。它们是将项目版本记录到版本库历史中的手段。

项目开发围绕着基本的编辑/暂存/提交模式。首先,在工作目录中编辑文件。准备好保存一份项目当前状态的副本时,就用git add进行阶段性修改。对暂存的快照满意后,用git commit将其提交到项目历史中。git reset命令用于撤销提交或暂存快照

除了git addgit commit,第三个命令 git push 对于完整的 Git 协作工作流程也必不可少。git push用于将提交的更改发送到远程存储库以进行协作。这使得其他团队成员能够访问一组已保存的更改。

The staging area-暂存区

git add命令的主要功能是将工作目录中的待处理更改推广到 git 暂存区域。暂存区是 Git 较为独特的功能之一,如果你来自 SVN(甚至 Mercurial)背景,可能需要一些时间来理解它。把它想象成工作目录和项目历史记录之间的缓冲区会有所帮助。暂存区与工作目录和提交历史一起被视为 Git 的 “三棵树”。

在提交到项目历史之前,暂存区可以让你把相关的改动归类为高度集中的快照,而不是提交上次提交后的所有改动。这意味着你可以对不相关的文件进行各种编辑,然后通过将相关变更添加到阶段并逐条提交,将它们分割成符合逻辑的提交。在任何版本控制系统中,创建原子提交都是很重要的,这样可以轻松跟踪错误,并在对项目其他部分影响最小的情况下还原更改。

常用命令

1
git add <file>

暂存 <file> 中的所有更改,以便下一次提交。

1
git add <directory>

暂存目录 <directory> 中的所有更改,以便下一次提交。

1
git add -p

开始交互式暂存会话,让你选择文件的部分内容添加到下一次提交中。系统会显示一大段修改,并提示你输入命令。使用 y 可以暂存该修改块,使用 n 可以忽略该修改块,使用 s 可以将其分割成更小的修改块,使用 e 可以手动编辑该修改块,使用 q 可以退出。

在启动一个新项目时,git add的功能与 svn import 相同。要创建当前目录的初始提交,请使用以下两条命令:

1
2
git add .
git commit

总结

回顾一下,git add 是一系列操作中的第一个命令,它指示 Git 将当前项目状态的快照 “保存 “到提交历史中。单独使用时,git add 会将工作目录中的待定修改推进到暂存区域。git status 命令用于检查版本库的当前状态,并可用于确认 git add 的推送。git reset 命令用于撤销 git add。然后使用 git commit 命令将暂存目录的快照提交到版本库的提交历史中。


git diff

Diffing 是一个接收两个输入数据集并输出它们之间差异的函数。git diff是一条多用途 Git 命令,执行时会在 Git 数据源上运行差异函数。这些数据源可以是提交、分支、文件等。本文将讨论git diff的常见调用方式和差异化工作流程模式。git diff命令通常与git statusgit log一起用于分析 Git 仓库的当前状态。

读取差异:输出

  1. 比较输入

    1
    diff --git a/diff_test.txt b/diff_test.txt

    这一行显示 diff 的输入源。我们可以看到,a/diff_test.txtb/diff_test.txt 已被传递给 diff

  2. 元数据

    1
    index 6b0c6cf..b37e70a 100644

    这一行显示一些 Git 内部元数据。您很可能不需要这些信息。输出中的数字对应 Git 对象版本哈希标识符。

  3. 变更标记

    1
    2
    --- a/diff_test.txt
    +++ b/diff_test.txt

    这是为每个差异输入源分配符号的案例。在本例中,来自文件 a/diff_test.txt 的变更用符号 --- 标记,来自文件 b/diff_test.txt 的变更用符号 +++ 标记。

  4. 变更高亮

    1
    git diff --color-words

    git diff 还有一种特殊模式,能以更好的粒度高亮显示改动:--color-words。该模式用空白标记添加和删除的行,然后进行差异化。

比较文件:git diff file

1
git diff HEAD ./path/to/file

git diff 命令可以通过一个明确的文件路径选项。如果给git diff传递了一个文件路径,diff 操作就会作用于指定的文件。

此示例的作用域是 ./path/to/file,调用时,它会将工作目录中的具体改动与索引进行比较,显示尚未暂存的改动。默认情况下,git diff会执行与HEAD的比较。

1
git diff --cached ./path/to/file

当使用--cached选项调用git diff时,diff 会将已缓存的变更与本地版本库进行比较。--cached选项与--staged选项同义。

比较所有变化

git diff命令不带文件路径的调用将比较整个存储库中的更改。上述文件特定示例可以在没有参数的情况下调用./path/to/file,并且在本地存储库中的所有文件中具有相同的输出结果。

自上次提交以来的更改

默认情况下,git diff将显示自上次提交以来所有未提交的更改。

1
git diff

比较两个不同提交之间的文件差异

git diff可以将提交的 Git refs 传递给 diff。例如 HEAD、标签和分支名。Git 中的每个提交都有一个commit ID,执行GIT LOG命令时可以得到。您也可以将此commit ID 传递给git diff

1
2
3
4
5
6
7
git log --pretty=oneline
957fbc92b123030c389bf8b4b874522bdf2db72c add feature
ce489262a1ee34340440e55a0b99ea6918e19e7a rename some classes
6b539f280d8b0ec4874671bae9c6bed80b788006 refactor some code for feature
646e7863348a427e1ed9163a9a96fa759112f102 add some copy to body

$:> git diff 957fbc92b123030c389bf8b4b874522bdf2db72c ce489262a1ee34340440e55a0b99ea6918e19e7a

比较分支

1
git diff branch1..branch2

本例介绍点运算符。示例中的两个点表示 diff 输入是两个分支的顶端。如果省略点,在分支之间使用空格,也会产生同样的效果。此外,还有一个三点操作符:

1
git diff branch1...branch2

比较两个分支的文件差异

要跨分支比较特定文件,请将文件的路径作为第三个参数传递给git diff

1
git diff main new_branch ./diff_test.txt

git stash

git stash可以暂时搁置(或储藏)您对工作副本所做的修改,这样您就可以处理其他事情,稍后再回来重新应用。如果您需要快速切换上下文并处理其他工作,但代码改动进行到一半还没准备好提交,那么stash就很方便了。

Stashing your work

git stash命令会获取未提交的修改(包括已暂存和未暂存的),将其保存起来以备后用,然后从工作副本中将其还原。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git status
On branch main
Changes to be committed:

new file: style.css

Changes not staged for commit:

modified: index.html

$ git stash
Saved working directory and index state WIP on main: 5002d47 our new homepage
HEAD is now at 5002d47 our new homepage

$ git status
On branch main
nothing to commit, working tree clean

这时,你可以自由地进行修改、创建新提交、切换分支以及执行其他任何 Git 操作;准备好后,再回来重新应用你的储藏。

请注意,储藏库是本地的 Git 仓库;推送时储藏库不会转移到服务器上。

Re-applying your stashed changes

你可以使用git stash pop命令重新应用之前储藏的更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git status
On branch main
nothing to commit, working tree clean
$ git stash pop
On branch main
Changes to be committed:

new file: style.css

Changes not staged for commit:

modified: index.html

Dropped refs/stash@{0} (32b3aa1d185dfe6d57b3c3cc3b32cbf3e380cc6a)

Popping 会重新应用之前储藏的更改,并从储藏库中将其删除。

或者,你也可以使用git stash apply将更改重新应用到工作副本,并将其继续保留在储藏库中:

1
2
3
4
5
6
7
8
9
$ git stash apply
On branch main
Changes to be committed:

new file: style.css

Changes not staged for commit:

modified: index.html

如果你想在多个分支中应用相同的隐藏更改,这一点非常有用。

现在你已经了解了储藏的基础知识,但还需要注意一个问题:默认情况下,Git 不会储藏对未跟踪或忽略的文件所做的修改。

Stashing untracked or ignored files

默认情况下,运行 git stash 会储藏以下内容:

  • 已添加到索引的变更(已暂存变更)
  • 对当前由 Git 跟踪的文件所做的更改(未暂存的更改)

但它不会储藏以下内容:

  • 工作副本中尚未暂存的新文件
  • 被忽略的文件

因此,如果我们在上面的例子中添加了第三个新文件,但没有将其放入暂存阶段(即没有运行git add),那么git stash也不会将其存放起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ script.js

$ git status
On branch main
Changes to be committed:

new file: style.css

Changes not staged for commit:

modified: index.html

Untracked files:

script.js

$ git stash
Saved working directory and index state WIP on main: 5002d47 our new homepage
HEAD is now at 5002d47 our new homepage

$ git status
On branch main
Untracked files:

script.js

添加-u选项(或--include-untracked选项)后,git stash也会将未跟踪的文件储藏起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ git status
On branch main
Changes to be committed:

new file: style.css

Changes not staged for commit:

modified: index.html

Untracked files:

script.js

$ git stash -u
Saved working directory and index state WIP on main: 5002d47 our new homepage
HEAD is now at 5002d47 our new homepage

$ git status
On branch main
nothing to commit, working tree clean

在运行git stash时,你也可以通过-a选项(或--all选项)来包含对忽略文件的修改。

Managing multiple stashes

你并不局限于一个储藏库。你可以多次运行 git stash 来创建多个储藏库,然后使用git stash list来查看它们。默认情况下,储藏库会在您创建储藏库的分支和提交上简单标识为 “WIP“(正在进行中)。时间久了,就很难记住每个匿藏库包含的内容:

1
2
3
4
$ git stash list
stash@{0}: WIP on main: 5002d47 our new homepage
stash@{1}: WIP on main: 5002d47 our new homepage
stash@{2}: WIP on main: 5002d47 our new homepage

为了提供更多的上下文信息,使用 git stash save "message "为储藏注释说明是个不错的做法:

1
2
3
4
5
6
7
8
$ git stash save "add style to our site"
Saved working directory and index state On main: add style to our site
HEAD is now at 5002d47 our new homepage

$ git stash list
stash@{0}: On main: add style to our site
stash@{1}: WIP on main: 5002d47 our new homepage
stash@{2}: WIP on main: 5002d47 our new homepage

默认情况下,git stash pop会重新应用最近创建的储藏: stash@{0}

例如,你可以通过最后一个参数传递储藏库的标识符来选择要重新应用的储藏库:

1
$ git stash pop stash@{2}

Viewing stash diffs

您可以使用git stash show查看储藏的摘要:

1
2
3
4
$ git stash show
index.html | 1 +
style.css | 3 +++
2 files changed, 4 insertions(+)

或者通过-p选项(或--patch)来查看储藏的完整差异:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ git stash show -p
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..d92368b
--- /dev/null
+++ b/style.css
@@ -0,0 +1,3 @@
+* {
+ text-decoration: blink;
+}
diff --git a/index.html b/index.html
index 9daeafb..ebdcbd2 100644
--- a/index.html
+++ b/index.html
@@ -1 +1,2 @@
+<link rel="stylesheet" href="style.css"/>

Partial stashes

你也可以选择只储藏一个文件、一组文件或文件中的单个改动。如果向git stash传递-p选项(或--patch),它将遍历工作副本中每个更改的“块”并询问您是否希望储藏它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git stash -p
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..d92368b
--- /dev/null
+++ b/style.css
@@ -0,0 +1,3 @@
+* {
+ text-decoration: blink;
+}
Stash this hunk [y,n,q,a,d,/,e,?]? y
diff --git a/index.html b/index.html
index 9daeafb..ebdcbd2 100644
--- a/index.html
+++ b/index.html
@@ -1 +1,2 @@
+<link rel="stylesheet" href="style.css"/>
Stash this hunk [y,n,q,a,d,/,e,?]? n

你可以点击? 查看所有的块命令。常用的有:

Command Description
/ search for a hunk by regex(通过正则表达式搜索)
? help
n don’t stash this hunk(不储藏这个修改块)
q quit (any hunks that have already been selected will be stashed) 退出
s split this hunk into smaller hunks(拆分成更小的块)
y stash this hunk(储藏这个修改块)

没有明确的 “中止 “命令,但点击 CTRL-C(SIGINT)可以中止储藏过程。


Creating a branch from your stash

如果分支上的更改与储藏库中的更改不同,则在弹出或应用存储时可能会遇到冲突。相反,您可以git stash branch创建一个新分支来将储藏的更改应用到新分支:

1
2
3
4
5
6
7
8
9
10
11
12
$ git stash branch add-stylesheet stash@{1}
Switched to a new branch 'add-stylesheet'
On branch add-stylesheet
Changes to be committed:

new file: style.css

Changes not staged for commit:

modified: index.html

Dropped refs/stash@{1} (32b3aa1d185dfe6d57b3c3cc3b32cbf3e380cc6a)

这会根据你创建储藏的提交检查出一个新的分支,然后将你储藏的更改添加到该分支中。

Cleaning up your stash

如果决定不再需要某个特定的储藏库,可以使用git stash drop删除它:

1
2
$ git stash drop stash@{1}
Dropped stash@{1} (17e2697fd8251df6163117cb3d58c1f62a5e7cdb)

或者,您也可以使用以下方法删除所有储藏库:

1
$ git stash clear

How git stash works

如果你只想知道如何使用 git stash,那就别读了。但如果你想知道 Git(和git stash)在底层是如何工作的,请继续往下读!

储藏库实际上是以提交对象的形式在版本库中编码的。.git/refs/stash 中的特殊 ref 指向最近创建的 stash,而之前创建的 stash 则由 stash ref 的 reflog 引用。这就是为什么你会用 stash@{n} 来引用 stash:你实际上是在引用 stash 引用 的第 n 个 reflog 条目。因为 stash 只是一个提交,所以可以用 git log 来检查它。

根据您存储的内容,单个git stash操作会创建两个或三个新提交。上图中的提交是:

  • stash@{0},一个新提交,用于存储运行 git stash 时工作副本中的跟踪文件
  • stash@{0}的第一个父级,也就是运行 git stash 时位于 HEAD 的已有提交
  • stash@{0}的第二个父级,一个新的提交代表您运行git stash时的索引
  • stash@{0}的第三个父级,一个新的提交,代表您运行git stash时工作副本中未跟踪的文件。该第三个父级仅在以下情况下创建:
    • 您的工作副本实际上包含了未跟踪文件;并且
    • 您在调用 git stash 时指定了 --include-untracked--all 选项。

git stash 如何将工作树和索引编码为提交:

  • stash之前,工作树可能包含对已跟踪文件、未跟踪文件和忽略文件的更改。其中一些更改也可能暂存于索引中。
  • 调用git stash将对跟踪文件的任何更改编码为 DAG 中的两项新提交:一项用于未暂存的更改,一项用于索引中暂存的更改。特殊refs/stash引用已更新以指向它们。
  • 使用 --include-untracked 选项还会将对未跟踪文件的任何修改编码为额外提交。
  • 使用 --all 选项会将对任何忽略文件的修改与对未跟踪文件的修改一起包含在同一提交中。

当你运行 git stash pop 时,上面提交的改动会被用来更新你的工作副本和索引,而 stash reflog 会被洗牌以移除pop的提交。请注意,弹出的提交不会被立即删除,但会成为未来垃圾回收的候选者。


.ignore

Git 将工作副本中的每个文件视为三种情况之一:

  1. tracked(已跟踪) – 之前已暂存或已提交的文件;

  2. untracked(未跟踪)–未暂存或未提交的文件;

  3. ignored(忽略)–明确告知 Git 忽略的文件。

忽略的文件通常是构建工件和机器生成的文件,这些文件可以从版本库源代码中导出,或者不应提交。常见的例子有:

  • 依赖缓存,如 /node_modules 或 /packages 的内容
  • 编译过的代码,如 .o、.pyc 和 .class 文件
  • 编译输出目录,如 /bin、/out 或 /target
  • 运行时生成的文件,如 .log、.lock 或 .tmp
  • 隐藏的系统文件,如 .DS_Store 或 Thumbs.db
  • 个人IDE配置文件,如 .idea/workspace.xml

被忽略的文件会被记录在一个名为 .gitignore 的特殊文件中,该文件位于版本库的根目录下。没有明确的 git 忽略命令:相反,当您有新文件需要忽略时,必须手动编辑并提交 .gitignore 文件。.gitignore 文件包含的模式会与版本库中的文件名进行匹配,以决定是否忽略这些文件。

Git ignore patterns

.gitignore 使用通配模式来匹配文件名。你可以使用各种符号来构建匹配模式:

模式 匹配示例 解释
**/logs logs/debug.log logs/monday/foo.bar build/logs/debug.log 您可以在模式前加上双星号,以匹配版本库中的任何目录。
**/logs/debug.log logs/debug.log build/logs/debug.log
但不是
logs/build/debug.log
您还可以使用双星号,根据文件名及其父目录的名称来匹配文件。
*.log debug.log
foo.log
.log
logs/debug.log
星号是通配符,可匹配 0 个或多个字符。
*.log
!important.log
debug.log
但不是
logs/debug.log
在模式前加上感叹号会否定该模式。如果文件与某个模式匹配,但也与文件后面定义的否定模式匹配,则不会被忽略。
/debug.log debug.log
但不是
logs/debug.log
在否定模式之后定义的模式将重新忽略之前否定的任何文件。
debug.log debug.log
logs/debug.log
前缀斜线只匹配版本库根目录下的文件。
debug?.log debug0.log
debugg.log
但不是
debug10.log
问号只能匹配一个字符。
debug[0-9].log debug0.log
debug1.log
但不是
debug10.log
方括号也可用于匹配指定范围内的单个字符。
debug[01].log debug0.log
debug1.log
但不是
debug2.log
debug01.log
方括号匹配指定字符集中的单个字符。
debug[!01].log debug2.log
但不是
debug0.log
debug1.log
debug01.log
感叹号可用于匹配指定字符集以外的任何字符。
debug[a-z].log debuga.log
debugb.log
但不是
debug1.log
范围可以是数字或字母。
logs logs
logs/debug.log
logs/latest/foo.bar
build/logs
build/logs/debug.log
如果不添加斜线,模式将同时匹配文件和该名称下的目录内容。在左边的匹配示例中,名为 logs 的目录和文件都会被忽略
logs/ logs/debug.log
logs/latest/foo.bar
build/logs/foo.bar
build/logs/latest/debug.log
添加斜线表示该模式是一个目录。版本库中与该名称匹配的任何目录的全部内容(包括其所有文件和子目录)都将被忽略
logs/ !logs/important.log logs/debug.log logs/important.log 等一下!在左边的示例中,logs/important.log 不应该被忽略吗?不对!由于 Git 中一个与性能相关的怪癖,你不能否定一个由于模式匹配目录而被忽略的文件
logs/**/debug.log logs/debug.log logs/monday/debug.log logs/monday/pm/debug.log 双星号可匹配零个或多个目录
logs/*day/debug.log logs/monday/debug.log logs/tuesday/debug.log
但不是
logs/latest/debug.log
目录名中也可以使用通配符。
logs/debug.log logs/debug.log
但不是
debug.log build/logs/debug.log
指定特定目录中文件的模式是相对于版本库根目录而言的。(如果你愿意,可以在前面加上斜线,但这并没有什么特别的作用)。

这些解释假定您的 .gitignore 文件按照惯例位于版本库的顶层目录中。如果您的存储库有多个 .gitignore 文件,只需在心里将“存储库根目录”替换为“包含 .gitignore 文件的目录”(并考虑统一它们,以确保您团队的理智)。

除了这些字符,你还可以使用 #.gitignore 文件中加入注释:

1
2
# ignore all logs
*.log

如果文件或目录中包含 .gitignore 模式字符,你可以使用 \ 来转义这些字符:

1
2
# ignore the file literally named foo[01].txt
foo\[01\].txt

Shared .gitignore files in your repository

Git 忽略规则通常在版本库根目录下的 .gitignore 文件中定义。不过,你也可以选择在版本库的不同目录中定义多个 .gitignore 文件。特定 .gitignore 文件中的每个模式都会相对于该文件的目录进行测试。不过最简单的方法还是在根目录下定义一个 .gitignore 文件。在签入 .gitignore 文件时,它会像版本库中的其他文件一样进行版本控制,并在推送时与队友共享。通常情况下,你应该只在 .gitignore 文件中包含有利于版本库其他用户的模式。

Personal Git ignore rules

你还可以在 .git/info/exclude 这个特殊文件中为特定版本库定义个人忽略模式。这些文件没有版本控制,也不随版本库一起发布,所以在这里加入可能只会对你有利的模式很合适。例如,如果你有自定义日志设置,或者有特殊的开发工具会在版本库的工作目录中生成文件,你可以考虑把它们添加到 .git/info/exclude 中,以防止它们被意外提交到版本库中。

Global Git ignore rules

此外,你还可以通过设置 Git core.excludesFile 属性,为本地系统中的所有仓库定义全局 Git 忽略模式。这个文件需要自己创建。如果你不确定把全局 .gitignore 文件放在哪里,那么您的主目录是一个不错的选择(并且可以方便以后查找)。创建文件后,您需要使用以下命令配置其位置git config

1
2
$ touch ~/.gitignore
$ git config --global core.excludesFile ~/.gitignore

由于不同的文件类型适用于不同的项目,因此在选择全局忽略的模式时应慎重。特殊的操作系统文件(如 .DS_Store 和 thumbs.db)或某些开发工具创建的临时文件是典型的全局忽略对象。

Ignoring a previously committed file

如果想忽略过去提交过的文件,需要先从版本库中删除该文件,然后为其添加 .gitignore 规则。在 git rm 中使用 --cached 选项意味着该文件将从版本库中删除,但会作为忽略文件保留在工作目录中。

1
2
3
4
5
6
$ echo debug.log >> .gitignore

$ git rm --cached debug.log
rm 'debug.log'

$ git commit -m "Start ignoring debug.log"

如果想从版本库和本地文件系统中删除文件,可以省略 --cached 选项。

Committing an ignored file

使用 git add-f(或 --force)选项,可以强制将忽略的文件提交到版本库:

1
2
3
4
5
6
$ cat .gitignore
*.log

$ git add -f debug.log

$ git commit -m "Force adding debug.log"

如果定义了通用匹配模式(如 *.log),但又想提交特定文件,可以考虑这样做。不过,更好的解决办法是定义一般规则的例外:

1
2
3
4
5
6
7
8
9
$ echo !debug.log >> .gitignore

$ cat .gitignore
*.log
!debug.log

$ git add debug.log

$ git commit -m "Adding debug.log"

对于您的其他开发伙伴来说,这种方法更明显,也更不容易混淆。

Stashing an ignored file

git stash是一项强大的 Git 功能,用于暂时储藏和还原本地更改,以便日后重新应用。如你所料,默认情况下,git stash 会忽略被忽略的文件,只存放 Git 追踪到的文件的改动。不过,你也可以使用 --all 选项调用 git stash,将被忽略和未被跟踪的文件的改动也储藏起来。

Debugging .gitignore files

如果你有复杂的 .gitignore模式,或者这些模式分布在多个 .gitignore 文件中,就很难找出某个文件被忽略的原因。你可以使用 git check-ignore 命令和 -v(或 --verbose)选项来确定是哪种模式导致了某个文件被忽略:

1
2
$ git check-ignore -v debug.log
.gitignore:3:*.log debug.log

输出显示:

1
<file containing the pattern> : <line number of the pattern> : <pattern>  <file name>

你可以向 git check-ignore 传递多个文件名,文件名本身甚至不必与版本库中存在的文件相对应。


检查存储库

git statue

git status命令会显示工作目录和暂存区域的状态。通过它,你可以看到哪些变更已被暂存,哪些未被暂存,以及哪些文件未被 Git 跟踪。状态输出不会显示任何有关已提交项目历史的信息。为此,你需要使用 git log

相关 git 命令

  • git tag
    • tag是指向 Git 历史记录中特定点的引用。 git tag通常用于捕获用于标记版本发布(即 v1.0.1)的历史记录点。
  • git blame
    • git blame 的高级功能是显示附加到文件中已提交行的作者元数据。这可以用来探索特定代码的历史,回答关于代码是什么、如何以及为什么被添加到版本库中的问题。
  • git log
    • git log 命令会显示已提交的快照。您可以用它列出项目历史、过滤历史记录并搜索特定变更。

Usage

1
git status

列出暂存、未暂存和未跟踪的文件。

git status 命令是一个相对简单的命令。它只需显示 git addgit commit的状态。状态信息还包括文件暂存/未暂存的相关说明。下面的示例输出显示了 git 状态调用的三个主要类别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# On branch main
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#modified: hello.py
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
#modified: main.py
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
#hello.pyc

Ignoring Files

未跟踪文件通常分为两类。它们要么是刚添加到项目中但尚未提交的文件,要么是编译过的二进制文件,如 .pyc、.obj、.exe 等。在 git 状态输出中包含前者肯定是有好处的,但后者会让人很难看出仓库里到底发生了什么。

因此,Git 可以让你完全忽略文件,方法是把路径放在一个叫做 .gitignore 的特殊文件中。任何你想忽略的文件都应该单独列一行,*符号可以用作通配符。例如,在项目根目录下的 .gitignore 文件中添加以下内容,就能阻止编译后的 Python 文件出现在 git status 中:

1
*.pyc

在提交更改之前,检查版本库的状态是个很好的做法,这样就不会不小心提交了一些你不想提交的内容。此示例显示了暂存和提交快照前后的版本库状态:

1
2
3
4
5
6
7
8
9
# Edit hello.py
git status
# hello.py is listed under "Changes not staged for commit"
git add hello.py
git status
# hello.py is listed under "Changes to be committed"
git commit
git status
# nothing to commit (working directory clean)

第一个状态输出将显示文件未暂存。git add 操作会反映在第二个 git 状态输出中,最后一个状态输出会告诉你没有要提交的内容–工作目录与最近的提交一致。有些 Git 命令(如 git merge)要求工作目录必须是干净的,以免意外覆盖改动。

git log

git log 命令会显示已提交的快照。通过它,您可以列出项目历史,对其进行过滤,并搜索特定的改动。git status 可以查看工作目录和暂存区域,而 git log 只能查看已提交的历史记录。

日志输出可通过多种方式自定义,从简单过滤提交到以完全由用户定义的格式显示。下面介绍一些最常见的 git 日志配置。

1
git log

使用默认格式显示整个提交历史。如果输出占用了一个屏幕以上,可以使用空格滚动,使用 q 退出。

1
git log -n <limit>

限制显示条数,例如,git log -n 3 只显示最近 3 次提交。

将每个提交压缩为一行。这有助于获得项目历史的高层概览。

1
git log --oneline
1
git log --stat

除了普通的 git log 信息外,还包括哪些文件被修改,以及每个文件被添加或删除的行数。

1
git log -p

显示代表每次提交的补丁。这将显示每次提交的完整差异,是项目历史最详细的视图。

1
git log --author="<pattern>"

搜索特定作者的提交。参数 <pattern> 可以是纯字符串或正则表达式。

1
git log --grep="<pattern>"

搜索特定commit 提交信息的提交。参数 <pattern> 可以是纯字符串或正则表达式。

1
git log <since>..<until>

只显示 <since><until> 之间的提交。两个参数都可以是commit ID、分支名称、HEAD 或任何其他类型的修订引用。

1
git log <file>

只显示包含指定文件的提交。这是查看特定文件历史记录的简便方法。

1
git log --graph --decorate --oneline

有几个有用的选项值得考虑。--graph 标志会在提交信息的左侧绘制基于文本的提交图表。--decorate(装饰)标记会添加分支名称或提交标签。--oneline(单线)会将提交信息显示在一行上,方便用户一目了然地浏览提交信息。

检查文件状态:

1
2
commit 3157ee3718e180a9476bf2e5cab8e3f1e78a73b7
Author: John Smith

大部分内容都很简单明了,但第一行需要解释一下。提交后的 40 个字符字符串是提交内容的 SHA-1 校验和。这样做有两个目的。首先,它可以确保提交的完整性–如果提交内容被破坏,提交将生成不同的校验和。其次,它可以作为提交的唯一 ID。

这个 ID 可以在 git log .. 等命令中用来指代特定的提交。例如,git log 3157e..5ab91 将显示 ID 为 3157e 和 5ab91 的提交之间的所有内容。除了校验和之外,分支名(分支模块中有讨论)和 HEAD 关键字也是引用单个提交的常用方法。HEAD 总是指当前提交,无论是分支还是特定提交。

~ 字符用于相对引用提交的父提交。例如,3157e~1 指的是 3157e 之前的提交,而 HEAD~3 则是当前提交的曾祖父。

前面提供了许多 git log 的示例,但请记住,多个选项可以合并为一条命令:

1
git log --author="John Smith" -p hello.py

这将显示约翰-史密斯对文件 hello.py 所做更改的完整差异。

..语法是比较分支的一个非常有用的工具。下一个示例将简要显示所有在some-feature中但不在main中的提交。

1
git log --oneline main..some-feature

git tag

本文将讨论 Git 标签概念和 git 标签命令。tag是指向 Git 历史中特定点的 ref。标签一般用于捕捉历史中的某一点,该点用于标记版本发布(如v1.0.1)。

标签就像一个不会改变的分支。与分支不同的是,标签在创建后就不会再有提交历史。有关分支的更多信息,请访问 git 分支页面。

本文将介绍不同类型的标签、如何创建标签、列出所有标签、删除标签、共享标签等内容。

创建标签

要创建新标签,请执行以下命令:

1
git tag <tagname>

<tagname> 替换为创建标签时版本仓库状态的语义标识符。常见的模式是使用版本号,如 git tag v1.4。Git 支持两种不同类型的标签:注释标签和轻量级标签。前面的例子创建了一个轻量级标签。轻量级标签和注释标签存储的元数据量不同。最佳做法是将注释标签视为公共标签,将轻量级标签视为私有标签。注释标签存储额外的元数据,例如:标签名称、电子邮件和日期。这对于公开发布来说是非常重要的数据。轻量级标签本质上是提交的 “书签”,它们只是一个名称和指向提交的指针,可用于创建指向相关提交的快速链接。

附加说明注释的标签

注释标签(annotated tags)作为存储在 Git 数据库中完整对象。它们存储了额外的元数据,如:标签名称、电子邮件和日期。与提交和提交信息类似,注释标签也有标签信息。此外,为了安全起见,注释标签可以使用 GNU Privacy Guard(GPG)进行签名和验证。建议使用 git tag 的最佳做法是,优先使用注释标签,而不是轻量级标签,这样就能获得所有相关的元数据。

1
git tag -a v1.4

执行该命令将创建一个标有 v1.4 的新注释标签。然后,该命令将打开配置的默认文本编辑器,提示进一步输入元数据。

1
git tag -a v1.4 -m "my version 1.4"

执行该命令的过程与之前的调用类似,不过这个版本的命令会传入 -m 选项和一条信息。这是一种类似于 git commit -m 的便捷方法,它会立即创建一个新标签,并放弃打开本地文本编辑器,转而保存通过 -m 选项传递的信息。

轻量级标签

1
git tag v1.4-lw

执行该命令将创建一个轻量级标签,标识为 v1.4-lw。创建轻量级标签时不使用 -a-s-m 选项。轻量级标签会创建一个新的标签校验和,并将其存储在项目 repo 的 .git/ 目录中。

列出所有标签

要列出 repo 中已存储的标记,请执行以下操作:

1
git tag

这将输出一个标签列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
v0.10.0
v0.10.0-rc1
v0.11.0
v0.11.0-rc1
v0.11.1
v0.11.2
v0.12.0
v0.12.0-rc1
v0.12.1
v0.12.2
v0.13.0
v0.13.0-rc1
v0.13.0-rc2

要细化标签列表,-l 选项可与通配符表达式一起使用:

1
2
3
4
5
6
7
8
9
10
11
$ git tag -l *-rc*
v0.10.0-rc1
v0.11.0-rc1
v0.12.0-rc1
v0.13.0-rc1
v0.13.0-rc2
v0.14.0-rc1
v0.9.0-rc1
v15.0.0-rc.1
v15.0.0-rc.2
v15.4.0-rc.3

上例中使用了 -l 选项和 -rc 通配符表达式,可返回所有标有 -rc 前缀(传统上用于识别候选发布版本)的标记列表。

标记旧提交

默认情况下,git tag将在HEAD引用的提交上创建一个标签。另外,也可以将 git tag 作为特定提交的引用。这将标记所传递的提交,而不是默认的 HEAD。要收集较早提交的列表,请执行 git log 命令。

1
2
3
4
5
$ git log --pretty=oneline
15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch 'feature'
a6b4c97498bd301d84096da251c98a07c7723e65 add update method for thing
0d52aaab4479697da7686c15f77a3d64d9165190 one more thing
6d52a271eda8725415634dd79daabbc4d9b6008e Merge branch 'experiment'

执行 git log 会输出提交列表。在本例中,我们将选取注释为 “Merge branch ‘feature’ “去创建新标签。我们需要引用提交的 SHA 哈希值传递给 Git:

1
git tag -a v1.2 15027957951b64cf874c3557a0f3547bd83b3ff6

执行上述git tag调用将将为SHA哈希值为’15027957951b64cf874c3557a0f3547bd83b3ff6’ 的提交创建一个新标签v1.2

重新标记/替换旧标签

如果试图创建一个与现有标签标识符相同的标签,Git 会抛出类似的错误:

1
fatal: tag 'v0.4' already exists

此外,如果您尝试用现有的标记标识符标记旧提交,Git 也会抛出同样的错误。

如果必须更新现有标签,则必须使用 -f FORCE 选项。

1
git tag -a -f v1.4 15027957951b64cf874c3557a0f3547bd83b3ff6

执行上述命令将把 15027957951b64cf874c3557a0f3547bd83b3ff6 提交映射到 v1.4 标签标识符。它将覆盖 v1.4 标签的任何现有内容。

共享:向远程仓库推送标签

共享标签与推送分支类似。默认情况下,git push 不会推送标签。标签必须明确传递给 git push

1
2
3
4
5
6
7
8
$ git push origin v1.4
Counting objects: 14, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (14/14), 2.05 KiB | 0 bytes/s, done.
Total 14 (delta 3), reused 0 (delta 0)
To git@bitbucket.com:atlasbro/gittagdocs.git
* [new tag] v1.4 -> v1.4

要同时推送多个标签,可在 git push 命令中加入 --tags 选项。当其他用户克隆或拉取 repo 时,他们就会收到新标签。

检查标签

您可以使用 git checkout 命令在标签上查看 repo 的状态。

1
git checkout v1.4

上述命令将签出 v1.4 标签。这将使 repo 处于分离HEAD的状态。这意味着任何更改都不会更新标签。它们将创建一个新的分离提交。这个新的分离提交不会成为任何分支的一部分,只能通过提交的 SHA 哈希值直接访问。因此,在分离 HEAD 状态下进行修改时,最好先创建一个新的分支。

删除标签

删除标签的操作很简单。向 git tag 传递 -d 选项和标签标识符,就能删除标识的标签。

1
2
3
4
5
6
7
8
$ git tag
v1
v2
v3
$ git tag -d v1
$ git tag
v2
v3

在本例中,执行git tag会显示一个标签列表,其中显示 v1v2v3,然后执行 git tag -d v1 会删除v1标签。


git blame

git blame 命令是一个多功能的故障诊断工具,有大量的使用选项。git blame 的高级功能是显示附加在文件的特定提交行上的作者元数据。这可以用来检查文件历史中的特定点,了解修改该行的最后一个作者是谁。git blame 还可以用来探索特定代码的历史,回答代码是什么、如何以及为什么被添加到版本库中的问题。

Git blame通常与 GUI 显示屏一起使用。在线 Git 托管网站(如 Bitbucket)提供的blame视图是 Git blame 的 UI 包装。这些视图在围绕拉取请求和提交的协作讨论中被引用。此外,大多数集成了 Git 的集成开发环境也有动态 blame 视图。

How it works

为了演示 git blame,我们需要一个有一定历史的版本库。我们将使用开源项目 git-blame-example。这个开源项目是一个简单的版本库,包含一个 README.md 文件,其中有一些来自不同作者的提交。git blame使用示例的第一步是git clone 该示例仓库。

1
git clone https://kevzettler@bitbucket.org/kevzettler/git-blame-example.git && cd git-blame-example

现在我们有了示例代码的副本,可以用git blame开始探索它了。使用git log可以查看示例仓库的状态。提交历史应该如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ git log
commit 548dabed82e4e5f3734c219d5a742b1c259926b2
Author: Juni Mukherjee <jmukherjee@atlassian.com>
Date: Thu Mar 1 19:55:15 2018 +0000

Another commit to help git blame track the who, the what, and the when

commit eb06faedb1fdd159d62e4438fc8dbe9c9fe0728b
Author: Juni Mukherjee <jmukherjee@atlassian.com>
Date: Thu Mar 1 19:53:23 2018 +0000

Creating the third commit, along with Kev and Albert, so that Kev can get git blame docs.

commit 990c2b6a84464fee153253dbf02e845a4db372bb
Merge: 82496ea 89feb84
Author: Albert So <aso@atlassian.com>
Date: Thu Mar 1 05:33:01 2018 +0000

Merged in albert-so/git-blame-example/albert-so/readmemd-edited-online-with-bitbucket-1519865641474 (pull request #2)

README.md edited online with Bitbucket

commit 89feb84d885fe33d1182f2112885c2a64a4206ec
Author: Albert So <aso@atlassian.com>
Date: Thu Mar 1 00:54:03 2018 +0000

README.md edited online with Bitbucket

git blame 只能对单个文件进行操作。任何有用的输出都需要文件路径。git blame 的默认执行方式只是输出命令帮助菜单。在本例中,我们将对 README.MD 文件进行操作。在 git 仓库的根目录中包含一个 README 文件作为项目的文档源是开源软件的常见做法。

1
git blame README.MD

执行上述命令后,我们将获得第一个blame输出示例。以下输出是 README 中全部责备输出的子集。此外,该输出是静态的,反映了本文撰写时软件包的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git blame README.md
82496ea3 (kevzettler 2018-02-28 13:37:02 -0800 1) # Git Blame example
82496ea3 (kevzettler 2018-02-28 13:37:02 -0800 2)
89feb84d (Albert So 2018-03-01 00:54:03 +0000 3) This repository is an example of a project with multiple contributors making commits.
82496ea3 (kevzettler 2018-02-28 13:37:02 -0800 4)
82496ea3 (kevzettler 2018-02-28 13:37:02 -0800 5) The repo use used elsewhere to demonstrate `git blame`
82496ea3 (kevzettler 2018-02-28 13:37:02 -0800 6)
89feb84d (Albert So 2018-03-01 00:54:03 +0000 7) Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod TEMPOR incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum
89feb84d (Albert So 2018-03-01 00:54:03 +0000 8)
eb06faed (Juni Mukherjee 2018-03-01 19:53:23 +0000 9) Annotates each line in the given file with information from the revision which last modified the line. Optionally, start annotating from the given revision.
eb06faed (Juni Mukherjee 2018-03-01 19:53:23 +0000 10)
548dabed (Juni Mukherjee 2018-03-01 19:55:15 +0000 11) Creating a line to support documentation needs for git blame.
548dabed (Juni Mukherjee 2018-03-01 19:55:15 +0000 12)
548dabed (Juni Mukherjee 2018-03-01 19:55:15 +0000 13) Also, it is important to have a few of these commits to clearly reflect the who, the what and the when. This will help Kev get good screenshots when he runs the git blame on this README.

这是 README.md 文件前 13 行的示例。为了更好地理解这些输出,让我们逐行分析。下表显示的是第 3 行的内容,表中的列表示该列的内容。

Id Author Timestamp Line Number Line Content
89feb84d Albert So 2018-03-01 00:54:03 +0000 3 This repository is an example of a project with multiple contributors making commits.

如果我们查看一下blame输出列表,就会发现一些问题。列表中有三位作者。除了项目维护者 Kev Zettler,还有 Albert So 和 Juni Mukherjee。作者通常是 git blame 输出中最有价值的部分。时间戳列也很有帮助。行内容列则显示了改动的内容。

常见选项

1
git blame -L 1,5 README.md

-L选项将把输出限制在所要求的行范围内。在这里,我们将输出限制在第 1 行至第 5 行。

1
git blame -e README.md

-e选项显示的是作者的电子邮件地址,而不是用户名。

1
git blame -w README.md

-w选项会忽略空白处的改动。如果前作者修改了文件的间距,将制表符换成了空格或添加了新行,那么很不幸,git blame 的输出就会因为显示了这些改动而变得模糊不清。

1
git blame -M README.md

-M选项可检测同一文件中被移动或复制的行。这将报告该行的原作者,而不是移动或复制该行的最后一位作者。

1
git blame -C README.md

-C选项可检测从其他文件中移动或复制的行。这将报告该行的原作者,而不是移动或复制该行的最后一位作者。

Git blame vs git log

虽然 git blame 会显示修改行最后修改的作者,但很多时候您还是想知道一行最初添加的时间。要做到这一点,使用 git blame 可能比较麻烦。这需要结合使用 -w、-C 和 -M 选项。使用 git log 命令会方便得多。

要列出添加或修改特定代码的所有原始提交,请执行带 -S 选项的 git log 命令。在 -S 选项后加上您要查找的代码。让我们以上面 README 输出中的一行为例。让我们以 README 输出第 12 行中的 “CSS3D and WebGL renderers. “为例。

1
2
3
4
$ git log -S"CSS3D and WebGL renderers." --pretty=format:'%h %an %ad %s'
e339d3c85 Mario Schuettel Tue Oct 13 16:51:06 2015 +0200 reverted README.md to original content
509c2cc35 Daniel Tue Sep 8 13:56:14 2015 +0200 Updated README
cb20237cc Mr.doob Mon Dec 31 00:22:36 2012 +0100 Removed DOMRenderer. Now with the CSS3DRenderer it has become irrelevant.

输出结果显示,README 中的内容被 3 位不同的作者添加或修改了 3 次。最初是由 Mr.doob 在 cb20237cc 提交中添加的。在本例中,git log 还预置了 –pretty-format 选项。该选项将 git log 的默认输出格式转换为与 git log 格式一致的格式。有关使用和配置选项的更多信息,请访问 git log 页面。

Summary

git blame 命令用于逐行检查文件内容,查看每一行的最后修改时间和作者。git blame 的输出格式可以通过各种命令行选项来改变。在线 Git 托管解决方案(如 Bitbucket)提供了责备视图,与使用命令行的 git blame 相比,它能提供更好的用户体验。git log 命令也有一些类似的 blame 功能,如需了解更多,请访问 git log 概述页面。


撤消提交和更改

在本节中,我们将讨论可用的 Git “撤销 “策略和命令。首先需要注意的是,Git 并没有像文字处理程序那样的传统 “撤销 “系统。避免将 Git 操作映射到任何传统的 “撤消 “思维中会有所裨益。此外,Git 有自己的 “撤消 “操作术语,在讨论中最好加以利用。这些术语包括重置(reset)、还原(revert)、签出(checkout)、清理(clean)等。

一个有趣的比喻是把 Git 看成一个时间轴管理工具。提交是项目历史时间轴上某个时间点或兴趣点的快照。此外,还可以通过使用分支来管理多条时间线。在 Git 中进行 “撤消 “时,通常是向后移动,或移动到另一条没有发生错误的时间线上。

本教程提供了处理项目历史版本所需的全部技能。首先,它将向你展示如何探索旧的提交,然后解释在项目历史中恢复公开提交与在本地机器上重置未发布的修改之间的区别。

查找丢失的内容:Reviewing old commits

任何版本控制系统背后的理念都是存储项目的 “安全 “副本,这样你就不必担心会不可挽回地破坏你的代码库。一旦建立了项目提交历史,就可以查看和重温历史中的任何提交。git log命令是查看 Git 仓库历史记录的最佳工具之一。在下面的例子中,我们使用git log获取了一个流行的开源图形库的最新提交列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
git log --oneline
e2f9a78fe Replaced FlyControls with OrbitControls
d35ce0178 Editor: Shortcuts panel Safari support.
9dbe8d0cf Editor: Sidebar.Controls to Sidebar.Settings.Shortcuts. Clean up.
05c5288fc Merge pull request #12612 from TyLindberg/editor-controls-panel
0d8b6e74b Merge pull request #12805 from harto/patch-1
23b20c22e Merge pull request #12801 from gam0022/improve-raymarching-example-v2
fe78029f1 Fix typo in documentation
7ce43c448 Merge pull request #12794 from WestLangley/dev-x
17452bb93 Merge pull request #12778 from OndrejSpanel/unitTestFixes
b5c1b5c70 Merge pull request #12799 from dhritzkiv/patch-21
1b48ff4d2 Updated builds.
88adbcdf6 WebVRManager: Clean up.
2720fbb08 Merge pull request #12803 from dmarcos/parentPoseObject
9ed629301 Check parent of poseObject instead of camera
219f3eb13 Update GLTFLoader.js
15f13bb3c Update GLTFLoader.js
6d9c22a3b Update uniforms only when onWindowResize
881b25b58 Update ProjectionMatrix on change aspect

每个提交(commit)都有一个唯一的SHA-1哈希值。这些 ID 用于浏览提交时间线和重访提交。默认情况下,git log 只显示当前选定分支的提交。您要找的提交完全有可能在另一个分支上。您可以执行 git log --branches=*,查看所有其他分支的所有提交。git branch 命令用于查看和访问其他分支。调用 git branch -a 命令将返回所有已知分支名称的列表。然后可以使用git log.

当找到要访问的历史节点的某个提交引用时,就可以使用git checkout命令访问该提交。Git 签出是将这些保存的快照 “加载 “到开发机器上的简单方法。在正常开发过程中,HEAD通常指向主分支或其他本地分支,但当你签出之前的提交时,HEAD不再指向分支,而是直接指向某个提交。这就是所谓的 “分离 HEAD “状态,可视化如下:

查看旧版本

此示例假设您已经开始开发一个疯狂的实验,但您不确定是否要保留它。为了帮助您做出决定,您需要在开始实验之前查看项目的状态。首先,您需要找到您想要查看的修订版本的 ID。

1
git log --oneline

假设您的项目历史记录如下所示:

1
2
3
4
5
b7119f2 Continue doing crazy things
872fa7e Try something crazy
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import

您可以使用 git checkout 查看 “对 hello.txt 做一些导入改动 “的提交,如下所示:

1
git checkout a1e8fb5

这将使你的工作目录与 a1e8fb5 提交时的状态完全一致。你可以查看文件、编译项目、运行测试,甚至编辑文件,而不必担心丢失项目的当前状态。在这里所做的一切都不会保存到版本库中。要继续开发,就必须回到项目的 “当前 “状态:

1
git checkout main

这假定您正在默认的主分支上进行开发。回到主分支后,您可以使用 git revertgit reset 撤销任何不希望的改动。

撤销已提交的快照

从技术上讲,”撤销 “提交有几种不同的策略。下面的示例假定我们的提交历史如下:

1
2
3
4
5
git log --oneline
872fa7e Try something crazy
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import

我们将重点撤销 872fa7e Try something crazy 提交。也许事情有点太疯狂了。

如何使用 git checkout 撤销提交

使用 git checkout 命令,我们可以签出上一个提交(a1e8fb5),使仓库处于疯狂提交之前的状态。签出特定提交将使版本库处于 “分离 HEAD “状态。这意味着你不再在任何分支上工作。在分离状态下,当你将分支改回已建立的分支时,你所做的任何新提交都将成为孤儿。孤儿提交会被 Git 的垃圾回收器删除。垃圾回收器会按照设定的时间间隔运行,并永久销毁已成为孤儿的提交。为了防止被垃圾回收器回收的提交,我们需要确保自己在一个分支上。

在分离的 HEAD 状态下,我们可以执行 git checkout -b new_branch_without_crazy_commit。这将创建一个名为 new_branch_without_crazy_commit 的新分支,并切换到该状态。现在,该 repo 已进入新的历史时间线,其中的 872fa7e 提交已不复存在。此时,我们可以继续在这个新分支上工作,而 872fa7e 提交已不复存在,并将其视为 “撤消”。不幸的是,如果你需要之前的分支,也许它是你的主分支,那么这种撤消策略就不合适了。让我们看看其他一些 “撤销 “策略。如需了解更多信息和示例,请参阅我们的 git check 深入讨论。

如何用 git revert 撤销公开提交

让我们回到最初的提交历史示例。包含 872fa7e 提交的历史。这一次,让我们试试 “撤销”(undo)。如果我们执行 git revert HEAD,Git 会创建一个与上次提交相反的新提交。这就在当前分支历史中添加了一个新的提交,现在看起来就像

1
2
3
4
5
6
git log --oneline
e2f9a78 Revert "Try something crazy"
872fa7e Try something crazy
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import

至此,我们在技术上再次 “撤销 “了 872fa7e 提交。虽然 872fa7e 仍存在于历史中,但新的 e2f9a78 提交是 872fa7e 变动的反转。与之前的签出策略不同,我们可以继续使用同一分支。这个解决方案的撤消效果令人满意。这是处理公共共享源的理想 “撤销 “方法。如果您需要保留精选且最少的 Git 历史记录,则此策略可能无法令人满意。

如何使用 git reset 撤消提交

git reset 是一个用途广泛、功能多样的命令。如果我们调用 git reset --hard a1e8fb5,提交历史将重置为指定的提交。现在用git log查看提交历史会如下所示

1
2
3
4
git log --oneline
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import

交历史中。此时,我们可以继续工作并创建新的提交,就好像这些 “疯狂 “的提交从未发生过一样。这种撤销修改的方法对历史记录的影响最为干净。reset 对于本地变更来说很好,但在使用共享远程版本库时却会增加复杂性。如果我们有一个推送了 872fa7e 提交的共享远程仓库,而我们试图用 git 推送一个我们已经重置了历史的分支,Git 会捕捉到这一点并抛出一个错误。Git 会认为推送的分支因为缺少提交而不是最新的。在这种情况下,git revert 应该是首选的撤销方法。

撤销最后一次提交

在上一节中,我们讨论了撤销提交的不同策略。这些策略同样适用于最近的提交。不过在某些情况下,你可能并不需要删除或重置最后一次提交。也许只是提交时间过早。在这种情况下,您可以修改最近的提交。一旦你在工作目录中做了更多改动,并用 git add 将它们暂存以备提交,你就可以执行 git commit --amend。这会让 Git 打开配置好的系统编辑器,让你修改上次的提交信息。新的改动将被添加到修改后的提交中。

撤销公开更改

当在一个拥有远程仓库的团队中工作时,在撤销更改时需要格外注意。Git reset 通常被视为一种 “本地 “撤销方法。在撤销私有分支的变更时,应使用重置。这样可以安全地将提交与其他开发者正在使用的分支隔离开来。如果在共享分支上执行重置,然后用 git push 远程推送该分支,就会出现问题。在这种情况下,Git 会阻止推送,抱怨被推送的分支与远程分支不同步,因为缺少提交。

撤销共享版本库的首选方法是 git revertrevertreset更安全,因为它不会从共享版本库历史中删除任何提交。还原会保留要撤销的提交,并创建一个新的提交来反转不想要的提交。这种方法对于共享远程协作更安全,因为远程开发人员可以拉取分支,然后接收新的还原提交,从而撤销不想要的提交。

Summary

我们介绍了许多在 Git 中撤销的高级策略。重要的是要记住,在 Git 项目中 “撤销 “的方法不止一种。本页讨论的大部分内容都涉及更深层次的话题,而这些话题在相关 Git 命令的具体页面中会有更详尽的解释。最常用的 “撤销 “工具是 git checkoutgit revertgit reset。需要记住的要点有:

  • 修改一旦提交,通常就是永久性的
  • 使用 git checkout 查看历史提交
  • git revert 是撤销共享版本库的公共修改的最佳工具
  • git reset 是撤销本地私有修改的最佳工具

除了主要的撤消命令,我们还学习了其他 Git 工具:用于查找丢失提交的 git log、用于撤消未提交改动的 git clean、用于修改暂存索引的 git add。

每个命令都有自己的深入文档。要进一步了解此处提到的某个命令,请访问相应链接。

git clean

在本节中,我们将重点详细讨论 git clean 命令。git clean 在某种程度上是一个 “撤销 “命令。git clean 可以说是其他命令(如 git resetgit checkout)的补充。这些命令针对的是之前添加到 Git 跟踪索引中的文件,而 git clean 命令针对的是未被跟踪的文件。未跟踪文件是指在您的 repo 工作目录中创建的、但尚未使用 git add 命令添加到版本库跟踪索引中的文件。

为了更好地展示跟踪文件和非跟踪文件的区别,请看下面的命令行示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ mkdir git_clean_test
$ cd git_clean_test/
$ git init .
Initialized empty Git repository in /Users/kev/code/git_clean_test/.git/
$ echo "tracked" > ./tracked_file
$ git add ./tracked_file
$ echo "untracked" > ./untracked_file
$ mkdir ./untracked_dir && touch ./untracked_dir/file
$ git status
On branch master

Initial commit

Changes to be committed: (use "git rm --cached <file>..." to unstage)

new file: tracked_file

Untracked files: (use "git add <file>..." to include in what will be committed) untracked_dir/ untracked_file

该示例在 git_clean_test 目录下创建了一个新的 Git 仓库。然后创建了一个跟踪文件(tracked_file),并将其添加到 Git 索引中,此外还创建了一个未跟踪文件(untracked_file)和一个未跟踪目录(untracked_dir)。然后,示例调用了 git status,输出显示了 Git 内部已跟踪和未跟踪的变更状态。有了这样的仓库状态,我们就可以执行 git clean 命令来演示其预期目的了。

1
$ git clean fatal: clean.requireForce defaults to true and neither -i, -n, nor -f given; refusing to clean

此时,执行默认的 git clean 命令可能会产生致命错误。上面的例子演示了这种情况。默认情况下,Git 的全局配置要求 git clean 必须通过 “force “选项才能启动。这是一个重要的安全机制。当最后执行时,git clean 是不可撤销的。完全执行后,git clean 会硬性删除文件系统,类似于执行命令行 rm 工具。运行前请确保您真的想删除未跟踪的文件。

常用选项和用法

鉴于前面对 git 清理默认行为和注意事项的解释,下面的内容将演示各种 git 清理用例及其操作所需的命令行选项。

1
-n

-n 选项将执行 git clean 的“试运行”。这会显示哪些文件将被移除,但不会真正移除。最好的做法是先执行一次git clean “试运行 “。我们可以在之前创建的演示版本中演示这个选项。

1
2
$ git clean -n
Would remove untracked_file

输出结果告诉我们,当执行 git clean 命令时,untracked_file 将被移除。注意这里的输出没有报告 untracked_dir。默认情况下,git clean 不会对目录进行递归操作。这是防止意外永久删除的另一种安全机制。

1
-f or --force

强制选项会启动实际删除当前目录中未跟踪文件的操作。除非 clean.requireForce 配置选项设置为 false,否则强制是必须的。这不会删除 .gitignore 指定的未跟踪文件夹或文件。现在让我们在示例仓库中执行一次实时的 git clean。

1
2
$ git clean -f 
Removing untracked_file

该命令将输出已删除的文件。这里可以看到 untracked_file 已被删除。此时执行 git status 或进行 ls 将显示 untracked_file 已被删除,并且无处可寻。默认情况下,git clean -f 会对当前目录下所有未被跟踪的文件执行操作。此外,还可以通过 -f 选项传递 < path > 值来删除特定文件。

1
2
git clean -f <path>
-d include directories

-d选项会告诉git clean您还想移除任何未跟踪的目录,默认情况下它会忽略这些目录。我们可以在之前的例子中添加 -d 选项:

1
2
3
4
$ git clean -dn
Would remove untracked_dir/
$ git clean -df
Removing untracked_dir/

在这里,我们使用 -dn 组合执行了一次 “试运行”,输出结果是 untracked_dir 将被移除。然后,我们执行了强制清理,得到的输出结果是 untracked_dir 已被删除。

1
-x force removal of ignored files

一种常见的软件发布模式是建立一个未提交到版本库跟踪索引的构建或发布目录。构建目录将包含从已提交源代码生成的短暂构建工程。该构建目录通常会添加到版本库的 .gitignore 文件中。清理该目录中的其他未跟踪文件也很方便。-x 选项会告诉 git clean 也包含任何被忽略的文件。与之前的 git clean 一样,在最终删除之前,最好先执行一次 “试运行”。-x 选项将作用于所有被忽略的文件,而不仅仅是项目构建时特定的文件。比如 ./.idea IDE 配置文件。

1
git clean -xf

-d 选项一样,-x 可以与其他选项一起传递和组合。本例演示了与 -f 的组合,可以移除当前目录中未跟踪的文件以及 Git 忽略的文件。

交互模式或 git clean 交互式

除了我们已经演示过的临时命令行执行外,git clean 还有一个 “交互式 “模式,可以通过 -i 选项启动。让我们重温一下本文导言中的示例仓库。在初始状态下,我们将启动一个交互式的清理会话。

1
2
3
4
5
6
$ git clean -di
Would remove the following items:
untracked_dir/ untracked_file
*** Commands ***
1: clean 2: filter by pattern 3: select by numbers 4: ask each 5: quit 6: help
What now>

我们使用 -d 选项启动了交互式会话,因此它也会作用于 untracked_dir。交互模式将显示 “What now> “提示,要求对未跟踪文件执行命令。命令本身很容易解释。我们将从命令 6:帮助开始,按随机顺序简要介绍每条命令。选择命令 6 将进一步解释其他命令:

1
2
3
4
5
6
7
8
What now> 6
clean - start cleaning
filter by pattern - exclude items from deletion
select by numbers - select items to be deleted by numbers
ask each - confirm each deletion (like "rm -i")
quit - stop cleaning
help - this screen
? - help for prompt selection
1
5: quit

直截了当,退出互动会话。

1
1: clean

将删除指定的项目。如果此时执行 1: cleanuntracked_dir/ untracked_file 将被删除

1
4: ask each

会遍历每个未跟踪的文件,并显示 “Y/N “提示是否删除。如下所示

1
2
3
4
5
*** Commands ***
1: clean 2: filter by pattern 3: select by numbers 4: ask each 5: quit 6: help
What now> 4
Remove untracked_dir/ [y/N]? N
Remove untracked_file [y/N]? N
1
2: filter by pattern

将显示额外提示,输入用于过滤未跟踪文件列表的信息。

1
2
3
4
5
6
7
8
Would remove the following items:
untracked_dir/ untracked_file
*** Commands ***
1: clean 2: filter by pattern 3: select by numbers 4: ask each 5: quit 6: help
What now> 2
untracked_dir/ untracked_file
Input ignore patterns>> *_file
untracked_dir/

在这里,我们输入 *_file 通配符模式,然后将未跟踪文件列表限制为 untracked_dir。

1
3: select by numbers

与命令 2 类似,命令 3 的作用也是完善未跟踪文件名列表。交互会话将提示输入与未跟踪文件名相对应的数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
Would remove the following items:
untracked_dir/ untracked_file
*** Commands ***
1: clean 2: filter by pattern 3: select by numbers 4: ask each 5: quit 6: help
What now> 3
1: untracked_dir/ 2: untracked_file
Select items to delete>> 2
1: untracked_dir/ * 2: untracked_file
Select items to delete>>
Would remove the following item:
untracked_file
*** Commands ***
1: clean 2: filter by pattern 3: select by numbers 4: ask each 5: quit 6: help

Summary

概括地说,git clean 是一种方便的方法,用于删除 repo 工作目录中未被跟踪的文件。未跟踪文件是指那些在仓库目录中,但尚未被 git add 添加到仓库索引中的文件。总的来说,git clean 的效果可以通过使用 git status 和操作系统原生的删除工具来实现。Git clean 可与 git reset 同时使用,以完全撤销版本库中的任何新增和提交。

git revert

git revert 命令可被视为 “撤销 “类型的命令,但它并不是传统的撤销操作。它不是从项目历史中删除提交,而是找出如何反转提交带来的改动,并用反转后的内容追加新的提交。这可以防止 Git 丢失历史记录,而这对于保持修订历史的完整性和可靠的协作非常重要。

当你想从项目历史中应用逆向提交时,就应该使用还原。例如,如果你在追踪一个 Bug 时发现它是由一次提交引入的,这就很有用了。你可以使用 git revert 自动完成所有这些工作,而不用手动去修复它,再提交一个新的快照。

How it works

git revert 命令用于撤销对版本库提交历史的修改。其他 “撤消 “命令,如 git checkout 和 git reset,会将 HEAD 和分支的 ref 指针移动到指定的提交。git revert 也需要指定的提交,但 git revert 不会将 ref 指针移动到该提交。

还原操作会使用指定的提交,反转该提交的改动,并创建一个新的 “还原提交”。然后 ref 指针就会更新,指向新的 revert 提交,使其成为分支的顶端。

为了演示,让我们使用下面的命令行示例创建一个 repo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ mkdir git_revert_test
$ cd git_revert_test/
$ git init .
Initialized empty Git repository in /git_revert_test/.git/
$ touch demo_file
$ git add demo_file
$ git commit -am"initial commit"
[main (root-commit) 299b15f] initial commit
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 demo_file
$ echo "initial content" >> demo_file
$ git commit -am"add new content to demo file"
[main 3602d88] add new content to demo file
n 1 file changed, 1 insertion(+)
$ echo "prepended line content" >> demo_file
$ git commit -am"prepend content to demo file"
[main 86bb32e] prepend content to demo file
1 file changed, 1 insertion(+)
$ git log --oneline
86bb32e prepend content to demo file
3602d88 add new content to demo file
299b15f initial commit

在这里,我们在名为 git_revert_test 的新建目录中初始化了一个 repo。我们对 repo 进行了 3 次提交,其中添加了一个文件 demo_file,并对其内容修改了两次。在仓库设置过程的最后,我们调用了 git log 来显示提交历史,总共显示了 3 次提交。这样,我们就可以启动 git revert 了。

1
2
$ git revert HEAD
[main b9cd081] Revert "prepend content to demo file" 1 file changed, 1 deletion(-)

git revert 希望传递一个提交引用,否则不会执行。这里我们传入了 HEAD。这将还原最新的提交。这与还原 3602d8815dbfa78cd37cd4d189552764b5e96c58 提交的行为相同。与合并类似,还原将创建一个新的提交,并打开配置的系统编辑器,提示输入新的提交信息。输入并保存提交信息后,Git 将恢复运行。现在我们可以用 git 日志查看 repo 的状态,发现在之前的日志中添加了新的提交:

1
$ git log --oneline 1061e79 Revert "prepend content to demo file" 86bb32e prepend content to demo file 3602d88 add new content to demo file 299b15f initial commit

请注意,第 3 次提交在还原后仍在项目历史中。git revert 并没有删除它,而是添加了一个新的提交来撤销它的改动。因此,第 2 次和第 4 次提交代表的是完全相同的代码库,而第 3 次提交仍保留在历史记录中,以备日后回溯。

常见选项

1
2
-e
--edit

这是默认选项,无需指定。该选项将打开配置的系统编辑器,并提示你在提交还原之前编辑提交信息。

1
--no-edit

这与 -e 选项相反。revert 不会打开编辑器。

1
2
-n
--no-commit

传递此选项将阻止 git revert 创建一个与目标提交相反的新提交。该选项不会创建新的提交,而是将反转的改动添加到暂存索引和工作目录中。这些是 Git 用来管理仓库状态的其他树。更多信息,请访问 git reset页面。

Resetting vs. reverting

重要的是要明白,git revert 只撤销一次提交,而不是通过删除所有后续提交来 “还原 “到项目之前的状态。在 Git 中,这被称为重置,而非还原。

resetting相比,Reverting有两个重要优势。首先,它不会改变项目历史,因此对于已经发布到共享仓库的提交来说,这是一个 “安全 “的操作。关于更改共享历史为何是危险的,请参阅 git reset 页面。

其次,git revert 可以针对历史中任意点的单个提交,而 git reset 只能从当前提交向后操作。举例来说,如果想用 git reset来撤消一次旧提交,就必须移除目标提交之后的所有提交,移除之后再重新提交所有后续提交。不用说,这不是一个优雅的撤消解决方案。关于 git revert 与其他 “撤消 “命令的区别,请参阅重置、签出和回退。

Summary

git revert 命令是一种向前移动的撤销操作,它提供了一种安全的撤销修改的方法。它不会删除或销毁提交历史中的提交,而是创建一个新的提交来反转指定的改动。就丢失工作而言,git revert 是比 git reset 更安全的选择。

git reset

git reset命令是一个复杂的多功能工具,用于撤销更改。它有三种主要的调用形式。这些形式与命令行参数--soft, --mixed, --hard相对应。这三个参数分别对应 Git 的三个内部状态管理机制:提交树(HEAD)、暂存索引和工作目录。

git reset & git 三棵树

要正确理解 git reset的用法,我们必须先了解 Git 的内部状态管理系统。这些机制有时被称为 Git 的 “三棵树”。三棵树 “可能名不副实,因为它们并非严格意义上的传统树形数据结构。不过,它们是基于节点和指针的数据结构,Git 用它来跟踪编辑的时间轴。演示这些机制的最佳方式是在版本库中创建一个变更集,并通过三棵树来跟踪它。

首先,我们将使用下面的命令创建一个新的版本库:

1
2
3
4
5
6
7
8
9
10
$ mkdir git_reset_test
$ cd git_reset_test/
$ git init .
Initialized empty Git repository in /git_reset_test/.git/
$ touch reset_lifecycle_file
$ git add reset_lifecycle_file
$ git commit -m"initial commit"
[main (root-commit) d386d86] initial commit
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 reset_lifecycle_file

上面的示例代码创建了一个新的 git 仓库,其中只有一个空文件 reset_lifecycle_file。此时,示例仓库中只有一次提交 (d386d86) 是在添加 reset_lifecycle_file 时提交的。

The working directory

我们要查看的第一个树是 “工作目录”。这棵树与本地文件系统同步,代表着对文件和目录内容所做的即时更改。

1
2
3
4
5
6
7
$ echo 'hello git reset' > reset_lifecycle_file
$ git status
On branch main
Changes not staged for commit:
(use "git add ..." to update what will be committed)
(use "git checkout -- ..." to discard changes in working directory)
modified: reset_lifecycle_file

在我们的演示版本库中,我们修改了 reset_lifecycle_file 并添加了一些内容。调用 git status 可以看到 Git 已经知道了文件的改动。这些改动目前是第一个树 “工作目录 “的一部分。git status 可以用来显示工作目录的改动。它们将以红色显示,并带有 “modified”前缀。

Staging index

接下来是 “暂存索引 “树。这棵树会跟踪工作目录中的变更,这些变更已通过 git add 进行了暂存,并将存储在下一次提交中。这棵树是一个复杂的内部缓存机制。git 通常会尽量向用户隐藏暂存索引的执行细节。

接下来是 “暂存索引 “树。这棵树会跟踪工作目录中的变更,这些变更已通过 git add 进行了推广,并将存储在下一次提交中。这棵树是一个复杂的内部缓存机制。Git 通常会尽量向用户隐藏暂存索引的执行细节。

要准确查看暂存索引的状态,我们必须使用一个鲜为人知的 Git 命令 git ls-filesgit ls-files 命令本质上是一个用于检查暂存索引树状态的调试工具。

1
2
git ls-files -s
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 reset_lifecycle_file

这里我们使用了 -s--stage 选项来执行 git ls-files。不使用 -s 选项时,git ls-files 的输出只是一个当前索引中的文件名和路径列表。-s选项会显示暂存索引中文件的附加元数据。这些元数据包括暂存内容的模式位、对象名称和阶段编号。在这里,我们感兴趣的是对象名称的第二个值(d7d77c1b04b5edd5acfc85de0b592449e5303770)。这是标准的 Git 对象 SHA-1 哈希值。它是文件内容的哈希值。提交历史存储了自己的 SHA 对象,用于识别提交和引用的指针,暂存索引也有自己的 SHA 对象,用于跟踪索引中文件的版本。

接下来,我们将把修改后的 reset_lifecycle_file 加到暂存索引中。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git add reset_lifecycle_file 


$ git status


On branch main Changes to be committed:


(use "git reset HEAD ..." to unstage)


modified: reset_lifecycle_file

在这里,我们调用了 git add reset_lifecycle_file,将文件添加到了暂存索引中。现在调用 git status,reset_lifecycle_file 在 “Changes to be committed”下显示为绿色。需要注意的是,git status 并不能真实反映暂存索引。git status 命令输出显示的是提交历史和暂存索引之间的变化。让我们来看看暂存索引的内容。

1
$ git ls-files -s 100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file

我们可以看到,reset_lifecycle_file 的对象 SHA 哈希值已从 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 更新为 d7d77c1b04b5edd5acfc85de0b592449e5303770。

Commit history

最后一棵树是提交历史。git commit 命令会将修改添加到永久快照中,该快照保存在 “提交历史 “中。该快照还包括提交时暂存索引的状态。

1
2
3
4
5
6
$ git commit -am"update content of reset_lifecycle_file"
[main dc67808] update content of reset_lifecycle_file
1 file changed, 1 insertion(+)
$ git status
On branch main
nothing to commit, working tree clean

在这里,我们创建了一个新提交,其信息为 “更新 resetlifecyclefile 的内容”。该变更集已被添加到提交历史中。此时调用 git status 会显示没有任何待处理的修改。执行 git log 会显示提交历史。现在,我们已经在三棵树中跟踪了这个变更集,可以开始使用 git reset了。

How it works

从表面上看,git reset 的行为与 git checkout 类似。git checkout 只对 HEAD ref 指针进行操作,而 git reset 则会移动 HEAD ref 指针和当前分支 ref 指针。为了更好地演示这一行为,请看下面的例子:

这个例子演示了主分支上的一系列提交。现在让我们执行并比较一下 git checkout bgit reset b

git checkout b

通过 git checkout,Main引用仍指向 d。HEAD 引用已被移动,现在指向提交 b。

git reset b

相比之下,git reset 会把 HEAD 和 Main 分支的引用都移到指定的提交上。

除了更新commit ref 指针,git 重置还会修改三棵树的状态。ref 指针的修改总是会发生,而且是对第三棵树,即提交树的更新。命令行参数–soft、–mixed 和–hard 会指示如何修改暂存索引树和工作目录树。

主要选项

git reset 的默认参数是 --mixed HEAD。这意味着执行 git reset 等同于执行 git reset --mixed HEAD。在这种形式中,HEAD 就是指定的提交。可以使用任何 Git SHA-1 commit哈希值来代替 HEAD。

‘–hard

这是最直接、最危险和最常用的选项。当使用 --hard 时,提交历史 ref 指针会更新到指定的提交。然后,暂存索引和工作目录会重置为与指定提交一致。任何之前对暂存索引和工作目录的待定更改都会被重置,以匹配提交树的状态。这意味着暂存索引和工作目录中的任何待处理工作都将丢失。

为了演示这一点,让我们继续之前建立的三棵树示例 repo。首先,让我们对 repo 做一些修改。在示例 repo 中执行以下命令:

1
2
3
$ echo 'new file content' > new_file
$ git add new_file
$ echo 'changed content' >> reset_lifecycle_file

这些命令创建了一个名为 new_file 的新文件,并将其添加到 repo 中。此外,reset_lifecycle_file 的内容也会被修改。有了这些改动,现在让我们用 git status 查看一下 repo 的状态。

1
2
3
4
5
6
7
8
9
10
11
12
$ git status
On branch main
Changes to be committed:
(use "git reset HEAD ..." to unstage)

new file: new_file

Changes not staged for commit:
(use "git add ..." to update what will be committed)
(use "git checkout -- ..." to discard changes in working directory)

modified: reset_lifecycle_file

在这里,我们调用了 git add reset_lifecycle_file,将文件添加到了暂存索引中。现在调用 git status,reset_lifecycle_file 在 “Changes to be committed “下显示为绿色。

需要注意的是,git status 并不能真实反映暂存索引。git status 命令输出显示的是提交历史和暂存索引之间的变化。让我们来看看暂存索引的内容。

1
2
3
$ git ls-files -s
100644 8e66654a5477b1bf4765946147c49509a431f963 0 new_file
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file

我们可以看到 new_file 已被添加到索引中。我们对 reset_lifecycle_file 进行了更新,但暂存索引的 SHA (d7d77c1b04b5edd5acfc85de0b592449e5303770) 仍保持不变。这是意料之中的事情,因为我们并没有使用 git add 将这些更改推广到暂存索引。这些改动存在于工作目录中。

现在,让我们执行 git reset –hard 并检查仓库的新状态。

1
2
3
4
5
6
7
$ git reset --hard
HEAD is now at dc67808 update content of reset_lifecycle_file
$ git status
On branch main
nothing to commit, working tree clean
$ git ls-files -s
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file

在这里,我们使用 --hard 选项执行了一次 “硬重置”。Git 的输出显示 HEAD 指向了最新提交 dc67808。

接下来,我们用 git status 检查 repo 的状态。Git 显示没有待处理的修改。我们还检查了暂存索引的状态,发现它已被重置到 new_file 被添加之前的位置。我们对 reset_lifecycle_file 所做的修改和 new_file 的添加都被销毁了。这种数据丢失是无法挽回的,因此必须引起注意。

‘–mixed

这是默认运行模式。ref 指针会被更新。暂存索引会重置为指定提交时的状态。任何从暂存索引中撤销的更改都会被移到工作目录中。让我们继续。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ echo 'new file content' > new_file
$ git add new_file
$ echo 'append content' >> reset_lifecycle_file
$ git add reset_lifecycle_file
$ git status
On branch main
Changes to be committed:
(use "git reset HEAD ..." to unstage)

new file: new_file
modified: reset_lifecycle_file


$ git ls-files -s
100644 8e66654a5477b1bf4765946147c49509a431f963 0 new_file
100644 7ab362db063f9e9426901092c00a3394b4bec53d 0 reset_lifecycle_file

在上面的示例中,我们对版本库做了一些修改。我们添加了一个 new_file,并修改了 reset_lifecycle_file 的内容。这些改动会通过 git add 应用到暂存索引中。有了这样的版本库,我们现在就要执行重置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git reset --mixed
$ git status
On branch main
Changes not staged for commit:
(use "git add ..." to update what will be committed)
(use "git checkout -- ..." to discard changes in working directory)

modified: reset_lifecycle_file

Untracked files:
(use "git add ..." to include in what will be committed)

new_file


no changes added to commit (use "git add" and/or "git commit -a")
$ git ls-files -s
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file

这里我们执行了一次 “mixed reset”。重申一遍,–mixed 是默认模式,与执行 git reset的效果相同。查看 git status 和 git ls-files 的输出,可以发现暂存索引已被重置到 reset_lifecycle_file 是索引中唯一文件的状态。reset_lifecycle_file 的 SHA 对象也被重置为之前的版本。

这里需要注意的是,git status 显示 reset_lifecycle_file 有修改,而且还有一个未跟踪的文件:new_file。这是显式--mixed行为。暂存索引已被重置,待处理的修改已被移入工作目录。

‘–soft

当传递–soft 参数时,将更新 ref 指针,重置到此为止。暂存索引和工作目录则保持不变。这种行为很难清晰演示。让我们继续演示 repo,为软重置做好准备。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ git add reset_lifecycle_file 


$ git ls-files -s


100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file


$ git status


On branch main


Changes to be committed:


(use "git reset HEAD ..." to unstage)


modified: reset_lifecycle_file


Untracked files:


(use "git add ..." to include in what will be committed)


new_file

在这里,我们再次使用 git add 将修改后的 reset_lifecycle_file 添加到暂存索引中。我们通过 git ls-files 的输出确认索引已经更新。git status 的输出现在显示绿色的 “Changes to be committed”。之前例子中的 new_file 作为一个未跟踪文件漂浮在工作目录中。让我们快速执行 rm new_file 删除该文件,因为在接下来的示例中我们不需要它了。

在版本库处于这种状态的情况下,我们现在执行软重置。

1
2
3
4
5
6
7
8
9
$ git reset --soft
$ git status
On branch main
Changes to be committed:
(use "git reset HEAD ..." to unstage)

modified: reset_lifecycle_file
$ git ls-files -s
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file

我们执行了 “soft reset”。使用 git status 和 git ls-files 检查 repo 的状态显示,没有任何变化。这是意料之中的。软重置只会重置提交历史。默认情况下,git 重置的目标提交是 HEAD。由于我们的提交历史已经在 HEAD 上了,而我们又隐式重置到了 HEAD,所以什么也没发生。

为了更好地理解和利用--soft,我们需要一个非 HEAD 的目标提交。我们有 reset_lifecycle_file 在暂存索引中等待。让我们创建一个新的提交。

1
$ git commit -m"prepend content to reset_lifecycle_file"

此时,我们的 repo 应该有三次提交。我们将回溯到第一次提交。为此,我们需要第一个提交的 ID。这可以通过查看 git 日志的输出找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git log
commit 62e793f6941c7e0d4ad9a1345a175fe8f45cb9df
Author: bitbucket
Date: Fri Dec 1 15:03:07 2017 -0800
prepend content to reset_lifecycle_file

commit dc67808a6da9f0dec51ed16d3d8823f28e1a72a
Author: bitbucket
Date: Fri Dec 1 10:21:57 2017 -0800

update content of reset_lifecycle_file

commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4

Author: bitbucket
Date: Thu Nov 30 16:50:39 2017 -0800

initial commit

请记住,每个系统的提交历史 ID 都是唯一的。这意味着本示例中的提交 ID 将与您在个人计算机上看到的不同。本例中我们感兴趣的提交 ID 是 780411da3b47117270c0e3a8d5dcfd11d28d04a4。这是对应于 “初始提交 “的 ID。找到这个 ID 后,我们将把它作为soft reset的目标。

在回到过去之前,让我们先检查一下 repo 的当前状态。

1
2
3
4
$ git status && git ls-files -s
On branch main
nothing to commit, working tree clean
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file

在这里,我们执行了 git status 和 git ls-files -s 的组合命令,结果显示软件仓库有待处理的更改,暂存索引中的 reset_lifecycle_file 版本为 67cc52710639e5da6b515416fd779d0741e3762e。有鉴于此,让我们执行一次软重置,回到第一次提交。

1
2
3
4
5
6
7
8
$git reset --soft 780411da3b47117270c0e3a8d5dcfd11d28d04a4
$ git status && git ls-files -s
On branch main
Changes to be committed:
(use "git reset HEAD ..." to unstage)

modified: reset_lifecycle_file
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file

上面的代码会执行一次 “软重置”,同时调用 git status 和 git ls-files 组合命令,输出版本库的状态。我们可以检查 repo 的状态输出,并注意到一些有趣的现象。首先,git status 显示 reset_lifecycle_file 有修改,并高亮显示这些修改是为下一次提交准备的。其次,git ls-files 输出显示暂存索引没有变化,并保留了之前的 SHA 67cc52710639e5da6b515416fd779d0741e3762e。

为了进一步弄清重置过程中发生了什么,让我们查看一下 git 日志:

1
$ git log commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4 Author: bitbucket  Date: Thu Nov 30 16:50:39 2017 -0800 initial commit

现在,日志输出显示提交历史中有一次提交。这有助于清楚地说明 –soft 所做的一切。与所有的 git 重置调用一样,重置的第一个动作是重置提交树。我们之前用–hard 和–mixed 重置的例子都是针对 HEAD 的,并没有及时移动提交树。在软重置过程中,这就是发生的全部情况。

这可能会让人困惑,为什么 git status 会显示有修改过的文件。--soft不会触及暂存索引,因此对暂存索引的更新会随着我们的提交历史记录而及时回溯。git ls-files -s 的输出显示 reset_lifecycle_file 的 SHA 没有变化,这就证实了这一点。需要提醒的是,git status 并不显示 “三棵树 “的状态,而是显示它们之间的差异。在本例中,它显示的是暂存索引比提交历史中的改动要早,就好像我们已经暂存了这些改动一样。

Resetting vs reverting

如果说 git revert 是撤销修改的 “安全 “方法,那么 git reset 就是危险的方法。使用 git reset 确实存在丢失工作的风险。Git reset永远不会删除提交,但提交可能会成为 “孤儿”,这意味着没有从 ref 访问它们的直接路径。这些 “孤儿 “提交通常可以用 git reflog 找到并恢复。Git 在运行内部垃圾回收器后,会永久删除任何 “孤儿 “提交。默认情况下,Git 会每隔 30 天运行一次垃圾回收器。提交历史是 “git 三棵树 “之一,其他两棵树(暂存索引和工作目录)并不像提交历史那样具有永久性。使用该工具时必须小心,因为它是唯一有可能丢失工作的 Git 命令之一。

Revert 是为了安全地撤销公开提交,而 git reset 则是为了撤销对暂存索引和工作目录的本地修改。由于目标不同,这两个命令的实现方式也不同:reset会完全删除更改集,而revert会保留原始更改集,并使用新提交来应用撤销。

Don’t reset public history

当快照被推送到公共仓库后,千万不要使用 git reset。发布提交后,你必须假定其他开发者也依赖于该提交。

删除其他团队成员还在继续开发的提交会给协作带来严重问题。当他们尝试与你的版本库同步时,看起来就像项目历史的一部分突然消失了。

一旦你在重置后添加了新提交,Git 就会认为你的本地历史与 origin/main 有所偏离,而同步版本库所需的合并提交很可能会让你的团队感到困惑和沮丧。

重点是,请确保 git reset <commit> 是用在出错的本地版本库上,而不是用在已发布的修改上。如果您需要修复公开提交,git revert 命令就是专门为此设计的。

Examples

1
git reset <file>

从暂存区域删除指定文件,但保留工作目录不变。这将取消文件的暂存,但不会覆盖任何更改。

1
git reset

重置暂存区域以匹配最新提交,但工作目录保持不变。这将在不覆盖任何更改的情况下解除所有文件的暂存,让你有机会从头开始重新构建暂存快照。

1
git reset --hard

重置暂存区域和工作目录,使其与最新提交一致。除了取消暂存更改,--hard 标志还会告诉 Git 覆盖工作目录中的所有更改。换句话说,这会抹去所有未提交的改动,所以在使用前,请确保你真的想扔掉本地的开发成果。

1
git reset  

将当前分支顶端向后移动到提交位置,重置暂存区域以匹配,但工作目录保持不变。自<提交>后所做的所有更改都将保留在工作目录中,这样你就可以使用更简洁、更原子化的快照来重新提交项目历史。

1
git reset --hard  

将当前分支顶端向后移动到<commit>,并重置暂存区域和工作目录,使其与之匹配。这不仅会抹去未提交的修改,还会抹去之后的所有提交。

取消文件暂存

在准备暂存快照时,经常会用到 git reset 命令。下一个示例假定您已经将两个名为 hello.py 和 main.py 的文件添加到了版本库中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Edit both hello.py and main.py

# Stage everything in the current directory
git add .

# Realize that the changes in hello.py and main.py
# should be committed in different snapshots

# Unstage main.py
git reset main.py

# Commit only hello.py
git commit -m "Make some changes to hello.py"

# Commit main.py in a separate snapshot
git add main.py
git commit -m "Edit main.py"

正如你所看到的,git reset 可以让你取消与下一次提交无关的改动,从而帮助你保持高度集中的提交。

删除本地提交

下一个示例展示了一个更高级的用例。它演示了当你在一个新实验上工作了一段时间,但在提交了几个快照后决定将其完全丢弃时会发生的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
# Create a new file called `foo.py` and add some code to it

# Commit it to the project history
git add foo.py
git commit -m "Start developing a crazy feature"

# Edit `foo.py` again and change some other tracked files, too

# Commit another snapshot
git commit -a -m "Continue my crazy feature"

# Decide to scrap the feature and remove the associated commits
git reset --hard HEAD~2

git reset HEAD~2 命令会将当前分支向后移动两次提交,从而有效地从项目历史中删除我们刚刚创建的两个快照。请记住,这种重置只能用于未发布的提交。如果你已经将提交推送到共享版本库,千万不要执行上述操作。

Summary

回顾一下,git reset是一个功能强大的命令,用于撤销对 Git 仓库状态的本地更改。git reset基于 “Git 的三棵树”。这三棵树分别是提交历史(HEAD)、暂存索引(Staging Index)和工作目录(Working Directory)。有三个命令行选项与这三棵树相对应。选项--soft--mixed--hard可以传递给 git reset

在本文中,我们还使用了其他几条 Git 命令来演示重置过程。关于这些命令的更多信息,请参见:git status、git log、git add、git checkout、git reflog 和 git revert。


git rm

刚开始使用 Git 时,一个常见问题是 “如何让 Git 不再跟踪某个(或某些)文件?git rm 命令用于从 Git 仓库中删除文件。它可以看作是 git add 命令的反义词。

git rm overview

git rm 命令可用于删除单个文件或文件集。git rm 的主要功能是从 Git 索引中移除跟踪的文件。此外,git rm 还能用于同时从暂存索引和工作目录中移除文件。没有只从工作目录删除文件的选项。被操作的文件必须与当前 HEAD 中的文件完全相同。如果文件的 HEAD 版本与暂存索引或工作树版本不一致,Git 会阻止删除操作。这种阻止是一种安全机制,以防止正在进行的修改被移除。

注意,git rm 不会移除分支。

使用方法

1
<file>…

指定要删除的目标文件。选项值可以是单个文件、以空格分隔的文件列表 file1 file2 file3 或通配符文件 (~./directory/*)。

1
2
-f
--force

-f 选项用于覆盖 Git 为确保 HEAD 中的文件与暂存索引和工作目录中的当前内容相匹配而进行的安全检查。

1
2
-n
--dry-run

“试运行”选项是一种安全措施,它将执行命令但不会实际删除文件。相反,它会输出它将删除哪些文件。

1
-r

-r选项是“递归”的简写。当以递归模式操作时,将删除目标目录以及该目录的所有内容。

1
--

分隔符选项用于明确区分文件名列表和传递给 git rm 的参数。 如果某些文件名的语法可能被误认为其他选项,这个选项就很有用。

1
--cached

缓存选项指定只在暂存索引中删除文件。工作目录文件将不会被删除。

1
--ignore-unmatch

这样,即使没有匹配的文件,命令也会以 0 sigterm 状态退出。这是一个 Unix 级别的状态代码。代码 0 表示命令调用成功。当使用 git rm 作为一个更大的 shell 脚本的一部分时,–ignore-unmatch 选项会很有用。

1
2
-q
--quiet

quiet 选项会隐藏 git rm 命令的输出。通常情况下,每删除一个文件,命令就会输出一行。

如何撤销 git rm

执行 git rm 并不是永久更新。该命令会更新暂存索引和工作目录。在创建新提交并将更改添加到提交历史之前,这些更改不会被持久化。这意味着这里的改动可以用普通的 Git 命令 “撤销”。

1
git reset HEAD

Reset会将当前暂存索引和工作目录还原回 HEAD 提交时的状态。这将撤销 git rm。

1
git checkout .

签出也会产生同样的效果,从 HEAD 恢复文件的最新版本。

如果执行了 git rm,但又创建了新的提交来坚持删除,可以使用 git reflog 来查找执行 git rm 之前的 ref。

该命令的 <file> 参数可以是精确路径、通配符文件 glob 模式或精确目录名。该命令只删除当前已提交到 Git 仓库的路径。

通配符文件全局匹配可跨目录。使用通配符 glob 时一定要谨慎。请看例子:directory/* 和 directory*。第一个示例将删除 directory/ 的所有子文件,而第二个示例将删除所有同级目录,如 directory1 directory2 directory_whatever,这可能是一个意想不到的结果。

git rm 命令只在当前分支上运行。移除事件只应用于工作目录和暂存索引树。在创建新提交之前,文件移除不会持续到版本库历史中。

示例

1
git rm Documentation/\*.txt

本例使用通配符文件删除Documentation目录及其任何子目录下的所有 *.txt 文件。

请注意,本例中的星号 * 是用斜线转义的;这是为了防止 shell 扩展通配符。通配符会扩展 Documentation/ 目录下的文件和子目录的路径名。

1
git rm -f git-*.sh

本例使用了强制选项,并以所有通配符 git-*.sh 文件为目标。强制选项明确地将目标文件从工作目录和暂存索引中删除。

如何删除文件系统中已不存在的文件

正如上文 “为什么使用 git rm 而不是 rm “一文所述,git rm 实际上是一个方便的命令,它结合了标准 shell rm 和 git add 命令,可以从工作目录中移除文件,并将移除的文件放到暂存索引中。如果仅使用标准 shell rm 命令就能移除多个文件,那么版本库就会变得非常累赘。

如果打算在下一次提交时记录所有明确移除的文件,git commit -a 会将所有移除事件添加到暂存索引中,为下一次提交做准备。

不过,如果想要持续移除用 shell rm 移除的文件,请使用下面的命令:

1
git diff --name-only --diff-filter=D -z | xargs -0 git rm --cached

该命令将生成工作目录中已删除文件的列表,并将该列表导入 git rm –cached 更新暂存索引。

Git rm summary

git rm 是一条在两个主要的 Git 内部状态管理树(工作目录和暂存索引)上运行的命令。它是一种方便的方法,结合了 shell 默认的 rm 命令和 git add 的效果。也就是说,它会首先从文件系统中移除目标文件,然后将移除事件添加到暂存索引中。该命令是 Git 中用于撤销更改的众多命令之一。


重写历史

Git commit --amend 和其他重写历史的方法

介绍

本教程将介绍各种重写和修改 Git 历史记录的方法。Git 使用几种不同的方法来记录更改。我们将讨论不同方法的优缺点,并举例说明如何使用它们。本教程将讨论覆盖已提交快照的一些最常见原因,并告诉你如何避免这样做的陷阱。

Git 的主要工作是确保你不会丢失已提交的变更。但它也能让你完全控制开发工作流程。这包括让你精确定义你的项目历史;然而,这也带来了丢失提交的可能性。Git 在提供历史记录重写命令时声明,使用这些命令可能会导致内容丢失。

Git 有多种存储历史和保存修改的机制。这些机制包括 Commit --amend, git rebasegit reflog。这些选项为你提供了强大的工作流程定制选项。在本教程结束时,你将熟悉一些命令,这些命令能让你重组 Git 提交,并能避免重写历史时常遇到的陷阱。

Changing the Last Commit: git commit --amend

git commit --amend 命令是修改最新提交的便捷方法。它能让你把已缓存的改动与上一次提交结合起来,而不是创建一个全新的提交。它也可以用来简单编辑上一次提交的信息,而不改变其快照。不过,修改并不只是修改最近的提交,而是完全替换它,这意味着修改后的提交将是一个拥有自己 ref 的新实体。对 Git 来说,它看起来就像一个全新的提交,在下图中用星号(*)表示。使用 git commit --amend 有几种常见情况。我们将在下面的章节中举例说明。

更改最近的 Git 提交信息

1
git commit --amend

比方说,你刚刚提交了文件,却在提交日志信息中犯了一个错误。在没有任何缓存的情况下运行这条命令,就能在不更改快照的情况下编辑前一次提交的信息。

在日常开发过程中,提交过早的情况时有发生。忘记暂存文件或提交信息格式错误都很容易发生。使用 --amend 标志就能很方便地修复这些小错误。

1
git commit --amend -m "an updated commit message"

添加 -m 选项后,就可以从命令行输入新信息,而无需打开编辑器。

更改已提交的文件

下面的示例演示了基于 Git 的开发中常见的一种情况。假设我们编辑了几个文件,想在一次快照中提交,但第一次提交时忘记添加其中一个文件。要修复这个错误,只需暂存另一个文件,然后使用 --amend 标志提交即可:

1
2
3
4
5
6
# Edit hello.py and main.py
git add hello.py
git commit
# Realize you forgot to add the changes from main.py
git add main.py
git commit --amend --no-edit

使用--no-edit(无编辑)标记,可以在不修改提交信息的情况下修改提交。由此产生的提交将取代不完整的提交,看起来就像我们在一个快照中提交了对 hello.py 和 main.py 的修改。

不要修改公共提交

修改后的提交实际上是全新的提交,之前的提交将不再出现在当前分支上。其后果与重置公共快照相同。避免修改其他开发人员的工作所基于的提交。这种情况会让其他开发人员感到困惑,而且恢复起来也很复杂。

Recap

git commit --amend可以让你使用最近的提交,并添加新的暂存修改。您可以从 Git 暂存区添加或移除修改,然后用 --amend 提交来应用。如果没有暂存的改动,--amend 仍会提示你修改上次提交的信息日志。在与其他团队成员共享的提交中使用 --amend 时要谨慎。修改与其他用户共享的提交可能需要混乱而冗长的合并冲突解决方案。

更改旧提交或多次提交

要修改旧提交或多个提交,可以使用 git rebase 将一系列提交合并为一个新的基本提交。在标准模式下,git rebase 可以让你真正改写历史–自动将当前工作分支中的提交应用到已通过的分支顶部。由于新的提交将替换旧的提交,因此不要在已公开推送的提交上使用 git rebase,否则会导致项目历史消失。

在这种或类似情况下,如果需要保留完整的项目历史,可以在 git rebase 中添加 -i 选项,以交互方式运行 rebase。这样,你就有机会在过程中修改单个提交,而不是移动所有提交。你可以在 git rebase 页面了解更多关于交互式rebase 和其他 rebase 命令的信息。

更改已提交的文件

rebase过程中,edite 命令将暂停该提交上的rebase回放,并允许您使用 git commit --amend 命令进行其他修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Stopped at 5d025d1... formatting
You can amend the commit now, with



git commit --amend



Once you are satisfied with your changes, run



git rebase --continue

Multiple messages

每个常规 Git 提交都会有一条日志信息,解释提交过程中发生了什么。这些信息对了解项目历史很有价值。在rebase过程中,你可以对提交执行一些命令来修改提交信息。

Squash commits for a clean history

通过 ssquash “命令,我们可以看到 rebase 的真正用途。Squash 允许你指定要将哪些提交合并到之前的提交中。这就是实现 “干净历史 “的方法。在 rebase 回放过程中,Git 会为每个提交执行指定的 rebase 命令

squash commits示例中,Git 会打开配置的文本编辑器,提示合并指定的提交信息。整个过程可视化如下:

请注意,使用 rebase 命令修改的提交与原始提交的 ID 不同。如果之前的提交已被改写,则标记为 pick 的提交会有一个新的 ID。

Recap

git rebase 使您能够修改历史记录,而交互式 rebase 使您可以在不留下“混乱”痕迹的情况下执行此操作。这创造了犯错和纠正错误以及完善工作的自由,同时仍然保持干净、线性的项目历史记录。

安全网:git reflog

引用日志,或称 “reflog”,是 Git 用于记录应用于分支提示和其他提交引用的更新的机制。reflog允许你回溯提交,即使它们没有被任何分支或标记引用。重写历史后,reflog 会包含分支旧状态的信息,允许你在必要时回到旧状态。

每次分支提示因任何原因更新时(通过切换分支、拉入新变更、重写历史或仅仅通过添加新提交),都会在 reflog 中添加一个新条目。在本节中,我们将对 git reflog 命令做一个高层次的了解,并探讨一些常见的用法。

使用方法

1
git reflog

这将显示本地版本库的 reflog。

1
git reflog --relative-date

这将显示指定相对日期信息(如 2 周前)的 reflog。

示例

为了理解 git reflog,我们先来看一个例子。

1
2
3
0a2e358 HEAD@{0}: reset: moving to HEAD~2
0254ea7 HEAD@{1}: checkout: moving from 2.2 to main
c10f740 HEAD@{2}: checkout: moving from main to 2.2

上面的引用日志显示了从主分支签出到 2.2 分支并返回。从那里开始,将硬重置到旧的提交。最新活动显示在顶部,标记为。HEAD@{0}

如果事实证明您不小心向后移动了,重写日志中将包含您意外删除 2 次提交之前的主提交(0254ea7)。

git reset --hard 0254ea7

通过 git reset,现在可以将 main 改回之前的提交状态。这为历史记录被意外更改提供了安全保障。

需要注意的是,reflog 只在变更已提交到本地版本库的情况下提供安全网,而且它只跟踪版本库分支tip的移动。此外,reflog 条目也有过期日期。reflog 条目的默认过期时间是 90 天。

如需了解更多信息,请参阅我们的 git reflog 页面。

Summary

在本文中,我们讨论了更改 git 历史记录和撤消 git 更改的几种方法。我们对 git rebase 过程进行了高层次的研究。一些关键要点是:

  • 有很多方法可以用 git 重写历史
  • 使用 git commit --amend 来修改最新的提交日志信息
  • 使用 git commit --amend 修改最新提交
  • 用于git rebase合并提交并修改分支的历史记录
  • git rebase -i与标准 git rebase 相比,对历史修改提供了更细粒度的控制。

了解有关我们在各个页面中介绍的命令的更多信息:


git rebase

本文档将深入讨论 git rebase 命令。Rebase 命令在建立仓库和重写历史页面中也有介绍。本页将更详细地介绍 git rebase 的配置和执行。这里将介绍常见的 Rebase 用例和陷阱。

Rebase 是两个 Git 工具之一,专门用于将一个分支的变更整合到另一个分支。另一个变更整合工具是 git merge。merge总是向前移动的变更记录。相反,rebase 具有强大的历史重写功能。Rebase 本身有两种主要模式: “手动 “和 “交互 “模式。下面我们将详细介绍不同的 Rebase 模式。

什么是 git rebase?

变基是将一系列提交移动或合并到一个新的基本提交的过程。变基在feature分支工作流程中最有用,也最容易可视化。一般流程可视化如下:

从内容的角度来看,变基是将分支的基础从一个提交更改为另一个提交,使分支看起来像是从另一个提交创建的。在内部,Git 通过创建新的提交并将它们应用到指定的base来实现这一点。理解这一点非常重要:尽管分支看起来相同,但它是由全新的提交组成的。

用法

变基的主要原因是维持线性项目历史。例如,考虑这样一种情况:在你开始创建feature分支后,main分支已经取得了进展。你想在feature分支中获得main分支的最新更新,但又想保持分支历史的完整性,这样看起来就像是你一直在最新的主分支上工作。这样做的好处是,以后可以将feature分支干净利落地合并回main分支。为什么要保持 “干净的历史”?在执行 Git 操作以调查回归的引入时,拥有清晰历史的好处就显而易见了。更真实的场景是

  1. 在主分支中发现一个错误。一个曾成功运行的功能现在被破坏了
  2. 开发人员检查所使用git log的主分支的历史记录,因为“干净的历史记录”使开发人员能够快速推断出项目的历史记录
  3. 开发人员无法通过git log确定 bug 是何时出现的,因此执行了 git bisect
  4. 由于 git 历史是干净的,git bisect 在查找回归时就有了一组精炼的提交进行比较。开发人员很快就能找到引入 bug 的提交,并采取相应措施

有关 git log 和 git bisect 的更多信息,请参阅它们各自的用法页面。

将feature分支整合到main分支有两种选择:直接merge或先变基后再合并。前者会导致 3 路合并和合并提交,而后者会导致快进合并和完美的线性历史记录。下图演示了变基到主分支如何促进快进合并。

变基是将上游更改集成到本地存储库的常用方法。用 Git 合并的方式拉入上游改动会导致每次想查看项目进展时都要提交一次多余的合并。另一方面,变基就像在说:”我想把我的改动建立在大家已经完成的基础上”。

不要rebase公共历史

正如我们之前在 “重写历史 “一文中讨论过的,一旦提交被推送到公共仓库,就不应该对其进行rebaserebase会用新提交替换旧提交,看起来就像项目历史的这一部分突然消失了。

Git rebase 标准版 vs git rebase 交互版

Git rebase interactive 是指 git rebase 接受 – i 参数。这代表 “交互式”。如果不带任何参数,命令将以标准模式运行。在这两种情况下,假设我们都创建了一个独立的特性分支。

1
2
3
4
# Create a feature branch based off of main 
git checkout -b feature_branch main
# Edit files
git commit -a -m "Adds new feature"

标准模式下的 git rebase 会自动将当前工作分支中的提交应用到已传递分支的头部。

1
git rebase <base>

它会自动将当前分支重定向到<base>上,<base>可以是任何类型的提交引用(例如 ID、分支名、标签或 HEAD 的相对引用)。

使用 -i 标志运行 git rebase 会开始一个交互式的rebase会话。交互式rebase不会盲目地将所有提交移到新的基础上,而是让你有机会在过程中修改单个提交。这样,你就可以通过删除、拆分和修改现有的一系列提交来清理历史。这就像是 Git commit –amend。

1
git rebase --interactive <base>

此操作会将当前分支重定向到<base>,但使用的是交互式重定向会话。这将打开一个编辑器,你可以为每个要重定向的提交输入命令(如下所述)。这些命令决定了如何将单个提交转移到新的base。你还可以重新排列提交列表,改变提交本身的顺序。一旦你为每个提交指定了命令,Git 就会开始回放应用 rebase 命令的提交。rebase编辑命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pick 2231360 some old commit
pick ee2adc2 Adds new feature


# Rebase 2cf755d..ee2adc2 onto 2cf755d (9 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit

其他 rebase 命令

正如改写历史页面所详述的,rebase 可用于修改旧提交、多次提交、已提交文件和多条信息。虽然这些都是最常见的应用,但 git rebase 还有额外的命令选项,在更复杂的应用中也很有用。

  • git rebase -- d 表示在回放过程中,该提交将从最终合并的提交块中丢弃。
  • git rebase -- p 表示保持提交原样。它不会修改提交信息或内容,在分支历史中仍是一个单独的提交。
  • git rebase -- x 在播放期间,在每个标记的提交上执行命令行 shell 脚本。一个有用的示例是在特定提交上运行代码库的测试套件,这可能有助于识别变基期间的回归。

Recap

交互式变基使您可以完全控制项目历史记录。这为开发人员提供了很大的自由,因为它允许他们在专注于编写代码时提交“混乱”的历史记录,然后在事后返回并清理它。

大多数开发人员都喜欢在将特性分支合并到主代码库之前,使用交互式rebase来完善它。这让他们有机会压制无关紧要的提交、删除过时的提交,并在提交到 “正式 “项目历史之前确保其他一切正常。在其他人看来,整个功能就是在一系列精心策划的提交中开发出来的。

交互式rebase的真正威力可以从生成的主分支的历史中看出。对于其他人来说,您似乎是一位出色的开发人员,第一次就以完美的提交实现了新功能。这就是交互式变基可以保持项目历史清晰且有意义的方式。

配置选项

使用 git config 可以设置一些 rebase 属性。这些选项将改变 git rebase 输出的外观和感觉。

  • rebase.stat: 一个布尔值,默认设置为 false。该选项可切换显示可视化 diffstat 内容,显示自上次变基以来发生的更改
  • rebase.autoSquash:布尔值,用于切换 –autosquash 行为
  • rebase.missingCommitsCheck:可设置为多个值,从而改变缺失提交时的回溯行为。
warn 在交互模式下打印警告输出,警告已删除的提交
error 停止重置并打印已删除的提交警告信息
ignore 默认设置为忽略任何缺失提交警告
  • rebase.instructionFormat:将用于格式化交互式变基显示的格式字符串

高级变基应用

命令行参数 –onto 可以传递给 git rebase。在 git rebase –onto 模式下,命令会扩展为

1
git rebase --onto <newbase> <oldbase>

--onto命令启用更强大的形式或变基,允许传递特定的引用作为变基的提示。假设我们有一个示例存储库,其分支如下:

1
2
3
4
5
o---o---o---o---o  main
\
o---o---o---o---o featureA
\
o---o---o featureB

featureB 基于 featureA,但是,我们意识到 featureB 不依赖于 featureA 的任何更改,并且可以从 main 中分支出来。

1
git rebase --onto main featureA featureB

featureA 是<oldbase>,main 是<newbase>,featureB 是<newbase>的 HEAD 所指向的引用。结果如下

1
2
3
4
5
6
                  o---o---o  featureB
/
o---o---o---o---o main
\
o---o---o---o---o featureA

了解rebase的危险

使用 Git Rebase 时需要考虑的一个警告是,在 rebase 工作流程中,合并冲突可能会变得更加频繁。如果您有一个长期存在的分支偏离了主分支,就会发生这种情况。最终你会想要针对 main 进行 rebase,那时它可能包含许多新的提交,你的分支更改可能会与这些提交发生冲突。这一点很容易解决,只要频繁地针对主分支rebase,并进行更频繁的提交即可。在处理冲突时,可以向 git rebase 传递 –continue 和 –abort 命令行参数,以推进或重置rebase。

更严重的变基警告是交互式历史重写造成的提交丢失。在交互模式下运行 rebase 并执行 squash 或 drop 等子命令,会从分支的即时日志中删除提交。乍一看,这些提交好像永远消失了。使用 git reflog 可以恢复这些提交,并撤销整个rebase。有关使用 git reflog 查找丢失提交的更多信息,请访问我们的 Git reflog 文档页面。

Git Rebase 本身并没有严重危险。当执行交互式变基重写历史并强制将结果推送到由其他用户共享的远程分支时,真正的危险情况就会出现。这是一种应该避免的模式,因为它能够在其他远程用户拉取时覆盖他们的工作。

从上游变基恢复

如果有其他用户执行了rebase并强制推送到您正在提交的分支,那么 git pull 就会用强制推送的提示覆盖您基于之前分支的任何提交。幸运的是,使用 git reflog 可以获得远程分支的 reflog。在远程分支的 reflog 中,你可以找到该分支被重置前的 ref。然后,你就可以使用 –onto 选项,根据该远程 ref 重定向你的分支,如上文 “高级变基应用 “部分所述。

Summary

在本文中,我们介绍了git rebase用法。我们讨论了基本和高级用例以及更高级的示例。一些关键讨论点是:

  • git rebase 标准模式与交互模式
  • git rebase 配置选项
  • git rebase –onto
  • git rebase 丢失提交

git reflog

本页将详细讨论 git reflog 命令。Git 使用一种称为引用日志或 “reflogs “的机制来跟踪分支顶端的更新。许多 Git 命令都接受一个参数来指定引用或 “ref”,也就是指向某个提交的指针。常见的例子包括

  • git checkout
  • git reset
  • git merge

Reflogs会记录本地仓库中 Git 分支的更新时间。除了分支提示 reflog,Git 储藏库也有一个特殊的 reflog。reflog存储在本地存储库 .git目录下的目录中。git reflog 目录位于 .git/logs/refs/heads/.、.git/logs/HEAD 和 .git/logs/refs/stash(如果在 repo 上使用了 git stash)。

我们在重写历史页面上对 git reflog 进行了深入讨论。本文档将介绍:git reflog 的扩展配置选项、git reflog 的常见用例和误区、如何使用 git reflog 撤销修改,以及更多。

基本用法

最基本的 Reflog 用例是调用:

1
git reflog

这实质上是一条捷径,相当于:

1
git reflog show HEAD

这将输出 HEAD reflog。你应该看到类似的输出:

1
2
3
4
5
6
7
8
eff544f HEAD@{0}: commit: migrate existing content
bf871fd HEAD@{1}: commit: Add Git Reflog outline
9a4491f HEAD@{2}: checkout: moving from main to git_reflog
9a4491f HEAD@{3}: checkout: moving from Git_Config to main
39b159a HEAD@{4}: commit: expand on git context
9b3aa71 HEAD@{5}: commit: more color clarification
f34388b HEAD@{6}: commit: expand on color support
9962aed HEAD@{7}: commit: a git editor -> the Git editor

Reflog参考

默认情况下,git reflog 会输出 HEAD ref 的 reflog。HEAD 是对当前活动分支的符号引用。引用日志也可用于其他引用。访问 git ref 的语法是 name@{qualifier}。访问 git ref 的语法是name@{qualifier}。除了HEADrefs 之外,还可以引用其他分支、标签、远程和 Git 储藏库。

您可以通过执行以下命令来获取所有引用的完整引用日志:

1
git reflog show --all 

要查看特定分支的 reflog,可将该分支名称传给 git reflog show。

Bitbucket 会显示 “创建新版本库 “页面。花点时间看看对话框的内容。除了版本库类型,你在这个页面上输入的所有内容都可以在以后修改。

1
2
3
4
5
6
git reflog show otherbranch



9a4491f otherbranch@{0}: commit: seperate articles into branch PRs
35aee4a otherbranch{1}: commit (initial): initial commit add git-init and setting-up-a-repo docs

执行此示例将显示 otherbranch 分支的 reflog。下面的示例假定您之前已使用 git stash 命令隐藏了一些改动。

1
2
3
git reflog stash

0d44de3 stash@{0}: WIP on git_reflog: c492574 flesh out intro

这将输出 Git 存储的引用日志。返回的 ref 指针可以传递给其他 Git 命令:

1
git diff stash@{0} otherbranch@{0}

执行该示例代码后,将显示 Git diff 输出,比较 stash@{0} 和 otherbranch@{0} 的改动。

每个 reflog 条目都附有一个时间戳。这些时间戳可用作 Git ref 指针语法的限定符标记。这样就能按时间过滤 Git reflog。以下是一些可用的时间限定符示例:

  • 1.minute.ago
  • 1.hour.ago
  • 1.day.ago
  • yesterday
  • 1.week.ago
  • 1.month.ago
  • 1.year.ago
  • 2011-05-17.09:00:00

时间限定词可以组合(例如1.day.2.hours.ago),此外还接受复数形式(例如5.minutes.ago)。

时间限定符可以传递给其他 git 命令。

1
git diff main@{0} main@{1.day.ago} 

此示例将当前的主分支与 1 天前的主分支进行比较。如果你想知道某段时间内发生的变化,这个示例非常有用。

子命令和配置选项

git reflog 接受一些附加参数,这些参数被视为子命令。

Show - git reflog show

show 默认为隐式传递。例如命令:

1
git reflog main@{0} 

等同于命令:

1
git reflog show main@{0} 

此外,git reflog show 是 git log -g –abbrev-commit –pretty=oneline 的别名。执行 git reflog show 会显示所传递的引用日志。

Expire - git reflog expire

expire子命令可清除旧的或无法访问的 reflog 条目。expire子命令有可能导致数据丢失。最终用户通常不会使用该子命令,但 git 内部会使用它。给 git reflog expire 传递 -n 或 –dry-run 选项会执行一次 “试运行”,输出哪些 reflog 条目被标记为要剪枝,但实际上不会剪枝。

默认情况下,reflog 的过期日期设置为 90 天。默认情况下,reflog 的过期日期设置为 90 天。过期时间可以通过向 git reflog expire 传递命令行参数 –expire=time 或设置 git 配置名 gc.reflogExpire 的值来指定。

Delete - git reflog delete

Delete子命令不言自明,它将删除传递的 reflog 条目。与expire一样,delete有可能丢失数据,终端用户通常不会调用。

恢复丢失的提交

Git 不会丢失任何东西,即使是在执行历史重写操作(如rebase或修改提交)时也是如此。在下一个例子中,假设我们对仓库做了一些新的改动。我们的 git log --pretty=oneline 看起来如下:

1
2
3
4
5
6
338fbcb41de10f7f2e54095f5649426cb4bf2458 extended content
1e63ceab309da94256db8fb1f35b1678fb74abd4 bunch of content
c49257493a95185997c87e0bc3a9481715270086 flesh out intro
eff544f986d270d7f97c77618314a06f024c7916 migrate existing content
bf871fd762d8ef2e146d7f0226e81a92f91975ad Add Git Reflog outline
35aee4a4404c42128bee8468a9517418ed0eb3dc initial commit add git-init and setting-up-a-repo docs

然后,我们提交这些更改并执行以下操作:

1
2
#make changes to HEAD
git commit -am "some WIP changes"

增加了新的提交。日志现在看起来像:

1
2
3
4
5
6
7
37656e19d4e4f1a9b419f57850c8f1974f871b07 some WIP changes
338fbcb41de10f7f2e54095f5649426cb4bf2458 extended content
1e63ceab309da94256db8fb1f35b1678fb74abd4 bunch of content
c49257493a95185997c87e0bc3a9481715270086 flesh out intro
eff544f986d270d7f97c77618314a06f024c7916 migrate existing content
bf871fd762d8ef2e146d7f0226e81a92f91975ad Add Git Reflog outline
35aee4a4404c42128bee8468a9517418ed0eb3dc initial commit add git-init and setting-up-a-repo docs

请在命令行输入以下内容,如果出现此错误

1
2
3
4
5
6
7
8
9
$ git clone

https://emmap1@bitbucket.org/emmap1/bitbucketstationlocations.git

Cloning into 'bitbucketspacestation'...

fatal: could not read

Password for 'https://emmap1@bitbucket.org': No such file or directory

此时,我们通过执行以下命令对主分支执行交互式变基:

1
git rebase -i origin/main

在变基过程中,我们使用 rebase s子命令标记压缩的提交。我们会将一些提交压入最新的 “some WIP changes “提交中。

因为我们删除了提交,所以现在的 git log输出看起来像:

1
2
40dhsoi37656e19d4e4f1a9b419f57850ch87dah987698hs some WIP changes
35aee4a4404c42128bee8468a9517418ed0eb3dc initial commit add git-init and setting-up-a-repo docs

如果我们检查一下此时的 git 日志,就会发现已经没有被标记为压缩(squashing)的提交了。如果我们想对其中一个被压缩的提交进行操作呢?也许从历史中删除它的改动?这就是利用 reflog 的好机会。

1
2
3
4
git reflog
37656e1 HEAD@{0}: rebase -i (finish): returning to refs/heads/git_reflog
37656e1 HEAD@{1}: rebase -i (start): checkout origin/main
37656e1 HEAD@{2}: commit: some WIP changes

我们可以看到有关于rebase开始和结束的引用日志条目,在这些条目之前是我们的”some WIP changes”提交。我们可以将 reflog ref 传递给 git reset,然后reset到rebase之前的提交。

意图

责任链模式(Chain of Responsibility)使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。

将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

与许多其他行为设计模式一样, 责任链会将特定行为转换为被称作处理者的独立对象。 在上述示例中, 每个检查步骤都可被抽取为仅有单个方法的类, 并执行检查操作。 请求及其数据则会被作为参数传递给该方法。

模式建议你将这些处理者连成一条链。 链上的每个处理者都有一个成员变量来保存对于下一处理者的引用。 除了处理请求外, 处理者还负责沿着链传递请求。 请求会在链上移动, 直至所有处理者都有机会对其进行处理。

最重要的是: 处理者可以决定不再沿着链传递请求, 这可高效地取消所有后续处理步骤。

责任链模式结构

现实世界的例子

兽人国王向他的军队发出响亮的命令。最接近反应的是兽人指挥官OrcCommander,然后是兽人军官OrcOfficer,然后是兽人士兵OrcSoldier。指挥官、军官和士兵形成责任链。

用上面的兽人来翻译我们的例子。首先,我们有这样的Request类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Request {

/**
* The type of this request, used by each item in the chain to see if they should or can handle
* this particular request.
*/
private final RequestType requestType;

/**
* The name of the request.
*/
private final String name;

/**
* Indicates if the request is handled or not. A request can only switch state from unhandled to
* handled, there's no way to 'unhandle' a request.
*/
private boolean handled;

public Request(RequestType requestType, String name) {
this.requestType = Objects.requireNonNull(requestType);
this.name = Objects.requireNonNull(name);
}

public RequestType getRequestType() {
return requestType;
}

public String getName() {
return name;
}

/**
* Mark the request as handled.
*/
public void markHandled() {
this.handled = true;
}

public boolean isHandled() {
return this.handled;
}

@Override
public String toString() {
return name;
}

}

接下来,是我们请求处理程序的层次结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public interface RequestHandler {

void setNextHandler(RequestHandler handler);

void handle(Request req);

String name();
}

public class OrcCommander implements RequestHandler {

private RequestHandler nextHandler;

@Override
public void setNextHandler(RequestHandler handler) {
this.nextHandler = handler;
}

@Override
public void handle(Request req) {
System.out.println(name() + " handle the request " + req);
this.nextHandler.handle(req);
}

@Override
public String name() {
return "Orc commander";
}
}

public class OrcOfficer implements RequestHandler {

private RequestHandler nextHandler;

@Override
public void setNextHandler(RequestHandler handler) {
this.nextHandler = handler;
}

@Override
public void handle(Request req) {
System.out.println(name() + " handle the request " + req);
this.nextHandler.handle(req);
}

@Override
public String name() {
return "Orc Officer";
}
}

public class OrcSoldier implements RequestHandler {

private RequestHandler nextHandler;

@Override
public void setNextHandler(RequestHandler handler) {
this.nextHandler = handler;
}

@Override
public void handle(Request req) {
System.out.println(name() + " handle the request " + req);
req.markHandled();
}

@Override
public String name() {
return "Orc Soldier";
}
}

兽人国王下达命令并形成请求链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class OrcKing {

private List<RequestHandler> handlers;

public OrcKing() {
buildRequestChain();
}

private void buildRequestChain() {
handlers = Arrays.asList(new OrcCommander(), new OrcOfficer(), new OrcSoldier());
for (int i = 0; i < handlers.size() - 1; i++) {
handlers.get(i).setNextHandler(handlers.get(i +1));
}
}

/**
* Handle request by the chain.
*/
public void makeRequest(Request req) {
handlers.get(0).handle(req);
}

}

测试:

1
2
3
4
5
6
7
8
public static void main(String[] args) {

Request request = new Request(RequestType.DEFEND_CASTLE, "defend castle");
OrcKing orcKing = new OrcKing();
orcKing.makeRequest(request);

System.out.println(request.isHandled());
}

程序输出:

1
2
3
4
Orc commander handle the request defend castle
Orc Officer handle the request defend castle
Orc Soldier handle the request defend castle
true

优缺点

Responsibility链有下列优点和缺点:

  • 降低耦合度

    该模式使得一个对象无需知道是其他哪一个对象处理其请求。对象仅需知道该请求会被“正确”地处理。

    接收者和发送者都没有对方的明确的信息,且链中的对象不需知道链的结构。

    结果是,职责链可简化对象的相互连接。它们仅需保持一个指向其后继者的引用,而不需保持它所有的候选接受者的引用。

  • 增强了给对象指派职责(Responsibility)的灵活性

    当在对象中分派职责时,职责链给你更多的灵活性。

    你可以通过在运行时刻对该链进行动态的增加或修改来增加或改变处理一个请求的那些职责。你可以将这种机制与静态的特例化处理对象的继承机制结合起来使用。

  • 不保证被接受

    既然一个请求没有明确的接收者,那么就不能保证它一定会被处理—该请求可能一直到链的末端都得不到处理。

    一个请求也可能因该链没有被正确配置而得不到处理。

适合应用场景

  1. 当程序需要使用不同方式处理不同种类请求, 而且请求类型和顺序预先未知时, 可以使用责任链模式。
    • 该模式能将多个处理者连接成一条链。 接收到请求后, 它会 “询问” 每个处理者是否能够对其进行处理。 这样所有处理者都有机会来处理请求。
  2. 当一个请求必须按顺序被多个处理者执行时, 可以使用该模式。
    • 无论你以何种顺序将处理者连接成一条链, 所有请求都会严格按照顺序通过链上的处理者。
  3. 如果所需处理者及其顺序必须在运行时进行改变, 可以使用责任链模式。
    • 如果在处理者类中有对引用成员变量的设定方法, 你将能动态地插入和移除处理者, 或者改变其顺序。

已知使用

  • java.util.logging.Logger#log()
  • Apache Commons Chain
  • javax.servlet.Filter#doFilter()

意图

外观模式(Facade Pattern):外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式。

模式结构

外观模式包含如下角色:

  • Facade: 外观角色
  • SubSystem:子系统角色

现实世界的例子

金矿如何运作? “好吧,矿工们到那里去挖金子吧!” 你说。 这就是你所相信的,因为你使用的是 goldmine 在外部提供的一个简单界面,在内部它必须做很多事情才能实现。 这个复杂子系统的简单接口就是外观。

程序化示例

让我们以上面的金矿为例。这里我们有矮人矿工的等级制度。首先有一个基类DwarvenMineWorker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public abstract class DwarvenMineWorker {

public abstract String name();

public abstract void work();

public void wakeUp() {
System.out.println(name() + " wakes up.");
}

public void goToMine() {
System.out.println(name() + " goes to the mine.");
}

public void goHome() {
System.out.println(name() + " goes home.");
}

public void goToSleep() {
System.out.println(name() + " goes to sleep.");
}

enum Action {
WAKE_UP,
GO_TO_MINE,
GO_HOME,
GO_TO_SLEEP,
WORK
}

public void action(Action... action) {
Arrays.stream(action).forEach(this::action);
}

private void action(Action action) {
switch (action) {
case WAKE_UP: wakeUp();
break;
case GO_TO_MINE: goToMine();
break;
case GO_HOME: goHome();
break;
case GO_TO_SLEEP: goToSleep();
break;
case WORK: work();
break;
default:
System.out.println("Undefine action!");
}
}

}

然后我们有具体的矮人类矮人隧道挖掘者DwarvenTunnelDigger,矮人掘金者DwarvenGoldDigger和矮人推车操作员DwarvenCartOperator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class DwarvenTunnelDigger extends DwarvenMineWorker {

@Override
public String name() {
return "DwarvenTunnelDigger";
}

@Override
public void work() {
System.out.println(this.name() + " creates another promising tunnel.");
}
}

public class DwarvenGoldDigger extends DwarvenMineWorker {

@Override
public String name() {
return "DwarvenGoldDigger";
}

@Override
public void work() {
System.out.println(this.name() + " digs for gold.");
}
}

public class DwarvenCartOperator extends DwarvenMineWorker {

@Override
public String name() {
return "DwarvenCartOperator";
}

@Override
public void work() {
System.out.println(this.name() + " moves gold chunks out of the mine.");
}
}

为了操作所有这些金矿工人,我们有矮人金矿门面DwarvenGoldmineFacade

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DwarvenGoldmineFacade {

private final List<DwarvenMineWorker> workers;

public DwarvenGoldmineFacade() {
workers = Arrays.asList(new DwarvenTunnelDigger(), new DwarvenGoldDigger(), new DwarvenCartOperator());
}

public void startNewDay() {
makeActions(workers, DwarvenMineWorker.Action.WAKE_UP, DwarvenMineWorker.Action.GO_TO_MINE);
}

public void digOutGold() {
makeActions(workers, DwarvenMineWorker.Action.WORK);
}

public void endDay() {
makeActions(workers, DwarvenMineWorker.Action.GO_HOME, DwarvenMineWorker.Action.GO_TO_SLEEP);
}

private static void makeActions(List<DwarvenMineWorker> workers, DwarvenMineWorker.Action... actions) {
workers.forEach(worker -> worker.action(actions));
}

}

现在让我们使用门面:

1
2
3
4
var facade = new DwarvenGoldmineFacade();
facade.startNewDay();
facade.digOutGold();
facade.endDay();

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DwarvenTunnelDigger wakes up.
DwarvenTunnelDigger goes to the mine.
DwarvenGoldDigger wakes up.
DwarvenGoldDigger goes to the mine.
DwarvenCartOperator wakes up.
DwarvenCartOperator goes to the mine.
DwarvenTunnelDigger creates another promising tunnel.
DwarvenGoldDigger digs for gold.
DwarvenCartOperator moves gold chunks out of the mine.
DwarvenTunnelDigger goes home.
DwarvenTunnelDigger goes to sleep.
DwarvenGoldDigger goes home.
DwarvenGoldDigger goes to sleep.
DwarvenCartOperator goes home.
DwarvenCartOperator goes to sleep.

外观模式适合应用场景

  • 子系统通常会随着时间的推进变得越来越复杂。 即便是应用了设计模式, 通常你也会创建更多的类。 尽管在多种情形中子系统可能是更灵活或易于复用的, 但其所需的配置和样板代码数量将会增长得更快。 为了解决这个问题, 外观将会提供指向子系统中最常用功能的快捷方式, 能够满足客户端的大部分需求。

  • 客户端和抽象的实现类之间存在许多依赖关系。引入外观将子系统与客户端和其他子系统解耦,从而提高子系统的独立性和可移植性。

  • 如果需要将子系统组织为多层结构,可以使用外观。创建外观来定义子系统中各层次的入口。 你可以要求子系统仅使用外观来进行交互, 以减少子系统之间的耦合。让我们回到视频转换框架的例子。 该框架可以拆分为两个层次: 音频相关和视频相关。 你可以为每个层次创建一个外观, 然后要求各层的类必须通过这些外观进行交互。 这种方式看上去与中介者模式非常相似。

模式分析

根据“单一职责原则”,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的入口。 -外观模式也是“迪米特法则”的体现,通过引入一个新的外观类可以降低原有系统的复杂度,同时降低客户类与子系统类的耦合度。 - 外观模式要求一个子系统的外部与其内部的通信通过一个统一的外观对象进行,外观类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道。 -外观模式的目的在于降低系统的复杂程度。 -外观模式从很大程度上提高了客户端使用的便捷性,使得客户端无须关心子系统的工作细节,通过外观角色即可调用相关功能。

总结

  • 在外观模式中,外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式。
  • 外观模式包含两个角色:外观角色是在客户端直接调用的角色,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理;在软件系统中可以同时有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能。
  • 外观模式要求一个子系统的外部与其内部的通信通过一个统一的外观对象进行,外观类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道。
  • 外观模式主要优点在于对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易,它实现了子系统与客户之间的松耦合关系,并降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程;其缺点在于不能很好地限制客户使用子系统类,而且在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
  • 外观模式适用情况包括:要为一个复杂子系统提供一个简单接口;客户程序与多个子系统之间存在很大的依赖性;在层次化结构中,需要定义系统中每一层的入口,使得层与层之间不直接产生联系。
0%