Linux驱动开发—在自己总线下注册设备和驱动

书接上回:Linux驱动开发---创建总线,创建属性文件-CSDN博客

创建完总线,就可以进行本次实验了

文章目录

前备知识

在 Linux 内核编程中,EXPORT_SYMBOL_GPL 用于将符号(例如变量、函数)导出到内核符号表中,使其可以被其他内核模块或驱动程序引用和使用。具体来说:

  • EXPORT_SYMBOL_GPL(symbol_name):将指定的符号(如结构体、函数等)导出,并且只允许那些以 GPL 许可证发布的模块使用该符号。这意味着该符号只能被 GPL 兼容的模块引用和使用。

导出符号的机制使得一个模块可以将某些符号公开给其他模块使用,而不需要将所有的实现都暴露出来。这对于模块化开发和代码复用非常有用。

如何引用导出的符号

如果一个符号通过 EXPORT_SYMBOL_GPL 导出,在另一个模块中可以直接使用这个符号,而无需显式地声明或定义它。为了使用导出的符号:

  1. 确保使用该符号的模块也使用 GPL 兼容的许可证。
  2. 在使用符号的模块中包含相关的头文件(如果符号在头文件中声明了)。
  3. 直接在代码中使用这个符号,编译器会自动解析并链接到导出的符号。

在总线下注册设备

内核中,已经提供了一个API供开发者来调用,具体为device_register

device_register 函数解析

device_register 是 Linux 内核中用于注册设备的函数。它将一个设备结构体 (struct device) 注册到系统中,使其成为内核设备模型的一部分。设备模型用于管理和组织设备与驱动程序的关系,并提供设备的层次结构和电源管理等功能。

主要功能包括

  1. 初始化设备 :
    • 调用 device_initialize 函数来初始化设备结构体。这包括为设备分配引用计数、初始化设备锁、设置设备状态等。
  2. 添加设备 :
    • 调用 device_add 函数将设备添加到内核设备模型中。device_add 会将设备插入到设备树(或设备层次结构)中,并执行设备的其他初始化操作,如为设备创建 sysfs 节点、触发 uevent 以通知用户空间有新设备加入等。
  3. 错误处理 :
    • 如果在设备添加过程中发生错误,device_register 会执行适当的清理操作,以确保没有资源泄漏。

函数定义:

int device_register(struct device *dev);

参数说明:

dev : 这是一个指向 struct device 结构体的指针,表示要注册的设备。该结构体包含了设备的基本信息,如设备名称、设备所属的总线、父设备、设备类等。

使用示例

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/device.h>
// my_bus 模块中的bus_type 已经导出到了内核符号表,在这里可以直接调用
extern struct bus_type my_bus_type;
void device_release(struct device *dev){
    printk("This is device release");
}
struct device mydev = {
    .init_name = "my_device",
    .bus = &my_bus_type,
    .release = device_release,
    .devt = ((255<<20|0)),
};
static int __init my_bus_device_init(void)
{
    int ret;
    // 往系统中注册一个设备结构体
    device_register(&mydev);
    return ret;
}
static void __exit my_bus_device_exit(void)
{
    device_unregister(&mydev);

}
module_init(my_bus_device_init);
module_exit(my_bus_device_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example of bus registration");
关键点:

device结构体中包括了众多字段,一般只需要关注:名称,所属于的总线,释放函数,设备号...

其中所属的总线,上文曾提到注册总线的方法,总线模块中已经包含了bus_type, 设备类中没必要再写一遍,可以导出bus_type内核符号,让设备模块进行调用即可。

导出方式EXPORT_SYMBOL_GPL

//注册总线的关键结构体
struct bus_type my_bus_type = {
    .name = "my_bus",
    .match = my_bus_match,
    .probe = my_bus_probe,
};
EXPORT_SYMBOL_GPL(my_bus_type);

引用方式: extern struct bus_type my_bus_type;

因此,一定要先加载总线模块,再加载设备模块,才能调用成功

实验结果

注:在sys目录查看某些文件,需要全路径,才能查看。

在总线下注册驱动

内核中,已经提供了一个API供开发者来调用,具体为driver_register

driver_register 函数解析

它是内核设备驱动模型中的核心函数之一,用于将驱动程序与内核设备模型关联起来,并将其挂载到对应的总线上。

函数定义

int driver_register(struct device_driver *drv);

主要功能:

1.检查参数有效性

  • 检查传入的 drv 是否有效,如果 drvNULL 或者它的某些关键字段(如 namebus)未被正确设置,driver_register 会返回错误。

2.初始化驱动程序结构体

  • 初始化 drv->kobj,它是用于内核对象模型 (kobject) 管理的内核对象。这一步是为了将驱动程序挂载到内核的 sysfs 文件系统中,便于用户空间和内核空间的交互。

3.将驱动程序添加到对应总线

  • 通过调用 bus_add_driver 函数,将驱动程序添加到它所属的总线(drv->bus)上。每个总线都有一组已注册的驱动程序,这一步将驱动程序加入到总线的驱动程序列表中。

4.触发设备与驱动的匹配

  • 在驱动程序被成功添加到总线后,内核会尝试匹配总线上已经存在的设备与新注册的驱动程序。如果匹配成功,则调用驱动程序的 probe 函数来初始化设备。

参数说明:

传入的参数为struct device_driver,主要字段有 名称,probe函数,remove函数,所在的总线...

使用示例

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/device.h>
// my_bus 模块中的bus_type 已经导出到了内核符号表,在这里可以直接调用
extern struct bus_type my_bus_type;


static int my_drv_probe (struct device *dev){
    printk("This is my driver probe function\n");
    return 0;
};
static int my_drv_remove (struct device *dev){
    printk("This is my driver remove function\n");
    return 0;
}

struct  device_driver my_device_drv =  {
    .name = "my_device", //与设备名称保持一致。 因为需要根据名称进行匹配。
    .bus = &my_bus_type,
    .probe = my_drv_probe,
    .remove = my_drv_remove,
};
static int __init my_bus_drv_init(void)
{
    int ret;
    ret = driver_register(&my_device_drv);
    return ret;
}

static void __exit my_bus_drv_exit(void)

{
    driver_unregister(&my_device_drv);
}
module_init(my_bus_drv_init);
module_exit(my_bus_drv_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Marxist");
MODULE_DESCRIPTION("A simple example of bus drv registration");

实验结果

看到指向设备的符号链接,这表明该驱动已经与设备成功匹配并绑定。

总线,设备,设备驱动 三者完整代码

总线:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/device.h>
//进行名称匹配
static int my_bus_match (struct device *dev, struct device_driver *drv){
    printk("match success\n");
    return  (strcmp(dev_name(dev),drv->name)==0);
}
static int my_bus_probe(struct device *dev){
    printk("new device probe success\n");
    struct device_driver *drv = dev->driver;    
    if(drv->probe){
        drv->probe(dev);
    }
    return 0;
}

//注册总线的关键结构体
struct bus_type my_bus_type = {
    .name = "my_bus",
    .match = my_bus_match,
    .probe = my_bus_probe,
};
EXPORT_SYMBOL_GPL(my_bus_type);
// 在总线目录下创建属性文件 与 kobject 创建属性文件 类似
static ssize_t my_bus_show(struct bus_type *bus, char *buf) {
    return sprintf(buf, "Hello from the bus attribute!\n");
}

static ssize_t my_bus_store(struct bus_type *bus, const char *buf, size_t count) {
    pr_info("Received from userspace: %s\n", buf);
    return count;
}
static BUS_ATTR(my_bus_attr, 0664, my_bus_show, my_bus_store);
static int __init my_bus_init(void)
{
    int ret;
    // 注册总线
    ret = bus_register(&my_bus_type);
    if (ret)
        return ret;

    // 添加属性文件到总线
    ret = bus_create_file(&my_bus_type, &bus_attr_my_bus_attr);
    if (ret)
        bus_unregister(&my_bus_type);

    return ret;
}

static void __exit my_bus_exit(void)

{
       // 移除属性文件
    bus_remove_file(&my_bus_type, &bus_attr_my_bus_attr);
    bus_unregister(&my_bus_type);
}
module_init(my_bus_init);
module_exit(my_bus_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example of bus registration");

设备

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/device.h>
// my_bus 模块中的bus_type 已经导出到了内核符号表,在这里可以直接调用
extern struct bus_type my_bus_type;

void device_release(struct device *dev){
    printk("This is device release");
}
struct device mydev = {
    .init_name = "my_device",
    .bus = &my_bus_type,
    .release = device_release,
    .devt = ((255<<20|0)),
};

static int __init my_bus_device_init(void)
{
    int ret;
    // 往系统中注册一个设备结构体
    device_register(&mydev);
    return ret;
}

static void __exit my_bus_device_exit(void)

{
    device_unregister(&mydev);

}
module_init(my_bus_device_init);
module_exit(my_bus_device_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example of bus registration");

驱动

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/device.h>
// my_bus 模块中的bus_type 已经导出到了内核符号表,在这里可以直接调用
extern struct bus_type my_bus_type;


static int my_drv_probe (struct device *dev){
    printk("This is my driver probe function\n");
    return 0;
};
static int my_drv_remove (struct device *dev){
    printk("This is my driver remove function\n");
    return 0;
}

struct  device_driver my_device_drv =  {
    .name = "my_device", //与设备名称保持一致。 因为需要根据名称进行匹配。
    .bus = &my_bus_type,
    .probe = my_drv_probe,
    .remove = my_drv_remove,
};
static int __init my_bus_drv_init(void)
{
    int ret;
    ret = driver_register(&my_device_drv);
    return ret;
}

static void __exit my_bus_drv_exit(void)

{
    driver_unregister(&my_device_drv);
}
module_init(my_bus_drv_init);
module_exit(my_bus_drv_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Marxist");
MODULE_DESCRIPTION("A simple example of bus drv registration");

注:先加载设备 或者先加载驱动都可以

加载设备和加载驱动没有先后顺序

在 Linux 内核中,设备和驱动的加载顺序可以任意(即设备可以先加载,驱动也可以先加载),这得益于内核设备驱动模型的设计灵活性

1. 设备与驱动的动态匹配

无论是设备还是驱动,内核都会在合适的时机进行匹配和绑定:

  • 设备先加载
    • 如果设备先加载,内核会将设备添加到设备列表中并挂载到对应的总线下。
    • 当驱动稍后加载时,内核会遍历总线上的所有设备,尝试匹配这些设备和新加载的驱动程序。如果匹配成功,会调用驱动的 probe 函数来初始化设备。
  • 驱动先加载
    • 如果驱动先加载,内核会将驱动添加到驱动列表中并挂载到对应的总线下。
    • 当设备稍后加载时,内核会遍历总线上的所有驱动程序,尝试匹配这些驱动程序和新加载的设备。如果匹配成功,会同样调用驱动的 probe 函数来初始化设备。

2. 总线的 match 机制

每个总线都有自己的 match 机制,通常由总线的 match 函数实现(例如,I2C 总线、SPI 总线等)。这个 match 函数负责根据设备和驱动的属性(如名字、ID等)进行匹配。一旦匹配成功,设备和驱动就会绑定,并调用 probe 函数。

这种 match 机制的灵活性保证了不论设备和驱动的加载顺序,匹配和绑定都可以在任何一方加载完成后自动完成。

3. 延迟匹配

如果在设备加载时,尚无与之匹配的驱动程序,设备会处于未绑定状态,等待驱动程序的加载。同样地,如果驱动程序加载时,尚无与之匹配的设备,驱动程序也会处于未绑定状态,直到设备加载并匹配成功。这种机制被称为"延迟匹配",确保设备和驱动可以在合适的时间进行匹配。

相关推荐
Zfox_5 分钟前
【Linux】进程信号全攻略(二)
linux·运维·c语言·c++
安於宿命10 分钟前
【Linux】简易版shell
linux·运维·服务器
黄小耶@22 分钟前
linux常见命令
linux·运维·服务器
叫我龙翔23 分钟前
【计网】实现reactor反应堆模型 --- 框架搭建
linux·运维·网络
古驿幽情25 分钟前
CentOS AppStream 8 手动更新 yum源
linux·运维·centos·yum
BillKu26 分钟前
Linux(CentOS)安装 Nginx
linux·运维·nginx·centos
BillKu29 分钟前
Linux(CentOS)yum update -y 事故
linux·运维·centos
a2663789634 分钟前
解决yum命令报错“Could not resolve host: mirrorlist.centos.org
linux·运维·centos
2739920292 小时前
Ubuntu20.04 安装build-essential问题
linux
wowocpp5 小时前
查看 linux ubuntu 分区 和 挂载 情况 lsblk
linux·运维·ubuntu