FreeBSD 上的 NFS 

Last Update: 2023-06-19

目录

FreeBSD NFS 服务端

启动服务

FreeBSD 系统内置了 NFS 的服务端与客户端,所以不需要安装其他的包。

NFSv3 依赖 3 个服务: rpcbind, nfsd 和 mountd。NFSv4 依赖 2 个服务: nfsd 和 mountd。

让 NFS 服务开机自启动,需要在 /etc/rc.conf 里添加:

# NFSv3
# /etc/rc.d/nfsd* 里面可以看到这样写会自动启动 mountd 和 rpcbind
# 所以下面两行可以不写
#rpcbind_enable="YES"
#mountd_enable="YES"
nfs_server_enable="YES"
# 允许挂载文件,需要配合 exports 里的 -alldir 使用
mountd_flags="-r"

# NFSv4
# 兼容 NFSv3
# /etc/rc.d/nfsd* 里面可以看到这样写会自动启动 mountd 和 rpcbind
nfs_server_enable="YES"
nfsv4_server_enable="YES"

# NFSv4 only
# /etc/rc.d/nfsd* 里面可以看到这样写会自动启动 mountd
nfs_server_enable="YES"
nfsv4_server_enable="YES"
nfsv4_server_only="YES"

# 在 NFSv4 的配置中
# nfsuserd 不用可以不启动
# 启动的话取消下面 2 行的注释
#nfsuserd_enable="YES"
#nfsuserd_flags="-domain <domain>"

注: /etc/rc.d/nfsd* 启动脚本内将 mountd 设为强制依赖,只要设置 nfs_server_enable="YES" 那么 mountd 也会自动启动。

要让被导出的文件系统能被支持 NFS v4 协议的 client 挂载,还要在 /etc/exports 里面加入以 V4: 开头的配置行,具体可以在翻译页面查看相关文档。

用 root 权限执行下面的命令,可启动支持 v3 和 v4 的 NFS 服务:

/etc/rc.d/rpcbind onerestart
/etc/rc.d/nfsd onerestart
/etc/rc.d/mountd onerestart

要启动只支持 v4 的 NFS 服务:

/etc/rc.d/nfds onerestart
/etc/rc.d/mountd onerestart

NFSv4 不再使用 mount protocol 而是在协议内自己实现了挂载流程,这意味着 client 挂载 server 导出的目录不再需要与 server 上的 mountd 通信。即,server 上的 mountd 可以不启动。然而, showmount 命令依赖于 mount protocol (由 server 上的 mountd 进程负责) 所以为了让 client 能查询可以挂载的路径,NFSv4 server 上需要启动 mountd 进程。但是, showmount 命令并不是要启动 mountd 进程的主要原因。

NFSv4 server 需要 mountd 的真正原因是,虽然 NFSv4 server 不再依赖 mount protocl,但是目前只有 mountd 程序可以将 exports 文件中的配置加载进内核中的 nfs 模块,这样 server 才能知道要以怎样的方式,导出哪些目录。这也意味着,当 exports 文件更新之后,只有 mountd 程序可以告诉内核中的 nfs 模块 exports 文件中的配置有哪些变化。这才是 NFSv4 server 即使不再依赖 mount protocol 也依旧需要启动 mountd 进程的原因。

此外,NFSv4 没有像 NFSv3 那样提供一个类似于 showmount(8) 一样的可以检索所有以 NFSv4 导出的目录的程序。但是通常,server 上会同时开启 NFSv4 和 NFSv3 的支持,这样被以 NFSv4 导出的目录也被以 NFSv3 导出了。这就意味着,可以用支持 NFSv3 的 showmount(8) 程序来检索所有被导出的目录。只使用 NFSv4 的话 server 就不会启动 rpcbind(8) 程序,showmount(8) 也就无法使用了。

修改 /etc/exports 文件后,必须让 mountd 服务根据新的内容重新生成配置。一种方法是通过给正在运行的服务程序发送 HUP 信号来完成:

kill -HUP `cat /var/run/mountd.pid`

或指定适当的参数来运行 /mountd rc(8)/ 脚本:

/etc/rc.d/mountd onereload

exports 常用配置参数

比如,指定两个 client 操作服务器数据时具有 lsz 用户的身份,其他 client 只能读取:

V4: /

/mnt/mymir2/data -alldirs -mapall=lsz 192.168.1.20
/mnt/mymir2/data -alldirs -mapall=lsz 192.168.1.30
/mnt/mymir2/data -alldirs -mapall=lsz -ro

/mnt/mymir2/videos -alldirs -mapall=lsz 192.168.1.20
/mnt/mymir2/videos -alldirs -mapall=lsz 192.168.1.30
/mnt/mymir2/videos -alldirs -mapall=lsz -ro

其中:

mapall 参数的详细说明

mapall 的写法是 -mapall=user[:group[:group...]]

关于这几个 map 的参数,其实 FreeBSD 官方文档说的也不是特别清楚。给几个例子帮助理解。现有用户:

# id lsz
uid=1001(lsz) gid=1001(lsz) groups=1001(lsz),0(wheel)

-mapall=lsz-mapall=lsz: 不是一回事: 前者是指 lsz 用户以及该用户所属的所有组,当 lsz 用户不满足文件操作的权限时,使用 lsz 用户所属的组再次尝试,直到 lsz 所属的所有组都被遍历一遍。后者是只使用 lsz 这个用户组,当文件操作权限不足时,不再以其他身份尝试。

例 1 有如下目录与 exports 配置:

# ll
drwxrwxr-x   3 nobody  wheel   3 Jun 11 15:40 tmp/

# cat /etc/exports
/storage0/tmp -alldirs -mapall=lsz: 192.168.2.11

tmp 目录被 client 挂载后 client 可以在 tmp 目录下创建文件,其判定过程为:

  1. -mapall=lsz: 表示只使用 lsz 用户作为操作身份,不遍历其所属用户组
  2. 开始使用 ugo 权限判定 lsz 用户是否可以写入:
    1. tmp 目录属于 nobody 用户,lsz 用户无法写入,判定写入失败
    2. tmp 目录属于 wheel 用户组,lsz 属于 wheel 用户组,wheel 用户组对 tmp 目录有写权限,lsz 用户可以写入,判定写入成功
  3. 写入成功

例 2 有如下目录与 exports 配置:

# ll
drwxr-xr-x   3 nobody  wheel   3 Jun 11 15:40 tmp/

# cat /etc/exports
/storage0/tmp -alldirs -mapall=lsz: 192.168.2.11

其与例 1 的不同在于 tmp 目录的属组的 w 权限没了。tmp 目录被 client 挂载后 client 不能在 tmp 目录下创建文件,其判定过程为:

  1. -mapall=lsz: 表示只使用 lsz 用户作为操作身份,不遍历其所属用户组
  2. 开始使用 ugo 权限判定 lsz 用户是否可以写入:
    1. tmp 目录属于 nobody 用户,不是 lsz,判定为 lsz 用户无法写入,判定写入失败
    2. tmp 目录属于 wheel 用户组,lsz 属于 wheel 用户组,wheel 用户组对 tmp 目录没有写权限,lsz 用户无法写入,判定写入失败
    3. other 没有写权限,lsz 用户无法写入,判定写入失败
  3. 写入失败

-mapall=lsz:nobody 中 lsz 不必属于 nobody 组: 这种写法会遍历 lsz 用户, lsz 用户组, wheel 用户组, nobody 用户组。挑一个能执行操作的身份来完成操作。

例 3 有如下目录与 exports 配置:

# ll
drwxr-xr-x   3 nobody  nobody   3 Jun 11 15:40 tmp/

# cat /etc/exports
/storage0/tmp -alldirs -mapall=lsz:nobody 192.168.2.11

tmp 目录被 client 挂载后 client 不能在 tmp 目录下创建文件,其判定过程为:

  1. -mapall=lsz:nobody 表示使用 lsz 用户作为操作身份,在操作失败时并且遍历其所属用户组与 nobody 组作为操作身份
    • 判定时遍历的顺序为 lsz 用户, lsz 用户组, wheel 用户组, nobody 用户组
  2. 开始使用 ugo 权限判定 lsz 用户是否可以写入:
    1. tmp 目录属于 nobody 用户,lsz 用户无法写入,判定写入失败
    2. tmp 目录属于 nobody 用户组,lsz 用户不属于 nobody 用户组,lsz 用户无法写入,判定写入失败
    3. other 没有写权限,lsz 用户无法写入,判定写入失败
  3. 开始使用 ugo 权限判定 lsz 用户组是否可以写入:
    • tmp 目录属于 nobody 用户组,不是 lsz 用户组,lsz 用户组无法写入,判定写入失败
  4. 开始使用 ugo 权限判定 wheel 用户组是否可以写入:
    • tmp 目录属于 nobody 用户组,不是 wheel 用户组,wheel 用户组无法写入,判定写入失败
  5. 开始使用 ugo 权限判定 nobody 用户组是否可以写入:
    • tmp 目录属于 nobody 用户组,nobody 用户组对 tmp 目录没有写权限,判定写入失败
  6. 写入失败

例 4 有如下目录与 exports 配置:

# ll
drwxrwxr-x   3 nobody  nobody   3 Jun 11 15:40 tmp/

# cat /etc/exports
/storage0/tmp -alldirs -mapall=lsz:nobody 192.168.2.11

其与例 3 的区别在于 nobody 用户组多了 w 权限。tmp 目录被 client 挂载后 client 可以在 tmp 目录下创建文件,其判定过程为:

  1. -mapall=lsz:nobody 表示使用 lsz 用户作为操作身份,在操作失败时并且遍历其所属用户组与 nobody 组作为操作身份
    • 判定时遍历的顺序为 lsz 用户, lsz 用户组, wheel 用户组, nobody 用户组
  2. 开始使用 ugo 权限判定 lsz 用户是否可以写入:
    1. tmp 目录属于 nobody 用户,lsz 用户无法写入,判定写入失败
    2. tmp 目录属于 nobody 用户组,nobody 用户组对 tmp 目录有写权限,lsz 用户不属于 nobody 用户组,lsz 用户无法写入,判定写入失败
    3. other 没有写权限,lsz 用户无法写入,判定写入失败
  3. 开始使用 ugo 权限判定 lsz 用户组是否可以写入:
    • tmp 目录属于 nobody 用户组,不是 lsz 用户组,lsz 用户组无法写入,判定写入失败
  4. 开始使用 ugo 权限判定 wheel 用户组是否可以写入:
    • tmp 目录属于 nobody 用户组,不是 wheel 用户组,wheel 用户组无法写入,判定写入失败
  5. 开始使用 ugo 权限判定 nobody 用户组是否可以写入:
    • tmp 目录属于 nobody 用户组,nobody 用户组对 tmp 目录有写权限,判定写入成功
  6. 写入成功

踩坑

不推荐使用 ZFS 的 sharenfs 功能

Solaris 上 ZFS 的 sharenfs 可以直接操作内核里的 NFS。但在 FreeBSD 上,sharenfs 做的是把配置写入 /etc/zfs/exports 再使用 FreeBSD 的 NFS 服务器根据这个文件重新生成导出配置。

但是 FreeBSD ZFS 上这样的 sharenfs 操作有时会出一些奇怪的问题。至少在 FreeBSD 13 还没解决。另外,FreeBSD 用的是 ZoL (ZFS on Linux) 而不是 Solaris 的 ZFS 代码,我猜这可能是问题的起因吧。

虽然对于一些简单的 NFS share 使用 sharenfs 没什么问题,但还是不建议用它。

使用传统的 exports(5) 文件是最稳妥的选择。

NFS v3 相关服务的重启顺序很重要

和 NFS v3 相关的程序有三个: nfsd, mountd 和 rpcbind

如果想要重启整个 NFS v3 服务,那么这三个服务的重启顺序应该是: rpcbind -> nfsd -> mountd

重启 rpcbind 时,nfsd 和 mountd 在 rpcbind 中的注册信息会丢失,这就导致此时 NFS 服务不可用。必须要再次重新启动 nfsd 和 mountd 两个进程使之重新在 rpcbind 中注册。

NFSv4 就没有这个问题,直接重启 nfsd 即可。

嵌套挂载的 ZFS 卷需要分别导出

zpool 上有这些 zfs 卷:

% zfs list
NAME                 USED  AVAIL     REFER  MOUNTPOINT
mymir2              2.55T  81.1G      104K  /mnt/mymir2
mymir2/data         99.7G  81.1G     99.7G  /mnt/mymir2/data
mymir2/videos       2.46T  81.1G     2.46T  /mnt/mymir2/videos

也就是说,以 mymir2 为根节点的树上,有 3 个节点,其中 mymir2/data 和 mymir2/videos 为两个同级的叶子节点。

此时要导出整个 mymir2 根节点 (即客户端挂载根节点 mymir2 后可以直接操作表现为两个子目录的两个叶子节点),那么如果在 /etc/exports 里写:

/mnt/mymir2 -alldirs -maproot=root -network 192.168.1.0 -mask 255.255.255.0

执行 kill -HUP mountd 重载配置文件之后,在客户端 (比如 windows) 使用 mount -o nolock -o mtype=hard -o timeout=60 \\192.168.1.10\mnt\mymir2 K:\ 进行挂载。

此时在 windows 上的 K:\ 路径下只会看到 data 和 videos 两个空目录。这意味着,对应的 mymir2/data 和 mymir2/videos 两个子文件系统 (叶子节点) 并没有被 NFS share 导出。对于 K:\data 和 K:\videos 的读写操作会直接作用在 mymir2 这个根节点上,不会进入叶子节点。

如果想要导出一个根节点和两个叶子节点 (一共 3 个 zfs 文件系统),只能在 /etc/exports 里这样写,分别导出:

/mnt/mymir2 -alldirs -maproot=root -network 192.168.1.0 -mask 255.255.255.0
/mnt/mymir2/data -alldirs -maproot=root -network 192.168.1.0 -mask 255.255.255.0
/mnt/mymir2/videos -alldirs -maproot=root -network 192.168.1.0 -mask 255.255.255.0

每个 exports 项只能导出一个文件系统。挂载时,也只能分别将导出的文件系统挂载到不同的挂载点。

FreeBSD NFS 客户端

FreeBSD 系统内置了 NFS 的服务端与客户端,所以不需要安装其他的包。

让 NFS 客户端开机自启动,需要在 /etc/rc.conf 里添加:

nfs_client_enable="YES"

用 root 权限执行 =/etc/rc.d/nfsclient onerestart= 可立即启动 NFS 客户端。

mount_nfs <ip>:/<shared_path> /<path_to_mount> 挂载 NFS 共享。

在 mount_nfs 命令中可以通过使用 -o nfsv3 或者 -o nfsv4 来指定使用的 NFS 版本。通常,mount_nfs 命令会自动与 server 从高版本向低版本协商。

要让 NFS share 在系统启动时自动挂载,则需要在 /etc/fstab 文件中添加:

<ip>:/<shared_path> /<path_to_mount> nfs rw 0 0