熱插拔即帶電插拔,在虛擬化場景下,熱插拔就是在虛擬機(jī)運行過程中對磁盤網(wǎng)卡等設(shè)備進(jìn)行動態(tài)調(diào)整。
常見的熱插拔機(jī)制有 ACPI 機(jī)制的熱插拔,PCIe-Native 機(jī)制的熱插拔。ACPI 機(jī)制的熱插拔依賴 ACPI 表,在 ACPI 表中會存放設(shè)備熱插拔相關(guān)的信息。PCIe-Native 機(jī)制的熱插拔是 PCI 規(guī)范中定義的,設(shè)備一般是熱插到 Root Port 設(shè)備上,Root Port 設(shè)備可以認(rèn)為是一個虛擬的橋設(shè)備,對應(yīng)一個插槽。Root Port 設(shè)備本身不支持熱插拔,因此需要在啟動虛擬機(jī)前提前配置。
目前,StratoVirt 標(biāo)準(zhǔn)機(jī)型中實現(xiàn)了基于 PCIe-Native 機(jī)制的熱插拔。支持熱插拔的設(shè)備包括磁盤、網(wǎng)卡、PCI 直通設(shè)備。
熱插拔的整體流程如下:
對于熱插主要分為兩步:
- 用戶通過 QMP 下發(fā) device_add 命令,StratoVirt 收到命令后會進(jìn)行設(shè)備的實例化,然后插入到對應(yīng)的 Root Port 設(shè)備上。
- Root Port 設(shè)備更新相關(guān)的寄存器配置,然后發(fā)送中斷通知虛擬機(jī)內(nèi)驅(qū)動處理。
對于熱拔也可以分為兩步:
- 用戶通過 QMP 下發(fā) device_del 命令,StratoVirt 收到命令后,更新 Root Port 中的寄存器,然后發(fā)送中斷通知虛擬機(jī)內(nèi)驅(qū)動處理。
- 虛擬機(jī)內(nèi)驅(qū)動處理后會回寫寄存器,觸發(fā) StratoVirt 側(cè)銷毀相應(yīng)設(shè)備。
具體實現(xiàn)
在 StratoVirt 的 pci/src/hotplug.rs 文件中定義了熱插拔特性,其中 plug 函數(shù)對應(yīng)熱插操作,用于熱插設(shè)備。unplug_request 函數(shù)對應(yīng)熱拔操作,用于發(fā)起熱拔設(shè)備請求,這里只是通知虛擬機(jī)內(nèi)驅(qū)動去處理熱拔請求,還未移除設(shè)備,可以理解為是一個異步請求。當(dāng)虛擬機(jī)內(nèi)驅(qū)動處理完成后,寫寄存器觸發(fā)設(shè)備下線后,會回調(diào) unplug 函數(shù)用于銷毀設(shè)備。
pub trait HotplugOps: Send {
/// Plug device, usually called when hot plug device in device_add.
fn plug(&mut self, dev: &Arc
>) -> 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
>) -> Result<()>; /// Remove the device.
fn unplug(&mut self, dev: &Arc
>) -> Result<()>; }
熱插實現(xiàn)
StratoVirt 里通過給 RootPort 實現(xiàn)了 HotplugOps 特性,使得 PCI 設(shè)備能夠熱插到 Root Port 設(shè)備上。
設(shè)備熱插的主要實現(xiàn)邏輯在 plug 函數(shù)里。首先獲取了設(shè)備的 devfn 號,也就是 Device 號和 Function 號,目前熱插只支持 Device 號和 Function 號都為 0 的設(shè)備。因此這里做了判斷。
然后會在 RootPort 設(shè)備的 PCI 配置空間中的 PCI Express Capability(PCI 配置空間和 PCI Express Capability 寄存器定義可以參考 PCI 規(guī)范)中設(shè)置 Slot 狀態(tài)寄存器和 Link 狀態(tài)寄存器,然后通過 hotplug_event_notify 函數(shù)發(fā)送中斷通知虛擬機(jī)。這里熱插設(shè)備主要是通過 Attention Button Pressed(對應(yīng) PCI_EXP_HP_EV_ABP)事件觸發(fā)的。
這里簡單介紹下不同標(biāo)記位的含義。
符號 | 描述 |
---|---|
PCI_EXP_SLTSTA | Slot Status Register 表示 Slot 狀態(tài)寄存器,不同的位表示 Slot 不同的狀態(tài) |
PCI_EXP_SLTSTA_PDS | Presence Detect State 表示 Slot 上設(shè)備的在位狀態(tài),置 1 表示在位 |
PCI_EXP_HP_EV_PDC | Presence Detect Changed 表示 Slot 上設(shè)備在位狀態(tài)是否發(fā)生變化 |
PCI_EXP_HP_EV_ABP | Attention Button Pressed 表示 Attention 按鈕被按下,該按鈕用于觸發(fā)熱插拔操作 |
PCI_EXP_LNKSTA | Link Status Register 表示 Link 狀態(tài)的寄存器 |
PCI_EXP_LNKSTA_DLLLA | Data Link Layer Link Active 表示數(shù)據(jù)鏈路控制和管理狀態(tài),置 1 表示處于 Active 狀態(tài) |
impl HotplugOps for RootPort {
fn plug(&mut self, dev: &Arc
>) -> 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 函數(shù)中會調(diào)用 MSIX 中斷的 notify 函數(shù)發(fā)送中斷到虛擬機(jī)內(nèi),虛擬機(jī)內(nèi) pciehp 驅(qū)動收到中斷后會處理相關(guān)的熱插請求。
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");
}
}
熱拔實現(xiàn)
對于設(shè)備熱拔請求的邏輯主要在 unplug_request 函數(shù),該函數(shù)負(fù)責(zé)更新寄存器,并且通過調(diào)用 hotplug_event_notify 函數(shù)發(fā)送中斷通知虛擬機(jī)內(nèi)驅(qū)動處理設(shè)備熱拔請求。
unplug_request 函數(shù)里主要是清零了 Link 狀態(tài)寄存器中的 PCI_EXP_LNKSTA_DLLLA 標(biāo)記位,并且在 Slot 狀態(tài)寄存器中的設(shè)置了 PCI_EXP_HP_EV_ABP 標(biāo)記位。從這里也可以發(fā)現(xiàn),其實無論是熱插請求還是熱拔請求,都是通過 Attention Button Pressed(對應(yīng) PCI_EXP_HP_EV_ABP)事件觸發(fā)的,虛擬機(jī)內(nèi)驅(qū)動會根據(jù)設(shè)備的在位狀態(tài)來判斷是熱插請求還是熱拔請求。
impl HotplugOps for RootPort {
fn unplug_request(&mut self, dev: &Arc
>) -> 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(())
}
}
對于熱拔設(shè)備,StratoVirt 側(cè)在更新寄存器發(fā)送中斷通知虛擬機(jī)內(nèi)驅(qū)動后,實際上還沒有真正的移除設(shè)備,而是等到虛擬機(jī)內(nèi)驅(qū)動處理后回寫寄存器通知 StratoVirt 側(cè)下線設(shè)備后,才會真正銷毀設(shè)備。
虛擬機(jī)內(nèi)驅(qū)動寫 Root Port 寄存器會調(diào)用到 write_config 函數(shù),在 write_config 函數(shù)里會調(diào)用 do_unplug 函數(shù)來處理熱拔設(shè)備相關(guān)的邏輯。
fn write_config(&mut self, offset: usize, data: &[u8]) {
...
self.do_unplug(offset, end, old_ctl);
}
do_unplug 函數(shù)里首先保證了寫入的寄存器是 Slot Control 寄存器,否則直接返回,不做處理。然后判斷在設(shè)備當(dāng)前在位的情況下,寫入的寄存器標(biāo)記位為 PCI_EXP_SLTCTL_PWR_IND_OFF 和 PCI_EXP_SLTCTL_PCC 時,并且這兩個標(biāo)記位發(fā)生了變化,也就是寫入之前的沒有這兩個標(biāo)記位,上述條件都滿足時,會調(diào)用 remove_devices 函數(shù)開始真正銷毀設(shè)備。
符號 | 描述 |
---|---|
PCI_EXP_SLTCTL_PCC | Power Controller Control 表示電源管理狀態(tài),置 1 表示上電狀態(tài) |
PCI_EXP_SLTCTL_PWR_IND_OFF | Power Indicator off 表示是否允許移除設(shè)備,置 1 表示設(shè)備允許被移除 |
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();
}
在調(diào)用 remove_devices 函數(shù)移除設(shè)備之后,調(diào)用 update_register_status 函數(shù)更新寄存器的狀態(tài),主要是清理了 Link 狀態(tài)和設(shè)備在位狀態(tài),并且設(shè)置了 Presence Detect Changed(對應(yīng) PCI_EXP_HP_EV_PDC)標(biāo)記位表示設(shè)備在位狀態(tài)發(fā)生了變化。
/// 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 還會設(shè)置 Command Completed(對應(yīng) PCI_EXP_HP_EV_CCI)表示命令處理完成,最后再發(fā)送中斷通知虛擬機(jī)內(nèi)驅(qū)動。至此,整個設(shè)備熱拔流程就結(jié)束了。
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 表示命令處理完成,可以處理下一條命令 |
總結(jié)
PCIe Native 機(jī)制的熱插拔主要是通過 Root Port 設(shè)備上的寄存器來表示不同狀態(tài),通過中斷來通知虛擬機(jī),從而實現(xiàn)了設(shè)備的熱插拔。
審核編輯:湯梓紅
-
熱插拔
+關(guān)注
關(guān)注
2文章
224瀏覽量
37354 -
PCIe
+關(guān)注
關(guān)注
15文章
1239瀏覽量
82658 -
虛擬機(jī)
+關(guān)注
關(guān)注
1文章
917瀏覽量
28202
原文標(biāo)題:StratoVirt 中的 PCI 設(shè)備熱插拔實現(xiàn)
文章出處:【微信號:openEulercommunity,微信公眾號:openEuler】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論