StratoVirt 中的 PCI 設備熱插拔實現

語言: CN / TW / HK

熱插拔即帶電插拔,在虛擬化場景下,熱插拔就是在虛擬機運行過程中對磁盤網卡等設備進行動態調整。

常見的熱插拔機制有 ACPI 機制的熱插拔,PCIe-Native 機制的熱插拔。ACPI 機制的熱插拔依賴 ACPI 表,在 ACPI 表中會存放設備熱插拔相關的信息。PCIe-Native 機制的熱插拔是 PCI 規範中定義的,設備一般是熱插到 Root Port 設備上,Root Port 設備可以認為是一個虛擬的橋設備,對應一個插槽。Root Port 設備本身不支持熱插拔,因此需要在啟動虛擬機前提前配置。

目前,StratoVirt 標準機型中實現了基於 PCIe-Native 機制的熱插拔。支持熱插拔的設備包括磁盤、網卡、PCI 直通設備。

熱插拔的整體流程如下:

對於熱插主要分為兩步:

  1. 用户通過 QMP 下發 device_add 命令,StratoVirt 收到命令後會進行設備的實例化,然後插入到對應的 Root Port 設備上。
  2. Root Port 設備更新相關的寄存器配置,然後發送中斷通知虛擬機內驅動處理。

對於熱拔也可以分為兩步:

  1. 用户通過 QMP 下發 device_del 命令,StratoVirt 收到命令後,更新 Root Port 中的寄存器,然後發送中斷通知虛擬機內驅動處理。
  2. 虛擬機內驅動處理後會回寫寄存器,觸發 StratoVirt 側銷燬相應設備。

具體實現

在 StratoVirt 的 pci/src/hotplug.rs 文件中定義了熱插拔特性,其中 plug 函數對應熱插操作,用於熱插設備。unplug_request 函數對應熱拔操作,用於發起熱拔設備請求,這裏只是通知虛擬機內驅動去處理熱拔請求,還未移除設備,可以理解為是一個異步請求。當虛擬機內驅動處理完成後,寫寄存器觸發設備下線後,會回調 unplug 函數用於銷燬設備。

pub trait HotplugOps: Send {    /// Plug device, usually called when hot plug device in device_add.    fn plug(&mut self, dev: &Arc<Mutex<dyn PciDevOps>>) -> Result<()>;
/// Unplug device request, usually called when hot unplug device in device_del. /// Only send unplug request to the guest OS, without actually removing the device. fn unplug_request(&mut self, dev: &Arc<Mutex<dyn PciDevOps>>) -> Result<()>;
/// Remove the device. fn unplug(&mut self, dev: &Arc<Mutex<dyn PciDevOps>>) -> Result<()>;}

熱插實現

StratoVirt 裏通過給 RootPort 實現了 HotplugOps 特性,使得 PCI 設備能夠熱插到 Root Port 設備上。

設備熱插的主要實現邏輯在 plug 函數裏。首先獲取了設備的 devfn 號,也就是 Device 號和 Function 號,目前熱插只支持 Device 號和 Function 號都為 0 的設備。因此這裏做了判斷。

然後會在 RootPort 設備的 PCI 配置空間中的 PCI Express Capability(PCI 配置空間和 PCI Express Capability 寄存器定義可以參考 PCI 規範)中設置 Slot 狀態寄存器和 Link 狀態寄存器,然後通過 hotplug_event_notify 函數發送中斷通知虛擬機。這裏熱插設備主要是通過 Attention Button Pressed(對應 PCI_EXP_HP_EV_ABP)事件觸發的。

這裏簡單介紹下不同標記位的含義。

符號 描述
PCI_EXP_SLTSTA Slot Status Register 表示 Slot 狀態寄存器,不同的位表示 Slot 不同的狀態
PCI_EXP_SLTSTA_PDS Presence Detect State 表示 Slot 上設備的在位狀態,置 1 表示在位
PCI_EXP_HP_EV_PDC Presence Detect Changed 表示 Slot 上設備在位狀態是否發生變化
PCI_EXP_HP_EV_ABP Attention Button Pressed 表示 Attention 按鈕被按下,該按鈕用於觸發熱插拔操作
PCI_EXP_LNKSTA Link Status Register 表示 Link 狀態的寄存器
PCI_EXP_LNKSTA_DLLLA Data Link Layer Link Active 表示數據鏈路控制和管理狀態,置 1 表示處於 Active 狀態
impl HotplugOps for RootPort {    fn plug(&mut self, dev: &Arc<Mutex<dyn PciDevOps>>) -> Result<()> {        let devfn = dev            .lock()            .unwrap()            .devfn()            .chain_err(|| "Failed to get devfn")?;        // Only if devfn is equal to 0, hot plugging is supported.        if devfn == 0 {            let offset = self.config.ext_cap_offset;            le_write_set_value_u16(                &mut self.config.config,                (offset + PCI_EXP_SLTSTA) as usize,                PCI_EXP_SLTSTA_PDS | PCI_EXP_HP_EV_PDC | PCI_EXP_HP_EV_ABP,            )?;            le_write_set_value_u16(                &mut self.config.config,                (offset + PCI_EXP_LNKSTA) as usize,                PCI_EXP_LNKSTA_NLW | PCI_EXP_LNKSTA_DLLLA,            )?;            self.hotplug_event_notify();        }        Ok(())    }}

在 hotplug_event_notify 函數中會調用 MSIX 中斷的 notify 函數發送中斷到虛擬機內,虛擬機內 pciehp 驅動收到中斷後會處理相關的熱插請求。

fn hotplug_event_notify(&mut self) {    if let Some(msix) = self.config.msix.as_mut() {        msix.lock()            .unwrap()            .notify(0, self.dev_id.load(Ordering::Acquire));    } else {        error!("Failed to send interrupt: msix does not exist");    }}

熱拔實現

對於設備熱拔請求的邏輯主要在 unplug_request 函數,該函數負責更新寄存器,並且通過調用 hotplug_event_notify 函數發送中斷通知虛擬機內驅動處理設備熱拔請求。

unplug_request 函數裏主要是清零了 Link 狀態寄存器中的 PCI_EXP_LNKSTA_DLLLA 標記位,並且在 Slot 狀態寄存器中的設置了 PCI_EXP_HP_EV_ABP 標記位。從這裏也可以發現,其實無論是熱插請求還是熱拔請求,都是通過 Attention Button Pressed(對應 PCI_EXP_HP_EV_ABP)事件觸發的,虛擬機內驅動會根據設備的在位狀態來判斷是熱插請求還是熱拔請求。

impl HotplugOps for RootPort {    fn unplug_request(&mut self, dev: &Arc<Mutex<dyn PciDevOps>>) -> Result<()> {        let devfn = dev            .lock()            .unwrap()            .devfn()            .chain_err(|| "Failed to get devfn")?;        if devfn != 0 {            return self.unplug(dev);        }
let offset = self.config.ext_cap_offset; le_write_clear_value_u16( &mut self.config.config, (offset + PCI_EXP_LNKSTA) as usize, PCI_EXP_LNKSTA_DLLLA, )?;
let mut slot_status = PCI_EXP_HP_EV_ABP; if let Some(&true) = FAST_UNPLUG_FEATURE.get() { slot_status |= PCI_EXP_HP_EV_PDC; } le_write_set_value_u16( &mut self.config.config, (offset + PCI_EXP_SLTSTA) as usize, slot_status, )?; self.hotplug_event_notify(); Ok(()) }}

對於熱拔設備,StratoVirt 側在更新寄存器發送中斷通知虛擬機內驅動後,實際上還沒有真正的移除設備,而是等到虛擬機內驅動處理後回寫寄存器通知 StratoVirt 側下線設備後,才會真正銷燬設備。

虛擬機內驅動寫 Root Port 寄存器會調用到 write_config 函數,在 write_config 函數裏會調用 do_unplug 函數來處理熱拔設備相關的邏輯。

    fn write_config(&mut self, offset: usize, data: &[u8]) {        ...
self.do_unplug(offset, end, old_ctl); }

do_unplug 函數裏首先保證了寫入的寄存器是 Slot Control 寄存器,否則直接返回,不做處理。然後判斷在設備當前在位的情況下,寫入的寄存器標記位為 PCI_EXP_SLTCTL_PWR_IND_OFF 和 PCI_EXP_SLTCTL_PCC 時,並且這兩個標記位發生了變化,也就是寫入之前的沒有這兩個標記位,上述條件都滿足時,會調用 remove_devices 函數開始真正銷燬設備。

符號 描述
PCI_EXP_SLTCTL_PCC Power Controller Control 表示電源管理狀態,置 1 表示上電狀態
PCI_EXP_SLTCTL_PWR_IND_OFF Power Indicator off 表示是否允許移除設備,置 1 表示設備允許被移除
fn do_unplug(&mut self, offset: usize, end: usize, old_ctl: u16) {    let cap_offset = self.config.ext_cap_offset;    // Only care the write config about slot control    if !ranges_overlap(        offset,        end,        (cap_offset + PCI_EXP_SLTCTL) as usize,        (cap_offset + PCI_EXP_SLTCTL + 2) as usize,    ) {        return;    }
let status = le_read_u16(&self.config.config, (cap_offset + PCI_EXP_SLTSTA) as usize).unwrap(); let val = le_read_u16(&self.config.config, offset).unwrap(); // Only unplug device when the slot is on // Don't unplug when slot is off for guest OS overwrite the off status before slot on. if (status & PCI_EXP_SLTSTA_PDS != 0) && (val as u16 & PCI_EXP_SLTCTL_PCC == PCI_EXP_SLTCTL_PCC) && (val as u16 & PCI_EXP_SLTCTL_PWR_IND_OFF == PCI_EXP_SLTCTL_PWR_IND_OFF) && (old_ctl & PCI_EXP_SLTCTL_PCC != PCI_EXP_SLTCTL_PCC || old_ctl & PCI_EXP_SLTCTL_PWR_IND_OFF != PCI_EXP_SLTCTL_PWR_IND_OFF) { self.remove_devices();
if let Err(e) = self.update_register_status() { error!("{}", e.display_chain()); error!("Failed to update register status"); } }
self.hotplug_command_completed(); self.hotplug_event_notify();}

在調用 remove_devices 函數移除設備之後,調用 update_register_status 函數更新寄存器的狀態,主要是清理了 Link 狀態和設備在位狀態,並且設置了 Presence Detect Changed(對應 PCI_EXP_HP_EV_PDC)標記位表示設備在位狀態發生了變化。

/// Update register when the guest OS trigger the removal of the device.fn update_register_status(&mut self) -> Result<()> {    let cap_offset = self.config.ext_cap_offset;    le_write_clear_value_u16(        &mut self.config.config,        (cap_offset + PCI_EXP_SLTSTA) as usize,        PCI_EXP_SLTSTA_PDS,    )?;    le_write_clear_value_u16(        &mut self.config.config,        (cap_offset + PCI_EXP_LNKSTA) as usize,        PCI_EXP_LNKSTA_DLLLA,    )?;    le_write_set_value_u16(        &mut self.config.config,        (cap_offset + PCI_EXP_SLTSTA) as usize,        PCI_EXP_SLTSTA_PDC,    )?;    Ok(())}

在更新完寄存器後,在 hotplug_command_completed 還會設置 Command Completed(對應 PCI_EXP_HP_EV_CCI)表示命令處理完成,最後再發送中斷通知虛擬機內驅動。至此,整個設備熱拔流程就結束了。

fn hotplug_command_completed(&mut self) {    if let Err(e) = le_write_set_value_u16(        &mut self.config.config,        (self.config.ext_cap_offset + PCI_EXP_SLTSTA) as usize,        PCI_EXP_HP_EV_CCI,    ) {        error!("{}", e.display_chain());        error!("Failed to write command completed");    }}
符號 描述
PCI_EXP_HP_EV_CCI Command Completed 表示命令處理完成,可以處理下一條命令

總結

PCIe Native 機制的熱插拔主要是通過 Root Port 設備上的寄存器來表示不同狀態,通過中斷來通知虛擬機,從而實現了設備的熱插拔。

加入我們

StratoVirt 中的熱插拔特性已經在 openEuler 社區開源,如果您對相關技術感興趣,歡迎您的圍觀。您可以掃描文末小助手微信二維碼,回覆 StratoVirt 加入 SIG 交流羣。

項目地址:https://gitee.com/openeuler/stratovirt

如有疑問,也歡迎在此提交 issue:https://gitee.com/openeuler/stratovirt/issues



本文分享自微信公眾號 - openEuler(openEulercommunity)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閲讀的你也加入,一起分享。