西电OS实验三:edu 设备驱动(李航)

个人博客:https://alive0103.github.io/
代码仓在GitHub:https://github.com/Alive0103/XDU-CS-lab
能点个Star就更好了,欢迎来逛逛哇~❣
为避免高质量博客突然被设置成VIP导致各位看不了,还请关注一下我再看,时时更新,关注不亏~

1. 实验题目

1.1 实验目的

通过本实验的学习,掌握信创操作系统内核定制中所常见 PCI 设备驱动适配技术。

1.2 实验内容

本次实验旨在让学生深入理解并实践 edu 设备驱动的开发。实验中,我们将提供 edu 设备驱
动的框架代码,学生需在此基础上完成关键代码的实现。具体实验要求如下:

  1. 补全框架中的 TODO 位置的缺失代码,包含 TODO 的函数如下所示:
    ◦ edu_driver_probe
    ▪为 edu_dev_info 实例分配内存
    ▪将 BAR 的总线地址映射到系统内存的虚拟地址
    ◦ edu_driver_remove
    ▪从设备结构体 dev 中提取 edu_dev_info 实例
    ▪补全 iounmap 函数的调用的参数
    ▪释放 edu_dev_info 实例
    ◦ kthread_handler
    ▪将用户传入的变量交给 edv 设备进行阶乘计算,并读出结果,注意加锁。结果
    放入 user_data 中的 data 数据成员中时,需要确保读写原子性
    ◦ edu_dev_open
    ▪完成 filp 与用户进程上下文信息的绑定操作
    ◦ edu_dev_release
    ▪释放 edu_dev_open 中分配的内存
    ◦ edu_dev_unlocked_ioctl
    ▪用户通过 ioctl 传入要计算阶乘的数值,并读取最后阶乘的结果。计算阶乘使
    用内核线程,线程代码放在 kthread_handler 中
  2. 实现驱动程序的 ioctl 调用处理功能。该调用需接收一个整型参数。当驱动程序接收
    到用户的 ioctl 调用后,需创建一个内核线程。在该内核线程中,利用 edu 设备的阶乘
    功能对传入的整型参数进行计算,并将计算结果存储于驱动程序中,以便用户进程后
    续获取。
  3. 驱动程序需具备识别不同进程调用的能力,确保将计算结果正确返回给对应的调用进
    程。
  4. 编写 C 语言应用程序,通过调用 edu 驱动的 ioctl 接口进行操作。首先,设置参数
    cmd 值为0,输入待计算的数值。等待一定时间后,将参数 cmd 值更改为1,再次调用
    ioctl 接口,以获取设备计算完成的结果。

2. 代码

现象正确,仅供参考。

#include <asm/io.h>
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/delay.h>
#include <linux/pid.h>
#include <linux/kernel.h>
#include <linux/kthread.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/atomic.h>
#include <linux/spinlock.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define EDU_DEV_MAJOR 200  /* 主设备号 */
#define EDU_DEV_NAME "edu" /* 设备名 */

struct edu_dev_info {
    resource_size_t io;
    long range, flags;
    void __iomem *ioaddr;
    int irq;
};

struct user_data {
    int id;
    atomic64_t data;
};

struct thread_data {
    struct user_data *user_data_ptr;
    int input_data;
};

// 声明函数
static int edu_driver_probe(struct pci_dev *dev, const struct pci_device_id *id);
static void edu_driver_remove(struct pci_dev *dev);

// 设备ID表
static struct pci_device_id id_table[] = {
    { PCI_DEVICE(0x1234, 0x11e8) }, // edu设备id
    { 0 } // 最后一组是0,表示结束
};

// PCI驱动结构体
static struct pci_driver pci_driver = {
    .name = "edu_driver",
    .id_table = id_table,
    .probe = edu_driver_probe,
    .remove = edu_driver_remove,
};

// 其他结构体定义
static struct edu_dev_info *edu_info;
static spinlock_t lock;
static int current_id = 0;

// 驱动的 Probe 函数
static int edu_driver_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
    int ret = 0;
    printk("executing edu driver probe function!\n");

    // 启用PCI设备
    ret = pci_enable_device(dev);
    if (ret) {
        printk(KERN_ERR "Failed to enable PCI device.\n");
        return -EIO;
    }

    // 分配内存给 edu_dev_info 实例
    edu_info = kmalloc(sizeof(struct edu_dev_info), GFP_KERNEL);
    if (!edu_info) {
        printk(KERN_ERR "Memory allocation for edu_dev_info failed.\n");
        return -ENOMEM;
    }

    // 请求设备的 I/O 和内存区域
    ret = pci_request_regions(dev, "edu_driver");
    if (ret) {
        printk("PCI request regions failed.\n");
        goto free_edu_info;
    }

    // 映射BAR的总线地址到系统内存的虚拟地址
    edu_info->ioaddr = pci_ioremap_bar(dev, 0); // 假设使用 BAR0
    if (!edu_info->ioaddr) {
        printk(KERN_ERR "Failed to map BAR to virtual memory.\n");
        goto release_regions;
    }

    // 将edu_dev_info实例与设备结构体 dev 关联
    pci_set_drvdata(dev, edu_info);

    printk("Probe succeeds. PCIE ioport addr starts at %llX, edu_info->ioaddr is 0x%p.\n", edu_info->io, edu_info->ioaddr);
    return 0;

release_regions:
    pci_release_regions(dev);
free_edu_info:
    kfree(edu_info);
    return ret;
}

// 驱动的 Remove 函数
static void edu_driver_remove(struct pci_dev *dev)
{
    struct edu_dev_info *info = pci_get_drvdata(dev);

    if (info) {
        iounmap(info->ioaddr);
        pci_release_regions(dev);
        kfree(info);
    }
    pci_disable_device(dev);
    printk("Device removed successfully.\n");
}

// 计算阶乘的内核线程
int kthread_handler(void *data)
{
    struct thread_data *thread_data_ptr = (struct thread_data *)data;
    uint64_t result = 1;
    int i;

    // 获取输入的数值并计算阶乘
    for (i = 1; i <= thread_data_ptr->input_data; i++) {
        result *= i;
    }

    // 加锁确保写入原子变量的原子性
    spin_lock(&lock);
    atomic64_set(&thread_data_ptr->user_data_ptr->data, result);
    spin_unlock(&lock);

    printk("Factorial computed: %llu\n", result);
    kfree(thread_data_ptr);
    return 0;
}

// open 函数
static int edu_dev_open(struct inode *inode, struct file *filp)
{
    struct user_data *user_data_ptr = kmalloc(sizeof(struct user_data), GFP_KERNEL);
    if (!user_data_ptr) {
        return -ENOMEM;
    }

    user_data_ptr->id = current_id++;
    atomic64_set(&user_data_ptr->data, 0);
    filp->private_data = user_data_ptr;
    return 0;
}

// close 函数
static int edu_dev_release(struct inode *inode, struct file *filp)
{
    struct user_data *user_data_ptr = filp->private_data;
    kfree(user_data_ptr); // 释放 open 时分配的内存
    return 0;
}

// ioctl 函数
long edu_dev_unlocked_ioctl(struct file *pfilp_t, unsigned int cmd, unsigned long arg)
{
    struct user_data *user_data_ptr = pfilp_t->private_data;
    if (cmd == 0) {
        // Write operation: Save the input argument (arg) into thread_data
        struct thread_data *data = kmalloc(sizeof(struct thread_data), GFP_KERNEL);
        if (!data) {
            printk(KERN_ERR "Memory allocation for thread_data failed.\n");
            return -ENOMEM;
        }
        data->user_data_ptr = user_data_ptr;
        data->input_data = arg;

        // Log the argument received for write operation
        printk("IOCTL Write: arg = %lu\n", arg);

        // Create a kernel thread to compute the factorial
        struct task_struct *task = kthread_create(kthread_handler, data, "factorial_thread");
        if (IS_ERR(task)) {
            kfree(data);
            return PTR_ERR(task);
        }
        wake_up_process(task); // Start the thread

        return 0;
    } else if (cmd == 1) {
        // Read operation: Return the computed result
        int result = atomic64_read(&user_data_ptr->data);

        // Log the result being read
        printk("IOCTL Read: result = %d\n", result);

        return result;
    }

    return -EINVAL; // Invalid command
}


// 文件操作结构体
static struct file_operations edu_dev_fops = {
    .open = edu_dev_open,
    .release = edu_dev_release,
    .unlocked_ioctl = edu_dev_unlocked_ioctl,
};

// 驱动初始化
static int __init edu_driver_init(void)
{
    int ret = register_chrdev(EDU_DEV_MAJOR, EDU_DEV_NAME, &edu_dev_fops);
    if (ret < 0) {
        printk("Failed to register char device\n");
        return ret;
    }

    ret = pci_register_driver(&pci_driver);
    if (ret) {
        unregister_chrdev(EDU_DEV_MAJOR, EDU_DEV_NAME);
        return ret;
    }

    spin_lock_init(&lock); // 初始化自旋锁
    printk("HELLO PCI\n");
    return 0;
}

// 驱动注销
static void __exit edu_driver_exit(void)
{
    unregister_chrdev(EDU_DEV_MAJOR, EDU_DEV_NAME);
    pci_unregister_driver(&pci_driver);
    printk("GOODBYE PCI\n");
}

MODULE_LICENSE("GPL");
module_init(edu_driver_init);
module_exit(edu_driver_exit);

在这里插入图片描述

写得不太规范,有点警告,问题不大。

2.1 edu_dev_blank.c

本实验的目的是开发一个 PCI 设备驱动,用于实现用户传入数值的阶乘计算。用户空间程序通过 ioctl 调用将数值传递给驱动,驱动程序创建内核线程执行计算,并将结果返回给用户空间程序。整个设计包括字符设备的注册、PCI 设备的初始化与移除、内核线程的创建与管理以及用户空间和内核空间的交互。

驱动程序的基本结构如下:

  1. 字符设备注册:在驱动加载时,注册字符设备并指定文件操作接口,以便用户程序可以通过 /dev/edu 访问驱动。

  2. PCI 设备探测与初始化:在系统发现匹配的 PCI 设备时,调用设备探测函数,初始化设备资源并将其映射到内核地址空间。

  3. 内核线程处理:当用户空间传入数据时,创建内核线程进行阶乘计算,将结果存储在设备结构中,供用户空间读取。

  4. 用户空间和内核空间交互:通过 ioctl 系统调用实现用户空间与内核驱动的数据传递,支持传递数据和获取计算结果的功能。

  5. 资源释放:在驱动卸载时,释放字符设备、PCI 资源、内存映射等,确保驱动的清理工作完整无误。

伪代码

驱动初始化和加载

function edu_driver_init:
    输出 "初始化驱动"

    注册字符设备 (EDU_DEV_MAJOR, EDU_DEV_NAME)
    if 注册失败:
        输出 "字符设备注册失败"
        return 错误代码

    注册 PCI 驱动 (pci_driver)
    if 注册失败:
        注销字符设备
        return 错误代码

    初始化自旋锁
    return 成功

PCI 设备探测与初始化

function edu_driver_probe(PCI设备 dev, 设备ID id):
    输出 "探测到PCI设备"

    启用PCI设备 dev
    if 启用失败:
        输出 "PCI设备启用失败"
        return 错误代码

    分配 edu_dev_info 内存
    if 内存分配失败:
        输出 "内存分配失败"
        return 错误代码

    请求 I/O 和内存资源
    if 请求失败:
        释放 edu_dev_info 内存
        return 错误代码

    将设备的 BAR 地址映射到内核地址空间
    if 映射失败:
        释放 I/O 和内存资源
        释放 edu_dev_info 内存
        return 错误代码

    将设备信息与 PCI 设备绑定
    输出 "设备初始化成功"
    return 成功

用户空间和内核空间的交互 (ioctl 处理函数)

function edu_dev_unlocked_ioctl(file结构 pfilp_t, 命令 cmd, 参数 arg):
    获取用户的上下文数据 (user_data)

    if cmd == 0:  # 用户请求计算阶乘
        分配线程数据结构 thread_data
        设置计算参数 (arg)
        创建内核线程 (kthread_handler, thread_data)
        if 线程创建失败:
            释放 thread_data
            return 错误代码
        启动线程

    else if cmd == 1:  # 用户请求读取计算结果
        返回 user_data 中存储的计算结果

    return 成功

内核线程处理函数 (计算阶乘)

function kthread_handler(线程数据 data):
    获取输入的数值 (data.input_data)
    计算阶乘 (循环计算结果 result)

    使用自旋锁加锁
    存储结果到 user_data 的原子变量中
    解锁自旋锁

    输出计算结果
    释放线程数据内存
    return 成功

设备关闭与资源释放

function edu_driver_remove(PCI设备 dev):
    获取设备私有数据 edu_dev_info

    if edu_dev_info 存在:
        取消内存映射
        释放 I/O 和内存资源
        释放 edu_dev_info 内存

    禁用 PCI 设备
    输出 "设备已移除"

驱动卸载

function edu_driver_exit:
    注销字符设备 (EDU_DEV_MAJOR, EDU_DEV_NAME)
    注销 PCI 驱动 (pci_driver)
    输出 "驱动已卸载"

2.2 Makefile

按照此思路写文件后执行make,有警告,而后加载内核出现找不到文件错误,可能是我文件没放对位置,
就比较离谱,
,应该在修改edu_env_blank.c文件前就进到lab/3/code修改edu_env_blank.c文件,而后修改Makefile如下:

(重命名edu_env_blank.c文件为edu_env.c就不用修改Makefile,直接下一步)

CC=gcc
CFLAGS=-std=c11

ifneq ($(KERNELRELEASE),)
# 指定编译的模块文件名,应与源文件名保持一致
obj-m := edu_dev_blank.o
else
# 内核目录和当前目录
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

# 默认目标:编译模块
all:
	make -C $(KDIR) M=$(PWD) modules

# 清理目标:删除生成的文件
clean:
	rm -f *.ko *.o *.symvers *.cmd .*.cmd *.mod *.mod.c *.order

# 安装目标:编译并插入模块,创建设备节点
install:
	make -C $(KDIR) M=$(PWD) modules
	sudo insmod edu_dev_blank.ko
	sudo mknod /dev/edu c 200 0

# 卸载目标:移除模块并删除设备节点
uninstall:
	sudo rmmod edu_dev_blank
	sudo rm -f /dev/edu

endif

成功编译运行!

3. 操作步骤

编写完代码后进行加载测试,若报错可以像我一样更改Makefile,以下为可能用到的命令:

# 编译模块
make

# 加载模块
sudo insmod edu_dev_blank.ko
sudo insmod edu_dev.ko # 实验四用这个,记得改Makeflie相应地方的文件名再make

# 创建设备节点
sudo mknod /dev/edu c 200 0

# 通过 dmesg 查看内核日志来确认模块是否加载成功
dmesg | tail # 能在日志中看到类似 “HELLO PCI” 的信息 用我的代码的话就会得到和我截图里一样的输出

# 运行用户程序user_space.cpp
gcc user_space.c -o user_space # 编译
gcc user_space.c -o user # 编译(实验四用这个生成user)
./user_space # 运行

sudo rmmod edu_dev_blank  # 卸载内核模块

sudo rmmod edu_dev # 实验四
sudo rm /dev/edu          # 删除设备节点

4. 结果

在这里插入图片描述

5. 问题回答

5.1 实验代码的语义解释

实验代码的核心功能是开发一个用于计算阶乘的 PCI 设备驱动。代码结构包括字符设备注册、PCI 设备初始化、内核线程创建和用户空间的交互。以下是关键部分的解释:

  1. 驱动初始化与加载:
  • edu_driver_init 函数注册字符设备,使用户空间可以通过设备文件 /dev/edu 访问驱动。然后注册 PCI 驱动,通过 pci_register_driver 将驱动与 PCI 子系统关联。当系统发现匹配的 PCI 设备时,会调用 edu_driver_probe 进行设备初始化。
  • edu_driver_exit 函数负责卸载驱动,释放字符设备和 PCI 驱动的注册。
  1. PCI 设备探测与初始化:
  • edu_driver_probe 函数在设备探测到时被调用,用于初始化设备。它首先通过 pci_enable_device 启用设备。接着,分配 edu_dev_info 结构体来存储设备信息,如设备 I/O 和内存区域。
  • 通过 pci_request_regions 请求设备的 I/O 和内存资源,避免资源冲突。然后通过 pci_ioremap_bar 将设备的 BAR 地址映射到内核虚拟地址空间,以便驱动可以直接访问设备的寄存器。
  • 最后,将设备信息指针与 PCI 设备结构体绑定,便于在移除时访问和释放资源。
  1. 字符设备操作:
  • edu_dev_open:在用户程序打开设备时调用,分配 user_data 结构体用于存储用户的计算数据,并将其存储在文件描述符的 private_data 中。
  • edu_dev_release:在用户程序关闭设备时调用,释放 user_data 结构体,避免内存泄漏。
  • edu_dev_unlocked_ioctl:通过 ioctl 接口与用户程序交互。用户传入的 cmd=0 表示计算阶乘,驱动创建内核线程执行计算;cmd=1 表示读取计算结果,驱动返回 user_data 中的结果。

4.内核线程处理:
kthread_handler 是一个内核线程函数,用于计算阶乘。当用户请求计算时,edu_dev_unlocked_ioctl 创建并启动该线程,将用户传入的数值作为输入,计算结果存储在 user_data 的原子变量中,供用户读取。

5.设备移除与资源释放:
edu_driver_remove 在驱动卸载时调用,用于释放设备资源。它会解除内存映射、释放 I/O 和内存资源、禁用设备,确保资源的干净释放。

5.2 如何访问 PCI 设备的寄存器

在 PCI 设备驱动中,访问设备寄存器的关键步骤如下:

  1. 启用 PCI 设备

首先,通过 pci_enable_device 函数启用设备,使其能够正常工作。此步骤必须在访问设备寄存器之前完成,否则系统无法保证设备会响应 I/O 和内存操作。

  1. 请求 I/O 和内存资源

使用 pci_request_regions 函数向系统请求设备的 I/O 和内存资源,确保该设备的资源不会与其他设备发生冲突。通过这种方式,系统会分配专门的 I/O 区域和内存区域给设备。

  1. 映射设备的基地址寄存器 (BAR)

PCI 设备的基地址寄存器(Base Address Register, BAR)包含了设备寄存器的地址。通过 pci_ioremap_bar 函数,可以将 BAR 地址映射到内核的虚拟地址空间,使得驱动可以直接使用指针访问设备的寄存器。例如:

edu_info->ioaddr = pci_ioremap_bar(dev, 0); // 假设使用 BAR0

这里的 edu_info->ioaddr 就是一个指向设备寄存器的虚拟地址指针,驱动可以通过它来访问设备的寄存器。

  1. 访问寄存器

一旦获得映射后的虚拟地址(如 edu_info->ioaddr),驱动程序可以使用内核提供的 I/O 读写函数(例如 readl、writel)来访问设备寄存器。例如:

value = readl(edu_info->ioaddr + offset);  // 读取寄存器
writel(data, edu_info->ioaddr + offset);   // 写入寄存器

其中 offset 是寄存器的偏移量,通过加上该偏移量可以定位到具体的寄存器地址。

5.3 启用线程的好处与问题

在该驱动程序中,通过 kthread_create 创建内核线程,用于执行用户请求的阶乘计算任务。使用线程有以下好处和问题:

  1. 启用线程的好处

异步计算:线程的引入使得计算任务可以异步执行,不会阻塞 ioctl 的调用流程。用户可以在请求计算后不必等待结果,而是可以稍后通过再次调用 ioctl 获取结果。这样提升了用户程序的响应速度,改善了用户体验。

分离计算任务:由于阶乘计算可能是一个耗时任务,将计算放到内核线程中可以将计算任务与驱动主流程分离,避免长时间占用 CPU。线程运行在内核空间,可以优先使用内核资源执行任务。

数据保护和安全性:线程使用自旋锁保护共享数据(即 user_data 中的结果变量),确保在并发环境下数据的一致性和安全性。这样可以避免用户多次请求计算时造成的数据竞争问题。

  1. 启用线程带来的问题

资源开销:每次请求计算时,驱动会创建一个新的内核线程,这会增加内存开销和 CPU 资源的使用。特别是当多个用户请求同时到达时,内核中会产生多个线程,可能导致系统资源被占用过多,进而影响系统性能。可以考虑线程复用或队列管理来优化资源利用。

数据同步和并发问题:虽然使用了自旋锁来保证数据的同步性,但在高并发情况下,大量的线程尝试访问同一个数据区域可能导致系统的自旋锁开销增大,甚至可能引发死锁等问题。因此,需要仔细设计并发控制,避免锁竞争带来的性能瓶颈。

线程管理和清理:在内核中创建线程需要严格管理和清理。未正确释放的线程可能导致内核资源泄漏和系统崩溃。在本驱动中,线程完成计算后会自动退出,但在实际开发中,需要更加全面的错误处理机制,确保线程在异常情况下能够安全退出。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alive~o.0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值