use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use kvm_ioctls::VmFd;
use vm_memory::{Bytes, GuestAddress, GuestMemory, GuestMemoryMmap};
use super::devices::{
avail_idx, avail_ring_entry, read_desc, write_used, VirtioBackend, VirtioQueueState,
VIRTIO_F_VERSION_1, VRING_DESC_F_NEXT,
};
const VIRTIO_BALLOON_F_STATS_VQ: u64 = 1 << 1;
const VIRTIO_BALLOON_F_DEFLATE_ON_OOM: u64 = 1 << 2;
const VIRTIO_BALLOON_F_FREE_PAGE_HINT: u64 = 1 << 3;
const VIRTIO_BALLOON_F_REPORTING: u64 = 1 << 5;
const OFFERED_FEATURES: u64 =
VIRTIO_F_VERSION_1 | VIRTIO_BALLOON_F_DEFLATE_ON_OOM | VIRTIO_BALLOON_F_REPORTING;
const PAGE_SHIFT: u32 = 12;
const PAGE_SIZE: u64 = 1 << PAGE_SHIFT;
const VIRTIO_MMIO_INT_CONFIG: u32 = 2;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum QueueRole {
Inflate,
Deflate,
Stats,
FreePageHint,
Reporting,
}
const OPTIONAL_QUEUES: [(u64, QueueRole); 3] = [
(VIRTIO_BALLOON_F_STATS_VQ, QueueRole::Stats),
(VIRTIO_BALLOON_F_FREE_PAGE_HINT, QueueRole::FreePageHint),
(VIRTIO_BALLOON_F_REPORTING, QueueRole::Reporting),
];
struct QueueLayout {
roles: Vec<QueueRole>,
}
impl QueueLayout {
fn for_features(features: u64) -> Self {
let mut roles = vec![QueueRole::Inflate, QueueRole::Deflate];
roles.extend(
OPTIONAL_QUEUES
.iter()
.filter(|(bit, _)| features & bit != 0)
.map(|(_, role)| *role),
);
QueueLayout { roles }
}
fn role(&self, idx: u16) -> Option<QueueRole> {
self.roles.get(usize::from(idx)).copied()
}
fn len(&self) -> usize {
self.roles.len()
}
}
struct IrqLine {
vm_fd: Arc<VmFd>,
irq: u32,
status: Arc<AtomicU32>,
}
pub struct BalloonControl {
total_bytes: u64,
target_pages: AtomicU32,
actual_pages: AtomicU32,
irq: Mutex<Option<IrqLine>>,
}
impl BalloonControl {
pub fn set_memory_target(&self, bytes: u64) {
let keep = bytes.min(self.total_bytes);
let pages = ((self.total_bytes - keep) / PAGE_SIZE) as u32;
self.target_pages.store(pages, Ordering::Release);
if let Some(line) = self.irq.lock().unwrap().as_ref() {
line.status
.fetch_or(VIRTIO_MMIO_INT_CONFIG, Ordering::Release);
let _ = line.vm_fd.set_irq_line(line.irq, true);
}
}
pub fn ballooned_bytes(&self) -> u64 {
self.actual_pages.load(Ordering::Acquire) as u64 * PAGE_SIZE
}
}
pub struct BalloonBackend {
control: Arc<BalloonControl>,
host_page: u64,
layout: QueueLayout,
}
impl BalloonBackend {
pub fn new(total_bytes: u64) -> Self {
let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
BalloonBackend {
control: Arc::new(BalloonControl {
total_bytes,
target_pages: AtomicU32::new(0),
actual_pages: AtomicU32::new(0),
irq: Mutex::new(None),
}),
host_page: if page > 0 { page as u64 } else { PAGE_SIZE },
layout: QueueLayout::for_features(OFFERED_FEATURES),
}
}
pub fn control(&self) -> Arc<BalloonControl> {
self.control.clone()
}
fn discard(&self, mem: &GuestMemoryMmap, gpa: u64, len: u64) {
let mask = !(self.host_page - 1);
let start = (gpa + self.host_page - 1) & mask;
let end = gpa.saturating_add(len) & mask;
if end <= start {
return;
}
let len = (end - start) as usize;
if !mem.check_range(GuestAddress(start), len) {
return;
}
if let Ok(ptr) = mem.get_host_address(GuestAddress(start)) {
unsafe {
libc::madvise(ptr as *mut libc::c_void, len, libc::MADV_DONTNEED);
}
}
}
fn discard_pfns(&self, mem: &GuestMemoryMmap, addr: u64, len: u32) {
let mut run: Option<(u64, u64)> = None;
for i in 0..(len / 4) as u64 {
let pfn = match mem.read_obj::<u32>(GuestAddress(addr + i * 4)) {
Ok(pfn) => pfn as u64,
Err(_) => break,
};
let gpa = pfn << PAGE_SHIFT;
run = match run {
Some((start, run_len)) if start + run_len == gpa => {
Some((start, run_len + PAGE_SIZE))
}
Some((start, run_len)) => {
self.discard(mem, start, run_len);
Some((gpa, PAGE_SIZE))
}
None => Some((gpa, PAGE_SIZE)),
};
}
if let Some((start, run_len)) = run {
self.discard(mem, start, run_len);
}
}
fn drain(&self, role: QueueRole, q: &mut VirtioQueueState, mem: &GuestMemoryMmap) {
if q.size == 0 {
return;
}
let current = avail_idx(mem, q.avail_addr);
while q.last_avail_idx != current {
let head = avail_ring_entry(mem, q.avail_addr, q.size, q.last_avail_idx);
let mut idx = head;
loop {
let desc = read_desc(mem, q.desc_addr, idx);
match role {
QueueRole::Inflate => self.discard_pfns(mem, desc.addr, desc.len),
QueueRole::Deflate => {}
QueueRole::Reporting => self.discard(mem, desc.addr, desc.len as u64),
QueueRole::Stats | QueueRole::FreePageHint => {
unreachable!("the balloon does not offer stats or free page hints")
}
}
if desc.flags & VRING_DESC_F_NEXT == 0 {
break;
}
idx = desc.next;
}
write_used(mem, q.used_addr, q.size, q.last_avail_idx, head as u32, 0);
q.last_avail_idx = q.last_avail_idx.wrapping_add(1);
}
}
}
impl VirtioBackend for BalloonBackend {
fn device_id(&self) -> u32 {
5
}
fn device_features(&self) -> u64 {
OFFERED_FEATURES
}
fn queue_count(&self) -> usize {
QueueLayout::for_features(OFFERED_FEATURES).len()
}
fn queue_max_size(&self) -> u16 {
128
}
fn config_read(&self, offset: u64) -> u32 {
match offset {
0 => self.control.target_pages.load(Ordering::Acquire),
4 => self.control.actual_pages.load(Ordering::Acquire),
_ => 0,
}
}
fn config_write(&mut self, offset: u64, value: u32) {
if offset == 4 {
self.control.actual_pages.store(value, Ordering::Release);
}
}
fn features_ok(&mut self, features: u64) {
self.layout = QueueLayout::for_features(features);
}
fn activate(
&mut self,
_: &[VirtioQueueState],
_: &GuestMemoryMmap,
vm_fd: &Arc<VmFd>,
irq: u32,
interrupt_status: Arc<AtomicU32>,
) {
*self.control.irq.lock().unwrap() = Some(IrqLine {
vm_fd: vm_fd.clone(),
irq,
status: interrupt_status,
});
}
fn process_queue(
&mut self,
queue_idx: u16,
queues: &mut [VirtioQueueState],
mem: &GuestMemoryMmap,
_: &Arc<VmFd>,
_: u32,
) {
let Some(role) = self.layout.role(queue_idx) else {
return;
};
if let Some(q) = queues.get_mut(usize::from(queue_idx)) {
if q.ready {
self.drain(role, q, mem);
}
}
}
fn reset(&mut self) {
*self.control.irq.lock().unwrap() = None;
self.control.actual_pages.store(0, Ordering::Release);
self.layout = QueueLayout::for_features(OFFERED_FEATURES);
}
}