前言

本文基于Linux kernel 4.19.0, 体系结构为aarch64.

PCIe hotplug概述

PCIe热插拔,是指在系统上电运行时,允许插入或拔出PCIe设备。拔出时不需要平台提前下电,插入时直接生效,不需要系统重启。这对于服务器实现高可靠性而言是一个必不可少的特性。

热插拔可以分为2种:通知式热插拔和暴力热插拔。这两种差异主要体现在拔盘的操作上。
设备插入时, 内核pciehp驱动走的都是通知式热插流程,以盘插入时在位信号的变化或者通知按钮的按下作为触发条件。
通知式热拔时,需要先通过软件指令,停止业务,移除pcie设备,再进行拔盘操作。
暴力热拔时,没有任何通知,直接进行拔盘动作。

热插拔组成部件

为了pcie热插拔功能的实现,PCIe协议定义了一系列需要实现的组件

bios热插拔选项 bios开启热插拔_ci


Indicators

标准模型中定义两个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操作正在进行软件的初始化

MRL
MRL是一种手动操作保留机制。保持插入卡在slot上,防止用户移除卡。系统添加了一个MRL sensor,以便侦测每个port对应的slot的MRL。

Electromechanical interlock(可选)
一种互斥机制,确保在热插拔流程都执行完成后,PCIe设备才可被物理移除。

Attention Button
Attention Button是hotplug中定义的一个开关按钮,一般会在slot上或者在卡片上。按一下表明要做一个hotplug动作或者removal动作。

Slot Numbering
槽位编号,由底板号(classis number)和物理槽位号(physical slot number)组成,可在用户接口上显示

热插拔代码分析

本文主要分析通知式热插拔在内核上的实现。

PCIe热插拔功能的实现需要pcie热插拔控制器和pcie热插拔驱动的配合。
代码主要集中在driver/pci/hotplug/pciehp_hpc.c driver/pci/hotplug/pciehp_ctrl.c
pciehp_hpc.c 主要负责控制器的初始化以及检测设备在位变化,attention button pressed, 电源错误等事件检测,检测到这些事件后,会上报热插拔中断。pciehp_ctrl.c代码主要是对热插拔各个events的具体处理。

1. pciehp初始化
PCIe热插拔是作为pcie端口服务实现的,它已在driver/pci/hotplug/pciehp_core.c中的pcie端口驱动程序中注册:

+->pcied_init()
	+-> pcie_port_service_register()

之后会调用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()

2. 注册热插拔中断服务

static inline int pciehp_request_irq(struct controller *ctrl)
{
	int retval, irq = ctrl->pcie->irq;
	....
	/* Installs the interrupt handler */
	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。
中断的高层处理过程可以参考我之前的这篇博文 【Linux 内核笔记之高层中断处理】

case IRQ_WAKE_THREAD:
			/*
			 * Catch drivers which return WAKE_THREAD but
			 * did not set up a thread function
			 */
			if (unlikely(!action->thread_fn)) {
				warn_no_thread(irq, action);
				break;
			}

			__irq_wake_thread(desc, action);

中断线程化之后,中断将作为内核线程运行而且被赋予不同的实时优先级,实时任务可以有比中断线程更高的优先级。这样,具有最高优先级的实时任务就能得到优先处理.

3. 热插拔控制器上报中断后,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;

	/*
	 * Interrupts only occur in D3hot or shallower (PCIe r4.0, sec 6.7.3.4).
	 */
	if (pdev->current_state == PCI_D3cold)      ----- (1)
		return IRQ_NONE;

	/*
	 * Keep the port accessible by holding a runtime PM ref on its parent.
	 * Defer resume of the parent to the IRQ thread if it's suspended.
	 * Mask the interrupt until then.
	 */
	if (parent) {					 ------- (2)
		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;
		}
	}

	pcie_capability_read_word(pdev, PCI_EXP_SLTSTA, &status);   ------- (2)
	if (status == (u16) ~0) {
		ctrl_info(ctrl, "%s: no response from device\n", __func__);
		if (parent)
			pm_runtime_put(parent);
		return IRQ_NONE;
	}

	/*
	 * Slot Status contains plain status bits as well as event
	 * notification bits; right now we only want the event bits.
	 */
	events = status & (PCI_EXP_SLTSTA_ABP | PCI_EXP_SLTSTA_PFD |
			   PCI_EXP_SLTSTA_PDC | PCI_EXP_SLTSTA_CC |
			   PCI_EXP_SLTSTA_DLLSC);                ---------- (4)

	/*
	 * If we've already reported a power fault, don't report it again
	 * until we've done something to handle it.
	 */
	if (ctrl->power_fault_detected)                        
		events &= ~PCI_EXP_SLTSTA_PFD;

	if (!events) {
		if (parent)
			pm_runtime_put(parent);
		return IRQ_NONE;
	}

	pcie_capability_write_word(pdev, PCI_EXP_SLTSTA, events);  ------ (4)
	ctrl_dbg(ctrl, "pending interrupts %#06x from Slot Status\n", events);
	if (parent)
		pm_runtime_put(parent);

	/*
	 * Command Completed notifications are not deferred to the
	 * IRQ thread because it may be waiting for their arrival.
	 */
	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;
	}

	/* Save pending events for consumption by IRQ thread. */
	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.
(4) 这里需要关注这些events即可。Attention button pressed (Attention按键按下), power fault detected (电源错误), presence detect changed (在位状态变化), command completed (命令完成), data link layer state changed (链路状态改变).

所以整个isr流程主要的处理就是决定哪些events可以通过system interrupt上报给系统,然后唤醒内核线程,处理线程函数pciehp_ist

4. 中断线程化处理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);
	struct slot *slot = ctrl->slot;
	irqreturn_t ret;
	u32 events;

	pci_config_pm_runtime_get(pdev);                    ---------- (1)

	/* rerun pciehp_isr() if the port was inaccessible on interrupt */
	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) {
			pci_config_pm_runtime_put(pdev);
			return ret;
		}
	}

	synchronize_hardirq(irq);
	events = atomic_xchg(&ctrl->pending_events, 0);
	if (!events) {
		pci_config_pm_runtime_put(pdev);
		return IRQ_NONE;
	}

	/* Check Attention Button Pressed */
	if (events & PCI_EXP_SLTSTA_ABP) {                 ---------------- (2)
		ctrl_info(ctrl, "Slot(%s): Attention button pressed\n",
			  slot_name(slot));
		pciehp_handle_button_press(slot);
	}

	/* Check Power Fault Detected */
	if ((events & PCI_EXP_SLTSTA_PFD) && !ctrl->power_fault_detected) {       ------ (3)
		ctrl->power_fault_detected = 1;
		ctrl_err(ctrl, "Slot(%s): Power fault\n", slot_name(slot));
		pciehp_set_attention_status(slot, 1);
		pciehp_green_led_off(slot);
	}

	/*
	 * Disable requests have higher priority than Presence Detect Changed
	 * or Data Link Layer State Changed events.
	 */
	down_read(&ctrl->reset_lock);
	if (events & DISABLE_SLOT)      ----- (4)                     
		pciehp_handle_disable_request(slot);
	else if (events & (PCI_EXP_SLTSTA_PDC | PCI_EXP_SLTSTA_DLLSC))      ------ (5)
		pciehp_handle_presence_or_link_change(slot, events);
	up_read(&ctrl->reset_lock);

	pci_config_pm_runtime_put(pdev);
	wake_up(&ctrl->requester);                 
	return IRQ_HANDLED;
}

(1) 这里主要涉及到的是runtime pm的运行机制。需要使用设备时,device driver调用pm_runtime_get接口,增加引用计数;不再使用设备时,device driver调用pm_runtime_put接口,减少引用计数。

(2) 检测到了attention button按钮被按下。检测到一个attention button pressed中断,对该中断的处理需要依据当前pcie槽位的状态

#define BLINKINGON_STATE		1
#define BLINKINGOFF_STATE		2
#define POWERON_STATE			3
#define POWEROFF_STATE			4

如果检测到当前槽位状态为POWEON, 就说明此时产生了一个hot-remove, 将状态设置为BLINKINGOFF, 表示pcie controller会等待5s后将该槽位下电。
如果检测到当前槽位状态为POWEOFF, 就说明此时需要热插,将状态设置为BLINKINGON, 表示pcie controller会等待5s后将该槽位上电。
如果检测到当前槽位状态已经处于BLINKINGON/OFF state, 说明此时需要取消上次的热插或热拔操作,将槽位的状态恢复成ON/OFF state.

bios热插拔选项 bios开启热插拔_暴力热插拔_02

如果5s内没有取消attention button, 最终会执行到pcie_init_slot中初始化的slot->work, 进行slot的enable或disable (pciehp_queue_pushbutton_work)。

+->pciehp_queue_pushbutton_work()
	+-> pciehp_request(ctrl, DISABLE_SLOT)
		+-> pciehp_disable_slot()
			+-> remove_board()
				+-> pciehp_unconfigure_device()
					+-> pci_stop_and_remove_bus_device()

(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()

总结

以一个流程图,对本文进行补充

热拔:

bios热插拔选项 bios开启热插拔_暴力热插拔_03


热插流程类似,不在赘述。

参考资料

  1. PCI.EXPRESS系统体系结构标准教材
  2. linux PCIe hotplug arch analysis