仓库源文

.. Kenneth Lee 版权所有 2017-2020

:Authors: Kenneth Lee :Version: 1.0

PCIE总线的保序模型


PCI 3.0规范(注意,这里说的是PCI LocalBus Specification,不是PCIE的标准,但后者 基于前者设计)附录E描述的保序模型,跨越了软件和硬件两层设计的众多概念,很容易导 致硬件设计者和软件设计者都对这里描述的模型在理解上产生误差,讨论不到一起去。本 文帮助厘清这种偏差。

本文厘清这种误解的方法,是让所有的概念都回到软硬件接口的位置上,让无论是软件还 是硬件的内部概念都映射回“软件访问地址空间”这个语言空间中。这种方式,也是作者提 议软硬件在讨论接口的时候需要使用的方法:无论我们用什么办法描述自己在满足接口上 的要求,我们都要把这种概念映射回“软件访问地址空间"这个模型中。

本文是为了和第三方讨论,不保证所总结的信息是正确的。

简单起见,本文忽略x86特有的io空间(通过in,out等指令形成的io地址空间),那个空间 的行为可以通过一般内存load, store的地址访问行为作为类比来理解。

这样,CPU对设备和内存的理解可以抽象为如下模型:

    .. figure:: _static/计算机结构1.png

CPU通过对不同地址的读写,实现对内存或者外设空间的存取。一条读写指令的完成时间, 取决于被读写的终点什么时候响应这个读写请求,如果终点响应慢了,或者这个终点不响 应了,CPU的读写指令就会延迟,或者直接挂死。软件运行在CPU上,这就是软件可以看见 的硬件行为的全部,无论你的总线系统有多少小九九,都必须反映到这个名称空间上。所 有的行为,都应该反馈为读写的结果,以及这些读写行为的开始和结束时间。

对于这个名称空间,我们还有两个最基本的补充。第一,内存部分,我们称为“内存子系统 ”,而不称为“DDR”,表示我们认为Cache和DDRC,DDR都是一体的,只要CPU读写某个地址的 时候能够拿到正确的值,我不关心你是从Cache中拿到的,还是直接从DDR中拿到的。

第二,CPU对读写指令一个重要的要求就是保序,每个CPU对内存或者IO访问的保序,只能 保证到本CPU内部。比如读入一个地址,然后计算,CPU肯定会保证读入完成后,才进行计 算。但它并不能保证它对内存或者IO空间的读写顺序,反映在其他CPU所观察到的情形上, 和本CPU上看到的是一致的。CPU的内存屏障指令,可以让CPU可以等待这些指令行为,被传 播到对应的观察者(其他CPU或者外设)上,但这种等待,是基于这些观察者的反馈,如果 观察者没有给出正确的反馈,本CPU不会知道。

外设也可以操作内存(DMA),外设和内存子系统间有什么约定,那是外设和总线系统之间 的事,CPU(软件)是不管的。外设只能把这种约定反映的顺序关系告知软件,从而形成 CPU软件和外设软件(也可以认为是固件)之间的通讯关系,这种通讯关系,是建立在我们 前面提到的内存访问关系之上的行为,而不被(软件工程师)认为是内存访问行为本身的 一部分。

现在我们在来看看PCIE是怎么连接进来的:

    .. figure:: _static/计算机结构2.png

我们看到了,PCIE子系统其实是总线的延伸,但由于PCIE这个总线系统和CPU的总线系统来 自不同的供应商(设计者),它们之间就必须有协议来优化和保证CPU(软件)的和终点( EP)之间的通讯。但这种保证,不能改变我们前面说过的那个名称空间。

也就是说,不管EP,PCIE总线和系统总线有多少交易, 这些交易都必须反映为CPU读写指 令上的行为。

好了,我们现在开始看PCIE本身实现上的难处。PCIE总线是个多层总线,很多时候,它的 速度比系统总线慢得多(时延或者带宽上都有可能慢)。为了提高速度,它用了两个优化 :

第一,写操作(Posted),可以由网桥直接响应完成。也就是说,CPU发下去的一个IO写, 在发到EP前,就可以结束(由PCIE的中间网桥响应结束)。(注:这个结论本身是错的, 只是说原理如此,帮助理解,理由下文解释)比如你执行:::

    *((volatile u32 *)0x12345678)=0x01;

向EP的特定地址写1(假定这是命令EP“开始执行”),这条指令返回(结束)了,其实EP并 没有执行,如果你随后发出另一个依赖这个“执行”的指令,后一个依赖的指令就可能失败 。这个设计带来的好处是,如果多个写操作之间没有依赖关系,你连续执行写操作,就有 机会让效率提高。

这种优化无法发生在读操作上,因为“读完成”需要提供读到的数据,所以请求没有发到EP 上,你无法返回。

第二,鼓励使用内存(CPU访问内存或者EP做DMA)取代IO访问进行通讯。如前所述,PCIE 写操作(Posted)是可以不用等待的,但读操作通常都需要做等待(这样会导致做完一个 通讯来回,再做下一个,严重拖慢带宽),所以,读IO空间会非常慢,更好的方法是,直 接让EP通过DMA写入内存,CPU从内存读数据。从而不会拖慢CPU的执行效率。一种常见的做 法是,比如我要做一个网卡收报的功能,网卡(EP)直接向内存中写数据,等待写够了, 最后用MSI消息(MSI消息也是一个从EP发起的写操作,只是对CPU的效果是产生中断)通知 CPU(驱动):“数据已经准备完成了,请从内存中获得数据”。

我要提醒的是,你EP和PCIE子系统可以知道你发出的是每个读写请求是有顺序要求的 (Strong Ordering),还是没有顺序要求的(Relaxed Ordering),但CPU(软件)是不会知 道的。所以你不能给软件引入RO,SO这些概念(不是说不能提,而是你谈接口的时候不能 依赖这个),你只能告知软件,数据有效的时机是什么(某个flags被置位,或者收到特定 中断以后)。

PCI 3.0标准附录E把这个软件的需求和硬件的行为放到一起了,对我来说,它的描述还是 很清楚的。但它不是软件编程指引,甚至我会认为它对软件编程没有任何指导价值,你必 须基于这个协议,给软件设计师提供EP和CPU对应驱动的通讯协议。

附录E能给软件提供的信息是:

  1. 内存之外的所有读写操作,都不是Posted操作。也就是说,软件对IO空间的所有读写行 为,从发送到总线一刻起,就是保序和缓慢的(必须由EP响应)。所以,快速通道是 CPU和EP通过内存进行通讯,但两者(在内存上)的保序关系,只有EP才有决定权(基 于Ordering Rules for a Bridge),CPU一侧完全不能控制(通过配置EP或者PCIE控制 器来改变是另一个回事)

2.多个CPU在IO空间访问上确定的顺序,不能被按顺序传递到EP上。软件不能依赖这一点设 计协议。换句话说,同步必须由EP完成,不能依靠CPU间的同步来实现对EP的控制(因为 Retry的存在)

  1. PCIE协议没有保证“对EP的读写操作会保证,CPU Cache中的数据被EP感知”,CPU必须主 动保证对EP的IO请求前,完成Cache的Flush。

注:页表本身的属性,内存访问本身是否带#LOCK标记,可以在一定程度上影响CPU对PCIE 总线发起的Transaction的行为,但由于这不影响我们的结论,这里一概忽略。