Skip to content
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

Improve vqueue encapsulation - prerequisite for vhost work #4312

Closed
wants to merge 2 commits into from

Conversation

majek
Copy link

@majek majek commented Dec 12, 2023

Changes

This PR refactors virtio.Queue and VirtioDevice.queues() interfaces in order to better encapsulate the vqueue.

Reason

This PR is part of #3707 issue, where we're trying to speed up Firecracker networking with vhost. From Firecracker point of view, Linux vhost interface is basically another implementation of virtqueue (or Virtual Queue) from Virtio spec. In order to integrate vhost into firecracker in the future we need more flexibility around vqueue implementation.

However, this is not easy. Firecracker virtio.Queue is deeply integrated into the code. It's imported and directly used from all the device implementations. In order to make it more plugable, we need to put some abstractions around it.

This PR creates a foundation for that. We don't change any functionality of Firecracker, just refactor the virtio.Queue interface a bit. These changes are necessary to create decent vhost code in the future, otherwise the spaghetti explodes fast.

We believe these changes are not controversial and generally improve the abstractions and code cleanliness.

License Acceptance

By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache 2.0 license. For more information on following
Developer Certificate of Origin and signing off your commits, please check
CONTRIBUTING.md.

PR Checklist

  • If a specific issue led to this PR, this PR closes the issue.
  • The description of changes is clear and encompassing.
  • Any required documentation changes (code and docs) are included in this PR.
  • API changes follow the Runbook for Firecracker API changes.
  • User-facing changes are mentioned in CHANGELOG.md.
  • All added/changed functionality is tested.
  • New TODOs link to an issue.
  • Commits meet contribution quality standards.

  • This functionality cannot be added in rust-vmm.

@majek majek changed the title Marek/prepare vqueue Improve vqueue encapsulation - prerequisite for vhost work Dec 12, 2023
@majek
Copy link
Author

majek commented Dec 12, 2023

Pulling in @bchalios @kalyazin. Let me know what you think.

Copy link

codecov bot commented Dec 12, 2023

Codecov Report

Attention: 11 lines in your changes are missing coverage. Please review.

Comparison is base (da60282) 81.65% compared to head (a321d24) 81.69%.
Report is 40 commits behind head on main.

Files Patch % Lines
.../vmm/src/devices/virtio/vhost_user_block/device.rs 0.00% 4 Missing ⚠️
src/vmm/src/devices/virtio/balloon/device.rs 50.00% 2 Missing ⚠️
src/vmm/src/devices/virtio/virtio_block/device.rs 50.00% 2 Missing ⚠️
src/vmm/src/devices/virtio/vsock/device.rs 50.00% 2 Missing ⚠️
src/vmm/src/devices/virtio/mmio.rs 92.85% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4312      +/-   ##
==========================================
+ Coverage   81.65%   81.69%   +0.03%     
==========================================
  Files         240      240              
  Lines       29367    29400      +33     
==========================================
+ Hits        23981    24017      +36     
+ Misses       5386     5383       -3     
Flag Coverage Δ
4.14-c5n.metal ?
4.14-c7g.metal 77.12% <87.91%> (+0.04%) ⬆️
4.14-m5d.metal 79.01% <87.91%> (+0.03%) ⬆️
4.14-m5n.metal ?
4.14-m6a.metal 78.12% <87.91%> (+0.03%) ⬆️
4.14-m6g.metal 77.12% <87.91%> (+0.04%) ⬆️
4.14-m6i.metal 78.99% <87.91%> (+0.03%) ⬆️
4.14-m7g.metal ?
5.10-c5n.metal ?
5.10-c7g.metal 80.01% <87.91%> (+0.03%) ⬆️
5.10-m5d.metal 81.67% <87.91%> (+0.02%) ⬆️
5.10-m5n.metal ?
5.10-m6a.metal 80.88% <87.91%> (+0.03%) ⬆️
5.10-m6g.metal 80.01% <87.91%> (+0.03%) ⬆️
5.10-m6i.metal 81.65% <87.91%> (+0.03%) ⬆️
5.10-m7g.metal ?
6.1-c5n.metal ?
6.1-c7g.metal 80.01% <87.91%> (+0.03%) ⬆️
6.1-m5d.metal 81.66% <87.91%> (+0.01%) ⬆️
6.1-m5n.metal ?
6.1-m6a.metal 80.88% <87.91%> (+0.03%) ⬆️
6.1-m6g.metal 80.01% <87.91%> (+0.03%) ⬆️
6.1-m6i.metal 81.65% <87.91%> (+0.03%) ⬆️
6.1-m7g.metal ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@bchalios
Copy link
Contributor

Thanks a lot for the contribution @majek.

My main comment has to do with this:

From Firecracker point of view, Linux vhost interface is basically another implementation of virtqueue (or Virtual Queue) from Virtio spec

I 'm interested to understand why you think that is the case. It is true that with vhost-net we will rely on Linux's implementation for the virtqueues, but IMHO the current Firecracker implementation provides enough interfaces to setup the Linux machinery. It is true that after that we won't need to use the Firecracker's queue implementation but I just can't understand how the changes in this PR make it easier to drive vhost-net.

@majek
Copy link
Author

majek commented Dec 19, 2023

@bchalios Absolutely. The essence of the change is:

pub trait VirtioDevice: AsAny {
...
    /// Returns the device queues.
-    fn queues(&self) -> &[Queue];
+    fn queues(&self) -> QueueIter;

VirtioDevice is implemented by all the devices firecracker supports. There are at least two "customers" for that interface. First is virtio / firecracker device stack. This is the part we want to keep on using in vhost case. We want device setup, management, mmio registers, interrupts, all of these things VirtioDevice / mmio.rs do well and there is no point in changing this.

Then there is the second user of VirtioDevice, which is device driver internally. The device driver wants to, roughly, queue and dequeue descriptors on the vqueue. For example net driver does this - it pops/pushes data onto vqueue. For vhost we don't want that. In vhost case the data path of the queue is transparent and there is just no implementation of .pop().

Therefore we can't just return &[Queue] in vhost case. We need vhost-specific implementation there.

In next PR we'll propose an enum over QueueIter, that is able to expose the two implementations of vqueue to the users of VirtioDevice like mmio.rs:

-pub type QueueIter<'a> = std::slice::Iter<'a, Queue>;
-pub type QueueIterMut<'a> = std::slice::IterMut<'a, Queue>;

+#[derive(Debug)]
+pub struct QueueIter<'a> {
 +   inner: QueueIterInner<'a>,
+}
+
+#[derive(Debug)]
+enum QueueIterInner<'a> {
+    Virtio(std::slice::Iter<'a, Queue>),
+    Vhost(std::slice::Iter<'a, VhostQueue>),
+}

Does this make more sense now?

In summary: we're going to propose an enum over QueueIter in a subsequent PR. Even if the enum is controversial, change in this PR is still beneficial, as it draws clearer boundary between Queue and mmio.rs, consider this:

-                    0x44 => self.with_queue(0, |q| u32::from(q.ready)),
+                    0x44 => self.with_queue(0, |q| u32::from(q.ready())),

Pretty trivial, but allows Queue to be abstracted away in the future. Without this abstraction doing any changes to Queue implementation is impossible.

@roypat
Copy link
Contributor

roypat commented Dec 21, 2023

Hey all,
If I'm understanding correctly, the problem we're trying to solve here is that Firecracker's current struct Queue contains both the virtio protocol configuration fields that are common to the existing devices and future vhost devices, and then also bits related to actually processing virtio requests that will not be needed by vhost devices (because the processing happens outside of Firecracker there). I think we can solve this problem in a slightly simpler way that does not require getters/setters (I'll get into why I don't think these should happen at the end), enums, or a struct VhostQueue. Essentially, my proposal would be

Diff to queue.rs/device.rs

(please note that this diff is minimal to bring across the idea, it doesnt include all the related bits needed to get a compiling build)

diff --git a/src/vmm/src/devices/virtio/device.rs b/src/vmm/src/devices/virtio/device.rs
index 80135576e..2ca50ce1f 100644
--- a/src/vmm/src/devices/virtio/device.rs
+++ b/src/vmm/src/devices/virtio/device.rs
@@ -12,7 +12,7 @@ use std::sync::Arc;
 use utils::eventfd::EventFd;
 
 use super::mmio::{VIRTIO_MMIO_INT_CONFIG, VIRTIO_MMIO_INT_VRING};
-use super::queue::Queue;
+use super::queue::{Queue, QueueConfig};
 use super::ActivateError;
 use crate::devices::virtio::AsAny;
 use crate::logger::{error, warn};
@@ -110,11 +110,8 @@ pub trait VirtioDevice: AsAny + Send {
     /// The virtio device type.
     fn device_type(&self) -> u32;
 
-    /// Returns the device queues.
-    fn queues(&self) -> &[Queue];
-
-    /// Returns a mutable reference to the device queues.
-    fn queues_mut(&mut self) -> &mut [Queue];
+    fn nth_queue_config(&self, n: u32) -> Option<&QueueConfig>;
+    fn nth_queue_config_mut(&mut self, n: u32) -> Option<&mut QueueConfig>;
 
     /// Returns the device queues event fds.
     fn queue_events(&self) -> &[EventFd];
diff --git a/src/vmm/src/devices/virtio/queue.rs b/src/vmm/src/devices/virtio/queue.rs
index 45cf6349f..6cb9da1a4 100644
--- a/src/vmm/src/devices/virtio/queue.rs
+++ b/src/vmm/src/devices/virtio/queue.rs
@@ -176,12 +176,7 @@ impl<'a> Iterator for DescriptorIterator<'a> {
     }
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
-/// A virtio queue's parameters.
-pub struct Queue {
-    /// The maximal size in elements offered by the device
-    pub(crate) max_size: u16,
-
+pub struct QueueConfig {
     /// The queue size in elements the driver selected
     pub size: u16,
 
@@ -196,6 +191,15 @@ pub struct Queue {
 
     /// Guest physical address of the used ring
     pub used_ring: GuestAddress,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+/// A virtio queue's parameters.
+pub struct Queue {
+    pub config: QueueConfig,
+
+    /// The maximal size in elements offered by the device
+    pub(crate) max_size: u16,
 
     pub(crate) next_avail: Wrapping<u16>,
     pub(crate) next_used: Wrapping<u16>,

The idea here is that, similar to a vhost device, the mmio code doesnt need to know about the Firecracker specific bits of our queue implementation. By factoring this part out into a common struct, and have the abstract iterface of VirtioDevice only know about VirtioConfig, we achieve the same goal of encapsulating Firecracker-devices-specific queue fields away from mmio code and vhost devices (e.g. in this world, a vhost device would not store a Vec<Queue>, like Firecracker's traditional virtio devices to, but rather a Vec<VirtioConfig>).

The change in interface from fn queue_configs() -> &[QueueConfig] to fn nth_queue_config(&self, n: usize) -> Option<&QueueConfig> is a bit unfortunate, but we cannot get a &[QueueConfig] from a Vec<Queue>. There will probably be a need for some sort of iterator API, á la

trait VirtioDevice {
    ...

    fn queue_iterator(&self) -> QueueIterator<'_> {
        QueueIterator(self, 0)
    }
}

struct QueueIterator<'a>(&'a dyn VirtioDevice, usize);
impl<'a> Iterator for QueueIterator<'a> {
    type Item = &'a QueueConfig;
    fn next(&mut self) -> Option<Self::Item> {
        let ret = self.0.nth_queue_config(self.1);
        self.1 += 1;
        ret
    }
}

but in general, I think it'd be a cleaner way to solve this problem.

As for getters and setters, I think they are generally considered unidiomatic in Rust, as unlike in most other languages, they are not a zero-cost abstraction. Only exposing a getter/setter looses expressiveness, as they prohibit pattern matching, and taking ownership of multiple struct fields at once. This is because in Rust, there are 3(ish) ways to "get" some field: Taking ownership and moving out through some fn take_x(self) -> X { self.x }, by immutable reference fn get_x(&self) -> &X {&self.x} and by mutable reference fn get_x_mut(&mut self) -> &mut X {&mut self.x} (but this one blurs the line to "setter"). Yet none of them allow me to pattern match multiple fields, or to move multiple fields out of the struct at once. I'm not saying that we'd run into these restrictions here, but its why I would generally choose direct field access over setters and getters in Rust, especially for "data only" types like struct VirtioConfig.

@kornelski
Copy link
Contributor

We've actually initially implemented the "get nth queue" approach first, but eventually moved away from it, because it turned out to be less usable.

This is because it's not possible to build an Iterator on top of it without use of unsafe. Repeated calls to a getter don't give a guarantee that items' lifetimes are disjoint, and such method works as a streaming iterator abstraction, but not Rust Iterator. OTOH the Iterator is more flexible, and has an nth() method too.


I agree that getters in Rust are not ideal, but once we have multiple queue implementations we'd use dyn Queue as the iterator item, which requires getters, because Rust doesn't support fields in traits. Fortunately fields on the Queue are small and copyable, so problems with simultaneous access to multiple fields don't happen.

@majek
Copy link
Author

majek commented Feb 15, 2024

Ok, I think we can avoid this abstractions if we reuse the block-vhost-user design.

@majek majek closed this Feb 15, 2024
@majek
Copy link
Author

majek commented Feb 19, 2024

Followup #4461

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants