文件系统是用来管理和组织磁盘数据的,在Linux中,面对一个原始的磁盘分区,可通过mkfs系列工具来制作文件系统(比如"mkfs.ext4 /dev/sda1")。这一步主要是在对应的磁盘分区上创建文件系统所需的super block和inode表(参考这篇文章),可近似对等于大家熟悉的windows系统的格式化。
安家落户
文件系统生成后,还不能直接使用,需要借助"mount"操作,将这个文件系统加入到Linux的管理,这样用户才能看到并访问。如下图所示,系统中有三个磁盘分区a, b和c,现在a分区的文件系统已经挂载,可以正常使用。而b分区和c分区的文件系统尚未挂载,此时它们对用户都是不可见的。
从代码实现的角度,挂载的过程就是将代表这个文件系统的"super_block"结构体,加入由之前已经挂载的所有filesystems组成的双向链表中(即"s_list")。
struct super_block {
struct list_head s_list;
基础的挂载命令大致如下:
mount -t
这里的"type"代表文件系统类型(比如tmpfs),一个文件系统所属的类型由"file_system_type"指向。同一文件系统类型可以有多个文件系统的实例(instance)。
属于同一类型的文件系统亦通过双向链表来连接(即"s_instances"),即一个文件系统既在这个链表中,也在前面提到的全局链表中。
struct file_system_type *s_type;
struct hlist_node s_instances;
"device"是文件系统所在的磁盘分区(比如"/dev/dsa1"),由"s_bdev"表示:
struct block_device *s_bdev;
unsigned long s_blocksize;
还是前面的那个例子,假设b分区的文件系统随后被挂载到了"/home"目录下:
这个路径即是"mount point",它需要事先存在,如果不存在,则mount操作并不会自动去新建一个对应的文件夹。挂载点可以通过以下命令进行移动,调整到新的位置:
mount --move
此过程对应了"super_block"中指向挂载路径的"s_root"的更改:
struct dentry *s_root;
内核中实现挂载的通用函数是mount_bdev():
struct dentry *mount_bdev(struct file_system_type *fs_type,
const char *dev_name, ... ,
int (*fill_super)(struct super_block *, void *, int))
这里的"fs_type"和"dev_name"就是前面介绍的文件系统类型和块设备名称,返回值就是挂载点。还有一个"fill_super"函数指针,它是由具体的文件系统在回调时填入的(以下代码以ext4的实现为例):
struct file_system_type {
struct dentry *(*mount) (struct file_system_type *, int, const char *, void *);
void (*kill_sb) (struct super_block *);
...
}
static struct file_system_type ext4_fs_type = {
.mount = ext4_mount,
.kill_sb = kill_block_super,
};
static struct dentry *ext4_mount(struct file_system_type *fs_type, int flags,
const char *dev_name, void *data)
{
return mount_bdev(fs_type, flags, dev_name, data, ext4_fill_super);
}
像debugfs, procfs, sysfs这些内存文件系统,没有对应的磁盘设备linux windows,因此挂载的时候""这一项可以填"none"或者"nodev",对应的函数实现是mount_nodev()。
【移宫换羽】
除了支持变换挂载点linux文件系统ext4,对挂载的方式和属性进行设置和调整更通用的方式是使用"-o"参数(代表option):
mount -o [device] [directory]
比如将一个iso镜像文件作为磁盘使用的loop挂载方式:
mount -o ro,loop Fedora-14-x86_64-Live-Desktop.iso /media/cdrom
或是通过"remount"来修改挂载分区的属性(比如将“只读”更改为“可读写”):
mount -o remount,rw 独辟蹊径
以上介绍的都属于基础的挂载操作,事实上,在Linux系统中,文件系统的挂载是非常灵活和自由的,下面来看两种花式的挂载方式。
【狡兔三窟】
同一文件系统可以被挂载到多个mount point,这被称为"bind mount"(多个路径是bind在一起的):
mount --bind
其实这并不难理解,它就像是文件系统层面的一个symbol link。比如你嫌debugfs默认的挂载路径"/sys/kernel/debug"太长,那可以把它在更顺手的位置再挂载一次:
不过其用途远不止于此,容器虚拟化的场景,才是它大显身手的地方。
【鸠占鹊巢】
反过来,不同的文件系统也可以共用同一个mount point,新挂载的文件系统会覆盖掉这个位置之前的文件系统。但如果使用"union mount"的形式,最后呈现的目录结构则是新旧文件系统merge后的结果:
mount /dev/sda /mnt
mount --union /dev/sdb /mnt
假设现在"/dev/sda"已经挂在了"/mnt"上,而后"/dev/sdb"也毫不客气的挂到了同一位置。merge之后,"/dev/sda"里有而"/dev/sdb"没有的(即图中的"file1"),还继续可见和可访问,其他的,就通通都是"/dev/sdb"的。也就是说,"/dev/sda"被部分覆盖了。
那"/dev/sda"里被覆盖的内容是彻底消失了么?并没有,等到"/dev/sdb"从"/mnt"上卸载后,这些被覆盖的内容又可以「重见天日」了。是不是看起来有点奇葩,可以用的目录那么多,连挂载点都要抢吗(又不是地铁早高峰有限的把手)。那这种挂载方式到底有什么用,或者说Linux为什么要支持呢?
举个例子,光盘里的ISO镜像通常被设为“只读”的,如果需要临时修改怎么办呢?挂载后把光盘的内容全部拷贝到磁盘上,再进行读写。可以,但是这样效率不高,而如果使用union mountlinux文件系统ext4,在光盘的挂载点上再挂载一个writable的目录,这样就可以修改该挂载点下的文件了,之后卸载这个可读写的目录,光盘里的内容还是原来的样子。
似乎应用场景还是比较有限?非也,union mount在大红大紫的docker容器的实现中起到了至关重要的作用,是构成docker存储和文件系统的支柱技术,这将在后面介绍docker的系列文章中详细讲述。
再别康桥
挂载的时候需要同时指定"device"和路径,而卸载的时候只需指定其中一个元素即可:
umount
umount
不过linux游戏,如果还有进程在读取这个文件系统里的文件,umount将不会成功。那如何知道是哪个进程在使用,又是在使用哪个文件呢?直接使用"lsof"命令即可查出:
篱编修竹
"/etc/fstab"配置文件定义了系统启动时需要挂载的文件系统和对应的挂载方式,启动之后,挂载的信息就被记录在了"/proc/mount"文件中,在接下来运行过程中对文件系统的mount/umount的结果,都将反映在"/proc/mount"中。
不过,我们需要查看系统已挂载的文件系统时,通常不会选择去读取"/proc/mount",而是直接使用不带任何参数的"mount"命令。可见,"mount"命令既可用于查看,也可用于挂载。
当挂载的文件系统较多时,"mount"的输出结果会看起来密密麻麻的,这时可使用使用"findmnt"命令,它以树形结构展示挂载信息,显得更加清晰:
参考: