网络块设备

作者:P. T. Breuer

1997 年 4 月,Pavel Machek 为他的网络块设备 (NBD) 编写了代码,当时他工作的载体是当时的 2.1.55 Linux 内核。Pavel 维护并改进了代码,并发布了四个后续版本,分别匹配内核 57、101、111 和 132。Andrzej M. Krzysztofowicz 贡献了 64 位修复,Stephen Tweedie 后来贡献了他的大量专业知识,尤其是在提供基于信号量的锁定方案以使代码实现 SMP 安全方面。我们为了在工业环境中使用而增强了它,在此我们描述该设备、驱动程序及其一些开发历史。

网络块设备驱动程序提供了一种访问模型,这种模型在以网络为导向的世界中将变得越来越普遍。它在本地客户端上模拟块设备,例如硬盘或硬盘分区,但通过网络连接到远程服务器,该服务器提供真正的物理后备。在本地,该设备看起来像一个磁盘分区,但它是远程设备的伪装<\#231>。远程服务器是一段轻量级的守护程序代码,提供对远程设备的真正访问,并且不需要在 Linux 下运行。本地操作系统将是 Linux,并且必须支持 Linux 内核 NBD 驱动程序和本地客户端守护程序。我们正在使用 NBD 设置来提供实时的异地存储和备份,但它也可以用于在世界任何地方虚拟地传输物理设备。

The Network Block Device

图 1. NBD 将远程资源作为本地资源呈现给客户端

NBD 具有 UNIX 系统组件的一些经典特征:它简单、紧凑且通用。文件系统可以挂载在 NBD 上(图 1)。NBD 可以用作软件 RAID 阵列中的组件(图 2)等等。与从同一远程机器挂载 NFS(网络文件系统)相比,通过 NBD 挂载原生 Linux EXT2 文件系统可提供更快的传输速率(有关使用 Pavel 驱动程序的近乎原始版本的计时,请参见表 1)。

The Network Block Device

图 2. 通过软件 RAID 和 NBD 实现远程镜像

表 1

通过 NBD 挂载的远程 Linux EXT2 文件系统在默认条件下的测试中表现得像具有约 1.5KB 缓冲区大小的 NFS。也就是说,它的明显缓冲区大小是默认的以太网传输单元大小 (MTU/MRU) 1.5KB,恰好是 NFS 默认缓冲区大小 1KB 的 1.5 倍。NBD 通过使用 TCP 而不是 UDP 作为传输协议来获得弹性。TCP 包含其自身的内在一致性和恢复机制。它是一种比 UDP 更重的协议,但对于 NBD 而言,TCP 中的开销被它节省的重传和纠错代码量所抵消。

NBD 可以用作中型邮件服务器 A 的实时假脱机镜像。故障转移到另一个房间的备份服务器 B,通过 100BT 网络连接。NBD 设备将主服务器连接到备份服务器,并在主服务器上提供 RAID-1 镜像的一半 (Y)。镜像的另一半是主服务器自己的邮件分区 X。复合设备 XY 被挂载为主服务器的邮件假脱机。

当 A 发生故障时,B 上的守护程序会检测到中断,断开 NBD 链接,检查其假脱机映像 Y,纠正小的缺陷,将其本地挂载为其邮件假脱机,并开始为 A 的广播邮件交换器 IP 地址设置别名,从而接管邮件服务。当 A 恢复时,它会被检测到,并且别名会被删除。然后 A 和 B 重新建立 NBD 链接,并且 A 上的邮件分区 X 与 NBD 映像 Y 重新同步。然后主服务器的 RAID-1 设备被重新启动,NBD 映像在 RAID 下被从属于它,并且邮件服务在正常配置中恢复。可以使设置对称,但这太令人困惑,无法在此处描述。

与常见的替代方案相比,这种方法具有优势。例如,一种方法是在空闲状态下维护备用邮件服务器,并在主服务器发生故障时启动它。这使得任何已假脱机的邮件在中断期间都不可用,但重新集成很容易。只需在修复主服务器后连接相应的文件即可。主服务器上的一些文件可能会丢失。

另一种方法是将邮件服务器分散在多台机器上,所有机器都通过 NFS 挂载相同的假脱机区域。但是,过去 NFS 上的文件锁定从未被证明是完全可靠的,并且 NFS 仍然无法令人满意地支持由多个客户端选择同时下载 50MB 邮件文件夹引起的活动突发。传输速率似乎随着文件夹大小呈指数级下降,并且在“软”模式下,传输可能会在不利的网络条件下崩溃。如果在同步(“硬”)模式下挂载 NFS 以提高 NFS 服务器在线时的可靠性,则 NFS 服务器的故障会导致客户端停止运行。NFS 服务器必须单独镜像。

表 2

第三种替代方案是维护一个每小时或每天更新的镜像。但是,这种方法会在中断期间及时回溯邮件假脱机,暂时丢失收到的邮件,并使重新集成变得困难。NBD 方法避免了这些问题,但存在从源文件系统将损坏导入镜像的风险,当源文件系统崩溃时。这是因为 NBD 操作是在块级别而不是文件系统级别进行日志记录的,因此完整的 NBD 操作可能仅表示部分完成的文件操作。如果源文件系统实际上崩溃,则损坏和随后的修复不会比源文件系统更糟;如果只是丢失了连接,则源系统在重新集成时可能比镜像状态更好。为了避免这个问题,必须更改 Linux 虚拟文件系统 (VFS) 层以标记由同一文件操作产生的块请求,并且 NBD 必须将这些请求作为一个单元进行日志记录。这意味着 VFS 层需要进行更改,我们尚未尝试实现这些更改。

鉴于 NFS 在 Linux 中的略微曲折的历史,可以认为 NBD 驱动程序不需要 NFS 代码即可提供网络文件系统,这在某种程度上是一种优势。(2.2。x 内核中 NFS 的速度明显优于 2.0。x 实现,可能快两倍,并且似乎不再遭受相对于传输大小的非线性减速。)该驱动程序根本没有文件系统代码。NBD 可以挂载为原生 EXT2、FAT、XFS 或远程资源碰巧格式化的任何格式。它受益于 Linux 块设备层中存在的缓冲所带来的性能提升。如果服务器从异步文件系统而不是另一端的原始物理设备提供服务,则缓冲带来的好处在连接的两端都会累积。在流式测量中(它读取相同的源两次),缓冲使读取速度提高了 100 倍,具体取决于预读和 CPU 负载等条件,并且写入速度似乎大约提高了 2 倍。使用我们的实验性驱动程序,我们看到“正常使用”中的原始写入速度通过 3c905 网卡在交换双工 100BT 网络上达到约 5MBps 到 6MBps。(引用的 5-6MBps 是在两端缓冲且传输约 16MB 或已安装 RAM 的 25% 的情况下实现的,因此缓冲是有效的但不是主要的。)

1998 年,作者之一 (Breuer) 将当时的 2.1.132 NBD 向后移植到稳定的 2.0 系列内核,首先采用 2.1.55 代码并恢复内核接口,然后并行 Pavel 后续版本中的增量更改,直到 2.1.132。该代码可从 Pavel 的 NBD 网页 (atrey.karlin.mff.cuni.cz/~pavel/nbd/nbd.html) 以及 Pavel 的增量更改中获得。最初的向后移植删除了新的内核 dcache 间接寻址,并将网络调用改回旧样式。最终的更改与 Pavel 源代码中后期的 64 位适配并行。

与原始代码一样,向后移植的代码包括一个内核模块以及对内核块驱动程序接口的小幅修改,以及用户空间服务器和客户端守护程序。事实证明,Pavel 是一位非常有帮助的通信员。

该驱动程序在我们在部门中使用的开发 Linux 机器上运行良好,在那里我们为多年维护了一个经过大量修改的 2.0.25 内核代码库。特别是随着 2.0.36 内核版本的感知稳健性,我们向前移植了驱动程序。令人惊讶的是,它完全失败了。它在仅传输约 0.5MB 后就锁定了任何客户端。

直到今天,我们仍不清楚该问题的确切性质。Stephen Tweedie 检查了移植驱动程序中的算法,并得出结论,它与 2.1.132 算法相同,后者可以工作,并且 Pavel 批准了协议。逐个补丁地回溯 2.0 系列内核未能找到驱动程序停止工作的任何点。它在任何标准版本中都无法工作。我们最好的猜测是,我们代码库中的非标准调度程序更改是导致初始端口在我们的开发环境中平稳运行的原因,并且 2.1 系列中的一般更改以某种方式消除了那里的问题。

实验表明,未完成的块请求的内核队列增长到大约 17 或 18 个长度,然后损坏了重要的指针。我们尝试了三到四种旨在减小队列大小的方法,并且从经验上看,似乎每种方法都可以控制问题。第一种方法 (Garcia) 将网络移出内核到用户空间守护程序,在那里,大概标准的调度策略用于消除未知的竞争条件。第二种方法 (Breuer) 将网络保留在内核中,但串行化网络传输,以便每次添加到挂起队列后立即删除,从而使队列大小始终等于零或一。第三种方法 (Garcia) 完全删除了队列机制,使其长度保持为零。第四种方法 (Breuer) 使客户端守护程序线程在每次传输后返回到用户空间,以便重新调度自身。

The Network Block Device

图 3。

NFS 提供星形配置,服务器位于中心(左)。NBD 在内部或外部 RAID 的帮助下,提供反转拓扑,具有多个服务器和一个客户端。

在相当长一段时间内,以串行模式运行似乎是最佳解决方案,尽管用户空间网络解决方案也很可靠。通过我们最近的开发工作,我们对这里工作的内核机制有了更深入的了解,尽管对原始不稳定性的明确解释尚不可用,但我们确实拥有完全稳定的驱动程序,这些驱动程序以完全异步的内核网络模式以及相同协议的用户空间版本工作。一个建议是 gcc 编译器可能错误生成代码(a>b=0; c>d=a; if(c>d>b) printk("bug"); 锁定之前生成的输出!),但可能是堆栈损坏导致从其他地方错误跳转到线性代码序列中。驱动程序代码的重新进入性增加和更宽松的网络传输协议肯定增加了稳定性,同时可以检测到问题,这对于竞争条件来说是一个很好的共同指标。

建立连接

建立基于 NBD 的文件系统的步骤

创建挂载在网络远程设备上的文件系统所需的五个步骤在图 4 中概述。例如,以下命令序列在服务器上的本地文件系统上创建一个大约 160MB 的文件,然后启动服务器以从端口 1077 提供服务。步骤 1 和 2 是

dd if=/dev/zero of=/mnt/remote bs=1024 count 16000
nbd-server 1077 /mnt/remote

在客户端,必须将驱动程序模块加载到内核中,并启动客户端守护程序。客户端守护程序需要服务器机器地址 (192.168.1.2)、端口号和将成为 NBD 的特殊设备文件的名称。在原始驱动程序中,这称为 /dev/nd0。步骤 3 是

insmod nbd.o
nbd=client 192.168.1.2 1077 /dev/nd0
然后可以在 NBD 上创建文件系统,并使用步骤 4 和 5 在本地挂载系统
mke2fs /dev/nd0
mount -text2 /dev/nd0 /mnt
在我们当前的驱动程序中,允许多个端口和地址,从而导致启动冗余连接。在这里,服务器提供多个端口而不是一个
dd if=/dev/zero of=/mnt/remote bs=1024 count=16000
nbd-server 1077 1078 1079 1080 /mnt/remote
当前客户端可以使用所有这些端口连接到服务器,在这里我们将其中两个端口定向到服务器上的第二个 IP (192.168.2.2),以便我们可以通过两台机器上的第二张网卡进行路由,从而使我们交换网络上的可用带宽加倍。
insmod nbd.o
nbd-client 192.168.1.2 1077 1078 192.168.2.2 1079 1080 /dev/nda
在当前驱动程序中,NBD 将自身呈现为分区块设备 nda,尽管“分区”不是以标准方式使用的。它们的设备文件 nda1、nda2 等被从属客户端守护程序用作内核通信通道。它们在设备中提供冗余和增加的带宽。整个设备文件 nda 是唯一接受标准块设备操作的文件。
驱动程序和协议概述

在插入内核模块时,驱动程序会向内核注册。当客户端守护程序首次连接到其服务器对应程序时,原始驱动程序会将套接字的文件描述符传递给内核。内核将描述符追溯到内部内核套接字结构,并在其自己的内部结构中注册内存地址以供后续使用。我们当前的驱动程序将网络保留在用户空间中,并且不注册套接字。

然后客户端守护程序和服务器守护程序执行握手例程。不需要其他设置,但握手可能会在当前一代驱动程序中建立 SSL 通道,这需要预先设置 SSL 证书和请求。

Pavel 的原始驱动程序代码包含内核中的两个主要线程。“客户端”线程属于客户端守护程序。守护程序的工作是启动与远程机器上的服务器守护程序的网络连接,并通过 ioctl 调用将其打开的套接字传递给内核。然后客户端守护程序在 ioctl 调用中用户端阻塞,而它的执行线程在内核中永远持续下去。它在内核中连续循环,通过网络套接字传输数据。终止守护程序也需要终止套接字,否则客户端守护程序将卡在内核 ioctl 中的循环中。

“内核”线程由于本地机器上的压力而零星地进入驱动程序。想象一下执行了 echo hello >! /dev/nd0(原始驱动程序的块设备名称是 nd0-127,它们的major number是 43)。echo 进程将通过块设备层进入内核,最终调用已注册的块设备请求处理程序以写入设备。NBD 的内核处理程序是函数 nbd_request。与所有块设备请求处理程序一样,nbd_request 执行一个连续循环 while(req = CURRENT),CURRENT 是内核宏,它扩展为写入请求结构的地址。在处理请求后,驱动程序使用 CURRENT = CURRENT->next 移动指针并循环。

内核线程的任务是执行以下操作

  • 将请求 req = CURRENT 链接到挂起传输列表的前面。

  • 嵌入唯一标识符,并通过网络将其副本发送到网络套接字另一端的服务器守护程序。

唯一标识符是请求 req 的内存地址。它仅在请求尚未得到服务时才是唯一的,但这已经足够了。(当驱动程序过去由于我们从未能够确定的神秘损坏而崩溃时,崩溃通常与重复条目和随之而来的循环列表相关联,这可能是一个线索。)

在网络的另一端,服务器守护程序接收写入请求,将“hello”写入其本地资源,并将包含请求唯一标识符的确认发送给客户端。

本地机器上的客户端守护程序线程在其循环中,在内核内被阻止从套接字读取,等待数据出现。它的任务现在是执行以下操作

  • 识别确认中的唯一标识符,将其与部分完成的请求链接列表中最旧的(最后一个)元素 req 进行比较。

  • 从未完成列表取消链接请求 req,并通过调用 end_request 告诉块层丢弃该结构。

此协议要求收到的确认是针对驱动程序内部列表尾部挂起的请求,而来自内核的新请求被添加到头部。TCP 可以保证这一点,因为 TCP 流的顺序性质。即使丢失一个数据包也会破坏当前驱动程序,但这也意味着 TCP 套接字已损坏。在这种情况下,套接字将返回错误。该错误消息允许驱动程序优雅地断开连接。

The Network Block Device

图 4。

NBD 中的内核网络与用户空间网络。用户空间网络需要额外的副本和其他开销,但提供了更大的灵活性。可以通过一次传输多个请求来抵消开销。

原始内核驱动程序中的客户端控制流在图 4 的左侧示意性地显示。黑色矩形表示一个请求。它由内核线程链接到设备的请求队列中,然后在内核中客户端守护程序的永久循环中被清除。客户端线程在内核中执行网络操作。在我们随后开发的驱动程序中,我们开始倾向于用户端网络,其中客户端线程在用户端处理从内核传输的请求副本。它反复进入内核以复制数据,然后以标准网络代码传输数据。开销大得多,但灵活性也大得多。可以通过一次传输多个请求来缓解开销,我们当前的驱动程序就是这样做的。通常,每次访问内核时都会传输 10 到 20 个每个块的请求。但是,内核空间和用户空间之间复制数据的成本是无法避免的。当客户端变为空闲时,多个客户端守护程序争夺内核请求,通过可能不同的路由和物理设备通过网络传输它们。情况如图 4 所示。每个客户端守护程序处理一个通道,但将调解任何请求。通道提供冗余、弹性和带宽。

The Network Block Device

图 5。

当前 NBD 驱动程序中的多个客户端守护程序捕获内核请求,通过跨多个不同网络通道的需求多路复用提供冗余和负载均衡。

表 3

表 3 显示了“在线”的完整数据协议序列。请注意,唯一 ID 是 64 位的,因此它可以使用请求的内存地址作为 64 位架构上的标识符。奇怪的是,请求的数据偏移量和长度在原始驱动程序中是 32 位字节偏移量,尽管它们是从扇区号(扇区每个 512 位)计算出来的,扇区号可能已经被使用过。这是原始 NBD 中的隐藏 32 位限制。我们的版本在 32 位文件系统或机器架构上实现了 64 位限制。服务器守护程序已修改为在多个不同的资源文件或设备之间多路复用超过 32 位的请求。

改进

表 4

我们的工业合作伙伴在驱动程序开发中的目标是改进对工业强度环境重要的领域(参见表 4)。

例如,当网络链接断开或服务器或客户端守护程序死机时,原始驱动程序会不可挽回地崩溃。即使重新连接,I/O 错误也会对更高的层(例如,包含的 RAID 堆栈)可见,这会导致 NBD 组件脱机。我们致力于通过使外部可观察到的故障更难以引起来提高稳健性,让驱动程序在内部处理任何瞬态问题。当前驱动程序在块级别记录事务。损坏的事务被驱动程序“回滚”并重新提交。双重完成的事务不被视为错误,第二次完成将被静默丢弃。

如果事务完成时间过长(“请求老化”),如果其标头或数据变得明显损坏,或者如果通信通道在事务启动和完成之间出错,则事务会损坏。为了保持其链接状态的最新映像,驱动程序在安静间隔发送简短的保持活动信号。数据仍然可能因按下电源开关而丢失,但不会因断开网络然后重新连接网络而丢失,也不会因仅关闭当前驱动程序中的几个通信通道中的一个或两个而丢失。驱动程序具有设计内置的冗余。此处的冗余意味着通道立即故障转移到备用通道,并为同一请求的多次传输和接收提供准备。

连接可能会以多种方式丢失,但不同路由的网络通道至少可以幸存下来。守护程序客户端线程(每个通道一个)根据需求和它们自身的可用性(也已尝试过基于内核的轮询多路复用)在它们之间进行通信的需求多路复用。在需求多路复用下,最快的工作客户端线程竞标并获得大部分工作,而停止或不工作的线程将其工作负载份额默认为工作线程。结合日志事务和请求老化,这似乎非常有效。通道自动“故障转移”到可用的替代方案。

速度也是行业中重要的卖点。我们的客户端守护程序线程是完全异步的,以实现最大速度。它们在内核内部不通过锁或信号量同步(除了原子化访问 NBD major 请求队列的一个信号量)。这意味着内核请求在其生命周期的各个阶段可能会被无序处理,因此必须仔细修改驱动程序以满足此要求。特别是,标准的 Linux 内核单链请求列表已转换为双链列表,挪用了原本未使用的指针字段。然后,驱动程序可以更轻松地“遍历队列”以在无序情况下搜索匹配项。内核真的应该实现双链请求队列结构。速度的主要制约因素可能是在一个内核请求到达时不加选择地唤醒所有客户端线程,但是当请求的到达速度快于守护程序发送请求的速度时,这种开销就会消失。

虽然从驱动程序外部可能无法观察到网络通道的故障转移,但它会在某种意义上降低通信质量。因此,我们当前的守护程序实现会重新连接和重新协商,以尝试恢复失败的通道。如果守护程序本身死机,那么它们会被守护程序自动重启,并且一旦身份验证协议成功重复,内核驱动程序将允许它们替换丢失的套接字连接。重新连接后,任何挂起的读/写请求都会解除阻塞(如果它们尚未通过另一个通道回滚和重新提交)。如果设备是 RAID 堆栈的一部分,则永远不会注意到中断。

远程机器和本地机器之间的连接,当像我们的驱动程序中那样通过多个端口进行多路复用时,可以在物理上路由到两个不同的网络接口,从而使可用带宽翻倍。跨 n 个 NIC 路由会将可用带宽乘以 n,直到 CPU 饱和为止。由于通过单个 3c905 NIC 在 NBD 上进行流式传输会将 Pentium 200MHz CPU 在 100BT 网络上的负载提高到约 15%,因此在单个 NIC 上可用的带宽增加系数约为 4 到 5(我们无法测试饱和水平,因为缺少足够的 PCI 插槽来执行此操作)。

另一个重要的工业要求是通信通道的绝对安全性。我们选择通过外部 SSL 连接传递所有通信,因为它将安全方面转移到我们信任的 SSL 实现。该决定要求我们将网络代码完全移出内核并移入客户端守护程序,客户端守护程序与 openSSL 代码链接。将代码移至用户端会降低协议速度,SSL 层还会进一步降低协议速度。最快(非平凡)的 SSL 算法使吞吐量下降 50%,最慢的下降 80%。另一种选择是通过 cipe 隧道在内核中传递通信链接,或者将服务器或客户端守护程序站点放在加密文件系统上,但我们尚未试验这些可能性。

SSL 为我们提供了具有最容易理解的安全含义的机制,我们选择了它。作为备用方案,驱动程序本身提供了一个原始身份验证协议——令牌在首次连接时交换,重新连接需要再次出示令牌。守护程序将此交换和整个会话包装在 openSSL 身份验证机制中,因此几乎不需要它。压缩在链接上传输的数据也将提供更大的带宽(以及可能的安全性),但到目前为止,我们尚未实现它。它可以应用于用户端守护程序代码中。

无论代码的其余部分可能存在或可能不存在哪些限制,服务器守护程序都受到 32 位架构上 EXT2 和其他文件系统的 32 位实现的约束。它们最多只能寻找到 31 位(32 位)偏移量,这会将服务的资源大小限制为 2GB(4GB)。但是,我们的服务器守护程序提供 RAID 线性或条带模式。在这些模式下,它们从两个或多个物理资源提供服务,每个物理资源的大小可能高达 2GB,从而使可用的总资源大小突破 32 位限制所需的任何数量。条带化在提高带宽方面似乎特别有效,原因不明。守护程序还可以直接从原始块设备提供服务。在这种情况下,我们不知道内核代码中是否存在 32 位限制——大概没有。

列表 1

/proc/nbdinfo 条目为我们提供了驱动程序状态的详细信息,包括已处理、出错等的请求数量。该页面是只读的(请参见列表 1)。

对默认驱动程序参数(例如,保持活动信号之间的间隔和预读块的数量)的调整是通过在加载驱动程序时通过模块选项进行的。目前没有机制可以在其他时间更改参数。可能需要可写的 \proc 条目或串行控制设备。客户端守护程序和从属客户端守护程序具有一定程度的控制权,它们在收到特定信号时向设备发送特殊的 ioctl。例如,USR2 信号会触发 ioctl,该 ioctl 会使所有剩余请求出错,将客户端守护程序线程从内核中弹出,并将驱动程序置于可以安全地从正在运行的内核中删除模块代码的状态。当然,这是一种旨在帮助调试的机制。这种方法的缺点是它需要客户端守护程序在运行才能行使控制权。在未来,我们打算实现基于 /proc 的可写接口。在设备中使用虚假分区信息也是获得更好报告和增强控制的吸引人的途径。

编写 Linux 设备驱动程序代码

编写驱动程序代码对于所有参与者来说都是一次有益的体验。对任何考虑编写内核代码的人的最佳建议是——不要这样做。如果必须这样做,请尽可能少地编写代码,并使其尽可能独立于其他任何东西。

只要事情进展顺利,实现自己的设计相对容易。然而,第一个错误揭示了困难。内核代码错误经常导致机器崩溃,几乎没有机会检测和纠正它们。每天 20 次重启周期可能接近每个人的极限。有时,我们不得不通过将版本之间的代码更改减半来查找错误,直到找到精确的行。由于适量的更改可能会导致(例如)200 行或更多的补丁,因此可能需要八个重新编译周期和测试才能找到所涉及的点更改。这还没有说明将补丁分成独立部分以便能够重新编译所涉及的脑力劳动,以及开发错误测试或首先识别行为异常所涉及的努力。通过代码对半查找错误,需要花费一到两周的时间是一个合理的估计。

拥有一个始终工作的内核代码非常重要。代码是否具有正确的功能并不重要,但它必须正确地执行其功能。代码开发必须计划分阶段从一个工作状态推进到另一个工作状态。绝不能存在驱动程序无法工作的开发阶段,例如更改了协议但尚未与其他地方的相应更改相平衡。

拥有一个工作版本意味着定期检入工作版本(我们使用 CVS)。检入仅发生在工作版本上。在少数情况下,我们不得不fork分支以允许两个开发领域独立进行(例如,将网络代码移出内核,同时重新设计重新连接协议),然后通过一系列非工作的小版本重新集成更改,但我们始终有一个可用的先前工作版本,我们试图对其进行最小的更改。

调试技术本质上包括通过 printk 语句生成可用的跟踪信息。我们在每个函数、活动和分支的入口和出口处都有 printk。这有助于我们发现编码错误发生的位置。然而,通常情况下,错误无法从代码跟踪中检测到,而必须通过行为分析来推断。我们曾遇到一个严重的错误,该错误在半个开发周期中都存在,直到集成测试开始才被检测到。它完全被正常的内核块缓冲所掩盖,并且仅在大型(超过 64MB)内存传输中才表现为明显的缓冲区损坏。当机器的其余部分处于高负载状态时,整个设备的 md5sum 有时会返回不同的结果。事实证明,这是两个简单的错误,一个在内核端,一个在服务器端,与缓冲完全无关。在这种情况下,集思广益可能的因果链并为其设计测试,然后运行测试并解释结果是唯一可行且正确的调试技术。这就是 18 世纪和 19 世纪阐述的“科学方法”,它适用于调试。

当必须吸收非自己设计的内核机制时,内核编码真正开始变得棘手。例如,与缓冲代码的交互在某种程度上不得不信任,因为阅读缓冲代码 (buffer.c) 本身并不能说明全部情况(例如,单独的内核线程何时以及如何释放缓冲区)。 好的建议是尝试将与其他内核机制的交互限制在绝对可预测的范围内,如有必要,可以通过模仿其他驱动程序示例来模式化交互。 在 NBD 驱动程序的情况下,最初是从环回驱动程序 (lo.c) 开发的,后者在整个过程中都作为有用的参考。

摘要

网络块设备将客户端通过网络连接到远程服务器,创建一个物理上远程的本地块设备。我们开发的驱动程序提供了冗余、可靠性和安全机制,使其可以用作工业环境中的实时备份存储介质,并允许其他更具想象力的模式。一个移动代理,可以将其家庭环境带到它访问的每个系统,也许?在速度方面,支持 EXT2 文件系统的 NBD 可以与 NFS 很好地竞争。

致谢

P. T. Breuer, (ptb@it.uc3m.es)

A. Martín Lopez

Arturo García Ares

加载 Disqus 评论