From 5caff0a8ad42e7e17b911ea5010c8b6191e9249b Mon Sep 17 00:00:00 2001 From: Viresh Kumar Date: Tue, 15 Jun 2021 17:12:53 +0530 Subject: [PATCH] gpio: Add boilerplate code This adds the boilerplate code and its dependencies to get the basic structure for the gpio crate. Signed-off-by: Viresh Kumar --- Cargo.toml | 1 + gpio/Cargo.toml | 24 ++++++ gpio/README.md | 1 + gpio/src/backend.rs | 192 +++++++++++++++++++++++++++++++++++++++++++ gpio/src/gpio.rs | 51 ++++++++++++ gpio/src/main.rs | 14 ++++ gpio/src/vhu_gpio.rs | 147 +++++++++++++++++++++++++++++++++ 7 files changed, 430 insertions(+) create mode 100644 gpio/Cargo.toml create mode 100644 gpio/README.md create mode 100644 gpio/src/backend.rs create mode 100644 gpio/src/gpio.rs create mode 100644 gpio/src/main.rs create mode 100644 gpio/src/vhu_gpio.rs diff --git a/Cargo.toml b/Cargo.toml index 92b4fd0..faee2f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ + "gpio", "i2c", "rng", ] diff --git a/gpio/Cargo.toml b/gpio/Cargo.toml new file mode 100644 index 0000000..1a5c214 --- /dev/null +++ b/gpio/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "vhost-device-gpio" +version = "0.1.0" +authors = ["Viresh Kumar "] +description = "vhost gpio backend device" +repository = "https://github.com/rust-vmm/vhost-device" +readme = "README.md" +keywords = ["gpio", "vhost", "virt", "backend"] +license = "Apache-2.0 OR BSD-3-Clause" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = ">=3.0", features = ["derive"] } +env_logger = ">=0.9" +libc = ">=0.2.95" +log = ">=0.4.6" +thiserror = "1.0" +vhost = { version = "0.4", features = ["vhost-user-slave"] } +vhost-user-backend = "0.3" +virtio-bindings = ">=0.1" +vm-memory = "0.7" +vmm-sys-util = "=0.9.0" diff --git a/gpio/README.md b/gpio/README.md new file mode 100644 index 0000000..5c6067a --- /dev/null +++ b/gpio/README.md @@ -0,0 +1 @@ +# vhost-device-gpio - GPIO emulation backend daemon diff --git a/gpio/src/backend.rs b/gpio/src/backend.rs new file mode 100644 index 0000000..debcc2d --- /dev/null +++ b/gpio/src/backend.rs @@ -0,0 +1,192 @@ +// VIRTIO GPIO Emulation via vhost-user +// +// Copyright 2022 Linaro Ltd. All Rights Reserved. +// Viresh Kumar +// +// SPDX-License-Identifier: Apache-2.0 + +use log::{info, warn}; +use std::convert::TryFrom; +use std::num::ParseIntError; +use std::sync::{Arc, RwLock}; +use std::thread::spawn; + +use clap::Parser; +use thiserror::Error as ThisError; +use vhost::{vhost_user, vhost_user::Listener}; +use vhost_user_backend::VhostUserDaemon; +use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap}; + +use crate::gpio::{GpioController, GpioDevice, PhysDevice}; +use crate::vhu_gpio::VhostUserGpioBackend; + +pub(crate) type Result = std::result::Result; + +#[derive(Debug, PartialEq, ThisError)] +/// Errors related to low level GPIO helpers +pub enum Error { + #[error("Invalid socket count: {0}")] + SocketCountInvalid(usize), + #[error("Socket count ({0}) doesn't match device count {1}")] + DeviceCountMismatch(usize, usize), + #[error("Duplicate device detected: {0}")] + DeviceDuplicate(u32), + #[error("Failed while parsing to integer: {0:?}")] + ParseFailure(ParseIntError), + #[error("Failed to join threads")] + FailedJoiningThreads, +} + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct GpioArgs { + /// Location of vhost-user Unix domain socket. This is suffixed by 0,1,2..socket_count-1. + #[clap(short, long)] + socket_path: String, + + /// Number of guests (sockets) to connect to. + #[clap(short = 'c', long, default_value_t = 1)] + socket_count: usize, + + /// List of GPIO devices, one for each guest, in the format [:]. The first entry is for + /// Guest that connects to socket 0, next one for socket 1, and so on. Each device number here + /// will be used to access the corresponding /dev/gpiochipX. Example, "-c 4 -l 3:4:6:1" + #[clap(short = 'l', long)] + device_list: String, +} + +#[derive(Debug, PartialEq)] +pub(crate) struct DeviceConfig { + inner: Vec, +} + +impl DeviceConfig { + fn new() -> Self { + Self { inner: Vec::new() } + } + + fn contains_device(&self, number: u32) -> bool { + self.inner.iter().any(|elem| *elem == number) + } + + fn push(&mut self, device: u32) -> Result<()> { + if self.contains_device(device) { + return Err(Error::DeviceDuplicate(device)); + } + + self.inner.push(device); + Ok(()) + } +} + +impl TryFrom<&str> for DeviceConfig { + type Error = Error; + + fn try_from(list: &str) -> Result { + let list: Vec<&str> = list.split(':').collect(); + let mut devices = DeviceConfig::new(); + + for info in list.iter() { + let number = info.parse::().map_err(Error::ParseFailure)?; + devices.push(number)?; + } + Ok(devices) + } +} + +#[derive(PartialEq, Debug)] +struct GpioConfiguration { + socket_path: String, + socket_count: usize, + devices: DeviceConfig, +} + +impl TryFrom for GpioConfiguration { + type Error = Error; + + fn try_from(args: GpioArgs) -> Result { + if args.socket_count == 0 { + return Err(Error::SocketCountInvalid(0)); + } + + let devices = DeviceConfig::try_from(args.device_list.as_str())?; + + if devices.inner.len() != args.socket_count as usize { + return Err(Error::DeviceCountMismatch( + args.socket_count, + devices.inner.len(), + )); + } + + Ok(GpioConfiguration { + socket_path: args.socket_path, + socket_count: args.socket_count, + devices, + }) + } +} + +fn start_backend(args: GpioArgs) -> Result<()> { + let config = GpioConfiguration::try_from(args).unwrap(); + let mut handles = Vec::new(); + + for i in 0..config.socket_count { + let socket = config.socket_path.to_owned() + &i.to_string(); + let device_num = config.devices.inner[i]; + + let handle = spawn(move || loop { + // A separate thread is spawned for each socket and can connect to a separate guest. + // These are run in an infinite loop to not require the daemon to be restarted once a + // guest exits. + // + // There isn't much value in complicating code here to return an error from the + // threads, and so the code uses unwrap() instead. The panic on a thread won't cause + // trouble to other threads/guests or the main() function and should be safe for the + // daemon. + let device = D::open(device_num).unwrap(); + let controller = GpioController::::new(device).unwrap(); + let backend = Arc::new(RwLock::new(VhostUserGpioBackend::new(controller).unwrap())); + let listener = Listener::new(socket.clone(), true).unwrap(); + + let mut daemon = VhostUserDaemon::new( + String::from("vhost-device-gpio-backend"), + backend.clone(), + GuestMemoryAtomic::new(GuestMemoryMmap::new()), + ) + .unwrap(); + + daemon.start(listener).unwrap(); + + match daemon.wait() { + Ok(()) => { + info!("Stopping cleanly."); + } + Err(vhost_user_backend::Error::HandleRequest( + vhost_user::Error::PartialMessage, + )) => { + info!("vhost-user connection closed with partial message. If the VM is shutting down, this is expected behavior; otherwise, it might be a bug."); + } + Err(e) => { + warn!("Error running daemon: {:?}", e); + } + } + + // No matter the result, we need to shut down the worker thread. + backend.read().unwrap().exit_event.write(1).unwrap(); + }); + + handles.push(handle); + } + + for handle in handles { + handle.join().map_err(|_| Error::FailedJoiningThreads)?; + } + + Ok(()) +} + +pub(crate) fn gpio_init() -> Result<()> { + env_logger::init(); + + start_backend::(GpioArgs::parse()) +} diff --git a/gpio/src/gpio.rs b/gpio/src/gpio.rs new file mode 100644 index 0000000..a2738e2 --- /dev/null +++ b/gpio/src/gpio.rs @@ -0,0 +1,51 @@ +// GPIO backend device +// +// Copyright 2022 Linaro Ltd. All Rights Reserved. +// Viresh Kumar +// +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error as ThisError; + +type Result = std::result::Result; + +#[derive(Clone, Debug, PartialEq, ThisError)] +/// Errors related to low level gpio helpers +pub(crate) enum Error { +} + +/// Trait that represents an GPIO Device. +/// +/// This trait is introduced for development purposes only, and should not +/// be used outside of this crate. The purpose of this trait is to provide a +/// mock implementation for the GPIO driver so that we can test the GPIO +/// functionality without the need of a physical device. +pub(crate) trait GpioDevice { + fn open(device: u32) -> Result + where + Self: Sized; +} + +pub(crate) struct PhysDevice {} + +impl GpioDevice for PhysDevice { + fn open(_device: u32) -> Result + where + Self: Sized, + { + Ok(Self {}) + } +} + +pub(crate) struct GpioController { + device: D, +} + +impl GpioController { + // Creates a new controller corresponding to `device`. + pub(crate) fn new(device: D) -> Result> { + Ok(GpioController { + device, + }) + } +} diff --git a/gpio/src/main.rs b/gpio/src/main.rs new file mode 100644 index 0000000..3531af3 --- /dev/null +++ b/gpio/src/main.rs @@ -0,0 +1,14 @@ +// VIRTIO GPIO Emulation via vhost-user +// +// Copyright 2022 Linaro Ltd. All Rights Reserved. +// Viresh Kumar +// +// SPDX-License-Identifier: Apache-2.0 + +mod backend; +mod gpio; +mod vhu_gpio; + +fn main() -> backend::Result<()> { + backend::gpio_init() +} diff --git a/gpio/src/vhu_gpio.rs b/gpio/src/vhu_gpio.rs new file mode 100644 index 0000000..f6708b9 --- /dev/null +++ b/gpio/src/vhu_gpio.rs @@ -0,0 +1,147 @@ +// vhost device gpio +// +// Copyright 2022 Linaro Ltd. All Rights Reserved. +// Viresh Kumar +// +// SPDX-License-Identifier: Apache-2.0 + +use std::{convert, io}; + +use thiserror::Error as ThisError; +use vhost::vhost_user::message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures}; +use vhost_user_backend::{VhostUserBackendMut, VringRwLock, VringT}; +use virtio_bindings::bindings::virtio_net::{VIRTIO_F_NOTIFY_ON_EMPTY, VIRTIO_F_VERSION_1}; +use virtio_bindings::bindings::virtio_ring::{ + VIRTIO_RING_F_EVENT_IDX, VIRTIO_RING_F_INDIRECT_DESC, +}; +use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap}; +use vmm_sys_util::epoll::EventSet; +use vmm_sys_util::eventfd::{EventFd, EFD_NONBLOCK}; + +use crate::gpio::{GpioController, GpioDevice}; + +const QUEUE_SIZE: usize = 256; +const NUM_QUEUES: usize = 2; + +/// Queues +const REQUEST_QUEUE: u16 = 0; + +type Result = std::result::Result; +type VhostUserBackendResult = std::result::Result; + +#[derive(Debug, ThisError)] +/// Errors related to vhost-device-gpio-daemon. +pub(crate) enum Error { + #[error("Failed to handle event, didn't match EPOLLIN")] + HandleEventNotEpollIn, + #[error("Failed to handle unknown event")] + HandleEventUnknown, +} + +impl convert::From for io::Error { + fn from(e: Error) -> Self { + io::Error::new(io::ErrorKind::Other, e) + } +} + +pub(crate) struct VhostUserGpioBackend { + controller: GpioController, + event_idx: bool, + pub(crate) exit_event: EventFd, +} + +impl VhostUserGpioBackend { + pub(crate) fn new(controller: GpioController) -> Result { + Ok(VhostUserGpioBackend { + controller, + event_idx: false, + exit_event: EventFd::new(EFD_NONBLOCK).expect("Creating exit eventfd"), + }) + } + + /// Process the messages in the vring and dispatch replies + fn process_request_queue(&self, _vring: &VringRwLock) -> Result<()> { + Ok(()) + } +} + +/// VhostUserBackendMut trait methods +impl VhostUserBackendMut + for VhostUserGpioBackend +{ + fn num_queues(&self) -> usize { + NUM_QUEUES + } + + fn max_queue_size(&self) -> usize { + QUEUE_SIZE + } + + fn features(&self) -> u64 { + // this matches the current libvhost defaults except VHOST_F_LOG_ALL + 1 << VIRTIO_F_VERSION_1 + | 1 << VIRTIO_F_NOTIFY_ON_EMPTY + | 1 << VIRTIO_RING_F_INDIRECT_DESC + | 1 << VIRTIO_RING_F_EVENT_IDX + | VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits() + } + + fn protocol_features(&self) -> VhostUserProtocolFeatures { + VhostUserProtocolFeatures::MQ + } + + fn set_event_idx(&mut self, enabled: bool) { + dbg!(self.event_idx = enabled); + } + + fn update_memory( + &mut self, + _mem: GuestMemoryAtomic, + ) -> VhostUserBackendResult<()> { + Ok(()) + } + + fn handle_event( + &mut self, + device_event: u16, + evset: EventSet, + vrings: &[VringRwLock], + _thread_id: usize, + ) -> VhostUserBackendResult { + if evset != EventSet::IN { + return Err(Error::HandleEventNotEpollIn.into()); + } + + match device_event { + REQUEST_QUEUE => { + let vring = &vrings[0]; + + if self.event_idx { + // vm-virtio's Queue implementation only checks avail_index + // once, so to properly support EVENT_IDX we need to keep + // calling process_request_queue() until it stops finding + // new requests on the queue. + loop { + vring.disable_notification().unwrap(); + self.process_request_queue(vring)?; + if !vring.enable_notification().unwrap() { + break; + } + } + } else { + // Without EVENT_IDX, a single call is enough. + self.process_request_queue(vring)?; + } + } + + _ => { + return Err(Error::HandleEventUnknown.into()); + } + } + Ok(false) + } + + fn exit_event(&self, _thread_index: usize) -> Option { + self.exit_event.try_clone().ok() + } +}