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;
|
||||
}
|
||||
|
||||
+241
-13
@@ -75,13 +75,44 @@ const SPACE_ID_LEN: usize = 32;
|
||||
const RAW_KEY_LEN: usize = 24;
|
||||
const RECORD_FIXED: usize = SPACE_ID_LEN + RAW_KEY_LEN + 8;
|
||||
|
||||
/// The device to read. A module parameter is the obvious next step; for the boot gate one
|
||||
/// fixed name is honest and has one less way to be wrong.
|
||||
const STORE_DEVICE: &core::ffi::CStr = c_str!("/dev/vda");
|
||||
// The device the store lives on, as the `cube_store=` boot parameter resolved it.
|
||||
//
|
||||
// This was a constant — *"for the boot gate one fixed name is honest and has one less way to
|
||||
// be wrong"* — and that was true while a virtual machine was the only place this driver ran.
|
||||
// It runs on the box now, the box's device is not `/dev/vda`, and a constant cannot be both.
|
||||
// So the box names its device on the kernel command line:
|
||||
//
|
||||
// cube_store=/dev/nvme0n1p2
|
||||
//
|
||||
// The parameter is declared in C (`cube_syscall.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. It is a `__setup` parameter rather
|
||||
// than a module parameter for the naming reason recorded in that file.
|
||||
//
|
||||
// (A `///` here would be an unused doc comment: it would attach to the `extern` block, which
|
||||
// documents nothing.)
|
||||
extern "C" {
|
||||
/// Returns a pointer to a static, NUL-terminated buffer holding the path.
|
||||
fn cubelinux_store_device() -> *const core::ffi::c_char;
|
||||
}
|
||||
|
||||
/// Refuse to pull an unbounded device into memory. The gate's images are tiny; this is the
|
||||
/// guard against a wrong device name turning a read into an allocation storm.
|
||||
const MAX_BYTES: usize = 64 * 1024 * 1024;
|
||||
/// The store's path, NUL-terminated, owned by the C side for the life of the kernel.
|
||||
fn store_device() -> *const u8 {
|
||||
// SAFETY: `cubelinux_store_device` returns a pointer to a static buffer that the module
|
||||
// parameter filled during boot and that nothing writes afterwards, so it stays valid and
|
||||
// NUL-terminated for as long as this runs.
|
||||
unsafe { cubelinux_store_device().cast::<u8>() }
|
||||
}
|
||||
|
||||
/// Refuse to pull an unbounded device into memory: the guard against a wrong device name
|
||||
/// turning a read into an allocation storm. It is sized for a *store device*, not for an image.
|
||||
///
|
||||
/// `Control::layout` gives each of the two slots a quarter of the device, so a device must be at
|
||||
/// least four times the image it holds — and a *sealed* image is larger than the plaintext one,
|
||||
/// because every record gains an envelope. A sealed 18.8 MiB store therefore needs a ~76 MiB
|
||||
/// device, which the original 64 MiB cap refused. 128 MiB leaves room for both to grow while
|
||||
/// still being a bound: pointing this at a whole 119 GiB disk is still refused.
|
||||
const MAX_BYTES: usize = 128 * 1024 * 1024;
|
||||
|
||||
/// The log's own magic, distinct from the image's so a reader that opens the wrong one
|
||||
/// cannot mistake it for the other.
|
||||
@@ -170,9 +201,10 @@ fn fnv1a64(bytes: &[u8], mut h: u64) -> u64 {
|
||||
/// filesystem, say), that is the moment to reach for the block layer directly.
|
||||
fn read_image(image: &mut KVVec<u8>) -> Result<()> {
|
||||
// O_RDONLY is 0 in Linux; filp_open takes the raw flags word.
|
||||
// SAFETY: STORE_DEVICE is a NUL-terminated C string literal, and filp_open either
|
||||
// returns a valid `struct file *` or an error pointer, which is checked below.
|
||||
let file = unsafe { bindings::filp_open(STORE_DEVICE.as_ptr().cast::<u8>(), 0, 0) };
|
||||
// SAFETY: store_device() is a NUL-terminated C string that the module parameter filled at
|
||||
// boot, and filp_open either returns a valid `struct file *` or an error pointer, which is
|
||||
// checked below.
|
||||
let file = unsafe { bindings::filp_open(store_device(), 0, 0) };
|
||||
let file = kernel::error::from_err_ptr(file)?;
|
||||
if file.is_null() {
|
||||
return Err(EINVAL);
|
||||
@@ -1017,9 +1049,9 @@ fn append(
|
||||
return Err(ENOSPC);
|
||||
}
|
||||
|
||||
// SAFETY: STORE_DEVICE is a NUL-terminated literal; filp_open returns a valid file or an
|
||||
// error pointer, which is checked. O_RDWR is 2.
|
||||
let file = unsafe { bindings::filp_open(STORE_DEVICE.as_ptr().cast::<u8>(), 2, 0) };
|
||||
// SAFETY: store_device() is a NUL-terminated C string filled at boot; filp_open returns a
|
||||
// valid file or an error pointer, which is checked. O_RDWR is 2.
|
||||
let file = unsafe { bindings::filp_open(store_device(), 2, 0) };
|
||||
let file = kernel::error::from_err_ptr(file)?;
|
||||
if file.is_null() {
|
||||
return Err(EINVAL);
|
||||
@@ -1134,7 +1166,7 @@ fn checkpoint(ctl: &Control, merged: &mut Merged) -> Result<u64, Error> {
|
||||
}
|
||||
|
||||
// SAFETY: as in `append`.
|
||||
let file = unsafe { bindings::filp_open(STORE_DEVICE.as_ptr().cast::<u8>(), 2, 0) };
|
||||
let file = unsafe { bindings::filp_open(store_device(), 2, 0) };
|
||||
let file = kernel::error::from_err_ptr(file)?;
|
||||
if file.is_null() {
|
||||
return Err(EINVAL);
|
||||
@@ -1295,6 +1327,202 @@ pub unsafe extern "C" fn cubelinux_kernel_get(
|
||||
}
|
||||
}
|
||||
|
||||
/// Pack one record the way the walk's uapi names it: `key(24) | value_len(u32, LE) | value`.
|
||||
///
|
||||
/// The space is not in the frame because the caller named it, and the order is the store's own
|
||||
/// (space, then key), so a listing taken through the kernel and one taken in userspace are
|
||||
/// byte-for-byte comparable — which is how this is tested.
|
||||
fn pack_record(key: &[u8], value: &[u8], out: &mut [u8], at: usize) -> Option<usize> {
|
||||
let need = RAW_KEY_LEN + 4 + value.len();
|
||||
if at + need > out.len() {
|
||||
return None;
|
||||
}
|
||||
out[at..at + RAW_KEY_LEN].copy_from_slice(&key[..RAW_KEY_LEN]);
|
||||
out[at + RAW_KEY_LEN..at + RAW_KEY_LEN + 4]
|
||||
.copy_from_slice(&(value.len() as u32).to_le_bytes());
|
||||
out[at + RAW_KEY_LEN + 4..at + need].copy_from_slice(value);
|
||||
Some(need)
|
||||
}
|
||||
|
||||
/// `CUBE_OP_ENUM`: walk the records of one space into the caller's buffer, `cursor` records in.
|
||||
///
|
||||
/// The cursor is a COUNT OF RECORDS ALREADY RETURNED, not a position in the image. It is opaque to
|
||||
/// the caller (pass back what you were given) and it survives an append, because the contract is
|
||||
/// "the records I had not yet seen" rather than a snapshot. A record can be seen twice if the
|
||||
/// image is rewritten underneath a walk; a caller that needs a snapshot takes one.
|
||||
///
|
||||
/// The walk is O(records) per call, deliberately. The alternative is a merged index held in the
|
||||
/// kernel and invalidated by every write — more to be wrong about than a listing is worth, and the
|
||||
/// live store's 69,636 records make a page of listing tens of milliseconds.
|
||||
///
|
||||
/// `out_len` receives the bytes written, or with -ERANGE the size the first record that did not fit
|
||||
/// would need — exactly as a read reports the size it wants.
|
||||
///
|
||||
/// # Safety
|
||||
/// `space` must point to 32 readable bytes; `buf` must hold `cap` writable bytes; `out_len` and
|
||||
/// `out_cursor` must each point to a writable `u64`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn cubelinux_kernel_enum(
|
||||
space: *const u8,
|
||||
cursor: u64,
|
||||
buf: *mut u8,
|
||||
cap: usize,
|
||||
out_len: *mut u64,
|
||||
out_cursor: *mut u64,
|
||||
) -> i32 {
|
||||
let mut wanted = [0u8; SPACE_ID_LEN];
|
||||
// SAFETY: the caller guarantees 32 readable bytes at `space`.
|
||||
unsafe { core::ptr::copy_nonoverlapping(space, wanted.as_mut_ptr(), SPACE_ID_LEN) };
|
||||
|
||||
let (device, layout) = match device_and_layout() {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => return -(e.to_errno() as i32),
|
||||
};
|
||||
let live = &device[layout.image_off..];
|
||||
let header = match parse_header(live) {
|
||||
Ok(h) => h,
|
||||
Err(what) => {
|
||||
pr_err!("cubelinux: {}\n", what);
|
||||
return -22; // -EINVAL
|
||||
}
|
||||
};
|
||||
let window = log_window(&device, &layout);
|
||||
let end = core::cmp::min(
|
||||
header.image_bytes.unwrap_or(live.len() as u64) as usize,
|
||||
live.len(),
|
||||
);
|
||||
let (mut merged, _applied) = match build_merged(&live[..end], window, &header) {
|
||||
Ok(m) => m,
|
||||
Err(what) => {
|
||||
pr_err!("cubelinux: cannot build the store: {}\n", what);
|
||||
return -22;
|
||||
}
|
||||
};
|
||||
merged.sort_entries();
|
||||
let entries = merged.entries.as_slice();
|
||||
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
|
||||
let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) };
|
||||
|
||||
let mut written = 0usize;
|
||||
let mut returned: u64 = 0;
|
||||
let mut seen: u64 = 0;
|
||||
let mut first_too_big: usize = 0;
|
||||
|
||||
let mut i = 0usize;
|
||||
while i < entries.len() {
|
||||
let head = &entries[i];
|
||||
// The winner of a coordinate is the LAST of its group: the newest write, or a removal.
|
||||
let mut last = i;
|
||||
while last + 1 < entries.len()
|
||||
&& entries[last + 1].space == head.space
|
||||
&& entries[last + 1].key == head.key
|
||||
{
|
||||
last += 1;
|
||||
}
|
||||
let winner = &entries[last];
|
||||
i = last + 1;
|
||||
if winner.deleted || winner.space != wanted {
|
||||
continue;
|
||||
}
|
||||
seen += 1;
|
||||
if seen <= cursor {
|
||||
continue;
|
||||
}
|
||||
match pack_record(&winner.key, merged.value(winner), out, written) {
|
||||
Some(n) => {
|
||||
written += n;
|
||||
returned += 1;
|
||||
}
|
||||
None => {
|
||||
first_too_big = RAW_KEY_LEN + 4 + winner.value_len as usize;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: both out-pointers are writable under this function's contract.
|
||||
unsafe {
|
||||
if first_too_big > 0 && written == 0 {
|
||||
// Not one whole record fits. Say how much it needs, the way a read does.
|
||||
*out_len = first_too_big as u64;
|
||||
*out_cursor = cursor;
|
||||
return -34; // -ERANGE
|
||||
}
|
||||
*out_len = written as u64;
|
||||
*out_cursor = cursor + returned;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// `CUBE_OP_SPACES`: the `cursor`-th distinct space that holds a record, or -ENOENT at the end.
|
||||
///
|
||||
/// An index rather than a count of records: a caller that wants a space's records walks it with
|
||||
/// `CUBE_OP_ENUM` once it has learned the name. Entries are already in (space, key) order, so the
|
||||
/// distinct spaces come out sorted.
|
||||
///
|
||||
/// # Safety
|
||||
/// `space_out` must point to 32 writable bytes.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn cubelinux_kernel_spaces(cursor: u64, space_out: *mut u8) -> i32 {
|
||||
let (device, layout) = match device_and_layout() {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => return -(e.to_errno() as i32),
|
||||
};
|
||||
let live = &device[layout.image_off..];
|
||||
let header = match parse_header(live) {
|
||||
Ok(h) => h,
|
||||
Err(what) => {
|
||||
pr_err!("cubelinux: {}\n", what);
|
||||
return -22;
|
||||
}
|
||||
};
|
||||
let window = log_window(&device, &layout);
|
||||
let end = core::cmp::min(
|
||||
header.image_bytes.unwrap_or(live.len() as u64) as usize,
|
||||
live.len(),
|
||||
);
|
||||
let (mut merged, _applied) = match build_merged(&live[..end], window, &header) {
|
||||
Ok(m) => m,
|
||||
Err(what) => {
|
||||
pr_err!("cubelinux: cannot build the store: {}\n", what);
|
||||
return -22;
|
||||
}
|
||||
};
|
||||
merged.sort_entries();
|
||||
let entries = merged.entries.as_slice();
|
||||
|
||||
let mut index: u64 = 0;
|
||||
let mut previous: Option<[u8; SPACE_ID_LEN]> = None;
|
||||
let mut i = 0usize;
|
||||
while i < entries.len() {
|
||||
let head = &entries[i];
|
||||
let mut last = i;
|
||||
while last + 1 < entries.len()
|
||||
&& entries[last + 1].space == head.space
|
||||
&& entries[last + 1].key == head.key
|
||||
{
|
||||
last += 1;
|
||||
}
|
||||
let winner = &entries[last];
|
||||
i = last + 1;
|
||||
if winner.deleted {
|
||||
continue;
|
||||
}
|
||||
if previous != Some(winner.space) {
|
||||
if index == cursor {
|
||||
// SAFETY: the caller guarantees 32 writable bytes at `space_out`.
|
||||
unsafe {
|
||||
core::ptr::copy_nonoverlapping(winner.space.as_ptr(), space_out, SPACE_ID_LEN);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
index += 1;
|
||||
previous = Some(winner.space);
|
||||
}
|
||||
}
|
||||
-2 // -ENOENT: no such space; the walk is finished
|
||||
}
|
||||
|
||||
/// `CUBE_OP_DEL`: remove the record at a coordinate.
|
||||
///
|
||||
/// A removal is a log entry, not an erasure: nothing in an append-only store is rewritten in
|
||||
|
||||
@@ -51,13 +51,20 @@ struct cube_args {
|
||||
* order — space first, then key — which is the order a checkpoint writes them and the order the
|
||||
* userspace store returns them, so a kernel listing and a userspace listing can be compared
|
||||
* directly. The space is not repeated per record: the caller named it.
|
||||
*
|
||||
* A walk ends when the cursor stops moving, and that is the only end signal: a batch holds as
|
||||
* many whole records as fit, so most batches come back short, and reading a short batch as the
|
||||
* end truncates a listing to its first batch. CUBE_OP_ENUM answers a finished walk with no
|
||||
* records and the cursor unchanged; CUBE_OP_SPACES answers it with -ENOENT, because asking for
|
||||
* the space after the last one is asking for a space that is not there. -ERANGE keeps its usual
|
||||
* meaning: not one whole record fits, and `len` says how much one needs.
|
||||
*/
|
||||
struct cube_enum_args {
|
||||
__u32 size; /* sizeof(struct cube_enum_args) as the caller built it */
|
||||
__u32 op; /* CUBE_OP_ENUM or CUBE_OP_SPACES */
|
||||
__u8 space[32]; /* in: the space to walk; out: the space found (CUBE_OP_SPACES) */
|
||||
__u64 cursor; /* in: 0 to start, or what the last call returned;
|
||||
* out: what to pass next
|
||||
* out: what to pass next — see the end-of-walk rule above
|
||||
*/
|
||||
__u64 value; /* user pointer: where to put the records */
|
||||
__u64 len; /* in: the buffer's capacity;
|
||||
|
||||
Reference in New Issue
Block a user