Unix有三大抽象:文件对I/O设备的抽象、进程与线程对执行过程的抽象和地址空间对内存的抽象。这篇文章将介绍文件对I/O设备的抽象,后面两种抽象主要放在《操作系统》中讲,不是这门课的重点。

预备知识

文件要长期保存,就必须存储在存储在硬盘这样的非易失性存储器中。以机械硬盘为例,一个硬盘包含多个盘片(记录面),每个记录面划分为若干磁道,每条磁道又可以划分为若干扇区。硬盘最小的读写单位就是一个扇区,通常为512B。

但是,读写硬盘都是很耗时的。如果每次都512B地操作,那么硬盘的读写就会占用大量的时间。所以,又将硬盘分块,块的大小一般为4KB,即8个扇区的大小

这里的物理上的“分块”和虚拟存储器中的“分页”在逻辑上是一一对应的(地址空间对内存的抽象):


Unix文件系统介绍

Unix的文件分为普通文件目录文件特殊文件三种。普通文件包含字节序列,如代码、文本等;目录文件不是标准的ASCII文本文件,它包含了其中的各种文件的信息(包括子目录,套娃);特殊文件包含外部设备的特定信息,如驱动等。一个外设对应一个文件。

文件系统的结构

上一篇中说了Unix的文件结构是下面这样的树形结构:

我们需要知道不同目录的主要功能:

  1. /:系统的根目录,一切从这里开始
  2. /bin:包含系统启动和运行必须的二进制文件(可执行程序)
  3. /boot:Linux内核、系统启动时的加载程序和配置文件等
  4. /dev:存放系统所能识别的所有设备的文件
  5. /etc:存放系统的配置文件
  6. /home:即用户目录。因为Unix是多用户操作系统,不同的用户拥有不同的权限;而每个用户都会在home目录下拥有一个自己的主目录,所以常常限制一般的用户只能在自己的主目录下创建文件,这样避免系统遭到用户行为的破坏。(超级用户的主目录为/root目录)
  7. lib:存放系统核心程序使用的共享库文件。
  8. tmp:存放临时文件,系统重启时失效。
  9. usr:包含了许多其他面向用户的子目录,如/usr/include存储C和C++的头文件,/usr/lib存放程序的(动态)库文件,/usr/bin存放用户使用的Unix程序……

这些目录的功能需要记忆,不要乱用。如果是超级用户特别要注意文件不能乱放,一般放到用户主目录下。

文件的权限

查看文件权限

使用ls -l选项输出文件的信息详细,会发现这样的字段:

image-20220620004719817

这个字段称为文件模式,它主要表示了不同的用户对该文件的访问权限。

文件模式包含10个字符,我们需要将其分为四组:

image-20220620005042490

  • 第一组为第1个字符,表示文件类型。-代表普通文件,d代表目录文件。
  • 第二组为第2-4个字符,表示文件所有者的访问权限。
  • 第三组为第5-7个字符,表示与所有者同一用户组的访问权限。
  • 第四组为第8-10个字符,表示其他用户的访问权限。

首先需要知道各字符的含义

image-20220620010536525

所以drwxr-xr-x表示文件的类型为目录文件,文件所有者的访问权限为可读可写可执行(进入)、用户组的访问权限为可读不可写可进入、其他用户同用户组一样。

然后需要知道用户的类型,在ls -l中也可以看到文件所有者和所在用户组:

image-20220620011347567

更改文件权限

更改文件权限,使用的是chmod命令。需要注意的是只有超级用户或该文件的创建者才有权力更改该文件的权限!

更改文件权限的方式有两种:八进制表示法符号表示法

  1. 八进制表示法

    三个字符的位置分别使用1和0表示有权限和无权限,那么就可以得到一个三位的二进制数,可以把它表示为八进制:

    image-20220620012152647

    1-3的权限实际上是不太合理的,更常使用的数字为0、4、6、7。如chmod 666 a.cpp

  2. 符号表示法

    使用u、g、o、a分别表示文件所有者、用户组、其他用户和所有用户,使用+或-字符表示对指定的用户增加或减去列出的权限。如chmod u+x g-w o-w


文件系统高级操作

要说Unix命令行最酷的一点是什么,我觉得就是重定向与管道。

重定向可以把命令行的标准输入(键盘输入)重定向为从文件中获取内容,也可以把命令行的标准输出结果(显示在屏幕上)重定向到文件中。

管道可以把多个命令串联起来,将前面命令的输出作为后一个命令的输入。

重定向

标准输入与输出

说到标准输入与标准输出,我们首先想到的应该是C语言的scanfprintf函数。scanf的作用是接收来自键盘的输入,printf的作用是将结果输出到屏幕上。

准确来说,scanf从标准输入stdin中按照特定格式读取输入;printf将结果输出到标准输出stdout中,再链接到屏幕上。

Linux中一切皆文件,stdinstdout也一样。它们不断地被输入,然后又不断地被读取,就像流一样,所以又被称为输入输出流。C++的cincout都很好地体现了这一点。

除了标准输入stdin、标准输出stdout外,还有标准错误stderr。它们的文件描述符分别为0、1、2。标准输出和标准输入都是会显示到屏幕上的内容。

标准输出重定向

我们使用ls命令会在屏幕上显示当前目录下的文件,它的原理是将该目录的内容输出到stdin中,再显示到屏幕上。

我们将ls的输出重定向到一个文件中:

image-20220620114548113

这里使用到了>进行输出重定向,>>也能进行输出重定向,它们的区别是是否覆盖写。注意观察下面文件的大小:

image-20220620115322920

容易想到,>重定向每次输入都会覆盖原来的内容,而>>重定向则是将输出添加到文件的末尾。所以根据情况我们可以选择不同的标准输出重定向方式。

标准错误重定向

如果我们ls一个不存在的目录,同时进行标准输出重定向。会发现,屏幕上仍然输出了内容,而重定向的文件却是空的。

image-20220620115833436

不奇怪吗?明明将输出重定向到了该文件中,却仍然输出到了屏幕上同时文件中什么内容都没有。实际上,这种错误信息用到的是另一种“缓冲”:标准错误stdout,它和stdout是相互独立的。如果执行出错,那么就会将stdout的内容显示在屏幕上。

之前说过标准错误的文件描述符为2,所以要将标准错误重定向到文件中,只需要2>2>>就行了。

image-20220620120338119

标准输入重定向

我们先来看一下cat命令,在shell中输入cat。然后我们从键盘上每输入一行,它聚会将输入打印到屏幕上。结合stdinstdout可以知道,当不给cat提供参数时,它默认从stdin获取输入,再将输入送到stdout中。

image-20220620125549267

那么,我们可以使用<cat的标准输入进行重定向,使它输出的是某个文件的内容:

image-20220620130121528

实际上,cat -n a.cpp也可以得到同样的效果:

image-20220620130236345

所以cat < a.cppcat a.cpp的区别是什么呢?我们尝试cat输出一个不存在的文件,会发现,前者是命令行报错,而后者是cat报错。

image-20220620130458191

cat < a.cppcat a.cpp的区别就是前者是shell打开文件,并将其作为标准输入传递给cat;后者是cat直接打开文件。

管道

管道的作用是把多个命令串联起来,将前面命令的标准输出作为后一个命令的标准输入。使用|将命令从左至右连接起来,如cat /usr/lib | less的作用是以分页的方式查看/usr/lib目录下的内容,相当于cat /usr/lib > templess temp

下面主要介绍一些管道的应用,希望能够抛砖引玉。

结果过滤

我们可以将结果进行一系列处理后再查看。

  1. ls /usr/bin | sort -r | less将/usr/lib目录下的内容按照字母倒序的顺序分页查看:

    image-20220620133611222

  2. ls -r | uniq | less将当前目录和子目录下的内容去重后以分页的形式查看:

    image-20220620133926401

结果搜索

第一篇中介绍的grep [选项] [搜索的字符串] [文件]可以从指定文件中搜索匹配的行。结合管道可以实现对结果的搜索。如搜索/usr/include中带std的头文件:

image-20220620134511286

管道的应用非常多,但因为我的知识水平有限,目前就想到这么多。遇到具体的情况再具体分析吧,使用管道是一种非常好的思路。


深入文件系统

什么是索引节点

本篇的最前面介绍了硬盘的基本知识,我们知道了文件数据都存储在硬盘上的一个个块中。除了存储文件本身的数据,还需要一个数据结构来存储文件的元信息,如文件的所有者、文件的创建日期、大小等。这个数据结构就是inode,即索引结点

从“索引”和“结点”两个词我们就可以猜得八九不离十了:Unix使用树(堆)的结构来记录索引,索引中的数据以键值对的形式存储。通过索引可以找到文件数据在硬盘中的位置。

我们可以使用stat [文件名]来查看该文件名对应的inode的信息:

image-20220620141621785

软硬链接

这里的链接类似于Windows上的快捷方式。Unix链接的实现有硬链接和软链接两种方式,它们的原理和特点不同。

硬链接

Unix系统的一个文件对应一个inode,但inode和文件名可以是一对多的关系。这意味着我们可以创建多个文件名都指向该inode,使用这些文件名都可以访问到同一个索引结点,进而访问同一个文件。

使用ln [源文件名] [硬链接文件名]可以创建一个硬链接:

image-20220620143024751

现在使用a.cpp和使用b.cpp都是一样的,因为它们都指向了同一个inode。甚至删除原来的a.cpp,依然能使用b.cpp访问文件,二者地位是平等的:

image-20220620143206770

软链接

软链接和原来文件的inode不同,但是软链接的内容是原文件的路径。访问软链接时将会根据路径自动访问原文件。

使用ln -s [源文件名] [软链接文件名]可以创建一个软链接:

image-20220620144043753

如果删除了a.cpp,那么访问b.cpp就会报错:

image-20220620144141133

探讨文件操作的原理

通过前面的学习我们知道了什么是inode,然后使用软链接和硬链接加深了对inode的理解。现在,我们结合inode来进一步探讨文件创建、删除、移动等操作的原理。

创建文件

创建文件就是为该文件的数据分配一块硬盘的空间,同时将文件的元信息存储在对应的inode中。由于创建文件的时候指定了一个文件名,所以再将该文件名硬链接到该inode结点即可。

删除文件

inode的link数减到0时(即没有文件名指向该inode结点了),文件无法被访问,相当于删除。

以后创建文件时如果发现一个inode的link数为0,那么就可以把它以及文件数据的空间覆盖掉了。

移动/重命名文件

移动文件或重命名文件,只是改变文件名(创建一个硬链接,再删除原文件名),不影响inode号码。

特别要注意的是移动操作,它并不会进行数据的拷贝。我们平时Window用的比较多,它将不同的磁盘挂载到不同区,不同区间的移动需要进行数据的拷贝;而Unix不同,整个文件系统就是一颗树,所以无需进行繁琐的文件拷贝操作。

目录的本质

前面说过,目录包含了其中的各种文件的信息。实际上,这些信息并没有保存在目录中。目录只是维护了文件名和对应inode的映射关系。

目录就相当于一个map,它包含了文件名和inode号的键值对。通过文件名这个键得到inode号这个值,然后访问到inode,进一步访问到文件数据本身。