요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
====================
TCM Userspace Design
====================
.. Contents:
1) Design
a) Background
b) Benefits
c) Design constraints
d) Implementation overview
i. Mailbox
ii. Command ring
iii. Data Area
e) Device discovery
f) Device events
g) Other contingencies
2) Writing a user pass-through handler
a) Discovering and configuring TCMU uio devices
b) Waiting for events on the device(s)
c) Managing the command ring
3) A final note
Design
======
TCM is another name for LIO, an in-kernel iSCSI target (server).
Existing TCM targets run in the kernel. TCMU (TCM in Userspace)
allows userspace programs to be written which act as iSCSI targets.
This document describes the design.
The existing kernel provides modules for different SCSI transport
protocols. TCM also modularizes the data storage. There are existing
modules for file, block device, RAM or using another SCSI device as
storage. These are called "backstores" or "storage engines". These
built-in modules are implemented entirely as kernel code.
Background
----------
In addition to modularizing the transport protocol used for carrying
SCSI commands ("fabrics"), the Linux kernel target, LIO, also modularizes
the actual data storage as well. These are referred to as "backstores"
or "storage engines". The target comes with backstores that allow a
file, a block device, RAM, or another SCSI device to be used for the
local storage needed for the exported SCSI LUN. Like the rest of LIO,
these are implemented entirely as kernel code.
These backstores cover the most common use cases, but not all. One new
use case that other non-kernel target solutions, such as tgt, are able
to support is using Gluster's GLFS or Ceph's RBD as a backstore. The
target then serves as a translator, allowing initiators to store data
in these non-traditional networked storage systems, while still only
using standard protocols themselves.
If the target is a userspace process, supporting these is easy. tgt,
for example, needs only a small adapter module for each, because the
modules just use the available userspace libraries for RBD and GLFS.
Adding support for these backstores in LIO is considerably more
difficult, because LIO is entirely kernel code. Instead of undertaking
the significant work to port the GLFS or RBD APIs and protocols to the
kernel, another approach is to create a userspace pass-through
backstore for LIO, "TCMU".
Benefits
--------
In addition to allowing relatively easy support for RBD and GLFS, TCMU
will also allow easier development of new backstores. TCMU combines
with the LIO loopback fabric to become something similar to FUSE
(Filesystem in Userspace), but at the SCSI layer instead of the
filesystem layer. A SUSE, if you will.
The disadvantage is there are more distinct components to configure, and
potentially to malfunction. This is unavoidable, but hopefully not
fatal if we're careful to keep things as simple as possible.
Design constraints
------------------
- Good performance: high throughput, low latency
- Cleanly handle if userspace:
1) never attaches
2) hangs
3) dies
4) misbehaves
- Allow future flexibility in user & kernel implementations
- Be reasonably memory-efficient
- Simple to configure & run
- Simple to write a userspace backend
Implementation overview
-----------------------
The core of the TCMU interface is a memory region that is shared
between kernel and userspace. Within this region is: a control area
(mailbox); a lockless producer/consumer circular buffer for commands
to be passed up, and status returned; and an in/out data buffer area.
TCMU uses the pre-existing UIO subsystem. UIO allows device driver
development in userspace, and this is conceptually very close to the
TCMU use case, except instead of a physical device, TCMU implements a
memory-mapped layout designed for SCSI commands. Using UIO also
benefits TCMU by handling device introspection (e.g. a way for
userspace to determine how large the shared region is) and signaling
mechanisms in both directions.
There are no embedded pointers in the memory region. Everything is
expressed as an offset from the region's starting address. This allows
the ring to still work if the user process dies and is restarted with
the region mapped at a different virtual address.
See target_core_user.h for the struct definitions.
The Mailbox
-----------
The mailbox is always at the start of the shared memory region, and
contains a version, details about the starting offset and size of the
command ring, and head and tail pointers to be used by the kernel and
userspace (respectively) to put commands on the ring, and indicate
when the commands are completed.
version - 1 (userspace should abort if otherwise)
flags:
- TCMU_MAILBOX_FLAG_CAP_OOOC:
indicates out-of-order completion is supported.
See "The Command Ring" for details.
cmdr_off
The offset of the start of the command ring from the start
of the memory region, to account for the mailbox size.
cmdr_size
The size of the command ring. This does *not* need to be a
power of two.
cmd_head
Modified by the kernel to indicate when a command has been
placed on the ring.
cmd_tail
Modified by userspace to indicate when it has completed
processing of a command.
The Command Ring
----------------
Commands are placed on the ring by the kernel incrementing
mailbox.cmd_head by the size of the command, modulo cmdr_size, and
then signaling userspace via uio_event_notify(). Once the command is
completed, userspace updates mailbox.cmd_tail in the same way and
signals the kernel via a 4-byte write(). When cmd_head equals
cmd_tail, the ring is empty -- no commands are currently waiting to be
processed by userspace.
TCMU commands are 8-byte aligned. They start with a common header
containing "len_op", a 32-bit value that stores the length, as well as
the opcode in the lowest unused bits. It also contains cmd_id and
flags fields for setting by the kernel (kflags) and userspace
(uflags).
Currently only two opcodes are defined, TCMU_OP_CMD and TCMU_OP_PAD.
When the opcode is CMD, the entry in the command ring is a struct
tcmu_cmd_entry. Userspace finds the SCSI CDB (Command Data Block) via
tcmu_cmd_entry.req.cdb_off. This is an offset from the start of the
overall shared memory region, not the entry. The data in/out buffers
are accessible via the req.iov[] array. iov_cnt contains the number of
entries in iov[] needed to describe either the Data-In or Data-Out
buffers. For bidirectional commands, iov_cnt specifies how many iovec
entries cover the Data-Out area, and iov_bidi_cnt specifies how many
iovec entries immediately after that in iov[] cover the Data-In
area. Just like other fields, iov.iov_base is an offset from the start
of the region.
When completing a command, userspace sets rsp.scsi_status, and
rsp.sense_buffer if necessary. Userspace then increments
mailbox.cmd_tail by entry.hdr.length (mod cmdr_size) and signals the
kernel via the UIO method, a 4-byte write to the file descriptor.
If TCMU_MAILBOX_FLAG_CAP_OOOC is set for mailbox->flags, kernel is
capable of handling out-of-order completions. In this case, userspace can
handle command in different order other than original. Since kernel would
still process the commands in the same order it appeared in the command
ring, userspace need to update the cmd->id when completing the
command(a.k.a steal the original command's entry).
When the opcode is PAD, userspace only updates cmd_tail as above --
it's a no-op. (The kernel inserts PAD entries to ensure each CMD entry
is contiguous within the command ring.)
More opcodes may be added in the future. If userspace encounters an
opcode it does not handle, it must set UNKNOWN_OP bit (bit 0) in
hdr.uflags, update cmd_tail, and proceed with processing additional
commands, if any.
The Data Area
-------------
This is shared-memory space after the command ring. The organization
of this area is not defined in the TCMU interface, and userspace
should access only the parts referenced by pending iovs.
Device Discovery
----------------
Other devices may be using UIO besides TCMU. Unrelated user processes
may also be handling different sets of TCMU devices. TCMU userspace
processes must find their devices by scanning sysfs
class/uio/uio*/name. For TCMU devices, these names will be of the
format::
tcm-user/<hba_num>/<device_name>/<subtype>/<path>
where "tcm-user" is common for all TCMU-backed UIO devices. <hba_num>
and <device_name> allow userspace to find the device's path in the
kernel target's configfs tree. Assuming the usual mount point, it is
found at::
/sys/kernel/config/target/core/user_<hba_num>/<device_name>
This location contains attributes such as "hw_block_size", that
userspace needs to know for correct operation.
<subtype> will be a userspace-process-unique string to identify the
TCMU device as expecting to be backed by a certain handler, and <path>
will be an additional handler-specific string for the user process to
configure the device, if needed. The name cannot contain ':', due to
LIO limitations.
For all devices so discovered, the user handler opens /dev/uioX and
calls mmap()::
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
where size must be equal to the value read from
/sys/class/uio/uioX/maps/map0/size.
Device Events
-------------
If a new device is added or removed, a notification will be broadcast
over netlink, using a generic netlink family name of "TCM-USER" and a
multicast group named "config". This will include the UIO name as
described in the previous section, as well as the UIO minor
number. This should allow userspace to identify both the UIO device and
the LIO device, so that after determining the device is supported
(based on subtype) it can take the appropriate action.
Other contingencies
-------------------
Userspace handler process never attaches:
- TCMU will post commands, and then abort them after a timeout period
(30 seconds.)
Userspace handler process is killed:
- It is still possible to restart and re-connect to TCMU
devices. Command ring is preserved. However, after the timeout period,
the kernel will abort pending tasks.
Userspace handler process hangs:
- The kernel will abort pending tasks after a timeout period.
Userspace handler process is malicious:
- The process can trivially break the handling of devices it controls,
but should not be able to access kernel memory outside its shared
memory areas.
Writing a user pass-through handler (with example code)
=======================================================
A user process handing a TCMU device must support the following:
a) Discovering and configuring TCMU uio devices
b) Waiting for events on the device(s)
c) Managing the command ring: Parsing operations and commands,
performing work as needed, setting response fields (scsi_status and
possibly sense_buffer), updating cmd_tail, and notifying the kernel
that work has been finished
First, consider instead writing a plugin for tcmu-runner. tcmu-runner
implements all of this, and provides a higher-level API for plugin
authors.
TCMU is designed so that multiple unrelated processes can manage TCMU
devices separately. All handlers should make sure to only open their
devices, based opon a known subtype string.
a) Discovering and configuring TCMU UIO devices::
/* 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);
}
c) Managing the command ring::
#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;
}
A final note
============
Please be careful to return codes as defined by the SCSI
specifications. These are different than some values defined in the
scsi/scsi.h include file. For example, CHECK CONDITION's status code
is 2, not 1.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
문서 구성
1-24이 문서는 TCMU 설계를 배경·효과·제약·구현 개요로 설명하고, shared-memory mailbox·command ring·data area, device discovery와 event, 예외 상황을 다룬다.
후반부는 userspace pass-through handler 작성법을 device 발견·설정, event 대기, command ring 관리로 나누어 예제 code와 함께 설명하며 마지막에는 SCSI status code 주의 사항을 덧붙인다.
설계 설명과 handler 구현 절차로 구성된다.
====================
TCM Userspace Design
====================
.. Contents:
1) Design
a) Background
b) Benefits
c) Design constraints
d) Implementation overview
i. Mailbox
ii. Command ring
iii. Data Area
e) Device discovery
f) Device events
g) Other contingencies
2) Writing a user pass-through handler
a) Discovering and configuring TCMU uio devices
b) Waiting for events on the device(s)
c) Managing the command ring
3) A final note
TCM·LIO와 userspace target
25-39TCM은 in-kernel iSCSI target server인 LIO의 다른 이름이다. 기존 TCM target은 kernel에서 실행되지만 TCMU, 즉 TCM in Userspace는 userspace program이 iSCSI target 역할을 하게 한다.
kernel은 여러 SCSI transport protocol module을 제공하고 TCM은 실제 data storage도 modularize한다. file, block device, RAM, 다른 SCSI device를 storage로 쓰는 module을 backstore 또는 storage engine이라고 하며 기존 built-in module은 전부 kernel code로 구현된다.
transport와 storage를 각각 교체 가능한 module로 분리한다.
Design
======
TCM is another name for LIO, an in-kernel iSCSI target (server).
Existing TCM targets run in the kernel. TCMU (TCM in Userspace)
allows userspace programs to be written which act as iSCSI targets.
This document describes the design.
The existing kernel provides modules for different SCSI transport
protocols. TCM also modularizes the data storage. There are existing
modules for file, block device, RAM or using another SCSI device as
storage. These are called "backstores" or "storage engines". These
built-in modules are implemented entirely as kernel code.
Networked userspace backstore가 필요한 이유
40-68LIO는 SCSI command를 운반하는 transport protocol인 fabric뿐 아니라 실제 data storage인 backstore도 modularize한다. 기본 backstore는 exported SCSI LUN의 local storage로 file, block device, RAM, 다른 SCSI device를 사용할 수 있게 하며 모두 kernel code다.
이 module들이 일반적인 용도는 다루지만 전부는 아니다. userspace target인 `tgt` 같은 solution은 Gluster GLFS나 Ceph RBD를 backstore로 사용할 수 있다. target은 translator가 되어 initiator에는 표준 SCSI protocol만 보이게 하면서 data는 비전통적인 network storage에 저장한다.
target이 userspace process이면 RBD와 GLFS의 기존 userspace library를 호출하는 작은 adapter module만 작성하면 된다.
LIO는 kernel code이므로 GLFS나 RBD API와 protocol 전체를 kernel로 port하는 일은 훨씬 어렵다. 이를 피하는 대안이 LIO용 userspace pass-through backstore인 TCMU다.
표준 SCSI LUN을 userspace storage library에 연결한다.
Background
----------
In addition to modularizing the transport protocol used for carrying
SCSI commands ("fabrics"), the Linux kernel target, LIO, also modularizes
the actual data storage as well. These are referred to as "backstores"
or "storage engines". The target comes with backstores that allow a
file, a block device, RAM, or another SCSI device to be used for the
local storage needed for the exported SCSI LUN. Like the rest of LIO,
these are implemented entirely as kernel code.
These backstores cover the most common use cases, but not all. One new
use case that other non-kernel target solutions, such as tgt, are able
to support is using Gluster's GLFS or Ceph's RBD as a backstore. The
target then serves as a translator, allowing initiators to store data
in these non-traditional networked storage systems, while still only
using standard protocols themselves.
If the target is a userspace process, supporting these is easy. tgt,
for example, needs only a small adapter module for each, because the
modules just use the available userspace libraries for RBD and GLFS.
Adding support for these backstores in LIO is considerably more
difficult, because LIO is entirely kernel code. Instead of undertaking
the significant work to port the GLFS or RBD APIs and protocols to the
kernel, another approach is to create a userspace pass-through
backstore for LIO, "TCMU".
새 backstore 개발의 이점과 비용
69-81TCMU는 RBD와 GLFS를 비교적 쉽게 지원할 뿐 아니라 새 backstore 개발도 쉽게 한다. LIO loopback fabric과 결합하면 filesystem layer의 FUSE와 비슷한 구조를 SCSI layer에서 구현한다. 원문은 이를 말장난으로 SUSE라고 부른다.
단점은 설정해야 할 구성 요소와 고장날 가능성이 있는 구성 요소가 늘어난다는 점이다. 이는 피할 수 없지만 각 부분을 가능한 단순하게 유지하면 치명적 문제로 이어질 가능성을 낮출 수 있다.
userspace 확장성과 운영 복잡성의 교환이다.
Benefits
--------
In addition to allowing relatively easy support for RBD and GLFS, TCMU
will also allow easier development of new backstores. TCMU combines
with the LIO loopback fabric to become something similar to FUSE
(Filesystem in Userspace), but at the SCSI layer instead of the
filesystem layer. A SUSE, if you will.
The disadvantage is there are more distinct components to configure, and
potentially to malfunction. This is unavoidable, but hopefully not
fatal if we're careful to keep things as simple as possible.
설계 제약
82-98설계 목표는 높은 throughput과 낮은 latency, 합리적인 memory 효율, 단순한 설정과 실행, 쉬운 userspace backend 작성이다.
userspace가 전혀 attach하지 않거나 hang·종료·오동작하는 모든 상황을 깔끔하게 처리해야 한다. 또한 향후 kernel과 userspace 구현이 확장될 수 있는 유연성을 남겨야 한다.
성능·복원력·확장성과 사용성을 함께 요구한다.
Design constraints
------------------
- Good performance: high throughput, low latency
- Cleanly handle if userspace:
1) never attaches
2) hangs
3) dies
4) misbehaves
- Allow future flexibility in user & kernel implementations
- Be reasonably memory-efficient
- Simple to configure & run
- Simple to write a userspace backend
Shared memory와 UIO 기반 구현
99-121TCMU interface의 핵심은 kernel과 userspace가 공유하는 memory region이다. 이 region에는 control area인 mailbox, command와 status를 전달하는 lockless producer/consumer circular buffer, input/output data buffer area가 있다.
TCMU는 기존 UIO subsystem을 사용한다. UIO는 userspace device driver 개발을 지원하며, TCMU는 physical device 대신 SCSI command용 memory-mapped layout을 구현한다는 차이가 있다.
UIO를 사용하면 userspace가 shared region 크기를 알아내는 device introspection과 양방향 signaling mechanism도 재사용할 수 있다.
memory region 안에는 pointer를 넣지 않고 모든 위치를 region 시작 address로부터의 offset으로 표현한다. 따라서 user process가 죽었다가 다른 virtual address에 region을 mapping해 재시작해도 ring이 계속 동작한다. 구조체 정의는 `target_core_user.h`에 있다.
하나의 mmap region 안에서 control, command, data를 offset으로 연결한다.
TCMU가 기존 subsystem에서 재사용하는 기능이다.
Implementation overview
-----------------------
The core of the TCMU interface is a memory region that is shared
between kernel and userspace. Within this region is: a control area
(mailbox); a lockless producer/consumer circular buffer for commands
to be passed up, and status returned; and an in/out data buffer area.
TCMU uses the pre-existing UIO subsystem. UIO allows device driver
development in userspace, and this is conceptually very close to the
TCMU use case, except instead of a physical device, TCMU implements a
memory-mapped layout designed for SCSI commands. Using UIO also
benefits TCMU by handling device introspection (e.g. a way for
userspace to determine how large the shared region is) and signaling
mechanisms in both directions.
There are no embedded pointers in the memory region. Everything is
expressed as an offset from the region's starting address. This allows
the ring to still work if the user process dies and is restarted with
the region mapped at a different virtual address.
See target_core_user.h for the struct definitions.
Mailbox field와 소유권
122-150mailbox는 shared memory region 시작에 항상 놓이며 version, command ring 시작 offset과 크기, kernel과 userspace가 사용하는 head와 tail을 담는다. head는 command가 ring에 놓였음을, tail은 command 처리가 끝났음을 나타낸다.
version은 1이어야 하고 다른 값이면 userspace가 중단해야 한다. `TCMU_MAILBOX_FLAG_CAP_OOOC` flag는 out-of-order completion 지원을 뜻한다.
`cmdr_off`는 region 시작부터 command ring 시작까지의 offset으로 mailbox 크기를 반영한다. `cmdr_size`는 command ring 크기이며 2의 거듭제곱일 필요가 없다.
`cmd_head`는 kernel이 command를 추가할 때 수정하고 `cmd_tail`은 userspace가 command 처리를 완료할 때 수정한다.
field별 의미와 writer를 구분한다.
The Mailbox
-----------
The mailbox is always at the start of the shared memory region, and
contains a version, details about the starting offset and size of the
command ring, and head and tail pointers to be used by the kernel and
userspace (respectively) to put commands on the ring, and indicate
when the commands are completed.
version - 1 (userspace should abort if otherwise)
flags:
- TCMU_MAILBOX_FLAG_CAP_OOOC:
indicates out-of-order completion is supported.
See "The Command Ring" for details.
cmdr_off
The offset of the start of the command ring from the start
of the memory region, to account for the mailbox size.
cmdr_size
The size of the command ring. This does *not* need to be a
power of two.
cmd_head
Modified by the kernel to indicate when a command has been
placed on the ring.
cmd_tail
Modified by userspace to indicate when it has completed
processing of a command.
Command ring entry와 완료 protocol
151-202kernel은 command size만큼 `mailbox.cmd_head`를 `cmdr_size` modulo로 증가시켜 ring에 command를 놓고 `uio_event_notify()`로 userspace에 알린다. userspace가 완료하면 같은 방식으로 `cmd_tail`을 증가시키고 file descriptor에 4 byte를 `write()`해 kernel에 알린다. `cmd_head == cmd_tail`이면 ring이 비었다.
TCMU command는 8-byte aligned이고 공통 header로 시작한다. 32-bit `len_op`는 length와 사용하지 않는 하위 bit의 opcode를 함께 저장한다. `cmd_id`, kernel이 설정하는 `kflags`, userspace가 설정하는 `uflags`도 있다.
현재 opcode는 `TCMU_OP_CMD`와 `TCMU_OP_PAD` 두 개다. CMD entry는 `struct tcmu_cmd_entry`이며 SCSI CDB는 `req.cdb_off` offset에서 찾는다. offset 기준은 entry가 아니라 shared region 시작이다.
Data-In과 Data-Out buffer는 `req.iov[]`로 접근한다. `iov_cnt`는 단방향 command의 buffer를 설명하는 entry 수다. bidirectional command에서는 먼저 `iov_cnt`개가 Data-Out을, 바로 뒤의 `iov_bidi_cnt`개가 Data-In을 설명한다. `iov_base`도 region 시작 기준 offset이다.
userspace는 완료할 때 `rsp.scsi_status`와 필요하면 `rsp.sense_buffer`를 설정한다. 이어 `entry.hdr.length`만큼 tail을 modulo 증가시키고 UIO fd에 4 byte를 써 kernel을 깨운다.
mailbox flag에 `TCMU_MAILBOX_FLAG_CAP_OOOC`가 있으면 kernel은 out-of-order completion을 처리할 수 있다. userspace는 원래 순서와 다르게 command를 처리할 수 있지만 kernel은 ring 순서로 완료를 보므로 완료할 original entry를 가져오는 방식으로 `cmd->id`를 갱신해야 한다.
PAD entry는 no-op이며 userspace는 tail만 갱신한다. kernel은 각 CMD entry가 ring 안에서 연속되도록 wrap 경계에 PAD를 삽입한다.
향후 opcode가 추가될 수 있다. 처리할 수 없는 opcode를 만나면 `hdr.uflags` bit 0인 `UNKNOWN_OP`를 설정하고 tail을 갱신한 뒤 다음 command를 계속 처리해야 한다.
head와 tail은 서로 다른 주체가 갱신한다.
ring entry의 offset과 count 해석이다.
The Command Ring
----------------
Commands are placed on the ring by the kernel incrementing
mailbox.cmd_head by the size of the command, modulo cmdr_size, and
then signaling userspace via uio_event_notify(). Once the command is
completed, userspace updates mailbox.cmd_tail in the same way and
signals the kernel via a 4-byte write(). When cmd_head equals
cmd_tail, the ring is empty -- no commands are currently waiting to be
processed by userspace.
TCMU commands are 8-byte aligned. They start with a common header
containing "len_op", a 32-bit value that stores the length, as well as
the opcode in the lowest unused bits. It also contains cmd_id and
flags fields for setting by the kernel (kflags) and userspace
(uflags).
Currently only two opcodes are defined, TCMU_OP_CMD and TCMU_OP_PAD.
When the opcode is CMD, the entry in the command ring is a struct
tcmu_cmd_entry. Userspace finds the SCSI CDB (Command Data Block) via
tcmu_cmd_entry.req.cdb_off. This is an offset from the start of the
overall shared memory region, not the entry. The data in/out buffers
are accessible via the req.iov[] array. iov_cnt contains the number of
entries in iov[] needed to describe either the Data-In or Data-Out
buffers. For bidirectional commands, iov_cnt specifies how many iovec
entries cover the Data-Out area, and iov_bidi_cnt specifies how many
iovec entries immediately after that in iov[] cover the Data-In
area. Just like other fields, iov.iov_base is an offset from the start
of the region.
When completing a command, userspace sets rsp.scsi_status, and
rsp.sense_buffer if necessary. Userspace then increments
mailbox.cmd_tail by entry.hdr.length (mod cmdr_size) and signals the
kernel via the UIO method, a 4-byte write to the file descriptor.
If TCMU_MAILBOX_FLAG_CAP_OOOC is set for mailbox->flags, kernel is
capable of handling out-of-order completions. In this case, userspace can
handle command in different order other than original. Since kernel would
still process the commands in the same order it appeared in the command
ring, userspace need to update the cmd->id when completing the
command(a.k.a steal the original command's entry).
When the opcode is PAD, userspace only updates cmd_tail as above --
it's a no-op. (The kernel inserts PAD entries to ensure each CMD entry
is contiguous within the command ring.)
More opcodes may be added in the future. If userspace encounters an
opcode it does not handle, it must set UNKNOWN_OP bit (bit 0) in
hdr.uflags, update cmd_tail, and proceed with processing additional
commands, if any.
Data area 접근 규칙
203-210data area는 command ring 뒤에 있는 shared-memory 공간이다. 이 영역의 내부 배치는 TCMU interface가 정의하지 않으며 userspace는 pending command의 iov가 참조하는 부분에만 접근해야 한다.
layout을 추측하지 않고 iov reference만 따른다.
The Data Area
-------------
This is shared-memory space after the command ring. The organization
of this area is not defined in the TCMU interface, and userspace
should access only the parts referenced by pending iovs.
sysfs UIO device 발견과 mmap
211-246UIO는 TCMU 외의 device도 사용할 수 있고 서로 무관한 process가 서로 다른 TCMU device 집합을 처리할 수 있다. handler는 `sysfs class/uio/uio*/name`을 scan해 자신이 담당할 device를 찾아야 한다.
TCMU UIO name 형식은 `tcm-user/<hba_num>/<device_name>/<subtype>/<path>`다. `tcm-user`는 모든 TCMU-backed UIO device의 공통 prefix다.
`hba_num`과 `device_name`으로 kernel target ConfigFS tree의 device path를 찾는다. 일반 mount point라면 `/sys/kernel/config/target/core/user_<hba_num>/<device_name>`이며 이 위치에는 올바른 동작에 필요한 `hw_block_size` 같은 attribute가 있다.
`subtype`은 userspace process가 특정 handler를 기대하는 TCMU device를 식별하는 고유 string이다. `path`는 필요할 때 handler가 device를 설정하는 데 쓰는 추가 string이다. LIO 제한 때문에 name에 colon `:`을 넣을 수 없다.
발견한 device마다 `/dev/uioX`를 열고 `mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)`을 호출한다. size는 `/sys/class/uio/uioX/maps/map0/size`에서 읽은 값과 같아야 한다.
UIO name에서 handler match와 ConfigFS path를 파생한다.
각 path segment의 역할이다.
Device Discovery
----------------
Other devices may be using UIO besides TCMU. Unrelated user processes
may also be handling different sets of TCMU devices. TCMU userspace
processes must find their devices by scanning sysfs
class/uio/uio*/name. For TCMU devices, these names will be of the
format::
tcm-user/<hba_num>/<device_name>/<subtype>/<path>
where "tcm-user" is common for all TCMU-backed UIO devices. <hba_num>
and <device_name> allow userspace to find the device's path in the
kernel target's configfs tree. Assuming the usual mount point, it is
found at::
/sys/kernel/config/target/core/user_<hba_num>/<device_name>
This location contains attributes such as "hw_block_size", that
userspace needs to know for correct operation.
<subtype> will be a userspace-process-unique string to identify the
TCMU device as expecting to be backed by a certain handler, and <path>
will be an additional handler-specific string for the user process to
configure the device, if needed. The name cannot contain ':', due to
LIO limitations.
For all devices so discovered, the user handler opens /dev/uioX and
calls mmap()::
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
where size must be equal to the value read from
/sys/class/uio/uioX/maps/map0/size.
Generic netlink device event
247-258device가 추가되거나 제거되면 generic netlink family `TCM-USER`의 multicast group `config`로 notification을 broadcast한다.
event에는 앞 절의 UIO name과 UIO minor number가 들어 있다. userspace는 subtype으로 지원 여부를 판단하고 UIO device와 LIO device를 모두 식별해 적절한 동작을 수행할 수 있다.
netlink event를 지원 가능한 handler action으로 연결한다.
Device Events
-------------
If a new device is added or removed, a notification will be broadcast
over netlink, using a generic netlink family name of "TCM-USER" and a
multicast group named "config". This will include the UIO name as
described in the previous section, as well as the UIO minor
number. This should allow userspace to identify both the UIO device and
the LIO device, so that after determining the device is supported
(based on subtype) it can take the appropriate action.
Userspace 장애와 timeout
259-283userspace handler가 attach하지 않으면 TCMU는 command를 post한 뒤 30초 timeout 후 abort한다.
handler process가 종료되어도 재시작해 TCMU device에 다시 연결할 수 있고 command ring은 보존된다. 다만 timeout이 지나면 kernel이 pending task를 abort한다.
handler가 hang한 경우에도 kernel은 timeout 뒤 pending task를 abort한다.
악의적인 process는 자신이 제어하는 device 처리를 쉽게 망가뜨릴 수 있지만, 자신에게 공유된 memory area 밖의 kernel memory에는 접근할 수 없어야 한다.
handler 상태별 kernel 동작이다.
Other contingencies
-------------------
Userspace handler process never attaches:
- TCMU will post commands, and then abort them after a timeout period
(30 seconds.)
Userspace handler process is killed:
- It is still possible to restart and re-connect to TCMU
devices. Command ring is preserved. However, after the timeout period,
the kernel will abort pending tasks.
Userspace handler process hangs:
- The kernel will abort pending tasks after a timeout period.
Userspace handler process is malicious:
- The process can trivially break the handling of devices it controls,
but should not be able to access kernel memory outside its shared
memory areas.
Userspace handler의 세 책임
284-303TCMU device를 처리하는 user process는 device 발견과 설정, device event 대기, command ring 관리를 지원해야 한다.
command ring 관리에는 operation과 command parsing, 필요한 work 수행, `scsi_status`와 필요 시 `sense_buffer` 설정, `cmd_tail` 갱신, 완료 사실을 kernel에 통지하는 절차가 모두 포함된다.
직접 구현하기 전에 `tcmu-runner` plugin 작성을 먼저 고려해야 한다. tcmu-runner는 이 low-level 절차 전체를 구현하고 plugin author에게 더 높은 수준의 API를 제공한다.
TCMU는 서로 무관한 여러 process가 device를 따로 관리할 수 있도록 설계되었다. 각 handler는 알려진 subtype string을 기준으로 자신이 담당하는 device만 열어야 한다.
device 소유권 선택부터 완료 통지까지의 반복 작업이다.
Writing a user pass-through handler (with example code)
=======================================================
A user process handing a TCMU device must support the following:
a) Discovering and configuring TCMU uio devices
b) Waiting for events on the device(s)
c) Managing the command ring: Parsing operations and commands,
performing work as needed, setting response fields (scsi_status and
possibly sense_buffer), updating cmd_tail, and notifying the kernel
that work has been finished
First, consider instead writing a plugin for tcmu-runner. tcmu-runner
implements all of this, and provides a higher-level API for plugin
authors.
TCMU is designed so that multiple unrelated processes can manage TCMU
devices separately. All handlers should make sure to only open their
devices, based opon a known subtype string.
UIO 발견·mapping과 event 대기 예제
304-345예제는 brevity를 위해 error checking을 생략한다. `/sys/class/uio/uio0/name`을 읽고 newline을 제거한 뒤 name이 `tcm-user`로 시작하는지 확인하며 실제 handler는 subtype도 추가로 검사해야 한다.
`/sys/class/uio/uioX/maps/map0/size`를 읽어 `strtoull()`로 mapping length를 얻는다. 이어 `/dev/uio0`을 read/write로 열고 shared read/write mapping을 만든다.
event loop는 UIO fd에서 4 byte를 blocking read한다. event가 도착하면 `handle_device_events(dev_fd, map)`을 호출해 command ring을 처리한다.
code가 읽는 file과 결과다.
UIO notification마다 mapped ring을 비울 때까지 처리한다.
a) Discovering and configuring TCMU UIO devices::
/* 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);
}
Command ring 처리 예제
346-398예제는 `<linux/target_core_user.h>`를 include한다. mapped region 시작을 `struct tcmu_mailbox *mb`로 보고 `cmdr_off + cmd_tail`에서 첫 `struct tcmu_cmd_entry`를 계산한다.
entry 위치가 `cmdr_off + cmd_head`에 도달할 때까지 loop한다. opcode가 `TCMU_OP_CMD`이면 region 시작에 `req.cdb_off`를 더해 CDB를 찾고 예제에서는 첫 SCSI opcode를 출력한다.
backend work가 성공하면 `rsp.scsi_status = SCSI_NO_SENSE`로 설정한다. 실패하면 실제 구현에서 `sense_buffer`도 채우고 status를 `SCSI_CHECK_CONDITION`으로 설정한다.
opcode가 PAD가 아닌 알 수 없는 값이면 `TCMU_UFLAG_UNKNOWN_OP`를 `hdr.uflags`에 설정한다. PAD는 response work 없이 tail만 이동한다.
각 entry 후 `tcmu_hdr_get_len()` 길이를 더해 `cmdr_size` modulo로 `cmd_tail`을 갱신하고 다음 entry를 다시 계산한다. 하나라도 처리했다면 32-bit 0을 UIO fd에 4 byte 써 완료를 kernel에 통지한다.
예제 code의 branch와 tail 갱신 순서다.
예제 handler가 각 entry type에 수행하는 작업이다.
c) Managing the command ring::
#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 specification status code 사용
399-405반환 code는 반드시 SCSI specification에 정의된 값을 사용해야 한다. 이 값은 `scsi/scsi.h`에 정의된 일부 값과 다르다. 예를 들어 CHECK CONDITION의 status code는 1이 아니라 2다.
header의 다른 상수와 혼동하면 안 되는 예다.
A final note
============
Please be careful to return codes as defined by the SCSI
specifications. These are different than some values defined in the
scsi/scsi.h include file. For example, CHECK CONDITION's status code
is 2, not 1.
요약·해설
tcmu-design.rst:1-405TCMU가 UIO와 offset 기반 shared memory를 이용해 LIO SCSI command를 userspace backstore로 전달하는 mailbox·command ring·data area 설계, device discovery, event, timeout과 handler 구현 절차를 설명합니다.