cubelinux: CUBE_OP_RANGE — a region walk that seeks on the box's key span
The seventh operation: `cube(2)` gains CUBE_OP_RANGE, a bounded walk of one space's records that lie in a box. Its argument block is its own (`cube_range_args`, versioned by `size` like the walk's), and the box travels as its six numbers for the reason a coordinate does — the key it has to become is the driver's business. The operation rests on the span that the shared file already owns. `key_span(lo, hi)` bounds every key in the box because the interleave is monotone on each axis, so a v3 image is **sought**: the fixed-stride index is binary-searched for the span's foot (`Addressed::lower_bound`) and read forward to its head, merging the log's edits exactly as a walk does. A packed v1/v2 image has no index to search, so its space is walked with the same span used only to stop early — and the contract is the same either way, so a caller is not told which path it got. The trap that shaped the code, and the reason it is written the way it is: **the span is a bound, not the set.** Keys of points outside the box fall inside it (Z-order amplification), so every candidate is decoded and tested against the box before it is returned — which is what `morton_decode`, the interleave's inverse, is for, now in the shared file with the same kind of hand-pinned tests the interleave has. And the cursor counts the records *in the box*, not the records of the space, because those are the records the walk returns. The over-coverage — records examined versus records returned — is the number this operation is meant to publish, and it is not wired to the caller yet: the cost gate measures it by comparing against the userspace store, which counts the same thing.
This commit is contained in:
@@ -468,9 +468,29 @@ pub fn morton_key(x: u64, y: u64, z: u64) -> [u8; RAW_KEY_LEN] {
|
||||
k
|
||||
}
|
||||
|
||||
/// The point a key encodes — the inverse of [`morton_key`].
|
||||
///
|
||||
/// Bit `n` of the key is bit `n / 3` of axis `n % 3`, which is the interleave read backwards.
|
||||
///
|
||||
/// Why the kernel needs it: a region walk is handed a **key span**, not a set of keys. A span is a
|
||||
/// bound, not the set — keys of points *outside* the box also fall inside it (Z-order
|
||||
/// amplification) — so every candidate found in the span has to be turned back into a point and
|
||||
/// tested for membership. Without this, the walk would return records that are outside the region
|
||||
/// the caller asked for, which is exactly the mistake the span's own doc warns about.
|
||||
pub fn morton_decode(k: &[u8; RAW_KEY_LEN]) -> [u64; 3] {
|
||||
let mut out = [0u64; 3];
|
||||
let mut n = 0;
|
||||
while n < RAW_KEY_LEN * 8 {
|
||||
if k[RAW_KEY_LEN - 1 - (n / 8)] & (1 << (n % 8)) != 0 {
|
||||
out[n % 3] |= 1u64 << (n / 3);
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Compare two keys as the 192-bit numbers they are. `a < b` means `a` sorts first.
|
||||
pub fn key_cmp(a: &[u8; RAW_KEY_LEN], b: &[u8; RAW_KEY_LEN]) -> core::cmp::Ordering {
|
||||
let mut i = 0;
|
||||
pub fn key_cmp(a: &[u8; RAW_KEY_LEN], b: &[u8; RAW_KEY_LEN]) -> core::cmp::Ordering { let mut i = 0;
|
||||
while i < RAW_KEY_LEN {
|
||||
if a[i] != b[i] {
|
||||
return if a[i] < b[i] {
|
||||
|
||||
@@ -45,6 +45,17 @@ 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 region walk (CUBE_OP_RANGE). The box travels as its six numbers for the same reason the
|
||||
* coordinate does: the format knowledge stays on the Rust side, which owns the key the box has to
|
||||
* become.
|
||||
*/
|
||||
int cubelinux_kernel_range(const __u8 *space,
|
||||
__u64 lo_x, __u64 lo_y, __u64 lo_z,
|
||||
__u64 hi_x, __u64 hi_y, __u64 hi_z,
|
||||
__u64 cursor, void *buf, size_t cap,
|
||||
__u64 *out_len, __u64 *out_cursor);
|
||||
|
||||
/* The store device path, resolved from the `cube_store=` boot parameter at boot. */
|
||||
const char *cubelinux_store_device(void);
|
||||
|
||||
@@ -283,7 +294,74 @@ static long cube_enum_op(unsigned int op, void __user *uargs)
|
||||
}
|
||||
|
||||
/*
|
||||
* One syscall, two argument blocks. They share a prefix — `size`, then `op` — so the size the
|
||||
* The region walk — CUBE_OP_RANGE — which travels in `struct cube_range_args`.
|
||||
*
|
||||
* Deliberately the same shape as the space walk above, because it is the same contract with one
|
||||
* more input: 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. Nobody guesses a size
|
||||
* and nobody gets half a record.
|
||||
*
|
||||
* The two things a caller must know beyond the walk's rules: the cursor counts the records **in the
|
||||
* box** rather than the records of the space (those are the records being returned), and the kernel
|
||||
* may *examine* more records than it returns, because it seeks on the box's key span and the span
|
||||
* is a bound rather than the set. That over-coverage is the honest cost of the seek, not a defect.
|
||||
*/
|
||||
static long cube_range_op(void __user *uargs)
|
||||
{
|
||||
struct cube_range_args r;
|
||||
void *buf = NULL;
|
||||
long ret = 0;
|
||||
u64 out_len = 0, out_cursor = 0;
|
||||
|
||||
if (copy_from_user(&r, uargs, sizeof(r)))
|
||||
return -EFAULT;
|
||||
if (r.size != sizeof(struct cube_range_args) || r.op != CUBE_OP_RANGE)
|
||||
return -EINVAL;
|
||||
|
||||
/*
|
||||
* An inverted box is empty, not an error: there is nothing in it, and saying so is the honest
|
||||
* answer. Answering it here also keeps the empty case away from the seek, where an inverted
|
||||
* span would be a range whose start is above its end.
|
||||
*/
|
||||
if (r.lo[0] > r.hi[0] || r.lo[1] > r.hi[1] || r.lo[2] > r.hi[2]) {
|
||||
r.len = 0;
|
||||
if (copy_to_user(uargs, &r, sizeof(r)))
|
||||
return -EFAULT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (r.len > CUBE_MAX_WALK)
|
||||
return -E2BIG;
|
||||
if (r.len > 0) {
|
||||
buf = kvmalloc(r.len, GFP_KERNEL);
|
||||
if (!buf)
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
ret = cubelinux_kernel_range(r.space, r.lo[0], r.lo[1], r.lo[2],
|
||||
r.hi[0], r.hi[1], r.hi[2],
|
||||
r.cursor, buf, r.len, &out_len, &out_cursor);
|
||||
if (ret == 0) {
|
||||
if (out_len > 0 && copy_to_user((void __user *)r.value, buf, out_len))
|
||||
ret = -EFAULT;
|
||||
r.len = out_len;
|
||||
r.cursor = out_cursor;
|
||||
if (copy_to_user(uargs, &r, sizeof(r)))
|
||||
ret = -EFAULT;
|
||||
} else if (ret == -ERANGE) {
|
||||
/* Nothing was written; `len` now says how much one record needs. */
|
||||
r.len = out_len;
|
||||
if (copy_to_user(uargs, &r, sizeof(r)))
|
||||
ret = -EFAULT;
|
||||
}
|
||||
|
||||
kvfree(buf);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* One syscall, three 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.
|
||||
@@ -298,5 +376,7 @@ SYSCALL_DEFINE2(cube, unsigned int, op, void __user *, uargs)
|
||||
return cube_args_op(op, uargs);
|
||||
if (size == sizeof(struct cube_enum_args))
|
||||
return cube_enum_op(op, uargs);
|
||||
if (size == sizeof(struct cube_range_args))
|
||||
return cube_range_op(uargs);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
@@ -2298,6 +2298,81 @@ impl Batch<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── The region walk: a box, its key span, and what decides membership ────────────────────
|
||||
|
||||
/// The inclusive box a region walk answers, and the key span that bounds it.
|
||||
///
|
||||
/// The span is the whole mechanism of `CUBE_OP_RANGE`: [`cube_format::key_span`] gives the two
|
||||
/// corner keys, and **every** key in the box lies between them because the interleave is monotone on
|
||||
/// each axis. So a sorted index can be binary-searched for the foot and read forward to the head.
|
||||
///
|
||||
/// It is a **bound and not the set**: a point *outside* the box can have a key inside the span (the
|
||||
/// classic Z-order amplification), so [`holds`](Self::holds) is what decides and a caller must ask
|
||||
/// it about every candidate. Confusing the bound for the set is the mistake that would make this
|
||||
/// return records from outside the region the caller named — and there is no decomposition here to
|
||||
/// blame it on, which is why the pair is checked in `cube-format`'s tests.
|
||||
struct Region {
|
||||
lo: [u64; 3],
|
||||
hi: [u64; 3],
|
||||
foot: [u8; RAW_KEY_LEN],
|
||||
head: [u8; RAW_KEY_LEN],
|
||||
}
|
||||
|
||||
impl Region {
|
||||
/// `None` for an empty box — inverted on some axis. An empty box has no span worth building,
|
||||
/// and building one would produce a range whose start is above its end.
|
||||
fn new(lo: [u64; 3], hi: [u64; 3]) -> Option<Self> {
|
||||
let mut i = 0;
|
||||
while i < 3 {
|
||||
if lo[i] > hi[i] {
|
||||
return None;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
let (foot, head) = cube_format::key_span(lo, hi);
|
||||
Some(Region { lo, hi, foot, head })
|
||||
}
|
||||
|
||||
/// Whether a key's point lies in the box — the test that keeps a bound from becoming a wrong
|
||||
/// answer. This is why `morton_decode` exists.
|
||||
fn holds(&self, key: &[u8; RAW_KEY_LEN]) -> bool {
|
||||
let p = cube_format::morton_decode(key);
|
||||
p[0] >= self.lo[0]
|
||||
&& p[0] <= self.hi[0]
|
||||
&& p[1] >= self.lo[1]
|
||||
&& p[1] <= self.hi[1]
|
||||
&& p[2] >= self.lo[2]
|
||||
&& p[2] <= self.hi[2]
|
||||
}
|
||||
|
||||
/// Whether a key sorts below the span, inside it, or above it.
|
||||
///
|
||||
/// A dedicated three-way rather than an `Ordering`, because `Ordering` has no room for it: a key
|
||||
/// between the foot and the head is `Less` *than the head* and that is exactly the case a walk
|
||||
/// must keep. Returning `key_cmp(key, head)` and matching on it conflates "inside the span" with
|
||||
/// "below the foot", which silently skips every record in the region the caller asked for —
|
||||
/// a mistake this code made for one edit, caught before it built.
|
||||
fn place(&self, key: &[u8; RAW_KEY_LEN]) -> Span {
|
||||
if cube_format::key_cmp(key, &self.foot) == core::cmp::Ordering::Less {
|
||||
Span::Below
|
||||
} else if cube_format::key_cmp(key, &self.head) == core::cmp::Ordering::Greater {
|
||||
Span::Above
|
||||
} else {
|
||||
Span::Inside
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a key sits relative to a region's key span. See [`Region::place`].
|
||||
enum Span {
|
||||
/// Sorts before the span's foot: not in the box, and skippable on a layout with no index.
|
||||
Below,
|
||||
/// Sorts within the span: a candidate, which still has to pass [`Region::holds`].
|
||||
Inside,
|
||||
/// Sorts past the span's head: the walk is finished, because the records are key-ordered.
|
||||
Above,
|
||||
}
|
||||
|
||||
// ── v3: the addressed reader ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// A v3 image is read at the offsets the header describes, so an operation reads the bytes it names
|
||||
@@ -2437,9 +2512,37 @@ impl Addressed {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// The first index position at or **after** `key`, within a space's index range.
|
||||
///
|
||||
/// This is `find` answering the other question. `find` asks "where is this exact key" and is only
|
||||
/// useful for a coordinate read; a region walk asks "where does this span begin", which is the
|
||||
/// same binary search over the same arithmetic addresses with `>=` in place of `==`. The walk
|
||||
/// then reads forward from here and stops when it passes the span's head, so the cost is the
|
||||
/// span rather than the space — and it is the reason the span needed no decomposition.
|
||||
fn lower_bound(
|
||||
&mut self,
|
||||
key: &[u8; RAW_KEY_LEN],
|
||||
first: u64,
|
||||
records: u64,
|
||||
) -> Result<u64> {
|
||||
let mut buf = KVVec::<u8>::new();
|
||||
let mut lo = 0u64;
|
||||
let mut hi = records;
|
||||
while lo < hi {
|
||||
let mid = lo + (hi - lo) / 2;
|
||||
let off = self.header.index_off + (first + mid) * INDEX_ENTRY as u64;
|
||||
self.read_image_at(off, INDEX_ENTRY, &mut buf)?;
|
||||
if &buf.as_slice()[..RAW_KEY_LEN] < &key[..] {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
Ok(first + lo)
|
||||
}
|
||||
|
||||
/// The value of an index entry, read from where the index says it is.
|
||||
fn value_at(&mut self, value_off: u64, value_len: u64) -> Result<KVVec<u8>> {
|
||||
let mut out = KVVec::<u8>::new();
|
||||
fn value_at(&mut self, value_off: u64, value_len: u64) -> Result<KVVec<u8>> { let mut out = KVVec::<u8>::new();
|
||||
self.read_image_at(value_off, value_len as usize, &mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
@@ -2711,6 +2814,166 @@ unsafe fn v3_enum(
|
||||
}
|
||||
0
|
||||
}
|
||||
/// `CUBE_OP_RANGE` against a v3 image: **seek** the index to the span's foot, then read forward.
|
||||
///
|
||||
/// This is `v3_enum` with two differences, and both are the operation's whole point:
|
||||
///
|
||||
/// * it starts at the **span's foot** rather than at the space's first record, found by a binary
|
||||
/// search over the fixed stride, so the cost is the span and not the space;
|
||||
/// * it decides with [`Region::holds`] rather than returning everything, and stops the moment the
|
||||
/// index walks past the span's head, because the index is sorted by key.
|
||||
///
|
||||
/// The log's edits interleave exactly as they do in a walk, so this is the same merge — with one
|
||||
/// added step, which is the log's half of the seek: an edit sorting below the foot cannot be in the
|
||||
/// box, because the span bounds every key in it.
|
||||
///
|
||||
/// Records **examined** can exceed records **returned**, and that excess is the honest cost of a
|
||||
/// bound: the span contains keys of points outside the box, and `holds` rejects them.
|
||||
///
|
||||
/// # Safety
|
||||
/// `buf` must hold `cap` writable bytes; `out_len` and `out_cursor` must point to writable `u64`s.
|
||||
unsafe fn v3_range(
|
||||
mut image: Addressed,
|
||||
wanted: [u8; SPACE_ID_LEN],
|
||||
region: Region,
|
||||
cursor: u64,
|
||||
buf: *mut u8,
|
||||
cap: usize,
|
||||
out_len: *mut u64,
|
||||
out_cursor: *mut u64,
|
||||
) -> i32 {
|
||||
let (first, records) = match image.space_entry(&wanted) {
|
||||
Ok(Some(entry)) => entry,
|
||||
Ok(None) => (0, 0),
|
||||
Err(e) => return -(e.to_errno() as i32),
|
||||
};
|
||||
// Own the log: the edits borrow it, and walking the image needs `&mut image`.
|
||||
let mut log = KVVec::<u8>::new();
|
||||
if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() {
|
||||
return -12; // -ENOMEM
|
||||
}
|
||||
let mut edits = KVVec::<LogEdit<'_>>::new();
|
||||
if log_edits(log.as_slice(), &wanted, &mut edits).is_err() {
|
||||
return -12;
|
||||
}
|
||||
|
||||
// The log's half of the seek.
|
||||
let mut edit_at = 0usize;
|
||||
while edit_at < edits.len()
|
||||
&& cube_format::key_cmp(edits.as_slice()[edit_at].key, ®ion.foot)
|
||||
== core::cmp::Ordering::Less
|
||||
{
|
||||
edit_at += 1;
|
||||
}
|
||||
// The index's half.
|
||||
let mut at = match image.lower_bound(®ion.foot, first, records) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return -(e.to_errno() as i32),
|
||||
};
|
||||
|
||||
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
|
||||
let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) };
|
||||
let mut batch = Batch {
|
||||
out,
|
||||
written: 0,
|
||||
returned: 0,
|
||||
seen: cursor,
|
||||
cursor,
|
||||
too_big: 0,
|
||||
};
|
||||
|
||||
let mut entry = KVVec::<u8>::new();
|
||||
loop {
|
||||
// The entries of one key arrive in log order, so the last of a key's group is the newest
|
||||
// word on it and the earlier ones are history.
|
||||
while edit_at + 1 < edits.len()
|
||||
&& edits.as_slice()[edit_at + 1].key == edits.as_slice()[edit_at].key
|
||||
{
|
||||
edit_at += 1;
|
||||
}
|
||||
let edit = edits.as_slice().get(edit_at).copied();
|
||||
let image_entry = if at < first + records {
|
||||
match image.index_entry(at, &mut entry) {
|
||||
Ok(pair) => Some(pair),
|
||||
Err(e) => return -(e.to_errno() as i32),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let image_key: Option<&[u8; RAW_KEY_LEN]> = match image_entry {
|
||||
Some(_) => entry.as_slice()[..RAW_KEY_LEN].try_into().ok(),
|
||||
None => None,
|
||||
};
|
||||
|
||||
// The smallest key still ahead of either stream. If it is past the head, the walk is done:
|
||||
// the index is sorted by key, so nothing further can be inside the span. This is what makes
|
||||
// the seek stop at the span instead of reading to the end of the space.
|
||||
let next_key = match (edit.map(|e| e.key), image_key) {
|
||||
(Some(e), Some(k)) => {
|
||||
if e < k {
|
||||
e
|
||||
} else {
|
||||
k
|
||||
}
|
||||
}
|
||||
(Some(e), None) => e,
|
||||
(None, Some(k)) => k,
|
||||
(None, None) => break,
|
||||
};
|
||||
if cube_format::key_cmp(next_key, ®ion.head) == core::cmp::Ordering::Greater {
|
||||
break;
|
||||
}
|
||||
|
||||
match (edit, image_entry, image_key) {
|
||||
// The log's word on a coordinate the image holds: it wins.
|
||||
(Some(e), Some(_), Some(key)) if e.key == key => {
|
||||
edit_at += 1;
|
||||
at += 1;
|
||||
if !e.deleted && region.holds(e.key) && !batch.offer(e.key, e.value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// A record only the log holds, in its key's place.
|
||||
(Some(e), Some(_), Some(key)) if e.key < key => {
|
||||
edit_at += 1;
|
||||
if !e.deleted && region.holds(e.key) && !batch.offer(e.key, e.value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The image's own record, which the log says nothing about.
|
||||
(_, Some((value_off, value_len)), Some(key)) => {
|
||||
let value = match image.value_at(value_off, value_len) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return -(e.to_errno() as i32),
|
||||
};
|
||||
at += 1;
|
||||
if region.holds(key) && !batch.offer(key, value.as_slice()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(Some(e), None, _) => {
|
||||
edit_at += 1;
|
||||
if !e.deleted && region.holds(e.key) && !batch.offer(e.key, e.value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(None, None, _) => break,
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: both out-pointers are writable under this function's contract.
|
||||
unsafe {
|
||||
if batch.too_big > 0 && batch.written == 0 {
|
||||
*out_len = batch.too_big as u64;
|
||||
*out_cursor = cursor;
|
||||
return -34; // -ERANGE
|
||||
}
|
||||
*out_len = batch.written as u64;
|
||||
*out_cursor = cursor + batch.returned;
|
||||
}
|
||||
0
|
||||
}
|
||||
///
|
||||
/// Returns the record's length, or a negative errno. When the value does not fit in `len` the
|
||||
/// bytes are *not* copied and the length is returned anyway, so the caller can size its buffer
|
||||
@@ -2867,6 +3130,114 @@ pub unsafe extern "C" fn cubelinux_kernel_enum(
|
||||
0
|
||||
}
|
||||
|
||||
/// `CUBE_OP_RANGE`: the live records of one space whose point lies in the caller's box.
|
||||
///
|
||||
/// The cursor means what it means for `CUBE_OP_ENUM` — a **count of records already returned**, so it
|
||||
/// is opaque, survives an append, and is the only end-of-walk signal — with one difference a caller
|
||||
/// must know: it counts the records **in the box**, because those are the records this returns.
|
||||
///
|
||||
/// The box travels as its six numbers rather than as a struct, for the reason a coordinate does: the
|
||||
/// key the box has to become is this side's business.
|
||||
///
|
||||
/// Two paths, and the difference between them is the honest difference between the layouts. A **v3**
|
||||
/// image is addressed, so this **seeks**: the box's corner keys bound every key in it, the index is
|
||||
/// sorted, and a binary search for the span's foot followed by a forward read to its head costs the
|
||||
/// span rather than the space. A **v1/v2** image is a packed list with no index, so there is nothing
|
||||
/// to search and the space is walked — the span still stops the walk early, but the cost is the walk.
|
||||
/// Neither path tells the caller which it got, because the contract is the same either way.
|
||||
///
|
||||
/// # 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_range(
|
||||
space: *const u8,
|
||||
lo_x: u64,
|
||||
lo_y: u64,
|
||||
lo_z: u64,
|
||||
hi_x: u64,
|
||||
hi_y: u64,
|
||||
hi_z: u64,
|
||||
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 region = match Region::new([lo_x, lo_y, lo_z], [hi_x, hi_y, hi_z]) {
|
||||
Some(region) => region,
|
||||
// An empty box. Nothing is in it, so the answer is nothing — and answering here keeps an
|
||||
// inverted span away from the seek, where it would be a range that starts above its end.
|
||||
// The shim answers this too; a second caller must not be able to reach the seek without it.
|
||||
None => {
|
||||
// SAFETY: both out-pointers are writable under this function's contract.
|
||||
unsafe {
|
||||
*out_len = 0;
|
||||
*out_cursor = cursor;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
match Addressed::open() {
|
||||
Ok(Some(image)) => {
|
||||
return unsafe { v3_range(image, wanted, region, cursor, buf, cap, out_len, out_cursor) }
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => return -(e.to_errno() as i32),
|
||||
}
|
||||
|
||||
let view = match read_view() {
|
||||
Ok(v) => v,
|
||||
Err(e) => return -(e.to_errno() as i32),
|
||||
};
|
||||
let mut walker = match SpaceWalker::new(&view, &wanted) {
|
||||
Ok(w) => w,
|
||||
Err(_) => return -12, // -ENOMEM
|
||||
};
|
||||
|
||||
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
|
||||
let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) };
|
||||
let mut batch = Batch {
|
||||
out,
|
||||
written: 0,
|
||||
returned: 0,
|
||||
seen: cursor,
|
||||
cursor,
|
||||
too_big: 0,
|
||||
};
|
||||
while let Some((key, value)) = walker.next() {
|
||||
match region.place(key) {
|
||||
// Below the span: not in the box, and skipping it is the seek's substitute on a layout
|
||||
// that has no index to search.
|
||||
Span::Below => continue,
|
||||
// Above the span: the walk is sorted by key, so nothing further can be in the box.
|
||||
Span::Above => break,
|
||||
Span::Inside => {}
|
||||
}
|
||||
if region.holds(key) && !batch.offer(key, value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: both out-pointers are writable under this function's contract.
|
||||
unsafe {
|
||||
if batch.too_big > 0 && batch.written == 0 {
|
||||
// Not one whole record fits. Say how much it needs, the way a read does.
|
||||
*out_len = batch.too_big as u64;
|
||||
*out_cursor = cursor;
|
||||
return -34; // -ERANGE
|
||||
}
|
||||
*out_len = batch.written as u64;
|
||||
*out_cursor = cursor + batch.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
|
||||
|
||||
@@ -42,6 +42,7 @@ struct cube_args {
|
||||
#define CUBE_OP_SYNC 4 /* fold the log into the image */
|
||||
#define CUBE_OP_ENUM 5 /* walk the records of a space, in batches */
|
||||
#define CUBE_OP_SPACES 6 /* walk the spaces that hold records */
|
||||
#define CUBE_OP_RANGE 7 /* walk the records of a space that lie in a box */
|
||||
|
||||
/*
|
||||
* The walk's argument block: its own block rather than a wider `cube_args`, because it needs a
|
||||
@@ -72,4 +73,42 @@ struct cube_enum_args {
|
||||
*/
|
||||
};
|
||||
|
||||
/*
|
||||
* The region walk's argument block: its own block, for the walk's reason — it needs a box, a cursor
|
||||
* and a buffer, and the coordinate would otherwise be both an input and an output. It is versioned
|
||||
* by `size` like the other two, so this interface grows by gaining a block rather than by being
|
||||
* replaced.
|
||||
*
|
||||
* A region is a box, inclusive on both corners. Records come back packed exactly as CUBE_OP_ENUM
|
||||
* packs them — `key(24) | value_len(u32, little-endian) | value`, in the store's own order — so a
|
||||
* kernel region answer and a userspace one can be compared byte for byte.
|
||||
*
|
||||
* The cursor is a COUNT OF RECORDS ALREADY RETURNED, and it is the end-of-walk signal for the same
|
||||
* reason as the walk's: a batch holds as many whole records as fit, so most batches come back
|
||||
* short, and reading a short batch as the end truncates the answer. The one difference is what it
|
||||
* counts — a walk counts the records of the space, a region walk counts the records *in the box*,
|
||||
* because those are the records it returns.
|
||||
*
|
||||
* Implementation, because it is what makes this cheap: **the kernel seeks.** The box's two corner
|
||||
* keys bound every key inside it (`cube_format::key_span`), so a v3 image's sorted index is
|
||||
* binary-searched for the foot of that span and read forward to its head. The span is a BOUND, not
|
||||
* the set — records *outside* the box also have keys inside it (Z-order amplification) — so each
|
||||
* candidate is decoded and tested against the box before it is returned. More records may therefore
|
||||
* be examined than are returned, and that over-coverage is the honest cost of the span.
|
||||
*/
|
||||
struct cube_range_args {
|
||||
__u32 size; /* sizeof(struct cube_range_args) as the caller built it */
|
||||
__u32 op; /* CUBE_OP_RANGE */
|
||||
__u8 space[32]; /* in: the space to search */
|
||||
__u64 lo[3]; /* in: the region's near corner, inclusive */
|
||||
__u64 hi[3]; /* in: the region's far corner, inclusive */
|
||||
__u64 cursor; /* in: 0 to start, or what the last call returned;
|
||||
* 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;
|
||||
* out: bytes written, or on -ERANGE what would be needed
|
||||
*/
|
||||
};
|
||||
|
||||
#endif /* _UAPI_LINUX_CUBE_H */
|
||||
|
||||
Reference in New Issue
Block a user