sys 下设备模型相关目录结构和其在代码中的对应

引言

写本文的念头起源于做 spi 实验时看到了一个目录

bash 复制代码
/sys/devices/platform/soc@3000000/4026000.spi/spi_master/spi1/spi1.0/spidev/spidev1.0

这么长的结构实在想看一下怎么组织起来的,探究的过程中发现其中涉及许多 /sys 下的目录,故就着资料整理出了本文

食用指南

关于本文框架

本文不以传统的 bus device driver 铁三角为线索进行组织分析,而是逐个分析 /sys 下的各个目录,因此阅读时可以暂时忘记 bus device driver 的关系,如 probe 和总线的 match 等概念,可能会减少理解难度

本文将以 sys -> sys/bus -> sys/class -> sys/devices -> 具体 spi 例子为线索梳理

关于示意图

本文中的图,全都不是笔者自己画的(实是网络上各位大牛画的图太清晰了,如有侵权我光速处理),是搬运自参考文档里的,函数名字是按着较早内核版本来的,但意思是可以表述清楚的

关于 attribute 和 链接

在 /sys 下有许多 attribute 文件,不会做过多说明,重点聚焦在 bus device driver class 这四个重点概念和目录的创建,目录的创建时机和 attribute 的生成时机基本一致

在 /sys 下有许多链接结构,链接在 sys/bus sys/class sys/devices 中互相穿插,导致介绍前面的内容时会涉及到后面的内容,可以用以下两点帮助记忆

  1. 所有 driver 和 device(通常是 dev_name) 互相的链接都是在 probe 阶段创建的(本文关注较少)
  2. 所有 涉及到的其他链接 都是 device_add (drivers/base/core.c) 这个函数搞出来的,看到链接或者感觉有些复杂的地方基本都找它就行

关于 kobject

阅读需要 kobject kset 前置知识,可以看下面的图或关注文中的附图,也可以搞清楚其中关系

前置知识可以参考:Linux 内核:设备驱动模型(1)sysfs 与 kobject 基类

帮助理解但不完全准确的说法:sysfs 下 kobject 就对应着目录, kset 也是一个 kobject,故 kset 也对应一个目录,是通过他含有的 kobject 结构对应的

关于开头问题的答案

跟着阅读顺序读下来会更容易理解结论,结论可以直接跳转到 SPI 相关 章节

本文对总线的分析以 platform 为例,对驱动模型的分析以 spi 为例


/SYS

要想探究 /sys/devices/...... 那么一大串目录,先探究一下最上层的目录 /sys/devices 是怎么来的

start_kernel() -> rest_init() ->kernel_init() -> do_basic_setup() -> driver_init() 代码路径:linux-5.4\init\main.c

下面的函数 driver_init 涉及到许多目录如 /sys/bus /sys/class /sys/devices /sys/bus/platform 的创建

scss 复制代码
// drivers/base/init.c
void __init driver_init(void) {
    /* These are the core pieces */
    devtmpfs_init();
    /* 详见 sys/devices */
    devices_init();
    /* 详见 sys/bus */
    buses_init();  
    /* 详见 sys/class */
    classes_init();

    firmware_init();
    hypervisor_init();

    /* These are also core pieces, but must come after the
     * core core pieces.
     */
    /* 详见 sys/bus/platform */
    platform_bus_init();
    system_bus_init();
    cpu_dev_init();
    memory_dev_init();
}

/SYS/BUS

第一部分先看一下 /sys/bus ,在设备模型接触最多的三个概念 bus devices driver 在这个目录下都有对应,bus 目录本身就可以描述大多数设备的拓扑结构。

buses_init 创建了 /sys/bus /sys/devices/system

arduino 复制代码
// drivers/base/bus.c
int __init buses_init(void)
{
        bus_kset = kset_create_and_add("bus", &bus_uevent_ops, NULL);

        system_kset = kset_create_and_add("system", NULL, &devices_kset->kobj);

        return 0;
}

下面以具体 platform 总线为例介绍 bus driver device 三者的关系。先看一下 /sys/bus/platform 是怎么创建的吧

/sys/bus/platform

图为,/sys/bus/platform 文件夹下文件内容

platform 总线的注册是由 platform_bus_init 函数完成的。该函数在内核启动阶段被调用,我们来简单看下调用过程:

start_kernel() -> rest_init() ->kernel_init() -> do_basic_setup() -> driver_init() -> platform_bus_init()

scss 复制代码
// drivers/base/platform.c
int __init platform_bus_init(void)  
{  
    int error;  

    early_platform_cleanup();  

    // 创建了 /sys/bus/platform 见后文 /sys/devices 
    error = device_register(&platform_bus);  
    if (error)  
        return error;  
    // bus 初始化 ------------------------------------------------------------------------------------------------------------
    error =  bus_register(&platform_bus_type);  
    if (error)  
        device_unregister(&platform_bus);  
    return error;  
}

device_register 在 /sys/devices 章着重分析,目前只需关注 bus_register

ini 复制代码
// drivers/base/bus.c
int bus_register(struct bus_type *bus)
{
        int retval;
        struct subsys_private *priv;
        struct lock_class_key *key = &bus->lock_key;

        // priv 和 bus 绑定
        // bus 的 priv 属性代表了 bus 的 kobject,kset 等
        // priv 的 subsys 结构即 kobject 相关
        priv->subsys.kobj.kset = bus_kset;
        priv->subsys.kobj.ktype = &bus_ktype;
        priv->drivers_autoprobe = 1;

        // bus->name = platform 以上图为例即修改文件夹名字为 platform
        retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);

        // 经上述绑定和此函数后 /sys/bus/platform 创建
        retval = kset_register(&priv->subsys);

        // 之后均在 /sys/bus/platform 下创建
        // 创建 uevent 文件
        retval = bus_create_file(bus, &bus_attr_uevent);

        // 创建 devices 文件夹
        priv->devices_kset = kset_create_and_add("devices", NULL,
                                                 &priv->subsys.kobj);
        // 创建 drivers文件夹
        priv->drivers_kset = kset_create_and_add("drivers", NULL,
                                                 &priv->subsys.kobj)
        // 创建 drivers_autoprobe 和 drivers_probe
        retval = add_probe_files(bus);

        // platform 下没有 bus_groups
        retval = bus_add_groups(bus, bus->bus_groups);
}

bus_register 小结

经过 bus_register() 函数注册之后,一种新类型 bus 在 sysfs 中的层次结构为:

图片来自:www.cnblogs.com/pwl999/p/15... 画的太好了

bus_register 调用后完成了图片中目录的创建,bus bus/devices bus/drivers 结构已然创建,但是目前后两个目录下都是空的,接下来关注 /sys/bus/xxx/drivers 和 /sys/bus/xxx/devices 目录下的文件是如何创建的

内核提供了 device_register 和 driver_register 两个接口,供各个 device 和 driver 使用,来在 sysfs 中创建目录结构。而这两个接口的逻辑之中包含,bus 模块的 bus_add_device 和 bus_add_driver ,让我们暂时摒弃 register 中其他的操作,重点关注 bus 模块下创建 device 和 driver 的部分。下面我们看看这两个接口的处理逻辑

bus 下的 drivers

/sys/bus/platform/drivers

device_driver 是依附于总线的,他并没有独立的根目录,与之对应的是 device ,每个 device 都会在 /sys/devices 下有特定的根目录空间。

因此 driver_register() 会注册驱动对象到 /sys/bus/bus_name/drivers/ 目录。而不像 device_register() 在 /sys/bus/bus_name/devices/ 目录下创建的都是符号链接。

进入 drivers 文件夹下,我们只关注 spi 相关的信息,发现有 drivers/spi 目录,进入 spi 目录,目录结构如图所示

/sys/drivers 目录下的 attribute 创建过程如下

rust 复制代码
// drivers/base/bus.c
int bus_add_driver(struct device_driver *drv)
{
    struct bus_type *bus;

    // 具体结构体见:d1-h\lichee\linux-5.4\drivers\base\base.h
    struct driver_private *priv;

    bus = bus_get(drv->bus);

    klist_init(&priv->klist_devices, NULL, NULL);
    // 将该指针保存在 device_driver 的p处
    priv->driver = drv;
    drv->p = priv;
    // 将 driver 的 kset(priv->kobj.kset)
    // 设置为 bus 的 drivers kset(bus->p->drivers_kset),
    // 这就意味着所有 driver 的 kobject 都位于
    // bus->p->drivers_kset 之下
    //(即:/sys/bus/xxx/drivers目录下)
    priv->kobj.kset = bus->p->drivers_kset;
    // 调用 kobject_init_and_add 接口,在 sysfs 中注册 driver 的 kobject,
    // 体现在 /sys/bus/xxx/drivers/ 目录下,如 /sys/bus/spi/drivers/spidev
    error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL,
                     "%s", drv->name);

    klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);
    // 将该 driver 保存在 bus 的 klist_drivers 链表中,
    // 并根据 drivers_autoprobe 的值,选择是否调用 driver_attach 进行 probe
    if (drv->bus->p->drivers_autoprobe) {
        error = driver_attach(drv);
    }
    module_add_driver(drv->owner, drv);
    // 调用 driver_create_file 接口,在 sysfs 的该 driver 的目录下,
    // 创建 uevent attribute
    error = driver_create_file(drv, &driver_attr_uevent);

    // 调用 driver_add_attrs 接口,在 sysfs 的该 driver 的目录下,
    // 创建由 bus->drv_attrs 指针定义的默认 attribute
    error = driver_add_groups(drv, bus->drv_groups);

    // 同时根据 suppress_bind_attrs 标志,决定是否在 sysfs 的该 driver 的目录下,
    // 创建 bind 和 unbind attribute
    if (!drv->suppress_bind_attrs) {
        error = add_bind_files(drv);

    }

    return 0;

}

platform_driver 的注册过程小结

经过 driver_register()函数注册以后,一种新类型 driver_register 在 sysfs 中的层次结构为:

再回顾一下 drivers/spi 的目录结构

bus 下的 devices

/sys/bus 下的 devices 都是链接,入下图所示,bus_add_device 的过程就是在创建链接

/sys/bus/platform/devices

/sys/bus/platform/devices 目录下的文件链接创建过程如下

rust 复制代码
// 调用过程 device_register -> device_add -> bus_add_device
// 具体见 /sys/devices
int bus_add_device(struct device *dev)
{
    struct bus_type *bus = bus_get(dev->bus);
    int error = 0;

    if (bus) {
        pr_debug("bus: '%s': add device %s\n", bus->name, dev_name(dev));
        // 调用内部的device_add_attrs接口,将由bus->dev_attrs指针定义的默认attribute添加到内核中,
        // 它们会体现在/sys/devices/xxx/xxx_device/目录中
        error = device_add_groups(dev, bus->dev_groups);

       // 在sys/bus/总线类型/devices/目录下,创建名字为dev_name的符号链接,指向sys/devices/相同设备名字
        error = sysfs_create_link(&bus->p->devices_kset->kobj,
                        &dev->kobj, dev_name(dev));

      // 在sys/devices/分类/设备名字/目录下创建目录名字为subsystem的符号链接,
      // 并且指向在sys/bus/总线类型&dev->bus->p->subsys.kobj, "subsystem"
        error = sysfs_create_link(&dev->kobj,
                &dev->bus->p->subsys.kobj, "subsystem");

        // 设备指针保存在bus->priv->klist_devices中
        klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices);
    }
    return 0;

}

以 spi 为例,其使用 sysfs_create_link() 函数完成在 sys/bus/spi/devices/ 目录下,创建 spi0.0 的符号链接,指向 sys/devices 相同设备名字

bus_add_device 的处理逻辑:

  • 调用 sysfs_create_link 接口,将该 device 在 sysfs 中的目录,链接到该 bus 的 devices 目录下,例如:
bash 复制代码
xxx# ls /sys/bus/spi/devices/spi1.0 -l 
lrwxrwxrwx root     root     2014-04-11 10:46 spi0.0 -> 
../../../devices/platform/s3c64xx-spi.1/spi_master/spi1/spi1.0 
其中/sys/devices/.../sp01.0,为该 device 在 sysfs 中真正的位置,
而为了方便管理,内核在该设备所在的 bus 的 xxx_bus/devices 目录中,创建了一个符号链接。

调用 sysfs_create_link 接口,在该设备的 sysfs 目录中(如/sys/devices/platform/alarm/)中,创建一个指向该设备所在 bus 目录的链接,取名为 subsystem,例如:

bash 复制代码
xxx # ls /sys/devices/platform/alarm/subsystem -l                                                
lrwxrwxrwx root     root         2014-04-11 10:28 subsystem -> ../../../bus/platform

device_register 调用的 device_add 调用的 bus_add_device ,目前先大概了解下目录结构,在 /sys/devices 章节中再看 device_register 的具体代码

经过 device_register() 函数注册以后,一种新类型 device 在 sysfs 中的层次结构为:

经过 device_register 后,创建的目录和链接关系如下:

probe 中的 sys 结构变化

rust 复制代码
// drivers/base/dd.c
static int really_probe(struct device *dev, struct device_driver *drv)  
{  
    if (driver_sysfs_add(dev)) {
                printk(KERN_ERR "%s: driver_sysfs_add(%s) failed\n",
                        __func__, dev_name(dev));
                goto probe_failed;
        }
    ...
}

// drivers/base/dd.c
static int driver_sysfs_add(struct device *dev)
{
        /* 在/sys/bus/XXX/drivers/XXX目录下建立symlink
        ,链接名为kobj->name
        链接指向/sys/devices/platform/XXX */
        ret = sysfs_create_link(&dev->driver->p->kobj, &dev->kobj,
                                kobject_name(&dev->kobj));

        /* 在/sys/devices/platform/XXX/下建立symlink,链接名为driver, 
        指向/sys/bus/xxx/drivers目录下的某个目录*/  

        ret = sysfs_create_link(&dev->kobj, &dev->driver->p->kobj,
                                "driver");
}

以 platform 总线下的 spi 为例,创建了如下两个链接,即在 device 的实际位置和 driver 的 sysfs 下位置创建了互相的链接,在文件目录下可以方便找到对应的 device/driver


/SYS/CLASS

class 抽象层面介绍

class 的概念搜了一些资料后还是没能弄的很清,归咎其原因应该在于,classs 有很多提供给用户侧的接口,但是目前没有很多对 class 的实践机会。延续本文主线,本章节不总结 class 承担的抽象概念,仅关注一个问题:这个 /sys/class 下的目录是怎么出现、什么时候出现的。

附上资料中抽象出的 class 概念

struct device 和 struct device_driver 这两个数据结构,其中 struct device 结构会包含一个 struct class 指针(这从侧面说明了 class 是 device 的集合,甚至,class 可以是 device 的 driver)。当某个 class driver 向内核注册了一个 class 后,需要使用该 class 的 device,通过把自身的 class 指针指向该 class 即可,剩下的事情,就由内核在注册 device 时处理了。

参考文章的表述非常抽象...,对目录的探究只需要知道 device 结构会有 class 指针,device 的 class 指针和 parent 指针会影响 device 的创建就行

/sys/class

bus 和 class 有相似的设计背景,他们都持有 subsys 结构,subsys 含有 kobject 和其下面的设备列表、驱动列表,说明其在抽象层面有类似的功能

貌似一个 device 不会同时具有 class 和 bus ,否则在 device_add 创建链接的阶段会报错,这个也说明了二者有很深的联系,下文的 /sys/devices 章节 对 device_add 的讲解会进一步说明

bus 没有内含 kobject 结构,其 kobject 用 bus->p->subsys.kobject 获取,(class 获取 kobject 的方式基本类似,也通过 struct subsys_private *p 数据结构)

/sys/class 目录的初始化如下

arduino 复制代码
//drivers/base/class.c
int __init classes_init(void)
{
        class_kset = kset_create_and_add("class", NULL, NULL);
        if (!class_kset)
                return -ENOMEM;
        return 0;
}

/sys/class/tty

可以以 uart 下的结构看一个例子理解 class 概念,/sys/class/tty 目录的初始化如下

ini 复制代码
// drivers/tty/tty_io.c
static int __init tty_class_init(void)
{
        tty_class = class_create(THIS_MODULE, "tty");
        if (IS_ERR(tty_class))
                return PTR_ERR(tty_class);
        tty_class->devnode = tty_devnode;
        return 0;
}

// drivers/base/class.c
// 调用关系 class_create -> class_register -> __class_register
int __class_register(struct class *cls, struct lock_class_key *key)
{
        struct subsys_private *cp;

        cp->subsys.kobj.kset = class_kset;
        error = kset_register(&cp->subsys);
}

经过 class_create ()函数注册以后,一种新类型 class 在 sysfs 中的层次结构为:

uart 的 tty

bash 复制代码
讲到 Class 的部分可以关注一下主线任务即 /sys/devices/platform/soc@3000000/4026000.spi/spi_master/spi1/spi1.0/spidev/spidev1.0
先给出该目录的名字含义在代码中的解释,之后再揭晓他们的本质
目录名:4026000.spi        /spi_master  /spi1            /spi1.0        /spidev   /spidev1.0
代码中:platform_device   /类名          /spi_master   /spi_device  /类名      /device
这个目录结构涉及 device 创建中的 subsystem class bus 概念,先以较简单的 uart 为例

以 uart 为例,/sys/class/tty 目录下,如果使能了 uart 串口 x 则会出现 ttySx 链接如以下形式:

ttyS0 -> ../../devices/platform/soc@3000000/2500000.uart/tty/ttyS0

回到 /sys/devices/platform/soc@3000000/2500000.uart 目录下观察 subsystem 可以看到存在两个 subsystem

本目录下 subsystem 指向 bus

subsystem -> ../../../../bus/platform

tty/ttyS1 目录下指向 class

subsystem -> ../../../../../../class/tty

前者的 subsystem 指向在 paltform, 通过该指向可以知道该设备是在 platform_device 相关操作即设备树解析时创建(调用 device_add )的,该目录是 devices 侧负责创建的。

后者 /tty/ttyS1/subsystem 指向 class/tty

tty_driver 和 device 在 uart 中的创建可以参考以下链接 www.cnblogs.com/easynote/p/...

/tty/ttyS1 目录创建流程,由 uart_register_driver 函数在 sunxi_uart_init 函数中执行,是驱动侧在 init 时进行的 device_add 操作

2500000.uart/tty/ttyS0 就是 device/class/device 的模式

两个 subsystem 指向不一样地方,这个模式的具体说明见后文 /sys/devices 章节

spi

/sys/class 下和 spi 有关的类目录如下,spi_master 和 spidev 文件夹的创建是 class_create 相关,与具体设备的创建无关

/sys/class/spi_master 和 /sys/class/spidev 下的目录创建与具体设备的创建有关,归根结底是 device_add 做的,涉及函数 device_add_class_symlinks ,前文说过会把 device_add_class_symlinks 和 bus_add_device 对比讲解,具体讲解放在 支线任务 -- bus 和 class

/sys/class/spi_master 目录结构如下

/sys/class/spidev 目录结构如下

device 的注册最终是由 device_add 接口(drivers/base/core.c)实现了,该接口中和 class 有关的动作包括:

  • 调用 device_add_class_symlinks 接口,创建各种符号链接,即:在对应 class 的目录下,创建指向 device 的符号链接;在 device 的目录下,创建名称为 subsystem、指向对应 class 目录的符号链接
  • 调用 device_add_attrs,添加由 class 指定的 attributes(class->dev_attrs)
rust 复制代码
// drivers/base/core.c
static int device_add_class_symlinks(struct device *dev)
{
        struct device_node *of_node = dev_of_node(dev);
        int error;

        if (of_node) {
                error = sysfs_create_link(&dev->kobj, of_node_kobj(of_node), "of_node");
                if (error)
                        dev_warn(dev, "Error %d creating of_node link\n",error);
                /* An error here doesn't warrant bringing down the device */
        }
        error = sysfs_create_link(&dev->kobj,
                                  &dev->class->p->subsys.kobj,
                                  "subsystem");
        if (dev->parent && device_is_not_partition(dev)) {
                error = sysfs_create_link(&dev->kobj, &dev->parent->kobj,
                                          "device");
        }
        /* class 中指向 device 位置的链接 */
        error = sysfs_create_link(&dev->class->p->subsys.kobj,
                                  &dev->kobj, dev_name(dev));
        return 0;
}

/SYS/DEVICES

终于到了前面出现了很多次的 /sys/devices 章节,扯点题外话,笔者自己看网络上大多讲 bus driver device 的文章都会有点割裂感,bus -> driver 部分还好,driver 大多时候就在 bus 目录下,紧密围绕 bus 去创建和 probe 设备。bus -> device 就避免不了讲 device 的创建,就又要关注 /sys/devices 和 /sys/class 才能把 device 创建讲清楚。device 就是创建在 devices,/sys/bus 下全是链接,而且还不是所有设备都在 /sys/bus ,/sys/class 下也有设备的链接,可能这就是割裂感产生的原因

感觉用 /sys 目录做区分就可以单独把 device 的创建拎出来,层次分的清楚、也可以看的更深一点

按照惯例先看一下根目录的创建,为了探究 /sys/devices/platform 下的目录需要关注两个目录的创建 /sys/devices 和 /sys/devices/platform

/sys/devices 目录的创建见下面代码,kset_create_and_add 和 kobject_create_and_add 都创建了 kobj, 区别在前者对 kset 专属的数据结构进行了初始化。

根目录

ini 复制代码
// drivers/base/core.c
int __init devices_init(void)
{
        // 创建 sys/devices
        devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);

        // 创建 sys/dev
        dev_kobj = kobject_create_and_add("dev", NULL);

        // 创建 sys/dev/block
        sysfs_dev_block_kobj = kobject_create_and_add("block", dev_kobj);

        // 创建 sys/dev/char
        sysfs_dev_char_kobj = kobject_create_and_add("char", dev_kobj);

}

小节 /sys/bus/platform 出现了这段代码,介绍了 bus_register, 在当前目录下需要关注 device_register ,第 8 行 device_register 创建了 /sys/bus/platform 目录

scss 复制代码
// drivers/base/platform.c
int __init platform_bus_init(void)  
{  
    int error;  

    early_platform_cleanup();  
    // /sys/devices/platform
    error = device_register(&platform_bus);  
    if (error)  
        return error;  
    error =  bus_register(&platform_bus_type);  
    if (error)  
        device_unregister(&platform_bus);  
    return error;  
}

明确一点,刚才这段代码是 /sys/bus/platform 的创建,因为总线也属于一个设备,所以有 device_register 的部分,我们需要关注的是,/sys/devices/platform 下的 device_register,不过其实应该是 /sys/devices/platform/soc...../xxxxx 下的 device_register ,因为设备树解析阶段会把目录创建到这层了

设备树如何把节点解析成 platform_device 可以看下设备树相关博客

/sys/devices/platform

现在关注一下 device_register ,device_register 调用了 device_add 为什么只加粗了 device_add,因为 device_register 着实有点短

device_register 的代码只有两行,但是非常关键,之后所有持有 device 的结构的初始化,包括 spi_master, spi_device,都会用各种方式调用这两个函数 device_initialize device_add。有必要重点关注下这两个函数

scss 复制代码
// drivers/base/core.c  
int device_register(struct device *dev)  
{  
    device_initialize(dev); /*初始化dev的某些字段*/  
    return device_add(dev); /*将设备添加到系统中*/  
}

device_initialize

scss 复制代码
// drivers/base/core.c
void device_initialize(struct device *dev)  
{  
    dev->kobj.kset = devices_kset;        /*设置kobj属于哪个kset,/sys/devices/*/  
    kobject_init(&dev->kobj, &device_ktype);/*初始化dev->kobj*/  

    // 其他初始化      
}

本文只关注目录结构,即 kobject 。代码中可以看到,所有 device 的 kset 都是 /sys/devices 这个目录持有的 kset,kset 是创建目录时找不到创建位置时的默认值,说明 device 于什么位置创建和其 kset 关系不大,重点要看其对 parent 的处理,在 device_add 中可以看到

终于可以看到 device_add 了,device_add 中和目录有关的代码段如下

ini 复制代码
// drivers/base/core.c
int device_add(struct device *dev)  
{  
        // 寻找父目录
        parent = get_device(dev->parent);
        kobj = get_device_parent(dev, parent);
        if (kobj)
                dev->kobj.parent = kobj;

        error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);

        /*在XXX下建立文件uevent*/  
        error = device_create_file(dev, &uevent_attr); 

        /* 在sys/dev/char/下建立symlink,名字为主设备号:次设备号,该链接指向XXX */  
        error = device_create_sys_dev_entry(dev);   

        error = device_create_file(dev, &dev_attr_uevent);

       // 这里创建了 class 相关的链接
        error = device_add_class_symlinks(dev);

        error = device_add_attrs(dev);

        // 这里创建了 bus 相关的链接,并且与之前 class 创建的有点关系
        error = bus_add_device(dev);

        error = dpm_sysfs_add(dev);

        device_pm_add(dev);

        if (MAJOR(dev->devt)) {
                error = device_create_file(dev, &dev_attr_dev);
                if (error)
                        goto DevAttrError;

                error = device_create_sys_dev_entry(dev);
                if (error)
                        goto SysEntryError;

                devtmpfs_create_node(dev);
        }
    }  
}

主线任务 -- 父目录获取

紧跟主线,device_add 代码 5 行获取 dev->parent 第 6 行根据 dev->parent dev->parent->class dev->class 决定父目录并返回到 kobj 中

get_device_parent() 的逻辑:如果 dev->class 为空,表示随父设备,有 parent 则返回 parent->kobj,没有则如果 dev->bus->dev_root 不为空,即 bus 提供了存放位置,则放到这里,否则返回 NULL。如果有 dev->class 呢,情况就比较复杂了,也许 device 有着与 parent 不同的 class,也许 device 还没有一个 parent,等等。

如果 parent 不为空,而且存在 parent->class,则还放在 parent 目录下。不然,要么 parent 不存在,要么 parent 没有 class,很难直接将有 class 的 device 放在 parent 下面。目前的解决方法很简单,在 parent 与 device 之间,再加一层表示 class 的目录,class->p->glue_dirs 就是专门存放这种中间 kobject 的 kset,在第一次创建出 glue_dir 后,之后再有相同 class 的 device 创建的话就可以沿用之前的 glue_dirs 了。如果 parent 都没有,那就把 /sys/devices/virtual 当做 parent。

关于为什么要有中间的以 class_name 为 glue_dirs 来组织结构,以 spi 为例,platform_device/spi_master/spi1 和 platform_device/spi_slave/spi1 都有可能出现,此法可以避免命名空间冲突,同时可以在 sys 目录下展现得更加直观

红色字就是主线任务的关键线索,代码如下,这个函数比较关键就把所有代码都放上来了

rust 复制代码
// drivers/base/core.c
static struct kobject *get_device_parent(struct device *dev,
                                         struct device *parent)
{
        if (dev->class) {
                struct kobject *kobj = NULL;
                struct kobject *parent_kobj;
                struct kobject *k;

#ifdef CONFIG_BLOCK
                /* block disks show up in /sys/block */
                if (sysfs_deprecated && dev->class == &block_class) {
                        if (parent && parent->class == &block_class)
                                return &parent->kobj;
                        return &block_class.p->subsys.kobj;
                }
#endif

                /*
                 * If we have no parent, we live in "virtual".
                 * Class-devices with a non class-device as parent, live
                 * in a "glue" directory to prevent namespace collisions.
                 */
                if (parent == NULL)
                        parent_kobj = virtual_device_parent(dev);
                else if (parent->class && !dev->class->ns_type)
                        return &parent->kobj;
                else
                        parent_kobj = &parent->kobj;

                mutex_lock(&gdp_mutex);

                /* find our class-directory at the parent and reference it */
                spin_lock(&dev->class->p->glue_dirs.list_lock);
                list_for_each_entry(k, &dev->class->p->glue_dirs.list, entry)
                        if (k->parent == parent_kobj) {
                                kobj = kobject_get(k);
                                break;
                        }
                spin_unlock(&dev->class->p->glue_dirs.list_lock);
                if (kobj) {
                        mutex_unlock(&gdp_mutex);
                        return kobj;
                }

                /* or create a new class-directory at the parent device */
                k = class_dir_create_and_add(dev->class, parent_kobj);
                /* do not emit an uevent for this simple "glue" directory */
                mutex_unlock(&gdp_mutex);
                return k;
        }

        /* subsystems can specify a default root directory for their devices */
        if (!parent && dev->bus && dev->bus->dev_root)
                return &dev->bus->dev_root->kobj;

        if (parent)
                return &parent->kobj;
        return NULL;
}

其实主线任务的答案已经可以给出,就是两个 /device/class/device 结构

bash 复制代码
目录名:4026000.spi        /spi_master  /spi1            /spi1.0        /spidev   /spidev1.0
本质:   device                /class          /device         /device        /class     /device
代码中:platform_device   /类名          /spi_master   /spi_device  /类名      /device

对 spi 下目录的具体分析放在 SPI 相关 章节

支线任务 -- bus 和 class

device_add 调用了 bus_add_device 和 device_add_class_symlinks 之前有提及过,class 和 bus 有很深的关联,他们都有一样的 subsys_private 结构体,都可以作为 subsystem 被 device 指向,现在看在 device_add 里也是成对调用和链接有关的函数。目前笔者对,bus 和 class 分别承担了 device 的何种抽象,这个概念不甚了解,只能把他们的一些对应找出来先放着。感觉探索这个概念,是之后实践中学习的线索。

/sys/bus: bus_add_device 已经在 /sys/bus/devices 出现过了,就是在 /sys/devices 和 /sys/bus 相关目录下建立链接,

ini 复制代码
// drivers/base/bus.c
int bus_add_device(struct device *dev)
{
    struct bus_type *bus = bus_get(dev->bus);  
    int error = 0;  

    if (bus) {  
        pr_debug("bus: '%s': add device %s\n", bus->name, dev_name(dev));  
        error = device_add_attrs(bus, dev);  

        /*在 sys/bus/XXX/devices 下建立 symlink,
        名字为设备名,该链接指向 /sys/devices/ 下的某个目录*/  
        error = sysfs_create_link(&bus->p->devices_kset->kobj,  
                        &dev->kobj, dev_name(dev));  

        /*在 sys/devices/ 的某个目录下建立 symlink,
        名字为 subsystem,该链接指向 /sys/bus/ 下的某个目录*/  
        error = sysfs_create_link(&dev->kobj,  
                &dev->bus->p->subsys.kobj, "subsystem");  

        /*在 sys/devices/ 的某个目录下建立 symlink,
        名字为 bus,该链接指向 /sys/bus/ 下的某个目录*/  
        error = make_deprecated_bus_links(dev);  

}

/sys/class: device_add_class_symlinks 在之前也出现过,也把其代码放过来看一下

rust 复制代码
// drivers/base/core.c
static int device_add_class_symlinks(struct device *dev)
{
        struct device_node *of_node = dev_of_node(dev);
        int error;

        if (of_node) {
                error = sysfs_create_link(&dev->kobj, of_node_kobj(of_node), "of_node");
                if (error)
                        dev_warn(dev, "Error %d creating of_node link\n",error);
                /* An error here doesn't warrant bringing down the device */
        }
        error = sysfs_create_link(&dev->kobj,
                                  &dev->class->p->subsys.kobj,
                                  "subsystem");
        if (dev->parent && device_is_not_partition(dev)) {
                error = sysfs_create_link(&dev->kobj, &dev->parent->kobj,
                                          "device");
        }
        /* class 中指向 device 位置的链接 */
        error = sysfs_create_link(&dev->class->p->subsys.kobj,
                                  &dev->kobj, dev_name(dev));
        return 0;
}

总结

device_register 小结

经过 device_register()函数注册以后,一种新类型 device 在 sysfs 中的层次结构为:


SPI 相关

引言

在本章开始前总结一下设备模型的层次

  • 第一层类 kobject、kset、subsys
  • 第二层类 bus_type、device、device_driver、class
  • 第三层类 spi_bus_type、spi_device、spi_driver

这个部分开始的目录,位于第三层类,SPI 抽象了一层自己的总线,拥有自己的总线拓扑结构,但大体结构上与 platform 类似。SPI 总线的初始化如下

SPI 部分的主要结构初始化过程见下图

上图只需要知道,spi_controller spi_device 的信息源自设备树,spi protocol driver 的信息在 spidev.c 下,其余可以边往下看边理解

/sys/bus/spi

sys/bus/spi 目录下的组织方式和 sys/bus/platform 类似,我们重点关注 sys/devices 和 sys/class 下 spi 相关目录的创建

/sys/bus/spi 目录初始化如下

ini 复制代码
// drivers/spi/spi.c
static int __init spi_init(void)
{
        int        status;

        buf = kmalloc(SPI_BUFSIZ, GFP_KERNEL);
        if (!buf) {
                status = -ENOMEM;
                goto err0;
        }

        // 关注这里 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
        status = bus_register(&spi_bus_type);
        if (status < 0)
                goto err1;

        // 和这里 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
        status = class_register(&spi_master_class);
}

刚才的代码段中 bus_register 初始化了 /sys/bus/spi 再往下就能看到 class_register 创建了 /sys/class/spi_master

/sys/devices 下不得不讲的 struct spi_master

struct spi_master 是一个持有了 device 的结构体,同时有个类名 class 也叫 spi_master (代码中即 spi_master_class) 这两个概念同时出现时需要予以分辨

往下探究 /sys/devices 下的 spi 目录必须先介绍 struct spi_master,可以说它负责了 spi 下目录结构的组织

struct spi_master 持有了 device 内存空间,故 struct spi_mater 是一个 device,与之前的 device 分析类似,struct spi_master 也需要 device_initialize 和 device_add 在此之外再添加 spi 相关的信息,其初始化在 spi_alloc_controller 注册在 spi_register_controller

arduino 复制代码
// driers/spi/spi.c
// dev 是 spi 代表的 platform_device 
// ctrl->dev 是 spi_controller 代表的 device 
// 他本身也是一个device
struct spi_controller *__spi_alloc_controller(struct device *dev,
                                              unsigned int size, bool slave)

        if (IS_ENABLED(CONFIG_SPI_SLAVE) && slave)
                ctlr->dev.class = &spi_slave_class;
        else
        // 在 sys/class 下的拓扑结构
        // 会在 /sys/class/spi_master 下生成链接指向该 dev
        // 的实际位置
                ctlr->dev.class = &spi_master_class;
        // 在 /sys/devices 下的拓扑结构
        // 在 /4026000.spi 目录下的结构
        ctlr->dev.parent = dev;
}

// driers/spi/spi.c
int spi_register_controller(struct spi_controller *ctlr)
{

        // dev 是 spi 代表的 platform_device 
        // ctrl->dev 是 spi_controller 代表的 device 
        struct device *dev = ctlr->dev.parent;

        // 代表 spi_master 的设备名称是 spix
        dev_set_name(&ctlr->dev, "spi%u", ctlr->bus_num);

        status = device_add(&ctlr->dev);
}

问题: ctrl->dev.parent 在文件目录中是 4026000.spi,ctrl->dev.name = spi1 ,为什么最后的文件目录结构是 4026000.spi/spi_master/spi1 ,而不是 4026000.spi/spi1

如果仔细读了 主线任务 -- 父目录获取 的话,心中应该已经有答案了

/sys/devices/platform/xx/4026000.spi

spi1 是 struct spi_master 的设备的名字,spi_master 是 struct spi_master 所属的 class 名, 在 device_add 中有如下逻辑

scss 复制代码
// drivers/base/core.c
int device_add(struct device *dev)
{
    kobj = get_device_parent(dev, parent);

}

static struct kobject *get_device_parent(struct device *dev,
                                         struct device *parent)
{
    // spi_master 的父设备是 platform_device
    // spi_master 的 class 是 spi_master 父设备的 class 不存在
    // 逻辑会走到这一个函数,创建 spi_master 文件夹作为 spi_master
    // 的 device 的父文件夹。不直接放到 platform_device 下
    k = class_dir_create_and_add(dev->class, parent_kobj);
}

以 spi 举例,spi 的 platform_device 的 subsystem 指向 bus/platform ,platform_device /spi_master/spi1 的 subsystem 指向 class/spi_master

这个模式在 uart 中也存在,uart 的 platform_device 的 subsystem 指向 bus/platform ,platform_device/tty/ttyS1 的 subsystem,指向 class/tty

/sys/devices/platform/xx/4026000.spi/spi_master/spi1/

前面说明了 spi1 也就是 spi_master 的设备的存放位置,还没有涉及到 spi_device, spi_driver 的概念,当前目录的内容如下

继续往下探究,可能的入口点是 spi1.0,spi1.0 的创建在 spi_register_controller -> of_register_spi_devices ,它是直接解析设备树创建的。 of_register_spi_devices -> of_register_spi_device -> spi_alloc_device 中绑定了与 master 的设备的关系

ini 复制代码
// drivers/spi/spi.c
struct spi_device *spi_alloc_device(struct spi_controller *ctlr)
{
        struct spi_device        *spi;

        spi->master = spi->controller = ctlr;
        spi->dev.parent = &ctlr->dev;
        spi->dev.bus = &spi_bus_type;
}

在 of_register_spi_devices -> of_register_spi_device -> spi_add_device -> spi_dev_set_name 中创建了该设备的名称 spi1.0

rust 复制代码
static void spi_dev_set_name(struct spi_device *spi)
{
        struct acpi_device *adev = ACPI_COMPANION(&spi->dev);

        dev_set_name(&spi->dev, "%s.%u", dev_name(&spi->controller->dev),
                     spi->chip_select);
}

spi1.0 的目录结构如下图所示,可以看到 spi1.0 作为 spi_device 和 platform_device 的目录结构很像,通过 driver 可以找到其对应的驱动名字和层级

spidev1.0 目录的创建在 spi_device 与 spi_driver probe 时

在 spi1.0 目录下的 spidev/spidev1.0 出现了之前的 /device//sub_device 模式

之前是

4026000.spi/spi_master/spi1

对应

platform_device/class/spi_device

这里是

spi1.0/spidev/spidev1.0

对应

spi_device/class/device

总结

至此算是对主线任务告一段落了,目前是从 /sys 的角度,看了一下一开始提到的主线任务,只是很功利地找到了文件结构的对应对 /sys 目录有了一定的理解,对 bus driver device class 的组织有了一定理解

但是对具体 spi 驱动模块,或是具体 platform 总线的框架的理解可能还是要从实践中去加深

/SYS/DEV

sys/dev 貌似和用户侧做响应的设备才会注册到这里,在这个目录下我们关注的 spi 有关的文件只有

lrwxrwxrwx 1 root root 0 Jan 1 08:00 153:0 -> ../../devices/platform/soc@3000000/4026000.spi/spi_master/spi1/spi1.0/spidev/spidev1.0

这说明了和用户侧进行对接的是 spidev1.0 设备

总结

这篇文章比较功利,就是做了 /sys 目录和代码的对应,每涉及到一个目录都牵扯很多知识点,好在这个主线任务足够聚焦,可以在较小范围内写完。

参考文档

官方文档

wowotech:Linux 设备模型 wowotech 很细,还有些别的概念的 gpio 设备树的,就着源码讲的

PCI 为例的设备模型讲解(超级详细)图很牛,用的全是这的图

Linux 设备驱动框架剖析 图很清楚

Linux 设备驱动模型和 sysfs 文件系统

Linux 内核部件分析 设备驱动模型之 device 针对 /drivers/base/core.c 的分析

相关推荐
东华果汁哥4 分钟前
【linux 免密登录】快速设置kafka01、kafka02、kafka03 三台机器免密登录
linux·运维·服务器
咖喱鱼蛋26 分钟前
Ubuntu安装Electron环境
linux·ubuntu·electron
ac.char30 分钟前
在 Ubuntu 系统上安装 npm 环境以及 nvm(Node Version Manager)
linux·ubuntu·npm
肖永威35 分钟前
CentOS环境上离线安装python3及相关包
linux·运维·机器学习·centos
tian2kong38 分钟前
Centos 7 修改YUM镜像源地址为阿里云镜像地址
linux·阿里云·centos
布鲁格若门42 分钟前
CentOS 7 桌面版安装 cuda 12.4
linux·运维·centos·cuda
C-cat.1 小时前
Linux|进程程序替换
linux·服务器·microsoft
dessler1 小时前
云计算&虚拟化-kvm-扩缩容cpu
linux·运维·云计算
怀澈1221 小时前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
DC_BLOG1 小时前
Linux-Apache静态资源
linux·运维·apache