From 7c4fa04ea32cf5e9851b406429c682762fb31b38 Mon Sep 17 00:00:00 2001 From: Philipp Schuster Date: Thu, 7 May 2026 14:19:36 +0200 Subject: [PATCH] vm-allocator: introduce bitmap-backed InterruptAllocator This introduces a bitmap-backed interrupt number allocator. The type is not yet used but will be in the next commit and enable graceful releasing of allocated interrupt numbers (GSIs). Co-authored-by: Sebastian Eydam On-behalf-of: SAP sebastian.eydam@sap.com Signed-off-by: Sebastian Eydam On-behalf-of: Philipp Schuster@sap.com Signed-off-by: Philipp Schuster --- Cargo.lock | 1 + vm-allocator/Cargo.toml | 1 + vm-allocator/src/gsi.rs | 244 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 144fd6129..aea8920a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2526,6 +2526,7 @@ version = "0.1.0" dependencies = [ "arch", "libc", + "thiserror", "vm-memory", ] diff --git a/vm-allocator/Cargo.toml b/vm-allocator/Cargo.toml index 382647931..e1eb864c9 100644 --- a/vm-allocator/Cargo.toml +++ b/vm-allocator/Cargo.toml @@ -11,6 +11,7 @@ kvm = ["arch/kvm"] [dependencies] libc = { workspace = true } +thiserror = { workspace = true } vm-memory = { workspace = true } [target.'cfg(any(target_arch = "aarch64", target_arch = "riscv64"))'.dependencies] diff --git a/vm-allocator/src/gsi.rs b/vm-allocator/src/gsi.rs index d58670a43..30e20e7e7 100644 --- a/vm-allocator/src/gsi.rs +++ b/vm-allocator/src/gsi.rs @@ -2,10 +2,15 @@ // // SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// Only for this commit +#![expect(unused)] + #[cfg(target_arch = "x86_64")] use std::collections::btree_map::BTreeMap; use std::result; +use thiserror::Error; + #[derive(Debug)] pub enum Error { Overflow, @@ -29,6 +34,138 @@ impl GsiApic { } } +/// Errors that may happen while allocating or freeing an interrupt. +#[derive(Error, Debug, PartialEq)] +pub enum InterruptAllocError { + /// Interrupt allocator is exhausted, i.e. out of interrupt vectors. + #[error("Interrupt allocator is exhausted (capacity: {0})")] + ExhaustedError(u32 /* capacity/size */), + + /// Tried to free an interrupt that wasn't allocated. + #[error("Interrupt was not allocated: {0}")] + AlreadyFree(u32 /* vector */), + + /// Tried to free an interrupt that is not in range of the interrupt allocator. + #[error("Interrupt vector is out of range: {0} (range: [{1},{2}))")] + OutOfRange( + u32, /* vector */ + u32, /* lower bound */ + u32, /* upper bound */ + ), +} + +/// Simple bitmap-backed interrupt allocator. +/// +/// The allocator can be configured with an offset. For example, to allocate +/// interrupt vectors in the range `[512, 1024)`, use an offset of 512 and a +/// size of 512. +#[derive(Debug)] +struct InterruptAllocator { + /// Backing store for bitmap. + words: Box<[usize]>, + /// The offset to start allocating interrupts from. + offset: u32, + /// Number of allocatable interrupt vectors starting at `offset`. + size: u32, +} + +impl InterruptAllocator { + /// Creates a new allocator. + fn new(size: u32, offset: u32) -> Self { + assert_ne!(size, 0); + assert!(offset.checked_add(size).is_some()); + + let num_words = size.div_ceil(usize::BITS); + let num_words = usize::try_from(num_words).unwrap(); + let mut words = vec![0; num_words].into_boxed_slice(); + words[num_words - 1] = Self::last_word_mask(size); + + Self { + words, + size, + offset, + } + } + + /// Returns the mask of the last word, ensuring that no more than requested + /// interrupts can be allocated. + fn last_word_mask(size: u32) -> usize { + let rem = size % usize::BITS; + + if rem == 0 { 0 } else { !((1 << rem) - 1) } + } + + /// Returns word and bit indices for a given vector index. + fn word_and_bit( + vector: u32, + ) -> ( + usize, /* index into `words` */ + usize, /* index into `words[w]` */ + ) { + let idx = usize::try_from(vector).unwrap(); + let bits = usize::try_from(usize::BITS).unwrap(); + (idx / bits, idx % bits) + } + + /// Allocates a vector by setting its bit in the bitmap. + /// + /// Returns an error if the allocator is exhausted. + fn alloc(&mut self) -> result::Result { + // Find the next word with capacity for allocating a vector. + let Some(idx) = self.words.iter().position(|&w| w != usize::MAX) else { + return Err(InterruptAllocError::ExhaustedError(self.size)); + }; + let word = &mut self.words[idx]; + + // Find lowest free bit. + let bit = (!*word).trailing_zeros() as usize; + // Set the bit. + *word |= 1 << bit; + // Calculate index, add offset and return. + + let bits = usize::try_from(usize::BITS).unwrap(); + let vector = idx * bits + bit; + let vector = u32::try_from(vector).unwrap(); + Ok(vector + self.offset) + } + + /// Frees a vector by clearing its bit in the bitmap. + /// + /// This vector is assumed to include the internal `offset`. + /// + /// Returns an error if the vector is already free. + fn free(&mut self, vector: u32) -> result::Result<(), InterruptAllocError> { + // At first we make sure that the vector is not out of range. + let begin = self.offset; + let end = begin + self.size; + if !(begin..end).contains(&vector) { + return Err(InterruptAllocError::OutOfRange( + vector, + self.offset, + self.offset + self.size, + )); + } + + let idx = vector.abs_diff(self.offset); + let (w, b) = Self::word_and_bit(idx); + let mask = 1 << b; + + // Let's first check whether the bit is set. + if self.words[w] & mask == 0 { + return Err(InterruptAllocError::AlreadyFree(vector)); + } + // Clear the bit and we are done! + self.words[w] &= !mask; + Ok(()) + } + + /// Returns the capacity of vectors that can be allocated. + #[cfg(target_arch = "x86_64")] + fn size(&self) -> u32 { + self.size + } +} + /// GsiAllocator pub struct GsiAllocator { #[cfg(target_arch = "x86_64")] @@ -112,3 +249,110 @@ impl Default for GsiAllocator { GsiAllocator::new() } } + +#[cfg(test)] +mod unit_tests { + use super::*; + + mod interrupt_allocator { + use super::*; + + #[test] + // Checks that the allocator can only allocate as many vectors as configured. + fn test_allocator_respects_size() { + for size in [1, 8, 16, 32, 63, 64, 65, 128] { + let mut allocator = InterruptAllocator::new(size, 0); + for _ in 0..size { + let _ = allocator.alloc().expect("should not be exhausted"); + } + allocator.alloc().expect_err("should be exhausted"); + } + } + + #[test] + // Checks that the allocator starts allocating vectors at the given offset. + fn test_allocator_respects_offset() { + for offset in [0, 1, 2, 3, 8, 16, 32, 64, 77, 128] { + let mut allocator = InterruptAllocator::new(8, offset); + let vec = allocator.alloc().unwrap(); + assert_eq!(offset, vec); + allocator.free(vec).unwrap(); + } + } + + #[test] + // Checks that the calculations in alloc and free are correct. + fn test_allocator_alloc_and_free_all_vectors() { + for size in [1, 3, 7, 8, 15, 16, 32, 63, 64, 65, 128, 4096] { + let mut allocator = InterruptAllocator::new(size, 0); + let mut num_vectors = 0; + while allocator.alloc().is_ok() { + num_vectors += 1; + } + assert_eq!(size, num_vectors); + num_vectors -= 1; + loop { + if let Err(e) = allocator.free(num_vectors) { + println!("Could not free {num_vectors}: {e}"); + break; + } + if let Some(v) = num_vectors.checked_sub(1) { + num_vectors = v; + } else { + break; + } + } + } + } + + #[test] + // Checks that freeing a vector that isn't allocated results in an error. + fn test_can_only_free_allocated_vectors() { + let mut allocator = InterruptAllocator::new(8, 0); + // Never-allocated vector. + assert_eq!(allocator.free(0), Err(InterruptAllocError::AlreadyFree(0))); + // Allocated then freed vector. + let vec = allocator.alloc().unwrap(); + allocator.free(vec).unwrap(); + assert_eq!( + allocator.free(vec), + Err(InterruptAllocError::AlreadyFree(vec)) + ); + } + + #[test] + // Checks that freeing a vector that is not in range of the allocator results + // in an error. + fn test_can_only_free_vectors_in_range() { + let size = 8; + let offset = 16; + let mut allocator = InterruptAllocator::new(size, offset); + for _ in 0..size { + let _ = allocator.alloc().expect("should not be exhausted"); + } + // Out of range above. + let vector_out_of_range = size + offset; + assert_eq!( + allocator.free(vector_out_of_range), + Err(InterruptAllocError::OutOfRange( + vector_out_of_range, + offset, + offset + size + )) + ); + // Out of range below. + assert_eq!( + allocator.free(offset - 1), + Err(InterruptAllocError::OutOfRange( + offset - 1, + offset, + offset + size + )) + ); + for i in 0..size { + let vector = i + allocator.offset; + allocator.free(vector).expect("should not be exhausted"); + } + } + } +}