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 交流群。

專案地址:http://gitee.com/openeuler/stratovirt

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



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