View Javadoc
1   /*
2    * Copyright 2024 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.channel.uring;
17  
18  import io.netty.channel.unix.Buffer;
19  import io.netty.channel.unix.Errors;
20  import io.netty.util.internal.logging.InternalLogger;
21  import io.netty.util.internal.logging.InternalLoggerFactory;
22  
23  import java.io.UncheckedIOException;
24  import java.lang.invoke.MethodHandles;
25  import java.lang.invoke.VarHandle;
26  import java.nio.ByteBuffer;
27  import java.nio.ByteOrder;
28  import java.util.StringJoiner;
29  
30  final class SubmissionQueue {
31      private static final InternalLogger logger = InternalLoggerFactory.getInstance(SubmissionQueue.class);
32  
33      static final int SQE_SIZE = 64;
34  
35      //these offsets are used to access specific properties
36      //SQE https://github.com/axboe/liburing/blob/liburing-2.6/src/include/liburing/io_uring.h#L30
37      private static final int SQE_OP_CODE_FIELD = 0;
38      private static final int SQE_FLAGS_FIELD = 1;
39      private static final int SQE_IOPRIO_FIELD = 2; // u16
40      private static final int SQE_FD_FIELD = 4; // s32
41      private static final int SQE_UNION1_FIELD = 8;
42      private static final int SQE_UNION2_FIELD = 16;
43      private static final int SQE_LEN_FIELD = 24;
44      private static final int SQE_UNION3_FIELD = 28;
45      private static final int SQE_USER_DATA_FIELD = 32;
46      private static final int SQE_UNION4_FIELD = 40;
47      private static final int SQE_PERSONALITY_FIELD = 42;
48      private static final int SQE_UNION5_FIELD = 44;
49      private static final int SQE_UNION6_FIELD = 48;
50  
51      // These unsigned integer pointers(shared with the kernel) will be changed by the kernel and us
52      // using a VarHandle.
53      private static final VarHandle INT_HANDLE =
54              MethodHandles.byteBufferViewVarHandle(int[].class, ByteOrder.nativeOrder());
55      private final ByteBuffer kHead;
56      private final ByteBuffer kTail;
57      private final ByteBuffer kflags;
58      private final ByteBuffer submissionQueueArray;
59  
60      final int ringEntries;
61      private final int ringMask; // = ringEntries - 1
62  
63      final int ringSize;
64      final long ringAddress;
65      final int ringFd;
66      int enterRingFd;
67      private int enterFlags;
68      private int head;
69      private int tail;
70  
71      private boolean closed;
72  
73      SubmissionQueue(ByteBuffer khead, ByteBuffer ktail, int ringMask, int ringEntries, ByteBuffer kflags,
74                      ByteBuffer submissionQueueArray,
75                      int ringSize, long ringAddress,
76                      int ringFd) {
77          this.kHead = khead;
78          this.kTail = ktail;
79          this.submissionQueueArray = submissionQueueArray;
80          this.ringSize = ringSize;
81          this.ringAddress = ringAddress;
82          this.ringFd = ringFd;
83          this.enterRingFd = ringFd;
84          this.ringEntries = ringEntries;
85          this.kflags = kflags;
86          this.ringMask = ringMask;
87          this.head = (int) INT_HANDLE.getVolatile(khead, 0);
88          this.tail = (int) INT_HANDLE.getVolatile(ktail, 0);
89      }
90  
91      int flags() {
92          if (closed) {
93              return 0;
94          }
95          // we only need memory_order_relaxed
96          return (int) INT_HANDLE.getOpaque(kflags, 0);
97      }
98  
99      long submissionQueueArrayAddress() {
100         return Buffer.memoryAddress(submissionQueueArray);
101     }
102 
103     void close() {
104         closed = true;
105     }
106 
107     private void checkClosed() {
108         if (closed) {
109             throw new IllegalStateException();
110         }
111     }
112 
113     void tryRegisterRingFd() {
114         checkClosed();
115         // Try to use IORING_REGISTER_RING_FDS.
116         // See https://manpages.debian.org/unstable/liburing-dev/io_uring_register.2.en.html#IORING_REGISTER_RING_FDS
117         int enterRingFd = Native.ioUringRegisterRingFds(ringFd);
118         int enterFlags;
119         if (enterRingFd < 0) {
120             // Use of IORING_REGISTER_RING_FDS failed, just use the ring fd directly.
121             enterRingFd = ringFd;
122             enterFlags = 0;
123         } else {
124             enterFlags = Native.IORING_ENTER_REGISTERED_RING;
125         }
126         // Opt out of iowait accounting while waiting on CQEs for pure networking workloads.
127         // See https://github.com/netty/netty/issues/16655
128         if (IoUring.isIoringEnterNoIoWaitEnabled()) {
129             enterFlags |= Native.IORING_ENTER_NO_IOWAIT;
130         }
131         this.enterRingFd = enterRingFd;
132         this.enterFlags = enterFlags;
133     }
134 
135     long enqueueSqe(byte opcode, byte flags, short ioPrio, int fd, long union1, long union2, int len,
136                              int union3, long udata, short union4, short personality, int union5, long union6) {
137         checkClosed();
138         int pending = tail - head;
139         if (pending == ringEntries) {
140             int submitted = submit();
141             if (submitted == 0) {
142                 // We have a problem, could not submit to make more room in the ring
143                 throw new RuntimeException("SQ ring full and no submissions accepted");
144             }
145         }
146         int sqe = sqeIndex(tail++, ringMask);
147 
148         //set sqe(submission queue) properties
149         submissionQueueArray.put(sqe + SQE_OP_CODE_FIELD, opcode);
150         submissionQueueArray.put(sqe + SQE_FLAGS_FIELD, flags);
151         // This constant is set up-front
152         submissionQueueArray.putShort(sqe + SQE_IOPRIO_FIELD, ioPrio);
153         submissionQueueArray.putInt(sqe + SQE_FD_FIELD, fd);
154         submissionQueueArray.putLong(sqe + SQE_UNION1_FIELD, union1);
155         submissionQueueArray.putLong(sqe + SQE_UNION2_FIELD, union2);
156         submissionQueueArray.putInt(sqe + SQE_LEN_FIELD, len);
157         submissionQueueArray.putInt(sqe + SQE_UNION3_FIELD, union3);
158         submissionQueueArray.putLong(sqe + SQE_USER_DATA_FIELD, udata);
159         submissionQueueArray.putShort(sqe + SQE_UNION4_FIELD, union4);
160         submissionQueueArray.putShort(sqe + SQE_PERSONALITY_FIELD, personality);
161         submissionQueueArray.putInt(sqe + SQE_UNION5_FIELD, union5);
162         submissionQueueArray.putLong(sqe + SQE_UNION6_FIELD, union6);
163 
164         if (logger.isTraceEnabled()) {
165             if (opcode == Native.IORING_OP_WRITEV || opcode == Native.IORING_OP_READV) {
166                 logger.trace("add(ring={}, enterRing:{} ): {}(fd={}, len={}, off={}, data={})",
167                         ringFd, enterRingFd, Native.opToStr(opcode), fd, len, union1, udata);
168             } else {
169                 logger.trace("add(ring={}, enterRing:{}): {}(fd={}, len={}, off={}, data={})",
170                         ringFd, enterRingFd, Native.opToStr(opcode), fd, len, union1, udata);
171             }
172         }
173         return udata;
174     }
175 
176     @Override
177     public String toString() {
178         StringJoiner sb = new StringJoiner(", ", "SubmissionQueue [", "]");
179         if (closed) {
180             sb.add("closed");
181         } else {
182             int pending = tail - head;
183             int idx = head;
184             for (int i = 0; i < pending; i++) {
185                 int sqe = sqeIndex(idx++, ringMask);
186                 sb.add(Native.opToStr(submissionQueueArray.get(sqe + SQE_OP_CODE_FIELD)) +
187                         "(fd=" + submissionQueueArray.getInt(sqe + SQE_FD_FIELD) + ')');
188             }
189         }
190         return sb.toString();
191     }
192 
193     private static int sqeIndex(int tail, int ringMask) {
194         return (tail & ringMask) * SQE_SIZE;
195     }
196 
197     long addNop(byte flags, long udata) {
198         return addNop(flags, 0, udata);
199     }
200 
201     long addNop(byte flags, int nopFlags, long udata) {
202         // Mimic what liburing does:
203         // https://github.com/axboe/liburing/blob/liburing-2.8/src/include/liburing.h#L592
204         return enqueueSqe(Native.IORING_OP_NOP, flags, (short) 0, -1, 0, 0, 0, nopFlags, udata,
205                 (short) 0, (short) 0, 0, 0);
206     }
207 
208     long addTimeout(long timeoutMemoryAddress, long udata) {
209         // Mimic what liburing does. We want to use a count of 1:
210         // https://github.com/axboe/liburing/blob/liburing-2.8/src/include/liburing.h#L599
211         return enqueueSqe(Native.IORING_OP_TIMEOUT, (byte) 0, (short) 0, -1, 1, timeoutMemoryAddress, 1,
212                 0, udata, (short) 0, (short) 0, 0, 0);
213     }
214 
215     long addLinkTimeout(long timeoutMemoryAddress, long extraData) {
216         // Mimic what liburing does:
217         // https://github.com/axboe/liburing/blob/liburing-2.8/src/include/liburing.h#L687
218         return enqueueSqe(Native.IORING_OP_LINK_TIMEOUT, (byte) 0, (short) 0, -1, 1, timeoutMemoryAddress, 1,
219                 0, extraData, (short) 0, (short) 0, 0, 0);
220     }
221 
222     long addEventFdRead(int fd, long bufferAddress, int pos, int limit, long udata) {
223         return enqueueSqe(Native.IORING_OP_READ, (byte) 0, (short) 0, fd, 0, bufferAddress + pos, limit - pos,
224                 0, udata, (short) 0, (short) 0, 0, 0);
225     }
226 
227     // Mimic what liburing does:
228     // https://github.com/axboe/liburing/blob/liburing-2.8/src/include/liburing.h#L673
229     long addCancel(long sqeToCancel, long udata) {
230         return enqueueSqe(Native.IORING_OP_ASYNC_CANCEL, (byte) 0, (short) 0, -1, 0, sqeToCancel, 0, 0,
231                 udata, (short) 0, (short) 0, 0, 0);
232     }
233 
234     /**
235      * Submit entries.
236      *
237      * @return  the number of submitted entries.
238      */
239     int submit() {
240         checkClosed();
241         int submit = tail - head;
242         return submit > 0 ? submit(submit, 0, 0) : 0;
243     }
244 
245     /**
246      * Submit entries and fetch completions. This method will block until there is at least one completion ready to be
247      * processed.
248      *
249      * @return  the number of submitted entries.
250      */
251     int submitAndGet() {
252         return submitAndGet0(1);
253     }
254 
255     /**
256      * Submit entries and fetch completions.
257      *
258      * @return  the number of submitted entries.
259      */
260     int submitAndGetNow() {
261         return submitAndGet0(0);
262     }
263 
264     private int submitAndGet0(int minComplete) {
265         checkClosed();
266         int submit = tail - head;
267         if (submit > 0) {
268             return submit(submit, minComplete, Native.IORING_ENTER_GETEVENTS);
269         }
270         assert submit == 0;
271         int ret = ioUringEnter(0, minComplete, Native.IORING_ENTER_GETEVENTS);
272         if (ret < 0) {
273             throw new UncheckedIOException(Errors.newIOException("io_uring_enter", ret));
274         }
275         return ret; // should be 0
276     }
277 
278     private int submit(int toSubmit, int minComplete, int flags) {
279         INT_HANDLE.setRelease(kTail, 0, tail);
280         int ret = ioUringEnter(toSubmit, minComplete, flags);
281         head = (int) INT_HANDLE.getVolatile(kHead, 0); // acquire memory barrier
282         if (ret != toSubmit) {
283             if (ret < 0) {
284                 throw new UncheckedIOException(Errors.newIOException("io_uring_enter", ret));
285             }
286         }
287         return ret;
288     }
289 
290     private int ioUringEnter(int toSubmit, int minComplete, int flags) {
291         int f = enterFlags | flags;
292 
293         if (IoUring.isSetupSubmitAllSupported()) {
294             return ioUringEnter0(toSubmit, minComplete, f);
295         }
296         // If IORING_SETUP_SUBMIT_ALL is not supported we need to loop until we submitted everything as
297         // io_uring_enter(...) will stop submitting once the first inline executed submission fails.
298         int submitted = 0;
299         for (;;) {
300             int ret = ioUringEnter0(toSubmit, minComplete, f);
301             if (ret < 0) {
302                 return ret;
303             }
304             submitted += ret;
305             if (ret == toSubmit) {
306                 return submitted;
307             }
308             if (logger.isTraceEnabled()) {
309                 // some submission might fail if these are done inline and failed.
310                 logger.trace("Not all submissions succeeded. Only {} of {} SQEs were submitted.", ret, toSubmit);
311             }
312             toSubmit -= ret;
313         }
314     }
315 
316     private int ioUringEnter0(int toSubmit, int minComplete, int f) {
317         if (logger.isTraceEnabled()) {
318             logger.trace("io_uring_enter(ring={}, enterRing={}, toSubmit={}, minComplete={}, flags={}): {}",
319                     ringFd, enterRingFd, toSubmit, minComplete, f, toString());
320         }
321         return Native.ioUringEnter(enterRingFd, toSubmit, minComplete, f);
322     }
323 
324     public int count() {
325         return tail - head;
326     }
327 
328     public int remaining() {
329         return ringEntries - count();
330     }
331 }