tcmu design

目录
1) TCM Userspace 设计
    a)背景
    b)优势
    c)设计约束
    d)实现概览
        i. 邮件
        ii. 命令环
        iii. 数据区域
    e)设备发现
    f)设备事件
    g)其他细节

2)编写用户态传递handler
    a)发现和配置TCMU uio 设备
    b)等待设备的事件
    c)管理命令环

3)总结


TCM Userspace 设计
    TCM是LIO的别名,内核态的iscsi target。TCM targets运行在内核 态。TCMU(TCM in Userspace)运行用户态程序就像iSCSI targets一样。本篇文章描述TCMU的设计。
    内核以模块的方式提供不同的SCSI传输协议。TCM也模块化数据存储,目前已经支持文件,块设备,RAM或其他额SCSI设备,这些称为backstore或storage engine。这些内建的模块完全在内核态实现。

背景 Background
    除了模块化传输协议来携带SCSI命令,Linux kernel target, LIO, 也模块化真实数据存储,这指的是 backstore或storage engine。backstore的target可以是文件,块设备,RAM或其他额SCSI设备需要暴露SCSI LUN。
    backstore包含了大部分通用的使用案例,但不是所有的。一种新的使用案例:其他的非内核target解决方案,如tgt,能够支持Gluster的GLFS和Ceph的RBD作为backstore。target相当于转换器,允许initiators通过标准协议存储数据到这些非传统的网络存储系统。
    如果target是用户态进程,支持这些就相对容易很多。例如tgt,仅仅需要很小的适配模块,因为这些模块只是使用了已经可用的rbd和glfs的用户态库。
    为LIO增加这些支持存在更多的困难。因为LIO是纯内核态代码。替代把GLFS和RBD API和protocols库移植到内核这么复杂的方案,另一种方案是为LIO创建用户空间pass-through backstore,称为TCMU。
    
优势 Benefits
    除了可以容易支持RBD和GLFS,TCMU将允许更加简单的方式来开发新的backstore。TCMU与LIO loopback组合,实现类似于FUSE的机制,只是SCSI layer替换了filesystem layer。
    这种机制的劣势是需要配置更多不同的组件,和存在故障的风险。如果我们希望保持尽可能的简单的话,这些事不可避免的,只能希望故障不是致命的影响。


设计约束 Design constraints
- 高性能: 高吞吐量,低延迟。
- 简介处理,如果用户空间发生如下故障:
    1) nerver attaches
    2) hangs
    3) dies
    4) misbehaves
- 允许未来在用户空间和内核空间的灵活实现。
- 合理的内存使用。
- 简单地配置和运行。
- 简单地编写用户空间backend。

实现概览 Implementation overview
     TCMU接口的核心是一段由用户态和内核态共享的内存区域。这块区域包括:1. 控制区域(mailbox);2. 无锁的生产者消费者环形buffer用于命令的传递和状态的返回;3. 数据in/out的缓冲区。
    TCMU使用了已经存在的UIO子系统。UIO子系统允许设备驱动在用户态开发,这个概念十分贴近TCMU的使用案例,除了物理设备,TCMU为SCSI命令实现了内存映射层。使用UIO也有利于TCMU处理设备的自省,如通过用户态去决定使用多大的共享区域,和两端的信号机制。
    内存区域是没有指针的,只有相对于内存区域起始位置的offset。通过这种机制可以使在用户进程挂掉或者重启使得内存区域在不同的虚拟地址空间的情况下,仍然保持工作。
    可以查看target_core_user.h查看结构的定义。

Mailbox
   mailbox总是在共享内存的开始位置,并且包含了version,开始位置的offset,command ring的大小,用户态和内核态存放ring command和command完成状态的head和tail指针。

version - 1 (如果是别的值,用户空间应该废弃)
flags:
- TCMU_MAILBOX_FLAG_CAP_OOOC: 标志支持out-of-order completion。"The Command Ring"详细描述。
cmdr_off - command ring在内存区域的起始位置的偏移量。
cmdr_size - command ring区域的大小。这不需要2的幂来表示。
cmd_head - 由内核修改,表示一个command已经放置到ring中。
cmd_tail - 由用户空间修改,表示一个command已经处理完成。

The Command Ring
    Command放置到ring上,kernel根据command的size移动mailbox.cmd_head指针,取模cmdr_size,并且通过uio_event_notify()通知用户空间。当命令执行完成,用户空间更新mailbox.cmd_tail,并且通过write() 4个字节通知内核。当cmd_head等于cmd_tail,ring为空 -- 没有command等待用户空间处理。
    TCMU commands是8字节对齐的。command以通用header起头,header包含了len_op,32位的值,用于存储command的长度,同时使用了最低的3个bit存储操作码opcode。也包含了cmd_id和flags,由内核设置的kflags和用户空间设置的uflags。
    现在只定义了两种操作码opcode: TCMU_OP_CMD 和 TCMU_OP_PAD。
    当操作码 opcode是CMD,在command ring的entry是结构struct tcmu_cmd_entry。用户空间通过tcmu_cmd_entry.req.cdb_off找到SCSI CDB(Command Data Block)。这是command在整个共享内存区域起始位置的偏移量,不是entry里面的偏移量。数据io缓冲通过req.iov[]数据访问。iov_cnt包含了iov[] entries的数量,需要区分Data-In还是Data-Out的缓冲。对于双向的command,iov_cnt指定多少iovec entries覆盖了Data-Out区域,iov_bidi_cnt指定了多少iovec entries覆盖了Data-In区域(紧接在Data-Out区域)。就像别的field一样,iov.iov_base是相对于内存区域起始位置的偏移量。
    当command执行完成,用户空间设置rsp.scsi_status,如果有需要也设置rsp.sense_buffer。用户空间根据entry.hdr.length(mod cmdr_size)增加mailbox.cmd_tail,并且通过UIO方法通知内核,4字节写到文件描述符。
    如果设置TCMU_MAILBOX_FLAG_CAP_OOOC到mailbox->flags,kernel可以处理out-of-order completions。在这种情况下,用户空间能够处理与源头不同的顺序。由于kernel处理command的顺序与command ring中的一致,所以用户空间在command执行完成时,需要更新cmd->id。
    当操作码 opcode是PAD,用户空间只会更新cmd_tail -- 因为是一个no-op。kernel插入PAD entries确保每个CMD entry在command ring中是连续的。
    未来会加入更多的opcode。如果用户空间遇到一个不能处理的opcode,必须要设置hdr.uflags的第0个bit为UNKNOWN_OP,更新cmd_tail,处理附加的commands。
    
数据区域 The Data Area
    数据区域是在command ring的后面,TCMU接口没有定义这片区域的结构,用户空间应该只访问pengding iovs指定的区域。

设备发现 Device Discovery
    其他非TCMU设备也可能使用UIO子系统,无关的用户态进程也能够处理TCMU devices。TCMU用户态进程肯定能通过扫描sysfs(class/uio/uio*/name)找到他们的设备。对于TCMU设备,名字将会是如下格式:
tcm-user////                                                                                                                      
    "tcm-user"的位置对所有TCMU-backend的UIO设备是通用的。允许用户空间找到设备在kernel target的configfs树种的路径。如通常的挂载点,在下面路径下找到:
/sys/kernel/config/target/core/user_/
    这个位置包含了一些属性,如“hw_block_size",用户空间需要知道这些数据,以执行正确的操作。
    是用户空间进程唯一的字符串来标识TCMU设备,由确定的handler作为backend。是附加的handler指定的字符串,由用户空间进程配置设备时使用,如果需要的话,名字不能包含":",这是由LIO限制的。
    所有的设备都是这样被发现的,用户空间handler打开/dev/uioX并调用mmap():
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
    mmap函数的size参数必须等于下面这个值
           /sys/class/uio/uioX/maps/map0/size  

设备事件 Device Events
    如果设备添加或者移除,一个通知事件将通过netlink广播,使用通用的netlink family名字: "TCM-USER" 和"config"多播组。这将包括UIO名称和UIO的次设备号。这允许用户空间识别UIO设备和LIO设备,所以在识别设备是否支持后,可以采取合适的动作。

其他细节 Other contingencies
用户空间handler进程不关联:
    - TCMU将post commands,并且abort这些command,在30秒超时后。
用户空间handler进程被kill掉:
    - 这有可能restart和re-connect TCMU设备。Command ring是保留的。然而,在超时时间后,kernel将abort延迟的任务。
用户空间handler进程被挂起:
    - 在超时时间后,kernel将abort延迟的任务。
用户空间handler进程是恶意的:
    - 恶意handler进程可以打断自身与TCMU设备的联系,但是不可能在外部访问共享内存区域。


编写用户态传递handler
用户进程处理TCMU设备必须支持以下几点:
    a)发现和配置TCMU uio 设备。
    b)在TCMU设备中等待事件。
    c)管理Command ring:解析operations和commands,执行必要的工作,设置response fields(scsi_status 和 possibly sense_buffer), 更新cmd_tail,并通知kernel已完成。
    首先,考虑写一个tcmu-runner plugin。tcmu-runner实现了上面所有的点,并为plugin提供更高层次的API。
    TCMU被设计为多个无关的进程能够独立管理TCMU设备。所有的handlers应该通过已知的subtype字符串确保只打开自身的设备。

    a)发现和配置TCMU uio 设备。
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)在TCMU设备中等待事件
while  (1) {
   char  buf[4];
 
   int  ret = read(dev_fd, buf, 4);  /* will block */
 
   handle_device_events(dev_fd, map);
}

     c)管理Command ring
#include 
 
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  specifications中定义的,要十分小心。这里有些值与scsi/scsi.h中定义的不一致。如CHECK CONDITION的状态码是2,不是1。

原文: https://github.com/torvalds/linux/blob/master/Documentation/target/tcmu-design.txt#L177

你可能感兴趣的:(存储,Linux内核)