Linux 有个叫 Network Block Device(NBD)的功能,也就是把一个远程位置挂载成块设备。NBD 需要两部分,服务端用来提供存储空间,客户端把服务端提供的存储空间抽象成块设备,两端之间通过网络通信。因为是块设备,所以可以当普通存储盘用,从任意位置读写数据。这样它就能拿来做一些很酷的事情,比如在上面开个 swap。

NBD 的层次比 NFS 低一级,NFS 服务器是把文件系统,客户端不用考虑服务器底层用的是什么文件系统;NBD 只负责提供空间,要是想存文件,要在 NBD 设备上创建文件系统,客户端的内核要挂载它。

这样想的话 NBD 和 NFS 的使用场景差别还蛮大的,至少不会有人拿 NBD 来共享文件吧。

安装

NBD 客户端的主要部分是在内核里的。发行版如果提供了 NBD 客户端的模块,直接 modprobe nbd 就好了。我这里默认会创建 16 个块设备,在 GNOME Disks 里看着十分喜感,觉得太疯牛病了的话设定 nbds_max=1 就好了。合起来是 modprobe nbd nbds_max=1

Milk-V Duo Buildroot SDK 内核默认配置是不开 NBD 的。打开也很简单,menuconfig 里找到 Device Drivers -> Block Devices -> Network block device,同时也启用 NBD 客户端的用户空间的软件包,编译即可。

服务端就很随意了,只要能提供存储、监听网络端口就行,不需要特权,更不需要依赖内核功能,所以在手机上也能随便开,比用 usb_gadget 提供存储对手机的要求低多了。常见的服务端有 nbd-servernbdkit 之类的,直接用包管理器装。

使用

只要设置好服务端和客户端就行了。

比如用 nbd-server 开一个 NBD 服务端,指定一个块设备(比如/dev/sdX,/dev/mmcblk0p0这样的物理设备,或者/dev/zram0。你要是想的话还可以是另一个 NBD 设备)或普通文件(换个说法就是 raw 格式的虚拟盘):

nbd-server -d [端口] [文件]

也可以用 nbdkit,就不再局限于文件了:

nbdkit -f -p [端口] <模块> <参数>

比如 nbdkit -f -p 9909 memory 4G,会开一个把数据放内存(堆,Heap)上的服务端

设置好服务端之后用客户端连接即可,仍然用 nbd-server 或者 nbdkit。这里直接调用的客户端是用户空间里的工具,实际用处只是和内核打交道罢了,所以用哪个都一样,也不需要和服务端的软件一致。

sudo nbd-client [主机] [端口] /dev/nbdX

或者

sudo nbd-client -d /dev/nbdX

设置完成之后它就是一个与远程主机相连的块设备,物理存储设备的基本功能它都有。

服务端由于某种原因和客户端断开的情况下会怎么样?答案是内核会报 I/O error,和硬盘突然坏掉的感觉差不多。用的时候记得找个连接质量好的环境。

滥用

Milk-V Duo 原版只有 64M 内存,用起来十分不爽,不如在 NBD 上开个 swap。摆脱了内存不足的烦恼,就可以拿它干一些更酷的事,比如跑个 Minecraft 服务器:

(看下 tmux 右下角的日期,是 2023 年 11 月。我是真能咕啊……)

这里用的是 Cuberite,是一个 C++ 写的 Minecraft 服务器,与原版服务器无关,性能比原版要好很多。我还是没能在它上面跑起原版服务器……

Cuberite 是在 RevyOS 上编译的,我在 openSUSE Tumbleweed 上编译的产物不知怎么就是会 Segfault。

话说从这里可以看到 Cuberite 在这个设置下内存占用没有超过 256M,所以倘若你恰好有块 256M 版本的 Milk-V Duo,大概可以在上面试下直接运行 Cuberite?性能应该会好得多。

用 NBD 开 swap,无论从哪个层面分析,性能都会比直接访问内存差得多(htop 里显示的很高的 IO wait 比例也证实了这一点)。访问 swap 上的数据时,要先把那部分数据传输到内存里,之后很可能又会把一部分暂时不用的内存放到 swap 上(因为这里总内存太小了),整个过程都是软件控制的,同时网络的性能通常也没内存与 CPU 通信的那条总线好,所以用 NBD 开 swap 无论访问延迟还是速率都远远不如真实内存。不过至少性能比开在 SD 卡上好多了。

这里我没把 swap 放 SD 卡上,也是因为 SD 卡受不了这个写入量。正常情况下把 swap 开在硬盘上,是为了让内存里一些基本不访问的数据(比如程序开起来之后就往内存里加载了什么数据,但是从始至终没有用过)放到外部存储上,让内存干它该干的事(存放需要大量快速随机读写的内容)。我们的情况是内存和 swap 里都是需要快速大量随机读写的内容,只是内存真的放不下罢了。