cubelinux: cube(2) walks the store — CUBE_OP_ENUM and CUBE_OP_SPACES
Enumeration was the one operation the interface did not have, and the one a capability interface owes an explanation for: a listing is the opposite of "knowing a coordinate is the authorisation to use it". So the walk is bounded and explicit. A caller names the space, holds a cursor, and gets as many whole records as fit in the buffer it offered, packed `key(24) | value_len(u32) | value` in the store's own order. SPACES walks the distinct spaces that hold a record, one per call; range is that walk with the region as a filter, applied by the caller rather than by a second operation in the kernel. Its own argument block, versioned by its own size: SYSCALL_DEFINE2 peeks `size` and routes — 80 bytes is the coordinate block, 64 is this one. An interface that cannot grow has to be replaced, and this one grows by being given a new block. The end of a walk is the cursor alone. A batch holds as many whole records as fit, so it is FULL only when a record lands on the boundary and most batches come back short; a caller that reads "the buffer was not filled" as "the space is exhausted" truncates its listing to the first batch and cannot tell. ENUM answers a finished walk with no records and the cursor unmoved; SPACES answers it with -ENOENT, because the space after the last one is not there. That rule is in the uapi header because it is a contract detail, not an implementation one. The kernel caches no merged index: each call walks the records in order, asks the log — small, and the only thing that can override one — what the winner is, and skips what the cursor has already covered. O(records) a call, tens of milliseconds for the live store, which is worth more than a cache every write would have to invalidate.
This commit is contained in:
+143
-1
@@ -15,8 +15,10 @@
|
||||
* everything that touches the store's bytes is in the other.
|
||||
*/
|
||||
|
||||
#include <linux/init.h>
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/string.h>
|
||||
#include <linux/syscalls.h>
|
||||
#include <linux/uaccess.h>
|
||||
#include <linux/cube.h>
|
||||
@@ -33,11 +35,66 @@ ssize_t cubelinux_kernel_get(const __u8 *space, __u64 x, __u64 y, __u64 z,
|
||||
int cubelinux_kernel_del(const __u8 *space, __u64 x, __u64 y, __u64 z);
|
||||
int cubelinux_kernel_sync(void);
|
||||
|
||||
/*
|
||||
* The walk (CUBE_OP_ENUM / CUBE_OP_SPACES). Its argument block travels through the same syscall
|
||||
* but is a different struct, so these take the pieces rather than a pointer to one — and the C
|
||||
* side keeps owning everything that touches a userspace pointer.
|
||||
*/
|
||||
int cubelinux_kernel_enum(const __u8 *space, __u64 cursor, void *buf, size_t cap,
|
||||
__u64 *out_len, __u64 *out_cursor);
|
||||
int cubelinux_kernel_spaces(__u64 cursor, __u8 *space_out);
|
||||
|
||||
/* The store device path, resolved from the `cube_store=` boot parameter at boot. */
|
||||
const char *cubelinux_store_device(void);
|
||||
|
||||
/*
|
||||
* Where the store lives.
|
||||
*
|
||||
* A fixed `/dev/vda` was honest while a virtual machine was the only place this ran; the same
|
||||
* driver now has to read the box's own device as well, and a constant cannot be both. So it is
|
||||
* a boot parameter, and the box names its device on the kernel command line:
|
||||
*
|
||||
* cube_store=/dev/nvme0n1p2
|
||||
*
|
||||
* (`__setup` rather than `module_param_string`, deliberately. Built-in code registers module
|
||||
* parameters under its *object's* name — `MODULE_PARAM_PREFIX` is `KBUILD_MODNAME "."` when
|
||||
* MODULE is not defined — so a `module_param_string` here would answer to
|
||||
* `cube_syscall.store_device`, named after this file rather than after the driver. That is a
|
||||
* name nobody would guess and one more thing to get wrong at 3am. A `__setup` parameter is
|
||||
* named exactly as written.)
|
||||
*
|
||||
* This lives in C because this kernel's Rust can express only *integer* module parameters —
|
||||
* rust/kernel/module_param.rs implements `ModuleParam` through `ParseInt` and nothing else —
|
||||
* and a path is not an integer. The Rust side asks for the string; it does not store it.
|
||||
*
|
||||
* The default keeps the boot gate's shape, so the QEMU rehearsal is unchanged.
|
||||
*/
|
||||
static char store_device_path[256] = "/dev/vda";
|
||||
|
||||
static int __init cube_store_setup(char *str)
|
||||
{
|
||||
strscpy(store_device_path, str, sizeof(store_device_path));
|
||||
return 1;
|
||||
}
|
||||
__setup("cube_store=", cube_store_setup);
|
||||
|
||||
/* The Rust half reads the path through this; the storage stays here. */
|
||||
const char *cubelinux_store_device(void)
|
||||
{
|
||||
return store_device_path;
|
||||
}
|
||||
|
||||
/* No value may be larger than this in one call. A coordinate store is not a bulk-file path;
|
||||
* a caller with more than this to store has more than one record to store. */
|
||||
#define CUBE_MAX_VALUE (16u * 1024u * 1024u)
|
||||
|
||||
SYSCALL_DEFINE2(cube, unsigned int, op, void __user *, uargs)
|
||||
/* The walk's buffer ceiling: the caller offers whatever it likes, up to this. */
|
||||
#define CUBE_MAX_WALK (16u * 1024u * 1024u)
|
||||
|
||||
/*
|
||||
* The coordinate operations — put, get, del, sync — which travel in `struct cube_args`.
|
||||
*/
|
||||
static long cube_args_op(unsigned int op, void __user *uargs)
|
||||
{
|
||||
struct cube_args args;
|
||||
void *buf = NULL;
|
||||
@@ -129,3 +186,88 @@ SYSCALL_DEFINE2(cube, unsigned int, op, void __user *, uargs)
|
||||
kvfree(buf);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* The walk — enum and spaces — which travel in `struct cube_enum_args`: its own block so that the
|
||||
* coordinate is not both an input and an output, and so the interface can grow by getting a new
|
||||
* size rather than being replaced.
|
||||
*
|
||||
* A batch fills the caller's buffer and returns how much was used plus the cursor to pass next.
|
||||
* A record that does not fit ends the batch; a record that cannot fit in any buffer the caller
|
||||
* offered comes back as -ERANGE with `len` saying what it would need, exactly as a read does. So
|
||||
* nobody guesses a size and nobody gets half a record.
|
||||
*/
|
||||
static long cube_enum_op(unsigned int op, void __user *uargs)
|
||||
{
|
||||
struct cube_enum_args e;
|
||||
void *buf = NULL;
|
||||
long ret = 0;
|
||||
u64 out_len = 0, out_cursor = 0;
|
||||
|
||||
if (copy_from_user(&e, uargs, sizeof(e)))
|
||||
return -EFAULT;
|
||||
if (e.size != sizeof(struct cube_enum_args))
|
||||
return -EINVAL;
|
||||
|
||||
if (op == CUBE_OP_SPACES) {
|
||||
__u8 found[32];
|
||||
|
||||
ret = cubelinux_kernel_spaces(e.cursor, found);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
memcpy(e.space, found, sizeof(found));
|
||||
/* An index here, not a count of records: hand back the one after this space. */
|
||||
e.cursor = e.cursor + 1;
|
||||
e.len = 0;
|
||||
if (copy_to_user(uargs, &e, sizeof(e)))
|
||||
return -EFAULT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (op != CUBE_OP_ENUM)
|
||||
return -EINVAL;
|
||||
if (e.len > CUBE_MAX_WALK)
|
||||
return -E2BIG;
|
||||
if (e.len > 0) {
|
||||
buf = kvmalloc(e.len, GFP_KERNEL);
|
||||
if (!buf)
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
ret = cubelinux_kernel_enum(e.space, e.cursor, buf, e.len, &out_len, &out_cursor);
|
||||
if (ret == 0) {
|
||||
if (out_len > 0 && copy_to_user((void __user *)e.value, buf, out_len))
|
||||
ret = -EFAULT;
|
||||
e.len = out_len;
|
||||
e.cursor = out_cursor;
|
||||
if (copy_to_user(uargs, &e, sizeof(e)))
|
||||
ret = -EFAULT;
|
||||
} else if (ret == -ERANGE) {
|
||||
/* Nothing was written; `len` now says how much one record needs. */
|
||||
e.len = out_len;
|
||||
if (copy_to_user(uargs, &e, sizeof(e)))
|
||||
ret = -EFAULT;
|
||||
}
|
||||
|
||||
kvfree(buf);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* One syscall, two argument blocks. They share a prefix — `size`, then `op` — so the size the
|
||||
* caller declares is what says which one arrived. That is the whole point of putting `size`
|
||||
* first: an interface that cannot grow has to be replaced, and this one grows by being given a
|
||||
* new block with a new size.
|
||||
*/
|
||||
SYSCALL_DEFINE2(cube, unsigned int, op, void __user *, uargs)
|
||||
{
|
||||
__u32 size;
|
||||
|
||||
if (copy_from_user(&size, uargs, sizeof(size)))
|
||||
return -EFAULT;
|
||||
if (size == sizeof(struct cube_args))
|
||||
return cube_args_op(op, uargs);
|
||||
if (size == sizeof(struct cube_enum_args))
|
||||
return cube_enum_op(op, uargs);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user