Prompt and scope
liburing provides a multishot accept operation that can produce multiple completion queue entries (CQEs) from one submission. The request can stop producing completions after an error or when the multishot flag is absent. The core skill is asynchronous systems programming and belongs to coding.
What interviewers assess
Strong answers explain IORINGCQEF_MORE, CQE ownership, submission and completion queue backpressure, accept errors, cancellation, and rearming. They probe kernel support instead of assuming a feature, avoid reusing buffers or user data prematurely, and compare the design with a conventional nonblocking accept loop.
Questions to clarify first
- Which kernel and liburing versions are deployed?
- Is the listening socket shared across workers or owned by one ring?
- What connection rate, burst size, and file-descriptor budget are expected?
- How are accepted sockets handed to protocol workers?
- What should happen when the multishot request terminates or the CQ is full?
- Is a portable or non-io_uring fallback required?
30-second answer framework
“I would probe support, submit one multishot accept, and process each CQE as an independent accepted socket. I would inspect IORINGCQEF_MORE; when it is absent, the request is no longer armed and must be resubmitted after handling the terminal result. The loop needs bounded CQ depth, explicit EMFILE and transient-error handling, socket handoff ownership, and a fallback accept loop. I would benchmark CPU, accepts per second, tail latency, drops, CQ overflow, and rearm gaps.”
Step-by-step answer
Step 1: Probe and configure
Use the ring probe or documented capability checks for the multishot accept operation and required flags. Set queue sizes from burst and handoff rates, configure close-on-exec and nonblocking behavior, and decide whether direct descriptors are worth their management cost.
Step 2: Submit one long-lived request
Prepare the multishot accept request with stable user data that identifies the listening socket and generation. Do not assume one SQE yields one CQE. Keep the request’s lifecycle in the event-loop state and avoid freeing associated state until its terminal completion is consumed.
Step 3: Consume every completion
For each CQE, check the result for an accepted descriptor or a negative error. Transfer ownership of a successful socket exactly once to the protocol worker. Inspect IORINGCQEF_MORE; if it is clear, mark the request inactive even if the current CQE was successful.
Step 4: Rearm and apply backpressure
After draining available CQEs, resubmit when the request is inactive and the system can accept more work. Bound handoff queues, pause or reject new work under file-descriptor pressure, and handle EMFILE, ENFILE, and transient network errors without a busy loop.
Step 5: Shut down and benchmark
Cancel or close the request during shutdown, drain CQEs, and close unowned accepted sockets. Compare multishot and conventional accept under identical cores, backlog, connection mix, and worker capacity. Measure rearm gaps and dropped or refused connections, not only syscall count.
Model answer
“Multishot accept reduces submission overhead, but it is a stream of CQEs with a finite request lifetime. I would probe support, submit stable user data, consume each accepted descriptor once, and inspect IORINGCQEF_MORE on every completion. Once the flag disappears, I would mark the request inactive and rearm it after handling the terminal result. Queue depth, handoff backpressure, EMFILE handling, shutdown draining, and a conventional accept fallback are part of correctness. Benchmarks must include rearm gaps, drops, tail latency, and CPU.”
Common mistakes
- Assuming the request is permanent → completions stop after an error or without MORE → rearm explicitly.
- Treating one CQE as the whole result → subsequent accepted sockets are missed → drain all CQEs.
- Freeing user data early → later completions use invalid state → retain state until terminal completion.
- Ignoring negative results → the loop spins or hides resource exhaustion → classify errors and back off.
- Unbounded handoff queue → accepted descriptors exhaust the process → apply descriptor and queue budgets.
- Benchmarking only syscalls → rearm gaps and drops remain hidden → measure end-to-end connection outcomes.
Follow-up questions
Follow-up 1: What does IORINGCQEF_MORE mean?
It indicates that the multishot request is expected to produce more CQEs. When it is absent, the request has ended and the application must not assume another completion will arrive.
Follow-up 2: Can a successful CQE be the terminal one?
Yes. A CQE can contain a valid accepted descriptor while lacking the MORE flag. Process the socket, then rearm the request.
Follow-up 3: How do you avoid descriptor exhaustion?
Bound protocol handoff, monitor RLIMIT_NOFILE, handle EMFILE and ENFILE, and stop accepting or shed load until capacity returns.
Follow-up 4: Why keep a fallback?
Kernel, liburing, container, or policy constraints may prevent io_uring. A nonblocking accept loop preserves availability and gives a correctness baseline for performance comparisons.