| // Copyright 2024 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! Defines an arena allocator backed by `base::MemoryMapping`. |
| |
| use std::cell::RefCell; |
| use std::collections::BTreeSet; |
| use std::fs::File; |
| |
| use anyhow::anyhow; |
| use anyhow::bail; |
| use anyhow::Context; |
| use anyhow::Result; |
| use base::MappedRegion; |
| use base::MemoryMapping; |
| use zerocopy::FromBytes; |
| use zerocopy::Immutable; |
| use zerocopy::IntoBytes; |
| use zerocopy::KnownLayout; |
| |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
| struct Region { |
| start: usize, |
| len: usize, |
| } |
| |
| /// Manages a set of regions that are not overlapping each other. |
| #[derive(Default)] |
| struct RegionManager(BTreeSet<Region>); |
| |
| impl RegionManager { |
| fn allocate(&mut self, start: usize, len: usize) -> Result<()> { |
| // Allocation needs to fail if there exists a region that overlaps with [start, start+len). |
| // A region r is overlapping with [start, start+len) if and only if: |
| // r.start <= (start+len) && start <= (r.start+r.len) |
| // |
| // So, we first find the last region where r.start <= (start+len) holds. |
| let left = self |
| .0 |
| .range( |
| ..Region { |
| start: start + len, |
| len: 0, |
| }, |
| ) |
| .next_back() |
| .copied(); |
| |
| // New region to be added. |
| let new = match left { |
| None => Region { start, len }, |
| Some(r) => { |
| if start < r.start + r.len { |
| bail!( |
| "range overlaps: existing: {:?}, new: {:?}", |
| left, |
| Region { start, len } |
| ); |
| } |
| |
| // if `r` and the new region is adjacent, merge them. |
| // otherwise, just return the new region. |
| if start == r.start + r.len { |
| let new = Region { |
| start: r.start, |
| len: r.len + len, |
| }; |
| self.0.remove(&r); |
| new |
| } else { |
| Region { start, len } |
| } |
| } |
| }; |
| |
| // If there exists a region that starts from `new.start + new.len`, |
| // it should be merged with `new`. |
| let right = self |
| .0 |
| .range( |
| Region { |
| start: new.start + new.len, |
| len: 0, |
| }.., |
| ) |
| .next() |
| .copied(); |
| match right { |
| Some(r) if r.start == new.start + new.len => { |
| // merge and insert |
| let merged = Region { |
| start: new.start, |
| len: new.len + r.len, |
| }; |
| self.0.remove(&r); |
| self.0.insert(merged); |
| } |
| Some(_) | None => { |
| // just insert |
| self.0.insert(new); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| #[cfg(test)] |
| fn to_vec(&self) -> Vec<&Region> { |
| self.0.iter().collect() |
| } |
| } |
| |
| #[test] |
| fn test_region_manager() { |
| let mut rm: RegionManager = Default::default(); |
| |
| rm.allocate(0, 5).unwrap(); |
| assert_eq!(rm.to_vec(), vec![&Region { start: 0, len: 5 }]); |
| rm.allocate(10, 5).unwrap(); |
| rm.allocate(15, 5).unwrap(); // will be merged into the previous one |
| assert_eq!( |
| rm.to_vec(), |
| vec![&Region { start: 0, len: 5 }, &Region { start: 10, len: 10 }] |
| ); |
| rm.allocate(3, 5).unwrap_err(); // fail |
| rm.allocate(8, 5).unwrap_err(); // fail |
| |
| rm.allocate(25, 5).unwrap(); |
| assert_eq!( |
| rm.to_vec(), |
| vec![ |
| &Region { start: 0, len: 5 }, |
| &Region { start: 10, len: 10 }, |
| &Region { start: 25, len: 5 } |
| ] |
| ); |
| |
| rm.allocate(5, 5).unwrap(); // will be merged to the existing two regions |
| assert_eq!( |
| rm.to_vec(), |
| vec![&Region { start: 0, len: 20 }, &Region { start: 25, len: 5 }] |
| ); |
| rm.allocate(20, 5).unwrap(); |
| assert_eq!(rm.to_vec(), vec![&Region { start: 0, len: 30 },]); |
| } |
| |
| #[derive(Debug, Clone, Copy, FromBytes, Immutable, IntoBytes, KnownLayout)] |
| #[repr(C)] |
| /// Represents a ID of a disk block. |
| pub struct BlockId(u32); |
| |
| impl From<u32> for BlockId { |
| fn from(value: u32) -> Self { |
| BlockId(value) |
| } |
| } |
| |
| impl From<BlockId> for u32 { |
| fn from(value: BlockId) -> Self { |
| value.0 |
| } |
| } |
| |
| impl BlockId { |
| pub fn as_bytes(&self) -> &[u8] { |
| self.0.as_bytes() |
| } |
| } |
| |
| /// Information on how to mmap a host file to ext2 blocks. |
| pub struct FileMappingInfo { |
| /// Offset in the memory that a file is mapped to. |
| pub mem_offset: usize, |
| /// The file to be mmap'd. |
| pub file: File, |
| /// The length of the mapping. |
| pub length: usize, |
| /// Offset in the file to start the mapping. |
| pub file_offset: usize, |
| } |
| |
| /// Memory arena backed by `base::MemoryMapping`. |
| /// |
| /// This struct takes a mutable referencet to the memory mapping so this arena won't arena the |
| /// region. |
| pub struct Arena<'a> { |
| mem: &'a mut MemoryMapping, |
| block_size: usize, |
| /// A set of regions that are not overlapping each other. |
| /// Use `RefCell` for interior mutability because the mutablity of `RegionManager` should be |
| /// independent from the mutability of the memory mapping. |
| regions: RefCell<RegionManager>, |
| |
| mappings: RefCell<Vec<FileMappingInfo>>, |
| } |
| |
| impl<'a> Arena<'a> { |
| /// Create a new arena backed by `len` bytes of `base::MemoryMapping`. |
| pub fn new(block_size: usize, mem: &'a mut MemoryMapping) -> Result<Self> { |
| Ok(Self { |
| mem, |
| block_size, |
| regions: Default::default(), |
| mappings: Default::default(), |
| }) |
| } |
| |
| /// A helper function to mark a region as reserved. |
| fn reserve(&self, mem_offset: usize, len: usize) -> Result<()> { |
| let mem_end = mem_offset.checked_add(len).context("mem_end overflow")?; |
| |
| if mem_end > self.mem.size() { |
| bail!( |
| "out of memory region: {mem_offset} + {len} > {}", |
| self.mem.size() |
| ); |
| } |
| |
| self.regions.borrow_mut().allocate(mem_offset, len)?; |
| |
| Ok(()) |
| } |
| |
| /// Reserves a region for mmap and stores the mmap information. |
| /// Note that `Arena` will not call mmap(). Instead, the owner of `Arena` instance must call |
| /// `into_mapping_info()` to retrieve the mapping information and call mmap later instead. |
| pub fn reserve_for_mmap( |
| &self, |
| mem_offset: usize, |
| length: usize, |
| file: File, |
| file_offset: usize, |
| ) -> Result<()> { |
| self.reserve(mem_offset, length)?; |
| self.mappings.borrow_mut().push(FileMappingInfo { |
| mem_offset, |
| length, |
| file: file.try_clone()?, |
| file_offset, |
| }); |
| |
| Ok(()) |
| } |
| |
| /// Allocate a new slice on an anonymous memory. |
| /// `Arena` structs guarantees that this area is not overlapping with other regions. |
| pub fn allocate_slice( |
| &self, |
| block: BlockId, |
| block_offset: usize, |
| len: usize, |
| ) -> Result<&'a mut [u8]> { |
| let offset = u32::from(block) as usize * self.block_size + block_offset; |
| self.reserve(offset, len)?; |
| |
| let new_addr = (self.mem.as_ptr() as usize) |
| .checked_add(offset) |
| .context("address overflow")?; |
| |
| // SAFETY: the memory region [new_addr, new_addr+len) is guaranteed to be valid. |
| let slice = unsafe { std::slice::from_raw_parts_mut(new_addr as *mut u8, len) }; |
| Ok(slice) |
| } |
| |
| /// Allocate a new region for a value with type `T`. |
| pub fn allocate<T: FromBytes + IntoBytes + KnownLayout>( |
| &self, |
| block: BlockId, |
| block_offset: usize, |
| ) -> Result<&'a mut T> { |
| let slice = self.allocate_slice(block, block_offset, std::mem::size_of::<T>())?; |
| T::mut_from_bytes(slice).map_err(|_| anyhow!("failed to interpret")) |
| } |
| |
| pub fn write_to_mem<T: IntoBytes + Immutable>( |
| &self, |
| block_id: BlockId, |
| block_offset: usize, |
| value: &T, |
| ) -> Result<()> { |
| let slice = self.allocate_slice(block_id, block_offset, std::mem::size_of::<T>())?; |
| slice.copy_from_slice(value.as_bytes()); |
| Ok(()) |
| } |
| |
| /// Consumes `Arena` and retrieve mmap information. |
| pub fn into_mapping_info(self) -> Vec<FileMappingInfo> { |
| self.mappings.take() |
| } |
| } |