先有根路径,还是先有文件系统?

3 阅读7分钟

先有根路径,还是先有文件系统?

我以前一直没真正想明白文件系统挂载。

因为教材通常都会直接写:

mount /dev/sdb1 /data

然后告诉你:

/dev/sdb1 挂载到 /data

这句话一开始看起来很正常。

但继续往下想,就会冒出一个更根的问题:

挂载是不是两个文件系统之间的事情?

因为 /data 这个路径,本来就得先存在。

而路径要存在,就说明当前系统里已经先有一套文件系统,至少得先有:

/
└── data

那问题就来了。

如果挂载本来就是“把一个文件系统接到另一个文件系统的目录节点上”,那么最开始那套文件系统是从哪来的?

这不就像死循环吗?

1. 普通挂载,确实是两个对象之间的关系

先说结论:

普通挂载,确实可以理解成两个对象之间的关系。

比如现在系统里已经有一套文件系统 A:

/
├── etc
├── home
└── data

现在还有另一个文件系统 B:

文件系统 B 的根
├── photo
└── backup

执行:

mount /dev/sdb1 /data

本质上做的是这件事:

文件系统 A 里的 /data
        ↓
文件系统 B 的根

也就是说,挂载点在当前路径树里,真正被接进来的是另一个文件系统的根。

所以如果只看普通挂载,你的直觉没有错:

它确实像是在两个文件系统之间建立关系。

2. 真正让人绕进去的,是第一个文件系统

问题不在普通挂载。

问题在于:

如果挂载总要先有一个已有文件系统,那第一个文件系统怎么出现?

这里最容易混淆的,是把“根路径 /”想成一个预先存在的目录。

好像系统一开始就先有一个空的:

/

然后再把第一个文件系统挂进去。

可这套想法本身就有问题。

因为如果 / 也是某个文件系统里的目录,那你其实还是在追问:

那这个更早的文件系统又挂在哪里?

死循环就是从这里来的。

3. 第一个文件系统,不是挂到某个目录上

真正的关键点就在这里:

第一个文件系统不是挂载到某个已有目录上。

它做的不是:

找到一个现成的 /
→ 再把文件系统挂进去

而是:

内核先有一个“绝对路径从哪里开始解析”的根指针
→ 直接让这个根指针指向某个文件系统的根

可以粗暴理解成:

root = filesystem.root;

这一步完成以后,/ 才真正有了含义。

所以第一个文件系统不是“挂到 / 上”。

更准确地说,是:

它先被内核选成根,于是它自己的根目录就成了 /

这就是为什么这里不存在死循环。

因为第一步根本不需要先有一个目录挂载点。

4. 为什么“先有一个内存文件系统”这个解释,还是容易让人继续糊涂

我后来发现,很多解释虽然已经比教材更进一步了,但还是没有真正打中我的疑问。

它们会说:

  • 系统刚启动时,先有一个小的内存文件系统
  • 或者先有一个临时根文件系统
  • 然后再切到真正的磁盘文件系统

这些话本身没错。

但它们很容易让人继续卡住。

因为我当时真正想问的是:

不管它是内存文件系统,还是磁盘文件系统,它不都还是“一个文件系统”吗?
那你到底是先用路径找到它,还是先有文件系统,路径才成立?

这才是关键。

答案是:

先有文件系统对象,后有路径含义。

也就是说,那个“最开始的小文件系统”不是靠 / 找到的。

它也不是先挂到某个路径上去的。

它仍然是先被内核直接设成根:

root = filesystem.root;

从这一步开始,/ 才第一次有意义。

所以“先有一个内存文件系统”这个说法,如果不补上这句,就还是会让人误以为:

先有路径
→ 再找到第一个文件系统

但真实顺序恰恰相反:

先有第一个文件系统对象
→ 它被内核设成根
→ 然后 / 才出现

这就是我后来真正想通的地方。

问题从来不在于“第一个文件系统是内存的还是磁盘的”。

问题在于:

第一个文件系统不是通过路径找到的,路径反而是它被选成根以后才成立的。

5. 那第一次连设备路径都还没有时,内核又是怎么找到根设备的?

这里还有一层更容易把人绕进去的东西。

就算你接受了“第一个文件系统不是靠挂载点挂上去的”,还是会继续追问:

可设备不也经常写成 /dev/sda1 这种路径吗?
第一次连根文件系统都还没有,那这个设备路径又是从哪来的?

答案是:

第一次找根设备时,内核依赖的不是 /dev/sda1 这个路径。

/dev/sda1 只是后面才出现的设备节点名字。

这里也要分开两个东西:

  • 内核里的设备对象
  • 文件系统里的设备路径

很多人会下意识觉得:

设备 = /dev/sda1

其实不是。

/dev/sda1 只是后来出现在 /dev 目录里的一个设备节点文件。

真正的磁盘、分区、块设备,内核在驱动加载以后早就已经识别出来了。第一次找根设备时,内核真正依赖的是:

  • 启动参数里的 root=
  • 驱动识别出来的块设备
  • 分区信息
  • UUID、PARTUUID 之类的标识

所以第一次找根设备的顺序不是:

先有 /dev/sda1
→ 再找到设备

而是:

内核先识别出设备对象
→ 选中那个设备或分区
→ 读取它上面的文件系统
→ 把这个文件系统设成根

/dev/sda1 这种路径,是后面 /dev 存在以后,系统再把内核里的设备对象暴露成设备节点文件,用户态程序才开始用它。

6. 所以其实要分清三件完全不同的事

现在可以把整个问题拆得更清楚一点。

第一件事,是内核识别设备对象:

磁盘 / 分区 / 块设备

这一步不依赖文件路径。

第二件事,是最初的根建立:

内核的根指针
      ↓
第一个文件系统的根

这一步也不依赖路径。

第三件事,才是普通挂载:

已有目录节点
      ↓
另一个文件系统的根

这时候你才真的需要一个已经存在的路径,比如 /data

所以“挂载是不是两个文件系统之间的事情”这个问题,最准确的回答其实是:

  • 对普通挂载来说,基本可以这么理解
  • 但最开始内核先要解决的,不是路径挂载问题
  • 而是先识别设备,再选一个文件系统当根

7. 真实过程其实就三步

如果把真实启动过程粗暴压缩,其实就是三步:

第一步

先启动内核。

内核里先有一个根指针,然后先创建一个最小的内存文件系统,并让根指针先指向它的根。

第二步

把最小启动环境解压进去。

也就是把 initramfs 里的那些启动文件解压到这个内存文件系统中。到这一步,系统已经有了一套可以工作的临时根文件系统。

第三步

再切换到真正的根。

内核和启动程序识别出真实设备,找到上面的真正根文件系统,然后把根指针重新切到那个文件系统的根上。

注意,这里切过去的不是“设备本身”,而是:

设备上那个文件系统的根目录。

所以真正变化的始终不是“把文件搬来搬去”,而是:

根指针当前指向哪一个文件系统的根