书接上回:Linux驱动开发---创建总线,创建属性文件-CSDN博客
创建完总线,就可以进行本次实验了
文章目录
前备知识
在 Linux 内核编程中,EXPORT_SYMBOL_GPL
用于将符号(例如变量、函数)导出到内核符号表中,使其可以被其他内核模块或驱动程序引用和使用。具体来说:
EXPORT_SYMBOL_GPL(symbol_name)
:将指定的符号(如结构体、函数等)导出,并且只允许那些以 GPL 许可证发布的模块使用该符号。这意味着该符号只能被 GPL 兼容的模块引用和使用。
导出符号的机制使得一个模块可以将某些符号公开给其他模块使用,而不需要将所有的实现都暴露出来。这对于模块化开发和代码复用非常有用。
如何引用导出的符号
如果一个符号通过 EXPORT_SYMBOL_GPL
导出,在另一个模块中可以直接使用这个符号,而无需显式地声明或定义它。为了使用导出的符号:
- 确保使用该符号的模块也使用 GPL 兼容的许可证。
- 在使用符号的模块中包含相关的头文件(如果符号在头文件中声明了)。
- 直接在代码中使用这个符号,编译器会自动解析并链接到导出的符号。
在总线下注册设备
内核中,已经提供了一个API供开发者来调用,具体为device_register
device_register 函数解析
device_register
是 Linux 内核中用于注册设备的函数。它将一个设备结构体 (struct device
) 注册到系统中,使其成为内核设备模型的一部分。设备模型用于管理和组织设备与驱动程序的关系,并提供设备的层次结构和电源管理等功能。
主要功能包括:
- 初始化设备 :
- 调用
device_initialize
函数来初始化设备结构体。这包括为设备分配引用计数、初始化设备锁、设置设备状态等。
- 调用
- 添加设备 :
- 调用
device_add
函数将设备添加到内核设备模型中。device_add
会将设备插入到设备树(或设备层次结构)中,并执行设备的其他初始化操作,如为设备创建 sysfs 节点、触发uevent
以通知用户空间有新设备加入等。
- 调用
- 错误处理 :
- 如果在设备添加过程中发生错误,
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
是否有效,如果drv
为NULL
或者它的某些关键字段(如name
和bus
)未被正确设置,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. 延迟匹配
如果在设备加载时,尚无与之匹配的驱动程序,设备会处于未绑定状态,等待驱动程序的加载。同样地,如果驱动程序加载时,尚无与之匹配的设备,驱动程序也会处于未绑定状态,直到设备加载并匹配成功。这种机制被称为"延迟匹配",确保设备和驱动可以在合适的时间进行匹配。