Linux 从 0 到 1(三) - 数据处理,管道,重定向,监视系统活动,后台运行及合并多个终端

数据处理

grep命令:筛选数据

grep是Globally search a Regular Expression and Print的缩写,意思是《全局搜索一个正则表达式,并且打印》。

grep命令的功能简单说来是在文件中查找关键字,并且显示关键字所在的行。

grep命令极为强大,也是Linux中使用最多的命令之一。它的强大之处在于它不仅可以实现简单的查找,而且可以配合《正则表达式》来实现比较复杂的查找。

grep的简单用法

grep的使用方法有很多种。但我们一开始先学习最基本的用法:

grep text file

可以看到,上面就是grep命令的最基本用法。

text代表要搜索的文本,file代表供搜索的文件。

我们用实际的例子来学习。比如我要在用户的家目录的 .bashrc 文件中搜索alias这个文本,而且显示所有包含alias的行。

grep alias .bashrc

grep命令列出了.bashrc文件中所有包含alias的行,并且在小编的终端中,以红色标出了每一个alias。其实grep更像是一个过滤器,它可以筛选出我们要找的对象。

如果我们要用grep命令在一个文件中查找用空格隔开的文本,那么就要加上双引号,例如:

grep "Hello World" file2
常用参数
  • -i参数:忽略大小写,i是英语ignore的缩写,表示“忽略”。
grep -i alias .bashrc
  • -n参数:显示行号,n是英语number的缩写,表示“数字,编号”。
grep -n alias .bashrc
  • -v参数:只显示文本不在的行,v是invert的缩写,表示“颠倒,倒置”。-v参数的作用与正常grep的作用正好颠倒,就是只显示搜索的文本不在的那些行。
grep -v alias .bashrc
  • -r参数:在所有子目录和子文件中查找,r是英语recursive的缩写,表示“递归”。
    如果用了-r参数,那么grep命令使用时的最后一个参数(grep text file这个模式中的file)需要换成directory,也就是必须是一个目录。因为-r参数是让grep命令能够在指定目录的所有子目录和子文件中查找文本。
grep -r "Hello World" folder/

表示在folder这个目录的所有子目录和子文件中查找Hello World这个文本。当然了,以上例子中,folder后面的斜杠(/)不是必须的,这里只是为了清楚表明folder是一个目录。只要folder是一个目录,Linux系统是不会搞错的。

Linux中还有一个rgrep的命令,它的作用相当于grep -r

grep 的高级用法:配合正则表达式

我们首先来看一眼以下的这个表格,表格中列出了最常用的一些正则表达式的字符以及其含义:

特殊字符 含义
. 匹配除 "\n" 之外的任何单个字符
^ 行首(匹配输入字符串的开始位置)
$ 行尾(匹配输入字符串的结束位置)
[] 在中括号中的任意一个字符
? 问号前面的元素出现零次或一次
* 星号前面的元素可能出现零次,一次或多次
+ 加号前面的元素必须出现一次以上(包含一次)
一根竖线 逻辑或
() 表达式的分组(表示范围和优先度)

首先,为了让grep命令知道我们要使用正则表达式,须要加上-E参数。例如:

grep -E Alias .bashrc

当然了,Linux也有一个命令egrep,其效果等同grep -E

不要怀疑,Alias也算是一个正则表达式,只不过没有用到上面表格中的特殊符号而已。
首先来看这个例子:

grep -E ^alias .bashrc

这个例子中,我们用到了这个特殊符号,上面的表格里对于已经做了说明:行首(匹配输入字符串的开始位置)。也就是说,^后面的字符须要出现在一行的开始。

因此,就搜出了如上图中的三行,这三行都是包含alias,并且以alias开头的。

再来举几个例子:

grep -E [Aa]lias .bashrc

再比如:

grep -E [0-4] .bashrc

用于搜索包含0至4的任一数字的行。

grep -E [a-zA-Z] .bashrc

用于搜索包含在a至z之间的任意字母或者A至Z之间的任意字母的行。

其实在Ubuntu这样的Linux发行版中,grep如果要和正则表达式配合,不加-E参数也是可以的,正则表达式始终是激活的。不过有的Unix发行版的系统可能不加-E参数就不能搜索正则表达式,因此为了兼容,我们一般时候会说加上-E参数比较好。

sort命令:为文件排序

为了演示,我们首先用文本编辑器(可以用nano)来创建一个文件,名叫name.txt比如,然后在里面写入以下的行:

John
Paul
Luc
Matthew
Mark
jude
Daniel
Samuel
Job

然后进行 sort:

sort name.txt

sort命令将name.txt文件中的行按照首字母的英文字典顺序进行了排列,而且sort命令并不区分大小写。

-o参数:将排序后的内容写入新文件

如果你打开name.txt文件,你会发现,经过了sort命令的“洗礼”,name.txt中的内容还是维持原来的顺序。

单独使用sort命令是不会真正改变文件内容的,只是把排序结果显示在终端上。

那我们要存储排序结果到新的文件怎么办呢?可以用-o参数。

sort -o name_sorted.txt name.txt

可以看到,name.txt经过sort命令排序之后的内容被储存在了新的文件name_sorted.txt中,而name.txt的内容是不变的。

-r参数:倒序排列

-r参数中的r是reverse的缩写,是“相反,反面”的意思,与普通的仅用sort命令正好相反。

sort -r name.txt
-R参数:随机排序

-R参数比较“无厘头”,因为它会让sort命令的排序变为随机,就是任意排序,也许每次都不一样。但在有些时候,还是很有用的。

sort -R name.txt
-n参数:对数字排序

对数字的排序有点特殊。默认地,仅用sort命令的时候,是不区分首字符是否是数字的,因此还是按照1-9的顺序来排序。例如138会排在25前面,因为1排在2的前面。

那如果我们要sort命令识别整个数字,比如按照大小顺序来说,25应该排在138前面,那该怎么办呢?

就可以请出我们的-n参数了。n是number的缩写。是英语“数字”的意思。-n参数用于对数字进行排序,按从小到大排序。

wc命令:文件的统计

wc是word count的缩写,因此,wc命令貌似是用来统计单词数目的,但其实wc的功能不仅止于此。wc命令还可以用来统计行数,字符数,字节数等。

跟前面的命令一样,wc命令的用法也是后接文件名。wc命令很有用,应该会成为你经常用到的命令之一。

wc name.txt

如果不加选项参数,那么wc命令的返回值有些特殊,有点晦涩难懂。

可以看到返回值是

9 9 50 name.txt

那么这三个数字:9,9,和50分别表示什么呢?

这三个数字,按顺序,分别表示:

  • 行数

  • 单词数

  • 字节数

因为我们之前创建name.txt时,每一行只有一个单词(英语名字),所以这里统计的行数和单词数都是9。

-l参数:统计行数

为了只统计行数,我们可以加上-l参数。l是英语line的缩写,表示“行”。

-w参数:统计单词数

w是英语word的缩写,word是英语“单词”的意思。因此-w参数用于统计单词。

-c参数:统计字节数

不知道为什么是c,因为byte或者octet(都可以表示《字节》)的首字母都不是c啊。不管了。也许c是character(英语“字符”的意思)的缩写吧。

-m参数:统计字符数

不知道为什么是m,因为character(英语“字符”)的首字母不是m啊。不管了。

uniq命令:删除文件中的重复内容

有时候,文件中包含重复的行。我们也许想要将重复的内容删除。

这时,uniq命令就显得很有用了。uniq是英语unique的缩写,表示“独一无二的”。

为了演示,我们创建一个文件repeat.txt(repeat是英语“重复”的意思),里面写入如下排序好的内容(因为uniq命令还是有点“呆”,因为它只能将连续的重复行变为一行):

Albert
China
France
France
France
John
Matthew
Matthew
patrick
Steve
Vincent

可以看到,有三个France连在一起,两个Matthew连在一起。

我们用uniq命令来处理看看:

uniq repeat.txt

最后结果是,三个连续的France只剩下一个了,两个连续的Matthew也只剩一个了。

但是uniq命令并不会改变原文件的内容,只会把处理后的内容显示出来。如果想将处理后的内容储存到一个新文件中,可以使用如下的方法:

uniq repeat.txt unique.txt
-c参数:统计重复的行数

-c参数用于显示重复的行数,如果是独一无二的行,那么数目就是1。

-d参数:只显示重复行的值
cut命令:剪切文件的一部分内容

cut命令用于对文件的每一行进行剪切处理。

-c参数:根据字符数来剪切

比如,我们要name.txt的每一行只保留第2至第4个字符。可以这样做:

cut -c 2-4 name.txt

流、管道、重定向

命令的输出可以有三个去向:

  • 终端

  • 文件

  • 其他命令的输入

到目前为止,我们只用过第一种形式:把命令的输出结果显示在终端。

>和>>:重定向到文件

我们先从最简单的开始。最简单的操作就是把命令的输出结果重定向到文件中,就不会在终端显示命令运行结果了。

在创建文件之前,我们来谈谈cut命令的进阶用法。

cut命令进阶:根据分隔符来剪切

首先,我们来看一种特殊的文件形式:CSV格式。

CSV是Comma Separated Values的缩写,翻成中文是《逗号分隔值》。

以下摘自百度百科:
逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本)。
纯文本意味着该文件是一个字符序列,不含必须像二进制数字那样被解读的数据。
CSV文件由任意数目的记录组成,记录间以某种换行符分隔;每条记录由字段组成,字段间的分隔符是其它字符或字符串,最常见的是逗号或制表符。
通常,所有记录都有完全相同的字段序列。

CSV文件的后缀名是.csv,通常可以被Excel等软件打开,打开之后会把分隔符隔开的各个数值填充到表格里。

我们来构建一个CSV文件。因为我们要用逗号作为分隔符,来学习cut命令的进阶使用:根据分隔符来剪切。

我们的CSV文件的内容可以如下,我们用Nano这样的文本编辑器来编写这个CSV文件,并且取名:notes.csv

Mack,95 / 100,很不错
Matthew,30 / 100,跟平时一样水
Louise,70 / 100,有进步
Luke,54 / 100,接近平均分了
John,68 / 100,不错,但还可以更好
Samuel,100 / 100,总是那么完美
David,40 / 100,退步挺大呀

如上图所示,我们的notes.csv文件中每一行由三部分组成,每部分由一个逗号分隔:

  • 学生名字

  • 成绩(满分是100分)

  • 评语

现在假如我们要从notes.csv文件中提取名字那一列,怎么办呢?我们不能用cut命令的-c参数啊,毕竟每个名字的字符数不相等。

我们看到文件中每一行的每一部分是用分隔符来隔开的,所以我们可以这样做:

需要用到两个参数:

  • -d参数:d是delimiter的缩写,是英语《分隔符》的意思。用于指定用什么分隔符(比如逗号,分号,双引号等等)。

  • -f参数:f是field的缩写,是英语《区域》的意思。表示剪切下用分隔符分隔的哪一块或哪几块区域。

我们的notes.csv文件是用逗号来分隔三个部分的,我们要剪切下来的是名字那一列,也就是第一部分。因此我们可以这样使用:

cut -d , -f 1 notes.csv

那如果我们只想剪切下评语部分呢?评语是第三部分:

cut -d , -f 3 notes.csv

那如果我们要第一和第三部分呢?可以这样:

cut -d , -f 1,3 notes.csv

同样地,我们可以用

cut -d , -f 2- notes.csv 

来剪切第二部分直到最后的内容。

好了,现在准备工作做好了,我们可以来学习重定向流符号了。

>:重定向到新的文件
我们知道虽然我们刚才用cut命令从notes.csv文件中剪切出来一些部分,但原始的notes.csv文件是不变的。

我们现在想要将剪切出来的部分储存到一个文件中,而不是像之前那样显示在终端里。

(为了方便,我们在家目录下新建一个redirect目录,将之前的notes.csv文件放到这个目录下,再用cd redirect命令定位到这个目录)

这个符号可以将命令的输出结果重定向到你自己选择的文件中。例如:

cut -d , -f 1 notes.csv > students.txt

有时候,我们既不想将命令的输出显示在终端,又不想将其储存到文件中,怎么办呢?

Linux中有一个俗称《黑洞》的文件,就是

/dev/null

/dev/null 文件是特殊文件,不是一个目录。此文件具有唯一的属性:它总是空的。它能使发送到/dev/null 的任何数据作废,就好像这些数据掉进了无底的黑洞一般。

因此,假如我们不需要在终端显示刚才那个cut命令的结果,也不想存储到文件里,那么可以这么做:

cut -d , -f 1 notes.csv > /dev/null

>>: 重定向到文件末尾
我们已经知道,单独一个>符号可以实现重定向到新的文件(覆盖文件内容),那么两个连在一起的>符号有什么作用呢?

>>的作用与>是类似的,不过它不会像>那么危险(如果文件已经存在,>符号会覆盖文件内容),而是将重定向的内容写入到文件末尾,起到追加的作用。如果文件不存在,也会被创建。

我们就来实践一下:

cut -d , -f 1 notes.csv >> students.txt

因为我们上一个例子中已经用>符号来重定向名字那列的内容到students.txt文件中了,所以上面的命令会追加同样内容到students.txt的末尾。

>>符号在很多情况下非常有用,比如你人不在电脑前,而你又想让终端为你记录程序运行的结果,就可以在一个日志文件的末尾一直写入。例如:

command >> results.log
2>,2>>,2>&1:重定向错误输出

我们首先来学点新知识。

stdin,stdout,stderr:标准输入,标准输出,标准错误输出

对于我们的终端命令行,我们从键盘向终端输入数据,这是标准输入,也就是stdin。

终端接收键盘输入的命令,会产生两种输出:

  • 标准输出:stdout。指终端输出的信息(不包括错误信息)。

  • 标准错误输出:stderr。指终端输出的错误信息。

第一个stdout就是我们到目前为止看到的那些Linux命令的正常运行结果,比如在终端中运行ls命令,我们以前也看到了它列出当前目录下所有文件。

这三个你也可以把它们看作流:

  • stdin:标准输入流。英语standard input的缩写(standart是英语“标准”的意思,input是英语“输入”的意思)。标准输入是指输入至程序的数据(通常是文件)。程序要求以读(read)操作来传输数据。并非所有程序都要求输入。如dir或ls程序运行时不用任何输入。 除非重定向,输入是预期由键盘获取的。 标准输入的文件描述符为 0

  • stdout:标准输出流。英语standard output的缩写(output是英语“输出”的意思)。标准输出是指程序写输出数据的流。程序要求数据传输使用写的运算。并非所有程序都要求输出。如mv或ren程序在成功完成时是没有输出的。 除非重导向,输出是预期显示在终端上的。 标准输出的文件描述符为 1 (一)。

  • stderr:标准错误输出流。英语standard error的缩写(error是英语“错误”的意思)。标准错误输出是另一个输出流,用于输出错误消息或诊断。它独立于标准输出,且标准输出和标准错误输出可以分别被重定向。标准错误输出的文件描述符为 2 (二);

文件描述符 名字 解释
0 stdin 标准输入
1 stdout 标准输出
2 stderr 标准错误输出

那什么是文件描述符呢?

文件描述符的英语是File Descriptor,简称fd。

文件描述符是一个用于表述指向文件的引用的抽象化概念。这定义本身也有点抽象。我们不需要太深入了解。大致只需要知道:

  • 文件描述符在形式上是一个非负整数。
  • 实际上,它是一个索引值,指向操作系统内核为每一个进程所维护的该进程打开文件的记录表。
  • 当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

文件描述符通常是Unix,Linux等系统的概念。在Windows中,也有类似的概念,但是Windows中称为《句柄》,就是handle。

其实,>和>>符号只是将标准输出重定向到文件,并不能将标准错误输出重定向到文件。
那么我们要重定向标准错误输出,该怎么办呢?

我们就要用到 2> 这个符号
为什么是2呢?记得上面说的吗?标准错误输出的文件描述符是2,所以这里的2表示标准错误输出。如果没有2,单独的>符号就是重定向标准输出(文件描述符为1)。

cat not_exist_file.csv > results.txt 2> errors.log

这个命令里有两个重定向:

>: results.txt:将标准输出重定向到results.txt文件中。

2>: errors.log:将标准错误输出重定向到errors.log文件中。

类似地,2>>符号用于将标准错误输出重定向到文件末尾。

合并输出
上面我们学习了如何将标准输出和标准错误输出分别重定向到不同文件。但是有的时候,我们比较“任性”,我们就想把标准输出和标准错误输出都重定向到同一个地方。怎么做呢?

须要使用 2>&1 这个组合符号。

看着怪怪的对吧?由四个字符组成。这个符号的作用是:将标准错误输出重定向到与标准输出相同的地方。

我们用实例演示一下:

cat not_exist_file.csv > results.txt 2>&1

上面的命令的作用是:将cat not_exist_file.csv这个命令的所有输出(标准输出和标准错误输出)都重定向到results.txt文件中。

如上图所示,运行cat not_exist_file.csv > results.txt 2>&1命令之后,因为not_exist_file.csv这个文件不存在,但又因为加了2>&1这个符号,所以标准输出(为空)和标准错误输出(cat: not_exist_file.csv: No such file or directory)都重定向到results.txt文件中了。

大家是否觉得要将标准输出和标准错误输出都重定向到文件末尾,应该是这样写:2>>&1 呢?

其实不然,这样是不对的。我们还是保持2>&1这个组合不变,只改变前面的符号就行了。例如:

cat not_exist_file.csv >> results.txt 2>&1
<,<<:从文件或键盘读取

到目前为止,这一课我们只讲了如何重定向命令的输出,也就是决定命令输出的信息的去向。那么接着我们可以做一点相反的事情:决定命令的输入来自哪里。

当然了,上面也说了,不是所有的命令都有输入,也不是所有的命令都有输出。

到目前为止,我们的命令的输入都来自于后面接的参数,这些参数有些是文件名,有些是目录名,等等。

但我们其实可以使命令的输入来自于文件或者键盘输入。如下图所示:

<:从文件中读取

我们用一个简单的例子来演示:

cat < notes.csv

如上图所示,cat < notes.csv的运行结果和cat notes.csv (不用重定向流符号)一模一样,都是在终端打印notes.csv的内容,那我们为什么需要<符号呢?

事实上,虽然cat < notes.csv的运行结果和cat notes.csv一样,但是原理却不一样:

  • cat notes.csv :这种情况下,cat命令接受的输入是notes.csv这个文件名,那么它要先打开notes.csv文件,然后打印出文件内容。

  • cat < notes.csv :这种情况下,cat命令接受的输入直接是notes.csv这个文件的内容,cat命令只负责将其内容打印。而打开文件并将文件内容传递给cat命令的工作则交给shell程序(也就是控制终端的程序)来完成。

所以,虽然结果看似一样,但是中间的过程却是不一样的。

<<:从键盘读取

<<符号的作用是将键盘的输入重定向为某个命令的输入,很多情况下都很有用。

我们用实例来说明:

sort -n << END

如上图所示,输入这条命令之后,回车,终端就进入了键盘输入模式。

我们知道sort -n的作用是按照从小到大进行排列。那么我们就输入一些数值吧(每输一个数值,用回车键来换行,接着输入下一个数值,输入END来结束输入,END被称为结束字符串。当然了,你可以用其他字符串,比如haha,nihao,不一定要用END。



可以看到,sort -n命令将我们输入的一串数值进行了由小到大的排序。

我们再试试其他命令与<<符号的配合,这次我们用wc命令吧,wc命令用于统计字符等,如果配合-m参数就是统计字符数。

wc -m << END

可以看到,《How many characters are there in this sentence ?》这句话中有49个字符。

当然了,我们也可以将之前学习的输出重定向符号和这一节的输入重定向符号结合使用:

sort -n << END > numbers_sorted.txt 2>&1

以上命令将sort -n命令对数值排序的结果都输入到numbers_sorted.txt文件中,如果有错误信息也输入。

|:管道

管道也算是重定向流的一种。

将两个命令连成管道,这是什么意思呢?简单的说就是将一个命令的输出作为另一个命令的输入。
大致来说:命令1的输出立即会变成命令2的输入。使用这个原理,我们可以用 | 符号连接无穷多个命令,构成很长的命令管道。

我们用几个实例来学习管道吧。

按学生名字排序

你应该还记得我们的notes.csv文件,其中有三部分,用逗号隔开的,第一部分是学生名字,第二部分是成绩,第三部分是评语。

之前我们用cut -d , -f 1 notes.csv命令来剪切了名字那一列。我们前一课学了sort命令,它用于排序文件内容。

那么为什么不把这两个命令用管道符连接起来呢?我们可以用sort命令对cut命令提取到的名字列进行排序:

cut -d , -f 1 notes.csv | sort

如上图所见,我们用sort命令对名字列按照首字母的字典顺序进行了排序。

如果我们将上面的命令再扩充一下,配合之前学习的输出重定向符号,就变成了这样:

cut -d , -f 1 notes.csv | sort > sorted_names.txt

我们将sort命令排序的结果重定向到sorted_names.txt文件中了。

根据大小排序目录

之前我们学过,du命令可以深入遍历当前目录下每个子目录,把所有文件的大小都做一个统计。

我们可以用cd命令来回到我们的家目录。然后运行du命令。

问题是:du命令要运行挺久的,因为小编家目录下文件很多。

单独用du命令的缺点我们也有目共睹了,一是可能要运行很久(如果文件很多),二是显示结果没有排序,杂乱无章。

但如果我们这样做就会清爽很多:

du | sort -nr | head

还记得head命令的用法么?如果不用-n参数指定显示行数,那么head会默认显示前10行。

所以以上命令的作用是:

  • du :深入遍历当前目录下每个子目录,把所有文件的大小都做一个统计。

  • sort -nr :sort命令的-n参数是按以数值来排序(此处是文件大小)排序,默认是小的在前;-r参数是倒序排列,有了-r参数,-n参数就变成大的数值在前了。

  • head :列出前十个数值(这里是文件大小)。

列出包含关键字的文件

还记得我们的好朋友grep命令吗?之前有学过,这个命令很强大,可以在文件中查找关键字,并且显示关键字所在的行。但有时,我们会觉得grep显示的信息太冗长了。每一行不仅有文件名,还有关键字出现的那一行文本,等等。

我们可以运行以下命令试试:

sudo grep log -Ir /var/log | cut -d : -f 1 | sort | uniq

这个命令做了什么呢?让我们一步步来分解:

  • sudo grep log -Ir /var/log:遍历/var/log这个目录及其子目录,列出所有包含log这个关键字的行。-I参数用于排除二进制文件。-r参数用于递归遍历。sudo命令是为了以root身份查找系统文件夹/var/log。

  • cut -d : -f 1从命令1的输出结果中只剪切出文件名那一列(由冒号分隔的第一个区域)。

  • sort将文件名的列以首字母的字典顺序进行排序。

  • uniq: 去掉重复的文件名。

监视系统活动

w命令:都有谁,在做什么?

第一个出场的命令是迄今为止我们遇到过的最短的命令,这个命令只有一个字母:w

就是这么一个很简单的命令,却挺实用,它可以帮助我们快速了解系统中目前有哪些用户登录着,以及他们在干什么。

如果你负责一台Linux服务器,有时候它会过载,变得很慢,这时你可以登录此服务器,然后运行w命令,快速了解到底发生了什么事。

如上图所见,目前Linux系统中只有一个用户登录,就是小编自己的用户,名叫oscar。

w命令输出的信息虽然短,但是比较密集,看第一眼并不容易了解到底是什么。但其实w命令给出的信息非常有用。

我们将其分解成不同的部分来解释,按照从上到下,从左到右的顺序。

  • 时间(用date命令也可以做到)
    我们看到信息的第一行中有 17:36:13,这就是当前时间:17点36分13秒。
    我们之前学过date这个命令,它可以显示当前日期,时间,和时区:

  • 运行时间(用uptime命令也可以做到)
    w命令的输出中,紧跟在当前时间后面的是 up 25 min
    up是英语《运行正常的》的意思。min是英语minute的缩写,表示《分钟》。
    所以 up 25 min 表示系统已经运行了25分钟了,就是从开机登录到现在经过的时间。
    这个信息还是挺有用的,因为一旦系统重启或关机,那么运行时间会重归0。

  • 负载(同样可以用uptime命令获知)
    在w命令的输出的第一行的右边,有三个数值,表示负载:

load average: 0.15, 0.11, 0.09

负载是系统活动的一个指标:

这三个数值从左到右分别表示:

  • 1分钟以内的平均负载(0.15)。

  • 5分钟之内的平均负载(0.11)。

  • 15分钟之内的平均负载(0.09)。

这些数值具体表示什么呢?

说起来稍微有点复杂。阅读手册,我们可以知道:这些数值表示一段时间内的平均活跃进程数(也就是使用CPU处理器的进程数。进程简单地说就是运行起来的程序)。

由此可知,近1分钟内平均有0.15个进程使用了处理器,也就是说处理器有15%的时间是活跃的。

负载的数值也取决于电脑的处理器的核心数目。一个单核的处理器如果负载超过了1,那就是过载了。双核的处理器如果负载超过了2,那就是过载了。四核的处理器如果负载超过了4,那就是过载。依次类推。

有的服务器过载的时候负载的数值甚至能达到50那么多。

如果负载值在较长的时间中都维持比较大的话,就说明系统出问题了。我们可以用tload命令来绘制随时间变化的负载《曲线》图。

tload

tload命令的输出(随时间变化):

图中横坐标是时间,纵坐标的高低表示负载大小,左边猛然上升的纵坐标值是因为小编打开了火狐浏览器。

  • 登录的用户列表(用who命令也可以做到)
    w命令的输出中,除去第一行,下面的行可以归为一个部分,就是登录的用户列表。
    这几行信息显示当下系统中连接的用户是哪几位,他们在做什么任务,任务进行多长时间了。
    可以看到,有两行都是以oscar开头,我目前是在自己的个人电脑上,所以Linux系统中只有我一个用户登录,那为什么有两行oscar呢?我们先来了解下各列的意义,就会明白了。
  • USER:用户名(登录名)。

  • TTY:Linux中默认提供了六个命令行终端和一个图形终端:tty1tty7。其中tty1tty6是命令行终端
    Ubuntu中可以通过Ctrl+Alt+F1~F7切换这7个终端。除了这7个基本的《大环境》终端,我们还可以在tty7中开很多不是全屏的终端,也就是我们平时用来输入命令行的图形终端(ctrl + shift + T快捷键),这些终端的名字是以pts开头的。pts是 pseudo terminal slave的缩写,表示“伪终端从属”。如果我新开一个图形终端,那么显示名称为pts/0,如上图中所示。如果我再开一个图形终端,那么它的名字就是pts/1。依次类推。

  • LOGIN@:用户连接系统的时间。

  • IDLE:用户有多久没活跃了(没运行任何命令)。idle是英语“不活跃的”的意思。

  • WHAT:当下用户正运行的程序。what是英语“什么”的意思。

在小编的情况,我们看到有两个用户,有两个oscar。第一个oscar对应的是登录Ubuntu系统的用户,也就是我们开机后登录的用户,进入的是Ubuntu的图形用户界面,可以从WHAT那列的信息看出来,这里是gnome-sessi,是gnome session(session是英语“会话”的意思)的缩写,表示Gnome这个桌面系统。第二个oscar对应的是打开当前我们正在操作的这个图形终端的用户,也是oscar,不过TTY那列是pts/0,WHAT那列是w,说明用户运行了w命令。没错啊,因为这些信息就是w命令运行的输出结果。

w命令虽然很有用,但是给出的信息还是不够详尽。我们下面来看看如何获得详细的系统进程的信息。

ps命令和top命令:列出运行的进程

简单说来,进程就是加载到内存中运行的程序。

大多数程序运行时都只在内存中启动一个进程。例如Linux中的OpenOffice这个软件。有的程序运行时却会创建不少进程,例如Google的Chrome浏览器,每开一个标签栏都会创建一个新的进程。

在网络服务器上,一般我们都是用Apache这个软件来发送网页给网民。Apache在运行时就会创建很多进程,分别负责不同的任务。一般的数据库软件,例如MySQL,PostgreSQL也是如此。因此,假如你发现一个程序创建了好多个进程的话,并没有什么好吃惊的。

在Linux中,我们有两个命令可以帮助我们查看系统中运行的进程。

ps 是Process Status的缩写,所以ps命令用于显示当前系统中的进程。

ps命令显示的进程列表不会随时间而更新,是静态的,只是运行ps命令当时的那个状态,或者说是一个进程的快照,英语称为snapshot,就好像照了一张照片一样。

我们试着不加任何参数直接运行ps命令:

可以看到显示结果有四列:

  • PID:进程号。pid是process identifier的缩写。每个进程有唯一的进程号。之后我们学习如何结束进程时需要用到进程号。

  • TTY:进程运行所在的终端。

  • TIME:进程运行了多久。

  • CMD:产生这个进程的程序名。如果你在进程列表中看到有好几行都是同样的程序名,那么就是同样的程序产生了不止一个进程(例如MySQL程序)。

在小编的情况,我们运行ps命令时显示了两个进程正在运行,一个是bash,也就是操控当前终端的shell程序。另一个是ps命令自己。

两个进程,这就是全部了?

当然不是。ps命令不带参数使用时只会列出当前运行ps命令的用户在当前这个终端中所运行的进程。有好多进程是root用户运行的,就没列出来。还有的进程,虽然也是当前用户运行的,但不是在当前的终端里,所以也没列出来。

ps命令有很多不同的参数。你可以用man ps来查看。可不要被众多的选项参数吓到。

我们不可能介绍每个参数,下面就介绍一些常用的参数吧。

-ef:列出所有进程

-ef参数可以使ps命令列出所有用户在所有终端的所有进程。

ps -ef

可以看到,用了-ef参数,因为列出所有用户的所有进程,所以比刚才不加参数的ps命令多了一些列。

例如第一列UID,是user identifier的缩写,表示《用户名》,也就是运行进程的用户。

-efH:以乔木状列出所有进程
ps -efH

比上面的-ef多加了一个H参数,可以使ps命令按照乔木状列出进程。有的进程是某些进程的子进程。

比如上图中,我们可以看到/sbin/init这个程序(进程)就启动了其他的进程,比如udevd --daemon,portmap,smbd -F,等等。

ps -u 用户名:列出此用户运行的进程。

可以看到有不少进程,单独用ps的时候只有两个进程,那是因为有好多进程不是在pts/0那个终端里运行的。

top:进程的动态列表

ps命令虽然强大,但有一个缺陷:是静态的,只能记录当下那一刻的进程列表。

但事实上,我们的系统是时刻变化的。有些进程这一秒在运行,下一秒可能就被终止了。所以ps命令并不能帮助我们实时监控系统的进程。

怎么才能获取一个随时间一直在变的进程列表呢?就需要用到top命令了。

这个列表是交互性的,而且实时更新。

第一行,你可以看到当前时间(04:02:41,就是凌晨4点02分41秒),系统运行时间(3:52,也就是3个小时52分钟),用户数目(2 users,就是“两个用户”),平均负载(load average: 0.50, 0.34, 0.25)。

接下来的几行,我们不过多解释了,因为这涉及到操作系统的一些概念。

在下面,PID,USER,PR,NI,VIRT,RES,SHR,S,%CPU,%MEM,TIME+,COMMAND开始的那个列表就是进程列表了。

但为什么相比之前ps -ef时进程少了这么多呢?

因为top命令无法显示全部的进程。它只显示排在前面的一些进程,为了整个信息区域正好占据终端中一个页面的大小。

默认地,这些进程是按照使用处理器的比率来排序的,从大到小(按照%CPU那一列的数值),最消耗处理器的进程排在最前面。所以如果你觉得自己的系统变慢了,过载了,查看列表中最开始的几个进程是个好主意。

前面说过了,top命令输出的信息页面不但是动态变化的,而且是可交互的。

我们用一些键盘的按键可以控制top的显示结果。

q:退出top

h:显示帮助文档,也就是哪些按键可以使用。按下任意键返回。

B:大写的B,加粗某些信息。

f:在进程列表中添加或删除某些列。

F:改变进程列表排序所参照的列。默认情况下,是按照%CPU那一列来排序。

u:依照用户来过滤显示。可以输入用户名,按回车。

k:结束某个进程。会让你输入要结束的进程的PID。

s:改变刷新页面的时间。默认地,页面每隔3秒刷新一次。

当然了,Linux下有很多优秀的软件,做得比top和ps更好,例如:Linux中比Top颜值更高的系统监控工具Glances。

Ctrl + C和kill命令:停止进程

有时候,系统会卡住。这在Linux中也是会发生的。

在Linux中停止进程有几种方法:

  • Ctrl + C:停止终端中正在运行的进程

顺便讲一个Linux下的小技巧,比如我们在终端下,用鼠标选中一段文字,然后按一下鼠标中间的滚轮,就会在终端里黏贴选中的文字。
而在终端中拷贝和黏贴,需要用“Ctrl+Shift+C”和“Ctrl+Shift+V”来执行。

  • kill命令:结束一个进程
    Ctrl + C这个组合键只能作用于当前终端中正在运行的程序,而Linux中,很多进程是你看不到的,在后台运行。

这时候就需要请出kill命令了。

kill命令后接需要结束的进程号,也就是之前我们看到过的PID。是一个数字,我们可以通过ps命令或top命令来获知进程的PID。

kill 8461

我们也可以同时用kill来结束好几个进程,只要用空格隔开它们的PID:

kill 8461 8561 1706

有时候,kill命令虽然作用于一个进程,但是它不终止,卡住了(因为kill命令会很友善地结束进程)。那怎么办呢?有没有比较简单粗暴的方式呢?

是有的,我们可以用kill -9来立即强制结束进程。比如:

kill -9 7291

立即结束PID是7291的进程,非常“野蛮粗暴”。

killall:结束多个进程

记得我们之前提过,同一个程序运行时可能启动多个进程。如果你想一下子结束所有这些进程,怎么做呢?

当然,你可以用kill + PID的方式一个个地结束这些进程,但是我们有一个更快捷的命令:killall。

all是英语《全部》的意思,所以killall命令就是用于结束全部要结束的进程。

不同于kill命令,killall命令后接程序名,而不是PID(进程号)。

假设现在我开了好几个图形终端,在每一个中我都运行find命令进行文件的查找。

这样,我用ps -u oscar | grep find就可以搜索出好多个find的进程。

如果我要结束所有这些find进程,可以这么做:

killall find
halt命令和reboot命令:停止和重启系统

既然我们刚学了如何用kill命令来结束进程,那么现在我们该来学习一下如何关闭和重启整个Linux系统了吧。

halt:关闭系统

需要以root身份才能关闭系统,所以我们可以这么做:

sudo halt
reboot:重启系统

同样地,需要以root身份才能重启系统。

实际上,halt和reboot这两个命令都调用了Linux的另一个命令:shutdown

其实还有一个命令:poweroff也可以用于关机,而且非常有效率。

poweroff

后台运行及合并多个终端

&符号和nohup命令:后台运行进程

我们到目前为止用终端做的事情都是眼目所能及的,也就是说:我们运行的命令都是在前台可见的。

这样有一个好处是我们可以看到命令运行的过程,有什么问题可以及时发现。但是也有缺陷,例如有的命令运行耗时良久,我们又不想无所事事,怎么办呢?难道我开一个终端专门执行一个耗时命令,然后为了能做其他事情,我再启动一个终端,那也很不方便。而且,这样的规避方法在非图形界面的终端(还记得我们的tty1~tty6吗?)中是难以实现的,因为只有一个终端窗口。

事实上,我们可以在同一个终端中同时运行好几个命令。怎么做呢?就需要用到后台进程的概念。

前台进程和后台进程

默认情况下,用户创建的进程都是前台进程。前台进程从键盘读取数据,并把处理结果输出到显示器。

我们可以看到前台进程的运行过程。例如,使用 ls 命令来遍历当前目录下的文件。

这个程序就运行在前台,它会直接把结果输出到显示器。如果 ls 命令需要数据(实际上不需要),那么它会等待用户从键盘输入。

当程序运行在前台时,由于命令提示符($)还未出现,用户不能输入其他命令;即使程序需要运行很长时间,也必须等待程序运行结束才能输入其他命令。

后台进程与键盘没有必然的关系。当然,后台进程也可能会等待键盘输入。

后台进程的优点是不必等待程序运行结束就可以输入其他命令。

那么怎么使一个进程(程序的实例)运行在后台呢?

&符号:在后台运行进程

前面说过,让一个进程在后台运行有几种方法。

我们带大家来学习第一种,很简单:就是在你要运行的命令最后加上&这个符号。

我们可以用熟悉的cp命令做例子。例如,我运行cp命令来拷贝文件:emacs的软件包。

cp emacs-24.4.tar.gz emacs-24.4-copy.tar.gz &

因为命令最后加了&符号,运行时此进程就成为了后台进程。终端输出了一些信息:

[1] 16525
  • [1]:这是此终端的后台进程的标号。因为这是第一个后台进程,所以标号为1。

  • 16525:这是进程号(PID),如果你想要结束这个后台进程,你可以用我们上一课学习的kill命令:

kill 16525

我们虽然看不到这个拷贝进程的“所作所为”,但它确实在后台默默进行着文件的拷贝。

如果我们用其他命令试一下,例如find命令,你会吃惊的。

例如我们运行:

find / -name "*log" &

但是你会发现,find命令虽然在后台运行了,但是终端还是会不断显示所有找到的内容或错误信息。虽然我们还可以在终端中输入其他命令,但是一直会跳出find搜索的结果还是很让人感到厌烦的。最后小编不得不把它停止(用kill命令)。

幸好,我们之前学过重定向,我们可以把find的输出结果重定向到文件里,就不会再来烦我们了。

find / -name "*log" > output_find &

这样就不会一直有信息输出了。

当然了,我们还可以更保险一些,将标准错误输出也重定向到同一个文件,这样就不会有任何输出了。

find / -name "*log" > output_find 2>&1 &

但现在有一个问题:虽然我们的进程是被放到后台了,在终端貌似看不到它的运行过程了。但是此进程还是与此终端相关联的,假如我们把终端关闭,那么这个进程也就会结束。

nohup命令:使进程与终端分离

&符号虽然常用,但却有一个不可忽视的缺点:后台进程与终端相关联,一旦终端关闭或者用户登出,进程就自动结束。

如果我们想让进程在以上情况下仍然继续在后台运行,那么我们须要用到nohup命令。

当用户注销(logout)或者网络断开时,终端会收到 HUP(是hangup的缩写,英语《挂断》的意思)信号从而关闭其所有子进程;终端被关闭时也会关闭其子进程。

我们可以用nohup命令使命令不受HUP信号影响。

我们用man来看一下nohup命令的解释:

run a command immune to hangups, with output to a not-tty

翻译出来大致就是:使得运行的命令不受hangup信号影响,而且输出会存放到一个非tty中。

nohup命令的用法很简单:在nohup命令之后接要运行的命令。

nohup ls

终端会输出信息:ignoring input and appending output to nohup.out

大致意思是:忽略输入,把输出追加到nohup.out文件中。

使用nohup命令后,输出会被默认地追加写入到一个叫nohup.out的文件里。

现在,我们的进程已经不受终端关闭或者用户断开连接的影响了,会一直运行。当然了,用kill命令还是可以结束此进程的。

Ctrl + Z,jobs,bg和fg命令:控制进程的前后台切换

我们来考虑一种情况:假如你要将进程转到后台运行,但是执行命令时忘记了在最后加上&符号。

如何再使此进程转为后台进程呢?有几种方法。我们一一来学习。

Ctrl + Z:转到后台,并暂停运行

我们用top命令来演示。运行:

top

因为top命令的作用是实时地显示各种系统信息和进程列表。这时,我们按下Ctrl + Z这个组合键:
可以看到终端显示了

[1]+ Stopped top

这行信息。

bg命令:使进程转到后台

经过上面的Ctrl + Z操作,我们可怜的top进程转入后台,并且被暂停运行了。

输入bg,然后回车。bg命令的作用是将命令转入后台运行。假如命令已经在后台,并且暂停着,那么bg命令会将其状态改为运行。

不加任何参数,bg命令会默认作用于最近的一个后台进程,也就是刚才被Ctrl + Z暂停的top进程。如果后面加 %1,%2这样的参数(不带%,直接1,2这样也可以),则是作用于指定标号的进程。因为进程转入后台之后,会显示它在当前终端下的后台进程编号。例如目前top进程转入了后台,它的进程编号是1(可以由[1]+推断)。依次类推,bg %2就是作用于编号为2的后台进程。

我们输入bg,然后回车。看到如下输出:

终端首先显示了:

[1]+ top &

表示top命令被转到了后台,但是接着,它又显示了这一条信息:

[1]+ Stopped top

咦,为什么top进程还是暂停着呢?anz理说bg命令会把在后台暂停的进程重新唤醒,使之在后台重新运行啊。

我们用ps命令来查看一下进程信息:

ps -aux

在上图中可以看到,top这个进程的进程号是23051,状态是T。

首先补充一些知识:

Linux中,进程有5种状态:

  • 运行 (正在运行或在运行队列中等待)
  • 中断 (休眠中, 受阻, 在等待某个条件的形成或接受到信号)
  • 不可中断 (收到信号不唤醒和不可运行, 进程必须等待直到有中断发生)
  • 僵死 (进程已终止, 但进程描述符存在, 直到父进程调用wait4()系统调用后释放)
  • 停止 (进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行运行)

ps命令标识进程的5种状态码:

  • D 不可中断 uninterruptible sleep (usually IO)
  • R 运行 runnable (on run queue)
  • S 中断 sleeping
  • T 停止 traced or stopped
  • Z 僵死 a defunct ("zombie") process

因此,我们的top进程的状态还是T,也就是停止(stopped)的状态。很奇怪是吗?

我们用其他的命令来测试看看。我们测试find命令。首先运行:

find / -name "*log" > find_log 2>&1

上面这个命令的作用是:在根目录 / 下查找以log结尾的文件,将标准输出和标准错误输出都重定向到find_log文件中。

因此,虽然上述命令在运行,但终端中看不到任何信息。

我们用Ctrl + Z来暂停此进程,并将其转到后台。然后再运行bg命令,使其重新在后台运行。

但是并没有显示Stopped,奇怪了,为什么bg作用于暂停的find命令后,并没有像刚才top命令一样仍然显示Stopped呢?

我们再用ps -aux 看一下:

可以看到,top命令的状态是T,也就是停止(Stopped)。而find命令的状态则是D,也即是不可中断的睡眠(但其实是在运行,等会我们就会看到)。

疑问:所以小编也不清楚为什么对普通的命令(例如find),bg命令是起作用的,会将其转成后台运行。但是对于top命令,bg为什么不能将其转成后台运行,可能是因为top命令本身比较特殊吧。也许是因为top命令是前台交互式命令,因此不能被置于后台运行。

jobs命令:显示后台进程状态

这个命令很强大。

jobs命令的作用是显示当前终端里的后台进程状态。虽然我们可以用ps命令来查看进程状态,但是ps命令输出的进程列表太长了。

我们可以用jobs命令来显示刚才那两个进程的状态:top进程和find进程。

jobs命令的输出共分三列,我们逐列来说明:

  • 显示后台进程标号:比如上例中top进程的标号是1,find进程的标号是2,如果还有其他后台进程,那么就会有[3],[4],等等。这个标号和PID(进程号)是不一样的。这个标号只是显示当前终端下的后台进程的一个编号。

  • 显示后台进程状态:比如Stopped是停止的意思,Running是运行的意思。还有其他状态。

  • 命令本身。

可以看到,我们的top进程确实是在后台暂停了,find进程在后台运行,因为显示Running。

fg命令:使进程转到前台

fg是英语foreground的意思,与bg命令相反,fg命令的作用是:使进程转为前台运行。

用法也很简单,和bg一样,如果不加参数,那么fg命令作用于最近的一个后台进程。如果加参数,如%2,那么表示作用于本终端中第二个后台进程。

我们用下面这个状态图来做个总结,应该就很清楚了:


解释一下上图:

  1. 如果我们运行一个程序,默认情况下,它会成为一个前台运行的进程。我们可以按组合键Ctrl + C来销毁此进程。

  2. 我们也可以使此进程在后台运行。假如运行程序时就用&放在命令最后,那么进程就会在后台运行。

  3. 假如在进程运行起来后,按Ctrl + Z,则进程会转到后台,并且停止。此时如果运行bg命令,则进程重新运行,并继续在后台。

  4. fg命令可以使进程转到前台,并且运行。

screen命令:合并多个终端

screen这个程序(所有命令其实都是程序)通常没有在Linux发行版里预装,如果你的Ubuntu系统里没有screen这个程序,那么可以如此安装:

sudo apt-get install screen

安装完之后,你可以输入screen命令。

screen命令用于在一个终端中打开多个终端,就好像在一个页面中开多个标签栏一样
但是screen打开的多个终端是重叠在一起的,如果你不知道,还以为只是打开了一个终端,但是我们会学习如何在各个打开的终端间切换。

在我们运行了screen命令后,再用回车键跳过那页介绍之后,我们看到终端里好像没发生什么变化,就跟之前我们看到的终端一样诶,那screen到底做了什么呢?

其实,screen为我们开了一个虚拟终端,就是在当前实际的终端里又开了一个终端。

如果你再运行screen,那么它又会新开一个虚拟终端。那么怎么退出每个新开的虚拟终端呢?可以按Ctrl + D或者用exit命令。

每次你按Ctrl + D或运行exit命令,都会关闭当前所在的虚拟终端,直到最后一个虚拟终端被关闭,screen程序退出,回到我们的实际终端里,如下图:

上图显示了[screen is terminating],表示所有screen开的虚拟终端都已关闭,screen退出。

在screen程序中,几乎所有的操作都是以Ctrl + a开始的。

以下所有的操作中,英文字母区分大小写。也就是说:b和B是不同的,前者就是按下键盘上的b键,B则是需要用Shift + b。

如何操作呢?首先,按下Ctrl + a键,然后松开Ctrl键和a键,再按其他键来完成一定的操作。

  • Ctrl + a再加?号:显示帮助页面
    我们先用Ctrl + a键(也就是同时按下Ctrl键和a键),然后松开这两个键,再按下?号
    可以看到,帮助页面显示了各种操作的实现方法。

比如说,你想要知道screen的版本号,那就是version,可以看到需要用到v键。但是光是按v键还不够,因为我们看到第一页第二行Command key: ^A ,就是说以下所有的操作,都需要先按下Ctrl + a键。^表示Ctrl键。

常用的组合按键

Ctrl + a,松开,再按c:创建一个新的虚拟终端。

Ctrl + a,松开,再按w:显示当前虚拟终端的列表。会显示在左下角。

Ctrl + a,松开,再按A:重命名当前虚拟终端。修改后的名字,你之后再用Ctrl + a,松开,再按w时就会看到。

Ctrl + a,松开,再按n:跳转到下一个虚拟终端。

Ctrl + a,松开,再按p:跳转到上一个虚拟终端。

Ctrl + a,松开,再按Ctrl + a:跳转到最近刚使用的那个虚拟终端。

Ctrl + a,松开,再按09数字键:跳转到第09号虚拟终端。

Ctrl + a,松开,再按 "(双引号):会让你选择跳转到哪个虚拟终端。

Ctrl + a,松开,再按k:关闭当前终端。

以上是一些常用的screen组合键,下面我们重点来看两个很有用的组合键,分别用于分割虚拟终端和分离screen。

  • Ctrl + a,松开,再按S:分割虚拟终端为多个小虚拟终端
    注意是大写的S,所以是Shif + s。如果这样操作一次,则当前虚拟终端被分割为上下两部分。如下图所示:


如果再按这样操作,就分割成3部分,4部分...

可以看到我在上面的半部分中运行了ls命令,下面的半部分暂时还没跳转过去操作,因此下半部分连命令行提示符也没有,空空的。

那我们如何跳转到下半部分去操作呢?

  • Ctrl + a,松开,再按Tab键。

光标就会跳转到下半部分了,但是还是没见有命令行提示符,那是因为还没为下半部分创建虚拟终端呢。

所以我们可以新建一个:

  • Ctrl + a,松开,再按c。或者打开一个现有的虚拟终端。

可以看到,我们用Ctrl + a,松开,再按c之后,下半部分的左下角的--变成了3 bash,说明新建了一个虚拟终端,编号是3,也就是第4个(虚拟终端的编号从0开始)。

我们在这个3 bash的虚拟终端里运行top命令,如下:

那么我们如何关闭新分割出来的虚拟终端呢?

  • Ctrl + a,松开,再按X(是大写的X,也就是Shift + x)。关闭新分割的虚拟终端。

  • Ctrl + a,松开,再按d:分离screen
    如果我们在screen程序中,就可以使screen程序与当前实际终端分离了,有点类似nohup命令的作用。这样我们就可以重回我们自己的实际终端了,而screen并没有退出,还在后台运行。

如果你要重回screen中,可以输入

screen -r

就又回到刚才的screen的虚拟终端里了。

我们可以使好几个screen进入分离(detached)状态。

之后再运行screen -r想要回去的时候,因为有两个screen分离进程了,实际终端会询问你要回到哪一个,如下图:

你想要回到哪一个就用:

screen -r 编号

就可以了。例如我要回到6815那个screen,只要这样:

screen -r 6815

如果你在实际终端下,输入:

screen -ls

则会列出当前打开着的screen进程。

推荐阅读更多精彩内容