-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Interrupt abstractions #71
Conversation
src/interrupt/legacy.rs
Outdated
use crate::interrupt::ConfigurableInterrupt; | ||
|
||
/// Definition for PCI INTx pins. | ||
#[derive(Copy, Clone, Debug)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just been reading about some good practices in Rust, and was thinking to start applying it going forward. One suggestion from there is to eagerly implement common traits. Do you mind adding at least the ones that you can derive from the list? PartialEq
is super useful with testing for example. This applies to all the other structure definitions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added common traits where applicable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, the rust-vmm crates all violate the snake case naming scheme proposed by https://rust-lang.github.io/api-guidelines/naming.html. I guess this is not fixable anymore?
fb4696d
to
3d87a2d
Compare
Please adjust test coverage to make CI happy, otherwise LGTM. |
@jiangliu @studychao could you please also take a look at the PR, thanks! |
} | ||
|
||
/// Disable generation of interrupts from this interrupt source. | ||
fn disable(&self) -> Result<()> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This trait mixes up multiple aspects of an interrupt:
- The part that is seen by a device (trigger)
- A super-trait of Interrupt where the underlying notifier (eventfd) can be extracted (notifier)
- The part that is operated by interrupt controllers (enable, disable, ack?)
These should be separate traits to simplify implementation and re-usability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think separating the notifier related functionality would improve the interface at least for the cases where the notifiers are not available.
For enable/disable I think moving them to another trait has diminishing returns. It's true that those methods are related to interrupt control but that can come either from VMM code or from a device crate code (e.g. a PCI crate which handles the parsing of MSI configuration).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved the notifier methods to a different trait. Please take a look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For enable/disable I think moving them to another trait has diminishing returns. It's true that those methods are related to interrupt control but that can come either from VMM code or from a device crate code (e.g. a PCI crate which handles the parsing of MSI configuration).
I see this from the point of what I have to implement for example to unit test a device that toggles interrupts. A simple device would take an interrupt pin as an input, which can be anything that implements the trigger
method. When I use a mock of this for unit testing with the current trait, I have to implement lots of unrelated things that the device should not even be able to call at all.
So in my mind it's very important to separate the two sides: One set of functions that the device is supposed to use. One set of functions that the interrupt controller is supposed to use. These should be two traits that also have no relation to each other. That you can implement them on the same struct is an implementation detail.
Or to put it a different way: The device should only be able to use interfaces that it is actually supposed to be using. This prevents people who may not fully understand the interrupt logic from using the interfaces the wrong way, which is a big win.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe as clarification: We are currently using rust-vmm code for parts of our virtualization stack at Cyberus Technology internally. We are interested in making it more generic and thus a bit more friendly to hypervisors that don't necessarily work like KVM. :)
This is also a reason why I'm a bit concerned about mixing up concerns in trait implementations and keeping it simple, straightforward and without guess work for the implementor of traits, because this has already caused some churn for us when we onboarded with vm-memory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha. I think it's good to have the separation for the use cases you mentioned. My simplifying assumption was that most implementations would mix both pure emulation functionality and some interrupt control code so it would be more easier to pass Interrupt objects that have more optional functionality in one larger interface.
But I'm definitely not against splitting them up. Here's what I propose (also merging the discussion about level-interrupts below):
- Interrupt traits used by device emulation to generate notifications:
pub trait EdgeInterrupt {
fn trigger(&self) -> Result<()>;
}
pub trait LevelInterrupt {
fn assert(&self) -> Result<()>;
fn de_assert(&self) -> Result<()>;
}
- Interrupt control traits used by interrupt controllers and interrupt configuration code:
pub trait Interrupt {
/// Enable generation of interrupts from this interrupt source.
fn enable(&self) -> Result<()>;
/// Disable generation of interrupts from this interrupt source.
fn disable(&self) -> Result<()>;
}
pub trait ConfigurableInterrupt: Interrupt {
/// Type describing the configuration spec of the interrupt.
type Cfg;
/// Update configuration of the interrupt.
fn update(&self, config: &Self::Cfg) -> Result<()>;
/// Returns the current configuration of the interrupt.
fn get_config(&self) -> Result<Self::Cfg>;
}
/// Trait for interrupts that can be masked or unmasked.
pub trait MaskableInterrupt: Interrupt {
fn mask(&self) -> Result<()>;
fn unmask(&self) -> Result<()>;
}
/// Trait implemented by devices through which a resampled interrupt that was cleared
/// can determine if the device still requires service
pub trait InterruptStatusChecker {
fn is_active(&self) -> bool;
}
/// Trait for Interrupts that can be automatically retriggered when resampled
/// Uses InterruptStatusChecker
pub trait AutoRetriggerInterrupt: Interrupt {
fn set_status_checker(&self, Arc<dyn InterruptStatusChecker>) -> Result<()>;
}
pub trait InterruptSourceGroup: Send {
/// [...]
}
- Conversion traits to get the underlying notifiers (for the irqfd use-case)
pub trait AsRefTriggerNotifier {
type NotifierType;
fn trigger_notifier(&self) -> &Self::NotifierType;
}
pub trait AsRefResampleNotifier {
type NotifierType;
fn resample_notifier(&self) -> &Self::NotifierType;
}
@blitz WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand the difference between enable/disable and mask/unmask, but in general I think this is pretty good. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This suggestion looks good to me. The only thing I'd like to change is the de_assert()
method name. Maybe unassert()
or clear()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/interrupt/mod.rs
Outdated
None | ||
} | ||
|
||
/// Called back when the CPU acknowledges the interrupt. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this mean? Is it called when the interrupt is logged into the LAPIC IRR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to update the doc a bit here. It's actually an EOI callback and it would be used for shared level-sensitive interrupts in order to inject the interrupt if the device still requires it (e.g. for INTx interrupts). It's equivalent to the ack_notifier()
the difference being that the latter would enable the resampled semnatics with external components like vfio.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For level-triggered interrupts the trigger
method seems not to be the right abstraction to me. In this case, the device would need a mechanism to assert/de-assert the interrupt line for correct emulation. Optimizations to avoid assert/deassert messages when the interrupt cannot be injected anyway would be a second step.
In any case, the flow for handling level triggered interrupts should be documented. I wouldn't have seen how this is supposed to work from the code alone.
d23438b
to
829f1c5
Compare
829f1c5
to
2dcca7a
Compare
Add a series of Traits and structs for defining the interface for interrupts. The goal is to allow individual development of device crates that can work with different types of interrupt mechanisms. The interrupt mechanism can be implemented in the VMM based on the platform or the available hardware (e.g. CPU, Interrupt controller). These set of traits allow devices to use a simple interface through which they can request, release, configure interrupts and also signal events to the VMM. Signed-off-by: Alexandru-Cezar Sardan <alsardan@amazon.com>
Add a brief description of the design of the Interrupt abstractions. Signed-off-by: Alexandru-Cezar Sardan <alsardan@amazon.com>
Interrupt abstractions currently don't have unit tests so coverage is decreased. Signed-off-by: Alexandru-Cezar Sardan <alsardan@amazon.com>
2dcca7a
to
be03951
Compare
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] | ||
pub struct MsiIrqConfig { | ||
/// High address to delivery message signaled interrupt. | ||
pub high_addr: u32, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not pub addr: u64
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This follows the PCI spec. See sections 6.8.1.4, 6.8.1.5 from here https://www.cl.cam.ac.uk/~djm202/pdf/specifications/pci/PCI_LB3.0_CB-2-6-04.pdf
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The spec describes a 64-bit Message Address that is represented in MSI-X as hi/lo 32-bit parts. It's not really important, but it's weird to represent it here as 32-bit hi/lo parts as well, when you can just as well make it the 64-bit address that it is.
MSIs are normal data writes on the bus. So when they talk of "address" they literally mean an address where the device will write what's in the data field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything looks documented, but I still don't feel like I understand the usecases that drive the complexity.
For devices that want to trigger MSIs, how does the interface look for them. Is there an example device somewhere?
/// The group allows a device to request and release interrupts and perform actions on the | ||
/// whole collection of interrupts like enable and disable for cases where enabling or disabling | ||
/// a single interrupt in the group does not make sense. For example, PCI MSI interrupts must be | ||
/// enabled as a group. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a KVM limitation or something that comes as a requirement from PCI passthrough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MSI or MSI-x is enabled by setting the enable bit in the message control struct. This refers to the whole set of MSI interrupts. You can mask each individual interrupt but you can't enable/disable them individually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a functional difference between masked and disabled MSI-X vectors from the interrupt subsystems perspective?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some controllers may have different registers to distinguish masked vs enabled interrupts. Take a look here: https://docs.microsoft.com/en-us/windows-hardware/drivers/gpio/gpio-interrupt-masks
There may also be functional differences in what happens when a masked interrupt is asserted. In some cases interrupt is not delivered when asserted until it is unmasked, whereas asserting a disabled interrupt should not keep the pending state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This document describes behavior of a (GPIO) interrupt controller. So what you are saying is that the implementation of this trait needs to know what kind of interrupt controller it is connected to. This is fine, I'm just trying to wrap my head around things. :)
fn allocate_interrupts(&mut self, size: usize) -> Result<()>; | ||
|
||
/// Release all interrupts within this group. | ||
fn free_interrupts(&mut self) -> Result<()>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When would allocate and free be called? Am I allowed to call any of the other methods when the group has not been allocated or already freed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
allocate_interrupts
and free_interrupts
should be called from interrupt setup code to reserve/release interrupts.
Typically you would return an error when enabled
is called on an empty group in the trait implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be called from interrupt setup code
What setup code are you referring to here? The whole "flow" of this API is not really clear to me yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is from the virtio code you linked to:
let mut interrupt_group = vmm.interrupt_manager.get_new_legacy_group().unwrap();
interrupt_group.allocate_interrupts(1).unwrap();
let irq = interrupt_group.get(0).unwrap();
irq.update(&LegacyIrqConfig {
interrupt_line: None,
interrupt_pin: None,
})
.unwrap();
let irq_config = irq.get_config().unwrap();
interrupt_group.enable().unwrap();
This is frankly a terrible interface for legacy interrupt pins and should not end up in code that is intended for use in other VMMs. It has multi-step construction, it violates the "simple things should be simple" goal and the code is completely non-obvious:
- Why do you need to "allocate" after getting the "group" (for one interrupt?) from the manager?
- Why do need to "update" the group with basically no information right after creating it?
- Why do you need to
get_config
the config right after sticking the empty config into it? - Why do you need to "enable" the interrupt?
- Why is it ok to unwrap all the results?
My advice here is to check out the interrupt setup in existing emulators/VMMs, for example qemu. It is very readable and this should especially be the goal for re-usable code.
Maybe the way forward is to merge this and iterate on it, but from my experience interfaces rarely become cleaner over time. I don't think I can contribute more here, unfortunately.
pub trait MsiInterrupt: ConfigurableInterrupt<Cfg = MsiIrqConfig> + MaskableInterrupt {} | ||
|
||
/// Blanket implementation for Interrupts that use a MsiIrqConfig. | ||
impl<T> MsiInterrupt for T where T: ConfigurableInterrupt<Cfg = MsiIrqConfig> + MaskableInterrupt {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have really no idea how this would be used by a device. Is there an example device to look at somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is used for convenience. It becomes tedious to pass around a long list of trait bounds for well known interrupt types.
I currently removed the example to keep this PR more focused following the discussion here #71 (comment). |
I think it's fine to keep the example out of this PR to increase focus, but there should be a matching device somewhere to understand how these interfaces are supposed to be used. For the pin-based interrupts everything is reasonably simple, I would still like to understand how the MSI interfaces are meant to be used though. |
pub struct LegacyIrqConfig { | ||
/// Input of the system interrupt controllers the device's interrupt pin is connected to. | ||
/// Implemented by any device that makes use of an interrupt pin. | ||
pub interrupt_line: Option<u32>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this be used by PCI devices to set their interrupt line field or are there other uses? Typically, the interrupt line field in the config space is just a normal R/W field that is written during boot by firmware. So device emulation should not be concerned with this value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a PCI device using an INTx interrupt interrupt_line may not be of significance for device emulation. But this value is of significance for setting up other components like the MPtable.
Also you would use interrupt_line
to configure a specific line that you want the interrupt assigned to (see https://github.com/alsrdn/firecracker/blob/ab0c9642162f8c749c516d02b43b144a91baa33e/src/vmm/src/device_manager/legacy.rs#L89).
For MSI it works the same way:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point it's better to merge the PR, and iterate on the interfaces/responsibilities in a future release to unblock the future work.
This PR addresses requirements from #68 and adds interrupt abstractions to vm-device allowing devices to be developed independently from the vmm and to use a common interface for configuring and triggering interrupts.
This PR is a simplified version of #23 and #21 that allows for defining of custom behaviour in the VMM for different interrupt types and also interrupt groups and also allows for easy extension and creation of other interrupt types in the future by combining Interrupt super traits and configuration types.
I've included a small example on the usage of the traits.
Another example on how it might be used in Firecracker can be found in these draft commits: alsrdn/firecracker@e1d6b7b and alsrdn/firecracker@d59bd41