基于diagnose-tools 学习字符设备
发布日期:2021-09-16 04:36:46 浏览次数:3 分类:技术文章

本文共 9017 字,大约阅读时间需要 30 分钟。

前言

这段时间在处理一个diagnose-tools 工具的一个问题,发现diagnose-tools 会在某些情况导致下导致内核模块加载失败, 在测试中发现原来 /sys/class 这个目录都没有,导致后面的模块加载失败了。

通过这个issue吧,算是初步理解了字符设备的使用和背后的原理吧,字符设备看起来使用简单,创建一个cdev,一个class, 一个device 然后关联起来就可以了,实际上背后的逻辑还是有点复杂了,牵涉到了 Linux 设备驱动管理这一大块知识点,下面我把针对字符设备的相关内容整理梳理下。

 

1、设备模型分析

sysfs 文件系统

sysfs 文件系统是一个类似于proc的基于内存的文件系统,用于将内核中的设备组织成层次化的结构,并把内核设备的详细数据以用户态的文件形式层次化的展示出来。其顶层目录主要有:

block 目录:  包含所有的块设备

device 目录: 包含系统所有设备,并根据设备挂接的总线类型来层次化展示。

bus 目录: 包含系统中所有的总线类型

drivers 目录: 包含系统中所有已注册的设备驱动

class 目录: 系统中的设备类型(如磁盘,网卡,声卡)

sysfs文件系统是一个虚拟的并且存在内存中的,所以重启后,就会丢失,它提供了一些方法,可以把内核中的数据结构、属性和用户空间相互链接。mount -t sysfs sysfs /sys 来把 sysfs 文件系统挂载到根目录下的 sys 目录,当然也可以手动挂载到其它目录。

总线、设备、驱动、类之间关系如图下所示:

图1:总线、设备、驱动、类之间关系

 

       关键数据结构

       kobject, kset

       kobject

       在sysfs里显示的每一个目录都对应一个 kobject ,sysfs目录下的树状结构是通过kobject建立连接的, kobject通过大量的链接构成一个多层次的体系结构,kobject的层次结构可以说是代表了linux设备模型,是linux设备模型的基本结构。

struct kobject {
const char
*name;
   //设备名称
struct list_head
entry;
   //用于kobject之间组成双向链表,与该kobject所属的kset里的list形成双向链表
struct kobject
*parent;
 //指向父对象
struct kset
*kset;
   //指向所属kset的指针
struct kobj_type
*ktype;
  //kobject类型结构体,包含kobject属性和销毁方法等
struct kernfs_node
*sd;
 //在sysfs中的路径
struct kref
kref;
//引用计数,当为0的时候,就会被删除
unsigned int state_initialized:1; //初始化状态,同时对kobject操作进步异常验证
unsigned int state_in_sysfs:1;
//在sysfs中kobject的状态
unsigned int state_add_uevent_sent:1;
//记录用户态上报的状态,仅记录是否上报,上报后是否被接收不是该字段关心的
unsigned int state_remove_uevent_sent:1;  //同上
unsigned int uevent_suppress:1;
   //如果该字段为1,表示忽略所有上报的uevent事件};

       其中的kref域表示该对象引用的计数,内核通过kref实现对象引用计数管理,内核提供两个函数kobject-get()、kobject-put(),分别用于增加和减少引用计数,当引用计数为0时,所有该对象使用的资源将被释放。

       在创建kobject的时候,会通过 kobject_add_internal 方法建立 kobject 与 kset 之间的连接,在这里还是对 parent 的设置,如果没有传入parent,那么该kobject的parent就设置为 kset 的 kobj,在这里 kset 的作用出现了。

static int kobject_add_internal(struct kobject *kobj){
struct kobject *parent;
parent = kobject_get(kobj->parent);
/* join kset if set, use it as parent if we do not already have one */
if (kobj->kset) {
if (!parent)
parent = kobject_get(&kobj->kset->kobj);
kobj_kset_join(kobj);
kobj->parent = parent;
}
在这里还是对 parent 的设置,如果没有传入parent,那么该kobject的parent就设置为 kset 的 kobj,在这里 kset 的作用出现了。
........}

       cdev

 对于kobject结构体,单独使用意义不大,linux通过将object嵌入到某个结构体中,这样这些结构体就能用到kobject结构体,比如字符设备 <linux/cdev.h> 中的 struct cdev 结构体,一个 cdev 结构体代表着一个字符设备。

struct cdev {
struct kobject kobj;
//嵌入kobject 结构体
struct module *owner;   //所属模块
const struct file_operations *ops;  //file operation
struct list_head list;
dev_t dev;
unsigned int count;};

把 kobject嵌入到某个结构体中后,它就拥有了kobject的标准功能。更重要的是,拥有了kobj成员可以便其成为设备模型架构对象中的一部分。通过 cdev->kobj.parent 指向所属 kset->kobj,并且把 cdev->kobj.entry和 cdev->kobj.kset.list 建立链接,使其建立层次结构。

       kset

kobject 通常通过 kset 组织成层次化的结构,kset 是具有相同类型的 kobject 的集合。

struct kset {
struct list_head list;   //这是一个链表头,和属于kset的kobject组成双向链表
spinlock_t list_lock;
//自旋锁,用于访问时链表时同步
struct kobject kobj;
 //内嵌的kobject对象,所有属于这个kset的kobject的parent都指向这个内嵌的kobj, 此外,kset还依赖kobj维护引用计数,kset的引用计数实际上就是内嵌kobj的引用计数
const struct kset_uevent_ops *uevent_ops; //kobject创建或被删除时会产生事件,kobject所属的kset过滤事件或给用户空间传递信息};

 

图2 kobject和kset 对应关系

 

    设备模型组件

          devices 组件

              系统中任一设备类型都由一个struct device 结构体来描述。同时又向sysfs注册一个kset,即devices (/sys/devices),这个就表示系统中所有设备所在的kset。

struct device {
struct device
*parent;
 //指向父设备,在字符设备里这个值为空,不用关注
struct kobject kobj;
//内嵌一个kobject对象
const char
*init_name;
 /* initial name of the device */
const struct device_type *type;
struct bus_type
*bus;
/* type of bus device is on */
struct device_driver *driver;
/* which driver has allocated this
   device */
u32
id;
/* device instance */
struct class
*class;
//指向设备所属的类
void (*release) (struct device *dev);   //释放设备时所调用的方法
.......};

           classes 组件

系统中的设备类由 struct class 描述,表示某一类设备,对应于sysfs文件系统中的 /sys/class 目录。

class 是一个设备的高层视图,它抽象出了底层的实现细节,从而允许用户空间使用设备所提供的功能,而不用关心设备是如何工作的。在class中,设备是按功能来归类,比如网卡,网卡,磁盘。很多设备并不属于某个总线,比如我们的字符设备就不属于某个总线,所以我们这里也不介绍总线相关的知识,不属于总线的设备,在管理方面不同于属于总线的设备。虽然不属于总线,但是字符设备归属于某个确定的class。

struct class {
const char
*name;
  //类名
struct module
*owner;
 //模块名
struct kobject
*dev_kobj;   //内嵌kobject,表示该类设备的引用计数管理
void (*class_release)(struct class *class);   //释放class调用函数,在创建class时指定
void (*dev_release)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
struct subsys_private *p;};

 

其中 struct subsys_private *p 是代表类的集合

struct subsys_private {
struct kset subsys;
struct list_head interfaces;   //interfaces是一个链表头,用来链接class上的各个接口,字符设备这里相当于是空的。
struct class *class;
.......};

类的创建会在 sysfs 文件系统的 class 目录下创建一个该类名的目录。

下面说明了设备模型对设备的管理

图3 设备模型

 

2、字符设备驱动开发

      字符设备驱动重要的数据结构

         字符设备的特点是以字节为单位,顺序传输,键盘,鼠标以及打印机都是属于字符设备,都可以用文件的形式表达出来,所以它的关键数据结构也和文件差不多,它们分别是file_operations, file, inode,以文件的方式提供给用户空间使用。这里不细列这些普通的文件类数据结构了。

      字符设备的接口

      字符设备使用struct cdev 结构体描述字符设备, struct cdev 定义在文件 linux/cdev.h 中

struct cdev {
struct kobject kobj;   //内嵌的kobject对象,用于对该设备的线用计数
struct module *owner;  //所属模块指针
const struct file_operations *ops;  //文件操作结构体指针,该设备操作对应的VFS的接口函数
struct list_head list;  
dev_t dev;   //设备号,一个32位数据,高12位为主设备号,低20位为次设备号
unsigned int count;};

      cdev里的kobj->parent,在cdev里没有赋值,是个空0x0,和struct class里的kobj->parent 指向 kset->kobj 不一样。当cdev里的kobj->parent时为空时,parent_kobj = virtual_device_parent(dev); 会指定一个parent设备为 virtual。

      字符设备驱动的组成

         字符设备驱动可以简单概括为两部分

         1、字符设备的加载和卸载

         2、字符设备对应的文件系统操作函数

 

部分代码流程分析

register_chrdev

     register_chrdev

         __register_chrdev_region   找到合适的chrdevs,获取字符设备的设备编号,并初始化 char_device_struct.

        cdev = cdev_alloc();    初始化 cdev 结构体

        err = cdev_add     给cdev结构体赋相应值,这里并没有创建文件或者文件夹

 

class_create

     class_create    创建并初始化class 结构体

        __class_register

           kset_register

               kobject_add_internal(&k->kobj);
               kobject_uevent(&k->kobj, KOBJ_ADD);

 

device_create

     device_create   

        device_create_groups_vargs  创建 struct dev 结构体

            device_register -> device_add

                  kobj = get_device_parent(dev, parent);    创建 /sys/devices 下的 device parent目录,并指定 parent,字符设备的parent 是 virtual 

                 device_create_file    创建 device 

                device_add_class_symlinks     建立 device 和 class 的软链

               

创建字符设备例子

 以下基于diagnose-tools 的代码写了个创建字符设备的例子,方便快速掌握字符设备的使用。

/* * 简单的字符设备例子 * * Copyright (C) 2020 wllabs. * * 作者: wllabs 
   
     * * License terms: GNU General Public License (GPL) version 3 * */#include 
    
     #include 
     
      #include 
      
       #include 
       
        #include 
        
         #include 
         
          #include 
          
           #include "uapi/diagnose.h"#define DIAG_DEV_NAME "wl-diagnose-tools"#define DIAG_IOCTL_TYPE_TEST 1static int diag_dev_major = -1;static struct class *diag_dev_class = NULL;static struct device *diag_dev = NULL;struct diag_dev {
           
struct cdev cdev;};static long diag_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){
int ret = -EINVAL;
int type, nr;
type = _IOC_TYPE(cmd);
nr = _IOC_NR(cmd);
switch (type) {
case DIAG_IOCTL_TYPE_TEST:
if (nr == 1) {
struct diag_ioctl_test val;
ret = copy_from_user(&val, (void *)arg, sizeof(struct diag_ioctl_test));
if (!ret) {
val.out = val.in + 1;
ret = copy_to_user((void *)arg, &val, sizeof(struct diag_ioctl_test));
}
}
break;
default:
break;
}
return ret;}static int diag_open(struct inode *inode, struct file *file){
__module_get(THIS_MODULE);
return 0;}static int diag_release(struct inode *inode, struct file *file){
module_put(THIS_MODULE);
return 0;}static const struct file_operations diag_fops = {
.open
   = diag_open,
.release
= diag_release,
.unlocked_ioctl = diag_ioctl,};static char *diag_devnode(struct device *dev, umode_t *mode){
if (mode)
*mode = S_IRUGO | S_IRWXUGO | S_IALLUGO;
return kstrdup("wl-diagnose-tools", GFP_KERNEL);;}int diag_dev_init(void){
int ret = 0;
diag_dev_major = register_chrdev(0, DIAG_DEV_NAME, &diag_fops);;
if (diag_dev_major < 0) {
printk("wl-diagnose-tools: failed to register device\n");
return diag_dev_major;
}
diag_dev_class = class_create(THIS_MODULE, DIAG_DEV_NAME);
if (IS_ERR(diag_dev_class)) {
ret = PTR_ERR(diag_dev_class);
printk(KERN_ERR "wl-diagnose-tools: class_create err=%d", ret);
unregister_chrdev(diag_dev_major, DIAG_DEV_NAME);
return ret;
}
diag_dev_class->devnode = diag_devnode;
diag_dev = device_create(diag_dev_class, NULL, MKDEV(diag_dev_major, 0), NULL, DIAG_DEV_NAME);
if (IS_ERR(diag_dev)) {
ret = PTR_ERR(diag_dev);
printk(KERN_ERR "wl-diagnose-tools: device_create err=%d", ret);
unregister_chrdev(diag_dev_major, DIAG_DEV_NAME);
class_destroy(diag_dev_class);
return ret;
}
return 0;}void diag_dev_cleanup(void){
if (diag_dev_major >= 0) {
unregister_chrdev(diag_dev_major, DIAG_DEV_NAME);
}
if (diag_dev != NULL) {
device_destroy(diag_dev_class, MKDEV(diag_dev_major, 0));
}
if (diag_dev_class != NULL) {
class_destroy(diag_dev_class);
}
diag_dev_major = -1;
diag_dev = NULL;
diag_dev_class = NULL;}static int __init diagnosis_init(void){
int ret = 0;
ret = diag_dev_init();
if (ret)
goto out_dev;
printk("wl-diagnose-tools in diagnosis_init\n");
return 0;out_dev:
return ret;}static void __exit diagnosis_exit(void){
printk("wl-diagnose-tools in diagnosis_exit\n");
diag_dev_cleanup();}module_init(diagnosis_init);module_exit(diagnosis_exit);MODULE_DESCRIPTION("Alibaba performance monitor module");MODULE_AUTHOR("wllabs  ");MODULE_LICENSE("GPL v2");

完整代码百度网盘链接: https://pan.baidu.com/s/1_ZPxSuVb9_pOckTMJvq0mw  密码: 3s5i

 

转载地址:https://blog.csdn.net/wllabs/article/details/111869529 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:csdn博客开始
下一篇:systemd 托管的进程热升级中,systemd的一些坑

发表评论

最新留言

能坚持,总会有不一样的收获!
[***.249.68.14]2022年05月22日 23时11分03秒