From 8bea2baeddfc479c5a8947e956bacc6ffcd52d82 Mon Sep 17 00:00:00 2001 From: dehengli Date: Mon, 15 Jan 2024 14:38:32 +0800 Subject: [PATCH 1/6] device: add memory resize event to ged. Extend ged event, add memory resize event. Signed-off-by: dehengli --- devices/src/acpi/ged.rs | 50 ++++++++++++++++++++++++++++++++++ machine/src/x86_64/standard.rs | 12 +++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/devices/src/acpi/ged.rs b/devices/src/acpi/ged.rs index 57629455..ac18aa9a 100644 --- a/devices/src/acpi/ged.rs +++ b/devices/src/acpi/ged.rs @@ -47,6 +47,7 @@ pub enum AcpiEvent { BatteryInf = 4, BatterySt = 8, CpuResize = 16, + MemoryResize = 32, } const AML_GED_EVT_REG: &str = "EREG"; @@ -56,17 +57,22 @@ pub struct GedEvent { power_button: Arc, #[cfg(target_arch = "x86_64")] cpu_resize: Arc, + #[cfg(target_arch = "x86_64")] + memory_resize: Arc, } impl GedEvent { pub fn new( power_button: Arc, #[cfg(target_arch = "x86_64")] cpu_resize: Arc, + #[cfg(target_arch = "x86_64")] memory_resize: Arc, ) -> GedEvent { GedEvent { power_button, #[cfg(target_arch = "x86_64")] cpu_resize, + #[cfg(target_arch = "x86_64")] + memory_resize, } } } @@ -111,6 +117,9 @@ impl Ged { #[cfg(target_arch = "x86_64")] ged.register_acpi_cpu_resize_event(ged_event.cpu_resize) .with_context(|| "Failed to register ACPI cpu resize event.")?; + #[cfg(target_arch = "x86_64")] + ged.register_acpi_memory_resize_event(ged_event.memory_resize) + .with_context(|| "Failed to register ACPI memory resize event.")?; Ok(dev.clone()) } @@ -171,6 +180,35 @@ impl Ged { Ok(()) } + #[cfg(target_arch = "x86_64")] + fn register_acpi_memory_resize_event(&self, mem_resize: Arc) -> Result<()> { + let mem_resize_fd = mem_resize.as_raw_fd(); + let clone_ged = self.clone(); + let mem_resize_handler: Rc = Rc::new(move |_, _| { + read_fd(mem_resize_fd); + clone_ged + .notification_type + .store(AcpiEvent::MemoryResize as u32, Ordering::SeqCst); + clone_ged.inject_interrupt(); + if QmpChannel::is_connected() { + event!(MemoryResize); + } + None + }); + + let notifier = EventNotifier::new( + NotifierOperation::AddShared, + mem_resize_fd, + None, + EventSet::IN, + vec![mem_resize_handler], + ); + + EventLoop::update_event(vec![notifier], None) + .with_context(|| "Failed to register memory resize notifier.")?; + Ok(()) + } + pub fn inject_acpi_event(&self, evt: AcpiEvent) { self.notification_type .fetch_or(evt as u32, Ordering::SeqCst); @@ -308,6 +346,18 @@ impl AmlBuilder for Ged { )); cpu_if_scope.append_child(AmlCallWithArgs1::new("\\_SB.PRES.CSCN", AmlOne)); method.append_child(cpu_if_scope); + + // Call memory hot(un)plug method. + let mut memory_if_scope = AmlIf::new(AmlEqual::new( + AmlAnd::new( + AmlLocal(0), + AmlInteger(AcpiEvent::MemoryResize as u64), + AmlLocal(1), + ), + AmlInteger(AcpiEvent::MemoryResize as u64), + )); + memory_if_scope.append_child(AmlCallWithArgs1::new("\\_SB.MHPC.MSCN", AmlOne)); + method.append_child(memory_if_scope); } acpi_dev.append_child(method); diff --git a/machine/src/x86_64/standard.rs b/machine/src/x86_64/standard.rs index 110fa2bd..4b334a8d 100644 --- a/machine/src/x86_64/standard.rs +++ b/machine/src/x86_64/standard.rs @@ -125,6 +125,8 @@ pub struct StdMachine { power_button: Arc, /// CPU Resize request, handle vm cpu hot(un)plug event. cpu_resize_req: Arc, + /// Memory Resize request, handle vm memory hot(un)plug event. + mem_resize_req: Arc, /// List contains the boot order of boot devices. boot_order_list: Arc>>, /// Cpu Controller. @@ -171,6 +173,10 @@ impl StdMachine { EventFd::new(libc::EFD_NONBLOCK) .with_context(|| MachineError::InitEventFdErr("cpu resize".to_string()))?, ), + mem_resize_req: Arc::new( + EventFd::new(libc::EFD_NONBLOCK) + .with_context(|| MachineError::InitEventFdErr("memory resize".to_string()))?, + ), boot_order_list: Arc::new(Mutex::new(Vec::new())), cpu_controller: None, }) @@ -492,7 +498,11 @@ impl MachineOps for StdMachine { let region_base: u64 = MEM_LAYOUT[LayoutEntryType::GedMmio as usize].0; let region_size: u64 = MEM_LAYOUT[LayoutEntryType::GedMmio as usize].1; - let ged_event = GedEvent::new(self.power_button.clone(), self.cpu_resize_req.clone()); + let ged_event = GedEvent::new( + self.power_button.clone(), + self.cpu_resize_req.clone(), + self.mem_resize_req.clone(), + ); ged.realize( &mut self.base.sysbus, ged_event, -- Gitee From 7918b0ed64f22138ce7cc9750b118e9818e12dea Mon Sep 17 00:00:00 2001 From: dehengli Date: Mon, 15 Jan 2024 14:42:12 +0800 Subject: [PATCH 2/6] device: add memory controller device for x86_64 standard machine 1. Add memory controller to handle ged memory resize(hotplug/hotpunplug) event. 2. X86_64 standard machine add memory controller address space in memory layout and init memory controller when vm realize. Signed-off-by: dehengli --- address_space/src/host_mmap.rs | 96 +++- address_space/src/lib.rs | 4 +- devices/src/acpi/memory_controller.rs | 797 ++++++++++++++++++++++++++ devices/src/acpi/mod.rs | 2 + machine/src/x86_64/standard.rs | 42 ++ 5 files changed, 917 insertions(+), 24 deletions(-) create mode 100644 devices/src/acpi/memory_controller.rs diff --git a/address_space/src/host_mmap.rs b/address_space/src/host_mmap.rs index 0bc8b3cd..b4ec1581 100644 --- a/address_space/src/host_mmap.rs +++ b/address_space/src/host_mmap.rs @@ -232,6 +232,77 @@ fn mem_prealloc(host_addr: u64, size: u64, nr_vcpus: u8) { } } +/// Create file backend with path. +/// +/// # Arguments +/// +/// * `path` - The path of file. +/// * `file_len` - The size of file. +pub fn create_file_backend_with_path(path: &str, file_len: u64) -> Result { + FileBackend::new_mem(path, file_len).with_context(|| "Failed to create file that backs memory") +} + +/// Create anonymous file backend. +/// +/// # Arguments +/// +/// * `file_len` - The size of file. +pub fn create_anonymous_file_backend(file_len: u64) -> Result { + let anon_fd = memfd_create( + &CString::new("stratovirt_anon_mem")?, + MemFdCreateFlag::empty(), + )?; + if anon_fd < 0 { + return Err(std::io::Error::last_os_error()).with_context(|| "Failed to create memfd"); + } + + // SAFETY: The parameters is constant. + let anon_file = unsafe { File::from_raw_fd(anon_fd) }; + anon_file + .set_len(file_len) + .with_context(|| "Failed to set the length of anonymous file that backs memory")?; + + Ok(FileBackend { + file: Arc::new(anon_file), + offset: 0, + page_size: host_page_size(), + }) +} + +/// Create hotplug memory. +/// +/// # Arguments +/// +/// * `guest_addr` - Guest physical address. +/// * `mem_config` - The config of hotplug memory. +/// * `name` - The name of ram region. +pub fn create_hotplug_mem( + guest_addr: GuestAddress, + mem_config: &MachineMemConfig, + name: &str, +) -> Result { + let mut f_back: Option = None; + + if let Some(path) = &mem_config.mem_path { + f_back = Some(create_file_backend_with_path(path, mem_config.mem_size)?); + } else if mem_config.mem_share { + f_back = Some(create_anonymous_file_backend(mem_config.mem_size)?); + } + let block = Arc::new(HostMemMapping::new( + guest_addr, + None, + mem_config.mem_size, + f_back, + mem_config.dump_guest_core, + mem_config.mem_share, + false, + )?); + + let region = Region::init_ram_region(block, name); + + Ok(region) +} + /// If the memory is not configured numa, use this /// /// # Arguments @@ -242,30 +313,9 @@ pub fn create_default_mem(mem_config: &MachineMemConfig, thread_num: u8) -> Resu let mut f_back: Option = None; if let Some(path) = &mem_config.mem_path { - f_back = Some( - FileBackend::new_mem(path, mem_config.mem_size) - .with_context(|| "Failed to create file that backs memory")?, - ); + f_back = Some(create_file_backend_with_path(path, mem_config.mem_size)?); } else if mem_config.mem_share { - let anon_fd = memfd_create( - &CString::new("stratovirt_anon_mem")?, - MemFdCreateFlag::empty(), - )?; - if anon_fd < 0 { - return Err(std::io::Error::last_os_error()).with_context(|| "Failed to create memfd"); - } - - // SAFETY: The parameters is constant. - let anon_file = unsafe { File::from_raw_fd(anon_fd) }; - anon_file - .set_len(mem_config.mem_size) - .with_context(|| "Failed to set the length of anonymous file that backs memory")?; - - f_back = Some(FileBackend { - file: Arc::new(anon_file), - offset: 0, - page_size: host_page_size(), - }); + f_back = Some(create_anonymous_file_backend(mem_config.mem_size)?); } let block = Arc::new(HostMemMapping::new( GuestAddress(0), diff --git a/address_space/src/lib.rs b/address_space/src/lib.rs index 3507e769..b5a25b73 100644 --- a/address_space/src/lib.rs +++ b/address_space/src/lib.rs @@ -94,7 +94,9 @@ pub use anyhow::Result; pub use crate::address_space::{AddressSpace, RegionCache}; pub use address::{AddressRange, GuestAddress}; pub use error::AddressSpaceError; -pub use host_mmap::{create_backend_mem, create_default_mem, FileBackend, HostMemMapping}; +pub use host_mmap::{ + create_backend_mem, create_default_mem, create_hotplug_mem, FileBackend, HostMemMapping, +}; #[cfg(target_arch = "x86_64")] pub use listener::KvmIoListener; pub use listener::KvmMemoryListener; diff --git a/devices/src/acpi/memory_controller.rs b/devices/src/acpi/memory_controller.rs new file mode 100644 index 00000000..e875dd06 --- /dev/null +++ b/devices/src/acpi/memory_controller.rs @@ -0,0 +1,797 @@ +// Copyright (c) 2023 China Telecom Co.,Ltd. All rights reserved. +// Copyright (c) 2023 Huawei Technologies Co.,Ltd. All rights reserved. +// +// StratoVirt is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::sync::{Arc, Mutex}; + +use anyhow::{bail, Context, Result}; +use log::{error, info}; + +use crate::sysbus::{SysBus, SysBusDevBase, SysBusDevOps, SysRes}; +use crate::{Device, DeviceBase}; +use acpi::{ + AcpiError, AmlAcquire, AmlAdd, AmlAddressSpaceType, AmlArg, AmlBuilder, AmlCallWithArgs1, + AmlCallWithArgs2, AmlCallWithArgs4, AmlCreateDWordField, AmlCreateQWordField, AmlDevice, + AmlEisaId, AmlEqual, AmlField, AmlFieldAccessType, AmlFieldLockRule, AmlFieldUnit, + AmlFieldUpdateRule, AmlIf, AmlInteger, AmlLLess, AmlLocal, AmlMethod, AmlMutex, AmlName, + AmlNameDecl, AmlNotify, AmlOne, AmlOpRegion, AmlQWordDesc, AmlRelease, AmlResTemplate, + AmlReturn, AmlScopeBuilder, AmlStore, AmlString, AmlSubtract, AmlZero, +}; +use address_space::{create_hotplug_mem, AddressSpace, GuestAddress, Region}; +use machine_manager::config::{memory_unit_conversion, MachineMemConfig, M}; + +const MEMORY_ENABLE_FLAG: u8 = 1; +const MEMORY_INSERTING_FLAG: u8 = 2; +const MEMORY_REMOVING_FLAG: u8 = 4; +const MEMORY_EJECT_FLAG: u8 = 8; + +const BASE_OFFSET_LOW: u64 = 0; +const BASE_OFFSET_HIGH: u64 = 0x4; +const LENGTH_OFFSET_LOW: u64 = 0x8; +const LENGTH_OFFSET_HIGH: u64 = 0xC; +const MEMORY_STATUS_OFFSET: u64 = 0x14; +const MEMORY_SELECTION_OFFSET: u64 = 0x18; +const MEMORY_SLOT_ID_OFFSET: u64 = 0x1C; +const MEMORY_SLOT_EVENT_CODE_OFFSET: u64 = 0x20; + +#[derive(Clone, Default)] +pub struct MemoryController { + base: SysBusDevBase, + // Number of memory slots. + slots: u8, + // Map of hotplugged slot id and device id. + id_map: HashMap, + // Map of slot id and region. + slot_regions: HashMap, + // Address space segments sorted by start address which are not continue, (start address, size). + address_segments: Vec<(u64, u64)>, + // Memory address space. + sys_mem: Option>, + // Hotplug options: + // true - hotplug a dimm. + // false - hotunplug a dimm. + // None - do nothing. + hotplug: Option>, + // Slot id to hot(un)plug memory. + slot_id: Arc, + // Max memory size of vm. + max_size: u64, + // Memory size attached to vm. + used_size: u64, + // Always point to continue address space(exclude address segments) start address. + min_free_address: u64, + // Acpi selected slot id (for status check). + selected_slot: u8, +} + +impl MemoryController { + pub fn realize( + mut self, + sysbus: &mut SysBus, + sys_mem: Arc, + mem_config: &MachineMemConfig, + min_free_address: u64, + region_base: u64, + region_size: u64, + ) -> Result>> { + self.sys_mem = Some(sys_mem); + self.slots = mem_config.slots; + self.max_size = mem_config.max_size; + self.used_size = mem_config.mem_size; + self.min_free_address = min_free_address; + + self.set_sys_resource(sysbus, region_base, region_size) + .with_context(|| AcpiError::Alignment(region_size.try_into().unwrap()))?; + + let dev = Arc::new(Mutex::new(self)); + sysbus.attach_device(&dev, region_base, region_size, "MemoryController")?; + + Ok(dev.clone()) + } + + pub fn add_address_segments(&mut self, address: u64, size: u64) -> Result<()> { + let mut join_ahead = false; + let mut join_after = false; + let mut position = 0_usize; + for (_, segment) in self.address_segments.iter().enumerate() { + if address < segment.0 { + if address + size == segment.0 { + join_after = true; + } + break; + } else { + position += 1; + } + } + if position > 0 { + let segment = self.address_segments.get(position - 1).unwrap(); + if segment.0 + segment.1 == address { + join_ahead = true; + } + } + + let mut new_sgement = (address, size); + if join_after { + let segment = self.address_segments.remove(position); + new_sgement.1 += segment.1; + } + if join_ahead { + let segment = self.address_segments.remove(position - 1); + new_sgement.0 = segment.0; + new_sgement.1 += segment.1; + self.address_segments.insert(position - 1, new_sgement); + return Ok(()); + } + + self.address_segments.insert(position, new_sgement); + Ok(()) + } + + pub fn check_available_mem_size(&self, mem_size: &str) -> Result { + if self.used_size == self.max_size { + bail!("There is no hotpluggable memory space.") + } + let ram_size = memory_unit_conversion(mem_size, M)?; + let free_size = self.max_size - self.used_size; + + if ram_size > free_size { + bail!( + "Doesn't have enough space to hotplug memory, only have {} byte space", + free_size + ) + } + + Ok(ram_size) + } + + pub fn check_id_valid(&self, input_device_id: &str, input_slot_id: u8) -> Result<()> { + if self.slots == 0 { + bail!("There is no hotpluggable slot for this VM.") + } + if input_slot_id >= self.slots { + bail!("Max slot id is {}.", self.slots - 1) + } + + for (slot_id, id) in &self.id_map { + if id == input_device_id { + bail!("Device id {} already existed.", input_device_id) + } + // If slot id exist and device id is not empty, this slot is used. + if slot_id == &input_slot_id && !id.is_empty() { + bail!("Slot id {} is used, device id is {}.", input_slot_id, id) + } + } + Ok(()) + } + + pub fn create_region( + &mut self, + offset: u64, + ram_size: u64, + device_id: &str, + mem_path: &str, + ) -> Result { + let mem_config = self.get_mem_config(ram_size, mem_path)?; + let region = create_hotplug_mem(GuestAddress(offset), &mem_config, device_id)?; + self.used_size += ram_size; + + Ok(region) + } + + pub fn create_region_in_segment( + &mut self, + index: usize, + offset: u64, + ram_size: u64, + device_id: &str, + mem_path: &str, + ) -> Result { + let mem_config = self.get_mem_config(ram_size, mem_path)?; + let region = create_hotplug_mem(GuestAddress(offset), &mem_config, device_id)?; + self.update_address_segment(index, ram_size)?; + self.used_size += ram_size; + Ok(region) + } + + pub fn find_available_address_segment(&self, size: u64) -> Option<(usize, u64)> { + for (index, segment) in self.address_segments.iter().enumerate() { + if size <= segment.1 { + return Some((index, segment.0)); + } + } + None + } + + pub fn get_mem_config(&self, mem_size: u64, mem_path: &str) -> Result { + let mut mem_config = MachineMemConfig { + mem_size, + ..Default::default() + }; + if mem_path.is_empty() { + mem_config.mem_path = None; + } else { + mem_config.mem_path = Some(mem_path.replace('\"', "")); + } + mem_config.mem_share = false; + mem_config.mem_prealloc = false; + Ok(mem_config) + } + + pub fn get_min_free_address(&self) -> u64 { + self.min_free_address + } + + pub fn set_min_free_address(&mut self, size: u64, increase: bool) -> Result<()> { + if increase { + self.min_free_address += size; + } else { + self.min_free_address -= size; + } + Ok(()) + } + + pub fn set_hotplug_memory( + &mut self, + device_id: String, + slot_id: u8, + region: Region, + offset: u64, + ) -> Result<()> { + let sys_mem = self.sys_mem.as_ref().unwrap(); + sys_mem.root().add_subregion(region.clone(), offset)?; + + self.slot_regions.insert(slot_id, region); + self.id_map.insert(slot_id, device_id); + self.slot_id.store(slot_id, Ordering::SeqCst); + if let Some(plug) = &self.hotplug { + plug.store(true, Ordering::SeqCst); + } else { + self.hotplug = Some(Arc::new(AtomicBool::new(true))); + } + + Ok(()) + } + + pub fn set_hotunplug_memory(&mut self, slot_id: u8) -> Result<()> { + if let Some(plug) = &self.hotplug { + plug.store(false, Ordering::SeqCst); + } else { + self.hotplug = Some(Arc::new(AtomicBool::new(false))); + } + self.slot_id.store(slot_id, Ordering::SeqCst); + Ok(()) + } + + pub fn find_slot_id_by_device_id(&self, input_device_id: &str) -> Option { + for (slot_id, device_id) in &self.id_map { + if device_id == input_device_id { + return Some(*slot_id); + } + } + None + } + + fn delete_slot(&mut self, slot_id: u8) -> Result<()> { + if let Some(sys_mem) = &self.sys_mem { + if let Some(region) = self.slot_regions.remove(&slot_id) { + self.id_map.remove(&slot_id); + let region_address = region.offset().0; + let size = region.size(); + sys_mem.root().delete_subregion(®ion).with_context(|| { + format!( + "Failed to delete region in memory space: base={},size={}", + region.offset().raw_value(), + region.size(), + ) + })?; + self.used_size -= size; + if region_address + size == self.min_free_address { + self.set_min_free_address(size, false)?; + } else { + self.add_address_segments(region_address, size)? + } + } else { + bail!("There is no region for slot {}", slot_id) + } + } else { + bail!("Sys mem is none"); + } + Ok(()) + } + + fn get_slot_info(&self, slot_id: u8) -> (u64, u64) { + if let Some(region) = self.slot_regions.get(&slot_id) { + (region.offset().raw_value(), region.size()) + } else { + (0, 0) + } + } + + fn update_address_segment(&mut self, index: usize, used_size: u64) -> Result<()> { + let segment = self.address_segments.remove(index); + if used_size < segment.1 { + self.address_segments + .insert(index, (segment.0 + used_size, segment.1 - used_size)); + } + Ok(()) + } +} + +impl Device for MemoryController { + fn device_base(&self) -> &DeviceBase { + &self.base.base + } + + fn device_base_mut(&mut self) -> &mut DeviceBase { + &mut self.base.base + } +} + +impl SysBusDevOps for MemoryController { + fn sysbusdev_base(&self) -> &SysBusDevBase { + &self.base + } + + fn sysbusdev_base_mut(&mut self) -> &mut SysBusDevBase { + &mut self.base + } + + fn read(&mut self, data: &mut [u8], _base: address_space::GuestAddress, offset: u64) -> bool { + let (memory_offset, memory_size) = self.get_slot_info(self.selected_slot); + match offset { + BASE_OFFSET_LOW => { + data.copy_from_slice(&memory_offset.to_le_bytes()[..4]); + } + BASE_OFFSET_HIGH => { + data.copy_from_slice(&memory_offset.to_le_bytes()[4..]); + } + LENGTH_OFFSET_LOW => { + data.copy_from_slice(&memory_size.to_le_bytes()[..4]); + } + LENGTH_OFFSET_HIGH => { + data.copy_from_slice(&memory_size.to_le_bytes()[4..]); + } + MEMORY_STATUS_OFFSET => { + data.fill(0); + if self.slot_regions.contains_key(&self.selected_slot) { + data[0] |= MEMORY_ENABLE_FLAG; + } + + if let Some(hotplug) = &self.hotplug { + if hotplug.load(Ordering::SeqCst) { + data[0] |= MEMORY_INSERTING_FLAG; + } else { + data[0] |= MEMORY_REMOVING_FLAG; + } + } + } + MEMORY_SLOT_ID_OFFSET => { + data[0] = self.slot_id.load(Ordering::SeqCst); + } + MEMORY_SLOT_EVENT_CODE_OFFSET => { + info!("Receive _OST event code {}", data[0]); + } + _ => { + error!( + "Unexpected offset for accessing memory manager device: {}", + offset + ); + return false; + } + } + true + } + + fn write(&mut self, data: &[u8], _base: address_space::GuestAddress, offset: u64) -> bool { + match offset { + MEMORY_SLOT_EVENT_CODE_OFFSET => { + info!("Receive _OST event code {}", data[0]); + } + MEMORY_SELECTION_OFFSET => { + self.selected_slot = data[0]; + } + MEMORY_STATUS_OFFSET => { + match data[0] { + // Reset hotplug flag after memory inserting notified. + MEMORY_INSERTING_FLAG => self.hotplug = None, + // Reset hotplug flag after memory removing notified. + MEMORY_REMOVING_FLAG => self.hotplug = None, + // Eject memory after guest os eject cpu device. + MEMORY_EJECT_FLAG => { + let slot_id = self.slot_id.load(Ordering::SeqCst); + if let Err(_e) = self.delete_slot(slot_id) { + error!("Eject failed {}", slot_id) + } + } + _ => { + error!( + "Unexpected data[0] value for memory status offset: {}", + data[0] + ); + return false; + } + } + } + _ => { + error!( + "Unexpected offset for write MemoryController device: {}", + offset + ); + return false; + } + } + true + } + + fn get_sys_resource(&mut self) -> Option<&mut SysRes> { + Some(&mut self.base.res) + } +} + +impl AmlBuilder for MemoryController { + fn aml_bytes(&self) -> Vec { + let res = self.base.res; + let mut memory_hotplug_controller = AmlDevice::new("MHPC"); + memory_hotplug_controller.append_child(AmlNameDecl::new("_HID", AmlEisaId::new("PNP0A06"))); + memory_hotplug_controller.append_child(AmlNameDecl::new( + "_UID", + AmlString("Memory Hotplug Controller".into()), + )); + memory_hotplug_controller.append_child(AmlMutex::new("MLCK", 0)); + let mut crs = AmlResTemplate::new(); + crs.append_child(AmlQWordDesc::new_memory( + acpi::AmlAddressSpaceDecode::Positive, + acpi::AmlCacheable::Cacheable, + acpi::AmlReadAndWrite::ReadWrite, + 0, + res.region_base, + res.region_base + res.region_size - 1, + 0, + res.region_size, + )); + memory_hotplug_controller.append_child(AmlNameDecl::new("_CRS", crs)); + let mhpr = AmlOpRegion::new( + "MHPR", + AmlAddressSpaceType::SystemMemory, + res.region_base, + res.region_size, + ); + memory_hotplug_controller.append_child(mhpr); + // Access by bytes => data[u8] in mmio read/write. + let mut mhpr_field_1 = AmlField::new( + "MHPR", + AmlFieldAccessType::DWord, + AmlFieldLockRule::NoLock, + AmlFieldUpdateRule::Preserve, + ); + + mhpr_field_1.append_child(AmlFieldUnit::new("MHBL".into(), 32)); + mhpr_field_1.append_child(AmlFieldUnit::new("MHBH".into(), 32)); + mhpr_field_1.append_child(AmlFieldUnit::new("MHLL".into(), 32)); + mhpr_field_1.append_child(AmlFieldUnit::new("MHLH".into(), 32)); + memory_hotplug_controller.append_child(mhpr_field_1); + let mut mhpr_field_2 = AmlField::new( + "MHPR", + AmlFieldAccessType::DWord, + AmlFieldLockRule::NoLock, + AmlFieldUpdateRule::Preserve, + ); + + mhpr_field_2.append_child(AmlFieldUnit::new(None, 128)); + mhpr_field_2.append_child(AmlFieldUnit::new("MHPX".into(), 32)); + memory_hotplug_controller.append_child(mhpr_field_2); + let mut mhpr_field_3 = AmlField::new( + "MHPR", + AmlFieldAccessType::Byte, + AmlFieldLockRule::NoLock, + AmlFieldUpdateRule::WriteAsZeros, + ); + mhpr_field_3.append_child(AmlFieldUnit::new(None, 160)); + mhpr_field_3.append_child(AmlFieldUnit::new("MEN_".into(), 1)); + mhpr_field_3.append_child(AmlFieldUnit::new("MINS".into(), 1)); + mhpr_field_3.append_child(AmlFieldUnit::new("MRMV".into(), 1)); + mhpr_field_3.append_child(AmlFieldUnit::new("MEJ_".into(), 1)); + memory_hotplug_controller.append_child(mhpr_field_3); + let mut mhpr_field_4 = AmlField::new( + "MHPR", + AmlFieldAccessType::DWord, + AmlFieldLockRule::NoLock, + AmlFieldUpdateRule::Preserve, + ); + mhpr_field_4.append_child(AmlFieldUnit::new(None, 192)); + mhpr_field_4.append_child(AmlFieldUnit::new("MSEL".into(), 32)); + mhpr_field_4.append_child(AmlFieldUnit::new("MSID".into(), 32)); + mhpr_field_4.append_child(AmlFieldUnit::new("MOEV".into(), 32)); + + memory_hotplug_controller.append_child(mhpr_field_4); + // Memory methods. + memory_hotplug_controller.append_child(AmlMemoryStatusMethod {}); + memory_hotplug_controller.append_child(AmlMemoryRangeMethod {}); + memory_hotplug_controller.append_child(AmlMemoryStatusIndicationMethod {}); + memory_hotplug_controller.append_child(AmlMemoryEjectMethod {}); + memory_hotplug_controller.append_child(AmlMemoryNotifyMethod { slots: self.slots }); + memory_hotplug_controller.append_child(AmlMemoryResizeMethod {}); + // Memory slots. + for slot_id in 0..self.slots { + memory_hotplug_controller.append_child(AmlMemorySlot { + slot_id, + dynamic: true, + }); + } + + memory_hotplug_controller.aml_bytes() + } +} + +struct AmlMemorySlot { + slot_id: u8, + dynamic: bool, +} + +impl AmlMemorySlot { + fn sta_method(&self, return_value: Option) -> AmlMethod { + let mut sta_method = AmlMethod::new("_STA", 0, false); + if let Some(value) = return_value { + sta_method.append_child(AmlReturn::with_value(AmlInteger(value))); + } else { + let call_method_msta = AmlCallWithArgs1::new("MSTA", AmlInteger(self.slot_id.into())); + sta_method.append_child(AmlReturn::with_value(call_method_msta)); + } + sta_method + } + + fn crs_method(&self) -> AmlMethod { + let mut crs_method = AmlMethod::new("_CRS", 0, false); + crs_method.append_child(AmlReturn::with_value(AmlCallWithArgs1::new( + "MCRS", + AmlInteger(self.slot_id.into()), + ))); + crs_method + } + + fn ost_method(&self) -> AmlMethod { + let mut ost_method = AmlMethod::new("_OST", 3, false); + ost_method.append_child(AmlReturn::with_value(AmlCallWithArgs4::new( + "MOST", + AmlInteger(self.slot_id.into()), + AmlArg(0), + AmlArg(1), + AmlArg(2), + ))); + ost_method + } + + fn ej0_method(&self) -> AmlMethod { + let mut ej0_method = AmlMethod::new("_EJ0", 1, false); + ej0_method.append_child(AmlCallWithArgs1::new( + "MEJ0", + AmlInteger(self.slot_id.into()), + )); + ej0_method + } +} + +impl AmlBuilder for AmlMemorySlot { + fn aml_bytes(&self) -> Vec { + let mut memory_device = AmlDevice::new(format!("\\_SB.MHPC.M{:03}", self.slot_id).as_str()); + memory_device.append_child(AmlNameDecl::new("_HID", AmlString("PNP0C80".into()))); + memory_device.append_child(AmlNameDecl::new("_UID", AmlInteger(self.slot_id.into()))); + if self.dynamic { + memory_device.append_child(self.sta_method(None)); + memory_device.append_child(self.crs_method()); + memory_device.append_child(self.ost_method()); + memory_device.append_child(self.ej0_method()); + } else { + memory_device.append_child(self.sta_method(Some(0xfu64))); + } + memory_device.aml_bytes() + } +} + +struct AmlMemoryRangeMethod {} + +impl AmlBuilder for AmlMemoryRangeMethod { + fn aml_bytes(&self) -> Vec { + let mut memory_range_method = AmlMethod::new("MCRS", 1, true); + + memory_range_method + .append_child(AmlAcquire::new(AmlName("\\_SB.MHPC.MLCK".into()), 0xffff)); + memory_range_method + .append_child(AmlStore::new(AmlArg(0), AmlName("\\_SB.MHPC.MSEL".into()))); + let mut mr64_res = AmlResTemplate::new(); + mr64_res.append_child(AmlQWordDesc::new_memory( + acpi::AmlAddressSpaceDecode::Positive, + acpi::AmlCacheable::Cacheable, + acpi::AmlReadAndWrite::ReadWrite, + 0, + 0x0000_0000_0000_0000u64, + 0xFFFF_FFFF_FFFF_FFFEu64, + 0, + 0xFFFF_FFFF_FFFF_FFFFu64, + )); + memory_range_method.append_child(AmlNameDecl::new("MR64", mr64_res)); + memory_range_method.append_child(AmlCreateQWordField::new( + AmlName("MR64".into()), + AmlInteger(14u64), + "MINL", + )); + memory_range_method.append_child(AmlCreateDWordField::new( + AmlName("MR64".into()), + AmlInteger(18u64), + "MINH", + )); + memory_range_method.append_child(AmlCreateQWordField::new( + AmlName("MR64".into()), + AmlInteger(22u64), + "MAXL", + )); + memory_range_method.append_child(AmlCreateDWordField::new( + AmlName("MR64".into()), + AmlInteger(26u64), + "MAXH", + )); + memory_range_method.append_child(AmlCreateQWordField::new( + AmlName("MR64".into()), + AmlInteger(38u64), + "LENL", + )); + memory_range_method.append_child(AmlCreateDWordField::new( + AmlName("MR64".into()), + AmlInteger(42u64), + "LENH", + )); + memory_range_method.append_child(AmlStore::new( + AmlName("\\_SB.MHPC.MHBL".into()), + AmlName("MINL".into()), + )); + memory_range_method.append_child(AmlStore::new( + AmlName("\\_SB.MHPC.MHBH".into()), + AmlName("MINH".into()), + )); + memory_range_method.append_child(AmlStore::new( + AmlName("\\_SB.MHPC.MHLL".into()), + AmlName("LENL".into()), + )); + memory_range_method.append_child(AmlStore::new( + AmlName("\\_SB.MHPC.MHLH".into()), + AmlName("LENH".into()), + )); + memory_range_method.append_child(AmlAdd::new( + AmlName("MINL".into()), + AmlName("LENL".into()), + AmlName("MAXL".into()), + )); + memory_range_method.append_child(AmlAdd::new( + AmlName("MINH".into()), + AmlName("LENH".into()), + AmlName("MAXH".into()), + )); + let mut if_scope = AmlIf::new(AmlLLess::new( + AmlName("MAXL".into()), + AmlName("MINL".into()), + )); + if_scope.append_child(AmlAdd::new( + AmlName("MAXH".into()), + AmlOne, + AmlName("MAXH".into()), + )); + memory_range_method.append_child(if_scope); + memory_range_method.append_child(AmlSubtract::new( + AmlName("MAXL".into()), + AmlOne, + AmlName("MAXL".into()), + )); + memory_range_method.append_child(AmlRelease::new(AmlName("MLCK".to_string()))); + memory_range_method.append_child(AmlReturn::with_value(AmlName("MR64".into()))); + memory_range_method.aml_bytes() + } +} + +struct AmlMemoryStatusIndicationMethod {} + +impl AmlBuilder for AmlMemoryStatusIndicationMethod { + fn aml_bytes(&self) -> Vec { + let mut memory_status_indication_method = AmlMethod::new("MOST", 4, false); + memory_status_indication_method + .append_child(AmlAcquire::new(AmlName("\\_SB.MHPC.MLCK".into()), 0xffff)); + + memory_status_indication_method + .append_child(AmlStore::new(AmlArg(2), AmlName("\\_SB.MHPC.MOEV".into()))); + memory_status_indication_method.append_child(AmlRelease::new(AmlName("MLCK".to_string()))); + memory_status_indication_method.aml_bytes() + } +} + +struct AmlMemoryNotifyMethod { + slots: u8, +} + +impl AmlBuilder for AmlMemoryNotifyMethod { + fn aml_bytes(&self) -> Vec { + let mut memory_notify_method = AmlMethod::new("MTFY", 2, true); + for slot_id in 0..self.slots { + let mut if_scope = AmlIf::new(AmlEqual::new(AmlArg(0), AmlInteger(slot_id.into()))); + if_scope.append_child(AmlNotify::new( + AmlName(format!("\\_SB.MHPC.M{:03}", slot_id)), + AmlArg(1), + )); + memory_notify_method.append_child(if_scope); + } + memory_notify_method.aml_bytes() + } +} + +pub struct AmlMemoryStatusMethod {} + +impl AmlBuilder for AmlMemoryStatusMethod { + fn aml_bytes(&self) -> Vec { + let mut msta_method = AmlMethod::new("MSTA", 1, true); + msta_method.append_child(AmlAcquire::new(AmlName("\\_SB.MHPC.MLCK".into()), 0xffff)); + msta_method.append_child(AmlStore::new(AmlZero, AmlLocal(0))); + msta_method.append_child(AmlStore::new(AmlArg(0), AmlName("\\_SB.MHPC.MSEL".into()))); + + let mut if_scope = AmlIf::new(AmlEqual::new(AmlName("\\_SB.MHPC.MEN_".into()), AmlOne)); + if_scope.append_child(AmlStore::new(AmlInteger(0xfu64), AmlLocal(0))); + msta_method.append_child(if_scope); + msta_method.append_child(AmlRelease::new(AmlName("\\_SB.MHPC.MLCK".to_string()))); + + msta_method.append_child(AmlReturn::with_value(AmlLocal(0))); + msta_method.aml_bytes() + } +} + +pub struct AmlMemoryEjectMethod {} + +impl AmlBuilder for AmlMemoryEjectMethod { + fn aml_bytes(&self) -> Vec { + let mut eject_method = AmlMethod::new("MEJ0", 1, true); + eject_method.append_child(AmlAcquire::new(AmlName("\\_SB.MHPC.MLCK".into()), 0xffff)); + eject_method.append_child(AmlStore::new(AmlOne, AmlName("\\_SB.MHPC.MEJ_".into()))); + eject_method.append_child(AmlRelease::new(AmlName("\\_SB.MHPC.MLCK".to_string()))); + eject_method.aml_bytes() + } +} + +pub struct AmlMemoryResizeMethod {} + +impl AmlBuilder for AmlMemoryResizeMethod { + fn aml_bytes(&self) -> Vec { + let mut mscn_method = AmlMethod::new("MSCN", 1, true); + mscn_method.append_child(AmlAcquire::new(AmlName("\\_SB.MHPC.MLCK".into()), 0xffff)); + mscn_method.append_child(AmlStore::new( + AmlName("\\_SB.MHPC.MSID".into()), + AmlLocal(0), + )); + + mscn_method.append_child(AmlStore::new( + AmlLocal(0), + AmlName("\\_SB.MHPC.MSEL".into()), + )); + + let mut if_plug_scope = + AmlIf::new(AmlEqual::new(AmlName("\\_SB.MHPC.MINS".into()), AmlOne)); + if_plug_scope.append_child(AmlCallWithArgs2::new("MTFY", AmlLocal(0), AmlOne)); + if_plug_scope.append_child(AmlStore::new(AmlOne, AmlName("\\_SB.MHPC.MINS".into()))); + mscn_method.append_child(if_plug_scope); + + let mut if_unplug_scope = + AmlIf::new(AmlEqual::new(AmlName("\\_SB.MHPC.MRMV".into()), AmlOne)); + if_unplug_scope.append_child(AmlCallWithArgs2::new("MTFY", AmlLocal(0), AmlInteger(3u64))); + if_unplug_scope.append_child(AmlStore::new(AmlOne, AmlName("\\_SB.MHPC.MRMV".into()))); + mscn_method.append_child(if_unplug_scope); + + mscn_method.append_child(AmlRelease::new(AmlName("\\_SB.MHPC.MLCK".to_string()))); + mscn_method.aml_bytes() + } +} diff --git a/devices/src/acpi/mod.rs b/devices/src/acpi/mod.rs index a97e0975..c5bca52d 100644 --- a/devices/src/acpi/mod.rs +++ b/devices/src/acpi/mod.rs @@ -13,4 +13,6 @@ #[cfg(target_arch = "x86_64")] pub mod cpu_controller; pub mod ged; +#[cfg(target_arch = "x86_64")] +pub mod memory_controller; pub mod power; diff --git a/machine/src/x86_64/standard.rs b/machine/src/x86_64/standard.rs index 4b334a8d..0c08afdb 100644 --- a/machine/src/x86_64/standard.rs +++ b/machine/src/x86_64/standard.rs @@ -35,6 +35,7 @@ use boot_loader::{load_linux, BootLoaderConfig}; use cpu::{CPUBootConfig, CPUInterface, CPUTopology, CPU}; use devices::acpi::cpu_controller::{CpuConfig, CpuController}; use devices::acpi::ged::{Ged, GedEvent}; +use devices::acpi::memory_controller::MemoryController; use devices::legacy::{ error::LegacyError as DevErrorKind, FwCfgEntryType, FwCfgIO, FwCfgOps, PFlash, Serial, RTC, SERIAL_ADDR, @@ -75,6 +76,7 @@ pub enum LayoutEntryType { PcieMmio, GedMmio, CpuController, + MemoryController, Mmio, IoApic, LocalApic, @@ -89,6 +91,7 @@ pub const MEM_LAYOUT: &[(u64, u64)] = &[ (0xC000_0000, 0x3000_0000), // PcieMmio (0xF000_0000, 0x04), // GedMmio (0xF000_0004, 0x03), // CpuController + (0xF000_0007, 0x50), // MemoryController (0xF010_0000, 0x200), // Mmio (0xFEC0_0000, 0x10_0000), // IoApic (0xFEE0_0000, 0x10_0000), // LocalApic @@ -131,6 +134,8 @@ pub struct StdMachine { boot_order_list: Arc>>, /// Cpu Controller. cpu_controller: Option>>, + /// Memory Controller. + mem_controller: Option>>, } impl StdMachine { @@ -179,6 +184,7 @@ impl StdMachine { ), boot_order_list: Arc::new(Mutex::new(Vec::new())), cpu_controller: None, + mem_controller: None, }) } @@ -274,6 +280,41 @@ impl StdMachine { self.cpu_controller = Some(realize_controller); Ok(()) } + + fn init_memory_controller(&mut self) -> Result<()> { + let mut memory_controller = MemoryController::default(); + + let region_base = MEM_LAYOUT[LayoutEntryType::MemoryController as usize].0; + let region_size = MEM_LAYOUT[LayoutEntryType::MemoryController as usize].1; + + let clone_vm_config = self.base.vm_config.clone(); + let mem_config = &clone_vm_config.lock().unwrap().machine_config.mem_config; + let mem_size = mem_config.mem_size; + let mem_below4g = MEM_LAYOUT[LayoutEntryType::MemBelow4g as usize].1; + let min_free_address = if mem_size > mem_below4g { + MEM_LAYOUT[LayoutEntryType::MemAbove4g as usize].0 + mem_size - mem_below4g + } else { + memory_controller.add_address_segments( + MEM_LAYOUT[LayoutEntryType::MemBelow4g as usize].0 + mem_size, + MEM_LAYOUT[LayoutEntryType::MemBelow4g as usize].1 - mem_size, + )?; + MEM_LAYOUT[LayoutEntryType::MemAbove4g as usize].0 + }; + + let realize_controller = memory_controller + .realize( + &mut self.base.sysbus, + self.base.sys_mem.clone(), + mem_config, + min_free_address, + region_base, + region_size, + ) + .with_context(|| "Failed to realize Memory Controller")?; + + self.mem_controller = Some(realize_controller); + Ok(()) + } } impl StdMachineOps for StdMachine { @@ -575,6 +616,7 @@ impl MachineOps for StdMachine { )?); locked_vm.init_cpu_controller(boot_config.unwrap(), topology, vm.clone())?; + locked_vm.init_memory_controller()?; if migrate.0 == MigrateMode::Unknown { if let Some(fw_cfg) = fwcfg { -- Gitee From 8bbb6930c531b98568877eb95a188bb33dc2d1d9 Mon Sep 17 00:00:00 2001 From: dehengli Date: Mon, 15 Jan 2024 15:19:37 +0800 Subject: [PATCH 3/6] machine: x86_64 standard machine realize memory hotplug Realize memory hotplug feature with qmp device_add command on x86_64 standard machine. Signed-off-by: dehengli --- machine/src/standard_common/mod.rs | 51 ++++++++++++++++++++ machine/src/x86_64/standard.rs | 36 ++++++++++++++ machine_manager/src/cmdline.rs | 7 ++- machine_manager/src/config/machine_config.rs | 22 ++++++++- machine_manager/src/qmp/qmp_schema.rs | 22 +++++++++ 5 files changed, 135 insertions(+), 3 deletions(-) diff --git a/machine/src/standard_common/mod.rs b/machine/src/standard_common/mod.rs index 30afc97f..5c320c5c 100644 --- a/machine/src/standard_common/mod.rs +++ b/machine/src/standard_common/mod.rs @@ -258,6 +258,23 @@ pub(crate) trait StdMachineOps: AcpiBuilder + MachineOps { #[cfg(target_arch = "x86_64")] fn find_cpu_id_by_device_id(&mut self, device_id: &str) -> Option; + /// Add memory device. + /// + /// # Argumemts + /// + /// * `device_id` - The name of memory device. + /// * `slot_id` - Slot id that memory want to plug into. + /// * `mem_size` - Memory size want to add. + /// * `memdev` - Memory-backend device. + #[cfg(target_arch = "x86_64")] + fn add_memory_device( + &mut self, + device_id: String, + slot_id: u8, + mem_size: &str, + memdev: &str, + ) -> Result<()>; + /// Register event notifier for reset of standard machine. /// /// # Arguments @@ -1232,6 +1249,29 @@ impl StdMachine { bail!("Argument cpu-id is required.") } } + + #[cfg(target_arch = "x86_64")] + fn plug_memory_device(&mut self, args: &qmp_schema::DeviceAddArgument) -> Result<()> { + if self.get_numa_nodes().is_some() { + bail!("Not support to hotplug memory in numa architecture now."); + } + let device_id = args.id.clone(); + if device_id.is_empty() { + bail!("Device id can't be empty.") + } + + if let Some(slot_id) = args.slot_id { + if let Some(mem_size) = &args.mem_size { + let mem_path = args.mem_path.as_ref().map_or("", String::as_str); + self.add_memory_device(device_id, slot_id, mem_size, mem_path)?; + Ok(()) + } else { + bail!("Argument mem-size is required.") + } + } else { + bail!("Argument slot-id is required.") + } + } } impl MachineAddressInterface for StdMachine { @@ -1467,6 +1507,17 @@ impl DeviceInterface for StdMachine { } return Response::create_empty_response(); } + #[cfg(target_arch = "x86_64")] + "generic-dimm" => { + if let Err(e) = self.plug_memory_device(args.as_ref()) { + error!("{:?}", e); + return Response::create_error_response( + qmp_schema::QmpErrorClass::GenericError(e.to_string()), + None, + ); + } + return Response::create_empty_response(); + } _ => { let err_str = format!("Failed to add device: Driver {} is not support", driver); return Response::create_error_response( diff --git a/machine/src/x86_64/standard.rs b/machine/src/x86_64/standard.rs index 0c08afdb..48ea0dec 100644 --- a/machine/src/x86_64/standard.rs +++ b/machine/src/x86_64/standard.rs @@ -438,6 +438,42 @@ impl StdMachineOps for StdMachine { let locked_controller = self.cpu_controller.as_ref().unwrap().lock().unwrap(); locked_controller.find_cpu_by_device_id(device_id) } + + fn add_memory_device( + &mut self, + device_id: String, + slot_id: u8, + mem_size: &str, + mem_path: &str, + ) -> Result<()> { + let mut locked_controller = self.mem_controller.as_ref().unwrap().lock().unwrap(); + locked_controller.check_id_valid(&device_id, slot_id)?; + let ram_size = locked_controller.check_available_mem_size(mem_size)?; + // Setup hotplug memory. + let migrate_info = self.get_migrate_info(); + if migrate_info.0 != MigrateMode::File { + // Try to create region in address segment. + let (offset, region) = if let Some((index, offset)) = + locked_controller.find_available_address_segment(ram_size) + { + let region = locked_controller + .create_region_in_segment(index, offset, ram_size, &device_id, mem_path)?; + (offset, region) + } else { + let offset = locked_controller.get_min_free_address(); + let region = + locked_controller.create_region(offset, ram_size, &device_id, mem_path)?; + locked_controller.set_min_free_address(ram_size, true)?; + (offset, region) + }; + locked_controller.set_hotplug_memory(device_id, slot_id, region, offset)?; + } + // Trigger GED memory resize event. + self.mem_resize_req + .write(1) + .with_context(|| "Failed to write cpu resize request.")?; + Ok(()) + } } impl MachineOps for StdMachine { diff --git a/machine_manager/src/cmdline.rs b/machine_manager/src/cmdline.rs index fbfd7b03..c7fb9fc9 100644 --- a/machine_manager/src/cmdline.rs +++ b/machine_manager/src/cmdline.rs @@ -130,8 +130,11 @@ pub fn create_args_parser<'a>() -> ArgParser<'a> { .arg( Arg::with_name("memory") .long("m") - .value_name("[size=][m|M|g|G]") - .help("configure guest RAM(default unit: MiB).") + .value_name("[size=][m|M|g|G][,slots=][,max-size=]") + .help("configure guest RAM(default unit: MiB). \ + 'size' is vm startup ram size, which can't hotplug/hotunplug. \ + 'slots' is the number of slot use for hotplug ram. \ + 'max-size' is the max ram size of the vm.") .takes_value(true), ) .arg( diff --git a/machine_manager/src/config/machine_config.rs b/machine_manager/src/config/machine_config.rs index 8195b965..9b367400 100644 --- a/machine_manager/src/config/machine_config.rs +++ b/machine_manager/src/config/machine_config.rs @@ -120,6 +120,8 @@ pub struct MachineMemConfig { pub mem_share: bool, pub mem_prealloc: bool, pub mem_zones: Option>, + pub slots: u8, + pub max_size: u64, } impl Default for MachineMemConfig { @@ -131,6 +133,8 @@ impl Default for MachineMemConfig { mem_share: false, mem_prealloc: false, mem_zones: None, + slots: 0, + max_size: DEFAULT_MEMSIZE * M, } } } @@ -280,7 +284,11 @@ impl VmConfig { /// Add '-m' memory config to `VmConfig`. pub fn add_memory(&mut self, mem_config: &str) -> Result<()> { let mut cmd_parser = CmdParser::new("m"); - cmd_parser.push("").push("size"); + cmd_parser + .push("") + .push("size") + .push("slots") + .push("max-size"); cmd_parser.parse(mem_config)?; @@ -295,7 +303,17 @@ impl VmConfig { ))); }; + if let Some(slots) = cmd_parser.get_value::("slots")? { + self.machine_config.mem_config.slots = slots.parse::().unwrap(); + } + + let max_mem_size = if let Some(max_size) = cmd_parser.get_value::("max-size")? { + memory_unit_conversion(&max_size, M)? + } else { + mem + }; self.machine_config.mem_config.mem_size = mem; + self.machine_config.mem_config.max_size = max_mem_size; Ok(()) } @@ -725,6 +743,8 @@ mod tests { dump_guest_core: false, mem_prealloc: false, mem_zones: None, + slots: 0, + max_size: DEFAULT_MEMSIZE * M, }; let mut machine_config = MachineConfig { mach_type: MachineType::MicroVm, diff --git a/machine_manager/src/qmp/qmp_schema.rs b/machine_manager/src/qmp/qmp_schema.rs index 19e99310..40d99dea 100644 --- a/machine_manager/src/qmp/qmp_schema.rs +++ b/machine_manager/src/qmp/qmp_schema.rs @@ -322,6 +322,12 @@ pub struct device_add { pub isobsize: Option, #[serde(rename = "cpu-id")] pub cpu_id: Option, + #[serde(rename = "mem-path")] + pub mem_path: Option, + #[serde(rename = "mem-size")] + pub mem_size: Option, + #[serde(rename = "slot-id")] + pub slot_id: Option, } pub type DeviceAddArgument = device_add; @@ -1772,6 +1778,7 @@ define_qmp_event_enum!( Resume("RESUME", Resume, default), Powerdown("POWERDOWN", Powerdown, default), CpuResize("CPU_RESIZE", CpuResize, default), + MemoryResize("MEMORY_RESIZE", MemoryResize, default), DeviceDeleted("DEVICE_DELETED", DeviceDeleted), BalloonChanged("BALLOON_CHANGED", BalloonInfo) ); @@ -1885,6 +1892,21 @@ pub struct Powerdown {} #[serde(deny_unknown_fields)] pub struct CpuResize {} +/// MemoryResize +/// +/// Emitted when the virtual machine memory hot(un)plug execution. +/// +/// # Examples +/// +/// ```text +/// <- { "event": "MEMORY_RESIZE", +/// "data": {}, +/// "timestamp": { "seconds": 1265044230, "microseconds": 450486 } } +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct MemoryResize {} + /// DeviceDeleted /// /// Emitted whenever the device removal completion is acknowledged by the guest. -- Gitee From 6ae24e1946e15f9965a44f4e0eceb7afe0ba2d61 Mon Sep 17 00:00:00 2001 From: dehengli Date: Wed, 27 Dec 2023 12:26:37 +0800 Subject: [PATCH 4/6] machine: x86_64 standard machine realize memory hotunplug Realize memory hotunplug feature with qmp device_del command on x86_64 standard machine. Signed-off-by: dehengli --- machine/src/standard_common/mod.rs | 28 ++++++++++++++++++++++++++++ machine/src/x86_64/standard.rs | 19 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/machine/src/standard_common/mod.rs b/machine/src/standard_common/mod.rs index 5c320c5c..296ac07d 100644 --- a/machine/src/standard_common/mod.rs +++ b/machine/src/standard_common/mod.rs @@ -275,6 +275,22 @@ pub(crate) trait StdMachineOps: AcpiBuilder + MachineOps { memdev: &str, ) -> Result<()>; + /// Remove memory device. + /// + /// # Argumemts + /// + /// * `slot_id` - The id of memory slot which wants to remove. + #[cfg(target_arch = "x86_64")] + fn remove_memory_device(&self, slot_id: u8) -> Result<()>; + + /// Find slot id by device id. + /// + /// # Arguments + /// + /// * `device_id` - The name of memory device. + #[cfg(target_arch = "x86_64")] + fn find_slot_id_by_device_id(&mut self, device_id: &str) -> Option; + /// Register event notifier for reset of standard machine. /// /// # Arguments @@ -1599,6 +1615,18 @@ impl DeviceInterface for StdMachine { }; } + // Assume it is a memory device, try to find this device in memory device + #[cfg(target_arch = "x86_64")] + if let Some(slot_id) = self.find_slot_id_by_device_id(&device_id) { + return match self.remove_memory_device(slot_id) { + Ok(()) => Response::create_empty_response(), + Err(e) => Response::create_error_response( + qmp_schema::QmpErrorClass::GenericError(e.to_string()), + None, + ), + }; + } + // The device is not a pci device and not a cpu device, assume it is a usb device. match self.handle_unplug_usb_request(device_id) { Ok(()) => Response::create_empty_response(), diff --git a/machine/src/x86_64/standard.rs b/machine/src/x86_64/standard.rs index 48ea0dec..f7c9e446 100644 --- a/machine/src/x86_64/standard.rs +++ b/machine/src/x86_64/standard.rs @@ -474,6 +474,25 @@ impl StdMachineOps for StdMachine { .with_context(|| "Failed to write cpu resize request.")?; Ok(()) } + + fn remove_memory_device(&self, slot_id: u8) -> Result<()> { + if self.base.numa_nodes.is_some() { + bail!("It's not support to hotunplug memory in numa architecture now.") + } + + let mut locked_controller = self.mem_controller.as_ref().unwrap().lock().unwrap(); + locked_controller.set_hotunplug_memory(slot_id)?; + // Trigger GED memory resize event. + self.mem_resize_req + .write(1) + .with_context(|| "Failed to write cpu resize request.")?; + Ok(()) + } + + fn find_slot_id_by_device_id(&mut self, device_id: &str) -> Option { + let locked_controller = self.mem_controller.as_ref().unwrap().lock().unwrap(); + locked_controller.find_slot_id_by_device_id(device_id) + } } impl MachineOps for StdMachine { -- Gitee From c8994257e20acd8d1d119dc0da60f4f2a610419a Mon Sep 17 00:00:00 2001 From: dehengli Date: Tue, 26 Dec 2023 11:11:40 +0800 Subject: [PATCH 5/6] tests: add memory hotplug test Signed-off-by: dehengli --- .../tests/x86_64/memory_hotplug_test.rs | 293 ++++++++++++++++++ tests/mod_test/tests/x86_64/mod.rs | 1 + 2 files changed, 294 insertions(+) create mode 100644 tests/mod_test/tests/x86_64/memory_hotplug_test.rs diff --git a/tests/mod_test/tests/x86_64/memory_hotplug_test.rs b/tests/mod_test/tests/x86_64/memory_hotplug_test.rs new file mode 100644 index 00000000..49b8dcb6 --- /dev/null +++ b/tests/mod_test/tests/x86_64/memory_hotplug_test.rs @@ -0,0 +1,293 @@ +// Copyright (c) 2023 China Telecom Co.,Ltd. All rights reserved. +// Copyright (c) 2023 Huawei Technologies Co.,Ltd. All rights reserved. +// +// StratoVirt is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +use std::borrow::BorrowMut; + +use serde_json::{json, Value}; + +use machine::x86_64::standard::{LayoutEntryType, MEM_LAYOUT}; +use mod_test::libtest::{test_init, TestState, MACHINE_TYPE_ARG}; + +pub const GED_ADDR_BASE: u64 = MEM_LAYOUT[LayoutEntryType::GedMmio as usize].0; + +fn ged_read_evt(ts: &TestState) -> u32 { + ts.readl(GED_ADDR_BASE) +} + +fn set_up(size: u64, slots: Option, max_size: Option) -> TestState { + // Vm extra_args. + let mut extra_args: Vec<&str> = Vec::new(); + let mut args: Vec<&str> = MACHINE_TYPE_ARG.split(' ').collect(); + extra_args.append(&mut args); + + let mem_args = if slots.is_some() && max_size.is_some() { + format!( + "-m {},slots={},max-size={}", + size, + slots.unwrap(), + max_size.unwrap(), + ) + } else if slots.is_none() && max_size.is_some() { + format!("-m {},max-size={}", size, max_size.unwrap()) + } else if slots.is_some() && max_size.is_none() { + format!("-m {},slots={}", size, slots.unwrap()) + } else { + format!("-m {}", size) + }; + args = mem_args[..].split(' ').collect(); + extra_args.append(&mut args); + + extra_args.push("-append"); + extra_args.push("root=/dev/vda reboot=k panic=1 movable_node"); + + let uefi_drive = + format!("-drive file=/usr/share/edk2/ovmf/OVMF_CODE.fd,if=pflash,unit=0,readonly=true"); + args = uefi_drive[..].split(' ').collect(); + extra_args.append(&mut args); + + let root_device = format!("-device pcie-root-port,port=0x0,addr=0x1.0x0,bus=pcie.0,id=pcie.1"); + args = root_device[..].split(' ').collect(); + extra_args.append(&mut args); + + args = "-disable-seccomp -daemonize".split(' ').collect(); + extra_args.append(&mut args); + + test_init(extra_args) +} + +fn hotplug_memory(test_state: &mut TestState, id: &str, slot_id: u8, mem_size: u64) -> Value { + test_state.borrow_mut().qmp(&format!( + "{{\"execute\": \"device_add\",\"arguments\": {{ \"id\": \"{id}\", \"driver\": \"generic-dimm\", \"slot-id\": {slot_id}, \"mem-size\": \"{mem_size}\"}}}}" + )) +} + +fn hotunplug_memory(test_state: &mut TestState, id: &str) -> Value { + test_state.borrow_mut().qmp(&format!( + "{{\"execute\": \"device_del\", \"arguments\": {{\"id\": \"{id}\"}}}}" + )) +} + +fn assert_response(result: Value, index: &str, expect: Option) { + if index == "return" { + assert_eq!(*result.get("return").unwrap(), json!({})) + } else { + assert_eq!( + result["error"]["desc"].as_str().unwrap().to_string(), + expect.unwrap(), + ) + } +} + +/// Normal memory hotplug. +/// TestStep: +/// 1. Send id mem-1, slot-id 0 and mem-size 512 hotplug qmp command. +/// 2. Read ged event, expect 32. +/// 3. Destroy VM. +/// Expect: +/// 1/2/3: success. +#[test] +fn normal_hotplug_memory() { + let mut ts = set_up(512, Some(1), Some(1024)); + + let ret = hotplug_memory(&mut ts, "mem-1", 0, 512); + assert_response(ret, "return", None); + + let event = ged_read_evt(&ts); + assert_eq!(event, 32); + + ts.borrow_mut().stop(); +} + +/// Normal memory hotunplug. +/// TestStep: +/// 1. Send id mem-1, slot-id 0 and mem-size 512 hotplug qmp command. +/// 2. Send id mem-1 hotunplug qmp command +/// 4. Read ged event, expect 32. +/// 4. Destroy VM. +/// Expect: +/// 1/2/3/4: success. +#[test] +pub fn normal_hotunplug_memory() { + let mut ts = set_up(512, Some(2), Some(1024)); + + let ret = hotplug_memory(&mut ts, "mem-1", 0, 512); + assert_response(ret, "return", None); + ts.qmp_read(); + + let ret = hotunplug_memory(&mut ts, "mem-1"); + assert_response(ret, "return", None); + + let event = ged_read_evt(&ts); + assert_eq!(event, 32); + + ts.borrow_mut().stop(); +} + +/// Hotplug memory with an existed id. +/// TestStep: +/// 1. Send id mem-1, slot-id 0 and mem-size 256 hotplug qmp command. +/// 2. Send id mem-1, slot-id 1 and mem-size 256 hotplug qmp command. +/// 3. Destroy VM. +/// Expect: +/// 1/3: Success. +/// 2: Failed. +#[test] +fn existed_id_hotplug_memory() { + let mut ts = set_up(512, Some(2), Some(1024)); + + let ret = hotplug_memory(&mut ts, "mem-1", 0, 256); + assert_response(ret, "return", None); + ts.qmp_read(); + + let ret = hotplug_memory(&mut ts, "mem-1", 1, 256); + assert_response( + ret, + "error", + Some("Device id mem-1 already existed.".to_string()), + ); + + ts.borrow_mut().stop(); +} + +/// Hotplug memory with an existed slot id. +/// TestStep: +/// 1. Send id mem-1, slot-id 0 and mem-size 512 hotplug qmp command. +/// 2. Send id mem-2, slot-id 0 and mem-size 512 hotplug qmp command. +/// 3. Destroy VM. +/// Expect: +/// 1/3: Success. +/// 2: Failed. +#[test] +fn existed_slot_id_hotplug_memory() { + let mut ts = set_up(512, Some(2), Some(2048)); + + let ret = hotplug_memory(&mut ts, "mem-1", 0, 512); + assert_response(ret, "return", None); + ts.qmp_read(); + + let ret = hotplug_memory(&mut ts, "mem-2", 0, 512); + assert_response( + ret, + "error", + Some("Slot id 0 is used, device id is mem-1.".to_string()), + ); + + ts.borrow_mut().stop(); +} + +/// Hotplug memory with empty id. +/// TestStep: +/// 1. Send empty id, slot-id 0 and mem-size 512 hotplug qmp command. +/// 2. Destroy VM. +/// Expect: +/// 2: Success. +/// 1: Failed. +#[test] +fn empty_id_hotplug_memory() { + let mut ts = set_up(512, Some(2), Some(1024)); + + let ret = hotplug_memory(&mut ts, "", 0, 512); + assert_response(ret, "error", Some("Device id is empty".to_string())); + + ts.borrow_mut().stop(); +} + +/// Hotplug memory with an overrange slot id. +/// TestStep: +/// 1. Send id mem-1, slot-id 0 and mem-size 512 hotplug qmp command. +/// 2. Send id mem-2, slot-id 1 and mem-size 512 hotplug qmp command. +/// 3. Destroy VM. +/// Expect: +/// 1/3: Success. +/// 2: Failed. +#[test] +fn overrange_hotplug_memory() { + let mut ts = set_up(512, Some(1), Some(1024)); + + let ret = hotplug_memory(&mut ts, "mem-1", 0, 128); + assert_response(ret, "return", None); + ts.qmp_read(); + + let ret = hotplug_memory(&mut ts, "mem-2", 1, 128); + assert_response(ret, "error", Some("Max slot id is 0.".to_string())); + + ts.borrow_mut().stop(); +} + +/// Hotplug memory with an oversize memory size. +/// TestStep: +/// 1. Send id mem-1, slot-id 0 and mem-size 1024 hotplug qmp command. +/// 2. Destroy VM. +/// Expect: +/// 2: Success. +/// 1: Failed. +#[test] +fn oversize_hotplug_memory() { + let mut ts = set_up(512, Some(1), Some(1024)); + + let ret = hotplug_memory(&mut ts, "mem-1", 0, 1024); + assert_response( + ret, + "error", + Some( + "Doesn't have enough space to hotplug memory, only have 536870912 byte space" + .to_string(), + ), + ); + + ts.borrow_mut().stop(); +} + +/// Hotplug memory when max-size is not explicitly configured. +/// TestSetp: +/// 1. Send id mem-1, slot-id 0 and mem-size 512 hotplug qmp command. +/// 2. Destroy VM. +/// Expect: +/// 2: Success. +/// 1: Failed. +#[test] +fn without_config_max_size_hotplug_memory() { + let mut ts = set_up(512, Some(1), None); + + let ret = hotplug_memory(&mut ts, "mem-1", 0, 1024); + + assert_response( + ret, + "error", + Some("There is no hotpluggable memory space.".to_string()), + ); + + ts.borrow_mut().stop(); +} + +/// Hotplug memory when slots is not explicitly configured. +/// TestSetp: +/// 1. Send id mem-1, slot-id 0 and mem-size 512 hotplug qmp command. +/// 2. Destroy VM. +/// Expect: +/// 2: Success. +/// 1: Failed. +#[test] +fn without_config_slots_hotplug_memory() { + let mut ts = set_up(512, None, Some(1024)); + + let ret = hotplug_memory(&mut ts, "mem-1", 0, 512); + + assert_response( + ret, + "error", + Some("There is no hotpluggable slot for this VM.".to_string()), + ); + + ts.borrow_mut().stop(); +} diff --git a/tests/mod_test/tests/x86_64/mod.rs b/tests/mod_test/tests/x86_64/mod.rs index 42cf6f78..8a2c20af 100644 --- a/tests/mod_test/tests/x86_64/mod.rs +++ b/tests/mod_test/tests/x86_64/mod.rs @@ -12,3 +12,4 @@ // See the Mulan PSL v2 for more details. mod cpu_hotplug_test; +mod memory_hotplug_test; -- Gitee From 7ec648b18479d9e1802e36bca793d6f517de4554 Mon Sep 17 00:00:00 2001 From: dehengli Date: Wed, 27 Dec 2023 14:19:40 +0800 Subject: [PATCH 6/6] docs: add documents for memory hotplug Add documents for x86_64 standard machine memory hotplug and hotunplug feature. Signed-off-by: dehengli --- docs/memory_hotplug.ch.md | 63 +++++++++++++++++++++++++++++++++++++++ docs/memory_hotplug.md | 61 +++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 docs/memory_hotplug.ch.md create mode 100644 docs/memory_hotplug.md diff --git a/docs/memory_hotplug.ch.md b/docs/memory_hotplug.ch.md new file mode 100644 index 00000000..9e29ac72 --- /dev/null +++ b/docs/memory_hotplug.ch.md @@ -0,0 +1,63 @@ +# 内存热插拔 + +StratoVirt支持对一个运行中的虚机进行内存的热插入和热拔出。该功能可以动态调整虚机的内存资源。目前,该功能只支持x86_64的标准虚机,并且不包含NUMA架构。 + +## 创建虚机 + +首先,创建一台虚机。 + +```shell +$ ./stratovirt \ + -machine q35 \ + -smp 1 \ + -m [size=],slots=,max-size= \ + -kernel path/to/std-vmlinuz \ + -append "console=ttyS0 root=/dev/vda reboot=k panic=1 movable_node" \ + -drive file=path/to/OVMF_CODE.fd,if=pflash,unit=0,readonly=true \ + -device pcie-root-port,port=0x0,addr=0x1.0x0,bus=pcie.0,id=pcie.1 \ + -drive file=path/to/rootfs,id=rootfs,readonly=true \ + -device virtio-blk-pci,drive=rootfs,bus=pcie.1,addr=0x0.0x0,id=blk-0 \ + -qmp unix:path/to/api/socket,server,nowait \ + -serial stdio +``` + +- `size`: 设置虚机的启动内存(默认单位: MiB), 启动内存不支持热拔出. +- `slots`: 设置用于热插拔内存的内存槽的数量为'n'. +- `max-size`: 设置虚机的最大内存值,包括了启动内存和热插拔内存. + +## 热插入内存 + +虚机启动后,通过QMP热插入内存: + +```shell +$ ncat -U /path/to/api/socket +{"QMP":{"version":{"qemu":{"micro":1,"minor":0,"major":5},"package":"StratoVirt-2.3.0"},"capabilities":[]}} +-> {"execute": "device_add", "arguments": { "id": "device-id", "driver": "generic-dimm", "mem-size": "mem_size", "slot-id": slotid, "mem-path": "mem_path"}} +<- {"return":{}} +<- {"event":"MEMORY_RESIZE","data":{},"timestamp":{"seconds":seconds, "microseconds":microseconds}} +``` + +- `id`: (必需) 内存设备的ID, 该ID应该为全局唯一的字符串。 +- `mem-size`: (必需) 想要插入的内存大小,默认单位是M(MiB),可用的单位包含[k, K, m, M, g, G]。热插入的内存大小需要是128M的整数倍。 +- `slot-id`: (必需) 内存的编号, 编号的取值范围是[0, `slots`)内的整数。 +- `mem-path`: (可选) 存储热插内存的文件路径。 + +## 热拔出内存 + +通过QMP热拔出内存: + +```shell +$ ncat -U /path/to/api/socket +{"QMP":{"version":{"qemu":{"micro":1,"minor":0,"major":5},"package":"StratoVirt-2.3.0"},"capabilities":[]}} +-> {"execute": "device_del", "arguments": { "id": "device-id"}} +<- {"return":{}} +<- {"event":"MEMORY_RESIZE","data":{},"timestamp":{"seconds":seconds, "microseconds":microseconds}} +``` + +## Limitations + +内存热插拔支持的虚机类型: +- `q35` (on x86_64 platform) + +内存热插拔不支持的设备和特性: +- `numa` \ No newline at end of file diff --git a/docs/memory_hotplug.md b/docs/memory_hotplug.md new file mode 100644 index 00000000..8755c26d --- /dev/null +++ b/docs/memory_hotplug.md @@ -0,0 +1,61 @@ +# Memory hotplug and hotunplug + +Stratovirt supports to hot(un)plug memory to a running VM. This feature supports dynamic adjustment of memory resources of VM. Currently, only standard VM with x86_64 architecture are supported, and NUMA architecture is not supported. + +## Create VM + +```shell +$ ./stratovirt \ + -machine q35 \ + -smp 1 \ + -m [size=],slots=,max-size= \ + -kernel path/to/std-vmlinuz \ + -append "console=ttyS0 root=/dev/vda reboot=k panic=1 movable_node" \ + -drive file=path/to/OVMF_CODE.fd,if=pflash,unit=0,readonly=true \ + -device pcie-root-port,port=0x0,addr=0x1.0x0,bus=pcie.0,id=pcie.1 \ + -drive file=path/to/rootfs,id=rootfs,readonly=true \ + -device virtio-blk-pci,drive=rootfs,bus=pcie.1,addr=0x0.0x0,id=blk-0 \ + -qmp unix:path/to/api/socket,server,nowait \ + -serial stdio +``` + +- `size`: Set the startup ram size(default unit: MiB) of VM, and can't be hotunplugged. +- `slots`: Set the number of slot to 'n' use for hotplug ram. +- `max-size`: Set the total ram of vm, including startup ram and hotplug ram. + +## Hotplug Memory + +After the VM boot up, hotplug memory with QMP: + +```shell +$ ncat -U /path/to/api/socket +{"QMP":{"version":{"qemu":{"micro":1,"minor":0,"major":5},"package":"StratoVirt-2.3.0"},"capabilities":[]}} +-> {"execute": "device_add", "arguments": { "id": "device-id", "driver": "generic-dimm", "mem-size": "mem_size", "slot-id": slotid, "mem-path": "mem_path"}} +<- {"return":{}} +<- {"event":"MEMORY_RESIZE","data":{},"timestamp":{"seconds":seconds, "microseconds":microseconds}} +``` + +- `id`: (Required) The ID of the Memory device, which should be a globally unique string. +- `mem-size`: (Required) The memory size wants to hotplug, default unit is M(starnds for MiB), available units are [k, K, m, M, g, G]. The hotplugged memory size should be an integer multiple of 128MiB. +- `slot-id`: (Required) The number of this memory device, whick can be an integer in the range of [0, `slots`) +- `mem-path`: (Optional) The file path that backs hotplug memory. + +## Hotunplug Memory + +hotunplug memory with QMP: + +```shell +$ ncat -U /path/to/api/socket +{"QMP":{"version":{"qemu":{"micro":1,"minor":0,"major":5},"package":"StratoVirt-2.3.0"},"capabilities":[]}} +-> {"execute": "device_del", "arguments": { "id": "device-id"}} +<- {"return":{}} +<- {"event":"MEMORY_RESIZE","data":{},"timestamp":{"seconds":seconds, "microseconds":microseconds}} +``` + +## Limitations + +Memory hot(un)plug support machine type: +- `q35` (on x86_64 platform) + +Some devices and feature don't support to be memory hotplug yet: +- `numa` \ No newline at end of file -- Gitee