Linux内核工程导论——总线:Linux PCI

PCI

PCI系列总线介绍

         首先,PCI是一种总线,PCI-e是PCI的升级版,在linux的软件系统中是统一都是driver/pci下。既然是一种总线,物理上就包括总线部分和支持该总线的设备。没有设备PCI总线的存在无意义,没有PCI总线的支持,PCI设备无法发挥作用(就得使用其他总线)。

         众多设备本身没有从属关系,如果放由他们任意接入系统,就会对可怜的CPU和内存资源产生竞争,因为哪个设备都希望首先获得并且获取最多的系统资源。网络接入里常用的CSDA/DA机制就是为了解决这种问题而存在。但是当网络速度达到了非常高的时候,这种竞争随机退避的算法对时间的浪费就会极大的影响性能。所以一般的高速网络要么是协调的,要么传输是物理上独立的信道。对于计算机系统来说,独立的物理信道由于成本原因不现实,但是一条物理信道上协调通信却是可行的,因为计算机系统的核心就是处理能力。

         这就是总线存在的原因。而总线有很多种,为何各种总线有好有坏呢?也就是要回答各个总线间的区别在哪里?因为总线的本质是调度资源,而调度分配资源的算法各异,所以评价调度资源的效率就是总线的最主要评价手段。但是除此之外,成本、可扩展性、可热插播机制等辅助功能(通常是方便使用的)的支持程度,也是一个总线是否被广泛接受的重要原因。市场是最好的决策者,各个总线在竞争中,很多淘汰了,很多存活了,最耀眼的就是pci,耀眼到以至于linux内核认为几乎所有设备都是挂载到PCI上的,甚至所有外部总线都是挂载pci总线上的。

         上面说的外部总线,例如USB总线的热插播能力,就是外部总线的一种,外部总线的通常特点是相对核心内部总线较慢,但是在可扩展性和热插播能力上做的更好。

Linux PCI

         首先PCI是总线,所以驱动必然要分为两部分:总线部分和接入总线的设备部分。总线部分描述和实现的就是PCI总线的规范,而接入总线的设备驱动则描述的是接入设备的行为。按照惯例,所有驱动都要注册到PCI总线部分的驱动,以方便总线驱动完成枚举、发现、省电、错误处理等统一的总线操作,并且以总线统一的认知方式提供设备的信息,例如商家、设备类型等。

         需要明白一个模型,就是驱动并不等于设备。驱动是存在于代码的软件模块,而设备是物理存在的。但是驱动是为设备服务的。当设备插入的时候(或之前)驱动已经存在于系统,否则设备不可识别。当设备移除的时候,驱动也不会消失(可以稍后卸载)。通过驱动早于设备识别启动,晚于设备移除而移除内存(可以不移除)。因为无论是设备的添加还是移除都需要驱动里对应的函数被执行。PCI设备也不例外,任何一种PCI设备在插入时,代表该种设备的驱动的probe函数执行,移除时,驱动的退出函数执行。一个驱动对应的是一种设备,一个设备通常对应一种驱动(但复合的设备可能对应多种)。

画个图

         也正是由于以上的原因,一个PCI设备由于可能内涵多种设备,该种复合PCI设备的驱动就有理由不使用总线的发现注册流程,而是自己去扫描发现属于自己能够驱动的设备。当然自己扫描也是利用pci总线的数据接口与未知设备通信。由于通信方式是统一的,如果对象设备是驱动所希望的,就会回复所期望的回复,而其他设备则不认识或回复不正确。也就是说这两种驱动的设备发现逻辑,一种是总线驱动调度的,一种是设备驱动调度的。然而却都是调用总线的传输接口。

画个图(两调度一数据)

PCI设备的初始化

         上面讲述了PCI设备的发现,发现后要进行初始化。要时刻铭记的一点是初始化是由物理设备驱动和总线驱动的软件代码实现的,而不是物理设备本身。

         那么这里就涉及到一个驱动代码和总线代码的交互问题。驱动代码想要做具体的事情都是要通过调用总线代码,而驱动代码决定如何去调用。这就相当于编程时使用一个库中函数。是一个调用关系。然而不同的是,这里的总线代码也有自己的逻辑。也就是可以看出是两个独立的线程实体。总线驱动和设备驱动同时运行,然而设备驱动依赖于总线驱动而正常工作。

       PCI地址空间

         PCI总线协议不仅规定了总线驱动的功能,还规定了想要在pci设备上通信的设备所需要具备的物理条件,任何一个宣称支持pci的设备都必须符合pci协议的规定。其中最重要的就是地址空间。

         地址空间是一个会出现在每个计算机系统的词语。因为所有的处理器对外只看得见地址和地址里面存储的内容。内存数据,设备控制寄存器,设备缓存,甚至磁盘数据等都是通过将自己映射到处理器可见的地址空间中才得以被处理器发现并使用。PCI作为一个高速总线,其总线本身的的寄存器就位于CPU的地址空间内。对于特定的CPU,以x86为例,CPU能看见两个地址空间:内存空间和IO空间,PCI规定,在x86结构下,PCI的放入入口位于IO空间。但是IO空间资源非常有限,所以PCI只占用了两个地址(共8字节)。

         由于PCI总线只占用了CPU的8个字节(两个地址字)的空间,而PCI的功能又很复杂,8个字节又得读又得写,8个字节够吗?答案是够的。PCI精细的将这64个位分解为特定的域,通过不同的组合访问不同的设备,并且双工的将两个地址字分为一个地址(CONFIG_ADDRESS)一个数据(CONFIG_DATA)。例如地址字有的域代表总线编号,有的代表设备编号,有的代表功能编号,有的代表前面域定位到的设备内部的寄存器的编号。这样前面刚定义的编号唯一索引到的设备内部的寄存器通过CONFIG_DATA暴露出来就可以读取和写入了。

PCI设备配置空间

         上一段说了寄存器编号,这是每个PCI设备都要提供的寄存器。PCI规定了PCI设备所需要提供的寄存器的数目和功能,如此上层的PCI总线就可以对任何种类的设备设置已知的寄存器编号获取和设置该设备的特定功能和信息。这些被PCI协议预定义的每个设备都必须实现的寄存器叫做PCI配置空间。PCI规定该空间总长度为256个字节,一共64个字节。因此写入CONFIG_ADDRESS的寄存器编号的取值范围是0-63。

         这64个配置空间寄存器也被PCI协议预定义。那么着64个寄存器都分别有和定义呢?如果全部列举就显得啰嗦,可以查阅。最重要的有两种:一个是描述设备信息的Device ID和Vendor ID(就是设备号和厂商号,vendor ID需要向PCI协会申请,你懂的,外国定义的标准都会留下这种空间让你申请,说白了就是收钱)。另外一个最重要的就是地址指针。

设备配置空间的访问方法

         刚说过可以通过寄存器直接访问,但是这是x86的访问方式,在mips或arm访问方式又是另有规定的。由于在不同的平台不同,所以linux就有了抽象的接口(甚至可以通过bios编程接口访问)。但是linux的抽象接口必定是与linux内部的抽象相关的,例如pci_(read|write)_config_(byte|word|dword) 函数可以直接读取一个设备的配置空间的寄存器,然而其需要提供的参数就是pci_dev *结构体。这是上层编程硬件无关的编程思想。

PCI设备内存缓存数据空间

         配置空间的地址指针是用来做啥的呢?我们刚说了CPU配置和访问PCI设备信息通过两个IO地址空间字进行,但是PCI是个高速总线,如此快的速度,两个字不可能完成全部的数据交互功能。我们也说了,这两个字是配置和获取信息使用的,并不是数据传输的空间。而数据想要传输,必须使用内存,要让CPU看见,也必须有内存空间的地址。这个地址是每个设备一份的,并且是不同的,因为每个设备都要传输数据,都需要缓存数据。因此定义这个地址的最好位置就是PCI设备配置空间的寄存器中。所以配置空间中就定义了。由于每个设备不可能固定的确定自己所要使用的内存地址(否则会冲突),应该由操作系统动态的根据当前内存的使用情况来动态的分配,因此配置空间的数据地址寄存器应该由操作系统,也就是PCI总线驱动写入。那么这个地址是如何确定的呢?

动态确定PCI设备的数据地址

         这个主题有些偏技术层面,但是手法很有趣,所以拿出来说一下。这个机制也是PCI总线协议规定的。由于每个PCI设备所需要的内存数据空间不是一样的(换句话说是PCI总线协议允许你不一样),也就是说每个设备不可以要求地址的具体位置,但是必须在配置空间中告诉系统要多大的内存。这两步是通过同一个寄存器完成的(基地址寄存器,32位)。一个设备会将该寄存器的部分为设为可写,部分位设为只读,当系统上电的时候会向全部的位写入1,如此由于部分位不可写,其值就为0,部分位可写,其值就为1。如此操作系统再读取就会得到一个设备相关的值,该值就表示需要的空间大小。比如该设备需要64KB的地址空间,这个值就是0xFFFF0000。系统得到大小后,就分配所需大小的内存并将其分配得到的地址写入该寄存器的可写部分。如此之后PCI设备就知道他所拥有的内存地址空间了。而内核中的其他组件就可以通过CONFIG_ADDRESS和CONFIG_DATA两个寄存器读取该设备的配置空间来获得该设备的内存数据地址了。

         这个地址位于PCI设备硬件上的最大好处就是DMA,由于设备直接可见数据地址,所以可以不经过CPU,直接使用DMA引擎将数据传输到内存。

初始化流程

         上面描述PCI设备内存基地址的确定是设备初始化的一部分。完整的初始化流程如下:

1.        使能PCI设备。要使用一个PCI设备必须得经过总线驱动的同意,否则后续操作命令总线驱动不予传达

2.        初始化PCI设备的内存数据空间、基地址寄存器和DMA

3.        向中断子系统申请中断号。数据DMA后要通知内核,通知内核要使用中断号来让内核知道是谁

设备移除的关闭流程正好是相反的。

PCI驱动职责与PCI总线驱动职责

         前面说过PCI设备驱动与PCI总线驱动如设备驱动依赖总线驱动的两个线程。总线驱动是固定的,设备驱动是要不同的设备商家使用总线驱动来实现的。而PCI总线中规定了很多操作,这些操作并不是每个设备驱动都要实现的,正因为是总线规定的,所以具有通用性,很多就实现在了PCI总线驱动中,所以PCI设备的驱动作者就得明白什么不需要自己实现,什么需要。例如Fast Back to Back writes机制就是实现在总线驱动中的。如设备ID,总线驱动中不可能全部包含,所以各个驱动就需要在总线驱动不包含的情况下自己定义在驱动中。

         另外,虽然总线驱动实现了PCI设备的发现和配置,怎么使用这个配置也是由总线规定的,但是实际的使用者却是驱动。例如PCI总线驱动配置了PCI设备的内存数据的地址,但是设备驱动在向这个地址写入数据的时候必须得了解到这个地址是内存地址,而驱动要写入的数据是要写到设备中的。写入到内存地址后,DMA并不一定立即启动将数据传输到设备,就算启动也需要时间。因此设备驱动在写入内存后如果要读,需要等待一段时间或者强制将数据立即刷到设备上。

PCI中断系统:MSI

         MessageSignaled Interrupts。PC机的物理系统上,中断是通过引脚的高低电瓶实现的,一般的CPU只有很少的中断输入,为了区别更多的中断,通常外置中断芯片,中断芯片向外提供很多中断引脚,通过查询中断芯片的寄存器获得时哪个设备的中断。先进一些的系统在CPU内部就有分级的中断标识,但所有这些都是通过引脚高低电平变化实现的。内核定位中断类型是通过树形的寄存器组织追踪哪个置位确定的。

         linux内核中抽象了这种硬件树形中断架构,因为不同的芯片树的组织不一样,甚至简单的直接是几个扁平的寄存器就可以代表所有中断,甚至中断类型不需要使用寄存器,而是使用中断函数去查询确定是哪个设备的中断。更有难以的处理的是多个CPU分享组织不同的中断。由于组织的不确定性,但区分不同中断的需求是确定的,linux内核提供了以中断号为核心的中断系统。每个中断对于linux来说都是某个中断号(例如32号中断)有中断,调用与该中断号关联的中断处理程序执行,而一个中断号可以挂载多个中断处理程序,以方便多个驱动共享同样的中断号。

         PCI协议为PCI设备定义了新的中断方式,虽然如此,其上层也是使用的操作系统的固定的中断方式。也就是说任何一个PCI设备发生了中断,PCI总线对应的中断引脚还是会起作用,中断号还是会被激发,对应的中断处理函数还是会被调用。所不同的是,PCI是个总线,对于CPU来说虽然只是一个PCI中断,但实际上可能是总线上任何一个设备的中断信号。PCI协议定义了一种协议来区别是哪个PCI设备的中断,这种协议叫做MSI(或升级版的MSI-X)。

         其原理是任何一个PCI设备发生中断,其向PCI总线的驱动程序发送一个信号消息,该消息代表了具体的中断信息。这种机制会在内存中模拟出类似传统的中断寄存器,只是大小不受限于一个寄存器的大小。因此MSI可以支持32个中断源,MSI-X可以支持2048个中断源。MSI的32个中断源在内存中必须连续分配,而MSI-X不需要。MSI的可能受限于单个CPU,而MSI-X可以跨CPU分配。可以看出MSI-X是对MSI的升级。

         MSI的存在,使得触发中断的时机可以由设备掌控(例如让数据传送完毕再触发中断),并且可以让一个PCI设备可以触发多种中断(这在传统架构中是不可以的)。如果没有这种机制,处理中断的步骤将会是PCI总线的中断引脚被触发,CPU只会知道是PCI总线上的某一个设备发生了中断,然后调用中断处理程序,遍历PCI总线上的所有设备,直到找到发出中断的设备。由于PCI速度快,发生中断很可能是并发的,如此的响应方式就会产生很多问题(例如饥饿)。

         当然linux允许你关闭PCI的MSI功能(当然PCI协议也是允许的),关闭之后就采用传统的中断处理方式去遍历寻找。显著降低效率,但是考虑到不是所有的PCI设备都支持MSI,所以这种支持是有必要的。

PCI-E错误处理

         一个总线协议标准一定会定义错误处理与恢复,相对应的一个总线的软件驱动也必须实现这种错误处理机制。在linux中这个机制实现的名字叫PCI Express Advanced Error

Reporting (AER) driver。

         PCI-E定义了两种错误处理方式:一种是所有设备都支持的baseline capability,提供最小支持,但是要求所有PCI设备都支持可以汇报。另一种是扩展的AER机制,其实就是提供更多的错误信息,方便前端用户调试和恢复。AER驱动就是收集这种扩展信息,并且提供给用户的机制。

PCI设备用户空间视图

         用户可以用lspci来查看当前的PCI设备。

         每一行的开头有3个数,第一个是总线编号,这里都是00(一个系统可以有多条总线,每个总线都是一个单独的域,叫做PCI域)。第二个是设备编号,可以唯一的定位一个设备。第三个是功能编号,一个设备可以有多个功能。        

sys目录下也有。

PCI总线协议

         物理上的东西,王齐的《PCIExpress体系结构导读》中说的比较详细。

         总的来说PCI是一种传统的总线结构,PCI-E则变成了目前高速设备正在普遍采用的网络结构。通常的总线结构相当于计算机网络的总线结构,所有设备共享总线,通过总线调度为各个设备提供服务。而网络结构相当于计算机网络的星型结构,系统中各个设备直接连接到交换机,交换机连接到路由器,各个路由器之间通过路由进行转发通信。现代计算机网络思想已经全面进入硬件内部领域。

         PCI-E由于是星型网络的。每个小型的PCI-E系统中只有一个RC(root complex,直接连接CPU的设备,相当于路由器),实际的架构就需要交换机设备,叫做switch。

画图:PCI_E星型拓扑

         每个switch可以有多个口,switch之间可以级联。所以必然有一个switch直接连接到RC,然后向下扩展成网络,每个switch的出口都可以接设备,并且只能接入一个。这在我们的交换机网络中是司空见惯的事情,但是在硬件中这却是一个不小的进步。因为之前的做法相当于每个switch口出来可以一条总线挂载多个设备。

         星型的网络结构显著的增加了吞吐量和链路利用效率和可扩展性,USB总线等现代总线都已经采用这种结构。USB总线的流行的根本原因可能也是因为这个精彩的架构的使用。

物理层

         PCI-E的物理层抛弃了PCI的单端信号(使用不稳定的地作为信号回路),改用了抗干扰能力极强的差分信号。采用的编码是8/10编码。8/10编码是IBM已经过期的专利编码,这个编码专门用在告诉串行总线。由于高速串行总线要求电流总体为0(直流平衡),所以数据流中的1和0的数目必须一样多。所以必须经过编码,能够让0和1一样多的编码就是8/10编码。

         如果连续的出现1或0也会导致物理链路出现问题(耦合电容充满),所以8/10编码还保证连续1或0不超过5个(这是标准的,PCI-E使用的8/10编码可以保证10个位中最多有6个1或0,从而大致的保证不连续)。编码的时候将8个字节分为3和5个,然后编码为4和6个。但是大致的保证并不能一定,所以PCI-E还有额外的机制防止万一叫CRD,这里就不多介绍。

协议数据包

         既然PCI-E采用了交换式架构,这也决定了其使用的可交换的数据包进行通信。协议定义了很多数据包,分为两层:数据链路层和事务层。链路层有链路训练的功能,总线事务也有很多种,例如存储器读写、配置读写、Message总线事务、原子操作。还有一些高级功能,如流量控制、虚通路管理等。两层都有数据数据报文和控制报文两种。下层为上层提供服务,上层使用下层的服务接口。

PCI总线与USB总线的关系

         你使用lspci命令列出所有的pci设备,你会发现在linux内核看来,大部分情况下usbcontroller是pci-e的一个设备,也就是说usb总线是挂载在pci(pci-e)总线上的。因此到usb的存储数据的真实流向应该是:用户——文件系统——通用块层——SCSI——USB——PCI。大家可能会疑惑的一点是从cpu出来数据明明是首先经过pci-e,然后再经过usb controller到达usb设备,为何内核中数据走向是先经过usb?一个更完整的单机流程如下:

用户——文件系统——通用块层——SCSI驱动——USB驱动——PCI驱动——PCI硬件——USB硬件——USB设备。还是强调一点:驱动和硬件不是一个实体。


你可能感兴趣的:(linux,linux,kernel,内核)