跳转至

Linux系统更多的文件类型

引言

上一节我们简单介绍了Linux文件系统视角下的文件类型,到目前为止,我们接触到的文件基本只有常规文件和文件夹。这一节我们将介绍Linux的文件系统下一些其他的文件类型,理解这些文件类型的概念对我们使用shell有很多好处。

标准输入输出流

程序输入输出的本质

当我们使用echo输出信息,或者我们在编程的时候使用printf或者print之类的库函数输出信息的时候,有没有想过,这些软件和库函数的底层是怎么实现的?我在第二章的时候曾经说过,向屏幕上输出信息要受到操作系统管理,也就是我们需要向操作系统请求这个服务。

我们很容易能想到的第一种方式是,操作系统可能向我们提供了某种系统调用,可以让我们向屏幕上输出信息。这没错,一些操作系统是这么做的。但是Linux系统的做法要更简单一些,不考虑那些有图形化界面的软件,程序向屏幕输出的信息都是文字,也就是我们要将文字以某种方式写到屏幕上。这个操作本质上和文件读写没有什么区别。

因此Linux操作系统直接重用了文件读写的那一系列系统调用。当然,为了向屏幕输出而不是向普通文件输出,Linux系统为每个程序在启动的时候都准备了几个特殊的文件描述符(第二章的时候介绍过文件描述符),分别是:

  • 0: 标准输入流(standard in, 简称stdin),一般用于获取用户向终端输入的内容。

  • 1: 标准输出流(standard out, 简称stdout),一般用于向屏幕上输出内容。

  • 2: 标准错误流(standard error, 简称stderr),一般用于向屏幕上输出错误信息。

这里,标准输出流和标准错误流默认都是向屏幕上输出信息。但是一般来说,程序向错误提示信息输出到标准错误流上,这样使用本章之后的方法可以将正常的输出信息和错误的输出信息分开。

现在我们可以对第二章的时候我们的那个程序做一点修改

#include <fcntl.h>
#include <unistd.h>

int main()
{
    char buf[20] = "Hello world!";
    write(1, buf, 12);
}
我们省去了打开文件的过程,使用write系统直接向文件描述符为1的文件写入。运行这个程序,我们会发现,我们在屏幕上输出了Hello world!

你是否会好奇,如果我向标准输入流写数据会怎么样?类似的我们还可以问,如果我从标准输出流读数据会怎么样?或许有人会说,都说了叫标准输入流了,肯定不能读吧。但是我建议大家,当遇到这种问题的时候,不要轻易下结论,自己去试一试。不过,我仍然推荐你按照约定的方式使用这些标准输入输出流,否则之后使用shell的重定向和管道之类功能的时候可能会发生意料之外的事情。

在shell中表示标准输入输出文件

shell中我们可以使用-表示标准输入输出文件,这个符号可以被使用在很多命令中。比如cat命令就可以接-表示从标准输入文件中读入,然后把内容输出到标准输出中,因此我们可以使用这样的命令来使用cat

cat test.txt - test.txt
上面这个命令中,cat首先会输出test.txt的内容,然后读取标准输入流中的内容输出到标准输出中,然后再输出一遍test.txt的内容。结合下面我们马上会讲到的输出重定向,我们可以实现将文件的内容和我们输入的内容连接起来并输出到另一个文件中。这也是向文件末尾附加内容的方式。

除了cat,我们甚至可以使用stat查看标准输入输出文件的信息。只需要使用下面这个命令。

stat -

输入输出重定向

简短的前置知识

我们现在其实还有一个问题上面没有提及,上面的代码相比于第二章的代码还有一个区别,就是我们还省去了关闭文件的过程。或许直觉上告诉我们,标准输入输出流怎么能被关掉呢?但是仍然,我建议大家亲自试一试。试一试下面这段C程序。

#include <fcntl.h>
#include <unistd.h>

int main()
{
    close(1);
    int fd = open("./test.txt", O_RDWR);
    write(1, "Close the standard out.", 23);
    close(fd);
}
这段程序首先关闭了文件描述符1,也就是标准输出流的文件描述符,然后以可读可写的方式打开了文件./test.txt,然后我们向文件描述符为1的文件写了一个字符串,最后关闭了我们打开的文件。

如果你运行了这个程序你会发现,屏幕上什么都没有输出,这说明标准输出流确实被我们关闭了。并且如果你用cat检查一下./test.txt这个文件中的内容,你会神奇地发现,我们写的字符串被写入到了这个文件当中。据此我们可以做这样的推断,当我们关闭了标准输出流的文件描述符1的时候,这个文件描述符就被操作系统回收了,之后当我们再次打开其他文件的时候,操作系统会按顺序给我们分配一个文件描述符。因为标准输出流的文件描述符刚好是最小的空闲的文件描述符,所以我们新打开的文件的文件描述符就是1了。

上面这些就是输入输出重定向的基本原理。

最后作为铺垫,还有必要简单介绍一下shell是如何启动一个程序的。Linux系统提供了两个系统调用,分别是forkexec。其中fork的作用是将当前进程(对于操作系统正在运行的程序被称为进程)完全复制一份,包括文件描述符的状态,exec的作用是一个进程里启动一个全新的程序替代当前这个正在运行的程序,但是文件描述符(和其他的一些东西)状态不变。当shell启动一个新的程序的时候,他会先fork复制一次自己,然后设置一些初始状态,然后用exec启动新的程序。也就是在forkexec之间会有一个短暂的时间存在两个shell的进程。

什么是输入输出重定向

那么结合上面两个内容,我们终于可以解释什么是输入输出重定向了。

以输出重定向为例,在shell的进程fork之后,exec之前,它关闭了标准输出流,然后重新打开了一个普通文件。这时,原本用做标准输出的文件描述符1会被分配给一个新的文件,但是exec启动的程序并不知道这件事,因此它还会照例输出,但是它以为会输出到屏幕上的东西现在就都被输出到新打开的这个文件了。这样,标准输出就被重定向到一个普通文件了。标准输入类似,程序以为是接受了用户输入但实际上却是从文件中读取的。

能实现这一点,关键就在于,在linux系统层面,标准输入输出也是一种特殊的文件。

这一小节的内容涉及了比较多操作系统的知识,即使没看懂也没关系,这并不影响使用输入输出重定向。你只需要知道什么是输入输出重定向就可以了。

怎么使用输入输出重定向

bash当中,我们可以使用><来表示输入输出重定向。

我觉得这两个符号很形象,>表示输出重定向(注意,这里会将标准输出流和标准错误流都重定向到同一个文件),后面接一个文件路径,表示输出的内容都被放进这个文件里了,<表示输入重定向,也是后面接一个文件,表示这个文件里的内容被输入进了这个程序。这回,我们终于可以再拿出第二章的时候使用的那个例子来详细解释一下了。

echo "Hello world!" > test.txt
我们前面已经学过了echo将后面的这段字符串输出到屏幕(也就是标准输出流)上,这里我们将输出流重定向到test.txt,也就是将echo原本会输出到屏幕上的内容写入到文件中了。

输入重定向的用法可能会我们想的稍有不同,你可能会想尝试下面这样的命令

echo < test.txt
并且预期这个命令会在屏幕上打印./test.txt中的内容。但是尝试之后我们会发现,并不会这样。这是为什么呢?这是因为标准输入和命令行参数不同。echo的作用是将命令行参数中的内容输出到标准输出,命令行参数是shell在启动echo这个程序的时候传给它的,但是标准输入是程序在运行的时候从文件描述符2中读取的。后面我们会讲到使用xargs命令可以将标准输入转换成命令行参数。

上面就是输入输出重定向的基础用法了。但是讲到这里,我们还有两个问题需要解决。这是输入输出重定向稍微进阶一点的用法。

第一个问题,在尝试过上面输出重定向的用法之后,我们会发现输出重定向的一些特性。首先如果重定向到的文件不存在的话,这个文件会被自动创建。另外,输出的内容会将输出重定向到的文件之前的内容覆盖。这第二个特性有的时候可能会带来一些麻烦。比如,我们想要反复执行一个程序并将它每一次的输出都写入到一个日志文件当中。这种时候,我们可能想要每次输出的时候都向这个文件的末尾继续写入。bash的输出重定向当然支持这种用法,使用>>这个符号即可。例如:

echo -e "\nTo be continue..." >> test.txt

第二个问题,上面我只涉及了标准输出的重定向,但是没有标准错误流的重定向。这是因为标准错误流没有单独专门的重定向方式。但是bash为其他的文件描述符的重定向提供了一种通用的方式,使用n>n>>(注意,没有空格)。使用这个符号表示将文件描述符n重定向到某个文件。但是从是上面我们提到的原理可以知道,这里只有使用0, 1, 2这三个操作系统为我们预先打开的文件描述符才有用。这里还有一种创建的用法,就是n>&mn>>&m也就是将文件描述符n重定向到m。同样的,这里nm只有是0, 1, 2才有用。

管道

前面我们学习和使用了一些shell命令,其中几乎每个命令都是单独使用的。我们前面还说过,shell命令可以看成是一种编程语言。但是我们在使用其他常规的编程语言的时候,我们写的程序通常不止一个语句。因此,我们很容易产生这样的一个问题,我们能否将命令连接起来使用。

这个问题更加准确地来说是,我们能否有某种方式将前一个命令运行的输出用于下一条命令的输入。

其实,利用上面学过的内容,我们已经可以实现这件事了。很容易能想到的一种方案是,将前一个命令的输出重定向到某个临时文件,之后将下一条的命令输入也重定向到同一个文件。但是,这种方案很不方便。首先,这两个命令的需要保证重定向的文件名相同,并且要保证它不跟某个已经存在的文件重名。另外,我们还需要在之后手动删除这个临时文件。

不过这种方案的思路是很好的,也就是我们需要某种临时文件,来保存上一条命令的输入,并且下一条命令可以从中取出这个结果。除此之外它最好是匿名的,并且在使用完成之后会被自动删除。

什么是管道

操作系统为我们提供了这种文件,也就是这一节的主角,管道。管道是一种特殊的文件,它主要有以下几个特点:

  1. 它没有名称,没有路径。

  2. 这个文件有两种文件描述符,一种只能读,一种只能写。(这里我说两种而不是两个,是因为文件描述符可以复制)

  3. 数据在这文件中是先入先出的,也就是先写入的数据总是会先被读出来。

  4. 当这个文件满的时候,写会被阻塞;当文件空的时候,读会被阻塞。

  5. 当这个文件的读和写两种文件描述符都全部被关闭的时候,这个文件会被自动删除。

如果你觉得上面说的这些特点太复杂了,你可以形象地来看这种文件。管道这个名字其实取得相当贴切,它就像一种数据的管道,我们只能从一端把数据放进去然后从另一端把数据取出来。

在使用管道的时候,我们只需要将前一个命令的标准输出流重定向到管道的写入一端,将后一个命令的标准输入流重定向到同一个管道的读取一端。这样数据就像水流一样,从前一条命令流向了后一条命令。

管道的第3条特点让我们可以保证数据的顺序保持不变。管道的第4条特点,让我们可以保证数据一定能到达。管道的第5条特点,让我们无需在之后删除管道。

如何使用管道

shell让我们可以很方便地使用管道。在bash中,我们只需要在两条命令之间使用符号|,就可以表示用管道将两条命令连接起来。现在我们可以再拿出tee的例子了。

现在我们有一个名叫test.txt的文件,我们想把这个文件的内容复制到多个文件当中。我们当然可以使用cat和输出重定向写入文件然后重复多次,但是这种方式有点麻烦。现在我们可以使用tee和管道来实现这个要求。你可以试试下面这个命令

cat test.txt | tee copy1.txt | tee copy2.txt > copy3.txt
这个命令首先使用cattest.txt中的内容复制到标准输出流中,然后使用管道将其复制到了第一个tee的标准输入流中。tee会将这些内容复制到文件copy1.txt它的标准输出流中。然后又有一个管道将这些内容复制到第二个tee的标准输入流中。第二个tee将这些数据复制到文件copy2.txt它的标准输出流中。最后,输出重定向把这些数据复制到了文件copy3.txt中。这个命令还可以无限接续下去,并且经过一点小小的改造还可以让最开始test.txt的内容打印到屏幕上。

在这个例子中,首先我们看到,管道可以连续使用,可以实现一个稍微有些复杂的顺序逻辑。用形象的方式来说的话,就像是数据沿着一个工厂流水线流动。每次经过一个命令,数据就经过了一个工序,最终将数据加工成了我们想要的样子。现在我们学习的命令还比较简单,后面我们将会学习更多可以按需要加工数据的命令。这样,我们利用管道写命令就像是从一些固定的加工模块(也就是命令行程序)中选取并组合成我们想要的流水线。

这里我还能看到,tee命令的作用就像是流水线的三通模块一样,可以将流水线上的数据复制一份出来到某个文件中,同样的数据还可以继续在流水线上流动,参与接下来的步骤。

有名称的管道

很有趣的是,GNU coreutils还给我们提供了可以创造有名称的管道的命令mkfifo(make fifo)。这里fifo就是First In First Out的缩写,也就是管道最重要的特点。用这条命令创建出来的文件拥有管道的2, 3, 4三个特点,但不满足1, 5这两条特点。也就是它拥有名称,并且需要我们手动删掉。

这条命令的基础用法很简单,就是后面使用一个位置参数表示创建的管道的文件路径。因为一般用到的很少,所以这里我们只做一个简单的介绍,感兴趣的同学可以自己去尝试一下。你们可以思考一下,怎样用命令检验它创建的文件是否有3, 4这两个特点(管道的特点2很难用命令行命令来验证)。

小结

这一章我们讲解了Linux系统中的两种特殊的文件,标准输出输出文件和管道,并且介绍了在shell中使用输入输出重定向和使用管道的方式。使用这两种能力,我们就可以将命令组合起来使用完成更加复杂的工作了。至此,初级篇和文件系统相关的内容就全部完成了(别想多了,中级篇还会介绍更多shell和文件系统相关的内容的)。从下一章开始,我们将会开始介绍如何使用shell操作计算机另一项,我们每个人现在都离不开的东西——网络。