input: Initial support for input emulation

This patch is to add support for vhost input device emulation.

The code skeleton heavily reuses vhost-device-i2c and vhost-device-rng,
including sharing the same license, and how to create the multi-thread
mode and daemon for backend virtual device.

It gives detailed usage for enabling virtual input device for guest in
the README.md.

Signed-off-by: Leo Yan <leo.yan@linaro.org>
This commit is contained in:
Leo Yan 2023-11-23 11:36:55 +08:00 committed by Manos Pitsidianakis
parent ddc25ecf34
commit 01e57d07c6
10 changed files with 758 additions and 0 deletions

View File

@ -4,6 +4,7 @@ resolver = "2"
members = [
"vhost-device-gpio",
"vhost-device-i2c",
"vhost-device-input",
"vhost-device-rng",
"vhost-device-scsi",
"vhost-device-scmi",

View File

@ -16,6 +16,7 @@ Here is the list of device backends that we support:
- [GPIO](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-gpio/README.md)
- [I2C](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-i2c/README.md)
- [Input](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-input/README.md)
- [RNG](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-rng/README.md)
- [SCMI](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-scmi/README.md)
- [SCSI](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-scsi/README.md)

View File

@ -0,0 +1,3 @@
# Upcoming Release
- First initial vhost-device-input daemon implementation.

View File

@ -0,0 +1,37 @@
[package]
name = "vhost-device-input"
version = "0.1.0"
authors = ["Leo Yan <leo.yan@linaro.org>"]
description = "vhost input backend device"
repository = "https://github.com/rust-vmm/vhost-device"
readme = "README.md"
keywords = ["virtio-input", "vhost-user", "vhost", "virtio", "rust-vmm"]
categories = ["virtualization"]
license = "Apache-2.0 OR BSD-3-Clause"
edition = "2021"
[features]
xen = ["vm-memory/xen", "vhost/xen", "vhost-user-backend/xen"]
[dependencies]
clap = { version = "4.4", features = ["derive"] }
env_logger = "0.10"
epoll = "4.3"
libc = "0.2"
log = "0.4"
rand = "0.8.5"
tempfile = "3.5"
thiserror = "1.0"
vhost = { version = "0.9", features = ["vhost-user-backend"] }
vhost-user-backend = "0.11"
virtio-bindings = "0.2.2"
virtio-queue = "0.10"
vm-memory = "0.13.1"
vmm-sys-util = "0.11"
evdev = "0.12"
nix = "0.26"
[dev-dependencies]
assert_matches = "1.5"
virtio-queue = { version = "0.10", features = ["test-utils"] }
vm-memory = { version = "0.13", features = ["backend-mmap", "backend-atomic"] }

View File

@ -0,0 +1 @@
../LICENSE-APACHE

View File

@ -0,0 +1 @@
../LICENSE-BSD-3-Clause

View File

@ -0,0 +1,63 @@
# vhost-device-input
## Synopsis
vhost-device-input --socket-path <SOCKET_PATH> --event-list <EVENT_LIST>
## Description
This program is a vhost-user backend that emulates a VirtIO input event.
It polls on a host's input event device (/dev/input/eventX) and passes the
input event data to guests.
This program is tested with QEMU's `vhost-user-input-pci`. The
implemenation is based on the vhost-user protocol and as such should be
interoperable with other virtual machine managers. Please see below for
working examples.
## Options
```text
-h, --help
Print help.
-s, --socket-path <SOCKET_PATH>
Location of vhost-user Unix domain sockets, this path will be suffixed with
0,1,2..event_count-1.
-e, --event-list <EVENT_LIST>
Input event device list in the format: event_device1,event_device2,...
Example: --event-list /dev/input/event14,/dev/input/event15
```
## Examples
The daemon should be started first:
```shell
host# vhost-device-input --socket-path /some/path/input.sock \
--event-list /dev/input/event14,/dev/input/event15
```
Note that from the above command the socket path "/some/path/input.sock0" and
"/some/path/input.sock1" will be created for input events "event14" and
"event15" respectively. This in turn needs to be communicated as chardev
sockets to QEMU in order for the backend daemon and access the Virtio queues
with the guest over the shared memory.
```shell
host# qemu-system -M virt \
-object memory-backend-file,id=mem,size=4G,mem-path=/dev/shm,share=on \
-chardev socket,path=/some/path/input.sock0,id=kbd0 \
-device vhost-user-input-pci,chardev=kdb0 \
-chardev socket,path=/some/path/input.sock1,id=mouse0 \
-device vhost-user-input-pci,chardev=mouse0 \
-numa node,memdev=mem \
...
```
## License
This project is licensed under either of
- [Apache License](http://www.apache.org/licenses/LICENSE-2.0), Version 2.0
- [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause)

View File

@ -0,0 +1,56 @@
// Low level input device definitions
//
// Copyright 2023 Linaro Ltd. All Rights Reserved.
// Leo Yan <leo.yan@linaro.org>
//
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
use evdev::{Device, FetchEventsSynced, InputId};
use nix::ioctl_read_buf;
use std::io;
use std::os::fd::{AsRawFd, RawFd};
use std::path::PathBuf;
/// Trait that operates on input event device. This main purpose for this
/// trait is to encapsulate a "Device" structure for accessing hardware
/// device or mock device for test.
pub trait InputDevice {
/// Open the input device specified by the path.
fn open(path: PathBuf) -> io::Result<Self>
where
Self: Sized;
/// Fetch input events.
fn fetch_events(&mut self) -> io::Result<FetchEventsSynced<'_>>;
/// Return the raw file descriptor.
fn get_raw_fd(&self) -> RawFd;
/// Return the Input ID.
fn input_id(&self) -> InputId;
}
impl InputDevice for Device {
fn open(path: PathBuf) -> io::Result<Self> {
Device::open(path)
}
fn fetch_events(&mut self) -> io::Result<FetchEventsSynced<'_>> {
Device::fetch_events(self)
}
fn get_raw_fd(&self) -> RawFd {
Device::as_raw_fd(self)
}
fn input_id(&self) -> InputId {
Device::input_id(self)
}
}
ioctl_read_buf!(eviocgname, b'E', 0x06, u8);
ioctl_read_buf!(eviocgbit_key, b'E', 0x21, u8);
ioctl_read_buf!(eviocgbit_relative, b'E', 0x22, u8);
ioctl_read_buf!(eviocgbit_absolute, b'E', 0x23, u8);
ioctl_read_buf!(eviocgbit_misc, b'E', 0x24, u8);
ioctl_read_buf!(eviocgbit_switch, b'E', 0x25, u8);

View File

@ -0,0 +1,171 @@
//
// Copyright 2023 Linaro Ltd. All Rights Reserved.
// Leo Yan <leo.yan@linaro.org>
//
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
mod input;
mod vhu_input;
use clap::Parser;
use evdev::Device;
use log::error;
use std::{
any::Any,
collections::HashMap,
path::PathBuf,
process::exit,
sync::{Arc, RwLock},
thread,
};
use thiserror::Error as ThisError;
use vhost_user_backend::VhostUserDaemon;
use vhu_input::VuInputBackend;
use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap};
use vmm_sys_util::epoll::EventSet;
use crate::input::*;
#[derive(Debug, ThisError)]
/// Errors related to vhost-device-input daemon.
pub(crate) enum Error {
#[error("Event device file doesn't exists or can't be accessed")]
AccessEventDeviceFile,
#[error("Could not create backend: {0}")]
CouldNotCreateBackend(std::io::Error),
#[error("Could not create daemon: {0}")]
CouldNotCreateDaemon(vhost_user_backend::Error),
#[error("Could not register input event into vring epoll")]
CouldNotRegisterInputEvent,
#[error("Fatal error: {0}")]
ServeFailed(vhost_user_backend::Error),
#[error("Thread `{0}` panicked")]
ThreadPanic(String, Box<dyn Any + Send>),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Parser, Debug, PartialEq)]
#[clap(author, version, about, long_about = None)]
struct InputArgs {
// Location of vhost-user Unix domain socket.
#[clap(short, long, value_name = "SOCKET")]
socket_path: PathBuf,
// Path for reading input events.
#[clap(
short = 'e',
long,
use_value_delimiter = true,
value_delimiter = ',',
required = true
)]
event_list: Vec<PathBuf>,
}
impl InputArgs {
pub fn generate_socket_paths(&self) -> Vec<PathBuf> {
let socket_file_name = self
.socket_path
.file_name()
.expect("socket_path has no filename.");
let socket_file_parent = self
.socket_path
.parent()
.expect("socket_path has no parent directory.");
let make_socket_path = |i: usize| -> PathBuf {
let mut file_name = socket_file_name.to_os_string();
file_name.push(std::ffi::OsStr::new(&i.to_string()));
socket_file_parent.join(&file_name)
};
(0..self.event_list.len()).map(make_socket_path).collect()
}
}
// This is the public API through which an external program starts the
/// vhost-device-input backend server.
pub(crate) fn start_backend_server<D: 'static + InputDevice + Send + Sync>(
socket: PathBuf,
event: PathBuf,
) -> Result<()> {
loop {
let ev_dev = D::open(event.clone()).map_err(|_| Error::AccessEventDeviceFile)?;
let raw_fd = ev_dev.get_raw_fd();
// If creating the VuInputBackend isn't successful there isn't much else to do than
// killing the thread, which .unwrap() does. When that happens an error code is
// generated and displayed by the runtime mechanic. Killing a thread doesn't affect
// the other threads spun-off by the daemon.
let vu_input_backend = Arc::new(RwLock::new(
VuInputBackend::new(ev_dev).map_err(Error::CouldNotCreateBackend)?,
));
let mut daemon = VhostUserDaemon::new(
String::from("vhost-device-input-backend"),
Arc::clone(&vu_input_backend),
GuestMemoryAtomic::new(GuestMemoryMmap::new()),
)
.map_err(Error::CouldNotCreateDaemon)?;
let handlers = daemon.get_epoll_handlers();
handlers[0]
.register_listener(raw_fd, EventSet::IN, vhu_input::EVENT_ID_IN_VRING_EPOLL)
.map_err(|_| Error::CouldNotRegisterInputEvent)?;
daemon.serve(&socket).map_err(Error::ServeFailed)?;
}
}
pub(crate) fn start_backend<D: 'static + InputDevice + Send + Sync>(
config: InputArgs,
) -> Result<()> {
let mut handles = HashMap::new();
let (senders, receiver) = std::sync::mpsc::channel();
for (thread_id, (socket, event)) in config
.generate_socket_paths()
.into_iter()
.zip(config.event_list.iter().cloned())
.enumerate()
{
let name = format!("vhu-vsock-input-{:?}", event);
let sender = senders.clone();
let handle = thread::Builder::new()
.name(name.clone())
.spawn(move || {
let result =
std::panic::catch_unwind(move || start_backend_server::<D>(socket, event));
// Notify the main thread that we are done.
sender.send(thread_id).unwrap();
result.map_err(|e| Error::ThreadPanic(name, e))?
})
.unwrap();
handles.insert(thread_id, handle);
}
while !handles.is_empty() {
let thread_id = receiver.recv().unwrap();
handles
.remove(&thread_id)
.unwrap()
.join()
.map_err(std::panic::resume_unwind)
.unwrap()?;
}
Ok(())
}
fn main() {
env_logger::init();
if let Err(e) = start_backend::<Device>(InputArgs::parse()) {
error!("{e}");
exit(1);
}
}

View File

@ -0,0 +1,424 @@
// VIRTIO Input Emulation via vhost-user
//
// Copyright 2023 Linaro Ltd. All Rights Reserved.
// Leo Yan <leo.yan@linaro.org>
//
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
use log::error;
use nix::libc;
use std::collections::VecDeque;
use std::io::{self, Result as IoResult};
use thiserror::Error as ThisError;
use vhost::vhost_user::message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures};
use vhost_user_backend::{VhostUserBackendMut, VringRwLock, VringT};
use virtio_bindings::bindings::virtio_config::VIRTIO_F_VERSION_1;
use virtio_bindings::bindings::virtio_ring::{
VIRTIO_RING_F_EVENT_IDX, VIRTIO_RING_F_INDIRECT_DESC,
};
use virtio_queue::QueueT;
use vm_memory::{ByteValued, Bytes, GuestAddressSpace, GuestMemoryAtomic, GuestMemoryMmap};
use vmm_sys_util::epoll::EventSet;
use vmm_sys_util::eventfd::{EventFd, EFD_NONBLOCK};
use crate::input::*;
pub const EVENT_ID_IN_VRING_EPOLL: u64 = 3;
const QUEUE_SIZE: usize = 1024;
const NUM_QUEUES: usize = 2;
const VIRTIO_INPUT_CFG_ID_NAME: u8 = 0x01;
const VIRTIO_INPUT_CFG_ID_DEVIDS: u8 = 0x03;
const VIRTIO_INPUT_CFG_EV_BITS: u8 = 0x11;
const VIRTIO_INPUT_CFG_SIZE: usize = 128;
const EV_SYN: u8 = 0x00;
const EV_KEY: u8 = 0x01;
const EV_REL: u8 = 0x02;
const EV_ABS: u8 = 0x03;
const EV_MSC: u8 = 0x04;
const EV_SW: u8 = 0x05;
const SYN_REPORT: u8 = 0x00;
#[repr(C, packed)]
#[derive(Copy, Clone, Debug)]
pub(crate) struct VuInputConfig {
select: u8,
subsel: u8,
size: u8,
reserved: [u8; 5],
val: [u8; VIRTIO_INPUT_CFG_SIZE],
}
// If deriving the 'Default' trait, an array is limited with a maximum size of 32 bytes,
// thus it cannot meet the length VIRTIO_INPUT_CFG_SIZE (128) for the 'val' array.
// Implement Default trait to accommodate array 'val'.
impl Default for VuInputConfig {
fn default() -> VuInputConfig {
VuInputConfig {
select: 0,
subsel: 0,
size: 0,
reserved: [0; 5],
val: [0; VIRTIO_INPUT_CFG_SIZE],
}
}
}
// SAFETY: The layout of the structure is fixed and can be initialized by
// reading its content from byte array.
unsafe impl ByteValued for VuInputConfig {}
#[repr(C, packed)]
#[derive(Copy, Clone, Debug, Default)]
pub(crate) struct VuInputEvent {
ev_type: u16,
code: u16,
value: u32,
}
// SAFETY: The layout of the structure is fixed and can be initialized by
// reading its content from byte array.
unsafe impl ByteValued for VuInputEvent {}
#[derive(Debug, Eq, PartialEq, ThisError)]
/// Errors related to vhost-device-input daemon.
pub(crate) enum VuInputError {
#[error("Notification send failed")]
SendNotificationFailed,
#[error("Can't create eventFd")]
EventFdError,
#[error("Failed to handle event")]
HandleEventNotEpollIn,
#[error("Unknown device event")]
HandleEventUnknownEvent,
#[error("Unknown config request: {0}")]
UnexpectedConfig(u8),
#[error("Failed to fetch event")]
UnexpectedFetchEventError,
#[error("Too many descriptors: {0}")]
UnexpectedDescriptorCount(usize),
#[error("Failed to read from the input device")]
UnexpectedInputDeviceError,
#[error("Failed to write descriptor to vring")]
UnexpectedWriteDescriptorError,
#[error("Failed to write event to vring")]
UnexpectedWriteVringError,
}
type Result<T> = std::result::Result<T, VuInputError>;
impl From<VuInputError> for io::Error {
fn from(e: VuInputError) -> Self {
Self::new(io::ErrorKind::Other, e)
}
}
pub(crate) struct VuInputBackend<T: InputDevice> {
event_idx: bool,
ev_dev: T,
pub exit_event: EventFd,
select: u8,
subsel: u8,
mem: Option<GuestMemoryAtomic<GuestMemoryMmap>>,
ev_list: VecDeque<VuInputEvent>,
}
impl<T: InputDevice> VuInputBackend<T> {
pub fn new(ev_dev: T) -> std::result::Result<Self, std::io::Error> {
Ok(VuInputBackend {
event_idx: false,
ev_dev,
exit_event: EventFd::new(EFD_NONBLOCK).map_err(|_| VuInputError::EventFdError)?,
select: 0,
subsel: 0,
mem: None,
ev_list: VecDeque::new(),
})
}
fn process_event(&mut self, vring: &VringRwLock) -> Result<bool> {
let last_sync_index = self
.ev_list
.iter()
.rposition(|event| event.ev_type == EV_SYN as u16 && event.code == SYN_REPORT as u16)
.unwrap_or(0);
if last_sync_index == 0 {
log::warn!("No available events on the list!");
return Ok(true);
}
let mut index = 0;
while index <= last_sync_index {
let event = self.ev_list.get(index).unwrap();
index += 1;
let mem = self.mem.as_ref().unwrap().memory();
let desc = vring
.get_mut()
.get_queue_mut()
.pop_descriptor_chain(mem.clone());
if let Some(desc_chain) = desc {
let descriptors: Vec<_> = desc_chain.clone().collect();
if descriptors.len() != 1 {
return Err(VuInputError::UnexpectedDescriptorCount(descriptors.len()));
}
let descriptor = descriptors[0];
desc_chain
.memory()
.write_obj(*event, descriptor.addr())
.map_err(|_| VuInputError::UnexpectedWriteDescriptorError)?;
if vring
.add_used(desc_chain.head_index(), event.as_slice().len() as u32)
.is_err()
{
log::error!("Couldn't write event data to the ring");
return Err(VuInputError::UnexpectedWriteVringError);
}
} else {
// Now cannot get available descriptor, which means the host cannot process
// event data in time and overrun happens in the backend. In this case,
// we simply drop the incomping input event and notify guest for handling
// events. At the end, it returns Ok(false) so can avoid exiting the thread loop.
self.ev_list.clear();
vring
.signal_used_queue()
.map_err(|_| VuInputError::SendNotificationFailed)?;
return Ok(false);
}
}
// Sent the events [0..last_sync_index] to vring and remove them from the list.
// The range end parameter is an exclusive value, so use 'last_sync_index + 1'.
self.ev_list.drain(0..last_sync_index + 1);
Ok(true)
}
/// Process the requests in the vring and dispatch replies
fn process_queue(&mut self, vring: &VringRwLock) -> Result<bool> {
let events = self
.ev_dev
.fetch_events()
.map_err(|_| VuInputError::UnexpectedFetchEventError)?;
for event in events {
let ev_raw_data = VuInputEvent {
ev_type: event.event_type().0,
code: event.code(),
value: event.value() as u32,
};
self.ev_list.push_back(ev_raw_data);
}
self.process_event(vring)?;
vring
.signal_used_queue()
.map_err(|_| VuInputError::SendNotificationFailed)?;
Ok(true)
}
pub fn read_event_config(&self) -> Result<VuInputConfig> {
let mut cfg: [u8; VIRTIO_INPUT_CFG_SIZE] = [0; VIRTIO_INPUT_CFG_SIZE];
let func: unsafe fn(nix::libc::c_int, &mut [u8]) -> nix::Result<libc::c_int> =
match self.subsel {
EV_KEY => eviocgbit_key,
EV_ABS => eviocgbit_absolute,
EV_REL => eviocgbit_relative,
EV_MSC => eviocgbit_misc,
EV_SW => eviocgbit_switch,
_ => {
return Err(VuInputError::HandleEventUnknownEvent);
}
};
// SAFETY: Safe as the file is a valid event device, the kernel will only
// update the correct amount of memory in func.
if unsafe { func(self.ev_dev.get_raw_fd(), &mut cfg) }.is_err() {
return Err(VuInputError::UnexpectedInputDeviceError);
}
let mut size: u8 = 0;
for (index, val) in cfg.iter().enumerate() {
if *val != 0 {
size = (index + 1) as u8;
}
}
Ok(VuInputConfig {
select: self.select,
subsel: self.subsel,
size,
reserved: [0; 5],
val: cfg,
})
}
pub fn read_name_config(&self) -> Result<VuInputConfig> {
let mut name: [u8; VIRTIO_INPUT_CFG_SIZE] = [0; VIRTIO_INPUT_CFG_SIZE];
// SAFETY: Safe as the file is a valid event device, the kernel will only
// update the correct amount of memory in func.
match unsafe { eviocgname(self.ev_dev.get_raw_fd(), name.as_mut_slice()) } {
Ok(len) if len as usize > name.len() => {
return Err(VuInputError::UnexpectedInputDeviceError);
}
Ok(len) if len <= 1 => {
return Err(VuInputError::UnexpectedInputDeviceError);
}
Err(_) => {
return Err(VuInputError::UnexpectedInputDeviceError);
}
_ => (),
}
let size = String::from_utf8(name.to_vec()).unwrap().len();
Ok(VuInputConfig {
select: self.select,
subsel: 0,
size: size as u8,
reserved: [0; 5],
val: name,
})
}
pub fn read_id_config(&self) -> Result<VuInputConfig> {
let input_id = self.ev_dev.input_id();
let mut dev_id = [
input_id.bus_type().0.as_slice(),
input_id.vendor().as_slice(),
input_id.product().as_slice(),
input_id.version().as_slice(),
]
.concat();
dev_id.resize(VIRTIO_INPUT_CFG_SIZE, 0);
Ok(VuInputConfig {
select: VIRTIO_INPUT_CFG_ID_DEVIDS,
subsel: 0,
size: VIRTIO_INPUT_CFG_SIZE as u8,
reserved: [0; 5],
val: dev_id.try_into().unwrap(),
})
}
}
/// VhostUserBackend trait methods
impl<T: 'static + InputDevice + Sync + Send> VhostUserBackendMut for VuInputBackend<T> {
type Bitmap = ();
type Vring = VringRwLock;
fn num_queues(&self) -> usize {
NUM_QUEUES
}
fn max_queue_size(&self) -> usize {
QUEUE_SIZE
}
fn features(&self) -> u64 {
1 << VIRTIO_F_VERSION_1
| 1 << VIRTIO_RING_F_INDIRECT_DESC
| 1 << VIRTIO_RING_F_EVENT_IDX
| VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits()
}
fn protocol_features(&self) -> VhostUserProtocolFeatures {
VhostUserProtocolFeatures::MQ | VhostUserProtocolFeatures::CONFIG
}
fn get_config(&self, offset: u32, size: u32) -> Vec<u8> {
let cfg = match self.select {
VIRTIO_INPUT_CFG_ID_NAME => self.read_name_config(),
VIRTIO_INPUT_CFG_ID_DEVIDS => self.read_id_config(),
VIRTIO_INPUT_CFG_EV_BITS => self.read_event_config(),
_ => Err(VuInputError::UnexpectedConfig(self.select)),
};
let val = match cfg {
Ok(v) => v.as_slice().to_vec(),
_ => vec![0; size as usize],
};
let mut result: Vec<_> = val
.as_slice()
.iter()
.skip(offset as usize)
.take(size as usize)
.copied()
.collect();
// pad with 0s up to `size`
result.resize(size as usize, 0);
result
}
// In virtio spec https://docs.oasis-open.org/virtio/virtio/v1.2/cs01/virtio-v1.2-cs01.pdf,
// section "5.8.5.1 Driver Requirements: Device Initialization", it doesn't mention to
// use 'offset' argument, so set it as unused.
fn set_config(&mut self, _offset: u32, buf: &[u8]) -> io::Result<()> {
self.select = buf[0];
self.subsel = buf[1];
Ok(())
}
fn set_event_idx(&mut self, enabled: bool) {
self.event_idx = enabled;
}
fn update_memory(&mut self, mem: GuestMemoryAtomic<GuestMemoryMmap>) -> IoResult<()> {
self.mem = Some(mem);
Ok(())
}
fn handle_event(
&mut self,
device_event: u16,
evset: EventSet,
vrings: &[VringRwLock],
_thread_id: usize,
) -> IoResult<()> {
if !self.event_idx {
self.ev_dev.fetch_events()?;
return Ok(());
}
if evset != EventSet::IN {
return Err(VuInputError::HandleEventNotEpollIn.into());
}
if device_event == EVENT_ID_IN_VRING_EPOLL as u16 {
let vring = &vrings[0];
if self.event_idx {
vring.disable_notification().unwrap();
self.process_queue(vring)?;
vring.enable_notification().unwrap();
} else {
// Without EVENT_IDX, a single call is enough.
self.process_queue(vring)?;
}
}
Ok(())
}
fn exit_event(&self, _thread_index: usize) -> Option<EventFd> {
self.exit_event.try_clone().ok()
}
}