Condy v1.9
C++ Asynchronous System Call Layer for Linux
Loading...
Searching...
No Matches
fuse-prime-fs.cpp
Go to the documentation of this file.
1
15
16#include <cassert>
17#include <charconv>
18#include <condy.hpp>
19#include <cstdlib>
20#include <cstring>
21#include <ctime>
22#include <dirent.h>
23#include <fcntl.h>
24#include <format>
25#include <iostream>
26#include <linux/fuse.h>
27#include <memory>
28#include <string>
29#include <sys/mount.h>
30#include <sys/signalfd.h>
31#include <sys/stat.h>
32#include <sys/sysinfo.h>
33#include <thread>
34#include <unistd.h>
35
36uint64_t number = 10;
37size_t queue_depth = 8;
38bool multi_thread = false;
39std::string mountpoint;
40
41constexpr auto *FUSE_DEV = "/dev/fuse";
42constexpr auto *FUSE_NAME = "fuse-prime-fs";
43constexpr size_t MAX_READAHEAD = 128ul * 1024;
44constexpr size_t MAX_PAGES = 32;
45constexpr size_t MAX_WRITE = 128ul * 1024;
46const size_t MAX_PAYLOAD_SZ = std::max<size_t>(
47 {FUSE_MIN_READ_BUFFER, MAX_WRITE, MAX_PAGES *sysconf(_SC_PAGESIZE)});
48constexpr int FIXED_FD = 0;
49
50void usage(const char *prog) {
51 std::cerr << std::format("Usage: {} [OPTIONS] <mountpoint>\n"
52 "Options:\n"
53 " -n NUM Max node number (default: 10)\n"
54 " -q NUM Queue depth per CPU (default: 8)\n"
55 " -m Enable multi-thread mode\n"
56 " -h Show this help\n",
57 prog);
58}
59
60void mount_fuse(int fuse_fd, const std::string &mountpoint) {
61 int fsfd = fsopen("fuse", 0);
62 if (fsfd < 0) {
63 std::perror("fsopen");
64 std::exit(1);
65 }
66
67 if (fsconfig(fsfd, FSCONFIG_SET_STRING, "fd",
68 std::to_string(fuse_fd).c_str(), 0) < 0) {
69 std::perror("fsconfig fd");
70 std::exit(1);
71 }
72 if (fsconfig(fsfd, FSCONFIG_SET_STRING, "source", FUSE_NAME, 0) < 0) {
73 std::perror("fsconfig source");
74 std::exit(1);
75 }
76 if (fsconfig(fsfd, FSCONFIG_SET_STRING, "subtype", FUSE_NAME, 0) < 0) {
77 std::perror("fsconfig subtype");
78 std::exit(1);
79 }
80 std::string rootmode = std::format(
81 "{:#o}", S_IFDIR | S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
82 if (fsconfig(fsfd, FSCONFIG_SET_STRING, "rootmode", rootmode.c_str(), 0) <
83 0) {
84 std::perror("fsconfig rootmode");
85 std::exit(1);
86 }
87 if (fsconfig(fsfd, FSCONFIG_SET_STRING, "user_id",
88 std::to_string(getuid()).c_str(), 0) < 0) {
89 std::perror("fsconfig user_id");
90 std::exit(1);
91 }
92 if (fsconfig(fsfd, FSCONFIG_SET_STRING, "group_id",
93 std::to_string(getgid()).c_str(), 0) < 0) {
94 std::perror("fsconfig group_id");
95 std::exit(1);
96 }
97 if (fsconfig(fsfd, FSCONFIG_CMD_CREATE, nullptr, nullptr, 0) < 0) {
98 std::perror("fsconfig create");
99 std::exit(1);
100 }
101
102 int mfd = fsmount(fsfd, 0, 0);
103 if (mfd < 0) {
104 std::perror("fsmount");
105 std::exit(1);
106 }
107
108 if (move_mount(mfd, "", AT_FDCWD, mountpoint.c_str(),
109 MOVE_MOUNT_F_EMPTY_PATH) < 0) {
110 std::perror("move_mount");
111 std::exit(1);
112 }
113
114 close(mfd);
115 close(fsfd);
116}
117
118struct FuseInitReq {
119 fuse_in_header in;
120 fuse_init_in init;
121};
122
123struct FuseInitResp {
124 fuse_out_header out;
125 fuse_init_out init;
126};
127
128int check_init_request(const FuseInitReq *req, size_t n) {
129 if (n < sizeof(FuseInitReq)) {
130 std::cerr << "FUSE_INIT request too small\n";
131 return -EPROTO;
132 }
133 if (req->in.opcode != FUSE_INIT) {
134 std::cerr << std::format("Expected FUSE_INIT, got {}\n",
135 req->in.opcode);
136 return -EPROTO;
137 }
138 if (req->init.major != FUSE_KERNEL_VERSION) {
139 std::cerr << std::format("Unsupported major version {}\n",
140 req->init.major);
141 return -EPROTO;
142 }
143 constexpr uint32_t FUSE_IO_URING_MINOR = 42;
144 if (req->init.minor < FUSE_IO_URING_MINOR) {
145 std::cerr << std::format("Unsupported minor version {}\n",
146 req->init.minor);
147 return -EOPNOTSUPP;
148 }
149 return 0;
150}
151
152void init_fuse(int fuse_fd) {
153 static char buf[FUSE_MIN_READ_BUFFER];
154 auto *req = reinterpret_cast<FuseInitReq *>(buf);
155 auto *resp = reinterpret_cast<FuseInitResp *>(buf);
156
157 ssize_t n = read(fuse_fd, buf, sizeof(buf));
158 if (n < 0) {
159 std::perror("read FUSE_INIT");
160 std::exit(1);
161 }
162 if (int err = check_init_request(req, n); err != 0) {
163 resp->out.len = sizeof(resp->out);
164 resp->out.error = err;
165 resp->out.unique = req->in.unique;
166 if (write(fuse_fd, buf, resp->out.len) < 0) {
167 std::perror("write FUSE_INIT error reply");
168 }
169 std::exit(1);
170 }
171
172 fuse_init_out init = {};
173 init.major = FUSE_KERNEL_VERSION;
174 init.minor = std::min<uint32_t>(FUSE_KERNEL_MINOR_VERSION, req->init.minor);
175 init.max_write = MAX_WRITE;
176 init.max_pages = MAX_PAGES;
177 init.max_readahead = MAX_READAHEAD;
178 init.flags =
179 FUSE_ASYNC_READ | FUSE_BIG_WRITES | FUSE_MAX_PAGES | FUSE_INIT_EXT;
180 init.flags2 = static_cast<uint32_t>(FUSE_OVER_IO_URING >> 32);
181
182 resp->out.len = sizeof(*resp);
183 resp->out.error = 0;
184 resp->out.unique = req->in.unique;
185 resp->init = init;
186
187 n = write(fuse_fd, buf, resp->out.len);
188 if (n < 0) {
189 std::perror("write FUSE_INIT reply");
190 std::exit(1);
191 }
192}
193
194auto fuse_register_cmd(iovec iov[2], uint16_t qid) {
196 FUSE_IO_URING_CMD_REGISTER, condy::fixed(FIXED_FD),
197 [iov, qid](io_uring_sqe *sqe) {
198 sqe->addr = reinterpret_cast<uint64_t>(iov);
199 sqe->len = 2;
200 auto *cmd = reinterpret_cast<fuse_uring_cmd_req *>(sqe->cmd);
201 *cmd = {};
202 cmd->qid = qid;
203 },
205}
206
207auto fuse_commit_and_fetch_cmd(uint16_t qid, uint64_t commit_id) {
209 FUSE_IO_URING_CMD_COMMIT_AND_FETCH, condy::fixed(FIXED_FD),
210 [qid, commit_id](io_uring_sqe *sqe) {
211 auto *cmd = reinterpret_cast<fuse_uring_cmd_req *>(sqe->cmd);
212 *cmd = {};
213 cmd->qid = qid;
214 cmd->commit_id = commit_id;
215 },
217}
218
219class FuseServer {
220public:
221 FuseServer(uint64_t number)
222 : number_(number), now_(time(nullptr)), uid_(getuid()), gid_(getgid()) {
223 }
224
225 condy::Coro<void> handle(fuse_uring_req_header *hdr, void *payload) {
226 auto opcode = reinterpret_cast<fuse_in_header *>(hdr->in_out)->opcode;
227 Request req{hdr, payload};
228
229 switch (opcode) {
230 case FUSE_LOOKUP:
231 do_lookup_(req);
232 break;
233 case FUSE_GETATTR:
234 do_getattr_(req);
235 break;
236 case FUSE_OPEN:
237 case FUSE_OPENDIR:
238 do_open_(req);
239 break;
240 case FUSE_READ:
241 do_read_(req);
242 break;
243 case FUSE_READDIR:
244 do_readdir_(req);
245 break;
246 case FUSE_STATFS:
247 do_statfs_(req);
248 break;
249 case FUSE_RELEASE:
250 case FUSE_RELEASEDIR:
251 case FUSE_FLUSH:
252 case FUSE_FSYNC:
253 case FUSE_FSYNCDIR:
254 case FUSE_ACCESS:
255 case FUSE_DESTROY:
256 req.reply_err(0);
257 break;
258 default:
259 req.reply_err(-ENOSYS);
260 break;
261 }
262 co_return;
263 }
264
265private:
266 struct Request {
267 fuse_uring_req_header *hdr;
268 void *payload;
269
270 uint64_t nodeid() const { return in()->nodeid; }
271
272 template <typename T> const T *op_in() const {
273 return reinterpret_cast<const T *>(hdr->op_in);
274 }
275
276 void reply_err(int err) {
277 out()->len = sizeof(*out());
278 out()->error = err;
279 out()->unique = in()->unique;
280 hdr->ring_ent_in_out.payload_sz = 0;
281 }
282
283 void reply_ok(const void *data, size_t size) {
284 out()->error = 0;
285 assert(size <= MAX_PAYLOAD_SZ);
286 if (data) {
287 std::memcpy(payload, data, size);
288 }
289 out()->len = sizeof(*out()) + size;
290 out()->unique = in()->unique;
291 hdr->ring_ent_in_out.payload_sz = size;
292 }
293
294 private:
295 fuse_in_header *in() const {
296 return reinterpret_cast<fuse_in_header *>(hdr->in_out);
297 }
298
299 fuse_out_header *out() const {
300 return reinterpret_cast<fuse_out_header *>(hdr->in_out);
301 }
302 };
303
304private:
305 void do_lookup_(Request &req) {
306 uint64_t parent = req.nodeid();
307 std::string_view name = static_cast<const char *>(req.payload);
308
309 if (!is_dir_(parent)) {
310 req.reply_err(-ENOTDIR);
311 return;
312 }
313
314 uint64_t num = 0;
315 auto [ptr, ec] = std::from_chars(name.begin(), name.end(), num);
316 if (ec != std::errc() || ptr != name.end() || num <= 1 ||
317 num >= num_of_(parent)) {
318 req.reply_err(-ENOENT);
319 return;
320 }
321
322 bool dir = is_dir_(num);
323 uint64_t size =
324 dir ? 4096 : static_cast<uint64_t>(std::to_string(num).size() + 1);
325
326 fuse_entry_out out = {};
327 out.nodeid = num;
328 fill_attr_(out.attr, out.nodeid, dir, size);
329 req.reply_ok(&out, sizeof(out));
330 }
331
332 void do_getattr_(Request &req) {
333 uint64_t num = num_of_(req.nodeid());
334 bool dir = is_dir_(req.nodeid());
335 uint64_t size =
336 dir ? 4096 : static_cast<uint64_t>(std::to_string(num).size() + 1);
337
338 fuse_attr_out out = {};
339 fill_attr_(out.attr, req.nodeid(), dir, size);
340 req.reply_ok(&out, sizeof(out));
341 }
342
343 void do_open_(Request &req) {
344 fuse_open_out out = {};
345 req.reply_ok(&out, sizeof(out));
346 }
347
348 void do_read_(Request &req) {
349 uint64_t num = num_of_(req.nodeid());
350 if (!is_dir_(req.nodeid())) {
351 std::string content = std::to_string(num) + "\n";
352 auto *ri = req.op_in<fuse_read_in>();
353 size_t off = std::min<size_t>(ri->offset, content.size());
354 size_t n = std::min<size_t>(ri->size, content.size() - off);
355 req.reply_ok(content.data() + off, n);
356 } else {
357 req.reply_err(-EISDIR);
358 }
359 }
360
361 void do_readdir_(Request &req) {
362 auto *ri = req.op_in<fuse_read_in>();
363 size_t max_reply = std::min<size_t>(ri->size, MAX_PAYLOAD_SZ);
364 size_t used = 0;
365
366 uint64_t num = num_of_(req.nodeid());
367 if (!is_dir_(req.nodeid())) {
368 req.reply_err(-ENOTDIR);
369 return;
370 }
371
372 auto emit = [payload = req.payload, max_reply,
373 &used](uint64_t ino, uint64_t entry_off, uint32_t type,
374 std::string_view name) -> bool {
375 size_t namelen = name.size();
376 size_t reclen = FUSE_REC_ALIGN(FUSE_NAME_OFFSET + namelen);
377 auto *d = reinterpret_cast<fuse_dirent *>(
378 static_cast<char *>(payload) + used);
379 if (used + reclen > max_reply) {
380 return false;
381 }
382 d->ino = ino;
383 d->off = entry_off;
384 d->namelen = namelen;
385 d->type = type;
386 std::memcpy(d->name, name.data(), namelen);
387 used += reclen;
388 return true;
389 };
390
391 // Range: [2, num)
392 for (uint64_t i = ri->offset; i + 2 < num; i++) {
393 uint64_t n = i + 2;
394 std::string name = std::to_string(n);
395 bool ok = emit(n, i + 1, is_dir_(n) ? DT_DIR : DT_REG, name);
396 if (!ok) {
397 break;
398 }
399 }
400
401 req.reply_ok(nullptr, used);
402 }
403
404 void do_statfs_(Request &req) {
405 fuse_statfs_out out = {};
406 req.reply_ok(&out, sizeof(out));
407 }
408
409private:
410 void fill_attr_(fuse_attr &attr, uint64_t ino, bool dir, uint64_t size) {
411 constexpr uint32_t DIR_MODE =
412 S_IFDIR | S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
413 constexpr uint32_t FILE_MODE = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
414
415 attr = {};
416 attr.ino = ino;
417 attr.size = size;
418 attr.blocks = (size + 511) / 512;
419 attr.atime = attr.mtime = attr.ctime = now_;
420 attr.mode = dir ? DIR_MODE : FILE_MODE;
421 attr.nlink = dir ? 2 : 1;
422 attr.uid = uid_;
423 attr.gid = gid_;
424 attr.blksize = 4096;
425 }
426
427 static bool is_prime_(uint64_t n) {
428 if (n < 2) {
429 return false;
430 }
431 for (uint64_t i = 2; i * i <= n; i++) {
432 if (n % i == 0) {
433 return false;
434 }
435 }
436 return true;
437 }
438
439 uint64_t num_of_(uint64_t nodeid) const {
440 return nodeid == ROOT_NODEID ? number_ : nodeid;
441 }
442
443 bool is_dir_(uint64_t nodeid) const {
444 return nodeid == ROOT_NODEID || !is_prime_(num_of_(nodeid));
445 }
446
447private:
448 static constexpr uint64_t ROOT_NODEID = 1;
449
450 uint64_t number_;
451 time_t now_;
452 uid_t uid_;
453 gid_t gid_;
454};
455
456condy::Coro<void> io_loop(uint16_t qid, fuse_uring_req_header *hdr,
457 void *payload, FuseServer &server) {
458 int r;
459 iovec iov[] = {
460 {hdr, sizeof(*hdr)},
461 {payload, MAX_PAYLOAD_SZ},
462 };
463
464 r = co_await fuse_register_cmd(iov, qid);
465 if (r == -ENOTCONN) {
466 co_return;
467 } else if (r < 0) {
468 std::cerr << std::format("REGISTER failed: {}\n", strerror(-r));
469 std::exit(1);
470 }
471
472 while (true) {
473 uint64_t commit_id = hdr->ring_ent_in_out.commit_id;
474
475 co_await server.handle(hdr, payload);
476
477 r = co_await fuse_commit_and_fetch_cmd(qid, commit_id);
478 if (r == -ENOTCONN) {
479 co_return;
480 } else if (r < 0) {
481 std::cerr << std::format("COMMIT_AND_FETCH failed: {}\n",
482 strerror(-r));
483 std::exit(1);
484 }
485 }
486}
487
488condy::Coro<void> io_queue(int fuse_fd, uint16_t qid,
489 fuse_uring_req_header *queue_headers,
490 void *queue_payloads, FuseServer &server) {
491 auto &fd_table = condy::current_runtime().fd_table();
492 int r = fd_table.init(&fuse_fd, 1);
493 if (r < 0) {
494 std::cerr << std::format("fd_table.init failed: {}\n", r);
495 std::exit(1);
496 }
497
498 std::vector<condy::Task<void>> tasks;
499 tasks.reserve(queue_depth);
500 for (size_t i = 0; i < queue_depth; i++) {
501 tasks.push_back(condy::co_spawn(io_loop(
502 qid, queue_headers + i,
503 static_cast<char *>(queue_payloads) + i * MAX_PAYLOAD_SZ, server)));
504 }
505 for (auto &t : tasks) {
506 co_await t;
507 }
508
509 fd_table.destroy();
510}
511
512void on_signal(int) { umount2(mountpoint.c_str(), MNT_DETACH); }
513
514int main(int argc, char **argv) noexcept(false) {
515 int opt;
516 while ((opt = getopt(argc, argv, "n:q:mh")) != -1) {
517 switch (opt) {
518 case 'n':
519 number = std::stoull(optarg);
520 break;
521 case 'q':
522 queue_depth = std::stoull(optarg);
523 break;
524 case 'm':
525 multi_thread = true;
526 break;
527 case 'h':
528 default:
529 usage(argv[0]);
530 return opt == 'h' ? 0 : 1;
531 }
532 }
533
534 if (number < 2) {
535 std::cerr << "fuse-uring: -n must be >= 2\n";
536 usage(argv[0]);
537 return 1;
538 }
539 if (queue_depth == 0) {
540 std::cerr << "fuse-uring: -q must be >= 1\n";
541 usage(argv[0]);
542 return 1;
543 }
544
545 if (optind >= argc) {
546 usage(argv[0]);
547 return 1;
548 }
549 mountpoint = argv[optind];
550
551 int fuse_fd = open(FUSE_DEV, O_RDWR | O_CLOEXEC);
552 if (fuse_fd < 0) {
553 std::perror("open /dev/fuse");
554 return 1;
555 }
556
557 mount_fuse(fuse_fd, mountpoint);
558 init_fuse(fuse_fd);
559
560 size_t possible_cpus = get_nprocs_conf();
561
562 size_t headers_size =
563 possible_cpus * queue_depth * sizeof(fuse_uring_req_header);
564 void *addr = mmap(nullptr, headers_size, PROT_READ | PROT_WRITE,
565 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
566 if (addr == MAP_FAILED) {
567 std::perror("mmap");
568 return 1;
569 }
570 auto *headers_base = reinterpret_cast<fuse_uring_req_header *>(addr);
571
572 size_t payloads_size = possible_cpus * queue_depth * MAX_PAYLOAD_SZ;
573 void *payloads_base = mmap(nullptr, payloads_size, PROT_READ | PROT_WRITE,
574 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
575 if (payloads_base == MAP_FAILED) {
576 std::perror("mmap");
577 return 1;
578 }
579
580 struct sigaction sa = {};
581 sa.sa_handler = on_signal;
582 sigemptyset(&sa.sa_mask);
583 sigaction(SIGINT, &sa, nullptr);
584 sigaction(SIGTERM, &sa, nullptr);
585
586 condy::RuntimeOptions options;
587 options.enable_sqe128();
588
589 std::vector<std::unique_ptr<condy::Runtime>> runtimes;
590 if (multi_thread) {
591 options.sq_size(queue_depth);
592 for (size_t i = 0; i < possible_cpus; i++) {
593 runtimes.push_back(std::make_unique<condy::Runtime>(options));
594 options.enable_attach_wq(*runtimes[0]);
595 }
596 } else {
597 options.sq_size(possible_cpus * queue_depth);
598 runtimes.push_back(std::make_unique<condy::Runtime>(options));
599 }
600
601 FuseServer server(number);
602
603 std::vector<condy::Task<void>> queue_tasks;
604 queue_tasks.reserve(possible_cpus);
605 for (size_t qid = 0; qid < possible_cpus; qid++) {
606 auto *queue_headers = headers_base + qid * queue_depth;
607 auto *queue_payloads = static_cast<char *>(payloads_base) +
608 qid * queue_depth * MAX_PAYLOAD_SZ;
609 auto queue =
610 io_queue(fuse_fd, qid, queue_headers, queue_payloads, server);
611 auto &runtime = multi_thread ? *runtimes[qid] : *runtimes[0];
612 queue_tasks.push_back(condy::co_spawn(runtime, std::move(queue)));
613 }
614
615 if (multi_thread) {
616 std::vector<std::jthread> threads;
617 threads.reserve(possible_cpus);
618 for (size_t i = 0; i < possible_cpus; i++) {
619 threads.emplace_back([&runtime = *runtimes[i]]() {
620 runtime.allow_exit();
621 runtime.run();
622 });
623 }
624 } else {
625 runtimes[0]->allow_exit();
626 runtimes[0]->run();
627 }
628
629 for (auto &t : queue_tasks) {
630 t.wait();
631 }
632
633 munmap(headers_base, headers_size);
634 munmap(payloads_base, payloads_size);
635
636 close(fuse_fd);
637
638 return 0;
639}
Coroutine type used to define a coroutine function.
Definition coro.hpp:25
Main include file for the Condy library.
Task< T, Allocator > co_spawn(Runtime &runtime, Coro< T, Allocator > coro) noexcept
Spawn a coroutine as a task in the given runtime.
Definition task.hpp:101
auto fixed(int fd)
Mark a file descriptor as fixed for io_uring operations.
Definition helpers.hpp:70
auto & current_runtime() noexcept
Get the current runtime.
Definition runtime.hpp:452
auto async_uring_cmd(int cmd_op, Fd fd, CmdFunc &&cmd_func, Args &&...handler_args)
See io_uring_prep_uring_cmd.
Self & enable_sqe128()
See IORING_SETUP_SQE128.
Self & enable_attach_wq(Runtime &other)
See IORING_SETUP_ATTACH_WQ.
Self & sq_size(size_t v)
Set SQ size.
A simple CQE handler that extracts the result from the CQE without any additional processing.