26#include <linux/fuse.h>
30#include <sys/signalfd.h>
32#include <sys/sysinfo.h>
37size_t queue_depth = 8;
38bool multi_thread =
false;
39std::string mountpoint;
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;
50void usage(
const char *prog) {
51 std::cerr << std::format(
"Usage: {} [OPTIONS] <mountpoint>\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",
60void mount_fuse(
int fuse_fd,
const std::string &mountpoint) {
61 int fsfd = fsopen(
"fuse", 0);
63 std::perror(
"fsopen");
67 if (fsconfig(fsfd, FSCONFIG_SET_STRING,
"fd",
68 std::to_string(fuse_fd).c_str(), 0) < 0) {
69 std::perror(
"fsconfig fd");
72 if (fsconfig(fsfd, FSCONFIG_SET_STRING,
"source", FUSE_NAME, 0) < 0) {
73 std::perror(
"fsconfig source");
76 if (fsconfig(fsfd, FSCONFIG_SET_STRING,
"subtype", FUSE_NAME, 0) < 0) {
77 std::perror(
"fsconfig subtype");
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) <
84 std::perror(
"fsconfig rootmode");
87 if (fsconfig(fsfd, FSCONFIG_SET_STRING,
"user_id",
88 std::to_string(getuid()).c_str(), 0) < 0) {
89 std::perror(
"fsconfig user_id");
92 if (fsconfig(fsfd, FSCONFIG_SET_STRING,
"group_id",
93 std::to_string(getgid()).c_str(), 0) < 0) {
94 std::perror(
"fsconfig group_id");
97 if (fsconfig(fsfd, FSCONFIG_CMD_CREATE,
nullptr,
nullptr, 0) < 0) {
98 std::perror(
"fsconfig create");
102 int mfd = fsmount(fsfd, 0, 0);
104 std::perror(
"fsmount");
108 if (move_mount(mfd,
"", AT_FDCWD, mountpoint.c_str(),
109 MOVE_MOUNT_F_EMPTY_PATH) < 0) {
110 std::perror(
"move_mount");
128int check_init_request(
const FuseInitReq *req,
size_t n) {
129 if (n <
sizeof(FuseInitReq)) {
130 std::cerr <<
"FUSE_INIT request too small\n";
133 if (req->in.opcode != FUSE_INIT) {
134 std::cerr << std::format(
"Expected FUSE_INIT, got {}\n",
138 if (req->init.major != FUSE_KERNEL_VERSION) {
139 std::cerr << std::format(
"Unsupported major version {}\n",
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",
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);
157 ssize_t n = read(fuse_fd, buf,
sizeof(buf));
159 std::perror(
"read FUSE_INIT");
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");
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;
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);
182 resp->out.len =
sizeof(*resp);
184 resp->out.unique = req->in.unique;
187 n = write(fuse_fd, buf, resp->out.len);
189 std::perror(
"write FUSE_INIT reply");
194auto fuse_register_cmd(iovec iov[2], uint16_t qid) {
197 [iov, qid](io_uring_sqe *sqe) {
198 sqe->addr =
reinterpret_cast<uint64_t
>(iov);
200 auto *cmd =
reinterpret_cast<fuse_uring_cmd_req *
>(sqe->cmd);
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);
214 cmd->commit_id = commit_id;
221 FuseServer(uint64_t number)
222 : number_(number), now_(time(nullptr)), uid_(getuid()), gid_(getgid()) {
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};
250 case FUSE_RELEASEDIR:
259 req.reply_err(-ENOSYS);
267 fuse_uring_req_header *hdr;
270 uint64_t nodeid()
const {
return in()->nodeid; }
272 template <
typename T>
const T *op_in()
const {
273 return reinterpret_cast<const T *
>(hdr->op_in);
276 void reply_err(
int err) {
277 out()->len =
sizeof(*out());
279 out()->unique = in()->unique;
280 hdr->ring_ent_in_out.payload_sz = 0;
283 void reply_ok(
const void *data,
size_t size) {
285 assert(size <= MAX_PAYLOAD_SZ);
287 std::memcpy(payload, data, size);
289 out()->len =
sizeof(*out()) + size;
290 out()->unique = in()->unique;
291 hdr->ring_ent_in_out.payload_sz = size;
295 fuse_in_header *in()
const {
296 return reinterpret_cast<fuse_in_header *
>(hdr->in_out);
299 fuse_out_header *out()
const {
300 return reinterpret_cast<fuse_out_header *
>(hdr->in_out);
305 void do_lookup_(Request &req) {
306 uint64_t parent = req.nodeid();
307 std::string_view name =
static_cast<const char *
>(req.payload);
309 if (!is_dir_(parent)) {
310 req.reply_err(-ENOTDIR);
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);
322 bool dir = is_dir_(num);
324 dir ? 4096 :
static_cast<uint64_t
>(std::to_string(num).size() + 1);
326 fuse_entry_out out = {};
328 fill_attr_(out.attr, out.nodeid, dir, size);
329 req.reply_ok(&out,
sizeof(out));
332 void do_getattr_(Request &req) {
333 uint64_t num = num_of_(req.nodeid());
334 bool dir = is_dir_(req.nodeid());
336 dir ? 4096 :
static_cast<uint64_t
>(std::to_string(num).size() + 1);
338 fuse_attr_out out = {};
339 fill_attr_(out.attr, req.nodeid(), dir, size);
340 req.reply_ok(&out,
sizeof(out));
343 void do_open_(Request &req) {
344 fuse_open_out out = {};
345 req.reply_ok(&out,
sizeof(out));
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);
357 req.reply_err(-EISDIR);
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);
366 uint64_t num = num_of_(req.nodeid());
367 if (!is_dir_(req.nodeid())) {
368 req.reply_err(-ENOTDIR);
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) {
384 d->namelen = namelen;
386 std::memcpy(d->name, name.data(), namelen);
392 for (uint64_t i = ri->offset; i + 2 < num; i++) {
394 std::string name = std::to_string(n);
395 bool ok = emit(n, i + 1, is_dir_(n) ? DT_DIR : DT_REG, name);
401 req.reply_ok(
nullptr, used);
404 void do_statfs_(Request &req) {
405 fuse_statfs_out out = {};
406 req.reply_ok(&out,
sizeof(out));
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;
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;
427 static bool is_prime_(uint64_t n) {
431 for (uint64_t i = 2; i * i <= n; i++) {
439 uint64_t num_of_(uint64_t nodeid)
const {
440 return nodeid == ROOT_NODEID ? number_ : nodeid;
443 bool is_dir_(uint64_t nodeid)
const {
444 return nodeid == ROOT_NODEID || !is_prime_(num_of_(nodeid));
448 static constexpr uint64_t ROOT_NODEID = 1;
457 void *payload, FuseServer &server) {
461 {payload, MAX_PAYLOAD_SZ},
464 r =
co_await fuse_register_cmd(iov, qid);
465 if (r == -ENOTCONN) {
468 std::cerr << std::format(
"REGISTER failed: {}\n", strerror(-r));
473 uint64_t commit_id = hdr->ring_ent_in_out.commit_id;
475 co_await server.handle(hdr, payload);
477 r =
co_await fuse_commit_and_fetch_cmd(qid, commit_id);
478 if (r == -ENOTCONN) {
481 std::cerr << std::format(
"COMMIT_AND_FETCH failed: {}\n",
489 fuse_uring_req_header *queue_headers,
490 void *queue_payloads, FuseServer &server) {
492 int r = fd_table.init(&fuse_fd, 1);
494 std::cerr << std::format(
"fd_table.init failed: {}\n", r);
498 std::vector<condy::Task<void>> tasks;
499 tasks.reserve(queue_depth);
500 for (
size_t i = 0; i < queue_depth; i++) {
502 qid, queue_headers + i,
503 static_cast<char *
>(queue_payloads) + i * MAX_PAYLOAD_SZ, server)));
505 for (
auto &t : tasks) {
512void on_signal(
int) { umount2(mountpoint.c_str(), MNT_DETACH); }
514int main(
int argc,
char **argv)
noexcept(
false) {
516 while ((opt = getopt(argc, argv,
"n:q:mh")) != -1) {
519 number = std::stoull(optarg);
522 queue_depth = std::stoull(optarg);
530 return opt ==
'h' ? 0 : 1;
535 std::cerr <<
"fuse-uring: -n must be >= 2\n";
539 if (queue_depth == 0) {
540 std::cerr <<
"fuse-uring: -q must be >= 1\n";
545 if (optind >= argc) {
549 mountpoint = argv[optind];
551 int fuse_fd = open(FUSE_DEV, O_RDWR | O_CLOEXEC);
553 std::perror(
"open /dev/fuse");
557 mount_fuse(fuse_fd, mountpoint);
560 size_t possible_cpus = get_nprocs_conf();
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) {
570 auto *headers_base =
reinterpret_cast<fuse_uring_req_header *
>(addr);
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) {
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);
589 std::vector<std::unique_ptr<condy::Runtime>> runtimes;
592 for (
size_t i = 0; i < possible_cpus; i++) {
593 runtimes.push_back(std::make_unique<condy::Runtime>(options));
597 options.
sq_size(possible_cpus * queue_depth);
598 runtimes.push_back(std::make_unique<condy::Runtime>(options));
601 FuseServer server(number);
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;
610 io_queue(fuse_fd, qid, queue_headers, queue_payloads, server);
611 auto &runtime = multi_thread ? *runtimes[qid] : *runtimes[0];
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();
625 runtimes[0]->allow_exit();
629 for (
auto &t : queue_tasks) {
633 munmap(headers_base, headers_size);
634 munmap(payloads_base, payloads_size);
Coroutine type used to define a coroutine function.
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.
auto fixed(int fd)
Mark a file descriptor as fixed for io_uring operations.
auto & current_runtime() noexcept
Get the current runtime.
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.