TCM 用户空间设计¶
设计¶
TCM 是 LIO 的另一个名称,它是一个内核内的 iSCSI 目标(服务器)。 现有的 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 层而不是文件系统层。 就像SUSE 一样,如果你愿意。
缺点是需要配置的组件更多,并且可能会出现故障。 这是不可避免的,但如果我们注意尽可能保持简单,希望不会是致命的。
设计约束¶
良好的性能:高吞吐量,低延迟
干净地处理以下用户空间情况
从未附加
挂起
死亡
行为不端
允许用户和内核实现在未来具有灵活性
合理地节省内存
易于配置和运行
易于编写用户空间后端
实施概述¶
TCMU 接口的核心是内核和用户空间之间共享的内存区域。 在此区域内:一个控制区域(邮箱); 一个无锁的生产者/消费者循环缓冲区,用于传递命令并返回状态; 以及一个输入/输出数据缓冲区区域。
TCMU 使用预先存在的 UIO 子系统。 UIO 允许在用户空间中进行设备驱动程序开发,这在概念上与 TCMU 用例非常接近,只是 TCMU 实现了一个专为 SCSI 命令设计的内存映射布局,而不是物理设备。 使用 UIO 还可以通过处理设备自检(例如,用户空间确定共享区域大小的方法)和双向信令机制来使 TCMU 受益。
内存区域中没有嵌入的指针。 一切都表示为相对于区域起始地址的偏移量。 这允许在用户进程死亡并在不同的虚拟地址映射该区域后,环仍然可以工作。
有关结构定义,请参见 target_core_user.h。
邮箱¶
邮箱始终位于共享内存区域的开头,并且包含版本、有关命令环的起始偏移量和大小的详细信息,以及内核和用户空间(分别)使用的头尾指针,用于将命令放入环中,并指示命令何时完成。
版本 - 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 指定有多少 iovec 条目紧随其后,在 iov[] 中覆盖数据输入区域。 与其他字段一样,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 接口中未定义此区域的组织,用户空间应仅访问待处理 iovs 引用的部分。
设备发现¶
除了 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。