<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Soul Of Free Loop &#187; NFS</title>
	<atom:link href="https://zohead.com/archives/tag/nfs/feed/" rel="self" type="application/rss+xml" />
	<link>https://zohead.com</link>
	<description>Uranus Zhou&#039;s Blog</description>
	<lastBuildDate>Sat, 19 Jul 2025 15:42:46 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.8</generator>
	<item>
		<title>在Docker容器中使用FUSE文件系统</title>
		<link>https://zohead.com/archives/docker-fuse/</link>
		<comments>https://zohead.com/archives/docker-fuse/#comments</comments>
		<pubDate>Tue, 19 Dec 2017 16:32:18 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[User-mode Linux]]></category>
		<category><![CDATA[FUSE]]></category>
		<category><![CDATA[httpfs2]]></category>
		<category><![CDATA[NFS]]></category>
		<category><![CDATA[Slirp]]></category>
		<category><![CDATA[UML]]></category>
		<category><![CDATA[VDE]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[文件系统]]></category>

		<guid isPermaLink="false">https://zohead.com/?p=1518</guid>
		<description><![CDATA[容器使用 FUSE 的问题 我们一般使用的 Docker 容器都是非特权容器，也就是说容器内的 root 用户并不拥有真正的 root 权限，这就导致很多属于系统管理员的操作都被禁用了。 最近有个在 IBM Bluemix 容器内部挂载 FUSE 文件系统的需求，例如我使用 davfs2 挂载 WebDAV 服务器不出意外地会报错： mount.davfs 命令报错表示无法打开 fuse 设备，而 fuse 设备实际上是存在的（说明 fuse 模块也已经加载了）： 从容器内部可以查看到 cgroup 实际允许访问的设备，并没有包含 fuse 设备： 手工允许 fuse 设备自然也是不可行的：  [&#8230;]]]></description>
				<content:encoded><![CDATA[<h2 id="docker-fuse-problem">容器使用 FUSE 的问题</h2>
<p>我们一般使用的 Docker 容器都是非特权容器，也就是说容器内的 root 用户并不拥有真正的 root 权限，这就导致很多属于系统管理员的操作都被禁用了。</p>
<p>最近有个在 IBM Bluemix 容器内部挂载 FUSE 文件系统的需求，例如我使用 davfs2 挂载 WebDAV 服务器不出意外地会报错：</p>
<pre class="brush: bash; title: ; notranslate">
root@instance-007a20ff:~# mount.davfs https://dav.jianguoyun.com/dav/ /mnt/
Please enter the username to authenticate with server
https://dav.jianguoyun.com/dav/ or hit enter for none.
  Username: xxx@xx.com
Please enter the password to authenticate user fcoe@qq.com with server
https://dav.jianguoyun.com/dav/ or hit enter for none.
  Password: 
mount.davfs: can't open fuse device
mount.davfs: trying coda kernel file system
mount.davfs: no free coda device to mount
</pre>
<p>mount.davfs 命令报错表示无法打开 fuse 设备，而 fuse 设备实际上是存在的（说明 fuse 模块也已经加载了）：</p>
<pre class="brush: bash; title: ; notranslate">
root@instance-007a20ff:~# cat /sys/devices/virtual/misc/fuse/dev
10:229
</pre>
<p>从容器内部可以查看到 cgroup 实际允许访问的设备，并没有包含 fuse 设备：</p>
<pre class="brush: bash; title: ; notranslate">
root@instance-007a20ff:~# cat /sys/fs/cgroup/devices/devices.list
c 1:5 rwm
c 1:3 rwm
c 1:9 rwm
c 1:8 rwm
c 5:0 rwm
c 5:1 rwm
c *:* m
b *:* m
c 1:7 rwm
c 136:* rwm
c 5:2 rwm
c 10:200 rwm
</pre>
<p>手工允许 fuse 设备自然也是不可行的：</p>
<pre class="brush: bash; title: ; notranslate">
root@instance-007a20ff:~# echo &quot;c 10:229 rwm&quot; &gt; /sys/fs/cgroup/devices/devices.allow
-bash: /sys/fs/cgroup/devices/devices.allow: Permission denied
</pre>
<p>另外由于 Bluemix 提供的是非特权容器，即使允许访问 /dev/fuse 设备也会因为没有 mount 权限而挂载失败。</p>
<h2 id="uml-modify">UML 系统修改</h2>
<p>虽然 Docker 容器内部不能直接挂载使用 FUSE 文件系统，但我想到如果用 User-mode Linux（以下简称 UML） 来实现在应用层再运行一个 Linux kernel，就可以在 UML guest 系统中挂载 FUSE 文件系统了，而且 UML 系统中也可以通过 hostfs 直接访问容器本身的文件系统。</p>
<p>有关 UML 的介绍和编译使用可以参考我之前写的 <a href="https://zohead.com/archives/openvz-uml-bbr/">小内存OpenVZ VPS使用UML开启BBR</a> 文章。</p>
<p>首先我们需要修改 UML kernel 配置：</p>
<ul>
<li>增加对 FUSE 的支持，这个是必须的了，否则无法在 UML guest 系统中使用 FUSE 文件系统；</li>
<li>增加了对 xfs、btrfs、ext4、squashfs、iso9660 等常见文件系统的支持（方便在 UML 系统中挂载各种文件系统镜像）；</li>
<li>另外为了支持将 UML guest 的文件系统导出，启用了 sunrpc 和 NFS 服务器支持；</li>
<li>UML 网络配置中增加了对 Slirp 和 VDE 网卡的支持。</li>
</ul>
<p>UML kernel 目前支持常见的几种网络模式：</p>
<ul>
<li>TUN/TAP <br />
最简单和常用的模式，不过 host kernel 需要支持 tun 或者 tap 设备，这个在 Docker 容器中一般都不可用的；</li>
<li>SLIP <br />
SLIP 串行线路 IP 支持，现在一般很少用到了，同样 host kernel 需要支持 slip 设备；</li>
<li>Slirp <br />
通过用户层的 slirp 程序实现 SLIP 连接，好处是不依赖任何内核层的设备，而且 slirp 可以支持非 root 用户使用，不过 Slirp 只支持模拟 IP 协议的数据包，详细可以参考 <a href="http://slirp.sourceforge.net/" target="_blank">Slirp</a> 开源项目的官方网站。</li>
<li>VDE <br />
VDE（Virtual Distributed Ethernet）可以在不同的计算机间实现软件定义的以太网络，同样支持在用户层以非 root 用户身份来运行，目前 Linux 上的 QEMU 和 KVM 虚拟机都支持 VDE 虚拟网络。</li>
</ul>
<p>鉴于我们需要在 Docker 容器中运行 UML 系统，目前只能使用 Slirp 和 VDE 模式的网卡，另外单独的 slirp 程序使用起来相对 VDE 也更简单（支持 VDE 的 UML kernel 可以直接调用 slirp 程序，不像 VDE 还需要预先使用 <code>vde_switch</code> 等命令配置软件交换机），因此这里的 UML 系统就使用 Slirp 网卡了。</p>
<p>当然原来基于 busybox 的 UML 系统用户层也做了些修改：</p>
<ul>
<li>增加 libfuse 支持挂载 FUSE 文件系统；</li>
<li>增加 rpcbind、nfs-common、nfs-kernel-server 等软件包，支持在 UML 系统中运行 NFS 服务器，导出 UML guest 的文件系统；</li>
<li>增加 dropbear，支持 SSH 和 SFTP 服务器和客户端了；</li>
<li>增加 httpfs2 FUSE 文件系统支持，来自 GitHub 上的 <a href="https://github.com/Tomas-M/httpfs2-enhanced" target="_blank">httpfs2-enhanced</a> 项目，方便挂载访问 HTTP 和 HTTPS 远程文件；</li>
<li>增加 archivemount FUSE 文件系统支持，方便直接挂载 zip、rar、tar.gz 等各种格式的压缩包以支持直接访问压缩包中的文件；</li>
<li>增加 <a href="http://curlftpfs.sourceforge.net" target="_blank">CurlFtpFs</a> FUSE 文件系统支持，支持挂载 FTP 远程文件；</li>
<li>增加 <a href="http://savannah.nongnu.org/projects/davfs2" target="_blank">davfs2</a> FUSE 文件系统支持，支持挂载远程 WebDAV 目录；</li>
<li>增加 <a href="https://github.com/libfuse/sshfs" target="_blank">sshfs</a> FUSE 文件系统支持，支持通过 SFTP 方式挂载远程主机目录。</li>
</ul>
<blockquote>
<p><strong>提示</strong></p>
<ul>
<li>httpfs2-enhanced 最好使用支持 SSL 和多线程的版本，可以加快响应速度并能挂载 HTTPS 的远程文件；</li>
<li>我在测试中发现 httpfs2-enhanced 工具在国内的网络环境下挂载某些 HTTP 远程文件存在一些问题，因此做了简单的修改，并 fork 到我自己的 GitHub 仓库里了。有需要的朋友可以参考我修改过的 <a href="https://github.com/zohead/httpfs2-enhanced" target="_blank">httpfs2-enhanced</a>，检出其中的 <a href="https://github.com/zohead/httpfs2-enhanced/tree/http-fix" target="_blank">http-fix</a> 分支即可，我也已经针对原项目提交了 Pull request。</li>
</ul>
</blockquote>
<p>为了方便使用，我给原来的 <code>uml-linux.sh</code> 执行脚本增加了新的 <code>uml.conf</code> 配置文件：</p>
<pre class="brush: bash; title: ; notranslate">
UML_ID=&quot;umlvm&quot;
UML_MEM=&quot;64M&quot;

UML_NETMODE=&quot;slirp&quot;
#HOST_ADDR=&quot;192.168.0.3&quot;
#UML_ADDR=&quot;192.168.0.4&quot;
#UML_DNS=&quot;&quot;

TAP_DEV=&quot;tap0&quot;
ROUTER_DEV=&quot;eth0&quot;
VDE_SWITCH=&quot;&quot;

REV_TCP_PORTS=&quot;&quot;
REV_UDP_PORTS=&quot;&quot;
#FWD_TCP_PORTS=&quot;111 892 2049 32803 662&quot;
#FWD_UDP_PORTS=&quot;111 892 2049 947 32769 662 660&quot;
</pre>
<p>简单说明如下：</p>
<ul>
<li><code>UML_MEM</code> 指定为 UML 系统分配多少内存，默认 64 MB；</li>
<li><code>UML_NETMODE</code> 比较重要，指定 UML 系统的网卡模式，目前支持 <code>tuntap</code>、<code>slirp</code>、<code>vde</code> 这三个选项；</li>
<li>如果是 <code>tuntap</code> 网卡模式，<code>TAP_DEV</code> 指定 host 主机上的 TUN 网卡设备名称，可以使用 <code>HOST_ADDR</code> 配置 host 主机 TUN 网卡的 IP 地址，<code>UML_ADDR</code> 配置 UML guest 主机的 IP 地址（最好和 <code>HOST_ADDR</code> 在同一网段），如果需要端口转发，那么还需要修改 <code>ROUTER_DEV</code> 指定 host 主机用于转发的物理网卡设备名称；</li>
<li>如果是 <code>slirp</code> 网卡模式，那么会直接使用 Slirp 默认固定的专用地址：<code>10.0.2.2</code> 为 host 主机地址，<code>10.0.2.15</code> 为 UML guest 主机的 IP 地址，并自动将 UML guest 系统内的 DNS 服务器地址设置为 <code>10.0.2.3</code> 通过 host 主机进行域名解析；</li>
<li>如果是 <code>vde</code> 网卡模式，那么必须修改 <code>VDE_SWITCH</code> 指定 VDE 软件交换机的路径，VDE 软件交换机需要通过 <code>vde_switch</code> 等命令预先配置，详细使用说明可以参考 <a href="http://wiki.virtualsquare.org/wiki/index.php/VDE_Basic_Networking" target="_blank">Virtualsquare VDE Wiki</a> 页面；</li>
<li><code>FWD_TCP_PORTS</code> 和 <code>FWD_UDP_PORTS</code> 指定进行转发的 TCP 和 UDP 端口列表（多个转发端口以空格隔开），转发端口支持 <code>10080-80</code> 这种形式（表示将 host 主机的 10080 端口转到 UML guest 的 80 端口），上面 <code>uml.conf</code> 中的注释列出来的是 UML 系统中对外的 NFS 服务器所需要的端口（根据实际情况也可以只允许 111、892、2049 这几个端口）。</li>
</ul>
<p>为了能够根据 <code>uml.conf</code> 文件配置 Slirp 的端口转发功能，我还为 slirp 程序增加了一个 <code>slirp.sh</code> wrapper 脚本：</p>
<pre class="brush: bash; title: ; notranslate">
#!/bin/sh
DIR=&quot;$( cd &quot;$( dirname &quot;$0&quot; )&quot; &amp;&amp; pwd )&quot;

[ -f $DIR/uml.conf ] &amp;&amp; . $DIR/uml.conf

CMD=`which slirp-fullbolt 2&gt;/dev/null || which slirp`

for i in $FWD_TCP_PORTS; do
	if [ &quot;x$i&quot; = &quot;x*&quot; ]; then
		continue
	fi
	CMD=&quot;$CMD \&quot;redir ${i%-*} ${i##*-}\&quot;&quot;
done
for i in $FWD_UDP_PORTS; do
	if [ &quot;x$i&quot; = &quot;x*&quot; ]; then
		continue
	fi
	CMD=&quot;$CMD \&quot;redir udp ${i%-*} ${i##*-}\&quot;&quot;
done

eval &quot;exec $CMD&quot;
</pre>
<p>由于 UML kernel 调用 slirp 程序时不支持附加参数，这里才通过 <code>slirp.sh</code> 脚本来实现，功能也非常简单，通过 slirp 程序的 redir 选项配置端口转发。</p>
<blockquote>
<p><strong>提示</strong></p>
<p>为了让 UML guest 系统的 Slirp 网络真正达到接近 host 主机的网络性能，host 主机上编译 slirp 程序时必须打开 <code>FULL_BOLT</code> 开关，并为 slirp 源代码打上 <a href="https://www.mail-archive.com/user-mode-linux-user@lists.sourceforge.net/msg07014.html" target="_blank">real full bolt</a> 的 patch，否则 UML guest 系统内通过 Slirp 访问外网的速度会很慢。</p>
<p>值得庆幸的是 Debian、Ubuntu 系统自带的 slirp 软件包一般都打上了这个 patch，而且提供了 <code>slirp-fullbolt</code> 和 <code>slirp</code> 这两个程序分别对应 <code>FULL_BOLT</code> 开启和关闭的 Slirp，我的 <code>slirp.sh</code> 脚本也对此做了判断，优先使用速度更快的 <code>slirp-fullbolt</code> 程序。</p>
</blockquote>
<p>至于新的启动 UML 系统的脚本 <code>uml-linux.sh</code> 稍微有点长，这里就不贴出来了，和原来相比的改动就是增加对 Slirp 和 VDE 网络的支持。</p>
<p>另外新的 <code>uml-linux.sh</code> 脚本改为默认前台方式启动 UML 系统；如果需要以后台方式启动 UML 系统，则可以用 <code>uml-linux.sh -D</code> 的方式来运行。</p>
<h2 id="uml-mount-fuse">使用 UML 挂载 FUSE 文件系统</h2>
<p>修改之后支持 FUSE 和 NFS 服务器的 UML 系统可以从这里下载（百度网盘备用）：</p>
<p><a href="https://zohead.com/downloads/uml-fuse-nfsd-x64.tar.xz">https://zohead.com/downloads/uml-fuse-nfsd-x64.tar.xz</a> <br />
<a href="https://pan.baidu.com/s/1bp6l7B5" target="_blank">https://pan.baidu.com/s/1bp6l7B5</a></p>
<p>解压缩下载下来的 <code>uml-fuse-nfsd-x64.tar.xz</code> 文件，运行其中的 <code>uml-linux</code> 脚本就可以启动 UML 系统了，此 UML 系统的 root 用户默认密码为 <code>uml</code>。</p>
<p>下面我以挂载 HTTP 远程 iso 文件中的安装包为例，介绍如何在 UML 系统中使用 FUSE。</p>
<p>首先使用 httpfs2 挂载阿里云上的 CentOS 6.9 iso 文件（这里为 httpfs2 指定 <code>-c /dev/null</code> 参数是为了去掉 httpfs2 的网络访问输出日志），挂载成功之后可以查看挂载点中的文件：</p>
<pre class="brush: bash; title: ; notranslate">
~ # httpfs2 -c /dev/null http://mirrors.aliyun.com/centos/6/isos/x86_64/CentOS-6.9-x86_64-minimal.iso /tmp
file name:      CentOS-6.9-x86_64-minimal.iso
host name:      mirrors.aliyun.com
port number:    80
protocol:       http
request path:   /centos/6/isos/x86_64/CentOS-6.9-x86_64-minimal.iso
auth data:      (null)
httpfs2: main: connecting to mirrors.aliyun.com port 80.
No SSL session data.
httpfs2: main: closing socket.
httpfs2: main: connecting to mirrors.aliyun.com port 80.
httpfs2: main: keeping socket open.
file size:      427819008
httpfs2: main: closing socket.
~ # ls -l /tmp
total 0
-r--r--r--    1 root     root     427819008 Mar 29  2017 CentOS-6.9-x86_64-minimal.iso
</pre>
<p>我们可以发现 httpfs2 的挂载点中实际上就只有一个远程文件名，我们可以直接挂载这个 iso 文件：</p>
<pre class="brush: bash; title: ; notranslate">
~ # mount -t iso9660 -o ro,loop /tmp/CentOS-6.9-x86_64-minimal.iso /media
~ # ls /media
CentOS_BuildTag                GPL                            RPM-GPG-KEY-CentOS-6           RPM-GPG-KEY-CentOS-Testing-6   isolinux
EFI                            Packages                       RPM-GPG-KEY-CentOS-Debug-6     TRANS.TBL                      repodata
EULA                           RELEASE-NOTES-en-US.html       RPM-GPG-KEY-CentOS-Security-6  images
</pre>
<p>到这里就可以直接查看远程 iso 文件中的软件包了，你可以很方便地将远程 iso 中的软件包拷贝到 host 主机的文件系统中。</p>
<p>如果你想直接拷贝软件包中的特定文件，也可以通过 archivemount 来实现哦：</p>
<pre class="brush: bash; title: ; notranslate">
~ # archivemount /media/Packages/sed-4.2.1-10.el6.x86_64.rpm /mnt
fuse: missing mountpoint parameter
~ # ls /mnt
bin  usr
</pre>
<p>这样直接复制软件包中的文件就实在太方便了（请忽略 archivemount 时的 fuse 警告，实际不影响使用）。</p>
<p>当然如果你想挂载 FTP 远程文件就可以通过 curlftpfs 命令来实现，也可以使用 mount.davfs 命令挂载 WebDAV 服务器上的文件（例如直接访问坚果云中的文件）。</p>
<h2 id="export-uml-fuse">导出 UML FUSE 文件系统</h2>
<p>有些情况下我们需要将 UML 系统中的 FUSE 挂载点导出给 host 主机或者网络中的其它主机使用，这时可以通过 hostfs、SFTP、NFS 等方式实现，分别简单说明一下。</p>
<h3 id="hostfs-export-fuse">hostfs 导出 FUSE</h3>
<p>这是最简单的方式，由于 UML 直接使用 hostfs 访问 host 主机的文件系统，因此 UML 系统内可以直接将 FUSE 中的文件拷贝到 hostfs 文件系统，只是这种方式 host 主机并不能直接访问 UML guest 主机的 FUSE 文件系统。</p>
<h3 id="sftp-export-fuse">SFTP 导出 FUSE</h3>
<p>这种方式也很简单，由于 UML 系统启动时自动运行了 dropbear 服务，我们可以先修改 <code>uml.conf</code> 配置文件设置 SSH 端口转发，将 host 主机的 2222 端口通过 Slirp 转发到 UML guest 系统内的 22 端口（如果 host 主机本身并没有运行 SSH 服务器，那甚至可以配置为 <code>FWD_TCP_PORTS="22"</code> 直接转发 22 端口）：</p>
<pre class="brush: bash; title: ; notranslate">
FWD_TCP_PORTS=&quot;2222-22&quot;
</pre>
<p>此时 host 主机就可以使用 scp 命令直接从 UML 的 FUSE 文件系统中拷贝文件了：</p>
<pre class="brush: bash; title: ; notranslate">
root@instance-007a20ff:~# scp -P 2222 root@192.168.1.52:/mnt/bin/sed /home
</pre>
<blockquote>
<p><strong>注意</strong></p>
<p>上面命令中的 <code>192.168.1.52</code> 应根据实际情况替换为 host 主机上实际访问网络的网卡 IP 地址，不能使用 localhost 或者 127.0.0.1，因为 slirp 默认会自动选择访问网络的网卡，并不会进行本地转发。</p>
</blockquote>
<h3 id="nfs-export-fuse">NFS 导出 FUSE</h3>
<p>首先需要修改 <code>uml.conf</code> 配置文件中的 <code>FWD_TCP_PORTS</code> 和 <code>FWD_UDP_PORTS</code> 值，将默认的 NFS 服务需要的 TCP 和 UDP 端口注释去掉，表示将这些端口转发到 UML 系统内。</p>
<blockquote>
<p><strong>提示</strong></p>
<p>如果你需要在 host 主机上直接 NFS 挂载 UML 系统里的 FUSE 文件系统，由于 sunrpc 的 111 端口无法修改而且需要转发到 UML 系统内，这种情况下 host 主机的 portmap 或者 rpcbind 服务需要保持关闭状态，防止 111 端口被占用。</p>
</blockquote>
<p>使用 <code>uml-linux.sh</code> 脚本启动 UML 系统，启动完成之后通过集成的 FUSE 相关工具挂载需要访问的 FUSE 文件系统。</p>
<p>假设需要导出 UML 系统中的 <code>/mnt</code> FUSE 文件系统，UML 中默认的 NFS 导出目录配置文件 <code>/etc/exports</code> 如下：</p>
<pre class="brush: plain; title: ; notranslate">
/mnt 0.0.0.0/0.0.0.0(async,insecure,no_subtree_check,rw,no_root_squash,fsid=0)
</pre>
<p>上面的配置文件表示将 /mnt 目录通过 NFS 导出，默认允许所有主机访问，你可以根据需要修改导出目录路径和允许访问的主机地址。</p>
<p>接着在 UML 系统中通过集成的服务脚本启动 rpcbind 和 nfs 服务：</p>
<pre class="brush: bash; title: ; notranslate">
~ # /etc/init.d/rpcbind start
Starting up rpcbind...
~ # /etc/init.d/nfs start
Starting nfs service:
fs.nfs.nlm_tcpport = 32803
fs.nfs.nlm_udpport = 32769
Exporting directories for NFS...
Starting NFS daemon: 
rpc.nfsd: address family inet6 not supported by protocol TCP
NFSD: the nfsdcld client tracking upcall will be removed in 3.10. Please transition to using nfsdcltrack.
NFSD: starting 90-second grace period (net 000000006035a880)
Starting NFS mountd:
</pre>
<p>如果一切正常的话，此时就可以在外部通过 NFS 挂载 UML 系统导出的 FUSE 文件系统进行访问了：</p>
<pre class="brush: bash; title: ; notranslate">
root@instance-007a20ff:~# mount -t nfs -o nolock,proto=tcp,mountproto=tcp 192.168.1.52:/mnt /media
</pre>
<blockquote>
<p><strong>提示</strong></p>
<p><code>nolock</code> 参数是为了防止如果挂载的主机上没有运行 portmap 或者 rpcbind 服务导致挂载失败的问题（如果直接在 host 主机上挂载，host 的对应服务需要关闭）； <br />
  <code>proto=tcp</code> 和 <code>mountproto=tcp</code> 参数指定 NFS 数据和挂载请求都使用 TCP 协议，防止使用的随机 UDP 端口无法被 slirp 转发的问题。</p>
</blockquote>
<p>这里需要说明的是如果直接在 Docker 容器（UML 的 host 主机）上挂载 NFS，虽然绝大多数 Docker 容器平台都默认支持 NFS 文件系统，但在非特权容器内部由于没有权限 mount 还是会失败的。</p>
<h2 id="summary">总结</h2>
<p>本文介绍的在容器中使用 UML 挂载 FUSE 文件系统并通过 NFS 导出 UML 文件系统的方法是一个比较小众的需求，不过也算达到我的目的了。</p>
<p>对于非特权 Docker 容器来说，虽然还不能直接挂载 UML guest 的文件系统，但初步看起来还是可以通过 <a href="https://github.com/lkl/linux" target="_blank">LKL</a>（Linux Kernel Library）在应用层访问 UML 网络并实现 NFS 挂载的，只是 LKL 库的 NFS 挂载目录并不能直接给 Docker 容器中的普通应用程序使用。</p>
<p>最后祝大家在即将到来的 2018 年能继续玩地开心 ^_^。</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/docker-fuse/feed/</wfw:commentRss>
		<slash:comments>9</slash:comments>
		</item>
		<item>
		<title>Linux下使用tcpkill工具重置NFS连接</title>
		<link>https://zohead.com/archives/tcpkill-nfs/</link>
		<comments>https://zohead.com/archives/tcpkill-nfs/#comments</comments>
		<pubDate>Wed, 14 Sep 2016 18:00:14 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[网络技术]]></category>
		<category><![CDATA[Dsniff]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[NFS]]></category>
		<category><![CDATA[RST]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[tcpkill]]></category>

		<guid isPermaLink="false">https://zohead.com/?p=1272</guid>
		<description><![CDATA[有些时候在 Linux 系统中使用 NFS 挂载远程共享（使用默认的 TCP 方式）之后，偶尔会因为网络异常出现 NFS 连接出错的问题，这种情况下使用任何 ls 或者 df 等等常用的命令对 NFS 挂载目录进行简单的查看操作都可能卡顿几十秒乃至几分钟的时间。 此时如果在别的 Linux 客户机系统上又是可以正常访问 NFS 共享的，而 NFS 服务器端考虑到有多个客户机正在使用不方便直接重启服务，一般只能等待 NFS 挂载连接恢复正常或者重启客户端系统，这样还是很麻烦的，为此我专门找了个使用 tcpkill 命令重置 NFS 连接的方法分享给大家。 tcpkill 命令属于 Dsniff  [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>有些时候在 Linux 系统中使用 NFS 挂载远程共享（使用默认的 TCP 方式）之后，偶尔会因为网络异常出现 NFS 连接出错的问题，这种情况下使用任何 ls 或者 df 等等常用的命令对 NFS 挂载目录进行简单的查看操作都可能卡顿几十秒乃至几分钟的时间。</p>
<p>此时如果在别的 Linux 客户机系统上又是可以正常访问 NFS 共享的，而 NFS 服务器端考虑到有多个客户机正在使用不方便直接重启服务，一般只能等待 NFS 挂载连接恢复正常或者重启客户端系统，这样还是很麻烦的，为此我专门找了个使用 tcpkill 命令重置 NFS 连接的方法分享给大家。</p>
<p>tcpkill 命令属于 Dsniff 嗅探工具包，Dsniff 本身是一个高级的网络嗅探器，Dsniff 可以将制造的数据包注入到网络，一般 Linux 系统中可以找到对应的 dsniff 软件包进行安装，我测试使用的 CentOS 6.1 64 位系统使用的下面的 RPM 安装包：</p>
<p><a href="http://rpm.repo.onapp.com/ramdisk-hv/centos6/dsniff/">http://rpm.repo.onapp.com/ramdisk-hv/centos6/dsniff/</a></p>
<p>tcpkill 的工作原理是利用 libpcap 库监控符合过滤条件的 TCP 连接并等待，当该 TCP 连接上有数据传输时就会被 tcpkill 感知到，不过作为应用程序的 tcpkill 当然不能直接关闭 TCP 连接，此时就会构造一条 RST 报文发回去直接导致 TCP 连接异常关闭（其实 TCP RST 就是 GFW 对敏感网页进行屏蔽的老手段之一哈）。</p>
<p>对于 NFS 共享使用的 TCP 连接而言，通过 tcpkill 发送 RST 报文导致连接异常关闭之后，NFS 客户端就能知道原有的连接不可用，会自动重新开启新的 TCP 连接，这样就能达到我们需要的重置 NFS 连接的效果。</p>
<p>下面具体实验看看，首先在 NFS 服务器上使用 <code>netstat</code> 命令确认所有 TCP 方式挂载的 NFS 连接，这里使用 NFS 服务的 <code>2049</code> 端口进行过滤：</p>
<pre class="brush: bash; title: ; notranslate">
~ # netstat -anp 2&gt;/dev/null | grep 2049
tcp        0      0 0.0.0.0:2049            0.0.0.0:*               LISTEN      -
tcp        0      0 192.168.1.56:2049       192.168.1.52:953        ESTABLISHED -
tcp        0      0 192.168.1.56:2049       192.168.1.129:824       ESTABLISHED -
udp        0      0 0.0.0.0:2049            0.0.0.0:*                           -
</pre>
<p>可以看到 NFS 服务器上有 192.168.1.52 和 192.168.1.129 这两个客户端正在连接，看看 tcpkill 命令的用法：</p>
<pre class="brush: bash; title: ; notranslate">
~ # tcpkill [-i interface] [-1...9] expression
</pre>
<p>tcpkill 的 <code>-i</code> 参数指定绑定哪个网卡，<code>expression</code> 就是 tcpdump 的过滤表达式参数，具体如何使用可以参考 tcpdump 的帮助信息：</p>
<p><a href="http://www.tcpdump.org/tcpdump_man.html">http://www.tcpdump.org/tcpdump_man.html</a></p>
<p>我们就以关闭上面的 192.168.1.52 客户端使用的 NFS 连接为例：</p>
<pre class="brush: bash; title: ; notranslate">
~ # tcpkill -i eth0 host 192.168.1.52 and port 2049
tcpkill: listening on eth0 [host 192.168.1.52 and port 2049]
</pre>
<p>tcpkill 命令启动之后会一直等待新的数据传输，我们就可以在 NFS 客户端上随便运行 ls 或者 df 等命令查看 NFS 共享文件夹就可以让系统发送新的 NFS 数据（仍然是有问题的 NFS 连接所以可能会卡住），此时服务器端的 tcpkill 命令就有反应了：</p>
<pre class="brush: bash; title: ; notranslate">
~ # tcpkill -i eth0 host 192.168.1.52 and port 2049
tcpkill: listening on eth0 [host 192.168.1.52 and port 2049]
192.168.1.52:953 &gt; 192.168.1.56:2049: R 1715581713:1715581713(0) win 0
192.168.1.52:953 &gt; 192.168.1.56:2049: R 1715581896:1715581896(0) win 0
192.168.1.52:953 &gt; 192.168.1.56:2049: R 1715582262:1715582262(0) win 0
192.168.1.56:2049 &gt; 192.168.1.52:953: R 624540755:624540755(0) win 0
192.168.1.56:2049 &gt; 192.168.1.52:953: R 624540919:624540919(0) win 0
192.168.1.56:2049 &gt; 192.168.1.52:953: R 624541247:624541247(0) win 0
192.168.1.52:953 &gt; 192.168.1.56:2049: R 1715581801:1715581801(0) win 0
192.168.1.52:953 &gt; 192.168.1.56:2049: R 1715581984:1715581984(0) win 0
192.168.1.52:953 &gt; 192.168.1.56:2049: R 1715582350:1715582350(0) win 0
</pre>
<p>可以看到 tcpkill 已经发送了 RST 包导致 NFS 客户端重新连接，如果客户端的 NFS 共享操作已经恢复正常了，就可以退出 tcpkill 命令了。</p>
<p>从上面的例子我们也会发现 tcpkill 默认是只能“关闭”活跃有数据传输的 TCP 连接的；对于非活跃的连接我们可能需要主动发送 SYN 包，并根据返回的 TCP 序列号构造新的 RST 包再次发送以达到关闭连接的效果，看起来已经有网友实现了一个支持关闭非活跃连接的 tcpkill 程序，有兴趣的朋友也可以关注看看哦。最后祝大家中秋佳节玩的开心 ^_^。</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/tcpkill-nfs/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>NFS和64位inode的问题</title>
		<link>https://zohead.com/archives/nfs-64bitinode/</link>
		<comments>https://zohead.com/archives/nfs-64bitinode/#comments</comments>
		<pubDate>Thu, 24 Jul 2014 15:41:16 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[存储]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[64位]]></category>
		<category><![CDATA[enable_ino64]]></category>
		<category><![CDATA[inode]]></category>
		<category><![CDATA[inode64]]></category>
		<category><![CDATA[kernel]]></category>
		<category><![CDATA[NFS]]></category>
		<category><![CDATA[XFS]]></category>
		<category><![CDATA[文件系统]]></category>

		<guid isPermaLink="false">http://zohead.com/?p=733</guid>
		<description><![CDATA[本文同步自（最佳显示效果请点击）：https://zohead.com/archives/nfs-64bitinode/ 最近在 XFS 文件系统上使用 NFS 时发现一些比较老的 Linux 客户端在挂载时会提示 stale file handle 错误，这似乎是服务器端的 NFS 共享文件夹信息不正确了，比较奇怪为什么新的 Linux 系统又是可以挂载使用的，准备一探究竟。 首先登录到服务器端（也是 Linux 系统，RHEL6 x86_64 服务器），查看 NFS 共享文件夹的状态（NFS 共享路径为 /nfs/share2）： 上面的 ls 命令特别增加了 -i 参数用于显示文件的 i [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>本文同步自（最佳显示效果请点击）：<a href="https://zohead.com/archives/nfs-64bitinode/" target="_blank">https://zohead.com/archives/nfs-64bitinode/</a></p>
<p>最近在 XFS 文件系统上使用 NFS 时发现一些比较老的 Linux 客户端在挂载时会提示 stale file handle 错误，这似乎是服务器端的 NFS 共享文件夹信息不正确了，比较奇怪为什么新的 Linux 系统又是可以挂载使用的，准备一探究竟。</p>
<p>首先登录到服务器端（也是 Linux 系统，RHEL6 x86_64 服务器），查看 NFS 共享文件夹的状态（NFS 共享路径为 /nfs/share2）：</p>
<p><pre class="brush: bash; highlight: [3,5,7]; title: ; notranslate">
/nfs # ls -dil *
    259 drwxrwxrwx    2 root     root             6 Jul 15 15:31 share1
4294967616 drwxrwxrwx    3 root     root            24 Jul 15 16:09 share2
/nfs # ls -dil share2/*
8589934912 drwxrwxrwx    2 root     root            24 Jul 15 16:09 share2/ppp
/nfs # ls -dil share2/ppp/*
8589934913 -rwxrwxrwx    1 root     root            15 Jul 15 16:15 share2/ppp/ooo
</pre>
</p>
<p>上面的 ls 命令特别增加了 -i 参数用于显示文件的 inode 值，这时就发现 /nfs/share2、/nfs/share2/ppp 这两个文件夹和 /nfs/share2/ppp/ooo 文件的 inode 值非常大，已经超过了一般的 32 位 inode 值限制，分别为：4294967616、8589934912、8589934913，而 /nfs/share1 文件夹的 inode 值则是 259。</p>
<p>查看运行 mount 命令发现 XFS 文件系统在挂载时使用了 inode64 参数指定使用 64 位的 inode 值来存储文件，使用 64 位 inode 主要用于解决容量非常大的文件系统在有可用空间但在使用 32 位 inode 值时可能还是无法再写入文件的问题的。然后就在之前不能挂载的 Linux 客户端上重新挂载 /nfs/share1 这个文件夹，结果却可以挂载了，看来很有可能就是 64 位 inode 造成不能挂载 /nfs/share2 共享文件夹的问题。</p>
<p>后来经过查找 kernel 中 NFS 文件系统的说明，Linux kernel 在 2.6.23 版本中可以直接支持 64 位的 NFS 文件 inode 值了，而且是通过给 nfs 这个内核模块增加 enable_ino64 参数来设置的，这个参数还是运行时可写的，一般新的发行版中如果 kernel 版本高于 2.6.23 都会支持的。另外有些发行厂商例如 RedHat 会在更新中提供支持，例如 RHEL 4 Update 7 这个比较老的系统中也支持 NFS 64 位 inode 值了。</p>
<p>下面在几个常见的 Linux 客户端系统中测试对 NFS 64 位 inode 的支持情况了：</p>
<p><strong>1、Fedora Core 5 32位系统（内核版本：2.6.22）：</strong></p>
<p>32 位的 Fedora Core 5 系统内核版本 2.6.22，虽然不支持 NFS 64 位 inode（也不支持 enable_ino64 参数），但还是能挂载的，而且使用时全部显示为 32 位 inode 值，看看效果：</p>
<p><pre class="brush: bash; highlight: [4,6,9]; title: ; notranslate">
[root@zzmlinux root]# mount -t nfs 192.168.1.145:/nfs/share2 /mnt
[root@zzmlinux root]# cd /mnt/
[root@zzmlinux mnt]# ls -dil
321 drwxrwxrwx  3 root root 24 Jul 15  2014 .
[root@zzmlinux mnt]# ls -dil *
322 drwxrwxrwx  2 root root 24 Jul 15  2014 ppp
[root@zzmlinux mnt]# cd ppp/
[root@zzmlinux ppp]# ls -dil *
323 -rwxrwxrwx  1 root root 5 Jul 15  2014 ooo
[root@zzmlinux ppp]# echo &quot;test something&quot; &gt; ooo
[root@zzmlinux ppp]# cat ooo
test something
</pre>
</p>
<p>从上面的命令输出结果可以看出：</p>
<p>/nfs/share2 文件夹本身在服务器端的 inode 值原本为 64 位的 4294967616，但在 32 位的 NFS 客户端这被转换成了 321（其实：4294967616 = 0xFFFFFFFF + 321）。相应的 /nfs/share2/ppp 文件夹的 inode 值由 8589934912 转换为 322 了（8589934912 = 2 * 0xFFFFFFFF + 322），/nfs/share2/ppp/ooo 文件的 inode 值由 8589934913 转换为 323 了（8589934913 = 2 * 0xFFFFFFFF + 323）。</p>
<p>这样 NFS 客户端算是也能正常使用了，但不能保证文件数量太多时不会出现问题的。</p>
<p><strong>2、RHEL 6 64位系统（内核版本：2.6.32）：</strong></p>
<p><pre class="brush: bash; highlight: [3,5,7]; title: ; notranslate">
[root@localhost work]# mount -t nfs 192.168.1.145:/nfs/share2 /mnt/nfs
[root@localhost work]# ls -dil /mnt/nfs
4294967616 drwxrwxrwx 3 root root 24  7月 15 2014 /mnt/nfs
[root@localhost work]# ls -dil /mnt/nfs/ppp
8589934912 drwxrwxrwx 2 root root 24  7月 15 2014 /mnt/nfs/ppp
[root@localhost work]# ls -dil /mnt/nfs/ppp/ooo
8589934913 -rwxrwxrwx 1 root root 15  7月 15 2014 /mnt/nfs/ppp/ooo
[root@localhost work]# cat /mnt/nfs/ppp/ooo
test something
</pre>
</p>
<p>RHEL6 64 位系统上的效果就比较明显了，所有文件显示的 inode 值与服务器端的完全一致，这样使用起来就完全没有问题了。</p>
<p><strong>3、Fedora 14 32位系统（内核版本：2.6.35）：</strong></p>
<p><pre class="brush: bash; highlight: [2,5,7,9]; title: ; notranslate">
[root@fedora14 ~]# cat /sys/module/nfs/parameters/enable_ino64
Y
[root@fedora14 ~]# mount -t nfs 192.168.1.145:/nfs/share2 /mnt
[root@fedora14 ~]# ls -dil /mnt
4294967616 drwxrwxrwx 3 root root 24 Jul 15 16:09 /mnt
[root@fedora14 ~]# ls -dil /mnt/ppp
8589934912 drwxrwxrwx 2 root root 24 Jul 15 16:09 /mnt/ppp
[root@fedora14 ~]# ls -dil /mnt/ppp/ooo
8589934913 -rwxrwxrwx 1 root root 15 Jul 15 16:15 /mnt/ppp/ooo
</pre>
</p>
<p>这里由于 Fedora 14 是 32 位系统，我们在挂载 NFS 之前特地查看了 enable_ino64 参数，发现默认已启用的。这样在挂载之后查看到的文件的 inode 值也是和服务器一样为 64 位的了。如果有兴趣将 enable_ino64 参数改为 N 的话，你会发现这些 inode 也被转换为 32 位的了。</p>
<p>其实我碰到的主要就是 Linux 下老的程序和新的 64 位 inode 的兼容问题，SGI 推荐用户使用 64 位的 inode，btrfs 和 ext4 现在也开始准备改为 64 位 inode 了，当然不免还有一些老的程序存在兼容性问题，这里有 SGI 的研发人员实际测试 64 位 inode 的兼容结果：</p>
<p><a href="http://blog.fmeh.org/2013/05/11/does-the-world-need-32-bit-inodes/" target="_blank">http://blog.fmeh.org/2013/05/11/does-the-world-need-32-bit-inodes/</a></p>
<p>上面看到的只使用 32 位 inode 的程序占比结果 10% 还是比较令人满意的，只支持 32 位 inode 的程序现在越来越少了。</p>
<p>本文为个人测试及分析结果，如其中存在错误还请提出指正哦，玩的开心~~~ ^_^</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/nfs-64bitinode/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>NFS读写块大小问题分析</title>
		<link>https://zohead.com/archives/nfs-rwsize/</link>
		<comments>https://zohead.com/archives/nfs-rwsize/#comments</comments>
		<pubDate>Sat, 05 Jul 2014 14:44:32 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[存储]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[NFS]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[UDP]]></category>
		<category><![CDATA[块大小]]></category>

		<guid isPermaLink="false">http://zohead.com/?p=723</guid>
		<description><![CDATA[本文同步自（最佳显示效果请点击）：https://zohead.com/archives/nfs-rwsize/ Linux NFS 客户端在挂载服务器的 NFS 共享时可以使用 rsize 和 wsize 参数指定 NFS 读写的块大小，但实际使用时发现并不完全凑效，下面简单分析一下。 我先在一台 RHEL6 客户端上挂载另一台 RHEL6 服务器上的 NFS 共享： 从上面可以看到不指定 rsize 和 wsize 参数时，默认的读写块大小都是 256KB（rsize=262144），而且使用的是 TCP 协议（proto=tcp）。 下面使用 UDP 协议挂载 NFS 共享： 从结果可以 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>本文同步自（最佳显示效果请点击）：<a href="https://zohead.com/archives/nfs-rwsize/" target="_blank">https://zohead.com/archives/nfs-rwsize/</a></p>
<p>Linux NFS 客户端在挂载服务器的 NFS 共享时可以使用 rsize 和 wsize 参数指定 NFS 读写的块大小，但实际使用时发现并不完全凑效，下面简单分析一下。</p>
<p>我先在一台 RHEL6 客户端上挂载另一台 RHEL6 服务器上的 NFS 共享：</p>
<p><pre class="brush: bash; title: ; notranslate">
[root@localhost ~]# mount -t nfs 192.168.1.122:/nfs/share /mnt/nfs
[root@localhost ~]# grep /mnt/nfs /proc/mounts
192.168.1.122:/nfs/share /mnt/nfs nfs rw,relatime,vers=3,rsize=262144,wsize=262144,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=192.168.1.122,mountvers=3,mountport=892,mountproto=udp,local_lock=none,addr=192.168.1.122 0 0
</pre>
</p>
<p>从上面可以看到不指定 rsize 和 wsize 参数时，默认的读写块大小都是 256KB（rsize=262144），而且使用的是 TCP 协议（proto=tcp）。</p>
<p>下面使用 UDP 协议挂载 NFS 共享：</p>
<p><pre class="brush: bash; title: ; notranslate">
[root@localhost ~]# mount -t nfs -o udp 192.168.1.122:/nfs/share /mnt/nfs
[root@localhost ~]# grep /mnt/nfs /proc/mounts
192.168.1.122:/nfs/share /mnt/nfs nfs rw,relatime,vers=3,rsize=32768,wsize=32768,namlen=255,hard,proto=udp,timeo=11,retrans=3,sec=sys,mountaddr=192.168.1.122,mountvers=3,mountport=892,mountproto=udp,local_lock=none,addr=192.168.1.122 0 0
</pre>
</p>
<p>从结果可以看出，使用 UDP 协议时块大小就只有 32KB 了。</p>
<p>准备在客户端这边修改 mount 参数将 NFS TCP 方式的读写块大小增加到 1MB：</p>
<p><pre class="brush: bash; title: ; notranslate">
[root@localhost ~]# mount -t nfs -o rsize=1048576,wsize=1048576 192.168.1.122:/nfs/share /mnt/nfs
[root@localhost ~]# grep /mnt/nfs /proc/mounts
192.168.1.122:/nfs/share /mnt/nfs nfs rw,relatime,vers=3,rsize=262144,wsize=262144,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=192.168.1.122,mountvers=3,mountport=892,mountproto=udp,local_lock=none,addr=192.168.1.122 0 0
</pre>
</p>
<p>但从上面的结果来看，实际使用的块大小还是 256KB。</p>
<p>在客户端这边修改 mount 参数将 NFS UDP 方式的读写块大小增加到 256KB：</p>
<p><pre class="brush: bash; title: ; notranslate">
[root@localhost ~]# mount -t nfs -o udp,rsize=262144,wsize=262144 192.168.1.122:/nfs/share /mnt/nfs
[root@localhost ~]# grep /mnt/nfs /proc/mounts
192.168.1.122:/nfs/share /mnt/nfs nfs rw,relatime,vers=3,rsize=32768,wsize=32768,namlen=255,hard,proto=udp,timeo=11,retrans=3,sec=sys,mountaddr=192.168.1.122,mountvers=3,mountport=892,mountproto=udp,local_lock=none,addr=192.168.1.122 0 0
</pre>
</p>
<p>UDP 模式下读写的块大小也无法修改，客户端似乎已经使用了最大的读写块大小。</p>
<p>没办法，下面来看看 Linux kernel 源代码，找出真正的原因，先在 include/linux/nfs_xdr.h 文件中找到了 NFS I/O 块大小的定义：</p>
<p><pre class="brush: cpp; title: include/linux/nfs_xdr.h; notranslate">
/*
 * To change the maximum rsize and wsize supported by the NFS client, adjust
 * NFS_MAX_FILE_IO_SIZE.  64KB is a typical maximum, but some servers can
 * support a megabyte or more.  The default is left at 4096 bytes, which is
 * reasonable for NFS over UDP.
 */
#define NFS_MAX_FILE_IO_SIZE    (1048576U)
#define NFS_DEF_FILE_IO_SIZE    (4096U)
#define NFS_MIN_FILE_IO_SIZE    (1024U)
</pre>
</p>
<p>这里可以看到 NFS 默认使用 4KB 块大小，客户端实际挂载时会做调整，最小 1KB，最大 1MB。</p>
<p>NFS 客户端在挂载时会与 NFS 服务器协商适合的读写块大小值，我们来看看 fs/nfs/client.c 文件中协商设置 NFS 文件系统信息的代码：</p>
<p><pre class="brush: cpp; highlight: [0,11,12,13,14]; title: fs/nfs/client.c; notranslate">
static void nfs_server_set_fsinfo(struct nfs_server *server, struct nfs_fsinfo *fsinfo)
{
	unsigned long max_rpc_payload;

	/* Work out a lot of parameters */
	if (server-&gt;rsize == 0)
		server-&gt;rsize = nfs_block_size(fsinfo-&gt;rtpref, NULL);
	if (server-&gt;wsize == 0)
		server-&gt;wsize = nfs_block_size(fsinfo-&gt;wtpref, NULL);

	if (fsinfo-&gt;rtmax &gt;= 512 &amp;&amp; server-&gt;rsize &gt; fsinfo-&gt;rtmax)
		server-&gt;rsize = nfs_block_size(fsinfo-&gt;rtmax, NULL);
	if (fsinfo-&gt;wtmax &gt;= 512 &amp;&amp; server-&gt;wsize &gt; fsinfo-&gt;wtmax)
		server-&gt;wsize = nfs_block_size(fsinfo-&gt;wtmax, NULL);

	max_rpc_payload = nfs_block_size(rpc_max_payload(server-&gt;client), NULL);
	if (server-&gt;rsize &gt; max_rpc_payload)
		server-&gt;rsize = max_rpc_payload;
	if (server-&gt;rsize &gt; NFS_MAX_FILE_IO_SIZE)
		server-&gt;rsize = NFS_MAX_FILE_IO_SIZE;
	server-&gt;rpages = (server-&gt;rsize + PAGE_CACHE_SIZE - 1) &gt;&gt; PAGE_CACHE_SHIFT;

	server-&gt;backing_dev_info.name = &quot;nfs&quot;;
	server-&gt;backing_dev_info.ra_pages = server-&gt;rpages * NFS_MAX_READAHEAD;

	if (server-&gt;wsize &gt; max_rpc_payload)
		server-&gt;wsize = max_rpc_payload;
	if (server-&gt;wsize &gt; NFS_MAX_FILE_IO_SIZE)
		server-&gt;wsize = NFS_MAX_FILE_IO_SIZE;
	/*......*/
}
</pre>
</p>
<p>从上面 nfs_server_set_fsinfo 函数的代码可以看到 NFS 客户端实际参考了服务器返回的 rtmax 和 wtmax 值，而这个值可以在挂载 NFS 文件系统时用抓包工具看到（NFS 使用的 RPC 协议）。</p>
<p>下面的图片中显示的就是 NFS 客户端中指定 rsize 和 wsize 参数为 1MB 时 Wireshark 上抓到的 NFS FSINFO 请求的实际数据；</p>
<div style="width: 513px" class="wp-caption alignnone"><a href="http://zohead.com/wp-content/uploads/nfs-mount-cap.jpg" target="_blank"><img alt="挂载NFS的网络抓包" src="http://zohead.com/wp-content/uploads/nfs-mount-cap.jpg" width="503" height="231" /></a><p class="wp-caption-text">挂载NFS的网络抓包</p></div>
<p>上面图片里小椭圆圈表示的是 NFS FSINFO 请求，大椭圆圈里就是服务器传过来的 rtmax 和 wtmax 值了，我们可以看到值就是 256KB。这样也就能解释了为什么客户端增大 NFS 读写块大小也不起作用了。</p>
<p>我们后台登陆到 NFS 服务器上，可以从 /proc/fs/nfsd/max_block_size 文件中看到当前 NFS 服务器的最大块大小，然后尝试修改它：</p>
<p><pre class="brush: bash; title: ; notranslate">
~ # cat /proc/fs/nfsd/max_block_size
262144
~ # echo 524288 &gt; /proc/fs/nfsd/max_block_size
~ # cat /proc/fs/nfsd/max_block_size
262144
</pre>
</p>
<p>可以看到当前 NFS 服务器的最大读写块大小确实是 256KB，但是我们想修改它的值的时候，却似乎又修改不了。这样只能再看看修改 max_block_size 的 kernel 源代码了，对应的代码在 nfsd/nfsctl.c 文件中：</p>
<p><pre class="brush: cpp; highlight: [14,15,18]; title: nfsd/nfsctl.c; notranslate">
static ssize_t write_maxblksize(struct file *file, char *buf, size_t size)
{
	char *mesg = buf;
	if (size &gt; 0) {
		int bsize;
		int rv = get_int(&amp;mesg, &amp;bsize);
		if (rv)
			return rv;
		/* force bsize into allowed range and
		 * required alignment.
		 */
		if (bsize &lt; 1024)
			bsize = 1024;
		if (bsize &gt; NFSSVC_MAXBLKSIZE)
			bsize = NFSSVC_MAXBLKSIZE;
		bsize &amp;= ~(1024-1);
		mutex_lock(&amp;nfsd_mutex);
		if (nfsd_serv &amp;&amp; nfsd_serv-&gt;sv_nrthreads) {
			mutex_unlock(&amp;nfsd_mutex);
			return -EBUSY;
		}
		nfsd_max_blksize = bsize;
		mutex_unlock(&amp;nfsd_mutex);
	}

	return scnprintf(buf, SIMPLE_TRANSACTION_LIMIT, &quot;%d\n&quot;,
							nfsd_max_blksize);
}
</pre>
</p>
<p>write_maxblksize 函数中判断了传入的参数，如果写入的值超过 NFSSVC_MAXBLKSIZE 值则固定为 NFSSVC_MAXBLKSIZE 值，那我们来看看 NFSSVC_MAXBLKSIZE 的定义：</p>
<p><pre class="brush: cpp; title: linux/nfsd/const.h; notranslate">
/*
 * Maximum blocksizes supported by daemon under various circumstances.
 */
#define NFSSVC_MAXBLKSIZE   RPCSVC_MAXPAYLOAD
/* NFSv2 is limited by the protocol specification, see RFC 1094 */
#define NFSSVC_MAXBLKSIZE_V2    (8*1024)
</pre>
</p>
<p>linux/nfsd/const.h 中 NFSSVC_MAXBLKSIZE 定义为了 RPCSVC_MAXPAYLOAD 的值，那看看 linux/sunrpc/svc.h 中 RPCSVC_MAXPAYLOAD 的实际值：</p>
<p><pre class="brush: cpp; title: linux/sunrpc/svc.h; notranslate">
/*
 * Maximum payload size supported by a kernel RPC server.
 * This is use to determine the max number of pages nfsd is
 * willing to return in a single READ operation.
 *
 * These happen to all be powers of 2, which is not strictly
 * necessary but helps enforce the real limitation, which is
 * that they should be multiples of PAGE_CACHE_SIZE.
 *
 * For UDP transports, a block plus NFS,RPC, and UDP headers
 * has to fit into the IP datagram limit of 64K.  The largest
 * feasible number for all known page sizes is probably 48K,
 * but we choose 32K here.  This is the same as the historical
 * Linux limit; someone who cares more about NFS/UDP performance
 * can test a larger number.
 *
 * For TCP transports we have more freedom.  A size of 1MB is
 * chosen to match the client limit.  Other OSes are known to
 * have larger limits, but those numbers are probably beyond
 * the point of diminishing returns.
 */
#define RPCSVC_MAXPAYLOAD   (1*1024*1024u)
#define RPCSVC_MAXPAYLOAD_TCP   RPCSVC_MAXPAYLOAD
#define RPCSVC_MAXPAYLOAD_UDP   (32*1024u)
</pre>
</p>
<p>从 linux/sunrpc/svc.h 中可以看到 NFS 读写块大小必须为 2 的幂，这样也大概知道读写块大小限制的原因了：</p>
<p>对于 UDP 来说，由于一个 UDP 包最大才 64KB，因此使用 UDP 协议的 NFS 读写块大小最大不超过 48KB，而 kernel 中则直接限制为 32KB 了；而使用 TCP 协议的 NFS 由于没有这个限制允许更大的读写块大小，但 Linux kernel 还是将其限制为 1MB 了。</p>
<p>至于 max_block_size 值不能直接修改的现象也找到原因了，在 nfsd/nfsctl.c 文件中高亮显示的第 18 行代码里判断了 NFS 服务器是否在启动运行，如果在运行则不允许修改。</p>
<p>下面就好办了，先卸载 NFS 共享的挂载，停止服务器的 NFS 服务，修改 max_block_size 值，然后重新启动 NFS 服务，</p>
<p><pre class="brush: bash; title: ; notranslate">
~ # service nfs stop
Shutting down NFS mountd:                                  [  OK  ]
Shutting down NFS daemon:                                  [  OK  ]
Shutting down NFS services:                                [  OK  ]
~ # echo 1048576 &gt; /proc/fs/nfsd/max_block_size
~ # cat /proc/fs/nfsd/max_block_size
1048576
~ # service nfs start
</pre>
</p>
<p>可以看到现在 NFS 的最大块大小可以修改了，接着在客户端中指定读写块大小并重新挂载 NFS 共享，这个时候客户端也能正确使用更大的块大小了：</p>
<p><pre class="brush: bash; title: ; notranslate">
[root@localhost fs]# mount -t nfs -o rsize=1048576,wsize=1048576 192.168.1.122:/nfs/share /mnt/nfs
[root@localhost fs]# grep /mnt/nfs /proc/mounts
192.168.1.122:/nfs/share /mnt/nfs nfs rw,relatime,vers=3,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=192.168.1.122,mountvers=3,mountport=892,mountproto=udp,local_lock=none,addr=192.168.1.122 0 0
</pre>
</p>
<p>如果要深究一下 NFS 服务器初始的最大块大小只有 256KB 的原因，可以看看 kernel 中 nfsd/nfssvc.c 文件中的代码：</p>
<p><pre class="brush: cpp; highlight: [13,20,22,23,24]; title: nfsd/nfssvc.c; notranslate">
int nfsd_create_serv(void)
{
	int err = 0;

	WARN_ON(!mutex_is_locked(&amp;nfsd_mutex));
	if (nfsd_serv) {
		svc_get(nfsd_serv);
		return 0;
	}
	if (nfsd_max_blksize == 0) {
		/* choose a suitable default */
		struct sysinfo i;
		si_meminfo(&amp;i);
		/* Aim for 1/4096 of memory per thread
		 * This gives 1MB on 4Gig machines
		 * But only uses 32K on 128M machines.
		 * Bottom out at 8K on 32M and smaller.
		 * Of course, this is only a default.
		 */
		nfsd_max_blksize = NFSSVC_MAXBLKSIZE;
		i.totalram &lt;&lt;= PAGE_SHIFT - 12;
		while (nfsd_max_blksize &gt; i.totalram &amp;&amp;
		       nfsd_max_blksize &gt;= 8*1024*2)
			nfsd_max_blksize /= 2;
	}

	nfsd_serv = svc_create_pooled(&amp;nfsd_program, nfsd_max_blksize,
				      nfsd_last_thread, nfsd, THIS_MODULE);
	if (nfsd_serv == NULL)
		err = -ENOMEM;
	else
		set_max_drc();

	do_gettimeofday(&amp;nfssvc_boot);		/* record boot time */
	return err;
}
</pre>
</p>
<p>从上面的可以看到，NFS 服务器在决定默认的最大读写块大小时考虑到内存占用情况，每个 NFS 内核线程最多只使用 1/4096 的物理内存大小，对于物理内存超过 4GB 的机器才使用最大的 1MB 读写块大小。</p>
<p>来看看我们使用的 NFS 服务器的内存情况，可以看到服务器只使用了 2GB 的内存：</p>
<p><pre class="brush: bash; title: ; notranslate">
~ # free
             total       used       free     shared    buffers     cached
Mem:       2040272     335476    1704796          0       2360      70076
-/+ buffers/cache:     263040    1777232
Swap:            0          0          0
</pre>
</p>
<p>按照 nfsd/nfssvc.c 文件中的代码，i.totalram 实际值为所有可用的物理内存的页数量，我们这里就是 2040272 / 4KB（默认的 PAGE_SIZE 页大小），按照高亮的第 22 - 24 行代码计算出来的默认最大块大小值就是 262144 了。</p>
<p>本文为个人经过测试及分析 Linux kernel 源代码得出的分析结果，如果其中存在错误还请提出指正哦~~~ ^_^</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/nfs-rwsize/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
	</channel>
</rss>
