Linux设备模型

Linux设备模型

笔记学习整理基于野火鲁班猫教程并且添加自己学习后理解的内容然后还有ai的一些总结。如果有说的不好或者不对的地方希望大家指正!!!

在开始之前先讲一下一级指针,二级指针和指针数组。

举例,一级指针是 int a=10; int *p=&a; 那么对于p里面存的就是变量a的地址,*p则是解引用,得到a的值为10。

举例,指针数组和二级指针,有如下代码(先不需要管细节,理解是咋样就好)

#include <stdio.h>

#include <一些其他的头文件>



struct attribute {

    const char *name;

    int mode;

};



int main() {

    // 1. 定义两个属性对象

    struct attribute attr1 = {"version", 0444};

    struct attribute attr2 = {"max_devs", 0444};



    // 2. 指针数组(一级指针的数组,以 NULL 结尾)

    struct attribute *attr_arr[] = {&attr1, &attr2, NULL};



    // 3. 二级指针指向指针数组的首地址

    struct attribute **pp = attr_arr;



    // 遍历:判断二级指针指向的一级指针是否为 NULL

    while (*pp != NULL) { // 解引用二级指针,拿到一级指针的值,判断是否为 NULL

        printf("属性名:%s,权限:%o\n", (*pp)->name, (*pp)->mode);

        pp++; // 二级指针移动到下一个数组元素

    }



    return 0;

}

我们可以看到attr_arr是一个存储多个一级指针的一级指针数组。指针数组的结束都放NULL。数组的名字是第一个数组元素的地址(下标为0),而这个元素是个一级指针,又因为二级指针是一个指向一级指针的指针,所以指针数组可以隐式转换为二级指针。然后下方把指针数组赋值给二级指针,这个二级指针pp存的是指针数组第一个元素的地址,然后*pp对二级指针解引用,得到的就是指针数组的第一个首元素。后续出现指针的++操作,这个操作是根据指针的类型大小占据的字节进行++。例如在代码中,pp++之后会指向指针数组第2个元素(下标为1),因为pp++的时候加的是struct attribute类型大小占据的字节,所以会跳到下一个数组元素。

Linux设备模型

Linux设备模型用来管理所有硬件设备、驱动程序,并建立它们之间的关联关系,同时对外(用户空间)提供标准化的交互接口(如 sysfs)。关键组成是总线-驱动-设备。作用有

  1. 管理硬件与驱动的生命周期
  2. 抽象硬件的共性,简化驱动开发
  3. 向用户空间开放标准化接口
  4. 实现热插拔

在根文件系统中有个/sys文件目录,里面记录各个设备之间的关系。/sys/bus目录下的每个子目录都是注册好了的总线类型。这里是设备按照总线类型分层放置的目录结构, 每个子目录(总线类型)下包含两个子目录——devices和drivers文件夹;其中devices下是该总线类型下的所有设备, 而这些设备都是符号链接,它们分别指向真正的设备(/sys/devices/下)。而drivers下是所有注册在这个总线上的驱动,每个driver子目录下 是一些可以观察和修改的driver参数。

/sys/devices目录下是全局设备结构体系,包含所有被发现的注册在各种总线上的各种物理设备。一般来说, 所有的物理设备都按其在总线上的拓扑结构来显示。/sys/devices是内核对系统中所有设备的分层次表达模型, 也是/sys文件系统管理设备的最重要的目录结构。

/sys/class目录下则是包含所有注册在kernel里面的设备类型,这是按照设备功能分类的设备模型, 我们知道每种设备都具有自己特定的功能,比如:鼠标的功能是作为人机交互的输入,按照设备功能分类无论它 挂载在哪条总线上都是归类到/sys/class/input下。

在总线上管理着两个链表,分别管理着设备和驱动,当我们向系统注册一个驱动时,便会向驱动的管理链表插入我们的新驱动, 同样当我们向系统注册一个设备时,便会向设备的管理链表插入我们的新设备。在插入的同时总线会执行一个bus_type结构体中match的方法对新插入的设备/驱动进行匹配。 (它们之间最简单的匹配方式则是对比名字,存在名字相同的设备/驱动便成功匹配)。 在匹配成功的时候会调用驱动device_driver结构体中probe方法(通常在probe中获取设备资源,具体的功能可由驱动编写人员自定义), 并且在移除设备或驱动时,会调用device_driver结构体中remove方法。

我要开始讲废话了,这里干看看不懂,可以往下翻直奔代码部分看完代码再回来看。

在内核中使用结构体bus_type来表示总线,如下所示:

1、name :指定总线的名称,当新注册一种总线类型时,会在/sys/bus目录创建一个新的目录,目录名就是该参数的值;

2、drv_groups、dev_groups、bus_groups :分别表示驱动、设备以及总线的属性。这些属性可以是内部变量、字符串等等。通常会在对应的/sys目录下在以文件的形式存在,对于驱动而言,在目录/sys/bus/<bus-name>/driver/<driver-name>存放了设备的默认属性;设备则在目录/sys/bus/<bus-name>/devices/<driver-name>中。这些文件一般是可读写的,用户可以通过读写操作来获取和设置这些attribute的值。

3、match :当向总线注册一个新的设备或者是新的驱动时,会调用该回调函数。该回调函数主要负责判断是否有注册了的驱动适合新的设备,或者新的驱动能否驱动总线上已注册但没有驱动匹配的设备;

4、uevent :总线上的设备发生添加、移除或者其它动作时,就会调用该函数,来通知驱动做出相应的对策。

5、probe :当总线将设备以及驱动相匹配之后,执行该回调函数,最终会调用驱动提供的probe函数。

6、remove :当设备从总线移除时,调用该回调函数;

7、suspend、resume :电源管理的相关函数,当总线进入睡眠模式时,会调用suspend回调函数;而resume回调函数则是在唤醒总线的状态下执行;

8、pm :电源管理的结构体,存放了一系列跟总线电源管理有关的函数,与device_driver结构体中的pm_ops有关;

9、p :该结构体用于存放特定的私有数据,其成员klist_devices和klist_drivers记录了挂载在该总线的设备和驱动;

上图是注册和注销总线的函数。

在内核使用device结构体来描述我们的物理设备,如下图:

1、init_name :指定该设备的名称,总线匹配时,一般会根据比较名字,来进行配对;

2、parent :表示该设备的父对象,前面提到过,旧版本的设备之间没有任何关联,引入Linux设备模型之后,设备之间呈树状结构,便于管理各种设备;

3、bus :表示该设备依赖于哪个总线,当我们注册设备时,内核便会将该设备注册到对应的总线。

4、of_node :存放设备树中匹配的设备节点。当内核使能设备树,总线负责将驱动的of_match_table以及设备树的compatible属性进行比较之后,将匹配的节点保存到该变量。

5、platform_data :一个指针,用于保存具体的平台相关的数据。具体的driver模块,可以将一些私有的数据,暂存在这里,需要使用的时候,再拿出来,因此设备模型并不关心该指针得实际含义。

6、driver_data :同上,驱动层可通过dev_set/get_drvdata函数来获取该成员;

7、class :指向了该设备对应类,开篇我们提到的触摸,鼠标以及键盘等设备,对于计算机而言,他们都具有相同的功能,都归属于输入设备。我们可以在/sys/class目录下对应的类找到该设备,如input、leds、pwm等目录;

8、dev :dev_t类型变量,字符设备章节提及过,它是用于标识设备的设备号,该变量主要用于向/sys目录中导出对应的设备。

9、release :回调函数,当设备被注销时,会调用该函数。如果我们没定义该函数时,移除设备时,会提示“Device ‘xxxx’ does not have a release() function, it is broken and must be fixed”的错误。

9、group :指向struct attribute_group类型的指针,指定该设备的属性;

10、p :是私有数据结构指针,该指针中会保存子设备链表、用于添加到bus/driver/prent等设备中的链表头等等。

上图是设备注册和注销的函数。

在内核中,使用device_driver结构体来描述我们的驱动,如下所示:

1、name :指定驱动名称,总线进行匹配时,利用该成员与设备名进行比较;

2、bus :表示该驱动依赖于哪个总线,内核需要保证在驱动执行之前,对应的总线能够正常工作;

3、suppress_bind_attrs :布尔量,用于指定是否通过sysfs导出bind与unbind文件,bind与unbind文件是驱动用于绑定/解绑关联的设备。

4、owner :表示该驱动的拥有者,一般设置为THIS_MODULE;

5、of_match_table :指定该驱动支持的设备类型。当内核使能设备树时,会利用该成员与设备树中的compatible属性进行比较。

6、remove :当设备从操作系统中拔出或者是系统重启时,会调用该回调函数;

7、probe :当驱动以及设备匹配后,会执行该回调函数,对设备进行初始化。通常的代码,都是以main函数开始执行的,但是在内核的驱动代码,都是从probe函数开始的。

8、group :指向struct attribute_group类型的指针,指定该驱动的属性;

上图是驱动注册和注销的函数。

整体大致流程如下:

正片开始------属性文件

属性文件(即 sysfs 中的属性文件)是 Linux 内核与用户空间交互的核心接口之一,核心好处是让内核态与用户态之间的 “数据交换、状态控制” 变得标准化、安全且灵活,具体体现在以下几点:

  1. 实现 “内核 - 用户空间” 的安全交互
  2. 标准化交互方式,降低开发 / 使用成本
  3. 灵活暴露内核状态与配置
  4. 支持自动化与调试

属性文件分为总线属性文件、设备属性文件、驱动属性文件。最关键的基础是attribute属性文件。

attribute属性文件:

/sys目录有各种子目录以及文件,前面讲过当我们注册新的总线、设备或驱动时,内核会在对应的地方创建一个新的目录,目录名为各自结构体的name成员,如果是注册总线则会有/sys/bus/<bus_name> 。如果是设备则会有/sys/bus/<bus_name>/devices/<device_name>。如果是驱动则会有/sys/bus/<bus_name>/ drivers /<driver_name>。每个子目录下的文件,都是内核导出到用户空间,用于控制我们的设备的。内核中以attribute结构体来描述/sys目录下的文件,如下所示:

后续我们会经常用到这个结构体相关的内容。

总线属性文件:

总线属性文件的结构体是struct bus_attribute,里面包含struct attribute用于描述当前要注册的总线属性文件的名称,同时有show回调函数,通过cat操作总线的属性文件则会触发。Store回调函数则通过echo操作总线的属性文件则会触发。BUS_ATTR是一个宏定义,但是他相当于一个初始化struct bus_attribute的操作。创建结构体并且对里面的变量赋值。bus_attr_##_name代表bus_attr_和_name会拼接成一个字符串,bus_attr_是固定前缀不可替换。_name是可以替换的,具体看你__ATTR(_name, _mode, _show, _store)的_name填写什么,如果是aaa,则最后struct bus_attribute结构体的名称将为bus_attr_aaa。_name填写属性文件的名称(version?debug?等),_mode填写权限,一般写0644或者0444。_show则是我们驱动开发自定义的函数,_store同理。但是BUS_ATTR只是初始化结构体并赋值,没有真正把文件创建到/sys对应目录中,下方的bus_create_file和bus_remove_file分别负责创建属性文件到对应目录(如/sys/bus/<bus_name>的目录下创建)和从对应目录删除文件。

设备属性文件:

结构和总线属性文件结构基本一样。按照总线文件属性的理解方式来理解设备属性文件就可以。但是创建属性文件的目录可能是/sys/bus/<bus_name>/devices/<device_name>。

驱动属性文件:

依旧是结构体结构差不多。按照总线文件属性的理解方式来理解设备属性文件就可以。但是创建属性文件的目录可能是/sys/bus/<bus_name>/drivers/< drivers _name>。并且我们发现这个驱动的宏定义实现和上述两个的不一样。这里只传了name,但是以前的都是传4个参数。实际上系统会自己处理完我们只传_name就可以(需要注册的驱动属性文件名称)。_mode根据调用宏来决定是0644.0444还是0244,那_show和_store回调函数呢?那如何设置这两个参数呢?在写驱动代码时,只需要你提供xxx_store以及xxx_show这两个函数, 并确保两个函数的xxx和DRIVER_ATTR类型的宏定义中名字是一致的即可。举例:version_show和version_store我自己定义了,同时DRIVER_ATTR_RW(version)。这样_name是version,_mode是0644,_show,_store分别是version_show和version_store。

代码举例讲解(这里没有写全头文件,我晚点再整理一整份可供测试的代码)

简单总结就是创建、导出属性文件、注册。

一、初始化自定义总线

int xbus_match(struct device *dev, struct device_driver *drv)

{

    printk("%s-%s\n",__FILE__, __func__);

    if(!strncmp(dev_name(dev), drv->name, strlen(drv->name))){

        printk("dev & drv match\n");

        return 1;

    }

    return 0;

}



static struct bus_type xbus = {

    .name = "xbus",

    .match = xbus_match,

};

EXPORT_SYMBOL(xbus);

1、__FILE__是一整个当前文件的路径,完整的,__func__是当前的函数是什么

2、xbus要导出,不然device和driver的文件没办法绑定总线

3、获取设备dev形参名称不能用dev->init_name,init_name 是 struct device 中用于初始化的临时名称字段,仅在设备初始化阶段有效,设备注册完成(device_register)后,init_name 会被内核标记为无效(比如置为 NULL)。而 bus_match 执行时该字段可能已失效 / 被清空,且内核规范明确禁止在初始化后使用 init_name。

二、导出总线属性文件

static char *bus_name = "xbus";



ssize_t xbus_test_show(struct bus_type *bus, char *buf)

{

    return sprintf(buf, "%s\n", bus_name);

}



BUS_ATTR(xbus_test, S_IRUSR, xbus_test_show, NULL);



三、注册总线

static __init int xbus_init(void)

{

    printk("xbus init\n");



    bus_register(&xbus);

    bus_create_file(&xbus, &bus_attr_xbus_test);

    return 0;

}

module_init(xbus_init);





static __exit void xbus_exit(void)

{

    printk("xbus exit\n");

    bus_remove_file(&xbus, &bus_attr_xbus_test);

    bus_unregister(&xbus);

}

module_exit(xbus_exit);



MODULE_AUTHOR("embedfire");

MODULE_LICENSE("GPL");
  1. bus_register会创建/sys/bus/xbus/和/sys/bus/xbus/uevent
  2. bus_create_file会在对应目录下创建总线属性文件
  3. 注销的时候先removefile在unregister

四、使用insmod命令加载xbus内核模块:

sudo insmod xbus.ko

五、定义新的设备

extern struct bus_type xbus;



void xdev_release(struct device *dev)

{

    printk("%s-%s\n", __FILE__, __func__);

}





static struct device xdev = {

    .init_name = "xdev",

    .bus = &xbus,

    .release = xdev_release,

};
  1. release函数一定要实现如果不实现,内核会在设备销毁时触发崩溃。struct device 中的 release 函数是内核设备模型的强制要求,核心原因与 kobject(内核对象)的生命周期管理有关。struct device 内部包含 struct kobject kobj,而 kobject 是内核对象模型的核心,采用引用计数(kref) 管理生命周期。当设备被注册(device_register)、被其他对象引用时,引用计数增加;当引用计数减为 0 时,内核会调用 kobject 关联的 release 函数(即 device->release),完成最终的资源清理,如果此时release没有被实现那就糟糕了。且代码的release仅供测试并不全面且不规范。
  2. struct device 中init_name在设备初始化完全后会被置为null。挂载对应总线,绑定release函数。

六、导出设备属性文件

unsigned long id = 0;

ssize_t xdev_id_show(struct device *dev, struct device_attribute *attr,

                char *buf)

{

    return sprintf(buf, "%d\n", id);

}



ssize_t xdev_id_store(struct device *dev, struct device_attribute *attr,

                const char *buf, size_t count)

{

    kstrtoul(buf, 10, &id);

    return count;

}





DEVICE_ATTR(xdev_id, S_IRUSR|S_IWUSR, xdev_id_show, xdev_id_store);
  1. kstrtoul(buf, 10, &id);第一个参数是从形参获得的一个缓冲区,存储着用户的echo的输入,第二个参数是填写进制,此处10进制。第三个参数是把buf内容转换后10进制存在第三个参数中。
  2. xdev_id_show 中的 buf是内核在处理用户读操作(如cat)时,自动分配的内核态缓冲区。
  3. xdev_id_store 中的 buf是内核将用户写操作(如echo)传入的数据,从用户态拷贝到内核态的缓冲区。

七、注册设备

static __init int xdev_init(void)

{

    printk("xdev init\n");

    device_register(&xdev);

    device_create_file(&xdev, &dev_attr_xdev_id);

    return 0;

}

module_init(xdev_init);





static __exit void xdev_exit(void)

{

    printk("xdev exit\n");

    device_remove_file(&xdev, &dev_attr_xdev_id);

    device_unregister(&xdev);

}

module_exit(xdev_exit);



MODULE_AUTHOR("embedfire");

MODULE_LICENSE("GPL");
  1. device_register:第一,把设备添加到 xbus 总线的设备链表中,成为 xbus 总线下的一个设备且关联设备与总线的核心回调;第二,创建设备根目录/sys/devices/xdev/,创建总线关联的符号链接:/sys/bus/xbus/devices/xdev/ → 指向 /sys/devices/xdev/,让总线能找到该设备;
  2. device_create_file为已注册的 xdev 设备,在其 sysfs 目录下创建指定的 xdev_id 属性文件(dev_attr_xdev_id因为后缀是xdev_id)。
  3. 注销的时候先removefile在unregister。

八、使用insmod命令加载xdev内核模块

sudo insmod xdev.ko

九、定义新的驱动

extern struct bus_type xbus;



int xdrv_probe(struct device *dev)

{

    printk("%s-%s\n", __FILE__, __func__);

    return 0;

}



int xdrv_remove(struct device *dev)

{

    printk("%s-%s\n", __FILE__, __func__);

    return 0;

}



static struct device_driver xdrv = {

    .name = "xdev",

    .bus = &xbus,

    .probe = xdrv_probe,

    .remove = xdrv_remove,

};
  1. 驱动probe函数将会在设备去驱动匹配的时候被调用。

十、导出驱动属性文件

char *name = "xdrv";

ssize_t drvname_show(struct device_driver *drv, char *buf)

{

    return sprintf(buf, "%s\n", name);

}



DRIVER_ATTR_RO(drvname);

1、_mode是0444。

2、DRIVER_ATTR_RO(drvname);执行绑定show函数的时候,一定要前缀与drvname一致。且该代码没有实现store,因为是ro(readonly)。

十一、注册驱动

static __init int xdrv_init(void)

{

    printk("xdrv init\n");

    driver_register(&xdrv);

    driver_create_file(&xdrv, &driver_attr_drvname);

    return 0;

}

module_init(xdrv_init);



static __exit void xdrv_exit(void)

{

    printk("xdrv exit\n");

    driver_remove_file(&xdrv, &driver_attr_drvname);

    driver_unregister(&xdrv);

}

module_exit(xdrv_exit);



MODULE_AUTHOR("embedfire");

MODULE_LICENSE("GPL");

1、driver_register:第一,校验驱动的合法性:检查驱动关联的总线(如 xbus)是否已注册,驱动的核心回调(如 probe/remove)是否合法。第二,把驱动添加到 xbus 总线的驱动链表中(xbus->p->drivers),成为 xbus 总线下的一个驱动;关联驱动与总线的匹配规则(即定义的 xbus_match 函数),为后续设备 - 驱动匹配做准备。第三,创建驱动根目录:/sys/bus/xbus/drivers/xdrv/(名称为驱动的 name 字段)。第四,创建标准控制文件:自动生成 bind/unbind/uevent 文件:

bind:手动绑定设备到驱动(写入设备名触发);

unbind:手动解绑设备与驱动;

uevent:驱动的热插拔事件文件

2、 driver_create_file为已注册的 xdev 设备,在其 sysfs 目录下创建指定的属性文件。

3、 注销的时候先removefile在unregister。

十二、使用insmod命令加载xdrv内核模块

sudo insmod xdrv.ko

使用命令 dmesg | tail 来查看模块加载过程的打印信息,当我们加载完设备和驱动之后,总线开始进行匹配,执行match函数, 发现这两个设备的名字是一致的,就将设备和驱动关联到一起,最后会执行驱动的probe函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值