☰
NFS /etc/exports 参数默认值与优先级:嵌入式开发避坑指南
2026/10/1 12:25:57 网站建设 项目流程

很多人配置/etc/exports的时候有个习惯:写完共享目录和允许的主机网段,顺手保存,然后就去客户端挂载了。你问他中间那几个括号里的参数是什么,他可能想都不想回你一句"默认的就行"。

但问题恰恰出在这个"默认"上。我是在拿 RK3568 开发板做 NFS 根文件系统启动的时候被狠狠教育过一次的:明明/etc/exports里写了共享,板子却反复报只读、报Operation not permitted,甚至直接VFS: Unable to mount root fs。后来用exportfs -v一看,一堆默认参数悄悄生效,和我以为的完全不是一回事。这篇就把 NFS/etc/exports的参数默认值和优先级一次讲透,适合正在搭 NFS 的嵌入式开发、Linux 服务器运维,以及想在 Ubuntu 上快速搭 NFS 共享的人。

1. 先把这一行拆开读:exports 文件的解析顺序与匹配逻辑

/etc/exports每行看起来简单,拆开却有三个部分:导出目录、主机说明符、括号里的选项列表。语法骨架是这样的:

导出目录 主机1(选项1,选项2) 主机2(选项3,选项4) 主机3

这里最容易踩的第一个坑就是:选项只对它前面的那个主机说明符生效。看个例子:

/home/tftp 192.168.1.100(rw,sync) 192.168.1.0/24(ro)

192.168.1.100拿到rw,sync,但整个网段里的其他机器拿到的是ro。如果你写成了下面这样,那所有机器都只能读:

/home/tftp 192.168.1.100 192.168.1.0/24(rw,sync)

注意,192.168.1.100后面没有括号,它就是一个裸主机说明符,所有选项默认。而括号里的rw,sync只挂在后面的网段上。这种无意识的缩进错误,比参数配错更隐蔽。

1.1 主机说明符的顺序和通配符陷阱

主机说明符支持好几种写法:单个 IP、主机名、*.example.com这种通配符域名、192.168.1.0/24网段、以及最粗暴的*。

我见过不少人想当然地认为"越具体的匹配优先级越高"。这句话在这里不成立。NFS 导出匹配遵循的是exports(5)里一个很容易被忽略的规则:如果一台客户端同时被同一导出行里的多个主机说明符匹配,按先出现者优先,而不是按谁更具体。

举个例子:

/home/nfs *(rw,sync,no_root_squash) 192.168.1.10(rw,sync,no_root_squash,no_subtree_check)

当192.168.1.10这台机器来访问时,它先匹配到了第一个*,后面那个专门给它写的主机说明符根本不生效。如果你想给某个 IP 特殊待遇,必须把它放在*前面:

/home/nfs 192.168.1.10(rw,sync,no_root_squash,no_subtree_check) *(rw,sync,no_root_squash)

这个顺序问题在嵌入式调试里特别常见。板子的 IP 是固定的,你本来想给它额外的insecure或no_subtree_check,结果因为通配符排在前面,半天没生效。

1.2 括号和逗号的边界问题与同目录多行

选项列表还有一些书写细节,出错率也很高。

第一,括号前面不要加空格。/home/nfs 192.168.1.0/24 (rw,sync)会被解析成两个主机说明符:一个裸 IP 网段和一个括号为开头的异常项,轻则行为诡异,重则导出失败。

第二,同一个主机说明符内,不要同时写互相冲突的选项,比如ro,rw。虽然多数发行版会取后一个,但 man 手册里明确说这种行为未定义,不同 nfs-utils 版本的解析结果可能都不一样,属于典型的自我埋坑。

第三,同一个共享目录尽量只导出一行。如果你写了:

/home/nfs 192.168.1.0/24(rw,sync) /home/nfs 192.168.2.0/24(ro)

不同版本的内核和 exportfs 工具对"同一目录多次导出"的处理方式并不一致,有的会合并选项,有的会直接覆盖。规范做法是把所有主机说明符放在同一行里,靠顺序控制匹配结果。

2. 不写选项时 NFS 悄悄给你的默认值

这是本文最核心的部分。很多人以为"没写就是没限制",实际上 Linux NFS 服务的默认值保守得很,甚至可以说有点坑。我把常用参数和默认值整理成了下面这张表,都是基于现代 Linux 发行版(nfs-utils 较新版本)的行为:

参数默认值不指定时的实际效果备注
ro/rwro只读,客户端就算用rw参数挂载也只能读需要写操作时务必显式写rw
sync/asyncsync(现代版本)写入请求实时落到服务端存储极老版本可能是async,别赌默认
root_squash/no_root_squashroot_squash客户端 root 被压缩成匿名用户 nobody嵌入式 NFS 根文件系统必须显式关掉
all_squash/no_all_squashno_all_squash普通用户按各自 uid 映射安全要求高才用all_squash
anonuid/anongid-2(显示为 nobody/65534)匿名用户映射到的 uid/gid需要固定权限时可显式指定
secure/insecuresecure强制客户端源端口小于 1024某些板卡 U-Boot 网络栈会因端口问题失败
wdelay/no_wdelaywdelay合并多个写请求延迟落盘,提升吞吐小文件交互场景可显式no_wdelay
subtree_check/no_subtree_check历史上为subtree_check每次访问检查文件是否仍在导出子目录内NFSv4 下建议显式no_subtree_check
secsec=sys使用 AUTH_SYS 明文身份,无加密敏感数据场景需要换krb5p
fsid自动生成NFSv4 伪文件系统根节点标识多目录 NFSv4 挂载时建议显式规划

2.1 读写与同步:ro、rw、sync、async

先说读写。/etc/exports中如果不写ro也不写rw,默认是只读。这个默认值坑了非常多的人。你挂载目录、列出文件一切正常,一写文件就报Read-only file system。服务端exportfs -v一看,显示ro,从头到尾没人写过这个单词,它就是默认值。

再说同步。现代 nfs-utils 默认sync,即 NFS 写入请求返回成功时,数据已经写入服务端存储。async则允许先返回成功、稍后再落盘,性能高一些,但服务端异常断电时丢数据的概率也大。嵌入式开发场景我强烈建议显式写sync,因为开发板经常直接断电重启,async的窗口期可能导致根文件系统数据损坏。

2.2 权限与匿名映射:root_squash、all_squash、anonuid

root_squash是第二个大坑。它的作用是:当客户端以 root 访问 NFS 时,服务端把 root 映射成匿名用户nobody(uid 通常为 65534)。这样客户端 root 在共享目录里就没有超管权限了。

这种设计本身是保护服务端的好机制,但如果你在调试一块 RK3568 开发板,板子上的根文件系统通过 NFS 挂载,所有初始化进程都是 root 身份运行,遇到root_squash就会表现出一堆诡异问题:touch一个文件提示Permission denied,chmod不生效,服务起不来。开发阶段最常见的操作就是在/etc/exports里显式加no_root_squash。

all_squash更极端,它把所有客户端用户都映射成匿名用户。通常只有在共享公共目录、不想让任何人保留 uid 时才用。anonuid/anongid则负责自定义这个"匿名用户"到底是谁,比如你希望所有映射后的文件都属于www-data,就可以写all_squash,anonuid=33,anongid=33。

2.3 端口、子目录、文件句柄:secure、subtree_check、fsid

secure这个默认值在嵌入式场景里很让人头疼。它的含义是:客户端连接 NFS 服务时,源端口必须小于 1024。传统上这是出于安全考虑,但很多开发板早期启动阶段的 NFS 客户端、某些 U-Boot 下的网络实现、或者 NAT 转换过的流量,源端口根本不满足条件,结果就是挂载被静默拒绝。遇到挂不上、但服务端日志和网络连通性都正常的情况,可以先试试显式加insecure。

subtree_check的默认行为在历史上是开启的。它检查客户端访问的文件是否真的在导出目录的子目录里。对于根文件系统这种目录树非常庞大的共享,开启它反而会带来性能和兼容性上的困扰,NFSv4 场景尤其不友好。现在的实践几乎都是显式加no_subtree_check。

fsid是 NFSv4 伪文件系统的关键参数。NFSv4 不再像 v3 那样直接暴露绝对路径,而是通过伪文件系统把导出的多个目录串起来。如果你导出多个目录且用 NFSv4 挂载,经常需要设置fsid=0来指定伪文件系统根,否则客户端mount -t nfs4会报No such file or directory,典型的"目录明明导出了却挂不上"的坑。

3. 参数优先级:从文件行序到客户端挂载的三层规则

标题既然叫"优先级对照表",这里就得把"谁压过谁"讲清楚。我按实际生效链路把优先级拆成三层。

3.1 同一条内多主机说明符:先到先得

这一层前面已经详细展开过,结论就是:同一导出行内,多个主机说明符按出现顺序匹配,先匹配到的生效,与具体程度无关。记住把特殊主机放前面,通配符放后面,就能规避大多数问题。

3.2/etc/exports文件、exportfs 命令行与内核导出的优先级

exportfs命令是另一个修改导出参数的入口。它有几种典型用法:

exportfs -ra # 重新读取 /etc/exports 并全量应用 exportfs -v # 查看当前内核中实际生效的导出列表和选项 exportfs -u 主机:/目录 # 撤销某个导出 exportfs -o ro 主机:/目录 # 临时以指定选项导出

当exportfs -o手动指定的选项与/etc/exports文件冲突时,命令行的优先级更高。但这种优先级是有代价的:临时导出只存在于当前运行的内核导出表中,重启 NFS 服务或重启机器后就没了,别指望它持久化。一个常见的错误是用exportfs -o rw临时导出后,忘了加其他选项,结果临时目录的权限和原配置不一致,服务重启后莫名其妙"配置失效"。

另一件值得注意的事是:exportfs -ra之后,/etc/exports里同一目录的旧导出会被更新覆盖。如果你中途用exportfs -o手动加过额外选项,重读配置文件后这些选项可能会被文件里的配置重置。所以排查问题时,永远以操作后的exportfs -v实际输出为准,而不是以你记忆中"刚才设过什么"为准。

3.3 客户端 mount 选项与服务器导出选项:取更严格值

这一层最容易被误读。很多人以为只要服务端rw,客户端想怎么挂都行;或者以为服务端no_root_squash,客户端就一定能拿到 root。实际情况是,最终行为要看服务端导出选项和客户端挂载选项两者中更严格的那个:

服务端导出客户端挂载最终效果
rwrw可写
rwro只读,客户端ro生效
rorw只读,服务端ro生效,客户端无法绕过
root_squash客户端 rootroot 被压成 nobody,客户端无法绕过
no_root_squash客户端 root保留 root 权限

也就是说,root_squash是服务端行为,客户端哪怕在mount命令里写再多参数也无法关闭它;反过来说,服务端rw但客户端挂载时写了ro,那客户端这边就是只读的。很多"明明导出了 rw 却写不进文件"的排查,最后都落在客户端mount -o ro这种低级失误上。

我调 RK3568 的时候还遇到过一种情况:同一块板子,用 U-Boot 里nfsroot=...,rw启动时能写,进入系统后用mount -t nfs手动挂载却只读。查了半天,就是 U-Boot 传进去的mount选项和我手动敲的不一样,一个带rw一个没带。所以排查读写问题时,先两边各执行一次mount看实际选项,再判断是谁限制了你。

4. RK3568 挂 NFS 根文件系统:这几个参数建议每次都显式写

这部分结合具体场景给一套能直接抄走的配置。假设场景是:一台 Ubuntu 主机,导出/home/nfs/rootfs作为 RK3568 开发板的根文件系统,板子从内核启动阶段就通过 NFS 挂载根目录。

4.1 从 Ubuntu 主机开始的一整套配置

先在主机上安装 NFS 服务:

sudo apt install nfs-kernel-server sudo mkdir -p /home/nfs/rootfs

然后把要导出的目录写进/etc/exports。我调试嵌入式板卡时,长期使用的一行配置是:

/home/nfs/rootfs 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check,insecure)

逐个解释为什么这么写:

  • rw:根文件系统必须可写,否则系统起来后一堆服务无法创建运行时文件。
  • sync:开发板经常直接断电,sync能把数据丢失窗口缩到最小。
  • no_root_squash:板子里的 init 和各类服务基本都以 root 跑,不关掉 root_squash 会遇到一堆权限错误。
  • no_subtree_check:根文件系统目录树庞大,关掉子目录检查能减少性能损耗和 NFSv4 兼容问题。
  • insecure:有些开发板的内核网络栈或 U-Boot 阶段的 NFS 客户端源端口大于 1024,默认secure会直接拒绝连接。

配置完后应用并检查:

sudo exportfs -ra sudo systemctl enable --now nfs-server sudo showmount -e localhost

如果你在 Ubuntu 上开启了防火墙,还需要放行 NFS 相关端口,至少包括 2049 和 rpcbind:

sudo ufw allow from 192.168.1.0/24 to any port 2049 proto tcp sudo ufw allow rpcbind

4.2 U-Boot 的 bootargs 与几种常见报错

RK3568 板子通常在 U-Boot 环境变量里配置启动参数,核心是这一串:

setenv bootargs 'root=/dev/nfs rw nfsroot=192.168.1.100:/home/nfs/rootfs,v3,tcp ip=dhcp'

root=/dev/nfs告诉内核根设备走 NFS,rw是内核挂载根文件系统时的挂载选项,nfsroot后面用逗号分隔 NFS 版本和传输协议。这里我显式指定了v3,tcp,因为嵌入式板卡早期启动阶段走 NFSv3 最成熟、最不容易出幺蛾子。如果你需要 NFSv4,除了内核参数加v4,还要把/etc/exports里的导出配合fsid=0来规划,两边的配合复杂度明显上升。

启动过程中如果卡住了,常见报错和对应思路如下:

  • VFS: Unable to mount root fs via NFS:这是最笼统的提示,后面通常跟具体错误码。先查三件事:主机能不能 ping 通板子、导出的路径是否和nfsroot一致、宿主机 NFS 服务是否在运行。很多时候是板子通过ip=dhcp拿到的 IP 不在192.168.1.0/24网段内,和/etc/exports的允许网段不匹配。
  • mount.nfs: Operation not permitted:优先查root_squash和secure这两个默认值。把/etc/exports改成no_root_squash加insecure,基本上能解决。
  • mount.nfs: No such file or directory:这个提示很误导人,目录明明存在。最常见原因是 NFSv4 伪文件系统问题,要么把nfsroot里的版本改回v3,要么服务端导出时显式设置fsid=0。

4.3 挂载后的权限验证方法

确认系统起来后,直接在板子上执行一组测试,验证参数是否真正生效:

touch /test_write # 验证可写 chown 0:0 /test_write # 验证 root 权限 ls -l /test_write # 看 owner 是否变成 nobody df -h / # 看挂载点和容量

如果chown 0:0 /test_write之后ls -l显示 owner 是nobody或者数字 65534,说明root_squash还在生效,主机端/etc/exports里的no_root_squash没有正确应用。这时候回到主机执行exportfs -v,大概率能看到配置被什么路径覆盖了。

5. 用 exportfs 和 showmount 核对最终生效参数

配置写对了还不够,关键是确认内核里"实际生效的参数"和你以为的一样。NFS 的坑就在于:文件里写得好看,不代表内核里跑的就是这份。

5.1exportfs -v是排查参数的最终依据

在 Ubuntu 主机执行:

exportfs -v

输出类似:

/home/nfs/rootfs 192.168.1.0/24(rw,sync,wdelay,root_squash,no_subtree_check,sec=sys,secure,no_all_squash)

注意看,即使你在/etc/exports里没写wdelay、没写root_squash,输出里照样可能出现这些单词,因为它们是被默认值补上的。只要看到不想生效的参数出现在这一行里,说明配置里默认值正在起作用。例如我前面那个"只写了/home/nfs/rootfs 192.168.1.0/24(rw,sync)却总是权限报错"的现场,exportfs -v 一输出,root_squash和secure赫然在列,问题一目了然。

exportfs -v可以看到带选项的列表,而showmount -e localhost只能看到导出了哪些目录和允许哪些主机,看不到选项细节。所以别依赖 showmount 判断参数,它是用来快速确认"导出是否存在"的。

5.2 客户端侧的确认手段

板子系统起来或手动挂载后,在客户端执行挂载命令并查看实际协商参数:

mount -t nfs -o rw 192.168.1.100:/home/nfs/rootfs /mnt mount | grep /mnt

mount输出会列出客户端视角看到的一些挂载选项,但注意:root_squash、secure这类服务端选项不会完整反映在客户端 mount 输出里,它们由服务端内核强制执行。所以最终的判断标准,永远是服务端exportfs -v的输出加上客户端表现出的行为。

5.3 实在查不出问题时,抓包是最后的底牌

如果读写法都正常、exportfs -v输出也说得通,但板子就是挂不上根文件系统,我建议直接在宿主机上抓包:

sudo tcpdump -i eth0 port 2049 -s 0 -w /tmp/nfs.pcap

然后让板子重新启动挂载 NFS,抓个几十秒后在 Wireshark 里打开,重点看 NFSv3 MOUNT 和 NFS 协议头里的状态码。NFS3ERR_ACCES对应权限问题,NFS3ERR_NOTSUPP往往是版本或选项不支持,NULL大量重传则提示网络链路都不通。抓包能帮你把"服务端配置问题"和"客户端网络问题"快速分开,不用靠猜。

我自己调试 RK3568 时最典型的一次就是:exportfs -v显示root_squash还在,但/etc/exports明明已经改成了no_root_squash。后来发现是exportfs -ra没执行,旧的内核导出表还挂着老的参数,重启服务后一切恢复正常。所以一套流程走完,最后一步永远是exportfs -ra && exportfs -v,把文件配置和内核实际状态对齐。这套"默认值意识 + 优先级意识 + 核对命令"的习惯养成了,NFS 的绝大多数坑都能在你动手之前先被排掉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询