【HARDWARE.0x00】PCI 设备简易食用手册

本文最后更新于:2023年10月27日 晚上

👴等会把你总线都给扬了

0x00.一切开始之前

因为笔者最近不懂为什么开始写 PCI 设备驱动了,但笔者是 网络空间安全专业 的本科生,此前基本上没有接触过与硬件相关联的知识(因为计组和微原课讲的就是个🐓⑧),所以开了这篇新的博客简要记载一些与 PCI 设备相关的基础知识、基本 Linux PCI 驱动的编写等

为了写这篇博客,笔者翻了笔者大二上学期的计算机组成原理的课本,还翻了大二下学期的微机原理的课本,发现这两门课根本就是什么都没讲……因为也弄不到微院那边相关的教材,所以本篇博客并没有一个系统性的指导来辅助写作,都是各种东拼西凑+笔者自己的理解,很多东西因为笔者自身水平低下的缘故只能一笔带过,因此可能会显得不够专业,希望读者见谅XD

如果需要更为专业的参考资料请直接参考 《PCI_Express_Technology》

以及笔者非常深刻的意识到在硬件这一块的知识笔者相比于那些专门搞硬件的人而言**确实差了很多…..**只能说继续努力吧XD

0x01. PCI basic knowledge

一、总线结构简述

我们都知道计算机的五个基本组件为:输入,输出,存储器,运算器(或数据通路),控制器。那么这几大组件之间怎么通信呢?答案是依靠系统总线

总线(bus)是一种将多个功能单元进行连接并允许功能单元之间进行数据交换的一种数据通路,在现代计算机中通常采用总线结构,即存在一根主要的公共通信干线,CPU 及各种设备都通过这跟总线进行通信

总线按功能可以分为以下三种类型:

  • 片内总线:芯片内的总线,位于 CPU 内部,用以在寄存器与寄存器、寄存器与 ALU 之间进行数据交换
  • 系统总线:计算机系统内各功能单元(CPU、主存、I/O)之间的公共通信干线,也称之为 内总线
  • 通信总线:用于计算机系统之间或是计算机系统与其他系统(例如远程通信设备)之间进行通信的总线,也称之为 外总线

总线是可以扩展的,即可以存在多个不同类型的总线相连,不同的设备接入到不同类型的总线上

image.png

二、PCI 概念简述

PCI 即 Peripheral Component Interconnect,是一种连接电脑主板和外部设备的总线标准,其通过多根 PCI bus 完成 CPU 与 多个 PCI 设备间的连接,,在 X86 硬件体系结构中几乎所有的设备都以各种形式连接到 PCI 设备树上

PCI express 是新一代的总线标准,它沿用既有的PCI编程概念及信号标准,并且构建了更加高速的串行通信系统标准

我们首先明确 PCI 标准中的三个基本组件:

  • PCI 设备(device):符合 PCI 总线标准的设备都可以称之为 PCI 设备,在一个 PCI 总线上可以包含多个 PCI 设备
  • PCI 总线(bus):用以连接多个 PCI 设备与多个 PCI 桥的通信干道
  • PCI 桥(bridge):总线之间的连接枢纽,主要有以下三种:
    • HOST/PCI 桥:也称为 PCI 主桥或者 PCI 总线控制器,用以连接 CPU 与 PCI 根总线,隔离设备地址空间与存储器地址空间,现代 PC 通常还会在其中集成内存控制器,称之为北桥芯片组(North Bridge Chipset)
    • PCI/ISA 桥:用于连接旧的 ISA 总线,通常还会集成中断控制器(如 i8359A),称之为南桥芯片组(South Bridge Chipset)
    • PCI-to-PCI 桥:用于连接 PCI 主总线(Primary Bus)与次总线(Secondary Bus)

PCI采用树形拓扑结构,一个典型的 PCI 架构如下图所示,由一个 PCI Host Bus 负责总的通信, 在 Host Bus 下挂载着一个或多个 PCI Root Bridge,一个 PCI Root Bridge 管理一个 PCI Local Bus 空间,挂载着一颗 PCI 总线树:

image.png

由此,一个多层 PCI 总线结构如下图所示:

image.png

我们来看一个现实中的经典例子,以下图的 Intel 440FX 芯片组为例,PCI Host Bridge 分隔开了存储器域与 PCI 设备域,其分别使用独立的地址空间

image.png

在 Linux 下我们可以使用 lspci 指令查看插在当前机器的 PCI bus 上的 PCI 设备,使用 -t 参数查看树形结构,-v 参数可以查看详细信息:

这里展示的结果有 virtio 设备是因为笔者是在阿里云学生机上使用的命令,这类机器一般其实都是用 Qemu 跑的虚拟机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ lspci
00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.2 USB controller: Intel Corporation 82371SB PIIX3 USB [Natoma/Triton II] (rev 01)
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Cirrus Logic GD 5446
00:03.0 Ethernet controller: Red Hat, Inc. Virtio network device
00:04.0 Communication controller: Red Hat, Inc. Virtio console
00:05.0 SCSI storage controller: Red Hat, Inc. Virtio block device
00:06.0 Unclassified device [00ff]: Red Hat, Inc. Virtio memory balloon
$ lspci -t -v
-[0000:00]-+-00.0 Intel Corporation 440FX - 82441FX PMC [Natoma]
+-01.0 Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
+-01.1 Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
+-01.2 Intel Corporation 82371SB PIIX3 USB [Natoma/Triton II]
+-01.3 Intel Corporation 82371AB/EB/MB PIIX4 ACPI
+-02.0 Cirrus Logic GD 5446
+-03.0 Red Hat, Inc. Virtio network device
+-04.0 Red Hat, Inc. Virtio console
+-05.0 Red Hat, Inc. Virtio block device
\-06.0 Red Hat, Inc. Virtio memory balloon

我们还可以使用 lshw -businfo 命令来获取设备信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ sudo lshw -businfo
[sudo] password for arttnba3:
Bus info Device Class Description
=========================================================
system Alibaba Cloud ECS
bus Motherboard
memory 96KiB BIOS
cpu@0 processor Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz
memory 2GiB System Memory
memory 2GiB DIMM RAM
pci@0000:00:00.0 bridge 440FX - 82441FX PMC [Natoma]
pci@0000:00:01.0 bridge 82371SB PIIX3 ISA [Natoma/Triton II]
pci@0000:00:01.1 storage 82371SB PIIX3 IDE [Natoma/Triton II]
pci@0000:00:01.2 bus 82371SB PIIX3 USB [Natoma/Triton II]
usb@1 usb1 bus UHCI Host Controller
usb@1:1 input QEMU USB Tablet
pci@0000:00:01.3 bridge 82371AB/EB/MB PIIX4 ACPI
pci@0000:00:02.0 display GD 5446
pci@0000:00:03.0 network Virtio network device
virtio@0 eth0 network Ethernet interface
pci@0000:00:04.0 communication Virtio console
virtio@1 generic Virtual I/O device
pci@0000:00:05.0 storage Virtio block device
virtio@2 /dev/vda disk 42GB Virtual I/O device
virtio@2,1 /dev/vda1 volume 39GiB EXT4 volume
pci@0000:00:06.0 generic Virtio memory balloon
virtio@3 generic Virtual I/O device
system PnP device PNP0b00
input PnP device PNP0303
input PnP device PNP0f13
storage PnP device PNP0700
communication PnP device PNP0501
veth073b1a5 network Ethernet interface
veth2c8670f network Ethernet interface
vethc0202a2 network Ethernet interface
veth49e878e network Ethernet interface

PCI 设备是在内核启动初始化阶段进行枚举的,这个时候可能有的设备还没准备好,从而没被枚举到,这种情况下我们可以使用如下命令重新进行设备枚举:

1
$ echo "1" > /sys/bus/pci/rescan

三、PCI 设备编号

每个PCI 设备都有着三个编号:总线编号(Bus Number)、设备编号(Device Number)与功能编号(Function Number),作为设备的唯一标识;在此之上还有 PCI 域的概念,一个 PCI 域上最多可以连接 256 根 PCI 总线

当我们使用 lspci 命令查看 PCI 设备信息时,在每个设备开头都可以看到形如 xx:yy.z 的十六进制编号,这个格式其实是 总线编号:设备编号.功能编号,当我们使用 lspci -v 查看 PCI 设备信息时,在总线编号前面的 4 位数字便是 PCI 域的编号

四、PCI 设备配置空间

每个 PCI 逻辑设备中都有着其自己的配置空间(configuration space),通常是设备地址空间的前 64 字节(新版的设备还扩展了 0x40~0xFF 这段配置空间),其中存放了一些设备的基本信息,如生厂商信息、IRQ中断号、mem 空间与 io 空间的起始地址与大小等

Intel 芯片组中我们可以使用 IO 空间的 CF8/CFC 地址(端口)来访问 PCI 设备的配置寄存器:

  • CF8CONFIG_ADDRESS,即 PCI 配置空间地址端口。
  • CFHCONFIG_DATA,即 PCI 配置空间数据端口。

当我们往 CONFIG_ADDRESS 端口填入对应的设备标识后,就可以从 CONFIG_DATA 端口上读写 PCI 配置空间的内存, CONFIG_ADDRESS 端口的格式如下:

  • 31 位:Enable 位
  • 23:16 位:总线编号
  • 15:11 位:设备编号
  • 10:8 位:功能编号
  • 7:2 位:配置空间寄存器编号
  • 1:0 位:恒为 00

除了通过端口访问外,我们也可以通过 MMIO 的方式访问一个 PCI 设备的地址空间

现在我们来看 PCI 配置空间的结构,PCI 设备分为 BridgeAgent 两类,故配置空间也分为相应的两类

Agent 类型配置空间又被称为 Type 00h,格式如下图所示:

image.png

相应地,Bridge 类型配置空间被称为 Type 01h,与 Agent 类型配置空间大同小异:

image.png

简单介绍几个比较重要的字段:

  • 设备标识相关:

    • Vendor ID:生产厂商的 ID,例如 Intel 设备通常为 0x8086

    • Device ID:具体设备的 ID,通常也是由厂家自行指定的

    • Class Code:类代码,用于区分设备类型

    • Revision ID:PCI 设备的版本号,可以看作 Device ID 的扩展

  • 设备状态相关:

    • Status:设备的状态字寄存器,各 bit 含义如下图所示:

      image.png

    • Command:设备的状态字寄存器,各 bit 含义如下图所示:

      image.png

  • 设备配置相关:

    • Base Address Registers:决定了 PCI 设备空间映射到系统空间的具体位置,有两种映射方式:MMIO 与 PMIO,映射方式由最低位决定,不可更改

    • Interrupt Pin:中断引脚,该寄存器表示设备所连接的引脚

    • Interrupt Line:中断编号

前面我们讲到 lspci 命令,我们可以使用 -s 来通过指定查看的具体 PCI 设备,通过 -m 查看部分信息,通过 -nn 查看比较详细的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ lspci -vv -s 00:02.0 -m
Device: 00:02.0
Class: VGA compatible controller
Vendor: Cirrus Logic
Device: GD 5446
SVendor: Red Hat, Inc.
SDevice: QEMU Virtual Machine

$ lspci -vv -s 00:02.0 -nn
00:02.0 VGA compatible controller [0300]: Cirrus Logic GD 5446 [1013:00b8] (prog-if 00 [VGA controller])
Subsystem: Red Hat, Inc. QEMU Virtual Machine [1af4:1100]
Control: I/O+ Mem+ BusMaster- SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR+ FastB2B- DisINTx-
Status: Cap- 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
Region 0: Memory at fc000000 (32-bit, prefetchable) [size=32M]
Region 1: Memory at febd0000 (32-bit, non-prefetchable) [size=4K]
Expansion ROM at 000c0000 [disabled] [size=128K]
Kernel driver in use: cirrus
Kernel modules: cirrusfb, cirrus

我们还可以直接使用 -x 参数来查看 PCI 设备的配置空间:

1
2
3
4
5
6
$ lspci -s 00:02.0 -x
00:02.0 VGA compatible controller: Cirrus Logic GD 5446
00: 13 10 b8 00 03 01 00 00 00 00 00 03 00 00 00 00
10: 08 00 00 fc 00 00 bd fe 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 f4 1a 00 11
30: 00 00 bc fe 00 00 00 00 00 00 00 00 00 00 00 00

在 Linux 当中我们也可以通过 procfs 或 sysfs 这样的文件系统来查看设备的相关配置信息,例如通过 /proc/bus/pci/00/00.0 文件我们同样可以查看 PCI 设备 00:02.0 的配置空间:

1
2
3
4
5
$ cat /proc/bus/pci/00/02.0 | xxd
00000000: 1310 b800 0301 0000 0000 0003 0000 0000 ................
00000010: 0800 00fc 0000 bdfe 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 f41a 0011 ................
00000030: 0000 bcfe 0000 0000 0000 0000 0000 0000 ................

通过 /sys/devices/pci0000:00/0000:00:02.0/resource 获取到的信息中每行表示一个地址空间,其中第一行为 MMIO,第二行为 PMIO,三列信息分别为起始地址、终止地址、标志位 ,:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ sudo cat /sys/devices/pci0000\:00/0000\:00\:02.0/resource
0x00000000fc000000 0x00000000fdffffff 0x0000000000042208
0x00000000febd0000 0x00000000febd0fff 0x0000000000040200
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000c0000 0x00000000000dffff 0x0000000000000212
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

通过 /sys/devices/pci0000:00/0000:00:02.0 下的其他文件也可以访问该设备的一些其他资源信息(例如通过 resource0 可以直接访问 MMIO 空间,resource1 则为其 PMIO 空间)

五、PCI Base Address register

I.基本概念

Base Address register(BAR)是 PCI 设备配置空间中非常重要的一部分,该组寄存器(也称之为 BAR空间)用以定义 PCI 需要的配置空间大小以及配置 PCI 设备占用的地址空间

我们都知道与设备通信有两种方式:MMIO 与 Port IO,相应地 BAR 的格式也有如下两种:

  • MMIO

    image.png

  • PMIO

    image.png

II. BAR 的初始化

当 PCI 设备复位后,其会在 BAR 中存放该设备所需使用的资源类型与大小,当操作系统对 PCI 总线进行配置时,首先会获取到 PCI 设备的 BAR 中的初始信息,之后根据该初始信息分配合理的 PCI 总线域地址,将其写回到 BAR 当中

通过 BAR 进行资源分配的具体过程如下:

  • 当 PCI 复位时,其会向 BAR 中写入资源信息,通过将低位的 bit 设置为 read only 的 0 来标识最小地址空间大小
  • 系统软件(例如 BIOS)通过向 BAR 写一个所有 bit 都为 1 的值来确定从哪个 bit 开始是可写的,从而获取到该 BAR 对应所需的最小地址空间,同时通过最低位来获取到 BAR 的类型,并对应为这些 BAR 空间分配地址,并将分配的地址写回 BAR 空间中

比如说低 20 bit 都不可写,那就是说这个 bar 所需要的地址空间最小为 1MB,最后从地址总线上分配一个1MB 对齐的地址写回 bar 里

III. 处理器域与 PCI 域间访问

需要注意的一点是,处理器使用存储器域的地址,而 BAR 寄存器存放 PCI 总线域的地址,因此处理器不能直接通过 BAR + offset 的方式访问 PCI 设备的 BAR 空间,而应当要将 PCI 总线域的地址转换为存储器域的地址

由此,PCI BAR 中地址在存储器域中皆有着相应的映像,当处理器访问 PCI 设备的地址空间时,首先访问该设备在存储器域中的地址空间,之后通过 HOST 主桥将存储器域上地址空间转换为 PCI 总线域的地址空间,最后通过 PCI 总线将数据发送到指定的设备中

反之亦然,当 PCI 设备需要访问存储器域的地址空间时(DMA 操作),首先需要访问该存储器地址空间所对应的 PCI 总线空间,之后通过 HOST 主桥将其转换为存储器地址空间,再由 DDR 控制器完成对存储器的读写

六、PCI 设备内存 & 端口空间与访问方式

前面我们讲了 PCI 设备与特性和配置相关的配置空间,现在我们来看与 PCI 设备与实际操作相关的内存映射空间与端口映射空间

所有 IO 设备的内存与端口空间需要被映射到对应的地址空间/端口空间中才能访问,这需要占用部分的内存地址空间与端口地址空间,即我们有两种映射外设资源的方式:

  • MMIO(Memory-mapped I/O):即内存映射 IO。这种方式将 IO 设备的内存与寄存器映射到指定的内存地址空间上,此时我们便可以通过常规的访问内存的方式来直接访问到设备的寄存器与内存
  • PMIO(Port-mapped I/O):即端口映射 IO。这种方式将 IO 设备的寄存器编码到指定的端口上,我们需要通过访问端口的方式来访问设备的寄存器与内存(例如在 x86 下通过 inout 这一类的指令可以读写端口)。IO 设备通过专用的针脚或者专用的总线与 CPU 连接,这与内存地址空间相独立,因此又称作 isolated I/O

完成映射之后通过相应的内存/端口访问到的便是 PCI 设备的内存/端口地址空间

例如实模式下的 0xA0000 ~ 0xBFFFF 这 128KB 地址空间通常被用作显存的映射,当我们在实模式下读写这块区域时通常便是直接读写显卡上的显存,而并非普通的内存

通过 procfs 的 /proc/iomem 我们可以查看物理地址空间的情况,其中我们便能看到各种设备所占用的地址空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ sudo cat /proc/iomem
00000000-00000fff : Reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c91ff : Video ROM
000c9800-000ca1ff : Adapter ROM
000ca800-000ccbff : Adapter ROM
000f0000-000fffff : Reserved
000f0000-000fffff : System ROM
00100000-7ffdffff : System RAM
1f400000-20200e70 : Kernel code
20200e71-2105843f : Kernel data
2132b000-217fffff : Kernel bss
7ffe0000-7fffffff : Reserved
80000000-febfffff : PCI Bus 0000:00
fc000000-fdffffff : 0000:00:02.0
fc000000-fdffffff : cirrus
feb80000-febbffff : 0000:00:03.0
febd0000-febd0fff : 0000:00:02.0
febd0000-febd0fff : cirrus
febd1000-febd1fff : 0000:00:03.0
febd2000-febd2fff : 0000:00:04.0
febd3000-febd3fff : 0000:00:05.0
fec00000-fec003ff : IOAPIC 0
fee00000-fee00fff : Local APIC
feffc000-feffffff : Reserved
fffc0000-ffffffff : Reserved

通过 procfs 的 /proc/ioports 我们可以查看 IO 端口情况,其中便包括各种设备对应的 PMIO 端口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
$ sudo cat /proc/ioports
0000-0cf7 : PCI Bus 0000:00
0000-001f : dma1
0020-0021 : pic1
0040-0043 : timer0
0050-0053 : timer1
0060-0060 : keyboard
0064-0064 : keyboard
0070-0071 : rtc0
0080-008f : dma page reg
00a0-00a1 : pic2
00c0-00df : dma2
00f0-00ff : fpu
0170-0177 : 0000:00:01.1
0170-0177 : ata_piix
01f0-01f7 : 0000:00:01.1
01f0-01f7 : ata_piix
0376-0376 : 0000:00:01.1
0376-0376 : ata_piix
03f2-03f2 : floppy
03f4-03f5 : floppy
03f6-03f6 : 0000:00:01.1
03f6-03f6 : ata_piix
03f7-03f7 : floppy
03f8-03ff : serial
0505-0505 : QEMU0001:00
0510-051b : QEMU0002:00
0510-051b : fw_cfg_io
0600-063f : 0000:00:01.3
0600-0603 : ACPI PM1a_EVT_BLK
0604-0605 : ACPI PM1a_CNT_BLK
0608-060b : ACPI PM_TMR
0700-070f : 0000:00:01.3
0700-0708 : piix4_smbus
0cf8-0cff : PCI conf1
0d00-adff : PCI Bus 0000:00
ae0f-aeff : PCI Bus 0000:00
af20-afdf : PCI Bus 0000:00
afe0-afe3 : ACPI GPE0_BLK
afe4-ffff : PCI Bus 0000:00
c000-c03f : 0000:00:05.0
c000-c03f : virtio-pci-legacy
c040-c05f : 0000:00:01.2
c040-c05f : uhci_hcd
c060-c07f : 0000:00:03.0
c060-c07f : virtio-pci-legacy
c080-c09f : 0000:00:04.0
c080-c09f : virtio-pci-legacy
c0a0-c0bf : 0000:00:06.0
c0a0-c0bf : virtio-pci-legacy
c0c0-c0cf : 0000:00:01.1
c0c0-c0cf : ata_piix

七、PCI 中断机制

PCI 设备有两种打中断的方法:传统的 INTx 中断与 MSI 中断,出于兼容的需要 PCIe 完全继承了这个特性

I. INTx 中断

INTx 类型的中断即传统的通过中断引脚来产生的中断,PCI 总线使用 INTA#INTB#INTC#INTD# 信号(低电平有效)向处理器发出中断请求,不过多数设备仅使用 INTA# 信号

下图为一个产生 INTA# 中断信号的流程:

  • 设备向南桥上的中断控制器打一个 INTA# ,中断控制器转为 INTR 信号后通过 APIC bus 打向处理器
  • 接受中断信号的处理器(未设置则默认都打到 CPU0)通过中断向量表执行对应的处理程序

image.png

在 PCI 总线中,设备的 INTx 引脚最终要连接到中断控制器的 IRQ 引脚 ,下图是一个三 PCI 插槽与中断控制器引脚进行连接的例子:

知乎偷的图

还记得我们前文所讲的 PCI 配置空间中的 Interrupt PinInterrupt Line 域吗?现在我们可以进一步明确其具体用途了:

  • Interrupt Pin:记录设备应该使用哪一个 INTx 中断信号
  • Interrupt Line:记录设备连接的引脚

image.png

II. MSI/MSI-X 中断

Message Signaled Interrupt 是一种更为现代化与普遍的 PCI 中断机制,MSI-eXtend 则为其升级版,该机制的引入是为了消除 INTx 的边带信号,目前绝大多数 PCIe 设备已不再使用传统的 INTx 中断,而是使用 MSI/MSI-X 提交中断请求

在 PCIe 设备中有着两个 Capability 结构,分别对应 MSI 与 MSI-X,通常一个 PCIe 设备仅会包含其中一个。对于 MSI 而言其 Capability ID 为 5,一共有四种结构,分别对应 32 位与 64 位的 Message 结构,以及对应的带上中断 Masking 的结构

知乎偷的图

MSI/MSI-X 本质上是通过向特定的内存区域进行写入来达到中断触发的效果,当 PCI 设备提交请求时,其向 MSI/MSI-x Capability 结构中的 Message Address 地址(PCI总线域)写入 Message Data 数据,从而产生一个存储器写 TLP,由此向处理器提交存储器写请求

MSI 仅支持 32 个连续的中断向量,而 MSI-X 支持 2048 个非连续的中断向量,但 MSI-X 的中断向量信息并不像 MSI 那样直接存放在配置空间,而是存放在 MMIO 空间中,通过BIR(Base address Indicator Register)与 BAR 来确定其在 MMIO 中的具体位置

image.png

image.png

其结构如下图所示:

image.png

八、Transaction Layer Package

上一节我们提到了一个词叫 TLP,这一节我们简要介绍一下这是一个什么东西

我们首先需要介绍 PCI 设备底层的通信结构,类似于计网的 OSI 七层模型,PCI 总线也可以由下到上划分为 物理层(Physical Layer)、数据链路层(Data Link Layer)、事务层(Transaction Layer),TLP 即 Transaction Layer Package:在事务层进行传输的数据包

image.png

0x02. Linux PCI 驱动编写(🕊)

有的时候你可能自己手工糊了一个 PCI 设备(?),万分欢喜地想要直接往自家💻的 PCI 插槽上一插就开用了,但是突然发现并没有一种万能的 PCI 驱动能够直接适配你自己造的 PCI 设备,那这个时候我们只好自己动手写一个驱动了:)

〇、QEMU PCI 设备模拟

因为笔者确实没有条件手搓一个 PCI 设备,所以这里只好用 QEMU 来模拟一个,笔者这里实现了一个通过 DMA 提供简单的数据异或功能的 PCI 设备

关于最基础的 QEMU 设备编写、QOM 等,参见这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
/* 
* arttnba3 PCI test device
*
* Copyright (c) 2022 arttnba3
* Author: arttnba3 <arttnba@gmail.com>
*
* This programme is just a simple pci device,
* which is cerated for my own learning about qemu.
* You can modify and use it freely.
*/

#include "qemu/osdep.h"
#include "hw/pci/pci.h"
#include "hw/qdev-properties.h"
#include "qemu/event_notifier.h"
#include "qemu/module.h"
#include "sysemu/kvm.h"
#include "qom/object.h"

enum {
A3DEV_STATUS_INIT = 0,
A3DEV_STATUS_READY,
A3DEV_STATUS_RUNNING,
A3DEV_STATUS_STOPPING,

A3DEV_STATUS_TYPES,
};

enum {
A3DEV_REGS_STATUS = 0,
A3DEV_REGS_INSN,

A3DEV_REGS_TYPES,
};

enum {
A3DEV_INSN_START = 0,
A3DEV_INSN_STOP,

A3DEV_INSN_TYPES,
};

typedef struct A3EncBufferInfo {
/*< public >*/
dma_addr_t addr;
uint8_t val;
uint32_t len;
} A3EncBufferInfo;

typedef struct A3PCIDevState {
/*< private >*/
PCIDevice parent_obj;

/*< public >*/
MemoryRegion mmio;
MemoryRegion pmio;
uint64_t regs[A3DEV_REGS_TYPES];
A3EncBufferInfo enc_buf;

QemuThread thread;
QemuMutex lock;
} A3PCIDevState;

typedef struct A3PCIDevClass {
/*< private >*/
PCIDeviceClass parent;
} A3PCIDevClass;

#define TYPE_A3DEV_PCI "a3dev-pci"
#define A3DEV_PCI(obj) \
OBJECT_CHECK(A3PCIDevState, (obj), TYPE_A3DEV_PCI)
#define A3DEV_PCI_GET_CLASS(obj) \
OBJECT_GET_CLASS(A3PCIDevClass, obj, TYPE_A3DEV_PCI)
#define A3DEV_PCI_CLASS(klass) \
OBJECT_CLASS_CHECK(A3PCIDevClass, klass, TYPE_A3DEV_PCI)

static void *a3dev_worker_thread(void *arg)
{
A3PCIDevState *ds = A3DEV_PCI(arg);
uint8_t cb;

for (uint32_t wlen = 0; wlen < ds->enc_buf.len; wlen++) {
if (ds->regs[A3DEV_REGS_STATUS] != A3DEV_STATUS_STOPPING) {
break;
}
pci_dma_read(&ds->parent_obj, ds->enc_buf.addr, &cb, 1);
cb ^= ds->enc_buf.val;
pci_dma_write(&ds->parent_obj, ds->enc_buf.addr, &cb, 1);
}

ds->regs[A3DEV_REGS_STATUS] = A3DEV_STATUS_READY;

return NULL;
}

static uint64_t
a3dev_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
A3PCIDevState *ds = A3DEV_PCI(opaque);

return *(uint64_t*)(((uint8_t*) &ds->enc_buf) + addr);
}

static void
a3dev_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
A3PCIDevState *ds = A3DEV_PCI(opaque);

if (ds->regs[A3DEV_REGS_STATUS] != A3DEV_STATUS_READY) {
return ;
}

switch (size) {
case 1:
*(uint8_t*)(((uint8_t*) &ds->enc_buf ) + addr) = val;
break;
case 2:
*(uint16_t*)(((uint8_t*) &ds->enc_buf ) + addr) = val;
break;
case 4:
*(uint32_t*)(((uint8_t*) &ds->enc_buf ) + addr) = val;
break;
case 8:
*(uint64_t*)(((uint8_t*) &ds->enc_buf ) + addr) = val;
break;
default:
break;
}
}

static uint64_t
a3dev_pmio_read(void *opaque, hwaddr addr, unsigned size)
{
A3PCIDevState *ds = A3DEV_PCI(opaque);

switch (addr) {
case A3DEV_REGS_STATUS:
return ds->regs[A3DEV_REGS_STATUS];
default:
return -1;
}
}

static void
a3dev_pmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
A3PCIDevState *ds = A3DEV_PCI(opaque);

qemu_mutex_lock(&ds->lock);

switch (addr) {
case A3DEV_REGS_INSN:
switch (val) {
case A3DEV_INSN_START:
if (ds->regs[A3DEV_REGS_STATUS] != A3DEV_STATUS_READY) {
break;
}
ds->regs[A3DEV_REGS_STATUS] = A3DEV_STATUS_RUNNING;
qemu_thread_create(&ds->thread, "a3dev-worker-thread",
a3dev_worker_thread, ds,
QEMU_THREAD_DETACHED);
break;
case A3DEV_INSN_STOP:
if (ds->regs[A3DEV_REGS_STATUS] != A3DEV_STATUS_RUNNING) {
break;
}
ds->regs[A3DEV_REGS_STATUS] = A3DEV_STATUS_STOPPING;
break;
default:
break;
}
break;
default:
break;
}

qemu_mutex_unlock(&ds->lock);
}

static const MemoryRegionOps a3dev_mmio_ops = {
.read = a3dev_mmio_read,
.write = a3dev_mmio_write,
.endianness = DEVICE_LITTLE_ENDIAN,
.valid = {
.max_access_size = 4,
.min_access_size = 1,
.unaligned = true,
},
.impl = {
.unaligned = true,
},
};

static const MemoryRegionOps a3dev_pmio_ops = {
.read = a3dev_pmio_read,
.write = a3dev_pmio_write,
.endianness = DEVICE_LITTLE_ENDIAN,
.valid = {
.max_access_size = 4,
.min_access_size = 1,
.unaligned = true,
},
.impl = {
.unaligned = true,
},
};

static void a3dev_pci_realize(PCIDevice *pci_dev, Error **errp)
{
A3PCIDevState *ds = A3DEV_PCI(pci_dev);

ds->regs[A3DEV_REGS_STATUS] = A3DEV_STATUS_INIT;

memory_region_init_io(&ds->mmio, OBJECT(ds), &a3dev_mmio_ops,
pci_dev, "a3dev-mmio", sizeof(ds->enc_buf));
pci_register_bar(pci_dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &ds->mmio);
memory_region_init_io(&ds->pmio, OBJECT(ds), &a3dev_pmio_ops,
pci_dev, "a3dev-pmio", A3DEV_REGS_TYPES);
pci_register_bar(pci_dev, 1, PCI_BASE_ADDRESS_SPACE_IO, &ds->pmio);

memset(&ds->enc_buf, 0, sizeof(ds->enc_buf));
qemu_mutex_init(&ds->lock);

ds->regs[A3DEV_REGS_STATUS] = A3DEV_STATUS_READY;
}

static void a3dev_instance_init(Object *obj)
{
// do something
}

static void a3dev_class_init(ObjectClass *oc, void *data)
{
DeviceClass *dc = DEVICE_CLASS(oc);
PCIDeviceClass *pci = PCI_DEVICE_CLASS(oc);

pci->realize = a3dev_pci_realize;
pci->vendor_id = PCI_VENDOR_ID_QEMU;
pci->device_id = 0x1919;
pci->revision = 0x81;
pci->class_id = PCI_CLASS_OTHERS;

dc->desc = "arttnba3 test PCI device";
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}

static const TypeInfo a3dev_type_info = {
.name = TYPE_A3DEV_PCI,
.parent = TYPE_PCI_DEVICE,
.instance_init = a3dev_instance_init,
.instance_size = sizeof(A3PCIDevState),
.class_size = sizeof(A3PCIDevClass),
.class_init = a3dev_class_init,
.interfaces = (InterfaceInfo[]) {
{ INTERFACE_CONVENTIONAL_PCI_DEVICE },
{ },
},
};

static void a3dev_register_types(void) {
type_register_static(&a3dev_type_info);
}

type_init(a3dev_register_types);

一、kernel 识别 PCI 设备的方式

I.基本结构

我们首先来看 Linux kernel 中的通用设备驱动模型,主要由三部分组成:总线(bus)、设备(device)、驱动(driver),具体的总线类型都是基于这一套机制去实现的

偷的图

对于 PCI 而言,总线中的各个组件在 Linux kernel 中对应的结构体如下图所示:

偷的图

下面是一张更加详细的展开图:

偷的图

上面的图只画出了 PCI 的总线(struct pci_bus)与 PCI 设备(struct pci_dev),还少了一个驱动结构,在内核中 PCI 驱动对应的实际上是 pci_driver 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct pci_driver {
struct list_head node;
const char *name;
const struct pci_device_id *id_table; /* Must be non-NULL for probe to be called */
int (*probe)(struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */
void (*remove)(struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */
int (*suspend)(struct pci_dev *dev, pm_message_t state); /* Device suspended */
int (*resume)(struct pci_dev *dev); /* Device woken up */
void (*shutdown)(struct pci_dev *dev);
int (*sriov_configure)(struct pci_dev *dev, int num_vfs); /* On PF */
int (*sriov_set_msix_vec_count)(struct pci_dev *vf, int msix_vec_count); /* On PF */
u32 (*sriov_get_vf_total_msix)(struct pci_dev *pf);
const struct pci_error_handlers *err_handler;
const struct attribute_group **groups;
const struct attribute_group **dev_groups;
struct device_driver driver;
struct pci_dynids dynids;
bool driver_managed_dma;
};

II.识别过程

讲完了基本结构,现在我们可以来看内核是怎么去识别 PCI 设备的了,在内核启动后各架构的初始化函数最终都会调用到 start_kernel(),于是存在如下调用链:

1
2
3
4
5
6
7
8
9
start_kernel()
arch_call_rest_init()
rest_init() // 启动三个进程 idle(0)、kernel_init(1)、kthreadd(2)
kernel_init()
kernel_init_freeable()
do_basic_setup()
driver_init()
devtmpfs_init() // 建立 devtmpfs,之后会被用户态init挂到/dev下面
buses_init() // 在 sysfs 根下建立 bus 目录

之后就是到各个模块的 init 函数,按照编译链接顺序,我们所关心的 PCI 相关函数的执行顺序应当如下:

1
2
3
4
5
6
7
pcibus_class_init()	// 注册 pci_bus class,创建 sysfs 下的 class/pci_bus 目录

pci_driver_init() // 注册 pci_bus_type,创建 sysfs 下的 bus/pci 目录

acpi_pci_init() // 注册 acpi_pci_bus,并设置电源管理的相应操作

acpi_init() // pcie 初始化入口,进行设备识别与模型建立

这里我们挑其中关键的几个来看,首先是 acpi_init(),存在如下调用路径:

前置知识:ACPI 规范

1
2
3
4
5
6
7
8
9
acpi_init()
pci_mmcfg_late_init() // 扫描 MCFG 表,获取所有设备PCI配置空间的基地址
acpi_scan_init()
acpi_pci_root_init()
acpi_scan_add_handler_with_hotplug() // 添加 handler:pci_root_handler
acpi_bus_scan() // 设备扫描,创建 ACPI 设备节点对象
acpi_bus_attach() // 处理单个节点并调用 acpi_bus_attach() 处理子节点(DFS)
acpi_scan_attach_handler() // 查找匹配的 handler 并调用 attach 指针
handler->attach(device, devid)

那么现在我们来看对应的 handler,为在 acpi_scan_add_handler_with_hotplug() 中注册的 pci_root_handler ,该变量定义于 /drivers/acpi/pci_host.c 中:

1
2
3
4
5
6
7
8
9
static struct acpi_scan_handler pci_root_handler = {
.ids = root_device_ids,
.attach = acpi_pci_root_add,
.detach = acpi_pci_root_remove,
.hotplug = {
.enabled = true,
.scan_dependent = acpi_pci_root_scan_dependent,
},
};

于是最后调用到 acpi_pci_root_add(),为设备节点创建对应的内核结构体

这个函数中间其实还有一些过程,但是👴摸了

接下来我们来看 pci_driver_init(),该函数在内核驱动模型中注册了 PCI 总线,并定义了相关的操作函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int __init pci_driver_init(void)
{
int ret;

ret = bus_register(&pci_bus_type);
if (ret)
return ret;

#ifdef CONFIG_PCIEPORTBUS
ret = bus_register(&pcie_port_bus_type);
if (ret)
return ret;
#endif
dma_debug_add_bus(&pci_bus_type);
return 0;
}
postcore_initcall(pci_driver_init);

该函数中调用了 bus_register() 来注册 PCI 总线,对应到符合内核设备驱动模型的总线类型的变量为 pci_bus_type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct bus_type pci_bus_type = {
.name = "pci",
.match = pci_bus_match,
.uevent = pci_uevent,
.probe = pci_device_probe,
.remove = pci_device_remove,
.shutdown = pci_device_shutdown,
.dev_groups = pci_dev_groups,
.bus_groups = pci_bus_groups,
.drv_groups = pci_drv_groups,
.pm = PCI_PM_OPS_PTR,
.num_vf = pci_bus_num_vf,
.dma_configure = pci_dma_configure,
.dma_cleanup = pci_dma_cleanup,
};
EXPORT_SYMBOL(pci_bus_type);

二、设备驱动框架

三、PCI probe - 设备识别

四、PCI remove - 设备移除


【HARDWARE.0x00】PCI 设备简易食用手册
https://arttnba3.github.io/2022/08/30/HARDWARE-0X00-PCI_DEVICE/
作者
arttnba3
发布于
2022年8月30日
许可协议