Skip to main content

buddy_slab_allocator/buddy/
global_node_pool.rs

1//! Global node pool for buddy allocator
2//!
3//! Provides a single pool of list nodes shared across all zones and orders.
4//! This eliminates the need for per-zone list pools and improves memory efficiency.
5
6use super::buddy_block::BuddyBlock;
7
8/// Simple linked list node used by the global node pool
9#[derive(Debug, Clone, Copy)]
10pub struct ListNode<T> {
11    pub data: T,
12    pub next: Option<usize>,
13}
14
15fn align_up(addr: usize, align: usize) -> usize {
16    debug_assert!(align.is_power_of_two());
17    (addr + align - 1) & !(align - 1)
18}
19
20/// Global node pool - all zones and orders share nodes from this pool
21///
22/// Nodes are carved from one or more contiguous memory regions provided
23/// by the page allocator. The pool manages a single free list of nodes
24/// using raw pointers (stored as usize indices).
25pub struct GlobalNodePool {
26    /// Free list head - points to first available node (node address)
27    free_head: Option<usize>,
28    /// Total number of nodes ever added to the pool
29    total_nodes: usize,
30    /// Current number of free nodes in the pool
31    free_nodes: usize,
32    /// Allocation statistics
33    total_allocations: usize,
34    total_deallocations: usize,
35}
36
37impl GlobalNodePool {
38    /// Create a new global node pool (uninitialized, must call init())
39    pub const fn new() -> Self {
40        Self {
41            free_head: None,
42            total_nodes: 0,
43            free_nodes: 0,
44            total_allocations: 0,
45            total_deallocations: 0,
46        }
47    }
48
49    /// Initialize the global node pool with a backing memory region
50    ///
51    /// The region is treated as raw memory and partitioned into
52    /// `ListNode<BuddyBlock>` objects, which are all added to the
53    /// pool's free list.
54    pub fn init(&mut self, region_start: usize, region_size: usize) {
55        self.free_head = None;
56        self.total_nodes = 0;
57        self.free_nodes = 0;
58        self.total_allocations = 0;
59        self.total_deallocations = 0;
60        self.add_region(region_start, region_size);
61    }
62
63    /// Add an additional memory region to the node pool
64    ///
65    /// This can be used to grow the pool at runtime by carving
66    /// more memory from the page allocator.
67    pub fn add_region(&mut self, region_start: usize, region_size: usize) {
68        let node_size = core::mem::size_of::<ListNode<BuddyBlock>>();
69        let align = core::mem::align_of::<ListNode<BuddyBlock>>();
70        if region_size < node_size {
71            return;
72        }
73
74        let mut current = align_up(region_start, align);
75        let end = region_start + region_size;
76
77        while current + node_size <= end {
78            unsafe {
79                let node_ptr = current as *mut ListNode<BuddyBlock>;
80                core::ptr::write(
81                    node_ptr,
82                    ListNode {
83                        data: core::mem::zeroed(),
84                        next: self.free_head,
85                    },
86                );
87            }
88
89            self.free_head = Some(current);
90            self.total_nodes += 1;
91            self.free_nodes += 1;
92            current += node_size;
93        }
94    }
95
96    /// Allocate a node from the pool
97    ///
98    /// Returns the index of the allocated node, or None if pool is exhausted
99    pub fn alloc_node(&mut self) -> Option<usize> {
100        let node_addr = self.free_head?;
101
102        unsafe {
103            let node = &mut *(node_addr as *mut ListNode<BuddyBlock>);
104            self.free_head = node.next;
105            node.next = None;
106        }
107
108        self.total_allocations += 1;
109        if self.free_nodes > 0 {
110            self.free_nodes -= 1;
111        } else {
112            panic!("free_nodes: {}", self.free_nodes);
113        }
114
115        Some(node_addr)
116    }
117
118    /// Deallocate a node back to the pool
119    ///
120    /// The node should not be part of any active list when freed
121    pub fn dealloc_node(&mut self, node_idx: usize) {
122        unsafe {
123            let node_ptr = node_idx as *mut ListNode<BuddyBlock>;
124            core::ptr::write(
125                node_ptr,
126                ListNode {
127                    data: core::mem::zeroed(),
128                    next: self.free_head,
129                },
130            );
131        }
132
133        self.free_head = Some(node_idx);
134        self.total_deallocations += 1;
135        self.free_nodes += 1;
136    }
137
138    /// Get a reference to a node by index
139    pub fn get_node(&self, node_idx: usize) -> Option<&ListNode<BuddyBlock>> {
140        Some(unsafe { &*(node_idx as *const ListNode<BuddyBlock>) })
141    }
142
143    /// Get a mutable reference to a node by index
144    pub fn get_node_mut(&mut self, node_idx: usize) -> Option<&mut ListNode<BuddyBlock>> {
145        Some(unsafe { &mut *(node_idx as *mut ListNode<BuddyBlock>) })
146    }
147
148    /// Get the number of free nodes in the pool
149    pub fn free_node_count(&self) -> usize {
150        self.free_nodes
151    }
152
153    /// Get the number of allocated nodes
154    pub fn allocated_node_count(&self) -> usize {
155        self.total_nodes.saturating_sub(self.free_nodes)
156    }
157
158    /// Get pool statistics
159    pub fn get_stats(&self) -> GlobalPoolStats {
160        GlobalPoolStats {
161            total_nodes: self.total_nodes,
162            free_nodes: self.free_nodes,
163            allocated_nodes: self.allocated_node_count(),
164            total_allocations: self.total_allocations,
165            total_deallocations: self.total_deallocations,
166        }
167    }
168}
169
170impl Default for GlobalNodePool {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176/// Global pool statistics
177#[derive(Debug, Default, Clone)]
178pub struct GlobalPoolStats {
179    pub total_nodes: usize,
180    pub free_nodes: usize,
181    pub allocated_nodes: usize,
182    pub total_allocations: usize,
183    pub total_deallocations: usize,
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    const TEST_NODE_COUNT: usize = 512;
191    #[test]
192    fn test_pool_init() {
193        let mut pool: GlobalNodePool = GlobalNodePool::new();
194        let mut backing = [ListNode {
195            data: BuddyBlock { order: 0, addr: 0 },
196            next: None,
197        }; TEST_NODE_COUNT];
198
199        let region_start = backing.as_mut_ptr() as usize;
200        let region_size = core::mem::size_of_val(&backing);
201        pool.init(region_start, region_size);
202
203        assert_eq!(pool.free_node_count(), TEST_NODE_COUNT);
204        assert_eq!(pool.allocated_node_count(), 0);
205    }
206
207    #[test]
208    fn test_alloc_dealloc() {
209        let mut pool: GlobalNodePool = GlobalNodePool::new();
210        let mut backing = [ListNode {
211            data: BuddyBlock { order: 0, addr: 0 },
212            next: None,
213        }; TEST_NODE_COUNT];
214
215        let region_start = backing.as_mut_ptr() as usize;
216        let region_size = core::mem::size_of_val(&backing);
217        pool.init(region_start, region_size);
218
219        let idx1 = pool.alloc_node().unwrap();
220        assert_eq!(pool.free_node_count(), TEST_NODE_COUNT - 1);
221        assert_eq!(pool.allocated_node_count(), 1);
222
223        let idx2 = pool.alloc_node().unwrap();
224        assert_eq!(pool.free_node_count(), TEST_NODE_COUNT - 2);
225
226        pool.dealloc_node(idx1);
227        assert_eq!(pool.free_node_count(), TEST_NODE_COUNT - 1);
228
229        pool.dealloc_node(idx2);
230        assert_eq!(pool.free_node_count(), TEST_NODE_COUNT);
231    }
232
233    #[test]
234    fn test_pool_exhaustion() {
235        let mut pool: GlobalNodePool = GlobalNodePool::new();
236        let mut backing = [ListNode {
237            data: BuddyBlock { order: 0, addr: 0 },
238            next: None,
239        }; TEST_NODE_COUNT];
240
241        let region_start = backing.as_mut_ptr() as usize;
242        let region_size = core::mem::size_of_val(&backing);
243        pool.init(region_start, region_size);
244
245        // Allocate all nodes
246        let mut indices = alloc::vec::Vec::new();
247        for _ in 0..TEST_NODE_COUNT {
248            indices.push(pool.alloc_node().unwrap());
249        }
250
251        assert_eq!(pool.free_node_count(), 0);
252        assert!(pool.alloc_node().is_none());
253
254        // Free one and allocate again
255        pool.dealloc_node(indices[0]);
256        assert_eq!(pool.free_node_count(), 1);
257        assert!(pool.alloc_node().is_some());
258    }
259
260    #[test]
261    fn test_node_access() {
262        let mut pool: GlobalNodePool = GlobalNodePool::new();
263        let mut backing = [ListNode {
264            data: BuddyBlock { order: 0, addr: 0 },
265            next: None,
266        }; TEST_NODE_COUNT];
267
268        let region_start = backing.as_mut_ptr() as usize;
269        let region_size = core::mem::size_of_val(&backing);
270        pool.init(region_start, region_size);
271
272        let idx = pool.alloc_node().unwrap();
273
274        // Get mutable reference and set data
275        if let Some(node) = pool.get_node_mut(idx) {
276            node.data = BuddyBlock {
277                order: 0,
278                addr: 0x1000,
279            };
280        }
281
282        // Get reference and read data
283        if let Some(node) = pool.get_node(idx) {
284            assert_eq!(node.data.order, 0);
285            assert_eq!(node.data.addr, 0x1000);
286        }
287    }
288
289    #[test]
290    fn test_stats() {
291        let mut pool: GlobalNodePool = GlobalNodePool::new();
292        let mut backing = [ListNode {
293            data: BuddyBlock { order: 0, addr: 0 },
294            next: None,
295        }; TEST_NODE_COUNT];
296
297        let region_start = backing.as_mut_ptr() as usize;
298        let region_size = core::mem::size_of_val(&backing);
299        pool.init(region_start, region_size);
300
301        let idx1 = pool.alloc_node().unwrap();
302        let _idx2 = pool.alloc_node().unwrap();
303
304        let stats = pool.get_stats();
305        assert_eq!(stats.total_nodes, TEST_NODE_COUNT);
306        assert_eq!(stats.free_nodes, TEST_NODE_COUNT - 2);
307        assert_eq!(stats.allocated_nodes, 2);
308        assert_eq!(stats.total_allocations, 2);
309        assert_eq!(stats.total_deallocations, 0);
310
311        pool.dealloc_node(idx1);
312        let stats2 = pool.get_stats();
313        assert_eq!(stats2.free_nodes, TEST_NODE_COUNT - 1);
314        assert_eq!(stats2.total_deallocations, 1);
315    }
316}