ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查

先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.service。结果下次重启,系统直接卡在了 emergency mode,根文件系统处于只读状态,日志刷屏提示无法写入关键目录。我当时还一头雾水:明明/etc/fstab里写着rw,为什么根分区启动后还是只读?

排查到最后,问题就出在这个被我随手禁用的服务上。它和fstab之间其实有一条非常隐蔽但又至关重要的“隐藏链路”。折腾完那次之后,我花了不少时间把 systemd 初始化阶段和挂载相关的机制彻底翻了个底朝天,越看越觉得这块内容是 Linux 系统管理的“盲区”,绝大多数资料只是告诉你“别动它”,但没人告诉你“为什么不能动”。

这篇文章就专门聊聊systemd-remount-fs.service和/etc/fstab之间的渊源,算是对我那次排查经历的完整复盘。看完之后你至少能搞清楚三件事:这个服务到底在启动阶段扮演什么角色;它跟 fstab 里的挂载参数是怎么联动起来的;真出了问题,怎么用最快的方式定位并解决。

2. 历史包袱:fstab 为什么能决定根文件系统的命运

2.1 fstab 的字段结构远比你想象的复杂

我们天天打交道,但可能很少认真审视过/etc/fstab。它的标准字段是六列:设备名、挂载点、文件系统类型、挂载选项、dump 备份标记、fsck 检查顺序。大多数人的注意力都集中在第一列和第四列上,比如把/dev/sda1挂载到/boot,选项写defaults,然后就完事了。

但真正跟根文件系统早期状态相关的,其实是第四列的挂载选项。这里面有个很容易被误解的细节:defaults并不是“系统默认最优解”,它实际上等价于rw,suid,dev,exec,auto,nouser,async。其中rw这个选项,决定了内核在挂载时以读写模式还是只读模式打开文件系统。fstab 里如果写的是ro,那就意味着启动之后根分区是只读的,哪怕内核启动参数root=里什么都没说。

这就会引出一个经典的问题:根文件系统在 Linux 启动过程中,到底是什么时候变成读写模式的?如果你去翻老式的 SysV init 脚本,会发现有一个脚本专门做mount -o remount,rw /的操作。而在 systemd 的世界里,这个操作被收敛到了一个更精致的服务里,也就是systemd-remount-fs.service。

2.2 启动早期根文件系统为什么必须是只读的

我曾经跟一个刚入行的同事解释这个概念,被问到一个特别好的问题:既然最终都要变成读写,为什么不能一开始就挂载成rw,折腾这一道干嘛?

这里涉及到一个文件系统一致性问题。根文件系统如果一开始就以读写模式挂载,那么任何崩溃或非正常关机都可能导致根分区上的元数据损坏。而只读挂载可以保证,在系统还没有完全进入用户态、各种服务还没开始大量写盘之前,根文件系统至少是“被动安全”的。只要有一步出问题,内核能保证根文件系统处于一个不会被继续写坏的状态。

另外还有一个更实际的工程原因:早期启动阶段,initramfs 还没完全交接给真正的根文件系统,此时根文件系统可能是通过网络设备(比如 iSCSI)或者加密设备映射出来的。此时贸然让它可写,一旦底层设备还没有完全就绪,写操作就是直接往黑洞里扔。先只读挂载,等所有必要的用户态服务和设备节点都准备好了,再统一做一次remount,改成读写模式,这是最稳妥的操作顺序。

3. systemd 的启动时序:remount-fs 服务到底卡在哪个环节

3.1 精细化拆解 initramfs 到真实根文件系统的交接过程

要真正理解systemd-remount-fs.service的作用,必须把它放到 systemd 完整的启动时序里去看。

系统加电后,引导加载器加载内核和 initramfs。initramfs 解压后作为第一个临时根文件系统,里面的 systemd(或者其他 init 程序)会完成各种硬件初始化、加载必要的内核模块、等待存储设备出现,然后挂载真正的根文件系统。这一步很关键:在 initramfs 阶段,systemd 会发现根文件系统可以访问了,但此时它可能会以只读方式挂载根,也可能根本不挂载根,而是执行switch_root切换到真实的根。

switch_root之后,系统的 PID 1 已经变成了真实根文件系统上的 systemd。此刻 systemd 会按依赖关系启动目标sysinit.target,而systemd-remount-fs.service就挂在这个目标下。它的 ExecStart 命令,本质上就是去读取/etc/fstab,然后只针对根文件系统的条目,执行一次remount。

3.2 systemd-remount-fs.service 的实际行为:不仅仅是简单的 mount -o remount

很多文档只告诉你这个服务会去重新挂载根文件系统,但我去翻源码才发现,它其实会调用一个内置的工具,把 fstab 里根条目对应的挂载选项收集起来,然后跟当前内核实际挂载参数做对比,最后用调整后的参数集合做 remount。

举个例子,假设 fstab 里根文件系统的选项是rw,relatime,而内核启动时因为某种原因(比如启动参数里带了ro)把根挂载成了只读,那么systemd-remount-fs.service就会执行类似mount -o remount,rw,relatime /的操作。如果 fstab 里根条目根本没有写,那它也什么都不做。

这里有一个非常值得注意的细节:systemd 内部处理时,会“智能合并”启动参数和 fstab 参数,而不是纯粹用 fstab 覆盖内核启动参数。如果内核参数里明确写了ro,而 fstab 里写了rw,那么启动参数优先级更高;反之,如果内核参数没写,那 fstab 里的rw就会被采纳。这个优先级关系,其实就是我们排查“为什么我的 fstab 写了 rw 却还是只读”问题时的第一把钥匙。

4. fstab 转换器:systemd 是如何“读”fstab 的

4.1 systemd-fstab-generator 的秘密做法

很多人对 systemd 有个误解,以为它像 SysV init 一样,启动时逐个去解析 fstab 文件里的条目。实际上 systemd 根本不会在运行时去“逐行读取 fstab”,而是在启动早期,通过一个叫做systemd-fstab-generator的工具,把 fstab 里的每一条记录,静态转换成一个对应的.mountunit 文件,放到内存文件系统/run/systemd/generator/目录下。

这个转换过程是决定性的,因为它把 fstab 里每一列的文本信息,全部变成了 systemd 的 unit 依赖图的一部分。比如挂载点/home对应生成home.mount,依赖关系是local-fs.target之后执行,而如果 fstab 里写了noauto,则生成的 mount unit 不会自动依赖 local-fs.target,需要手动启动。

对于根文件系统条目,generator 的表现就比较特殊了。它不会生成一个独立的.mountunit,因为根文件系统在 initramfs 阶段已经挂载了,此时再去“挂载”一次没有意义。但 generator 会把这个条目的挂载选项保存下来,作为systemd-remount-fs.service后续读取的依据。

4.2 为什么说“fstab 里的根条目被特殊对待”

理解这段渊源,是理解整个机制的枢纽。启动时,systemd-remount-fs.service会向 generator 生成的数据目录查询根文件系统挂载条目。实际上,systemd 的主程序在启动早期会直接从/etc/fstab读取根条目信息,并且把这个信息传递给 remount 逻辑。

所以,合理准确的说法是:systemd-remount-fs.service是 systemd 启动过程中“执行”的那一步,而 fstab 则是“数据来源”和“决策依据”。两者之间没有直接的 ExecStart 一行行的读写关系,而是通过 systemd 的单元解析机制和 generator 机制间接建立连接。这也解释了为什么修改 fstab 之后,不需要重启也能生效更新生成的单元,但需要运行systemctl daemon-reload来刷新状态。

5. 实操排查:remount-fs 出问题时,我们真正要关注什么

5.1 问题一:fstab 选项被“无视”了

这是一个非常高频率的坑。你在 fstab 里给根分区加了一个特殊选项,比如discard(TRIM)或者noatime,重启之后发现根本没有生效。用mount查看,挂载选项还是原来的内核默认值。

排查思路其实就一句话:看启动参数(kernel command line)里有没有比你 fstab 选项优先级更高的东西。比如内核命令行里带了rootflags=noatime,那么 fstab 里写再多noatime也没用,因为内核会坚持使用rootflags传进去的参数。另一个更常见的场景是内核命令行里写了ro,此时 fstab 里的rw被 remount-fs 服务执行时“尊重”内核参数,就不会变成读写模式。

如果确认内核参数没问题,那就要看 initramfs 阶段的配置了。不少发行版的 initramfs 工具(比如 dracut),在生成 initramfs 时会重新生成一份 fstab 快照,或者直接把根挂载参数固化进 initramfs 里。如果你改的是/etc/fstab,但 initramfs 里的旧配置还保留着,那 root 的挂载参数永远是你旧的配置。

解决办法是更新 initramfs,在大多数发行版上执行对应命令(比如基于 dracut 的发行版需要重新生成 initramfs 镜像),然后重启。这次追查我第一次就是栽在这一点上,改完 fstab 只重启不更新 initramfs,结果参数永远不对。

5.2 问题二:remount-fs 服务启动失败导致根只读

如果systemd-remount-fs.service失败了,根文件系统很可能保持只读。此时系统不会完全死掉,但很多依赖写盘的服务会异常,日志文件写不进去,数据库起不来,最后的结局通常是系统进入 emergency mode。

查看服务状态时,你可能看到类似 “Failed to remount root filesystem” 的报错。这种报错的原因多半是 fstab 里根条目的挂载选项跟当前文件系统状态冲突。比如文件系统被标记为需要 fsck,而 fsck 还没跑,此时 remount 成rw会被内核拒绝(因为日志需要重放或文件系统需要修复)。

这种时候,第一反应不要去强行 remount,而是先确认根文件系统是不是真的干净可写。用只读方式跑一遍 fsck 检查,确认无误后,再尝试手工执行mount -o remount,rw /,看能不能成功。如果手工这一步能成功,而服务启动时失败,那多半是时序问题——fsck 服务和 remount 服务的依赖关系没配置好。可以去查一下对应发行版里systemd-fsck-root.service和systemd-remount-fs.service的启动顺序。

5.3 问题三:云服务器或容器场景下,remount-fs 带来的意外“惊喜”

在云主机或者 Docker 容器里,根文件系统往往并不是直接从块设备挂载的,而是通过某种网络映射或者 overlayfs 叠加的。这种场景下,systemd-remount-fs.service的行为尤其微妙。

举个例子,在某些云主机上,根分区可能实际是 LVM 逻辑卷,而你 fstab 里的根条目用的是 UUID。如果 LVM 激活的时机比 remount-fs 晚(虽然现代 udev 规则和 systemd 依赖已经尽量保证了顺序,但偶尔仍有偏差),那 remount 就可能找不到设备。更常见的是,云主机使用的 virtio-blk 设备,在启动阶段内核里加载了对应驱动,但设备的rw标志位被某种策略锁定了。遇到这种情况,不要迷信 fstab 能“遥控”一切,先去看内核日志里和块设备读写相关的记录。

容器场景反而是最安全的,因为容器里根本没有独立的根挂载动作,remount-fs 往往会被容器运行时禁用或者忽略。但如果你在容器里看到根文件系统变成只读,那多半不是这个服务的问题,而是宿主层面或者 overlayfs 底层出了问题。

6. 深度影响范围:改 fstab 之前想清楚后果

6.1 根只读还是一个特性?为什么嵌入式场景喜欢故意用 ro

聊到这儿,不得不提一个反直觉的场景:嵌入式设备经常故意把根文件系统配置成只读。原因很直白——防止 Flash 芯片被频繁写坏,同时也能避免因为意外断电导致根文件系统元数据损坏。

这时候,systemd-remount-fs.service反而成了“不支持我们场景”的服务。很多嵌入式工程师的做法,是直接在systemd-remount-fs.service的配置里加入空白的 ExecStart,或者把这个服务 mask 掉,让根文件系统牢牢锁在只读状态。但这属于一种刻意设计,跟故障产生的“被动只读”有本质区别。

如果你确定要这么做,我强烈建议在 fstab 根条目的选项里明明白白写上ro,并把 remount-fs 服务 mask 掉,而不是仅仅依赖内核启动参数。这样系统后续哪怕是重新触发模块加载或者 udev 事件,也不会被其他逻辑意外改成rw。

6.2 修改 fstab 根条目的“最佳实践”:三步走方案

既然 fstab 跟 remount-fs 服务绑定得这么紧,那我们在修改根文件系统挂载参数时,就必须有一套严谨的操作流程,避免改完重启直接进 emergency mode。

第一步,先备份当前的 fstab 和内核挂载参数。用mount命令记录当前根分区的实际挂载选项是一项必须做的工作。

第二步,用findmnt工具验证你要修改的参数当前是否已经生效。findmnt /会告诉你根文件系统的当前挂载来源、类型和选项,这比肉眼读 fstab 靠谱得多。

第三步,修改完 fstab 之后,先执行mount -o remount手动验证一遍(用你打算写入的新参数),确认安全了再重启。这一步千万不能跳过,否则你会发现某次修改中的拼写错误或者内核不支持的特性参数,会在重启时直接把你干趴下。

这个三步走的流程我用了很多年,几乎每次都能在正式重启前发现潜在问题。有一次我在 fstab 里给根分区加了size=2048m这种明显只适用于 tmpfs 的选项,手动 remount 的时候内核直接回了错误,我得以在重启前及时修正。

6.3 性能调优选项到底应该放内核参数还是 fstab

一个很多人纠结的问题:像noatime这样对性能有明显提升的选项,到底是放内核命令行参数里,还是放 fstab 里?

我的个人建议是,如果只针对根分区,那首选内核命令行参数rootflags=noatime,因为它的优先级最高,而且根本不受 remount-fs 服务解析逻辑的影响,initramfs 阶段就能直接生效。如果你要管理的分区不止根分区这一个,那 fstab 是唯一合理的落点,因为内核参数没法精确到每个分区。

需要特别注意,fstab 里noatime的生效时机是 remount-fs 服务执行成功之后。如果你通过某种手段提前访问了根文件系统(比如日志服务启动得非常早),那么在 remount 之前写入的文件,atime 更新策略还是旧的。对绝大多数场景来说这无所谓,但如果你的应用对 atime 有严格一致性要求,就得考虑把这项参数提前到内核阶段给上。

7. 几个高级玩法:自定义 remount 行为与调试技巧

7.1 如何完全自定义 remount-fs 的触发条件

如果说 fstab 里的脏数据分区需要延迟挂载,但根文件系统必须早期挂载,这种需求该怎么表达?答案是:不要修改systemd-remount-fs.service本体,而是通过添加一个 drop-in 配置文件来调整它的行为。

举个例子,如果我希望 remount-fs 服务在某些特定设备完全就绪后才执行,可以在/etc/systemd/system/systemd-remount-fs.service.d/目录下创建一个配置文件,往里面写入After=和Requires=等指令。systemd 的 drop-in 机制会把这些配置跟原始服务合并,不会影响其他依赖关系。

但是我要提醒一句:不要轻易把systemd-remount-fs.service的Requires=加得太死,因为根文件系统的 remount 是很多服务的隐式前提。加依赖之前,一定要花点时间梳理目标依赖图,用systemctl list-dependencies看清楚你加的那个目标到底是否真的需要在 remount 之前完成。

7.2 快速调试技巧:让 remount 过程 “开口说话”

当你怀疑 remount-fs 服务出了问题,最快的验证方式不是瞎猜,而是打开服务的详细日志。执行journalctl -u systemd-remount-fs.service -b -1可以看到上一次启动时这个服务的完整日志输出,内容包括它读到了哪些参数、做了哪些 remount 决策、最后成败如何。

如果日志里什么都没有,那大概率是服务根本没被触发,可以检查这个服务有没有被 enable,以及local-fs.target是否被正确加入到了默认启动目标里。另一种可能性是你的 fstab 里根本没有根文件系统的明确条目,systemd 就会认为“不需要额外处理”,直接跳过 remount 流程。

还有一个小技巧:用systemd-analyze verify /etc/fstab可以直接让 systemd 尝试解析并验证 fstab 的语法正确性。它能识别一些明显错误,比如挂载点不存在、设备名拼写错误等。每次改完 fstab,我都习惯跑一遍这个命令再重启,花的时间不超过两秒,却能规避大量启动事故。

7.3 与 read-write 状态相关的其他 systemd 服务

除了systemd-remount-fs.service,还有一个服务值得关注,叫systemd-fsck-root.service。它在 remount-fs 服务之前执行,做根文件系统的检查。如果 fsck 发现文件系统有异常,它可能会直接要求 remount-fs 不要继续 remount,从而导致根文件系统保持只读状态。这是很多“莫名其妙只读”问题根源所在。

这两者的依赖关系其实是 systemd 自动生成的:systemd-fsck-root.service成功之后才允许systemd-remount-fs.service执行。所以如果你用 tune2fs 给根文件系统设置了强制检查标记(比如设置了最大挂载次数),那么下一次启动时,fsck 会先跑,如果 fsck 由于某种原因失败了,remount 就会被卡住。

这个连环依赖是我在调试一台文件系统频繁异常断电的机器时发现的。当时根系统总是只读启动,我一直以为是 fstab 写错,后来看 journal 才发现 fsck 卡在日志重放阶段,根本没有完成检查,后续的 remount 自然就被阻塞了。

8. 实战复盘:一次完整的只读根文件系统故障修复过程

先把整个排查过程按时间线捋一遍,方便你以后遇到类似问题时按图索骥。

某次一台测试服务器意外断电之后,重启进入 emergency mode。第一反应是看日志,确认根文件系统只读。用mount查看,根分区确实以ro方式挂载。

接下来检查systemctl status systemd-remount-fs.service,状态是 failed,日志里显示 exec 失败,没有更详细的错误信息。于是我去看内核日志dmesg | tail -50,发现文件系统被标记为脏,日志设备需要 replay。也就是说,真正的元凶其实是断电导致的文件系统不一致。

这时候我没有直接强制 remount 成 rw,而是先执行了fsck.ext4 -f /dev/对应设备(以只读方式检查)。检查过程中发现大量节点错误,自动修复完成。修复完后再执行mount -o remount,rw /,这次成功了。

最后登录系统,用journalctl -u systemd-remount-fs.service -f观察正常情况下该服务的启动输出,确认一切恢复。整个过程看起来简单,但如果不理解 fstab、fsck、remount-fs 三者的联动关系,很容易走弯路。比如我不止一次看到有人遇到只读根就直接mount -o remount,rw /硬来,结果文件系统本来就有问题,强制 remount 反而导致更严重的损坏。

9. 经验收尾:你不需要记住所有细节,但必须记住这几条

跟这个服务打交道这几年,踩过的坑不少,但真正值得沉淀下来的核心经验不算多。先说最想提醒各位的一条:永远不要在系统运行的时候直接改 fstab 里的根分区条目,而不预先验证新参数的可行性。一句话总结就是,先手动 remount 验证,再改文件,最后才重启。

另一条经验是:看清楚你的内核命令行参数。很多人排查 remount 问题时,眼光都死盯着 fstab,完全忽略了内核命令行里可能存在的ro标志或者rootflags参数。实际上,在 systemd 的世界里,内核命令行对根挂载选项的影响力往往比 fstab 更大,优先级也更高。

最后一条是善用 journal 和systemd-analyze这两个工具。systemd 的复杂机制很容易让人产生“玄学感”,但它的日志记录和依赖解析工具其实已经把绝大部分问题透明化了。遇到挂载问题,先看 journal,先做 verify,再用逻辑推演,而不是像无头苍蝇一样反复重启试错,这才是资深从业者和新手的核心区别。

对我个人而言,systemd-remount-fs.service是一个完美的观察窗口,通过它可以看到 Linux 启动流程中“静态配置”和“动态执行”之间如何互相制约。弄懂了它,再去理解 systemd 里其他生成器生成的 unit,就会觉得豁然开朗。以后你在系统里看到任何看似“不知道为什么要存在”的服务,可以多留一个心眼,追一追它背后绑定的那个配置文件,往往能挖出意想不到的故事。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进