@@ -32,9 +32,13 @@ type USBCDC struct {
3232 // inflight is the number of bytes currently submitted to the USB IN endpoint.
3333 inflight atomic.Uint32
3434
35- // txActive serializes the USB CDC TX pump between Write and the TX
36- // completion handler. This matters on multicore targets where they can run
37- // concurrently.
35+ // txActive is the TX-pump ownership flag: 0 = idle, 1 = a pump owns the TX
36+ // path. Claimed once (kickTx, CAS 0->1), held across every in-flight packet
37+ // and the TX-complete IRQ, and released only when the ring drains. While it
38+ // is set, kickTx's CAS fails and no second pump starts, which serializes the
39+ // pump against Write across cores. Same model as Linux NAPI_STATE_SCHED:
40+ // held across completion, dropped only with a recheck
41+ // (Documentation/networking/napi.rst).
3842 txActive atomic.Uint32
3943
4044 rbuf [1 ]byte
@@ -113,7 +117,9 @@ func (usbcdc *USBCDC) Write(data []byte) (n int, err error) {
113117 return n , nil
114118}
115119
116- // kickTx starts the TX pump if it is idle.
120+ // kickTx claims the TX pump for a producer. This CAS is the only start-from-idle
121+ // edge; if it fails, a pump already owns the path and will drain what we just
122+ // enqueued -- see the recheck in sendFromRing.
117123func (usbcdc * USBCDC ) kickTx () {
118124 if ! usbcdc .txActive .CompareAndSwap (0 , 1 ) {
119125 return
@@ -122,6 +128,9 @@ func (usbcdc *USBCDC) kickTx() {
122128}
123129
124130func (usbcdc * USBCDC ) txhandler () {
131+ // TX-complete IRQ. The pump is still owned here (txActive stayed 1 across the
132+ // in-flight packet), so continue WITHOUT re-claiming -- pairs with the CAS in
133+ // kickTx. A CAS here would see the flag already set, bail, and stall the chain.
125134 inflight := usbcdc .inflight .Load ()
126135 if inflight == 0 {
127136 return
@@ -131,54 +140,35 @@ func (usbcdc *USBCDC) txhandler() {
131140 usbcdc .sendFromRing ()
132141}
133142
134- // sendFromRing submits one USB IN packet from the TX ring, or shuts down
135- // the TX pump if the ring is empty.
136- //
137- // The caller must own txActive (either having just acquired it via CAS,
138- // or continuing ownership from a previous packet submission).
139- // For kickTx: newly acquired via CAS.
140- // For txhandler: inherited from the previous packet submission.
141- //
142- // While the caller owns txActive:
143- // - If data is available, one packet is sent and txActive remains set
144- // for the in-flight packet (ownership continues to txhandler).
145- // - If the ring is empty, txActive is cleared and the pump shuts down
146- // (with a final re-check to avoid a missed wakeup from Write).
143+ // sendFromRing runs one step of the TX pump: submit one IN packet, or release the
144+ // pump if the ring is empty. Precondition: txActive == 1 (from kickTx's CAS, or
145+ // still held from the previous packet when entered via txhandler).
147146func (usbcdc * USBCDC ) sendFromRing () {
148- // This loop sends at most one USB IN packet per entry.
149- //
150- // If the TX ring has data, one packet is handed to the USB hardware and
151- // txActive remains set until txhandler continues the pump after
152- // completion.
153- //
154- // If the TX ring appears empty, this is the shutdown path. Clear
155- // txActive, then re-check the ring to avoid missing a Write that added
156- // data while txActive was still set.
157147 for {
158148 d1 , _ := usbcdc .tx .Peek ()
159149 if len (d1 ) == 0 {
150+ // Release the pump, then re-scan the ring: closes the missed-wakeup
151+ // race where Write Put()s data and kickTx's CAS then fails (txActive
152+ // still set), leaving the data for this pump to drain. The Store(0)
153+ // is ordered before the Used() load -- and, in the producer, Put()
154+ // before its CAS -- by the sequential consistency of Go's atomics, so
155+ // neither side misses the other (assumes the ring's accesses are
156+ // atomic too). cf. napi_complete_done() clearing NAPI_STATE_SCHED
157+ // then rechecking.
160158 usbcdc .txActive .Store (0 )
161- // Avoid a missed wakeup.
162- //
163- // A concurrent Write may have appended data while txActive was
164- // still set. In that case kickTx would see an active pump and
165- // return without starting another transfer. After clearing
166- // txActive, re-check the ring and reclaim txActive if this pump
167- // must continue.
168- switch {
169- case usbcdc .tx .Used () == 0 :
170- return
171- case ! usbcdc .txActive .CompareAndSwap (0 , 1 ):
172- return
159+ if usbcdc .tx .Used () == 0 {
160+ return // ring empty and pump released; done
173161 }
174- // New data appeared during shutdown, and this caller reclaimed
175- // txActive. Continue and re-peek the ring.
176- continue
162+ if ! usbcdc .txActive .CompareAndSwap (0 , 1 ) {
163+ return // another producer re-claimed the pump; let it run
164+ }
165+ continue // re-claimed; re-peek and keep pumping
177166 }
167+
178168 chunk := d1 [:min (usb .EndpointPacketSize , len (d1 ))]
179169 usbcdc .inflight .Store (uint32 (len (chunk )))
180170 machine .SendUSBInPacket (cdcEndpointIn , chunk )
181- return
171+ return // in flight; txActive stays set, txhandler continues
182172 }
183173}
184174
0 commit comments