0%

【设备驱动】PCI 设备驱动程序(二)MSI 中断

传统上,外设向 CPU 发送中断请求时,会通过物理总线(PCB上的丝印)向中断控制器发送电信号,中断控制器再进一步将中断信号发送到 CPU,这也是通过物理上与 CPU 引脚连接实现的。这就带来了一个问题,CPU 或 中断控制器的物理引脚总是有限的,设备却是无限的,想要让每个设备都物理上连接到中断控制器是不现实的,所以必须通过中断线复用/共享的方式来实现上述要求。在中断共享的情况下,CPU 收到一个中断信号,需要尝试运行所有共享该中断号的 ISR,这必然带来了性能上的损耗。而 MSI (Message Signaled Interrupt)中断通过基于消息的方式传递中断,从而避免了上述问题。

什么是 MSI

MSI 消息信号中断,通过使用消息信号而不是硬件中断引脚来通知处理器有中断请求。本质上,消息就是设备向一个特定地址的写入的特定数据

  • 当设备需要中断时,它会生成一个消息,通常是向特定内存地址写入一个特定的数据值。
  • 处理器和中断控制器监控这些特定内存地址,当检测到写入操作时,会将其解释为中断请求。

MSI 功能最初在 PCI 2.2 中指定,后来在 PCI 3.0 中得到增强,以允许每个中断单独被屏蔽。MSI-X 功能也随 PCI 3.0 引入。它每个设备支持的中断比 MSI 更多,并允许中断被独立配置

设备可能同时支持 MSI 和 MSI-X,但一次只能启用其中一个。

使用 MSI 的好处

与传统基于引脚的重点,基于消息信号的 MSI/MSI-X 中断有如下优点:

  • 无需共享:基于引脚的 PCI 中断通常在多个设备之间共享。为支持这一点,内核必须调用与每个中断相关联的中断处理程序,这导致整个系统的性能降低。MSI 从不共享,因此不会出现此问题。

  • 更多中断 :通常 PCI 只有一个中断引脚,但是该设备往往可能会产生多种事件,使用基于引脚的中的时,通常的做法是在中断服务程序中再次查询设备寄存器来确定设备事件,这无疑减慢了中断处理速度。使用 MSI 中断,可以为每种事件设置不同的中断(不同消息)和中断处理函数,换句话说,设备可以支持更多中断,允许每个中断专门用于不同的目的。

  • 数据一致性 :在基于引脚的中断中,设备通过硬件中断引脚向处理器发送中断信号。然而,存在一个潜在的问题:设备在写入数据到内存后,立即触发中断信号,可能导致中断信号在所有数据完全写入内存之前到达处理器。这种情况在设备通过 PCI-PCI 桥连接时尤为明显,因为桥接设备可能引入额外的延迟。(简单描述就是,中断信号传递的速度快于数据写入内存速度)。对于 MSI 中断来说,不存在这个问题,因为中断信号也是写入内存的,这时只要保证数据和中断消息的写入顺序即可。

使用 MSI

PCI 设备被初始化为使用基于引脚的中断。设备驱动程序必须设置设备以使用 MSI 或 MSI-X。并非所有机器都能正确支持 MSI,对于那些机器,下面描述的 API 将简单地失败,设备将继续使用基于引脚的中断。

内核配置

要支持 MSI 或 MSI-X,内核必须在启用 CONFIG_PCI_MSI 选项的情况下构建。此选项仅在某些架构上可用,并且可能还取决于其他一些选项的设置。例如,在 x86 上,您还必须启用 X86_UP_APICSMP 才能看到 CONFIG_PCI_MSI 选项。

分配和释放中断向量

大部分的硬件工作是在 PCI 层为驱动程序完成的。驱动程序只需要请求 PCI 层为该设备设置 MSI 功能。

若要自动使用 MSI 或 MSI-X 中断向量,请使用以下函数,该函数为设备分配多个中断号/向量:

1
2
3
/* 成功时返回分配的中断数量,失败返回负值 */
int pci_alloc_irq_vectors(struct pci_dev *dev, unsigned int min_vecs,
unsigned int max_vecs, unsigned int flags);
  • dev:内核中,PCI 设备的抽象
  • min_vecs:请求申请的中断向量的数量下限
  • max_vecs:请求申请的中断向量的数量上限
  • flags:指定设备和驱动程序可以使用哪种类型的中断(PCI_IRQ_LEGACYPCI_IRQ_MSIPCI_IRQ_MSIX)。一个方便的快捷方式 PCI_IRQ_ALL_TYPES 可用于请求任何可能的中断类型。

有获取就应该有分配,在设备 remove 时,应该释放申请的中断:

1
void pci_free_irq_vectors(struct pci_dev *dev);

如果一个设备同时支持 MSI-X 和 MSI 功能,上述 API 将优先使用 MSI-X 设施而非 MSI 设施。MSI-X 支持 1 到 2048 之间的任意数量的中断。相比之下,MSI 被限制为最多 32 个中断(并且必须是 2 的幂)。此外,MSI 中断向量必须连续分配,因此系统可能无法为 MSI 分配与为 MSI-X 一样多的向量。

在某些平台上,MSI 中断必须全部针对同一组 CPU,而 MSI-X 中断可以全部针对不同的 CPU。

如果一个设备既不支持 MSI-X 也不支持 MSI,它将回退到单个传统 IRQ 向量。

尽可能多的分配方式

1
2
3
nvec = pci_alloc_irq_vectors(pdev, 1, nvec, PCI_IRQ_ALL_TYPES)
if (nvec < 0)
goto out_err;

MSI 或 MSI-X 中断的典型用法是分配尽可能多的向量,可能达到设备支持的极限。如果 nvec 大于设备支持的数量,它将自动被限制为支持的极限,因此无需事先查询支持的向量数量。

固定数量的分配方式

1
2
3
ret = pci_alloc_irq_vectors(pdev, nvec, nvec, PCI_IRQ_ALL_TYPES);
if (ret < 0)
goto out_err;

如果不想处理可变数量的中断,那么直接使用上面这种方式即可,nvec 为固定的中断数量。

唯一中断的分配方式

1
2
3
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_ALL_TYPES);
if (ret < 0)
goto out_err;

很简单,就是只申请 1 个中断向量。

只申请 MSI/MSI-X 中断

1
2
3
nvec = pci_alloc_irq_vectors(pdev, 1, nvec, PCI_IRQ_MSI | PCI_IRQ_MSIX);
if (nvec < 0)
goto out_err;

某些设备可能不支持使用传统的线路中断,在这种情况下,驱动程序可以指定仅 MSI 或 MSI-X。

MSI 中断向量转换到 Linux IRQ

要获取传递给 request_irq()free_irq() 的 Linux IRQ 编号以及向量,要使用以下函数:

1
int pci_irq_vector(struct pci_dev *dev, unsigned int nr);

废弃接口

下面这些接口会在老代码中看到,但务必不要继续使用:

1
2
3
4
5
pci_enable_msi()              /* deprecated */
pci_disable_msi() /* deprecated */
pci_enable_msix_range() /* deprecated */
pci_enable_msix_exact() /* deprecated */
pci_disable_msix() /* deprecated */

驱动编写

模块入口和出口

1
2
3
4
5
6
7
8
9
10
11
static int __init pci_init(void)
{
return pci_register_driver(&pci_drv);
}

static void __exit pci_exit(void)
{
pci_unregister_driver(&pci_drv);
}
module_init(pci_init);
module_exit(pci_exit);

pci_driver 驱动对象

1
2
3
4
5
6
static struct pci_driver pci_drv = {
.name = MODULE_NAME,
.id_table = ids,
.probe = pci_probe,
.remove = pci_remove,
};

id_table 设备匹配表

1
2
3
4
5
6
7
8
static struct pci_device_id ids[] = {
{
.vendor = PCI_VENDOR_ID_QEMU,
.device = PCI_DEVICE_ID_QEMU,
},
{ 0 },
};
MODULE_DEVICE_TABLE(pci, ids);

pci 私有设备对象

保存了驱动程序会经常使用的一些成员,如设备号,字符设备,pci_dev 等,还对设备的 BAR 空间,irq 等进行保存:

1
2
3
4
5
6
7
8
9
10
struct pci_private_t {
dev_t devt;
struct cdev cdev;
struct pci_dev *pdev;
uint32_t irq_nr;
uint32_t irqs[PCI_VEC_NUMBER];
uint32_t bar_nr;
void *__iomem bars[PCI_BAR_NUMBER];
uint32_t barlens[PCI_BAR_NUMBER];
};

probe 探测设备

使能 PCI 设备

按照 PCI 设备驱动的编写顺序,首先使能设备:

1
2
3
4
5
retval = pci_enable_device(pdev);
if (retval < 0) {
dev_err(&pdev->dev, "can't enable PCI device\n");
return -ENODEV;
}

为 MMIO 预留空间

内存(MMIO)和 I/O 端口地址不应直接从 PCI 设备配置空间读取。应使用 pci_dev 结构中的值,因为 PCI “总线地址” 可能已被特定于架构/芯片组的内核支持重映射为 “主机物理” 地址。

1
2
3
4
5
retval = pci_request_regions(pdev, MODULE_NAME);
if (retval < 0) {
dev_err(&pdev->dev, "pci_request_regions failed\n");
return retval;
}

为设备数据申请空间

1
2
3
4
5
6
pci_prv = kzalloc(sizeof(struct pci_private_t), GFP_KERNEL);
if (!pci_prv) {
dev_err(&pdev->dev, "can't alloc memory for pci device\n");
return -ENOMEM;
}
pci_prv->pdev = pdev;

申请 MSI 中断

1
2
3
retval = irq_alloc(pci_prv);
if (retval < 0)
goto irq_alloc_err;

具体实现都在 irq_alloc 函数中,该函数首先使用上面提到的接口 pci_alloc_irq_vectors 申请 MSI 中断向量,然后将 MSI 向量转换成 Linux IRQ 号,并保存到设备数据中,如下:

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
static int irq_alloc(struct pci_private_t *pci_prv)
{
int retval = 0, i;
int nvec, irq;
struct pci_dev *pdev = pci_prv->pdev;

nvec = pci_alloc_irq_vectors(pdev, 1, PCI_VEC_NUMBER, PCI_IRQ_MSI | PCI_IRQ_MSIX);
if (nvec < 0) {
dev_err(&pdev->dev, "pci_alloc_irq_vectors failed\n");
retval = nvec;
goto exit_err;
}

for (i = 0; i < nvec; i++) {
irq = pci_irq_vector(pdev, i);
/* 使用 pci_prv->irqs 的地址作为释放时的 cookie */
retval = request_irq(irq, default_pci_isr, 0, MODULE_NAME, pci_prv->irqs + i);
if (retval < 0) {
dev_err(&pci_prv->pdev->dev, "request_irq failed\n");
goto req_irq_err;
}
pci_prv->irqs[i] = irq;
pci_prv->irq_nr++;
}

return 0;

req_irq_err:
irq_free(pci_prv);
exit_err:
return retval;
}

相应的,释放 IRQ 的函数如下:

1
2
3
4
5
6
7
8
9
10
static void irq_free(struct pci_private_t *pci_prv)
{
int i;

for (i = 0; i < pci_prv->irq_nr; ++i) {
if (pci_prv->irqs[i])
free_irq(pci_prv->irqs[i], pci_prv->irqs + i);
}
pci_free_irq_vectors(pci_prv->pdev);
}

中断处理函数如下,只是简单打印:

1
2
3
4
5
6
7
8
static irqreturn_t default_pci_isr(int irq, void *dev_id)
{
struct pci_private_t *pci_prv = dev_id;
(void)pci_prv;

pr_debug("pci interrupt triggered!\n");
return IRQ_HANDLED;
}

映射 BAR 空间

这部分在本系列文章已经讲述过,这里不在赘述,直接看代码:

1
2
3
4
5
retval = map_bars(pci_prv);
if (retval < 0) {
dev_err(&pdev->dev, "can't map bars\n");
goto map_bars_err;
}

其主要的实现也封装到了函数 map_bars 中,具体实现如下:

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
static int map_sigal_bar(struct pci_private_t *pci_prv, int idx)
{
struct pci_dev *pdev = pci_prv->pdev;
resource_size_t bar_start;
resource_size_t bar_len;
void *__iomem bar_map;

bar_start = pci_resource_start(pdev, idx);
bar_len = pci_resource_len(pdev, idx);

if (bar_len == 0)
return 0;

bar_map = pci_iomap(pdev, idx, bar_len);

pr_info("BAR%d at 0x%llx mapped at 0x%p, length=%llu(0x%llx)\n", idx, (u64)bar_start, \
bar_map, (u64)bar_len, (u64)bar_len);

pci_prv->bars[pci_prv->bar_nr++] = bar_map;
pci_prv->barlens[pci_prv->bar_nr] = bar_len;

return (int)bar_len;
}

static int map_bars(struct pci_private_t *pci_prv)
{
int retval = 0;
int i;

for (i = 0; i < PCI_BAR_NUMBER; ++i) {
retval = map_sigal_bar(pci_prv, i);
if (retval == 0) {
continue;
} else if (retval < 0) {
retval = -EINVAL;
goto fail;
}
}
fail:
unmap_bars(pci_prv);
return retval;
}

相应的,也有 unmap_bars

1
2
3
4
5
6
7
static void unmap_bars(struct pci_private_t *pci_prv)
{
int i;

for (i = 0; i < pci_prv->bar_nr; i++)
pci_iounmap(pci_prv->pdev, pci_prv->bars[i]);
}

创建字符设备

1
2
3
4
5
retval = create_cdev(pci_prv);
if (retval < 0) {
dev_err(&pdev->dev, "can't create cdev\n");
goto creat_cdev_err;
}

看具体实现,比较常规:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int create_cdev(struct pci_private_t *pci_prv)
{
int retval = 0;

retval = alloc_chrdev_region(&pci_prv->devt, 0, 1, MODULE_NAME);
if (retval < 0)
return retval;

cdev_init(&pci_prv->cdev, &pci_fops);
retval = cdev_add(&pci_prv->cdev, pci_prv->devt, 1);
if (retval < 0)
goto cdev_add_err;

cdev_add_err:
unregister_chrdev_region(pci_prv->devt, 1);
return retval;
}

remove 移除设备

按照和 probe 相反的顺序释放设备占用的资源:

1
2
3
4
5
6
7
8
9
10
static void pci_remove(struct pci_dev *pdev)
{
struct pci_private_t *pci_prv = dev_get_drvdata(&pdev->dev);

destory_cdev(pci_prv);
unmap_bars(pci_prv);
irq_free(pci_prv);
pci_release_regions(pdev);
kfree(pci_prv);
}

完整的驱动代码

和上一节的代码相同,这里直接放出所有细节,内核版本是 5.10 :

头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef __MAIN_H
#define __MAIN_H

#define MODULE_NAME "pci_skel"
#define PCI_VENDOR_ID_QEMU 0x1234
#define PCI_DEVICE_ID_QEMU 0x7863

#define PCI_BAR_NUMBER 6
#define PCI_VEC_NUMBER 8

struct pci_private_t {
dev_t devt;
struct cdev cdev;
struct pci_dev *pdev;
uint32_t irq_nr;
uint32_t irqs[PCI_VEC_NUMBER];
uint32_t bar_nr;
void *__iomem bars[PCI_BAR_NUMBER];
uint32_t barlens[PCI_BAR_NUMBER];
};

#endif /* __MAIN_H */

源文件

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
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/irq.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/types.h>

#include "main.h"

static int pci_open(struct inode *node, struct file *filp)
{
dev_t devt = node->i_rdev;
struct pci_private_t *pci_prv = container_of(&devt, struct pci_private_t, devt);

iowrite32(1, pci_prv->bars[0]);

pr_err("%s", __FUNCTION__);
return 0;
}

static int pci_release(struct inode *node, struct file *filp)
{
pr_err("%s() is invoked\n", __FUNCTION__);
return 0;
}


static struct file_operations pci_fops = {
.owner = THIS_MODULE,
.open = pci_open,
.release = pci_release,
};

static struct pci_device_id ids[] = {
{
.vendor = PCI_VENDOR_ID_QEMU,
.device = PCI_DEVICE_ID_QEMU,
},
{ 0 },
};
MODULE_DEVICE_TABLE(pci, ids);

static int map_sigal_bar(struct pci_private_t *pci_prv, int idx)
{
struct pci_dev *pdev = pci_prv->pdev;
resource_size_t bar_start;
resource_size_t bar_len;
void *__iomem bar_map;

bar_start = pci_resource_start(pdev, idx);
bar_len = pci_resource_len(pdev, idx);

if (bar_len == 0)
return 0;

bar_map = pci_iomap(pdev, idx, bar_len);

pr_info("BAR%d at 0x%llx mapped at 0x%p, length=%llu(0x%llx)\n", idx, (u64)bar_start, \
bar_map, (u64)bar_len, (u64)bar_len);

pci_prv->bars[pci_prv->bar_nr++] = bar_map;
pci_prv->barlens[pci_prv->bar_nr] = bar_len;

return (int)bar_len;
}

static void unmap_bars(struct pci_private_t *pci_prv)
{
int i;

for (i = 0; i < pci_prv->bar_nr; i++)
pci_iounmap(pci_prv->pdev, pci_prv->bars[i]);
}

static int map_bars(struct pci_private_t *pci_prv)
{
int retval = 0;
int i;

for (i = 0; i < PCI_BAR_NUMBER; ++i) {
retval = map_sigal_bar(pci_prv, i);
if (retval == 0) {
continue;
} else if (retval < 0) {
retval = -EINVAL;
goto fail;
}
}
fail:
unmap_bars(pci_prv);
return retval;
}

static irqreturn_t default_pci_isr(int irq, void *dev_id)
{
struct pci_private_t *pci_prv = dev_id;
(void)pci_prv;

pr_debug("pci interrupt triggered!\n");
return IRQ_HANDLED;
}

static void irq_free(struct pci_private_t *pci_prv)
{
int i;

for (i = 0; i < pci_prv->irq_nr; ++i) {
if (pci_prv->irqs[i])
free_irq(pci_prv->irqs[i], pci_prv->irqs + i);
}
pci_free_irq_vectors(pci_prv->pdev);
}

static int irq_alloc(struct pci_private_t *pci_prv)
{
int retval = 0, i;
int nvec, irq;
struct pci_dev *pdev = pci_prv->pdev;

nvec = pci_alloc_irq_vectors(pdev, 1, PCI_VEC_NUMBER, PCI_IRQ_MSI | PCI_IRQ_MSIX);
if (nvec < 0) {
dev_err(&pdev->dev, "pci_alloc_irq_vectors failed\n");
retval = nvec;
goto exit_err;
}

for (i = 0; i < nvec; i++) {
irq = pci_irq_vector(pdev, i);
retval = request_irq(irq, default_pci_isr, 0, MODULE_NAME, pci_prv->irqs + i);
if (retval < 0) {
dev_err(&pci_prv->pdev->dev, "request_irq failed\n");
goto req_irq_err;
}
pci_prv->irqs[i] = irq;
pci_prv->irq_nr++;
}

return 0;

req_irq_err:
irq_free(pci_prv);
exit_err:
return retval;
}

static void destory_cdev(struct pci_private_t *pci_prv)
{
cdev_del(&pci_prv->cdev);
unregister_chrdev_region(pci_prv->devt, 1);
}

static int create_cdev(struct pci_private_t *pci_prv)
{
int retval = 0;

retval = alloc_chrdev_region(&pci_prv->devt, 0, 1, MODULE_NAME);
if (retval < 0)
return retval;

cdev_init(&pci_prv->cdev, &pci_fops);
retval = cdev_add(&pci_prv->cdev, pci_prv->devt, 1);
if (retval < 0)
goto cdev_add_err;

cdev_add_err:
unregister_chrdev_region(pci_prv->devt, 1);
return retval;
}

static int pci_probe(struct pci_dev *pdev, const struct pci_device_id *ids){
uint8_t reversion;
int retval = 0;
struct pci_private_t *pci_prv;

pr_debug("New pci device probing\n");

retval = pci_enable_device(pdev);
if (retval < 0) {
dev_err(&pdev->dev, "can't enable PCI device\n");
return -ENODEV;
}

retval = pci_request_regions(pdev, MODULE_NAME);
if (retval < 0) {
dev_err(&pdev->dev, "pci_request_regions failed\n");
return retval;
}

pci_read_config_byte(pdev, PCI_REVISION_ID, &reversion);
pr_debug("Revision: %d\n", reversion);

pci_prv = kzalloc(sizeof(struct pci_private_t), GFP_KERNEL);
if (!pci_prv) {
dev_err(&pdev->dev, "can't alloc memory for pci device\n");
return -ENOMEM;
}
pci_prv->pdev = pdev;

retval = irq_alloc(pci_prv);
if (retval < 0)
goto irq_alloc_err;

retval = map_bars(pci_prv);
if (retval < 0) {
dev_err(&pdev->dev, "can't map bars\n");
goto map_bars_err;
}

retval = create_cdev(pci_prv);
if (retval < 0) {
dev_err(&pdev->dev, "can't create cdev\n");
goto creat_cdev_err;
}
dev_set_drvdata(&pdev->dev, pci_prv);

return 0;

creat_cdev_err:
unmap_bars(pci_prv);
map_bars_err:
irq_free(pci_prv);
irq_alloc_err:
kfree(pci_prv);
pci_release_regions(pdev);
return retval;
}

static void pci_remove(struct pci_dev *pdev)
{
struct pci_private_t *pci_prv = dev_get_drvdata(&pdev->dev);

destory_cdev(pci_prv);
unmap_bars(pci_prv);
irq_free(pci_prv);
pci_release_regions(pdev);
kfree(pci_prv);
}

static struct pci_driver pci_drv = {
.name = MODULE_NAME,
.id_table = ids,
.probe = pci_probe,
.remove = pci_remove,
};

static int __init pci_init(void)
{
return pci_register_driver(&pci_drv);
}

static void __exit pci_exit(void)
{
pci_unregister_driver(&pci_drv);
}

module_init(pci_init);
module_exit(pci_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Huashan Sun <huashan.sun@qq.com>");