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 <sebastian.eydam@cyberus-technology.de>
On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>

On-behalf-of: Philipp Schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
This commit is contained in:
Philipp Schuster
2026-05-07 14:19:36 +02:00
committed by Rob Bradford
parent 90859452ab
commit 7c4fa04ea3
3 changed files with 246 additions and 0 deletions

View File

@@ -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]

View File

@@ -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<u32, InterruptAllocError> {
// 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");
}
}
}
}