一、概述
许多PCI总线控制器能够探测到总线发生的错误,例如数据和地址线的奇偶校验错误,也称之为SERR、PERR错误。一些高级的芯片能够处理这些错误,比如有PCIe芯片、PCI主桥芯片。典型的操作是断开受影响设备的连接,停止对其所有的I/O操作,其目的是防止系统崩溃掉。例如,由于DMA的野地址操作致使系统内存数据损坏。此时需要提供重连机制,这样有问题的PCI设备就可以复位,并恢复到工作状态中。复位阶段的操作需要设备驱动和PCI控制器芯片之间协调配合进行。
二、错误产生后需要做的事情
错误产生后需要进行上报和恢复,而上报和恢复需要执行以下几个步骤。
首先,当PCI硬件错误导致总线断开时,该事件需要尽快报告给所有受影响的设备驱动程序,包括多功能卡上的多个设备驱动程序实例。采取这个措施的目的是,可以使设备驱动程序避免在自旋循环中等待某个I/O空间寄存器发生变化时发生死锁问题,也可以给驱动程序有机会推迟即将到来的I/O操作。
其次,恢复操作需要分为几个阶段执行。大部分恢复的复杂性是由处理多功能设备的需求所造成的,即与它们关联的有多个设备驱动程序的设备。在第一阶段,每个驱动程序被允许指示想要的复位类型,这些选择可以是简单的重新启用I/O或请求slot复位。
如果任何驱动请求slot复位,将是被允许的。当复位或重新启用I/O,所有的驱动将再次被通知,这样会执行设备配置操作。当这些操作完成了,"resume normal operations"事件将会被发出。
选择基于内核的实现而不是用户空间实现的最大原因是,需要处理存储媒介的PCI设备总线断开需求,特别是从根文件系统的设备断开。如果根文件系统被断开,用户空间机制将不得不绕很大弯路才能完成恢复。几乎所有当前的Linux文件系统都不能容忍从底层块设备断开/重新连接。相比之下,在设备驱动程序中总线错误很容易管理。事实上,大多数设备驱动程序已经处理非常相似的恢复过程;例如,SCSI通用层已经提供了处理SCSI总线错误和SCSI总线复位的重要机制。
三、结构体
错误恢复API的支持以pci_driver结构体中的新字段所指向的函数指针的形式暴露给驱动程序。如果未能挂载回调函数于结构体中,驱动程序是不能“感知”这些操作的,原因是实际采取的恢复步骤是平台发起的。
结构体的格式:
struct pci_error_handlers
{
int (*error_detected)(struct pci_dev *dev, enum pci_channel_state);
int (*mmio_enabled)(struct pci_dev *dev);
int (*link_reset)(struct pci_dev *dev);
int (*slot_reset)(struct pci_dev *dev);
void (*resume)(struct pci_dev *dev);
};
可能的通道状态:
enum pci_channel_state {
pci_channel_io_normal, /* I/O channel is in normal state */
pci_channel_io_frozen, /* I/O to channel is blocked */
pci_channel_io_perm_failure, /* PCI card is dead */
};
可能的返回值:
enum pci_ers_result {
PCI_ERS_RESULT_NONE, /* no result/none/not supported in device driver */
PCI_ERS_RESULT_CAN_RECOVER, /* Device driver can recover without slot reset */
PCI_ERS_RESULT_NEED_RESET, /* Device driver wants slot to be reset. */
PCI_ERS_RESULT_DISCONNECT, /* Device has completely failed, is unrecoverable */
PCI_ERS_RESULT_RECOVERED, /* Device driver is fully recovered and operational */
};
驱动程序不需要实现所有的回调函数;如果实现任何一个,则必须要实现error_detected()。如果回调函数没有实现,则相关的特性也是被认为不支持的。例如如果mmio_enabled() 和resume()没有实现,那么驱动程序不会做任何恢复和要求复位的操作。如果link_reset()没有实现,那么也不会关心链路复位。通常情况下,驱动程序还是有必要实现slot_reset()。
四、错误恢复步骤
对PCI错误事件进行恢复所采取的步骤与平台相关,但将遵循下面描述的一般顺序。
步骤0:Error Event
PCI硬件检测到PCI总线错误。在powerpc上,插槽是隔离的,因为所有的I/O都被阻塞:所有读返回0xffffffff,所有写被忽略。
步骤1:Notification
平台调用error_detected() 回调函数通知受错误影响的驱动程序。
此时,设备可能无法访问。驱动程序可能因为失败的I/O操作已经注意到有错误产生,但这是合适的同步点,换句话说,它给驱动程序一个清理、或等待事务挂起(定时器等)来完成的机会;此时驱动可以使用信号量、调度等,但不能操作设备。驱动程序不应该做任何新的I/O操作直到函数返回。对于任务上下文中的调用,这是一个“静止”点。
系统中所有参与的驱动程序必须要实现这个回调函数。驱动程序必须返回以下结果代码之一:
(1)PCI_ERS_RESULT_CAN_RECOVER:驱动程序可以通过I/O操作恢复硬件或者提取诊断信息,则返回这个结果(见下面mmio_enable)。
(2)PCI_ERS_RESULT_NEED_RESET:驱动程序不执行slot reset不能恢复,则返回这个。
(3)PCI_ERS_RESULT_DISCONNECT:驱动程序如果不想恢复,则返回这个。
下一步的操作将取决于驱动程序返回的结果。
如果段/槽上的所有驱动程序返回PCI_ERS_RESULT_CAN_RECOVER,那么平台应该在槽上重新启用I/O操作 (如果平台没有槽隔离,不需要做任何特别的事情),恢复操作则执行步骤2 (MMIO Enable)。
如果任何一个驱动需要进行slot复位(由于返回的是PCI_ERS_RESULT_NEED_RESET),则恢复操作执行步骤4(Slot reset)。
如果平台无法恢复slot,则执行步骤6 (Permanent Failure)。
步骤2:MMIO Enable
平台对设备重新启用MMIO(但通常不是DMA),然后在所有受影响的设备驱动程序调用mmio_enabled()回调函数。
这个是“前期的恢复”调用。此时允许I/O操作,但有一些限制,比如DMA访问是不允许的。这不是一个让驱动重新启动操作的回调函数,只是peek/poke设备,用来提取诊断信息。如果有的话,如触发设备本地复位或其他类似的操作,但不是重启操作。如果段上所有的驱动程序都允许可以尝试恢复,并且硬件没有执行自动链路复位,则执行此回调。如果平台不能在没有slot复位或链路复位的情况下重新启用I/O操作,则不会调用这个回调函数,而是直接进入步骤3 (Link Reset)或步骤4 (Slot Reset)。
驱动程序必须返回以下结果代码之一:
(1)PCI_ERS_RESULT_RECOVERED
如果驱动认为设备已经完全正常,并且已经准备好再次启动正常的驱动操作,则返回此值。不能保证驱动程序会被允许继续,因为同一段上的另一个驱动程序可能会失败,从而在支持它的平台上触发一个slot复位。
(2) PCI_ERS_RESULT_NEED_RESET 如果驱动认为在当前状态下是不可恢复的,其需要slot复位才能继续,则返回此值。
(3)PCI_ERS_RESULT_DISCONNECT 与上面一样。完全失败,甚至在复位驱动程序后没有恢复。(需要更精确的定义)
下一步取决于驱动程序返回的结果。如果所有驱动程序都返回了PCI_ERS_RESULT_RECOVERED,那么平台将继续执行步骤3 (Link Reset)或步骤5 (Resume Operations)。
如果任何一个驱动返回了PCI_ERS_RESULT_NEED_RESET,则平台将执行步骤4(Slot Reset)
步骤3:Link Reset
平台复位链路,然后调用所有受影响的设备驱动程序的link_reset()回调函数。这个是当检测到非致命错误时会执行此操作,可以通过链路复位解决该错误。这个调用通知驱动程序复位,并且驱动程序应该检查设备是否处于工作状态。
此时驱动程序不应该重新启动正常的I/O操作。驱动应该限制自己探测设备以检查其可恢复性状态。如果一切正常,那么平台将在所有驱动程序已经反馈调用了link_reset()后调用resume()。
结果代码:同步骤3一样 (MMIO Enabled)
然后平台继续步骤4(Slot Reset)或步骤5(Resume Operations)。
步骤4:Slot Reset
对于PCI_ERS_RESULT_NEED_RESET的返回值操作,平台将对PCI设备执行slot复位。平台执行slot复位的实际步骤是依赖于平台的。在slot复位完成后,平台将调用设备的slot_reset()回调函数。
对于大多数PCI设备,软复位满足了恢复的要求。对于那些slot复位不能满足恢复要求的设备,提供了可选的基本复位,以支持少部分的PCIe设备。
如果设备支持PCI热插拔机制,那么复位可能操作电源开关来实现。
平台将PCI的配置恢复到复位后的状态,而不是最近的状态,这个措施是非常重要的。软复位之后,设备驱动通常使用标准的初始化流程,而非常规的配置则会引起设备挂死,内核崩溃或者数据破坏。
这个调用给驱动程序重新初始化硬件的机会(重新下载固件等)。此时,驱动程序可能会假设设备处于一个全新的状态,并且功能齐全。插槽被解冻,驱动程序可以完全访问PCI配置空间、内存映射I/O空间和DMA。中断(Legacy, MSI,或MSI- x)也将可用。
此时驱动不应该重启正常的I/O操作。如果所有的设备驱动上报回调函数执行成功,则平台将调用resume()完成恢复操作,这样允许驱动重启正常的I/O操作。
当设备在复位后依旧不能对设备进行操作,那么驱动将返回致命的错误。如果平台已经尝试了软复位,可能此时需要尝试一下硬复位,然后再尝试软复位。如果设备仍然不能恢复,此时就没有任何办法了,平台将上报Permanent Failure,也就是设备被认为损坏了。
多功能卡的驱动程序需要相互协调,以确定哪个驱动程序实例将执行一次性或全局设备初始化。
结果代码:PCI_ERS_RESULT_DISCONNECT
PCIe设备的驱动需要在probe函数里实现一个基本复位。
平台继续到步骤5(Resume Operations)或步骤6(Permanent Failure)。
步骤5:Resume Operations
如果段上的所有驱动程序都从之前的3个回调中返回了PCI_ERS_RESULT_RECOVERED,平台将在所有受影响的设备驱动程序上调用resume()回调函数。回调的目的是告诉驱动重启活动,一切都回恢复正常并能够正常运行。这个回调函数不会返回结果。
此时如果一个新的错误产生,则平台将重新执行错误恢复流程。
步骤6:Permanent Failure
如果产生Permanent Failure的问题,平台是无法恢复设备的,此时平台将调用error_detected(),里面将包含PCI通道失败的状态。
此时,设备驱动程序应该处于最坏的状况。它应该取消所有挂起的I/O,拒绝所有新的I/O,返回-EIO到更高的层。然后设备驱动程序应该清理所有的内存并从内核操作中移除自己,就像在系统关闭时所做的那样。
平台通常会以某种方式通知系统操作员产生Permanent Failure。如果设备可以热插拔,操作员可能会想要移除并替换设备。然而需要注意的是,并非所有的失败都是真正的“permanent”,有些是由于过热引起的,有些是由于插的位置不好引起的。许多PCI错误事件是由软件错误引起的,例如由于编程错误引起的DMA野指针访问或伪分割事务。