mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
vmm: introduce ACPI CPU hotplug controller (fix deadlock)
Extract AcpiCpuHotplugController from CpuManager and move the BusDevice implementation to the new type. This separates VMM-internal vCPU management from the guest-visible ACPI CPU hotplug MMIO interface. Besides clarifying responsibilities and reducing technical debt, this fixes a rare deadlock involving pause handling and MMIO access. New responsibilities: - CpuManager manages VMM-internal vCPU lifecycle and coordination - AcpiCpuHotplugController implements the guest-visible ACPI CPU hotplug MMIO interface A vCPU thread may exit KVM_RUN to perform an MMIO access previously handled by CpuManager. If the VMM thread begins processing a `pause` event before that MMIO operation acquires access to CpuManager, CpuManager::pause() will block waiting for the vCPU thread to ACK the pause, while the vCPU thread is blocked waiting to complete the MMIO operation through the same CpuManager - which it can never lock - the VMM is deadlocked. This can occur during early boot or CPU hotplug when pause events race with MMIO accesses. The issue is rare and timing-dependent, but real. For reproducing: run `ch-remote pause|resume` in a loop while booting a Linux VM (via direct kernel boot). With the new design, these MMIO operations no longer depend on CpuManager, which removes the deadlock path entirely. On-behalf-of: SAP philipp.schuster@sap.com Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
This commit is contained in:
committed by
Rob Bradford
parent
6d0d4bc5e2
commit
5ff4696cea
233
vmm/src/cpu.rs
233
vmm/src/cpu.rs
@@ -682,8 +682,8 @@ pub struct CpuManager {
|
|||||||
reset_evt: EventFd,
|
reset_evt: EventFd,
|
||||||
#[cfg(feature = "guest_debug")]
|
#[cfg(feature = "guest_debug")]
|
||||||
vm_debug_evt: EventFd,
|
vm_debug_evt: EventFd,
|
||||||
|
// Shared with AcpiCpuHotplugController
|
||||||
vcpu_states: Arc<Mutex<Vec<VcpuState>>>,
|
vcpu_states: Arc<Mutex<Vec<VcpuState>>>,
|
||||||
selected_cpu: u32,
|
|
||||||
vcpus: Vec<Arc<Mutex<Vcpu>>>,
|
vcpus: Vec<Arc<Mutex<Vcpu>>>,
|
||||||
seccomp_action: SeccompAction,
|
seccomp_action: SeccompAction,
|
||||||
vm_ops: Arc<dyn VmOps>,
|
vm_ops: Arc<dyn VmOps>,
|
||||||
@@ -699,14 +699,6 @@ pub struct CpuManager {
|
|||||||
core_scheduling_group_leader: Arc<AtomicI32>,
|
core_scheduling_group_leader: Arc<AtomicI32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const CPU_ENABLE_FLAG: usize = 0;
|
|
||||||
const CPU_INSERTING_FLAG: usize = 1;
|
|
||||||
const CPU_REMOVING_FLAG: usize = 2;
|
|
||||||
const CPU_EJECT_FLAG: usize = 3;
|
|
||||||
|
|
||||||
const CPU_STATUS_OFFSET: u64 = 4;
|
|
||||||
const CPU_SELECTION_OFFSET: u64 = 0;
|
|
||||||
|
|
||||||
/// State of the core scheduling group leader election for VM-wide cookie
|
/// State of the core scheduling group leader election for VM-wide cookie
|
||||||
/// sharing.
|
/// sharing.
|
||||||
///
|
///
|
||||||
@@ -737,85 +729,6 @@ impl TryFrom<i32> for CoreSchedulingLeader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BusDevice for CpuManager {
|
|
||||||
fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) {
|
|
||||||
// The Linux kernel, quite reasonably, doesn't zero the memory it gives us.
|
|
||||||
data.fill(0);
|
|
||||||
let vcpu_states = self.vcpu_states.lock().unwrap();
|
|
||||||
|
|
||||||
match offset {
|
|
||||||
CPU_SELECTION_OFFSET => {
|
|
||||||
assert!(data.len() >= core::mem::size_of::<u32>());
|
|
||||||
data[0..core::mem::size_of::<u32>()]
|
|
||||||
.copy_from_slice(&self.selected_cpu.to_le_bytes());
|
|
||||||
}
|
|
||||||
CPU_STATUS_OFFSET => {
|
|
||||||
if self.selected_cpu < self.max_vcpus() {
|
|
||||||
let state = &vcpu_states[usize::try_from(self.selected_cpu).unwrap()];
|
|
||||||
if state.active() {
|
|
||||||
data[0] |= 1 << CPU_ENABLE_FLAG;
|
|
||||||
}
|
|
||||||
if state.inserting {
|
|
||||||
data[0] |= 1 << CPU_INSERTING_FLAG;
|
|
||||||
}
|
|
||||||
if state.removing {
|
|
||||||
data[0] |= 1 << CPU_REMOVING_FLAG;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warn!("Out of range vCPU id: {}", self.selected_cpu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
warn!("Unexpected offset for accessing CPU manager device: {offset:#}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write(&mut self, _base: u64, offset: u64, data: &[u8]) -> Option<Arc<Barrier>> {
|
|
||||||
match offset {
|
|
||||||
CPU_SELECTION_OFFSET => {
|
|
||||||
assert!(data.len() >= core::mem::size_of::<u32>());
|
|
||||||
self.selected_cpu =
|
|
||||||
u32::from_le_bytes(data[0..core::mem::size_of::<u32>()].try_into().unwrap());
|
|
||||||
}
|
|
||||||
CPU_STATUS_OFFSET => {
|
|
||||||
if self.selected_cpu < self.max_vcpus() {
|
|
||||||
let eject = {
|
|
||||||
// This structure is not shared with the vCPU thread, therefore, holding the
|
|
||||||
// lock for the entire function doesn't cause any deadlock.
|
|
||||||
let mut vcpu_states = self.vcpu_states.lock().unwrap();
|
|
||||||
let state = &mut vcpu_states[usize::try_from(self.selected_cpu).unwrap()];
|
|
||||||
|
|
||||||
if (data[0] & (1 << CPU_INSERTING_FLAG) == 1 << CPU_INSERTING_FLAG)
|
|
||||||
&& state.inserting
|
|
||||||
{
|
|
||||||
state.inserting = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data[0] & (1 << CPU_REMOVING_FLAG) == 1 << CPU_REMOVING_FLAG)
|
|
||||||
&& state.removing
|
|
||||||
{
|
|
||||||
state.removing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
data[0] & (1 << CPU_EJECT_FLAG) == 1 << CPU_EJECT_FLAG
|
|
||||||
};
|
|
||||||
|
|
||||||
if eject && let Err(e) = self.remove_vcpu(self.selected_cpu) {
|
|
||||||
error!("Error removing vCPU: {e:?}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warn!("Out of range vCPU id: {}", self.selected_cpu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
warn!("Unexpected offset for accessing CPU manager device: {offset:#}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct VcpuState {
|
struct VcpuState {
|
||||||
inserting: bool,
|
inserting: bool,
|
||||||
@@ -965,7 +878,6 @@ impl CpuManager {
|
|||||||
reset_evt,
|
reset_evt,
|
||||||
#[cfg(feature = "guest_debug")]
|
#[cfg(feature = "guest_debug")]
|
||||||
vm_debug_evt,
|
vm_debug_evt,
|
||||||
selected_cpu: 0,
|
|
||||||
vcpus: Vec::with_capacity(max_vcpus),
|
vcpus: Vec::with_capacity(max_vcpus),
|
||||||
seccomp_action,
|
seccomp_action,
|
||||||
vm_ops,
|
vm_ops,
|
||||||
@@ -1543,23 +1455,6 @@ impl CpuManager {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_vcpu(&mut self, cpu_id: u32) -> Result<()> {
|
|
||||||
info!("Removing vCPU: cpu_id = {cpu_id}");
|
|
||||||
let mut vcpu_states = self.vcpu_states.lock().unwrap();
|
|
||||||
let state = &mut vcpu_states[usize::try_from(cpu_id).unwrap()];
|
|
||||||
state.kill.store(true, Ordering::SeqCst);
|
|
||||||
state.signal_thread();
|
|
||||||
state.wait_until_signal_acknowledged()?;
|
|
||||||
state.join_thread()?;
|
|
||||||
state.handle = None;
|
|
||||||
|
|
||||||
// Once the thread has exited, clear the "kill" so that it can reused
|
|
||||||
state.kill.store(false, Ordering::SeqCst);
|
|
||||||
state.pending_removal.store(false, Ordering::SeqCst);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_boot_vcpus(
|
pub fn create_boot_vcpus(
|
||||||
&mut self,
|
&mut self,
|
||||||
snapshot: Option<&Snapshot>,
|
snapshot: Option<&Snapshot>,
|
||||||
@@ -3203,6 +3098,132 @@ impl CpuElf64Writable for CpuManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// MMIO-accessible controller for handling ACPI hotplug and unplug events.
|
||||||
|
///
|
||||||
|
/// Shares state about the vCPUs with the [`CpuManager`].
|
||||||
|
pub struct AcpiCpuHotplugController {
|
||||||
|
/// The currently selected CPU by the guest.
|
||||||
|
selected_cpu: u32,
|
||||||
|
/// Shared vCPU state with [`CpuManager`].
|
||||||
|
vcpu_states: Arc<Mutex<Vec<VcpuState>>>,
|
||||||
|
/// Maximum number of vCPUS of the VM.
|
||||||
|
max_vcpus: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcpiCpuHotplugController {
|
||||||
|
const CPU_ENABLE_FLAG: usize = 0;
|
||||||
|
const CPU_INSERTING_FLAG: usize = 1;
|
||||||
|
const CPU_REMOVING_FLAG: usize = 2;
|
||||||
|
const CPU_EJECT_FLAG: usize = 3;
|
||||||
|
|
||||||
|
const CPU_SELECTION_OFFSET: u64 = 0;
|
||||||
|
const CPU_STATUS_OFFSET: u64 = 4;
|
||||||
|
|
||||||
|
/// Creates a new [`AcpiCpuHotplugController`].
|
||||||
|
pub fn new(cpu_manager: &CpuManager) -> AcpiCpuHotplugController {
|
||||||
|
Self {
|
||||||
|
max_vcpus: cpu_manager.config.max_vcpus,
|
||||||
|
selected_cpu: 0,
|
||||||
|
vcpu_states: cpu_manager.vcpu_states.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a vCPU from the guest.
|
||||||
|
///
|
||||||
|
/// The corresponding vCPU thread will be gracefully stopped and joined.
|
||||||
|
fn remove_vcpu(cpu_id: u32, state: &mut VcpuState) -> Result<()> {
|
||||||
|
info!("Removing vCPU: cpu_id = {cpu_id}");
|
||||||
|
state.kill.store(true, Ordering::SeqCst);
|
||||||
|
state.signal_thread();
|
||||||
|
state.wait_until_signal_acknowledged()?;
|
||||||
|
state.join_thread()?;
|
||||||
|
state.handle = None;
|
||||||
|
|
||||||
|
// Once the thread has exited, clear the "kill" so that it can reused
|
||||||
|
state.kill.store(false, Ordering::SeqCst);
|
||||||
|
state.pending_removal.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BusDevice for AcpiCpuHotplugController {
|
||||||
|
fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) {
|
||||||
|
// The Linux kernel, quite reasonably, doesn't zero the memory it gives us.
|
||||||
|
data.fill(0);
|
||||||
|
let vcpu_states = self.vcpu_states.lock().unwrap();
|
||||||
|
|
||||||
|
match offset {
|
||||||
|
Self::CPU_SELECTION_OFFSET => {
|
||||||
|
assert!(data.len() >= core::mem::size_of::<u32>());
|
||||||
|
data[0..core::mem::size_of::<u32>()]
|
||||||
|
.copy_from_slice(&self.selected_cpu.to_le_bytes());
|
||||||
|
}
|
||||||
|
Self::CPU_STATUS_OFFSET => {
|
||||||
|
if self.selected_cpu < self.max_vcpus {
|
||||||
|
let state = &vcpu_states[usize::try_from(self.selected_cpu).unwrap()];
|
||||||
|
if state.active() {
|
||||||
|
data[0] |= 1 << Self::CPU_ENABLE_FLAG;
|
||||||
|
}
|
||||||
|
if state.inserting {
|
||||||
|
data[0] |= 1 << Self::CPU_INSERTING_FLAG;
|
||||||
|
}
|
||||||
|
if state.removing {
|
||||||
|
data[0] |= 1 << Self::CPU_REMOVING_FLAG;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Out of range vCPU id: {}", self.selected_cpu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("Unexpected offset for accessing CPU manager device: {offset:#}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, _base: u64, offset: u64, data: &[u8]) -> Option<Arc<Barrier>> {
|
||||||
|
match offset {
|
||||||
|
Self::CPU_SELECTION_OFFSET => {
|
||||||
|
assert!(data.len() >= core::mem::size_of::<u32>());
|
||||||
|
self.selected_cpu =
|
||||||
|
u32::from_le_bytes(data[0..core::mem::size_of::<u32>()].try_into().unwrap());
|
||||||
|
}
|
||||||
|
Self::CPU_STATUS_OFFSET => {
|
||||||
|
if self.selected_cpu < self.max_vcpus {
|
||||||
|
// This structure is not shared with the vCPU thread, therefore, holding the
|
||||||
|
// lock for the entire function doesn't cause any deadlock.
|
||||||
|
let mut vcpu_states = self.vcpu_states.lock().unwrap();
|
||||||
|
let state = &mut vcpu_states[usize::try_from(self.selected_cpu).unwrap()];
|
||||||
|
// The ACPI code writes back a 1 to acknowledge the insertion
|
||||||
|
if (data[0] & (1 << Self::CPU_INSERTING_FLAG) == 1 << Self::CPU_INSERTING_FLAG)
|
||||||
|
&& state.inserting
|
||||||
|
{
|
||||||
|
state.inserting = false;
|
||||||
|
}
|
||||||
|
// Ditto for removal
|
||||||
|
if (data[0] & (1 << Self::CPU_REMOVING_FLAG) == 1 << Self::CPU_REMOVING_FLAG)
|
||||||
|
&& state.removing
|
||||||
|
{
|
||||||
|
state.removing = false;
|
||||||
|
}
|
||||||
|
// Trigger removal of vCPU:
|
||||||
|
if data[0] & (1 << Self::CPU_EJECT_FLAG) == 1 << Self::CPU_EJECT_FLAG
|
||||||
|
&& let Err(e) = Self::remove_vcpu(self.selected_cpu, state)
|
||||||
|
{
|
||||||
|
error!("Error removing vCPU: {e:?}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Out of range vCPU id: {}", self.selected_cpu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("Unexpected offset for accessing CPU manager device: {offset:#}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
|
#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod unit_tests {
|
mod unit_tests {
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ use vm_virtio::{AccessPlatform, VirtioDeviceType};
|
|||||||
use vmm_sys_util::eventfd::EventFd;
|
use vmm_sys_util::eventfd::EventFd;
|
||||||
|
|
||||||
use crate::console_devices::{ConsoleDeviceError, ConsoleInfo, ConsoleTransport};
|
use crate::console_devices::{ConsoleDeviceError, ConsoleInfo, ConsoleTransport};
|
||||||
use crate::cpu::{CPU_MANAGER_ACPI_SIZE, CpuManager};
|
use crate::cpu::{AcpiCpuHotplugController, CPU_MANAGER_ACPI_SIZE, CpuManager};
|
||||||
use crate::device_tree::{DeviceNode, DeviceTree};
|
use crate::device_tree::{DeviceNode, DeviceTree};
|
||||||
use crate::interrupt::{LegacyUserspaceInterruptManager, MsiInterruptManager};
|
use crate::interrupt::{LegacyUserspaceInterruptManager, MsiInterruptManager};
|
||||||
use crate::memory_manager::{Error as MemoryManagerError, MEMORY_MANAGER_ACPI_SIZE, MemoryManager};
|
use crate::memory_manager::{Error as MemoryManagerError, MEMORY_MANAGER_ACPI_SIZE, MemoryManager};
|
||||||
@@ -1026,6 +1026,10 @@ pub struct DeviceManager {
|
|||||||
// CPU Manager
|
// CPU Manager
|
||||||
cpu_manager: Arc<Mutex<CpuManager>>,
|
cpu_manager: Arc<Mutex<CpuManager>>,
|
||||||
|
|
||||||
|
/// Owned version needed to keep the bus device alive (the bus only holds
|
||||||
|
/// a weak reference).
|
||||||
|
_acpi_cpu_hotplug_controller: Arc<Mutex<AcpiCpuHotplugController>>,
|
||||||
|
|
||||||
// The virtio devices on the system
|
// The virtio devices on the system
|
||||||
virtio_devices: Vec<MetaVirtioDevice>,
|
virtio_devices: Vec<MetaVirtioDevice>,
|
||||||
|
|
||||||
@@ -1324,6 +1328,10 @@ impl DeviceManager {
|
|||||||
)?);
|
)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let acpi_cpu_hotplug_controller =
|
||||||
|
AcpiCpuHotplugController::new(&cpu_manager.lock().unwrap());
|
||||||
|
let acpi_cpu_hotplug_controller = Arc::new(Mutex::new(acpi_cpu_hotplug_controller));
|
||||||
|
|
||||||
if dynamic {
|
if dynamic {
|
||||||
let acpi_address = address_manager
|
let acpi_address = address_manager
|
||||||
.allocator
|
.allocator
|
||||||
@@ -1335,7 +1343,7 @@ impl DeviceManager {
|
|||||||
address_manager
|
address_manager
|
||||||
.mmio_bus
|
.mmio_bus
|
||||||
.insert(
|
.insert(
|
||||||
cpu_manager.clone(),
|
acpi_cpu_hotplug_controller.clone(),
|
||||||
acpi_address.0,
|
acpi_address.0,
|
||||||
CPU_MANAGER_ACPI_SIZE as u64,
|
CPU_MANAGER_ACPI_SIZE as u64,
|
||||||
)
|
)
|
||||||
@@ -1429,6 +1437,7 @@ impl DeviceManager {
|
|||||||
fw_cfg: None,
|
fw_cfg: None,
|
||||||
#[cfg(feature = "ivshmem")]
|
#[cfg(feature = "ivshmem")]
|
||||||
ivshmem_device: None,
|
ivshmem_device: None,
|
||||||
|
_acpi_cpu_hotplug_controller: acpi_cpu_hotplug_controller,
|
||||||
};
|
};
|
||||||
|
|
||||||
let device_manager = Arc::new(Mutex::new(device_manager));
|
let device_manager = Arc::new(Mutex::new(device_manager));
|
||||||
|
|||||||
Reference in New Issue
Block a user