Linux DRBD-9 内核对象模型详解:resource、connection、device 与 peer_device 矩阵结构
DRBD(Distributed Replicated Block Device)是 Linux 内核中一个"共享无、同步复制"的块设备驱动,常被视作"网络 RAID 1",是构建高可用集群时替代共享存储的基础构件。本文以内核文档 data-structure-v9.rst 为核心,完整讲解 DRBD-9 在内核中的四类核心对象(resource / connection / device / peer_device)的组织方式、它们之间的矩阵式互联关系,以及双链表与 idr 两套索引机制,并结合 drivers/block/drbd 目录下的真实源码实现(结构体定义、全局链表、遍历宏)逐层印证文档所述的设计。读完后,你将能够理解 DRBD 设备树的内核对象布局、各对象的引用计数规则,以及在代码中如何按卷号(volume number)或次设备号(minor number)快速定位对象。
1. DRBD 的对象层级:resource → device / connection → peer_device
文档开篇给出的基本模型是:一个节点上可以有多个 DRBD resource(资源);每个 resource 下有多个 device(设备,也称 volume,卷),以及到其它节点("peer nodes",对端节点)的 connection(连接)。每个 DRBD device 在本地都表现为一个块设备(即用户空间看到的 /dev/drbdX)。
用一句话概括层级关系:
- resource 是顶层容器,对应一组逻辑上关联的卷和一条(或多条)对端连接;
- device 表示本节点的一个卷,对外是一个块设备,由次设备号 minor 唯一标识;
- connection 表示本节点与某个对端节点之间的一条网络连接;
- peer_device 表示"本设备的某个卷"在"某条连接的远端"的映像,它坐落在 device 与 connection 的每一个交叉点上。
这个模型在 Kconfig 帮助文本中也有呼应:每个 minor 设备有 'primary' 或 'secondary' 角色,主节点上的应用通过 /dev/drbdX 访问数据,每次写入都会发送到本地"下层块设备",并同时通过网络发往处于 'secondary' 状态的对端节点,可参见 drivers/block/drbd/Kconfig。
2. 矩阵结构:peer_device 位于 device × connection 的交叉点
文档最核心的部分是用一张矩阵图描述对象互联方式:
/--------------+---------------+.....+---------------\
| resource | device | | device |
+--------------+---------------+.....+---------------+
| connection | peer_device | | peer_device |
+--------------+---------------+.....+---------------+
: : : : :
: : : : :
+--------------+---------------+.....+---------------+
| connection | peer_device | | peer_device |
\--------------+---------------+.....+---------------/
解读这张图需要把握三条规则(均为文档原文的要点,且可逐条在源码中得到印证):
- 横向(同一行):device 可以从其 resource 按**卷号(volume number)**索引到;同理,peer_device 可以从其 connection 按卷号索引到。
- 纵向(同一列/行方向):对象之间通过双向链表互联。
- 回指针(back pointers):peer_device 指回它所属的 connection 和 device;connection 和 device 都指回它们的 resource。
与文档对照 drivers/block/drbd/drbd_int.h 中 struct drbd_peer_device 的定义,回指针的说法完全成立:
struct drbd_peer_device {
struct list_head peer_devices; /* 挂入 device->peer_devices 链表 */
struct drbd_device *device; /* 回指针:指向本地 device */
struct drbd_connection *connection; /* 回指针:指向所属 connection */
struct work_struct send_acks_work;
...
};
而"横向按卷号索引"在源码中由 idr(ID Radix tree) 实现:
struct drbd_resource中有struct idr devices; /* volume number to device mapping */,见 drbd_int.h#L595;struct drbd_connection中有struct idr peer_devices; /* volume number to peer device mapping */,见 drbd_int.h#L630。
查找入口是 drbd_int.h 中的内联函数 conn_peer_device():
static inline struct drbd_peer_device *
conn_peer_device(struct drbd_connection *connection, int volume_number)
{
return idr_find(&connection->peer_devices, volume_number);
}
这正是文档所说"peer_devices can be accessed from connections by their volume number"的代码级实现。
3. 全局索引:drbd_resources 链表与 drbd_devices idr
文档指出,除了矩阵内部关系外还有两套全局索引:
- 所有 resource 都挂在
drbd_resources这条双向链表中; - 所有 device 可以通过次设备号(minor device number)经
drbd_devices这个 idr 查找到。
源码中这两个全局对象在 drivers/block/drbd/drbd_main.c 定义,并在 drbd_int.h 中导出,注释明确写明了并发约束——RCU 保护、更新需持有 genl_lock():
/* drbd_int.h */
extern struct idr drbd_devices; /* RCU, updates: genl_lock() */
extern struct list_head drbd_resources; /* RCU, updates: genl_lock() */
按 minor 查找 device 的入口是 drbd_int.h#L942-L945:
static inline struct drbd_device *minor_to_device(unsigned int minor)
{
return (struct drbd_device *)idr_find(&drbd_devices, minor);
}
resource 的创建与入链过程在 drbd_main.c 的 drbd_create_resource() 中可以看到全貌:分配对象、kref_init() 初始化引用计数、idr_init(&resource->devices) 初始化卷号索引、INIT_LIST_HEAD(&resource->connections) 初始化连接链表,最后 list_add_tail_rcu(&resource->resources, &drbd_resources) 把新 resource 以 RCU 方式挂到全局链表尾部——与文档"All resources are in the drbd_resources double-linked list"一一对应。
此外,DRBD 代码提供了一整套遍历宏来沿这些结构游走,均定义在 drbd_int.h#L958-L983:
#define for_each_resource(resource, _resources) \
list_for_each_entry(resource, _resources, resources)
#define for_each_resource_rcu(resource, _resources) \
list_for_each_entry_rcu(resource, _resources, resources)
#define for_each_connection(connection, resource) \
list_for_each_entry(connection, &resource->connections, connections)
#define for_each_peer_device(peer_device, device) \
list_for_each_entry(peer_device, &device->peer_devices, peer_devices)
每一组都有普通 / _rcu / _safe 三个变体,分别对应直接遍历、RCU 读侧遍历和遍历中允许删除的安全遍历。这解释了文档所说的"objects are connected by double linked lists"在工程上是如何被安全地消费的:读路径(如 drbd_main.c 中 for_each_resource_rcu())无锁,写路径(如 drbd_main.c#L2333 中 for_each_resource_safe())持锁删除。
4. 引用计数与对象生命周期
文档最后一段明确了四类对象的生命周期规则,这是理解整个对象模型"谁拥有谁"的关键:
drbd_resource、drbd_connection、drbd_device 对象是引用计数的;peer_device 对象仅用于在 device 与 connection 之间建立链接,其生命周期由它所引用的 device 和 connection 的生命周期决定。
对应源码证据:
struct drbd_resource内嵌struct kref kref;(drbd_int.h#L594);struct drbd_connection内嵌struct kref kref;(drbd_int.h#L629);struct drbd_device内嵌struct kref kref;(drbd_int.h#L768);- 而
struct drbd_peer_device中没有 kref 成员,它只有list_head、两个回指针和一个工作项(drbd_int.h#L739-L747)。
也就是说,peer_device 是一个"关系对象":只要某个 device 还存在、某条 connection 还连着重,它们交叉点上的 peer_device 就存在;device 或 connection 任一被销毁,peer_device 随之消失。这种设计让矩阵的规模(卷数 × 连接数)不需要额外的生命周期管理代码。
5. 结构体要点速查:从源码看每类对象持有什么
为便于把文档概念落到代码上,下面按文档骨架列出四类对象的关键成员(摘录自 drivers/block/drbd/drbd_int.h):
5.1 struct drbd_resource(drbd_int.h#L586-L610)
| 成员 | 作用 |
|---|---|
char *name |
资源名 |
struct kref kref |
引用计数 |
struct idr devices |
卷号 → device 的映射(矩阵"横向"索引) |
struct list_head connections |
该资源的连接双向链表(矩阵"纵向") |
struct list_head resources |
挂入全局 drbd_resources 链表的节点 |
struct res_opts res_opts |
资源级配置 |
struct mutex conf_update / adm_mutex |
配置热更新、管理操作串行化 |
unsigned susp:1; susp_nod:1; susp_fen:1; |
三种挂起状态:用户挂起、无数据挂起、fence 处理挂起 |
enum write_ordering_e write_ordering |
写排序策略(创建时默认 WO_BDEV_FLUSH,见 drbd_main.c#L2528) |
cpumask_var_t cpu_mask |
该资源线程绑定的 CPU 掩码 |
5.2 struct drbd_connection(drbd_int.h#L621-L707)
| 成员 | 作用 |
|---|---|
struct list_head connections |
挂入 resource->connections 链表的节点 |
struct drbd_resource *resource |
回指针:指向所属资源 |
struct kref kref |
引用计数 |
struct idr peer_devices |
卷号 → peer_device 的映射(矩阵"横向"索引) |
enum drbd_conns cstate |
连接状态(文档说明取值范围为 C_STANDALONE 至 C_WF_REPORT_PARAMS) |
struct net_conf *net_conf |
RCU 保护的网络配置 |
struct drbd_socket data / meta |
双通道套接字:数据/屏障/状态包 与 ping/ack 元数据包分离 |
struct list_head transfer_log |
尚未完全处理完的请求(Transfer Log) |
struct drbd_thread receiver / worker / ack_receiver |
接收线程、工作线程、ack 接收线程 |
struct drbd_work_queue sender_work |
发送侧工作队列 |
可以看到一条 connection 同时承载了"矩阵纵列"的角色(connections 链表 + resource 回指针 + peer_devices idr)与完整的网络收发基础设施。
5.3 struct drbd_device(drbd_int.h#L749-L839)
| 成员 | 作用 |
|---|---|
struct drbd_resource *resource |
回指针:指向所属资源 |
struct list_head peer_devices |
该设备上所有 peer_device 的双向链表(矩阵"纵向") |
unsigned int vnr |
卷号(volume number within the connection) |
unsigned int minor |
次设备号,是全局 drbd_devices idr 的键 |
struct kref kref |
引用计数 |
struct drbd_backing_dev *ldev |
下层块设备(drbdsetup 配置) |
struct request_queue *rq_queue / struct gendisk *vdisk |
暴露给块层的请求队列与虚拟磁盘(即 /dev/drbdX 背后的 gendisk) |
union drbd_dev_state state |
磁盘状态(D_ 前缀)集合 |
atomic_t ap_pending_cnt 等一组原子计数 |
in-flight 请求统计(网络在途、等待本地完成等) |
struct rb_root read_requests / write_requests |
未完成本地请求的区间树 |
5.4 struct drbd_peer_device
如第 2 节所示,仅四个实质成员:peer_devices 链表节点、device 回指针、connection 回指针、send_acks_work。它是纯粹的连接关系载体。
6. 把文档模型落到运行时的三条查找路径
综合以上,文档描述的矩阵模型对应三条最常用运行路径,均在源码中可直接验证:
- 用户态 → device:内核接口拿到 minor 后调用
minor_to_device(minor),经全局drbd_devicesidr 直接定位(O(log N)),见 drbd_int.h#L942-L945。 - resource → device(按卷号):
idr_find(&resource->devices, volume_number)语义的 idr 查找;resource->devices在drbd_create_resource()中初始化(drbd_main.c#L2526)。 - connection → peer_device(按卷号):
conn_peer_device(connection, volume_number)(drbd_int.h#L952-L956)。
反向的"游走"则靠双向链表与遍历宏:从 device 出发 for_each_peer_device() 找到它在所有连接上的映像,从 resource 出发 for_each_connection() 找到所有对端连接。例如 drbd_nl.c#L3755 与 drbd_main.c#L2763 都采用了"先定位 device、再 for_each_peer_device 遍历其对端"的模式,正是矩阵纵列遍历的典型用法。
7. 小结:这套数据结构解决了什么问题
回到文档的动机——"Starting with Linux v3.14 we are reorganizing DRBD to use this data structure"(从 Linux v3.14 起,DRBD 重组为使用这套数据结构)。DRBD-9 的对象模型可以用三点总结:
- 矩阵化:device 与 connection 的笛卡尔积由 peer_device 承载,天然支持多卷(多 volume)× 多对端(含 dual-Primary、多节点组网)的场景;
- 双索引:横向用 idr 按卷号 O(log N) 定位,纵向用双向链表(RCU 友好)遍历,另加全局
drbd_devicesidr 支持按 minor 直接寻址; - 生命周期分层:resource / connection / device 三级引用计数保证共享安全,peer_device 无计数、随宿主对象存亡,避免了对矩阵交叉点的额外引用管理负担。
文档本身很短,但它勾勒的骨架是阅读 drivers/block/drbd 全部约 25 个源文件的"地图":先建立这四类对象与矩阵关系的心智模型,再去读 drbd_main.c(对象创建与生命周期)、drbd_nl.c(netlink 管理接口如何遍历这些结构)以及 drbd_int.h(结构与遍历宏),就能把 DRBD 的每个请求处理路径(写数据、resync、fencing)放到正确的对象上理解。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00