TCM 用户空间设计¶
设计¶
TCM 是内核内 iSCSI 目标(服务器)LIO 的另一个名称。现有的 TCM 目标在内核中运行。TCMU(用户空间中的 TCM)允许编写用户空间程序作为 iSCSI 目标。本文档描述了其设计。
现有的内核为不同的 SCSI 传输协议提供了模块。TCM 也将数据存储模块化。目前有用于文件、块设备、RAM 或使用另一个 SCSI 设备作为存储的模块。这些被称为“后端存储”或“存储引擎”。这些内置模块完全作为内核代码实现。
背景¶
除了将用于承载 SCSI 命令的传输协议(“结构”)模块化之外,Linux 内核目标 LIO 还将实际的数据存储模块化。这些被称为“后端存储”或“存储引擎”。该目标带有后端存储,允许使用文件、块设备、RAM 或另一个 SCSI 设备作为导出的 SCSI LUN 所需的本地存储。与 LIO 的其余部分一样,这些都完全作为内核代码实现。
这些后端存储涵盖了最常见的用例,但并非全部。其他非内核目标解决方案(例如 tgt)能够支持的一个新用例是使用 Gluster 的 GLFS 或 Ceph 的 RBD 作为后端存储。然后,该目标充当翻译器,允许启动器将数据存储在这些非传统的网络存储系统中,同时仍然只使用标准协议本身。
如果目标是用户空间进程,则支持这些很简单。例如,tgt 只需要为每个后端存储提供一个小的适配器模块,因为这些模块仅使用 RBD 和 GLFS 的可用用户空间库。
在 LIO 中添加对这些后端存储的支持要困难得多,因为 LIO 完全是内核代码。与其进行大量工作将 GLFS 或 RBD API 和协议移植到内核,另一种方法是为 LIO 创建一个用户空间直通后端存储,“TCMU”。
优势¶
除了允许相对容易地支持 RBD 和 GLFS 之外,TCMU 还将允许更轻松地开发新的后端存储。TCMU 与 LIO 回环结构结合使用,类似于 FUSE(用户空间文件系统),但位于 SCSI 层而不是文件系统层。可以将其理解为 SCSI 层的 FUSE。
缺点是需要配置的组件更多,并且可能发生故障。这是不可避免的,但如果我们小心地尽可能保持简单,希望不会致命。
设计约束¶
良好的性能:高吞吐量,低延迟
如果用户空间
从未附加
挂起
死亡
行为不端
允许用户和内核实现具有未来的灵活性
合理地节省内存
易于配置和运行
易于编写用户空间后端
实现概述¶
TCMU 接口的核心是内核和用户空间之间共享的内存区域。在此区域内:有一个控制区域(邮箱);一个无锁的生产者/消费者循环缓冲区,用于传递命令和返回状态;以及一个输入/输出数据缓冲区区域。
TCMU 使用预先存在的 UIO 子系统。UIO 允许在用户空间中开发设备驱动程序,这在概念上与 TCMU 用例非常接近,只是 TCMU 实现的是专为 SCSI 命令设计的内存映射布局,而不是物理设备。使用 UIO 还可以通过处理设备自检(例如,让用户空间确定共享区域大小的方法)以及双向信号机制来使 TCMU 受益。
内存区域中没有嵌入的指针。一切都表示为相对于该区域起始地址的偏移量。如果用户进程死亡并重新启动,且该区域映射到不同的虚拟地址,这允许环仍然工作。
有关结构定义,请参阅 target_core_user.h。
邮箱¶
邮箱始终位于共享内存区域的开头,包含版本、有关命令环的起始偏移量和大小的详细信息,以及内核和用户空间(分别)用于在环上放置命令以及指示命令何时完成的头尾指针。
version - 1(否则用户空间应中止)
- 标志
- TCMU_MAILBOX_FLAG_CAP_OOOC
指示支持乱序完成。有关详细信息,请参阅“命令环”。
- cmdr_off
命令环的起始位置相对于内存区域起始位置的偏移量,用于考虑邮箱大小。
- cmdr_size
命令环的大小。这不需要是 2 的幂。
- cmd_head
由内核修改以指示何时在环上放置命令。
- cmd_tail
由用户空间修改以指示何时完成对命令的处理。
命令环¶
内核通过将 mailbox.cmd_head 增加命令的大小(模 cmdr_size)并将结果放置在环上,然后通过uio_event_notify()
向用户空间发送信号来将命令放置在环上。一旦命令完成,用户空间以相同的方式更新 mailbox.cmd_tail 并通过 4 字节的 write() 向内核发送信号。当 cmd_head 等于 cmd_tail 时,环为空 — 当前没有命令在等待用户空间处理。
TCMU 命令是 8 字节对齐的。它们以一个公共标头开头,其中包含“len_op”,这是一个 32 位值,存储长度以及最低的未使用位中的操作码。它还包含 cmd_id 和标志字段,供内核 (kflags) 和用户空间 (uflags) 设置。
当前仅定义了两个操作码,TCMU_OP_CMD 和 TCMU_OP_PAD。
当操作码为 CMD 时,命令环中的条目是一个 struct tcmu_cmd_entry。用户空间通过 tcmu_cmd_entry.req.cdb_off 查找 SCSI CDB(命令数据块)。这是相对于整个共享内存区域的起始位置的偏移量,而不是相对于条目的偏移量。可以通过 req.iov[] 数组访问输入/输出数据缓冲区。iov_cnt 包含 iov[] 中需要描述数据输入或数据输出缓冲区的条目数。对于双向命令,iov_cnt 指定有多少 iovec 条目覆盖数据输出区域,iov_bidi_cnt 指定紧随其后的 iov[] 中有多少 iovec 条目覆盖数据输入区域。与其他字段一样,iov.iov_base 是相对于区域起始位置的偏移量。
完成命令时,用户空间设置 rsp.scsi_status 以及必要时的 rsp.sense_buffer。然后,用户空间将 mailbox.cmd_tail 增加 entry.hdr.length(mod cmdr_size),并通过 UIO 方法(向文件描述符写入 4 个字节)向内核发送信号。
如果为 mailbox->flags 设置了 TCMU_MAILBOX_FLAG_CAP_OOOC,则内核能够处理乱序完成。在这种情况下,用户空间可以以不同于原始顺序的顺序处理命令。由于内核仍然会按照命令在命令环中出现的顺序处理命令,因此用户空间在完成命令时需要更新 cmd->id(即窃取原始命令的条目)。
当操作码为 PAD 时,用户空间仅如上所述更新 cmd_tail — 它是空操作。(内核插入 PAD 条目以确保每个 CMD 条目在命令环内是连续的。)
将来可能会添加更多操作码。如果用户空间遇到它不处理的操作码,则必须在 hdr.uflags 中设置 UNKNOWN_OP 位(第 0 位),更新 cmd_tail,并继续处理其他命令(如果有)。
数据区域¶
这是命令环之后的共享内存空间。此区域的组织未在 TCMU 接口中定义,用户空间应仅访问由挂起的 iov 引用的部分。
设备发现¶
除了 TCMU 之外,其他设备也可能正在使用 UIO。不相关的用户进程也可能正在处理不同的 TCMU 设备集。TCMU 用户空间进程必须通过扫描 sysfs class/uio/uio*/name 来查找其设备。对于 TCMU 设备,这些名称的格式如下
tcm-user/<hba_num>/<device_name>/<subtype>/<path>
其中 “tcm-user” 对于所有由 TCMU 支持的 UIO 设备都是通用的。<hba_num> 和 <device_name> 允许用户空间在内核目标的 configfs 树中找到设备的路径。假设通常的挂载点,它位于
/sys/kernel/config/target/core/user_<hba_num>/<device_name>
此位置包含诸如“hw_block_size”之类的属性,用户空间需要知道这些属性才能正确操作。
<subtype> 将是一个用户空间进程唯一的字符串,用于标识 TCMU 设备,该设备期望由某个处理程序支持,<path> 将是一个额外的特定于处理程序的字符串,供用户进程在需要时配置设备。由于 LIO 限制,名称不能包含“:”。
对于所有如此发现的设备,用户处理程序打开 /dev/uioX 并调用 mmap()
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
其中 size 必须等于从 /sys/class/uio/uioX/maps/map0/size 读取的值。
设备事件¶
如果添加或删除新设备,将通过 netlink 广播通知,使用名为“TCM-USER”的通用 netlink 族名称和一个名为“config”的多播组。 这将包括上一节中描述的 UIO 名称以及 UIO 次设备号。 这应该允许用户空间识别 UIO 设备和 LIO 设备,以便在确定设备受支持(基于子类型)后,它可以采取适当的操作。
其他意外情况¶
用户空间处理程序进程从未附加
TCMU 将发布命令,然后在超时时间段(30 秒)后中止它们。
用户空间处理程序进程被杀死
仍然可以重新启动并重新连接到 TCMU 设备。 命令环被保留。 但是,在超时时间段过后,内核将中止挂起的任务。
用户空间处理程序进程挂起
内核将在超时时间段后中止挂起的任务。
用户空间处理程序进程是恶意的
该进程可以很容易地破坏它控制的设备的处理,但不应该能够访问其共享内存区域之外的内核内存。
编写用户直通处理程序(带有示例代码)¶
处理 TCMU 设备的用户进程必须支持以下内容
发现和配置 TCMU uio 设备
等待设备上的事件
管理命令环:解析操作和命令,根据需要执行工作,设置响应字段(scsi_status 和可能的 sense_buffer),更新 cmd_tail,并通知内核工作已完成
首先,考虑为 tcmu-runner 编写插件。 tcmu-runner 实现了所有这些,并为插件作者提供了更高级别的 API。
TCMU 的设计使得多个不相关的进程可以单独管理 TCMU 设备。 所有处理程序都应确保仅根据已知的子类型字符串打开其设备。
发现和配置 TCMU UIO 设备
/* error checking omitted for brevity */ int fd, dev_fd; char buf[256]; unsigned long long map_len; void *map; fd = open("/sys/class/uio/uio0/name", O_RDONLY); ret = read(fd, buf, sizeof(buf)); close(fd); buf[ret-1] = '\0'; /* null-terminate and chop off the \n */ /* we only want uio devices whose name is a format we expect */ if (strncmp(buf, "tcm-user", 8)) exit(-1); /* Further checking for subtype also needed here */ fd = open(/sys/class/uio/%s/maps/map0/size, O_RDONLY); ret = read(fd, buf, sizeof(buf)); close(fd); str_buf[ret-1] = '\0'; /* null-terminate and chop off the \n */ map_len = strtoull(buf, NULL, 0); dev_fd = open("/dev/uio0", O_RDWR); map = mmap(NULL, map_len, PROT_READ|PROT_WRITE, MAP_SHARED, dev_fd, 0); b) Waiting for events on the device(s) while (1) { char buf[4]; int ret = read(dev_fd, buf, 4); /* will block */ handle_device_events(dev_fd, map); }
管理命令环
#include <linux/target_core_user.h> int handle_device_events(int fd, void *map) { struct tcmu_mailbox *mb = map; struct tcmu_cmd_entry *ent = (void *) mb + mb->cmdr_off + mb->cmd_tail; int did_some_work = 0; /* Process events from cmd ring until we catch up with cmd_head */ while (ent != (void *)mb + mb->cmdr_off + mb->cmd_head) { if (tcmu_hdr_get_op(ent->hdr.len_op) == TCMU_OP_CMD) { uint8_t *cdb = (void *)mb + ent->req.cdb_off; bool success = true; /* Handle command here. */ printf("SCSI opcode: 0x%x\n", cdb[0]); /* Set response fields */ if (success) ent->rsp.scsi_status = SCSI_NO_SENSE; else { /* Also fill in rsp->sense_buffer here */ ent->rsp.scsi_status = SCSI_CHECK_CONDITION; } } else if (tcmu_hdr_get_op(ent->hdr.len_op) != TCMU_OP_PAD) { /* Tell the kernel we didn't handle unknown opcodes */ ent->hdr.uflags |= TCMU_UFLAG_UNKNOWN_OP; } else { /* Do nothing for PAD entries except update cmd_tail */ } /* update cmd_tail */ mb->cmd_tail = (mb->cmd_tail + tcmu_hdr_get_len(&ent->hdr)) % mb->cmdr_size; ent = (void *) mb + mb->cmdr_off + mb->cmd_tail; did_some_work = 1; } /* Notify the kernel that work has been finished */ if (did_some_work) { uint32_t buf = 0; write(fd, &buf, 4); } return 0; }
最后说明¶
请注意返回 SCSI 规范定义的代码。 这些代码与 scsi/scsi.h 头文件中定义的某些值不同。 例如,CHECK CONDITION 的状态代码为 2,而不是 1。