Linux | PCIe Hotplug | 概念及工作原理的不完全总结
本文对 PCIe
热插拔的概念及工作原理进行不完全总结。
更新: 2022 / 12 / 31
Linux | PCIe Hotplug | 概念及工作原理的不完全总结
1992年初始版本的 PCI
规范并不支持运行时的板卡添加和移除。在20世纪90年代后期到21世纪初期,各种专有的热拔插控制器或者与厂商无关的标准热拔插控制器被构思出来,同时 Linux
通过位于 drivers/pci/hotplug
的驱动开始支持热拔插功能。然而,直到2002年 Linux
才开始支持PCI Express
的热拔插功能,但是具体的形式随时间不断变化。最初,PCI Express
热拔插主要是为服务器的热交换板卡或笔记本的 ExpressCards
设计的,如今,它已经广泛地应用于数据中心 ( 数据中心 NVMe Flash
硬盘需要运行时拔插 ) 和雷电接口。
热插拔
PCIe
热插拔,是指在系统上电运行时,允许插入或拔出 PCIe
设备。拔出时不需要平台提前下电,插入时直接生效,不需要系统重启。这对于服务器实现高可靠性而言是一个必不可少的特性。
热插拔可以分为2种,通知式热插拔
和 暴力热插拔
。这两种差异主要体现在拔盘的操作上。
- 设备插入时, 内核
pciehp
驱动走的都是通知式热插流程,以盘插入时在位信号的变化或者通知按钮的按下作为触发条件。通知式热拔时,需要先通过软件指令,停止业务,移除pcie
设备,再进行拔盘操作。 - 暴力热拔时,没有任何通知,直接进行拔盘动作。
组成部件
为了 pcie
热插拔功能的实现,PCIe
协议定义了一系列需要实现的组件:
组件 | 目的 |
---|---|
Indicator | show the power and attention state of the slot。 标准模型中定义两个 indicators :一个Power Indicator 和一个 Attention Indicator 。都会有三种状态:ON ;OFF ;Blinking 。 |
Attention Indicator 是黄色的灯,用来指示出现了操作的问题,或者表示 hotplug slot 已经被识别到了,手动操作可以很容易定位到。ON : 表示热插拔槽位故障OFF : 表示一切正常BLINKING : 表示热插拔流程正在执行 | |
Power Indicator 是绿色的灯,表示 slot 的 power 状态,可以用 blink 指示总线对用户的操作产生了响应。OFF : 表示槽位下电,可以进行设备的热插或热拔ON : 表示热插拔操作已完成,槽位上电,不可以进行热插或热拔BLINKING : 表示此时正在处于上电或下电槽位, 或者此时 attention button 被按下,正在等待反馈,或者表示 hot-plug 操作正在进行软件的初始化 | |
Manually-operated Retention Latch (MRL) | Holds adds-in cards in place |
MRL Sensor | Allows the Port and system software to detect the MRL being openedMRL 是一种手动操作保留机制。保持插入卡在 slot 上,防止用户移除卡。系统添加了一个 MRL sensor ,以便侦测每个 port 对应的 slot 的 MRL 。 |
Electromechanical Interlock | Prevents removal of add-in cards while slot is powered 一种互斥机制,确保在热插拔流程都执行完成后 PCIe 设备才可被物理移除。 |
Attention Button | Allows user to request Hot-Plug operations 是 hotplug 中定义的一个开关按钮,一般会在 slot 上或者在卡片上,按一下表明要做一个 hotplug 动作或者 removal 动作。 |
Software User Interface | Allows user to request Hot-Plug operations |
Slot Numbering | Provides visual identification of slots 槽位编号,由底板号( Classic Number )和物理槽位号( Physical Slot Number ) 组成,可在用户接口上显示。 |
实现代码
通知式热插拔
热拔的流程图如下所示:
热插流程类似,不再赘述。
PCIe
热插拔功能的实现需要 pcie
热插拔控制器和 pcie
热插拔驱动的配合。
代码主要集中在 driver/pci/hotplug/pciehp_hpc.c
6 和 driver/pci/hotplug/pciehp_ctrl.c
7。
pciehp_hpc.c
主要负责控制器的初始化以及检测设备在位变化、attention button pressed
、电源错误等事件检测,检测到这些事件后,会上报热插拔中断。pciehp_ctrl.c
代码主要是对热插拔各个events
的具体处理。
pciehp
初始化
PCIe
热插拔是作为pcie
端口服务实现的,它已在driver/pci/hotplug/pciehp_core.c
8 中的pcie
端口驱动程序中注册:
int __init pcie_hp_init(void){int retval = 0;retval = pcie_port_service_register(&hpdriver_portdrv);pr_debug("pcie_port_service_register = %d\n", retval);if (retval)pr_debug("Failure to register service\n");return retval;}
之后会调用 pcie_probe
进行端口注册,
+->pciehp_probe(struct pcie_device *dev)+-> pcie_init()+-> pcie_init_slot() // 该函数中会创建hotplug_slot, hotplug_slot_info, hotplug_slot_ops等热插拔驱动关键的数据结构+-> init_slot()+-> pci_hp_initialize()+-> pci_create_slot()+-> kobject_init_and_add() // 添加sysfs对象+-> list_add(&slot->list, &parent->slots) // 将hotplug_slot添加到pci_hotplug_slot_list+-> pcie_init_notification()+-> pciehp_request_irq() //中断申请+-> pcie_enable_notification()+-> pciehp_enable_slot() // 使能hp槽位+-> board_added()
- 注册热插拔中断服务 6
static inline int pciehp_request_irq(struct controller *ctrl){int retval, irq = ctrl->pcie->irq;....retval = request_threaded_irq(irq, pciehp_isr, pciehp_ist, IRQF_SHARED, MY_NAME, ctrl);...}
这里使用了中断线程化,创建一个中断服务 pciehp_isr
, 并且创建了一个 irq_thread
内核线程以及线程会执行的函数 pciehp_ist
。
当中断发生时,中断处理程序会先去处理 pciehp_isr
, 如果 pciehp_isr
返回IRQ_WAKE_THREAD
, 会去唤醒内核线程,处理 pciehp_ist
。
case IRQ_WAKE_THREAD:if (unlikely(!action->thread_fn)) {warn_no_thread(irq, action);break;}__irq_wake_thread(desc, action);
中断线程化之后,中断将作为内核线程运行而且被赋予不同的实时优先级,实时任务可以有比中断线程更高的优先级。这样,具有最高优先级的实时任务就能得到优先处理。
- 热插拔控制器上报中断后,
pciehp
驱动会进入pcie_isr
函数执行
static irqreturn_t pciehp_isr(int irq, void *dev_id){struct controller *ctrl = (struct controller *)dev_id;struct pci_dev *pdev = ctrl_dev(ctrl);struct device *parent = pdev->dev.parent;u16 status, events = 0;if (pdev->current_state == PCI_D3cold || (!(ctrl->slot_ctrl & PCI_EXP_SLTCTL_HPIE) && !pciehp_poll_mode))return IRQ_NONE;if (parent) {pm_runtime_get_noresume(parent);if (!pm_runtime_active(parent)) {pm_runtime_put(parent);disable_irq_nosync(irq);atomic_or(RERUN_ISR, &ctrl->pending_events);return IRQ_WAKE_THREAD;}}read_status:pcie_capability_read_word(pdev, PCI_EXP_SLTSTA, &status);if (PCI_POSSIBLE_ERROR(status)) {ctrl_info(ctrl, "%s: no response from device\n", __func__);if (parent)pm_runtime_put(parent);return IRQ_NONE;}status &= PCI_EXP_SLTSTA_ABP | PCI_EXP_SLTSTA_PFD | PCI_EXP_SLTSTA_PDC | PCI_EXP_SLTSTA_CC | PCI_EXP_SLTSTA_DLLSC;if (ctrl->power_fault_detected)status &= ~PCI_EXP_SLTSTA_PFD;else if (status & PCI_EXP_SLTSTA_PFD)ctrl->power_fault_detected = true;events |= status;if (!events) {if (parent)pm_runtime_put(parent);return IRQ_NONE;}if (status) {pcie_capability_write_word(pdev, PCI_EXP_SLTSTA, status);if (pci_dev_msi_enabled(pdev) && !pciehp_poll_mode)goto read_status;}ctrl_dbg(ctrl, "pending interrupts %#06x from Slot Status\n", events);if (parent)pm_runtime_put(parent);if (events & PCI_EXP_SLTSTA_CC) {ctrl->cmd_busy = 0;smp_mb();wake_up(&ctrl->queue);if (events == PCI_EXP_SLTSTA_CC)return IRQ_HANDLED;events &= ~PCI_EXP_SLTSTA_CC;}if (pdev->ignore_hotplug) {ctrl_dbg(ctrl, "ignoring hotplug event %#06x\n", events);return IRQ_HANDLED;}atomic_or(events, &ctrl->pending_events);return IRQ_WAKE_THREAD;}
对 pciehp_isr
函数几个操作进行分析:
[1.] 检查设备当前状态是否处于 D3_cold
状态。
D3hot
( 通常只称作 D3
)是设备的 软关闭
状态,在此状态下,总线扫描可以检测到设备,并且发送给设备的命令可能会导致它再次打开电源,而在 D3cold
中,将切断所有电源,只保留少量电源以驱动设备的唤醒逻辑。
因此,在这里检测到如果处于 D3cold
状态,直接退出。
[2.] 如果设备存在父设备,在该设备 resume
前要先 resume
父设备
[3.] 读 pcie slot status
确认 events
状态。hotplug controller
监控各种 events
并把这些 events
上报给 hotplug system driver
。
这里需要关注这些 events
即可。Attention button pressed
( Attention
按键按下), power fault detected
(电源错误), presence detect changed
(在位状态变化), command completed
(命令完成), data link layer state changed
(链路状态改变)。
所以整个 isr
流程主要的处理就是决定哪些 events
可以通过 system interrupt
上报给系统,然后唤醒内核线程,处理线程函数 pciehp_ist
- 中断线程化处理
pciehp_ist
static irqreturn_t pciehp_ist(int irq, void *dev_id){struct controller *ctrl = (struct controller *)dev_id;struct pci_dev *pdev = ctrl_dev(ctrl);irqreturn_t ret;u32 events;ctrl->ist_running = true;pci_config_pm_runtime_get(pdev);if (atomic_fetch_and(~RERUN_ISR, &ctrl->pending_events) & RERUN_ISR) {ret = pciehp_isr(irq, dev_id);enable_irq(irq);if (ret != IRQ_WAKE_THREAD)goto out;}synchronize_hardirq(irq);events = atomic_xchg(&ctrl->pending_events, 0);if (!events) {ret = IRQ_NONE;goto out;}if (events & PCI_EXP_SLTSTA_ABP) {ctrl_info(ctrl, "Slot(%s): Attention button pressed\n", slot_name(ctrl));pciehp_handle_button_press(ctrl);}if (events & PCI_EXP_SLTSTA_PFD) {ctrl_err(ctrl, "Slot(%s): Power fault\n", slot_name(ctrl));pciehp_set_indicators(ctrl, PCI_EXP_SLTCTL_PWR_IND_OFF, PCI_EXP_SLTCTL_ATTN_IND_ON);}if ((events & PCI_EXP_SLTSTA_DLLSC) && pci_dpc_recovered(pdev) && ctrl->state == ON_STATE) {events &= ~PCI_EXP_SLTSTA_DLLSC;pciehp_ignore_dpc_link_change(ctrl, pdev, irq);}down_read_nested(&ctrl->reset_lock, ctrl->depth);if (events & DISABLE_SLOT)pciehp_handle_disable_request(ctrl);else if (events & (PCI_EXP_SLTSTA_PDC | PCI_EXP_SLTSTA_DLLSC))pciehp_handle_presence_or_link_change(ctrl, events);up_read(&ctrl->reset_lock);ret = IRQ_HANDLED;out:pci_config_pm_runtime_put(pdev);ctrl->ist_running = false;wake_up(&ctrl->requester);return ret;}
[1.] 这里主要涉及到的是 runtime pm
的运行机制。
需要使用设备时,device driver
调用pm_runtime_get
接口,增加引用计数;
不再使用设备时,device driver
调用 pm_runtime_put
接口,减少引用计数。
[2.] 检测到了 attention button
按钮被按下。检测到一个 attention button pressed
中断,对该中断的处理需要依据当前 pcie
槽位的状态
#define BLINKINGON_STATE1#define BLINKINGOFF_STATE2#define POWERON_STATE3#define POWEROFF_STATE4
- 如果检测到当前槽位状态为
POWEON
,就说明此时产生了一个hot-remove
,将状态设置为BLINKINGOFF
, 表示pcie controller
会等待5 s
后将该槽位下电。 - 如果检测到当前槽位状态为
POWEOFF
, 就说明此时需要热插,将状态设置为BLINKINGON
, 表示pcie controller
会等待5 s
后将该槽位上电。 - 如果检测到当前槽位状态已经处于
BLINKINGON / OFF state
, 说明此时需要取消上次的热插或热拔操作,将槽位的状态恢复成ON / OFF state
。
如果 5 s
内没有取消 attention button
, 最终会执行到 pcie_init_slot
中初始化的 slot->work
, 进行slot
的 enable
或 disable
( pciehp_queue_pushbutton_work
)。
[3.] 检测到了电源异常。
将 power_fault_detected
标志先置为 1
,表示电源异常不会重复处理,然后将电源指示灯熄灭。
[4.] 检测到了 disble_slot
DISABLE_SLOT
主要用于响应用户通过 sysfs
或者 attention button
禁用槽位的请求,此事件的优先级要高于在位信号状态或者链路状态改变。
[5.] 如果检测到在位状态改变或链路状态改变
- 如果
pcie slot
槽位处于上电状态,却产生了在位状态改变的event
,说明产生了暴力热拔操作, 此时直接将槽位下电。
+->pciehp_handle_presence_or_link_change()+-> slot->state = POWEROFF_STATE;+-> pciehp_disable_slot()+->remove_board()
- 如果链路状态正常并且
pcie
卡处于在位状态,进行热插的处理。
+->present || link_active+-> pciehp_enable_slot() +-> board_added() +-> pciehp_configure_device()
线程中断
Linux
的 PCIe
热拔插驱动 pciehp
在 2004 年被 Dely Sy
引入。Kenji Kaneshige
对其进行了第一次清理和修订,直到 2011 年才结束相关工作。在这之后,贡献者们的工作大多局限于完善驱动的缺陷,尤其是事件处理。
Threaded Interrupts
是内核中主要的中断处理模式,是实时 Linux
的基石,但是不幸的,它们直到 Kaneshige
的修订完成后才被引入。因此,pciehp
的硬中断处理器识别发生了什么事件,比如 link-up
或者 link-down
,并为每个事件安排工作项。这种方法的问题是当执行 ( work item
)时,link
状态可以再次发生变化。此外,如果 link
状态翻转比硬件处理器速度快时,可能导致检测到不均衡的 link-up
和 link-down
事件。最终,多个 work item
并存的可能性以及它们的交互方式使得很难判断事件处理代码的正确性。当前,PCI
维护员 Bjorn Helgaas
称 pciph
的事件处理复杂且怪异。除非驱动的修订不再是一个选项时,那么驱动的基本反思是不可避免地。
在 Linux 4.19
版本中,把 pciehp
转换为线程中断处理 ( threaded interrupt handling
)。现在硬件中断处理器负责收集事件,中断线程负责处理事件。检测 link
变化是 link up
还是 link down
这项工作推迟到中断线程中事件处理函数处理,以避免处理陈旧的事件。新的方法可以快速处理事件序列 (比如,link down
后紧接着 link up
),可以容忍 pcie slot
启动过程中 link
状态发生翻转 (比如电磁干扰引起的)。补丁集也修复了大量的 bug
和做了大量清理工作,因此,PCI Express
驱动的可靠性和健壮性得到了显著的提升。
对于 Linux 4.20
版本中安排的后续补丁将从pcihp
和其他热拔插驱动中删除将近 500
行代码,进而使代码简化和标准化。
功耗管理
Linux 4.19
增加了运行时挂起 PCIe
热拔插端口的功能,这项功能对雷电控制器的掉电是必不可少,雷电控制器在操作系统中表现为一个 PCIe
上游端口和多个 PCIe
下游端口。在控制器掉电前,所有的 PCIe
端口运行时挂起。Linux
从 4.8
版本开始就可以运行时挂起上游端口,但是在 4.19
版本前并不能挂起运行时挂起下游端口。
运行时挂起一个雷电 PCIe
端口本身并不会引起任何节能,端口将会对通过聚集 IO Switch
传 输的 PCIe
数据包进行封装和解封,那么只要给 Switch
供电,那么就会消耗能量。然而,Linux
的功耗管理模型要求所有的子设备在其父节点挂起前被挂起。通过运行时挂起雷电控制器的所有端口,它的父端口(根端口)允许挂起,这反过来通过 ACPI
平台方法触发掉电。控制器下电可以节约大约 1.5W
的功耗。
换句话说,运行时挂起雷电 PCIe
端口满足了 Linux
的层级化功耗管理模型。单个雷电 PCIe
端口在 PCI
电源状态 D0
(全功耗) 或者 D3hot
(挂起) 这两种状态消耗同样多的功耗,但是当所有端口运行时挂起时控制器作为整体是可以掉电的。在 MacBook
平台,雷电控制器掉电功能可能需要进一步打补丁解决,预计在 4.21
版本中出现。
当一个 PCIe
热拔插端口运行时挂起时热拔插事件处理发生一个有趣的细节:如果它的父端口也运行时挂起,那么该端口是不可访问的。因此,它不能带内发出中断信号,内核不能与之交互,或者甚至在父端口恢复运行时挂起前不能确定事件类型。当前有两种硬件方法可以解决这个问题。
第一个方法遵循 PCIe
规范:热拔插端口信号是一个电源管理事件 ( PME
),它可以通过平台提供带外信号方式发生,比如一个通用 IO
引脚 ( 在 PCIE
中的 WAKE#
信号)。PME
唤醒雷电Host
控制器下面所有层级,随之,热拔插端口可以被访问。这个方法被联想和 Dell
笔记本采用。它允许在连接设备的情况下让控制器掉电。Mika Westerberg
已经在 4.20
版本中提交了支持该功能的补丁。
第二种方法是非标准的:雷电硬件直到哪个通道建立了连接,因此可以将聚合 IO
层发生的热拔插事件转换为一个 overlaid PCIe
层发生的热拔插事件。这样,当设备被添加或移除时,无论其自己还是其父端口是否处在 D3hot
状态,都会魔幻般从受影响的 PCIe
端口接收到一个中断。这种方法在 Apple Macs
( 雷电 1
) 中采用,只要设备已经连接的情况下雷电 Host
控制器需要保持供电。在 4.19
版本中已经添加了这种功能。
运行时电源管理当前对非-雷电热拔插端口是禁止的,因为当非雷电热拔插端口在 D3hot
状态时可能引起诸如不可屏蔽的中断 ( NMI
)。厂商可以在命令行传递 pcie_port_pm=force
验证它们的热拔插端口是否支持运行时挂起,也许这个功能可能在晚些时候默认使能。
意外移除
初始 PCIe
规范定义了标准用法模型,该模型定义了手动保留锁固定板卡位置和一个按钮用于请求从操作系统移除一个 PCIe Slot
。但是当前热拔插实现通常忽略这些元素和仅仅使用surprise removal
这种方式。
当一个设备被拽出时,pciehp
要求它的驱动解绑,然后 bring down
该槽位。但是直到这一切发生,对设备的读请求将会在 17ms
后溢出,然后返回一个全 1
的响应。这个超时降低了请求任务的速度,如果构造的响应对实际的数据是错误的,那么该任务可能崩溃或者陷入到无限循环中。因此,驱动需要从一个设备读取的数据的有效性,尤其是,检查全 1
但是不是一个有效响应的情况。一个惯用的方法是调用 pci_device_is_present()
,该函数读取厂商 ID
寄存器,检查该寄存器是否全 1
。然而,这也不是万能药;如果发生一个 PCIe
不可修复的错误,那么设备也可能以全 1
进行响应,但是如果错误可以恢复,那么也可以还原到有效响应。此外,对于不支持的请求或者位于桥地址窗口内内任一目标设备基址寄存器 ( BARs
) 外的读请求也可能返回全 1
响应。唯一能够权威且明确识别移除的实体是 pciehp
。
许多驱动甚至 PCI
核并不会对一个全 1
响应的每个读进行检查。效力于 Facebook “Lighting"存储架构的工程师需要艰难地学习这个问题。Surprise
移除一个 NVMe
硬盘实体阵列可能花费数秒钟时间,也会偶发性引起 MCE
( machine-check exceptions
)。拔出过程如此缓慢以至于驱动可能在完成上一个拔出处理过程前,认为自己在跟一个新插入的硬盘进行交互。由Keith Busch
在 Linux 4.12
版本提交的补丁中一个成果是让 pciehp
为 surprise
移除的设备设置标志位,在 PCI
核中在一些战略性的位置跳过该设备的访问。这足以将移除过程加速到毫秒级别。尤其是当设置标志位后,pci_device_is_present()
返回 false
。之前,如果一个设备跟另一个设备快速交换,那么一旦新设备的厂商 ID
可读后对于已移除的设备会错误地返回 True
。
在 Benjamin Herrenschmidt's behest
中,由 Busch
提交的另一个补丁已经安排引入到 4.20
版本,目的是统一 PCI
设备错误状态的标志。错误状态可以用于识别设备是经历一个不可修复错误,但是有机会恢复后的临时性不可访问,还是永久性不可访问。驱动也会直接检查 struct pci_dev
结构体中 error_state
成员变量或者调用 pci_channel_offline()
来确定设备的可访问性。
然而,Helgass
对标志使用表示了疑虑。其一,标志是异步设置的,因此,在设备被移除到标志被设置之间存在一个延迟。驱动开发者需要谨慎小心:即使根据标志位设备似乎在位,但是设备可能已经不在了。相反,如果设置了标志位,标志提供了明确指示,后来的设备访问是无效的,可以跳过。因此,设置标志位并不会使驱动开发者免于验证来自设备的响应,但是一旦设置标志位,它可以作为一个 cache
,避免了不明确的厂商ID检查。简而言之,问题只是得到了缓解,而没有完美解决。尽管一个完美的解决方案似乎是不可能的,但是我们不能获得用户的互斥锁来阻止用户召回设备,处于性能考量,我们不能在每次设备访问后都对设备在位变化进行检查。Austin Bolen
指出新的 PCIe
扩展 ( root port programmed IO
) 允许失败设备访问的同步异常处理,这样看起来似乎是一个完美的解决方案。但是这个特征在未来一段时间是不可得的。
Helgaas
对标志的第二个担心是标志可能使 surprise
拔插发生的 bug
更加难以发现。当设置标志后,这些 bug
变得更难以发现。For example, a search for the advanced error recovery (AER) capability on device removal caused numerous configuration-space accesses and, before introduction of the flag, was noticeable through a significant slowdown upon surprise removal
。但是恰当的方式是:缓存AER能力的位置,而不是使用标志跳过配置访问糊弄问题。
错误处理整合
向线程中断的转变也减轻了整合 pciehp
处理 PCIe
不可修复错误的处理压力:当在热拔插端口或者子端口发生这样错误时,它可能引起一个 link-down
事件。但是有时候错误可能通过软件得以恢复,比如执行 secondary bus reset
。在这种情况下,pciehp
通过使设备和驱动解绑,同时使slot
掉电来应对 link-down
事件是不合时宜的。相反,它应该等待以确定错误是否可以恢复,如果可以恢复,那么忽略该 link
事件。为了达到这个目的,Busch
和 Sinan Kaya
当前致力于开发补丁将 pciehp
中的 AER
与下游端口控制服务驱动联系在一起。
移除BAR
为 PCIe
设备分配了内存范围,用于在设备的 BAR
中配置的内存映射 I/O
。内存范围通常由 BIOS
预定义,但 Linux
可能会在枚举时移动它们。PCI
设备上游的桥将其地址窗口配置为正确路由以设备 BAR
为目标的事务。
当设备被热添加时,它们的内存需求可能无法适应其上游网桥的窗口,从而需要重新组织资源:需要移动相邻的 BAR
并调整网桥窗口。 MacOS
于 2013
年获得此功能以改进 Thunderbolt
支持,并将其称为 PCIe pause
。驱动程序被告知暂停对受影响设备的 I/O
;取消暂停时,BAR
可能已更改,并且驱动程序需要重新配置其设备并根据需要更新内部数据结构。
Sergey Miroshnichenko
最近提交了将 BAR
迁移到 Linux
的初始补丁,获得了积极的反响。这些补丁使用现有的回调在 PCI reset
之前暂停对设备的访问并在之后重新启动访问。drivers
将不得不选择加入 BAR
移除。除了 BAR
之外,MacOS
还支持 PCI bus number
和消息信号中断的重新分配;Miroshnichenko
正在考虑在补丁集的未来修订版中添加它。
参考链接
来源地址:https://blog.csdn.net/MissMango0820/article/details/128497422
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341