第八十六章:挂载之道
斩灵期涉及内核源码:
一
林小源在森林边缘遇到了一面巨大的悬崖。
悬崖的表面光滑如镜,镜中映出另一片森林的景象——那片森林的树木形态完全不同,树干上刻着"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 的森林——那里还有更多的秘密等着他。
这下他懂了——挂载嘛,就是给两个独立的文件系统搭一座桥,让它们共用一棵目录树。
/*
* 挂载的概念:
*
* /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");#include <stdio.h>
/*
* 挂载的概念:
*
* /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 后原目录内容恢复
*/
int main() {
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");
return 0;
}道藏笔记
内核启示
挂载就是把一个文件系统接到目录树的某个节点上。不挂载的文件系统是访问不了的——你得先给它找个"挂载点"。
mount 系统调用的参数很好记:source 是设备路径(比如 /dev/sdb1),target 是挂载点(比如 /home),fstype 是文件系统类型(比如 ext4),后面还有 flags 和 data。flags 控制行为——MS_RDONLY 只读挂载,MS_NOEXEC 禁止执行,MS_NOSUID 忽略 setuid。
挂载有个容易忽略的效果:挂载后,原来挂载点目录里的内容会被"遮盖",你看到的是新文件系统的根目录。umount 之后原来的内容才恢复。
再往深了说,不同进程可以有不同的挂载表——这就是 mount 命名空间。Docker 就靠这个隔离文件系统,容器里的进程以为自己在根目录,其实只是宿主机上的一个子目录。
挂载之试
把一个文件系统接到目录树挂载点上的系统调用或命令,本章反复称为什么?