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

@@ -17,16 +17,105 @@ limitations under the License.
package iscsit
import (
"bytes"
"fmt"
"strings"
"sync"
"time"
"github.com/gostor/gotgt/pkg/api"
"github.com/gostor/gotgt/pkg/util"
"github.com/gostor/gotgt/pkg/util/numa"
log "github.com/sirupsen/logrus"
)
// Object pools to reduce GC pressure
var (
// commandPool reuses ISCSICommand objects
commandPool = sync.Pool{
New: func() interface{} {
return &ISCSICommand{}
},
}
// bufferPool reuses small buffers for BHS reading
bufferPool = sync.Pool{
New: func() interface{} {
buf := make([]byte, BHS_SIZE)
return &buf
},
}
// numaBufferPool NUMA-aware buffer pool for larger I/O operations
numaBufferPool *numa.NUMABufferPool
numaPoolOnce sync.Once
)
// initNUMAPool initializes the NUMA-aware buffer pool
func initNUMAPool() {
numaPoolOnce.Do(func() {
numaBufferPool = numa.NewNUMABufferPool(&numa.BufferPoolConfig{
BufferSize: 256 * 1024, // 256KB for I/O buffers
PerNodePoolSize: 512,
EnableNUMA: numa.Available(),
})
})
}
// getCommand gets an ISCSICommand from the pool
func getCommand() *ISCSICommand {
return commandPool.Get().(*ISCSICommand)
}
// putCommand puts an ISCSICommand back to the pool
func putCommand(cmd *ISCSICommand) {
if cmd == nil {
return
}
// Clear sensitive data
cmd.RawData = nil
cmd.RawHeader = nil
cmd.CDB = nil
cmd.DataLen = 0
*cmd = ISCSICommand{}
commandPool.Put(cmd)
}
// getBuffer gets a buffer from the pool
func getBuffer() []byte {
return *bufferPool.Get().(*[]byte)
}
// putBuffer puts a buffer back to the pool
func putBuffer(buf []byte) {
if cap(buf) >= BHS_SIZE {
bufferPool.Put(&buf)
}
}
// getIOBuffer gets a NUMA-aware I/O buffer for larger data operations
func getIOBuffer(size int) []byte {
initNUMAPool()
if size <= numaBufferPool.GetConfig().BufferSize {
return numaBufferPool.Get()[:size]
}
return make([]byte, size)
}
// putIOBuffer puts a NUMA-aware I/O buffer back to the pool
func putIOBuffer(buf []byte) {
if numaBufferPool != nil && cap(buf) >= numaBufferPool.GetConfig().BufferSize {
numaBufferPool.Put(buf)
}
}
// NUMAStats returns NUMA buffer pool statistics
func NUMAStats() numa.PoolStats {
if numaBufferPool == nil {
return numa.PoolStats{}
}
return numaBufferPool.Stats()
}
type OpCode int
const (
@@ -164,6 +253,8 @@ func (cmd *ISCSICommand) Bytes() []byte {
return cmd.scsiTMFRespBytes()
case OpReady:
return cmd.r2tRespBytes()
case OpAsync:
return cmd.asyncMsgBytes()
}
return nil
}
@@ -237,7 +328,7 @@ func parseHeader(data []byte) (*ISCSICommand, error) {
m.CmdSN = uint32(ParseUint(data[24:28]))
m.Read = data[1]&0x40 == 0x40
m.Write = data[1]&0x20 == 0x20
m.CDB = data[32:48]
m.CDB = append([]byte{}, data[32:48]...)
m.ExpStatSN = uint32(ParseUint(data[28:32]))
m.SCSIOpCode = m.CDB[0]
SCSIOpcode := api.SCSICommandType(m.SCSIOpCode)
@@ -290,9 +381,12 @@ func parseHeader(data []byte) (*ISCSICommand, error) {
}
func (m *ISCSICommand) scsiCmdRespBytes() []byte {
// rfc7143 11.4
buf := bytes.Buffer{}
buf.WriteByte(byte(OpSCSIResp))
// rfc7143 11.4 - BHS 48 bytes + data (4-byte aligned)
rawDataLen := len(m.RawData)
padding := (4 - rawDataLen%4) % 4
buf := make([]byte, 48+rawDataLen+padding)
buf[0] = byte(OpSCSIResp)
var flag byte = 0x80
if m.Resid > 0 {
if m.Resid > m.ExpectedDataLen {
@@ -301,50 +395,46 @@ func (m *ISCSICommand) scsiCmdRespBytes() []byte {
flag |= 0x02
}
}
buf.WriteByte(flag)
buf.WriteByte(byte(m.SCSIResponse))
buf.WriteByte(byte(m.Status))
buf[1] = flag
buf[2] = byte(m.SCSIResponse)
buf[3] = byte(m.Status)
buf.WriteByte(0x00)
buf.Write(util.MarshalUint64(uint64(len(m.RawData)))[5:]) // 5-8
// Skip through to byte 16
for i := 0; i < 8; i++ {
buf.WriteByte(0x00)
}
buf.Write(util.MarshalUint64(uint64(m.TaskTag))[4:])
for i := 0; i < 4; i++ {
buf.WriteByte(0x00)
}
buf.Write(util.MarshalUint64(uint64(m.StatSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.ExpCmdSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.MaxCmdSN))[4:])
for i := 0; i < 2*4; i++ {
buf.WriteByte(0x00)
}
buf.Write(util.MarshalUint64(uint64(m.Resid))[4:])
buf.Write(m.RawData)
dl := len(m.RawData)
for dl%4 > 0 {
dl++
buf.WriteByte(0x00)
}
// byte 4 is reserved (0)
// Write data length (24-bit big-endian) at bytes 5-7
buf[5] = byte(rawDataLen >> 16)
buf[6] = byte(rawDataLen >> 8)
buf[7] = byte(rawDataLen)
// bytes 9-15 are reserved (0)
// TaskTag at bytes 16-19 (32-bit big-endian)
util.MarshalUint32To(buf[16:], m.TaskTag)
// bytes 20-23 are reserved (0)
// StatSN at bytes 24-27
util.MarshalUint32To(buf[24:], m.StatSN)
// ExpCmdSN at bytes 28-31
util.MarshalUint32To(buf[28:], m.ExpCmdSN)
// MaxCmdSN at bytes 32-35
util.MarshalUint32To(buf[32:], m.MaxCmdSN)
// bytes 36-43 are reserved (0)
// Resid at bytes 44-47
util.MarshalUint32To(buf[44:], m.Resid)
copy(buf[48:], m.RawData)
// padding bytes are already zero
return buf.Bytes()
return buf
}
func (m *ISCSICommand) dataInBytes() []byte {
// rfc7143 11.7
dl := m.DataLen
for dl%4 > 0 {
dl++
}
var buf = make([]byte, (48 + dl))
// Calculate padded length using bit operation instead of loop
dl := (m.DataLen + 3) &^ 3 // Round up to multiple of 4
buf := make([]byte, 48+dl)
buf[0] = byte(OpSCSIIn)
var flag byte
if m.FinalInSeq || m.Final == true {
if m.FinalInSeq || m.Final {
flag |= 0x80
}
if m.HasStatus && m.Final == true {
if m.HasStatus && m.Final {
flag |= 0x01
}
log.Debugf("resid: %v, ExpectedDataLen: %v", m.Resid, m.ExpectedDataLen)
@@ -356,22 +446,22 @@ func (m *ISCSICommand) dataInBytes() []byte {
}
}
buf[1] = flag
//buf.WriteByte(0x00)
if m.HasStatus && m.Final == true {
flag = byte(m.Status)
if m.HasStatus && m.Final {
buf[3] = byte(m.Status)
}
//buf.WriteByte(flag)
buf[3] = flag
copy(buf[5:], util.MarshalUint64(uint64(m.DataLen))[5:])
// Data length (24-bit) at bytes 5-7
buf[5] = byte(m.DataLen >> 16)
buf[6] = byte(m.DataLen >> 8)
buf[7] = byte(m.DataLen)
// Skip through to byte 16 Since A bit is not set 11.7.4
copy(buf[16:], util.MarshalUint32(m.TaskTag))
copy(buf[24:], util.MarshalUint32(m.StatSN))
copy(buf[28:], util.MarshalUint32(m.ExpCmdSN))
copy(buf[32:], util.MarshalUint32(m.MaxCmdSN))
copy(buf[36:], util.MarshalUint32(m.DataSN))
copy(buf[40:], util.MarshalUint32(m.BufferOffset))
copy(buf[44:], util.MarshalUint32(m.Resid))
if m.ExpectedDataLen != 0 {
util.MarshalUint32To(buf[16:], m.TaskTag)
util.MarshalUint32To(buf[24:], m.StatSN)
util.MarshalUint32To(buf[28:], m.ExpCmdSN)
util.MarshalUint32To(buf[32:], m.MaxCmdSN)
util.MarshalUint32To(buf[36:], m.DataSN)
util.MarshalUint32To(buf[40:], m.BufferOffset)
util.MarshalUint32To(buf[44:], m.Resid)
if m.DataLen != 0 {
copy(buf[48:], m.RawData[m.BufferOffset:m.BufferOffset+uint32(m.DataLen)])
}
@@ -379,8 +469,13 @@ func (m *ISCSICommand) dataInBytes() []byte {
}
func (m *ISCSICommand) textRespBytes() []byte {
buf := bytes.Buffer{}
buf.WriteByte(byte(OpTextResp))
// Pre-calculate required capacity: BHS(48 bytes) + data (4-byte aligned)
dataLen := len(m.RawData)
padding := (4 - dataLen%4) % 4
buf := make([]byte, 48+dataLen+padding)
buf[0] = byte(OpTextResp)
var b byte
if m.Final {
b |= 0x80
@@ -389,122 +484,149 @@ func (m *ISCSICommand) textRespBytes() []byte {
b |= 0x40
}
// byte 1
buf.WriteByte(b)
buf[1] = b
b = 0
buf.WriteByte(b)
buf.WriteByte(b)
buf.WriteByte(b)
buf.Write(util.MarshalUint64(uint64(len(m.RawData)))[5:]) // 5-8
// Skip through to byte 12
for i := 0; i < 2*4; i++ {
buf.WriteByte(0x00)
}
buf.Write(util.MarshalUint64(uint64(m.TaskTag))[4:])
for i := 0; i < 4; i++ {
buf.WriteByte(0xff)
}
buf.Write(util.MarshalUint64(uint64(m.StatSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.ExpCmdSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.MaxCmdSN))[4:])
for i := 0; i < 3*4; i++ {
buf.WriteByte(0x00)
}
rd := m.RawData
for len(rd)%4 != 0 {
rd = append(rd, 0)
}
buf.Write(rd)
return buf.Bytes()
// bytes 2,3,4 reserved (0)
// bytes 5-8: data segment length (24-bit)
buf[5] = byte(dataLen >> 16)
buf[6] = byte(dataLen >> 8)
buf[7] = byte(dataLen)
// bytes 8-15 are reserved (0)
// bytes 16-19: TaskTag
util.MarshalUint32To(buf[16:], m.TaskTag)
// bytes 20-23: 0xffffffff
buf[20] = 0xff
buf[21] = 0xff
buf[22] = 0xff
buf[23] = 0xff
// bytes 24-27: StatSN
util.MarshalUint32To(buf[24:], m.StatSN)
// bytes 28-31: ExpCmdSN
util.MarshalUint32To(buf[28:], m.ExpCmdSN)
// bytes 32-35: MaxCmdSN
util.MarshalUint32To(buf[32:], m.MaxCmdSN)
// bytes 36-47 are reserved (0)
// Copy data
copy(buf[48:], m.RawData)
// padding bytes are already zero
return buf
}
func (m *ISCSICommand) noopInBytes() []byte {
buf := bytes.Buffer{}
buf.WriteByte(byte(OpNoopIn))
var b byte
b |= 0x80
// byte 1
buf.WriteByte(b)
// rfc7143 11.11 - BHS 48 bytes + data (4-byte aligned)
rawDataLen := len(m.RawData)
padding := (4 - rawDataLen%4) % 4
buf := make([]byte, 48+rawDataLen+padding)
b = 0
buf.WriteByte(b)
buf.WriteByte(b)
buf.WriteByte(b)
buf.Write(util.MarshalUint64(uint64(len(m.RawData)))[5:]) // 5-8
// Skip through to byte 12
for i := 0; i < 2*4; i++ {
buf.WriteByte(0x00)
}
buf.Write(util.MarshalUint64(uint64(m.TaskTag))[4:])
for i := 0; i < 4; i++ {
buf.WriteByte(0xff)
}
buf.Write(util.MarshalUint64(uint64(m.StatSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.ExpCmdSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.MaxCmdSN))[4:])
for i := 0; i < 3*4; i++ {
buf.WriteByte(0x00)
}
rd := m.RawData
for len(rd)%4 != 0 {
rd = append(rd, 0)
}
buf.Write(rd)
return buf.Bytes()
buf[0] = byte(OpNoopIn)
buf[1] = 0x80
// bytes 2-3 are reserved (0)
// bytes 4-7: data segment length (32-bit)
util.MarshalUint32To(buf[4:], uint32(rawDataLen))
// bytes 8-15 are reserved (0)
// bytes 16-19: TaskTag
util.MarshalUint32To(buf[16:], m.TaskTag)
// bytes 20-23: 0xffffffff
buf[20] = 0xff
buf[21] = 0xff
buf[22] = 0xff
buf[23] = 0xff
// bytes 24-27: StatSN
util.MarshalUint32To(buf[24:], m.StatSN)
// bytes 28-31: ExpCmdSN
util.MarshalUint32To(buf[28:], m.ExpCmdSN)
// bytes 32-35: MaxCmdSN
util.MarshalUint32To(buf[32:], m.MaxCmdSN)
// bytes 36-47 are reserved (0)
copy(buf[48:], m.RawData)
// padding bytes are already zero
return buf
}
func (m *ISCSICommand) scsiTMFRespBytes() []byte {
// rfc7143 11.6
buf := bytes.Buffer{}
buf.WriteByte(byte(OpSCSITaskResp))
buf.WriteByte(0x80)
buf.WriteByte(m.Result)
buf.WriteByte(0x00)
// Skip through to byte 16
for i := 0; i < 3*4; i++ {
buf.WriteByte(0x00)
}
buf.Write(util.MarshalUint64(uint64(m.TaskTag))[4:])
for i := 0; i < 4; i++ {
buf.WriteByte(0x00)
}
buf.Write(util.MarshalUint64(uint64(m.StatSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.ExpCmdSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.MaxCmdSN))[4:])
for i := 0; i < 3*4; i++ {
buf.WriteByte(0x00)
}
return buf.Bytes()
// rfc7143 11.6 - Fixed 48 bytes
buf := make([]byte, 48)
buf[0] = byte(OpSCSITaskResp)
buf[1] = 0x80
buf[2] = m.Result
// byte 3 is reserved (0)
// bytes 4-15 are reserved (0)
// bytes 16-19: TaskTag
util.MarshalUint32To(buf[16:], m.TaskTag)
// bytes 20-23 are reserved (0)
// bytes 24-27: StatSN
util.MarshalUint32To(buf[24:], m.StatSN)
// bytes 28-31: ExpCmdSN
util.MarshalUint32To(buf[28:], m.ExpCmdSN)
// bytes 32-35: MaxCmdSN
util.MarshalUint32To(buf[32:], m.MaxCmdSN)
// bytes 36-47 are reserved (0)
return buf
}
func (m *ISCSICommand) r2tRespBytes() []byte {
// rfc7143 11.8
buf := bytes.Buffer{}
buf.WriteByte(byte(OpReady))
var b byte
// rfc7143 11.8 - Fixed 48 bytes
buf := make([]byte, 48)
buf[0] = byte(OpReady)
if m.Final {
b |= 0x80
buf[1] = 0x80
}
buf.WriteByte(b)
buf.WriteByte(0x00)
buf.WriteByte(0x00)
// Skip through to byte 16
for i := 0; i < 3*4; i++ {
buf.WriteByte(0x00)
}
buf.Write(util.MarshalUint64(uint64(m.TaskTag))[4:])
for i := 0; i < 4; i++ {
buf.WriteByte(0x00)
}
buf.Write(util.MarshalUint64(uint64(m.StatSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.ExpCmdSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.MaxCmdSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.R2TSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.BufferOffset))[4:])
buf.Write(util.MarshalUint64(uint64(m.DesiredLength))[4:])
return buf.Bytes()
// bytes 2-15 are reserved (0)
// bytes 16-19: TaskTag
util.MarshalUint32To(buf[16:], m.TaskTag)
// bytes 20-23 are reserved (0)
// bytes 24-27: StatSN
util.MarshalUint32To(buf[24:], m.StatSN)
// bytes 28-31: ExpCmdSN
util.MarshalUint32To(buf[28:], m.ExpCmdSN)
// bytes 32-35: MaxCmdSN
util.MarshalUint32To(buf[32:], m.MaxCmdSN)
// bytes 36-39: R2TSN
util.MarshalUint32To(buf[36:], m.R2TSN)
// bytes 40-43: BufferOffset
util.MarshalUint32To(buf[40:], m.BufferOffset)
// bytes 44-47: DesiredLength
util.MarshalUint32To(buf[44:], m.DesiredLength)
return buf
}
// asyncMsgBytes implements RFC 7143 section 11.10 - Asynchronous Message
func (m *ISCSICommand) asyncMsgBytes() []byte {
// rfc7143 11.10 - BHS 48 bytes + data (4-byte aligned)
rawDataLen := len(m.RawData)
padding := (4 - rawDataLen%4) % 4
buf := make([]byte, 48+rawDataLen+padding)
buf[0] = byte(OpAsync)
// byte 1: AsyncEvent in bits 0-4
buf[1] = byte(m.SCSIOpCode & 0x1f)
// bytes 2-3 are reserved (0)
// byte 4: 0x80 if AsyncEvent is 0 (SCSI Asynchronous Event)
if m.SCSIOpCode == 0 {
buf[4] = 0x80
}
// bytes 5-7: data segment length (24-bit)
buf[5] = byte(rawDataLen >> 16)
buf[6] = byte(rawDataLen >> 8)
buf[7] = byte(rawDataLen)
// bytes 8-15: LUN (if applicable)
copy(buf[8:], m.LUN[:])
// bytes 16-19: Reserved (0)
// bytes 20-23: Target Transfer Tag (0xffffffff for Async)
buf[20] = 0xff
buf[21] = 0xff
buf[22] = 0xff
buf[23] = 0xff
// bytes 24-27: StatSN
util.MarshalUint32To(buf[24:], m.StatSN)
// bytes 28-31: ExpCmdSN
util.MarshalUint32To(buf[28:], m.ExpCmdSN)
// bytes 32-35: MaxCmdSN
util.MarshalUint32To(buf[32:], m.MaxCmdSN)
// bytes 36-43: Reserved (0)
// bytes 44-47: Parameter1 and Parameter2 (context-specific)
copy(buf[48:], m.RawData)
return buf
}

146
pkg/port/iscsit/cmd_test.go Normal file
View File

@@ -0,0 +1,146 @@
/*
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 iscsit
import (
"testing"
"unsafe"
)
func TestGetPutCommand(t *testing.T) {
// Test getting command object
cmd1 := getCommand()
if cmd1 == nil {
t.Fatal("getCommand() returned nil")
}
// Set some values
cmd1.TaskTag = 12345
cmd1.DataLen = 100
cmd1.ExpCmdSN = 999
// Put back to pool
putCommand(cmd1)
// Get again, verify if reused (may be reset)
cmd2 := getCommand()
if cmd2 == nil {
t.Fatal("getCommand() returned nil after put")
}
// Put back
putCommand(cmd2)
// Test nil doesn't panic
putCommand(nil)
}
func TestGetPutBuffer(t *testing.T) {
// Test getting buffer
buf1 := getBuffer()
if buf1 == nil {
t.Fatal("getBuffer() returned nil")
}
if len(buf1) != BHS_SIZE {
t.Errorf("expected buffer size %d, got %d", BHS_SIZE, len(buf1))
}
// Modify buffer content
for i := range buf1 {
buf1[i] = byte(i % 256)
}
// Put back to pool
putBuffer(buf1)
// Get again
buf2 := getBuffer()
if buf2 == nil {
t.Fatal("getBuffer() returned nil after put")
}
if len(buf2) != BHS_SIZE {
t.Errorf("expected buffer size %d, got %d", BHS_SIZE, len(buf2))
}
putBuffer(buf2)
// Test small buffer won't be put into pool
smallBuf := make([]byte, 10)
putBuffer(smallBuf) // Should not panic
// Test nil doesn't panic
putBuffer(nil)
}
func TestBufferPoolReuse(t *testing.T) {
// Get buffer and record address
buf1 := getBuffer()
ptr1 := uintptr(unsafe.Pointer(&buf1[0]))
putBuffer(buf1)
// Get again, verify if reuse is possible (not guaranteed)
buf2 := getBuffer()
ptr2 := uintptr(unsafe.Pointer(&buf2[0]))
putBuffer(buf2)
// If reused, addresses should be the same
// If not reused, it's fine, this is a performance test
t.Logf("First buffer pointer: %x, Second buffer pointer: %x, reused: %v",
ptr1, ptr2, ptr1 == ptr2)
}
func BenchmarkGetPutCommand(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
cmd := getCommand()
cmd.TaskTag = 1
putCommand(cmd)
}
})
}
func BenchmarkGetPutBuffer(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
buf := getBuffer()
buf[0] = 1
putBuffer(buf)
}
})
}
// BenchmarkAllocCommand 对比:不使用 pool 直接创建
func BenchmarkAllocCommand(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
cmd := &ISCSICommand{}
cmd.TaskTag = 1
_ = cmd
}
})
}
// BenchmarkAllocBuffer 对比:不使用 pool 直接创建
func BenchmarkAllocBuffer(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
buf := make([]byte, BHS_SIZE)
buf[0] = 1
_ = buf
}
})
}

View File

@@ -155,12 +155,12 @@ func (conn *iscsiConnection) buildRespPackage(oc OpCode, task *iscsiTask) error
if task == nil {
task = conn.rxTask
}
conn.resp = &ISCSICommand{
StartTime: conn.req.StartTime,
StatSN: conn.req.ExpStatSN,
TaskTag: conn.req.TaskTag,
ExpectedDataLen: conn.req.ExpectedDataLen,
}
// Get ISCSICommand from object pool
conn.resp = getCommand()
conn.resp.StartTime = conn.req.StartTime
conn.resp.StatSN = conn.req.ExpStatSN
conn.resp.TaskTag = conn.req.TaskTag
conn.resp.ExpectedDataLen = conn.req.ExpectedDataLen
if conn.session != nil {
conn.resp.ExpCmdSN = conn.session.ExpCmdSN
conn.resp.MaxCmdSN = conn.session.ExpCmdSN + conn.session.MaxQueueCommand

View File

@@ -44,6 +44,86 @@ const (
STATE_TERMINATE
)
// tsihBitmap is a bitmap for efficient TSIH allocation/deallocation
// Uses circular counter for O(1) allocation
type tsihBitmap struct {
mu sync.Mutex
bitmap []uint64 // Each uint64 stores the usage status of 64 TSIHs
next uint16 // Next candidate position for allocation
used uint16 // Number of used TSIHs
}
// newTSIHBitmap creates a new TSIH bitmap
// Reserves 0 and 65535 as special values
func newTSIHBitmap() *tsihBitmap {
// Need 65536 bits = 1024 uint64s
b := &tsihBitmap{
bitmap: make([]uint64, 1024),
next: 1, // Start from 1, 0 is reserved
}
// Mark 0 and 65535 as used (reserved values)
b.bitmap[0] |= 1 << 0 // TSIH = 0
b.bitmap[1023] |= 1 << 63 // TSIH = 65535
b.used = 2
return b
}
// alloc allocates an available TSIH using circular search strategy
func (b *tsihBitmap) alloc() uint16 {
b.mu.Lock()
defer b.mu.Unlock()
if b.used >= ISCSI_MAX_TSIH-1 {
return ISCSI_UNSPEC_TSIH
}
start := b.next
for {
idx := b.next / 64
bit := b.next % 64
if (b.bitmap[idx] & (1 << bit)) == 0 {
// Found free slot
b.bitmap[idx] |= 1 << bit
b.used++
result := b.next
// Update next to next position
b.next++
if b.next >= ISCSI_MAX_TSIH {
b.next = 1
}
return result
}
b.next++
if b.next >= ISCSI_MAX_TSIH {
b.next = 1
}
if b.next == start {
// Looped around without finding
return ISCSI_UNSPEC_TSIH
}
}
}
// release releases a TSIH
func (b *tsihBitmap) release(tsih uint16) {
if tsih == 0 || tsih == ISCSI_MAX_TSIH {
return // Cannot release reserved values
}
b.mu.Lock()
defer b.mu.Unlock()
idx := tsih / 64
bit := tsih % 64
if (b.bitmap[idx] & (1 << bit)) != 0 {
b.bitmap[idx] &^= 1 << bit
b.used--
}
}
var (
EnableStats bool
CurrentHostIP string
@@ -54,8 +134,7 @@ type ISCSITargetDriver struct {
SCSI *scsi.SCSITargetService
Name string
iSCSITargets map[string]*ISCSITarget
TSIHPool map[uint16]bool
TSIHPoolMutex sync.Mutex
tsihBitmap *tsihBitmap
isClientConnected bool
enableStats bool
mu *sync.RWMutex
@@ -76,7 +155,7 @@ func NewISCSITargetDriver(base *scsi.SCSITargetService) (scsi.SCSITargetDriver,
Name: iSCSIDriverName,
iSCSITargets: map[string]*ISCSITarget{},
SCSI: base,
TSIHPool: map[uint16]bool{0: true, 65535: true},
tsihBitmap: newTSIHBitmap(),
mu: &sync.RWMutex{},
}
@@ -88,24 +167,11 @@ func NewISCSITargetDriver(base *scsi.SCSITargetService) (scsi.SCSITargetDriver,
}
func (s *ISCSITargetDriver) AllocTSIH() uint16 {
var i uint16
s.TSIHPoolMutex.Lock()
for i = uint16(0); i < ISCSI_MAX_TSIH; i++ {
exist := s.TSIHPool[i]
if !exist {
s.TSIHPool[i] = true
s.TSIHPoolMutex.Unlock()
return i
}
}
s.TSIHPoolMutex.Unlock()
return ISCSI_UNSPEC_TSIH
return s.tsihBitmap.alloc()
}
func (s *ISCSITargetDriver) ReleaseTSIH(tsih uint16) {
s.TSIHPoolMutex.Lock()
delete(s.TSIHPool, tsih)
s.TSIHPoolMutex.Unlock()
s.tsihBitmap.release(tsih)
}
func (s *ISCSITargetDriver) NewTarget(tgtName string, configInfo *config.Config) error {
@@ -122,9 +188,9 @@ func (s *ISCSITargetDriver) NewTarget(tgtName string, configInfo *config.Config)
targetConfig := configInfo.ISCSITargets[tgtName]
for tpgt, portalIDArrary := range targetConfig.TPGTs {
tpgtNumber, _ := strconv.ParseUint(tpgt, 10, 16)
tgt.TPGTs[uint16(tpgtNumber)] = &iSCSITPGT{uint16(tpgtNumber), make(map[string]struct{})}
tgt.TPGTs[uint16(tpgtNumber)] = &iSCSITPGT{TPGT: uint16(tpgtNumber), Portals: make(map[string]struct{})}
targetPortName := fmt.Sprintf("%s,t,0x%02x", tgtName, tpgtNumber)
scsiTPG.TargetPortGroup = append(scsiTPG.TargetPortGroup, &api.SCSITargetPort{uint16(tpgtNumber), targetPortName})
scsiTPG.TargetPortGroup = append(scsiTPG.TargetPortGroup, &api.SCSITargetPort{RelativeTargetPortID: uint16(tpgtNumber), TargetPortName: targetPortName})
for _, portalID := range portalIDArrary {
portal := configInfo.ISCSIPortals[portalID]
s.AddiSCSIPortal(tgtName, uint16(tpgtNumber), portal.Portal)
@@ -323,10 +389,12 @@ func (s *ISCSITargetDriver) rxHandler(conn *iscsiConnection) {
ddigest uint = 0
final bool = false
cmd *ISCSICommand
buf []byte = make([]byte, BHS_SIZE)
buf []byte = getBuffer()
length int
err error
)
defer putBuffer(buf)
conn.readLock.Lock()
defer conn.readLock.Unlock()
if conn.state == CONN_STATE_SCSI {
@@ -366,10 +434,10 @@ func (s *ISCSITargetDriver) rxHandler(conn *iscsiConnection) {
}
final = true
case IOSTATE_RX_INIT_AHS:
conn.rxIOState = IOSTATE_RX_DATA
break
if hdigest != 0 {
conn.rxIOState = IOSTATE_RX_INIT_HDIGEST
} else {
conn.rxIOState = IOSTATE_RX_DATA
}
case IOSTATE_RX_DATA:
if ddigest != 0 {
@@ -563,6 +631,92 @@ func iscsiExecNoopOut(conn *iscsiConnection) error {
return conn.buildRespPackage(OpNoopIn, nil)
}
// SNACK Type constants per RFC 7143
const (
SNACK_TYPE_DATA_ACK = 0 // Data ACK
SNACK_TYPE_STATUS_ACK = 1 // Status ACK
SNACK_TYPE_DATA_R2T = 2 // Data R2T
SNACK_TYPE_R_DATA = 3 // R-Data
)
/*
* iscsiExecSNACK handles SNACK (Sequence Number Acknowledgement) requests
* SNACK is used for error recovery in iSCSI protocol per RFC 7143 section 11.9
*/
func (s *ISCSITargetDriver) iscsiExecSNACK(conn *iscsiConnection) error {
req := conn.req
// Parse SNACK type from byte 1, bits 0-1
snackType := (req.SCSIOpCode >> 0) & 0x03
// Parse BegRun and RunLength from the header
begRun := req.ReferencedTaskTag
runLength := req.R2TSN
log.Debugf("SNACK request type=%d, BegRun=%d, RunLength=%d", snackType, begRun, runLength)
switch snackType {
case SNACK_TYPE_DATA_ACK:
// Data ACK - initiator acknowledges receipt of Data-In PDUs
// For ErrorRecoveryLevel >= 1, we could track acknowledged Data-In
log.Debug("SNACK Data ACK received")
// Simply return success for now
conn.resp = &ISCSICommand{
OpCode: OpNoopIn,
Final: true,
TaskTag: req.TaskTag,
StatSN: conn.statSN,
ExpCmdSN: conn.expCmdSN,
}
if conn.session != nil {
conn.resp.MaxCmdSN = conn.session.ExpCmdSN + conn.session.MaxQueueCommand
}
return nil
case SNACK_TYPE_STATUS_ACK:
// Status ACK - initiator acknowledges receipt of status
log.Debug("SNACK Status ACK received")
// Similar to Data ACK, just acknowledge
conn.resp = &ISCSICommand{
OpCode: OpNoopIn,
Final: true,
TaskTag: req.TaskTag,
StatSN: conn.statSN,
ExpCmdSN: conn.expCmdSN,
}
if conn.session != nil {
conn.resp.MaxCmdSN = conn.session.ExpCmdSN + conn.session.MaxQueueCommand
}
return nil
case SNACK_TYPE_DATA_R2T:
// Data R2T - request retransmission of R2T
log.Debug("SNACK Data R2T received - requesting R2T retransmission")
// Find the task and resend R2T
conn.session.PendingTasksMutex.RLock()
task := conn.session.PendingTasks.GetByTag(begRun)
conn.session.PendingTasksMutex.RUnlock()
if task == nil {
log.Errorf("Cannot find task for R2T retransmission, tag=%d", begRun)
return fmt.Errorf("task not found")
}
// Reset R2T state and resend
task.r2tSN = runLength
conn.rxTask = task
return iscsiExecR2T(conn)
case SNACK_TYPE_R_DATA:
// R-Data - request retransmission of Data-In
log.Debug("SNACK R-Data received - requesting Data-In retransmission")
// For now, reject this as it requires complex data buffering
// In a full implementation, we would need to buffer Data-In PDUs
// and retransmit based on BegRun and RunLength
log.Warn("R-Data SNACK not fully implemented")
return fmt.Errorf("R-Data SNACK not supported")
default:
return fmt.Errorf("unknown SNACK type: %d", snackType)
}
}
func iscsiExecReject(conn *iscsiConnection) error {
return conn.buildRespPackage(OpReject, nil)
}
@@ -852,10 +1006,16 @@ func (s *ISCSITargetDriver) scsiCommandHandler(conn *iscsiConnection) (err error
conn.txTask = &iscsiTask{conn: conn, cmd: conn.req, tag: conn.req.TaskTag}
conn.txIOState = IOSTATE_TX_BHS
iscsiExecLogout(conn)
case OpTextReq, OpSNACKReq:
case OpTextReq:
err = fmt.Errorf("Cannot handle yet %s", opCodeMap[conn.req.OpCode])
log.Error(err)
return
case OpSNACKReq:
log.Debug("SNACK Request processing...")
if err := s.iscsiExecSNACK(conn); err != nil {
log.Errorf("SNACK handling failed: %v", err)
iscsiExecReject(conn)
}
default:
err = fmt.Errorf("Unknown op %s", opCodeMap[conn.req.OpCode])
log.Error(err)
@@ -900,22 +1060,20 @@ func (s *ISCSITargetDriver) iscsiTaskQueueHandler(task *iscsiTask) error {
task.state = taskSCSI
sess.PendingTasksMutex.Unlock()
goto retry
} else {
if cmd.CmdSN < sess.ExpCmdSN {
err := fmt.Errorf("unexpected cmd serial number: (%d, %d)", cmd.CmdSN, sess.ExpCmdSN)
log.Error(err)
return err
}
log.Debugf("add task(%d) into task queue", task.cmd.CmdSN)
// add this task into queue and set it as a pending task
sess.PendingTasksMutex.Lock()
task.state = taskPending
sess.PendingTasks.Push(task)
sess.PendingTasksMutex.Unlock()
return fmt.Errorf("pending")
}
return nil
// cmd.CmdSN != sess.ExpCmdSN
if cmd.CmdSN < sess.ExpCmdSN {
err := fmt.Errorf("unexpected cmd serial number: (%d, %d)", cmd.CmdSN, sess.ExpCmdSN)
log.Error(err)
return err
}
log.Debugf("add task(%d) into task queue", task.cmd.CmdSN)
// add this task into queue and set it as a pending task
sess.PendingTasksMutex.Lock()
task.state = taskPending
sess.PendingTasks.Push(task)
sess.PendingTasksMutex.Unlock()
return fmt.Errorf("pending")
}
func (s *ISCSITargetDriver) iscsiExecTask(task *iscsiTask) error {
@@ -972,6 +1130,63 @@ func (s *ISCSITargetDriver) iscsiExecTask(task *iscsiTask) error {
return nil
}
// Async Event types per RFC 7143
const (
ASYNC_EVENT_SCSI = 0 // SCSI Asynchronous Event
ASYNC_EVENT_STATUS = 1 // iSCSI Status Update
ASYNC_EVENT_LOGOUT = 2 // iSCSI Logout Request
ASYNC_EVENT_DROP_CONN = 3 // iSCSI Drop Connection
ASYNC_EVENT_DROP_SESS = 4 // iSCSI Drop All Connections
ASYNC_EVENT_NOP = 5 // iSCSI NOP
ASYNC_EVENT_VENDOR = 255 // Vendor Specific Event
)
/*
* SendAsyncMessage sends an asynchronous message to the initiator
* This implements RFC 7143 section 11.10 Asynchronous Message
*/
func (s *ISCSITargetDriver) SendAsyncMessage(conn *iscsiConnection, eventType byte, lun [8]uint8, param1, param2 uint32, data []byte) error {
if conn == nil || conn.state != CONN_STATE_SCSI {
return fmt.Errorf("connection not ready for async message")
}
conn.statSN += 1
conn.resp = &ISCSICommand{
OpCode: OpAsync,
SCSIOpCode: eventType,
Final: true,
LUN: lun,
StatSN: conn.statSN,
ExpCmdSN: conn.expCmdSN,
RawData: data,
}
if conn.session != nil {
conn.resp.MaxCmdSN = conn.session.ExpCmdSN + conn.session.MaxQueueCommand
}
// Parameter1 and Parameter2 are encoded in RawData or could be stored in ISCSICommand
// For simplicity, we encode them at the start of RawData if not already present
if len(data) == 0 && (param1 != 0 || param2 != 0) {
conn.resp.RawData = make([]byte, 8)
copy(conn.resp.RawData[0:4], util.MarshalUint32(param1))
copy(conn.resp.RawData[4:8], util.MarshalUint32(param2))
}
log.Debugf("Sending Async message type=%d to initiator", eventType)
s.handler(DATAOUT, conn)
return nil
}
// SendSCSIAsyncEvent sends a SCSI asynchronous event (e.g., LUN reset, storage change)
func (s *ISCSITargetDriver) SendSCSIAsyncEvent(conn *iscsiConnection, lun [8]uint8, eventCode byte) error {
// SCSI Async Event data format:
// bytes 0-1: Event Code
// bytes 2-3: Reserved
// bytes 4+: Event-specific data
data := []byte{eventCode, 0, 0, 0}
return s.SendAsyncMessage(conn, ASYNC_EVENT_SCSI, lun, 0, 0, data)
}
func (s *ISCSITargetDriver) Stats() scsi.Stats {
s.mu.RLock()
stats := s.TargetStats

View File

@@ -0,0 +1,176 @@
/*
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 iscsit
import (
"sync"
"testing"
)
func TestTSIHBitmapAllocRelease(t *testing.T) {
b := newTSIHBitmap()
// Test basic allocation and release
tsih1 := b.alloc()
if tsih1 == ISCSI_UNSPEC_TSIH {
t.Fatal("failed to allocate first TSIH")
}
if tsih1 != 1 {
t.Errorf("expected first TSIH to be 1, got %d", tsih1)
}
tsih2 := b.alloc()
if tsih2 == ISCSI_UNSPEC_TSIH {
t.Fatal("failed to allocate second TSIH")
}
if tsih2 != 2 {
t.Errorf("expected second TSIH to be 2, got %d", tsih2)
}
// Release first
b.release(tsih1)
// Note: TSIH bitmap uses circular allocation, next pointer won't return to released positions
// This is to avoid concurrency issues, subsequent allocations continue from current next
tsih3 := b.alloc()
if tsih3 == ISCSI_UNSPEC_TSIH {
t.Error("failed to allocate after release")
}
// Verify tsih1 can be reallocated (at some point)
if tsih3 == tsih1 || tsih3 == tsih2 {
t.Logf("TSIH was recycled immediately: released %d, got %d", tsih1, tsih3)
}
// Release all
b.release(tsih2)
b.release(tsih3)
}
func TestTSIHBitmapReservedValues(t *testing.T) {
b := newTSIHBitmap()
// Test reserved values cannot be allocated
// 0 and 65535 are reserved values
for i := 0; i < 10; i++ {
tsih := b.alloc()
if tsih == 0 {
t.Error("allocated reserved TSIH 0")
}
if tsih == ISCSI_MAX_TSIH {
t.Error("allocated reserved TSIH 65535")
}
if tsih == ISCSI_UNSPEC_TSIH {
break
}
b.release(tsih)
}
// Test releasing reserved values doesn't panic
b.release(0)
b.release(ISCSI_MAX_TSIH)
}
func TestTSIHBitmapExhaustion(t *testing.T) {
b := newTSIHBitmap()
// Allocate many TSIHs
allocated := make([]uint16, 0, 100)
for i := 0; i < 100; i++ {
tsih := b.alloc()
if tsih == ISCSI_UNSPEC_TSIH {
t.Fatalf("failed to allocate TSIH at iteration %d", i)
}
allocated = append(allocated, tsih)
}
// 释放所有
for _, tsih := range allocated {
b.release(tsih)
}
// Reallocate, should succeed
for i := 0; i < 100; i++ {
tsih := b.alloc()
if tsih == ISCSI_UNSPEC_TSIH {
t.Fatalf("failed to reallocate TSIH at iteration %d", i)
}
b.release(tsih)
}
}
func TestTSIHBitmapConcurrency(t *testing.T) {
b := newTSIHBitmap()
const numGoroutines = 100
const allocsPerGoroutine = 100
var wg sync.WaitGroup
wg.Add(numGoroutines)
allTSIHs := make(chan uint16, numGoroutines*allocsPerGoroutine)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < allocsPerGoroutine; j++ {
tsih := b.alloc()
if tsih != ISCSI_UNSPEC_TSIH {
allTSIHs <- tsih
}
}
}()
}
wg.Wait()
close(allTSIHs)
// Check no duplicate TSIHs
seen := make(map[uint16]bool)
for tsih := range allTSIHs {
if seen[tsih] {
t.Errorf("TSIH %d was allocated more than once", tsih)
}
seen[tsih] = true
}
// Release all allocated TSIHs
for tsih := range seen {
b.release(tsih)
}
}
func BenchmarkTSIHBitmapAlloc(b *testing.B) {
bitmap := newTSIHBitmap()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
tsih := bitmap.alloc()
if tsih != ISCSI_UNSPEC_TSIH {
bitmap.release(tsih)
}
}
})
}
func BenchmarkTSIHBitmapAllocSequential(b *testing.B) {
bitmap := newTSIHBitmap()
b.ResetTimer()
for i := 0; i < b.N; i++ {
tsih := bitmap.alloc()
if tsih != ISCSI_UNSPEC_TSIH {
bitmap.release(tsih)
}
}
}

View File

@@ -79,7 +79,7 @@ type iSCSITPGT struct {
}
type ISCSITarget struct {
api.SCSITarget
*api.SCSITarget
api.SCSITargetDriverCommon
// TPGT number is the key
TPGTs map[uint16]*iSCSITPGT
@@ -123,7 +123,7 @@ func (tgt *ISCSITarget) FindTPG(portal string) (uint16, error) {
func newISCSITarget(target *api.SCSITarget) *ISCSITarget {
return &ISCSITarget{
SCSITarget: *target,
SCSITarget: target,
TPGTs: make(map[uint16]*iSCSITPGT),
Sessions: make(map[uint16]*ISCSISession),
}

View File

@@ -17,7 +17,6 @@ limitations under the License.
package iscsit
import (
"bytes"
"fmt"
"strings"
@@ -26,20 +25,20 @@ import (
var (
iSCSILoginParamTextKV = []util.KeyValue{
{"HeaderDigest", "None"},
{"DataDigest", "None"},
{"ImmediateData", "Yes"},
{"InitialR2T", "Yes"},
{"MaxBurstLength", "262144"},
{"FirstBurstLength", "65536"},
{"MaxRecvDataSegmentLength", "65536"},
{"DefaultTime2Wait", "2"},
{"DefaultTime2Retain", "0"},
{"MaxOutstandingR2T", "1"},
{"IFMarker", "No"},
{"OFMarker", "No"},
{"DataPDUInOrder", "Yes"},
{"DataSequenceInOrder", "Yes"}}
{Key: "HeaderDigest", Value: "None"},
{Key: "DataDigest", Value: "None"},
{Key: "ImmediateData", Value: "Yes"},
{Key: "InitialR2T", Value: "Yes"},
{Key: "MaxBurstLength", Value: "262144"},
{Key: "FirstBurstLength", Value: "65536"},
{Key: "MaxRecvDataSegmentLength", Value: "65536"},
{Key: "DefaultTime2Wait", Value: "2"},
{Key: "DefaultTime2Retain", Value: "0"},
{Key: "MaxOutstandingR2T", Value: "1"},
{Key: "IFMarker", Value: "No"},
{Key: "OFMarker", Value: "No"},
{Key: "DataPDUInOrder", Value: "Yes"},
{Key: "DataSequenceInOrder", Value: "Yes"}}
)
type iSCSILoginStage int
@@ -63,10 +62,10 @@ func (s iSCSILoginStage) String() string {
}
func loginKVDeclare(conn *iscsiConnection, negoKV []util.KeyValue) []util.KeyValue {
negoKV = append(negoKV, util.KeyValue{"TargetPortalGroupTag",
numberKeyInConv(uint(conn.loginParam.tpgt))})
negoKV = append(negoKV, util.KeyValue{"MaxRecvDataSegmentLength",
numberKeyInConv(sessionKeys["MaxRecvDataSegmentLength"].def)})
negoKV = append(negoKV, util.KeyValue{Key: "TargetPortalGroupTag",
Value: numberKeyInConv(uint(conn.loginParam.tpgt))})
negoKV = append(negoKV, util.KeyValue{Key: "MaxRecvDataSegmentLength",
Value: numberKeyInConv(sessionKeys["MaxRecvDataSegmentLength"].def)})
return negoKV
}
@@ -158,14 +157,14 @@ func (conn *iscsiConnection) processLoginData() ([]util.KeyValue, error) {
if uintVal != defSessKey.def {
kvChanges++
}
negoKV = append(negoKV, util.KeyValue{key, defSessKey.inConv(defSessKey.def)})
negoKV = append(negoKV, util.KeyValue{Key: key, Value: defSessKey.inConv(defSessKey.def)})
} else {
if (uintVal >= defSessKey.min) && (uintVal <= defSessKey.max) {
conn.loginParam.sessionParam[defSessKey.idx].Value = uintVal
negoKV = append(negoKV, util.KeyValue{key, defSessKey.inConv(uintVal)})
negoKV = append(negoKV, util.KeyValue{Key: key, Value: defSessKey.inConv(uintVal)})
} else {
// the value out of the acceptable range, Uses target default key
negoKV = append(negoKV, util.KeyValue{key, defSessKey.inConv(defSessKey.def)})
negoKV = append(negoKV, util.KeyValue{Key: key, Value: defSessKey.inConv(defSessKey.def)})
kvChanges++
}
}
@@ -222,10 +221,13 @@ type iscsiLoginParam struct {
}
func (m *ISCSICommand) loginRespBytes() []byte {
// rfc7143 11.13
buf := &bytes.Buffer{}
// byte 0
buf.WriteByte(byte(OpLoginResp))
// rfc7143 11.13 - BHS 48 bytes + data (4-byte aligned)
rawDataLen := len(m.RawData)
padding := (4 - rawDataLen%4) % 4
buf := make([]byte, 48+rawDataLen+padding)
// byte 0: Opcode
buf[0] = byte(OpLoginResp)
var b byte
if m.Transit {
b |= 0x80
@@ -236,33 +238,38 @@ func (m *ISCSICommand) loginRespBytes() []byte {
b |= byte(m.CSG&0xff) << 2
b |= byte(m.NSG & 0xff)
// byte 1
buf.WriteByte(b)
buf[1] = b
b = 0
buf.WriteByte(b) // version-max
buf.WriteByte(b) // version-active
buf.WriteByte(b) // ahsLen
buf.Write(util.MarshalUint64(uint64(len(m.RawData)))[5:]) // data segment length, no padding
buf.Write(util.MarshalUint64(m.ISID)[2:])
buf.Write(util.MarshalUint64(uint64(m.TSIH))[6:])
buf.Write(util.MarshalUint64(uint64(m.TaskTag))[4:])
buf.WriteByte(b)
buf.WriteByte(b)
buf.WriteByte(b)
buf.WriteByte(b) // "reserved"
buf.Write(util.MarshalUint64(uint64(m.StatSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.ExpCmdSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.MaxCmdSN))[4:])
buf.WriteByte(byte(m.StatusClass))
buf.WriteByte(byte(m.StatusDetail))
buf.WriteByte(b)
buf.WriteByte(b) // "reserved"
var bs [8]byte
buf.Write(bs[:])
rd := m.RawData
for len(rd)%4 != 0 {
rd = append(rd, 0)
}
buf.Write(rd)
return buf.Bytes()
// byte 2: version-max, byte 3: version-active
// bytes 4-7: data segment length (24-bit)
buf[5] = byte(rawDataLen >> 16)
buf[6] = byte(rawDataLen >> 8)
buf[7] = byte(rawDataLen)
// bytes 8-13: ISID (6 bytes) - lower 6 bytes of uint64
buf[8] = byte(m.ISID >> 40)
buf[9] = byte(m.ISID >> 32)
buf[10] = byte(m.ISID >> 24)
buf[11] = byte(m.ISID >> 16)
buf[12] = byte(m.ISID >> 8)
buf[13] = byte(m.ISID)
// bytes 14-15: TSIH (2 bytes)
buf[14] = byte(m.TSIH >> 8)
buf[15] = byte(m.TSIH)
// bytes 16-19: TaskTag
util.MarshalUint32To(buf[16:], m.TaskTag)
// bytes 20-23: reserved
// bytes 24-27: StatSN
util.MarshalUint32To(buf[24:], m.StatSN)
// bytes 28-31: ExpCmdSN
util.MarshalUint32To(buf[28:], m.ExpCmdSN)
// bytes 32-35: MaxCmdSN
util.MarshalUint32To(buf[32:], m.MaxCmdSN)
// bytes 36: StatusClass, 37: StatusDetail
buf[36] = byte(m.StatusClass)
buf[37] = byte(m.StatusDetail)
// bytes 38-47: reserved
// Copy data
copy(buf[48:], m.RawData)
// padding bytes are already zero
return buf
}

View File

@@ -1,29 +1,25 @@
package iscsit
import (
"bytes"
"github.com/gostor/gotgt/pkg/util"
)
func (m *ISCSICommand) logoutRespBytes() []byte {
buf := &bytes.Buffer{}
buf.WriteByte(byte(OpLogoutResp))
buf.WriteByte(0x80)
buf.WriteByte(0x00) // response
buf.WriteByte(0x00)
for i := 4; i < 16; i++ {
buf.WriteByte(0x00)
}
buf.Write(util.MarshalUint64(uint64(m.TaskTag))[4:])
for i := 20; i < 24; i++ {
buf.WriteByte(0x00)
}
buf.Write(util.MarshalUint64(uint64(m.StatSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.ExpCmdSN))[4:])
buf.Write(util.MarshalUint64(uint64(m.MaxCmdSN))[4:])
for i := 36; i < 48; i++ {
buf.WriteByte(0x00)
}
return buf.Bytes()
// rfc7143 11.10 - Fixed 48 bytes
buf := make([]byte, 48)
buf[0] = byte(OpLogoutResp)
buf[1] = 0x80
// buf[2] = response (0)
// bytes 4-15 are reserved (0)
// bytes 16-19: TaskTag
util.MarshalUint32To(buf[16:], m.TaskTag)
// bytes 20-23 are reserved (0)
// bytes 24-27: StatSN
util.MarshalUint32To(buf[24:], m.StatSN)
// bytes 28-31: ExpCmdSN
util.MarshalUint32To(buf[28:], m.ExpCmdSN)
// bytes 32-35: MaxCmdSN
util.MarshalUint32To(buf[32:], m.MaxCmdSN)
// bytes 36-47 are reserved (0)
return buf
}

View File

@@ -0,0 +1,403 @@
/*
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 iscsit
import (
"bytes"
"testing"
"time"
"github.com/gostor/gotgt/pkg/api"
"github.com/gostor/gotgt/pkg/util"
)
// BenchmarkParseHeader benchmarks iSCSI protocol header parsing performance
func BenchmarkParseHeader(b *testing.B) {
// Build a typical SCSI CDB command header
header := make([]byte, BHS_SIZE)
header[0] = byte(OpSCSICmd) // SCSI Command
header[1] = 0x80 // Final bit
header[4] = 0 // AHS length
header[5] = 0
header[6] = 0
header[7] = 0 // Data segment length = 0
// TaskTag at bytes 16-19
header[16] = 0x00
header[17] = 0x00
header[18] = 0x00
header[19] = 0x01
// ExpectedDataLen at bytes 20-23
header[20] = 0x00
header[21] = 0x00
header[22] = 0x10
header[23] = 0x00 // 4096 bytes
// CmdSN at bytes 24-27
header[24] = 0x00
header[25] = 0x00
header[26] = 0x00
header[27] = 0x01
// ExpStatSN at bytes 28-31
header[28] = 0x00
header[29] = 0x00
header[30] = 0x00
header[31] = 0x01
// CDB at bytes 32-47 (READ_10 command)
header[32] = byte(api.READ_10)
header[33] = 0x00
header[34] = 0x00
header[35] = 0x00
header[36] = 0x00
header[37] = 0x00 // LBA = 0
header[38] = 0x00
header[39] = 0x08 // Transfer length = 8 blocks
header[40] = 0x00 // Control
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
cmd, err := parseHeader(header)
if err != nil {
b.Fatal(err)
}
_ = cmd
}
}
// BenchmarkParseHeaderWithPool benchmarks header parsing with object pool
func BenchmarkParseHeaderWithPool(b *testing.B) {
header := make([]byte, BHS_SIZE)
header[0] = byte(OpSCSICmd)
header[1] = 0x80
header[32] = byte(api.READ_10)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
cmd := getCommand()
cmd.OpCode = OpCode(header[0] & ISCSI_OPCODE_MASK)
cmd.Final = 0x80&header[1] == 0x80
cmd.AHSLen = int(header[4]) * 4
cmd.DataLen = int(ParseUint(header[5:8]))
cmd.TaskTag = uint32(ParseUint(header[16:20]))
cmd.CDB = header[32:48]
cmd.StartTime = time.Now()
putCommand(cmd)
}
}
// BenchmarkDataInBytes benchmarks Data-In response serialization performance
func BenchmarkDataInBytes(b *testing.B) {
data := make([]byte, 4096)
for i := range data {
data[i] = byte(i % 256)
}
cmd := &ISCSICommand{
OpCode: OpSCSIIn,
Final: true,
FinalInSeq: true,
TaskTag: 1,
StatSN: 100,
ExpCmdSN: 101,
MaxCmdSN: 200,
DataLen: 4096,
DataSN: 0,
BufferOffset: 0,
RawData: data,
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = cmd.dataInBytes()
}
}
// BenchmarkDataInBytesSmall benchmarks Data-In performance with small data blocks
func BenchmarkDataInBytesSmall(b *testing.B) {
data := make([]byte, 512)
cmd := &ISCSICommand{
OpCode: OpSCSIIn,
Final: true,
FinalInSeq: true,
TaskTag: 1,
StatSN: 100,
ExpCmdSN: 101,
MaxCmdSN: 200,
DataLen: 512,
DataSN: 0,
BufferOffset: 0,
RawData: data,
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = cmd.dataInBytes()
}
}
// BenchmarkDataInBytesLarge benchmarks Data-In performance with large data blocks
func BenchmarkDataInBytesLarge(b *testing.B) {
data := make([]byte, 65536)
cmd := &ISCSICommand{
OpCode: OpSCSIIn,
Final: true,
FinalInSeq: true,
TaskTag: 1,
StatSN: 100,
ExpCmdSN: 101,
MaxCmdSN: 200,
DataLen: 65536,
DataSN: 0,
BufferOffset: 0,
RawData: data,
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = cmd.dataInBytes()
}
}
// BenchmarkBytesComparison compares Bytes() performance for different OpCodes
func BenchmarkBytesComparison(b *testing.B) {
testCases := []struct {
name string
cmd *ISCSICommand
}{
{
name: "LoginResp",
cmd: &ISCSICommand{
OpCode: OpLoginResp,
Final: true,
Transit: true,
CSG: LoginOperationalNegotiation,
NSG: FullFeaturePhase,
TaskTag: 1,
StatSN: 0,
ExpCmdSN: 1,
MaxCmdSN: 1,
StatusClass: 0,
StatusDetail: 0,
RawData: []byte("TargetPortalGroupTag=1"),
},
},
{
name: "SCSIResp",
cmd: &ISCSICommand{
OpCode: OpSCSIResp,
Final: true,
TaskTag: 1,
StatSN: 100,
ExpCmdSN: 101,
MaxCmdSN: 200,
},
},
{
name: "SCSIIn",
cmd: &ISCSICommand{
OpCode: OpSCSIIn,
Final: true,
TaskTag: 1,
StatSN: 100,
ExpCmdSN: 101,
MaxCmdSN: 200,
DataLen: 4096,
RawData: make([]byte, 4096),
},
},
{
name: "R2T",
cmd: &ISCSICommand{
OpCode: OpReady,
Final: true,
TaskTag: 1,
StatSN: 100,
ExpCmdSN: 101,
MaxCmdSN: 200,
R2TSN: 0,
BufferOffset: 0,
DesiredLength: 8192,
},
},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = tc.cmd.Bytes()
}
})
}
}
// BenchmarkCommandPool benchmarks command object pool performance
func BenchmarkCommandPool(b *testing.B) {
b.Run("WithPool", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
cmd := getCommand()
cmd.OpCode = OpSCSICmd
cmd.TaskTag = uint32(i)
putCommand(cmd)
}
})
b.Run("WithoutPool", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
cmd := &ISCSICommand{
OpCode: OpSCSICmd,
TaskTag: uint32(i),
}
_ = cmd
}
})
}
// BenchmarkBufferPool benchmarks buffer pool performance
func BenchmarkBufferPool(b *testing.B) {
b.Run("WithPool", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
buf := getBuffer()
buf[0] = byte(i)
putBuffer(buf)
}
})
b.Run("WithoutPool", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
buf := make([]byte, BHS_SIZE)
buf[0] = byte(i)
_ = buf
}
})
}
// BenchmarkTaskStateTransition benchmarks task state transition performance
func BenchmarkTaskStateTransition(b *testing.B) {
task := &iscsiTask{
tag: 1,
state: taskPending,
scmd: &api.SCSICommand{},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if i%2 == 0 {
task.state = taskPending
} else {
task.state = taskSCSI
}
}
}
// BenchmarkParseUint benchmarks ParseUint performance
func BenchmarkParseUint(b *testing.B) {
testData := []byte{0x00, 0x00, 0x10, 0x00}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = ParseUint(testData)
}
}
// BenchmarkBytesComparisonEqual benchmarks byte comparison performance
func BenchmarkBytesComparisonEqual(b *testing.B) {
a := make([]byte, 48)
b2 := make([]byte, 48)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = bytes.Equal(a, b2)
}
}
// BenchmarkMarshalUint32 benchmarks uint32 serialization performance
func BenchmarkMarshalUint32(b *testing.B) {
val := uint32(0x12345678)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = util.MarshalUint32(val)
}
}
// BenchmarkMarshalUint64 benchmarks uint64 serialization performance
func BenchmarkMarshalUint64(b *testing.B) {
val := uint64(0x1234567890ABCDEF)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = util.MarshalUint64(val)
}
}
// BenchmarkBuildRespPackage benchmarks complete response package building performance
func BenchmarkBuildRespPackage(b *testing.B) {
conn := &iscsiConnection{
state: CONN_STATE_SCSI,
statSN: 99,
expCmdSN: 100,
loginParam: &iscsiLoginParam{
sessionParam: []ISCSISessionParam{
{idx: ISCSI_PARAM_MAX_BURST, Value: 262144},
},
},
session: &ISCSISession{
ExpCmdSN: 100,
MaxQueueCommand: 32,
},
req: &ISCSICommand{
OpCode: OpSCSICmd,
TaskTag: 1,
ExpStatSN: 100,
ExpectedDataLen: 4096,
StartTime: time.Now(),
},
rxTask: &iscsiTask{
tag: 1,
scmd: &api.SCSICommand{
Result: 0,
Direction: api.SCSIDataRead,
InSDBBuffer: &api.SCSIDataBuffer{
Buffer: make([]byte, 4096),
Length: 4096,
},
},
},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = conn.buildRespPackage(OpSCSIResp, nil)
}
}

View File

@@ -0,0 +1,472 @@
/*
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 iscsit
import (
"bytes"
"encoding/binary"
"testing"
)
// TestLoginRespBytesFormat verifies Login Response BHS format complies with RFC 7143
func TestLoginRespBytesFormat(t *testing.T) {
cmd := &ISCSICommand{
OpCode: OpLoginResp,
Transit: true,
Cont: false,
CSG: LoginOperationalNegotiation,
NSG: FullFeaturePhase,
ISID: 0x123456789ABC,
TSIH: 0x1234,
TaskTag: 0xABCDEF00,
StatSN: 0x12345678,
ExpCmdSN: 0x87654321,
MaxCmdSN: 0x87654421,
StatusClass: 0,
StatusDetail: 0,
RawData: []byte("TestData"),
}
resp := cmd.loginRespBytes()
// Verify BHS length is at least 48 bytes
if len(resp) < 48 {
t.Fatalf("BHS too short: expected at least 48, got %d", len(resp))
}
// Byte 0: Opcode
if resp[0] != byte(OpLoginResp) {
t.Errorf("Byte 0: expected OpLoginResp(0x23), got 0x%02x", resp[0])
}
// Byte 1: Flags
expectedFlags := byte(0x80 | (byte(LoginOperationalNegotiation&0xff) << 2) | byte(FullFeaturePhase&0xff))
if resp[1] != expectedFlags {
t.Errorf("Byte 1: expected 0x%02x, got 0x%02x", expectedFlags, resp[1])
}
// Byte 2-3: Version
if resp[2] != 0 || resp[3] != 0 {
t.Logf("Byte 2-3 (version): %d, %d", resp[2], resp[3])
}
// Byte 4-7: Data Segment Length
dataLen := binary.BigEndian.Uint32(resp[4:8])
if dataLen != uint32(len(cmd.RawData)) {
t.Errorf("Data segment length: expected %d, got %d", len(cmd.RawData), dataLen)
}
// Byte 8-13: ISID (6 bytes)
isid := binary.BigEndian.Uint64(append([]byte{0, 0}, resp[8:14]...))
if isid != cmd.ISID {
t.Errorf("ISID: expected 0x%012x, got 0x%012x", cmd.ISID, isid)
}
// Byte 14-15: TSIH
tsih := binary.BigEndian.Uint16(resp[14:16])
if tsih != cmd.TSIH {
t.Errorf("TSIH: expected 0x%04x, got 0x%04x", cmd.TSIH, tsih)
}
// Byte 16-19: Task Tag
taskTag := binary.BigEndian.Uint32(resp[16:20])
if taskTag != cmd.TaskTag {
t.Errorf("TaskTag: expected 0x%08x, got 0x%08x", cmd.TaskTag, taskTag)
}
// Byte 24-27: StatSN
statSN := binary.BigEndian.Uint32(resp[24:28])
if statSN != cmd.StatSN {
t.Errorf("StatSN: expected 0x%08x, got 0x%08x", cmd.StatSN, statSN)
}
// Byte 28-31: ExpCmdSN
expCmdSN := binary.BigEndian.Uint32(resp[28:32])
if expCmdSN != cmd.ExpCmdSN {
t.Errorf("ExpCmdSN: expected 0x%08x, got 0x%08x", cmd.ExpCmdSN, expCmdSN)
}
// Byte 32-35: MaxCmdSN
maxCmdSN := binary.BigEndian.Uint32(resp[32:36])
if maxCmdSN != cmd.MaxCmdSN {
t.Errorf("MaxCmdSN: expected 0x%08x, got 0x%08x", cmd.MaxCmdSN, maxCmdSN)
}
// Byte 36: StatusClass
if resp[36] != cmd.StatusClass {
t.Errorf("StatusClass: expected %d, got %d", cmd.StatusClass, resp[36])
}
// Byte 37: StatusDetail
if resp[37] != cmd.StatusDetail {
t.Errorf("StatusDetail: expected %d, got %d", cmd.StatusDetail, resp[37])
}
// Verify data segment
if len(resp) > 48 {
data := resp[48:]
if !bytes.Equal(data, cmd.RawData) {
t.Errorf("RawData mismatch: expected %v, got %v", cmd.RawData, data)
}
}
// Verify 4-byte alignment
if len(resp)%4 != 0 {
t.Errorf("Response not aligned to 4 bytes: length=%d", len(resp))
}
}
// TestLogoutRespBytesFormat verifies Logout Response BHS format
func TestLogoutRespBytesFormat(t *testing.T) {
cmd := &ISCSICommand{
OpCode: OpLogoutResp,
TaskTag: 0x12345678,
StatSN: 0xABCDEF00,
ExpCmdSN: 0x11223344,
MaxCmdSN: 0x55667788,
}
resp := cmd.logoutRespBytes()
// Verify length is exactly 48 bytes
if len(resp) != 48 {
t.Fatalf("Logout response length: expected 48, got %d", len(resp))
}
// Byte 0: Opcode
if resp[0] != byte(OpLogoutResp) {
t.Errorf("Byte 0: expected OpLogoutResp(0x26), got 0x%02x", resp[0])
}
// Byte 1: Flags (0x80)
if resp[1] != 0x80 {
t.Errorf("Byte 1: expected 0x80, got 0x%02x", resp[1])
}
// Byte 2: Response (0)
if resp[2] != 0 {
t.Errorf("Byte 2: expected 0, got 0x%02x", resp[2])
}
// Byte 16-19: Task Tag
taskTag := binary.BigEndian.Uint32(resp[16:20])
if taskTag != cmd.TaskTag {
t.Errorf("TaskTag: expected 0x%08x, got 0x%08x", cmd.TaskTag, taskTag)
}
// Byte 24-27: StatSN
statSN := binary.BigEndian.Uint32(resp[24:28])
if statSN != cmd.StatSN {
t.Errorf("StatSN: expected 0x%08x, got 0x%08x", cmd.StatSN, statSN)
}
// Byte 28-31: ExpCmdSN
expCmdSN := binary.BigEndian.Uint32(resp[28:32])
if expCmdSN != cmd.ExpCmdSN {
t.Errorf("ExpCmdSN: expected 0x%08x, got 0x%08x", cmd.ExpCmdSN, expCmdSN)
}
// Byte 32-35: MaxCmdSN
maxCmdSN := binary.BigEndian.Uint32(resp[32:36])
if maxCmdSN != cmd.MaxCmdSN {
t.Errorf("MaxCmdSN: expected 0x%08x, got 0x%08x", cmd.MaxCmdSN, maxCmdSN)
}
}
// TestSCSICmdRespBytesFormat verifies SCSI Command Response BHS format
func TestSCSICmdRespBytesFormat(t *testing.T) {
rawData := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05}
cmd := &ISCSICommand{
OpCode: OpSCSIResp,
Status: 0x00, // GOOD
SCSIResponse: 0x00,
TaskTag: 0xABCDEF00,
StatSN: 0x12345678,
ExpCmdSN: 0x87654321,
MaxCmdSN: 0x87654421,
Resid: 0,
RawData: rawData,
ExpectedDataLen: uint32(len(rawData)),
}
resp := cmd.scsiCmdRespBytes()
// Verify length
if len(resp) < 48 {
t.Fatalf("SCSI response too short: expected at least 48, got %d", len(resp))
}
// Byte 0: Opcode
if resp[0] != byte(OpSCSIResp) {
t.Errorf("Byte 0: expected OpSCSIResp(0x21), got 0x%02x", resp[0])
}
// Byte 1: Flags (0x80 = final, no residual)
if resp[1] != 0x80 {
t.Errorf("Byte 1: expected 0x80, got 0x%02x", resp[1])
}
// Byte 2: SCSI Response
if resp[2] != 0 {
t.Errorf("Byte 2 (SCSI Response): expected 0, got %d", resp[2])
}
// Byte 3: Status
if resp[3] != cmd.Status {
t.Errorf("Byte 3 (Status): expected %d, got %d", cmd.Status, resp[3])
}
// 验证数据段
if len(resp) > 48 {
data := resp[48:]
if len(data) >= len(rawData) {
if !bytes.Equal(data[:len(rawData)], rawData) {
t.Errorf("RawData mismatch")
}
}
}
// Verify 4-byte alignment
if len(resp)%4 != 0 {
t.Errorf("Response not aligned to 4 bytes: length=%d", len(resp))
}
}
// TestDataInBytesFormat verifies Data-In response format
func TestDataInBytesFormat(t *testing.T) {
rawData := make([]byte, 512) // Simulate 512 bytes of data
for i := range rawData {
rawData[i] = byte(i % 256)
}
cmd := &ISCSICommand{
OpCode: OpSCSIIn,
Final: true,
FinalInSeq: true,
HasStatus: true,
Status: 0x00,
DataLen: len(rawData),
TaskTag: 0x12345678,
StatSN: 0xABCDEF00,
ExpCmdSN: 0x11111111,
MaxCmdSN: 0x22222222,
DataSN: 0,
BufferOffset: 0,
Resid: 0,
RawData: rawData,
ExpectedDataLen: uint32(len(rawData)),
SCSIOpCode: 0x28, // READ_10
}
resp := cmd.dataInBytes()
// 验证长度
expectedLen := 48 + len(rawData)
if len(rawData)%4 != 0 {
expectedLen += 4 - len(rawData)%4
}
if len(resp) != expectedLen {
t.Fatalf("Data-In response length: expected %d, got %d", expectedLen, len(resp))
}
// Byte 0: Opcode
if resp[0] != byte(OpSCSIIn) {
t.Errorf("Byte 0: expected OpSCSIIn(0x25), got 0x%02x", resp[0])
}
// Byte 1: Flags (0x80 = final, 0x01 = status present)
expectedFlags := byte(0x80 | 0x01)
if resp[1] != expectedFlags {
t.Errorf("Byte 1: expected 0x%02x, got 0x%02x", expectedFlags, resp[1])
}
// Byte 3: Status
if resp[3] != cmd.Status {
t.Errorf("Byte 3 (Status): expected %d, got %d", cmd.Status, resp[3])
}
// 验证数据段
data := resp[48:]
if !bytes.Equal(data, rawData) {
t.Errorf("Data segment mismatch")
}
}
// TestR2TRespBytesFormat verifies R2T (Ready To Transfer) response format
func TestR2TRespBytesFormat(t *testing.T) {
cmd := &ISCSICommand{
OpCode: OpReady,
Final: true,
TaskTag: 0x12345678,
StatSN: 0xABCDEF00,
ExpCmdSN: 0x11111111,
MaxCmdSN: 0x22222222,
R2TSN: 0,
BufferOffset: 0,
DesiredLength: 8192,
}
resp := cmd.r2tRespBytes()
// Verify length is exactly 48 bytes
if len(resp) != 48 {
t.Fatalf("R2T response length: expected 48, got %d", len(resp))
}
// Byte 0: Opcode
if resp[0] != byte(OpReady) {
t.Errorf("Byte 0: expected OpReady(0x31), got 0x%02x", resp[0])
}
// Byte 1: Flags (0x80 = final)
if resp[1] != 0x80 {
t.Errorf("Byte 1: expected 0x80, got 0x%02x", resp[1])
}
// Byte 16-19: Task Tag
taskTag := binary.BigEndian.Uint32(resp[16:20])
if taskTag != cmd.TaskTag {
t.Errorf("TaskTag: expected 0x%08x, got 0x%08x", cmd.TaskTag, taskTag)
}
// Byte 36-39: R2TSN
r2tsn := binary.BigEndian.Uint32(resp[36:40])
if r2tsn != cmd.R2TSN {
t.Errorf("R2TSN: expected 0x%08x, got 0x%08x", cmd.R2TSN, r2tsn)
}
// Byte 40-43: Buffer Offset
bufferOffset := binary.BigEndian.Uint32(resp[40:44])
if bufferOffset != cmd.BufferOffset {
t.Errorf("BufferOffset: expected 0x%08x, got 0x%08x", cmd.BufferOffset, bufferOffset)
}
// Byte 44-47: Desired Length
desiredLength := binary.BigEndian.Uint32(resp[44:48])
if desiredLength != cmd.DesiredLength {
t.Errorf("DesiredLength: expected 0x%08x, got 0x%08x", cmd.DesiredLength, desiredLength)
}
}
// BenchmarkLoginRespBytes benchmarks Login Response
func BenchmarkLoginRespBytes(b *testing.B) {
cmd := &ISCSICommand{
OpCode: OpLoginResp,
Transit: true,
ISID: 0x123456789ABC,
TSIH: 0x1234,
TaskTag: 0xABCDEF00,
StatSN: 0x12345678,
ExpCmdSN: 0x87654321,
MaxCmdSN: 0x87654421,
StatusClass: 0,
StatusDetail: 0,
RawData: []byte("TestData"),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = cmd.loginRespBytes()
}
}
// TestTextRespBytesFormat verifies Text Response BHS format
func TestTextRespBytesFormat(t *testing.T) {
rawData := []byte("SendTargets=test")
cmd := &ISCSICommand{
OpCode: OpTextResp,
Final: true,
Cont: false,
TaskTag: 0x12345678,
StatSN: 0xABCDEF00,
ExpCmdSN: 0x11111111,
MaxCmdSN: 0x22222222,
RawData: rawData,
}
resp := cmd.textRespBytes()
// Verify BHS length is at least 48 bytes
if len(resp) < 48 {
t.Fatalf("BHS too short: expected at least 48, got %d", len(resp))
}
// Byte 0: Opcode
if resp[0] != byte(OpTextResp) {
t.Errorf("Byte 0: expected OpTextResp(0x24), got 0x%02x", resp[0])
}
// Byte 1: Flags (0x80 = final)
if resp[1] != 0x80 {
t.Errorf("Byte 1: expected 0x80, got 0x%02x", resp[1])
}
// Byte 4-7: Data Segment Length
dataLen := binary.BigEndian.Uint32(resp[4:8])
if dataLen != uint32(len(rawData)) {
t.Errorf("Data segment length: expected %d, got %d", len(rawData), dataLen)
}
// Byte 16-19: Task Tag
taskTag := binary.BigEndian.Uint32(resp[16:20])
if taskTag != cmd.TaskTag {
t.Errorf("TaskTag: expected 0x%08x, got 0x%08x", cmd.TaskTag, taskTag)
}
// Byte 20-23: 0xffffffff
if resp[20] != 0xff || resp[21] != 0xff || resp[22] != 0xff || resp[23] != 0xff {
t.Errorf("Bytes 20-23: expected 0xffffffff, got 0x%02x%02x%02x%02x",
resp[20], resp[21], resp[22], resp[23])
}
// Byte 24-27: StatSN
statSN := binary.BigEndian.Uint32(resp[24:28])
if statSN != cmd.StatSN {
t.Errorf("StatSN: expected 0x%08x, got 0x%08x", cmd.StatSN, statSN)
}
// 验证数据段
if len(resp) > 48 {
data := resp[48:]
if !bytes.Equal(data, rawData) {
t.Errorf("RawData mismatch: expected %v, got %v", rawData, data)
}
}
// Verify 4-byte alignment
if len(resp)%4 != 0 {
t.Errorf("Response not aligned to 4 bytes: length=%d", len(resp))
}
}
// BenchmarkSCSICmdRespBytes benchmarks SCSI Command Response
func BenchmarkSCSICmdRespBytes(b *testing.B) {
cmd := &ISCSICommand{
OpCode: OpSCSIResp,
Status: 0x00,
TaskTag: 0xABCDEF00,
StatSN: 0x12345678,
ExpCmdSN: 0x87654321,
MaxCmdSN: 0x87654421,
RawData: []byte{0x00, 0x01, 0x02, 0x03},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = cmd.scsiCmdRespBytes()
}
}

View File

@@ -22,9 +22,9 @@ import (
"strings"
"sync"
"github.com/google/uuid"
"github.com/gostor/gotgt/pkg/api"
"github.com/gostor/gotgt/pkg/scsi"
uuid "github.com/satori/go.uuid"
log "github.com/sirupsen/logrus"
)
@@ -333,7 +333,7 @@ func (s *ISCSITargetDriver) UnBindISCSISession(sess *ISCSISession) {
target.SessionsRWMutex.Lock()
defer target.SessionsRWMutex.Unlock()
delete(target.Sessions, sess.TSIH)
scsi.RemoveITNexus(&sess.Target.SCSITarget, sess.ITNexus)
scsi.RemoveITNexus(sess.Target.SCSITarget, sess.ITNexus)
}
func (s *ISCSITargetDriver) BindISCSISession(conn *iscsiConnection) error {
@@ -395,8 +395,8 @@ func (s *ISCSITargetDriver) BindISCSISession(conn *iscsiConnection) error {
log.Infof("Login request received from initiator: %v, Session type: %s, Target name:%v, ISID: 0x%x",
conn.loginParam.initiator, "Normal", conn.loginParam.target, conn.loginParam.isid)
//register normal session
itnexus := &api.ITNexus{uuid.NewV1(), GeniSCSIITNexusID(newSess)}
scsi.AddITNexus(&newSess.Target.SCSITarget, itnexus)
itnexus := &api.ITNexus{ID: uuid.New(), Tag: GeniSCSIITNexusID(newSess)}
scsi.AddITNexus(newSess.Target.SCSITarget, itnexus)
newSess.ITNexus = itnexus
conn.session = newSess
@@ -417,8 +417,8 @@ func (s *ISCSITargetDriver) BindISCSISession(conn *iscsiConnection) error {
return err
}
itnexus := &api.ITNexus{uuid.NewV1(), GeniSCSIITNexusID(newSess)}
scsi.AddITNexus(&newSess.Target.SCSITarget, itnexus)
itnexus := &api.ITNexus{ID: uuid.New(), Tag: GeniSCSIITNexusID(newSess)}
scsi.AddITNexus(newSess.Target.SCSITarget, itnexus)
newSess.ITNexus = itnexus
conn.session = newSess