14.1 设备专用文件(设备文件)
设备专用文件与系统的某个设备相对应。在内核中,每种设备类型都有与之相对应的设备驱动程序,用来处理设备的所有 I/O 请求。设备驱动程序属内核代码单元,可执行一系列操作,(通常)与相关硬件的输入/输出动作相对应。由设备驱动程序提供的 API 是固定的,包含的操作对应于系统调用 open()、close()、read()、write()、mmap()以及 ioctl()。每个设备驱动程序所提供的接口一致,这隐藏了每个设备在操作方面的差异,从而满足了 I/O 操作的通用性。
可将设备划分为以下两种类型。
- 字符型设备基于每个字符来处理数据。终端和键盘都属于字符型设备。
- 块设备则每次处理一块数据。块的大小取决于设备类型,但通常为 512 字节的倍数。磁盘和磁带设备都属于块设备。
与其他类型的文件一样,设备文件总会出现在文件系统中,通常位于/dev 目录下。超级用户可使用 mknod 命令创建设备文件,特权级程序(CAP_MKNOD)执行 mknod()系统调用亦可完成相同任务。
在 Linux 的早期版本中,/dev 包含了系统中所有可能设备的条目,即使某些设备实际并未与系统连接。这意味着/dev 会包含数以千计的未用设备项,从而导致了两个缺点:其一,对于需要扫描该目录内容的应用而言,降低了程序的执行速度;其二,根据该目录下的内容无法发现系统中实际存在哪些设备。Linux2.6 运用 udev 程序解决了上述问题。该程序所依赖的 sysfs文件系统,是装载于/sys 下的伪文件系统,将设备和其他内核对象的相关信息导出至用户空间。
设备 ID
每个设备文件都有主、辅 ID 号各一。主 ID 号标识一般的设备等级,内核会使用主 ID 号查找与该类设备相应的驱动程序。辅 ID 号能够在一般等级中唯一标识特定设备。命令 ls –l可显示出设备文件的主、辅 ID。
设备文件的 i 节点中记录了设备文件的主、辅 ID。每个设备驱动程序都会将自己与特定主设备号的关联关系向内核注册,藉此建立设备专用文件和设备驱动程序之间的关系。内核是不会使用设备文件名来查找驱动程序的。
14.2 磁盘和分区
常规文件和目录通常都存放在硬盘设备里。(其他设备也能存放文件和目录,比如,CD-ROM、flash 内存卡以及虚拟磁盘等,但这里主要关注的是硬盘设备。)下面几节会介绍磁盘的组织方式,以及如何对其分区。
14.2.1 磁盘驱动器
硬盘驱动器是一种机械装置,由一个或多个高速旋转(每分钟旋转数以千计)的盘片组成。通过在磁盘上快速移动的读/写磁头,便可获取/修改磁盘表面的磁性编码信息。磁盘表面信息物理上存储于称为磁道(track)的一组同心圆上。磁道自身又被划分为若干扇区,每个扇区则包含一系列物理块。物理块的容量一般为 512 字节(或 512 的倍数),代表了驱动器可读/写的最小信息单元。
14.2.2 磁盘分区
可将每块磁盘划分为一个或多个(不重叠的)分区。内核则将每个分区视为位于/dev 路径下的单独设备。
系统管理员可使用 fdisk 命令来决定磁盘分区的编号、大小和类型。命令 fdisk –l 会列出磁盘上的所有分区。Linux 专有文件/proc/partitions 记录了系统中每个磁盘分区的主辅设备编号、大小和名称。
磁盘分区可容纳任何类型的信息,但通常只会包含以下之一。
- 文件系统:用来存放常规文件。
- 数据区域:可做为裸设备对其进行访问(一些数据库管理系统会使用该技术)。
- 交换区域:供内核的内存管理之用。
14.3 文件系统
文件系统是对常规文件和目录的组织集合。用于创建文件系统的命令是 mkfs。
Linux 的强项之一便是支持种类繁多的文件系统,如下所示。
- 传统的 ext2 文件系统。
- 各种原生(native)UNIX 文件系统,比如,Minix、System V 以及 BSD 文件系统。
- 微软的 FAT、FAT32 以及 NTFS 文件系统。
- ISO 9660 CD-ROM 文件系统。
- Apple Macintosh 的 HFS。
- 一系列网络文件系统,包括广为使用的 SUN NFS、IBM 和微软的 SMB、Novell NCP 以及 Carnegie Mellon 大学开发的 Coda 文件系统。
- 一系列日志文件系统,包括 ext3、ext4、Reiserfs、JFS、XFS 以及 Btrfs。
从 Linux 的专有文件/proc/filesystems 中可以查看当前为内核所知的文件系统类型。
文件系统结构
在文件系统中,用来分配空间的基本单位是逻辑块,亦即文件系统所在磁盘设备上若干连续的物理块。例如,在 ext2 文件系统上,逻辑块的大小为 1024、2048 或 4096 字节。
文件系统由以下几部分组成。
-
引导块:总是作为文件系统的首块。引导块不为文件系统所用,只是包含用来引导操作系统的信息。操作系统虽然只需一个引导块,但所有文件系统都设有引导块(其中的绝大多数都未使用)。
-
超级块:紧随引导块之后的一个独立块,包含与文件系统有关的参数信息,其中包括:
- i 节点表容量;
- 文件系统中逻辑块的大小;
- 以逻辑块计,文件系统的大小;
驻留于同一物理设备上的不同文件系统,其类型、大小以及参数设置(比如,块大小)都可以有所不同。这也是将一块磁盘划分为多个分区的原因之一。
-
i 节点表:文件系统中的每个文件或目录在 i 节点表中都对应着唯一一条记录。这条记录登记了关乎文件的各种信息。下一节会深入讨论 i 节点。有时也将i 节点表称为 i-list。
-
数据块:文件系统的大部分空间都用于存放数据,以构成驻留于文件系统之上的文件和目录。
14.4 i 节点
针对驻留于文件系统上的每个文件,文件系统的 i 节点表会包含一个 i 节点(索引节点的简称)。对 i 节点的标识,采用的是 i 节点表中的顺续位置,以数字表示。文件的 i 节点号(或简称为 i 号)是 ls –li 命令所显示的第一列。i 节点所维护的信息如下所示。
- 文件类型(比如,常规文件、目录、符号链接,以及字符设备等)。
- 文件属主(亦称用户 ID 或 UID)。
- 文件属组(亦称为组 ID 或 GID)。
- 3 类用户的访问权限:属主(有时也称为用户)、属组以及其他用户(属主和属组用户之外的用户)。
- 3 个时间戳:对文件的最后访问时间(ls –lu 所显示的时间)、对文件的最后修改时间(也是 ls –l 所默认显示的时间),以及文件状态的最后改变时间(ls –lc 所显示的最后改变 i 节点信息的时间)。值得注意的是,与其他 UNIX 实现一样,大多数 Linux 文件系统不会记录文件的创建时间。
- 指向文件的硬链接数量。
- 文件的大小,以字节为单位。
- 实际分配给文件的块数量,以 512 字节块为单位。这一数字可能不会简单等同于文件的字节大小,因为考虑文件中包含空洞的情形,分配给文件的块数可能会低于根据文件正常大小(以字节为单位)所计算出的块数。
- 指向文件数据块的指针。
ext2 中的 i 节点和数据块指针
类似于大多数 UNIX 文件系统,ext2 文件系统在存储文件时,数据块不一定连续,甚至不一定按顺序存放(尽管 ext2 会尝试将数据块彼此靠近存储)。为了定位文件数据块,内核在 i 节点内维护有一组指针。
无需连续存储文件块,使得文件系统对磁盘空间的利用更为高效。特别是,还能降低空闲磁盘空间的碎片化程度,即因众多不连续空闲磁盘碎片(因其空间太小而无法使用)而导致的磁盘空间浪费。换言之,对空闲磁盘空间的高效利用,是以已分配磁盘空间中文件的碎片化为代价的。
在 ext2 中,每个 i 节点包含 15 个指针。其中的前 12 个指针(图 14-2 中编号为 0~11 的指针)指向文件前 12 个块在文件系统中的位置。接下来,是一个指向指针块的指针,提供了文件的第 13 个以及后续数据块的位置。指针块中指针的数量取决于文件系统中块的大小。每个指针需占用 4 字节,因此指针的数量可能在 256(块容量为 1024 字节)~1024(块容量为4096 字节)之间。这样就考虑了大型文件的情况。即便是对于巨型文件,第 14 个指针(图中编号为 13)是一个双重间接指针 — 指向指针块,其块中指针进而指向指针块,此块中指针最终才指向文件的数据块。只要有体量巨大的文件,就会随之产生更深一层的递进:图中 i节点的最后一个指针属于三重间接指针。
这一貌似复杂的系统,其设计意图是为了满足多重需求。首先,该系统在维持 i 节点结构大小固定的同时,支持任意大小的文件。其次,文件系统既可以以不连续方式来存储文件块,又可通过 lseek()随机访问文件,而内核只需计算所要遵循的指针。最后,对于在大多数系统中占绝对多数的小文件而言,这种设计满足了对文件数据块的快速访问:通过 i 节点的直接指针访问,一击必中。
14.5 虚拟文件系统(VFS)
Linux 所支持的各种文件系统,其实现细节均不相同。举例来说,这些差异包括文件块的分配方式,以及目录的组织方式。如果每个与文件打交道的程序都需要理解各种文件系统的具体细节,那么编写与各类文件系统交互的程序将近乎于不可能完成的任务。虚拟文件系统(VFS,有时也称为虚拟文件交换)是一种内核特性,通过为文件系统操作创建抽象层来解决上述问题。
- VFS 针对文件系统定义了一套通用接口。所有与文件交互的程序都会按照这一接口来进行操作。
- 每种文件系统都会提供 VFS 接口的实现。
这样一来,程序只需理解 VFS 接口,而无需过问具体文件系统的实现细节。VFS 接口的操作与涉及文件系统和目录的所有常规系统调用相对应,这些系统调用有open()、read()、write()、lseek()、close()、truncate()、stat()、mount()、umount()、mmap()、mkdir()、link()、unlink()、symlink()以及 rename()。
14.6 日志文件系统
ext2 文件系统在系统崩溃之后,为确保文件系统的完整性,重启时必须对文件系统的一致性进行检查(fcsk)。一致性检查需要遍历整个文件系统。如果文件系统较小,只需几秒或几分钟便可完成。而在大型文件系统上,上述操作可能会历时数小时,这对于需要保持高可用性的系统来说(比如,网络服务器),情况就非常严重。
采用日志文件系统,则无需在系统崩溃后对文件进行漫长的一致性检查。在实际更新元数据之前,日志文件系统会将这些更新操作记录于专用的磁盘日志文件中。对元数据更新的记录是按其相关性分组(以事务的方式记录)进行的。在事务处理过程中,一旦系统崩溃,系统重启时便可利用日志重做(redo)任何不完整的更新,同时为文件系统恢复一致性状态。(借用数据库的说法,日志文件系统能够确保总是将文件元数据事务作为一个完整单元来提交。)系统崩溃之后,即便是超大型的日志文件系统,通常也会在几秒之内复原,因而对于有高可用性需求的系统极具吸引力。
14.7 单根目录层级和挂载点
与其他 UNIX 系统一样,Linux 上所有文件系统中的文件都位于单根目录树下,树根就是根目录“/”。其他的文件系统都挂载在根目录之下,被视为整个目录层级的子树(subtree)。
在 Linux 系统中,最常见的目录以及所对应的存放内容如表
目录名称 | 应放置文件的内容 |
---|---|
/boot | 开机所需文件 — 内核、开机菜单以及所需配置文件等 |
/dev | 以文件形式存放任何设备与接口 |
/etc | 配置文件 |
/home | 用户主目录 |
/bin | 存放单用户模式下还可以操作的命令 |
/lib | 开机时用到的函数库,以及/bin 与/sbin 下面的命令要调用的函数 |
/sbin | 开机过程中需要的命令 |
/media | 用于挂载设备文件的目录 |
/opt | 放置第三方的软件 |
/root | 系统管理员的家目录 |
/srv | 一些网络服务的数据文件目录 |
/tmp | 任何人均可使用的“共享”临时目录 |
/proc | 虚拟文件系统,例如系统内核、进程、外部设备及网络状态等 |
/usr/local | 用户自行安装的软件 |
/usr/sbin | Linux 系统开机时不会使用到的软件/命令/脚本 |
/usr/share | 帮助与说明文件,也可放置共享文件 |
/var | 主要存放经常变化的文件,如日志 |
/lost+found | 当文件系统发生错误时,将一些丢失的文件片段存放在这里 |
超级用户可使用如下命令来挂载文件系统:
$ mount device directory
这条命令会将名为 device 的文件系统挂接到目录层级中由 directory 所指定的目录,即文件系统的挂载点。可使用 unmount 命令卸载文件系统,然后在另一个挂载点再次挂载文件系统,从而改变文件系统的挂载点。不带任何参数来执行 mount 命令,可以列出当前已挂载的文件系统.
14.8 文件系统的挂载和卸载
系统调用 mount()和 umount()运行特权级进程(CAP_SYS_ADMIN)以挂载或卸载文件系统。在讨论这两个系统调用之前,需要先了解以下 3 个文件,其中包含了当前已挂载或可挂载的文件系统信息。
- 通过 Linux 专有的虚拟文件/proc/mounts,可查看当前已挂载文件系统的列表。/proc/mounts 是内核数据结构的接口,因此总是包含已挂载文件系统的精确信息。随着引入了前述的每进程挂载命名空间特性,如今,每个进程都拥有一个/proc/PID/mounts 文件,其中会列出组成进程挂载空间的挂载点,而/proc/mounts 只是指向/proc/self/mounts 的符号链接。
- mount(8)和 umount(8)命令会自动维护/etc/mtab 文件,该文件所包含的信息与/proc/mounts 的内容相类似,只是略微详细一些。特别是,etc/mtab 包含了传递给mount(8)的文件系统专有选项,这并未在/proc/mounts 中出现。但是,因为系统调用mount()和 umount()并不更新/etc/mtab,如果某些挂载或卸载了设备的应用程序没有更新该文件,那么/etc/mtab 可能会变得不准确。
- /etc/fstab(由系统管理员手工维护)包含了对系统支持的所有文件系统的描述,该文件可供 mount(8)、umount(8)以及 fsck(8)所用。
如/proc/mounts 中的一条记录(一行):
/dev/sda9 /boot ext3 rw 0 0
这条记录包含了 6 个字段。
- 已挂载设备名。
- 设备的挂载点。
- 文件系统类型。
- 挂载标志。上例的 rw 表示以可读写方式挂载文件系统。
- 一个数字,dump(8)会使用其来控制对文件系统的备份操作。只有/etc/fstab 文件才会用到该字段和第 6 个字段,在/proc/mounts 和/etc/mtab 中,该字段总是为 0。
- 一个数字,在系统引导时,用于控制 fsck(8)对文件系统的检查顺序。getfsent(3)和 getmntent(3)手册页记录了用于从上述文件中读取记录的函数。
14.8.1 挂载文件系统:mount()
mount()系统调用将由 source 指定设备所包含的文件系统,挂载到由 target 指定的目录下。
#include <sys/mount.h>
int mount(const char *source, const char *target, const char *fstype, unsigned long mountflags, const void *data);
/* 成功返回0,失败返回-1 */
除了将磁盘文件系统挂载到一目录下之外,mount()还可以执行其他任务。
参数 fstype 是一字符串,用来标识设备所含文件系统的类型,比如,ext4 或 btrfs。
参数 mountflags 为一位掩码,通过对下表中所示的 0 个或多个标志进行或(OR)操作而得出。
标记 | 用途 |
---|---|
MS_BIND | 建立绑定挂载,mount()会忽略fstype、data 参数,以及 mountflags 中除 MS_REC 之外的标志 |
MS_DIRSYNC | 同步更新路径,采用 MS_DIRSYNC 标志的应用程序在确保同步更新目录(比如,open(pathname,O_CREAT)、rename()、link()、unlink()、symlink()以及 mkdir())的同时,还无需消耗同步更新文件所带来的成本。 |
MS_MANDLOCK | 允许对该文件系统中的文件强行锁定记录 |
MS_MOVE | 以原子操作将挂载点移到新位置 |
MS_NOATIME | 不更新文件的最后访问时间,使用该标志意在消除额外的磁盘访问,避免在每次访问文件时都去更新文件 i 节点。对某些应用程序来说,维护这一时间戳意义不大,而放弃这一做法还能显著提升性能 |
MS_NODEV | 不允许访问设备,不允许访问此文件系统上的块设备和字符设备。设计这一特性的目的是为了保障系统安全,规避如下情况:假设用户插入了可移动磁盘,而磁盘中又包含了可随意访问系统的设备专有文件。 |
MS_NODIRATIME | 不更新目录的最后访问时间,与MS_NOATIME类似 |
MS_NOEXEC | 不允许在此文件系统上执行程序(或脚本)。该标志用于文件系统包含非 Linux 可执行文件的场景。 |
MS_NOSUID | 禁用 set-user-ID 和 set-group-ID 程序,这属于安全特性,意在防止用户从可移动磁盘上运行 set-user-ID 和 set-group-ID 程序。 |
MS_RDONLY | 以只读方式挂载,不能修改或创建文件,也不能修改现有文件。 |
MS_REC | 递归挂载,以递归方式将挂载动作施之于子树下的所有挂载。 |
MS_RELATIME | 只有当最后访问时间早于最后修改时间或最后状态变更时间时,才对前者进行更新 |
MS_REMOUNT | 使用新的 mountflags 和 data 重新挂载 |
MS_STRICTATIME | 总是更新最后访问时间 |
MS_SYNCHRONOUS | 使得所有文件和目录同步更新 |
mount()的最后一个参数 data 是一个指向信息缓冲区的指针,对其信息的解释则取决于文件系统。就大多数文件系统而言,该参数是一字符串,包含了以逗号分隔的选项设置。
程序示例:使用 mount()
#include <sys/mount.h>
#include "tlpi_hdr.h"
static void
usageError(const char *progName, const char *msg)
{
if (msg != NULL)
fprintf(stderr, "%s", msg);
fprintf(stderr, "Usage: %s [options] source target\n\n", progName);
fprintf(stderr, "Available options:\n");
#define fpe(str) fprintf(stderr, " " str) /* Shorter! */
fpe("-t fstype [e.g., 'ext2' or 'reiserfs']\n");
fpe("-o data [file system-dependent options,\n");
fpe(" e.g., 'bsdgroups' for ext2]\n");
fpe("-f mountflags can include any of:\n");
#define fpe2(str) fprintf(stderr, " " str)
fpe2("b - MS_BIND create a bind mount\n");
fpe2("d - MS_DIRSYNC synchronous directory updates\n");
fpe2("l - MS_MANDLOCK permit mandatory locking\n");
fpe2("m - MS_MOVE atomically move subtree\n");
fpe2("A - MS_NOATIME don't update atime (last access time)\n");
fpe2("V - MS_NODEV don't permit device access\n");
fpe2("D - MS_NODIRATIME don't update atime on directories\n");
fpe2("E - MS_NOEXEC don't allow executables\n");
fpe2("S - MS_NOSUID disable set-user/group-ID programs\n");
fpe2("r - MS_RDONLY read-only mount\n");
fpe2("c - MS_REC recursive mount\n");
fpe2("R - MS_REMOUNT remount\n");
fpe2("s - MS_SYNCHRONOUS make writes synchronous\n");
exit(EXIT_FAILURE);
}
int
main(int argc, char *argv[])
{
unsigned long flags;
char *data, *fstype;
int j, opt;
flags = 0;
data = NULL;
fstype = NULL;
/* getopt()函数用于分析命令行参数,argc, argv与main函数相同,分别代表参数个数和内容,
"o:t:f:"表示-o,-t,-f三个选项,':'的意思是其后面必须紧跟一个参数*/
while ((opt = getopt(argc, argv, "o:t:f:")) != -1) {
switch (opt) {
case 'o': /* 提取参数-o后面的参数data */
data = optarg;
break;
case 't': /* 提取-t后面的参数fstype */
fstype = optarg;
break;
case 'f': /* 提取-f后面的参数flags */
for (j = 0; j < strlen(optarg); j++) {
switch (optarg[j]) {
case 'b': flags |= MS_BIND; break;
case 'd': flags |= MS_DIRSYNC; break;
case 'l': flags |= MS_MANDLOCK; break;
case 'm': flags |= MS_MOVE; break;
case 'A': flags |= MS_NOATIME; break;
case 'V': flags |= MS_NODEV; break;
case 'D': flags |= MS_NODIRATIME; break;
case 'E': flags |= MS_NOEXEC; break;
case 'S': flags |= MS_NOSUID; break;
case 'r': flags |= MS_RDONLY; break;
case 'c': flags |= MS_REC; break;
case 'R': flags |= MS_REMOUNT; break;
case 's': flags |= MS_SYNCHRONOUS; break;
default: usageError(argv[0], NULL);
}
}
break;
default:
usageError(argv[0], NULL);
}
}
if (argc != optind + 2)
usageError(argv[0], "Wrong number of arguments\n");
if (mount(argv[optind], argv[optind + 1], fstype, flags, data) == -1)
errExit("mount");
exit(EXIT_SUCCESS);
}
$ su
Password:
mkdir /testfs
./t_mount -t ext2 -o bsdgroups /dev/sda12 /testfs
cat /proc/mount | grep sda12
/dev/sda12 /testfs ext3 rw 0 0
grep sda12 /etc/mtab
# 由于该程序并未更新/etc/mtab,所以上面的 grep 命令并未产生任何输出
# 继续以只读方式重新挂载文件系统
./t_mount -f Rr /dev/sda12 /testfs
cat /proc/mount | grep sda12
/dev/sda12 /testfs ext3 ro 0 0
# 最后再将挂载点移动至目录层级内的新位置
mkdir /demo
./t_mount -f m /testfs /demo
./t_mount -f Rr /dev/sda12 /testfs
/dev/sda12 /demo ext3 ro 0 0
14.8.2 卸载文件系统:umount()和 umount2()
umount()系统调用用于卸载已挂载的文件系统。
#include <sys/mount.h>
int umount(const char *target); /* 成功返回0,失败返回-1 */
target 参数指定待卸载文件系统的挂载点。
无法卸载正在使用中的(busy)文件系统,意即这一文件系统上有文件被打开,或者进程的当前工作目录驻留在此文件系统下。针对使用中的文件系统调用 umount(),系统会返回EBUSY 错误。
系统调用 umount2()是 umount()的扩展版。通过 flags 参数,umount2()可对卸载操作施以更精密的控制。
#include <sys/mount.h>
int umount(const char *target,int flags); /* 成功返回0,失败返回-1 */
标志位掩码参数flags由下列 0 个或多个值相或(OR)而成。
- MNT_DETACH。执行 lazy 卸载。对挂载点加以标记,一方面允许已使用了挂载点的进程得以继续使用,同时禁止任何其他进程对该挂载点发起新的访问。当所有进程不再使用访问点时,系统会卸载相应的文件系统。
- MNT_EXPIRE。将挂载点标记为到期(expired)。若首次调用 umount2()时指定了该标志,且挂载点处于空闲状态,则该调用将以失败告终,并返回 EAGAIN 错误,同时将挂载点标记为到期。(如果挂载点处于在用状态,那么调用也将失败,并返回 EBUSY 错误,但不会将挂载点标记为到期。)只要无任何后续进程发起对挂载点的访问,该挂载点便会一直保持到期状态。再度调用umount2()时,如指定 MNT_EXPIRE 标志,将卸载到期的挂载点。这就提供了一种机制,以卸载在某段时间内未用的文件系统。该标志不能与 MNT_DETACH 或 MNT_FORCE 标志一并使用。
- MNT_FORCE。即便文件系统(只对 NFS 挂载有效)处于在用状态,依然将其强行卸载。采用这一选项能会造成数据丢失。
- UMOUNT_NOFOLLOW。
- 若 target 为符号链接,则不对其进行解引用。该标志专为某些 set-user-ID-root 程序而设计 — 此类程序允许非特权级用户执行卸载操作,意在避免安全性问题的发生(例如,若 target为符号链接,且被改变以指向另外的位置)
14.9 高级挂载特性
14.9.1 在多个挂载点挂载文件系统
可以将一个文件系统挂载于文件系统内的多个位置。由于每个挂载点下的目录子树内容都相同,在一个挂载点下对目录子树所做的改变,同样可见诸于其他挂载点
$ su
Password:
mkdir /testfs
mkdir /demo
mount /dev/sda12 /testfs
mount /dev/sda12 /demo
mount | grep sda12
/dev/sda12 on /testfs type ext3 (rw)
/dev/sda12 on /demo type ext3 (rw)
# 在挂载点一(/testfs)下对目录子树所做的改变,在挂载点二(/demo)下完全可见
touch /testfs/myfile
ls /demo
myfile
14.9.2 多次挂载同一挂载点
Linux 允许针对同一挂载点执行多次挂载。每次新挂载都会隐藏之前可见于挂载点下的目录子树。卸载最后一次挂载时,挂载点下上次挂载的内容会再次显示
$ su
Password:
mount /dev/sda12 /testfs
touch /testfs/myfile
mount /dev/sda13 /testfs
mount | grep testfs
/dev/sda12 on /testfs type ext3 (rw)
/dev/sda13 on /demo type reiserfs (rw)
touch /testfs/newfile
ls /testfs
newfile
umount /testfs
mount | grep testfs
/dev/sda12 on /testfs type ext3 (rw)
ls /testfs
myfile
在现有且在用的挂载点上执行新的挂载操作是此类堆叠挂载的用法之一。持有打开文件描述符的进程、建立 chroot 监禁区(jail)的进程,以及工作目录位于老挂载点之下的进程将继续在旧有挂载下运行,而针对挂载点发起新访问的进程将使用新挂载。结合 MNT_DETACH标志下的 unmount 操作,则无需将文件系统置为单用户模式,即可为其提供平滑迁移。
14.9.3 基于每次挂载的挂载标志
绑定挂载(由使用 MS_BIND 标志的 mount()调用来创建)是指在文件系统目录层级的另一处挂载目录或文件。这将导致文件或目录在两处同时可见。绑定挂载有些类似于硬链接,但存在两个方面的差异。
- 绑定挂载可以跨越多个文件系统挂载点,甚至不拘于 chroot 监禁区(jail)。
- 可针对目录执行绑定挂载。
cat > f1
Chance is always powerful. Let your hook be always cast.
touch f2
mount --bind f1 f2
mount | grep f1
/testfs/f1 on /testfs/f2 type none (rw,bind)
cat >> f2
In the pool where you least expect it, will be a fish.
cat f1
Chance is always powerful. Let your hook be always cast.
In the pool where you least expect it, will be a fish.
rm f2
rm: cannot unlink 'f2': Device or resource busy
umount f2
rm f2
14.9.4 递归绑定挂载
默认情况下,如果使用 MS_BIND 为某个目录创建了绑定挂载,那么只会将该目录挂载到新位置。假设源目录下还存在子挂载(submount),则不会将这些子挂载复制到挂载 target 之下。Linux 2.4.11 添加了 MS_REC 标志,若与 MS_BIND 相或(OR)并作为标志参数的一部分传入 mount(),则会将子挂载复制到挂载目标下,此之谓递归绑定挂载。采用 mount(8)命令所提供的–rbind 选项,可在 shell 中完成相同任务,参见如下 shell 会话。
mkdir top1
mkdir src1
touch src1/aaa
mount --bind src1 top1
mkdir top1/sub
mkdir src2
touch src2/bbb
mount --bind src2 top1/sub
find top1
top1
top1/aaa
top1/sub
top1/sub/bbb
# 若采用非递归操作,新挂载不会复制子挂载
mkdir dir1
mount --bind top dir1
find dir1
dir1
dir1/aaa
dir1/sub
# 创建递归挂载
mkdir dir2
mount --rbind top dir2
find dir2
dir2
dir2/aaa
dir2/sub
dir2/sub/bbb
14.10 虚拟内存文件系统:tmpfs
在 Linux 上,已经开发出了林林总总基于内存的文件系统。迄今为止,其中最为复杂的则非 tmpfs 文件系统莫属,该系统在 Linux 2.4 中首度出现。较之于其他基于内存的文件系统,其独特之处在于它属于虚拟内存文件系统。这意味着,该文件系统不但使用 RAM,而且在RAM 耗尽的情况下,还会利用交换空间。tmpfs 文件系统是一个 Linux 内核的可选组件,通过 CONFIG_TMPFS 选项加以配置。
要创建 tmpfs 文件系统,请使用如下形式的命令:
mount -t tmpfs source target
其中“source”可以是任意名称,其唯一的意义是在/proc/mounts 中显示,并通过mount 和 df 命令显示出来。与往常一样,target 是该文件系统的挂载点。请注意,无需使用mkfs 预先创建一个文件系统,内核会将此视为 mount()系统调用的一部分自动加以执行。
作为使用 tmpfs 的例子之一,可采用堆叠挂载(无需顾忌/tmp 目录目前是否处于在用状态),创建一 tmpfs 文件系统并将其挂载至/tmp,如下所示:
mount -t tmpfs newmp /tmp
cat /proc/mounts | grep tmp
new /tmp tmpfs rw 0 0
有时,会使用如上命令(或/etc/fstab 中的等价条目)来改善应用程序(比如,编译器)的性能,此类应用程序因创建临时性文件而频繁使用/tmp 目录。
默认情况下,允许将 tmpfs 文件系统的大小提高至 RAM 容量的一半,但在创建文件系统或之后重新挂载时,可使用 mount 的 size=nbytes 选项为该文件系统的大小设置不同的上限值。(tmpfs 文件系统仅会根据其当前所持有的文件来消耗内存和交换空间。)
一旦卸载 tmpfs 文件系统,或者遭遇系统崩溃,那么该文件系统中的所有数据都将丢失,
除了用于用户应用程序以外,tmpfs 文件系统还有以下两个特殊用途。
- 由内核内部挂载的隐形 tmpfs 文件系统,用于实现 System V 共享内存和共享匿名内存映射。
- 挂载于/dev/shm的tmpfs 文件系统,为glibc用以实现POSIX 共享内存和POSIX 信号量。
14.11 获得与文件系统有关的信息:statvfs()
**statvfs()**和 fstatvfs()库函数能够获得与已挂载文件系统有关的信息。
#include <sys/statvfs.h>
int statvfs(const char *pathname, struct statvfs *statvfsbuf); /* 成功返回0,失败返回-1 */
int fstatvfs(int fd, struct statvfs *statvfsbuf); /* 成功返回0,失败返回-1 */
两者之间唯一的区别在于其标识文件系统的方式。statvfs()需使用 pathname 来指定文件系统中任一文件的名称。而 fstatvfs()则需使用打开文件描述符 fd,来指代文件系统中的任一文件。二者均返回一个 statvfs 结构,属于由 statvfsbuf 所指向的缓冲区,其中包含了关乎文件系统的信息。