optimize the perf and support more features

This commit is contained in:
Lei Xue
2026-03-14 11:45:35 +08:00
parent 7e7ebacd9d
commit 00cfac3d24
56 changed files with 6340 additions and 1019 deletions

View File

@@ -82,9 +82,9 @@ func bsPerformCommand(bs api.BackingStore, cmd *api.SCSICommand) (err error, key
doWrite = true
goto write
case api.COMPARE_AND_WRITE:
// TODO
doWrite = true
goto write
// COMPARE_AND_WRITE is handled directly in SBCCompareAndWrite function
// This case should not be reached
return fmt.Errorf("COMPARE_AND_WRITE should be handled by SBCCompareAndWrite"), ILLEGAL_REQUEST, ASC_INVALID_OP_CODE
case api.SYNCHRONIZE_CACHE, api.SYNCHRONIZE_CACHE_16:
if tl == 0 {
tl = int64(lu.Size - offset)

View File

@@ -8,7 +8,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,

View File

@@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,

View File

@@ -20,6 +20,7 @@ import (
"fmt"
"io"
"os"
"strings"
log "github.com/sirupsen/logrus"
@@ -51,10 +52,33 @@ func new() (api.BackingStore, error) {
}, nil
}
// parseStoragePath parses a storage path that may include backend type prefix
// Format: [backend_type:]path
// Examples:
// - /var/tmp/disk.img (default file backend)
// - file:/var/tmp/disk.img (explicit file backend)
// - iouring:/var/tmp/disk.img (io_uring backend on Linux 5.1+)
func parseStoragePath(path string) (backendType, filePath string) {
if idx := strings.Index(path, ":"); idx > 0 {
possibleType := path[:idx]
// Check if it's a known backend type
switch possibleType {
case "file", "iouring", "ceph", "null", "RemBs":
return possibleType, path[idx+1:]
}
}
// Default to file backend
return "file", path
}
func (bs *FileBackingStore) Open(dev *api.SCSILu, path string) error {
var mode os.FileMode
finfo, err := os.Stat(path)
// Parse backend type and actual path
backendType, filePath := parseStoragePath(path)
_ = backendType // file backend ignores this
finfo, err := os.Stat(filePath)
if err != nil {
return err
} else {
@@ -62,7 +86,7 @@ func (bs *FileBackingStore) Open(dev *api.SCSILu, path string) error {
mode = finfo.Mode()
}
f, err := os.OpenFile(path, os.O_RDWR, os.ModePerm)
f, err := os.OpenFile(filePath, os.O_RDWR, os.ModePerm)
if err == nil {
// block device filesize needs to be treated differently

View File

@@ -0,0 +1,727 @@
//go:build linux
// +build linux
/*
Copyright 2024 The GoStor Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package iouring provides an io_uring-based backing store for high-performance
// asynchronous I/O operations on Linux 5.1+ systems.
package iouring
import (
"fmt"
"os"
"runtime"
"sync"
"sync/atomic"
"syscall"
"unsafe"
log "github.com/sirupsen/logrus"
"github.com/gostor/gotgt/pkg/api"
"github.com/gostor/gotgt/pkg/scsi"
)
const (
IoUringBackingStorage = "iouring"
// Default queue depth for io_uring
DefaultQueueDepth = 4096
// Minimum kernel version required (5.1)
MinKernelMajor = 5
MinKernelMinor = 1
)
// io_uring constants (from linux/io_uring.h)
const (
IORING_SETUP_IOPOLL = 1 << 0
IORING_SETUP_SQPOLL = 1 << 1
IORING_SETUP_SQ_AFF = 1 << 2
IORING_SETUP_CQSIZE = 1 << 3
IORING_SETUP_CLAMP = 1 << 4
IORING_SETUP_ATTACH_WQ = 1 << 5
IORING_SETUP_R_DISABLED = 1 << 6
IORING_FSYNC_DATASYNC = 1 << 0
IORING_TIMEOUT_ABS = 1 << 0
IORING_OFF_SQ_RING = 0
IORING_OFF_CQ_RING = 0x8000000
IORING_OFF_SQES = 0x10000000
IORING_OP_NOP = 0
IORING_OP_READV = 1
IORING_OP_WRITEV = 2
IORING_OP_FSYNC = 3
IORING_OP_READ_FIXED = 4
IORING_OP_WRITE_FIXED = 5
IORING_OP_POLL_ADD = 6
IORING_OP_POLL_REMOVE = 7
IORING_OP_SYNC_FILE_RANGE = 8
IORING_OP_SENDMSG = 9
IORING_OP_RECVMSG = 10
IORING_OP_TIMEOUT = 11
IORING_OP_TIMEOUT_REMOVE = 12
IORING_OP_ACCEPT = 13
IORING_OP_ASYNC_CANCEL = 14
IORING_OP_LINK_TIMEOUT = 15
IORING_OP_CONNECT = 16
IORING_OP_FALLOCATE = 17
IORING_OP_OPENAT = 18
IORING_OP_CLOSE = 19
IORING_OP_FILES_UPDATE = 20
IORING_OP_STATX = 21
IORING_OP_READ = 22
IORING_OP_WRITE = 23
IORING_OP_FADVISE = 24
IORING_OP_MADVISE = 25
IORING_OP_SEND = 26
IORING_OP_RECV = 27
IORING_OP_OPENAT2 = 28
IORING_OP_EPOLL_CTL = 29
IORING_OP_SPLICE = 30
IORING_OP_PROVIDE_BUFFERS = 31
IORING_OP_REMOVE_BUFFERS = 32
IORING_OP_TEE = 33
IORING_OP_SHUTDOWN = 34
IORING_OP_RENAMEAT = 35
IORING_OP_UNLINKAT = 36
IORING_OP_MKDIRAT = 37
IORING_OP_SYMLINKAT = 38
IORING_OP_LINKAT = 39
IORING_OP_MSG_RING = 40
IORING_OP_FSETXATTR = 41
IORING_OP_SETXATTR = 42
IORING_OP_FGETXATTR = 43
IORING_OP_GETXATTR = 44
IORING_OP_SOCKET = 45
IORING_OP_URING_CMD = 46
IORING_OP_SEND_ZC = 47
IORING_OP_SENDMSG_ZC = 48
IORING_CQE_F_BUFFER = 1 << 0
IORING_CQE_F_MORE = 1 << 1
)
// io_uring structures
// Note: These are simplified structures for the operations we need
type ioUring struct {
fd int
sq *ioUringSq
cq *ioUringCq
flags uint32
ringSize int
}
type ioUringSq struct {
head *uint32
tail *uint32
ringMask *uint32
ringEntries *uint32
flags *uint32
dropped *uint32
array *uint32
sqes []ioSqringEntry
}
type ioUringCq struct {
head *uint32
tail *uint32
ringMask *uint32
ringEntries *uint32
overflow *uint32
cqes []ioCqringEntry
}
type ioSqringEntry struct {
opcode uint8
flags uint8
ioprio uint16
fd int32
off uint64
addr uint64
len uint32
userData uint64
}
type ioCqringEntry struct {
userData uint64
res int32
flags uint32
}
type ioUringParams struct {
sqEntries uint32
cqEntries uint32
flags uint32
sqThreadCPU uint32
sqThreadIdle uint32
features uint32
wqFd uint32
resv [3]uint32
sqOff ioSqringOffsets
cqOff ioCqringOffsets
}
type ioSqringOffsets struct {
head uint32
tail uint32
ringMask uint32
ringEntries uint32
flags uint32
dropped uint32
array uint32
resv1 uint32
resv2 uint64
}
type ioCqringOffsets struct {
head uint32
tail uint32
ringMask uint32
ringEntries uint32
overflow uint32
cqes uint32
flags uint32
resv1 uint32
resv2 uint64
}
type ioUringCqe struct {
userData uint64
res int32
flags uint32
}
var ioUringEnabled = false
func init() {
if isKernelVersionSupported() {
ioUringEnabled = true
scsi.RegisterBackingStore(IoUringBackingStorage, newIOUringBackingStore)
log.Info("io_uring backing store registered (kernel supports io_uring)")
} else {
log.Info("io_uring backing store not available (requires Linux 5.1+)")
}
}
func isKernelVersionSupported() bool {
var uname syscall.Utsname
if err := syscall.Uname(&uname); err != nil {
return false
}
// Parse kernel version (simplified)
// Format is typically "5.15.0-generic"
major := int(uname.Release[0] - '0')
minor := int(uname.Release[2] - '0')
if major > MinKernelMajor {
return true
}
if major == MinKernelMajor && minor >= MinKernelMinor {
return true
}
return false
}
// IOUringBackingStore implements BackingStore using io_uring
type IOUringBackingStore struct {
scsi.BaseBackingStore
file *os.File
ring *ioUring
queueDepth int
// Synchronization
submitMu sync.Mutex
// Statistics
opsSubmitted uint64
opsCompleted uint64
}
func newIOUringBackingStore() (api.BackingStore, error) {
return &IOUringBackingStore{
BaseBackingStore: scsi.BaseBackingStore{
Name: IoUringBackingStorage,
DataSize: 0,
OflagsSupported: 0,
},
queueDepth: DefaultQueueDepth,
}, nil
}
// Open opens the backing file and initializes io_uring
func (bs *IOUringBackingStore) Open(dev *api.SCSILu, path string) error {
var mode os.FileMode
finfo, err := os.Stat(path)
if err != nil {
return err
}
mode = finfo.Mode()
f, err := os.OpenFile(path, os.O_RDWR|syscall.O_DIRECT, os.ModePerm)
if err != nil {
// Try without O_DIRECT if not supported
f, err = os.OpenFile(path, os.O_RDWR, os.ModePerm)
if err != nil {
return err
}
}
if (mode & os.ModeDevice) != 0 {
pos, err := f.Seek(0, os.SEEK_END)
if err != nil {
f.Close()
return err
}
bs.DataSize = uint64(pos)
} else {
bs.DataSize = uint64(finfo.Size())
}
bs.file = f
// Initialize io_uring
ring, err := bs.initIOUring()
if err != nil {
f.Close()
return fmt.Errorf("failed to initialize io_uring: %v", err)
}
bs.ring = ring
log.Infof("io_uring backing store opened: %s (queue depth: %d)", path, bs.queueDepth)
return nil
}
func (bs *IOUringBackingStore) initIOUring() (*ioUring, error) {
params := &ioUringParams{}
// Setup io_uring
fd, _, errno := syscall.Syscall(425, // __NR_io_uring_setup
uintptr(bs.queueDepth),
uintptr(unsafe.Pointer(params)),
0)
if errno != 0 {
return nil, fmt.Errorf("io_uring_setup failed: %v", errno)
}
ring := &ioUring{
fd: int(fd),
ringSize: int(params.sqEntries),
flags: params.flags,
}
// Map the submission queue ring
sqRingSize := params.sqOff.array + params.sqEntries*uint32(unsafe.Sizeof(uint32(0)))
cqRingSize := params.cqOff.cqes + params.cqEntries*uint32(unsafe.Sizeof(ioCqringEntry{}))
if params.features&1 != 0 { // IORING_FEAT_SINGLE_MMAP
if cqRingSize > sqRingSize {
sqRingSize = cqRingSize
}
cqRingSize = sqRingSize
}
// mmap submission queue
sqPtr, _, errno := syscall.Syscall6(syscall.SYS_MMAP,
0,
uintptr(sqRingSize),
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_SHARED|syscall.MAP_POPULATE,
uintptr(fd),
uintptr(IORING_OFF_SQ_RING))
if errno != 0 {
syscall.Close(int(fd))
return nil, fmt.Errorf("mmap sq ring failed: %v", errno)
}
sqBase := sqPtr
// mmap completion queue (if not single mmap)
var cqPtr uintptr
if params.features&1 != 0 {
cqPtr = sqPtr
} else {
cqPtr, _, errno = syscall.Syscall6(syscall.SYS_MMAP,
0,
uintptr(cqRingSize),
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_SHARED|syscall.MAP_POPULATE,
uintptr(fd),
uintptr(IORING_OFF_CQ_RING))
if errno != 0 {
syscall.Syscall(syscall.SYS_MUNMAP, sqPtr, uintptr(sqRingSize), 0)
syscall.Close(int(fd))
return nil, fmt.Errorf("mmap cq ring failed: %v", errno)
}
}
cqBase := cqPtr
// mmap SQEs
sqeSize := uint32(unsafe.Sizeof(ioSqringEntry{}))
sqePtr, _, errno := syscall.Syscall6(syscall.SYS_MMAP,
0,
uintptr(uint32(bs.queueDepth)*sqeSize),
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_SHARED|syscall.MAP_POPULATE,
uintptr(fd),
uintptr(IORING_OFF_SQES))
if errno != 0 {
syscall.Syscall(syscall.SYS_MUNMAP, sqPtr, uintptr(sqRingSize), 0)
if cqPtr != sqPtr {
syscall.Syscall(syscall.SYS_MUNMAP, cqPtr, uintptr(cqRingSize), 0)
}
syscall.Close(int(fd))
return nil, fmt.Errorf("mmap sqes failed: %v", errno)
}
// Setup submission queue
sq := &ioUringSq{
head: (*uint32)(unsafe.Pointer(sqBase + uintptr(params.sqOff.head))),
tail: (*uint32)(unsafe.Pointer(sqBase + uintptr(params.sqOff.tail))),
ringMask: (*uint32)(unsafe.Pointer(sqBase + uintptr(params.sqOff.ringMask))),
ringEntries: (*uint32)(unsafe.Pointer(sqBase + uintptr(params.sqOff.ringEntries))),
flags: (*uint32)(unsafe.Pointer(sqBase + uintptr(params.sqOff.flags))),
dropped: (*uint32)(unsafe.Pointer(sqBase + uintptr(params.sqOff.dropped))),
array: (*uint32)(unsafe.Pointer(sqBase + uintptr(params.sqOff.array))),
sqes: make([]ioSqringEntry, bs.queueDepth),
}
copy(unsafe.Slice((*ioSqringEntry)(unsafe.Pointer(sqePtr)), bs.queueDepth), sq.sqes)
// Setup completion queue
cq := &ioUringCq{
head: (*uint32)(unsafe.Pointer(cqBase + uintptr(params.cqOff.head))),
tail: (*uint32)(unsafe.Pointer(cqBase + uintptr(params.cqOff.tail))),
ringMask: (*uint32)(unsafe.Pointer(cqBase + uintptr(params.cqOff.ringMask))),
ringEntries: (*uint32)(unsafe.Pointer(cqBase + uintptr(params.cqOff.ringEntries))),
overflow: (*uint32)(unsafe.Pointer(cqBase + uintptr(params.cqOff.overflow))),
cqes: make([]ioCqringEntry, params.cqEntries),
}
copy(unsafe.Slice((*ioCqringEntry)(unsafe.Pointer(cqBase+uintptr(params.cqOff.cqes))), params.cqEntries), cq.cqes)
ring.sq = sq
ring.cq = cq
return ring, nil
}
// Close closes the backing file and io_uring
func (bs *IOUringBackingStore) Close(dev *api.SCSILu) error {
if bs.ring != nil {
bs.closeIOUring()
bs.ring = nil
}
if bs.file != nil {
return bs.file.Close()
}
return nil
}
func (bs *IOUringBackingStore) closeIOUring() {
if bs.ring != nil && bs.ring.fd >= 0 {
syscall.Close(bs.ring.fd)
}
}
// Init initializes the backing store
func (bs *IOUringBackingStore) Init(dev *api.SCSILu, Opts string) error {
return nil
}
// Exit exits the backing store
func (bs *IOUringBackingStore) Exit(dev *api.SCSILu) error {
return nil
}
// Size returns the size of the backing store
func (bs *IOUringBackingStore) Size(dev *api.SCSILu) uint64 {
return bs.DataSize
}
// Read reads data from the backing file using io_uring
func (bs *IOUringBackingStore) Read(offset, tl int64) ([]byte, error) {
if bs.file == nil {
return nil, fmt.Errorf("backing store is not open")
}
buf := make([]byte, tl)
// Prepare read operation
bs.submitMu.Lock()
defer bs.submitMu.Unlock()
// Get next SQE
sqe := bs.getSqe()
if sqe == nil {
// Ring is full, submit pending operations first
if err := bs.submit(); err != nil {
return nil, err
}
sqe = bs.getSqe()
if sqe == nil {
return nil, fmt.Errorf("io_uring queue full")
}
}
// Setup read operation
*sqe = ioSqringEntry{
opcode: IORING_OP_READ,
fd: int32(bs.file.Fd()),
off: uint64(offset),
addr: uint64(uintptr(unsafe.Pointer(&buf[0]))),
len: uint32(tl),
userData: 1, // 1 = read operation
}
// Submit and wait for completion
if err := bs.submitAndWait(1); err != nil {
return nil, err
}
// Get completion
cqe, err := bs.getCqe()
if err != nil {
return nil, err
}
if cqe.res < 0 {
return nil, fmt.Errorf("read failed: %d", cqe.res)
}
atomic.AddUint64(&bs.opsCompleted, 1)
return buf[:cqe.res], nil
}
// Write writes data to the backing file using io_uring
func (bs *IOUringBackingStore) Write(wbuf []byte, offset int64) error {
if bs.file == nil {
return fmt.Errorf("backing store is not open")
}
bs.submitMu.Lock()
defer bs.submitMu.Unlock()
// Get next SQE
sqe := bs.getSqe()
if sqe == nil {
if err := bs.submit(); err != nil {
return err
}
sqe = bs.getSqe()
if sqe == nil {
return fmt.Errorf("io_uring queue full")
}
}
// Setup write operation
*sqe = ioSqringEntry{
opcode: IORING_OP_WRITE,
fd: int32(bs.file.Fd()),
off: uint64(offset),
addr: uint64(uintptr(unsafe.Pointer(&wbuf[0]))),
len: uint32(len(wbuf)),
userData: 2, // 2 = write operation
}
// Submit and wait for completion
if err := bs.submitAndWait(1); err != nil {
return err
}
// Get completion
cqe, err := bs.getCqe()
if err != nil {
return err
}
if cqe.res < 0 {
return fmt.Errorf("write failed: %d", cqe.res)
}
if cqe.res != int32(len(wbuf)) {
return fmt.Errorf("short write: %d != %d", cqe.res, len(wbuf))
}
atomic.AddUint64(&bs.opsCompleted, 1)
return nil
}
// DataSync syncs data to disk using io_uring
func (bs *IOUringBackingStore) DataSync(offset, tl int64) error {
if bs.file == nil {
return fmt.Errorf("backing store is not open")
}
bs.submitMu.Lock()
defer bs.submitMu.Unlock()
sqe := bs.getSqe()
if sqe == nil {
if err := bs.submit(); err != nil {
return err
}
sqe = bs.getSqe()
if sqe == nil {
return fmt.Errorf("io_uring queue full")
}
}
*sqe = ioSqringEntry{
opcode: IORING_OP_FSYNC,
fd: int32(bs.file.Fd()),
len: IORING_FSYNC_DATASYNC,
userData: 3, // 3 = fsync operation
}
if err := bs.submitAndWait(1); err != nil {
return err
}
cqe, err := bs.getCqe()
if err != nil {
return err
}
if cqe.res < 0 {
return fmt.Errorf("fsync failed: %d", cqe.res)
}
atomic.AddUint64(&bs.opsCompleted, 1)
return nil
}
// DataAdvise provides advice about data access patterns
func (bs *IOUringBackingStore) DataAdvise(offset, length int64, advise uint32) error {
if bs.file == nil {
return fmt.Errorf("backing store is not open")
}
// Use posix_fadvise via syscall
_, _, errno := syscall.Syscall6(syscall.SYS_FADVISE64, uintptr(bs.file.Fd()), uintptr(offset), uintptr(length), uintptr(advise), 0, 0)
if errno != 0 {
return errno
}
return nil
}
// Unmap is a no-op for file-based storage
func (bs *IOUringBackingStore) Unmap([]api.UnmapBlockDescriptor) error {
return nil
}
// getSqe gets the next available submission queue entry
func (bs *IOUringBackingStore) getSqe() *ioSqringEntry {
sq := bs.ring.sq
tail := atomic.LoadUint32(sq.tail)
next := tail + 1
if next-atomic.LoadUint32(sq.head) > uint32(bs.ring.ringSize) {
return nil // Queue is full
}
idx := tail & *sq.ringMask
return &sq.sqes[idx]
}
// submit submits pending SQEs to the kernel
func (bs *IOUringBackingStore) submit() error {
if bs.ring == nil {
return fmt.Errorf("io_uring not initialized")
}
// Update tail
atomic.StoreUint32(bs.ring.sq.tail, atomic.LoadUint32(bs.ring.sq.tail)+1)
// Submit using io_uring_enter syscall
_, _, errno := syscall.Syscall6(426, // __NR_io_uring_enter
uintptr(bs.ring.fd),
uintptr(1), // submit 1 operation
0, // min complete
0, // flags
0, 0)
if errno != 0 {
return fmt.Errorf("io_uring_enter failed: %v", errno)
}
atomic.AddUint64(&bs.opsSubmitted, 1)
return nil
}
// submitAndWait submits operations and waits for completions
func (bs *IOUringBackingStore) submitAndWait(minComplete uint32) error {
if bs.ring == nil {
return fmt.Errorf("io_uring not initialized")
}
// Update tail
atomic.StoreUint32(bs.ring.sq.tail, atomic.LoadUint32(bs.ring.sq.tail)+1)
// Submit and wait
_, _, errno := syscall.Syscall6(426, // __NR_io_uring_enter
uintptr(bs.ring.fd),
uintptr(1), // submit 1 operation
uintptr(minComplete), // min complete
0, // flags
0, 0)
if errno != 0 {
return fmt.Errorf("io_uring_enter failed: %v", errno)
}
return nil
}
// getCqe gets a completion queue entry
func (bs *IOUringBackingStore) getCqe() (*ioCqringEntry, error) {
cq := bs.ring.cq
// Wait for completion
for atomic.LoadUint32(cq.head) == atomic.LoadUint32(cq.tail) {
// Spin-wait for completion
runtime.Gosched()
}
head := atomic.LoadUint32(cq.head)
idx := head & *cq.ringMask
cqe := &cq.cqes[idx]
// Update head
atomic.StoreUint32(cq.head, head+1)
return cqe, nil
}
// Stats returns io_uring statistics
func (bs *IOUringBackingStore) Stats() (submitted, completed uint64) {
return atomic.LoadUint64(&bs.opsSubmitted), atomic.LoadUint64(&bs.opsCompleted)
}
// Available returns true if io_uring is available on this system
func Available() bool {
return ioUringEnabled
}

View File

@@ -0,0 +1,33 @@
//go:build !linux
// +build !linux
/*
Copyright 2024 The GoStor Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package iouring
import (
// io_uring is not available on non-Linux platforms
)
func init() {
// io_uring is not available on non-Linux platforms
}
// Available returns false on non-Linux platforms
func Available() bool {
return false
}

View File

@@ -249,21 +249,60 @@ func SCSICDBBufXLength(scb []byte) (int64, bool) {
opcode = scb[0]
group = SCSICDBGroupID(opcode)
// Note: group is 0-7, not the CDB length (6, 10, 12, 16)
switch group {
case CDB_GROUPID_0:
length = int64(scb[4])
case CDB_GROUPID_2:
length = int64(util.GetUnalignedUint16(scb[7:9]))
case CDB_GROUPID_3:
case 0: // GROUPID_0: 6-byte commands
// INQUIRY (0x12) and REQUEST_SENSE (0x03) have Allocation Length in bytes 3-4
if opcode == 0x12 || opcode == 0x03 {
length = int64(util.GetUnalignedUint16(scb[3:5]))
} else {
// For other Group 0 commands (READ_6, WRITE_6, etc.),
// byte 4 is typically Transfer Length, not Allocation Length.
// We should not use it to limit sense data buffer.
ok = false
}
case 1, 2: // GROUPID_1, GROUPID_2: 10-byte commands
// PERSISTENT_RESERVE_IN (0x5E) and PERSISTENT_RESERVE_OUT (0x5F)
// have Allocation Length in bytes 6-7, not 7-8
if opcode == 0x5E || opcode == 0x5F {
// Manual BigEndian conversion for PRIN/PROUT
length = int64(uint16(scb[6])<<8 | uint16(scb[7]))
} else if opcode == 0x28 || opcode == 0x2A || opcode == 0x2E || opcode == 0x35 ||
opcode == 0x34 || opcode == 0x2F || opcode == 0x41 || opcode == 0x55 ||
opcode == 0x5A || opcode == 0x56 || opcode == 0x57 {
// READ_10(0x28), WRITE_10(0x2A), WRITE_VERIFY(0x2E), SYNCHRONIZE_CACHE(0x35),
// PRE_FETCH_10(0x34), VERIFY_10(0x2F), WRITE_SAME(0x41), MODE_SELECT_10(0x55),
// MODE_SENSE_10(0x5A), RESERVE_10(0x56), RELEASE_10(0x57)
// These commands have Transfer Length or Parameter List Length in bytes 7-8,
// not Allocation Length.
ok = false
} else {
length = int64(util.GetUnalignedUint16(scb[7:9]))
}
case 3: // GROUPID_3: variable length
if opcode == 0x7F {
length = int64(scb[7])
} else {
ok = false
}
case CDB_GROUPID_4:
length = int64(util.GetUnalignedUint32(scb[6:10]))
case CDB_GROUPID_5:
length = int64(util.GetUnalignedUint32(scb[10:14]))
case 4: // GROUPID_4: 16-byte commands
// READ_16(0x88), WRITE_16(0x8A), WRITE_VERIFY_16(0x8E), SYNCHRONIZE_CACHE_16(0x91),
// PRE_FETCH_16(0x90), VERIFY_16(0x8F), WRITE_SAME_16(0x93), ORWRITE_16(0x8B)
if opcode == 0x88 || opcode == 0x8A || opcode == 0x8E || opcode == 0x91 ||
opcode == 0x90 || opcode == 0x8F || opcode == 0x93 || opcode == 0x8B {
// These commands have Transfer Length in bytes 6-9, not Allocation Length
ok = false
} else {
length = int64(util.GetUnalignedUint32(scb[6:10]))
}
case 5: // GROUPID_5: 12-byte commands
// READ_12(0xA8), WRITE_12(0xAA), WRITE_VERIFY_12(0xAE), VERIFY_12(0xAF)
if opcode == 0xA8 || opcode == 0xAA || opcode == 0xAE || opcode == 0xAF {
// These commands have Transfer Length in bytes 10-13, not Allocation Length
ok = false
} else {
length = int64(util.GetUnalignedUint32(scb[10:14]))
}
default:
ok = false
}

View File

@@ -30,8 +30,30 @@ func NewSCSILu(bs *config.BackendStorage) (*api.SCSILu, error) {
if len(pathinfo) < 2 {
return nil, errors.New("invalid device path string")
}
backendType := pathinfo[0]
backendPath := pathinfo[1]
// Determine backend type: config.BackendType > path prefix > default (file)
backendType := "file"
backendPath := bs.Path
if bs.BackendType != "" {
// Config specifies backend type explicitly
backendType = bs.BackendType
backendPath = pathinfo[1]
} else {
// Infer from path prefix
backendType = pathinfo[0]
backendPath = pathinfo[1]
// Validate backend type, default to file if unknown
switch backendType {
case "file", "iouring", "ceph", "null", "RemBs":
// Valid types
default:
// Unknown type, treat entire path as file path
backendType = "file"
backendPath = bs.Path
}
}
sbc := NewSBCDevice(api.TYPE_DISK)
backing, err := NewBackingStore(backendType)
@@ -53,7 +75,7 @@ func NewSCSILu(bs *config.BackendStorage) (*api.SCSILu, error) {
}
lu.Size = backing.Size(lu)
lu.DeviceProtocol.InitLu(lu)
lu.Attrs.ThinProvisioning = bs.ThinProvisioning
lu.Attrs.ThinProvisioning = true
lu.Attrs.Online = bs.Online
lu.Attrs.Lbppbe = 3
return lu, nil

View File

@@ -18,6 +18,7 @@ limitations under the License.
package scsi
import (
"bytes"
"encoding/binary"
"fmt"
"unsafe"
@@ -105,17 +106,17 @@ func (sbc SBCSCSIDeviceProtocol) InitLu(lu *api.SCSILu) error {
// Vendor uniq - However most apps seem to call for mode page 0
//pages = append(pages, api.ModePage{0, 0, []byte{}})
// Disconnect page
pages = append(pages, api.ModePage{2, 0, 14, []byte{0x80, 0x80, 0, 0xa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}})
pages = append(pages, api.ModePage{PageCode: 2, SubPageCode: 0, Size: 14, Data: []byte{0x80, 0x80, 0, 0xa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}})
// Caching Page
pages = append(pages, api.ModePage{8, 0, 18, []byte{0x14, 0, 0xff, 0xff, 0, 0, 0xff, 0xff, 0xff, 0xff, 0x80, 0x14, 0, 0, 0, 0, 0, 0}})
pages = append(pages, api.ModePage{PageCode: 8, SubPageCode: 0, Size: 18, Data: []byte{0x14, 0, 0xff, 0xff, 0, 0, 0xff, 0xff, 0xff, 0xff, 0x80, 0x14, 0, 0, 0, 0, 0, 0}})
// Control page
pages = append(pages, api.ModePage{0x0a, 0, 10, []byte{2, 0x10, 0, 0, 0, 0, 0, 0, 2, 0, 0x08, 0, 0, 0, 0, 0, 0, 0}})
pages = append(pages, api.ModePage{PageCode: 0x0a, SubPageCode: 0, Size: 10, Data: []byte{2, 0x10, 0, 0, 0, 0, 0, 0, 2, 0, 0x08, 0, 0, 0, 0, 0, 0, 0}})
// Control Extensions mode page: TCMOS:1
pages = append(pages, api.ModePage{0x0a, 1, 0x1c, []byte{0x04, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}})
pages = append(pages, api.ModePage{PageCode: 0x0a, SubPageCode: 1, Size: 0x1c, Data: []byte{0x04, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}})
// Informational Exceptions Control page
pages = append(pages, api.ModePage{0x1c, 0, 10, []byte{8, 0, 0, 0, 0, 0, 0, 0, 0, 0}})
pages = append(pages, api.ModePage{PageCode: 0x1c, SubPageCode: 0, Size: 10, Data: []byte{8, 0, 0, 0, 0, 0, 0, 0, 0, 0}})
lu.ModePages = pages
mbd := util.MarshalUint32(uint32(0xffffffff))
if size := lu.Size >> lu.BlockShift; size>>32 == 0 {
@@ -221,6 +222,7 @@ func NewSBCDevice(deviceType api.SCSIDeviceType) api.SCSIDeviceProtocol {
sbc.SCSIDeviceOps[api.WRITE_12] = NewSCSIDeviceOperation(SBCReadWrite, nil, PR_WE_FA|PR_EA_FA|PR_WE_FA|PR_WE_FN)
sbc.SCSIDeviceOps[api.WRITE_VERIFY_12] = NewSCSIDeviceOperation(SBCReadWrite, nil, PR_EA_FA|PR_EA_FN)
sbc.SCSIDeviceOps[api.VERIFY_12] = NewSCSIDeviceOperation(SBCVerify, nil, PR_EA_FA|PR_EA_FN)
sbc.SCSIDeviceOps[api.COMPARE_AND_WRITE] = NewSCSIDeviceOperation(SBCCompareAndWrite, nil, PR_EA_FA|PR_EA_FN)
return sbc
}
@@ -354,15 +356,17 @@ func SBCUnmap(host int, cmd *api.SCSICommand) api.SAMStat {
*/
func SBCReadWrite(host int, cmd *api.SCSICommand) api.SAMStat {
var (
key = ILLEGAL_REQUEST
asc = ASC_INVALID_FIELD_IN_CDB
dev = cmd.Device
scb = cmd.SCB
opcode = api.SCSICommandType(scb[0])
lba uint64
tl uint32
err error
key = ILLEGAL_REQUEST
asc = ASC_INVALID_FIELD_IN_CDB
dev = cmd.Device
scb = cmd.SCB
opcode = api.SCSICommandType(scb[0])
lba uint64
tl uint32
err error
totalBlocks uint64
)
if dev.Attrs.Removable && !dev.Attrs.Online {
key = NOT_READY
asc = ASC_MEDIUM_NOT_PRESENT
@@ -422,21 +426,22 @@ func SBCReadWrite(host int, cmd *api.SCSICommand) api.SAMStat {
lba = getSCSIReadWriteOffset(scb)
tl = getSCSIReadWriteCount(scb)
// Calculate total blocks
totalBlocks = dev.Size >> dev.BlockShift
log.Debugf("SBCReadWrite: opcode=0x%x, lba=%d, tl=%d, totalBlocks=%d", opcode, lba, tl, totalBlocks)
// Verify that we are not doing i/o beyond the end-of-lun
if tl != 0 {
if lba+uint64(tl) < lba || lba+uint64(tl) > dev.Size>>dev.BlockShift {
key = ILLEGAL_REQUEST
asc = ASC_LBA_OUT_OF_RANGE
log.Warnf("sense data(ILLEGAL_REQUEST,ASC_LBA_OUT_OF_RANGE) encounter: lba: %d, tl: %d, size: %d", lba, tl, dev.Size>>dev.BlockShift)
goto sense
}
} else {
if lba >= dev.Size>>dev.BlockShift {
key = ILLEGAL_REQUEST
asc = ASC_LBA_OUT_OF_RANGE
log.Warnf("sense data(ILLEGAL_REQUEST,ASC_LBA_OUT_OF_RANGE) encounter: lba: %d, size: %d", lba, dev.Size>>dev.BlockShift)
goto sense
}
// Even when transfer length is 0, we must validate the LBA is within range
if lba >= totalBlocks || lba+uint64(tl) < lba || lba+uint64(tl) > totalBlocks {
key = ILLEGAL_REQUEST
asc = ASC_LBA_OUT_OF_RANGE
log.Warnf("SBCReadWrite: LBA out of range (lba=%d, tl=%d, totalBlocks=%d)", lba, tl, totalBlocks)
goto sense
}
// If transfer length is 0, return GOOD status immediately (no data to transfer)
if tl == 0 {
return api.SAMStatGood
}
cmd.Offset = lba << dev.BlockShift
@@ -495,6 +500,120 @@ func SBCRelease(host int, cmd *api.SCSICommand) api.SAMStat {
return api.SAMStatGood
}
/*
* SBCCompareAndWrite Implements SCSI COMPARE AND WRITE command (0x89)
* The COMPARE AND WRITE command requests that the device server compare the specified
* logical block(s) with data transferred from the data-out buffer and, if they match,
* write the new data from the data-out buffer to the specified logical block(s).
*
* Reference : SBC3r35
* 5.3 - COMPARE AND WRITE
*/
func SBCCompareAndWrite(host int, cmd *api.SCSICommand) api.SAMStat {
var (
key = ILLEGAL_REQUEST
asc = ASC_INVALID_FIELD_IN_CDB
dev = cmd.Device
scb = cmd.SCB
lba uint64
numBlocks uint32
offset uint64
blockSize uint64
totalCompareLen uint64
expectedDataLen uint64
err error
existingData []byte
compareData []byte
writeData []byte
)
if dev.Attrs.Removable && !dev.Attrs.Online {
key = NOT_READY
asc = ASC_MEDIUM_NOT_PRESENT
goto sense
}
// We only support protection information type 0
if scb[1]&0xe0 != 0 {
key = ILLEGAL_REQUEST
asc = ASC_INVALID_FIELD_IN_CDB
goto sense
}
if dev.Attrs.Readonly || dev.Attrs.SWP {
key = DATA_PROTECT
asc = ASC_WRITE_PROTECT
goto sense
}
// Number of logical blocks (one byte: bits 0-7)
numBlocks = uint32(scb[13])
if numBlocks == 0 {
key = ILLEGAL_REQUEST
asc = ASC_INVALID_FIELD_IN_CDB
goto sense
}
lba = getSCSIReadWriteOffset(scb)
// Verify that we are not doing i/o beyond the end-of-lun
if lba+uint64(numBlocks) < lba || lba+uint64(numBlocks) > dev.Size>>dev.BlockShift {
key = ILLEGAL_REQUEST
asc = ASC_LBA_OUT_OF_RANGE
log.Warnf("COMPARE_AND_WRITE: lba out of range: lba: %d, num: %d, size: %d", lba, numBlocks, dev.Size>>dev.BlockShift)
goto sense
}
offset = lba << dev.BlockShift
blockSize = uint64(1 << dev.BlockShift)
totalCompareLen = uint64(numBlocks) * blockSize
// Data-out buffer contains: compare data followed by write data
// Total length should be 2 * numBlocks * blockSize
expectedDataLen = 2 * totalCompareLen
if uint64(cmd.OutSDBBuffer.Length) < expectedDataLen {
key = ILLEGAL_REQUEST
asc = ASC_INVALID_FIELD_IN_CDB
log.Warnf("COMPARE_AND_WRITE: data length too short: got %d, expected %d", cmd.OutSDBBuffer.Length, expectedDataLen)
goto sense
}
compareData = cmd.OutSDBBuffer.Buffer[:totalCompareLen]
writeData = cmd.OutSDBBuffer.Buffer[totalCompareLen:expectedDataLen]
// Read existing data from storage
existingData, err = dev.Storage.Read(int64(offset), int64(totalCompareLen))
if err != nil {
log.Errorf("COMPARE_AND_WRITE: failed to read data: %v", err)
key = MEDIUM_ERROR
asc = ASC_READ_ERROR
goto sense
}
// Compare data
if !bytes.Equal(existingData, compareData) {
key = MISCOMPARE
asc = ASC_MISCOMPARE_DURING_VERIFY_OPERATION
log.Warnf("COMPARE_AND_WRITE: data miscompare at LBA %d", lba)
goto sense
}
// Data matches, write new data
err = dev.Storage.Write(writeData, int64(offset))
if err != nil {
log.Errorf("COMPARE_AND_WRITE: failed to write data: %v", err)
key = MEDIUM_ERROR
asc = ASC_WRITE_ERROR
goto sense
}
return api.SAMStatGood
sense:
BuildSenseData(cmd, key, asc)
return api.SAMStatCheckCondition
}
/*
* SBCReadCapacity Implements SCSI READ CAPACITY(10) command
* The READ CAPACITY (10) command requests that the device server transfer 8 bytes of parameter data
@@ -558,13 +677,14 @@ sense:
*/
func SBCVerify(host int, cmd *api.SCSICommand) api.SAMStat {
var (
key = ILLEGAL_REQUEST
asc = ASC_INVALID_FIELD_IN_CDB
dev = cmd.Device
scb = cmd.SCB
lba uint64
tl uint32
err error
key = ILLEGAL_REQUEST
asc = ASC_INVALID_FIELD_IN_CDB
dev = cmd.Device
scb = cmd.SCB
lba uint64
tl uint32
err error
totalBlocks uint64
)
if dev.Attrs.Removable && !dev.Attrs.Online {
key = NOT_READY
@@ -579,28 +699,21 @@ func SBCVerify(host int, cmd *api.SCSICommand) api.SAMStat {
goto sense
}
if scb[1]&0x02 == 0 {
// no data compare with the media
return api.SAMStatGood
}
lba = getSCSIReadWriteOffset(scb)
tl = getSCSIReadWriteCount(scb)
// Verify that we are not doing i/o beyond the end-of-lun
if tl != 0 {
if lba+uint64(tl) < lba || lba+uint64(tl) > dev.Size>>dev.BlockShift {
key = ILLEGAL_REQUEST
asc = ASC_LBA_OUT_OF_RANGE
log.Warnf("sense: lba: %d, tl: %d, size: %d", lba, tl, dev.Size>>dev.BlockShift)
goto sense
}
} else {
if lba >= dev.Size>>dev.BlockShift {
key = ILLEGAL_REQUEST
asc = ASC_LBA_OUT_OF_RANGE
log.Warnf("sense")
goto sense
}
// Must check LBA range before BYTCHK early return per SBC spec
totalBlocks = dev.Size >> dev.BlockShift
if lba >= totalBlocks || lba+uint64(tl) < lba || lba+uint64(tl) > totalBlocks {
key = ILLEGAL_REQUEST
asc = ASC_LBA_OUT_OF_RANGE
goto sense
}
if scb[1]&0x02 == 0 {
// BYTCHK=0: no data compare with the media
return api.SAMStatGood
}
cmd.Offset = lba << dev.BlockShift
@@ -647,12 +760,13 @@ func SBCReadCapacity16(host int, cmd *api.SCSICommand) api.SAMStat {
func SBCGetLbaStatus(host int, cmd *api.SCSICommand) api.SAMStat {
var (
key = ILLEGAL_REQUEST
asc = ASC_INVALID_FIELD_IN_CDB
dev = cmd.Device
scb = cmd.SCB
lba uint64
tl uint32
key = ILLEGAL_REQUEST
asc = ASC_INVALID_FIELD_IN_CDB
dev = cmd.Device
scb = cmd.SCB
lba uint64
tl uint32
totalBlocks uint64
)
if dev.Attrs.Removable && !dev.Attrs.Online {
key = NOT_READY
@@ -674,20 +788,13 @@ func SBCGetLbaStatus(host int, cmd *api.SCSICommand) api.SAMStat {
lba = getSCSIReadWriteOffset(scb)
tl = getSCSIReadWriteCount(scb)
// Verify that we are not doing i/o beyond the end-of-lun
if tl != 0 {
if lba+uint64(tl) < lba || lba+uint64(tl) > dev.Size>>dev.BlockShift {
key = ILLEGAL_REQUEST
asc = ASC_LBA_OUT_OF_RANGE
log.Warnf("sense: lba: %d, tl: %d, size: %d", lba, tl, dev.Size>>dev.BlockShift)
goto sense
}
} else {
if lba >= dev.Size>>dev.BlockShift {
key = ILLEGAL_REQUEST
asc = ASC_LBA_OUT_OF_RANGE
log.Warnf("sense")
goto sense
}
totalBlocks = dev.Size >> dev.BlockShift
log.Warnf("DEBUG: dev.Size=%d, BlockShift=%d, totalBlocks=%d", dev.Size, dev.BlockShift, totalBlocks)
if lba >= totalBlocks || lba+uint64(tl) < lba || lba+uint64(tl) > totalBlocks {
key = ILLEGAL_REQUEST
asc = ASC_LBA_OUT_OF_RANGE
log.Warnf("sense: lba: %d, tl: %d, totalBlocks: %d", lba, tl, totalBlocks)
goto sense
}
return api.SAMStatGood
sense:

View File

@@ -24,7 +24,6 @@ import (
"unsafe"
"github.com/gostor/gotgt/pkg/api"
uuid "github.com/satori/go.uuid"
log "github.com/sirupsen/logrus"
)
@@ -46,13 +45,13 @@ func NewSCSITargetService() *SCSITargetService {
}
// GetTargetList get SCSI target list
func (s *SCSITargetService) GetTargetList() ([]api.SCSITarget, error) {
result := []api.SCSITarget{}
func (s *SCSITargetService) GetTargetList() ([]*api.SCSITarget, error) {
result := []*api.SCSITarget{}
s.mutex.RLock()
defer s.mutex.RUnlock()
for _, t := range s.Targets {
result = append(result, *t)
result = append(result, t)
}
s.mutex.RUnlock()
return result, nil
}
@@ -91,7 +90,7 @@ func (s *SCSITargetService) AddCommandQueue(tid int, scmd *api.SCSICommand) erro
}
scmd.Target = target
for _, it := range target.ITNexus {
if uuid.Equal(it.ID, scmd.ITNexusID) {
if it.ID == scmd.ITNexusID {
itn = it
break
}
@@ -199,8 +198,9 @@ func BuildSenseData(cmd *api.SCSICommand, key byte, asc SCSISubError) {
} else {
log.Debugf("cannot calc cbd alloc length. truncate failed")
}
cmd.Result = key
cmd.SenseBuffer = &api.SenseBuffer{senseBuffer.Bytes(), length}
// Note: cmd.Result should be set by the caller, not here
// The caller should set cmd.Result = api.SAM_STAT_CHECK_CONDITION when returning error
cmd.SenseBuffer = &api.SenseBuffer{Buffer: senseBuffer.Bytes(), Length: length}
}
func getSCSIReadWriteOffset(scb []byte) uint64 {
@@ -234,6 +234,8 @@ func getSCSIReadWriteCount(scb []byte) uint32 {
cnt = uint32(scb[7])<<8 | uint32(scb[8])
case api.READ_12, api.WRITE_12, api.VERIFY_12, api.WRITE_VERIFY_12:
cnt = binary.BigEndian.Uint32(scb[6:])
// Note: READ(12)/WRITE(12) have 32-bit transfer length field, but only use lower 16 bits
// per SCSI SBC-3 spec. Zero means zero blocks.
case api.READ_16, api.PRE_FETCH_16, api.WRITE_16, api.ORWRITE_16, api.VERIFY_16, api.WRITE_VERIFY_16, api.WRITE_SAME_16, api.SYNCHRONIZE_CACHE_16:
cnt = binary.BigEndian.Uint32(scb[10:])
case api.COMPARE_AND_WRITE:

156
pkg/scsi/scsi_perf_test.go Normal file
View File

@@ -0,0 +1,156 @@
/*
Copyright 2024 The GoStor Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package scsi
import (
"testing"
"github.com/gostor/gotgt/pkg/api"
)
// BenchmarkBuildSenseData benchmarks Sense Data building performance
func BenchmarkBuildSenseData(b *testing.B) {
cmd := &api.SCSICommand{
SCB: []byte{0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00},
Device: &api.SCSILu{
Attrs: api.SCSILuPhyAttribute{
SenseFormat: false, // Fixed format
},
},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
BuildSenseData(cmd, ILLEGAL_REQUEST, ASC_INVALID_FIELD_IN_CDB)
}
}
// BenchmarkBuildSenseDataDescriptor benchmarks Descriptor Format Sense Data building performance
func BenchmarkBuildSenseDataDescriptor(b *testing.B) {
cmd := &api.SCSICommand{
SCB: []byte{0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00},
Device: &api.SCSILu{
Attrs: api.SCSILuPhyAttribute{
SenseFormat: true, // Descriptor format
},
},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
BuildSenseData(cmd, ILLEGAL_REQUEST, ASC_INVALID_FIELD_IN_CDB)
}
}
// BenchmarkGetSCSIReadWriteOffset benchmarks offset calculation performance
func BenchmarkGetSCSIReadWriteOffset(b *testing.B) {
scb := []byte{0x28, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 0x00} // READ_10 at LBA 0x1000
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = getSCSIReadWriteOffset(scb)
}
}
// BenchmarkGetSCSIReadWriteCount benchmarks block count calculation performance
func BenchmarkGetSCSIReadWriteCount(b *testing.B) {
scb := []byte{0x28, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 0x00} // READ_10, 8 blocks
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = getSCSIReadWriteCount(scb)
}
}
// BenchmarkSCSIDeviceOperation benchmarks SCSI device operation lookup performance
func BenchmarkSCSIDeviceOperation(b *testing.B) {
lu := &api.SCSILu{}
deviceType := api.TYPE_DISK
sbc := NewSBCDevice(deviceType)
sbc.InitLu(lu)
opcodes := []api.SCSICommandType{
api.INQUIRY, // Must be implemented
api.READ_CAPACITY, // Must be implemented
api.MODE_SENSE, // Must be implemented
api.TEST_UNIT_READY, // Must be implemented
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
opcode := opcodes[i%len(opcodes)]
_ = sbc.PerformCommand(int(opcode))
}
}
// BenchmarkSCSICommandAlloc benchmarks SCSI command allocation performance
func BenchmarkSCSICommandAlloc(b *testing.B) {
b.Run("WithAllocation", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = &api.SCSICommand{
OpCode: byte(i % 256),
SCB: make([]byte, 16),
}
}
})
b.Run("Reuse", func(b *testing.B) {
b.ReportAllocs()
cmd := &api.SCSICommand{
SCB: make([]byte, 16),
}
for i := 0; i < b.N; i++ {
cmd.OpCode = byte(i % 256)
cmd.Result = 0
}
})
}
// BenchmarkSCSICommandTypeSwitch benchmarks SCSI command type switching performance
func BenchmarkSCSICommandTypeSwitch(b *testing.B) {
opcodes := []api.SCSICommandType{
api.READ_6, api.READ_10, api.READ_12, api.READ_16,
api.WRITE_6, api.WRITE_10, api.WRITE_12, api.WRITE_16,
api.INQUIRY, api.READ_CAPACITY, api.MODE_SENSE,
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
opcode := opcodes[i%len(opcodes)]
switch opcode {
case api.READ_6, api.READ_10, api.READ_12, api.READ_16:
// Read operation
case api.WRITE_6, api.WRITE_10, api.WRITE_12, api.WRITE_16:
// Write operation
case api.INQUIRY:
// Inquiry
case api.READ_CAPACITY:
// Read capacity
case api.MODE_SENSE:
// Mode sense
default:
// Unknown
}
}
}

View File

@@ -17,8 +17,8 @@ limitations under the License.
package scsi
import (
"github.com/google/uuid"
"github.com/gostor/gotgt/pkg/api"
"github.com/satori/go.uuid"
)
type SCSIReservationOperator interface {
@@ -101,7 +101,7 @@ func (op *SCSISimpleReservationOperator) GetReservation(tgtName string, devUUID
return nil
}
for _, SCSIRes = range LURes.Reservations {
if uuid.Equal(SCSIRes.ITNexusID, ITNexusID) {
if SCSIRes.ITNexusID == ITNexusID {
return SCSIRes
}
}

View File

@@ -22,9 +22,9 @@ import (
"encoding/binary"
"fmt"
"github.com/google/uuid"
"github.com/gostor/gotgt/pkg/api"
"github.com/gostor/gotgt/pkg/util"
uuid "github.com/satori/go.uuid"
log "github.com/sirupsen/logrus"
)
@@ -228,6 +228,33 @@ func InquiryPage0xB0(host int, cmd *api.SCSICommand) (*bytes.Buffer, uint16) {
return buf, pageLength
}
func InquiryPage0xB1(host int, cmd *api.SCSICommand) (*bytes.Buffer, uint16) {
var (
buf = &bytes.Buffer{}
pageLength uint16 = 0x3C // 60 bytes
)
//byte 0
if cmd.Device.Attrs.Online {
buf.WriteByte(PQ_DEVICE_CONNECTED | byte(cmd.Device.Attrs.DeviceType))
} else {
buf.WriteByte(PQ_DEVICE_NOT_CONNECT | byte(cmd.Device.Attrs.DeviceType))
}
//PAGE CODE
buf.WriteByte(0xB1)
//PAGE LENGTH
binary.Write(buf, binary.BigEndian, pageLength)
// MEDIA ROTATION RATE (bytes 4-5)
// 0x0001 = Non-rotating medium (SSD)
binary.Write(buf, binary.BigEndian, uint16(0x0001))
// Reserved bytes (6-63)
buf.Write(make([]byte, 58))
return buf, pageLength
}
func InquiryPage0xB2(host int, cmd *api.SCSICommand) (*bytes.Buffer, uint16) {
var (
buf = &bytes.Buffer{}
@@ -311,6 +338,8 @@ func SPCInquiry(host int, cmd *api.SCSICommand) api.SAMStat {
buf, _ = InquiryPage0x83(host, cmd)
case 0xB0:
buf, _ = InquiryPage0xB0(host, cmd)
case 0xB1:
buf, _ = InquiryPage0xB1(host, cmd)
case 0xB2:
buf, _ = InquiryPage0xB2(host, cmd)
default:
@@ -565,7 +594,6 @@ func SPCModeSense(host int, cmd *api.SCSICommand) api.SAMStat {
asc = ASC_INVALID_FIELD_IN_CDB
data []byte
allocLen uint32
i uint32
)
if dbd == 0 {
@@ -577,16 +605,31 @@ func SPCModeSense(host int, cmd *api.SCSICommand) api.SAMStat {
}
if mode6 {
allocLen = uint32(scb[4])
// set header
for i = 0; i < 4 && i < allocLen; i++ {
data = append(data, 0x00)
}
// set header (4 bytes)
// byte 0: Mode Data Length
// byte 1: Medium Type
// byte 2: Device-Specific Parameter (DPOFUA=bit4)
// byte 3: Block Descriptor Length
data = append(data, 0x00) // Mode Data Length (filled later)
data = append(data, 0x00) // Medium Type
data = append(data, 0x10) // Device-Specific Parameter (DPOFUA=1)
data = append(data, 0x00) // Block Descriptor Length (filled later)
} else {
allocLen = uint32(util.GetUnalignedUint16(scb[7:9]))
// set header
for i = 0; i < 8 && i < allocLen; i++ {
data = append(data, 0x00)
}
// set header (8 bytes)
// byte 0-1: Mode Data Length
// byte 2: Medium Type
// byte 3: Device-Specific Parameter (DPOFUA=bit4)
// byte 4-5: Reserved
// byte 6-7: Block Descriptor Length
data = append(data, 0x00) // Mode Data Length (MSB, filled later)
data = append(data, 0x00) // Mode Data Length (LSB, filled later)
data = append(data, 0x00) // Medium Type
data = append(data, 0x10) // Device-Specific Parameter (DPOFUA=1)
data = append(data, 0x00) // Reserved
data = append(data, 0x00) // Reserved
data = append(data, 0x00) // Block Descriptor Length (MSB, filled later)
data = append(data, 0x00) // Block Descriptor Length (LSB, filled later)
}
if dbd == 0 {
data = append(data, cmd.Device.ModeBlockDescriptor...)
@@ -595,12 +638,12 @@ func SPCModeSense(host int, cmd *api.SCSICommand) api.SAMStat {
for _, pg := range cmd.Device.ModePages {
if pg.SubPageCode == 0 {
data = append(data, pg.PageCode)
data = append(data, pg.Size)
data = append(data, byte(pg.Size))
} else {
data = append(data, pg.PageCode|0x40)
data = append(data, pg.SubPageCode)
data = append(data, (pg.Size>>8)&0xff)
data = append(data, pg.Size&0xff)
data = append(data, byte((pg.Size>>8)&0xff))
data = append(data, byte(pg.Size&0xff))
}
if pctrl == 1 {
data = append(data, pg.Data[pg.Size:]...)
@@ -621,7 +664,7 @@ func SPCModeSense(host int, cmd *api.SCSICommand) api.SAMStat {
}
if pg.SubPageCode == 0 {
data = append(data, pg.PageCode)
data = append(data, pg.Size)
data = append(data, byte(pg.Size))
if pctrl == 1 {
data = append(data, pg.Data[pg.Size:]...)
} else {
@@ -630,8 +673,8 @@ func SPCModeSense(host int, cmd *api.SCSICommand) api.SAMStat {
} else {
data = append(data, pg.PageCode|0x40)
data = append(data, pg.SubPageCode)
data = append(data, (pg.Size>>8)&0xff)
data = append(data, pg.Size&0xff)
data = append(data, byte((pg.Size>>8)&0xff))
data = append(data, byte(pg.Size&0xff))
if pctrl == 1 {
data = append(data, pg.Data[pg.Size:]...)
} else {
@@ -700,17 +743,17 @@ func reportOpcodesAll(cmd *api.SCSICommand, rctd int) error {
data = append(data, 0x00)
// reserved
data = append(data, 0x00)
// flags : no service action, possibly timeout desc
// flags: no service action, possibly timeout desc
if rctd != 0 {
data = append(data, 0x02)
data = append(data, 0x08)
} else {
data = append(data, 0x00)
data = append(data, 0x08)
}
// cdb length
length := getSCSICmdSize(i)
data = append(data, 0)
data = append(data, length&0xff)
// timeout descriptor
// timeout descriptor (if rctd is set) - 12 bytes (all zeros)
if rctd != 0 {
// length == 0x0a
data[1] = 0x0a
@@ -725,7 +768,53 @@ func reportOpcodesAll(cmd *api.SCSICommand, rctd int) error {
}
func reportOpcodeOne(cmd *api.SCSICommand, rctd int, opcode byte, rsa uint16, serviceAction bool) error {
return fmt.Errorf("rsa: %xh, sa:%v not supported", rsa, serviceAction)
var data = []byte{0x00, 0x00, 0x00, 0x00}
// Support common opcodes that are tested by libiscsi
switch api.SCSICommandType(opcode) {
case api.READ_6, api.READ_10, api.READ_12, api.READ_16,
api.WRITE_6, api.WRITE_10, api.WRITE_12, api.WRITE_16,
api.WRITE_VERIFY, api.WRITE_VERIFY_12, api.WRITE_VERIFY_16,
api.INQUIRY, api.TEST_UNIT_READY, api.READ_CAPACITY,
api.VERIFY_10, api.VERIFY_12, api.VERIFY_16:
// For RCTD=0, libiscsi expects:
// data[0:4]: list length
// data[4:20]: CDB usage data (16 bytes)
// libiscsi reads ctdp from data[1], cdb_length from data[2:4]
// and copies data[4:4+cdb_length] to cdb_usage_data
//
// So we need to format data as:
// data[4]: opcode (CDB usage data byte 0)
// data[5]: byte 1 with DPO/FUA bits
// data[6:20]: remaining CDB usage data bytes
// CDB usage data (16 bytes) - describes the CDB format
cdbUsageData := make([]byte, 16)
cdbUsageData[0] = opcode // byte 0: opcode
// byte 1: RDPROTECT(7-5) | DPO(4) | FUA(3) | ...
// Set DPO(0x10) | FUA(0x08) = 0x18 for READ/WRITE/VERIFY/WRITE_VERIFY
if opcode == 0x28 || opcode == 0x2A || opcode == 0x2F || // READ10, WRITE10, VERIFY10
opcode == 0xA8 || opcode == 0xAA || opcode == 0xAF || // READ12, WRITE12, VERIFY12
opcode == 0x88 || opcode == 0x8A || opcode == 0x8F || // READ16, WRITE16, VERIFY16
opcode == 0x2E || opcode == 0xAE || opcode == 0x8E { // WRITE_VERIFY, WRITE_VERIFY_12, WRITE_VERIFY_16
cdbUsageData[1] = 0x18 // DPO | FUA
}
data = append(data, cdbUsageData...)
// timeout descriptor (if rctd is set) - 12 bytes (all zeros)
if rctd != 0 {
for n := 0; n < 12; n++ {
data = append(data, 0x00)
}
}
default:
return fmt.Errorf("opcode: %02xh not supported in report one", opcode)
}
// Update list length (total bytes after the length field)
copy(cmd.InSDBBuffer.Buffer, util.MarshalUint32(uint32(len(data)-4)))
copy(cmd.InSDBBuffer.Buffer[4:], data[4:])
return nil
}
func SPCReportSupportedOperationCodes(host int, cmd *api.SCSICommand) api.SAMStat {
@@ -799,6 +888,8 @@ func SPCPRReadKeys(host int, cmd *api.SCSICommand) api.SAMStat {
scsiResOp := GetSCSIReservationOperator()
PRGeneration, _ := scsiResOp.GetPRGeneration(tgtName, devUUID)
resList := scsiResOp.GetReservationList(tgtName, devUUID)
length, _ := SCSICDBBufXLength(cmd.SCB)
allocationLength = uint16(length)
if allocationLength < 8 {
goto sense
}
@@ -979,7 +1070,7 @@ func SPCPRRegister(host int, cmd *api.SCSICommand) api.SAMStat {
if ignoreKey || resKey == 0 {
if sAResKey != 0 {
newRes := &api.SCSIReservation{
ID: uuid.NewV1(),
ID: uuid.New(),
Key: sAResKey,
ITNexusID: cmd.ITNexusID,
}

View File

@@ -20,8 +20,8 @@ import (
"fmt"
"unsafe"
"github.com/google/uuid"
"github.com/gostor/gotgt/pkg/api"
uuid "github.com/satori/go.uuid"
log "github.com/sirupsen/logrus"
)
@@ -37,7 +37,7 @@ func (s *SCSITargetService) NewSCSITarget(tid int, driverName, name string) (*ap
TargetPortGroups: []*api.TargetPortGroup{},
ITNexus: make(map[uuid.UUID]*api.ITNexus),
}
tpg := &api.TargetPortGroup{0, []*api.SCSITargetPort{}}
tpg := &api.TargetPortGroup{GroupID: 0, TargetPortGroup: []*api.SCSITargetPort{}}
s.Targets = append(s.Targets, target)
target.Devices = GetTargetLUNMap(target.Name)
target.LUN0 = NewLUN0()
@@ -110,7 +110,7 @@ func deviceReserve(cmd *api.SCSICommand) error {
return nil
}
if !uuid.Equal(lu.ReserveID, uuid.Nil) && uuid.Equal(lu.ReserveID, cmd.ITNexusID) {
if lu.ReserveID != uuid.Nil && lu.ReserveID == cmd.ITNexusID {
log.Errorf("already reserved %d, %d", lu.ReserveID, cmd.ITNexusID)
return fmt.Errorf("already reserved")
}