Skip to main content

ax_alloc/
lib.rs

1//! [ArceOS](https://github.com/arceos-org/arceos) global memory allocator.
2//!
3//! It provides [`GlobalAllocator`], which implements the trait
4//! [`core::alloc::GlobalAlloc`]. A static global variable of type
5//! [`GlobalAllocator`] is defined with the `#[global_allocator]` attribute, to
6//! be registered as the standard library's default allocator.
7
8#![no_std]
9
10#[allow(unused_imports)]
11#[macro_use]
12extern crate log;
13extern crate alloc;
14
15use core::{
16    alloc::Layout,
17    fmt,
18    ptr::NonNull,
19    sync::atomic::{AtomicBool, Ordering},
20};
21
22use strum::{IntoStaticStr, VariantArray};
23
24const PAGE_SIZE: usize = 0x1000;
25#[cfg(any(tlsf, buddy_slab, test))]
26const MIN_RECLAIM_PAGES: usize = 16;
27#[cfg(any(tlsf, buddy_slab, test))]
28const MAX_RECLAIM_ATTEMPTS: usize = 4;
29
30/// A function that tries to reclaim physical pages (e.g. by evicting
31/// clean file-backed page cache pages). Returns the number of pages freed.
32pub type PageReclaimFn = fn(num_pages: usize) -> usize;
33
34static PAGE_RECLAIM_FN: ax_sync::SpinLock<Option<PageReclaimFn>> = ax_sync::SpinLock::new(None);
35static PAGE_RECLAIM_ACTIVE: AtomicBool = AtomicBool::new(false);
36
37struct PageReclaimLease;
38
39impl PageReclaimLease {
40    fn try_acquire() -> Option<Self> {
41        PAGE_RECLAIM_ACTIVE
42            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
43            .ok()
44            .map(|_| Self)
45    }
46}
47
48impl Drop for PageReclaimLease {
49    fn drop(&mut self) {
50        PAGE_RECLAIM_ACTIVE.store(false, Ordering::Release);
51    }
52}
53
54/// Register a callback that the allocator invokes when a page or Rust heap
55/// allocation cannot be satisfied.
56///
57/// The callback is an allocator-pressure capability: it must not allocate from
58/// this allocator, perform I/O, wait for contended locks, or invoke unknown
59/// callbacks. It may use try-lock based, clean-page-only eviction.
60pub fn register_page_reclaim_fn(f: PageReclaimFn) {
61    *PAGE_RECLAIM_FN.lock_irqsave() = Some(f);
62}
63
64/// Try to reclaim physical pages by invoking the registered callback.
65/// Returns the number of pages actually freed.
66///
67/// The registration lock and allocator backend lock are released before the
68/// callback runs. A typed lease rejects recursive or concurrent reclaim; this
69/// is the allocator equivalent of Linux's bounded direct-reclaim context.
70pub fn try_page_reclaim(num_pages: usize) -> usize {
71    let Some(_lease) = PageReclaimLease::try_acquire() else {
72        return 0;
73    };
74    let reclaim_fn = { *PAGE_RECLAIM_FN.lock_irqsave() };
75    reclaim_fn.map_or(0, |f| f(num_pages))
76}
77
78#[cfg(any(tlsf, buddy_slab, test))]
79pub(crate) fn retry_after_page_reclaim<T>(
80    target_pages: usize,
81    mut attempt: impl FnMut() -> AllocResult<T>,
82    mut reclaim: impl FnMut(usize) -> usize,
83) -> AllocResult<T> {
84    match attempt() {
85        Ok(value) => return Ok(value),
86        Err(AllocError::NoMemory) => {}
87        Err(error) => return Err(error),
88    }
89
90    let target_pages = target_pages.max(MIN_RECLAIM_PAGES);
91    for _ in 0..MAX_RECLAIM_ATTEMPTS {
92        let reclaimed = reclaim(target_pages);
93
94        // Retry even without local progress: another CPU may have completed a
95        // reclaim or deallocation after the first allocation attempt failed.
96        match attempt() {
97            Ok(value) => return Ok(value),
98            Err(AllocError::NoMemory) if reclaimed != 0 => {}
99            Err(error) => return Err(error),
100        }
101    }
102    Err(AllocError::NoMemory)
103}
104
105#[cfg(any(tlsf, buddy_slab))]
106pub(crate) fn retry_after_registered_reclaim<T>(
107    target_pages: usize,
108    attempt: impl FnMut() -> AllocResult<T>,
109) -> AllocResult<T> {
110    retry_after_page_reclaim(target_pages, attempt, try_page_reclaim)
111}
112
113#[cfg(any(tlsf, buddy_slab))]
114pub(crate) const fn layout_reclaim_pages(layout: Layout) -> usize {
115    layout.size().div_ceil(PAGE_SIZE)
116}
117
118mod page;
119pub use page::GlobalPage;
120
121/// Tracking of memory usage, enabled with the `tracking` feature.
122#[cfg(feature = "tracking")]
123pub mod tracking;
124
125/// Kinds of memory usage for tracking.
126#[repr(u8)]
127#[derive(Debug, Clone, Copy, PartialEq, Eq, VariantArray, IntoStaticStr)]
128pub enum UsageKind {
129    /// Heap allocations made by kernel Rust code.
130    RustHeap,
131    /// Virtual memory, usually used for user space.
132    VirtMem,
133    /// Page cache for file systems.
134    PageCache,
135    /// Page tables.
136    PageTable,
137    /// Page-backed kernel task stacks.
138    TaskStack,
139    /// DMA memory.
140    Dma,
141    /// Memory used by [`GlobalPage`].
142    Global,
143}
144
145/// Statistics of memory usages.
146#[derive(Clone, Copy)]
147pub struct Usages([usize; UsageKind::VARIANTS.len()]);
148
149impl Usages {
150    const fn new() -> Self {
151        Self([0; UsageKind::VARIANTS.len()])
152    }
153
154    #[allow(dead_code)]
155    fn alloc(&mut self, kind: UsageKind, size: usize) {
156        self.0[kind as usize] += size;
157    }
158
159    #[allow(dead_code)]
160    fn dealloc(&mut self, kind: UsageKind, size: usize) {
161        self.0[kind as usize] -= size;
162    }
163
164    /// Get the memory usage for a specific kind.
165    pub fn get(&self, kind: UsageKind) -> usize {
166        self.0[kind as usize]
167    }
168}
169
170impl fmt::Debug for Usages {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        let mut d = f.debug_struct("UsageStats");
173        for &kind in UsageKind::VARIANTS {
174            d.field(kind.into(), &self.0[kind as usize]);
175        }
176        d.finish()
177    }
178}
179
180/// The error type used for allocation operations in `ax-alloc`.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
182pub enum AllocError {
183    /// Invalid size, alignment, or other input parameter.
184    #[error("invalid allocation parameter")]
185    InvalidParam,
186    /// The allocator has already been initialized.
187    #[error("allocator is already initialized")]
188    AlreadyInitialized,
189    /// A region overlaps with an existing managed region.
190    #[error("memory region overlaps an existing allocation region")]
191    MemoryOverlap,
192    /// Not enough memory is available to satisfy the request.
193    #[error("not enough memory")]
194    NoMemory,
195    /// Attempted to deallocate memory that was not allocated.
196    #[error("memory was not allocated by this allocator")]
197    NotAllocated,
198    /// The allocator has not been initialized.
199    #[error("allocator is not initialized")]
200    NotInitialized,
201    /// The requested address or entity was not found.
202    #[error("allocation was not found")]
203    NotFound,
204}
205
206/// A [`Result`] alias with [`AllocError`] as the error type.
207pub type AllocResult<T = ()> = Result<T, AllocError>;
208
209/// Unified allocator operations provided by all `ax-alloc` backends.
210pub trait AllocatorOps {
211    /// Returns the allocator name.
212    fn name(&self) -> &'static str;
213
214    /// Initializes the allocator with the given region.
215    fn init(&self, start_vaddr: usize, size: usize) -> AllocResult;
216
217    /// Adds an extra memory region to the allocator.
218    fn add_memory(&self, start_vaddr: usize, size: usize) -> AllocResult;
219
220    /// Allocates arbitrary bytes.
221    fn alloc(&self, layout: Layout) -> AllocResult<NonNull<u8>>;
222
223    /// Deallocates a prior byte allocation.
224    fn dealloc(&self, pos: NonNull<u8>, layout: Layout);
225
226    /// Allocates contiguous pages.
227    ///
228    /// `align` is the requested byte alignment, not a log2/exponent.
229    /// It must be a power-of-two byte alignment accepted by the backend page allocator.
230    fn alloc_pages(&self, num_pages: usize, align: usize, kind: UsageKind) -> AllocResult<usize>;
231
232    /// Allocates contiguous DMA32 pages.
233    ///
234    /// `align` is the requested byte alignment, not a log2/exponent.
235    /// It must be a power-of-two byte alignment accepted by the backend page allocator.
236    fn alloc_dma32_pages(
237        &self,
238        num_pages: usize,
239        align: usize,
240        kind: UsageKind,
241    ) -> AllocResult<usize>;
242
243    /// Allocates contiguous pages starting from the given address.
244    ///
245    /// `align` is the requested byte alignment, not a log2/exponent.
246    /// It must be a power-of-two byte alignment accepted by the backend page allocator.
247    fn alloc_pages_at(
248        &self,
249        start: usize,
250        num_pages: usize,
251        align: usize,
252        kind: UsageKind,
253    ) -> AllocResult<usize>;
254
255    /// Deallocates a prior page allocation.
256    fn dealloc_pages(&self, pos: usize, num_pages: usize, kind: UsageKind);
257
258    /// Returns used byte count.
259    fn used_bytes(&self) -> usize;
260
261    /// Returns available byte count.
262    fn available_bytes(&self) -> usize;
263
264    /// Returns used page count.
265    fn used_pages(&self) -> usize;
266
267    /// Returns available page count.
268    fn available_pages(&self) -> usize;
269
270    /// Returns usage statistics.
271    fn usages(&self) -> Usages;
272}
273
274// Select implementation based on build.rs-generated cfg flags.
275#[cfg(buddy_slab)]
276mod buddy_slab;
277#[cfg(not(any(tlsf, buddy_slab)))]
278mod stub_impl;
279#[cfg(tlsf)]
280mod tlsf_impl;
281
282#[cfg(buddy_slab)]
283use buddy_slab as imp;
284pub use imp::{
285    DefaultByteAllocator, GlobalAllocator, global_add_memory, global_init, init_percpu_slab,
286};
287#[cfg(not(any(tlsf, buddy_slab)))]
288use stub_impl as imp;
289#[cfg(tlsf)]
290use tlsf_impl as imp;
291
292/// Returns the reference to the global allocator.
293pub fn global_allocator() -> &'static GlobalAllocator {
294    imp::global_allocator()
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn no_memory_retries_after_reclaim_progress() {
303        let mut attempts = 0;
304        let mut reclaims = 0;
305        let result = retry_after_page_reclaim(
306            1,
307            || {
308                attempts += 1;
309                (attempts == 2).then_some(42).ok_or(AllocError::NoMemory)
310            },
311            |target| {
312                reclaims += 1;
313                assert_eq!(target, MIN_RECLAIM_PAGES);
314                1
315            },
316        );
317
318        assert_eq!(result, Ok(42));
319        assert_eq!(attempts, 2);
320        assert_eq!(reclaims, 1);
321    }
322
323    #[test]
324    fn zero_reclaim_progress_gets_one_concurrent_retry() {
325        let mut attempts = 0;
326        let mut reclaims = 0;
327        let result = retry_after_page_reclaim::<()>(
328            32,
329            || {
330                attempts += 1;
331                Err(AllocError::NoMemory)
332            },
333            |target| {
334                reclaims += 1;
335                assert_eq!(target, 32);
336                0
337            },
338        );
339
340        assert_eq!(result, Err(AllocError::NoMemory));
341        assert_eq!(attempts, 2);
342        assert_eq!(reclaims, 1);
343    }
344
345    #[test]
346    fn non_memory_error_does_not_enter_reclaim() {
347        let mut reclaims = 0;
348        let result = retry_after_page_reclaim::<()>(
349            1,
350            || Err(AllocError::InvalidParam),
351            |_| {
352                reclaims += 1;
353                1
354            },
355        );
356
357        assert_eq!(result, Err(AllocError::InvalidParam));
358        assert_eq!(reclaims, 0);
359    }
360
361    #[test]
362    fn reclaim_progress_has_a_bounded_retry_budget() {
363        let mut attempts = 0;
364        let mut reclaims = 0;
365        let result = retry_after_page_reclaim::<()>(
366            usize::MAX,
367            || {
368                attempts += 1;
369                Err(AllocError::NoMemory)
370            },
371            |_| {
372                reclaims += 1;
373                1
374            },
375        );
376
377        assert_eq!(result, Err(AllocError::NoMemory));
378        assert_eq!(attempts, MAX_RECLAIM_ATTEMPTS + 1);
379        assert_eq!(reclaims, MAX_RECLAIM_ATTEMPTS);
380    }
381}