欢迎光临
我们一直在努力

基于Linux-4.9.88的SPI子系统研究(1)

​研究思路:
1.如何阅读
看到一个新的结构体不要一开始就想把所有成员看透,纵向跟着流程走,以 bus_register 这个动作为线索。只有当这个流程执行到某一步,用到了某个结构体的某个成员时,那个成员才有意义。

2.做日志
①做日志的第一步是准备好你的坐标记录,先在开头写下当前研究的函数名 (例如bus_register) 以及研究动机,比如是为了摸清总线注册的资源分配流程。接着进入“单步追踪模式”,不理会没被调用的代码分支,只盯着当前函数执行的主干路径,每当代码执行一个实质性动作时,就用一句通俗易懂的话记录在案:比如执行到内存分配时记下“申请私有账本空间”,执行到 kset_register 时记下“在 /sys/bus 下创建主目录”,确保记录是线性,易读的。

②在追踪过程中,一旦遇到结构体成员的访问,点开该成员的定义并标注它在内存中的角色,例如看到 klist_devices 就在日志旁边备注“这是存放未来所有子设备的点名册”,并将这种成员归类为“容器”或“标识”。

③核心的“画图建模”环节,在纸上或绘图工具中画出 bus_type 和 subsys_private 两个方框,用双向箭头连接它们,并根据代码里的 kset_create_and_add 动作引出分支,画出 /sys 下的目录层级,把抽象的指针关系变成看得见的拓扑结构。

3.打印跟踪
结合打印调试的实测数据,把你从串口终端(如 MobaXterm 或 dmesg 命令)中抓取到的真实内存地址或执行时间点填入日志,比如记录下“platform总线的 priv 实际地址是 0xXXXX”,用事实来印证你之前的理论推导。

内核文件路径

1.平台总线的搭建

static int __init spi_init(void)
{
intstatus;

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);
if (status < 0)
goto err2;

if (IS_ENABLED(CONFIG_OF_DYNAMIC))
WARN_ON(of_reconfig_notifier_register(&spi_of_notifier));
if (IS_ENABLED(CONFIG_ACPI))
WARN_ON(acpi_reconfig_notifier_register(&spi_acpi_notifier));

return 0;

err2:
bus_unregister(&spi_bus_type);
err1:
kfree(buf);
buf = NULL;
err0:
return status;
}

由spi_init入手,发现了bus_register与class_register,故深入研究该部分。

在这里插入图片描述

1.1 bus_register

bus_register函数定义在 Linux 内核处理底层设备管理的核心文件中:
文件路径:drivers/base/bus.c
函数声明:位于 include/linux/device.h

int bus_register(struct bus_type *bus)
{
int retval;
struct subsys_private *priv;
struct lock_class_key *key = &bus->lock_key;

priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);
if (!priv)
return ENOMEM;

priv->bus = bus;
bus->p = priv;

BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);

retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);
if (retval)
goto out;

priv->subsys.kobj.kset = bus_kset;
priv->subsys.kobj.ktype = &bus_ktype;
priv->drivers_autoprobe = 1;

retval = kset_register(&priv->subsys);
if (retval)
goto out;

retval = bus_create_file(bus, &bus_attr_uevent);
if (retval)
goto bus_uevent_fail;

priv->devices_kset = kset_create_and_add("devices", NULL,
&priv->subsys.kobj);
if (!priv->devices_kset) {
retval = ENOMEM;
goto bus_devices_fail;
}

priv->drivers_kset = kset_create_and_add("drivers", NULL,
&priv->subsys.kobj);
if (!priv->drivers_kset) {
retval = ENOMEM;
goto bus_drivers_fail;
}

INIT_LIST_HEAD(&priv->interfaces);
__mutex_init(&priv->mutex, "subsys mutex", key);
klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);
klist_init(&priv->klist_drivers, NULL, NULL);

retval = add_probe_files(bus);
if (retval)
goto bus_probe_files_fail;

retval = bus_add_groups(bus, bus->bus_groups);
if (retval)
goto bus_groups_fail;

pr_debug("bus: '%s': registered\\n", bus->name);
return 0;

bus_groups_fail:
remove_probe_files(bus);
bus_probe_files_fail:
kset_unregister(bus->p->drivers_kset);
bus_drivers_fail:
kset_unregister(bus->p->devices_kset);
bus_devices_fail:
bus_remove_file(bus, &bus_attr_uevent);
bus_uevent_fail:
kset_unregister(&bus->p->subsys);
out:
kfree(bus->p);
bus->p = NULL;
return retval;
}

看到函数的参数以及前面定义的结构体

struct bus_type *bus
struct subsys_private *priv

不要直接点进去看是什么

观察函数后面,发现用到了它们的成员

priv->bus = bus;
bus->p = priv;

1.1.1 subsys_private(驱动私有数据)

此时再进入结构体struct subsys_private *priv;内部看用到的成员有什么作用

/**
* struct subsys_private – structure to hold the private to the driver core portions of the bus_type/class structure.
*
* @subsys – the struct kset that defines this subsystem
* @devices_kset – the subsystem's 'devices' directory
* @interfaces – list of subsystem interfaces associated
* @mutex – protect the devices, and interfaces lists.
*
* @drivers_kset – the list of drivers associated
* @klist_devices – the klist to iterate over the @devices_kset
* @klist_drivers – the klist to iterate over the @drivers_kset
* @bus_notifier – the bus notifier list for anything that cares about things
* on this bus.
* @bus – pointer back to the struct bus_type that this structure is associated
* with.
*
* @glue_dirs – "glue" directory to put in-between the parent device to
* avoid namespace conflicts
* @class – pointer back to the struct class that this structure is associated
* with.
*
* This structure is the one that is the actual kobject allowing struct
* bus_type/class to be statically allocated safely. Nothing outside of the
* driver core should ever touch these fields.
*/

struct subsys_private {
struct kset subsys;
struct kset *devices_kset;
struct list_head interfaces;
struct mutex mutex;

struct kset *drivers_kset;
struct klist klist_devices;
struct klist klist_drivers;
struct blocking_notifier_head bus_notifier;
unsigned int drivers_autoprobe:1;
struct bus_type *bus;

struct kset glue_dirs;
struct class *class;
};

根据描述可知pirv是用于存放 bus_type(总线)或 class(类)结构中,仅由驱动核心层(Driver Core)私有持有的数据部分

1.1.2 bus_type(总线类型)

再深入bus成员的结构体定义,可发现描述

/**
* struct bus_type – The bus type of the device
* @p:The private data of the driver core, only the driver core can
*touch this.

即bus是设备的总线类型,p则是驱动核心的私有数据,只允许驱动核心使用,故*p就是用来访问这个数据的指针。

priv->bus = bus;
bus->p = priv;

因此可以知道这两行代码的含义:
将驱动的私有数据和总线类型绑定在一起

1.1.3 kzalloc

priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);
if (!priv)
return ENOMEM;

为驱动核心(Driver Core)开辟私有管理空间(subsys_private)并清零,如果 priv 为空,说明系统内存耗尽,注册任务失败

1.1.4 BLOCKING_INIT_NOTIFIER_HEAD

BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);

BLOCKING:代表“阻塞”。这意味着当总线发出通知消息时,接收者处理这个消息的过程是允许休眠的。

1.1.4.1 操作性质

在单片机编程中,常担心中断里不能做耗时操作。Linux 内核通过前缀来区分这种性质:
BLOCKING (阻塞):表示发送通知的这个过程可以被中断、可以休眠。它内部使用“信号量”(Semaphore)来保护通知链。

ATOMIC (原子):这是另一种常见的通知头。如果前缀是 ATOMIC,表示发送通知时绝不能休眠(比如在中断处理程序中发送),它内部使用“自旋锁”(Spinlock)。

INIT_NOTIFIER_HEAD:初始化一个通知链表的头部。

&priv->bus_notifier:指向我们在 subsys_private 内存空间里刚申请好的那个通知头成员。

1.1.4.2 bus_notifier

struct blocking_notifier_head bus_notifier;

结构体定义

struct blocking_notifier_head {
*struct rw_semaphore* rwsem;
*struct notifier_block __rcu* *head;
};

rw_semaphore: 读写锁,保护数据时允许大家同时读,但改写时要排队,且拿不到锁时进程会休眠

notifier_block _rcu:

__rcu:全称是 Read-Copy-Update,允许多个人同时读数据,而不需要加锁;但如果你想改数据,你得先拷贝一份副本,在副本上改完,再找机会“替换”回去

notifier_block 里的 next 指针被标记了 __rcu:
1.读这个指针很快,不需要加锁:因为 RCU 保证了即便有人在修改链表,你读到的数据也不会导致系统崩溃。

2.如果想修改这个 next 指针,不能直接赋值,必须使用专门的 RCU 函数(如 rcu_assign_pointer)

struct ==notifier_block== {
notifier_fn_t notifier_call;
struct ==notifier_block== __rcu *next;
int priority;
};

由结构体定义看到是嵌套的,知道notifier_block是一个链表结构,notifier_call说明这个结构体的作用是用来提供回调函数的,priority则是执行的优先级。

1.1.5 kobject_set_name

retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);
if (retval)
goto out;

/**
* kobject_set_name – Set the name of a kobject
* @kobj: struct kobject to set the name of
* @fmt: format string used to build the name
*
* This sets the name of the kobject. If you have already added the
* kobject to the system, you must call kobject_rename() in order to
* change the name of the kobject.
*/

int kobject_set_name(struct kobject *kobj, const char *fmt, ...)
{
va_list vargs;
int retval;

va_start(vargs, fmt);
retval = kobject_set_name_vargs(kobj, fmt, vargs);
va_end(vargs);

return retval;
}

kobject_set_name – 设置 kobject 的名称
@kobj: 指向要设置名称的 struct kobject 结构体指针。
@fmt: 用于构建名称的格式化字符串(类似于 printf 的格式)。
该函数用于设置 kobject 的名称。如果你已经将该 kobject 添加到了系统中(即已注册),则必须调用 kobject_rename() 来更改其名称

将程序员在 bus_type 里写的字符串名字,绑定到内核私有管理结构 subsys_private 的核心 kobject 上。

kobject (重点)

struct kobject {
const char*name;
struct list_headentry;
struct kobject*parent;
struct kset*kset;
struct kobj_type*ktype;
struct kernfs_node*sd; /* sysfs directory entry */
struct krefkref;
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
struct delayed_workrelease;
#endif
unsigned int state_initialized:1;
unsigned int state_in_sysfs:1;
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
unsigned int uevent_suppress:1;
};

身份信息类

const char *name

解释:对象的名称。
作用:它直接决定了 /sys 目录下那个文件夹的名字。比如你刚才看到的 kobject_set_name 就是在填这一项。

struct kref kref

解释:引用计数器。
作用:内核的“生死簿”。每当有人引用这个对象,计数加 1;释放时减 1。当减到 0 时,内核才会真正销毁这块内存。这防止了“你还在用这个总线,结果系统把它删了”导致的崩溃。

组织关系类

struct kobject *parent

解释:指向父对象的指针。
作用:建立层级关系。在 /sys 里,这决定了谁是谁的子目录。

struct kset *kset

解释:指向所属“集合”的指针。
作用:这是“分类”逻辑。比如所有的总线都属于 bus_kset,这让内核能批量管理同类对象。

struct list_head entry

解释:链表节点。
作用:同一个 kset 下的所有 kobject 会通过这个成员串成一串,方便内核“点名”查询。

行为逻辑类

struct kobj_type *ktype

解释:指向“类型”定义的指针。
作用:它定义了这个对象**“被销毁时该干嘛”(release 函数)以及“用户读写它的文件时该干嘛”**。它是 kobject 的灵魂,决定了文件夹里能看到哪些属性文件。

struct kernfs_node *sd

解释:指向 sysfs 文件系统内部节点的指针。
作用:这是连接“内存对象”和“磁盘/内存文件系统”的桥梁。有了它,这个 kobject 才能在 /sys 目录下真实显影。

状态标志类

state_initialized //是否已初始化。

state_in_sysfs //是否已经出现在了 /sys 目录树中。

state_add_uevent_sent //是否已经向用户空间发送了创建通知消息(热插拔事件)。

state_remove_uevent_sent //是否发送了删除通知消息。

uevent_suppress //是否抑制(不发)热插拔消息。有的对象太小,不需要用户空间。

调试类

struct delayed_work release

解释:延迟释放任务。
作用:仅在开启调试开关时有效。它故意延迟销毁对象,用来检测那些在对象死后还试图访问它的非法行为(Use-After-Free)。

kobject 的本质:
它是一个通用的“挂钩”。内核通过把这个小挂钩埋进复杂的结构体(如 bus_type),就让这些结构体具备了起名、排队、数数、发消息的能力

核心逻辑:名 + 位 = 路径
name:决定文件夹文字
kset / parent:决定父目录位置

kset = (某个具体的 kset 对象)

如果你把 kset 指向内核预定义的 bus_kset,就会创建一个路径为 /sys/bus/xxx的文件夹

为什么必须用 kset 对象而非路径字符串?
①从“寻址”到“归类”:字符串只是一个地址,而 kset 是一个组织。
②指针 vs 字符串:指针是机器语言,直接、高效;字符串是人类语言,模糊。指针操作是原子的,或者很容易通过锁保护。如果用字符串路径,内核在查找过程中,如果路径上的某个文件夹被别人删了(比如另一个总线正在注销),字符串解析就会崩溃。指针直接指向对象内存,比查找字符串安全。
③容器作用:所有的总线(kobject)在入职时,都会把自己挂在 bus_kset 的链表上。内核只需要遍历这个链表,就能精准找到所有总线
④一致性:属于同一个 kset 的对象,通常会共享一些处理逻辑(比如怎么发热插拔消息)。如果只填路径,内核就丢掉了这种“类别”的概念,这种“类别”在内核里叫 uevent_ops(热插拔事件操作集)。同一个 kset 里的成员(如所有的总线)在发生插拔时,都会调用 kset 统一提供的函数来格式化消息。这保证了用户空间收到的“总线消息”格式是一模一样的。

路径合成的优先级
在内核真实创建文件夹时(也就是调用 kobject_add 时),它会按照以下顺序
①优先看 parent 指针:如果 kobj->parent 有值,它就直接在这个父目录下创建。
②次看 kset 指针:如果 parent 为空,但设置了 kset,它就会把 kset 内部嵌套的那个 kobject 当作父节点。
③最后看顶层:如果两者都为空,它就会直接在 /sys 的根目录下创建。

kobject生成的文件夹作用:
①内核状态的直观展示(只读)
作用:它把原本只能用调试器(如 GDB)才能看到的 C 语言结构体成员,变成了人能读懂的文本文件。
例如:
可以cat 一下某个文件,看看总线版本号、当前电源状态、或者已经挂载了多少个设备。
例子:查看 /sys/bus/usb/devices/ 就能一眼看到你的 USB 鼠标在内核眼里是什么样子的。

②运行时的参数调校(可写)
作用:通过向文件夹里的特定文件写入数据,你可以实时修改内核的行为,而不需要关机重启或重新编译代码。
例如:
手动扫描:往 rescan 文件写个 1,总线就会立刻去寻找新硬件。
解绑驱动:往 unbind 文件写个设备名字,驱动就会立刻释放对该硬件的控制权,这在处理硬件故障或更换驱动时极其有用。

③热插拔事件的触发源(通知)
这是最自动化的一点:它是用户空间程序的“发令枪”。
作用:当这个文件夹被创建的那一刻,内核会发送一个 uevent 广播。
你可以干什么(或者说系统会自动做什么):
用户空间的 udev 或 mdev 监听到文件夹创建的消息,会立刻去 /dev 下建立对应的设备节点(比如 /dev/sda)。
它可以根据文件夹里的信息,自动匹配并加载最合适的驱动程序(.ko 文件)

1.1.6 kobject属性设置

priv->subsys.kobj.kset = bus_kset;

struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
const struct kset_uevent_ops *uevent_ops;
};

  • struct list_head list; —— “管理名单”
    功能:这是一个双向循环链表头。
    解释:所有属于这个kset 的 kobject 都会通过它们自己的 entry 成员挂在这个链表上。
    意义:内核只需要遍历这个 list,就能找到该 kset 下所有的成员。
    比如 ls /sys/bus/spi/devices 看到的内容,底层就是通过这个链表扫出来的。
  • spinlock_t list_lock; —— “锁机制”
    功能:自旋锁。
    解释:内核是多线程(多核)并发运行的。可能同时有两个驱动在注册设备,都要往这个 list 里写数据。
    意义:为了防止两个线程把链表改乱了,任何人在往 list 里增删成员前,必须先拿到这把锁。这保证了并发安全性。
  • struct kobject kobj; —— “根基”
    功能:内嵌的 kobject 结构体。
    解释:正如你之前总结的,文件夹必须是 kobject。kset 既然要在 sysfs 里显示为一个文件夹,它就必须内嵌一个 kobject。
    意义:这意味着 kset 继承了 kobject 的所有特性(引用计数、名字、父节点)。
  • const struct kset_uevent_ops *uevent_ops; —— “外交部/沟通窗口”
    功能:热插拔事件操作函数集。
    解释:这是 kset 的灵魂所在。当旗下的 kobject 发生变化时,内核会调用这里的函数。
    filter:过滤。决定这个事件要不要上报给用户空间(比如有些内部动作不需要打扰 udev)。
    name:给事件起个名字。
    uevent:添加环境变量(比如告诉用户空间“这个新插入的设备叫啥、序列号是多少”)。
    意义:它定义了该科室对外的“外交策略”。
  • priv->subsys.kobj:这是你当前正在创建的总线(比如 PCI 或 SPI)。
    bus_kset:这是内核启动时就建好的,代表 /sys/bus/ 目录。
    赋值操作:向内核宣布这根总线的 kobject 是属于 bus 这个kset管理。

    内核会将subsys.kobj 挂到 bus_kset 内部的list。这样,内核只要遍历 bus_kset->list,就能数出系统里一共有多少总线。总线从此会使用 bus_kset 定义的 uevent_ops 来处理热插拔消息。

    priv->subsys.kobj.ktype = &bus_ktype;

    ktype 决定了这个 kobject 的行为和属性,bus_ktype 是一套内核标准模具。它里面定义了:当用户访问这个文件夹时,哪些读写函数会被调用;当总线被注销时,调用哪个 release 函数来清理内存。

    例子:echo 1 > /sys/bus/pci/rescan
    当这个shell指令输入后,其实就是执行了rescan文件夹绑定的回调函数,所以才能实现设备的扫描功能。

    priv->drivers_autoprobe = 1;

    开启“自动匹配”模式。设置为 1(真)意味着:以后只要有新设备插到这个总线上,或者有新驱动注册到这个总线上,内核都会主动、自动地去尝试让它们匹配。

    1.1.7 kset_register

    retval = kset_register(&priv->subsys);
    if (retval)
    goto out;

    ①目录创建(sysfs 落地)
    调用底层的 kobject_add。
    逻辑:内核查看 &priv->subsys.kobj。发现它的 name 是 pci,kset 是 bus_kset(指向 /sys/bus)。
    物理结果:内核在文件系统中真正创建 /sys/bus/pci/ 这个文件夹。
    属性生成:同时,它会根据 ktype,在文件夹里生成 drivers、devices 以及 rescan 等属性文件。

    ②组织入网(链表挂载)
    逻辑:将该总线的 kset 节点挂载到内核全局的 bus_kset 链表上。
    物理结果:从此,内核只要遍历 bus_kset,就能在内存里找到这根 PCI 总线。

    总线从内核的一块私有内存,变成了全系统可见的正式成员

    1.1.8 bus_create_file

    retval = bus_create_file(bus, &bus_attr_uevent);
    if (retval)
    goto bus_uevent_fail;

    bus_create_file:这是一个功能函数,专门用来在 /sys/bus/[总线名]/ 目录下创建一个属性文件。

    &bus_attr_uevent:这是要创建的内容。它定义了这个文件的名字叫 uevent,以及当你 cat 或者 echo 这个文件时,内核应该调用哪个函数。

    uevent 文件就是手动触发通知的开关。
    场景:有时候用户空间的 udev 漏掉了一个消息。
    操作:可以在 shell 里输入 echo add > /sys/bus/pci/uevent。
    原理:往这个文件写了 “add”。

    内核定义的事件动作有一套固定的“词库”,主要包括:

    指令 (Action)对应场景用户空间 udev 的典型反应
    add 发现新硬件 / 重新枚举 创建设备节点、加载 .ko 驱动
    remove 硬件拔出 / 驱动卸载 删除设备节点、清理缓存
    change 属性变化(电量、容量) 更新图标、发送系统通知
    online 逻辑激活(如 CPU 核心) 开始分配任务给该资源
    unbind 驱动与设备脱离 标记设备为“无驱动”状态

    uevent文件就是手动触发这些动作指令的载体

    当你向 uevent 文件写入 add 时,你实际上是在"伪造"一个现场。你告诉内核:“请假装这个总线刚刚才被添加到系统中,并把这个消息广播出去。”

    应用场景:
    ①如果在 udev 进程启动之前,总线就已经注册完了,udev 就会错过那个原始的广播。通过手动 echo add,可以给 udev “补课”,让它重新处理一遍。
    ②规则调试:如果你刚刚写了一条新的 udev 规则,想测试它灵不灵,不用重启电脑,直接 echo add 触发一次伪事件即可。
    ③驱动重新匹配:有时候驱动安装晚了,没赶上设备注册,手动 add 一下可以诱导系统重新尝试匹配。

    内核顺着 bus_attr_uevent 绑定的回调函数,重新发射一次该总线的 add 事件。
    udev 再次收到消息,重新去扫描驱动。

    1.1.9 kset_create_and_add

    priv->devices_kset = kset_create_and_add("devices", NULL,
    &priv->subsys.kobj);
    if (!priv->devices_kset) {
    retval = ENOMEM;
    goto bus_devices_fail;
    }

    devices:这是要在总线下创建的子目录名字。
    NULL:这里本可以填 uevent_ops,但填 NULL 意味着这个子目录直接继承它父节点(也就是该总线)的动作准则。
    &priv->subsys.kobj:指向了总线的 kobject,所以新创建的 devices 目录会出现在 /sys/bus/pci/ 里面,而不是在根目录下。

    作用:在kobject的目录下再划分成不同的文件夹,划分层级,形成合理的组织形式,例如device和driver。方便之后的操作与查看。

    1.1.10 初始化链表

    INIT_LIST_HEAD(&priv->interfaces);

    动作:初始化一个空的双向链表头。
    含义:interface(接口)是总线的一种“插件”机制。
    作用:有些功能(比如特定的电源管理逻辑)可能并不属于某个具体的设备或驱动,而是横跨整个总线的。内核通过这个链表来维护这些扩展功能。
    比喻:给办公室留一个插线板,方便以后接入各种外接设备(插件)。

    __mutex_init(&priv->mutex, "subsys mutex", key);

    动作:初始化一个互斥锁(Mutex)。
    含义:内核是一个多任务环境,可能有多个线程同时在往这个总线上插设备或卸载驱动。
    作用:确保同一时间只有一个操作能修改总线的数据结构。

    klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);

    动作:初始化总线的设备花名册。
    特殊点:它带了 get 和 put 函数。
    含义:klist 是内核的一种高级链表,它在遍历时会自动增加对象的引用计数。
    作用:防止你正在查看某个设备信息时,这个设备突然被别人拔掉了。get/put 保证了只要它还在花名册上,它的内存就不会被释放。

    klist_init(&priv->klist_drivers, NULL, NULL);

    动作:初始化总线的驱动花名册。
    含义:这里没有传 get/put,因为驱动(.ko 模块)通常比设备要稳定得多,由模块管理机制另行保护。
    作用:记录当前有哪些驱动程序已经注册到了这根总线上。

    1.1.11 add_probe_files

    retval = add_probe_files(bus);
    if (retval)
    goto bus_probe_files_fail;

    在 /sys/bus/[总线名]/ 目录下创建两个非常重要的属性文件:drivers_probe 和 drivers_autoprobe

    drivers_autoprobe(自动匹配)
    默认值:通常是 1。
    作用:决定了当有新硬件插入时,总线是否自动搜寻驱动。
    手动干预:如果执行 echo 0 > drivers_autoprobe,插任何设备,内核都会视而不见,直到手动干预。

    drivers_probe(手动匹配)
    作用:当你有一个特定的设备一直匹配不到驱动时,你可以把设备的名字写入这个文件。
    操作:echo “0000:01:00.0” > /sys/bus/pci/drivers_probe。
    后果:内核会强行触发一次探测逻辑,尝试为这个特定设备寻找合适的驱动。

    1.1.12 bus_add_groups

    retval = bus_add_groups(bus, bus->bus_groups);
    if (retval)
    goto bus_groups_fail;

    bus_add_groups是一个批量创建文件的函数

    bus_groups 是一个指向 struct attribute_group 指针数组的指针。
    总线添加一个叫 my_custom_feature 的功能,需要准备三样东西:
    读函数 (Show):当用户 cat 这个文件时,内核执行的代码。
    写函数 (Store):当用户 echo 这个文件时,内核执行的代码。
    属性定义:把函数包装成一个 attribute 对象,并起个名字。

    举例:在 Linux 总线开发中,如果你想在 /sys/bus/my_bus/ 目录下创建一个名为 version 的文件

    第一步:编写动作函数 (Show/Store)

    struct bus_attribute {
    struct attribute attr;
    ssize_t (*show)(struct bus_type *, char *); // <— 读操作
    ssize_t (*store)(struct bus_type *, const char *, size_t); // <— 写操作
    };

    首先要定义当用户读取文件时,内核应该返回什么。

    // 读函数:用户 cat /sys/bus/my_bus/version 时触发
    static ssize_t version_show(struct bus_type *bus, char *buf) {
    return sprintf(buf, "v1.0.0-alpha\\n"); // 将内容打印到缓冲区返回给用户
    }

    第二步:实例化属性 (Attribute)
    使用内核宏将函数包装成一个具体的“属性对象”。

    // 自动创建名为 bus_attr_version 的结构体对象
    // RO 表示 Read Only (只读),它会自动关联到 version_show 函数
    static BUS_ATTR_RO(version);

    宏BUS_ATTR_RO在预编译阶段会做三件事:
    自动起名:它会把 version 拼接成 bus_attr_version。
    强制关联:它会自动寻找名为 version_show 的函数,并将其赋值给 .show 指针。
    权限锁定:RO 代表 Read Only,它会把文件权限设为 0444(只读),且不分配 .store 指针。

    所以在第一步定义的函数 version_show 中,version 代表了属性文件的名称,而 show 后缀则遵循了内核的命名规范,代表该函数负责‘读’操作。
    关键在于第二步使用的宏 BUS_ATTR_RO(version):它在预编译阶段自动寻找名为 version_show 的函数,并将其函数地址赋值给 struct bus_attribute 中的 .show 指针。正是通过这个宏的‘牵线搭桥’,当用户访问 sysfs 文件时,内核才能精准地跳转并执行你定义的 version_show 函数。

    属性结构体针对的对象show 函数的参数宏命令
    struct bus_attribute 总线 (struct bus_type *bus, …) BUS_ATTR_RO
    struct device_attribute 设备 (struct device *dev, …) DEVICE_ATTR_RO
    struct driver_attribute 驱动 (struct device_driver *drv, …) DRIVER_ATTR_RO

    BUS_ATTR_RO 的自动化操作:
    BUS_ATTR_RO(name) 不只是定义了一个名字,实际上它完成了从逻辑到属性的闭环:
    自动关联:根据 name 自动寻找并绑定 name_show 回调函数。
    自动限权:通过 RO 标识位,强制将 sysfs 文件设为只读。
    自动实例化:宏会构造出一个名为 bus_attr_name 的 struct bus_attribute 实例。这个实例包含了文件名、权限和回调函数。
    【最终执行逻辑】:
    当用户在终端执行 cat /sys/bus/xxx/name 时,sysfs 框架会通过这个自动生成的实例找到绑定的 name_show 回调并执行,从而将内核数据呈现给用户。

    实战案例:从 Shell 命令到内核函数
    假设我们在 my_bus 总线下定义了一个属性 abc:
    定义阶段:通过 BUS_ATTR_RW(abc) 宏,内核会自动将 abc_show 和 abc_store 两个函数包装成一个属性,即可读可写,
    对于读操作:宏通过 .show 接口将 abc_show 函数与 cat 命令挂钩。用户读取时,内核通过该函数将数据“泵”向用户空间。
    对于写操作:宏通过 .store 接口将 abc_store 函数与 echo 命令挂钩。用户写入时,内核将字符串作为参数“喂”给该函数。
    生成阶段:完成 bus_add_groups 后,系统会自动创建文件路径:/sys/bus/my_bus/abc。
    读操作 (cat):
    执行命令:cat /sys/bus/my_bus/abc
    内核动作:自动调用 abc_show,并将函数返回的字符串显示在屏幕上。
    写操作 (echo):
    执行命令:echo “efg” > /sys/bus/my_bus/abc
    内核动作:自动调用 abc_store,并将用户输入的 “efg” 作为参数传递给函数,供内核解析。
    (当执行 echo “efg” > /sys/bus/my_bus/abc 时,字符串 “efg” 会被内核存放在 abc_store 函数的 buf 参数里)

    第三步:集合成数组 (Attribute Array)
    一个功能组可能包含多个文件,将它们统一放进一个指针数组中。

    static struct attribute *my_bus_attrs[] = {
    &bus_attr_version.attr, // 将 version 属性加入数组
    // … 这里可以加入更多属性 (如 rescan 等)
    NULL, // 必须以 NULL 结尾,作为遍历结束的标志
    };

    第四步:封装成组 (Attribute Group)
    将数组包装进 attribute_group 结构体中。这是内核 sysfs 操作的基本单位。

    static const struct attribute_group my_bus_group = {
    .attrs = my_bus_attrs, // 关联属性数组
    // .name = "stats", // (可选) 如果加上这个,文件会出现在 /sys/bus/my_bus/stats/ 目录下
    };

    第五步:形成最终清单 (Groups Array)
    由于一个总线可能有多种不同性质的功能(如基础功能、电源管理、调试接口),因此内核要求提交一个“组的数组”。

    const struct attribute_group *my_bus_groups[] = {
    &my_bus_group, // 加入我们刚刚定义的组
    NULL, // 同样以 NULL 结尾
    };
    // 最后将清单交给总线
    // my_bus.bus_groups = my_bus_groups;

    ①清单化 (Listing)
    my_bus_attrs[]
    把分散在各处的属性(文件)收拢起来,排好队,放进一个数组里。这解决了“有什么”的问题。
    ②结构化 (Structuring)
    my_bus_group
    把数组包装进 struct attribute_group。
    内核可读,内核的 sysfs 函数(如 sysfs_create_group)最喜欢的输入参数就是这个结构体。
    附加功能:这一层不仅是包装,它还可以给这组文件起个“共同的名字”。如果你在 group 里设置了 .name = “statistics”,那么这些文件就会出现在 /sys/bus/my_bus/statistics/ 目录下,而不是直接在根目录。
    ③层级化与扩展性 (Hierarchy)
    my_bus_groups[]
    如果不嵌套,第二步其实已经够用了。
    多模块管理:比如 PCI 总线,它有“核心属性组”、“电源管理属性组”、“热插拔属性组”。
    灵活组合:通过这种“指针数组”的形式,内核可以非常方便地用一个 while 循环遍历所有的 group,直到遇到 NULL。

    1.1.13 注册失败销毁

    pr_debug(“bus: ‘%s’: registered\\n”, bus->name);
    return 0;

    只有当所有的注册步骤(直到最后的 bus_add_groups)都返回 0(代表成功)时,代码才会运行到这里。

    动作:打印一条调试信息,告诉开发者总线注册成功。

    直接返回:执行 return 0。这意味着后面所有的 fail 标签代码都不会被执行。CPU 会直接跳出函数,去处理别的事情。

    如果中间任何一个步骤失败(比如 retval 变成了非 0 值),代码就会通过 goto 强行降落到对应的 Label(标签)。一旦 goto 到了某个标签,它会从那个点开始,一直向下执行,直到函数末尾(倒序删除前面的操作)。

    bus_groups_fail:
    remove_probe_files(bus);
    bus_probe_files_fail:
    kset_unregister(bus->p->drivers_kset);
    bus_drivers_fail:
    kset_unregister(bus->p->devices_kset);
    bus_devices_fail:
    bus_remove_file(bus, &bus_attr_uevent);
    bus_uevent_fail:
    kset_unregister(&bus->p->subsys);
    out:
    kfree(bus->p);
    bus->p = NULL;
    return retval;
    }

    1.1.14 kobject的重要性

    kobject其实是要创建的总线的根目录,其他子目录都是以此为基础创建
    kobject:sysfs 里的“锚点”
    在内核里,在 /sys 目录下的每一个文件夹,它的背后一定对应着一个 kobject。

    文件夹是 kobject,那文件夹里面的文件(比如 uevent, version, rescan)呢?
    这些文件不是 kobject。它们是 attribute(属性)。
    比喻:kobject 是容器(文件夹),而 attribute 是容器里的内容(文件)。

    总线的根目录:当你注册 PCI 总线时,/sys/bus/pci/ 这个文件夹其实就是 bus->p->subsys.kobj 在文件系统里的映射。

    它是“父亲”:后面创建的 devices 和 drivers 目录,在创建时都必须明确指定这个 kobject 作为它们的 parent

    为什么内核要让“文件夹 == kobject”?

    可视化:内核对象是看不见摸不着的内存块。把它变成文件夹,你就能用 ls 看到它,用 cd 进入它。
    交互化:文件夹里的文件(属性)给了你用 cat 和 echo 操控内核的机会。
    层次化:文件夹的嵌套完美地反映了内核硬件、总线、驱动之间的父子层级关系。

    在 /sys 的世界里,kobject 就是文件夹的“灵魂”,文件夹就是 kobject 的“肉身”。

    逻辑结构

    /sys (根节点):整个内核世界的起点。

    kobject (骨架):建立父子层级(parent 指针),决定了 /sys/bus/pci/devices 这种长长的路径。

    kset (容器):特殊的 kobject,不仅自己是文件夹,还负责把同类的 kobject(比如所有的 PCI 设备)像“文件夹里的文件夹”一样拎在一起。

    attribute (叶子):最终落脚点,是用户唯一能读写的数据单元。

    赞(0)
    未经允许不得转载:171主机测评 » 基于Linux-4.9.88的SPI子系统研究(1)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址