searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

qemu设备模拟&模拟设备内核态驱动开发指南

2025-07-01 05:47:21
3
0

1. 概述

本文讲述创建一个简单的QEMU模拟设备,并为其开发内核态驱动流程。文档整体分为设备模拟和驱动开发两部分。


2. QEMU设备模拟实现

2.1 编码思路

下文将模拟一个PCI设备,提供内存映射IO(MMIO)功能,允许用户空间通过驱动对设备bar寄存器进行数据dma读写。

第一步:PCI 设备类型注册

#define TYPE_PCI_PRACTICE_DEVICE "pratice"

static void pci_pratice_register_types(void)
{
    static InterfaceInfo interfaces[] = {
        { INTERFACE_CONVENTIONAL_PCI_DEVICE },
        { },
    };
    static const TypeInfo pratice_info = {
        .name          = TYPE_PCI_PRATICE_DEVICE,
        .parent        = TYPE_PCI_DEVICE,
        .instance_size = sizeof(PraticeState),
        .class_init    = pratice_class_init, //设备类的初始化函数
        .interfaces = interfaces,
    };

    type_register_static(&pratice_info);
}
type_init(pci_pratice_register_types)

第二步:实现设备类的初始化函数,这里需要注意的是在 Class 的初始化函数中我们应当设置父类 PCIDeviceClass 的一系列基本属性(也就是 PCI 设备的基本属性)

static void pratice_class_init(ObjectClass *class, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(class);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(class);

		// 设置PCI设备属性
    k->realize = pci_pratice_realize;
    k->vendor_id = PCI_VENDOR_ID_PRATICE;
    k->device_id = PCI_DEVICE_ID_PRATICE;
    k->revision = PRATICE_PCI_REVID;
    k->class_id = PCI_CLASS_OTHERS;
    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}

第三步:设备实例初始化函数实现

// 设备实例结构体定义
struct PraticeState {
    PCIDevice parent_obj;    // 继承PCI设备
    MemoryRegion mmio;       // MMIO内存区域
    char buf[DMA_SIZE];      // 设备BAR空间,设备存储空间
};

// 设备实例初始化
static void pci_pratice_realize(PCIDevice *pdev, Error **errp)
{
    PraticeState *pratice = PRATICE(pdev);
    // 初始化MMIO区域
    memory_region_init_io(&pratice->mmio, OBJECT(pratice), &pratice_mmio_ops, pratice, "pratice-mmio", 4 * KiB);
    // 注册PCI BAR
    pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &pratice->mmio);
}

第四步:设备MMIO内存区域读写回调函数定义,需要注意的是这里传入的 hwaddr 类型参数其实为相对addr而非绝对addr

// MMIO区域操作集合
static const MemoryRegionOps pratice_mmio_ops = {
    .read = pratice_mmio_read,
    .write = pratice_mmio_write
};

// 实现MMIO操作回调
static uint64_t pratice_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
    // 从设备缓冲区读取数据
    memcpy(&val, &pratice->buf[addr], size);
    return val;
}
static void pratice_mmio_write(void *opaque, hwaddr addr, uint64_t val,
                unsigned size)
{
    memcpy(&pratice->buf[addr], &val, size);
}

2.2 编译步骤和运行Qemu

第一步:在 构建系统中加入设备

hw/misc/meson.build 中加入如下语句:

softmmu_ss.add(when: 'CONFIG_PCI_PRATICE', if_true: files('pratice.c'))

并在 hw/misc/Kconfig 中添加如下内容,这表示我们的设备会在 CONFIG_PCI_DEVICES=y 时编译:

config PRATICE
    bool
    default y if PCI_DEVICES
    depends on PCI

第二步:在Qemu运行脚本附加上 -device pratice ,之后随便起一个 Linux 系统,此时使用 lspci 指令我们便能看到我们新添加的 pci 设备。红框部分为设备pci属性 vender_id:device_id (revision)
image.png

3. Linux内核驱动开发

3.1 驱动代码实现

第一步:定义设备信息

#define DRIVER_NAME "pratice"
#define PCI_VENDOR_ID_PRATICE 0x2025
#define PCI_DEVICE_ID_PRATICE 0x0605
#define BAR 0
#define DMA_SIZE 4096

第二步定义设备结构体

struct pratice_device {
    struct pci_dev *pdev;
    struct cdev cdev;
    dev_t devno;
    void __iomem *mmio;
    struct mutex lock;
};

第三步:内核模块install/remove函数实现

static struct pci_device_id ids[] = {
    { PCI_DEVICE(PCI_VENDOR_ID_PRATICE, PCI_DEVICE_ID_PRATICE) },
    { 0 }
};
MODULE_DEVICE_TABLE(pci, ids);

static struct pci_driver pratice_driver = {
    .name = DRIVER_NAME,
    .id_table = ids,
    .probe = pratice_probe,
    .remove = pratice_remove,
};

static int __init pratice_init(void) {
    int ret;
    if ((ret = pci_register_driver(&pratice_driver)) < 0) {
        printk(KERN_ERR "[%s] Init failed. \\n", DRIVER_NAME);
        return ret;
    }

    printk(KERN_INFO "[%s] Init sucessfully. \\n", DRIVER_NAME);
    return ret;
}

static void __exit pratice_exit(void) {
    pci_unregister_driver(&pratice_driver);
    printk(KERN_INFO "[%s] exited. \\n", DRIVER_NAME);
}

module_init(pratice_init);
module_exit(pratice_exit);

第四步:探测和移除函数实现 (pratice_probe、pratice_remove)

static int pratice_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    // 启用PCI设备
    pci_enable_device(pdev);

    // 映射MMIO区域
    dev->mmio = pci_iomap(pdev, BAR, pci_resource_len(pdev, BAR));

    // 注册字符设备
    alloc_chrdev_region(&dev->devno, 0, 1, DRIVER_NAME);
    cdev_init(&dev->cdev, &fops);
    cdev_add(&dev->cdev, dev->devno, 1);

    // 创建设备节点
    device_create(pratice_class, NULL, dev->devno, NULL, "pratice");

    // 初始化互斥锁
    mutex_init(&dev->lock);
}

static void pratice_remove(struct pci_dev *pdev)
{
    struct pratice_device *dev = pci_get_drvdata(pdev);

    device_destroy(pratice_class, dev->devno);
    class_destroy(pratice_class);
    cdev_del(&dev->cdev);
    unregister_chrdev_region(dev->devno, 1);
    pci_iounmap(pdev, dev->mmio);
    mutex_destroy(&dev->lock);
    kfree(dev);
    pci_disable_device(pdev);
}

第五步:文件操作实现

static const struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = pratice_read,
    .write = pratice_write,
    .open = pratice_open
};
static ssize_t pratice_read(struct file *filp, char __user *buf, ...)
{
    // 1. 检查访问边界
    // 2. 加锁保护
    // 3. memcpy_fromio读取硬件数据
    // 4. copy_to_user传至用户空间
}
static ssize_t pratice_read(struct file *filp, char __user *buf, ...)
{
    // 1. 检查访问边界
    // 2. 加锁保护
    // 3. memcpy_fromio读取硬件数据
    // 4. copy_to_user传至用户空间
}

static int edu_wxd_open(struct inode *inode, struct file *filp)
{
    // 1.通过字符设备结构找到对应的驱动设备实例
    // 2. 将设备实例保存到文件对象的私有数据区
}

3.2 编译和install驱动

第一步:编译

make -C $(KERNELDIR) M=$(PWD) modules

第二步:install驱动

insmod pratice_driver.ko

3.3 用户空间测试

在用户空间打开设备文件,进行相应的读写测试。


4. 总结

本文档包括:

  1. 如何在QEMU中创建自定义PCI设备
  2. 如何实现模拟设备驱动

此学习case可作为开发真实硬件驱动的基础框架,后续可扩展支持中断等高级功能。

0条评论
0 / 1000
w****n
2文章数
0粉丝数
w****n
2 文章 | 0 粉丝
w****n
2文章数
0粉丝数
w****n
2 文章 | 0 粉丝
原创

qemu设备模拟&模拟设备内核态驱动开发指南

2025-07-01 05:47:21
3
0

1. 概述

本文讲述创建一个简单的QEMU模拟设备,并为其开发内核态驱动流程。文档整体分为设备模拟和驱动开发两部分。


2. QEMU设备模拟实现

2.1 编码思路

下文将模拟一个PCI设备,提供内存映射IO(MMIO)功能,允许用户空间通过驱动对设备bar寄存器进行数据dma读写。

第一步:PCI 设备类型注册

#define TYPE_PCI_PRACTICE_DEVICE "pratice"

static void pci_pratice_register_types(void)
{
    static InterfaceInfo interfaces[] = {
        { INTERFACE_CONVENTIONAL_PCI_DEVICE },
        { },
    };
    static const TypeInfo pratice_info = {
        .name          = TYPE_PCI_PRATICE_DEVICE,
        .parent        = TYPE_PCI_DEVICE,
        .instance_size = sizeof(PraticeState),
        .class_init    = pratice_class_init, //设备类的初始化函数
        .interfaces = interfaces,
    };

    type_register_static(&pratice_info);
}
type_init(pci_pratice_register_types)

第二步:实现设备类的初始化函数,这里需要注意的是在 Class 的初始化函数中我们应当设置父类 PCIDeviceClass 的一系列基本属性(也就是 PCI 设备的基本属性)

static void pratice_class_init(ObjectClass *class, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(class);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(class);

		// 设置PCI设备属性
    k->realize = pci_pratice_realize;
    k->vendor_id = PCI_VENDOR_ID_PRATICE;
    k->device_id = PCI_DEVICE_ID_PRATICE;
    k->revision = PRATICE_PCI_REVID;
    k->class_id = PCI_CLASS_OTHERS;
    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}

第三步:设备实例初始化函数实现

// 设备实例结构体定义
struct PraticeState {
    PCIDevice parent_obj;    // 继承PCI设备
    MemoryRegion mmio;       // MMIO内存区域
    char buf[DMA_SIZE];      // 设备BAR空间,设备存储空间
};

// 设备实例初始化
static void pci_pratice_realize(PCIDevice *pdev, Error **errp)
{
    PraticeState *pratice = PRATICE(pdev);
    // 初始化MMIO区域
    memory_region_init_io(&pratice->mmio, OBJECT(pratice), &pratice_mmio_ops, pratice, "pratice-mmio", 4 * KiB);
    // 注册PCI BAR
    pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &pratice->mmio);
}

第四步:设备MMIO内存区域读写回调函数定义,需要注意的是这里传入的 hwaddr 类型参数其实为相对addr而非绝对addr

// MMIO区域操作集合
static const MemoryRegionOps pratice_mmio_ops = {
    .read = pratice_mmio_read,
    .write = pratice_mmio_write
};

// 实现MMIO操作回调
static uint64_t pratice_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
    // 从设备缓冲区读取数据
    memcpy(&val, &pratice->buf[addr], size);
    return val;
}
static void pratice_mmio_write(void *opaque, hwaddr addr, uint64_t val,
                unsigned size)
{
    memcpy(&pratice->buf[addr], &val, size);
}

2.2 编译步骤和运行Qemu

第一步:在 构建系统中加入设备

hw/misc/meson.build 中加入如下语句:

softmmu_ss.add(when: 'CONFIG_PCI_PRATICE', if_true: files('pratice.c'))

并在 hw/misc/Kconfig 中添加如下内容,这表示我们的设备会在 CONFIG_PCI_DEVICES=y 时编译:

config PRATICE
    bool
    default y if PCI_DEVICES
    depends on PCI

第二步:在Qemu运行脚本附加上 -device pratice ,之后随便起一个 Linux 系统,此时使用 lspci 指令我们便能看到我们新添加的 pci 设备。红框部分为设备pci属性 vender_id:device_id (revision)
image.png

3. Linux内核驱动开发

3.1 驱动代码实现

第一步:定义设备信息

#define DRIVER_NAME "pratice"
#define PCI_VENDOR_ID_PRATICE 0x2025
#define PCI_DEVICE_ID_PRATICE 0x0605
#define BAR 0
#define DMA_SIZE 4096

第二步定义设备结构体

struct pratice_device {
    struct pci_dev *pdev;
    struct cdev cdev;
    dev_t devno;
    void __iomem *mmio;
    struct mutex lock;
};

第三步:内核模块install/remove函数实现

static struct pci_device_id ids[] = {
    { PCI_DEVICE(PCI_VENDOR_ID_PRATICE, PCI_DEVICE_ID_PRATICE) },
    { 0 }
};
MODULE_DEVICE_TABLE(pci, ids);

static struct pci_driver pratice_driver = {
    .name = DRIVER_NAME,
    .id_table = ids,
    .probe = pratice_probe,
    .remove = pratice_remove,
};

static int __init pratice_init(void) {
    int ret;
    if ((ret = pci_register_driver(&pratice_driver)) < 0) {
        printk(KERN_ERR "[%s] Init failed. \\n", DRIVER_NAME);
        return ret;
    }

    printk(KERN_INFO "[%s] Init sucessfully. \\n", DRIVER_NAME);
    return ret;
}

static void __exit pratice_exit(void) {
    pci_unregister_driver(&pratice_driver);
    printk(KERN_INFO "[%s] exited. \\n", DRIVER_NAME);
}

module_init(pratice_init);
module_exit(pratice_exit);

第四步:探测和移除函数实现 (pratice_probe、pratice_remove)

static int pratice_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    // 启用PCI设备
    pci_enable_device(pdev);

    // 映射MMIO区域
    dev->mmio = pci_iomap(pdev, BAR, pci_resource_len(pdev, BAR));

    // 注册字符设备
    alloc_chrdev_region(&dev->devno, 0, 1, DRIVER_NAME);
    cdev_init(&dev->cdev, &fops);
    cdev_add(&dev->cdev, dev->devno, 1);

    // 创建设备节点
    device_create(pratice_class, NULL, dev->devno, NULL, "pratice");

    // 初始化互斥锁
    mutex_init(&dev->lock);
}

static void pratice_remove(struct pci_dev *pdev)
{
    struct pratice_device *dev = pci_get_drvdata(pdev);

    device_destroy(pratice_class, dev->devno);
    class_destroy(pratice_class);
    cdev_del(&dev->cdev);
    unregister_chrdev_region(dev->devno, 1);
    pci_iounmap(pdev, dev->mmio);
    mutex_destroy(&dev->lock);
    kfree(dev);
    pci_disable_device(pdev);
}

第五步:文件操作实现

static const struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = pratice_read,
    .write = pratice_write,
    .open = pratice_open
};
static ssize_t pratice_read(struct file *filp, char __user *buf, ...)
{
    // 1. 检查访问边界
    // 2. 加锁保护
    // 3. memcpy_fromio读取硬件数据
    // 4. copy_to_user传至用户空间
}
static ssize_t pratice_read(struct file *filp, char __user *buf, ...)
{
    // 1. 检查访问边界
    // 2. 加锁保护
    // 3. memcpy_fromio读取硬件数据
    // 4. copy_to_user传至用户空间
}

static int edu_wxd_open(struct inode *inode, struct file *filp)
{
    // 1.通过字符设备结构找到对应的驱动设备实例
    // 2. 将设备实例保存到文件对象的私有数据区
}

3.2 编译和install驱动

第一步:编译

make -C $(KERNELDIR) M=$(PWD) modules

第二步:install驱动

insmod pratice_driver.ko

3.3 用户空间测试

在用户空间打开设备文件,进行相应的读写测试。


4. 总结

本文档包括:

  1. 如何在QEMU中创建自定义PCI设备
  2. 如何实现模拟设备驱动

此学习case可作为开发真实硬件驱动的基础框架,后续可扩展支持中断等高级功能。

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0