-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathalloc_cache.rs
More file actions
181 lines (158 loc) · 5.49 KB
/
Copy pathalloc_cache.rs
File metadata and controls
181 lines (158 loc) · 5.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
//! linux-parity: complete
//! linux-source: vendor/linux/io_uring/alloc_cache.c
//! test-origin: linux:vendor/linux/io_uring/alloc_cache.c
//! Per-ring slab cache for short-lived io_uring objects (poll table entries,
//! async_data, etc.). LIFO free-list with a hard size cap.
//!
//! Ref: vendor/linux/io_uring/alloc_cache.c
//! Ref: vendor/linux/io_uring/alloc_cache.h
extern crate alloc;
use alloc::boxed::Box;
use alloc::vec::Vec;
use core::marker::PhantomData;
/// Linux: `#define IO_ALLOC_CACHE_MAX 128`.
pub const IO_ALLOC_CACHE_MAX: u32 = 128;
/// `struct io_alloc_cache` — LIFO free-list bounded by `max_cached`.
///
/// In Linux the cache stores raw `void *` so any object type can be cached.
/// We expose a typed wrapper so callers don't have to cast. Behavioral parity
/// (LIFO order, bounded size, `init_clear` zeroing) is preserved.
pub struct AllocCache<T> {
entries: Vec<Box<T>>,
max_cached: u32,
/// Number of leading bytes to zero on `get()` to match `init_bytes`.
init_clear: u32,
_phantom: PhantomData<T>,
}
impl<T: Default> AllocCache<T> {
/// Mirrors `io_alloc_cache_init`. Returns `Err(())` if `max_nr` is zero
/// (matches Linux's "returns true on failure" but with a Rust shape).
pub fn init(max_nr: u32, init_bytes: u32) -> Result<Self, ()> {
if max_nr == 0 {
return Err(());
}
Ok(Self {
entries: Vec::with_capacity(max_nr as usize),
max_cached: max_nr,
init_clear: init_bytes,
_phantom: PhantomData,
})
}
/// Mirrors `io_alloc_cache_put`. Returns `true` if the object was
/// cached, `false` if the cache was full (caller drops the object).
pub fn put(&mut self, obj: Box<T>) -> bool {
if (self.entries.len() as u32) < self.max_cached {
self.entries.push(obj);
true
} else {
false
}
}
/// Mirrors `io_alloc_cache_get`. LIFO — the last `put()` is the next
/// `get()`. Returns `None` when empty.
pub fn get(&mut self) -> Option<Box<T>> {
let mut obj = self.entries.pop()?;
if self.init_clear > 0 {
// Mirror `memset(entry, 0, cache->init_clear)`. Bounded to
// `init_clear` bytes so we match Linux even when T is larger.
let clear = (self.init_clear as usize).min(core::mem::size_of::<T>());
unsafe {
core::ptr::write_bytes(obj.as_mut() as *mut T as *mut u8, 0, clear);
}
}
Some(obj)
}
/// Mirrors `io_cache_alloc`. Tries the free-list first, then heap-alloc.
pub fn alloc(&mut self) -> Box<T> {
self.get().unwrap_or_else(|| Box::new(T::default()))
}
/// Mirrors `io_cache_free`. Caches if room, else drops.
pub fn free(&mut self, obj: Box<T>) {
let _ = self.put(obj);
}
/// Mirrors `io_alloc_cache_free` (drains the cache).
pub fn drain(&mut self) {
self.entries.clear();
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn capacity(&self) -> u32 {
self.max_cached
}
}
impl<T> Drop for AllocCache<T> {
fn drop(&mut self) {
self.entries.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default, Debug, PartialEq, Eq)]
struct Item {
x: u32,
y: u32,
}
#[test]
fn io_alloc_cache_max_matches_linux() {
// Mirrors `#define IO_ALLOC_CACHE_MAX 128` in alloc_cache.h.
assert_eq!(IO_ALLOC_CACHE_MAX, 128);
}
#[test]
fn init_returns_err_for_zero_capacity() {
// Linux returns `true` (failure) when kvmalloc_array(0, ...) fails;
// we surface that as Err(()).
let r: Result<AllocCache<Item>, ()> = AllocCache::init(0, 0);
assert!(r.is_err());
}
#[test]
fn put_and_get_are_lifo() {
let mut c: AllocCache<Item> = AllocCache::init(4, 0).unwrap();
c.put(Box::new(Item { x: 1, y: 1 }));
c.put(Box::new(Item { x: 2, y: 2 }));
c.put(Box::new(Item { x: 3, y: 3 }));
assert_eq!(c.get().unwrap().x, 3);
assert_eq!(c.get().unwrap().x, 2);
assert_eq!(c.get().unwrap().x, 1);
assert!(c.get().is_none());
}
#[test]
fn put_rejects_when_full() {
let mut c: AllocCache<Item> = AllocCache::init(2, 0).unwrap();
assert!(c.put(Box::new(Item::default())));
assert!(c.put(Box::new(Item::default())));
// Third put exceeds max_cached — must return false (caller frees).
assert!(!c.put(Box::new(Item::default())));
}
#[test]
fn alloc_falls_back_to_heap_when_empty() {
let mut c: AllocCache<Item> = AllocCache::init(4, 0).unwrap();
let obj = c.alloc();
// Empty cache produces a fresh default; not a panic.
assert_eq!(*obj, Item::default());
}
#[test]
fn init_clear_zeroes_leading_bytes_on_get() {
let mut c: AllocCache<Item> = AllocCache::init(2, 4).unwrap();
c.put(Box::new(Item {
x: 0xdead,
y: 0xbeef,
}));
let got = c.get().unwrap();
// First 4 bytes (= x) zeroed; y untouched.
assert_eq!(got.x, 0);
assert_eq!(got.y, 0xbeef);
}
#[test]
fn drain_clears_cache() {
let mut c: AllocCache<Item> = AllocCache::init(4, 0).unwrap();
c.put(Box::new(Item::default()));
c.put(Box::new(Item::default()));
c.drain();
assert!(c.is_empty());
}
}