先有根路径,还是先有文件系统?
我以前一直没真正想明白文件系统挂载。
因为教材通常都会直接写:
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 里的那些启动文件解压到这个内存文件系统中。到这一步,系统已经有了一套可以工作的临时根文件系统。
第三步
再切换到真正的根。
内核和启动程序识别出真实设备,找到上面的真正根文件系统,然后把根指针重新切到那个文件系统的根上。
注意,这里切过去的不是“设备本身”,而是:
设备上那个文件系统的根目录。
所以真正变化的始终不是“把文件搬来搬去”,而是:
根指针当前指向哪一个文件系统的根