Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vm-allocator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,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]
Expand Down
187 changes: 147 additions & 40 deletions vm-allocator/src/gsi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@

#[cfg(target_arch = "x86_64")]
use std::collections::btree_map::BTreeMap;
use std::result;
use std::result::Result;
use std::sync::Mutex;

#[derive(Debug)]
pub enum Error {
Overflow,
}

pub type Result<T> = result::Result<T, Error>;
use thiserror::Error;

/// GsiApic
#[cfg(target_arch = "x86_64")]
Expand All @@ -29,80 +25,191 @@ impl GsiApic {
}
}

#[derive(Debug)]
struct InterruptAllocator {
Copy link
Member

@phip1611 phip1611 Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Pleae add a helpful documentation comment to this struct

  2. This is just a bitmap, aye? Could you please rename words to bitmap then? As the size of this struct is fixed for the lifetime of the struct, please use Box<[usize]> as type. You can use into_boxed_slice() on the vector in the constructor.

words: Vec<usize>,
size: u32,
offset: u32,
}

/// Errors that may happen while allocating or freeing an interrupt.
#[derive(Error, Debug)]
pub enum InterruptAllocError {
/// Interrupt allocator is exhausted, i.e. out of interrupt vectors.
#[error("Interrupt allocator is exhausted")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interrupt allocator is exhausted (capacity: {self.words.capacity()}`

ExhaustedError(),
Copy link
Member

@phip1611 phip1611 Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExhaustedError, (no ())


/// Tried to free an interrupt that wasn't allocated.
#[error("Interrupt was not allocated: {0}")]
AlreadyFree(u32),

/// Tried to free an interrupt that is not in range of the interrupt allocator.
#[error("Interrupt vector is out of range: {0}")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interrupt vector is out of range: {0} (max: {1})"

OutOfRange(u32),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OutOfRange(u32 /* vector */, u32 /* range/max */),

}

// Maximum number of IRQ routes according to the kernel code.
const KVM_MAX_IRQ_ROUTES: u32 = 4096;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For upstreaming: This should be either made generic or gated behind feature = "kvm". What does MSHV do here?


impl InterruptAllocator {
fn new(size: u32, offset: u32) -> Self {
assert_ne!(size, 0);
let bits_per_word = usize::BITS;
let num_words = (size + bits_per_word - 1).div_ceil(bits_per_word);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this way it is even shorter:

let num_words = (size + usize::BITS - 1).div_ceil(usize::BITS);


let mut words = vec![0usize; num_words as usize];
words[(num_words - 1) as usize] = Self::last_word(size);

Self {
words,
size,
offset,
}
}

fn last_word(size: u32) -> usize {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really sure about why you use word here. Isn't this last_irq()?

let rem = size % usize::BITS;
if rem == 0 {
0usize
} else {
!((1usize << rem) - 1)
}
}

fn free(&mut self, vector: u32) -> Result<(), InterruptAllocError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fn free_irq()?

// At first we make sure that the vector is not out of range.
if !(self.offset..self.offset + self.size).contains(&vector) {
return Err(InterruptAllocError::OutOfRange(vector));
}

let idx = vector.abs_diff(self.offset);
let (w, b) = Self::word_and_bit(idx);

let mask = 1usize << b;

// Let's first check whether the bit is set.
if self.words[w] & mask == 0usize {
return Err(InterruptAllocError::AlreadyFree(vector));
}
// Clear the bit and we are done!
self.words[w] &= !mask;
Ok(())
}

fn word_and_bit(vector: u32) -> (usize, usize) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename: fn bitmap_indices(vector: u32) -> (usize /*row */, usize /* col */)

let idx = vector as usize;
let bits = usize::BITS as usize;
(idx / bits, idx & bits)
}

fn alloc(&mut self) -> Result<u32, InterruptAllocError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: fn alloc_irq()

let bits = usize::BITS as usize;
for idx in 0..self.words.len() {
let word = self.words[idx];

// If every bit is set, we can continue.
if word == !0usize {
continue;
}

// Find lowest free bit.
let bit = (!word).trailing_zeros() as usize;
// Set the bit.
self.words[idx] |= 1usize << bit;
// Calculate index, add offset and return.
let vector = idx * bits + bit;
return Ok(vector as u32 + self.offset);
}

Err(InterruptAllocError::ExhaustedError())
}
}

/// GsiAllocator
pub struct GsiAllocator {
#[cfg(target_arch = "x86_64")]
apics: BTreeMap<u32, u32>,
next_irq: u32,
next_gsi: u32,
irqs: Mutex<InterruptAllocator>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this really need to be a mutex? I'd expect the whole GsiAllocator to be locked by callers?

gsis: Mutex<InterruptAllocator>,
}

impl GsiAllocator {
#[cfg(target_arch = "x86_64")]
/// New GSI allocator
pub fn new(apics: &[GsiApic]) -> Self {
let mut allocator = GsiAllocator {
apics: BTreeMap::new(),
next_irq: 0xffff_ffff,
next_gsi: 0,
};
let mut allocator_apics = BTreeMap::new();
let mut next_irq = 0xffff_ffffu32;
let mut next_gsi = 0u32;

for apic in apics {
if apic.base < allocator.next_irq {
allocator.next_irq = apic.base;
if apic.base < next_irq {
next_irq = apic.base;
}

if apic.base + apic.irqs > allocator.next_gsi {
allocator.next_gsi = apic.base + apic.irqs;
if apic.base + apic.irqs > next_gsi {
next_gsi = apic.base + apic.irqs;
}

allocator.apics.insert(apic.base, apic.irqs);
allocator_apics.insert(apic.base, apic.irqs);
}

allocator
Self {
apics: allocator_apics,
irqs: Mutex::new(InterruptAllocator::new(
KVM_MAX_IRQ_ROUTES - next_irq,
next_irq,
)),
gsis: Mutex::new(InterruptAllocator::new(
KVM_MAX_IRQ_ROUTES - next_gsi,
next_gsi,
)),
}
}

#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
/// New GSI allocator
pub fn new() -> Self {
GsiAllocator {
next_irq: arch::IRQ_BASE,
next_gsi: arch::IRQ_BASE,
irqs: Mutex::new(InterruptAllocator::new(
KVM_MAX_IRQ_ROUTES - arch::IRQ_BASE,
arch::IRQ_BASE,
)),
gsis: Mutex::new(InterruptAllocator::new(
KVM_MAX_IRQ_ROUTES - arch::IRQ_BASE,
arch::IRQ_BASE,
)),
}
}

/// Allocate a GSI
pub fn allocate_gsi(&mut self) -> Result<u32> {
let gsi = self.next_gsi;
self.next_gsi = self.next_gsi.checked_add(1).ok_or(Error::Overflow)?;
Ok(gsi)
pub fn allocate_gsi(&mut self) -> Result<u32, InterruptAllocError> {
self.gsis.lock().unwrap().alloc()
}

/// Frees a GSI
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit/idea: we could also totally over-engineer this:

Each Gsi could be a smart object with a Weak reference to the allcoator. Once the element drops, it removes itself from the GsiAllocator`

Copy link
Author

@amphi amphi Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we could do that. But I am unsure whether we should do that.

Sounds totally over engineered, but on the other hand this would solve the question when to free the interrupts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the current design is fine. We already have InterruptLine with drop semantics.

pub fn free_gsi(&mut self, vector: u32) -> Result<(), InterruptAllocError> {
self.gsis.lock().unwrap().free(vector)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

about commit vm-allocator: free GSIs when they are no longer used

[...] That way we shouldn't exhaust the [...]

Be more confident ;) Replace shouldn't with won't

}

#[cfg(target_arch = "x86_64")]
/// Allocate an IRQ
pub fn allocate_irq(&mut self) -> Result<u32> {
let mut irq: u32 = 0;
pub fn allocate_irq(&mut self) -> Result<u32, InterruptAllocError> {
let next_irq = self.irqs.lock().unwrap().alloc()?;
for (base, irqs) in self.apics.iter() {
// HACKHACK - This only works with 1 single IOAPIC...
if self.next_irq >= *base && self.next_irq < *base + *irqs {
irq = self.next_irq;
self.next_irq += 1;
if next_irq >= *base && next_irq < *base + *irqs {
return Ok(next_irq);
}
}

if irq == 0 {
return Err(Error::Overflow);
}

Ok(irq)
self.irqs.lock().unwrap().free(next_irq)?;
Err(InterruptAllocError::ExhaustedError())
}

#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
/// Allocate an IRQ
pub fn allocate_irq(&mut self) -> Result<u32> {
let irq = self.next_irq;
self.next_irq = self.next_irq.checked_add(1).ok_or(Error::Overflow)?;
Ok(irq)
pub fn allocate_irq(&mut self) -> Result<u32, InterruptAllocError> {
self.irqs.lock().unwrap().alloc()
}
}

Expand Down
2 changes: 1 addition & 1 deletion vm-allocator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ pub mod page_size;
mod system;

pub use crate::address::AddressAllocator;
pub use crate::gsi::GsiAllocator;
#[cfg(target_arch = "x86_64")]
pub use crate::gsi::GsiApic;
pub use crate::gsi::{GsiAllocator, InterruptAllocError};
pub use crate::system::SystemAllocator;
mod memory_slot;
pub use memory_slot::MemorySlotAllocator;
27 changes: 16 additions & 11 deletions vm-allocator/src/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
use vm_memory::{GuestAddress, GuestUsize};

use crate::address::AddressAllocator;
use crate::gsi::GsiAllocator;
#[cfg(target_arch = "x86_64")]
use crate::gsi::GsiApic;
use crate::gsi::{GsiAllocator, InterruptAllocError};
use crate::page_size::get_page_size;

/// Manages allocating system resources such as address space and interrupt numbers.
Expand All @@ -31,17 +31,17 @@ use crate::page_size::get_page_size;
/// GuestAddress(0x10000000), 0x10000000,
/// #[cfg(target_arch = "x86_64")] &[GsiApic::new(5, 19)]).unwrap();
/// #[cfg(target_arch = "x86_64")]
/// assert_eq!(allocator.allocate_irq(), Some(5));
/// assert_eq!(allocator.allocate_irq().unwrap(), 5);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the old assert_eq! scheme!

assert_eq!(allocator.allocate_irq(), Some(5))

provides much better error output if something is wrong

/// #[cfg(target_arch = "aarch64")]
/// assert_eq!(allocator.allocate_irq(), Some(32));
/// assert_eq!(allocator.allocate_irq().unwrap(), 32);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

/// #[cfg(target_arch = "riscv64")]
/// assert_eq!(allocator.allocate_irq(), Some(0));
/// assert_eq!(allocator.allocate_irq().unwrap(), 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

/// #[cfg(target_arch = "x86_64")]
/// assert_eq!(allocator.allocate_irq(), Some(6));
/// assert_eq!(allocator.allocate_irq().unwrap(), 6);
/// #[cfg(target_arch = "aarch64")]
/// assert_eq!(allocator.allocate_irq(), Some(33));
/// assert_eq!(allocator.allocate_irq().unwrap(), 33);
/// #[cfg(target_arch = "riscv64")]
/// assert_eq!(allocator.allocate_irq(), Some(1));
/// assert_eq!(allocator.allocate_irq().unwrap(), 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

/// assert_eq!(allocator.allocate_platform_mmio_addresses(None, 0x1000, Some(0x1000)), Some(GuestAddress(0x1fff_f000)));
///
/// ```
Expand Down Expand Up @@ -82,13 +82,18 @@ impl SystemAllocator {
}

/// Reserves the next available system irq number.
pub fn allocate_irq(&mut self) -> Option<u32> {
self.gsi_allocator.allocate_irq().ok()
pub fn allocate_irq(&mut self) -> Result<u32, InterruptAllocError> {
self.gsi_allocator.allocate_irq()
}

/// Reserves the next available GSI.
pub fn allocate_gsi(&mut self) -> Option<u32> {
self.gsi_allocator.allocate_gsi().ok()
pub fn allocate_gsi(&mut self) -> Result<u32, InterruptAllocError> {
self.gsi_allocator.allocate_gsi()
}

/// Frees the given GSI.
pub fn free_gsi(&mut self, vector: u32) -> Result<(), InterruptAllocError> {
self.gsi_allocator.free_gsi(vector)
}

/// Reserves a section of `size` bytes of IO address space.
Expand Down
4 changes: 2 additions & 2 deletions vmm/src/device_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ use virtio_devices::{
AccessPlatformMapping, ActivateError, Block, Endpoint, IommuMapping, PostMigrationAnnouncer,
VdpaDmaMapping, VirtioMemMappingSource,
};
use vm_allocator::{AddressAllocator, SystemAllocator};
use vm_allocator::{AddressAllocator, InterruptAllocError, SystemAllocator};
use vm_device::dma_mapping::ExternalDmaMapping;
use vm_device::interrupt::{
InterruptIndex, InterruptManager, LegacyIrqGroupConfig, MsiIrqGroupConfig,
Expand Down Expand Up @@ -274,7 +274,7 @@ pub enum DeviceManagerError {

/// Cannot allocate IRQ.
#[error("Cannot allocate IRQ")]
AllocateIrq,
AllocateIrq(#[from] InterruptAllocError),

/// Cannot configure the IRQ.
#[error("Cannot configure the IRQ")]
Expand Down
17 changes: 13 additions & 4 deletions vmm/src/interrupt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,23 @@ struct InterruptRoute {
gsi: u32,
irq_fd: EventFd,
registered: AtomicBool,
allocator: Arc<Mutex<SystemAllocator>>,
}

impl InterruptRoute {
pub fn new(allocator: &mut SystemAllocator) -> Result<Self> {
pub fn new(allocator: Arc<Mutex<SystemAllocator>>) -> Result<Self> {
let irq_fd = EventFd::new(libc::EFD_NONBLOCK)?;
let gsi = allocator
.lock()
.unwrap()
.allocate_gsi()
.ok_or_else(|| io::Error::other("Failed allocating new GSI"))?;
.map_err(|e| io::Error::other(format!("Failed allocating new GSI: {e}")))?;

Ok(InterruptRoute {
gsi,
irq_fd,
registered: AtomicBool::new(false),
allocator,
})
}

Expand Down Expand Up @@ -77,6 +81,12 @@ impl InterruptRoute {
}
}

impl Drop for InterruptRoute {
fn drop(&mut self) {
let _ = self.allocator.lock().unwrap().free_gsi(self.gsi);
}
}

pub struct RoutingEntry {
route: IrqRoutingEntry,
masked: bool,
Expand Down Expand Up @@ -292,11 +302,10 @@ impl InterruptManager for MsiInterruptManager {
type GroupConfig = MsiIrqGroupConfig;

fn create_group(&self, config: Self::GroupConfig) -> Result<Arc<dyn InterruptSourceGroup>> {
let mut allocator = self.allocator.lock().unwrap();
let mut irq_routes: HashMap<InterruptIndex, InterruptRoute> =
HashMap::with_capacity(config.count as usize);
for i in config.base..config.base + config.count {
irq_routes.insert(i, InterruptRoute::new(&mut allocator)?);
irq_routes.insert(i, InterruptRoute::new(self.allocator.clone())?);
}

Ok(Arc::new(MsiInterruptGroup::new(
Expand Down
Loading
Loading