1. 内核层引言
在上一篇文章中,我们从系统架构维度梳理了 Linux SPI 子系统的框架图,对整体的数据流向与分层机制建立了宏观认知。站在架构基础上,对研究路线进行了梳理:
匹配(总线) → \rightarrow → 传输(标准化API) → \rightarrow → 解耦(接口/主控层) → \rightarrow → 物理控制(硬件) → \rightarrow → 用户交互
内核层(抽象层 -> 接口/主控层 -> 打通):这是最难也最核心的部分。通过 spi_register_controller 和 spi_register_driver 将这三者打通,掌握 Linux 驱动模型的解耦哲学(即控制器驱动只管传输,设备驱动只管协议,核心层负责撮合)。
硬件层(内核控制硬件):这是验证你理解的环节。研究 spi_transfer_one_message 或 transfer_one 回调函数,看看数据是如何从内核缓冲区流向硬件寄存器的,这会让你对"SPI 传输"不再有疑惑。
用户层(顶层控制):这是终点。研究 spidev.c 字符驱动,看用户空间的 ioctl 是如何通过抽象层到达控制器驱动的。
本篇将深入代码内核层,拆解 SPI 子系统各模块的执行逻辑。
驱动的开发往往遵循'总线---设备---驱动'的模型,而一切的起点,在于如何在内核中确立 SPI 的专属协议总线。今天,我们从 SPI 子系统的初始化入口开始。"
在 /drivers/spi/spi.c 中,我们找到创建总线的代码,搜索 'bus_register' 并且在其之后执行 dump_stack 跟踪其调用流程。
c
status = bus_register(&spi_bus_type);
pr_info("KD_LOG: test the function: dump_stack");
pr_info("KD_LOG: here is dump_stack");
dump_stack();
2. 总线创建流程(抽象层)
启动开发板输入:
dmesg | grep "KD_LOG: test*" -A 20
截取 bus_register 相关的部分:
[ 0.308448] KD_LOG: test the function: dump_stack
[ 0.308467] KD_LOG: here is dump_stack
[ 0.308500] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 4.9.88 #4
[ 0.308512] Hardware name: Freescale i.MX6 UltraLite (Device Tree)
[ 0.308578] [<80112a34>] (unwind_backtrace) from [<8010dc2c>] (show_stack+0x20/0x24)
[ 0.308616] [<8010dc2c>] (show_stack) from [<80469964>] (dump_stack+0x80/0x94)
[ 0.308653] [<80469964>] (dump_stack) from [<8115edd8>] (spi_init+0x70/0xc4)
[ 0.308686] [<8115edd8>] (spi_init) from [<80101cc4>] (do_one_initcall+0x54/0x180)
[ 0.308721] [<80101cc4>] (do_one_initcall) from [<81100e9c>] (kernel_init_freeable+0x16c/0x204)
[ 0.308759] [<81100e9c>] (kernel_init_freeable) from [<80b7e868>] (kernel_init+0x18/0x120)
[ 0.308796] [<80b7e868>] (kernel_init) from [<80109350>] (ret_from_fork+0x14/0x24)
通过这部分打印信息,我们可以分析得到 SPI 注册总线的调用链,通过倒序调用分析可以得到如下关系:
ret_from_fork → kernel_init → kernel_init_freeable → do_one_initcall → spi_init
通过 dump_stack() 的输出,我们清晰地看到整个 SPI 子系统的注册逻辑起始于 spi_init,深入 spi_init 函数内部进行分析。
在 /drivers/spi/spi.c 搜索 ' spi_init ' ,在文件末尾可以发现如下代码:
c
postcore_initcall(spi_init);
postcore_initcall 是 Linux 内核中用于控制模块初始化顺序的宏。它是内核初始化机制(Initcall 机制)的一部分。
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);
pr_info("KD_LOG: test the function: dump_stack");
pr_info("KD_LOG: here is dump_stack");
dump_stack();
pr_info("KD_LOG: here is fstrace");
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 内容的执行过程:
spi_init (SPI 子系统初始化入口)
|
|__ 内存准备: kmalloc (为核心层分配公共传输缓冲区)
|
|__ 构建总线骨架: bus_register (注册 SPI 总线,定义匹配与管理规则)
|
|__ 建立设备类: class_register (在 /sys/class 下创建节点,实现控制器可视化)
既然注册了总线,那么我们来看一下总线类型都定义了些什么内容:
c
struct bus_type spi_bus_type = {
.name = "spi",
.dev_groups = spi_dev_groups,
.match = spi_match_device,
.uevent = spi_uevent,
};
2.1 .name = "spi"
当内核启动并加载 SPI 核心驱动后,会看到这个目录 /sys/bus/spi/。所有的 SPI 设备和驱动都会挂载在这个目录下。可以通过 ls /sys/bus/spi/devices 和 ls /sys/bus/spi/drivers 验证这一点。
[root@100ask:/sys/bus]# ls
clockevents event_source iio platform serio usb-serial
clocksource gpio mdio_bus rpmsg soc virtio
container hid mmc scsi spi workqueue
cpu i2c nvmem sdio usb
2.2 .dev_groups = spi_dev_groups
它定义了在 /sys/bus/spi/devices/xxx/ 下会显示哪些属性文件(如 modalias、bus 等)。这些属性文件是用户空间调试驱动的"窗口"。
2.3 .match = spi_match_device
这是 bus_register 之后,内核最频繁调用的函数。每当有新的驱动被加载(spi_register_driver)或新的设备被发现(spi_add_device)时,总线就会调用这个函数来寻找是否有对应的设备/驱动相匹配。
接下来分析这个匹配过程:
c
static int spi_match_device(struct device *dev, struct device_driver *drv)
{
const struct spi_device *spi = to_spi_device(dev);
const struct spi_driver *sdrv = to_spi_driver(drv);
/* Attempt an OF style match */
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI */
if (acpi_driver_match_device(dev, drv))
return 1;
if (sdrv->id_table)
return !!spi_match_id(sdrv->id_table, spi);
return strcmp(spi->modalias, drv->name) == 0;
}
2.3.1 通用类型到专有类型的转化
我们先观察这个函数传进来的参数,和内部的前两行代码。
参数为: struct device *dev 与 struct device_driver *drv
前两行代码:to_spi_device(dev) 与 to_spi_driver(drv)
我们可以得知,这个函数先将传递进来的通用设备和驱动类型转化为专用的SPI类型
c
static inline struct spi_device *to_spi_device(struct device *dev)
{
return dev ? container_of(dev, struct spi_device, dev) : NULL;
}
这行代码使用了 C 语言的三元运算符
条件 ? 结果为真 : 结果为假
条件 dev ?:检查传入的指针 dev 是否为 NULL。这是内核开发中的"防御性编程",防止空指针解引用导致内核崩溃(Panic)。
结果为真 container_of(...):如果 dev 有效,执行核心转换宏 container_of。
结果为假 : NULL:如果 dev 本身就是空,直接返回 NULL。
所以现在我们要分析 container_of 具体做了什么事情,返回的值是什么。
以下是 struct spi_device 的内存布局:
c
struct spi_device {
struct device dev;
struct spi_master *master;
u32 max_speed_hz;
u8 chip_select;
u8 bits_per_word;
u16 mode;
...
...
...
...
};
我们发现通用的设备类型包含在SPI设备类型之中,且是SPI设备类型的第一个成员
内存地址的重叠与语义的叠加
内存是"死的":
内存里仅仅是一块连续的字节序列。
结构体是"活的":
结构体定义告诉编译器如何去"解读"这块内存。
当你把 struct device 放在 struct spi_device 的第一个成员位置时,这块内存的起始地址(比如 0x1000)既是 struct device 的开头,也是 struct spi_device 的开头。
属性的叠加:对于 CPU 来说,它看到的是 0x1000 开始的字节流。但对于 C 编译器来说:
如果你用 struct device * 指针指向它,你只能看到 dev 的属性(比如 parent,kobj)。
如果你用 struct spi_device * 指针指向它,你不仅能看到 dev 的属性,还能顺着往下看,看到 chip_select 等 SPI 特有属性。
结论:这块地址本身并没有变,变的是你"看它的视角"。
接下来分析 container_of(dev, struct spi_device, dev) :
你可能会疑惑,既然地址是一样的,为什么还要用 container_of ?这是一个极其深刻的问题。
情况 A:地址是一样的(当 device 是第一个成员时)
如果 struct device 是 struct spi_device 的第一个成员,偏移量为 0,那么 container_of 做的减法运算其实就是 地址 - 0。在这种情况下,container_of 看起来确实有点多余,直接强制转换 (struct spi_device *)dev 也能达到目的。
情况 B:地址是不一样的(核心价值所在)这是 container_of 真正存在的意义。在 Linux 内核中,一个结构体里可能会内嵌多个不同类型的 device 或其他基类成员,或者 device 不在第一个位置。
例如,假设你的代码逻辑变了:
cstruct my_spi_controller { int id; struct device dev; // 此时 dev 不在第一个位置,偏移量不是 0 };如果你只有 struct device *dev 这个指针,你如何找回 my_spi_controller 的起始地址?
你不能简单地做强制转换,因为 dev 的起始地址比结构体的起始地址大 sizeof(int) 个字节。
你必须执行:
地址 - 偏移量
container_of 的最终使命:
它保证了无论你的基类成员在结构体的哪个位置,只要你知道它是"哪种类型"里的"哪个成员",它就能帮你算出这个结构体真正的、最原始的起始地址。
2.3.2 匹配规则
从 spi_match_device 可知,匹配的优先级如下:
- OF 匹配(设备树匹配)
- ACPI 匹配
- ID 表 匹配
- 名字 匹配 (strcmp)
这里主要介绍第一优先级的匹配流程:
c
if (of_driver_match_device(dev, drv))
return 1;
进入of_driver_match_device
c
static inline int of_driver_match_device(struct device *dev,
const struct device_driver *drv)
{
return of_match_device(drv->of_match_table, dev) != NULL;
}
进入of_match_device
c
/**
* of_match_device - 判断设备是否与驱动的支持列表匹配
* @matches: 指向驱动程序定义的 of_device_id 数组(支持列表)
* @dev: 指向正在寻找驱动的设备结构体(硬件实体)
*/
const struct of_device_id *of_match_device(const struct of_device_id *matches,
const struct device *dev)
{
/* * 第一步:安全性与前提检查
* 1. !matches: 如果驱动没有定义 of_match_table,则无法进行设备树匹配。
* 2. !dev->of_node: 如果设备不是从设备树中解析出来的(不含 DTS 节点信息),
* 则无法通过设备树路径进行匹配。
*/
if ((!matches) || (!dev->of_node))
return NULL;
/* * 第二步:执行核心匹配逻辑
* 剥离外部的 struct device,取出核心的 dev->of_node(设备树节点指针),
* 交给更底层的 of_match_node 函数进行详细的字符串比对。
*/
return of_match_node(matches, dev->of_node);
}
进入of_match_node
c
/**
* of_match_node - 判断一个设备树节点(device_node)是否在匹配表中有对应项
* @matches: 驱动提供的支持列表数组
* @node: 要进行匹配的设备树节点实体
*
* 这是用于设备匹配的底层工具函数。
*/
const struct of_device_id *of_match_node(const struct of_device_id *matches,
const struct device_node *node)
{
const struct of_device_id *match;
unsigned long flags;
/* 1. 锁住整个设备树,并保存当前中断状态,防止匹配时数据被篡改 */
raw_spin_lock_irqsave(&devtree_lock, flags);
/* 2. 进入真正的"核心逻辑圈",进行循环比对 */
match = __of_match_node(matches, node);
/* 3. 解锁并恢复中断 */
raw_spin_unlock_irqrestore(&devtree_lock, flags);
return match;
}
进入__of_match_node
c
/**
* __of_match_node - (内部函数) 遍历匹配表,寻找与节点最匹配的项
* @matches: 指向驱动支持列表数组的指针(即 ids 数组)
* @node: 指向设备树节点的指针(硬件信息)
* * 注意:调用此函数前必须已经获取了 devtree_lock 锁。
*/
static const struct of_device_id *__of_match_node(const struct of_device_id *matches,
const struct device_node *node)
{
const struct of_device_id *best_match = NULL;
int score, best_score = 0;
/* 基础安全检查:如果驱动根本没写匹配表,直接返回 NULL */
if (!matches)
return NULL;
/**
* 核心循环:遍历 matches 数组
* 判断条件:只要 name、type 或 compatible 任意一个不为空,说明还没到数组末尾。
* 这里的迭代(matches++)利用了 C 语言指针自增,每次向后移动一个结构体大小。
*/
for (; matches->name[0] || matches->type[0] || matches->compatible[0]; matches++) {
/* 调用底层函数,计算当前项与硬件节点的"匹配得分" */
score = __of_device_is_compatible(node, matches->compatible,
matches->type, matches->name);
/**
* 优胜劣汰逻辑:
* 如果当前这一项的匹配得分比之前记录的最高分还要高,
* 则更新"最佳匹配"指针,并记录新的最高分。
*/
if (score > best_score) {
best_match = matches;
best_score = score;
}
}
/* 返回得分最高的那一项(如果一项都没匹配上,则返回 NULL) */
return best_match;
}
- 为什么 compatible 的顺序很重要?
设备树节点通常会列出多个 compatible 字符串,例如:
compatible = "fsl,imx6q-uart", "arm,pl011";
第一个是具体型号(得分高)。
第二个是通用型号(得分低)。
__of_device_is_compatible 会根据字符串在 DTS 列表中的位置来打分。越靠前的字符串,得分越高。这个 for 循环确保了即使驱动能支持这两个,它也会因为分值竞争而选中更具体、更靠前的那个。- 为什么数组末尾一定要加 { }?
如果没有那个空的 { },这个 for 循环的判断条件 matches->compatible0 就会在内存中一直往后跑,直到触碰到非法的内存地址导致内核崩溃(Kernel Panic)。
进入__of_device_is_compatible
c
/**
* 优先级顺序(1 为最高):
* 1. 特定兼容名 && 类型 && 名称 (全中)
* 2. 特定兼容名 && 类型
* 3. 特定兼容名 && 名称
* 4. 特定兼容名 (最常用,只要型号对上就行)
* ...
* 11. 仅名称匹配 (最无奈的匹配)
*/
static int __of_device_is_compatible(const struct device_node *device,
const char *compat, const char *type, const char *name)
{
/* ... 变量定义 ... */
/* 1. 兼容性匹配(最高优先级) */
if (compat && compat[0]) {
/* 寻找硬件节点里的 "compatible" 属性 */
prop = __of_find_property(device, "compatible", NULL);
/* 遍历硬件节点里列出的所有兼容性字符串 */
for (cp = of_prop_next_string(prop, NULL); cp;
cp = of_prop_next_string(prop, cp), index++) {
/* 如果匹配成功 */
if (of_compat_cmp(cp, compat, strlen(compat)) == 0) {
/* 给出一个巨大的基础分,并根据位置 index 减去一点微小的差值 */
score = INT_MAX/2 - (index << 2);
break;
}
}
/* 如果驱动要求匹配 compat 但遍历完都没找到,直接判死刑返回 0 */
if (!score) return 0;
}
/* 2. 设备类型匹配(次优先级) */
if (type && type[0]) {
if (!device->type || of_node_cmp(type, device->type))
return 0; // 类型不符,判死刑
score += 2; // 类型对上了,加 2 分
}
/* 3. 名称匹配(最低优先级) */
if (name && name[0]) {
if (!device->name || of_node_cmp(name, device->name))
return 0; // 名称不符,判死刑
score++; // 名称对上了,加 1 分
}
return score;
}
__of_find_property
of_prop_next_string
of_compat_cmp
of_node_cmp
这几个匹配函数的本质基本都是利用宏实现strcmp或者strcasecmp
宏定义
#define of_compat_cmp(s1, s2, l) strcasecmp((s1), (s2))
内核对 compatible 字符串很宽容,忽略大小写 。这意味着你在驱动里写 "OVTI,ov5640",而设备树里写 "OVTI,OV5640",它们依然能匹配成功,为了代码可读性和专业感,业界标准做法是一律使用小写和中划线.
#define of_prop_cmp(s1, s2) strcmp((s1), (s2))
属性名(如 clock-frequency、interrupts)必须一字不差,大小写错了内核就完全认不出来。这是为了保证底层硬件描述的严谨性。
#define of_node_cmp(s1, s2) strcasecmp((s1), (s2))
例如:
c
static struct property *__of_find_property(const struct device_node *np,
const char *name, int *lenp)
{
struct property *pp;
if (!np) return NULL; // 如果节点不存在,直接返回
/*A. 初始化:pp = np->properties
动作:将指针 pp 指向该设备节点的第一个属性。
B. 循环条件:pp
动作:只要 pp 不是 NULL,循环就继续。
C. 步进:pp = pp->next
动作:将 pp 移向下一个属性。*/
for (pp = np->properties; pp; pp = pp->next) {
/* 核心:使用 strcmp 比较属性名是否一模一样 */
if (of_prop_cmp(pp->name, name) == 0) {
/* 如果找到了,且调用者关心数据长度 */
if (lenp)
*lenp = pp->length; // 把长度(如 4 字节或字符串长度)写回去
break; // 找到了就不再继续翻了
}
}
return pp; // 返回找到的"标签"指针,没找到则返回 NULL
}
举例:
1. 场景模拟:摄像头的"参数清单"
假设你的设备树(DTS)里有这样一个摄像头节点:
代码段
ov5640: camera@3c {
compatible = "ovti,ov5640"; // 属性 1
reg = <0x3c>; // 属性 2
clocks = <&clk_cam>; // 属性 3
};
当内核解析这段 DTS 后,在内存中会生成一个 struct device_node(即参数 np)。这个节点内部维护了一个单向链表,把这些属性一个个串起来。
2. 匹配过程:模拟调用 __of_find_property(np, "reg", &len)
当你调用这个函数寻找 "reg" 属性时,内核会像翻书一样:
第一步:进入函数
np 指向我们的摄像头节点。
name 是我们要找的目标 "reg"。
lenp 是一个用来接收数据长度的"空袋子"。
第二步:开始 for 循环(链表遍历)
第一次迭代:指针 pp 指向第一个属性 compatible。
执行 of_prop_cmp("compatible", "reg")。
结果:不匹配。
第二次迭代:指针 pp 移动到下一个(pp = pp->next),指向 reg。
执行 of_prop_cmp("reg", "reg")。
结果:匹配成功 (== 0)!
第三步:收尾与返回
记录长度:如果传了 lenp,就把 reg 属性的长度(这里是 4 字节)填进去:*lenp = 4。
跳出循环:执行 break。
返回指针:把包含 0x3c 这个数值的 property 结构体地址返回给调用者。
2.4 .uevent = spi_uevent
事件通知机制,当设备被添加或移除时,该函数负责向用户空间(udev/mdev)发送信号。比如当陀螺仪 ICM-20948 被识别时,内核通过这个函数告诉用户空间:"有一个新设备出现了,请为它创建设备节点 /dev/spidevX.Y。"
3. 主程序控制器创建流程(抽象层)
同样的,截取 bus_register 的 dump_stack 调用链日志进行分析:
[ 0.308927] KD_LOG: here is class_register
[ 0.308958] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 4.9.88 #5
[ 0.308972] Hardware name: Freescale i.MX6 UltraLite (Device Tree)
[ 0.309011] [<80112a34>] (unwind_backtrace) from [<8010dc2c>] (show_stack+0x20/0x24)
[ 0.309043] [<8010dc2c>] (show_stack) from [<80469964>] (dump_stack+0x80/0x94)
[ 0.309075] [<80469964>] (dump_stack) from [<8115edf4>] (spi_init+0x8c/0xc0)
[ 0.309107] [<8115edf4>] (spi_init) from [<80101cc4>] (do_one_initcall+0x54/0x180)
[ 0.309137] [<80101cc4>] (do_one_initcall) from [<81100e9c>] (kernel_init_freeable+0x16c/0x204)
[ 0.309169] [<81100e9c>] (kernel_init_freeable) from [<80b7e868>] (kernel_init+0x18/0x120)
[ 0.309201] [<80b7e868>] (kernel_init) from [<80109350>] (ret_from_fork+0x14/0x24)
发现它的调用链和 bus_register 是相同的,都是由 spi_init 调用,所以我们直接分析这个 class_register 即可。
class 是为了向用户空间抽象出统一的设备操作接口(如/sys/class/spi_master/)
class_register 核心职能解析:
- 对象归类:通过分配 subsys_private 空间,初始化该类的内部管理数据结构(账本)。
- sysfs 挂载:调用 kset_register 将该类注册为内核对象容器,使其在 /sys/class/ 下可见。
- 设备接口桥梁:它是后续 device_create 函数的基石。没有 class 注册,内核就不知道新增加的 SPI 控制器应该被分类到哪里,也就无法在 /dev/ 下生成对应的节点。
3.1 对象归类
c
struct subsys_private *cp;
cp = kzalloc(sizeof(*cp), GFP_KERNEL);
在 Linux 内核中,struct class(也就是 cls)是一个公开的结构体。它定义在头文件中,所有的驱动代码都可以访问它。如果内核把所有管理数据(比如设备链表、互斥锁、属性列表)都放在 struct class 里,结构会变得极其臃肿且难以维护,一旦内部管理数据结构改变,所有包含该头文件的驱动代码都必须重新编译。为了解决这个问题,内核设计了"私有结构体"模式:
struct class:给外界看的"名片"(包含类名、销毁回调等)。
struct subsys_private:内核内部使用的"后台账本"(隐藏在驱动开发者视野之外的复杂管理数据)
c
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;
};
-
struct subsys_private *cp; 的本质:定义内部管理上下文
这一行的作用是在函数栈上声明一个指针,用于操作该类的私有管理上下文(Private Context)。
技术层面:在 Linux 内核中,struct class 是一个公开暴露的结构体,内核倾向于将其设计为"只读"或"配置型"的属性容器。而 struct subsys_private 则包含了该类在运行时必须维护的复杂状态数据,例如:
klist_devices:该类下所有已注册设备的链表。
interfaces:该类支持的各种接口链表。
mutex:保护该类数据结构的互斥锁。
架构意义:通过将这些运行时状态变量移出 struct class,内核实现了接口与实现的分离。这确保了驱动开发者在操作 struct class 时,不会无意中破坏底层的设备管理链表。
-
cp = kzalloc(sizeof(*cp), GFP_KERNEL); 的本质:初始化内存空间
这一行的作用是动态分配并清零该类的私有数据内存区 。
动态分配:内核使用 kzalloc 从堆(Heap)中分配一块连续的物理内存。之所以选择动态分配,是因为该类在系统中存在的数量是可变的(可以在运行时注册多个类),且其生命周期由 class_register 到 class_unregister 决定。
内存清零(Zeroing):kzalloc 中的 z 代表 zero。这在内核编程中至关重要,它确保了所有未显式初始化的成员(如链表头、互斥锁指针)在起始状态下均为 NULL 或 0,避免了使用未初始化内存带来的安全风险。
上下文绑定:分配后的内存起始地址被赋值给指针 cp。这块内存区域随后将成为该类生命周期内所有管理数据的载体。
3.2 sysfs 挂载
c
error = kobject_set_name(&cp->subsys.kobj, "%s", cls->name);
...
cp->class = cls;
cls->p = cp;
kobject_set_name:设定在 /sys/class/ 下显示的文件夹名字(即 cls->name)。
双向指针绑定:cp->class = cls 与 cls->p = cp。这是实现"公有结构"与"私有结构"互访的关键,之后无论拿到 class 还是 subsys_private,都能通过指针回溯找到对方。
3.3 设备接口桥梁
c
error = kset_register(&cp->subsys);
这是将该类正式接入内核设备模型的关键函数。kset_register 会将该类的 kobject 放入内核的全局对象管理系统,并在 /sys/class/ 目录下实际创建对应的目录节点。一旦执行成功,用户空间(如 udev)就可以通过 sysfs 看到这个分类了。
3.4 kobject
c
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct kernfs_node *sd; /* sysfs directory entry */
struct kref kref;
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
struct delayed_work release;
#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;
};
如果把 Linux 内核的设备管理系统比作一座大厦,kobject 就是那块最基础的"砖头"。没有它,所有的驱动、总线、类(Class)都无法在内核中被统一管理。
对象的抽象基类:
在 C 语言中没有 class(类)的概念。内核开发者使用 kobject 来模拟面向对象中的"基类"。任何一个需要被系统统一管理的实体(如设备、驱动、总线、类),内部都会包含一个 kobject。
类比:你可以把 kobject 看作所有内核对象的"身份证"。只要一个对象拥有 kobject,内核就能识别它、统计它、并在 /sys/ 目录下为它创建节点。
引用计数:
这是 kobject 最核心的功能。内核中很多对象(如一个设备)被多个地方引用(例如总线引用它、驱动引用它、用户空间进程也在使用它)。
职能:kobject 负责记录引用计数。当计数归零时,kobject 会自动触发清理函数,把这块内存释放掉。这完美解决了内核中极其复杂的内存泄漏问题。
Sysfs 的映射点:
你刚才看到的 /sys/bus/、/sys/class/ 等目录,其背后的每一个条目,本质上都是一个 kobject 对应的结果。
职能:当内核注册一个 kobject 时,它会负责在 sysfs 对应的虚拟文件系统中创建一个目录,从而实现"内核对象在用户空间的透明可视化"。
4. 标准化 API(抽象层)
在前面两章中,我们介绍了抽象层中的总线匹配,明白了内核是如何注册总线并且创建主程序控制器的,接下来我们要研究标准化API:
核心职能:
标准化 API 的存在意义在于解耦。驱动开发者不需要知道底层是 GPIO 模拟的 SPI 还是高速的 DMA 控制器,只需要调用这些函数即可。
-
数据封装:将驱动程序提供的 struct spi_transfer(单次传输描述)封装进 struct spi_message(完整的消息对象)。
-
队列化调度:将生成的 spi_message 挂入控制器的传输队列。
-
同步与异步的交付:
spi_sync (同步):阻塞调用。驱动必须等待传输彻底完成(调用返回即代表数据已发送/接收完毕)。
spi_async (异步):非阻塞调用。驱动发出请求后立即返回,底层传输完成后通过回调函数通知驱动。
参考笔者的这篇文章 数据传输
5. 传输序列化 (抽象层)
在 Linux SPI 中,pump(泵)的意思是"推动任务流转"。
它的职责是:不管驱动往队列里塞了多少个消息,它都负责按顺序取出一个、丢给硬件驱动执行,等执行完了,再接着取下一个。
这种设计将复杂的"异步并发"变成了"线性的串行执行",极大地简化了底层硬件驱动的编写难度(底层驱动不需要处理复杂的重入问题)。
参考笔者的这篇文章 数据传输链路