应用保存配置、追加日志、读取图片时,通常只看到文件名和读写接口。内核却需要回答更多问题:这个名字属于哪个目录,目录位于哪个文件系统,调用者有没有权限,数据当前是在内存还是已经具备持久化保证?这些问题决定了服务能否启动,也决定了机器异常重启以后还能找回什么。
本课建立文件、目录、inode、路径和挂载的基本模型,再把权限、页缓存与同步放回这个模型。读完后,应能解释“文件存在却打不开”“目录还在但数据盘没挂上”“写入成功却丢失最后的记录”等现象,并能为服务安排明确的可写路径。这里讨论公开 POSIX 与 Linux 接口层面的通用行为,不展开具体文件系统的内部实现。
文件抽象:把数据交给名字与接口
普通文件可以先理解为由偏移量定位的字节序列。偏移量表示从文件开头计算的位置,应用通过读取和写入访问其中的字节。文本、图片、压缩包在这层模型中都没有特殊地位;内容的解释方式由应用决定。扩展名帮助人或程序选择处理方式,却不会自动让内核把文件变成某种格式。
目录负责组织名字,普通文件负责承载内容。Linux 还把设备等对象纳入文件接口,但这不表示它们都能像普通文件那样任意定位或永久保存。看到“都能打开”,应想到接口可以相近;判断能否跳转位置、能否同步、是否占用磁盘空间,仍要看具体对象的语义。
应用调用 open 后得到文件描述符,它是进程用来引用已打开对象的整数。随后的 read、write 等调用通过这个描述符工作。路径主要用于找到并打开对象,不要求每次读取都重新从根目录寻找。由此可以理解:名字发生变化,不必使已经打开的对象立即失效。
内核把应用请求转成文件系统和存储设备可以处理的操作。应用无需自行决定字节落在哪个物理位置,却必须遵守接口返回值。例如 write 返回正数表示本次接受的字节数,可能少于请求量;返回错误则需要处理。仅仅调用过写入函数,不能作为“所有数据已经保存”的证据。

设应用准备保存 4096 字节配置。它需要检查打开是否成功,继续处理可能的短写,并检查同步等关键操作的结果。如果磁盘空间不足,错误可能出现在写入阶段,也可能延迟到后续同步阶段。封装库减少了重复代码,但不会取消这些边界。应先明确所用库的“保存完成”究竟承诺了什么。
文件系统提供的抽象让应用能够使用稳定的名字和接口;它没有替应用决定保存策略。临时缓存允许重建,业务记录可能要求确认后不可丢失,这 2 类文件即使都由 write 写入,也需要不同的完成条件。
inode 与目录:名字和对象分开理解
inode 是文件系统中描述对象的结构,通常记录对象类型、所有者、权限、大小以及定位数据所需的信息。inode 编号在所属文件系统内标识对象;不同文件系统可以出现相同编号,因此不能把编号单独当作整台机器通用的身份。此处只建立识别模型,不追踪数据块如何排列。
目录可以理解为“名字到对象标识”的映射。例如目录 /data 中的 report.txt 指向某个 inode,而 inode 再关联文件内容。文件名不是普通文件 inode 自带的唯一路径。目录自身也是文件系统对象,拥有自己的 inode、所有者和权限;修改目录项与修改文件内容是不同的操作。
硬链接说明了这种分离。为普通文件建立另一个硬链接后,新增名字指向原来的 inode,内容没有因此复制。通过任意名字修改内容,其他名字看到的也是同一对象。硬链接通常不能跨文件系统建立,因为目标 inode 的身份依赖其所属文件系统。复制文件则创建独立对象,后续修改可以彼此独立。
符号链接保存的是目标路径文本。解析路径时遇到它,内核按规则继续寻找目标;目标可以在其他文件系统,也可能根本不存在。相对目标路径以符号链接所在目录为基准解释,而非以执行命令时的工作目录为基准。硬链接与符号链接都能让对象通过另一个名字被访问,但它们依赖的关系不同。

删除普通文件名通常意味着移除目录项。若还有硬链接,文件仍可通过其他名字访问;若名字已全部移除,但进程仍持有打开的引用,对象占用的空间通常还不能立即回收。等相关引用释放后,文件系统才能回收相应资源。这解释了删除大日志后 df 可用空间没有马上增加的常见现象。
重命名也应按目录项理解。在同一文件系统内,用 rename 替换目标名字可以提供原子的名称切换:其他访问者不会看到名字只替换了半截。已经打开旧对象的进程可以继续访问旧对象,后来按路径打开的进程则访问新对象。原子切换不等于断电后必然保留;跨文件系统的移动也不能直接套用同样的保证。
路径与挂载:沿着目录走,遇到边界再切换
绝对路径从进程所见的根目录 / 出发;相对路径从当前工作目录出发。路径 /srv/app/config/settings.json 可以拆成若干分量,内核依次查找 srv、app、config,最后找到 settings.json。中间某段不存在、不是目录或不允许穿越,都会让最终访问失败。
. 表示当前目录,.. 表示父目录。路径看似只是字符串,却不能总靠字符串替换判断最终对象:符号链接会改变后续解析位置,挂载会改变某个目录下可见的文件系统。涉及访问控制时,应由实际的路径解析与受约束的接口确定目标,不能只检查路径文本是否带有某个前缀。
以从工作目录 /srv/app 打开 config/settings.json 为例,先在工作目录找 config,再进入该目录找 settings.json。若 config 是指向 /etc/myapp 的符号链接,后续查找会转到对应位置。如果 /etc/myapp 所在文件系统只读,成功读取配置并不意味着可以在那里写回配置。

挂载把某个文件系统接入当前可见的目录树。挂载点是接入位置,例如把数据盘上的文件系统挂到 /srv/data。应用仍使用普通路径 /srv/data/orders.log,但走到挂载点后,后续查找进入被挂载的文件系统。路径表达的是可见位置,不直接告诉应用背后的设备是什么。
挂载前,/srv/data 可以是根文件系统中的普通目录,里面也可能已有文件。挂载后,这些原有内容被新挂载的内容遮住,并没有自动复制或删除。卸载后才会重新显现。反过来,如果预期的数据盘没有挂载,应用可能把文件写进根文件系统上同名的目录,造成“业务能运行,但根盘被写满”。

挂载可以附带只读等选项。文件自身的权限位允许写入,也不能越过只读挂载的限制。规划服务路径时,需要同时问“目录允许谁写”和“承载目录的文件系统是否允许写”。容器等环境还可能让进程看到不同的挂载视图;排障应尽量在服务实际运行的环境里观察,不能直接把另一处终端所见的树当作服务所见。
假设 /srv/data 上的 latest 是指向 releases/v2 的符号链接,访问 /srv/data/latest/config.json 时,既要经过挂载边界,又要解释链接目标。符号链接本身不提供目标可用性的保证。迁移路径后,应重新确认链接解析结果、目标权限与挂载状态,而不只是检查链接文件是否还在。
权限位:文件能做什么,目录能走到哪里
传统权限把访问者分成所有者、所属组和其他用户 3 类,每类都有读 r、写 w、执行 x 这 3 位。基础判断中,匹配所有者就看所有者位,否则匹配所属组就看组位,再否则看其他用户位,并不是把各类权限任意相加。实际 Linux 还可能受 ACL、安全模块等机制影响,本课先掌握传统权限模型。
对普通文件,读允许读取内容,写允许修改内容,执行允许按可执行对象的规则尝试运行。执行位不会让任意字节自动变成可运行程序。对目录,读允许列出名字,执行表示搜索或穿越,写则与创建、删除、重命名目录项有关;实际修改目录项通常还需要该目录的执行权限。
目录权限是初学者最容易混淆的地方。拥有目录的执行权限而没有读权限时,可能无法列出目录内容,却能访问名字已知且自身权限允许的文件。反过来,能够列出名字也不表示可以打开其中对象。路径上每个需要经过的目录都要满足相应搜索权限,末端文件可读并不能补偿沿途目录不可穿越。
删除文件主要受父目录权限约束,不以文件自身可写为必要条件。共享目录还可能设置 sticky 位来限制谁能删除其中条目。因而,把重要文件改成只读,并不能单独防止有权修改其父目录的人删除这个名字。需要把文件内容权限与目录项管理权限分别检查。
八进制权限数字把每类的读、写、执行分别记为 4、2、1。例如 640 表示所有者可读写、组可读、其他用户无权限;目录的 750 表示所有者可读写和穿越、组可读和穿越、其他用户无权限。目录若误设为 640,即使看起来“有读权限”,也缺少常用的穿越能力。

新建对象的最终权限还受到创建请求与 umask 等规则影响。umask 用来屏蔽权限位,不是为任意对象补齐执行位。例如请求 666 且 umask 为 027 时,在没有默认 ACL 等额外影响的基础模型下,得到 640。不能把某次终端里建出的权限直接当作服务启动后也会得到的结果。
以专用账户运行服务时,可以让程序与配置归管理账户所有,让数据目录归服务账户或适当的服务组管理。可写范围应覆盖真实需求,并有清晰用途。遇到拒绝访问,先检查进程身份、沿途目录和挂载选项,再修正具体问题。把所有路径都设为 777 会扩大能修改内容或目录项的主体,也掩盖原本的部署错误。
页缓存与持久化:成功返回分别承诺了什么
沿用页缓存的概念,常见的普通文件缓冲写入先把数据交给内核管理的内存页,并将相应页面标为脏页。内核稍后安排回写,把修改向存储栈提交。这样可以合并操作、减少等待;代价是应用看到写入成功时,数据可能仍依赖尚未完成的后续工作。
应区分应用自己的缓冲区、内核页缓存和存储设备。标准库的缓冲输出可能尚未调用内核写入接口;例如 fflush 主要把标准 I/O 缓冲中的数据推向底层接口,不等价于保证已经落到持久介质。普通 close 也不能作为通用的持久化屏障。讨论“刷盘”前,应先说清跨越的是哪层边界。
fsync(fd) 用于请求把该文件修改过的数据和所需元数据同步到持久存储,并等待操作完成。应用必须检查返回结果。fdatasync 侧重数据以及后续正确读取这些数据所必需的元数据;选择哪种接口,应依据所需保证和支持情况。不能仅凭函数名称判断错误处理可以省略。
这些接口的保证还依赖文件系统、驱动、设备与相关缓存正确实现持久化协议。成功同步是应用能够使用的重要边界,却不等于任意硬件故障下都不会丢失,也不自动成为备份。对持久性的要求,应说明故障假设,例如突然掉电、进程崩溃或整块设备损坏;不同故障需要不同措施。

文件内容和目录项也有不同的同步需求。在 Linux 常见本地文件系统上,新建文件后仅对文件执行 fsync,通常还不足以保证其目录项也已经持久化。需要持久化名称变更时,还应按支持的方式同步相关父目录。路径涉及跨目录变更时,应识别受影响的目录,并核对目标环境的具体语义。
可靠替换配置的常见思路是:在目标目录创建临时文件,写完整内容并检查结果,对临时文件执行 fsync;关闭并检查错误后,在同一文件系统内重命名替换目标,再对父目录执行 fsync。它把完整内容准备、名称原子切换与持久化分开处理。每步出错都需要明确的返回或恢复策略,不能把最后的成功提示提前到中间步骤。
这仍不是多文件事务。例如先替换配置,再替换索引,进程可能在 2 次替换之间崩溃。业务若要求它们保持一致,需要额外的版本或恢复协议。本课的边界是识别文件系统接口提供什么保证,避免从单个文件的原子替换推出整个业务操作都具有原子性。
交互采用理想化的追加写模型:写入速率固定,每隔一段时间完成 1 次成功同步。同步间隔越长,最近尚未确认持久化的数据量可能越大。例如每秒写入 100 条、每隔 5 秒完成同步时,故障前可能存在接近 500 条尚未获得保证的记录。这是无阻塞等假设下的示意,不是实际系统的严格上界;同步耗时、失败和积压都会改变结果。
如果业务要求“已确认订单不能因掉电丢失”,就应让业务确认发生在相应数据完成所需同步之后,并处理同步失败。如果允许丢失最近 5 秒的遥测样本,可以采用不同策略,但仍需验证负载高峰时的表现。文件系统不会替业务定义哪次返回才算对用户作出了承诺。
观察 df 与 mount:从可见路径追到承载位置
命令观察应从实际报错路径出发。df -h /srv/data 显示该路径所处文件系统的容量信息;df -i /srv/data 查看 inode 使用情况。有可用字节不代表一定能新建文件:inode 可能耗尽,配额也可能限制写入。容量显示宜当作诊断线索,不能代替对实际操作返回值的检查。
mount 不带参数时可列出当前挂载信息,帮助确认来源、挂载点和选项。Linux 上的 findmnt -T /srv/data 可查找承载指定路径的挂载。若数据盘缺失,它可能返回覆盖该目录的根文件系统,因此还要核对实际目标和来源是否符合预期,不能把“命令查到了结果”等同于“预期挂载已就绪”。
pwd
ls -ld /srv /srv/data
ls -l /srv/data
stat /srv/data
findmnt -T /srv/data
mount
df -h /srv/data
df -i /srv/data
这些命令用于只读观察;在实际环境使用时将示例路径换成目标路径。ls -ld 检查目录本身,ls -l 列出目录条目,stat 提供类型、权限、大小、inode 等信息。列表能否成功同样受权限影响。报告排查结论时,应同时记录观察环境、路径和挂载来源,避免只保存某个百分比。
如果 df 显示使用量很高,而目录遍历统计偏小,可能有已删除但仍被进程打开的文件,也可能涉及遍历范围、挂载遮蔽或其他统计口径差异。应先提出可核对的解释,再追查持有文件的进程。不要为“释放空间”直接删除更多不明用途的文件,更不要把重启当作理解引用关系的替代。
只读挂载、权限不足和空间不足都可能表现为“保存失败”,但修正方法不同。前者要检查挂载策略与路径规划,中者要核对身份和权限,后者要检查容量、inode 与配额。准确记录系统调用或库提供的错误信息,能把含糊现象转成可以逐项验证的原因。
例如,上传服务把每个分片保存成独立的小文件。在 inode 数量有限的文件系统上,大量分片可能先耗尽 inode,此时 df -h 仍显示剩余容量,新建文件却返回 ENOSPC。对已有文件继续写入有时仍能成功,因为它不必分配新的 inode,但仍需要可用数据空间。排查时应结合 df -i 确认瓶颈,并核对分片是否已合并、是否仍被任务引用,再清理过期对象。仅截断文件会释放部分数据空间,却保留文件名和 inode,因而不能解决这种 inode 耗尽。
常见坑:写完即落盘,还有哪些相似误判
“读回来正确,说明已经落盘”并不成立。读取可能直接命中页缓存,甚至仍由应用缓冲提供。它说明当前读取链路能获得期望数据,却没有验证掉电之后的状态。同样,在文件管理器里看到新名字,也只能说明当前可见的目录状态,不能替代内容与目录项的同步保证。
“文件只有很小,肯定能完整写入”也不能作为协议。小文件仍可能遇到错误,跨多次写入仍可能留下中间状态。需要完整发布配置时,可通过临时文件与重命名组织读者可见的状态,再补齐同步和错误处理;直接覆盖原文件时,应接受并处理失败后旧内容已被部分修改的可能性。
“目录存在,所以数据盘存在”混淆了路径与挂载。目录可能由镜像预先创建,也可能留在根文件系统中。启动检查要验证实际挂载来源、目标与可写状态,并按业务需要决定数据盘缺失时拒绝启动还是降级。若允许降级,应明确写到哪里以及重启后如何处理,避免静默改变持久化位置。
“同步成功,所以不需要备份”则混淆了持久化和恢复范围。同步面向已约定的存储故障模型,不负责恢复误删、错误覆盖或整块介质损坏。备份与恢复演练属于另外的责任。规划时可以同时需要同步与备份,分别解释它们防范什么,以及恢复后允许回到哪个时间点。
用一次写入时序核对这些误判。服务把 8 KiB 配置写进临时文件并 rename 到正式路径,若只调用 write 而不对文件与父目录做 fsync,进程崩溃后可能只留下旧配置或空文件;用 write 返回成功到真正落盘之间,在繁忙云盘上常见还有数毫秒到数十毫秒的窗口。验收应在写入路径中注入杀进程或断电模拟,确认重启后读到的是完整新版本,而不是“刚才 cat 看见过”的缓存视图。备份则另计:即使 fsync 成功,误删后仍需要能回到例如 15 分钟前的快照点。
案例:只读根文件系统配合数据盘
设某服务的程序部署在 /opt/order-app,启动配置放在 /etc/order-app,订单记录放在 /srv/order-data。根文件系统以只读方式运行,数据盘挂载到 /srv/order-data。这样可以把程序发布与业务数据的生命周期分开:替换程序镜像时,不把业务数据当成镜像内容重新创建。
只读根并不意味着服务无需写入。应提前盘点数据库文件、业务追加记录、临时文件、缓存和运行状态文件。需要跨重启保留的数据放在数据盘;可重建的临时内容可放在单独的内存文件系统等位置,并明确容量与重启后丢失的性质。程序若默认向安装目录写日志,应调整配置或输出方式,使写入位置符合部署约定。

权限安排可以让管理账户拥有程序与配置,让专用服务账户拥有数据目录;临时目录也只授予必要主体访问。只读挂载提供挂载层限制,所有权与权限则限定写入主体。数据盘上的文件系统可写,并不意味着服务应能写入盘上所有目录;多个服务共享存储时仍需划分各自的数据边界。
启动顺序要求先准备并检查挂载,再启动依赖它的服务。检查不能只问路径是否存在,应核对预期来源、挂载点和选项,再按运行身份确认所需访问能力。若数据盘缺失,应给出明确错误并停止依赖持久数据的工作。只读根可以阻止部分误写,却不能替代启动依赖和运行时监测。
记录订单时,服务先完成必要写入和同步,再按业务协议确认成功。磁盘满、文件系统转为只读或同步失败时,服务应停止作出无法兑现的成功承诺,并报告可诊断的错误。监测容量、inode、写入错误与同步延迟,是为了在数据路径失效或性能恶化时及时采取措施。
重启恢复还要确认重新挂载的是原有数据,而非意外初始化的新目录。应用应按既定格式识别完整记录,处理未完成尾部,并使恢复过程可重复。这个案例不要求选择某种具体文件系统,而是要求程序、权限、挂载与业务确认共同遵守清晰的边界。
放进方案里
在部署方案中,应先列出服务会读取和写入的路径,并为每个可写目录说明用途、承载位置、写入身份、预计容量和保留期限。程序与静态配置可以位于只读根文件系统,业务数据放在明确挂载的数据盘,可重建缓存与临时文件另行安排。说明哪些内容在重启后必须存在,哪些允许丢失,避免把“有目录”当成存储已经准备好。
为数据盘定义启动依赖和失败行为。检查应核对实际挂载来源、目标与选项,并在服务运行环境中确认访问能力;数据盘未就绪时,依赖持久数据的服务应拒绝启动或进入已经定义的降级状态。权限设计同时覆盖文件与沿途目录,以专用账户授予必要访问能力,不能只记录末端文件的权限数字。
持久化策略要与业务确认位置对应。明确普通写入成功、同步成功和对外确认分别发生在何处,写清允许丢失的数据窗口,以及同步失败、空间不足、只读错误时如何处理。若采用临时文件替换配置,应描述内容同步、名称切换与父目录同步的顺序;涉及多份文件的一致性,则补充独立的恢复规则。
容量规划应同时关注字节、inode、日志增长和临时文件峰值,并设置可执行的清理与告警策略。可在验收中检查挂载缺失、权限错误、写满和异常重启等情形,观察服务是否给出正确状态以及恢复后保留了哪些数据。记录所验证的文件系统与设备假设,不能用单次正常关机的结果替代异常恢复证据。
最后,把备份保留与恢复步骤写进运维约定,说明整盘损坏或误删时从哪里恢复、预计需要多久以及可能损失多少数据。交付材料应使接手者能从业务路径找到实际挂载、从运行身份判断访问权限、从成功响应追到持久化边界,并能依据监测与错误记录定位故障。
小结与自测:带着存储边界走向 H14
文件接口提供读写抽象,目录把名字映射到对象,inode 记录对象属性与数据定位信息。路径解析逐段前进,挂载把多个文件系统接入可见目录树。权限既约束内容,也约束能否穿越目录以及修改目录项。普通缓冲写入、同步完成与业务确认是需要明确区分的状态。
可以用以下问题检查自己的模型:为什么 2 个名字可能共享内容?为什么删掉日志后空间可能没有立即释放?为什么文件可读却因父目录权限而打不开?为什么数据目录存在仍不能证明数据盘已挂载?为什么写完再读正确仍不能证明断电后可恢复?回答时应指出对应的对象、路径或持久化边界,而不只给出命令。
再把案例中的同步间隔从 5 秒改为 1 秒:理想模型下潜在未确认数据量如何变化?真实系统还要测量哪些延迟和失败?若要求已确认的订单不丢失,成功响应又应该放在哪里?这些问题把文件系统行为连接到业务语义。
后续进入 H14 时,可继续追问存储硬件如何承接操作系统发出的请求、不同层次的完成信号意味着什么。本课先留下可迁移的判断方法:从名字找到对象,从路径找到承载位置,从权限找到访问主体,从成功返回找到它实际承诺的边界。
下一课转向 S6 Linux 入门:把本章验收过的结论带进后续主题,写成可检查的方案约束。