QEMU EDU 设备驱动
QEMU EDU 设备是 QEMU 中用于设备驱动程序教学的设备。在马萨里克大学的 Linux 内核课程中,学生可以使用这个虚拟设备编写一个包含 I/O、IRQ、DMA 等的驱动程序。
设备规范:EDU device — QEMU documentation
设备源码:https://github.com/qemu/qemu/blob/master/hw/misc/edu.c
本文参考已有的项目重新从零实现 QEMU EDU 的驱动程序。参考项目:
- kokol16/EDU-driver: A Linux driver for Qemu’s device EDU
- ysan/qemu-edu-driver: Device driver for QEMU educational PCI device.
由于文章中的代码是由浅入深,不断叠加修改,因此若文中有不合理或疏忽的地方请参考最终的源码。
源码地址:https://github.com/jklincn/qemu_edu_driver
QEMU 启动
本文使用的 QEMU 版本为 8.2.5,QEMU 的安装以及磁盘镜像的准备可以参考[在 WSL 中使用 QEMU 搭建 PCIe 模拟环境]。
qemu-system-x86_64 -enable-kvm \
-M q35 \
-cpu SapphireRapids-v2 \
-smp 8 \
-m 16G \
-hda ubuntu.qcow2 \
-netdev user,id=net0,hostfwd=tcp::10022-:22 \
-device e1000,netdev=net0 \
-device edu
此处对网络设备的设置是为了可以使用 vscode 进行远程连接,方便开发。
进入系统后使用 lspci 应该可以看到 edu 设备,即 00:03.0 Unclassified device [00ff]: Device 1234:11e8 (rev 10)
$ lspci
00:00.0 Host bridge: Intel Corporation 82G33/G31/P35/P31 Express DRAM Controller
00:01.0 VGA compatible controller: Device 1234:1111 (rev 02)
00:02.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)
00:03.0 Unclassified device [00ff]: Device 1234:11e8 (rev 10)
00:1f.0 ISA bridge: Intel Corporation 82801IB (ICH9) LPC Interface Controller (rev 02)
00:1f.2 SATA controller: Intel Corporation 82801IR/IO/IH (ICH9R/DO/DH) 6 port SATA Controller [AHCI mode] (rev 02)
00:1f.3 SMBus: Intel Corporation 82801I (ICH9 Family) SMBus Controller (rev 02)
编写 PCI 驱动程序
这一部分可以先阅读 Linux 官方文档:1. How To Write Linux PCI Drivers — The Linux Kernel documentation。
简单驱动模板
先搭一个大体的框架,这和 PCI 无关,这是为了测试当前的环境配置,该文件命名为 qemu_edu_driver.c
#include <linux/module.h>
#define DRIVER_NAME "qemu_edu"
static int edu_init() {
printk(KERN_INFO "[%s] Init sucessful. \n", DRIVER_NAME);
return 0;
}
static void edu_exit() {
}
module_init(edu_init);
module_exit(edu_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("QEMU EDU Device Driver");
再创建一个 Makefile
obj-m := qemu_edu_driver.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
CFLAGS=-Wall
modules:
make -C $(KERNELDIR) M=$(PWD) modules
clean:
make -C $(KERNELDIR) M=$(PWD) clean
.PHONY: modules clean
安装编译工具与内核头文件
sudo apt install build-essential linux-headers-$(uname -r)
进行编译和加载
make
sudo insmod qemu_edu_driver.ko
sudo dmesg | grep qemu_edu
如果一切正常,可以看到有 [qemu_edu] Init sucessful. 这样的输出
注册驱动程序
这里主要涉及到 pci_register_driver() 函数,其接口是
/* Proper probing supporting hot-pluggable devices */
int __must_check __pci_register_driver(struct pci_driver *, struct module *,
const char *mod_name);
/* pci_register_driver() must be a macro so KBUILD_MODNAME can be expanded */
#define pci_register_driver(driver) \
__pci_register_driver(driver, THIS_MODULE, KBUILD_MODNAME)
因此我们只需要准备好 pci_driver 结构体
static struct pci_driver pci_driver = {
.name = DRIVER_NAME,
.id_table = pci_ids,
.probe = edu_probe,
.remove = edu_remove,
};
其中还涉及到 pci_device_id 结构体,我们把 EDU 设备的信息填充进去,并使用 MODULE_DEVICE_TABLE 进行导出。
static struct pci_device_id pci_ids[] = {
{
PCI_DEVICE(0x1234, 0x11e8)},
{
0,
}};
这里对于 probe 和 remove 的处理我们先定义两个空函数,其声明可见 pci_driver 结构体。
static int edu_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
return 0;
}
static void edu_remove(struct pci_dev *pdev) {
}
从 __pci_register_driver() 的描述中可以看到其返回值定义:如果有错误发生则返回负数的错误码,否则返回 0,因此做一个判断检查函数是否执行成功。
目前完整的代码如下
#include <linux/module.h>
#include <linux/pci.h>
#define DRIVER_NAME "qemu_edu"
static struct pci_device_id pci_ids[] = {
{
PCI_DEVICE(0x1234, 0x11e8)},
{
0,
}};
MODULE_DEVICE_TABLE(pci, pci_ids);
static int edu_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
return 0;
}
static void edu_remove(struct pci_dev *pdev) {
}
static struct pci_driver pci_driver = {
.name = DRIVER_NAME,
.id_table = pci_ids,
.probe = edu_probe,
.remove = edu_remove,
};
static int __init edu_init(void) {
int ret;
if ((ret = pci_register_driver(&pci_driver)) < 0) {
printk(KERN_INFO "[%s] Init failed. \n", DRIVER_NAME);
return ret;
}
printk(KERN_INFO "[%s] Init sucessful. \n", DRIVER_NAME);
return ret;
}
static void __exit edu_exit(void) {
pci_unregister_driver(&pci_driver);
printk(KERN_INFO "[%s] exit sucessful. \n", DRIVER_NAME);
}
module_init(edu_init);
module_exit(edu_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("QEMU EDU Device Driver");
实现 probe 和 remove 函数
根据文档,probe 函数主要做的事情包括:
- 启用设备
- 请求 MMIO/IOP 资源
- 设置 DMA 掩码大小(包括一致性 DMA 和流式 DMA)
- 分配和初始化共享控制数据(pci_allocate_coherent())
- 访问设备配置空间(如果需要)
- 注册 IRQ 处理程序(request_irq())
- 初始化非 PCI(即芯片的 LAN/SCSI/ 等部分)
- 启用 DMA/处理 引擎
我们根据这个顺序来依次实现它:
void __iomem *mmio_base;
static int edu_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
int ret;
// Enable the PCI device
if ((ret = pci_enable_device(pdev)) < 0) {
printk(KERN_ERR "[%s] pci_enable_device failed. \n", DRIVER_NAME);
return ret;
}
// Request MMIO/IOP resources
if ((ret = pci_request_region(pdev, BAR, DRIVER_NAME)) < 0) {
printk(KERN_ERR "[%s] pci_request_region failed. \n", DRIVER_NAME);
goto disable_device;
}
// Set the DMA mask size
// EDU device supports only 28 bits by default
if ((ret = dma_set_mask_and_coherent(&(pdev->dev), DMA_BIT_MASK(28)) <