Skip to content

第八十六章:挂载之道

斩灵期

涉及内核源码:

林小源在森林边缘遇到了一面巨大的悬崖。

悬崖的表面光滑如镜,镜中映出另一片森林的景象——那片森林的树木形态完全不同,树干上刻着"tmpfs"的字样。两片森林之间没有路,只有一道深不见底的鸿沟。

"怎么过去?"林小源问。

身后传来一个低沉的声音:"挂载。"

林小源转身,看到一个穿着灰色长袍的老者。老者的长袍上绣着密密麻麻的路径——"/dev/sda1 → /""/dev/sdb1 → /home""tmpfs → /tmp""proc → /proc"。

"我是 mount 命令的化身,"老者说,"我的工作是把不同的文件系统连接到同一棵目录树上。"

老者抬起手,指向悬崖。一道光桥从悬崖边缘延伸出去,连接到对面的森林。光桥落下的瞬间,悬崖上的镜面碎裂,露出一条通往 tmpfs 森林的道路。

"这就是挂载,"老者说,"一个文件系统的根目录,连接到另一个文件系统的某个目录上。那个目录叫挂载点。"

林小源走到光桥上,低头看——桥面透明,可以看到鸿沟深处无数的数据块在流转。

"mount 系统调用的参数呢?"他问。

老者从袖中掏出一块玉简,上面刻着五个字:"source、target、fstype、flags、data。"

"source 是设备路径,比如 /dev/sdb1。target 是挂载点,比如 /home。fstype 是文件系统类型,比如 ext4。flags 是挂载标志——MS_RDONLY 只读,MS_NOEXEC 禁止执行,MS_NOSUID 忽略 setuid。"

林小源走过光桥,进入了 tmpfs 森林。

这片森林和 ext4 的森林截然不同——树木是半透明的,像是用光线编织而成。树干上没有磁盘地址,只有内存地址。

"tmpfs 的数据存在内存里,"老者跟在后面说,"不占磁盘空间。重启就没了。"

林小源伸手触摸一棵 tmpfs 的树。树干温热,但比 ext4 的古树轻得多——像是空心的。

"挂载点的遮盖效应呢?"他忽然问。

老者停下脚步,指了指脚下。光桥的另一端连接着一个目录——这个目录里原本有三棵小树,但现在它们被完全遮住了,取而代之的是 tmpfs 森林的入口。

"挂载的时候,"老者说,"原来目录的内容被覆盖了。你看到的是新文件系统的根目录。umount 之后,原来的内容才会恢复。"

"就像在门口放了一块幕布?"

"差不多,"老者说,"幕布后面的东西还在,只是你看不到。"

林小源回头望向 ext4 森林的方向。从这边看过去,光桥已经消失了——两片森林之间的连接断开了。

"umount 就是拆桥,"老者说,"拆桥之前,必须确保没有进程在使用这片森林。否则 umount 会失败——EBUSY。"

林小源在 tmpfs 森林中走了很久,来到了一片奇异的区域。

这片区域被一层薄薄的光幕笼罩着。光幕内,一棵棵透明的树木排列整齐;光幕外,是 ext4 森林的参天古木。两片森林互不干扰,各自独立。

"这是 mount 命名空间,"老者说,声音变得严肃起来,"不同进程可以有不同的挂载表。光幕内的进程看到的是一个世界,光幕外的进程看到的是另一个世界。"

"容器?"

"对,"老者说,"Docker 就是用 mount 命名空间来隔离文件系统的。容器里的进程以为自己在 /,其实只是宿主机上的一个子目录。它看不到宿主机的其他挂载点——因为它的挂载表是独立的。"

林小源伸手触碰光幕。光幕微微震颤,但没有破碎。他透过光幕看进去,里面的进程正在读写文件,完全不知道外面还有一个更大的世界。

"隔离,"他低声说,"从挂载开始。"

老者点了点头:"mount 命名空间是容器的第一个隔离层。有了它,容器才能有自己独立的文件系统视图。"

林小源收回手,转身走向光桥消失的位置。他需要回到 ext4 的森林——那里还有更多的秘密等着他。

这下他懂了——挂载嘛,就是给两个独立的文件系统搭一座桥,让它们共用一棵目录树。


c
/*
 * 挂载的概念:
 *
 *   /dev/sda1 (ext4) → 挂载到 /
 *   /dev/sdb1 (ext4) → 挂载到 /home
 *   tmpfs → 挂载到 /tmp
 *   proc → 挂载到 /proc
 *
 * 挂载后:
 *   /home/user/file.txt
 *   实际访问 /dev/sdb1 上的 file.txt
 *
 * mount 系统调用:
 *   mount("/dev/sdb1", "/home", "ext4", 0, NULL)
 *
 * mount 命令:
 *   mount -t ext4 /dev/sdb1 /home
 *   mount -t tmpfs tmpfs /tmp
 *
 * 挂载点:
 *   原来目录的内容被"遮盖"
 *   新文件系统的根目录替代原目录
 *   umount 后原目录内容恢复
 */

printf("=== mount — 挂载文件系统 ===\n\n");

printf("挂载示例:\n");
printf("  /dev/sda1 (ext4) → /\n");
printf("  /dev/sdb1 (ext4) → /home\n");
printf("  tmpfs → /tmp\n");
printf("  proc → /proc\n");
printf("  sysfs → /sys\n\n");

printf("挂载后的目录树:\n");
printf("  /\n");
printf("  ├── bin/\n");
printf("  ├── etc/\n");
printf("  ├── home/  ← /dev/sdb1\n");
printf("  │   └── user/\n");
printf("  │       └── file.txt\n");
printf("  ├── proc/  ← procfs\n");
printf("  ├── sys/   ← sysfs\n");
printf("  └── tmp/   ← tmpfs\n\n");

printf("--- mount 系统调用 ---\n");
printf("mount(source, target, fstype, flags, data)\n");
printf("  source: 设备路径(如 /dev/sdb1)\n");
printf("  target: 挂载点(如 /home)\n");
printf("  fstype: 文件系统类型(如 ext4)\n");
printf("  flags: 挂载标志(如 MS_RDONLY)\n\n");

printf("--- 挂载标志 ---\n");
printf("MS_RDONLY: 只读挂载\n");
printf("MS_NOEXEC: 禁止执行\n");
printf("MS_NOSUID: 忽略 setuid\n");
printf("MS_NODEV: 禁止设备文件\n\n");

printf("--- 绑定挂载 ---\n");
printf("mount --bind /src /dst\n");
printf("  把 /src 挂载到 /dst\n");
printf("  两个路径指向同一文件系统\n\n");

printf("--- 命名空间 ---\n");
printf("不同进程可以有不同的挂载表\n");
printf("容器使用 mount 命名空间隔离\n");

道藏笔记

内核启示

挂载就是把一个文件系统接到目录树的某个节点上。不挂载的文件系统是访问不了的——你得先给它找个"挂载点"。

mount 系统调用的参数很好记:source 是设备路径(比如 /dev/sdb1),target 是挂载点(比如 /home),fstype 是文件系统类型(比如 ext4),后面还有 flags 和 data。flags 控制行为——MS_RDONLY 只读挂载,MS_NOEXEC 禁止执行,MS_NOSUID 忽略 setuid。

挂载有个容易忽略的效果:挂载后,原来挂载点目录里的内容会被"遮盖",你看到的是新文件系统的根目录。umount 之后原来的内容才恢复。

再往深了说,不同进程可以有不同的挂载表——这就是 mount 命名空间。Docker 就靠这个隔离文件系统,容器里的进程以为自己在根目录,其实只是宿主机上的一个子目录。


破关试炼

挂载之试

把一个文件系统接到目录树挂载点上的系统调用或命令,本章反复称为什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道