Linux下的驱动开发二

一、IO模型

  1. I/O 模型在操作系统中用于处理应用程序与设备驱动之间的数据传输。
  2. I/O 通信模型的核心是解决程序与设备之间如何高效、合理地进行数据通信。不同的模型通过阻塞、非阻塞、同步、异步的方式来控制数据流和处理 I/O 请求。

注:在驱动开发中可以定义一个全局结构体

  1. 用于全局管理驱动程序的状态和资源。
  2. 在驱动开发中,有时需要一个全局的数据结构来保存设备的状态、驱动配置、缓存、锁等信息,这样不同的驱动程序函数可以通过该结构体访问和修改共享的资源。
  3. 作用
    • 统一管理驱动状态: 全局结构体通常保存驱动程序的各种状态信息。例如,设备的注册信息、分配的内存、硬件寄存器映射等都可以保存在这个结构体中。
    • 共享资源: 驱动程序通常会有多个函数被内核调用,比如初始化函数、读写函数、中断处理函数等。使用全局结构体,可以让这些函数方便地共享和访问同样的数据或资源。
    • 简化代码结构: 将驱动程序涉及的所有全局变量封装在一个结构体中,使代码更加清晰、结构化,同时也减少了全局变量命名冲突的可能性。

一、阻塞 I/O 模型

  1. 阻塞 I/O 是最常见的 I/O 模型。当应用程序发起 I/O 请求时,如果数据没有准备好,应用程序将进入阻塞状态,等待数据准备完毕后,驱动程序会唤醒阻塞的应用程序。此时,应用程序才能继续执行。
  2. 工作流程
cpp 复制代码
应用程序向驱动发送读取数据的请求。
如果驱动程序中数据尚未准备好,应用程序进入 睡眠状态,等待数据到来。
当数据准备好后,驱动程序会唤醒应用程序,程序从睡眠状态恢复,继续读取数据。
完成 I/O 操作后,应用程序继续执行其他任务。
  1. 具体步骤
    a. 定义并初始化等待队列
c 复制代码
wait_queue_head_t wqhead;//在全局结构体中
在结构体 global_struct 中定义了 wqhead,表示等待队列头,用于管理睡眠进程的队列。

在mod_init() 初始化函数中,通过 init_waitqueue_head() 初始化等待队列头。
init_waitqueue_head(&gstruct.wqhead);

b. 进程睡眠机制

c 复制代码
在 chdev_read 函数中,当应用程序尝试从驱动读取数据时,程序会检查数据是否可用。如果数据不可用,进程就会进入睡眠状态,挂到等待队列上。
wait_event_interruptible(pt_gstruct->wqhead, pt_gstruct->data_flag);

c. 唤醒机制

cpp 复制代码
当数据到达时,会通过 chdev_write 函数进行处理
pt_gstruct->data_flag = 1;//数据写入后,data_flag 被设置为 1,表示数据已准备好。
wake_up_interruptible(&pt_gstruct->wqhead);
wake_up_interruptible() 函数用于唤醒等待队列 wqhead 上的所有进程,唤醒后,之前处于睡眠状态的进程会继续执行 chdev_read 函数中的代码,读取数据。

二、非阻塞I/O实现

  1. 非阻塞 I/O 的实现逻辑与阻塞 I/O 类似,但不进入睡眠状态。如果数据尚未准备好,非阻塞 I/O 立即返回错误 -EAGAIN,告诉应用程序稍后重试。
cpp 复制代码
if (file->f_flags & O_NONBLOCK) {    // 检查文件标志是否为非阻塞模式
    if (pt_gstruct->data_flag == 0)  // 没有数据可用
        return -EAGAIN;              // 返回 EAGAIN 错误,表示无数据,稍后重试
}

三、异步通知

  1. 异步通知(Asynchronous Notification)是 Linux 内核提供的一种机制,它允许设备驱动在数据准备好或状态发生变化时,主动向应用程序发送信号(通常是 SIGIO 信号),通知应用程序及时处理。
  2. 这种机制在应用程序不需要不断轮询设备状态的情况下非常有用。
  3. 设备驱动中的结构定义
cpp 复制代码
struct global_struct {
    struct class *cls;
    int major, minor;
    struct cdev cdev_obj;
    wait_queue_head_t wqhead;    // 等待队列头
    char sharebuf[128];          // 共享缓冲区
    int data_flag;               // 标记数据是否可用,0 表示无数据,1 表示有数据
    struct fasync_struct *fapp;  // 异步通知结构,用于管理异步通知
};
  1. 驱动程序的 fasync 实现
cpp 复制代码
int chdev_fasync(int fd, struct file *file, int on)
{
    struct global_struct *pt_gstruct = file->private_data;

    // fasync_helper 负责将文件描述符和进程与异步通知关联或解除关联
    return fasync_helper(fd, file, on, &pt_gstruct->fapp);
}
  1. 实现数据写入时的异步通知
cpp 复制代码
//当驱动程序中有新数据时,通过 kill_fasync 向注册了异步通知的进程发送信号。
ssize_t chdev_write(struct file *file, const char __user *usr, size_t sz, loff_t *loff)
{
    struct global_struct *pt_gstruct = file->private_data;
    long ret;

    // 将用户数据复制到共享缓冲区
    ret = copy_from_user(pt_gstruct->sharebuf, usr, sz);
    if (ret > 0) {
        printk("%s-%d copy_from_user err\n", __func__, __LINE__);
        return -3;
    }

    // 数据写入完成后,标记数据已经准备好
    pt_gstruct->data_flag = 1;

    // 唤醒所有等待在等待队列上的进程
    wake_up_interruptible(&pt_gstruct->wqhead);

    // 发送异步通知信号,通知应用程序有数据可读
    kill_fasync(&pt_gstruct->fapp, SIGIO, POLL_IN);

    return sz;
}

二、内核中断驱动

  1. Linux 内核中的中断处理系统使得硬件能够通知内核发生了特定事件(如数据就绪、设备完成某项任务等),并让内核采取相应的处理措施。

1. 内核中获取中断号

  1. 在 ARM 等嵌入式平台上,设备通常使用设备树 (Device Tree)来描述硬件信息。驱动程序可以从设备树节点中获取与设备相关的中断号。
    • 直接写入
cpp 复制代码
device_node {
    compatible = "mydevice";
    reg = <0x12340000 0x1000>;
    interrupts = <30 2>;  // 中断号 30,触发类型 2(下降沿触发)
};
    • 设备树描述了硬件信息并提供给内核使用。在编译设备树时,我们使用交叉编译工具和特定的架构。
cpp 复制代码
make dtbs ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-
-  拷贝到共享目录
编译完成的 .dtb 文件需要通过某种方式传输到开发板。通常,我们会将 .dtb 文件拷贝到一个共享目录(如 NFS、TFTP 或本地磁盘),以便通过 U-Boot 加载和烧录。
cpp 复制代码
cp arch/arm/boot/dts/your_device.dtb /path/to/shared/directory/
	- 通过 U-Boot 烧录设备树文件
cpp 复制代码
loady 41000000
	- movi write dtb 命令将 .dtb 文件写入设备存储
	- 验证设备树文件是否生效
	- 重启linux,在 /proc/device-tree/ 目录会有你的 节点文件目录
实现代码
  • of_find_node_by_path("/key2"); // 从设备树中查找路径为 /key2 的节点
  • irq_of_parse_and_map(of_node, 0); // 从设备节点获取中断号
  • 具体代码
cpp 复制代码
// Linux 内核中断驱动程序
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/irqreturn.h>
#include <linux/of.h>
#include <linux/interrupt.h>
#include <linux/of_irq.h>

/* 定义全局结构体 */
struct global_struct {
    int irqno;  // 保存从设备树中解析出的中断号
    int xxx;    // 其他私有数据,用户可以根据需要扩展
};

/* 定义全局变量 */
struct global_struct gstruct;

/* 中断处理函数 */
irqreturn_t irq_key_handle(int irqno, void *args)
{
    struct global_struct *pt_gstruct = args;  // 从传入的参数中获取私有数据

    printk(KERN_INFO "%s - 中断发生,irqno = %d, 设备中断号 = %d\n", 
           __func__, irqno, pt_gstruct->irqno);

    // 执行中断处理逻辑,例如:读取硬件状态、清除中断标志等

    return IRQ_HANDLED;  // 返回 IRQ_HANDLED 表示中断已处理完成
}

/* 模块初始化函数 */
int mod_init(void)
{
    int ret;
    int irqno;
    struct device_node *of_node;

    printk(KERN_INFO "中断驱动模块初始化\n");

    /* 1. 通过设备树路径查找设备节点 */
    of_node = of_find_node_by_path("/key2");  // 从设备树中查找路径为 /key2 的节点
    if (!of_node) {
        printk(KERN_ERR "%s - 无法找到设备树节点 /key2\n", __func__);
        return -EINVAL;  // 返回错误码
    }

    /* 2. 从设备树节点解析中断号 */
    irqno = irq_of_parse_and_map(of_node, 0);  // 从设备节点获取中断号
    if (irqno < 0) {
        printk(KERN_ERR "%s - 无法从设备树节点中解析中断号\n", __func__);
        return -EINVAL;  // 返回错误码
    }

    printk(KERN_INFO "%s - 获取的中断号为 %d\n", __func__, irqno);

    /* 3. 保存中断号到全局结构体中 */
    gstruct.irqno = irqno;
    gstruct.xxx = 10086;  // 示例私有数据

    /* 4. 注册中断处理程序 */
    ret = request_irq(irqno, irq_key_handle, IRQF_TRIGGER_FALLING, "key2_intr", &gstruct);
    if (ret < 0) {
        printk(KERN_ERR "%s - 注册中断处理程序失败,错误码:%d\n", __func__, ret);
        return ret;  // 返回错误码
    }

    printk(KERN_INFO "中断处理程序注册成功,中断号为 %d\n", irqno);
    return 0;  // 模块初始化成功
}

/* 模块退出函数 */
void mod_exit(void)
{
    /* 释放中断 */
    free_irq(gstruct.irqno, &gstruct);  // 释放之前注册的中断
    printk(KERN_INFO "中断驱动模块卸载\n");
}

/* 声明模块的初始化和退出函数 */
module_init(mod_init);
module_exit(mod_exit);

/* 模块的许可信息 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple IRQ Device Driver using Device Tree");
MODULE_VERSION("1.0");
简明步骤
  1. 编写设备树节点:在设备树(.dts 文件)中定义设备的中断号和触发类型。
    • 在设备树(.dts 文件)中定义设备的中断号和触发类型。
cpp 复制代码
key2: gpio-keys {
    compatible = "gpio-keys";
    interrupts = <30 IRQ_TYPE_EDGE_FALLING>;  // 中断号 30,下降沿触发
};
  1. 编写中断驱动程序:
  • 定义驱动的全局结构体,保存中断号和其他私有数据。
  • 实现中断处理函数。
cpp 复制代码
irqreturn_t irq_key_handle(int irqno, void *args)
{
    printk(KERN_INFO "中断发生,irqno = %d\n", irqno);
    return IRQ_HANDLED;
}
  1. 从设备树获取中断号
  • 使用 of_find_node_by_path("/key2") 查找设备树节点。
  • 使用 irq_of_parse_and_map() 解析设备树中的中断号。
cpp 复制代码
struct device_node *of_node = of_find_node_by_path("/key2");
int irqno = irq_of_parse_and_map(of_node, 0);
  1. 注册中断处理程序:使用 request_irq() 函数注册中断处理函数。
cpp 复制代码
request_irq(irqno, irq_key_handle, IRQF_TRIGGER_FALLING, "key2_intr", &gstruct);
  1. 释放中断资源(在模块退出时):在 mod_exit 函数中,使用 free_irq() 释放中断资源。
cpp 复制代码
free_irq(gstruct.irqno, &gstruct);
  1. 编译并加载内核模块:编译驱动模块并加载到内核中,验证中断是否正常工作。

中断下半部分

  1. 在 Linux 内核中,中断处理分为上半部分和下半部分。上半部分(Top Half)是中断处理函数(ISR,Interrupt Service Routine),它在中断发生时立即执行,尽量简短以提高系统响应速度。为了避免在中断上下文中执行复杂或耗时的操作,Linux 提供了下半部分(Bottom Half)机制,用于延迟处理不需要立即执行的任务。
  2. LINUX系统中执行单元具体分别如下:
执行单元 优先级 是否允许睡眠 适合任务类型
进程 可以睡眠 耗时或不耗时任务,不紧急任务
中断下半部分 耗时任务,紧急任务
中断上半部分 否(禁止睡眠) 短时间任务,不耗时,紧急任务

中断下半部分机制

中断下半部分机制 优先级 是否允许睡眠 适用场景 使用方式
软中断 很高 内核专用,处理大量事件 内核开发者使用,驱动开发者不使用
Tasklet 中等 驱动开发中常用,处理短任务 1. 定义并初始化 Tasklet 对象
2. 在中断上半部分中调度 Tasklet
工作队列 处理复杂、长时间或阻塞任务 1. 定义并初始化 Workqueue 对象
2. 在中断上半部分中调度 Workqueue
Tasklet的使用方法
  1. 定义tasklet_struct用于管理和定义Tasklet
cpp 复制代码
struct tasklet_struct {
    void (*func)(unsigned long data);  // Tasklet 的处理函数
    unsigned long data;                // 传递给处理函数的私有数据
};
  1. 初始化Tasklet_struct
  • tasklet_init() 函数用于初始化 tasklet_struct 结构体,指定 Tasklet 的处理函数和私有数据。
cpp 复制代码
void tasklet_init(struct tasklet_struct *t, 
                  void (*func)(unsigned long), unsigned long data);
  1. 调度Tasklet
  • tasklet_schedule() 用于调度 Tasklet,告诉内核这个 Tasklet 需要执行。
  • tasklet_schedule() 实际上不会立即执行 Tasklet,而是将 Tasklet 标记为可执行的。
  • 当中断处理完成后,内核会检测到 Tasklet 已经被调度,会在稍后的软中断上下文中执行 Tasklet。
cpp 复制代码
void tasklet_schedule(struct tasklet_struct *t);
  1. 注意事项
  • tasklet_kill():在模块卸载时,需要调用 tasklet_kill(),它会确保 Tasklet 完成后才退出,以免在卸载模块时 Tasklet 仍在执行。
  • 软中断上下文:Tasklet 是在软中断上下文中执行的,因此不能进行睡眠操作,也不能执行阻塞的 I/O 操作。
  1. 具体代码
cpp 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>

// Tasklet 处理函数
void my_tasklet_func(unsigned long data)
{
    printk(KERN_INFO "Tasklet 执行,数据: %lu\n", data);
}

// 定义 Tasklet 对象
struct tasklet_struct my_tasklet;

irqreturn_t irq_handler(int irq, void *dev_id)
{
    printk(KERN_INFO "中断发生,调度 Tasklet\n");
    // 调度 Tasklet 执行
    tasklet_schedule(&my_tasklet);
    return IRQ_HANDLED;
}

static int __init my_init(void)
{
    int irq = 19;  // 假设使用中断号 19

    // 初始化 Tasklet,传递处理函数和私有数据
    tasklet_init(&my_tasklet, my_tasklet_func, 100);

    // 注册中断处理程序
    if (request_irq(irq, irq_handler, IRQF_SHARED, "my_tasklet_device", NULL)) {
        printk(KERN_ERR "无法注册中断处理程序\n");
        return -1;
    }

    printk(KERN_INFO "模块加载成功,Tasklet 初始化完成\n");
    return 0;
}

static void __exit my_exit(void)
{
    // 杀掉 Tasklet 确保它已经完成
    tasklet_kill(&my_tasklet);

    // 释放中断
    free_irq(19, NULL);

    printk(KERN_INFO "模块卸载,Tasklet 资源已释放\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Tasklet 示例");
工作队列的使用方法
  1. 工作队列(Workqueue)是 Linux 内核中提供的一种机制,用于将任务推迟到进程上下文中执行。
  2. 与 Tasklet 和软中断不同,工作队列允许进行阻塞操作和睡眠,因此适用于处理更为复杂、耗时的任务,如文件 I/O 或长时间的计算任务。
  3. 工作队列是在内核线程中执行的,运行环境与普通内核线程一致,能够进行各种内核操作。
具体代码
  1. 定义工作队列
    • 工作队列通过 struct work_struct 定义
cpp 复制代码
struct work_struct workqueue_obj;
  1. 初始化工作队列
  • 在模块初始化时,使用 INIT_WORK() 初始化工作队列对象,并关联任务函数 workqueue_handle
cpp 复制代码
INIT_WORK(&gstruct.workqueue_obj, workqueue_handle);
  1. 调度工作队列
  • 在中断处理函数的下半部分 tasklet_fun 中,通过 schedule_work() 来调度工作队列
c 复制代码
schedule_work(&gstruct.workqueue_obj);
  1. 工作队列处理函数
  • 工作队列函数 workqueue_handle 是实际执行任务的地方,它运行在内核线程上下文中,因此可以执行阻塞操作
cpp 复制代码
void workqueue_handle(struct work_struct *work)
{
    struct global_struct *pt_gstruct = container_of(work, struct global_struct, workqueue_obj);

    /* 中断产生数据 */
    pt_gstruct->keycnt++;
    sprintf(pt_gstruct->sharebuf, "hello usrread, keycnt=%d", pt_gstruct->keycnt);

    /* 唤醒等待队列,通知有数据可读 */
    pt_gstruct->data_flag = 1;
    wake_up_interruptible(&pt_gstruct->wqhead);

    /* 发送信号给用户进程,通知数据到达 */
    kill_fasync(&pt_gstruct->fapp, SIGIO, POLL_IN);

    printk("%s-%d irqno=%d\n", __func__, __LINE__, pt_gstruct->irqno);
}
  1. 模块退出时的清理
  • 在模块卸载时,需要确保工作队列中的任务已完成或取消,避免在模块卸载后还有未完成的任务
cpp 复制代码
cancel_work_sync(&gstruct.workqueue_obj);
  1. 代码示例
c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/gpio.h>

/* 假设 GPIO 中断号为 17 */
#define GPIO_IRQ_NUM 17

/* 全局工作队列对象 */
static struct work_struct my_workqueue;

/* 工作队列处理函数 */
void workqueue_func(struct work_struct *work)
{
    printk(KERN_INFO "工作队列任务执行:处理复杂的任务\n");

    /* 模拟耗时操作 */
    msleep(2000);  // 模拟阻塞操作

    printk(KERN_INFO "工作队列任务完成\n");
}

/* 中断处理程序(上半部分) */
irqreturn_t irq_handler(int irq, void *dev_id)
{
    printk(KERN_INFO "中断触发:调度工作队列\n");

    /* 调度工作队列 */
    schedule_work(&my_workqueue);

    return IRQ_HANDLED;
}

/* 模块初始化 */
static int __init my_module_init(void)
{
    int ret;

    /* 初始化工作队列 */
    INIT_WORK(&my_workqueue, workqueue_func);

    /* 注册中断处理程序 */
    ret = request_irq(GPIO_IRQ_NUM, irq_handler, IRQF_SHARED, "my_workqueue_device", &my_workqueue);
    if (ret) {
        printk(KERN_ERR "无法注册中断处理程序\n");
        return ret;
    }

    printk(KERN_INFO "工作队列模块已加载\n");
    return 0;
}

/* 模块卸载 */
static void __exit my_module_exit(void)
{
    /* 确保工作队列任务已完成或被取消 */
    cancel_work_sync(&my_workqueue);

    /* 释放中断 */
    free_irq(GPIO_IRQ_NUM, &my_workqueue);

    printk(KERN_INFO "工作队列模块已卸载\n");
}

/* 模块入口和出口 */
module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("工作队列使用示例");
相关推荐
内核程序员kevin1 小时前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
朝九晚五ฺ5 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream6 小时前
Linux的桌面
linux
xiaozhiwise6 小时前
Makefile 之 自动化变量
linux
意疏8 小时前
【Linux 篇】Docker 的容器之海与镜像之岛:于 Linux 系统内探索容器化的奇妙航行
linux·docker
BLEACH-heiqiyihu8 小时前
RedHat7—Linux中kickstart自动安装脚本制作
linux·运维·服务器
一只爱撸猫的程序猿8 小时前
一个简单的Linux 服务器性能优化案例
linux·mysql·nginx
我的K840910 小时前
Flink整合Hudi及使用
linux·服务器·flink
19004310 小时前
linux6:常见命令介绍
linux·运维·服务器