本文最后更新于:2023年10月27日 中午
👴等会把你总线都给扬了
0x00.一切开始之前 因为笔者最近不懂为什么开始写 PCI 设备驱动了,但笔者是 网络空间安全专业 的本科生,此前基本上没有接触过与硬件相关联的知识(因为计组和微原课讲的就是个🐓⑧),所以开了这篇新的博客简要记载一些与 PCI 设备相关的基础知识、基本 Linux PCI 驱动的编写等
为了写这篇博客,笔者翻了笔者大二上学期的计算机组成原理的课本,还翻了大二下学期的微机原理的课本,发现这两门课根本就是什么都没讲……因为也弄不到微院那边相关的教材,所以本篇博客 并没有一个系统性的指导 来辅助写作,都是各种东拼西凑+笔者自己的理解,很多东西因为笔者自身水平低下的缘故只能一笔带过 ,因此可能会显得不够专业,希望读者见谅XD
如果需要更为专业的参考资料请直接参考 《PCI_Express_Technology》
以及笔者非常深刻的意识到在硬件这一块的知识笔者相比于那些专门搞硬件的人而言**确实差了很多…..**只能说继续努力吧XD
0x01. PCI basic knowledge 一、总线结构简述 我们都知道计算机的五个基本组件为:输入,输出,存储器,运算器(或数据通路),控制器 。那么这几大组件之间怎么通信呢?答案是依靠系统总线
总线 (bus)是一种将多个功能单元进行连接并允许功能单元之间进行数据交换的一种数据通路,在现代计算机中通常采用总线结构,即存在一根主要的公共通信干线,CPU 及各种设备都通过这跟总线进行通信
总线按功能可以分为以下三种类型:
片内总线 :芯片内的总线,位于 CPU 内部,用以在寄存器与寄存器、寄存器与 ALU 之间进行数据交换
系统总线 :计算机系统内各功能单元(CPU、主存、I/O)之间的公共通信干线,也称之为 内总线
通信总线 :用于计算机系统之间或是计算机系统与其他系统(例如远程通信设备)之间进行通信的总线,也称之为 外总线
总线是可以扩展的,即可以存在多个不同类型的总线相连,不同的设备接入到不同类型的总线上
二、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 总线树:
由此,一个多层 PCI 总线结构如下图所示:
我们来看一个现实中的经典例子,以下图的 Intel 440FX
芯片组为例,PCI Host Bridge 分隔开了存储器域与 PCI 设备域,其分别使用独立的地址空间 :
在 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 设备的配置寄存器:
CF8
:CONFIG_ADDRESS ,即 PCI 配置空间地址端口。
CFH
:CONFIG_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 设备分为 Bridge
与 Agent
两类,故配置空间也分为相应的两类
Agent 类型配置空间又被称为 Type 00h
,格式如下图所示:
相应地,Bridge 类型配置空间被称为 Type 01h
,与 Agent 类型配置空间大同小异:
简单介绍几个比较重要的字段:
设备标识相关:
Vendor ID
:生产厂商的 ID,例如 Intel 设备通常为 0x8086
Device ID
:具体设备的 ID,通常也是由厂家自行指定的
Class Code
:类代码,用于区分设备类型
Revision ID
:PCI 设备的版本号,可以看作 Device ID 的扩展
设备状态相关:
设备配置相关:
前面我们讲到 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
PMIO
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 下通过 in
与 out
这一类的指令可以读写端口)。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)通过中断向量表执行对应的处理程序
在 PCI 总线中,设备的 INTx 引脚
最终要连接到中断控制器的 IRQ 引脚
,下图是一个三 PCI 插槽与中断控制器引脚进行连接的例子:
还记得我们前文所讲的 PCI 配置空间中的 Interrupt Pin
与 Interrupt Line
域吗?现在我们可以进一步明确其具体用途了:
Interrupt Pin
:记录设备应该使用哪一个 INTx 中断信号
Interrupt Line
:记录设备连接的引脚
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 中的具体位置
其结构如下图所示:
八、Transaction Layer Package 上一节我们提到了一个词叫 TLP
,这一节我们简要介绍一下这是一个什么东西
我们首先需要介绍 PCI 设备底层的通信结构,类似于计网的 OSI 七层模型,PCI 总线也可以由下到上划分为 物理层(Physical Layer)、数据链路层(Data Link Layer)、事务层(Transaction Layer) ,TLP 即 Transaction Layer Package :在事务层进行传输的数据包
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 #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 { dma_addr_t addr; uint8_t val; uint32_t len; } A3EncBufferInfo;typedef struct A3PCIDevState { PCIDevice parent_obj; MemoryRegion mmio; MemoryRegion pmio; uint64_t regs[A3DEV_REGS_TYPES]; A3EncBufferInfo enc_buf; QemuThread thread; QemuMutex lock; } A3PCIDevState;typedef struct A3PCIDevClass { 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) { }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 ; int (*probe)(struct pci_dev *dev, const struct pci_device_id *id); void (*remove)(struct pci_dev *dev); int (*suspend)(struct pci_dev *dev, pm_message_t state); int (*resume)(struct pci_dev *dev); void (*shutdown)(struct pci_dev *dev); int (*sriov_configure)(struct pci_dev *dev, int num_vfs); int (*sriov_set_msix_vec_count)(struct pci_dev *vf, int msix_vec_count); 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 () kernel_init () kernel_init_freeable () do_basic_setup () driver_init () devtmpfs_init () buses_init ()
之后就是到各个模块的 init 函数,按照编译链接顺序,我们所关心的 PCI 相关函数的执行顺序应当如下:
1 2 3 4 5 6 7 pcibus_class_init () ↓pci_driver_init () ↓acpi_pci_init () ↓acpi_init ()
这里我们挑其中关键的几个来看,首先是 acpi_init()
,存在如下调用路径:
前置知识:ACPI 规范
1 2 3 4 5 6 7 8 9 acpi_init () pci_mmcfg_late_init () acpi_scan_init () acpi_pci_root_init () acpi_scan_add_handler_with_hotplug () acpi_bus_scan () acpi_bus_attach () acpi_scan_attach_handler () 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 - 设备移除