13#include "ublk/ublk_cmd.h"
21#include <system_error>
28inline bool need_alloc_buf(uint64_t flags)
noexcept {
29 return !(flags & (UBLK_F_SUPPORT_ZERO_COPY | UBLK_F_USER_COPY));
32inline bool need_io_buf(uint64_t flags, uint32_t op_flags)
noexcept {
33 bool zero_copy = flags & UBLK_F_SUPPORT_ZERO_COPY;
34 bool auto_reg = flags & UBLK_F_AUTO_BUF_REG;
35 bool need_reg_buf = op_flags & UBLK_IO_F_NEED_REG_BUF;
36 return need_reg_buf || (zero_copy && !auto_reg);
39inline off_t io_desc_offset(
size_t q_id)
noexcept {
40 return UBLKSRV_CMD_BUF_OFFSET +
41 q_id * UBLK_MAX_QUEUE_DEPTH *
sizeof(ublksrv_io_desc);
45concept has_res =
requires(T t) {
46 { t.res } -> std::convertible_to<int32_t>;
50concept has_zone_lba =
requires(T t) {
51 { t.zone_lba } -> std::convertible_to<uint64_t>;
54inline void extract_io_result(
auto &&r, int32_t &res,
55 uint64_t &zone_lba)
noexcept {
56 using R = std::decay_t<
decltype(r)>;
57 if constexpr (std::convertible_to<R, int32_t>) {
61 static_assert(has_res<R>,
62 "returned result is neither int32_t nor has a "
66 if constexpr (has_zone_lba<R>) {
67 zone_lba = r.zone_lba;
72inline std::error_code normalize_error(
const auto &err,
int v)
noexcept {
73 using E = std::decay_t<
decltype(err)>;
74 if constexpr (std::same_as<E, std::error_code>) {
76 }
else if constexpr (std::same_as<E, std::exception_ptr>) {
78 std::rethrow_exception(err);
79 }
catch (
const std::system_error &se) {
82 return std::error_code(v, std::generic_category());
85 return std::error_code(v, std::generic_category());
89template <
typename Sender>
90inline auto wrap_handle_io(Sender &&sender, int32_t &res,
91 uint64_t &zone_lba)
noexcept {
92 return std::forward<Sender>(sender) |
94 std::error_code(ECANCELED, std::generic_category())) |
95 ex::upon_error([](
const auto &err)
noexcept {
96 return normalize_error(err, EIO);
98 ex::then([&](
auto r)
noexcept {
99 using R =
decltype(r);
100 if constexpr (std::same_as<R, std::error_code>) {
104 extract_io_result(r, res, zone_lba);
106 return std::make_tuple(res, zone_lba);
110template <
typename Fd, IoHandler Handler,
typename Alloc>
class IoQueue {
112 IoQueue(Fd ublkc_fd,
int ublkc_fd_raw, uint16_t q_id, uint64_t flags,
113 uint16_t queue_depth, uint32_t max_io_buf_bytes, Handler &handler,
115 : ublkc_fd_(ublkc_fd), q_id_(q_id), flags_(flags),
116 queue_depth_(queue_depth), max_io_buf_bytes_(max_io_buf_bytes),
117 handler_(handler), alloc_(alloc) {
119 auto d = defer([&]()
noexcept {
125 size_t size = queue_depth_ *
sizeof(ublksrv_io_desc);
126 auto off = io_desc_offset(q_id_);
127 void *addr = mmap(0, size, PROT_READ, MAP_SHARED | MAP_POPULATE,
129 if (addr == MAP_FAILED) {
130 throw std::system_error(errno, std::generic_category(),
133 iod_base_ =
reinterpret_cast<ublksrv_io_desc *
>(addr);
135 const size_t page = sysconf(_SC_PAGESIZE);
136 if (need_alloc_buf(flags_)) {
137 buf_bases_.reserve(queue_depth_);
138 for (uint16_t tag = 0; tag < queue_depth_; tag++) {
139 void *buf = alloc_aligned(alloc_, max_io_buf_bytes_, page);
140 buf_bases_.push_back(buf);
147 IoQueue(
const IoQueue &) =
delete;
148 IoQueue &operator=(
const IoQueue &) =
delete;
149 IoQueue(IoQueue &&) =
delete;
150 IoQueue &operator=(IoQueue &&) =
delete;
152 ~IoQueue() { cleanup_(); }
155 template <
typename Sched> ex::task<void, TaskEnv<Sched, Alloc>> run() {
156 if constexpr (QueueHandler<Handler>) {
157 co_await handler_.init_queue(q_id_);
160 auto d = defer([&]()
noexcept {
161 if constexpr (QueueHandler<Handler>) {
162 handler_.destroy_queue(q_id_);
166 ex::simple_counting_scope scope;
167 AllocVector<std::exception_ptr,
decltype(alloc_)> errs(queue_depth_,
169 auto sched =
co_await ex::read_env(ex::get_start_scheduler);
170 for (uint16_t tag = 0; tag < queue_depth_; tag++) {
171 auto task = worker_<Sched>(tag, iod_base_ + tag, get_buf_(tag));
173 ex::starts_on(sched, std::move(task)) |
174 ex::upon_error([&, tag](
const std::exception_ptr &ep)
noexcept {
177 ex::spawn(std::move(s), scope.get_token());
180 co_await scope.join();
182 for (
auto &ep : errs) {
184 std::rethrow_exception(ep);
190 void cleanup_() noexcept {
191 if (!buf_bases_.empty()) {
192 const size_t page = sysconf(_SC_PAGESIZE);
193 for (
auto *buf : buf_bases_) {
194 free_aligned(alloc_, buf, max_io_buf_bytes_, page);
199 size_t size = queue_depth_ *
sizeof(ublksrv_io_desc);
200 munmap(iod_base_, size);
205 void *get_buf_(uint16_t tag)
const noexcept {
206 if (buf_bases_.empty()) {
209 return buf_bases_[tag];
212 template <
typename Sched>
213 ex::task<void, TaskEnv<Sched, Alloc>>
214 worker_(uint16_t tag,
const ublksrv_io_desc *iod,
void *buf) {
216 bool zero_copy = flags_ & UBLK_F_SUPPORT_ZERO_COPY;
217 bool auto_reg = flags_ & UBLK_F_AUTO_BUF_REG;
219 uint64_t buf_addr =
reinterpret_cast<uint64_t
>(buf);
220 uint64_t sqe_addr = 0;
222 ublk_auto_buf_reg reg = {};
224 reg.flags = UBLK_AUTO_BUF_REG_FALLBACK;
225 sqe_addr = ublk_auto_buf_reg_to_sqe_addr(®);
231 [&](std::error_code ec)
noexcept {
return -ec.value(); }));
232 if (r == UBLK_IO_RES_NEED_GET_DATA) {
234 ex::upon_error([&](std::error_code ec)
noexcept {
238 if (r == UBLK_IO_RES_ABORT) {
241 throw std::system_error(-r, std::generic_category(),
"fetch_req");
245 bool io_buf = need_io_buf(flags_, iod->op_flags);
252 const IoData io_data{q_id_, tag, iod, buf};
253 co_await wrap_handle_io(handler_.handle_io(io_data), res, zone_lba);
259 uint64_t curr_buf_addr = buf_addr;
260 if (ublksrv_get_op(iod) == UBLK_IO_OP_ZONE_APPEND) {
261 assert(buf_addr == 0);
262 curr_buf_addr = zone_lba;
265 curr_buf_addr, sqe_addr) |
266 ex::upon_error([&](std::error_code ec)
noexcept {
269 if (r == UBLK_IO_RES_NEED_GET_DATA) {
272 ex::upon_error([&](std::error_code ec)
noexcept {
276 if (r == UBLK_IO_RES_ABORT) {
279 throw std::system_error(-r, std::generic_category(),
280 "commit_and_fetch_req");
289 uint16_t queue_depth_;
290 uint32_t max_io_buf_bytes_;
293 ublksrv_io_desc *iod_base_ =
nullptr;
294 AllocVector<void *, Alloc> buf_bases_;
297template <
typename Fd, IoHandler Handler,
typename Alloc>
class BatchIoQueue {
299 BatchIoQueue(Fd ublkc_fd,
int ublkc_fd_raw, uint16_t q_id, uint64_t flags,
300 uint16_t queue_depth, uint32_t max_io_buf_bytes,
301 Handler &handler,
const Alloc &alloc)
302 : ublkc_fd_(ublkc_fd), q_id_(q_id), flags_(flags),
303 queue_depth_(queue_depth), max_io_buf_bytes_(max_io_buf_bytes),
304 handler_(handler), alloc_(alloc) {
306 auto d = defer([&]()
noexcept {
313 size_t size = queue_depth_ *
sizeof(ublksrv_io_desc);
314 auto off = io_desc_offset(q_id_);
315 void *addr = mmap(0, size, PROT_READ, MAP_SHARED | MAP_POPULATE,
317 if (addr == MAP_FAILED) {
318 throw std::system_error(errno, std::generic_category(),
321 iod_base_ =
reinterpret_cast<ublksrv_io_desc *
>(addr);
324 const size_t page = sysconf(_SC_PAGESIZE);
326 size_t size = align_up(queue_depth_ * commit_element_size_(), page);
327 commit_buf_size_ = size;
328 commit_buf_[0] = alloc_aligned(alloc_, size * 2, page);
329 commit_buf_[1] =
static_cast<char *
>(commit_buf_[0]) + size;
330 if (mlock(commit_buf_[0], size * 2) < 0) {
331 throw std::system_error(errno, std::generic_category(),
337 size_t size = align_up(queue_depth_ *
sizeof(uint16_t), page);
338 fetch_buf_size_ = size;
339 fetch_buf_[0] =
reinterpret_cast<uint16_t *
>(
340 alloc_aligned(alloc_, size * 2, page));
341 fetch_buf_[1] =
reinterpret_cast<uint16_t *
>(
342 reinterpret_cast<char *
>(fetch_buf_[0]) + size);
343 if (mlock(fetch_buf_[0], size * 2) < 0) {
344 throw std::system_error(errno, std::generic_category(),
349 if (need_alloc_buf(flags_)) {
350 buf_bases_.reserve(queue_depth_);
351 for (uint16_t tag = 0; tag < queue_depth_; tag++) {
352 void *buf = alloc_aligned(alloc_, max_io_buf_bytes_, page);
353 buf_bases_.push_back(buf);
360 BatchIoQueue(
const BatchIoQueue &) =
delete;
361 BatchIoQueue &operator=(
const BatchIoQueue &) =
delete;
362 BatchIoQueue(BatchIoQueue &&) =
delete;
363 BatchIoQueue &operator=(BatchIoQueue &&) =
delete;
365 ~BatchIoQueue() { cleanup_(); }
368 template <
typename Sched> ex::task<void, TaskEnv<Sched, Alloc>> run() {
369 auto sched =
co_await ex::read_env(ex::get_scheduler);
370 auto alloc =
co_await ex::read_env(ex::get_allocator);
372 if constexpr (QueueHandler<Handler>) {
373 co_await handler_.init_queue(q_id_);
376 auto d = defer([&]()
noexcept {
377 if constexpr (QueueHandler<Handler>) {
378 handler_.destroy_queue(q_id_);
382 for (uint16_t tag = 0; tag < queue_depth_; tag++) {
383 set_result_(0, tag, tag, 0);
386 queue_depth_, commit_element_size_(),
388 queue_depth_ * commit_element_size_());
390 FlusherState flusher;
391 AllocVector<WorkerState,
decltype(alloc)> workers(queue_depth_, alloc);
393 ex::simple_counting_scope scope;
394 std::exception_ptr flusher_err;
395 AllocVector<std::exception_ptr,
decltype(alloc)> worker_errs(
396 queue_depth_, alloc);
397 bool worker_stopped =
false;
398 size_t running_workers = queue_depth_;
401 auto task = flusher_<Sched>(flusher, running_workers);
402 auto s = ex::starts_on(sched, std::move(task)) |
403 ex::upon_error([&](std::exception_ptr ep)
noexcept {
404 flusher_err = std::move(ep);
406 ex::spawn(std::move(s), scope.get_token());
408 for (uint16_t tag = 0; tag < queue_depth_; tag++) {
409 auto task = worker_<Sched>(workers[tag], flusher, worker_stopped,
410 tag, iod_base_ + tag, get_buf_(tag));
412 ex::starts_on(sched, std::move(task)) |
413 ex::upon_error([&, tag](
const std::exception_ptr &ep)
noexcept {
414 worker_errs[tag] = ep;
416 ex::then([&]()
noexcept {
417 if (--running_workers == 0) {
418 flusher.futex.notify_one();
421 ex::spawn(std::move(s), scope.get_token());
424 condy::ProvidedBufferQueue queue(2, IOU_PBUF_RING_INC);
425 queue.push(condy::buffer(fetch_buf_[0], fetch_buf_size_));
426 queue.push(condy::buffer(fetch_buf_[1], fetch_buf_size_));
430 auto fetch_cb = [&](std::pair<int32_t, condy::BufferInfo> r)
noexcept {
431 auto &[res, info] = r;
433 auto *fetch_buf = fetch_buf_[bid];
435 size_t nr_tags =
static_cast<size_t>(res) /
sizeof(uint16_t);
436 size_t end = off + nr_tags;
437 for (
size_t j = off; j < end; j++) {
438 uint16_t tag = fetch_buf[j];
439 auto &worker = workers[tag];
440 write_once_(worker.flag,
true);
441 worker.futex.notify_one();
444 bool consumed = info.num_buffers;
446 auto r = queue.push(condy::buffer(fetch_buf, fetch_buf_size_));
456 ex::then([](int32_t r, condy::BufferInfo)
noexcept {
return r; }) |
458 [&](std::error_code ec)
noexcept {
return -ec.value(); }) |
459 ex::then([&](int32_t r)
noexcept { res = r; }) |
460 ex::then([&]()
noexcept {
461 worker_stopped =
true;
462 for (
auto &worker : workers) {
463 worker.futex.notify_one();
467 co_await ex::when_all(std::move(s), scope.join());
469 for (
auto &ep : worker_errs) {
471 std::rethrow_exception(ep);
475 std::rethrow_exception(flusher_err);
478 if (res != UBLK_IO_RES_ABORT) {
479 throw std::system_error(-res, std::generic_category(),
485 void cleanup_() noexcept {
486 const size_t page = sysconf(_SC_PAGESIZE);
487 if (!buf_bases_.empty()) {
488 for (
auto *buf : buf_bases_) {
489 free_aligned(alloc_, buf, max_io_buf_bytes_, page);
494 munlock(fetch_buf_[0], fetch_buf_size_ * 2);
495 free_aligned(alloc_, fetch_buf_[0], fetch_buf_size_ * 2, page);
497 if (commit_buf_[0]) {
498 munlock(commit_buf_[0], commit_buf_size_ * 2);
499 free_aligned(alloc_, commit_buf_[0], commit_buf_size_ * 2, page);
502 size_t size = queue_depth_ *
sizeof(ublksrv_io_desc);
503 munmap(iod_base_, size);
508 template <
typename T>
509 static auto read_once_(
const std::atomic<T> &a)
noexcept {
510 return a.load(std::memory_order_relaxed);
513 template <
typename T,
typename V>
514 static void write_once_(std::atomic<T> &a, V v)
noexcept {
515 a.store(v, std::memory_order_relaxed);
518 bool need_f_buf_addr_() noexcept {
519 return !(flags_ & UBLK_F_AUTO_BUF_REG) && need_alloc_buf(flags_);
522 bool need_f_zone_lba_() noexcept {
return flags_ & UBLK_F_ZONED; }
524 uint16_t get_batch_flags_() noexcept {
526 if (flags_ & UBLK_F_AUTO_BUF_REG) {
527 f |= UBLK_BATCH_F_AUTO_BUF_REG_FALLBACK;
528 }
else if (need_f_buf_addr_()) {
529 f |= UBLK_BATCH_F_HAS_BUF_ADDR;
531 if (need_f_zone_lba_()) {
532 f |= UBLK_BATCH_F_HAS_ZONE_LBA;
537 size_t commit_element_size_() noexcept {
538 size_t size =
sizeof(ublk_elem_header);
539 if (need_f_buf_addr_()) {
540 size +=
sizeof(uint64_t);
542 if (need_f_zone_lba_()) {
543 size +=
sizeof(uint64_t);
548 ublk_elem_header &commit_f_hdr(
size_t index,
size_t pos)
noexcept {
549 return *
reinterpret_cast<ublk_elem_header *
>(
550 static_cast<char *
>(commit_buf_[index]) +
551 pos * commit_element_size_());
554 uint64_t &commit_f_buf_addr(
size_t index,
size_t pos)
noexcept {
555 assert(need_f_buf_addr_());
556 return *
reinterpret_cast<uint64_t *
>(
557 reinterpret_cast<char *
>(&commit_f_hdr(index, pos)) +
558 sizeof(ublk_elem_header));
561 uint64_t &commit_f_zone_lba(
size_t index,
size_t pos)
noexcept {
562 assert(need_f_zone_lba_());
563 size_t off =
sizeof(ublk_elem_header);
564 if (need_f_buf_addr_()) {
565 off +=
sizeof(uint64_t);
567 return *
reinterpret_cast<uint64_t *
>(
568 reinterpret_cast<char *
>(&commit_f_hdr(index, pos)) + off);
571 void set_result_(
size_t index,
size_t pos, uint16_t tag,
572 int32_t res)
noexcept {
573 auto &hdr = commit_f_hdr(index, pos);
576 if (flags_ & UBLK_F_AUTO_BUF_REG) {
578 }
else if (need_f_buf_addr_()) {
579 commit_f_buf_addr(index, pos) =
580 reinterpret_cast<uint64_t
>(buf_bases_[tag]);
584 void set_zone_lba_(
size_t index,
size_t pos, uint64_t lba)
noexcept {
585 assert(need_f_zone_lba_());
586 commit_f_zone_lba(index, pos) = lba;
589 void *get_buf_(uint16_t tag)
const noexcept {
590 if (buf_bases_.empty()) {
593 return buf_bases_[tag];
597 struct FlusherState {
599 std::atomic<uint16_t> pending = 0;
600 condy::Futex<uint16_t> futex{pending};
603 template <
typename Sched>
604 ex::task<void, TaskEnv<Sched, Alloc>> flusher_(FlusherState &state,
605 size_t &running_workers) {
606 auto sched =
co_await ex::read_env(ex::get_start_scheduler);
608 auto nr = read_once_(state.pending);
610 auto cur_index = state.index;
611 state.index = 1 - state.index;
612 write_once_(state.pending, 0);
614 ublkc_fd_, q_id_, get_batch_flags_(), nr,
615 commit_element_size_(), commit_buf_[cur_index],
616 nr * commit_element_size_()) |
617 ex::then([&](int32_t r) {
618 if (r != nr * commit_element_size_()) {
619 throw std::runtime_error(
620 "commit_io_cmds returned unexpected byte "
624 co_await std::move(s);
625 co_await ex::schedule(sched);
626 }
else if (running_workers == 0) {
629 co_await state.futex.wait(0);
635 std::atomic<bool> flag =
false;
636 condy::Futex<bool> futex{flag};
639 template <
typename Sched>
640 ex::task<void, TaskEnv<Sched, Alloc>>
641 worker_(WorkerState &state, FlusherState &flusher,
bool &stopped,
642 uint16_t tag,
const ublksrv_io_desc *iod,
void *buf) {
644 if (!stopped && !read_once_(state.flag)) {
645 co_await state.futex.wait(
false);
650 assert(read_once_(state.flag));
651 write_once_(state.flag,
false);
653 bool io_buf = need_io_buf(flags_, iod->op_flags);
660 const IoData io_data{q_id_, tag, iod, buf};
661 co_await wrap_handle_io(handler_.handle_io(io_data), res, zone_lba);
667 auto cur_index = flusher.index;
668 auto slot = read_once_(flusher.pending);
669 set_result_(cur_index, slot, tag, res);
670 if (ublksrv_get_op(iod) == UBLK_IO_OP_ZONE_APPEND) {
671 set_zone_lba_(cur_index, slot, zone_lba);
673 write_once_(flusher.pending, slot + 1);
675 flusher.futex.notify_one();
684 uint16_t queue_depth_;
685 uint32_t max_io_buf_bytes_;
688 ublksrv_io_desc *iod_base_ =
nullptr;
689 size_t commit_buf_size_ = 0;
690 void *commit_buf_[2] = {};
691 size_t fetch_buf_size_ = 0;
692 uint16_t *fetch_buf_[2] = {};
693 AllocVector<void *, Alloc> buf_bases_;
The std::execution backend namespace used by ublk-cpp.
Handler concepts for ublk server.
auto need_get_data(Fd fd, uint16_t q_id, uint16_t tag, uint64_t buf_addr) noexcept
Copy the data of a write request into the user buffer after the driver returns UBLK_IO_RES_NEED_GET_D...
auto commit_and_fetch_req(Fd fd, uint16_t q_id, uint16_t tag, int32_t result, uint64_t buf_addr, uint64_t sqe_addr) noexcept
Overload of commit_and_fetch_req() with an explicit sqe->addr, used for automatic request buffer regi...
auto fetch_io_cmds(Fd fd, condy::ProvidedBufferQueue &buffers, uint16_t q_id, MultiShotFunc &&func) noexcept
Fetch I/O commands in multishot style into a provided buffer queue (UBLK_F_BATCH_IO).
auto commit_io_cmds(Fd fd, uint16_t q_id, uint16_t flags, uint16_t nr_elem, uint8_t elem_bytes, const void *elems, uint32_t elems_len) noexcept
Commit a batch of completed I/O commands for a queue (UBLK_F_BATCH_IO).
auto unregister_io_buf(Fd fd, uint16_t q_id, uint16_t tag, uint64_t buf_index) noexcept
Unregister the buffer of an in-flight request from the io_uring buffer table after zero-copy I/O comp...
auto register_io_buf(Fd fd, uint16_t q_id, uint16_t tag, uint64_t buf_index) noexcept
Register the buffer of an in-flight request into the io_uring buffer table for zero-copy I/O (UBLK_F_...
auto prep_io_cmds(Fd fd, uint16_t q_id, uint16_t flags, uint16_t nr_elem, uint8_t elem_bytes, const void *elems, uint32_t elems_len) noexcept
Prepare a batch of I/O commands for a queue (UBLK_F_BATCH_IO).
auto fetch_req(Fd fd, uint16_t q_id, uint16_t tag, uint64_t buf_addr, uint64_t sqe_addr) noexcept
Overload of fetch_req() with an explicit sqe->addr, used for automatic request buffer registration (U...
The main namespace of the ublk-cpp library.
Low-level io_uring command senders for ublk control and I/O commands.