NFS 导出把服务器文件系统路径与客户端及访问策略关联起来。在 Linux 上,exportfs 根据 /etc/exports、/etc/exports.d 下合格文件或命令行更新,维护 /var/lib/nfs/etab。rpc.mountd 是守护进程,不是第三份配置文件;其 MOUNT 服务与旧版协议流程有关,NFSv4 挂载并不要求这个独立服务。应先明确版本和服务职责,再解释客户端的结果。
配置文本可能与有效导出不同,但这只是拒绝访问的一种原因,不能从文件看起来正确就断定导出表过期。还要核对目标服务器、协议、导出策略、身份及文件权限。服务器的持久性和客户端缓存也要分开:应用看到一次写入成功,并不单独证明服务器已把相应数据提交到稳定存储。目录可见、访问获准和写入可恢复,是不同层次的事实。
文件与表
/etc/exports 的条目把目录与客户端及选项关联。/srv/data 10.0.0.0/24(rw,sync,no_subtree_check,root_squash) 展示一个网络,后面还能追加有自己选项的客户端组。主机名、网段和 netgroup 有各自匹配规则。exportfs -ra 会协调配置与导出,但其他 exportfs 操作或服务管理也能应用改变;它不是唯一生效途径,不能把所有差异都归因于忘记运行这一条。
showmount -e 向服务器 MNT 服务询问公布的导出。信息完整性取决于实现,只有 NFSv4 的服务器甚至可能不暴露此服务。因此它既不是完整有效策略转储,也不证明某客户端一定能访问共享。原文四条配置对三条导出以及 NOENT 的故事没有找回原始运行记录;缺失导出也不能保证所有客户端总收到同一个错误码。诊断应保留实际返回信息。
选项真正决定了什么
导出默认只读,rw 才允许写入,但文件权限及其他控制仍然生效。sync 约束服务器按稳定存储要求回复,async 则允许改动提交前回复,崩溃可能丢失已确认的数据。两者都不会消除客户端缓存,也不让每次应用写入自动变成同步持久提交。nfs-utils 在 1.0.0 版本之后把默认从 async 改为 sync;理解版本历史时要与实际客户端操作分开。
root_squash 把请求中的零 uid/gid 映射到匿名身份,no_root_squash 关闭该保护。all_squash 映射全部身份,anonuid/anongid 指定匿名值。这些选项不是客户端认证,也不能让任意公开共享自动安全。subtree_check 既有安全影响,也可能干扰打开文件后的重命名;nfs-utils 1.1.0 起默认改为 no_subtree_check。应按真实导出层级和信任边界做选择。
挂载,以及 id 映射陷阱
示例 mount -t nfs -o vers=4.2 server:/srv/data /mnt/data 要求服务器支持该版本且路径适用于实际导出。NFSv4 路径相对于导出命名空间,fsid=0 标识导出根,并不要求开放宿主机整个根文件系统。hard 挂载会持续重试超时请求,soft 则可能返回 EIO 并带来数据损坏风险。挂载选项本身不能保证某次进程一定处于指定状态。
在 AUTH_SYS 下,数字请求身份参与权限检查;服务器找不到某个非零 ID 对应的账户名字,不会自动把它匿名化。NFSv4 属主名字映射和更强认证又引入其他身份边界。应结合 id、ls -ln、squash 策略及映射配置检查。显示为 65534 或 nobody,可能来自有意匿名映射,也可能是映射失败,所以它是需要调查的现象,不是单一原因的证明。
坏掉的样子
导出确实过期时,需要核对包含 /etc/exports.d 及命令行覆盖的预期配置,再协调并检查结果。exportfs -ra 会改变服务器策略,也可能影响已有访问,执行前须审核。配置不一致与名称解析、网络可达、协议支持或权限失败不同,不能因为一个客户端操作失败就直接刷新策略。还应记录变更后的有效结果,而不只是保留编辑过的文件。
服务器不可用会让操作等待,严重影响应用,却不能因此断言所有碰到 hard 挂载的进程永久无法终止。原文三十五秒 showmount 失败在此没有原始证据,它也不能代表内核挂载客户端的超时行为。intr 在 Linux 2.6.25 之后被忽略;改为 soft 必须权衡数据完整性,不能只是为了让等待更短,就把潜在读写错误当作已经修复服务。
快速验证
以下原命令保持不变并暂缓执行。它们混合了服务器检查、对 localhost 的 MNT 查询、策略修改以及客户端观察,却没有分别绑定主机。本轮没有操作 NFS 服务、发送 RPC、刷新导出或执行挂载;这段内容也不能作为实际读写或恢复通过的证据:
exportfs -v
showmount -e localhost
exportfs -ra && exportfs -v
mount | grep 'type nfs'
id; ls -ln /mnt/data | head
应比较策略,而不是打印出来的选项顺序。下面的离线提案只支持一条 /srv/data IPv4 网段导出,采用 nfs-utils 自 1.1.0 起的文档默认值。它展开省略的默认项,拒绝冲突、重复和未知选项,并比较原样路径与规范化网段。主机名、netgroup、通配符、多种安全机制及额外记录均不支持,不能静默当成等价。
import ipaddress
import re
# A single-export, numeric IPv4-network fixture; not a general exports parser.
def export_policy(text):
match = re.fullmatch(
r"(/srv/data)[ \t\n]+([0-9./]+)\(([^()\s]+)\)\n", text)
if match is None or "/" not in match[2]:
raise ValueError("incomplete or unsupported export record")
client = ipaddress.IPv4Network(match[2], strict=True)
mask = match[2].split("/")[1]
if mask not in (str(client.prefixlen), str(client.netmask)):
raise ValueError("noncanonical prefix or unsupported hostmask")
groups = [
("ro", "rw"), ("sync", "async"), ("wdelay", "no_wdelay"),
("hide", "nohide"), ("no_subtree_check", "subtree_check"),
("secure", "insecure"), ("root_squash", "no_root_squash"),
("no_all_squash", "all_squash")]
defaults = {group[0]: group[0] for group in groups}
defaults.update(sec="sys", anonuid=65534, anongid=65534)
owner = {token: group[0] for group in groups for token in group}
seen = set()
for token in match[3].split(","):
if token in owner:
name, value = owner[token], token
elif token == "sec=sys":
name, value = "sec", "sys"
elif re.fullmatch(r"anon(?:uid|gid)=[0-9]+", token):
name, raw = token.split("=")
value = int(raw)
if not 0 <= value < 2**32 - 1:
raise ValueError("unsupported anonymous ID")
else:
raise ValueError(f"unsupported option: {token}")
if name in seen:
raise ValueError(f"duplicate or conflicting option: {name}")
seen.add(name)
defaults[name] = value
return match[1], str(client), defaults
def compare_exports(configured, effective, *, complete, returncode, stderr):
if (complete is not True or type(returncode) is not int or returncode != 0
or type(stderr) is not str or stderr != ""):
raise ValueError("failed, partial or unverified capture")
if export_policy(configured) != export_policy(effective):
raise ValueError("export path, client network or policy differs")
# Fabricated complete inputs exercise comparison only; no NFS command runs.
configured = "/srv/data 10.0.0.0/24(rw,sync,no_subtree_check,root_squash)\n"
effective = ("/srv/data\n\t10.0.0.0/255.255.255.0"
"(sync,wdelay,hide,no_subtree_check,sec=sys,rw,secure,"
"root_squash,no_all_squash)\n")
compare_exports(configured, effective, complete=True, returncode=0, stderr="")
print("synthetic policy comparison passed; live NFS remains untested")
预期只输出合成策略比较通过的消息。若要建立真实证据,必须取得同一明确服务器上的完整成功采集,覆盖所有配置输入和预期命令行覆盖;不能根据过滤后的输出自行声称完整。其他配置须单独审阅。本轮没有采集、RPC、挂载或访问测试;本机发现和属主列表也不能证明访问权限。完整授权实验仍需明确服务器与客户端、按协议检查,并单独批准策略修改;此比较提案不因此获得真实 NFS 执行通过。
来源
- exports(5),导出语法、squash 选项、sync/async 与 subtree_check 默认值,已核验 2026-09-16。
- exportfs(8),/var/lib/nfs/etab 中的主导出表以及 exportfs 如何维护它,已核验 2026-09-16。
- nfs(5),挂载选项,含 hard/soft、timeo 以及 intr 的失效,已核验 2026-09-16。
- showmount(8),从客户端查询服务器的导出列表,已核验 2026-09-16。