|
| 1 | +/** |
| 2 | + * @file loop.cpp |
| 3 | + * @brief A ublk loop device server |
| 4 | + * @details Maps a regular file or block device into a block device, |
| 5 | + * similar to the kernel loop driver. |
| 6 | + */ |
| 7 | + |
| 8 | +#include <bit> |
| 9 | +#include <cerrno> |
| 10 | +#include <condy.hpp> |
| 11 | +#include <csignal> |
| 12 | +#include <cstdint> |
| 13 | +#include <cstring> |
| 14 | +#include <exception> |
| 15 | +#include <fcntl.h> |
| 16 | +#include <getopt.h> |
| 17 | +#include <iostream> |
| 18 | +#include <linux/fs.h> |
| 19 | +#include <print> |
| 20 | +#include <string> |
| 21 | +#include <sys/ioctl.h> |
| 22 | +#include <sys/signalfd.h> |
| 23 | +#include <sys/stat.h> |
| 24 | +#include <system_error> |
| 25 | +#include <thread> |
| 26 | +#include <ublk.hpp> |
| 27 | +#include <unistd.h> |
| 28 | + |
| 29 | +namespace ex = condy::detail::ex; |
| 30 | + |
| 31 | +namespace { |
| 32 | + |
| 33 | +constexpr size_t SECTOR_SIZE = 512; |
| 34 | +constexpr uint16_t DEFAULT_QUEUE_DEPTH = 64; |
| 35 | +constexpr uint32_t DEFAULT_IO_BUF_BYTES = 512u << 10; |
| 36 | + |
| 37 | +struct FileInfo { |
| 38 | + uint64_t size = 0; |
| 39 | + uint8_t logical_bs_shift = 9; |
| 40 | + uint8_t physical_bs_shift = 12; |
| 41 | +}; |
| 42 | + |
| 43 | +uint8_t bs_shift_of(uint64_t v) noexcept { |
| 44 | + if (v > 0 && (v & (v - 1)) == 0) { |
| 45 | + return static_cast<uint8_t>(std::bit_width(v) - 1); |
| 46 | + } |
| 47 | + return 9; |
| 48 | +} |
| 49 | + |
| 50 | +void lo_file_size(int fd, FileInfo &info) { |
| 51 | + struct stat st; |
| 52 | + if (fstat(fd, &st) < 0) { |
| 53 | + throw std::system_error(errno, std::generic_category(), |
| 54 | + "fstat backing file"); |
| 55 | + } |
| 56 | + |
| 57 | + if (S_ISBLK(st.st_mode)) { |
| 58 | + uint64_t size = 0; |
| 59 | + int lsz = 0; |
| 60 | + int psz = 0; |
| 61 | + if (ioctl(fd, BLKGETSIZE64, &size) < 0) { |
| 62 | + throw std::system_error(errno, std::generic_category(), |
| 63 | + "BLKGETSIZE64"); |
| 64 | + } |
| 65 | + if (ioctl(fd, BLKSSZGET, &lsz) < 0) { |
| 66 | + throw std::system_error(errno, std::generic_category(), |
| 67 | + "BLKSSZGET"); |
| 68 | + } |
| 69 | + if (ioctl(fd, BLKPBSZGET, &psz) < 0) { |
| 70 | + throw std::system_error(errno, std::generic_category(), |
| 71 | + "BLKPBSZGET"); |
| 72 | + } |
| 73 | + info.size = size; |
| 74 | + info.logical_bs_shift = bs_shift_of(static_cast<uint64_t>(lsz)); |
| 75 | + info.physical_bs_shift = bs_shift_of(static_cast<uint64_t>(psz)); |
| 76 | + return; |
| 77 | + } |
| 78 | + |
| 79 | + if (S_ISREG(st.st_mode)) { |
| 80 | + info.size = static_cast<uint64_t>(st.st_size); |
| 81 | + info.logical_bs_shift = 9; |
| 82 | + info.physical_bs_shift = 12; |
| 83 | + return; |
| 84 | + } |
| 85 | + |
| 86 | + throw std::runtime_error("unsupported backing file type"); |
| 87 | +} |
| 88 | + |
| 89 | +void prep_dev_info(ublksrv_ctrl_dev_info &info, uint32_t dev_id, |
| 90 | + uint16_t nr_queues, uint16_t depth, uint32_t buf_bytes, |
| 91 | + uint64_t flags) noexcept { |
| 92 | + info = {}; |
| 93 | + info.dev_id = dev_id; |
| 94 | + info.nr_hw_queues = nr_queues; |
| 95 | + info.queue_depth = depth; |
| 96 | + info.max_io_buf_bytes = buf_bytes; |
| 97 | + info.flags = flags; |
| 98 | +} |
| 99 | + |
| 100 | +void prep_params(ublk_params ¶ms, const ublksrv_ctrl_dev_info &info, |
| 101 | + uint64_t dev_sectors, uint8_t logical_bs_shift, |
| 102 | + uint8_t physical_bs_shift) noexcept { |
| 103 | + params = {}; |
| 104 | + params.len = sizeof(params); |
| 105 | + params.types = UBLK_PARAM_TYPE_BASIC; |
| 106 | + params.basic.attrs = UBLK_ATTR_VOLATILE_CACHE; |
| 107 | + params.basic.logical_bs_shift = logical_bs_shift; |
| 108 | + params.basic.physical_bs_shift = physical_bs_shift; |
| 109 | + params.basic.io_opt_shift = 12; |
| 110 | + params.basic.io_min_shift = 9; |
| 111 | + params.basic.max_sectors = info.max_io_buf_bytes >> 9; |
| 112 | + params.basic.dev_sectors = dev_sectors; |
| 113 | +} |
| 114 | + |
| 115 | +struct LoopHandler { |
| 116 | + static constexpr int BACKING_FD = 1; |
| 117 | + |
| 118 | + int backing_fd = -1; |
| 119 | + |
| 120 | + auto init_queue(uint16_t /*q_id*/) noexcept { |
| 121 | + int fd = backing_fd; |
| 122 | + return ex::just() | ex::then([fd]() { |
| 123 | + auto &fd_table = condy::current_runtime().fd_table(); |
| 124 | + int r = fd_table.update(BACKING_FD, &fd, 1); |
| 125 | + if (r != 1) { |
| 126 | + throw std::system_error( |
| 127 | + -r, std::generic_category(), |
| 128 | + "ublk-loop: register backing file"); |
| 129 | + } |
| 130 | + }); |
| 131 | + } |
| 132 | + |
| 133 | + void destroy_queue(uint16_t /*q_id*/) noexcept { |
| 134 | + int fd = -1; |
| 135 | + auto &fd_table = condy::current_runtime().fd_table(); |
| 136 | + fd_table.update(BACKING_FD, &fd, 1); |
| 137 | + } |
| 138 | + |
| 139 | + ex::task<int32_t> handle_io(const ublk::IoData &data) noexcept { |
| 140 | + uint8_t op = ublksrv_get_op(data.iod); |
| 141 | + uint64_t off = |
| 142 | + static_cast<uint64_t>(data.iod->start_sector) * SECTOR_SIZE; |
| 143 | + uint32_t bytes = data.iod->nr_sectors * SECTOR_SIZE; |
| 144 | + |
| 145 | + switch (op) { |
| 146 | + case UBLK_IO_OP_READ: |
| 147 | + co_return co_await condy::async_read( |
| 148 | + condy::fixed(BACKING_FD), condy::buffer(data.buf, bytes), off); |
| 149 | + case UBLK_IO_OP_WRITE: |
| 150 | + co_return co_await condy::async_write( |
| 151 | + condy::fixed(BACKING_FD), condy::buffer(data.buf, bytes), off); |
| 152 | + case UBLK_IO_OP_FLUSH: |
| 153 | + co_return co_await condy::async_fsync(condy::fixed(BACKING_FD), |
| 154 | + IORING_FSYNC_DATASYNC); |
| 155 | + default: |
| 156 | + co_return -EINVAL; |
| 157 | + } |
| 158 | + } |
| 159 | +}; |
| 160 | + |
| 161 | +static_assert(ublk::IoHandler<LoopHandler>); |
| 162 | +static_assert(ublk::QueueHandler<LoopHandler>); |
| 163 | + |
| 164 | +ex::task<void> wait_signal(int ctrl_fd, int signal_fd, uint32_t dev_id, |
| 165 | + ex::inplace_stop_source &source) { |
| 166 | + std::println("ublk-loop: ublk device {} is running...", dev_id); |
| 167 | + auto parent_token = co_await ex::read_env(ex::get_stop_token); |
| 168 | + auto stop_request = [&] noexcept { source.request_stop(); }; |
| 169 | + ex::inplace_stop_callback<decltype(stop_request)> cb{ |
| 170 | + parent_token, std::move(stop_request)}; |
| 171 | + signalfd_siginfo si; |
| 172 | + co_await (condy::async_read(signal_fd, condy::buffer(&si, sizeof(si)), 0) | |
| 173 | + ex::write_env(ex::prop{ex::get_stop_token, source.get_token()})); |
| 174 | + std::println("ublk-loop: received signal {}, shutting down...", |
| 175 | + si.ssi_signo); |
| 176 | + co_await ublk::stop_dev(ctrl_fd, dev_id); |
| 177 | +} |
| 178 | + |
| 179 | +void print_usage(const char *prog) { |
| 180 | + std::println("Usage: {} [OPTIONS] -f <backing_file>", prog); |
| 181 | + std::println( |
| 182 | + "Run a ublk loop device backed by a regular file or block device."); |
| 183 | + std::println(); |
| 184 | + std::println("Options:"); |
| 185 | + std::println(" -f <file> backing file or block device (required)"); |
| 186 | + std::println( |
| 187 | + " -n <dev_id> ublk device id, -1: auto-allocation (default)"); |
| 188 | + std::println(" -q <queues> nr_hw_queues (default: 1)"); |
| 189 | + std::println( |
| 190 | + " -d <depth> queue depth, max in-flight io commands (default: 64)"); |
| 191 | + std::println(" -b <bytes> io buffer size (default: 524288)"); |
| 192 | + std::println( |
| 193 | + " -p enable UBLK_F_UNPRIVILEGED_DEV (unprivileged mode)"); |
| 194 | + std::println(" -h show this help and exit"); |
| 195 | +} |
| 196 | + |
| 197 | +} // namespace |
| 198 | + |
| 199 | +// NOLINTNEXTLINE(bugprone-unsafe-to-allow-exceptions) |
| 200 | +int main(int argc, char *argv[]) noexcept(false) { |
| 201 | + uint32_t dev_id = -1; |
| 202 | + uint16_t nr_queues = 1; |
| 203 | + uint16_t queue_depth = DEFAULT_QUEUE_DEPTH; |
| 204 | + uint32_t buf_bytes = DEFAULT_IO_BUF_BYTES; |
| 205 | + bool unprivileged = false; |
| 206 | + std::string backing_path; |
| 207 | + |
| 208 | + int opt; |
| 209 | + while ((opt = getopt(argc, argv, "f:n:q:d:b:ph")) != -1) { |
| 210 | + switch (opt) { |
| 211 | + case 'f': |
| 212 | + backing_path = optarg; |
| 213 | + break; |
| 214 | + case 'n': |
| 215 | + dev_id = std::stoul(optarg); |
| 216 | + break; |
| 217 | + case 'q': |
| 218 | + nr_queues = std::stoul(optarg); |
| 219 | + break; |
| 220 | + case 'd': |
| 221 | + queue_depth = std::stoul(optarg); |
| 222 | + break; |
| 223 | + case 'b': |
| 224 | + buf_bytes = std::stoul(optarg); |
| 225 | + break; |
| 226 | + case 'p': |
| 227 | + unprivileged = true; |
| 228 | + break; |
| 229 | + case 'h': |
| 230 | + default: |
| 231 | + print_usage(argv[0]); |
| 232 | + return opt == 'h' ? 0 : 1; |
| 233 | + } |
| 234 | + } |
| 235 | + |
| 236 | + if (backing_path.empty()) { |
| 237 | + print_usage(argv[0]); |
| 238 | + return 1; |
| 239 | + } |
| 240 | + |
| 241 | + try { |
| 242 | + int backing_fd = open(backing_path.c_str(), O_RDWR); |
| 243 | + if (backing_fd < 0) { |
| 244 | + throw std::system_error(errno, std::generic_category(), |
| 245 | + "open backing file"); |
| 246 | + } |
| 247 | + auto d_backing = |
| 248 | + ublk::detail::defer([&] noexcept { close(backing_fd); }); |
| 249 | + |
| 250 | + int flags = fcntl(backing_fd, F_GETFL); |
| 251 | + if (flags >= 0 && fcntl(backing_fd, F_SETFL, flags | O_DIRECT) < 0) { |
| 252 | + std::println( |
| 253 | + std::cerr, |
| 254 | + "ublk-loop: failed to set O_DIRECT on backing file: {}", |
| 255 | + std::strerror(errno)); |
| 256 | + } |
| 257 | + |
| 258 | + FileInfo file_info; |
| 259 | + lo_file_size(backing_fd, file_info); |
| 260 | + uint64_t dev_sectors = file_info.size / SECTOR_SIZE; |
| 261 | + std::println("ublk-loop: backing file {} ({} bytes, {} sectors, " |
| 262 | + "logical {}B, physical {}B)", |
| 263 | + backing_path, file_info.size, dev_sectors, |
| 264 | + 1u << file_info.logical_bs_shift, |
| 265 | + 1u << file_info.physical_bs_shift); |
| 266 | + |
| 267 | + sigset_t mask; |
| 268 | + sigemptyset(&mask); |
| 269 | + sigaddset(&mask, SIGINT); |
| 270 | + sigaddset(&mask, SIGTERM); |
| 271 | + sigprocmask(SIG_BLOCK, &mask, nullptr); |
| 272 | + |
| 273 | + int signal_fd = signalfd(-1, &mask, SFD_NONBLOCK); |
| 274 | + if (signal_fd < 0) { |
| 275 | + throw std::system_error(errno, std::generic_category(), "signalfd"); |
| 276 | + } |
| 277 | + auto d = ublk::detail::defer([&] noexcept { close(signal_fd); }); |
| 278 | + |
| 279 | + int ctrl_fd = open("/dev/ublk-control", O_RDWR | O_CLOEXEC); |
| 280 | + if (ctrl_fd < 0) { |
| 281 | + throw std::system_error(errno, std::generic_category(), |
| 282 | + "open /dev/ublk-control"); |
| 283 | + } |
| 284 | + auto d2 = ublk::detail::defer([&] noexcept { close(ctrl_fd); }); |
| 285 | + |
| 286 | + condy::RuntimeOptions options; |
| 287 | + options.enable_sqe128(); |
| 288 | + condy::Runtime runtime(options); |
| 289 | + std::jthread loop([&]() { runtime.run(); }); |
| 290 | + auto d3 = ublk::detail::defer([&] noexcept { runtime.allow_exit(); }); |
| 291 | + ex::scheduler auto sched = condy::get_scheduler(runtime); |
| 292 | + |
| 293 | + uint64_t features; |
| 294 | + ublksrv_ctrl_dev_info info; |
| 295 | + ublk_params params; |
| 296 | + LoopHandler handler{backing_fd}; |
| 297 | + ex::inplace_stop_source stop_source; |
| 298 | + |
| 299 | + ex::sender auto s = |
| 300 | + ublk::get_features(ctrl_fd, &features) | |
| 301 | + ex::let_value([&]() noexcept { |
| 302 | + uint64_t flags = 0; |
| 303 | + if (features & UBLK_F_USER_RECOVERY) { |
| 304 | + flags |= UBLK_F_USER_RECOVERY; |
| 305 | + } |
| 306 | + if (unprivileged) { |
| 307 | + flags |= UBLK_F_UNPRIVILEGED_DEV; |
| 308 | + } |
| 309 | + prep_dev_info(info, dev_id, nr_queues, queue_depth, buf_bytes, |
| 310 | + flags); |
| 311 | + return ublk::daemon::setup(ctrl_fd, &info); |
| 312 | + }) | |
| 313 | + ex::let_value([&](bool) noexcept { |
| 314 | + prep_params(params, info, dev_sectors, |
| 315 | + file_info.logical_bs_shift, |
| 316 | + file_info.physical_bs_shift); |
| 317 | + return ublk::daemon::configure(ctrl_fd, info.dev_id, ¶ms); |
| 318 | + }) | |
| 319 | + ex::let_value([&](bool) noexcept { |
| 320 | + ex::sender auto daemon = |
| 321 | + ublk::daemon::run(ctrl_fd, info.dev_id, handler, |
| 322 | + {.nr_files = 2}) | |
| 323 | + ex::then([&]() noexcept { stop_source.request_stop(); }); |
| 324 | + ex::sender auto start = |
| 325 | + ublk::daemon::start(ctrl_fd, info.dev_id, getpid()) | |
| 326 | + ex::let_value([&]() { |
| 327 | + return wait_signal(ctrl_fd, signal_fd, info.dev_id, |
| 328 | + stop_source); |
| 329 | + }); |
| 330 | + return ex::when_all(daemon, start); |
| 331 | + }) | |
| 332 | + ex::let_value( |
| 333 | + [&]() noexcept { return ublk::del_dev(ctrl_fd, info.dev_id); }); |
| 334 | + |
| 335 | + ex::sync_wait(ex::starts_on(sched, s)); |
| 336 | + } catch (const std::system_error &e) { |
| 337 | + std::println(std::cerr, "ublk-loop: {}", e.what()); |
| 338 | + return e.code().value(); |
| 339 | + } catch (const std::exception &e) { |
| 340 | + std::println(std::cerr, "ublk-loop: {}", e.what()); |
| 341 | + return 1; |
| 342 | + } |
| 343 | + |
| 344 | + return 0; |
| 345 | +} |
0 commit comments