目录

为什么需要文件系统?

程序 = 数据结构 + 算法,所有的应用程序都需要存储和检索信息,进程运行时它能够在内存空间内存储一定量的信息。然而,存储的容量受到进程的用户态虚拟内存空间大小的限制。用户进程的数据需要持久化在拥有更大容量的外存(磁盘)空间。

磁盘(Magnetic disk)一直以来都是持久化数据的设备,理论上,只要有了读写操作就能解决持久化数据存取的问题。但事实上,磁盘需要支持更多的操作,特别是在多程序运行的大型系统上(如服务器)。在这种情况下,很容易产生一些问题,例如:

  • 你如何找到这些信息?
  • 你如何保证一个用户不会读取另外一个用户的数据?
  • 你怎么知道哪些块是空闲的?

为了解决浙西问题,UNIX 设计者提出一个新的抽象 - 文件。在 UNIX-like 操作系统一致秉承着 一切皆文件 的设计理念。

文件(Files)是由进程创建的逻辑信息单元。一个磁盘会包含成百上千万个文件。进程能够读取已存在的文件,并在需要时修改它们。存储在文件中的信息必须是持久的,这也就是说,不会因为进程的创建和终止而受影响。一个文件只能在当用户明确删除的时候才能消失。文件由操作系统进行管理,有关文件的构造、命名、访问、使用、保护、实现和管理方式都是操作系统设计的主要内容。

事实上,如果你能把每个文件都看作一个独立的地址空间,那么你真正理解了文件的概念。操作系统中处理文件的部分称为文件系统(File system)。

Linux 的虚拟文件系统

文件系统是对一个存储设备上的数据和元数据进行组织的机制。由于定义如此宽泛,所以 Linux 文件系统的接口实现采用了分层的体系结构:将用户接口层、文件系统实现和操作存储设备的驱动程序分隔开,从而构成了一个虚拟文件系统(Virtual File System,VFS)。

在同一操作系统下,可能会使用到不同的文件系统。在计算机世界中,任何问题都可以添加一个层级来加个代理来解决。UNIX 操作系统通过虚拟文件系统来将多种文件系统构成一个有序的结构。其关键思想是:抽象出所有文件系统都共有的部分,并将这部分代码放在一层,这一层再调用具体文件系统来管理数据。

Linux 使用最普遍的文件系统是 ExtX,也能够支持 FAT、 FAT32、 VFAT 和 ISO9660 等不同类型的文件系统,从而可以方便地和其它操作系统交换数据。

  • ext2:早期 Linux 中常用的文件系统。
  • ext3:ext2 的升级版,带有日志功能。
  • ext4:较新的文件系统版本。
  • RAMFS:内存文件系统,速度很快。

VFS 隐藏了各种硬件的具体细节,把文件系统操作和不同文件系统的具体实现细节分离了开来,为所有的设备提供了统一的接口,VFS 提供了多达数十种不同的文件系统。VFS 可以分为逻辑文件系统和设备驱动程序:

  • 逻辑文件系统指 Linux 所支持的文件系统,如 extX、FAT 等。
  • 设备驱动程序指为每一种硬件控制器所编写的设备驱动程序模块。

VFS 对用户进程提供一个 “上层” 接口,这个接口就是著名的 POSIX 接口。这些来自用户进程的调用,都是标准的 POSIX 系统调用,比如 open、read、write 和 seek 等。VFS 也有一个对于实际文件系统的 “下层” 接口,包含了许多针对特定文件系统的功能调用。

Linux 操作系统原理 — 文件系统 — 虚拟文件系统_Linux

用户空间包含一些应用程序(例如,文件系统的使用者)和 GNU C 库(glibc),它们为文件系统调用(打开、读取、写和关闭)提供用户接口。系统调用接口的作用就像是交换器,它将系统调用从用户空间发送到内核空间中的适当端点。

VFS 是底层文件系统的主要接口。这个组件导出一组接口,然后将它们抽象到各个文件系统,各个文件系统的行为可能差异很大。有两个针对文件系统对象的缓存(inode 和 dentry)。它们缓存最近使用过的文件系统对象。

Linux 操作系统原理 — 文件系统 — 虚拟文件系统_Linux 操作系统原理_02

当系统启动时,根文件系统在 VFS 中注册。另外,当装载其他文件系统时,不管在启动时还是在操作过程中,它们也必须在 VFS 中注册。另外,在引导时或操作期间挂载其他文件系统时,它们也必须向 VFS 注册。当文件系统注册时,其基本作用是提供 VFS 所需功能的地址列表、调用向量表、或者 VFS 对象。因此一旦文件系统注册到 VFS,它就知道从哪里开始读取数据块。

装载文件系统后就可以使用它了。比如,如果一个文件系统装载到 /usr 并且一个进程调用它:

open("/usr/include/unistd.h", O_RDONLY)

当解析路径时, VFS 看到新的文件系统被挂载到 /usr,并且通过搜索已经装载文件系统的超级块来确定它的超块。然后它找到它所转载的文件的根目录,在那里查找路径 include/unistd.h。然后 VFS 创建一个 vnode 并调用实际文件系统,以返回所有的在文件 inode 中的信息。这个信息和其他信息一起复制到 vnode (内存中)。而这些其他信息中最重要的是指向包含调用 vnode 操作的函数表的指针,比如 read、write 和 close 等。

当 vnode 被创建后,为了进程调用,VFS 在文件描述符表中创建一个表项,并将它指向新的 vnode,最后,VFS 向调用者返回文件描述符,所以调用者可以用它去 read、write 或者 close 文件。

当进程用文件描述符进行一个读操作时,VFS 通过进程表和文件描述符确定 vnode 的位置,并跟随指针指向函数表,这样就调用了处理 read 函数,运行在实际系统中的代码并得到所请求的块。VFS 不知道请求时来源于本地硬盘、还是来源于网络中的远程文件系统、CD-ROM 、USB 或者其他介质,所有相关的数据结构如下图所示:从调用者进程号和文件描述符开始,进而是 vnode,读函数指针,然后是对实际文件系统的访问函数定位。

Linux 操作系统原理 — 文件系统 — 虚拟文件系统_Linux 操作系统原理_03

创建 Linux 文件系统

创建一个经过初始化的文件:

$ dd if=/dev/zero of=file.img bs=1k count=10000
10000+0 records in
10000+0 records out

现在有了一个 10MB 的 file.img 文件。使用 losetup 命令将一个循环设备与这个文件关联起来,让它看起来像一个块设备,而不是文件系统中的常规文件:

$ losetup /dev/loop0 file.img

这个文件现在作为一个块设备出现(由 /dev/loop0 表示)。然后用 mke2fs 在这个设备上创建一个文件系统。这个命令创建一个指定大小的新的 ext2 文件系统:

$ mke2fs -c /dev/loop0 10000
mke2fs 1.35 (28-Feb-2004)
max_blocks 1024000, rsv_groups = 1250, rsv_gdb = 39
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
2512 inodes, 10000 blocks
500 blocks (5.00%) reserved for the super user
...

使用 mount 命令将循环设备(/dev/loop0)所表示的 file.img 文件挂装到挂装点 /mnt/point1。注意,文件系统类型指定为 ext2:

$ mkdir /mnt/point1
$ mount -t ext2 /dev/loop0 /mnt/point1

还可以继续这个过程:在刚才挂装的文件系统中创建一个新文件,将它与一个循环设备关联起来,再在上面创建另一个文件系统。

$ dd if=/dev/zero of=/mnt/point1/file.img bs=1k count=1000
1000+0 records in
1000+0 records out

$ losetup /dev/loop1 /mnt/point1/file.img

$ mke2fs -c /dev/loop1 1000
mke2fs 1.35 (28-Feb-2004)
max_blocks 1024000, rsv_groups = 125, rsv_gdb = 3
Filesystem label=
...

$ mkdir /mnt/point2
$ mount -t ext2 /dev/loop1 /mnt/point2
$ ls /mnt/point2
lost+found
$ ls /mnt/point1
file.img lost+found

通过这个简单的演示很容易体会到 Linux 文件系统(和循环设备)是多么强大。可以按照相同的方法在文件上用循环设备创建加密的文件系统。可以在需要时使用循环设备临时挂装文件,这有助于保护数据。