Skip to content

Commit 9dcbf3a

Browse files
committed
add loop example
1 parent 97b2eb7 commit 9dcbf3a

3 files changed

Lines changed: 350 additions & 0 deletions

File tree

‎docs/examples.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44

55
- [ublk-nop](nop_8cpp.html)
66
A minimal no-op ublk block device server demonstrating the full daemon lifecycle.
7+
- [ublk-loop](loop_8cpp.html)
8+
A ublk loop device server mapping a regular file into a block device. It is a minimal example with real functionality, using a regular file or block device as the backend.

‎examples/CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
add_executable(ublk-nop nop.cpp)
22
target_link_libraries(ublk-nop PRIVATE ublkcpp)
3+
4+
add_executable(ublk-loop loop.cpp)
5+
target_link_libraries(ublk-loop PRIVATE ublkcpp)

‎examples/loop.cpp‎

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
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 &params, 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, &params);
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

Comments
 (0)