进程
为了弄清楚这节课程的内容,也就是管道,我们先来讨论一下进程。
我们知道,应用的可执行文件是放在文件系统里,把可执行文件启动,就会在操作系统里(具体来说是内存中)形成一个应用的副本,这个副本就是进程。
插一个小知识,以后你再遇到面试题:什么是进程?
ps
如果你要看当前的进程,可以用ps指令。p 代表 processes,也就是进程;s 代表 snapshot,也就是快照。所谓快照,就是像拍照一样。
如上图所示,我启动了两个进程,ps和bash。ps 就是我刚刚启动的,被ps自己捕捉到了;bash是因为我开了这个控制台,执行的shell是bash。
当然操作系统也不可能只有这么几个进程,这是因为不带任何参数的ps指令显示的是同一个电传打字机(TTY上)的进程。TTY 这个概念是一个历史的概念,过去用来传递信息,现在已经被传真、邮件、微信等取代。
操作系统上的 TTY 是一个输入输出终端的概念,比如用户打开 bash,操作系统就为用户分配了一个输入输出终端。没有加任何参数的ps只显示在同一个 TTY 的进程。
如果想看到所有的进程,可以用ps -e,-e没有特殊含义,只是为了和-A区分开。我们通常不直接用ps -e而是用ps -ef,这是因为-f可以带上更多的描述字段,如下图所示:
UID 指进程的所有者;
PID 是进程的唯一标识;
PPID 是进程的父进程 ID;
C 是 CPU 的利用率(就是 CPU 占用);
STIME 是开始时间;
TTY 是进程所在的 TTY,如果没有 TTY 就是 ?号;
TIME;
CMD 是进程启动时的命令,如果不是一个 Shell 命令,而是用方括号括起来,那就是系统进程或者内核过程。
另外一个用得比较多的是ps aux,它和ps -ef能力差不多,但是是 BSD 风格的。就是加州伯克利分校研发的 Unix 分支版本的衍生风格,这种风格其实不太好描述,我截了一张图,你可以体会一下:
top
另外还有一个和ps能力差不多,但是显示的不是快照而是实时更新数据的top指令。因为自带的top显示的内容有点少, 所以我喜欢用一个叫作htop的指令,我们先看一下使用效果,如下图所示:
管道(Pipeline)
现在你已经掌握了一点点进程的基础,下面我们来学习管道,管道(Pipeline)的作用是在命令和命令之间,传递数据。比如说一个命令的结果,就可以作为另一个命令的输入。我们了解了进程,所以这里说的命令就是进程。更准确地说,管道在进程间传递数据。
输入输出流
每个进程拥有自己的标准输入流、标准输出流、标准错误流。
这几个标准流说起来很复杂,但其实都是文件。
标准输入流(用 0 表示)可以作为进程执行的上下文(进程执行可以从输入流中获取数据)。
标准输出流(用 1 表示)中写入的结果会被打印到屏幕上。
如果进程在执行过程中发生异常,那么异常信息会被记录到标准错误流(用 2 表示)中。
重定向
我们执行一个指令,比如ls -l,结果会写入标准输出流,进而被打印。这时可以用重定向符将结果重定向到一个文件,比如说ls -l > out,这样out文件就会有ls -l的结果;而屏幕上也不会再打印ls -l的结果。
具体来说>符号叫作覆盖重定向;>>叫作追加重定向。>每次都会把目标文件覆盖,>>会在目标文件中追加。比如你每次启动一个程序日志都写入/var/log/somelogfile中,可以这样操作,如下所示:
start.sh >> /var/log/somelogfile
经过这样的操作后,每次执行程序日志就不会被覆盖了。
另外还有一种情况,比如我们输入:
ls1 > out
结果并不会存入out文件,因为ls1指令是不存在的。结果会输出到标准错误流中,仍然在屏幕上。这里我们可以把标准错误流也重定向到标准输出流,然后再重定向到文件。
ls1 &> out
这个写法等价于:
ls1 > out 2>&1
相当于把ls1的标准输出流重定向到out,因为ls1 > out出错了,所以标准错误流被定向到了标准输出流。&代表一种引用关系,具体代表的是ls1 >out的标准输出流。
管道的作用和分类
有了进程和重定向的知识,接下来我们梳理下管道的作用。管道(Pipeline)将一个进程的输出流定向到另一个进程的输入流,就像水管一样,作用就是把这两个文件接起来。如果一个进程输出了一个字符 X,那么另一个进程就会获得 X 这个输入。
管道和重定向很像,但是管道是一个连接一个进行计算,重定向是将一个文件的内容定向到另一个文件,这二者经常会结合使用。
Linux 中的管道也是文件,有两种类型的管道:
1、匿名管道(Unnamed Pipeline),这种管道也在文件系统中,但是它只是一个存储节点,不属于任何一个目录。说白了,就是没有路径。
2、命名管道(Named Pipeline),这种管道就是一个文件,有自己的路径。
FIFO
管道具有 FIFO(First In First Out),FIFO 和排队场景一样,先排到的先获得。所以先流入管道文件的数据,也会先流出去传递给管道下游的进程。
使用场景分析
接下来我们以多个场景举例帮助你深入学习管道。
排序
比如我们用ls,希望按照文件名排序倒序,可以使用匿名管道,将ls的结果传递给sort指令去排序。你看,这样ls的开发者就不用关心排序问题了。
去重
另一个比较常见的场景是去重,比如有一个字典文件,里面都是词语。如下所示:
Apple
Banana
Apple
Banana
如果我们想要去重可以使用uniq指令,uniq指令能够找到文件中相邻的重复行,然后去重。但是我们上面的文件重复行是交替的,所以不可以直接用uniq,因此可以先sort这个文件,然后利用管道将sort的结果重定向到uniq指令。指令如下:
筛选
有时候我们想根据正则模式筛选对应的内容。比如说我们想找到项目文件下所有文件名中含有Spring的文件。就可以利用grep指令,操作如下:
find ./ | grep Spring
find ./递归列出当前目录下所有目录中的文件。grep从find的输出流中找出含有Spring关键字的行。
如果我们希望包含Spring但不包含MyBatis就可以这样操作:
find ./ | grep Spring | grep -v MyBatis
grep -v是匹配不包含 MyBatis 的结果。
数行数
还有一个比较常见的场景是数行数。比如你写了一个 Java 文件想知道里面有多少行,就可以使用wc -l指令,如下所示:
但是如果你想知道当前目录下有多少个文件,可以用ls | wc -l,如下所示:
接下来请你思考一个问题:我们如何知道当前java的项目目录下有多少行代码?
提示一下。你可以使用下面这个指令:
find -i ".java" ./ | wc -l
中间结果
管道一个接着一个,是一个计算逻辑。有时候我们想要把中间的结果保存下来,这就需要用到tee指令。tee指令从标准输入流中读取数据到标准输出流。
这时候,你可能会问: 这不是什么都没做吗?
别急,tee还有一个能力,就是自己利用这个过程把输入流中读取到的数据存到文件中。比如下面这条指令:
find ./ -i "*.java" | tee JavaList | grep Spring
这句指令的意思是从当前目录中找到所有含有 Spring 关键字的 Java 文件。tee 本身不影响指令的执行,但是 tee 会把 find 指令的结果保存到 JavaList 文件中。
tee这个执行就像英文字母中的 T 一样,连通管道两端,下面又开了口。这个开口,在函数式编程里面叫作副作用。