From bb998d4de1b3505754434b1f73b572c68ad6c9a8 Mon Sep 17 00:00:00 2001 From: Robin He Date: Mon, 11 May 2026 16:25:31 +0900 Subject: [PATCH 01/13] fix: expose CQE completion error helpers Signed-off-by: Robin He --- cqe_view.go | 4 ++-- errors.go | 25 +++++++++++++++++++++++++ errors_darwin.go | 25 +++++++++++++++++++++++++ errors_test.go | 35 +++++++++++++++++++++++++++++++++++ examples/file_io_test.go | 12 ++++++------ examples/testhelpers_test.go | 4 ++-- 6 files changed, 95 insertions(+), 10 deletions(-) diff --git a/cqe_view.go b/cqe_view.go index 71bbfda..48ef023 100644 --- a/cqe_view.go +++ b/cqe_view.go @@ -27,8 +27,8 @@ import "code.hybscloud.com/iofd" // for i := range n { // cqe := cqes[i] // // Observe the kernel facts first. -// if cqe.Res < 0 { -// return fmt.Errorf("completion failed: op=%d fd=%d res=%d", cqe.Op(), cqe.FD(), cqe.Res) +// if err := cqe.Err(); err != nil { +// return fmt.Errorf("completion failed: op=%d fd=%d: %w", cqe.Op(), cqe.FD(), err) // } // fmt.Printf("completed op=%d on fd=%d with res=%d\n", cqe.Op(), cqe.FD(), cqe.Res) // if cqe.HasMore() { diff --git a/errors.go b/errors.go index 428e866..a7234ed 100644 --- a/errors.go +++ b/errors.go @@ -202,3 +202,28 @@ func errFromErrno(errno uintptr) error { return zcall.Errno(errno) } } + +// CompletionError decodes a CQE result into the package error model. +// Non-negative results are successful byte counts or operation values. +// Negative results are kernel -errno values. +func CompletionError(res int32) error { + if res >= 0 { + return nil + } + return errFromErrno(uintptr(-int64(res))) +} + +// Err decodes c.Res as a completion error. +func (c *CQEView) Err() error { + return CompletionError(c.Res) +} + +// Err decodes c.Res as a completion error. +func (c *DirectCQE) Err() error { + return CompletionError(c.Res) +} + +// Err decodes c.Res as a completion error. +func (c *ExtCQE) Err() error { + return CompletionError(c.Res) +} diff --git a/errors_darwin.go b/errors_darwin.go index fee0f12..574d9b0 100644 --- a/errors_darwin.go +++ b/errors_darwin.go @@ -157,3 +157,28 @@ func errFromErrno(errno uintptr) error { return syscall.Errno(errno) } } + +// CompletionError decodes a CQE result into the package error model. +// Non-negative results are successful byte counts or operation values. +// Negative results are kernel -errno values. +func CompletionError(res int32) error { + if res >= 0 { + return nil + } + return errFromErrno(uintptr(-int64(res))) +} + +// Err decodes c.Res as a completion error. +func (c *CQEView) Err() error { + return CompletionError(c.Res) +} + +// Err decodes c.Res as a completion error. +func (c *DirectCQE) Err() error { + return CompletionError(c.Res) +} + +// Err decodes c.Res as a completion error. +func (c *ExtCQE) Err() error { + return CompletionError(c.Res) +} diff --git a/errors_test.go b/errors_test.go index 311a157..df22801 100644 --- a/errors_test.go +++ b/errors_test.go @@ -7,6 +7,7 @@ package uring import ( + "errors" "testing" "code.hybscloud.com/iox" @@ -59,6 +60,40 @@ func TestErrFromErrno(t *testing.T) { } } +func TestCompletionError(t *testing.T) { + tests := []struct { + name string + res int32 + want error + }{ + {"positive", 7, nil}, + {"zero", 0, nil}, + {"would block", -int32(EAGAIN), iox.ErrWouldBlock}, + {"canceled", -int32(ECANCELED), ErrCanceled}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CompletionError(tt.res) + if !errors.Is(got, tt.want) { + t.Fatalf("CompletionError(%d) = %v, want %v", tt.res, got, tt.want) + } + }) + } +} + +func TestCompletionErrMethods(t *testing.T) { + if err := (&CQEView{Res: 1}).Err(); err != nil { + t.Fatalf("CQEView.Err positive = %v", err) + } + if err := (&DirectCQE{Res: -int32(EAGAIN)}).Err(); !errors.Is(err, iox.ErrWouldBlock) { + t.Fatalf("DirectCQE.Err = %v, want %v", err, iox.ErrWouldBlock) + } + if err := (&ExtCQE{Res: -int32(ECANCELED)}).Err(); !errors.Is(err, ErrCanceled) { + t.Fatalf("ExtCQE.Err = %v, want %v", err, ErrCanceled) + } +} + func TestErrFromErrno_UnmappedReturnsRawErrno(t *testing.T) { // Use an errno that's not mapped unmapped := uintptr(999) diff --git a/examples/file_io_test.go b/examples/file_io_test.go index 44d8201..79f16bf 100644 --- a/examples/file_io_test.go +++ b/examples/file_io_test.go @@ -68,8 +68,8 @@ func TestFileWriteRead(t *testing.T) { if !ok { t.Fatal("Write did not complete") } - if cqe.Res < 0 { - t.Fatalf("Write failed: %d", cqe.Res) + if err := cqe.Err(); err != nil { + t.Fatalf("Write failed: %v", err) } if int(cqe.Res) != len(testData) { t.Errorf("Write: got %d bytes, want %d", cqe.Res, len(testData)) @@ -92,8 +92,8 @@ func TestFileWriteRead(t *testing.T) { if !ok { t.Fatal("Read did not complete") } - if cqe.Res < 0 { - t.Fatalf("Read failed: %d", cqe.Res) + if err := cqe.Err(); err != nil { + t.Fatalf("Read failed: %v", err) } t.Logf("Read completed: %d bytes", cqe.Res) @@ -157,8 +157,8 @@ func TestBatchedFileOps(t *testing.T) { } for i := 0; i < n; i++ { if cqes[i].Op() == uring.IORING_OP_WRITE { - if cqes[i].Res < 0 { - t.Errorf("Write failed: %d", cqes[i].Res) + if err := cqes[i].Err(); err != nil { + t.Errorf("Write failed: %v", err) } completed++ } diff --git a/examples/testhelpers_test.go b/examples/testhelpers_test.go index 564ddd3..ed26b55 100644 --- a/examples/testhelpers_test.go +++ b/examples/testhelpers_test.go @@ -97,8 +97,8 @@ func closeFd(fd uintptr) { // cancellation serialization, unsubscribe suppression, or ExtSQE retirement. func dispatchSimplifiedMultishotCQE(handler uring.MultishotHandler, cqe uring.CQEView) bool { step := uring.MultishotStep{CQE: cqe} - if cqe.Res < 0 { - step.Err = zcall.Errno(uintptr(-cqe.Res)) + if err := cqe.Err(); err != nil { + step.Err = err step.Cancelled = cqe.Res == -int32(uring.ECANCELED) } From 0af5d941418c2cb6fe24aa18bfd7cf14deff9299 Mon Sep 17 00:00:00 2001 From: Robin He Date: Tue, 12 May 2026 12:36:17 +0900 Subject: [PATCH 02/13] fix: make IOPOLL waits observe visible completions Signed-off-by: Robin He --- cqe_direct_linux.go | 10 ++++-- cqe_extended_linux.go | 10 ++++-- interface_linux.go | 21 +++++++----- io_uring_linux.go | 74 ++++++++++++++++++++++++++++++++----------- 4 files changed, 82 insertions(+), 33 deletions(-) diff --git a/cqe_direct_linux.go b/cqe_direct_linux.go index 6277aaa..3c4b947 100644 --- a/cqe_direct_linux.go +++ b/cqe_direct_linux.go @@ -77,6 +77,8 @@ func (c *DirectCQE) IsNotification() bool { // // On single-issuer rings it is not safe for concurrent use with submit, Stop, // or ResizeRings; caller must serialize those operations. +// On IOPOLL rings WaitDirect also performs the nonblocking poll enter needed +// to make completions visible. // Returns the number of CQEs retrieved, ErrCQOverflow when the ring enters CQ // overflow and no CQEs are immediately claimable, or iox.ErrWouldBlock if none // are available. @@ -104,9 +106,11 @@ func (ur *ioUring) waitBatchDirect(cqes []DirectCQE) (int, error) { h := atomic.LoadUint32(ur.cq.kHead) t := atomic.LoadUint32(ur.cq.kTail) if h == t { - err := ur.cqEmptyErr() - ur.unlockSubmitState() - return 0, err + if err := ur.observeCQEmptyLocked(); err != nil { + ur.unlockSubmitState() + return 0, err + } + continue } // Calculate batch size diff --git a/cqe_extended_linux.go b/cqe_extended_linux.go index 1131908..75a746b 100644 --- a/cqe_extended_linux.go +++ b/cqe_extended_linux.go @@ -94,6 +94,8 @@ func (c *ExtCQE) FD() iofd.FD { // // On single-issuer rings it is not safe for concurrent use with submit, Stop, // or ResizeRings; caller must serialize those operations. +// On IOPOLL rings WaitExtended also performs the nonblocking poll enter needed +// to make completions visible. // Caller-side completion code must keep completion referents reachable until // CQE reap and serialize retirement. // For multishot CQEs, return Ext to the pool only after !HasMore(). @@ -124,9 +126,11 @@ func (ur *ioUring) waitBatchExtended(cqes []ExtCQE) (int, error) { h := atomic.LoadUint32(ur.cq.kHead) t := atomic.LoadUint32(ur.cq.kTail) if h == t { - err := ur.cqEmptyErr() - ur.unlockSubmitState() - return 0, err + if err := ur.observeCQEmptyLocked(); err != nil { + ur.unlockSubmitState() + return 0, err + } + continue } // Calculate batch size diff --git a/interface_linux.go b/interface_linux.go index 703ea7c..acc96bb 100644 --- a/interface_linux.go +++ b/interface_linux.go @@ -315,7 +315,8 @@ func (ur *Uring) releaseRegisteredBufRings() error { // Wait flushes pending submissions, drives deferred task work when needed, and // collects completion events into cqes. On single-issuer rings it is not safe // for concurrent use with submit, Stop, or ResizeRings; caller must serialize -// those operations. It returns the number of events received, ErrCQOverflow +// those operations. On IOPOLL rings Wait also performs the nonblocking poll +// enter needed to make completions visible. It returns the number of events received, ErrCQOverflow // when the ring enters CQ overflow and no CQEs are immediately claimable, or // `iox.ErrWouldBlock` if the CQ is empty. // @@ -328,8 +329,8 @@ func (ur *Uring) releaseRegisteredBufRings() error { // n, err := ring.Wait(cqes) // for i := range n { // cqe := &cqes[i] -// if cqe.Res < 0 { -// handleCompletionError(cqe.Op(), cqe.Res) +// if err := cqe.Err(); err != nil { +// handleCompletionError(cqe.Op(), err) // continue // } // if cqe.Extended() { @@ -736,7 +737,8 @@ type SendTargets interface { // // Zero-copy notes: // - Produces two CQEs per send: completion (IORING_CQE_F_MORE) + notification -// - Buffer must not be modified until notification CQE is received +// - p must remain valid until all target sends complete +// - Zero-copy sends keep registered buffers immutable until notification CQE // - Requires TCP sockets; returns EOPNOTSUPP on Unix sockets or loopback // // Parameters: @@ -851,7 +853,7 @@ func (ur *Uring) Multicast(sqeCtx SQEContext, targets SendTargets, bufIndex int, // // Zero-copy notes: // - Produces two CQEs per send: completion (IORING_CQE_F_MORE) + notification -// - Buffer must not be modified until notification CQE is received +// - Registered buffers must remain immutable until notification CQE // - May return EOPNOTSUPP on Unix sockets or loopback func (ur *Uring) MulticastZeroCopy(sqeCtx SQEContext, targets SendTargets, bufIndex int, offset int64, n int, options ...OpOptionFunc) error { count := targets.Count() @@ -1133,7 +1135,8 @@ func (ur *Uring) TimeoutUpdate(sqeCtx SQEContext, userData uint64, d time.Durati return ur.timeoutUpdate(ctx, userData, ts, uflags) } -// AsyncCancel cancels a pending async operation. +// AsyncCancel cancels a pending async operation matched by the user_data value +// of the original SQE. func (ur *Uring) AsyncCancel(sqeCtx SQEContext, targetUserData uint64, options ...OpOptionFunc) error { flags := ur.asyncCancelOptions(options) ctx := sqeCtx.OrFlags(flags) @@ -1584,26 +1587,28 @@ func (ur *Uring) ZCRXExport(zcrxID uint32) (int, error) { // ======================================== // FSetXattr sets an extended attribute on a file descriptor. +// Caller must keep value valid until the operation completes. func (ur *Uring) FSetXattr(sqeCtx SQEContext, name string, value []byte, flags int, options ...OpOptionFunc) error { opFlags := ur.operationOptions(options) return ur.ioUring.fsetxattr(sqeCtx.OrFlags(opFlags), name, value, flags) } // SetXattr sets an extended attribute on a path. +// Caller must keep value valid until the operation completes. func (ur *Uring) SetXattr(sqeCtx SQEContext, path, name string, value []byte, flags int, options ...OpOptionFunc) error { opFlags := ur.operationOptions(options) return ur.ioUring.setxattr(sqeCtx.OrFlags(opFlags), path, name, value, flags) } // FGetXattr gets an extended attribute from a file descriptor. -// The result length is returned in the CQE. +// Caller must keep value valid until completion. The result length is returned in the CQE. func (ur *Uring) FGetXattr(sqeCtx SQEContext, name string, value []byte, options ...OpOptionFunc) error { flags := ur.operationOptions(options) return ur.ioUring.fgetxattr(sqeCtx.OrFlags(flags), name, value) } // GetXattr gets an extended attribute from a path. -// The result length is returned in the CQE. +// Caller must keep value valid until completion. The result length is returned in the CQE. func (ur *Uring) GetXattr(sqeCtx SQEContext, path, name string, value []byte, options ...OpOptionFunc) error { flags := ur.operationOptions(options) return ur.ioUring.getxattr(sqeCtx.OrFlags(flags), path, name, value) diff --git a/io_uring_linux.go b/io_uring_linux.go index febd937..b83aeac 100644 --- a/io_uring_linux.go +++ b/io_uring_linux.go @@ -827,27 +827,37 @@ func (ur *ioUring) sqCount() int { // avoid shared-submit locking. func (ur *ioUring) enter() error { ur.lockSubmitState() - if ur.closed.Load() || ur.sq.kFlags == nil || ur.sq.kHead == nil || ur.sq.kTail == nil || ur.sq.kRingMask == nil { + if ur.closed.Load() || ur.sq.kFlags == nil || ur.sq.kHead == nil || ur.sq.kTail == nil { ur.unlockSubmitState() return ErrClosed } - flags := atomic.LoadUint32(ur.sq.kFlags) - if flags&IORING_SQ_NEED_WAKEUP == IORING_SQ_NEED_WAKEUP { - _, err := ioUringEnter(ur.ringFd, uintptr(ur.params.sqEntries), 0, IORING_ENTER_SQ_WAKEUP) - // ErrExists/ErrInProgress means another enter is already in progress. - if err != nil && err != ErrExists && err != ErrInProgress { - ur.unlockSubmitState() - return err - } + if err := ur.wakeSQPollLocked(); err != nil { + ur.unlockSubmitState() + return err + } + if err := ur.flushSubmitLocked(); err != nil { + ur.unlockSubmitState() + return err } - - err := ur.enterPending() ur.unlockSubmitState() - return err + return nil } -func (ur *ioUring) enterPending() error { +func (ur *ioUring) wakeSQPollLocked() error { + flags := atomic.LoadUint32(ur.sq.kFlags) + if flags&IORING_SQ_NEED_WAKEUP == 0 { + return nil + } + _, err := ioUringEnter(ur.ringFd, uintptr(ur.params.sqEntries), 0, IORING_ENTER_SQ_WAKEUP) + // ErrExists/ErrInProgress means another enter is already in progress. + if err != nil && err != ErrExists && err != ErrInProgress { + return err + } + return nil +} + +func (ur *ioUring) flushSubmitLocked() error { // Atomic loads pair with publishSubmitSlot's stores. sqHead := atomic.LoadUint32(ur.sq.kHead) sqTail := atomic.LoadUint32(ur.sq.kTail) @@ -869,6 +879,28 @@ func (ur *ioUring) enterPending() error { return nil } +func (ur *ioUring) cqReady() bool { + h := atomic.LoadUint32(ur.cq.kHead) + t := atomic.LoadUint32(ur.cq.kTail) + return h != t +} + +// observeCQEmptyLocked performs the nonblocking IOPOLL visibility enter at the +// empty-CQ boundary. A nil return means the CQ became visible and callers should +// reread head/tail; a non-nil return is the terminal empty/error verdict. +func (ur *ioUring) observeCQEmptyLocked() error { + if ur.params.flags&IORING_SETUP_IOPOLL == 0 { + return ur.cqEmptyErr() + } + if err := ur.poll(0); err != nil { + return err + } + if ur.cqReady() { + return nil + } + return ur.cqEmptyErr() +} + func (ur *ioUring) poll(n int) error { if ur.params.flags&IORING_SETUP_IOPOLL == 0 { return nil @@ -924,9 +956,11 @@ func (ur *ioUring) wait() (*ioUringCqe, error) { for { h, t := atomic.LoadUint32(ur.cq.kHead), atomic.LoadUint32(ur.cq.kTail) if h == t { - err := ur.cqEmptyErr() - ur.unlockSubmitState() - return nil, err + if err := ur.observeCQEmptyLocked(); err != nil { + ur.unlockSubmitState() + return nil, err + } + continue } // Acquire barrier makes CQE contents visible after reading head and tail. @@ -959,9 +993,11 @@ func (ur *ioUring) waitBatch(cqes []CQEView) (int, error) { h := atomic.LoadUint32(ur.cq.kHead) t := atomic.LoadUint32(ur.cq.kTail) if h == t { - err := ur.cqEmptyErr() - ur.unlockSubmitState() - return 0, err + if err := ur.observeCQEmptyLocked(); err != nil { + ur.unlockSubmitState() + return 0, err + } + continue } // Calculate batch size: min(available, requested) From 28a4828760c25fc329ae2de2d542953fb25a9145 Mon Sep 17 00:00:00 2001 From: Robin He Date: Wed, 13 May 2026 17:16:45 +0900 Subject: [PATCH 03/13] fix: route multishot completions through owned subscriptions Signed-off-by: Robin He --- multishot.go | 102 +++++++++++++----- multishot_darwin.go | 5 + multishot_internal_linux_test.go | 174 ++++++++++++++++++++++++++++-- multishot_manual_dispatch_test.go | 29 ----- multishot_test.go | 6 +- 5 files changed, 251 insertions(+), 65 deletions(-) delete mode 100644 multishot_manual_dispatch_test.go diff --git a/multishot.go b/multishot.go index 1106993..da4fe40 100644 --- a/multishot.go +++ b/multishot.go @@ -123,7 +123,12 @@ const ( // 4. If callbacks stay enabled, one `OnMultishotStop` runs at most once // // Thread Safety: -// `Cancel` and `Unsubscribe` are safe from any goroutine. +// `Cancel` and `Unsubscribe` are safe for the subscription state itself, but +// cancel submission follows the ring's submit-state serialization contract. On +// default single-issuer rings, call them from the ring owner or otherwise +// serialize them with submit, Wait, WaitDirect, WaitExtended, Stop, and resize +// operations. On MultiIssuers rings, the shared-submit lock serializes the +// cancel SQE. // Observer callbacks run on the goroutine that dispatches the CQE, usually `Wait`. type MultishotSubscription struct { ring *Uring @@ -131,12 +136,16 @@ type MultishotSubscription struct { userData uint64 // Kernel-visible user_data for cancellation handler MultishotHandler // Convenience callback adapter; routing policy stays in the caller-side runtime state atomic.Uint32 // Long-lived subscription state - canceling atomic.Bool // Serializes cancel submission without claiming kernel progress early + cancelSubmit atomic.Bool // Protects userData from ExtSQE reuse while cancel SQE is being submitted unsubscribed atomic.Bool // Suppresses callbacks that have not started yet } -// Cancel asks the kernel to stop this multishot operation. -// The subscription remains live until a terminal CQE arrives. +// Cancel asks the kernel to stop this multishot operation. The subscription +// remains live until a terminal CQE arrives. Cancel follows the ring's +// submit-state serialization contract: on default single-issuer rings, call it +// from the ring owner or otherwise serialize it with submit, Wait, WaitDirect, +// WaitExtended, Stop, and resize operations; on MultiIssuers rings, the +// shared-submit lock serializes the cancel SQE. // It is safe to call more than once. // // If callbacks remain enabled, later CQEs may still deliver `OnMultishotStep` @@ -155,6 +164,8 @@ func (s *MultishotSubscription) Cancel() error { // In-flight callbacks may still finish after `Unsubscribe` returns. If the cancel // submission fails, the kernel request can remain live until it terminates // naturally, but further callbacks stay suppressed. +// Unsubscribe follows the same cancel submission serialization contract as +// [MultishotSubscription.Cancel]. func (s *MultishotSubscription) Unsubscribe() { s.unsubscribed.Store(true) _ = s.Cancel() @@ -175,6 +186,33 @@ func (s *MultishotSubscription) State() SubscriptionState { return SubscriptionState(s.state.Load()) } +// HandleCQE processes a copied CQE observation for this subscription. +// It returns true when the CQE belongs to this route and was handled. It returns +// false for non-extended CQEs, foreign routes, or stale observations whose +// pooled ExtSQE is no longer owned by this subscription. +// +// HandleCQE does not wait, retry, rearm, or resubmit. Caller-side runtime code +// owns polling cadence and any policy after the subscription reaches its +// terminal `!HasMore()` observation. +func (s *MultishotSubscription) HandleCQE(cqe CQEView) bool { + if !cqe.Extended() { + return false + } + if cqe.Context().Raw() != s.userData { + return false + } + ext := cqe.ExtSQE() + if ext == nil { + return false + } + owner, _ := extAnchors(ext).owner.(*MultishotSubscription) + if owner != s { + return false + } + s.handleCQEView(cqe) + return true +} + //go:nosplit func (s *MultishotSubscription) tryCancel() bool { return s.state.CompareAndSwap(uint32(SubscriptionActive), uint32(SubscriptionCancelling)) @@ -294,27 +332,25 @@ func multishotCQEHandler(ring *Uring, _ *ioUringSqe, cqe *ioUringCqe) { sub.handleCQE(cqe) } -func (s *MultishotSubscription) decodeStep(cqe *ioUringCqe) MultishotStep { - view := CQEView{ +func (s *MultishotSubscription) handleCQE(cqe *ioUringCqe) { + s.handleCQEView(CQEView{ Res: cqe.res, Flags: cqe.flags, - ctx: PackExtended(s.ext.Load()), - } - step := MultishotStep{CQE: view} - if view.Res < 0 { - step.Err = errFromErrno(uintptr(-view.Res)) - step.Cancelled = view.Res == -int32(ECANCELED) - } - return step + ctx: SQEContextFromRaw(cqe.userData), + }) } -func (s *MultishotSubscription) handleCQE(cqe *ioUringCqe) { +func (s *MultishotSubscription) handleCQEView(cqe CQEView) { if s.unsubscribed.Load() { s.retireIfTerminal(cqe) return } - step := s.decodeStep(cqe) + step := MultishotStep{CQE: cqe} + if cqe.Res < 0 { + step.Err = errFromErrno(uintptr(-cqe.Res)) + step.Cancelled = cqe.Res == -int32(ECANCELED) + } action := s.handler.OnMultishotStep(step) if step.Final() { s.finish(step.Err, step.Cancelled) @@ -337,12 +373,15 @@ func (s *MultishotSubscription) submitCancelUsing(submit func(uint64) error) err if s.State() != SubscriptionActive { return nil } - if !s.canceling.CompareAndSwap(false, true) { + if !s.cancelSubmit.CompareAndSwap(false, true) { + return nil + } + defer s.finishCancelSubmit() + if s.State() != SubscriptionActive { return nil } err := submit(s.userData) if err != nil { - s.canceling.Store(false) return err } if s.State() == SubscriptionActive { @@ -351,20 +390,35 @@ func (s *MultishotSubscription) submitCancelUsing(submit func(uint64) error) err return nil } +func (s *MultishotSubscription) finishCancelSubmit() { + s.cancelSubmit.Store(false) + if s.State() == SubscriptionStopped { + s.retireExt() + } +} + func (s *MultishotSubscription) finish(err error, cancelled bool) { - s.markStopped() + if SubscriptionState(s.state.Swap(uint32(SubscriptionStopped))) == SubscriptionStopped { + return + } if !s.unsubscribed.Load() { s.handler.OnMultishotStop(err, cancelled) } - s.retireExt() + if !s.cancelSubmit.Load() { + s.retireExt() + } } // retireIfTerminal retires the unsubscribed subscription once the terminal CQE // arrives after callbacks have been suppressed. -func (s *MultishotSubscription) retireIfTerminal(cqe *ioUringCqe) { - if cqe.flags&IORING_CQE_F_MORE == 0 { - s.markStopped() - s.retireExt() +func (s *MultishotSubscription) retireIfTerminal(cqe CQEView) { + if !cqe.HasMore() { + if SubscriptionState(s.state.Swap(uint32(SubscriptionStopped))) == SubscriptionStopped { + return + } + if !s.cancelSubmit.Load() { + s.retireExt() + } } } diff --git a/multishot_darwin.go b/multishot_darwin.go index 796ea57..9beb749 100644 --- a/multishot_darwin.go +++ b/multishot_darwin.go @@ -74,6 +74,11 @@ func (s *MultishotSubscription) State() SubscriptionState { return SubscriptionStopped } +// HandleCQE reports that multishot subscriptions are not supported on Darwin. +func (s *MultishotSubscription) HandleCQE(cqe CQEView) bool { + return false +} + // AcceptMultishot creates a multishot accept subscription (stub on Darwin). func (ur *Uring) AcceptMultishot(sqeCtx SQEContext, handler MultishotHandler, options ...OpOptionFunc) (*MultishotSubscription, error) { return nil, ErrNotSupported diff --git a/multishot_internal_linux_test.go b/multishot_internal_linux_test.go index ad1c0c9..bfed3d9 100644 --- a/multishot_internal_linux_test.go +++ b/multishot_internal_linux_test.go @@ -18,6 +18,8 @@ import ( const testLockedBufferMem = 1 << 18 +var benchmarkMultishotHandleCQESink bool + type noopMultishotHandler struct{} type stopOnProgressHandler struct{} @@ -305,10 +307,10 @@ func TestMultishotSubscriptionSubmitCancelStateTracksEnqueue(t *testing.T) { if got := sub.State(); got != SubscriptionActive { t.Fatalf("State after failed cancel enqueue: got %v, want %v", got, SubscriptionActive) } - if !sub.canceling.CompareAndSwap(false, true) { - t.Fatal("canceling flag remained set after failed enqueue") + if !sub.cancelSubmit.CompareAndSwap(false, true) { + t.Fatal("cancel-submit guard remained set after failed enqueue") } - sub.canceling.Store(false) + sub.cancelSubmit.Store(false) if err := sub.submitCancelUsing(func(uint64) error { return nil }); err != nil { t.Fatalf("submitCancelUsing(success): %v", err) @@ -349,9 +351,10 @@ func TestMultishotCancellingStillDeliversSteps(t *testing.T) { handler: handler, } sub.ext.Store(ext) + sub.userData = PackExtended(ext).Raw() sub.state.Store(uint32(SubscriptionCancelling)) - sub.handleCQE(&ioUringCqe{res: 7, flags: IORING_CQE_F_MORE}) + sub.handleCQE(&ioUringCqe{userData: sub.userData, res: 7, flags: IORING_CQE_F_MORE}) if got := len(handler.stepErrs); got != 1 { t.Fatalf("step callbacks while cancelling: got %d, want 1", got) @@ -386,19 +389,59 @@ func TestMultishotFinalSuccessSkipsCancelAndStops(t *testing.T) { sub.userData = PackExtended(ext).Raw() sub.state.Store(uint32(SubscriptionActive)) - sub.handleCQE(&ioUringCqe{res: 1, flags: 0}) + sub.handleCQE(&ioUringCqe{userData: sub.userData, res: 1, flags: 0}) if got := sub.State(); got != SubscriptionStopped { t.Fatalf("State after final CQE: got %v, want %v", got, SubscriptionStopped) } - if sub.canceling.Load() { - t.Fatal("canceling set after final CQE") + if sub.cancelSubmit.Load() { + t.Fatal("cancel-submit guard set after final CQE") } if sub.ext.Load() != nil { t.Fatal("ExtSQE not retired after final CQE") } } +func TestMultishotCancelSubmitRetainsExtUntilSubmitReturns(t *testing.T) { + pool := NewContextPools(1) + + ext := pool.Extended() + if ext == nil { + t.Fatal("pool exhausted") + } + + sub := &MultishotSubscription{ + ring: &Uring{ctxPools: pool}, + handler: noopMultishotHandler{}, + } + sub.ext.Store(ext) + sub.userData = PackExtended(ext).Raw() + sub.state.Store(uint32(SubscriptionActive)) + + err := sub.submitCancelUsing(func(uint64) error { + sub.finish(nil, false) + if sub.ext.Load() == nil { + t.Fatal("ExtSQE retired while cancel submit still used userData") + } + if got := pool.Extended(); got != nil { + t.Fatal("ExtSQE returned to pool while cancel submit still used userData") + } + return nil + }) + if err != nil { + t.Fatalf("submitCancelUsing: %v", err) + } + if got := sub.State(); got != SubscriptionStopped { + t.Fatalf("State after terminal/cancel race = %v, want %v", got, SubscriptionStopped) + } + if sub.ext.Load() != nil { + t.Fatal("ExtSQE not retired after cancel submit returned") + } + if got := pool.Extended(); got == nil { + t.Fatal("ExtSQE not returned to pool after cancel submit returned") + } +} + func TestMultishotFinalErrorDeliversErrorThenStopped(t *testing.T) { pool := NewContextPools(16) @@ -413,9 +456,10 @@ func TestMultishotFinalErrorDeliversErrorThenStopped(t *testing.T) { handler: handler, } sub.ext.Store(ext) + sub.userData = PackExtended(ext).Raw() sub.state.Store(uint32(SubscriptionActive)) - sub.handleCQE(&ioUringCqe{res: -int32(EINVAL), flags: 0}) + sub.handleCQE(&ioUringCqe{userData: sub.userData, res: -int32(EINVAL), flags: 0}) if got := len(handler.stepErrs); got != 1 { t.Fatalf("step callbacks: got %d, want 1", got) @@ -445,10 +489,11 @@ func TestMultishotUnsubscribedSuppressesCallbacks(t *testing.T) { handler: handler, } sub.ext.Store(ext) + sub.userData = PackExtended(ext).Raw() sub.state.Store(uint32(SubscriptionActive)) sub.unsubscribed.Store(true) - sub.handleCQE(&ioUringCqe{res: 1, flags: 0}) + sub.handleCQE(&ioUringCqe{userData: sub.userData, res: 1, flags: 0}) if got := len(handler.stepErrs); got != 0 { t.Fatalf("callbacks after unsubscribe: got %d, want 0", got) @@ -464,6 +509,117 @@ func TestMultishotUnsubscribedSuppressesCallbacks(t *testing.T) { } } +func TestMultishotHandleCQEClaimsRoute(t *testing.T) { + pool := NewContextPools(16) + + ext := pool.Extended() + if ext == nil { + t.Fatal("pool exhausted") + } + + handler := &recordingMultishotHandler{} + sub := &MultishotSubscription{ + ring: &Uring{ctxPools: pool}, + handler: handler, + } + sub.ext.Store(ext) + sub.userData = PackExtended(ext).Raw() + sub.state.Store(uint32(SubscriptionActive)) + extAnchors(ext).owner = sub + + progress := CQEView{Res: 11, Flags: IORING_CQE_F_MORE, ctx: PackExtended(ext)} + if !sub.HandleCQE(progress) { + t.Fatal("HandleCQE did not claim route progress CQE") + } + if got := len(handler.stepErrs); got != 1 { + t.Fatalf("step callbacks after progress: got %d, want 1", got) + } + if got := len(handler.stopErrs); got != 0 { + t.Fatalf("stop callbacks after progress: got %d, want 0", got) + } + if got := sub.State(); got != SubscriptionActive { + t.Fatalf("State after progress CQE: got %v, want %v", got, SubscriptionActive) + } + + final := CQEView{Res: 0, Flags: 0, ctx: PackExtended(ext)} + if !sub.HandleCQE(final) { + t.Fatal("HandleCQE did not claim route final CQE") + } + if got := len(handler.stepErrs); got != 2 { + t.Fatalf("step callbacks after final: got %d, want 2", got) + } + if got := len(handler.stopErrs); got != 1 { + t.Fatalf("stop callbacks after final: got %d, want 1", got) + } + if got := sub.State(); got != SubscriptionStopped { + t.Fatalf("State after final CQE: got %v, want %v", got, SubscriptionStopped) + } + if sub.ext.Load() != nil { + t.Fatal("ExtSQE not retired after handled final CQE") + } +} + +func BenchmarkMultishotHandleCQE(b *testing.B) { + pool := NewContextPools(16) + + ext := pool.Extended() + if ext == nil { + b.Fatal("pool exhausted") + } + + sub := &MultishotSubscription{ + ring: &Uring{ctxPools: pool}, + handler: noopMultishotHandler{}, + } + sub.ext.Store(ext) + sub.userData = PackExtended(ext).Raw() + sub.state.Store(uint32(SubscriptionActive)) + extAnchors(ext).owner = sub + + cqe := CQEView{Res: 11, Flags: IORING_CQE_F_MORE, ctx: PackExtended(ext)} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchmarkMultishotHandleCQESink = sub.HandleCQE(cqe) + } +} + +func TestMultishotHandleCQERejectsForeignRoute(t *testing.T) { + pool := NewContextPools(16) + + ext := pool.Extended() + if ext == nil { + t.Fatal("pool exhausted") + } + foreign := pool.Extended() + if foreign == nil { + t.Fatal("pool exhausted for foreign route") + } + + handler := &recordingMultishotHandler{} + sub := &MultishotSubscription{ + ring: &Uring{ctxPools: pool}, + handler: handler, + } + sub.ext.Store(ext) + sub.userData = PackExtended(ext).Raw() + sub.state.Store(uint32(SubscriptionActive)) + extAnchors(ext).owner = sub + + if sub.HandleCQE(CQEView{Res: 1, Flags: IORING_CQE_F_MORE, ctx: PackDirect(IORING_OP_ACCEPT, 0, 0, 0)}) { + t.Fatal("HandleCQE claimed direct CQE") + } + if sub.HandleCQE(CQEView{Res: 1, Flags: IORING_CQE_F_MORE, ctx: PackExtended(foreign)}) { + t.Fatal("HandleCQE claimed foreign extended CQE") + } + if got := len(handler.stepErrs); got != 0 { + t.Fatalf("step callbacks for rejected CQEs: got %d, want 0", got) + } + if got := sub.State(); got != SubscriptionActive { + t.Fatalf("State after rejected CQEs: got %v, want %v", got, SubscriptionActive) + } +} + func TestMultishotCancelRaceTerminalCQE(t *testing.T) { ring, err := New(func(opt *Options) { opt.LockedBufferMem = testLockedBufferMem diff --git a/multishot_manual_dispatch_test.go b/multishot_manual_dispatch_test.go deleted file mode 100644 index e0ded1e..0000000 --- a/multishot_manual_dispatch_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// ©Hayabusa Cloud Co., Ltd. 2026. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -//go:build linux - -package uring_test - -import ( - "code.hybscloud.com/uring" - "code.hybscloud.com/zcall" -) - -// dispatchSimplifiedMultishotCQE is a test helper for direct handler exercises. -// It intentionally skips the internal MultishotSubscription state machine and therefore -// must not be read as the authoritative runtime lifecycle. -func dispatchSimplifiedMultishotCQE(handler uring.MultishotHandler, cqe uring.CQEView) bool { - step := uring.MultishotStep{CQE: cqe} - if cqe.Res < 0 { - step.Err = zcall.Errno(uintptr(-cqe.Res)) - step.Cancelled = cqe.Res == -int32(uring.ECANCELED) - } - - keep := handler.OnMultishotStep(step) == uring.MultishotContinue - if step.Final() { - handler.OnMultishotStop(step.Err, step.Cancelled) - } - return keep -} diff --git a/multishot_test.go b/multishot_test.go index 3d76255..747575c 100644 --- a/multishot_test.go +++ b/multishot_test.go @@ -162,8 +162,8 @@ func TestSubmitAcceptMultishotCancelTerminates(t *testing.T) { } cancelCtx := uring.PackDirect(uring.IORING_OP_ASYNC_CANCEL, 0, 1, fd) - if err := ring.AsyncCancelFD(cancelCtx, false); err != nil { - t.Fatalf("AsyncCancelFD: %v", err) + if err := ring.AsyncCancel(cancelCtx, acceptCtx.Raw()); err != nil { + t.Fatalf("AsyncCancel: %v", err) } terminalAccept := false @@ -191,7 +191,7 @@ func TestSubmitAcceptMultishotCancelTerminates(t *testing.T) { terminalAccept = true } case uring.IORING_OP_ASYNC_CANCEL: - // AsyncCancelFD completion is expected alongside the terminal accept CQE. + // AsyncCancel completion is expected alongside the terminal accept CQE. } } if !terminalAccept { From c5525243f043edd3f68af97633263e2e8ef6e734 Mon Sep 17 00:00:00 2001 From: Robin He Date: Wed, 13 May 2026 17:18:36 +0900 Subject: [PATCH 04/13] docs: align completion-boundary usage guidance Signed-off-by: Robin He --- ctx.go | 4 +++- ctx_darwin.go | 2 ++ doc.go | 20 +++++++++++++++----- interface_darwin.go | 5 +++-- operations_linux.go | 2 +- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/ctx.go b/ctx.go index ade62f4..33d950f 100644 --- a/ctx.go +++ b/ctx.go @@ -261,7 +261,9 @@ func (c SQEContext) IndirectSQE() *IndirectSQE { } // ExtSQE stores a full SQE and 64 bytes of user data. -// Callers must stop using it after the matching pool release. +// Callers must stop using it after the matching pool release. The GC does not +// trace UserData; callers that place pointer-bearing values there must keep the +// referents reachable outside UserData. type ExtSQE struct { SQE ioUringSqe // 64 bytes - full system context UserData [64]byte // 64 bytes - flexible user interpretation diff --git a/ctx_darwin.go b/ctx_darwin.go index 0dafb05..c3098c5 100644 --- a/ctx_darwin.go +++ b/ctx_darwin.go @@ -226,6 +226,8 @@ type IndirectSQE struct { var _ [64 - unsafe.Sizeof(IndirectSQE{})]struct{} // ExtSQE stub for non-Linux (must match linux ExtSQE layout for shared Ctx types). +// The GC does not trace UserData; callers that place pointer-bearing values +// there must keep the referents reachable outside UserData. type ExtSQE struct { SQE ioUringSqe // 64 bytes UserData [64]byte // 64 bytes diff --git a/doc.go b/doc.go index ddff635..2947e6d 100644 --- a/doc.go +++ b/doc.go @@ -57,8 +57,8 @@ // if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { // continue // } -// if cqe.Res < 0 { -// return fmt.Errorf("uring read failed: res=%d", cqe.Res) +// if err := cqe.Err(); err != nil { +// return fmt.Errorf("uring read failed: %w", err) // } // handle(buf[:int(cqe.Res)]) // return nil @@ -73,14 +73,24 @@ // // sqeCtx := uring.ForFD(listenerFD) // sub, err := ring.AcceptMultishot(sqeCtx, handler) +// if err != nil { +// return err +// } // -// // Process CQEs - caller-side runtime code routes decoded CQEs +// // Process CQEs by routing this subscription first. // for i := range n { -// dispatch(handler, cqes[i]) +// if sub.HandleCQE(cqes[i]) { +// continue +// } +// dispatch(ring, cqes[i]) // } // // // Cancel when done -// sub.Cancel() +// // On single-issuer rings, call Cancel from the ring owner or otherwise +// // serialize it with submit, Wait, WaitDirect, WaitExtended, Stop, and resize operations. +// if err := sub.Cancel(); err != nil { +// return err +// } // // Listener setup advances with [DecodeListenerCQE], [PrepareListenerBind], // [PrepareListenerListen], and [SetListenerReady]. [ListenerManager] is a thin diff --git a/interface_darwin.go b/interface_darwin.go index 3fe1a42..c50ddfa 100644 --- a/interface_darwin.go +++ b/interface_darwin.go @@ -126,7 +126,7 @@ func newSetupOptions(opt Options, sqe128 bool) []func(params *ioUringParams) { // It wraps the kernel io_uring instance with buffer management and typed operations. // Default rings use the single-issuer fast path, so submit-state operations // are not safe for concurrent use by multiple goroutines; caller must -// serialize submit, Wait/enter, and Stop unless MultiIssuers is enabled. +// serialize submit, Wait, WaitDirect, WaitExtended, and Stop unless MultiIssuers is enabled. type Uring struct { *ioUring *Options @@ -856,7 +856,8 @@ func (ur *Uring) TimeoutUpdate(sqeCtx SQEContext, userData uint64, d time.Durati return ErrNotSupported } -// AsyncCancel cancels a pending async operation. +// AsyncCancel cancels a pending async operation matched by the user_data value +// of the original SQE. func (ur *Uring) AsyncCancel(sqeCtx SQEContext, targetUserData uint64, options ...OpOptionFunc) error { flags := ur.operationOptions(options) ctx := sqeCtx.OrFlags(flags) diff --git a/operations_linux.go b/operations_linux.go index cd3b4db..bde7dc7 100644 --- a/operations_linux.go +++ b/operations_linux.go @@ -325,7 +325,7 @@ func (ur *ioUring) acceptDirect(sqeCtx SQEContext, ioprio uint16, fileIndex uint return ur.submitPacked9(sqeCtx.WithOp(IORING_OP_ACCEPT), ioprio, 0, 0, 0, SOCK_NONBLOCK, 0, int32(kernelIndex)) } -// asyncCancel cancels a pending async operation. +// asyncCancel cancels a pending async operation matched by original SQE user_data. func (ur *ioUring) asyncCancel(sqeCtx SQEContext, targetUserData uint64) error { return ur.submitPacked3(sqeCtx.WithOp(IORING_OP_ASYNC_CANCEL), 0, targetUserData, 0) } From dc53472cf37edd82ab6a7d82c924638315e4d49c Mon Sep 17 00:00:00 2001 From: Robin He Date: Wed, 13 May 2026 17:20:29 +0900 Subject: [PATCH 05/13] chore: bump dependency versions Signed-off-by: Robin He --- go.mod | 16 ++++++++-------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 902f02e..49e93b1 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module code.hybscloud.com/uring go 1.26 require ( - code.hybscloud.com/dwcas v0.1.4 - code.hybscloud.com/framer v0.1.4 - code.hybscloud.com/iobuf v0.3.2 - code.hybscloud.com/iofd v0.3.4 - code.hybscloud.com/iox v0.3.4 - code.hybscloud.com/sock v0.3.2 - code.hybscloud.com/spin v0.1.5 - code.hybscloud.com/zcall v0.4.1 + code.hybscloud.com/dwcas v0.1.5 + code.hybscloud.com/framer v0.1.5 + code.hybscloud.com/iobuf v0.3.3 + code.hybscloud.com/iofd v0.3.6 + code.hybscloud.com/iox v0.3.6 + code.hybscloud.com/sock v0.3.3 + code.hybscloud.com/spin v0.1.6 + code.hybscloud.com/zcall v0.4.4 ) diff --git a/go.sum b/go.sum index 7844cd8..504f89b 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,16 @@ -code.hybscloud.com/dwcas v0.1.4 h1:twhAW4/O2SagaEyDm07u9OMaukGpfg0+id6nPAHoR9E= -code.hybscloud.com/dwcas v0.1.4/go.mod h1:XgYaxFoiBGTTM1fBJLhuPuG2xRfohYWzX1MvNUmW7dY= -code.hybscloud.com/framer v0.1.4 h1:qlX47R16bGX+Ts35wdrFBqVvxXoSKs018AzWJM+XC54= -code.hybscloud.com/framer v0.1.4/go.mod h1:xqw/V4xu9Lp9sIhuSnk0unqkct4VTdIJSafzBLazrgc= -code.hybscloud.com/iobuf v0.3.2 h1:9jwHb9ZKRWs9cSe0W8xGQixAXY0ZLQzsUh5NrgxZfw8= -code.hybscloud.com/iobuf v0.3.2/go.mod h1:6bp/JHdrA31nZYnhHVVRmN2Jpwm6A9Ynai0xtYMyBS4= -code.hybscloud.com/iofd v0.3.4 h1:RewH8MKtz0GUPaJZiEpssxJBzgS4V8J8iYowP8TVIts= -code.hybscloud.com/iofd v0.3.4/go.mod h1:BCt1A8Y9KzYuVesBVHVWYHfTxGdR8XFbTm6eq/nWu50= -code.hybscloud.com/iox v0.3.4 h1:2FMPyFH/AAkRAru/zKeo2oaIrmZgLdc4QDfVjKhGXCs= -code.hybscloud.com/iox v0.3.4/go.mod h1:LNxkaP3I0NWbnV2ia5zye8VMrk9kRCGbeIRZR2eGePA= -code.hybscloud.com/sock v0.3.2 h1:gUU8y8AZRG1YguPI1Hrd7s8HjS6qxrUEDzctUl7RYTM= -code.hybscloud.com/sock v0.3.2/go.mod h1:wWSGbZDe+FnKDsU+HASn4KrsTfR6NwIUNuRc0qAO0O4= -code.hybscloud.com/spin v0.1.5 h1:Xxi9mIMFmEl9JbCG8SEUnyfuNMT+6xdPu4twq8EqYK4= -code.hybscloud.com/spin v0.1.5/go.mod h1:BSOZ7kI43eIPohSVPQXZRf/KJYtRW3gJx82CwyLM64g= -code.hybscloud.com/zcall v0.4.1 h1:iyxUoDlz9a28oyW8IVAkVIygmhh6HKnJSxOvQENqBFM= -code.hybscloud.com/zcall v0.4.1/go.mod h1:KvMSYCr1JbOj6y/WNeGKGY+a8+Q0hN1egbhSgAp+NqQ= +code.hybscloud.com/dwcas v0.1.5 h1:eTDWcCYUmF1F8HGN7d0I7CIh9uqq3fETTeO587gWwnM= +code.hybscloud.com/dwcas v0.1.5/go.mod h1:XgYaxFoiBGTTM1fBJLhuPuG2xRfohYWzX1MvNUmW7dY= +code.hybscloud.com/framer v0.1.5 h1:iSnjt24AzwZPOmSInv8aDJN/T1oyOSdN1cyvaYe6SYA= +code.hybscloud.com/framer v0.1.5/go.mod h1:YuNOHOsb51+czOomfGOKsvq1tfF+hXYWPavFIv9d39c= +code.hybscloud.com/iobuf v0.3.3 h1:yUrh+C7UIZidP21bfh1hFCY+oVoWj0OJoKDhIOlRJGk= +code.hybscloud.com/iobuf v0.3.3/go.mod h1:C1ItcRUC2JBkrLnnjlDbYzZzCPPO50BHPmeRmca69VQ= +code.hybscloud.com/iofd v0.3.6 h1:J6exBCWB7dW4m4CjiPjZ/D1nltQZiCu0PzHCJ3QktLI= +code.hybscloud.com/iofd v0.3.6/go.mod h1:pXenWykMhsY2hzthmZjA5rn7mhRujgk+6JxJIiYkX8k= +code.hybscloud.com/iox v0.3.6 h1:8NX2eemmEO1F4P+R0h4gL1YA22Zi+ljD/5TxLsOw2go= +code.hybscloud.com/iox v0.3.6/go.mod h1:LNxkaP3I0NWbnV2ia5zye8VMrk9kRCGbeIRZR2eGePA= +code.hybscloud.com/sock v0.3.3 h1:i96Y7C5ZBRblMb7yfLgIrRQ8NcsM78mW2byigkNFGog= +code.hybscloud.com/sock v0.3.3/go.mod h1:m51pCIw7TPGVI1wh0ajCbtXZ2IFkE98mE0nAInPQgPg= +code.hybscloud.com/spin v0.1.6 h1:r8C+yyzriTVrktdgQcSGJKZ8qJ+eORkVDxba95/cBIU= +code.hybscloud.com/spin v0.1.6/go.mod h1:BSOZ7kI43eIPohSVPQXZRf/KJYtRW3gJx82CwyLM64g= +code.hybscloud.com/zcall v0.4.4 h1:/+C9PqnhLSomS85gb3yKeDDDPqfp7z/U+r+fF3YXorQ= +code.hybscloud.com/zcall v0.4.4/go.mod h1:KvMSYCr1JbOj6y/WNeGKGY+a8+Q0hN1egbhSgAp+NqQ= From 4df8f391fd17c6fd2b6ad1efbfdc2ea6b3d5f90f Mon Sep 17 00:00:00 2001 From: Robin He Date: Wed, 13 May 2026 18:00:33 +0900 Subject: [PATCH 06/13] docs: clarify serialization requirements Signed-off-by: Robin He --- cqe_direct_linux.go | 2 +- interface_linux.go | 13 +++++++------ submit_state.go | 5 +++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/cqe_direct_linux.go b/cqe_direct_linux.go index 3c4b947..d23b26f 100644 --- a/cqe_direct_linux.go +++ b/cqe_direct_linux.go @@ -22,7 +22,7 @@ import ( // (PackDirect) for all submissions. This avoids the 3-way mode check // that the generic Wait/CQEView path requires per-CQE. // -// Layout: 24 bytes (fits in 1/3 cache line, no padding needed) +// Layout: 16 bytes (fits in 1/4 cache line, no padding needed) type DirectCQE struct { Res int32 // Completion result (bytes transferred or negative errno) Flags uint32 // CQE flags (IORING_CQE_F_*) diff --git a/interface_linux.go b/interface_linux.go index acc96bb..76aec85 100644 --- a/interface_linux.go +++ b/interface_linux.go @@ -54,9 +54,10 @@ type Options struct { // MultiIssuers enables the shared-submit configuration for rings that accept // submissions from multiple goroutines. When false, New requests // SINGLE_ISSUER + DEFER_TASKRUN and callers must serialize submit-state - // operations such as submit, Wait/enter, Stop, and ring resize so the - // default fast path can skip shared synchronization. When true, it requests - // COOP_TASKRUN and keeps the shared-submit synchronization path. + // operations such as submit, Wait, WaitDirect, WaitExtended, Stop, and ring + // resize so the default fast path can skip shared synchronization. When + // true, it requests COOP_TASKRUN and keeps the shared-submit synchronization + // path. MultiIssuers bool // NotifySucceed ensures CQEs are generated for all successful operations. NotifySucceed bool @@ -166,8 +167,8 @@ func newSetupOptions(opt Options, sqe128 bool) []func(params *ioUringParams) { // It wraps the kernel io_uring instance with buffer management and typed operations. // Default rings use the single-issuer fast path, so submit-state operations // are not safe for concurrent use by multiple goroutines; caller must -// serialize submit, Wait/enter, Stop, and ResizeRings unless MultiIssuers is -// enabled. +// serialize submit, Wait, WaitDirect, WaitExtended, Stop, and ResizeRings +// unless MultiIssuers is enabled. type Uring struct { *ioUring *Options @@ -1538,7 +1539,7 @@ func (ur *Uring) CloneBuffersFromRegistered(srcRegisteredIdx int, srcOff, dstOff // ResizeRings resizes the SQ and CQ rings of this io_uring instance. // On single-issuer rings it is not safe for concurrent use with submit, -// Wait/enter, or Stop; caller must serialize those operations. +// Wait, WaitDirect, WaitExtended, or Stop; caller must serialize those operations. // This allows dynamic adjustment of ring sizes without recreating the ring. // // Requirements: diff --git a/submit_state.go b/submit_state.go index b739373..7b39d6a 100644 --- a/submit_state.go +++ b/submit_state.go @@ -15,8 +15,9 @@ import ( // lockSubmitState takes the shared-submit lock when submit-state operations may // arrive from multiple goroutines. Single-issuer rings intentionally skip this -// lock and rely on caller serialization of submit, Wait/enter, Stop, and -// ResizeRings so the hot path avoids shared synchronization overhead. +// lock and rely on caller serialization of submit, Wait, WaitDirect, +// WaitExtended, Stop, and ResizeRings so the hot path avoids shared +// synchronization overhead. func (ur *ioUring) lockSubmitState() { if !ur.submit.shared { return From 25b470862b90000a7018cd2160532c91300add42 Mon Sep 17 00:00:00 2001 From: Robin He Date: Wed, 13 May 2026 18:35:13 +0900 Subject: [PATCH 07/13] test: consolidate test coverage Signed-off-by: Robin He --- cq_overflow_test.go | 1 + cq_stride_linux_test.go | 120 ------------ cqe_extended_internal_test.go | 54 ------ cqe_view_test.go | 41 +++++ extended_sqe_reset_internal_linux_test.go | 106 ----------- interface_wrapper_internal_linux_test.go | 210 +++++++++++++++++++++ io_uring_darwin_internal_test.go | 94 ++++++++++ io_uring_linux_test.go | 213 ++++++++++++++++++++++ sqe_view_darwin_test.go | 103 ----------- testhelpers_linux_test.go | 17 ++ zerocopy_test.go | 21 ++- 11 files changed, 595 insertions(+), 385 deletions(-) delete mode 100644 cq_stride_linux_test.go delete mode 100644 cqe_extended_internal_test.go delete mode 100644 extended_sqe_reset_internal_linux_test.go delete mode 100644 sqe_view_darwin_test.go diff --git a/cq_overflow_test.go b/cq_overflow_test.go index 14e2c1d..7393ff9 100644 --- a/cq_overflow_test.go +++ b/cq_overflow_test.go @@ -21,6 +21,7 @@ func newCQOverflowTestRing() (*ioUring, *uint32, *uint32, *uint32) { overflow := uint32(0) return &ioUring{ + params: &ioUringParams{}, sq: ioUringSq{ kFlags: &sqFlags, }, diff --git a/cq_stride_linux_test.go b/cq_stride_linux_test.go deleted file mode 100644 index fe34bea..0000000 --- a/cq_stride_linux_test.go +++ /dev/null @@ -1,120 +0,0 @@ -// ©Hayabusa Cloud Co., Ltd. 2026. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -//go:build linux - -package uring - -import "testing" - -func newCQStrideTestRing() *ioUring { - head := uint32(0) - tail := uint32(2) - mask := uint32(1) - overflow := uint32(0) - - return &ioUring{ - cq: ioUringCq{ - kHead: &head, - kTail: &tail, - kRingMask: &mask, - kOverflow: &overflow, - cqes: make([]ioUringCqe, 4), - cqeStride: cqeStrideFromFlags(IORING_SETUP_CQE32), - }, - } -} - -func TestWaitUsesCQE32Stride(t *testing.T) { - ur := newCQStrideTestRing() - ur.cq.cqes[0] = ioUringCqe{userData: PackDirect(IORING_OP_NOP, 0, 0, 1).Raw(), res: 11} - ur.cq.cqes[2] = ioUringCqe{userData: PackDirect(IORING_OP_NOP, 0, 0, 2).Raw(), res: 22} - - first, err := ur.wait() - if err != nil { - t.Fatalf("wait first: %v", err) - } - if got := first.res; got != 11 { - t.Fatalf("first.res = %d, want 11", got) - } - - second, err := ur.wait() - if err != nil { - t.Fatalf("wait second: %v", err) - } - if got := second.res; got != 22 { - t.Fatalf("second.res = %d, want 22", got) - } - if got := SQEContextFromRaw(second.userData).FD(); got != 2 { - t.Fatalf("second.userData FD = %d, want 2", got) - } -} - -func TestWaitBatchUsesCQE32Stride(t *testing.T) { - ur := newCQStrideTestRing() - ur.cq.cqes[0] = ioUringCqe{userData: PackDirect(IORING_OP_NOP, 0, 0, 1).Raw(), res: 11} - ur.cq.cqes[2] = ioUringCqe{userData: PackDirect(IORING_OP_NOP, 0, 0, 2).Raw(), res: 22} - - cqes := make([]CQEView, 2) - n, err := ur.waitBatch(cqes) - if err != nil { - t.Fatalf("waitBatch: %v", err) - } - if n != 2 { - t.Fatalf("waitBatch count = %d, want 2", n) - } - if got := cqes[1].Res; got != 22 { - t.Fatalf("cqes[1].Res = %d, want 22", got) - } - if got := cqes[1].ctx.FD(); got != 2 { - t.Fatalf("cqes[1].ctx.FD = %d, want 2", got) - } -} - -func TestWaitBatchDirectUsesCQE32Stride(t *testing.T) { - ur := newCQStrideTestRing() - ur.cq.cqes[0] = ioUringCqe{userData: PackDirect(IORING_OP_NOP, IOSQE_IO_LINK, 3, 1).Raw(), res: 11} - ur.cq.cqes[2] = ioUringCqe{userData: PackDirect(IORING_OP_RECV, IOSQE_BUFFER_SELECT, 7, 2).Raw(), res: 22} - - cqes := make([]DirectCQE, 2) - n, err := ur.waitBatchDirect(cqes) - if err != nil { - t.Fatalf("waitBatchDirect: %v", err) - } - if n != 2 { - t.Fatalf("waitBatchDirect count = %d, want 2", n) - } - if got := cqes[1].Res; got != 22 { - t.Fatalf("cqes[1].Res = %d, want 22", got) - } - if got := cqes[1].FD; got != 2 { - t.Fatalf("cqes[1].FD = %d, want 2", got) - } - if got := cqes[1].BufGroup; got != 7 { - t.Fatalf("cqes[1].BufGroup = %d, want 7", got) - } -} - -func TestWaitBatchExtendedUsesCQE32Stride(t *testing.T) { - ur := newCQStrideTestRing() - ext1 := &ExtSQE{} - ext2 := &ExtSQE{} - ur.cq.cqes[0] = ioUringCqe{userData: PackExtended(ext1).Raw(), res: 11} - ur.cq.cqes[2] = ioUringCqe{userData: PackExtended(ext2).Raw(), res: 22} - - cqes := make([]ExtCQE, 2) - n, err := ur.waitBatchExtended(cqes) - if err != nil { - t.Fatalf("waitBatchExtended: %v", err) - } - if n != 2 { - t.Fatalf("waitBatchExtended count = %d, want 2", n) - } - if got := cqes[1].Res; got != 22 { - t.Fatalf("cqes[1].Res = %d, want 22", got) - } - if cqes[1].Ext != ext2 { - t.Fatalf("cqes[1].Ext = %p, want %p", cqes[1].Ext, ext2) - } -} diff --git a/cqe_extended_internal_test.go b/cqe_extended_internal_test.go deleted file mode 100644 index 78633f3..0000000 --- a/cqe_extended_internal_test.go +++ /dev/null @@ -1,54 +0,0 @@ -// ©Hayabusa Cloud Co., Ltd. 2026. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -//go:build linux - -package uring_test - -import ( - "testing" - - "code.hybscloud.com/iofd" - "code.hybscloud.com/uring" -) - -func TestExtCQEFields(t *testing.T) { - pool := uring.NewContextPools(16) - - ext := pool.Extended() - if ext == nil { - t.Fatal("pool exhausted") - } - defer pool.PutExtended(ext) - - if err := uring.PrepareListenerSocket(ext, uring.AF_INET, uring.SOCK_STREAM|uring.SOCK_CLOEXEC, uring.IPPROTO_TCP, nil, 128, nil); err != nil { - t.Fatalf("PrepareListenerSocket: %v", err) - } - uring.PrepareListenerBind(ext, iofd.FD(42)) - - cqe := uring.ExtCQE{ - Res: 7, - Flags: uring.IORING_CQE_F_MORE | uring.IORING_CQE_F_BUFFER | (19 << uring.IORING_CQE_BUFFER_SHIFT), - Ext: ext, - } - - if !cqe.IsSuccess() { - t.Fatal("IsSuccess: got false, want true") - } - if !cqe.HasMore() { - t.Fatal("HasMore: got false, want true") - } - if !cqe.HasBuffer() { - t.Fatal("HasBuffer: got false, want true") - } - if got := cqe.BufID(); got != 19 { - t.Fatalf("BufID: got %d, want 19", got) - } - if got := cqe.Op(); got != uring.IORING_OP_BIND { - t.Fatalf("Op: got %d, want %d", got, uring.IORING_OP_BIND) - } - if got := cqe.FD(); got != 42 { - t.Fatalf("FD: got %d, want 42", got) - } -} diff --git a/cqe_view_test.go b/cqe_view_test.go index aca7cde..d702035 100644 --- a/cqe_view_test.go +++ b/cqe_view_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "code.hybscloud.com/iofd" "code.hybscloud.com/iox" "code.hybscloud.com/uring" ) @@ -94,3 +95,43 @@ func TestCQEViewFlags(t *testing.T) { } t.Fatal("NOP completion not received") } + +func TestExtCQEFields(t *testing.T) { + pool := uring.NewContextPools(16) + + ext := pool.Extended() + if ext == nil { + t.Fatal("pool exhausted") + } + defer pool.PutExtended(ext) + + if err := uring.PrepareListenerSocket(ext, uring.AF_INET, uring.SOCK_STREAM|uring.SOCK_CLOEXEC, uring.IPPROTO_TCP, nil, 128, nil); err != nil { + t.Fatalf("PrepareListenerSocket: %v", err) + } + uring.PrepareListenerBind(ext, iofd.FD(42)) + + cqe := uring.ExtCQE{ + Res: 7, + Flags: uring.IORING_CQE_F_MORE | uring.IORING_CQE_F_BUFFER | (19 << uring.IORING_CQE_BUFFER_SHIFT), + Ext: ext, + } + + if !cqe.IsSuccess() { + t.Fatal("IsSuccess: got false, want true") + } + if !cqe.HasMore() { + t.Fatal("HasMore: got false, want true") + } + if !cqe.HasBuffer() { + t.Fatal("HasBuffer: got false, want true") + } + if got := cqe.BufID(); got != 19 { + t.Fatalf("BufID: got %d, want 19", got) + } + if got := cqe.Op(); got != uring.IORING_OP_BIND { + t.Fatalf("Op: got %d, want %d", got, uring.IORING_OP_BIND) + } + if got := cqe.FD(); got != 42 { + t.Fatalf("FD: got %d, want 42", got) + } +} diff --git a/extended_sqe_reset_internal_linux_test.go b/extended_sqe_reset_internal_linux_test.go deleted file mode 100644 index b03cdde..0000000 --- a/extended_sqe_reset_internal_linux_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// ©Hayabusa Cloud Co., Ltd. 2026. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -//go:build linux - -package uring - -import ( - "testing" - - "code.hybscloud.com/iofd" -) - -func poisonNextExtendedSQE(pool *ContextPools) { - pool.extended[0].ext.SQE = ioUringSqe{ - opcode: IORING_OP_SOCKET, - flags: 0xff, - ioprio: 0xffff, - fd: -99, - off: 99, - addr: 99, - len: 99, - uflags: 0xdeadbeef, - userData: 99, - bufIndex: 7, - personality: 3, - spliceFdIn: 11, - pad: [2]uint64{13, 17}, - } -} - -func assertNoStaleExtendedSQEFields(t *testing.T, sqe *ioUringSqe, wantBufIndex uint16) { - t.Helper() - - if got := sqe.off; got != 0 { - t.Fatalf("SQE.off = %d, want 0", got) - } - if got := sqe.uflags; got != 0 { - t.Fatalf("SQE.uflags = %#x, want 0", got) - } - if got := sqe.bufIndex; got != wantBufIndex { - t.Fatalf("SQE.bufIndex = %d, want %d", got, wantBufIndex) - } - if got := sqe.personality; got != 0 { - t.Fatalf("SQE.personality = %d, want 0", got) - } - if got := sqe.spliceFdIn; got != 0 { - t.Fatalf("SQE.spliceFdIn = %d, want 0", got) - } - if got := sqe.pad; got != [2]uint64{} { - t.Fatalf("SQE.pad = %#v, want zero", got) - } -} - -func TestIncrementalRecvClearsBorrowedExtendedSQE(t *testing.T) { - ring := newWrapperTestRing(t) - pool := NewContextPools(1) - poisonNextExtendedSQE(pool) - - receiver := NewIncrementalReceiver(ring, pool, 7, 256, make([]byte, 256), 1) - if err := receiver.Recv(11, nil); err != nil { - t.Fatalf("Recv: %v", err) - } - - sqe := lastSubmittedSQE(t, ring) - if got, want := sqe.opcode, uint8(IORING_OP_RECV); got != want { - t.Fatalf("SQE.opcode = %d, want %d", got, want) - } - assertNoStaleExtendedSQEFields(t, sqe, 7) -} - -func TestZCTrackerSendZCClearsBorrowedExtendedSQE(t *testing.T) { - ring := newWrapperTestRing(t) - pool := NewContextPools(1) - poisonNextExtendedSQE(pool) - - tracker := NewZCTracker(ring, pool) - if err := tracker.SendZC(iofd.FD(11), []byte("payload"), nil); err != nil { - t.Fatalf("SendZC: %v", err) - } - - sqe := lastSubmittedSQE(t, ring) - if got, want := sqe.opcode, uint8(IORING_OP_SEND_ZC); got != want { - t.Fatalf("SQE.opcode = %d, want %d", got, want) - } - assertNoStaleExtendedSQEFields(t, sqe, 0) -} - -func TestZCTrackerSendZCFixedClearsBorrowedExtendedSQE(t *testing.T) { - ring := newWrapperTestRing(t) - ring.bufs = [][]byte{[]byte("registered-payload")} - pool := NewContextPools(1) - poisonNextExtendedSQE(pool) - - tracker := NewZCTracker(ring, pool) - if err := tracker.SendZCFixed(iofd.FD(11), 0, 0, 10, nil); err != nil { - t.Fatalf("SendZCFixed: %v", err) - } - - sqe := lastSubmittedSQE(t, ring) - if got, want := sqe.opcode, uint8(IORING_OP_SEND_ZC); got != want { - t.Fatalf("SQE.opcode = %d, want %d", got, want) - } - assertNoStaleExtendedSQEFields(t, sqe, 0) -} diff --git a/interface_wrapper_internal_linux_test.go b/interface_wrapper_internal_linux_test.go index fa28247..dc17a50 100644 --- a/interface_wrapper_internal_linux_test.go +++ b/interface_wrapper_internal_linux_test.go @@ -11,6 +11,8 @@ import ( "net" "testing" "unsafe" + + "code.hybscloud.com/iofd" ) func assertZeroedSQETail(t *testing.T, sqe *ioUringSqe) { @@ -1097,3 +1099,211 @@ func TestMsgRingFDWrapperEncodesExpectedSQEs(t *testing.T) { }) } } + +func newRegistrationFailureTestRing() *Uring { + head, tail, mask := uint32(0), uint32(0), uint32(EntriesNano-1) + return &Uring{ + ioUring: &ioUring{ + ringFd: -1, + params: &ioUringParams{ + sqEntries: EntriesNano, + cqEntries: EntriesNano * 2, + }, + sq: ioUringSq{ + kHead: &head, + kTail: &tail, + kRingMask: &mask, + }, + }, + Features: &Features{ + SQEntries: EntriesNano, + CQEntries: EntriesNano * 2, + }, + } +} + +func TestAdvancedRegistrationWrappersReturnKernelErrors(t *testing.T) { + ring := newRegistrationFailureTestRing() + area := ZCRXAreaReg{} + region := RegionDesc{} + + tests := []struct { + name string + call func() error + }{ + { + name: "RegisterZCRXIfq", + call: func() error { + _, _, err := ring.RegisterZCRXIfq(1, 2, 4, &area, ®ion, 0) + return err + }, + }, + { + name: "ZCRXFlushRQ", + call: func() error { return ring.ZCRXFlushRQ(1) }, + }, + { + name: "QueryZCRX", + call: func() error { + _, err := ring.QueryZCRX() + return err + }, + }, + { + name: "QuerySCQ", + call: func() error { + _, err := ring.QuerySCQ() + return err + }, + }, + { + name: "RegisterMemRegion", + call: func() error { return ring.RegisterMemRegion(®ion, 0) }, + }, + { + name: "RegisterNAPI", + call: func() error { return ring.RegisterNAPI(0, true, IO_URING_NAPI_TRACKING_STATIC) }, + }, + { + name: "NAPIAddStaticID", + call: func() error { return ring.NAPIAddStaticID(7) }, + }, + { + name: "NAPIDelStaticID", + call: func() error { return ring.NAPIDelStaticID(7) }, + }, + { + name: "UnregisterNAPI", + call: func() error { return ring.UnregisterNAPI() }, + }, + { + name: "CloneBuffers", + call: func() error { return ring.CloneBuffers(-1, 1, 2, 3, true) }, + }, + { + name: "CloneBuffersFromRegistered", + call: func() error { return ring.CloneBuffersFromRegistered(5, 1, 2, 3, true) }, + }, + { + name: "ZCRXExport", + call: func() error { + _, err := ring.ZCRXExport(1) + return err + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.call(); err == nil { + t.Fatal("error = nil, want kernel error from invalid ring fd") + } + }) + } +} + +func TestResizeRingsPropagatesRegisterFailure(t *testing.T) { + ring := newRegistrationFailureTestRing() + if err := ring.ResizeRings(EntriesNano, EntriesNano*2); err == nil { + t.Fatal("ResizeRings error = nil, want kernel error from invalid ring fd") + } + if ring.Features.SQEntries != EntriesNano { + t.Fatalf("SQEntries = %d, want %d", ring.Features.SQEntries, EntriesNano) + } + if ring.Features.CQEntries != EntriesNano*2 { + t.Fatalf("CQEntries = %d, want %d", ring.Features.CQEntries, EntriesNano*2) + } +} + +func poisonNextExtendedSQE(pool *ContextPools) { + pool.extended[0].ext.SQE = ioUringSqe{ + opcode: IORING_OP_SOCKET, + flags: 0xff, + ioprio: 0xffff, + fd: -99, + off: 99, + addr: 99, + len: 99, + uflags: 0xdeadbeef, + userData: 99, + bufIndex: 7, + personality: 3, + spliceFdIn: 11, + pad: [2]uint64{13, 17}, + } +} + +func assertNoStaleExtendedSQEFields(t *testing.T, sqe *ioUringSqe, wantBufIndex uint16) { + t.Helper() + + if got := sqe.off; got != 0 { + t.Fatalf("SQE.off = %d, want 0", got) + } + if got := sqe.uflags; got != 0 { + t.Fatalf("SQE.uflags = %#x, want 0", got) + } + if got := sqe.bufIndex; got != wantBufIndex { + t.Fatalf("SQE.bufIndex = %d, want %d", got, wantBufIndex) + } + if got := sqe.personality; got != 0 { + t.Fatalf("SQE.personality = %d, want 0", got) + } + if got := sqe.spliceFdIn; got != 0 { + t.Fatalf("SQE.spliceFdIn = %d, want 0", got) + } + if got := sqe.pad; got != [2]uint64{} { + t.Fatalf("SQE.pad = %#v, want zero", got) + } +} + +func TestIncrementalRecvClearsBorrowedExtendedSQE(t *testing.T) { + ring := newWrapperTestRing(t) + pool := NewContextPools(1) + poisonNextExtendedSQE(pool) + + receiver := NewIncrementalReceiver(ring, pool, 7, 256, make([]byte, 256), 1) + if err := receiver.Recv(11, nil); err != nil { + t.Fatalf("Recv: %v", err) + } + + sqe := lastSubmittedSQE(t, ring) + if got, want := sqe.opcode, uint8(IORING_OP_RECV); got != want { + t.Fatalf("SQE.opcode = %d, want %d", got, want) + } + assertNoStaleExtendedSQEFields(t, sqe, 7) +} + +func TestZCTrackerSendZCClearsBorrowedExtendedSQE(t *testing.T) { + ring := newWrapperTestRing(t) + pool := NewContextPools(1) + poisonNextExtendedSQE(pool) + + tracker := NewZCTracker(ring, pool) + if err := tracker.SendZC(iofd.FD(11), []byte("payload"), nil); err != nil { + t.Fatalf("SendZC: %v", err) + } + + sqe := lastSubmittedSQE(t, ring) + if got, want := sqe.opcode, uint8(IORING_OP_SEND_ZC); got != want { + t.Fatalf("SQE.opcode = %d, want %d", got, want) + } + assertNoStaleExtendedSQEFields(t, sqe, 0) +} + +func TestZCTrackerSendZCFixedClearsBorrowedExtendedSQE(t *testing.T) { + ring := newWrapperTestRing(t) + ring.bufs = [][]byte{[]byte("registered-payload")} + pool := NewContextPools(1) + poisonNextExtendedSQE(pool) + + tracker := NewZCTracker(ring, pool) + if err := tracker.SendZCFixed(iofd.FD(11), 0, 0, 10, nil); err != nil { + t.Fatalf("SendZCFixed: %v", err) + } + + sqe := lastSubmittedSQE(t, ring) + if got, want := sqe.opcode, uint8(IORING_OP_SEND_ZC); got != want { + t.Fatalf("SQE.opcode = %d, want %d", got, want) + } + assertNoStaleExtendedSQEFields(t, sqe, 0) +} diff --git a/io_uring_darwin_internal_test.go b/io_uring_darwin_internal_test.go index ec33b90..ff52ff5 100644 --- a/io_uring_darwin_internal_test.go +++ b/io_uring_darwin_internal_test.go @@ -72,3 +72,97 @@ func TestDarwinDoCloseRejectsEmptyRegisteredFileSlot(t *testing.T) { t.Fatalf("sqeFile should stay open on EBADF: %v", err) } } + +func TestDarwinSQEView(t *testing.T) { + t.Run("ViewSQE from IndirectSQE", func(t *testing.T) { + indirect := &IndirectSQE{} + indirect.opcode = IORING_OP_RECV + indirect.flags = IOSQE_BUFFER_SELECT | IOSQE_ASYNC + indirect.ioprio = 42 + indirect.fd = 100 + indirect.off = 0x1234567890 + indirect.addr = 0xDEADBEEF + indirect.len = 4096 + indirect.uflags = 0x8000 + indirect.bufIndex = 7 + indirect.personality = 3 + indirect.spliceFdIn = 50 + indirect.userData = PackDirect(IORING_OP_RECV, indirect.flags, indirect.bufIndex, indirect.fd).Raw() + + view := ViewSQE(indirect) + + if !view.Valid() { + t.Error("expected Valid() to be true") + } + if view.Opcode() != IORING_OP_RECV { + t.Errorf("Opcode() = %d, want %d", view.Opcode(), IORING_OP_RECV) + } + if view.Flags() != IOSQE_BUFFER_SELECT|IOSQE_ASYNC { + t.Errorf("Flags() = %d, want %d", view.Flags(), IOSQE_BUFFER_SELECT|IOSQE_ASYNC) + } + if view.IoPrio() != 42 { + t.Errorf("IoPrio() = %d, want 42", view.IoPrio()) + } + if view.RawFD() != 100 { + t.Errorf("RawFD() = %d, want 100", view.RawFD()) + } + if view.Off() != 0x1234567890 { + t.Errorf("Off() = %x, want 0x1234567890", view.Off()) + } + if view.Addr() != 0xDEADBEEF { + t.Errorf("Addr() = %x, want 0xDEADBEEF", view.Addr()) + } + if view.Len() != 4096 { + t.Errorf("Len() = %d, want 4096", view.Len()) + } + if view.UFlags() != 0x8000 { + t.Errorf("UFlags() = %x, want 0x8000", view.UFlags()) + } + if view.UserData() != indirect.userData { + t.Errorf("UserData() = %x, want %x", view.UserData(), indirect.userData) + } + if view.BufIndex() != 7 { + t.Errorf("BufIndex() = %d, want 7", view.BufIndex()) + } + if view.BufGroup() != 7 { + t.Errorf("BufGroup() = %d, want 7", view.BufGroup()) + } + if view.Personality() != 3 { + t.Errorf("Personality() = %d, want 3", view.Personality()) + } + if view.SpliceFDIn() != 50 { + t.Errorf("SpliceFDIn() = %d, want 50", view.SpliceFDIn()) + } + if view.FileIndex() != 50 { + t.Errorf("FileIndex() = %d, want 50", view.FileIndex()) + } + if !view.HasBufferSelect() { + t.Error("expected HasBufferSelect() true") + } + if !view.HasAsync() { + t.Error("expected HasAsync() true") + } + }) + + t.Run("ViewExtSQE from ExtSQE", func(t *testing.T) { + ext := &ExtSQE{} + ext.SQE.opcode = IORING_OP_SEND + ext.SQE.flags = IOSQE_CQE_SKIP_SUCCESS + ext.SQE.fd = 200 + + view := ViewExtSQE(ext) + + if !view.Valid() { + t.Error("expected Valid() to be true") + } + if view.Opcode() != IORING_OP_SEND { + t.Errorf("Opcode() = %d, want %d", view.Opcode(), IORING_OP_SEND) + } + if view.RawFD() != 200 { + t.Errorf("RawFD() = %d, want 200", view.RawFD()) + } + if !view.HasCQESkipSuccess() { + t.Error("expected HasCQESkipSuccess() to be true") + } + }) +} diff --git a/io_uring_linux_test.go b/io_uring_linux_test.go index fca4572..b1a48d8 100644 --- a/io_uring_linux_test.go +++ b/io_uring_linux_test.go @@ -909,3 +909,216 @@ func TestCqRingAdvanceZero(t *testing.T) { t.Fatalf("cq.advance(0) changed head: %d -> %d", headBefore, headAfter) } } + +func TestCQReady(t *testing.T) { + var head uint32 + var tail uint32 + ur := &ioUring{params: &ioUringParams{}, cq: ioUringCq{kHead: &head, kTail: &tail}} + + if ur.cqReady() { + t.Fatal("empty CQ reported ready") + } + + tail = 1 + if !ur.cqReady() { + t.Fatal("visible CQE was not reported ready") + } + + head = 1 + if ur.cqReady() { + t.Fatal("reaped CQ reported ready") + } +} + +func TestObserveCQEmptyLockedNonIOPOLL(t *testing.T) { + var head uint32 + var tail uint32 + ur := &ioUring{params: &ioUringParams{}, cq: ioUringCq{kHead: &head, kTail: &tail}} + + if err := ur.observeCQEmptyLocked(); !errors.Is(err, iox.ErrWouldBlock) { + t.Fatalf("observeCQEmptyLocked() = %v, want %v", err, iox.ErrWouldBlock) + } +} + +func TestRingLayoutHelpers(t *testing.T) { + if got, want := sqeBytesFromFlags(0), int(unsafe.Sizeof(ioUringSqe{})); got != want { + t.Fatalf("sqeBytesFromFlags(0) = %d, want %d", got, want) + } + if got, want := sqeBytesFromFlags(IORING_SETUP_SQE128), int(unsafe.Sizeof(ioUringSqe128{})); got != want { + t.Fatalf("sqeBytesFromFlags(SQE128) = %d, want %d", got, want) + } + if got, want := sqeStrideFromFlags(IORING_SETUP_SQE128), unsafe.Sizeof(ioUringSqe128{}); got != want { + t.Fatalf("sqeStrideFromFlags(SQE128) = %d, want %d", got, want) + } + if got, want := cqeBytesFromFlags(0), int(unsafe.Sizeof(ioUringCqe{})); got != want { + t.Fatalf("cqeBytesFromFlags(0) = %d, want %d", got, want) + } + if got, want := cqeBytesFromFlags(IORING_SETUP_CQE32), 2*int(unsafe.Sizeof(ioUringCqe{})); got != want { + t.Fatalf("cqeBytesFromFlags(CQE32) = %d, want %d", got, want) + } + if got := cqeStrideFromFlags(IORING_SETUP_CQE32); got != 2 { + t.Fatalf("cqeStrideFromFlags(CQE32) = %d, want 2", got) + } + + ur := &ioUring{ + sq: ioUringSq{ringSz: 64}, + cq: ioUringCq{ringSz: 96}, + ops: []ioUringProbeOp{{op: IORING_OP_NOP}}, + } + if got := ur.ringMapSz(); got != 96 { + t.Fatalf("ringMapSz() = %d, want 96", got) + } + if !ur.supportsOpcode(IORING_OP_NOP) { + t.Fatal("supportsOpcode did not find registered NOP") + } + if ur.supportsOpcode(IORING_OP_READ) { + t.Fatal("supportsOpcode found unregistered READ") + } + + var head uint32 = 1 + var tail uint32 = 4 + ur.sq.kHead = &head + ur.sq.kTail = &tail + if got := ur.sqCount(); got != 3 { + t.Fatalf("sqCount() = %d, want 3", got) + } +} + +func TestSubmitSlotResetAndInlineCmdData(t *testing.T) { + sqe := ioUringSqe{opcode: IORING_OP_NOP, fd: 7, len: 3} + slot := submitSlot{sqe: &sqe} + if data := slot.inlineCmdData128(); data != nil { + t.Fatalf("inlineCmdData128() = %v, want nil for 64-byte SQE", data) + } + slot.reset() + if sqe != (ioUringSqe{}) { + t.Fatalf("reset 64-byte SQE = %+v, want zero", sqe) + } + + sqe128 := ioUringSqe128{ioUringSqe: ioUringSqe{opcode: IORING_OP_URING_CMD}} + slot = submitSlot{sqe: &sqe, sqe128: &sqe128} + data := slot.inlineCmdData128() + if len(data) != uringCmd128DataMax { + t.Fatalf("inlineCmdData128 len = %d, want %d", len(data), uringCmd128DataMax) + } + data[0] = 0xaa + data[len(data)-1] = 0xbb + if sqe128.pad[0] == 0 || sqe128.extra[len(sqe128.extra)-1] != 0xbb { + t.Fatal("inlineCmdData128 did not expose the inline command data region") + } + slot.reset() + if sqe128 != (ioUringSqe128{}) { + t.Fatalf("reset 128-byte SQE = %+v, want zero", sqe128) + } +} + +func newCQStrideTestRing() *ioUring { + head := uint32(0) + tail := uint32(2) + mask := uint32(1) + overflow := uint32(0) + + return &ioUring{ + cq: ioUringCq{ + kHead: &head, + kTail: &tail, + kRingMask: &mask, + kOverflow: &overflow, + cqes: make([]ioUringCqe, 4), + cqeStride: cqeStrideFromFlags(IORING_SETUP_CQE32), + }, + } +} + +func TestWaitUsesCQE32Stride(t *testing.T) { + ur := newCQStrideTestRing() + ur.cq.cqes[0] = ioUringCqe{userData: PackDirect(IORING_OP_NOP, 0, 0, 1).Raw(), res: 11} + ur.cq.cqes[2] = ioUringCqe{userData: PackDirect(IORING_OP_NOP, 0, 0, 2).Raw(), res: 22} + + first, err := ur.wait() + if err != nil { + t.Fatalf("wait first: %v", err) + } + if got := first.res; got != 11 { + t.Fatalf("first.res = %d, want 11", got) + } + + second, err := ur.wait() + if err != nil { + t.Fatalf("wait second: %v", err) + } + if got := second.res; got != 22 { + t.Fatalf("second.res = %d, want 22", got) + } + if got := SQEContextFromRaw(second.userData).FD(); got != 2 { + t.Fatalf("second.userData FD = %d, want 2", got) + } +} + +func TestWaitBatchUsesCQE32Stride(t *testing.T) { + ur := newCQStrideTestRing() + ur.cq.cqes[0] = ioUringCqe{userData: PackDirect(IORING_OP_NOP, 0, 0, 1).Raw(), res: 11} + ur.cq.cqes[2] = ioUringCqe{userData: PackDirect(IORING_OP_NOP, 0, 0, 2).Raw(), res: 22} + + cqes := make([]CQEView, 2) + n, err := ur.waitBatch(cqes) + if err != nil { + t.Fatalf("waitBatch: %v", err) + } + if n != 2 { + t.Fatalf("waitBatch count = %d, want 2", n) + } + if got := cqes[1].Res; got != 22 { + t.Fatalf("cqes[1].Res = %d, want 22", got) + } + if got := cqes[1].ctx.FD(); got != 2 { + t.Fatalf("cqes[1].ctx.FD = %d, want 2", got) + } +} + +func TestWaitBatchDirectUsesCQE32Stride(t *testing.T) { + ur := newCQStrideTestRing() + ur.cq.cqes[0] = ioUringCqe{userData: PackDirect(IORING_OP_NOP, IOSQE_IO_LINK, 3, 1).Raw(), res: 11} + ur.cq.cqes[2] = ioUringCqe{userData: PackDirect(IORING_OP_RECV, IOSQE_BUFFER_SELECT, 7, 2).Raw(), res: 22} + + cqes := make([]DirectCQE, 2) + n, err := ur.waitBatchDirect(cqes) + if err != nil { + t.Fatalf("waitBatchDirect: %v", err) + } + if n != 2 { + t.Fatalf("waitBatchDirect count = %d, want 2", n) + } + if got := cqes[1].Res; got != 22 { + t.Fatalf("cqes[1].Res = %d, want 22", got) + } + if got := cqes[1].FD; got != 2 { + t.Fatalf("cqes[1].FD = %d, want 2", got) + } + if got := cqes[1].BufGroup; got != 7 { + t.Fatalf("cqes[1].BufGroup = %d, want 7", got) + } +} + +func TestWaitBatchExtendedUsesCQE32Stride(t *testing.T) { + ur := newCQStrideTestRing() + ext1 := &ExtSQE{} + ext2 := &ExtSQE{} + ur.cq.cqes[0] = ioUringCqe{userData: PackExtended(ext1).Raw(), res: 11} + ur.cq.cqes[2] = ioUringCqe{userData: PackExtended(ext2).Raw(), res: 22} + + cqes := make([]ExtCQE, 2) + n, err := ur.waitBatchExtended(cqes) + if err != nil { + t.Fatalf("waitBatchExtended: %v", err) + } + if n != 2 { + t.Fatalf("waitBatchExtended count = %d, want 2", n) + } + if got := cqes[1].Res; got != 22 { + t.Fatalf("cqes[1].Res = %d, want 22", got) + } + if cqes[1].Ext != ext2 { + t.Fatalf("cqes[1].Ext = %p, want %p", cqes[1].Ext, ext2) + } +} diff --git a/sqe_view_darwin_test.go b/sqe_view_darwin_test.go deleted file mode 100644 index 6c06344..0000000 --- a/sqe_view_darwin_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// ©Hayabusa Cloud Co., Ltd. 2026. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -//go:build darwin - -package uring - -import "testing" - -func TestDarwinSQEView(t *testing.T) { - t.Run("ViewSQE from IndirectSQE", func(t *testing.T) { - indirect := &IndirectSQE{} - indirect.opcode = IORING_OP_RECV - indirect.flags = IOSQE_BUFFER_SELECT | IOSQE_ASYNC - indirect.ioprio = 42 - indirect.fd = 100 - indirect.off = 0x1234567890 - indirect.addr = 0xDEADBEEF - indirect.len = 4096 - indirect.uflags = 0x8000 - indirect.bufIndex = 7 - indirect.personality = 3 - indirect.spliceFdIn = 50 - indirect.userData = PackDirect(IORING_OP_RECV, indirect.flags, indirect.bufIndex, indirect.fd).Raw() - - view := ViewSQE(indirect) - - if !view.Valid() { - t.Error("expected Valid() to be true") - } - if view.Opcode() != IORING_OP_RECV { - t.Errorf("Opcode() = %d, want %d", view.Opcode(), IORING_OP_RECV) - } - if view.Flags() != IOSQE_BUFFER_SELECT|IOSQE_ASYNC { - t.Errorf("Flags() = %d, want %d", view.Flags(), IOSQE_BUFFER_SELECT|IOSQE_ASYNC) - } - if view.IoPrio() != 42 { - t.Errorf("IoPrio() = %d, want 42", view.IoPrio()) - } - if view.RawFD() != 100 { - t.Errorf("RawFD() = %d, want 100", view.RawFD()) - } - if view.Off() != 0x1234567890 { - t.Errorf("Off() = %x, want 0x1234567890", view.Off()) - } - if view.Addr() != 0xDEADBEEF { - t.Errorf("Addr() = %x, want 0xDEADBEEF", view.Addr()) - } - if view.Len() != 4096 { - t.Errorf("Len() = %d, want 4096", view.Len()) - } - if view.UFlags() != 0x8000 { - t.Errorf("UFlags() = %x, want 0x8000", view.UFlags()) - } - if view.UserData() != indirect.userData { - t.Errorf("UserData() = %x, want %x", view.UserData(), indirect.userData) - } - if view.BufIndex() != 7 { - t.Errorf("BufIndex() = %d, want 7", view.BufIndex()) - } - if view.BufGroup() != 7 { - t.Errorf("BufGroup() = %d, want 7", view.BufGroup()) - } - if view.Personality() != 3 { - t.Errorf("Personality() = %d, want 3", view.Personality()) - } - if view.SpliceFDIn() != 50 { - t.Errorf("SpliceFDIn() = %d, want 50", view.SpliceFDIn()) - } - if view.FileIndex() != 50 { - t.Errorf("FileIndex() = %d, want 50", view.FileIndex()) - } - if !view.HasBufferSelect() { - t.Error("expected HasBufferSelect() true") - } - if !view.HasAsync() { - t.Error("expected HasAsync() true") - } - }) - - t.Run("ViewExtSQE from ExtSQE", func(t *testing.T) { - ext := &ExtSQE{} - ext.SQE.opcode = IORING_OP_SEND - ext.SQE.flags = IOSQE_CQE_SKIP_SUCCESS - ext.SQE.fd = 200 - - view := ViewExtSQE(ext) - - if !view.Valid() { - t.Error("expected Valid() to be true") - } - if view.Opcode() != IORING_OP_SEND { - t.Errorf("Opcode() = %d, want %d", view.Opcode(), IORING_OP_SEND) - } - if view.RawFD() != 200 { - t.Errorf("RawFD() = %d, want 200", view.RawFD()) - } - if !view.HasCQESkipSuccess() { - t.Error("expected HasCQESkipSuccess() to be true") - } - }) -} diff --git a/testhelpers_linux_test.go b/testhelpers_linux_test.go index a3b08e8..052c074 100644 --- a/testhelpers_linux_test.go +++ b/testhelpers_linux_test.go @@ -136,3 +136,20 @@ func writeTestFD(fd iofd.FD, buf []byte) (int, uintptr) { n, errno := zcall.Write(uintptr(fd), buf) return int(n), errno } + +// dispatchSimplifiedMultishotCQE is a test helper for direct handler exercises. +// It intentionally skips the internal MultishotSubscription state machine and therefore +// must not be read as the authoritative runtime lifecycle. +func dispatchSimplifiedMultishotCQE(handler uring.MultishotHandler, cqe uring.CQEView) bool { + step := uring.MultishotStep{CQE: cqe} + if cqe.Res < 0 { + step.Err = zcall.Errno(uintptr(-cqe.Res)) + step.Cancelled = cqe.Res == -int32(uring.ECANCELED) + } + + keep := handler.OnMultishotStep(step) == uring.MultishotContinue + if step.Final() { + handler.OnMultishotStop(step.Err, step.Cancelled) + } + return keep +} diff --git a/zerocopy_test.go b/zerocopy_test.go index 752abea..5a6337b 100644 --- a/zerocopy_test.go +++ b/zerocopy_test.go @@ -823,8 +823,9 @@ func TestZeroCopySequentialSends(t *testing.T) { const numSends = 5 for seq := 0; seq < numSends; seq++ { - // Write unique message - msg := []byte("Sequential message " + string(rune('0'+seq))) + // Keep the payload above MulticastZeroCopy's single-target threshold so + // this test observes SEND_ZC CQEs instead of the fixed-send fallback. + msg := bytes.Repeat([]byte{byte('0' + seq)}, 2048) copy(regBuf, msg) targets := &singleTarget{fd: fds[1]} @@ -840,6 +841,7 @@ func TestZeroCopySequentialSends(t *testing.T) { b := iox.Backoff{} opDone := false notifDone := false + unsupported := false for time.Now().Before(deadline) && (!opDone || !notifDone) { n, _ := ring.Wait(cqes) @@ -852,15 +854,30 @@ func TestZeroCopySequentialSends(t *testing.T) { opDone = true if cqe.Res == -95 { t.Logf("send[%d]: EOPNOTSUPP", seq) + unsupported = true notifDone = true // Skip waiting for notification } else if cqe.Res < 0 { t.Errorf("send[%d] failed: res=%d", seq, cqe.Res) + } else if !cqe.HasMore() { + notifDone = true } } } } + if n > 0 { + b.Reset() + } b.Wait() } + if !opDone { + t.Fatalf("send[%d]: SEND_ZC operation CQE not observed", seq) + } + if !notifDone { + t.Fatalf("send[%d]: SEND_ZC notification CQE not observed", seq) + } + if unsupported { + t.Skip("SEND_ZC is not supported for Unix socket pairs in this environment") + } } t.Logf("completed %d sequential sends", numSends) From a958dc21262e60981be94d32b63962b0a96af375 Mon Sep 17 00:00:00 2001 From: Robin He Date: Wed, 13 May 2026 19:09:27 +0900 Subject: [PATCH 08/13] docs: refine CQE mode comments Signed-off-by: Robin He --- cqe_direct_darwin.go | 4 ++-- cqe_direct_linux.go | 19 +++++++++---------- cqe_extended_darwin.go | 4 ++-- cqe_extended_linux.go | 17 +++++++++-------- doc.go | 23 +++++++++++------------ 5 files changed, 33 insertions(+), 34 deletions(-) diff --git a/cqe_direct_darwin.go b/cqe_direct_darwin.go index 63afbfe..ad53868 100644 --- a/cqe_direct_darwin.go +++ b/cqe_direct_darwin.go @@ -11,7 +11,7 @@ import ( "code.hybscloud.com/iox" ) -// DirectCQE is a zero-overhead CQE for Direct mode operations. +// DirectCQE is a compact copied CQE for Direct mode operations. type DirectCQE struct { Res int32 Flags uint32 @@ -37,7 +37,7 @@ func (c *DirectCQE) BufID() uint16 { return cqeBufID(c.Flags) } // IsNotification reports whether this is a zero-copy notification CQE. func (c *DirectCQE) IsNotification() bool { return cqeIsNotification(c.Flags) } -// WaitDirect retrieves completion events using Direct mode fast-path (darwin stub). +// WaitDirect retrieves completion events using the Direct mode fast path (darwin stub). // On single-issuer rings it is not safe for concurrent use with submit, Stop, // or ResizeRings; caller must serialize those operations. func (ur *Uring) WaitDirect(cqes []DirectCQE) (int, error) { diff --git a/cqe_direct_linux.go b/cqe_direct_linux.go index d23b26f..e7bfbe4 100644 --- a/cqe_direct_linux.go +++ b/cqe_direct_linux.go @@ -14,15 +14,14 @@ import ( "code.hybscloud.com/spin" ) -// DirectCQE is a zero-overhead CQE for Direct mode operations. -// It contains the completion result and unpacked context fields -// without any mode checking or pointer indirection. +// DirectCQE is a compact copied CQE for Direct mode operations. +// It stores the completion result and unpacked context fields without mode +// checking or pointer indirection. // -// Use WaitDirect when your application exclusively uses Direct mode -// (PackDirect) for all submissions. This avoids the 3-way mode check -// that the generic Wait/CQEView path requires per-CQE. +// Use WaitDirect when every submitted operation uses Direct mode (PackDirect). +// That path skips the generic Wait/CQEView mode dispatch per CQE. // -// Layout: 16 bytes (fits in 1/4 cache line, no padding needed) +// Layout: 16 bytes on supported platforms. type DirectCQE struct { Res int32 // Completion result (bytes transferred or negative errno) Flags uint32 // CQE flags (IORING_CQE_F_*) @@ -68,12 +67,12 @@ func (c *DirectCQE) IsNotification() bool { return cqeIsNotification(c.Flags) } -// WaitDirect retrieves completion events using Direct mode fast-path. +// WaitDirect retrieves completion events using the Direct mode fast path. // This method skips mode detection since all CQEs are assumed to be // from Direct mode submissions (PackDirect). // -// For applications using only Direct mode, this skips the mode dispatch -// that Wait([]CQEView) performs per CQE. +// For applications using only Direct mode, this skips the generic mode dispatch +// that Wait performs per CQE. // // On single-issuer rings it is not safe for concurrent use with submit, Stop, // or ResizeRings; caller must serialize those operations. diff --git a/cqe_extended_darwin.go b/cqe_extended_darwin.go index 490c005..b8ce94f 100644 --- a/cqe_extended_darwin.go +++ b/cqe_extended_darwin.go @@ -11,7 +11,7 @@ import ( "code.hybscloud.com/iox" ) -// ExtCQE is a zero-overhead CQE for Extended mode operations. +// ExtCQE is a compact copied CQE for Extended mode operations. type ExtCQE struct { Res int32 Flags uint32 @@ -43,7 +43,7 @@ func (c *ExtCQE) Op() uint8 { return c.Ext.SQE.opcode } // FD returns the file descriptor from the stored SQE. func (c *ExtCQE) FD() iofd.FD { return iofd.FD(c.Ext.SQE.fd) } -// WaitExtended retrieves completion events using Extended mode fast-path (darwin stub). +// WaitExtended retrieves completion events using the Extended mode fast path (darwin stub). // On single-issuer rings it is not safe for concurrent use with submit, Stop, // or ResizeRings; caller must serialize those operations. func (ur *Uring) WaitExtended(cqes []ExtCQE) (int, error) { diff --git a/cqe_extended_linux.go b/cqe_extended_linux.go index 75a746b..ffb705d 100644 --- a/cqe_extended_linux.go +++ b/cqe_extended_linux.go @@ -14,14 +14,15 @@ import ( "code.hybscloud.com/spin" ) -// ExtCQE is a zero-overhead CQE for Extended mode operations. -// It provides direct access to the borrowed ExtSQE pointer without mode checking. +// ExtCQE is a compact copied CQE for Extended mode operations. +// It stores the completion result, CQE flags, and borrowed ExtSQE pointer +// without mode checking. // -// Use WaitExtended when your application exclusively uses Extended mode -// (PackExtended) for all submissions. This avoids the 3-way mode check -// that the generic Wait/CQEView path requires per-CQE. +// Use WaitExtended when every submitted operation uses Extended mode +// (PackExtended). That path skips the generic Wait/CQEView mode dispatch per +// CQE. // -// Layout: 16 bytes (fits in 1/4 cache line) +// Layout: 16 bytes on supported 64-bit platforms. type ExtCQE struct { Res int32 // Completion result (bytes transferred or negative errno) Flags uint32 // CQE flags (IORING_CQE_F_*) @@ -89,8 +90,8 @@ func (c *ExtCQE) FD() iofd.FD { // This method skips mode detection since all CQEs are assumed to be // from Extended mode submissions (PackExtended). // -// For applications using only Extended mode, this skips the mode dispatch -// that Wait([]CQEView) performs per CQE. +// For applications using only Extended mode, this skips the generic mode +// dispatch that Wait performs per CQE. // // On single-issuer rings it is not safe for concurrent use with submit, Stop, // or ResizeRings; caller must serialize those operations. diff --git a/doc.go b/doc.go index 2947e6d..a02ecbc 100644 --- a/doc.go +++ b/doc.go @@ -116,7 +116,7 @@ // // [SQEContext] packs submission metadata into `user_data`. // -// Direct mode layout (zero allocation, most common): +// Direct mode layout (inline context, zero allocation): // // ┌─────────┬─────────┬──────────────┬────────────────────────────┬────┐ // │ Op (8b) │Flags(8b)│ BufGrp (16b) │ FD (30b) │Mode│ @@ -178,17 +178,16 @@ // `MultishotStop` to request cancellation after the current step. The request // is local until the cancel SQE is successfully enqueued. // -// # Token Affinity at the Multishot Seam -// -// One live subscription names one live backend obligation. The kernel may -// emit multiple CQEs against the same SQE before the obligation terminates; -// each CQE carries `IORING_CQE_F_MORE` until the last. The package preserves -// a one-to-one correspondence between a submitted [ExtSQE] (and its encoded -// `user_data`) and the logical subscription, releasing the ExtSQE to its -// pool only when the terminating CQE (`!HasMore()`) is observed. This is -// the kernel-side foot of the affine-token discipline that caller runtimes -// enforce at their own seams: an intermediate CQE discharges no obligation, -// and the terminal CQE discharges exactly one. +// # Multishot Subscription Lifecycle +// +// A live subscription keeps its submitted [ExtSQE] until the terminal CQE for +// that operation is handled. The kernel may emit multiple CQEs for the same +// submission before that terminal CQE; each non-terminal CQE carries +// `IORING_CQE_F_MORE`. The package keeps the submitted ExtSQE and encoded +// `user_data` associated with the subscription, and returns the ExtSQE to its +// pool only after handling a CQE without `HasMore()`. Caller-side runtime code +// should treat intermediate CQEs as progress for the live subscription and the +// final CQE as the point where the subscription can be retired. // // # Runtime Boundary // From 61a82ad60eea10d28cb1c8b8f57a657c9da3adc0 Mon Sep 17 00:00:00 2001 From: Robin He Date: Wed, 13 May 2026 19:11:44 +0900 Subject: [PATCH 09/13] docs: fix README files Signed-off-by: Robin He --- README.es.md | 40 +++++++++++++++++++-------------------- README.fr.md | 50 +++++++++++++++++++++---------------------------- README.ja.md | 37 +++++++++++++++++++----------------- README.md | 37 +++++++++++++++++++----------------- README.zh-CN.md | 35 +++++++++++++++++++--------------- 5 files changed, 101 insertions(+), 98 deletions(-) diff --git a/README.es.md b/README.es.md index 12d58e7..adc1162 100644 --- a/README.es.md +++ b/README.es.md @@ -108,11 +108,11 @@ for { backoff.Reset() for i := range n { cqe := cqes[i] - if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { + if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { continue } - if cqe.Res < 0 { - return fmt.Errorf("uring read failed: res=%d", cqe.Res) + if err := cqe.Err(); err != nil { + return fmt.Errorf("uring read failed: %w", err) } handle(buf[:int(cqe.Res)]) return nil @@ -120,10 +120,7 @@ for { } ``` -`Wait` vacía los envíos pendientes antes de recoger completados. En rings de emisor único, también realiza la entrada al -kernel necesaria para que el trabajo diferido avance una vez vaciada la SQ; el llamador debe serializar `Wait`/`enter` -con las operaciones de estado de envío. Si `iox.Classify(err) == iox.OutcomeWouldBlock`, eso indica que no hay ningún -completado observable en el límite actual. +`Wait` vacía los envíos pendientes antes de recoger completados. En rings de emisor único, también realiza la entrada al kernel necesaria para que el trabajo diferido avance una vez vaciada la SQ; el llamador debe serializar `Wait`, `WaitDirect` y `WaitExtended` con las demás operaciones de estado de envío. Si `iox.Classify(err) == iox.OutcomeWouldBlock`, eso indica que no hay ningún completado observable en el límite actual. `Start` y `Stop` forman el par de ciclo de vida del ring. `Stop` es idempotente y deja el ring permanentemente inutilizable, por lo que solo debe llamarse tras drenar todas las operaciones en vuelo, recoger los CQE pendientes y @@ -234,8 +231,8 @@ if n == 0 { for i := 0; i < n; i++ { cqe := cqes[i] - if cqe.Res < 0 { - return fmt.Errorf("completion failed: op=%d fd=%d res=%d", cqe.Op(), cqe.FD(), cqe.Res) + if err := cqe.Err(); err != nil { + return fmt.Errorf("completion failed: op=%d fd=%d: %w", cqe.Op(), cqe.FD(), err) } switch cqe.Op() { @@ -427,8 +424,7 @@ se clasifique como `iox.OutcomeWouldBlock` o no recoja ningún CQE, y `backoff.R ### Ciclo de vida de suscripciones multishot -Una operación multishot genera un flujo de CQEs hasta que el kernel envía uno final (sin `IORING_CQE_F_MORE`). El código -de ejecución del llamador rastrea las suscripciones y gestiona la reemisión: +Una operación multishot genera un flujo de CQEs hasta que el kernel envía uno final (sin `IORING_CQE_F_MORE`). El código de ejecución del llamador encamina cada CQE por la suscripción devuelta antes de pasar al resto del despachador: ```go handler := uring.NewMultishotSubscriber(). @@ -446,12 +442,20 @@ handler := uring.NewMultishotSubscriber(). } }) -_, err := ring.AcceptMultishot(acceptCtx, handler.Handler()) +sub, err := ring.AcceptMultishot(acceptCtx, handler.Handler()) +if err != nil { + return err +} + +for i := range n { + if sub.HandleCQE(cqes[i]) { + continue + } + dispatch(ring, cqes[i]) +} ``` -`OnMultishotStep` observa cada finalización; devuelva `MultishotContinue` para mantener el flujo o `MultishotStop` para -solicitar la cancelación. `OnMultishotStop` se ejecuta una vez en el estado terminal. Úselo para limpieza y -resuscripción condicional. +`OnMultishotStep` observa cada finalización; devuelva `MultishotContinue` para mantener el flujo o `MultishotStop` para solicitar la cancelación. `OnMultishotStop` se ejecuta una vez en el estado terminal. Úselo para limpieza y resuscripción condicional. En los rings de emisor único predeterminados, llame a `Cancel` / `Unsubscribe` desde el propietario del ring o serialícelos con envío, `Wait`, `WaitDirect`, `WaitExtended`, `Stop` y operaciones de redimensionado. En rings con `MultiIssuers`, la ruta de envío compartida serializa sus SQE de cancelación. ### Estado por conexión con contextos tipados @@ -651,11 +655,7 @@ A nivel de paquete, `listener_example_test.go` cubre la creación de escuchas co - Active `NotifySucceed` cuando necesite un CQE visible por cada operación exitosa. - `ring.Features` informa de las entradas reales de SQ y CQ, el ancho de la ranura SQE y el orden de bytes que usa este paquete al interpretar `user_data`. -- Deje `MultiIssuers` desactivado en la configuración predeterminada de emisor único (`SINGLE_ISSUER` + - `DEFER_TASKRUN`), en la que una sola ruta de ejecución del llamador serializa las operaciones de estado de envío ( - `submit`, `Wait`/`enter`, `Stop` y resize). Actívelo solo cuando varios goroutines necesiten envío concurrente o - entrada - del lado de espera; esto conmuta el ring a la configuración de envío compartido con `COOP_TASKRUN`. +- Deje `MultiIssuers` desactivado en la configuración predeterminada de emisor único (`SINGLE_ISSUER` + `DEFER_TASKRUN`), en la que una sola ruta de ejecución del llamador serializa las operaciones de estado de envío (`submit`, `Wait`, `WaitDirect`, `WaitExtended`, `Stop` y resize). Actívelo solo cuando varios goroutines necesiten envío concurrente o llamadas concurrentes a `Wait`, `WaitDirect` o `WaitExtended`; esto conmuta el ring a la configuración de envío compartido con `COOP_TASKRUN`. - `EpollWait` requiere que `timeout` sea `0`; use `LinkTimeout` cuando necesite un plazo. - Las vistas prestadas de completado y los contextos en pool deben liberarse o descartarse con prontitud. - `ListenerOp.Close` cierra el FD del escucha de inmediato. Si aún hay un CQE de configuración pendiente, drene ese CQE diff --git a/README.fr.md b/README.fr.md index 979be30..e785c8e 100644 --- a/README.fr.md +++ b/README.fr.md @@ -104,11 +104,11 @@ for { backoff.Reset() for i := range n { cqe := cqes[i] - if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { + if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { continue } - if cqe.Res < 0 { - return fmt.Errorf("uring read failed: res=%d", cqe.Res) + if err := cqe.Err(); err != nil { + return fmt.Errorf("uring read failed: %w", err) } handle(buf[:int(cqe.Res)]) return nil @@ -116,10 +116,7 @@ for { } ``` -`Wait` purge les soumissions en attente avant de récupérer les complétions. Sur un ring mono-émetteur, il émet aussi -l'entrée noyau nécessaire pour que le travail différé progresse une fois la SQ vidée ; l'appelant doit -sérialiser `Wait`/`enter` avec les opérations d'état de soumission. Lorsque `iox.Classify(err)` produit -`iox.OutcomeWouldBlock`, aucune complétion n'est observable à l'interface courante. +`Wait` purge les soumissions en attente avant de récupérer les complétions. Sur un ring mono-émetteur, il émet aussi l'entrée noyau nécessaire pour que le travail différé progresse une fois la SQ vidée ; l'appelant doit sérialiser `Wait`, `WaitDirect` et `WaitExtended` avec les autres opérations d'état de soumission. Lorsque `iox.Classify(err)` produit `iox.OutcomeWouldBlock`, aucune complétion n'est observable à l'interface courante. `Start` et `Stop` constituent la paire de cycle de vie du ring. `Stop` est idempotent et rend le ring définitivement inutilisable ; on ne doit donc l'appeler qu'après avoir drainé toutes les opérations en vol, récupéré les CQE en attente @@ -229,8 +226,8 @@ if n == 0 { for i := 0; i < n; i++ { cqe := cqes[i] - if cqe.Res < 0 { - return fmt.Errorf("completion failed: op=%d fd=%d res=%d", cqe.Op(), cqe.FD(), cqe.Res) + if err := cqe.Err(); err != nil { + return fmt.Errorf("completion failed: op=%d fd=%d: %w", cqe.Op(), cqe.FD(), err) } switch cqe.Op() { @@ -354,14 +351,7 @@ travers de l'interface. ## Frontière d'exécution -Les couches d'exécution au-dessus de `uring` doivent l'utiliser comme backend noyau, pas comme ordonnanceur. La -frontière -idéale est unidirectionnelle : `uring` prépare les SQE, récupère les CQE, préserve `user_data`, expose `res` et fanions -des CQE, et rapporte les faits de propriété ; le code d'exécution côté appelant corrèle ces observations avec ses -propres -jetons, applique les reprises et l'attente progressive, route les gestionnaires et sessions, regroupe les soumissions et -libère les ressources -terminales. +Les couches d'exécution au-dessus de `uring` doivent l'utiliser comme backend noyau, pas comme ordonnanceur. La frontière idéale est unidirectionnelle : `uring` prépare les SQE, récupère les CQE, préserve `user_data`, expose `res` et fanions des CQE, et rapporte les faits de propriété ; le code d'exécution côté appelant corrèle ces observations avec ses propres jetons, applique les reprises et l'attente progressive, achemine les gestionnaires et sessions, regroupe les soumissions et libère les ressources terminales. Un pont d'exécution peut consommer les CQE en mode Extended lorsque l'exécution abstraite a besoin des faits de complétion. @@ -425,8 +415,7 @@ classe comme `iox.OutcomeWouldBlock` ou ne récupère aucun CQE, puis `backoff.R ### Cycle de vie des souscriptions multishot -Une opération multishot produit un flux de CQEs jusqu'à ce que le noyau envoie un CQE final (sans `IORING_CQE_F_MORE`). -Le code d'exécution côté appelant suit les souscriptions et gère la re-soumission : +Une opération multishot produit un flux de CQEs jusqu'à ce que le noyau envoie un CQE final (sans `IORING_CQE_F_MORE`). Le code d'exécution côté appelant achemine chaque CQE par la souscription renvoyée avant de passer au reste du répartiteur : ```go handler := uring.NewMultishotSubscriber(). @@ -444,12 +433,20 @@ handler := uring.NewMultishotSubscriber(). } }) -_, err := ring.AcceptMultishot(acceptCtx, handler.Handler()) +sub, err := ring.AcceptMultishot(acceptCtx, handler.Handler()) +if err != nil { + return err +} + +for i := range n { + if sub.HandleCQE(cqes[i]) { + continue + } + dispatch(ring, cqes[i]) +} ``` -`OnMultishotStep` observe chaque complétion ; on renvoie `MultishotContinue` pour maintenir le flux ou `MultishotStop` -pour demander l'annulation. `OnMultishotStop` s'exécute une seule fois à l'état terminal. On l'utilise pour le nettoyage -et la re-souscription conditionnelle. +`OnMultishotStep` observe chaque complétion ; on renvoie `MultishotContinue` pour maintenir le flux ou `MultishotStop` pour demander l'annulation. `OnMultishotStop` s'exécute une seule fois à l'état terminal. On l'utilise pour le nettoyage et la re-souscription conditionnelle. Sur les rings mono-émetteur par défaut, appelez `Cancel` / `Unsubscribe` depuis le propriétaire du ring ou sérialisez-les avec les opérations de soumission, `Wait`, `WaitDirect`, `WaitExtended`, `Stop` et le redimensionnement. Sur les rings `MultiIssuers`, le chemin de soumission partagé sérialise leurs SQE d'annulation. ### État par connexion via des contextes typés @@ -652,12 +649,7 @@ Au niveau du package, `listener_example_test.go` couvre la création d'écouteur - `ring.Features` indique le nombre effectif d'entrées SQ et CQ, la largeur des emplacements SQE, ainsi que l'ordre des octets utilisé par le package pour interpréter `user_data`. -- Laissez `MultiIssuers` désactivé pour la configuration mono-émetteur par défaut (`SINGLE_ISSUER` + `DEFER_TASKRUN`), - dans laquelle un seul chemin d'exécution de l'appelant sérialise les opérations d'état de soumission (`submit`, - `Wait`/ - `enter`, `Stop` et resize). N'activez ce fanion que lorsque plusieurs goroutines nécessitent une soumission - concurrente ou une entrée côté attente ; cela bascule le ring vers la configuration de soumission partagée - `COOP_TASKRUN`. +- Laissez `MultiIssuers` désactivé pour la configuration mono-émetteur par défaut (`SINGLE_ISSUER` + `DEFER_TASKRUN`), dans laquelle un seul chemin d'exécution de l'appelant sérialise les opérations d'état de soumission (`submit`, `Wait`, `WaitDirect`, `WaitExtended`, `Stop` et resize). N'activez ce fanion que lorsque plusieurs goroutines nécessitent des soumissions concurrentes ou des appels concurrents à `Wait`, `WaitDirect` ou `WaitExtended` ; cela bascule le ring vers la configuration de soumission partagée `COOP_TASKRUN`. - `EpollWait` exige que `timeout` vaille `0` ; utilisez `LinkTimeout` si vous avez besoin d'une échéance. - Les vues de complétion empruntées et les contextes issus des pools doivent être libérés ou abandonnés sans délai. - `ListenerOp.Close` ferme le FD de l'écouteur immédiatement. Si un CQE de mise en place est encore en attente, diff --git a/README.ja.md b/README.ja.md index 93c93c9..b9c0474 100644 --- a/README.ja.md +++ b/README.ja.md @@ -97,11 +97,11 @@ for { backoff.Reset() for i := range n { cqe := cqes[i] - if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { + if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { continue } - if cqe.Res < 0 { - return fmt.Errorf("uring read failed: res=%d", cqe.Res) + if err := cqe.Err(); err != nil { + return fmt.Errorf("uring read failed: %w", err) } handle(buf[:int(cqe.Res)]) return nil @@ -109,10 +109,7 @@ for { } ``` -`Wait` は未送信の SQE をフラッシュしてから完了を回収します。単一発行者リングでは、SQ が空になったあとも遅延タスクを進めるためのカーネル -enter も発行します。呼び出し側は `Wait`/`enter` と他の発行状態操作を直列化する必要があります。 -`Wait` が返した `err` を `iox.Classify(err)` で分類して `iox.OutcomeWouldBlock` になった場合、それは現時点で -境界上に観測可能な完了がないことを示します。 +`Wait` は未送信の SQE をフラッシュしてから完了を回収します。単一発行者リングでは、SQ が空になったあとも遅延タスクを進めるためのカーネル enter も発行します。呼び出し側は `Wait`、`WaitDirect`、`WaitExtended` と他の発行状態操作を直列化する必要があります。`Wait` が返した `err` を `iox.Classify(err)` で分類して `iox.OutcomeWouldBlock` になった場合、それは現時点で境界上に観測可能な完了がないことを示します。 `Start` と `Stop` がリングのライフサイクルを構成します。`Stop` は冪等ですが、呼び出すとリングは恒久的に使用不能になります。進行中の操作をすべて完了させ、未回収の CQE を回収し、multishot サブスクリプションを停止してから呼び出してください。 @@ -218,8 +215,8 @@ if n == 0 { for i := 0; i < n; i++ { cqe := cqes[i] - if cqe.Res < 0 { - return fmt.Errorf("completion failed: op=%d fd=%d res=%d", cqe.Op(), cqe.FD(), cqe.Res) + if err := cqe.Err(); err != nil { + return fmt.Errorf("completion failed: op=%d fd=%d: %w", cqe.Op(), cqe.FD(), err) } switch cqe.Op() { @@ -396,8 +393,7 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { ### マルチショットサブスクリプションのライフサイクル -マルチショット操作は、カーネルが最終 CQE(`IORING_CQE_F_MORE` なし)を送るまで CQE -のストリームを生成します。呼び出し側ランタイムコードがサブスクリプションを追跡し、再発行を管理します。 +マルチショット操作は、カーネルが最終 CQE(`IORING_CQE_F_MORE` なし)を送るまで CQE のストリームを生成します。呼び出し側ランタイムコードは各 CQE を返されたサブスクリプションへ先にルーティングしてから残りのディスパッチャへ渡します。 ```go handler := uring.NewMultishotSubscriber(). @@ -415,11 +411,20 @@ handler := uring.NewMultishotSubscriber(). } }) -_, err := ring.AcceptMultishot(acceptCtx, handler.Handler()) +sub, err := ring.AcceptMultishot(acceptCtx, handler.Handler()) +if err != nil { + return err +} + +for i := range n { + if sub.HandleCQE(cqes[i]) { + continue + } + dispatch(ring, cqes[i]) +} ``` -`OnMultishotStep` は各完了を観察します。ストリームを維持するなら `MultishotContinue` を、キャンセルを要求するなら -`MultishotStop` を返します。`OnMultishotStop` は終端状態で一度だけ実行されます。クリーンアップと条件付き再サブスクリプションに使用します。 +`OnMultishotStep` は各完了を観察します。ストリームを維持するなら `MultishotContinue` を、キャンセルを要求するなら `MultishotStop` を返します。`OnMultishotStop` は終端状態で一度だけ実行されます。クリーンアップと条件付き再サブスクリプションに使用します。既定の単一発行者リングでは、`Cancel` / `Unsubscribe` はリング所有者から呼び出すか、発行、`Wait`、`WaitDirect`、`WaitExtended`、`Stop`、リサイズ操作と直列化してください。`MultiIssuers` リングでは、共有発行パスがこれらのキャンセル SQE を直列化します。 ### 型付きコンテキストによるコネクション状態 @@ -608,9 +613,7 @@ TCP クライアントの connect/send フローをそれぞれ示していま - すべての成功操作に可視の CQE が必要な場合は `NotifySucceed` を有効にする。 - `ring.Features` は実際の SQ エントリ数・CQ エントリ数・SQE スロット幅・`user_data` 解釈時のバイト順を返す。 -- 既定では `MultiIssuers` を無効のまま、単一発行者構成(`SINGLE_ISSUER` + `DEFER_TASKRUN`)を使用する。この構成では、呼び出し側が - 1 つの実行パスで発行状態操作(`submit`・`Wait`/`enter`・`Stop`・resize)を直列化する。複数 goroutine から並行して - submit や wait 側 enter を行う必要がある場合にのみ `MultiIssuers` を有効にし、`COOP_TASKRUN` 構成に切り替える。 +- 既定では `MultiIssuers` を無効のまま、単一発行者構成(`SINGLE_ISSUER` + `DEFER_TASKRUN`)を使用する。この構成では、呼び出し側が 1 つの実行パスで発行状態操作(`submit`、`Wait`、`WaitDirect`、`WaitExtended`、`Stop`、resize)を直列化する。複数 goroutine から並行して発行または `Wait`、`WaitDirect`、`WaitExtended` 呼び出しを行う必要がある場合にのみ `MultiIssuers` を有効にし、`COOP_TASKRUN` 構成に切り替える。 - `EpollWait` の `timeout` は `0` のままにすること。期限が必要な場合は `LinkTimeout` を使う。 - 借用中の完了ビューやプール由来のコンテキストは速やかに解放すること。 - `ListenerOp.Close` はリスナー FD を即座に閉じる。構築中の CQE が未回収の場合は、その CQE を回収してから再度 `Close` diff --git a/README.md b/README.md index 2fbb892..ce2af8a 100644 --- a/README.md +++ b/README.md @@ -100,11 +100,11 @@ for { backoff.Reset() for i := range n { cqe := cqes[i] - if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { + if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { continue } - if cqe.Res < 0 { - return fmt.Errorf("uring read failed: res=%d", cqe.Res) + if err := cqe.Err(); err != nil { + return fmt.Errorf("uring read failed: %w", err) } handle(buf[:int(cqe.Res)]) return nil @@ -112,10 +112,7 @@ for { } ``` -`Wait` flushes pending submissions, then reaps completions. On single-issuer rings it also issues the kernel enter that -keeps deferred task work moving once the SQ drains; the caller must serialize `Wait`/`enter` with submit-state -operations. If `iox.Classify(err)` yields `iox.OutcomeWouldBlock`, no completion is currently observable at the -boundary. +`Wait` flushes pending submissions, then reaps completions. On single-issuer rings it also issues the kernel enter that keeps deferred task work moving once the SQ drains; the caller must serialize `Wait`, `WaitDirect`, and `WaitExtended` with other submit-state operations. If `iox.Classify(err)` yields `iox.OutcomeWouldBlock`, no completion is currently observable at the boundary. `Start` and `Stop` form the ring lifecycle pair. `Stop` is idempotent and renders the ring permanently unusable; call it only after you have drained all in-flight operations, reaped outstanding CQEs, and quiesced live multishot @@ -223,8 +220,8 @@ if n == 0 { for i := 0; i < n; i++ { cqe := cqes[i] - if cqe.Res < 0 { - return fmt.Errorf("completion failed: op=%d fd=%d res=%d", cqe.Op(), cqe.FD(), cqe.Res) + if err := cqe.Err(); err != nil { + return fmt.Errorf("completion failed: op=%d fd=%d: %w", cqe.Op(), cqe.FD(), err) } switch cqe.Op() { @@ -405,7 +402,7 @@ stays caller-owned: call `backoff.Wait()` on `iox.OutcomeWouldBlock` or when `Wa ### Multishot subscription lifecycle A multishot operation produces a stream of CQEs until the kernel sends a final one (without `IORING_CQE_F_MORE`). -Caller-side runtime code tracks subscriptions and handles resubmission: +Caller-side runtime code routes each CQE through the returned subscription before falling back to the rest of the dispatcher: ```go handler := uring.NewMultishotSubscriber(). @@ -423,11 +420,20 @@ handler := uring.NewMultishotSubscriber(). } }) -_, err := ring.AcceptMultishot(acceptCtx, handler.Handler()) +sub, err := ring.AcceptMultishot(acceptCtx, handler.Handler()) +if err != nil { + return err +} + +for i := range n { + if sub.HandleCQE(cqes[i]) { + continue + } + dispatch(ring, cqes[i]) +} ``` -`OnMultishotStep` observes each completion; return `MultishotContinue` to keep the stream or `MultishotStop` to request -cancellation. `OnMultishotStop` runs once at the terminal state. Use it for cleanup and conditional resubscription. +`OnMultishotStep` observes each completion; return `MultishotContinue` to keep the stream or `MultishotStop` to request cancellation. `OnMultishotStop` runs once at the terminal state. Use it for cleanup and conditional resubscription. On default single-issuer rings, call `Cancel` / `Unsubscribe` from the ring owner or otherwise serialize them with submit, `Wait`, `WaitDirect`, `WaitExtended`, `Stop`, and resize operations. On `MultiIssuers` rings, the shared-submit path serializes their cancel SQEs. ### Per-connection state with typed contexts @@ -622,10 +628,7 @@ The package-level `listener_example_test.go` covers listener creation with multi - Enable `NotifySucceed` when you need a visible CQE for every successful operation. - `ring.Features` reports actual SQ/CQ entry counts, SQE slot width, and the byte order used to interpret `user_data`. -- Leave `MultiIssuers` unset for the default single-issuer configuration (`SINGLE_ISSUER` + `DEFER_TASKRUN`) when a - single execution path serializes submit-state operations (`submit`, `Wait`/`enter`, `Stop`, and resize). Set it only - when multiple goroutines need concurrent submission or wait-side enter; this switches the ring to the shared-submit - `COOP_TASKRUN` configuration. +- Leave `MultiIssuers` unset for the default single-issuer configuration (`SINGLE_ISSUER` + `DEFER_TASKRUN`) when a single execution path serializes submit-state operations (`submit`, `Wait`, `WaitDirect`, `WaitExtended`, `Stop`, and resize). Set it only when multiple goroutines need concurrent submission or concurrent calls to `Wait`, `WaitDirect`, or `WaitExtended`; this switches the ring to the shared-submit `COOP_TASKRUN` configuration. - `EpollWait` requires `timeout` to remain `0`; use `LinkTimeout` when you need a deadline. - Release or discard borrowed completion views and pooled contexts promptly. - `ListenerOp.Close` closes the listener FD immediately. If a setup CQE is still pending, drain it first, then call diff --git a/README.zh-CN.md b/README.zh-CN.md index d7353ca..f247f3c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -95,11 +95,11 @@ for { backoff.Reset() for i := range n { cqe := cqes[i] - if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { + if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { continue } - if cqe.Res < 0 { - return fmt.Errorf("uring read failed: res=%d", cqe.Res) + if err := cqe.Err(); err != nil { + return fmt.Errorf("uring read failed: %w", err) } handle(buf[:int(cqe.Res)]) return nil @@ -107,9 +107,7 @@ for { } ``` -`Wait` 先刷新待提交项,再回收完成事件。在单提交者 ring 上,它还会在 SQ 排空后向内核发起进入调用,以推进延迟任务执行;调用方需保证 -`Wait`/`enter` 与提交状态操作串行执行。当 `Wait` 返回的 `err` 经 `iox.Classify(err)` 分类为 -`iox.OutcomeWouldBlock` 时,表示当前边界上没有可观察到的完成事件。 +`Wait` 先刷新待提交项,再回收完成事件。在单提交者 ring 上,它还会在 SQ 排空后向内核发起进入调用,以推进延迟任务执行;调用方需保证 `Wait`、`WaitDirect` 和 `WaitExtended` 与其他提交状态操作串行执行。当 `Wait` 返回的 `err` 经 `iox.Classify(err)` 分类为 `iox.OutcomeWouldBlock` 时,表示当前边界上没有可观察到的完成事件。 `Start` 与 `Stop` 是 ring 生命周期的配对操作。`Stop` 幂等但不可逆,调用后 ring 将永久不可用。调用 `Stop` 前,需确保所有进行中的操作已完成、未处理的 CQE 已回收、活跃的 multishot 订阅已终止。 @@ -210,8 +208,8 @@ if n == 0 { for i := 0; i < n; i++ { cqe := cqes[i] - if cqe.Res < 0 { - return fmt.Errorf("completion failed: op=%d fd=%d res=%d", cqe.Op(), cqe.FD(), cqe.Res) + if err := cqe.Err(); err != nil { + return fmt.Errorf("completion failed: op=%d fd=%d: %w", cqe.Op(), cqe.FD(), err) } switch cqe.Op() { @@ -379,7 +377,7 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { ### Multishot 订阅生命周期 -Multishot 操作产生 CQE 流,直到内核发送最终 CQE(不含 `IORING_CQE_F_MORE`)。调用方运行时代码负责追踪订阅状态并管理重新提交。 +Multishot 操作产生 CQE 流,直到内核发送最终 CQE(不含 `IORING_CQE_F_MORE`)。调用方运行时代码先把每个 CQE 交给返回的订阅,再进入其余分发器。 ```go handler := uring.NewMultishotSubscriber(). @@ -397,11 +395,20 @@ handler := uring.NewMultishotSubscriber(). } }) -_, err := ring.AcceptMultishot(acceptCtx, handler.Handler()) +sub, err := ring.AcceptMultishot(acceptCtx, handler.Handler()) +if err != nil { + return err +} + +for i := range n { + if sub.HandleCQE(cqes[i]) { + continue + } + dispatch(ring, cqes[i]) +} ``` -`OnMultishotStep` 观察每次完成;返回 `MultishotContinue` 保持流,返回 `MultishotStop` 请求取消。`OnMultishotStop` -在终态执行一次,用于清理和按条件重新订阅。 +`OnMultishotStep` 观察每次完成;返回 `MultishotContinue` 保持流,返回 `MultishotStop` 请求取消。`OnMultishotStop` 在终态执行一次,用于清理和按条件重新订阅。在默认单提交者 ring 上,应从 ring 所有者调用 `Cancel` / `Unsubscribe`,或将它们与提交、`Wait`、`WaitDirect`、`WaitExtended`、`Stop` 以及 resize 操作串行化。启用 `MultiIssuers` 的 ring 由共享提交路径串行化这些取消 SQE。 ### 类型化上下文承载连接状态 @@ -586,9 +593,7 @@ connect/send 流程。 - 若需为每个成功操作生成可见的 CQE,启用 `NotifySucceed`。 - `ring.Features` 报告实际 SQ/CQ 条目数、SQE 槽宽以及本包解析 `user_data` 的字节序。 -- 默认不启用 `MultiIssuers`,此时采用单提交者配置(`SINGLE_ISSUER` + `DEFER_TASKRUN`),由调用方的单一执行路径串行化 - 提交状态操作(`submit`、`Wait`/`enter`、`Stop` 及 resize)。仅当多个 goroutine 需并发提交或执行等待侧进入时才启用 - `MultiIssuers`,这会切换为共享提交的 `COOP_TASKRUN` 配置。 +- 默认不启用 `MultiIssuers`,此时采用单提交者配置(`SINGLE_ISSUER` + `DEFER_TASKRUN`),由调用方的单一执行路径串行化提交状态操作(`submit`、`Wait`、`WaitDirect`、`WaitExtended`、`Stop` 及 resize)。仅当多个 goroutine 需并发提交或并发调用 `Wait`、`WaitDirect`、`WaitExtended` 时才启用 `MultiIssuers`,这会切换为共享提交的 `COOP_TASKRUN` 配置。 - `EpollWait` 要求 `timeout` 为 `0`;如需设置截止时间,使用 `LinkTimeout`。 - 借用式完成视图与池化上下文应及时释放。 - `ListenerOp.Close` 会立即关闭监听 FD。若仍有设置 CQE 待处理,需先回收该 CQE,再调用 `Close` 将借用的 `ExtSQE` 归还池中。 From 6cf07499007e38bd6fca6ecbd9ef65f6c220bd60 Mon Sep 17 00:00:00 2001 From: Robin He Date: Thu, 14 May 2026 17:52:44 +0900 Subject: [PATCH 10/13] docs: polish completion-boundary GoDoc Signed-off-by: Robin He --- cqe_direct_darwin.go | 4 ++-- cqe_direct_linux.go | 5 +++-- cqe_extended_darwin.go | 4 ++-- cqe_extended_linux.go | 5 +++-- cqe_view.go | 3 +++ cqe_view_darwin.go | 5 ++++- doc.go | 9 ++++++--- interface_darwin.go | 7 ++++--- interface_linux.go | 11 ++++++----- 9 files changed, 33 insertions(+), 20 deletions(-) diff --git a/cqe_direct_darwin.go b/cqe_direct_darwin.go index ad53868..0b362cb 100644 --- a/cqe_direct_darwin.go +++ b/cqe_direct_darwin.go @@ -38,8 +38,8 @@ func (c *DirectCQE) BufID() uint16 { return cqeBufID(c.Flags) } func (c *DirectCQE) IsNotification() bool { return cqeIsNotification(c.Flags) } // WaitDirect retrieves completion events using the Direct mode fast path (darwin stub). -// On single-issuer rings it is not safe for concurrent use with submit, Stop, -// or ResizeRings; caller must serialize those operations. +// On single-issuer rings it is not safe for concurrent use with submit, Wait, +// WaitDirect, WaitExtended, or Stop; caller must serialize those operations. func (ur *Uring) WaitDirect(cqes []DirectCQE) (int, error) { if err := ur.ioUring.enter(); err != nil { return 0, err diff --git a/cqe_direct_linux.go b/cqe_direct_linux.go index e7bfbe4..1452d19 100644 --- a/cqe_direct_linux.go +++ b/cqe_direct_linux.go @@ -74,8 +74,9 @@ func (c *DirectCQE) IsNotification() bool { // For applications using only Direct mode, this skips the generic mode dispatch // that Wait performs per CQE. // -// On single-issuer rings it is not safe for concurrent use with submit, Stop, -// or ResizeRings; caller must serialize those operations. +// On single-issuer rings it is not safe for concurrent use with submit, Wait, +// WaitDirect, WaitExtended, Stop, or ResizeRings; caller must serialize those +// operations. // On IOPOLL rings WaitDirect also performs the nonblocking poll enter needed // to make completions visible. // Returns the number of CQEs retrieved, ErrCQOverflow when the ring enters CQ diff --git a/cqe_extended_darwin.go b/cqe_extended_darwin.go index b8ce94f..e3c9d20 100644 --- a/cqe_extended_darwin.go +++ b/cqe_extended_darwin.go @@ -44,8 +44,8 @@ func (c *ExtCQE) Op() uint8 { return c.Ext.SQE.opcode } func (c *ExtCQE) FD() iofd.FD { return iofd.FD(c.Ext.SQE.fd) } // WaitExtended retrieves completion events using the Extended mode fast path (darwin stub). -// On single-issuer rings it is not safe for concurrent use with submit, Stop, -// or ResizeRings; caller must serialize those operations. +// On single-issuer rings it is not safe for concurrent use with submit, Wait, +// WaitDirect, WaitExtended, or Stop; caller must serialize those operations. func (ur *Uring) WaitExtended(cqes []ExtCQE) (int, error) { if err := ur.ioUring.enter(); err != nil { return 0, err diff --git a/cqe_extended_linux.go b/cqe_extended_linux.go index ffb705d..b8ee645 100644 --- a/cqe_extended_linux.go +++ b/cqe_extended_linux.go @@ -93,8 +93,9 @@ func (c *ExtCQE) FD() iofd.FD { // For applications using only Extended mode, this skips the generic mode // dispatch that Wait performs per CQE. // -// On single-issuer rings it is not safe for concurrent use with submit, Stop, -// or ResizeRings; caller must serialize those operations. +// On single-issuer rings it is not safe for concurrent use with submit, Wait, +// WaitDirect, WaitExtended, Stop, or ResizeRings; caller must serialize those +// operations. // On IOPOLL rings WaitExtended also performs the nonblocking poll enter needed // to make completions visible. // Caller-side completion code must keep completion referents reachable until diff --git a/cqe_view.go b/cqe_view.go index 48ef023..027569d 100644 --- a/cqe_view.go +++ b/cqe_view.go @@ -12,6 +12,9 @@ import "code.hybscloud.com/iofd" // It exposes kernel completion facts directly and lets caller-side runtime // code decide how to route or interpret them. When available, // it also exposes the submission context that produced those facts. +// A copied CQEView is a completion observation, not durable route state. If +// caller code stores it beyond the current dispatch turn, caller code must keep +// its own route state. // // # Property Patterns // diff --git a/cqe_view_darwin.go b/cqe_view_darwin.go index 525ae7d..0a99a55 100644 --- a/cqe_view_darwin.go +++ b/cqe_view_darwin.go @@ -8,7 +8,10 @@ package uring import "code.hybscloud.com/iofd" -// CQEView provides a compatibility view into a completion queue entry. +// CQEView provides a view into a completion queue entry. +// A copied CQEView is a completion observation, not durable route state. If +// caller code stores it beyond the current dispatch turn, caller code must keep +// its own route state. type CQEView struct { Res int32 Flags uint32 diff --git a/doc.go b/doc.go index a02ecbc..ba266f7 100644 --- a/doc.go +++ b/doc.go @@ -69,7 +69,10 @@ // [Uring.SubmitReceiveBundleMultishot] submit raw multishot SQEs and keep the // kernel-boundary flow explicit. [Uring.AcceptMultishot] and // [Uring.ReceiveMultishot] use the same kernel path and return a -// [MultishotSubscription] when caller code wants callback-driven retirement. +// [MultishotSubscription] for caller-owned callback dispatch in the same +// serialized completion loop. If caller code keeps copied CQEs beyond that +// loop, it must keep its own route state and reject observations for retired +// subscriptions. // // sqeCtx := uring.ForFD(listenerFD) // sub, err := ring.AcceptMultishot(sqeCtx, handler) @@ -77,7 +80,7 @@ // return err // } // -// // Process CQEs by routing this subscription first. +// // Process CQEs in the same serialized completion loop. // for i := range n { // if sub.HandleCQE(cqes[i]) { // continue @@ -87,7 +90,7 @@ // // // Cancel when done // // On single-issuer rings, call Cancel from the ring owner or otherwise -// // serialize it with submit, Wait, WaitDirect, WaitExtended, Stop, and resize operations. +// // serialize it with submit, Wait, WaitDirect, WaitExtended, Stop, and ResizeRings. // if err := sub.Cancel(); err != nil { // return err // } diff --git a/interface_darwin.go b/interface_darwin.go index c50ddfa..2145591 100644 --- a/interface_darwin.go +++ b/interface_darwin.go @@ -223,9 +223,10 @@ func (ur *Uring) Stop() error { } // Wait flushes pending submissions and collects completion events into cqes. -// On single-issuer rings it is not safe for concurrent use with submit or -// Stop; caller must serialize those operations. It returns the number of -// events received, or `iox.ErrWouldBlock` if the CQ is empty. +// On single-issuer rings it is not safe for concurrent use with submit, Wait, +// WaitDirect, WaitExtended, or Stop; caller must serialize those operations. +// It returns the number of events received, or `iox.ErrWouldBlock` if the CQ +// is empty. func (ur *Uring) Wait(cqes []CQEView) (n int, err error) { err = ur.ioUring.enter() if err != nil { diff --git a/interface_linux.go b/interface_linux.go index 76aec85..6def4b0 100644 --- a/interface_linux.go +++ b/interface_linux.go @@ -315,11 +315,12 @@ func (ur *Uring) releaseRegisteredBufRings() error { // Wait flushes pending submissions, drives deferred task work when needed, and // collects completion events into cqes. On single-issuer rings it is not safe -// for concurrent use with submit, Stop, or ResizeRings; caller must serialize -// those operations. On IOPOLL rings Wait also performs the nonblocking poll -// enter needed to make completions visible. It returns the number of events received, ErrCQOverflow -// when the ring enters CQ overflow and no CQEs are immediately claimable, or -// `iox.ErrWouldBlock` if the CQ is empty. +// for concurrent use with submit, Wait, WaitDirect, WaitExtended, Stop, or +// ResizeRings; caller must serialize those operations. On IOPOLL rings Wait +// also performs the nonblocking poll enter needed to make completions visible. +// It returns the number of events received, ErrCQOverflow when the ring enters +// CQ overflow and no CQEs are immediately claimable, or `iox.ErrWouldBlock` if +// the CQ is empty. // // CQEView provides direct field access to Res and Flags, and methods to access // the submission context based on mode (Direct, Indirect, Extended). From 2c82129f9c19e48d0992ff396a8a1d89878dfa5a Mon Sep 17 00:00:00 2001 From: Robin He Date: Thu, 14 May 2026 17:53:30 +0900 Subject: [PATCH 11/13] docs: improve multishot subscription comments Signed-off-by: Robin He --- multishot.go | 18 ++++++++++++------ multishot_darwin.go | 3 ++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/multishot.go b/multishot.go index da4fe40..b95bae9 100644 --- a/multishot.go +++ b/multishot.go @@ -126,8 +126,8 @@ const ( // `Cancel` and `Unsubscribe` are safe for the subscription state itself, but // cancel submission follows the ring's submit-state serialization contract. On // default single-issuer rings, call them from the ring owner or otherwise -// serialize them with submit, Wait, WaitDirect, WaitExtended, Stop, and resize -// operations. On MultiIssuers rings, the shared-submit lock serializes the +// serialize them with submit, Wait, WaitDirect, WaitExtended, Stop, and +// ResizeRings. On MultiIssuers rings, the shared-submit lock serializes the // cancel SQE. // Observer callbacks run on the goroutine that dispatches the CQE, usually `Wait`. type MultishotSubscription struct { @@ -144,7 +144,7 @@ type MultishotSubscription struct { // remains live until a terminal CQE arrives. Cancel follows the ring's // submit-state serialization contract: on default single-issuer rings, call it // from the ring owner or otherwise serialize it with submit, Wait, WaitDirect, -// WaitExtended, Stop, and resize operations; on MultiIssuers rings, the +// WaitExtended, Stop, and ResizeRings; on MultiIssuers rings, the // shared-submit lock serializes the cancel SQE. // It is safe to call more than once. // @@ -188,8 +188,13 @@ func (s *MultishotSubscription) State() SubscriptionState { // HandleCQE processes a copied CQE observation for this subscription. // It returns true when the CQE belongs to this route and was handled. It returns -// false for non-extended CQEs, foreign routes, or stale observations whose -// pooled ExtSQE is no longer owned by this subscription. +// false for non-extended CQEs, foreign routes, or observations whose current +// pooled ExtSQE owner is no longer this subscription. +// +// HandleCQE is for immediate dispatch in the caller's serialized completion +// loop. Caller code must call it before the observed ExtSQE can be retired and +// reused. If caller code keeps copied CQEs beyond that loop, caller code must +// keep its own route state and reject observations for retired subscriptions. // // HandleCQE does not wait, retry, rearm, or resubmit. Caller-side runtime code // owns polling cadence and any policy after the subscription reaches its @@ -333,10 +338,11 @@ func multishotCQEHandler(ring *Uring, _ *ioUringSqe, cqe *ioUringCqe) { } func (s *MultishotSubscription) handleCQE(cqe *ioUringCqe) { + ctx := SQEContextFromRaw(cqe.userData) s.handleCQEView(CQEView{ Res: cqe.res, Flags: cqe.flags, - ctx: SQEContextFromRaw(cqe.userData), + ctx: ctx, }) } diff --git a/multishot_darwin.go b/multishot_darwin.go index 9beb749..60a8172 100644 --- a/multishot_darwin.go +++ b/multishot_darwin.go @@ -44,7 +44,8 @@ const ( SubscriptionStopped ) -// MultishotSubscription is a subscription to a multishot io_uring operation (stub on Darwin). +// MultishotSubscription tracks one multishot io_uring operation. +// On Darwin, multishot operations are not supported. type MultishotSubscription struct { ring *Uring ext *ExtSQE From ba853434bb2d4235bff5f40546f24acd46948842 Mon Sep 17 00:00:00 2001 From: Robin He Date: Thu, 14 May 2026 17:54:56 +0900 Subject: [PATCH 12/13] test: improve multishot test coverage Signed-off-by: Robin He --- multishot_internal_linux_test.go | 69 +++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/multishot_internal_linux_test.go b/multishot_internal_linux_test.go index bfed3d9..ee03881 100644 --- a/multishot_internal_linux_test.go +++ b/multishot_internal_linux_test.go @@ -20,6 +20,21 @@ const testLockedBufferMem = 1 << 18 var benchmarkMultishotHandleCQESink bool +func bindMultishotTestSubscription(sub *MultishotSubscription, ext *ExtSQE) { + ctx := PackExtended(ext) + sub.userData = ctx.Raw() + extAnchors(ext).owner = sub +} + +func multishotTestCQEView(ext *ExtSQE, res int32, flags uint32) CQEView { + ctx := PackExtended(ext) + return CQEView{ + Res: res, + Flags: flags, + ctx: ctx, + } +} + type noopMultishotHandler struct{} type stopOnProgressHandler struct{} @@ -351,7 +366,7 @@ func TestMultishotCancellingStillDeliversSteps(t *testing.T) { handler: handler, } sub.ext.Store(ext) - sub.userData = PackExtended(ext).Raw() + bindMultishotTestSubscription(sub, ext) sub.state.Store(uint32(SubscriptionCancelling)) sub.handleCQE(&ioUringCqe{userData: sub.userData, res: 7, flags: IORING_CQE_F_MORE}) @@ -386,7 +401,7 @@ func TestMultishotFinalSuccessSkipsCancelAndStops(t *testing.T) { handler: stopOnProgressHandler{}, } sub.ext.Store(ext) - sub.userData = PackExtended(ext).Raw() + bindMultishotTestSubscription(sub, ext) sub.state.Store(uint32(SubscriptionActive)) sub.handleCQE(&ioUringCqe{userData: sub.userData, res: 1, flags: 0}) @@ -415,7 +430,7 @@ func TestMultishotCancelSubmitRetainsExtUntilSubmitReturns(t *testing.T) { handler: noopMultishotHandler{}, } sub.ext.Store(ext) - sub.userData = PackExtended(ext).Raw() + bindMultishotTestSubscription(sub, ext) sub.state.Store(uint32(SubscriptionActive)) err := sub.submitCancelUsing(func(uint64) error { @@ -456,7 +471,7 @@ func TestMultishotFinalErrorDeliversErrorThenStopped(t *testing.T) { handler: handler, } sub.ext.Store(ext) - sub.userData = PackExtended(ext).Raw() + bindMultishotTestSubscription(sub, ext) sub.state.Store(uint32(SubscriptionActive)) sub.handleCQE(&ioUringCqe{userData: sub.userData, res: -int32(EINVAL), flags: 0}) @@ -489,7 +504,7 @@ func TestMultishotUnsubscribedSuppressesCallbacks(t *testing.T) { handler: handler, } sub.ext.Store(ext) - sub.userData = PackExtended(ext).Raw() + bindMultishotTestSubscription(sub, ext) sub.state.Store(uint32(SubscriptionActive)) sub.unsubscribed.Store(true) @@ -523,11 +538,10 @@ func TestMultishotHandleCQEClaimsRoute(t *testing.T) { handler: handler, } sub.ext.Store(ext) - sub.userData = PackExtended(ext).Raw() + bindMultishotTestSubscription(sub, ext) sub.state.Store(uint32(SubscriptionActive)) - extAnchors(ext).owner = sub - progress := CQEView{Res: 11, Flags: IORING_CQE_F_MORE, ctx: PackExtended(ext)} + progress := multishotTestCQEView(ext, 11, IORING_CQE_F_MORE) if !sub.HandleCQE(progress) { t.Fatal("HandleCQE did not claim route progress CQE") } @@ -541,7 +555,7 @@ func TestMultishotHandleCQEClaimsRoute(t *testing.T) { t.Fatalf("State after progress CQE: got %v, want %v", got, SubscriptionActive) } - final := CQEView{Res: 0, Flags: 0, ctx: PackExtended(ext)} + final := multishotTestCQEView(ext, 0, 0) if !sub.HandleCQE(final) { t.Fatal("HandleCQE did not claim route final CQE") } @@ -559,6 +573,33 @@ func TestMultishotHandleCQEClaimsRoute(t *testing.T) { } } +func TestMultishotHandleCQERejectsRetiredRoute(t *testing.T) { + pool := NewContextPools(1) + + ext := pool.Extended() + if ext == nil { + t.Fatal("pool exhausted") + } + + handler := &recordingMultishotHandler{} + sub := &MultishotSubscription{ + ring: &Uring{ctxPools: pool}, + handler: handler, + } + sub.ext.Store(ext) + bindMultishotTestSubscription(sub, ext) + sub.state.Store(uint32(SubscriptionActive)) + + cqe := multishotTestCQEView(ext, 17, IORING_CQE_F_MORE) + sub.retireExt() + if sub.HandleCQE(cqe) { + t.Fatal("HandleCQE claimed CQE after route retirement") + } + if got := len(handler.stepErrs); got != 0 { + t.Fatalf("callbacks after retired route: got %d, want 0", got) + } +} + func BenchmarkMultishotHandleCQE(b *testing.B) { pool := NewContextPools(16) @@ -572,11 +613,10 @@ func BenchmarkMultishotHandleCQE(b *testing.B) { handler: noopMultishotHandler{}, } sub.ext.Store(ext) - sub.userData = PackExtended(ext).Raw() + bindMultishotTestSubscription(sub, ext) sub.state.Store(uint32(SubscriptionActive)) - extAnchors(ext).owner = sub - cqe := CQEView{Res: 11, Flags: IORING_CQE_F_MORE, ctx: PackExtended(ext)} + cqe := multishotTestCQEView(ext, 11, IORING_CQE_F_MORE) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { @@ -602,14 +642,13 @@ func TestMultishotHandleCQERejectsForeignRoute(t *testing.T) { handler: handler, } sub.ext.Store(ext) - sub.userData = PackExtended(ext).Raw() + bindMultishotTestSubscription(sub, ext) sub.state.Store(uint32(SubscriptionActive)) - extAnchors(ext).owner = sub if sub.HandleCQE(CQEView{Res: 1, Flags: IORING_CQE_F_MORE, ctx: PackDirect(IORING_OP_ACCEPT, 0, 0, 0)}) { t.Fatal("HandleCQE claimed direct CQE") } - if sub.HandleCQE(CQEView{Res: 1, Flags: IORING_CQE_F_MORE, ctx: PackExtended(foreign)}) { + if sub.HandleCQE(multishotTestCQEView(foreign, 1, IORING_CQE_F_MORE)) { t.Fatal("HandleCQE claimed foreign extended CQE") } if got := len(handler.stepErrs); got != 0 { From 8fbd62f2cb609fb9ae57f196c228209ae451a4e1 Mon Sep 17 00:00:00 2001 From: Robin He Date: Thu, 14 May 2026 18:04:25 +0900 Subject: [PATCH 13/13] docs: reformat and refine README files Signed-off-by: Robin He --- README.es.md | 196 +++++++++++++----------------------------------- README.fr.md | 191 +++++++++++++--------------------------------- README.ja.md | 128 +++++++++++-------------------- README.md | 160 ++++++++++++--------------------------- README.zh-CN.md | 105 ++++++++++---------------- 5 files changed, 241 insertions(+), 539 deletions(-) diff --git a/README.es.md b/README.es.md index adc1162..43b4928 100644 --- a/README.es.md +++ b/README.es.md @@ -11,16 +11,9 @@ Idioma: [English](./README.md) | [简体中文](./README.zh-CN.md) | **Español* ## Descripción general -`uring` es el paquete del espacio de trabajo que expone la interfaz de `io_uring` frente al kernel de Linux. Se encarga -de crear -y arrancar rings, preparar SQE, decodificar CQE, transportar la identidad de envío a través de `user_data`, y ofrecer -registro de búferes, operaciones multishot y primitivas de configuración de escuchas. +`uring` es el paquete Go que expone la interfaz de `io_uring` frente al kernel de Linux. Crea y arranca rings, prepara SQE, decodifica CQE, transporta la identidad de envío a través de `user_data` y ofrece registro de búferes, operaciones multishot y primitivas de configuración de escuchas, sin convertirse en un planificador. -El diseño sigue un principio de frontera explícita: la mecánica orientada al kernel y los hechos observables de -completado permanecen en el borde de la API, mientras que la política y la composición quedan por encima de esa -frontera. El código de ejecución del llamador posee la correlación de completados, los reintentos y la espera -progresiva, -el enrutamiento de manejadores y sesiones, el ciclo de vida de conexiones y la liberación terminal de recursos. +El diseño sigue un principio de frontera explícita: la mecánica orientada al kernel y los hechos observables de completado permanecen en el borde de la API, mientras que la política y la composición quedan por encima de esa frontera. El código de ejecución del llamador posee la correlación de completados, los reintentos y la espera progresiva, el enrutamiento de manejadores y sesiones, el ciclo de vida de conexiones y la liberación terminal de recursos. Las superficies principales son: @@ -37,13 +30,9 @@ Las superficies principales son: uname -r ``` -`uring` asume la línea base 6.18+ y no incluye ramas de reserva para kernels anteriores. Use un kernel compatible en -lugar -de esperar ramas de compatibilidad dentro del paquete. +`uring` asume la línea base 6.18+ y no incluye ramas de reserva para kernels anteriores. Arranque un kernel compatible en lugar de esperar ramas de compatibilidad dentro del paquete. -En Debian 13, la rama estable del kernel puede estar aún por debajo de esa línea base. Consulte la sección de -actualización del kernel en Debian 13 si necesita instalar el kernel más reciente empaquetado por Debian que cumpla el -requisito de 6.18. +En Debian 13, la rama estable del kernel puede estar aún por debajo de 6.18. Consulte [Actualización del kernel en Debian 13](#actualización-del-kernel-en-debian-13) para obtener un kernel empaquetado por Debian que cumpla el requisito. ```bash go get code.hybscloud.com/uring @@ -51,22 +40,15 @@ go get code.hybscloud.com/uring ### Actualización del kernel en Debian 13 -La rama estable de Debian 13 incluye el kernel 6.12. La suite `trixie-backports` proporciona un kernel 6.18+ empaquetado -por Debian. Consulte [SETUP.md](./SETUP.md) para las instrucciones paso a paso. +La rama estable de Debian 13 incluye el kernel 6.12. La suite `trixie-backports` proporciona un kernel 6.18+ empaquetado por Debian. Consulte [SETUP.md](./SETUP.md) para las instrucciones paso a paso. ### Resolución de problemas -La creación del ring puede devolver `ENOMEM`, `EPERM` o `ENOSYS` según los límites de memlock, la configuración de -sysctl o el soporte del kernel. Los entornos de ejecución de contenedores bloquean las llamadas al sistema de `io_uring` -por defecto. -Consulte [SETUP.md](./SETUP.md) para el diagnóstico y la resolución. +La creación del ring puede devolver `ENOMEM`, `EPERM` o `ENOSYS` según los límites de memlock, la configuración de sysctl o el soporte del kernel. Los entornos de ejecución de contenedores bloquean las llamadas al sistema de `io_uring` por defecto. Consulte [SETUP.md](./SETUP.md) para el diagnóstico y la resolución. ## Ciclo de vida del ring -`New` devuelve un ring sin iniciar. Antes de enviar operaciones es necesario llamar a `Start`. `Start` registra los -recursos del ring y lo habilita; `New`, por su parte, construye los pools de contexto de forma anticipada. El ejemplo -siguiente envía una lectura de archivo, espera el CQE correspondiente y usa `iox.Classify` para conservar -`ErrWouldBlock` como resultado semántico de falta de progreso, no como fallo. +`New` devuelve un ring sin iniciar y construye los pools de contexto de forma anticipada. Antes de enviar operaciones es necesario llamar a `Start`, que registra los recursos del ring y lo habilita. El ejemplo siguiente envía una lectura de archivo, espera el CQE correspondiente y usa `iox.Classify` para conservar `ErrWouldBlock` como resultado semántico de falta de progreso, no como fallo. ```go ring, err := uring.New(func(o *uring.Options) { @@ -122,9 +104,7 @@ for { `Wait` vacía los envíos pendientes antes de recoger completados. En rings de emisor único, también realiza la entrada al kernel necesaria para que el trabajo diferido avance una vez vaciada la SQ; el llamador debe serializar `Wait`, `WaitDirect` y `WaitExtended` con las demás operaciones de estado de envío. Si `iox.Classify(err) == iox.OutcomeWouldBlock`, eso indica que no hay ningún completado observable en el límite actual. -`Start` y `Stop` forman el par de ciclo de vida del ring. `Stop` es idempotente y deja el ring permanentemente -inutilizable, por lo que solo debe llamarse tras drenar todas las operaciones en vuelo, recoger los CQE pendientes y -detener las suscripciones multishot activas. +`Start` y `Stop` forman el par de ciclo de vida del ring. `Stop` es idempotente y deja el ring permanentemente inutilizable, por lo que solo debe llamarse tras drenar todas las operaciones en vuelo, recoger los CQE pendientes y detener las suscripciones multishot activas. ## Tipos y operaciones @@ -163,15 +143,13 @@ Operaciones: | Ring msg | `MsgRing`, `MsgRingFD`, `FixedFdInstall`, `FilesUpdate` | | Cmd | `UringCmd`, `UringCmd128`, `Nop`, `Nop128` | -`Nop128` y `UringCmd128` requieren un ring creado con `Options.SQE128`, y el kernel debe anunciar soporte para los -opcodes correspondientes. De lo contrario, devuelven `ErrNotSupported`. +`Nop128` y `UringCmd128` requieren un ring creado con `Options.SQE128` y soporte del kernel para los opcodes correspondientes. Sin ambos, devuelven `ErrNotSupported`. `Uring.Close` envía `IORING_OP_CLOSE` sobre un descriptor de archivo destino. No es un método de desmontaje del ring. ## Transporte de contexto -`SQEContext` es el token de identidad principal en `uring`. En modo directo, empaqueta el opcode, las marcas del SQE, el -identificador de grupo de búferes y el descriptor de archivo en un único valor de 64 bits. +`SQEContext` es el token de identidad principal en este paquete. En modo directo, empaqueta el opcode, las marcas del SQE, el identificador de grupo de búferes y el descriptor de archivo en un único valor de 64 bits. ```go sqeCtx := uring.ForFD(fd). @@ -187,16 +165,9 @@ Los tres modos de contexto son: | Indirect | Puntero a `IndirectSQE` | Cuando 64 bits no bastan para el SQE completo | | Extended | Puntero a `ExtSQE` | SQE completo más 64 bytes de datos de usuario | -En la ruta habitual, parta de `ForFD` o `PackDirect` y añada solo los bits que desee volver a observar tras el -completado. `WithFlags` reemplaza el conjunto completo de marcas, por lo que conviene calcular la unión antes de -invocarlo. +En la ruta habitual, parta de `ForFD` o `PackDirect` y añada solo los bits que desee volver a observar tras el completado. `WithFlags` reemplaza el conjunto completo de marcas, por lo que conviene calcular la unión antes de invocarlo. -Cuando se necesiten metadatos del llamador que no caben en el layout directo de 64 bits, tome prestado un `ExtSQE`, -escriba en su `UserData` mediante `Ctx*Of` o `ViewCtx*`, y vuelva a empaquetarlo como `SQEContext`. Es preferible usar -cargas escalares. Si una superposición sin procesar o una vista tipada almacena punteros de Go, interfaces, valores de -función, slices, -strings, maps, chans o structs que los contengan, mantenga las raíces vivas fuera de `UserData`, ya que el GC no rastrea -esos bytes sin procesar. +Cuando se necesiten metadatos del llamador que no caben en el layout directo de 64 bits, tome prestado un `ExtSQE`, escriba en su `UserData` mediante `Ctx*Of` o `ViewCtx*` y vuelva a empaquetarlo como `SQEContext`. Es preferible usar cargas escalares. Si una superposición sin procesar o una vista tipada almacena punteros de Go, interfaces, valores de función, slices, strings, maps, chans o structs que los contengan, mantenga las raíces vivas fuera de `UserData`, ya que el GC no rastrea esos bytes sin procesar. ```go ext := ring.ExtSQE() @@ -207,13 +178,11 @@ sqeCtx := uring.PackExtended(ext) fmt.Printf("sqe context mode=%d seq=%d\n", sqeCtx.Mode(), meta.Val1) ``` -`NewContextPools` devuelve pools listos para usar. Llame a `Reset` solo después de haber devuelto todos los contextos -prestados y cuando desee reutilizar el conjunto de pools. +`NewContextPools` devuelve pools listos para usar. Llame a `Reset` solo después de haber devuelto todos los contextos prestados y cuando desee reutilizar el conjunto de pools. ### Despacho de completados con `CQEView` -`uring` no expone un tipo de contexto de completado separado. Todo el despacho de completados pasa por `CQEView`; -invoque `cqe.Context()` cuando necesite recuperar el token de envío original. +`uring` no expone un tipo de contexto de completado separado. Todo el despacho de completados pasa por `CQEView`; invoque `cqe.Context()` cuando necesite recuperar el token de envío original. ```go cqes := make([]uring.CQEView, 64) @@ -250,16 +219,11 @@ for i := 0; i < n; i++ { } ``` -Al completarse la operación, `CQEView` decodifica el modo de contexto correspondiente bajo demanda. `CQEView`, -`IndirectSQE`, `ExtSQE` y los búferes prestados no deben sobrevivir más allá de su tiempo de vida documentado. +Al completarse la operación, `CQEView` decodifica el modo de contexto correspondiente bajo demanda. `CQEView`, `IndirectSQE`, `ExtSQE` y los búferes prestados no deben sobrevivir más allá de su tiempo de vida documentado. ## Provisión de búferes -`uring` ofrece tres rutas prácticas para búferes. Los búferes registrados quedan fijados durante el arranque del ring y -se usan con I/O de archivo con búfer fijo. Los anillos de búferes provistos permiten que el kernel elija un búfer de -recepción y -devuelva su ID en el CQE. Las recepciones agrupadas consumen un rango lógico contiguo de búferes provistos y lo exponen -mediante `BundleIterator`. +`uring` ofrece tres rutas prácticas para búferes. Los búferes registrados quedan fijados durante el arranque del ring y se usan con I/O de archivo con búfer fijo. Los anillos de búferes provistos permiten que el kernel elija un búfer de recepción y devuelva su ID en el CQE. Las recepciones agrupadas consumen un rango lógico contiguo de búferes provistos y lo exponen mediante `BundleIterator`. - búferes provistos de tamaño fijo mediante `ReadBufferSize` y `ReadBufferNum` - grupos de búferes de varios tamaños mediante `MultiSizeBuffer` @@ -274,16 +238,14 @@ ring, err := uring.New(func(o *uring.Options) { }) ``` -Use `OptionsForBudget` para partir de un presupuesto de memoria explícito, y `BufferConfigForBudget` para inspeccionar -la distribución por niveles elegida para dicho presupuesto: +Use `OptionsForBudget` para partir de un presupuesto de memoria explícito, y `BufferConfigForBudget` para inspeccionar la distribución por niveles elegida para dicho presupuesto: ```go cfg, scale := uring.BufferConfigForBudget(256 * uring.MiB) fmt.Printf("buffer tiers=%+v scale=%d\n", cfg, scale) ``` -El I/O con búfer fijo usa un búfer registrado por índice. La slice devuelta pertenece al ring; manténgala viva hasta que -termine la operación fija: +El I/O con búfer fijo usa un búfer registrado por índice. La slice devuelta pertenece al ring; manténgala viva hasta que termine la operación fija: ```go buf := ring.RegisteredBuffer(0) @@ -296,8 +258,7 @@ if err := ring.WriteFixed(ctx, 0, len(payload)); err != nil { } ``` -Para recibir en socket con selección de búfer por el kernel, pase `nil` como búfer de recepción y solicite la clase de -tamaño deseada. La finalización indica qué búfer fue elegido: +Para recibir en socket con selección de búfer por el kernel, pase `nil` como búfer de recepción y solicite la clase de tamaño deseada. La finalización indica qué búfer fue elegido: ```go recvCtx := uring.PackDirect(uring.IORING_OP_RECV, 0, 0, 0) @@ -312,8 +273,7 @@ if cqe.HasBuffer() { } ``` -Las recepciones agrupadas usan el mismo almacenamiento de búferes provistos, pero pueden consumir más de un búfer en un -solo CQE. Procese el iterador y después recicle los slots consumidos: +Las recepciones agrupadas usan el mismo almacenamiento de búferes provistos, pero pueden consumir más de un búfer en un solo CQE. Procese el iterador y después recicle los slots consumidos: ```go if err := ring.ReceiveBundle(recvCtx, &socketFD, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { @@ -328,17 +288,13 @@ if it, ok := ring.BundleIterator(cqe, cqe.BufGroup()); ok { } ``` -Los búferes registrados requieren memoria fijada. Si el registro de búferes grandes falla, aumente -`RLIMIT_MEMLOCK` o reduzca el presupuesto. +Los búferes registrados requieren memoria fijada. Si el registro de búferes grandes falla, aumente `RLIMIT_MEMLOCK` o reduzca el presupuesto. ## Operaciones multishot y de escucha -`AcceptMultishot`, `ReceiveMultishot`, `SubmitAcceptMultishot`, `SubmitAcceptDirectMultishot`, `SubmitReceiveMultishot` -y `SubmitReceiveBundleMultishot` envían operaciones de socket multishot. +`AcceptMultishot`, `ReceiveMultishot`, `SubmitAcceptMultishot`, `SubmitAcceptDirectMultishot`, `SubmitReceiveMultishot` y `SubmitReceiveBundleMultishot` envían operaciones de socket multishot. -`uring` deja fuera del paquete la política de enrutamiento de CQE. La configuración del escucha avanza a través de -`DecodeListenerCQE`, `PrepareListenerBind`, `PrepareListenerListen` y `SetListenerReady`; es el llamador quien decide -cómo se despachan los completados y cuándo se detiene la cadena. +`uring` deja fuera del paquete la política de enrutamiento de CQE. La configuración del escucha avanza a través de `DecodeListenerCQE`, `PrepareListenerBind`, `PrepareListenerListen` y `SetListenerReady`; es el llamador quien decide cómo se despachan los completados y cuándo se detiene la cadena. ## Arquitectura de implementación @@ -348,41 +304,25 @@ La frontera de implementación se define así: 2. `Start` registra los búferes y habilita el ring para la línea base fija de Linux 6.18+. 3. Los métodos de operación declaran intención escribiendo SQE. 4. `Wait` vacía los envíos y devuelve observaciones prestadas de CQE. -5. El código de ejecución del llamador decide planificación, reintentos, espera, enrutamiento de conexión/sesión y - política - terminal de recursos. +5. El código de ejecución del llamador decide planificación, reintentos, espera, enrutamiento de conexión/sesión y política terminal de recursos. -De este modo, `uring` se mantiene centrado en la mecánica frente al kernel y preserva el significado de los completados -a través de la frontera. +De este modo, `uring` se mantiene centrado en la mecánica frente al kernel y preserva el significado de los completados a través de la frontera. ## Frontera de ejecución -Las capas de ejecución por encima de `uring` deben usarlo como backend del kernel, no como planificador. La frontera -ideal es -unidireccional: `uring` prepara SQEs, recoge CQEs, preserva `user_data`, expone `res` y marcas de CQE, e informa hechos -de propiedad; el código de ejecución del llamador correlaciona esas observaciones con sus propios tokens, aplica -reintentos y espera progresiva, enruta manejadores y sesiones, agrupa envíos y libera recursos terminales. +Las capas de ejecución por encima de `uring` deben usarlo como backend del kernel, no como planificador. La frontera ideal es unidireccional: `uring` prepara SQEs, recoge CQEs, preserva `user_data`, expone `res` y marcas de CQE, e informa hechos de propiedad; el código de ejecución del llamador correlaciona esas observaciones con sus propios tokens, aplica reintentos y espera progresiva, enruta manejadores y sesiones, agrupa envíos y libera recursos terminales. -Un puente de ejecución puede consumir CQEs en modo Extended cuando la ejecución abstracta necesita hechos de completado. -Un -entorno de ejecución por conexión también puede sondear CQEs Extended sin procesar directamente cuando necesita -resultado de CQE, marcas, -ID de búfer y token codificado antes de reducir el evento a devoluciones de llamada de manejador. +Un puente de ejecución puede consumir CQEs en modo Extended cuando la ejecución abstracta necesita hechos de completado. Un entorno de ejecución por conexión también puede sondear CQEs Extended sin procesar directamente cuando necesita el resultado de CQE, las marcas, el ID de búfer y el token codificado antes de reducir el evento a devoluciones de llamada de manejador. -Las capas de contexto y ejecución abstracta por encima de esta frontera no cambian el rol de `uring` como frontera del -kernel. +Las capas de contexto y ejecución abstracta por encima de esta frontera no cambian el rol de `uring` como frontera del kernel. ## Patrones para la capa de aplicación -`uring` expone los mecanismos orientados al kernel; la planificación, los reintentos, el seguimiento de conexiones y la -interpretación del protocolo corresponden a las capas superiores. Los patrones siguientes describen la frontera que debe -preservar un entorno de ejecución del llamador. +`uring` expone los mecanismos orientados al kernel; la planificación, los reintentos, el seguimiento de conexiones y la interpretación del protocolo corresponden a las capas superiores. Los patrones siguientes describen la frontera que debe preservar un entorno de ejecución del llamador. ### Bucle de eventos propietario del ring -En modo de emisor único (el predeterminado), una goroutine serializa todas las operaciones de envío. Un bucle típico -emite trabajo pendiente, aplica un `iox.Backoff` propiedad del llamador cuando `Wait` no informa progreso observable y -despacha las finalizaciones: +En modo de emisor único (el predeterminado), una goroutine serializa todas las operaciones de estado de envío. Un bucle típico emite el trabajo pendiente, aplica un `iox.Backoff` propiedad del llamador cuando `Wait` no informa progreso observable y despacha las finalizaciones: ```go func runLoop(ring *uring.Uring, stop <-chan struct{}) error { @@ -416,11 +356,7 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { } ``` -Todos los métodos del ring, incluidos `Send`, `Receive`, `AcceptMultishot` y `Wait`, se ejecutan en esta goroutine. El -trabajo procedente de otras goroutines entra en el bucle a través de un canal o una cola sin bloqueos; no se deben -invocar -los métodos del ring directamente. `iox.Backoff` sigue siendo propiedad del llamador: use `backoff.Wait()` cuando `Wait` -se clasifique como `iox.OutcomeWouldBlock` o no recoja ningún CQE, y `backoff.Reset()` tras cualquier lote con `n > 0`. +Todos los métodos del ring, incluidos `Send`, `Receive`, `AcceptMultishot` y `Wait`, se ejecutan en esta goroutine. El trabajo procedente de otras goroutines entra en el bucle a través de un canal o una cola sin bloqueos; no se deben invocar los métodos del ring directamente. `iox.Backoff` sigue siendo propiedad del llamador: use `backoff.Wait()` cuando `Wait` se clasifique como `iox.OutcomeWouldBlock` o no recoja ningún CQE, y `backoff.Reset()` tras cualquier lote con `n > 0`. ### Ciclo de vida de suscripciones multishot @@ -447,6 +383,8 @@ if err != nil { return err } +// Despacho en el mismo bucle de finalización serializado. Si el código llamador +// conserva CQE copiadas más allá de ese bucle, debe mantener su propio estado de ruta. for i := range n { if sub.HandleCQE(cqes[i]) { continue @@ -455,12 +393,11 @@ for i := range n { } ``` -`OnMultishotStep` observa cada finalización; devuelva `MultishotContinue` para mantener el flujo o `MultishotStop` para solicitar la cancelación. `OnMultishotStop` se ejecuta una vez en el estado terminal. Úselo para limpieza y resuscripción condicional. En los rings de emisor único predeterminados, llame a `Cancel` / `Unsubscribe` desde el propietario del ring o serialícelos con envío, `Wait`, `WaitDirect`, `WaitExtended`, `Stop` y operaciones de redimensionado. En rings con `MultiIssuers`, la ruta de envío compartida serializa sus SQE de cancelación. +`OnMultishotStep` observa cada finalización; devuelva `MultishotContinue` para mantener el flujo o `MultishotStop` para solicitar la cancelación. `OnMultishotStop` se ejecuta una vez en el estado terminal. Úselo para limpieza y resuscripción condicional. `HandleCQE` sirve para el despacho inmediato en el bucle de finalización serializado del llamador. Si el código llamador conserva CQE copiadas más allá de ese bucle, debe mantener su propio estado de ruta y rechazar observaciones de suscripciones ya retiradas. En los rings de emisor único predeterminados, llame a `Cancel` / `Unsubscribe` desde el propietario del ring o serialícelos con envío, `Wait`, `WaitDirect`, `WaitExtended`, `Stop` y `ResizeRings`. En rings con `MultiIssuers`, la ruta de envío compartida serializa sus SQE de cancelación. ### Estado por conexión con contextos tipados -Los contextos extendidos transportan referencias por conexión a lo largo del ciclo completo envío → completado, sin -necesidad de una tabla de búsqueda global: +Los contextos extendidos transportan referencias por conexión a lo largo del ciclo completo envío → completado, sin necesidad de una tabla de búsqueda global: ```go type ConnState struct { @@ -490,16 +427,11 @@ seq := ctx.Val1 ring.PutExtSQE(ext) ``` -Mantenga las raíces de punteros Go activas accesibles fuera de `UserData`. El GC no rastrea esos bytes crudos. El -conjunto de raíces sidecar adjunto a cada slot `ExtSQE` se encarga de esto para los protocolos internos multishot y -de escucha, pero el código de ejecución del llamador que coloca refs tipados debe mantenerlos accesibles de forma -independiente. +Mantenga las raíces de punteros Go activas accesibles fuera de `UserData`. El GC no rastrea esos bytes crudos. El conjunto de raíces sidecar adjunto a cada slot `ExtSQE` se encarga de esto para los protocolos internos multishot y de escucha, pero el código de ejecución del llamador que coloca refs tipados debe mantenerlos accesibles de forma independiente. ### Composición de plazos -`LinkTimeout` adjunta un plazo al SQE anterior a través de una cadena `IOSQE_IO_LINK`. La operación y el tiempo de -espera -compiten: exactamente uno se completa y el otro se cancela. +`LinkTimeout` adjunta un plazo al SQE anterior a través de una cadena `IOSQE_IO_LINK`. La operación y el tiempo de espera compiten: exactamente uno se completa y el otro se cancela. ```go recvCtx := uring.ForFD(fd). @@ -516,9 +448,7 @@ if err := ring.LinkTimeout(timeoutCtx, 5*time.Second); err != nil { } ``` -La capa de ejecución del llamador maneja ambos resultados: una recepción exitosa cancela el tiempo de espera, y un -tiempo de espera -disparado cancela la recepción. Ambos producen CQEs que el bucle de despacho debe observar. +La capa de ejecución del llamador maneja ambos resultados: una recepción exitosa cancela el tiempo de espera, y un tiempo de espera disparado cancela la recepción. Ambos producen CQEs que el bucle de despacho debe observar. ## Patrones de uso en TCP @@ -531,8 +461,7 @@ Los siguientes son los flujos más cortos, pensados para leerse junto con las pr ### Servidor echo TCP -Use `ListenerManager` para que el paquete prepare la cadena socket → bind → listen; a continuación, inicie multishot -accept y multishot receive sobre los FD de conexión activos. +Use `ListenerManager` para que el paquete prepare la cadena socket → bind → listen; a continuación, inicie multishot accept y multishot receive sobre los FD de conexión activos. ```go pool := uring.NewContextPools(32) @@ -557,14 +486,11 @@ if err != nil { defer recvSub.Cancel() ``` -`listener_example_test.go` cubre la preparación del escucha y el accept multishot, `examples/multishot_test.go` muestra -los CQE del lado del manejador en multishot receive, y `examples/echo_test.go` ilustra el flujo echo completo sobre -loopback. +`listener_example_test.go` cubre la preparación del escucha y el accept multishot, `examples/multishot_test.go` muestra los CQE del lado del manejador en multishot receive, y `examples/echo_test.go` ilustra el flujo echo completo sobre loopback. ### Cliente TCP -Cree el socket, espere el completado de `IORING_OP_SOCKET` y convierta el FD devuelto en un `iofd.FD` para usarlo con -`Connect`, `Send` y `Receive`. +Cree el socket, espere el completado de `IORING_OP_SOCKET` y convierta el FD devuelto en un `iofd.FD` para usarlo con `Connect`, `Send` y `Receive`. ```go clientCtx := uring.PackDirect(uring.IORING_OP_SOCKET, 0, 0, 0) @@ -590,31 +516,24 @@ if err := ring.Receive(recvCtx, &clientFD, buf); err != nil { } ``` -Reutilice el bucle `Wait` de la sección de ciclo de vida del ring tras cada envío para observar el completado -correspondiente. El archivo `socket_integration_linux_test.go` cubre el flujo de connect/send. +Reutilice el bucle `Wait` de la sección de ciclo de vida del ring tras cada envío para observar el completado correspondiente. El archivo `socket_integration_linux_test.go` cubre el flujo de connect/send. ## Recepción de copia cero (ZCRX) `ZCRXReceiver` gestiona la recepción de copia cero desde una cola RX de hardware de NIC mediante `io_uring`. -`NewZCRXReceiver` está preparado para rings con CQE de 32 bytes (`IORING_SETUP_CQE32`). La superficie actual de -`Options` no expone esa marca de configuración, de modo que los rings creados por la ruta estándar de `New` provocan que -este constructor devuelva `ErrNotSupported`. Hasta que se exponga una ruta de configuración CQE32, esta sección -documenta el contrato de frontera del receptor y no una receta pública ejecutable. +`NewZCRXReceiver` está preparado para rings con CQE de 32 bytes (`IORING_SETUP_CQE32`). La superficie actual de `Options` no expone esa marca de configuración, de modo que los rings creados por la ruta estándar de `New` provocan que este constructor devuelva `ErrNotSupported`. Hasta que se exponga una ruta de configuración CQE32, esta sección documenta el contrato de frontera del receptor y no una receta pública ejecutable. ### Ciclo de vida -1. Con un ring habilitado para CQE32, cree el receptor con `NewZCRXReceiver`. El constructor registra la cola de - interfaz ZCRX, mapea el área de reposición y prepara el ring de reposición. +1. Con un ring habilitado para CQE32, cree el receptor con `NewZCRXReceiver`. El constructor registra la cola de interfaz ZCRX, mapea el área de reposición y prepara el ring de reposición. 2. Llame a `Start` para enviar la operación extendida `RECV_ZC` en el ring. 3. En la ruta de despacho de CQE, los completados ZCRX se enrutan al `ZCRXHandler`: - - `OnData` entrega un `ZCRXBuffer` que apunta al área mapeada por la NIC. Llame a `Release` al terminar para reponer - el slot ante el kernel. Devuelva `false` para solicitar una parada de mejor esfuerzo. - - `OnError` entrega errores de CQE. Devuelva `false` para solicitar una parada de mejor esfuerzo. - - `OnStopped` se ejecuta una vez durante la retirada terminal, antes de que el estado pase a `Stopped`. + - `OnData` entrega un `ZCRXBuffer` que apunta al área mapeada por la NIC. Llame a `Release` al terminar para reponer el slot ante el kernel. Devuelva `false` para solicitar una parada de mejor esfuerzo. + - `OnError` entrega errores de CQE. Devuelva `false` para solicitar una parada de mejor esfuerzo. + - `OnStopped` se ejecuta una vez durante la retirada terminal, antes de que el estado pase a `Stopped`. 4. Llame a `Stop` para enviar un async cancel. El receptor transita por `Stopping` → `Retiring` → `Stopped`. -5. Consulte `Stopped` hasta que devuelva `true`, detenga el ring propietario y llame entonces a `Close` para liberar el - área mapeada y el mapeo del ring de reposición. +5. Consulte `Stopped` hasta que devuelva `true`, detenga el ring propietario y llame entonces a `Close` para liberar el área mapeada y el mapeo del ring de reposición. ### Máquina de estados @@ -628,8 +547,7 @@ Idle → Active → Stopping → Retiring → Stopped - `OnData` y `OnError` se invocan en serie desde el goroutine de despacho de CQE. - `Release` es de productor único; llámelo exclusivamente desde el goroutine de despacho. -- `Stop` solo debe llamarse cuando se garantice que no hay concurrencia con el despacho de CQE. Se trata de un contrato - de serialización del lado del llamador. +- `Stop` solo debe llamarse cuando se garantice que no hay concurrencia con el despacho de CQE. Se trata de un contrato de serialización del lado del llamador. ## Ejemplos @@ -647,26 +565,20 @@ Las pruebas de ejemplo en `uring/examples/` ilustran la API en la práctica. - `echo_test.go`, flujos de servidor echo TCP y UDP ping-pong - `timeout_linux_test.go`, operaciones de tiempo de espera y tiempo de espera enlazado -A nivel de paquete, `listener_example_test.go` cubre la creación de escuchas con accept multishot, y -`socket_integration_linux_test.go` cubre el flujo del cliente TCP de connect/send. +A nivel de paquete, `listener_example_test.go` cubre la creación de escuchas con accept multishot, y `socket_integration_linux_test.go` cubre el flujo del cliente TCP de connect/send. ## Notas operativas - Active `NotifySucceed` cuando necesite un CQE visible por cada operación exitosa. -- `ring.Features` informa de las entradas reales de SQ y CQ, el ancho de la ranura SQE y el orden de bytes que usa este - paquete al interpretar `user_data`. -- Deje `MultiIssuers` desactivado en la configuración predeterminada de emisor único (`SINGLE_ISSUER` + `DEFER_TASKRUN`), en la que una sola ruta de ejecución del llamador serializa las operaciones de estado de envío (`submit`, `Wait`, `WaitDirect`, `WaitExtended`, `Stop` y resize). Actívelo solo cuando varios goroutines necesiten envío concurrente o llamadas concurrentes a `Wait`, `WaitDirect` o `WaitExtended`; esto conmuta el ring a la configuración de envío compartido con `COOP_TASKRUN`. +- `ring.Features` informa de las entradas reales de SQ y CQ, el ancho de la ranura SQE y el orden de bytes que usa este paquete al interpretar `user_data`. +- Deje `MultiIssuers` desactivado en la configuración predeterminada de emisor único (`SINGLE_ISSUER` + `DEFER_TASKRUN`), en la que una sola ruta de ejecución del llamador serializa las operaciones de estado de envío (`submit`, `Wait`, `WaitDirect`, `WaitExtended`, `Stop` y `ResizeRings`). Actívelo solo cuando varios goroutines necesiten envío concurrente o llamadas concurrentes a `Wait`, `WaitDirect` o `WaitExtended`; esto conmuta el ring a la configuración de envío compartido con `COOP_TASKRUN`. - `EpollWait` requiere que `timeout` sea `0`; use `LinkTimeout` cuando necesite un plazo. - Las vistas prestadas de completado y los contextos en pool deben liberarse o descartarse con prontitud. -- `ListenerOp.Close` cierra el FD del escucha de inmediato. Si aún hay un CQE de configuración pendiente, drene ese CQE - y vuelva a llamar a `Close` para devolver el `ExtSQE` prestado al pool. +- `ListenerOp.Close` cierra el FD del escucha de inmediato. Si aún hay un CQE de configuración pendiente, drene ese CQE y vuelva a llamar a `Close` para devolver el `ExtSQE` prestado al pool. ## Soporte de plataforma -`uring` apunta a Go 1.26+ y Linux 6.18+ en la ruta real respaldada por el kernel. La mayoría de los archivos de -implementación y pruebas de ejemplo están protegidos con `//go:build linux`. Los archivos de Darwin proporcionan solo -resguardos de compilación para la superficie compartida; las capacidades exclusivas de Linux siguen siendo exclusivas de -Linux y no alteran la línea base de ejecución en Linux descrita arriba. +`uring` apunta a Go 1.26+ y Linux 6.18+ en la ruta real respaldada por el kernel. La mayoría de los archivos de implementación y pruebas de ejemplo están protegidos con `//go:build linux`. Los archivos de Darwin proporcionan solo resguardos de compilación para la superficie compartida; las capacidades exclusivas de Linux siguen siendo exclusivas de Linux y no alteran la línea base de ejecución en Linux descrita arriba. ## Licencia diff --git a/README.fr.md b/README.fr.md index e785c8e..8d2775b 100644 --- a/README.fr.md +++ b/README.fr.md @@ -11,14 +11,9 @@ Langue: [English](./README.md) | [简体中文](./README.zh-CN.md) | [Español]( ## Vue d'ensemble -`uring` est le package de l'espace de travail qui expose l'interface noyau Linux `io_uring`. Il crée et démarre les -rings, prépare les SQE, décode les CQE, achemine l'identité de soumission via `user_data`, et fournit l'enregistrement -de tampons, les opérations multishot ainsi que les primitives de mise en place des écouteurs. +`uring` est le package Go qui expose l'interface noyau Linux `io_uring`. Il crée et démarre les rings, prépare les SQE, décode les CQE, achemine l'identité de soumission via `user_data` et fournit l'enregistrement de tampons, les opérations multishot ainsi que les primitives de mise en place des écouteurs, sans pour autant devenir un ordonnanceur. -`uring` repose sur une conception à interface explicite : la mécanique côté noyau et les faits de complétion observables -se situent au bord de l'API, tandis que la politique et la composition relèvent des couches supérieures. Le code -d'exécution côté appelant possède la corrélation des complétions, les tentatives de reprise et l'attente progressive, le -routage des gestionnaires et des sessions, le cycle de vie des connexions et la libération terminale des ressources. +`uring` repose sur une conception à interface explicite : la mécanique côté noyau et les faits de complétion observables se situent au bord de l'API, tandis que la politique et la composition relèvent des couches supérieures. Le code d'exécution côté appelant possède la corrélation des complétions, les tentatives de reprise et l'attente progressive, le routage des gestionnaires et des sessions, le cycle de vie des connexions et la libération terminale des ressources. Les surfaces principales sont : @@ -35,12 +30,9 @@ Les surfaces principales sont : uname -r ``` -`uring` suppose la base 6.18+ et ne contient aucune branche de repli pour les noyaux plus anciens. Démarrez un noyau -pris en charge -plutôt que d'attendre des branches de compatibilité dans ce package. +`uring` suppose la base 6.18+ et ne contient aucune branche de repli pour les noyaux plus anciens. Démarrez un noyau pris en charge plutôt que d'attendre des branches de compatibilité à l'intérieur de ce package. -Sous Debian 13, le noyau de la branche stable peut rester en deçà de ce seuil. Consultez la section sur la mise à niveau -du noyau Debian 13 ci-dessous si vous avez besoin d'un noyau Debian plus récent satisfaisant l'exigence 6.18. +Sous Debian 13, le noyau de la branche stable peut rester en deçà de 6.18. Consultez [Mise à niveau du noyau Debian 13](#mise-à-niveau-du-noyau-debian-13) pour obtenir un noyau empaqueté par Debian qui satisfait l'exigence. ```bash go get code.hybscloud.com/uring @@ -48,21 +40,15 @@ go get code.hybscloud.com/uring ### Mise à niveau du noyau Debian 13 -La branche stable de Debian 13 fournit le noyau 6.12. La suite `trixie-backports` met à disposition un noyau 6.18+ -empaqueté par Debian. Consultez [SETUP.md](./SETUP.md) pour la marche à suivre détaillée. +La branche stable de Debian 13 fournit le noyau 6.12. La suite `trixie-backports` met à disposition un noyau 6.18+ empaqueté par Debian. Consultez [SETUP.md](./SETUP.md) pour la marche à suivre détaillée. ### Dépannage -La création du ring peut renvoyer `ENOMEM`, `EPERM` ou `ENOSYS` selon les limites memlock, la configuration sysctl ou le -support noyau. Les environnements d'exécution de conteneurs bloquent les appels système `io_uring` par défaut. -Consultez [SETUP.md](./SETUP.md) pour le diagnostic et la résolution. +La création du ring peut renvoyer `ENOMEM`, `EPERM` ou `ENOSYS` selon les limites memlock, la configuration sysctl ou le support noyau. Les environnements d'exécution de conteneurs bloquent par défaut les appels système `io_uring`. Consultez [SETUP.md](./SETUP.md) pour le diagnostic et la résolution. ## Cycle de vie du ring -`New` renvoie un ring non démarré. Il faut appeler `Start` avant de soumettre des opérations. `Start` enregistre les -ressources du ring et l'active ; `New`, de son côté, construit les pools de contexte de manière anticipée. L'exemple -ci-dessous soumet une lecture de fichier, attend le CQE correspondant et utilise `iox.Classify` afin que `ErrWouldBlock` -reste un résultat sémantique d'absence de progrès, et non une défaillance. +`New` renvoie un ring non démarré et construit les pools de contexte de manière anticipée. Avant de soumettre des opérations, appelez `Start`, qui enregistre les ressources du ring et l'active. L'exemple ci-dessous soumet une lecture de fichier, attend le CQE correspondant et utilise `iox.Classify` afin que `ErrWouldBlock` reste un résultat sémantique d'absence de progrès, et non une défaillance. ```go ring, err := uring.New(func(o *uring.Options) { @@ -118,9 +104,7 @@ for { `Wait` purge les soumissions en attente avant de récupérer les complétions. Sur un ring mono-émetteur, il émet aussi l'entrée noyau nécessaire pour que le travail différé progresse une fois la SQ vidée ; l'appelant doit sérialiser `Wait`, `WaitDirect` et `WaitExtended` avec les autres opérations d'état de soumission. Lorsque `iox.Classify(err)` produit `iox.OutcomeWouldBlock`, aucune complétion n'est observable à l'interface courante. -`Start` et `Stop` constituent la paire de cycle de vie du ring. `Stop` est idempotent et rend le ring définitivement -inutilisable ; on ne doit donc l'appeler qu'après avoir drainé toutes les opérations en vol, récupéré les CQE en attente -et arrêté les abonnements multishot encore actifs. +`Start` et `Stop` constituent la paire de cycle de vie du ring. `Stop` est idempotent et rend le ring définitivement inutilisable ; on ne doit donc l'appeler qu'après avoir drainé toutes les opérations en vol, récupéré les CQE en attente et arrêté les abonnements multishot encore actifs. ## Types et opérations @@ -159,16 +143,13 @@ Opérations : | Ring msg | `MsgRing`, `MsgRingFD`, `FixedFdInstall`, `FilesUpdate` | | Cmd | `UringCmd`, `UringCmd128`, `Nop`, `Nop128` | -`Nop128` et `UringCmd128` nécessitent un ring créé avec `Options.SQE128` ; le noyau doit en outre annoncer la prise en -charge des opcodes correspondants, faute de quoi ces méthodes renvoient `ErrNotSupported`. +`Nop128` et `UringCmd128` nécessitent un ring créé avec `Options.SQE128` et la prise en charge par le noyau des opcodes correspondants. Sans ces deux conditions, ces méthodes renvoient `ErrNotSupported`. -`Uring.Close` soumet un `IORING_OP_CLOSE` pour un descripteur de fichier cible. Il ne s'agit pas d'une méthode de -destruction du ring. +`Uring.Close` soumet un `IORING_OP_CLOSE` pour un descripteur de fichier cible. Il ne s'agit pas d'une méthode de destruction du ring. ## Transport du contexte -`SQEContext` est le jeton d'identité principal de `uring`. En mode direct, il encode l'opcode, les fanions SQE, -l'identifiant de groupe de tampons et le descripteur de fichier dans une seule valeur de 64 bits. +`SQEContext` est le jeton d'identité principal de ce package. En mode direct, il encode l'opcode, les fanions SQE, l'identifiant de groupe de tampons et le descripteur de fichier dans une seule valeur de 64 bits. ```go sqeCtx := uring.ForFD(fd). @@ -184,14 +165,9 @@ Les trois modes de contexte sont : | Indirect | Pointeur vers `IndirectSQE` | Lorsque 64 bits ne suffisent pas pour le SQE complet | | Extended | Pointeur vers `ExtSQE` | SQE complet plus 64 octets de données utilisateur | -Sur le chemin courant, on part de `ForFD` ou `PackDirect` en n'ajoutant que les bits que l'on souhaite retrouver à la -complétion. `WithFlags` remplace la totalité des fanions : il convient donc de calculer les unions avant l'appel. +Sur le chemin courant, on part de `ForFD` ou `PackDirect` en n'ajoutant que les bits que l'on souhaite retrouver à la complétion. `WithFlags` remplace la totalité des fanions : il convient donc de calculer les unions avant l'appel. -Lorsqu'on a besoin de métadonnées contrôlées par l'appelant au-delà du layout direct sur 64 bits, on emprunte un -`ExtSQE`, on écrit dans son champ `UserData` via `Ctx*Of` ou `ViewCtx*`, puis on le ré-encode en `SQEContext`. Préférez -des charges scalaires à cet endroit. Si une superposition brute ou une vue typée y stocke des pointeurs Go, des -interfaces, des valeurs de fonction, des tranches, des chaînes, des tables de hachage, des canaux ou des structures qui -en contiennent, conservez les racines vivantes en dehors de `UserData`, car le GC ne trace pas ces octets bruts. +Lorsqu'on a besoin de métadonnées contrôlées par l'appelant au-delà du layout direct sur 64 bits, on emprunte un `ExtSQE`, on écrit dans son champ `UserData` via `Ctx*Of` ou `ViewCtx*`, puis on le ré-encode en `SQEContext`. Préférez des charges scalaires à cet endroit. Si une superposition brute ou une vue typée y stocke des pointeurs Go, des interfaces, des valeurs de fonction, des tranches, des chaînes, des tables de hachage, des canaux ou des structures qui en contiennent, conservez les racines vivantes en dehors de `UserData`, car le GC ne trace pas ces octets bruts. ```go ext := ring.ExtSQE() @@ -202,13 +178,11 @@ sqeCtx := uring.PackExtended(ext) fmt.Printf("sqe context mode=%d seq=%d\n", sqeCtx.Mode(), meta.Val1) ``` -`NewContextPools` renvoie des pools prêts à l'emploi. N'appelez `Reset` qu'après avoir restitué tous les contextes -empruntés et uniquement si vous souhaitez réutiliser l'ensemble de pools. +`NewContextPools` renvoie des pools prêts à l'emploi. N'appelez `Reset` qu'après avoir restitué tous les contextes empruntés et uniquement si vous souhaitez réutiliser l'ensemble de pools. ### Dispatch des complétions avec `CQEView` -`uring` n'expose pas de type dédié au contexte de complétion. La distribution des complétions passe par `CQEView` ; on -appelle `cqe.Context()` pour récupérer le jeton de soumission d'origine. +`uring` n'expose pas de type dédié au contexte de complétion. La distribution des complétions passe par `CQEView` ; on appelle `cqe.Context()` pour récupérer le jeton de soumission d'origine. ```go cqes := make([]uring.CQEView, 64) @@ -245,22 +219,17 @@ for i := 0; i < n; i++ { } ``` -À la complétion, `CQEView` décode le mode de contexte à la demande. `CQEView`, `IndirectSQE`, `ExtSQE` et les tampons -empruntés ne doivent pas survivre au-delà de leur durée de vie documentée. +À la complétion, `CQEView` décode le mode de contexte à la demande. `CQEView`, `IndirectSQE`, `ExtSQE` et les tampons empruntés ne doivent pas survivre au-delà de leur durée de vie documentée. ## Fourniture de tampons -`uring` propose trois chemins pratiques pour les tampons. Les tampons enregistrés sont épinglés au démarrage du ring et -servent aux I/O fichier sur tampon fixe. Les anneaux de tampons fournis laissent le noyau choisir un tampon de réception -et -renvoyer son ID dans le CQE. Les réceptions groupées consomment une plage logique contiguë de tampons fournis et -l'exposent via `BundleIterator`. +`uring` propose trois chemins pratiques pour les tampons. Les tampons enregistrés sont épinglés au démarrage du ring et servent aux I/O fichier sur tampon fixe. Les anneaux de tampons fournis laissent le noyau choisir un tampon de réception et renvoyer son ID dans le CQE. Les réceptions groupées consomment une plage logique contiguë de tampons fournis et l'exposent via `BundleIterator`. - des tampons fournis de taille fixe via `ReadBufferSize` et `ReadBufferNum` - des groupes de tampons multi-tailles via `MultiSizeBuffer` - des tampons fixes enregistrés via `LockedBufferMem`, `RegisteredBuffer`, `ReadFixed` et `WriteFixed` -Pour la plupart des systèmes, les fonctions auxiliaires de configuration offrent un point d'entrée direct : +Pour la plupart des systèmes, les fonctions auxiliaires de configuration constituent le point d'entrée le plus direct : ```go opts := uring.OptionsForSystem(uring.MachineMemory4GB) @@ -269,16 +238,14 @@ ring, err := uring.New(func(o *uring.Options) { }) ``` -On utilise `OptionsForBudget` pour partir d'un budget mémoire explicite, et `BufferConfigForBudget` pour inspecter la -répartition par niveaux retenue pour ce budget : +On utilise `OptionsForBudget` pour partir d'un budget mémoire explicite, et `BufferConfigForBudget` pour inspecter la répartition par niveaux retenue pour ce budget : ```go cfg, scale := uring.BufferConfigForBudget(256 * uring.MiB) fmt.Printf("buffer tiers=%+v scale=%d\n", cfg, scale) ``` -L'I/O sur tampon fixe utilise un tampon enregistré par index. La tranche renvoyée appartient au ring ; gardez-la vivante -jusqu'à la complétion de l'opération fixe : +L'I/O sur tampon fixe utilise un tampon enregistré par index. La tranche renvoyée appartient au ring ; gardez-la vivante jusqu'à la complétion de l'opération fixe : ```go buf := ring.RegisteredBuffer(0) @@ -291,8 +258,7 @@ if err := ring.WriteFixed(ctx, 0, len(payload)); err != nil { } ``` -Pour recevoir sur un socket avec sélection de tampon par le noyau, passez `nil` comme tampon de réception et demandez la -classe de taille voulue. La complétion indique quel tampon a été choisi : +Pour recevoir sur un socket avec sélection de tampon par le noyau, passez `nil` comme tampon de réception et demandez la classe de taille voulue. La complétion indique quel tampon a été choisi : ```go recvCtx := uring.PackDirect(uring.IORING_OP_RECV, 0, 0, 0) @@ -307,8 +273,7 @@ if cqe.HasBuffer() { } ``` -Les réceptions groupées utilisent le même stockage de tampons fournis, mais peuvent consommer plusieurs tampons avec un -seul CQE. Traitez l'itérateur, puis recyclez les slots consommés : +Les réceptions groupées utilisent le même stockage de tampons fournis, mais peuvent consommer plusieurs tampons avec un seul CQE. Traitez l'itérateur, puis recyclez les slots consommés : ```go if err := ring.ReceiveBundle(recvCtx, &socketFD, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { @@ -323,16 +288,13 @@ if it, ok := ring.BundleIterator(cqe, cqe.BufGroup()); ok { } ``` -Les tampons enregistrés nécessitent de la mémoire épinglée. En cas d'échec de l'enregistrement de tampons volumineux, -augmentez `RLIMIT_MEMLOCK` ou réduisez le budget mémoire. +Les tampons enregistrés nécessitent de la mémoire épinglée. En cas d'échec de l'enregistrement de tampons volumineux, augmentez `RLIMIT_MEMLOCK` ou réduisez le budget mémoire. ## Opérations multishot et écouteur `AcceptMultishot`, `ReceiveMultishot`, `SubmitAcceptMultishot`, `SubmitAcceptDirectMultishot`, `SubmitReceiveMultishot` et `SubmitReceiveBundleMultishot` soumettent des opérations socket multishot. -La politique de routage des CQE reste hors du package. La mise en place de l'écouteur progresse via `DecodeListenerCQE`, -`PrepareListenerBind`, `PrepareListenerListen` et `SetListenerReady` ; c'est l'appelant qui décide de la distribution -des complétions et de l'arrêt de la chaîne. +La politique de routage des CQE reste hors du package. La mise en place de l'écouteur progresse via `DecodeListenerCQE`, `PrepareListenerBind`, `PrepareListenerListen` et `SetListenerReady` ; c'est l'appelant qui décide de la distribution des complétions et de l'arrêt de la chaîne. ## Architecture de l'implémentation @@ -342,38 +304,25 @@ L'implémentation se structure autour des couches suivantes : 2. `Start` enregistre les tampons et active le ring conformément à la base fixe Linux 6.18+. 3. Les méthodes d'opération publient l'intention en écrivant des SQE. 4. `Wait` purge les soumissions et renvoie des vues CQE empruntées. -5. Le code d'exécution côté appelant décide de l'ordonnancement, des reprises, de l'attente, du routage - connexion/session et - de la politique terminale des ressources. +5. Le code d'exécution côté appelant décide de l'ordonnancement, des reprises, de l'attente, du routage connexion/session et de la politique terminale des ressources. -De cette manière, `uring` reste focalisé sur la mécanique côté noyau tout en préservant la sémantique des complétions au -travers de l'interface. +De cette manière, `uring` reste focalisé sur la mécanique côté noyau tout en préservant la sémantique des complétions au travers de l'interface. ## Frontière d'exécution Les couches d'exécution au-dessus de `uring` doivent l'utiliser comme backend noyau, pas comme ordonnanceur. La frontière idéale est unidirectionnelle : `uring` prépare les SQE, récupère les CQE, préserve `user_data`, expose `res` et fanions des CQE, et rapporte les faits de propriété ; le code d'exécution côté appelant corrèle ces observations avec ses propres jetons, applique les reprises et l'attente progressive, achemine les gestionnaires et sessions, regroupe les soumissions et libère les ressources terminales. -Un pont d'exécution peut consommer les CQE en mode Extended lorsque l'exécution abstraite a besoin des faits de -complétion. -Un environnement d'exécution par connexion peut aussi sonder directement les CQE Extended bruts lorsqu'il a besoin du -résultat CQE, des -fanions, de l'ID de tampon et du jeton encodé avant de réduire l'événement en rappels de gestionnaire. +Un pont d'exécution peut consommer les CQE en mode Extended lorsque l'exécution abstraite a besoin des faits de complétion. Un environnement d'exécution par connexion peut aussi sonder directement les CQE Extended bruts lorsqu'il a besoin du résultat CQE, des fanions, de l'ID de tampon et du jeton encodé avant de réduire l'événement en rappels de gestionnaire. -Les couches de contexte et d'exécution abstraite au-dessus de cette frontière ne modifient pas le rôle de frontière -noyau de `uring`. +Les couches de contexte et d'exécution abstraite au-dessus de cette frontière ne modifient pas le rôle de frontière noyau de `uring`. ## Patrons pour la couche applicative -`uring` expose les mécanismes tournés vers le noyau ; l'ordonnancement, les tentatives de reprise, le suivi de -connexions et l'interprétation du protocole relèvent des couches supérieures. Les patrons ci-dessous décrivent la -frontière qu'un environnement d'exécution côté appelant doit préserver. +`uring` expose les mécanismes tournés vers le noyau ; l'ordonnancement, les tentatives de reprise, le suivi de connexions et l'interprétation du protocole relèvent des couches supérieures. Les patrons ci-dessous décrivent la frontière qu'un environnement d'exécution côté appelant doit préserver. ### Boucle d'événements propriétaire du ring -En mode mono-émetteur (le mode par défaut), une seule goroutine sérialise toutes les opérations côté soumission. Une -boucle -classique soumet le travail en attente, applique un `iox.Backoff` détenu par l'appelant quand `Wait` ne signale aucun -progrès observable, puis distribue les complétions : +En mode mono-émetteur (le mode par défaut), une seule goroutine sérialise toutes les opérations d'état de soumission. Une boucle classique soumet le travail en attente, applique un `iox.Backoff` détenu par l'appelant quand `Wait` ne signale aucun progrès observable, puis distribue les complétions : ```go func runLoop(ring *uring.Uring, stop <-chan struct{}) error { @@ -407,11 +356,7 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { } ``` -Les méthodes du ring, dont `Send`, `Receive`, `AcceptMultishot` et `Wait`, s'exécutent sur cette goroutine. Le travail -provenant d'autres goroutines entre dans la boucle via un canal ou une file sans verrou ; on n'appelle pas directement -les -méthodes du ring depuis l'extérieur. `iox.Backoff` reste côté appelant : on appelle `backoff.Wait()` quand `Wait` se -classe comme `iox.OutcomeWouldBlock` ou ne récupère aucun CQE, puis `backoff.Reset()` après tout lot avec `n > 0`. +Les méthodes du ring, dont `Send`, `Receive`, `AcceptMultishot` et `Wait`, s'exécutent sur cette goroutine. Le travail provenant d'autres goroutines entre dans la boucle via un canal ou une file sans verrou ; on n'appelle pas directement les méthodes du ring depuis l'extérieur. `iox.Backoff` reste côté appelant : on appelle `backoff.Wait()` quand `Wait` est classé comme `iox.OutcomeWouldBlock` ou ne récupère aucun CQE, puis `backoff.Reset()` après tout lot avec `n > 0`. ### Cycle de vie des souscriptions multishot @@ -438,6 +383,8 @@ if err != nil { return err } +// Dispatch dans la même boucle de complétion sérialisée. Si le code appelant +// conserve des CQE copiées après cette boucle, il doit garder son propre état de routage. for i := range n { if sub.HandleCQE(cqes[i]) { continue @@ -446,13 +393,11 @@ for i := range n { } ``` -`OnMultishotStep` observe chaque complétion ; on renvoie `MultishotContinue` pour maintenir le flux ou `MultishotStop` pour demander l'annulation. `OnMultishotStop` s'exécute une seule fois à l'état terminal. On l'utilise pour le nettoyage et la re-souscription conditionnelle. Sur les rings mono-émetteur par défaut, appelez `Cancel` / `Unsubscribe` depuis le propriétaire du ring ou sérialisez-les avec les opérations de soumission, `Wait`, `WaitDirect`, `WaitExtended`, `Stop` et le redimensionnement. Sur les rings `MultiIssuers`, le chemin de soumission partagé sérialise leurs SQE d'annulation. +`OnMultishotStep` observe chaque complétion ; on renvoie `MultishotContinue` pour maintenir le flux ou `MultishotStop` pour demander l'annulation. `OnMultishotStop` s'exécute une seule fois à l'état terminal. On l'utilise pour le nettoyage et la re-souscription conditionnelle. `HandleCQE` sert au dispatch immédiat dans la boucle de complétion sérialisée de l'appelant. Si le code appelant conserve des CQE copiées après cette boucle, il doit garder son propre état de routage et rejeter les observations de souscriptions déjà retirées. Sur les rings mono-émetteur par défaut, appelez `Cancel` / `Unsubscribe` depuis le propriétaire du ring ou sérialisez-les avec les opérations de soumission, `Wait`, `WaitDirect`, `WaitExtended`, `Stop` et `ResizeRings`. Sur les rings `MultiIssuers`, le chemin de soumission partagé sérialise leurs SQE d'annulation. ### État par connexion via des contextes typés -Les contextes étendus transportent les références par connexion tout au long du cycle soumission → complétion, sans -recourir à -une table de correspondance globale : +Les contextes étendus transportent les références par connexion tout au long du cycle soumission → complétion, sans recourir à une table de correspondance globale : ```go type ConnState struct { @@ -482,15 +427,11 @@ seq := ctx.Val1 ring.PutExtSQE(ext) ``` -On maintient les racines de pointeurs Go actives accessibles en dehors de `UserData`. Le GC ne trace pas ces octets -bruts. Le jeu de racines sidecar rattaché à chaque slot `ExtSQE` s'en charge pour les protocoles internes multishot et -d'écouteur, mais le code d'exécution appelant qui place des refs typés doit les garder accessibles de manière -indépendante. +On maintient les racines de pointeurs Go actives accessibles en dehors de `UserData`. Le GC ne trace pas ces octets bruts. Le jeu de racines sidecar rattaché à chaque slot `ExtSQE` s'en charge pour les protocoles internes multishot et d'écouteur, mais le code d'exécution appelant qui place des références typées doit les garder accessibles de manière indépendante. ### Composition de délais -`LinkTimeout` attache un délai au SQE précédent via une chaîne `IOSQE_IO_LINK`. L'opération et le délai entrent en -concurrence : l'un aboutit, l'autre est annulé. +`LinkTimeout` attache un délai au SQE précédent via une chaîne `IOSQE_IO_LINK`. L'opération et le délai entrent en concurrence : l'un aboutit, l'autre est annulé. ```go recvCtx := uring.ForFD(fd). @@ -507,8 +448,7 @@ if err := ring.LinkTimeout(timeoutCtx, 5*time.Second); err != nil { } ``` -La couche d'exécution appelante gère les deux issues : une réception réussie annule le délai, et un délai déclenché -annule la réception. Les deux produisent des CQEs que la boucle de distribution doit observer. +La couche d'exécution côté appelant gère les deux issues : une réception réussie annule le délai, et un délai déclenché annule la réception. Les deux produisent des CQE que la boucle de distribution doit observer. ## Parcours TCP courants @@ -521,8 +461,7 @@ Les parcours les plus concis, à lire en regard des tests : ### Serveur echo TCP -On utilise `ListenerManager` pour que le package prépare la chaîne socket → bind → listen, puis on démarre le multishot -accept et le multishot receive sur les FD de connexion actifs. +On utilise `ListenerManager` pour que le package prépare la chaîne socket → bind → listen, puis on démarre le multishot accept et le multishot receive sur les FD de connexion actifs. ```go pool := uring.NewContextPools(32) @@ -547,15 +486,11 @@ if err != nil { defer recvSub.Cancel() ``` -`listener_example_test.go` couvre la mise en place de l'écouteur avec accept multishot, `examples/multishot_test.go` -détaille les CQE côté gestionnaire pour le multishot receive, et `examples/echo_test.go` illustre le parcours echo -loopback -complet. +`listener_example_test.go` couvre la mise en place de l'écouteur avec accept multishot, `examples/multishot_test.go` détaille les CQE côté gestionnaire pour le multishot receive, et `examples/echo_test.go` illustre le parcours echo loopback complet. ### Client TCP -On crée d'abord le socket, on attend la complétion `IORING_OP_SOCKET`, puis on convertit le FD renvoyé en `iofd.FD` pour -l'utiliser avec `Connect`, `Send` et `Receive`. +On crée d'abord le socket, on attend la complétion `IORING_OP_SOCKET`, puis on convertit le FD renvoyé en `iofd.FD` pour l'utiliser avec `Connect`, `Send` et `Receive`. ```go clientCtx := uring.PackDirect(uring.IORING_OP_SOCKET, 0, 0, 0) @@ -581,32 +516,24 @@ if err := ring.Receive(recvCtx, &clientFD, buf); err != nil { } ``` -On réutilise la boucle `Wait` décrite dans la section cycle de vie du ring après chaque soumission pour observer la -complétion correspondante. Le fichier `socket_integration_linux_test.go` couvre le flux connect/send côté client TCP. +On réutilise la boucle `Wait` décrite dans la section cycle de vie du ring après chaque soumission pour observer la complétion correspondante. Le fichier `socket_integration_linux_test.go` couvre le flux connect/send côté client TCP. ## Réception sans copie (ZCRX) `ZCRXReceiver` gère la réception sans copie depuis une file RX matérielle de NIC via `io_uring`. -`NewZCRXReceiver` est prévu pour les rings créés avec des CQE de 32 octets (`IORING_SETUP_CQE32`). La surface `Options` -actuelle n'expose pas ce fanion de configuration ; par conséquent, les rings créés via le chemin standard de `New` -conduisent ce constructeur à renvoyer `ErrNotSupported`. Tant qu'un chemin de configuration CQE32 n'est pas exposé, -cette section documente le contrat de frontière du récepteur plutôt qu'une recette publique exécutable. +`NewZCRXReceiver` est prévu pour les rings créés avec des CQE de 32 octets (`IORING_SETUP_CQE32`). La surface `Options` actuelle n'expose pas ce fanion de configuration ; par conséquent, les rings créés via le chemin standard de `New` conduisent ce constructeur à renvoyer `ErrNotSupported`. Tant qu'un chemin de configuration CQE32 n'est pas exposé, cette section documente le contrat de frontière du récepteur plutôt qu'une recette publique exécutable. ### Cycle de vie -1. Avec un ring compatible CQE32, on crée le récepteur via `NewZCRXReceiver`. Le constructeur enregistre la file - d'interface ZCRX, mappe la zone de remplissage et prépare le ring de remplissage. +1. Avec un ring compatible CQE32, on crée le récepteur via `NewZCRXReceiver`. Le constructeur enregistre la file d'interface ZCRX, mappe la zone de remplissage et prépare le ring de remplissage. 2. Appelez `Start` pour soumettre l'opération étendue `RECV_ZC` sur le ring. 3. Sur le chemin de distribution des CQE, les complétions ZCRX sont acheminées vers le `ZCRXHandler` : - - `OnData` livre un `ZCRXBuffer` pointant vers la zone mappée par la NIC. On appelle `Release` une fois le - traitement terminé pour restituer le slot au noyau. Renvoyer `false` demande un arrêt au mieux. + - `OnData` livre un `ZCRXBuffer` pointant vers la zone mappée par la NIC. On appelle `Release` une fois le traitement terminé pour restituer le slot au noyau. Renvoyer `false` demande un arrêt au mieux. - `OnError` livre les erreurs CQE. Renvoyer `false` demande un arrêt au mieux. - - `OnStopped` s'exécute une fois lors du retrait terminal avant que l'état ne devienne `Stopped`. -4. On appelle `Stop` pour soumettre une annulation asynchrone. Le récepteur transite par `Stopping` → `Retiring` → - `Stopped`. -5. On interroge `Stopped` jusqu'à ce qu'il renvoie `true`, on arrête le ring propriétaire, puis on appelle `Close` pour - libérer la zone mappée et le mappage du ring de remplissage. + - `OnStopped` s'exécute une fois lors du retrait terminal avant que l'état ne devienne `Stopped`. +4. On appelle `Stop` pour soumettre une annulation asynchrone. Le récepteur transite par `Stopping` → `Retiring` → `Stopped`. +5. On interroge `Stopped` jusqu'à ce qu'il renvoie `true`, on arrête le ring propriétaire, puis on appelle `Close` pour libérer la zone mappée et le mappage du ring de remplissage. ### Machine à états @@ -620,9 +547,7 @@ Idle → Active → Stopping → Retiring → Stopped - `OnData` et `OnError` sont appelés en série depuis la goroutine de distribution CQE. - `Release` est mono-producteur : il ne doit être appelé que depuis la goroutine de distribution. -- `Stop` ne doit être appelé que lorsqu'il est garanti non concurrent avec la distribution CQE. Il s'agit d'un contrat - de - sérialisation côté appelant. +- `Stop` ne doit être appelé que lorsqu'il est garanti non concurrent avec la distribution CQE. Il s'agit d'un contrat de sérialisation côté appelant. ## Exemples @@ -640,28 +565,20 @@ Les tests d'exemple dans `uring/examples/` illustrent l'API en situation concrè - `echo_test.go`, parcours serveur echo TCP et ping-pong UDP - `timeout_linux_test.go`, opérations de délai et de délai lié -Au niveau du package, `listener_example_test.go` couvre la création d'écouteur et l'accept multishot, tandis que -`socket_integration_linux_test.go` couvre le flux client TCP connect/send. +Au niveau du package, `listener_example_test.go` couvre la création d'écouteur et l'accept multishot, tandis que `socket_integration_linux_test.go` couvre le flux client TCP connect/send. ## Notes opérationnelles - Activez `NotifySucceed` pour obtenir un CQE visible à chaque opération réussie. -- `ring.Features` indique le nombre effectif d'entrées SQ et CQ, la largeur des emplacements SQE, ainsi que l'ordre des - octets - utilisé par le package pour interpréter `user_data`. -- Laissez `MultiIssuers` désactivé pour la configuration mono-émetteur par défaut (`SINGLE_ISSUER` + `DEFER_TASKRUN`), dans laquelle un seul chemin d'exécution de l'appelant sérialise les opérations d'état de soumission (`submit`, `Wait`, `WaitDirect`, `WaitExtended`, `Stop` et resize). N'activez ce fanion que lorsque plusieurs goroutines nécessitent des soumissions concurrentes ou des appels concurrents à `Wait`, `WaitDirect` ou `WaitExtended` ; cela bascule le ring vers la configuration de soumission partagée `COOP_TASKRUN`. +- `ring.Features` indique le nombre effectif d'entrées SQ et CQ, la largeur des emplacements SQE, ainsi que l'ordre des octets utilisé par le package pour interpréter `user_data`. +- Laissez `MultiIssuers` désactivé pour la configuration mono-émetteur par défaut (`SINGLE_ISSUER` + `DEFER_TASKRUN`), dans laquelle un seul chemin d'exécution de l'appelant sérialise les opérations d'état de soumission (`submit`, `Wait`, `WaitDirect`, `WaitExtended`, `Stop` et `ResizeRings`). N'activez ce fanion que lorsque plusieurs goroutines nécessitent des soumissions concurrentes ou des appels concurrents à `Wait`, `WaitDirect` ou `WaitExtended` ; cela bascule le ring vers la configuration de soumission partagée `COOP_TASKRUN`. - `EpollWait` exige que `timeout` vaille `0` ; utilisez `LinkTimeout` si vous avez besoin d'une échéance. - Les vues de complétion empruntées et les contextes issus des pools doivent être libérés ou abandonnés sans délai. -- `ListenerOp.Close` ferme le FD de l'écouteur immédiatement. Si un CQE de mise en place est encore en attente, - drainez-le - d'abord, puis rappelez `Close` pour restituer le `ExtSQE` emprunté au pool. +- `ListenerOp.Close` ferme le FD de l'écouteur immédiatement. Si un CQE de mise en place est encore en attente, drainez-le d'abord, puis rappelez `Close` pour restituer le `ExtSQE` emprunté au pool. ## Support de plateforme -`uring` cible Go 1.26+ et Linux 6.18+ pour le chemin réel adossé au noyau. La plupart des fichiers d'implémentation et -des tests d'exemple portent la directive `//go:build linux`. Les fichiers Darwin fournissent uniquement des bouchons de -compilation pour la surface partagée ; les capacités propres à Linux restent propres à Linux et ne modifient en rien la -base d'exécution Linux décrite ci-dessus. +`uring` cible Go 1.26+ et Linux 6.18+ pour le chemin réel adossé au noyau. La plupart des fichiers d'implémentation et des tests d'exemple portent la directive `//go:build linux` ; les fichiers Darwin fournissent uniquement des bouchons de compilation pour la surface partagée, et les capacités propres à Linux restent propres à Linux sans modifier en rien la base d'exécution Linux décrite ci-dessus. ## Licence diff --git a/README.ja.md b/README.ja.md index b9c0474..6f721d3 100644 --- a/README.ja.md +++ b/README.ja.md @@ -11,11 +11,9 @@ Go で Linux 6.18+ の `io_uring` カーネル境界を扱うパッケージ。 ## 概要 -`uring` は Linux `io_uring` のカーネル境界を提供するワークスペースパッケージです。リングの生成・開始、SQE の組み立て、CQE -の解釈、`user_data` による発行元識別の伝達を担い、バッファ登録・マルチショット操作・リスナー構築の基本操作も備えています。 +`uring` は Linux `io_uring` へのカーネル境界を提供する Go パッケージです。リングの生成・開始、SQE の組み立て、CQE の解釈、`user_data` による発行元識別の伝達を担い、バッファ登録・マルチショット操作・リスナー構築の基本操作をスケジューラにしないまま公開します。 -設計方針として、カーネル側の機構と完了結果の観測を API -境界に留め、スケジューリングやリトライなどの方針決定は上位層に委ねます。呼び出し側のランタイムコードが、完了の相関付け、再試行とバックオフ、ハンドラとセッションのルーティング、コネクションライフサイクル、終端時のリソース解放を担当します。 +パッケージは境界を明示に保ちます:カーネル側の機構と完了結果の観測はこの API にとどまり、方針や組み立ては上位層が担うという役割分担です。呼び出し側のランタイムコードが、完了の相関付け、再試行とバックオフ、ハンドラとセッションのルーティング、コネクションライフサイクル、終端時のリソース解放を担当します。 主な API は以下のとおりです。 @@ -32,10 +30,9 @@ Go で Linux 6.18+ の `io_uring` カーネル境界を扱うパッケージ。 uname -r ``` -`uring` は 6.18+ のベースラインを前提とし、古いカーネル向けのフォールバック分岐を持ちません。そのため、古いカーネル向けの互換分岐ではなく、要件を満たすカーネルを起動してください。 +`uring` は 6.18+ のベースラインを前提とし、古いカーネル向けのフォールバック分岐を持ちません。本パッケージ内に互換シムを期待するのではなく、要件を満たすカーネルを起動してください。 -Debian 13 の stable カーネルはこの要件を満たさない場合があります。6.18 以上の Debian -パッケージ版カーネルが必要な場合は、下記の [Debian 13 カーネル更新](#debian-13-カーネル更新) を参照してください。 +Debian 13 の stable カーネルは 6.18 未満となる場合があります。Debian パッケージ版の 6.18+ カーネルを入手する手順は、下記の [Debian 13 カーネル更新](#debian-13-カーネル更新) を参照してください。 ```bash go get code.hybscloud.com/uring @@ -43,19 +40,15 @@ go get code.hybscloud.com/uring ### Debian 13 カーネル更新 -Debian 13 の安定版トラックが提供するカーネルは 6.12 です。`trixie-backports` スイートから Debian パッケージ済みの 6.18+ -カーネルを導入できます。手順の詳細は [SETUP.md](./SETUP.md) を参照してください。 +Debian 13 の安定版トラックが提供するカーネルは 6.12 です。`trixie-backports` スイートから Debian パッケージ済みの 6.18+ カーネルを導入できます。手順の詳細は [SETUP.md](./SETUP.md) を参照してください。 ### トラブルシューティング -リング生成時に `ENOMEM`・`EPERM`・`ENOSYS` が返る場合、memlock 上限、sysctl 設定、またはカーネルのサポート状況が原因です。コンテナランタイムはデフォルトで -`io_uring` システムコールをブロックします。診断と解決手順は [SETUP.md](./SETUP.md) を参照してください。 +リング生成時に `ENOMEM`・`EPERM`・`ENOSYS` が返る場合、memlock 上限、sysctl 設定、またはカーネルのサポート状況が原因です。コンテナランタイムはデフォルトで `io_uring` システムコールをブロックします。診断と解決手順は [SETUP.md](./SETUP.md) を参照してください。 ## リングのライフサイクル -`New` は未開始状態のリングを返します。操作の発行に先立ち `Start` を呼び出してください。`New` がコンテキストプールを即座に構築し、 -`Start` がリングリソースの登録と有効化を行います。次の例ではファイル read を発行し、対応する CQE を待ち、`iox.Classify` -によって `ErrWouldBlock` を失敗ではなく「進展なし」の意味として扱います。 +`New` は未開始状態のリングを返し、コンテキストプールを即座に構築します。操作の発行に先立ち `Start` を呼び出し、リングリソースの登録と有効化を行います。次の例ではファイル read を発行し、対応する CQE を待ち、`iox.Classify` によって `ErrWouldBlock` を失敗ではなく「進展なし」の意味として扱います。 ```go ring, err := uring.New(func(o *uring.Options) { @@ -111,8 +104,7 @@ for { `Wait` は未送信の SQE をフラッシュしてから完了を回収します。単一発行者リングでは、SQ が空になったあとも遅延タスクを進めるためのカーネル enter も発行します。呼び出し側は `Wait`、`WaitDirect`、`WaitExtended` と他の発行状態操作を直列化する必要があります。`Wait` が返した `err` を `iox.Classify(err)` で分類して `iox.OutcomeWouldBlock` になった場合、それは現時点で境界上に観測可能な完了がないことを示します。 -`Start` と `Stop` がリングのライフサイクルを構成します。`Stop` は冪等ですが、呼び出すとリングは恒久的に使用不能になります。進行中の操作をすべて完了させ、未回収の -CQE を回収し、multishot サブスクリプションを停止してから呼び出してください。 +`Start` と `Stop` がリングのライフサイクルを構成します。`Stop` は冪等ですが、呼び出すとリングは恒久的に使用不能になります。進行中の操作をすべて完了させ、未回収の CQE を回収し、multishot サブスクリプションを停止してから呼び出してください。 ## 型と操作 @@ -151,15 +143,13 @@ CQE を回収し、multishot サブスクリプションを停止してから呼 | リングメッセージ | `MsgRing`, `MsgRingFD`, `FixedFdInstall`, `FilesUpdate` | | コマンド | `UringCmd`, `UringCmd128`, `Nop`, `Nop128` | -`Nop128` と `UringCmd128` は `Options.SQE128` 付きで作成したリングを必要とし、対応する opcode -がカーネルで利用可能でなければなりません。条件を満たさない場合は `ErrNotSupported` を返します。 +`Nop128` と `UringCmd128` は `Options.SQE128` 付きで作成したリングと、対応する opcode のカーネルサポートの両方を必要とします。いずれかが欠ける場合は `ErrNotSupported` を返します。 `Uring.Close` は指定したファイルディスクリプタに `IORING_OP_CLOSE` を発行します。リング自体の破棄ではありません。 ## コンテキスト伝達 -`SQEContext` は `uring` の主要な識別トークンです。direct モードでは opcode・SQE flags・buffer group ID・ファイルディスクリプタを -64 ビット値ひとつに格納します。 +`SQEContext` はこのパッケージの主要な識別トークンです。direct モードでは opcode、SQE flags、buffer group ID、ファイルディスクリプタを 64 ビット値ひとつに格納します。 ```go sqeCtx := uring.ForFD(fd). @@ -175,13 +165,9 @@ sqeCtx := uring.ForFD(fd). | Indirect | `IndirectSQE` へのポインタ | 64 ビットでは SQE 全体を表現できない場合 | | Extended | `ExtSQE` へのポインタ | 完全な SQE と 64 バイトのユーザーデータ | -通常は `ForFD` または `PackDirect` から開始し、完了時に参照したい情報だけを付加します。`WithFlags` -はフラグ集合全体を上書きするため、論理和は事前に計算してから渡してください。 +通常は `ForFD` または `PackDirect` から開始し、完了時に参照したい情報だけを付加します。`WithFlags` はフラグ集合全体を上書きするため、論理和は事前に計算してから渡してください。 -direct の 64 ビット配置に収まらない独自メタデータが必要な場合は、`ExtSQE` を借用し、`Ctx*Of` や `ViewCtx*` で `UserData` -に書き込んだうえで `SQEContext` に再パックします。`UserData` にはスカラー値を格納するのが望ましく、Go -ポインタ、インターフェース値、関数値、スライス、文字列、マップ、チャネル、またはそれらを含む構造体を生のオーバーレイや型付きビュー経由で置く場合は、GC -が生バイトを走査しないため、参照元を `UserData` の外に保持する必要があります。 +direct の 64 ビット配置に収まらない独自メタデータが必要な場合は、`ExtSQE` を借用し、`Ctx*Of` や `ViewCtx*` で `UserData` に書き込んだうえで `SQEContext` に再パックします。`UserData` にはスカラー値を格納するのが望ましく、Go ポインタ、インターフェース値、関数値、スライス、文字列、マップ、チャネル、またはそれらを含む構造体を生のオーバーレイや型付きビュー経由で置く場合は、GC がその生バイトを走査しないため、参照元を `UserData` の外に保持する必要があります。 ```go ext := ring.ExtSQE() @@ -196,8 +182,7 @@ fmt.Printf("sqe context mode=%d seq=%d\n", sqeCtx.Mode(), meta.Val1) ### `CQEView` による完了ディスパッチ -`uring` は完了コンテキスト専用の型を持ちません。完了のディスパッチは `CQEView` を介して行い、発行時のトークンが必要であれば -`cqe.Context()` で取得します。 +`uring` は完了コンテキスト専用の型を持ちません。完了のディスパッチは `CQEView` を介して行い、発行時のトークンが必要であれば `cqe.Context()` で取得します。 ```go cqes := make([]uring.CQEView, 64) @@ -234,20 +219,17 @@ for i := 0; i < n; i++ { } ``` -完了時、`CQEView` は対応するコンテキストモードをオンデマンドでデコードします。`CQEView`・`IndirectSQE`・`ExtSQE` -・借用バッファは、それぞれ定められたライフタイムを超えて保持しないでください。 +完了時、`CQEView` は対応するコンテキストモードをオンデマンドでデコードします。`CQEView`、`IndirectSQE`、`ExtSQE`、借用バッファは、それぞれ定められたライフタイムを超えて保持しないでください。 ## バッファ供給 -`uring` には実用上 3 つのバッファ経路があります。登録バッファはリング開始時に固定され、固定バッファのファイル I/O -で使います。提供バッファリングではカーネルが受信バッファを選び、選択されたバッファ ID を CQE に返します。バンドル受信は -1 つの CQE で連続した論理バッファ範囲を消費し、その範囲を `BundleIterator` で公開します。 +`uring` には実用上 3 つのバッファ経路があります。登録バッファはリング開始時に固定され、固定バッファのファイル I/O で使います。提供バッファリングではカーネルが受信バッファを選び、選択されたバッファ ID を CQE に返します。バンドル受信は 1 つの CQE で連続した論理バッファ範囲を消費し、その範囲を `BundleIterator` で公開します。 - `ReadBufferSize`・`ReadBufferNum` による固定サイズの提供バッファ - `MultiSizeBuffer` によるマルチサイズバッファグループ - `LockedBufferMem`・`RegisteredBuffer`・`ReadFixed`・`WriteFixed` による登録固定バッファ -多くの場合、設定ヘルパーから始めるのが簡単です。 +多くのシステムでは、設定ヘルパーから始めるのが手っ取り早い入り口です。 ```go opts := uring.OptionsForSystem(uring.MachineMemory4GB) @@ -256,8 +238,7 @@ ring, err := uring.New(func(o *uring.Options) { }) ``` -メモリ予算を明示的に指定したい場合は `OptionsForBudget` を、予算に対して選択される階層構成を確認したい場合は -`BufferConfigForBudget` を使用してください。 +メモリ予算を明示的に指定したい場合は `OptionsForBudget` を、予算に対して選択される階層構成を確認したい場合は `BufferConfigForBudget` を使用してください。 ```go cfg, scale := uring.BufferConfigForBudget(256 * uring.MiB) @@ -293,8 +274,7 @@ if cqe.HasBuffer() { } ``` -バンドル受信は同じ提供バッファストレージを使いますが、1 つの CQE -で複数のバッファを消費することがあります。イテレータを処理したら、消費したスロットを回収してください。 +バンドル受信は同じ提供バッファストレージを使いますが、1 つの CQE で複数のバッファを消費することがあります。イテレータを処理したら、消費したスロットを回収してください。 ```go if err := ring.ReceiveBundle(recvCtx, &socketFD, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { @@ -309,16 +289,13 @@ if it, ok := ring.BundleIterator(cqe, cqe.BufGroup()); ok { } ``` -登録バッファにはピン留めされたメモリが必要です。大きなバッファの登録に失敗する場合は `RLIMIT_MEMLOCK` -を引き上げるか、メモリ予算を小さくしてください。 +登録バッファにはピン留めされたメモリが必要です。大きなバッファの登録に失敗する場合は `RLIMIT_MEMLOCK` を引き上げるか、メモリ予算を小さくしてください。 ## マルチショットとリスナー操作 -`AcceptMultishot`・`ReceiveMultishot`・`SubmitAcceptMultishot`・`SubmitAcceptDirectMultishot`・`SubmitReceiveMultishot`・ -`SubmitReceiveBundleMultishot` がマルチショットのソケット操作を発行します。 +`AcceptMultishot`、`ReceiveMultishot`、`SubmitAcceptMultishot`、`SubmitAcceptDirectMultishot`、`SubmitReceiveMultishot`、`SubmitReceiveBundleMultishot` がマルチショットのソケット操作を発行します。 -CQE のルーティング方針はパッケージ外に委ねています。リスナーの構築は `DecodeListenerCQE`・`PrepareListenerBind`・ -`PrepareListenerListen`・`SetListenerReady` の順で進みますが、完了のディスパッチ方法や連鎖の停止条件は呼び出し側が決定します。 +CQE のルーティング方針はパッケージ外に委ねています。リスナーの構築は `DecodeListenerCQE`、`PrepareListenerBind`、`PrepareListenerListen`、`SetListenerReady` の順で進みますが、完了のディスパッチ方法や連鎖の停止条件は呼び出し側が決定します。 ## 実装構造 @@ -334,14 +311,9 @@ CQE のルーティング方針はパッケージ外に委ねています。リ ## ランタイム境界 -`uring` の上位にあるランタイム層は、これをスケジューラではなくカーネルバックエンドとして扱うべきです。理想的な境界は一方向です。 -`uring` は SQE の準備、CQE の回収、`user_data` の保持、CQE `res` とフラグの公開、所有権事実の報告を担います。呼び出し側ランタイムコードは、それらの観測を自身の -トークンと相関付け、再試行とバックオフを適用し、ハンドラとセッションをルーティングし、発行をバッチ化し、終端リソースを解放します。 +`uring` の上位にあるランタイム層は、これをスケジューラではなくカーネルバックエンドとして扱うべきです。理想的な境界は一方向で、`uring` は SQE の準備、CQE の回収、`user_data` の保持、CQE `res` とフラグの公開、所有権事実の報告を担い、呼び出し側ランタイムコードはそれらの観測を自身のトークンと相関付け、再試行とバックオフを適用し、ハンドラとセッションをルーティングし、発行をバッチ化し、終端リソースを解放します。 -抽象実行が完了事実を必要とする場合、ランタイムブリッジは Extended モード CQE を消費できます。コネクション単位のランタイムは、CQE -の結果、フラグ、バッファ ID、エンコード済みトークンを必要とする場合、イベントをハンドラコールバックに還元する前に生の -Extended CQE -を直接ポーリングできます。 +抽象実行が完了事実を必要とする場合、ランタイムブリッジは Extended モード CQE を消費できます。コネクション単位のランタイムは、CQE の結果、フラグ、バッファ ID、エンコード済みトークンを必要とする場合、イベントをハンドラコールバックに還元する前に生の Extended CQE を直接ポーリングできます。 この境界の上にあるコンテキスト層と抽象実行層は、`uring` のカーネル境界としての役割を変更しません。 @@ -351,8 +323,7 @@ Extended CQE ### リング所有イベントループ -シングルイシュアーモード(デフォルト)では、1 つの goroutine がすべての発行側操作を直列化します。一般的なループは保留中のワークを発行し、 -`Wait` が観測可能な進展を返さないときは呼び出し側の `iox.Backoff` を適用し、完了を振り分けます。 +シングルイシュアーモード(デフォルト)では、1 つの goroutine がすべての発行状態操作を直列化します。一般的なループでは保留中のワークを発行し、`Wait` が観測可能な進展を返さないときは呼び出し側の `iox.Backoff` を適用し、完了を振り分けます。 ```go func runLoop(ring *uring.Uring, stop <-chan struct{}) error { @@ -386,10 +357,7 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { } ``` -`Send`、`Receive`、`AcceptMultishot`、`Wait` などのリングメソッドはすべてこの goroutine 上で実行します。他の goroutine -からの作業はチャネルまたはロックフリーキューを経由してループに渡します。リングメソッドを直接呼び出してはなりません。 -`iox.Backoff` は呼び出し側が所有します。`Wait` が `iox.OutcomeWouldBlock` に分類された場合、または CQE を 1 件も回収できなかった場合は -`backoff.Wait()` を呼び、`n > 0` のバッチを回収できたら `backoff.Reset()` を呼びます。 +`Send`、`Receive`、`AcceptMultishot`、`Wait` などのリングメソッドはすべてこの goroutine 上で実行します。他の goroutine からの作業はチャネルまたはロックフリーキューを経由してループに渡します。リングメソッドを直接呼び出してはなりません。`iox.Backoff` は呼び出し側が所有します。`Wait` が `iox.OutcomeWouldBlock` に分類された場合、または CQE を 1 件も回収できなかった場合は `backoff.Wait()` を呼び、`n > 0` のバッチを回収できたら `backoff.Reset()` を呼びます。 ### マルチショットサブスクリプションのライフサイクル @@ -416,6 +384,8 @@ if err != nil { return err } +// 同じ直列化された完了ループ内で dispatch します。コピーした CQE を +// このループの後も保持する場合、呼び出し側が独自の route state を保持してください。 for i := range n { if sub.HandleCQE(cqes[i]) { continue @@ -424,7 +394,7 @@ for i := range n { } ``` -`OnMultishotStep` は各完了を観察します。ストリームを維持するなら `MultishotContinue` を、キャンセルを要求するなら `MultishotStop` を返します。`OnMultishotStop` は終端状態で一度だけ実行されます。クリーンアップと条件付き再サブスクリプションに使用します。既定の単一発行者リングでは、`Cancel` / `Unsubscribe` はリング所有者から呼び出すか、発行、`Wait`、`WaitDirect`、`WaitExtended`、`Stop`、リサイズ操作と直列化してください。`MultiIssuers` リングでは、共有発行パスがこれらのキャンセル SQE を直列化します。 +`OnMultishotStep` は各完了を観察します。ストリームを維持するなら `MultishotContinue` を、キャンセルを要求するなら `MultishotStop` を返します。`OnMultishotStop` は終端状態で一度だけ実行されます。クリーンアップと条件付き再サブスクリプションに使用します。`HandleCQE` は呼び出し側の直列化された完了ループ内で即時 dispatch するための補助です。コピー済み CQE をそのループの後も保持する場合、呼び出し側が独自の route state を保持し、すでに解放されたサブスクリプションの観察を拒否してください。既定の単一発行者リングでは、`Cancel` / `Unsubscribe` はリング所有者から呼び出すか、発行、`Wait`、`WaitDirect`、`WaitExtended`、`Stop`、`ResizeRings` と直列化してください。`MultiIssuers` リングでは、共有発行パスがこれらのキャンセル SQE を直列化します。 ### 型付きコンテキストによるコネクション状態 @@ -458,12 +428,11 @@ seq := ctx.Val1 ring.PutExtSQE(ext) ``` -ライブな Go ポインタルートは `UserData` の外側で到達可能に保ってください。GC はそれらの生バイトをトレースしません。内部のマルチショットおよびリスナープロトコルでは各 -`ExtSQE` スロットに付属するサイドカーのルートセットが処理しますが、型付き参照を配置するフレームワークコードは独自に到達可能性を維持する必要があります。 +ライブな Go ポインタルートは `UserData` の外側で到達可能に保ってください。GC はそれらの生バイトをトレースしません。内部のマルチショットおよびリスナープロトコルでは各 `ExtSQE` スロットに付属するサイドカーのルートセットが処理しますが、型付き参照を配置する呼び出し側ランタイムコードは独自に到達可能性を維持する必要があります。 ### デッドライン合成 -`LinkTimeout` は `IOSQE_IO_LINK` チェーンを通じて直前の SQE にデッドラインを付与します。操作とタイムアウトが競合し、一方が完了すると他方はキャンセルされます。 +`LinkTimeout` は `IOSQE_IO_LINK` チェーンを通じて直前の SQE にデッドラインを付与します。操作とタイムアウトが競合し、一方が完了するともう一方はキャンセルされます。 ```go recvCtx := uring.ForFD(fd). @@ -480,8 +449,7 @@ if err := ring.LinkTimeout(timeoutCtx, 5*time.Second); err != nil { } ``` -フレームワーク層は両方の結果を処理します。受信成功はタイムアウトをキャンセルし、タイムアウト発火は受信をキャンセルします。いずれも -CQE を生成するため、ディスパッチループで観察する必要があります。 +呼び出し側ランタイムは両方の結果を処理します。受信成功はタイムアウトをキャンセルし、タイムアウト発火は受信をキャンセルします。いずれも CQE を生成するため、ディスパッチループで観察する必要があります。 ## TCP の使用パターン @@ -494,8 +462,7 @@ CQE を生成するため、ディスパッチループで観察する必要が ### TCP Echo サーバ -socket → bind → listen の一連の手順をパッケージに任せるには `ListenerManager` を使います。リスナーの準備が完了したら、接続済み -FD に対して multishot accept と multishot receive を開始します。 +socket → bind → listen の一連の手順をパッケージに任せるには `ListenerManager` を使います。リスナーの準備が完了したら、接続済み FD に対して multishot accept と multishot receive を開始します。 ```go pool := uring.NewContextPools(32) @@ -520,13 +487,11 @@ if err != nil { defer recvSub.Cancel() ``` -`listener_example_test.go` はリスナーの構築と multishot accept、`examples/multishot_test.go` はハンドラ側の multishot -receive CQE の処理、`examples/echo_test.go` はループバック echo の一連の流れをそれぞれ示しています。 +`listener_example_test.go` はリスナーの構築と multishot accept、`examples/multishot_test.go` はハンドラ側の multishot receive CQE の処理、`examples/echo_test.go` はループバック echo の一連の流れをそれぞれ示しています。 ### TCP クライアント -ソケットを作成し、`IORING_OP_SOCKET` の完了を待ちます。返された FD を `iofd.FD` に変換したうえで `Connect`・`Send`・ -`Receive` に渡します。 +ソケットを作成し、`IORING_OP_SOCKET` の完了を待ちます。返された FD を `iofd.FD` に変換したうえで `Connect`、`Send`、`Receive` に渡します。 ```go clientCtx := uring.PackDirect(uring.IORING_OP_SOCKET, 0, 0, 0) @@ -552,25 +517,20 @@ if err := ring.Receive(recvCtx, &clientFD, buf); err != nil { } ``` -各発行のあとは「リングのライフサイクル」節で示した `Wait` ループで対応する完了を取得します。パッケージレベルの -`socket_integration_linux_test.go` に connect/send の一連の流れがあります。 +各発行のあとは「リングのライフサイクル」節で示した `Wait` ループを使って対応する完了を取得します。パッケージレベルの `socket_integration_linux_test.go` が connect/send の一連の流れをカバーしています。 ## ゼロコピー受信(ZCRX) `ZCRXReceiver` は `io_uring` を通じて NIC ハードウェア RX キューからゼロコピー受信を行う仕組みを管理します。 -`NewZCRXReceiver` は 32 バイト CQE(`IORING_SETUP_CQE32`)で作成されたリング向けに用意されています。現在の `Options` はこの -設定フラグを公開していないため、標準の `New` で作成したリングに対してはこのコンストラクタは `ErrNotSupported` -を返します。CQE32 の設定経路が公開されるまでは、この節は実行可能な公開セットアップ手順ではなく、レシーバの境界契約を説明するものです。 +`NewZCRXReceiver` は 32 バイト CQE(`IORING_SETUP_CQE32`)で作成されたリング向けに用意されています。現在の `Options` はこの設定フラグを公開していないため、標準の `New` で作成したリングに対してはこのコンストラクタは `ErrNotSupported` を返します。CQE32 の設定経路が公開されるまでは、この節は実行可能な公開セットアップ手順ではなく、レシーバの境界契約を説明するものです。 ### ライフサイクル -1. CQE32 対応リング上で `NewZCRXReceiver` を生成する。コンストラクタが ZCRX インターフェースキューの登録、補充 - 領域のマッピング、補充リングの準備を行う。 +1. CQE32 対応リング上で `NewZCRXReceiver` を生成する。コンストラクタが ZCRX インターフェースキューの登録、補充領域のマッピング、補充リングの準備を行う。 2. `Start` を呼び出し、リング上で拡張 `RECV_ZC` 操作を発行する。 3. CQE のディスパッチ経路で ZCRX 完了が `ZCRXHandler` に届く。 - - `OnData`: NIC マップ領域を指す `ZCRXBuffer` を受け取る。処理後に `Release` を呼んでカーネルにスロットを返却する。ベストエフォートでの停止を要求するには - `false` を返す。 + - `OnData`: NIC マップ領域を指す `ZCRXBuffer` を受け取る。処理後に `Release` を呼んでカーネルにスロットを返却する。ベストエフォートでの停止を要求するには `false` を返す。 - `OnError`: CQE エラーを受け取る。ベストエフォートでの停止を要求するには `false` を返す。 - `OnStopped`: 状態が `Stopped` に移行する直前に一度だけ呼ばれる。 4. `Stop` を呼び出して非同期キャンセルを発行する。レシーバは `Stopping` → `Retiring` → `Stopped` と遷移する。 @@ -606,24 +566,20 @@ Idle → Active → Stopping → Retiring → Stopped - `echo_test.go`: TCP echo サーバと UDP ping-pong - `timeout_linux_test.go`: タイムアウトとリンクタイムアウト -パッケージレベルの `listener_example_test.go` はリスナー構築と multishot accept を、`socket_integration_linux_test.go` は -TCP クライアントの connect/send フローをそれぞれ示しています。 +パッケージレベルの `listener_example_test.go` はリスナー構築と multishot accept を、`socket_integration_linux_test.go` は TCP クライアントの connect/send フローをそれぞれ示しています。 ## 運用上の注意 - すべての成功操作に可視の CQE が必要な場合は `NotifySucceed` を有効にする。 - `ring.Features` は実際の SQ エントリ数・CQ エントリ数・SQE スロット幅・`user_data` 解釈時のバイト順を返す。 -- 既定では `MultiIssuers` を無効のまま、単一発行者構成(`SINGLE_ISSUER` + `DEFER_TASKRUN`)を使用する。この構成では、呼び出し側が 1 つの実行パスで発行状態操作(`submit`、`Wait`、`WaitDirect`、`WaitExtended`、`Stop`、resize)を直列化する。複数 goroutine から並行して発行または `Wait`、`WaitDirect`、`WaitExtended` 呼び出しを行う必要がある場合にのみ `MultiIssuers` を有効にし、`COOP_TASKRUN` 構成に切り替える。 +- 既定では `MultiIssuers` を無効のまま、単一発行者構成(`SINGLE_ISSUER` + `DEFER_TASKRUN`)を使用する。この構成では、呼び出し側が 1 つの実行パスで発行状態操作(`submit`、`Wait`、`WaitDirect`、`WaitExtended`、`Stop`、`ResizeRings`)を直列化する。複数 goroutine から並行して発行または `Wait`、`WaitDirect`、`WaitExtended` 呼び出しを行う必要がある場合にのみ `MultiIssuers` を有効にし、`COOP_TASKRUN` 構成に切り替える。 - `EpollWait` の `timeout` は `0` のままにすること。期限が必要な場合は `LinkTimeout` を使う。 - 借用中の完了ビューやプール由来のコンテキストは速やかに解放すること。 -- `ListenerOp.Close` はリスナー FD を即座に閉じる。構築中の CQE が未回収の場合は、その CQE を回収してから再度 `Close` - を呼び出し、借用中の `ExtSQE` をプールに返却する。 +- `ListenerOp.Close` はリスナー FD を即座に閉じる。構築中の CQE が未回収の場合は、その CQE を回収してから再度 `Close` を呼び出し、借用中の `ExtSQE` をプールに返却する。 ## 対応プラットフォーム -`uring` の実カーネルパスは Go 1.26+ / Linux 6.18+ を対象としています。実装ファイルとテストの大半は `//go:build linux` -で保護されています。Darwin 向けファイルは共有 API 面向けのコンパイルスタブのみを提供します。Linux 専用機能は引き続き -Linux 専用であり、上述の Linux 実行時ベースラインを変えません。 +`uring` の実カーネルパスは Go 1.26+ / Linux 6.18+ を対象としています。実装ファイルとテストの大半は `//go:build linux` で保護されており、Darwin 向けファイルは共有 API 面向けのコンパイルスタブのみを提供します。Linux 専用機能は引き続き Linux 専用であり、上述の Linux 実行時ベースラインを変えません。 ## ライセンス diff --git a/README.md b/README.md index ce2af8a..541eb7d 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,9 @@ Language: **English** | [简体中文](./README.zh-CN.md) | [Español](./README. ## Overview -`uring` is the kernel-facing boundary for Linux `io_uring`. It creates and starts rings, prepares SQEs, decodes CQEs, -carries submission identity through `user_data`, and exposes buffer registration, multishot operations, and -listener-setup primitives without turning them into a scheduler. +`uring` is the kernel-facing boundary for Linux `io_uring`. It creates and starts rings, prepares SQEs, decodes CQEs, carries submission identity through `user_data`, and exposes buffer registration, multishot operations, and listener-setup primitives without turning them into a scheduler. -The package keeps the boundary explicit: kernel mechanics and observable completion facts live here; policy and -composition live above it. Caller-side runtime code owns completion correlation, retry/backoff, handler and session -routing, connection lifecycle, and terminal resource release. +The package keeps the boundary explicit: kernel mechanics and observable completion facts live here; policy and composition live above it. Caller-side runtime code owns completion correlation, retry/backoff, handler and session routing, connection lifecycle, and terminal resource release. The primary surfaces are: @@ -34,11 +30,9 @@ The primary surfaces are: uname -r ``` -`uring` assumes the 6.18+ baseline and carries no fallback branches for older kernels. Boot a supported kernel instead -of expecting compatibility shims inside this package. +`uring` assumes the 6.18+ baseline and carries no fallback branches for older kernels. Boot a supported kernel instead of expecting compatibility shims inside this package. -Debian 13's stable kernel track may still be below 6.18. See [Debian 13 kernel upgrade](#debian-13-kernel-upgrade) for -the backports path to a kernel that meets the requirement. +Debian 13's stable kernel track may still be below 6.18. See [Debian 13 kernel upgrade](#debian-13-kernel-upgrade) for the backports path to a kernel that meets the requirement. ```bash go get code.hybscloud.com/uring @@ -46,19 +40,15 @@ go get code.hybscloud.com/uring ### Debian 13 kernel upgrade -Debian 13 ships kernel 6.12 in its stable track. The `trixie-backports` suite provides a Debian-packaged 6.18+ kernel. -See [SETUP.md](./SETUP.md) for step-by-step instructions. +Debian 13 ships kernel 6.12 in its stable track. The `trixie-backports` suite provides a Debian-packaged 6.18+ kernel. See [SETUP.md](./SETUP.md) for step-by-step instructions. ### Troubleshooting -Ring creation may return `ENOMEM`, `EPERM`, or `ENOSYS` depending on memlock limits, sysctl settings, or kernel support. -Container runtimes block `io_uring` syscalls by default. See [SETUP.md](./SETUP.md) for diagnosis and resolution. +Ring creation may return `ENOMEM`, `EPERM`, or `ENOSYS` depending on memlock limits, sysctl settings, or kernel support. Container runtimes block `io_uring` syscalls by default. See [SETUP.md](./SETUP.md) for diagnosis and resolution. ## Ring lifecycle -`New` returns an unstarted ring and eagerly constructs the context pools. Call `Start` before submitting operations; it -registers ring resources and enables the ring. The example below submits a read, waits for the matching CQE, and uses -`iox.Classify` so `ErrWouldBlock` stays a semantic no-progress result rather than a failure. +`New` returns an unstarted ring and eagerly constructs the context pools. Call `Start` before submitting operations; it registers ring resources and enables the ring. The example below submits a read, waits for the matching CQE, and uses `iox.Classify` so `ErrWouldBlock` stays a semantic no-progress result rather than a failure. ```go ring, err := uring.New(func(o *uring.Options) { @@ -114,9 +104,7 @@ for { `Wait` flushes pending submissions, then reaps completions. On single-issuer rings it also issues the kernel enter that keeps deferred task work moving once the SQ drains; the caller must serialize `Wait`, `WaitDirect`, and `WaitExtended` with other submit-state operations. If `iox.Classify(err)` yields `iox.OutcomeWouldBlock`, no completion is currently observable at the boundary. -`Start` and `Stop` form the ring lifecycle pair. `Stop` is idempotent and renders the ring permanently unusable; call it -only after you have drained all in-flight operations, reaped outstanding CQEs, and quiesced live multishot -subscriptions. +`Start` and `Stop` form the ring lifecycle pair. `Stop` is idempotent and renders the ring permanently unusable; call it only after you have drained all in-flight operations, reaped outstanding CQEs, and quiesced live multishot subscriptions. ## Types and operations @@ -155,15 +143,13 @@ Operations: | Ring msg | `MsgRing`, `MsgRingFD`, `FixedFdInstall`, `FilesUpdate` | | Cmd | `UringCmd`, `UringCmd128`, `Nop`, `Nop128` | -`Nop128` and `UringCmd128` require a ring created with `Options.SQE128` and kernel support for the corresponding -opcodes. Without both, they return `ErrNotSupported`. +`Nop128` and `UringCmd128` require a ring created with `Options.SQE128` and kernel support for the corresponding opcodes. Without both, they return `ErrNotSupported`. `Uring.Close` submits `IORING_OP_CLOSE` for a target file descriptor. It is not a ring teardown method. ## Context transport -`SQEContext` is the primary identity token. In direct mode it packs the opcode, SQE flags, buffer-group ID, and file -descriptor into a single 64-bit value. +`SQEContext` is the primary identity token. In direct mode it packs the opcode, SQE flags, buffer-group ID, and file descriptor into a single 64-bit value. ```go sqeCtx := uring.ForFD(fd). @@ -179,13 +165,9 @@ The three context modes are: | Indirect | Pointer to `IndirectSQE` | Full SQE payload when 64 bits are not enough | | Extended | Pointer to `ExtSQE` | Full SQE plus 64 bytes of user data | -For the common path, start with `ForFD` or `PackDirect` and attach only the bits you need to see again at completion -time. `WithFlags` replaces the entire flag set, so compute unions before calling it. +For the common path, start with `ForFD` or `PackDirect` and attach only the bits you need to see again at completion time. `WithFlags` replaces the entire flag set, so compute unions before calling it. -When you need caller-owned metadata beyond the 64-bit direct layout, borrow an `ExtSQE`, write into its `UserData` -through `Ctx*Of` or `ViewCtx*`, and pack it back into an `SQEContext`. Prefer scalar payloads. If a raw overlay or typed -view stores Go pointers, interfaces, func values, slices, strings, maps, chans, or structs containing them, keep the -live roots outside `UserData`; the GC does not trace those raw bytes. +When you need caller-owned metadata beyond the 64-bit direct layout, borrow an `ExtSQE`, write into its `UserData` through `Ctx*Of` or `ViewCtx*`, and pack it back into an `SQEContext`. Prefer scalar payloads. If a raw overlay or typed view stores Go pointers, interfaces, func values, slices, strings, maps, chans, or structs containing them, keep the live roots outside `UserData`; the GC does not trace those raw bytes. ```go ext := ring.ExtSQE() @@ -196,13 +178,11 @@ sqeCtx := uring.PackExtended(ext) fmt.Printf("sqe context mode=%d seq=%d\n", sqeCtx.Mode(), meta.Val1) ``` -`NewContextPools` returns pools that are ready to use. Call `Reset` only once all borrowed contexts have been returned -and you want to reuse the pool set. +`NewContextPools` returns pools that are ready to use. Call `Reset` only once all borrowed contexts have been returned and you want to reuse the pool set. ### Completion dispatch with `CQEView` -There is no separate completion-context type. All completion dispatch goes through `CQEView`; call `cqe.Context()` to -recover the original submission token. +There is no separate completion-context type. All completion dispatch goes through `CQEView`; call `cqe.Context()` to recover the original submission token. ```go cqes := make([]uring.CQEView, 64) @@ -239,14 +219,11 @@ for i := 0; i < n; i++ { } ``` -`CQEView` decodes the matching context mode on demand at completion time. `CQEView`, `IndirectSQE`, `ExtSQE`, and -borrowed buffers must not outlive their documented lifetimes. +`CQEView` decodes the matching context mode on demand at completion time. `CQEView`, `IndirectSQE`, `ExtSQE`, and borrowed buffers must not outlive their documented lifetimes. ## Buffer provisioning -`uring` has three practical buffer paths. Registered buffers are pinned during ring setup and used by fixed-buffer file -I/O. Provided buffer rings let the kernel choose a receive buffer and report the selected buffer ID in the CQE. Bundle -receives consume a contiguous logical range of provided buffers and expose that range through `BundleIterator`. +`uring` has three practical buffer paths. Registered buffers are pinned during ring setup and used by fixed-buffer file I/O. Provided buffer rings let the kernel choose a receive buffer and report the selected buffer ID in the CQE. Bundle receives consume a contiguous logical range of provided buffers and expose that range through `BundleIterator`. - fixed-size provided buffers through `ReadBufferSize` and `ReadBufferNum` - multi-size buffer groups through `MultiSizeBuffer` @@ -261,16 +238,14 @@ ring, err := uring.New(func(o *uring.Options) { }) ``` -Use `OptionsForBudget` to start from an explicit memory budget, or `BufferConfigForBudget` to inspect the tier layout -chosen for a given budget: +Use `OptionsForBudget` to start from an explicit memory budget, or `BufferConfigForBudget` to inspect the tier layout chosen for a given budget: ```go cfg, scale := uring.BufferConfigForBudget(256 * uring.MiB) fmt.Printf("buffer tiers=%+v scale=%d\n", cfg, scale) ``` -Fixed-buffer I/O uses a registered buffer by index. The returned slice is ring-owned memory; keep it live until the -fixed operation completes: +Fixed-buffer I/O uses a registered buffer by index. The returned slice is ring-owned memory; keep it live until the fixed operation completes: ```go buf := ring.RegisteredBuffer(0) @@ -283,8 +258,7 @@ if err := ring.WriteFixed(ctx, 0, len(payload)); err != nil { } ``` -For socket receive with kernel buffer selection, pass `nil` as the receive buffer and request the size class you want. -The completion reports which buffer was selected: +For socket receive with kernel buffer selection, pass `nil` as the receive buffer and request the size class you want. The completion reports which buffer was selected: ```go recvCtx := uring.PackDirect(uring.IORING_OP_RECV, 0, 0, 0) @@ -299,8 +273,7 @@ if cqe.HasBuffer() { } ``` -Bundle receives use the same provided-buffer storage but may consume more than one buffer in a single CQE. Process the -iterator, then recycle the consumed slots: +Bundle receives use the same provided-buffer storage but may consume more than one buffer in a single CQE. Process the iterator, then recycle the consumed slots: ```go if err := ring.ReceiveBundle(recvCtx, &socketFD, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { @@ -319,12 +292,9 @@ Registered buffers require pinned memory. If large buffer registration fails, in ## Multishot and listener operations -`AcceptMultishot`, `ReceiveMultishot`, `SubmitAcceptMultishot`, `SubmitAcceptDirectMultishot`, `SubmitReceiveMultishot`, -and `SubmitReceiveBundleMultishot` each submit a multishot socket operation. +`AcceptMultishot`, `ReceiveMultishot`, `SubmitAcceptMultishot`, `SubmitAcceptDirectMultishot`, `SubmitReceiveMultishot`, and `SubmitReceiveBundleMultishot` each submit a multishot socket operation. -CQE routing policy stays outside the package. Listener setup progresses through `DecodeListenerCQE`, -`PrepareListenerBind`, `PrepareListenerListen`, and `SetListenerReady`; the caller decides how to dispatch completions -and when to stop the chain. +CQE routing policy stays outside the package. Listener setup progresses through `DecodeListenerCQE`, `PrepareListenerBind`, `PrepareListenerListen`, and `SetListenerReady`; the caller decides how to dispatch completions and when to stop the chain. ## Architecture implementation @@ -334,33 +304,25 @@ The implementation sits at this boundary: 2. `Start` registers buffers and enables the ring for the 6.18+ baseline. 3. Operation methods express intent by writing SQEs. 4. `Wait` flushes submissions and returns borrowed CQE views. -5. Caller-side runtime code decides scheduling, retries, parking, connection/session routing, and terminal resource - policy. +5. Caller-side runtime code decides scheduling, retries, parking, connection/session routing, and terminal resource policy. This keeps `uring` focused on kernel-facing mechanics and preserves completion meaning across the boundary. ## Runtime boundary -Runtime layers above `uring` should use it as the kernel backend, not as a scheduler. The ideal seam is one-way: `uring` -prepares SQEs, reaps CQEs, preserves `user_data`, exposes CQE `res` and flags, and reports ownership facts; caller-side -runtime code correlates those observations with its own tokens, applies retry/backoff, routes handlers and sessions, -batches submissions, and releases terminal resources. +Runtime layers above `uring` should use it as the kernel backend, not as a scheduler. The ideal seam is one-way: `uring` prepares SQEs, reaps CQEs, preserves `user_data`, exposes CQE `res` and flags, and reports ownership facts; caller-side runtime code correlates those observations with its own tokens, applies retry/backoff, routes handlers and sessions, batches submissions, and releases terminal resources. -A runtime bridge can consume Extended-mode CQEs when abstract execution needs completion facts. A connection-scoped -runtime can also poll raw Extended CQEs directly when it needs the CQE result, flags, buffer ID, and encoded token -before reducing the event to handler callbacks. +A runtime bridge can consume Extended-mode CQEs when abstract execution needs completion facts. A connection-scoped runtime can also poll raw Extended CQEs directly when it needs the CQE result, flags, buffer ID, and encoded token before reducing the event to handler callbacks. Context and abstract-execution layers above this boundary do not change `uring`'s kernel-boundary role. ## Application-layer patterns -`uring` exposes kernel mechanics; scheduling, retry, connection tracking, and protocol interpretation belong in the -layers above it. The patterns below describe the boundary a caller-side runtime must preserve. +`uring` exposes kernel mechanics; scheduling, retry, connection tracking, and protocol interpretation belong in the layers above it. The patterns below describe the boundary a caller-side runtime must preserve. ### Ring-owning event loop -In single-issuer mode (the default), one goroutine serializes all submit-state operations. A typical loop submits -pending work, applies caller-owned `iox.Backoff` when `Wait` reports no observable progress, and dispatches completions: +In single-issuer mode (the default), one goroutine serializes all submit-state operations. A typical loop submits pending work, applies caller-owned `iox.Backoff` when `Wait` reports no observable progress, and dispatches completions: ```go func runLoop(ring *uring.Uring, stop <-chan struct{}) error { @@ -394,15 +356,11 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { } ``` -All ring methods, including `Send`, `Receive`, `AcceptMultishot`, and `Wait`, run on this goroutine. Work from other -goroutines enters the loop through a channel or a lock-free queue, not by calling ring methods directly. `iox.Backoff` -stays caller-owned: call `backoff.Wait()` on `iox.OutcomeWouldBlock` or when `Wait` returns no CQEs, and -`backoff.Reset()` after any batch with `n > 0`. +All ring methods, including `Send`, `Receive`, `AcceptMultishot`, and `Wait`, run on this goroutine. Work from other goroutines enters the loop through a channel or a lock-free queue, not by calling ring methods directly. `iox.Backoff` stays caller-owned: call `backoff.Wait()` on `iox.OutcomeWouldBlock` or when `Wait` returns no CQEs, and `backoff.Reset()` after any batch with `n > 0`. ### Multishot subscription lifecycle -A multishot operation produces a stream of CQEs until the kernel sends a final one (without `IORING_CQE_F_MORE`). -Caller-side runtime code routes each CQE through the returned subscription before falling back to the rest of the dispatcher: +A multishot operation produces a stream of CQEs until the kernel sends a final one (without `IORING_CQE_F_MORE`). Caller-side code routes each CQE through the returned subscription in the same serialized completion loop before falling back to the rest of the dispatcher: ```go handler := uring.NewMultishotSubscriber(). @@ -425,6 +383,8 @@ if err != nil { return err } +// Dispatch in the same serialized completion loop. If caller code stores +// copied CQEs beyond this loop, it must keep its own route state. for i := range n { if sub.HandleCQE(cqes[i]) { continue @@ -433,12 +393,11 @@ for i := range n { } ``` -`OnMultishotStep` observes each completion; return `MultishotContinue` to keep the stream or `MultishotStop` to request cancellation. `OnMultishotStop` runs once at the terminal state. Use it for cleanup and conditional resubscription. On default single-issuer rings, call `Cancel` / `Unsubscribe` from the ring owner or otherwise serialize them with submit, `Wait`, `WaitDirect`, `WaitExtended`, `Stop`, and resize operations. On `MultiIssuers` rings, the shared-submit path serializes their cancel SQEs. +`OnMultishotStep` observes each completion; return `MultishotContinue` to keep the stream or `MultishotStop` to request cancellation. `OnMultishotStop` runs once at the terminal state. Use it for cleanup and conditional resubscription. `HandleCQE` is for immediate dispatch in the caller's serialized completion loop. If caller code stores copied CQEs beyond that loop, it must keep its own route state and reject observations for retired subscriptions. On default single-issuer rings, call `Cancel` / `Unsubscribe` from the ring owner or otherwise serialize them with submit, `Wait`, `WaitDirect`, `WaitExtended`, `Stop`, and `ResizeRings`. On `MultiIssuers` rings, the shared-submit path serializes their cancel SQEs. ### Per-connection state with typed contexts -Extended contexts carry per-connection references through the submit → complete round-trip without a global lookup -table: +Extended contexts carry per-connection references through the submit → complete round-trip without a global lookup table: ```go type ConnState struct { @@ -468,14 +427,11 @@ seq := ctx.Val1 ring.PutExtSQE(ext) ``` -Keep live Go pointer roots reachable outside `UserData`. The GC does not trace those raw bytes. The sidecar root set -attached to each `ExtSQE` slot handles this for internal multishot and listener protocols, but caller-side runtime code -that places typed refs must keep them reachable independently. +Keep live Go pointer roots reachable outside `UserData`. The GC does not trace those raw bytes. The sidecar root set attached to each `ExtSQE` slot handles this for internal multishot and listener protocols, but caller-side runtime code that places typed refs must keep them reachable independently. ### Deadline composition -`LinkTimeout` attaches a deadline to the preceding SQE through an `IOSQE_IO_LINK` chain. The operation and the timeout -race: exactly one completes, and the other is cancelled. +`LinkTimeout` attaches a deadline to the preceding SQE through an `IOSQE_IO_LINK` chain. The operation and the timeout race: exactly one completes, and the other is cancelled. ```go recvCtx := uring.ForFD(fd). @@ -492,8 +448,7 @@ if err := ring.LinkTimeout(timeoutCtx, 5*time.Second); err != nil { } ``` -The caller-side runtime handles both outcomes: a successful receive cancels the timeout, and a fired timeout cancels the -receive. Both produce CQEs that the dispatch loop must observe. +The caller-side runtime handles both outcomes: a successful receive cancels the timeout, and a fired timeout cancels the receive. Both produce CQEs that the dispatch loop must observe. ## TCP usage patterns @@ -506,9 +461,7 @@ These are the shortest flows, meant to be read alongside the tests: ### TCP echo server -`ListenerManager` prepares the socket → bind → listen chain for you. The listener handler's bool-return callbacks are -control-flow hooks: `true` advances to the next setup phase, `false` aborts before it. Once the listener is live, start -multishot accept and multishot receive on the connection FDs. +`ListenerManager` prepares the socket → bind → listen chain for you. The listener handler's bool-return callbacks are control-flow hooks: `true` advances to the next setup phase, `false` aborts before it. Once the listener is live, start multishot accept and multishot receive on the connection FDs. ```go pool := uring.NewContextPools(32) @@ -533,13 +486,11 @@ if err != nil { defer recvSub.Cancel() ``` -`listener_example_test.go` covers listener setup with multishot accept, `examples/multishot_test.go` covers handler-side -multishot receive CQEs, and `examples/echo_test.go` covers the full loopback echo flow. +`listener_example_test.go` covers listener setup with multishot accept, `examples/multishot_test.go` covers handler-side multishot receive CQEs, and `examples/echo_test.go` covers the full loopback echo flow. ### TCP client -Create a socket, wait for the `IORING_OP_SOCKET` completion, then wrap the returned FD in an `iofd.FD` for `Connect`, -`Send`, and `Receive`. +Create a socket, wait for the `IORING_OP_SOCKET` completion, then wrap the returned FD in an `iofd.FD` for `Connect`, `Send`, and `Receive`. ```go clientCtx := uring.PackDirect(uring.IORING_OP_SOCKET, 0, 0, 0) @@ -565,31 +516,24 @@ if err := ring.Receive(recvCtx, &clientFD, buf); err != nil { } ``` -After each submit, reuse the `Wait` loop from the ring lifecycle section to observe the matching completion. -`socket_integration_linux_test.go` at the package level covers the connect/send cycle. +After each submit, reuse the `Wait` loop from the ring lifecycle section to observe the matching completion. `socket_integration_linux_test.go` at the package level covers the connect/send cycle. ## Zero-copy receive (ZCRX) `ZCRXReceiver` drives zero-copy receive from a NIC hardware RX queue through `io_uring`. -`NewZCRXReceiver` is wired for rings with 32-byte CQEs (`IORING_SETUP_CQE32`). The current `Options` surface does not -expose that setup flag, so rings created through the standard `New` path cause this constructor to return -`ErrNotSupported`. Until a CQE32 setup path is exposed, this section documents the receiver boundary contract rather -than a runnable public setup recipe. +`NewZCRXReceiver` is wired for rings with 32-byte CQEs (`IORING_SETUP_CQE32`). The current `Options` surface does not expose that setup flag, so rings created through the standard `New` path cause this constructor to return `ErrNotSupported`. Until a CQE32 setup path is exposed, this section documents the receiver boundary contract rather than a runnable public setup recipe. ### Lifecycle -1. With a CQE32-enabled ring, create the receiver with `NewZCRXReceiver`. The constructor registers the ZCRX interface - queue, maps the refill area, and prepares the refill ring. +1. With a CQE32-enabled ring, create the receiver with `NewZCRXReceiver`. The constructor registers the ZCRX interface queue, maps the refill area, and prepares the refill ring. 2. Call `Start` to submit the extended `RECV_ZC` operation. 3. On the CQE dispatch path, ZCRX completions route to the `ZCRXHandler`: - - `OnData` delivers a `ZCRXBuffer` pointing into the NIC-mapped area. Call `Release` when done to return the slot to - the kernel. Return `false` to request a best-effort stop. - - `OnError` delivers CQE errors. Return `false` to request a best-effort stop. - - `OnStopped` fires once during terminal retirement, before the state reaches `Stopped`. + - `OnData` delivers a `ZCRXBuffer` pointing into the NIC-mapped area. Call `Release` when done to return the slot to the kernel. Return `false` to request a best-effort stop. + - `OnError` delivers CQE errors. Return `false` to request a best-effort stop. + - `OnStopped` fires once during terminal retirement, before the state reaches `Stopped`. 4. Call `Stop` to submit an async cancel. The receiver transitions through `Stopping` → `Retiring` → `Stopped`. -5. Poll `Stopped` until it returns `true`, stop the owning ring, then call `Close` to release the mapped area and the - refill-ring mapping. +5. Poll `Stopped` until it returns `true`, stop the owning ring, then call `Close` to release the mapped area and the refill-ring mapping. ### State machine @@ -621,24 +565,20 @@ The example tests in `uring/examples/` show the API in practice. - `echo_test.go`, TCP echo server and UDP ping-pong flows - `timeout_linux_test.go`, timeout and linked-timeout operations -The package-level `listener_example_test.go` covers listener creation with multishot accept, and -`socket_integration_linux_test.go` covers the TCP client connect/send flow. +The package-level `listener_example_test.go` covers listener creation with multishot accept, and `socket_integration_linux_test.go` covers the TCP client connect/send flow. ## Operational notes - Enable `NotifySucceed` when you need a visible CQE for every successful operation. - `ring.Features` reports actual SQ/CQ entry counts, SQE slot width, and the byte order used to interpret `user_data`. -- Leave `MultiIssuers` unset for the default single-issuer configuration (`SINGLE_ISSUER` + `DEFER_TASKRUN`) when a single execution path serializes submit-state operations (`submit`, `Wait`, `WaitDirect`, `WaitExtended`, `Stop`, and resize). Set it only when multiple goroutines need concurrent submission or concurrent calls to `Wait`, `WaitDirect`, or `WaitExtended`; this switches the ring to the shared-submit `COOP_TASKRUN` configuration. +- Leave `MultiIssuers` unset for the default single-issuer configuration (`SINGLE_ISSUER` + `DEFER_TASKRUN`) when a single execution path serializes submit-state operations (`submit`, `Wait`, `WaitDirect`, `WaitExtended`, `Stop`, and `ResizeRings`). Set it only when multiple goroutines need concurrent submission or concurrent calls to `Wait`, `WaitDirect`, or `WaitExtended`; this switches the ring to the shared-submit `COOP_TASKRUN` configuration. - `EpollWait` requires `timeout` to remain `0`; use `LinkTimeout` when you need a deadline. - Release or discard borrowed completion views and pooled contexts promptly. -- `ListenerOp.Close` closes the listener FD immediately. If a setup CQE is still pending, drain it first, then call - `Close` again to return the borrowed `ExtSQE` to the pool. +- `ListenerOp.Close` closes the listener FD immediately. If a setup CQE is still pending, drain it first, then call `Close` again to return the borrowed `ExtSQE` to the pool. ## Platform support -`uring` targets Go 1.26+ and Linux 6.18+ on the real kernel-backed path. Most source files and example tests carry a -`//go:build linux` guard. Darwin files provide compile stubs for the shared surface only; Linux-only capabilities remain -Linux-only and do not change the Linux runtime baseline. +`uring` targets Go 1.26+ and Linux 6.18+ on the real kernel-backed path. Most source files and example tests carry a `//go:build linux` guard. Darwin files provide compile stubs for the shared surface only; Linux-only capabilities remain Linux-only and do not change the Linux runtime baseline. ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index f247f3c..a30f503 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -11,11 +11,9 @@ Go `io_uring` 内核接口包,面向 Linux 6.18+。 ## 概述 -`uring` 负责与 Linux 内核的 `io_uring` 接口交互:创建并启动 ring、填充 SQE、解码 CQE,借助 `user_data` -传递提交标识,并提供缓冲区注册、multishot 操作及监听器初始化等原语。 +`uring` 是 Linux `io_uring` 的内核侧边界:负责创建并启动 ring、填充 SQE、解码 CQE,借助 `user_data` 传递提交标识,并提供缓冲区注册、multishot 操作及监听器初始化等原语,本身并不充当调度器。 -设计原则是明确划分边界:内核侧的机制与可观测的完成事实留在 API -层面,调度策略与组合逻辑由上层负责。调用方运行时代码负责完成关联、重试与退避、处理器与会话路由、连接生命周期以及终态资源释放。 +设计原则是明确划分边界:内核侧的机制与可观测的完成事实留在本 API 层面,调度策略与组合逻辑由上层负责。调用方运行时代码负责完成事件的关联、重试与退避、处理器与会话路由、连接生命周期以及终态资源释放。 核心类型: @@ -32,9 +30,9 @@ Go `io_uring` 内核接口包,面向 Linux 6.18+。 uname -r ``` -`uring` 假定运行环境满足 6.18+ 基线,且不为旧内核保留任何回退分支。请启动满足要求的内核,而不是期待此包为旧内核提供兼容分支。 +`uring` 假定运行环境满足 6.18+ 基线,且不为旧内核保留任何回退分支。请直接启动满足要求的内核,而不是期待此包内部提供兼容分支。 -Debian 13 的稳定源中内核版本可能低于此要求。如需升级,请参见下方 Debian 13 内核升级一节。 +Debian 13 的稳定源中内核版本可能仍低于 6.18。需要升级时请参见下文 [Debian 13 内核升级](#debian-13-内核升级) 一节。 ```bash go get code.hybscloud.com/uring @@ -42,18 +40,15 @@ go get code.hybscloud.com/uring ### Debian 13 内核升级 -Debian 13 稳定版提供的内核为 6.12。`trixie-backports` 软件源提供经过 Debian 打包的 6.18+ -内核。详细步骤见 [SETUP.md](./SETUP.md)。 +Debian 13 稳定版默认提供的内核为 6.12,`trixie-backports` 软件源提供经过 Debian 打包的 6.18+ 内核。详细步骤见 [SETUP.md](./SETUP.md)。 ### 常见问题排查 -Ring 创建可能返回 `ENOMEM`、`EPERM` 或 `ENOSYS`,原因分别涉及 memlock 上限、sysctl 配置或内核支持情况。容器运行时默认阻止 -`io_uring` 系统调用。诊断与解决方法见 [SETUP.md](./SETUP.md)。 +Ring 创建可能返回 `ENOMEM`、`EPERM` 或 `ENOSYS`,分别对应 memlock 上限、sysctl 配置或内核支持等问题。容器运行时默认会阻止 `io_uring` 系统调用。诊断与解决方法见 [SETUP.md](./SETUP.md)。 ## Ring 生命周期 -`New` 返回一个未启动的 ring,提交操作前须先调用 `Start`。`New` 会预先构建上下文池,`Start` 则负责注册资源并启用 -ring。下面的示例提交一次文件读取,等待匹配的 CQE,并使用 `iox.Classify` 保持 `ErrWouldBlock` 的“暂无进展”语义,而不是把它当作失败处理。 +`New` 返回一个未启动的 ring,并立即构建上下文池;提交操作前须先调用 `Start`,由它注册 ring 资源并启用 ring。下面的示例提交一次文件读取,等待匹配的 CQE,并使用 `iox.Classify` 保持 `ErrWouldBlock` 的“暂无进展”语义,而不是把它当作失败处理。 ```go ring, err := uring.New(func(o *uring.Options) { @@ -109,8 +104,7 @@ for { `Wait` 先刷新待提交项,再回收完成事件。在单提交者 ring 上,它还会在 SQ 排空后向内核发起进入调用,以推进延迟任务执行;调用方需保证 `Wait`、`WaitDirect` 和 `WaitExtended` 与其他提交状态操作串行执行。当 `Wait` 返回的 `err` 经 `iox.Classify(err)` 分类为 `iox.OutcomeWouldBlock` 时,表示当前边界上没有可观察到的完成事件。 -`Start` 与 `Stop` 是 ring 生命周期的配对操作。`Stop` 幂等但不可逆,调用后 ring 将永久不可用。调用 `Stop` -前,需确保所有进行中的操作已完成、未处理的 CQE 已回收、活跃的 multishot 订阅已终止。 +`Start` 与 `Stop` 是 ring 生命周期的配对操作。`Stop` 幂等但不可逆,调用后 ring 将永久不可用;调用 `Stop` 前必须确保所有进行中的操作已完成、未处理的 CQE 已回收、活跃的 multishot 订阅已终止。 ## 类型与操作 @@ -137,11 +131,11 @@ for { | 套接字 | `TCP4Socket`, `TCP6Socket`, `UDP4Socket`, `UDP6Socket`, `UDPLITE4Socket`, `UDPLITE6Socket`, `SCTP4Socket`, `SCTP6Socket`, `UnixSocket`, `SocketRaw`,以及 `*Direct` 变体 | | 连接 | `Bind`, `Listen`, `Accept`, `AcceptDirect`, `Connect`, `Shutdown` | | 套接字 I/O | `Receive`, `Send`, `RecvMsg`, `SendMsg`, `ReceiveBundle`, `ReceiveZeroCopy`, `Multicast`, `MulticastZeroCopy` | -| 多次触发 | `AcceptMultishot`, `ReceiveMultishot`, `SubmitAcceptMultishot`, `SubmitAcceptDirectMultishot`, `SubmitReceiveMultishot`, `SubmitReceiveBundleMultishot` | +| Multishot | `AcceptMultishot`, `ReceiveMultishot`, `SubmitAcceptMultishot`, `SubmitAcceptDirectMultishot`, `SubmitReceiveMultishot`, `SubmitReceiveBundleMultishot` | | 文件 I/O | `Read`, `Write`, `ReadV`, `WriteV`, `ReadFixed`, `WriteFixed`, `ReadvFixed`, `WritevFixed` | | 文件管理 | `OpenAt`, `Close`, `Sync`, `Fallocate`, `FTruncate`, `Statx`, `RenameAt`, `UnlinkAt`, `MkdirAt`, `SymlinkAt`, `LinkAt` | | 扩展属性 | `FGetXattr`, `FSetXattr`, `GetXattr`, `SetXattr` | -| 数据转移 | `Splice`, `Tee`, `Pipe`, `SyncFileRange`, `FileAdvise` | +| 数据传输 | `Splice`, `Tee`, `Pipe`, `SyncFileRange`, `FileAdvise` | | 超时 | `Timeout`, `TimeoutRemove`, `TimeoutUpdate`, `LinkTimeout` | | 取消 | `AsyncCancel`, `AsyncCancelFD`, `AsyncCancelOpcode`, `AsyncCancelAny`, `AsyncCancelAll` | | 轮询 | `PollAdd`, `PollRemove`, `PollUpdate`, `PollAddLevel`, `PollAddMultishot`, `PollAddMultishotLevel` | @@ -155,7 +149,7 @@ for { ## 上下文传递 -`SQEContext` 是 `uring` 的核心标识令牌。Direct 模式将 opcode、SQE flags、buffer-group ID 和文件描述符打包为一个 64 位值。 +`SQEContext` 是本包的核心标识令牌。Direct 模式将 opcode、SQE flags、buffer-group ID 与文件描述符打包为一个 64 位值。 ```go sqeCtx := uring.ForFD(fd). @@ -171,11 +165,9 @@ sqeCtx := uring.ForFD(fd). | Indirect | 指向 `IndirectSQE` 的指针 | 64 位不足以表达完整 SQE 负载时 | | Extended | 指向 `ExtSQE` 的指针 | 完整 SQE 加 64 字节用户数据 | -常见路径下,从 `ForFD` 或 `PackDirect` 开始,只填入完成时需要回溯的信息。`WithFlags` 会整体替换 flag 集合,调用前应先计算好联合值。 +常见路径下,从 `ForFD` 或 `PackDirect` 开始,只填入完成时需要回溯的信息;`WithFlags` 会整体替换 flag 集合,调用前应先计算好联合值。 -当 direct 的 64 位布局不足以携带所需元数据时,可从池中借用 `ExtSQE`,通过 `Ctx*Of` 或 `ViewCtx*` 写入 `UserData`,再打包为 -`SQEContext`。此处应优先使用标量负载。若通过原始覆盖视图或类型化视图存放了 Go 指针、接口值、函数值、切片、字符串、映射、通道或含有这些成员的结构体,必须将活跃根保留在 -`UserData` 之外,因为 GC 不会扫描这段原始字节。 +当 direct 的 64 位布局不足以携带所需元数据时,可从池中借用 `ExtSQE`,通过 `Ctx*Of` 或 `ViewCtx*` 写入 `UserData`,再打包为 `SQEContext`。这里应优先使用标量负载;若通过原始覆盖视图或类型化视图存放了 Go 指针、接口值、函数值、切片、字符串、映射、通道或含有这些成员的结构体,必须将活跃根保留在 `UserData` 之外,因为 GC 不会扫描这段原始字节。 ```go ext := ring.ExtSQE() @@ -231,9 +223,7 @@ for i := 0; i < n; i++ { ## 缓冲区供给 -`uring` 提供三类常用缓冲区路径:注册缓冲区在 ring 启动时固定并用于固定缓冲区文件 I/O;提供缓冲区环 -让内核在接收完成时选择缓冲区,并在 CQE 中返回缓冲区 ID;捆绑接收可以在一个 CQE 中消耗一段连续的逻辑缓冲区范围,并通过 -`BundleIterator` 暴露该范围。 +`uring` 提供三类常用缓冲区路径:注册缓冲区在 ring 启动时固定并用于固定缓冲区文件 I/O;提供缓冲区环让内核在接收完成时选择缓冲区,并在 CQE 中返回缓冲区 ID;捆绑接收可以在一个 CQE 中消耗一段连续的逻辑缓冲区范围,并通过 `BundleIterator` 暴露该范围。 - 通过 `ReadBufferSize` 与 `ReadBufferNum` 配置固定尺寸的提供缓冲区 - 通过 `MultiSizeBuffer` 启用多尺寸缓冲区组 @@ -302,11 +292,9 @@ if it, ok := ring.BundleIterator(cqe, cqe.BufGroup()); ok { ## Multishot 与监听器操作 -`AcceptMultishot`、`ReceiveMultishot`、`SubmitAcceptMultishot`、`SubmitAcceptDirectMultishot`、`SubmitReceiveMultishot` 和 -`SubmitReceiveBundleMultishot` 用于提交 multishot socket 操作。 +`AcceptMultishot`、`ReceiveMultishot`、`SubmitAcceptMultishot`、`SubmitAcceptDirectMultishot`、`SubmitReceiveMultishot` 和 `SubmitReceiveBundleMultishot` 用于提交 multishot socket 操作。 -CQE 路由策略由调用方自行实现,不在本包范围内。监听器的配置流程通过 `DecodeListenerCQE`、`PrepareListenerBind`、 -`PrepareListenerListen` 和 `SetListenerReady` 逐步推进,由调用方决定完成事件的分发方式和停止时机。 +CQE 路由策略由调用方自行实现,不在本包范围内。监听器的配置流程通过 `DecodeListenerCQE`、`PrepareListenerBind`、`PrepareListenerListen` 和 `SetListenerReady` 逐步推进,由调用方决定完成事件的分发方式与停止时机。 ## 架构实现 @@ -322,11 +310,9 @@ CQE 路由策略由调用方自行实现,不在本包范围内。监听器的 ## 运行时边界 -`uring` 之上的运行时层应将其用作内核后端,而不是调度器。理想边界是单向的:`uring` 准备 SQE、回收 CQE、保留 `user_data`、暴露 -CQE `res` 与标志,并报告所有权事实;调用方运行时代码将这些观测与自身令牌关联,应用重试与退避,路由处理器与会话,批量提交,并释放终态资源。 +`uring` 之上的运行时层应将其用作内核后端,而不是调度器。理想边界是单向的:`uring` 准备 SQE、回收 CQE、保留 `user_data`、暴露 CQE `res` 与标志,并报告所有权事实;调用方运行时代码将这些观测与自身令牌关联,应用重试与退避,路由处理器与会话,批量提交,并释放终态资源。 -当抽象执行需要完成事实时,运行时桥接层可以消费 Extended 模式 CQE。连接级运行时也可以在需要 CQE 结果、标志、缓冲区 ID 和编码 -令牌时直接轮询原始 Extended CQE,然后再将事件归约为处理器回调。 +当抽象执行需要完成事实时,运行时桥接层可以消费 Extended 模式 CQE。连接级运行时也可以在需要 CQE 结果、标志、缓冲区 ID 与编码令牌时直接轮询原始 Extended CQE,然后再将事件归约为处理器回调。 位于该边界之上的上下文层与抽象执行层不会改变 `uring` 的内核边界职责。 @@ -336,8 +322,7 @@ CQE `res` 与标志,并报告所有权事实;调用方运行时代码将这 ### Ring 所有者事件循环 -在单发行者模式(默认)下,一个 goroutine 串行化所有提交侧操作。典型循环为:发行待处理工作,在 `Wait` 没有返回可观察进展时使用调用方持有的 -`iox.Backoff`,然后分发完成事件。 +在单提交者模式(默认)下,一个 goroutine 串行化所有提交状态操作。典型循环下发起待处理工作,在 `Wait` 没有返回可观察进展时使用调用方持有的 `iox.Backoff`,然后分发完成事件。 ```go func runLoop(ring *uring.Uring, stop <-chan struct{}) error { @@ -371,9 +356,7 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { } ``` -所有 ring 方法,包括 `Send`、`Receive`、`AcceptMultishot` 和 `Wait`,均在该 goroutine 上执行。来自其他 goroutine 的工作通过 -通道或无锁队列进入循环,不可直接调用 ring 方法。`iox.Backoff` 由调用方持有:`Wait` 分类为 `iox.OutcomeWouldBlock`,或一次 -`Wait` 没有回收到任何 CQE 时,调用 `backoff.Wait()`;回收到 `n > 0` 的 CQE 批次后,调用 `backoff.Reset()`。 +所有 ring 方法,包括 `Send`、`Receive`、`AcceptMultishot` 和 `Wait`,均在该 goroutine 上执行。来自其他 goroutine 的工作通过通道或无锁队列进入循环,不可直接调用 ring 方法。`iox.Backoff` 由调用方持有:当 `Wait` 返回的错误被分类为 `iox.OutcomeWouldBlock`,或一次 `Wait` 没有回收到任何 CQE 时,调用 `backoff.Wait()`;回收到 `n > 0` 的 CQE 批次后,调用 `backoff.Reset()`。 ### Multishot 订阅生命周期 @@ -400,6 +383,8 @@ if err != nil { return err } +// 在同一个串行化完成循环中 dispatch。若调用方在该循环之后保留复制 CQE, +// 调用方必须维护自己的 route state。 for i := range n { if sub.HandleCQE(cqes[i]) { continue @@ -408,11 +393,11 @@ for i := range n { } ``` -`OnMultishotStep` 观察每次完成;返回 `MultishotContinue` 保持流,返回 `MultishotStop` 请求取消。`OnMultishotStop` 在终态执行一次,用于清理和按条件重新订阅。在默认单提交者 ring 上,应从 ring 所有者调用 `Cancel` / `Unsubscribe`,或将它们与提交、`Wait`、`WaitDirect`、`WaitExtended`、`Stop` 以及 resize 操作串行化。启用 `MultiIssuers` 的 ring 由共享提交路径串行化这些取消 SQE。 +`OnMultishotStep` 观察每次完成;返回 `MultishotContinue` 保持流,返回 `MultishotStop` 请求取消。`OnMultishotStop` 在终态执行一次,用于清理和按条件重新订阅。`HandleCQE` 用于调用方串行化完成循环内的立即 dispatch;若调用方在该循环之后保留复制 CQE,调用方必须维护自己的 route state,并拒绝已退役订阅的观察。在默认单提交者 ring 上,应从 ring 所有者调用 `Cancel` / `Unsubscribe`,或将它们与提交、`Wait`、`WaitDirect`、`WaitExtended`、`Stop` 以及 `ResizeRings` 串行化。启用 `MultiIssuers` 的 ring 由共享提交路径串行化这些取消 SQE。 ### 类型化上下文承载连接状态 -扩展上下文通过提交 → 完成往返全程携带连接级引用,无需全局查找表。 +扩展上下文可以在提交 → 完成的往返全程携带连接级引用,无需全局查找表: ```go type ConnState struct { @@ -442,12 +427,11 @@ seq := ctx.Val1 ring.PutExtSQE(ext) ``` -活跃的 Go 指针根须在 `UserData` 之外保持可达。GC 不会追踪裸字节。内部 multishot 和监听器协议由每个 `ExtSQE` 槽上的 -旁路根集处理,但放置类型化引用的框架代码须自行维护可达性。 +活跃的 Go 指针根必须在 `UserData` 之外保持可达,因为 GC 不会追踪这些原始字节。内部 multishot 和监听器协议由附属于每个 `ExtSQE` 槽位的旁路根集处理,但放置类型化引用的调用方运行时代码需自行维护可达性。 ### 截止时间组合 -`LinkTimeout` 通过 `IOSQE_IO_LINK` 链将截止时间附加到前一个 SQE。操作与超时竞争:一方完成,另一方被取消。 +`LinkTimeout` 通过 `IOSQE_IO_LINK` 链将截止时间附加到前一个 SQE 上。操作与超时互相竞争:一方完成,另一方被取消。 ```go recvCtx := uring.ForFD(fd). @@ -464,7 +448,7 @@ if err := ring.LinkTimeout(timeoutCtx, 5*time.Second); err != nil { } ``` -框架层处理两种结果:接收成功取消超时,超时触发取消接收。两者均产生 CQE,分发循环须观察处理。 +调用方运行时需同时处理两种结果:接收成功会取消超时,超时触发会取消接收。两者均产生 CQE,分发循环必须观察处理。 ## TCP 使用模式 @@ -477,8 +461,7 @@ if err := ring.LinkTimeout(timeoutCtx, 5*time.Second); err != nil { ### TCP Echo 服务器 -使用 `ListenerManager` 可自动完成 socket → bind → listen 链路的准备工作,之后在活跃连接的 FD 上启动 multishot accept 和 -multishot receive。 +使用 `ListenerManager` 可自动完成 socket → bind → listen 链路的准备工作,之后在活跃连接的 FD 上启动 multishot accept 和 multishot receive。 ```go pool := uring.NewContextPools(32) @@ -503,8 +486,7 @@ if err != nil { defer recvSub.Cancel() ``` -`listener_example_test.go` 演示监听器创建与 multishot accept;`examples/multishot_test.go` 演示处理器侧的 multishot -receive CQE 处理流程;`examples/echo_test.go` 则给出更完整的 loopback echo 示例。 +`listener_example_test.go` 演示监听器创建与 multishot accept;`examples/multishot_test.go` 演示处理器侧的 multishot receive CQE 处理流程;`examples/echo_test.go` 则给出完整的 loopback echo 示例。 ### TCP 客户端 @@ -534,27 +516,24 @@ if err := ring.Receive(recvCtx, &clientFD, buf); err != nil { } ``` -每次提交后,可复用“Ring 生命周期”一节中的 `Wait` 循环来等待完成事件。包级 `socket_integration_linux_test.go` 覆盖了 -connect/send 流程。 +每次提交后,可复用“Ring 生命周期”一节中的 `Wait` 循环来等待完成事件。包级 `socket_integration_linux_test.go` 覆盖了 connect/send 流程。 ## 零拷贝接收(ZCRX) `ZCRXReceiver` 管理通过 `io_uring` 从 NIC 硬件 RX 队列进行的零拷贝接收。 -`NewZCRXReceiver` 面向以 32 字节 CQE(`IORING_SETUP_CQE32`)创建的 ring。当前 `Options` 尚未暴露该设置标志,因此通过标准 -`New` 创建的 ring 会使该构造器返回 `ErrNotSupported`。在 CQE32 设置路径公开前,本节记录的是接收器边界契约,而不是可直接执行的公开设置流程。 +`NewZCRXReceiver` 面向以 32 字节 CQE(`IORING_SETUP_CQE32`)创建的 ring。当前 `Options` 尚未暴露该设置标志,因此通过标准 `New` 创建的 ring 会使该构造器返回 `ErrNotSupported`。在 CQE32 设置路径公开前,本节仅记录接收器的边界契约,而非可直接执行的公开设置流程。 ### 生命周期 -1. 在支持 CQE32 的 ring 上调用 `NewZCRXReceiver` 创建接收器。构造器会注册 ZCRX 接口队列、映射补充区域并准备补充 - ring。 +1. 在支持 CQE32 的 ring 上调用 `NewZCRXReceiver` 创建接收器。构造器会注册 ZCRX 接口队列、映射补充区域并准备补充 ring。 2. 调用 `Start`,在 ring 上提交扩展 `RECV_ZC` 操作。 3. CQE 分发时,ZCRX 完成事件路由至 `ZCRXHandler`: - - `OnData` 交付指向 NIC 映射区域的 `ZCRXBuffer`。处理完毕后调用 `Release` 将槽位回填给内核。返回 `false` 请求尽力停止。 - - `OnError` 交付 CQE 错误。返回 `false` 请求尽力停止。 - - `OnStopped` 在进入 `Stopped` 前的终态退出阶段调用一次。 + - `OnData` 交付指向 NIC 映射区域的 `ZCRXBuffer`,处理完毕后调用 `Release` 将槽位回填给内核;返回 `false` 请求尽力停止。 + - `OnError` 交付 CQE 错误;返回 `false` 请求尽力停止。 + - `OnStopped` 在进入 `Stopped` 前的终态退出阶段被调用一次。 4. 调用 `Stop` 提交异步取消,接收器依次经历 `Stopping` → `Retiring` → `Stopped`。 -5. 轮询 `Stopped` 直至返回 `true`,停止所属 ring,再调用 `Close` 释放映射区域和补充 ring。 +5. 轮询 `Stopped` 直至返回 `true`,停止所属 ring,再调用 `Close` 释放映射区域与补充 ring。 ### 状态机 @@ -568,7 +547,7 @@ Idle → Active → Stopping → Retiring → Stopped - `OnData` 与 `OnError` 在 CQE 分发 goroutine 中串行调用。 - `Release` 为单生产者操作,仅限在分发 goroutine 中调用。 -- 调用 `Stop` 时须保证与 CQE 分发不并发,这是调用方侧的串行化约定。 +- 调用 `Stop` 时必须确保与 CQE 分发不并发,这是调用方需保证的串行化约定。 ## 示例 @@ -586,22 +565,20 @@ Idle → Active → Stopping → Retiring → Stopped - `echo_test.go`,TCP echo 服务器与 UDP ping-pong 流程 - `timeout_linux_test.go`,超时与链式超时操作 -包级 `listener_example_test.go` 演示监听器创建与 multishot accept,`socket_integration_linux_test.go` 演示 TCP 客户端 -connect/send 流程。 +包级 `listener_example_test.go` 演示监听器创建与 multishot accept,`socket_integration_linux_test.go` 演示 TCP 客户端的 connect/send 流程。 -## 注意事项 +## 运行注意事项 - 若需为每个成功操作生成可见的 CQE,启用 `NotifySucceed`。 - `ring.Features` 报告实际 SQ/CQ 条目数、SQE 槽宽以及本包解析 `user_data` 的字节序。 -- 默认不启用 `MultiIssuers`,此时采用单提交者配置(`SINGLE_ISSUER` + `DEFER_TASKRUN`),由调用方的单一执行路径串行化提交状态操作(`submit`、`Wait`、`WaitDirect`、`WaitExtended`、`Stop` 及 resize)。仅当多个 goroutine 需并发提交或并发调用 `Wait`、`WaitDirect`、`WaitExtended` 时才启用 `MultiIssuers`,这会切换为共享提交的 `COOP_TASKRUN` 配置。 +- 默认不启用 `MultiIssuers`,此时采用单提交者配置(`SINGLE_ISSUER` + `DEFER_TASKRUN`),由调用方的单一执行路径串行化提交状态操作(`submit`、`Wait`、`WaitDirect`、`WaitExtended`、`Stop` 及 `ResizeRings`)。仅当多个 goroutine 需并发提交或并发调用 `Wait`、`WaitDirect`、`WaitExtended` 时才启用 `MultiIssuers`,这会切换为共享提交的 `COOP_TASKRUN` 配置。 - `EpollWait` 要求 `timeout` 为 `0`;如需设置截止时间,使用 `LinkTimeout`。 - 借用式完成视图与池化上下文应及时释放。 - `ListenerOp.Close` 会立即关闭监听 FD。若仍有设置 CQE 待处理,需先回收该 CQE,再调用 `Close` 将借用的 `ExtSQE` 归还池中。 ## 平台支持 -`uring` 的真实内核路径目标为 Go 1.26+ / Linux 6.18+。大部分实现文件和示例测试由 `//go:build linux` 约束。Darwin 文件只为共享 -API 表面提供编译桩;Linux 专属能力仍然仅限 Linux,不改变上述 Linux 运行时基线。 +`uring` 的真实内核路径面向 Go 1.26+ / Linux 6.18+。大部分实现文件与示例测试由 `//go:build linux` 约束;Darwin 文件仅为共享 API 表面提供编译桩,Linux 专属能力仍仅限 Linux,不会改变上述 Linux 运行时基线。 ## 许可证